forked from aosabook/500lines
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
22 changed files
with
1,007 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
# `tiny_gfx`: A tiny rasterizer | ||
|
||
A *rasterizer* is a piece of software that converts arbitrary shapes | ||
into *raster images*, which is just a funny name for rectangular grids | ||
of pixels. A rasterizer, in some way or another, is at the heart of | ||
pretty much every modern display technology today, from computer | ||
displays to e-ink displays to printers both 2D and 3D (the graphics in | ||
laser shows are the most notable exception). | ||
|
||
In this chapter, I will teach you a little about how rasterizers work, | ||
by describing `tiny_gfx`, a simple rasterizer in pure Python. Along | ||
the way we will pick up some techniques that show up repeatedly in | ||
computer graphics code. Having a bit of mathematical background on | ||
linear algebra will help, but I hope the presentation will be | ||
self-contained. | ||
|
||
`tiny_gfx` is not practical in many ways. Besides being slow (see | ||
below), the main shortcoming in `tiny_gfx` is that shapes are all of a | ||
single solid color. Still, for 500 lines of code, `tiny_gfx` has a | ||
relatively large set of features: | ||
|
||
- alpha blending for semi-transparent colors | ||
- polygonal shapes (possibly concave, but not self-intersecting) | ||
(TODO: right now only convex polygons are supported) | ||
- circles and ellipses | ||
- transformations of shapes | ||
- boolean operations on shapes (union, intersection, subtraction) | ||
- antialiasing | ||
- empty space skipping and fast rasterization of runs for general | ||
shapes | ||
|
||
Maybe most interestingly, shapes in `tiny_gfx` are extensible. New | ||
shapes are easily added, and they compose well with the other parts of | ||
the code. | ||
|
||
A description of the current version of the code is in `doc/README.md`. | ||
|
||
## A performance caveat | ||
|
||
Rasterizers are so central to display technology that their | ||
performance can make or break a piece of software, and these days the | ||
fastest rasterizers are all based in hardware. Your videogame graphics | ||
card (and even the cheapest smartphones these days) is rasterizing | ||
polygons in highly parallel processors; 192 cores is a typical | ||
number. It should be no surprise, then, that the rasterizer we will | ||
see here is slow: if CPU-intensive tasks in Python run around 50 times | ||
slower than heavily-optimized, low-level code, and if a graphics | ||
driver has around 200 cores at its disposal, a slowdown of 10,000 | ||
times should not be surprising. In reality, `tiny_gfx` is closer to | ||
1,000,000 times slower than the special-purpose graphics rasterizer | ||
from the laptop in which I'm developing it. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
# Overview | ||
|
||
By far, the most important class in this project is `Shape`, in | ||
`shape.py`. | ||
|
||
|
||
## `shape.py` | ||
|
||
`Shape` 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](http://www.cs.utah.edu/~shirley/papers/thesis/ch5b.pdf)). 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.py` | ||
|
||
This 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 `Vector`s | ||
|
||
* utility functions to build transforms (which should perhaps be | ||
`classmethod`s of Transform, except that using them leads to long, | ||
unreadable lines for things that should have short names) | ||
|
||
|
||
## `color.py` | ||
|
||
* `Color`: Self-contained RGBA class in plain old dumb RGBA. | ||
|
||
## `csg.py` | ||
|
||
This 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.py` | ||
|
||
`Ellipse` 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.py` | ||
|
||
`PPMImage`: Simple, self-contained class that stores a two-dimensional array of | ||
`Color` pixels, and writes them as a | ||
[PPM file](http://netpbm.sourceforge.net/doc/ppm.html). | ||
|
||
|
||
## `poly.py` | ||
|
||
`ConvexPoly` 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.py` | ||
|
||
`Scene` 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. | ||
|
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
from shape import * | ||
from geometry import * | ||
from poly import * | ||
from color import * | ||
from ellipse import * | ||
from image import * | ||
from scene import * | ||
from csg import * |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
class Color: | ||
def __init__(self, r=0, g=0, b=0, a=1, rgb=None): | ||
self.rgb = rgb or (r, g, b) | ||
self.a = a | ||
def draw(self, o): | ||
if self.a == o.a == 0.0: | ||
return | ||
if o.a == 1.0: | ||
self.rgb = o.rgb | ||
self.a = 1 | ||
else: | ||
u = 1.0 - o.a | ||
self.rgb = (u * self.rgb[0] + o.a * o.rgb[0], | ||
u * self.rgb[1] + o.a * o.rgb[1], | ||
u * self.rgb[2] + o.a * o.rgb[2]) | ||
self.a = 1.0 - (1.0 - self.a) * (1.0 - o.a) | ||
def fainter(self, k): | ||
return Color(rgb=self.rgb, a=self.a*k) | ||
def as_ppm(self): | ||
def byte(v): | ||
return int(v ** (1.0 / 2.2) * 255) | ||
return "%c%c%c" % (byte(self.rgb[0] * self.a), | ||
byte(self.rgb[1] * self.a), | ||
byte(self.rgb[2] * self.a)) | ||
def __repr__(self): | ||
return "[" + str(self.rgb) + "," + str(self.a) + "]" | ||
@staticmethod | ||
def hex(code, a=1): | ||
if len(code) == 4: | ||
return Color(int(code[1], 16) / 15.0, | ||
int(code[2], 16) / 15.0, | ||
int(code[3], 16) / 15.0, a) | ||
elif len(code) == 7: | ||
return Color(int(code[1:3], 16) / 255.0, | ||
int(code[3:5], 16) / 255.0, | ||
int(code[5:7], 16) / 255.0, a) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
from shape import Shape | ||
from geometry import * | ||
|
||
class CSG(Shape): | ||
def __init__(self, v1, v2, color=None): | ||
Shape.__init__(self, color or v1.color or v2.color) | ||
self.v1 = v1 | ||
self.v2 = v2 | ||
def transform(self, t): | ||
return self.__class__(self.v1.transform(t), self.v2.transform(t), | ||
color=self.color) | ||
|
||
class Union(CSG): | ||
def __init__(self, v1, v2, color=None): | ||
CSG.__init__(self, v1, v2, color=color) | ||
self.bound = AABox.from_vectors(v1.bound.low, v1.bound.high, | ||
v2.bound.low, v2.bound.high) | ||
def contains(self, p): | ||
return self.v1.contains(p) or self.v2.contains(p) | ||
def signed_distance_bound(self, p): | ||
b1 = self.v1.signed_distance_bound(p) | ||
b2 = self.v2.signed_distance_bound(p) | ||
return b1 if b1 > b2 else b2 | ||
|
||
class Intersection(CSG): | ||
def __init__(self, v1, v2, color=None): | ||
CSG.__init__(self, v1, v2, color=color) | ||
self.bound = v1.bound.intersection(v2.bound) | ||
def contains(self, p): | ||
return self.v1.contains(p) and self.v2.contains(p) | ||
def signed_distance_bound(self, p): | ||
b1 = self.v1.signed_distance_bound(p) | ||
b2 = self.v2.signed_distance_bound(p) | ||
return b1 if b1 < b2 else b2 | ||
|
||
class Subtraction(CSG): | ||
def __init__(self, v1, v2, color=None): | ||
CSG.__init__(self, v1, v2, color=color) | ||
self.bound = self.v1.bound | ||
def contains(self, p): | ||
return self.v1.contains(p) and not self.v2.contains(p) | ||
def signed_distance_bound(self, p): | ||
b1 = self.v1.signed_distance_bound(p) | ||
b2 = -self.v2.signed_distance_bound(p) | ||
return b1 if b1 < b2 else b2 |
Oops, something went wrong.