Rendering the Static Icosphere in Unity Gaming Engine

This post builds on the concepts contained in the previous post: “Recursion of the Icosphere and Maintaining Adjacency.”

Why are we using Unity?

PISES is rendered, for the time being, with the Unity Gaming Engine. I selected Unity about a year ago, when I was asked unexpectedly to demo PISES before it had any sort of visual system.

I chose Unity because, at the time, it seemed like it had a more gentle learning curve than other gaming engines. I needed something simple and straightforward that I could dig into quickly in order to punch out a reasonable proof of concept for the demo which I quite rashly had agreed to. Whether or not Unity was actually the Gaming Engine most suited for this task, I’m still not really sure.

A year later, I believe there may be many better options for PISES. Unity has both its niceties and its shortcomings. The good news is that I have been very careful throughout PISES’ development to keep its backend data systems and its Unity rendering systems totally airgapped, should they ever need to be separated. For now, I will continue forward in Unity, and if there is ever a drastic change in PISES’ funding and staffing situation (one can dream), it won’t be too difficult to break up with Unity.

Sidequest: Purge the Ancestors

In my last post, the constructor method for the Abstract Icosphere called the (quite grim sounding) method, “purgeAncestry(),” which I promised we would explain later. That time is now.

It turns out that creating this Unity mesh will be mostly a battle of performance. Later, when we get into the dynamic, scaling properties of our mesh, every performance boost we can get will count. Cultivating a few thousand triangles that we do not need will become a noticeable dead-weight.

If we choose to begin with a starting Icosphere of recursive depth N, then we do not need to retain knowledge of any ancestors of recursive depth <N. If our Icosphere starts at N, it may recurse more deeply than N, and we must retain all ancestral knowledge for depth N and beyond, but the ancestral knowledge of <N is no longer necessary.

purgeAncestry() is a simple method that we only need run only once, at the very end of startup.

// This purges any triangles which are not currently visible, freeing up memory.
// This should only be called at the completion of construction, 
// when the icosphere is of a UNIFORM RECURSIVE DEPTH.
// All hell will break loose if this is called after asymmetric recursion.
private void purgeAncestry()
{
    // Temporary list to avoid concurrent modification
    List<int> facesToPurge = new List<int>();

    foreach (Triangle t in faces.Values)
    {
        if (!t.visible)
        {
            facesToPurge.Add(t.uniqueIndex);
        }
    }

    foreach (int index in facesToPurge)
    {
        faces.Remove(index);
    }
}

Building a Mesh in Unity

A Unity Mesh, much like an Icosphere, is very conveniently defined by its vertices and triangles. The mesh has an abundance of fields and detail, but for our preliminary purposes, we are only interested in three of them:

  1. vertices, an array of Vector3 representing all vertex positions.
  2. triangles, an array containing indices into the vertex array. Every three entries in triangles represents the three vertices of a mesh triangle.
  3. uv, an array of Vector2 which indicate the texture data for a particular triangle of the mesh.

The Vertex and Triangle Arrays

The relationship between these three arrays is not very intuitive. vertices is simple enough: it is an array of all vertices, represented by Vector3.

Every entry in triangles represents an index of vertices. Every triad of adjacent entries in triangles represents a triangle of the mesh.

So, to define a singular, unit equilateral triangle, our arrays would look like this:

Now, imagine we wish to define a second equilateral triangle, rotated 180 degrees, which shares two vertices with triangle 1. Our arrays would look like this:

As you can see, vertices can be re-used between different triangles. Additionally, the ordering of each triangle’s vertices is of great importance.

For a triangle to be visible, its vertices must be clockwise, relative to the observer. This means that every triangle has a “front” and a “back.” When viewed from its back (with the vertices arranged counter-clockwise relative to the observer), the triangle is not rendered by Unity.

The concept behind this is that, if the mesh defines a three-dimensional, closed polyhedron, it would be a waste of resources to render its interior. Therefore, all triangle’s “front” faces should be facing outward.

Luckily for us, this is exactly how we specified the vertices of the Triangle objects in our AbstractIcosphere.

The UV Array

The uv array is an array of Vector2s which indicate a (u, v) coordinate inside a flat image. We use (u, v) coordinates to avoid confusion with the global (x, y) coordinates that the world-space is defined in.

Each entry within the uv array directly corresponds with the same entry in the triangle array. Therefore, every triad of entries in the uv array defines a closed triangle – inside the flat image.

This triangle inside the flat image will be projected on to the corresponding triangle on the surface of the mesh.

Since a mesh in Unity can only have one texture, it is therefore common practice to create a “texture atlas:” a single image file containing all of the different triangular images you might ever need to project on to your mesh.

Let us continue with our above example, ascribing t1 and t2 textures.

For our purposes, instead of pictures from Alien, we will simply use two different textures: a grey triangle for our “unobserved” faces and a slightly less grey triangle for our “observed” faces. What constitutes an “observed” and “unobserved” triangle will be explained later.

Setup Inside the Unity Editor

  1. Create an empty GameObject in the Unity Editor (Let us name it “Icosphere”).
  2. In the GameObject’s inspector, Add a Script Component (Let us name it “IcosphereGenerator.cs”).
  3. Outside of Unity, create and save an image to use as your Triangle Atlas – I used GIMP. Write down the pixel coordinates of each triangle’s vertices in your image.
  4. Back in the Unity Editor, create a new Material. Let us name it “Triangle Atlas.”
  5. Set the Material’s Albedo to the image you just created.
  6. Open the Inspector of your “Icosphere” GameObject. Replace the material with Triangle Atlas.
  7. In the inspector, open the Mesh Renderer. Drop down its Materials. Set size to 1 and set Element 0 to “Triangle Atlas.”
  8. Open up “IcosphereGenerator.cs” in your text editor: it’s time to code.

Harvesting Mesh Data from the Abstract Icosphere

We now have all of the information we need to harvest our mesh data from the Abstract Icosphere.

We will begin with a simple Awake() method, which Unity will run before all else on program start. This awake method will initialize the icosphere, acquisition the Mesh from the Game Object to which this script is attached, initialize a few other variables which we’ll come to later, and call our heavier methods, HarvestMeshData and CreateMesh.

void Awake()
{
    // Generate Icosphere
    icosphere = new AbstractIcosphere(startingRecursiveDepth);

    // Obtain Mesh
    mesh = GetComponent<MeshFilter>().mesh;

    // Initialize Observation Ring
    observationGroup = new HashSet<int>();

    // Set Scale
    scale = 500;

    // Harvest data & build mesh
    HarvestMeshData();
    CreateMesh();
}

Now let’s dive into HarvestMeshData. Here, we will comb our AbstractIcosphere for the information required to construct our mesh.

// TRIANGLE ATLAS LOCATIONS
Vector2 atlas_unobserved_triangle_a = new Vector2(0.0f        , 3222f / 4000f);
Vector2 atlas_unobserved_triangle_b = new Vector2(449f / 4000f, 1.0f         );
Vector2 atlas_unobserved_triangle_c = new Vector2(898f / 4000f, 3222f / 4000f);

Vector2 atlas_observed_triangle_a = new Vector2( 898f / 4000f, 3222f / 4000f);
Vector2 atlas_observed_triangle_b = new Vector2(1347f / 4000f, 1.0f         );
Vector2 atlas_observed_triangle_c = new Vector2(1795f / 4000f, 3222f / 4000f);

void HarvestMeshData()
{
    // Initialize collections. 
    vertices = new List<Vector3>();
    triangles = new List<int>();
    UV = new List<Vector2>();

    // Initialize Mappings.
    meshToAbstractIcosphereMap = new Dictionary<int, int>();
    abstractIcosphereToUvMap = new Dictionary<int, int>();

    // Initialize Counters
    int currentMeshTriange = 0;

    // For each triangle in the Icosphere,
    foreach(Triangle abstractTriangle in icosphere.faces.Values)
    {
        // If the triangle is not visible, do not render it.
        if (!abstractTriangle.visible)
        {
            continue;
        }

        // Add the vertices to the mesh.  
        vertices.Add(scale * new Vector3(abstractTriangle.v1.X, abstractTriangle.v1.Y, abstractTriangle.v1.Z));
        vertices.Add(scale * new Vector3(abstractTriangle.v2.X, abstractTriangle.v2.Y, abstractTriangle.v2.Z));
        vertices.Add(scale * new Vector3(abstractTriangle.v3.X, abstractTriangle.v3.Y, abstractTriangle.v3.Z));

        // Maintain a mapping of mesh triangle indices to abstract triangle indices. 
        meshToAbstractIcosphereMap.Add(currentMeshTriange, abstractTriangle.uniqueIndex);

        // Create the mesh triangle.
        triangles.Add((currentMeshTriange*3));
        triangles.Add((currentMeshTriange*3)+1);
        triangles.Add((currentMeshTriange*3)+2);

        // Create a mapping from the abstract triangle index to the UV index.
        abstractIcosphereToUvMap.Add(abstractTriangle.uniqueIndex, UV.Count);

        // Set UVs, maintaining track of which triangle corresponds to which UV index.
        UV.Add(atlas_unobserved_triangle_a);
        UV.Add(atlas_unobserved_triangle_b);
        UV.Add(atlas_unobserved_triangle_c);

        currentMeshTriange++;
    }
}

Notice that in the course of this method, we maintain two sets of mappings:

  1. A mapping from the start of each triad in triangles to the correspondent abstract triangle index in AbstractIcosphere
  2. A mapping from the abstract triangle indices of AbstractIcosphere to the start of their correspondent triads in the UV array

We will not use these mappings right now, but they will become important later.

Now that we have harvested all of the mesh data, we must physically create the mesh in CreateMesh().

void CreateMesh()
{
    mesh.Clear();
    mesh.vertices = vertices.ToArray();
    mesh.triangles = triangles.ToArray();
    mesh.uv = UV.ToArray();
    GetComponent<MeshCollider>().sharedMesh = mesh;
}

In the last line of this method, we correspond the GameObject’s MeshCollider with its Mesh. We will rely on this collider later.

At this point, we will have visualized our AbstractIcosphere of a uniform recursive depth. Here is the Icosphere with startingRecursiveDepth = 4.

Next Steps

This Icosphere is not yet interactive. Ultimately, we want the icosphere to recurse when the observer moves below a certain threshold of distance. Additionally, we only want to recurse the area directly beneath the observer – not the entire sphere.

In the next post, we will solve the problem of discerning the region of observation. What might at first seem a simple task turns out to be the most challenging problem we have faced so far.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s