diff --git a/src/app/pages/algorithms/fractal3d/fractal3d.component.html b/src/app/pages/algorithms/fractal3d/fractal3d.component.html index 07ac16f..abb834c 100644 --- a/src/app/pages/algorithms/fractal3d/fractal3d.component.html +++ b/src/app/pages/algorithms/fractal3d/fractal3d.component.html @@ -11,8 +11,9 @@ -
- -
+ + diff --git a/src/app/pages/algorithms/fractal3d/fractal3d.component.ts b/src/app/pages/algorithms/fractal3d/fractal3d.component.ts index 3d2d84a..3927dbe 100644 --- a/src/app/pages/algorithms/fractal3d/fractal3d.component.ts +++ b/src/app/pages/algorithms/fractal3d/fractal3d.component.ts @@ -1,5 +1,5 @@ -import {AfterViewInit, Component, ElementRef, inject, NgZone, OnDestroy, ViewChild} from '@angular/core'; -import {ArcRotateCamera, Engine, MeshBuilder, Scene, ShaderMaterial, Vector2, Vector3} from '@babylonjs/core'; +import {Component} from '@angular/core'; +import {ArcRotateCamera, Camera, ShaderMaterial} from '@babylonjs/core'; import {MANDELBULB_FRAGMENT, MANDELBULB_VERTEX} from './fractal.shader'; import {Information} from '../information/information'; import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card'; @@ -7,6 +7,7 @@ import {TranslatePipe} from '@ngx-translate/core'; import {AlgorithmInformation} from '../information/information.models'; import {UrlConstants} from '../../../constants/UrlConstants'; import {MatButton} from '@angular/material/button'; +import {BabylonCanvas, RenderCallback, RenderConfig} from '../../../shared/rendering/canvas/babylon-canvas.component'; @Component({ selector: 'app-fractal3d', @@ -17,15 +18,13 @@ import {MatButton} from '@angular/material/button'; MatCardHeader, MatCardTitle, TranslatePipe, - MatButton + MatButton, + BabylonCanvas ], templateUrl: './fractal3d.component.html', styleUrl: './fractal3d.component.scss', }) -export class Fractal3dComponent implements AfterViewInit, OnDestroy { - readonly ngZone = inject(NgZone); - - @ViewChild('renderCanvas') canvasRef!: ElementRef; +export class Fractal3dComponent { algoInformation: AlgorithmInformation = { title: 'FRACTAL3D.EXPLANATION.TITLE', @@ -51,100 +50,35 @@ export class Fractal3dComponent implements AfterViewInit, OnDestroy { disclaimerListEntry: ['FRACTAL3D.EXPLANATION.DISCLAIMER_1', 'FRACTAL3D.EXPLANATION.DISCLAIMER_2', 'FRACTAL3D.EXPLANATION.DISCLAIMER_3', 'FRACTAL3D.EXPLANATION.DISCLAIMER_4'] }; + fractalConfig: RenderConfig = { + mode: '3D', + vertexShader: MANDELBULB_VERTEX, + fragmentShader: MANDELBULB_FRAGMENT, + uniformNames: ["power", "fractalType"] + }; + private readonly fractalPower = 8; - private engine!: Engine; - private scene!: Scene; - private shaderMaterial!: ShaderMaterial; private time = 0; - private triggerCamUpdate = false; - private cameraPosition: number = 3.5; + private oldType = 0; public currentFractalType = 0; - ngAfterViewInit(): void { - this.ngZone.runOutsideAngular(() => { - this.initBabylon(); - }); - } + onRender: RenderCallback = (material: ShaderMaterial, camera: Camera) => { + this.time += 0.005; - private initBabylon(): void { - const canvas = this.canvasRef.nativeElement; - this.engine = new Engine(canvas, true); - this.scene = new Scene(this.engine); + if (this.oldType != this.currentFractalType && camera instanceof ArcRotateCamera) { + this.oldType = this.currentFractalType; + camera.radius = this.currentFractalType == 1 ? 15 : 4; + } - const camera = new ArcRotateCamera("Camera", 0, Math.PI / 2, 4, Vector3.Zero(), this.scene); - camera.wheelPrecision = 100; - camera.minZ = 0.1; - camera.maxZ = 100; - camera.lowerRadiusLimit = 1.5; - camera.upperRadiusLimit = 20; - camera.attachControl(this.canvasRef.nativeElement, true); + material.setFloat("time", this.time); + material.setFloat("power", this.fractalPower); + material.setInt("fractalType", this.currentFractalType); + }; - canvas.addEventListener('wheel', (evt: WheelEvent) => { - evt.preventDefault(); - }, { passive: false }); - - const plane = MeshBuilder.CreatePlane("plane", { size: 10 }, this.scene); - plane.parent = camera; - plane.position.z = 1; - plane.alwaysSelectAsActiveMesh = true; - - this.shaderMaterial = new ShaderMaterial( - "mandelbulbShader", - this.scene, - { - vertexSource: MANDELBULB_VERTEX, - fragmentSource: MANDELBULB_FRAGMENT - }, - { - attributes: ["position", "uv"], - uniforms: ["time", "resolution", "cameraPosition", "targetPosition", "power", "fractalType"] - } - ); - this.shaderMaterial.disableDepthWrite = true; - this.shaderMaterial.backFaceCulling = false; - - plane.material = this.shaderMaterial; - - this.engine.runRenderLoop(() => { - this.time += 0.005; - - if (this.triggerCamUpdate) - { - this.triggerCamUpdate = false; - camera.radius = this.cameraPosition; - } - - if (this.shaderMaterial) { - this.shaderMaterial.setFloat("time", this.time); - this.shaderMaterial.setVector2("resolution", new Vector2(canvas.width, canvas.height)); - this.shaderMaterial.setVector3("cameraPosition", camera.position); - this.shaderMaterial.setVector3("targetPosition", camera.target); - this.shaderMaterial.setFloat("power", this.fractalPower); - this.shaderMaterial.setInt("fractalType", this.currentFractalType); - } - - this.scene.render(); - }); - - window.addEventListener('resize', () => this.engine.resize()); - } onFractalTypeChange(type: number): void { this.currentFractalType = type; - if (type === 0 ||type === 2) - { - this.cameraPosition = 4; - } - else { - this.cameraPosition = 15; - } - this.triggerCamUpdate = true; } - ngOnDestroy(): void { - if (this.engine) { - this.engine.dispose(); - } - } } diff --git a/src/app/shared/rendering/canvas/babylon-canvas.component.html b/src/app/shared/rendering/canvas/babylon-canvas.component.html new file mode 100644 index 0000000..ea533fa --- /dev/null +++ b/src/app/shared/rendering/canvas/babylon-canvas.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/app/shared/rendering/canvas/babylon-canvas.component.scss b/src/app/shared/rendering/canvas/babylon-canvas.component.scss new file mode 100644 index 0000000..daadde6 --- /dev/null +++ b/src/app/shared/rendering/canvas/babylon-canvas.component.scss @@ -0,0 +1,2 @@ +.canvas-container { width: 100%; height: 1000px; } +canvas { width: 100%; height: 100%; touch-action: none; border-width: 0; border-color: transparent; border-style: hidden; } diff --git a/src/app/shared/rendering/canvas/babylon-canvas.component.ts b/src/app/shared/rendering/canvas/babylon-canvas.component.ts new file mode 100644 index 0000000..92802bb --- /dev/null +++ b/src/app/shared/rendering/canvas/babylon-canvas.component.ts @@ -0,0 +1,154 @@ +import {AfterViewInit, Component, ElementRef, inject, Input, NgZone, OnDestroy, ViewChild} from '@angular/core'; +import {ArcRotateCamera, Camera, Engine, MeshBuilder, Scene, ShaderMaterial, Vector2, Vector3} from '@babylonjs/core'; + +export interface RenderConfig { + mode: '2D' | '3D'; + vertexShader: string; + fragmentShader: string; + uniformNames: string[]; +} + +export type RenderCallback = (material: ShaderMaterial, camera: Camera, canvas: HTMLCanvasElement, scene: Scene) => void; + +@Component({ + selector: 'app-babylon-canvas', + imports: [], + templateUrl: './babylon-canvas.component.html', + 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; + + private engine!: Engine; + private scene!: Scene; + private shaderMaterial!: ShaderMaterial; + private camera!: Camera; + + ngAfterViewInit(): void { + this.ngZone.runOutsideAngular(() => { + this.initBabylon(); + }); + } + + /*ngOnChanges(changes: SimpleChanges): void { + //if something changes during runtime, new materials are necessary ans needs maybe build here + }*/ + + ngOnDestroy(): void { + const canvas = this.canvasRef?.nativeElement; + if (canvas) { + //remove listener if needed + } + if (this.engine) { + this.engine.dispose(); + } + } + + private initBabylon(): void { + const canvas = this.canvasRef.nativeElement; + this.engine = new Engine(canvas, true); + this.scene = new Scene(this.engine); + this.setupCamera(canvas); + canvas.addEventListener('wheel', (evt: WheelEvent) => evt.preventDefault(), { passive: false }); + this.createShaderMaterial(); + this.createFullScreenRect(); + this.addRenderLoop(canvas); + this.addResizeHandler(); + } + + private setupCamera(canvas: HTMLCanvasElement) { + if (this.config.mode === '3D') { + this.setup3dCamera(canvas); + return; + } + + this.setup2dCamera(canvas); + } + + private setup2dCamera(canvas: HTMLCanvasElement) { + const cam = new ArcRotateCamera("Camera2D", -Math.PI / 2, Math.PI / 2, 10, Vector3.Zero(), this.scene); + cam.mode = Camera.ORTHOGRAPHIC_CAMERA; + + const aspect = canvas.width / canvas.height; + const viewSize = 10; + cam.orthoLeft = -viewSize * aspect / 2; + cam.orthoRight = viewSize * aspect / 2; + cam.orthoTop = viewSize / 2; + cam.orthoBottom = -viewSize / 2; + + cam.attachControl(canvas, true, false); + this.camera = cam; + } + + private setup3dCamera(canvas: HTMLCanvasElement) { + const cam = new ArcRotateCamera("Camera", 0, Math.PI / 2, 4, Vector3.Zero(), this.scene); + cam.wheelPrecision = 100; + cam.minZ = 0.1; + cam.maxZ = 100; + cam.lowerRadiusLimit = 1.5; + cam.upperRadiusLimit = 20; + cam.attachControl(canvas, true); + this.camera = cam; + } + + private createFullScreenRect() { + const plane = MeshBuilder.CreatePlane("plane", {size: 100}, this.scene); + + if (this.config.mode === '3D') { + plane.parent = this.camera; + plane.position.z = 1; + } else { + plane.lookAt(this.camera.position); + } + plane.alwaysSelectAsActiveMesh = true; + + plane.material = this.shaderMaterial; + } + + private createShaderMaterial() { + this.shaderMaterial = new ShaderMaterial( + "shaderMaterial", + this.scene, + { + vertexSource: this.config.vertexShader, + fragmentSource: this.config.fragmentShader + }, + { + attributes: ["position", "uv"], + uniforms: ["time", "resolution", "cameraPosition", "targetPosition", ...this.config.uniformNames] + } + ); + this.shaderMaterial.disableDepthWrite = true; + this.shaderMaterial.backFaceCulling = false; + } + + private addRenderLoop(canvas: HTMLCanvasElement) { + this.engine.runRenderLoop(() => { + + // callback call to call specific uniforms + if (this.renderCallback) { + 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); + + this.scene.render(); + }); + } + + private addResizeHandler() { + window.addEventListener('resize', () => { + this.engine.resize(); + if (this.config.mode === '2D' && this.camera instanceof ArcRotateCamera && this.camera.mode === Camera.ORTHOGRAPHIC_CAMERA) { + //maybe update the aspect ratio here + } + }); + } +}