iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

GLSL Shaders

Custom shaders give you direct control over how each pixel is computed. In Three.js, ShaderMaterial (and RawShaderMaterial for raw GLSL) lets you write vertex and fragment shaders, pass uniforms and attributes, and produce effects no built-in material can.

GLSL, uniforms, custom material

EXAMPLE
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';

// 1) Setup
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, innerWidth/innerHeight, 0.1, 100);
camera.position.set(0, 0, 2);
new OrbitControls(camera, renderer.domElement);

// 2) Simplest custom shader — pulsing colour
const material = new THREE.ShaderMaterial({
    uniforms: {
        uTime:  { value: 0 },
        uColor: { value: new THREE.Color('#4f46e5') },
    },
    vertexShader: `
        varying vec2 vUv;
        void main() {
            vUv = uv;
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
    `,
    fragmentShader: `
        uniform float uTime;
        uniform vec3  uColor;
        varying vec2  vUv;
        void main() {
            float pulse = 0.5 + 0.5 * sin(uTime * 2.0);
            vec3 c = mix(uColor, vec3(1.0), vUv.y * pulse);
            gl_FragColor = vec4(c, 1.0);
        }
    `,
});

const quad = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), material);
scene.add(quad);

const clock = new THREE.Clock();
function loop() {
    material.uniforms.uTime.value = clock.getElapsedTime();
    renderer.render(scene, camera);
    requestAnimationFrame(loop);
}
loop();

// 3) Vertex displacement — wavy plane
const wavyMat = new THREE.ShaderMaterial({
    uniforms: { uTime: { value: 0 } },
    vertexShader: `
        uniform float uTime;
        varying vec2  vUv;
        void main() {
            vUv = uv;
            vec3 p = position;
            p.z += sin(uv.x * 10.0 + uTime) * 0.15 + cos(uv.y * 8.0 - uTime) * 0.1;
            gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
        }
    `,
    fragmentShader: `
        varying vec2 vUv;
        void main() {
            gl_FragColor = vec4(vUv, 0.5, 1.0);
        }
    `,
    wireframe: true,
});
const plane = new THREE.Mesh(new THREE.PlaneGeometry(2, 2, 100, 100), wavyMat);
plane.rotation.x = -1.0;
scene.add(plane);

// 4) Common GLSL uniforms Three.js injects (with ShaderMaterial)
//   uniform mat4  modelMatrix;       — model -> world
//   uniform mat4  modelViewMatrix;   — model -> view (camera)
//   uniform mat4  projectionMatrix;  — view -> clip
//   uniform mat3  normalMatrix;      — for normals
//   uniform mat4  viewMatrix;
//   uniform vec3  cameraPosition;
//
// Attributes:
//   attribute vec3 position;
//   attribute vec3 normal;
//   attribute vec2 uv;
//
// You don't declare these for ShaderMaterial — they're auto-injected. RawShaderMaterial gives you a blank slate.

// 5) Adding lighting in a custom material
const lambertish = new THREE.ShaderMaterial({
    uniforms: {
        uLightDir: { value: new THREE.Vector3(1, 1, 1).normalize() },
        uColor:    { value: new THREE.Color('#ff7e5f') },
    },
    vertexShader: `
        varying vec3 vNormal;
        void main() {
            vNormal = normalize(normalMatrix * normal);
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
    `,
    fragmentShader: `
        uniform vec3 uLightDir;
        uniform vec3 uColor;
        varying vec3 vNormal;
        void main() {
            float diff = max(dot(vNormal, uLightDir), 0.0);
            gl_FragColor = vec4(uColor * (0.2 + 0.8 * diff), 1.0);
        }
    `,
});

const sphere = new THREE.Mesh(new THREE.IcosahedronGeometry(0.5, 4), lambertish);
sphere.position.x = -1;
scene.add(sphere);

// 6) Textures as uniforms
const tex = new THREE.TextureLoader().load('/img/checker.png');
tex.colorSpace = THREE.SRGBColorSpace;

const texMat = new THREE.ShaderMaterial({
    uniforms: { uMap: { value: tex } },
    vertexShader: `
        varying vec2 vUv;
        void main() {
            vUv = uv;
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
    `,
    fragmentShader: `
        uniform sampler2D uMap;
        varying vec2 vUv;
        void main() {
            vec4 c = texture2D(uMap, vUv);
            gl_FragColor = vec4(c.rgb, c.a);
        }
    `,
});

// 7) Patching built-in materials — onBeforeCompile (advanced)
const pbr = new THREE.MeshStandardMaterial({ color: 0x4f46e5 });
pbr.onBeforeCompile = (shader) => {
    shader.uniforms.uTime = { value: 0 };
    shader.vertexShader = `
        uniform float uTime;
        ${shader.vertexShader.replace(
            '#include <begin_vertex>',
            `
            #include <begin_vertex>
            transformed.z += sin(transformed.x * 4.0 + uTime) * 0.05;
            `,
        )}`;
    pbr.userData.shader = shader;
};

// Drive uTime per frame
// if (pbr.userData.shader) pbr.userData.shader.uniforms.uTime.value = t;

// 8) Resolution + DPR uniforms — post-process & screen-space effects
const pp = new THREE.ShaderMaterial({
    uniforms: {
        uResolution: { value: new THREE.Vector2(innerWidth, innerHeight) },
        uMap:        { value: tex },
    },
    vertexShader: `
        varying vec2 vUv;
        void main() {
            vUv = uv;
            gl_Position = vec4(position, 1.0);   // full-screen quad
        }
    `,
    fragmentShader: `
        uniform vec2 uResolution;
        uniform sampler2D uMap;
        varying vec2 vUv;
        void main() {
            vec2 px = 1.0 / uResolution;
            vec3 c = vec3(0.0);
            for (int x = -1; x <= 1; x++)
                for (int y = -1; y <= 1; y++)
                    c += texture2D(uMap, vUv + vec2(float(x), float(y)) * px).rgb;
            gl_FragColor = vec4(c / 9.0, 1.0);    // 3x3 box blur
        }
    `,
});

// 9) Debugging tools
// • Spector.js     — browser extension; capture every draw call
// • renderer.debug.checkShaderErrors = true   (default in development)
// • Inject 'gl_FragColor = vec4(vUv, 0, 1)' to see UV mapping
// • Use linear-space colors for math; convert sRGB at the boundary
// • Use renderer.info to track draw calls / triangles

// 10) Performance tips
// • Reuse materials and uniforms instead of cloning per object
// • Avoid 'discard' in fragment shaders when possible — kills hierarchical-Z
// • Bake noise into a texture instead of computing in shader
// • Use mediump precision in mobile fragment shaders
// • Don't index into arrays with non-uniform expressions — slow on some GPUs

// 11) Common bugs
//   • Black mesh → likely no light or wrong normalMatrix
//   • Texture all gray → forgot colorSpace = SRGBColorSpace
//   • Geometry distorted → missing perspective division (use projectionMatrix * modelViewMatrix * vec4(p, 1.0))
//   • Uniforms set but no effect — shader was created with stale uniforms object; update the same reference
//   • UV out of range → texture wrapping mode is REPEAT vs CLAMP_TO_EDGE matters
//   • 'Error: invalid type for uniform' — JS value type doesn't match the GLSL declaration
//   • Material instances sharing uniforms — clone uniforms or use Material.clone() per object

Why it matters

Start with ShaderMaterial and let Three.js inject the standard matrices + attributes — you only have to think about what each vertex and pixel should compute. Drive uniforms from JS each frame (time, mouse, resolution), bake noise into textures when shaders get slow, and use onBeforeCompile to patch MeshStandardMaterial when you want PBR plus a small custom twist.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
const mat = new THREE.ShaderMaterial({
    uniforms: { uTime: { value: 0 } },
    vertexShader: vert,
    fragmentShader: frag,
});
Try it Yourself »

Discussion

Loading…