probe
A recursive ray tracer running inside Scratch — analytic sphere and plane intersection, Phong shading, hard shadows, mirror reflections and Schlick Fresnel — drawn progressively to the pen layer so you watch the image resolve. PROGRESSIVE RENDERING Each sample costs a primary ray, a shadow ray per hit and a reflection ray per bounce — measured over the default view, 2.8 rays per pixel — and there is no budget for doing that 172,800 times in one Scratch frame. So the image is four passes of halving block size, 24 → 12 → 6 → 3. Pass 1 is about 300 samples and lands immediately; each later pass quadruples the resolution but only samples the points the coarser pass did not, because the coarse grid is a strict subset of the fine one. The whole sequence costs about 4/3 of the final pass instead of five separate renders, which is why the picture sharpens in place. What makes it feel progressive is the split between goboscript's two kinds of procedure. render is a nowarp proc — a custom block with screen refresh — so its row loop yields one frame per row. Everything under it is a func, which always compiles to "run without screen refresh", so the ray casting for a whole row completes inside that one frame. Put the ray loop in the nowarp proc and every intersection test costs a frame; put the row loop in a func and the stage freezes until the image is finished. RECURSION, FLATTENED A Whitted tracer spawns exactly one reflection ray per hit, so the recursion is a tail call and folds into a loop carrying a throughput colour — the running product of every Fresnel term and metal tint on the path so far. That is not just tidier here, it is close to necessary: goboscript function locals are ordinary sprite variables, so a genuinely recursive call would overwrite its caller's state and each level would need an explicit stack in a list. Two more decisions that earn their keep: Shadow rays are any-hit, not closest-hit, and they skip the ground plane entirely. The light sits above every object in the scene, so the plane can never lie between a lit point and it — that is a scene invariant, not an approximation, and it makes a shadow ray about half the cost of a primary one. A one-node bounding volume. All six spheres fit in a sphere of radius 4.3; a ray that misses that can only see floor or sky. Measured at the default view, 19.5% of traversals take that path, skipping 383,000 sphere quadratics per frame. Both the numpy reference and the running project report the same figure — the HUD says 20%. VERIFICATION tools/mkbanner.py is a numpy port of the same scene, camera, Fresnel term, shadow test and tone map, and it is what generates the thumbnail — so the browse-page image is the renderer's real output, not a mock-up. It is also how the shading was checked: the project was run headless, its 9,720 finished pass-4 pen dots were pulled back out with their colours, and every one was compared against the reference. **Maximum error 1/255 on any channel, mean 0.12/255, zero samples off by more than 2/255** — the difference is entirely the last rounding step. HONEST LIMITATIONS - No anti-aliasing. One ray per sample, so sphere silhouettes are stepped at the block size. Supersampling would multiply an already slow render. - The tone map is a clamp, and gamma is sqrt (2.0) rather than the sRGB curve with its linear toe. All original - code, art and sound. See Inside is open.