Custom shaders
Quick answer
This tutorial shows when material parameters are not enough and a custom GLSL shader is required. It contrasts ShaderMaterial, which injects projectionMatrix, modelViewMatrix and normalMatrix, with RawShaderMaterial, then writes a height-based color ramp and a time-driven vertex displacement using uniforms. It covers varying declarations, transparency with depthWrite and linear color values. Readers can shade terrain by elevation and animate vertices without rebuilding geometry.
Key points
- Use ShaderMaterial so projectionMatrix, modelViewMatrix and normalMatrix are injected.
- Update uniforms.uTime.value from clock.getElapsedTime on every frame.
- Match varying types exactly between the vertex and fragment stages.
Example
const vertexShader = `
varying vec3 vNormal;
varying float vHeight;
void main() {
vNormal = normalize(normalMatrix * normal);
vHeight = position.y;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragmentShader = `
uniform vec3 lowColor;
uniform vec3 highColor;
varying vec3 vNormal;
varying float vHeight;
void main() {
float t = clamp((vHeight - 20.0) / 60.0, 0.0, 1.0);
vec3 base = mix(lowColor, highColor, t);
float light = 0.45 + 0.55 * max(dot(vNormal, vec3(0.0, 1.0, 0.0)), 0.0);
gl_FragColor = vec4(base * light, 1.0);
}
`;
const terrainMaterial = new THREE.ShaderMaterial({
vertexShader: vertexShader,
fragmentShader: fragmentShader,
uniforms: {
lowColor: { value: new THREE.Color('#1f5fd0') },
highColor: { value: new THREE.Color('#d0453a') }
},
side: THREE.DoubleSide
});const shaderMaterial = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
uScale: { value: 0.15 }
},
vertexShader: `
uniform float uTime;
uniform float uScale;
varying float vWave;
void main() {
vWave = sin(position.x * uScale + uTime) * 0.5 + 0.5;
vec3 displaced = position;
displaced.y += vWave * 2.0;
gl_Position = projectionMatrix * modelViewMatrix * vec4(displaced, 1.0);
}
`,
fragmentShader: `
varying float vWave;
void main() {
gl_FragColor = vec4(0.12, 0.40, 0.85, 0.6 + vWave * 0.4);
}
`,
transparent: true,
depthWrite: false
});
const clock = new THREE.Clock();
renderer.setAnimationLoop(function () {
shaderMaterial.uniforms.uTime.value = clock.getElapsedTime();
renderer.render(scene, camera);
});
Notes
- Compile errors only appear in the console log and are never thrown as exceptions.
- Writing a hex literal into GLSL skips THREE.Color's sRGB conversion and looks too bright.
English edition written for Atlaslog. Code is taken from the original Chinese page and runs unchanged.
Atlaslog (2026). Custom shaders. https://www.jingweizhi.com/en/three/tutorials/custom-shader.html (accessed 2026-11-03).