From 954211b3cf852a5e3daa95a044f404908806e973 Mon Sep 17 00:00:00 2001 From: Lobo Date: Mon, 23 Feb 2026 11:02:54 +0100 Subject: [PATCH 1/8] Add cloth simulation page with WGSL shaders Introduce a new cloth simulation feature: adds ClothComponent (TS/HTML/SCSS) and WGSL compute/vertex/fragment shaders implementing an XPBD-based cloth sim. Wire up routing and RouterConstants, add the cloth entry to the algorithms list, and add English/German i18n strings. Also include small refactors/renames for algorithm-category and algorithms.service imports and update BabylonCanvas to tolerate optional shader configuration and avoid null access during setup. --- src/app/app.routes.ts | 3 +- src/app/constants/RouterConstants.ts | 7 + .../{models => }/algorithm-category.ts | 0 .../pages/algorithms/algorithms.component.ts | 4 +- .../{service => }/algorithms.service.ts | 10 +- .../algorithms/cloth/cloth.component.html | 12 + .../algorithms/cloth/cloth.component.scss | 0 .../pages/algorithms/cloth/cloth.component.ts | 207 ++++++++++++++++++ .../pages/algorithms/cloth/cloth.shader.ts | 195 +++++++++++++++++ .../canvas/babylon-canvas.component.ts | 20 +- src/assets/i18n/de.json | 7 + src/assets/i18n/en.json | 7 + 12 files changed, 461 insertions(+), 11 deletions(-) rename src/app/pages/algorithms/{models => }/algorithm-category.ts (100%) rename src/app/pages/algorithms/{service => }/algorithms.service.ts (84%) create mode 100644 src/app/pages/algorithms/cloth/cloth.component.html create mode 100644 src/app/pages/algorithms/cloth/cloth.component.scss create mode 100644 src/app/pages/algorithms/cloth/cloth.component.ts create mode 100644 src/app/pages/algorithms/cloth/cloth.shader.ts diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index d079fbb..8e69cac 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -14,6 +14,7 @@ export const routes: Routes = [ { path: RouterConstants.LABYRINTH.PATH, component: RouterConstants.LABYRINTH.COMPONENT}, { path: RouterConstants.FRACTAL.PATH, component: RouterConstants.FRACTAL.COMPONENT}, { path: RouterConstants.FRACTAL3d.PATH, component: RouterConstants.FRACTAL3d.COMPONENT}, - { path: RouterConstants.PENDULUM.PATH, component: RouterConstants.PENDULUM.COMPONENT} + { path: RouterConstants.PENDULUM.PATH, component: RouterConstants.PENDULUM.COMPONENT}, + { path: RouterConstants.CLOTH.PATH, component: RouterConstants.CLOTH.COMPONENT} ]; diff --git a/src/app/constants/RouterConstants.ts b/src/app/constants/RouterConstants.ts index ac1efab..1ab61ea 100644 --- a/src/app/constants/RouterConstants.ts +++ b/src/app/constants/RouterConstants.ts @@ -9,6 +9,7 @@ import {LabyrinthComponent} from '../pages/algorithms/pathfinding/labyrinth/laby import {FractalComponent} from '../pages/algorithms/fractal/fractal.component'; import {Fractal3dComponent} from '../pages/algorithms/fractal3d/fractal3d.component'; import PendulumComponent from '../pages/algorithms/pendulum/pendulum.component'; +import {ClothComponent} from '../pages/algorithms/cloth/cloth.component'; export class RouterConstants { @@ -72,6 +73,12 @@ export class RouterConstants { COMPONENT: PendulumComponent }; + static readonly CLOTH = { + PATH: 'algorithms/cloth', + LINK: '/algorithms/cloth', + COMPONENT: ClothComponent + }; + static readonly IMPRINT = { PATH: 'imprint', LINK: '/imprint', diff --git a/src/app/pages/algorithms/models/algorithm-category.ts b/src/app/pages/algorithms/algorithm-category.ts similarity index 100% rename from src/app/pages/algorithms/models/algorithm-category.ts rename to src/app/pages/algorithms/algorithm-category.ts diff --git a/src/app/pages/algorithms/algorithms.component.ts b/src/app/pages/algorithms/algorithms.component.ts index 8639131..722dd97 100644 --- a/src/app/pages/algorithms/algorithms.component.ts +++ b/src/app/pages/algorithms/algorithms.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit, inject } from '@angular/core'; -import { AlgorithmsService } from './service/algorithms.service'; -import { AlgorithmCategory } from './models/algorithm-category'; +import { AlgorithmsService } from './algorithms.service'; +import { AlgorithmCategory } from './algorithm-category'; import { Observable } from 'rxjs'; import { CommonModule } from '@angular/common'; import { RouterLink } from '@angular/router'; diff --git a/src/app/pages/algorithms/service/algorithms.service.ts b/src/app/pages/algorithms/algorithms.service.ts similarity index 84% rename from src/app/pages/algorithms/service/algorithms.service.ts rename to src/app/pages/algorithms/algorithms.service.ts index c56b771..0db4f56 100644 --- a/src/app/pages/algorithms/service/algorithms.service.ts +++ b/src/app/pages/algorithms/algorithms.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; -import { AlgorithmCategory } from '../models/algorithm-category'; +import { AlgorithmCategory } from './algorithm-category'; import { Observable, of } from 'rxjs'; -import {RouterConstants} from '../../../constants/RouterConstants'; +import {RouterConstants} from '../../constants/RouterConstants'; @Injectable({ providedIn: 'root' @@ -50,6 +50,12 @@ export class AlgorithmsService { title: 'ALGORITHM.PENDULUM.TITLE', description: 'ALGORITHM.PENDULUM.DESCRIPTION', routerLink: RouterConstants.PENDULUM.LINK + }, + { + id: 'cloth', + title: 'ALGORITHM.CLOTH.TITLE', + description: 'ALGORITHM.CLOTH.DESCRIPTION', + routerLink: RouterConstants.CLOTH.LINK } ]; diff --git a/src/app/pages/algorithms/cloth/cloth.component.html b/src/app/pages/algorithms/cloth/cloth.component.html new file mode 100644 index 0000000..f98a204 --- /dev/null +++ b/src/app/pages/algorithms/cloth/cloth.component.html @@ -0,0 +1,12 @@ + + + {{ 'CLOTH.TITLE' | translate }} + + + + + diff --git a/src/app/pages/algorithms/cloth/cloth.component.scss b/src/app/pages/algorithms/cloth/cloth.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/pages/algorithms/cloth/cloth.component.ts b/src/app/pages/algorithms/cloth/cloth.component.ts new file mode 100644 index 0000000..7dc5086 --- /dev/null +++ b/src/app/pages/algorithms/cloth/cloth.component.ts @@ -0,0 +1,207 @@ +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 } 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', + imports: [ + MatCard, + MatCardContent, + MatCardHeader, + MatCardTitle, + TranslatePipe, + BabylonCanvas + ], + templateUrl: './cloth.component.html', + styleUrl: './cloth.component.scss', +}) +export class ClothComponent { + private currentSceneData: SceneEventData | null = null; + + renderConfig: RenderConfig = { + mode: '3D', + initialViewSize: 20, + shaderLanguage: ShaderLanguage.WGSL + }; + + onSceneReady(event: SceneEventData) { + this.currentSceneData = event; + this.createSimulation(); + } + + private createSimulation() { + if (!this.currentSceneData){ + return; + } + const {engine, scene} = this.currentSceneData; + + // --- 1. CONFIGURE CLOTH GRID --- + const gridWidth = 50; // 50x50 = 2500 Vertices (Increase this later!) + 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); + + // --- 2. INITIALIZE CPU ARRAYS (Strict vec4 alignment) --- + const positionsData = new Float32Array(numVertices * 4); + const prevPositionsData = new Float32Array(numVertices * 4); + const velocitiesData = new Float32Array(numVertices * 4); + const constraintsData = new Float32Array(numConstraints * 4); + + // Fill Initial Positions + for (let y = 0; y < gridHeight; y++) { + for (let x = 0; x < gridWidth; x++) { + const idx = (y * gridWidth + x) * 4; + + // Center the cloth around X=0, let it hang down in Y + positionsData[idx + 0] = (x - gridWidth / 2) * spacing; // X + positionsData[idx + 1] = 5.0 - (y * spacing); // Y (Start at height 5) + positionsData[idx + 2] = 0.0; // Z + + // Inverse Mass (w-component): Pin the top row! + // If y == 0, mass is 0.0 (pinned). Otherwise 1.0 (moves freely) + positionsData[idx + 3] = (y === 0) ? 0.0 : 1.0; + + // PrevPositions start identical + prevPositionsData[idx + 0] = positionsData[idx + 0]; + prevPositionsData[idx + 1] = positionsData[idx + 1]; + prevPositionsData[idx + 2] = positionsData[idx + 2]; + prevPositionsData[idx + 3] = positionsData[idx + 3]; + } + } + + // Fill Constraints (Simple Grid: connect right and connect down) + let cIdx = 0; + for (let y = 0; y < gridHeight; y++) { + for (let x = 0; x < gridWidth; x++) { + const indexA = y * gridWidth + x; + + // Connect to right neighbor + if (x < gridWidth - 1) { + constraintsData[cIdx * 4 + 0] = indexA; // Vertex A + constraintsData[cIdx * 4 + 1] = indexA + 1; // Vertex B + constraintsData[cIdx * 4 + 2] = spacing; // Rest length + constraintsData[cIdx * 4 + 3] = 1.0; // Active flag + cIdx++; + } + // Connect to bottom neighbor + if (y < gridHeight - 1) { + constraintsData[cIdx * 4 + 0] = indexA; // Vertex A + constraintsData[cIdx * 4 + 1] = indexA + gridWidth; // Vertex B + constraintsData[cIdx * 4 + 2] = spacing; // Rest length + constraintsData[cIdx * 4 + 3] = 1.0; // Active flag + cIdx++; + } + } + } + + // Parameters Data + const paramsData = new Float32Array(8); // Matches the WGSL struct (dt, gravity, etc.) + + // --- 3. 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); + // Automatically initialized to 0 by WebGPU, no update needed initially + + const constraintsBuffer = new StorageBuffer(engine, constraintsData.byteLength); + constraintsBuffer.update(constraintsData); + + const paramsBuffer = new StorageBuffer(engine, paramsData.byteLength); + + // --- 4. 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); + + // --- SETUP: csSolve (XPBD Constraints) --- + const csSolve = new ComputeShader("solve", engine, { computeSource: CLOTH_SOLVE_COMPUTE_WGSL }, { + bindingsMapping: { + "p": { group: 0, binding: 0 }, + "positions": { group: 0, binding: 1 }, + "constraints": { group: 0, binding: 2 } + } + }); + csSolve.setStorageBuffer("p", paramsBuffer); + csSolve.setStorageBuffer("positions", positionsBuffer); + csSolve.setStorageBuffer("constraints", constraintsBuffer); + + // --- SETUP: csVelocity (Update Velocities) --- + 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); + + // --- 5. SETUP RENDER MESH --- + // We create a ground mesh that matches our grid size, but we will OVERWRITE its vertices in the shader. + const clothMesh = MeshBuilder.CreateGround("cloth", { width: 10, height: 10, subdivisions: gridWidth - 1 }, scene); + + const clothMaterial = new ShaderMaterial("clothMat", scene, { + vertexSource: CLOTH_VERTEX_SHADER_WGSL, + fragmentSource: CLOTH_FRAGMENT_SHADER_WGSL + }, { + attributes: ["position", "uv"], + uniforms: ["viewProjection"], + storageBuffers: ["positions"], + shaderLanguage: ShaderLanguage.WGSL + }); + + clothMaterial.setStorageBuffer("positions", positionsBuffer); + clothMesh.material = clothMaterial; + + clothMaterial.setStorageBuffer("positions", positionsBuffer); + clothMesh.material = clothMaterial; + + // --- 6. RENDER LOOP --- + scene.onBeforeRenderObservable.clear(); + scene.onBeforeRenderObservable.add(() => { + + // 1. Update Parameters (just an example, bind your simParams here) + paramsData[0] = 0.016; // dt + paramsData[1] = -9.81; // gravity + paramsData[2] = 0.001; // compliance (stiffness) + paramsData[3] = numVertices; + paramsData[4] = numConstraints; + paramsBuffer.update(paramsData); + + // 2. Dispatch Compute Shaders in sequence! + const dispatchXVertices = Math.ceil(numVertices / 64); + const dispatchXConstraints = Math.ceil(numConstraints / 64); + + csIntegrate.dispatch(dispatchXVertices, 1, 1); + + // For XPBD stability, you often run the solver multiple times (substeps) + for (let i = 0; i < 5; i++) { + csSolve.dispatch(dispatchXConstraints, 1, 1); + } + + 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 new file mode 100644 index 0000000..62068bb --- /dev/null +++ b/src/app/pages/algorithms/cloth/cloth.shader.ts @@ -0,0 +1,195 @@ +// --- SHARED DATA STRUCTURES --- +export const CLOTH_SHARED_STRUCTS = ` + struct Params { + dt: f32, // Time step per substep + gravity_y: f32, // Gravity (e.g. -9.81) + compliance: f32, // Inverse stiffness (0.0 = completely rigid) + numVertices: f32, // Total number of vertices + numConstraints: f32, // Total number of springs + pad1: f32, // Padding + pad2: f32, // Padding + pad3: f32 // Padding (8 * f32 = 32 bytes) + }; +`; + +// ========================================== +// VERTEX SHADER +// ========================================== +export const CLOTH_VERTEX_SHADER_WGSL = ` + attribute uv : vec2; + + // Storage Buffer + var positions : array>; + + // Babylon Preprocessor-Magie + uniform viewProjection : mat4x4; + varying vUV : vec2; + + @vertex + fn main(input : VertexInputs) -> FragmentInputs { + var output : FragmentInputs; + + let worldPos = positions[input.vertexIndex].xyz; + + output.position = uniforms.viewProjection * vec4(worldPos, 1.0); + output.vUV = input.uv; + + return output; + } +`; + +// ========================================== +// FRAGMENT SHADER (Bleibt exakt gleich) +// ========================================== +export const CLOTH_FRAGMENT_SHADER_WGSL = ` + varying vUV : vec2; + + @fragment + fn main(input: FragmentInputs) -> FragmentOutputs { + var output: FragmentOutputs; + + let color = vec3(input.vUV.x * 0.8, input.vUV.y * 0.8, 0.9); + output.color = vec4(color, 1.0); + + return output; + } +`; + +// ===================================================================== +// PASS 1: INTEGRATION (Apply Forces & Predict Positions) +// ===================================================================== +export const CLOTH_INTEGRATE_COMPUTE_WGSL = CLOTH_SHARED_STRUCTS + ` + @group(0) @binding(0) var p : Params; + @group(0) @binding(1) var positions : array>; + @group(0) @binding(2) var prev_positions : array>; + @group(0) @binding(3) var velocities : array>; + + @compute @workgroup_size(64) + fn main(@builtin(global_invocation_id) global_id : vec3) { + let idx = global_id.x; + if (f32(idx) >= p.numVertices) { return; } + + var pos = positions[idx]; + var vel = velocities[idx]; + let invMass = pos.w; // w stores inverse mass (0.0 = pinned/static) + + // Only move if it is not pinned + if (invMass > 0.0) { + // 1. Apply Gravity: v = v + g * dt + vel.y = vel.y + (p.gravity_y * p.dt); + + // 2. Save current position for later velocity calculation + prev_positions[idx] = pos; + + // 3. Predict new position: p = p + v * dt + pos.x = pos.x + vel.x * p.dt; + pos.y = pos.y + vel.y * p.dt; + pos.z = pos.z + vel.z * p.dt; + + positions[idx] = pos; + velocities[idx] = vel; + } + } +`; + +// ===================================================================== +// PASS 2: SOLVE CONSTRAINTS (The core of XPBD) +// ===================================================================== +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>; + + @compute @workgroup_size(64) + fn main(@builtin(global_invocation_id) global_id : vec3) { + let idx = global_id.x; + if (f32(idx) >= p.numConstraints) { return; } + + let constraint = constraints[idx]; + let isActive = constraint.w; // 1.0 = Active, 0.0 = Cut/Broken + + // If the cloth is cut here, skip this constraint! + if (isActive < 0.5) { return; } + + let idA = u32(constraint.x); + let idB = u32(constraint.y); + let restLength = constraint.z; + + var pA = positions[idA]; + var pB = positions[idB]; + + let wA = pA.w; // Inverse mass A + let wB = pB.w; // Inverse mass B + let wSum = wA + wB; + + // If both points are pinned, do nothing + if (wSum <= 0.0) { return; } + + let dir = pA.xyz - pB.xyz; + let dist = length(dir); + + // Prevent division by zero + if (dist < 0.0001) { return; } + + // XPBD Calculation (Extended Position-Based Dynamics) + let n = dir / dist; + let C = dist - restLength; // Constraint violation (how much it stretched) + + // Calculate the correction factor (alpha represents the XPBD compliance) + let alpha = p.compliance / (p.dt * p.dt); + let lambda = -C / (wSum + alpha); + + // Apply position corrections directly to the points + let corrA = n * (lambda * wA); + let corrB = n * (-lambda * wB); + + // NOTE: In a multi-threaded GPU environment without "Graph Coloring", + // writing directly to positions like this can cause minor race conditions + // (flickering). We will handle Graph Coloring in the TypeScript setup! + + if (wA > 0.0) { + positions[idA].x = positions[idA].x + corrA.x; + positions[idA].y = positions[idA].y + corrA.y; + positions[idA].z = positions[idA].z + corrA.z; + } + if (wB > 0.0) { + positions[idB].x = positions[idB].x + corrB.x; + positions[idB].y = positions[idB].y + corrB.y; + positions[idB].z = positions[idB].z + corrB.z; + } + } +`; + +// ===================================================================== +// PASS 3: VELOCITY UPDATE (Derive velocity from position changes) +// ===================================================================== +export const CLOTH_VELOCITY_COMPUTE_WGSL = CLOTH_SHARED_STRUCTS + ` + @group(0) @binding(0) var p : Params; + @group(0) @binding(1) var positions : array>; + @group(0) @binding(2) var prev_positions : array>; + @group(0) @binding(3) var velocities : array>; + + @compute @workgroup_size(64) + fn main(@builtin(global_invocation_id) global_id : vec3) { + let idx = global_id.x; + if (f32(idx) >= p.numVertices) { return; } + + let pos = positions[idx]; + let prev = prev_positions[idx]; + let invMass = pos.w; + + if (invMass > 0.0) { + var vel = velocities[idx]; + + // v = (p - p_prev) / dt + vel.x = (pos.x - prev.x) / p.dt; + vel.y = (pos.y - prev.y) / p.dt; + vel.z = (pos.z - prev.z) / p.dt; + + // Optional: Add simple damping here + // vel = vel * 0.99; + + velocities[idx] = vel; + } + } +`; diff --git a/src/app/shared/rendering/canvas/babylon-canvas.component.ts b/src/app/shared/rendering/canvas/babylon-canvas.component.ts index 975a187..8e61be0 100644 --- a/src/app/shared/rendering/canvas/babylon-canvas.component.ts +++ b/src/app/shared/rendering/canvas/babylon-canvas.component.ts @@ -5,9 +5,9 @@ export interface RenderConfig { mode: '2D' | '3D'; shaderLanguage?: number; //0 GLSL, 1 WGSL initialViewSize: number; - vertexShader: string; - fragmentShader: string; - uniformNames: string[]; + vertexShader?: string; + fragmentShader?: string; + uniformNames?: string[]; uniformBufferNames?: string[]; } @@ -120,8 +120,10 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy { } private createFullScreenRect() { + if (!this.config.vertexShader || !this.config.fragmentShader) { + return; + } const plane = MeshBuilder.CreatePlane("plane", {size: 100}, this.scene); - if (this.config.mode === '3D') { plane.parent = this.camera; plane.position.z = 1; @@ -134,6 +136,10 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy { } private createShaderMaterial() { + if (!this.config.vertexShader || !this.config.fragmentShader || !this.config.uniformNames) { + return; + } + this.shaderMaterial = new ShaderMaterial( "shaderMaterial", this.scene, @@ -161,8 +167,10 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy { } // default uniforms which maybe each scene has - this.shaderMaterial.setVector2("resolution", new Vector2(canvas.width, canvas.height)); - this.shaderMaterial.setVector3("cameraPosition", this.camera.position); + if (this.shaderMaterial) { + this.shaderMaterial.setVector2("resolution", new Vector2(canvas.width, canvas.height)); + this.shaderMaterial.setVector3("cameraPosition", this.camera.position); + } this.scene.render(); }); diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 6f46219..dcd275b 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -471,6 +471,9 @@ "DISCLAIMER_BOTTOM": "HINWEIS: Wenn zuviele Impulse in das System gegeben werden, wird die Simulation instabil. Dann hängt das Pendel nur noch runter und es muss neu gestartet werden." } }, + "CLOTH": { + "TITLE": "Stoff-Simulation" + }, "ALGORITHM": { "TITLE": "Algorithmen", "PATHFINDING": { @@ -501,6 +504,10 @@ "TITLE": "Doppel-Pendel", "DESCRIPTION": "Visualisierung einer chaotischen Doppel-Pendel-Simulation mit WebGPU." }, + "CLOTH": { + "TITLE": "Stoff-Simulation", + "DESCRIPTION": "Simulation on Stoff mit WebGPU." + } "NOTE": "HINWEIS", "GRID_HEIGHT": "Höhe", "GRID_WIDTH": "Beite" diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index a5edf3d..186685b 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -470,6 +470,9 @@ "DISCLAIMER_BOTTOM": "NOTE: If too many impulses are fed into the system, the simulation becomes unstable. The pendulum will then just hang down and the simulation will have to be restarted." } }, + "CLOTH": { + "TITLE": "Cloth simulation" + }, "ALGORITHM": { "TITLE": "Algorithms", "PATHFINDING": { @@ -500,6 +503,10 @@ "TITLE": "Double pendulum", "DESCRIPTION": "Visualisation of a chaotic double pendulum simulation with WebGPU." }, + "CLOTH": { + "TITLE": "Cloth simulation", + "DESCRIPTION": "Simulation of cloth with WebGPU." + }, "NOTE": "Note", "GRID_HEIGHT": "Height", "GRID_WIDTH": "Width" From 746022c48daeb7fbbd635fa088d83e454346b1f4 Mon Sep 17 00:00:00 2001 From: Lobo Date: Mon, 23 Feb 2026 11:07:09 +0100 Subject: [PATCH 2/8] Update cloth.component.ts --- src/app/pages/algorithms/cloth/cloth.component.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/app/pages/algorithms/cloth/cloth.component.ts b/src/app/pages/algorithms/cloth/cloth.component.ts index 7dc5086..b0e1998 100644 --- a/src/app/pages/algorithms/cloth/cloth.component.ts +++ b/src/app/pages/algorithms/cloth/cloth.component.ts @@ -2,7 +2,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/rendering/canvas/babylon-canvas.component'; -import { ComputeShader, StorageBuffer, MeshBuilder, ShaderMaterial, ShaderLanguage } from '@babylonjs/core'; +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({ @@ -172,11 +172,16 @@ export class ClothComponent { shaderLanguage: ShaderLanguage.WGSL }); + clothMaterial.backFaceCulling = false; clothMaterial.setStorageBuffer("positions", positionsBuffer); clothMesh.material = clothMaterial; - clothMaterial.setStorageBuffer("positions", positionsBuffer); - clothMesh.material = clothMaterial; + const camera = scene.activeCamera as ArcRotateCamera; + if (camera) { + camera.alpha = Math.PI / 4; + camera.beta = Math.PI / 2.5; + camera.radius = 15; + } // --- 6. RENDER LOOP --- scene.onBeforeRenderObservable.clear(); @@ -194,14 +199,14 @@ export class ClothComponent { const dispatchXVertices = Math.ceil(numVertices / 64); const dispatchXConstraints = Math.ceil(numConstraints / 64); - csIntegrate.dispatch(dispatchXVertices, 1, 1); + /*csIntegrate.dispatch(dispatchXVertices, 1, 1); // For XPBD stability, you often run the solver multiple times (substeps) for (let i = 0; i < 5; i++) { csSolve.dispatch(dispatchXConstraints, 1, 1); } - csVelocity.dispatch(dispatchXVertices, 1, 1); + csVelocity.dispatch(dispatchXVertices, 1, 1);*/ }); } } From 728dbc047f5af1c407cdd3687f591ac4c8bf3c5e Mon Sep 17 00:00:00 2001 From: Lobo Date: Mon, 23 Feb 2026 11:19:28 +0100 Subject: [PATCH 3/8] Use 4-phase graph-coloring for constraints Split cloth constraints into 4 graph-colored phases (horizontal even/odd, vertical even/odd) instead of one big constraints array. Create dynamic JS arrays (constraintsP0..P3) with an addConstraint helper, allocate four GPU constraint buffers and four corresponding solve compute shaders (csSolve0..csSolve3) via a createSolver helper, and dispatch them per substep to avoid write-write races. Update integrate/velocity shader bindings setup and dispatch logic; keep positions/prevPositions/velocities buffers as before. In WGSL, mark constraints as read-only and use arrayLength(&constraints) to bound-check the constraint index instead of relying on a CPU-side count. Also tweak sim parameter (compliance lowered) and minor refactors/cleanups for clarity and consistency. --- .../pages/algorithms/cloth/cloth.component.ts | 154 ++++++++---------- .../pages/algorithms/cloth/cloth.shader.ts | 25 +-- 2 files changed, 77 insertions(+), 102 deletions(-) diff --git a/src/app/pages/algorithms/cloth/cloth.component.ts b/src/app/pages/algorithms/cloth/cloth.component.ts index b0e1998..84e3748 100644 --- a/src/app/pages/algorithms/cloth/cloth.component.ts +++ b/src/app/pages/algorithms/cloth/cloth.component.ts @@ -47,27 +47,30 @@ export class ClothComponent { // Calculate approximate constraints (horizontal + vertical edges) const numConstraints = (gridWidth - 1) * gridHeight + gridWidth * (gridHeight - 1); - // --- 2. INITIALIZE CPU ARRAYS (Strict vec4 alignment) --- const positionsData = new Float32Array(numVertices * 4); const prevPositionsData = new Float32Array(numVertices * 4); const velocitiesData = new Float32Array(numVertices * 4); - const constraintsData = new Float32Array(numConstraints * 4); - // Fill Initial Positions + // Arrays für unsere 4 Phasen (dynamische Größe, da wir pushen) + 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) => { + arr.push(a, b, spacing, 1.0); + }; + + // Positionen füllen (bleibt wie vorher) 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 - // Center the cloth around X=0, let it hang down in Y - positionsData[idx + 0] = (x - gridWidth / 2) * spacing; // X - positionsData[idx + 1] = 5.0 - (y * spacing); // Y (Start at height 5) - positionsData[idx + 2] = 0.0; // Z - - // Inverse Mass (w-component): Pin the top row! - // If y == 0, mass is 0.0 (pinned). Otherwise 1.0 (moves freely) - positionsData[idx + 3] = (y === 0) ? 0.0 : 1.0; - - // PrevPositions start identical prevPositionsData[idx + 0] = positionsData[idx + 0]; prevPositionsData[idx + 1] = positionsData[idx + 1]; prevPositionsData[idx + 2] = positionsData[idx + 2]; @@ -75,33 +78,25 @@ export class ClothComponent { } } - // Fill Constraints (Simple Grid: connect right and connect down) - let cIdx = 0; + // --- GRAPH COLORING: Constraints in 4 Phasen füllen --- + // Phase 0: Horizontal Gerade for (let y = 0; y < gridHeight; y++) { - for (let x = 0; x < gridWidth; x++) { - const indexA = y * gridWidth + x; - - // Connect to right neighbor - if (x < gridWidth - 1) { - constraintsData[cIdx * 4 + 0] = indexA; // Vertex A - constraintsData[cIdx * 4 + 1] = indexA + 1; // Vertex B - constraintsData[cIdx * 4 + 2] = spacing; // Rest length - constraintsData[cIdx * 4 + 3] = 1.0; // Active flag - cIdx++; - } - // Connect to bottom neighbor - if (y < gridHeight - 1) { - constraintsData[cIdx * 4 + 0] = indexA; // Vertex A - constraintsData[cIdx * 4 + 1] = indexA + gridWidth; // Vertex B - constraintsData[cIdx * 4 + 2] = spacing; // Rest length - constraintsData[cIdx * 4 + 3] = 1.0; // Active flag - cIdx++; - } - } + for (let x = 0; x < gridWidth - 1; x += 2) addConstraint(constraintsP0, y * gridWidth + x, y * gridWidth + x + 1); + } + // Phase 1: Horizontal Ungerade + 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); + } + // Phase 2: Vertikal Gerade + 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: Vertikal Ungerade + 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); } - // Parameters Data - const paramsData = new Float32Array(8); // Matches the WGSL struct (dt, gravity, etc.) + const paramsData = new Float32Array(8); // --- 3. CREATE GPU STORAGE BUFFERS --- const positionsBuffer = new StorageBuffer(engine, positionsData.byteLength); @@ -111,52 +106,38 @@ export class ClothComponent { prevPositionsBuffer.update(prevPositionsData); const velocitiesBuffer = new StorageBuffer(engine, velocitiesData.byteLength); - // Automatically initialized to 0 by WebGPU, no update needed initially - - const constraintsBuffer = new StorageBuffer(engine, constraintsData.byteLength); - constraintsBuffer.update(constraintsData); - 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)); + // --- 4. 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); - // --- SETUP: csSolve (XPBD Constraints) --- - const csSolve = new ComputeShader("solve", engine, { computeSource: CLOTH_SOLVE_COMPUTE_WGSL }, { - bindingsMapping: { - "p": { group: 0, binding: 0 }, - "positions": { group: 0, binding: 1 }, - "constraints": { group: 0, binding: 2 } - } - }); - csSolve.setStorageBuffer("p", paramsBuffer); - csSolve.setStorageBuffer("positions", positionsBuffer); - csSolve.setStorageBuffer("constraints", constraintsBuffer); + // Hilfsfunktion, um die 4 Solve-Shader sauber zu erstellen + const createSolver = (name: string, cBuffer: StorageBuffer) => { + 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 } } + }); + cs.setStorageBuffer("p", paramsBuffer); cs.setStorageBuffer("positions", positionsBuffer); cs.setStorageBuffer("constraints", cBuffer); + return cs; + }; + + const csSolve0 = createSolver("solve0", cBuffer0); + const csSolve1 = createSolver("solve1", cBuffer1); + const csSolve2 = createSolver("solve2", cBuffer2); + const csSolve3 = createSolver("solve3", cBuffer3); - // --- SETUP: csVelocity (Update Velocities) --- 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. @@ -180,33 +161,34 @@ export class ClothComponent { if (camera) { camera.alpha = Math.PI / 4; camera.beta = Math.PI / 2.5; - camera.radius = 15; + camera.radius = 15; } // --- 6. RENDER LOOP --- scene.onBeforeRenderObservable.clear(); scene.onBeforeRenderObservable.add(() => { - // 1. Update Parameters (just an example, bind your simParams here) - paramsData[0] = 0.016; // dt - paramsData[1] = -9.81; // gravity - paramsData[2] = 0.001; // compliance (stiffness) + paramsData[0] = 0.016; + paramsData[1] = -9.81; + paramsData[2] = 0.0001; // Compliance (sehr klein = steifer Stoff) paramsData[3] = numVertices; - paramsData[4] = numConstraints; paramsBuffer.update(paramsData); - // 2. Dispatch Compute Shaders in sequence! const dispatchXVertices = Math.ceil(numVertices / 64); - const dispatchXConstraints = Math.ceil(numConstraints / 64); - /*csIntegrate.dispatch(dispatchXVertices, 1, 1); + // 1. Positionen vorhersehen + csIntegrate.dispatch(dispatchXVertices, 1, 1); - // For XPBD stability, you often run the solver multiple times (substeps) + // 2. XPBD Solver (Substeps) - Jede Farbe einzeln lösen! for (let i = 0; i < 5; i++) { - csSolve.dispatch(dispatchXConstraints, 1, 1); + 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); } - csVelocity.dispatch(dispatchXVertices, 1, 1);*/ + // 3. Geschwindigkeiten aktualisieren + 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 62068bb..5d06ed2 100644 --- a/src/app/pages/algorithms/cloth/cloth.shader.ts +++ b/src/app/pages/algorithms/cloth/cloth.shader.ts @@ -98,17 +98,18 @@ 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>; + @group(0) @binding(2) var constraints : array>; // <--- Nur "read", da wir sie hier nicht verändern @compute @workgroup_size(64) fn main(@builtin(global_invocation_id) global_id : vec3) { let idx = global_id.x; - if (f32(idx) >= p.numConstraints) { return; } + + // HIER: Wir fragen die GPU direkt, wie groß das übergebene Array ist! + if (idx >= arrayLength(&constraints)) { return; } let constraint = constraints[idx]; - let isActive = constraint.w; // 1.0 = Active, 0.0 = Cut/Broken + let isActive = constraint.w; - // If the cloth is cut here, skip this constraint! if (isActive < 0.5) { return; } let idA = u32(constraint.x); @@ -118,35 +119,27 @@ export const CLOTH_SOLVE_COMPUTE_WGSL = CLOTH_SHARED_STRUCTS + ` var pA = positions[idA]; var pB = positions[idB]; - let wA = pA.w; // Inverse mass A - let wB = pB.w; // Inverse mass B + let wA = pA.w; + let wB = pB.w; let wSum = wA + wB; - // If both points are pinned, do nothing if (wSum <= 0.0) { return; } let dir = pA.xyz - pB.xyz; let dist = length(dir); - // Prevent division by zero if (dist < 0.0001) { return; } - // XPBD Calculation (Extended Position-Based Dynamics) let n = dir / dist; - let C = dist - restLength; // Constraint violation (how much it stretched) + let C = dist - restLength; - // Calculate the correction factor (alpha represents the XPBD compliance) let alpha = p.compliance / (p.dt * p.dt); let lambda = -C / (wSum + alpha); - // Apply position corrections directly to the points let corrA = n * (lambda * wA); let corrB = n * (-lambda * wB); - // NOTE: In a multi-threaded GPU environment without "Graph Coloring", - // writing directly to positions like this can cause minor race conditions - // (flickering). We will handle Graph Coloring in the TypeScript setup! - + // This is because we are using graph coloring to be thread safe if (wA > 0.0) { positions[idA].x = positions[idA].x + corrA.x; positions[idA].y = positions[idA].y + corrA.y; From f6562066911b4edc987bdbc694687f3789ff9ecc Mon Sep 17 00:00:00 2001 From: Lobo Date: Mon, 23 Feb 2026 11:30:05 +0100 Subject: [PATCH 4/8] Refactor cloth component and shaders Improve readability, typing and structure for the cloth simulation component and WGSL shaders. Changes include: formatted imports, added file/header JSDoc and inline comments, made renderConfig and lifecycle methods public with explicit types, renamed component selector to 'app-cloth', converted several functions to typed helpers (e.g. createAndPopulateBuffer, addConstraint), consolidated buffer creation, and cleaned up compute shader binding mappings. Shader file receives file header and minor comment clarifications and a bounds check comment; overall changes are stylistic and organizational to increase maintainability and clarity without altering core algorithm behavior. --- .../pages/algorithms/cloth/cloth.component.ts | 156 ++++++++++++------ .../pages/algorithms/cloth/cloth.shader.ts | 15 +- 2 files changed, 113 insertions(+), 58 deletions(-) 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]; From ed0e370e9d627b6be301dda5a437815bd648cc5f Mon Sep 17 00:00:00 2001 From: Lobo Date: Tue, 24 Feb 2026 08:27:59 +0100 Subject: [PATCH 5/8] Fixed some styling issues --- src/app/pages/algorithms/algorithms.component.html | 8 +++----- src/app/pages/algorithms/cloth/cloth.component.ts | 2 +- .../pages/algorithms/fractal/fractal.component.ts | 2 +- .../algorithms/fractal3d/fractal3d.component.ts | 2 +- .../pages/algorithms/pendulum/pendulum.component.ts | 2 +- src/app/pages/projects/projects.component.html | 2 +- .../particles-background.component.scss | 1 + .../render-canvas}/babylon-canvas.component.html | 0 .../render-canvas}/babylon-canvas.component.scss | 0 .../render-canvas}/babylon-canvas.component.ts | 0 src/styles.scss | 13 +++++++++++-- 11 files changed, 20 insertions(+), 12 deletions(-) rename src/app/shared/{rendering/canvas => components/render-canvas}/babylon-canvas.component.html (100%) rename src/app/shared/{rendering/canvas => components/render-canvas}/babylon-canvas.component.scss (100%) rename src/app/shared/{rendering/canvas => components/render-canvas}/babylon-canvas.component.ts (100%) diff --git a/src/app/pages/algorithms/algorithms.component.html b/src/app/pages/algorithms/algorithms.component.html index 2bad6cd..33b785e 100644 --- a/src/app/pages/algorithms/algorithms.component.html +++ b/src/app/pages/algorithms/algorithms.component.html @@ -1,8 +1,7 @@ -
-

{{ 'ALGORITHM.TITLE' |translate }}

-
+

{{ 'ALGORITHM.TITLE' |translate }}

+
@for (category of categories$ | async; track category.id) { - + {{ category.title | translate }} @@ -12,4 +11,3 @@ }
-
diff --git a/src/app/pages/algorithms/cloth/cloth.component.ts b/src/app/pages/algorithms/cloth/cloth.component.ts index 20b81a0..f51779d 100644 --- a/src/app/pages/algorithms/cloth/cloth.component.ts +++ b/src/app/pages/algorithms/cloth/cloth.component.ts @@ -6,7 +6,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/rendering/canvas/babylon-canvas.component'; +import { BabylonCanvas, RenderConfig, SceneEventData } from '../../../shared/components/render-canvas/babylon-canvas.component'; import { ComputeShader, StorageBuffer, MeshBuilder, ShaderMaterial, ShaderLanguage, ArcRotateCamera } from '@babylonjs/core'; import { CLOTH_FRAGMENT_SHADER_WGSL, diff --git a/src/app/pages/algorithms/fractal/fractal.component.ts b/src/app/pages/algorithms/fractal/fractal.component.ts index acd18aa..c1c6707 100644 --- a/src/app/pages/algorithms/fractal/fractal.component.ts +++ b/src/app/pages/algorithms/fractal/fractal.component.ts @@ -8,7 +8,7 @@ import {MatSelect} from '@angular/material/select'; import {AlgorithmInformation} from '../information/information.models'; import {UrlConstants} from '../../../constants/UrlConstants'; import {FormsModule} from '@angular/forms'; -import {BabylonCanvas, RenderCallback, RenderConfig, SceneEventData} from '../../../shared/rendering/canvas/babylon-canvas.component'; +import {BabylonCanvas, RenderCallback, RenderConfig, SceneEventData} from '../../../shared/components/render-canvas/babylon-canvas.component'; import {FRACTAL2D_FRAGMENT, FRACTAL2D_VERTEX} from './fractal.shader'; import {PointerEventTypes, PointerInfo, ShaderMaterial, Vector2} from '@babylonjs/core'; import {MatButton} from '@angular/material/button'; diff --git a/src/app/pages/algorithms/fractal3d/fractal3d.component.ts b/src/app/pages/algorithms/fractal3d/fractal3d.component.ts index b65351b..b9244b7 100644 --- a/src/app/pages/algorithms/fractal3d/fractal3d.component.ts +++ b/src/app/pages/algorithms/fractal3d/fractal3d.component.ts @@ -7,7 +7,7 @@ import {TranslatePipe} from '@ngx-translate/core'; import {AlgorithmInformation} from '../information/information.models'; import {UrlConstants} from '../../../constants/UrlConstants'; import {MatButton} from '@angular/material/button'; -import {BabylonCanvas, RenderCallback, RenderConfig} from '../../../shared/rendering/canvas/babylon-canvas.component'; +import {BabylonCanvas, RenderCallback, RenderConfig} from '../../../shared/components/render-canvas/babylon-canvas.component'; @Component({ selector: 'app-fractal3d', diff --git a/src/app/pages/algorithms/pendulum/pendulum.component.ts b/src/app/pages/algorithms/pendulum/pendulum.component.ts index 3c27ef4..67f0d78 100644 --- a/src/app/pages/algorithms/pendulum/pendulum.component.ts +++ b/src/app/pages/algorithms/pendulum/pendulum.component.ts @@ -1,5 +1,5 @@ import {Component} from '@angular/core'; -import {BabylonCanvas, RenderConfig, SceneEventData} from '../../../shared/rendering/canvas/babylon-canvas.component'; +import {BabylonCanvas, RenderConfig, SceneEventData} from '../../../shared/components/render-canvas/babylon-canvas.component'; import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card'; import {ComputeShader, ShaderLanguage, StorageBuffer} from '@babylonjs/core'; import {PENDULUM_FRAGMENT_SHADER_WGSL, PENDULUM_PHYSIC_COMPUTE_SHADER_WGSL, PENDULUM_RENDER_COMPUTE_SHADER_WGSL, PENDULUM_VERTEX_SHADER_WGSL} from './pendulum.shader'; diff --git a/src/app/pages/projects/projects.component.html b/src/app/pages/projects/projects.component.html index 5d5f968..f11ad3d 100644 --- a/src/app/pages/projects/projects.component.html +++ b/src/app/pages/projects/projects.component.html @@ -1,4 +1,4 @@ -
+
@if (featuredProject(); as project) { diff --git a/src/app/shared/components/particles-background/particles-background.component.scss b/src/app/shared/components/particles-background/particles-background.component.scss index 1024a3c..2f4354d 100644 --- a/src/app/shared/components/particles-background/particles-background.component.scss +++ b/src/app/shared/components/particles-background/particles-background.component.scss @@ -11,4 +11,5 @@ canvas { display: block; width: 100%; height: 100%; + border-width: 0; } diff --git a/src/app/shared/rendering/canvas/babylon-canvas.component.html b/src/app/shared/components/render-canvas/babylon-canvas.component.html similarity index 100% rename from src/app/shared/rendering/canvas/babylon-canvas.component.html rename to src/app/shared/components/render-canvas/babylon-canvas.component.html diff --git a/src/app/shared/rendering/canvas/babylon-canvas.component.scss b/src/app/shared/components/render-canvas/babylon-canvas.component.scss similarity index 100% rename from src/app/shared/rendering/canvas/babylon-canvas.component.scss rename to src/app/shared/components/render-canvas/babylon-canvas.component.scss diff --git a/src/app/shared/rendering/canvas/babylon-canvas.component.ts b/src/app/shared/components/render-canvas/babylon-canvas.component.ts similarity index 100% rename from src/app/shared/rendering/canvas/babylon-canvas.component.ts rename to src/app/shared/components/render-canvas/babylon-canvas.component.ts diff --git a/src/styles.scss b/src/styles.scss index d4a918e..13b6e7c 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -702,12 +702,21 @@ app-root { } /* ---- Projects Page & Dialog ---- */ -.project-grid { +.card-grid { display: grid; gap: clamp(1rem, 3vw, 1.5rem); grid-template-columns: repeat(auto-fill, minmax(min(100%, 450px), 1fr)); max-width: var(--app-maxWidth); - margin: 0 auto; + margin-right: 1rem; + margin-left: 1rem; + margin-top: auto; +} + +.algo-card { + transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out; + display: flex; + flex-direction: column; + cursor: pointer; } .project-card { From 14d7a78ac492938195c54370a789d4b9d1f6a89f Mon Sep 17 00:00:00 2001 From: Lobo Date: Tue, 24 Feb 2026 08:46:57 +0100 Subject: [PATCH 6/8] Added wind to cloth simulation --- .../algorithms/cloth/cloth.component.html | 7 +++ .../pages/algorithms/cloth/cloth.component.ts | 53 +++++++++++++++---- .../pages/algorithms/cloth/cloth.shader.ts | 34 +++++++----- src/assets/i18n/de.json | 4 +- src/assets/i18n/en.json | 4 +- 5 files changed, 77 insertions(+), 25 deletions(-) diff --git a/src/app/pages/algorithms/cloth/cloth.component.html b/src/app/pages/algorithms/cloth/cloth.component.html index f98a204..ed559ae 100644 --- a/src/app/pages/algorithms/cloth/cloth.component.html +++ b/src/app/pages/algorithms/cloth/cloth.component.html @@ -3,6 +3,13 @@ {{ 'CLOTH.TITLE' | translate }} +
+
+ +
+
{ + this.simulationTime += engine.getDeltaTime() / 1000.0; + + const windX = this.isWindActive ? 5.0 : 0.0; + const windY = 0.0; + const windZ = this.isWindActive ? 15.0 : 0.0; + + const baseCompliance = 0.00001; + const scaledCompliance = baseCompliance * particleInvMass * spacing; + paramsData[0] = 0.016; paramsData[1] = -9.81; - paramsData[2] = 0.0001; // Compliance (very small = stiff fabric) + paramsData[2] = scaledCompliance; //scaled stiffness paramsData[3] = numVertices; + paramsData[4] = windX; + paramsData[5] = windY; + paramsData[6] = windZ; + paramsData[7] = this.simulationTime; + paramsBuffer.update(paramsData); const dispatchXVertices = Math.ceil(numVertices / 64); @@ -230,7 +262,8 @@ export class ClothComponent { csIntegrate.dispatch(dispatchXVertices, 1, 1); // 2. XPBD Solver (Substeps) - Solve each color individually - for (let i = 0; i < 5; i++) { + const substeps = 15; + 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); diff --git a/src/app/pages/algorithms/cloth/cloth.shader.ts b/src/app/pages/algorithms/cloth/cloth.shader.ts index f8ea79c..08d055b 100644 --- a/src/app/pages/algorithms/cloth/cloth.shader.ts +++ b/src/app/pages/algorithms/cloth/cloth.shader.ts @@ -6,14 +6,14 @@ // --- SHARED DATA STRUCTURES --- export const CLOTH_SHARED_STRUCTS = ` struct Params { - dt: f32, // Time step per substep - gravity_y: f32, // Gravity (e.g. -9.81) - compliance: f32, // Inverse stiffness (0.0 = completely rigid) - numVertices: f32, // Total number of vertices - numConstraints: f32, // Total number of springs - pad1: f32, // Padding - pad2: f32, // Padding - pad3: f32 // Padding (8 * f32 = 32 bytes) + dt: f32, + gravity_y: f32, + compliance: f32, + numVertices: f32, + wind_x: f32, + wind_y: f32, + wind_z: f32, + time: f32 }; `; @@ -76,17 +76,25 @@ export const CLOTH_INTEGRATE_COMPUTE_WGSL = CLOTH_SHARED_STRUCTS + ` var pos = positions[idx]; var vel = velocities[idx]; - let invMass = pos.w; // w stores inverse mass (0.0 = pinned/static) + let invMass = pos.w; - // Only move if it is not pinned if (invMass > 0.0) { - // 1. Apply Gravity: v = v + g * dt vel.y = vel.y + (p.gravity_y * p.dt); - // 2. Save current position for later velocity calculation + let flutter = sin(pos.x * 2.0 + p.time * 5.0) * cos(pos.y * 2.0 + p.time * 3.0); + + let windForce = vec3( + p.wind_x + (flutter * p.wind_x * 0.8), + p.wind_y + (flutter * 2.0), // Leichter Auftrieb durchs Flattern + p.wind_z + (flutter * p.wind_z * 0.8) + ); + + vel.x = vel.x + (windForce.x * p.dt); + vel.y = vel.y + (windForce.y * p.dt); + vel.z = vel.z + (windForce.z * p.dt); + prev_positions[idx] = pos; - // 3. Predict new position: p = p + v * dt pos.x = pos.x + vel.x * p.dt; pos.y = pos.y + vel.y * p.dt; pos.z = pos.z + vel.z * p.dt; diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index dcd275b..014c568 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -472,7 +472,9 @@ } }, "CLOTH": { - "TITLE": "Stoff-Simulation" + "TITLE": "Stoff-Simulation", + "WIND_ON": "Wind Einschalten", + "WIND_OFF": "Wind Ausschalten" }, "ALGORITHM": { "TITLE": "Algorithmen", diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 186685b..4681f5e 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -471,7 +471,9 @@ } }, "CLOTH": { - "TITLE": "Cloth simulation" + "TITLE": "Cloth simulation", + "WIND_ON": "Wind On", + "WIND_OFF": "Wind Off" }, "ALGORITHM": { "TITLE": "Algorithms", From 12411e58bff3f57162c3d68ac58b3ecee3e78104 Mon Sep 17 00:00:00 2001 From: Lobo Date: Tue, 24 Feb 2026 08:51:53 +0100 Subject: [PATCH 7/8] Refactored the cloth class for better reading --- .../pages/algorithms/cloth/cloth.component.ts | 267 ++++++++++-------- src/app/pages/algorithms/cloth/cloth.model.ts | 36 +++ 2 files changed, 184 insertions(+), 119 deletions(-) create mode 100644 src/app/pages/algorithms/cloth/cloth.model.ts 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; +} From ab3bca43958dd5b9b91b6720aadeeefdbcd9b66c Mon Sep 17 00:00:00 2001 From: Lobo Date: Tue, 24 Feb 2026 09:28:16 +0100 Subject: [PATCH 8/8] Cloth: add info, outline, diagonals, shader Add an informational panel and mesh-outline toggle to the cloth demo, plus richer physics and shading. The cloth component now provides AlgorithmInformation to an view and a toggleMesh() that flips the mesh wireframe. Constraint generation was extended with four diagonal phases (constraintsP4..P7) and the solver loop was generalized to iterate solver pipelines, improving parallel XPBD constraint handling. The WGSL vertex/fragment shaders were updated to pass world positions, compute normals, add simple lighting and a grid-based base color. Also update information template/model to support optional translated entry names and expand i18n (DE/EN) with cloth texts and a Docker key. --- .../algorithms/cloth/cloth.component.html | 4 + .../pages/algorithms/cloth/cloth.component.ts | 85 +++++++++++++++++-- .../pages/algorithms/cloth/cloth.shader.ts | 24 ++++-- .../algorithms/information/information.html | 9 +- .../information/information.models.ts | 2 +- src/assets/i18n/de.json | 29 +++++-- src/assets/i18n/en.json | 23 ++++- 7 files changed, 154 insertions(+), 22 deletions(-) diff --git a/src/app/pages/algorithms/cloth/cloth.component.html b/src/app/pages/algorithms/cloth/cloth.component.html index ed559ae..ccbe516 100644 --- a/src/app/pages/algorithms/cloth/cloth.component.html +++ b/src/app/pages/algorithms/cloth/cloth.component.html @@ -3,11 +3,15 @@ {{ 'CLOTH.TITLE' | translate }} +
+
{ + arr.push(a, b, diagSpacing, 1.0); + }; + + for (let y = 0; y < config.gridHeight - 1; y++) { + const arr = (y % 2 === 0) ? constraintsP4 : constraintsP5; + for (let x = 0; x < config.gridWidth - 1; x++) { + addDiagConstraint(arr, y * config.gridWidth + x, (y + 1) * config.gridWidth + (x + 1)); + } + } + + for (let y = 0; y < config.gridHeight - 1; y++) { + const arr = (y % 2 === 0) ? constraintsP6 : constraintsP7; + for (let x = 0; x < config.gridWidth - 1; x++) { + addDiagConstraint(arr, y * config.gridWidth + (x + 1), (y + 1) * config.gridWidth + x); + } + } + return { positions: positionsData, prevPositions: prevPositionsData, velocities: velocitiesData, - constraints: [constraintsP0, constraintsP1, constraintsP2, constraintsP3], + constraints: [ + constraintsP0, constraintsP1, constraintsP2, constraintsP3, + constraintsP4, constraintsP5, constraintsP6, constraintsP7 + ], params: new Float32Array(8) }; } @@ -293,10 +365,9 @@ export class ClothComponent { // 2. XPBD Solver (Substeps) - Graph Coloring Phase for (let i = 0; i < substeps; i++) { - 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); + for (let phase = 0; phase < pipelines.solvers.length; phase++) { + pipelines.solvers[phase].dispatch(dispatchXConstraints[phase], 1, 1); + } } // 3. Update velocities diff --git a/src/app/pages/algorithms/cloth/cloth.shader.ts b/src/app/pages/algorithms/cloth/cloth.shader.ts index 08d055b..94652d7 100644 --- a/src/app/pages/algorithms/cloth/cloth.shader.ts +++ b/src/app/pages/algorithms/cloth/cloth.shader.ts @@ -22,22 +22,23 @@ export const CLOTH_SHARED_STRUCTS = ` // ========================================== export const CLOTH_VERTEX_SHADER_WGSL = ` attribute uv : vec2; - - // Storage Buffer var positions : array>; - // Babylon Preprocessor Magic uniform viewProjection : mat4x4; + + // Varyings, um Daten an den Fragment-Shader zu senden varying vUV : vec2; + varying vWorldPos : vec3; // NEU: Wir brauchen die 3D-Position für das Licht! @vertex fn main(input : VertexInputs) -> FragmentInputs { var output : FragmentInputs; let worldPos = positions[input.vertexIndex].xyz; - output.position = uniforms.viewProjection * vec4(worldPos, 1.0); + output.vUV = input.uv; + output.vWorldPos = worldPos; // Position weitergeben return output; } @@ -48,13 +49,24 @@ export const CLOTH_VERTEX_SHADER_WGSL = ` // ========================================== export const CLOTH_FRAGMENT_SHADER_WGSL = ` varying vUV : vec2; + varying vWorldPos : vec3; @fragment fn main(input: FragmentInputs) -> FragmentOutputs { var output: FragmentOutputs; - let color = vec3(input.vUV.x * 0.8, input.vUV.y * 0.8, 0.9); - output.color = vec4(color, 1.0); + let dx = dpdx(input.vWorldPos); + let dy = dpdy(input.vWorldPos); + let normal = normalize(cross(dx, dy)); + let lightDir = normalize(vec3(1.0, 1.0, 0.5)); + let diffuse = max(0.0, abs(dot(normal, lightDir))); + let ambient = 0.3; + let lightIntensity = ambient + (diffuse * 0.7); + let grid = (floor(input.vUV.x * 20.0) + floor(input.vUV.y * 20.0)) % 2.0; + let baseColor = mix(vec3(0.8, 0.4, 0.15), vec3(0.9, 0.5, 0.2), grid); + let finalColor = baseColor * lightIntensity; + + output.color = vec4(finalColor, 1.0); return output; } diff --git a/src/app/pages/algorithms/information/information.html b/src/app/pages/algorithms/information/information.html index a38c736..617374f 100644 --- a/src/app/pages/algorithms/information/information.html +++ b/src/app/pages/algorithms/information/information.html @@ -5,7 +5,14 @@ @for (algo of algorithmInformation.entries; track algo) {

- {{ algo.name }} {{ algo.description | translate }} + + @if(algo.translateName){ + {{ algo.name | translate}} + } @else { + {{ algo.name }} + } + + {{ algo.description | translate }} Wikipedia

} diff --git a/src/app/pages/algorithms/information/information.models.ts b/src/app/pages/algorithms/information/information.models.ts index 9cd064b..7b7e8bc 100644 --- a/src/app/pages/algorithms/information/information.models.ts +++ b/src/app/pages/algorithms/information/information.models.ts @@ -10,5 +10,5 @@ export interface AlgorithmEntry { name: string; description: string; link: string; - + translateName?: boolean; } diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 014c568..d278b6f 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -57,7 +57,8 @@ "K8S": "Kubernetes / k3d", "POSTGRES": "PostgreSQL", "MONGO": "MongoDB", - "GRAFANA": "Grafana/Prometheus" + "GRAFANA": "Grafana/Prometheus", + "DOCKER": "Docker" }, "XP": { "COMPANY8": { @@ -472,9 +473,27 @@ } }, "CLOTH": { - "TITLE": "Stoff-Simulation", + "TITLE": "Stoffsimulation", "WIND_ON": "Wind Einschalten", - "WIND_OFF": "Wind Ausschalten" + "WIND_OFF": "Wind Ausschalten", + "OUTLINE_ON": "Mesh anzeigen", + "OUTLINE_OFF": "Mesh ausschalten", + "EXPLANATION": { + "TITLE": "Echtzeit-Stoffsimulation auf der GPU", + "CLOTH_SIMULATION_EXPLANATION_TITLE": "Stoffsimulation", + "XPBD_EXPLANATION_TITLE": "XPBD (Extended Position-Based Dynamics)", + "GPU_PARALLELIZATION_EXPLANATION_TITLE": "GPU Parallelisierung", + "DATA_STRUCTURES_EXPLANATION_TITLE": "Datenstrukturen", + "CLOTH_SIMULATION_EXPLANATION": "Stoffsimulationen modellieren Textilien meist als ein Gitter aus Massepunkten (Vertices), die durch unsichtbare Verbindungen zusammengehalten werden. Ziel ist es, physikalische Einflüsse wie Schwerkraft, Wind und Kollisionen in Echtzeit darzustellen, ohne dass das Material zerreißt oder sich unnatürlich wie Gummi dehnt.", + "XPBD_EXPLANATION": "XPBD (Extended Position-Based Dynamics) ist ein moderner Algorithmus, der statt Beschleunigungen direkt die Positionen der Punkte manipuliert, um Abstandsbedingungen (Constraints) zu erfüllen. Das 'Extended' bedeutet, dass echte physikalische Steifigkeit unabhängig von der Framerate simuliert wird. Vorteil: Absolut stabil, explodiert nicht und topologische Änderungen (wie das Zerschneiden von Stoff) sind trivial. Nachteil: Es ist ein iteratives Näherungsverfahren und physikalisch minimal weniger akkurat als komplexe Matrix-Löser.", + "GPU_PARALLELIZATION_EXPLANATION": "Um zehntausende Punkte parallel auf der Grafikkarte zu berechnen, muss man 'Race Conditions' verhindern – also dass zwei Rechenkerne gleichzeitig denselben Knotenpunkt verschieben. Die Lösung nennt sich 'Independent Sets' (oder Graph Coloring): Die Verbindungen werden in isolierte Gruppen (z. B. 4 Phasen bei einem Gitter) unterteilt, in denen sich kein einziger Punkt überschneidet. So kann die GPU jede Gruppe blind und mit maximaler Geschwindigkeit abarbeiten.", + "DATA_STRUCTURES_EXPLANATION": "Für maximale GPU-Performance müssen Daten speicherfreundlich ausgerichtet werden (16-Byte-Alignment). Anstatt viele einzelne Variablen zu nutzen, packt man Informationen clever in 4er-Blöcke (vec4). Ein Vertex speichert so z. B. [X, Y, Z, Inverse_Masse]. Hat ein Punkt die inverse Masse 0.0, wird er vom Algorithmus ignoriert und schwebt unbeweglich in der Luft – ein eleganter Trick für Aufhängungen ohne extra Wenn-Dann-Abfragen.", + "DISCLAIMER": "XPBD vs. Masse-Feder-Systeme: In der physikalischen Simulation gibt es grundlegende Architektur-Unterschiede beim Lösen der Gleichungen:", + "DISCLAIMER_1": "Klassische Masse-Feder-Systeme: Hier werden Kräfte (Hookesches Gesetz) berechnet, die zu Beschleunigungen und schließlich zu neuen Positionen führen. Es gibt zwei Wege, diese mathematisch in die Zukunft zu rechnen (Integration):", + "DISCLAIMER_2": "Explizite Löser (z.B. Forward Euler): Sie berechnen den nächsten Schritt stur aus dem aktuellen Zustand. Sie sind leicht zu programmieren, aber bei steifen Stoffen extrem instabil. Die Kräfte schaukeln sich auf und die Simulation 'explodiert', sofern man keine winzigen, sehr leistungsfressenden Zeitschritte wählt.", + "DISCLAIMER_3": "Implizite Löser (z.B. Backward Euler): Sie berechnen den nächsten Schritt basierend auf dem zukünftigen Zustand. Das ist mathematisch enorm stabil, erfordert aber das Lösen riesiger globaler Matrix-Gleichungssysteme in jedem Frame. Dies ist auf der GPU schwerer zu parallelisieren und bricht zusammen, wenn sich die Struktur ändert (z. B. durch Zerschneiden des Stoffs).", + "DISCLAIMER_4": "Der XPBD-Kompromiss: XPBD umgeht dieses komplexe Matrix-Problem völlig, indem es als lokaler Löser arbeitet. Es kombiniert die unbedingte Stabilität eines impliziten Lösers mit der enormen Geschwindigkeit, Parallelisierbarkeit und dynamischen Anpassungsfähigkeit eines expliziten Systems." + } }, "ALGORITHM": { "TITLE": "Algorithmen", @@ -507,9 +526,9 @@ "DESCRIPTION": "Visualisierung einer chaotischen Doppel-Pendel-Simulation mit WebGPU." }, "CLOTH": { - "TITLE": "Stoff-Simulation", + "TITLE": "Stoffsimulation", "DESCRIPTION": "Simulation on Stoff mit WebGPU." - } + }, "NOTE": "HINWEIS", "GRID_HEIGHT": "Höhe", "GRID_WIDTH": "Beite" diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 4681f5e..bffef7c 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -57,7 +57,8 @@ "K8S": "Kubernetes / k3d", "POSTGRES": "PostgreSQL", "MONGO": "MongoDB", - "GRAFANA": "Grafana/Prometheus" + "GRAFANA": "Grafana/Prometheus", + "DOCKER": "Docker" }, "XP": { "COMPANY8": { @@ -473,7 +474,25 @@ "CLOTH": { "TITLE": "Cloth simulation", "WIND_ON": "Wind On", - "WIND_OFF": "Wind Off" + "WIND_OFF": "Wind Off", + "OUTLINE_ON": "Show Mesh", + "OUTLINE_OFF": "Hide Mesh", + "EXPLANATION": { + "TITLE": "Real-time Cloth Simulation on the GPU", + "CLOTH_SIMULATION_EXPLANATION_TITLE": "Cloth Simulation", + "XPBD_EXPLANATION_TITLE": "XPBD (Extended Position-Based Dynamics)", + "GPU_PARALLELIZATION_EXPLANATION_TITLE": "GPU Parallelization", + "DATA_STRUCTURES_EXPLANATION_TITLE": "Data Structures", + "CLOTH_SIMULATION_EXPLANATION": "Cloth simulations usually model textiles as a grid of mass points (vertices) held together by invisible connections. The goal is to represent physical influences like gravity, wind, and collisions in real time without the material tearing or stretching unnaturally like rubber.", + "XPBD_EXPLANATION": "XPBD (Extended Position-Based Dynamics) is a modern algorithm that manipulates point positions directly to satisfy distance conditions (constraints) instead of calculating accelerations. The 'Extended' means that true physical stiffness is simulated independently of the framerate. Advantage: Absolutely stable, does not explode, and topological changes (like cutting cloth) are trivial. Disadvantage: It is an iterative approximation method and slightly less physically accurate than complex matrix solvers.", + "GPU_PARALLELIZATION_EXPLANATION": "To calculate tens of thousands of points in parallel on the graphics card, one must prevent 'race conditions' – i.e., two processing cores shifting the same node at the exact same time. The solution is called 'Independent Sets' (or Graph Coloring): The connections are divided into isolated groups (e.g., 4 phases for a 2D grid) in which not a single point overlaps. This allows the GPU to process each group blindly and at maximum speed.", + "DATA_STRUCTURES_EXPLANATION": "For maximum GPU performance, data must be memory-aligned (16-byte alignment). Instead of using many individual variables, information is cleverly packed into blocks of four (vec4). A vertex stores, for example, [X, Y, Z, Inverse_Mass]. If a point has an inverse mass of 0.0, the algorithm ignores it, and it floats motionlessly in the air – an elegant trick for pinning cloth without extra if/then statements.", + "DISCLAIMER": "XPBD vs. Mass-Spring Systems: In physical simulations, there are fundamental architectural differences when solving equations:", + "DISCLAIMER_1": "Classical Mass-Spring Systems: Here, forces (Hooke's Law) are calculated, leading to accelerations and ultimately new positions. There are two ways to mathematically project these into the future (integration):", + "DISCLAIMER_2": "Explicit Solvers (e.g., Forward Euler): These rigidly calculate the next step solely from the current state. They are easy to program but extremely unstable for stiff cloths. Forces can escalate and the simulation 'explodes' unless tiny, very performance-heavy time steps are chosen.", + "DISCLAIMER_3": "Implicit Solvers (e.g., Backward Euler): These calculate the next step based on the future state. This is mathematically highly stable but requires solving massive global matrix equation systems in every frame. This is harder to parallelize on the GPU and breaks down if the structure changes (e.g., when the cloth is cut).", + "DISCLAIMER_4": "The XPBD Compromise: XPBD completely bypasses this complex matrix problem by acting as a local solver. It combines the absolute stability of an implicit solver with the enormous speed, parallelizability, and dynamic adaptability of an explicit system." + } }, "ALGORITHM": { "TITLE": "Algorithms",