Saturday, 19th March 2011
Nodes and Edges
In this first tutorial, we will create our first 3D wireframe. We will:
- Define Node and Edge objects
- Define a Wireframe object consisting of Nodes and Edges
- Define a Wireframe cube
In order to display and manipulate objects on the screen, we have to describe them mathematically. One way to represent a simple shape is with a list of nodes, edges and faces.
- Nodes are points with three coordinates, x, y and z.
- Edges (sometimes called vertices) are lines connecting two nodes.
- Faces are surfaces bounded by multiple edges.
To start with we'll work with wireframe objects, which are only made up of nodes and edges. That way we won't have to worry about shading and figuring out which parts of the object are in front of which others until later.
So let's start by creating some objects (in a file called wireframe.py) with the appropriate properties:
class Node: def __init__(self, coordinates): self.x = coordinates self.y = coordinates self.z = coordinates class Edge: def __init__(self, start, stop): self.start = start self.stop = stop class Wireframe: def __init__(self): self.nodes =  self.edges = 
We should also give the Wireframe class a couple of methods to make it easy to define the nodes and edges of a Wireframe object:
def addNodes(self, nodeList): for node in nodeList: self.nodes.append(Node(node)) def addEdges(self, edgeList): for (start, stop) in edgeList: self.edges.append(Edge(self.nodes[start], self.nodes[stop]))
Both these methods take lists, which I think is the most versatile approach. The addNodes function takes a list of coordinates. The addEdges function takes a list of pairs of node indices to join. For example, we can add three nodes and create an edge between the second two like so:
my_wireframe = Wireframe() my_wireframe.addNodes([(0,0,0), (1,2,3), (3,2,1)]) my_wireframe.addEdges([(1,2)])
Now would be a good time to add a couple of output functions to the Wireframe class to make debugging easier later on:
def outputNodes(self): print "\n --- Nodes --- " for i, node in enumerate(self.nodes): print " %d: (%.2f, %.2f, %.2f)" % (i, node.x, node.y, node.z) def outputEdges(self): print "\n --- Edges --- " for i, edge in enumerate(self.edges): print " %d: (%.2f, %.2f, %.2f)" % (i, edge.start.x, edge.start.y, edge.start.z), print "to (%.2f, %.2f, %.2f)" % (edge.stop.x, edge.stop.y, edge.stop.z)
I think the simplest shape to start working with is a cube. Although a tetrahedron has fewer sides, its sides aren't orthogonal, which makes things a bit trickier. When I tried to work out the coordinates of a regular tetrahedron, it turned out to be more complicated than I anticipated.
The nodes of a unit cube can be easily defined with a list comprehension:
if __name__ == "__main__": cube_nodes = [(x,y,z) for x in (0,1) for y in (0,1) for z in (0,1)]
If you're not familiar with list comprehensions, then now might be a good time to learn how they work, because I think they're one of the best things in Python and so use them whenever I can. The above list comprehension creates a list of every possible 3-tuple of the digits 0 and 1, thus defining the points of a unit cube.
(In this diagram, I represent the z-axis increasing as you move into the screen, but you could equally have it decreasing as you move into the screen. Similarly, the y-axis increases as you move up the screen, which standard for Cartesian axes, but not how computer graphics are generally displayed. I'll discuss this more in the next tutorial, but for now it's not really important.)
We can now create the nodes of our cube:
cube = Wireframe() cube.addNodes(cube_nodes)
The edges are a little trickier to define. I find it easiest to connect them in groups that are parallel. For example, if you look at the diagram below, you can see that the edges parallel to the x-axis connect the node pairs (0,4), (1,5), (2,6) and (3,7), so we can define them with a list comprehension:
cube.addEdges([(n,n+4) for n in range(0,4)])
The remaining edges can be added like so:
cube.addEdges([(n,n+1) for n in range(0,8,2)]) cube.addEdges([(n,n+2) for n in (0,1,4,5)])
Verify the cube has the properties you expect with:
Now we have a cube object, but it only exists as an abstract object. In the next tutorial, we'll display it.