• Stars
    star
    361
  • Rank 117,957 (Top 3 %)
  • Language
    JavaScript
  • License
    MIT License
  • Created about 8 years ago
  • Updated about 8 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Using custom vertex and fragment shaders in ThreeJS
jam3-lesson ยป webgl ยป shader-threejs

WebGL Lessons โ€” ThreeJS Shaders

In this lesson, we'll learn how to apply Fragment and Vertex shaders to a ThreeJS mesh in WebGL. Make sure you read webgl-shader-intro first to understand the basics of GLSL.

Contents

Why ThreeJS?

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.

This lesson will be easier if you already have some basic experience with ThreeJS Scene, Camera, WebGLRenderer etc.

Setup

You can download ThreeJS directly from the site, but for our purposes we will use npm and budo for a faster development workflow.

๐Ÿ’ก If you haven't used npm or budo, see our lesson Modules for Frontend JavaScript for more details.

First, you should clone this repo and install the dependencies:

git clone https://github.com/Jam3/jam3-lesson-webgl-shader-threejs.git

# move into directory
cd jam3-lesson-webgl-shader-threejs

# install dependencies
npm install

# start dev server
npm run start

Now you should be able to open http://localhost:9966/ to see a white bunny.

This project uses Babel through babelify (a browserify transform). It also uses brfs, which can statically inline files like shaders.

Code Overview

The code is split into a few different files:

  • ./lib/index.js
    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:
global.THREE = require('three');
  • ./lib/createApp.js
    This boilerplate creates a basic ThreeJS application using a helper module, orbit-controls, for 3D camera movement.

    You can review the code in this file, but it won't be essential to this lesson.

  • ./lib/createBunnyGeometry.js
    This helper method creates a ThreeJS geometry from a mesh primitive, in this case a 3D bunny.

    For the purpose of this lesson, you can skip over this file.

  • ./lib/shader.frag
    This is the fragment shader which you will be editing in this lesson.

  • ./lib/shader.vert
    This is the fragment shader which you will be editing in this lesson.

  • ./lib/reference/
    Here you can find some shader references for each step in this lesson.

Mesh & Shader Materials

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:

const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 'red' });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

In our ./lib/index.js, we have some similar code that sets up a 3D bunny geometry:

// Get a nicely prepared geometry
const geometry = createBunnyGeometry({ flat: true });

Next, we create a material for the bunny.

We use a RawShaderMaterial, which is a special type of material that accepts vertexShader and fragmentShader strings. We are using brfs, a source transform, so that we can keep our shaders as separate files and inline them at bundle-time.

const path = require('path');
const fs = require('fs');

// Create our vertex/fragment shaders
const material = new THREE.RawShaderMaterial({
  vertexShader: fs.readFileSync(path.join(__dirname, 'shader.vert'), 'utf8'),
  fragmentShader: fs.readFileSync(path.join(__dirname, 'shader.frag'), 'utf8')
});

๐Ÿ’ก Later, you may wish to explore glslify instead of brfs.

Our bunny mesh is then created the same was as our box:

// Setup our mesh
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

Step 1: Your First Shader

Reference: shader.vert, shader.frag

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.

Vertex Shader

The vertex shader at ./lib/shader.vert looks like this:

attribute vec3 position;

uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;

void main () {
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

Fragment and vertex shaders require a main() function โ€” it gets called by WebGL for each vertex (or fragment) in our mesh.

This vertex shader introduces two new concepts: attributes and uniforms.

Attributes

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.

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.

Our bunny vertex shader only has one attribute for the vertex position.

attribute vec3 position;
Uniforms

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.

For example, we could use a uniform to define a constant RGB color for our mesh.

ThreeJS provides a few built-in uniforms for shaders, such as the following 4x4 matrices, as mat4 data types:

  • projectionMatrix โ€” used to convert 3D world units into 2D screen-space.
  • viewMatrix โ€” an inverse of our PerspectiveCamera's world matrix
  • modelMatrix โ€” the model-space matrix of our Mesh
  • modelViewMatrix โ€” a combination of the view and model matrix

๐Ÿ’ก If you aren't familiar with matrices, don't fret! We plan to cover the basics of vector math in a later lesson.

If you use one of these, you will need to define it like so:

uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;

Projecting The Vertex

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.

To do this, we use the following pattern:

gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);

๐Ÿ’ก 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.

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.

attribute vec4 position;

uniform mat4 projectionMatrix;
uniform mat4 modelMatrix;
uniform mat4 viewMatrix;

void main () {
  gl_Position = projectionMatrix * viewMatrix * modelMatrix * position;
}

Fragment Shader

If you open up ./lib/shader.frag, you'll see the fragment shader:

precision highp float;

void main () {
  gl_FragColor = vec4(1.0);
}

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.

๐Ÿ’ก The default precision for vertex shaders is highp, so we didn't need to define it earlier.

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 fragColor variable.

Step 2: Distance from Center

Reference: shader2.vert, shader2.frag

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).

You can edit your shader.vert and shader.frag to see the new changes locally.

Vertex Shader

In this step, our vertex shader looks like this:

attribute vec4 position;

uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;

varying float distFromCenter;

void main () {
  distFromCenter = length(position.xyz);
  gl_Position = projectionMatrix * modelViewMatrix * position;
}

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).

๐Ÿ’ก You can also use the distance built-in, like so:

distFromCenter = distance(position.xyz, vec3(0.0));

Fragment Shader

precision highp float;

varying float distFromCenter;
void main () {
  gl_FragColor = vec4(vec3(distFromCenter), 1.0);
}

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.

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.

Step 3: Visualizing Normals

Reference: shader3.vert, shader3.frag

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.

A normal is a unit vector, which means its components are normalized to the range -1.0 to 1.0.

Vertex Shader

attribute vec4 position;
attribute vec3 normal;

uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;

varying vec3 vNormal;

void main () {
  vNormal = normal;
  gl_Position = projectionMatrix * modelViewMatrix * position;
}

In this shader, we include a new attribute, normal, and pass it along to the fragment shader with the vNormal varying.

Fragment Shader

precision highp float;

varying vec3 vNormal;

void main () {
  gl_FragColor = vec4(vNormal, 1.0);
}

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.

๐Ÿ’ก 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.

Step 4: Exploding Triangles

Reference: shader4.vert, shader4.frag

Now, let's push each triangle along its face normal to create an explosion effect! ๐Ÿ”ฅ

Vertex Shader

attribute vec4 position;
attribute vec3 normal;

uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;

varying vec3 vNormal;

void main () {
  vNormal = normal;

  vec4 offset = position;
  float dist = 0.25;
  offset.xyz += normal * dist;
  gl_Position = projectionMatrix * modelViewMatrix * offset;
}

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.

We can leave the fragment shader unchanged.

Step 5: Animation

Reference: shader5.vert, shader5.frag

Lastly, we'll animate the explosion by adding a uniform to the shader material.

This needs to be set up from JavaScript, so make sure our index.js defines a uniforms option in the material:

// Create our vertex/fragment shaders
const material = new THREE.RawShaderMaterial({
  vertexShader: fs.readFileSync(path.join(__dirname, 'shader.vert'), 'utf8'),
  fragmentShader: fs.readFileSync(path.join(__dirname, 'shader.frag'), 'utf8'),
  uniforms: {
    time: { type: 'f', value: 0 }
  }
});

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 for others), and provide a default value.

Then, our render loop increments the value every frame, and changes the value field of our time uniform.

// Time since beginning
let time = 0;

// Start our render loop
createLoop((dt) => {
  // update time
  time += dt / 1000;

  // set value
  material.uniforms.time.value = time;

  // render
  ...
}).start();

Vertex Shader

attribute vec4 position;
attribute vec3 normal;

uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;

uniform float time;

varying vec3 vNormal;

void main () {
  vNormal = normal;

  vec4 offset = position;

  // Animate between 0 and 1
  // sin(x) returns a value in [-1...1] range
  float dist = sin(time) * 0.5 + 0.5;

  offset.xyz += normal * dist;
  gl_Position = projectionMatrix * modelViewMatrix * offset;
}

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.

uniform float time;

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:

float dist = sin(time) * 0.5 + 0.5;

Voilร ! We have an exploding bunny! ๐Ÿฐ

Appendix: ShaderMaterial vs RawShaderMaterial

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.

Instead, you can use ShaderMaterial and skip some definitions, such as ThreeJS's built-in attributes, uniforms and fragment shader precision:

void main () {
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position.xyz, 1.0);
}

Next Steps

Stay tuned for future lessons on ThreeJS shaders, BufferGeometry, custom attributes, and more!

More Repositories

1

math-as-code

a cheat-sheet for mathematical notation in code form
14,818
star
2

devtool

[OBSOLETE] runs Node.js programs through Chromium DevTools
JavaScript
3,774
star
3

nice-color-palettes

nice colour palettes as JSON
JavaScript
848
star
4

three-bmfont-text

renders BMFont files in ThreeJS with word-wrapping
JavaScript
764
star
5

glsl-fast-gaussian-blur

optimized single-pass blur shaders for GLSL
JavaScript
659
star
6

hihat

๐ŸŽฉ local Node/Browser development with Chrome DevTools
JavaScript
447
star
7

jam3-lesson-webgl-shader-intro

A brief introduction to fragment shaders.
307
star
8

web-audio-player

a cross-browser WebAudio player
JavaScript
244
star
9

360-image-viewer

A standalone panorama viewer with WebGL
JavaScript
243
star
10

ae-to-json

will export an After Effects project as a JSON object
JavaScript
225
star
11

msdf-bmfont

Generate BMFont texture and spec using msdfgen
JavaScript
161
star
12

jam3-lesson

142
star
13

audiobuffer-to-wav

convert an AudioBuffer to .wav format
JavaScript
132
star
14

Invisible-Highway

Invisible Highway is an experiment in controlling physical things in the real world by drawing in AR. Simply make a pathway along the floor on your phone and the robot car will follow that path on the actual floor in your room. A custom highway with scenery is generated along the path to make the robots a little more scenic on your phone screen.
C#
130
star
15

awesome-streetview

beautiful [lat, lng] Google Street View locations
JavaScript
129
star
16

ffmpeg-gif

shell script to convert video to high quality GIF with ffmpeg
JavaScript
124
star
17

jam3-lesson-module-basics

intro to modular programming for frontend JavaScript
113
star
18

orbit-controls

generic controls for orbiting a target in 3D
JavaScript
110
star
19

nextjs-boilerplate

Jam3 NextJS Generator for SPA, SSG, SSR and JAMStack applications
TypeScript
107
star
20

svg-to-image

convert SVG text to a Image that can be drawn in canvas
JavaScript
103
star
21

voice-activity-detection

Voice activity detection
JavaScript
102
star
22

extract-streetview

extract street view spherical images and depth information
JavaScript
101
star
23

react-f1

F1 ui animation library for React
JavaScript
90
star
24

webgl-react-boilerplate

WebGL React App โšก๏ธ
JavaScript
87
star
25

opentype-layout

word wraps and lays out Opentype.js glyphs
JavaScript
86
star
26

chaikin-smooth

Chaikin's smoothing algorithm for 2D polylines
JavaScript
82
star
27

f1

A stateful ui library
JavaScript
78
star
28

touch-scroll-physics

scroll physics for a scroll pane with edge bounce and velocity
JavaScript
77
star
29

preloader

A library for loading common web assets
JavaScript
69
star
30

three-png-stream

streams ThreeJS render target pixel data
JavaScript
66
star
31

layout-bmfont-text

word-wraps and lays out text glyphs
JavaScript
59
star
32

react-background-video-player

React background video component with simple player API
JavaScript
58
star
33

ios-safe-audio-context

create a WebAudio context that works in iOS and everywhere else
JavaScript
57
star
34

perspective-camera

a high-level 3D perspective camera
JavaScript
53
star
35

glsl-hsl2rgb

HSL to RGB color conversion in GLSL
GLSL
50
star
36

touches

simplified touch/mouse events for flick and swipe
JavaScript
45
star
37

ios-video-test

a test of inline iOS video playback
JavaScript
45
star
38

three-path-geometry

thick 2D lines for ThreeJS
JavaScript
42
star
39

jam3-lesson-canvas2d

open source code for a Canvas2D workshop at Jam3
JavaScript
41
star
40

maya-json-export

a generic Maya to JSON exporter for triangle meshes
JavaScript
41
star
41

glsl-100-to-300

transpiles GLSL tokens from v100 to v300 es
JavaScript
39
star
42

xhr-request

tiny http client for Node and the browser
JavaScript
39
star
43

webvr-gearvr-test

a simple test of WebVR running natively in GearVR
JavaScript
38
star
44

generator-jam3

This is a generator for Jam3 projects
JavaScript
37
star
45

google-maps-api

Get up and running with the google maps API quickly
JavaScript
34
star
46

babel-plugin-static-fs

statically transforms Node fs for the browser
JavaScript
33
star
47

ae-to-json-cli

This is a command line application to export After Effects files as JSON
JavaScript
33
star
48

tech-we-use

A list of technologies: modules, libraries, and tools we use
32
star
49

load-bmfont

loads a BMFont file in Node and the browser
JavaScript
31
star
50

jam3-testing-tools

a brief intro to testing tools
30
star
51

gl-pixel-stream

streaming gl.readPixels from an FBO
JavaScript
30
star
52

jam3-lesson-module-creation

introduction to creating a new npm module
29
star
53

threejs-generate-gif

Generate an animated GIF (using the GPU) from a threejs scene
JavaScript
29
star
54

tap-dev-tool

prettifies TAP in the browser's console
JavaScript
29
star
55

text-split

Utility for splitting text into chunks based on regex.
JavaScript
27
star
56

three-simplicial-complex

render simplicial complexes with ThreeJS
JavaScript
27
star
57

three-buffer-vertex-data

an easy way to set vertex data on a BufferGeometry
JavaScript
27
star
58

parse-dds

parses the headers of a DDS texture file
JavaScript
26
star
59

threejs-post-process-example

a tutorial on ThreeJS post processing
JavaScript
24
star
60

add-px-to-style

Will add px to the end of style values which are Numbers
JavaScript
24
star
61

google-panorama-by-location

gets a Google StreetView by [ lat, lng ]
JavaScript
24
star
62

mesh-heightmap-contours

Given a heightmap, generate a "contoured" terrain mesh
JavaScript
24
star
63

detect-import-require

list require and import paths from a JavaScript source
JavaScript
24
star
64

says

cross-platform 'say' command using Electron
JavaScript
23
star
65

background-cover

Simulate 'background-size: cover' on HTMLVideoElement and HTMLImageElement
JavaScript
23
star
66

analyser-frequency-average

gets an average intensity between two frequency ranges
JavaScript
23
star
67

webgl-components

Modular components and utilities used for WebGL based projects ๐Ÿ’…
TypeScript
22
star
68

video-element

A simple HTML5/YouTube Video Element with a unified interface
JavaScript
22
star
69

three-fluid-demo

ThreeJS fluid simulation demo
GLSL
22
star
70

delaunify

randomly delaunay-triangulates an image
JavaScript
22
star
71

camera-unproject

unproject 2D point to 3D coordinate
JavaScript
21
star
72

heightmap-contours

Generate a series of 2D contour meshes over a heightmap
JavaScript
21
star
73

glsl-blend-overlay

blend mode 'overlay' for GLSL
C
20
star
74

ray-3d

a high-level ray picking helper for 3D intersection
JavaScript
19
star
75

scroll-manager

A handler for scrolling inside elements with different eases
JavaScript
19
star
76

touch-pinch

minimal two-finger pinch gesture detection
JavaScript
19
star
77

three-orbit-viewer

quick harness for viewing a mesh with orbit viewer
JavaScript
18
star
78

css-transform-to-mat4

Will take a string which is a css transform value (2d or 3d) and return a mat4 or 3d transformation matrix from the string.
JavaScript
17
star
79

gl-shader-output

test a shader's gl_FragColor output on a 1x1 canvas
JavaScript
17
star
80

gh-api-stream

streams JSON content from a GitHub API
JavaScript
17
star
81

uploadr

CLI tool which uploads a folder via SFTP
JavaScript
17
star
82

camera-spin

Mouse/touch-draggable first-person camera
JavaScript
17
star
83

exif-orientation-image

Properly displays an image via canvas based on the exif orientation data.
JavaScript
16
star
84

jam3-lessons-react

Quick and brief reference for React development
15
star
85

preview-dds

preview and save DDS textures from the command line
JavaScript
15
star
86

meetup-creative-coding-webgl

A WebGL experiment created for Jam3's Creative Coding Meetup on October 23rd 2019.
JavaScript
14
star
87

detect-audio-autoplay

detects whether the browser can auto-play audio
JavaScript
14
star
88

ae-threejs-multichannel-sdf

A signed distance field Effect plugin for Adobe After Effects
C
13
star
89

slot-machine-button

๐ŸŽฐ React Slot Machine Button
JavaScript
13
star
90

google-maps-image-api-url

This module will return a string which is a url to load an image from the Google Maps Image API
JavaScript
13
star
91

webgl-to-canvas2d

Convert a webgl context or webgl canvas into a 2d canvas
JavaScript
13
star
92

f1-dom

Create f1 ui with the dom
JavaScript
12
star
93

nyg

Not another yeoman generator, a simplified project generator based around prompts and events.
JavaScript
12
star
94

camera-picking-ray

creates a picking ray for a 2D/3D camera
JavaScript
11
star
95

scroll-bar-width

Detect browser scrollbar size
JavaScript
11
star
96

nyg-jam3

Second generation of the Jam3 Generator, many new features and breaking change features
JavaScript
11
star
97

awwwards-stream

scrape Awwwards data
JavaScript
11
star
98

adviser

Jam3 quality advisor. Integrates checking for best practices at Jam3
JavaScript
11
star
99

google-assistant-21-days-of-gratitude

JavaScript
11
star
100

google-assistant-greeting-cards

JavaScript
10
star