diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts
index d079fbb..8e69cac 100644
--- a/src/app/app.routes.ts
+++ b/src/app/app.routes.ts
@@ -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}
];
diff --git a/src/app/constants/RouterConstants.ts b/src/app/constants/RouterConstants.ts
index ac1efab..1ab61ea 100644
--- a/src/app/constants/RouterConstants.ts
+++ b/src/app/constants/RouterConstants.ts
@@ -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',
diff --git a/src/app/pages/algorithms/models/algorithm-category.ts b/src/app/pages/algorithms/algorithm-category.ts
similarity index 100%
rename from src/app/pages/algorithms/models/algorithm-category.ts
rename to src/app/pages/algorithms/algorithm-category.ts
diff --git a/src/app/pages/algorithms/algorithms.component.ts b/src/app/pages/algorithms/algorithms.component.ts
index 8639131..722dd97 100644
--- a/src/app/pages/algorithms/algorithms.component.ts
+++ b/src/app/pages/algorithms/algorithms.component.ts
@@ -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';
diff --git a/src/app/pages/algorithms/service/algorithms.service.ts b/src/app/pages/algorithms/algorithms.service.ts
similarity index 84%
rename from src/app/pages/algorithms/service/algorithms.service.ts
rename to src/app/pages/algorithms/algorithms.service.ts
index c56b771..0db4f56 100644
--- a/src/app/pages/algorithms/service/algorithms.service.ts
+++ b/src/app/pages/algorithms/algorithms.service.ts
@@ -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
}
];
diff --git a/src/app/pages/algorithms/cloth/cloth.component.html b/src/app/pages/algorithms/cloth/cloth.component.html
new file mode 100644
index 0000000..f98a204
--- /dev/null
+++ b/src/app/pages/algorithms/cloth/cloth.component.html
@@ -0,0 +1,12 @@
+
+
+ {{ 'CLOTH.TITLE' | translate }}
+
+
+
+
+
diff --git a/src/app/pages/algorithms/cloth/cloth.component.scss b/src/app/pages/algorithms/cloth/cloth.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/pages/algorithms/cloth/cloth.component.ts b/src/app/pages/algorithms/cloth/cloth.component.ts
new file mode 100644
index 0000000..7dc5086
--- /dev/null
+++ b/src/app/pages/algorithms/cloth/cloth.component.ts
@@ -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 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);
+ });
+ }
+}
diff --git a/src/app/pages/algorithms/cloth/cloth.shader.ts b/src/app/pages/algorithms/cloth/cloth.shader.ts
new file mode 100644
index 0000000..62068bb
--- /dev/null
+++ b/src/app/pages/algorithms/cloth/cloth.shader.ts
@@ -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;
+
+ // Storage Buffer
+ var positions : array>;
+
+ // Babylon Preprocessor-Magie
+ uniform viewProjection : mat4x4;
+ varying vUV : vec2;
+
+ @vertex
+ fn main(input : VertexInputs) -> FragmentInputs {
+ var output : FragmentInputs;
+
+ let worldPos = positions[input.vertexIndex].xyz;
+
+ output.position = uniforms.viewProjection * vec4(worldPos, 1.0);
+ output.vUV = input.uv;
+
+ return output;
+ }
+`;
+
+// ==========================================
+// FRAGMENT SHADER (Bleibt exakt gleich)
+// ==========================================
+export const CLOTH_FRAGMENT_SHADER_WGSL = `
+ varying vUV : vec2;
+
+ @fragment
+ fn main(input: FragmentInputs) -> FragmentOutputs {
+ var output: FragmentOutputs;
+
+ let color = vec3(input.vUV.x * 0.8, input.vUV.y * 0.8, 0.9);
+ output.color = vec4(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 p : Params;
+ @group(0) @binding(1) var positions : array>;
+ @group(0) @binding(2) var prev_positions : array>;
+ @group(0) @binding(3) var velocities : array>;
+
+ @compute @workgroup_size(64)
+ fn main(@builtin(global_invocation_id) global_id : vec3) {
+ 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 p : Params;
+ @group(0) @binding(1) var positions : array>;
+ @group(0) @binding(2) var constraints : array>;
+
+ @compute @workgroup_size(64)
+ fn main(@builtin(global_invocation_id) global_id : vec3) {
+ 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 p : Params;
+ @group(0) @binding(1) var positions : array>;
+ @group(0) @binding(2) var prev_positions : array>;
+ @group(0) @binding(3) var velocities : array>;
+
+ @compute @workgroup_size(64)
+ fn main(@builtin(global_invocation_id) global_id : vec3) {
+ 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;
+ }
+ }
+`;
diff --git a/src/app/shared/rendering/canvas/babylon-canvas.component.ts b/src/app/shared/rendering/canvas/babylon-canvas.component.ts
index 975a187..8e61be0 100644
--- a/src/app/shared/rendering/canvas/babylon-canvas.component.ts
+++ b/src/app/shared/rendering/canvas/babylon-canvas.component.ts
@@ -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();
});
diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json
index 6f46219..dcd275b 100644
--- a/src/assets/i18n/de.json
+++ b/src/assets/i18n/de.json
@@ -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."
}
},
+ "CLOTH": {
+ "TITLE": "Stoff-Simulation"
+ },
"ALGORITHM": {
"TITLE": "Algorithmen",
"PATHFINDING": {
@@ -501,6 +504,10 @@
"TITLE": "Doppel-Pendel",
"DESCRIPTION": "Visualisierung einer chaotischen Doppel-Pendel-Simulation mit WebGPU."
},
+ "CLOTH": {
+ "TITLE": "Stoff-Simulation",
+ "DESCRIPTION": "Simulation on Stoff mit WebGPU."
+ }
"NOTE": "HINWEIS",
"GRID_HEIGHT": "Höhe",
"GRID_WIDTH": "Beite"
diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json
index a5edf3d..186685b 100644
--- a/src/assets/i18n/en.json
+++ b/src/assets/i18n/en.json
@@ -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."
}
},
+ "CLOTH": {
+ "TITLE": "Cloth simulation"
+ },
"ALGORITHM": {
"TITLE": "Algorithms",
"PATHFINDING": {
@@ -500,6 +503,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"