preview
WASD • Space
🔥 slow (GPU melter) 📱 RIP mobile users
source
<!DOCTYPE html>
<html>
<head>
    <title>Oortic | First, Steps</title>
    <style>
        body { margin: 0; overflow: hidden; background: #000; font-family: monospace; user-select: none; }
        #glcanvas { position: absolute; left: 0; top: 0; right: 0; bottom: 0; margin: auto; display: block; }
        #fps { position: absolute; left: 12px; top: 12px; color: #0f0; font-size: 14px; font-weight: bold; pointer-events: none; display: none; text-shadow: 1px 1px 0 #000; }
    </style>
</head>
<body>
    <canvas id="glcanvas"></canvas>
    <div id="fps">FPS: 0</div>

<script>
// Polyglot SDF - compiles to JS for physics and GLSL for rendering
// mode - 0: distance only | 1: distance + normal
const SDF_LOGIC = `
vec4 map(vec3 p, int mode) {
    vec4 res = vec4(0.0, 1.0, 0.0, 1000.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 parity = mod(idx + idz, 2.0);
            float sx = (parity < 0.5) ? 0.99 : 0.49; 
            float sz = (parity < 0.5) ? 0.49 : 0.99;
            float cy = (idx + idz) * -0.25;

            float lx = p.x - (idx + 0.5);
            float ly = p.y - cy;
            float lz = p.z - (idz + 0.5);

            float ax = abs(lx) - sx;
            float ay = abs(ly) - 0.5;
            float az = abs(lz) - sz;

            float mx = max(ax, 0.0);
            float my = max(ay, 0.0);
            float mz = max(az, 0.0);

            float mMax = max(ax, max(ay, az));
            float inside = min(mMax, 0.0);

            float d = length(vec3(mx, my, mz)) + inside - 0.02;

            if(d < res.w) {
                float nx = 0.0; float ny = 1.0; float nz = 0.0;

                if(mode > 0) {
                    if(mMax > 0.0) {
                        float len = length(vec3(mx, my, mz));
                        nx = (mx > 0.0 ? ((lx > 0.0) ? 1.0 : -1.0) * mx : 0.0) / len;
                        ny = (my > 0.0 ? ((ly > 0.0) ? 1.0 : -1.0) * my : 0.0) / len;
                        nz = (mz > 0.0 ? ((lz > 0.0) ? 1.0 : -1.0) * mz : 0.0) / len;
                    } else {
                        if (ax == mMax)      nx = (lx > 0.0) ? 1.0 : -1.0;
                        else if (ay == mMax) ny = (ly > 0.0) ? 1.0 : -1.0;
                        else                 nz = (lz > 0.0) ? 1.0 : -1.0;
                    }
                }
                res = vec4(nx, ny, nz, d);
            }
        }
    }
    return res;
}
`;
</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}}

    float hash(vec2 xy) { return fract(sin(dot(xy, vec2(12.9898, 78.233))) * 43758.5453123); }

    float calcAO(vec3 p, vec3 n) {
        float occ = 0.0; float sca = 1.0;
        for(int i=0; i<4; i++) {
            float h = 0.02 + 0.05 * float(i);
            float d = map(p + h * n, 0).w;
            occ += (h - d) * sca; sca *= 0.9;
        }
        return clamp(1.0 - 2.0 * occ, 0.0, 1.0);
    }
    vec3 aces(vec3 x) {
        const float a = 2.51; const float b = 0.03; const float c = 2.43; const float d = 0.59; const float e = 0.14;
        return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0);
    }

    void main() {
        vec2 uv = (gl_FragCoord.xy - 0.5 * u_res.xy) / u_res.y;
        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); 
        vec3 sunDir = normalize(vec3(0.5, 0.6, -0.5));

        // Sky
        float skyT = clamp(0.5 * rd.y + 0.5, 0.0, 1.0);
        vec3 skyCol = mix(vec3(0.6, 0.7, 0.8), vec3(0.1, 0.15, 0.25), skyT);
        float sun_focus = max(0.0, dot(rd, sunDir));
        skyCol += vec3(1.0, 0.95, 0.8) * (2.0*pow(sun_focus, 512.0) + 100.0*pow(sun_focus, 2048.0) + 0.05*pow(sun_focus, 64.0));

        // March
        float t = 0.0; float dist = 0.0; vec4 data;
        
        // Mode 0 loop: Compiler strips normal logic here!
        for(float i = 0.0; i < 200.0; i++) {
            vec3 p = ro + rd * t;
            // PASS 0 for Fast Mode
            data = map(p, 0); 
            dist = data.w;
            if(dist < 0.001 || t > 30.0) break;
            t += dist;
        }

        vec3 col = vec3(0.0);
        if(dist < 0.01) {
            vec3 p = ro + rd * t;
            
            vec3 n = map(p, 1).xyz;
            
            float ao = calcAO(p, n);
            vec3 mate = vec3(0.15, 0.16, 0.18); 
            
            float sunDif = clamp(dot(n, sunDir), 0.0, 1.0);
            float skyDif = clamp(0.5 + 0.5 * n.y, 0.0, 1.0);
            
            float shadow = 1.0; float shT = 0.02; 
            for(int i = 0; i < 32; i++) {
                float h = map(p + sunDir * shT, 0).w;
                if(h < 0.001) { shadow = 0.0; break; }
                shadow = min(shadow, 4.0 * h / shT);
                shT += h; if(shT > 10.0) break;
            }

            vec3 lin = 1.5 * sunDif * vec3(1.0, 0.95, 0.8) * shadow * ao;
            lin += 0.5 * skyDif * vec3(0.4, 0.6, 0.9) * ao;
            col = mate * lin;
            col = mix(col, skyCol, 1.0 - exp(-0.1 * t));
        } else {
            col = skyCol;
        }

        col += (noise - 0.5) * 0.01;
        col = aces(col);
        col = pow(col, vec3(0.4545)); 
        gl_FragColor = vec4(col, 1.0);
    }
</script>

<script>
// --- 2. JS MATH HELPERS ---
const _v4 = {x:0, y:0, z:0, w:0};
const vec3 = (x,y,z) => ({x,y,z});
const vec4 = (x,y,z,w) => { _v4.x=x; _v4.y=y; _v4.z=z; _v4.w=w; return _v4; };
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 sign = Math.sign;
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));
const normalize = (v) => { let l = length(v); return l===0?v:{x:v.x/l, y:v.y/l, z:v.z/l}; };

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

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

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); 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") };

// --- VIEWPORT ---
const TARGET_ASPECT = 2.39; 
const RENDER_SCALE = 0.67; 
let resizeQueued = false;
function resizeCanvasToViewport() {
    const cssW = window.innerWidth; const cssH = window.innerHeight;
    const dpr = window.devicePixelRatio || 1;
    let cropW = cssW, cropH = cssH;
    if(cssW / cssH > TARGET_ASPECT) { cropH = cssH; cropW = cssH * TARGET_ASPECT; } else { cropW = cssW; cropH = cssW / TARGET_ASPECT; }
    cvs.style.width = cropW + "px"; cvs.style.height = cropH + "px";
    const w = Math.max(1, Math.round(cropW * dpr * RENDER_SCALE));
    const h = Math.max(1, Math.round(cropH * dpr * RENDER_SCALE));
    if(cvs.width !== w || cvs.height !== h) { cvs.width = w; cvs.height = h; gl.viewport(0, 0, w, h); gl.uniform2f(loc.res, w, h); }
}
window.addEventListener("resize", () => { if(!resizeQueued){ resizeQueued=true; requestAnimationFrame(() => { resizeQueued=false; resizeCanvasToViewport(); }); } });
resizeCanvasToViewport();

// --- FPS ---
const fpsEl = document.getElementById("fps");
let fpsVisible = false; let lastFrameTime = null; let emaDt = 16.7; let lastFpsUiUpdate = 0;
function toggleFps() { fpsVisible = !fpsVisible; fpsEl.style.display = fpsVisible ? "block" : "none"; }

// --- PHYSICS ---
let player = { x: 0.8, y: 0.8, z: 0.2, vx: 0.0, vy: 0.0, vz: 0.0, h: 0.25, r: 0.01 };
let cam = { pitch: 0.0, yaw: -1.5707963268 };
const keys = {};
let jump = false;

window.onkeydown = (e) => { if(e.code === "F10") { e.preventDefault(); toggleFps(); return; } if(e.code === "Space" && !e.repeat) jump = true; keys[e.code] = true; };
window.onkeyup = (e) => { if(e.code !== "F10") keys[e.code] = false; };
cvs.onclick = () => cvs.requestPointerLock();
document.onmousemove = e => { if(document.pointerLockElement === cvs) { cam.yaw -= e.movementX * 0.002; cam.pitch = Math.max(-1.5, Math.min(1.5, cam.pitch - e.movementY * 0.002)); } };

function loop(time) {
    if(lastFrameTime !== null) { let dt = time - lastFrameTime; emaDt = emaDt * 0.9 + dt * 0.1; if(fpsVisible && (time - lastFpsUiUpdate > 250)) { fpsEl.textContent = "FPS: " + (1000/Math.max(1, emaDt)).toFixed(0); lastFpsUiUpdate = time; } }
    lastFrameTime = time;

    let px = player.x; let py = player.y; let pz = player.z;

    if(jump && map(player, 0).w < 0.001) player.vy = 0.015;
    jump = false;

    player.x += player.vx; player.y += player.vy; player.z += player.vz;

    player.y -= 0.0008; 
    let s = Math.sin(cam.yaw), c = Math.cos(cam.yaw);
    let speed = 0.002;
    if(keys.KeyW) { player.x += s*speed; player.z += c*speed; }
    if(keys.KeyS) { player.x -= s*speed; player.z -= c*speed; }
    if(keys.KeyA) { player.x += c*speed; player.z -= s*speed; } 
    if(keys.KeyD) { player.x -= c*speed; player.z += s*speed; } 

    // Collision
    for(let sub=0; sub<4; sub++) {
        let p = {x:player.x, y:player.y + player.r, z:player.z};
        let res = map(p, 1); 
        if(res.w < player.r) { let pen = player.r - res.w; player.x += res.x * pen; player.y += res.y * pen; player.z += res.z * pen; }
        
        p = {x:player.x, y:player.y + player.r, z:player.z};
        p.y = player.y + player.h - player.r;
        res = map(p, 1);
        if(res.w < player.r) { let pen = player.r - res.w; player.x += res.x * pen; player.y += res.y * pen; player.z += res.z * pen; }
    }
    player.vx = (player.x - px) * 0.8; player.vy = (player.y - py) * 0.96; player.vz = (player.z - pz) * 0.8;

    let eye = {x:player.x, y:player.y + player.h * 0.9, z:player.z};
    gl.uniform3f(loc.cam, eye.x, eye.y, eye.z);
    gl.uniform3f(loc.tgt, eye.x + s*Math.cos(cam.pitch), eye.y + Math.sin(cam.pitch), eye.z + c*Math.cos(cam.pitch));
    gl.uniform1f(loc.time, time * 0.001);
    gl.drawArrays(gl.TRIANGLES, 0, 6);
    requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
</script>
</body>
</html>

remixes

no remixes yet... email to remix@oortic.net