This was the concieved as an application for the font glyphs I collected as part of this project. With a collection of 50k+ glyphs, some of which are pretty exotic shapes, I thought that there might be some cool potential for form synthesis here. The basic concept is that by stamping these shapes into a voxel space, and applying a few different randomly selected operations to them, these interesting, sometimes symmetrical shapes can be produced.
As I mentioned at the end of that article, I have always found the toy problem of a 'spaceship generator' interesting, creating some type of structure that the mind might imagine to represent some kind of a spaceship. This was written as a header, and included in the Voraldo codebase, alongside the optimized glyph collection. The finished model is put into the loadbuffer, the same way as in the previous CPU-side shape gen code, and then copied with the same logic, allowing for the mask values of the existing data to be respected.
With the help of Clepirelli on the Graphics Programming Discord, I decided that std::unordered_map
, a hashmap impelementation, was a good fit for this operation ( at some point I want to implement my own replacement for this functionality, in the future ) - I had initially been trying to do this with an array, clipped to the size of the desired volume, but iteration became difficult, slow, and not very functional for the operations I was trying to implement. By using a representation consisting of a std::unordered_map< glm::ivec3, rgba >
, I was able to handle only occupied voxels, not needing to even think about any voxels which were left empty - it ended up being a much more efficient way to do it. This carries with it a number of benefits - namely, trivial negative indexing of voxels through the use of glm::ivec3
keys and very efficient storage and iteration as part of the std::unordered_map
implementation.
In order to generate some kind of coherent color scheme, I used Inigo Quillez's palette logic. The process is relatively simple, using 4 vec4 control points, you can create some nice, smooth gradients. I found some instability when randomly generating these control points, but he provides some configs that produce very nice gradients. The original implementation uses vec3's, but it's a simple extension to add an alpha channel. If you do randomly generate these values, it makes sense to clamp each channel in the output to the desired range because it can create too large or too small ( negative ) values.
vec4 palette( in float t, in vec4 a, in vec4 b, in vec4 c, in vec4 d ) { return a + b * cos( 6.28318 * ( c * t + d ) ); }
Each of the manipulation operations creates a new hashmap and writes the new values into it. This is to overcome some strange corruption issues I was having using the .clear()
method, like you can see in the picture here. In order to randomly generate a shape, these operations are applied iteratively, in a randomly selected order. There is relatively heavy use of std::random
within this list. Operations are as follows:
This iterates through all entries in the hashmap, and returns the minimum and maximum index along each axis, X, Y, and Z.
This gets the bounding box, as a first operation, then creates a new hashmap. The new hashmap is an exact copy of the previous one, but with no negative indices. This just involves a translation, by subtracting the computed minimum indices from every element in the hashmap.
This randomly selects a random number of glyphs, about a dozen or so, and draws them with a randomly selected thickness and orientation into the volume. This is a simple copy of the extruded glyph, which overwrites any contents that are in the footprint of the new glyph.
This operation randomly picks one of six faces ( +/- X, +/- Y, +/- Z ), and takes a random amount off of that face. This is implemented by writing to the new hashmap all values which are less than / greater than the selected threshold along the selected axis, effectively shaving off some amount of the block.
This creates a new hashmap, with one of the axes effectively inverted. The indices along the selected axis become ( maxAlongAxis - currentIndex )
, making it so that the block has been flipped along that axis. This operation is used in conjunction with the mirror operation, in order to implement mirroring on positive and negative sides.
This does the Square Model operation, in order to make sure there are no negative indices present in the model. Once that finishes, the new model is constructed as all voxels, doubled across the zero of the randomly selected axis. This means that for every voxel present in the original model, for example, ( x1, y1, z1 )
, there now exists another voxel with the same contents at ( -x1, y1, z1 )
, if X is the selected axis.
I feel that this turned out well, and produced some interesting results. At some point I want to return to this and write my own hashmap to hold the data, potentially find some ways to tune it for this particular application. Adding more operations to manipulate the data might be an interesting thing to get into, as well.
I think there is some real potential in this, for automated procedural generation in the space game that I eventually want to make - by using a given color scheme and maybe constraining to small set of glyphs, to get some visual sense shared across multiple randomly generated spaceships. By picking a set of parameters like this, you could have some consistent style shared across ships from a given faction, make it look like they had colors and some geometric features in common. This could be made very flexible, with near-infinite variation, even when constrained to a very small set of glyphs.
Last updated 11/28/2021