0.00
60.0 fps

Mirrored Bouncy Balls

I took the Phong shaders balls and fiddled...again

Log in to post a comment.

#version 300 es
precision highp float;

uniform float iTime;
uniform vec2  iResolution;
out vec4 fragColor;

//–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
// PARAMETERS & CONSTANTS
//–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
const int   MAX_MARCHING_STEPS = 100;
const float MIN_DIST = 0.001;
const float MAX_DIST = 100.0;

// Object IDs: 0–2 for spheres, 3 for floor.
struct SceneResult {
  float dist;
  int objectId;    // 0,1,2 for spheres; 3 for floor.
  int sphereIndex; // Which sphere (if any) was hit.
};

//–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
// SDF FUNCTIONS
//–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––

// A deformed sphere SDF (to simulate squash/stretch when bouncing).
// 'deform' holds the horizontal (x,z) scale in .x and vertical in .y.
float sdSphereDeformed(vec3 p, float r, vec2 deform) {
  vec3 q = vec3(p.x / deform.x, p.y / deform.y, p.z / deform.x);
  return length(q) - r;
}

// SDF for a horizontal plane (the floor) at y = -1.0.
float sdPlane(vec3 p, float h) {
  return p.y - h;
}

//–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
// ANIMATED RIPPLE FUNCTION
//–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
// This function computes a ripple that emanates from 'center' and
// depends on 'lt', the time (in seconds) elapsed since impact.
float rippleFromImpact(vec2 pos, vec2 center, float lt) {
    float dist = length(pos - center);
    float rippleSpeed = 1.5;   // How fast the ripple front moves outward.
    float frequency   = 10.0;  // Frequency of the ripple oscillation.
    float damping     = 2.0;   // Controls how quickly the ripple decays.
    float wave = sin(frequency * (dist - rippleSpeed * lt));
    // Envelope: maximum at impact (lt=0) and decays over time.
    float envelope = exp(-damping * lt) * (1.0 - smoothstep(0.0, 0.2, lt));
    return 0.1 * wave * envelope / (1.0 + 10.0 * dist);
}

//–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
// SCENE MAP: Three bouncing spheres and a rippled floor with animated ripples
//–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
SceneResult map(in vec3 p) {
  SceneResult res;
  res.dist = MAX_DIST;
  res.objectId = -1;
  res.sphereIndex = -1;
  
  // Parameters for the spheres.
  float sphereRadius = 0.3;
  float orbitRadius  = 0.8;
  
  // Process each of the 3 spheres.
  for (int i = 0; i < 3; i++) {
    // Each sphere gets its own phase offset.
    float phase = 6.2831853 * float(i) / 3.0;
    float t_i   = iTime + phase;
    // Bounce using the absolute value makes the motion symmetric.
    float bounce = abs(sin(t_i));
    
    // Compute the sphere’s center. They orbit in the xz–plane and bounce in y.
    vec3 center;
    center.x = orbitRadius * cos(iTime + phase);
    center.z = orbitRadius * sin(iTime + phase);
    center.y = -1.0 + sphereRadius + bounce;
    
    // Simulate squash/stretch on impact.
    float impact   = 1.0 - smoothstep(0.0, 0.2, bounce);
    float deformX  = mix(1.0, 1.2, impact); // horizontal expansion
    float deformY  = mix(1.0, 0.8, impact); // vertical squash
    
    float d = sdSphereDeformed(p - center, sphereRadius, vec2(deformX, deformY));
    if (d < res.dist) {
      res.dist       = d;
      res.objectId   = i;  // sphere id (0,1,2)
      res.sphereIndex = i;
    }
  }
  
  // Floor: basic SDF for a plane at y = -1.0.
  float dPlane = sdPlane(p, -1.0);
  // Add animated ripples from each sphere’s impact.
  // We simulate a ripple for a short duration after each impact.
  float period = 3.14;   // Approximate bounce period (in seconds).
  float T_ripple = 0.5;  // Duration (in seconds) for which a ripple is visible.
  for (int i = 0; i < 3; i++) {
    float phase = 6.2831853 * float(i) / 3.0;
    float t_i   = iTime + phase;
    // Compute time since impact for this sphere.
    float lt = mod(t_i, period);
    if (lt < T_ripple) {
      vec2 sphereXZ;
      sphereXZ.x = orbitRadius * cos(iTime + phase);
      sphereXZ.y = orbitRadius * sin(iTime + phase);
      dPlane += rippleFromImpact(p.xz, sphereXZ, lt);
    }
  }
  
  // Return whichever is closer: a sphere or the floor.
  if (dPlane < res.dist) {
    res.dist       = dPlane;
    res.objectId   = 3; // floor id
    res.sphereIndex = -1;
  }
  
  return res;
}

//–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
// ESTIMATING NORMALS
//–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
vec3 getNormal(vec3 p) {
  float h = 0.0001;
  return normalize(vec3(
    map(p + vec3(h, 0, 0)).dist - map(p - vec3(h, 0, 0)).dist,
    map(p + vec3(0, h, 0)).dist - map(p - vec3(0, h, 0)).dist,
    map(p + vec3(0, 0, h)).dist - map(p - vec3(0, 0, h)).dist
  ));
}

//–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
// RAY MARCHING
//–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
vec3 rayMarch(vec3 ro, vec3 rd, out int hitObject, out int sphereIndex) {
  float t = 0.0;
  hitObject   = -1;
  sphereIndex = -1;
  for (int i = 0; i < MAX_MARCHING_STEPS; i++) {
    vec3 p = ro + rd * t;
    SceneResult res = map(p);
    if (res.dist < MIN_DIST) {
      hitObject   = res.objectId;
      sphereIndex = res.sphereIndex;
      return p;
    }
    if (t > MAX_DIST) break;
    t += res.dist;
  }
  return ro + rd * t;
}

//–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
// LIGHTING & FRESNEL
//–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
vec3 getLightDir(vec3 p) {
  // A static overhead light
  vec3 lightPos = vec3(0.0, 15.0, 0.0);
  return normalize(lightPos - p);
}

vec3 phongLighting(vec3 p, vec3 normal, vec3 viewDir, vec3 baseColor) {
  vec3 lightDir = getLightDir(p);
  float diff = max(dot(normal, lightDir), 0.0);
  vec3 reflDir = reflect(-lightDir, normal);
  float spec = pow(max(dot(viewDir, reflDir), 0.0), 32.0);
  return baseColor * diff + vec3(1.0) * spec;
}

float fresnel(vec3 I, vec3 N, float ior) {
  float cosi = clamp(dot(I, N), -1.0, 1.0);
  float etai = 1.0, etat = ior;
  if (cosi > 0.0) { float tmp = etai; etai = etat; etat = tmp; }
  float sint = etai / etat * sqrt(max(0.0, 1.0 - cosi * cosi));
  if (sint >= 1.0) {
    return 1.0;
  } else {
    float cost = sqrt(max(0.0, 1.0 - sint * sint));
    cosi = abs(cosi);
    float Rs = ((etat * cosi) - (etai * cost)) / ((etat * cosi) + (etai * cost));
    float Rp = ((etai * cosi) - (etat * cost)) / ((etai * cosi) + (etat * cost));
    return (Rs * Rs + Rp * Rp) / 2.0;
  }
}

// Simple gradient sky: blue at the top fading to white at horizon
vec3 skyColor(vec3 rd) {
  float t = clamp(rd.y * 0.5 + 0.5, 0.0, 1.0);
  vec3 horizon = vec3(0.8, 0.9, 1.0);
  vec3 zenith  = vec3(0.2, 0.5, 0.9);
  return mix(horizon, zenith, t);
}

//–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
// RAY-TRACING: REFLECTIONS & REFRACTIONS
//–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
vec3 trace(vec3 ro, vec3 rd) {
  vec3 color = vec3(0.0);
  vec3 attenuation = vec3(1.0);
  
  // Allow up to 2 bounces.
  for (int bounce = 0; bounce < 2; bounce++) {
    int hitObject, sphereIndex;
    vec3 hitPos = rayMarch(ro, rd, hitObject, sphereIndex);
    
    // If nothing is hit, return a dark background.
    if (hitObject == -1) {
      // Instead of a flat dark background, sample the sky
      color += attenuation * skyColor(rd);
      break;
    }
    
    vec3 normal  = getNormal(hitPos);
    vec3 viewDir = normalize(-rd);
    
    if (hitObject == 3) {
      // Hit the floor: apply a chequerboard pattern.
      vec2 uvFloor = hitPos.xz * 2.0;
      float checker = mod(floor(uvFloor.x) + floor(uvFloor.y), 2.0);
      vec3 colorA = vec3(0.1, 0.1, 0.1);
      vec3 colorB = vec3(0.8, 0.8, 0.8);
      vec3 base = mix(colorA, colorB, checker);
      
      //vec3 lit  = phongLighting(hitPos, normal, viewDir, base);
      vec3 ambient = 0.2 * skyColor(normal); // light coming from the sky
      vec3 lit     = ambient + phongLighting(hitPos, normal, viewDir, base);
      color += attenuation * lit;
      rd = reflect(rd, normal);
      ro = hitPos + normal * MIN_DIST * 2.0;
      attenuation *= 0.8;
    } else {
      // MIRRORED SPHERE
      rd = reflect(rd, normal);
      ro = hitPos + normal * MIN_DIST * 2.0;
      // no color loss for a perfect mirror
      attenuation *= 1.0;
      // let it bounce again
      continue;
    }
  }
  
  return color;
}

//–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
// MAIN – Set up camera ray and output final colour.
//–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
void main() {
  // Normalized pixel coordinates (with y scaled by iResolution.y).
  vec2 uv = (gl_FragCoord.xy - 0.5 * iResolution.xy) / iResolution.y;
  
  // A simple camera: positioned at (0,0,3) looking toward -z.
  vec3 ro = vec3(0.0, 0.0, 3.0);
  vec3 rd = normalize(vec3(uv, -1.5));
  
  vec3 col = trace(ro, rd);
  
  fragColor = vec4(col, 1.0);
}