Three.js ShaderMaterial — Custom Shader Effects and Techniques
In this tutorial, you will learn about Three.js ShaderMaterial. We cover key concepts, practical examples, and best practices to help you master this topic.
Three.js ShaderMaterial lets you create custom rendering effects by writing GLSL shaders while retaining Three.js coordinate transforms and uniform management.
What You'll Learn
By the end of this guide, you will build custom ShaderMaterial effects including gradient coloring, displacement mapping, fresnel glow, animated textures, and materials that respond to Three.js lights.
Why ShaderMaterial Matters
While MeshStandardMaterial handles 90% of use cases, the remaining 10% requires custom shader logic. Durga Antivirus Pro uses ShaderMaterial for its threat scanning animation — a sweeping beam effect that reveals hidden network nodes, created entirely in the fragment shader.
flowchart LR
A[JavaScript Uniforms] --> B[Vertex Shader]
A --> C[Fragment Shader]
D[Geometry Attributes] --> B
E[Texture Samplers] --> C
B --> C
C --> F[Framebuffer]
F --> G[Screen]
Gradient ShaderMaterial
var material = new THREE.ShaderMaterial({
uniforms: {
uColorA: { value: new THREE.Color(0xff0066) },
uColorB: { value: new THREE.Color(0x00ffcc) }
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform vec3 uColorA;
uniform vec3 uColorB;
varying vec2 vUv;
void main() {
vec3 color = mix(uColorA, uColorB, vUv.y);
gl_FragColor = vec4(color, 1.0);
}
`
});
Expected output: A mesh with a smooth vertical gradient from pink at the bottom to cyan at the top.
Fresnel Glow Effect
A fresnel effect makes the edges of an object glow, like a force field.
var material = new THREE.ShaderMaterial({
uniforms: {
uColor: { value: new THREE.Color(0x00ccff) },
uIntensity: { value: 1.5 }
},
vertexShader: `
varying vec3 vNormal;
varying vec3 vViewDir;
void main() {
vec4 worldPos = modelMatrix * vec4(position, 1.0);
vNormal = normalize(normalMatrix * normal);
vViewDir = normalize(cameraPosition - worldPos.xyz);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform vec3 uColor;
uniform float uIntensity;
varying vec3 vNormal;
varying vec3 vViewDir;
void main() {
float fresnel = 1.0 - dot(vNormal, vViewDir);
fresnel = pow(fresnel, 3.0) * uIntensity;
gl_FragColor = vec4(uColor, fresnel);
}
`,
transparent: true,
side: THREE.DoubleSide
});
Expected output: A sphere with a cyan glow concentrated at the edges, fading toward the center, with transparency.
Dissolve Effect
A dissolve effect makes an object disintegrate over time using a noise texture.
uniform sampler2D uNoiseTex;
uniform float uProgress;
uniform vec3 uEdgeColor;
varying vec2 vUv;
void main() {
vec4 texColor = texture2D(uNoiseTex, vUv);
float cutoff = 1.0 - uProgress;
if (texColor.r < cutoff) discard;
float edge = smoothstep(cutoff - 0.1, cutoff, texColor.r);
vec3 finalColor = mix(uEdgeColor, vec3(1.0), edge);
gl_FragColor = vec4(finalColor, 1.0);
}
Expected output: An object that progressively disappears from random points, with a bright edge glow at the dissolve boundary.
Combining With Three.js Lights
To make ShaderMaterial respond to lights, include the chunk-based lighting:
var material = new THREE.ShaderMaterial({
uniforms: {
uColor: { value: new THREE.Color(0x4488ff) },
uRoughness: { value: 0.3 }
},
vertexShader: `
varying vec3 vNormal;
varying vec3 vViewDir;
void main() {
vec4 worldPos = modelMatrix * vec4(position, 1.0);
vNormal = normalize(normalMatrix * normal);
vViewDir = normalize(cameraPosition - worldPos.xyz);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform vec3 uColor;
uniform float uRoughness;
varying vec3 vNormal;
varying vec3 vViewDir;
void main() {
vec3 normal = normalize(vNormal);
vec3 viewDir = normalize(vViewDir);
vec3 lightDir = normalize(vec3(1.0, 2.0, 1.0));
float diff = max(dot(normal, lightDir), 0.0);
vec3 halfDir = normalize(lightDir + viewDir);
float spec = pow(max(dot(normal, halfDir), 0.0), 32.0);
vec3 ambient = uColor * 0.2;
vec3 diffuse = uColor * diff * 0.8;
vec3 specular = vec3(1.0) * spec * 0.3;
gl_FragColor = vec4(ambient + diffuse + specular, 1.0);
}
`
});
Expected output: A mesh with realistic directional lighting, diffuse shading, and specular highlights, matching Three.js standard material appearance.
Common Mistakes
1. Copying Shader Code Without Adjusting
Three.js provides built-in uniforms like modelMatrix, viewMatrix, projectionMatrix, normalMatrix, and cameraPosition. Use these exact names.
2. Forgetting transparent: true on Transparent Shaders
Setting alpha in gl_FragColor to less than 1.0 requires transparent: true in the material options.
3. Not Setting side: THREE.DoubleSide
Single-sided materials cull back faces. For effects like fresnel on a sphere, enable double-sided.
4. Mixing Up uv and position Coordinates
uv is 0-1 texture coordinates. position is 3D vertex positions. They are not interchangeable.
5. Modifying Uniforms Without needsUpdate
Changing uniform structure at runtime requires material.uniformsNeedUpdate = true to re-upload uniform data.
Practice Questions
Q1: How do you make a ShaderMaterial respond to Three.js lights?
A: Manually compute lighting in the fragment shader using normal, cameraPosition, and light direction vectors, or use Three.js shader chunks.
Q2: What is the fresnel effect?
A: A phenomenon where surfaces are more reflective at grazing angles. In shaders, implemented as 1.0 - dot(normal, viewDir), raised to a power.
Q3: How do you create a dissolve animation? A: Sample a noise texture, compare against a progress threshold, discard fragments above the threshold, and add an edge glow with smoothstep.
Q4: Why do you need transparent: true for some shaders?
A: Without it, Three.js ignores the alpha channel from gl_FragColor, rendering the material fully opaque regardless of alpha values.
Q5: What does normalMatrix do?
A: It transforms normals from object space to view space, accounting for non-uniform scaling. Essential for correct lighting.
Challenge: Create a ShaderMaterial that simulates a holographic projection effect — semi-transparent, with horizontal scan lines, color shift at edges, and slight flickering intensity.
FAQ
Try It Yourself — Hologram Shader
Build a complete page with a ShaderMaterial implementing a hologram effect on a rotating 3D model. Include scan lines, edge glow, and slight color shift.
<!DOCTYPE html>
<html>
<head>
<title>Hologram Shader</title>
<style>
body { margin: 0; overflow: hidden; background: #0a0a1a; }
</style>
</head>
<body>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { RGBELoader } from "three/addons/loaders/RGBELoader.js";
var scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a1a);
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(3, 2, 5);
var renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
var controls = new OrbitControls(camera, renderer.domElement);
var uniforms = {
uTime: { value: 0 },
uColor: { value: new THREE.Color(0x00ccff) },
uIntensity: { value: 1.5 }
};
var vertexShader = `
varying vec3 vNormal;
varying vec3 vViewDir;
varying vec2 vUv;
void main() {
vUv = uv;
vec4 worldPos = modelMatrix * vec4(position, 1.0);
vNormal = normalize(normalMatrix * normal);
vViewDir = normalize(cameraPosition - worldPos.xyz);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
var fragmentShader = `
uniform vec3 uColor;
uniform float uTime;
uniform float uIntensity;
varying vec3 vNormal;
varying vec3 vViewDir;
varying vec2 vUv;
void main() {
float fresnel = 1.0 - abs(dot(normalize(vNormal), normalize(vViewDir)));
fresnel = pow(fresnel, 2.5) * uIntensity;
float scanLine = sin(vUv.y * 80.0 + uTime * 5.0) * 0.5 + 0.5;
scanLine = step(0.8, scanLine);
float flicker = 0.9 + sin(uTime * 3.0) * 0.1;
vec3 baseColor = uColor * fresnel * flicker;
baseColor += vec3(0.2, 0.5, 1.0) * scanLine * 0.3;
float alpha = fresnel * 0.7 + 0.1;
gl_FragColor = vec4(baseColor, alpha);
}
`;
var material = new THREE.ShaderMaterial({
uniforms: uniforms,
vertexShader: vertexShader,
fragmentShader: fragmentShader,
transparent: true,
side: THREE.DoubleSide,
depthWrite: false
});
var geometry = new THREE.TorusKnotGeometry(1, 0.3, 128, 32);
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
var clock = new THREE.Clock();
function animate() {
uniforms.uTime.value = clock.getElapsedTime();
mesh.rotation.x += 0.005;
mesh.rotation.y += 0.01;
controls.update();
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
animate();
window.addEventListener("resize", function() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>
What's Next
Learn mouse picking with Raycaster for interactive 3D applications.
Raycaster — Mouse picking and intersection testing. Physics — Adding physics to Three.js scenes.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro