threejs-shaders

Three.js shaders - GLSL, ShaderMaterial, uniforms, custom effects. Use when creating custom visual effects, modifying vertices, writing fragment shaders, or extending built-in materials.

By cloudai-x · 10,293 installs

npx skills add cloudai-x/threejs-skills --skill threejs-shaders

Source repository · Upstream listing

Three.js Shaders Targets three.js r186. Examples use WebGLRenderer; the WebGPU and TSL section uses WebGPURenderer. Quick Start 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: 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 ). Uniforms The matching GLSL declarations — sample the texture with texture(mapValue, vUv) : 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: 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 Fresnel Use one space consistently. World space with cameraPosition : 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 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 ). 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. 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. 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 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 . 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. 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: 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. 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. Porting GLSL Rewrite GLSL functions as Fn s 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: { val