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):
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:
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?
As @Gnietschow said, you need to use the "smooth normals" of those vertices to know the direction to expand the outline:
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
}