├── .gitignore ├── types ├── webgpu-radix-sort.d.ts └── global.d.ts ├── public └── cubemap │ ├── negx.png │ ├── negy.png │ ├── negz.png │ ├── posx.png │ ├── posy.png │ ├── posz.png │ └── README.md ├── vite.config.ts ├── sph ├── grid │ ├── gridClear.wgsl │ ├── gridBuild.wgsl │ └── reorderParticles.wgsl ├── copyPosition.wgsl ├── integrate.wgsl ├── density.wgsl ├── force.wgsl └── sph.ts ├── tsconfig.json ├── package.json ├── mls-mpm ├── clearGrid.wgsl ├── copyPosition.wgsl ├── updateGrid.wgsl ├── p2g_1.wgsl ├── p2g_2.wgsl ├── g2p.wgsl └── mls-mpm.ts ├── common.ts ├── .github └── workflows │ └── main.yml ├── render ├── fullScreen.wgsl ├── gaussian.wgsl ├── thicknessMap.wgsl ├── thickness.wgsl ├── depthMap.wgsl ├── bilateral.wgsl ├── sphere.wgsl ├── fluid.wgsl └── fluidRender.ts ├── LICENSE ├── index.html ├── camera.ts ├── README.md └── main.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | a.out -------------------------------------------------------------------------------- /types/webgpu-radix-sort.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'webgpu-radix-sort'; -------------------------------------------------------------------------------- /public/cubemap/negx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matsuoka-601/WebGPU-Ocean/HEAD/public/cubemap/negx.png -------------------------------------------------------------------------------- /public/cubemap/negy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matsuoka-601/WebGPU-Ocean/HEAD/public/cubemap/negy.png -------------------------------------------------------------------------------- /public/cubemap/negz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matsuoka-601/WebGPU-Ocean/HEAD/public/cubemap/negz.png -------------------------------------------------------------------------------- /public/cubemap/posx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matsuoka-601/WebGPU-Ocean/HEAD/public/cubemap/posx.png -------------------------------------------------------------------------------- /public/cubemap/posy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matsuoka-601/WebGPU-Ocean/HEAD/public/cubemap/posy.png -------------------------------------------------------------------------------- /public/cubemap/posz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matsuoka-601/WebGPU-Ocean/HEAD/public/cubemap/posz.png -------------------------------------------------------------------------------- /types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.wgsl' { 2 | const value: string; 3 | export default value; 4 | } 5 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import glsl from 'vite-plugin-glsl'; 3 | 4 | export default defineConfig({ 5 | plugins: [glsl()], 6 | }); -------------------------------------------------------------------------------- /public/cubemap/README.md: -------------------------------------------------------------------------------- 1 | Cubemap image : "Industrial Sunset 02 (Pure Sky) 2 | " by Jarod Guest and Sergej Majboroda, licensed under CC0. 3 | 4 | Source: https://polyhaven.com/a/industrial_sunset_02_puresky -------------------------------------------------------------------------------- /sph/grid/gridClear.wgsl: -------------------------------------------------------------------------------- 1 | @group(0) @binding(0) var cellParticleCount : array; 2 | 3 | @compute 4 | @workgroup_size(64) 5 | fn main(@builtin(global_invocation_id) id : vec3) 6 | { 7 | // TODO : ここは変えなくていいか? 8 | if (id.x < arrayLength(&cellParticleCount)) { 9 | cellParticleCount[id.x] = 0u; 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "strictPropertyInitialization": false, // for reset in camera 9 | "skipLibCheck": true, 10 | "types": ["@webgpu/types"] 11 | }, 12 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "scripts": { 4 | "build": "npx vite build", 5 | "serve": "npx vite" 6 | }, 7 | "devDependencies": { 8 | "@webgpu/types": "^0.1.51", 9 | "typescript": "^5.7.2", 10 | "wgpu-matrix": "^3.3.0", 11 | "vite": "^6.0.3", 12 | "vite-plugin-glsl": "^1.3.1", 13 | "webgpu-radix-sort": "^1.0.7" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /mls-mpm/clearGrid.wgsl: -------------------------------------------------------------------------------- 1 | struct Cell { 2 | vx: i32, 3 | vy: i32, 4 | vz: i32, 5 | mass: i32, 6 | } 7 | 8 | @group(0) @binding(0) var cells: array; 9 | 10 | @compute @workgroup_size(64) 11 | fn clearGrid(@builtin(global_invocation_id) id: vec3) { 12 | if (id.x < arrayLength(&cells)) { 13 | cells[id.x].mass = 0; 14 | cells[id.x].vx = 0; 15 | cells[id.x].vy = 0; 16 | cells[id.x].vz = 0; 17 | } 18 | } -------------------------------------------------------------------------------- /common.ts: -------------------------------------------------------------------------------- 1 | export const renderUniformsValues = new ArrayBuffer(272); 2 | export const renderUniformsViews = { 3 | texel_size: new Float32Array(renderUniformsValues, 0, 2), 4 | sphere_size: new Float32Array(renderUniformsValues, 8, 2), 5 | inv_projection_matrix: new Float32Array(renderUniformsValues, 16, 16), 6 | projection_matrix: new Float32Array(renderUniformsValues, 80, 16), 7 | view_matrix: new Float32Array(renderUniformsValues, 144, 16), 8 | inv_view_matrix: new Float32Array(renderUniformsValues, 208, 16), 9 | }; 10 | 11 | export const numParticlesMax = 200000; 12 | -------------------------------------------------------------------------------- /mls-mpm/copyPosition.wgsl: -------------------------------------------------------------------------------- 1 | struct Particle { 2 | position: vec3f, 3 | v: vec3f, 4 | C: mat3x3f, 5 | } 6 | 7 | struct PosVel { 8 | position: vec3f, 9 | v: vec3f, 10 | } 11 | 12 | @group(0) @binding(0) var particles: array; 13 | @group(0) @binding(1) var posvel: array; 14 | 15 | @compute @workgroup_size(64) 16 | fn copyPosition(@builtin(global_invocation_id) id: vec3) { 17 | if (id.x < arrayLength(&particles)) { // 変える 18 | posvel[id.x].position = particles[id.x].position; 19 | posvel[id.x].v = particles[id.x].v; 20 | } 21 | } -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build and deploy 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | build-and-deploy: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v3 16 | 17 | - name: Set up Node.js 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: '18' 21 | 22 | - name: Install dependencies 23 | run: npm install 24 | 25 | - name: Build with Vite 26 | run: npm run build 27 | 28 | - name: Deploy to deploy branch 29 | uses: JamesIves/github-pages-deploy-action@v4.3.3 30 | with: 31 | branch: deploy 32 | folder: dist 33 | -------------------------------------------------------------------------------- /render/fullScreen.wgsl: -------------------------------------------------------------------------------- 1 | struct VertexOutput { 2 | @builtin(position) position : vec4f, 3 | @location(0) uv : vec2f, 4 | @location(1) iuv : vec2f, 5 | } 6 | 7 | override screenWidth: f32; 8 | override screenHeight: f32; 9 | 10 | @vertex 11 | fn vs(@builtin(vertex_index) vertex_index : u32) -> VertexOutput { 12 | var out: VertexOutput; 13 | 14 | var pos = array( 15 | vec2( 1.0, 1.0), 16 | vec2( 1.0, -1.0), 17 | vec2(-1.0, -1.0), 18 | vec2( 1.0, 1.0), 19 | vec2(-1.0, -1.0), 20 | vec2(-1.0, 1.0), 21 | ); 22 | 23 | var uv = array( 24 | vec2(1.0, 0.0), 25 | vec2(1.0, 1.0), 26 | vec2(0.0, 1.0), 27 | vec2(1.0, 0.0), 28 | vec2(0.0, 1.0), 29 | vec2(0.0, 0.0), 30 | ); 31 | 32 | out.position = vec4(pos[vertex_index], 0.0, 1.0); 33 | out.uv = uv[vertex_index]; 34 | out.iuv = out.uv * vec2f(screenWidth, screenHeight); 35 | 36 | return out; 37 | } -------------------------------------------------------------------------------- /sph/copyPosition.wgsl: -------------------------------------------------------------------------------- 1 | struct Particle { 2 | position: vec3f, 3 | v: vec3f, 4 | force: vec3f, 5 | density: f32, 6 | nearDensity: f32, 7 | } 8 | 9 | struct PosVel { 10 | position: vec3f, 11 | v: vec3f, 12 | } 13 | 14 | struct SPHParams { 15 | mass: f32, 16 | kernelRadius: f32, 17 | kernelRadiusPow2: f32, 18 | kernelRadiusPow5: f32, 19 | kernelRadiusPow6: f32, 20 | kernelRadiusPow9: f32, 21 | dt: f32, 22 | stiffness: f32, 23 | nearStiffness: f32, 24 | restDensity: f32, 25 | viscosity: f32, 26 | n: u32 27 | } 28 | 29 | @group(0) @binding(0) var particles: array; 30 | @group(0) @binding(1) var posvel: array; 31 | @group(0) @binding(2) var env: SPHParams; 32 | 33 | @compute @workgroup_size(64) 34 | fn copyPosition(@builtin(global_invocation_id) id: vec3) { 35 | if (id.x < env.n) { 36 | posvel[id.x].position = particles[id.x].position; 37 | posvel[id.x].v = particles[id.x].v; 38 | } 39 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 matsuoka-601 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /render/gaussian.wgsl: -------------------------------------------------------------------------------- 1 | // @group(0) @binding(0) var texture_sampler: sampler; 2 | @group(0) @binding(1) var texture: texture_2d; 3 | @group(0) @binding(2) var uniforms: FilterUniforms; 4 | 5 | struct FragmentInput { 6 | @location(0) uv: vec2f, 7 | @location(1) iuv: vec2f 8 | } 9 | 10 | struct FilterUniforms { 11 | blur_dir: vec2f, // 解像度で割る 12 | } 13 | 14 | @fragment 15 | fn fs(input: FragmentInput) -> @location(0) vec4f { 16 | // thickness は unfilterable か? 17 | var thickness: f32 = textureLoad(texture, vec2u(input.iuv), 0).r; 18 | if (thickness == 0.) { 19 | return vec4f(0., 0., 0., 1.); 20 | } 21 | 22 | // var filter_size: i32 = i32(uniforms.filter_size); 23 | var filter_size: i32 = 30; // とりあえずべた書き 24 | var sigma: f32 = f32(filter_size) / 3.0; 25 | var two_sigma: f32 = 2.0 * sigma * sigma; 26 | 27 | var sum = 0.; 28 | var wsum = 0.; 29 | 30 | for (var x: i32 = -filter_size; x <= filter_size; x++) { 31 | var coords: vec2f = vec2f(f32(x)); 32 | var sampled_thickness: f32 = textureLoad(texture, vec2u(input.iuv + uniforms.blur_dir * coords), 0).r; 33 | 34 | var w: f32 = exp(-coords.x * coords.x / two_sigma); 35 | 36 | sum += sampled_thickness * w; 37 | wsum += w; 38 | } 39 | 40 | sum /= wsum; 41 | 42 | return vec4f(sum, 0., 0., 1.); 43 | } -------------------------------------------------------------------------------- /render/thicknessMap.wgsl: -------------------------------------------------------------------------------- 1 | struct RenderUniforms { 2 | texel_size: vec2f, 3 | sphere_size: f32, 4 | inv_projection_matrix: mat4x4f, 5 | projection_matrix: mat4x4f, 6 | view_matrix: mat4x4f, 7 | inv_view_matrix: mat4x4f, 8 | } 9 | 10 | struct VertexOutput { 11 | @builtin(position) position: vec4f, 12 | @location(0) uv: vec2f, 13 | } 14 | 15 | struct FragmentInput { 16 | @location(0) uv: vec2f, 17 | } 18 | 19 | struct PosVel { 20 | position: vec3f, 21 | v: vec3f, 22 | } 23 | 24 | @group(0) @binding(0) var particles: array; 25 | @group(0) @binding(1) var uniforms: RenderUniforms; 26 | 27 | @vertex 28 | fn vs( 29 | @builtin(vertex_index) vertex_index: u32, 30 | @builtin(instance_index) instance_index: u32 31 | ) -> VertexOutput { 32 | var corner_positions = array( 33 | vec2( 0.5, 0.5), 34 | vec2( 0.5, -0.5), 35 | vec2(-0.5, -0.5), 36 | vec2( 0.5, 0.5), 37 | vec2(-0.5, -0.5), 38 | vec2(-0.5, 0.5), 39 | ); 40 | 41 | let corner = vec3(corner_positions[vertex_index] * uniforms.sphere_size, 0.0); 42 | let uv = corner_positions[vertex_index] + 0.5; 43 | 44 | let real_position = particles[instance_index].position; 45 | let view_position = (uniforms.view_matrix * vec4f(real_position, 1.0)).xyz; 46 | 47 | let out_position = uniforms.projection_matrix * vec4f(view_position + corner, 1.0); 48 | 49 | return VertexOutput(out_position, uv); 50 | } 51 | 52 | @fragment 53 | fn fs(input: FragmentInput) -> @location(0) vec4f { 54 | var normalxy: vec2f = input.uv * 2.0 - 1.0; 55 | var r2: f32 = dot(normalxy, normalxy); 56 | if (r2 > 1.0) { 57 | discard; 58 | } 59 | var thickness: f32 = sqrt(1.0 - r2); 60 | let particle_alpha = 0.05; 61 | 62 | return vec4f(vec3f(particle_alpha * thickness), 1.0); 63 | } -------------------------------------------------------------------------------- /sph/grid/gridBuild.wgsl: -------------------------------------------------------------------------------- 1 | struct Particle { 2 | position: vec3f, 3 | v: vec3f, 4 | force: vec3f, 5 | density: f32, 6 | nearDensity: f32, 7 | } 8 | 9 | struct Environment { 10 | xGrids: i32, 11 | yGrids: i32, 12 | zGrids: i32, 13 | cellSize: f32, 14 | xHalf: f32, 15 | yHalf: f32, 16 | zHalf: f32, 17 | offset: f32, 18 | } 19 | 20 | struct SPHParams { 21 | mass: f32, 22 | kernelRadius: f32, 23 | kernelRadiusPow2: f32, 24 | kernelRadiusPow5: f32, 25 | kernelRadiusPow6: f32, 26 | kernelRadiusPow9: f32, 27 | dt: f32, 28 | stiffness: f32, 29 | nearStiffness: f32, 30 | restDensity: f32, 31 | viscosity: f32, 32 | n: u32 33 | } 34 | 35 | @group(0) @binding(0) var cellParticleCount : array>; 36 | @group(0) @binding(1) var particleCellOffset : array; 37 | @group(0) @binding(2) var particles: array; 38 | @group(0) @binding(3) var env: Environment; 39 | @group(0) @binding(4) var params: SPHParams; 40 | 41 | fn cellId(position: vec3f) -> i32 { 42 | let xi: i32 = i32(floor((position.x + env.xHalf + env.offset) / env.cellSize)); 43 | let yi: i32 = i32(floor((position.y + env.yHalf + env.offset) / env.cellSize)); 44 | let zi: i32 = i32(floor((position.z + env.zHalf + env.offset) / env.cellSize)); 45 | 46 | return xi + yi * env.xGrids + zi * env.xGrids * env.yGrids; 47 | } 48 | 49 | @compute 50 | @workgroup_size(64) 51 | fn main(@builtin(global_invocation_id) id : vec3) 52 | { 53 | if (id.x < params.n) 54 | { 55 | let cellID: i32 = cellId(particles[id.x].position); 56 | // TODO : 変える 57 | if (cellID < env.xGrids * env.yGrids * env.zGrids) { 58 | particleCellOffset[id.x] = atomicAdd(&cellParticleCount[cellID], 1u); 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /mls-mpm/updateGrid.wgsl: -------------------------------------------------------------------------------- 1 | struct Cell { 2 | vx: i32, 3 | vy: i32, 4 | vz: i32, 5 | mass: i32, 6 | } 7 | 8 | override fixed_point_multiplier: f32; 9 | override dt: f32; 10 | 11 | @group(0) @binding(0) var cells: array; 12 | @group(0) @binding(1) var real_box_size: vec3f; 13 | @group(0) @binding(2) var init_box_size: vec3f; 14 | 15 | fn encodeFixedPoint(floating_point: f32) -> i32 { 16 | return i32(floating_point * fixed_point_multiplier); 17 | } 18 | fn decodeFixedPoint(fixed_point: i32) -> f32 { 19 | return f32(fixed_point) / fixed_point_multiplier; 20 | } 21 | 22 | 23 | @compute @workgroup_size(64) 24 | fn updateGrid(@builtin(global_invocation_id) id: vec3) { 25 | if (id.x < arrayLength(&cells)) { 26 | if (cells[id.x].mass > 0) { // 0 との比較は普通にしてよい 27 | var float_v: vec3f = vec3f( 28 | decodeFixedPoint(cells[id.x].vx), 29 | decodeFixedPoint(cells[id.x].vy), 30 | decodeFixedPoint(cells[id.x].vz) 31 | ); 32 | float_v /= decodeFixedPoint(cells[id.x].mass); 33 | cells[id.x].vx = encodeFixedPoint(float_v.x); 34 | cells[id.x].vy = encodeFixedPoint(float_v.y + -0.3 * dt); 35 | cells[id.x].vz = encodeFixedPoint(float_v.z); 36 | 37 | var x: i32 = i32(id.x) / i32(init_box_size.z) / i32(init_box_size.y); 38 | var y: i32 = (i32(id.x) / i32(init_box_size.z)) % i32(init_box_size.y); 39 | var z: i32 = i32(id.x) % i32(init_box_size.z); 40 | // 整数を ceil したら,その整数に一致するかは確認する必要があり 41 | if (x < 2 || x > i32(ceil(real_box_size.x) - 3)) { cells[id.x].vx = 0; } 42 | if (y < 2 || y > i32(ceil(real_box_size.y) - 3)) { cells[id.x].vy = 0; } 43 | if (z < 2 || z > i32(ceil(real_box_size.z) - 3)) { cells[id.x].vz = 0; } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /render/thickness.wgsl: -------------------------------------------------------------------------------- 1 | struct RenderUniforms { 2 | texel_size: vec2f, 3 | sphere_size: f32, 4 | inv_projection_matrix: mat4x4f, 5 | projection_matrix: mat4x4f, 6 | view_matrix: mat4x4f, 7 | inv_view_matrix: mat4x4f, 8 | } 9 | 10 | struct VertexOutput { 11 | @builtin(position) position: vec4f, 12 | @location(0) uv: vec2f, 13 | } 14 | 15 | struct FragmentInput { 16 | @location(0) uv: vec2f, 17 | } 18 | 19 | struct PosVel { 20 | position: vec3f, 21 | v: vec3f, 22 | } 23 | 24 | @group(0) @binding(0) var particles: array; 25 | @group(0) @binding(1) var uniforms: RenderUniforms; 26 | 27 | @vertex 28 | fn vs( 29 | @builtin(vertex_index) vertex_index: u32, 30 | @builtin(instance_index) instance_index: u32 31 | ) -> VertexOutput { 32 | var corner_positions = array( 33 | vec2( 0.5, 0.5), 34 | vec2( 0.5, -0.5), 35 | vec2(-0.5, -0.5), 36 | vec2( 0.5, 0.5), 37 | vec2(-0.5, -0.5), 38 | vec2(-0.5, 0.5), 39 | ); 40 | 41 | 42 | // let speed = sqrt(dot(particles[instance_index].velocity, particles[instance_index].velocity)); 43 | // let sz = max(0., uniforms.size - 0.00 * speed); 44 | let corner = vec3(corner_positions[vertex_index] * uniforms.sphere_size, 0.0); 45 | let uv = corner_positions[vertex_index] + 0.5; 46 | 47 | let real_position = particles[instance_index].position; 48 | let view_position = (uniforms.view_matrix * vec4f(real_position, 1.0)).xyz; 49 | 50 | let out_position = uniforms.projection_matrix * vec4f(view_position + corner, 1.0); 51 | 52 | return VertexOutput(out_position, uv); 53 | } 54 | 55 | @fragment 56 | fn fs(input: FragmentInput) -> @location(0) vec4f { 57 | var normalxy: vec2f = input.uv * 2.0 - 1.0; 58 | var r2: f32 = dot(normalxy, normalxy); 59 | if (r2 > 1.0) { 60 | discard; 61 | } 62 | var thickness: f32 = sqrt(1.0 - r2); 63 | let particle_alpha = 0.05; 64 | 65 | return vec4f(vec3f(particle_alpha * thickness), 1.0); 66 | } -------------------------------------------------------------------------------- /sph/grid/reorderParticles.wgsl: -------------------------------------------------------------------------------- 1 | struct Particle { 2 | position: vec3f, 3 | v: vec3f, 4 | force: vec3f, 5 | density: f32, 6 | nearDensity: f32, 7 | } 8 | 9 | struct SPHParams { 10 | mass: f32, 11 | kernelRadius: f32, 12 | kernelRadiusPow2: f32, 13 | kernelRadiusPow5: f32, 14 | kernelRadiusPow6: f32, 15 | kernelRadiusPow9: f32, 16 | dt: f32, 17 | stiffness: f32, 18 | nearStiffness: f32, 19 | restDensity: f32, 20 | viscosity: f32, 21 | n: u32 22 | } 23 | 24 | @group(0) @binding(0) var sourceParticles: array; 25 | @group(0) @binding(1) var targetParticles: array; 26 | @group(0) @binding(2) var cellParticleCount : array; 27 | @group(0) @binding(3) var particleCellOffset : array; 28 | @group(0) @binding(4) var env : Environment; 29 | @group(0) @binding(5) var params : SPHParams; 30 | 31 | struct Environment { 32 | xGrids: i32, 33 | yGrids: i32, 34 | zGrids: i32, 35 | cellSize: f32, 36 | xHalf: f32, 37 | yHalf: f32, 38 | zHalf: f32, 39 | offset: f32, 40 | } 41 | 42 | fn cellId(position: vec3f) -> i32 { 43 | let xi: i32 = i32(floor((position.x + env.xHalf + env.offset) / env.cellSize)); 44 | let yi: i32 = i32(floor((position.y + env.yHalf + env.offset) / env.cellSize)); 45 | let zi: i32 = i32(floor((position.z + env.zHalf + env.offset) / env.cellSize)); 46 | 47 | return xi + yi * env.xGrids + zi * env.xGrids * env.yGrids; 48 | } 49 | 50 | @compute 51 | @workgroup_size(64) 52 | fn main(@builtin(global_invocation_id) id : vec3) { 53 | if (id.x < params.n) { 54 | let cellId: i32 = cellId(sourceParticles[id.x].position); 55 | // TODO : 変える 56 | if (cellId < env.xGrids * env.yGrids * env.zGrids) { 57 | let targetIndex = cellParticleCount[cellId + 1] - particleCellOffset[id.x] - 1; 58 | if (targetIndex < params.n) { 59 | targetParticles[targetIndex] = sourceParticles[id.x]; 60 | } 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /sph/integrate.wgsl: -------------------------------------------------------------------------------- 1 | struct Particle { 2 | position: vec3f, 3 | v: vec3f, 4 | force: vec3f, 5 | density: f32, 6 | nearDensity: f32, 7 | } 8 | 9 | struct RealBoxSize { 10 | xHalf: f32, 11 | yHalf: f32, 12 | zHalf: f32, 13 | } 14 | 15 | struct SPHParams { 16 | mass: f32, 17 | kernelRadius: f32, 18 | kernelRadiusPow2: f32, 19 | kernelRadiusPow5: f32, 20 | kernelRadiusPow6: f32, 21 | kernelRadiusPow9: f32, 22 | dt: f32, 23 | stiffness: f32, 24 | nearStiffness: f32, 25 | restDensity: f32, 26 | viscosity: f32, 27 | n: u32 28 | } 29 | 30 | @group(0) @binding(0) var particles: array; 31 | @group(0) @binding(1) var realBoxSize: RealBoxSize; 32 | @group(0) @binding(2) var params: SPHParams; 33 | 34 | @compute @workgroup_size(64) 35 | fn integrate(@builtin(global_invocation_id) id: vec3) { 36 | if (id.x < params.n) { 37 | // avoid zero division 38 | if (particles[id.x].density != 0.) { 39 | var a = particles[id.x].force / particles[id.x].density; 40 | 41 | let xPlusDist = realBoxSize.xHalf - particles[id.x].position.x; 42 | let xMinusDist = realBoxSize.xHalf + particles[id.x].position.x; 43 | let yPlusDist = realBoxSize.yHalf - particles[id.x].position.y; 44 | let yMinusDist = realBoxSize.yHalf + particles[id.x].position.y; 45 | let zPlusDist = realBoxSize.zHalf - particles[id.x].position.z; 46 | let zMinusDist = realBoxSize.zHalf + particles[id.x].position.z; 47 | 48 | let wallStiffness = 8000.; 49 | 50 | let xPlusForce = vec3f(1., 0., 0.) * wallStiffness * min(xPlusDist, 0.); 51 | let xMinusForce = vec3f(-1., 0., 0.) * wallStiffness * min(xMinusDist, 0.); 52 | let yPlusForce = vec3f(0., 1., 0.) * wallStiffness * min(yPlusDist, 0.); 53 | let yMinusForce = vec3f(0., -1., 0.) * wallStiffness * min(yMinusDist, 0.); 54 | let zPlusForce = vec3f(0., 0., 1.) * wallStiffness * min(zPlusDist, 0.); 55 | let zMinusForce = vec3f(0., 0., -1.) * wallStiffness * min(zMinusDist, 0.); 56 | 57 | let xForce = xPlusForce + xMinusForce; 58 | let yForce = yPlusForce + yMinusForce; 59 | let zForce = zPlusForce + zMinusForce; 60 | 61 | a += xForce + yForce + zForce; 62 | particles[id.x].v += params.dt * a; 63 | particles[id.x].position += params.dt * particles[id.x].v; 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /render/depthMap.wgsl: -------------------------------------------------------------------------------- 1 | struct VertexOutput { 2 | @builtin(position) position: vec4f, 3 | @location(0) uv: vec2f, 4 | @location(1) view_position: vec3f, 5 | } 6 | 7 | struct FragmentInput { 8 | @location(0) uv: vec2f, 9 | @location(1) view_position: vec3f, 10 | } 11 | 12 | struct FragmentOutput { 13 | @location(0) frag_color: vec4f, 14 | @builtin(frag_depth) frag_depth: f32, 15 | } 16 | 17 | struct RenderUniforms { 18 | texel_size: vec2f, 19 | sphere_size: f32, 20 | inv_projection_matrix: mat4x4f, 21 | projection_matrix: mat4x4f, 22 | view_matrix: mat4x4f, 23 | inv_view_matrix: mat4x4f, 24 | } 25 | 26 | struct PosVel { 27 | position: vec3f, 28 | v: vec3f, 29 | } 30 | 31 | @group(0) @binding(0) var particles: array; 32 | @group(0) @binding(1) var uniforms: RenderUniforms; 33 | 34 | @vertex 35 | fn vs( 36 | @builtin(vertex_index) vertex_index: u32, 37 | @builtin(instance_index) instance_index: u32 38 | ) -> VertexOutput { 39 | var corner_positions = array( 40 | vec2( 0.5, 0.5), 41 | vec2( 0.5, -0.5), 42 | vec2(-0.5, -0.5), 43 | vec2( 0.5, 0.5), 44 | vec2(-0.5, -0.5), 45 | vec2(-0.5, 0.5), 46 | ); 47 | 48 | // let speed = sqrt(dot(particles[instance_index].velocity, particles[instance_index].velocity)); 49 | // let sz = max(0., uniforms.size - 0.00 * speed); 50 | let corner = vec3(corner_positions[vertex_index] * uniforms.sphere_size, 0.0); 51 | let uv = corner_positions[vertex_index] + 0.5; 52 | 53 | let real_position = particles[instance_index].position; 54 | let view_position = (uniforms.view_matrix * vec4f(real_position, 1.0)).xyz; 55 | 56 | let out_position = uniforms.projection_matrix * vec4f(view_position + corner, 1.0); 57 | 58 | return VertexOutput(out_position, uv, view_position); 59 | } 60 | 61 | @fragment 62 | fn fs(input: FragmentInput) -> FragmentOutput { 63 | var out: FragmentOutput; 64 | 65 | var normalxy: vec2f = input.uv * 2.0 - 1.0; 66 | var r2: f32 = dot(normalxy, normalxy); 67 | if (r2 > 1.0) { 68 | discard; 69 | } 70 | var normalz = sqrt(1.0 - r2); 71 | var normal = vec3(normalxy, normalz); 72 | 73 | var radius = uniforms.sphere_size / 2; 74 | var real_view_pos: vec4f = vec4f(input.view_position + normal * radius, 1.0); 75 | var clip_space_pos: vec4f = uniforms.projection_matrix * real_view_pos; 76 | out.frag_depth = clip_space_pos.z / clip_space_pos.w; 77 | 78 | out.frag_color = vec4(real_view_pos.z, 0., 0., 1.); 79 | return out; 80 | } -------------------------------------------------------------------------------- /mls-mpm/p2g_1.wgsl: -------------------------------------------------------------------------------- 1 | struct Particle { 2 | position: vec3f, 3 | v: vec3f, 4 | C: mat3x3f, 5 | } 6 | struct Cell { 7 | vx: atomic, 8 | vy: atomic, 9 | vz: atomic, 10 | mass: atomic, 11 | } 12 | 13 | override fixed_point_multiplier: f32; 14 | 15 | fn encodeFixedPoint(floating_point: f32) -> i32 { 16 | return i32(floating_point * fixed_point_multiplier); 17 | } 18 | 19 | 20 | @group(0) @binding(0) var particles: array; 21 | @group(0) @binding(1) var cells: array; 22 | @group(0) @binding(2) var init_box_size: vec3f; 23 | 24 | @compute @workgroup_size(64) 25 | fn p2g_1(@builtin(global_invocation_id) id: vec3) { 26 | if (id.x < arrayLength(&particles)) { 27 | var weights: array; 28 | 29 | let particle = particles[id.x]; 30 | let cell_idx: vec3f = floor(particle.position); 31 | let cell_diff: vec3f = particle.position - (cell_idx + 0.5f); 32 | weights[0] = 0.5f * (0.5f - cell_diff) * (0.5f - cell_diff); 33 | weights[1] = 0.75f - cell_diff * cell_diff; 34 | weights[2] = 0.5f * (0.5f + cell_diff) * (0.5f + cell_diff); 35 | 36 | let C: mat3x3f = particle.C; 37 | 38 | for (var gx = 0; gx < 3; gx++) { 39 | for (var gy = 0; gy < 3; gy++) { 40 | for (var gz = 0; gz < 3; gz++) { 41 | let weight: f32 = weights[gx].x * weights[gy].y * weights[gz].z; 42 | let cell_x: vec3f = vec3f( 43 | cell_idx.x + f32(gx) - 1., 44 | cell_idx.y + f32(gy) - 1., 45 | cell_idx.z + f32(gz) - 1. 46 | ); 47 | let cell_dist = (cell_x + 0.5f) - particle.position; 48 | 49 | let Q: vec3f = C * cell_dist; 50 | 51 | let mass_contrib: f32 = weight * 1.0; // assuming particle.mass = 1.0 52 | let vel_contrib: vec3f = mass_contrib * (particle.v + Q); 53 | let cell_index: i32 = 54 | i32(cell_x.x) * i32(init_box_size.y) * i32(init_box_size.z) + 55 | i32(cell_x.y) * i32(init_box_size.z) + 56 | i32(cell_x.z); 57 | atomicAdd(&cells[cell_index].mass, encodeFixedPoint(mass_contrib)); 58 | atomicAdd(&cells[cell_index].vx, encodeFixedPoint(vel_contrib.x)); 59 | atomicAdd(&cells[cell_index].vy, encodeFixedPoint(vel_contrib.y)); 60 | atomicAdd(&cells[cell_index].vz, encodeFixedPoint(vel_contrib.z)); 61 | } 62 | } 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /render/bilateral.wgsl: -------------------------------------------------------------------------------- 1 | // @group(0) @binding(0) var texture_sampler: sampler; 2 | @group(0) @binding(1) var texture: texture_2d; 3 | @group(0) @binding(2) var uniforms: FilterUniforms; 4 | 5 | struct FragmentInput { 6 | @location(0) uv: vec2f, 7 | @location(1) iuv: vec2f 8 | } 9 | 10 | override depth_threshold: f32; // これは何? 11 | override projected_particle_constant: f32; // これは Babylon.js で計算していたやつか. 12 | override max_filter_size: f32; 13 | struct FilterUniforms { 14 | blur_dir: vec2f, // 解像度で割る 15 | } 16 | 17 | 18 | @fragment 19 | fn fs(input: FragmentInput) -> @location(0) vec4f { 20 | // 正かどうかを確かめる 21 | var depth: f32 = abs(textureLoad(texture, vec2u(input.iuv), 0).r); 22 | 23 | // ここが有効になるためには,背景の depth を適切に設定しなきゃいけないな. 24 | if (depth >= 1e4 || depth <= 0.) { 25 | return vec4f(vec3f(depth), 1.); 26 | } 27 | 28 | // depth は正か? 29 | var filter_size: i32 = min(i32(max_filter_size), i32(ceil(projected_particle_constant / depth))); 30 | 31 | // ここのパラメータ設定がよくわからない 32 | var sigma: f32 = f32(filter_size) / 3.0; 33 | var two_sigma: f32 = 2.0 * sigma * sigma; 34 | var sigma_depth: f32 = depth_threshold / 3.0; 35 | var two_sigma_depth: f32 = 2.0 * sigma_depth * sigma_depth; 36 | 37 | var sum: f32 = 0.0; 38 | var wsum: f32 = 0.0; 39 | for (var x: i32 = -filter_size; x <= filter_size; x++) { 40 | var coords: vec2f = vec2f(f32(x)); 41 | var sampled_depth: f32 = abs(textureLoad(texture, vec2u(input.iuv + coords * uniforms.blur_dir), 0).r); 42 | // sampled_depth = (depth + sampled_depth) / 2; 43 | 44 | var rr: f32 = dot(coords, coords); 45 | var w: f32 = exp(-rr / two_sigma); 46 | 47 | var r_depth: f32 = sampled_depth - depth; 48 | var wd: f32 = exp(-r_depth * r_depth / two_sigma_depth); 49 | sum += sampled_depth * w * wd; 50 | wsum += w * wd; 51 | } 52 | 53 | // for (var x: i32 = -filter_size; x <= filter_size; x++) { 54 | // for (var y: i32 = -filter_size; y <= filter_size; y++) { 55 | // var coords: vec2f = vec2f(f32(x), f32(y)); 56 | // var sampled_depth: f32 = abs(textureLoad(texture, vec2u(input.iuv + coords * vec2f(1.0, 1.0)), 0).r); 57 | 58 | // var rr: f32 = dot(coords, coords); 59 | // var w: f32 = exp(-rr / two_sigma); 60 | 61 | // var r_depth: f32 = sampled_depth - depth; 62 | // var wd: f32 = exp(-r_depth * r_depth / two_sigma_depth); 63 | 64 | // sum += sampled_depth * w * wd; 65 | // wsum += w * wd; 66 | // } 67 | // } 68 | 69 | sum /= wsum; 70 | // if (wsum > 0.) { 71 | // sum /= wsum; 72 | // } 73 | 74 | return vec4f(sum, 0., 0., 1.); 75 | } 76 | -------------------------------------------------------------------------------- /render/sphere.wgsl: -------------------------------------------------------------------------------- 1 | struct VertexOutput { 2 | @builtin(position) position: vec4f, 3 | @location(0) uv: vec2f, 4 | @location(1) view_position: vec3f, 5 | @location(2) speed: f32, 6 | } 7 | 8 | struct FragmentInput { 9 | @location(0) uv: vec2f, 10 | @location(1) view_position: vec3f, 11 | @location(2) speed: f32, 12 | } 13 | 14 | struct FragmentOutput { 15 | @location(0) frag_color: vec4f, 16 | @builtin(frag_depth) frag_depth: f32, 17 | } 18 | 19 | struct RenderUniforms { 20 | texel_size: vec2f, 21 | sphere_size: f32, 22 | inv_projection_matrix: mat4x4f, 23 | projection_matrix: mat4x4f, 24 | view_matrix: mat4x4f, 25 | inv_view_matrix: mat4x4f, 26 | } 27 | 28 | struct PosVel { 29 | position: vec3f, 30 | v: vec3f, 31 | } 32 | 33 | @group(0) @binding(0) var particles: array; 34 | @group(0) @binding(1) var uniforms: RenderUniforms; 35 | 36 | @vertex 37 | fn vs( 38 | @builtin(vertex_index) vertex_index: u32, 39 | @builtin(instance_index) instance_index: u32 40 | ) -> VertexOutput { 41 | var corner_positions = array( 42 | vec2( 0.5, 0.5), 43 | vec2( 0.5, -0.5), 44 | vec2(-0.5, -0.5), 45 | vec2( 0.5, 0.5), 46 | vec2(-0.5, -0.5), 47 | vec2(-0.5, 0.5), 48 | ); 49 | 50 | let corner = vec3(corner_positions[vertex_index] * uniforms.sphere_size, 0.0); 51 | let uv = corner_positions[vertex_index] + 0.5; 52 | 53 | let real_position = particles[instance_index].position; 54 | let view_position = (uniforms.view_matrix * vec4f(real_position, 1.0)).xyz; 55 | 56 | let out_position = uniforms.projection_matrix * vec4f(view_position + corner, 1.0); 57 | 58 | let speed = sqrt(dot(particles[instance_index].v, particles[instance_index].v)); 59 | 60 | return VertexOutput(out_position, uv, view_position, speed); 61 | } 62 | 63 | fn value_to_color(value: f32) -> vec3 { 64 | // let col0 = vec3f(29, 71, 158) / 256; 65 | let col0 = vec3f(0, 0.4, 0.8); 66 | let col1 = vec3f(35, 161, 165) / 256; 67 | let col2 = vec3f(95, 254, 150) / 256; 68 | let col3 = vec3f(243, 250, 49) / 256; 69 | let col4 = vec3f(255, 165, 0) / 256; 70 | 71 | 72 | if (0 <= value && value < 0.25) { 73 | let t = value / 0.25; 74 | return mix(col0, col1, t); 75 | } else if (0.25 <= value && value < 0.50) { 76 | let t = (value - 0.25) / 0.25; 77 | return mix(col1, col2, t); 78 | } else if (0.50 <= value && value < 0.75) { 79 | let t = (value - 0.50) / 0.25; 80 | return mix(col2, col3, t); 81 | } else { 82 | let t = (value - 0.75) / 0.25; 83 | return mix(col3, col4, t); 84 | } 85 | 86 | } 87 | 88 | @fragment 89 | fn fs(input: FragmentInput) -> FragmentOutput { 90 | var out: FragmentOutput; 91 | 92 | var normalxy: vec2f = input.uv * 2.0 - 1.0; 93 | var r2: f32 = dot(normalxy, normalxy); 94 | if (r2 > 1.0) { 95 | discard; 96 | } 97 | var normalz = sqrt(1.0 - r2); 98 | var normal = vec3(normalxy, normalz); 99 | 100 | var radius = uniforms.sphere_size / 2; 101 | var real_view_pos: vec4f = vec4f(input.view_position + normal * radius, 1.0); 102 | var clip_space_pos: vec4f = uniforms.projection_matrix * real_view_pos; 103 | out.frag_depth = clip_space_pos.z / clip_space_pos.w; 104 | 105 | var diffuse: f32 = max(0.0, dot(normal, normalize(vec3(1.0, 1.0, 1.0)))); 106 | var color: vec3f = value_to_color(input.speed / 1.5); 107 | 108 | out.frag_color = vec4(color * diffuse, 1.); 109 | return out; 110 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | WebGPU-Ocean 7 | 67 | 68 | 69 | 70 |
71 |
72 | 76 |
77 |
78 | 79 | 80 | 81 |
82 |
83 |
84 | Simulation Mode 85 |
86 |
87 |
88 |
89 | Number of Particles 90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
Drag to move camera
99 |
Scroll to zoom in and out
100 |
101 |
102 | loading now...(won't take longer than 10 seconds I guess) 103 |
104 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /mls-mpm/p2g_2.wgsl: -------------------------------------------------------------------------------- 1 | struct Particle { 2 | position: vec3f, 3 | v: vec3f, 4 | C: mat3x3f, 5 | } 6 | struct Cell { 7 | vx: atomic, 8 | vy: atomic, 9 | vz: atomic, 10 | mass: i32, 11 | } 12 | 13 | override fixed_point_multiplier: f32; 14 | override stiffness: f32; 15 | override rest_density: f32; 16 | override dynamic_viscosity: f32; 17 | override dt: f32; 18 | 19 | fn encodeFixedPoint(floating_point: f32) -> i32 { 20 | return i32(floating_point * fixed_point_multiplier); 21 | } 22 | fn decodeFixedPoint(fixed_point: i32) -> f32 { 23 | return f32(fixed_point) / fixed_point_multiplier; 24 | } 25 | 26 | @group(0) @binding(0) var particles: array; 27 | @group(0) @binding(1) var cells: array; 28 | @group(0) @binding(2) var init_box_size: vec3f; 29 | 30 | @compute @workgroup_size(64) 31 | fn p2g_2(@builtin(global_invocation_id) id: vec3) { 32 | if (id.x < arrayLength(&particles)) { 33 | var weights: array; 34 | 35 | let particle = particles[id.x]; 36 | let cell_idx: vec3f = floor(particle.position); 37 | let cell_diff: vec3f = particle.position - (cell_idx + 0.5f); 38 | weights[0] = 0.5f * (0.5f - cell_diff) * (0.5f - cell_diff); 39 | weights[1] = 0.75f - cell_diff * cell_diff; 40 | weights[2] = 0.5f * (0.5f + cell_diff) * (0.5f + cell_diff); 41 | 42 | var density: f32 = 0.; 43 | for (var gx = 0; gx < 3; gx++) { 44 | for (var gy = 0; gy < 3; gy++) { 45 | for (var gz = 0; gz < 3; gz++) { 46 | let weight: f32 = weights[gx].x * weights[gy].y * weights[gz].z; 47 | let cell_x: vec3f = vec3f( 48 | cell_idx.x + f32(gx) - 1., 49 | cell_idx.y + f32(gy) - 1., 50 | cell_idx.z + f32(gz) - 1. 51 | ); 52 | let cell_index: i32 = 53 | i32(cell_x.x) * i32(init_box_size.y) * i32(init_box_size.z) + 54 | i32(cell_x.y) * i32(init_box_size.z) + 55 | i32(cell_x.z); 56 | density += decodeFixedPoint(cells[cell_index].mass) * weight; 57 | } 58 | } 59 | } 60 | 61 | let volume: f32 = 1.0 / density; // particle.mass = 1.0; 62 | 63 | let pressure: f32 = max(-0.0, stiffness * (pow(density / rest_density, 5.) - 1)); 64 | 65 | var stress: mat3x3f = mat3x3f(-pressure, 0, 0, 0, -pressure, 0, 0, 0, -pressure); 66 | let dudv: mat3x3f = particle.C; 67 | let strain: mat3x3f = dudv + transpose(dudv); 68 | stress += dynamic_viscosity * strain; 69 | 70 | let eq_16_term0 = -volume * 4 * stress * dt; 71 | 72 | for (var gx = 0; gx < 3; gx++) { 73 | for (var gy = 0; gy < 3; gy++) { 74 | for (var gz = 0; gz < 3; gz++) { 75 | let weight: f32 = weights[gx].x * weights[gy].y * weights[gz].z; 76 | let cell_x: vec3f = vec3f( 77 | cell_idx.x + f32(gx) - 1., 78 | cell_idx.y + f32(gy) - 1., 79 | cell_idx.z + f32(gz) - 1. 80 | ); 81 | let cell_dist = (cell_x + 0.5f) - particle.position; 82 | let cell_index: i32 = 83 | i32(cell_x.x) * i32(init_box_size.y) * i32(init_box_size.z) + 84 | i32(cell_x.y) * i32(init_box_size.z) + 85 | i32(cell_x.z); 86 | let momentum: vec3f = eq_16_term0 * weight * cell_dist; 87 | atomicAdd(&cells[cell_index].vx, encodeFixedPoint(momentum.x)); 88 | atomicAdd(&cells[cell_index].vy, encodeFixedPoint(momentum.y)); 89 | atomicAdd(&cells[cell_index].vz, encodeFixedPoint(momentum.z)); 90 | } 91 | } 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /camera.ts: -------------------------------------------------------------------------------- 1 | import { mat4 } from 'wgpu-matrix' 2 | import { renderUniformsValues, renderUniformsViews } from './common' 3 | 4 | export class Camera { 5 | isDragging: boolean 6 | prevX: number 7 | prevY: number 8 | currentXtheta: number 9 | currentYtheta: number 10 | maxYTheta: number 11 | minYTheta: number 12 | sensitivity: number 13 | currentDistance: number 14 | maxDistance: number 15 | minDistance: number 16 | target: number[] 17 | fov: number 18 | zoomRate: number 19 | 20 | constructor (canvasElement: HTMLCanvasElement) { 21 | canvasElement.addEventListener("mousedown", (event: MouseEvent) => { 22 | this.isDragging = true; 23 | this.prevX = event.clientX; 24 | this.prevY = event.clientY; 25 | }); 26 | 27 | canvasElement.addEventListener("wheel", (event: WheelEvent) => { 28 | event.preventDefault(); 29 | var scrollDelta = event.deltaY; 30 | this.currentDistance += ((scrollDelta > 0) ? 1 : -1) * this.zoomRate; 31 | if (this.currentDistance < this.minDistance) this.currentDistance = this.minDistance; 32 | if (this.currentDistance > this.maxDistance) this.currentDistance = this.maxDistance; 33 | this.recalculateView() 34 | }) 35 | 36 | canvasElement.addEventListener("mousemove", (event: MouseEvent) => { 37 | if (this.isDragging) { 38 | const currentX = event.clientX; 39 | const currentY = event.clientY; 40 | const deltaX = this.prevX - currentX; 41 | const deltaY = this.prevY - currentY; 42 | this.currentXtheta += this.sensitivity * deltaX; 43 | this.currentYtheta += this.sensitivity * deltaY; 44 | if (this.currentYtheta > this.maxYTheta) this.currentYtheta = this.maxYTheta 45 | if (this.currentYtheta < this.minYTheta) this.currentYtheta = this.minYTheta 46 | this.prevX = currentX; 47 | this.prevY = currentY; 48 | this.recalculateView() 49 | } 50 | }); 51 | 52 | canvasElement.addEventListener("mouseup", () => { 53 | if (this.isDragging) this.isDragging = false; 54 | }); 55 | } 56 | 57 | reset(canvasElement: HTMLCanvasElement, initDistance: number, target: number[], fov: number, zoomRate: number) { 58 | this.isDragging = false 59 | this.prevX = 0 60 | this.prevY = 0 61 | this.currentXtheta = Math.PI / 4 * 1 62 | this.currentYtheta = -Math.PI / 12 63 | this.maxYTheta = 0 64 | this.minYTheta = -0.99 * Math.PI / 2. 65 | this.sensitivity = 0.005 66 | this.currentDistance = initDistance 67 | this.maxDistance = 2. * this.currentDistance 68 | this.minDistance = 0.3 * this.currentDistance 69 | this.target = target 70 | this.fov = fov 71 | this.zoomRate = zoomRate 72 | 73 | const aspect = canvasElement.clientWidth / canvasElement.clientHeight 74 | const projection = mat4.perspective(fov, aspect, 0.1, 500) // TODO : ここの max を変える 75 | renderUniformsViews.projection_matrix.set(projection) 76 | renderUniformsViews.inv_projection_matrix.set(mat4.inverse(projection)) 77 | this.recalculateView() 78 | } 79 | 80 | recalculateView() { 81 | var mat = mat4.identity(); 82 | mat4.translate(mat, this.target, mat) 83 | mat4.rotateY(mat, this.currentXtheta, mat) 84 | mat4.rotateX(mat, this.currentYtheta, mat) 85 | mat4.translate(mat, [0, 0, this.currentDistance], mat) 86 | var position = mat4.multiply(mat, [0, 0, 0, 1]) 87 | 88 | const view = mat4.lookAt( 89 | [position[0], position[1], position[2]], // position 90 | this.target, // target 91 | [0, 1, 0], // up 92 | ) 93 | 94 | renderUniformsViews.view_matrix.set(view) 95 | renderUniformsViews.inv_view_matrix.set(mat4.inverse(view)) 96 | } 97 | } -------------------------------------------------------------------------------- /render/fluid.wgsl: -------------------------------------------------------------------------------- 1 | @group(0) @binding(0) var texture_sampler: sampler; 2 | @group(0) @binding(1) var texture: texture_2d; 3 | @group(0) @binding(2) var uniforms: RenderUniforms; 4 | @group(0) @binding(3) var thickness_texture: texture_2d; 5 | @group(0) @binding(4) var envmap_texture: texture_cube; 6 | 7 | struct RenderUniforms { 8 | texel_size: vec2f, 9 | sphere_size: f32, 10 | inv_projection_matrix: mat4x4f, 11 | projection_matrix: mat4x4f, 12 | view_matrix: mat4x4f, 13 | inv_view_matrix: mat4x4f, 14 | } 15 | 16 | struct FragmentInput { 17 | @location(0) uv: vec2f, 18 | @location(1) iuv: vec2f, 19 | } 20 | 21 | fn computeViewPosFromUVDepth(tex_coord: vec2f, depth: f32) -> vec3f { 22 | var ndc: vec4f = vec4f(tex_coord.x * 2.0 - 1.0, 1.0 - 2.0 * tex_coord.y, 0.0, 1.0); 23 | // なんかこれで合う 24 | ndc.z = -uniforms.projection_matrix[2].z + uniforms.projection_matrix[3].z / depth; 25 | ndc.w = 1.0; 26 | 27 | var eye_pos: vec4f = uniforms.inv_projection_matrix * ndc; 28 | 29 | return eye_pos.xyz / eye_pos.w; 30 | } 31 | 32 | fn getViewPosFromTexCoord(tex_coord: vec2f, iuv: vec2f) -> vec3f { 33 | var depth: f32 = abs(textureLoad(texture, vec2u(iuv), 0).x); 34 | return computeViewPosFromUVDepth(tex_coord, depth); 35 | } 36 | 37 | @fragment 38 | fn fs(input: FragmentInput) -> @location(0) vec4f { 39 | var depth: f32 = abs(textureLoad(texture, vec2u(input.iuv), 0).r); 40 | 41 | let bgColor: vec3f = vec3f(0.8, 0.8, 0.8); 42 | 43 | if (depth >= 1e4 || depth <= 0.) { 44 | return vec4f(bgColor, 1.); 45 | } 46 | 47 | var viewPos: vec3f = computeViewPosFromUVDepth(input.uv, depth); // z は負 48 | 49 | var ddx: vec3f = getViewPosFromTexCoord(input.uv + vec2f(uniforms.texel_size.x, 0.), input.iuv + vec2f(1.0, 0.0)) - viewPos; 50 | var ddy: vec3f = getViewPosFromTexCoord(input.uv + vec2f(0., uniforms.texel_size.y), input.iuv + vec2f(0.0, 1.0)) - viewPos; 51 | var ddx2: vec3f = viewPos - getViewPosFromTexCoord(input.uv + vec2f(-uniforms.texel_size.x, 0.), input.iuv + vec2f(-1.0, 0.0)); 52 | var ddy2: vec3f = viewPos - getViewPosFromTexCoord(input.uv + vec2f(0., -uniforms.texel_size.y), input.iuv + vec2f(0.0, -1.0)); 53 | 54 | if (abs(ddx.z) > abs(ddx2.z)) { 55 | ddx = ddx2; 56 | } 57 | if (abs(ddy.z) > abs(ddy2.z)) { 58 | ddy = ddy2; 59 | } 60 | 61 | var normal: vec3f = -normalize(cross(ddx, ddy)); 62 | var rayDir = normalize(viewPos); 63 | var lightDir = normalize((uniforms.view_matrix * vec4f(0, 0, -1, 0.)).xyz); 64 | var H: vec3f = normalize(lightDir - rayDir); 65 | var specular: f32 = pow(max(0.0, dot(H, normal)), 250.); 66 | var diffuse: f32 = max(0.0, dot(lightDir, normal)) * 1.0; 67 | 68 | var density = 1.5; 69 | 70 | var thickness = textureLoad(thickness_texture, vec2u(input.iuv), 0).r; 71 | var diffuseColor = vec3f(0.085, 0.6375, 0.9); 72 | var transmittance: vec3f = exp(-density * thickness * (1.0 - diffuseColor)); 73 | var refractionColor: vec3f = bgColor * transmittance; 74 | 75 | let F0 = 0.02; 76 | var fresnel: f32 = clamp(F0 + (1.0 - F0) * pow(1.0 - dot(normal, -rayDir), 5.0), 0., 1.0); 77 | 78 | var reflectionDir: vec3f = reflect(rayDir, normal); 79 | var reflectionDirWorld: vec3f = (uniforms.inv_view_matrix * vec4f(reflectionDir, 0.0)).xyz; 80 | var reflectionColor: vec3f = textureSampleLevel(envmap_texture, texture_sampler, reflectionDirWorld, 0.).rgb; 81 | var finalColor = 1.0 * specular + mix(refractionColor, reflectionColor, fresnel); 82 | 83 | return vec4f(finalColor, 1.0); 84 | 85 | // return vec4f(viewPos.y * 100, 0, 0, 1.0); 86 | 87 | // 法線 88 | // return vec4f(0.5 * normal + 0.5, 1.); 89 | // 法線の y 成分 90 | // return vec4f(vec3f(normal.x, 0, 0), 1); 91 | // return vec4f(vec3f(normal.y, 0, 0), 1); 92 | // return vec4f(vec3f(normal.z, 0, 0), 1); 93 | // specular だけ 94 | // return vec4f(vec3f(specular), 1); 95 | // reflection だけ 96 | // return vec4f(reflectionColor, 1.); 97 | // return vec4f(fresnel, 0., 0., 1.); 98 | } 99 | -------------------------------------------------------------------------------- /mls-mpm/g2p.wgsl: -------------------------------------------------------------------------------- 1 | struct Particle { 2 | position: vec3f, 3 | v: vec3f, 4 | C: mat3x3f, 5 | } 6 | struct Cell { 7 | vx: i32, 8 | vy: i32, 9 | vz: i32, 10 | mass: i32, 11 | } 12 | 13 | override fixed_point_multiplier: f32; 14 | override dt: f32; 15 | 16 | @group(0) @binding(0) var particles: array; 17 | @group(0) @binding(1) var cells: array; 18 | @group(0) @binding(2) var real_box_size: vec3f; 19 | @group(0) @binding(3) var init_box_size: vec3f; 20 | 21 | fn decodeFixedPoint(fixed_point: i32) -> f32 { 22 | return f32(fixed_point) / fixed_point_multiplier; 23 | } 24 | 25 | 26 | @compute @workgroup_size(64) 27 | fn g2p(@builtin(global_invocation_id) id: vec3) { 28 | if (id.x < arrayLength(&particles)) { 29 | particles[id.x].v = vec3f(0.); 30 | var weights: array; 31 | 32 | let particle = particles[id.x]; 33 | let cell_idx: vec3f = floor(particle.position); 34 | let cell_diff: vec3f = particle.position - (cell_idx + 0.5f); 35 | weights[0] = 0.5f * (0.5f - cell_diff) * (0.5f - cell_diff); 36 | weights[1] = 0.75f - cell_diff * cell_diff; 37 | weights[2] = 0.5f * (0.5f + cell_diff) * (0.5f + cell_diff); 38 | 39 | var B: mat3x3f = mat3x3f(vec3f(0.), vec3f(0.), vec3f(0.)); 40 | for (var gx = 0; gx < 3; gx++) { 41 | for (var gy = 0; gy < 3; gy++) { 42 | for (var gz = 0; gz < 3; gz++) { 43 | let weight: f32 = weights[gx].x * weights[gy].y * weights[gz].z; 44 | let cell_x: vec3f = vec3f( 45 | cell_idx.x + f32(gx) - 1., 46 | cell_idx.y + f32(gy) - 1., 47 | cell_idx.z + f32(gz) - 1. 48 | ); 49 | let cell_dist: vec3f = (cell_x + 0.5f) - particle.position; 50 | let cell_index: i32 = 51 | i32(cell_x.x) * i32(init_box_size.y) * i32(init_box_size.z) + 52 | i32(cell_x.y) * i32(init_box_size.z) + 53 | i32(cell_x.z); 54 | let weighted_velocity: vec3f = vec3f( 55 | decodeFixedPoint(cells[cell_index].vx), 56 | decodeFixedPoint(cells[cell_index].vy), 57 | decodeFixedPoint(cells[cell_index].vz) 58 | ) * weight; 59 | let term: mat3x3f = mat3x3f( 60 | weighted_velocity * cell_dist.x, 61 | weighted_velocity * cell_dist.y, 62 | weighted_velocity * cell_dist.z 63 | ); 64 | 65 | B += term; 66 | 67 | particles[id.x].v += weighted_velocity; 68 | } 69 | } 70 | } 71 | 72 | particles[id.x].C = B * 4.0f; 73 | particles[id.x].position += particles[id.x].v * dt; 74 | particles[id.x].position = vec3f( 75 | clamp(particles[id.x].position.x, 1., real_box_size.x - 2.), 76 | clamp(particles[id.x].position.y, 1., real_box_size.y - 2.), 77 | clamp(particles[id.x].position.z, 1., real_box_size.z - 2.) 78 | ); 79 | 80 | let k = 3.0; 81 | let wall_stiffness = 0.3; 82 | let x_n: vec3f = particles[id.x].position + particles[id.x].v * dt * k; 83 | let wall_min: vec3f = vec3f(3.); 84 | let wall_max: vec3f = real_box_size - 4.; 85 | if (x_n.x < wall_min.x) { particles[id.x].v.x += wall_stiffness * (wall_min.x - x_n.x); } 86 | if (x_n.x > wall_max.x) { particles[id.x].v.x += wall_stiffness * (wall_max.x - x_n.x); } 87 | if (x_n.y < wall_min.y) { particles[id.x].v.y += wall_stiffness * (wall_min.y - x_n.y); } 88 | if (x_n.y > wall_max.y) { particles[id.x].v.y += wall_stiffness * (wall_max.y - x_n.y); } 89 | if (x_n.z < wall_min.z) { particles[id.x].v.z += wall_stiffness * (wall_min.z - x_n.z); } 90 | if (x_n.z > wall_max.z) { particles[id.x].v.z += wall_stiffness * (wall_max.z - x_n.z); } 91 | } 92 | } -------------------------------------------------------------------------------- /sph/density.wgsl: -------------------------------------------------------------------------------- 1 | struct Particle { 2 | position: vec3f, 3 | v: vec3f, 4 | force: vec3f, 5 | density: f32, 6 | nearDensity: f32, 7 | } 8 | 9 | struct Environment { 10 | xGrids: i32, 11 | yGrids: i32, 12 | zGrids: i32, 13 | cellSize: f32, 14 | xHalf: f32, 15 | yHalf: f32, 16 | zHalf: f32, 17 | offset: f32, 18 | } 19 | 20 | struct SPHParams { 21 | mass: f32, 22 | kernelRadius: f32, 23 | kernelRadiusPow2: f32, 24 | kernelRadiusPow5: f32, 25 | kernelRadiusPow6: f32, 26 | kernelRadiusPow9: f32, 27 | dt: f32, 28 | stiffness: f32, 29 | nearStiffness: f32, 30 | restDensity: f32, 31 | viscosity: f32, 32 | n: u32 33 | } 34 | 35 | @group(0) @binding(0) var particles: array; 36 | @group(0) @binding(1) var sortedParticles: array; 37 | @group(0) @binding(2) var prefixSum: array; 38 | @group(0) @binding(3) var env: Environment; 39 | @group(0) @binding(4) var params: SPHParams; 40 | 41 | fn nearDensityKernel(r: f32) -> f32 { 42 | let scale = 15.0 / (3.1415926535 * params.kernelRadiusPow6); 43 | let d = params.kernelRadius - r; 44 | return scale * d * d * d; 45 | } 46 | 47 | fn densityKernel(r: f32) -> f32 { 48 | let scale = 315.0 / (64. * 3.1415926535 * params.kernelRadiusPow9); 49 | let dd = params.kernelRadiusPow2 - r * r; 50 | return scale * dd * dd * dd; 51 | } 52 | 53 | fn cellPosition(v: vec3f) -> vec3i { 54 | let xi = i32(floor((v.x + env.xHalf + env.offset) / env.cellSize)); 55 | let yi = i32(floor((v.y + env.yHalf + env.offset) / env.cellSize)); 56 | let zi = i32(floor((v.z + env.zHalf + env.offset) / env.cellSize)); 57 | return vec3i(xi, yi, zi); 58 | } 59 | 60 | fn cellNumberFromId(xi: i32, yi: i32, zi: i32) -> i32 { 61 | return xi + yi * env.xGrids + zi * env.xGrids * env.yGrids; 62 | } 63 | 64 | @compute @workgroup_size(64) 65 | fn computeDensity(@builtin(global_invocation_id) id: vec3) { 66 | if (id.x < params.n) { 67 | particles[id.x].density = 0.0; 68 | particles[id.x].nearDensity = 0.0; 69 | let pos_i = particles[id.x].position; 70 | let n = params.n; 71 | 72 | let v = cellPosition(pos_i); 73 | if (v.x < env.xGrids && 0 <= v.x && 74 | v.y < env.yGrids && 0 <= v.y && 75 | v.z < env.zGrids && 0 <= v.z) 76 | { 77 | for (var dz = max(-1, -v.z); dz <= min(1, env.zGrids - v.z - 1); dz++) { 78 | for (var dy = max(-1, -v.y); dy <= min(1, env.yGrids - v.y - 1); dy++) { 79 | let dxMin = max(-1, -v.x); 80 | let dxMax = min(1, env.xGrids - v.x - 1); 81 | let startCellNum = cellNumberFromId(v.x + dxMin, v.y + dy, v.z + dz); 82 | let endCellNum = cellNumberFromId(v.x + dxMax, v.y + dy, v.z + dz); 83 | let start = prefixSum[startCellNum]; 84 | let end = prefixSum[endCellNum + 1]; 85 | for (var j = start; j < end; j++) { 86 | let pos_j = sortedParticles[j].position; 87 | let r2 = dot(pos_i - pos_j, pos_i - pos_j); 88 | if (r2 < params.kernelRadiusPow2) { 89 | particles[id.x].density += params.mass * densityKernel(sqrt(r2)); 90 | particles[id.x].nearDensity += params.mass * nearDensityKernel(sqrt(r2)); 91 | } 92 | } 93 | } 94 | } 95 | } 96 | 97 | 98 | // for (var j = 0u; j < n; j = j + 1) { 99 | // let pos_j = particles[j].position; 100 | // let r2 = dot(pos_i - pos_j, pos_i - pos_j); 101 | // if (r2 < params.kernelRadiusPow2) { 102 | // particles[id.x].density += params.mass * densityKernel(sqrt(r2)); 103 | // particles[id.x].nearDensity += params.mass * nearDensityKernel(sqrt(r2)); 104 | // } 105 | // } 106 | } 107 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebGPU-Ocean 2 | A real-time 3d fluid simulation implemented in WebGPU. Works on your browsers which support WebGPU. 3 | 4 | [Try demo here!](https://webgpu-ocean.netlify.app/) 5 | 6 | ![webgpu-ocean-demo](https://github.com/user-attachments/assets/5b008b16-7d46-4e09-af21-d70f6fa2ec20) 7 | 8 | The following are the characteristics of the simulation. 9 | - [**Moving Least Squares Material Point Method (MLS-MPM)**](https://yzhu.io/publication/mpmmls2018siggraph/paper.pdf) by Hu et al. is implemented for the simulation. This algorithm enabled real-time simulation with **~100,000 particles on integrated graphics** and **~300,000 particles on decent GPUs** 10 | - [nialltl's article](https://nialltl.neocities.org/articles/mpm_guide) helped a lot when implementing MLS-MPM. Huge thanks for them! 11 | - Particle to Grid (P2G) stage is implemented with atomicAdd. 12 | - **Smoothed Particle Hydrodynamics (SPH)** based on [Particle-Based Fluid Simulation for Interactive Applications](https://matthias-research.github.io/pages/publications/sca03.pdf) by Müller et al. is also implemented. 13 | - You can enable SPH simulation by clicking "SPH" button on the top right. 14 | - For **fast neighborhood search on GPU**, an algorithm described in [FAST FIXED-RADIUS NEAREST NEIGHBORS: INTERACTIVE MILLION-PARTICLE FLUIDS](https://ramakarl.com/pdfs/2014_Hoetzlein_FastFixedRadius_Neighbors.pdf) is used. 15 | - **Screen-Space Fluid Rendering** described in [GDC 2010 slide](https://developer.download.nvidia.com/presentations/2010/gdc/Direct3D_Effects.pdf) is used for real-time rendering of the fluid. 16 | ## Implementation details of MLS-MPM 17 | Initially the simulation in this project was based on **Smoothed Particle Hydrodynamics (SPH)**. However, since the neighborhood search is really expensive, the maximum number of particles that can be simulated in real-time was 30,000 at best on integrated graphics. So I decided to implement **Moving Least Squares Material Point Method (MLS-MPM)** which is completely free from neighborhood search. The results were very good, enabling real-time simulations of **~100,000 particles on integrated graphics** and **~300,000 particles on decent GPUs.** 18 | 19 | My implementation of MLS-MPM is based on [nialltl's article](https://nialltl.neocities.org/articles/mpm_guide). According to the article, vanilla implementation of MPM is not suitable for real-time simulation since the inaccuracy of the estimate of the volume forces the timestep to be small. To tackle this problem, the article suggests recalculating volume every simulation step. This technique is very effective for setting a high timestep and currently requires only **2 simulation steps per frame.** (TBH this is a bit too large timestep so occasionally the simulation explodes) 20 | 21 | Implementing 3D version of nialltl's MLS-MPM in WebGPU was relatively straightforward, but there was one difficult point : **Particle to Grid (P2G) stage.** In the P2G stage, it's required to scatter particle data to grids in parallel. The most standard way to do this in WebGPU is using `atomicAdd`. However, since `atomicAdd` exists only for 32bit integers, it's impossible to directly use it to scatter data which is held as floating-point number. To avoid this problem, **fixed-point number** is used. That is, the data itself is hold as integers and multiplied by a constant (e.g. `1e-7`) to decode the data as floating-point numbers. This way it's possible to use `atomicAdd` for scattering particle data to grids. (I discovered this technique in [pbmpm](https://github.com/electronicarts/pbmpm) repository, a reference implementation for [A Position Based Material Point Method (SIGGRAPH 2024)](https://media.contentapi.ea.com/content/dam/ea/seed/presentations/seed-siggraph2024-pbmpm-paper.pdf) by Chris Lewin. ) 22 | ## How to run 23 | ``` 24 | npm install 25 | npm run serve 26 | ``` 27 | If you have trouble running the repo, feel free to open an issue. 28 | ## TODO 29 | - ~~Implement MLS-MPM~~ ⇒ **Done** 30 | - Currently, the bottleneck of the simulation is the neighborhood search in SPH. Therefore, implementing MLS-MPM would allow us to handle even larger real-time simulation (with > 100,000 particles?) since it doesn't require neighborhood search. 31 | - Now I'm actively learning MLS-MPM. But it will be harder than learning classical SPH, so any help would be appreciated :) 32 | - Implement a rendering method described in [Unified Spray, Foam and Bubbles for Particle-Based Fluids](https://cg.informatik.uni-freiburg.de/publications/2012_CGI_sprayFoamBubbles.pdf) 33 | - This would make the simulation look more spectacular! 34 | - But I suspect this method might be a bit too expensive for real-time simulation since it requires neighborhood search. Is there a cheaper way to generate foams?🤔 35 | - ~~Use better rendering method with less artifacts like [Narrow-Range Filter](https://dl.acm.org/doi/10.1145/3203201)~~ ⇒ **Done** (but not in this repo, see [Splash](https://github.com/matsuoka-601/Splash) !) 36 | - Currently, there are some artifacts derived from bilateral filter in rendered fluid. Using Narrow-Range Filter would reduce those artifacts. 37 | ## Known bug (feel free to fix them!) 38 | - SPH mode seems to crash on Mac 39 | -------------------------------------------------------------------------------- /sph/force.wgsl: -------------------------------------------------------------------------------- 1 | struct Particle { 2 | position: vec3f, 3 | v: vec3f, 4 | force: vec3f, 5 | density: f32, 6 | nearDensity: f32, 7 | } 8 | 9 | struct Environment { 10 | xGrids: i32, 11 | yGrids: i32, 12 | zGrids: i32, 13 | cellSize: f32, 14 | xHalf: f32, 15 | yHalf: f32, 16 | zHalf: f32, 17 | offset: f32, 18 | } 19 | 20 | struct SPHParams { 21 | mass: f32, 22 | kernelRadius: f32, 23 | kernelRadiusPow2: f32, 24 | kernelRadiusPow5: f32, 25 | kernelRadiusPow6: f32, 26 | kernelRadiusPow9: f32, 27 | dt: f32, 28 | stiffness: f32, 29 | nearStiffness: f32, 30 | restDensity: f32, 31 | viscosity: f32, 32 | n: u32 33 | } 34 | 35 | @group(0) @binding(0) var particles: array; 36 | @group(0) @binding(1) var sortedParticles: array; 37 | @group(0) @binding(2) var prefixSum: array; 38 | @group(0) @binding(3) var env: Environment; 39 | @group(0) @binding(4) var params: SPHParams; 40 | 41 | fn densityKernelGradient(r: f32) -> f32 { 42 | let scale: f32 = 45.0 / (3.1415926535 * params.kernelRadiusPow6); // pow 使うと遅いかも 43 | let d = params.kernelRadius - r; 44 | return scale * d * d; 45 | } 46 | 47 | fn nearDensityKernelGradient(r: f32) -> f32 { 48 | let scale: f32 = 45.0 / (3.1415926535 * params.kernelRadiusPow5); // 直す 49 | let a = params.kernelRadiusPow9; 50 | let d = params.kernelRadius - r; 51 | return scale * d * d; 52 | } 53 | 54 | fn viscosityKernelLaplacian(r: f32) -> f32 { 55 | let scale: f32 = 45.0 / (3.1415926535 * params.kernelRadiusPow6); 56 | // let dd = kernelRadius * kernelRadius - r * r; 57 | let d = params.kernelRadius - r; 58 | return scale * d; 59 | } 60 | 61 | fn cellPosition(v: vec3f) -> vec3i { 62 | let xi = i32(floor((v.x + env.xHalf + env.offset) / env.cellSize)); 63 | let yi = i32(floor((v.y + env.yHalf + env.offset) / env.cellSize)); 64 | let zi = i32(floor((v.z + env.zHalf + env.offset) / env.cellSize)); 65 | return vec3i(xi, yi, zi); 66 | } 67 | 68 | fn cellNumberFromId(xi: i32, yi: i32, zi: i32) -> i32 { 69 | return xi + yi * env.xGrids + zi * env.xGrids * env.yGrids; 70 | } 71 | 72 | @compute @workgroup_size(64) 73 | fn computeForce(@builtin(global_invocation_id) id: vec3) { 74 | if (id.x < params.n) { 75 | let n = params.n; 76 | let density_i = particles[id.x].density; 77 | let nearDensity_i = particles[id.x].nearDensity; 78 | let pos_i = particles[id.x].position; 79 | var fPress = vec3(0.0, 0.0, 0.0); 80 | var fVisc = vec3(0.0, 0.0, 0.0); 81 | 82 | let v = cellPosition(pos_i); 83 | if (v.x < env.xGrids && 0 <= v.x && 84 | v.y < env.yGrids && 0 <= v.y && 85 | v.z < env.zGrids && 0 <= v.z) 86 | { 87 | if (v.x < env.xGrids && v.y < env.yGrids && v.z < env.zGrids) { 88 | for (var dz = max(-1, -v.z); dz <= min(1, env.zGrids - v.z - 1); dz++) { 89 | for (var dy = max(-1, -v.y); dy <= min(1, env.yGrids - v.y - 1); dy++) { 90 | let dxMin = max(-1, -v.x); 91 | let dxMax = min(1, env.xGrids - v.x - 1); 92 | let startCellNum = cellNumberFromId(v.x + dxMin, v.y + dy, v.z + dz); 93 | let endCellNum = cellNumberFromId(v.x + dxMax, v.y + dy, v.z + dz); 94 | let start = prefixSum[startCellNum]; 95 | let end = prefixSum[endCellNum + 1]; 96 | for (var j = start; j < end; j++) { 97 | let density_j = sortedParticles[j].density; 98 | let nearDensity_j = sortedParticles[j].nearDensity; 99 | let pos_j = sortedParticles[j].position; 100 | let r2 = dot(pos_i - pos_j, pos_i - pos_j); 101 | if (density_j == 0. || nearDensity_j == 0.) { 102 | continue; 103 | } 104 | if (r2 < params.kernelRadiusPow2 && 1e-64 < r2) { 105 | let r = sqrt(r2); 106 | let pressure_i = params.stiffness * (density_i - params.restDensity); 107 | let pressure_j = params.stiffness * (density_j - params.restDensity); 108 | let nearPressure_i = params.nearStiffness * nearDensity_i; 109 | let nearPressure_j = params.nearStiffness * nearDensity_j; 110 | let sharedPressure = (pressure_i + pressure_j) / 2.0; 111 | let nearSharedPressure = (nearPressure_i + nearPressure_j) / 2.0; 112 | let dir = normalize(pos_j - pos_i); 113 | fPress += -params.mass * sharedPressure * dir * densityKernelGradient(r) / density_j; 114 | fPress += -params.mass * nearSharedPressure * dir * nearDensityKernelGradient(r) / nearDensity_j; 115 | let relativeSpeed = sortedParticles[j].v - particles[id.x].v; 116 | fVisc += params.mass * relativeSpeed * viscosityKernelLaplacian(r) / density_j; 117 | } 118 | } 119 | } 120 | } 121 | } 122 | } 123 | 124 | // // var cnt2 = 0.; 125 | // for (var j = 0u; j < n; j = j + 1) { 126 | // if (id.x == j) { 127 | // continue; 128 | // } 129 | // let density_j = particles[j].density; 130 | // let nearDensity_j = particles[j].nearDensity; 131 | // let pos_j = particles[j].position; 132 | // let r2 = dot(pos_i - pos_j, pos_i - pos_j); 133 | // if (r2 < params.kernelRadiusPow2 && 1e-64 < r2) { 134 | // let r = sqrt(r2); 135 | // let pressure_i = params.stiffness * (density_i - params.restDensity); 136 | // let pressure_j = params.stiffness * (density_j - params.restDensity); 137 | // let nearPressure_i = params.nearStiffness * nearDensity_i; 138 | // let nearPressure_j = params.nearStiffness * nearDensity_j; 139 | // let sharedPressure = (pressure_i + pressure_j) / 2.0; 140 | // let nearSharedPressure = (nearPressure_i + nearPressure_j) / 2.0; 141 | // let dir = normalize(pos_j - pos_i); 142 | // fPress += -params.mass * sharedPressure * dir * densityKernelGradient(r) / density_j; 143 | // fPress += -params.mass * nearSharedPressure * dir * nearDensityKernelGradient(r) / nearDensity_j; 144 | // let relativeSpeed = particles[j].v - particles[id.x].v; 145 | // fVisc += params.mass * relativeSpeed * viscosityKernelLaplacian(r) / density_j; 146 | // } 147 | // } 148 | 149 | fVisc *= params.viscosity; 150 | let fGrv: vec3f = density_i * vec3f(0.0, -9.8, 0.0); 151 | particles[id.x].force = fPress + fVisc + fGrv; 152 | } 153 | } -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | import { PrefixSumKernel } from 'webgpu-radix-sort'; 2 | import { mat4 } from 'wgpu-matrix' 3 | 4 | import { Camera } from './camera' 5 | import { mlsmpmParticleStructSize, MLSMPMSimulator } from './mls-mpm/mls-mpm' 6 | import { SPHSimulator, sphParticleStructSize } from './sph/sph'; 7 | import { renderUniformsViews, renderUniformsValues, numParticlesMax } from './common' 8 | import { FluidRenderer } from './render/fluidRender' 9 | 10 | /// 11 | 12 | 13 | async function init() { 14 | const canvas: HTMLCanvasElement = document.querySelector('canvas')! 15 | 16 | if (!navigator.gpu) { 17 | alert("WebGPU is not supported on your browser."); 18 | throw new Error() 19 | } 20 | 21 | const adapter = await navigator.gpu.requestAdapter() 22 | 23 | if (!adapter) { 24 | alert("Adapter is not available."); 25 | throw new Error() 26 | } 27 | 28 | const device = await adapter.requestDevice() 29 | 30 | const context = canvas.getContext('webgpu') as GPUCanvasContext 31 | 32 | if (!context) { 33 | throw new Error() 34 | } 35 | 36 | // const { devicePixelRatio } = window 37 | // let devicePixelRatio = 3.0; 38 | let devicePixelRatio = 0.7; 39 | canvas.width = devicePixelRatio * canvas.clientWidth 40 | canvas.height = devicePixelRatio * canvas.clientHeight 41 | 42 | const presentationFormat = navigator.gpu.getPreferredCanvasFormat() 43 | 44 | context.configure({ 45 | device, 46 | format: presentationFormat, 47 | }) 48 | 49 | return { canvas, device, presentationFormat, context } 50 | } 51 | 52 | async function main() { 53 | const { canvas, device, presentationFormat, context } = await init(); 54 | 55 | console.log("initialization done") 56 | 57 | context.configure({ 58 | device, 59 | format: presentationFormat, 60 | }) 61 | 62 | let cubemapTexture: GPUTexture; 63 | { 64 | // The order of the array layers is [+X, -X, +Y, -Y, +Z, -Z] 65 | const imgSrcs = [ 66 | 'cubemap/posx.png', 67 | 'cubemap/negx.png', 68 | 'cubemap/posy.png', 69 | 'cubemap/negy.png', 70 | 'cubemap/posz.png', 71 | 'cubemap/negz.png', 72 | ]; 73 | const promises = imgSrcs.map(async (src) => { 74 | const response = await fetch(src); 75 | return createImageBitmap(await response.blob()); 76 | }); 77 | const imageBitmaps = await Promise.all(promises); 78 | 79 | cubemapTexture = device.createTexture({ 80 | dimension: '2d', 81 | // Create a 2d array texture. 82 | // Assume each image has the same size. 83 | size: [imageBitmaps[0].width, imageBitmaps[0].height, 6], 84 | format: 'rgba8unorm', 85 | usage: 86 | GPUTextureUsage.TEXTURE_BINDING | 87 | GPUTextureUsage.COPY_DST | 88 | GPUTextureUsage.RENDER_ATTACHMENT, 89 | }); 90 | 91 | for (let i = 0; i < imageBitmaps.length; i++) { 92 | const imageBitmap = imageBitmaps[i]; 93 | device.queue.copyExternalImageToTexture( 94 | { source: imageBitmap }, 95 | { texture: cubemapTexture, origin: [0, 0, i] }, 96 | [imageBitmap.width, imageBitmap.height] 97 | ); 98 | } 99 | } 100 | const cubemapTextureView = cubemapTexture.createView({ 101 | dimension: 'cube', 102 | }); 103 | console.log("cubemap initialization done") 104 | 105 | // uniform buffer を作る 106 | renderUniformsViews.texel_size.set([1.0 / canvas.width, 1.0 / canvas.height]); 107 | 108 | // storage buffer を作る 109 | const maxParticleStructSize = Math.max(mlsmpmParticleStructSize, sphParticleStructSize) 110 | const particleBuffer = device.createBuffer({ 111 | label: 'particles buffer', 112 | size: maxParticleStructSize * numParticlesMax, 113 | usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, 114 | }) 115 | const posvelBuffer = device.createBuffer({ 116 | label: 'position buffer', 117 | size: 32 * numParticlesMax, // 32 = 2 x vec3f + padding 118 | usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, 119 | }) 120 | const renderUniformBuffer = device.createBuffer({ 121 | label: 'filter uniform buffer', 122 | size: renderUniformsValues.byteLength, 123 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 124 | }) 125 | 126 | console.log("buffer allocating done") 127 | 128 | let mlsmpmNumParticleParams = [40000, 70000, 120000, 200000] 129 | let mlsmpmInitBoxSizes = [[35, 25, 55], [40, 30, 60], [45, 40, 80], [50, 50, 80]] 130 | let mlsmpmInitDistances = [60, 70, 90, 100] 131 | let sphNumParticleParams = [10000, 20000, 30000, 40000] 132 | let sphInitBoxSizes = [[0.7, 2.0, 0.7], [1.0, 2.0, 1.0], [1.2, 2.0, 1.2], [1.4, 2.0, 1.4]] 133 | let sphInitDistances = [2.6, 3.0, 3.4, 3.8] 134 | 135 | const canvasElement = document.getElementById("fluidCanvas") as HTMLCanvasElement; 136 | // シミュレーション,カメラの初期化 137 | const mlsmpmFov = 45 * Math.PI / 180 138 | const mlsmpmRadius = 0.6 139 | const mlsmpmDiameter = 2 * mlsmpmRadius 140 | const mlsmpmZoomRate = 1.5 141 | const mlsmpmSimulator = new MLSMPMSimulator(particleBuffer, posvelBuffer, mlsmpmDiameter, device) 142 | const sphFov = 45 * Math.PI / 180 143 | const sphRadius = 0.04 144 | const sphDiameter = 2 * sphRadius 145 | const sphZoomRate = 0.05 146 | const sphSimulator = new SPHSimulator(particleBuffer, posvelBuffer, sphDiameter, device) 147 | 148 | const mlsmpmRenderer = new FluidRenderer(device, canvas, presentationFormat, mlsmpmRadius, mlsmpmFov, posvelBuffer, renderUniformBuffer, cubemapTextureView) 149 | const sphRenderer = new FluidRenderer(device, canvas, presentationFormat, sphRadius, sphFov, posvelBuffer, renderUniformBuffer, cubemapTextureView) 150 | 151 | console.log("simulator initialization done") 152 | 153 | const camera = new Camera(canvasElement); 154 | 155 | // ボタン押下の監視 156 | let numberButtonForm = document.getElementById('number-button') as HTMLFormElement; 157 | let numberButtonPressed = false; 158 | let numberButtonPressedButton = "1" 159 | numberButtonForm.addEventListener('change', function(event) { 160 | const target = event.target as HTMLInputElement 161 | if (target?.name === 'options') { 162 | numberButtonPressed = true 163 | numberButtonPressedButton = target.value 164 | } 165 | }); 166 | let simulationModeForm = document.getElementById('simulation-mode') as HTMLFormElement; 167 | let simulationModePressed = false; 168 | let simulationModePressedButton = "mls-mpm" 169 | simulationModeForm.addEventListener('change', function(event) { 170 | const target = event.target as HTMLInputElement 171 | if (target?.name === 'options') { 172 | simulationModePressed = true 173 | simulationModePressedButton = target.value 174 | } 175 | }); 176 | 177 | const smallValue = document.getElementById("small-value") as HTMLSpanElement; 178 | const mediumValue = document.getElementById("medium-value") as HTMLSpanElement; 179 | const largeValue = document.getElementById("large-value") as HTMLSpanElement; 180 | const veryLargeValue = document.getElementById("very-large-value") as HTMLSpanElement; 181 | 182 | // デバイスロストの監視 183 | let errorLog = document.getElementById('error-reason') as HTMLSpanElement; 184 | errorLog.textContent = ""; 185 | device.lost.then(info => { 186 | const reason = info.reason ? `reason: ${info.reason}` : 'unknown reason'; 187 | errorLog.textContent = reason; 188 | }); 189 | 190 | // はじめは mls-mpm 191 | const initDistance = mlsmpmInitDistances[1] 192 | let initBoxSize = mlsmpmInitBoxSizes[1] 193 | let realBoxSize = [...initBoxSize]; 194 | mlsmpmSimulator.reset(mlsmpmNumParticleParams[1], mlsmpmInitBoxSizes[1]) 195 | camera.reset(canvasElement, initDistance, [initBoxSize[0] / 2, initBoxSize[1] / 4, initBoxSize[2] / 2], 196 | mlsmpmFov, mlsmpmZoomRate) 197 | 198 | smallValue.textContent = "40,000" 199 | mediumValue.textContent = "70,000" 200 | largeValue.textContent = "120,000" 201 | veryLargeValue.textContent = "200,000" 202 | 203 | let sphereRenderFl = false 204 | let sphFl = false 205 | let boxWidthRatio = 1. 206 | 207 | console.log("simulation start") 208 | async function frame() { 209 | const start = performance.now(); 210 | 211 | if (simulationModePressed) { 212 | if (simulationModePressedButton == "mlsmpm") { 213 | sphFl = false 214 | smallValue.textContent = "40,000" 215 | mediumValue.textContent = "70,000" 216 | largeValue.textContent = "120,000" 217 | veryLargeValue.textContent = "200,000" 218 | } else { 219 | sphFl = true 220 | smallValue.textContent = "10,000" 221 | mediumValue.textContent = "20,000" 222 | largeValue.textContent = "30,000" 223 | veryLargeValue.textContent = "40,000" 224 | } 225 | simulationModePressed = false 226 | numberButtonPressed = true 227 | } 228 | 229 | if (numberButtonPressed) { 230 | const paramsIdx = parseInt(numberButtonPressedButton) 231 | if (sphFl) { 232 | initBoxSize = sphInitBoxSizes[paramsIdx] 233 | sphSimulator.reset(sphNumParticleParams[paramsIdx], initBoxSize) 234 | camera.reset(canvasElement, sphInitDistances[paramsIdx], [0, -initBoxSize[1] + 0.1, 0], 235 | sphFov, sphZoomRate) 236 | } else { 237 | initBoxSize = mlsmpmInitBoxSizes[paramsIdx] 238 | mlsmpmSimulator.reset(mlsmpmNumParticleParams[paramsIdx], initBoxSize) 239 | camera.reset(canvasElement, mlsmpmInitDistances[paramsIdx], [initBoxSize[0] / 2, initBoxSize[1] / 4, initBoxSize[2] / 2], 240 | mlsmpmFov, mlsmpmZoomRate) 241 | } 242 | realBoxSize = [...initBoxSize] 243 | let slider = document.getElementById("slider") as HTMLInputElement 244 | slider.value = "100" 245 | numberButtonPressed = false 246 | } 247 | 248 | // ボックスサイズの変更 249 | const slider = document.getElementById("slider") as HTMLInputElement 250 | const particle = document.getElementById("particle") as HTMLInputElement 251 | sphereRenderFl = particle.checked 252 | let curBoxWidthRatio = parseInt(slider.value) / 200 + 0.5 253 | const minClosingSpeed = sphFl ? -0.015 : -0.007 254 | const dVal = Math.max(curBoxWidthRatio - boxWidthRatio, minClosingSpeed) 255 | boxWidthRatio += dVal 256 | 257 | // 行列の更新 258 | realBoxSize[2] = initBoxSize[2] * boxWidthRatio 259 | if (sphFl) { 260 | sphSimulator.changeBoxSize(realBoxSize) 261 | } else { 262 | mlsmpmSimulator.changeBoxSize(realBoxSize) 263 | } 264 | device.queue.writeBuffer(renderUniformBuffer, 0, renderUniformsValues) 265 | 266 | const commandEncoder = device.createCommandEncoder() 267 | 268 | // 計算のためのパス 269 | if (sphFl) { 270 | sphSimulator.execute(commandEncoder) 271 | sphRenderer.execute(context, commandEncoder, sphSimulator.numParticles, sphereRenderFl) 272 | } else { 273 | mlsmpmSimulator.execute(commandEncoder) 274 | mlsmpmRenderer.execute(context, commandEncoder, mlsmpmSimulator.numParticles, sphereRenderFl) 275 | } 276 | 277 | device.queue.submit([commandEncoder.finish()]) 278 | const end = performance.now(); 279 | // console.log(`js: ${(end - start).toFixed(1)}ms`); 280 | 281 | requestAnimationFrame(frame) 282 | } 283 | requestAnimationFrame(frame) 284 | } 285 | 286 | main() -------------------------------------------------------------------------------- /mls-mpm/mls-mpm.ts: -------------------------------------------------------------------------------- 1 | import clearGrid from './clearGrid.wgsl'; 2 | import p2g_1 from './p2g_1.wgsl'; 3 | import p2g_2 from './p2g_2.wgsl'; 4 | import updateGrid from './updateGrid.wgsl'; 5 | import g2p from './g2p.wgsl'; 6 | import copyPosition from './copyPosition.wgsl' 7 | 8 | import { numParticlesMax, renderUniformsViews } from '../common'; 9 | 10 | export const mlsmpmParticleStructSize = 80 11 | 12 | export class MLSMPMSimulator { 13 | max_x_grids = 64; 14 | max_y_grids = 64; 15 | max_z_grids = 64; 16 | cellStructSize = 16; 17 | realBoxSizeBuffer: GPUBuffer 18 | initBoxSizeBuffer: GPUBuffer 19 | numParticles = 0 20 | gridCount = 0 21 | 22 | clearGridPipeline: GPUComputePipeline 23 | p2g1Pipeline: GPUComputePipeline 24 | p2g2Pipeline: GPUComputePipeline 25 | updateGridPipeline: GPUComputePipeline 26 | g2pPipeline: GPUComputePipeline 27 | copyPositionPipeline: GPUComputePipeline 28 | 29 | clearGridBindGroup: GPUBindGroup 30 | p2g1BindGroup: GPUBindGroup 31 | p2g2BindGroup: GPUBindGroup 32 | updateGridBindGroup: GPUBindGroup 33 | g2pBindGroup: GPUBindGroup 34 | copyPositionBindGroup: GPUBindGroup 35 | 36 | particleBuffer: GPUBuffer 37 | 38 | device: GPUDevice 39 | 40 | renderDiameter: number 41 | 42 | constructor (particleBuffer: GPUBuffer, posvelBuffer: GPUBuffer, renderDiameter: number, device: GPUDevice) 43 | { 44 | this.device = device 45 | this.renderDiameter = renderDiameter 46 | const clearGridModule = device.createShaderModule({ code: clearGrid }); 47 | const p2g1Module = device.createShaderModule({ code: p2g_1 }); 48 | const p2g2Module = device.createShaderModule({ code: p2g_2 }); 49 | const updateGridModule = device.createShaderModule({ code: updateGrid }); 50 | const g2pModule = device.createShaderModule({ code: g2p }); 51 | const copyPositionModule = device.createShaderModule({ code: copyPosition }); 52 | 53 | const constants = { 54 | stiffness: 3., 55 | restDensity: 4., 56 | dynamic_viscosity: 0.1, 57 | dt: 0.20, 58 | fixed_point_multiplier: 1e7, 59 | } 60 | 61 | this.clearGridPipeline = device.createComputePipeline({ 62 | label: "clear grid pipeline", 63 | layout: 'auto', 64 | compute: { 65 | module: clearGridModule, 66 | } 67 | }) 68 | this.p2g1Pipeline = device.createComputePipeline({ 69 | label: "p2g 1 pipeline", 70 | layout: 'auto', 71 | compute: { 72 | module: p2g1Module, 73 | constants: { 74 | 'fixed_point_multiplier': constants.fixed_point_multiplier 75 | }, 76 | } 77 | }) 78 | this.p2g2Pipeline = device.createComputePipeline({ 79 | label: "p2g 2 pipeline", 80 | layout: 'auto', 81 | compute: { 82 | module: p2g2Module, 83 | constants: { 84 | 'fixed_point_multiplier': constants.fixed_point_multiplier, 85 | 'stiffness': constants.stiffness, 86 | 'rest_density': constants.restDensity, 87 | 'dynamic_viscosity': constants.dynamic_viscosity, 88 | 'dt': constants.dt, 89 | }, 90 | } 91 | }) 92 | this.updateGridPipeline = device.createComputePipeline({ 93 | label: "update grid pipeline", 94 | layout: 'auto', 95 | compute: { 96 | module: updateGridModule, 97 | constants: { 98 | 'fixed_point_multiplier': constants.fixed_point_multiplier, 99 | 'dt': constants.dt, 100 | }, 101 | } 102 | }); 103 | this.g2pPipeline = device.createComputePipeline({ 104 | label: "g2p pipeline", 105 | layout: 'auto', 106 | compute: { 107 | module: g2pModule, 108 | constants: { 109 | 'fixed_point_multiplier': constants.fixed_point_multiplier, 110 | 'dt': constants.dt, 111 | }, 112 | } 113 | }); 114 | this.copyPositionPipeline = device.createComputePipeline({ 115 | label: "copy position pipeline", 116 | layout: 'auto', 117 | compute: { 118 | module: copyPositionModule, 119 | } 120 | }); 121 | 122 | const maxGridCount = this.max_x_grids * this.max_y_grids * this.max_z_grids; 123 | const realBoxSizeValues = new ArrayBuffer(12); 124 | const initBoxSizeValues = new ArrayBuffer(12); 125 | 126 | const cellBuffer = device.createBuffer({ 127 | label: 'cells buffer', 128 | size: this.cellStructSize * maxGridCount, 129 | usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, 130 | }) 131 | this.realBoxSizeBuffer = device.createBuffer({ 132 | label: 'real box size buffer', 133 | size: realBoxSizeValues.byteLength, 134 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 135 | }) 136 | this.initBoxSizeBuffer = device.createBuffer({ 137 | label: 'init box size buffer', 138 | size: initBoxSizeValues.byteLength, 139 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 140 | }) 141 | device.queue.writeBuffer(this.initBoxSizeBuffer, 0, initBoxSizeValues); 142 | device.queue.writeBuffer(this.realBoxSizeBuffer, 0, realBoxSizeValues); 143 | 144 | // BindGroup 145 | this.clearGridBindGroup = device.createBindGroup({ 146 | layout: this.clearGridPipeline.getBindGroupLayout(0), 147 | entries: [ 148 | { binding: 0, resource: { buffer: cellBuffer }}, 149 | ], 150 | }) 151 | this.p2g1BindGroup = device.createBindGroup({ 152 | layout: this.p2g1Pipeline.getBindGroupLayout(0), 153 | entries: [ 154 | { binding: 0, resource: { buffer: particleBuffer }}, 155 | { binding: 1, resource: { buffer: cellBuffer }}, 156 | { binding: 2, resource: { buffer: this.initBoxSizeBuffer }}, 157 | ], 158 | }) 159 | this.p2g2BindGroup = device.createBindGroup({ 160 | layout: this.p2g2Pipeline.getBindGroupLayout(0), 161 | entries: [ 162 | { binding: 0, resource: { buffer: particleBuffer }}, 163 | { binding: 1, resource: { buffer: cellBuffer }}, 164 | { binding: 2, resource: { buffer: this.initBoxSizeBuffer }}, 165 | ] 166 | }) 167 | this.updateGridBindGroup = device.createBindGroup({ 168 | layout: this.updateGridPipeline.getBindGroupLayout(0), 169 | entries: [ 170 | { binding: 0, resource: { buffer: cellBuffer }}, 171 | { binding: 1, resource: { buffer: this.realBoxSizeBuffer }}, 172 | { binding: 2, resource: { buffer: this.initBoxSizeBuffer }}, 173 | ], 174 | }) 175 | this.g2pBindGroup = device.createBindGroup({ 176 | layout: this.g2pPipeline.getBindGroupLayout(0), 177 | entries: [ 178 | { binding: 0, resource: { buffer: particleBuffer }}, 179 | { binding: 1, resource: { buffer: cellBuffer }}, 180 | { binding: 2, resource: { buffer: this.realBoxSizeBuffer }}, 181 | { binding: 3, resource: { buffer: this.initBoxSizeBuffer }}, 182 | ], 183 | }) 184 | this.copyPositionBindGroup = device.createBindGroup({ 185 | layout: this.copyPositionPipeline.getBindGroupLayout(0), 186 | entries: [ 187 | { binding: 0, resource: { buffer: particleBuffer }}, 188 | { binding: 1, resource: { buffer: posvelBuffer }}, 189 | ] 190 | }) 191 | 192 | this.particleBuffer = particleBuffer 193 | } 194 | 195 | initDambreak(initBoxSize: number[], numParticles: number) { 196 | let particlesBuf = new ArrayBuffer(mlsmpmParticleStructSize * numParticlesMax); 197 | const spacing = 0.65; 198 | 199 | this.numParticles = 0; 200 | 201 | for (let j = 0; j < initBoxSize[1] * 0.80 && this.numParticles < numParticles; j += spacing) { 202 | for (let i = 3; i < initBoxSize[0] - 4 && this.numParticles < numParticles; i += spacing) { 203 | for (let k = 3; k < initBoxSize[2] / 2 && this.numParticles < numParticles; k += spacing) { 204 | const offset = mlsmpmParticleStructSize * this.numParticles; 205 | const particleViews = { 206 | position: new Float32Array(particlesBuf, offset + 0, 3), 207 | v: new Float32Array(particlesBuf, offset + 16, 3), 208 | C: new Float32Array(particlesBuf, offset + 32, 12), 209 | }; 210 | const jitter = 2.0 * Math.random(); 211 | particleViews.position.set([i + jitter, j + jitter, k + jitter]); 212 | this.numParticles++; 213 | } 214 | } 215 | } 216 | 217 | let particles = new ArrayBuffer(mlsmpmParticleStructSize * this.numParticles); 218 | const oldView = new Uint8Array(particlesBuf); 219 | const newView = new Uint8Array(particles); 220 | newView.set(oldView.subarray(0, newView.length)); 221 | 222 | return particles; 223 | } 224 | 225 | reset(numParticles: number, initBoxSize: number[]) { 226 | renderUniformsViews.sphere_size.set([this.renderDiameter]) 227 | const particleData = this.initDambreak(initBoxSize, numParticles); 228 | const maxGridCount = this.max_x_grids * this.max_y_grids * this.max_z_grids; 229 | this.gridCount = Math.ceil(initBoxSize[0]) * Math.ceil(initBoxSize[1]) * Math.ceil(initBoxSize[2]); 230 | if (this.gridCount > maxGridCount) { 231 | throw new Error("gridCount should be equal to or less than maxGridCount") 232 | } 233 | const realBoxSizeValues = new ArrayBuffer(12); 234 | const realBoxSizeViews = new Float32Array(realBoxSizeValues); 235 | const initBoxSizeValues = new ArrayBuffer(12); 236 | const initBoxSizeViews = new Float32Array(initBoxSizeValues); 237 | initBoxSizeViews.set(initBoxSize); 238 | realBoxSizeViews.set(initBoxSize); 239 | this.device.queue.writeBuffer(this.initBoxSizeBuffer, 0, initBoxSizeValues); 240 | this.device.queue.writeBuffer(this.realBoxSizeBuffer, 0, realBoxSizeValues); 241 | this.device.queue.writeBuffer(this.particleBuffer, 0, particleData) 242 | console.log(this.numParticles) 243 | } 244 | 245 | execute(commandEncoder: GPUCommandEncoder) { 246 | const computePass = commandEncoder.beginComputePass(); 247 | for (let i = 0; i < 2; i++) { 248 | computePass.setBindGroup(0, this.clearGridBindGroup); 249 | computePass.setPipeline(this.clearGridPipeline); 250 | computePass.dispatchWorkgroups(Math.ceil(this.gridCount / 64)) // これは gridCount だよな? 251 | computePass.setBindGroup(0, this.p2g1BindGroup) 252 | computePass.setPipeline(this.p2g1Pipeline) 253 | computePass.dispatchWorkgroups(Math.ceil(this.numParticles / 64)) 254 | computePass.setBindGroup(0, this.p2g2BindGroup) 255 | computePass.setPipeline(this.p2g2Pipeline) 256 | computePass.dispatchWorkgroups(Math.ceil(this.numParticles / 64)) 257 | computePass.setBindGroup(0, this.updateGridBindGroup) 258 | computePass.setPipeline(this.updateGridPipeline) 259 | computePass.dispatchWorkgroups(Math.ceil(this.gridCount / 64)) 260 | computePass.setBindGroup(0, this.g2pBindGroup) 261 | computePass.setPipeline(this.g2pPipeline) 262 | computePass.dispatchWorkgroups(Math.ceil(this.numParticles / 64)) 263 | computePass.setBindGroup(0, this.copyPositionBindGroup) 264 | computePass.setPipeline(this.copyPositionPipeline) 265 | computePass.dispatchWorkgroups(Math.ceil(this.numParticles / 64)) 266 | } 267 | computePass.end() 268 | } 269 | 270 | changeBoxSize(realBoxSize: number[]) { 271 | const realBoxSizeValues = new ArrayBuffer(12); 272 | const realBoxSizeViews = new Float32Array(realBoxSizeValues); 273 | realBoxSizeViews.set(realBoxSize) 274 | this.device.queue.writeBuffer(this.realBoxSizeBuffer, 0, realBoxSizeViews) 275 | } 276 | } -------------------------------------------------------------------------------- /sph/sph.ts: -------------------------------------------------------------------------------- 1 | import gridClear from './grid/gridClear.wgsl' 2 | import gridBuild from './grid/gridBuild.wgsl' 3 | import reorderParticles from './grid/reorderParticles.wgsl' 4 | import density from './density.wgsl' 5 | import force from './force.wgsl' 6 | import integrate from './integrate.wgsl' 7 | import copyPosition from './copyPosition.wgsl' 8 | 9 | import { PrefixSumKernel } from 'webgpu-radix-sort'; 10 | 11 | import { renderUniformsViews, numParticlesMax } from '../common'; 12 | 13 | export const sphParticleStructSize = 64 14 | 15 | export class SPHSimulator { 16 | device: GPUDevice 17 | 18 | gridClearPipeline: GPUComputePipeline 19 | gridBuildPipeline: GPUComputePipeline 20 | reorderPipeline: GPUComputePipeline 21 | densityPipeline: GPUComputePipeline 22 | forcePipeline: GPUComputePipeline 23 | integratePipeline: GPUComputePipeline 24 | copyPositionPipeline: GPUComputePipeline 25 | 26 | gridClearBindGroup: GPUBindGroup 27 | gridBuildBindGroup: GPUBindGroup 28 | reorderBindGroup: GPUBindGroup 29 | densityBindGroup: GPUBindGroup 30 | forceBindGroup: GPUBindGroup 31 | integrateBindGroup: GPUBindGroup 32 | copyPositionBindGroup: GPUBindGroup 33 | 34 | cellParticleCountBuffer: GPUBuffer 35 | particleBuffer: GPUBuffer 36 | realBoxSizeBuffer: GPUBuffer 37 | sphParamsBuffer: GPUBuffer 38 | 39 | prefixSumKernel: any 40 | 41 | kernelRadius = 0.07 42 | numParticles = 0 43 | gridCount = 0 44 | 45 | renderDiameter: number 46 | 47 | constructor (particleBuffer: GPUBuffer, posvelBuffer: GPUBuffer, renderDiameter: number, device: GPUDevice) { 48 | this.device = device 49 | this.renderDiameter = renderDiameter 50 | const densityModule = device.createShaderModule({ code: density }) 51 | const forceModule = device.createShaderModule({ code: force }) 52 | const integrateModule = device.createShaderModule({ code: integrate }) 53 | const gridBuildModule = device.createShaderModule({ code: gridBuild }) 54 | const gridClearModule = device.createShaderModule({ code: gridClear }) 55 | const reorderParticlesModule = device.createShaderModule({ code: reorderParticles }) 56 | const copyPositionModule = device.createShaderModule({ code: copyPosition }) 57 | 58 | const cellSize = 1.0 * this.kernelRadius 59 | const xHalfMax = 2.0 60 | const yHalfMax = 2.0 61 | const zHalfMax = 2.0 62 | const xLen = 2.0 * xHalfMax 63 | const yLen = 2.0 * yHalfMax 64 | const zLen = 2.0 * zHalfMax 65 | const sentinel = 4 * cellSize 66 | const xGrids = Math.ceil((xLen + sentinel) / cellSize) 67 | const yGrids = Math.ceil((yLen + sentinel) / cellSize) 68 | const zGrids = Math.ceil((zLen + sentinel) / cellSize) 69 | this.gridCount = xGrids * yGrids * zGrids; 70 | const offset = sentinel / 2; 71 | 72 | const stiffness = 20; 73 | const nearStiffness = 1.0; 74 | const mass = 1.0; 75 | const restDensity = 15000; 76 | const viscosity = 100; 77 | const dt = 0.006; 78 | 79 | this.gridClearPipeline = device.createComputePipeline({ 80 | label: "grid clear pipeline", 81 | layout: 'auto', 82 | compute: { 83 | module: gridClearModule, 84 | } 85 | }) 86 | this.gridBuildPipeline = device.createComputePipeline({ 87 | label: "grid build pipeline", 88 | layout: 'auto', 89 | compute: { 90 | module: gridBuildModule, 91 | } 92 | }) 93 | this.reorderPipeline = device.createComputePipeline({ 94 | label: "reorder pipeline", 95 | layout: 'auto', 96 | compute: { 97 | module: reorderParticlesModule, 98 | } 99 | }) 100 | this.densityPipeline = device.createComputePipeline({ 101 | label: "density pipeline", 102 | layout: 'auto', 103 | compute: { 104 | module: densityModule, 105 | } 106 | }); 107 | this.forcePipeline = device.createComputePipeline({ 108 | label: "force pipeline", 109 | layout: 'auto', 110 | compute: { 111 | module: forceModule, 112 | } 113 | }); 114 | this.integratePipeline = device.createComputePipeline({ 115 | label: "integrate pipeline", 116 | layout: 'auto', 117 | compute: { 118 | module: integrateModule, 119 | } 120 | }); 121 | this.copyPositionPipeline = device.createComputePipeline({ 122 | label: "copy position pipeline", 123 | layout: 'auto', 124 | compute: { 125 | module: copyPositionModule, 126 | } 127 | }); 128 | 129 | const environmentValues = new ArrayBuffer(32); 130 | const environmentViews = { 131 | xGrids: new Int32Array(environmentValues, 0, 1), 132 | yGrids: new Int32Array(environmentValues, 4, 1), 133 | zGrids: new Int32Array(environmentValues, 8, 1), 134 | cellSize: new Float32Array(environmentValues, 12, 1), 135 | xHalf: new Float32Array(environmentValues, 16, 1), 136 | yHalf: new Float32Array(environmentValues, 20, 1), 137 | zHalf: new Float32Array(environmentValues, 24, 1), 138 | offset: new Float32Array(environmentValues, 28, 1), 139 | } 140 | environmentViews.xGrids.set([xGrids]); 141 | environmentViews.yGrids.set([yGrids]); 142 | environmentViews.zGrids.set([zGrids]); 143 | environmentViews.cellSize.set([cellSize]); 144 | environmentViews.xHalf.set([xHalfMax]); 145 | environmentViews.yHalf.set([yHalfMax]); 146 | environmentViews.zHalf.set([zHalfMax]); 147 | environmentViews.offset.set([offset]); 148 | 149 | const sphParamsValues = new ArrayBuffer(48); 150 | const sphParamsViews = { 151 | mass: new Float32Array(sphParamsValues, 0, 1), 152 | kernelRadius: new Float32Array(sphParamsValues, 4, 1), 153 | kernelRadiusPow2: new Float32Array(sphParamsValues, 8, 1), 154 | kernelRadiusPow5: new Float32Array(sphParamsValues, 12, 1), 155 | kernelRadiusPow6: new Float32Array(sphParamsValues, 16, 1), 156 | kernelRadiusPow9: new Float32Array(sphParamsValues, 20, 1), 157 | dt: new Float32Array(sphParamsValues, 24, 1), 158 | stiffness: new Float32Array(sphParamsValues, 28, 1), 159 | nearStiffness: new Float32Array(sphParamsValues, 32, 1), 160 | restDensity: new Float32Array(sphParamsValues, 36, 1), 161 | viscosity: new Float32Array(sphParamsValues, 40, 1), 162 | n: new Uint32Array(sphParamsValues, 44, 1), 163 | }; 164 | sphParamsViews.mass.set([mass]) 165 | sphParamsViews.kernelRadius.set([this.kernelRadius]) 166 | sphParamsViews.kernelRadiusPow2.set([Math.pow(this.kernelRadius, 2)]) 167 | sphParamsViews.kernelRadiusPow5.set([Math.pow(this.kernelRadius, 5)]) 168 | sphParamsViews.kernelRadiusPow6.set([Math.pow(this.kernelRadius, 6)]) 169 | sphParamsViews.kernelRadiusPow9.set([Math.pow(this.kernelRadius, 9)]) 170 | sphParamsViews.dt.set([dt]) 171 | sphParamsViews.stiffness.set([stiffness]) 172 | sphParamsViews.nearStiffness.set([nearStiffness]) 173 | sphParamsViews.restDensity.set([restDensity]) 174 | sphParamsViews.viscosity.set([viscosity]) 175 | // n はあとで 176 | 177 | 178 | const realBoxSizeValues = new ArrayBuffer(12); 179 | this.cellParticleCountBuffer = device.createBuffer({ // 累積和はここに保存 180 | label: 'cell particle count buffer', 181 | size: 4 * (this.gridCount + 1), // 1 要素余分にとっておく 182 | usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, 183 | }) 184 | const targetParticlesBuffer = device.createBuffer({ 185 | label: 'target particles buffer', 186 | size: sphParticleStructSize * numParticlesMax, 187 | usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, 188 | }) 189 | const particleCellOffsetBuffer = device.createBuffer({ 190 | label: 'particle cell offset buffer', 191 | size: 4 * numParticlesMax, 192 | usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, 193 | }) 194 | this.realBoxSizeBuffer = device.createBuffer({ 195 | label: 'real box size buffer', 196 | size: realBoxSizeValues.byteLength, 197 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 198 | }) 199 | const environmentBuffer = device.createBuffer({ 200 | label: 'environment buffer', 201 | size: environmentValues.byteLength, 202 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 203 | }) 204 | this.sphParamsBuffer = device.createBuffer({ 205 | label: 'sph params buffer', 206 | size: sphParamsValues.byteLength, 207 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 208 | }) 209 | device.queue.writeBuffer(environmentBuffer, 0, environmentValues) 210 | device.queue.writeBuffer(this.sphParamsBuffer, 0, sphParamsValues) 211 | 212 | // BindGroup 213 | this.gridClearBindGroup = device.createBindGroup({ 214 | layout: this.gridClearPipeline.getBindGroupLayout(0), 215 | entries: [ 216 | { binding: 0, resource: { buffer: this.cellParticleCountBuffer }}, 217 | ], 218 | }) 219 | this.gridBuildBindGroup = device.createBindGroup({ 220 | layout: this.gridBuildPipeline.getBindGroupLayout(0), 221 | entries: [ 222 | { binding: 0, resource: { buffer: this.cellParticleCountBuffer }}, 223 | { binding: 1, resource: { buffer: particleCellOffsetBuffer }}, 224 | { binding: 2, resource: { buffer: particleBuffer }}, 225 | { binding: 3, resource: { buffer: environmentBuffer }}, 226 | { binding: 4, resource: { buffer: this.sphParamsBuffer }}, 227 | ], 228 | }) 229 | this.reorderBindGroup = device.createBindGroup({ 230 | layout: this.reorderPipeline.getBindGroupLayout(0), 231 | entries: [ 232 | { binding: 0, resource: { buffer: particleBuffer }}, 233 | { binding: 1, resource: { buffer: targetParticlesBuffer }}, 234 | { binding: 2, resource: { buffer: this.cellParticleCountBuffer }}, 235 | { binding: 3, resource: { buffer: particleCellOffsetBuffer }}, 236 | { binding: 4, resource: { buffer: environmentBuffer }}, 237 | { binding: 5, resource: { buffer: this.sphParamsBuffer }}, 238 | ] 239 | }) 240 | 241 | this.densityBindGroup = device.createBindGroup({ 242 | layout: this.densityPipeline.getBindGroupLayout(0), 243 | entries: [ 244 | { binding: 0, resource: { buffer: particleBuffer }}, 245 | { binding: 1, resource: { buffer: targetParticlesBuffer }}, 246 | { binding: 2, resource: { buffer: this.cellParticleCountBuffer }}, 247 | { binding: 3, resource: { buffer: environmentBuffer }}, 248 | { binding: 4, resource: { buffer: this.sphParamsBuffer }}, 249 | ], 250 | }) 251 | this.forceBindGroup = device.createBindGroup({ 252 | layout: this.forcePipeline.getBindGroupLayout(0), 253 | entries: [ 254 | { binding: 0, resource: { buffer: particleBuffer }}, 255 | { binding: 1, resource: { buffer: targetParticlesBuffer }}, 256 | { binding: 2, resource: { buffer: this.cellParticleCountBuffer }}, 257 | { binding: 3, resource: { buffer: environmentBuffer }}, 258 | { binding: 4, resource: { buffer: this.sphParamsBuffer }}, 259 | ], 260 | }) 261 | this.integrateBindGroup = device.createBindGroup({ 262 | layout: this.integratePipeline.getBindGroupLayout(0), 263 | entries: [ 264 | { binding: 0, resource: { buffer: particleBuffer }}, 265 | { binding: 1, resource: { buffer: this.realBoxSizeBuffer }}, 266 | { binding: 2, resource: { buffer: this.sphParamsBuffer }}, 267 | ], 268 | }) 269 | this.copyPositionBindGroup = device.createBindGroup({ 270 | layout: this.copyPositionPipeline.getBindGroupLayout(0), 271 | entries: [ 272 | { binding: 0, resource: { buffer: particleBuffer }}, 273 | { binding: 1, resource: { buffer: posvelBuffer }}, 274 | { binding: 2, resource: { buffer: this.sphParamsBuffer }}, 275 | ], 276 | }) 277 | 278 | this.particleBuffer = particleBuffer 279 | } 280 | 281 | reset(numParticles: number, initHalfBoxSize: number[]) { 282 | renderUniformsViews.sphere_size.set([this.renderDiameter]) 283 | const particleData = this.initDambreak(initHalfBoxSize, numParticles) 284 | const realBoxSizeValues = new ArrayBuffer(12); 285 | const realBoxSizeViews = { 286 | xHalf: new Float32Array(realBoxSizeValues, 0, 1), 287 | yHalf: new Float32Array(realBoxSizeValues, 4, 1), 288 | zHalf: new Float32Array(realBoxSizeValues, 8, 1), 289 | }; 290 | realBoxSizeViews.xHalf.set([initHalfBoxSize[0]]); 291 | realBoxSizeViews.yHalf.set([initHalfBoxSize[1]]); 292 | realBoxSizeViews.zHalf.set([initHalfBoxSize[2]]); 293 | const numParticleValue = new Float32Array(1); 294 | numParticleValue[0] = this.numParticles 295 | console.log(this.numParticles) 296 | this.device.queue.writeBuffer(this.sphParamsBuffer, 44, numParticleValue) // TODO : avoid hardcoding 297 | this.device.queue.writeBuffer(this.particleBuffer, 0, particleData) 298 | this.device.queue.writeBuffer(this.realBoxSizeBuffer, 0, realBoxSizeValues) 299 | } 300 | 301 | execute(commandEncoder: GPUCommandEncoder) { 302 | const computePass = commandEncoder.beginComputePass(); 303 | for (let i = 0; i < 2; i++) { 304 | computePass.setBindGroup(0, this.gridClearBindGroup); 305 | computePass.setPipeline(this.gridClearPipeline); 306 | computePass.dispatchWorkgroups(Math.ceil((this.gridCount + 1) / 64)) 307 | computePass.setBindGroup(0, this.gridBuildBindGroup); 308 | computePass.setPipeline(this.gridBuildPipeline); 309 | computePass.dispatchWorkgroups(Math.ceil(this.numParticles / 64)) 310 | this.prefixSumKernel = new PrefixSumKernel({ 311 | device: this.device, data: this.cellParticleCountBuffer, count: this.gridCount + 1 312 | }) 313 | this.prefixSumKernel.dispatch(computePass); 314 | computePass.setBindGroup(0, this.reorderBindGroup); 315 | computePass.setPipeline(this.reorderPipeline) 316 | computePass.dispatchWorkgroups(Math.ceil(this.numParticles / 64)) 317 | 318 | computePass.setBindGroup(0, this.densityBindGroup) 319 | computePass.setPipeline(this.densityPipeline) 320 | computePass.dispatchWorkgroups(Math.ceil(this.numParticles / 64)) 321 | computePass.setBindGroup(0, this.reorderBindGroup); 322 | computePass.setPipeline(this.reorderPipeline) 323 | computePass.dispatchWorkgroups(Math.ceil(this.numParticles / 64)) 324 | computePass.setBindGroup(0, this.forceBindGroup) 325 | computePass.setPipeline(this.forcePipeline) 326 | computePass.dispatchWorkgroups(Math.ceil(this.numParticles / 64)) 327 | computePass.setBindGroup(0, this.integrateBindGroup) 328 | computePass.setPipeline(this.integratePipeline) 329 | computePass.dispatchWorkgroups(Math.ceil(this.numParticles / 64)) 330 | computePass.setBindGroup(0, this.copyPositionBindGroup) 331 | computePass.setPipeline(this.copyPositionPipeline) 332 | computePass.dispatchWorkgroups(Math.ceil(this.numParticles / 64)) 333 | } 334 | 335 | computePass.end() 336 | } 337 | 338 | initDambreak(initHalfBoxSize: number[], numParticles: number) { 339 | let particlesBuf = new ArrayBuffer(sphParticleStructSize * numParticles); 340 | this.numParticles = 0; 341 | const DIST_FACTOR = 0.5 342 | 343 | for (var y = -initHalfBoxSize[1] * 0.95; this.numParticles < numParticles; y += DIST_FACTOR * this.kernelRadius) { 344 | for (var x = -0.95 * initHalfBoxSize[0]; x < 0.95 * initHalfBoxSize[0] && this.numParticles < numParticles; x += DIST_FACTOR * this.kernelRadius) { 345 | for (var z = -0.95 * initHalfBoxSize[2]; z < 0 * initHalfBoxSize[2] && this.numParticles < numParticles; z += DIST_FACTOR * this.kernelRadius) { 346 | let jitter = 0.001 * Math.random(); 347 | const offset = sphParticleStructSize * this.numParticles; 348 | const particleViews = { 349 | position: new Float32Array(particlesBuf, offset + 0, 3), 350 | v: new Float32Array(particlesBuf, offset + 16, 3), 351 | force: new Float32Array(particlesBuf, offset + 32, 3), 352 | density: new Float32Array(particlesBuf, offset + 44, 1), 353 | nearDensity: new Float32Array(particlesBuf, offset + 48, 1), 354 | }; 355 | particleViews.position.set([x + jitter, y + jitter, z + jitter]); 356 | this.numParticles++; 357 | } 358 | } 359 | } 360 | 361 | console.log(this.numParticles) 362 | return particlesBuf; 363 | } 364 | 365 | changeBoxSize(realBoxSize: number[]) { 366 | const realBoxSizeValues = new ArrayBuffer(12); 367 | const realBoxSizeViews = new Float32Array(realBoxSizeValues); 368 | realBoxSizeViews.set(realBoxSize) 369 | this.device.queue.writeBuffer(this.realBoxSizeBuffer, 0, realBoxSizeViews) 370 | } 371 | } -------------------------------------------------------------------------------- /render/fluidRender.ts: -------------------------------------------------------------------------------- 1 | import depthMap from './depthMap.wgsl' 2 | import depthFilter from './bilateral.wgsl' 3 | import fluid from './fluid.wgsl' 4 | import fullScreen from './fullScreen.wgsl' 5 | import thicknessMap from './thicknessMap.wgsl' 6 | import gaussian from './gaussian.wgsl' 7 | import sphere from './sphere.wgsl' 8 | 9 | 10 | export class FluidRenderer { 11 | depthMapPipeline: GPURenderPipeline 12 | depthFilterPipeline: GPURenderPipeline 13 | thicknessMapPipeline: GPURenderPipeline 14 | thicknessFilterPipeline: GPURenderPipeline 15 | fluidPipeline: GPURenderPipeline 16 | spherePipeline: GPURenderPipeline 17 | 18 | depthMapTextureView: GPUTextureView 19 | tmpDepthMapTextureView: GPUTextureView 20 | thicknessTextureView: GPUTextureView 21 | tmpThicknessTextureView: GPUTextureView 22 | depthTestTextureView: GPUTextureView 23 | 24 | 25 | depthMapBindGroup: GPUBindGroup 26 | depthFilterBindGroups: GPUBindGroup[] 27 | thicknessMapBindGroup: GPUBindGroup 28 | thicknessFilterBindGroups: GPUBindGroup[] 29 | fluidBindGroup: GPUBindGroup 30 | sphereBindGroup: GPUBindGroup 31 | 32 | 33 | device: GPUDevice 34 | constructor( 35 | device: GPUDevice, canvas: HTMLCanvasElement, presentationFormat: GPUTextureFormat, 36 | radius: number, fov: number, posvelBuffer: GPUBuffer, 37 | renderUniformBuffer: GPUBuffer, cubemapTextureView: GPUTextureView 38 | ) { 39 | this.device = device 40 | const maxFilterSize = 100 41 | const blurdDepthScale = 10 42 | const diameter = 2 * radius 43 | const blurFilterSize = 12 44 | 45 | const screenConstants = { 46 | 'screenHeight': canvas.height, 47 | 'screenWidth': canvas.width, 48 | } 49 | // TODO : filter size を設定できるようにする 50 | const filterConstants = { 51 | 'depth_threshold' : radius * blurdDepthScale, 52 | 'max_filter_size' : maxFilterSize, 53 | 'projected_particle_constant' : (blurFilterSize * diameter * 0.05 * (canvas.height / 2)) / Math.tan(fov / 2), 54 | } 55 | const sampler = device.createSampler({ 56 | magFilter: 'linear', 57 | minFilter: 'linear' 58 | }); 59 | 60 | const vertexModule = device.createShaderModule({ code: fullScreen }) 61 | const depthMapModule = device.createShaderModule({ code: depthMap }) 62 | const depthFilterModule = device.createShaderModule({ code: depthFilter }) 63 | const fluidModule = device.createShaderModule({ code: fluid }) 64 | const sphereModule = device.createShaderModule({ code: sphere }) 65 | const thicknessMapModule = device.createShaderModule({ code: thicknessMap }) 66 | const thicknessFilterModule = device.createShaderModule({ code: gaussian }) 67 | 68 | // pipelines 69 | this.spherePipeline = device.createRenderPipeline({ 70 | label: 'ball pipeline', 71 | layout: 'auto', 72 | vertex: { module: sphereModule }, 73 | fragment: { 74 | module: sphereModule, 75 | targets: [ 76 | { 77 | format: presentationFormat, 78 | } 79 | ] 80 | }, 81 | primitive: { 82 | topology: 'triangle-list', 83 | }, 84 | depthStencil: { 85 | depthWriteEnabled: true, 86 | depthCompare: 'less', 87 | format: 'depth32float' 88 | } 89 | }) 90 | this.depthMapPipeline = device.createRenderPipeline({ 91 | label: 'depth map pipeline', 92 | layout: 'auto', 93 | vertex: { module: depthMapModule }, 94 | fragment: { 95 | module: depthMapModule, 96 | targets: [ 97 | { 98 | format: 'r32float', 99 | }, 100 | ], 101 | }, 102 | primitive: { 103 | topology: 'triangle-list', 104 | }, 105 | depthStencil: { 106 | depthWriteEnabled: true, 107 | depthCompare: 'less', 108 | format: 'depth32float' 109 | } 110 | }) 111 | this.depthFilterPipeline = device.createRenderPipeline({ 112 | label: 'filter pipeline', 113 | layout: 'auto', 114 | vertex: { 115 | module: vertexModule, 116 | constants: screenConstants 117 | }, 118 | fragment: { 119 | module: depthFilterModule, 120 | constants: filterConstants, 121 | targets: [ 122 | { 123 | format: 'r32float', 124 | }, 125 | ], 126 | }, 127 | primitive: { 128 | topology: 'triangle-list', 129 | }, 130 | }); 131 | this.thicknessMapPipeline = device.createRenderPipeline({ 132 | label: 'thickness map pipeline', 133 | layout: 'auto', 134 | vertex: { 135 | module: thicknessMapModule, 136 | }, 137 | fragment: { 138 | module: thicknessMapModule, 139 | targets: [ 140 | { 141 | format: 'r16float', 142 | writeMask: GPUColorWrite.RED, 143 | blend: { 144 | color: { operation: 'add', srcFactor: 'one', dstFactor: 'one' }, 145 | alpha: { operation: 'add', srcFactor: 'one', dstFactor: 'one' }, 146 | } 147 | } 148 | ], 149 | }, 150 | primitive: { 151 | topology: 'triangle-list', 152 | }, 153 | }); 154 | this.thicknessFilterPipeline = device.createRenderPipeline({ 155 | label: 'thickness filter pipeline', 156 | layout: 'auto', 157 | vertex: { 158 | module: vertexModule, 159 | constants: screenConstants 160 | }, 161 | fragment: { 162 | module: thicknessFilterModule, 163 | targets: [ 164 | { 165 | format: 'r16float', 166 | }, 167 | ], 168 | }, 169 | primitive: { 170 | topology: 'triangle-list', 171 | }, 172 | }); 173 | this.fluidPipeline = device.createRenderPipeline({ 174 | label: 'fluid rendering pipeline', 175 | layout: 'auto', 176 | vertex: { 177 | module: vertexModule, 178 | constants: screenConstants 179 | }, 180 | fragment: { 181 | module: fluidModule, 182 | targets: [ 183 | { 184 | format: presentationFormat 185 | } 186 | ], 187 | }, 188 | primitive: { 189 | topology: 'triangle-list', 190 | }, 191 | }); 192 | 193 | // textures 194 | const depthMapTexture = device.createTexture({ 195 | label: 'depth map texture', 196 | size: [canvas.width, canvas.height, 1], 197 | usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING, 198 | format: 'r32float', 199 | }); 200 | const tmpDepthMapTexture = device.createTexture({ 201 | label: 'temporary depth map texture', 202 | size: [canvas.width, canvas.height, 1], 203 | usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING, 204 | format: 'r32float', 205 | }); 206 | const thicknessTexture = device.createTexture({ 207 | label: 'thickness map texture', 208 | size: [canvas.width, canvas.height, 1], 209 | usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING, 210 | format: 'r16float', 211 | }); 212 | const tmpThicknessTexture = device.createTexture({ 213 | label: 'temporary thickness map texture', 214 | size: [canvas.width, canvas.height, 1], 215 | usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING, 216 | format: 'r16float', 217 | }); 218 | const depthTestTexture = device.createTexture({ 219 | size: [canvas.width, canvas.height, 1], 220 | format: 'depth32float', 221 | usage: GPUTextureUsage.RENDER_ATTACHMENT, 222 | }) 223 | this.depthMapTextureView = depthMapTexture.createView() 224 | this.tmpDepthMapTextureView = tmpDepthMapTexture.createView() 225 | this.thicknessTextureView = thicknessTexture.createView() 226 | this.tmpThicknessTextureView = tmpThicknessTexture.createView() 227 | this.depthTestTextureView = depthTestTexture.createView() 228 | 229 | // buffer 230 | const filterXUniformsValues = new ArrayBuffer(8); 231 | const filterYUniformsValues = new ArrayBuffer(8); 232 | const filterXUniformsViews = { blur_dir: new Float32Array(filterXUniformsValues) }; 233 | const filterYUniformsViews = { blur_dir: new Float32Array(filterYUniformsValues) }; 234 | filterXUniformsViews.blur_dir.set([1.0, 0.0]); 235 | filterYUniformsViews.blur_dir.set([0.0, 1.0]); 236 | const filterXUniformBuffer = device.createBuffer({ 237 | label: 'filter uniform buffer', 238 | size: filterXUniformsValues.byteLength, 239 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 240 | }) 241 | const filterYUniformBuffer = device.createBuffer({ 242 | label: 'filter uniform buffer', 243 | size: filterYUniformsValues.byteLength, 244 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 245 | }) 246 | device.queue.writeBuffer(filterXUniformBuffer, 0, filterXUniformsValues); 247 | device.queue.writeBuffer(filterYUniformBuffer, 0, filterYUniformsValues); 248 | 249 | // bindGroup 250 | this.depthMapBindGroup = device.createBindGroup({ 251 | label: 'depth map bind group', 252 | layout: this.depthMapPipeline.getBindGroupLayout(0), 253 | entries: [ 254 | { binding: 0, resource: { buffer: posvelBuffer }}, 255 | { binding: 1, resource: { buffer: renderUniformBuffer }}, 256 | ] 257 | }) 258 | this.depthFilterBindGroups = [] 259 | this.depthFilterBindGroups = [ 260 | device.createBindGroup({ 261 | label: 'filterX bind group', 262 | layout: this.depthFilterPipeline.getBindGroupLayout(0), 263 | entries: [ 264 | // { binding: 0, resource: sampler }, 265 | { binding: 1, resource: this.depthMapTextureView }, // 元の領域から読み込む 266 | { binding: 2, resource: { buffer: filterXUniformBuffer } }, 267 | ], 268 | }), 269 | device.createBindGroup({ 270 | label: 'filterY bind group', 271 | layout: this.depthFilterPipeline.getBindGroupLayout(0), 272 | entries: [ 273 | // { binding: 0, resource: sampler }, 274 | { binding: 1, resource: this.tmpDepthMapTextureView }, // 一時領域から読み込む 275 | { binding: 2, resource: { buffer: filterYUniformBuffer }} 276 | ], 277 | }) 278 | ]; 279 | this.thicknessMapBindGroup = device.createBindGroup({ 280 | label: 'thickness map bind group', 281 | layout: this.thicknessMapPipeline.getBindGroupLayout(0), 282 | entries: [ 283 | { binding: 0, resource: { buffer: posvelBuffer }}, 284 | { binding: 1, resource: { buffer: renderUniformBuffer }}, 285 | ], 286 | }) 287 | this.thicknessFilterBindGroups = [] 288 | this.thicknessFilterBindGroups = [ 289 | device.createBindGroup({ 290 | label: 'thickness filterX bind group', 291 | layout: this.thicknessFilterPipeline.getBindGroupLayout(0), 292 | entries: [ 293 | // { binding: 0, resource: sampler }, 294 | { binding: 1, resource: this.thicknessTextureView }, 295 | { binding: 2, resource: { buffer: filterXUniformBuffer } }, 296 | ], 297 | }), 298 | device.createBindGroup({ 299 | label: 'thickness filterY bind group', 300 | layout: this.thicknessFilterPipeline.getBindGroupLayout(0), 301 | entries: [ 302 | // { binding: 0, resource: sampler }, 303 | { binding: 1, resource: this.tmpThicknessTextureView }, 304 | { binding: 2, resource: { buffer: filterYUniformBuffer } }, 305 | ], 306 | }), 307 | ] 308 | 309 | this.fluidBindGroup = device.createBindGroup({ 310 | label: 'fluid bind group', 311 | layout: this.fluidPipeline.getBindGroupLayout(0), 312 | entries: [ 313 | { binding: 0, resource: sampler }, 314 | { binding: 1, resource: this.depthMapTextureView }, 315 | { binding: 2, resource: { buffer: renderUniformBuffer } }, 316 | { binding: 3, resource: this.thicknessTextureView }, 317 | { binding: 4, resource: cubemapTextureView }, 318 | ], 319 | }) 320 | 321 | this.sphereBindGroup = device.createBindGroup({ 322 | label: 'ball bind group', 323 | layout: this.spherePipeline.getBindGroupLayout(0), 324 | entries: [ 325 | { binding: 0, resource: { buffer: posvelBuffer }}, 326 | { binding: 1, resource: { buffer: renderUniformBuffer }}, 327 | ] 328 | }) 329 | } 330 | 331 | 332 | execute(context: GPUCanvasContext, commandEncoder: GPUCommandEncoder, 333 | numParticles: number, sphereRenderFl: boolean) 334 | { 335 | // これらも前もって作っておけるんじゃないか? 336 | const depthMapPassDescriptor: GPURenderPassDescriptor = { 337 | colorAttachments: [ 338 | { 339 | view: this.depthMapTextureView, 340 | clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, 341 | loadOp: 'clear', 342 | storeOp: 'store', 343 | }, 344 | ], 345 | depthStencilAttachment: { 346 | view: this.depthTestTextureView, 347 | depthClearValue: 1.0, 348 | depthLoadOp: 'clear', 349 | depthStoreOp: 'store', 350 | }, 351 | } 352 | 353 | const depthFilterPassDescriptors: GPURenderPassDescriptor[] = [ 354 | { 355 | colorAttachments: [ 356 | { 357 | view: this.tmpDepthMapTextureView, 358 | clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, 359 | loadOp: 'clear', 360 | storeOp: 'store', 361 | }, 362 | ], 363 | }, 364 | { 365 | colorAttachments: [ 366 | { 367 | view: this.depthMapTextureView, 368 | clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, 369 | loadOp: 'clear', 370 | storeOp: 'store', 371 | }, 372 | ], 373 | } 374 | ] 375 | 376 | const thicknessMapPassDescriptor: GPURenderPassDescriptor = { 377 | colorAttachments: [ 378 | { 379 | view: this.thicknessTextureView, 380 | clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, 381 | loadOp: 'clear', 382 | storeOp: 'store', 383 | }, 384 | ], 385 | } 386 | 387 | const thicknessFilterPassDescriptors: GPURenderPassDescriptor[] = [ 388 | { 389 | colorAttachments: [ 390 | { 391 | view: this.tmpThicknessTextureView, // 一時領域へ書き込み 392 | clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, 393 | loadOp: 'clear', 394 | storeOp: 'store', 395 | }, 396 | ], 397 | }, 398 | { 399 | colorAttachments: [ 400 | { 401 | view: this.thicknessTextureView, // Y のパスはもとに戻す 402 | clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, 403 | loadOp: 'clear', 404 | storeOp: 'store', 405 | }, 406 | ], 407 | } 408 | ] 409 | 410 | const fluidPassDescriptor: GPURenderPassDescriptor = { 411 | colorAttachments: [ 412 | { 413 | view: context.getCurrentTexture().createView(), 414 | clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, 415 | loadOp: 'clear', 416 | storeOp: 'store', 417 | }, 418 | ], 419 | } 420 | 421 | const spherePassDescriptor: GPURenderPassDescriptor = { 422 | colorAttachments: [ 423 | { 424 | view: context.getCurrentTexture().createView(), 425 | clearValue: { r: 0.8, g: 0.8, b: 0.8, a: 1.0 }, 426 | loadOp: 'clear', 427 | storeOp: 'store', 428 | }, 429 | ], 430 | depthStencilAttachment: { 431 | view: this.depthTestTextureView, 432 | depthClearValue: 1.0, 433 | depthLoadOp: 'clear', 434 | depthStoreOp: 'store', 435 | }, 436 | } 437 | 438 | if (!sphereRenderFl) { 439 | const depthMapPassEncoder = commandEncoder.beginRenderPass(depthMapPassDescriptor); 440 | depthMapPassEncoder.setBindGroup(0, this.depthMapBindGroup); 441 | depthMapPassEncoder.setPipeline(this.depthMapPipeline); 442 | depthMapPassEncoder.draw(6, numParticles); 443 | depthMapPassEncoder.end(); 444 | for (var iter = 0; iter < 4; iter++) { 445 | const depthFilterPassEncoderX = commandEncoder.beginRenderPass(depthFilterPassDescriptors[0]); 446 | depthFilterPassEncoderX.setBindGroup(0, this.depthFilterBindGroups[0]); 447 | depthFilterPassEncoderX.setPipeline(this.depthFilterPipeline); 448 | depthFilterPassEncoderX.draw(6); 449 | depthFilterPassEncoderX.end(); 450 | const filterPassEncoderY = commandEncoder.beginRenderPass(depthFilterPassDescriptors[1]); 451 | filterPassEncoderY.setBindGroup(0, this.depthFilterBindGroups[1]); 452 | filterPassEncoderY.setPipeline(this.depthFilterPipeline); 453 | filterPassEncoderY.draw(6); 454 | filterPassEncoderY.end(); 455 | } 456 | 457 | const thicknessMapPassEncoder = commandEncoder.beginRenderPass(thicknessMapPassDescriptor); 458 | thicknessMapPassEncoder.setBindGroup(0, this.thicknessMapBindGroup); 459 | thicknessMapPassEncoder.setPipeline(this.thicknessMapPipeline); 460 | thicknessMapPassEncoder.draw(6, numParticles); 461 | thicknessMapPassEncoder.end(); 462 | 463 | for (var iter = 0; iter < 1; iter++) { // 多いか? 464 | const thicknessFilterPassEncoderX = commandEncoder.beginRenderPass(thicknessFilterPassDescriptors[0]); 465 | thicknessFilterPassEncoderX.setBindGroup(0, this.thicknessFilterBindGroups[0]); 466 | thicknessFilterPassEncoderX.setPipeline(this.thicknessFilterPipeline); 467 | thicknessFilterPassEncoderX.draw(6); 468 | thicknessFilterPassEncoderX.end(); 469 | const thicknessFilterPassEncoderY = commandEncoder.beginRenderPass(thicknessFilterPassDescriptors[1]); 470 | thicknessFilterPassEncoderY.setBindGroup(0, this.thicknessFilterBindGroups[1]); 471 | thicknessFilterPassEncoderY.setPipeline(this.thicknessFilterPipeline); 472 | thicknessFilterPassEncoderY.draw(6); 473 | thicknessFilterPassEncoderY.end(); 474 | } 475 | 476 | const fluidPassEncoder = commandEncoder.beginRenderPass(fluidPassDescriptor); 477 | fluidPassEncoder.setBindGroup(0, this.fluidBindGroup); 478 | fluidPassEncoder.setPipeline(this.fluidPipeline); 479 | fluidPassEncoder.draw(6); 480 | fluidPassEncoder.end(); 481 | } else { 482 | const spherePassEncoder = commandEncoder.beginRenderPass(spherePassDescriptor); 483 | spherePassEncoder.setBindGroup(0, this.sphereBindGroup); 484 | spherePassEncoder.setPipeline(this.spherePipeline); 485 | spherePassEncoder.draw(6, numParticles); 486 | spherePassEncoder.end(); 487 | } 488 | } 489 | } --------------------------------------------------------------------------------