I’ve been wanting to make a system for a 2D game that would offer varying biomes. The two pieces I started on were creating noise-based shaders for each type of block/square, and working on deforming the individual quads so that they weren’t perfectly square, but did always match up along seams.
The beginning of this video briefly shows the near-infinite world space variance for the four shaders that I’ve got working so far. Clockwise from the top left, the materials are: iron, copper, coal, and gold. The movement is actually the whole tileset being moved around the world space (the camera auto-follows the center of the tileset). The second part of the video shows the deformed quads – which actually brings me to the “right” and “wrong” way to devise triangles for a mesh.
Pedants would say this is very much the “wrong” way to do so – that near-square shapes should be developed by pairs of triangles in any case where it’s possible. And for mapping textures to quads, that is certainly true, however the use case here is different.
First, there are no textures being utilizes. With it being strictly shader-based, and with the shader specifying values based on world positions, the triangle design doesn’t matter at all for the visual effects. Additionally, the methods I’m using allows for easy programmatic deformation with a virtually unlimited number of points along each side of the quad to be used for the deformation. Currently that’s being controlled by specifying the number of line segments each side should be broken down into.


This image is just breaking the quad’s sides into two segments each.


Here’s five segments per side.


And lastly ten segments per side.
You can see that with five and ten segments, the actual deformation in the upper left is not manifold in two dimensions. But because the visual is being driven by shaders, it doesn’t actually cause any issues, and the tiles to the left and above this tile still line up properly meaning there’s also no z-fighting. While I still need to clean up the noise used for the deformations a bit, there’s a benefit to knowing that even errant geometry isn’t going to cause issues (because in a randomly generated world with tens of thousands of tiles, there’s always the chance for float-based math to be off).
The additional benefit to using “non-standard” triangles radiating out from the center is that it makes programmatic variation much easier to accomplish as no tile needs to know anything about any of it’s neighbors to deform and still fit properly. This actually leads into another useful factor noted below. But here, the math and calculations are just much easier. In the list of vertices, vertices[0]
is also point (0, 0) of the quad – the center. All other vertices radiate outward, starting from the upper-lefthand corner and moving clockwise around the quad. This also means that setting the mesh triangle[] array is easier because every triplet starts with 0, and then stutter counts upward, e.g. – (0, 1, 2, 0, 2, 3, 0, 3, 4, 0, 4, 5, 0, 5, 6, …)
With such a simple set, a basic for-loop
allows this to be done without prior knowledge of how many segments each quad has.
As mentioned above, there’s another useful bit here. If you look more closely at one of the before and after images, you’ll notice that the quad is centered on the world grid, which is not necessarily the default in Unity. Typically, a quad at coordinates (0, 0) would have it’s upper-lefthand corner at (0, 0) and it’s opposite corner at either (0, 1) or (0, -1) depending on how you have things set up. Here, when building the quad programmatically, rather than using the typical range of (0, 1) I use the range (-0.5, 0.5).
There are two primary reasons for this. From an object control perspective, this means that the tile at (5, -6) is centered at (5, -6), so destruction of that tile is the destruction of a 1×1 area centered at that position; there’s no need to worry about which direction the quad expands from it’s origin because the origin is the center. The second reason is from a programmatic geometry view. Because all tiles are centered, deforming the geometry along each x- and y- value between tiles is consistent between negative and positive worldspace.
Let’s take a little tutorial approach here to see what the code looks like. Here’s the creation of the quad itself. Yes, there’s some housekeeping to do with this yet, but it’s functional and fast.
void CreateQuad()
{
Mesh mesh = new Mesh();
mesh.name = "ScriptedMesh";
Vector3[] vertices = new Vector3[1 + (4 * (stepsXY.Length - 1))];
//Center
vertices[0] = new Vector3(0f, 0f, 0f);
for (int i = 0; i < stepsXY.Length - 1; i++)
{
//Top
vertices[(0 * (stepsXY.Length - 1)) + i + 1] = new Vector3(stepsXY[i], stepsXY[stepsXY.Length - 1], 0f);
//Right
vertices[(1 * (stepsXY.Length - 1)) + i + 1] = new Vector3(stepsXY[stepsXY.Length - 1], stepsXY[stepsXY.Length - 1 - i], 0f);
//Bottom
vertices[(2 * (stepsXY.Length - 1)) + i + 1] = new Vector3(stepsXY[stepsXY.Length - 1 - i], stepsXY[0], 0f);
//Left
vertices[(3 * (stepsXY.Length - 1)) + i + 1] = new Vector3(stepsXY[0], stepsXY[i], 0f);
}
Vector3[] normals = new Vector3[vertices.Length];
for (int i = 0; i < normals.Length; i++)
normals[i] = Vector3.forward;
int[] triangles = new int[4 * (stepsXY.Length - 1) * 3];
int innerIndex = 0;
for (int i = 0; i < 4 * (stepsXY.Length - 1); i++)
{
triangles[innerIndex++] = 0;
triangles[innerIndex++] = i + 1;
triangles[innerIndex++] = i + 2;
}
triangles[innerIndex - 1] = 1;
mesh.vertices = vertices;
mesh.normals = normals;
mesh.triangles = triangles;
mesh.RecalculateBounds();
GameObject quad = new GameObject("Block");
quad.transform.position = position;
quad.transform.parent = this.parent.transform;
MeshFilter meshFilter = (MeshFilter)quad.AddComponent(typeof(MeshFilter));
meshFilter.mesh = mesh;
MeshRenderer meshRenderer = (MeshRenderer)quad.AddComponent(typeof(MeshRenderer));
meshRenderer.material = this.bMat.Material;
this.self = quad;
}
We create the mesh, and then determine the number of vertices. Again, because we aren’t turning a quad into a bunch of squares, this is an easy calculation, and there are no vertices aside from the center vertex that needs to be added.
Vector3[] vertices = new Vector3[1 + (4 * (stepsXY.Length - 1))];
Here, the number of vertices is 1
for the center, plus 4 * (stepsXY.Length - 1)
where stepsXY
is the number of vertices along each side. We’re subtracting 1 from each side because the second corner vertex of a given side will be the first vertex for the other side. In other words, if we’re breaking each side into two segments, you’d have {(-0.5, 0.5), (0, 0.5), (0.5, 0.5)}
for the top, and {(0.5, 0.5), (0.5, 0), (0.5, -0.5)}
for the right side. We don’t want or need to have (0.5, 0.5)
listed twice in the array of vertices, so the -1 prevents that from happening.
We then add the center vertex to the array, and run through a for-loop that also only needs to execute stepsXY.Length - 1
times, as each loop hits the same point on all four sides. Yes, I used 0 * …
in the first (top) calculations – this is just for clarity. You’ll also notice that in the right and bottom calculations, we’re subtracting i
rather than adding it. This is so that triangles calculation later continues to be easier and all vertices in the array exist in clockwise order starting at vertices[1]
.
All normals are forward-facing, so it’s easy to just fill the normals array with Vector3.Forward
given as many places as you have vertices.
Now the triangles array is initialized (remember to multiply it by 3 since each triangle has three vertices). Using an index/iteration value that is external to the for-loop allows for quick calculation; remember from above that the whole array is sets of 0, x, y where x and y stutter-step upwards. Finally, we set the very last triangle point back to 1 – the first value in the vertex array that is on the outer edge (this completes the circuit around the quad).
The rest is just building the mesh out. You may have noticed that I don’t build an array of UVs, nor plug UVs into the mesh building. Again, because I’m not using textures, there’s no mapping from a texture to the mesh, and therefore UVs are not needed. The shader doesn’t care about UVs. Of course, you could build shaders that DO care about UV calculations, in which case UVs would also need to be added (which may be a bit more complicated given the triangle geometry here).
Now we’ll look at the deformation code.
public void DeformQuad()
{
MeshFilter mf = this.self.GetComponent<MeshFilter>();
Mesh m = mf.mesh;
Vector3[] vertices = m.vertices;
for (int i = 0; i < vertices.Length; i++)
{
if ((vertices[i].x == stepsXY[0] || vertices[i].x == stepsXY[stepsXY.Length - 1]) && (vertices[i].y == stepsXY[0] || vertices[i].y == stepsXY[stepsXY.Length - 1]))
continue;
// Top
if (vertices[i].y == stepsXY[stepsXY.Length - 1])
{
float noiseValue = Map(Mathf.PerlinNoise(this.position.x + vertices[i].x, this.position.y + stepsXY[stepsXY.Length - 1]), 0f, 1f, -0.3f, 0.3f);
vertices[i] = new Vector3(vertices[i].x + noiseValue, vertices[i].y + noiseValue, 0f);
}
// Bottom
if (vertices[i].y == stepsXY[0])
{
float noiseValue = Map(Mathf.PerlinNoise(this.position.x + vertices[i].x, this.position.y + stepsXY[0]), 0f, 1f, -0.3f, 0.3f);
vertices[i] = new Vector3(vertices[i].x + noiseValue, vertices[i].y + noiseValue, 0f);
}
// Left
if (vertices[i].x == stepsXY[stepsXY.Length - 1])
{
float noiseValue = Map(Mathf.PerlinNoise(this.position.x + stepsXY[stepsXY.Length - 1], this.position.y + vertices[i].y), 0f, 1f, -0.3f, 0.3f);
vertices[i] = new Vector3(vertices[i].x + noiseValue, vertices[i].y + noiseValue, 0f);
}
// Right
if (vertices[i].x == stepsXY[0])
{
float noiseValue = Map(Mathf.PerlinNoise(this.position.x + stepsXY[0], this.position.y + vertices[i].y), 0f, 1f, -0.3f, 0.3f);
vertices[i] = new Vector3(vertices[i].x + noiseValue, vertices[i].y + noiseValue, 0f);
}
}
m.vertices = vertices;
m.RecalculateBounds();
}
Currently, I’m just using Unity’s built-in Perlin Noise methods in the Mathf library. This leaves much to be desired, but before I dove into creating a noise function, I wanted to ensure this all worked as I expected. Basically, this just extracts the vertices from the mesh, performs the calculations on them, and rebuilds the mesh. The first if-statement is intended to keep the corners of each quad from becoming out of alignment. It probably won’t be kept, but it was something I was trying out.
You can also see that I map the noise function’s return values from (0, 1) to (-0.3, 0.3) as I don’t want any deformed vertex landing at or close to the center of another tile. I need to play around with this value some still, but it will depend on the noise function I end up with later on.
From a performance stance, it might be better to deform the vertices as the mesh is being created initially rather than creating a perfectly square mesh then proceeding to deform it. But this is the type of optimization that will almost certainly hinder legibility of the code, and keeping the two functions separate allows for more easily making changes to either function. And really, the amount of time taken is pretty small. Even with large sets of tiles, it takes no more than ~40μs to generate and ~27μs to deform each quad, for about 17s for over 250,000 tiles. The obvious plan would eventually be to chunk them (ala Minecraft), and there’s almost definitely some room for fine-tuning the process. Plus, this is just executing it in the editor, so it would almost certainly perform better in a release executable.
You must be logged in to post a comment.