Alex Mason

Softwarrister, friend to animals, beloved by all

Dev log #6 – Triangles

It’s time to drop the simple capsule shapes and build something more chunky and rocky, while retaining some elegance and regularity. That means I’m drawing my own polygons. There’s no way to make an arbitrary polygon sprite in Unity, so I turned to the 3D mesh resource.

I started by downloading Blender to see if I could easily put some vertices together and export a mesh file. I tried to learn Blender through tutorials about five years ago, but gave up for numerous reasons. This time was no different. I don’t like to wrestle with analog hand movements to achieve a result, primarily. I also don’t have much artistic vision, in the literal sense. Geometry was the hardest math class for me in high school, once we got to measuring secants and hats on circles. I’m a verbal, functional thinker, so my visuals have to come from that foundation.

I decided to download a free trial of Matlab, which I haven’t used since my avionics job about 8 years ago. I could use Python’s matplotlib package, and I probably will when the trial period ends, but I might need Matlab chops if I get back into the embedded industry. All I remembered was that there was something weird about indexing arrays and matrices. Not only does it use 1-based indexing to fit math notation, but it also uses parens instead of square brackets.

Vertices

Anyway, I started with a function to produce the vertices for a regular N-sided polygon and display it. the polyshape(x,y) function takes the x and y coordinates as separate column vectors, and a simple call to plot(pshape) puts it on the screen. The vertices of a regular polygon are evenly spaced along a circle of a given radius. The angular step between vertices is a full rotation (2PI radians) divided by the number of sides of the whole polygon.

function [x,y] = reg_vertices(order, num_verts, radius, rotation)
x = zeros([1, num_verts]);
y = zeros([1, num_verts]);
angle_step = 2*pi/order;
for i = 1:num_verts+1
    angle = angle_step*(i-1) + rotation;
    x(i) = cos(angle)*radius;
    y(i) = sin(angle)*radius;
end
end
The plot of a 12-sided regular polygon with radius 1

The above code has an argument order, which is the number of sides of the regular polygon we’re drawing from, while the num_verts parameter allows us to produce only some of the vertices, which we can then insert into another list of vertices to produce a component shape. In this case, i want to start with squares and replace one side with a half-polygon cap, or replace one vertex with a quarter-polygon corner.

BY stepping my angle from zero, I’m naturally producing vertices in a counter-clockwise order, so when I embed them in a list with square vertices, I want to keep that ordering. The result is a messy but edifying use of matlab’s range syntax.

function [x,y] = corner_piece(order, corner_index)
% order is the number of sides of the regular polygon the corner vertices
% draw from. It needs to be a multiple of 4.
% corner_index determines the quadrant the corner belongs to
num_verts = order/4;
radius = 1;
rotation = (corner_index-1)*pi/2;
[corner_x, corner_y] = reg_vertices(order, num_verts, radius, rotation);
% quad in counter-clockwise order
x = radius*[1 -1 -1 1];
y = radius*[1 1 -1 -1];
pre_corner = max(corner_index-1, 1);
x = [x(1:pre_corner) corner_x x(corner_index+1:end)];
y = [y(1:pre_corner) corner_y y(corner_index+1:end)];
if corner_index == 1
    x = x(2:end);
    y = y(2:end);
end
end
A corner piece with order 12 and corner index 3

To keep things simple, I’ve decided to only use values of order that are multiples of 4. The cap piece function is similar, but removes 2 of the quad vertices.

From here, the next step is to put the pieces together. I want to specify an orthogonally connected group of cells through a matrix of 1s and 0s, and compose the rounded corners and caps in a natural way. The rules bear echos of a cellular automaton.

  • A cell adjacent to one other cell has a poly-cap in the opposite direction.
  • a cell adjacent to two other cells has a poly-corner opposite the corner where the adjacent cells meet.
  • A cell adjacent to three or four cells is a square.

Each cell has a 2×2 span, fitting a quad of (1,1), (-1,1), (-1,-1), (1,-1). This makes it easy to use caps and corners with radius 1. I’ll spare you the code of the “rock builder” with all its boundary conditions and offsets. The result is that I can plot the following with just this matrix and order as inputs.

m =     [0     0     1     0;

1 1 1 0;
1 0 1 1;];
build_rock(m, 12);

Now how do I get Unity to show the shape? I inspected the quad mesh

Here I see there are 4 vertices with some information, and 6 indices for 2 triangles. Luckily, I know what this means thanks to an OpenGL tutorial. Whatever graphics library your engine is built on, there will be a way to register vertex data with a buffer that goes into the GPU. The buffer can be associated with a shader program, and there are tools for specifying an arbitrary format for the data, associating each chunk of bytes to an input to the vertex shader. If you just tell the library to draw an array of vertices on its own, three at a time will be taken to represent a triangle. However, a vertex at a given point is almost always used for multiple triangles, so to save space on the vertex buffer, we can tell the library to draw from the element buffer, which is a list of vertex indices, again taken three at a time to form triangles. Therefore I need to generate these indices in addition to my X and Y coordinates.

Coming up with triangles is easy because Each piece of my rock is convex. I didn’t prove this mathematically, but intuition and a bit of testing tells me that I can “fan out” triangles from any one point of a convex shape without trouble. to see why the strategy breaks if the shape isn’t convex, Try moving a vertex or two to the center.

This makes The code for triangle data very simple.

tri = [];
for k = 2:num_verts-1
    tri = [tri k (k+1) 1];
end

There’s a problem with the above code that we’ll address later. But First, we need to get this information into a mesh component. Unity provides a helpful example. the important part is that you can call SetVertices(Vector3[]) and SetTriangles(int[]) on a mesh, in that order. I wrote my data to a simple text file like so:

lines = [strjoin(string(x), ", ") strjoin(string(y), ", ") strjoin(string(tri), ", ")];
writelines(lines, "C:\Users\my\path\poly1.txt")

To read the file in C#, LINQ proved quite handy in the code below. There might be a better way to do this, but I’m sated. I also plan to use a texture, so the last line essentially treats the texture as a square in local space and masks it with the mesh. Some readers may be rightfully baffled with the symbol uv. I believe it comes from the language of substitution in caclulus and coordinate transforms, because we’re creating a piecewise linear mapping between texture space and world space.

        string[] lines = File.ReadAllLines(filename);
        float[] x_coord = (from x in lines[0].Split(", ") select float.Parse(x)).ToArray();
        float[] y_coord = (from y in lines[1].Split(", ") select float.Parse(y)).ToArray();
        Vector3[] vertices = x_coord.Zip(y_coord, (x, y) => (new Vector3(x, y, 0f))).ToArray();
        mesh.vertices = vertices;
        int[] triangles = (from index in lines[2].Split(", ") select int.Parse(index)).ToArray();
        mesh.triangles = triangles;
        mesh.uv = x_coord.Zip(y_coord, (x, y) => (new Vector2(x / texture_length, y / texture_length))).ToArray();

As foreshadowed, nothing shows up at first, until I rotate the mesh 180 degrees about the y-axis.

This is because I was writing triangles in counter-clockwise order, and Unity takes that order to mean the triangle is facing away from my camera, by default. It’s explained here, under the section titled “Winding Order.”

The order of the vertices in each group in the index array is called the winding order. Unity uses winding order to determine whether a face is front-facing or back-facing, and in turn whether it should render a face or cull it (exclude it from rendering). By default, Unity renders front-facing polygons and culls back-facing polygons. Unity uses a clockwise winding order, which means that Unity considers any face where the indices connect in a clockwise direction is front facing.

An easy fix. All I had to do was reverse the first two indices when writing the triangle array.

tri = [];
for k = 2:num_verts-1
    tri = [tri (k+1) k 1];
end

With a few more tweaks to the translation, I had my new block shape in the right place. The next task was to build the collider.

Colliders

Unfortunately, I can’t use a mesh collider with 2D physics. The first option I considered was CUstomCollider2D. its SetCustomShapes function seemed perfect for adding my generated pieces as an aggregate, without having to work out an outline path for the whole rock. It was simple enough to add piece_sizes to my matlab code and write it to a fourth line in my little file. However, I couldn’t create the polygon shapes for the group, because this particular shape class can’t have more than 8 vertices. They call it “a balance between flexibility and performance,” but I’m skeptical.

PolygonCollider2D places no limit on the number of paths I can add, nor the number of points on those paths. Is this not subject to the same performance concern? Maybe a Unity Engineer will explain the unique qualities of the PhysicsShape2D one day. I could just make a PhysicsShapeGroup2D with all the triangles I specified for the mesh for the custom collider, but decided that the PolygonCollider2D was the appropriate way to go.

As mentioned earlier, the fourth line of my rock geometry file is a sequence showing the number of vertices in each shape. Therefore I can just copy them from the vertex list, chunk by chunk, into the polygon collider. It looks like this:

        int vertexCursor = 0;
        int polyIndex = 0;
        collider.pathCount = shapeLengths.Length;
        foreach (int length in shapeLengths) {
            var polySpan = new List<Vector2>(vertices2D[vertexCursor..(vertexCursor+length)]);
            collider.SetPath(polyIndex, polySpan);
            vertexCursor += length;
            ++polyIndex;
        }

And there we have it! I don’t know how I want to place the number. I’ll probably keep it as instrumentation and represent destructible health thhrough color and texture.

Low framerate loom recording of my polygon collider matching the shape of my mesh.

Next time: shaders, and perhaps uploading better recordings to YouTube

Cover art: The Louvre, minus the obvious. Credit: Shvets Anna. “Photo of people near Water FOuntain” via Pexels.