blob: f1754709cdac718baa2c930004531f90bbc3e6e8 [file] [log] [blame]
<!-- Based off of https://github.com/austinEng/webgpu-samples/blob/master/compute_boids.html -->
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=1200">
<title>WebGPU Compute Shader Flocking</title>
<script src="scripts/gl-matrix-min.js"></script>
<link rel="stylesheet" href="css/style.css"/>
<style>
body {
font-family: system-ui;
color: #f7f7ff;
background-color: rgb(38, 38, 127);
text-align: center;
}
canvas {
margin: 0 auto;
}
</style>
</head>
<body>
<div id="contents">
<h1>Compute Shader Flocking</h1>
<p>
This demo uses a compute shader to update the positions of objects in parallel.
</p>
<canvas></canvas>
</div>
<div id="error">
<h2>WebGPU not available</h2>
<p>
Make sure you are on a system with WebGPU enabled. In
Safari, first make sure the Developer Menu is visible (Preferences >
Advanced), then Develop > Experimental Features > WebGPU.
</p>
<p>
In addition, you must be using Safari Technology Preview 92 or above.
You can get the latest STP <a href="https://developer.apple.com/safari/download/">here</a>.
</p>
</div>
<script>
if (!navigator.gpu || GPUBufferUsage.COPY_SRC === undefined)
document.body.className = 'error';
const numParticles = 1500;
const renderShadersWHLSL = `
vertex float4 vertex_main(float2 particlePos : attribute(0), float2 particleVel : attribute(1), float2 position : attribute(2)) : SV_Position
{
float angle = -atan(particleVel.x / particleVel.y);
float2 result = float2(position.x * cos(angle) - position.y * sin(angle),
position.x * sin(angle) + position.y * cos(angle));
return float4(result + particlePos, 0, 1);
}
fragment float4 fragment_main() : SV_Target 0
{
return float4(1.0, 1.0, 1.0, 1.0);
}
`;
const computeShaderWHLSL = `
struct Particle {
float2 pos;
float2 vel;
}
struct SimParams {
float deltaT;
float rule1Distance;
float rule2Distance;
float rule3Distance;
float rule1Scale;
float rule2Scale;
float rule3Scale;
}
[numthreads(1, 1, 1)]
compute void compute_main(constant SimParams[] paramsBuffer : register(b0), device Particle[] particlesA : register(u1), device Particle[] particlesB : register(u2), float3 threadID : SV_DispatchThreadID) {
uint index = uint(threadID.x);
SimParams params = paramsBuffer[0];
if (index >= ${numParticles}) {
return;
}
float2 vPos = particlesA[index].pos;
float2 vVel = particlesA[index].vel;
float2 cMass = float2(0.0, 0.0);
float2 cVel = float2(0.0, 0.0);
float2 colVel = float2(0.0, 0.0);
float cMassCount = 0.0;
float cVelCount = 0.0;
float2 pos;
float2 vel;
for (uint i = 0; i < ${numParticles}; ++i) {
if (i == index) { continue; }
pos = particlesA[i].pos.xy;
vel = particlesA[i].vel.xy;
if (distance(pos, vPos) < params.rule1Distance) {
cMass += pos;
cMassCount++;
}
if (distance(pos, vPos) < params.rule2Distance) {
colVel -= (pos - vPos);
}
if (distance(pos, vPos) < params.rule3Distance) {
cVel += vel;
cVelCount++;
}
}
if (cMassCount > 0.0) {
cMass = cMass / cMassCount - vPos;
}
if (cVelCount > 0.0) {
cVel = cVel / cVelCount;
}
vVel += cMass * params.rule1Scale + colVel * params.rule2Scale + cVel * params.rule3Scale;
// clamp velocity for a more pleasing simulation.
vVel = normalize(vVel) * clamp(length(vVel), 0.0, 0.1);
// kinematic update
vPos += vVel * params.deltaT;
// Wrap around boundary
if (vPos.x < -1.0) vPos.x = 1.0;
if (vPos.x > 1.0) vPos.x = -1.0;
if (vPos.y < -1.0) vPos.y = 1.0;
if (vPos.y > 1.0) vPos.y = -1.0;
particlesB[index].pos = vPos;
particlesB[index].vel = vVel;
}
`;
async function init() {
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
const canvas = document.querySelector('canvas');
let size = window.innerWidth < window.innerHeight ? window.innerWidth : window.innerHeight;
size *= 0.75;
canvas.width = size;
canvas.height = size;
const context = canvas.getContext('gpu');
const swapChain = context.configureSwapChain({
device,
format: "bgra8unorm"
});
const computeBindGroupLayout = device.createBindGroupLayout({
bindings: [
{ binding: 0, visibility: GPUShaderStage.COMPUTE, type: "uniform-buffer" },
{ binding: 1, visibility: GPUShaderStage.COMPUTE, type: "storage-buffer" },
{ binding: 2, visibility: GPUShaderStage.COMPUTE, type: "storage-buffer" },
],
});
const computePipelineLayout = device.createPipelineLayout({
bindGroupLayouts: [computeBindGroupLayout],
});
device.pushErrorScope('validation');
const renderModule = device.createShaderModule({ code: renderShadersWHLSL, isWHLSL: true });
const renderPipeline = device.createRenderPipeline({
layout: device.createPipelineLayout({ bindGroupLayouts: [] }),
vertexStage: {
module: renderModule,
entryPoint: "vertex_main"
},
fragmentStage: {
module: renderModule,
entryPoint: "fragment_main"
},
primitiveTopology: "triangle-list",
depthStencilState: {
depthWriteEnabled: true,
depthCompare: "less",
format: "depth32float-stencil8",
stencilFront: {},
stencilBack: {},
},
vertexInput: {
indexFormat: "uint32",
vertexBuffers: [{
// instanced particles buffer
stride: 4 * 4,
stepMode: "instance",
attributeSet: [{
// instance position
shaderLocation: 0,
offset: 0,
format: "float2"
}, {
// instance velocity
shaderLocation: 1,
offset: 2 * 4,
format: "float2"
}],
}, {
// vertex buffer
stride: 2 * 4,
stepMode: "vertex",
attributeSet: [{
// vertex positions
shaderLocation: 2,
offset: 0,
format: "float2"
}],
}],
},
rasterizationState: {
frontFace: 'ccw',
cullMode: 'none',
},
colorStates: [{
format: "bgra8unorm",
alphaBlend: {},
colorBlend: {},
}],
});
device.popErrorScope().then(error => {
if (error)
console.error("Render shaders: " + error.message);
});
device.pushErrorScope('validation');
const computePipeline = device.createComputePipeline({
layout: computePipelineLayout,
computeStage: {
module: device.createShaderModule({
code: computeShaderWHLSL, isWHLSL: true
}),
entryPoint: "compute_main",
}
});
device.popErrorScope().then(error => {
if (error)
console.error("Compute shader: " + error.message);
});
const depthTexture = device.createTexture({
size: { width: canvas.width, height: canvas.height, depth: 1 },
arrayLayerCount: 1,
mipLevelCount: 1,
sampleCount: 1,
dimension: "2d",
format: "depth32float-stencil8",
usage: GPUTextureUsage.OUTPUT_ATTACHMENT
});
const renderPassDescriptor = {
colorAttachments: [{
loadOp: "clear",
clearColor: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
storeOp: "store",
}],
depthStencilAttachment: {
attachment: depthTexture.createDefaultView(),
depthLoadOp: "clear",
depthStoreOp: "store",
clearDepth: 1.0,
stencilLoadValue: 0,
stencilStoreOp: "store",
}
};
const vertexBufferData = new Float32Array([-0.01, -0.02, 0.01, -0.02, 0.00, 0.02]);
const [verticesBuffer, verticesArrayBuffer] = device.createBufferMapped({
size: vertexBufferData.byteLength,
usage: GPUBufferUsage.VERTEX,
});
new Float32Array(verticesArrayBuffer).set(vertexBufferData);
verticesBuffer.unmap();
const simParamData = new Float32Array([0.04, 0.1, 0.025, 0.025, 0.02, 0.05, 0.005]);
const [simParamBuffer, simParamArrayBuffer] = device.createBufferMapped({
size: simParamData.byteLength,
usage: GPUBufferUsage.UNIFORM,
});
new Float32Array(simParamArrayBuffer).set(simParamData);
simParamBuffer.unmap();
const initialParticleData = new Float32Array(numParticles * 4);
for (let i = 0; i < numParticles; ++i) {
initialParticleData[4 * i + 0] = 2 * (Math.random() - 0.5);
initialParticleData[4 * i + 1] = 2 * (Math.random() - 0.5);
initialParticleData[4 * i + 2] = 2 * (Math.random() - 0.5) * 0.1;
initialParticleData[4 * i + 3] = 2 * (Math.random() - 0.5) * 0.1;
}
const particleBuffers = new Array(2);
const particleBindGroups = new Array(2);
for (let i = 0; i < 2; ++i) {
let particleArrayBuffer;
[particleBuffers[i], particleArrayBuffer] = device.createBufferMapped({
size: initialParticleData.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.STORAGE
});
new Float32Array(particleArrayBuffer).set(initialParticleData);
particleBuffers[i].unmap();
}
for (let i = 0; i < 2; ++i) {
particleBindGroups[i] = device.createBindGroup({
layout: computeBindGroupLayout,
bindings: [{
binding: 0,
resource: {
buffer: simParamBuffer,
offset: 0,
size: simParamData.byteLength
},
}, {
binding: 1,
resource: {
buffer: particleBuffers[i],
offset: 0,
size: initialParticleData.byteLength,
},
}, {
binding: 2,
resource: {
buffer: particleBuffers[(i + 1) % 2],
offset: 0,
size: initialParticleData.byteLength,
},
}],
});
}
let t = 0;
function frame() {
renderPassDescriptor.colorAttachments[0].attachment = swapChain.getCurrentTexture().createDefaultView();
const commandEncoder = device.createCommandEncoder({});
{
const passEncoder = commandEncoder.beginComputePass();
passEncoder.setPipeline(computePipeline);
passEncoder.setBindGroup(0, particleBindGroups[t % 2]);
passEncoder.dispatch(numParticles, 1, 1);
passEncoder.endPass();
}
{
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.setPipeline(renderPipeline);
passEncoder.setVertexBuffers(0, [particleBuffers[(t + 1) % 2], verticesBuffer], [0, 0]);
passEncoder.draw(3, numParticles, 0, 0);
passEncoder.endPass();
}
device.getQueue().submit([commandEncoder.finish()]);
++t;
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
init();
</script>
</body>
</html>