#define RES    iResolution
#define MINRES min(RES.x, RES.y)
#define ZERO   (min(iFrame, 0))

const float PI = 3.14159265359;

// WS = world space SS = screen space
float gWSEps = 0.001;  // WS epsilon for surfaces
float gSSEps;          // SS epsilon for smoothstep
float gSSLw;           // SS line width
float gT;

const vec3 gV0 = vec3(0.0);
const vec3 gV1 = vec3(1.0);
const vec3 gVx = vec3(1.0, 0.0, 0.0);
const vec3 gVy = vec3(0.0, 1.0, 0.0);
const vec3 gVz = vec3(0.0, 0.0, 1.0);

const vec3 gMagenta = vec3(1.0, 0.0, 1.0);
const vec3 gGreen   = vec3(0.0, 1.0, 0.0);

mat2 rot2(float theta) {
    float c = cos(theta);
    float s = sin(theta);
    return mat2(c, s, -s, c);
}

float opU(float A, float B) {
    return min(A, B);
}
float opS(float A, float B) {
    return -min(-A, B);
}

//https://iquilezles.org/articles/distfunctions
float sdBox(vec3 p, vec3 b) {
    vec3 q = abs(p) - b;
    return length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0);
}

float sdSphere(vec3 p, float r) {
    return length(p) - r;
}

float sdCylX(vec3 p, float r) {
    return length(p.yz) - r;
}

float sdCylY(vec3 p, float r) {
    return length(p.xz) - r;
}

float sdCylZ(vec3 p, float r) {
    return length(p.xy) - r;
}
float sdPlnY(vec3 p) {
    return p.y;
}
float sdSlabX(vec3 p, float r) {
    return abs(p.x) - r;
}
float sdSlabY(vec3 p, float r) {
    return abs(p.y) - r;
}
float sdSlabZ(vec3 p, float r) {
    return abs(p.z) - r;
}

// Fork of "Ordinary Glass, with Comments" by elenzil. https://shadertoy.com/view/7dlBRf
// 2022-02-23 00:29:54

/*

    Ordinary Glass, with Comments
    -----------------------------
    
    I've been wanting for a long time to try an approach to glass.
    The main tricky/interesting part is how to handle the growing number of rays.
    In raytracing simple materials, each pixel results in one ray
    that travels into the scene and hits something, and maybe gets reflected,
    but it's still one ray at a time.
    But when a ray hits a glassy surface, it splits into two:
    one child ray gets transmitted through the material and one gets reflected.
    Then each of those split into two, and so on.
    
    In a normal CPU environment you would handle this ray-branching with recursion
    but GPUs have no recusion. So I thought I'd try keeping a queue of rays,
    and have a loop that runs forever, pulling rays from the queue and either
    finishing the ray by contributing color to the pixel or forking into one or more
    child-rays. Along the way each ray has to remember what share of contribution
    it has for the scene.
    
    It looks like this:
    
    1. Find the initial ray leaving the pixel and entering the scene.
       This ray has full contribution of 1.  Add it to the queue.

    2. While the ray queue is not empty:
       * pull a ray out of the queue.
       * march it until it hits a surface or reaches the marching limit.
         * if it reaches the marching limit, color it Sky.
         * if it reaches a surface:
           * use some fraction of the ray's "contribution" for diffuse shading.
             this involves another marching step for shadows.
           * use the remaining fraction to create child rays for reflection and transmission,
             and add them to the queue.
             
    This was my first time marching through the dark (in) side of the SDF,
    so it's likely there are some thing I'm missing there.
    There are still some artifacts I haven't figured out.
        * speckles in the plain featureless portions of surfaces
        * something's weird with floor reflections plus transmission
        * high attenuation coefficient yields weird results
        * black edges on iOS.
          - this makes me this something's uninitialized.
    
    I've heard that arrays and globals have poor performance in GLSL,
    but it seems to do allright. The array size is being used is just 6 or so.
               
    There may be other approaches to this,
    I haven't checked how folks like Dr2 and byt3_m3chanic are doing it.
    .. Now I have. A lot of folks are using a stack,
    but some of the best ones just treat each interface-crossing
    as either entirely transmissive or entirely reflective, based on total internal reflection.


    I've tried to add Shane-style comments, but it's still fairly messy.
*/


// Fork of "elenzil marcher base" by elenzil. https://shadertoy.com/view/fdffDn
// 2022-02-12 15:40:33

// todo
// [x] optimize queue!
// [x] actual attenuation while in material
// [x] total internal reflection
// [ ] fresnel ?
// [ ] better environment
// [ ] vignette
// [ ] consider a conveyor-belt style presentation ?
// [ ] AA
// [ ] per-object materials
// [ ] shadows that know about transparency
// [ ] diffusion


// the maximum number of simultaneous rays in the backlog.
// the number of rays per pixel can be more than this.
const uint  gMaximumRaysInQueue = 10u;

// a ray must have at least this much contribution left to be enqueued.
const float gMinimumRayContribution = 0.005f;

// #define HEATMAP


struct ray_t {
    // origin
    vec3  ro;
    
    // direction
    vec3  rd;
    
    // what amount of the pixel this ray is contributing. [0, 1]
    float contribution;
    
    // whether this ray is on the inside of the SDF.
    bool  internal;
};


////////////////////////////////////////////////////////////////////////////
// This section of code implements a ring-buffer queue for holding rays.
// Using a queue of rays instead of a stack
// because earlier child-rays are more important than later.
// ie, if we used a stack, the less-contributing rays would get processed
// earlier than the more-contributing rays, eating up our budgets.
// This is a pretty standard implementation of a ring-buffer, not special to GLSL.
// The only difference from an implementation in say C++ is no error-checking!

// my kingdom for templates in GLSL..
#define QTYPE ray_t
const uint gQCapacity = gMaximumRaysInQueue;
const uint gQNumSlots = gQCapacity + 1u;
QTYPE gQ[gQNumSlots];
uint gQHead = 0u;
uint gQTail = 0u;

// the number of items in the queue
uint QCount() {
	if (gQHead >= gQTail) {
		return gQHead - gQTail;
	}
	else {
		return gQNumSlots - (gQTail - gQHead);
	}
}

// the remaining capacity of the queue
uint QSpaceLeft() {
	return gQCapacity - QCount();
}

bool QIsFull() {
	return QSpaceLeft() == 0u;
}

bool QIsEmpty() {
	return QCount() == 0u;
}

// add an item to the head of the queue.
// only call this if the queue is not empty !
void QEnqueue(QTYPE item) {
	gQHead = (gQHead + 1u) % gQNumSlots;
	gQ[gQHead] = item;
}

// pull an item off the tail of the queue.
// only call this if the queue is not empty !
QTYPE QDequeue() {
	gQTail = (gQTail + 1u) % gQNumSlots;
	return gQ[gQTail];
}

// conditionally add a ray to the queue
bool addRay(in ray_t ray) {
    if (QIsFull()) {
        return false;
    }
    if (ray.contribution < gMinimumRayContribution) {
        return false;
    }
    QEnqueue(ray);
    return true;
}

// do not call this if the queue is empty !
ray_t popRay() {
    return QDequeue();
}

// this section has a bunch of globals which are configured once per pixel per frame,
// and then re-used multiple times.
// for example, rotation matrices used in the core map() function.
// I haven't noticed other people doing this, perhaps there's a reason..
float gViewTheta = 0.0;
vec3  gSceneCenter = gVy * 1.4;
float gRounding = 0.075;
vec3  gLightDirection = normalize(vec3(1.0, -2.0, 1.0));
float gUnderStepFactor = 1.0;
vec2  gRes;
bool  gDoStereo = false;

float gSSZoom;
vec2  gM;
mat2  gSceneRot1;
mat2  gSceneRot2;
mat2  gSceneRot3;
mat2  gSceneRot4;
float gBoxTwist = 0.0;
vec3  gOuterBox;
vec3  gInnerBox;
vec3  gCornerBox;
float gCornerBoxRounding;
float gMouseTargetRad;
vec3  gBallPos;
float gBallRad;
float gBallSep;

// the maximum number of steps to raymarch.
int   gMaxMarchStepsExternal = 250;
int   gMaxMarchStepsInternal = 200;

// pre-calculate values which are used in the core map() sdf function.
void configMap() {
    gSceneRot1 = rot2( -gViewTheta * 0.5);
    gSceneRot2 = rot2(sin(gViewTheta * 4.0) * 0.1);
    gSceneRot3 = rot2(cos(gViewTheta * 4.0) * 0.1);
    gSceneRot4 = rot2(gT);
    
    gOuterBox = vec3(1.0);
    gInnerBox = vec3(mix(0.0, 0.98, smoothstep(-0.9, 0.9, -cos(gT * 0.621))));
    
    float CBf = smoothstep(2.0, 3.0, gT);
    gCornerBox         = CBf * vec3(0.6);
    gCornerBoxRounding = CBf * gRounding;    
    
    gBallRad = 0.3;
    gBallSep = 0.1;

    // these odd numbers like 0.91, 0.31 all over the place
    // are just to keep the various cyclic aspects from lining up
    // so that the exact state of the scene keeps varying over time.
    gBallPos = gSceneCenter;
    gBallPos.x  += -cos(gT * 0.91 ) * 1.5;
    gBallPos.z  +=  sin(gT * 0.91 ) * 1.5;
    gBallPos.y  +=  sin(gT * 0.31) * (1.3 - gBallRad - gBallSep);
    
    // some twist for the box.
    // twisting ruins the accuracy of the distance field,
    // so while twisting we under-step during marching.
    float twistT = gT / 4.123;
    float btc = smoothstep(0.7, 1.0, -cos(twistT));
    gBoxTwist = btc * 0.7 * sign(sin(twistT / 2.0));
    gUnderStepFactor = mix(1.0, 0.82, btc);
}

// more configuration.
void configGlobals0() {
    gMouseTargetRad = 50.0;
}

void configGlobals1() {
    gSSZoom = 0.8;

    gT = iTime * PI / 10.0;
    gSSEps  = 3.0/MINRES/gSSZoom;
    gSSLw   = 2.0/MINRES/gSSZoom;

    vec2 M = iMouse.xy;
    if (gDoStereo || length(M) < gMouseTargetRad) {
        M = (vec2(cos(gT * 0.751) * 0.8, cos(gT * 0.631)) * 0.4 + 0.4) * gRes.xy;
    };
    gM = M / gRes.xy;
    gViewTheta = gM.x * PI * 2.0 + sin(gT * 0.71) * 0.1;
}

// a variable to track the number of calls to map().
// this can be displayed as a 'heat map'.
float gMapCalls = 0.0;

// the core signed-distance-function routine.
float map(vec3 p) {
    float d = 1e9;
    
    gMapCalls += 1.0;
    
    // mod() the incoming point by a fairly large value,
    // just to get some extra geometry in the background.
    float repD = 50.0;
    p.xz = mod(p.xz + repD/2.0, repD) - repD/2.0;
    
    // move to the center of our scene
    vec3 pp = p - gSceneCenter;
    
    // apply some view-dependent wobble
    pp.xz *= gSceneRot1;
    pp.yz *= gSceneRot2;
    pp.xy *= gSceneRot3;
    
    mat2 twist = rot2(pp.z * gBoxTwist);
    pp.xy *= twist;
    
    // the main cube
    d  = opU(d, sdBox   (pp, gOuterBox - gRounding) - gRounding);
    
    // minus a sube inside of it which grows and shrinks.
    d  = opS(d, sdBox   (pp, gInnerBox - gRounding) - gRounding);
   
    // use abs() to now operate on each corner at once
    vec3 ppp = abs(pp) - gV1 * 1.0;
    // subtract a cube from each/the corner
    d = opS(d, sdBox(ppp, gCornerBox) - gCornerBoxRounding);
    
    // move to the ball
    ppp = p - gBallPos;
    
    // subtract a 'force field' from around where the ball will be
    d = opS(d, sdSphere(ppp, gBallRad + gBallSep));
    
    // work in a sub-distance metric.
    // this isn't really needed for what's here now,
    // but it was useful for a more complex ball thing for a while.
    float dd = 1e9;
    // a regular sphere
    dd = opU(dd, sdSphere(ppp, gBallRad));
    // minus a growing/shrinking sphere inside of it.
    dd = opS(dd, sdSphere(ppp, gBallRad * mix(0.0, 0.95, smoothstep(-0.4, 0.4, sin(gT * 2.31)))));
    // union our sub-distance field with the main one.
    d = opU(d, dd);
    
    // add a ground plane.
    // I'm having some strange artifacts with the ground plane and reflectivity.
    d = opU(d, sdPlnY(p));
  
    return d;
}


// Tetrahedral normal technique with a loop to avoid inlining getSDF()
// This should improve compilation times
// https://iquilezles.org/articles/normalsSDF
vec3 getNormal(vec3 p){
    vec3 n = vec3(0.0);
    for(int i = ZERO; i < 4; i++){
        vec3 e = 0.5773*(2.0*vec3((((i+3)>>1)&1),((i>>1)&1),(i&1))-1.0);
        n += e*map(p+e*gWSEps);
    }
    return normalize(n);
}

// return a ray from the camera through our pixel and into the scene.
// uv should be 0 in the center of the viewport, not 0.5.
ray_t getCamRay(vec2 uv, bool isLeftEye) {

    ray_t ray;
    
    float stereoSep = -0.1 * (isLeftEye ? -1.0 : 1.0);
    
    // this seed ray is the whole contribution of the pixel!
    // .. or at least it is until I add anti-aliasing.
    ray.contribution = 1.0;

    float zoomFac = 0.7;
    
    // where the whole camera is looking to.
    // ping-pongs between the flying ball and the main cube.
    float viewPingPong = smoothstep(0.8, 1.0, -cos(gT * 0.113));
    vec3  lookDst = mix(gSceneCenter, gBallPos, viewPingPong);
    
    // where the camera is looking from.
    // also ping-pongs.
    vec3  lookSrc = vec3(cos(gViewTheta), (1.4 - gM.y * 1.3) * 4.0/3.0, sin(gViewTheta)) * 3.0;
    lookSrc = mix(lookSrc, gBallPos * vec3(2.2, 1.3, 2.2), viewPingPong);
    
    // some rough logic to keep the camera a safe distance away from the ball and cube
    float lsl  = length(lookSrc);
    float lsl2 = max(lsl, 4.0);
    lookSrc = lookSrc / lsl * lsl2;
    
    // construct an orthobasis for the camera. altho I don't turn it into an actual matrix.
    vec3  camFw   = normalize(lookDst - lookSrc);
    vec3  camRt   = normalize(cross(camFw, gVy));
    vec3  camUp   = cross(camRt, camFw);
    
    lookSrc += camRt * stereoSep;
    camFw   = normalize(lookDst - lookSrc);
    camRt   = normalize(cross(camFw, gVy));
    camUp   = cross(camRt, camFw);
    

    
    // determine a 'look to' point for this specific pixel.
    // gV0 is vec3(0).
    vec3  p       = gV0;
    p            += lookSrc;
    p            += camFw;
    p            += camRt * uv.x * zoomFac;
    p            += camUp * uv.y * zoomFac;
    
    // copy it to the ray.
    ray.ro        = lookSrc;
    ray.rd        = normalize(p - ray.ro);
    
    // if the camera is initially inside an object, things are going to look weird,
    // but at least this gives them a shot at correctness.
    ray.internal  = map(p) < 0.0;
    
    return ray;
}

// ordinary raymarching.
// return distance along ray to nearest intersection.
// also returns the minimum distance from the ray to a surface. ie, often 0.
float marchExternal(vec3 ro, vec3 rd, out float minD) {
    float t = 0.0;
    minD = 1e9;
    
    vec3 p;
    for (int n = 0; n < gMaxMarchStepsExternal && (dot(p, p) < 1e4); ++n) {
        p = ro + rd * t;
        float d = map(p);
        minD = min(minD, d);
        if (d < gWSEps) {
            minD = 0.0;
            return t;
        }
        
        t += d * gUnderStepFactor;
    }
    
    return 1e9;
}

// raymarching in the negative side of the SDF.
float marchInternal(vec3 ro, vec3 rd) {

    float t = 0.0;
    
    for (int n = 0; n < gMaxMarchStepsInternal && t < 1e2; ++n) {
        vec3  p = ro + rd * t;
        float d = map(p);
        if (d > -gWSEps) {
            return t;
        }

        t -= d * gUnderStepFactor;
    }
    
    return 1e9;
}

// a function for some 'sky'. takes direction only, no positon.
// could be replaced with a cubemap.
vec3 sky(vec3 dir) {
    float theta = atan(dir.z, dir.x);
    vec3 rgb = abs(dir);
    float absst = abs(sin(theta * 10.0 ));
    rgb = mix(rgb, vec3(1.0), 0.2 * smoothstep(0.05, 0.0, absst - 0.2));
    rgb *= mix(1.0, smoothstep(-0.1, 0.1, dir.y), 0.7);
    rgb = mix(rgb, vec3(rgb.x + rgb.y + rgb.z) / 3.0, absst);
    return rgb;
}

// takes a ray off the queue, marches it, potentially adds children to the queue.
// repeats indefinitely until either the queue is empty or we pass a limit.
// in a recursive implementation this would be "processRay()", and would call itself.
vec3 processRays() {

    // rgb is the output pixel color. start with black.
    vec3 rgb = gV0;
    
    // track how many rays we've processed.
    // stop when either that number is too large,
    // or when the queue is empty.
    uint processingIter;
    for (processingIter = 0u;
        processingIter < gQCapacity * 3u && !QIsEmpty();
        ++processingIter) {
        
        // take the oldest ray out of the queue.
        // on the first call to this method, there is only one ray in the queue,
        // so the queue is empty after this call.
        ray_t ray = popRay();
        
        // march either external or internal, depending on the ray.
        float distanceAlongRayToSurface;
        if (ray.internal) {
            distanceAlongRayToSurface = marchInternal(ray.ro, ray.rd);
        }
        else {
            float _unused;
            distanceAlongRayToSurface = marchExternal(ray.ro, ray.rd, _unused);
        }
        
        // if the returned distance is "near", call it a surface.
        // if it's "far", call it the sky.
        if (distanceAlongRayToSurface < 1e4) {
        
            // the position of the intersection
            vec3  p    = ray.ro + distanceAlongRayToSurface * ray.rd;
            vec3  grad = getNormal(p);
            vec3  n    = grad;
            
            // If the ray is internal,
            // we need to adjust the normal to point inwards.
            // We also attenuate the ray's contribution according
            // to how much material the ray passed through.
            if (ray.internal) {
                // surface normal is from the gradient, so flip it.
                n = -n;
                
                // attenuation
                ray.contribution *= exp(-1.0 * distanceAlongRayToSurface);
            }
            
            // poor-man's materials: there's floor, sky, and everything else.
            bool isFloor = p.y < 0.001;
            
            // diffuse component
            float diffAmt = isFloor ? 0.9 : 0.05;
            // dot the surface normal with our light direction
            float diff = max(0.0, dot(n, -gLightDirection));
            
            // shadows
            // only do these if there's a point.
            if (diff * diffAmt * ray.contribution > 0.01) {
                // for shadows we march towards the light
                // and take advantage of the ease with which raymarching
                // tells you approximately how close you came to any surface,
                // and use that for a little soft shadowing.
                // Soft shadowing tends to being out the artifacts in raymarching,
                // so we understep significantly.
                // Don't forget to offset the initial point from the surface by a bit,
                // and not to use 'n' for that, because for internal rays it points inward.
                float minD;
                const float penumbra = 0.1;
                float saveUnderStep = gUnderStepFactor;
                int   saveMaxSteps  = gMaxMarchStepsExternal;
                gMaxMarchStepsExternal = 200;
                gUnderStepFactor *= 0.3;
                marchExternal(p + grad * (penumbra + 0.01), -gLightDirection, minD);
                diff *= smoothstep(0.0, penumbra, minD);
                gUnderStepFactor = saveUnderStep;
                gMaxMarchStepsExternal = saveMaxSteps;
            }
            
            // almost done with diffuse shading.
            // calculate albedo, the inherent color of the surface.
            // it's white for glass, and patterned for the floor.
            // note this is still modulated by the 'diffAmt' factor.
            vec3 albedo = gV1 * 0.5;
            if (isFloor) {
                float x = abs(cos(p.z)) - 0.5 * -cos(p.x * 1.5);
                x = sqrt(x);
                albedo = gV1 * 0.4 * smoothstep(0.49, 0.51, x) + 0.05;
                albedo = (albedo) / (1.0 + length(p) * 0.5);
                albedo.r *= 0.5;
            }
            
            // ambient light.
            // it's good to add in some of this
            // so that albedo texture that's in shadow isn't lost.
            diff = max(0.04, diff);
            
            // add the diffuse lighting to the pixel.
            rgb += diff * ray.contribution * diffAmt * albedo;
            
            // add rays for reflection and transmission

            // "eta" is the greek letter ?.
            // it's the ratio of the two indices of refraction of the mediums.
            // eg, air to glass.
            // we animate the index of refraction a little,

            const float ior_air     = 1.0003;
            const float ior_water   = 1.333;
            const float ior_quartz  = 1.46;
            const float ior_diamond = 2.42;

            float eta = ior_air / ior_diamond;

            // if we're transitioning from inside to outside,
            // eta should be inverted.
            if (ray.internal) {
                eta = 1.0 / eta;
            }

            // The next chunk of code determines the contributions
            // of the reflected and refracted rays.

            // this is the portion of the ray contribution left for
            // reflection and refraction, after whatever diffuse used up.
            float reflectAndRefractAmt = 1.0 - diffAmt;

            // this is what portion of the reflect/refract portion is reflect vs. refract.
            // 0 = all reflection, no transmission
            // 1 = all transmission, no reflection
            float reflectVsRefract = smoothstep(-1.0, -0.5, cos(gT * 0.221));

            reflectVsRefract = min(0.95, reflectVsRefract);


            // glsl conveniently provides these helpers:
            vec3  reflectDir = reflect(ray.rd, n);
            vec3  refractDir = refract(ray.rd, n, eta);

            // refract() returns 0 if there's total internal reflection.
            // it's critical to handle this case because it's common.
            bool totalInternal = dot(refractDir, refractDir) == 0.0;

            // no transmission if total internal reflection.
            // no transmission for the floor
            if (isFloor || totalInternal) {
                reflectVsRefract = 0.0;
            }

            // The portion of the contribution for reflection and transmission
            float reflectAmt = reflectAndRefractAmt * (1.0 - reflectVsRefract);
            float refractAmt = reflectAndRefractAmt * reflectVsRefract;
            float reflectContribution = ray.contribution * reflectAmt;
            float refractContribution = ray.contribution * refractAmt;

            // the origin of the ray for reflection should be offset from the surface a bit.
            // the origin for transmission should be inset into the surface a bit.
            vec3  reflectSrc = p + n * gWSEps * 2.0;
            vec3  refractSrc = p - n * gWSEps * 2.0;


            // enqueue the reflected ray. it copies the parent ray's internal-ness
            if (!addRay(ray_t(reflectSrc, reflectDir, reflectContribution,  ray.internal))) {
                // ray was not enqueued, either the queue was full or the contribution was too small.
                rgb += sky(ray.rd) * reflectContribution;
            }

            // enqueue the transmitted ray. it inverts the parent ray's internal-ness
            if (!addRay(ray_t(refractSrc, refractDir, refractContribution, !ray.internal))) {
                // ray was not enqueued, either the queue was full or the contribution was too small.
                rgb += sky(ray.rd) * refractContribution;
            }
            
        }
        else {
            // this ray did not hit a surface. it's the sky!
            // ray contribution is still super important here,
            // as that's handling all the attenuation due to transmission, reflection.
            rgb += sky(ray.rd) * ray.contribution;
        }
    }
    
    return rgb;
}

void addCircle(inout vec3 rgb, in vec2 p, float rad) {
    float lp = length(p);
    rgb = mix(rgb, gV0, 0.1 * smoothstep(2.0, 0.0,     lp - rad));
    rgb = mix(rgb, gV1, 0.2 * smoothstep(2.0, 0.0, abs(lp - rad)));
}

void mainImage(out vec4 RGBA, in vec2 XY) {
    configGlobals0();
    
    vec2 xy = XY;
    
    gDoStereo = length(vec2(iMouse.x, RES.y - iMouse.y)) < gMouseTargetRad;    
    gRes = gDoStereo ? vec2(RES.x / 2.0, RES.y) : RES.xy;    
    bool isLeftEye = XY.x < gRes.x;    
    if (gDoStereo && !isLeftEye) {
        xy.x -= gRes.x;        
    }
    
    // set up some things we'll re-use
    configGlobals1();
    
    // configure the geometry of the scene.
    // this is called once per pixel here,
    // but map() is called hundreds of times.
    configMap();
    
    // screen-space coordinates with 0,0 at the center
    vec2 uv = (xy - gRes.xy / 2.0) / MINRES * 2.0 / gSSZoom;
    
    // kick things off with a single ray for this pixel
    addRay(getCamRay(uv, isLeftEye));

    // march the ray and all its children
    vec3 rgb = processRays();
    
    // gamma
    rgb = pow(rgb, vec3(1./2.2));
    
    // "UI"
    addCircle(rgb, XY, gMouseTargetRad);
    addCircle(rgb, vec2(XY.x, iResolution.y - XY.y), gMouseTargetRad);

    #ifdef HEATMAP
    float pixelExpense = clamp(gMapCalls/1000.0, 0.0, 1.0);
    rgb   *= 0.2;
    rgb.r += pixelExpense;
    #endif
    
    RGBA = vec4(rgb, 1.0);
}


