Documentation

Shader Example: Tunnel 003

Your content here

Header

Your content here

Snippets (Version 1)

Shadertoy

// Shadertoy - Raymarched Neon Tunnel
// Actual SDF tunnel with bend, twist, ribs, glow, and forward motion.

#define MAX_STEPS 120
#define MAX_DIST  80.0
#define SURF_DIST 0.0015
#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)
{
    p = fract(p * vec2(123.34, 345.45));
    p += dot(p, p + 34.345);
    return fract(p.x * p.y);
}

float noise(vec3 x)
{
    vec3 p = floor(x);
    vec3 f = fract(x);
    f = f * f * (3.0 - 2.0 * f);

    float n = p.x + p.y * 57.0 + 113.0 * p.z;

    float a = hash11(n +   0.0);
    float b = hash11(n +   1.0);
    float c = hash11(n +  57.0);
    float d = hash11(n +  58.0);
    float e = hash11(n + 113.0);
    float f1= hash11(n + 114.0);
    float g = hash11(n + 170.0);
    float h = hash11(n + 171.0);

    return mix(
        mix(mix(a, b, f.x), mix(c, d, f.x), f.y),
        mix(mix(e, f1,f.x), mix(g, h, f.x), f.y),
        f.z
    );
}

float fbm(vec3 p)
{
    float v = 0.0;
    float a = 0.5;
    for (int i = 0; i < 5; i++)
    {
        v += a * noise(p);
        p = p * 2.02 + vec3(1.7, -0.9, 1.3);
        a *= 0.5;
    }
    return v;
}

vec3 palette(float t)
{
    vec3 a = vec3(0.5);
    vec3 b = vec3(0.5);
    vec3 c = vec3(1.0);
    vec3 d = vec3(0.00, 0.18, 0.38);
    return a + b * cos(TAU * (c * t + d));
}

// Tunnel centerline offset as a function of z.
// The world bends because the tunnel's center moves through space.
vec2 tunnelCenter(float z)
{
    return vec2(
        0.9 * sin(z * 0.18) + 0.35 * sin(z * 0.51),
        0.7 * cos(z * 0.16) + 0.25 * cos(z * 0.43)
    );
}

vec2 localTubeFrame(vec3 p)
{
    vec2 c = tunnelCenter(p.z);
    vec2 q = p.xy - c;

    float tw = 0.55 * sin(p.z * 0.23) + 0.15 * sin(p.z * 0.71);
    q *= rot(tw);

    return q;
}

// SDF for the tunnel wall. Negative inside the solid wall region near the shell.
// We raymarch the shell itself, not just a cylinder.
float map(vec3 p)
{
    vec2 q = localTubeFrame(p);
    float r = length(q);
    float ang = atan(q.y, q.x);

    float baseRadius =
        1.10
        + 0.08 * sin(p.z * 0.70)
        + 0.05 * sin(p.z * 1.90 + sin(p.z * 0.2));

    float shell = abs(r - baseRadius) - 0.10;

    float ribs = sin(ang * 14.0 + p.z * 2.4);
    ribs = 0.5 + 0.5 * ribs;
    ribs = pow(ribs, 8.0);

    float lanes = sin(ang * 6.0 - p.z * 1.3);
    lanes = 0.5 + 0.5 * lanes;
    lanes = pow(lanes, 18.0);

    float detail = fbm(vec3(ang * 2.5, r * 3.0, p.z * 0.4));
    shell -= 0.025 * ribs;
    shell -= 0.035 * lanes;
    shell -= 0.015 * detail;

    return shell;
}

vec3 getNormal(vec3 p)
{
    vec2 e = vec2(0.0015, 0.0);
    float d = map(p);
    vec3 n = d - vec3(
        map(p - e.xyy),
        map(p - e.yxy),
        map(p - e.yyx)
    );
    return normalize(n);
}

float softShadow(vec3 ro, vec3 rd, float mint, float maxt, float k)
{
    float res = 1.0;
    float t = mint;

    for (int i = 0; i < 40; i++)
    {
        float h = map(ro + rd * t);
        res = min(res, k * h / t);
        t += clamp(h, 0.02, 0.25);
        if (h < 0.001 || t > maxt) break;
    }

    return clamp(res, 0.0, 1.0);
}

float ao(vec3 p, vec3 n)
{
    float occ = 0.0;
    float sca = 1.0;
    for (int i = 1; i <= 5; i++)
    {
        float h = 0.03 * float(i);
        float d = map(p + n * h);
        occ += (h - d) * sca;
        sca *= 0.7;
    }
    return clamp(1.0 - occ, 0.0, 1.0);
}

bool raymarch(vec3 ro, vec3 rd, out vec3 hitPos, out float tHit, out float glowAccum)
{
    float t = 0.0;
    glowAccum = 0.0;

    for (int i = 0; i < MAX_STEPS; i++)
    {
        vec3 p = ro + rd * t;
        float d = map(p);

        // accumulate near-surface glow
        glowAccum += 0.02 / (0.02 + abs(d) * abs(d) * 40.0);

        if (d < SURF_DIST)
        {
            hitPos = p;
            tHit = t;
            return true;
        }

        if (t > MAX_DIST) break;
        t += clamp(d, 0.006, 0.35);
    }

    hitPos = ro + rd * t;
    tHit = t;
    return false;
}

vec3 background(vec3 rd)
{
    float t = 0.5 + 0.5 * rd.y;
    vec3 col = mix(vec3(0.005, 0.008, 0.015), vec3(0.02, 0.03, 0.06), t);

    vec2 suv = rd.xy / max(0.2, abs(rd.z));
    vec2 gv = fract(suv * 28.0) - 0.5;
    vec2 id = floor(suv * 28.0);

    float n = hash21(id);
    float d = length(gv);
    float star = smoothstep(0.035, 0.0, d) * step(0.992, n);

    col += vec3(0.6, 0.8, 1.2) * star * 2.0;
    return col;
}

void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
    vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
    float time = iTime;

    // Forward speed through tunnel
    float speed = 8.0;
    float zTravel = time * speed;

    // Camera follows the tunnel centerline
    vec3 ro = vec3(tunnelCenter(zTravel), zTravel);

    // Look ahead down the tunnel
    float lookZ = zTravel + 1.5;
    vec3 target = vec3(tunnelCenter(lookZ), lookZ);

    // Camera wobble
    ro.xy += 0.03 * vec2(sin(time * 1.7), cos(time * 1.2));

    // Build camera basis
    vec3 forward = normalize(target - ro);
    vec3 right = normalize(cross(vec3(0.0, 1.0, 0.0), forward));
    vec3 up = cross(forward, right);

    vec3 rd = normalize(forward + uv.x * right + uv.y * up);

    vec3 hitPos;
    float tHit;
    float glowAccum;
    bool hit = raymarch(ro, rd, hitPos, tHit, glowAccum);

    vec3 col = background(rd);

    if (hit)
    {
        vec3 n = getNormal(hitPos);
        vec2 q = localTubeFrame(hitPos);
        float r = length(q);
        float ang = atan(q.y, q.x);

        float baseRadius =
            1.10
            + 0.08 * sin(hitPos.z * 0.70)
            + 0.05 * sin(hitPos.z * 1.90 + sin(hitPos.z * 0.2));

        float ribs = sin(ang * 14.0 + hitPos.z * 2.4);
        ribs = pow(0.5 + 0.5 * ribs, 8.0);

        float lanes = sin(ang * 6.0 - hitPos.z * 1.3);
        lanes = pow(0.5 + 0.5 * lanes, 18.0);

        float rings = sin(hitPos.z * 5.2);
        rings = pow(0.5 + 0.5 * rings, 20.0);

        float panelNoise = fbm(vec3(ang * 3.0, r * 4.0, hitPos.z * 0.6));

        float hue = hitPos.z * 0.035 + ang * 0.12 + panelNoise * 0.35;
        vec3 base = palette(hue);

        vec3 lightPos = ro + vec3(0.0, 0.0, 3.0);
        vec3 l = normalize(lightPos - hitPos);
        vec3 v = normalize(ro - hitPos);
        vec3 h = normalize(l + v);

        float diff = max(dot(n, l), 0.0);
        float spec = pow(max(dot(n, h), 0.0), 48.0);

        float sh = softShadow(hitPos + n * 0.01, l, 0.02, 6.0, 12.0);
        float occ = ao(hitPos, n);

        float fres = pow(1.0 - max(dot(n, v), 0.0), 3.0);

        vec3 emissive = vec3(0.0);
        emissive += palette(hue + 0.08) * ribs * 1.6;
        emissive += vec3(0.15, 0.85, 1.8) * lanes * 2.1;
        emissive += vec3(1.3, 0.2, 1.6) * rings * 0.8;
        emissive += base * panelNoise * 0.25;

        vec3 surf = base * (0.10 + 0.90 * diff * sh) * occ;
        surf += vec3(1.0) * spec * 0.9 * sh;
        surf += emissive;
        surf += base * fres * 0.35;

        // Distance fog toward neon
        float fog = 1.0 - exp(-tHit * 0.035);
        vec3 fogCol = palette(hitPos.z * 0.03 + 0.2) * 0.25;
        col = mix(surf, fogCol, fog);
    }

    // Volumetric-ish accumulated glow from near surfaces
    col += vec3(0.08, 0.25, 0.55) * glowAccum * 0.22;
    col += vec3(0.45, 0.10, 0.65) * glowAccum * 0.08;

    // Bright center boost / motion feel
    float center = 0.03 / (dot(uv, uv) + 0.04);
    col += vec3(0.08, 0.16, 0.30) * center;

    // Vignette
    float vig = smoothstep(1.4, 0.2, length(uv));
    col *= vig;

    // Tonemap + gamma
    col = 1.0 - exp(-col * 0.95);
    col = pow(col, vec3(0.92));

    fragColor = vec4(col, 1.0);
}

Variations

Version 2

With obstacles.

Shadertoy

// Shadertoy - Neon Tunnel + Chrome Drones + Floating Obstacles

#define MAX_STEPS 140
#define MAX_DIST  90.0
#define SURF_DIST 0.0015
#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)
{
    p = fract(p * vec2(123.34, 345.45));
    p += dot(p, p + 34.345);
    return fract(p.x * p.y);
}

vec2 hash22(vec2 p)
{
    float n = hash21(p);
    return vec2(n, hash21(p + 19.37));
}

float noise(vec3 x)
{
    vec3 p = floor(x);
    vec3 f = fract(x);
    f = f * f * (3.0 - 2.0 * f);

    float n = p.x + p.y * 57.0 + 113.0 * p.z;

    float a = hash11(n +   0.0);
    float b = hash11(n +   1.0);
    float c = hash11(n +  57.0);
    float d = hash11(n +  58.0);
    float e = hash11(n + 113.0);
    float f1= hash11(n + 114.0);
    float g = hash11(n + 170.0);
    float h = hash11(n + 171.0);

    return mix(
        mix(mix(a, b, f.x), mix(c, d, f.x), f.y),
        mix(mix(e, f1, f.x), mix(g, h, f.x), f.y),
        f.z
    );
}

float fbm(vec3 p)
{
    float v = 0.0;
    float a = 0.5;
    for (int i = 0; i < 5; i++)
    {
        v += a * noise(p);
        p = p * 2.02 + vec3(1.7, -0.9, 1.3);
        a *= 0.5;
    }
    return v;
}

vec3 palette(float t)
{
    vec3 a = vec3(0.5);
    vec3 b = vec3(0.5);
    vec3 c = vec3(1.0);
    vec3 d = vec3(0.00, 0.18, 0.38);
    return a + b * cos(TAU * (c * t + d));
}

vec2 tunnelCenter(float z)
{
    return vec2(
        0.9 * sin(z * 0.18) + 0.35 * sin(z * 0.51),
        0.7 * cos(z * 0.16) + 0.25 * cos(z * 0.43)
    );
}

vec2 localTubeFrame(vec3 p)
{
    vec2 c = tunnelCenter(p.z);
    vec2 q = p.xy - c;

    float tw = 0.55 * sin(p.z * 0.23) + 0.15 * sin(p.z * 0.71);
    q *= rot(tw);

    return q;
}

float sdBox(vec3 p, vec3 b)
{
    vec3 q = abs(p) - b;
    return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0);
}

float sdOcta(vec3 p, float s)
{
    p = abs(p);
    return (p.x + p.y + p.z - s) * 0.57735027;
}

float smin(float a, float b, float k)
{
    float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
    return mix(b, a, h) - k * h * (1.0 - h);
}

struct Res
{
    float d;
    float m;
};

Res makeRes(float d, float m)
{
    Res r;
    r.d = d;
    r.m = m;
    return r;
}

Res opU(Res a, Res b)
{
    if (a.d < b.d)
        return a;
    else
        return b;
}

Res mapScene(vec3 p)
{
    vec2 q = localTubeFrame(p);
    float r = length(q);
    float ang = atan(q.y, q.x);

    float baseRadius =
        1.10
        + 0.08 * sin(p.z * 0.70)
        + 0.05 * sin(p.z * 1.90 + sin(p.z * 0.2));

    float shell = abs(r - baseRadius) - 0.10;

    float ribs = sin(ang * 14.0 + p.z * 2.4);
    ribs = pow(0.5 + 0.5 * ribs, 8.0);

    float lanes = sin(ang * 6.0 - p.z * 1.3);
    lanes = pow(0.5 + 0.5 * lanes, 18.0);

    float detail = fbm(vec3(ang * 2.5, r * 3.0, p.z * 0.4));
    shell -= 0.025 * ribs;
    shell -= 0.035 * lanes;
    shell -= 0.015 * detail;

    Res res = makeRes(shell, 1.0); // 1=tunnel

    // Repeating objects down the tunnel in z-cells
    float cell = floor((p.z + 3.0) / 4.0);
    float localZ = mod(p.z + 3.0, 4.0) - 2.0;

    for (int i = -1; i <= 1; i++)
    {
        float cidx = cell + float(i);
        float zc = cidx * 4.0;

        vec2 rnd = hash22(vec2(cidx, 7.13));
        vec2 rnd2 = hash22(vec2(cidx, 13.77));

        float a0 = rnd.x * TAU + p.z * 0.03;
        float rad = 0.55 + 0.18 * sin(cidx * 1.7);
        vec2 off = vec2(cos(a0), sin(a0)) * rad;

        vec3 lp = p;
        lp.xy -= tunnelCenter(zc);
        lp.xy = rot(-(0.55 * sin(zc * 0.23) + 0.15 * sin(zc * 0.71))) * lp.xy;
        lp.xy -= off;
        lp.z -= zc;

        // Chrome drone: spinning octahedron with a small body blend
        vec3 dp = lp;
        dp.xy *= rot(iTime * 1.6 + cidx);
        dp.xz *= rot(iTime * 1.1 + rnd.y * 4.0);

        float droneCore = sdOcta(dp, 0.34);
        float droneBody = sdBox(dp, vec3(0.08, 0.08, 0.28));
        float drone = smin(droneCore, droneBody, 0.14);
        drone -= 0.02 * sin(18.0 * atan(dp.y, dp.x) + iTime * 3.0);

        res = opU(res, makeRes(drone, 2.0)); // 2=chrome drone

        // Floating obstacle: glowing cube / gate fragment
        vec3 op = p;
        op.xy -= tunnelCenter(zc + 1.8);
        op.xy = rot(-(0.55 * sin((zc + 1.8) * 0.23) + 0.15 * sin((zc + 1.8) * 0.71))) * op.xy;

        float oa = rnd2.x * TAU + iTime * (0.4 + rnd2.y);
        vec2 ooff = vec2(cos(oa), sin(oa)) * (0.28 + 0.18 * rnd2.x);
        op.xy -= ooff;
        op.z -= (zc + 1.8);

        op.xy *= rot(iTime * 0.8 + cidx * 2.0);
        op.yz *= rot(iTime * 0.6 + cidx * 0.7);

        float obstacle = sdBox(op, vec3(0.18 + 0.08 * rnd2.y));
        res = opU(res, makeRes(obstacle, 3.0)); // 3=obstacle
    }

    return res;
}

vec3 getNormal(vec3 p)
{
    vec2 e = vec2(0.0015, 0.0);
    float d = mapScene(p).d;
    vec3 n = d - vec3(
        mapScene(p - e.xyy).d,
        mapScene(p - e.yxy).d,
        mapScene(p - e.yyx).d
    );
    return normalize(n);
}

float softShadow(vec3 ro, vec3 rd, float mint, float maxt, float k)
{
    float res = 1.0;
    float t = mint;

    for (int i = 0; i < 40; i++)
    {
        float h = mapScene(ro + rd * t).d;
        res = min(res, k * h / t);
        t += clamp(h, 0.02, 0.25);
        if (h < 0.001 || t > maxt) break;
    }

    return clamp(res, 0.0, 1.0);
}

float ao(vec3 p, vec3 n)
{
    float occ = 0.0;
    float sca = 1.0;
    for (int i = 1; i <= 5; i++)
    {
        float h = 0.03 * float(i);
        float d = mapScene(p + n * h).d;
        occ += (h - d) * sca;
        sca *= 0.7;
    }
    return clamp(1.0 - occ, 0.0, 1.0);
}

bool raymarch(vec3 ro, vec3 rd, out vec3 hitPos, out float tHit, out float glowAccum, out float matId)
{
    float t = 0.0;
    glowAccum = 0.0;
    matId = 0.0;

    for (int i = 0; i < MAX_STEPS; i++)
    {
        vec3 p = ro + rd * t;
        Res h = mapScene(p);

        glowAccum += 0.02 / (0.02 + abs(h.d) * abs(h.d) * 40.0);

        if (h.d < SURF_DIST)
        {
            hitPos = p;
            tHit = t;
            matId = h.m;
            return true;
        }

        if (t > MAX_DIST) break;
        t += clamp(h.d, 0.006, 0.35);
    }

    hitPos = ro + rd * t;
    tHit = t;
    return false;
}

vec3 background(vec3 rd)
{
    float t = 0.5 + 0.5 * rd.y;
    vec3 col = mix(vec3(0.005, 0.008, 0.015), vec3(0.02, 0.03, 0.06), t);

    vec2 suv = rd.xy / max(0.2, abs(rd.z));
    vec2 gv = fract(suv * 28.0) - 0.5;
    vec2 id = floor(suv * 28.0);

    float n = hash21(id);
    float d = length(gv);
    float star = smoothstep(0.035, 0.0, d) * step(0.992, n);

    col += vec3(0.6, 0.8, 1.2) * star * 2.0;
    return col;
}

vec3 tunnelEnv(vec3 p, vec3 rd)
{
    vec2 q = localTubeFrame(p + rd * 1.8);
    float ang = atan(q.y, q.x);
    float z = p.z + rd.z * 2.0;
    float phase = z * 0.04 + ang * 0.12;

    vec3 env = palette(phase) * 0.25;

    float stripes = pow(0.5 + 0.5 * sin(ang * 12.0 + z * 2.8), 12.0);
    float rings   = pow(0.5 + 0.5 * sin(z * 6.0), 20.0);

    env += vec3(0.10, 0.45, 1.10) * stripes;
    env += vec3(1.10, 0.15, 1.30) * rings * 0.4;

    return env;
}

vec3 shadeTunnel(vec3 ro, vec3 rd, vec3 p, vec3 n, float tHit)
{
    vec2 q = localTubeFrame(p);
    float r = length(q);
    float ang = atan(q.y, q.x);

    float panelNoise = fbm(vec3(ang * 3.0, r * 4.0, p.z * 0.6));
    float hue = p.z * 0.035 + ang * 0.12 + panelNoise * 0.35;
    vec3 base = palette(hue);

    float ribs = pow(0.5 + 0.5 * sin(ang * 14.0 + p.z * 2.4), 8.0);
    float lanes = pow(0.5 + 0.5 * sin(ang * 6.0 - p.z * 1.3), 18.0);
    float rings = pow(0.5 + 0.5 * sin(p.z * 5.2), 20.0);

    vec3 lightPos = ro + vec3(0.0, 0.0, 3.0);
    vec3 l = normalize(lightPos - p);
    vec3 v = normalize(ro - p);
    vec3 h = normalize(l + v);

    float diff = max(dot(n, l), 0.0);
    float spec = pow(max(dot(n, h), 0.0), 48.0);
    float sh = softShadow(p + n * 0.01, l, 0.02, 6.0, 12.0);
    float occ = ao(p, n);
    float fres = pow(1.0 - max(dot(n, v), 0.0), 3.0);

    vec3 emissive = vec3(0.0);
    emissive += palette(hue + 0.08) * ribs * 1.6;
    emissive += vec3(0.15, 0.85, 1.8) * lanes * 2.1;
    emissive += vec3(1.3, 0.2, 1.6) * rings * 0.8;
    emissive += base * panelNoise * 0.25;

    vec3 surf = base * (0.10 + 0.90 * diff * sh) * occ;
    surf += vec3(1.0) * spec * 0.9 * sh;
    surf += emissive;
    surf += base * fres * 0.35;

    float fog = 1.0 - exp(-tHit * 0.035);
    vec3 fogCol = palette(p.z * 0.03 + 0.2) * 0.25;
    return mix(surf, fogCol, fog);
}

vec3 shadeDrone(vec3 ro, vec3 rd, vec3 p, vec3 n, float tHit)
{
    vec3 v = normalize(ro - p);
    vec3 refl = reflect(-v, n);

    vec3 env = tunnelEnv(p, refl);
    vec3 lightPos = ro + vec3(0.0, 0.0, 3.0);
    vec3 l = normalize(lightPos - p);
    vec3 h = normalize(l + v);

    float diff = max(dot(n, l), 0.0);
    float spec = pow(max(dot(n, h), 0.0), 96.0);
    float fres = pow(1.0 - max(dot(n, v), 0.0), 5.0);
    float occ = ao(p, n);

    float seam = pow(0.5 + 0.5 * sin(20.0 * atan(n.y, n.x) + p.z * 8.0), 24.0);

    vec3 col = vec3(0.03) + env * 1.3;
    col += vec3(1.0) * spec * 0.8;
    col += vec3(0.10, 0.90, 1.60) * seam * 0.9;
    col += diff * vec3(0.08);
    col = mix(col, vec3(1.0), fres * 0.25);
    col *= occ;

    float fog = 1.0 - exp(-tHit * 0.04);
    return mix(col, palette(p.z * 0.03) * 0.18, fog);
}

vec3 shadeObstacle(vec3 ro, vec3 rd, vec3 p, vec3 n, float tHit)
{
    vec3 v = normalize(ro - p);
    vec3 lightPos = ro + vec3(0.0, 0.0, 3.0);
    vec3 l = normalize(lightPos - p);
    vec3 h = normalize(l + v);

    float diff = max(dot(n, l), 0.0);
    float spec = pow(max(dot(n, h), 0.0), 32.0);
    float fres = pow(1.0 - max(dot(n, v), 0.0), 4.0);
    float occ = ao(p, n);

    float edge = pow(1.0 - abs(dot(n, normalize(vec3(1.0, 1.0, 1.0)))), 8.0);
    float pulse = pow(0.5 + 0.5 * sin(p.z * 8.0 - iTime * 8.0), 10.0);

    vec3 base = vec3(0.04, 0.03, 0.05);
    vec3 emi = vec3(1.2, 0.15, 1.5) * edge + vec3(0.1, 0.7, 1.5) * pulse;

    vec3 col = base * (0.15 + 0.85 * diff) * occ;
    col += vec3(1.0) * spec * 0.6;
    col += emi;
    col += fres * vec3(0.4, 0.1, 0.7);

    float fog = 1.0 - exp(-tHit * 0.04);
    return mix(col, palette(p.z * 0.025) * 0.20, fog);
}

void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
    vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
    float time = iTime;

    float speed = 8.0;
    float zTravel = time * speed;

    vec3 ro = vec3(tunnelCenter(zTravel), zTravel);
    float lookZ = zTravel + 1.5;
    vec3 target = vec3(tunnelCenter(lookZ), lookZ);

    ro.xy += 0.03 * vec2(sin(time * 1.7), cos(time * 1.2));

    vec3 forward = normalize(target - ro);
    vec3 right = normalize(cross(vec3(0.0, 1.0, 0.0), forward));
    vec3 up = cross(forward, right);

    vec3 rd = normalize(forward + uv.x * right + uv.y * up);

    vec3 hitPos;
    float tHit;
    float glowAccum;
    float matId;
    bool hit = raymarch(ro, rd, hitPos, tHit, glowAccum, matId);

    vec3 col = background(rd);

    if (hit)
    {
        vec3 n = getNormal(hitPos);

        if (matId < 1.5)
            col = shadeTunnel(ro, rd, hitPos, n, tHit);
        else if (matId < 2.5)
            col = shadeDrone(ro, rd, hitPos, n, tHit);
        else
            col = shadeObstacle(ro, rd, hitPos, n, tHit);
    }

    col += vec3(0.08, 0.25, 0.55) * glowAccum * 0.22;
    col += vec3(0.45, 0.10, 0.65) * glowAccum * 0.08;

    float center = 0.03 / (dot(uv, uv) + 0.04);
    col += vec3(0.08, 0.16, 0.30) * center;

    float vig = smoothstep(1.4, 0.2, length(uv));
    col *= vig;

    col = 1.0 - exp(-col * 0.95);
    col = pow(col, vec3(0.92));

    fragColor = vec4(col, 1.0);
}