UI Skill

threejs-shaders

"Custom GLSL vertex and fragment shaders: uniforms, varyings, shader chunks, extending built-in materials, instanced attributes, plus the same effects as node materials with TSL on WebGPU. Use when writing a shader, a custom material or effect, moving vertices in a shader, or porting GLSL to TSL."

Path: cloudai-x/threejs-shadersStars: 0Author: cloudai-x更新于:2026/05/01 10:05

目录

  1. Quick Start
  2. ShaderMaterial vs RawShaderMaterial
  3. Uniforms
  4. Output Color and Tone Mapping
  5. Common Shader Patterns
  6. Vertex Displacement
  7. Fresnel
  8. Dissolve
  9. Instanced Meshes and Custom Attributes
  10. Extending Built-in Materials
  11. Shader Chunks
  12. Debugging
  13. WebGPU and TSL
  14. Node materials
  15. Uniforms and time
  16. Textures and discard
  17. Vertex displacement
  18. Reusable logic
  19. Porting GLSL
  20. Debugging
  21. Common Mistakes
  22. See Also

Three.js Shaders

Targets three.js r186. Examples use WebGLRenderer; the WebGPU and TSL section uses WebGPURenderer.

Quick Start

import * as THREE from "three";

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 0, 2);

const material = new THREE.ShaderMaterial({
  uniforms: {
    time: { value: 0 },
    color: { value: new THREE.Color(0x0066ff) }, // hex converted to the linear working space
  },
  vertexShader: /* glsl */ `
    varying vec2 vUv;

    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: /* glsl */ `
    uniform float time;
    uniform vec3 color;
    varying vec2 vUv;

    void main() {
      float wave = sin(vUv.x * 10.0 + time * 2.0) * 0.5 + 0.5;
      gl_FragColor = vec4(color * wave, 1.0);
      #include <tonemapping_fragment>
      #include <colorspace_fragment>
    }
  `,
});

const mesh = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material);
scene.add(mesh);

const timer = new THREE.Timer();
timer.connect(document);

renderer.setAnimationLoop(animate);

function animate() {
  timer.update();
  material.uniforms.time.value = timer.getElapsed();
  renderer.render(scene, camera);
}

ShaderMaterial vs RawShaderMaterial

Every ShaderMaterial compiles as GLSL ES 3.00 — three.js always prepends #version 300 es (WebGL 1 support is gone). texture(), textureLod(), textureSize() and dFdx/fwidth work with no flags; texture2D, textureCube, attribute, varying and gl_FragColor are #define aliases, so most GLSL ES 1.00 shaders compile unchanged (not ones that name a variable texture, write gl_FragData or keep an #extension line, which fails because three.js puts declarations before your source; delete it, the features are core). Leave glslVersion unset: setting THREE.GLSL3 only removes the gl_FragColor alias, forcing you to declare your own output variable. ShaderMaterial injects the usual declarations:

const material = new THREE.ShaderMaterial({
  vertexShader: /* glsl */ `
    // Declared for you: uniforms modelMatrix, modelViewMatrix, projectionMatrix,
    // viewMatrix, normalMatrix, cameraPosition; attributes position, normal, uv,
    // instanceMatrix (InstancedMesh) and color (vertexColors: true)

    varying vec3 vNormal;

    void main() {
      vNormal = normalize(normalMatrix * normal); // view-space normal
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: /* glsl */ `
    varying vec3 vNormal;

    void main() {
      gl_FragColor = vec4(vNormal * 0.5 + 0.5, 1.0);
      #include <tonemapping_fragment>
      #include <colorspace_fragment>
    }
  `,
});

RawShaderMaterial gets only #define SHADER_TYPE, #define SHADER_NAME and your defines (plus #version 300 es once glslVersion is set): no declarations and no precision statement. Always set glslVersion: THREE.GLSL3 (without it the shader is GLSL ES 1.00, which has no texture()), write precision highp float;, use in/out and declare your own output. Declare the matrices you need in the shader and never pass them through uniforms: the renderer sets modelMatrix, modelViewMatrix, projectionMatrix, viewMatrix, normalMatrix and cameraPosition for any program that declares them, and a same-named entry in uniforms is either overwritten every draw (modelMatrix, modelViewMatrix, normalMatrix) or replaces the camera's value (projectionMatrix, viewMatrix, cameraPosition).

const material = new THREE.RawShaderMaterial({
  glslVersion: THREE.GLSL3,
  vertexShader: /* glsl */ `
    precision highp float;

    in vec3 position;

    uniform mat4 modelViewMatrix;
    uniform mat4 projectionMatrix;

    void main() {
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: /* glsl */ `
    precision highp float;

    out highp vec4 fragColor;

    void main() {
      fragColor = vec4(1.0, 0.0, 0.0, 1.0);
    }
  `,
});

Uniforms

const texture = await new THREE.TextureLoader().loadAsync("textures/diffuse.jpg");
texture.colorSpace = THREE.SRGBColorSpace; // a color image: decoded to linear when sampled

const material = new THREE.ShaderMaterial({
  uniforms: {
    floatValue: { value: 1.5 },
    vec3Value: { value: new THREE.Vector3(1, 2, 3) },
    colorValue: { value: new THREE.Color(0xff0000) }, // hex converted to the linear working space
    mat4Value: { value: new THREE.Matrix4() },
    mapValue: { value: texture },
  },
  // vertexShader and fragmentShader as usual
});

The matching GLSL declarations — sample the texture with texture(mapValue, vUv):

uniform float floatValue;
uniform vec3 vec3Value;
uniform vec3 colorValue;    // a THREE.Color arrives as vec3
uniform mat4 mat4Value;
uniform sampler2D mapValue;

Update a uniform by assigning .value from the frame loop (material.uniforms.floatValue.value = 2.0;); vectors and colors mutate in place (material.uniforms.vec3Value.value.set(1, 2, 3);). Never name a uniform position, normal, uv, modelViewMatrix or cameraPosition — three.js already declares those in the vertex shader, and the redeclaration is a compile error.

Output Color and Tone Mapping

THREE.Color converts hex input from sRGB to the linear working color space, so a color uniform holds linear values. A ShaderMaterial writes its output as-is: tone mapping and the output color space conversion happen only if the fragment shader ends with the two includes:

gl_FragColor = vec4(color, 1.0);
#include <tonemapping_fragment>
#include <colorspace_fragment>

tonemapping_fragment applies renderer.toneMapping (a no-op with NoToneMapping); colorspace_fragment encodes to renderer.outputColorSpace (SRGBColorSpace by default). Without them, mid-tone colors render darker than the same hex on a built-in material. Both chunks write gl_FragColor, so they require glslVersion unset. An 8-bit RGBA texture with texture.colorSpace = THREE.SRGBColorSpace decodes to linear on sampling, so sampled colors combine correctly with the includes in place. A VideoTexture does not: three.js uploads it without sRGB decoding and decodes it only inside built-in materials, so decode it yourself with sRGBTransferEOTF(texture(videoMap, vUv)) (the ShaderMaterial prefix declares that function). Rendering into a render target, as inside an EffectComposer, both includes leave the color linear and OutputPass encodes it at the end.

Common Shader Patterns

Vertex Displacement

const material = new THREE.ShaderMaterial({
  uniforms: {
    time: { value: 0 },
    amplitude: { value: 0.2 },
  },
  vertexShader: /* glsl */ `
    uniform float time;
    uniform float amplitude;

    void main() {
      vec3 pos = position;
      pos.z += sin(pos.x * 5.0 + time) * amplitude;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
    }
  `,
  fragmentShader: /* glsl */ `
    void main() {
      gl_FragColor = vec4(0.5, 0.8, 1.0, 1.0);
      #include <tonemapping_fragment>
      #include <colorspace_fragment>
    }
  `,
});

const mesh = new THREE.Mesh(new THREE.PlaneGeometry(2, 2, 64, 64), material); // displacement needs subdivisions

Fresnel

Use one space consistently. World space with cameraPosition:

const material = new THREE.ShaderMaterial({
  vertexShader: /* glsl */ `
    varying vec3 vWorldNormal;
    varying vec3 vWorldPosition;

    void main() {
      vWorldNormal = normalize(mat3(modelMatrix) * normal); // world space, uniform scale assumed
      vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: /* glsl */ `
    varying vec3 vWorldNormal;
    varying vec3 vWorldPosition;

    void main() {
      vec3 viewDirection = normalize(cameraPosition - vWorldPosition);
      float fresnel = pow(1.0 - max(dot(viewDirection, vWorldNormal), 0.0), 3.0);

      gl_FragColor = vec4(mix(vec3(0.0, 0.0, 0.5), vec3(0.5, 0.8, 1.0), fresnel), 1.0);
      #include <tonemapping_fragment>
      #include <colorspace_fragment>
    }
  `,
});

normalMatrix * normal is view space — pair it with the view-space direction to the camera, never with cameraPosition. For rim lighting keep both in view space: in the vertex shader vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); vViewDir = -mvPosition.xyz; vNormal = normalMatrix * normal;, in the fragment shader float rim = pow(1.0 - max(dot(normalize(vViewDir), normalize(vNormal)), 0.0), 4.0);.

Dissolve

uniform float progress;
uniform sampler2D noiseMap;
varying vec2 vUv;

void main() {
  float noise = texture(noiseMap, vUv).r;
  if (noise < progress) discard;

  float edge = smoothstep(progress, progress + 0.1, noise);
  gl_FragColor = vec4(mix(vec3(1.0, 0.5, 0.0), vec3(0.5), edge), 1.0);
  #include <tonemapping_fragment>
  #include <colorspace_fragment>
}

Instanced Meshes and Custom Attributes

Three.js declares position, normal, uv, instanceMatrix (for an InstancedMesh), instanceColor (once setColorAt has been called) and color (with vertexColors: true). Anything else — a custom per-vertex attribute, uv1 to uv3, tangent — you declare yourself with attribute (the alias for in).

const instanceCount = 100;
const geometry = new THREE.PlaneGeometry(0.2, 0.2);
const offsets = new Float32Array(instanceCount * 3);
// fill offsets with one vec3 per instance
geometry.setAttribute("aOffset", new THREE.InstancedBufferAttribute(offsets, 3));

const material = new THREE.ShaderMaterial({
  vertexShader: /* glsl */ `
    attribute vec3 aOffset;

    void main() {
      // instanceMatrix is declared by three.js; the shader must apply it
      gl_Position = projectionMatrix * modelViewMatrix * instanceMatrix * vec4(position + aOffset, 1.0);
    }
  `,
  fragmentShader: /* glsl */ `
    void main() {
      gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
    }
  `,
});

const mesh = new THREE.InstancedMesh(geometry, material, instanceCount);
mesh.frustumCulled = false; // the bounds come from instanceMatrix and ignore aOffset

For a per-vertex attribute use geometry.setAttribute("aOffset", new THREE.BufferAttribute(new Float32Array(geometry.attributes.position.count * 3), 3)); — the shader declaration is the same.

Extending Built-in Materials

onBeforeCompile rewrites the shader strings of a built-in material. It runs once per program, before includes are resolved.

const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });

material.onBeforeCompile = (shader) => {
  shader.uniforms.time = { value: 0 };
  material.userData.shader = shader; // keep the reference for updates

  shader.vertexShader = "uniform float time;\n" + shader.vertexShader;
  shader.vertexShader = shader.vertexShader.replace(
    "#include <begin_vertex>",
    `
    #include <begin_vertex>
    transformed.y += sin(position.x * 10.0 + time) * 0.1;
    `,
  );
};

// in the frame loop, with timer.update() first as in the Quick Start:
// if (material.userData.shader) material.userData.shader.uniforms.time.value = timer.getElapsed();

Injection points in the built-in shaders: vertex #include <begin_vertex> (declares vec3 transformed = vec3(position)), #include <beginnormal_vertex> (vec3 objectNormal) and #include <project_vertex> (sets gl_Position); fragment #include <color_fragment> (diffuseColor after map and vertex colors), #include <opaque_fragment> (writes gl_FragColor before tone mapping) and #include <fog_fragment> (after tone mapping, color space and fog).

If onBeforeCompile produces different shaders for different materials (for example depending on material.userData values), also define material.customProgramCacheKey() returning a distinct string per variant, or the renderer reuses one cached program for all of them.

Shader Chunks

#include <chunk> works in ShaderMaterial and RawShaderMaterial source and reuses three.js helper functions; perspectiveDepthToViewZ below comes from the packing chunk.

const fragmentShader = /* glsl */ `
  #include <packing>

  uniform sampler2D depthTexture;
  uniform float cameraNear; // set from camera.near
  uniform float cameraFar; // set from camera.far
  varying vec2 vUv;

  void main() {
    float viewZ = perspectiveDepthToViewZ(texture(depthTexture, vUv).r, cameraNear, cameraFar);
    gl_FragColor = vec4(vec3(-viewZ / cameraFar), 1.0);
  }
`;

Only use chunk keys that exist in this release (log Object.keys(THREE.ShaderChunk) to check): there is no output_fragment chunk — the final output chunk is opaque_fragment.

Debugging

// While renderer.debug.checkShaderErrors is true (the default), a failed program logs each failing shader's errors
// with the numbered source lines around the first one. A handler replaces that report and runs only on a compile or link failure:
renderer.debug.onShaderError = (gl, program, vertexShader, fragmentShader) => {
  console.error(gl.getProgramInfoLog(program));
  for (const shader of [vertexShader, fragmentShader]) {
    const log = gl.getShaderInfoLog(shader);
    if (log) console.error(log, gl.getShaderSource(shader)); // the fully assembled shader
  }
};

Debug visually by writing the value under test to the output: gl_FragColor = vec4(vUv, 0.0, 1.0); for UVs, gl_FragColor = vec4(vNormal * 0.5 + 0.5, 1.0); for normals.

WebGPU and TSL

ShaderMaterial, RawShaderMaterial and onBeforeCompile run only under WebGLRenderer. Under WebGPURenderer a ShaderMaterial logs Material "ShaderMaterial" is not compatible and falls back to a plain node material, and onBeforeCompile is never called — build the effect as a node material with TSL instead. WebGPURenderer falls back to WebGL 2 by itself when WebGPU is unavailable, so one TSL code path covers both backends.

Node materials

An unlit effect uses MeshBasicNodeMaterial; MeshStandardNodeMaterial is lit and renders black without lights or an environment. A ShaderMaterial maps to MeshBasicNodeMaterial (unlit) or to the node slots of MeshStandardNodeMaterial (lit); replacing the whole fragment output uses fragmentNode. The familiar slots exist as node properties: colorNode, opacityNode, emissiveNode, normalNode, positionNode.

import * as THREE from "three/webgpu";
import { color, mix, sin, time, uv } from "three/tsl";

const renderer = new THREE.WebGPURenderer({ antialias: true });
await renderer.init(); // before any other use of the renderer
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x111827); // the WebGPU canvas is transparent by default
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 0, 2);

const material = new THREE.MeshBasicNodeMaterial();
material.colorNode = mix(color(0x004466), color(0x66ddff), sin(uv().x.mul(20).add(time)).mul(0.5).add(0.5));

scene.add(new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material));

renderer.setAnimationLoop(animate);

function animate() {
  renderer.render(scene, camera);
}

Uniforms and time

uniform(value) creates an updatable uniform; time is a built-in node that updates itself every frame and replaces a hand-fed time uniform.

import * as THREE from "three/webgpu";
import { abs, mx_fractal_noise_float, time, uniform, uv, vec3 } from "three/tsl";

const material = new THREE.MeshBasicNodeMaterial();

const amplitude = uniform(0.5);
material.colorNode = vec3(abs(mx_fractal_noise_float(uv().mul(10).add(time.mul(0.2)))).mul(amplitude));

// update from JS without touching the node graph:
amplitude.value = 1.0;

Textures and discard

texture(map, uvNode) samples a texture (texture(map) uses the mesh UVs); .discard() on a condition drops the fragment, as discard does in GLSL. The Dissolve shader above as TSL:

import * as THREE from "three/webgpu";
import { Fn, color, mix, smoothstep, texture, uniform, uv } from "three/tsl";

const noiseMap = await new THREE.TextureLoader().loadAsync("textures/noise.png"); // scalar data: keep NoColorSpace
const progress = uniform(0); // animate progress.value from 0 to 1

const material = new THREE.MeshBasicNodeMaterial();
material.colorNode = Fn(() => {
  const n = texture(noiseMap, uv()).r;
  n.lessThan(progress).discard();
  return mix(color(1, 0.5, 0), color(0.5, 0.5, 0.5), smoothstep(progress, progress.add(0.1), n));
})();

Vertex displacement

positionNode replaces the transformed local position. Build it on positionLocal, which already carries skinning, morph targets, displacement maps, batching and InstancedMesh transforms; positionGeometry is the raw attribute, for rest-pose input only. Displacement needs a subdivided mesh.

import * as THREE from "three/webgpu";
import { normalLocal, positionLocal, sin, time } from "three/tsl";

const sun = new THREE.DirectionalLight(0xffffff, 2);
sun.position.set(1, 2, 3);
scene.add(sun, new THREE.HemisphereLight(0xffffff, 0x223344, 1)); // lit material: black without lights or scene.environment

const material = new THREE.MeshStandardNodeMaterial({ flatShading: true }); // normals from the displaced surface; vertex normals ignore positionNode
material.positionNode = positionLocal.add(normalLocal.mul(sin(positionLocal.y.mul(4).add(time))).mul(0.2));

const mesh = new THREE.Mesh(new THREE.PlaneGeometry(2, 2, 96, 96), material);
mesh.rotation.x = -Math.PI / 3; // tilt so the displacement along the normal is visible
scene.add(mesh);

Reusable logic

Write reusable logic as a TSL Fn; it compiles to both GLSL and WGSL. varying(node, "name") passes a value from the vertex stage to the fragment stage.

import * as THREE from "three/webgpu";
import { Fn, color, mix, positionWorld, sin, varying } from "three/tsl";

const heightFade = Fn(([y]) => mix(color(0x003344), color(0x88eeff), sin(y.mul(3)).mul(0.5).add(0.5)));

const material = new THREE.MeshBasicNodeMaterial();
const vWorldY = varying(positionWorld.y, "vWorldY"); // interpolated from vertex to fragment stage
material.colorNode = heightFade(vWorldY);

Porting GLSL

Rewrite GLSL functions as Fns instead of pasting shader strings: glslFn runs only on the WebGL 2 backend and wgslFn only on the WebGPU backend, so neither is portable. Compute shaders dispatch through renderer.compute() after await renderer.init().

Debugging

await renderer.debug.getShaderAsync(scene, camera, mesh) returns the generated { vertexShader, fragmentShader } for one mesh.

Common Mistakes

  • const clock = new THREE.Clock(); → const timer = new THREE.Timer(); timer.connect(document);: Clock is deprecated in r183; call timer.update() first in each frame, then timer.getElapsed() or timer.getDelta().
  • "#include <output_fragment>" → "#include <opaque_fragment>": output_fragment does not exist, so a string replace targeting it silently matches nothing.
  • fragmentShader: "uniform vec3 color;\nvoid main() {\n gl_FragColor = vec4(color, 1.0);\n}" → fragmentShader: "uniform vec3 color;\nvoid main() {\n gl_FragColor = vec4(color, 1.0);\n #include <tonemapping_fragment>\n #include <colorspace_fragment>\n}": color uniforms hold linear values and without the two includes, each on its own line, the output is never encoded, so mid-tones render darker than on built-in materials.
  • const renderer = new THREE.WebGPURenderer(); const material = new THREE.ShaderMaterial({ vertexShader, fragmentShader }); → const material = new THREE.MeshBasicNodeMaterial(); material.colorNode = color(0xff0000);: under WebGPURenderer ShaderMaterial logs "not compatible" and onBeforeCompile never runs; these APIs work only under WebGLRenderer.
  • material.positionNode = positionGeometry.add(offset) → material.positionNode = positionLocal.add(offset): positionLocal already carries skinning, morph targets, batching and instanced transforms; positionGeometry is the raw attribute.
  • import { time } from "three/nodes" → import { time } from "three/tsl": the three/nodes module does not exist; TSL names come from three/tsl, node materials from three/webgpu.
  • import { toVarying } from "three/tsl" → varying(node, "vName"): the exported function is varying(); toVarying exists only as the method form node.toVarying("vName").
  • import { uniforms, PI2 } from "three/tsl" → import { uniformArray, TWO_PI } from "three/tsl": uniformArray() was renamed from uniforms() in r168 and TWO_PI from PI2 in r181.
  • amplitude.label("amp") → amplitude.setName("amp"): setName() was renamed from label() in r179; label() still sets the name but logs a deprecation warning.
  • texture(map).uv(uvNode) → texture(map).sample(uvNode): TextureNode.sample() was renamed from uv() in r172.
  • import { screen, viewportTopLeft, directionToColor } from "three/tsl" → import { blendScreen, viewportUV, packNormalToRGB } from "three/tsl": blendScreen was renamed from screen() in r171, viewportUV from viewportTopLeft in r168 and packNormalToRGB from directionToColor() in r185.
  • rangeFog(color, near, far) → fog(color, rangeFogFactor(near, far)): three/tsl no longer exports rangeFog(); fog() takes a factor node.
  • const t = uniform(0); t.value = timer.getElapsed(); → time: the built-in time node updates itself every frame, so the uniform round-trip is unnecessary.
  • const n = glslFn(glslSource); → const n = Fn(([p]) => mx_noise_float(p));: glslFn runs only on the WebGL 2 backend and wgslFn only on the WebGPU backend; a TSL Fn compiles for both.
  • uniforms: { modelViewMatrix: { value: new THREE.Matrix4() } } → uniform mat4 modelViewMatrix;: the renderer sets the built-in matrices for any program that declares them, so a uniforms entry is overwritten every draw (modelMatrix, modelViewMatrix, normalMatrix) or replaces the camera's value (projectionMatrix, viewMatrix, cameraPosition); the GLSL declaration is for a RawShaderMaterial, since a ShaderMaterial already declares the matrices and redeclaring one fails to compile.
  • new THREE.ShaderMaterial({ transparent: true, opacity: 0.5 }) → new THREE.ShaderMaterial({ transparent: true, uniforms: { opacity: { value: 0.5 } } }): ShaderMaterial never uploads material.opacity, so declare uniform float opacity and write it to the alpha channel.
  • uniforms: { position: { value: new THREE.Vector3() } } → uniforms: { offset: { value: new THREE.Vector3() } }: position collides with the attribute three.js declares in every ShaderMaterial vertex shader.
  • new THREE.ShaderMaterial({ glslVersion: THREE.GLSL3, vertexShader, fragmentShader }) → new THREE.ShaderMaterial({ vertexShader, fragmentShader }): ShaderMaterial always compiles as GLSL ES 3.00, and GLSL3 only removes the gl_FragColor alias, so a shader that still writes gl_FragColor stops compiling.
  • new THREE.RawShaderMaterial({ vertexShader, fragmentShader }) → new THREE.RawShaderMaterial({ vertexShader, fragmentShader, glslVersion: THREE.GLSL3 }): without glslVersion a RawShaderMaterial compiles as GLSL ES 1.00, which has no texture().
  • new THREE.ShaderMaterial({ vertexShader, fragmentShader, extensions: { derivatives: true, fragDepth: true } }) → new THREE.ShaderMaterial({ vertexShader, fragmentShader }): the WebGL 1 extension keys were removed in r163 and are ignored; dFdx, gl_FragDepth and textureLod are core in GLSL ES 3.00, and extensions now holds only clipCullDistance and multiDraw.
  • blending: THREE.MultiplyBlending → blending: THREE.MultiplyBlending, premultipliedAlpha: true: MultiplyBlending and SubtractiveBlending set no blend function and log an error unless premultiplied alpha is on, and a ShaderMaterial with alpha below 1 must then premultiply its own output with #include <premultiplied_alpha_fragment>.
  • gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); → gl_Position = projectionMatrix * modelViewMatrix * instanceMatrix * vec4(position, 1.0);: on an InstancedMesh three.js declares instanceMatrix but the shader must apply it, or every instance draws at the mesh's own transform.
  • varying vec2 vUv1; void main() { vUv1 = uv1; } → attribute vec2 uv1; varying vec2 vUv1; void main() { vUv1 = uv1; }: three.js declares position, normal and uv for a ShaderMaterial; uv1 to uv3 and tangent need your own declaration.
  • dot(normalize(cameraPosition - vWorldPosition), normalize(normalMatrix * normal)) → dot(normalize(cameraPosition - vWorldPosition), normalize(mat3(modelMatrix) * normal)): normalMatrix produces view-space normals while cameraPosition is world space, so the term drifts as the camera rotates.
  • material.onBeforeCompile = (shader) => console.log(shader.fragmentShader); → renderer.debug.onShaderError = (gl, program, vs, fs) => console.error(gl.getShaderSource(fs));: onBeforeCompile runs before includes are resolved and the prefix is added, while onShaderError receives the assembled shaders when a program fails to compile or link.

See Also

  • threejs-materials — built-in materials and their texture maps, when a standard material already does the job
  • threejs-textures — loading textures, color spaces and UV channels for maps sampled in shaders
  • threejs-postprocessing — full-screen passes (bloom, outline) instead of per-object shaders