top of page
Search
  • alexlpc2015

[Shader Notes] #07 - Wireframe

Hi and welcome to my Shader Notes Series!

This is a comprehensive project with notes about shaders of nearly every common effect included (implemented in Unity). I will explain the key points and theories behind them for you to understand them thoroughly and then enjoy creating your own shaders with proficiency.

Let's start learning together!


#07 - Wireframe

Difficulty: 6/10

Points covered:

  • Geometry Shader

  • Triangle Maths

Here we are creating a wireframe shader effect just like what you’ll see in a 3D modeling software or a wireframe unity scene. The basic theory of the shader is to calculate the smallest distance to a side, for each vertex in a triangle. To access the triangle data, we need to utilize the geometry shader.

The geometry shader step is between the vertex and fragment shaders in the graphics pipeline. It receives the processed triangle data from the vertex shader and then passes it to the fragment shader after rasterization.


Final effect:



Setup:

Create a new material and attach it to an object.

Prepare the properties and declare them in CGPROGRAM.

//Properties
_WireColor ("Wire Color", Color) = (0,0,0,1)
_FillColor ("Fill Color", Color) = (1,1,1,1)
_WireWidth ("Wire Width", Range(0.1,2)) = 0.2

Point #1. Preparing for the Geometry Shader

To include the geometry function in our shader, first declare its pragma just like the vertex & fragment shaders.

#pragma vertex vert
#pragma fragment frag
#pragma geometry geom

Instead of writing a v2f struct to pass data, we need to split it into two stages: v2g and g2f.

//Application -> vertex
struct appdata
{
    float4 vertex: POSITION;
    float2 uv: TEXCOORD0;
};

//vertex -> geometry
struct v2g
{
    float2 uv: TEXCOORD0;
    float4 vertex: SV_POSITION;
};

//geometry -> fragment
struct g2f
{
    float2 uv: TEXCOORD0;
    float4 vertex: SV_POSITION;
    float3 dist: NORMAL1;
};

Then, set up the vertex shader. Remember to return v2g, not v2f.

v2g vert(appdata v)
{
    v2g o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.uv = v.uv;
    return o;
}

Now create an empty geometry shader template. It looks like this.

[maxvertexcount(n)] specifies the number of vertices a “triangle” can have. You can add or delete vertices in geometry shaders!

IN[3] indicates the input vertices. They have the type v2g.

triStream indicates the output vertices. They have the type g2f.

[maxvertexcount(3)]
void geom(triangle v2g IN[3], inout TriangleStream < g2f > triStream)
{

}

Point #2. Calculating the Distance

Recall that our goal is to calculate the distance from every vertex to its opposite side. We can utilize the interpolated data later on to find the minimum distance from a fragment to the sides of its triangle.

A simple way to calculate the distance is to divide the area of the triangle by the length of a certain side. This way we can get the length of the line perpendicular to a side, which is exactly what we want.

//We are dealing with triangles, so the vertex count is 3
[maxvertexcount(3)]
void geom(triangle v2g IN[3], inout TriangleStream < g2f > triStream)
{
  //Convert the vertices to points in viewport coordinates
	float2 p0 = IN[0].vertex.xy / IN[0].vertex.w;
  float2 p1 = IN[1].vertex.xy / IN[1].vertex.w;
  float2 p2 = IN[2].vertex.xy / IN[2].vertex.w;

	//Side vectors
  float2 v0 = p2 - p1;
  float2 v1 = p2 - p0;
  float2 v2 = p1 - p0;

  //Area of triangle using the definition of the cross product
  float area = abs(v1.x * v2.y - v1.y * v2.x);

  //Initialize output
  g2f OUT;
  OUT.vertex = IN[0].vertex;
  OUT.uv = IN[0].uv;
  //Actually calculate the distance by dividing area by length
  OUT.dist = float3(area / length(v0), 0, 0);
  //Don't forget to append the vertex!
  triStream.Append(OUT);

  //Repeat for the other two vertices
  OUT.vertex = IN[1].vertex;
  OUT.uv = IN[1].uv;
  OUT.dist = float3(0, area / length(v1), 0);
  triStream.Append(OUT);

  OUT.vertex = IN[2].vertex;
  OUT.uv = IN[2].uv;
  OUT.dist = float3(0, 0, area / length(v2));
  triStream.Append(OUT);
}

In the fragment shader, calculate the minimum distance between a fragment and the sides of its triangle. If the value is small, meaning that the pixel is near the boundaries, we color it.

fixed4 frag(g2f i): SV_Target
{
    fixed4 col_Wire;
    float d = min(i.dist.x, min(i.dist.y, i.dist.z));
    col_Wire.rgb = d < _WireWidth * 0.003 / i.vertex.w ?_WireColor: _FillColor;
    col_Wire.a = 1;
    return col_Wire;
}

Now our wireframe shader is complete! Looks nice!


Full Code:

Shader "AlexLiu/WireFrame"
{
    Properties
    {
        _WireColor ("Wire Color", Color) = (0,0,0,1)
        _FillColor ("Fill Color", Color) = (1,1,1,1)
        _WireWidth ("Wire Width", Range(0.1,2)) = 0.2
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma geometry geom

            #include "UnityCG.cginc"

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

            struct v2g
            {
                float2 uv: TEXCOORD0;
                float4 vertex: SV_POSITION;
            };

            struct g2f
            {
                float2 uv: TEXCOORD0;
                float4 vertex: SV_POSITION;
                float3 dist: NORMAL1;
            };

            v2g vert(appdata v)
            {
                v2g o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }

            [maxvertexcount(3)]
             void geom(triangle v2g IN[3], inout TriangleStream < g2f > triStream)
            {
                float2 p0 = IN[0].vertex.xy / IN[0].vertex.w;
                float2 p1 = IN[1].vertex.xy / IN[1].vertex.w;
                float2 p2 = IN[2].vertex.xy / IN[2].vertex.w;

                float2 v0 = p2 - p1;
                float2 v1 = p2 - p0;
                float2 v2 = p1 - p0;
                float area = abs(v1.x * v2.y - v1.y * v2.x);

                g2f OUT;
                OUT.vertex = IN[0].vertex;
                OUT.uv = IN[0].uv;
                OUT.dist = float3(area / length(v0), 0, 0);
                triStream.Append(OUT);

                OUT.vertex = IN[1].vertex;
                OUT.uv = IN[1].uv;
                OUT.dist = float3(0, area / length(v1), 0);
                triStream.Append(OUT);

                OUT.vertex = IN[2].vertex;
                OUT.uv = IN[2].uv;
                OUT.dist = float3(0, 0, area / length(v2));
                triStream.Append(OUT);
            }

            fixed4 _WireColor;
            fixed4 _FillColor;
            float _WireWidth;        

            fixed4 frag(g2f i): SV_Target
            {
                fixed4 col_Wire;
                float d = min(i.dist.x, min(i.dist.y, i.dist.z));
                col_Wire.rgb = d < _WireWidth * 0.003 / i.vertex.w ?_WireColor: _FillColor;
                col_Wire.a = 1;
                return col_Wire;
            }
            ENDCG
        }
    }
}

Thank you for reading!

7 views0 comments

Recent Posts

See All
Post: Blog2_Post
bottom of page