<!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 • 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>