├── .babelrc
├── .gitignore
├── .npmignore
├── LICENSE.md
├── README.md
├── app
└── index.html
├── images
├── 1.png
├── 2.png
├── 3.png
├── 4.png
├── 5.png
└── triangle.png
├── lib
├── createApp.js
├── createBunnyGeometry.js
├── index.js
├── reference
│ ├── shader1.frag
│ ├── shader1.vert
│ ├── shader2.frag
│ ├── shader2.vert
│ ├── shader3.frag
│ ├── shader3.vert
│ ├── shader4.frag
│ ├── shader4.vert
│ ├── shader5.frag
│ └── shader5.vert
├── shader.frag
└── shader.vert
└── 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 Jam3
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 | ##### [jam3-lesson](https://github.com/Jam3/jam3-lesson) » webgl » shader-threejs
2 |
3 | ---
4 |
5 | # WebGL Lessons — ThreeJS Shaders
6 |
7 | In this lesson, we'll learn how to apply Fragment and Vertex shaders to a [ThreeJS](http://threejs.org/) mesh in WebGL. Make sure you read [webgl-shader-intro](https://github.com/Jam3/jam3-lesson-webgl-shader-intro) first to understand the basics of GLSL.
8 |
9 | ## Contents
10 |
11 | - [Why ThreeJS?](#why-threejs)
12 | - [Setup](#setup)
13 | - [Code Overview](#code-overview)
14 | - [Mesh & Shader Materials](#mesh--shader-materials)
15 | - [Step 1: Your First Shader](#step-1-your-first-shader)
16 | - [Attributes](#attributes)
17 | - [Uniforms](#uniforms)
18 | - [Projecting the Vertex](#projecting-the-vertex)
19 | - [Step 2: Distance from Center](#step-2-distance-from-center)
20 | - [Step 3: Visualizing Normals](#step-3-visualizing-normals)
21 | - [Step 4: Exploding Triangles](#step-4-exploding-triangles)
22 | - [Step 5: Animation](#step-5-animation)
23 | - [Appendix: ShaderMaterial vs RawShaderMaterial](#appendix-shadermaterial-vs-rawshadermaterial)
24 |
25 | ## Why ThreeJS?
26 |
27 | Writing raw WebGL involves a lot of boilerplate and adds a great deal of complexity. Instead, we will use ThreeJS to provide a layer of convenience and abstraction, and also to ease ourselves into more advanced ThreeJS lessons.
28 |
29 | This lesson will be easier if you already have some basic experience with ThreeJS Scene, Camera, WebGLRenderer etc.
30 |
31 | ## Setup
32 |
33 | You can download [ThreeJS](http://threejs.org/) directly from the site, but for our purposes we will use [npm](https://www.npmjs.com/) and [budo](https://github.com/mattdesl/budo/) for a faster development workflow.
34 |
35 | > :bulb: If you haven't used npm or budo, see our lesson [Modules for Frontend JavaScript](https://github.com/Jam3/jam3-lesson-module-basics) for more details.
36 |
37 | First, you should clone this repo and install the dependencies:
38 |
39 | ```sh
40 | git clone https://github.com/Jam3/jam3-lesson-webgl-shader-threejs.git
41 |
42 | # move into directory
43 | cd jam3-lesson-webgl-shader-threejs
44 |
45 | # install dependencies
46 | npm install
47 |
48 | # start dev server
49 | npm run start
50 | ```
51 |
52 | Now you should be able to open [http://localhost:9966/](http://localhost:9966/) to see a white bunny.
53 |
54 |
55 |
56 | This project uses Babel through [babelify](https://github.com/babel/babelify) (a browserify transform). It also uses [brfs](https://github.com/substack/brfs), which can statically inline files like shaders.
57 |
58 | ## Code Overview
59 |
60 | The code is split into a few different files:
61 |
62 | - **[`./lib/index.js`](./lib/index.js)**
63 | This holds the "guts" of our demo, creating our application, geometry, mesh, and render loop. Since it is the root file, we use this to ensure the `THREE` global can be accessed from other files:
64 | ```js
65 | global.THREE = require('three');
66 | ```
67 | - **[`./lib/createApp.js`](./lib/createApp.js)**
68 | This boilerplate creates a basic ThreeJS application using a helper module, [orbit-controls](https://github.com/Jam3/orbit-controls), for 3D camera movement.
69 |
70 | You can review the code in this file, but it won't be essential to this lesson.
71 |
72 | - **[`./lib/createBunnyGeometry.js`](./lib/createBunnyGeometry.js)**
73 | This helper method creates a ThreeJS geometry from a *mesh primitive*, in this case a 3D bunny.
74 |
75 | For the purpose of this lesson, you can skip over this file.
76 | - **[`./lib/shader.frag`](./lib/shader.frag)**
77 | This is the fragment shader which you will be editing in this lesson.
78 |
79 | - **[`./lib/shader.vert`](./lib/shader.vert)**
80 | This is the fragment shader which you will be editing in this lesson.
81 |
82 | - **[`./lib/reference/`](./lib/reference/)**
83 | Here you can find some shader references for each step in this lesson.
84 |
85 | ## Mesh & Shader Materials
86 |
87 | In ThreeJS, a `Mesh` is the basic building block for rendering 3D shapes. It is made up of a `Geometry` (often re-used for memory) and a `Material`. For example, this code adds a red cube to the scene:
88 |
89 | ```js
90 | const geometry = new THREE.BoxGeometry(1, 1, 1);
91 | const material = new THREE.MeshBasicMaterial({ color: 'red' });
92 | const mesh = new THREE.Mesh(geometry, material);
93 | scene.add(mesh);
94 | ```
95 |
96 | In our [./lib/index.js](./lib/index.js), we have some similar code that sets up a 3D bunny geometry:
97 |
98 | ```js
99 | // Get a nicely prepared geometry
100 | const geometry = createBunnyGeometry({ flat: true });
101 | ```
102 |
103 | Next, we create a material for the bunny.
104 |
105 | We use a `RawShaderMaterial`, which is a special type of material that accepts `vertexShader` and `fragmentShader` strings. We are using [brfs](https://github.com/substack/brfs), a source transform, so that we can keep our shaders as separate files and inline them at bundle-time.
106 |
107 | ```js
108 | const path = require('path');
109 | const fs = require('fs');
110 |
111 | // Create our vertex/fragment shaders
112 | const material = new THREE.RawShaderMaterial({
113 | vertexShader: fs.readFileSync(path.join(__dirname, 'shader.vert'), 'utf8'),
114 | fragmentShader: fs.readFileSync(path.join(__dirname, 'shader.frag'), 'utf8')
115 | });
116 | ```
117 |
118 | > :bulb: Later, you may wish to explore [glslify](https://github.com/stackgl/glslify) instead of `brfs`.
119 |
120 | Our bunny mesh is then created the same was as our box:
121 |
122 | ```js
123 | // Setup our mesh
124 | const mesh = new THREE.Mesh(geometry, material);
125 | scene.add(mesh);
126 | ```
127 |
128 | ### Step 1: Your First Shader
129 |
130 | Reference: [shader.vert](./lib/shader.vert), [shader.frag](./lib/shader.frag)
131 |
132 |
133 |
134 | The initial shader provided with this repo just renders the 3D mesh with a flat white material. It's a good idea to always start with a basic shader before you dive into other effects.
135 |
136 | #### Vertex Shader
137 |
138 | The vertex shader at [`./lib/shader.vert`](./lib/shader.vert) looks like this:
139 |
140 | ```glsl
141 | attribute vec3 position;
142 |
143 | uniform mat4 projectionMatrix;
144 | uniform mat4 modelViewMatrix;
145 |
146 | void main () {
147 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
148 | }
149 | ```
150 |
151 | Fragment and vertex shaders require a `main()` function — it gets called by WebGL for each vertex (or fragment) in our mesh.
152 |
153 | This vertex shader introduces two new concepts: **attributes** and **uniforms**.
154 |
155 | ##### Attributes
156 |
157 | A vertex shader runs on each *vertex* in a geometry. A vertex can contain various *attributes*, such as Position (XYZ), UV coordinates (XY), Normal (XYZ), etc. The attributes are set up in JavaScript, and end up as read-only inputs to our vertex shader.
158 |
159 | For example, the triangle below has 3 *vertices*, each vertex has one *attribute* that holds the XYZ position. For this triangle, the vertex shader would be executed 3 times by the GPU.
160 |
161 |
162 |
163 | Our bunny vertex shader only has one attribute for the vertex position.
164 |
165 | ```glsl
166 | attribute vec3 position;
167 | ```
168 |
169 | ##### Uniforms
170 |
171 | Vertex and fragment shaders can also have *uniforms*, which is a constant value across all triangles (or fragments) in a render call. This value, set from JavaScript, is read-only in the vertex and fragment shaders.
172 |
173 | For example, we could use a `uniform` to define a constant RGB color for our mesh.
174 |
175 | ThreeJS provides a few built-in uniforms for shaders, such as the following 4x4 matrices, as `mat4` data types:
176 |
177 | - `projectionMatrix` — used to convert 3D world units into 2D screen-space.
178 | - `viewMatrix` — an inverse of our `PerspectiveCamera`'s world matrix
179 | - `modelMatrix` — the model-space matrix of our `Mesh`
180 | - `modelViewMatrix` — a combination of the view and model matrix
181 |
182 | > :bulb: If you aren't familiar with matrices, don't fret! We plan to cover the basics of vector math in a later lesson.
183 |
184 | If you use one of these, you will need to define it like so:
185 |
186 | ```glsl
187 | uniform mat4 projectionMatrix;
188 | uniform mat4 modelViewMatrix;
189 | ```
190 |
191 | #### Projecting The Vertex
192 |
193 | The role of the vertex shader is to turn our 3D data (e.g. position) into features that WebGL's rasterizer can use to fill our shapes on a 2D screen. This is known as "projecting" 3D world coordinates into 2D screen-space coordinates.
194 |
195 | To do this, we use the following pattern:
196 |
197 | ```glsl
198 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
199 | ```
200 |
201 | > :bulb: It's not yet necessary to understand why `1.0` is used as the `w` component in position. If you are curious, you can read more [here](http://www.tomdalling.com/blog/modern-opengl/explaining-homogenous-coordinates-and-projective-geometry/).
202 |
203 | The above could also be written a little differently, by specifying `vec4` for the attribute (WebGL will expand vectors with `w=1.0` by default) and multiplying each matrix independently.
204 |
205 | ```glsl
206 | attribute vec4 position;
207 |
208 | uniform mat4 projectionMatrix;
209 | uniform mat4 modelMatrix;
210 | uniform mat4 viewMatrix;
211 |
212 | void main () {
213 | gl_Position = projectionMatrix * viewMatrix * modelMatrix * position;
214 | }
215 | ```
216 |
217 | #### Fragment Shader
218 |
219 | If you open up [`./lib/shader.frag`](./lib/shader.frag), you'll see the fragment shader:
220 |
221 | ```glsl
222 | precision highp float;
223 |
224 | void main () {
225 | gl_FragColor = vec4(1.0);
226 | }
227 | ```
228 |
229 | The first line, with `precision`, defines the floating point precision the GPU should use by default for all floats. When using `RawShaderMaterial`, you will need to specify this at the top of your fragment shaders. You can use `lowp`, `mediump`, or `highp`, but typically `highp` is recommended.
230 |
231 | > :bulb: The default precision for vertex shaders is `highp`, so we didn't need to define it earlier.
232 |
233 | Then, we have `gl_FragColor`, which is a builtin write-only `vec4` for the output color. You should always write all 4 channels to this in your fragment shader. In our case, we are using pure white: `(1.0, 1.0, 1.0, 1.0)`. This is similar to [ShaderToy's](http://shadertoy.com/) `fragColor` variable.
234 |
235 | ## Step 2: Distance from Center
236 |
237 | Reference: [shader2.vert](./lib/reference/shader2.vert), [shader2.frag](./lib/reference/shader2.frag)
238 |
239 |
240 |
241 | Next, we'll apply a radial gradient to our mesh by coloring each pixel based on its XYZ distance from world center, `(0, 0, 0)`.
242 |
243 | You can edit your `shader.vert` and `shader.frag` to see the new changes locally.
244 |
245 | ### Vertex Shader
246 |
247 | In this step, our vertex shader looks like this:
248 |
249 | ```glsl
250 | attribute vec4 position;
251 |
252 | uniform mat4 projectionMatrix;
253 | uniform mat4 modelViewMatrix;
254 |
255 | varying float distFromCenter;
256 |
257 | void main () {
258 | distFromCenter = length(position.xyz);
259 | gl_Position = projectionMatrix * modelViewMatrix * position;
260 | }
261 | ```
262 |
263 | This shader introduces a new concept: **varyings**. This is a write-only value that will get passed down the pipeline to the fragment shader. Here, we take the magnitude of the `position` vector, i.e. compute its distance from origin `vec3(0.0)`.
264 |
265 | > :bulb: You can also use the `distance` built-in, like so:
266 | ```glsl
267 | distFromCenter = distance(position.xyz, vec3(0.0));
268 | ```
269 |
270 | ### Fragment Shader
271 |
272 | ```glsl
273 | precision highp float;
274 |
275 | varying float distFromCenter;
276 | void main () {
277 | gl_FragColor = vec4(vec3(distFromCenter), 1.0);
278 | }
279 | ```
280 |
281 | In a fragment shader, varyings are read-only. They are inputs from the vertex shader. Their values are interpolated between vertices, so if you have a `0.0` float coming from one vertex, and `1.0` coming from another, each fragment will end up with some values in-between.
282 |
283 | In our case, the fragment RGB color is set to the `distFromCenter`, which gives us values that are black near the world origin, and more white as the vertices move away from this point.
284 |
285 |
286 | ## Step 3: Visualizing Normals
287 |
288 | Reference: [shader3.vert](./lib/reference/shader3.vert), [shader3.frag](./lib/reference/shader3.frag)
289 |
290 |
291 |
292 | In this step, we visualize the *normals* of the mesh. This is a new attribute, already specified in `createBunnyGeometry.js`, that defines the way each triangle points away from the center. Normals are often used in surface lighting and other effects.
293 |
294 | A normal is a unit vector, which means its components are normalized to the range -1.0 to 1.0.
295 |
296 | ### Vertex Shader
297 |
298 | ```glsl
299 | attribute vec4 position;
300 | attribute vec3 normal;
301 |
302 | uniform mat4 projectionMatrix;
303 | uniform mat4 modelViewMatrix;
304 |
305 | varying vec3 vNormal;
306 |
307 | void main () {
308 | vNormal = normal;
309 | gl_Position = projectionMatrix * modelViewMatrix * position;
310 | }
311 | ```
312 |
313 | In this shader, we include a new attribute, `normal`, and pass it along to the fragment shader with the `vNormal` varying.
314 |
315 | ### Fragment Shader
316 |
317 | ```glsl
318 | precision highp float;
319 |
320 | varying vec3 vNormal;
321 |
322 | void main () {
323 | gl_FragColor = vec4(vNormal, 1.0);
324 | }
325 | ```
326 |
327 | Here, we simply render the passed `vNormal` for each fragment. It ends up looking nice, even though some values will be clamped because they are less than 0.0.
328 |
329 | > :bulb: The geometry in `index.js` is created with `{ flat: true }`, which means the normals should be separated. You can try toggling that to see how it looks with combined (smooth) normals.
330 |
331 | ## Step 4: Exploding Triangles
332 |
333 | Reference: [shader4.vert](./lib/reference/shader4.vert), [shader4.frag](./lib/reference/shader4.frag)
334 |
335 |
336 |
337 | Now, let's push each triangle along its face normal to create an explosion effect! :fire:
338 |
339 | ### Vertex Shader
340 |
341 | ```glsl
342 | attribute vec4 position;
343 | attribute vec3 normal;
344 |
345 | uniform mat4 projectionMatrix;
346 | uniform mat4 modelViewMatrix;
347 |
348 | varying vec3 vNormal;
349 |
350 | void main () {
351 | vNormal = normal;
352 |
353 | vec4 offset = position;
354 | float dist = 0.25;
355 | offset.xyz += normal * dist;
356 | gl_Position = projectionMatrix * modelViewMatrix * offset;
357 | }
358 | ```
359 |
360 | In this shader, we offset each vertex position by a vector. In this case, we use the `normal` to know which direction the position should be offset, and a `dist` scalar to determine how far away the triangles should be pushed.
361 |
362 | We can leave the fragment shader unchanged.
363 |
364 | ## Step 5: Animation
365 |
366 | Reference: [shader5.vert](./lib/reference/shader5.vert), [shader5.frag](./lib/reference/shader5.frag)
367 |
368 |
369 |
370 | Lastly, we'll animate the explosion by adding a uniform to the shader material.
371 |
372 | This needs to be set up from JavaScript, so make sure our `index.js` defines a `uniforms` option in the material:
373 |
374 | ```js
375 | // Create our vertex/fragment shaders
376 | const material = new THREE.RawShaderMaterial({
377 | vertexShader: fs.readFileSync(path.join(__dirname, 'shader.vert'), 'utf8'),
378 | fragmentShader: fs.readFileSync(path.join(__dirname, 'shader.frag'), 'utf8'),
379 | uniforms: {
380 | time: { type: 'f', value: 0 }
381 | }
382 | });
383 | ```
384 |
385 | Above, we define a `time` uniform (which will be accessed by that name in the shader), give it a float type (ThreeJS uses `'f'` — see [here](https://github.com/mrdoob/three.js/wiki/Uniforms-types) for others), and provide a default value.
386 |
387 | Then, our render loop increments the value every frame, and changes the `value` field of our `time` uniform.
388 |
389 | ```js
390 | // Time since beginning
391 | let time = 0;
392 |
393 | // Start our render loop
394 | createLoop((dt) => {
395 | // update time
396 | time += dt / 1000;
397 |
398 | // set value
399 | material.uniforms.time.value = time;
400 |
401 | // render
402 | ...
403 | }).start();
404 | ```
405 |
406 | ### Vertex Shader
407 |
408 | ```glsl
409 | attribute vec4 position;
410 | attribute vec3 normal;
411 |
412 | uniform mat4 projectionMatrix;
413 | uniform mat4 modelViewMatrix;
414 |
415 | uniform float time;
416 |
417 | varying vec3 vNormal;
418 |
419 | void main () {
420 | vNormal = normal;
421 |
422 | vec4 offset = position;
423 |
424 | // Animate between 0 and 1
425 | // sin(x) returns a value in [-1...1] range
426 | float dist = sin(time) * 0.5 + 0.5;
427 |
428 | offset.xyz += normal * dist;
429 | gl_Position = projectionMatrix * modelViewMatrix * offset;
430 | }
431 | ```
432 |
433 | In this shader, we first define our uniform before our main function. Since it's a uniform, it will be the same value for all vertices in that render call.
434 |
435 | ```glsl
436 | uniform float time;
437 | ```
438 |
439 | We can use the built-in `sin()` function (equivalent to `Math.sin` in JavaScript), which gives us back a value from -1.0 to 1.0. Then we normalize it into 0 to 1 range, so that our mesh is always exploding outward:
440 |
441 | ```glsl
442 | float dist = sin(time) * 0.5 + 0.5;
443 | ```
444 |
445 | Voilà! We have an exploding bunny! :rabbit:
446 |
447 | ## Appendix: ShaderMaterial vs RawShaderMaterial
448 |
449 | At some point you may wonder why ThreeJS has both `ShaderMaterial` and `RawShaderMaterial`. Typically I suggest using `RawShaderMaterial` since it is less error-prone, but it means you have to be a bit more verbose and manually specify precision, extensions, etc.
450 |
451 | Instead, you can use `ShaderMaterial` and skip some definitions, such as ThreeJS's built-in attributes, uniforms and fragment shader precision:
452 |
453 | ```glsl
454 | void main () {
455 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position.xyz, 1.0);
456 | }
457 | ```
458 |
459 | ## Next Steps
460 |
461 | Stay tuned for future lessons on ThreeJS shaders, BufferGeometry, custom attributes, and more!
--------------------------------------------------------------------------------
/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |