diff --git a/package-lock.json b/package-lock.json index 5e8f88c..9458617 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,9 +17,9 @@ "@angular/material": "~21.1.0", "@angular/platform-browser": "~21.1.0", "@angular/router": "~21.1.0", + "@babylonjs/core": "^8.50.5", "@ngx-translate/core": "~17.0.0", "@ngx-translate/http-loader": "~17.0.0", - "babylonjs": "^8.50.5", "inquirer": "^13.2.2", "rxjs": "~7.8.2", "swiper": "~12.1.0", @@ -1150,6 +1150,12 @@ "node": ">=6.9.0" } }, + "node_modules/@babylonjs/core": { + "version": "8.50.5", + "resolved": "https://registry.npmjs.org/@babylonjs/core/-/core-8.50.5.tgz", + "integrity": "sha512-rrHAYJmsZOuvC6wdNhGNgdn9JlMxBhk30YhBWGPSxYnnUD7/n/U9Pb3a8k0lpTHipbCfjJ+J5r6GdXtPEikhEg==", + "license": "Apache-2.0" + }, "node_modules/@emnapi/core": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", @@ -6449,13 +6455,6 @@ } } }, - "node_modules/babylonjs": { - "version": "8.50.5", - "resolved": "https://registry.npmjs.org/babylonjs/-/babylonjs-8.50.5.tgz", - "integrity": "sha512-r4CvDBY798k0E8FqOrj2pg5If7MJhELACIpQw8r1TT4TXNWNXxlqrNmKtSzxauWFQgankwVbKhe41YA8k+tQMw==", - "hasInstallScript": true, - "license": "Apache-2.0" - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", diff --git a/package.json b/package.json index 50fb22b..9bac666 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,9 @@ "@angular/material": "~21.1.0", "@angular/platform-browser": "~21.1.0", "@angular/router": "~21.1.0", + "@babylonjs/core": "^8.50.5", "@ngx-translate/core": "~17.0.0", "@ngx-translate/http-loader": "~17.0.0", - "babylonjs": "^8.50.5", "inquirer": "^13.2.2", "rxjs": "~7.8.2", "swiper": "~12.1.0", diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 0ad4a7e..c8960c9 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -12,6 +12,7 @@ export const routes: Routes = [ { path: RouterConstants.IMPRINT.PATH, component: RouterConstants.IMPRINT.COMPONENT}, { path: RouterConstants.GOL.PATH, component: RouterConstants.GOL.COMPONENT}, { path: RouterConstants.LABYRINTH.PATH, component: RouterConstants.LABYRINTH.COMPONENT}, - { path: RouterConstants.FRACTAL.PATH, component: RouterConstants.FRACTAL.COMPONENT} + { path: RouterConstants.FRACTAL.PATH, component: RouterConstants.FRACTAL.COMPONENT}, + { path: RouterConstants.FRACTAL3d.PATH, component: RouterConstants.FRACTAL3d.COMPONENT} ]; diff --git a/src/app/constants/RouterConstants.ts b/src/app/constants/RouterConstants.ts index b60e8fa..6b9b9d9 100644 --- a/src/app/constants/RouterConstants.ts +++ b/src/app/constants/RouterConstants.ts @@ -7,6 +7,7 @@ import {SortingComponent} from '../pages/algorithms/sorting/sorting.component'; import {ConwayGolComponent} from '../pages/algorithms/conway-gol/conway-gol.component'; import {LabyrinthComponent} from '../pages/algorithms/pathfinding/labyrinth/labyrinth.component'; import {FractalComponent} from '../pages/algorithms/fractal/fractal.component'; +import {Fractal3dComponent} from '../pages/algorithms/fractal3d/fractal3d.component'; export class RouterConstants { @@ -58,6 +59,12 @@ export class RouterConstants { COMPONENT: FractalComponent }; + static readonly FRACTAL3d = { + PATH: 'algorithms/fractal3d', + LINK: '/algorithms/fractal3d', + COMPONENT: Fractal3dComponent + }; + static readonly IMPRINT = { PATH: 'imprint', LINK: '/imprint', diff --git a/src/app/pages/algorithms/fractal3d/fraktal-shader.model.ts b/src/app/pages/algorithms/fractal3d/fractal.shader.ts similarity index 70% rename from src/app/pages/algorithms/fractal3d/fraktal-shader.model.ts rename to src/app/pages/algorithms/fractal3d/fractal.shader.ts index 944edb1..d52630e 100644 --- a/src/app/pages/algorithms/fractal3d/fraktal-shader.model.ts +++ b/src/app/pages/algorithms/fractal3d/fractal.shader.ts @@ -1,14 +1,25 @@ -const mandelbulbFragmentShader = ` -precision highp float; +export const MANDELBULB_VERTEX = /* glsl */` + precision highp float; + attribute vec3 position; + attribute vec2 uv; + varying vec2 vUV; -// Uniforms (Werte von der CPU) + void main(void) { + gl_Position = vec4(position, 1.0); + vUV = uv; + } +`; + +export const MANDELBULB_FRAGMENT = /* glsl */` + precision highp float; + +// Uniforms uniform float time; uniform vec2 resolution; uniform vec3 cameraPosition; uniform vec3 targetPosition; -uniform float power; // Die "Potenz" des Fraktals (z.B. 8.0) +uniform float power; -// Hilfsfunktion: Rotation mat2 rot(float a) { float s = sin(a), c = cos(a); return mat2(c, -s, s, c); @@ -20,26 +31,21 @@ float map(vec3 pos) { float dr = 1.0; float r = 0.0; - // Die Fraktal-Iteration for (int i = 0; i < 8; i++) { r = length(z); if (r > 2.0) break; - // Konvertierung in Polarkoordinaten float theta = acos(z.y / r); float phi = atan(z.z, z.x); dr = pow(r, power - 1.0) * power * dr + 1.0; - // Skalieren und Rotieren (Power) float zr = pow(r, power); theta = theta * power; phi = phi * power; - // Zurück zu Kartesischen Koordinaten z = zr * vec3(sin(theta) * cos(phi), cos(theta), sin(theta) * sin(phi)); - // Den ursprünglichen Punkt addieren (wie beim Mandelbrot z = z^2 + c) z += pos; } return 0.5 * log(r) * r / dr; @@ -51,14 +57,13 @@ float raymarch(vec3 ro, vec3 rd) { for(int i = 0; i < 100; i++) { vec3 pos = ro + t * rd; float d = map(pos); - if(d < 0.001) return t; // Getroffen! - if(t > 10.0) break; // Zu weit weg + if(d < 0.001) return t; + if(t > 10.0) break; t += d; } - return -1.0; // Nichts getroffen + return -1.0; } -// --- Normalenberechnung für Licht --- vec3 getNormal(vec3 p) { float d = map(p); vec2 e = vec2(0.001, 0.0); @@ -70,10 +75,8 @@ vec3 getNormal(vec3 p) { } void main(void) { - // UV Koordinaten zentrieren (-1 bis 1) und Aspekt korrigieren vec2 uv = (gl_FragCoord.xy - 0.5 * resolution.xy) / resolution.y; - // Kamera Setup (LookAt) vec3 ro = cameraPosition; // Ray Origin vec3 ta = targetPosition; // Target LookAt @@ -83,21 +86,19 @@ void main(void) { vec3 rd = normalize(fwd + uv.x * right + uv.y * up); // Ray Direction - // Rendern float t = raymarch(ro, rd); - vec3 color = vec3(0.0); // Hintergrund Schwarz + vec3 color = vec3(0.0); if(t > 0.0) { vec3 pos = ro + t * rd; vec3 nor = getNormal(pos); - // Einfaches Licht (Phong) + //easy phong light vec3 lightDir = normalize(vec3(1.0, 1.0, -1.0)); float diff = max(dot(nor, lightDir), 0.0); float amb = 0.2; // Ambient - // Farbe basierend auf Position (gibt diesen Alien-Look) vec3 baseColor = vec3(0.5) + 0.5 * cos(vec3(0.0, 0.4, 0.8) + length(pos) * 2.0); color = baseColor * (diff + amb); diff --git a/src/app/pages/algorithms/fractal3d/fractal3d.component.html b/src/app/pages/algorithms/fractal3d/fractal3d.component.html index 7d2e388..ea533fa 100644 --- a/src/app/pages/algorithms/fractal3d/fractal3d.component.html +++ b/src/app/pages/algorithms/fractal3d/fractal3d.component.html @@ -1 +1,3 @@ -

fractal3d works!

+
+ +
diff --git a/src/app/pages/algorithms/fractal3d/fractal3d.component.scss b/src/app/pages/algorithms/fractal3d/fractal3d.component.scss index e69de29..ffc6ccf 100644 --- a/src/app/pages/algorithms/fractal3d/fractal3d.component.scss +++ b/src/app/pages/algorithms/fractal3d/fractal3d.component.scss @@ -0,0 +1,2 @@ +.canvas-container { width: 100%; height: 600px; overflow: hidden; } +canvas { width: 100%; height: 100%; touch-action: none; } diff --git a/src/app/pages/algorithms/fractal3d/fractal3d.component.ts b/src/app/pages/algorithms/fractal3d/fractal3d.component.ts index 4d84a37..ffa9edc 100644 --- a/src/app/pages/algorithms/fractal3d/fractal3d.component.ts +++ b/src/app/pages/algorithms/fractal3d/fractal3d.component.ts @@ -1,4 +1,6 @@ -import { Component } from '@angular/core'; +import {AfterViewInit, Component, ElementRef, inject, NgZone, OnDestroy, ViewChild} from '@angular/core'; +import {Engine, FreeCamera, MeshBuilder, Scene, ShaderMaterial, Vector2, Vector3} from '@babylonjs/core'; +import {MANDELBULB_FRAGMENT, MANDELBULB_VERTEX} from './fractal.shader'; @Component({ selector: 'app-fractal3d', @@ -6,6 +8,78 @@ import { Component } from '@angular/core'; templateUrl: './fractal3d.component.html', styleUrl: './fractal3d.component.scss', }) -export class Fractal3dComponent { +export class Fractal3dComponent implements AfterViewInit, OnDestroy { + readonly ngZone = inject(NgZone); + @ViewChild('renderCanvas') canvasRef!: ElementRef; + + private engine!: Engine; + private scene!: Scene; + private shaderMaterial!: ShaderMaterial; + + private cameraPos = new Vector3(0, 0, -3.5); + private targetPos = new Vector3(0, 0, 0); + private fractalPower = 8.0; + private time = 0; + + ngAfterViewInit(): void { + this.ngZone.runOutsideAngular(() => { + this.initBabylon(); + }); + } + + private initBabylon(): void { + const canvas = this.canvasRef.nativeElement; + this.engine = new Engine(canvas, true); + this.scene = new Scene(this.engine); + + const camera = new FreeCamera("camera1", new Vector3(0, 0, -1), this.scene); + camera.setTarget(Vector3.Zero()); + + const plane = MeshBuilder.CreatePlane("plane", { size: 2 }, this.scene); + + this.shaderMaterial = new ShaderMaterial( + "mandelbulbShader", + this.scene, + { + vertexSource: MANDELBULB_VERTEX, + fragmentSource: MANDELBULB_FRAGMENT + }, + { + attributes: ["position", "uv"], + uniforms: ["time", "resolution", "cameraPosition", "targetPosition", "power"] + } + ); + + plane.material = this.shaderMaterial; + + this.engine.runRenderLoop(() => { + this.time += 0.01; + this.updateShaderUniforms(canvas); + this.scene.render(); + }); + + window.addEventListener('resize', () => this.engine.resize()); + } + + private updateShaderUniforms(canvas: HTMLCanvasElement): void { + if (!this.shaderMaterial) return; + + this.shaderMaterial.setFloat("time", this.time); + this.shaderMaterial.setVector2("resolution", new Vector2(canvas.width, canvas.height)); + this.shaderMaterial.setVector3("cameraPosition", this.cameraPos); + this.shaderMaterial.setVector3("targetPosition", this.targetPos); + this.shaderMaterial.setFloat("power", this.fractalPower); + + // Optional: Hier könnte man cameraPos basierend auf Mausbewegung ändern (Orbiting) + // z.B.: + // this.cameraPos.x = Math.sin(this.time * 0.5) * 3.5; + // this.cameraPos.z = Math.cos(this.time * 0.5) * 3.5; + } + + ngOnDestroy(): void { + if (this.engine) { + this.engine.dispose(); + } + } } diff --git a/src/app/pages/algorithms/service/algorithms.service.ts b/src/app/pages/algorithms/service/algorithms.service.ts index dc51478..bb9e889 100644 --- a/src/app/pages/algorithms/service/algorithms.service.ts +++ b/src/app/pages/algorithms/service/algorithms.service.ts @@ -38,6 +38,12 @@ export class AlgorithmsService { title: 'ALGORITHM.FRACTAL.TITLE', description: 'ALGORITHM.FRACTAL.DESCRIPTION', routerLink: RouterConstants.FRACTAL.LINK + }, + { + id: 'fractal3d', + title: 'ALGORITHM.FRACTAL3D.TITLE', + description: 'ALGORITHM.FRACTAL3D.DESCRIPTION', + routerLink: RouterConstants.FRACTAL3d.LINK } ]; diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 19264c7..7d59a4f 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -417,6 +417,10 @@ "TITLE": "Fraktale", "DESCRIPTION": "Visualisierung von komplexe, geometrische Mustern, die sich selbst in immer kleineren Maßstäben ähneln (Selbstähnlichkeit)." }, + "FRACTAL3D": { + "TITLE": "Fraktale 3D", + "DESCRIPTION": "3D-Visualisierung von komplexe, geometrische Mustern, die sich selbst in immer kleineren Maßstäben ähneln (Selbstähnlichkeit)." + }, "NOTE": "HINWEIS", "GRID_HEIGHT": "Höhe", "GRID_WIDTH": "Beite" diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index f7acef4..087ca1c 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -417,6 +417,10 @@ "TITLE": "Fractals", "DESCRIPTION": "Visualisation of complex geometric patterns that resemble each other on increasingly smaller scales (self-similarity)." }, + "FRACTAL3D": { + "TITLE": "Fractals 3D", + "DESCRIPTION": "3D Visualisation of complex geometric patterns that resemble each other on increasingly smaller scales (self-similarity)." + }, "NOTE": "Note", "GRID_HEIGHT": "Height", "GRID_WIDTH": "Width"