Skip to content

Commit aedb6eb

Browse files
committed
init
0 parents  commit aedb6eb

16 files changed

+1618
-0
lines changed

.gitignore

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
*.local
14+
15+
# Editor directories and files
16+
.vscode/*
17+
!.vscode/extensions.json
18+
.idea
19+
.DS_Store
20+
*.suo
21+
*.ntvs*
22+
*.njsproj
23+
*.sln
24+
*.sw?

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Stable Fluids in WebGPU
2+
3+
This is the accompanying code for my Blog Post Stable Fluids in WebGPU.
4+
5+
At time of writing, you will need to use Chrome Canary, and enable WebGPU in flags.
6+
7+
## Dependencies
8+
9+
- **MiniGPU:** Takes some pain out of getting WebGPU programs up and running.
10+
- [**glMatrix:**](https://glmatrix.net/) For performing vector and matrix operations on Float32Arrays
11+
- [**twgl.js:**](https://twgljs.org/) A WebGL Library, just used as a convinience to create geometry vertices
12+
13+
### MiniGPU
14+
15+
MiniGPU is my own little library which handles some of the WebGPU boilerplate. In this demo it's mainly used to set up Buffers and BindGroups, and to generate the commands needed to run the shader code.
16+
17+
If you are new to WebGPU, I would recommend you do some background reading to understand what's being obfuscated.
18+
19+
## Getting Started
20+
21+
First, clone the repo and install dependencies
22+
23+
`$ npm install`
24+
25+
or
26+
27+
`$ yarn install`
28+
29+
Then start the development server (using Vite)
30+
31+
`$ npm run dev`
32+
33+
or
34+
35+
`$ yarn run dev`

index.html

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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>MiniGPU Stable Fluids</title>
7+
</head>
8+
<body>
9+
<canvas></canvas>
10+
<script type="module" src="./main.js"></script>
11+
</body>
12+
<style>
13+
canvas {
14+
position: fixed;
15+
top: 0;
16+
left: 0;
17+
width: 100%;
18+
height: 100%;
19+
}
20+
</style>
21+
</html>

main.js

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
import {
2+
Helpers,
3+
Clock,
4+
Computer,
5+
Renderer,
6+
ComputeProgram,
7+
RenderProgram,
8+
Geometry,
9+
UniformsInput,
10+
PingPongBufferInput,
11+
StructuredFloat32Array,
12+
} from "mini-gpu";
13+
import { primitives } from "twgl.js";
14+
import { vec2 } from "gl-matrix";
15+
16+
// With Vite, if we add ?raw to the path, we get it as the plain text shader
17+
import shaderHeader from "./shaders/header.wgsl?raw";
18+
import shaderCommon from "./shaders/common.wgsl?raw";
19+
import boundaryShader from "./shaders/boundary.wgsl?raw";
20+
import externalForceShader from "./shaders/external-force.wgsl?raw";
21+
import advectionShader from "./shaders/advection.wgsl?raw";
22+
import viscousityShader from "./shaders/viscousity.wgsl?raw";
23+
import divergenceShader from "./shaders/divergence.wgsl?raw";
24+
import pressureShader from "./shaders/pressure.wgsl?raw";
25+
import pressureGradientShader from "./shaders/pressure-gradient.wgsl?raw";
26+
import renderShader from "./shaders/render.wgsl?raw";
27+
28+
const WORKGROUP_SIZE = 256; // Must match the workgroup size of our compute shaders
29+
const RESOLUTION = 0.25; // How big the simulation grid will be, with respect to the pixel dimentions of the renderer
30+
const VISCOSITY = 2; // How 'sticky' our fluid will be (higher = more sticky)
31+
32+
const canvas = document.querySelector("canvas");
33+
const clock = new Clock();
34+
35+
let computer, renderer;
36+
const resolution = vec2.create(); // The pixel dimentions of the renderer
37+
const simulationResolution = vec2.create(); // the grid dimentions of the flow field
38+
39+
let isMouseDown = false;
40+
const mousePosition = vec2.create();
41+
const mouseDelta = vec2.create(); // On each frame we'll calculate where the mouse is and how much it's moved, to add external force to the fluid
42+
43+
let uniforms, simulationInput;
44+
let boundaryProgram,
45+
advectionProgram,
46+
externalForceProgram,
47+
viscousityProgram,
48+
divergenceProgram,
49+
pressureProgram,
50+
pressureGradientProgram;
51+
let renderProgram;
52+
53+
const animate = () => {
54+
const { delta } = clock.tick();
55+
56+
// Apply damping to the mouseDelta so it converges to zero when the mouse stops moving
57+
vec2.scale(mouseDelta, mouseDelta, 1 - 0.01 * delta);
58+
59+
// MiniGPU allows us to access and update our uniforms by name
60+
uniforms.member.delta_time = Math.max(Math.min(delta, 33.33), 8) / 1000; // Clamp to keep in a sensible range.
61+
uniforms.member.mouse_position = mousePosition;
62+
uniforms.member.mouse_delta = mouseDelta;
63+
64+
// Run a compute program with MiniGPU
65+
computer.run(boundaryProgram);
66+
simulationInput.step(); // After every computation, swap the ping-pong buffers, so the output buffer becomes the input buffer for the next
67+
68+
computer.run(advectionProgram);
69+
simulationInput.step();
70+
71+
computer.run(externalForceProgram);
72+
simulationInput.step();
73+
74+
// No need to run this if viscousity is zero
75+
if (VISCOSITY > 0) {
76+
// Run for multiple relaxation steps
77+
for (let i = 0; i < 24; i++) {
78+
computer.run(viscousityProgram);
79+
simulationInput.step();
80+
}
81+
}
82+
83+
computer.run(divergenceProgram);
84+
simulationInput.step();
85+
86+
// Run for multiple relaxation steps
87+
for (let i = 0; i < 24; i++) {
88+
computer.run(pressureProgram);
89+
simulationInput.step();
90+
}
91+
92+
computer.run(pressureGradientProgram);
93+
simulationInput.step();
94+
95+
// Render a render program with MiniGPU
96+
renderer.render(renderProgram);
97+
98+
// Loop
99+
requestAnimationFrame(animate);
100+
};
101+
102+
const onMouseMove = (e) => {
103+
if (!isMouseDown) return;
104+
const { clientX, clientY, movementX, movementY } = e;
105+
106+
// Add the mouse movement to the delta in both directions
107+
mouseDelta[0] += movementX;
108+
mouseDelta[1] += movementY;
109+
110+
// We want the mouse position to match the same dimentions as the renderer resolution
111+
vec2.set(
112+
mousePosition,
113+
clientX * renderer.pixelRatio,
114+
clientY * renderer.pixelRatio
115+
);
116+
};
117+
118+
const onMouseDown = () => (isMouseDown = true);
119+
const onMouseUp = () => (isMouseDown = false);
120+
121+
const init = async () => {
122+
// MiniGPU helper access the GPU Device
123+
const device = await Helpers.requestWebGPU();
124+
125+
// MiniGPU Computer and Renderer to run out programs
126+
computer = new Computer(device);
127+
renderer = new Renderer(device, canvas);
128+
129+
vec2.set(resolution, renderer.width, renderer.height);
130+
vec2.set(
131+
simulationResolution,
132+
Math.round(renderer.width * RESOLUTION),
133+
Math.round(renderer.height * RESOLUTION)
134+
);
135+
136+
// MiniGPU input which helps to set up and structure a buffer to use for our uniforms.
137+
uniforms = new UniformsInput(device, {
138+
resolution: resolution,
139+
simulation_resolution: simulationResolution,
140+
delta_time: 8.33 / 1000, // The timestep (as a fraction of a second), which will be calculated and updated on each frame
141+
viscosity: VISCOSITY,
142+
mouse_position: mousePosition,
143+
mouse_delta: mouseDelta,
144+
});
145+
146+
const dataSize = simulationResolution[0] * simulationResolution[1]; // Simulation width * height, to get our total number of grid cells
147+
148+
// MiniGPU extention of a Float32Array, which allows us to pass in a structure description and item count. Creating the array (including padding) is handled for you.
149+
const data = new StructuredFloat32Array(
150+
{
151+
velocity: () => [0, 0],
152+
divergence: 0,
153+
pressure: 0,
154+
},
155+
dataSize
156+
);
157+
158+
// MiniGPU input which creates two buffers which can be swapped to enable running a feedback loop
159+
simulationInput = new PingPongBufferInput(device, data);
160+
161+
const inputs = {
162+
simulationInput,
163+
uniforms,
164+
};
165+
166+
// MiniGPU compute program which can be run with a Computer
167+
boundaryProgram = new ComputeProgram(
168+
device,
169+
`${shaderHeader} ${shaderCommon} ${boundaryShader}`,
170+
inputs,
171+
data.count,
172+
WORKGROUP_SIZE
173+
);
174+
175+
advectionProgram = new ComputeProgram(
176+
device,
177+
`${shaderHeader} ${shaderCommon} ${advectionShader}`,
178+
inputs,
179+
data.count,
180+
WORKGROUP_SIZE
181+
);
182+
183+
externalForceProgram = new ComputeProgram(
184+
device,
185+
`${shaderHeader} ${shaderCommon} ${externalForceShader}`,
186+
inputs,
187+
data.count,
188+
WORKGROUP_SIZE
189+
);
190+
191+
viscousityProgram = new ComputeProgram(
192+
device,
193+
`${shaderHeader} ${shaderCommon} ${viscousityShader}`,
194+
inputs,
195+
data.count,
196+
WORKGROUP_SIZE
197+
);
198+
199+
divergenceProgram = new ComputeProgram(
200+
device,
201+
`${shaderHeader} ${shaderCommon} ${divergenceShader}`,
202+
inputs,
203+
data.count,
204+
WORKGROUP_SIZE
205+
);
206+
207+
pressureProgram = new ComputeProgram(
208+
device,
209+
`${shaderHeader} ${shaderCommon} ${pressureShader}`,
210+
inputs,
211+
data.count,
212+
WORKGROUP_SIZE
213+
);
214+
215+
pressureGradientProgram = new ComputeProgram(
216+
device,
217+
`${shaderHeader} ${shaderCommon} ${pressureGradientShader}`,
218+
inputs,
219+
data.count,
220+
WORKGROUP_SIZE
221+
);
222+
223+
// MiniGPU helps to create the buffers needed to run the vertex shader
224+
const geometry = new Geometry(
225+
renderer,
226+
primitives.createPlaneVertices(2, 2), // Using twgl.js to create plane vertices (these are created Y+ but are flipped to Z+ in the vertex shader)
227+
1
228+
);
229+
230+
// MiniGPU compute render which can be run with a Renderer
231+
renderProgram = new RenderProgram(
232+
renderer,
233+
`${shaderHeader} ${shaderCommon} ${renderShader}`,
234+
geometry,
235+
{
236+
simulation: simulationInput,
237+
uniforms,
238+
}
239+
);
240+
241+
// Used to add external force
242+
window.addEventListener("mousemove", onMouseMove);
243+
window.addEventListener("mouseup", onMouseUp);
244+
window.addEventListener("mousedown", onMouseDown);
245+
246+
requestAnimationFrame(animate);
247+
};
248+
249+
init();

0 commit comments

Comments
 (0)