Shader Example: Black Hole
Live Demo
Fragment Shader
Copy paste from below to see its effect; Use the Divooka version when using with Shader Lab in Divooka.
Usage
A few useful tweaks:
- Bigger black hole: increase the event horizon check from
0.82to about1.0 - Stronger lensing: increase
bend = 0.09 / r2 - Thicker disk: increase
if (py < 0.22)and reduceexp(-y * 28.0) - More dramatic glow: increase the
ringandglowmultipliers - Faster disk motion: increase
iTime * 2.6,4.0, etc.
Snippets
WebGL
// Features:
// - Gravitational lensing warp
// - Dark event horizon
// - Glowing accretion disk
// - Procedural starfield / nebula background
// - Mild animated swirl / cinematic motion
//
// Controls:
// - Mouse X rotates camera
// - Mouse Y changes tilt a bit
precision mediump float;
uniform vec2 u_Resolution;
uniform float u_Time;
uniform vec3 u_Mouse; // x, y, pressed
#define PI 3.14159265359
#define TAU 6.28318530718
mat2 rot(float a)
{
float c = cos(a), s = sin(a);
return mat2(c,-s,s,c);
}
float hash11(float p)
{
p = fract(p * 0.1031);
p *= p + 33.33;
p *= p + p;
return fract(p);
}
float hash21(vec2 p)
{
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
vec2 hash22(vec2 p)
{
vec3 p3 = fract(vec3(p.xyx) * vec3(0.1031, 0.1030, 0.0973));
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.xx + p3.yz) * p3.zy);
}
float noise(vec2 p)
{
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash21(i + vec2(0.0, 0.0));
float b = hash21(i + vec2(1.0, 0.0));
float c = hash21(i + vec2(0.0, 1.0));
float d = hash21(i + vec2(1.0, 1.0));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
float fbm(vec2 p)
{
float v = 0.0;
float a = 0.5;
for (int i = 0; i < 6; i++)
{
v += a * noise(p);
p = p * 2.03 + vec2(13.1, 7.7);
a *= 0.5;
}
return v;
}
vec3 bgColor(vec3 rd)
{
// Spherical coords
float u = atan(rd.z, rd.x) / TAU;
float v = asin(clamp(rd.y, -1.0, 1.0)) / PI;
vec2 p = vec2(u, v);
p.x += 0.02 * u_Time;
// Nebula
float n1 = fbm(p * vec2(8.0, 4.0) + vec2(0.0, 0.1 * u_Time));
float n2 = fbm(p * vec2(14.0, 7.0) - vec2(0.05 * u_Time, 0.0));
float neb = smoothstep(0.45, 0.9, n1 * 0.75 + n2 * 0.35);
vec3 col = vec3(0.004, 0.006, 0.012);
col += neb * vec3(0.08, 0.16, 0.32);
col += pow(max(n2, 0.0), 4.0) * vec3(0.2, 0.08, 0.35) * 0.5;
// Stars
vec2 sp = p * vec2(220.0, 110.0);
vec2 cell = floor(sp);
vec2 f = fract(sp) - 0.5;
vec2 rnd = hash22(cell);
vec2 starPos = (rnd - 0.5) * 0.7;
float d = length(f - starPos);
float star = smoothstep(0.03, 0.0, d);
star *= step(0.992, hash21(cell + 17.3));
float tw = 0.65 + 0.35 * sin(u_Time * (4.0 + rnd.x * 8.0) + rnd.y * TAU);
vec3 starCol = mix(vec3(0.8, 0.9, 1.0), vec3(1.0, 0.85, 0.7), rnd.x);
col += star * tw * starCol * (1.5 + 2.5 * rnd.y);
// Bigger sparse stars
vec2 sp2 = p * vec2(80.0, 40.0);
vec2 cell2 = floor(sp2);
vec2 f2 = fract(sp2) - 0.5;
vec2 rnd2 = hash22(cell2 + 99.7);
float d2 = length(f2 - (rnd2 - 0.5) * 0.6);
float star2 = smoothstep(0.06, 0.0, d2) * step(0.9975, hash21(cell2 + 123.4));
col += star2 * vec3(1.2, 1.1, 1.0) * 3.0;
return col;
}
float diskDensity(vec3 p)
{
float r = length(p.xz);
float y = abs(p.y);
float band = exp(-y * 28.0);
float inner = smoothstep(0.85, 1.6, r);
float outer = 1.0 - smoothstep(3.8, 7.5, r);
float a = atan(p.z, p.x);
float swirl = sin(a * 6.0 - r * 2.7 - u_Time * 2.6);
float spiral = 0.6 + 0.4 * swirl;
float turb = fbm(vec2(a * 2.0, r * 1.2) + vec2(u_Time * 0.1, -u_Time * 0.35));
turb = mix(0.65, 1.35, turb);
return band * inner * outer * spiral * turb;
}
vec3 diskColor(vec3 p)
{
float r = length(p.xz);
float heat = exp(-0.55 * max(r - 1.0, 0.0));
vec3 hot = vec3(1.8, 1.2, 0.7);
vec3 warm = vec3(1.2, 0.45, 0.08);
vec3 cool = vec3(0.8, 0.15, 0.03);
vec3 col = mix(cool, warm, smoothstep(2.8, 1.4, r));
col = mix(col, hot, heat);
float a = atan(p.z, p.x);
float streak = 0.75 + 0.25 * sin(a * 18.0 - r * 5.0 - u_Time * 4.0);
return col * streak;
}
vec3 render(vec2 fragCoord)
{
vec2 uv = (fragCoord - 0.5 * u_Resolution.xy) / u_Resolution.y;
float mx = (u_Mouse.z > 0.0) ? (u_Mouse.x / u_Resolution.x) : 0.63;
float my = (u_Mouse.z > 0.0) ? (u_Mouse.y / u_Resolution.y) : 0.42;
// Camera
vec3 ro = vec3(0.0, 0.35 + (my - 0.5) * 1.2, 8.5);
vec3 ta = vec3(0.0, 0.15, 0.0);
float yaw = (mx - 0.5) * 1.8;
ro.xz *= rot(yaw);
ta.xz *= rot(yaw);
vec3 ww = normalize(ta - ro);
vec3 uu = normalize(cross(vec3(0.0, 1.0, 0.0), ww));
vec3 vv = cross(ww, uu);
vec3 rd = normalize(uu * uv.x + vv * uv.y + ww * 1.8);
vec3 pos = ro;
vec3 dir = rd;
vec3 col = vec3(0.0);
vec3 trans = vec3(1.0);
const int STEPS = 160;
float dt = 0.055;
for (int i = 0; i < STEPS; i++)
{
float r = length(pos);
float r2 = max(r * r, 0.08);
// Gravitational bending toward the center
// Not physically exact; tuned for appearance.
float bend = 0.09 / r2;
dir = normalize(dir - pos * bend * dt);
vec3 prev = pos;
pos += dir * dt;
// Event horizon
if (length(pos) < 0.82)
{
trans *= 0.0;
break;
}
// Accretion disk intersection region around y=0
float py = abs(pos.y);
if (py < 0.22)
{
float dens = diskDensity(pos) * 0.22;
if (dens > 0.001)
{
vec3 dcol = diskColor(pos);
// Doppler-ish brightening depending on orbital direction
vec3 tangent = normalize(vec3(-pos.z, 0.0, pos.x));
float beaming = pow(max(0.0, dot(tangent, -dir)) * 0.5 + 0.5, 3.0);
dcol *= 0.7 + 1.8 * beaming;
// Glow near photon ring
float rr = length(pos.xz);
float ring = exp(-pow((rr - 1.25) * 5.5, 2.0));
dcol += vec3(1.4, 0.8, 0.35) * ring * 1.5;
float alpha = clamp(dens, 0.0, 0.35);
col += trans * dcol * alpha;
trans *= (1.0 - alpha);
if (dot(trans, vec3(0.333)) < 0.01) break;
}
}
// Soft halo / lensing glow around black hole
float glow = exp(-pow(max(r - 1.1, 0.0) * 1.2, 1.35)) * 0.006;
col += trans * vec3(0.9, 0.72, 0.45) * glow;
}
// Background seen through bent ray
vec3 bg = bgColor(dir);
col += trans * bg;
// Dark central silhouette and bright photon ring in screen space
float centerDist = length(uv);
float hole = smoothstep(0.24, 0.19, centerDist);
col *= (1.0 - 0.92 * hole);
float ring = exp(-pow((centerDist - 0.23) * 18.0, 2.0));
col += vec3(1.1, 0.75, 0.35) * ring * 0.45;
// Vignette
col *= 1.0 - 0.35 * dot(uv, uv);
// Tonemap + gamma
col = 1.0 - exp(-col * 1.15);
col = pow(col, vec3(0.4545));
return col;
}
void main()
{
vec3 col = render(gl_FragCoord.xy);
gl_FragColor = vec4(col, 1.0);
}
ShaderToy
// Features:
// - Gravitational lensing warp
// - Dark event horizon
// - Glowing accretion disk
// - Procedural starfield / nebula background
// - Mild animated swirl / cinematic motion
//
// Controls:
// - Mouse X rotates camera
// - Mouse Y changes tilt a bit
#define PI 3.14159265359
#define TAU 6.28318530718
mat2 rot(float a)
{
float c = cos(a), s = sin(a);
return mat2(c,-s,s,c);
}
float hash11(float p)
{
p = fract(p * 0.1031);
p *= p + 33.33;
p *= p + p;
return fract(p);
}
float hash21(vec2 p)
{
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
vec2 hash22(vec2 p)
{
vec3 p3 = fract(vec3(p.xyx) * vec3(0.1031, 0.1030, 0.0973));
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.xx + p3.yz) * p3.zy);
}
float noise(vec2 p)
{
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash21(i + vec2(0.0, 0.0));
float b = hash21(i + vec2(1.0, 0.0));
float c = hash21(i + vec2(0.0, 1.0));
float d = hash21(i + vec2(1.0, 1.0));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
float fbm(vec2 p)
{
float v = 0.0;
float a = 0.5;
for (int i = 0; i < 6; i++)
{
v += a * noise(p);
p = p * 2.03 + vec2(13.1, 7.7);
a *= 0.5;
}
return v;
}
vec3 bgColor(vec3 rd)
{
// Spherical coords
float u = atan(rd.z, rd.x) / TAU;
float v = asin(clamp(rd.y, -1.0, 1.0)) / PI;
vec2 p = vec2(u, v);
p.x += 0.02 * iTime;
// Nebula
float n1 = fbm(p * vec2(8.0, 4.0) + vec2(0.0, 0.1 * iTime));
float n2 = fbm(p * vec2(14.0, 7.0) - vec2(0.05 * iTime, 0.0));
float neb = smoothstep(0.45, 0.9, n1 * 0.75 + n2 * 0.35);
vec3 col = vec3(0.004, 0.006, 0.012);
col += neb * vec3(0.08, 0.16, 0.32);
col += pow(max(n2, 0.0), 4.0) * vec3(0.2, 0.08, 0.35) * 0.5;
// Stars
vec2 sp = p * vec2(220.0, 110.0);
vec2 cell = floor(sp);
vec2 f = fract(sp) - 0.5;
vec2 rnd = hash22(cell);
vec2 starPos = (rnd - 0.5) * 0.7;
float d = length(f - starPos);
float star = smoothstep(0.03, 0.0, d);
star *= step(0.992, hash21(cell + 17.3));
float tw = 0.65 + 0.35 * sin(iTime * (4.0 + rnd.x * 8.0) + rnd.y * TAU);
vec3 starCol = mix(vec3(0.8, 0.9, 1.0), vec3(1.0, 0.85, 0.7), rnd.x);
col += star * tw * starCol * (1.5 + 2.5 * rnd.y);
// Bigger sparse stars
vec2 sp2 = p * vec2(80.0, 40.0);
vec2 cell2 = floor(sp2);
vec2 f2 = fract(sp2) - 0.5;
vec2 rnd2 = hash22(cell2 + 99.7);
float d2 = length(f2 - (rnd2 - 0.5) * 0.6);
float star2 = smoothstep(0.06, 0.0, d2) * step(0.9975, hash21(cell2 + 123.4));
col += star2 * vec3(1.2, 1.1, 1.0) * 3.0;
return col;
}
float diskDensity(vec3 p)
{
float r = length(p.xz);
float y = abs(p.y);
float band = exp(-y * 28.0);
float inner = smoothstep(0.85, 1.6, r);
float outer = 1.0 - smoothstep(3.8, 7.5, r);
float a = atan(p.z, p.x);
float swirl = sin(a * 6.0 - r * 2.7 - iTime * 2.6);
float spiral = 0.6 + 0.4 * swirl;
float turb = fbm(vec2(a * 2.0, r * 1.2) + vec2(iTime * 0.1, -iTime * 0.35));
turb = mix(0.65, 1.35, turb);
return band * inner * outer * spiral * turb;
}
vec3 diskColor(vec3 p)
{
float r = length(p.xz);
float heat = exp(-0.55 * max(r - 1.0, 0.0));
vec3 hot = vec3(1.8, 1.2, 0.7);
vec3 warm = vec3(1.2, 0.45, 0.08);
vec3 cool = vec3(0.8, 0.15, 0.03);
vec3 col = mix(cool, warm, smoothstep(2.8, 1.4, r));
col = mix(col, hot, heat);
float a = atan(p.z, p.x);
float streak = 0.75 + 0.25 * sin(a * 18.0 - r * 5.0 - iTime * 4.0);
return col * streak;
}
vec3 render(vec2 fragCoord)
{
vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
float mx = (iMouse.z > 0.0) ? (iMouse.x / iResolution.x) : 0.63;
float my = (iMouse.z > 0.0) ? (iMouse.y / iResolution.y) : 0.42;
// Camera
vec3 ro = vec3(0.0, 0.35 + (my - 0.5) * 1.2, 8.5);
vec3 ta = vec3(0.0, 0.15, 0.0);
float yaw = (mx - 0.5) * 1.8;
ro.xz *= rot(yaw);
ta.xz *= rot(yaw);
vec3 ww = normalize(ta - ro);
vec3 uu = normalize(cross(vec3(0.0, 1.0, 0.0), ww));
vec3 vv = cross(ww, uu);
vec3 rd = normalize(uu * uv.x + vv * uv.y + ww * 1.8);
vec3 pos = ro;
vec3 dir = rd;
vec3 col = vec3(0.0);
vec3 trans = vec3(1.0);
const int STEPS = 160;
float dt = 0.055;
for (int i = 0; i < STEPS; i++)
{
float r = length(pos);
float r2 = max(r * r, 0.08);
// Gravitational bending toward the center
// Not physically exact; tuned for appearance.
float bend = 0.09 / r2;
dir = normalize(dir - pos * bend * dt);
vec3 prev = pos;
pos += dir * dt;
// Event horizon
if (length(pos) < 0.82)
{
trans *= 0.0;
break;
}
// Accretion disk intersection region around y=0
float py = abs(pos.y);
if (py < 0.22)
{
float dens = diskDensity(pos) * 0.22;
if (dens > 0.001)
{
vec3 dcol = diskColor(pos);
// Doppler-ish brightening depending on orbital direction
vec3 tangent = normalize(vec3(-pos.z, 0.0, pos.x));
float beaming = pow(max(0.0, dot(tangent, -dir)) * 0.5 + 0.5, 3.0);
dcol *= 0.7 + 1.8 * beaming;
// Glow near photon ring
float rr = length(pos.xz);
float ring = exp(-pow((rr - 1.25) * 5.5, 2.0));
dcol += vec3(1.4, 0.8, 0.35) * ring * 1.5;
float alpha = clamp(dens, 0.0, 0.35);
col += trans * dcol * alpha;
trans *= (1.0 - alpha);
if (dot(trans, vec3(0.333)) < 0.01) break;
}
}
// Soft halo / lensing glow around black hole
float glow = exp(-pow(max(r - 1.1, 0.0) * 1.2, 1.35)) * 0.006;
col += trans * vec3(0.9, 0.72, 0.45) * glow;
}
// Background seen through bent ray
vec3 bg = bgColor(dir);
col += trans * bg;
// Dark central silhouette and bright photon ring in screen space
float centerDist = length(uv);
float hole = smoothstep(0.24, 0.19, centerDist);
col *= (1.0 - 0.92 * hole);
float ring = exp(-pow((centerDist - 0.23) * 18.0, 2.0));
col += vec3(1.1, 0.75, 0.35) * ring * 0.45;
// Vignette
col *= 1.0 - 0.35 * dot(uv, uv);
// Tonemap + gamma
col = 1.0 - exp(-col * 1.15);
col = pow(col, vec3(0.4545));
return col;
}
void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
vec3 col = render(fragCoord);
fragColor = vec4(col, 1.0);
}