Building Polyhedra
(Python + POV-Ray)

First posted: Sept 15, 2005
Last modified: Oct 05, 2005

Fig 1: Rendered program output (see linked Python source code in For Further Reading)

My typical design over the years has been to isolate the mathematical objects, such as vectors and polyhedra, from their output format. Math objects should be platform agnostic in some sense.

However, convenience and theoretical purity represent a trade-off a lot of the time, and in this use case, I eliminate the middle man by letting my math objects "speak POV-Ray" i.e. my vector objects know how to write scene description language directly and don't have to go through some POVwriter object per my habitual design.

This might be a good way to teach refactoring. Start with POV-Ray as your only output format, and develop around that for awhile. Then introduce VRML and/or X3D. Now it becomes more obvious why we might want specialized writer objects to translate math objects into specific output formats. But that needn't be where we start. Starting with POV-Ray alone is a fine beginning.

The Vector class provides just enough infrastructure to support scaling and adding. In this example, we don't rotate our vectors. This is consistent with the objective: to get some scene description language for POV-Ray. POV-Ray has its own easy-to-use rotation methods. Let's just rely on those for now -- no reason to clutter our story with too many convergent/divergent plot lines.

class Vector(object):

    color = 'Red'
    def __init__(self, xyz): = xyz

    def write(self):
        basic = "cylinder {<0,0,0>, <%s,%s,%s>, 0.1" %
        return "%s %s" % (basic, "pigment { color %s } no_shadow }" % self.color)
    def __repr__(self):
        return "Vector <%s, %s, %s>" %
    def __mul__(self, scalar):
        xyz = tuple([scalar * i for i in])
        return Vector(xyz)

    def __add__(self, other):
        xyz = tuple([ i+j for i,j in zip(,])
        return Vector(xyz)

    def _get_xyz(self):
        return '<%s, %s, %s>' %

    def _get_length(self):
        return pow(sum([i**2 for i in xyz]), 0.5)
    coords = property(_get_xyz)

    length = property(_get_length)

As I explained to my daughter yesterday, screenwriters often kill off characters just to keep the plot lines manageable. If too many people learn of Clark's secret in Smallville, then the storylines fly apart. So yes, the tabloid journalist learns the truth, but Clark's dad destroys the tape, and Lex has to shoot the journalist (who is selfishly trying to kill the dad). But I digress. Just keep it simple, is the lesson here.[1]

The Edge class needs two vectors to define an edge. A vector alone always considers the origin to be its tail, so one xyz tuple is sufficient to place a POV-Ray cylinder. An edge, on the other hand, starts anywhere and ends anywhere, so two vectors are required, and the edge runs tip to tip. This has long been my practice.[2]

class Edge(object):

    color = 'Red'

    def __init__(self, v0, v1):
        self.v0 = v0
        self.v1 = v1

    def __repr__(self):       
        return "Edge %s, %s" % (self.v0.coords, self.v1.coords)

    def write(self):
        basic = "cylinder {%s, %s, 0.1" % (self.v0.coords, self.v1.coords)

The Sphere class (I almost called it the Point class -- woulda been OK) gets used in Polyhedra to round off the edge cylinders.

The Polyhedron class needs two data structures: a dictionary of vectors and a "wiring diagram" showing how the vector tips get connected into faces. From face-tuples, edges are distilled. Again, our polyhedra know how to output themselves directly in POV-Ray's scene description language, without a middle man.

My Header and Camera classes are new. My POVwriter used to take care of those details, but since we're talking directly to POV-Ray here, we might as well split these out and suggest an expandable API.

class Header (object):

    # defaults

    bgcolor = "color Gray50"
    lightloc0 = "<300, 300, -1000>"
    lightloc1 = "<-300, 300, -1000>"    
    lightcolor = "White"
    def write():
        return """
#include ""

background { %(bgcolor)s }
light_source{ %(lightloc0)s %(lightcolor)s }
light_source{ %(lightloc1)s %(lightcolor)s }
""" % Header.__dict__

Given the large number of user-modifiable parameters when it comes to the header and camera, why not just set a bunch of default attributes, then string substitute directly from the class dictionary, in case any were changed? This design gives me an opportunity to show static methods in action.

My demo main() calls a function that builds an icosahedron from three mutually perpendicular golden rectangles. Coming up with the "wiring diagram" (face tuples) may require a student to draw and/or label a physical model. I used StrangeAttractors from Design Science Toys (this was not Roger Silber's design, was a far superior toy to Roger's Connection).[3]

Fig 2: Icosa with Spokes by Kirby Urner, Python + POV-Ray
A graphic for StrangeAttractors from Design Science Toys
(click image for larger view)

The main() function also scales the first icosahedron by 80% to get a smaller one, nested inside in a different color (see output above).

Icosa with internal golden rectangles,
and labeled with red dots

(prototype dark gray coneheads)

[1] Smallville: a popular TV series dealing with the mythical superman character's boyhood in the fictional Smallville, Kansas (much of the series was actually shot in British Columbia, Canada).

[2] If you want to see a linear algebra textbook that takes a very similar approach, check out Elementary Linear Algebra by Stewart Venit and Wayne Bishop, ISBN 0-534-91689-9 (see Lines in Rm, Chapter 1). I added this to my collection after participating in debates with Wayne on math-teach (Math Forum) for a number of years.

[3] There's a long and twisted story regarding the relationship between Roger of Roger's Connection and Stu & Cary's Design Science Toys -- but here is not the place for it.

For further reading:

Return to CP4E

oregon.gif - 8.3 K
Oregon Curriculum Network