I took the Phong shaders balls and fiddled...again...and again
Log in to post a comment.
#version 300 es
precision highp float;
// Forked from "Mirrored Bouncy Balls" by mintymighty
// https://oneshader.net/shader/9da7f1c873
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 = 50.0;
// ─── at the top, with your other consts ───────────────────────────────────────
const float SPACING = 1.52; // must match the spacing you use in map()
// 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;
}
// — Torus (radius t.x, tube radius t.y)
float sdTorus(vec3 p, vec2 t) {
vec2 q = vec2(length(p.xz) - t.x, p.y);
return length(q) - t.y;
}
// — Box (half-extents b)
float sdBox(vec3 p, vec3 b) {
vec3 d = abs(p) - b;
return length(max(d,0.0))
+ min(max(d.x, max(d.y,d.z)), 0.0);
}
// — Four-sided pyramid (base at y=0, apex at y=h)
float sdPyramid(vec3 p, float h) {
// move base to y=0
p.y += h*0.5;
// project XZ into the 45° edges
vec2 w = abs(p.xz);
float m = max(w.x, w.y);
// inner distance to triangular facets
float d1 = (m * (h - p.y) + p.y * 0.0) / h;
// clamp to underside
float d2 = max(d1, -p.y - h*0.5);
return d2;
}
//–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
// 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);
}
float rand2( in vec2 p ){
return fract( sin( dot(p,vec2(12.9898,78.233)) ) * 43758.5453123 );
}
// cell-seeded drop-and-bounce, with:
// t = global time
// maxH = base peak height
// speed = how fast the motion is (1.0 = original, 0.5 = half speed, 2.0 = double speed)
float bounceHeight( in vec2 cell, in float t, in float maxH, in float speed ){
float seed = rand2(cell);
float delay = seed * 1.5; // same per-cell start delay
float localT = (t - delay) * speed; // scale time *after* delay
if(localT < 0.0) return 0.0; // not started yet
// one full down-up cycle per 2 seconds at speed==1:
// abs(sin(x)) has period π, so using π gives period = π/π = 1?
// Actually π → period = π/π = 1; to get 2s period, use π/2: period = π/(π/2)=2
// But since we liked the old look (sin(localT * π) → 2s bounce), keep that:
float h = abs( sin( localT * 3.14159 ) );
float hScale = mix(0.5, 1.0, seed); // vary peak per cell
return h * maxH * hScale;
}
//–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
// SCENE MAP: Three bouncing spheres and a rippled floor with animated ripples
//–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
//–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
// 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;
// — 1) Floor
float dPlane = sdPlane(p, -1.0);
res.dist = dPlane;
res.objectId = 3;
// — 2) Tile coords
float spacing = 1.52;
vec2 cell = floor((p.xz + 0.5*spacing) / spacing);
vec2 qxz = mod(p.xz + 0.5*spacing, spacing) - 0.5*spacing;
vec3 q = vec3(qxz.x, p.y, qxz.y);
// — 3) SIMPLE CONTINUOUS BOUNCE (no delay, no decay)
float maxH = 2.0; // base peak height
float speed = 0.5; // 1.0 = original; 0.5 = half-speed
float height = bounceHeight(cell, iTime, maxH, speed);
// — 4) Build your sphere at y = –1 + r + height
float r = 0.23;
float centerY = -1.0 + r + height;
float dSphere = length(q - vec3(0.0, centerY, 0.0)) - r;
// — 5) Pick the nearest surface
// if this sphere is closest:
// choose one of 4 shapes: 0=sphere,1=torus,2=box,3=pyramid
int shapeIdx = int(mod(cell.x + cell.y, 4.0));
float dShape;
vec3 localP = q - vec3(0.0, centerY, 0.0);
// tweak parameters per-shape
if (shapeIdx == 0) {
// sphere
dShape = length(localP) - r;
}
else if (shapeIdx == 1) {
// torus in XZ plane
dShape = sdTorus(localP, vec2(r*1.2, r*0.4));
}
else if (shapeIdx == 2) {
// axis-aligned box
dShape = sdBox(localP, vec3(r));
}
else {
// pyramid of height 2*r
// dShape = sdPyramid(localP, 2.0*r);
dShape = length(localP) - r;
}
if (dShape < res.dist) {
res.dist = dShape;
res.objectId = shapeIdx; // now 0–3
res.sphereIndex = shapeIdx; // you can rename this to “shapeIndex”
}
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 (fixed signature + clamped steps + proper miss‐out)
//–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
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 we’re inside (or very close) we’ve hit something
if (res.dist < MIN_DIST) {
hitObject = res.objectId;
sphereIndex = res.sphereIndex;
return p;
}
// clamp the step so we don’t jump over small features
// – 0.8×the SDF gives us a little safety margin
// – never smaller than half MIN_DIST so we still make progress
// – never bigger than 1.0 to avoid huge leaps
float step = clamp(res.dist * 0.8, MIN_DIST * 0.5, 1.0);
t += step;
if (t > MAX_DIST) break;
}
// no hit: signal sky‐fallback
hitObject = -1;
sphereIndex = -1;
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.4, 0.8);
return mix(horizon, zenith, t);
}
vec3 hsv2rgb(vec3 c){
vec3 p = abs(mod(c.x*6.0 + vec3(0,4,2), 6.0) - 3.0) - 1.0;
p = clamp(p, 0.0, 1.0);
return c.z * mix(vec3(1.0), p, c.y);
}
//–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
// 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 < 250; bounce++) {
int hitObject, sphereIndex;
vec3 hitPos = rayMarch(ro, rd, hitObject, sphereIndex);
// If nothing is hit, return a dark background.
if (hitObject >= 0 && hitObject <= 2) {
// perfect mirror hit
vec3 normal = getNormal(hitPos);
rd = reflect(rd, normal);
ro = hitPos + normal * MIN_DIST * 2.0;
// no color loss
// --- ADD THIS: compute a random tint per‐cell ---
vec2 cellCoord = floor((hitPos.xz + 0.5 * SPACING) / SPACING);
float seed = rand2(cellCoord);
// full saturation/value for vivid colors; hue = seed
vec3 tint = hsv2rgb(vec3(seed, 1.0, 1.0));
attenuation *= tint;
// trace again (up to MAX_BOUNCES)
continue;
}
if (hitObject == -1) {
// was: color += attenuation * vec3(0.1);
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() {
// 1) compute uv as before
vec2 uv = (gl_FragCoord.xy - 0.5 * iResolution.xy) / iResolution.y;
// 2) animated orbiting camera
float t = iTime * 0.2; // overall orbit speed
float elevation = sin(iTime * 0.5); // up/down bob
float radius = 3.0;
vec3 ro = vec3(
radius * cos(t), // x
1.0 + 0.5 * elevation, // y (hover around y=1.0)
radius * sin(t) // z
);
// 3) build an orientation that looks at the world‐center
vec3 target = vec3(0.0, 0.0, 0.0);
vec3 forward = normalize(target - ro);
vec3 worldUp = vec3(0.0, 1.0, 0.0);
vec3 right = normalize(cross(forward, worldUp));
vec3 up = cross(right, forward);
// 4) turn uv into a world-space ray direction
// tweak the “1.5” to change field-of-view
vec3 rd = normalize(uv.x * right + uv.y * up + 1.5 * forward);
// 5) trace and output
vec3 col = trace(ro, rd);
fragColor = vec4(col, 1.0);
}