Build your own ray tracer, and render a lit sphere from nothing
Ray tracing has a reputation as the heavyweight of computer graphics, the thing that renders photorealistic movie frames over hours on render farms and that finally arrived in real-time games when GPUs got dedicated hardware for it. That reputation makes people assume it is complicated. The truth is the opposite: the core idea is one of the simplest in all of graphics, simple enough that you can render a shaded, three-dimensional, correctly lit sphere in about 30 lines of plain code, no libraries, no GPU. Build it and the whole field opens up.
The one idea
To make an image, you simulate light in reverse. Real light leaves a source, bounces around, and some of it lands in your eye. Tracing every photon forward would be hopeless, almost none reach the eye. So you go backward: for each pixel on your screen, shoot a single ray from the eye through that pixel out into the scene, and ask, "what is the first thing this ray hits?" Whatever it hits determines the pixel's color. That is ray tracing. Everything else, shadows, reflections, glass, is just asking follow-up questions once you know where the ray landed. The foundational operation, done once per pixel, is a ray-versus-object intersection test.
Build the intersection
The math for a ray hitting a sphere is a quadratic equation. A ray is a starting point plus a direction; a sphere is a center and a radius. Substitute the ray into the sphere's equation and you get a quadratic in t, the distance along the ray. If it has real solutions, the ray hits; the smaller positive one is the near surface.
def hit_sphere(O, D, C, r): # ray origin O, unit direction D, sphere C/r
oc = sub(O, C)
b = 2*dot(oc, D)
c = dot(oc, oc) - r*r
disc = b*b - 4*c # discriminant (a = 1 since D is unit length)
if disc < 0: return None # no real roots: the ray misses
t = (-b - math.sqrt(disc)) / 2 # nearest hit
return t if t > 0 else None # in front of the eye, not behind
Add light
Knowing you hit a sphere gives you a silhouette, a flat disk. What makes it look round is shading. At the hit point, compute the surface normal, the direction pointing straight out of the sphere, which is just the hit point minus the center, normalized. Then apply the most important rule in shading, Lambert's law: a surface is brightest when it faces the light head-on and dims as it turns away, following the cosine of the angle between the normal and the light. That cosine is just a dot product.
P = (O[0]+t*D[0], O[1]+t*D[1], O[2]+t*D[2]) # the exact point hit
N = norm(sub(P, sphere_c)) # surface normal there
brightness = max(0.0, dot(N, light)) # Lambert: cosine of the angle
Three details that matter:
- The normal is everything. It is what turns a flat circle into a sphere, because brightness varies across the surface exactly as the normal turns toward and away from the light. Every shading model, from plastic to metal to skin, starts by asking which way the surface faces.
- The
max(0.0, ...)matters physically: a surface facing away from the light gets a negative dot product, and there is no such thing as negative light. Clamp it to zero and that part falls into shadow. - Rays are traced independently, one per pixel, touching no shared state. That is why ray tracing maps so beautifully onto GPUs: a million pixels are a million independent little programs.
Proof: a sphere that looks like a sphere
Render to text, mapping brightness to characters from dark to bright, with the light up and to the left:
%%##*
@@@%%#*+=
@@@@%##*+=-
@@@@%%##*+=-:
@@@@%%##**+=-:
@@@%%##**++=-:
%%%%###*++=-:.
%%%###**+==-:.
####***+==-:.
##***++==-::.
***+++==--:.
=+++===-::.
====--::.
:-:::..
...
That is unmistakably a sphere, not a flat disk. The bright @ region is where the surface faces the light directly, up and to the left, and it fades smoothly through #, +, -, to the dark limb where the surface curves away. Nobody drew a gradient. The shape emerged from the geometry: shoot a ray per character cell, find the sphere, compute the normal, dot it with the light. Real ray tracers do the identical thing, just outputting color pixels instead of characters and asking more follow-up questions per hit.
Where this shows up
This skeleton is the whole field in miniature. Add a second ray from the hit point toward the light, and if it hits something first, that point is in shadow. Bounce a ray off the surface and trace it again, and you get reflections. Bend it as it enters a surface, and you get glass. Trace many random bounces and average them, and you get path tracing, the physically accurate technique behind modern film rendering and the "RT" in ray-traced games. Every one of those is another question asked after the same core step you just built.
If you want to add shadows, reflections, multiple objects, and a real image output, then push toward path tracing, that is the path the general-coding track on IWTLP builds, one ray at a time.