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;