Vala プログラミング

WebGPU プログラミング

おなが@京都先端科学大

WebGPU SPH シミュレーション ( Chrome Canary )

WebGPUを使って、SPH(Smoothed Particle Hydrodynamics)のシミュレーションを行いました。
f:id:onagat12:20200921143837g:plain

メインPCをMacBook Pro(13in.)に変えました。Chrome Canaryを使って表示して
います。

SPHの計算は、前回のjuliaによる計算と同様ですが、ここではcompute shaderを
使って、GPUで計算しています。
compute shaderでの計算は、Vulkanによるcompute shaderを用いた計算(2d)を
参考にしました。
SPH simulation in Vulkan compute shader
https://github.com/multiprecision/sph_vulkan

sphの計算は、以下の様に行います。
1 粒子の密度(density), 圧力(pressure)を計算する。
2 1で計算したdensity, pressureを使って、力の圧力項(puressure_force)と力の
  粘性項(viscosity_force)を計算する。
3 2で計算したpuressure_forceとviscosity_forceに、重力項(gravity)、境界の壁
  から受ける力(adj)を加えて、新しい速度と新しい位置を計算する。

これをcompute shaderで行うには、1つのshaderでは出来ません。3つのshaderに分けて行います。
1つ目のshader(compute_density_pressure)
 densityとpressureの式を記述し、dispatchを行う。
2つ目のshader(compute_force)
 bufferに保存されたdensityとpressureを使って、puressure_forceと
 viscosity_forceの式を記述し、dispatchを行う。
3つ目のshader(compute_integrate)
 bufferに保存されたpuressure_force、viscosity_forceと重力、壁による力を
 加えた加速度を使って、新しい速度と位置を計算する式を記述し、dispatchを
 行う。

プログラムには、以下のライブラリを使用しています。
1 point(cubeで表現)を描画するためのcubeプログラム utils.js
  WebGPU Examples https://github.com/tsherif/webgpu-examples
2 cameraプログラム webgl-util.min.js
  WebGPU Experiments https://github.com/Twinklebear/webgpu-experiments
3 matrix and vector計算ライブラリ gl-matrix.js
  https://github.com/toji/gl-matrix

プログラムの注意点
1 forceBuffer の buffer size
  size: 16 * NUM_PARTICLES と 16 にする。(shader では vec3 force[] と設定)
2 pipeline の vertexBuffers - arrayStride
  粒子 position の buffer size は 16 * NUM_PARTICLES としているので、
  vertexBuffers の arrayStride も 16 にする。
  (粒子は instance として表示している。)

最近、WebGPUの仕様変更が2つありました。
1 createBufferMapped()の廃止
  createBufferMapped()が使えなくなりましたので、次の様に変更します。

const [positionBuffer, positionBufferMap] = device.createBufferMapped({
    size: positionData.byteLength,
    usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE
});
new Float32Array(positionBufferMap).set(positionData);
positionBuffer.unmap();
->
const positionBuffer = device.createBuffer({
     size: positionData.byteLength,
     usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE
});
device.defaultQueue.writeBuffer(positionBuffer, 0, positionData);
または、
const positionBuffer = device.createBuffer({
    mappedAtCreation: true,
    size: positionData.byteLength,
    usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE
});
var mapping = positionBuffer.getMappedRange();
new Float32Array(mapping).set(positionData);
positionBuffer.unmap();

2 setIndexBuffer()の変更

renderPass.setIndexBuffer(planeindexBuf);(uint16はpipelineのvertexStateで設定)
->
renderPass.setIndexBuffer(planeindexBuf, "uint16");


プログラム
sph-3d.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>WebGPU</title>
    <script src="gl-matrix.js"></script>
    <script src="webgl-util.min.js"></script>
    <script src="utils.js"></script>
</head>
<body>
<canvas id="webgpu-canvas" width="800" height="600"></canvas>
<script>

(async () => {

    // Get GPU adapter and device
    // Load glslang Module
    const [adapter, glslang] = await Promise.all([
        navigator.gpu.requestAdapter(),
        import("https://unpkg.com/@webgpu/glslang@0.0.15/dist/web-devel/glslang.js").then(m => m.default())
    ]);
    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.8, -0.7, -0.2];
    const center = [-0.8, -1.0, -1.2];
    const up = [0, 1, 0];

    // initial particle data
    // position
    var positionData = new Float32Array(4 * NUM_PARTICLES);
    var x = 0;
    var y = 0;
    var z = 0;
    for (let i = 0; i < positionData.length; i += 4)
    {
        positionData[i]     = pos_min[0] + L0 * x;
        positionData[i + 1] = pos_min[1] + L0 * y;
        positionData[i + 2] = pos_min[2] + L0 * z;
        positionData[i + 3] = 1;
        x++;
        if (x >= 6)
        {
            x = 0;
            z++;
        }
        if (z >= 10)
        {
            x = 0;
            z = 0;
            y++;
        }
    }
    //console.log(“positionData: ", positionData);

    // velocity
    var velocityData = new Float32Array(4 * NUM_PARTICLES);
    for (let i = 0; i < velocityData.length; i += 4) {
        velocityData[i]     = 0.0;
        velocityData[i + 1] = 0.0;
        velocityData[i + 2] = 0.0;
        velocityData[i + 3] = 1;
    }
    //console.log(“velocityData: ", velocityData);

    // Shaders
    // compute shader
    const compute_density_pressure = `
    #version 450

    // constants
    const int particles = ${NUM_PARTICLES};
    const float mass = ${PARTICLE_MASS};
    const float h = ${SMOOTHING_LENGTH};

    const float rest_density = ${REST_DENSITY};
    const float pressure_stiffness = ${PRESSURE_STIFFNESS};
    const float w_rho_coef = ${WEIGHT_RHO_COEF};

    layout(std430, binding = 0) buffer position_block
    {
        vec3 position[];
    };

    layout(std430, binding = 1) buffer velocity_block
    {
        vec3 velocity[];
    };

    layout(std430, binding = 2) buffer force_block
    {
        vec3 force[];
    }; 

    layout(std430, binding = 3) buffer density_block
    {
        float density[];
    };

    layout(std430, binding = 4) buffer pressure_block
    {
        float pressure[];
    };
    
    void main()
    {
        uint i = gl_GlobalInvocationID.x;

        // compute density
        float density_sum = 0.0;
        for (uint j = 0; j < particles; j++)
        {
            vec3 delta = position[i] - position[j];
            float r = length(delta);

            if (r < h)
            {
                float rh = r / h;
                float rh2 = 1 - rh * rh;
                density_sum += mass * w_rho_coef * rh2 * rh2 * rh2;
            }
        }
        density[i] = density_sum;
        // compute pressure
        pressure[i] = max(pressure_stiffness * (density[i] - rest_density), 0.0);
    }
    `.trim();

    const compute_force = `
    #version 450

    // constants
    const int NUM_PARTICLES = ${NUM_PARTICLES};
    const float mass = ${PARTICLE_MASS};
    const float h = ${SMOOTHING_LENGTH};

    const float visc = ${VISC};
    const float w_pressure_coef = ${WEIGHT_PRESSURE_COEF};
    const float w_visc_coef = ${WEIGHT_VISCOSITY_COEF};

    //const vec3 gravity = vec3(0, -9806.65, 0.0);
    const vec3 gravity = vec3(0, -9.8, 0.0);

    layout(std430, binding = 0) buffer position_block
    {
        vec3 position[];
    };

    layout(std430, binding = 1) buffer velocity_block
    {
        vec3 velocity[];
    };

    layout(std430, binding = 2) buffer force_block
    {
        vec3 force[];
    };

    layout(std430, binding = 3) buffer density_block
    {
        float density[];
    };

    layout(std430, binding = 4) buffer pressure_block
    {
        float pressure[];
    };

    void main()
    {
        uint i = gl_GlobalInvocationID.x;

        // forces
        vec3 pressure_force = vec3(0, 0, 0);
        vec3 viscosity_force = vec3(0, 0, 0);

        for (uint j = 0; j < NUM_PARTICLES; j++)
        {
            if (i == j)
            {
                continue;
            }
            vec3 delta = position[i] - position[j];
            float r = length(delta);

            if (r < h)
            {
                float rh = 1 - r / h;

                pressure_force += 0.5 * mass * (pressure[i] + pressure[j]) / density[j] *
                    w_pressure_coef * rh * rh * normalize(delta);

                vec3 vji = velocity[j] - velocity[i];
                viscosity_force += visc * mass * vji / density[j] * w_visc_coef * rh;
            }
        }
        vec3 external_force = density[i] * gravity;

        force[i] = pressure_force + viscosity_force + external_force;
    }
    `.trim();

    const compute_integrate = `
    #version 450

    // constants
    const float TIME_STEP = ${DT};
    const float radius = ${PARTICLE_RADIUS};
    const float epsiron = ${EPSIRON};
    const float extstiff = ${EXT_STIFF};
    const float extdamp = ${EXT_DAMP};

    const vec3 min_position = vec3(${pos_min[0]}, ${pos_min[1]}, ${pos_min[2]});
    const vec3 max_position = vec3(${pos_max[0]}, ${pos_max[1]}, ${pos_max[2]});

    layout(std430, binding = 0) buffer position_block
    {
        vec3 position[];
    };

    layout(std430, binding = 1) buffer velocity_block
    {
        vec3 velocity[];
    };

    layout(std430, binding = 2) buffer force_block
    {
        vec3 force[];
    };

    layout(std430, binding = 3) buffer density_block
    {
        float density[];
    };

    layout(std430, binding = 4) buffer pressure_block
    {
        float pressure[];
    };

    void main()
    {
        uint i = gl_GlobalInvocationID.x;

        vec3 acceleration = force[i] / density[i];
        vec3 current_pos = position[i];
        vec3 current_vel = velocity[i];

        vec3 diff_min = 2.0 * radius - (current_pos - min_position);
        vec3 diff_max = 2.0 * radius - (max_position - current_pos);

        // boundary conditions
        if (diff_min.x > epsiron)
        {
            vec3 normal = vec3( 1.0, 0.0, 0.0 );
            float adj = extstiff * diff_min.x - extdamp * dot( normal, current_vel );
            acceleration += adj * normal;
        }
        if (diff_max.x > epsiron)
        {
            vec3 normal = vec3( -1.0, 0.0, 0.0 );
            float adj = extstiff * diff_max.x - extdamp * dot( normal, current_vel );
            acceleration += adj * normal;
        }
        if (diff_min.y > epsiron)
        {
            vec3 normal = vec3( 0.0, 1.0, 0.0 );
            float adj = extstiff * diff_min.y - extdamp * dot( normal, current_vel );
            acceleration += adj * normal;
        }
        if (diff_max.y > epsiron)
        {
            vec3 normal = vec3( 0.0, -1.0, 0.0 );
            float adj = extstiff * diff_max.y - extdamp * dot( normal, current_vel );
            acceleration += adj * normal;
        }
        if (diff_min.z > epsiron)
        {
            vec3 normal = vec3( 0.0, 0.0, 1.0 );
            float adj = extstiff * diff_min.z - extdamp * dot( normal, current_vel );
            acceleration += adj * normal;
        }
        if (diff_max.z > epsiron)
        {
            vec3 normal = vec3( 0.0, 0.0, -1.0 );
            float adj = extstiff * diff_max.z - extdamp * dot( normal, current_vel );
            acceleration += adj * normal;
        }

        vec3 new_velocity = current_vel + TIME_STEP * acceleration;
        vec3 new_position = current_pos + TIME_STEP * new_velocity;
        velocity[i] = new_velocity;
        position[i] = new_position;
    }
    `.trim();

    // Setup shader modules
    var compModule_density = device.createShaderModule({
        code: glslang.compileGLSL(compute_density_pressure, "compute")
    });
    var computeStage_density = {
        module: compModule_density,
        entryPoint: "main"
    };
    var compModule_force = device.createShaderModule({
        code: glslang.compileGLSL(compute_force, "compute")
    });
    var computeStage_force = {
        module: compModule_force,
        entryPoint: "main"
    };

    var compModule_integrate = device.createShaderModule({
        code: glslang.compileGLSL(compute_integrate, "compute")
    });
    var computeStage_integrate = {
        module: compModule_integrate,
        entryPoint: "main"
    };

    const positionBuffer = device.createBuffer({
        size: positionData.byteLength,
        usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE
    });
    device.defaultQueue.writeBuffer(positionBuffer, 0, positionData);

    const velocityBuffer = device.createBuffer({
        size: velocityData.byteLength,
        usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE
    });
    device.defaultQueue.writeBuffer(velocityBuffer, 0, velocityData);

    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
    });

    // compute bind group
    const computeBindGroupLayout = device.createBindGroupLayout({
        entries: [
            {
                binding: 0,
                visibility: GPUShaderStage.COMPUTE,
                type: "storage-buffer"
            },
            {
                binding: 1,
                visibility: GPUShaderStage.COMPUTE,
                type: "storage-buffer"
            },
            {
                binding: 2,
                visibility: GPUShaderStage.COMPUTE,
                type: "storage-buffer"
            },
            {
                binding: 3,
                visibility: GPUShaderStage.COMPUTE,
                type: "storage-buffer"
            },
            {
                binding: 4,
                visibility: GPUShaderStage.COMPUTE,
                type: "storage-buffer"
            },
        ]
    });

    const computeBindGroup = device.createBindGroup({
        layout: computeBindGroupLayout,
        entries: [
            {
                binding: 0,
                resource: {
                    buffer: positionBuffer
                }
            },
            {
                binding: 1,
                resource: {
                    buffer: velocityBuffer
                }
            },
            {
                binding: 2,
                resource: {
                    buffer: forceBuffer
                }
            },
            {
                binding: 3,
                resource: {
                    buffer: densityBuffer
                }
            },
            {
                binding: 4,
                resource: {
                    buffer: pressureBuffer
                }
            },
        ]
    });

    const computePipeline_density = device.createComputePipeline({
        layout: device.createPipelineLayout({bindGroupLayouts: [computeBindGroupLayout]}),
        computeStage: computeStage_density,
    });

    const computePipeline_force = device.createComputePipeline({
        layout: device.createPipelineLayout({bindGroupLayouts: [computeBindGroupLayout]}),
        computeStage: computeStage_force,
    });

    const computePipeline_integrate = device.createComputePipeline({
        layout: device.createPipelineLayout({bindGroupLayouts: [computeBindGroupLayout]}),
        computeStage: computeStage_integrate,
    });

    // vertex shader
    const vs_point = `
    #version 450

    layout(location=0) in vec3 vertexPosition;
    layout(location=2) in vec3 position;

    layout(set = 0, binding = 0, std140) uniform ViewParams {
        mat4 proj_view;
    };

    const float scale = ${POINT_SIZE};

    void main() {
        mat4 scaleMTX = mat4(
            scale, 0, 0, 0,
            0, scale , 0, 0,
            0, 0, scale, 0,
            position, 1
        );
        
        gl_Position = proj_view * scaleMTX * vec4(vertexPosition, 1.0);
    }
    `.trim();

    const fs_point = `
    #version 450

    layout(location=0) out vec4 fragColor;

    void main() {
        fragColor = vec4(1.0, 0.0, 0.0, 1.0);
    }
    `.trim();

    // plane
    const vs_plane = `
    #version 450

    layout(location=0) in vec4 vertexPosition;
    layout(location=1) in vec4 color;

    layout(location=0) out vec4 vColor;

    layout(set = 0, binding = 0, std140) uniform ViewParams {
        mat4 proj_view;
    };

    void main() {
        vColor = color;
        gl_Position = proj_view * vertexPosition;
    }
    `.trim();

    // fragment shader
    const fs_plane = `
    #version 450

    layout(location=0) in vec4 vColor;
    layout(location=0) out vec4 fragColor;

    void main() {
        fragColor = vColor;
    }
    `.trim();

    // Setup shader modules
    var vertModule_point = device.createShaderModule({code: glslang.compileGLSL(vs_point, "vertex")});
    var vertexStage_point =  {
        module: vertModule_point,
        entryPoint: "main"
    };

    var vertModule_plane = device.createShaderModule({code: glslang.compileGLSL(vs_plane, "vertex")});
    var vertexStage_plane =  {
        module: vertModule_plane,
        entryPoint: "main"
    };

    var fragModule_point = device.createShaderModule({code: glslang.compileGLSL(fs_point, "fragment")});
    var fragmentStage_point =  {
        module: fragModule_point,
        entryPoint: "main"
    };

    var fragModule_plane = device.createShaderModule({code: glslang.compileGLSL(fs_plane, "fragment")});
    var fragmentStage_plane =  {
        module: fragModule_plane,
        entryPoint: "main"
    };

    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.defaultQueue.writeBuffer(vertexBuffer, 0, cubeData.positions);

    // Setup vertexState
    var vertexState_point = {
        vertexBuffers: [
            {
                arrayStride: 12,
                attributes: [{
                    shaderLocation: 0,
                    format: "float3",
                    offset: 0
                }]
            },
            {
                arrayStride: 16,
                stepMode: "instance",
                attributes: [{
                    shaderLocation: 2,
                    format: "float3",
                    offset: 0
                }]
            },
        ]
    };

    // planes
    const planeData = new Float32Array([
        // bottom
        -1.0, -1.0,  1.0, 1.0, // position v0
            0.6, 0.6, 0.6, 1.0, //color
        -1.0, -1.0, -1.0, 1.0, // v1
            0.6, 0.6, 0.6, 1.0,
         1.0, -1.0, -1.0, 1.0, // v2
            0.6, 0.6, 0.6, 1.0,
         1.0, -1.0,  1.0, 1.0, // v3
            0.6, 0.6, 0.6, 1.0,
       // back
        -1.0,  1.0, -1.0, 1.0, // v4
            0.0, 0.7, 0.7, 1.0,
        -1.0, -1.0, -1.0, 1.0, // v5
            0.0, 0.7, 0.7, 1.0,
         1.0, -1.0, -1.0, 1.0, // v6
            0.0, 0.7, 0.7, 1.0,
         1.0,  1.0, -1.0, 1.0, // v7
            0.0, 0.7, 0.7, 1.0,
        // left
        -1.0,  1.0,  1.0, 1.0, // v8
            0.0, 0.9, 0.9, 1.0,
        -1.0, -1.0,  1.0, 1.0, // v9
            0.0, 0.9, 0.9, 1.0,
        -1.0, -1.0, -1.0, 1.0, // v10
            0.0, 0.9, 0.9, 1.0,
        -1.0,  1.0, -1.0, 1.0, // v12
            0.0, 0.9, 0.9, 1.0,
    ]);

    var planedataBuf = device.createBuffer({
        mappedAtCreation: true,
        size: planeData.byteLength,
        usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
    });
    var mapping = planedataBuf.getMappedRange();
    new Float32Array(mapping).set(planeData);
    planedataBuf.unmap();

    const planeindexData = new Uint16Array([
         0,  1,  2,  0,  2,  3, // bottom
         4,  5,  6,  4,  6,  7, // back
         8,  9, 10,  8, 10, 11, // left
    ]);

    var planeindexBuf = device.createBuffer({
        mappedAtCreation: true,
        size: planeindexData.byteLength,
        usage: GPUBufferUsage.INDEX
    });
    var mapping = planeindexBuf.getMappedRange();
    new Uint16Array(mapping).set(planeindexData);
    planeindexBuf.unmap();

    var vertexState_plane = {
        vertexBuffers: [
            {
                arrayStride: 2 * 4 * 4,
                attributes: [
                    {
                        format: "float4",
                        offset: 0,
                        shaderLocation: 0
                    },
                    {
                        format: "float4",
                        offset: 4 * 4,
                        shaderLocation: 1
                    }
                ]
            }
        ],
    };

    // Setup render outputs ( swapcahain depth texture)
    var swapChainFormat = "bgra8unorm";
    var swapChain = context.configureSwapChain({
        device,
        format: swapChainFormat,
        usage: GPUTextureUsage.OUTPUT_ATTACHMENT
    });

    var depthFormat = "depth24plus-stencil8";
    var depthTexture = device.createTexture({
        size: {
            width: canvas.width,
            height: canvas.height,
            depth: 1
        },
        format: depthFormat,
        usage: GPUTextureUsage.OUTPUT_ATTACHMENT
    });

    // Create bind group layout (uniform buffer)
    var bindGroupLayout = device.createBindGroupLayout({
        entries: [
            {
                binding: 0,
                visibility: GPUShaderStage.VERTEX,
                type: "uniform-buffer"
            }
        ]
    });

    // Create render pipeline
    var layout = device.createPipelineLayout({bindGroupLayouts: [bindGroupLayout]});
    var pipeline_point = device.createRenderPipeline({
        layout: layout,
        vertexStage: vertexStage_point,
        fragmentStage: fragmentStage_point,
        primitiveTopology: "triangle-list",
        vertexState: vertexState_point,
        colorStates: [{
            format: swapChainFormat
        }],
        depthStencilState: {
            format: depthFormat,
            depthWriteEnabled: true,
            depthCompare: "less"
        }
    });

    // plane
    var pipeline_plane = device.createRenderPipeline({
        layout: layout,
        vertexStage: vertexStage_plane,
        fragmentStage: fragmentStage_plane,
        primitiveTopology: "triangle-list",
        vertexState: vertexState_plane,
        colorStates: [{
            format: swapChainFormat
        }],
        depthStencilState: {
            format: depthFormat,
            depthWriteEnabled: true,
            depthCompare: "less"
        }
    });

    // Setup renderPassDesc
    var renderPassDesc = {
        colorAttachments: [{
            attachment: undefined,
            loadValue: [0.3, 0.3, 0.3, 1]
        }],
        depthStencilAttachment: {
            attachment: 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: bindGroupLayout,
        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);
        computePass.dispatch(NUM_PARTICLES);

        computePass.setPipeline(computePipeline_force);
        computePass.setBindGroup(0, computeBindGroup);
        computePass.dispatch(NUM_PARTICLES);

        computePass.setPipeline(computePipeline_integrate);
        computePass.setBindGroup(0, computeBindGroup);
        computePass.dispatch(NUM_PARTICLES);

        computePass.endPass();

        // render pass
        // SwapChain framebuffer
        renderPassDesc.colorAttachments[0].attachment =
            swapChain.getCurrentTexture().createView();

        // write projView to viewParamsBuffer
        projView = mat4.mul(projView, projection, camera.camera);
        device.defaultQueue.writeBuffer(viewParamsBuffer, 0, projView);

        var renderPass = commandEncoder.beginRenderPass(renderPassDesc);

        // draw points
        renderPass.setPipeline(pipeline_point);
        renderPass.setVertexBuffer(0, vertexBuffer);
        renderPass.setVertexBuffer(1, positionBuffer);
        renderPass.setBindGroup(0, bindGroup);
        renderPass.draw(numVertices, NUM_PARTICLES, 0, 0);
        // draw plane
        renderPass.setPipeline(pipeline_plane);
        renderPass.setVertexBuffer(0, planedataBuf);
        renderPass.setIndexBuffer(planeindexBuf, "uint16");
        renderPass.drawIndexed(planeindexData.length, 1, 0, 0, 0);

        renderPass.endPass();

        device.defaultQueue.submit([commandEncoder.finish()]);

        requestAnimationFrame(frame);
    });
})();
</script>
</body>
</html>