Is wireframe effect possible without geometry shader?

I am trying to write a shader that highlight the edge between each pair of vertices, much like the UCLA Wireframe Shader.

All implementations I came across require geometry shader support. I am wondering if this is possible with just vertex and fragment shaders?

(Of course, I can use texture to workaround it; but I am trying to avoid create custom textures for each model that I need this effect. On the other hand, I could just use geometry shader but it presents quite a problem when porting to iOS, which lacks support.)

I have also asked this on gamedev.stackexchange.com

You can using barycentric coordinates data for creating the effect and don’t need geometry shader but need adding the barycentric data in the mesh. You can read the theory here.
http://codeflow.org/entries/2012/aug/02/easy-wireframe-display-with-barycentric-coordinates/
I have made one selling in asset store.

https://www.assetstore.unity3d.com/en/#!/content/68384

And more alternative you can found in the stoe.
https://www.assetstore.unity3d.com/en/#!/content/18794
https://www.assetstore.unity3d.com/en/#!/content/67386

Alternatively, I have create a shader which using grid to mimic the wireframe effect (Under review in assets store). This one is drawing a grid on the surface with fix spacing. No geometry shader and no need to edit the mesh.
_*http://www.digicrafts.com.hk/unity3d/easy-wireframe-grid*_
The principal is getting the vertex position and divide it to fixed spacing value. So, you can get a checker liked uv. And use the color different to draw the wireframe.

1 Like

Interesting, about barycentric data, I wonder if it’s possible to set them on uv2 instead of color32?

I am using color32 for vertex coloring, so it’s quite important for me to preserve them.

Yes, any uv or color slots should be ok. You may use uv4 if you have other uv.

Wireframe SeeThrough Shader

Shader "Custom/WireframeSeeThrough_Geom"
{
    Properties
    {
        _WireColor("Wireframe Color", Color) = (1,1,1,1)
        _WireWidth("Wireframe Width", Float) = 1.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100
        Conservative True

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

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2g
            {
                float4 vertex : SV_POSITION;
            };

            float4 _WireColor;
            float _WireWidth;

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

            [maxvertexcount(18)]
            void geom(triangle appdata vertices[3], inout TriangleStream<v2g> triStream)
            {
                v2g OUT;
                float3 faceNormal = cross(vertices[1].vertex.xyz - vertices[0].vertex.xyz, vertices[2].vertex.xyz - vertices[0].vertex.xyz);
                faceNormal = normalize(faceNormal);

                // Expand each edge into two quads
                for (int i = 0; i < 3; ++i)
                {
                    int next = (i + 1) % 3;
                    float4 dir = normalize(vertices[next].vertex - vertices[i].vertex);
                    float4 normal = float4(faceNormal, 0);
                    float4 offset = float4(_WireWidth * normalize(cross(normal.xyz, dir.xyz)),0);

                    float4 p1 = vertices[i].vertex - offset;
                    float4 p2 = vertices[i].vertex + offset;
                    float4 p3 = vertices[next].vertex + offset;
                    float4 p4 = vertices[next].vertex - offset;

                    // First Quad
                    OUT.vertex = p1; triStream.Append(OUT);
                    OUT.vertex = p2; triStream.Append(OUT);
                    OUT.vertex = p3; triStream.Append(OUT);

                    // Second Quad
                    OUT.vertex = p1; triStream.Append(OUT);
                    OUT.vertex = p3; triStream.Append(OUT);
                    OUT.vertex = p4; triStream.Append(OUT);
                }
            }

            float4 frag (v2g f) : SV_Target
            {
                return _WireColor;
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

Wireframe Shader (with backface cull)

Shader "Custom/WireframeBackfaceCull_Geom"
{
    Properties
    {
        _WireColor("Wireframe Color", Color) = (1,1,1,1)
        _WireWidth("Wireframe Width", Float) = 1.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100
        Conservative True

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

            #include "UnityCG.cginc"

            struct appdata{
                float4 vertex : POSITION;
            };

            struct v2g{
                float4 vertex : SV_POSITION;
                float3 world : TEXCOORD0;
            };

            float4 _WireColor;
            float _WireWidth;

            v2g vert (appdata v){
                v2g o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.world = mul(unity_ObjectToWorld, v.vertex);
                return o;
            }

            float isTriFacingCamera(v2g vertices[3]){
                // Calculate the normal of the triangle
                float3 v1 = vertices[0].world.xyz;
                float3 v2 = vertices[1].world.xyz;
                float3 v3 = vertices[2].world.xyz;
                float3 normal = cross(v2 - v1, v3 - v1);
                // Check if the triangle is facing the camera
                float3 viewDirection = normalize(_WorldSpaceCameraPos - v1);
                return dot(normal, viewDirection);
            }

            [maxvertexcount(18)]
            void geom(triangle v2g vertices[3], inout TriangleStream<v2g> triStream){
                v2g OUT;
                if(isTriFacingCamera(vertices) < 0){return;}

                float3 faceNormal = cross(vertices[1].vertex.xyz - vertices[0].vertex.xyz, vertices[2].vertex.xyz - vertices[0].vertex.xyz);
                faceNormal = normalize(faceNormal);

                // Expand each edge into two quads
                for (int i = 0; i < 3; ++i){
                    int next = (i + 1) % 3;
                    float4 dir = normalize(vertices[next].vertex - vertices[i].vertex);
                    float4 offset = float4(_WireWidth * normalize(cross(faceNormal, dir.xyz)),0);

                    float4 p1 = vertices[i].vertex - offset;
                    float4 p2 = vertices[i].vertex + offset;
                    float4 p3 = vertices[next].vertex + offset;
                    float4 p4 = vertices[next].vertex - offset;

                    // First Quad
                    OUT.world = float3(0,0,0);
                    OUT.vertex = p1; triStream.Append(OUT);
                    OUT.vertex = p2; triStream.Append(OUT);
                    OUT.vertex = p3; triStream.Append(OUT);

                    // Second Quad
                    OUT.vertex = p1; triStream.Append(OUT);
                    OUT.vertex = p3; triStream.Append(OUT);
                    OUT.vertex = p4; triStream.Append(OUT);
                }
            }

            float4 frag (v2g f) : SV_Target{
                return _WireColor;
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

Opaque Wireframe Shader with Texture

Shader "Custom/WireframeWithTexture"
{
    Properties
    {
        _MainTex("MainTexture", 2D) = "white"{}
        _WireColor("Wireframe Color", Color) = (1,1,1,1)
        _WireWidth("Wireframe Width", Range(0.001, 0.1)) = 0.05
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100
        Conservative True

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

            #include "UnityCG.cginc"

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

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

            sampler2D _MainTex;
            float4 _WireColor;
            float _WireWidth;

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

            float isTriFacingCamera(v2g vertices[3]){
                // Calculate the normal of the triangle
                float3 v1 = vertices[0].world.xyz;
                float3 v2 = vertices[1].world.xyz;
                float3 v3 = vertices[2].world.xyz;
                float3 normal = cross(v2 - v1, v3 - v1);
                // Check if the triangle is facing the camera
                float3 viewDirection = normalize(_WorldSpaceCameraPos - v1);
                return dot(normal, viewDirection);
            }

            [maxvertexcount(21)]
            void geom(triangle v2g vertices[3], inout TriangleStream<v2g> triStream){
                v2g OUT;

                //main triangle, that represents the geometry:
                OUT.world = float3(1,1,1);
                OUT.vertex = vertices[0].vertex;
                OUT.uv = vertices[0].uv;
                triStream.Append(OUT);

                OUT.vertex = vertices[1].vertex;
                OUT.uv = vertices[1].uv;
                triStream.Append(OUT);

                OUT.vertex = vertices[2].vertex;
                OUT.uv = vertices[2].uv;
                triStream.Append(OUT);

                triStream.RestartStrip();

                if(isTriFacingCamera(vertices) < 0){return;}

                //wireframe as 2 quads, three times:

                float3 faceNormal = cross(vertices[1].vertex.xyz - vertices[0].vertex.xyz, vertices[2].vertex.xyz - vertices[0].vertex.xyz);
                faceNormal = normalize(faceNormal);

                float4 zOffset =  float4(0,0,0.00001,0);

                // Expand each edge into two quads
                for (int i = 0; i < 3; ++i){
                    int next = (i + 1) % 3;
                    float4 dir = normalize(vertices[next].vertex - vertices[i].vertex);
                    float4 offset = float4(_WireWidth * normalize(cross(faceNormal, dir.xyz)),0);

                    float4 p1 = vertices[i].vertex - offset + zOffset;
                    float4 p2 = vertices[i].vertex + offset + zOffset;
                    float4 p3 = vertices[next].vertex + offset + zOffset;
                    float4 p4 = vertices[next].vertex - offset + zOffset;

                    // First Quad
                    OUT.world = float3(0,0,0);
                    OUT.vertex = p1; triStream.Append(OUT);
                    OUT.vertex = p2; triStream.Append(OUT);
                    OUT.vertex = p3; triStream.Append(OUT);

                    // Second Quad
                    OUT.vertex = p1; triStream.Append(OUT);
                    OUT.vertex = p3; triStream.Append(OUT);
                    OUT.vertex = p4; triStream.Append(OUT);
                }
            }

            float4 frag (v2g f) : SV_Target{
                fixed4 col = tex2D(_MainTex, f.uv);
                       col = lerp(_WireColor, col, f.world.x);
                return col;
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

There is also SpatialMappingWireframe By Unity team themselves. It’s quite nice (the best in my opinion), but I needed to adjust the frag function to use one color:
Wireframe by Unity (+ texture + wire color with alpha)

// Unity built-in shader source. Copyright (c) 2016 Unity Technologies. MIT license (see license.txt)
// from https://github.com/TwoTailsGames/Unity-Built-in-Shaders/blob/master/DefaultResourcesExtra/VR/Shaders/SpatialMappingWireframe.shader
// This is adjusted version, allowing to tweak color of Wireframe, make it barely visible (alpha of color).
// Allows for a texture.
Shader "Custom/VRWireframe (Unlit Texture)"
{
    Properties
    {
        _WireThickness ("Wire Thickness", RANGE(0, 800)) = 100
        _MainTex("MainTexture", 2D) = "white"{}
        _WireColor("Wireframe Color", Color) = (1,1,1,1)
    }

    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            // Wireframe shader based on the the following
            // http://developer.download.nvidia.com/SDK/10/direct3d/Source/SolidWireframe/Doc/SolidWireframe.pdf

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

            #include "UnityCG.cginc"

            float _WireThickness;
            sampler2D _MainTex;
            fixed4 _WireColor;

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

            struct v2g
            {
                float4 projectionSpaceVertex : SV_POSITION;
                float2 uv : TEXCOORD0;
                float4 worldSpacePosition : TEXCOORD1;
            };

            struct g2f
            {
                float4 projectionSpaceVertex : SV_POSITION;
                float2 uv : TEXCOORD0;
                float4 worldSpacePosition : TEXCOORD1;
                float4 dist : TEXCOORD2;
            };

            v2g vert (appdata v)
            {
                v2g o;
                o.projectionSpaceVertex = UnityObjectToClipPos(v.vertex);
                o.worldSpacePosition = mul(unity_ObjectToWorld, v.vertex);
                o.uv = v.uv;
                return o;
            }

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

                float2 edge0 = p2 - p1;
                float2 edge1 = p2 - p0;
                float2 edge2 = p1 - p0;

                // To find the distance to the opposite edge, we take the
                // formula for finding the area of a triangle Area = Base/2 * Height,
                // and solve for the Height = (Area * 2)/Base.
                // We can get the area of a triangle by taking its cross product
                // divided by 2.  However we can avoid dividing our area/base by 2
                // since our cross product will already be double our area.
                float area = abs(edge1.x * edge2.y - edge1.y * edge2.x);
                float wireThickness = 800 - _WireThickness;

                g2f o;
                o.worldSpacePosition = i[0].worldSpacePosition;
                o.projectionSpaceVertex = i[0].projectionSpaceVertex;
                o.dist.xyz = float3( (area / length(edge0)), 0.0, 0.0) * o.projectionSpaceVertex.w * wireThickness;
                o.dist.w = 1.0 / o.projectionSpaceVertex.w;
                o.uv = i[0].uv;
                triangleStream.Append(o);

                o.worldSpacePosition = i[1].worldSpacePosition;
                o.projectionSpaceVertex = i[1].projectionSpaceVertex;
                o.dist.xyz = float3(0.0, (area / length(edge1)), 0.0) * o.projectionSpaceVertex.w * wireThickness;
                o.dist.w = 1.0 / o.projectionSpaceVertex.w;
                o.uv = i[1].uv;
                triangleStream.Append(o);

                o.worldSpacePosition = i[2].worldSpacePosition;
                o.projectionSpaceVertex = i[2].projectionSpaceVertex;
                o.dist.xyz = float3(0.0, 0.0, (area / length(edge2))) * o.projectionSpaceVertex.w * wireThickness;
                o.dist.w = 1.0 / o.projectionSpaceVertex.w;
                o.uv = i[2].uv;
                triangleStream.Append(o);
            }

            fixed4 frag (g2f i) : SV_Target{
                float minDistanceToEdge = min(i.dist[0], min(i.dist[1], i.dist[2])) * i.dist[3];

                // Early out if we know we are not on a line segment.
                float4 texCol = tex2D(_MainTex, i.uv);

                // Smooth our line out
                float t = exp2(-2 * minDistanceToEdge * minDistanceToEdge);
                      t *= _WireColor.a;

                fixed4 finalColor = lerp(texCol, _WireColor, t);
                finalColor.a = 1;

                return finalColor;
            }
            ENDCG
        }
    }
}

how would you only display quads and not diagonals