Back to Components
Interactive Scratch-to-Reveal Fluid Canvas with Light/Dark Theme Switcher
Component

Interactive Scratch-to-Reveal Fluid Canvas with Light/Dark Theme Switcher

CodewithLord
December 17, 2025

An interactive scratch-to-reveal web experience using a WebGPU-powered fluid canvas, combined with a modern light/dark theme switcher and SVG-based UI controls.

📌 Description


This project demonstrates an **interactive scratch-based visual experience** using an HTML `` element designed for **fluid and GPU-accelerated effects**. Users are encouraged to *“scratch”* the canvas area to reveal dynamic visuals, creating a magical and engaging interaction.

Additionally, a modern light/dark theme switcher is implemented using radio inputs and SVG icons, allowing seamless theme transitions with a visually rich UI.

Key highlights include:

  • WebGPU-ready fluid canvas setup
  • Minimal and semantic HTML structure
  • Accessible theme switcher with SVG icons
  • Scalable layout for creative hero sections


🧱 HTML Structure


The HTML layout is divided into three main sections:

  1. Hero container with titles and canvas
  2. Canvas element for fluid interaction
  3. Theme switcher UI for light/dark mode
1<!DOCTYPE html> 2<html lang="en"> 3<head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>Document</title> 7 <link rel="stylesheet" href="style.css"> 8</head> 9<body> 10 <section id='container'> 11 <h1 class='a-title'>Scratch here</h1> 12 <h2 class='a-second-title'>See the magic</h2> 13 <canvas id="fluid-webgpu"></canvas> 14</section> 15 16<!-- switcher light-dark--> 17<fieldset class="switcher"> 18 <legend class="switcher__legend">Choose theme</legend> 19 <label class="switcher__option"> 20 <input class="switcher__input" type="radio" name="theme" value="light" c-option="1" checked /> 21 <svg class="switcher__icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 36 36"> 22 <path fill="var(--c)" fill-rule="evenodd" d="M18 12a6 6 0 1 1 0 12 6 6 0 0 1 0-12Zm0 2a4 4 0 1 0 0 8 4 4 0 0 0 0-8Z" clip-rule="evenodd" /> 23 <path fill="var(--c)" d="M17 6.038a1 1 0 1 1 2 0v3a1 1 0 0 1-2 0v-3ZM24.244 7.742a1 1 0 1 1 1.618 1.176L24.1 11.345a1 1 0 1 1-1.618-1.176l1.763-2.427ZM29.104 13.379a1 1 0 0 1 .618 1.902l-2.854.927a1 1 0 1 1-.618-1.902l2.854-.927ZM29.722 20.795a1 1 0 0 1-.619 1.902l-2.853-.927a1 1 0 1 1 .618-1.902l2.854.927ZM25.862 27.159a1 1 0 0 1-1.618 1.175l-1.763-2.427a1 1 0 1 1 1.618-1.175l1.763 2.427ZM19 30.038a1 1 0 0 1-2 0v-3a1 1 0 1 1 2 0v3ZM11.755 28.334a1 1 0 0 1-1.618-1.175l1.764-2.427a1 1 0 1 1 1.618 1.175l-1.764 2.427ZM6.896 22.697a1 1 0 1 1-.618-1.902l2.853-.927a1 1 0 1 1 .618 1.902l-2.853.927ZM6.278 15.28a1 1 0 1 1 .618-1.901l2.853.927a1 1 0 1 1-.618 1.902l-2.853-.927ZM10.137 8.918a1 1 0 0 1 1.618-1.176l1.764 2.427a1 1 0 0 1-1.618 1.176l-1.764-2.427Z" /> 24 </svg> 25 </label> 26 <label class="switcher__option"> 27 <input class="switcher__input" type="radio" name="theme" value="dark" c-option="2" /> 28 <svg class="switcher__icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 36 36"> 29 <path fill="var(--c)" d="M12.5 8.473a10.968 10.968 0 0 1 8.785-.97 7.435 7.435 0 0 0-3.737 4.672l-.09.373A7.454 7.454 0 0 0 28.732 20.4a10.97 10.97 0 0 1-5.232 7.125l-.497.27c-5.014 2.566-11.175.916-14.234-3.813l-.295-.483C5.53 18.403 7.13 11.93 12.017 8.77l.483-.297Zm4.234.616a8.946 8.946 0 0 0-2.805.883l-.429.234A9 9 0 0 0 10.206 22.5l.241.395A9 9 0 0 0 22.5 25.794l.416-.255a8.94 8.94 0 0 0 2.167-1.99 9.433 9.433 0 0 1-2.782-.313c-5.043-1.352-8.036-6.535-6.686-11.578l.147-.491c.242-.745.573-1.44.972-2.078Z" /> 30 </svg> 31 </label> 32 33 <!-- <div class="switcher__toggle"></div> --> 34 <div class="switcher__filter"> 35 <svg> 36 <filter id="switcher" primitiveUnits="objectBoundingBox"> 37 <feImage result="map" width="100%" height="100%" x="0" y="0" href="data:image/webp;base64,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" /> 38 39 <feGaussianBlur in="SourceGraphic" stdDeviation="0.04" result="blur" /> 40 <feDisplacementMap id="disp" in="blur" in2="map" scale="0.5" xChannelSelector="R" yChannelSelector="G"> 41 </feDisplacementMap> 42 </filter> 43 44 <filter id="toggler" primitiveUnits="objectBoundingBox"> 45 <feImage result="map" width="100%" height="100%" x="0" y="0" href="data:image/webp;base64,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" /> 46 47 <feGaussianBlur in="SourceGraphic" stdDeviation="0.01" result="blur" /> 48 <feDisplacementMap id="disp" in="blur" in2="map" scale="0.5" xChannelSelector="R" yChannelSelector="G"> 49 </feDisplacementMap> 50 </filter> 51 </svg> 52 </div> 53</fieldset> 54<script src="script.js"></script> 55</body> 56</html> 57

🎨 CSS Overview


The CSS (linked via style.css) is responsible for:

Full-screen hero layout

Typography alignment and spacing

Canvas positioning

Animated and stylized theme switcher

SVG filter effects for smooth UI transitions

Key styling concepts include:

Centered canvas interaction area

Layered text above the canvas

Gooey and blur effects for the switcher UI

CSS variables for light/dark theme colors

⚠️ The heavy SVG filter data is intentionally handled in CSS/SVG to offload complexity from JavaScript.

🎨 CSS Code


1html, 2body { 3 overflow: clip; 4} 5 6:root { 7 --c-glass: #bbbbbc; 8 --c-light: #fff; 9 --c-dark: #000; 10 11 --c-content: #224; 12 --c-action: #0052f5; 13 14 --c-bg: #fff; 15 16 --glass-reflex-dark: 1; 17 --glass-reflex-light: 1; 18 19 --saturation: 150%; 20 21 font-size: 20px; 22 font-family: sans-serif; 23 font-optical-sizing: auto; 24 25 transition: background 400ms cubic-bezier(1, 0, 0.4, 1), 26 color 400ms cubic-bezier(1, 0, 0.4, 1); 27} 28 29body { 30 margin: 0; 31 position: absolute; 32 width: 100%; 33 height: 100%; 34} 35 36#container { 37 margin: 0; 38 padding: 0; 39 display: flex; 40 flex-direction: column; 41 justify-content: center; 42 align-items: center; 43 width: 100%; 44 height: 100%; 45 background: var(--c-bg); 46 color: var(--c-content); 47} 48 49.a-title { 50 position: absolute; 51 color: transparent; 52 -webkit-background-clip: text; 53 -webkit-text-fill-color: transparent; 54 background-image: conic-gradient(#ed0101, blue); 55 pointer-events: none; 56 mix-blend-mode: difference; 57 filter: drop-shadow(2px 4px 6px black); 58} 59.a-second-title { 60 position: absolute; 61 margin-top: 25vh; 62 pointer-events: none; 63 -webkit-text-stroke: 1.3px white; 64 letter-spacing: 1.125px; 65 font-size: -webkit-xxx-large; 66 font-weight: 900; 67 mix-blend-mode: color-burn; 68 :has(input[value="dark"]:checked) & { 69 mix-blend-mode: color-dodge; 70 } 71} 72 73canvas { 74 width: 100%; 75 height: 100%; 76 background: var(--c-bg); 77} 78 79/*glass switcher*/ 80:has(input[value="dark"]:checked) { 81 --c-glass: #bbbbbc; 82 --c-light: #fff; 83 --c-dark: #000; 84 85 --c-content: #e1e1e1; 86 --c-action: #ffdc03; 87 88 --c-bg: #000; 89 90 --glass-reflex-dark: 2; 91 --glass-reflex-light: 0.3; 92 93 --saturation: 150%; 94} 95 96.switcher { 97 position: fixed; 98 z-index: 2; 99 top: 40px; 100 left: 50%; 101 translate: -50%; 102 display: flex; 103 align-items: center; 104 gap: 8px; 105 width: 168px; /* Adjusted for two options */ 106 max-width: 168px; /* Adjusted for two options */ 107 height: 70px; 108 box-sizing: border-box; 109 padding: 8px 12px 10px; 110 margin: 0 auto; 111 border: none; 112 border-radius: 99em; 113 font-size: var(--fz); 114 background-color: color-mix(in srgb, var(--c-glass) 12%, transparent); 115 backdrop-filter: blur(8px) url(#switcher) saturate(var(--saturation)); 116 -webkit-backdrop-filter: blur(8px) saturate(var(--saturation)); 117 box-shadow: inset 0 0 0 1px 118 color-mix( 119 in srgb, 120 var(--c-light) calc(var(--glass-reflex-light) * 10%), 121 transparent 122 ), 123 inset 1.8px 3px 0px -2px color-mix(in srgb, var(--c-light) 124 calc(var(--glass-reflex-light) * 90%), transparent), 125 inset -2px -2px 0px -2px color-mix(in srgb, var(--c-light) 126 calc(var(--glass-reflex-light) * 80%), transparent), 127 inset -3px -8px 1px -6px color-mix(in srgb, var(--c-light) 128 calc(var(--glass-reflex-light) * 60%), transparent), 129 inset -0.3px -1px 4px 0px 130 color-mix( 131 in srgb, 132 var(--c-dark) calc(var(--glass-reflex-dark) * 12%), 133 transparent 134 ), 135 inset -1.5px 2.5px 0px -2px 136 color-mix( 137 in srgb, 138 var(--c-dark) calc(var(--glass-reflex-dark) * 20%), 139 transparent 140 ), 141 inset 0px 3px 4px -2px color-mix(in srgb, var(--c-dark) 142 calc(var(--glass-reflex-dark) * 20%), transparent), 143 inset 2px -6.5px 1px -4px 144 color-mix( 145 in srgb, 146 var(--c-dark) calc(var(--glass-reflex-dark) * 10%), 147 transparent 148 ), 149 0px 1px 5px 0px 150 color-mix( 151 in srgb, 152 var(--c-dark) calc(var(--glass-reflex-dark) * 10%), 153 transparent 154 ), 155 0px 6px 16px 0px 156 color-mix( 157 in srgb, 158 var(--c-dark) calc(var(--glass-reflex-dark) * 8%), 159 transparent 160 ); 161 transition: background-color 400ms cubic-bezier(1, 0, 0.4, 1), 162 box-shadow 400ms cubic-bezier(1, 0, 0.4, 1); 163 backdrop-filter: blur(5px); 164} 165 166.switcher__legend { 167 position: absolute; 168 width: 1px; 169 height: 1px; 170 margin: -1px; 171 border: 0; 172 padding: 0; 173 white-space: nowrap; 174 clip-path: inset(100%); 175 clip: rect(0 0 0 0); 176 overflow: hidden; 177} 178 179.switcher__input { 180 clip: rect(0 0 0 0); 181 clip-path: inset(100%); 182 height: 1px; 183 width: 1px; 184 overflow: hidden; 185 position: absolute; 186 white-space: nowrap; 187} 188 189.switcher__icon { 190 display: block; 191 width: 100%; 192 transition: scale 200ms cubic-bezier(0.5, 0, 0, 1); 193} 194 195.switcher__filter { 196 position: absolute; 197 width: 0; 198 height: 0; 199 z-index: -1; 200} 201 202.switcher__option { 203 --c: var(--c-content); 204 display: flex; 205 justify-content: center; 206 align-items: center; 207 padding: 0 16px; 208 width: 68px; 209 height: 100%; 210 box-sizing: border-box; 211 border-radius: 99em; 212 opacity: 1; 213 transition: all 160ms; 214} 215 216.switcher__option:hover { 217 --c: var(--c-action); 218 cursor: pointer; 219} 220 221.switcher__option:hover .switcher__icon { 222 scale: 1.2; 223} 224 225.switcher__option:has(input:checked) { 226 --c: var(--c-content); 227 cursor: auto; 228} 229 230.switcher__option:has(input:checked) .switcher__icon { 231 scale: 1; 232} 233 234.switcher::after { 235 content: ""; 236 position: absolute; 237 left: 4px; 238 top: 4px; 239 display: block; 240 width: 84px; 241 height: calc(100% - 10px); 242 border-radius: 99em; 243 background-color: color-mix(in srgb, var(--c-glass) 36%, transparent); 244 z-index: -1; 245 box-shadow: inset 0 0 0 1px 246 color-mix( 247 in srgb, 248 var(--c-light) calc(var(--glass-reflex-light) * 10%), 249 transparent 250 ), 251 inset 2px 1px 0px -1px color-mix(in srgb, var(--c-light) 252 calc(var(--glass-reflex-light) * 90%), transparent), 253 inset -1.5px -1px 0px -1px color-mix(in srgb, var(--c-light) 254 calc(var(--glass-reflex-light) * 80%), transparent), 255 inset -2px -6px 1px -5px color-mix(in srgb, var(--c-light) 256 calc(var(--glass-reflex-light) * 60%), transparent), 257 inset -1px 2px 3px -1px 258 color-mix( 259 in srgb, 260 var(--c-dark) calc(var(--glass-reflex-dark) * 20%), 261 transparent 262 ), 263 inset 0px -4px 1px -2px 264 color-mix( 265 in srgb, 266 var(--c-dark) calc(var(--glass-reflex-dark) * 10%), 267 transparent 268 ), 269 0px 3px 6px 0px 270 color-mix( 271 in srgb, 272 var(--c-dark) calc(var(--glass-reflex-dark) * 8%), 273 transparent 274 ); 275} 276 277.switcher:has(input[c-option="1"]:checked)::after { 278 translate: 0 0; 279 transform-origin: right; 280 transition: background-color 400ms cubic-bezier(1, 0, 0.4, 1), 281 box-shadow 400ms cubic-bezier(1, 0, 0.4, 1), 282 translate 400ms cubic-bezier(1, 0, 0.4, 1); 283 animation: scaleToggle 440ms ease; 284} 285 286.switcher:has(input[c-option="2"]:checked)::after { 287 translate: 76px 0; 288 transition: background-color 400ms cubic-bezier(1, 0, 0.4, 1), 289 box-shadow 400ms cubic-bezier(1, 0, 0.4, 1), 290 translate 400ms cubic-bezier(1, 0, 0.4, 1); 291 animation: scaleToggle2 440ms ease; 292 transform-origin: left; /* Set transform-origin for the second option */ 293} 294 295@keyframes scaleToggle { 296 0% { 297 scale: 1 1; 298 } 299 50% { 300 scale: 1.1 1; 301 } 302 100% { 303 scale: 1 1; 304 } 305} 306 307@keyframes scaleToggle2 { 308 0% { 309 scale: 1 1; 310 } 311 50% { 312 scale: 1.1 1; /* Adjusted scale for consistency */ 313 } 314 100% { 315 scale: 1 1; 316 } 317}

How It Works (High-Level)


  1. WebGPU Initialization

Detects WebGPU support

Requests adapter and device

Configures the canvas for GPU rendering

Dynamically resizes buffers based on device limits


  1. Compute Shader Pipeline

Each simulation step is broken into GPU compute passes:

Velocity Update → Injects mouse force

Advection → Moves velocity and dye through the grid

Divergence Calculation → Detects compressibility

Pressure Solve → Enforces incompressibility

Gradient Subtraction → Corrects velocity

Vorticity Confinement → Adds swirling motion

Boundary Handling → Prevents fluid escape

  1. Dye Simulation

Dye is stored as RGB buffers

Mouse splats inject colored dye

Procedural palette animation based on time

Diffusion creates smooth color blending

  1. Rendering

A fullscreen quad renders dye buffers

Fragment shader samples dye storage buffers

Alpha is derived from dye intensity

Output is blended directly onto the canvas

Key Features


🚀 GPU-accelerated fluid simulation

🧠 WGSL compute shader architecture

🖱️ Interactive mouse-driven forces

🌊 Realistic swirling and diffusion

🎨 Procedural color palettes

🧩 Modular program-based design

⚡ Optimized buffer reuse and ping-ponging

Javascript Code


1/*main*/ 2let settings = { 3 grid_size: 64, 4 dye_size: 256, 5 sim_speed: 5, 6 contain_fluid: true, 7 velocity_add_intensity: 0.28, 8 velocity_add_radius: 0.001, 9 velocity_diffusion: 1, 10 dye_add_intensity: 0.8, 11 dye_add_radius: 0.0035, 12 dye_diffusion: 0.96204, 13 viscosity: 0, 14 vorticity: 0, 15 pressure_iterations: 8, 16 buffer_view: "dye", 17 input_symmetry: "none" 18}; 19 20let device, presentationFormat, canvas, context; 21 22const mouseInfos = { 23 current: null, 24 last: null, 25 velocity: null 26}; 27 28// Buffers 29let velocity, 30 velocity0, 31 dye, 32 dye0, 33 divergence, 34 divergence0, 35 pressure, 36 pressure0, 37 vorticity; 38 39// Uniforms 40const globalUniforms = {}; 41let time, 42 dt, 43 mouse, 44 grid, 45 uSimSpeed, 46 vel_force, 47 vel_radius, 48 vel_diff, 49 dye_force, 50 dye_radius, 51 dye_diff; 52let viscosity, uVorticity, containFluid, uSymmetry, uRenderIntensity; 53 54// Programs 55let checkerProgram, 56 updateDyeProgram, 57 updateProgram, 58 advectProgram, 59 boundaryProgram, 60 divergenceProgram; 61let boundaryDivProgram, 62 pressureProgram, 63 boundaryPressureProgram, 64 gradientSubtractProgram, 65 advectDyeProgram; 66let clearPressureProgram, 67 vorticityProgram, 68 vorticityConfinmentProgram, 69 renderProgram; 70 71function handlePointerMove(e) { 72 const pointer = e.touches ? e.touches[0] : e; 73 const rect = canvas.getBoundingClientRect(); 74 75 if (!mouseInfos.current) mouseInfos.current = []; 76 mouseInfos.current[0] = (pointer.clientX - rect.left) / rect.width; 77 mouseInfos.current[1] = 1 - (pointer.clientY - rect.top) / rect.height; // Invert Y 78} 79 80function onWebGPUDetectionError(error) { 81 console.log("Could not initialize WebGPU: " + error); 82 document.querySelector(".webgpu-not-supported").style.visibility = "visible"; 83 return false; 84} 85 86// Init the WebGPU context by checking first if everything is supported 87// Returns true on init success, false otherwise 88async function initContext() { 89 if (navigator.gpu == null) 90 return onWebGPUDetectionError("WebGPU NOT Supported"); 91 92 const adapter = await navigator.gpu.requestAdapter(); 93 if (!adapter) return onWebGPUDetectionError("No adapter found"); 94 95 device = await adapter.requestDevice(); 96 97 canvas = document.getElementById("fluid-webgpu"); 98 context = canvas.getContext("webgpu"); 99 if (!context) return onWebGPUDetectionError("Canvas does not support WebGPU"); 100 101 // If we got here, WebGPU seems to be supported 102 103 // Init canvas 104 canvas.style.width = "100%"; 105 canvas.style.height = "100%"; 106 canvas.addEventListener("mousemove", handlePointerMove); 107 canvas.addEventListener("touchmove", (e) => { 108 e.preventDefault(); 109 handlePointerMove(e); 110 }); 111 canvas.addEventListener("touchstart", (e) => { 112 e.preventDefault(); 113 handlePointerMove(e); 114 mouseInfos.last = [...mouseInfos.current]; 115 }); 116 117 // Init context 118 presentationFormat = navigator.gpu.getPreferredCanvasFormat(adapter); 119 120 context.configure({ 121 device, 122 format: presentationFormat, 123 usage: GPUTextureUsage.RENDER_ATTACHMENT, 124 alphaMode: "premultiplied" 125 }); 126 127 // Init buffer sizes 128 initSizes(); 129 130 // Resize event 131 let resizeTimeout; 132 window.addEventListener("resize", () => { 133 clearTimeout(resizeTimeout); 134 resizeTimeout = setTimeout(refreshSizes, 150); 135 }); 136 137 return true; 138} 139 140function refreshSizes() { 141 initSizes(); 142 initBuffers(); 143 initPrograms(); 144 globalUniforms.gridSize.needsUpdate = [ 145 settings.grid_w, 146 settings.grid_h, 147 settings.dye_w, 148 settings.dye_h, 149 settings.dx, 150 settings.rdx, 151 settings.dyeRdx 152 ]; 153} 154 155// Init buffer & canvas dimensions to fit the screen while keeping the aspect ratio 156// and downscaling the dimensions if they exceed the browsers capabilities 157function initSizes() { 158 const dpr = window.devicePixelRatio || 1; 159 const aspectRatio = window.innerWidth / window.innerHeight; 160 const maxBufferSize = device.limits.maxStorageBufferBindingSize; 161 const maxCanvasSize = device.limits.maxTextureDimension2D; 162 163 // Fit to screen while keeping the aspect ratio 164 const getPreferredDimensions = (baseSize) => { 165 let w, h; 166 const scaledBaseSize = baseSize * dpr; 167 168 if (aspectRatio > 1) { 169 h = scaledBaseSize; 170 w = Math.floor(h * aspectRatio); 171 } else { 172 w = scaledBaseSize; 173 h = Math.floor(w / aspectRatio); 174 } 175 176 return getValidDimensions(w, h); 177 }; 178 179 // Downscale if necessary to prevent crashes 180 const getValidDimensions = (w, h) => { 181 let downRatio = 1; 182 183 // Prevent buffer size overflow 184 if (w * h * 4 >= maxBufferSize) 185 downRatio = Math.sqrt(maxBufferSize / (w * h * 4)); 186 187 // Prevent canvas size overflow 188 if (w > maxCanvasSize) downRatio = maxCanvasSize / w; 189 else if (h > maxCanvasSize) downRatio = maxCanvasSize / h; 190 191 return { 192 w: Math.floor(w * downRatio), 193 h: Math.floor(h * downRatio) 194 }; 195 }; 196 197 // Calculate simulation buffer dimensions 198 let gridSize = getPreferredDimensions(settings.grid_size); 199 settings.grid_w = gridSize.w; 200 settings.grid_h = gridSize.h; 201 202 // Calculate dye & canvas buffer dimensions 203 let dyeSize = getPreferredDimensions(settings.dye_size); 204 settings.dye_w = dyeSize.w; 205 settings.dye_h = dyeSize.h; 206 207 // Useful values for the simulation 208 settings.rdx = settings.grid_size * 4; 209 settings.dyeRdx = settings.dye_size * 4; 210 settings.dx = 1 / settings.rdx; 211 212 // Resize the canvas 213 canvas.width = settings.dye_w; 214 canvas.height = settings.dye_h; 215} 216 217/*shaders*/ 218const STRUCT_GRID_SIZE = ` 219struct GridSize { 220 w : f32, 221 h : f32, 222 dyeW: f32, 223 dyeH: f32, 224 dx : f32, 225 rdx : f32, 226 dyeRdx : f32 227}`; 228 229const STRUCT_MOUSE = ` 230struct Mouse { 231 pos: vec2<f32>, 232 vel: vec2<f32>, 233}`; 234 235// This code initialize the pos and index variables and target only interior cells 236const COMPUTE_START = ` 237var pos = vec2<f32>(global_id.xy); 238 239if (pos.x == 0 || pos.y == 0 || pos.x >= uGrid.w - 1 || pos.y >= uGrid.h - 1) { 240 return; 241} 242 243let index = ID(pos.x, pos.y);`; 244 245const COMPUTE_START_DYE = ` 246var pos = vec2<f32>(global_id.xy); 247 248if (pos.x == 0 || pos.y == 0 || pos.x >= uGrid.dyeW - 1 || pos.y >= uGrid.dyeH - 1) { 249 return; 250} 251 252let index = ID(pos.x, pos.y);`; 253 254// This code initialize the pos and index variables and target all cells 255const COMPUTE_START_ALL = ` 256var pos = vec2<f32>(global_id.xy); 257 258if (pos.x >= uGrid.w || pos.y >= uGrid.h) { 259 return; 260} 261 262let index = ID(pos.x, pos.y);`; 263 264const SPLAT_CODE = ` 265var m = uMouse.pos; 266var v = uMouse.vel*2.; 267 268var splat = createSplat(p, m, v, uRadius); 269if (uSymmetry == 1. || uSymmetry == 3.) {splat += createSplat(p, vec2(1. - m.x, m.y), v * vec2(-1., 1.), uRadius);} 270if (uSymmetry == 2. || uSymmetry == 3.) {splat += createSplat(p, vec2(m.x, 1. - m.y), v * vec2(1., -1.), uRadius);} 271if (uSymmetry == 3. || uSymmetry == 4.) {splat += createSplat(p, vec2(1. - m.x, 1. - m.y), v * vec2(-1., -1.), uRadius);} 272`; 273 274/// APPLY FORCE SHADER /// 275 276const updateVelocityShader = /* wgsl */ ` 277 278${STRUCT_GRID_SIZE} 279 280struct Mouse { 281 pos: vec2<f32>, 282 vel: vec2<f32>, 283} 284@group(0) @binding(0) var<storage, read> x_in : array<f32>; 285@group(0) @binding(1) var<storage, read> y_in : array<f32>; 286@group(0) @binding(2) var<storage, read_write> x_out : array<f32>; 287@group(0) @binding(3) var<storage, read_write> y_out : array<f32>; 288@group(0) @binding(4) var<uniform> uGrid: GridSize; 289@group(0) @binding(5) var<uniform> uMouse: Mouse; 290@group(0) @binding(6) var<uniform> uForce : f32; 291@group(0) @binding(7) var<uniform> uRadius : f32; 292@group(0) @binding(8) var<uniform> uDiffusion : f32; 293@group(0) @binding(9) var<uniform> uDt : f32; 294@group(0) @binding(10) var<uniform> uTime : f32; 295@group(0) @binding(11) var<uniform> uSymmetry : f32; 296 297fn ID(x : f32, y : f32) -> u32 { return u32(x + y * uGrid.w); } 298fn inBetween(x : f32, lower : f32, upper : f32) -> bool { 299 return x > lower && x < upper; 300} 301fn inBounds(pos : vec2<f32>, xMin : f32, xMax : f32, yMin: f32, yMax : f32) -> bool { 302 return inBetween(pos.x, xMin * uGrid.w, xMax * uGrid.w) && inBetween(pos.y, yMin * uGrid.h, yMax * uGrid.h); 303} 304 305fn createSplat(pos : vec2<f32>, splatPos : vec2<f32>, vel : vec2<f32>, radius : f32) -> vec2<f32> { 306 var p = pos - splatPos; 307 p.x *= uGrid.w / uGrid.h; 308 var v = vel; 309 v.x *= uGrid.w / uGrid.h; 310 var splat = exp(-dot(p, p) / radius) * v; 311 return splat; 312} 313 314@compute @workgroup_size(8, 8) 315fn main(@builtin(global_invocation_id) global_id : vec3<u32>) { 316 317 ${COMPUTE_START} 318 319 let tmpT = uTime; 320 var p = pos/vec2(uGrid.w, uGrid.h); 321 322 ${SPLAT_CODE} 323 324 splat *= uForce * uDt * 200.; 325 326 x_out[index] = x_in[index]*uDiffusion + splat.x; 327 y_out[index] = y_in[index]*uDiffusion + splat.y; 328}`; 329 330const updateDyeShader = /* wgsl */ ` 331 332${STRUCT_GRID_SIZE} 333 334struct Mouse { 335 pos: vec2<f32>, 336 vel: vec2<f32>, 337} 338@group(0) @binding(0) var<storage, read> x_in : array<f32>; 339@group(0) @binding(1) var<storage, read> y_in : array<f32>; 340@group(0) @binding(2) var<storage, read> z_in : array<f32>; 341@group(0) @binding(3) var<storage, read_write> x_out : array<f32>; 342@group(0) @binding(4) var<storage, read_write> y_out : array<f32>; 343@group(0) @binding(5) var<storage, read_write> z_out : array<f32>; 344@group(0) @binding(6) var<uniform> uGrid: GridSize; 345@group(0) @binding(7) var<uniform> uMouse: Mouse; 346@group(0) @binding(8) var<uniform> uForce : f32; 347@group(0) @binding(9) var<uniform> uRadius : f32; 348@group(0) @binding(10) var<uniform> uDiffusion : f32; 349@group(0) @binding(11) var<uniform> uTime : f32; 350@group(0) @binding(12) var<uniform> uDt : f32; 351@group(0) @binding(13) var<uniform> uSymmetry : f32; 352 353fn ID(x : f32, y : f32) -> u32 { return u32(x + y * uGrid.dyeW); } 354fn inBetween(x : f32, lower : f32, upper : f32) -> bool { 355 return x > lower && x < upper; 356} 357fn inBounds(pos : vec2<f32>, xMin : f32, xMax : f32, yMin: f32, yMax : f32) -> bool { 358 return inBetween(pos.x, xMin * uGrid.dyeW, xMax * uGrid.dyeW) && inBetween(pos.y, yMin * uGrid.dyeH, yMax * uGrid.dyeH); 359} 360// cosine based palette, 4 vec3 params 361fn palette(t : f32, a : vec3<f32>, b : vec3<f32>, c : vec3<f32>, d : vec3<f32> ) -> vec3<f32> { 362 return a + b*cos( 6.28318*(c*t+d) ); 363} 364 365fn createSplat(pos : vec2<f32>, splatPos : vec2<f32>, vel : vec2<f32>, radius : f32) -> vec3<f32> { 366 var p = pos - splatPos; 367 p.x *= uGrid.w / uGrid.h; 368 var v = vel; 369 v.x *= uGrid.w / uGrid.h; 370 var splat = exp(-dot(p, p) / radius) * length(v); 371 return vec3(splat); 372} 373 374@compute @workgroup_size(8, 8) 375fn main(@builtin(global_invocation_id) global_id : vec3<u32>) { 376 377 ${COMPUTE_START_DYE} 378 379 let col_incr = 0.15; 380 let col_start = palette(uTime/8., vec3(1), vec3(0.5), vec3(1), vec3(0, col_incr, col_incr*2.)); 381 382 var p = pos/vec2(uGrid.dyeW, uGrid.dyeH); 383 384 ${SPLAT_CODE} 385 386 splat *= col_start * uForce * uDt * 100.; 387 388 x_out[index] = max(0., x_in[index]*uDiffusion + splat.x); 389 y_out[index] = max(0., y_in[index]*uDiffusion + splat.y); 390 z_out[index] = max(0., z_in[index]*uDiffusion + splat.z); 391}`; 392 393/// ADVECT SHADER /// 394 395const advectShader = /* wgsl */ ` 396 397${STRUCT_GRID_SIZE} 398 399@group(0) @binding(0) var<storage, read> x_in : array<f32>; 400@group(0) @binding(1) var<storage, read> y_in : array<f32>; 401@group(0) @binding(2) var<storage, read> x_vel : array<f32>; 402@group(0) @binding(3) var<storage, read> y_vel : array<f32>; 403@group(0) @binding(4) var<storage, read_write> x_out : array<f32>; 404@group(0) @binding(5) var<storage, read_write> y_out : array<f32>; 405@group(0) @binding(6) var<uniform> uGrid : GridSize; 406@group(0) @binding(7) var<uniform> uDt : f32; 407 408fn ID(x : f32, y : f32) -> u32 { return u32(x + y * uGrid.w); } 409fn in(x : f32, y : f32) -> vec2<f32> { let id = ID(x, y); return vec2(x_in[id], y_in[id]); } 410 411@compute @workgroup_size(8, 8) 412fn main(@builtin(global_invocation_id) global_id : vec3<u32>) { 413 414 ${COMPUTE_START} 415 416 var x = pos.x - uDt * uGrid.rdx * x_vel[index]; 417 var y = pos.y - uDt * uGrid.rdx * y_vel[index]; 418 419 if (x < 0) { x = 0; } 420 else if (x >= uGrid.w - 1) { x = uGrid.w - 1; } 421 if (y < 0) { y = 0; } 422 else if (y >= uGrid.h - 1) { y = uGrid.h - 1; } 423 424 let x1 = floor(x); 425 let y1 = floor(y); 426 let x2 = x1 + 1; 427 let y2 = y1 + 1; 428 429 let TL = in(x1, y2); 430 let TR = in(x2, y2); 431 let BL = in(x1, y1); 432 let BR = in(x2, y1); 433 434 let xMod = fract(x); 435 let yMod = fract(y); 436 437 let bilerp = mix( mix(BL, BR, xMod), mix(TL, TR, xMod), yMod ); 438 439 x_out[index] = bilerp.x; 440 y_out[index] = bilerp.y; 441}`; 442 443const advectDyeShader = /* wgsl */ ` 444 445${STRUCT_GRID_SIZE} 446 447@group(0) @binding(0) var<storage, read> x_in : array<f32>; 448@group(0) @binding(1) var<storage, read> y_in : array<f32>; 449@group(0) @binding(2) var<storage, read> z_in : array<f32>; 450@group(0) @binding(3) var<storage, read> x_vel : array<f32>; 451@group(0) @binding(4) var<storage, read> y_vel : array<f32>; 452@group(0) @binding(5) var<storage, read_write> x_out : array<f32>; 453@group(0) @binding(6) var<storage, read_write> y_out : array<f32>; 454@group(0) @binding(7) var<storage, read_write> z_out : array<f32>; 455@group(0) @binding(8) var<uniform> uGrid : GridSize; 456@group(0) @binding(9) var<uniform> uDt : f32; 457 458fn ID(x : f32, y : f32) -> u32 { return u32(x + y * uGrid.dyeW); } 459fn in(x : f32, y : f32) -> vec3<f32> { let id = ID(x, y); return vec3(x_in[id], y_in[id], z_in[id]); } 460fn vel(x : f32, y : f32) -> vec2<f32> { 461 let id = u32(i32(x) + i32(y) * i32(uGrid.w)); 462 return vec2(x_vel[id], y_vel[id]); 463} 464 465fn vel_bilerp(x0 : f32, y0 : f32) -> vec2<f32> { 466 var x = x0 * uGrid.w / uGrid.dyeW; 467 var y = y0 * uGrid.h / uGrid.dyeH; 468 469 if (x < 0) { x = 0; } 470 else if (x >= uGrid.w - 1) { x = uGrid.w - 1; } 471 if (y < 0) { y = 0; } 472 else if (y >= uGrid.h - 1) { y = uGrid.h - 1; } 473 474 let x1 = floor(x); 475 let y1 = floor(y); 476 let x2 = x1 + 1; 477 let y2 = y1 + 1; 478 479 let TL = vel(x1, y2); 480 let TR = vel(x2, y2); 481 let BL = vel(x1, y1); 482 let BR = vel(x2, y1); 483 484 let xMod = fract(x); 485 let yMod = fract(y); 486 487 return mix( mix(BL, BR, xMod), mix(TL, TR, xMod), yMod ); 488} 489 490@compute @workgroup_size(8, 8) 491fn main(@builtin(global_invocation_id) global_id : vec3<u32>) { 492 493 ${COMPUTE_START_DYE} 494 495 let V = vel_bilerp(pos.x, pos.y); 496 497 var x = pos.x - uDt * uGrid.dyeRdx * V.x; 498 var y = pos.y - uDt * uGrid.dyeRdx * V.y; 499 500 if (x < 0) { x = 0; } 501 else if (x >= uGrid.dyeW - 1) { x = uGrid.dyeW - 1; } 502 if (y < 0) { y = 0; } 503 else if (y >= uGrid.dyeH - 1) { y = uGrid.dyeH - 1; } 504 505 let x1 = floor(x); 506 let y1 = floor(y); 507 let x2 = x1 + 1; 508 let y2 = y1 + 1; 509 510 let TL = in(x1, y2); 511 let TR = in(x2, y2); 512 let BL = in(x1, y1); 513 let BR = in(x2, y1); 514 515 let xMod = fract(x); 516 let yMod = fract(y); 517 518 let bilerp = mix( mix(BL, BR, xMod), mix(TL, TR, xMod), yMod ); 519 520 x_out[index] = bilerp.x; 521 y_out[index] = bilerp.y; 522 z_out[index] = bilerp.z; 523}`; 524 525/// DIVERGENCE SHADER /// 526 527const divergenceShader = /* wgsl */ ` 528 529${STRUCT_GRID_SIZE} 530 531@group(0) @binding(0) var<storage, read> x_vel : array<f32>; 532@group(0) @binding(1) var<storage, read> y_vel : array<f32>; 533@group(0) @binding(2) var<storage, read_write> div : array<f32>; 534@group(0) @binding(3) var<uniform> uGrid : GridSize; 535 536fn ID(x : f32, y : f32) -> u32 { return u32(x + y * uGrid.w); } 537fn vel(x : f32, y : f32) -> vec2<f32> { let id = ID(x, y); return vec2(x_vel[id], y_vel[id]); } 538 539@compute @workgroup_size(8, 8) 540fn main(@builtin(global_invocation_id) global_id : vec3<u32>) { 541 542 ${COMPUTE_START} 543 544 let L = vel(pos.x - 1, pos.y).x; 545 let R = vel(pos.x + 1, pos.y).x; 546 let B = vel(pos.x, pos.y - 1).y; 547 let T = vel(pos.x, pos.y + 1).y; 548 549 div[index] = 0.5 * uGrid.rdx * ((R - L) + (T - B)); 550}`; 551 552/// PRESSURE SHADER /// 553 554const pressureShader = /* wgsl */ ` 555 556${STRUCT_GRID_SIZE} 557 558@group(0) @binding(0) var<storage, read> pres_in : array<f32>; 559@group(0) @binding(1) var<storage, read> div : array<f32>; 560@group(0) @binding(2) var<storage, read_write> pres_out : array<f32>; 561@group(0) @binding(3) var<uniform> uGrid : GridSize; 562 563fn ID(x : f32, y : f32) -> u32 { return u32(x + y * uGrid.w); } 564fn in(x : f32, y : f32) -> f32 { let id = ID(x, y); return pres_in[id]; } 565 566@compute @workgroup_size(8, 8) 567fn main(@builtin(global_invocation_id) global_id : vec3<u32>) { 568 569 ${COMPUTE_START} 570 571 let L = pos - vec2(1, 0); 572 let R = pos + vec2(1, 0); 573 let B = pos - vec2(0, 1); 574 let T = pos + vec2(0, 1); 575 576 let Lx = in(L.x, L.y); 577 let Rx = in(R.x, R.y); 578 let Bx = in(B.x, B.y); 579 let Tx = in(T.x, T.y); 580 581 let bC = div[index]; 582 583 let alpha = -(uGrid.dx * uGrid.dx); 584 let rBeta = .25; 585 586 pres_out[index] = (Lx + Rx + Bx + Tx + alpha * bC) * rBeta; 587}`; 588 589/// GRADIENT SUBTRACT SHADER /// 590 591const gradientSubtractShader = /* wgsl */ ` 592 593${STRUCT_GRID_SIZE} 594 595@group(0) @binding(0) var<storage, read> pressure : array<f32>; 596@group(0) @binding(1) var<storage, read> x_vel : array<f32>; 597@group(0) @binding(2) var<storage, read> y_vel : array<f32>; 598@group(0) @binding(3) var<storage, read_write> x_out : array<f32>; 599@group(0) @binding(4) var<storage, read_write> y_out : array<f32>; 600@group(0) @binding(5) var<uniform> uGrid : GridSize; 601 602fn ID(x : f32, y : f32) -> u32 { return u32(x + y * uGrid.w); } 603fn pres(x : f32, y : f32) -> f32 { let id = ID(x, y); return pressure[id]; } 604 605@compute @workgroup_size(8, 8) 606fn main(@builtin(global_invocation_id) global_id : vec3<u32>) { 607 608 ${COMPUTE_START} 609 610 let L = pos - vec2(1, 0); 611 let R = pos + vec2(1, 0); 612 let B = pos - vec2(0, 1); 613 let T = pos + vec2(0, 1); 614 615 let xL = pres(L.x, L.y); 616 let xR = pres(R.x, R.y); 617 let yB = pres(B.x, B.y); 618 let yT = pres(T.x, T.y); 619 620 let finalX = x_vel[index] - .5 * uGrid.rdx * (xR - xL); 621 let finalY = y_vel[index] - .5 * uGrid.rdx * (yT - yB); 622 623 x_out[index] = finalX; 624 y_out[index] = finalY; 625}`; 626 627/// VORTICITY SHADER /// 628 629const vorticityShader = /* wgsl */ ` 630 631${STRUCT_GRID_SIZE} 632 633@group(0) @binding(0) var<storage, read> x_vel : array<f32>; 634@group(0) @binding(1) var<storage, read> y_vel : array<f32>; 635@group(0) @binding(2) var<storage, read_write> vorticity : array<f32>; 636@group(0) @binding(3) var<uniform> uGrid : GridSize; 637 638fn ID(x : f32, y : f32) -> u32 { return u32(x + y * uGrid.w); } 639fn vel(x : f32, y : f32) -> vec2<f32> { let id = ID(x, y); return vec2(x_vel[id], y_vel[id]); } 640 641@compute @workgroup_size(8, 8) 642fn main(@builtin(global_invocation_id) global_id : vec3<u32>) { 643 644 ${COMPUTE_START} 645 646 let Ly = vel(pos.x - 1, pos.y).y; 647 let Ry = vel(pos.x + 1, pos.y).y; 648 let Bx = vel(pos.x, pos.y - 1).x; 649 let Tx = vel(pos.x, pos.y + 1).x; 650 651 vorticity[index] = 0.5 * uGrid.rdx * ((Ry - Ly) - (Tx - Bx)); 652}`; 653 654/// VORTICITY CONFINMENT SHADER /// 655 656const vorticityConfinmentShader = /* wgsl */ ` 657 658${STRUCT_GRID_SIZE} 659 660@group(0) @binding(0) var<storage, read> x_vel_in : array<f32>; 661@group(0) @binding(1) var<storage, read> y_vel_in : array<f32>; 662@group(0) @binding(2) var<storage, read> vorticity : array<f32>; 663@group(0) @binding(3) var<storage, read_write> x_vel_out : array<f32>; 664@group(0) @binding(4) var<storage, read_write> y_vel_out : array<f32>; 665@group(0) @binding(5) var<uniform> uGrid : GridSize; 666@group(0) @binding(6) var<uniform> uDt : f32; 667@group(0) @binding(7) var<uniform> uVorticity : f32; 668 669fn ID(x : f32, y : f32) -> u32 { return u32(x + y * uGrid.w); } 670fn vort(x : f32, y : f32) -> f32 { let id = ID(x, y); return vorticity[id]; } 671 672@compute @workgroup_size(8, 8) 673fn main(@builtin(global_invocation_id) global_id : vec3<u32>) { 674 675 ${COMPUTE_START} 676 677 let L = vort(pos.x - 1, pos.y); 678 let R = vort(pos.x + 1, pos.y); 679 let B = vort(pos.x, pos.y - 1); 680 let T = vort(pos.x, pos.y + 1); 681 let C = vorticity[index]; 682 683 var force = 0.5 * uGrid.rdx * vec2(abs(T) - abs(B), abs(R) - abs(L)); 684 685 let epsilon = 2.4414e-4; 686 let magSqr = max(epsilon, dot(force, force)); 687 688 force = force / sqrt(magSqr); 689 force *= uGrid.dx * uVorticity * uDt * C * vec2(1, -1); 690 691 x_vel_out[index] = x_vel_in[index] + force.x; 692 y_vel_out[index] = y_vel_in[index] + force.y; 693}`; 694 695/// CLEAR PRESSURE SHADER /// 696 697const clearPressureShader = /* wgsl */ ` 698 699${STRUCT_GRID_SIZE} 700 701@group(0) @binding(0) var<storage, read> x_in : array<f32>; 702@group(0) @binding(1) var<storage, read_write> x_out : array<f32>; 703@group(0) @binding(2) var<uniform> uGrid : GridSize; 704@group(0) @binding(3) var<uniform> uVisc : f32; 705 706fn ID(x : f32, y : f32) -> u32 { return u32(x + y * uGrid.w); } 707 708@compute @workgroup_size(8, 8) 709fn main(@builtin(global_invocation_id) global_id : vec3<u32>) { 710 711 ${COMPUTE_START_ALL} 712 713 x_out[index] = x_in[index]*uVisc; 714}`; 715 716/// BOUNDARY SHADER /// 717 718const boundaryShader = /* wgsl */ ` 719 720${STRUCT_GRID_SIZE} 721 722@group(0) @binding(0) var<storage, read> x_in : array<f32>; 723@group(0) @binding(1) var<storage, read> y_in : array<f32>; 724@group(0) @binding(2) var<storage, read_write> x_out : array<f32>; 725@group(0) @binding(3) var<storage, read_write> y_out : array<f32>; 726@group(0) @binding(4) var<uniform> uGrid : GridSize; 727@group(0) @binding(5) var<uniform> containFluid : f32; 728 729fn ID(x : f32, y : f32) -> u32 { return u32(x + y * uGrid.w); } 730 731@compute @workgroup_size(8, 8) 732fn main(@builtin(global_invocation_id) global_id : vec3<u32>) { 733 734 ${COMPUTE_START_ALL} 735 736 // disable scale to disable contained bounds 737 var scaleX = 1.; 738 var scaleY = 1.; 739 740 if (pos.x == 0) { pos.x += 1; scaleX = -1.; } 741 else if (pos.x == uGrid.w - 1) { pos.x -= 1; scaleX = -1.; } 742 if (pos.y == 0) { pos.y += 1; scaleY = -1.; } 743 else if (pos.y == uGrid.h - 1) { pos.y -= 1; scaleY = -1.; } 744 745 if (containFluid == 0.) { 746 scaleX = 1.; 747 scaleY = 1.; 748 } 749 750 x_out[index] = x_in[ID(pos.x, pos.y)] * scaleX; 751 y_out[index] = y_in[ID(pos.x, pos.y)] * scaleY; 752}`; 753 754/// BOUNDARY PRESSURE SHADER /// 755 756const boundaryPressureShader = /* wgsl */ ` 757 758${STRUCT_GRID_SIZE} 759 760@group(0) @binding(0) var<storage, read> x_in : array<f32>; 761@group(0) @binding(1) var<storage, read_write> x_out : array<f32>; 762@group(0) @binding(2) var<uniform> uGrid : GridSize; 763 764fn ID(x : f32, y : f32) -> u32 { return u32(x + y * uGrid.w); } 765 766@compute @workgroup_size(8, 8) 767fn main(@builtin(global_invocation_id) global_id : vec3<u32>) { 768 769 ${COMPUTE_START_ALL} 770 771 if (pos.x == 0) { pos.x += 1; } 772 else if (pos.x == uGrid.w - 1) { pos.x -= 1; } 773 if (pos.y == 0) { pos.y += 1; } 774 else if (pos.y == uGrid.h - 1) { pos.y -= 1; } 775 776 x_out[index] = x_in[ID(pos.x, pos.y)]; 777}`; 778 779const checkerboardShader = /* wgsl */ ` 780 781${STRUCT_GRID_SIZE} 782 783@group(0) @binding(0) var<storage, read_write> x_out : array<f32>; 784@group(0) @binding(1) var<storage, read_write> y_out : array<f32>; 785@group(0) @binding(2) var<storage, read_write> z_out : array<f32>; 786@group(0) @binding(3) var<uniform> uGrid : GridSize; 787@group(0) @binding(4) var<uniform> uTime : f32; 788 789fn ID(x : f32, y : f32) -> u32 { return u32(x + y * uGrid.dyeW); } 790 791fn noise(p_ : vec3<f32>) -> f32 { 792 var p = p_; 793 var ip=floor(p); 794 p-=ip; 795 var s=vec3(7.,157.,113.); 796 var h=vec4(0.,s.y, s.z,s.y+s.z)+dot(ip,s); 797 p=p*p*(3. - 2.*p); 798 h=mix(fract(sin(h)*43758.5),fract(sin(h+s.x)*43758.5),p.x); 799 var r=mix(h.xz,h.yw,p.y); 800 h.x = r.x; 801 h.y = r.y; 802 return mix(h.x,h.y,p.z); 803} 804 805fn fbm(p_ : vec3<f32>, octaveNum : i32) -> vec2<f32> { 806 var p=p_; 807 var acc = vec2(0.); 808 var freq = 1.0; 809 var amp = 0.5; 810 var shift = vec3(100.); 811 for (var i = 0; i < octaveNum; i++) { 812 acc += vec2(noise(p), noise(p + vec3(0.,0.,10.))) * amp; 813 p = p * 2.0 + shift; 814 amp *= 0.5; 815 } 816 return acc; 817} 818 819@compute @workgroup_size(8, 8) 820fn main(@builtin(global_invocation_id) global_id : vec3<u32>) { 821 822 ${COMPUTE_START_DYE} 823 824 var uv = pos/vec2(uGrid.dyeW, uGrid.dyeH); 825 var zoom = 4.; 826 827 var smallNoise = fbm(vec3(uv.x*zoom*2., uv.y*zoom*2., uTime+2.145), 7) - .5; 828 var bigNoise = fbm(vec3(uv.x*zoom, uv.y*zoom, uTime*.1+30.), 7) - .5; 829 830 var noise = max(length(bigNoise) * 0.035, 0.); 831 var noise2 = max(length(smallNoise) * 0.035, 0.); 832 833 noise = noise + noise2 * .05; 834 835 var czoom = 4.; 836 var n = fbm(vec3(uv.x*czoom, uv.y*czoom, uTime*.1+63.1), 7)*.75+.25; 837 var n2 = fbm(vec3(uv.x*czoom, uv.y*czoom, uTime*.1+23.4), 7)*.75+.25; 838 839 var col = vec3(1.); 840 841 x_out[index] += noise * col.x; 842 y_out[index] += noise * col.y; 843 z_out[index] += noise * col.z; 844}`; 845 846/*render*/ 847const renderShader = /* wgsl */ ` 848${STRUCT_GRID_SIZE} 849 850struct VertexOut { 851 @builtin(position) position : vec4<f32>, 852 @location(1) uv : vec2<f32>, 853}; 854 855@group(0) @binding(0) var<storage, read> fieldX : array<f32>; 856@group(0) @binding(1) var<storage, read> fieldY : array<f32>; 857@group(0) @binding(2) var<storage, read> fieldZ : array<f32>; 858@group(0) @binding(3) var<uniform> uGrid : GridSize; 859@group(0) @binding(4) var<uniform> multiplier : f32; 860 861 862@vertex 863fn vertex_main(@location(0) position: vec4<f32>) -> VertexOut 864{ 865 var output : VertexOut; 866 output.position = position; 867 output.uv = position.xy*.5+.5; 868 return output; 869} 870 871@fragment 872fn fragment_main(fragData : VertexOut) -> @location(0) vec4<f32> 873{ 874 var w = uGrid.dyeW; 875 var h = uGrid.dyeH; 876 877 let fuv = vec2<f32>((floor(fragData.uv*vec2(w, h)))); 878 let id = u32(fuv.x + fuv.y * w); 879 880 let r = fieldX[id]; 881 let g = fieldY[id]; 882 let b = fieldZ[id]; 883 let col = vec3(r, g, b); 884 885 let alpha = clamp(length(col), 0.0, 1.0); 886 return vec4(col * multiplier, alpha); 887} 888`; 889 890// Renders 3 (r, g, b) storage buffers to the canvas 891class RenderProgram { 892 constructor() { 893 const vertices = new Float32Array([ 894 -1, 895 -1, 896 0, 897 1, 898 -1, 899 1, 900 0, 901 1, 902 1, 903 -1, 904 0, 905 1, 906 1, 907 -1, 908 0, 909 1, 910 -1, 911 1, 912 0, 913 1, 914 1, 915 1, 916 0, 917 1 918 ]); 919 920 this.vertexBuffer = device.createBuffer({ 921 size: vertices.byteLength, 922 usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, 923 mappedAtCreation: true 924 }); 925 new Float32Array(this.vertexBuffer.getMappedRange()).set(vertices); 926 this.vertexBuffer.unmap(); 927 928 const vertexBuffersDescriptors = [ 929 { 930 attributes: [ 931 { 932 shaderLocation: 0, 933 offset: 0, 934 format: "float32x4" 935 } 936 ], 937 arrayStride: 16, 938 stepMode: "vertex" 939 } 940 ]; 941 942 const shaderModule = device.createShaderModule({ 943 code: renderShader 944 }); 945 946 this.renderPipeline = device.createRenderPipeline({ 947 layout: "auto", 948 vertex: { 949 module: shaderModule, 950 entryPoint: "vertex_main", 951 buffers: vertexBuffersDescriptors 952 }, 953 fragment: { 954 module: shaderModule, 955 entryPoint: "fragment_main", 956 targets: [ 957 { 958 format: presentationFormat 959 } 960 ] 961 }, 962 primitive: { 963 topology: "triangle-list" 964 } 965 }); 966 967 // The r,g,b buffer containing the data to render 968 this.buffer = new DynamicBuffer({ 969 dims: 3, 970 w: settings.dye_w, 971 h: settings.dye_h 972 }); 973 974 // Uniforms 975 const entries = [ 976 ...this.buffer.buffers, 977 globalUniforms.gridSize.buffer, 978 globalUniforms.render_intensity_multiplier.buffer 979 ].map((b, i) => ({ 980 binding: i, 981 resource: { buffer: b } 982 })); 983 984 this.renderBindGroup = device.createBindGroup({ 985 layout: this.renderPipeline.getBindGroupLayout(0), 986 entries 987 }); 988 989 this.renderPassDescriptor = { 990 colorAttachments: [ 991 { 992 clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 0.0 }, 993 loadOp: "clear", 994 storeOp: "store" 995 } 996 ] 997 }; 998 } 999 1000 // Dispatch a draw command to render on the canvas 1001 dispatch(commandEncoder) { 1002 this.renderPassDescriptor.colorAttachments[0].view = context 1003 .getCurrentTexture() 1004 .createView(); 1005 1006 const renderPassEncoder = commandEncoder.beginRenderPass( 1007 this.renderPassDescriptor 1008 ); 1009 1010 renderPassEncoder.setPipeline(this.renderPipeline); 1011 renderPassEncoder.setBindGroup(0, this.renderBindGroup); 1012 renderPassEncoder.setVertexBuffer(0, this.vertexBuffer); 1013 renderPassEncoder.draw(6); 1014 renderPassEncoder.end(); 1015 } 1016} 1017 1018/*utils*/ 1019// Creates and manage multi-dimensional buffers by creating a buffer for each dimension 1020class DynamicBuffer { 1021 constructor({ 1022 dims = 1, // Number of dimensions 1023 w = settings.grid_w, // Buffer width 1024 h = settings.grid_h // Buffer height 1025 } = {}) { 1026 this.dims = dims; 1027 this.bufferSize = w * h * 4; 1028 this.w = w; 1029 this.h = h; 1030 this.buffers = new Array(dims).fill().map((_) => 1031 device.createBuffer({ 1032 size: this.bufferSize, 1033 usage: 1034 GPUBufferUsage.STORAGE | 1035 GPUBufferUsage.COPY_SRC | 1036 GPUBufferUsage.COPY_DST 1037 }) 1038 ); 1039 } 1040 1041 // Copy each buffer to another DynamicBuffer's buffers. 1042 // If the dimensions don't match, the last non-empty dimension will be copied instead 1043 copyTo(buffer, commandEncoder) { 1044 for (let i = 0; i < Math.max(this.dims, buffer.dims); i++) { 1045 commandEncoder.copyBufferToBuffer( 1046 this.buffers[Math.min(i, this.buffers.length - 1)], 1047 0, 1048 buffer.buffers[Math.min(i, buffer.buffers.length - 1)], 1049 0, 1050 this.bufferSize 1051 ); 1052 } 1053 } 1054 1055 // Reset all the buffers 1056 clear(queue) { 1057 for (let i = 0; i < this.dims; i++) { 1058 queue.writeBuffer(this.buffers[i], 0, new Float32Array(this.w * this.h)); 1059 } 1060 } 1061} 1062 1063// Manage uniform buffers relative to the compute shaders 1064class Uniform { 1065 constructor(name, { size, value } = {}) { 1066 this.name = name; 1067 this.size = size ?? (value && typeof value === "object" ? value.length : 1); 1068 this.needsUpdate = false; 1069 1070 if (this.size === 1) { 1071 if (settings[name] == null) { 1072 settings[name] = value ?? 0; 1073 this.alwaysUpdate = true; 1074 } 1075 } 1076 1077 if (this.size === 1 || value != null) { 1078 this.buffer = device.createBuffer({ 1079 mappedAtCreation: true, 1080 size: this.size * 4, 1081 usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST 1082 }); 1083 1084 const arrayBuffer = this.buffer.getMappedRange(); 1085 const sourceValue = value ?? [settings[this.name]]; 1086 const sourceArray = 1087 typeof sourceValue === "number" 1088 ? [sourceValue] 1089 : Array.isArray(sourceValue) 1090 ? sourceValue 1091 : [0]; // Default to [0] if value is invalid 1092 new Float32Array(arrayBuffer).set(new Float32Array(sourceArray)); 1093 this.buffer.unmap(); 1094 } else { 1095 this.buffer = device.createBuffer({ 1096 size: this.size * 4, 1097 usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST 1098 }); 1099 } 1100 1101 globalUniforms[name] = this; 1102 } 1103 1104 setValue(value) { 1105 settings[this.name] = value; 1106 this.needsUpdate = true; 1107 } 1108 1109 update(queue, value) { 1110 if (this.needsUpdate || this.alwaysUpdate || value != null) { 1111 if (typeof this.needsUpdate !== "boolean") value = this.needsUpdate; 1112 queue.writeBuffer( 1113 this.buffer, 1114 0, 1115 new Float32Array(value ?? [parseFloat(settings[this.name])]), 1116 0, 1117 this.size 1118 ); 1119 this.needsUpdate = false; 1120 } 1121 } 1122} 1123 1124// Creates a shader module, compute pipeline & bind group to use with the GPU 1125class Program { 1126 constructor({ 1127 buffers = [], // Storage buffers 1128 uniforms = [], // Uniform buffers 1129 shader, // WGSL Compute Shader as a string 1130 dispatchX = settings.grid_w, // Dispatch workers width 1131 dispatchY = settings.grid_h // Dispatch workers height 1132 }) { 1133 this.computePipeline = device.createComputePipeline({ 1134 layout: "auto", 1135 compute: { 1136 module: device.createShaderModule({ code: shader }), 1137 entryPoint: "main" 1138 } 1139 }); 1140 1141 const storageEntries = buffers.map((b) => b.buffers).flat(); 1142 const uniformEntries = uniforms 1143 .filter((u) => u && u.buffer) 1144 .map((u) => u.buffer); 1145 1146 const allEntries = [...storageEntries, ...uniformEntries].map( 1147 (buffer, i) => ({ 1148 binding: i, 1149 resource: { buffer } 1150 }) 1151 ); 1152 1153 this.bindGroup = device.createBindGroup({ 1154 layout: this.computePipeline.getBindGroupLayout(0), 1155 entries: allEntries 1156 }); 1157 1158 this.dispatchX = dispatchX; 1159 this.dispatchY = dispatchY; 1160 } 1161 1162 dispatch(passEncoder) { 1163 passEncoder.setPipeline(this.computePipeline); 1164 passEncoder.setBindGroup(0, this.bindGroup); 1165 passEncoder.dispatchWorkgroups( 1166 Math.ceil(this.dispatchX / 8), 1167 Math.ceil(this.dispatchY / 8) 1168 ); 1169 } 1170} 1171 1172/// Useful classes for cleaner understanding of the input and output buffers 1173/// used in the declarations of programs & fluid simulation steps 1174 1175class AdvectProgram extends Program { 1176 constructor({ 1177 in_quantity, 1178 in_velocity, 1179 out_quantity, 1180 uniforms, 1181 shader = advectShader, 1182 ...props 1183 }) { 1184 uniforms ??= [globalUniforms.gridSize]; 1185 super({ 1186 buffers: [in_quantity, in_velocity, out_quantity], 1187 uniforms, 1188 shader, 1189 ...props 1190 }); 1191 } 1192} 1193 1194class DivergenceProgram extends Program { 1195 constructor({ 1196 in_velocity, 1197 out_divergence, 1198 uniforms, 1199 shader = divergenceShader 1200 }) { 1201 uniforms ??= [globalUniforms.gridSize]; 1202 super({ buffers: [in_velocity, out_divergence], uniforms, shader }); 1203 } 1204} 1205 1206class PressureProgram extends Program { 1207 constructor({ 1208 in_pressure, 1209 in_divergence, 1210 out_pressure, 1211 uniforms, 1212 shader = pressureShader 1213 }) { 1214 uniforms ??= [globalUniforms.gridSize]; 1215 super({ 1216 buffers: [in_pressure, in_divergence, out_pressure], 1217 uniforms, 1218 shader 1219 }); 1220 } 1221} 1222 1223class GradientSubtractProgram extends Program { 1224 constructor({ 1225 in_pressure, 1226 in_velocity, 1227 out_velocity, 1228 uniforms, 1229 shader = gradientSubtractShader 1230 }) { 1231 uniforms ??= [globalUniforms.gridSize]; 1232 super({ 1233 buffers: [in_pressure, in_velocity, out_velocity], 1234 uniforms, 1235 shader 1236 }); 1237 } 1238} 1239 1240class BoundaryProgram extends Program { 1241 constructor({ 1242 in_quantity, 1243 out_quantity, 1244 uniforms, 1245 shader = boundaryShader 1246 }) { 1247 uniforms ??= [globalUniforms.gridSize]; 1248 super({ buffers: [in_quantity, out_quantity], uniforms, shader }); 1249 } 1250} 1251 1252class UpdateProgram extends Program { 1253 constructor({ 1254 in_quantity, 1255 out_quantity, 1256 uniforms, 1257 shader = updateVelocityShader, 1258 ...props 1259 }) { 1260 uniforms ??= [globalUniforms.gridSize]; 1261 super({ buffers: [in_quantity, out_quantity], uniforms, shader, ...props }); 1262 } 1263} 1264 1265class VorticityProgram extends Program { 1266 constructor({ 1267 in_velocity, 1268 out_vorticity, 1269 uniforms, 1270 shader = vorticityShader, 1271 ...props 1272 }) { 1273 uniforms ??= [globalUniforms.gridSize]; 1274 super({ 1275 buffers: [in_velocity, out_vorticity], 1276 uniforms, 1277 shader, 1278 ...props 1279 }); 1280 } 1281} 1282 1283class VorticityConfinmentProgram extends Program { 1284 constructor({ 1285 in_velocity, 1286 in_vorticity, 1287 out_velocity, 1288 uniforms, 1289 shader = vorticityConfinmentShader, 1290 ...props 1291 }) { 1292 uniforms ??= [globalUniforms.gridSize]; 1293 super({ 1294 buffers: [in_velocity, in_vorticity, out_velocity], 1295 uniforms, 1296 shader, 1297 ...props 1298 }); 1299 } 1300} 1301 1302function initBuffers() { 1303 velocity = new DynamicBuffer({ dims: 2 }); 1304 velocity0 = new DynamicBuffer({ dims: 2 }); 1305 1306 dye = new DynamicBuffer({ dims: 3, w: settings.dye_w, h: settings.dye_h }); 1307 dye0 = new DynamicBuffer({ dims: 3, w: settings.dye_w, h: settings.dye_h }); 1308 1309 divergence = new DynamicBuffer(); 1310 divergence0 = new DynamicBuffer(); 1311 1312 pressure = new DynamicBuffer(); 1313 pressure0 = new DynamicBuffer(); 1314 1315 vorticity = new DynamicBuffer(); 1316} 1317 1318function initUniforms() { 1319 time = new Uniform("time"); 1320 dt = new Uniform("dt"); 1321 mouse = new Uniform("mouseInfos", { size: 4 }); 1322 grid = new Uniform("gridSize", { 1323 size: 7, 1324 value: [ 1325 settings.grid_w, 1326 settings.grid_h, 1327 settings.dye_w, 1328 settings.dye_h, 1329 settings.dx, 1330 settings.rdx, 1331 settings.dyeRdx 1332 ] 1333 }); 1334 uSimSpeed = new Uniform("sim_speed", { value: settings.sim_speed }); 1335 vel_force = new Uniform("velocity_add_intensity", { 1336 value: settings.velocity_add_intensity 1337 }); 1338 vel_radius = new Uniform("velocity_add_radius", { 1339 value: settings.velocity_add_radius 1340 }); 1341 vel_diff = new Uniform("velocity_diffusion", { 1342 value: settings.velocity_diffusion 1343 }); 1344 dye_force = new Uniform("dye_add_intensity", { 1345 value: settings.dye_add_intensity 1346 }); 1347 dye_radius = new Uniform("dye_add_radius", { 1348 value: settings.dye_add_radius 1349 }); 1350 dye_diff = new Uniform("dye_diffusion", { 1351 value: settings.dye_diffusion 1352 }); 1353 viscosity = new Uniform("viscosity", { 1354 value: settings.viscosity 1355 }); 1356 uVorticity = new Uniform("vorticity", { 1357 value: settings.vorticity 1358 }); 1359 containFluid = new Uniform("contain_fluid", { 1360 value: settings.contain_fluid 1361 }); 1362 uSymmetry = new Uniform("mouse_type", { value: 0 }); 1363 uRenderIntensity = new Uniform("render_intensity_multiplier", { value: 1 }); 1364} 1365 1366function initPrograms() { 1367 checkerProgram = new Program({ 1368 buffers: [dye], 1369 shader: checkerboardShader, 1370 dispatchX: settings.dye_w, 1371 dispatchY: settings.dye_h, 1372 uniforms: [grid, time] 1373 }); 1374 1375 updateDyeProgram = new UpdateProgram({ 1376 in_quantity: dye, 1377 out_quantity: dye0, 1378 uniforms: [ 1379 grid, 1380 mouse, 1381 dye_force, 1382 dye_radius, 1383 dye_diff, 1384 time, 1385 dt, 1386 uSymmetry 1387 ], 1388 dispatchX: settings.dye_w, 1389 dispatchY: settings.dye_h, 1390 shader: updateDyeShader 1391 }); 1392 1393 updateProgram = new UpdateProgram({ 1394 in_quantity: velocity, 1395 out_quantity: velocity0, 1396 uniforms: [ 1397 grid, 1398 mouse, 1399 vel_force, 1400 vel_radius, 1401 vel_diff, 1402 dt, 1403 time, 1404 uSymmetry 1405 ] 1406 }); 1407 1408 advectProgram = new AdvectProgram({ 1409 in_quantity: velocity0, 1410 in_velocity: velocity0, 1411 out_quantity: velocity, 1412 uniforms: [grid, dt] 1413 }); 1414 1415 boundaryProgram = new BoundaryProgram({ 1416 in_quantity: velocity, 1417 out_quantity: velocity0, 1418 uniforms: [grid, containFluid] 1419 }); 1420 1421 divergenceProgram = new DivergenceProgram({ 1422 in_velocity: velocity0, 1423 out_divergence: divergence0 1424 }); 1425 1426 boundaryDivProgram = new BoundaryProgram({ 1427 in_quantity: divergence0, 1428 out_quantity: divergence, 1429 shader: boundaryPressureShader 1430 }); 1431 1432 pressureProgram = new PressureProgram({ 1433 in_pressure: pressure, 1434 in_divergence: divergence, 1435 out_pressure: pressure0 1436 }); 1437 1438 boundaryPressureProgram = new BoundaryProgram({ 1439 in_quantity: pressure0, 1440 out_quantity: pressure, 1441 shader: boundaryPressureShader 1442 }); 1443 1444 gradientSubtractProgram = new GradientSubtractProgram({ 1445 in_pressure: pressure, 1446 in_velocity: velocity0, 1447 out_velocity: velocity 1448 }); 1449 1450 advectDyeProgram = new AdvectProgram({ 1451 in_quantity: dye0, 1452 in_velocity: velocity, 1453 out_quantity: dye, 1454 uniforms: [grid, dt], 1455 dispatchX: settings.dye_w, 1456 dispatchY: settings.dye_h, 1457 shader: advectDyeShader 1458 }); 1459 1460 clearPressureProgram = new UpdateProgram({ 1461 in_quantity: pressure, 1462 out_quantity: pressure0, 1463 uniforms: [grid, viscosity], 1464 shader: clearPressureShader 1465 }); 1466 1467 vorticityProgram = new VorticityProgram({ 1468 in_velocity: velocity, 1469 out_vorticity: vorticity 1470 }); 1471 1472 vorticityConfinmentProgram = new VorticityConfinmentProgram({ 1473 in_velocity: velocity, 1474 in_vorticity: vorticity, 1475 out_velocity: velocity0, 1476 uniforms: [grid, dt, uVorticity] 1477 }); 1478 1479 renderProgram = new RenderProgram(); 1480} 1481 1482async function main() { 1483 // Init WebGPU Context 1484 const initializationSuccess = await initContext(); 1485 if (!initializationSuccess) return; 1486 1487 // Init buffers, uniforms and programs 1488 initBuffers(); 1489 initUniforms(); 1490 initPrograms(); 1491 1492 // Simulation reset 1493 function reset() { 1494 velocity.clear(device.queue); 1495 dye.clear(device.queue); 1496 pressure.clear(device.queue); 1497 1498 settings.time = 0; 1499 } 1500 settings.reset = reset; 1501 1502 // Fluid simulation step 1503 function dispatchComputePipeline(passEncoder) { 1504 // Add velocity and dye at the mouse position 1505 updateDyeProgram.dispatch(passEncoder); 1506 updateProgram.dispatch(passEncoder); 1507 1508 // Advect the velocity field through itself 1509 advectProgram.dispatch(passEncoder); 1510 boundaryProgram.dispatch(passEncoder); // boundary conditions 1511 1512 // Compute the divergence 1513 divergenceProgram.dispatch(passEncoder); 1514 boundaryDivProgram.dispatch(passEncoder); // boundary conditions 1515 1516 // Solve the jacobi-pressure equation 1517 for (let i = 0; i < settings.pressure_iterations; i++) { 1518 pressureProgram.dispatch(passEncoder); 1519 boundaryPressureProgram.dispatch(passEncoder); // boundary conditions 1520 } 1521 1522 // Subtract the pressure from the velocity field 1523 gradientSubtractProgram.dispatch(passEncoder); 1524 clearPressureProgram.dispatch(passEncoder); 1525 1526 // Compute & apply vorticity confinment 1527 vorticityProgram.dispatch(passEncoder); 1528 vorticityConfinmentProgram.dispatch(passEncoder); 1529 1530 // Advect the dye through the velocity field 1531 advectDyeProgram.dispatch(passEncoder); 1532 } 1533 1534 let lastFrame = performance.now(); 1535 1536 // Render loop 1537 async function step() { 1538 requestAnimationFrame(step); 1539 1540 // Update time 1541 const now = performance.now(); 1542 settings.dt = 1543 Math.min(1 / 60, (now - lastFrame) / 1000) * settings.sim_speed; 1544 settings.time += settings.dt; 1545 lastFrame = now; 1546 1547 // Update uniforms 1548 Object.values(globalUniforms).forEach((u) => u.update(device.queue)); 1549 1550 // Update custom uniform 1551 if (mouseInfos.current) { 1552 let dx = mouseInfos.last ? mouseInfos.current[0] - mouseInfos.last[0] : 0; 1553 let dy = mouseInfos.last ? mouseInfos.current[1] - mouseInfos.last[1] : 0; 1554 1555 const isMobile = "ontouchstart" in window || navigator.maxTouchPoints > 0; 1556 if (isMobile) { 1557 const touchStrengthMultiplier = 0.2; 1558 dx *= touchStrengthMultiplier; 1559 dy *= touchStrengthMultiplier; 1560 } 1561 1562 mouseInfos.velocity = [dx, dy]; 1563 1564 mouse.update(device.queue, [ 1565 ...mouseInfos.current, 1566 ...mouseInfos.velocity 1567 ]); 1568 mouseInfos.last = [...mouseInfos.current]; 1569 } 1570 1571 // Compute fluid 1572 const commandEncoder = device.createCommandEncoder(); 1573 const passEncoder = commandEncoder.beginComputePass(); 1574 dispatchComputePipeline(passEncoder); 1575 passEncoder.end(); 1576 1577 velocity0.copyTo(velocity, commandEncoder); 1578 pressure0.copyTo(pressure, commandEncoder); 1579 dye.copyTo(renderProgram.buffer, commandEncoder); 1580 1581 // Draw fluid 1582 renderProgram.dispatch(commandEncoder); 1583 1584 // Send commands to the GPU 1585 const gpuCommands = commandEncoder.finish(); 1586 device.queue.submit([gpuCommands]); 1587 } 1588 1589 step(); 1590} 1591 1592main();

Love this component?

Explore more components and build amazing UIs.

View All Components