par_shapes.h

When tinkering with a new graphics API or raytracing kernel, I often want to use procedurally-generated shapes to avoid worrying about loading art assets.

Hence my motivation for authoring a single-file, zero-dependency, C99 library that can generate simple shapes and perform basic operations on them. These operations include:

  • Applying affine transformations
  • Computing surface normals
  • Welding colocated vertices

The library provides a set of functions that populate fields of the following structure.

typedef struct par_shapes_mesh_s {
    float* points;           // Flat list of 3-tuples (X Y Z X Y Z...)
    int npoints;             // Number of points
    PAR_SHAPES_T* triangles; // Flat list of 3-tuples (I J K I J K...)
    int ntriangles;          // Number of triangles
    float* normals;          // Optional list of 3-tuples (X Y Z X Y Z...)
    float* tcoords;          // Optional list of 2-tuples (U V U V U V...)
} par_shapes_mesh;

The two optional fields might be null, but every other field is guaranteed to have valid values. The PAR_SHAPES_T macro defaults to uint16_t, which can be overridden if desired.

When you’re done extracting the data you need from the mesh, be sure to free it:

par_shapes_mesh* m = par_shapes_create_subdivided_sphere(1);
// ...
par_shapes_free_mesh(m);

Platonic Solids

The above scene was constructed like so:

dodecahedron = par_shapes_create_dodecahedron();
par_shapes_translate(dodecahedron, 0, 0.934, 0);

tetrahedron = par_shapes_create_tetrahedron();
par_shapes_translate(tetrahedron, 1, 0, 3.5);

octohedron = par_shapes_create_octohedron();
par_shapes_translate(octohedron, -2.25, 0.9, -.5);

icosahedron = par_shapes_create_icosahedron();
par_shapes_translate(icosahedron, -1, 0.8, 3.5);

cube = par_shapes_create_cube();
par_shapes_rotate(cube, PAR_PI / 5.0, (float[]){0, 1, 0});
par_shapes_translate(cube, 1, 0, 0.5);
par_shapes_scale(cube, 1.2, 1.2, 1.2);

Upon creation, platonic solids are welded and do not have normals or texture coordinates.

Since normals can only be per-vertex, you might need to “unweld” a mesh (i.e., dereference its index buffer) in order to perform lighting with facet normals. Here’s how to unweld the dodecahedron and populate its normals field:

dodecahedron = par_shapes_create_dodecahedron();
par_shapes_unweld(dodecahedron, true);
par_shapes_compute_normals(dodecahedron);

Note that unwelding a mesh creates a rather silly index buffer: 0, 1, 2, etc. You can leave the index buffer untouched by passing false to the unweld function, although that makes the mesh invalid.

Parametric Surfaces

The shapes library can also generate various parametric surfaces. Unlike the platonic solids, these shapes are populated with smooth normals and texture coordinates, right out of the box.

The above scene was constructed like so:

// Tessellate an open-ended cylinder with 30 slices and 3 stacks.
shape = par_shapes_create_cylinder(30, 3);
par_shapes_rotate(shape, -PARG_PI / 2.0, (float[]) {1, 0, 0});
par_shapes_translate(shape, 0, 0, 3);

// Create a disk-shaped cylinder cap with 30 slices.
shape = par_shapes_create_disk(1, 30, (float[]){0, 1, 3},
    (float[]){0, 1, 0});

// Instantiate a dome shape.
shape = par_shapes_create_hemisphere(10, 10);
par_shapes_scale(shape, 0.2, 0.2, 0.2);
par_shapes_translate(shape, 0, 1, 3);

// Create a rectangular backdrop.
shape = par_shapes_create_plane(3, 3);
par_shapes_translate(shape, -0.5, 0, 1);
par_shapes_scale(shape, 4, 1.5, 1);

// Place a submerged donut into the scene.
shape = par_shapes_create_torus(30, 40, 0.1);
par_shapes_scale(shape, 2, 2, 2);
par_shapes_translate(shape, 0, 0, 3);

All parametric generators take two tessellation levels: slices and stacks, which control the number of divisions across the UV domain.

If the ready-made surfaces are insufficient, clients can use a callback function to create a custom parametric surface:

typedef void (*par_shapes_fn)(float const*, float*, void*);

// Create a parametric surface from a callback function that consumes a 2D
// point in [0,1] and produces a 3D point.
par_shapes_mesh* par_shapes_create_parametric(par_shapes_fn, int slices,
    int stacks, void* userdata);

Spheres

The shapes library provides two ways of generating spheres, and both have a fixed size and position: radius=1 and center=(0,0,0). Clients can easily scale / translate the result as needed.

// Create a sphere with texture coordinates and small triangles near the poles.
par_shapes_mesh* par_shapes_create_parametric_sphere(int slices, int stacks);

// Generate a sphere from a subdivided icosahedron, which produces a nicer
// distribution of triangles, but no texture coordinates.
par_shapes_mesh* par_shapes_create_subdivided_sphere(int nsubdivisions);

In the above image, the left and center spheres are subdivided, and the right sphere is parametric.

Miscellaneous

The library also provides generator functions for various shapes beyond what was covered here; see the header file for more information.

// Generate a rock shape that sits on the Y=0 plane, and sinks into it a bit.
// This includes smooth normals but no texture coordinates.
par_shapes_mesh* par_shapes_create_rock(int seed, int nsubdivisions);

// Create trees or vegetation by executing a recursive turtle graphics program.
// The program is a list of command-argument pairs.  See the unit test for
// an example.
par_shapes_mesh* par_shapes_create_lsystem(char const* program, int slices,
    int maxdepth);

// Tessellate a trefoil knot; same arguments as par_shapes_create_torus.
par_shapes_mesh* par_shapes_create_trefoil_knot(int slices, int stacks,
    float radius);

Here are links to the library and the github repo that it lives in.