Modern Web

Custom WebGL Liquid Shaders in Three.js: Achieving Fluid Physics at 60 FPS

Master GLSL shader development in Three.js. Write a custom ShaderMaterial to render mouse-reactive liquid surfaces and fluid-like wave equations at 60 FPS.

Sachin Sharma
Sachin SharmaCreator
Jun 1, 2026
5 min read
Custom WebGL Liquid Shaders in Three.js: Achieving Fluid Physics at 60 FPS
Featured Resource
Quick Overview

Master GLSL shader development in Three.js. Write a custom ShaderMaterial to render mouse-reactive liquid surfaces and fluid-like wave equations at 60 FPS.

Custom WebGL Liquid Shaders in Three.js: Achieving Fluid Physics at 60 FPS

Creating highly interactive, visually arresting web experiences requires stepping off V8's CPU thread and letting the GPU handle the heavy lifting. While CPU-based particle math drops frames rapidly, custom GLSL (OpenGL Shading Language) shaders executed on the GPU can render millions of mathematical calculations at a consistent, butter-smooth 60 FPS.

One of the most premium, sought-after graphics effects is interactive liquid simulation.

In this guide, we will explore how to write custom Vertex and Fragment Shaders using Three.js's ShaderMaterial to build a highly responsive, liquid-like surface that reacts dynamically to mouse coordinate movement.


⚡ 1. The Shader Pipeline: Vertex vs Fragment

A shader is a program that runs directly on the graphics card. The WebGL pipeline uses two distinct shaders to render any 3D object:

  1. 2.
    Vertex Shader: Handles the geometry. It runs once for every single vertex in your 3D mesh. It calculates the position of the vertex in 3D space and can deform or animate the geometry (e.g., creating wave heights).
  2. 4.
    Fragment (Pixel) Shader: Handles the coloring. It runs once for every single pixel that makes up the surface of the rendered mesh. It calculates the final RGB color and transparency values, handling lighting, reflections, and liquid gradients.
[Mesh Vertices] ──> [Vertex Shader (Deformation)] ──> [Rasterizer] ──> [Fragment Shader (Coloring)] ──> [GPU Screen Output]

🏗️ 2. Designing the Fluid Wave Vertex Shader

To simulate waves in real-time, we will deform a flat plane using a mathematical combination of sine waves and perlin noise inside the Vertex Shader.

We pass three Uniforms (global variables sent from CPU to GPU) to animate the waves over time and map mouse interaction:

  • uTime: The elapsed time to animate the wave phase.
  • uMouse: The 2D coordinates of the cursor to pull the wave peak toward the mouse.
  • uIntensity: Controls the height of the waves.

The Vertex Shader code (vertex.glsl):

glsl
uniform float uTime; uniform vec2 uMouse; uniform float uIntensity; varying vec2 vUv; varying float vElevation; // Standard Pseudo-Random 2D Noise function float noise(vec2 p) { return sin(p.x * 12.7 + p.y * 31.1) * 43758.5453; } void main() { vUv = uv; vec4 modelPosition = modelMatrix * vec4(position, 1.0); // 1. Calculate a dynamic elevation based on multiple overlapping sine waves float elevation = sin(modelPosition.x * 3.0 + uTime * 2.0) * 0.15; elevation += sin(modelPosition.y * 2.0 + uTime * 1.5) * 0.1; // 2. Add mouse-reactive localized pull float dist = distance(modelPosition.xy, uMouse); if (dist < 0.8) { elevation += (1.0 - (dist / 0.8)) * uIntensity * 0.3; } modelPosition.z += elevation; vec4 viewPosition = viewMatrix * modelPosition; vec4 projectedPosition = projectionMatrix * viewPosition; gl_Position = projectedPosition; // Pass elevation down to the fragment shader for visual color depth mapping vElevation = elevation; }

💻 3. Creating the Liquid Color Fragment Shader

Once the vertices are deformed, the Fragment Shader colors the surface. To make it look like premium liquid, we'll map the pixel colors dynamically based on their local vElevation height, creating beautiful depth gradients.

The Fragment Shader code (fragment.glsl):

glsl
uniform vec3 uDeepColor; uniform vec3 uSurfaceColor; varying vec2 vUv; varying float vElevation; void main() { // Map elevation (-0.25 to 0.25) to a clean 0.0 to 1.0 range float mixStrength = (vElevation + 0.25) * 2.0; // Blend deep blue with glowing cyan based on wave heights vec3 color = mix(uDeepColor, uSurfaceColor, mixStrength); gl_FragColor = vec4(color, 0.95); }

🚀 4. Integrating the Shaders into Three.js

Now let's bind the GLSL code blocks into Three.js using THREE.ShaderMaterial and orchestrate the frame loop in JavaScript.

javascript
import * as THREE from 'three'; const scene = new THREE.Scene(); // 1. Create a highly detailed plane mesh (more vertices = smoother waves) const geometry = new THREE.PlaneGeometry(2, 2, 128, 128); // 2. Define the uniform structures const uniforms = { uTime: { value: 0.0 }, uMouse: { value: new THREE.Vector2(0, 0) }, uIntensity: { value: 0.0 }, uDeepColor: { value: new THREE.Color('#010a15') }, uSurfaceColor: { value: new THREE.Color('#00f2fe') } }; // 3. Attach custom shaders const material = new THREE.ShaderMaterial({ vertexShader: vertexShaderSourceCode, fragmentShader: fragmentShaderSourceCode, uniforms: uniforms, transparent: true }); const mesh = new THREE.Mesh(geometry, material); scene.add(mesh); // 4. Update the mouse uniforms on cursor movements let targetMouse = new THREE.Vector2(0, 0); window.addEventListener('mousemove', (event) => { // Normalize coordinates between -1.0 and 1.0 targetMouse.x = (event.clientX / window.innerWidth) * 2 - 1; targetMouse.y = -(event.clientY / window.innerHeight) * 2 + 1; uniforms.uIntensity.value = 1.0; }); // 5. Run the Render Loop const clock = new THREE.Clock(); function tick() { const elapsedTime = clock.getElapsedTime(); // Update uniforms uniforms.uTime.value = elapsedTime; // Smoothly interpolate (Lerp) mouse uniforms to prevent color jumping uniforms.uMouse.value.lerp(targetMouse, 0.08); // Slow down the pull intensity when mouse is still uniforms.uIntensity.value *= 0.96; renderer.render(scene, camera); window.requestAnimationFrame(tick); } tick();

🏁 5. Conclusion: WebGL Shaders are a Superpower

Writing raw GLSL inside Three.js breaks the boundaries of standard CSS and flat layout graphics. By deforming meshes on the GPU and blending colors based on elevation height in real-time, you deliver cutting-edge interactive graphics that perform beautifully even on mobile screens.

Sachin Sharma

Sachin Sharma

Software Developer

Building digital experiences at the intersection of design and code. Sharing weekly insights on engineering, productivity, and the future of tech.