[Rod Stephens Books]
Index Books Python Examples About Rod Contact
[Mastodon] [Bluesky] [Facebook]
[Build Your Own Python Action Arcade!]

[Build Your Own Ray Tracer With Python]

[Beginning Database Design Solutions, Second Edition]

[Beginning Software Engineering, Second Edition]

[Essential Algorithms, Second Edition]

[The Modern C# Challenge]

[WPF 3d, Three-Dimensional Graphics with WPF and C#]

[The C# Helper Top 100]

[Interview Puzzles Dissected]

Title: Draw a ray-traced beveled cube in Python

[A ray-traced beveled cube inside reflective walls drawn in Python]

If you've followed my posts for a while, you know that I love algorithms. Computer graphics let you use algorithms to produce complex, colorful, and captivating pictures. Ray tracing is particularly fascinating because it uses a simple algorithmic idea (albeit with sometimes less simple additions) to produce incredible images.

[Build Your Own Ray Tracer With Python] This post uses techniques from my book Build Your Own Ray Tracer With Python to draw a beveled cube (a cube with its edges shaved down). In the picture on the right, I've colored the cube's shaved edges and vertices in different colors so they're easy to see.

Finding Vertices

[Finding the vertices for a beveled cube] The first step in designing a new solid, or at least a complicated solid like this one, is making a good drawing showing where all of its vertices are. I started with a hand-drawn picture on the back of an envelope, but after a while it got too hard to see which points were above which others so I drew the picture on the right in Microsoft Word. The dashed lines show the non-beveled cube. Black quadrilaterals show the beveled cube's major faces that are closest to the viewing position. The blue quadrilaterals show the major faces that are farther away. Red triangles show how the corners are shaved down.

Figuring out the coordinates for each point may seem confusing, but it's not too hard if you have decent spatial reasoning skills. Before we get to that, though, we should talk about coordinate systems.

[A right-handed coordinate system] The coordinate system that I use has the Y axis pointing up, the X axis pointing to the right, and the Z axis pointing out of the screen toward you as shown in the picture on the left. This is called a right-handed coordinate system because, if you align the fingers of your right hand so they point in the positive direction along the X axis and curl them up toward the positive Y axis, your thumb points along the positive Z axis. (I looked around once and about half of the three-dimensional graphics systems out there use this coordinate system and about half use a left-handed system where the Z axis points away from you.)

To find a vertex's coordinates, let the cube's half-width be W so its vertices have coordinates (±W, ±W, ±W). Let the bevel's width be B. A vertex on the beveled cube has coordinates that are the same as those of the closest cube vertex plus or minus B for two of its coordinates.

For example, consider vertex h, which lies in the major face e-f-g-h. That face lies within the cube's face with Z coordinate W. The corner nearest point h has coordinates (W, W, W). The point h has the same Z coordinate but its X and Y coordinates are moved closer to the origin by B so its coordinates are (W-B, W-B, W).

By paying close attention to the picture, you can find the coordinates for the other vertices similarly.

Making a Beveled Cube

Finding the beveled cube's vertices is the hardest part. After that building the beveled cube itself is fairly straightforward, if a bit long. The RtMesh class defined in my book represents a "mesh" containing triangles that share the same material. That class's add_beveled_cube method adds triangles to form a beveled cube. Here's that method's code.

def add_beveled_cube(self, center, W, B, draw_faces=True, draw_edges=True, draw_corners=True): '''Add a cube with the edges truncated.''' # Find the vertices. a = center + W * Vector3(-1, +1, +1) + B * Vector3(+1, 0, -1) b = center + W * Vector3(+1, +1, +1) + B * Vector3(-1, 0, -1) c = center + W * Vector3(+1, +1, -1) + B * Vector3(-1, 0, +1) d = center + W * Vector3(-1, +1, -1) + B * Vector3(+1, 0, +1) e = center + W * Vector3(-1, +1, +1) + B * Vector3(+1, -1, 0) f = center + W * Vector3(-1, -1, +1) + B * Vector3(+1, +1, 0) g = center + W * Vector3(+1, -1, +1) + B * Vector3(-1, +1, 0) h = center + W * Vector3(+1, +1, +1) + B * Vector3(-1, -1, 0) i = center + W * Vector3(+1, +1, +1) + B * Vector3(0, -1, -1) j = center + W * Vector3(+1, -1, +1) + B * Vector3(0, +1, -1) k = center + W * Vector3(+1, -1, -1) + B * Vector3(0, +1, +1) l = center + W * Vector3(+1, +1, -1) + B * Vector3(0, -1, +1) m = center + W * Vector3(+1, +1, -1) + B * Vector3(-1, -1, 0) n = center + W * Vector3(+1, -1, -1) + B * Vector3(-1, +1, 0) o = center + W * Vector3(-1, -1, -1) + B * Vector3(+1, +1, 0) p = center + W * Vector3(-1, +1, -1) + B * Vector3(+1, -1, 0) q = center + W * Vector3(-1, +1, -1) + B * Vector3(0, -1, +1) r = center + W * Vector3(-1, -1, -1) + B * Vector3(0, +1, +1) s = center + W * Vector3(-1, -1, +1) + B * Vector3(0, +1, -1) t = center + W * Vector3(-1, +1, +1) + B * Vector3(0, -1, -1) u = center + W * Vector3(-1, -1, +1) + B * Vector3(+1, 0, -1) v = center + W * Vector3(-1, -1, -1) + B * Vector3(+1, 0, +1) w = center + W * Vector3(+1, -1, -1) + B * Vector3(-1, 0, +1) x = center + W * Vector3(+1, -1, +1) + B * Vector3(-1, 0, -1) # Draw faces. if draw_faces: self.add_polygon([a, b, c, d]) self.add_polygon([e, f, g, h]) self.add_polygon([i, j, k, l]) self.add_polygon([m, n, o, p]) self.add_polygon([q, r, s, t]) self.add_polygon([u, v, w, x]) # Draw edges. if draw_edges: self.add_polygon([a, e, h, b]) self.add_polygon([b, i, l, c]) self.add_polygon([c, m, p, d]) self.add_polygon([d, q, t, a]) self.add_polygon([e, t, s, f]) self.add_polygon([h, g, j, i]) self.add_polygon([l, k, n, m]) self.add_polygon([p, o, r, q]) self.add_polygon([f, u, x, g]) self.add_polygon([j, x, w, k]) self.add_polygon([n, w, v, o]) self.add_polygon([s, r, v, u]) # Draw corners. if draw_corners: self.add_polygon([a, t, e]) self.add_polygon([b, h, i]) self.add_polygon([c, l, m]) self.add_polygon([d, p, q]) self.add_polygon([o, v, r]) self.add_polygon([f, s, u]) self.add_polygon([g, x, j]) self.add_polygon([n, w, k])

Here's a description of the method's parameters.
    center—The cube's center. W—The cube's half-width. B—The bevel's width. draw_faces—If True, the method makes triangles for the cube's major faces. draw_edges—If True, the method makes triangles for the cube's edge faces. draw_corners—If True, the method makes triangles for the cube's corner faces.
The method starts by finding the coordinates for all of the beveled cube's vertices a through x. For each, it starts with the closest cube vertex and it adds an offset to move to the beveled cube's vertex.

For example, consider again vertex h, which is found by this calculation:

h = center + scale * Vector3(+1, +1, +1) + gap * Vector3(-1, -1, 0)

The code starts at the cube's center. To get to the closest cube vertex, we need to add (W, W, W). The code does that by multiplying the vector Vector3(+1, +1, +1) by the cube's half-width W and adding it to center.

Next, the code subtracts B from the point's X and Y coordinates. To do that, it multiples the vector Vector3(-1, -1, 0) by the bevel width B and adding that to the point.

This kind of calculation means we can work with the more convenient coordinates ranging from -1 to +1 and then use vector multiplication and addition to get the correct points.

After it find all of the vertex positions, the code checks the draw_faces, draw_edges, and draw_corners parameters to see which parts of the beveled cube it should draw. An RtMesh object draws all of its triangles with a single shared material, so all of its triangles have the same colors. I added the draw_xxx parameters so we can draw the different parts of the cube in different colors by putting them into separate RtMesh objects.

For whichever of those parameters is True, the method calls the RtMesh object's add_polygon method to create the appropriate polygons. Notice that the major faces and the edge faces are quadrilaterals and the corner faces are triangles. If you look again at the picture at the top of this post, you'll see that this make sense.

Making a Scene

The main program's define_scene method creates the program's scene. It positions and orients the ray tracer's camera, sets the viewing angle, creates an RtScene object, defines lights, and creates the objects. Here's the part of the method that defines the beveled cube.

# Beveled cube. center = Vector3() W = 1.5 B = 0.3 face_cube = RtMesh('face_cube', RtMaterial(RtColor.from_name('blue'))) self.scene.objects.append(face_cube) face_cube.add_beveled_cube(center, W, B, draw_faces=True, draw_edges=False, draw_corners=False) edge_cube = RtMesh('edge_cube', RtMaterial(RtColor.from_name('purple'))) self.scene.objects.append(edge_cube) edge_cube.add_beveled_cube(center, W, B, draw_faces=False, draw_edges=True, draw_corners=False) corner_cube = RtMesh('corner_cube', RtMaterial(RtColor.from_name('red'))) self.scene.objects.append(corner_cube) corner_cube.add_beveled_cube(center, W, B, draw_faces=False, draw_edges=False, draw_corners=True)

This code defines the cube's center, half-width, and bevel width. It then creates an RtMesh object, adds it to the program's scene objects, and calls its add_beveled_cube method to create a beveled cube. It repeats those steps three times to draw the major faces, edges, and corners with different materials.

The rest of the program is the same as those used in the later parts of my book. Read the book to see other details.

TIP: Ray tracing performs a LOT of calculations so the program takes a long time to draw its scene. My computer takes about 90 seconds to draw the scene without the reflective walls and it takes almost 5 minutes to draw with the walls.

To save time, initially start with a small picture (perhaps 200×200 pixels) and don't draw the walls. The picture on the right shows the program drawing without the reflective walls.

Tweak the camera's position and viewing angle until you get a result that you like. Then make a larger picture, drawing the walls if you want to.

[A ray-traced beveled cube drawn in Python]

Conclusion

[Build Your Own Ray Tracer With Python] Building a ray tracer is a fair amount of work, although it's surprisingly easy if you take it one step at a time. After you have the ray tracer built, creating new shapes like a beveled cube is pretty easy, although it may take a while if the shape has many vertices like this one does.

See my book Build Your Own Ray Tracer With Python for more details.

To see an overview of the book, a gallery of sample pictures, and code downloads, CLICK HERE.

To see the book on Amazon, CLICK HERE.

Download the example to experiment with it and to see additional details.

© 2025 Rocky Mountain Computer Consulting, Inc. All rights reserved.