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
+ }
+ });
+ }
+}