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": {