Three.js Shaders GLSL — Vertex and Fragment Shader Programming
In this tutorial, you will learn about Three.js Shaders GLSL. We cover key concepts, practical examples, and best practices to help you master this topic.
GLSL shaders in Three.js are programs that run on the GPU to control vertex positions and pixel colors, enabling custom visual effects beyond built-in materials.
What You'll Learn
By the end of this guide, you will understand the difference between vertex and fragment shaders, write basic GLSL code, use ShaderMaterial with uniforms and attributes, create wave displacements and color gradients, and debug shader compilation errors.
Why Shaders Matter
Built-in materials are powerful but limited. Shaders give you direct control over every pixel. In Durga Antivirus Pro, custom shaders render real-time threat heat maps, wireframe overlays, and animated scanning effects that would be impossible with standard materials.
flowchart LR
A[JavaScript Code] --> B[Vertex Shader]
B --> C[Fragment Shader]
D[Geometry Data] --> B
E[Uniforms] --> B
E --> C
F[Textures] --> C
C --> G[Screen Pixels]
ShaderMaterial — Your Pipeline
ShaderMaterial lets you provide custom vertex and fragment shader code.
var material = new THREE.ShaderMaterial({
vertexShader: vertexCode,
fragmentShader: fragmentCode,
uniforms: {
uTime: { value: 0 },
uColor: { value: new THREE.Color(0x4488ff) }
}
});
Why uniforms: Uniforms are values passed from JavaScript to the shader that remain constant for the entire draw call. Use them for time, colors, light positions, and other per-frame data.
Your First Shader — Color Gradient
Vertex shader:
void main() {
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
vec4 viewPosition = viewMatrix * modelPosition;
vec4 projectedPosition = projectionMatrix * viewPosition;
gl_Position = projectedPosition;
}
Fragment shader:
uniform vec3 uColor;
uniform float uTime;
void main() {
vec3 color = uColor;
color.r += sin(uTime) * 0.3;
gl_FragColor = vec4(color, 1.0);
}
Expected output: A mesh whose color pulses between blue and purple over time.
Wave Displacement — Moving Vertices
uniform float uTime;
void main() {
vec3 pos = position;
float wave = sin(pos.x * 2.0 + uTime) * 0.2;
pos.z += wave;
vec4 modelPosition = modelMatrix * vec4(pos, 1.0);
vec4 viewPosition = viewMatrix * modelPosition;
vec4 projectedPosition = projectionMatrix * viewPosition;
gl_Position = projectedPosition;
}
Expected output: A plane whose surface undulates like ocean waves, with crests and troughs moving over time.
Why vertex shaders: The vertex shader runs once per vertex. By modifying position before projection, we create geometry deformation at no CPU cost.
Passing Data Between Shaders
// Vertex shader
varying float vElevation;
void main() {
vElevation = sin(position.x * 2.0 + uTime) * 0.2;
// ... rest of vertex transform
}
// Fragment shader
varying float vElevation;
void main() {
float mixValue = (vElevation + 0.2) / 0.4;
vec3 lowColor = vec3(0.0, 0.2, 0.5);
vec3 highColor = vec3(0.8, 0.9, 1.0);
vec3 color = mix(lowColor, highColor, mixValue);
gl_FragColor = vec4(color, 1.0);
}
Expected output: A wave plane where higher elevations are lighter blue and lower elevations are darker blue, creating a contour map effect.
Varyings: Variables passed from vertex to fragment shader. The GPU interpolates them across fragments between vertices.
Texture Sampling in Shaders
uniform sampler2D uTexture;
uniform float uTime;
varying vec2 vUv;
void main() {
vec2 uv = vUv;
uv.x += sin(uTime) * 0.05;
vec4 texColor = texture2D(uTexture, uv);
gl_FragColor = texColor;
}
Expected output: A texture with a subtle horizontal oscillation, creating a shimmering or liquid effect.
Common Mistakes
1. Not Converting Three.js Built-in Names
Three.js automatically provides uniforms like modelMatrix, viewMatrix, projectionMatrix, normalMatrix. Use these exact names in GLSL.
2. Forgetting precision Statement
Mobile GPUs often fail without a precision qualifier:
precision highp float;
3. Mismatched Varying Names
The varying name must be identical in vertex and fragment shaders. The case-sensitive mismatch is a common compile error.
4. Using position Without Declaring It
position, normal, uv are Three.js built-in attributes. Just use them directly in the vertex shader.
5. Neglecting to Call material.needsUpdate = true
After changing shader code at runtime, set material.needsUpdate = true to trigger recompilation.
6. Debugging With console.log in Shaders
You cannot use JavaScript inside shaders. Use gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0) to visually debug — red means the pixel hit that branch.
Practice Questions
Q1: What is the difference between a vertex shader and a fragment shader? A: The vertex shader runs once per vertex to compute positions. The fragment shader runs once per pixel to compute colors.
Q2: What are uniforms in GLSL? A: Uniforms are global variables passed from JavaScript that stay constant for a draw call. Used for time, colors, light positions.
Q3: What do varyings do? A: Varyings pass interpolated data from the vertex shader to the fragment shader. The GPU interpolates values between vertices across fragments.
Q4: How do you use a texture in a custom shader?
A: Declare a uniform sampler2D uTexture, pass it from JavaScript as a THREE.Texture, and sample it with texture2D(uTexture, vUv).
Q5: Why do you need modelMatrix, viewMatrix, and projectionMatrix?
A: They transform vertices from object space to world space (model), then to view space (camera), then to clip space (projection) for rendering.
Challenge: Create a custom shader that draws an animated lava lamp effect — blobby shapes that rise, merge, and change color over time. Use noise functions in the fragment shader.
FAQ
Try It Yourself — Shader Playground
Build a complete page with a custom ShaderMaterial on a plane. Controls adjust wave amplitude, speed, and color. Watch the vertex displacement and fragment coloring update in real time.
<!DOCTYPE html>
<html>
<head>
<title>Shader Playground</title>
<style>
body { margin: 0; overflow: hidden; background: #000; font-family: sans-serif; }
#ui { position: absolute; top: 10px; left: 10px; color: white; background: rgba(0,0,0,0.8); padding: 15px; border-radius: 8px; width: 200px; font-size: 13px; }
#ui input[type="range"] { width: 100%; }
</style>
</head>
<body>
<div id="ui">
<label>Amplitude <span id="ampVal">0.2</span></label>
<input type="range" id="amplitude" min="0" max="0.5" step="0.01" value="0.2">
<label>Speed <span id="spdVal">1.0</span></label>
<input type="range" id="speed" min="0" max="3" step="0.1" value="1">
<label>Color Intensity <span id="colVal">0.5</span></label>
<input type="range" id="colorIntensity" min="0" max="1" step="0.01" value="0.5">
</div>
<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";
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(4, 3, 6);
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 },
uAmplitude: { value: 0.2 },
uSpeed: { value: 1.0 },
uColorIntensity: { value: 0.5 }
};
var vertexShader = `
uniform float uTime;
uniform float uAmplitude;
uniform float uSpeed;
varying float vElevation;
void main() {
vec3 pos = position;
float wave = sin(pos.x * 3.0 + uTime * uSpeed) * cos(pos.y * 2.0 + uTime * 0.7 * uSpeed) * uAmplitude;
pos.z += wave;
vElevation = wave;
vec4 modelPos = modelMatrix * vec4(pos, 1.0);
vec4 viewPos = viewMatrix * modelPos;
gl_Position = projectionMatrix * viewPos;
}
`;
var fragmentShader = `
uniform float uColorIntensity;
uniform vec3 uColor;
varying float vElevation;
void main() {
float intensity = 0.5 + vElevation * 5.0 * uColorIntensity;
vec3 color = vec3(0.1, 0.3, 0.8) * intensity;
color += vec3(0.2, 0.6, 1.0) * (1.0 - intensity) * 0.3;
gl_FragColor = vec4(color, 1.0);
}
`;
var material = new THREE.ShaderMaterial({
uniforms: uniforms,
vertexShader: vertexShader,
fragmentShader: fragmentShader,
side: THREE.DoubleSide,
wireframe: false
});
var geometry = new THREE.PlaneGeometry(5, 5, 64, 64);
geometry.rotateX(-Math.PI / 2);
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
document.getElementById("amplitude").addEventListener("input", function() {
uniforms.uAmplitude.value = parseFloat(this.value);
document.getElementById("ampVal").textContent = this.value;
});
document.getElementById("speed").addEventListener("input", function() {
uniforms.uSpeed.value = parseFloat(this.value);
document.getElementById("spdVal").textContent = this.value;
});
document.getElementById("colorIntensity").addEventListener("input", function() {
uniforms.uColorIntensity.value = parseFloat(this.value);
document.getElementById("colVal").textContent = this.value;
});
var clock = new THREE.Clock();
function animate() {
uniforms.uTime.value = clock.getElapsedTime();
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
Apply shader knowledge with ShaderMaterial for complex effects.
Shader Material — Advanced ShaderMaterial techniques. Raycaster — Mouse picking and intersection detection.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro