3D Wireframes in SVG (via Python)

SVG is great for line art. It scales nicely for high DPI displays without using much bandwidth. However SVG was not designed for 3D, so it does not provide mechanisms for applying perspective transformation or hidden surface elimination.

These limitations can be overcome for simple meshes by baking the perspective transformation, carefully ordering the paths within the SVG document, and paying attention to the winding direction of projected polygons.

In this post I will show how to use Python to generate vector art as seen at the top of the page, including the fully lit 3D Möbius tube.

To see the complete code that I used to generate all the SVG images on this page, go to this GitHub repo.

First we need to define the classic ingredients that you’ll find in almost any 3D renderer: classes for a viewport, camera, mesh, and scene:

The viewport defines the rectangular region within the final image that the camera projects to. This can be left set to its default values unless the image contains multiple panels.

The camera encompasses the view matrix and the projection matrix. We can use pyrr to generate these; it provides create_look_at and create_perspective_projection functions.

The mesh has a list of faces, a shader, and a style dictionary that gets applied to the SVG group that represents the mesh.

Wait, a shader in SVG? Well, in this context the “shader” is an optional callback function that consumes a mesh face and produces a style dictionary that gets applied to the projected polygon.

The mesh also contains a three-dimensional numpy array called faces whose shape is n⨯m⨯3 where n is the number of faces and m is the number of vertices per face (e.g. m=4 for quad meshes). The last axis has a length of 3 because the mesh consists of X Y Z coordinates.

Before we get to the implementation of our SVG generator, let’s look at how we’d use the above classes to create an image that looks like this:

First, we need to come up with the face data for the octahedron. Simple enough:

The above code snippet generates a 8⨯3⨯3 face array by dereferencing the vertex buffer using numpy’s “fancy indexing” feature.

Next, let’s set up the scene and invoke the renderer. Note the use of the aforementioned pyrr module to compute proper 4x4 matrices.

The stroke width is very small because our renderer normally sets up a SVG viewBox with width and height of 1.0, spanning the region from [-0.5, -0.5] to [+0.5, +0.5].

Our implementation will use the svgwrite module, which accepts style dictionaries that map sensibly to SVG attributes. Note that we use round for joining strokes, which is necessary for making a nice wireframe.

The render is kicked off using the Engine class, which is the only API type that we haven’t mentioned yet. This brings us to the next section…

The engine is responsible for consuming a scene description and generating an SVG file. At a high level it simply iterates though the views and creates a SVG group for each mesh:

Note the usage of numpy.dot to multiply one 4x4 matrix with another. The resulting matrix will be used to project the homogeneous coordinates onto the viewing plane.

The real meat of the renderer is in the engine’s _create_group method, which consumes a mesh and produces an SVG group containing a list of polygons. Some of this code is similar to the OpenGL vertex pipeline.

Some interesting things to note in the above implementation:

Numpy is used to perform most of the math operations en masse, such as applying the 4x4 transform to each vertex in the mesh.

Faces are sorted back-to-front in a very approximate way according to the Z centroid. (see my post about visibility sorting)

The face winding direction (clockwise vs counterclockwise) is determined by evaluating a cross product and passing the result (positive vs negative) to the shading function.

If the shading function returns None , the face is skipped. This can be used to achieve backface culling if desired.

The above image was generated by evaluating parametric equations.

First we define a function that consumes LOD factors (slices and stacks) and a callback function that evaluates a parametric equation. It produces a quad mesh. In abbreviated form, the function looks like this:

(For the complete code, see the GitHub repo.)

We also need to provide callback functions for the shapes of interest:

Note that the above wireframe has varying width. The trick is to completely avoid using stroke. Instead we vary the fill style of each face by examining the divisibility of the face index.

The above scene culls away some of the faces to reveal the inside of the mesh. We draw the sphere in two passes: first backfacing triangles, then frontfacing triangles.

Since the shading callback is given a face index, it can look at the original face and compute a facet normal. This allows us to generate reasonable lighting. Not exactly photorealistic but this is vector art! Here’s the shader I used for the above effect. Note that it culls away backfaces to help optimize the SVG a bit.

Let’s wrap up with one more example of lighting:

The above shape is another parametric surface, similar to the Klein bottle and sphere. The parametric callback looks like this:

Thanks for reading this post! Some references: