source
<!DOCTYPE html>
<html>
<head>
<title>Oortic | Bryce</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; top: 10px; left: 10px; color: rgba(255,255,255,0.85); pointer-events: none; text-shadow: 1px 1px 2px #000; display: none; }
</style>
</head>
<body>
<canvas id="glcanvas"></canvas>
<div id="fps">0 fps</div>
<script>
// --- 1. POLYGLOT SDF ---
const SDF_LOGIC = `
int g_matID = 0;
// IQ's Exact Cone + True Analytic Normal
vec4 sdConeAndNormal( vec3 p, vec2 c, float h ) {
vec2 q = vec2(h*(c.x/c.y), -h);
float lxz = length(vec2(p.x, p.z));
vec2 w = vec2( lxz, p.y );
float ddot_wq = w.x*q.x + w.y*q.y;
float ddot_qq = q.x*q.x + q.y*q.y;
float clamp1 = clamp( ddot_wq/ddot_qq, 0.0, 1.0 );
vec2 a = vec2(w.x - q.x*clamp1, w.y - q.y*clamp1);
float clamp2 = clamp( w.x/q.x, 0.0, 1.0 );
vec2 b = vec2(w.x - q.x*clamp2, w.y - q.y);
float k = sign( q.y );
float dot_aa = a.x*a.x + a.y*a.y;
float dot_bb = b.x*b.x + b.y*b.y;
float d2 = min(dot_aa, dot_bb);
float s = max( k*(w.x*q.y - w.y*q.x), k*(w.y - q.y) );
float ds = (s < 0.0) ? -1.0 : 1.0;
float d = sqrt(d2) * ds;
float nx = 0.0; float ny = 0.0; float nz = 0.0;
if (dot_aa < dot_bb) {
if (lxz > 0.0001) { nx = (p.x/lxz)*c.y; ny = c.x; nz = (p.z/lxz)*c.y; }
else { nx = 0.0; ny = 1.0; nz = 0.0; }
} else {
if (w.x < q.x) { nx = 0.0; ny = -1.0; nz = 0.0; }
else {
float l_b = sqrt(dot_bb);
vec2 n2 = vec2(b.x/l_b, b.y/l_b);
if (lxz > 0.0001) { nx = (p.x/lxz)*n2.x; ny = n2.y; nz = (p.z/lxz)*n2.x; }
else { nx = 0.0; ny = -1.0; nz = 0.0; }
}
}
return vec4(nx, ny, nz, d);
}
vec4 mapSpherePrim(vec3 p, vec3 center, float r, int mode) {
vec3 dp = vec3(p.x-center.x, p.y-center.y, p.z-center.z);
float d = length(dp) - r;
float nx = 0.0; float ny = 0.0; float nz = 0.0;
if(mode == 1) {
vec3 n = normalize(dp);
nx = n.x; ny = n.y; nz = n.z;
}
return vec4(nx, ny, nz, d);
}
vec4 mapFloorPrim(vec3 p, int mode) {
float dGrout = p.y + 0.04;
float dTiles = 1000.0;
float ix = floor(p.x);
float iz = floor(p.z);
vec2 bestCenter = vec2(0.0);
for(int xi = -1; xi <= 1; xi++) {
float x = float(xi);
for(int zi = -1; zi <= 1; zi++) {
float z = float(zi);
vec2 id = vec2(ix + x, iz + z);
vec2 center = id + 0.5;
vec3 local = vec3(p.x - center.x, p.y + 0.53, p.z - center.y);
float boxD = length(vec3(max(abs(local.x)-0.45, 0.0), max(abs(local.y)-0.5, 0.0), max(abs(local.z)-0.45, 0.0))) - 0.03;
if(boxD < dTiles) {
dTiles = boxD;
bestCenter = center;
}
}
}
float d = min(dGrout, dTiles);
float nx = 0.0; float ny = 0.0; float nz = 0.0;
if (mode == 1) {
if (dGrout < dTiles) {
nx = 0.0; ny = 1.0; nz = 0.0;
} else {
vec3 local = vec3(p.x - bestCenter.x, p.y + 0.53, p.z - bestCenter.y);
vec3 bDim = vec3(0.45, 0.5, 0.45);
vec3 d3 = vec3(abs(local.x) - bDim.x, abs(local.y) - bDim.y, abs(local.z) - bDim.z);
vec3 mq = vec3(max(d3.x, 0.0), max(d3.y, 0.0), max(d3.z, 0.0));
float lmq = length(mq);
if (lmq > 0.0001) {
nx = sign(local.x) * (mq.x / lmq);
ny = sign(local.y) * (mq.y / lmq);
nz = sign(local.z) * (mq.z / lmq);
} else {
if (d3.y > d3.x && d3.y > d3.z) ny = sign(local.y);
else if (d3.x > d3.z) nx = sign(local.x);
else nz = sign(local.z);
}
}
}
return vec4(nx, ny, nz, d);
}
vec4 mapConePrim(vec3 p, vec3 base, vec3 tip, float rBase, int mode) {
vec3 axisY = vec3(tip.x-base.x, tip.y-base.y, tip.z-base.z);
float h = length(axisY);
vec3 vY = vec3(axisY.x/h, axisY.y/h, axisY.z/h);
vec3 ref_axis = (abs(vY.y) > 0.999) ? vec3(1.0, 0.0, 0.0) : vec3(0.0, 1.0, 0.0);
vec3 vX = normalize(cross(ref_axis, vY));
vec3 vZ = cross(vY, vX);
vec3 pt = vec3(p.x-tip.x, p.y-tip.y, p.z-tip.z);
vec3 q = vec3(dot(pt, vX), dot(pt, vY), dot(pt, vZ));
float hyp = sqrt(rBase*rBase + h*h);
vec2 c = vec2(rBase/hyp, h/hyp);
vec4 coneData = sdConeAndNormal(q, c, h);
float d = coneData.w;
float nx = 0.0; float ny = 0.0; float nz = 0.0;
if (mode == 1) {
vec3 ln = vec3(coneData.x, coneData.y, coneData.z);
vec3 n = vec3(
ln.x*vX.x + ln.y*vY.x + ln.z*vZ.x,
ln.x*vX.y + ln.y*vY.y + ln.z*vZ.y,
ln.x*vX.z + ln.y*vY.z + ln.z*vZ.z
);
nx = n.x; ny = n.y; nz = n.z;
}
return vec4(nx, ny, nz, d);
}
vec4 map(vec3 p, int mode) {
vec3 base = vec3(g_coneB.x, g_coneB.y, g_coneB.z);
vec3 tip = vec3(g_coneT.x, g_coneT.y, g_coneT.z);
vec3 ballPos = vec3(g_ball.x, g_ball.y, g_ball.z);
vec4 floorData = mapFloorPrim(p, mode);
vec4 ballData = mapSpherePrim(p, ballPos, 1.0, mode);
vec4 coneData = mapConePrim(p, base, tip, 0.8, mode);
float d = floorData.w;
int matID = 1;
float nx = floorData.x; float ny = floorData.y; float nz = floorData.z;
if (ballData.w < d) { d = ballData.w; matID = 2; nx = ballData.x; ny = ballData.y; nz = ballData.z; }
if (coneData.w < d) { d = coneData.w; matID = 3; nx = coneData.x; ny = coneData.y; nz = coneData.z; }
g_matID = matID;
return vec4(nx, ny, nz, d);
}
// Scene SDF without the cone (for cone particle collisions).
// Keeps the same material IDs for floor (1) and ball (2).
vec4 mapNoCone(vec3 p, int mode) {
vec3 ballPos = vec3(g_ball.x, g_ball.y, g_ball.z);
vec4 floorData = mapFloorPrim(p, mode);
vec4 ballData = mapSpherePrim(p, ballPos, 1.0, mode);
float d = floorData.w;
int matID = 1;
float nx = floorData.x; float ny = floorData.y; float nz = floorData.z;
if (ballData.w < d) { d = ballData.w; matID = 2; nx = ballData.x; ny = ballData.y; nz = ballData.z; }
g_matID = matID;
return vec4(nx, ny, nz, d);
}
// Floor-only SDF (for physics collisions against static world).
vec4 mapFloorOnly(vec3 p, int mode) {
vec4 floorData = mapFloorPrim(p, mode);
g_matID = 1;
return floorData;
}
`;
</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;
uniform vec3 g_ball;
uniform vec3 g_coneB;
uniform vec3 g_coneT;
{{SDF_LOGIC}}
float hash(vec2 xy) { return fract(sin(dot(xy, vec2(12.9898, 78.233))) * 43758.5453123); }
float noise(vec2 p) {
vec2 i = floor(p); vec2 f = fract(p); f = f*f*(3.0-2.0*f);
float a = hash(i); float b = hash(i + vec2(1.0, 0.0)); float c = hash(i + vec2(0.0, 1.0)); float d = hash(i + vec2(1.0, 1.0));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
// 3 octaves: better bang-for-buck than 4
float fbm(vec2 p) {
float f = 0.0; float amp = 0.5;
for(int i = 0; i < 3; i++) { f += amp * noise(p); p *= 2.0; amp *= 0.5; }
return f;
}
vec3 aces(vec3 x) { return clamp((x*(2.51*x+0.03))/(x*(2.43*x+0.59)+0.14),0.0,1.0); }
vec3 getSky(vec3 rd, vec3 sunDir) {
float skyT = clamp(0.5 * rd.y + 0.5, 0.0, 1.0);
vec3 col = mix(vec3(0.35, 0.6, 0.9), vec3(0.05, 0.2, 0.6), skyT);
float sun = max(0.0, dot(rd, sunDir));
col += vec3(1.0, 0.95, 0.8) * (150.0*pow(sun, 2048.0) + 8.0*pow(sun, 256.0));
return col;
}
vec3 getRefractedRay(vec3 p, vec3 rd, vec3 n, float ior, out vec3 pEx) {
vec3 rdIn = refract(rd, n, 1.0/ior);
if(length(rdIn) == 0.0) return reflect(rd, n);
vec3 currP = p;
vec3 currRd = rdIn;
vec3 currN = n;
vec3 rdOut = vec3(0.0);
for(int bounce = 0; bounce < 3; bounce++) {
float dist = 2.0 * max(0.0, dot(-currRd, currN));
pEx = currP + currRd * dist;
vec3 nEx = -normalize(pEx - g_ball);
rdOut = refract(currRd, nEx, ior);
if(length(rdOut) > 0.001) break;
currRd = reflect(currRd, nEx);
currP = pEx;
currN = nEx;
rdOut = currRd;
}
return rdOut;
}
// Opinionated: secondary rays are CHEAP (no shadows, no recursive reflections)
vec3 getSceneColor(vec3 ro, vec3 rd, vec3 sunDir) {
float t = 0.02;
float d = 0.0;
for(int i=0; i<80; i++) {
vec3 p = ro + rd * t;
d = map(p, 0).w;
if(d < 0.005) {
int hitMat = g_matID;
vec4 mData = map(p, 1);
vec3 n = vec3(mData.x, mData.y, mData.z);
// Glass in secondary: single IOR + quick behind-glass sample
if(hitMat == 2) {
float ior = 1.517;
float R0 = pow((1.0 - ior) / (1.0 + ior), 2.0);
float fre = R0 + (1.0 - R0) * pow(1.0 - max(0.0, dot(-rd, n)), 5.0);
vec3 pEx;
vec3 rdOut = getRefractedRay(p, rd, n, ior, pEx);
vec3 transCol = getSky(rdOut, sunDir);
// Cheaper than gemini's 40: still decent, much cheaper
float rt = 0.02;
for(int j=0; j<24; j++) {
vec3 rp = pEx + rdOut * rt;
float rdD = map(rp, 0).w;
if(rdD < 0.005) {
int rMat = g_matID;
vec4 rData = map(rp, 1);
vec3 rn = vec3(rData.x, rData.y, rData.z);
if (rMat == 1) {
float fchk = mod(floor(rp.x)+floor(rp.z), 2.0);
vec3 bc = (fchk > 0.5) ? vec3(0.02) : vec3(0.75);
if (rp.y < -0.02) bc = vec3(0.03);
float dirt = smoothstep(0.35, 0.75, fbm(rp.xz * 2.0));
vec3 mc = mix(bc, bc * 0.25, dirt);
vec3 lin = 2.0 * max(0.0, dot(vec3(0,1,0), sunDir)) * vec3(1.0, 0.95, 0.8) + 0.6 * vec3(0.3, 0.5, 0.8);
transCol = mix(mc * lin, transCol, 1.0 - exp(-0.03 * rt));
} else if (rMat == 3) {
vec3 lin = 2.0 * max(0.0, dot(rn, sunDir)) * vec3(1.0, 0.95, 0.8) + 0.6 * vec3(0.3, 0.5, 0.8);
transCol = mix(vec3(0.0,0.4,0.4) * lin, transCol, 1.0 - exp(-0.03 * rt));
}
break;
}
rt += rdD;
if(rt > 20.0) break;
}
transCol *= vec3(0.96, 0.98, 1.0);
transCol *= (1.0 - fre);
vec3 refCol = getSky(reflect(rd, n), sunDir);
return mix(transCol, refCol, fre);
}
if(hitMat == 3) {
vec3 lin = 2.0 * max(0.0, dot(n, sunDir)) * vec3(1.0, 0.95, 0.8) + 0.6 * vec3(0.3, 0.5, 0.8);
return vec3(0.0,0.4,0.4) * lin;
}
float fchk = mod(floor(p.x)+floor(p.z), 2.0);
vec3 baseColor = (fchk > 0.5) ? vec3(0.02) : vec3(0.75);
if (p.y < -0.02) baseColor = vec3(0.03);
float dirt = smoothstep(0.35, 0.75, fbm(p.xz * 2.0));
vec3 mc = mix(baseColor, baseColor * 0.25, dirt);
vec3 lin = 2.0 * max(0.0, dot(n, sunDir)) * vec3(1.0, 0.95, 0.8) + 0.6 * vec3(0.3, 0.5, 0.8);
return mix(mc * lin, getSky(rd, sunDir), 1.0 - exp(-0.03 * t));
}
t += d;
if(t > 50.0) break;
}
return getSky(rd, sunDir);
}
void main() {
vec2 uv = (gl_FragCoord.xy - 0.5 * u_res.xy) / u_res.y;
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));
float t = 0.0; float d = 0.0;
for(int i=0; i<120; i++) {
vec3 p = ro + rd * t;
d = map(p, 0).w;
if(d < 0.002 || t > 50.0) break;
t += d;
}
vec3 col = vec3(0.0);
if(d < 0.005 && t < 50.0) {
vec3 p = ro + rd * t;
int hitMat = g_matID;
vec4 data = map(p, 1);
vec3 n = vec3(data.x, data.y, data.z);
float sunDif = clamp(dot(n, sunDir), 0.0, 1.0);
float skyDif = 0.5 + 0.5 * n.y;
// Soft shadows only on primary (reduced steps for perf)
float shadow = 1.0;
float st = 0.05;
for(int i=0; i<16; i++) {
float h = map(p + sunDir * st, 0).w;
if(h < 0.001) { shadow = 0.0; break; }
shadow = min(shadow, 8.0*h/st);
st += h; if(st>12.0) break;
}
vec3 lin = 2.0 * sunDif * vec3(1.0, 0.95, 0.8) * shadow;
lin += 0.6 * skyDif * vec3(0.3, 0.5, 0.8);
if(hitMat == 2) {
float iorR = 1.514;
float iorG = 1.517;
float iorB = 1.522;
float cosTheta = max(0.0, dot(-rd, n));
float R0 = pow((1.0 - iorG) / (1.0 + iorG), 2.0);
float fre = R0 + (1.0 - R0) * pow(1.0 - cosTheta, 5.0);
// Offset to avoid self-intersection acne/banding in reflections
vec3 refCol = getSceneColor(p + n * 0.05, reflect(rd, n), sunDir);
vec3 pExR, pExG, pExB;
vec3 rdOutR = getRefractedRay(p, rd, n, iorR, pExR);
vec3 rdOutG = getRefractedRay(p, rd, n, iorG, pExG);
vec3 rdOutB = getRefractedRay(p, rd, n, iorB, pExB);
float envR = getSceneColor(pExR, rdOutR, sunDir).r;
float envG = getSceneColor(pExG, rdOutG, sunDir).g;
float envB = getSceneColor(pExB, rdOutB, sunDir).b;
vec3 transCol = vec3(envR, envG, envB);
transCol *= vec3(0.96, 0.98, 1.0);
transCol *= (1.0 - fre);
col = mix(transCol, refCol, fre);
} else if(hitMat == 3) {
col = vec3(0.0, 0.4, 0.4) * lin;
col += pow(max(0.0, dot(reflect(rd,n), sunDir)), 64.0) * shadow * 0.5;
} else {
float fchk = mod(floor(p.x)+floor(p.z), 2.0);
vec3 baseColor = (fchk > 0.5) ? vec3(0.02) : vec3(0.75);
if (p.y < -0.02) baseColor = vec3(0.03);
float dirt = smoothstep(0.35, 0.75, fbm(p.xz * 2.0));
vec3 mc = mix(baseColor, baseColor * 0.25, dirt);
col = mc * lin;
float refMask = 1.0 - dirt;
float fre = 0.02 + 0.98 * pow(1.0 - max(0.0, dot(-rd, n)), 5.0);
if(fre * refMask > 0.015) {
vec3 rDir = reflect(rd, n);
// Offset to avoid floor self-hit causing banded/choppy reflections
vec3 rCol = getSceneColor(p + n * 0.05, rDir, sunDir);
col = mix(col, rCol, fre * refMask);
}
}
col = mix(col, getSky(rd, sunDir), 1.0 - exp(-0.03 * t));
} else {
col = getSky(rd, sunDir);
}
col = aces(col);
col = pow(col, vec3(0.4545));
gl_FragColor = vec4(col, 1.0);
}
</script>
<script>
// --- JS MATH SHIMS ---
const vec3 = (x,y,z) => ({x,y,z});
const vec2 = (x,y) => ({x,y});
const vec4 = (x,y,z,w) => ({x,y,z,w});
const float = (x) => x;
const int = (x) => Math.floor(x);
const bool = (x) => !!x;
const length = (v) => Math.sqrt(v.x*v.x + (v.y*v.y||0) + (v.z*v.z||0));
const dot = (a,b) => a.x*b.x + a.y*b.y + (a.z*b.z||0);
const cross = (a,b) => vec3(a.y*b.z - a.z*b.y, a.z*b.x - a.x*b.z, a.x*b.y - a.y*b.x);
const normalize = (v) => { let l = length(v); return l===0?v:{x:v.x/l, y:v.y/l, z:v.z/l}; };
const min = Math.min; const max = Math.max; const abs = Math.abs; const sqrt = Math.sqrt; const sign = Math.sign; const floor = Math.floor;
const clamp = (v, mn, mx) => Math.max(mn, Math.min(mx, v));
// --- PHYSICS STATE ---
window.g_ball = { x: -2.0, y: 1.0, z: 4.0 };
window.g_coneB = { x: 2.0, y: 0.0, z: 4.0 };
window.g_coneT = { x: 2.0, y: 2.0, z: 4.0 };
let pBall = { p: window.g_ball, o: {x:-2,y:1,z:4}, r: 1.0, m: 1.0 };
// Cone particle rig: 5 particles at base (center + 4 rim), 1 at top. All same weight.
const CONE_LEN = 2.0;
const CONE_RB = 0.8;
const CONE_PART_R = 0.08;
const CONE_IM = 1.0; // equal weights
function mkParticle(x,y,z) { return { p:{x,y,z}, o:{x,y,z} }; }
function mkParticleRef(p) { return { p, o:{x:p.x, y:p.y, z:p.z} }; }
let coneBase = [
mkParticleRef(window.g_coneB), // center (shared with polyglot)
mkParticle(window.g_coneB.x + CONE_RB, window.g_coneB.y, window.g_coneB.z), // +x
mkParticle(window.g_coneB.x - CONE_RB, window.g_coneB.y, window.g_coneB.z), // -x
mkParticle(window.g_coneB.x, window.g_coneB.y, window.g_coneB.z + CONE_RB), // +z
mkParticle(window.g_coneB.x, window.g_coneB.y, window.g_coneB.z - CONE_RB) // -z
];
let coneTop = mkParticleRef(window.g_coneT); // top (shared with polyglot)
// --- COMPILER ---
let jsSrc = SDF_LOGIC
.replace(/^[ \t]*(?:float|vec2|vec3|vec4|int|bool)\s+([a-zA-Z0-9_]+)\s*\((.*)\)/gm, function(match, name, args) {
let cleanArgs = args.replace(/\b(float|vec2|vec3|vec4|int|bool)\s+/g, '');
return `function ${name}(${cleanArgs})`;
})
.replace(/\b(float|vec2|vec3|vec4|int|bool)\s+(?=[a-zA-Z_])/g, 'let ');
const map = new Function('p', 'mode', jsSrc + ' return map(p, mode);');
const mapNoCone = new Function('p', 'mode', jsSrc + ' return mapNoCone(p, mode);');
const mapFloorOnly = new Function('p', 'mode', jsSrc + ' return mapFloorOnly(p, mode);');
// Expose material id from polyglot scope for physics decisions.
const mapInfo = new Function('p', 'mode', jsSrc + ' const r = map(p, mode); return { r: r, m: g_matID };');
const mapNoConeInfo = new Function('p', 'mode', jsSrc + ' const r = mapNoCone(p, mode); return { r: r, m: g_matID };');
const mapFloorOnlyInfo = new Function('p', 'mode', jsSrc + ' const r = mapFloorOnly(p, mode); return { r: r, m: g_matID };');
// --- WEBGL SETUP ---
const cvs = document.getElementById('glcanvas');
const gl = cvs.getContext('webgl');
if(!gl) throw new Error("WebGL unavailable (getContext('webgl') returned null).");
const pid = gl.createProgram();
function mustCompileShader(sh, label) {
if(!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
const log = gl.getShaderInfoLog(sh) || "(no shader info log)";
throw new Error(label + " shader failed to compile:\n" + log);
}
}
const vs = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vs, document.getElementById('vs').text);
gl.compileShader(vs);
mustCompileShader(vs, "Vertex");
const fs = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fs, document.getElementById('fs').text.replace('{{SDF_LOGIC}}', SDF_LOGIC));
gl.compileShader(fs);
mustCompileShader(fs, "Fragment");
gl.attachShader(pid, vs); gl.attachShader(pid, fs);
gl.linkProgram(pid);
// Intentionally skip LINK_STATUS query to avoid driver stalls on some machines.
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);
const posLoc = gl.getAttribLocation(pid, "position");
if(posLoc < 0) throw new Error('Shader attribute "position" not found/active.');
gl.enableVertexAttribArray(posLoc); gl.vertexAttribPointer(posLoc, 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"),
ball: gl.getUniformLocation(pid, "g_ball"),
coneB: gl.getUniformLocation(pid, "g_coneB"),
coneT: gl.getUniformLocation(pid, "g_coneT")
};
// --- LOOP ---
let player = { x: 0, y: 1, z: 0, vx:0, vy:0, vz:0, r: 0.5 };
let cam = { yaw: 0, pitch: 0 };
let keys = {}; let jump = false;
const fpsEl = document.getElementById('fps');
let showFps = false;
let fpsLastT = performance.now();
let fpsFrames = 0;
// Render at a lower internal resolution for performance (CSS size stays full).
const RENDER_SCALE = 0.67;
window.onresize = () => {
const dpr = window.devicePixelRatio || 1;
cvs.style.width=window.innerWidth+"px"; cvs.style.height=window.innerHeight+"px";
cvs.width=Math.floor(window.innerWidth*dpr*RENDER_SCALE);
cvs.height=Math.floor(window.innerHeight*dpr*RENDER_SCALE);
gl.viewport(0,0,cvs.width,cvs.height); gl.uniform2f(loc.res,cvs.width,cvs.height);
};
window.onresize();
window.onkeydown=e=>{
if(e.code === 'F10') {
showFps = !showFps;
fpsEl.style.display = showFps ? 'block' : 'none';
e.preventDefault();
return;
}
keys[e.code]=true; if(e.code==='Space')jump=true;
};
window.onkeyup=e=>keys[e.code]=false;
cvs.onclick=()=>cvs.requestPointerLock();
document.onmousemove=e=>{if(document.pointerLockElement){cam.yaw-=e.movementX*0.002; cam.pitch=Math.max(-1.5, Math.min(1.5, cam.pitch-e.movementY*0.002));}};
function verlet(p, o, dt, drag, rad) {
// Classic: keep this simple/stable (dt intentionally ignored here for now)
let vx = (p.x - o.x) * drag, vy = (p.y - o.y) * drag, vz = (p.z - o.z) * drag;
o.x = p.x; o.y = p.y; o.z = p.z;
p.x += vx; p.y += vy - 0.01; p.z += vz;
if(p.y < rad) { p.y = rad; let f = 0.1; p.x -= vx*f; p.z -= vz*f; }
}
function solveLink(p1, p2, len, im1, im2) {
let dx = p1.x-p2.x, dy = p1.y-p2.y, dz = p1.z-p2.z;
let d = Math.sqrt(dx*dx+dy*dy+dz*dz);
if(d < 1e-8) return;
let diff = (d - len) / d;
let sc = diff * 0.5;
let w = 1 / (im1+im2);
p1.x -= dx*sc*(im1*w); p1.y -= dy*sc*(im1*w); p1.z -= dz*sc*(im1*w);
p2.x += dx*sc*(im2*w); p2.y += dy*sc*(im2*w); p2.z += dz*sc*(im2*w);
}
function verletParticle(pt, dt, drag, rad) {
verlet(pt.p, pt.o, dt, drag, rad);
}
function resolveSdfStaticPosOnly(pt, r, infoFn) {
const info = infoFn(pt.p, 1);
const res = info.r;
if(res.w >= r) return false;
const pen = (r - res.w);
pt.p.x += res.x * pen; pt.p.y += res.y * pen; pt.p.z += res.z * pen;
return true;
}
function solveCollPosOnly(p, r, objP, objR, wp, wo) {
let dx = p.x-objP.x, dy = p.y-objP.y, dz = p.z-objP.z;
let d = Math.sqrt(dx*dx+dy*dy+dz*dz);
if(d < 1e-8) return;
let pen = (r+objR) - d;
if(pen > 0) {
let nx = dx/d, ny = dy/d, nz = dz/d;
p.x += nx*(pen*wp); p.y += ny*(pen*wp); p.z += nz*(pen*wp);
objP.x -= nx*(pen*wo); objP.y -= ny*(pen*wo); objP.z -= nz*(pen*wo);
}
}
function nearestConeParticle(pos) {
let best = coneTop;
let bestD = 1e9;
const all = [coneTop, ...coneBase];
for(const pt of all) {
const dx = pt.p.x - pos.x, dy = pt.p.y - pos.y, dz = pt.p.z - pos.z;
const d = dx*dx + dy*dy + dz*dz;
if(d < bestD) { bestD = d; best = pt; }
}
return best;
}
// todo theres got to be an amazing way to do sdf cone verlet physics
function solveCone(dt) {
// Integrate particles
for(const pt of coneBase) verletParticle(pt, 1, 0.98, 0.0);
verletParticle(coneTop, 1, 0.98, 0.0);
const tipRimLen = Math.sqrt(CONE_LEN*CONE_LEN + CONE_RB*CONE_RB);
const rimAdjLen = Math.SQRT2 * CONE_RB;
// Constraint iterations
for(let it = 0; it < 5; it++) {
// Tip to base center
solveLink(coneTop.p, coneBase[0].p, CONE_LEN, CONE_IM, CONE_IM);
// Base center to rims
solveLink(coneBase[1].p, coneBase[0].p, CONE_RB, CONE_IM, CONE_IM);
solveLink(coneBase[2].p, coneBase[0].p, CONE_RB, CONE_IM, CONE_IM);
solveLink(coneBase[3].p, coneBase[0].p, CONE_RB, CONE_IM, CONE_IM);
solveLink(coneBase[4].p, coneBase[0].p, CONE_RB, CONE_IM, CONE_IM);
// Rim adjacency (square)
solveLink(coneBase[1].p, coneBase[3].p, rimAdjLen, CONE_IM, CONE_IM);
solveLink(coneBase[3].p, coneBase[2].p, rimAdjLen, CONE_IM, CONE_IM);
solveLink(coneBase[2].p, coneBase[4].p, rimAdjLen, CONE_IM, CONE_IM);
solveLink(coneBase[4].p, coneBase[1].p, rimAdjLen, CONE_IM, CONE_IM);
// Cross braces
solveLink(coneBase[1].p, coneBase[2].p, 2.0*CONE_RB, CONE_IM, CONE_IM);
solveLink(coneBase[3].p, coneBase[4].p, 2.0*CONE_RB, CONE_IM, CONE_IM);
// Tip to rims (keeps axis stable)
solveLink(coneTop.p, coneBase[1].p, tipRimLen, CONE_IM, CONE_IM);
solveLink(coneTop.p, coneBase[2].p, tipRimLen, CONE_IM, CONE_IM);
solveLink(coneTop.p, coneBase[3].p, tipRimLen, CONE_IM, CONE_IM);
solveLink(coneTop.p, coneBase[4].p, tipRimLen, CONE_IM, CONE_IM);
}
// Collide particles against static world (floor only) - position correction only
for(const pt of [coneTop, ...coneBase]) resolveSdfStaticPosOnly(pt, CONE_PART_R, mapFloorOnlyInfo);
// Ball vs cone particles - position correction only
for(const pt of [coneTop, ...coneBase]) solveCollPosOnly(pBall.p, pBall.r, pt.p, CONE_PART_R, 0.5, 0.5);
}
function tick() {
if(showFps) {
fpsFrames++;
const t = performance.now();
const dms = t - fpsLastT;
if(dms >= 250) {
fpsEl.textContent = (fpsFrames * 1000 / dms).toFixed(1) + " fps";
fpsFrames = 0;
fpsLastT = t;
}
}
let px = player.x, py = player.y, pz = player.z;
if(jump && player.y <= 0.55) player.vy = 0.15; jump=false;
player.x += player.vx; player.y += player.vy; player.z += player.vz;
player.y -= 0.012; if(player.y < 0.5) player.y = 0.5;
let s = Math.sin(cam.yaw), c = Math.cos(cam.yaw);
if(keys.KeyW) { player.x += s*0.005; player.z += c*0.005; }
if(keys.KeyS) { player.x -= s*0.005; player.z -= c*0.005; }
if(keys.KeyA) { player.x += c*0.005; player.z -= s*0.005; }
if(keys.KeyD) { player.x -= c*0.005; player.z += s*0.005; }
player.vx = (player.x - px)*0.85; player.vy = (player.y - py)*0.95; player.vz = (player.z - pz)*0.85;
verlet(pBall.p, pBall.o, 1, 0.99, pBall.r);
solveCone(1);
// Player pushes objects via SDF separation only (no impulse hacks)
let info = mapInfo(player, 1);
let res = info.r;
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;
if(info.m === 2) {
pBall.p.x -= res.x * pen;
pBall.p.y -= res.y * pen;
pBall.p.z -= res.z * pen;
} else if(info.m === 3) {
const pt = nearestConeParticle(player);
pt.p.x -= res.x * pen;
pt.p.y -= res.y * pen;
pt.p.z -= res.z * pen;
}
}
// Ball vs floor SDF - position only (prevents tunneling through tiles)
resolveSdfStaticPosOnly({p:pBall.p}, pBall.r, mapFloorOnlyInfo);
// Update uniforms
let eye = {x:player.x, y:player.y+0.4, 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, performance.now()*0.001);
gl.uniform3f(loc.ball, pBall.p.x, pBall.p.y, pBall.p.z);
gl.uniform3f(loc.coneB, window.g_coneB.x, window.g_coneB.y, window.g_coneB.z);
gl.uniform3f(loc.coneT, window.g_coneT.x, window.g_coneT.y, window.g_coneT.z);
gl.drawArrays(gl.TRIANGLES, 0, 6);
requestAnimationFrame(tick);
}
tick();
</script>
</body>
</html>
remixes
no remixes yet... email to remix@oortic.net
