Skip to content

Instantly share code, notes, and snippets.

@shricodev
Created June 14, 2025 11:07
Show Gist options
  • Save shricodev/ad6e91b6de8781be28a007d62b5a48d9 to your computer and use it in GitHub Desktop.
Save shricodev/ad6e91b6de8781be28a007d62b5a48d9 to your computer and use it in GitHub Desktop.
Black Hole Simulation (Developed by OpenAI o3 Pro Model) - Blog Demo
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Three.js Black-Hole Shader</title>
<style>
html,
body {
margin: 0;
padding: 0;
overflow: hidden;
background: #000;
font-family: sans-serif;
}
#ui {
position: fixed;
left: 20px;
top: 20px;
z-index: 10;
display: flex;
flex-direction: column;
gap: 8px;
}
button {
padding: 6px 14px;
border: none;
border-radius: 4px;
background: #111;
color: #fff;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
button:hover {
background: #fff;
color: #000;
}
</style>
</head>
<body>
<div id="ui">
<button id="echoBtn">Disk Echo</button>
<button id="themeBtn">Theme : Ice</button>
</div>
<canvas id="c"></canvas>
<!-- Three.js CDN (Corrected URL) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/0.153.0/three.min.js"></script>
<script>
(() => {
// ---------- 1. Boiler-plate Three.js ----------
const canvas = document.getElementById("c");
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); // full-screen quad
// ---------- 2. Themes ----------
const themes = [
{
name: "Ice",
primary: "#4fc1ff",
secondary: "#c9f0ff",
tertiary: "#ffffff",
},
{
name: "Ember",
primary: "#ff891c",
secondary: "#ffefac",
tertiary: "#ff3b00",
},
{
name: "UltraViolet",
primary: "#ae00ff",
secondary: "#ffcbff",
tertiary: "#00d4ff",
},
];
let themeIndex = 0;
function hex2rgb(hex) {
const c = parseInt(hex.slice(1), 16);
return new THREE.Color(
((c >> 16) & 255) / 255,
((c >> 8) & 255) / 255,
(c & 255) / 255,
);
}
// ---------- 3. Shader uniforms ----------
const uniforms = {
uTime: { value: 0 },
uPrimary: { value: hex2rgb(themes[0].primary) },
uSecondary: { value: hex2rgb(themes[0].secondary) },
uTertiary: { value: hex2rgb(themes[0].tertiary) },
uRippleStart: { value: -1000 }, // long ago – means “off”
uResolution: { value: new THREE.Vector2() },
};
// ---------- 4. GLSL – vertex & fragment ----------
const vtx = `
varying vec2 vUv;
void main(){
vUv = uv;
gl_Position = vec4(position,1.0);
}`;
const frag = `
precision highp float;
varying vec2 vUv;
uniform float uTime;
uniform vec3 uPrimary;
uniform vec3 uSecondary;
uniform vec3 uTertiary;
uniform float uRippleStart;
uniform vec2 uResolution;
#define PI 3.14159265359
// Very tiny hash-based pseudo-noise for starfield
float hash(vec2 p){
p = fract(p*vec2(123.34,456.21));
p += dot(p,p+45.32);
return fract(p.x*p.y);
}
// -------------------------------------------------
void main(){
// Normalized device coords centred at (0,0)
vec2 uv = vUv*uResolution / min(uResolution.x,uResolution.y);
vec2 cUv = uv - 0.5*uResolution/min(uResolution.x,uResolution.y);
float r = length(cUv);
// Background – cheap starfield
float stars = step(0.997,hash(floor(uv*vec2(3.0,4.0))));
vec3 bg = mix(vec3(0.0), vec3(1.0), stars);
// Gravitational lensing swirl (very stylised)
float bend = 0.15/r;
float ang = atan(cUv.y,cUv.x) + bend;
vec2 warped = vec2(cos(ang),sin(ang))*r;
// Accretion disk – horizontal band, emissive gradient
float disk = smoothstep(0.03,0.0,abs(warped.y));
vec3 diskCol = mix(uPrimary,uSecondary,0.5+0.5*sin(uTime*3.0+r*40.0));
// Event horizon
float horizon = smoothstep(0.16,0.18,r);
// -------------------------------------------------
vec3 col = mix(bg,diskCol,disk); // add accretion disk
col = mix(col,vec3(0.0),horizon); // carve the black hole
// Ripple (Disk Echo) -------------------------------------------------
float rippleTime = uTime - uRippleStart;
if(rippleTime > 0.0){
float rippleRadius = rippleTime*0.12; // speed
float width = 0.015;
float rip = smoothstep(width,0.0,abs(r-rippleRadius));
col += uTertiary * rip * (1.0-rippleTime*0.2); // fade out slowly
}
gl_FragColor = vec4(col,1.0);
}`;
const material = new THREE.ShaderMaterial({
uniforms,
vertexShader: vtx,
fragmentShader: frag,
});
scene.add(new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material));
// ---------- 5. Resize ----------
function onResize() {
renderer.setSize(window.innerWidth, window.innerHeight);
uniforms.uResolution.value.set(
renderer.domElement.width,
renderer.domElement.height,
);
}
window.addEventListener("resize", onResize);
onResize();
// ---------- 6. Animation loop ----------
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
uniforms.uTime.value = clock.getElapsedTime();
renderer.render(scene, camera);
}
animate();
// ---------- 7. UI Controls ----------
const echoBtn = document.getElementById("echoBtn");
const themeBtn = document.getElementById("themeBtn");
echoBtn.addEventListener("click", () => {
uniforms.uRippleStart.value = uniforms.uTime.value; // trigger ripple
});
themeBtn.addEventListener("click", () => {
themeIndex = (themeIndex + 1) % themes.length;
const t = themes[themeIndex];
uniforms.uPrimary.value = hex2rgb(t.primary);
uniforms.uSecondary.value = hex2rgb(t.secondary);
uniforms.uTertiary.value = hex2rgb(t.tertiary);
themeBtn.textContent = `Theme : ${t.name}`;
});
})(); // IIFE
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment