From 38e256544fb5e6857b56306bf756b9e117054d93 Mon Sep 17 00:00:00 2001 From: Lobo Date: Sat, 14 Feb 2026 11:42:00 +0100 Subject: [PATCH] No failing webGPU version found --- .../algorithms/fractal/fractal.component.ts | 7 +- .../fractal3d/fractal3d.component.ts | 1 + .../path-tracing/path-tracing-shader.ts | 137 ++++++++++++++++++ .../path-tracing/path-tracing.component.html | 2 +- .../path-tracing/path-tracing.component.ts | 88 ++++++++--- .../path-tracing/path-tracing.shader.ts | 10 -- .../canvas/babylon-canvas.component.ts | 61 +++++--- 7 files changed, 255 insertions(+), 51 deletions(-) create mode 100644 src/app/pages/algorithms/path-tracing/path-tracing-shader.ts delete mode 100644 src/app/pages/algorithms/path-tracing/path-tracing.shader.ts diff --git a/src/app/pages/algorithms/fractal/fractal.component.ts b/src/app/pages/algorithms/fractal/fractal.component.ts index d7aed47..276e99f 100644 --- a/src/app/pages/algorithms/fractal/fractal.component.ts +++ b/src/app/pages/algorithms/fractal/fractal.component.ts @@ -10,7 +10,7 @@ import {UrlConstants} from '../../../constants/UrlConstants'; import {FormsModule} from '@angular/forms'; import {BabylonCanvas, RenderCallback, RenderConfig} 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 {PointerEventTypes, PointerInfo, Scene, ShaderMaterial, Vector2, WebGPUEngine} from '@babylonjs/core'; import {MatButton} from '@angular/material/button'; import {MatIcon} from '@angular/material/icon'; import {NgxSliderModule, Options} from '@angular-slider/ngx-slider'; @@ -74,6 +74,7 @@ export class FractalComponent implements OnInit { renderConfig: RenderConfig = { mode: '2D', + pipeline: 'Material', initialViewSize: 100, vertexShader: FRACTAL2D_VERTEX, fragmentShader: FRACTAL2D_FRAGMENT, @@ -155,8 +156,8 @@ export class FractalComponent implements OnInit { } } - onSceneReady(scene: Scene): void { - scene.onPointerObservable.add((pointerInfo) => { + onSceneReady(options: {scene: Scene, engine: WebGPUEngine}): void { + options.scene.onPointerObservable.add((pointerInfo) => { switch (pointerInfo.type) { case PointerEventTypes.POINTERDOWN: diff --git a/src/app/pages/algorithms/fractal3d/fractal3d.component.ts b/src/app/pages/algorithms/fractal3d/fractal3d.component.ts index b65351b..32c677c 100644 --- a/src/app/pages/algorithms/fractal3d/fractal3d.component.ts +++ b/src/app/pages/algorithms/fractal3d/fractal3d.component.ts @@ -52,6 +52,7 @@ export class Fractal3dComponent { fractalConfig: RenderConfig = { mode: '3D', + pipeline: 'Material', initialViewSize: 4, vertexShader: MANDELBULB_VERTEX, fragmentShader: MANDELBULB_FRAGMENT, diff --git a/src/app/pages/algorithms/path-tracing/path-tracing-shader.ts b/src/app/pages/algorithms/path-tracing/path-tracing-shader.ts new file mode 100644 index 0000000..998ce51 --- /dev/null +++ b/src/app/pages/algorithms/path-tracing/path-tracing-shader.ts @@ -0,0 +1,137 @@ +export const PATH_TRACING_SHADER = ` +struct Camera { + position: vec4, + forward: vec4, + right: vec4, + up: vec4 +}; + +struct SceneParams { + values: vec4 +}; + +struct Sphere { + center: vec3, + radius: f32, + color: vec3, + emission: vec3 +}; + +@group(0) @binding(0) var outputTex : texture_storage_2d; +// Uniform -> Storage (read) +@group(0) @binding(1) var cam : Camera; +// Uniform -> Storage (read) +@group(0) @binding(2) var params : SceneParams; + +fn getSceneSphere(i: i32) -> Sphere { + var s: Sphere; + s.emission = vec3(0.0); + + if (i == 0) { s.center = vec3(-100.5, 0.0, 0.0); s.radius = 100.0; s.color = vec3(0.8, 0.1, 0.1); } + else if (i == 1) { s.center = vec3( 100.5, 0.0, 0.0); s.radius = 100.0; s.color = vec3(0.1, 0.8, 0.1); } + else if (i == 2) { s.center = vec3(0.0, 100.5, 0.0); s.radius = 100.0; s.color = vec3(0.8, 0.8, 0.8); } + else if (i == 3) { s.center = vec3(0.0, -100.5, 0.0); s.radius = 100.0; s.color = vec3(0.8, 0.8, 0.8); } + else if (i == 4) { s.center = vec3(0.0, 0.0, 100.5); s.radius = 100.0; s.color = vec3(0.8, 0.8, 0.8); } + else if (i == 5) { s.center = vec3(0.0, 1.5, 0.0); s.radius = 0.3; s.color = vec3(1.0); s.emission = vec3(15.0); } + else if (i == 6) { s.center = vec3(-0.3, -0.3, -0.3); s.radius = 0.25; s.color = vec3(0.9, 0.9, 0.1); } + else { s.center = vec3(0.3, -0.3, 0.2); s.radius = 0.25; s.color = vec3(0.2, 0.2, 0.9); } + + return s; +} + +fn hitSphere(ro: vec3, rd: vec3, s: Sphere) -> f32 { + let oc = ro - s.center; + let b = dot(oc, rd); + let c = dot(oc, oc) - s.radius * s.radius; + let h = b*b - c; + if (h < 0.0) { return -1.0; } + return -b - sqrt(h); +} + +fn rand(seed: ptr) -> f32 { + *seed = *seed * 747796405u + 2891336453u; + let word = ((*seed >> ((*seed >> 28u) + 4u)) ^ *seed) * 277803737u; + return f32((word >> 22u) ^ word) / 4294967296.0; +} + +fn randomHemisphereDir(normal: vec3, seed: ptr) -> vec3 { + let r1 = rand(seed); + let r2 = rand(seed); + let theta = 6.283185 * r1; + let phi = acos(2.0 * r2 - 1.0); + let x = sin(phi) * cos(theta); + let y = sin(phi) * sin(theta); + let z = cos(phi); + let v = normalize(vec3(x, y, z)); + if (dot(v, normal) < 0.0) { return -v; } + return v; +} + +@compute @workgroup_size(8, 8, 1) +fn main(@builtin(global_invocation_id) global_id : vec3) { + let dims = textureDimensions(outputTex); + let coord = vec2(global_id.xy); + + if (coord.x >= i32(dims.x) || coord.y >= i32(dims.y)) { return; } + + let uv = (vec2(coord) / vec2(dims)) * 2.0 - 1.0; + let aspect = f32(dims.x) / f32(dims.y); + let screenPos = vec2(uv.x * aspect, -uv.y); + + var ro = cam.position.xyz; + var rd = normalize(cam.forward.xyz + cam.right.xyz * screenPos.x + cam.up.xyz * screenPos.y); + + var col = vec3(0.0); + var throughput = vec3(1.0); + // Zugriff auf params.values statt params.x + var seed = u32(global_id.x + global_id.y * dims.x) + u32(params.values.x) * 719393u; + + for (var i = 0; i < 4; i++) { + var tMin = 10000.0; + var hitIndex = -1; + + for (var j = 0; j < 8; j++) { + let s = getSceneSphere(j); + let t = hitSphere(ro, rd, s); + if (t > 0.001 && t < tMin) { + tMin = t; + hitIndex = j; + } + } + + if (hitIndex == -1) { + col = col + throughput * vec3(0.1, 0.1, 0.15); + break; + } + + let hitSphere = getSceneSphere(hitIndex); + let hitPos = ro + rd * tMin; + let normal = normalize(hitPos - hitSphere.center); + + if (length(hitSphere.emission) > 0.0) { + col = col + throughput * hitSphere.emission; + break; + } + + throughput = throughput * hitSphere.color; + ro = hitPos; + rd = randomHemisphereDir(normal, &seed); + } + + // Debug: Falls Bild immer noch schwarz, entkommentieren: + // textureStore(outputTex, coord, vec4(1.0, 0.0, 0.0, 1.0)); + + textureStore(outputTex, coord, vec4(col, 1.0)); +} +`; + + +export const RED_SHADER = ` +@group(0) @binding(0) var outputTex : texture_storage_2d; + +@compute @workgroup_size(1, 1, 1) +fn main(@builtin(global_invocation_id) global_id : vec3) { + // Schreibe Rot (R=1, G=0, B=0, A=1) an die Pixel-Position + textureStore(outputTex, global_id.xy, vec4(1.0, 0.0, 0.0, 1.0)); +} +`; diff --git a/src/app/pages/algorithms/path-tracing/path-tracing.component.html b/src/app/pages/algorithms/path-tracing/path-tracing.component.html index 346f8f4..d3be535 100644 --- a/src/app/pages/algorithms/path-tracing/path-tracing.component.html +++ b/src/app/pages/algorithms/path-tracing/path-tracing.component.html @@ -6,7 +6,7 @@ diff --git a/src/app/pages/algorithms/path-tracing/path-tracing.component.ts b/src/app/pages/algorithms/path-tracing/path-tracing.component.ts index 9e978bf..303a457 100644 --- a/src/app/pages/algorithms/path-tracing/path-tracing.component.ts +++ b/src/app/pages/algorithms/path-tracing/path-tracing.component.ts @@ -1,10 +1,17 @@ import { Component } from '@angular/core'; -import {BabylonCanvas, RenderCallback, RenderConfig} from '../../../shared/rendering/canvas/babylon-canvas.component'; -import {Information} from '../information/information'; -import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card'; -import {TranslatePipe} from '@ngx-translate/core'; -import {AlgorithmInformation} from '../information/information.models'; -import {PATH_TRACING_VERTEX_SHADER} from './path-tracing.shader'; +import { BabylonCanvas, RenderConfig } from '../../../shared/rendering/canvas/babylon-canvas.component'; +import { Information } from '../information/information'; +import { MatCard, MatCardContent, MatCardHeader, MatCardTitle } from '@angular/material/card'; +import { TranslatePipe } from '@ngx-translate/core'; +import { AlgorithmInformation } from '../information/information.models'; +import { + ComputeShader, + Layer, + RawTexture, + Scene, + WebGPUEngine, + Constants +} from '@babylonjs/core'; @Component({ selector: 'app-path-tracing', @@ -23,26 +30,73 @@ import {PATH_TRACING_VERTEX_SHADER} from './path-tracing.shader'; export class PathTracingComponent { algoInformation: AlgorithmInformation = { - title: '', - entries: [ - ], + title: 'WebGPU Debug', + entries: [], disclaimer: '', disclaimerBottom: '', - disclaimerListEntry: [''] + disclaimerListEntry: [] }; fractalConfig: RenderConfig = { mode: '3D', + pipeline: 'Compute', initialViewSize: 4, - vertexShader: PATH_TRACING_VERTEX_SHADER, - fragmentShader: '', - uniformNames: ["time", "power", "fractalType"] }; - private time = 0; + private cs!: ComputeShader; + private texture!: RawTexture; - onRender: RenderCallback = () => { - this.time += 0.005; - }; + onSceneReady(payload: { scene: Scene, engine: WebGPUEngine }) { + const { scene, engine } = payload; + const canvas = engine.getRenderingCanvas()!; + const width = 512; + const height = 512; + // 1. Textur erstellen (Storage) + this.texture = new RawTexture( + new Uint8Array(width * height * 4), + width, + height, + Constants.TEXTUREFORMAT_RGBA, + scene, + false, + false, + Constants.TEXTURE_NEAREST_SAMPLINGMODE, + Constants.TEXTURETYPE_UNSIGNED_BYTE, + Constants.TEXTURE_CREATIONFLAG_STORAGE + ); + + // 2. Minimal-Shader + const shaderCode = ` + @group(0) @binding(0) var outputTex : texture_storage_2d; + @compute @workgroup_size(8, 8, 1) + fn main(@builtin(global_invocation_id) gid : vec3) { + textureStore(outputTex, gid.xy, vec4(0.0, 1.0, 0.0, 1.0)); + } + `; + + // 3. Shader erstellen + this.cs = new ComputeShader( + "simple", + engine, + { computeSource: shaderCode }, + { + bindingsMapping: { "outputTex": { group: 0, binding: 0 } }, + entryPoint: "main" + } + ); + + this.cs.setTexture("outputTex", this.texture); + + // 4. Layer + const layer = new Layer("viewLayer", null, scene); + layer.texture = this.texture; + + // 5. Der Trick: Einmaliger Dispatch nach einer kurzen Pause + // Das umgeht alle "isReady" oder Binding-Timing Probleme + setTimeout(() => { + console.log("Forcing Compute Dispatch..."); + this.cs.dispatch(width / 8, height / 8, 1); + }, 200); + } } diff --git a/src/app/pages/algorithms/path-tracing/path-tracing.shader.ts b/src/app/pages/algorithms/path-tracing/path-tracing.shader.ts deleted file mode 100644 index 5bd86e2..0000000 --- a/src/app/pages/algorithms/path-tracing/path-tracing.shader.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const PATH_TRACING_VERTEX_SHADER = /* glsl */` - attribute vec3 position; - attribute vec2 uv; - varying vec2 vUV; - - void main(void) { - gl_Position = vec4(position, 1.0); - vUV = uv; - } -`; diff --git a/src/app/shared/rendering/canvas/babylon-canvas.component.ts b/src/app/shared/rendering/canvas/babylon-canvas.component.ts index 55d3a79..8af4741 100644 --- a/src/app/shared/rendering/canvas/babylon-canvas.component.ts +++ b/src/app/shared/rendering/canvas/babylon-canvas.component.ts @@ -1,12 +1,13 @@ -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 {AfterViewInit, Component, ElementRef, EventEmitter, Input, OnDestroy, Output, ViewChild} from '@angular/core'; +import {ArcRotateCamera, Camera, MeshBuilder, Scene, ShaderMaterial, Vector2, Vector3, WebGPUEngine} from '@babylonjs/core'; export interface RenderConfig { mode: '2D' | '3D'; + pipeline: 'Material' | 'Compute'; initialViewSize: number; - vertexShader: string; - fragmentShader: string; - uniformNames: string[]; + vertexShader?: string; + fragmentShader?: string; + uniformNames?: string[]; } export type RenderCallback = (material: ShaderMaterial, camera: Camera, canvas: HTMLCanvasElement, scene: Scene) => void; @@ -18,24 +19,21 @@ export type RenderCallback = (material: ShaderMaterial, camera: Camera, canvas: styleUrl: './babylon-canvas.component.scss', }) export class BabylonCanvas implements AfterViewInit, OnDestroy { - readonly ngZone = inject(NgZone); @ViewChild('renderCanvas', { static: true }) canvasRef!: ElementRef; @Input({ required: true }) config!: RenderConfig; @Input() renderCallback?: RenderCallback; - @Output() sceneReady = new EventEmitter(); + @Output() sceneReady = new EventEmitter<{scene: Scene, engine: WebGPUEngine}>(); - private engine!: Engine; + private engine!: WebGPUEngine; private scene!: Scene; private shaderMaterial!: ShaderMaterial; private camera!: Camera; ngAfterViewInit(): void { - this.ngZone.runOutsideAngular(() => { - this.initBabylon(); - }); + this.initBabylon().then(r => console.log("Rendering engine initialized.")); } /*ngOnChanges(changes: SimpleChanges): void { @@ -52,15 +50,27 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy { } } - private initBabylon(): void { + async initBabylon(): Promise { + if (!navigator.gpu) { + alert("Your browser does not support webgpu, maybe you have activate the hardware acceleration.!"); + return; + } + const canvas = this.canvasRef.nativeElement; - this.engine = new Engine(canvas, true); + this.engine = new WebGPUEngine(canvas, { + antialias: true + }); + await this.engine.initAsync(); + + this.scene = new Scene(this.engine); this.setupCamera(canvas); canvas.addEventListener('wheel', (evt: WheelEvent) => evt.preventDefault(), { passive: false }); - this.createShaderMaterial(); - this.createFullScreenRect(); - this.sceneReady.emit(this.scene); + if (this.config.pipeline !== 'Compute') { + this.createShaderMaterial(); + this.createFullScreenRect(); + } + this.sceneReady.emit({ scene: this.scene, engine: this.engine }); this.addRenderLoop(canvas); this.addResizeHandler(); } @@ -102,6 +112,8 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy { } private createFullScreenRect() { + if (!this.shaderMaterial) return; + const plane = MeshBuilder.CreatePlane("plane", {size: 110}, this.scene); if (this.config.mode === '3D') { @@ -116,6 +128,13 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy { } private createShaderMaterial() { + + if (!this.config.vertexShader || !this.config.fragmentShader || !this.config.uniformNames) + { + console.warn("Bablyon canvas needs a vertex shader, a fragment shader and a uniforms array.\n"); + return; + } + this.shaderMaterial = new ShaderMaterial( "shaderMaterial", this.scene, @@ -135,14 +154,16 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy { private addRenderLoop(canvas: HTMLCanvasElement) { this.engine.runRenderLoop(() => { - // callback call to call specific uniforms if (this.renderCallback) { + // callback call to call specific uniforms this.renderCallback(this.shaderMaterial, this.camera, canvas, this.scene); } - // default uniforms which maybe each scene has - this.shaderMaterial.setVector2("resolution", new Vector2(canvas.width, canvas.height)); - this.shaderMaterial.setVector3("cameraPosition", this.camera.position); + if (this.shaderMaterial) { + // default uniforms which maybe each material scene has + this.shaderMaterial.setVector2("resolution", new Vector2(canvas.width, canvas.height)); + this.shaderMaterial.setVector3("cameraPosition", this.camera.position); + } this.scene.render(); });