feature/clothsimulation #28
@@ -14,6 +14,7 @@ export const routes: Routes = [
|
||||
{ path: RouterConstants.LABYRINTH.PATH, component: RouterConstants.LABYRINTH.COMPONENT},
|
||||
{ path: RouterConstants.FRACTAL.PATH, component: RouterConstants.FRACTAL.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 {Fractal3dComponent} from '../pages/algorithms/fractal3d/fractal3d.component';
|
||||
import PendulumComponent from '../pages/algorithms/pendulum/pendulum.component';
|
||||
import {ClothComponent} from '../pages/algorithms/cloth/cloth.component';
|
||||
|
||||
export class RouterConstants {
|
||||
|
||||
@@ -72,6 +73,12 @@ export class RouterConstants {
|
||||
COMPONENT: PendulumComponent
|
||||
};
|
||||
|
||||
static readonly CLOTH = {
|
||||
PATH: 'algorithms/cloth',
|
||||
LINK: '/algorithms/cloth',
|
||||
COMPONENT: ClothComponent
|
||||
};
|
||||
|
||||
static readonly IMPRINT = {
|
||||
PATH: 'imprint',
|
||||
LINK: '/imprint',
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<div class="algo-container">
|
||||
<h1>{{ 'ALGORITHM.TITLE' |translate }}</h1>
|
||||
<div class="category-cards">
|
||||
<h1>{{ 'ALGORITHM.TITLE' |translate }}</h1>
|
||||
<div class="card-grid">
|
||||
@for (category of categories$ | async; track category.id) {
|
||||
<mat-card [routerLink]="[category.routerLink]">
|
||||
<mat-card class="algo-card" [routerLink]="[category.routerLink]">
|
||||
<mat-card-header>
|
||||
<mat-card-title>{{ category.title | translate }}</mat-card-title>
|
||||
</mat-card-header>
|
||||
@@ -12,4 +11,3 @@
|
||||
</mat-card>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { AlgorithmsService } from './service/algorithms.service';
|
||||
import { AlgorithmCategory } from './models/algorithm-category';
|
||||
import { AlgorithmsService } from './algorithms.service';
|
||||
import { AlgorithmCategory } from './algorithm-category';
|
||||
import { Observable } from 'rxjs';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterLink } from '@angular/router';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AlgorithmCategory } from '../models/algorithm-category';
|
||||
import { AlgorithmCategory } from './algorithm-category';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import {RouterConstants} from '../../../constants/RouterConstants';
|
||||
import {RouterConstants} from '../../constants/RouterConstants';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@@ -50,6 +50,12 @@ export class AlgorithmsService {
|
||||
title: 'ALGORITHM.PENDULUM.TITLE',
|
||||
description: 'ALGORITHM.PENDULUM.DESCRIPTION',
|
||||
routerLink: RouterConstants.PENDULUM.LINK
|
||||
},
|
||||
{
|
||||
id: 'cloth',
|
||||
title: 'ALGORITHM.CLOTH.TITLE',
|
||||
description: 'ALGORITHM.CLOTH.DESCRIPTION',
|
||||
routerLink: RouterConstants.CLOTH.LINK
|
||||
}
|
||||
];
|
||||
|
||||
23
src/app/pages/algorithms/cloth/cloth.component.html
Normal file
23
src/app/pages/algorithms/cloth/cloth.component.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<mat-card class="algo-container">
|
||||
<mat-card-header>
|
||||
<mat-card-title>{{ 'CLOTH.TITLE' | translate }}</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<app-information [algorithmInformation]="algoInformation"/>
|
||||
<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>
|
||||
<button mat-raised-button color="primary" (click)="toggleMesh()">
|
||||
{{ isOutlineActive ? ('CLOTH.OUTLINE_OFF' | translate) : ('CLOTH.OUTLINE_ON' | translate) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<app-babylon-canvas
|
||||
[config]="renderConfig"
|
||||
(sceneReady)="onSceneReady($event)"
|
||||
(sceneResized)="onSceneReady($event)"
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
377
src/app/pages/algorithms/cloth/cloth.component.ts
Normal file
377
src/app/pages/algorithms/cloth/cloth.component.ts
Normal file
@@ -0,0 +1,377 @@
|
||||
/**
|
||||
* File: cloth.component.ts
|
||||
* Description: Component for cloth simulation using WebGPU compute shaders.
|
||||
*/
|
||||
|
||||
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, GroundMesh, WebGPUEngine, Scene} 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';
|
||||
import {MatButton} from '@angular/material/button';
|
||||
import {ClothBuffers, ClothConfig, ClothData, ClothPipelines} from './cloth.model';
|
||||
import {Information} from '../information/information';
|
||||
import {AlgorithmInformation} from '../information/information.models';
|
||||
import {UrlConstants} from '../../../constants/UrlConstants';
|
||||
|
||||
@Component({
|
||||
selector: 'app-cloth',
|
||||
imports: [
|
||||
MatCard,
|
||||
MatCardContent,
|
||||
MatCardHeader,
|
||||
MatCardTitle,
|
||||
TranslatePipe,
|
||||
BabylonCanvas,
|
||||
MatButton,
|
||||
Information
|
||||
],
|
||||
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 isOutlineActive: boolean = false;
|
||||
|
||||
public renderConfig: RenderConfig = {
|
||||
mode: '3D',
|
||||
initialViewSize: 20,
|
||||
shaderLanguage: ShaderLanguage.WGSL
|
||||
};
|
||||
|
||||
algoInformation: AlgorithmInformation = {
|
||||
title: 'CLOTH.EXPLANATION.TITLE',
|
||||
entries: [
|
||||
{
|
||||
name: 'CLOTH.EXPLANATION.CLOTH_SIMULATION_EXPLANATION_TITLE',
|
||||
description: 'CLOTH.EXPLANATION.CLOTH_SIMULATION_EXPLANATION',
|
||||
link: UrlConstants.MANDELBULB_WIKI,
|
||||
translateName: true
|
||||
},
|
||||
{
|
||||
name: 'CLOTH.EXPLANATION.XPBD_EXPLANATION_TITLE',
|
||||
description: 'CLOTH.EXPLANATION.XPBD_EXPLANATION',
|
||||
link: UrlConstants.MANDELBOX_WIKI,
|
||||
translateName: true
|
||||
},
|
||||
{
|
||||
name: 'CLOTH.EXPLANATION.GPU_PARALLELIZATION_EXPLANATION_TITLE',
|
||||
description: 'CLOTH.EXPLANATION.GPU_PARALLELIZATION_EXPLANATION',
|
||||
link: UrlConstants.JULIA3D_WIKI,
|
||||
translateName: true
|
||||
},
|
||||
{
|
||||
name: 'CLOTH.EXPLANATION.DATA_STRUCTURES_EXPLANATION_TITLE',
|
||||
description: 'CLOTH.EXPLANATION.DATA_STRUCTURES_EXPLANATION',
|
||||
link: UrlConstants.JULIA3D_WIKI,
|
||||
translateName: true
|
||||
}
|
||||
],
|
||||
disclaimer: 'CLOTH.EXPLANATION.DISCLAIMER',
|
||||
disclaimerBottom: '',
|
||||
disclaimerListEntry: ['CLOTH.EXPLANATION.DISCLAIMER_1', 'CLOTH.EXPLANATION.DISCLAIMER_2', 'CLOTH.EXPLANATION.DISCLAIMER_3', 'CLOTH.EXPLANATION.DISCLAIMER_4']
|
||||
};
|
||||
|
||||
/**
|
||||
* Called when the Babylon scene is ready.
|
||||
* @param event The scene event data.
|
||||
*/
|
||||
public onSceneReady(event: SceneEventData): void {
|
||||
this.currentSceneData = event;
|
||||
this.createSimulation();
|
||||
}
|
||||
|
||||
public toggleWind(): void {
|
||||
this.isWindActive = !this.isWindActive;
|
||||
}
|
||||
|
||||
public toggleMesh(): void {
|
||||
this.isOutlineActive = !this.isOutlineActive;
|
||||
if (!this.clothMesh?.material) {
|
||||
return;
|
||||
}
|
||||
this.clothMesh.material.wireframe = this.isOutlineActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes and starts the cloth simulation.
|
||||
*/
|
||||
private createSimulation(): void {
|
||||
if (!this.currentSceneData) return;
|
||||
|
||||
const { engine, scene } = this.currentSceneData;
|
||||
|
||||
// 1. Define physics parameters
|
||||
const config = this.getClothConfig();
|
||||
|
||||
// 2. Generate initial CPU data (positions, constraints)
|
||||
const clothData = this.generateClothData(config);
|
||||
|
||||
// 3. Upload to GPU
|
||||
const buffers = this.createStorageBuffers(engine, clothData);
|
||||
|
||||
// 4. Create Compute Shaders
|
||||
const pipelines = this.setupComputePipelines(engine, buffers);
|
||||
|
||||
// 5. Setup Rendering (Mesh, Material, Camera)
|
||||
this.setupRenderMesh(scene, config, buffers.positions);
|
||||
|
||||
// 6. Start the physics loop
|
||||
this.startRenderLoop(engine, scene, config, buffers, pipelines);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// 1. CONFIGURATION
|
||||
// ========================================================================
|
||||
private getClothConfig(): ClothConfig {
|
||||
const gridWidth = 100;
|
||||
const gridHeight = 100;
|
||||
const spacing = 0.05;
|
||||
const density = 1.0;
|
||||
const particleArea = spacing * spacing;
|
||||
const particleMass = density * particleArea;
|
||||
|
||||
return {
|
||||
gridWidth,
|
||||
gridHeight,
|
||||
spacing,
|
||||
density,
|
||||
numVertices: gridWidth * gridHeight,
|
||||
particleInvMass: 1.0 / particleMass
|
||||
};
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// 2. DATA GENERATION (CPU)
|
||||
// ========================================================================
|
||||
private generateClothData(config: ClothConfig): ClothData {
|
||||
const positionsData = new Float32Array(config.numVertices * 4);
|
||||
const prevPositionsData = new Float32Array(config.numVertices * 4);
|
||||
const velocitiesData = new Float32Array(config.numVertices * 4);
|
||||
|
||||
const constraintsP0: number[] = [];
|
||||
const constraintsP1: number[] = [];
|
||||
const constraintsP2: number[] = [];
|
||||
const constraintsP3: number[] = [];
|
||||
|
||||
const addConstraint = (arr: number[], a: number, b: number): void => {
|
||||
arr.push(a, b, config.spacing, 1.0);
|
||||
};
|
||||
|
||||
// Fill positions (Pin top row)
|
||||
for (let y = 0; y < config.gridHeight; y++) {
|
||||
for (let x = 0; x < config.gridWidth; x++) {
|
||||
const idx = (y * config.gridWidth + x) * 4;
|
||||
positionsData[idx + 0] = (x - config.gridWidth / 2) * config.spacing;
|
||||
positionsData[idx + 1] = 5.0 - (y * config.spacing);
|
||||
positionsData[idx + 2] = 0.0;
|
||||
positionsData[idx + 3] = (y === 0) ? 0.0 : config.particleInvMass;
|
||||
|
||||
prevPositionsData[idx + 0] = positionsData[idx + 0];
|
||||
prevPositionsData[idx + 1] = positionsData[idx + 1];
|
||||
prevPositionsData[idx + 2] = positionsData[idx + 2];
|
||||
prevPositionsData[idx + 3] = positionsData[idx + 3];
|
||||
}
|
||||
}
|
||||
|
||||
// Graph Coloring (4 Phases)
|
||||
for (let y = 0; y < config.gridHeight; y++) {
|
||||
for (let x = 0; x < config.gridWidth - 1; x += 2) addConstraint(constraintsP0, y * config.gridWidth + x, y * config.gridWidth + x + 1);
|
||||
for (let x = 1; x < config.gridWidth - 1; x += 2) addConstraint(constraintsP1, y * config.gridWidth + x, y * config.gridWidth + x + 1);
|
||||
}
|
||||
for (let y = 0; y < config.gridHeight - 1; y += 2) {
|
||||
for (let x = 0; x < config.gridWidth; x++) addConstraint(constraintsP2, y * config.gridWidth + x, (y + 1) * config.gridWidth + x);
|
||||
}
|
||||
for (let y = 1; y < config.gridHeight - 1; y += 2) {
|
||||
for (let x = 0; x < config.gridWidth; x++) addConstraint(constraintsP3, y * config.gridWidth + x, (y + 1) * config.gridWidth + x);
|
||||
}
|
||||
|
||||
const constraintsP4: number[] = [];
|
||||
const constraintsP5: number[] = [];
|
||||
const constraintsP6: number[] = [];
|
||||
const constraintsP7: number[] = [];
|
||||
|
||||
const diagSpacing = config.spacing * Math.SQRT2;
|
||||
const addDiagConstraint = (arr: number[], a: number, b: number): void => {
|
||||
arr.push(a, b, diagSpacing, 1.0);
|
||||
};
|
||||
|
||||
for (let y = 0; y < config.gridHeight - 1; y++) {
|
||||
const arr = (y % 2 === 0) ? constraintsP4 : constraintsP5;
|
||||
for (let x = 0; x < config.gridWidth - 1; x++) {
|
||||
addDiagConstraint(arr, y * config.gridWidth + x, (y + 1) * config.gridWidth + (x + 1));
|
||||
}
|
||||
}
|
||||
|
||||
for (let y = 0; y < config.gridHeight - 1; y++) {
|
||||
const arr = (y % 2 === 0) ? constraintsP6 : constraintsP7;
|
||||
for (let x = 0; x < config.gridWidth - 1; x++) {
|
||||
addDiagConstraint(arr, y * config.gridWidth + (x + 1), (y + 1) * config.gridWidth + x);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
positions: positionsData,
|
||||
prevPositions: prevPositionsData,
|
||||
velocities: velocitiesData,
|
||||
constraints: [
|
||||
constraintsP0, constraintsP1, constraintsP2, constraintsP3,
|
||||
constraintsP4, constraintsP5, constraintsP6, constraintsP7
|
||||
],
|
||||
params: new Float32Array(8)
|
||||
};
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// 3. BUFFER CREATION (GPU)
|
||||
// ========================================================================
|
||||
private createStorageBuffers(engine: WebGPUEngine, data: ClothData): ClothBuffers {
|
||||
const createBuffer = (arrayData: Float32Array | number[]): StorageBuffer => {
|
||||
const buffer = new StorageBuffer(engine, arrayData.length * 4);
|
||||
buffer.update(arrayData instanceof Float32Array ? arrayData : new Float32Array(arrayData));
|
||||
return buffer;
|
||||
};
|
||||
|
||||
return {
|
||||
positions: createBuffer(data.positions),
|
||||
prevPositions: createBuffer(data.prevPositions),
|
||||
velocities: createBuffer(data.velocities),
|
||||
params: createBuffer(data.params),
|
||||
constraints: data.constraints.map(cData => createBuffer(cData))
|
||||
};
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// 4. COMPUTE SHADERS
|
||||
// ========================================================================
|
||||
private setupComputePipelines(engine: WebGPUEngine, buffers: ClothBuffers): ClothPipelines {
|
||||
|
||||
// Helper for integrating & velocity
|
||||
const createBasicShader = (name: string, source: string) => {
|
||||
const cs = new ComputeShader(name, engine, { computeSource: source }, {
|
||||
bindingsMapping: {
|
||||
"p": { group: 0, binding: 0 },
|
||||
"positions": { group: 0, binding: 1 },
|
||||
"prev_positions": { group: 0, binding: 2 },
|
||||
"velocities": { group: 0, binding: 3 }
|
||||
}
|
||||
});
|
||||
cs.setStorageBuffer("p", buffers.params);
|
||||
cs.setStorageBuffer("positions", buffers.positions);
|
||||
cs.setStorageBuffer("prev_positions", buffers.prevPositions);
|
||||
cs.setStorageBuffer("velocities", buffers.velocities);
|
||||
return cs;
|
||||
};
|
||||
|
||||
// Helper for solvers
|
||||
const createSolverShader = (name: string, constraintBuffer: 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", buffers.params);
|
||||
cs.setStorageBuffer("positions", buffers.positions);
|
||||
cs.setStorageBuffer("constraints", constraintBuffer);
|
||||
return cs;
|
||||
};
|
||||
|
||||
return {
|
||||
integrate: createBasicShader("integrate", CLOTH_INTEGRATE_COMPUTE_WGSL),
|
||||
solvers: buffers.constraints.map((cBuffer, i) => createSolverShader(`solve${i}`, cBuffer)),
|
||||
velocity: createBasicShader("velocity", CLOTH_VELOCITY_COMPUTE_WGSL)
|
||||
};
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// 5. RENDERING SETUP
|
||||
// ========================================================================
|
||||
private setupRenderMesh(scene: Scene, config: ClothConfig, positionsBuffer: StorageBuffer): void {
|
||||
if (this.clothMesh) {
|
||||
scene.removeMesh(this.clothMesh);
|
||||
}
|
||||
|
||||
this.clothMesh = MeshBuilder.CreateGround("cloth", { width: 10, height: 10, subdivisions: config.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.backFaceCulling = false;
|
||||
clothMaterial.setStorageBuffer("positions", positionsBuffer);
|
||||
this.clothMesh.material = clothMaterial;
|
||||
|
||||
const camera = scene.activeCamera as ArcRotateCamera;
|
||||
if (camera) {
|
||||
camera.alpha = Math.PI / 4;
|
||||
camera.beta = Math.PI / 2.5;
|
||||
camera.radius = 15;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// 6. RENDER LOOP
|
||||
// ========================================================================
|
||||
private startRenderLoop(engine: WebGPUEngine, scene: Scene, config: ClothConfig, buffers: ClothBuffers, pipelines: ClothPipelines): void {
|
||||
const paramsData = new Float32Array(8);
|
||||
|
||||
// Pre-calculate constraint dispatch sizes for the 4 phases
|
||||
const constraintsLength = buffers.constraints.map(b => (b as any)._buffer.capacity / 4 / 4); // Elements / vec4 length
|
||||
const dispatchXConstraints = constraintsLength.map(len => Math.ceil(len / 64));
|
||||
const dispatchXVertices = Math.ceil(config.numVertices / 64);
|
||||
const substeps = 15;
|
||||
|
||||
scene.onBeforeRenderObservable.clear();
|
||||
scene.onBeforeRenderObservable.add(() => {
|
||||
this.simulationTime += engine.getDeltaTime() / 1000.0;
|
||||
|
||||
// Update Physics Parameters
|
||||
const windX = this.isWindActive ? 5.0 : 0.0;
|
||||
const windY = 0.0;
|
||||
const windZ = this.isWindActive ? 15.0 : 0.0;
|
||||
const scaledCompliance = 0.00001 * config.particleInvMass * config.spacing;
|
||||
|
||||
paramsData[0] = 0.016; // dt
|
||||
paramsData[1] = -9.81; // gravity
|
||||
paramsData[2] = scaledCompliance;
|
||||
paramsData[3] = config.numVertices;
|
||||
paramsData[4] = windX;
|
||||
paramsData[5] = windY;
|
||||
paramsData[6] = windZ;
|
||||
paramsData[7] = this.simulationTime;
|
||||
|
||||
buffers.params.update(paramsData);
|
||||
|
||||
// 1. Predict positions
|
||||
pipelines.integrate.dispatch(dispatchXVertices, 1, 1);
|
||||
|
||||
// 2. XPBD Solver (Substeps) - Graph Coloring Phase
|
||||
for (let i = 0; i < substeps; i++) {
|
||||
for (let phase = 0; phase < pipelines.solvers.length; phase++) {
|
||||
pipelines.solvers[phase].dispatch(dispatchXConstraints[phase], 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Update velocities
|
||||
pipelines.velocity.dispatch(dispatchXVertices, 1, 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
36
src/app/pages/algorithms/cloth/cloth.model.ts
Normal file
36
src/app/pages/algorithms/cloth/cloth.model.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// --- SIMULATION CONFIGURATION ---
|
||||
import {ComputeShader, StorageBuffer} from '@babylonjs/core';
|
||||
|
||||
export interface ClothConfig {
|
||||
gridWidth: number;
|
||||
gridHeight: number;
|
||||
spacing: number;
|
||||
density: number;
|
||||
numVertices: number;
|
||||
particleInvMass: number;
|
||||
}
|
||||
|
||||
// --- RAW CPU DATA ---
|
||||
export interface ClothData {
|
||||
positions: Float32Array;
|
||||
prevPositions: Float32Array;
|
||||
velocities: Float32Array;
|
||||
constraints: number[][]; // Array containing the 4 phases
|
||||
params: Float32Array;
|
||||
}
|
||||
|
||||
// --- WEBGPU BUFFERS ---
|
||||
export interface ClothBuffers {
|
||||
positions: StorageBuffer;
|
||||
prevPositions: StorageBuffer;
|
||||
velocities: StorageBuffer;
|
||||
params: StorageBuffer;
|
||||
constraints: StorageBuffer[]; // 4 phase buffers
|
||||
}
|
||||
|
||||
// --- COMPUTE PIPELINES ---
|
||||
export interface ClothPipelines {
|
||||
integrate: ComputeShader;
|
||||
solvers: ComputeShader[]; // 4 solve shaders
|
||||
velocity: ComputeShader;
|
||||
}
|
||||
213
src/app/pages/algorithms/cloth/cloth.shader.ts
Normal file
213
src/app/pages/algorithms/cloth/cloth.shader.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* File: cloth.shader.ts
|
||||
* Description: WGSL shaders for cloth simulation and rendering.
|
||||
*/
|
||||
|
||||
// --- SHARED DATA STRUCTURES ---
|
||||
export const CLOTH_SHARED_STRUCTS = `
|
||||
struct Params {
|
||||
dt: f32,
|
||||
gravity_y: f32,
|
||||
compliance: f32,
|
||||
numVertices: f32,
|
||||
wind_x: f32,
|
||||
wind_y: f32,
|
||||
wind_z: f32,
|
||||
time: f32
|
||||
};
|
||||
`;
|
||||
|
||||
// ==========================================
|
||||
// VERTEX SHADER
|
||||
// ==========================================
|
||||
export const CLOTH_VERTEX_SHADER_WGSL = `
|
||||
attribute uv : vec2<f32>;
|
||||
var<storage, read> positions : array<vec4<f32>>;
|
||||
|
||||
uniform viewProjection : mat4x4<f32>;
|
||||
|
||||
// Varyings, um Daten an den Fragment-Shader zu senden
|
||||
varying vUV : vec2<f32>;
|
||||
varying vWorldPos : vec3<f32>; // NEU: Wir brauchen die 3D-Position für das Licht!
|
||||
|
||||
@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;
|
||||
output.vWorldPos = worldPos; // Position weitergeben
|
||||
|
||||
return output;
|
||||
}
|
||||
`;
|
||||
|
||||
// ==========================================
|
||||
// FRAGMENT SHADER
|
||||
// ==========================================
|
||||
export const CLOTH_FRAGMENT_SHADER_WGSL = `
|
||||
varying vUV : vec2<f32>;
|
||||
varying vWorldPos : vec3<f32>;
|
||||
|
||||
@fragment
|
||||
fn main(input: FragmentInputs) -> FragmentOutputs {
|
||||
var output: FragmentOutputs;
|
||||
|
||||
let dx = dpdx(input.vWorldPos);
|
||||
let dy = dpdy(input.vWorldPos);
|
||||
let normal = normalize(cross(dx, dy));
|
||||
let lightDir = normalize(vec3<f32>(1.0, 1.0, 0.5));
|
||||
let diffuse = max(0.0, abs(dot(normal, lightDir)));
|
||||
let ambient = 0.3;
|
||||
let lightIntensity = ambient + (diffuse * 0.7);
|
||||
let grid = (floor(input.vUV.x * 20.0) + floor(input.vUV.y * 20.0)) % 2.0;
|
||||
let baseColor = mix(vec3<f32>(0.8, 0.4, 0.15), vec3<f32>(0.9, 0.5, 0.2), grid);
|
||||
let finalColor = baseColor * lightIntensity;
|
||||
|
||||
output.color = vec4<f32>(finalColor, 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;
|
||||
|
||||
if (invMass > 0.0) {
|
||||
vel.y = vel.y + (p.gravity_y * p.dt);
|
||||
|
||||
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;
|
||||
|
||||
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> constraints : array<vec4<f32>>; // <--- Read-only as we do not modify them here
|
||||
|
||||
@compute @workgroup_size(64)
|
||||
fn main(@builtin(global_invocation_id) global_id : vec3<u32>) {
|
||||
let idx = global_id.x;
|
||||
|
||||
// Query the GPU directly for the length of the passed array
|
||||
if (idx >= arrayLength(&constraints)) { return; }
|
||||
|
||||
let constraint = constraints[idx];
|
||||
let isActive = constraint.w;
|
||||
|
||||
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;
|
||||
let wB = pB.w;
|
||||
let wSum = wA + wB;
|
||||
|
||||
if (wSum <= 0.0) { return; }
|
||||
|
||||
let dir = pA.xyz - pB.xyz;
|
||||
let dist = length(dir);
|
||||
|
||||
if (dist < 0.0001) { return; }
|
||||
|
||||
let n = dir / dist;
|
||||
let C = dist - restLength;
|
||||
|
||||
let alpha = p.compliance / (p.dt * p.dt);
|
||||
let lambda = -C / (wSum + alpha);
|
||||
|
||||
let corrA = n * (lambda * wA);
|
||||
let corrB = n * (-lambda * wB);
|
||||
|
||||
// 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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -8,7 +8,7 @@ import {MatSelect} from '@angular/material/select';
|
||||
import {AlgorithmInformation} from '../information/information.models';
|
||||
import {UrlConstants} from '../../../constants/UrlConstants';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {BabylonCanvas, RenderCallback, RenderConfig, SceneEventData} from '../../../shared/rendering/canvas/babylon-canvas.component';
|
||||
import {BabylonCanvas, RenderCallback, RenderConfig, SceneEventData} from '../../../shared/components/render-canvas/babylon-canvas.component';
|
||||
import {FRACTAL2D_FRAGMENT, FRACTAL2D_VERTEX} from './fractal.shader';
|
||||
import {PointerEventTypes, PointerInfo, ShaderMaterial, Vector2} from '@babylonjs/core';
|
||||
import {MatButton} from '@angular/material/button';
|
||||
|
||||
@@ -7,7 +7,7 @@ import {TranslatePipe} from '@ngx-translate/core';
|
||||
import {AlgorithmInformation} from '../information/information.models';
|
||||
import {UrlConstants} from '../../../constants/UrlConstants';
|
||||
import {MatButton} from '@angular/material/button';
|
||||
import {BabylonCanvas, RenderCallback, RenderConfig} from '../../../shared/rendering/canvas/babylon-canvas.component';
|
||||
import {BabylonCanvas, RenderCallback, RenderConfig} from '../../../shared/components/render-canvas/babylon-canvas.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-fractal3d',
|
||||
|
||||
@@ -5,7 +5,14 @@
|
||||
@for (algo of algorithmInformation.entries; track algo)
|
||||
{
|
||||
<p>
|
||||
<strong>{{ algo.name }}</strong> {{ algo.description | translate }}
|
||||
<strong>
|
||||
@if(algo.translateName){
|
||||
{{ algo.name | translate}}
|
||||
} @else {
|
||||
{{ algo.name }}
|
||||
}
|
||||
</strong>
|
||||
{{ algo.description | translate }}
|
||||
<a href="{{algo.link}}" target="_blank" rel="noopener noreferrer">Wikipedia</a>
|
||||
</p>
|
||||
}
|
||||
|
||||
@@ -10,5 +10,5 @@ export interface AlgorithmEntry {
|
||||
name: string;
|
||||
description: string;
|
||||
link: string;
|
||||
|
||||
translateName?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {BabylonCanvas, RenderConfig, SceneEventData} from '../../../shared/rendering/canvas/babylon-canvas.component';
|
||||
import {BabylonCanvas, RenderConfig, SceneEventData} from '../../../shared/components/render-canvas/babylon-canvas.component';
|
||||
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
|
||||
import {ComputeShader, ShaderLanguage, StorageBuffer} from '@babylonjs/core';
|
||||
import {PENDULUM_FRAGMENT_SHADER_WGSL, PENDULUM_PHYSIC_COMPUTE_SHADER_WGSL, PENDULUM_RENDER_COMPUTE_SHADER_WGSL, PENDULUM_VERTEX_SHADER_WGSL} from './pendulum.shader';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="project-grid">
|
||||
<div class="card-grid">
|
||||
@if (featuredProject(); as project) {
|
||||
<mat-card class="project-card featured">
|
||||
<mat-card-header>
|
||||
|
||||
@@ -11,4 +11,5 @@ canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ export interface RenderConfig {
|
||||
mode: '2D' | '3D';
|
||||
shaderLanguage?: number; //0 GLSL, 1 WGSL
|
||||
initialViewSize: number;
|
||||
vertexShader: string;
|
||||
fragmentShader: string;
|
||||
uniformNames: string[];
|
||||
vertexShader?: string;
|
||||
fragmentShader?: string;
|
||||
uniformNames?: string[];
|
||||
uniformBufferNames?: string[];
|
||||
}
|
||||
|
||||
@@ -120,8 +120,10 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
private createFullScreenRect() {
|
||||
if (!this.config.vertexShader || !this.config.fragmentShader) {
|
||||
return;
|
||||
}
|
||||
const plane = MeshBuilder.CreatePlane("plane", {size: 100}, this.scene);
|
||||
|
||||
if (this.config.mode === '3D') {
|
||||
plane.parent = this.camera;
|
||||
plane.position.z = 1;
|
||||
@@ -134,6 +136,10 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
private createShaderMaterial() {
|
||||
if (!this.config.vertexShader || !this.config.fragmentShader || !this.config.uniformNames) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.shaderMaterial = new ShaderMaterial(
|
||||
"shaderMaterial",
|
||||
this.scene,
|
||||
@@ -161,8 +167,10 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
// default uniforms which maybe each scene has
|
||||
this.shaderMaterial.setVector2("resolution", new Vector2(canvas.width, canvas.height));
|
||||
this.shaderMaterial.setVector3("cameraPosition", this.camera.position);
|
||||
if (this.shaderMaterial) {
|
||||
this.shaderMaterial.setVector2("resolution", new Vector2(canvas.width, canvas.height));
|
||||
this.shaderMaterial.setVector3("cameraPosition", this.camera.position);
|
||||
}
|
||||
|
||||
this.scene.render();
|
||||
});
|
||||
@@ -57,7 +57,8 @@
|
||||
"K8S": "Kubernetes / k3d",
|
||||
"POSTGRES": "PostgreSQL",
|
||||
"MONGO": "MongoDB",
|
||||
"GRAFANA": "Grafana/Prometheus"
|
||||
"GRAFANA": "Grafana/Prometheus",
|
||||
"DOCKER": "Docker"
|
||||
},
|
||||
"XP": {
|
||||
"COMPANY8": {
|
||||
@@ -471,6 +472,29 @@
|
||||
"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": "Stoffsimulation",
|
||||
"WIND_ON": "Wind Einschalten",
|
||||
"WIND_OFF": "Wind Ausschalten",
|
||||
"OUTLINE_ON": "Mesh anzeigen",
|
||||
"OUTLINE_OFF": "Mesh ausschalten",
|
||||
"EXPLANATION": {
|
||||
"TITLE": "Echtzeit-Stoffsimulation auf der GPU",
|
||||
"CLOTH_SIMULATION_EXPLANATION_TITLE": "Stoffsimulation",
|
||||
"XPBD_EXPLANATION_TITLE": "XPBD (Extended Position-Based Dynamics)",
|
||||
"GPU_PARALLELIZATION_EXPLANATION_TITLE": "GPU Parallelisierung",
|
||||
"DATA_STRUCTURES_EXPLANATION_TITLE": "Datenstrukturen",
|
||||
"CLOTH_SIMULATION_EXPLANATION": "Stoffsimulationen modellieren Textilien meist als ein Gitter aus Massepunkten (Vertices), die durch unsichtbare Verbindungen zusammengehalten werden. Ziel ist es, physikalische Einflüsse wie Schwerkraft, Wind und Kollisionen in Echtzeit darzustellen, ohne dass das Material zerreißt oder sich unnatürlich wie Gummi dehnt.",
|
||||
"XPBD_EXPLANATION": "XPBD (Extended Position-Based Dynamics) ist ein moderner Algorithmus, der statt Beschleunigungen direkt die Positionen der Punkte manipuliert, um Abstandsbedingungen (Constraints) zu erfüllen. Das 'Extended' bedeutet, dass echte physikalische Steifigkeit unabhängig von der Framerate simuliert wird. Vorteil: Absolut stabil, explodiert nicht und topologische Änderungen (wie das Zerschneiden von Stoff) sind trivial. Nachteil: Es ist ein iteratives Näherungsverfahren und physikalisch minimal weniger akkurat als komplexe Matrix-Löser.",
|
||||
"GPU_PARALLELIZATION_EXPLANATION": "Um zehntausende Punkte parallel auf der Grafikkarte zu berechnen, muss man 'Race Conditions' verhindern – also dass zwei Rechenkerne gleichzeitig denselben Knotenpunkt verschieben. Die Lösung nennt sich 'Independent Sets' (oder Graph Coloring): Die Verbindungen werden in isolierte Gruppen (z. B. 4 Phasen bei einem Gitter) unterteilt, in denen sich kein einziger Punkt überschneidet. So kann die GPU jede Gruppe blind und mit maximaler Geschwindigkeit abarbeiten.",
|
||||
"DATA_STRUCTURES_EXPLANATION": "Für maximale GPU-Performance müssen Daten speicherfreundlich ausgerichtet werden (16-Byte-Alignment). Anstatt viele einzelne Variablen zu nutzen, packt man Informationen clever in 4er-Blöcke (vec4). Ein Vertex speichert so z. B. [X, Y, Z, Inverse_Masse]. Hat ein Punkt die inverse Masse 0.0, wird er vom Algorithmus ignoriert und schwebt unbeweglich in der Luft – ein eleganter Trick für Aufhängungen ohne extra Wenn-Dann-Abfragen.",
|
||||
"DISCLAIMER": "XPBD vs. Masse-Feder-Systeme: In der physikalischen Simulation gibt es grundlegende Architektur-Unterschiede beim Lösen der Gleichungen:",
|
||||
"DISCLAIMER_1": "Klassische Masse-Feder-Systeme: Hier werden Kräfte (Hookesches Gesetz) berechnet, die zu Beschleunigungen und schließlich zu neuen Positionen führen. Es gibt zwei Wege, diese mathematisch in die Zukunft zu rechnen (Integration):",
|
||||
"DISCLAIMER_2": "Explizite Löser (z.B. Forward Euler): Sie berechnen den nächsten Schritt stur aus dem aktuellen Zustand. Sie sind leicht zu programmieren, aber bei steifen Stoffen extrem instabil. Die Kräfte schaukeln sich auf und die Simulation 'explodiert', sofern man keine winzigen, sehr leistungsfressenden Zeitschritte wählt.",
|
||||
"DISCLAIMER_3": "Implizite Löser (z.B. Backward Euler): Sie berechnen den nächsten Schritt basierend auf dem zukünftigen Zustand. Das ist mathematisch enorm stabil, erfordert aber das Lösen riesiger globaler Matrix-Gleichungssysteme in jedem Frame. Dies ist auf der GPU schwerer zu parallelisieren und bricht zusammen, wenn sich die Struktur ändert (z. B. durch Zerschneiden des Stoffs).",
|
||||
"DISCLAIMER_4": "Der XPBD-Kompromiss: XPBD umgeht dieses komplexe Matrix-Problem völlig, indem es als lokaler Löser arbeitet. Es kombiniert die unbedingte Stabilität eines impliziten Lösers mit der enormen Geschwindigkeit, Parallelisierbarkeit und dynamischen Anpassungsfähigkeit eines expliziten Systems."
|
||||
}
|
||||
},
|
||||
"ALGORITHM": {
|
||||
"TITLE": "Algorithmen",
|
||||
"PATHFINDING": {
|
||||
@@ -501,6 +525,10 @@
|
||||
"TITLE": "Doppel-Pendel",
|
||||
"DESCRIPTION": "Visualisierung einer chaotischen Doppel-Pendel-Simulation mit WebGPU."
|
||||
},
|
||||
"CLOTH": {
|
||||
"TITLE": "Stoffsimulation",
|
||||
"DESCRIPTION": "Simulation on Stoff mit WebGPU."
|
||||
},
|
||||
"NOTE": "HINWEIS",
|
||||
"GRID_HEIGHT": "Höhe",
|
||||
"GRID_WIDTH": "Beite"
|
||||
|
||||
@@ -57,7 +57,8 @@
|
||||
"K8S": "Kubernetes / k3d",
|
||||
"POSTGRES": "PostgreSQL",
|
||||
"MONGO": "MongoDB",
|
||||
"GRAFANA": "Grafana/Prometheus"
|
||||
"GRAFANA": "Grafana/Prometheus",
|
||||
"DOCKER": "Docker"
|
||||
},
|
||||
"XP": {
|
||||
"COMPANY8": {
|
||||
@@ -470,6 +471,29 @@
|
||||
"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",
|
||||
"WIND_ON": "Wind On",
|
||||
"WIND_OFF": "Wind Off",
|
||||
"OUTLINE_ON": "Show Mesh",
|
||||
"OUTLINE_OFF": "Hide Mesh",
|
||||
"EXPLANATION": {
|
||||
"TITLE": "Real-time Cloth Simulation on the GPU",
|
||||
"CLOTH_SIMULATION_EXPLANATION_TITLE": "Cloth Simulation",
|
||||
"XPBD_EXPLANATION_TITLE": "XPBD (Extended Position-Based Dynamics)",
|
||||
"GPU_PARALLELIZATION_EXPLANATION_TITLE": "GPU Parallelization",
|
||||
"DATA_STRUCTURES_EXPLANATION_TITLE": "Data Structures",
|
||||
"CLOTH_SIMULATION_EXPLANATION": "Cloth simulations usually model textiles as a grid of mass points (vertices) held together by invisible connections. The goal is to represent physical influences like gravity, wind, and collisions in real time without the material tearing or stretching unnaturally like rubber.",
|
||||
"XPBD_EXPLANATION": "XPBD (Extended Position-Based Dynamics) is a modern algorithm that manipulates point positions directly to satisfy distance conditions (constraints) instead of calculating accelerations. The 'Extended' means that true physical stiffness is simulated independently of the framerate. Advantage: Absolutely stable, does not explode, and topological changes (like cutting cloth) are trivial. Disadvantage: It is an iterative approximation method and slightly less physically accurate than complex matrix solvers.",
|
||||
"GPU_PARALLELIZATION_EXPLANATION": "To calculate tens of thousands of points in parallel on the graphics card, one must prevent 'race conditions' – i.e., two processing cores shifting the same node at the exact same time. The solution is called 'Independent Sets' (or Graph Coloring): The connections are divided into isolated groups (e.g., 4 phases for a 2D grid) in which not a single point overlaps. This allows the GPU to process each group blindly and at maximum speed.",
|
||||
"DATA_STRUCTURES_EXPLANATION": "For maximum GPU performance, data must be memory-aligned (16-byte alignment). Instead of using many individual variables, information is cleverly packed into blocks of four (vec4). A vertex stores, for example, [X, Y, Z, Inverse_Mass]. If a point has an inverse mass of 0.0, the algorithm ignores it, and it floats motionlessly in the air – an elegant trick for pinning cloth without extra if/then statements.",
|
||||
"DISCLAIMER": "XPBD vs. Mass-Spring Systems: In physical simulations, there are fundamental architectural differences when solving equations:",
|
||||
"DISCLAIMER_1": "Classical Mass-Spring Systems: Here, forces (Hooke's Law) are calculated, leading to accelerations and ultimately new positions. There are two ways to mathematically project these into the future (integration):",
|
||||
"DISCLAIMER_2": "Explicit Solvers (e.g., Forward Euler): These rigidly calculate the next step solely from the current state. They are easy to program but extremely unstable for stiff cloths. Forces can escalate and the simulation 'explodes' unless tiny, very performance-heavy time steps are chosen.",
|
||||
"DISCLAIMER_3": "Implicit Solvers (e.g., Backward Euler): These calculate the next step based on the future state. This is mathematically highly stable but requires solving massive global matrix equation systems in every frame. This is harder to parallelize on the GPU and breaks down if the structure changes (e.g., when the cloth is cut).",
|
||||
"DISCLAIMER_4": "The XPBD Compromise: XPBD completely bypasses this complex matrix problem by acting as a local solver. It combines the absolute stability of an implicit solver with the enormous speed, parallelizability, and dynamic adaptability of an explicit system."
|
||||
}
|
||||
},
|
||||
"ALGORITHM": {
|
||||
"TITLE": "Algorithms",
|
||||
"PATHFINDING": {
|
||||
@@ -500,6 +524,10 @@
|
||||
"TITLE": "Double pendulum",
|
||||
"DESCRIPTION": "Visualisation of a chaotic double pendulum simulation with WebGPU."
|
||||
},
|
||||
"CLOTH": {
|
||||
"TITLE": "Cloth simulation",
|
||||
"DESCRIPTION": "Simulation of cloth with WebGPU."
|
||||
},
|
||||
"NOTE": "Note",
|
||||
"GRID_HEIGHT": "Height",
|
||||
"GRID_WIDTH": "Width"
|
||||
|
||||
@@ -702,12 +702,21 @@ app-root {
|
||||
}
|
||||
|
||||
/* ---- Projects Page & Dialog ---- */
|
||||
.project-grid {
|
||||
.card-grid {
|
||||
display: grid;
|
||||
gap: clamp(1rem, 3vw, 1.5rem);
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(100%, 450px), 1fr));
|
||||
max-width: var(--app-maxWidth);
|
||||
margin: 0 auto;
|
||||
margin-right: 1rem;
|
||||
margin-left: 1rem;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.algo-card {
|
||||
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
|
||||
Reference in New Issue
Block a user