oortic.net
screenshot
WASD • space
code listing
<!DOCTYPE html>
<html>
<head>
    <title>Infinite Staircase: Dithered</title>
    <style>
        body { margin: 0; overflow: hidden; background: #000; font-family: monospace; user-select: none; }
        #ui { 
            position: absolute; top: 20px; width: 100%; pointer-events: none; 
            text-align: center; color: #fff; text-shadow: 0 2px 4px black; 
            display: flex; flex-direction: column; gap: 8px; mix-blend-mode: overlay;
        }
        .title { font-size: 24px; font-weight: bold; letter-spacing: 6px; text-transform: uppercase; }
        .sub { font-size: 11px; opacity: 0.8; letter-spacing: 1px; }
    </style>
</head>
<body>
    <div id="ui">
        <div class="title">The Descent</div>
        <div class="sub">WASD MOVE &bull; SPACE JUMP</div>
    </div>
    <canvas id="glcanvas"></canvas>

<script>
// --- 1. POLYGLOT SDF (GLSL + JS Compatible) ---
const SDF_LOGIC = `
float map(vec3 p) {
    float minDist = 1000.0;
    
    float cx = floor(p.x);
    float cz = floor(p.z);

    // Grid Search
    for(float x = -1.0; x <= 1.0; x += 1.0) {
        for(float z = -1.0; z <= 1.0; z += 1.0) {
            
            float idx = cx + x;
            float idz = cz + z;
            float sum = idx + idz;
            
            float parity = mod(sum, 2.0);
            float sx = 0.0; float sz = 0.0;
            
            if(parity < 0.5) { sx = 0.99; sz = 0.49; } 
            else             { sx = 0.49; sz = 0.99; }

            float cellHeight = sum * -0.25;

            float cenX = idx + 0.5;
            float cenY = cellHeight; 
            float cenZ = idz + 0.5;

            float dx = abs(p.x - cenX) - sx;
            float dy = abs(p.y - cenY) - 0.5;
            float dz = abs(p.z - cenZ) - sz;

            float mx = max(dx, 0.0);
            float my = max(dy, 0.0);
            float mz = max(dz, 0.0);
            
            float inside = min(max(dx, max(dy, dz)), 0.0);
            float d = length(vec3(mx, my, mz)) + inside - 0.02;

            if(d < minDist) minDist = d;
        }
    }
    return minDist;
}
`;
</script>

<script id="vs" type="x-shader/x-vertex">
    attribute vec2 position;
    void main() { gl_Position = vec4(position, 0.0, 1.0); }
</script>

<script id="fs" type="x-shader/x-fragment">
    precision highp float;
    uniform vec2 u_res;
    uniform vec3 u_camPos, u_camTgt;
    uniform float u_time;

    {{SDF_LOGIC}}

    // Standard Gold Noise Hash (Returns float 0.0 - 1.0)
    float hash(vec2 xy) {
        return fract(sin(dot(xy, vec2(12.9898, 78.233))) * 43758.5453123);
    }

    vec3 calcNormal(vec3 p) {
        float minDist = 1000.0;
        vec3 normal = vec3(0.0, 1.0, 0.0);
        float cx = floor(p.x);
        float cz = floor(p.z);

        for(float x = -1.0; x <= 1.0; x += 1.0) {
            for(float z = -1.0; z <= 1.0; z += 1.0) {
                float idx = cx + x;
                float idz = cz + z;
                float sum = idx + idz;
                
                // Safe selection without ternary nesting
                float parity = mod(sum, 2.0);
                float sx = 0.0; float sz = 0.0;
                if(parity < 0.5) { sx = 0.99; sz = 0.49; } else { sx = 0.49; sz = 0.99; }

                float cellHeight = sum * -0.25;
                vec3 center = vec3(idx + 0.5, cellHeight, idz + 0.5);
                vec3 halfSize = vec3(sx, 0.5, sz);
                
                vec3 localP = p - center;
                vec3 dVec = abs(localP) - halfSize;
                float d = length(max(dVec, 0.0)) + min(max(dVec.x, max(dVec.y, dVec.z)), 0.0) - 0.02;

                if(d < minDist) {
                    minDist = d;
                    vec3 clamped = clamp(localP, -halfSize, halfSize);
                    vec3 diff = localP - clamped;
                    normal = normalize(diff);
                }
            }
        }
        return normal;
    }

    float calcAO(vec3 p, vec3 n) {
        float occ = 0.0;
        float sca = 1.0;
        for(int i=0; i<5; i++) {
            float h = 0.02 + 0.05 * float(i);
            float d = map(p + h * n);
            occ += (h - d) * sca;
            sca *= 0.9;
        }
        return clamp(1.0 - 2.0 * occ, 0.0, 1.0);
    }

    void main() {
        vec2 uv = (gl_FragCoord.xy - 0.5 * u_res.xy) / u_res.y;
        
        // Generate noise
        float noise = hash(uv * 10.0 + u_time);

        vec3 ro = u_camPos;
        vec3 cw = normalize(u_camTgt - ro);
        vec3 cp = vec3(0,1,0);
        vec3 cu = normalize(cross(cw,cp));
        vec3 cv = normalize(cross(cu,cw));
        vec3 rd = normalize(uv.x * cu + uv.y * cv + 1.2 * cw); 

        float t = 0.0;
        float dist = 0.0;
        const float MAX_STEPS = 256.0; 
        const float MAX_DIST = 100.0;
        
        for(float i=0.0; i<MAX_STEPS; i++) {
            vec3 p = ro + rd*t;
            dist = map(p);
            if(dist < 0.001 || t > MAX_DIST) break;
            t += dist;
        }

        vec3 col = vec3(0.0);

        if(dist < 0.01) {
            vec3 p = ro + rd*t;
            vec3 n = calcNormal(p);
            float ao = calcAO(p, n);

            vec3 mate = vec3(0.2, 0.21, 0.23); 
            vec3 sunDir = normalize(vec3(0.5, 0.8, -0.5));
            float sunDif = clamp(dot(n, sunDir), 0.0, 1.0);
            
            // Soft Shadow with Noise Jitter
            float shadow = 1.0;
            float shT = 0.02 + 0.05 * noise; 
            
            for(int i=0; i<16; i++) {
                float h = map(p + sunDir * shT);
                if(h < 0.001) { shadow = 0.0; break; }
                shadow = min(shadow, 8.0 * h / shT);
                shT += h;
                if(shT > 5.0) break;
            }
            
            vec3 lin = vec3(0.0);
            lin += 1.5 * sunDif * vec3(1.0, 0.9, 0.8) * shadow;
            lin += 0.3 * vec3(0.5, 0.6, 0.8) * n.y; 
            
            col = mate * lin * ao;

            float fog = 1.0 - exp(-0.18 * t);
            vec3 fogCol = vec3(0.5, 0.6, 0.7);
            col = mix(col, fogCol, fog);
        } else {
            vec3 sky = mix(vec3(0.5, 0.6, 0.7), vec3(0.8, 0.85, 0.9), uv.y*0.5+0.5);
            //float t_sky1 = smoothstep(0.01, 0.05, dist);
            //float t_sky2 = smoothstep(5.0, 6.0, t);
            //col = mix(vec3(0.0), sky, t_sky1*t_sky2);
            col = sky;
        }

        //col = pow(col, vec3(0.4545)); 

        col = col * 5.0;
        col = col / (col + 1.0);
        
        // Final Dither
        col += (noise - 0.5) * 0.03;

        gl_FragColor = vec4(col, 1.0);
    }
</script>

<script>
// --- 2. JS MATH HELPERS ---
const vec3 = (x,y,z) => ({x,y,z});
const length = (v) => Math.sqrt(v.x*v.x + (v.y*v.y || 0) + (v.z*v.z || 0)); 
const max = Math.max; const min = Math.min; const floor = Math.floor; const abs = Math.abs;
const dot = (a,b) => a.x*b.x + a.y*b.y + a.z*b.z;
const mod = (n, m) => ((n % m) + m) % m;
const clamp = (v, mn, mx) => Math.max(mn, Math.min(mx, v));

// --- 3. COMPILER ---
const JS_SOURCE = SDF_LOGIC
    .replace(/(float|vec2|vec3|int)\s/g, 'let ') 
    .replace(/let\s+map\s*\(\s*let\s+p\s*\)/, 'function map(p)');
const map = new Function('p', JS_SOURCE + ' return map(p);');

// --- WEBGL SETUP ---
const cvs = document.getElementById('glcanvas');
const gl = cvs.getContext('webgl');
const pid = gl.createProgram();

// Helper to check for errors
function createShader(type, source) {
    const s = gl.createShader(type);
    gl.shaderSource(s, source);
    gl.compileShader(s);
    if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
        console.error(gl.getShaderInfoLog(s));
        return null;
    }
    return s;
}

const vs = createShader(gl.VERTEX_SHADER, document.getElementById('vs').text);
const fsSrc = document.getElementById('fs').text.replace('{{SDF_LOGIC}}', SDF_LOGIC);
const fs = createShader(gl.FRAGMENT_SHADER, fsSrc);

if(vs && fs) {
    gl.attachShader(pid, vs); 
    gl.attachShader(pid, fs); 
    gl.linkProgram(pid); 
    if (!gl.getProgramParameter(pid, gl.LINK_STATUS)) {
        console.error(gl.getProgramInfoLog(pid));
    }
    gl.useProgram(pid);
}

gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, -1,1, 1,-1, 1,1]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);

const loc = { res: gl.getUniformLocation(pid, "u_res"), cam: gl.getUniformLocation(pid, "u_camPos"), tgt: gl.getUniformLocation(pid, "u_camTgt"), time: gl.getUniformLocation(pid, "u_time") };

// --- VERLET PHYSICS ---
let player = { 
    x: 1.5, y: 0.5, z: 1.5, 
    px: 1.5, py: 0.5, pz: 1.5, 
    h: 0.25, r: 0.01,
    vx: 0, vy: 0, vz: 0
};
let cam = { pitch: -0.5, yaw: 0.7 };
const keys = {};

window.onkeydown = e => keys[e.code] = true;
window.onkeyup = e => keys[e.code] = false;

cvs.onclick = () => cvs.requestPointerLock();
document.onmousemove = e => {
    if(document.pointerLockElement === cvs) {
        cam.yaw -= e.movementX * 0.002;
        cam.pitch -= e.movementY * 0.002;
        cam.pitch = Math.max(-1.5, Math.min(1.5, cam.pitch));
    }
};

function getNormal(p) {
    let e = 0.001;
    return {
        x: map({x:p.x+e, y:p.y, z:p.z}) - map({x:p.x-e, y:p.y, z:p.z}),
        y: map({x:p.x, y:p.y+e, z:p.z}) - map({x:p.x, y:p.y-e, z:p.z}),
        z: map({x:p.x, y:p.y, z:p.z+e}) - map({x:p.x, y:p.y, z:p.z-e})
    };
}
function norm(v) { let l = Math.sqrt(v.x*v.x+v.y*v.y+v.z*v.z); return l===0?v:{x:v.x/l, y:v.y/l, z:v.z/l}; }

function loop(time) {
    if(cvs.width !== window.innerWidth) { cvs.width = window.innerWidth; cvs.height = window.innerHeight; gl.viewport(0,0,cvs.width,cvs.height); gl.uniform2f(loc.res, cvs.width, cvs.height); }

    player.y -= 0.001; 

    let s = Math.sin(cam.yaw), c = Math.cos(cam.yaw);
    let fwd = {x:s, z:c}, right = {x:c, z:-s};
    let speed = 0.002;

    if(keys.KeyW) { player.x += fwd.x*speed; player.z += fwd.z*speed; }
    if(keys.KeyS) { player.x -= fwd.x*speed; player.z -= fwd.z*speed; }
    if(keys.KeyA) { player.x += right.x*speed; player.z += right.z*speed; } 
    if(keys.KeyD) { player.x -= right.x*speed; player.z -= right.z*speed; } 
    
    let groundDist = map({x:player.x, y:player.y, z:player.z});
    if(keys.Space && groundDist < 0.001) {
        player.y += 0.02;
    }

    let fric = 0.8;
    let fricy = 0.96;
    let vx = (player.x - player.px) * fric;
    let vy = (player.y - player.py) * fricy;
    let vz = (player.z - player.pz) * fric;

    player.px = player.x; player.py = player.y; player.pz = player.z;
    player.x += vx; player.y += vy; player.z += vz;

    for(let sub=0; sub<4; sub++) {
        let p = {x:player.x, y:player.y + player.r, z:player.z};
        let d = map(p);
        if(d < player.r) {
            let n = norm(getNormal(p));
            let pen = player.r - d;
            player.x += n.x * pen; player.y += n.y * pen; player.z += n.z * pen;
        }
        p.y = player.y + player.h - player.r;
        d = map(p);
        if(d < player.r) {
            let n = norm(getNormal(p));
            let pen = player.r - d;
            player.x += n.x * pen; player.y += n.y * pen; player.z += n.z * pen;
        }
    }

    let eye = {x:player.x, y:player.y + player.h * 0.9, z:player.z};
    let tx = eye.x + Math.sin(cam.yaw) * Math.cos(cam.pitch);
    let ty = eye.y + Math.sin(cam.pitch);
    let tz = eye.z + Math.cos(cam.yaw) * Math.cos(cam.pitch);

    gl.uniform3f(loc.cam, eye.x, eye.y, eye.z);
    gl.uniform3f(loc.tgt, tx, ty, tz);
    gl.uniform1f(loc.time, time * 0.001);

    gl.drawArrays(gl.TRIANGLES, 0, 6);
    requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
</script>
</body>
</html>