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); }