Skip to main content

Mastering Three.js Shaders: From Basics to Mind-Bending Effects

Deep dive into creating stunning visual effects with custom GLSL shaders in Three.js - from particle systems to fluid simulations that will make your web experiences unforgettable.

Mastering Three.js Shaders: Creating Digital Art That MOVES Souls

The moment when mathematics becomes poetry, when code transforms into visual ecstasy - that’s the power of shaders.

The Philosophy of Visual Programming

Before we dive into the technical depths, let’s understand what we’re really doing here. Shaders aren’t just code - they’re digital brushstrokes that paint directly onto the fabric of reality. Every pixel becomes a canvas, every frame a masterpiece.

When you master shaders, you’re not just learning WebGL. You’re learning to bend light itself to your will.

Understanding the Shader Pipeline

The Journey of a Vertex

// Vertex Shader - The Birth of Form
attribute vec3 position;
attribute vec2 uv;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
uniform float time;

varying vec2 vUv;
varying vec3 vPosition;

void main() {
    vUv = uv;
    
    // Transform the vertex - this is where magic begins
    vec3 pos = position;
    
    // Add some life with sine waves
    pos.z += sin(pos.x * 4.0 + time) * 0.1;
    pos.z += cos(pos.y * 3.0 + time * 0.7) * 0.1;
    
    vPosition = pos;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}

The Soul of Color

// Fragment Shader - Where Dreams Become Pixels
uniform float time;
uniform vec2 resolution;
uniform vec3 color1;
uniform vec3 color2;

varying vec2 vUv;
varying vec3 vPosition;

// Noise function for organic movement
float noise(vec2 st) {
    vec2 i = floor(st);
    vec2 f = fract(st);
    
    vec2 u = f * f * (3.0 - 2.0 * f);
    
    return mix(
        mix(dot(random2(i + vec2(0.0,0.0)), f - vec2(0.0,0.0)), 
            dot(random2(i + vec2(1.0,0.0)), f - vec2(1.0,0.0)), u.x),
        mix(dot(random2(i + vec2(0.0,1.0)), f - vec2(0.0,1.0)), 
            dot(random2(i + vec2(1.0,1.0)), f - vec2(1.0,1.0)), u.x), 
        u.y
    );
}

void main() {
    vec2 st = vUv;
    
    // Create flowing energy patterns
    float n = noise(st * 8.0 + time * 0.5);
    n += noise(st * 16.0 + time * 0.3) * 0.5;
    n += noise(st * 32.0 + time * 0.1) * 0.25;
    
    // Color mixing based on noise and position
    vec3 color = mix(color1, color2, n);
    
    // Add fresnel effect for extra depth
    float fresnel = 1.0 - dot(normalize(vPosition), vec3(0.0, 0.0, 1.0));
    color += fresnel * 0.3;
    
    gl_FragColor = vec4(color, 1.0);
}

Building Particle Systems That Dance

The Mathematics of Beauty

Particle systems are where physics meets artistry. Each particle is a story, each movement a brushstroke in time.

class AdvancedParticleSystem {
    constructor(scene, count = 10000) {
        this.scene = scene;
        this.count = count;
        this.time = 0;
        
        this.createParticles();
        this.createMaterial();
        this.createMesh();
    }
    
    createParticles() {
        this.positions = new Float32Array(this.count * 3);
        this.colors = new Float32Array(this.count * 3);
        this.sizes = new Float32Array(this.count);
        this.velocities = new Float32Array(this.count * 3);
        
        for (let i = 0; i < this.count; i++) {
            const i3 = i * 3;
            
            // Spiral galaxy formation
            const radius = Math.pow(Math.random(), 0.6) * 15;
            const spinAngle = radius * 0.3;
            const branchAngle = (i % 6) / 6 * Math.PI * 2;
            
            // Position
            this.positions[i3] = Math.cos(branchAngle + spinAngle) * radius;
            this.positions[i3 + 1] = (Math.random() - 0.5) * 0.5;
            this.positions[i3 + 2] = Math.sin(branchAngle + spinAngle) * radius;
            
            // Velocity for natural movement
            this.velocities[i3] = (Math.random() - 0.5) * 0.01;
            this.velocities[i3 + 1] = (Math.random() - 0.5) * 0.005;
            this.velocities[i3 + 2] = (Math.random() - 0.5) * 0.01;
            
            // Colors based on distance and branch
            const distance = Math.sqrt(
                this.positions[i3] ** 2 + 
                this.positions[i3 + 2] ** 2
            );
            
            const colorMix = distance / 15;
            const branchColor = (i % 6) / 6;
            
            this.colors[i3] = Math.max(0.2, 1 - colorMix + branchColor * 0.3);
            this.colors[i3 + 1] = Math.max(0.1, 0.5 - colorMix * 0.5);
            this.colors[i3 + 2] = Math.max(0.3, colorMix + branchColor * 0.2);
            
            // Size variation
            this.sizes[i] = Math.random() * 2 + 0.5;
        }
    }
    
    createMaterial() {
        this.material = new THREE.ShaderMaterial({
            uniforms: {
                time: { value: 0 },
                pixelRatio: { value: Math.min(window.devicePixelRatio, 2) }
            },
            vertexShader: `
                attribute float size;
                attribute vec3 color;
                
                uniform float time;
                uniform float pixelRatio;
                
                varying vec3 vColor;
                varying float vAlpha;
                
                void main() {
                    vColor = color;
                    
                    // Pulsing effect based on time and position
                    float pulse = sin(time * 2.0 + position.x * 0.1) * 0.5 + 0.5;
                    vAlpha = pulse * 0.8 + 0.2;
                    
                    // Transform to screen space
                    vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
                    
                    // Size attenuation with distance
                    float sizeAttenuation = 1.0 / -mvPosition.z;
                    
                    gl_PointSize = size * sizeAttenuation * 300.0 * pixelRatio * pulse;
                    gl_Position = projectionMatrix * mvPosition;
                }
            `,
            fragmentShader: `
                varying vec3 vColor;
                varying float vAlpha;
                
                void main() {
                    // Create circular particles with soft edges
                    vec2 center = gl_PointCoord - vec2(0.5);
                    float dist = length(center);
                    
                    if (dist > 0.5) discard;
                    
                    // Soft falloff
                    float alpha = 1.0 - smoothstep(0.0, 0.5, dist);
                    alpha *= vAlpha;
                    
                    // Add core brightness
                    float core = 1.0 - smoothstep(0.0, 0.1, dist);
                    vec3 color = vColor + core * 0.5;
                    
                    gl_FragColor = vec4(color, alpha);
                }
            `,
            blending: THREE.AdditiveBlending,
            transparent: true,
            vertexColors: true,
            depthWrite: false
        });
    }
    
    createMesh() {
        this.geometry = new THREE.BufferGeometry();
        this.geometry.setAttribute('position', new THREE.BufferAttribute(this.positions, 3));
        this.geometry.setAttribute('color', new THREE.BufferAttribute(this.colors, 3));
        this.geometry.setAttribute('size', new THREE.BufferAttribute(this.sizes, 1));
        
        this.points = new THREE.Points(this.geometry, this.material);
        this.scene.add(this.points);
    }
    
    update(deltaTime) {
        this.time += deltaTime;
        this.material.uniforms.time.value = this.time;
        
        // Update particle positions with physics
        for (let i = 0; i < this.count; i++) {
            const i3 = i * 3;
            
            // Apply velocities
            this.positions[i3] += this.velocities[i3];
            this.positions[i3 + 1] += this.velocities[i3 + 1];
            this.positions[i3 + 2] += this.velocities[i3 + 2];
            
            // Add gravitational pull toward center
            const distance = Math.sqrt(
                this.positions[i3] ** 2 + 
                this.positions[i3 + 1] ** 2 + 
                this.positions[i3 + 2] ** 2
            );
            
            if (distance > 0) {
                const force = 0.0001;
                this.velocities[i3] -= (this.positions[i3] / distance) * force;
                this.velocities[i3 + 1] -= (this.positions[i3 + 1] / distance) * force * 0.1;
                this.velocities[i3 + 2] -= (this.positions[i3 + 2] / distance) * force;
            }
            
            // Reset particles that drift too far
            if (distance > 20) {
                const radius = Math.pow(Math.random(), 0.6) * 15;
                const angle = Math.random() * Math.PI * 2;
                
                this.positions[i3] = Math.cos(angle) * radius;
                this.positions[i3 + 1] = (Math.random() - 0.5) * 0.5;
                this.positions[i3 + 2] = Math.sin(angle) * radius;
                
                this.velocities[i3] = (Math.random() - 0.5) * 0.01;
                this.velocities[i3 + 1] = (Math.random() - 0.5) * 0.005;
                this.velocities[i3 + 2] = (Math.random() - 0.5) * 0.01;
            }
        }
        
        // Update geometry
        this.geometry.attributes.position.needsUpdate = true;
        
        // Rotate the entire system for visual appeal
        this.points.rotation.y += deltaTime * 0.05;
    }
}

Advanced Shader Techniques

Post-Processing Magic

The real artistry happens in post-processing. This is where good visuals become unforgettable experiences.

class AdvancedPostProcessing {
    constructor(renderer, scene, camera) {
        this.renderer = renderer;
        this.scene = scene;
        this.camera = camera;
        
        this.setupComposer();
        this.createCustomPasses();
    }
    
    setupComposer() {
        this.composer = new EffectComposer(this.renderer);
        this.composer.addPass(new RenderPass(this.scene, this.camera));
    }
    
    createCustomPasses() {
        // Bloom with custom parameters
        this.bloomPass = new UnrealBloomPass(
            new THREE.Vector2(window.innerWidth, window.innerHeight),
            1.5,    // strength
            0.4,    // radius
            0.85    // threshold
        );
        this.composer.addPass(this.bloomPass);
        
        // Custom film grain effect
        this.filmGrainPass = new ShaderPass({
            uniforms: {
                tDiffuse: { value: null },
                time: { value: 0 },
                intensity: { value: 0.1 }
            },
            vertexShader: `
                varying vec2 vUv;
                void main() {
                    vUv = uv;
                    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
                }
            `,
            fragmentShader: `
                uniform sampler2D tDiffuse;
                uniform float time;
                uniform float intensity;
                varying vec2 vUv;
                
                float random(vec2 st) {
                    return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123);
                }
                
                void main() {
                    vec4 color = texture2D(tDiffuse, vUv);
                    
                    // Add film grain
                    float grain = random(vUv + time) * intensity;
                    color.rgb += grain;
                    
                    // Slight vignette
                    float dist = distance(vUv, vec2(0.5));
                    color.rgb *= 1.0 - dist * 0.3;
                    
                    gl_FragColor = color;
                }
            `
        });
        this.composer.addPass(this.filmGrainPass);
        
        // Chromatic aberration for that premium feel
        this.chromaticPass = new ShaderPass({
            uniforms: {
                tDiffuse: { value: null },
                amount: { value: 0.005 }
            },
            vertexShader: `
                varying vec2 vUv;
                void main() {
                    vUv = uv;
                    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
                }
            `,
            fragmentShader: `
                uniform sampler2D tDiffuse;
                uniform float amount;
                varying vec2 vUv;
                
                void main() {
                    vec2 offset = amount * (vUv - 0.5);
                    
                    vec4 color = texture2D(tDiffuse, vUv);
                    color.r = texture2D(tDiffuse, vUv + offset).r;
                    color.b = texture2D(tDiffuse, vUv - offset).b;
                    
                    gl_FragColor = color;
                }
            `
        });
        this.composer.addPass(this.chromaticPass);
    }
    
    update(time) {
        if (this.filmGrainPass) {
            this.filmGrainPass.uniforms.time.value = time;
        }
    }
    
    render() {
        this.composer.render();
    }
    
    resize(width, height) {
        this.composer.setSize(width, height);
        this.bloomPass.setSize(width, height);
    }
}

The Art of Performance Optimization

GPU Memory Management

When creating visual art, performance isn’t just technical - it’s aesthetic. Stuttering frames kill the magic.

class PerformanceOptimizedShader {
    constructor() {
        this.geometryPool = new Map();
        this.materialPool = new Map();
        this.texturePool = new Map();
    }
    
    // Reuse geometries instead of creating new ones
    getGeometry(type, ...params) {
        const key = `${type}_${params.join('_')}`;
        
        if (!this.geometryPool.has(key)) {
            let geometry;
            switch (type) {
                case 'sphere':
                    geometry = new THREE.SphereGeometry(...params);
                    break;
                case 'plane':
                    geometry = new THREE.PlaneGeometry(...params);
                    break;
                default:
                    throw new Error(`Unknown geometry type: ${type}`);
            }
            this.geometryPool.set(key, geometry);
        }
        
        return this.geometryPool.get(key);
    }
    
    // Batch similar objects for better performance
    createInstancedMesh(geometry, material, count) {
        const mesh = new THREE.InstancedMesh(geometry, material, count);
        
        // Set up instanced attributes
        const dummy = new THREE.Object3D();
        const colors = new Float32Array(count * 3);
        
        for (let i = 0; i < count; i++) {
            // Random positioning
            dummy.position.set(
                (Math.random() - 0.5) * 20,
                (Math.random() - 0.5) * 20,
                (Math.random() - 0.5) * 20
            );
            
            dummy.rotation.set(
                Math.random() * Math.PI * 2,
                Math.random() * Math.PI * 2,
                Math.random() * Math.PI * 2
            );
            
            dummy.scale.setScalar(Math.random() * 0.5 + 0.5);
            dummy.updateMatrix();
            
            mesh.setMatrixAt(i, dummy.matrix);
            
            // Random colors
            const color = new THREE.Color().setHSL(Math.random(), 0.7, 0.6);
            colors[i * 3] = color.r;
            colors[i * 3 + 1] = color.g;
            colors[i * 3 + 2] = color.b;
        }
        
        mesh.instanceMatrix.needsUpdate = true;
        mesh.geometry.setAttribute('instanceColor', 
            new THREE.InstancedBufferAttribute(colors, 3));
        
        return mesh;
    }
    
    // Clean up resources properly
    dispose() {
        this.geometryPool.forEach(geometry => geometry.dispose());
        this.materialPool.forEach(material => material.dispose());
        this.texturePool.forEach(texture => texture.dispose());
        
        this.geometryPool.clear();
        this.materialPool.clear();
        this.texturePool.clear();
    }
}

Creating Emotional Connections Through Code

The Psychology of Visual Effects

The best shader effects don’t just look good - they feel good. They create emotional responses that stick with users long after they leave your site.

Warm vs Cool Color Palettes

// Warm palette - creates comfort, energy, excitement
vec3 warmPalette(float t) {
    vec3 a = vec3(0.5, 0.5, 0.5);
    vec3 b = vec3(0.5, 0.5, 0.5);
    vec3 c = vec3(1.0, 1.0, 1.0);
    vec3 d = vec3(0.263, 0.416, 0.557);
    
    return a + b * cos(6.28318 * (c * t + d));
}

// Cool palette - creates calm, professional, trustworthy feel
vec3 coolPalette(float t) {
    vec3 a = vec3(0.5, 0.5, 0.5);
    vec3 b = vec3(0.5, 0.5, 0.5);
    vec3 c = vec3(1.0, 1.0, 0.5);
    vec3 d = vec3(0.8, 0.9, 0.3);
    
    return a + b * cos(6.28318 * (c * t + d));
}

Movement Patterns That Feel Natural

// Organic movement - feels alive and natural
float organicMovement(vec2 pos, float time) {
    return sin(pos.x * 0.5 + time) * 
           cos(pos.y * 0.3 + time * 0.7) * 0.1 +
           sin(pos.x * 2.0 + time * 1.3) * 
           cos(pos.y * 1.5 + time * 0.5) * 0.05;
}

// Mechanical movement - feels precise and controlled
float mechanicalMovement(vec2 pos, float time) {
    float grid = step(0.5, fract(pos.x * 5.0)) * step(0.5, fract(pos.y * 5.0));
    return grid * sin(time * 2.0) * 0.1;
}

Real-World Implementation Tips

Browser Compatibility and Fallbacks

class ShaderCompatibilityManager {
    constructor() {
        this.capabilities = this.detectCapabilities();
        this.fallbackStrategies = new Map();
    }
    
    detectCapabilities() {
        const canvas = document.createElement('canvas');
        const gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
        
        if (!gl) {
            return { webgl: false };
        }
        
        return {
            webgl: true,
            webgl2: !!canvas.getContext('webgl2'),
            maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE),
            maxVertexUniforms: gl.getParameter(gl.MAX_VERTEX_UNIFORM_VECTORS),
            maxFragmentUniforms: gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS),
            extensions: {
                derivatives: !!gl.getExtension('OES_standard_derivatives'),
                textureFloat: !!gl.getExtension('OES_texture_float'),
                drawBuffers: !!gl.getExtension('WEBGL_draw_buffers')
            }
        };
    }
    
    createOptimizedMaterial(complexity = 'high') {
        if (!this.capabilities.webgl) {
            return this.createFallbackMaterial();
        }
        
        switch (complexity) {
            case 'high':
                if (this.capabilities.webgl2 && this.capabilities.extensions.textureFloat) {
                    return this.createHighQualityMaterial();
                }
                // Fall through to medium
            case 'medium':
                if (this.capabilities.extensions.derivatives) {
                    return this.createMediumQualityMaterial();
                }
                // Fall through to low
            case 'low':
            default:
                return this.createLowQualityMaterial();
        }
    }
    
    createFallbackMaterial() {
        // CSS-based fallback for devices without WebGL
        return new THREE.MeshBasicMaterial({
            color: 0x4a90e2,
            transparent: true,
            opacity: 0.8
        });
    }
}

The Future of Web Graphics

As we push the boundaries of what’s possible in the browser, remember that the most important element isn’t the technology - it’s the human connection. Every shader you write, every effect you create, should serve a purpose: to make someone feel something.

The web is becoming a canvas for digital art that rivals anything we’ve seen before. With WebGPU on the horizon and browsers becoming more powerful every day, we’re entering an era where the only limit is our imagination.

Your Journey Starts Now

The techniques in this guide are just the beginning. The real magic happens when you take these foundations and push them into uncharted territory. Create effects that no one has seen before. Make visuals that stop people in their tracks.

Remember: Code is poetry, shaders are paintings, and you are the artist.


Ready to create your own visual masterpieces? Check out our 3D Lab to see these techniques in action, or dive into our Advanced Graphics Course to master the art of digital beauty.