unity-game-engineshaderhlslcgshaderlab

multiplying vertex data causes uneven outline (shaderlab unity3d)


I have created a simple outline shader in Unity3D's shader lab with two passes: pass one scales the object up by multiplying vertex information along a vertex normal and pass two draws the regular (base pass) version of the object. The problem is in the code for the outline pass:

Pass {
    Name "OUTLINE"

    ZWrite Off
    Blend SrcAlpha OneMinusSrcAlpha // Normal

    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    #include "UnityCG.cginc"

    struct appdata {
        float4 vertex : POSITION;
        float3 normal : NORMAL;
    };

    struct v2f {
        float4 position : POSITION;
        float3 normal : NORMAL;
    };

    uniform float _OutlineWidth;
    uniform float4 _OutlineColor;

    v2f vert(appdata v) {
        v.vertex.xyz *=  _OutlineWidth;

        v2f o;
        o.position = UnityObjectToClipPos(v.vertex);
        return o;
    }

    half4 frag(v2f i) : COLOR {
        return _OutlineColor;
    }

    ENDCG
}

_OutlineWidth and _OutlineColor are of Range and Color types, respectively. I have applied this shader to a couple of "tetromino-like" meshes that have been created programmatically. The result is this (click on links):

image one image two

As you can see, an outline is created, but the outline is not uniform width along the outer edge of the object. Along one of the faces, the outline is larger--it is larger along the face that is furthest from the center of the shape. And with non-convex shapes the problem is magnified; outline may not even encompass the shape at all:

image three

I understand that this is due to the vertex position being relative to the center of the shape and that the line v.vertex.xyz *= _OutlineWidth merely multiplies this position by a constant amount (putting it further away from the center of the object). How do I modify my code so that the outline pixels are calculated independent of the shape's center and with integrity to the true outline of the object?


Solution

  • As @Gnietschow said, you need to use the "smooth normals" of those vertices to know the direction to expand the outline:

    hard vs smooth normals. Source: FrostSoft

    One way to do this is to calculate the soft normals in C# and then assign them as vertex data. This example uses uv2 and uv3 channels to hold the smooth normal components:

    Mesh mesh = GetComponent<MeshFilter>().mesh;
    
    Vector3[] meshVertices = mesh.vertices;
    //map vertex positions to the ids of all vertices at that position
    Dictionary<Vector3, List<int>> vertexMerge = new Dictionary<Vector3, List<int>>();
    for(int i = 0; i < mesh.vertexCount; i++) {
        Vector3 vectorPosition = meshVertices[i];
    
        if(!vertexMerge.ContainsKey(vectorPosition)) {
            //if not already in our collection as a key, add it as a key
            vertexMerge.Add(vectorPosition, new List<int>());
        }
    
        //add the vertex id to our collection
        vertexMerge[vectorPosition].Add(i);
    }
    
    //map vertexIDs to the averaged normal
    Vector3[] meshNormals = mesh.normals;
    Vector3[] vertexAveragedNormals = new Vector3[mesh.vertexCount];
    
    foreach (List<int> duplicatedVertices in vertexMerge.Values) {
        //calculate average normal
        Vector3 sumOfNormals = Vector3.zero;
        foreach (int vertexIndex in duplicatedVertices) {
            sumOfNormals += meshNormals[vertexIndex];
        }
    
        Vector3 averagedNormal = (sumOfNormals /= duplicatedVertices.Count).normalized; //average is sum divided by the number of summed elements
    
        //write the result to our output
        foreach (int vertexIndex in duplicatedVertices) {
            vertexAveragedNormals[vertexIndex] = averagedNormal;
        }
    }
    
    
    //write the result to mesh.
    //x and y components shoved into uv3, z component shoved into uv4, with w component of 1.
    Vector2[] vertexAveragedNormalsXY = new Vector2[mesh.vertexCount];
    Vector2[] vertexAveragedNormalsZW = new Vector2[mesh.vertexCount];
    for(int i = 0; i < mesh.vertexCount; i++) {
        Vector3 normal = vertexAveragedNormals[i];
        vertexAveragedNormalsXY[i] = new Vector2(normal.x, normal.y);
        vertexAveragedNormalsZW[i] = new Vector2(normal.z, 1);
    }
    
    mesh.uv3 = vertexAveragedNormalsXY;
    mesh.uv4 = vertexAveragedNormalsZW;
    

    Source: Reddeyfish-

    Then, you use TEXCOORD2 and TEXCOORD3 to reconstruct the smooth normals in vert. Use those to move your vertex position accordingly, while keeping the vertex normal the same hard normal for any lighting purposes:

    Pass {
        Name "OUTLINE"
    
        ZWrite Off
        Blend SrcAlpha OneMinusSrcAlpha // Normal
    
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        #include "UnityCG.cginc"
    
        struct appdata {
            float4 vertex : POSITION;
            float3 normal : NORMAL;
            float4 texcoord2 : TEXCOORD2;
            float4 texcoord3 : TEXCOORD3;
        };
    
        struct v2f {
            float4 position : POSITION;
            float3 normal : NORMAL;
        };
    
        uniform float _OutlineWidth;
        uniform float4 _OutlineColor;
    
        v2f vert(appdata v) {
            // add the outline width in the direction of the shared normal
            float3 sharedNormal = float3(v.texcoord2.xy, v.texcoord3.x);
    
            v.vertex.xyz +=  _OutlineWidth * sharedNormal;  
    
            v2f o;
            o.position = UnityObjectToClipPos(v.vertex);
            return o;
        }
    
        half4 frag(v2f i) : COLOR {
            return _OutlineColor;
        }
    
        ENDCG
    }