precision highp float;
// COMMON DEFINITIONS
#define EPS 0.002
#define FLOAT_MAX float(0xffffffffu)
#define MOD(A, B) (A - B * floor(A / B))
#define MIN2(A) (min(A.x, A.y))
#define MIN3(A) (min(A.x, min(A.y, A.z)))
#define CROSS(X, Y) vec3(X.y*Y.z - X.z*Y.y, X.z*Y.x - X.x*Y.z, X.x*Y.y - X.y*Y.x)
#define SATURATE(A) clamp(A, 0.0, 1.0)
#define PI (3.14159265359)
#define TAU (6.28318530718)
#define OO vec2(0.0, 0.0)
#define IO vec2(1.0, 0.0)
#define OI vec2(0.0, 1.0)
#define II vec2(1.0, 1.0)
#define JO vec2(-1.0, 0.0)
#define OJ vec2(0.0, -1.0)
#define JJ vec2(-1.0, -1.0)
#define IJ vec2(1.0, -1.0)
#define JI vec2(-1.0, 1.0)
#define OOO vec3(0.0, 0.0, 0.0)
#define IOO vec3(1.0, 0.0, 0.0)
#define OIO vec3(0.0, 1.0, 0.0)
#define OOI vec3(0.0, 0.0, 1.0)
#define IOI vec3(1.0, 0.0, 1.0)
#define IIO vec3(1.0, 1.0, 0.0)
#define OII vec3(0.0, 1.0, 1.0)
#define III vec3(1.0, 1.0, 1.0)
#define JOO vec3(-1.0, 0.0, 0.0)
#define OJO vec3(0.0, -1.0, 0.0)
#define OOJ vec3(0.0, 0.0, -1.0)
#define JJO vec3(-1.0, -1.0, 0.0)
#define JOJ vec3(-1.0, 0.0, -1.0)
#define OJJ vec3(0.0, -1.0, -1.0)
#define JJJ vec3(-1.0, -1.0, -1.0)
#define IJJ vec3(1.0, -1.0, -1.0)
#define JIJ vec3(-1.0, 1.0, -1.0)
#define JJI vec3(-1.0, -1.0, 1.0)
#define IIJ vec3(1.0, 1.0, -1.0)
#define IJI vec3(1.0, -1.0, 1.0)
#define JII vec3(-1.0, 1.0, 1.0)
#define IOJ vec3(1.0, 0.0, -1.0)
#define JIO vec3(-1.0, 1.0, 0.0)
#define IJO vec3(1.0, -1.0, 0.0)
//TRANSFORMATIONS
mat3 TransformBasis(vec3 y, vec3 z)
{
   vec3 x = normalize(CROSS(y, z));
   y = CROSS(z, x);
   return mat3(x, y, z);
}
mat2 rotate2d(float a) {
   float s = sin(a);
   float c = cos(a);
   return mat2(c, -s, s, c);
}
//DISTANCE FUNCTIONS
float sdSphere(vec3 p, float r)
{
   return length(p) - r;
}
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 sdStairs(vec2 p)
{
   vec2 pf = vec2(p.x + p.y, p.x + p.y) * 0.5;
   vec2 d = p - vec2(floor(pf.x), ceil(pf.y));
   vec2 d2 = p - vec2(floor(pf.x + 0.5), floor(pf.y + 0.5));
   float d3 = length(vec2(min(d.x, 0.0), max(d.y, 0.0)));
   float d4 = length(vec2(max(d2.x, 0.0), min(d2.y, 0.0)));
   return d3 - d4;
}
float sdStairs(vec2 p, float h)
{
   p.xy = p.y < p.x ? p.yx : p.xy;
   return sdStairs(p - vec2(0.0, h));
}
float sdStairs(vec3 p, float h, float w)
{
   float x = abs(p.x) - w;
   float d = sdStairs(p.zy, h);
   return max(x, d);
}
//INFO
struct Surface {
   int surfaceId;
   int objectId;
   float distance;
};
struct Material {
   vec3 baseColor;
   float roughness;
   vec3 emission;
};
Surface minSurface(Surface a, Surface b)
{
   if (a.distance < b.distance)
   {
       return a;
   }
   return b;
}
//HASH FUNCTIONS
uint Pcg(uint v)
{
       uint state = v * 747796405u + 2891336453u;
       uint word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;
       return (word >> 22u) ^ word;
}
uvec2 Pcg2d(uvec2 v)
{
   v = v * 1664525u + 1013904223u;
   v.x += v.y * 1664525u;
   v.y += v.x * 1664525u;
   v = v ^ (v>>16u);
   v.x += v.y * 1664525u;
   v.y += v.x * 1664525u;
   v = v ^ (v>>16u);
   return v;
}
uvec3 Pcg3d(uvec3 v)
{
   v = v * 1664525u + 1013904223u;
   v.x += v.y*v.z;
   v.y += v.z*v.x;
   v.z += v.x*v.y;
   v ^= v >> 16u;
   v.x += v.y*v.z;
   v.y += v.z*v.x;
   v.z += v.x*v.y;
   return v;
}
uvec4 Pcg4d(uvec4 v)
{
   v = v * 1664525u + 1013904223u;
   v.x += v.y*v.w;
   v.y += v.z*v.x;
   v.z += v.x*v.y;
   v.w += v.y*v.z;
   v ^= v >> 16u;
   v.x += v.y*v.w;
   v.y += v.z*v.x;
   v.z += v.x*v.y;
   v.w += v.y*v.z;
   return v;
}
float Pcg01(uint v) { return float(Pcg(v)) / FLOAT_MAX; }
vec2 Pcg01(uvec2 v) { return vec2(Pcg2d(v)) / FLOAT_MAX; }
vec3 Pcg01(uvec3 v) { return vec3(Pcg3d(v)) / FLOAT_MAX; }
vec4 Pcg01(uvec4 v) { return vec4(Pcg4d(v)) / FLOAT_MAX; }
float Pcg01(int v) { return Pcg01(uint(v)); }
vec2 Pcg01(ivec2 v) { return Pcg01(uvec2(v)); }
vec3 Pcg01(ivec3 v) { return Pcg01(uvec3(v)); }
vec4 Pcg01(ivec4 v) { return Pcg01(uvec4(v)); }
float Pcg01(float v) { return Pcg01(floatBitsToUint(v)); }
vec2 Pcg01(vec2 v) { return Pcg01(floatBitsToUint(v)); }
vec3 Pcg01(vec3 v) { return Pcg01(floatBitsToUint(v)); }
vec4 Pcg01(vec4 v) { return Pcg01(floatBitsToUint(v)); }
//BRDF FUNCTIONS
vec3 Lambert(vec3 baseColor, vec3 N, vec3 L)
{
   return baseColor / dot(N, L);
}
vec3 FresnelSchlick(float VdotH, vec3 F0) {
   // return F0 + (1.0 - F0) * pow(1.0 - VdotH, 5.0);
   return F0 + (1.0 - F0) * exp2((-5.55473 * VdotH - 6.98316) * VdotH);
}
vec3 GGX(vec3 N, vec3 V, vec3 L, float roughness, vec3 baseColor)
{
   vec3 H = normalize(L + V);
   float NdotH = max(dot(N, H), 0.0);
   float NdotV = max(dot(N, V), 0.0);
   float NdotL = max(dot(N, L), 0.0);
   float VdotH = max(dot(V, H), 0.0);
   float r = (roughness + 1.0);
   float k = (r * r) / 8.0;
   vec3 F = FresnelSchlick(VdotH, baseColor);
   return F * VdotH / (NdotH * (NdotV * (1.0 - k) + k) * (NdotL * (1.0 - k) + k));
}
//SAMPLING FUNCTIONS
vec3 SampleSphere(vec2 xi)
{
       float a = xi.x * PI * 2.0;
       float z = xi.y * 2.0 - 1.0;
   float r = sqrt(1.0 - z * z);
       return vec3(r * cos(a), r * sin(a), z);
}
vec3 SampleHemiSphere(vec2 xi, vec3 dir)
{
       vec3 v = SampleSphere(xi);
       return dot(dir, v) < 0.0 ? -v : v;
}
vec3 ImportanceSampleGGX(vec2 xi, float roughness, vec3 n, vec3 v)
{
   float a = roughness * roughness;
   float phi = 2.0 * PI * xi.x;
   float cosTheta = sqrt((1.0 - xi.y) / (1.0 + (a * a - 1.0) * xi.y));
   float sinTheta = sqrt(1.0 - cosTheta * cosTheta);
   vec3 h = vec3(sinTheta * cos(phi), sinTheta * sin(phi), cosTheta);
   vec3 up = abs(n.z) < 0.999 ? vec3(0, 0, 1) : vec3(1, 0, 0);
   vec3 tangentX = CROSS(up, n);
   vec3 tangentY = CROSS(n, tangentX);
   return reflect(v, tangentX * h.x + tangentY * h.y + n * h.z);
}
vec3 ImportanceSampleLambert(vec2 xi, vec3 n)
{
   float phi = 2.0 * PI * xi.x;
   float cosTheta = sqrt(1.0f - xi.x);
   float sinTheta = sqrt(xi.y);
   vec3 h = vec3(sinTheta * cos(phi), sinTheta * sin(phi), cosTheta);
   vec3 up = abs(n.z) < 0.999 ? vec3(0, 0, 1) : vec3(1, 0, 0);
   vec3 tangentX = CROSS(up, n);
   vec3 tangentY = CROSS(n, tangentX);
   return tangentX * h.x + tangentY * h.y + n * h.z;
}

uniform vec2 resolution;
uniform float time;
uniform sampler2D backbuffer;

const float dof = 1.5;
const int marchingStep = 70;
const float maxDistance = 400.0;
const float stairsHeight = 10.5;
const float stairsWidth = 11.0;
const float stairsTilingSize = 150.0;
const int bounceLimit = 2;
const int iterMax = 2;
const float wallWidth = 0.48;

const int ballNum = 15;
const float ballRadius = 5.0;
const float deltaTime = 0.05;
const float gravity = 1.0;
const float attraction = 10.0;
const float reflection = 10.0;
const float friction = -20.0;
const float randomize = 1.0;
const vec3 ballPosMax = vec3(200, 150, 400);
const vec3 ballPosMin = vec3(-200, -150, 75);
const vec3 ballVelMin = vec3(-100, -100, -100);
const vec3 ballVelMax = vec3(100, 100, 100);
vec3 ballPos[ballNum];
vec3 ballVel[ballNum];

vec3 cameraPos, cameraDir, cameraUp;

const float bpm = 84.0;
float elapsedTime;
float beatTime;
float sbeatTime;
const int phaseNum = 15;
const int phaseBPM[phaseNum + 1] =  int[](0, 8, 16, 20, 24, 32, 36, 40, 48, 64, 72, 80, 96, 112, 128, 144);
float phasePeriod[phaseNum + 1];
float phaseFrag[phaseNum];
float phaseTime = 0.0;

void CalcBeatTime()
{
   float scaledTime = elapsedTime * bpm / 60.0;
   beatTime = floor(scaledTime);
   sbeatTime = fract(scaledTime);
   sbeatTime = beatTime + pow(sbeatTime, 20.0);
}

void CalcPhase()
{
   phaseTime = 0.0;
   for(int i = 0; i < phaseNum; i++){
       phasePeriod[i + 1] = float(phaseBPM[i + 1]) / bpm * 60.0;
       phaseFrag[i] = step(phasePeriod[i], elapsedTime) * step(elapsedTime, phasePeriod[i + 1]);
       phaseTime += phaseFrag[i] * (elapsedTime - phasePeriod[i]);
   }
}

void CalcCameraParams(){
   if(phaseFrag[10] < 0.5) cameraUp = vec3(0.0, 1.0, 0.0);
   if(phaseFrag[10] > 0.5) cameraUp = vec3(0.0, cos(-phaseTime * 0.1 + PI * 0.25), sin(-phaseTime * 0.1 + PI * 0.25));

   if(phaseFrag[0] > 0.5) cameraDir = vec3(0.0, 0.0, 1.0);
   if(phaseFrag[1] > 0.5) cameraDir = vec3(cos(phaseTime * 0.1), 0.0, sin(phaseTime * 0.1));
   if(phaseFrag[2] > 0.5) cameraDir = vec3(cos(phaseTime * 0.1 + PI * 0.75), 0.0, sin(phaseTime * 0.1 + PI * 0.75));
   if(phaseFrag[3] > 0.5) cameraDir = vec3(cos(-phaseTime * 0.07 + PI), sin(-phaseTime * 0.08 + PI), sin(-phaseTime * 0.07 + PI));
   if(phaseFrag[4] > 0.5) cameraDir = vec3(-1.0, 0.2, 0.8);
   if(phaseFrag[5] > 0.5) cameraDir = vec3(-1.0, 0.0, 1.0);
   if(phaseFrag[6] > 0.5) cameraDir = vec3(-1.5, sin(-phaseTime * 0.08 + PI), 1.0);
   if(phaseFrag[7] > 0.5) cameraDir = vec3(1.0, sin(-phaseTime * 0.08 + PI), 1.0);
   if(phaseFrag[8] > 0.5) cameraDir = vec3(0.0, 0.0, 1.0);
   if(phaseFrag[9] > 0.5) cameraDir = vec3(cos(phaseTime * 0.2 + PI * 0.25), 0.0, sin(phaseTime * 0.2 + PI * 0.25));
   if(phaseFrag[10] > 0.5) cameraDir = vec3(0.0, cos(-phaseTime * 0.1 + PI * 0.6), sin(-phaseTime * 0.1 + PI * 0.6));
   if(phaseFrag[11] > 0.5) cameraDir = vec3(0.0, 0.0, 1.0);
   if(phaseFrag[12] > 0.5) cameraDir = vec3(cos(-phaseTime * 0.1 + PI * 0.25), 0.0, sin(-phaseTime * 0.1 + PI * 0.25));
   if(phaseFrag[13] > 0.5) cameraDir = vec3(vec3(cos(phaseTime * 0.1 + PI * 0.5), -sin(phaseTime * 0.1 + PI * 0.5), sin(phaseTime * 0.1 + PI * 0.5)));
   if(phaseFrag[14] > 0.5) cameraDir = vec3(cos(phaseTime * 0.1 + PI * 1.5), -0.5, sin(phaseTime * 0.1 + PI * 1.5));

   if(phaseFrag[0] > 0.5) cameraPos = (vec3(0.0, -20.0, 15.0) + cameraDir * phaseTime * 0.8);
   if(phaseFrag[1] > 0.5) cameraPos = (vec3(0.0, -20.0, 15.0));
   if(phaseFrag[2] > 0.5) cameraPos = (vec3(0.0, -20.0, 15.0));
   if(phaseFrag[3] > 0.5) cameraPos = (vec3(0.0, -20.0, 15.0));
   if(phaseFrag[4] > 0.5) cameraPos = (vec3(4.0, -62.0, 1.0));
   if(phaseFrag[5] > 0.5) cameraPos = (vec3(0.0, -50.0 + phaseTime * 1.0, -15.0 + phaseTime * 1.0));
   if(phaseFrag[6] > 0.5) cameraPos = (vec3(4.0, -40.0, 1.0));
   if(phaseFrag[7] > 0.5) cameraPos = (vec3(2.0, -30.0, 1.0));
   if(phaseFrag[8] > 0.5) cameraPos = (vec3(0.0, -10.0 + phaseTime * 1.0, 25.0 + phaseTime * 1.0));
   if(phaseFrag[9] > 0.5) cameraPos = (vec3(0.0, 20.0, 55.0));
   if(phaseFrag[10] > 0.5) cameraPos = (vec3(0.0, 20.0, 55.0));
   if(phaseFrag[11] > 0.5) cameraPos = (vec3(0.0, 20.0, 55.0 + phaseTime * 1.5));
   if(phaseFrag[12] > 0.5) cameraPos = (vec3(0.0, 15.0, 225.0) - cameraDir * 50.0);
   if(phaseFrag[13] > 0.5) cameraPos = (vec3(0.0, 15.0, 225.0)  - cameraDir * 80.0);
   if(phaseFrag[14] > 0.5) cameraPos = (vec3(0.0, 25.0, 75.0) - cameraDir * 130.0);
}

float sdTruchetStairs(vec3 p, float s, float t)
{
   float hs = s * 0.5;
   vec3 p1 = mix(p - hs * IIO, p + hs * JOJ, t);
   vec3 p2 = mix(p.zyx * JII + hs * IOJ, p.yzx + hs * JIO, t);
   vec3 p3 = mix(p.yxz * JII + hs * IIO, p.zyx * JII + hs * IJO, t);
   float d1 = sdStairs(p1 - OII * sbeatTime * 3.0 * step(phasePeriod[8], elapsedTime), stairsHeight, stairsWidth);
   float d2 = sdStairs(p2 - OII * sbeatTime * 3.0 * step(phasePeriod[8], elapsedTime), stairsHeight, stairsWidth);
   float d3 = sdStairs(p3 - OII * sbeatTime * 3.0 * step(phasePeriod[8], elapsedTime), stairsHeight, stairsWidth);
   return min(min(d1, d2), d3);
}

float sdTruchetTiledStairs(vec3 p, float s){
   float hs = s * 0.5;
   vec3 pf = MOD(p, s);
   vec3 pi = p - pf;
   float s1 = Pcg01(pi + vec3(0, 38, 0)).x;
   float s2 = Pcg01(pi + vec3(hs, 0, 0)).x;
   pf.xz = s1 < 0.25 ? (pf - hs).zx * JI + hs:
           s1 < 0.5 ? (pf - hs).xz * JJ + hs:
           s1 < 0.75 ? (pf - hs).zx * IJ + hs:
           pf.xz;
   return sdTruchetStairs(pf, s, step(s2, 0.5));
}

#define STAIRS_MAP(p) sdTruchetTiledStairs(p - vec3(stairsTilingSize * 0.5, stairsTilingSize * 0.5, stairsTilingSize * 0.5), stairsTilingSize)
vec3 GetStairsGrad(vec3 p)
{
   const float e = EPS;
   const vec2 k = vec2(1, -1);
   return k.xyy * STAIRS_MAP(p + k.xyy * e) +
           k.yyx * STAIRS_MAP(p + k.yyx * e) +
           k.yxy * STAIRS_MAP(p + k.yxy * e) +
           k.xxx * STAIRS_MAP(p + k.xxx * e);
}

Surface WallMap(vec3 p)
{
   p.y += (phaseTime * 10.0 * Pcg01(int(p.z))) * phaseFrag[14];

   ivec3 seed = ivec3(0.0, p.y, p.z);
   vec3 hash = Pcg01(seed);

   vec3 p1 = vec3(p.x, fract(p.y) - 0.5, fract(p.z) - 0.5);

   float dx =
       SATURATE(-(abs((floor(-p.y) + floor(p.z)) * 0.1 + hash.x * 0.4 - 6.2) - 1.0)) * pow(abs(sin(phaseTime / phasePeriod[1] * 1.0 * PI)), 4.0) * phaseFrag[0] +
       SATURATE(-(abs((floor(-p.y) + floor(p.z)) * 0.1 + hash.x * 0.4 - 7.5 + phaseTime) - 1.0)) * phaseFrag[1] +
       SATURATE(-(abs((floor(-p.y) + floor(p.z)) * 0.05 + hash.x * 0.4 - 10.0 + phaseTime * 1.6) - 1.0)) * phaseFrag[1] +
       SATURATE(-(abs((floor(-p.y) + floor(p.z)) * 0.1 + hash.x * 1.5 - 7.5 + phaseTime) - 1.0)) * phaseFrag[2] +
       SATURATE(-(abs((floor(-p.y) + floor(p.z)) * 0.1 + hash.x * 1.5 - 5.0 + phaseTime) - 1.0)) * phaseFrag[3] +
       SATURATE(abs(sin(length(floor(vec3(0.0, p.y + 60.0, p.z))) * 0.2 + hash.x * 0.6 - phaseTime * 1.5) * 1.5) - 0.5) * phaseFrag[4] +
       step(Pcg01(ivec4(vec4(p.x, p.y, floor(p.z) * 0.3 * Pcg01(int(sbeatTime)), sbeatTime * 10.0))).x, 0.3) * (phaseFrag[5] + phaseFrag[6] + phaseFrag[7]) +
       mix(
           step(Pcg01(ivec4(vec4(p.x, p.y, floor(p.z) * 0.3 * Pcg01(int(sbeatTime)), sbeatTime * 10.0))).x, 0.3),
           SATURATE(sin(sbeatTime * 2.0 + (hash.x + hash.y) * PI) - 0.3),
           SATURATE(phaseTime / phasePeriod[9] * 1.3)
       ) * phaseFrag[8] +
       SATURATE(sin(sbeatTime * 2.0 + (hash.x + hash.y) * PI) - 0.3) * (phaseFrag[9] + phaseFrag[10] + phaseFrag[11]) +
       3.0 * (phaseFrag[12] + phaseFrag[13] + phaseFrag[14])
   ;

   float d = dx * wallWidth * 2.0;
   vec3 delta = IOO * (d - (stairsWidth + wallWidth));
   vec3 bSize = III * wallWidth * 0.8;
   vec3 pSizeV = bSize * vec3(1.0, 0.15, 1.0);
   vec3 pSizeH = bSize * vec3(1.0, 1.0, 0.15);

   float sdWall1 = sdBox(p1 - delta, wallWidth * III);
   sdWall1 = max(sdWall1,
       -mix(
           min(sdBox(p1 - wallWidth * IOO - delta, pSizeV), sdBox(p1 - wallWidth * IOO - delta, pSizeH)),
           sdBox(p1 - wallWidth * IOO - delta, bSize), 1.0 - SATURATE(dx)));
   sdWall1 = max(sdWall1, sdBox(p, stairsTilingSize * 0.5 * III));

   float sdWall2 = sdBox(p1 + delta, wallWidth * III);
   sdWall2 = max(sdWall2,
       -mix(
           min(sdBox(p1 + wallWidth * IOO + delta, pSizeV), sdBox(p1 + wallWidth * IOO + delta, pSizeH)),
           sdBox(p1 + wallWidth * IOO + delta, bSize), 1.0 - SATURATE(dx)));
   sdWall2 = max(sdWall2, sdBox(p, stairsTilingSize * 0.5 * III));

   Surface wall1 = Surface(2, 0, sdWall1);
   Surface wall2 = Surface(2, 1, sdWall2);

   return minSurface(wall1, wall2);
}

void LoadBallParams(vec2 resolution)
{
   for(int i = 0; i < ballNum; i++)
   {
       vec2 uv1 = (vec2(i, 0) + vec2(0.5, 0.5)) / resolution;
       vec2 uv2 = (vec2(i, 1) + vec2(0.5, 0.5)) / resolution;
       vec3 p = texture(backbuffer, uv1).xyz;
       vec3 v = texture(backbuffer, uv2).xyz;
       ballPos[i] = p * (ballPosMax - ballPosMin) + ballPosMin;
       ballVel[i] = v * (ballVelMax - ballVelMin) + ballVelMin;
   }
}

void SaveBallParams(ivec2 fragCoord, inout vec4 col)
{
   for(int i = 0; i < ballNum; i++)
   {
       vec3 vel = ballVel[i];
       vec3 pos = ballPos[i];

       vec3 rand = Pcg01(vec4(pos.xz, i, elapsedTime)).xyz * 2.0 - 1.0;
       float d = STAIRS_MAP(pos);
       vec3 g = GetStairsGrad(pos);
       vec3 norm = length(g) < 0.001 ? OIO : normalize(g);
       vec3 up = abs(norm.z) < 0.999 ? OOI : IOO;
       vec3 bitangent = CROSS(up, norm);
       vec3 tangent = CROSS(norm, bitangent);
       vel = d < ballRadius ?
           reflection * norm - friction * tangent + randomize * rand :
           vel + (-OIO * gravity - norm * attraction) * deltaTime;

       pos += vel * deltaTime;
       if(elapsedTime < phasePeriod[11]||
           pos.x < ballPosMin.x || pos.x > ballPosMax.x ||
           pos.y < ballPosMin.y || pos.y > ballPosMax.y ||
           pos.z < ballPosMin.z || pos.z > ballPosMax.z){
           pos = mix(ballPosMin, ballPosMax, rand * 0.2 + vec3(0.4, 0.6, 0.5));
           vel = OOO;
       }

       vel = SATURATE((vel - ballVelMin) / (ballVelMax - ballVelMin));
       pos = SATURATE((pos - ballPosMin) / (ballPosMax - ballPosMin));

       if(fragCoord.x == i)
       {
                if(fragCoord.y == 0) { col = vec4(pos, 0.0); }
           else if(fragCoord.y == 1) { col = vec4(vel, 0.0); }
       }
   }
}

Surface Map(vec3 p)
{
   float sdStairs = STAIRS_MAP(p);
   Surface s = Surface(0, 0, sdStairs);

   vec3 ballPosCenter = (ballPosMax + ballPosMin) * 0.5;
   vec3 ballArea = ballPosMax - ballPosMin;

   s = minSurface(s, WallMap(p));

   vec3 p1 = p - vec3(0.0, 19.0, 75.0);
   p1.x -= 4.0;
   p1.xz = rotate2d(mix(0.0, PI * 0.75, SATURATE((elapsedTime - phasePeriod[11] - 3.0) * 0.15))) * p1.xz;
   p1.x += 4.0;
   Surface door = Surface(3, 0, sdBox(p1, vec3(4.0, 9.0, wallWidth * 0.5)));
   s = minSurface(s, door);

   Surface doorWall = Surface(4, 0, max(
       sdBox(p - vec3(0.0, 40.0, stairsTilingSize * 0.5), vec3(stairsWidth * 1.2, 30.0, wallWidth)),
       -sdBox(p - vec3(0.0, 19.0, 75.0), vec3(4.0, 9.0, wallWidth * 1.1)))
   );
   s = minSurface(s, doorWall);

   Surface monitor = Surface(1, 0, sdBox(p - vec3(0.0, 32.0, 73.0), vec3(5.333, 3.0, wallWidth * 0.5)));
   s = minSurface(s, monitor);

   Surface room = Surface(5, 0, max(
       sdBox(p - ballPosCenter, ballArea * 0.5),
       -sdBox(p - ballPosCenter + vec3(0.0, 0.0, 20.0), ballArea * 0.5 - vec3(5.0, 5.0, 5.0)))
   );
   s = minSurface(s, room);

   for(int i = 0; i < ballNum; i++){
       float sd = sdSphere(p - ballPos[i], ballRadius);
       Surface ball = Surface(6, i, sd);
       s = minSurface(s, ball);
   }

   return s;
}

#define LIMIT_MARCHING_DISTANCE(D,RD,RP) mix(D, min(MIN3(((1.0 * step(0.0, RD) - MOD(RP, 1.0)) / RD)) + EPS, D), \
step(RP.y, stairsTilingSize * 0.5) * step(-stairsTilingSize * 0.5, RP.y) * step(RP.z, stairsTilingSize * 0.5) * step(-stairsTilingSize * 0.5, RP.z) * \
(step(RP.x, -stairsWidth - wallWidth + wallWidth * 8.0) * step(-stairsWidth - wallWidth - wallWidth * 8.0, RP.x) + step(RP.x, stairsWidth + wallWidth + wallWidth * 8.0) * step(stairsWidth + wallWidth - wallWidth * 8.0, RP.x))\
)
#define MAP(P) Map(P)

bool RayMarching(vec3 ro, vec3 rd, int stepCount, float maxDistance, out vec3 rp, out Surface s)
{
   rp = ro;
   float rl = 0.0;
   for (int i = 0; i < stepCount; i++)
   {
       s = MAP(rp);
       float d = s.distance;
       if (abs(d) < EPS){ return true; }
       if (rl > maxDistance){ break; }
       d = LIMIT_MARCHING_DISTANCE(d, rd, rp);
       rl += d;
       rp = ro + rd * rl;
       if(elapsedTime < -1.0) break;
   }
   return false;
}

vec3 GetGrad(vec3 p)
{
   const float e = EPS;
   const vec2 k = vec2(1, -1);
   return k.xyy * MAP(p + k.xyy * e).distance +
          k.yyx * MAP(p + k.yyx * e).distance +
          k.yxy * MAP(p + k.yxy * e).distance +
          k.xxx * MAP(p + k.xxx * e).distance;
}

vec3 GetNormal(vec3 p)
{
   return normalize(GetGrad(p));
}

Material GetMaterial(Surface s, vec3 p)
{
   Material m = Material(III, 0.0, OOO);

   vec3 defaultColor = mix(
       mix(vec3(0.7, 0.8, 1.0), vec3(0.6, 0.05, 0.1), SATURATE(elapsedTime - phasePeriod[9] - 3.0)),
       vec3(0.7, 0.8, 1.0), SATURATE(elapsedTime - phasePeriod[10] - 7.0));

   if(s.surfaceId == 0) //Stairs
   {
       ivec3 seed = ivec3(p.x, p.y, p.z);
       vec3 hash = Pcg01(seed);

       m.baseColor = defaultColor;
       m.roughness = mix(0.01, 0.25, hash.x);
       m.emission = OOO;
       return m;
   }
   else if(s.surfaceId == 1) //Monitor
   {
       float mask = step(sin(p.x * 2.0 + sbeatTime * 3.0 - p.y * 2.0), 0.0);

       int seed = int((p.x * 2.0 + sbeatTime * 3.0 - p.y * 2.0) / PI);
       float hash = Pcg01(seed);

       vec3 color =
       mix(vec3(1.0, 0.7, 0.8),
       mix(vec3(0.7, 0.8, 1.0),
       vec3(0.7, 1.0, 0.8),
       step(hash, 0.33)), step(hash, 0.66));

       color = mix(defaultColor, color, SATURATE(elapsedTime - phasePeriod[10] - 7.0));

       m.baseColor = OOO;
       m.roughness = 1.0;
       m.emission = mask * color;
       return m;
   }
   else if(s.surfaceId == 2) //Wall
   {
       float mask = s.objectId == 0 ? SATURATE(p.x + stairsWidth) : SATURATE(- p.x + stairsWidth);
       mask *= 0.6;

       ivec2 seed = ivec2(p.y, p.z);
       vec2 hash = Pcg01(seed);

       vec3 color =
       mix(vec3(1.0, 0.7, 0.8),
       mix(vec3(0.7, 0.8, 1.0),
       vec3(0.7, 1.0, 0.8),
       step(hash.x, 0.33)), step(hash.x, 0.66));

       color = mix(defaultColor, color, SATURATE(elapsedTime - phasePeriod[10] - 7.0));

       m.baseColor = color;
       m.roughness = 0.4;
       m.emission = mix(vec3(0.0, 0.0, 0.0), color, mask);
       return m;
   }
   else if(s.surfaceId == 3) //door
   {
       m.baseColor = III;
       m.roughness = 0.01;
       m.emission = OOO;
       return m;
   }
   else if(s.surfaceId == 4) //door wall
   {
       ivec2 seed = ivec2(p.x, p.y);
       vec2 hash = Pcg01(seed);
       m.baseColor = III;
       m.roughness = mix(0.02, 0.98, hash.x * hash.y);
       m.emission = OOO;
       return m;
   }
   else if(s.surfaceId == 5) //room
   {
       ivec3 seed = ivec3(vec3(p.x, p.y, p.z) * 0.1);
       seed.x += int(sbeatTime * 30.0);
       seed.y += int(sbeatTime * 30.0);
       seed.z += int(sbeatTime * 30.0);
       vec3 hash = Pcg01(seed);

       float mask = step(sin(p.x * 0.1 + elapsedTime * 3.0 - p.y * 0.1), 0.0);

       m.baseColor = III;
       m.roughness = mix(0.03, 0.98, hash.x);
       m.emission = mix(0.1, 0.5, mask) * defaultColor;
       return m;
   }
   else if(s.surfaceId == 6) //ball
   {
       vec3 hash = Pcg01(ivec3(s.objectId, 0, 0));
       m.baseColor = hash;
       m.roughness = 0.03;
       m.emission = hash * 0.5;
       return m;
   }

   return m;
}

vec3 SampleRadiance(vec3 ro0, vec3 rd0, vec3 color)
{
   vec3 sum = OOO;
   Surface s;
   bool hit;
   Material m;
   vec3 n;
   vec2 rand;
   vec3 brdfOverPdf;
   vec3 v;
   vec3 hitPos;
   for(int iter = 0; iter < iterMax; iter++)
   {
       vec3 ro = ro0;
       vec3 rd = rd0;
       vec3 acc = OOO;
       vec3 weight = III;
       for (int bounce = 0; bounce <= bounceLimit; bounce++)
       {
           hit = RayMarching(ro, rd, marchingStep, maxDistance, hitPos, s);
           if(!hit) {
               acc += color * weight;
               break;
           }
           m = GetMaterial(s, hitPos);
           n = GetNormal(hitPos);
           rand = Pcg01(vec4(hitPos, float(iter * bounceLimit + bounce) + elapsedTime)).xy;
           ro = hitPos + n * EPS * 2.0;

           if(m.roughness > 0.99)
           {
               rd = ImportanceSampleLambert(rand, n);
               brdfOverPdf = Lambert(m.baseColor, n, rd);
           }
           else
           {
               v = -rd;
               rd = ImportanceSampleGGX(rand, m.roughness, n, rd);
               if (dot(rd, n) < 0.0 ) { break; }
               brdfOverPdf = GGX(n, v, rd, m.roughness, m.baseColor);
           }
           acc += m.emission * weight;
           weight *= brdfOverPdf * max(dot(rd, n), 0.0);
           if (dot(weight, weight) < EPS) { break; }
       }
       sum += acc;
   }
   return sum / float(iterMax);
}

out vec4 outColor;
void main()
{
   vec2 r = resolution.xy;
   elapsedTime = mod(time, float(phaseBPM[15]) / bpm * 60.0);
   CalcBeatTime();
   CalcPhase();
   CalcCameraParams();
   LoadBallParams(r);
   vec4 col = vec4(0.0, 0.0, 0.0, 1.0);
   vec3 p = vec3((gl_FragCoord.xy * 2.0 - r) / min(r.x, r.y), 0.0);
   cameraDir = normalize(cameraDir);
   cameraUp = normalize(cameraUp);
   vec3 cameraRight = CROSS(cameraUp, cameraDir);
   cameraUp = CROSS(cameraDir, cameraRight);
   vec3 ray = normalize(cameraRight * p.x + cameraUp * p.y + cameraDir * dof);
   vec3 ro = cameraPos;
   vec3 rd = ray;
   col.xyz = SampleRadiance(ro, rd, col.xyz);
   SaveBallParams(ivec2(gl_FragCoord.xy), col);
   outColor = col;
}