Procedurally randomizing meshes with predictablility

Posted by Thomas Neumüller on 2022-01-25

When creating a game where the same mesh is reused many times, there is the risk of noticeable repetition. To make your game feel more natural and organic, you need to introduce small varieties, such as randomly positioned chimneys, windows or foliage, for example. This usually requires a lot of effort, because the meshes have to be prepared to allow for variations, or multiple versions of the same object have to be modelled that are then randomly chosen from.

Today we want to look at a simpler way of achieving variety, that while it doesn’t achieve the same quality of results, can be done with much less effort, which can be decisive for small teams with limited resources (or publishers who don’t want to put the smallest possible amount of effort into their games in order to maximize profit).

Using pseudo-randomness and vertex shading, we want to offset the vertices of a mesh by a small amount to make it look slightly different each time it is used in the game.

Naive approach

Naively, we can just displace every vertex by a random amount. Random in this case means by the value of a noise that is seeded with the position of the vertex in world space. True randomness in shaders doesn’t make sense, unless you want the player to have a seizure - the random value would be different in each frame, we want the same “random” result for each vertex in every frame, just a different one for each vertex. Noises that are seeded with the vertice’s world space position give us just that. We use two noises, one for each X and Z offset (I don’t want vertical offset in this case).

The following shader graph would give the following result:

As you can see, while promising, there is an obvious issue with this approach: Vertices right next to each other can sometimes be offset by an amount so different from each other, that the geometry becomes self-intersecting. The likelyhood of such errors can be reduced by lowering the scale of the noise, but a high scale gives better results in terms of randomness across the whole mesh: A small scale means the offset across all vertices will be more similar, possibly just shifting the whole mesh into one direction without introducing much variety. Large noise scale is therefore desirable.

Controlling vertex offsets with a UV map

Luckily, there are possibilities to “annotate” vertices with extra data, one of which are UV maps. A mesh can contain multiple UV maps, the first (more accurately called “zeroeth”, because it is usually referred to as UV0) of which is famously used for textures, but maps UV1 and up can be used for any data the developer desires. We will use it to control the offset of vertices.

Instead of seeding the noise with the world space position of each vertex, we will seed it with the root position of the mesh (obtained via a custom function node containing the code below) to which we add the value of UV1.

Out = mul(unity_ObjectToWorld, float4(0,0,0,1));

UV1 now will be set up to contain equal values for vertices that we want to have an equal offset, so the vertices on the corners of the roof of our example house for example. In your 3d modelling program of choice (I am using Blender, because it’s good and free), add an additional UV map. Populate it by projecting UV coordinates from above, so that each vertice’s UV1 coordinate conforms to it’s X and Z position.

Now, in the UV editor, select groups of vertices that should have the same offset and merge them. The result will look something like this:

The shader graph will receive the following modifications:

The variables Intensity and Randomness control the amount of displacement as well as the scale of the noise.

The result then looks like this:

Additionally, the graph could be set up to not move UVs that are at the very top left in the UV editor at all, so vertices that should receive no offset can be moved there. This could be done by calculating the distance of the UV1 coordinate from (0,0) and feed it into an edge node with the edge at 0.06 and multiplying the final offset with the output of the edge node.

Thoughts or questions? Leave me a comment!