Rendering Wireframe Vector Graphics in Unity

I’m a big sucker for glowing wireframe vector arcade style games like Atari’s Battlezone and Star Wars. Many years ago I started to write my own 3D Vector graphic engine in OpenGL and got quite far. Unfortunately I struggled to implement a decent collision engine, and keeping the engine up to with the multitude of OpenGL updates was challenging. Reluctantly I abandoned it, but hoped one day to return to it.

Recently restart my quest and I looked at the various 3D engines on the market. I finally I opted for Unity. I had many reservation as I found the UI and workflow very confusing, however I persevered. I’ve now been using unity for just under six weeks and things are coming together.

In the meantime I thought it could be useful to other to share how I managed to implement hidden line wireframe graphics in unity.

Basic Wireframe Set Up

To create the wireframe mesh I used the MeshTopology api. This was very quick to set up. First you need to create an array to store all the positions of the vertices in the model. Below are the vertices to create a tie fighter.

Vector3[] verts = new Vector3[] {
new Vector3(-2.17f, 2.437f, -1.407f),
new Vector3(-2.17f, 0f, -2.814f),
new Vector3(-2.17f, 0f, -2.814f),
new Vector3(-2.17f, -2.437f, -1.407f),
new Vector3(-2.17f, -2.437f, 1.407f),
new Vector3(-2.17f, -2.437f, -1.407f),
new Vector3(-2.17f, 0f, 2.814f),
new Vector3(-2.17f, -2.437f, 1.407f),
new Vector3(-2.17f, 0f, 2.814f),
new Vector3(-2.17f, 2.437f, 1.407f),
new Vector3(-2.17f, 2.437f, 1.407f),
new Vector3(-2.17f, 2.437f, -1.407f),
new Vector3(-2.17f, 0.272f, -0.157f),
new Vector3(-2.17f, 0.272f, 0.157f),
new Vector3(-2.17f, 0f, -0.314f),
new Vector3(-2.17f, 0.272f, -0.157f),
new Vector3(-2.17f, -0.272f, -0.157f),
new Vector3(-2.17f, 0f, -0.314f),
new Vector3(-2.17f, 0.272f, 0.157f),
new Vector3(-2.17f, 0f, 0.314f),
new Vector3(-2.17f, -0.272f, 0.157f),
new Vector3(-2.17f, 0f, 0.314f),
new Vector3(-2.17f, -0.272f, 0.157f),
new Vector3(-2.17f, -0.272f, -0.157f),
new Vector3(-2.17f, 2.437f, 1.407f),
new Vector3(-2.17f, 0.272f, 0.157f),
new Vector3(-2.17f, 2.437f, -1.407f),
new Vector3(-2.17f, 0.272f, -0.157f),
new Vector3(-2.17f, 0f, -2.814f),
new Vector3(-2.17f, 0f, -0.314f),
new Vector3(-2.17f, 0f, 2.814f),
new Vector3(-2.17f, 0f, 0.314f),
new Vector3(-2.17f, -2.437f, 1.407f),
new Vector3(-2.17f, -0.272f, 0.157f),
new Vector3(-2.17f, -2.437f, -1.407f),
new Vector3(-2.17f, -0.272f, -0.157f),
new Vector3(2.17f, 2.437f, 1.407f),
new Vector3(2.17f, 2.437f, -1.407f),
new Vector3(2.17f, 0f, -2.814f),
new Vector3(2.17f, 2.437f, -1.407f),
new Vector3(2.17f, 0f, 2.814f),
new Vector3(2.17f, 2.437f, 1.407f),
new Vector3(2.17f, 0f, 2.814f),
new Vector3(2.17f, -2.437f, 1.407f),
new Vector3(2.17f, -2.437f, 1.407f),
new Vector3(2.17f, -2.437f, -1.407f),
new Vector3(2.17f, -2.437f, -1.407f),
new Vector3(2.17f, 0f, -2.814f),
new Vector3(2.17f, 0.272f, -0.157f),
new Vector3(2.17f, 0f, -0.314f),
new Vector3(2.17f, 0.272f, 0.157f),
new Vector3(2.17f, 0.272f, -0.157f),
new Vector3(2.17f, 0f, -0.314f),
new Vector3(2.17f, -0.272f, -0.157f),
new Vector3(2.17f, 0.272f, 0.157f),
new Vector3(2.17f, 0f, 0.314f),
new Vector3(2.17f, -0.272f, 0.157f),
new Vector3(2.17f, 0f, 0.314f),
new Vector3(2.17f, -0.272f, -0.157f),
new Vector3(2.17f, -0.272f, 0.157f),
new Vector3(2.17f, -2.437f, -1.407f),
new Vector3(2.17f, -0.272f, -0.157f),
new Vector3(2.17f, 2.437f, 1.407f),
new Vector3(2.17f, 0.272f, 0.157f),
new Vector3(2.17f, 2.437f, -1.407f),
new Vector3(2.17f, 0.272f, -0.157f),
new Vector3(2.17f, 0f, -2.814f),
new Vector3(2.17f, 0f, -0.314f),
new Vector3(2.17f, -2.437f, 1.407f),
new Vector3(2.17f, -0.272f, 0.157f),
new Vector3(2.17f, 0f, 2.814f),
new Vector3(2.17f, 0f, 0.314f),
new Vector3(-0.347f, 0.492f, -0.764f),
new Vector3(-0.596f, -0.001f, -0.77f),
new Vector3(0.319f, 0.492f, -0.764f),
new Vector3(-0.347f, 0.492f, -0.764f),
new Vector3(0.319f, 0.492f, -0.764f),
new Vector3(0.569f, -0.001f, -0.77f),
new Vector3(0.319f, -0.495f, -0.764f),
new Vector3(0.569f, -0.001f, -0.77f),
new Vector3(-0.347f, -0.495f, -0.764f),
new Vector3(0.319f, -0.495f, -0.764f),
new Vector3(-0.347f, -0.495f, -0.764f),
new Vector3(-0.596f, -0.001f, -0.77f),
new Vector3(-0.347f, 0.492f, -0.764f),
new Vector3(-0.384f, 0.749f, -0.433f),
new Vector3(0.319f, 0.492f, -0.764f),
new Vector3(0.357f, 0.749f, -0.433f),
new Vector3(0.357f, 0.749f, -0.433f),
new Vector3(-0.384f, 0.749f, -0.433f),
new Vector3(-0.384f, -0.751f, -0.433f),
new Vector3(-0.347f, -0.495f, -0.764f),
new Vector3(-0.596f, -0.001f, -0.77f),
new Vector3(-0.889f, 0f, -0.314f),
new Vector3(0.319f, -0.495f, -0.764f),
new Vector3(0.357f, -0.751f, -0.433f),
new Vector3(-0.384f, -0.751f, -0.433f),
new Vector3(0.357f, -0.751f, -0.433f),
new Vector3(-0.384f, -0.751f, 0.433f),
new Vector3(-0.384f, -0.751f, -0.433f),
new Vector3(-0.384f, -0.751f, 0.433f),
new Vector3(0.357f, -0.751f, 0.433f),
new Vector3(0.357f, -0.751f, 0.433f),
new Vector3(0.357f, -0.751f, -0.433f),
new Vector3(0.865f, 0f, -0.314f),
new Vector3(0.569f, -0.001f, -0.77f),
new Vector3(0.865f, -0.272f, -0.157f),
new Vector3(0.865f, 0f, -0.314f),
new Vector3(0.357f, -0.751f, -0.433f),
new Vector3(0.865f, -0.272f, -0.157f),
new Vector3(-0.889f, -0.272f, -0.157f),
new Vector3(-0.889f, 0f, -0.314f),
new Vector3(-0.384f, -0.751f, -0.433f),
new Vector3(-0.889f, -0.272f, -0.157f),
new Vector3(-0.889f, 0f, -0.314f),
new Vector3(-0.889f, 0.272f, -0.157f),
new Vector3(-0.384f, 0.749f, 0.433f),
new Vector3(-0.384f, 0.749f, -0.433f),
new Vector3(0.357f, 0.749f, -0.433f),
new Vector3(0.357f, 0.749f, 0.433f),
new Vector3(0.357f, 0.749f, 0.433f),
new Vector3(-0.384f, 0.749f, 0.433f),
new Vector3(0.865f, 0.272f, -0.157f),
new Vector3(0.357f, 0.749f, -0.433f),
new Vector3(0.865f, 0.272f, 0.157f),
new Vector3(0.357f, 0.749f, 0.433f),
new Vector3(0.865f, 0f, -0.314f),
new Vector3(0.865f, 0.272f, -0.157f),
new Vector3(0.865f, 0.272f, -0.157f),
new Vector3(0.865f, 0.272f, 0.157f),
new Vector3(-0.384f, 0.749f, -0.433f),
new Vector3(-0.889f, 0.272f, -0.157f),
new Vector3(-0.384f, 0.749f, 0.433f),
new Vector3(-0.889f, 0.272f, 0.157f),
new Vector3(-0.888f, 0.272f, -0.158f),
new Vector3(-0.889f, 0.272f, 0.157f),
new Vector3(2.17f, 0.272f, -0.157f),
new Vector3(0.865f, 0.272f, -0.157f),
new Vector3(2.17f, 0f, -0.314f),
new Vector3(0.865f, 0f, -0.314f),
new Vector3(2.17f, -0.272f, -0.157f),
new Vector3(0.865f, -0.272f, -0.157f),
new Vector3(2.17f, 0.272f, 0.157f),
new Vector3(0.865f, 0.272f, 0.157f),
new Vector3(-2.17f, 0.272f, -0.157f),
new Vector3(-0.888f, 0.272f, -0.158f),
new Vector3(-2.17f, 0f, -0.314f),
new Vector3(-0.889f, 0f, -0.314f),
new Vector3(-2.17f, -0.272f, -0.157f),
new Vector3(-0.889f, -0.272f, -0.157f),
new Vector3(-2.17f, 0.272f, 0.157f),
new Vector3(-0.889f, 0.272f, 0.157f),
new Vector3(0.357f, -0.001f, 0.866f),
new Vector3(0.357f, 0.749f, 0.433f),
new Vector3(-0.384f, -0.001f, 0.866f),
new Vector3(-0.384f, 0.749f, 0.433f),
new Vector3(0.865f, 0f, 0.314f),
new Vector3(0.865f, 0.272f, 0.157f),
new Vector3(2.17f, 0f, 0.314f),
new Vector3(0.865f, 0f, 0.314f),
new Vector3(0.865f, -0.272f, 0.157f),
new Vector3(0.865f, 0f, 0.314f),
new Vector3(2.17f, -0.272f, 0.157f),
new Vector3(0.865f, -0.272f, 0.157f),
new Vector3(0.865f, 0f, 0.314f),
new Vector3(0.357f, -0.001f, 0.866f),
new Vector3(-0.384f, -0.001f, 0.866f),
new Vector3(-0.889f, 0f, 0.314f),
new Vector3(0.357f, -0.001f, 0.866f),
new Vector3(-0.384f, -0.001f, 0.866f),
new Vector3(0.357f, -0.751f, 0.433f),
new Vector3(0.357f, -0.001f, 0.866f),
new Vector3(-0.384f, -0.751f, 0.433f),
new Vector3(-0.384f, -0.001f, 0.866f),
new Vector3(0.865f, -0.272f, 0.157f),
new Vector3(0.357f, -0.751f, 0.433f),
new Vector3(-0.384f, -0.751f, 0.433f),
new Vector3(-0.889f, -0.272f, 0.157f),
new Vector3(-0.889f, 0.272f, 0.141f),
new Vector3(-0.889f, 0f, 0.314f),
new Vector3(-2.17f, 0f, 0.314f),
new Vector3(-0.889f, 0f, 0.314f),
new Vector3(-2.17f, -0.272f, 0.157f),
new Vector3(-0.889f, -0.272f, 0.157f),
new Vector3(-0.889f, -0.272f, 0.157f),
new Vector3(-0.889f, 0f, 0.314f),
new Vector3(0.865f, -0.272f, 0.157f),
new Vector3(0.865f, -0.272f, -0.157f),
new Vector3(-0.889f, -0.272f, 0.157f),
new Vector3(-0.889f, -0.272f, -0.157f) };

Then you need to create an array that describes how these vertices are linked together.

// INDICES
int[] indicesForLineStrip = new int[verts.Length];

// link all the verts together sequentially
for (int i = 0; i < verts.Length; i++)
{
      indicesForLineStrip[i] = i;
}

The you need array to store the colour information for each line. In my case it was a simple case of marking each line green.

// COLOURS
Color[] mcolors = new Color[verts.Length];

for (int i = 0; i < verts.Length; i++)
{
      mcolors[i] = Color.green;
};

Finally we need to pull all these definitions together into a mesh.

// MESH - Assign verts, indices and colours
mesh = new Mesh
{
vertices = verts,
colors = mcolors
};
mesh.SetIndices(indicesForLineStrip, MeshTopology.Lines, 0);
mesh.RecalculateBounds();

Below is the result of the displaying the mesh (with a slight bloom shader for the glow).

I was pleased with the results and the performance was very good. I happily have a thousand tie fighters moving around at over a 100 fps.

Hidden Line Removal

For my games I will sometimes want to render hidden wireframe graphics. The easiest way to do this is to actually render 2 meshes. The first is solid black version of the mesh with all the faces filled in using the unlit material shader in Unity. Then over the top of that render the wireframe mesh from above. This way the black mesh will obscure the lines which should not be shown. The challenge I ran into was that my model had single faces that needed to be visible from both side (the hexagon side wings). Below you will that wings are only solid on one side. This is because the black mesh was being back faced culled.

Another problem is that the wireframes lines aren’t as thick as they should be and sometimes fade in and out. This is because both meshes are on the same Z plane i.e. they are occupying exactly the same position in world space. Therefore the GPU doesn’t know which pixel from which mesh to render first. This is known as Z Buffer Fighting.

Shader Improvement To Solve Rendering Issue

To solve this problem I needed to update the unlit shader in unity. The key changes were to first disable the back face culling using the CULL OFF tag. Then to resolve the Z buffer fighting, I added the Offset 1 , 1 tag. This offsets the z depth when rendering i.e. ensures that the rendering of material has a higher Z value. This means the black mesh is now rendered slightly behind the wireframe mesh.

Shader "Unlit/2_sided_unlit"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "black" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100
    CULL OFF
    Offset 1, 1

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}

Below we can see that the lines are now rendering at the full thickness and the z buffer fighting has been removed.

Alternative Methods

During my journey to build my wireframe renderer I did experiment with several other assets. I tried Vectosity which was excellent, but I simply couldn’t get the performance or look I wanted. This could well be down to my lack of experience with Unity. I would still highly recommend it though.

I also took a look at several shaders which took meshes and rendered them as a wireframe. The challenge I found with these is that I they often rendered every triangle / line that the mesh had. Plus I had no control over the colours of each line. So while these were fast again I could not control the output enough needed for my games.

1 thought on “Rendering Wireframe Vector Graphics in Unity”

Leave a reply

Your email address will not be published. Required fields are marked *