WebGPU SPH シミュレーション(2)
(4月26日修正)
WGSL の最近の仕様変更により、4月6日付のプログラムは実行できなくなり
ました。
エラー箇所
1Parser error: A structure type with a [[block]] decoration cannot be used
as an element of an array
変更
[[block]] struct Particle { pos: vec3<f32>; vel: vec3<f32>; }; -> [[block]] をとる
2 Parser error: variables declared in the
an [[access]] qualified structure type
変更
var<storage> particle: Particles; -> var<storage> particle: [[access(read_write)]] Particles;
上記のほか、WebGPU と WGSL のワーニングがあります。
(WGSL のワーニングは、上記のエラーを修正すると表示されなくなります。)
WebGPU 1ComputePipeline computeStage: -> compute: 2RenderPassDescriptor colorAttachments, depthStencilAttachment attachment: -> view: WGSL 1 GlobalInvocationID entry point function の引数にする [[builtin(global_invocation_id)]] var<in> GlobalInvocationID : vec3<u32>; -> fn main( [[builtin(global_invocation_id)]] GlobalInvocationID : vec3<u32> ) 2 fn main() -> void { } 戻り値が void の場合、void と return は不要 3 const -> let
(4月6日)
前回のWebGPU SPH シミュレーションを、WebGPUとWGSL(WebGPU Shading
Language)の最新のバージョンで書き直しました。
(Chrome Canary で実行しています。)
WebGPU APIとWGSLの最新仕様は、以下のサイトにあります。
・WebGPU https://gpuweb.github.io/gpuweb/
・WGSL https://gpuweb.github.io/gpuweb/wgsl/
具体的な書き方は、以下のサイトが参考になります。
・http://austin-eng.com/webgpu-samples/
・https://github.com/cx20/webgpu-test
WebGPU 主な変更点
1 RenderPipeline
RenderPipelineDescriptorの書式が変更になりました。
layout がなくなり、メンバーが vertex, primitive, depthStencil, multisample,
fragment となっています。
var pipeline_point = device.createRenderPipeline({ vertex: { module: device.createShaderModule({ code: renderShaders.vertex}), entryPoint: "main", buffers: [ { arrayStride: 12, attributes: [{ shaderLocation: 0, format: "float32x3", offset: 0 }] }, { arrayStride: 16 * 2, stepMode: "instance", attributes: [{ shaderLocation: 2, format: "float32x3", offset: 0 }] } ], }, primitive: { topology: "triangle-list" }, depthStencil: { format: depthFormat, depthWriteEnabled: true, depthCompare: "less" }, fragment: { module: device.createShaderModule({ code: renderShaders.fragment}), entryPoint: "main", targets: [{ format: "bgra8unorm", }], }, });
vertex には module(vertex shader を設定)、entryPoint(shader で最初に実行される関数を指定)、buffers メンバーがあります。buffers に VertexBufferLayout(arrayStride: , stepMode: , attribute: ) を記述します。これは、
以前の vertexState に相当する部分です。
fragment メンバーにはmodule、entryPoint に加えて、targets があります。
targets に以前のcolorStateにあったformat を記述します。
ComputePipelineDescriptorでも、layout メンバーがなくなりました。
2 BindGroup の layout 設定
BindGroup の layout 設定は、Pipeline の getBindGroupLayout(index) メソッド
で行います。
Pipeline は内部スロットとして PipelineLayout を持っており、また
PipelineLayoutは内部スロットとして、BindGroupLayout のリストを持って
います。これらはPipeline の作成時に生成されます。
BindGroup の layout は、この BindGroupLayout のリストから取得します。
layout: pipeline_point.getBindGroupLayout(0),
3 defaultQueue
データをバッファに書き込む際に使用する defaultQueue が queue に
変更されます。
device.queue.writeBuffer(particleBuffer, 0, particleData);
その他
・GPUTextureUsage.OUTPUT_ATTACHMENT
-> GPUTextureUsage.RENDER_ATTACHMENT
・vertex format ‘float3' -> 'float32x3'
WGSL
WGSL とこれまでの GLSL では、書式が大分異なります。ここでは Shader
作成時の注意点を挙げて起きます。
1 storage buffer、uniform buffer の設定
storage buffer や uniform buffer の変数は、structure またはその配列にする
必要があります。
・storage buffer [[block]] struct Particle { pos: vec3<f32>; vel: vec3<f32>; }; [[block]] struct Particles { particles: array<Particle, ${NUM_PARTICLES}>; }; [[group(0), binding(0)]] var<storage> particle: Particles; [[block]] struct DensityBuffer { dens: array<f32>; }; [[group(0), binding(1)]] var<storage> density: DensityBuffer; ・uniform buffer [[block]] struct Uniforms { proj_view : mat4x4<f32>; }; [[binding(0), group(0)]] var<uniform> uniforms : Uniforms;
2 演算
・var j: u32 = 0u; 0 だけでは i32 になる。
・j = j + 1u; インクリメント演算子はない。
・型の異なる変数の演算
force: vec3<f32>, density: f32 の場合、
force / density や force - density の計算はできない。
各成分ごとに計算する。
* shader を WGSL で記述すると、glslang モジュールを使う必要はありません。
プログラム
(4月26日更新)
sph-3d.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>WebGPU SPH</title> <script src="../libs/gl-matrix-min.js"></script> <script src="../libs/webgl-util.js"></script> <script src="../libs/utils.js"></script> </head> <body> <canvas id="webgpu-canvas" width="800" height="600"></canvas> <script> (async () => { // Get GPU adapter and device const adapter = await navigator.gpu.requestAdapter(); const device = await adapter.requestDevice(); // Get canvas and context const canvas = document.getElementById("webgpu-canvas"); const context = canvas.getContext("gpupresent"); // Constants const MATH_PI = Math.PI; const NUM_PARTICLES = 1000; const POINT_SIZE = 0.005; const REST_DENSITY = 1000.0; const L0 = 0.01; const N0 = 12; const PARTICLE_MASS = REST_DENSITY * L0**3; const SMOOTHING_LENGTH = (3.0 * N0 / (4 * MATH_PI))**(1.0/3.0) * L0; const WEIGHT_RHO_COEF = 315.0 / (64.0 * MATH_PI * SMOOTHING_LENGTH**3); const WEIGHT_PRESSURE_COEF = 45.0 / (MATH_PI * SMOOTHING_LENGTH**4); const WEIGHT_VISCOSITY_COEF = 45.0 / (MATH_PI * SMOOTHING_LENGTH**5); const PRESSURE_STIFFNESS = 15.0; const VISC = 3.0; // boundary condition ( penalty method ) const PARTICLE_RADIUS = L0 / 2; const EPSIRON = 0.0001; const EXT_STIFF = 10000.0; const EXT_DAMP = 250.0; const DT = 0.0025; // particle min- and max-position const pos_min = [-1.0, -1.0, -1.0]; const pos_max = [-0.7, 1.0, -0.9]; // camera const eye = [-0.7, -1.0, -0.2]; const center = [-0.7, -1.0, -1.2]; const up = [0, 1, 0]; // initial particle data var particleData = new Float32Array(8 * NUM_PARTICLES); var x = 0; var y = 0; var z = 0; for (let i = 0; i < particleData.length; i += 8) { // position particleData[i] = pos_min[0] + L0 * x; particleData[i + 1] = pos_min[1] + L0 * y; particleData[i + 2] = pos_min[2] + L0 * z; particleData[i + 3] = 1; x++; if (x >= 6) { x = 0; z++; } if (z >= 10) { x = 0; z = 0; y++; } // velocity particleData[i + 4] = 0.0; particleData[i + 5] = 0.0; particleData[i + 6] = 0.0; particleData[i + 7] = 1; } console.log("particleData: ", particleData); // compute shader const computeShaders = { density_pressure:` struct Particle { pos: vec3<f32>; vel: vec3<f32>; }; [[block]] struct Particles { particles: array<Particle, ${NUM_PARTICLES}>; }; [[group(0), binding(0)]] var<storage> particle: [[access(read_write)]] Particles; [[block]] struct DensityBuffer { dens: array<f32>; }; [[group(0), binding(1)]] var<storage> density: [[access(read_write)]] DensityBuffer; [[block]] struct PressureBuffer { pres: array<f32>; }; [[group(0), binding(2)]] var<storage> pressure: [[access(read_write)]] PressureBuffer; [[stage(compute)]] fn main([[builtin(global_invocation_id)]] GlobalInvocationID : vec3<u32>) { // constants let particleNum : u32 = ${NUM_PARTICLES}u; let mass : f32 = ${PARTICLE_MASS}; let h : f32 = ${SMOOTHING_LENGTH}; let rest_density : f32 = f32(${REST_DENSITY}); let pressure_stiffness : f32 = f32(${PRESSURE_STIFFNESS}); let w_rho_coef : f32 = ${WEIGHT_RHO_COEF}; var index : u32 = GlobalInvocationID.x; var position_i: vec3<f32> = particle.particles[index].pos; // compute density var density_sum: f32 = 0.0; for (var j: u32 = 0u; j < particleNum; j = j + 1u) { var position_j: vec3<f32> = particle.particles[j].pos; var delta: vec3<f32> = position_i - position_j; var r: f32 = length(delta); if (r < h) { var rh: f32 = r / h; var rh2: f32 = 1.0 - rh * rh; density_sum = density_sum + mass * w_rho_coef * rh2 * rh2 * rh2; } } density.dens[index] = density_sum; // compute pressure pressure.pres[index] = max(pressure_stiffness * (density_sum - rest_density), 0.0); }`, force:` struct Particle { pos: vec3<f32>; vel: vec3<f32>; }; [[block]] struct Particles { particles: array<Particle, ${NUM_PARTICLES}>; }; [[group(0), binding(0)]] var<storage> particle: [[access(read_write)]] Particles; struct Force { force: vec3<f32>; }; [[block]] struct Forces { forces: array<Force, ${NUM_PARTICLES}>; }; [[group(0), binding(1)]] var<storage> force: [[access(read_write)]] Forces; [[block]] struct DensityBuffer { dens: array<f32>; }; [[group(0), binding(2)]] var<storage> density: [[access(read_write)]] DensityBuffer; [[block]] struct PressureBuffer { pres: array<f32>; }; [[group(0), binding(3)]] var<storage> pressure: [[access(read_write)]] PressureBuffer; [[stage(compute)]] fn main([[builtin(global_invocation_id)]] GlobalInvocationID : vec3<u32>) { // constants let particleNum: u32 = ${NUM_PARTICLES}u; let mass: f32 = ${PARTICLE_MASS}; let h: f32 = ${SMOOTHING_LENGTH}; let visc: f32 = f32(${VISC}); let w_pressure_coef: f32 = ${WEIGHT_PRESSURE_COEF}; let w_visc_coef: f32 = ${WEIGHT_VISCOSITY_COEF}; let gravity: vec3<f32> = vec3<f32>(0.0, -9.8, 0.0); var index: u32 = GlobalInvocationID.x; // forces var pressure_force: vec3<f32> = vec3<f32>(0.0, 0.0, 0.0); var viscosity_force: vec3<f32> = vec3<f32>(0.0, 0.0, 0.0); var position_i: vec3<f32> = particle.particles[index].pos; var velocity_i: vec3<f32> = particle.particles[index].vel; var pressure_i: f32 = pressure.pres[index]; for (var j: u32 = 0u; j < particleNum; j = j + 1u) { if (j == index) { continue; } var position_j: vec3<f32> = particle.particles[j].pos; var delta: vec3<f32> = position_i - position_j; var r: f32 = length(delta); var velocity_j: vec3<f32> = particle.particles[j].vel; var density_j: f32 = density.dens[j]; var pressure_j: f32 = pressure.pres[j]; if (r < h) { var rh: f32 = 1.0 - r / h; pressure_force = pressure_force + 0.5 * mass * (pressure_i + pressure_j) / density_j * w_pressure_coef * rh * rh * normalize(delta); var vji: vec3<f32> = velocity_j - velocity_i; viscosity_force = viscosity_force + visc * (mass/density_j) * vji * w_visc_coef * rh; } } var external_force: vec3<f32> = density.dens[index] * gravity; force.forces[index].force = pressure_force + viscosity_force + external_force; }`, integrate:` struct Particle { pos: vec3<f32>; vel: vec3<f32>; }; [[block]] struct Particles { particles: array<Particle, ${NUM_PARTICLES}>; }; [[group(0), binding(0)]] var<storage> particle: [[access(read_write)]] Particles; struct Force { force: vec3<f32>; }; [[block]] struct Forces { forces: array<Force, ${NUM_PARTICLES}>; }; [[group(0), binding(1)]] var<storage> force: [[access(read_write)]] Forces; [[block]] struct DensityBuffer { dens: array<f32>; }; [[group(0), binding(2)]] var<storage> density: [[access(read_write)]] DensityBuffer; [[stage(compute)]] fn main([[builtin(global_invocation_id)]] GlobalInvocationID : vec3<u32>) { // constants let TIME_STEP: f32 = ${DT}; let radius: f32 = ${PARTICLE_RADIUS}; let epsiron: f32 = ${EPSIRON}; let extstiff: f32 = f32(${EXT_STIFF}); let extdamp: f32 = f32(${EXT_DAMP}); let min_position: vec3<f32> = vec3<f32>(f32(${pos_min[0]}), f32(${pos_min[1]}), f32(${pos_min[2]})); let max_position: vec3<f32> = vec3<f32>(f32(${pos_max[0]}), f32(${pos_max[1]}), f32(${pos_max[2]})); var index: u32 = GlobalInvocationID.x; // integrate var density_i: f32 = density.dens[index]; var acceleration: vec3<f32> = vec3<f32>( force.forces[index].force.x / density_i, force.forces[index].force.y / density_i, force.forces[index].force.z / density_i ); var current_pos: vec3<f32> = particle.particles[index].pos; var current_vel: vec3<f32> = particle.particles[index].vel; var diff_min: vec3<f32> = vec3<f32>( 2.0 * radius - (current_pos.x - min_position.x), 2.0 * radius - (current_pos.y - min_position.y), 2.0 * radius - (current_pos.z - min_position.z) ); var diff_max: vec3<f32> = vec3<f32>( 2.0 * radius - (max_position.x - current_pos.x), 2.0 * radius - (max_position.y - current_pos.y), 2.0 * radius - (max_position.z - current_pos.z) ); // boundary conditions var normal: vec3<f32>; var adj: f32; if (diff_min.x > epsiron) { normal = vec3<f32>( 1.0, 0.0, 0.0 ); adj = extstiff * diff_min.x - extdamp * dot( normal, current_vel ); acceleration = acceleration + adj * normal; } if (diff_max.x > epsiron) { normal = vec3<f32>( -1.0, 0.0, 0.0 ); adj = extstiff * diff_max.x - extdamp * dot( normal, current_vel ); acceleration = acceleration + adj * normal; } if (diff_min.y > epsiron) { normal = vec3<f32>( 0.0, 1.0, 0.0 ); adj = extstiff * diff_min.y - extdamp * dot( normal, current_vel ); acceleration = acceleration + adj * normal; } if (diff_max.y > epsiron) { normal = vec3<f32>( 0.0, -1.0, 0.0 ); adj = extstiff * diff_max.y - extdamp * dot( normal, current_vel ); acceleration = acceleration + adj * normal; } if (diff_min.z > epsiron) { normal = vec3<f32>( 0.0, 0.0, 1.0 ); adj = extstiff * diff_min.z - extdamp * dot( normal, current_vel ); acceleration = acceleration + adj * normal; } if (diff_max.z > epsiron) { normal = vec3<f32>( 0.0, 0.0, -1.0 ); adj = extstiff * diff_max.z - extdamp * dot( normal, current_vel ); acceleration = acceleration + adj * normal; } var new_velocity: vec3<f32> = current_vel + TIME_STEP * acceleration; var new_position: vec3<f32> = current_pos + TIME_STEP * new_velocity; particle.particles[index].vel = new_velocity; particle.particles[index].pos = new_position; }` }; const particleBuffer = device.createBuffer({ size: particleData.byteLength, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE }); device.queue.writeBuffer(particleBuffer, 0, particleData); const forceBuffer = device.createBuffer({ size: 16 * NUM_PARTICLES, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }); const densityBuffer = device.createBuffer({ size: 4 * NUM_PARTICLES, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }); const pressureBuffer = device.createBuffer({ size: 4 * NUM_PARTICLES, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }); const computePipeline_density = device.createComputePipeline({ compute: { module: device.createShaderModule({ code: computeShaders.density_pressure }), entryPoint: "main" } }); const computePipeline_force = device.createComputePipeline({ compute: { module: device.createShaderModule({ code: computeShaders.force }), entryPoint: "main" } }); const computePipeline_integrate = device.createComputePipeline({ compute: { module: device.createShaderModule({ code: computeShaders.integrate }), entryPoint: "main" } }); const computeBindGroup_density = device.createBindGroup({ layout: computePipeline_density.getBindGroupLayout(0), entries: [ { binding: 0, resource: { buffer: particleBuffer } }, { binding: 1, resource: { buffer: densityBuffer } }, { binding: 2, resource: { buffer: pressureBuffer } }, ] }); const computeBindGroup_force = device.createBindGroup({ layout: computePipeline_force.getBindGroupLayout(0), entries: [ { binding: 0, resource: { buffer: particleBuffer } }, { binding: 1, resource: { buffer: forceBuffer } }, { binding: 2, resource: { buffer: densityBuffer } }, { binding: 3, resource: { buffer: pressureBuffer } }, ] }); const computeBindGroup_integrate = device.createBindGroup({ layout: computePipeline_integrate.getBindGroupLayout(0), entries: [ { binding: 0, resource: { buffer: particleBuffer } }, { binding: 1, resource: { buffer: forceBuffer } }, { binding: 2, resource: { buffer: densityBuffer } }, ] }); // render shader const renderShaders = { vertex:` [[block]] struct Uniforms { proj_view : mat4x4<f32>; }; [[binding(0), group(0)]] var<uniform> uniforms : Uniforms; [[location(0)]] var<in> vertexPosition : vec3<f32>; [[location(2)]] var<in> position : vec3<f32>; [[builtin(position)]] var<out> Position : vec4<f32>; [[stage(vertex)]] fn main() -> void { const scale : f32 = ${POINT_SIZE}; var scaleMTX : mat4x4<f32> = mat4x4<f32>( vec4<f32>(scale, 0.0, 0.0, 0.0), vec4<f32>(0.0, scale, 0.0, 0.0), vec4<f32>(0.0, 0.0, scale, 0.0), vec4<f32>(position, 1.0) ); Position = uniforms.proj_view * scaleMTX * vec4<f32>(vertexPosition, 1.0); return; }`, fragment:` [[location(0)]] var<out> outColor : vec4<f32>; [[stage(fragment)]] fn main() -> void { outColor = vec4<f32>(1.0, 0.0, 0.0, 1.0); return; }` }; const cubeData = utils.createCube(); const numVertices = cubeData.positions.length / 3; const vertexBuffer = device.createBuffer({ size: cubeData.positions.byteLength, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST }); device.queue.writeBuffer(vertexBuffer, 0, cubeData.positions); // Setup render outputs ( swapcahain depth texture) var swapChainFormat = "bgra8unorm"; var swapChain = context.configureSwapChain({ device, format: swapChainFormat, usage: GPUTextureUsage.RENDER_ATTACHMENT }); var depthFormat = "depth24plus-stencil8"; var depthTexture = device.createTexture({ size: { width: canvas.width, height: canvas.height, }, format: depthFormat, usage: GPUTextureUsage.RENDER_ATTACHMENT }); // Create render pipeline var pipeline_point = device.createRenderPipeline({ vertex: { module: device.createShaderModule({ code: renderShaders.vertex}), entryPoint: "main", buffers: [ { arrayStride: 12, attributes: [{ shaderLocation: 0, format: "float32x3", offset: 0 }] }, { arrayStride: 16 * 2, stepMode: "instance", attributes: [{ shaderLocation: 2, format: "float32x3", offset: 0 }] } ], }, primitive: { topology: "triangle-list" }, depthStencil: { format: depthFormat, depthWriteEnabled: true, depthCompare: "less" }, fragment: { module: device.createShaderModule({ code: renderShaders.fragment}), entryPoint: "main", targets: [{ format: "bgra8unorm", }], }, }); // Setup renderPassDesc var renderPassDesc = { colorAttachments: [{ view: undefined, loadValue: [0.3, 0.3, 0.3, 1] }], depthStencilAttachment: { view: depthTexture.createView(), depthLoadValue: 1.0, depthStoreOp: "store", stencilLoadValue: 0, stencilStoreOp: "store" } }; // Create uniform buffer var viewParamsBuffer = device.createBuffer({ size: 16 * 4, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); // Create bind group (setup uniform buffer) var bindGroup = device.createBindGroup({ layout: pipeline_point.getBindGroupLayout(0), entries: [ { binding: 0, resource: { buffer: viewParamsBuffer } } ] }); // Create arcball camera and view projection matrix var camera = new ArcballCamera(eye, center, up, 0.5, [canvas.width, canvas.height]); var projection = mat4.perspective(mat4.create(), 50 * Math.PI / 180.0, canvas.width / canvas.height, 0.1, 100); // projection_view matrix var projView = mat4.create(); // Controller var controller = new Controller(); controller.mousemove = function(prev, cur, evt) { if (evt.buttons == 1) { camera.rotate(prev, cur); } else if (evt.buttons == 2) { camera.pan([cur[0] - prev[0], prev[1] - cur[1]]); } }; controller.wheel = function(amt) { camera.zoom(amt * 3.0); }; controller.registerForCanvas(canvas); var canvasVisible = false; var observer = new IntersectionObserver(function(e) { if (e[0].isIntersecting) { canvasVisible = true; } else { canvasVisible = false; } }, {threshold: [0]}); observer.observe(canvas); // render requestAnimationFrame(function frame() { // command buffer var commandEncoder = device.createCommandEncoder(); // compute pass var computePass = commandEncoder.beginComputePass(); computePass.setPipeline(computePipeline_density); computePass.setBindGroup(0, computeBindGroup_density); computePass.dispatch(NUM_PARTICLES); computePass.setPipeline(computePipeline_force); computePass.setBindGroup(0, computeBindGroup_force); computePass.dispatch(NUM_PARTICLES); computePass.setPipeline(computePipeline_integrate); computePass.setBindGroup(0, computeBindGroup_integrate); computePass.dispatch(NUM_PARTICLES); computePass.endPass(); // render pass // SwapChain framebuffer renderPassDesc.colorAttachments[0].view = swapChain.getCurrentTexture().createView(); // write projView to viewParamsBuffer projView = mat4.mul(projView, projection, camera.camera); device.queue.writeBuffer(viewParamsBuffer, 0, projView); var renderPass = commandEncoder.beginRenderPass(renderPassDesc); renderPass.setPipeline(pipeline_point); renderPass.setVertexBuffer(0, vertexBuffer); renderPass.setVertexBuffer(1, particleBuffer); renderPass.setBindGroup(0, bindGroup); renderPass.draw(numVertices, NUM_PARTICLES, 0, 0); renderPass.endPass(); device.queue.submit([commandEncoder.finish()]); requestAnimationFrame(frame); }); })(); </script> </body> </html>
次のライブラリを使用しています。
・数学関連(gl-matrix-min.js)
・カメラ(webgl-util.js) https://github.com/Twinklebear/webgpu-experiments
・cubeデータ(utils.js) https://github.com/tsherif/webgpu-examples