incomplete/rasterizer/doc/README.md
By far, the most important class in this project is Shape, in
shape.py.
shape.pyShape is the main class of tiny_gfx. The class itself contains a
very basic constructor, and the main code for the scanline rasterizer
itself, in Shape.draw.
Concrete subclasses of Shape need to implement two methods:
contains and signed_distance_bound. Both methods take a point in
the plane. contains should return true if the shape contains the
point (no surprise there). signed_distance_bound is the main method
used by tiny_gfx to accelerate the rendering. The idea is that
signed_distance_bound should return a real value that describes a
distance certificate. If this value (call it r) is negative, then the shape is
promising that not only is the point is not in the shape, but no
points inside a ball of radius -r around the point is in the shape as
well. If the value is positive, then the shape is
promising that not only is the point in the shape, but a ball of
radius r is entirely contained in the shape as well. If the shape
cannot guarantee anything either way, it should return zero. In other
words, signed_distance_bound is used by Shape.draw to skip large
numbers of checks against contains, both inside and outside the
shape. It's ok if signed_distance_bound is conservative. In fact,
always returning zero would be correct behavior. It would just be
much slower.
Finally, concrete subclasses of Shape must have a self.bound field
that stores an AABox object that represents an axis-aligned bounding
box of the object. The bounding box does not need to be exact, but it
must be conservative: self.contains(p) must imply
self.bounds.contains(p). The smaller the area of this box, the more
efficient rasterization will be.
Shape.draw works by traversing candidate image pixels inside the
shape bounding box in row order. Anti-aliasing is performed by
jittered sampling (see Figure 5.16
here). Note
that if Shape.draw knows that the pixel is entirely inside the image
(because r from above is greater than the length of a pixel
diagonal), then no anti-aliasing tests are needed. This allows the
default setting for super-sampling to be to take 36 samples inside a
pixel with no large loss in performance. By the same token, if r is
a negative number, then Shape.draw skips those pixels with no
further checks. Anti-aliasing is performed by counting the number of
points that pass the contains call, and updating the image pixel.
geometry.pyThis file contains geometry classes generally needed for the rasterizer.
Vector, a 2D vector with your basic operator overloading and
methods. In this code we use this class to store both points and
vectors. There are reasons why this is a bad idea, but for sake of
simplicity and brevity, we do it.
AABox, a 2D axis-aligned bounding box
HalfPlane, a class that models one-half of the 2D plane by a
linear equation
Transform, a class for 2D affine transformations of Vectors
utility functions to build transforms (which should perhaps be
classmethods of Transform, except that using them leads to long,
unreadable lines for things that should have short names)
color.pyColor: Self-contained RGBA class in plain old dumb RGBA.csg.pyThis file contains classes used for Boolean operations with
shapes. (CSG stands for Constructive Solid Geometry, the three-letter
acronym used in graphics for the idea). The base class is CSG,
and there's a class for each supported Boolean operation: Union,
Intersection and Subtraction.
If the signed_distance_bound and contains ideas above makes sense,
then the code for the three classes should be self-explanatory.
ellipse.pyEllipse is the most complicated Shape available, and is presented
as an example of the generality of the approach used here. The
internal representation for an ellipse is the implicit equation form,
which defines the ellipse as the set of points for which a certain
function is less than 0. In this case, the function is a quadratic
polynomial in x and y.
Ellipse.contains simply evaluates the implicit equation.
The code for Ellipse.signed_distance_bound is actually relatively
clever and non-trivial. This flavor of geometric insights is prevalent
in graphics, so I wanted to give an actual example of it in the
code. However, it takes a diagram to show why it works, so I don't
really expect you to understand it without comments.
I have to draw a diagram to explain how it works, so if you run into trouble on that one, send me a message and I'll move it to the top of my priority queue.
There's more description of the code for Ellipse under rasterizer/ellipse.md.
image.pyPPMImage: Simple, self-contained class that stores a two-dimensional array of
Color pixels, and writes them as a
PPM file.
poly.pyConvexPoly represents a convex polygon by a set of
half-planes. Shape.contains simply tests all half-planes
corresponding to each edge of the polygon, and
Shape.signed_distance_bound takes the most conservative value across
all of the shape half-planes. This actually gives values of 0 for
points on the "infinite line" spanned by polygon edges, but that's
fine because the result needs only be conservative.
scene.pyScene stores a hierarchical scene graph, where each node is either a
Shape or a Scene itself. In addition, each Scene object carries
an affine transform that's applied to every element under it. By
having different scenes holding the same object lists with different
transformations, it becomes easy to express scenes with repeated
elements.