-
-
Save shricodev/4a2b8751a90e9bbe4d8dafbd172ee521 to your computer and use it in GitHub Desktop.
Black Hole Simulation (Developed by Gemini 2.5 Pro Model) - Blog Demo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>Interactive Cinematic Black Hole - Three.js</title> | |
| <style> | |
| body { margin: 0; overflow: hidden; background-color: #000; color: #fff; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } | |
| canvas { display: block; } | |
| #ui-container { | |
| position: absolute; | |
| top: 15px; | |
| left: 15px; | |
| background: rgba(0, 0, 0, 0.5); | |
| padding: 15px; | |
| border-radius: 10px; | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| backdrop-filter: blur(5px); | |
| -webkit-backdrop-filter: blur(5px); | |
| } | |
| h3 { margin-top: 0; margin-bottom: 10px; font-weight: 300; border-bottom: 1px solid rgba(255, 255, 255, 0.2); padding-bottom: 5px; } | |
| .ui-section { margin-bottom: 15px; } | |
| .ui-section:last-child { margin-bottom: 0; } | |
| .color-buttons button { | |
| padding: 8px 12px; | |
| border: 1px solid rgba(255, 255, 255, 0.4); | |
| background-color: transparent; | |
| color: #fff; | |
| cursor: pointer; | |
| margin-right: 5px; | |
| border-radius: 5px; | |
| transition: background-color 0.2s, color: 0.2s; | |
| } | |
| .color-buttons button:hover { | |
| background-color: #fff; | |
| color: #000; | |
| } | |
| label { display: block; margin-bottom: 8px; } | |
| input[type="range"] { | |
| width: 200px; | |
| cursor: pointer; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- UI Controls --> | |
| <div id="ui-container"> | |
| <div class="ui-section"> | |
| <h3>Photon Ring Color</h3> | |
| <div class="color-buttons"> | |
| <button data-palette="spectral">Spectral</button> | |
| <button data-palette="solar">Solar</button> | |
| <button data-palette="abyss">Abyss</button> | |
| </div> | |
| </div> | |
| <div class="ui-section"> | |
| <label for="echo-slider">Disk Echo Intensity</label> | |
| <input | |
| type="range" | |
| id="echo-slider" | |
| min="0" | |
| max="2" | |
| step="0.05" | |
| value="1.0" | |
| /> | |
| </div> | |
| </div> | |
| <script type="module"> | |
| import * as THREE from "https://esm.sh/three"; | |
| import { OrbitControls } from "https://esm.sh/three/examples/jsm/controls/OrbitControls.js"; | |
| import { EffectComposer } from "https://esm.sh/three/examples/jsm/postprocessing/EffectComposer.js"; | |
| import { RenderPass } from "https://esm.sh/three/examples/jsm/postprocessing/RenderPass.js"; | |
| import { UnrealBloomPass } from "https://esm.sh/three/examples/jsm/postprocessing/UnrealBloomPass.js"; | |
| import { ShaderPass } from "https://esm.sh/three/examples/jsm/postprocessing/ShaderPass.js"; | |
| // --- GLOBAL VARIABLES --- | |
| let scene, camera, renderer, controls, composer, clock; | |
| let blackHoleGroup, accretionDisk, photonRing; | |
| // --- COLOR PALETTES FOR THE RING --- | |
| const ringColorPalettes = { | |
| spectral: { | |
| c1: new THREE.Color(0xcc00ff), // Magenta | |
| c2: new THREE.Color(0x4b0082), // Indigo | |
| c3: new THREE.Color(0x8a2be2), // BlueViolet | |
| }, | |
| solar: { | |
| c1: new THREE.Color(0xffd700), // Gold | |
| c2: new THREE.Color(0xff4500), // OrangeRed | |
| c3: new THREE.Color(0xff8c00), // DarkOrange | |
| }, | |
| abyss: { | |
| c1: new THREE.Color(0x00ffff), // Cyan | |
| c2: new THREE.Color(0x004d4d), // Dark Teal | |
| c3: new THREE.Color(0x0d98ba), // Light Blue | |
| }, | |
| }; | |
| // --- INITIALIZATION --- | |
| function init() { | |
| clock = new THREE.Clock(); | |
| scene = new THREE.Scene(); | |
| camera = new THREE.PerspectiveCamera( | |
| 75, | |
| window.innerWidth / window.innerHeight, | |
| 0.1, | |
| 2000, | |
| ); | |
| camera.position.set(0, 5, 25); | |
| renderer = new THREE.WebGLRenderer({ antialias: true }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.setPixelRatio(window.devicePixelRatio); | |
| document.body.appendChild(renderer.domElement); | |
| controls = new OrbitControls(camera, renderer.domElement); | |
| controls.enableDamping = true; | |
| controls.dampingFactor = 0.05; | |
| controls.screenSpacePanning = false; | |
| controls.minDistance = 5; | |
| controls.maxDistance = 100; | |
| controls.autoRotate = true; | |
| controls.autoRotateSpeed = 0.2; | |
| createStarfield(); | |
| createNebulae(); | |
| blackHoleGroup = new THREE.Group(); | |
| scene.add(blackHoleGroup); | |
| const eventHorizon = new THREE.Mesh( | |
| new THREE.SphereGeometry(4.8, 64, 64), | |
| new THREE.MeshBasicMaterial({ color: 0x000000 }), | |
| ); | |
| photonRing = createPhotonRing(); | |
| accretionDisk = createAccretionDisk(); | |
| blackHoleGroup.add(eventHorizon, photonRing, accretionDisk); | |
| blackHoleGroup.rotation.x = Math.PI / 3; | |
| blackHoleGroup.rotation.y = Math.PI / 6; | |
| setupPostProcessing(); | |
| setupUI(); // Setup the interactive UI | |
| window.addEventListener("resize", onWindowResize); | |
| } | |
| // --- UI SETUP --- | |
| function setupUI() { | |
| // Ring Color Buttons | |
| document.querySelectorAll(".color-buttons button").forEach((button) => { | |
| button.addEventListener("click", () => { | |
| const paletteName = button.getAttribute("data-palette"); | |
| const palette = ringColorPalettes[paletteName]; | |
| if (palette && photonRing) { | |
| photonRing.material.uniforms.uColor1.value = palette.c1; | |
| photonRing.material.uniforms.uColor2.value = palette.c2; | |
| photonRing.material.uniforms.uColor3.value = palette.c3; | |
| } | |
| }); | |
| }); | |
| // Disk Echo Slider | |
| const echoSlider = document.getElementById("echo-slider"); | |
| echoSlider.addEventListener("input", (event) => { | |
| if (accretionDisk) { | |
| accretionDisk.material.uniforms.uEchoStrength.value = parseFloat( | |
| event.target.value, | |
| ); | |
| } | |
| }); | |
| } | |
| // --- OBJECT CREATION FUNCTIONS --- | |
| function createStarfield() { | |
| const vertices = []; | |
| for (let i = 0; i < 5000; i++) | |
| vertices.push(THREE.MathUtils.randFloatSpread(2000)); | |
| const geometry = new THREE.BufferGeometry(); | |
| geometry.setAttribute( | |
| "position", | |
| new THREE.Float32BufferAttribute(vertices, 3), | |
| ); | |
| const material = new THREE.PointsMaterial({ | |
| color: 0xffffff, | |
| size: 0.7, | |
| sizeAttenuation: true, | |
| }); | |
| scene.add(new THREE.Points(geometry, material)); | |
| } | |
| function createNebulae() { | |
| const textureLoader = new THREE.TextureLoader(); | |
| textureLoader.load( | |
| "https://threejs.org/examples/textures/lava/cloud.png", | |
| (texture) => { | |
| const geo = new THREE.PlaneGeometry(300, 300); | |
| const mat = new THREE.MeshLambertMaterial({ | |
| map: texture, | |
| emissiveIntensity: 2, | |
| opacity: 0.25, | |
| transparent: true, | |
| blending: THREE.AdditiveBlending, | |
| depthWrite: false, | |
| }); | |
| for (let i = 0; i < 15; i++) { | |
| const nebula = new THREE.Mesh(geo, mat.clone()); | |
| nebula.position.set( | |
| (Math.random() - 0.5) * 1000, | |
| (Math.random() - 0.5) * 500, | |
| (Math.random() - 0.5) * 1000 - 500, | |
| ); | |
| nebula.rotation.z = Math.random() * Math.PI * 2; | |
| nebula.material.color.setHSL(Math.random() * 0.2 + 0.6, 0.7, 0.5); | |
| scene.add(nebula); | |
| } | |
| }, | |
| ); | |
| } | |
| function createPhotonRing() { | |
| const geometry = new THREE.TorusGeometry(7.5, 0.3, 32, 256); | |
| const defaultPalette = ringColorPalettes.spectral; | |
| const material = new THREE.ShaderMaterial({ | |
| uniforms: { | |
| uTime: { value: 0.0 }, | |
| uColor1: { value: defaultPalette.c1 }, | |
| uColor2: { value: defaultPalette.c2 }, | |
| uColor3: { value: defaultPalette.c3 }, | |
| }, | |
| vertexShader: ` | |
| varying vec2 vUv; | |
| void main() { | |
| vUv = uv; | |
| gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); | |
| } | |
| `, | |
| fragmentShader: ` | |
| varying vec2 vUv; | |
| uniform float uTime; | |
| uniform vec3 uColor1; | |
| uniform vec3 uColor2; | |
| uniform vec3 uColor3; | |
| vec3 colorGradient(float t, vec3 c1, vec3 c2, vec3 c3) { | |
| return mix(mix(c1, c2, smoothstep(0.0, 0.5, t)), c3, smoothstep(0.5, 1.0, t)); | |
| } | |
| void main() { | |
| float angle = vUv.x * 2.0 * 3.14159; | |
| float flicker = 0.8 + 0.2 * sin(angle * 15.0 + uTime * 3.0); | |
| vec3 finalColor = colorGradient(fract(vUv.x + uTime * 0.05), uColor1, uColor2, uColor3); | |
| gl_FragColor = vec4(finalColor * flicker, 1.0); | |
| } | |
| `, | |
| blending: THREE.AdditiveBlending, | |
| transparent: true, | |
| }); | |
| return new THREE.Mesh(geometry, material); | |
| } | |
| function createAccretionDisk() { | |
| const geometry = new THREE.RingGeometry(8, 20, 128, 64); | |
| geometry.rotateX(-Math.PI / 2); | |
| // Simplex noise function used in shaders | |
| const snoise = ` | |
| vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; } | |
| vec2 mod289(vec2 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; } | |
| vec3 permute(vec3 x) { return mod289(((x*34.0)+1.0)*x); } | |
| float snoise(vec2 v) { | |
| const vec4 C = vec4(0.211324865405187, 0.366025403784439, -0.577350269189626, 0.024390243902439); | |
| vec2 i = floor(v + dot(v, C.yy)); vec2 x0 = v - i + dot(i, C.xx); | |
| vec2 i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0); | |
| vec4 x12 = x0.xyxy + C.xxzz; x12.xy -= i1; | |
| i = mod289(i); | |
| vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 )) + i.x + vec3(0.0, i1.x, 1.0 )); | |
| vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0); | |
| m = m*m; m = m*m; | |
| vec3 x = 2.0 * fract(p * C.www) - 1.0; vec3 h = abs(x) - 0.5; | |
| vec3 ox = floor(x + 0.5); vec3 a0 = x - ox; | |
| m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h ); | |
| vec3 g; g.x = a0.x * x0.x + h.x * x0.y; g.yz = a0.yz * x12.xz + h.yz * x12.yw; | |
| return 130.0 * dot(m, g); | |
| } | |
| `; | |
| const material = new THREE.ShaderMaterial({ | |
| uniforms: { | |
| uTime: { value: 0.0 }, | |
| uEchoStrength: { value: 1.0 }, | |
| }, | |
| vertexShader: | |
| snoise + | |
| ` | |
| varying vec2 vUv; | |
| varying float vRadius; | |
| uniform float uTime; | |
| void main() { | |
| vUv = uv; | |
| vec3 pos = position; | |
| vRadius = length(pos.xz); | |
| float angle = atan(pos.x, pos.z); | |
| float noise = snoise(vec2(angle * 3.0, vRadius * 0.5 - uTime * 0.2)); | |
| pos.y += noise * 0.3 * (1.0 - smoothstep(8.0, 15.0, vRadius)); | |
| gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); | |
| } | |
| `, | |
| fragmentShader: | |
| snoise + | |
| ` | |
| varying vec2 vUv; | |
| varying float vRadius; | |
| uniform float uTime; | |
| uniform float uEchoStrength; | |
| void main() { | |
| float t = smoothstep(8.0, 20.0, vRadius); | |
| vec3 innerColor = vec3(1.0, 0.8, 0.2); | |
| vec3 outerColor = vec3(0.2, 0.1, 0.8); | |
| vec3 color = mix(innerColor, outerColor, t); | |
| float angle = atan(vUv.y - 0.5, vUv.x - 0.5); | |
| float noise = snoise(vec2(vRadius * 0.8 - uTime * 0.5, angle * 5.0)); | |
| float streaks = snoise(vec2(vRadius * 0.3 - uTime * 0.8, angle * 20.0)); | |
| streaks = pow(abs(streaks), 3.0); | |
| float intensity = 1.0 + (noise * 0.4 + streaks * 0.5) * uEchoStrength; | |
| float alpha = smoothstep(0.0, 0.5, intensity); | |
| gl_FragColor = vec4(color * intensity, alpha); | |
| } | |
| `, | |
| blending: THREE.AdditiveBlending, | |
| transparent: true, | |
| side: THREE.DoubleSide, | |
| depthWrite: false, | |
| }); | |
| return new THREE.Mesh(geometry, material); | |
| } | |
| function setupPostProcessing() { | |
| composer = new EffectComposer(renderer); | |
| composer.addPass(new RenderPass(scene, camera)); | |
| const lensingShader = { | |
| uniforms: { | |
| tDiffuse: { value: null }, | |
| uStrength: { value: 0.03 }, | |
| uRadius: { value: 0.4 }, | |
| }, | |
| vertexShader: `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`, | |
| fragmentShader: ` | |
| uniform sampler2D tDiffuse; uniform float uStrength; uniform float uRadius; varying vec2 vUv; | |
| void main() { | |
| vec2 center = vec2(0.5, 0.5); vec2 uv = vUv; | |
| float dist = distance(uv, center); | |
| if (dist < uRadius) { uv += normalize(center - uv) * smoothstep(uRadius, 0.0, dist) * uStrength; } | |
| gl_FragColor = texture2D(tDiffuse, uv); | |
| }`, | |
| }; | |
| composer.addPass(new ShaderPass(lensingShader)); | |
| const bloomPass = new UnrealBloomPass( | |
| new THREE.Vector2(window.innerWidth, window.innerHeight), | |
| 1.5, | |
| 0.4, | |
| 0.85, | |
| ); | |
| bloomPass.threshold = 0.21; | |
| bloomPass.strength = 0.6; | |
| bloomPass.radius = 0.55; | |
| composer.addPass(bloomPass); | |
| } | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| const elapsedTime = clock.getElapsedTime(); | |
| if (accretionDisk) | |
| accretionDisk.material.uniforms.uTime.value = elapsedTime; | |
| if (photonRing) photonRing.material.uniforms.uTime.value = elapsedTime; | |
| if (scene.children[0].type === "Points") | |
| scene.children[0].rotation.y = elapsedTime * 0.01; | |
| controls.update(); | |
| composer.render(); | |
| } | |
| function onWindowResize() { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| composer.setSize(window.innerWidth, window.innerHeight); | |
| } | |
| init(); | |
| animate(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment