feature/clothsimulation #28

Merged
lobo merged 8 commits from feature/clothsimulation into main 2026-02-24 09:31:35 +01:00
5 changed files with 77 additions and 25 deletions
Showing only changes of commit 14d7a78ac4 - Show all commits

View File

@@ -3,6 +3,13 @@
<mat-card-title>{{ 'CLOTH.TITLE' | translate }}</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="controls-container">
<div class="controls-panel">
<button mat-raised-button color="primary" (click)="toggleWind()">
{{ isWindActive ? ('CLOTH.WIND_OFF' | translate) : ('CLOTH.WIND_ON' | translate) }}
</button>
</div>
</div>
<app-babylon-canvas
[config]="renderConfig"
(sceneReady)="onSceneReady($event)"

View File

@@ -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 } from '@babylonjs/core';
import {ComputeShader, StorageBuffer, MeshBuilder, ShaderMaterial, ShaderLanguage, ArcRotateCamera, GroundMesh} from '@babylonjs/core';
import {
CLOTH_FRAGMENT_SHADER_WGSL,
CLOTH_INTEGRATE_COMPUTE_WGSL,
@@ -15,6 +15,7 @@ import {
CLOTH_VELOCITY_COMPUTE_WGSL,
CLOTH_VERTEX_SHADER_WGSL
} from './cloth.shader';
import {MatButton} from '@angular/material/button';
@Component({
selector: 'app-cloth',
@@ -24,13 +25,18 @@ import {
MatCardHeader,
MatCardTitle,
TranslatePipe,
BabylonCanvas
BabylonCanvas,
MatButton
],
templateUrl: './cloth.component.html',
styleUrl: './cloth.component.scss',
})
export class ClothComponent {
private currentSceneData: SceneEventData | null = null;
private simulationTime: number = 0;
private clothMesh: GroundMesh | null = null;
public isWindActive: boolean = false;
public renderConfig: RenderConfig = {
mode: '3D',
@@ -47,6 +53,10 @@ export class ClothComponent {
this.createSimulation();
}
public toggleWind(): void {
this.isWindActive = !this.isWindActive;
}
/**
* Initializes and starts the cloth simulation.
*/
@@ -58,10 +68,14 @@ export class ClothComponent {
const { engine, scene } = this.currentSceneData;
// --- 1. CONFIGURE CLOTH GRID ---
const gridWidth = 50;
const gridHeight = 50;
const gridWidth = 100;
const gridHeight = 100;
const spacing = 0.05;
const numVertices = gridWidth * gridHeight;
const spacing = 0.1;
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);
@@ -85,7 +99,7 @@ export class ClothComponent {
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;
positionsData[idx + 3] = (y === 0) ? 0.0 : particleInvMass;
prevPositionsData[idx + 0] = positionsData[idx + 0];
prevPositionsData[idx + 1] = positionsData[idx + 1];
@@ -192,7 +206,11 @@ export class ClothComponent {
csVelocity.setStorageBuffer("velocities", velocitiesBuffer);
// --- 4. SETUP RENDER MESH ---
const clothMesh = MeshBuilder.CreateGround("cloth", { width: 10, height: 10, subdivisions: gridWidth - 1 }, scene);
if (this.clothMesh)
{
scene.removeMesh(this.clothMesh);
}
this.clothMesh = MeshBuilder.CreateGround("cloth", { width: 10, height: 10, subdivisions: gridWidth - 1 }, scene);
const clothMaterial = new ShaderMaterial("clothMat", scene, {
vertexSource: CLOTH_VERTEX_SHADER_WGSL,
@@ -206,7 +224,7 @@ export class ClothComponent {
clothMaterial.backFaceCulling = false;
clothMaterial.setStorageBuffer("positions", positionsBuffer);
clothMesh.material = clothMaterial;
this.clothMesh.material = clothMaterial;
const camera = scene.activeCamera as ArcRotateCamera;
if (camera) {
@@ -218,10 +236,24 @@ export class ClothComponent {
// --- 5. RENDER LOOP ---
scene.onBeforeRenderObservable.clear();
scene.onBeforeRenderObservable.add(() => {
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);

View File

@@ -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<f32>(
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;

View File

@@ -472,7 +472,9 @@
}
},
"CLOTH": {
"TITLE": "Stoff-Simulation"
"TITLE": "Stoff-Simulation",
"WIND_ON": "Wind Einschalten",
"WIND_OFF": "Wind Ausschalten"
},
"ALGORITHM": {
"TITLE": "Algorithmen",

View File

@@ -471,7 +471,9 @@
}
},
"CLOTH": {
"TITLE": "Cloth simulation"
"TITLE": "Cloth simulation",
"WIND_ON": "Wind On",
"WIND_OFF": "Wind Off"
},
"ALGORITHM": {
"TITLE": "Algorithms",