Documentation

Shader Example: Black Hole

<div class="playground_container">

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.82 to about 1.0
  • Stronger lensing: increase bend = 0.09 / r2
  • Thicker disk: increase if (py < 0.22) and reduce exp(-y * 28.0)
  • More dramatic glow: increase the ring and glow multipliers
  • 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);
}