Add pendulum demo and WGSL support

Introduce a new Pendulum demo (component, template, stylesheet) and wire it into routing and the algorithms list. Extend Babylon canvas API to emit a SceneReadyEvent (scene + engine) and accept a shaderLanguage option in RenderConfig so materials/shaders can target WGSL; update Fractal to consume the new SceneReadyEvent signature. Also add i18n entries for the pendulum demo.
This commit is contained in:
2026-02-18 11:58:25 +01:00
parent 68e21489ea
commit 55ece27e1c
10 changed files with 215 additions and 9 deletions

View File

@@ -13,6 +13,7 @@ export const routes: Routes = [
{ path: RouterConstants.GOL.PATH, component: RouterConstants.GOL.COMPONENT},
{ 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.FRACTAL3d.PATH, component: RouterConstants.FRACTAL3d.COMPONENT},
{ path: RouterConstants.PENDULUM.PATH, component: RouterConstants.PENDULUM.COMPONENT}
];

View File

@@ -8,6 +8,7 @@ import {ConwayGolComponent} from '../pages/algorithms/conway-gol/conway-gol.comp
import {LabyrinthComponent} from '../pages/algorithms/pathfinding/labyrinth/labyrinth.component';
import {FractalComponent} from '../pages/algorithms/fractal/fractal.component';
import {Fractal3dComponent} from '../pages/algorithms/fractal3d/fractal3d.component';
import {PendulumComponent} from '../pages/algorithms/pendle/pendulum.component';
export class RouterConstants {
@@ -65,6 +66,12 @@ export class RouterConstants {
COMPONENT: Fractal3dComponent
};
static readonly PENDULUM = {
PATH: 'algorithms/pendulum',
LINK: '/algorithms/pendulum',
COMPONENT: PendulumComponent
};
static readonly IMPRINT = {
PATH: 'imprint',
LINK: '/imprint',

View File

@@ -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} from '../../../shared/rendering/canvas/babylon-canvas.component';
import {BabylonCanvas, RenderCallback, RenderConfig, SceneReadyEvent} from '../../../shared/rendering/canvas/babylon-canvas.component';
import {FRACTAL2D_FRAGMENT, FRACTAL2D_VERTEX} from './fractal.shader';
import {PointerEventTypes, PointerInfo, Scene, ShaderMaterial, Vector2} from '@babylonjs/core';
import {MatButton} from '@angular/material/button';
@@ -155,8 +155,8 @@ export class FractalComponent implements OnInit {
}
}
onSceneReady(scene: Scene): void {
scene.onPointerObservable.add((pointerInfo) => {
onSceneReady(event: SceneReadyEvent): void {
event.scene.onPointerObservable.add((pointerInfo) => {
switch (pointerInfo.type) {
case PointerEventTypes.POINTERDOWN:

View File

@@ -0,0 +1,11 @@
<mat-card class="container">
<mat-card-header>
<mat-card-title>Ich bin ein Pendel - blub</mat-card-title>
</mat-card-header>
<mat-card-content>
<app-babylon-canvas
[config]="renderConfig"
(sceneReady)="onSceneReady($event)"
/>
</mat-card-content>
</mat-card>

View File

@@ -0,0 +1,164 @@
import {Component} from '@angular/core';
import {BabylonCanvas, RenderConfig, SceneReadyEvent} from '../../../shared/rendering/canvas/babylon-canvas.component';
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
import {ComputeShader, ShaderLanguage, StorageBuffer, UniformBuffer} from '@babylonjs/core';
@Component({
selector: 'app-pendulum',
imports: [
BabylonCanvas,
MatCard,
MatCardContent,
MatCardHeader,
MatCardTitle,
],
templateUrl: './pendulum.component.html',
styleUrl: './pendulum.component.scss',
})
export class PendulumComponent {
// --- VERTEX SHADER ---
// Das hat funktioniert. Wir definieren Attribute, Babylon baut 'VertexInputs'.
// Wir geben 'FragmentInputs' zurück (was Babylon aus unseren varying baut).
private readonly vertexShaderWGSL = `
attribute position : vec3<f32>;
attribute uv : vec2<f32>;
varying vUV : vec2<f32>;
@vertex
fn main(input : VertexInputs) -> FragmentInputs {
var output : FragmentInputs;
output.position = vec4<f32>(input.position, 1.0);
output.vUV = input.uv;
return output;
}
`;
// --- FRAGMENT SHADER (FIXED) ---
// Änderungen:
// 1. Rückgabetyp ist jetzt 'FragmentOutputs' (das Babylon Struct).
// 2. Wir schreiben in 'fragmentOutputs.color'.
// 3. Wir returnen 'fragmentOutputs'.
private readonly fragmentShaderWGSL = `
varying vUV : vec2<f32>;
var<storage, read> pixelBuffer : array<f32>;
var<uniform> params : Params;
struct Params {
resolution: vec2<f32>,
time: f32
};
@fragment
fn main(input : FragmentInputs) -> FragmentOutputs {
let x = u32(input.vUV.x * params.resolution.x);
let y = u32(input.vUV.y * params.resolution.y);
let width = u32(params.resolution.x);
let index = y * width + x;
let total = u32(params.resolution.x * params.resolution.y);
// Default Farbe (schwarz)
var color = vec4<f32>(0.0, 0.0, 0.0, 1.0);
if (index < total) {
let val = pixelBuffer[index];
color = vec4<f32>(val, val * 0.5, 0.2, 1.0);
}
// Babylon stellt die Variable 'fragmentOutputs' bereit.
// Das Feld heißt standardmäßig 'color'.
fragmentOutputs.color = color;
return fragmentOutputs;
}
`;
// --- COMPUTE SHADER (Unverändert) ---
private readonly computeShaderWGSL = `
@group(0) @binding(0) var<storage, read_write> pixelBuffer : array<f32>;
@group(0) @binding(1) var<uniform> params : Params;
struct Params {
resolution: vec2<f32>,
time: f32
};
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) global_id : vec3<u32>) {
let index = global_id.x;
let totalPixels = u32(params.resolution.x * params.resolution.y);
if (index >= totalPixels) { return; }
let width = u32(params.resolution.x);
let x = f32(index % width);
let y = f32(index / width);
// Zeit-Variable nutzen für Animation
let value = sin(x * 0.05 + params.time) * cos(y * 0.05 + params.time);
pixelBuffer[index] = value * 0.5 + 0.5;
}
`;
renderConfig: RenderConfig = {
mode: '2D',
initialViewSize: 2,
shaderLanguage: ShaderLanguage.WGSL,
vertexShader: this.vertexShaderWGSL,
fragmentShader: this.fragmentShaderWGSL,
uniformNames: ["params"]
};
onSceneReady(event: SceneReadyEvent) {
const engine = event.engine;
const scene = event.scene;
const width = engine.getRenderWidth();
const height = engine.getRenderHeight();
const totalPixels = width * height;
// Buffer: 1 Float pro Pixel
const bufferSize = totalPixels * 4;
const pixelBuffer = new StorageBuffer(engine, bufferSize);
// Uniform Buffer
const ubo = new UniformBuffer(engine);
ubo.addUniform("resolution", 2);
ubo.addUniform("time", 1);
ubo.update();
// Compute Shader
const cs = new ComputeShader("myCompute", engine, {
computeSource: this.computeShaderWGSL
}, {
bindingsMapping: {
"pixelBuffer": { group: 0, binding: 0 },
"params": { group: 0, binding: 1 }
}
});
cs.setStorageBuffer("pixelBuffer", pixelBuffer);
cs.setUniformBuffer("params", ubo);
// Material Setup
const plane = scene.getMeshByName("plane");
if (plane && plane.material) {
const mat = plane.material as any;
mat.setStorageBuffer("pixelBuffer", pixelBuffer);
mat.setUniformBuffer("params", ubo);
}
// Render Loop
let time = 0;
scene.onBeforeRenderObservable.add(() => {
time += engine.getDeltaTime() / 1000.0;
ubo.updateFloat2("resolution", width, height);
ubo.updateFloat("time", time);
ubo.update();
const dispatchCount = Math.ceil(totalPixels / 64);
cs.dispatch(dispatchCount, 1, 1);
});
}
}

View File

@@ -44,6 +44,12 @@ export class AlgorithmsService {
title: 'ALGORITHM.FRACTAL3D.TITLE',
description: 'ALGORITHM.FRACTAL3D.DESCRIPTION',
routerLink: RouterConstants.FRACTAL3d.LINK
},
{
id: 'pendulum',
title: 'ALGORITHM.PENDULUM.TITLE',
description: 'ALGORITHM.PENDULUM.DESCRIPTION',
routerLink: RouterConstants.PENDULUM.LINK
}
];

View File

@@ -1,9 +1,9 @@
import {AfterViewInit, Component, ElementRef, EventEmitter, inject, Input, NgZone, OnDestroy, Output, ViewChild} from '@angular/core';
import {ArcRotateCamera, Camera, MeshBuilder, Scene, ShaderMaterial, Vector2, Vector3, WebGPUEngine} from '@babylonjs/core';
import {ArcRotateCamera, Camera, MeshBuilder, Scene, ShaderLanguage, ShaderMaterial, Vector2, Vector3, WebGPUEngine} from '@babylonjs/core';
export interface RenderConfig {
mode: '2D' | '3D';
computeShaderSupport?: boolean;
shaderLanguage?: number; //0 GLSL, 1 WGSL
initialViewSize: number;
vertexShader: string;
fragmentShader: string;
@@ -12,6 +12,11 @@ export interface RenderConfig {
export type RenderCallback = (material: ShaderMaterial, camera: Camera, canvas: HTMLCanvasElement, scene: Scene) => void;
export interface SceneReadyEvent {
scene: Scene;
engine: WebGPUEngine;
}
@Component({
selector: 'app-babylon-canvas',
imports: [],
@@ -26,7 +31,7 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
@Input({ required: true }) config!: RenderConfig;
@Input() renderCallback?: RenderCallback;
@Output() sceneReady = new EventEmitter<Scene>();
@Output() sceneReady = new EventEmitter<SceneReadyEvent>();
private engine!: WebGPUEngine;
private scene!: Scene;
@@ -63,7 +68,10 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
this.addListener(canvas);
this.createShaderMaterial();
this.createFullScreenRect();
this.sceneReady.emit(this.scene);
this.sceneReady.emit({
scene: this.scene,
engine: this.engine
});
this.addRenderLoop(canvas);
});
}
@@ -133,7 +141,8 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
},
{
attributes: ["position", "uv"],
uniforms: ["resolution", "cameraPosition", ...this.config.uniformNames]
uniforms: ["resolution", "cameraPosition", ...this.config.uniformNames],
shaderLanguage: this.config.shaderLanguage ?? ShaderLanguage.GLSL
}
);
this.shaderMaterial.disableDepthWrite = true;

View File

@@ -441,6 +441,10 @@
"TITLE": "Fraktale 3D",
"DESCRIPTION": "3D-Visualisierung von komplexe, geometrische Mustern, die sich selbst in immer kleineren Maßstäben ähneln (Selbstähnlichkeit)."
},
"PENDULUM": {
"TITLE": "Pendel",
"DESCRIPTION": "Noch ein WebGPU test."
},
"NOTE": "HINWEIS",
"GRID_HEIGHT": "Höhe",
"GRID_WIDTH": "Beite"

View File

@@ -440,6 +440,10 @@
"TITLE": "Fractals 3D",
"DESCRIPTION": "3D Visualisation of complex geometric patterns that resemble each other on increasingly smaller scales (self-similarity)."
},
"PENDULUM": {
"TITLE": "Pendulum",
"DESCRIPTION": "Just a test atm."
},
"NOTE": "Note",
"GRID_HEIGHT": "Height",
"GRID_WIDTH": "Width"