| <!-- 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> |