|< <

Bryce

> >|
preview
WASD • Space
🔥 slow (GPU melter) 📱 RIP mobile users
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