the workshop
One matrix, four dials
A perspective camera, a unit cube and the depth it produces. The numbers are computed in this page by the library itself, compiled to JavaScript; the drawing is only the ink that follows from them.
Drag a dial. The cube is the same cube throughout; only the camera changes.
near plane → · far plane →
The red row is the depth row: the only one that mentions the near and far planes, and the only one that changes when you move far.
What the page calls
Everything on this page comes from one call. The page passes five
numbers — the field of view in degrees, the eye's distance along
z, the two planes and the aspect ratio of the sheet — and
the library answers with the matrix and the projected corners.
let eye = @moonmath.Vec3::new(1.6, 1.4, distance)
let view = @moonmath.Mat4::look_at(eye, @moonmath.Vec3::zero(), @moonmath.Vec3::unit_y())
let projection = @moonmath.Mat4::perspective(
@moonmath.to_radians(field_of_view),
aspect,
near,
far,
)
let view_projection = projection * view
// Eight corners of a cube from -1 to 1, projected one at a time.
let ndc = view_projection.transform_point(corner)
The bridge that carries the answer across is a dozen lines in
src/examples/web/: it writes the sixteen numbers and the
eight points into a string the page can read. Nothing is reimplemented
on the other side — the ink is the only thing the page adds.
Move the far dial and the cube does not move. The first two rows of the projection do not mention \(f\): it takes part only in depth and in clipping. The curve, which is all depth, moves a great deal.
Why the curve bends
Depth is a projection of a projection. The clip-space \(z\) is affine in the distance, and then it is divided by \(w\), which is the distance itself — so what arrives is a hyperbola:
$$ z_{\text{ndc}}(d) = \frac{f}{f-n} - \frac{fn}{f-n}\cdot\frac{1}{d} $$At the near plane \(d = n\) the two terms cancel and the depth is zero; at the far plane \(d = f\) the second term is \(n/(f-n)\) and what is left is one. Between them, the curve is already at nine tenths of its height when the distance is ten times the near plane, whatever the far plane is:
$$ \frac{f}{f-n} - \frac{fn}{f-n}\cdot\frac{1}{10n} = \frac{f}{f-n}\left(1 - \frac{1}{10}\right) = 0.9\cdot\frac{f}{f-n} $$This is the reason a depth buffer spends its precision where the eye can tell the difference, and the reason a near plane of 0.01 costs more than it looks: it buys a hundredth of a metre and charges the whole buffer for it.
A special function, drawn from the library
The error function erf(x) and its complement
erfc(x), sampled by moonmath/special
compiled to JavaScript and drawn here as ink. The page plots the
numbers the library returned, over -3..3.
The solid line is erf, from -1 to 1; the faint line is
erfc, its mirror about one half.
The two lowest modes of a circular drum, J_0 and
J_1, over 0..20. They are the same call
into the same package, and the curve crosses the point where the
series gives way to the asymptotic expansion without a seam — which
is the whole reason for choosing between them by argument rather than
by taste.
J_0 is the solid line, J_1 the faint one;
the zeros are where a struck drum has no motion at that radius.