写点“悄悄话”
2025-10-31
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>Fluid motion #webgpu version</title>
<meta
name="viewport"
content="width=device-width,initial-scale=1,minimal-ui,shrink-to-fit=no,viewport-fit=cover"
/>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<meta
name="description"
content="Fluid motion with webGPU, which makes it more performant and future proof"
/>
<link
rel="stylesheet"
href="https://public.codepenassets.com/css/normalize-5.0.0.min.css"
/>
<style>
html,
body {
overflow: clip;
}
:root {
--c-glass: #bbbbbc;
--c-light: #fff;
--c-dark: #000;
--c-content: #224;
--c-action: #0052f5;
--c-bg: #fff;
--glass-reflex-dark: 1;
--glass-reflex-light: 1;
--saturation: 150%;
font-size: 20px;
font-family: sans-serif;
font-optical-sizing: auto;
transition: background 400ms cubic-bezier(1, 0, 0.4, 1),
color 400ms cubic-bezier(1, 0, 0.4, 1);
}
body {
margin: 0;
position: absolute;
width: 100%;
height: 100%;
}
#container {
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
background: var(--c-bg);
color: var(--c-content);
}
.a-title {
position: absolute;
color: transparent;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-image: conic-gradient(#ed0101, blue);
pointer-events: none;
mix-blend-mode: difference;
filter: drop-shadow(2px 4px 6px black);
}
.a-second-title {
position: absolute;
margin-top: 25vh;
pointer-events: none;
-webkit-text-stroke: 1.3px white;
letter-spacing: 1.125px;
font-size: -webkit-xxx-large;
font-weight: 900;
mix-blend-mode: color-burn;
:has(input[value="dark"]:checked) & {
mix-blend-mode: color-dodge;
}
}
canvas {
width: 100%;
height: 100%;
background: var(--c-bg);
}
/*glass switcher*/
:has(input[value="dark"]:checked) {
--c-glass: #bbbbbc;
--c-light: #fff;
--c-dark: #000;
--c-content: #e1e1e1;
--c-action: #ffdc03;
--c-bg: #000;
--glass-reflex-dark: 2;
--glass-reflex-light: 0.3;
--saturation: 150%;
}
.switcher {
position: fixed;
z-index: 2;
top: 40px;
left: 50%;
translate: -50%;
display: flex;
align-items: center;
gap: 8px;
width: 168px; /* Adjusted for two options */
max-width: 168px; /* Adjusted for two options */
height: 70px;
box-sizing: border-box;
padding: 8px 12px 10px;
margin: 0 auto;
border: none;
border-radius: 99em;
font-size: var(--fz);
background-color: color-mix(in srgb, var(--c-glass) 12%, transparent);
backdrop-filter: blur(8px) url(#switcher) saturate(var(--saturation));
-webkit-backdrop-filter: blur(8px) saturate(var(--saturation));
box-shadow: inset 0 0 0 1px
color-mix(
in srgb,
var(--c-light) calc(var(--glass-reflex-light) * 10%),
transparent
),
inset 1.8px 3px 0px -2px color-mix(in srgb, var(--c-light)
calc(var(--glass-reflex-light) * 90%), transparent),
inset -2px -2px 0px -2px color-mix(in srgb, var(--c-light)
calc(var(--glass-reflex-light) * 80%), transparent),
inset -3px -8px 1px -6px color-mix(in srgb, var(--c-light)
calc(var(--glass-reflex-light) * 60%), transparent),
inset -0.3px -1px 4px 0px
color-mix(
in srgb,
var(--c-dark) calc(var(--glass-reflex-dark) * 12%),
transparent
),
inset -1.5px 2.5px 0px -2px
color-mix(
in srgb,
var(--c-dark) calc(var(--glass-reflex-dark) * 20%),
transparent
),
inset 0px 3px 4px -2px color-mix(in srgb, var(--c-dark)
calc(var(--glass-reflex-dark) * 20%), transparent),
inset 2px -6.5px 1px -4px
color-mix(
in srgb,
var(--c-dark) calc(var(--glass-reflex-dark) * 10%),
transparent
),
0px 1px 5px 0px
color-mix(
in srgb,
var(--c-dark) calc(var(--glass-reflex-dark) * 10%),
transparent
),
0px 6px 16px 0px
color-mix(
in srgb,
var(--c-dark) calc(var(--glass-reflex-dark) * 8%),
transparent
);
transition: background-color 400ms cubic-bezier(1, 0, 0.4, 1),
box-shadow 400ms cubic-bezier(1, 0, 0.4, 1);
-webkit-backdrop-filter: blur(5px);
backdrop-filter: blur(5px);
}
.switcher__legend {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
border: 0;
padding: 0;
white-space: nowrap;
-webkit-clip-path: inset(100%);
clip-path: inset(100%);
clip: rect(0 0 0 0);
overflow: hidden;
}
.switcher__input {
clip: rect(0 0 0 0);
-webkit-clip-path: inset(100%);
clip-path: inset(100%);
height: 1px;
width: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
}
.switcher__icon {
display: block;
width: 100%;
transition: scale 200ms cubic-bezier(0.5, 0, 0, 1);
}
.switcher__filter {
position: absolute;
width: 0;
height: 0;
z-index: -1;
}
.switcher__option {
--c: var(--c-content);
display: flex;
justify-content: center;
align-items: center;
padding: 0 16px;
width: 68px;
height: 100%;
box-sizing: border-box;
border-radius: 99em;
opacity: 1;
transition: all 160ms;
}
.switcher__option:hover {
--c: var(--c-action);
cursor: pointer;
}
.switcher__option:hover .switcher__icon {
scale: 1.2;
}
.switcher__option:has(input:checked) {
--c: var(--c-content);
cursor: auto;
}
.switcher__option:has(input:checked) .switcher__icon {
scale: 1;
}
.switcher::after {
content: "";
position: absolute;
left: 4px;
top: 4px;
display: block;
width: 84px;
height: calc(100% - 10px);
border-radius: 99em;
background-color: color-mix(in srgb, var(--c-glass) 36%, transparent);
z-index: -1;
box-shadow: inset 0 0 0 1px
color-mix(
in srgb,
var(--c-light) calc(var(--glass-reflex-light) * 10%),
transparent
),
inset 2px 1px 0px -1px color-mix(in srgb, var(--c-light)
calc(var(--glass-reflex-light) * 90%), transparent),
inset -1.5px -1px 0px -1px color-mix(in srgb, var(--c-light)
calc(var(--glass-reflex-light) * 80%), transparent),
inset -2px -6px 1px -5px color-mix(in srgb, var(--c-light)
calc(var(--glass-reflex-light) * 60%), transparent),
inset -1px 2px 3px -1px
color-mix(
in srgb,
var(--c-dark) calc(var(--glass-reflex-dark) * 20%),
transparent
),
inset 0px -4px 1px -2px
color-mix(
in srgb,
var(--c-dark) calc(var(--glass-reflex-dark) * 10%),
transparent
),
0px 3px 6px 0px
color-mix(
in srgb,
var(--c-dark) calc(var(--glass-reflex-dark) * 8%),
transparent
);
}
.switcher:has(input[c-option="1"]:checked)::after {
translate: 0 0;
transform-origin: right;
transition: background-color 400ms cubic-bezier(1, 0, 0.4, 1),
box-shadow 400ms cubic-bezier(1, 0, 0.4, 1),
translate 400ms cubic-bezier(1, 0, 0.4, 1);
-webkit-animation: scaleToggle 440ms ease;
animation: scaleToggle 440ms ease;
}
.switcher:has(input[c-option="2"]:checked)::after {
translate: 76px 0;
transition: background-color 400ms cubic-bezier(1, 0, 0.4, 1),
box-shadow 400ms cubic-bezier(1, 0, 0.4, 1),
translate 400ms cubic-bezier(1, 0, 0.4, 1);
-webkit-animation: scaleToggle2 440ms ease;
animation: scaleToggle2 440ms ease;
transform-origin: left; /* Set transform-origin for the second option */
}
@-webkit-keyframes scaleToggle {
0% {
scale: 1 1;
}
50% {
scale: 1.1 1;
}
100% {
scale: 1 1;
}
}
@keyframes scaleToggle {
0% {
scale: 1 1;
}
50% {
scale: 1.1 1;
}
100% {
scale: 1 1;
}
}
@-webkit-keyframes scaleToggle2 {
0% {
scale: 1 1;
}
50% {
scale: 1.1 1; /* Adjusted scale for consistency */
}
100% {
scale: 1 1;
}
}
@keyframes scaleToggle2 {
0% {
scale: 1 1;
}
50% {
scale: 1.1 1; /* Adjusted scale for consistency */
}
100% {
scale: 1 1;
}
}
</style>
</head>
<body>
<section id="container">
<h1 class="a-title">在这里涂鸦</h1>
<h2 class="a-second-title">偷偷写点悄悄话在这里</h2>
<canvas id="fluid-webgpu"></canvas>
</section>
<!-- switcher light-dark-->
<fieldset class="switcher">
<legend class="switcher__legend">选择主题</legend>
<label class="switcher__option">
<input
class="switcher__input"
type="radio"
name="theme"
value="light"
c-option="1"
checked
/>
<svg
class="switcher__icon"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 36 36"
>
<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"
/>
<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"
/>
</svg>
</label>
<label class="switcher__option">
<input
class="switcher__input"
type="radio"
name="theme"
value="dark"
c-option="2"
/>
<svg
class="switcher__icon"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 36 36"
>
<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"
/>
</svg>
</label>
<!-- <div class="switcher__toggle"></div> -->
<div class="switcher__filter">
<svg>
<filter id="switcher" primitiveUnits="objectBoundingBox">
<feImage
result="map"
width="100%"
height="100%"
x="0"
y="0"
href="data:image/webp;base64,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"
/>
<feGaussianBlur
in="SourceGraphic"
stdDeviation="0.04"
result="blur"
/>
<feDisplacementMap
id="disp"
in="blur"
in2="map"
scale="0.5"
xChannelSelector="R"
yChannelSelector="G"
></feDisplacementMap>
</filter>
<filter id="toggler" primitiveUnits="objectBoundingBox">
<feImage
result="map"
width="100%"
height="100%"
x="0"
y="0"
href="data:image/webp;base64,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"
/>
<feGaussianBlur
in="SourceGraphic"
stdDeviation="0.01"
result="blur"
/>
<feDisplacementMap
id="disp"
in="blur"
in2="map"
scale="0.5"
xChannelSelector="R"
yChannelSelector="G"
></feDisplacementMap>
</filter>
</svg>
</div>
</fieldset>
<script>
/*main*/
let settings = {
grid_size: 64,
dye_size: 256,
sim_speed: 5,
contain_fluid: true,
velocity_add_intensity: 0.28,
velocity_add_radius: 0.001,
velocity_diffusion: 1,
dye_add_intensity: 0.8,
dye_add_radius: 0.0035,
dye_diffusion: 0.96204,
viscosity: 0,
vorticity: 0,
pressure_iterations: 8,
buffer_view: "dye",
input_symmetry: "none",
};
let device, presentationFormat, canvas, context;
const mouseInfos = {
current: null,
last: null,
velocity: null,
};
// Buffers
let velocity,
velocity0,
dye,
dye0,
divergence,
divergence0,
pressure,
pressure0,
vorticity;
// Uniforms
const globalUniforms = {};
let time,
dt,
mouse,
grid,
uSimSpeed,
vel_force,
vel_radius,
vel_diff,
dye_force,
dye_radius,
dye_diff;
let viscosity, uVorticity, containFluid, uSymmetry, uRenderIntensity;
// Programs
let checkerProgram,
updateDyeProgram,
updateProgram,
advectProgram,
boundaryProgram,
divergenceProgram;
let boundaryDivProgram,
pressureProgram,
boundaryPressureProgram,
gradientSubtractProgram,
advectDyeProgram;
let clearPressureProgram,
vorticityProgram,
vorticityConfinmentProgram,
renderProgram;
function handlePointerMove(e) {
const pointer = e.touches ? e.touches[0] : e;
const rect = canvas.getBoundingClientRect();
if (!mouseInfos.current) mouseInfos.current = [];
mouseInfos.current[0] = (pointer.clientX - rect.left) / rect.width;
mouseInfos.current[1] = 1 - (pointer.clientY - rect.top) / rect.height; // Invert Y
}
function onWebGPUDetectionError(error) {
console.log("Could not initialize WebGPU: " + error);
document.querySelector(".webgpu-not-supported").style.visibility =
"visible";
return false;
}
// Init the WebGPU context by checking first if everything is supported
// Returns true on init success, false otherwise
async function initContext() {
if (navigator.gpu == null)
return onWebGPUDetectionError("WebGPU NOT Supported");
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) return onWebGPUDetectionError("No adapter found");
device = await adapter.requestDevice();
canvas = document.getElementById("fluid-webgpu");
context = canvas.getContext("webgpu");
if (!context)
return onWebGPUDetectionError("Canvas does not support WebGPU");
// If we got here, WebGPU seems to be supported
// Init canvas
canvas.style.width = "100%";
canvas.style.height = "100%";
canvas.addEventListener("mousemove", handlePointerMove);
canvas.addEventListener("touchmove", (e) => {
e.preventDefault();
handlePointerMove(e);
});
canvas.addEventListener("touchstart", (e) => {
e.preventDefault();
handlePointerMove(e);
mouseInfos.last = [...mouseInfos.current];
});
// Init context
presentationFormat = navigator.gpu.getPreferredCanvasFormat(adapter);
context.configure({
device,
format: presentationFormat,
usage: GPUTextureUsage.RENDER_ATTACHMENT,
alphaMode: "premultiplied",
});
// Init buffer sizes
initSizes();
// Resize event
let resizeTimeout;
window.addEventListener("resize", () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(refreshSizes, 150);
});
return true;
}
function refreshSizes() {
initSizes();
initBuffers();
initPrograms();
globalUniforms.gridSize.needsUpdate = [
settings.grid_w,
settings.grid_h,
settings.dye_w,
settings.dye_h,
settings.dx,
settings.rdx,
settings.dyeRdx,
];
}
// Init buffer & canvas dimensions to fit the screen while keeping the aspect ratio
// and downscaling the dimensions if they exceed the browsers capabilities
function initSizes() {
const dpr = window.devicePixelRatio || 1;
const aspectRatio = window.innerWidth / window.innerHeight;
const maxBufferSize = device.limits.maxStorageBufferBindingSize;
const maxCanvasSize = device.limits.maxTextureDimension2D;
// Fit to screen while keeping the aspect ratio
const getPreferredDimensions = (baseSize) => {
let w, h;
const scaledBaseSize = baseSize * dpr;
if (aspectRatio > 1) {
h = scaledBaseSize;
w = Math.floor(h * aspectRatio);
} else {
w = scaledBaseSize;
h = Math.floor(w / aspectRatio);
}
return getValidDimensions(w, h);
};
// Downscale if necessary to prevent crashes
const getValidDimensions = (w, h) => {
let downRatio = 1;
// Prevent buffer size overflow
if (w * h * 4 >= maxBufferSize)
downRatio = Math.sqrt(maxBufferSize / (w * h * 4));
// Prevent canvas size overflow
if (w > maxCanvasSize) downRatio = maxCanvasSize / w;
else if (h > maxCanvasSize) downRatio = maxCanvasSize / h;
return {
w: Math.floor(w * downRatio),
h: Math.floor(h * downRatio),
};
};
// Calculate simulation buffer dimensions
let gridSize = getPreferredDimensions(settings.grid_size);
settings.grid_w = gridSize.w;
settings.grid_h = gridSize.h;
// Calculate dye & canvas buffer dimensions
let dyeSize = getPreferredDimensions(settings.dye_size);
settings.dye_w = dyeSize.w;
settings.dye_h = dyeSize.h;
// Useful values for the simulation
settings.rdx = settings.grid_size * 4;
settings.dyeRdx = settings.dye_size * 4;
settings.dx = 1 / settings.rdx;
// Resize the canvas
canvas.width = settings.dye_w;
canvas.height = settings.dye_h;
}
/*shaders*/
const STRUCT_GRID_SIZE = `
struct GridSize {
w : f32,
h : f32,
dyeW: f32,
dyeH: f32,
dx : f32,
rdx : f32,
dyeRdx : f32
}`;
const STRUCT_MOUSE = `
struct Mouse {
pos: vec2<f32>,
vel: vec2<f32>,
}`;
// This code initialize the pos and index variables and target only interior cells
const COMPUTE_START = `
var pos = vec2<f32>(global_id.xy);
if (pos.x == 0 || pos.y == 0 || pos.x >= uGrid.w - 1 || pos.y >= uGrid.h - 1) {
return;
}
let index = ID(pos.x, pos.y);`;
const COMPUTE_START_DYE = `
var pos = vec2<f32>(global_id.xy);
if (pos.x == 0 || pos.y == 0 || pos.x >= uGrid.dyeW - 1 || pos.y >= uGrid.dyeH - 1) {
return;
}
let index = ID(pos.x, pos.y);`;
// This code initialize the pos and index variables and target all cells
const COMPUTE_START_ALL = `
var pos = vec2<f32>(global_id.xy);
if (pos.x >= uGrid.w || pos.y >= uGrid.h) {
return;
}
let index = ID(pos.x, pos.y);`;
const SPLAT_CODE = `
var m = uMouse.pos;
var v = uMouse.vel*2.;
var splat = createSplat(p, m, v, uRadius);
if (uSymmetry == 1. || uSymmetry == 3.) {splat += createSplat(p, vec2(1. - m.x, m.y), v * vec2(-1., 1.), uRadius);}
if (uSymmetry == 2. || uSymmetry == 3.) {splat += createSplat(p, vec2(m.x, 1. - m.y), v * vec2(1., -1.), uRadius);}
if (uSymmetry == 3. || uSymmetry == 4.) {splat += createSplat(p, vec2(1. - m.x, 1. - m.y), v * vec2(-1., -1.), uRadius);}
`;
/// APPLY FORCE SHADER ///
const updateVelocityShader = /* wgsl */ `
${STRUCT_GRID_SIZE}
struct Mouse {
pos: vec2<f32>,
vel: vec2<f32>,
}
@group(0) @binding(0) var<storage, read> x_in : array<f32>;
@group(0) @binding(1) var<storage, read> y_in : array<f32>;
@group(0) @binding(2) var<storage, read_write> x_out : array<f32>;
@group(0) @binding(3) var<storage, read_write> y_out : array<f32>;
@group(0) @binding(4) var<uniform> uGrid: GridSize;
@group(0) @binding(5) var<uniform> uMouse: Mouse;
@group(0) @binding(6) var<uniform> uForce : f32;
@group(0) @binding(7) var<uniform> uRadius : f32;
@group(0) @binding(8) var<uniform> uDiffusion : f32;
@group(0) @binding(9) var<uniform> uDt : f32;
@group(0) @binding(10) var<uniform> uTime : f32;
@group(0) @binding(11) var<uniform> uSymmetry : f32;
fn ID(x : f32, y : f32) -> u32 { return u32(x + y * uGrid.w); }
fn inBetween(x : f32, lower : f32, upper : f32) -> bool {
return x > lower && x < upper;
}
fn inBounds(pos : vec2<f32>, xMin : f32, xMax : f32, yMin: f32, yMax : f32) -> bool {
return inBetween(pos.x, xMin * uGrid.w, xMax * uGrid.w) && inBetween(pos.y, yMin * uGrid.h, yMax * uGrid.h);
}
fn createSplat(pos : vec2<f32>, splatPos : vec2<f32>, vel : vec2<f32>, radius : f32) -> vec2<f32> {
var p = pos - splatPos;
p.x *= uGrid.w / uGrid.h;
var v = vel;
v.x *= uGrid.w / uGrid.h;
var splat = exp(-dot(p, p) / radius) * v;
return splat;
}
@compute @workgroup_size(8, 8)
fn main(@builtin(global_invocation_id) global_id : vec3<u32>) {
${COMPUTE_START}
let tmpT = uTime;
var p = pos/vec2(uGrid.w, uGrid.h);
${SPLAT_CODE}
splat *= uForce * uDt * 200.;
x_out[index] = x_in[index]*uDiffusion + splat.x;
y_out[index] = y_in[index]*uDiffusion + splat.y;
}`;
const updateDyeShader = /* wgsl */ `
${STRUCT_GRID_SIZE}
struct Mouse {
pos: vec2<f32>,
vel: vec2<f32>,
}
@group(0) @binding(0) var<storage, read> x_in : array<f32>;
@group(0) @binding(1) var<storage, read> y_in : array<f32>;
@group(0) @binding(2) var<storage, read> z_in : array<f32>;
@group(0) @binding(3) var<storage, read_write> x_out : array<f32>;
@group(0) @binding(4) var<storage, read_write> y_out : array<f32>;
@group(0) @binding(5) var<storage, read_write> z_out : array<f32>;
@group(0) @binding(6) var<uniform> uGrid: GridSize;
@group(0) @binding(7) var<uniform> uMouse: Mouse;
@group(0) @binding(8) var<uniform> uForce : f32;
@group(0) @binding(9) var<uniform> uRadius : f32;
@group(0) @binding(10) var<uniform> uDiffusion : f32;
@group(0) @binding(11) var<uniform> uTime : f32;
@group(0) @binding(12) var<uniform> uDt : f32;
@group(0) @binding(13) var<uniform> uSymmetry : f32;
fn ID(x : f32, y : f32) -> u32 { return u32(x + y * uGrid.dyeW); }
fn inBetween(x : f32, lower : f32, upper : f32) -> bool {
return x > lower && x < upper;
}
fn inBounds(pos : vec2<f32>, xMin : f32, xMax : f32, yMin: f32, yMax : f32) -> bool {
return inBetween(pos.x, xMin * uGrid.dyeW, xMax * uGrid.dyeW) && inBetween(pos.y, yMin * uGrid.dyeH, yMax * uGrid.dyeH);
}
// cosine based palette, 4 vec3 params
fn palette(t : f32, a : vec3<f32>, b : vec3<f32>, c : vec3<f32>, d : vec3<f32> ) -> vec3<f32> {
return a + b*cos( 6.28318*(c*t+d) );
}
fn createSplat(pos : vec2<f32>, splatPos : vec2<f32>, vel : vec2<f32>, radius : f32) -> vec3<f32> {
var p = pos - splatPos;
p.x *= uGrid.w / uGrid.h;
var v = vel;
v.x *= uGrid.w / uGrid.h;
var splat = exp(-dot(p, p) / radius) * length(v);
return vec3(splat);
}
@compute @workgroup_size(8, 8)
fn main(@builtin(global_invocation_id) global_id : vec3<u32>) {
${COMPUTE_START_DYE}
let col_incr = 0.15;
let col_start = palette(uTime/8., vec3(1), vec3(0.5), vec3(1), vec3(0, col_incr, col_incr*2.));
var p = pos/vec2(uGrid.dyeW, uGrid.dyeH);
${SPLAT_CODE}
splat *= col_start * uForce * uDt * 100.;
x_out[index] = max(0., x_in[index]*uDiffusion + splat.x);
y_out[index] = max(0., y_in[index]*uDiffusion + splat.y);
z_out[index] = max(0., z_in[index]*uDiffusion + splat.z);
}`;
/// ADVECT SHADER ///
const advectShader = /* wgsl */ `
${STRUCT_GRID_SIZE}
@group(0) @binding(0) var<storage, read> x_in : array<f32>;
@group(0) @binding(1) var<storage, read> y_in : array<f32>;
@group(0) @binding(2) var<storage, read> x_vel : array<f32>;
@group(0) @binding(3) var<storage, read> y_vel : array<f32>;
@group(0) @binding(4) var<storage, read_write> x_out : array<f32>;
@group(0) @binding(5) var<storage, read_write> y_out : array<f32>;
@group(0) @binding(6) var<uniform> uGrid : GridSize;
@group(0) @binding(7) var<uniform> uDt : f32;
fn ID(x : f32, y : f32) -> u32 { return u32(x + y * uGrid.w); }
fn in(x : f32, y : f32) -> vec2<f32> { let id = ID(x, y); return vec2(x_in[id], y_in[id]); }
@compute @workgroup_size(8, 8)
fn main(@builtin(global_invocation_id) global_id : vec3<u32>) {
${COMPUTE_START}
var x = pos.x - uDt * uGrid.rdx * x_vel[index];
var y = pos.y - uDt * uGrid.rdx * y_vel[index];
if (x < 0) { x = 0; }
else if (x >= uGrid.w - 1) { x = uGrid.w - 1; }
if (y < 0) { y = 0; }
else if (y >= uGrid.h - 1) { y = uGrid.h - 1; }
let x1 = floor(x);
let y1 = floor(y);
let x2 = x1 + 1;
let y2 = y1 + 1;
let TL = in(x1, y2);
let TR = in(x2, y2);
let BL = in(x1, y1);
let BR = in(x2, y1);
let xMod = fract(x);
let yMod = fract(y);
let bilerp = mix( mix(BL, BR, xMod), mix(TL, TR, xMod), yMod );
x_out[index] = bilerp.x;
y_out[index] = bilerp.y;
}`;
const advectDyeShader = /* wgsl */ `
${STRUCT_GRID_SIZE}
@group(0) @binding(0) var<storage, read> x_in : array<f32>;
@group(0) @binding(1) var<storage, read> y_in : array<f32>;
@group(0) @binding(2) var<storage, read> z_in : array<f32>;
@group(0) @binding(3) var<storage, read> x_vel : array<f32>;
@group(0) @binding(4) var<storage, read> y_vel : array<f32>;
@group(0) @binding(5) var<storage, read_write> x_out : array<f32>;
@group(0) @binding(6) var<storage, read_write> y_out : array<f32>;
@group(0) @binding(7) var<storage, read_write> z_out : array<f32>;
@group(0) @binding(8) var<uniform> uGrid : GridSize;
@group(0) @binding(9) var<uniform> uDt : f32;
fn ID(x : f32, y : f32) -> u32 { return u32(x + y * uGrid.dyeW); }
fn in(x : f32, y : f32) -> vec3<f32> { let id = ID(x, y); return vec3(x_in[id], y_in[id], z_in[id]); }
fn vel(x : f32, y : f32) -> vec2<f32> {
let id = u32(i32(x) + i32(y) * i32(uGrid.w));
return vec2(x_vel[id], y_vel[id]);
}
fn vel_bilerp(x0 : f32, y0 : f32) -> vec2<f32> {
var x = x0 * uGrid.w / uGrid.dyeW;
var y = y0 * uGrid.h / uGrid.dyeH;
if (x < 0) { x = 0; }
else if (x >= uGrid.w - 1) { x = uGrid.w - 1; }
if (y < 0) { y = 0; }
else if (y >= uGrid.h - 1) { y = uGrid.h - 1; }
let x1 = floor(x);
let y1 = floor(y);
let x2 = x1 + 1;
let y2 = y1 + 1;
let TL = vel(x1, y2);
let TR = vel(x2, y2);
let BL = vel(x1, y1);
let BR = vel(x2, y1);
let xMod = fract(x);
let yMod = fract(y);
return mix( mix(BL, BR, xMod), mix(TL, TR, xMod), yMod );
}
@compute @workgroup_size(8, 8)
fn main(@builtin(global_invocation_id) global_id : vec3<u32>) {
${COMPUTE_START_DYE}
let V = vel_bilerp(pos.x, pos.y);
var x = pos.x - uDt * uGrid.dyeRdx * V.x;
var y = pos.y - uDt * uGrid.dyeRdx * V.y;
if (x < 0) { x = 0; }
else if (x >= uGrid.dyeW - 1) { x = uGrid.dyeW - 1; }
if (y < 0) { y = 0; }
else if (y >= uGrid.dyeH - 1) { y = uGrid.dyeH - 1; }
let x1 = floor(x);
let y1 = floor(y);
let x2 = x1 + 1;
let y2 = y1 + 1;
let TL = in(x1, y2);
let TR = in(x2, y2);
let BL = in(x1, y1);
let BR = in(x2, y1);
let xMod = fract(x);
let yMod = fract(y);
let bilerp = mix( mix(BL, BR, xMod), mix(TL, TR, xMod), yMod );
x_out[index] = bilerp.x;
y_out[index] = bilerp.y;
z_out[index] = bilerp.z;
}`;
/// DIVERGENCE SHADER ///
const divergenceShader = /* wgsl */ `
${STRUCT_GRID_SIZE}
@group(0) @binding(0) var<storage, read> x_vel : array<f32>;
@group(0) @binding(1) var<storage, read> y_vel : array<f32>;
@group(0) @binding(2) var<storage, read_write> div : array<f32>;
@group(0) @binding(3) var<uniform> uGrid : GridSize;
fn ID(x : f32, y : f32) -> u32 { return u32(x + y * uGrid.w); }
fn vel(x : f32, y : f32) -> vec2<f32> { let id = ID(x, y); return vec2(x_vel[id], y_vel[id]); }
@compute @workgroup_size(8, 8)
fn main(@builtin(global_invocation_id) global_id : vec3<u32>) {
${COMPUTE_START}
let L = vel(pos.x - 1, pos.y).x;
let R = vel(pos.x + 1, pos.y).x;
let B = vel(pos.x, pos.y - 1).y;
let T = vel(pos.x, pos.y + 1).y;
div[index] = 0.5 * uGrid.rdx * ((R - L) + (T - B));
}`;
/// PRESSURE SHADER ///
const pressureShader = /* wgsl */ `
${STRUCT_GRID_SIZE}
@group(0) @binding(0) var<storage, read> pres_in : array<f32>;
@group(0) @binding(1) var<storage, read> div : array<f32>;
@group(0) @binding(2) var<storage, read_write> pres_out : array<f32>;
@group(0) @binding(3) var<uniform> uGrid : GridSize;
fn ID(x : f32, y : f32) -> u32 { return u32(x + y * uGrid.w); }
fn in(x : f32, y : f32) -> f32 { let id = ID(x, y); return pres_in[id]; }
@compute @workgroup_size(8, 8)
fn main(@builtin(global_invocation_id) global_id : vec3<u32>) {
${COMPUTE_START}
let L = pos - vec2(1, 0);
let R = pos + vec2(1, 0);
let B = pos - vec2(0, 1);
let T = pos + vec2(0, 1);
let Lx = in(L.x, L.y);
let Rx = in(R.x, R.y);
let Bx = in(B.x, B.y);
let Tx = in(T.x, T.y);
let bC = div[index];
let alpha = -(uGrid.dx * uGrid.dx);
let rBeta = .25;
pres_out[index] = (Lx + Rx + Bx + Tx + alpha * bC) * rBeta;
}`;
/// GRADIENT SUBTRACT SHADER ///
const gradientSubtractShader = /* wgsl */ `
${STRUCT_GRID_SIZE}
@group(0) @binding(0) var<storage, read> pressure : array<f32>;
@group(0) @binding(1) var<storage, read> x_vel : array<f32>;
@group(0) @binding(2) var<storage, read> y_vel : array<f32>;
@group(0) @binding(3) var<storage, read_write> x_out : array<f32>;
@group(0) @binding(4) var<storage, read_write> y_out : array<f32>;
@group(0) @binding(5) var<uniform> uGrid : GridSize;
fn ID(x : f32, y : f32) -> u32 { return u32(x + y * uGrid.w); }
fn pres(x : f32, y : f32) -> f32 { let id = ID(x, y); return pressure[id]; }
@compute @workgroup_size(8, 8)
fn main(@builtin(global_invocation_id) global_id : vec3<u32>) {
${COMPUTE_START}
let L = pos - vec2(1, 0);
let R = pos + vec2(1, 0);
let B = pos - vec2(0, 1);
let T = pos + vec2(0, 1);
let xL = pres(L.x, L.y);
let xR = pres(R.x, R.y);
let yB = pres(B.x, B.y);
let yT = pres(T.x, T.y);
let finalX = x_vel[index] - .5 * uGrid.rdx * (xR - xL);
let finalY = y_vel[index] - .5 * uGrid.rdx * (yT - yB);
x_out[index] = finalX;
y_out[index] = finalY;
}`;
/// VORTICITY SHADER ///
const vorticityShader = /* wgsl */ `
${STRUCT_GRID_SIZE}
@group(0) @binding(0) var<storage, read> x_vel : array<f32>;
@group(0) @binding(1) var<storage, read> y_vel : array<f32>;
@group(0) @binding(2) var<storage, read_write> vorticity : array<f32>;
@group(0) @binding(3) var<uniform> uGrid : GridSize;
fn ID(x : f32, y : f32) -> u32 { return u32(x + y * uGrid.w); }
fn vel(x : f32, y : f32) -> vec2<f32> { let id = ID(x, y); return vec2(x_vel[id], y_vel[id]); }
@compute @workgroup_size(8, 8)
fn main(@builtin(global_invocation_id) global_id : vec3<u32>) {
${COMPUTE_START}
let Ly = vel(pos.x - 1, pos.y).y;
let Ry = vel(pos.x + 1, pos.y).y;
let Bx = vel(pos.x, pos.y - 1).x;
let Tx = vel(pos.x, pos.y + 1).x;
vorticity[index] = 0.5 * uGrid.rdx * ((Ry - Ly) - (Tx - Bx));
}`;
/// VORTICITY CONFINMENT SHADER ///
const vorticityConfinmentShader = /* wgsl */ `
${STRUCT_GRID_SIZE}
@group(0) @binding(0) var<storage, read> x_vel_in : array<f32>;
@group(0) @binding(1) var<storage, read> y_vel_in : array<f32>;
@group(0) @binding(2) var<storage, read> vorticity : array<f32>;
@group(0) @binding(3) var<storage, read_write> x_vel_out : array<f32>;
@group(0) @binding(4) var<storage, read_write> y_vel_out : array<f32>;
@group(0) @binding(5) var<uniform> uGrid : GridSize;
@group(0) @binding(6) var<uniform> uDt : f32;
@group(0) @binding(7) var<uniform> uVorticity : f32;
fn ID(x : f32, y : f32) -> u32 { return u32(x + y * uGrid.w); }
fn vort(x : f32, y : f32) -> f32 { let id = ID(x, y); return vorticity[id]; }
@compute @workgroup_size(8, 8)
fn main(@builtin(global_invocation_id) global_id : vec3<u32>) {
${COMPUTE_START}
let L = vort(pos.x - 1, pos.y);
let R = vort(pos.x + 1, pos.y);
let B = vort(pos.x, pos.y - 1);
let T = vort(pos.x, pos.y + 1);
let C = vorticity[index];
var force = 0.5 * uGrid.rdx * vec2(abs(T) - abs(B), abs(R) - abs(L));
let epsilon = 2.4414e-4;
let magSqr = max(epsilon, dot(force, force));
force = force / sqrt(magSqr);
force *= uGrid.dx * uVorticity * uDt * C * vec2(1, -1);
x_vel_out[index] = x_vel_in[index] + force.x;
y_vel_out[index] = y_vel_in[index] + force.y;
}`;
/// CLEAR PRESSURE SHADER ///
const clearPressureShader = /* wgsl */ `
${STRUCT_GRID_SIZE}
@group(0) @binding(0) var<storage, read> x_in : array<f32>;
@group(0) @binding(1) var<storage, read_write> x_out : array<f32>;
@group(0) @binding(2) var<uniform> uGrid : GridSize;
@group(0) @binding(3) var<uniform> uVisc : f32;
fn ID(x : f32, y : f32) -> u32 { return u32(x + y * uGrid.w); }
@compute @workgroup_size(8, 8)
fn main(@builtin(global_invocation_id) global_id : vec3<u32>) {
${COMPUTE_START_ALL}
x_out[index] = x_in[index]*uVisc;
}`;
/// BOUNDARY SHADER ///
const boundaryShader = /* wgsl */ `
${STRUCT_GRID_SIZE}
@group(0) @binding(0) var<storage, read> x_in : array<f32>;
@group(0) @binding(1) var<storage, read> y_in : array<f32>;
@group(0) @binding(2) var<storage, read_write> x_out : array<f32>;
@group(0) @binding(3) var<storage, read_write> y_out : array<f32>;
@group(0) @binding(4) var<uniform> uGrid : GridSize;
@group(0) @binding(5) var<uniform> containFluid : f32;
fn ID(x : f32, y : f32) -> u32 { return u32(x + y * uGrid.w); }
@compute @workgroup_size(8, 8)
fn main(@builtin(global_invocation_id) global_id : vec3<u32>) {
${COMPUTE_START_ALL}
// disable scale to disable contained bounds
var scaleX = 1.;
var scaleY = 1.;
if (pos.x == 0) { pos.x += 1; scaleX = -1.; }
else if (pos.x == uGrid.w - 1) { pos.x -= 1; scaleX = -1.; }
if (pos.y == 0) { pos.y += 1; scaleY = -1.; }
else if (pos.y == uGrid.h - 1) { pos.y -= 1; scaleY = -1.; }
if (containFluid == 0.) {
scaleX = 1.;
scaleY = 1.;
}
x_out[index] = x_in[ID(pos.x, pos.y)] * scaleX;
y_out[index] = y_in[ID(pos.x, pos.y)] * scaleY;
}`;
/// BOUNDARY PRESSURE SHADER ///
const boundaryPressureShader = /* wgsl */ `
${STRUCT_GRID_SIZE}
@group(0) @binding(0) var<storage, read> x_in : array<f32>;
@group(0) @binding(1) var<storage, read_write> x_out : array<f32>;
@group(0) @binding(2) var<uniform> uGrid : GridSize;
fn ID(x : f32, y : f32) -> u32 { return u32(x + y * uGrid.w); }
@compute @workgroup_size(8, 8)
fn main(@builtin(global_invocation_id) global_id : vec3<u32>) {
${COMPUTE_START_ALL}
if (pos.x == 0) { pos.x += 1; }
else if (pos.x == uGrid.w - 1) { pos.x -= 1; }
if (pos.y == 0) { pos.y += 1; }
else if (pos.y == uGrid.h - 1) { pos.y -= 1; }
x_out[index] = x_in[ID(pos.x, pos.y)];
}`;
const checkerboardShader = /* wgsl */ `
${STRUCT_GRID_SIZE}
@group(0) @binding(0) var<storage, read_write> x_out : array<f32>;
@group(0) @binding(1) var<storage, read_write> y_out : array<f32>;
@group(0) @binding(2) var<storage, read_write> z_out : array<f32>;
@group(0) @binding(3) var<uniform> uGrid : GridSize;
@group(0) @binding(4) var<uniform> uTime : f32;
fn ID(x : f32, y : f32) -> u32 { return u32(x + y * uGrid.dyeW); }
fn noise(p_ : vec3<f32>) -> f32 {
var p = p_;
var ip=floor(p);
p-=ip;
var s=vec3(7.,157.,113.);
var h=vec4(0.,s.y, s.z,s.y+s.z)+dot(ip,s);
p=p*p*(3. - 2.*p);
h=mix(fract(sin(h)*43758.5),fract(sin(h+s.x)*43758.5),p.x);
var r=mix(h.xz,h.yw,p.y);
h.x = r.x;
h.y = r.y;
return mix(h.x,h.y,p.z);
}
fn fbm(p_ : vec3<f32>, octaveNum : i32) -> vec2<f32> {
var p=p_;
var acc = vec2(0.);
var freq = 1.0;
var amp = 0.5;
var shift = vec3(100.);
for (var i = 0; i < octaveNum; i++) {
acc += vec2(noise(p), noise(p + vec3(0.,0.,10.))) * amp;
p = p * 2.0 + shift;
amp *= 0.5;
}
return acc;
}
@compute @workgroup_size(8, 8)
fn main(@builtin(global_invocation_id) global_id : vec3<u32>) {
${COMPUTE_START_DYE}
var uv = pos/vec2(uGrid.dyeW, uGrid.dyeH);
var zoom = 4.;
var smallNoise = fbm(vec3(uv.x*zoom*2., uv.y*zoom*2., uTime+2.145), 7) - .5;
var bigNoise = fbm(vec3(uv.x*zoom, uv.y*zoom, uTime*.1+30.), 7) - .5;
var noise = max(length(bigNoise) * 0.035, 0.);
var noise2 = max(length(smallNoise) * 0.035, 0.);
noise = noise + noise2 * .05;
var czoom = 4.;
var n = fbm(vec3(uv.x*czoom, uv.y*czoom, uTime*.1+63.1), 7)*.75+.25;
var n2 = fbm(vec3(uv.x*czoom, uv.y*czoom, uTime*.1+23.4), 7)*.75+.25;
var col = vec3(1.);
x_out[index] += noise * col.x;
y_out[index] += noise * col.y;
z_out[index] += noise * col.z;
}`;
/*render*/
const renderShader = /* wgsl */ `
${STRUCT_GRID_SIZE}
struct VertexOut {
@builtin(position) position : vec4<f32>,
@location(1) uv : vec2<f32>,
};
@group(0) @binding(0) var<storage, read> fieldX : array<f32>;
@group(0) @binding(1) var<storage, read> fieldY : array<f32>;
@group(0) @binding(2) var<storage, read> fieldZ : array<f32>;
@group(0) @binding(3) var<uniform> uGrid : GridSize;
@group(0) @binding(4) var<uniform> multiplier : f32;
@vertex
fn vertex_main(@location(0) position: vec4<f32>) -> VertexOut
{
var output : VertexOut;
output.position = position;
output.uv = position.xy*.5+.5;
return output;
}
@fragment
fn fragment_main(fragData : VertexOut) -> @location(0) vec4<f32>
{
var w = uGrid.dyeW;
var h = uGrid.dyeH;
let fuv = vec2<f32>((floor(fragData.uv*vec2(w, h))));
let id = u32(fuv.x + fuv.y * w);
let r = fieldX[id];
let g = fieldY[id];
let b = fieldZ[id];
let col = vec3(r, g, b);
let alpha = clamp(length(col), 0.0, 1.0);
return vec4(col * multiplier, alpha);
}
`;
// Renders 3 (r, g, b) storage buffers to the canvas
class RenderProgram {
constructor() {
const vertices = new Float32Array([
-1, -1, 0, 1, -1, 1, 0, 1, 1, -1, 0, 1, 1, -1, 0, 1, -1, 1, 0, 1, 1,
1, 0, 1,
]);
this.vertexBuffer = device.createBuffer({
size: vertices.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
mappedAtCreation: true,
});
new Float32Array(this.vertexBuffer.getMappedRange()).set(vertices);
this.vertexBuffer.unmap();
const vertexBuffersDescriptors = [
{
attributes: [
{
shaderLocation: 0,
offset: 0,
format: "float32x4",
},
],
arrayStride: 16,
stepMode: "vertex",
},
];
const shaderModule = device.createShaderModule({
code: renderShader,
});
this.renderPipeline = device.createRenderPipeline({
layout: "auto",
vertex: {
module: shaderModule,
entryPoint: "vertex_main",
buffers: vertexBuffersDescriptors,
},
fragment: {
module: shaderModule,
entryPoint: "fragment_main",
targets: [
{
format: presentationFormat,
},
],
},
primitive: {
topology: "triangle-list",
},
});
// The r,g,b buffer containing the data to render
this.buffer = new DynamicBuffer({
dims: 3,
w: settings.dye_w,
h: settings.dye_h,
});
// Uniforms
const entries = [
...this.buffer.buffers,
globalUniforms.gridSize.buffer,
globalUniforms.render_intensity_multiplier.buffer,
].map((b, i) => ({
binding: i,
resource: { buffer: b },
}));
this.renderBindGroup = device.createBindGroup({
layout: this.renderPipeline.getBindGroupLayout(0),
entries,
});
this.renderPassDescriptor = {
colorAttachments: [
{
clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 0.0 },
loadOp: "clear",
storeOp: "store",
},
],
};
}
// Dispatch a draw command to render on the canvas
dispatch(commandEncoder) {
this.renderPassDescriptor.colorAttachments[0].view = context
.getCurrentTexture()
.createView();
const renderPassEncoder = commandEncoder.beginRenderPass(
this.renderPassDescriptor
);
renderPassEncoder.setPipeline(this.renderPipeline);
renderPassEncoder.setBindGroup(0, this.renderBindGroup);
renderPassEncoder.setVertexBuffer(0, this.vertexBuffer);
renderPassEncoder.draw(6);
renderPassEncoder.end();
}
}
/*utils*/
// Creates and manage multi-dimensional buffers by creating a buffer for each dimension
class DynamicBuffer {
constructor({
dims = 1, // Number of dimensions
w = settings.grid_w, // Buffer width
h = settings.grid_h, // Buffer height
} = {}) {
this.dims = dims;
this.bufferSize = w * h * 4;
this.w = w;
this.h = h;
this.buffers = new Array(dims).fill().map((_) =>
device.createBuffer({
size: this.bufferSize,
usage:
GPUBufferUsage.STORAGE |
GPUBufferUsage.COPY_SRC |
GPUBufferUsage.COPY_DST,
})
);
}
// Copy each buffer to another DynamicBuffer's buffers.
// If the dimensions don't match, the last non-empty dimension will be copied instead
copyTo(buffer, commandEncoder) {
for (let i = 0; i < Math.max(this.dims, buffer.dims); i++) {
commandEncoder.copyBufferToBuffer(
this.buffers[Math.min(i, this.buffers.length - 1)],
0,
buffer.buffers[Math.min(i, buffer.buffers.length - 1)],
0,
this.bufferSize
);
}
}
// Reset all the buffers
clear(queue) {
for (let i = 0; i < this.dims; i++) {
queue.writeBuffer(
this.buffers[i],
0,
new Float32Array(this.w * this.h)
);
}
}
}
// Manage uniform buffers relative to the compute shaders
class Uniform {
constructor(name, { size, value } = {}) {
this.name = name;
this.size =
size ?? (value && typeof value === "object" ? value.length : 1);
this.needsUpdate = false;
if (this.size === 1) {
if (settings[name] == null) {
settings[name] = value ?? 0;
this.alwaysUpdate = true;
}
}
if (this.size === 1 || value != null) {
this.buffer = device.createBuffer({
mappedAtCreation: true,
size: this.size * 4,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const arrayBuffer = this.buffer.getMappedRange();
const sourceValue = value ?? [settings[this.name]];
const sourceArray =
typeof sourceValue === "number"
? [sourceValue]
: Array.isArray(sourceValue)
? sourceValue
: [0]; // Default to [0] if value is invalid
new Float32Array(arrayBuffer).set(new Float32Array(sourceArray));
this.buffer.unmap();
} else {
this.buffer = device.createBuffer({
size: this.size * 4,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
}
globalUniforms[name] = this;
}
setValue(value) {
settings[this.name] = value;
this.needsUpdate = true;
}
update(queue, value) {
if (this.needsUpdate || this.alwaysUpdate || value != null) {
if (typeof this.needsUpdate !== "boolean") value = this.needsUpdate;
queue.writeBuffer(
this.buffer,
0,
new Float32Array(value ?? [parseFloat(settings[this.name])]),
0,
this.size
);
this.needsUpdate = false;
}
}
}
// Creates a shader module, compute pipeline & bind group to use with the GPU
class Program {
constructor({
buffers = [], // Storage buffers
uniforms = [], // Uniform buffers
shader, // WGSL Compute Shader as a string
dispatchX = settings.grid_w, // Dispatch workers width
dispatchY = settings.grid_h, // Dispatch workers height
}) {
this.computePipeline = device.createComputePipeline({
layout: "auto",
compute: {
module: device.createShaderModule({ code: shader }),
entryPoint: "main",
},
});
const storageEntries = buffers.map((b) => b.buffers).flat();
const uniformEntries = uniforms
.filter((u) => u && u.buffer)
.map((u) => u.buffer);
const allEntries = [...storageEntries, ...uniformEntries].map(
(buffer, i) => ({
binding: i,
resource: { buffer },
})
);
this.bindGroup = device.createBindGroup({
layout: this.computePipeline.getBindGroupLayout(0),
entries: allEntries,
});
this.dispatchX = dispatchX;
this.dispatchY = dispatchY;
}
dispatch(passEncoder) {
passEncoder.setPipeline(this.computePipeline);
passEncoder.setBindGroup(0, this.bindGroup);
passEncoder.dispatchWorkgroups(
Math.ceil(this.dispatchX / 8),
Math.ceil(this.dispatchY / 8)
);
}
}
/// Useful classes for cleaner understanding of the input and output buffers
/// used in the declarations of programs & fluid simulation steps
class AdvectProgram extends Program {
constructor({
in_quantity,
in_velocity,
out_quantity,
uniforms,
shader = advectShader,
...props
}) {
uniforms ??= [globalUniforms.gridSize];
super({
buffers: [in_quantity, in_velocity, out_quantity],
uniforms,
shader,
...props,
});
}
}
class DivergenceProgram extends Program {
constructor({
in_velocity,
out_divergence,
uniforms,
shader = divergenceShader,
}) {
uniforms ??= [globalUniforms.gridSize];
super({ buffers: [in_velocity, out_divergence], uniforms, shader });
}
}
class PressureProgram extends Program {
constructor({
in_pressure,
in_divergence,
out_pressure,
uniforms,
shader = pressureShader,
}) {
uniforms ??= [globalUniforms.gridSize];
super({
buffers: [in_pressure, in_divergence, out_pressure],
uniforms,
shader,
});
}
}
class GradientSubtractProgram extends Program {
constructor({
in_pressure,
in_velocity,
out_velocity,
uniforms,
shader = gradientSubtractShader,
}) {
uniforms ??= [globalUniforms.gridSize];
super({
buffers: [in_pressure, in_velocity, out_velocity],
uniforms,
shader,
});
}
}
class BoundaryProgram extends Program {
constructor({
in_quantity,
out_quantity,
uniforms,
shader = boundaryShader,
}) {
uniforms ??= [globalUniforms.gridSize];
super({ buffers: [in_quantity, out_quantity], uniforms, shader });
}
}
class UpdateProgram extends Program {
constructor({
in_quantity,
out_quantity,
uniforms,
shader = updateVelocityShader,
...props
}) {
uniforms ??= [globalUniforms.gridSize];
super({
buffers: [in_quantity, out_quantity],
uniforms,
shader,
...props,
});
}
}
class VorticityProgram extends Program {
constructor({
in_velocity,
out_vorticity,
uniforms,
shader = vorticityShader,
...props
}) {
uniforms ??= [globalUniforms.gridSize];
super({
buffers: [in_velocity, out_vorticity],
uniforms,
shader,
...props,
});
}
}
class VorticityConfinmentProgram extends Program {
constructor({
in_velocity,
in_vorticity,
out_velocity,
uniforms,
shader = vorticityConfinmentShader,
...props
}) {
uniforms ??= [globalUniforms.gridSize];
super({
buffers: [in_velocity, in_vorticity, out_velocity],
uniforms,
shader,
...props,
});
}
}
function initBuffers() {
velocity = new DynamicBuffer({ dims: 2 });
velocity0 = new DynamicBuffer({ dims: 2 });
dye = new DynamicBuffer({
dims: 3,
w: settings.dye_w,
h: settings.dye_h,
});
dye0 = new DynamicBuffer({
dims: 3,
w: settings.dye_w,
h: settings.dye_h,
});
divergence = new DynamicBuffer();
divergence0 = new DynamicBuffer();
pressure = new DynamicBuffer();
pressure0 = new DynamicBuffer();
vorticity = new DynamicBuffer();
}
function initUniforms() {
time = new Uniform("time");
dt = new Uniform("dt");
mouse = new Uniform("mouseInfos", { size: 4 });
grid = new Uniform("gridSize", {
size: 7,
value: [
settings.grid_w,
settings.grid_h,
settings.dye_w,
settings.dye_h,
settings.dx,
settings.rdx,
settings.dyeRdx,
],
});
uSimSpeed = new Uniform("sim_speed", { value: settings.sim_speed });
vel_force = new Uniform("velocity_add_intensity", {
value: settings.velocity_add_intensity,
});
vel_radius = new Uniform("velocity_add_radius", {
value: settings.velocity_add_radius,
});
vel_diff = new Uniform("velocity_diffusion", {
value: settings.velocity_diffusion,
});
dye_force = new Uniform("dye_add_intensity", {
value: settings.dye_add_intensity,
});
dye_radius = new Uniform("dye_add_radius", {
value: settings.dye_add_radius,
});
dye_diff = new Uniform("dye_diffusion", {
value: settings.dye_diffusion,
});
viscosity = new Uniform("viscosity", {
value: settings.viscosity,
});
uVorticity = new Uniform("vorticity", {
value: settings.vorticity,
});
containFluid = new Uniform("contain_fluid", {
value: settings.contain_fluid,
});
uSymmetry = new Uniform("mouse_type", { value: 0 });
uRenderIntensity = new Uniform("render_intensity_multiplier", {
value: 1,
});
}
function initPrograms() {
checkerProgram = new Program({
buffers: [dye],
shader: checkerboardShader,
dispatchX: settings.dye_w,
dispatchY: settings.dye_h,
uniforms: [grid, time],
});
updateDyeProgram = new UpdateProgram({
in_quantity: dye,
out_quantity: dye0,
uniforms: [
grid,
mouse,
dye_force,
dye_radius,
dye_diff,
time,
dt,
uSymmetry,
],
dispatchX: settings.dye_w,
dispatchY: settings.dye_h,
shader: updateDyeShader,
});
updateProgram = new UpdateProgram({
in_quantity: velocity,
out_quantity: velocity0,
uniforms: [
grid,
mouse,
vel_force,
vel_radius,
vel_diff,
dt,
time,
uSymmetry,
],
});
advectProgram = new AdvectProgram({
in_quantity: velocity0,
in_velocity: velocity0,
out_quantity: velocity,
uniforms: [grid, dt],
});
boundaryProgram = new BoundaryProgram({
in_quantity: velocity,
out_quantity: velocity0,
uniforms: [grid, containFluid],
});
divergenceProgram = new DivergenceProgram({
in_velocity: velocity0,
out_divergence: divergence0,
});
boundaryDivProgram = new BoundaryProgram({
in_quantity: divergence0,
out_quantity: divergence,
shader: boundaryPressureShader,
});
pressureProgram = new PressureProgram({
in_pressure: pressure,
in_divergence: divergence,
out_pressure: pressure0,
});
boundaryPressureProgram = new BoundaryProgram({
in_quantity: pressure0,
out_quantity: pressure,
shader: boundaryPressureShader,
});
gradientSubtractProgram = new GradientSubtractProgram({
in_pressure: pressure,
in_velocity: velocity0,
out_velocity: velocity,
});
advectDyeProgram = new AdvectProgram({
in_quantity: dye0,
in_velocity: velocity,
out_quantity: dye,
uniforms: [grid, dt],
dispatchX: settings.dye_w,
dispatchY: settings.dye_h,
shader: advectDyeShader,
});
clearPressureProgram = new UpdateProgram({
in_quantity: pressure,
out_quantity: pressure0,
uniforms: [grid, viscosity],
shader: clearPressureShader,
});
vorticityProgram = new VorticityProgram({
in_velocity: velocity,
out_vorticity: vorticity,
});
vorticityConfinmentProgram = new VorticityConfinmentProgram({
in_velocity: velocity,
in_vorticity: vorticity,
out_velocity: velocity0,
uniforms: [grid, dt, uVorticity],
});
renderProgram = new RenderProgram();
}
async function main() {
// Init WebGPU Context
const initializationSuccess = await initContext();
if (!initializationSuccess) return;
// Init buffers, uniforms and programs
initBuffers();
initUniforms();
initPrograms();
// Simulation reset
function reset() {
velocity.clear(device.queue);
dye.clear(device.queue);
pressure.clear(device.queue);
settings.time = 0;
}
settings.reset = reset;
// Fluid simulation step
function dispatchComputePipeline(passEncoder) {
// Add velocity and dye at the mouse position
updateDyeProgram.dispatch(passEncoder);
updateProgram.dispatch(passEncoder);
// Advect the velocity field through itself
advectProgram.dispatch(passEncoder);
boundaryProgram.dispatch(passEncoder); // boundary conditions
// Compute the divergence
divergenceProgram.dispatch(passEncoder);
boundaryDivProgram.dispatch(passEncoder); // boundary conditions
// Solve the jacobi-pressure equation
for (let i = 0; i < settings.pressure_iterations; i++) {
pressureProgram.dispatch(passEncoder);
boundaryPressureProgram.dispatch(passEncoder); // boundary conditions
}
// Subtract the pressure from the velocity field
gradientSubtractProgram.dispatch(passEncoder);
clearPressureProgram.dispatch(passEncoder);
// Compute & apply vorticity confinment
vorticityProgram.dispatch(passEncoder);
vorticityConfinmentProgram.dispatch(passEncoder);
// Advect the dye through the velocity field
advectDyeProgram.dispatch(passEncoder);
}
let lastFrame = performance.now();
// Render loop
async function step() {
requestAnimationFrame(step);
// Update time
const now = performance.now();
settings.dt =
Math.min(1 / 60, (now - lastFrame) / 1000) * settings.sim_speed;
settings.time += settings.dt;
lastFrame = now;
// Update uniforms
Object.values(globalUniforms).forEach((u) => u.update(device.queue));
// Update custom uniform
if (mouseInfos.current) {
let dx = mouseInfos.last
? mouseInfos.current[0] - mouseInfos.last[0]
: 0;
let dy = mouseInfos.last
? mouseInfos.current[1] - mouseInfos.last[1]
: 0;
const isMobile =
"ontouchstart" in window || navigator.maxTouchPoints > 0;
if (isMobile) {
const touchStrengthMultiplier = 0.2;
dx *= touchStrengthMultiplier;
dy *= touchStrengthMultiplier;
}
mouseInfos.velocity = [dx, dy];
mouse.update(device.queue, [
...mouseInfos.current,
...mouseInfos.velocity,
]);
mouseInfos.last = [...mouseInfos.current];
}
// Compute fluid
const commandEncoder = device.createCommandEncoder();
const passEncoder = commandEncoder.beginComputePass();
dispatchComputePipeline(passEncoder);
passEncoder.end();
velocity0.copyTo(velocity, commandEncoder);
pressure0.copyTo(pressure, commandEncoder);
dye.copyTo(renderProgram.buffer, commandEncoder);
// Draw fluid
renderProgram.dispatch(commandEncoder);
// Send commands to the GPU
const gpuCommands = commandEncoder.finish();
device.queue.submit([gpuCommands]);
}
step();
}
main();
</script>
</body>
</html>
