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.
This commit is contained in:
@@ -47,27 +47,30 @@ export class ClothComponent {
|
|||||||
// Calculate approximate constraints (horizontal + vertical edges)
|
// Calculate approximate constraints (horizontal + vertical edges)
|
||||||
const numConstraints = (gridWidth - 1) * gridHeight + gridWidth * (gridHeight - 1);
|
const numConstraints = (gridWidth - 1) * gridHeight + gridWidth * (gridHeight - 1);
|
||||||
|
|
||||||
// --- 2. INITIALIZE CPU ARRAYS (Strict vec4<f32> alignment) ---
|
|
||||||
const positionsData = new Float32Array(numVertices * 4);
|
const positionsData = new Float32Array(numVertices * 4);
|
||||||
const prevPositionsData = new Float32Array(numVertices * 4);
|
const prevPositionsData = new Float32Array(numVertices * 4);
|
||||||
const velocitiesData = 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 y = 0; y < gridHeight; y++) {
|
||||||
for (let x = 0; x < gridWidth; x++) {
|
for (let x = 0; x < gridWidth; x++) {
|
||||||
const idx = (y * gridWidth + x) * 4;
|
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 + 0] = positionsData[idx + 0];
|
||||||
prevPositionsData[idx + 1] = positionsData[idx + 1];
|
prevPositionsData[idx + 1] = positionsData[idx + 1];
|
||||||
prevPositionsData[idx + 2] = positionsData[idx + 2];
|
prevPositionsData[idx + 2] = positionsData[idx + 2];
|
||||||
@@ -75,33 +78,25 @@ export class ClothComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill Constraints (Simple Grid: connect right and connect down)
|
// --- GRAPH COLORING: Constraints in 4 Phasen füllen ---
|
||||||
let cIdx = 0;
|
// Phase 0: Horizontal Gerade
|
||||||
for (let y = 0; y < gridHeight; y++) {
|
for (let y = 0; y < gridHeight; y++) {
|
||||||
for (let x = 0; x < gridWidth; x++) {
|
for (let x = 0; x < gridWidth - 1; x += 2) addConstraint(constraintsP0, y * gridWidth + x, y * gridWidth + x + 1);
|
||||||
const indexA = y * gridWidth + x;
|
}
|
||||||
|
// Phase 1: Horizontal Ungerade
|
||||||
// Connect to right neighbor
|
for (let y = 0; y < gridHeight; y++) {
|
||||||
if (x < gridWidth - 1) {
|
for (let x = 1; x < gridWidth - 1; x += 2) addConstraint(constraintsP1, y * gridWidth + x, y * gridWidth + x + 1);
|
||||||
constraintsData[cIdx * 4 + 0] = indexA; // Vertex A
|
}
|
||||||
constraintsData[cIdx * 4 + 1] = indexA + 1; // Vertex B
|
// Phase 2: Vertikal Gerade
|
||||||
constraintsData[cIdx * 4 + 2] = spacing; // Rest length
|
for (let y = 0; y < gridHeight - 1; y += 2) {
|
||||||
constraintsData[cIdx * 4 + 3] = 1.0; // Active flag
|
for (let x = 0; x < gridWidth; x++) addConstraint(constraintsP2, y * gridWidth + x, (y + 1) * gridWidth + x);
|
||||||
cIdx++;
|
}
|
||||||
}
|
// Phase 3: Vertikal Ungerade
|
||||||
// Connect to bottom neighbor
|
for (let y = 1; y < gridHeight - 1; y += 2) {
|
||||||
if (y < gridHeight - 1) {
|
for (let x = 0; x < gridWidth; x++) addConstraint(constraintsP3, y * gridWidth + x, (y + 1) * gridWidth + x);
|
||||||
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);
|
||||||
const paramsData = new Float32Array(8); // Matches the WGSL struct (dt, gravity, etc.)
|
|
||||||
|
|
||||||
// --- 3. CREATE GPU STORAGE BUFFERS ---
|
// --- 3. CREATE GPU STORAGE BUFFERS ---
|
||||||
const positionsBuffer = new StorageBuffer(engine, positionsData.byteLength);
|
const positionsBuffer = new StorageBuffer(engine, positionsData.byteLength);
|
||||||
@@ -111,52 +106,38 @@ export class ClothComponent {
|
|||||||
prevPositionsBuffer.update(prevPositionsData);
|
prevPositionsBuffer.update(prevPositionsData);
|
||||||
|
|
||||||
const velocitiesBuffer = new StorageBuffer(engine, velocitiesData.byteLength);
|
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);
|
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 ---
|
// --- 4. SETUP COMPUTE SHADERS ---
|
||||||
const csIntegrate = new ComputeShader("integrate", engine, { computeSource: CLOTH_INTEGRATE_COMPUTE_WGSL }, {
|
const csIntegrate = new ComputeShader("integrate", engine, { computeSource: CLOTH_INTEGRATE_COMPUTE_WGSL }, {
|
||||||
bindingsMapping: {
|
bindingsMapping: { "p": { group: 0, binding: 0 }, "positions": { group: 0, binding: 1 }, "prev_positions": { group: 0, binding: 2 }, "velocities": { group: 0, binding: 3 } }
|
||||||
"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("p", paramsBuffer); csIntegrate.setStorageBuffer("positions", positionsBuffer); csIntegrate.setStorageBuffer("prev_positions", prevPositionsBuffer); csIntegrate.setStorageBuffer("velocities", velocitiesBuffer);
|
||||||
csIntegrate.setStorageBuffer("positions", positionsBuffer);
|
|
||||||
csIntegrate.setStorageBuffer("prev_positions", prevPositionsBuffer);
|
|
||||||
csIntegrate.setStorageBuffer("velocities", velocitiesBuffer);
|
|
||||||
|
|
||||||
// --- SETUP: csSolve (XPBD Constraints) ---
|
// Hilfsfunktion, um die 4 Solve-Shader sauber zu erstellen
|
||||||
const csSolve = new ComputeShader("solve", engine, { computeSource: CLOTH_SOLVE_COMPUTE_WGSL }, {
|
const createSolver = (name: string, cBuffer: StorageBuffer) => {
|
||||||
bindingsMapping: {
|
const cs = new ComputeShader(name, engine, { computeSource: CLOTH_SOLVE_COMPUTE_WGSL }, {
|
||||||
"p": { group: 0, binding: 0 },
|
bindingsMapping: { "p": { group: 0, binding: 0 }, "positions": { group: 0, binding: 1 }, "constraints": { group: 0, binding: 2 } }
|
||||||
"positions": { group: 0, binding: 1 },
|
});
|
||||||
"constraints": { group: 0, binding: 2 }
|
cs.setStorageBuffer("p", paramsBuffer); cs.setStorageBuffer("positions", positionsBuffer); cs.setStorageBuffer("constraints", cBuffer);
|
||||||
}
|
return cs;
|
||||||
});
|
};
|
||||||
csSolve.setStorageBuffer("p", paramsBuffer);
|
|
||||||
csSolve.setStorageBuffer("positions", positionsBuffer);
|
const csSolve0 = createSolver("solve0", cBuffer0);
|
||||||
csSolve.setStorageBuffer("constraints", constraintsBuffer);
|
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 }, {
|
const csVelocity = new ComputeShader("velocity", engine, { computeSource: CLOTH_VELOCITY_COMPUTE_WGSL }, {
|
||||||
bindingsMapping: {
|
bindingsMapping: { "p": { group: 0, binding: 0 }, "positions": { group: 0, binding: 1 }, "prev_positions": { group: 0, binding: 2 }, "velocities": { group: 0, binding: 3 } }
|
||||||
"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("p", paramsBuffer); csVelocity.setStorageBuffer("positions", positionsBuffer); csVelocity.setStorageBuffer("prev_positions", prevPositionsBuffer); csVelocity.setStorageBuffer("velocities", velocitiesBuffer);
|
||||||
csVelocity.setStorageBuffer("positions", positionsBuffer);
|
|
||||||
csVelocity.setStorageBuffer("prev_positions", prevPositionsBuffer);
|
|
||||||
csVelocity.setStorageBuffer("velocities", velocitiesBuffer);
|
|
||||||
|
|
||||||
// --- 5. SETUP RENDER MESH ---
|
// --- 5. SETUP RENDER MESH ---
|
||||||
// We create a ground mesh that matches our grid size, but we will OVERWRITE its vertices in the shader.
|
// 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) {
|
if (camera) {
|
||||||
camera.alpha = Math.PI / 4;
|
camera.alpha = Math.PI / 4;
|
||||||
camera.beta = Math.PI / 2.5;
|
camera.beta = Math.PI / 2.5;
|
||||||
camera.radius = 15;
|
camera.radius = 15;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 6. RENDER LOOP ---
|
// --- 6. RENDER LOOP ---
|
||||||
scene.onBeforeRenderObservable.clear();
|
scene.onBeforeRenderObservable.clear();
|
||||||
scene.onBeforeRenderObservable.add(() => {
|
scene.onBeforeRenderObservable.add(() => {
|
||||||
|
|
||||||
// 1. Update Parameters (just an example, bind your simParams here)
|
paramsData[0] = 0.016;
|
||||||
paramsData[0] = 0.016; // dt
|
paramsData[1] = -9.81;
|
||||||
paramsData[1] = -9.81; // gravity
|
paramsData[2] = 0.0001; // Compliance (sehr klein = steifer Stoff)
|
||||||
paramsData[2] = 0.001; // compliance (stiffness)
|
|
||||||
paramsData[3] = numVertices;
|
paramsData[3] = numVertices;
|
||||||
paramsData[4] = numConstraints;
|
|
||||||
paramsBuffer.update(paramsData);
|
paramsBuffer.update(paramsData);
|
||||||
|
|
||||||
// 2. Dispatch Compute Shaders in sequence!
|
|
||||||
const dispatchXVertices = Math.ceil(numVertices / 64);
|
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++) {
|
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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,17 +98,18 @@ export const CLOTH_INTEGRATE_COMPUTE_WGSL = CLOTH_SHARED_STRUCTS + `
|
|||||||
export const CLOTH_SOLVE_COMPUTE_WGSL = CLOTH_SHARED_STRUCTS + `
|
export const CLOTH_SOLVE_COMPUTE_WGSL = CLOTH_SHARED_STRUCTS + `
|
||||||
@group(0) @binding(0) var<storage, read> p : Params;
|
@group(0) @binding(0) var<storage, read> p : Params;
|
||||||
@group(0) @binding(1) var<storage, read_write> positions : array<vec4<f32>>;
|
@group(0) @binding(1) var<storage, read_write> positions : array<vec4<f32>>;
|
||||||
@group(0) @binding(2) var<storage, read_write> constraints : array<vec4<f32>>;
|
@group(0) @binding(2) var<storage, read> constraints : array<vec4<f32>>; // <--- Nur "read", da wir sie hier nicht verändern
|
||||||
|
|
||||||
@compute @workgroup_size(64)
|
@compute @workgroup_size(64)
|
||||||
fn main(@builtin(global_invocation_id) global_id : vec3<u32>) {
|
fn main(@builtin(global_invocation_id) global_id : vec3<u32>) {
|
||||||
let idx = global_id.x;
|
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 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; }
|
if (isActive < 0.5) { return; }
|
||||||
|
|
||||||
let idA = u32(constraint.x);
|
let idA = u32(constraint.x);
|
||||||
@@ -118,35 +119,27 @@ export const CLOTH_SOLVE_COMPUTE_WGSL = CLOTH_SHARED_STRUCTS + `
|
|||||||
var pA = positions[idA];
|
var pA = positions[idA];
|
||||||
var pB = positions[idB];
|
var pB = positions[idB];
|
||||||
|
|
||||||
let wA = pA.w; // Inverse mass A
|
let wA = pA.w;
|
||||||
let wB = pB.w; // Inverse mass B
|
let wB = pB.w;
|
||||||
let wSum = wA + wB;
|
let wSum = wA + wB;
|
||||||
|
|
||||||
// If both points are pinned, do nothing
|
|
||||||
if (wSum <= 0.0) { return; }
|
if (wSum <= 0.0) { return; }
|
||||||
|
|
||||||
let dir = pA.xyz - pB.xyz;
|
let dir = pA.xyz - pB.xyz;
|
||||||
let dist = length(dir);
|
let dist = length(dir);
|
||||||
|
|
||||||
// Prevent division by zero
|
|
||||||
if (dist < 0.0001) { return; }
|
if (dist < 0.0001) { return; }
|
||||||
|
|
||||||
// XPBD Calculation (Extended Position-Based Dynamics)
|
|
||||||
let n = dir / dist;
|
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 alpha = p.compliance / (p.dt * p.dt);
|
||||||
let lambda = -C / (wSum + alpha);
|
let lambda = -C / (wSum + alpha);
|
||||||
|
|
||||||
// Apply position corrections directly to the points
|
|
||||||
let corrA = n * (lambda * wA);
|
let corrA = n * (lambda * wA);
|
||||||
let corrB = n * (-lambda * wB);
|
let corrB = n * (-lambda * wB);
|
||||||
|
|
||||||
// NOTE: In a multi-threaded GPU environment without "Graph Coloring",
|
// This is because we are using graph coloring to be thread safe
|
||||||
// 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) {
|
if (wA > 0.0) {
|
||||||
positions[idA].x = positions[idA].x + corrA.x;
|
positions[idA].x = positions[idA].x + corrA.x;
|
||||||
positions[idA].y = positions[idA].y + corrA.y;
|
positions[idA].y = positions[idA].y + corrA.y;
|
||||||
|
|||||||
Reference in New Issue
Block a user