Refactored #2

- Refactored shader code and typescript code
- Made it more clear
- Added some comments
This commit is contained in:
2026-02-21 10:03:01 +01:00
parent 66df3a7f88
commit 13f99ac7ae
2 changed files with 109 additions and 128 deletions

View File

@@ -18,6 +18,7 @@ import {PENDULUM_FRAGMENT_SHADER_WGSL, PENDULUM_PHYSIC_COMPUTE_SHADER_WGSL, PEND
}) })
export class PendulumComponent { export class PendulumComponent {
// --- CONFIGURATION ---
renderConfig: RenderConfig = { renderConfig: RenderConfig = {
mode: '2D', mode: '2D',
initialViewSize: 2, initialViewSize: 2,
@@ -28,86 +29,84 @@ export class PendulumComponent {
uniformBufferNames: [] uniformBufferNames: []
}; };
onSceneReady(event: SceneReadyEvent) { // Central management of physics parameters
const engine = event.engine; private readonly simParams = {
const scene = event.scene; 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(); engine.resize();
const width = engine.getRenderWidth(); const width = engine.getRenderWidth();
const height = engine.getRenderHeight(); const height = engine.getRenderHeight();
const totalPixels = width * height; const totalPixels = width * height;
// Pixel buffer for image data // --- 1. BUFFERS ---
const pixelBuffer = new StorageBuffer(engine, totalPixels * 4); const pixelBuffer = new StorageBuffer(engine, totalPixels * 4);
// Physics buffer for physics values
const stateBuffer = new StorageBuffer(engine, 4 * 4); 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 paramsBuffer = new StorageBuffer(engine, 10 * 4);
const paramsData = new Float32Array(10); const paramsData = new Float32Array(10);
const csPhysics = new ComputeShader("physics", engine, { // --- 2. SHADERS ---
computeSource: PENDULUM_PHYSIC_COMPUTE_SHADER_WGSL const csPhysics = new ComputeShader("physics", engine,
}, { { computeSource: PENDULUM_PHYSIC_COMPUTE_SHADER_WGSL },
bindingsMapping: { { bindingsMapping: { "state": { group: 0, binding: 0 }, "p": { group: 0, binding: 1 } } }
"state": { group: 0, binding: 0 }, );
"p": { group: 0, binding: 1 }
}
});
csPhysics.setStorageBuffer("state", stateBuffer); csPhysics.setStorageBuffer("state", stateBuffer);
csPhysics.setStorageBuffer("p", paramsBuffer); // Nutzen jetzt StorageBuffer csPhysics.setStorageBuffer("p", paramsBuffer);
const csRender = new ComputeShader("render", engine, { const csRender = new ComputeShader("render", engine,
computeSource: PENDULUM_RENDER_COMPUTE_SHADER_WGSL { computeSource: PENDULUM_RENDER_COMPUTE_SHADER_WGSL },
}, { { bindingsMapping: { "pixelBuffer": { group: 0, binding: 0 }, "p": { group: 0, binding: 1 }, "state": { group: 0, binding: 2 } } }
bindingsMapping: { );
"pixelBuffer": { group: 0, binding: 0 },
"p": { group: 0, binding: 1 },
"state": { group: 0, binding: 2 }
}
});
csRender.setStorageBuffer("pixelBuffer", pixelBuffer); csRender.setStorageBuffer("pixelBuffer", pixelBuffer);
csRender.setStorageBuffer("p", paramsBuffer); csRender.setStorageBuffer("p", paramsBuffer);
csRender.setStorageBuffer("state", stateBuffer); csRender.setStorageBuffer("state", stateBuffer);
// Material Setup // --- 3. MATERIAL ---
const plane = scene.getMeshByName("plane"); const plane = scene.getMeshByName("plane");
if (plane?.material) { if (plane?.material) {
const mat = plane.material as any; const mat = plane.material as any;
mat.setStorageBuffer("pixelBuffer", pixelBuffer); mat.setStorageBuffer("pixelBuffer", pixelBuffer);
mat.setStorageBuffer("paramsBuffer", paramsBuffer); mat.setStorageBuffer("p", paramsBuffer);
} }
let time = 0; // --- 4. RENDER LOOP ---
const dt = 0.015;
scene.onBeforeRenderObservable.add(() => { scene.onBeforeRenderObservable.add(() => {
time += dt; this.simParams.time += this.simParams.dt;
const currentWidth = engine.getRenderWidth(); const currentWidth = engine.getRenderWidth();
const currentHeight = engine.getRenderHeight(); const currentHeight = engine.getRenderHeight();
// Fill parameter array (must match the exact order of the WGSL struct!)
paramsData[0] = currentWidth; paramsData[0] = currentWidth;
paramsData[1] = currentHeight; paramsData[1] = currentHeight;
paramsData[2] = time; paramsData[2] = this.simParams.time;
paramsData[3] = dt; paramsData[3] = this.simParams.dt;
paramsData[4] = 9.81; // g paramsData[4] = this.simParams.g;
paramsData[5] = 2.0; // m1 paramsData[5] = this.simParams.m1;
paramsData[6] = 1.0; // m2 paramsData[6] = this.simParams.m2;
paramsData[7] = 1.5; // l1 paramsData[7] = this.simParams.l1;
paramsData[8] = 1.2; // l2 paramsData[8] = this.simParams.l2;
paramsData[9] = 0.99; // damping paramsData[9] = this.simParams.damping;
paramsBuffer.update(paramsData); paramsBuffer.update(paramsData);
//dispatching physics // Trigger simulation and rendering
csPhysics.dispatch(1, 1, 1); csPhysics.dispatch(1, 1, 1);
//doing rendering const dispatchCount = Math.ceil((currentWidth * currentHeight) / 64);
const totalPixels = currentWidth * currentHeight;
const dispatchCount = Math.ceil(totalPixels / 64);
csRender.dispatch(dispatchCount, 1, 1); csRender.dispatch(dispatchCount, 1, 1);
}); });
} }

View File

@@ -1,7 +1,6 @@
//Simple Pass-Through Shader //Simple Pass-Through Shader
export const PENDULUM_VERTEX_SHADER_WGSL = ` export const PENDULUM_VERTEX_SHADER_WGSL = `
attribute position : vec3<f32>; attribute position : vec3<f32>;
attribute uv : vec2<f32>;
@vertex @vertex
fn main(input : VertexInputs) -> FragmentInputs { fn main(input : VertexInputs) -> FragmentInputs {
@@ -11,49 +10,62 @@ export const PENDULUM_VERTEX_SHADER_WGSL = `
} }
`; `;
// --- 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 //Fragment Shader to display the pixel buffer
export const PENDULUM_FRAGMENT_SHADER_WGSL = ` export const PENDULUM_FRAGMENT_SHADER_WGSL = SHARED_STRUCTS + `
var<storage, read> pixelBuffer : array<f32>; var<storage, read> pixelBuffer : array<f32>;
var<storage, read> paramsBuffer : array<f32>; var<storage, read> p : Params;
@fragment @fragment
fn main(input : FragmentInputs) -> FragmentOutputs { fn main(input : FragmentInputs) -> FragmentOutputs {
let width = u32(paramsBuffer[0]); let width = u32(p.width);
let height = u32(paramsBuffer[1]); let height = u32(p.height);
// Fallback if buffer is not loaded yet
if (width == 0u || height == 0u) { if (width == 0u || height == 0u) {
fragmentOutputs.color = vec4<f32>(0.5, 0.0, 0.0, 1.0); fragmentOutputs.color = vec4<f32>(0.5, 0.0, 0.0, 1.0);
return fragmentOutputs; return fragmentOutputs;
} }
// ============================================================== // Direct access to the pixel via screen coordinates
// 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!
// ==============================================================
let x = u32(input.position.x); let x = u32(input.position.x);
let y = u32(input.position.y); 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) { if (x >= width || y >= height) {
fragmentOutputs.color = vec4<f32>(1.0, 0.0, 0.0, 1.0); fragmentOutputs.color = vec4<f32>(0.0, 0.0, 0.0, 1.0);
return fragmentOutputs; return fragmentOutputs;
} }
let index = y * width + x; let index = y * width + x;
let val = pixelBuffer[index]; let val = pixelBuffer[index];
var color = vec3<f32>(0.1, 0.1, 0.15); var color = vec3<f32>(0.1, 0.1, 0.15); // Background
//pendulum color if (val > 0.1) { color = vec3<f32>(0.5, 0.5, 0.5); } // Line
if (val > 0.1) { if (val > 0.8) { color = vec3<f32>(1.0, 1.0, 1.0); } // Mass
color = vec3<f32>(0.5, 0.5, 0.5);
}
//mass color
if (val > 0.8) {
color = vec3<f32>(1.0, 1.0, 1.0);
}
fragmentOutputs.color = vec4<f32>(color, 1.0); fragmentOutputs.color = vec4<f32>(color, 1.0);
return fragmentOutputs; return fragmentOutputs;
@@ -62,16 +74,9 @@ export const PENDULUM_FRAGMENT_SHADER_WGSL = `
//Math for the double pendulum //Math for the double pendulum
//https://en.wikipedia.org/wiki/Double_pendulum //https://en.wikipedia.org/wiki/Double_pendulum
export const PENDULUM_PHYSIC_COMPUTE_SHADER_WGSL = ` export const PENDULUM_PHYSIC_COMPUTE_SHADER_WGSL = SHARED_STRUCTS + `
struct State {
theta1: f32,
theta2: f32,
v1: f32,
v2: f32
};
@group(0) @binding(0) var<storage, read_write> state : State; @group(0) @binding(0) var<storage, read_write> state : State;
@group(0) @binding(1) var<storage, read> p : array<f32>; @group(0) @binding(1) var<storage, read> p : Params;
@compute @workgroup_size(1) @compute @workgroup_size(1)
fn main() { fn main() {
@@ -80,49 +85,38 @@ export const PENDULUM_PHYSIC_COMPUTE_SHADER_WGSL = `
let v1 = state.v1; let v1 = state.v1;
let v2 = state.v2; let v2 = state.v2;
let dt = p[3]; let delta_t = t1 - t2;
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 num1 = -g * (2.0 * m1 + m2) * sin(t1) // Equations split for better readability
- m2 * g * sin(t1 - 2.0 * t2) let num1 = -p.g * (2.0 * p.m1 + p.m2) * sin(t1)
- 2.0 * sin(t1 - t2) * m2 * (v2 * v2 * l2 + v1 * v1 * l1 * cos(t1 - t2)); - p.m2 * p.g * sin(t1 - 2.0 * t2)
let den1 = l1 * (2.0 * m1 + m2 - m2 * cos(2.0 * 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 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 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 = l2 * (2.0 * m1 + m2 - m2 * cos(2.0 * t1 - 2.0 * t2)); let den2 = p.l2 * (2.0 * p.m1 + p.m2 - p.m2 * cos(2.0 * delta_t));
let a2 = num2 / den2; let a2 = num2 / den2;
let new_v1 = (v1 + a1 * dt) * damping; // Integration (Semi-Implicit Euler)
let new_v2 = (v2 + a2 * dt) * damping; let new_v1 = (v1 + a1 * p.dt) * p.damping;
let new_v2 = (v2 + a2 * p.dt) * p.damping;
state.v1 = new_v1; state.v1 = new_v1;
state.v2 = new_v2; state.v2 = new_v2;
state.theta1 = t1 + new_v1 * dt; state.theta1 = t1 + new_v1 * p.dt;
state.theta2 = t2 + new_v2 * dt; state.theta2 = t2 + new_v2 * p.dt;
} }
`; `;
//Pixel data to visualize the pendulum //Pixel data to visualize the pendulum
export const PENDULUM_RENDER_COMPUTE_SHADER_WGSL = ` export const PENDULUM_RENDER_COMPUTE_SHADER_WGSL = SHARED_STRUCTS + `
struct State {
theta1: f32,
theta2: f32,
v1: f32,
v2: f32
};
@group(0) @binding(0) var<storage, read_write> pixelBuffer : array<f32>; @group(0) @binding(0) var<storage, read_write> pixelBuffer : array<f32>;
@group(0) @binding(1) var<storage, read> p : array<f32>; @group(0) @binding(1) var<storage, read> p : Params;
@group(0) @binding(2) var<storage, read> state : State; @group(0) @binding(2) var<storage, read> state : State;
fn sdSegment(p: vec2<f32>, a: vec2<f32>, b: vec2<f32>) -> f32 { fn sdSegment(point: vec2<f32>, a: vec2<f32>, b: vec2<f32>) -> f32 {
let pa = p - a; let pa = point - a;
let ba = b - a; let ba = b - a;
let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
return length(pa - ba * h); return length(pa - ba * h);
@@ -131,49 +125,37 @@ export const PENDULUM_RENDER_COMPUTE_SHADER_WGSL = `
@compute @workgroup_size(64) @compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) global_id : vec3<u32>) { fn main(@builtin(global_invocation_id) global_id : vec3<u32>) {
let index = global_id.x; let index = global_id.x;
let width = u32(p.width);
let width = u32(p[0]); let height = u32(p.height);
let height = u32(p[1]);
if (index >= width * height) { return; } if (index >= width * height) { return; }
let x = f32(index % width); let x = f32(index % width);
let y = f32(index / width); let y = f32(index / width);
let uv = vec2<f32>(x / p[0], y / p[1]); let uv = vec2<f32>(x / p.width, y / p.height);
let aspect = p[0] / p[1]; let aspect = p.width / p.height;
let uv_corr = vec2<f32>(uv.x * aspect, uv.y); let uv_corr = vec2<f32>(uv.x * aspect, uv.y);
var newVal = 0.0; var newVal = 0.0; // Clear background
// Pendulum origin // Pendulum geometry
let origin = vec2<f32>(0.5 * aspect, 0.3); let origin = vec2<f32>(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 displayScale = 0.15;
let p1 = origin + vec2<f32>(s1, c1) * p[7] * displayScale; // Calculate positions
let p2 = p1 + vec2<f32>(s2, c2) * p[8] * displayScale; let p1 = origin + vec2<f32>(sin(state.theta1), cos(state.theta1)) * p.l1 * displayScale;
let p2 = p1 + vec2<f32>(sin(state.theta2), cos(state.theta2)) * p.l2 * displayScale;
// Distances
let dLine1 = sdSegment(uv_corr, origin, p1); let dLine1 = sdSegment(uv_corr, origin, p1);
let dLine2 = sdSegment(uv_corr, p1, p2); let dLine2 = sdSegment(uv_corr, p1, p2);
let dMass1 = length(uv_corr - p1); let dMass1 = length(uv_corr - p1);
let dMass2 = length(uv_corr - p2); let dMass2 = length(uv_corr - p2);
let lineThick = 0.003; // Draw
let m1Radius = 0.02; if (dLine1 < 0.003 || dLine2 < 0.003) { newVal = 0.5; }
let m2Radius = 0.02; if (dMass1 < 0.02 || dMass2 < 0.02) { newVal = 1.0; }
if (dLine1 < lineThick || dLine2 < lineThick) {
newVal = 0.5;
}
if (dMass1 < m1Radius || dMass2 < m2Radius) {
newVal = 1.0;
}
pixelBuffer[index] = newVal; pixelBuffer[index] = newVal;
} }