//https://www.shadertoy.com/view/MldcD4

#pragma iChannel0 rock_tiles.jpg
#pragma iChannel1 abstract1.jpg     // should use cubemap

// in theory these two sets of three numbers should each add to 1.0
// but I find that not to matter much to the image
// I blame gamma, with no evidence at all

// how much light is reflected, scattered and transmitted
// by the outer surface of the balls
#define EXTGLOSS 0.3
#define EXTMATTE 0.3
#define EXTTRANS 0.7
// and the inner surface
#define INTGLOSS 0.3
#define INTMATTE 0.2
#define INTTRANS 0.8
//#define INTATTENUATION 0.75
//#define INTCOLOR vec4(0.3, 0.9, 0.6, 1.0)
// and the floor
#define FLOORMATTE 0.7
#define FLOORGLOSS 0.4
#define FLOORSMOOTH 4.0
#define FLOORSCALE 0.2
#define FLOORBUMPR 0.005
#define SHADOWDEPTH 0.5
// and the sky
#define HIGHLIGHTBASE 0.8
#define HIGHLIGHTDEPTH 10.0
// refractive index
#define ETA 0.85

// raytracing params
#define RCOUNT 20
#define MAXITER 10
#define MINPROP 0.1
#define ZOOM 1.1

// don't touch this probably
#define OOETA 1./ETA
#define FLOORBUMPX vec2(FLOORBUMPR, 0.)
#define FLOORBUMPY vec2(0., FLOORBUMPR)

// all balls are unit spheres, at least for now
#define BCOUNT 4
vec3[BCOUNT] balls = vec3[BCOUNT](
    vec3(0., 1., 1.1547),
    vec3(-1., 1., -0.57735),
    vec3(+1., 1., -0.57735),
    vec3(0., 2.633, 0.));

struct R {
    bool on;
    vec3 cam;
    vec3 ray;
    float prop;
    int insideBall; // -1 for 'nope'
};
R[RCOUNT] allRays;
    
bool doneAny = false;
void addRay(R newRay) {
    float minProp = 2.0;
    int minI = -1;
    for (int i = 0; i < RCOUNT; ++i)
        if (!allRays[i].on) {
    		doneAny = true;
            allRays[i] = newRay;
            return;
        } else if (allRays[i].prop < minProp) {
            minProp = allRays[i].prop;
            minI = i;
        }
    if (minProp < newRay.prop) {
        doneAny = true;
        allRays[minI] = newRay;
    }
}

float point2rayDistSq(vec3 p, vec3 ray) {
    vec3 dd = p - dot(p, ray) / dot(ray, ray) * ray;
    return dot(dd, dd);
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 uv = (fragCoord - iResolution.xy * 0.5) / iResolution.y;
    vec3 ray = normalize(vec3(uv, ZOOM));
	float theta = iMouse.a > 0.5
        	? -6.3 * iMouse.x / iResolution.x
        	: iTime * 0.165,
        s = sin(theta),
        c = cos(theta);
    addRay(R(true,
             vec3(6. * s,
                  iMouse.a > 0.5
                  	? 2.0 - 2.0 * iMouse.y / iResolution.y
                  	: (1.3 + cos(iTime * 0.1)),
                  6. * c),
             vec3(ray.x * c - ray.z * s,
             	  ray.y,
                  ray.z * -c + ray.x * -s),
             1.0,
             -1));
    
    fragColor = vec4(0.);
    for (int j = 0; doneAny && j < MAXITER; ++j) {
        doneAny = false;
        for (int i = 0; i < RCOUNT; ++i) {
            R ray = allRays[i];
            if (!ray.on || ray.prop < MINPROP) continue;
            allRays[i].on = false;
            if (ray.insideBall >= 0) {
                // calculate where the ray exits the ball
                // add a bit of matte reflection plus some refraction
                // and partial internal (gloss) reflection.
                // we don't calculate how much refraction there should be
                // compared to reflection, or attenuate light as it passes through
                vec3 centre = balls[ray.insideBall],
                    cc = centre - ray.cam,
                    imp = ray.cam + ray.ray *
                    	(dot(cc, ray.ray) +
                     		sqrt(1. - point2rayDistSq(cc, ray.ray)));
                #ifdef INTATTENUATION
                	float atten = pow(INTATTENUATION, length(imp - ray.cam));
                	#ifdef INTCOLOR
                		fragColor += INTCOLOR * (1. - atten) * ray.prop;
                	#endif
                	ray.prop *= atten;
                #endif
                fragColor += max(0., cc.y) * INTMATTE * ray.prop;
                addRay(R(true,
                         imp,
                         refract(ray.ray, centre - imp, OOETA),
                         ray.prop * INTTRANS,
                         -1));
                addRay(R(true,
                         imp,
                         reflect(ray.ray, centre - imp),
                         ray.prop * INTGLOSS,
                         -1));
            } else {
                // check if the ray hits a ball...
                float minD = 1000000000., minDs;
                int minI = -1;
                vec3 minCentre;
                for (int b = 0; b < BCOUNT; ++b) {
                    vec3 centre = balls[b], cc = centre - ray.cam;
                    float ds = point2rayDistSq(cc, ray.ray);
                    if (ds < 1.) {
                        float distancetoCentre = dot(cc, ray.ray);
                        if (distancetoCentre > 0. && distancetoCentre < minD) {
                            minD = distancetoCentre;
                            minDs = ds;
                            minI = b;
                            minCentre = centre;
                        }
                    }
                }
                if (minI >= 0) {
                    // if it did, shade for matte reflection,
                    // and then spawn new rays for gloss and refraction
                	vec3 imp = ray.cam + ray.ray * (minD - sqrt(1. - minDs)),
                        normal = imp - minCentre;
                    fragColor += max(0., normal.y) * EXTMATTE * ray.prop;
                    addRay(R(true,
                            imp,
                            reflect(ray.ray, normal),
                            ray.prop * EXTGLOSS,
                            -1));
                    // all refraction has the one index at the moment
                    // life, or at least one frame, is too short for chromatic abberation
                    addRay(R(true,
                            imp,
                            refract(ray.ray, normal, ETA),
                            ray.prop * EXTTRANS,
                            minI));
                } else if (ray.ray.y < 0.) {
                    // if the ray's pointing down, draw the floor
                    vec2 floorUv = ray.cam.xz -
                        ray.ray.xz * ray.cam.y / ray.ray.y;
                    vec4 c = texture(iChannel0, floorUv * FLOORSCALE);
                    for (int i = 0; i < BCOUNT; ++i) {
                        vec2 xy = balls[i].xz - floorUv;
                        if (dot(xy, xy) < 1.) {
                            c *= SHADOWDEPTH;
                            break;
                        }
                    }
                    fragColor += c * ray.prop * FLOORMATTE;
                    // and let's add a bump-mapped reflection ray
                    float dx = texture(iChannel0, floorUv * FLOORSCALE + FLOORBUMPX).r -
                        	texture(iChannel0, floorUv * FLOORSCALE - FLOORBUMPX).r,
                        dy = texture(iChannel0, floorUv * FLOORSCALE + FLOORBUMPY).r -
                        	texture(iChannel0, floorUv * FLOORSCALE - FLOORBUMPY).r;
                    addRay(R(true,
                            vec3(floorUv.x, 0., floorUv.y),
                            reflect(ray.ray, normalize(vec3(dx, FLOORSMOOTH, dy))),
                            ray.prop * FLOORGLOSS,
                            -1));
                } else {
                    // lastly, add a skybox.
                    vec4 c = texture(iChannel1, vec2(ray.ray));
                    // shoot up the contrast way above 100% for the highlights
                    // so we get bright whites even when ray.prop drops quite low
                    if (c.r > HIGHLIGHTBASE)
                        c.r = (c.r - HIGHLIGHTBASE) * HIGHLIGHTDEPTH + HIGHLIGHTBASE;
                    if (c.g > HIGHLIGHTBASE)
                        c.g = (c.g - HIGHLIGHTBASE) * HIGHLIGHTDEPTH + HIGHLIGHTBASE;
                    if (c.b > HIGHLIGHTBASE)
                        c.b = (c.b - HIGHLIGHTBASE) * HIGHLIGHTDEPTH + HIGHLIGHTBASE;
                    fragColor += c * ray.prop;
                }
            }
        }
    }
    fragColor.a = 1.;
}