Added wind to cloth simulation
This commit is contained in:
@@ -3,6 +3,13 @@
|
|||||||
<mat-card-title>{{ 'CLOTH.TITLE' | translate }}</mat-card-title>
|
<mat-card-title>{{ 'CLOTH.TITLE' | translate }}</mat-card-title>
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
<mat-card-content>
|
<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
|
<app-babylon-canvas
|
||||||
[config]="renderConfig"
|
[config]="renderConfig"
|
||||||
(sceneReady)="onSceneReady($event)"
|
(sceneReady)="onSceneReady($event)"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Component } from '@angular/core';
|
|||||||
import { MatCard, MatCardContent, MatCardHeader, MatCardTitle } from '@angular/material/card';
|
import { MatCard, MatCardContent, MatCardHeader, MatCardTitle } from '@angular/material/card';
|
||||||
import { TranslatePipe } from '@ngx-translate/core';
|
import { TranslatePipe } from '@ngx-translate/core';
|
||||||
import { BabylonCanvas, RenderConfig, SceneEventData } from '../../../shared/components/render-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 {ComputeShader, StorageBuffer, MeshBuilder, ShaderMaterial, ShaderLanguage, ArcRotateCamera, GroundMesh} from '@babylonjs/core';
|
||||||
import {
|
import {
|
||||||
CLOTH_FRAGMENT_SHADER_WGSL,
|
CLOTH_FRAGMENT_SHADER_WGSL,
|
||||||
CLOTH_INTEGRATE_COMPUTE_WGSL,
|
CLOTH_INTEGRATE_COMPUTE_WGSL,
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
CLOTH_VELOCITY_COMPUTE_WGSL,
|
CLOTH_VELOCITY_COMPUTE_WGSL,
|
||||||
CLOTH_VERTEX_SHADER_WGSL
|
CLOTH_VERTEX_SHADER_WGSL
|
||||||
} from './cloth.shader';
|
} from './cloth.shader';
|
||||||
|
import {MatButton} from '@angular/material/button';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-cloth',
|
selector: 'app-cloth',
|
||||||
@@ -24,13 +25,18 @@ import {
|
|||||||
MatCardHeader,
|
MatCardHeader,
|
||||||
MatCardTitle,
|
MatCardTitle,
|
||||||
TranslatePipe,
|
TranslatePipe,
|
||||||
BabylonCanvas
|
BabylonCanvas,
|
||||||
|
MatButton
|
||||||
],
|
],
|
||||||
templateUrl: './cloth.component.html',
|
templateUrl: './cloth.component.html',
|
||||||
styleUrl: './cloth.component.scss',
|
styleUrl: './cloth.component.scss',
|
||||||
})
|
})
|
||||||
export class ClothComponent {
|
export class ClothComponent {
|
||||||
private currentSceneData: SceneEventData | null = null;
|
private currentSceneData: SceneEventData | null = null;
|
||||||
|
private simulationTime: number = 0;
|
||||||
|
private clothMesh: GroundMesh | null = null;
|
||||||
|
public isWindActive: boolean = false;
|
||||||
|
|
||||||
|
|
||||||
public renderConfig: RenderConfig = {
|
public renderConfig: RenderConfig = {
|
||||||
mode: '3D',
|
mode: '3D',
|
||||||
@@ -47,6 +53,10 @@ export class ClothComponent {
|
|||||||
this.createSimulation();
|
this.createSimulation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public toggleWind(): void {
|
||||||
|
this.isWindActive = !this.isWindActive;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes and starts the cloth simulation.
|
* Initializes and starts the cloth simulation.
|
||||||
*/
|
*/
|
||||||
@@ -58,10 +68,14 @@ export class ClothComponent {
|
|||||||
const { engine, scene } = this.currentSceneData;
|
const { engine, scene } = this.currentSceneData;
|
||||||
|
|
||||||
// --- 1. CONFIGURE CLOTH GRID ---
|
// --- 1. CONFIGURE CLOTH GRID ---
|
||||||
const gridWidth = 50;
|
const gridWidth = 100;
|
||||||
const gridHeight = 50;
|
const gridHeight = 100;
|
||||||
|
const spacing = 0.05;
|
||||||
const numVertices = gridWidth * gridHeight;
|
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 positionsData = new Float32Array(numVertices * 4);
|
||||||
const prevPositionsData = 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 + 0] = (x - gridWidth / 2) * spacing;
|
||||||
positionsData[idx + 1] = 5.0 - (y * spacing);
|
positionsData[idx + 1] = 5.0 - (y * spacing);
|
||||||
positionsData[idx + 2] = 0.0;
|
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 + 0] = positionsData[idx + 0];
|
||||||
prevPositionsData[idx + 1] = positionsData[idx + 1];
|
prevPositionsData[idx + 1] = positionsData[idx + 1];
|
||||||
@@ -192,7 +206,11 @@ export class ClothComponent {
|
|||||||
csVelocity.setStorageBuffer("velocities", velocitiesBuffer);
|
csVelocity.setStorageBuffer("velocities", velocitiesBuffer);
|
||||||
|
|
||||||
// --- 4. SETUP RENDER MESH ---
|
// --- 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, {
|
const clothMaterial = new ShaderMaterial("clothMat", scene, {
|
||||||
vertexSource: CLOTH_VERTEX_SHADER_WGSL,
|
vertexSource: CLOTH_VERTEX_SHADER_WGSL,
|
||||||
@@ -206,7 +224,7 @@ export class ClothComponent {
|
|||||||
|
|
||||||
clothMaterial.backFaceCulling = false;
|
clothMaterial.backFaceCulling = false;
|
||||||
clothMaterial.setStorageBuffer("positions", positionsBuffer);
|
clothMaterial.setStorageBuffer("positions", positionsBuffer);
|
||||||
clothMesh.material = clothMaterial;
|
this.clothMesh.material = clothMaterial;
|
||||||
|
|
||||||
const camera = scene.activeCamera as ArcRotateCamera;
|
const camera = scene.activeCamera as ArcRotateCamera;
|
||||||
if (camera) {
|
if (camera) {
|
||||||
@@ -218,10 +236,24 @@ export class ClothComponent {
|
|||||||
// --- 5. RENDER LOOP ---
|
// --- 5. RENDER LOOP ---
|
||||||
scene.onBeforeRenderObservable.clear();
|
scene.onBeforeRenderObservable.clear();
|
||||||
scene.onBeforeRenderObservable.add(() => {
|
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[0] = 0.016;
|
||||||
paramsData[1] = -9.81;
|
paramsData[1] = -9.81;
|
||||||
paramsData[2] = 0.0001; // Compliance (very small = stiff fabric)
|
paramsData[2] = scaledCompliance; //scaled stiffness
|
||||||
paramsData[3] = numVertices;
|
paramsData[3] = numVertices;
|
||||||
|
paramsData[4] = windX;
|
||||||
|
paramsData[5] = windY;
|
||||||
|
paramsData[6] = windZ;
|
||||||
|
paramsData[7] = this.simulationTime;
|
||||||
|
|
||||||
paramsBuffer.update(paramsData);
|
paramsBuffer.update(paramsData);
|
||||||
|
|
||||||
const dispatchXVertices = Math.ceil(numVertices / 64);
|
const dispatchXVertices = Math.ceil(numVertices / 64);
|
||||||
@@ -230,7 +262,8 @@ export class ClothComponent {
|
|||||||
csIntegrate.dispatch(dispatchXVertices, 1, 1);
|
csIntegrate.dispatch(dispatchXVertices, 1, 1);
|
||||||
|
|
||||||
// 2. XPBD Solver (Substeps) - Solve each color individually
|
// 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);
|
csSolve0.dispatch(Math.ceil((constraintsP0.length / 4) / 64), 1, 1);
|
||||||
csSolve1.dispatch(Math.ceil((constraintsP1.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);
|
csSolve2.dispatch(Math.ceil((constraintsP2.length / 4) / 64), 1, 1);
|
||||||
|
|||||||
@@ -6,14 +6,14 @@
|
|||||||
// --- SHARED DATA STRUCTURES ---
|
// --- SHARED DATA STRUCTURES ---
|
||||||
export const CLOTH_SHARED_STRUCTS = `
|
export const CLOTH_SHARED_STRUCTS = `
|
||||||
struct Params {
|
struct Params {
|
||||||
dt: f32, // Time step per substep
|
dt: f32,
|
||||||
gravity_y: f32, // Gravity (e.g. -9.81)
|
gravity_y: f32,
|
||||||
compliance: f32, // Inverse stiffness (0.0 = completely rigid)
|
compliance: f32,
|
||||||
numVertices: f32, // Total number of vertices
|
numVertices: f32,
|
||||||
numConstraints: f32, // Total number of springs
|
wind_x: f32,
|
||||||
pad1: f32, // Padding
|
wind_y: f32,
|
||||||
pad2: f32, // Padding
|
wind_z: f32,
|
||||||
pad3: f32 // Padding (8 * f32 = 32 bytes)
|
time: f32
|
||||||
};
|
};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -76,17 +76,25 @@ export const CLOTH_INTEGRATE_COMPUTE_WGSL = CLOTH_SHARED_STRUCTS + `
|
|||||||
|
|
||||||
var pos = positions[idx];
|
var pos = positions[idx];
|
||||||
var vel = velocities[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) {
|
if (invMass > 0.0) {
|
||||||
// 1. Apply Gravity: v = v + g * dt
|
|
||||||
vel.y = vel.y + (p.gravity_y * p.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;
|
prev_positions[idx] = pos;
|
||||||
|
|
||||||
// 3. Predict new position: p = p + v * dt
|
|
||||||
pos.x = pos.x + vel.x * p.dt;
|
pos.x = pos.x + vel.x * p.dt;
|
||||||
pos.y = pos.y + vel.y * p.dt;
|
pos.y = pos.y + vel.y * p.dt;
|
||||||
pos.z = pos.z + vel.z * p.dt;
|
pos.z = pos.z + vel.z * p.dt;
|
||||||
|
|||||||
@@ -472,7 +472,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"CLOTH": {
|
"CLOTH": {
|
||||||
"TITLE": "Stoff-Simulation"
|
"TITLE": "Stoff-Simulation",
|
||||||
|
"WIND_ON": "Wind Einschalten",
|
||||||
|
"WIND_OFF": "Wind Ausschalten"
|
||||||
},
|
},
|
||||||
"ALGORITHM": {
|
"ALGORITHM": {
|
||||||
"TITLE": "Algorithmen",
|
"TITLE": "Algorithmen",
|
||||||
|
|||||||
@@ -471,7 +471,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"CLOTH": {
|
"CLOTH": {
|
||||||
"TITLE": "Cloth simulation"
|
"TITLE": "Cloth simulation",
|
||||||
|
"WIND_ON": "Wind On",
|
||||||
|
"WIND_OFF": "Wind Off"
|
||||||
},
|
},
|
||||||
"ALGORITHM": {
|
"ALGORITHM": {
|
||||||
"TITLE": "Algorithms",
|
"TITLE": "Algorithms",
|
||||||
|
|||||||
Reference in New Issue
Block a user