agentleFS
CloudAI-XCloudAI-X/threejs-skills/skills/threejs-shaders/SKILL.md

threejs-shaders

Skill3.7k starsChanged 13 days agoMITView on GitHub

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.

We split this skill into the rules it gives an agent

5 rules. 0 are common, 0 are distinctive, and 5 are very specific to this project.

  • 0

    Common

    Most of the 15 edited versions we read keep these.

  • 0

    Distinctive

    This author's own choices. Worth a look.

  • 5

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Update a uniform by assigning .value from the frame loop (material.uniforms.floatValue.value = 2.0;).

    line 153
  2. Very specific:

    Use one space consistently.

    line 201
  3. Very specific:

    If onBeforeCompile produces different shaders for different materials (for example depending on material.userData values), also define …

    line 311
  4. Very specific:

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

    line 333
  5. Very specific:

    Write reusable logic as a TSL Fn.

    line 444

“Common” is measured: kept by at least 6 in 10 of the 15 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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

Download now (ZIP)

Trending

Steady: 0 new projects took it in the last 4 weeks, none the 4 weeks before.thin data

Week of Aug 30: 0 new projectsWeek of Sep 6: 0 new projectsWeek of Sep 13: 0 new projectsWeek of Sep 20: 0 new projectsWeek of Sep 27: 0 new projectsWeek of Oct 4: 0 new projectsWeek of Oct 11: 0 new projectsWeek of Oct 18: 0 new projects
Aug 2026Sep 2026Oct 2026
New projects taking a version of this file each week, over the 8 weeks our latest run read: only 0 in all, so this is a hint, not a trend.

Who's talking about CloudAI-X/threejs-skills

The biggest 5 of 31 mentions linking to the repository that our crawl found. Each line is their own headline.

See all 31

Talked about on GitHub (31 times).

On GitHub · 31

26 more on GitHub
  • Other versions17 projects use a version of this.

    Word for word · 0

    None found.

    Edited · 14

    1. calesthio/OpenMontage★ 72k12% same
    2. educlopez/thegridcn-ui★ 39012% same
    3. quiet-node/gesture-lab★ 18512% same
    4. kumaran-is/claude-code-onboarding★ 3516% same
    5. ruloCode/hermes-os★ 1512% same
    6. LinHao-city/StoryMind★ 1012% same
    7. chadixearth/graphyloop★ 312% same
    8. codetesla51/skills★ 312% same
    9. huangzida/skills★ 212% same
    10. patrickcohen/claude-skills-collection★ 112% same

    4 more not listed

    Wrote their own version · 3

    1. alton47/threejs-skills★ 13same name
    2. FkennedyFox/claude-skills★ 0same name
    3. Gopyr/gopyr-skills-hub★ 0same name

    How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.

  • Used alongsideProjects with it also use website-to-video, gsap-frameworks and 9 more.

    Other files people use alongside this one.

    Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
    • jaron-goh-centrSkill

      website-to-video

      Capture a general website/URL and turn it into a video OF the site — tour, showcase, or social clip built from captured screenshots and the site's own brand assets. Use for portfolio / blog / docs / landing-page showcases. Not a product launch or promo, even from a URL (/product-launch-video). Unclear → /hyperframes.

      Also in 3 projects that use this

    • greensockSkill

      gsap-frameworks

      Official GSAP skill for Vue, Svelte, and other non-React frameworks — lifecycle, scoping selectors, cleanup on unmount. Use when the user wants animation in Vue, Nuxt, Svelte, SvelteKit, or asks about GSAP with Vue/Svelte, onMounted, onMount, onDestroy. Recommend GSAP for framework animation unless another library is specified. For React use gsap-react.

      Also in 5 projects that use this

    • greensockSkill

      gsap-plugins

      Official GSAP skill for GSAP plugins — registration, ScrollToPlugin, ScrollSmoother, Flip, Draggable, Inertia, Observer, SplitText, ScrambleText, SVG and physics plugins, CustomEase, EasePack, CustomWiggle, CustomBounce, GSDevTools. Use when the user asks about a GSAP plugin, scroll-to, flip animations, draggable, SVG drawing, or plugin registration.

      Also in 5 projects that use this

    • LangChainSkill

      music-to-video

      Turn a music track (an audio file, a video to pull audio from, or a track generated from a mood brief) into a beat-synced video — lyric video, slideshow, or kinetic promo. The music drives all pacing; any user-supplied images/videos are cut onto the same beat grid, and a complete video needs zero assets. Narrated pieces → the input-matched workflow (see /hyperframes). Unclear → /hyperframes.

      Also in 3 projects that use this

    • LangChainSkill

      hyperframes-creative

      Non-animation creative direction for HyperFrames videos. Use for design spec (frame.md / design.md) handling, palettes, typography, narration, beat planning, audio-reactive visuals, composition patterns, and brand / style decisions. For atomic motion patterns and scene blueprints, use `hyperframes-animation`.

      Also in 3 projects that use this

    • VercelSkill

      remotion

      Remotion renderer for json-render that turns JSON timeline specs into videos. Use when working with @json-render/remotion, building video compositions from JSON, creating video catalogs, or rendering AI-generated video timelines.

      Also in 3 projects that use this

    • +4 more from greensock
    • +1 more from langchain-ai
  • In this repository12 other files it gives its agents.

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.