diff --git a/src/app/pages/algorithms/pendulum/pendulum.component.ts b/src/app/pages/algorithms/pendulum/pendulum.component.ts index 1824ae0..377d590 100644 --- a/src/app/pages/algorithms/pendulum/pendulum.component.ts +++ b/src/app/pages/algorithms/pendulum/pendulum.component.ts @@ -18,6 +18,7 @@ import {PENDULUM_FRAGMENT_SHADER_WGSL, PENDULUM_PHYSIC_COMPUTE_SHADER_WGSL, PEND }) export class PendulumComponent { + // --- CONFIGURATION --- renderConfig: RenderConfig = { mode: '2D', initialViewSize: 2, @@ -28,86 +29,84 @@ export class PendulumComponent { uniformBufferNames: [] }; - onSceneReady(event: SceneReadyEvent) { - const engine = event.engine; - const scene = event.scene; + // Central management of physics parameters + private readonly simParams = { + time: 0, + dt: 0.015, + g: 9.81, + m1: 2.0, + m2: 1.0, + l1: 1.5, + l2: 1.2, + damping: 0.999 // Less damping for longer swinging + }; + onSceneReady(event: SceneReadyEvent) { + const { engine, scene } = event; engine.resize(); const width = engine.getRenderWidth(); const height = engine.getRenderHeight(); const totalPixels = width * height; - // Pixel buffer for image data + // --- 1. BUFFERS --- const pixelBuffer = new StorageBuffer(engine, totalPixels * 4); - // Physics buffer for physics values const stateBuffer = new StorageBuffer(engine, 4 * 4); - stateBuffer.update(new Float32Array([Math.PI / 4, Math.PI / 2, 0, 0])); + stateBuffer.update(new Float32Array([Math.PI / 4, Math.PI / 2, 0, 0])); // Initial angles const paramsBuffer = new StorageBuffer(engine, 10 * 4); const paramsData = new Float32Array(10); - const csPhysics = new ComputeShader("physics", engine, { - computeSource: PENDULUM_PHYSIC_COMPUTE_SHADER_WGSL - }, { - bindingsMapping: { - "state": { group: 0, binding: 0 }, - "p": { group: 0, binding: 1 } - } - }); + // --- 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); // Nutzen jetzt StorageBuffer + 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 } - } - }); + 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); - // Material Setup + // --- 3. MATERIAL --- const plane = scene.getMeshByName("plane"); if (plane?.material) { const mat = plane.material as any; mat.setStorageBuffer("pixelBuffer", pixelBuffer); - mat.setStorageBuffer("paramsBuffer", paramsBuffer); + mat.setStorageBuffer("p", paramsBuffer); } - let time = 0; - const dt = 0.015; - + // --- 4. RENDER LOOP --- scene.onBeforeRenderObservable.add(() => { - time += dt; + 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] = time; - paramsData[3] = dt; - paramsData[4] = 9.81; // g - paramsData[5] = 2.0; // m1 - paramsData[6] = 1.0; // m2 - paramsData[7] = 1.5; // l1 - paramsData[8] = 1.2; // l2 - paramsData[9] = 0.99; // damping + 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; paramsBuffer.update(paramsData); - //dispatching physics + // Trigger simulation and rendering csPhysics.dispatch(1, 1, 1); - //doing rendering - const totalPixels = currentWidth * currentHeight; - const dispatchCount = Math.ceil(totalPixels / 64); + const dispatchCount = Math.ceil((currentWidth * currentHeight) / 64); csRender.dispatch(dispatchCount, 1, 1); }); } diff --git a/src/app/pages/algorithms/pendulum/pendulum.shader.ts b/src/app/pages/algorithms/pendulum/pendulum.shader.ts index 6c980cb..2ad76f5 100644 --- a/src/app/pages/algorithms/pendulum/pendulum.shader.ts +++ b/src/app/pages/algorithms/pendulum/pendulum.shader.ts @@ -1,7 +1,6 @@ //Simple Pass-Through Shader export const PENDULUM_VERTEX_SHADER_WGSL = ` attribute position : vec3; - attribute uv : vec2; @vertex fn main(input : VertexInputs) -> FragmentInputs { @@ -9,51 +8,64 @@ export const PENDULUM_VERTEX_SHADER_WGSL = ` 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 + }; + + struct State { + theta1: f32, + theta2: f32, + v1: f32, + v2: f32 + }; +`; //Fragment Shader to display the pixel buffer -export const PENDULUM_FRAGMENT_SHADER_WGSL = ` +export const PENDULUM_FRAGMENT_SHADER_WGSL = SHARED_STRUCTS + ` var pixelBuffer : array; - var paramsBuffer : array; + var p : Params; @fragment fn main(input : FragmentInputs) -> FragmentOutputs { - let width = u32(paramsBuffer[0]); - let height = u32(paramsBuffer[1]); + let width = u32(p.width); + let height = u32(p.height); + // Fallback if buffer is not loaded yet if (width == 0u || height == 0u) { fragmentOutputs.color = vec4(0.5, 0.0, 0.0, 1.0); return fragmentOutputs; } - // ============================================================== - // DER MAGISCHE TRICK: Wir ignorieren die kaputten UV-Koordinaten! - // input.position enthält die exakten Bildschirm-Pixelkoordinaten - // (z.B. x geht von 0 bis 1000, y geht von 0 bis 1000). - // Damit lesen wir den Puffer 1:1 auf Pixel-Ebene aus! - // ============================================================== + // Direct access to the pixel via screen coordinates let x = u32(input.position.x); let y = u32(input.position.y); - // Range check, if outside it is painted red + // Boundary check to prevent reading outside the buffer if (x >= width || y >= height) { - fragmentOutputs.color = vec4(1.0, 0.0, 0.0, 1.0); + fragmentOutputs.color = vec4(0.0, 0.0, 0.0, 1.0); return fragmentOutputs; } let index = y * width + x; let val = pixelBuffer[index]; - var color = vec3(0.1, 0.1, 0.15); - //pendulum color - if (val > 0.1) { - color = vec3(0.5, 0.5, 0.5); - } - - //mass color - if (val > 0.8) { - color = vec3(1.0, 1.0, 1.0); - } + var color = vec3(0.1, 0.1, 0.15); // Background + if (val > 0.1) { color = vec3(0.5, 0.5, 0.5); } // Line + if (val > 0.8) { color = vec3(1.0, 1.0, 1.0); } // Mass fragmentOutputs.color = vec4(color, 1.0); return fragmentOutputs; @@ -62,16 +74,9 @@ export const PENDULUM_FRAGMENT_SHADER_WGSL = ` //Math for the double pendulum //https://en.wikipedia.org/wiki/Double_pendulum -export const PENDULUM_PHYSIC_COMPUTE_SHADER_WGSL = ` - struct State { - theta1: f32, - theta2: f32, - v1: f32, - v2: f32 - }; - +export const PENDULUM_PHYSIC_COMPUTE_SHADER_WGSL = SHARED_STRUCTS + ` @group(0) @binding(0) var state : State; - @group(0) @binding(1) var p : array; + @group(0) @binding(1) var p : Params; @compute @workgroup_size(1) fn main() { @@ -80,49 +85,38 @@ export const PENDULUM_PHYSIC_COMPUTE_SHADER_WGSL = ` let v1 = state.v1; let v2 = state.v2; - let dt = p[3]; - let g = p[4]; - let m1 = p[5]; - let m2 = p[6]; - let l1 = p[7]; - let l2 = p[8]; - let damping = p[9]; + let delta_t = t1 - t2; - let num1 = -g * (2.0 * m1 + m2) * sin(t1) - - m2 * g * sin(t1 - 2.0 * t2) - - 2.0 * sin(t1 - t2) * m2 * (v2 * v2 * l2 + v1 * v1 * l1 * cos(t1 - t2)); - let den1 = l1 * (2.0 * m1 + m2 - m2 * cos(2.0 * t1 - 2.0 * t2)); + // Equations split for better readability + 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(t1 - t2) * (v1 * v1 * l1 * (m1 + m2) + g * (m1 + m2) * cos(t1) + v2 * v2 * l2 * m2 * cos(t1 - t2)); - let den2 = l2 * (2.0 * m1 + m2 - m2 * cos(2.0 * t1 - 2.0 * t2)); + 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 * dt) * damping; - let new_v2 = (v2 + a2 * dt) * damping; + // Integration (Semi-Implicit Euler) + let new_v1 = (v1 + a1 * p.dt) * p.damping; + let new_v2 = (v2 + a2 * p.dt) * p.damping; state.v1 = new_v1; state.v2 = new_v2; - state.theta1 = t1 + new_v1 * dt; - state.theta2 = t2 + new_v2 * dt; + 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 = ` - struct State { - theta1: f32, - theta2: f32, - v1: f32, - v2: f32 - }; - +export const PENDULUM_RENDER_COMPUTE_SHADER_WGSL = SHARED_STRUCTS + ` @group(0) @binding(0) var pixelBuffer : array; - @group(0) @binding(1) var p : array; + @group(0) @binding(1) var p : Params; @group(0) @binding(2) var state : State; - fn sdSegment(p: vec2, a: vec2, b: vec2) -> f32 { - let pa = p - a; + 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); @@ -131,49 +125,37 @@ export const PENDULUM_RENDER_COMPUTE_SHADER_WGSL = ` @compute @workgroup_size(64) fn main(@builtin(global_invocation_id) global_id : vec3) { let index = global_id.x; - - let width = u32(p[0]); - let height = u32(p[1]); + 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[0], y / p[1]); + let uv = vec2(x / p.width, y / p.height); - let aspect = p[0] / p[1]; + let aspect = p.width / p.height; let uv_corr = vec2(uv.x * aspect, uv.y); - var newVal = 0.0; + var newVal = 0.0; // Clear background - // Pendulum origin + // Pendulum geometry let origin = vec2(0.5 * aspect, 0.3); - - let s1 = sin(state.theta1); - let c1 = cos(state.theta1); - let s2 = sin(state.theta2); - let c2 = cos(state.theta2); - let displayScale = 0.15; - let p1 = origin + vec2(s1, c1) * p[7] * displayScale; - let p2 = p1 + vec2(s2, c2) * p[8] * displayScale; + // Calculate positions + 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; + // Distances 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); - let lineThick = 0.003; - let m1Radius = 0.02; - let m2Radius = 0.02; - - if (dLine1 < lineThick || dLine2 < lineThick) { - newVal = 0.5; - } - if (dMass1 < m1Radius || dMass2 < m2Radius) { - newVal = 1.0; - } + // Draw + if (dLine1 < 0.003 || dLine2 < 0.003) { newVal = 0.5; } + if (dMass1 < 0.02 || dMass2 < 0.02) { newVal = 1.0; } pixelBuffer[index] = newVal; }