Added: trunk/Websites/webkit.org/demos/webgpu/compute-boids.html (0 => 249446)
--- trunk/Websites/webkit.org/demos/webgpu/compute-boids.html (rev 0)
+++ trunk/Websites/webkit.org/demos/webgpu/compute-boids.html 2019-09-04 00:20:57 UTC (rev 249446)
@@ -0,0 +1,373 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <meta name="viewport" content="width=1200">
+ <title>WebGPU Compute Flocking</title>
+ <script src=""
+ <link rel="stylesheet" href=""
+ <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 Flocking</h1>
+ <p>
+ This demo uses a compute shader to update the positions of objects in parallel.
+ </p>
+ <canvas width="1200" height="1200"></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>
+ </div>
+ <script>
+ if (!navigator.gpu)
+ 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');
+ const context = canvas.getContext('gpu');
+
+ const swapChain = context.configureSwapChain({
+ device,
+ format: "bgra8unorm"
+ });
+
+ const computeBindGroupLayout = device.createBindGroupLayout({
+ bindings: [
+ { binding: 0, visibility: GPUShaderStageBit.COMPUTE, type: "uniform-buffer" },
+ { binding: 1, visibility: GPUShaderStageBit.COMPUTE, type: "storage-buffer" },
+ { binding: 2, visibility: GPUShaderStageBit.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>