Code viewer for World: Ray Tracing
'use strict';

// ── Shared vertex shader ──────────────────────────────────────────────────
const VERT_SRC = `#version 300 es
in  vec2 aPosition;
out vec2 vUV;
void main() {
    vUV         = aPosition * 0.5 + 0.5;
    gl_Position = vec4(aPosition, 0.0, 1.0);
}`;

// ── Path-tracing fragment shader ──────────────────────────────────────────
const TRACE_SRC = `#version 300 es
precision highp float;
precision highp int;

in  vec2 vUV;
out vec4 fragColor;

uniform vec2      uResolution;  // render FBO resolution
uniform sampler2D uPreviousFrame;
uniform int       uFrameIndex;  // 1-based; used for running-mean weight

#define SAMPLES  2
#define MAXDEPTH 5
#define PI       3.14159265359
#define DIFF 0
#define SPEC 1
#define REFR 2
#define NUM_SPHERES 9

// PCG hash — all integer arithmetic, no float precision loss at large frame counts
uint rng_state;
float rand() {
    rng_state = rng_state * 747796405u + 2891336453u;
    uint word = ((rng_state >> ((rng_state >> 28u) + 4u)) ^ rng_state) * 277803737u;
    word = (word >> 22u) ^ word;
    return float(word) / 4294967296.0;
}

struct Ray    { vec3 o, d; };
struct Sphere { float r; vec3 p, e, c; int refl; };

Sphere spheres[NUM_SPHERES];
void initSpheres() {
    spheres[0] = Sphere(1e5,  vec3(-1e5+1.,     40.8,  81.6),    vec3(0.), vec3(.75,.25,.25), DIFF);
    spheres[1] = Sphere(1e5,  vec3( 1e5+99.,    40.8,  81.6),    vec3(0.), vec3(.25,.25,.75), DIFF);
    spheres[2] = Sphere(1e5,  vec3(50.,         40.8, -1e5),     vec3(0.), vec3(.75),          DIFF);
    spheres[3] = Sphere(1e5,  vec3(50.,         40.8,  1e5+170.),vec3(0.), vec3(0.),           DIFF);
    spheres[4] = Sphere(1e5,  vec3(50.,        -1e5,   81.6),    vec3(0.), vec3(.75),          DIFF);
    spheres[5] = Sphere(1e5,  vec3(50.,      1e5+81.6, 81.6),    vec3(0.), vec3(.75),          DIFF);
    spheres[6] = Sphere(16.5, vec3(27.,         16.5,  47.),     vec3(0.), vec3(1.),            SPEC);
    spheres[7] = Sphere(16.5, vec3(73.,         16.5,  78.),     vec3(0.), vec3(.7, 1., .9),   REFR);
    spheres[8] = Sphere(600., vec3(50.,        681.33,  81.6),   vec3(12.),vec3(0.),           DIFF);
}

float hitSphere(Sphere s, Ray r) {
    vec3  op  = s.p - r.o;
    float b   = dot(op, r.d);
    float det = b*b - dot(op, op) + s.r*s.r;
    if (det < 0.) return 0.;
    det = sqrt(det);
    float t = b - det;
    if (t > 1e-3) return t;
    t = b + det;
    return t > 1e-3 ? t : 0.;
}

int hitScene(Ray r, out float t, out Sphere s, int avoid) {
    int id = -1;
    t = 1e5;
    s = spheres[0];
    for (int i = 0; i < NUM_SPHERES; ++i) {
        float d = hitSphere(spheres[i], r);
        if (i != avoid && d > 0. && d < t) { t = d; id = i; s = spheres[i]; }
    }
    return id;
}

// Cosine-weighted hemisphere sample aligned to direction d
vec3 jitter(vec3 d, float phi, float sina, float cosa) {
    vec3 w = normalize(d), u = normalize(cross(w.yzx, w)), v = cross(w, u);
    return (u*cos(phi) + v*sin(phi)) * sina + w * cosa;
}

vec3 radiance(Ray r) {
    vec3 acc = vec3(0.), mask = vec3(1.);
    int id = -1;
    for (int depth = 0; depth < MAXDEPTH; ++depth) {
        float t; Sphere obj;
        if ((id = hitScene(r, t, obj, id)) < 0) break;
        vec3 x  = r.o + r.d * t;
        vec3 n  = normalize(x - obj.p);
        vec3 nl = n * sign(-dot(n, r.d));

        acc += mask * obj.e;

        if (obj.refl == DIFF) {
            float r2 = rand();
            r = Ray(x, jitter(nl, 2.*PI*rand(), sqrt(r2), sqrt(1.-r2)));
            mask *= obj.c;
        } else if (obj.refl == SPEC) {
            r = Ray(x, reflect(r.d, n));
            mask *= obj.c;
        } else {
            float a = dot(n, r.d), ddn = abs(a);
            float nc = 1., nt = 1.5, nnt = mix(nc/nt, nt/nc, float(a > 0.));
            float cos2t = 1. - nnt*nnt*(1.-ddn*ddn);
            r = Ray(x, reflect(r.d, n));
            if (cos2t > 0.) {
                vec3  tdir = normalize(r.d*nnt + sign(a)*n*(ddn*nnt+sqrt(cos2t)));
                float R0   = (nt-nc)*(nt-nc)/((nt+nc)*(nt+nc));
                float c    = 1. - mix(ddn, abs(dot(tdir, n)), float(a > 0.));
                float Re   = R0+(1.-R0)*c*c*c*c*c, P = .25+.5*Re, RP = Re/P, TP = (1.-Re)/(1.-P);
                if (rand() < P) { mask *= RP; }
                else            { mask *= obj.c * TP; r = Ray(x, tdir); }
            }
        }
    }
    return acc;
}

void main() {
    initSpheres();

    // Integer-based seed — decorrelated per pixel and per frame, no float precision decay
    rng_state = uint(gl_FragCoord.x) * 1973u
              + uint(gl_FragCoord.y) * 9277u
              + uint(uFrameIndex)    * 26699u;
    rng_state ^= rng_state >> 16u;
    rand(); // advance once to escape the low-entropy initial state

    vec2 uv = (vUV - 0.5) * 2.0;

    vec3 camPos = vec3(50., 40.8, 169.);
    vec3 cz = normalize(vec3(50., 40., 81.6) - camPos);
    vec3 cx = vec3(1., 0., 0.);
    vec3 cy = normalize(cross(cx, cz)); cx = cross(cz, cy);

    vec3 color = vec3(0.);
    for (int i = 0; i < SAMPLES; ++i) {
        color += radiance(Ray(camPos,
            normalize(0.53135 * (uResolution.x/uResolution.y * uv.x * cx
                                 + uv.y * cy) + cz)));
    }
    color /= float(SAMPLES);

    // ── Correct progressive accumulation: Welford's incremental mean ─────
    // Numerically stable for arbitrarily large frame counts.
    // Avoids multiplying the accumulated value by n (which overflows RGBA16F
    // once n grows beyond a few thousand).
    // n=1 → fragColor = acc + (color - acc) = color  (first frame, acc is 0)
    float n   = float(uFrameIndex);
    vec3  acc = texture(uPreviousFrame, vUV).rgb;
    fragColor = vec4(acc + (color - acc) / n, 1.0);
}`;

// ── Tone-mapping / gamma post-process ────────────────────────────────────
const POST_SRC = `#version 300 es
precision highp float;

in  vec2 vUV;
out vec4 fragColor;

uniform sampler2D uTexture;

vec3 ACESFilm(vec3 x) {
    float a=2.51, b=0.03, c=2.43, d=0.59, e=0.14;
    return clamp((x*(a*x+b))/(x*(c*x+d)+e), 0.0, 1.0);
}

void main() {
    vec3 col = texture(uTexture, vUV).rgb;
    col = ACESFilm(col);
    col = pow(col, vec3(1.0/2.2));
    fragColor = vec4(col, 1.0);
}`;

// ── WebGL 2 renderer ──────────────────────────────────────────────────────
function initRenderer() {
    // Canvas setup
    const canvas = document.createElement('canvas');
    Object.assign(canvas.style, {
        display: 'block', position: 'fixed',
        top: '0', left: '0', width: '100%', height: '100%'
    });
    document.body.style.cssText = 'margin:0;overflow:hidden;background:#000';
    document.body.appendChild(canvas);

    const displayW = window.innerWidth;
    const displayH = window.innerHeight;
    const renderW  = Math.floor(displayW / 1.5);
    const renderH  = Math.floor(displayH / 1.5);
    canvas.width   = displayW;
    canvas.height  = displayH;

    const gl = canvas.getContext('webgl2', { alpha: false, antialias: false });
    if (!gl) { alert('WebGL 2 is not supported in this browser.'); return; }
    if (!gl.getExtension('EXT_color_buffer_float')) {
        alert('EXT_color_buffer_float not available — float framebuffers unsupported.'); return;
    }

    // ── Shader helpers ───────────────────────────────────────────────────
    function compileShader(type, src) {
        const sh = gl.createShader(type);
        gl.shaderSource(sh, src);
        gl.compileShader(sh);
        if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS))
            throw new Error('Shader compile error:\n' + gl.getShaderInfoLog(sh));
        return sh;
    }

    function createProgram(vSrc, fSrc) {
        const prog = gl.createProgram();
        gl.attachShader(prog, compileShader(gl.VERTEX_SHADER,   vSrc));
        gl.attachShader(prog, compileShader(gl.FRAGMENT_SHADER, fSrc));
        gl.linkProgram(prog);
        if (!gl.getProgramParameter(prog, gl.LINK_STATUS))
            throw new Error('Program link error:\n' + gl.getProgramInfoLog(prog));
        return prog;
    }

    // ── Geometry: full-screen triangle strip ─────────────────────────────
    const quadBuf = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
    gl.bufferData(gl.ARRAY_BUFFER,
        new Float32Array([-1,-1,  1,-1,  -1,1,  1,1]), gl.STATIC_DRAW);

    function createVAO(prog) {
        const vao = gl.createVertexArray();
        gl.bindVertexArray(vao);
        gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
        const loc = gl.getAttribLocation(prog, 'aPosition');
        gl.enableVertexAttribArray(loc);
        gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
        gl.bindVertexArray(null);
        return vao;
    }

    // ── RGBA16F ping-pong FBOs for HDR accumulation ───────────────────────
    function createFloatFBO(w, h) {
        const tex = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, tex);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, w, h, 0,
                      gl.RGBA, gl.HALF_FLOAT, null);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

        const fbo = gl.createFramebuffer();
        gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
        gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0,
                                gl.TEXTURE_2D, tex, 0);
        if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE)
            throw new Error('Framebuffer incomplete');
        gl.bindFramebuffer(gl.FRAMEBUFFER, null);
        return { fbo, tex };
    }

    // ── Build GPU programs ────────────────────────────────────────────────
    const traceProg = createProgram(VERT_SRC, TRACE_SRC);
    const postProg  = createProgram(VERT_SRC, POST_SRC);
    const traceVAO  = createVAO(traceProg);
    const postVAO   = createVAO(postProg);

    // Cache uniform locations — never call getUniformLocation per-frame
    const uTrace = {
        uPreviousFrame: gl.getUniformLocation(traceProg, 'uPreviousFrame'),
        uResolution:    gl.getUniformLocation(traceProg, 'uResolution'),
        uFrameIndex:    gl.getUniformLocation(traceProg, 'uFrameIndex'),
    };
    const uPost = {
        uTexture: gl.getUniformLocation(postProg, 'uTexture'),
    };

    // Two RGBA16F FBOs; initialise both to black
    const fbos = [createFloatFBO(renderW, renderH), createFloatFBO(renderW, renderH)];
    for (const { fbo } of fbos) {
        gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
        gl.clearColor(0, 0, 0, 0);
        gl.clear(gl.COLOR_BUFFER_BIT);
    }
    gl.bindFramebuffer(gl.FRAMEBUFFER, null);

    // ── Render loop ───────────────────────────────────────────────────────
    let frameIndex = 1;

    function frame(timestamp) {
        // Ping-pong: frameIndex=1 → write fbos[1], read fbos[0] (black init)
        //            frameIndex=2 → write fbos[0], read fbos[1], etc.
        const readIdx  = (frameIndex - 1) & 1;
        const writeIdx = frameIndex       & 1;
        const read     = fbos[readIdx];
        const write    = fbos[writeIdx];

        // Pass 1 — path trace into write FBO
        gl.bindFramebuffer(gl.FRAMEBUFFER, write.fbo);
        gl.viewport(0, 0, renderW, renderH);
        gl.useProgram(traceProg);
        gl.bindVertexArray(traceVAO);

        gl.activeTexture(gl.TEXTURE0);
        gl.bindTexture(gl.TEXTURE_2D, read.tex);
        gl.uniform1i(uTrace.uPreviousFrame, 0);
        gl.uniform2f(uTrace.uResolution,    renderW, renderH);
        gl.uniform1i(uTrace.uFrameIndex,    frameIndex);

        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

        // Pass 2 — tone-map write FBO to screen
        gl.bindFramebuffer(gl.FRAMEBUFFER, null);
        gl.viewport(0, 0, displayW, displayH);
        gl.useProgram(postProg);
        gl.bindVertexArray(postVAO);

        gl.activeTexture(gl.TEXTURE0);
        gl.bindTexture(gl.TEXTURE_2D, write.tex);
        gl.uniform1i(uPost.uTexture, 0);

        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

        frameIndex++;
        requestAnimationFrame(frame);
    }

    requestAnimationFrame(frame);
}

if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initRenderer);
} else {
    initRenderer();
}