Skip to content

Instantly share code, notes, and snippets.

@shricodev
Created June 14, 2025 10:48
Show Gist options
  • Select an option

  • Save shricodev/4a2b8751a90e9bbe4d8dafbd172ee521 to your computer and use it in GitHub Desktop.

Select an option

Save shricodev/4a2b8751a90e9bbe4d8dafbd172ee521 to your computer and use it in GitHub Desktop.
Black Hole Simulation (Developed by Gemini 2.5 Pro Model) - Blog Demo
<!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