Three Js Shader example

How it works

A plane drawn by a GLSL fragment shader, with a panel for its uniforms. Seven shaders, one canvas.

The class in src/js/handler/three-shaders is the plain Three.js (three on npm) setup of the cube page with the mesh swapped for a 2×2 plane and a ShaderMaterial: a vertex shader shared by every variation, which passes the UV coordinates on, and a fragment shader chosen by name. Each variation is one file in variations/ with its GLSL, the initial values of its uniforms, and a function that adds the controls for them to a lil-gui panel, so a control changes a uniform and the next frame shows it.

The panel is appended inside the frame, top right. A shader that animates (reveal, with Motion on) declares needsTime and the loop advances its uTime uniform by 0.016 per frame. The material is transparent, so shaders that write an alpha below 1 let the page show through.

The markup

One element with the js--three-webgl-shader hook, a component class that gives it a height, and data-shader-type with the shader to start with. The select is optional: if the frame contains one with the js--three-webgl-shader-select hook, the handler passes it to the class and every change swaps the shader on the same canvas. The option values are the shader types; the markup of the select is the form select component.

<!-- The element is the frame; the class appends a canvas that fills it. data-shader-type picks the first shader -->
<div class="c--shader-a js--three-webgl-shader" data-shader-type="grid">
  <!-- Optional: a select inside the frame switches the shader on the same canvas -->
  <div class="c--shader-a__hd">
    <div class="c--form-group-a">
      <label class="c--label-a" for="shader-type">Shader</label>
      <div class="c--form-select-a">
        <select id="shader-type" class="c--form-select-a__item js--three-webgl-shader-select">
          <option value="grid">Grid</option>
          <option value="gradient">Gradient</option>
          <option value="circle">Circle</option>
          <option value="stripes">Stripes</option>
          <option value="reveal">Reveal</option>
          <option value="wiggle">Wiggle</option>
          <option value="noise">Noise</option>
        </select>
      </div>
    </div>
  </div>
</div>

The handler

Handler.js looks for the select inside the element and passes it along; null when there is none, and the class then draws the one shader of the attribute:

this.config = ({ element }) => ({
    element,
    selectElement: element.querySelector(".js--three-webgl-shader-select"),
});

The switch is setShader in ThreeGLShader.js. It disposes the current material and the panel, builds both again from the new variation and keeps the renderer, the scene and the canvas. The select calls it on change, and any other code can call it on the instance:

/**
 * Switches the shader drawn on the plane. The old material and its GUI are disposed and
 * rebuilt from the new variation; the renderer, scene and canvas stay.
 * @param {String} type - A key of SHADERS: grid, gradient, circle, stripes, reveal, wiggle or noise
 */
setShader(type) {
  if (!this.mesh || !SHADERS[type] || type === this.shaderType) return;

  this.shaderType = type;
  this.shaderConfig = SHADERS[type];
  this.debugObject = { ...this.shaderConfig.debug };
  this.DOM.element.dataset.shaderType = type;

  if (this.material) this.material.dispose();
  this.material = this._createMaterial();
  this.mesh.material = this.material;

  if (this.gui) this.gui.destroy();
  this._setupGUI();

  if (this.DOM.selectElement && this.DOM.selectElement.value !== type) {
    this.DOM.selectElement.value = type;
  }
}

This is a variation. fragment is the GLSL, uniforms returns fresh uniform objects for every material, debug holds the values the panel edits and gui wires each control to its uniform:

export const gridShader = {
  fragment: /* glsl */ `
    varying vec2 vUv;
    uniform vec3 uColor;
    uniform float uGridSize;

    float random(vec2 st) {
      return fract(sin(dot(st, vec2(12.9898, 78.233))) * 43758.5453);
    }

    void main() {
      vec2 grid = floor(vUv * uGridSize);
      float strength = random(grid);
      gl_FragColor = vec4(uColor * strength, 1.0);
    }
  `,
  uniforms: () => ({
    uColor: { value: new THREE.Color("#e94560") },
    uGridSize: { value: 10 }
  }),
  debug: {
    color: "#e94560",
    gridSize: 10
  },
  gui: (gui, debug, material) => {
    gui.addColor(debug, "color").name("Color").onChange((v) => {
      material.uniforms.uColor.value.set(v);
    });
    gui.add(debug, "gridSize", 2, 50, 1).name("Grid Size").onChange((v) => {
      material.uniforms.uGridSize.value = v;
    });
  }
};

What the framework does for you

  • Lazy loading. An element is only built when it gets within 30px of the viewport (Boostify scroll). ThreeGLShader.js, with three, lil-gui and the seven variations, is imported through the Manager on first use.
  • Page transitions. Before swup replaces the content every instance is destroyed: the frame loop is cancelled, the resize and select listeners removed, the panel destroyed, the geometry and material disposed, the scene cleared, the renderer disposed and its canvas taken out of the DOM.
  • One renderer per instance. Every element gets its own WebGLRenderer and WebGL context. That is why this page has one canvas and a select rather than one canvas per shader: browsers allow a limited number of live contexts, and switching the material costs nothing.

Shader

One canvas. Pick a shader in the select; tune it in the panel.

Attributes shader-type="grid"js--three-webgl-shader-select

Starts on grid, as the attribute says. Changing the select swaps the fragment shader and rebuilds the panel for its uniforms; the values of the previous shader are not kept, every variation starts from its defaults. The panel edits the uniforms of the material directly, so there is nothing to apply.

<!-- The element is the frame; the class appends a canvas that fills it. data-shader-type picks the first shader -->
<div class="c--shader-a js--three-webgl-shader" data-shader-type="grid">
  <!-- Optional: a select inside the frame switches the shader on the same canvas -->
  <div class="c--shader-a__hd">
    <div class="c--form-group-a">
      <label class="c--label-a" for="shader-type">Shader</label>
      <div class="c--form-select-a">
        <select id="shader-type" class="c--form-select-a__item js--three-webgl-shader-select">
          <option value="grid">Grid</option>
          <option value="gradient">Gradient</option>
          <option value="circle">Circle</option>
          <option value="stripes">Stripes</option>
          <option value="reveal">Reveal</option>
          <option value="wiggle">Wiggle</option>
          <option value="noise">Noise</option>
        </select>
      </div>
    </div>
  </div>
</div>
Live example
shader-type="grid"js--three-webgl-shader-select

Options reference

One attribute, one optional hook, and seven shaders with their panel controls.

Reference

HTML

Attribute or hookValuesWhat it does
data-shader-typeone of the types below, default gridThe shader drawn first. A missing or unknown value falls back to grid. The class writes the current type back to it on every switch
js--three-webgl-shader-selecta <select> inside the frame, optionalSwitches the shader on change; every option value is a type. The class sets it to the current type when it starts

Shaders

TypeWhat it drawsPanel controls
gridRandom grid patternColor, Grid Size
gradientVertical gradient between two colorsColor A, Color B
circleA circle cut out of a planeColor, Radius, Opacity
stripesVertical stripesColor, Frequency
revealPerlin noise with a directional wipe and a progress control; animated when Motion is onColor, Scale, Progress, Feather, Direction, Dir. Strength, Motion, Motion Speed
wiggleA wavy ringColor, Frequency, Amplitude, Opacity
noisePerlin noise patternColor, Scale, Opacity

Class

In the classValueWhat it does
setShader(type)methodSwitches the shader from code. Ignores an unknown type and the current one
PerspectiveCamera(75, …), position.z = 2fov 75With the 2×2 plane, the plane fills the height of the frame
uTime += 0.016per frameThe speed of animated shaders, in animate
SHADERS in variations/index.jsmap of type to variationAdd a file to variations/ and an entry here to add a shader

The size of the canvas is the size of the element. c--shader-a is one example of sizing, with its __hd element placing the select over the canvas; js--three-webgl-shader and js--three-webgl-shader-select are hooks and carry no styles.