/* 
Day 13: Converted the mirrored boxes to glass, made hole in glass, coloured shadows.. that's it.
*/

//#define DEBUG

// Rotation
#define R(p,a) p=cos(a)*p+sin(a)*vec2(-p.y,p.x);
#define kFLIPY vec3(1, -1, 1)

#define kINFINITY 10000.0 // An unimaginably large number

// maximum iteration count
#define kMAXITERS 120
#define kEPSILON 0.001
#define kMAXINTERSECTIONS 1

// refractive index
#define kREFRACT 1.0/1.5

// materials
#define kFLOORMATERIAL 0
#define kGLASSMATERIAL 1
#define kMIRRORMATERIAL 2

#define kFLOORCOLOUR vec4(0.7, 0.65, 0.6, 1.0)
    
// A ray. Has origin + direction.
struct Ray {
    vec3 origin;
    vec3 dir;
};
    
// Distance to nearest surface
struct SDResult {
    float d; // Distance
    int material; // Nearest material
};

// A camera. Has a position and a direction. 
struct Camera {
    vec3 pos;
    Ray ray;
};
    
// A disk. Has position, size, colour.
struct Disk {
    vec3 pos;
    float radius;
    vec3 col;
};
    
struct Sphere {
    vec3 pos;
    float radius;
};
    
struct Box {
	vec3 pos;
	vec3 size;
    float radius;
};
    
float eps = kEPSILON;
float divergence;

// Normalised random number, borrowed from Hornet's noise distributions: https://www.shadertoy.com/view/4ssXRX
float nrand(vec2 n) {
	return fract(sin(dot(n.xy, vec2(12.9898, 78.233)))* 43758.5453);
}

vec3 texrand(vec2 n) {
    return texture(iChannel2, n).xyz;
}
    
// Distance to sphere (signed)
float sphereDist(in Ray ray, in Sphere sphere) {
    return length(ray.origin - sphere.pos) - sphere.radius;
}

// Distance to sphere surface
float uSphereDist(in Ray ray, in Sphere sphere) {
    return abs(length(ray.origin - sphere.pos) - sphere.radius);
}

// Distance to box surface (signed)
float boxDist(in Ray ray, in Box box) {
    vec3 dist = abs(ray.origin - box.pos) - (box.size * 0.5);
    vec3 cDist = max(dist, 0.0);
    return min(max(dist.x, max(dist.y, dist.z)), 0.0) + length(cDist) - box.radius;
}

// Distance to box surface
float uBoxDist(in Ray ray, in Box box) {
    return abs(length(max(abs(ray.origin - box.pos) - (box.size * 0.5), 0.0)) - box.radius);
}

// distance to floor
float floorDist(in Ray ray) {
    float dist = ray.origin.y;
    return dist;
}

float hashForCell(in vec3 pos, in float cellSize) {
    float hash = nrand(floor(pos.xz / cellSize) + 68.0);
    return hash;
}

vec3 randomColourForCell(in vec3 pos, in float cellSize) {
	float hash = hashForCell(pos, cellSize); 
    return vec3(
        nrand(vec2(hash * 2.0, hash * 4.0)),
        nrand(vec2(hash * 4.0, hash * 8.0)),
        nrand(vec2(hash * 8.0, hash * 16.0))
	);
	vec3 c = vec3(hash, mod(hash + 0.15, 1.0), mod(hash + 0.3, 1.0)) * 0.75;
}

// Get the distance to the scene (returns a struct containing distance and nearest material)
SDResult sceneDist(in Ray ray) {
    SDResult result;
    
    // Get distance to floor
    float distToFloor = floorDist(ray);
    
    // We'll mess with the ray but need the origin later, so preserve it
    vec3 o = ray.origin;
    float rippleBase = sin(o.x * 8.0);
    
    float boxSize = 6.0;
    
    // Repeat the ray through space. Anything inside this space naturally gets repeated...

        ray.origin.xz = mod(ray.origin.xz, vec2(20.0)); 
        
    
    // Get a hash value for the 'cell' we're in
    float hash = hashForCell(o, 20.0);
    vec3 offset = vec3(
        10.0 + sin(hash * 8.0) * 4.0,
        0.0,
        10.0 + sin(hash * 8.0 + 1.5) * 4.0
        );
    float y = cos(hash * 4.0) * boxSize * 0.5;
    
    offset.y += boxSize * 0.5 + sin(hash * 4.0);
                                    
    Sphere sphere0 = Sphere(offset, 3.0);
    float distToSphere = sphereDist(ray, sphere0);
    
    // Rotate the cube by rotating the ray...
    ray.origin -= offset;
    ray.origin.xy = R(ray.origin.xy, hash * 4.0);
    offset.y += sin(hash * 4.0 + 4.0);
    ray.origin.xz = R(ray.origin.xz, hash * 4.0 + 4.0);
    ray.origin += offset;
    
    // A box
    Box b0 = Box(offset, vec3(boxSize), 0.4);
    
    // get the distance 
    float distToB0 = boxDist(ray, b0);
    
    rippleBase = mix(sin(distToB0 * 8.0), rippleBase, smoothstep(2.5, 4.0, distToB0));
    distToFloor += rippleBase* 0.1;
    
    //Find the neares of the floor and box0
    distToFloor = max(distToFloor, -distToB0 + eps);
    result.d = min(distToFloor, max(distToB0, -distToSphere));
    result.material = result.d == distToFloor ? kFLOORMATERIAL : kGLASSMATERIAL;
    
    return result;
}

// Gets the normal
vec3 normal(in Ray ray) {
    vec2 eps = vec2(0.0001, 0);
    float baseDist = sceneDist(ray).d;
 	return normalize(vec3(
        sceneDist(Ray(ray.origin + eps.xyy, ray.dir)).d - baseDist,
        sceneDist(Ray(ray.origin + eps.yxy, ray.dir)).d - baseDist,
        sceneDist(Ray(ray.origin + eps.yyx, ray.dir)).d - baseDist
        ));
}

// Moves the ray to the surface. Helps avoid artefacts due to ray intersection imprecision.
void clampToSurface(in Ray ray, in float d, inout vec3 n) {
 	ray.origin += n * d;
 	d = sceneDist(ray).d;
 	n = normal(ray);
}

// Calulcate a fresnel term for reflections
float fresnelTerm(in Ray ray, in vec3 n, in float power) {
	float fresnel = min(1., dot(ray.dir, n) + 1.0);
	fresnel = pow(fresnel, power);
    return fresnel;
}

/*
---- LIGHTING ----
*/

float occlusion(in Ray ray, in vec3 n) {
    float o = 0.0;
    ray.dir = n;
    float x = 0.1;
    for (int i=0; i<5; i++) {
    	ray.origin += x;
        float d = sceneDist(ray).d;
        o += max(x - d, 0.0);
        
        x *= 2.0;
    }
 	return 1.0 - o * 0.5;;
}

// The main marching loop
void marchRay(inout Ray ray, inout vec4 colour) {
    bool inside = false; // are we inside or outside the glass object
    vec4 impact = vec4(1.0); // This decreases each time the ray passes through glass, darkening colours
    bool hit = false;
#ifdef DEBUG   
vec4 debugColour = vec4(1, 0, 0, 1);
#endif
    
    SDResult result;
    vec3 n;
    vec3 glassStartPos;
    
    for (int i=0; i<kMAXITERS; i++) {
        // Get distance to nearest surface
        result = sceneDist(ray);
        
        // Step half that distance along ray (helps reduce artefacts)
        float stepDistance = result.material == kGLASSMATERIAL ? abs(result.d) : result.d;
        ray.origin += ray.dir * stepDistance * 0.3;  
        
        if (stepDistance < eps) {
            // colision
            
            // normal
            // Get the normal, then clamp the intersection to the surface
    		n = normal(ray);
            clampToSurface(ray, stepDistance, n);
    
            hit = true;
            
            if ( result.material == kFLOORMATERIAL ) {
                // ray hit floor
                
                // Add some noise to the normal, since this is pretending to be grit...
                vec3 randomNoise = texrand(ray.origin.xz * 0.4);
                n = mix(n, normalize(vec3(randomNoise.x, 1, randomNoise.y)), randomNoise.z);
                
                // Colour is just grey with crappy fake lighting...
                colour += mix(
                    kFLOORCOLOUR, 
                    vec4(0,0,0,1), 
                    pow(max((-n.x+n.y) * 0.5, 0.0), 2.0)
                ) * impact;
                impact *= 0.;
                break;
            }
            
            // check what material it is...
            
            if (result.material == kMIRRORMATERIAL) {
                // it's a mirror, reflect the ray
                ray.dir = reflect(ray.dir, n);
                    
                // Step 2x epsilon into object along normal to ensure we're beyond the surface
                // (prevents multiple intersections with same surface)
                ray.origin += n * eps * 2.0;
                float hash = hashForCell(ray.origin, 1.0);
                vec3 c = vec3(hash, mod(hash + 0.15, 1.0), mod(hash + 0.3, 1.0)) * 0.75;
                impact *= vec4(c, 1.0);
                    
#ifdef DEBUG
    debugColour.rgb = ray.dir;
    break;
#endif
            } else {
                // glass material
            
                if (inside) {
                	// refract glass -> air
                	ray.dir = refract(-n, -ray.dir, 1.0/kREFRACT);
                    
                    // Find out how much to tint (how far through the glass did we go?)
                    float glassTravelDist =  1.-clamp(distance(glassStartPos, ray.origin) / 16.0, 0., 1.);
                    
                    // Get a random colour
                    float hash = hashForCell(ray.origin, 20.0); 
                    vec3 c = randomColourForCell(ray.origin, 20.0);
                	impact *= mix(vec4(1), vec4(c, 1.0), glassTravelDist);
                    
#ifdef DEBUG
    debugColour.rgb = vec3(1);
    break;
#endif
      
                
              	} else {
               		// refract air -> glass
                	glassStartPos = ray.origin;
                    
              	  	// Mix the reflection in, according to the fresnel term
                	float fresnel = fresnelTerm(ray, n, 2.0);
    				colour = mix(
                    	colour, 
                    	texture(iChannel1, reflect(ray.dir, n) * kFLIPY), 
                    	vec4(fresnel) * impact);
                    impact *= 1.0 - fresnel;
    			
                	// refract the ray
            		ray.dir = refract(ray.dir, n, kREFRACT);
                }
            
            		// Flip in/out status
           		 	inside = !inside;
            		ray.origin += (inside ? -n : n) * eps * 2.0;
            }
            
            // Intersection count inc, break if over limit
            if (ray.dir.y > 0.0 && ray.origin.y > 5.0) { break; }
            
            // Step 2x epsilon into object along normal to ensure we're beyond the surface
            // (prevents multiple intersections with same surface)
        }
        
        // increase epsilon
        eps += divergence * stepDistance;
    }
    
    // So far we've traced the ray and accumulated reflections, now we need to add the background.
    //if (result.material != kFLOORMATERIAL) {
    if (ray.dir.y > 0.0) {
        colour += texture(iChannel0, ray.dir * kFLIPY) * impact;// / float(steps+1);
        //colour = vec4(1,0,0,1);
    } else {
        colour += kFLOORCOLOUR * impact;
        float occludeFactor = occlusion(ray, n);
        colour.rgb *= mix(vec3(1), randomColourForCell(ray.origin, 20.0) * 0.25, 1.-occludeFactor);
    }
    //}
#ifdef DEBUG
 //   debugColour.rgb = ray.dir;
colour = debugColour;
//colour.rgb = vec3(float(steps)/8.);
//colour.rgb = ray.dir;
#endif
}

// Sets up a camera at a position, pointing at a target.
// uv = fragment position (-1..1) and fov is >0 (<1 is telephoto, 1 is standard, 2 is fisheye-like)
Camera setupCam(in vec3 pos, in vec3 target, in float fov, in vec2 uv) {
		// cam setup
    // Create camera at pos
	Camera cam;
    cam.pos = pos;
    
    // A ray too
    Ray ray;
    ray.origin = pos;
    
    // FOV is a simple affair...
    uv *= fov;
    
    // Now we determine hte ray direction
	vec3 cw = normalize (target - pos );
	vec3 cp = vec3 (0.0, 1.0, 0.0);
	vec3 cu = normalize ( cross(cw,cp) );
	vec3 cv = normalize ( cross (cu,cw) );
    
	ray.dir = normalize ( uv.x*cu + uv.y*cv + 0.5 *cw);
    
    // Add the ray to the camera and our work here is done.
	cam.ray = ray;
    
    // Ray divergence
    divergence = fov / iResolution.x;
    
	return cam;
}

vec3 camPath(in float time) {
    float r = 15.0;
    return vec3(sin(time) * r, sin(time*2.) + r, cos(time) * r);
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // We'll need a camera. And some perspective.
    
	// Get some coords for the camera angle from the frag coords. Convert to -1..1 range.
    vec2 uv = fragCoord.xy / iResolution.xy;
    uv = uv * 2. - 1.;
    
    // Aspect correction so we don't get oval bokeh
    uv.y *= iResolution.y/iResolution.x;
    
    // Make a camera with ALL NEW AND IMPROVED! camera code :)
    float camTime = iTime / 4.0;
    vec3 camPos = camPath(camTime);
    vec3 camTarget = vec3(0);
    //camTarget.y -= 3.0;
    Camera cam = setupCam(camPos, camTarget, 0.500, uv);
    
    // Let's raymarch some stuff and inject that into the scene...
    
    // Create an empty colour
    vec4 col = vec4(0.0);
    
    // Trace that ray!
    marchRay(cam.ray, col);
    
	fragColor = vec4(col.rgb,1.0);
}
