/*
"Christmas 2016 orbs" by Emmanuel Keller aka Tambako - December 2016
License Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
Contact: tamby@tambako.ch
*/

#define pi 3.14159265359

// Switches, you can play with them!
#define show_gold
#define gold_bump
#define specular
#define star_orbs
#define orbs_rings
#define reflections
#define ss_scatering
#define show_snow

//#define antialias

struct Lamp
{
  	vec3 position;
  	vec3 color;
  	float intensity;
  	float attenuation;
};

struct RenderData
{
  	vec3 col;
  	vec3 pos;
  	vec3 norm;
  	int objnr;
};

// Every object of the scene has its ID
#define SKY_OBJ      0
#define BALL_OBJ     1

Lamp lamps[3];

// Campera parameters
vec3 campos = vec3(0., -0., 10.);
vec3 camtarget = vec3(0., 0., 0.);
vec3 camdir = vec3(0., 0., 0.);
float fov = 5.0;

// Ambient light parameters
const vec3 ambientColor = vec3(0.5);
const float ambientint = 0.05;

// Shading parameters
const float ssstrmr = 0.85;
const float sssInt = 0.39;
const vec3 sssColor = vec3(0.85, 0.92, 1.);
const float specint = 0.4;
const float specshin = 60.;

// Gold parameters
const vec3 goldColor = vec3(1.1, 0.91, 0.52);
const vec3 goldColor2 = vec3(1.1, 0.97, 0.68);
const vec3 goldColor3 = vec3(1.06, 0.86, 0.55);
const float goldRef = 0.99;
const float goldTreshold = 0.8;
const float goldFrequency = 40.;
const float goldBump = 0.013;

// Ball parameters
const vec3 ballColor = vec3(1.0, 0.97, 0.94);
const float ballRadius = 1.12;
const float ballIOR = 2.2;

// Snow parameters
const vec3 snowColor = vec3(0.9, 0.95, 1.0);
const float snowHeight = 0.44;
const float snowHeightDist = 0.12;
const float snowPosVar = 0.28;
const float snowPosVarFreq = 2.5;
const float snowPosPow = 0.27;
const float snowThicknessFactor = 0.12;
const float snowBumpBig = 0.36;
const float snowBumpBigFreq = 3.2;
const float snowBumpSmall = 0.011;
const float snowBumpSmallFreq = 185.;

// Tracing parameters
const float normdelta = 0.001;
const float maxdist = 40.;

const float stLampsInt = 0.55;
// Rotating lamps parameters
const int nbLamps = 16;
const float lampsGenInt = 0.5;
const float lampsAttenuation = 0.6;
const float lampsMinRadius = 1.3;
const float lampsMaxRadius = 1.7;
const float lampsMinSpeed = 1.0;
const float lampsMaxSpeed = 4.7;
const float lampsMinIntensity = 0.7;
const float lampsMaxIntensity = 1.0;
const float lampsMinIntensityVariation = 0.25;
const float lampsMaxIntensityVariation = 0.65;
const float lampsMinIntensityFrequency = 8.;
const float lampsMaxIntensityFrequency = 42.;

// Some parameters of the star of orbs
const float starRad = 30000.;
const float starRingRad = 0.028;
const float starNbBranches = 6.;
const float starPow = 3.;
const float starStrength = 0.5;

// Antialias. Change from 1 to 2 or more AT YOUR OWN RISK! It may CRASH your browser while compiling!
const float aawidth = 0.8;
const int aasamples = 2;

vec3 speccol = vec3(0.);

void init()
{
    lamps[0] = Lamp(vec3(-500., 300., -500.), vec3(0.81, 0.89, 1.), 2.9, 0.001);
    lamps[1] = Lamp(vec3(150., 400., 200.), vec3(0.74, 0.82, 1.), 2.2, 0.001);
    lamps[2] = Lamp(vec3(250., -400., 100.), vec3(0.6, 0.73, 1.), 1., 0.001);
}

vec2 rotateVec(vec2 vect, float angle)
{
    vec2 rv;
    rv.x = vect.x*cos(angle) - vect.y*sin(angle);
    rv.y = vect.x*sin(angle) + vect.y*cos(angle);
    return rv;
}

vec3 hsv2rgb(in vec3 c)
{
    vec3 rgb = clamp(abs(mod(c.x*6.0+vec3(0.0,4.0,2.0),6.0)-3.0)-1.0, 0.0, 1.0);
	rgb = rgb*rgb*(3.0-2.0*rgb); // cubic smoothing

	return c.z * mix(vec3(1.0), rgb, c.y);
}

// 1D hash function
float hash(float n)
{
    return fract(sin(n)*753.5453123);
}

// From https://www.shadertoy.com/view/4sfGzS
float noise(vec3 x)
{
    //x.x = mod(x.x, 0.4);
    vec3 p = floor(x);
    vec3 f = fract(x);
    f = f*f*(3.0-2.0*f);

    float n = p.x + p.y*157.0 + 113.0*p.z;
    return mix(mix(mix(hash(n+  0.0), hash(n+  1.0),f.x),
                   mix(hash(n+157.0), hash(n+158.0),f.x),f.y),
               mix(mix(hash(n+113.0), hash(n+114.0),f.x),
                   mix(hash(n+270.0), hash(n+271.0),f.x),f.y),f.z);
}

float noisePattern(vec3 pos)
{
    return noise(normalize(pos)*2.5);
}

vec3 snowPos(vec3 pos)
{
    return pos + snowPosVar*noise(pos*snowPosVarFreq);
}

float snowValue(vec3 pos)
{
    #ifdef show_snow
    return smoothstep(snowHeight - 0.004, snowHeight, snowPos(pos).y);
    #else
    return 0.;
    #endif
}

float snowThickness(vec3 pos)
{
    #ifdef show_snow
    vec3 pos2 = pos + 0.5*noise(pos*snowBumpSmallFreq*1.743);
    float st = pow(smoothstep(snowHeight, snowHeight + snowHeightDist, snowPos(pos).y), snowPosPow);
    return st*(1. + snowBumpBig*noise(pos*snowBumpBigFreq))*(1. + snowBumpSmall*noise(pos2*snowBumpSmallFreq));
    //return st*(1. + snowBumpBig*noise(pos*snowBumpBigFreq));
    #else
    return 0.;
    #endif
}

float goldValue(vec3 pos)
{
    #ifdef show_gold
    vec3 pos2 = pos + 0.1*noise(pos*12.);
    return (1. - snowValue(pos))*smoothstep(goldTreshold, goldTreshold + 0.01, sin(noisePattern(pos2)*goldFrequency));
    #else
    return 0.;
    #endif
}

float map_ball(vec3 pos)
{
    float snow = snowThicknessFactor*snowThickness(pos);
    float df = length(pos) - ballRadius - snow;

    #ifdef show_gold
    #ifdef gold_bump
    vec3 pos2 = pos + 0.1*noise(pos*12.);
    df+= goldBump*(1. - snowValue(pos))*smoothstep(goldTreshold + 0.15, goldTreshold + 0.01, sin(noisePattern(pos2)*goldFrequency));
    #endif
    #endif

    return df;
}

vec2 map(vec3 pos)
{
    float ball = map_ball(pos);
    vec2 res = vec2(ball, BALL_OBJ);

    return res;
}

// Main tracing function
float lastDist;
vec2 trace(vec3 cam, vec3 ray, float maxdist)
{
    float t = 0.1;
    float objnr = 0.;
    vec3 pos;
    float dist;

  	for (int i = 0; i < 128; ++i)
    {
    	pos = ray*t + cam;
        vec2 res = map(pos);
        dist = res.x;
        if (dist>maxdist || abs(dist)<0.005)
            break;
        t+= dist*0.5;
        objnr = abs(res.y);
  	}
    lastDist = t;
  	return vec2(t, objnr);
}

vec3 getNormal(vec3 pos, float e)
{
    vec3 n = vec3(0.0);
    for( int i=0; i<4; i++ )
    {
        vec3 e2 = 0.5773*(2.0*vec3((((i+3)>>1)&1),((i>>1)&1),(i&1))-1.0);
        n += e2*map(pos + e*e2).x;
    }
    return normalize(n);
}

vec3 colorRamp3(vec3 col1, vec3 col2, vec3 col3, float v)
{
   return mix(mix(col1, col2, smoothstep(0.0, 0.5, v)), col3, smoothstep(0.5, 1.0, v));
}

// Gets the color of the sky
vec3 sky_color(vec3 ray)
{
    //float r = texture(iChannel0, ray).r;
    ray+= 0.35*noise(ray*6.2);
    ray+= 0.05*noise(ray*21.5);
    ray+= 0.02*noise(ray*72.5);
    float r = 2.*ray.y;
    vec3 col1 = vec3(0.0, 0.15, 0.35);
    vec3 col2 = vec3(0.2, 0.3, 0.5);
    vec3 col3 = vec3(0.5, 0.6, 0.7);
    return colorRamp3(col1, col2, col3, r);
}

vec3 getGoldColor(vec3 pos)
{
    pos+= 0.4*noise(pos*24.);
    float t = noise(pos*30.);
    vec3 col = mix(goldColor, goldColor2, smoothstep(0.55, 0.95, t));
    col = mix(col, goldColor3, smoothstep(0.45, 0.25, t));
    return col;
}

vec3 getBallColor(vec3 pos)
{
	vec3 col1 = mix(ballColor, getGoldColor(pos), pow(goldValue(pos), 4.));
    return mix(col1, snowColor, snowValue(pos));
}

// Combines the colors
vec3 getColor(vec3 norm, vec3 pos, int objnr, vec3 ray)
{
   return objnr==BALL_OBJ?getBallColor(pos):sky_color(ray);
}

// Fresnel reflectance factor through Schlick's approximation: https://en.wikipedia.org/wiki/Schlick's_approximation
float fresnel(vec3 ray, vec3 norm, float n2)
{
   float n1 = 1.; // air
   float angle = acos(-dot(ray, norm));
   float r0 = dot((n1-n2)/(n1+n2), (n1-n2)/(n1+n2));
   float r = r0 + (1. - r0)*pow(1. - cos(angle), 5.);
   return clamp(r, 0., 0.8);
}

// Shading of the objects pro lamp
vec3 lampShading(Lamp lamp, vec3 norm, vec3 pos, vec3 ocol, int objnr, int lampnr)
{
    vec3 pl = normalize(lamp.position - pos);
    float dlp = distance(lamp.position, pos);
    vec3 pli = pl/pow(1. + lamp.attenuation*dlp, 2.);
    float dnp = dot(norm, pli);

    lamp.intensity*=stLampsInt;

    // Diffuse shading
    vec3 col;
    col = ocol*lamp.color*lamp.intensity*smoothstep(-0.1, 1., dnp); //clamp(dnp, 0., 1.);

    // Specular shading
    #ifdef specular

    float specint2 = specint*(1. - snowValue(pos));
    //if (dot(norm, lamp.position - pos) > 0.0)
        speccol+= lamp.color*lamp.intensity*specint2*pow(max(0.0, dot(reflect(pl, norm), normalize(pos - campos))), specshin);
    #endif

 	// Sub surface scattering from https://www.shadertoy.com/view/MdXSzX
    #ifdef ss_scatering
    float transmission = map(pos + pl*ssstrmr).x/ssstrmr;
	vec3 sssLight = sssColor*lamp.color*smoothstep(0.0,1.0, transmission);
    float sssInt2 = sssInt*snowValue(pos);
    col = col*(1. - sssInt2) + sssInt2*sssLight;
    #endif

    return col;
}

// Shading of the objects over all static lamps
vec3 lampsShading(vec3 norm, vec3 pos, vec3 ocol, int objnr)
{
    vec3 col = vec3(0.);
    for (int l=0; l<3; l++) // lamps.length()
        col+= lampShading(lamps[l], norm, pos, ocol, objnr, l);

    return col;
}

float getLampIntensity(int lampNr, float t)
{
    float li = lampsMinIntensity + (lampsMaxIntensity - lampsMinIntensity)*hash(float(lampNr*156 + 541));
    float liv = lampsMinIntensityVariation + (lampsMaxIntensityVariation - lampsMinIntensityVariation)*hash(float(lampNr*674 + 247));
    float lif = lampsMinIntensityFrequency + (lampsMaxIntensityFrequency - lampsMinIntensityFrequency)*hash(float(lampNr*842 + 617));
    return lampsGenInt*(li + liv*(sin(t*lif)*0.5 + 0.5));
}

vec3 getLampColor(int lampNr)
{
    float hue = hash(float(lampNr*621 + 127));
    vec3 hsv = vec3(hue, 1.0, 1.0);
    return hsv2rgb(hsv);
}

vec3 getLampPosition(int lampNr, float t)
{
    t+= 15.*hash(float(lampNr*541 + 315));
    float r = lampsMinRadius + (lampsMaxRadius - lampsMinRadius)*hash(float(lampNr*348 + 173));
    float s = lampsMinSpeed + (lampsMaxSpeed - lampsMinSpeed)*hash(float(lampNr*447 + 914));
    float a1 = 2.*pi*hash(float(lampNr*519 + 327));
    float a2 = 2.*pi*hash(float(lampNr*864 + 616));
    vec3 pos = vec3(0, rotateVec(vec2(r, 0), s*t));
    pos.xz = rotateVec(pos.xz, a1);
    pos.xy = rotateVec(pos.xy, a2);
    return pos;
}

// Shading of the objects pro lamp
vec3 lampShading2(float lintensity, vec3 lcolor, vec3 lpos, vec3 norm, vec3 pos, vec3 ocol)
{
    vec3 pl = normalize(lpos - pos);
    float dlp = distance(lpos, pos);
    vec3 pli = pl/pow(1. + lampsAttenuation*dlp, 2.);
    float dnp = dot(norm, pli);

    // Diffuse shading
    vec3 col;
    col = ocol*lcolor*lintensity*smoothstep(-0.1, 1., dnp); //clamp(dnp, 0., 1.);

    // Specular shading
    #ifdef specular
    float specint2 = 1.7*specint*(1. - snowValue(pos));
    speccol+= lcolor*lintensity*specint2*pow(max(0.0, dot(reflect(pl, norm), normalize(pos - campos))), specshin);
    #endif

    return col;
}

// Shading of the objects over all static lamps
vec3 lampsShading2(vec3 norm, vec3 pos, vec3 ocol)
{
    vec3 col = vec3(0.);
    float lintensity;
    vec3 lcolor;
    vec3 lpos;

    for (int l=0; l<nbLamps; l++)
    {
        lintensity = getLampIntensity(l, iTime);
        lcolor = getLampColor(l);
        lpos = getLampPosition(l, iTime);
        col+= lampShading2(lintensity, lcolor, lpos, norm, pos, ocol);
    }
    return col;
}

// From https://www.shadertoy.com/view/lsSXzD, modified
vec3 GetCameraRayDir(vec2 vWindow, vec3 vCameraDir, float fov)
{
	vec3 vForward = normalize(vCameraDir);
	vec3 vRight = normalize(cross(vec3(0.0, 1.0, 0.0), vForward));
	vec3 vUp = normalize(cross(vForward, vRight));

	vec3 vDir = normalize(vWindow.x * vRight + vWindow.y * vUp + vForward * fov);

	return vDir;
}

vec3 getFlares(vec3 ray)
{
	vec3 rc = vec3(0.);
    float lintensity;
    vec3 lcolor;
    vec3 lpos;

    for (int l=0; l<nbLamps; l++)
    {
        lintensity = getLampIntensity(l, iTime);
        lcolor = getLampColor(l);
        lpos = getLampPosition(l, iTime);

        if (distance(campos, lpos)<lastDist)
        {
            float lp = pow(max(0.0, dot(ray, normalize(lpos - campos))), 20000.);
            #ifdef orbs_rings
            float lp2 = 0.08*smoothstep(starRingRad*0.85, starRingRad, lp)*smoothstep(starRingRad*1.8, starRingRad, lp);
            #endif

            #ifdef star_orbs
            // Complicated stuff to calculate the projection of the lamp position on the image plane
            // From: http://stackoverflow.com/questions/23472048/projecting-3d-points-to-2d-plane
            vec3 v3 = (lpos - campos) - dot((lpos - campos), ray) * ray;
            vec3 vx = vec3(0., 1., 0.);
            vx = normalize(vx - dot(vx, ray)*ray);
            //vec3 vy = cross(ray, vx);
            vec3 vy = vec3(ray.y*vx.z - ray.z*vx.y, ray.z*vx.x - ray.x*vx.z, ray.x*vx.y - ray.y*vx.x);
            float a = atan(dot(v3, vx)/dot(v3, vy));
            // Gives the angle a star look
            float spp = starRad*(1. + starStrength*pow(sin(a*starNbBranches), starPow));
            lp = pow(max(0.0, dot(ray, normalize(lpos - campos))), spp);
            #endif

            #ifdef orbs_rings
            lp+= lp2;
            #endif

            rc+= 5.5*clamp(normalize(mix(lcolor, vec3(1.), 0.55*pow(lp, 1.5)))*lintensity*specint*lp, 0., 1.);
        }
    }
    return rc;
}

// Sets the position of the camera with the mouse and calculates its direction
const float axm = 4.;
const float aym = 1.5;
void setCamera()
{
   vec2 iMouse2;
   if (iMouse.x==0. && iMouse.y==0.)
      iMouse2 = vec2(0.5, 0.5);
   else
      iMouse2 = iMouse.xy/iResolution.xy;

   campos = vec3(8.5, 0., 0.);
   campos.xy = rotateVec(campos.xy, -iMouse2.y*aym + aym*0.5);
   campos.yz = rotateVec(campos.yz, -iMouse2.y*aym + aym*0.5);
   campos.xz = rotateVec(campos.xz, -iMouse2.x*axm);

   camtarget = vec3(0.);
   camdir = camtarget - campos;
}

// Tracing and rendering a ray
RenderData trace0(vec3 tpos, vec3 ray, float maxdist)
{
    vec2 tr = trace(tpos, ray, maxdist);
    float tx = tr.x;
    int objnr = int(tr.y);
    vec3 col;
    vec3 pos = tpos + tx*ray;
    vec3 norm;

    if (tx<maxdist*0.95)
    {
        norm = getNormal(pos, normdelta);
        col = getColor(norm, pos, objnr, ray);

        // Shading
        col = ambientColor*ambientint + lampsShading(norm, pos, col, objnr) + lampsShading2(norm, pos, col);
    }
    else
    {
        objnr = SKY_OBJ;
        col = vec3(0.);
    }
    return RenderData(col, pos, norm, objnr);
}

// Main render function with reflections and refractions
vec4 render(vec2 fragCoord)
{
  	vec2 uv = fragCoord.xy / iResolution.xy;
  	uv = uv*2.0 - 1.0;
  	uv.x*= iResolution.x / iResolution.y;

    speccol = vec3(0.);

  	vec3 ray = GetCameraRayDir(uv, camdir, fov);
  	RenderData traceinf = trace0(campos, ray, maxdist);
  	//vec3 col = screen(traceinf.col, getFlares(ray);
    vec3 col = traceinf.col;
    vec3 refray;

    if (traceinf.objnr==BALL_OBJ)
    {
        float gv = goldValue(traceinf.pos);

        #ifdef reflections
        refray = reflect(ray, traceinf.norm);
        float rf = fresnel(ray, traceinf.norm, ballIOR)*(1. - snowValue(traceinf.pos));
        vec3 colBa = mix(col, sky_color(refray), rf);
        vec3 goldColor = getGoldColor(traceinf.pos);
        vec3 colGo = mix(col, goldColor*sky_color(refray), goldRef);
        col = mix(colBa, colGo, gv);
        col+= speccol*mix(vec3(1.), goldColor*goldColor, gv);
        #endif
    }
    if (traceinf.objnr==SKY_OBJ)
    col+= sky_color(ray);
    col+= getFlares(ray);

  	return vec4(col, 1.0);
}

void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
    init();
    setCamera();

    // Antialiasing.
    #ifdef antialias
    vec4 vs = vec4(0.);
    for (int j=0;j<aasamples ;j++)
    {
       float oy = float(j)*aawidth/max(float(aasamples-1), 1.);
       for (int i=0;i<aasamples ;i++)
       {
          float ox = float(i)*aawidth/max(float(aasamples-1), 1.);
          vs+= render(fragCoord + vec2(ox, oy));
       }
    }
    vec2 uv = fragCoord.xy / iResolution.xy;
    fragColor = vs/vec4(aasamples*aasamples);
    #else
    fragColor = render(fragCoord);
    #endif
}
