Cloth: add info, outline, diagonals, shader
All checks were successful
Build, Test & Push Frontend / quality-check (pull_request) Successful in 2m1s
Build, Test & Push Frontend / docker (pull_request) Has been skipped

Add an informational panel and mesh-outline toggle to the cloth demo, plus richer physics and shading. The cloth component now provides AlgorithmInformation to an <app-information> view and a toggleMesh() that flips the mesh wireframe. Constraint generation was extended with four diagonal phases (constraintsP4..P7) and the solver loop was generalized to iterate solver pipelines, improving parallel XPBD constraint handling. The WGSL vertex/fragment shaders were updated to pass world positions, compute normals, add simple lighting and a grid-based base color. Also update information template/model to support optional translated entry names and expand i18n (DE/EN) with cloth texts and a Docker key.
This commit is contained in:
2026-02-24 09:28:16 +01:00
parent 12411e58bf
commit ab3bca4395
7 changed files with 154 additions and 22 deletions

View File

@@ -3,11 +3,15 @@
<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

View File

@@ -17,6 +17,9 @@ import {
} 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',
@@ -27,7 +30,8 @@ import {ClothBuffers, ClothConfig, ClothData, ClothPipelines} from './cloth.mode
MatCardTitle,
TranslatePipe,
BabylonCanvas,
MatButton
MatButton,
Information
],
templateUrl: './cloth.component.html',
styleUrl: './cloth.component.scss',
@@ -37,7 +41,7 @@ export class ClothComponent {
private simulationTime: number = 0;
private clothMesh: GroundMesh | null = null;
public isWindActive: boolean = false;
public isOutlineActive: boolean = false;
public renderConfig: RenderConfig = {
mode: '3D',
@@ -45,6 +49,39 @@ export class ClothComponent {
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.
@@ -58,6 +95,14 @@ export class ClothComponent {
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.
*/
@@ -151,11 +196,38 @@ export class ClothComponent {
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],
constraints: [
constraintsP0, constraintsP1, constraintsP2, constraintsP3,
constraintsP4, constraintsP5, constraintsP6, constraintsP7
],
params: new Float32Array(8)
};
}
@@ -293,10 +365,9 @@ export class ClothComponent {
// 2. XPBD Solver (Substeps) - Graph Coloring Phase
for (let i = 0; i < substeps; i++) {
pipelines.solvers[0].dispatch(dispatchXConstraints[0], 1, 1);
pipelines.solvers[1].dispatch(dispatchXConstraints[1], 1, 1);
pipelines.solvers[2].dispatch(dispatchXConstraints[2], 1, 1);
pipelines.solvers[3].dispatch(dispatchXConstraints[3], 1, 1);
for (let phase = 0; phase < pipelines.solvers.length; phase++) {
pipelines.solvers[phase].dispatch(dispatchXConstraints[phase], 1, 1);
}
}
// 3. Update velocities

View File

@@ -22,22 +22,23 @@ export const CLOTH_SHARED_STRUCTS = `
// ==========================================
export const CLOTH_VERTEX_SHADER_WGSL = `
attribute uv : vec2<f32>;
// Storage Buffer
var<storage, read> positions : array<vec4<f32>>;
// Babylon Preprocessor Magic
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;
}
@@ -48,13 +49,24 @@ export const CLOTH_VERTEX_SHADER_WGSL = `
// ==========================================
export const CLOTH_FRAGMENT_SHADER_WGSL = `
varying vUV : vec2<f32>;
varying vWorldPos : vec3<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);
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;
}

View File

@@ -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>
}

View File

@@ -10,5 +10,5 @@ export interface AlgorithmEntry {
name: string;
description: string;
link: string;
translateName?: boolean;
}