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.
This commit is contained in:
@@ -14,6 +14,7 @@ export const routes: Routes = [
|
|||||||
{ path: RouterConstants.LABYRINTH.PATH, component: RouterConstants.LABYRINTH.COMPONENT},
|
{ path: RouterConstants.LABYRINTH.PATH, component: RouterConstants.LABYRINTH.COMPONENT},
|
||||||
{ path: RouterConstants.FRACTAL.PATH, component: RouterConstants.FRACTAL.COMPONENT},
|
{ path: RouterConstants.FRACTAL.PATH, component: RouterConstants.FRACTAL.COMPONENT},
|
||||||
{ path: RouterConstants.FRACTAL3d.PATH, component: RouterConstants.FRACTAL3d.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}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {LabyrinthComponent} from '../pages/algorithms/pathfinding/labyrinth/laby
|
|||||||
import {FractalComponent} from '../pages/algorithms/fractal/fractal.component';
|
import {FractalComponent} from '../pages/algorithms/fractal/fractal.component';
|
||||||
import {Fractal3dComponent} from '../pages/algorithms/fractal3d/fractal3d.component';
|
import {Fractal3dComponent} from '../pages/algorithms/fractal3d/fractal3d.component';
|
||||||
import PendulumComponent from '../pages/algorithms/pendulum/pendulum.component';
|
import PendulumComponent from '../pages/algorithms/pendulum/pendulum.component';
|
||||||
|
import {ClothComponent} from '../pages/algorithms/cloth/cloth.component';
|
||||||
|
|
||||||
export class RouterConstants {
|
export class RouterConstants {
|
||||||
|
|
||||||
@@ -72,6 +73,12 @@ export class RouterConstants {
|
|||||||
COMPONENT: PendulumComponent
|
COMPONENT: PendulumComponent
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static readonly CLOTH = {
|
||||||
|
PATH: 'algorithms/cloth',
|
||||||
|
LINK: '/algorithms/cloth',
|
||||||
|
COMPONENT: ClothComponent
|
||||||
|
};
|
||||||
|
|
||||||
static readonly IMPRINT = {
|
static readonly IMPRINT = {
|
||||||
PATH: 'imprint',
|
PATH: 'imprint',
|
||||||
LINK: '/imprint',
|
LINK: '/imprint',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Component, OnInit, inject } from '@angular/core';
|
import { Component, OnInit, inject } from '@angular/core';
|
||||||
import { AlgorithmsService } from './service/algorithms.service';
|
import { AlgorithmsService } from './algorithms.service';
|
||||||
import { AlgorithmCategory } from './models/algorithm-category';
|
import { AlgorithmCategory } from './algorithm-category';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { AlgorithmCategory } from '../models/algorithm-category';
|
import { AlgorithmCategory } from './algorithm-category';
|
||||||
import { Observable, of } from 'rxjs';
|
import { Observable, of } from 'rxjs';
|
||||||
import {RouterConstants} from '../../../constants/RouterConstants';
|
import {RouterConstants} from '../../constants/RouterConstants';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@@ -50,6 +50,12 @@ export class AlgorithmsService {
|
|||||||
title: 'ALGORITHM.PENDULUM.TITLE',
|
title: 'ALGORITHM.PENDULUM.TITLE',
|
||||||
description: 'ALGORITHM.PENDULUM.DESCRIPTION',
|
description: 'ALGORITHM.PENDULUM.DESCRIPTION',
|
||||||
routerLink: RouterConstants.PENDULUM.LINK
|
routerLink: RouterConstants.PENDULUM.LINK
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cloth',
|
||||||
|
title: 'ALGORITHM.CLOTH.TITLE',
|
||||||
|
description: 'ALGORITHM.CLOTH.DESCRIPTION',
|
||||||
|
routerLink: RouterConstants.CLOTH.LINK
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
12
src/app/pages/algorithms/cloth/cloth.component.html
Normal file
12
src/app/pages/algorithms/cloth/cloth.component.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<mat-card class="algo-container">
|
||||||
|
<mat-card-header>
|
||||||
|
<mat-card-title>{{ 'CLOTH.TITLE' | translate }}</mat-card-title>
|
||||||
|
</mat-card-header>
|
||||||
|
<mat-card-content>
|
||||||
|
<app-babylon-canvas
|
||||||
|
[config]="renderConfig"
|
||||||
|
(sceneReady)="onSceneReady($event)"
|
||||||
|
(sceneResized)="onSceneReady($event)"
|
||||||
|
/>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
0
src/app/pages/algorithms/cloth/cloth.component.scss
Normal file
0
src/app/pages/algorithms/cloth/cloth.component.scss
Normal file
207
src/app/pages/algorithms/cloth/cloth.component.ts
Normal file
207
src/app/pages/algorithms/cloth/cloth.component.ts
Normal file
@@ -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<f32> 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
195
src/app/pages/algorithms/cloth/cloth.shader.ts
Normal file
195
src/app/pages/algorithms/cloth/cloth.shader.ts
Normal file
@@ -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<f32>;
|
||||||
|
|
||||||
|
// Storage Buffer
|
||||||
|
var<storage, read> positions : array<vec4<f32>>;
|
||||||
|
|
||||||
|
// Babylon Preprocessor-Magie
|
||||||
|
uniform viewProjection : mat4x4<f32>;
|
||||||
|
varying vUV : vec2<f32>;
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn main(input : VertexInputs) -> FragmentInputs {
|
||||||
|
var output : FragmentInputs;
|
||||||
|
|
||||||
|
let worldPos = positions[input.vertexIndex].xyz;
|
||||||
|
|
||||||
|
output.position = uniforms.viewProjection * vec4<f32>(worldPos, 1.0);
|
||||||
|
output.vUV = input.uv;
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// FRAGMENT SHADER (Bleibt exakt gleich)
|
||||||
|
// ==========================================
|
||||||
|
export const CLOTH_FRAGMENT_SHADER_WGSL = `
|
||||||
|
varying vUV : vec2<f32>;
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn main(input: FragmentInputs) -> FragmentOutputs {
|
||||||
|
var output: FragmentOutputs;
|
||||||
|
|
||||||
|
let color = vec3<f32>(input.vUV.x * 0.8, input.vUV.y * 0.8, 0.9);
|
||||||
|
output.color = vec4<f32>(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<storage, read> p : Params;
|
||||||
|
@group(0) @binding(1) var<storage, read_write> positions : array<vec4<f32>>;
|
||||||
|
@group(0) @binding(2) var<storage, read_write> prev_positions : array<vec4<f32>>;
|
||||||
|
@group(0) @binding(3) var<storage, read_write> velocities : array<vec4<f32>>;
|
||||||
|
|
||||||
|
@compute @workgroup_size(64)
|
||||||
|
fn main(@builtin(global_invocation_id) global_id : vec3<u32>) {
|
||||||
|
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<storage, read> p : Params;
|
||||||
|
@group(0) @binding(1) var<storage, read_write> positions : array<vec4<f32>>;
|
||||||
|
@group(0) @binding(2) var<storage, read_write> constraints : array<vec4<f32>>;
|
||||||
|
|
||||||
|
@compute @workgroup_size(64)
|
||||||
|
fn main(@builtin(global_invocation_id) global_id : vec3<u32>) {
|
||||||
|
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<storage, read> p : Params;
|
||||||
|
@group(0) @binding(1) var<storage, read_write> positions : array<vec4<f32>>;
|
||||||
|
@group(0) @binding(2) var<storage, read_write> prev_positions : array<vec4<f32>>;
|
||||||
|
@group(0) @binding(3) var<storage, read_write> velocities : array<vec4<f32>>;
|
||||||
|
|
||||||
|
@compute @workgroup_size(64)
|
||||||
|
fn main(@builtin(global_invocation_id) global_id : vec3<u32>) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -5,9 +5,9 @@ export interface RenderConfig {
|
|||||||
mode: '2D' | '3D';
|
mode: '2D' | '3D';
|
||||||
shaderLanguage?: number; //0 GLSL, 1 WGSL
|
shaderLanguage?: number; //0 GLSL, 1 WGSL
|
||||||
initialViewSize: number;
|
initialViewSize: number;
|
||||||
vertexShader: string;
|
vertexShader?: string;
|
||||||
fragmentShader: string;
|
fragmentShader?: string;
|
||||||
uniformNames: string[];
|
uniformNames?: string[];
|
||||||
uniformBufferNames?: string[];
|
uniformBufferNames?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,8 +120,10 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private createFullScreenRect() {
|
private createFullScreenRect() {
|
||||||
|
if (!this.config.vertexShader || !this.config.fragmentShader) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const plane = MeshBuilder.CreatePlane("plane", {size: 100}, this.scene);
|
const plane = MeshBuilder.CreatePlane("plane", {size: 100}, this.scene);
|
||||||
|
|
||||||
if (this.config.mode === '3D') {
|
if (this.config.mode === '3D') {
|
||||||
plane.parent = this.camera;
|
plane.parent = this.camera;
|
||||||
plane.position.z = 1;
|
plane.position.z = 1;
|
||||||
@@ -134,6 +136,10 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private createShaderMaterial() {
|
private createShaderMaterial() {
|
||||||
|
if (!this.config.vertexShader || !this.config.fragmentShader || !this.config.uniformNames) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.shaderMaterial = new ShaderMaterial(
|
this.shaderMaterial = new ShaderMaterial(
|
||||||
"shaderMaterial",
|
"shaderMaterial",
|
||||||
this.scene,
|
this.scene,
|
||||||
@@ -161,8 +167,10 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// default uniforms which maybe each scene has
|
// default uniforms which maybe each scene has
|
||||||
this.shaderMaterial.setVector2("resolution", new Vector2(canvas.width, canvas.height));
|
if (this.shaderMaterial) {
|
||||||
this.shaderMaterial.setVector3("cameraPosition", this.camera.position);
|
this.shaderMaterial.setVector2("resolution", new Vector2(canvas.width, canvas.height));
|
||||||
|
this.shaderMaterial.setVector3("cameraPosition", this.camera.position);
|
||||||
|
}
|
||||||
|
|
||||||
this.scene.render();
|
this.scene.render();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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."
|
"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": {
|
"ALGORITHM": {
|
||||||
"TITLE": "Algorithmen",
|
"TITLE": "Algorithmen",
|
||||||
"PATHFINDING": {
|
"PATHFINDING": {
|
||||||
@@ -501,6 +504,10 @@
|
|||||||
"TITLE": "Doppel-Pendel",
|
"TITLE": "Doppel-Pendel",
|
||||||
"DESCRIPTION": "Visualisierung einer chaotischen Doppel-Pendel-Simulation mit WebGPU."
|
"DESCRIPTION": "Visualisierung einer chaotischen Doppel-Pendel-Simulation mit WebGPU."
|
||||||
},
|
},
|
||||||
|
"CLOTH": {
|
||||||
|
"TITLE": "Stoff-Simulation",
|
||||||
|
"DESCRIPTION": "Simulation on Stoff mit WebGPU."
|
||||||
|
}
|
||||||
"NOTE": "HINWEIS",
|
"NOTE": "HINWEIS",
|
||||||
"GRID_HEIGHT": "Höhe",
|
"GRID_HEIGHT": "Höhe",
|
||||||
"GRID_WIDTH": "Beite"
|
"GRID_WIDTH": "Beite"
|
||||||
|
|||||||
@@ -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."
|
"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": {
|
"ALGORITHM": {
|
||||||
"TITLE": "Algorithms",
|
"TITLE": "Algorithms",
|
||||||
"PATHFINDING": {
|
"PATHFINDING": {
|
||||||
@@ -500,6 +503,10 @@
|
|||||||
"TITLE": "Double pendulum",
|
"TITLE": "Double pendulum",
|
||||||
"DESCRIPTION": "Visualisation of a chaotic double pendulum simulation with WebGPU."
|
"DESCRIPTION": "Visualisation of a chaotic double pendulum simulation with WebGPU."
|
||||||
},
|
},
|
||||||
|
"CLOTH": {
|
||||||
|
"TITLE": "Cloth simulation",
|
||||||
|
"DESCRIPTION": "Simulation of cloth with WebGPU."
|
||||||
|
},
|
||||||
"NOTE": "Note",
|
"NOTE": "Note",
|
||||||
"GRID_HEIGHT": "Height",
|
"GRID_HEIGHT": "Height",
|
||||||
"GRID_WIDTH": "Width"
|
"GRID_WIDTH": "Width"
|
||||||
|
|||||||
Reference in New Issue
Block a user