diff --git a/src/app/pages/algorithms/cloth/cloth.component.ts b/src/app/pages/algorithms/cloth/cloth.component.ts index 84e3748..20b81a0 100644 --- a/src/app/pages/algorithms/cloth/cloth.component.ts +++ b/src/app/pages/algorithms/cloth/cloth.component.ts @@ -1,12 +1,23 @@ +/** + * File: cloth.component.ts + * Description: Component for cloth simulation using WebGPU compute shaders. + */ + 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/rendering/canvas/babylon-canvas.component'; -import {ComputeShader, StorageBuffer, MeshBuilder, ShaderMaterial, ShaderLanguage, ArcRotateCamera} from '@babylonjs/core'; -import {CLOTH_FRAGMENT_SHADER_WGSL, CLOTH_INTEGRATE_COMPUTE_WGSL, CLOTH_SOLVE_COMPUTE_WGSL, CLOTH_VELOCITY_COMPUTE_WGSL, CLOTH_VERTEX_SHADER_WGSL} from './cloth.shader'; +import { MatCard, MatCardContent, MatCardHeader, MatCardTitle } from '@angular/material/card'; +import { TranslatePipe } from '@ngx-translate/core'; +import { BabylonCanvas, RenderConfig, SceneEventData } from '../../../shared/rendering/canvas/babylon-canvas.component'; +import { ComputeShader, StorageBuffer, MeshBuilder, ShaderMaterial, ShaderLanguage, ArcRotateCamera } from '@babylonjs/core'; +import { + CLOTH_FRAGMENT_SHADER_WGSL, + CLOTH_INTEGRATE_COMPUTE_WGSL, + CLOTH_SOLVE_COMPUTE_WGSL, + CLOTH_VELOCITY_COMPUTE_WGSL, + CLOTH_VERTEX_SHADER_WGSL +} from './cloth.shader'; @Component({ - selector: 'app-cloth.component', + selector: 'app-cloth', imports: [ MatCard, MatCardContent, @@ -21,55 +32,60 @@ import {CLOTH_FRAGMENT_SHADER_WGSL, CLOTH_INTEGRATE_COMPUTE_WGSL, CLOTH_SOLVE_CO export class ClothComponent { private currentSceneData: SceneEventData | null = null; - renderConfig: RenderConfig = { + public renderConfig: RenderConfig = { mode: '3D', initialViewSize: 20, shaderLanguage: ShaderLanguage.WGSL }; - onSceneReady(event: SceneEventData) { + /** + * Called when the Babylon scene is ready. + * @param event The scene event data. + */ + public onSceneReady(event: SceneEventData): void { this.currentSceneData = event; this.createSimulation(); } - private createSimulation() { - if (!this.currentSceneData){ + /** + * Initializes and starts the cloth simulation. + */ + private createSimulation(): void { + if (!this.currentSceneData) { return; } - const {engine, scene} = this.currentSceneData; + + const { engine, scene } = this.currentSceneData; // --- 1. CONFIGURE CLOTH GRID --- - const gridWidth = 50; // 50x50 = 2500 Vertices (Increase this later!) + const gridWidth = 50; const gridHeight = 50; const numVertices = gridWidth * gridHeight; - const spacing = 0.1; // Distance between points - - // Calculate approximate constraints (horizontal + vertical edges) - const numConstraints = (gridWidth - 1) * gridHeight + gridWidth * (gridHeight - 1); + const spacing = 0.1; const positionsData = new Float32Array(numVertices * 4); const prevPositionsData = new Float32Array(numVertices * 4); const velocitiesData = new Float32Array(numVertices * 4); - // Arrays für unsere 4 Phasen (dynamische Größe, da wir pushen) + // Arrays for our 4 phases (dynamic size as we push) const constraintsP0: number[] = []; const constraintsP1: number[] = []; const constraintsP2: number[] = []; const constraintsP3: number[] = []; - // Hilfsfunktion zum sauberen Hinzufügen (vec4-Struktur) - const addConstraint = (arr: number[], a: number, b: number) => { + // Helper function for clean adding (vec4 structure) + const addConstraint = (arr: number[], a: number, b: number): void => { arr.push(a, b, spacing, 1.0); }; - // Positionen füllen (bleibt wie vorher) + // 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); positionsData[idx + 2] = 0.0; - positionsData[idx + 3] = (y === 0) ? 0.0 : 1.0; // Oben festpinnen + positionsData[idx + 3] = (y === 0) ? 0.0 : 1.0; prevPositionsData[idx + 0] = positionsData[idx + 0]; prevPositionsData[idx + 1] = positionsData[idx + 1]; @@ -78,27 +94,35 @@ export class ClothComponent { } } - // --- GRAPH COLORING: Constraints in 4 Phasen füllen --- - // Phase 0: Horizontal Gerade + // --- 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); + for (let x = 0; x < gridWidth - 1; x += 2) { + addConstraint(constraintsP0, y * gridWidth + x, y * gridWidth + x + 1); + } } - // Phase 1: Horizontal Ungerade + // 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 x = 1; x < gridWidth - 1; x += 2) { + addConstraint(constraintsP1, y * gridWidth + x, y * gridWidth + x + 1); + } } - // Phase 2: Vertikal Gerade + // 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); + for (let x = 0; x < gridWidth; x++) { + addConstraint(constraintsP2, y * gridWidth + x, (y + 1) * gridWidth + x); + } } - // Phase 3: Vertikal Ungerade + // 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 x = 0; x < gridWidth; x++) { + addConstraint(constraintsP3, y * gridWidth + x, (y + 1) * gridWidth + x); + } } const paramsData = new Float32Array(8); - // --- 3. CREATE GPU STORAGE BUFFERS --- + // --- 2. CREATE GPU STORAGE BUFFERS --- const positionsBuffer = new StorageBuffer(engine, positionsData.byteLength); positionsBuffer.update(positionsData); @@ -108,24 +132,44 @@ export class ClothComponent { const velocitiesBuffer = new StorageBuffer(engine, velocitiesData.byteLength); const paramsBuffer = new StorageBuffer(engine, paramsData.byteLength); - // Erstelle 4 separate Buffer für die 4 Phasen - const cBuffer0 = new StorageBuffer(engine, constraintsP0.length * 4); cBuffer0.update(new Float32Array(constraintsP0)); - const cBuffer1 = new StorageBuffer(engine, constraintsP1.length * 4); cBuffer1.update(new Float32Array(constraintsP1)); - const cBuffer2 = new StorageBuffer(engine, constraintsP2.length * 4); cBuffer2.update(new Float32Array(constraintsP2)); - const cBuffer3 = new StorageBuffer(engine, constraintsP3.length * 4); cBuffer3.update(new Float32Array(constraintsP3)); + // 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)); + return buffer; + }; - // --- 4. SETUP COMPUTE SHADERS --- + const cBuffer0 = createAndPopulateBuffer(constraintsP0); + const cBuffer1 = createAndPopulateBuffer(constraintsP1); + const cBuffer2 = createAndPopulateBuffer(constraintsP2); + const cBuffer3 = createAndPopulateBuffer(constraintsP3); + + // --- 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 } } + 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); + csIntegrate.setStorageBuffer("p", paramsBuffer); + csIntegrate.setStorageBuffer("positions", positionsBuffer); + csIntegrate.setStorageBuffer("prev_positions", prevPositionsBuffer); + csIntegrate.setStorageBuffer("velocities", velocitiesBuffer); - // Hilfsfunktion, um die 4 Solve-Shader sauber zu erstellen - const createSolver = (name: string, cBuffer: StorageBuffer) => { + // Helper function to create the 4 solve shaders + const createSolver = (name: string, cBuffer: StorageBuffer): ComputeShader => { const cs = new ComputeShader(name, engine, { computeSource: CLOTH_SOLVE_COMPUTE_WGSL }, { - bindingsMapping: { "p": { group: 0, binding: 0 }, "positions": { group: 0, binding: 1 }, "constraints": { group: 0, binding: 2 } } + bindingsMapping: { + "p": { group: 0, binding: 0 }, + "positions": { group: 0, binding: 1 }, + "constraints": { group: 0, binding: 2 } + } }); - cs.setStorageBuffer("p", paramsBuffer); cs.setStorageBuffer("positions", positionsBuffer); cs.setStorageBuffer("constraints", cBuffer); + cs.setStorageBuffer("p", paramsBuffer); + cs.setStorageBuffer("positions", positionsBuffer); + cs.setStorageBuffer("constraints", cBuffer); return cs; }; @@ -135,12 +179,19 @@ export class ClothComponent { const csSolve3 = createSolver("solve3", cBuffer3); 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 } } + 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); + csVelocity.setStorageBuffer("p", paramsBuffer); + csVelocity.setStorageBuffer("positions", positionsBuffer); + csVelocity.setStorageBuffer("prev_positions", prevPositionsBuffer); + csVelocity.setStorageBuffer("velocities", velocitiesBuffer); - // --- 5. SETUP RENDER MESH --- - // We create a ground mesh that matches our grid size, but we will OVERWRITE its vertices in the shader. + // --- 4. SETUP RENDER MESH --- const clothMesh = MeshBuilder.CreateGround("cloth", { width: 10, height: 10, subdivisions: gridWidth - 1 }, scene); const clothMaterial = new ShaderMaterial("clothMat", scene, { @@ -164,22 +215,21 @@ export class ClothComponent { camera.radius = 15; } - // --- 6. RENDER LOOP --- + // --- 5. RENDER LOOP --- scene.onBeforeRenderObservable.clear(); scene.onBeforeRenderObservable.add(() => { - paramsData[0] = 0.016; paramsData[1] = -9.81; - paramsData[2] = 0.0001; // Compliance (sehr klein = steifer Stoff) + paramsData[2] = 0.0001; // Compliance (very small = stiff fabric) paramsData[3] = numVertices; paramsBuffer.update(paramsData); const dispatchXVertices = Math.ceil(numVertices / 64); - // 1. Positionen vorhersehen + // 1. Predict positions csIntegrate.dispatch(dispatchXVertices, 1, 1); - // 2. XPBD Solver (Substeps) - Jede Farbe einzeln lösen! + // 2. XPBD Solver (Substeps) - Solve each color individually for (let i = 0; i < 5; i++) { csSolve0.dispatch(Math.ceil((constraintsP0.length / 4) / 64), 1, 1); csSolve1.dispatch(Math.ceil((constraintsP1.length / 4) / 64), 1, 1); @@ -187,7 +237,7 @@ export class ClothComponent { csSolve3.dispatch(Math.ceil((constraintsP3.length / 4) / 64), 1, 1); } - // 3. Geschwindigkeiten aktualisieren + // 3. Update velocities csVelocity.dispatch(dispatchXVertices, 1, 1); }); } diff --git a/src/app/pages/algorithms/cloth/cloth.shader.ts b/src/app/pages/algorithms/cloth/cloth.shader.ts index 5d06ed2..f8ea79c 100644 --- a/src/app/pages/algorithms/cloth/cloth.shader.ts +++ b/src/app/pages/algorithms/cloth/cloth.shader.ts @@ -1,4 +1,9 @@ -// --- SHARED DATA STRUCTURES --- +/** + * File: cloth.shader.ts + * Description: WGSL shaders for cloth simulation and rendering. + */ + +// --- SHARED DATA STRUCTURES --- export const CLOTH_SHARED_STRUCTS = ` struct Params { dt: f32, // Time step per substep @@ -21,7 +26,7 @@ export const CLOTH_VERTEX_SHADER_WGSL = ` // Storage Buffer var positions : array>; - // Babylon Preprocessor-Magie + // Babylon Preprocessor Magic uniform viewProjection : mat4x4; varying vUV : vec2; @@ -39,7 +44,7 @@ export const CLOTH_VERTEX_SHADER_WGSL = ` `; // ========================================== -// FRAGMENT SHADER (Bleibt exakt gleich) +// FRAGMENT SHADER // ========================================== export const CLOTH_FRAGMENT_SHADER_WGSL = ` varying vUV : vec2; @@ -98,13 +103,13 @@ export const CLOTH_INTEGRATE_COMPUTE_WGSL = CLOTH_SHARED_STRUCTS + ` export const CLOTH_SOLVE_COMPUTE_WGSL = CLOTH_SHARED_STRUCTS + ` @group(0) @binding(0) var p : Params; @group(0) @binding(1) var positions : array>; - @group(0) @binding(2) var constraints : array>; // <--- Nur "read", da wir sie hier nicht verändern + @group(0) @binding(2) var constraints : array>; // <--- Read-only as we do not modify them here @compute @workgroup_size(64) fn main(@builtin(global_invocation_id) global_id : vec3) { let idx = global_id.x; - // HIER: Wir fragen die GPU direkt, wie groß das übergebene Array ist! + // Query the GPU directly for the length of the passed array if (idx >= arrayLength(&constraints)) { return; } let constraint = constraints[idx];