From ba3dc4d92827d971e652ba9f91e9aa5f09c97485 Mon Sep 17 00:00:00 2001 From: LoboTheDark Date: Wed, 11 Feb 2026 08:31:27 +0100 Subject: [PATCH 1/2] First image of fractal Next the nasty stuff like movement and ui :-D --- package-lock.json | 15 ++-- package.json | 2 +- src/app/app.routes.ts | 3 +- src/app/constants/RouterConstants.ts | 7 ++ ...ktal-shader.model.ts => fractal.shader.ts} | 41 +++++----- .../fractal3d/fractal3d.component.html | 4 +- .../fractal3d/fractal3d.component.scss | 2 + .../fractal3d/fractal3d.component.ts | 78 ++++++++++++++++++- .../algorithms/service/algorithms.service.ts | 6 ++ src/assets/i18n/de.json | 4 + src/assets/i18n/en.json | 4 + 11 files changed, 133 insertions(+), 33 deletions(-) rename src/app/pages/algorithms/fractal3d/{fraktal-shader.model.ts => fractal.shader.ts} (70%) 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" From 159d82d6021bf8ae9d70d1f433e0f77288cf4afd Mon Sep 17 00:00:00 2001 From: LoboTheDark Date: Wed, 11 Feb 2026 11:09:07 +0100 Subject: [PATCH 2/2] Finally added 3d fractals --- src/app/constants/UrlConstants.ts | 3 + .../algorithms/fractal3d/fractal.shader.ts | 245 +++++++++++++----- .../fractal3d/fractal3d.component.html | 21 +- .../fractal3d/fractal3d.component.scss | 4 +- .../fractal3d/fractal3d.component.ts | 111 ++++++-- src/assets/i18n/de.json | 18 ++ src/assets/i18n/en.json | 18 ++ 7 files changed, 323 insertions(+), 97 deletions(-) diff --git a/src/app/constants/UrlConstants.ts b/src/app/constants/UrlConstants.ts index c44df1a..640b9d5 100644 --- a/src/app/constants/UrlConstants.ts +++ b/src/app/constants/UrlConstants.ts @@ -14,4 +14,7 @@ static readonly JULIA_WIKI = 'https://de.wikipedia.org/wiki/Julia-Menge' static readonly NEWTON_FRACTAL_WIKI = 'https://de.wikipedia.org/wiki/Newtonfraktal' static readonly BURNING_SHIP_WIKI = 'https://de.wikipedia.org/wiki/Burning_ship_(Fraktal)' + static readonly MANDELBULB_WIKI = 'https://de.wikipedia.org/wiki/Mandelknolle' + static readonly MANDELBOX_WIKI = 'https://de.wikipedia.org/wiki/Mandelbox' + static readonly JULIA3D_WIKI = 'https://de.wikipedia.org/wiki/Mandelknolle' } diff --git a/src/app/pages/algorithms/fractal3d/fractal.shader.ts b/src/app/pages/algorithms/fractal3d/fractal.shader.ts index d52630e..30354bc 100644 --- a/src/app/pages/algorithms/fractal3d/fractal.shader.ts +++ b/src/app/pages/algorithms/fractal3d/fractal.shader.ts @@ -13,97 +13,208 @@ export const MANDELBULB_FRAGMENT = /* glsl */` precision highp float; -// Uniforms -uniform float time; -uniform vec2 resolution; -uniform vec3 cameraPosition; -uniform vec3 targetPosition; -uniform float power; + uniform float time; + uniform vec2 resolution; + uniform vec3 cameraPosition; + uniform vec3 targetPosition; + uniform float power; + uniform int fractalType; // 0 = Bulb, 1 = Box, 2 = Julia -mat2 rot(float a) { - float s = sin(a), c = cos(a); - return mat2(c, -s, s, c); -} + // --- Palettes --- + vec3 palette( in float t, in vec3 a, in vec3 b, in vec3 c, in vec3 d ) { + return a + b*cos( 6.28318*(c*t+d) ); + } -// --- Distance Estimator (Mandelbulb) --- -float map(vec3 pos) { - vec3 z = pos; - float dr = 1.0; - float r = 0.0; + // Global trap for coloring + float minTrap = 1000.0; - for (int i = 0; i < 8; i++) { - r = length(z); - if (r > 2.0) break; + // --- Shape 1: Mandelbulb --- + float mapMandelbulb(vec3 pos, out float trap) { + vec3 z = pos; + float dr = 1.0; + float r = 0.0; + trap = 1000.0; - float theta = acos(z.y / r); - float phi = atan(z.z, z.x); + for (int i = 0; i < 8; i++) { + r = length(z); + if (r > 100.0) break; + trap = min(trap, r); - dr = pow(r, power - 1.0) * power * dr + 1.0; + float theta = acos(z.y / r); + float phi = atan(z.z, z.x); + dr = pow(r, power - 1.0) * power * dr + 1.0; + float zr = pow(r, power); + theta = theta * power; + phi = phi * power; + z = zr * vec3(sin(theta) * cos(phi), cos(theta), sin(theta) * sin(phi)); + z += pos; + } + return 0.5 * log(r) * r / dr; + } - float zr = pow(r, power); - theta = theta * power; - phi = phi * power; + // --- Shape 2: Mandelbox --- + float mapMandelbox(vec3 pos, out float trap) { + vec3 z = pos; + float dr = 1.0; + float scale = 2.8; // Fixed scale for good look + trap = 1000.0; - z = zr * vec3(sin(theta) * cos(phi), cos(theta), sin(theta) * sin(phi)); + for (int i = 0; i < 15; i++) { + // Box fold + z = clamp(z, -1.0, 1.0) * 2.0 - z; - z += pos; + // Sphere fold + float r2 = dot(z, z); + trap = min(trap, r2); // Trap based on sphere fold + + if (r2 < 0.25) { + z = z * 4.0; + dr = dr * 4.0; + } else if (r2 < 1.0) { + z = z / r2; + dr = dr / r2; + } + + z = z * scale + pos; + dr = dr * abs(scale) + 1.0; + } + return (length(z) - abs(scale - 1.0)) / dr; + } + + // --- Shape 3: Julia Bulb --- + float mapJulia(vec3 pos, out float trap) { + vec3 z = pos; + float dr = 1.0; + float r = 0.0; + trap = 1000.0; + + // Constant C for Julia set (animating slightly makes it alive) + vec3 c = vec3(0.35, 0.45, -0.1) + vec3(sin(time*0.1)*0.2); + + for (int i = 0; i < 8; i++) { + r = length(z); + if (r > 100.0) break; // Higher escape radius for Julia + trap = min(trap, r); + + float theta = acos(z.y / r); + float phi = atan(z.z, z.x); + dr = pow(r, power - 1.0) * power * dr + 1.0; + float zr = pow(r, power); + theta = theta * power; + phi = phi * power; + z = zr * vec3(sin(theta) * cos(phi), cos(theta), sin(theta) * sin(phi)); + z += c; // Add C instead of pos + } + return 0.5 * log(r) * r / dr; + } + + // --- Main Map Dispatcher --- + float map(vec3 pos) { + float d = 0.0; + float currentTrap = 0.0; + + if (fractalType == 1) { + d = mapMandelbox(pos, currentTrap); + } else if (fractalType == 2) { + d = mapJulia(pos, currentTrap); + } else { + d = mapMandelbulb(pos, currentTrap); + } + + minTrap = currentTrap; // Update global + return d; + } + + // --- Raymarching --- + bool intersectSphere(vec3 ro, vec3 rd, vec3 c, float r, out float t0, out float t1) { + vec3 oc = ro - c; + float b = dot(oc, rd); + float c2 = dot(oc, oc) - r * r; + float h = b*b - c2; + if (h < 0.0) return false; + h = sqrt(h); + t0 = -b - h; + t1 = -b + h; + return true; + } + + float raymarch(vec3 ro, vec3 rd) { + // Bounding sphere around fractal center (here: origin) + vec3 center = vec3(0.0); + float radius = 6.0; + + float tEnter, tExit; + if (!intersectSphere(ro, rd, center, radius, tEnter, tExit)) { + return -1.0; } - return 0.5 * log(r) * r / dr; -} -// --- Raymarching --- -float raymarch(vec3 ro, vec3 rd) { - float t = 0.0; - for(int i = 0; i < 100; i++) { + float t = max(tEnter, 0.0); + float tMax = tExit; + + for (int i = 0; i < 128; i++) { vec3 pos = ro + t * rd; float d = map(pos); - if(d < 0.001) return t; - if(t > 10.0) break; - t += d; + + // distance-based epsilon is more stable for zoom-out + float eps = max(0.001, 0.0005 * t); + + if (d < eps) return t; + t += d * 0.8; // safety factor against overshoot + if (t > tMax) break; } return -1.0; } -vec3 getNormal(vec3 p) { - float d = map(p); - vec2 e = vec2(0.001, 0.0); - return normalize(vec3( - d - map(p - e.xyy), - d - map(p - e.yxy), - d - map(p - e.yyx) - )); -} + vec3 getNormal(vec3 p) { + float d = map(p); + vec2 e = vec2(0.001, 0.0); + return normalize(vec3( + d - map(p - e.xyy), + d - map(p - e.yxy), + d - map(p - e.yyx) + )); + } -void main(void) { - vec2 uv = (gl_FragCoord.xy - 0.5 * resolution.xy) / resolution.y; + void main(void) { + vec2 uv = (gl_FragCoord.xy - 0.5 * resolution.xy) / resolution.y; + vec3 ro = cameraPosition; + vec3 ta = targetPosition; - vec3 ro = cameraPosition; // Ray Origin - vec3 ta = targetPosition; // Target LookAt + vec3 fwd = normalize(ta - ro); + vec3 right = normalize(cross(vec3(0.0, 1.0, 0.0), fwd)); + vec3 up = normalize(cross(fwd, right)); + vec3 rd = normalize(fwd + uv.x * right + uv.y * up); - vec3 fwd = normalize(ta - ro); - vec3 right = normalize(cross(vec3(0,1,0), fwd)); - vec3 up = normalize(cross(fwd, right)); + vec3 color = vec3(0.1); - vec3 rd = normalize(fwd + uv.x * right + uv.y * up); // Ray Direction + float t = raymarch(ro, rd); - float t = raymarch(ro, rd); + if(t > 0.0) { + vec3 pos = ro + t * rd; + vec3 nor = getNormal(pos); - vec3 color = vec3(0.0); + // Different colors for different shapes + vec3 colParamsA = vec3(0.5, 0.5, 0.5); + vec3 colParamsB = vec3(0.5, 0.5, 0.5); + vec3 colParamsC = vec3(1.0, 1.0, 1.0); + vec3 colParamsD = vec3(0.80, 0.90, 0.30); - if(t > 0.0) { - vec3 pos = ro + t * rd; - vec3 nor = getNormal(pos); + if (fractalType == 1) { // Box: Sci-Fi Blue/Grey + colParamsD = vec3(0.0, 0.1, 0.2); + } + if (fractalType == 2) { // Julia: Alien Green/Purple + colParamsD = vec3(0.8, 0.0, 0.2); + } - //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 + vec3 materialColor = palette(minTrap, colParamsA, colParamsB, colParamsC, colParamsD); - vec3 baseColor = vec3(0.5) + 0.5 * cos(vec3(0.0, 0.4, 0.8) + length(pos) * 2.0); + float camLight = max(0.0, dot(nor, -rd)); + float ambient = 0.4; + vec3 lighting = vec3(1.0) * (camLight * 0.7 + ambient); + color = materialColor * lighting; + } - color = baseColor * (diff + amb); - } - - gl_FragColor = vec4(color, 1.0); -} + color = pow(color, vec3(0.8)); + gl_FragColor = vec4(color, 1.0); + } `; diff --git a/src/app/pages/algorithms/fractal3d/fractal3d.component.html b/src/app/pages/algorithms/fractal3d/fractal3d.component.html index ea533fa..07ac16f 100644 --- a/src/app/pages/algorithms/fractal3d/fractal3d.component.html +++ b/src/app/pages/algorithms/fractal3d/fractal3d.component.html @@ -1,3 +1,18 @@ -
- -
+ + + {{ 'FRACTAL3D.TITLE' | translate }} + + + +
+
+ + + +
+
+
+ +
+
+
diff --git a/src/app/pages/algorithms/fractal3d/fractal3d.component.scss b/src/app/pages/algorithms/fractal3d/fractal3d.component.scss index ffc6ccf..daadde6 100644 --- a/src/app/pages/algorithms/fractal3d/fractal3d.component.scss +++ b/src/app/pages/algorithms/fractal3d/fractal3d.component.scss @@ -1,2 +1,2 @@ -.canvas-container { width: 100%; height: 600px; overflow: hidden; } -canvas { width: 100%; height: 100%; touch-action: none; } +.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/pages/algorithms/fractal3d/fractal3d.component.ts b/src/app/pages/algorithms/fractal3d/fractal3d.component.ts index ffa9edc..d0312f8 100644 --- a/src/app/pages/algorithms/fractal3d/fractal3d.component.ts +++ b/src/app/pages/algorithms/fractal3d/fractal3d.component.ts @@ -1,10 +1,24 @@ import {AfterViewInit, Component, ElementRef, inject, NgZone, OnDestroy, ViewChild} from '@angular/core'; -import {Engine, FreeCamera, MeshBuilder, Scene, ShaderMaterial, Vector2, Vector3} from '@babylonjs/core'; +import {ArcRotateCamera, Engine, MeshBuilder, Scene, ShaderMaterial, Vector2, Vector3} 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'; +import {TranslatePipe} from '@ngx-translate/core'; +import {AlgorithmInformation} from '../information/information.models'; +import {UrlConstants} from '../../../constants/UrlConstants'; +import {MatButton} from '@angular/material/button'; @Component({ selector: 'app-fractal3d', - imports: [], + imports: [ + Information, + MatCard, + MatCardContent, + MatCardHeader, + MatCardTitle, + TranslatePipe, + MatButton + ], templateUrl: './fractal3d.component.html', styleUrl: './fractal3d.component.scss', }) @@ -13,14 +27,39 @@ export class Fractal3dComponent implements AfterViewInit, OnDestroy { @ViewChild('renderCanvas') canvasRef!: ElementRef; + algoInformation: AlgorithmInformation = { + title: 'FRACTAL3D.EXPLANATION.TITLE', + entries: [ + { + name: 'Mandel-Bulb', + description: 'FRACTAL3D.EXPLANATION.MANDELBULB_EXPLANATION', + link: UrlConstants.MANDELBULB_WIKI + }, + { + name: 'Mandelbox', + description: 'FRACTAL3D.EXPLANATION.MANDELBOX_EXPLANATION', + link: UrlConstants.MANDELBOX_WIKI + }, + { + name: 'Julia-Bulb', + description: 'FRACTAL3D.EXPLANATION.JULIA_EXPLANATION', + link: UrlConstants.JULIA3D_WIKI + } + ], + disclaimer: 'FRACTAL3D.EXPLANATION.DISCLAIMER', + disclaimerBottom: '', + disclaimerListEntry: ['FRACTAL3D.EXPLANATION.DISCLAIMER_1', 'FRACTAL3D.EXPLANATION.DISCLAIMER_2', 'FRACTAL3D.EXPLANATION.DISCLAIMER_3', 'FRACTAL3D.EXPLANATION.DISCLAIMER_4'] + }; + + private readonly fractalPower = 8; 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; + private triggerCamUpdate = false; + private cameraPosition: number = 3.5; + public currentFractalType = 0; + ngAfterViewInit(): void { this.ngZone.runOutsideAngular(() => { @@ -33,10 +72,18 @@ export class Fractal3dComponent implements AfterViewInit, OnDestroy { 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 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); - const plane = MeshBuilder.CreatePlane("plane", { size: 2 }, this.scene); + const plane = MeshBuilder.CreatePlane("plane", { size: 10 }, this.scene); + plane.parent = camera; + plane.position.z = 1; + plane.alwaysSelectAsActiveMesh = true; this.shaderMaterial = new ShaderMaterial( "mandelbulbShader", @@ -47,34 +94,48 @@ export class Fractal3dComponent implements AfterViewInit, OnDestroy { }, { attributes: ["position", "uv"], - uniforms: ["time", "resolution", "cameraPosition", "targetPosition", "power"] + 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.01; - this.updateShaderUniforms(canvas); + 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()); } - 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; + onFractalTypeChange(type: number): void { + this.currentFractalType = type; + if (type === 0 ||type === 2) + { + this.cameraPosition = 4; + } + else { + this.cameraPosition = 15; + } + this.triggerCamUpdate = true; } ngOnDestroy(): void { diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 7d59a4f..d013a9b 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -395,6 +395,24 @@ "DISCLAIMER_4": "Rechenintensität: Da für jeden Pixel hunderte Berechnungen durchgeführt werden, sind Fraktale ein klassischer Benchmark für die Performance von Grafikprozessoren (GPUs)." } }, + "FRACTAL3D": { + "TITLE": "3D Fraktale", + "ALGORITHM": "Algorithmen", + "MANDELBULB": "Mandelbulb", + "MANDELBOX": "Mandelbox", + "JULIA": "Julia", + "EXPLANATION": { + "TITLE": "3D Fraktale Welten", + "MANDELBULB_EXPLANATION": "gilt als der 'Heilige Gral' der 3D-Fraktale. Da komplexe Zahlen nur zweidimensional sind, nutzt dieses Fraktal sphärische Koordinaten und hohe Potenzen (meist v^8 + c), um die Mandelbrot-Menge in den Raum zu projizieren. Vorteil: Es entsteht eine organische, extrem detaillierte Struktur, die an Pflanzen, Korallen oder außerirdische Landschaften erinnert.", + "MANDELBOX_EXPLANATION": "basiert nicht auf glatten Kurven, sondern auf geometrischem 'Falten' und Skalieren (Box-Folding & Sphere-Folding). Der Raum wird wie Papier immer wieder gefaltet und gespiegelt. Vorteil: Erzeugt streng geometrische, mechanisch wirkende Strukturen, die wie endlose futuristische Städte, der Borg-Würfel oder komplexe Sci-Fi-Architektur aussehen.", + "JULIA_EXPLANATION": "ist das 3D-Pendant zur 2D-Julia-Menge. Während der Mandelbulb eine 'Karte' aller Fraktale ist, fixiert man hier den Parameter 'c' und variiert den Startpunkt zudem variiert es mit der Zeit. Vorteil: Anders als der massive Mandelbulb sind Julia-Bulbs oft hohle, komplexe Tunnelsysteme oder blasenartige Strukturen, die sich perfekt eignen, um mit der Kamera hindurchzufliegen.", + "DISCLAIMER": "Diese Visualisierung nutzt eine Technik namens 'Raymarching' (Sphere Tracing). Das bedeutet:", + "DISCLAIMER_1": "Keine Polygone: Es gibt keine Dreiecke oder Gitter. Die Form wird rein mathematisch für jeden Pixel in Echtzeit berechnet.", + "DISCLAIMER_2": "Distance Estimation: Der Algorithmus 'tastet' sich mit Lichtstrahlen voran, indem er berechnet, wie weit das nächste Objekt entfernt ist, ohne es sofort zu berühren.", + "DISCLAIMER_3": "Unendliche Details: Da die Oberfläche mathematisch definiert ist, verpixelt sie nicht beim Zoom – es erscheinen immer neue Strukturen.", + "DISCLAIMER_4": "Licht & Schatten: Um die Tiefe sichtbar zu machen, werden Lichtreflexionen und Schatten (Ambient Occlusion) basierend auf der Krümmung der Formel simuliert." + } + }, "ALGORITHM": { "TITLE": "Algorithmen", "PATHFINDING": { diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 087ca1c..12df05b 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -395,6 +395,24 @@ } }, + "FRACTAL3D": { + "TITLE": "3D Fractals", + "ALGORITHM": "Algorithms", + "MANDELBULB": "Mandelbulb", + "MANDELBOX": "Mandelbox", + "JULIA": "Julia", + "EXPLANATION": { + "TITLE": "3D Fractal Worlds", + "MANDELBULB_EXPLANATION": "is considered the 'Holy Grail' of 3D fractals. Since complex numbers are only two-dimensional, this fractal uses spherical coordinates and high powers (usually v^8 + c) to project the Mandelbrot set into 3D space. Benefit: Creates an organic, extremely detailed structure reminiscent of plants, coral reefs, or alien landscapes.", + "MANDELBOX_EXPLANATION": "is based not on smooth curves, but on geometric 'folding' and scaling (Box-Folding & Sphere-Folding). Space is repeatedly folded and mirrored like origami. Benefit: Produces strictly geometric, mechanical-looking structures that resemble endless futuristic cities, the Borg cube, or complex sci-fi architecture.", + "JULIA_EXPLANATION": "is the 3D counterpart to the 2D Julia set. While the Mandelbulb is a 'map' of all fractals, here we fix the parameter 'c' and vary the starting point and it changes over time. Benefit: Unlike the solid Mandelbulb, Julia Bulbs are often hollow, forming complex tunnel systems or bubble-like structures perfect for flying through with the camera.", + "DISCLAIMER": "This visualization uses a technique called 'Raymarching' (Sphere Tracing). This means:", + "DISCLAIMER_1": "No Polygons: There are no triangles or meshes. The shape is calculated mathematically for every pixel in real-time.", + "DISCLAIMER_2": "Distance Estimation: The algorithm 'marches' light rays forward by calculating the safe distance to the nearest object without hitting it immediately.", + "DISCLAIMER_3": "Infinite Detail: Since the surface is mathematically defined, it never pixelates when zooming in – new structures always emerge.", + "DISCLAIMER_4": "Light & Shadow: To visualize depth, light reflections and shadows (Ambient Occlusion) are simulated based on the curvature of the formula." + } + }, "ALGORITHM": { "TITLE": "Algorithms", "PATHFINDING": {