Vala プログラミング

WebGPU プログラミング

おなが@京都先端科学大

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 storage class must be of
 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