diff --git a/src/app/pages/algorithms/cloth/cloth.component.ts b/src/app/pages/algorithms/cloth/cloth.component.ts index f23adb8..06b057a 100644 --- a/src/app/pages/algorithms/cloth/cloth.component.ts +++ b/src/app/pages/algorithms/cloth/cloth.component.ts @@ -7,7 +7,7 @@ import { Component } from '@angular/core'; import { MatCard, MatCardContent, MatCardHeader, MatCardTitle } from '@angular/material/card'; import { TranslatePipe } from '@ngx-translate/core'; import { BabylonCanvas, RenderConfig, SceneEventData } from '../../../shared/components/render-canvas/babylon-canvas.component'; -import {ComputeShader, StorageBuffer, MeshBuilder, ShaderMaterial, ShaderLanguage, ArcRotateCamera, GroundMesh} from '@babylonjs/core'; +import {ComputeShader, StorageBuffer, MeshBuilder, ShaderMaterial, ShaderLanguage, ArcRotateCamera, GroundMesh, WebGPUEngine, Scene} from '@babylonjs/core'; import { CLOTH_FRAGMENT_SHADER_WGSL, CLOTH_INTEGRATE_COMPUTE_WGSL, @@ -16,6 +16,7 @@ import { CLOTH_VERTEX_SHADER_WGSL } from './cloth.shader'; import {MatButton} from '@angular/material/button'; +import {ClothBuffers, ClothConfig, ClothData, ClothPipelines} from './cloth.model'; @Component({ selector: 'app-cloth', @@ -61,45 +62,75 @@ export class ClothComponent { * Initializes and starts the cloth simulation. */ private createSimulation(): void { - if (!this.currentSceneData) { - return; - } + if (!this.currentSceneData) return; const { engine, scene } = this.currentSceneData; - // --- 1. CONFIGURE CLOTH GRID --- + // 1. Define physics parameters + const config = this.getClothConfig(); + + // 2. Generate initial CPU data (positions, constraints) + const clothData = this.generateClothData(config); + + // 3. Upload to GPU + const buffers = this.createStorageBuffers(engine, clothData); + + // 4. Create Compute Shaders + const pipelines = this.setupComputePipelines(engine, buffers); + + // 5. Setup Rendering (Mesh, Material, Camera) + this.setupRenderMesh(scene, config, buffers.positions); + + // 6. Start the physics loop + this.startRenderLoop(engine, scene, config, buffers, pipelines); + } + + // ======================================================================== + // 1. CONFIGURATION + // ======================================================================== + private getClothConfig(): ClothConfig { const gridWidth = 100; const gridHeight = 100; const spacing = 0.05; - const numVertices = gridWidth * gridHeight; const density = 1.0; const particleArea = spacing * spacing; const particleMass = density * particleArea; - const particleInvMass = 1.0 / particleMass; - const positionsData = new Float32Array(numVertices * 4); - const prevPositionsData = new Float32Array(numVertices * 4); - const velocitiesData = new Float32Array(numVertices * 4); + return { + gridWidth, + gridHeight, + spacing, + density, + numVertices: gridWidth * gridHeight, + particleInvMass: 1.0 / particleMass + }; + } + + // ======================================================================== + // 2. DATA GENERATION (CPU) + // ======================================================================== + private generateClothData(config: ClothConfig): ClothData { + const positionsData = new Float32Array(config.numVertices * 4); + const prevPositionsData = new Float32Array(config.numVertices * 4); + const velocitiesData = new Float32Array(config.numVertices * 4); - // Arrays for our 4 phases (dynamic size as we push) const constraintsP0: number[] = []; const constraintsP1: number[] = []; const constraintsP2: number[] = []; const constraintsP3: number[] = []; - // Helper function for clean adding (vec4 structure) const addConstraint = (arr: number[], a: number, b: number): void => { - arr.push(a, b, spacing, 1.0); + arr.push(a, b, config.spacing, 1.0); }; - // Fill positions and pin the top edge - for (let y = 0; y < gridHeight; y++) { - for (let x = 0; x < gridWidth; x++) { - const idx = (y * gridWidth + x) * 4; - positionsData[idx + 0] = (x - gridWidth / 2) * spacing; - positionsData[idx + 1] = 5.0 - (y * spacing); + // Fill positions (Pin top row) + for (let y = 0; y < config.gridHeight; y++) { + for (let x = 0; x < config.gridWidth; x++) { + const idx = (y * config.gridWidth + x) * 4; + positionsData[idx + 0] = (x - config.gridWidth / 2) * config.spacing; + positionsData[idx + 1] = 5.0 - (y * config.spacing); positionsData[idx + 2] = 0.0; - positionsData[idx + 3] = (y === 0) ? 0.0 : particleInvMass; + positionsData[idx + 3] = (y === 0) ? 0.0 : config.particleInvMass; prevPositionsData[idx + 0] = positionsData[idx + 0]; prevPositionsData[idx + 1] = positionsData[idx + 1]; @@ -108,72 +139,70 @@ export class ClothComponent { } } - // --- GRAPH COLORING: Fill constraints in 4 phases --- - // Phase 0: Horizontal Even - for (let y = 0; y < gridHeight; y++) { - for (let x = 0; x < gridWidth - 1; x += 2) { - addConstraint(constraintsP0, y * gridWidth + x, y * gridWidth + x + 1); - } + // Graph Coloring (4 Phases) + for (let y = 0; y < config.gridHeight; y++) { + for (let x = 0; x < config.gridWidth - 1; x += 2) addConstraint(constraintsP0, y * config.gridWidth + x, y * config.gridWidth + x + 1); + for (let x = 1; x < config.gridWidth - 1; x += 2) addConstraint(constraintsP1, y * config.gridWidth + x, y * config.gridWidth + x + 1); } - // Phase 1: Horizontal Odd - for (let y = 0; y < gridHeight; y++) { - for (let x = 1; x < gridWidth - 1; x += 2) { - addConstraint(constraintsP1, y * gridWidth + x, y * gridWidth + x + 1); - } + for (let y = 0; y < config.gridHeight - 1; y += 2) { + for (let x = 0; x < config.gridWidth; x++) addConstraint(constraintsP2, y * config.gridWidth + x, (y + 1) * config.gridWidth + x); } - // Phase 2: Vertical Even - for (let y = 0; y < gridHeight - 1; y += 2) { - for (let x = 0; x < gridWidth; x++) { - addConstraint(constraintsP2, y * gridWidth + x, (y + 1) * gridWidth + x); - } - } - // Phase 3: Vertical Odd - for (let y = 1; y < gridHeight - 1; y += 2) { - for (let x = 0; x < gridWidth; x++) { - addConstraint(constraintsP3, y * gridWidth + x, (y + 1) * gridWidth + x); - } + for (let y = 1; y < config.gridHeight - 1; y += 2) { + for (let x = 0; x < config.gridWidth; x++) addConstraint(constraintsP3, y * config.gridWidth + x, (y + 1) * config.gridWidth + x); } - const paramsData = new Float32Array(8); + return { + positions: positionsData, + prevPositions: prevPositionsData, + velocities: velocitiesData, + constraints: [constraintsP0, constraintsP1, constraintsP2, constraintsP3], + params: new Float32Array(8) + }; + } - // --- 2. CREATE GPU STORAGE BUFFERS --- - const positionsBuffer = new StorageBuffer(engine, positionsData.byteLength); - positionsBuffer.update(positionsData); - - const prevPositionsBuffer = new StorageBuffer(engine, prevPositionsData.byteLength); - prevPositionsBuffer.update(prevPositionsData); - - const velocitiesBuffer = new StorageBuffer(engine, velocitiesData.byteLength); - const paramsBuffer = new StorageBuffer(engine, paramsData.byteLength); - - // Create 4 separate buffers for the 4 phases - const createAndPopulateBuffer = (data: number[]): StorageBuffer => { - const buffer = new StorageBuffer(engine, data.length * 4); - buffer.update(new Float32Array(data)); + // ======================================================================== + // 3. BUFFER CREATION (GPU) + // ======================================================================== + private createStorageBuffers(engine: WebGPUEngine, data: ClothData): ClothBuffers { + const createBuffer = (arrayData: Float32Array | number[]): StorageBuffer => { + const buffer = new StorageBuffer(engine, arrayData.length * 4); + buffer.update(arrayData instanceof Float32Array ? arrayData : new Float32Array(arrayData)); return buffer; }; - const cBuffer0 = createAndPopulateBuffer(constraintsP0); - const cBuffer1 = createAndPopulateBuffer(constraintsP1); - const cBuffer2 = createAndPopulateBuffer(constraintsP2); - const cBuffer3 = createAndPopulateBuffer(constraintsP3); + return { + positions: createBuffer(data.positions), + prevPositions: createBuffer(data.prevPositions), + velocities: createBuffer(data.velocities), + params: createBuffer(data.params), + constraints: data.constraints.map(cData => createBuffer(cData)) + }; + } - // --- 3. SETUP COMPUTE SHADERS --- - const csIntegrate = new ComputeShader("integrate", engine, { computeSource: CLOTH_INTEGRATE_COMPUTE_WGSL }, { - bindingsMapping: { - "p": { group: 0, binding: 0 }, - "positions": { group: 0, binding: 1 }, - "prev_positions": { group: 0, binding: 2 }, - "velocities": { group: 0, binding: 3 } - } - }); - csIntegrate.setStorageBuffer("p", paramsBuffer); - csIntegrate.setStorageBuffer("positions", positionsBuffer); - csIntegrate.setStorageBuffer("prev_positions", prevPositionsBuffer); - csIntegrate.setStorageBuffer("velocities", velocitiesBuffer); + // ======================================================================== + // 4. COMPUTE SHADERS + // ======================================================================== + private setupComputePipelines(engine: WebGPUEngine, buffers: ClothBuffers): ClothPipelines { - // Helper function to create the 4 solve shaders - const createSolver = (name: string, cBuffer: StorageBuffer): ComputeShader => { + // Helper for integrating & velocity + const createBasicShader = (name: string, source: string) => { + const cs = new ComputeShader(name, engine, { computeSource: source }, { + bindingsMapping: { + "p": { group: 0, binding: 0 }, + "positions": { group: 0, binding: 1 }, + "prev_positions": { group: 0, binding: 2 }, + "velocities": { group: 0, binding: 3 } + } + }); + cs.setStorageBuffer("p", buffers.params); + cs.setStorageBuffer("positions", buffers.positions); + cs.setStorageBuffer("prev_positions", buffers.prevPositions); + cs.setStorageBuffer("velocities", buffers.velocities); + return cs; + }; + + // Helper for solvers + const createSolverShader = (name: string, constraintBuffer: StorageBuffer) => { const cs = new ComputeShader(name, engine, { computeSource: CLOTH_SOLVE_COMPUTE_WGSL }, { bindingsMapping: { "p": { group: 0, binding: 0 }, @@ -181,36 +210,28 @@ export class ClothComponent { "constraints": { group: 0, binding: 2 } } }); - cs.setStorageBuffer("p", paramsBuffer); - cs.setStorageBuffer("positions", positionsBuffer); - cs.setStorageBuffer("constraints", cBuffer); + cs.setStorageBuffer("p", buffers.params); + cs.setStorageBuffer("positions", buffers.positions); + cs.setStorageBuffer("constraints", constraintBuffer); return cs; }; - const csSolve0 = createSolver("solve0", cBuffer0); - const csSolve1 = createSolver("solve1", cBuffer1); - const csSolve2 = createSolver("solve2", cBuffer2); - const csSolve3 = createSolver("solve3", cBuffer3); + return { + integrate: createBasicShader("integrate", CLOTH_INTEGRATE_COMPUTE_WGSL), + solvers: buffers.constraints.map((cBuffer, i) => createSolverShader(`solve${i}`, cBuffer)), + velocity: createBasicShader("velocity", CLOTH_VELOCITY_COMPUTE_WGSL) + }; + } - const csVelocity = new ComputeShader("velocity", engine, { computeSource: CLOTH_VELOCITY_COMPUTE_WGSL }, { - bindingsMapping: { - "p": { group: 0, binding: 0 }, - "positions": { group: 0, binding: 1 }, - "prev_positions": { group: 0, binding: 2 }, - "velocities": { group: 0, binding: 3 } - } - }); - csVelocity.setStorageBuffer("p", paramsBuffer); - csVelocity.setStorageBuffer("positions", positionsBuffer); - csVelocity.setStorageBuffer("prev_positions", prevPositionsBuffer); - csVelocity.setStorageBuffer("velocities", velocitiesBuffer); - - // --- 4. SETUP RENDER MESH --- - if (this.clothMesh) - { + // ======================================================================== + // 5. RENDERING SETUP + // ======================================================================== + private setupRenderMesh(scene: Scene, config: ClothConfig, positionsBuffer: StorageBuffer): void { + if (this.clothMesh) { scene.removeMesh(this.clothMesh); } - this.clothMesh = MeshBuilder.CreateGround("cloth", { width: 10, height: 10, subdivisions: gridWidth - 1 }, scene); + + this.clothMesh = MeshBuilder.CreateGround("cloth", { width: 10, height: 10, subdivisions: config.gridWidth - 1 }, scene); const clothMaterial = new ShaderMaterial("clothMat", scene, { vertexSource: CLOTH_VERTEX_SHADER_WGSL, @@ -232,46 +253,54 @@ export class ClothComponent { camera.beta = Math.PI / 2.5; camera.radius = 15; } + } + + // ======================================================================== + // 6. RENDER LOOP + // ======================================================================== + private startRenderLoop(engine: WebGPUEngine, scene: Scene, config: ClothConfig, buffers: ClothBuffers, pipelines: ClothPipelines): void { + const paramsData = new Float32Array(8); + + // Pre-calculate constraint dispatch sizes for the 4 phases + const constraintsLength = buffers.constraints.map(b => (b as any)._buffer.capacity / 4 / 4); // Elements / vec4 length + const dispatchXConstraints = constraintsLength.map(len => Math.ceil(len / 64)); + const dispatchXVertices = Math.ceil(config.numVertices / 64); + const substeps = 15; - // --- 5. RENDER LOOP --- scene.onBeforeRenderObservable.clear(); scene.onBeforeRenderObservable.add(() => { this.simulationTime += engine.getDeltaTime() / 1000.0; + // Update Physics Parameters const windX = this.isWindActive ? 5.0 : 0.0; const windY = 0.0; const windZ = this.isWindActive ? 15.0 : 0.0; + const scaledCompliance = 0.00001 * config.particleInvMass * config.spacing; - const baseCompliance = 0.00001; - const scaledCompliance = baseCompliance * particleInvMass * spacing; - - paramsData[0] = 0.016; - paramsData[1] = -9.81; - paramsData[2] = scaledCompliance; //scaled stiffness - paramsData[3] = numVertices; + paramsData[0] = 0.016; // dt + paramsData[1] = -9.81; // gravity + paramsData[2] = scaledCompliance; + paramsData[3] = config.numVertices; paramsData[4] = windX; paramsData[5] = windY; paramsData[6] = windZ; paramsData[7] = this.simulationTime; - paramsBuffer.update(paramsData); - - const dispatchXVertices = Math.ceil(numVertices / 64); + buffers.params.update(paramsData); // 1. Predict positions - csIntegrate.dispatch(dispatchXVertices, 1, 1); + pipelines.integrate.dispatch(dispatchXVertices, 1, 1); - // 2. XPBD Solver (Substeps) - Solve each color individually - const substeps = 15; + // 2. XPBD Solver (Substeps) - Graph Coloring Phase for (let i = 0; i < substeps; i++) { - csSolve0.dispatch(Math.ceil((constraintsP0.length / 4) / 64), 1, 1); - csSolve1.dispatch(Math.ceil((constraintsP1.length / 4) / 64), 1, 1); - csSolve2.dispatch(Math.ceil((constraintsP2.length / 4) / 64), 1, 1); - csSolve3.dispatch(Math.ceil((constraintsP3.length / 4) / 64), 1, 1); + pipelines.solvers[0].dispatch(dispatchXConstraints[0], 1, 1); + pipelines.solvers[1].dispatch(dispatchXConstraints[1], 1, 1); + pipelines.solvers[2].dispatch(dispatchXConstraints[2], 1, 1); + pipelines.solvers[3].dispatch(dispatchXConstraints[3], 1, 1); } // 3. Update velocities - csVelocity.dispatch(dispatchXVertices, 1, 1); + pipelines.velocity.dispatch(dispatchXVertices, 1, 1); }); } } diff --git a/src/app/pages/algorithms/cloth/cloth.model.ts b/src/app/pages/algorithms/cloth/cloth.model.ts new file mode 100644 index 0000000..494295e --- /dev/null +++ b/src/app/pages/algorithms/cloth/cloth.model.ts @@ -0,0 +1,36 @@ +// --- SIMULATION CONFIGURATION --- +import {ComputeShader, StorageBuffer} from '@babylonjs/core'; + +export interface ClothConfig { + gridWidth: number; + gridHeight: number; + spacing: number; + density: number; + numVertices: number; + particleInvMass: number; +} + +// --- RAW CPU DATA --- +export interface ClothData { + positions: Float32Array; + prevPositions: Float32Array; + velocities: Float32Array; + constraints: number[][]; // Array containing the 4 phases + params: Float32Array; +} + +// --- WEBGPU BUFFERS --- +export interface ClothBuffers { + positions: StorageBuffer; + prevPositions: StorageBuffer; + velocities: StorageBuffer; + params: StorageBuffer; + constraints: StorageBuffer[]; // 4 phase buffers +} + +// --- COMPUTE PIPELINES --- +export interface ClothPipelines { + integrate: ComputeShader; + solvers: ComputeShader[]; // 4 solve shaders + velocity: ComputeShader; +}