├── .babelrc
├── .gitignore
├── .npmignore
├── LICENSE.md
├── README.md
├── app
├── assets
│ └── font
│ │ ├── ProximaNovaBold.woff
│ │ └── ProximaNovaRegular.woff
├── iframe.html
├── index.html
└── vendor
│ ├── three.js
│ └── three.min.js
├── blogpost
├── comp.jpg
├── cylinder.png
├── demo1.jpg
├── demo2.jpg
├── final.jpg
├── frenet1.jpg
├── frenet2.jpg
├── loop1.gif
├── loop2.gif
├── p1.jpg
├── p2.jpg
├── p3.jpg
├── parametric.png
├── post.md
├── shader.png
└── xmas.jpg
├── index.js
├── lib
├── components
│ └── createTubes.js
├── createApp.js
├── geom
│ └── createTubeGeometry.js
├── shaders
│ ├── tube.frag
│ └── tube.vert
└── util
│ ├── isMobile.js
│ ├── query.js
│ └── random.js
└── package.json
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | presets: [ "es2015" ]
3 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | bower_components
2 | node_modules
3 | *.log
4 | .DS_Store
5 | bundle.js
6 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | bower_components
2 | node_modules
3 | *.log
4 | .DS_Store
5 | bundle.js
6 | test
7 | test.js
8 | demo/
9 | .npmignore
10 | LICENSE.md
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | Copyright (c) 2016 Matt DesLauriers
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all
12 | copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
20 | OR OTHER DEALINGS IN THE SOFTWARE.
21 |
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # parametric-curves
2 |
3 | Fast animated curve rendering on the GPU with parametric equations.
4 |
5 |
6 |
7 | The vertex shader supports local Frenet-Serret frames (approximations) and more robust Parallel Transport frames (at expense of performance).
8 |
9 | #### Live Demo
10 |
11 | http://parametric-curves.surge.sh/
12 |
13 | #### Blog Post
14 |
15 | https://mattdesl.svbtle.com/shaping-curves-with-parametric-equations
16 |
17 | #### Running Locally
18 |
19 | The demo can be run locally with:
20 |
21 | ```sh
22 | git clone https://github.com/mattdesl/parametric-curves.git
23 | cd parametric-curves
24 | npm install
25 | npm run start
26 | ```
27 |
28 | Then open `localhost:9966`.
29 |
30 | ## License
31 |
32 | MIT, see [LICENSE.md](http://github.com/mattdesl/parametric-curves/blob/master/LICENSE.md) for details.
33 |
--------------------------------------------------------------------------------
/app/assets/font/ProximaNovaBold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattdesl/parametric-curves/deec8372ef141af367833af9375966856a0e31f0/app/assets/font/ProximaNovaBold.woff
--------------------------------------------------------------------------------
/app/assets/font/ProximaNovaRegular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattdesl/parametric-curves/deec8372ef141af367833af9375966856a0e31f0/app/assets/font/ProximaNovaRegular.woff
--------------------------------------------------------------------------------
/app/iframe.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | iFrame test
7 |
24 |
25 |
26 | (more content)
27 |
28 |
29 | (more content)
30 |
34 |
35 |
--------------------------------------------------------------------------------
/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | parametric-curves
7 |
65 |
66 |
67 |
68 | hover + tap to interact
69 |
70 |
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/blogpost/comp.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattdesl/parametric-curves/deec8372ef141af367833af9375966856a0e31f0/blogpost/comp.jpg
--------------------------------------------------------------------------------
/blogpost/cylinder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattdesl/parametric-curves/deec8372ef141af367833af9375966856a0e31f0/blogpost/cylinder.png
--------------------------------------------------------------------------------
/blogpost/demo1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattdesl/parametric-curves/deec8372ef141af367833af9375966856a0e31f0/blogpost/demo1.jpg
--------------------------------------------------------------------------------
/blogpost/demo2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattdesl/parametric-curves/deec8372ef141af367833af9375966856a0e31f0/blogpost/demo2.jpg
--------------------------------------------------------------------------------
/blogpost/final.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattdesl/parametric-curves/deec8372ef141af367833af9375966856a0e31f0/blogpost/final.jpg
--------------------------------------------------------------------------------
/blogpost/frenet1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattdesl/parametric-curves/deec8372ef141af367833af9375966856a0e31f0/blogpost/frenet1.jpg
--------------------------------------------------------------------------------
/blogpost/frenet2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattdesl/parametric-curves/deec8372ef141af367833af9375966856a0e31f0/blogpost/frenet2.jpg
--------------------------------------------------------------------------------
/blogpost/loop1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattdesl/parametric-curves/deec8372ef141af367833af9375966856a0e31f0/blogpost/loop1.gif
--------------------------------------------------------------------------------
/blogpost/loop2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattdesl/parametric-curves/deec8372ef141af367833af9375966856a0e31f0/blogpost/loop2.gif
--------------------------------------------------------------------------------
/blogpost/p1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattdesl/parametric-curves/deec8372ef141af367833af9375966856a0e31f0/blogpost/p1.jpg
--------------------------------------------------------------------------------
/blogpost/p2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattdesl/parametric-curves/deec8372ef141af367833af9375966856a0e31f0/blogpost/p2.jpg
--------------------------------------------------------------------------------
/blogpost/p3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattdesl/parametric-curves/deec8372ef141af367833af9375966856a0e31f0/blogpost/p3.jpg
--------------------------------------------------------------------------------
/blogpost/parametric.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattdesl/parametric-curves/deec8372ef141af367833af9375966856a0e31f0/blogpost/parametric.png
--------------------------------------------------------------------------------
/blogpost/post.md:
--------------------------------------------------------------------------------
1 | # Shaping Curves with Parametric Equations
2 |
3 | 
4 |
5 | This post explores a technique to render volumetric curves on the GPU — ideal for shapes like ribbons, tubes and rope. The curves are defined by a parametric equation in the vertex shader, allowing us to animate hundreds and even thousands of curves with minimal overhead.
6 |
7 | Parametric curves aren't a novel idea in WebGL; ThreeJS already supports something called ExtrudeGeometry. You can read about some of its implementation details [here](http://www.lab4games.net/zz85/blog/2012/04/24/spline-extrusions-tubes-and-knots-of-sorts/). This class can be used to extrude a 3D curve or path into a volumetric line, like a 3D tube. However, since the code runs on the CPU and generates a new geometry, it isn't well suited for animating the curve every frame, let alone several hundred curves.
8 |
9 | Instead, let's see what we can accomplish with just a vertex shader. The technique presented here has various downsides and isn't very robust, but it can look great in certain cases and tends to be fast to compute. At the end of this post, we'll end up with something like the WebGL scene below — use your mouse on desktop or tap on mobile to interact with it.
10 |
11 |
12 |
13 | I used this technique for the swirling 3D lines in my [Christmas Experiment](https://christmasexperiments.com/2016/19/sugar/) this year. The same experiment uses parametric equations for the bouncing surface — so the concepts here will carry over nicely to other areas.
14 |
15 | 
16 |
17 | I'm also using volumetric lines for a "neon tube" effect in an upcoming demo; you can see some screenshots here:
18 |
19 | 
20 |
21 | # Source Code
22 |
23 | You can follow along with the source code for the interactive demo here:
24 |
25 | https://github.com/mattdesl/parametric-curves/
26 |
27 | # Building the Tube Geometry
28 |
29 | For this to work, we're going to re-purpose a `THREE.CylinderGeometry` so that our UVs, end caps and faces all line up like a regular cylinder geometry. If it goes to plan, we should be able to render a simple cylinder with our volumetric curve code.
30 |
31 | 
32 |
33 | However, our geometry won't work with the built-in ThreeJS shaders because it's composed of the following custom vertex attributes:
34 |
35 | - `position` a *one dimensional* float along the X axis in the range -0.5 to 0.5, telling us the distance of the vertex along the curve
36 | - `angle` a float in radians in the range -π to π, telling us how far *around* the tube this vertex is
37 |
38 | When building the geometry, we also have to decide how much to subdivide our curve (i.e. length segments) and how many sides our tube should have (i.e. radial segments).
39 |
40 | Some pseudocode:
41 |
42 | ```js
43 | const temp = new THREE.Vector2();
44 |
45 | const baseGeometry = new THREE.CylinderGeometry(
46 | 1, 1, 1, // radius and length
47 | numSides,
48 | subdivisions
49 | );
50 |
51 | // will be used as vertex attributes
52 | const angles = [];
53 | const positions = [];
54 |
55 | // get vertex attributes
56 | for each face in baseGeometry:
57 | for each vertex in face:
58 | // normalize the 2D position
59 | temp.set(vertex.y, vertex.z).normalize();
60 |
61 | // find the radial angle around the tube
62 | const angle = Math.atan2(temp.y, temp.x);
63 | angles.push(angle);
64 |
65 | // copy the X position
66 | positions.push(vertex.x);
67 | ```
68 |
69 | See [createTubeGeometry.js](https://github.com/mattdesl/parametric-curves/blob/master/lib/geom/createTubeGeometry.js) for the final source. The full function also copies the `uv` attribute so that we can texture the tube if desired (e.g. adding normal mapping). We can refine the geometry later with the `subdivisions` and `numSides` parameters.
70 |
71 | With this utility function, we can create a new ThreeJS geometry:
72 |
73 | ```js
74 | const createTubeGeometry = require('../util/createTubeGeometry');
75 |
76 | // tweak these to your liking
77 | const numSides = 8;
78 | const subdivisions = 50;
79 |
80 | // create the ThreeJS Geometry
81 | const geometry = createTubeGeometry(numSides, subdivisions);
82 | ```
83 |
84 | # Setting up the Shader
85 |
86 | The next step is to create a material that encapsulates our snazzy vertex shader. We'll be using [glslify](https://github.com/stackgl/glslify) here to pre-process our shaders and make our lives a bit easier.
87 |
88 | ```js
89 | const glslify = require('glslify');
90 |
91 | const vert = glslify(__dirname + '/../shaders/tube.vert');
92 | const frag = glslify(__dirname + '/../shaders/tube.frag');
93 | const material = new THREE.RawShaderMaterial({
94 | vertexShader: vert,
95 | fragmentShader: frag,
96 | side: THREE.FrontSide,
97 | extensions: {
98 | deriviatives: true
99 | },
100 | defines: {
101 | lengthSegments: subdivisions.toFixed(1),
102 | FLAT_SHADED: false
103 | },
104 | uniforms: {
105 | thickness: { type: 'f', value: 1 },
106 | time: { type: 'f', value: 0 },
107 | radialSegments: { type: 'f', value: numSides }
108 | }
109 | });
110 | ```
111 |
112 | Notice the `extensions` and `defines` fields; these will later be used for flat normals. `FLAT_SHADED` should be false if you plan to have a smooth tube-like geometry, or set to true if you are looking for something like a triangular prism or twisted rectangle.
113 |
114 | The shader will also need to know the `subdivisions` we used in `createTubeGeometry`, which is passed as a `#define` constant so we can use it in loop expressions.
115 |
116 | Before we move onto the GLSL, we already have all the features we need to construct a 3D object. We can add it to our scene like so:
117 |
118 | ```js
119 | const mesh = new THREE.Mesh(geometry, material);
120 | mesh.frustumCulled = false;
121 | scene.add(mesh);
122 | ```
123 |
124 | The only gotcha is that we should disable frustum culling, since our geometry only contains a 1-dimensional `position` attribute which causes issues with ThreeJS's built-in frustum culling.
125 |
126 | # The Vertex Shader
127 |
128 | There are two key functions: the parametric function, and solving the Frenet-Serret frame.
129 |
130 | You can follow along with the complete shader here:
131 | [shaders/tube.vert](https://github.com/mattdesl/parametric-curves/blob/master/lib/shaders/tube.vert)
132 |
133 | ## The Parametric Function
134 |
135 | The parametric function is the most important one, as it allows us to manipulate the design and shape of our curves. Later, we'll explore how we can create some more interesting functions, but for now we want to just build a tube that undulates along the Y axis like a wave.
136 |
137 |
138 |
139 | > **Tip:** You can enter parametric equations into Google to see how they look!
140 |
141 | The input to this function will be *t*, the arc length of the curve normalized to `[0.0 .. 1.0]` range, where `0.0` is the start point of the curve and `1.0` is the end point. However, many equations will also work with inputs below zero and above one, which might be useful if you're altering the *t* parameter before computing the curve.
142 |
143 | The function will return the 3D position of the curve at *t* distance along it, in world units.
144 |
145 | For the *x* axis, we can use `t * 2.0 - 1.0` to get a value from -1.0 (start cap) to 1.0 (end cap). For the *y* axis, we will use `sin(t + time)` which will make the tube appear to glide slowly up and down.
146 |
147 | ```glsl
148 | vec3 sample (float t) {
149 | float x = t * 2.0 - 1.0;
150 | float y = sin(t + time);
151 | return vec3(x, y, 0.0);
152 | }
153 | ```
154 |
155 | Using this as our parametric equation will give us the following animated curve:
156 |
157 |
158 |
159 | ## Solving the Frenet-Serret Frame
160 |
161 | Once we have a `sample(t)` function, we can use it to construct the normals and position for the tube geometry at each vertex.
162 |
163 | By sampling the current and next point in the curve, we can find the **T**angent, **N**ormal and **B**inormal, also called the [Frenet-Serret Frame or TNB Frame](https://en.wikipedia.org/wiki/Frenet%E2%80%93Serret_formulas).
164 |
165 | With the computed frame, we can extrude away from the center line of the curve using the `angle` attribute we stored earlier. We multiply the extrusion by `volume`, a 2D vector which acts as the radius (or thickness) of our tube. Since it's 2D, we could "pinch" the tube to look more like a flat or oval shape.
166 |
167 | ```glsl
168 | void createTube (float t, vec2 volume, out vec3 pos, out vec3 normal) {
169 | // find next sample along curve
170 | float nextT = t + (1.0 / lengthSegments);
171 |
172 | // sample the curve in two places
173 | vec3 cur = sample(t);
174 | vec3 next = sample(nextT);
175 |
176 | // compute the Frenet-Serret frame
177 | vec3 T = normalize(next - cur);
178 | vec3 B = normalize(cross(T, next + cur));
179 | vec3 N = -normalize(cross(B, T));
180 |
181 | // extrude outward to create a tube
182 | float tubeAngle = angle;
183 | float circX = cos(tubeAngle);
184 | float circY = sin(tubeAngle);
185 |
186 | // compute position and normal
187 | normal.xyz = normalize(B * circX + N * circY);
188 | pos.xyz = cur + B * volume.x * circX + N * volume.y * circY;
189 | }
190 | ```
191 |
192 | # The Fragment Shader
193 |
194 | The fragment shader is fairly basic: it decides whether to use the smooth normal we computed above, or whether to approximate a flat normal using [glsl-face-normal](https://github.com/stackgl/glsl-face-normal).
195 |
196 | The "shading" is just rendering the Y-normal in a 0 to 1 range to give the tube some depth.
197 |
198 | ```glsl
199 | #extension GL_OES_standard_derivatives : enable
200 | precision highp float;
201 |
202 | varying vec3 vNormal;
203 | varying vec2 vUv;
204 | varying vec3 vViewPosition;
205 |
206 | #pragma glslify: faceNormal = require('glsl-face-normal');
207 |
208 | void main () {
209 | vec3 normal = vNormal;
210 | #ifdef FLAT_SHADED
211 | normal = faceNormal(vViewPosition);
212 | #endif
213 |
214 | float diffuse = normal.y * 0.5 + 0.5;
215 | gl_FragColor = vec4(vec3(diffuse), 1.0);
216 | }
217 | ```
218 |
219 | With all that in place, we get a shaded tube that can fly around in 3D space.
220 |
221 |
222 |
223 | # Designing with Math
224 |
225 | Ok! Let's kick it up a notch by changing `sample(t)`, our parametric equation.
226 |
227 | We can start with a circle, where *t* is an angle from 0 to 2π.
228 |
229 | ```glsl
230 | vec3 sample (float t) {
231 | float angle = t * 2.0 * PI;
232 | vec2 rot = vec2(cos(angle), sin(angle));
233 | return vec3(rot, 0.0);
234 | }
235 | ```
236 |
237 |
238 |
239 | ---
240 |
241 | If we give some depth to the *z* parameter, we can create a corkscrew.
242 |
243 | ```glsl
244 | vec3 sample (float t) {
245 | float angle = t * 2.0 * PI;
246 | vec2 rot = vec2(cos(angle), sin(angle));
247 | float z = t * 2.0 - 1.0;
248 | return vec3(rot, z);
249 | }
250 | ```
251 |
252 |
253 |
254 | > **Tip:** Try multiplying `angle` by a whole number to add more twists!
255 |
256 | ---
257 |
258 | We can also use 3D spherical coordinates as a base, instead of a 2D circle.
259 |
260 | ```glsl
261 | vec3 spherical (float r, float phi, float theta) {
262 | return vec3(
263 | r * cos(phi) * cos(theta),
264 | r * cos(phi) * sin(theta),
265 | r * sin(phi)
266 | );
267 | }
268 |
269 | vec3 sample (float t) {
270 | float angle = t * 2.0 * PI;
271 |
272 | float radius = 1.0;
273 | float phi = t * 2.0 * PI;
274 | float theta = (t * 2.0 - 1.0);
275 |
276 | return spherical(radius, time + phi, theta);
277 | }
278 | ```
279 |
280 | Which gives us:
281 |
282 |
283 |
284 | ---
285 |
286 | The `r` (radius), `phi` and `theta` parameters can be a function of `t` to create some interesting shapes like [torus knots](http://paulbourke.net/geometry/knots/).
287 |
288 | After some trial and error, we end up with something like this:
289 |
290 | ```glsl
291 | vec3 sample (float t) {
292 | float beta = t * PI;
293 |
294 | float r = sin(beta * 2.0) * 0.75;
295 | float phi = sin(beta * 8.0 + time);
296 | float theta = 4.0 * beta;
297 |
298 | return spherical(r, phi, theta);
299 | }
300 | ```
301 |
302 |
303 |
304 | # Multiple Instances
305 |
306 | Things really start to take shape once you add in more curve meshes. For performance, they should all share the same geometry we created earlier.
307 |
308 |
309 |
310 | Our final parametric function looks very similar to our last step, but with some angles offset by an `index` uniform. The `index` float ranges from 0.0 to 1.0 and is the result of `meshIndex / (totalMeshes - 1)`. The image above uses 40 curves with 300 subdivisions and a random thickness per curve mesh.
311 |
312 | ```glsl
313 | // import an easing function for nicer animations
314 | #pragma glslify: ease = require('glsl-easings/exponential-in-out');
315 |
316 | vec3 sample (float t) {
317 | float beta = t * PI;
318 |
319 | float ripple = ease(sin(t * 2.0 * PI + time)) * 0.25;
320 | float noise = time + index * ripple * 8.0;
321 |
322 | float r = sin(index * 0.75 + beta * 2.0) * 0.75;
323 | float theta = 4.0 * beta + index * 0.25;
324 | float phi = sin(index * 2.0 + beta * 8.0 + noise);
325 |
326 | return spherical(r, phi, theta);
327 | }
328 | ```
329 |
330 | We're also modulating the per-vertex `volume` before solving the Frenet-Serret frame. This gives each curve some variety in thickness along its length.
331 |
332 | ```glsl
333 | // build our tube geometry
334 | vec2 volume = vec2(thickness);
335 |
336 | // animate the curve thickness
337 | float vOff = index * 20.0 + time * 2.5;
338 | float vAngle = t * lengthSegments * 0.5 + vOff;
339 | float vMod = sin(vAngle) * 0.5 + 0.5;
340 | volume += 0.01 * vMod;
341 |
342 | ... createTube(...);
343 | ```
344 |
345 | Then we add some fake rim lighting in the fragment step and mix it with the Z-normal of the tube:
346 |
347 | ```glsl
348 | ...
349 | vec3 V = normalize(vViewPosition);
350 | float vDotN = 1.0 - max(dot(V, normal), 0.0);
351 | float rim = smoothstep(0.5, 1.0, vDotN);
352 |
353 | float diffuse = normal.z * 0.5 + 0.5;
354 | diffuse += rim * 2.0;
355 | ...
356 | ```
357 |
358 | Lastly, we add a small effect for color transitions, which you can see in the final shaders [here](https://github.com/mattdesl/parametric-curves/tree/master/lib/shaders).
359 |
360 | # Gotchas
361 |
362 | ## Twists & Vanishing Curves
363 |
364 | As I mentioned in the intro, this technique has some serious downsides. One is that, depending on your equation, the Frenet-Serret frame might lead to chaotic twists in rotation. Below is a particularly bad edge case that shows a lot of twists:
365 |
366 | ```glsl
367 | vec3 sample (float t) {
368 | return vec3(t * 2.0 - 1.0, t * 2.0 - 1.0, 0.0);
369 | }
370 | ```
371 |
372 |
373 |
374 | To solve this, we need to use [Parallel Transport frames](https://pdfs.semanticscholar.org/7e65/2313c1f8183a0f43acce58ae8d8caf370a6b.pdf). For each vertex, we need to solve all the Frenet-Serret frames that have come before it. This is extremely expensive, and depending on your subdivision and number of sides, you might only be able to render a handful of curves before you reach a vertex shader bottleneck.
375 |
376 | The final vertex shader provides a `ROBUST` define flag [that solves this](https://github.com/mattdesl/parametric-curves/blob/45f321fd43af3a0786aa2dd4016931cc39325944/lib/shaders/tube.vert#L66-L162) issue, at the expense of performance:
377 |
378 |
379 |
380 | See [here](https://github.com/mattdesl/parametric-curves/blob/45f321fd43af3a0786aa2dd4016931cc39325944/lib/components/createTubes.js#L30-L31) to enable the flag.
381 |
382 | A similar problem arises with exactly straight lines, which will disappear entirely using our fast Frenet-Serret approach.
383 |
384 | ```glsl
385 | vec3 sample (float t) {
386 | return vec3(t * 2.0 - 1.0, 0.0, 0.0);
387 | }
388 | ```
389 |
390 | Again, you can enable the `ROBUST` define at the cost of performance, or jitter your components slightly so the line is no longer exactly straight.
391 |
392 | ## End Cap Normals
393 |
394 | Another unsolved problem in this demo is the normals of the end caps. They look a little puffy when using smooth normals, but ideally they should appear flat. I'd be curious to hear if others have an idea of how to solve this.
395 |
396 | ## Closed Curves
397 |
398 | This demo does not attempt to render closed curves — it just so happens that, with the fast Frenet-Serret approach, the curve seems to close naturally. The same parametric equations with the `ROBUST` flag will *not* close properly, as Parallel Transport requires an additional (expensive) pass over the segments to close the curve properly.
399 |
400 | # Next Steps
401 |
402 | There are lots of interesting things we can do from here, like:
403 |
404 | - use a [custom MeshStandardMaterial](https://gist.github.com/mattdesl/034c5daf2cf5a01c458bc9584cbe6744) for shading and reflections
405 | - modulating the *t* parameter before sending it to the parametric equation, e.g. to make it appear like each curve is being drawn in.
406 | - use instanced buffer geometry to reduce the number of draw calls
407 | - use noise and texture reads in our parametric equation for a variety of effects
408 | - try extruding `volume` with a different shape, not just a circle (i.e. a star or rounded box)
409 |
410 | # Further Reading
411 |
412 | - [Parallel Transport Approach to Curve Framing](https://pdfs.semanticscholar.org/7e65/2313c1f8183a0f43acce58ae8d8caf370a6b.pdf)
413 | - [Spline Extrustions, Tubes and Knots of Sorts](http://www.lab4games.net/zz85/blog/2012/04/24/spline-extrusions-tubes-and-knots-of-sorts/)
414 |
415 | Enjoy!
--------------------------------------------------------------------------------
/blogpost/shader.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattdesl/parametric-curves/deec8372ef141af367833af9375966856a0e31f0/blogpost/shader.png
--------------------------------------------------------------------------------
/blogpost/xmas.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattdesl/parametric-curves/deec8372ef141af367833af9375966856a0e31f0/blogpost/xmas.jpg
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | require('fastclick')(document.body);
2 |
3 | const query = require('./lib/util/query');
4 | const isMobile = require('./lib/util/isMobile');
5 | const createApp = require('./lib/createApp');
6 | const createLoop = require('raf-loop');
7 | const classes = require('element-class');
8 |
9 | // some hand-picked colors
10 | const palettes = [ '#f7803c', '#b3204d', '#cbe86b', '#2b4e72', '#d4ee5e', '#ff003c', '#e6ac27', '#d95b43', '#a3a948', '#838689', '#556270', '#292c37', '#fa6900', '#eb7b59', '#ff4e50', '#9d9d93', '#00a8c6', '#2b4e72', '#e4844a', '#9cc4e4', '#515151' ];
11 | let paletteIndex = 0;
12 |
13 | const createTubes = require('./lib/components/createTubes');
14 |
15 | const infoElement = document.querySelector('.info-container');
16 |
17 | const app = createApp({
18 | canvas: document.querySelector('#canvas'),
19 | alpha: true
20 | });
21 |
22 | const background = 'hsl(0, 0%, 100%)';
23 | document.body.style.background = background;
24 | app.renderer.setClearColor(0xffffff, 0);
25 |
26 | setupCursor();
27 | start();
28 |
29 | function setupCursor () {
30 | if (query.orbitControls) {
31 | const onMouseGrab = () => classes(app.canvas).add('grabbing');
32 | const onMouseUngrab = () => classes(app.canvas).remove('grabbing');
33 | app.canvas.addEventListener('mousedown', onMouseGrab, false);
34 | document.addEventListener('mouseup', onMouseUngrab, false);
35 | }
36 | classes(app.canvas).add(query.orbitControls ? 'grab' : 'clickable');
37 | }
38 |
39 | function start () {
40 | const line = createTubes(app);
41 | app.scene.add(line.object3d);
42 |
43 | const skipFrames = query.skipFrames;
44 | let intervalTime = 0;
45 |
46 | // no context menu on mobile...
47 | if (isMobile) app.canvas.oncontextmenu = () => false;
48 |
49 | app.canvas.addEventListener('touchstart', tap);
50 | if (!isMobile) app.canvas.addEventListener('mousedown', tap);
51 | if (isMobile) infoElement.textContent = 'tap to interact';
52 | infoElement.style.visibility = 'visible';
53 |
54 | if (query.renderOnce) tick(0);
55 | else createLoop(tick).start();
56 |
57 | function tick (dt = 0) {
58 | intervalTime += dt;
59 | if (intervalTime > 1000 / 20) {
60 | intervalTime = 0;
61 | } else if (skipFrames) {
62 | return;
63 | }
64 | line.update(dt);
65 | app.tick(dt);
66 | app.render();
67 | }
68 |
69 | function tap (ev) {
70 | // ev.preventDefault();
71 | line.setPalette(palettes[paletteIndex++ % palettes.length]);
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/lib/components/createTubes.js:
--------------------------------------------------------------------------------
1 | const createTubeGeometry = require('../geom/createTubeGeometry');
2 | const glslify = require('glslify');
3 | const path = require('path');
4 | const newArray = require('new-array');
5 | const tweenr = require('tweenr')();
6 | const isMobile = require('../util/isMobile');
7 |
8 | const {
9 | randomFloat
10 | } = require('../util/random');
11 |
12 | module.exports = function (app) {
13 | const totalMeshes = isMobile ? 30 : 40;
14 | const isSquare = false;
15 | const subdivisions = isMobile ? 200 : 300;
16 |
17 | const numSides = isSquare ? 4 : 8;
18 | const openEnded = false;
19 | const geometry = createTubeGeometry(numSides, subdivisions, openEnded);
20 |
21 | const baseMaterial = new THREE.RawShaderMaterial({
22 | vertexShader: glslify(path.resolve(__dirname, '../shaders/tube.vert')),
23 | fragmentShader: glslify(path.resolve(__dirname, '../shaders/tube.frag')),
24 | side: THREE.FrontSide,
25 | extensions: {
26 | deriviatives: true
27 | },
28 | defines: {
29 | lengthSegments: subdivisions.toFixed(1),
30 | ROBUST: false,
31 | ROBUST_NORMALS: true, // can be disabled for a slight optimization
32 | FLAT_SHADED: isSquare
33 | },
34 | uniforms: {
35 | thickness: { type: 'f', value: 1 },
36 | time: { type: 'f', value: 0 },
37 | color: { type: 'c', value: new THREE.Color('#303030') },
38 | animateRadius: { type: 'f', value: 0 },
39 | animateStrength: { type: 'f', value: 0 },
40 | index: { type: 'f', value: 0 },
41 | totalMeshes: { type: 'f', value: totalMeshes },
42 | radialSegments: { type: 'f', value: numSides }
43 | }
44 | });
45 |
46 | const lines = newArray(totalMeshes).map((_, i) => {
47 | const t = totalMeshes <= 1 ? 0 : i / (totalMeshes - 1);
48 |
49 | const material = baseMaterial.clone();
50 | material.uniforms = THREE.UniformsUtils.clone(material.uniforms);
51 | material.uniforms.index.value = t;
52 | material.uniforms.thickness.value = randomFloat(0.005, 0.0075);
53 |
54 | const mesh = new THREE.Mesh(geometry, material);
55 | mesh.frustumCulled = false; // to avoid ThreeJS errors
56 | return mesh;
57 | });
58 |
59 | // add to a parent container
60 | const container = new THREE.Object3D();
61 | lines.forEach(mesh => container.add(mesh));
62 |
63 | return {
64 | object3d: container,
65 | update,
66 | setPalette
67 | };
68 |
69 | // animate in a new color palette
70 | function setPalette (palette) {
71 | tweenr.cancel();
72 | lines.forEach((mesh, i) => {
73 | const uniforms = mesh.material.uniforms;
74 | uniforms.color.value.set(palette);
75 |
76 | const delay = i * 0.004;
77 | uniforms.animateRadius.value = 0;
78 | uniforms.animateStrength.value = 1;
79 | tweenr.to(uniforms.animateRadius, { value: 1, duration: 0.5, delay, ease: 'epxoOut' });
80 | tweenr.to(uniforms.animateStrength, { value: 0, duration: 1, delay, ease: 'expoInOut' });
81 | });
82 | }
83 |
84 | function update (dt) {
85 | dt = dt / 1000;
86 | lines.forEach(mesh => {
87 | mesh.material.uniforms.time.value += dt;
88 | });
89 | }
90 | };
91 |
--------------------------------------------------------------------------------
/lib/createApp.js:
--------------------------------------------------------------------------------
1 | /*
2 | This is a generic "ThreeJS Application"
3 | helper which sets up a renderer and camera
4 | controls.
5 | */
6 |
7 | const createControls = require('orbit-controls');
8 | const assign = require('object-assign');
9 | const defined = require('defined');
10 | const mouseEventOffset = require('mouse-event-offset');
11 | const tweenr = require('tweenr')();
12 | const isMobile = require('./util/isMobile');
13 |
14 | const query = require('./util/query');
15 |
16 | module.exports = createApp;
17 | function createApp (opt = {}) {
18 | // Scale for retina
19 | const dpr = defined(query.dpr, Math.min(2, window.devicePixelRatio));
20 |
21 | const cameraDistance = isMobile ? 2 : 1.75;
22 | const theta = 0 * Math.PI / 180;
23 | const angleOffset = 20;
24 | const mouseOffset = new THREE.Vector2();
25 | const tmpQuat1 = new THREE.Quaternion();
26 | const tmpQuat2 = new THREE.Quaternion();
27 | const AXIS_X = new THREE.Vector3(1, 0, 0);
28 | const AXIS_Y = new THREE.Vector3(0, 1, 0);
29 |
30 | // Our WebGL renderer with alpha and device-scaled
31 | const renderer = new THREE.WebGLRenderer(assign({
32 | alpha: false,
33 | stencil: false,
34 | antialias: true // default enabled
35 | }, opt));
36 | renderer.setPixelRatio(dpr);
37 | renderer.gammaFactor = 2.2;
38 | renderer.gammaOutput = true;
39 | renderer.gammaInput = true;
40 | renderer.sortObjects = false;
41 |
42 | // Add the