Babylon.js WebGPU Cloth Simulation
Babylon.js WebGPU を用いて、XPBD(extended position-based dynamics) による Cloth シミュレーションを行なってみました。
XPBD の計算は、compute shader で GPU 計算を行なっています。
XPBD による Cloth シミュレーションは、下記を参照しました。
https://matthias-research.github.io/pages/
Ten Minute Physics
14 The secret of cloth simulation
16 Simulation on the GPU
14 では、XPBD は CPU 計算を行い、メッシュオブジェクトの検出に Three.js のレイキャスト を使っています。
16 では、Python 言語でプログラムを記述しており、Warp ライブラリーを用いて XPBD のGPU 計算を行なっています。メッシュオブジェクトの検出には、OpenGL と GLU、GLUT ライブラリーを用いています。
ここでは、この2つを組み合わせる形で、Babylon.js の WebGPU を用いて、compute shader で XPBD の GPU 計算を行い、レイキャストを用いてメッシュオブジェクトの検出を行なっている。
実行結果
プログラム
ClothSim-xpbd-WebGPU.html
<!DOCTYPE html> <head> <title>Babylon.js Cloth Sim</title> <script src="https://preview.babylonjs.com/babylon.js"></script> <script src="customMesh3D.js"></script> </head> <body> <canvas id="renderCanvas" width="450" height="450"></canvas> <script> async function init() { const canvas = document.getElementById("renderCanvas"); const engine = new BABYLON.WebGPUEngine(canvas); await engine.initAsync(); const scene = new BABYLON.Scene(engine); scene.clearColor = new BABYLON.Color3(0.8, 0.8, 0.8); const camera = new BABYLON.ArcRotateCamera("camera", -Math.PI / 2, Math.PI / 2, 8, new BABYLON.Vector3(0, 0.0, 0), scene); camera.attachControl(canvas, true); const light = new BABYLON.HemisphericLight("light1", new BABYLON.Vector3(0, 1, 0), scene); light.intensity = 0.8; // Constants let gravity = [0.0, -10.0, 0.0]; let dt = 1.0 / 60.0; let numSubsteps = 5; let cwidth = 2; let cheight = 3; let cdivisions = 50; cdivisions = (cdivisions%2 == 0) ? cdivisions : cdivisions - 1; let stretchCompliance = 0.0000001; let bendingCompliance = 0.5; class Cloth { constructor(mesh, scene) { let meshMat = new BABYLON.StandardMaterial("meshMat", scene); meshMat.diffuseColor = BABYLON.Color3.Red(); meshMat.backFaceCulling = false; this.customMesh = new BABYLON.Mesh("custom", scene); this.customMesh.material = meshMat; this.customMesh.userData = this; // for raycasting let vertexData = new BABYLON.VertexData(); vertexData.positions = mesh.vertices; vertexData.indices = mesh.indices; vertexData.applyToMesh(this.customMesh, true); this.numParticles = mesh.vertices.length / 3; console.log("numParticles", this.numParticles); this.iw_max = cdivisions + 1; this.ih_max = cdivisions + 1; let defaultMass = 1.0; this.defaultSpacingX = cwidth / cdivisions; this.defaultSpacingY = cheight / cdivisions; // cpu data this.points = []; for (let i = 0; i < this.numParticles; ++i) { let iw = i % this.iw_max; let ih = Math.floor(i / this.iw_max); let invMass = ((ih == 0) && (iw == 0) || (ih == 0) && (iw == this.iw_max-1)) ? 0 : 1 / defaultMass; this.points.push({ pos: new BABYLON.Vector3( mesh.vertices[3 * i + 0], mesh.vertices[3 * i + 1], mesh.vertices[3 * i + 2]), vel: new BABYLON.Vector3(0, 0, 0), prevPos: new BABYLON.Vector3( mesh.vertices[3 * i + 0], mesh.vertices[3 * i + 1], mesh.vertices[3 * i + 2]), invMass: invMass, }); } // buffer data this.particleData = new Float32Array(this.numParticles * 16); for (let i = 0; i < this.numParticles; ++i) { let p = this.points[i]; // pos this.particleData[16 * i + 0] = p.pos.x; this.particleData[16 * i + 1] = p.pos.y; this.particleData[16 * i + 2] = p.pos.z; this.particleData[16 * i + 3] = 1.0; // vel this.particleData[16 * i + 4] = p.vel.x; this.particleData[16 * i + 5] = p.vel.y; this.particleData[16 * i + 6] = p.vel.z; this.particleData[16 * i + 7] = 1.0; // prePos this.particleData[16 * i + 8] = p.prevPos.x; this.particleData[16 * i + 9] = p.prevPos.y; this.particleData[16 * i + 10] = p.prevPos.z; this.particleData[16 * i + 11] = 1.0; // invMass this.particleData[16 * i + 12] = p.invMass; // dummy this.particleData[16 * i + 13] = 0.0; this.particleData[16 * i + 14] = 0.0; this.particleData[16 * i + 15] = 0.0; } console.log("particleData", Array.from(this.particleData)); this.particleBuffers = new BABYLON.StorageBuffer(engine, this.particleData.byteLength, BABYLON.Constants.BUFFER_CREATIONFLAG_VERTEX | BABYLON.Constants.BUFFER_CREATIONFLAG_READWRITE); this.particleBuffers.update(this.particleData); this.grabId = -1; this.grabInvMass = 0.0; this.grabIdData = new Int32Array([this.grabId]); this.grabIdBuffer = new BABYLON.StorageBuffer(engine, this.grabIdData.byteLength, BABYLON.Constants.BUFFER_CREATIONFLAG_READWRITE); this.grabIdBuffer.update(this.grabIdData); this.grabInvMassData = new Float32Array([this.grabInvMass]); this.grabInvMassBuffer = new BABYLON.StorageBuffer(engine, this.grabInvMassData.byteLength, BABYLON.Constants.BUFFER_CREATIONFLAG_READWRITE); this.grabInvMassBuffer.update(this.grabInvMassData); // Uniform buffer this.uBuffer = new BABYLON.UniformBuffer(engine, undefined, undefined, "uBuffer"); this.uBuffer.addUniform("passNr", 1); // uniform startGrab this.grab1UBuffer = new BABYLON.UniformBuffer(engine, undefined, undefined, "grab1UBuffer"); this.grab1UBuffer.addUniform("pos", 3); // unihorm moveGrabbed this.grab12UBuffer = new BABYLON.UniformBuffer(engine, undefined, undefined, "grab12UBuffer"); this.grab12UBuffer.addUniform("pos", 3); // uniform endGrab this.grab2UBuffer = new BABYLON.UniformBuffer(engine, undefined, undefined, "grab2UBuffer"); this.grab2UBuffer.addUniform("vel", 3); this.setComputeShader(); } setComputeShader() { const computeShader = { integrate:` struct Particle { pos : vec3<f32>, vel : vec3<f32>, prePos : vec3<f32>, @align(16) invMass: f32, }; @group(0) @binding(0) var<storage, read_write> particles : array<Particle>; @compute @workgroup_size(64) fn main(@builtin(global_invocation_id) GlobalInvocationID : vec3<u32>) { let numSubsteps: u32 = ${numSubsteps}; let dt: f32 = ${dt}; let sdt: f32 = dt / f32(numSubsteps); let gravity: f32 = ${gravity[1]}; var index : u32 = GlobalInvocationID.x; let vInvMass: f32 = particles[index].invMass; if (vInvMass == 0.0) { return; } var vPos : vec3<f32> = particles[index].pos; var vVel : vec3<f32> = particles[index].vel; vVel.y += gravity * sdt; particles[index].prePos = vPos; vPos = vPos + vVel * sdt; particles[index].pos = vPos; particles[index].vel = vVel; }`, solveStretchConst1:` struct Params { passNr: u32, }; @group(0) @binding(0) var<uniform> params : Params; struct Particle { pos : vec3<f32>, vel : vec3<f32>, prePos : vec3<f32>, @align(16) invMass: f32, }; @group(0) @binding(1) var<storage, read_write> particles : array<Particle>; @compute @workgroup_size(1) // dispatch iw_max fn main(@builtin(global_invocation_id) GlobalInvocationID : vec3<u32>) { let numSubsteps: u32 = ${numSubsteps}; let dt: f32 = ${dt}; let sdt: f32 = dt / f32(numSubsteps); let d0: f32 = ${this.defaultSpacingY}; let compliance: f32 = ${stretchCompliance}; let iw_max: u32 = ${this.iw_max}; let ih_max: u32 = ${this.ih_max}; let passNr: u32 = params.passNr; var index : u32 = GlobalInvocationID.x; for (var ih: u32 = passNr; ih <= (ih_max - 1u) - 2u + passNr; ih = ih + 2u) { var id0: u32 = index + ih * iw_max; var id1: u32 = index + (ih + 1u) * iw_max; var w0: f32 = particles[id0].invMass; var w1: f32 = particles[id1].invMass; var w: f32 = w0 + w1; if (w == 0.0) {return;} var p0 : vec3<f32> = particles[id0].pos; var p1 : vec3<f32> = particles[id1].pos; var grad: vec3<f32> = p1 - p0; var d: f32 = length(grad); let alpha: f32 = compliance / sdt / sdt; var lambda: f32 = (d - d0) / (w + alpha); grad = normalize(grad); var delta_p0: vec3<f32> = grad * (w0 * lambda); particles[id0].pos = p0 + delta_p0; var delta_p1: vec3<f32> = grad * (-w1 * lambda); particles[id1].pos = p1 + delta_p1; } }`, solveStretchConst2:` struct Params { passNr: u32, }; @group(0) @binding(0) var<uniform> params : Params; struct Particle { pos : vec3<f32>, vel : vec3<f32>, prePos : vec3<f32>, @align(16) invMass: f32, }; @group(0) @binding(1) var<storage, read_write> particles : array<Particle>; @compute @workgroup_size(1) // dispatch ih_max fn main(@builtin(global_invocation_id) GlobalInvocationID : vec3<u32>) { let numSubsteps: u32 = ${numSubsteps}; let dt: f32 = ${dt}; let sdt: f32 = dt / f32(numSubsteps); let d0: f32 = ${this.defaultSpacingX}; let compliance: f32 = ${stretchCompliance}; let iw_max: u32 = ${this.iw_max}; let passNr: u32 = params.passNr; var index : u32 = GlobalInvocationID.x; for (var iw: u32 = passNr; iw <= (iw_max - 1u) - 2u + passNr; iw = iw + 2u) { var id0: u32 = iw + index * iw_max; var id1: u32 = (iw + 1u) + index * iw_max; var w0: f32 = particles[id0].invMass; var w1: f32 = particles[id1].invMass; var w: f32 = w0 + w1; if (w == 0.0) {return;} var p0 : vec3<f32> = particles[id0].pos; var p1 : vec3<f32> = particles[id1].pos; var grad: vec3<f32> = p1 - p0; var d: f32 = length(grad); let alpha: f32 = compliance / sdt / sdt; var lambda: f32 = (d - d0) / (w + alpha); grad = normalize(grad); var delta_p0: vec3<f32> = grad * (w0 * lambda); particles[id0].pos = p0 + delta_p0; var delta_p1: vec3<f32> = grad * (-w1 * lambda); particles[id1].pos = p1 + delta_p1; } }`, solveShearConst1:` struct Params { passNr: u32, }; @group(0) @binding(0) var<uniform> params : Params; struct Particle { pos : vec3<f32>, vel : vec3<f32>, prePos : vec3<f32>, @align(16) invMass: f32, }; @group(0) @binding(1) var<storage, read_write> particles : array<Particle>; @compute @workgroup_size(1) // dispatch iw_max-1 fn main(@builtin(global_invocation_id) GlobalInvocationID : vec3<u32>) { let numSubsteps: u32 = ${numSubsteps}; let dt: f32 = ${dt}; let sdt: f32 = dt / f32(numSubsteps); let d0X: vec2<f32> = vec2(${this.defaultSpacingX}, 0); let d0Y: vec2<f32> = vec2(0, ${this.defaultSpacingY}); let d0: f32 = length(d0X - d0Y); let compliance: f32 = ${stretchCompliance}; let iw_max: u32 = ${this.iw_max}; let ih_max: u32 = ${this.ih_max}; let passNr: u32 = params.passNr; var index : u32 = GlobalInvocationID.x; for (var ih: u32 = passNr; ih <= (ih_max - 1u) - 2u + passNr; ih = ih + 2u) { var id0: u32 = index + ih * iw_max; var id1: u32 = (index + 1u) + (ih + 1u) * iw_max; var w0: f32 = particles[id0].invMass; var w1: f32 = particles[id1].invMass; var w: f32 = w0 + w1; if (w == 0.0) {return;} var p0 : vec3<f32> = particles[id0].pos; var p1 : vec3<f32> = particles[id1].pos; var grad: vec3<f32> = p1 - p0; var d: f32 = length(grad); let alpha: f32 = compliance / sdt / sdt; var lambda: f32 = (d - d0) / (w + alpha); grad = normalize(grad); var delta_p0: vec3<f32> = grad * (w0 * lambda); particles[id0].pos = p0 + delta_p0; var delta_p1: vec3<f32> = grad * (-w1 * lambda); particles[id1].pos = p1 + delta_p1; } }`, solveShearConst2:` struct Params { passNr: u32, }; @group(0) @binding(0) var<uniform> params : Params; struct Particle { pos : vec3<f32>, vel : vec3<f32>, prePos : vec3<f32>, @align(16) invMass: f32, }; @group(0) @binding(1) var<storage, read_write> particles : array<Particle>; @compute @workgroup_size(1) // dispatch ih_max-1 fn main(@builtin(global_invocation_id) GlobalInvocationID : vec3<u32>) { let numSubsteps: u32 = ${numSubsteps}; let dt: f32 = ${dt}; let sdt: f32 = dt / f32(numSubsteps); let d0X: vec2<f32> = vec2(${this.defaultSpacingX}, 0); let d0Y: vec2<f32> = vec2(0, ${this.defaultSpacingY}); let d0: f32 = length(d0X + d0Y); let compliance: f32 = ${stretchCompliance}; let iw_max: u32 = ${this.iw_max}; let passNr: u32 = params.passNr; var index : u32 = GlobalInvocationID.x; for (var iw: u32 = passNr; iw <= (iw_max - 1u) - 2u + passNr; iw = iw + 2u) { var id0: u32 = (iw + 1u) + index * iw_max; var id1: u32 = iw + (index + 1u) * iw_max; var w0: f32 = particles[id0].invMass; var w1: f32 = particles[id1].invMass; var w: f32 = w0 + w1; if (w == 0.0) {return;} var p0 : vec3<f32> = particles[id0].pos; var p1 : vec3<f32> = particles[id1].pos; var grad: vec3<f32> = p1 - p0; var d: f32 = length(grad); let alpha: f32 = compliance / sdt / sdt; var lambda: f32 = (d - d0) / (w + alpha); grad = normalize(grad); var delta_p0: vec3<f32> = grad * (w0 * lambda); particles[id0].pos = p0 + delta_p0; var delta_p1: vec3<f32> = grad * (-w1 * lambda); particles[id1].pos = p1 + delta_p1; } }`, solveBendConst1:` struct Params { passNr: u32, }; @group(0) @binding(0) var<uniform> params : Params; struct Particle { pos : vec3<f32>, vel : vec3<f32>, prePos : vec3<f32>, @align(16) invMass: f32, }; @group(0) @binding(1) var<storage, read_write> particles : array<Particle>; @compute @workgroup_size(1) // dispatch iw_max fn main(@builtin(global_invocation_id) GlobalInvocationID : vec3<u32>) { let numSubsteps: u32 = ${numSubsteps}; let dt: f32 = ${dt}; let sdt: f32 = dt / f32(numSubsteps); let spacingY: f32 = ${this.defaultSpacingY}; let d0: f32 = spacingY * 2; let compliance: f32 = ${bendingCompliance}; let iw_max: u32 = ${this.iw_max}; let ih: u32 = params.passNr; var index : u32 = GlobalInvocationID.x; var id0: u32 = index + ih * iw_max; var id1: u32 = index + (ih + 2u) * iw_max; var w0: f32 = particles[id0].invMass; var w1: f32 = particles[id1].invMass; var w: f32 = w0 + w1; if (w == 0.0) {return;} var p0 : vec3<f32> = particles[id0].pos; var p1 : vec3<f32> = particles[id1].pos; var grad: vec3<f32> = p1 - p0; var d: f32 = length(grad); let alpha: f32 = compliance / sdt / sdt; var lambda: f32 = (d - d0) / (w + alpha); grad = normalize(grad); var delta_p0: vec3<f32> = grad * (w0 * lambda); particles[id0].pos = p0 + delta_p0; var delta_p1: vec3<f32> = grad * (-w1 * lambda); particles[id1].pos = p1 + delta_p1; }`, solveBendConst2:` struct Params { passNr: u32, }; @group(0) @binding(0) var<uniform> params : Params; struct Particle { pos : vec3<f32>, vel : vec3<f32>, prePos : vec3<f32>, @align(16) invMass: f32, }; @group(0) @binding(1) var<storage, read_write> particles : array<Particle>; @compute @workgroup_size(1) // dispatch ih_max fn main(@builtin(global_invocation_id) GlobalInvocationID : vec3<u32>) { let numSubsteps: u32 = ${numSubsteps}; let dt: f32 = ${dt}; let sdt: f32 = dt / f32(numSubsteps); let spacingX: f32 = ${this.defaultSpacingX}; let d0: f32 = spacingX * 2; let compliance: f32 = ${bendingCompliance}; let iw_max: u32 = ${this.iw_max}; let iw: u32 = params.passNr; var index : u32 = GlobalInvocationID.x; var id0: u32 = iw + index * iw_max; var id1: u32 = (iw + 2u) + index * iw_max; var w0: f32 = particles[id0].invMass; var w1: f32 = particles[id1].invMass; var w: f32 = w0 + w1; if (w == 0.0) {return;} var p0 : vec3<f32> = particles[id0].pos; var p1 : vec3<f32> = particles[id1].pos; var grad: vec3<f32> = p1 - p0; var d: f32 = length(grad); let alpha: f32 = compliance / sdt / sdt; var lambda: f32 = (d - d0) / (w + alpha); grad = normalize(grad); var delta_p0: vec3<f32> = grad * (w0 * lambda); particles[id0].pos = p0 + delta_p0; var delta_p1: vec3<f32> = grad * (-w1 * lambda); particles[id1].pos = p1 + delta_p1; }`, updateVel:` struct Particle { pos : vec3<f32>, vel : vec3<f32>, prePos : vec3<f32>, @align(16) invMass: f32, }; @group(0) @binding(0) var<storage, read_write> particles : array<Particle>; @compute @workgroup_size(64) fn main(@builtin(global_invocation_id) GlobalInvocationID : vec3<u32>) { let numSubsteps: u32 = ${numSubsteps}; let dt: f32 = ${dt}; let sdt: f32 = dt / f32(numSubsteps); var index : u32 = GlobalInvocationID.x; let vInvMass: f32 = particles[index].invMass; if (vInvMass == 0.0) { return; } var vPos : vec3<f32> = particles[index].pos; var vPrePos : vec3<f32> = particles[index].prePos; var vVel : vec3<f32> = (vPos - vPrePos) / sdt; particles[index].vel = vVel; }`, startGrab:` struct Particle { pos : vec3<f32>, vel : vec3<f32>, prePos : vec3<f32>, @align(16) invMass: f32, }; @group(0) @binding(0) var<storage, read_write> particles : array<Particle>; @group(0) @binding(1) var<storage, read_write> grabId : i32; @group(0) @binding(2) var<storage, read_write> grabInvMass : f32; struct Params { pos: vec3<f32>, }; @group(0) @binding(3) var<uniform> params : Params; @compute @workgroup_size(1) // dispatch: 1 fn main(@builtin(global_invocation_id) GlobalInvocationID : vec3<u32>) { var index: u32 = GlobalInvocationID.x; let pos: vec3<f32> = params.pos; let gId: i32 = grabId; var gInvMass: f32 = 0.0; if (gId >= 0) { gInvMass = particles[gId].invMass; particles[gId].invMass = 0.0; particles[gId].pos = pos; } grabInvMass = gInvMass; }`, moveGrabbed:` struct Particle { pos : vec3<f32>, vel : vec3<f32>, prePos : vec3<f32>, @align(16) invMass: f32, }; @group(0) @binding(0) var<storage, read_write> particles : array<Particle>; @group(0) @binding(1) var<storage, read_write> grabId : i32; struct Params { pos: vec3<f32>, }; @group(0) @binding(2) var<uniform> params : Params; @compute @workgroup_size(1) // dispatch: 1 fn main(@builtin(global_invocation_id) GlobalInvocationID : vec3<u32>) { var index: u32 = GlobalInvocationID.x; let pos: vec3<f32> = params.pos; let gId: i32 = grabId; if (gId >= 0) { particles[gId].pos = pos; } }`, endGrab:` struct Particle { pos : vec3<f32>, vel : vec3<f32>, prePos : vec3<f32>, @align(16) invMass: f32, }; @group(0) @binding(0) var<storage, read_write> particles : array<Particle>; @group(0) @binding(1) var<storage, read_write> grabId : i32; @group(0) @binding(2) var<storage, read_write> grabInvMass : f32; struct Params { vel: vec3<f32>, }; @group(0) @binding(3) var<uniform> params : Params; @compute @workgroup_size(1) // dispatch: 1 fn main(@builtin(global_invocation_id) GlobalInvocationID : vec3<u32>) { var index: u32 = GlobalInvocationID.x; let vel: vec3<f32> = params.vel; let gId: i32 = grabId; if (gId >= 0) { particles[gId].invMass = grabInvMass; particles[gId].vel = vel; } grabId = -1; }` }; // cs integrate this.csIntegrate = new BABYLON.ComputeShader("csIntegrate", engine, { computeSource: computeShader.integrate }, { bindingsMapping: { "particle": { group: 0, binding: 0 }, } }); this.csIntegrate.setStorageBuffer("particle", this.particleBuffers); // cs solveStretchConst1 this.csSolveStretchConst1 = []; for (let passNr = 0; passNr < 2; passNr++) { const csSolve1 = new BABYLON.ComputeShader("csSolve1", engine, { computeSource: computeShader.solveStretchConst1 }, { bindingsMapping: { "params": { group: 0, binding: 0 }, "particle": { group: 0, binding: 1 }, } }); csSolve1.setUniformBuffer("params", this.uBuffer); csSolve1.setStorageBuffer("particle", this.particleBuffers); this.csSolveStretchConst1.push(csSolve1); } // cs solveStretchConst2 this.csSolveStretchConst2 = []; for (let passNr = 0; passNr < 2; passNr++) { const csSolve2 = new BABYLON.ComputeShader("csSolve2", engine, { computeSource: computeShader.solveStretchConst2 }, { bindingsMapping: { "params": { group: 0, binding: 0 }, "particle": { group: 0, binding: 1 }, } }); csSolve2.setUniformBuffer("params", this.uBuffer); csSolve2.setStorageBuffer("particle", this.particleBuffers); this.csSolveStretchConst2.push(csSolve2); } // cs solveShearConst1 this.csSolveShearConst1 = []; for (let passNr = 0; passNr < 2; passNr++) { const csShear1 = new BABYLON.ComputeShader("csShear1", engine, { computeSource: computeShader.solveShearConst1 }, { bindingsMapping: { "params": { group: 0, binding: 0 }, "particle": { group: 0, binding: 1 }, } }); csShear1.setUniformBuffer("params", this.uBuffer); csShear1.setStorageBuffer("particle", this.particleBuffers); this.csSolveShearConst1.push(csShear1); } this.csSolveShearConst2 = []; for (let passNr = 0; passNr < 2; passNr++) { const csShear2 = new BABYLON.ComputeShader("csShear2", engine, { computeSource: computeShader.solveShearConst2 }, { bindingsMapping: { "params": { group: 0, binding: 0 }, "particle": { group: 0, binding: 1 }, } }); csShear2.setUniformBuffer("params", this.uBuffer); csShear2.setStorageBuffer("particle", this.particleBuffers); this.csSolveShearConst2.push(csShear2); } // cs solveBendConst1 this.csSolveBendConst1 = []; // passNr -> ih for (let ih = 0; ih < (this.ih_max-1) - 1; ih++) { const csBend1 = new BABYLON.ComputeShader("csBend1", engine, { computeSource: computeShader.solveBendConst1 }, { bindingsMapping: { "params": { group: 0, binding: 0 }, "particle": { group: 0, binding: 1 }, } }); csBend1.setUniformBuffer("params", this.uBuffer); csBend1.setStorageBuffer("particle", this.particleBuffers); this.csSolveBendConst1.push(csBend1); } // cs solveBendConst2 this.csSolveBendConst2 = []; // passNr -> iw for (let iw = 0; iw < (this.iw_max-1) - 1; iw++) { const csBend2 = new BABYLON.ComputeShader("csBend2", engine, { computeSource: computeShader.solveBendConst2 }, { bindingsMapping: { "params": { group: 0, binding: 0 }, "particle": { group: 0, binding: 1 }, } }); csBend2.setUniformBuffer("params", this.uBuffer); csBend2.setStorageBuffer("particle", this.particleBuffers); this.csSolveBendConst2.push(csBend2); } // cs updateVel this.csUpdateVel = new BABYLON.ComputeShader("csUpdateVel", engine, { computeSource: computeShader.updateVel }, { bindingsMapping: { "particle": { group: 0, binding: 0 }, } }); this.csUpdateVel.setStorageBuffer("particle", this.particleBuffers); // cs startGrab this.csStartGrab = new BABYLON.ComputeShader("csStartGrab", engine, { computeSource: computeShader.startGrab }, { bindingsMapping: { "particle": { group: 0, binding: 0 }, "grabId": { group: 0, binding: 1 }, "grabInvMass": { group: 0, binding: 2 }, "params": { group: 0, binding: 3 }, } }); this.csStartGrab.setStorageBuffer("particle", this.particleBuffers); this.csStartGrab.setStorageBuffer("grabId", this.grabIdBuffer); this.csStartGrab.setStorageBuffer("grabInvMass", this.grabInvMassBuffer); this.csStartGrab.setUniformBuffer("params", this.grab1UBuffer); // cs moveGrabbed this.csMoveGrabbed = new BABYLON.ComputeShader("csMoveGrabbed", engine, { computeSource: computeShader.moveGrabbed }, { bindingsMapping: { "particle": { group: 0, binding: 0 }, "grabId": { group: 0, binding: 1 }, "params": { group: 0, binding: 2 }, } }); this.csMoveGrabbed.setStorageBuffer("particle", this.particleBuffers); this.csMoveGrabbed.setStorageBuffer("grabId", this.grabIdBuffer); this.csMoveGrabbed.setUniformBuffer("params", this.grab12UBuffer); // cs endGrab this.csEndGrab = new BABYLON.ComputeShader("csEndGrab", engine, { computeSource: computeShader.endGrab }, { bindingsMapping: { "particle": { group: 0, binding: 0 }, "grabId": { group: 0, binding: 1 }, "grabInvMass": { group: 0, binding: 2 }, "params": { group: 0, binding: 3 }, } }); this.csEndGrab.setStorageBuffer("particle", this.particleBuffers); this.csEndGrab.setStorageBuffer("grabId", this.grabIdBuffer); this.csEndGrab.setStorageBuffer("grabInvMass", this.grabInvMassBuffer); this.csEndGrab.setUniformBuffer("params", this.grab2UBuffer); } updateParams(passNr) { this.uBuffer.updateInt("passNr", passNr); this.uBuffer.update(); } simulate() { // Integrate this.csIntegrate.dispatch(Math.ceil(this.numParticles / 64)); // solve stretch constraints for (let passNr = 0; passNr < 2; passNr++) { this.updateParams(passNr); this.csSolveStretchConst1[passNr].dispatch(this.iw_max); } for (let passNr = 0; passNr < 2; passNr++) { this.updateParams(passNr); this.csSolveStretchConst2[passNr].dispatch(this.ih_max); } // solve shear constraints for (let passNr = 0; passNr < 2; passNr++) { this.updateParams(passNr); this.csSolveShearConst1[passNr].dispatch(this.iw_max-1); } for (let passNr = 0; passNr < 2; passNr++) { this.updateParams(passNr); this.csSolveShearConst2[passNr].dispatch(this.ih_max-1); } // solve bending constraints for (let ih = 0; ih < (this.ih_max-1) - 1; ih++) { this.updateParams(ih); this.csSolveBendConst1[ih].dispatch(this.iw_max); } for (let iw = 0; iw < (this.iw_max-1 - 1); iw++) { this.updateParams(iw); this.csSolveBendConst2[iw].dispatch(this.ih_max); } // update velocities this.csUpdateVel.dispatch(Math.ceil(this.numParticles / 64)); } updateMeshes() { const meshVertexBuffer = this.customMesh.getVertexBuffer("position"); this.particleBuffers.read().then((res) => { const resFloats = new Float32Array(res.buffer); let pos = []; for (let i = 0; i < this.numParticles; ++i) { pos.push( resFloats[16 * i], resFloats[16 * i + 1], resFloats[16 * i + 2]); } meshVertexBuffer.update(pos); }); } startGrab(pos) { this.grab1UBuffer.updateVector3("pos", pos); this.grab1UBuffer.update(); this.grabIdBuffer.update(new Int32Array([this.grabId])); this.csStartGrab.dispatch(1); this.grabIdBuffer.read().then((res) => { const resFloats = new Int32Array(res.buffer); console.log("res gId", resFloats[0]); }); this.grabInvMassBuffer.read().then((res) => { const resFloats = new Float32Array(res.buffer); console.log("res gInvMass", resFloats[0]); }); } moveGrabbed(pos, vel) { this.grab12UBuffer.updateVector3("pos", pos); this.grab12UBuffer.update(); this.csMoveGrabbed.dispatch(1); } endGrab(pos, vel) { this.grab2UBuffer.updateVector3("vel", vel); this.grab2UBuffer.update(); this.csEndGrab.dispatch(1); } } //-- Grabber -------- class Grabber { constructor() { this.physicsObject = null; this.distance = 0.0; this.prevPos = new BABYLON.Vector3(); this.vel = new BABYLON.Vector3(); this.time = 0.0; } increaseTime(dt) { this.time += dt; } start(pickInfo) { this.physicsObject = null; let obj = pickInfo.pickedMesh.userData; let indices = pickInfo.pickedMesh.getIndices(); console.log("faceId", pickInfo.faceId, "index", indices[pickInfo.faceId * 3], indices[pickInfo.faceId * 3 + 1], indices[pickInfo.faceId * 3 + 2]); this.physicsObject = obj; this.distance = pickInfo.distance; this.ray = pickInfo.ray; let pos = pickInfo.pickedPoint; let minD2 = Number.MAX_VALUE; let gId = -1; for (let j = 0; j < 3; j++) { let idx = indices[pickInfo.faceId * 3 + j]; let obj_pos = this.physicsObject.points[idx].pos; let d2 = BABYLON.Vector3.DistanceSquared(pos, obj_pos); if (d2 < minD2) { minD2 = d2; gId = idx; } } console.log("gId", gId); obj.grabId = gId; this.physicsObject.startGrab(pos); this.prevPos.copyFrom(pos); this.vel.set(0.0, 0.0, 0.0); this.time = 0.0; } move(pickInfo) { if (this.physicsObject) { let pos = pickInfo.ray.origin.clone(); pos = pos.add(pickInfo.ray.direction.scale(this.distance)); this.vel.copyFrom(pos); this.vel = this.vel.subtract(this.prevPos); if (this.time > 0.0) { this.vel.scale(1 / this.time); } else { this.vel.set(0.0, 0.0, 0.0); } this.prevPos.copyFrom(pos); this.time = 0.0; this.physicsObject.moveGrabbed(pos, this.vel); } } end(pickInfo) { if (this.physicsObject) { this.physicsObject.endGrab(this.prevPos, this.vel); this.physicsObject = null; } } } //-- mouse event function mouse_event() { scene.onPointerDown = function (event, pickInfo){ console.log("mouseDown pickInfo", pickInfo); console.log("mouseDown pickInfo", pickInfo.hit); if (pickInfo.hit == true) { console.log("mouseDown pickInfo id", pickInfo.pickedMesh.id); console.log("mouseDown pickInfo faceId", pickInfo.faceId); gGrabber.start(pickInfo); camera.detachControl(canvas); } } scene.onPointerMove = function (event, pickInfo){ gGrabber.move(pickInfo); } scene.onPointerUp = function (event, pickInfo){ gGrabber.end(pickInfo); camera.attachControl(canvas, true); } } function update() { mouse_event(); for (let step = 0; step < numSubsteps; step++) { cloth.simulate(); } cloth.updateMeshes(); gGrabber.increaseTime(dt); } let clothMesh = createClothMesh(cwidth, cheight, cdivisions); let cloth = new Cloth(clothMesh, scene); let gGrabber = new Grabber(); scene.registerBeforeRender(function () { update(); }); engine.runRenderLoop(() => { scene.render(); }); }; init(); </script> </body> </html>
customMesh3D.js
// create Mesh Vertices and Indices function createClothMesh(width, height, subdivision) { let xi, yi, numXY, spaceX, spaceY; numXY = subdivision; spaceX = width / subdivision; spaceY = height / subdivision; let positions = []; let px, py; for (yi = 0; yi < (numXY + 1); yi++) for (xi = 0; xi < (numXY + 1); xi++) { let id = xi + yi * (numXY + 1); px = (-numXY * 0.5 + xi) * spaceX; py = ( numXY * 0.5 - yi) * spaceY; pz = 0.0; //console.log("id position", id, px, py,pz); positions.push(px, py, pz); } //console.log("positions", positions); let indices = []; let id0, id1, id2, id3; for (yi = 0; yi < numXY; yi++) for (xi = 0; xi < numXY; xi++) { id0 = yi * (numXY + 1) + xi; id1 = (yi + 1) * (numXY + 1) + xi; id2 = (yi + 1) * (numXY + 1) + xi + 1; id3 = yi * (numXY + 1) + xi + 1; indices.push(id0, id1, id2); indices.push(id0, id2, id3); } return { name: "custom", vertices: positions, indices: indices} }