Part IV · Path tracing · Chapter 17
The camera's lens
Chapter 1's camera is a pinhole: every ray starts at one point, and everything is sharp at every distance. A real camera has a lens, a disc of glass, and only things at one distance are sharp. Giving the renderer's camera a lens takes a few lines. For a while it was also how the renderer saw a floor through glass, a story section 3 tells.
1A lens brings one distance to a point
Glass bends light where it enters and where it leaves (chapter 10 says by how much). A lens is glass curved so that the bending brings light together: all the light from one point at a certain distance, wherever it passes through the lens, meets again at one point behind it, on the film. Light from a nearer or farther point meets in front of the film or behind it, and on the film it is a small blurred disc.
The renderer runs this backwards, from the eye, and needs no glass for it. ShadyTracer's lens is a thin lens: a disc of radius R round the eye (here, seen from the side, the thick bar; the eye drawn just behind it marks where the pinhole was), focused at a distance F (the red dashed line, the focus plane, the flat surface at that distance; drag it by its handle F). The rule is one sentence: each pixel looks at one point of the focus plane, and all its rays aim there, each from a random point of the lens. That point is where the pinhole's ray for the pixel crosses the focus plane. So a point on the focus plane is reached from every point of the lens by the rays of one pixel: the rays drawn from across the lens all meet there, and the picture shows it as one dot.
Which picture? As in chapter 1, the renderer has no film behind the eye: its picture is a flat screen one unit in front of the eye (the short grey line marked "picture, 1 out"), and a pixel is the direction from the eye through one point of it. The column on the right is that picture drawn larger, top at the top: where a ray lands on it says which pixel the ray belongs to. The point is the orange dot under its handle, and where it lands is orange too.
Drag the point nearer. Now the rays from across the lens that pass through it are the rays of different pixels: carried on (dashed), they cross the focus plane at different heights, and each height is another pixel's point. The ray from the lens's top edge is drawn in green, and so is the mark where it lands on the picture. On the picture the point spreads into a streak, its blur (in 3D a disc, the circle of confusion). The figure's lengths are in metres, as the engine's are.
The streak's length is 2R |1/F − 1/D| on the picture one unit out (the fold-out below works it out): here 2 × 0.6 × |1/6 − 1/3.2| = 0.175. How much of the picture is that? The picture reaches tan(fov/2) above the axis and as far below (the field of view is the angle from its bottom edge to its top, so half of it is the angle from the axis to the top). For this camera's 40° that is tan 20° = 0.364, so the blur is 0.175 / 0.364 = 0.48 of the half-height. A picture 1000 pixels high has a half-height of 500, so the blur is about 240 pixels. Larger lens, larger blur; nearer the focus, smaller.
Shrink the lens to nothing (R = 0) and every ray starts at the eye: the pinhole, sharp everywhere. In ShadyTracer a radius of 0 is the pinhole exactly, the same ray from the same random numbers, so a picture without a lens is the one it always was.
The mathsThe blur
A ray from the lens point at height a to the point at distance D and height h rises h − a over a distance D, so over a distance F it rises (h − a) F/D (similar triangles). At the focus plane it is at height Y = a + (h − a) F/D. That point of the focus plane belongs to the pixel whose pinhole ray passes through it. The pinhole ray starts at the eye, at height 0, and reaches height Y over a distance F; over one unit it reaches Y/F, and that is its place on the picture one unit out:
y = YF = aF + hD − aD = hD + a (1F − 1D)
Over the lens, a runs from −R to R, so y runs over 2R |1/F − 1/D|: the blur. It is zero at the focus (D = F), grows with the lens, and grows with how far 1/D is from 1/F. So a point twice as far as the focus blurs less than one half as far. Twice as far, D = 2F, gives |1/F − 1/(2F)| = 1/(2F); half as far, D = F/2, so 1/D = 2/F, gives |1/F − 2/F| = 1/F, twice as much. Divide by tan(fov/2), the picture's half-height at one unit, for the share of the half-height. In 3D the streak is a disc, the circle of confusion.
lensImage. Sliders set the lens's radius and its focus.2Depth of field
The same in 3D, a picture made the way the renderer makes it. A real lens gathers light through every point of its opening at once, and a pixel shows the average over all of them. The renderer cannot trace them all, so each sample takes one random point of the lens for every pixel (chapter 12): averaged over samples, the picture becomes that average, and the blur appears as samples add up, grainy at first. Three balls stand at three distances: red near, blue in the middle, gold far. With a lens of radius 0, a pinhole, all three are sharp; 4 samples a pixel are enough, since there is nothing to average over the lens.
With a lens of radius 0.125 m focused on the middle ball, the near and the far one blur, each by how far its 1/D is from 1/F. This is depth of field: the range of distances that look sharp. (ShadyTracer's camera settings ask for the aperture, the lens's diameter: 0.25 here.)
Focused on the near ball, it sharpens and the others go soft.
A wider lens, of radius 0.3 m, and only the first 2 samples: every pixel has averaged two points of the lens, and the blur is a grain of dots.
After 48 samples the grain has gone: the blur is itself an average over the lens. The wider lens shrinks the range that looks sharp: the middle and the far ball are much softer now.
lensDisc and lensRay).
Choose the ball in focus, the lens's radius, and whether to stop after 2 samples.3How ShadyTracer does it
Why a lens, beyond depth of field? Take a floor inside a glass prism, lit by a lamp through the prism, and seen through the prism. The light's path is lamp → glass → floor → glass → eye. A path tracer builds such a path one step at a time (chapter 13). At glass it has no choice: the glass's rule decides where light goes on (chapter 10). At a matte floor it picks one random direction to go on in. And from a matte point it can aim a shadow ray straight at a lamp (chapter 14), but not through glass, since a shadow ray goes straight and the light would have to bend. Built from the eye, the path passes the glass (decided), meets the floor (one random direction), passes the glass again (decided), and must then happen to hit the lamp: for a small lamp seldom, for a point lamp never. Built from the lamp the other way (light tracing, chapters 18 and 19), it meets the same wall at the other end: after the floor's one random choice, the glass decides the rest, and the path must happen to hit the eye, which for a pinhole is a point. The code calls these paths SDS: specular (glass or a mirror, decided), diffuse (matte, free), specular. A lens is an area, which light leaving the glass can hit. The lens's own file says so:
The camera ray, in the kernel that makes them, piece by piece. right, up and fwd are the
camera's three directions as seen in the scene; fwd points backwards, out of the scene, so −fwd looks into it.
dirCam is the pixel's point on the picture one unit in front of the eye, in the camera's own
directions: xy runs from −1 to 1 across the picture, so x·fx across and y·fy up (fx and fy are
the picture's half-width and half-height at one unit), and −1 along fwd, which is one unit forward. The bracket
turns it into a direction in the scene: the pinhole's ray, before it is made one unit long. Times F
(UB.camLens.y; UB holds the values handed to the GPU) it reaches the focus plane.
Minus off, the lens point's offset from the eye, it is the way from the lens point to that point of
the focus plane; normalize makes it one unit long. lensDisc turns two random numbers
u and v into a point of the disc, evenly over its area: at a distance R√u
from the middle, at an angle 2πv (the square root because the outer rings are larger, and must get as
many points as their area).
The camera hands the kernels half its aperture (the radius) and its focus, or the distance to what it looks at when no focus is set, so a view you turn round its target stays focused on it (). An orthographic camera has no lens: it is the kind with no perspective, every ray parallel.
4Tricks and trade-offs
Each is stamped with how it is known; point at a stamp to see what it means. The first is for readers who have read chapters 18 and 19.
The lens's area cancels. When light found by tracing from a lamp is connected to the camera, the
connection picks a point on the lens, and the pixel's value is an average over the lens: one divides by the
lens's area, the other's chance of picking that point carries the same area, and the two cancel. So the
connection's weight is the pinhole's, measured from the lens point instead of the eye (lens.js, "A
pixel's value, written as a measurement").
A crossing needs no shadow ray. A light traced from a ray file is seen when its ray passes through
the lens (section 5). The march moves a ray in steps that the distance to the nearest surface allows, so nothing
is in the way of any step: a step that crosses the lens reached it, and the renderer adds it to the picture without
asking again whether the way was clear (lens.js, "A faceless light seen through the lens").
No lens, no change. The lens's two random numbers are drawn whether there is a lens or not, so turning the lens on or off changes nothing else in the sequence of random numbers a picture uses.
5What went wrong before
- A black floor in a prism. Section 3's story: with a pinhole, the floor in the prism was black (the user's picture, 2026-10-08), and the lens, an area, was added partly for that.
- White specks. From 0.43.0 photons that happened to pass through the lens added their light to the pixel they crossed. That counted the floor behind glass only when a photon happened through: white specks in the user's picture of 2026-10-15. The photon map (chapter 19) took that light over in 0.61.0, through any aperture or none, and the lens kept only its depth of field.
- A ray file nobody could see. A ray file is a lamp maker's list of the rays leaving a lamp, with no surface: no camera ray can hit it, and its light reached the picture only from the surfaces it lit. Looking at the lamp itself through a lens, the user saw nothing (2026-11-04). Since version 0.83.0 a ray of such a light that passes through the lens adds itself where a real camera would put it, straight from the lamp or by a mirror or clear glass, and only then: light that has bounced off a matte surface is that surface's to show, and counting it at the lens too would count it twice. Through a pinhole it still shows nothing, since no ray passes through a point.
- Focal length and aperture that did nothing. Depth-of-field settings once existed that no part
of the renderer used; they were removed in version 0.22.6, and the lens came back, working, as aperture and focus
in 0.43.0 (the comment in
lib/camera.js).
6Try it
- In the first figure, put the point twice as far as the focus, then half as far. Which blurs more, and why does 1/D say so?
- In the 3D figure, focus on the far ball and open the aperture. Does the near ball blur more than the middle one?
- In ShadyTracer, open the camera's settings over the picture and set an aperture and a focus; watch the picture settle more slowly where it is blurred.