2-Sharing and Crossing
2-Sharing and Crossing
You'll learn to
This tutorial has been made with Unity 4.5.2. It might not work for older versions.
Reusing Vertices
When triangulating cells, we consider each cell in isolation. This keeps things simple, but results
in quite a lot of vertices. For example, an isolated filled voxel results in four cells containing a
single triangle, each with three vertices, for a total of twelve vertices.
In this example there are really only five unique vertices, but the central vertex is included four
times while the others are included twice each. So if we could let adjacent cells share vertices,
we would reduce the vertex count considerably.
To reuse vertices, we have to keep track of them. The most straightforward approach would be to
store a vertex index per voxel, but we don't really have to remember that many at once. As we
https://catlikecoding.com/unity/tutorials/marching-squares-2/ 1/19
2022/5/28 15:36 Marching Squares 2, a Unity C# Tutorial
triangulate the grids one row of cells at a time, we can suffice with caching the vertex indices for
one row of cells. While this is a bit more involved, it means our cache size is linear instead of
quadratic, so it scales better.
For a single row of cells, we need to keep track of two rows of vertex indices. These are the
minimum and maximum vertex rows, or in our case the bottom and top rows. There is also a
middle row, which is for all vertices along the vertical edges between cells of the row.
After completing a row of cells, the current maximum row becomes the minimum row for the next
row of cells. This shifting works the same as the shifting of dummy voxels used to fill the gaps
between grids.
Before filling the cache, we need to know its size. The minimum and maximum cache rows need
room for one vertex index per voxel plus one for each X edge between those voxels. And
vertices of the gap cell need to be cached too, which adds another two. So these caches have to
be arrays with a length equal to twice the grid resolution plus one.
The middle row only needs room for one index per Y edge. Actually, as this part of the cache isn't
needed for the next cell row, we don't need to remember the entire row. We can suffice by
caching two indices and shifting those, as if they're two other minimum and maximum arrays of
length one.
Now it's time to start coding again. Add two cache arrays and two cache integers to VoxelGrid
and initialize the arrays in Awake.
https://catlikecoding.com/unity/tutorials/marching-squares-2/ 2/19
2022/5/28 15:36 Marching Squares 2, a Unity C# Tutorial
When triangulating, we now have to fill the cache. To start the process, we have to fill the initial
row.
if (xNeighbor != null) {
dummyX.BecomeXDummyOf(xNeighbor.voxels[0], gridSize);
}
FillFirstRowCache();
TriangulateCellRows();
if (yNeighbor != null) {
TriangulateGapRow();
}
mesh.vertices = vertices.ToArray();
mesh.triangles = triangles.ToArray();
}
The first step of the first row is to check if the first corner needs a vertex, and if so cache it.
Caching the corner vertex means that we add it to the vertex list and store its index in our cache.
As we're always working on the top row, we store it in the maximum cache row. Of course you
should only add the vertex if the voxel is actually filled.
To cache the rest of the row, we have to visited successive pairs of edges and corners. We
create another method for that, and besides passing the voxels we also pass it the cache index.
As we visit two additional vertices per cell, the cache index increases twice as fast as the cell
index.
The edge only has to be cached if the two corner voxels are different, because that's when there
is an edge crossing. Then the next corner, which is the second corner of the current cell, is
cached.
After that we have to cache the vertices of the gap cell. Let's also move the dummy code into
FillFirstCacheRow so its initialization and use are in the same place.
https://catlikecoding.com/unity/tutorials/marching-squares-2/ 3/19
2022/5/28 15:36 Marching Squares 2, a Unity C# Tutorial
FillFirstRowCache();
TriangulateCellRows();
if (yNeighbor != null) {
TriangulateGapRow();
}
mesh.vertices = vertices.ToArray();
mesh.triangles = triangles.ToArray();
}
Now we continue with triangulating the cell rows. Each row, we begin by swapping the cache
rows so the previous maximum becomes the new minimum.
Each row, again start with the first corner and keep caching the next edge and corner. As we're
working on the top vertices of the cells, we have to add the resolution to the current cell index.
Now we also have to cache the edge vertices of the middle row. We can use a single method for
that which both shifts the cache and adds the vertex. This method needs to be called at the start
of each row for the leftmost edge and once for the right edge of each cell. Because the same
voxels will now be needed a couple of times in the inner loop, I put them in variables for clarity.
https://catlikecoding.com/unity/tutorials/marching-squares-2/ 4/19
2022/5/28 15:36 Marching Squares 2, a Unity C# Tutorial
CacheNextMiddleEdge(voxels[i], voxels[i + resolution]);
And the gap cell at the end of each rows needs caching too.
The last step is to deal with the gap row at the top of each grid. The approach is the same, but
with more dummies.
if (xNeighbor != null) {
dummyT.BecomeXYDummyOf(xyNeighbor.voxels[0], gridSize);
CacheNextEdgeAndCorner(cells * 2, dummyY, dummyT);
CacheNextMiddleEdge(dummyX, dummyT);
TriangulateCell(voxels[voxels.Length - 1], dummyX, dummyY, dummyT);
}
}
Right now we're caching all vertices, but aren't actually using them yet. So we just end up with
even more vertices. We need new versions of our polygon methods that use indices instead of
vertices. They're the same as the old ones, except that the parameters are integers and they no
longer add vertices to the vertex list themselves.
https://catlikecoding.com/unity/tutorials/marching-squares-2/ 5/19
2022/5/28 15:36 Marching Squares 2, a Unity C# Tutorial
triangles.Add(b);
triangles.Add(c);
triangles.Add(a);
triangles.Add(c);
triangles.Add(d);
}
To retrieve the cached vertices, we need to pass along the cache index to TriangulateCell, so
give it another parameter.
We already calculated the cache index while processing the cells, so if we remember it we can
simply pass it to TriangulateCell as well.
if (xNeighbor != null) {
dummyT.BecomeXYDummyOf(xyNeighbor.voxels[0], gridSize);
int cacheIndex = cells * 2;
CacheNextEdgeAndCorner(cacheIndex, dummyY, dummyT);
CacheNextMiddleEdge(dummyX, dummyT);
TriangulateCell(cacheIndex, voxels[voxels.Length - 1], dummyX, dummyY, dummyT);
}
}
Now we have to update the switch statement in TriangulateCell. Each vertex argument has to
be replaced with the corresponding cache entry. Here is the complete mapping.
a.position becomes rowCacheMin[i]
a.xEdgePosition becomes rowCacheMin[i + 1]
b.position becomes rowCacheMin[i + 2]
c.position becomes rowCacheMax[i]
c.xEdgePosition becomes rowCacheMax[i + 1]
d.position becomes rowCacheMax[i + 2]
a.yEdgePosition becomes edgeCacheMin
b.yEdgePosition becomes edgeCacheMax
https://catlikecoding.com/unity/tutorials/marching-squares-2/ 6/19
2022/5/28 15:36 Marching Squares 2, a Unity C# Tutorial
private void TriangulateCell (int i, Voxel a, Voxel b, Voxel c, Voxel d) {
int cellType = 0;
if (a.state) {
cellType |= 1;
}
if (b.state) {
cellType |= 2;
}
if (c.state) {
cellType |= 4;
}
if (d.state) {
cellType |= 8;
}
switch (cellType) {
case 0:
return;
case 1:
AddTriangle(rowCacheMin[i], edgeCacheMin, rowCacheMin[i + 1]);
break;
case 2:
AddTriangle(rowCacheMin[i + 2], rowCacheMin[i + 1], edgeCacheMax);
break;
case 3:
AddQuad(rowCacheMin[i], edgeCacheMin, edgeCacheMax, rowCacheMin[i + 2]);
break;
case 4:
AddTriangle(rowCacheMax[i], rowCacheMax[i + 1], edgeCacheMin);
break;
case 5:
AddQuad(rowCacheMin[i], rowCacheMax[i], rowCacheMax[i + 1], rowCacheMin[i + 1]);
break;
case 6:
AddTriangle(rowCacheMin[i + 2], rowCacheMin[i + 1], edgeCacheMax);
AddTriangle(rowCacheMax[i], rowCacheMax[i + 1], edgeCacheMin);
break;
case 7:
AddPentagon(
rowCacheMin[i], rowCacheMax[i], rowCacheMax[i + 1], edgeCacheMax, rowCacheMin[i + 2]);
break;
case 8:
AddTriangle(rowCacheMax[i + 2], edgeCacheMax, rowCacheMax[i + 1]);
break;
case 9:
AddTriangle(rowCacheMin[i], edgeCacheMin, rowCacheMin[i + 1]);
AddTriangle(rowCacheMax[i + 2], edgeCacheMax, rowCacheMax[i + 1]);
break;
case 10:
AddQuad(rowCacheMin[i + 1], rowCacheMax[i + 1], rowCacheMax[i + 2], rowCacheMin[i + 2]);
break;
case 11:
AddPentagon(
rowCacheMin[i + 2], rowCacheMin[i], edgeCacheMin, rowCacheMax[i + 1], rowCacheMax[i + 2]);
break;
case 12:
AddQuad(edgeCacheMin, rowCacheMax[i], rowCacheMax[i + 2], edgeCacheMax);
break;
case 13:
AddPentagon(
rowCacheMax[i], rowCacheMax[i + 2], edgeCacheMax, rowCacheMin[i + 1], rowCacheMin[i]);
break;
case 14:
AddPentagon(
rowCacheMax[i + 2],rowCacheMin[i + 2], rowCacheMin[i + 1], edgeCacheMin, rowCacheMax[i]);
break;
case 15:
AddQuad(rowCacheMin[i], rowCacheMax[i], rowCacheMax[i + 2], rowCacheMin[i + 2]);
break;
}
}
That should do it, your meshes should now contain considerably less vertices! You can now also
get rid of the old polygon methods that have vertex parameters.
Our new approach also allows for another simplification and optimization of Voxel. We used to
store edge crossing as vectors, because that way we could directly pass them into our polygon
methods. As we no longer do that, we can get rid of the redundant coordinates, storing only two
edge floats per vertex instead of four.
https://catlikecoding.com/unity/tutorials/marching-squares-2/ 7/19
2022/5/28 15:36 Marching Squares 2, a Unity C# Tutorial
public float xEdge, yEdge;
public Voxel () {}
The only places where VoxelGrid needs the edge positions are in CacheNextEdgeAndCorner and
CacheNextMiddleEdge. We have to update these methods so they construct the edge vertices
when needed, retrieving the missing coordinate from the voxel's position.
The previous section was useful, but didn't result in any visual change. This section is different,
we're going to work on edge crossings.
Until now we've fixed edge crossings to the midpoint between voxels, which produces very rigid
visuals. It's time we loosen up and calculate the actual edge intersections. But first, let's visualize
our stencils so we can better see what we're editing.
Visualizing Stencils
To show the stencils we need objects. Create a default cube for the square stencil and a default
cylinder for the circle stencil. The cylinder needs to be rotated 90 degrees around the X axis so it
https://catlikecoding.com/unity/tutorials/marching-squares-2/ 8/19
2022/5/28 15:36 Marching Squares 2, a Unity C# Tutorial
looks like a circle from our point of view. Remove the colliders from both of them, so they don't
interfere with the input detection.
Give them some semitransparent material so you can both see the stencil shape and the grid
while painting. I made a new Stencil material with the default Transparent / Diffuse shader and a
red color with alpha set to 127.
Make them children of Voxel Map and deactivate them so they're not visible. Then add a public
Transform array to VoxelMap and assign the shapes to them so they match the stencil options.
We should show the visualization of the current stencil whenever the cursor hovers over the
map, not only when drawing. To support this, swap and combine the if-statements in
VoxelMap.Update and activate or deactivate the visualization as needed.
To correctly position and scale the visualization, we have perform the same calculations as in
EditVoxels, so add those in. Because the calculation uses the bottom-left of the grid as its local
origin, we have to undo this offset after figuring out the voxel's position.
if (Input.GetMouseButton(0)) {
EditVoxels(transform.InverseTransformPoint(hitInfo.point));
}
center.x -= halfSize;
center.y -= halfSize;
visualization.localPosition = center;
https://catlikecoding.com/unity/tutorials/marching-squares-2/ 9/19
2022/5/28 15:36 Marching Squares 2, a Unity C# Tutorial
visualization.localScale = Vector3.one * ((radiusIndex + 0.5f) * voxelSize * 2f);
visualization.gameObject.SetActive(true);
Now we can see the stencil and can confirm that only the voxels inside its shape are affected.
The next step is to no longer snap the stencil to exact voxel positions when editing. Because you
might actually want to snap, let's make this optional. Add a configuration option and only snap
the stencil's position when this is desired.
if (Input.GetMouseButton(0)) {
EditVoxels(transform.InverseTransformPoint(hitInfo.point));
}
…
}
To snap or not.
Of course right now this only affects the stencil's visualization. To support this option for editing,
VoxelStencil needs to work with actual positions instead of voxel coordinates. This means that
we have to replace its integers with floats.
https://catlikecoding.com/unity/tutorials/marching-squares-2/ 10/19
2022/5/28 15:36 Marching Squares 2, a Unity C# Tutorial
this.fillType = fillType;
this.radius = radius;
}
Its Apply method now also needs to work with the actual position of a voxel. Let's adjust it so you
provide it with a voxel which it directly edits, instead of returning the new voxel state. To make
sure the voxel lies inside the square area, we now have to check whether the voxel's position lies
within the stencil bounds.
And VoxelStencilCirle needs to be adjusted as well. This stencil already performed an explicit
bounds check, it just needs to use floats now.
Now let's change EditVoxels so it uses the center that we already calculated in Update. We used
to add a one-voxel padding in the negative directions to make sure that gaps were updated
property. As we now also have to make sure that edge crossings are updated at the edge of the
stencil area, we have to add padding in the positive directions as well. Also note that we no
longer need to worry about a voxel offset in the loops when updating the stencil center.
https://catlikecoding.com/unity/tutorials/marching-squares-2/ 11/19
2022/5/28 15:36 Marching Squares 2, a Unity C# Tutorial
}
}
The final change is to VoxelGrid.Apply, which needs to figure out which voxels are covered.
Free editing.
Now we can edit without snapping, which results in new patterns, but it still doesn't look much
like the stencil visualization. So now we're going to figure out exact edge crossings. We'll do this
one step at a time.
Add a method to VoxelStencil to compute the horizontal crossing between two voxels. First
check whether the voxels are different, which means that there is a crossing. If so, try to find it.
How to calculate the exact intersection point depends on the stencil, so let's use a virtual method.
As the basic stencil is a square, first check whether edge is inside the vertical bounds of the
square. If not, the horizontal crossing isn't caused by this application of the stencil, it was already
there.
https://catlikecoding.com/unity/tutorials/marching-squares-2/ 12/19
2022/5/28 15:36 Marching Squares 2, a Unity C# Tutorial
Now we need to check which of the two voxels might lie inside the stencil's area. First consider
the case of the left voxel matching our fill type. This means that the edge might cross the right
side of the stencil. We should actually check this, because it could also be an old crossing that
lies somewhere to the right or left of our stencil. If we're sure that it's actually passing through our
stencil's border, we can set its exact position.
We have to consider a possible crossing on the left side the of the stencil as well.
if (xMin.state == fillType) {
if (xMin.position.x <= XEnd && xMax.position.x >= XEnd) {
xMin.xEdge = XEnd;
}
}
else if (xMax.state == fillType) {
if (xMin.position.x <= XStart && xMax.position.x >= XStart) {
xMin.xEdge = XStart;
}
}
https://catlikecoding.com/unity/tutorials/marching-squares-2/ 13/19
2022/5/28 15:36 Marching Squares 2, a Unity C# Tutorial
Now VoxelGrid needs to actually set the edge crossing. This needs to be done after the stencil
has been applied to the voxels.
We need to increase the calculated voxel area by one step in each direction to cover all potential
edges. Also check whether we need to pass gaps and if we have to cover the last vertical row,
because all these cases require special attention.
private void SetCrossings (VoxelStencil stencil, int xStart, int xEnd, int yStart, int yEnd) {
bool crossHorizontalGap = false;
bool lastVerticalRow = false;
bool crossVerticalGap = false;
if (xStart > 0) {
xStart -= 1;
}
if (xEnd == resolution - 1) {
xEnd -= 1;
crossHorizontalGap = xNeighbor != null;
}
if (yStart > 0) {
yStart -= 1;
}
if (yEnd == resolution - 1) {
yEnd -= 1;
lastVerticalRow = true;
crossVerticalGap = yNeighbor != null;
}
}
Then loop over all cells, setting their bottom horizontal edges.
Voxel a, b;
for (int y = yStart; y <= yEnd; y++) {
int i = y * resolution + xStart;
b = voxels[i];
for (int x = xStart; x <= xEnd; x++, i++) {
a = b;
b = voxels[i + 1];
stencil.SetHorizontalCrossing(a, b);
}
}
Check the left vertical edges as well, and the vertical rightmost edge at the end of each row.
https://catlikecoding.com/unity/tutorials/marching-squares-2/ 14/19
2022/5/28 15:36 Marching Squares 2, a Unity C# Tutorial
stencil.SetVerticalCrossing(a, voxels[i + resolution]);
}
stencil.SetVerticalCrossing(b, voxels[i + resolution]);
if (crossHorizontalGap) {
dummyX.BecomeXDummyOf(xNeighbor.voxels[y * resolution], gridSize);
stencil.SetHorizontalCrossing(b, dummyX);
}
}
And finally we have to do the same for the last vertical row, using a dummy to cross the vertical
gap is there's a neighbor.
if (includeLastVerticalRow) {
int i = voxels.Length - resolution + xStart;
b = voxels[i];
for (int x = xStart; x <= xEnd; x++, i++) {
a = b;
b = voxels[i + 1];
stencil.SetHorizontalCrossing(a, b);
if (crossVerticalGap) {
dummyY.BecomeYDummyOf(yNeighbor.voxels[x], gridSize);
stencil.SetVerticalCrossing(a, dummyY);
}
}
if (crossVerticalGap) {
dummyY.BecomeYDummyOf(yNeighbor.voxels[xEnd + 1], gridSize);
stencil.SetVerticalCrossing(b, dummyY);
}
if (crossHorizontalGap) {
dummyX.BecomeXDummyOf(xNeighbor.voxels[voxels.Length - resolution], gridSize);
stencil.SetHorizontalCrossing(b, dummyX);
}
}
Free crossings.
Now we get exact edge crossing, at least when using the square stencil. But you'll notice that
edge positions are always replaced when painting. It looks like the contour is pulled toward the
stencil, which is not intuitive. It makes more sense to only adjust edge positions when doing so
would increase the covered area, not when it would reduce it. To do so, we need to compare the
new edge position to the previous one.
Comparing with the current edge position only makes sense if there actually was an edge
crossing. So we need some way to indicate that there is no old edge data. As local positions are
https://catlikecoding.com/unity/tutorials/marching-squares-2/ 15/19
2022/5/28 15:36 Marching Squares 2, a Unity C# Tutorial
always positive inside a voxel grid, we could use a negative value for this purpose. We should
make sure the number stays negative even when offset by a dummy. Using the minimum
possible value for a float takes care of that.
As we start without edge crossings, each Voxel should initially have negative edge data.
xEdge = float.MinValue;
yEdge = float.MinValue;
}
As VoxelStencil knows how to figure out edge crossings, give it the responsibility of erasing old
edge data when it finds that there's no crossing.
Then it can check whether there's old edge data and if that should be replaced, before actually
storing the new position.
https://catlikecoding.com/unity/tutorials/marching-squares-2/ 16/19
2022/5/28 15:36 Marching Squares 2, a Unity C# Tutorial
Finally, VoxelStencilCircle needs its own crossing logic, otherwise it produces nonsensical
edges. That means we have to compute the intersection of a line and a circle, which fortunately
is easy because we're working with strictly horizontal and vertical lines. First consider the
horizontal right side.
https://catlikecoding.com/unity/tutorials/marching-squares-2/ 17/19
2022/5/28 15:36 Marching Squares 2, a Unity C# Tutorial
if (yMin.yEdge == float.MinValue || yMin.yEdge > y) {
yMin.yEdge = y;
}
}
}
}
Our circle stencil finally produces something that looks like a circle! Of course a larger radius
results in a better approximation, as that covers more voxels and edges. Unfortunately squares
still don't have sharp corners, but we'll take care of that in the next tutorial.
Downloads
marching-squares-2-01.unitypackage
The project after Reusing Vertices.
marching-squares-2-finished.unitypackage
The finished project.
The theoretical worst cases would be if either the grid is empty or only the four corner voxels of the grid were filled. In
those cases there is no vertex reduction at all, but they have only zero and twelve vertices in total anyway.
A nontrivial bad case is a grid with lots of isolated filled voxels. A voxel on the edge of the grid still has no reduction, but
there will be at most four of those. Isolated voxels along the grid edge can be reduced from six to four vertices, which is
a 33% reduction. If the voxel doesn't touch the edge it goes from twelve to five vertices, which is a 58% reduction. 33%
is already a significant reduction, and as there are typically more internal than edge voxels in a grid we can expect the
final savings to be around 50%, which is very good.
If we start considering clumps of filled voxels the results get even better. An isolated 3x3 block of filled voxels that
doesn't touch the grid edge has twelve edge vertices and nine voxel vertices, a total of 21. Without sharing we would
end up with 60 vertices, so the reduction is 65%. A 5x5 block is reduced from 140 to 45 vertices, which is 67%.
So let's use a conservative estimate that vertex reuse cuts the amount of vertices in half, which benefits both memory,
the CPU, and the GPU. This is quite significant, especially as we don't have to store much extra data to make it
possible.
As the voxel grids are two-dimensional, their size is equal to the voxel resolution squared. Hence, they scale
quadratically with the voxel resolution. That's why you cannot use a very high resolution. If we were to cache all vertices
at once, the cache would also scale quadratically with the voxel resolution.
Deciding to only cache enough data to work on one cell row at a time allows us to use a one-dimensional cache, hence
it scales linearly. As the voxel resolution increases, the cache size will quickly become insignificant compared to the
voxel data.
No, it is not needed to give each grid its own cache data. You could get away with storing the caches once as static
data. This would no longer make the cache scale with the amount of grids, making its size practically a non-issue.
I didn't bother with this optimization because it's not needed at the scale of this tutorial. To properly do it, you'd have to
ensure that the shared cache is large enough to support the largest grid size that you're using, in case you have
https://catlikecoding.com/unity/tutorials/marching-squares-2/ 18/19
2022/5/28 15:36 Marching Squares 2, a Unity C# Tutorial
multiple voxels maps with different grid resolutions. Besides that, a shared cache wouldn't work if you were to use multi-
threading to triangulate multiple grids at the same time. It might seem like overkill to worry about those cases, but
they're good examples of things that will cause obscure bugs in some future version of your program.
As we remove two floats from the object, the short answer is that this optimization saves 8 bytes per voxel. But that
doesn't say much if we don't know the total size of the object. Unfortunately the memory layout of an object is up to the
compiler and depends on whether compiling for 32-bit or 64-bit systems. To get a general idea of size, let's assume
we're building for 32-bit. In that case we start with 8 bytes for the object header. The three vectors add 24 bytes and the
boolean adds another byte, so that's a total of 33 bytes. However, we can assume that the objects will be aligned to 4-
byte boundaries, so we end up with 36 bytes per object. Besides that the voxel grids must also store a reference to the
object, which effectively adds another 4 bytes somewhere else in memory, increasing the total to 40 bytes per voxel.
So we end up saving 8 bytes out of 40, reducing the total to 32 bytes. That's a 20% reduction, which is not bad at all.
Gaps can appear in thin lines when you happen to connect two voxels diagonally, because we decided to always
disconnect voxels for those ambiguous cell types. That we're now figuring out edge connections does not change this,
but it makes it more obvious that sometimes these voxel should actually have been connected. This is something that
we'll deal with in the next tutorial.
You could certainly use a boolean. The you have to add two additional boolean variables to Voxel. That might increase
the memory size of the object, but due to the 4-byte alignment you could probably store up to three extra boolean
variables without actually changing its size.
However, using a negative value to indicate a special case is also perfectly fine, as long as you know what you're doing.
In this case negative values are clearly special, as local positions cannot be negative. Typically -1 is used, but we
couldn't because a dummy offset could transform that into an invalid positive value.
© Catlike Coding
https://catlikecoding.com/unity/tutorials/marching-squares-2/ 19/19