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
