diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index c8960c9..d079fbb 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -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} ]; diff --git a/src/app/constants/RouterConstants.ts b/src/app/constants/RouterConstants.ts index 6b9b9d9..ac1efab 100644 --- a/src/app/constants/RouterConstants.ts +++ b/src/app/constants/RouterConstants.ts @@ -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/pendulum/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', diff --git a/src/app/constants/UrlConstants.ts b/src/app/constants/UrlConstants.ts index 640b9d5..cc33dc8 100644 --- a/src/app/constants/UrlConstants.ts +++ b/src/app/constants/UrlConstants.ts @@ -17,4 +17,5 @@ static readonly MANDELBULB_WIKI = 'https://de.wikipedia.org/wiki/Mandelknolle' static readonly MANDELBOX_WIKI = 'https://de.wikipedia.org/wiki/Mandelbox' static readonly JULIA3D_WIKI = 'https://de.wikipedia.org/wiki/Mandelknolle' + static readonly DOUBLE_PENDULUM_WIKI = 'https://de.wikipedia.org/wiki/Doppelpendel' } diff --git a/src/app/pages/algorithms/conway-gol/conway-gol.component.html b/src/app/pages/algorithms/conway-gol/conway-gol.component.html index 7bb078b..d0cfcc0 100644 --- a/src/app/pages/algorithms/conway-gol/conway-gol.component.html +++ b/src/app/pages/algorithms/conway-gol/conway-gol.component.html @@ -35,8 +35,8 @@ }

{{ 'SORTING.EXECUTION_TIME' | translate }}: {{ executionTime }} ms

-
- +
+ {{ 'ALGORITHM.GRID_HEIGHT' | translate }} - + {{ 'ALGORITHM.GRID_WIDTH' | translate }} - + {{ 'GOL.SPEED' | translate }} { + onSceneReady(event: SceneEventData): void { + event.scene.onPointerObservable.add((pointerInfo) => { switch (pointerInfo.type) { case PointerEventTypes.POINTERDOWN: diff --git a/src/app/pages/algorithms/pathfinding/pathfinding.component.html b/src/app/pages/algorithms/pathfinding/pathfinding.component.html index 9110a47..bfc4f13 100644 --- a/src/app/pages/algorithms/pathfinding/pathfinding.component.html +++ b/src/app/pages/algorithms/pathfinding/pathfinding.component.html @@ -26,8 +26,8 @@
-
- +
+ {{ 'ALGORITHM.GRID_HEIGHT' | translate }} - + {{ 'ALGORITHM.GRID_WIDTH' | translate }} + + {{ 'PENDULUM.TITLE' | translate }} + + + +
+
+

{{ 'PENDULUM.TRAIL_DECAY_TIME' | translate }}

+ +

{{ 'PENDULUM.ATTRACTION' | translate }}

+ +
+
+

{{ 'PENDULUM.L1_LENGTH' | translate }}

+ +

{{ 'PENDULUM.L2_LENGTH' | translate }}

+ +
+
+

{{ 'PENDULUM.M1_MASS' | translate }}

+ +

{{ 'PENDULUM.M2_MASS' | translate }}

+ +
+
+

{{ 'PENDULUM.DAMPING' | translate }}

+ +
+
+ + + +
+
+ L1 + L2 + M1 + M2 +
+
+ +
+ diff --git a/src/app/pages/algorithms/pendulum/pendulum.component.scss b/src/app/pages/algorithms/pendulum/pendulum.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/pages/algorithms/pendulum/pendulum.component.ts b/src/app/pages/algorithms/pendulum/pendulum.component.ts new file mode 100644 index 0000000..3c27ef4 --- /dev/null +++ b/src/app/pages/algorithms/pendulum/pendulum.component.ts @@ -0,0 +1,240 @@ +import {Component} from '@angular/core'; +import {BabylonCanvas, RenderConfig, SceneEventData} from '../../../shared/rendering/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'; +import {FormsModule} from '@angular/forms'; +import {NgxSliderModule, Options} from '@angular-slider/ngx-slider'; +import {DEFAULT_DAMPING, DEFAULT_G, DEFAULT_L1_LENGTH, DEFAULT_M1_MASS, DEFAULT_L2_LENGTH, DEFAULT_M2_MASS, DEFAULT_TRAIL_DECAY, MAX_DAMPING, MAX_G, MAX_LENGTH, MAX_MASS, MAX_TRAIL_DECAY, MIN_DAMPING, MIN_G, MIN_LENGTH, MIN_MASS, MIN_TRAIL_DECAY, IMPULSE_M2, IMPULSE_M1} from './pendulum.model'; +import {TranslatePipe} from '@ngx-translate/core'; +import {MatButton} from '@angular/material/button'; +import {Information} from '../information/information'; +import {AlgorithmInformation} from '../information/information.models'; +import {UrlConstants} from '../../../constants/UrlConstants'; + +@Component({ + selector: 'app-pendulum', + imports: [ + BabylonCanvas, + MatCard, + MatCardContent, + MatCardHeader, + MatCardTitle, + FormsModule, + NgxSliderModule, + TranslatePipe, + MatButton, + Information, + ], + templateUrl: './pendulum.component.html', + styleUrl: './pendulum.component.scss', +}) +class PendulumComponent { + + // --- CONFIGURATION --- + algoInformation: AlgorithmInformation = { + title: 'PENDULUM.EXPLANATION.TITLE', + entries: [ + { + name: '', + description: 'PENDULUM.EXPLANATION.EXPLANATION', + link: UrlConstants.DOUBLE_PENDULUM_WIKI + } + ], + disclaimer: 'PENDULUM.EXPLANATION.DISCLAIMER', + disclaimerBottom: 'PENDULUM.EXPLANATION.DISCLAIMER_BOTTOM', + disclaimerListEntry: ['PENDULUM.EXPLANATION.DISCLAIMER_1', 'PENDULUM.EXPLANATION.DISCLAIMER_2', 'PENDULUM.EXPLANATION.DISCLAIMER_3', 'PENDULUM.EXPLANATION.DISCLAIMER_4'] + }; + + + renderConfig: RenderConfig = { + mode: '2D', + initialViewSize: 2, + shaderLanguage: ShaderLanguage.WGSL, + vertexShader: PENDULUM_VERTEX_SHADER_WGSL, + fragmentShader: PENDULUM_FRAGMENT_SHADER_WGSL, + uniformNames: [], + uniformBufferNames: [] + }; + + trailDecayOptions: Options = { + floor: MIN_TRAIL_DECAY, + ceil: MAX_TRAIL_DECAY, + logScale: false, + step: 0.001, + showTicks: false, + hideLimitLabels: false, + hidePointerLabels: false + }; + + gravityOptions: Options = { + floor: MIN_G, + ceil: MAX_G, + logScale: false, + step: 0.01, + showTicks: false, + hideLimitLabels: false, + hidePointerLabels: false + }; + + dampingOptions: Options = { + floor: MAX_DAMPING, + ceil: MIN_DAMPING, + logScale: false, + step: 0.001, + showTicks: false, + hideLimitLabels: false, + hidePointerLabels: false + }; + + lengthOptions: Options = { + floor: MIN_LENGTH, + ceil: MAX_LENGTH, + logScale: false, + step: 0.1, + showTicks: false, + hideLimitLabels: false, + hidePointerLabels: false + }; + + massOptions: Options = { + floor: MIN_MASS, + ceil: MAX_MASS, + logScale: false, + step: 0.1, + showTicks: false, + hideLimitLabels: false, + hidePointerLabels: false + }; + + // Central management of physics parameters + readonly simParams = { + time: 0, + dt: 0.015, + g: DEFAULT_G, + m1: DEFAULT_M1_MASS, + m2: DEFAULT_M2_MASS, + l1: DEFAULT_L1_LENGTH, + l2: DEFAULT_L2_LENGTH, + damping: DEFAULT_DAMPING, + trailDecay: DEFAULT_TRAIL_DECAY, + impulseM1: 0.0, + impulseM2: 0.0, + }; + + private currentSceneData: SceneEventData | null = null; + + onSceneReady(event: SceneEventData) { + this.currentSceneData = event; + this.createSimulation(); + } + + private createSimulation() { + if (!this.currentSceneData){ + return; + } + const {engine, scene} = this.currentSceneData; + engine.resize(); + + const width = engine.getRenderWidth(); + const height = engine.getRenderHeight(); + const totalPixels = width * height; + + // --- 1. BUFFERS --- + const pixelBuffer = new StorageBuffer(engine, totalPixels * 4); + + const stateBuffer = new StorageBuffer(engine, 4 * 4); + stateBuffer.update(new Float32Array([Math.PI / 4, Math.PI / 2, 0, 0])); // Initial angles + + const paramsBuffer = new StorageBuffer(engine, 14 * 4); + const paramsData = new Float32Array(14); + + // --- 2. SHADERS --- + const csPhysics = new ComputeShader("physics", engine, + {computeSource: PENDULUM_PHYSIC_COMPUTE_SHADER_WGSL}, + {bindingsMapping: {"state": {group: 0, binding: 0}, "p": {group: 0, binding: 1}}} + ); + csPhysics.setStorageBuffer("state", stateBuffer); + csPhysics.setStorageBuffer("p", paramsBuffer); + + const csRender = new ComputeShader("render", engine, + {computeSource: PENDULUM_RENDER_COMPUTE_SHADER_WGSL}, + {bindingsMapping: {"pixelBuffer": {group: 0, binding: 0}, "p": {group: 0, binding: 1}, "state": {group: 0, binding: 2}}} + ); + csRender.setStorageBuffer("pixelBuffer", pixelBuffer); + csRender.setStorageBuffer("p", paramsBuffer); + csRender.setStorageBuffer("state", stateBuffer); + + // --- 3. MATERIAL --- + const plane = scene.getMeshByName("plane"); + if (plane?.material) { + const mat = plane.material as any; + mat.setStorageBuffer("pixelBuffer", pixelBuffer); + mat.setStorageBuffer("p", paramsBuffer); + } + + //remove old observables if available + scene.onBeforeRenderObservable.clear(); + // --- 4. RENDER LOOP --- + scene.onBeforeRenderObservable.add(() => { + this.simParams.time += this.simParams.dt; + + const currentWidth = engine.getRenderWidth(); + const currentHeight = engine.getRenderHeight(); + + // Fill parameter array (must match the exact order of the WGSL struct!) + paramsData[0] = currentWidth; + paramsData[1] = currentHeight; + paramsData[2] = this.simParams.time; + paramsData[3] = this.simParams.dt; + paramsData[4] = this.simParams.g; + paramsData[5] = this.simParams.m1; + paramsData[6] = this.simParams.m2; + paramsData[7] = this.simParams.l1; + paramsData[8] = this.simParams.l2; + paramsData[9] = this.simParams.damping; + paramsData[10] = this.simParams.trailDecay; + paramsData[11] = this.simParams.impulseM1; + paramsData[12] = this.simParams.impulseM2; + paramsData[13] = 0; // Pad + + this.resetImpulses(); + + paramsBuffer.update(paramsData); + + // Trigger simulation and rendering + csPhysics.dispatch(1, 1, 1); + + const dispatchCount = Math.ceil((currentWidth * currentHeight) / 64); + csRender.dispatch(dispatchCount, 1, 1); + }); + } + + private resetImpulses() { + if (this.simParams.impulseM1 !== 0.0) { + this.simParams.impulseM1 = 0; + } + + if (this.simParams.impulseM2 !== 0.0) { + this.simParams.impulseM2 = 0; + } + } + + pushPendulum(m1: boolean) { + if (m1) + { + this.simParams.impulseM1 = IMPULSE_M1; + return; + } + + this.simParams.impulseM2 = IMPULSE_M2; + } + + resetPendulum() { + this.createSimulation(); + } +} + + + +export default PendulumComponent diff --git a/src/app/pages/algorithms/pendulum/pendulum.model.ts b/src/app/pages/algorithms/pendulum/pendulum.model.ts new file mode 100644 index 0000000..63d48b1 --- /dev/null +++ b/src/app/pages/algorithms/pendulum/pendulum.model.ts @@ -0,0 +1,24 @@ +export const DEFAULT_G = 9.81; +export const MIN_G = 2; +export const MAX_G = 15; + +export const DEFAULT_DAMPING = 0.999; +export const MIN_DAMPING = 1; +export const MAX_DAMPING = 0.7; + +export const DEFAULT_TRAIL_DECAY = 0.96; +export const MIN_TRAIL_DECAY = 0.2; +export const MAX_TRAIL_DECAY = 0.9999; + +export const DEFAULT_L1_LENGTH = 1.5; +export const DEFAULT_L2_LENGTH = 1.2; +export const MIN_LENGTH = 0.2; +export const MAX_LENGTH = 3; + +export const DEFAULT_M1_MASS = 2; +export const DEFAULT_M2_MASS = 1; +export const MIN_MASS = 0.1; +export const MAX_MASS = 5; + +export const IMPULSE_M1 = 7; +export const IMPULSE_M2 = 15; diff --git a/src/app/pages/algorithms/pendulum/pendulum.shader.ts b/src/app/pages/algorithms/pendulum/pendulum.shader.ts new file mode 100644 index 0000000..c095e70 --- /dev/null +++ b/src/app/pages/algorithms/pendulum/pendulum.shader.ts @@ -0,0 +1,236 @@ +//Simple Pass-Through Shader +export const PENDULUM_VERTEX_SHADER_WGSL = ` + attribute position : vec3; + + @vertex + fn main(input : VertexInputs) -> FragmentInputs { + var output : FragmentInputs; + output.position = vec4(input.position, 1.0); + return output; + } +`; + +// --- SHARED DATA STRUCTURES --- +// These structs map exactly to the Float32Array in the TypeScript code. +const SHARED_STRUCTS = ` + struct Params { + width: f32, + height: f32, + time: f32, + dt: f32, + g: f32, + m1: f32, + m2: f32, + l1: f32, + l2: f32, + damping: f32, + trailDecay: f32, + impulseM1: f32, + impulseM2: f32, + pad: f32 // <-- Padding for safe 16-byte memory alignment + }; + + struct State { + theta1: f32, + theta2: f32, + v1: f32, + v2: f32 + }; +`; + +//Fragment Shader to display the pixel buffer +export const PENDULUM_FRAGMENT_SHADER_WGSL = SHARED_STRUCTS + ` + var pixelBuffer : array; + var p : Params; + + @fragment + fn main(input : FragmentInputs) -> FragmentOutputs { + let width = u32(p.width); + let height = u32(p.height); + + if (width == 0u || height == 0u) { + fragmentOutputs.color = vec4(0.5, 0.0, 0.0, 1.0); + return fragmentOutputs; + } + + let x = u32(input.position.x); + let y = u32(input.position.y); + + if (x >= width || y >= height) { + fragmentOutputs.color = vec4(0.0, 0.0, 0.0, 1.0); + return fragmentOutputs; + } + + let index = y * width + x; + + // --- THE MAGIC DECODING --- + var val = pixelBuffer[index]; + var isLine1 = false; + var isLine2 = false; + + // 1. Check for overlays (Lines) + if (val >= 20.0) { + isLine2 = true; + val = val - 20.0; + } else if (val >= 10.0) { + isLine1 = true; + val = val - 10.0; + } + + // 2. Check which trail it is + var isTrail2 = false; + if (val >= 2.0) { + isTrail2 = true; + val = val - 2.0; + } + + // 3. What remains is purely the fading intensity (0.0 to 1.0) + let trailIntensity = val; + + // --- COLORS --- + let bgColor = vec3(0.1, 0.1, 0.15); + let mass1Color = vec3(1.0, 0.0, 0.0); // Red + let mass2Color = vec3(0.0, 1.0, 0.0); // Green + let line1Color = vec3(1.0, 1.0, 0.0); // Yellow + let line2Color = vec3(1.0, 0.0, 1.0); // Magenta + + var massColor = mass1Color; + if (isTrail2) { + massColor = mass2Color; + } + + // Calculate background blending with the trail + var finalColor = mix(bgColor, massColor, clamp(trailIntensity, 0.0, 1.0)); + + // Overwrite with the line colors if necessary + if (isLine1) { finalColor = line1Color; } + if (isLine2) { finalColor = line2Color; } + + fragmentOutputs.color = vec4(finalColor, 1.0); + return fragmentOutputs; + } +`; + +//Math for the double pendulum +//https://en.wikipedia.org/wiki/Double_pendulum +export const PENDULUM_PHYSIC_COMPUTE_SHADER_WGSL = SHARED_STRUCTS + ` + @group(0) @binding(0) var state : State; + @group(0) @binding(1) var p : Params; + + @compute @workgroup_size(1) + fn main() { + let t1 = state.theta1; + let t2 = state.theta2; + let v1 = state.v1; + let v2 = state.v2; + + let delta_t = t1 - t2; + + let num1 = -p.g * (2.0 * p.m1 + p.m2) * sin(t1) + - p.m2 * p.g * sin(t1 - 2.0 * t2) + - 2.0 * sin(delta_t) * p.m2 * (v2 * v2 * p.l2 + v1 * v1 * p.l1 * cos(delta_t)); + let den1 = p.l1 * (2.0 * p.m1 + p.m2 - p.m2 * cos(2.0 * delta_t)); + let a1 = num1 / den1; + + let num2 = 2.0 * sin(delta_t) * (v1 * v1 * p.l1 * (p.m1 + p.m2) + p.g * (p.m1 + p.m2) * cos(t1) + v2 * v2 * p.l2 * p.m2 * cos(delta_t)); + let den2 = p.l2 * (2.0 * p.m1 + p.m2 - p.m2 * cos(2.0 * delta_t)); + let a2 = num2 / den2; + + let new_v1 = (v1 + a1 * p.dt) * p.damping + p.impulseM1; + let new_v2 = (v2 + a2 * p.dt) * p.damping + p.impulseM2; + + state.v1 = new_v1; + state.v2 = new_v2; + state.theta1 = t1 + new_v1 * p.dt; + state.theta2 = t2 + new_v2 * p.dt; + } +`; + +//Pixel data to visualize the pendulum +export const PENDULUM_RENDER_COMPUTE_SHADER_WGSL = SHARED_STRUCTS + ` + @group(0) @binding(0) var pixelBuffer : array; + @group(0) @binding(1) var p : Params; + @group(0) @binding(2) var state : State; + + fn sdSegment(point: vec2, a: vec2, b: vec2) -> f32 { + let pa = point - a; + let ba = b - a; + let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); + return length(pa - ba * h); + } + + @compute @workgroup_size(64) + fn main(@builtin(global_invocation_id) global_id : vec3) { + let index = global_id.x; + let width = u32(p.width); + let height = u32(p.height); + + if (index >= width * height) { return; } + + let x = f32(index % width); + let y = f32(index / width); + let uv = vec2(x / p.width, y / p.height); + + let aspect = p.width / p.height; + let uv_corr = vec2(uv.x * aspect, uv.y); + + // --- 1. EXTRACT & DECAY OLD MEMORY --- + var memory = pixelBuffer[index]; + + // Strip line overlays from the previous frame + if (memory >= 20.0) { memory = memory - 20.0; } + else if (memory >= 10.0) { memory = memory - 10.0; } + + // Check if the memory belongs to Trail 2 + var isTrail2 = false; + if (memory >= 2.0) { + isTrail2 = true; + memory = memory - 2.0; + } + + // Apply decay to the pure intensity + memory = memory * p.trailDecay; + + // --- 2. CALCULATE GEOMETRY --- + let origin = vec2(0.5 * aspect, 0.3); + let displayScale = 0.15; + + let p1 = origin + vec2(sin(state.theta1), cos(state.theta1)) * p.l1 * displayScale; + let p2 = p1 + vec2(sin(state.theta2), cos(state.theta2)) * p.l2 * displayScale; + + let dLine1 = sdSegment(uv_corr, origin, p1); + let dLine2 = sdSegment(uv_corr, p1, p2); + let dMass1 = length(uv_corr - p1); + let dMass2 = length(uv_corr - p2); + + // --- 3. SMART LAYERING --- + var baseVal = 0.0; + + // Base Layer (Masses & Trails) + if (dMass1 < 0.02) { + baseVal = 1.0; // Mass 1 = 1.0 (Trail 1 Max) + } else if (dMass2 < 0.02) { + baseVal = 3.0; // Mass 2 = 2.0 (Flag) + 1.0 (Trail 2 Max) + } else { + // Write fading memory back + if (isTrail2) { + baseVal = memory + 2.0; + } else { + baseVal = memory; + } + } + + // Overlay Layer (Lines) + var overlay = 0.0; + // Don't draw lines over the masses (Clean Z-Index) + if (dMass1 < 0.02 || dMass2 < 0.02) { + overlay = 0.0; + } else if (dLine1 < 0.003) { + overlay = 10.0; + } else if (dLine2 < 0.003) { + overlay = 20.0; + } + + pixelBuffer[index] = baseVal + overlay; + } +`; diff --git a/src/app/pages/algorithms/service/algorithms.service.ts b/src/app/pages/algorithms/service/algorithms.service.ts index bb9e889..c56b771 100644 --- a/src/app/pages/algorithms/service/algorithms.service.ts +++ b/src/app/pages/algorithms/service/algorithms.service.ts @@ -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 } ]; diff --git a/src/app/shared/rendering/canvas/babylon-canvas.component.scss b/src/app/shared/rendering/canvas/babylon-canvas.component.scss index aca3f68..b482a7a 100644 --- a/src/app/shared/rendering/canvas/babylon-canvas.component.scss +++ b/src/app/shared/rendering/canvas/babylon-canvas.component.scss @@ -1,16 +1,19 @@ .canvas-container { - width: 100%; display: flex; justify-content: center; align-items: center; - background-color: #1a1a1a; + width: 100%; + + max-width: 1000px; + max-height: 1000px; + margin: 0 auto; } canvas { - aspect-ratio: 1 / 1; - + display: block; width: 100%; - height: auto; + height: 100%; + aspect-ratio: 1 / 1; min-width: 200px; min-height: 200px; @@ -18,8 +21,7 @@ canvas { max-height: 1000px; touch-action: none; - display: block; - border: none; + border-radius: 20px; outline: none; } diff --git a/src/app/shared/rendering/canvas/babylon-canvas.component.ts b/src/app/shared/rendering/canvas/babylon-canvas.component.ts index 5ad06cf..975a187 100644 --- a/src/app/shared/rendering/canvas/babylon-canvas.component.ts +++ b/src/app/shared/rendering/canvas/babylon-canvas.component.ts @@ -1,16 +1,23 @@ import {AfterViewInit, Component, ElementRef, EventEmitter, inject, Input, NgZone, OnDestroy, Output, ViewChild} from '@angular/core'; -import {ArcRotateCamera, Camera, Engine, MeshBuilder, Scene, ShaderMaterial, Vector2, Vector3} from '@babylonjs/core'; +import {ArcRotateCamera, Camera, MeshBuilder, Scene, ShaderLanguage, ShaderMaterial, Vector2, Vector3, WebGPUEngine} from '@babylonjs/core'; export interface RenderConfig { mode: '2D' | '3D'; + shaderLanguage?: number; //0 GLSL, 1 WGSL initialViewSize: number; vertexShader: string; fragmentShader: string; uniformNames: string[]; + uniformBufferNames?: string[]; } export type RenderCallback = (material: ShaderMaterial, camera: Camera, canvas: HTMLCanvasElement, scene: Scene) => void; +export interface SceneEventData { + scene: Scene; + engine: WebGPUEngine; +} + @Component({ selector: 'app-babylon-canvas', imports: [], @@ -25,21 +32,20 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy { @Input({ required: true }) config!: RenderConfig; @Input() renderCallback?: RenderCallback; - @Output() sceneReady = new EventEmitter(); + @Output() sceneReady = new EventEmitter(); + @Output() sceneResized = new EventEmitter(); - private engine!: Engine; + private engine!: WebGPUEngine; private scene!: Scene; private shaderMaterial!: ShaderMaterial; private camera!: Camera; //Listener - private resizeHandler = () => this.handleResize(); - private wheelHandler = (evt: WheelEvent) => evt.preventDefault(); + private readonly resizeHandler = () => this.handleResize(); + private readonly wheelHandler = (evt: WheelEvent) => evt.preventDefault(); ngAfterViewInit(): void { - this.ngZone.runOutsideAngular(() => { - this.initBabylon(); - }); + this.initBabylon().then(() => { console.log("Engine initialized"); }); } ngOnDestroy(): void { @@ -55,16 +61,21 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy { } } - private initBabylon(): void { + private async initBabylon(): Promise { const canvas = this.canvasRef.nativeElement; - this.engine = new Engine(canvas, true); - this.scene = new Scene(this.engine); - this.setupCamera(canvas); - this.addListener(canvas); - this.createShaderMaterial(); - this.createFullScreenRect(); - this.sceneReady.emit(this.scene); - this.addRenderLoop(canvas); + this.engine = new WebGPUEngine(canvas); + await this.engine.initAsync().then(() => { + this.scene = new Scene(this.engine); + this.setupCamera(canvas); + this.addListener(canvas); + this.createShaderMaterial(); + this.createFullScreenRect(); + this.sceneReady.emit({ + scene: this.scene, + engine: this.engine + }); + this.addRenderLoop(canvas); + }); } private addListener(canvas: HTMLCanvasElement) { @@ -109,7 +120,7 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy { } private createFullScreenRect() { - const plane = MeshBuilder.CreatePlane("plane", {size: 110}, this.scene); + const plane = MeshBuilder.CreatePlane("plane", {size: 100}, this.scene); if (this.config.mode === '3D') { plane.parent = this.camera; @@ -132,7 +143,9 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy { }, { attributes: ["position", "uv"], - uniforms: ["resolution", "cameraPosition", ...this.config.uniformNames] + uniforms: ["resolution", "cameraPosition", ...this.config.uniformNames], + uniformBuffers: this.config.uniformBufferNames ?? [], + shaderLanguage: this.config.shaderLanguage ?? ShaderLanguage.GLSL } ); this.shaderMaterial.disableDepthWrite = true; @@ -167,5 +180,10 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy { this.camera.orthoTop = viewSize / 2; this.camera.orthoBottom = -viewSize / 2; } + + this.sceneResized?.emit({ + scene: this.scene, + engine: this.engine + }); } } diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 0fa1e57..d20c574 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -1,7 +1,7 @@ { "APP": { "TITLE": "Playground", - "COPYRIGHT": "Bilder urheberrechtlich geschützt, keine Nutzung ohne Zustimmung!" + "COPYRIGHT": "Bilder und Sourcecode sind urheberrechtlich geschützt, keine Nutzung ohne Zustimmung!" }, "TOPBAR": { "ABOUT": "Über mich", @@ -415,6 +415,29 @@ "DISCLAIMER_4": "Licht & Schatten: Um die Tiefe sichtbar zu machen, werden Lichtreflexionen und Schatten (Ambient Occlusion) basierend auf der Krümmung der Formel simuliert." } }, + "PENDULUM": { + "TITLE": "Doppel-Pendel", + "TRAIL_DECAY_TIME": "Spurlänge", + "DAMPING": "Dämpfung", + "ATTRACTION": "Anziehungskraft", + "L1_LENGTH": "Länge L1", + "L2_LENGTH": "Länge L2", + "M1_MASS": "Masse M1", + "M2_MASS": "Masse M2", + "POKE_M1": "Schubse M1", + "POKE_M2": "Schubse M2", + "RESET": "Neustarten", + "EXPLANATION": { + "TITLE": "Chaostheorie: Das Doppelpendel", + "EXPLANATION": "Das Doppelpendel ist eines der bekanntesten und faszinierendsten Beispiele der Physik für ein dynamisches System, das 'deterministisches Chaos' erzeugt. Es besteht schlicht aus einem einfachen Pendel, an dessen unterem Ende ein zweites Pendel befestigt ist. Obwohl die zugrundeliegenden Bewegungsgesetze der klassischen Mechanik streng mathematisch definiert sind, ist das Verhalten des Doppelpendels auf lange Sicht absolut unvorhersehbar. Es gilt in der Physik als das klassische Vorzeigeobjekt für den sogenannten Schmetterlingseffekt.", + "DISCLAIMER": "Diese WebGPU-Simulation berechnet die Bewegungs- und Beschleunigungsgleichungen des Pendels 60-mal pro Sekunde in Echtzeit. Dabei gelten folgende Besonderheiten:", + "DISCLAIMER_1": "Extreme Sensitivität: Winzigste Änderungen in den Startbedingungen (z.B. ein Tausendstel Grad Abweichung im Startwinkel oder bei der Masse) führen schon nach kurzer Zeit zu einer völlig anderen, chaotischen Flugbahn.", + "DISCLAIMER_2": "Deterministisches Chaos: Die Bewegung wirkt zwar völlig wild und zufällig, ist es aber nicht. Startest du die Simulation mit exakt denselben Werten neu, wird das Pendel zu 100 % denselben Weg fliegen.", + "DISCLAIMER_3": "Numerische Integration: Da Computer Zeit nicht stufenlos, sondern in winzigen Schritten (dt) berechnen, entstehen bei jedem Frame winzige mathematische Rundungsfehler. Diese summieren sich auf und beeinflussen das Chaos zusätzlich.", + "DISCLAIMER_4": "Energieerhaltung & Reibung: In einem perfekten physikalischen System ohne Widerstand würde das Pendel ewig weiterschwingen. Für eine natürliche Optik nutzt der Algorithmus einen künstlichen Dämpfungsfaktor, der Luftreibung simuliert und das System irgendwann beruhigt.", + "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." + } + }, "ALGORITHM": { "TITLE": "Algorithmen", "PATHFINDING": { @@ -441,6 +464,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": "Doppel-Pendel", + "DESCRIPTION": "Visualisierung einer chaotischen Doppel-Pendel-Simulation 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 f770896..4c970d5 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -1,7 +1,7 @@ { "APP": { "TITLE": "Playground", - "COPYRIGHT": "Images protected by copyright, no use without permission!" + "COPYRIGHT": "Images and code protected by copyright, no use without permission!" }, "TOPBAR": { "ABOUT": "About me", @@ -414,6 +414,29 @@ "DISCLAIMER_4": "Light & Shadow: To visualize depth, light reflections and shadows (Ambient Occlusion) are simulated based on the curvature of the formula." } }, + "PENDULUM": { + "TITLE": "Double pendulum", + "TRAIL_DECAY_TIME": "Trail length", + "DAMPING": "Damping", + "ATTRACTION": "Attraction", + "L1_LENGTH": "Length L1", + "L2_LENGTH": "Length L2", + "M1_MASS": "Mass M1", + "M2_MASS": "Mass M2", + "POKE_M1": "Poke M1", + "POKE_M2": "Poke M2", + "RESET": "Reset", + "EXPLANATION": { + "TITLE": "Chaos Theory: The Double Pendulum", + "EXPLANATION": "The double pendulum is one of physics' most famous and fascinating examples of a dynamic system that generates 'deterministic chaos'. It simply consists of a standard pendulum with a second pendulum attached to its lower end. Although the underlying laws of classical mechanics are strictly mathematically defined, the long-term behavior of the double pendulum is absolutely unpredictable. In physics, it is considered the classic showcase object for the so-called butterfly effect.", + "DISCLAIMER": "This WebGPU simulation calculates the motion and acceleration equations of the pendulum 60 times per second in real-time. The following characteristics apply:", + "DISCLAIMER_1": "Extreme Sensitivity: The tiniest changes in the initial conditions (e.g., a thousandth of a degree deviation in the starting angle or mass) lead to a completely different, chaotic trajectory after just a short time.", + "DISCLAIMER_2": "Deterministic Chaos: The movement may look completely wild and random, but it isn't. If you restart the simulation with the exact same values, the pendulum will follow 100% the same path.", + "DISCLAIMER_3": "Numerical Integration: Since computers do not calculate time continuously but in tiny steps (dt), minute mathematical rounding errors occur in every frame. These add up over time and further influence the chaos.", + "DISCLAIMER_4": "Energy Conservation & Friction: In a perfect physical system without resistance, the pendulum would swing forever. For a natural look, the algorithm uses an artificial damping factor that simulates air friction and eventually brings the system to a halt.", + "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." + } + }, "ALGORITHM": { "TITLE": "Algorithms", "PATHFINDING": { @@ -440,6 +463,10 @@ "TITLE": "Fractals 3D", "DESCRIPTION": "3D Visualisation of complex geometric patterns that resemble each other on increasingly smaller scales (self-similarity)." }, + "PENDULUM": { + "TITLE": "Double pendulum", + "DESCRIPTION": "Visualisation of a chaotic double pendulum simulation with WebGPU." + }, "NOTE": "Note", "GRID_HEIGHT": "Height", "GRID_WIDTH": "Width" diff --git a/src/styles.scss b/src/styles.scss index fdba66c..ccbe57b 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -262,13 +262,13 @@ a { } } -.grid-size { +.input-container { display: flex; gap: 0.75rem; align-items: center; flex-wrap: wrap; - .grid-field { + .input-field { width: 150px; } } @@ -301,6 +301,10 @@ canvas { &.path { background-color: gold; } &.empty { background-color: lightgray; } &.alive { background-color: black; } + &.L1 { background-color: yellow; } + &.L2 { background-color: magenta; } + &.M1 { background-color: red; } + &.M2 { background-color: green; } } } @@ -310,6 +314,12 @@ canvas { margin-bottom: 1rem; } +.slider-control-container { + display: flex; + align-items: center; + gap: 10px; +} + /* Sorting Visualization */ .sorting-visualization-area { display: flex; @@ -339,4 +349,4 @@ canvas { background-color: #4caf50; /* Green for sorted */ } } -} \ No newline at end of file +}