最近在学习渲染,入门的时候也学了Back facing描边法,但是表现在正方体上割裂严重,就先略过了。
看了这篇文章才了解到平均法线的做法,作者将平均法线存到模型的切线数据中,也提及了可以转换到切线空间再存到颜色或者uv上。因为他只做了存到切线数据的方案,我就想试试使用切线空间法线的方案,写出来才算融会贯通了。
对作者的工具稍作修改,将网格保存到本地。
《UnityShader入门精要》这本书的法线贴图相关章节,提到了TBN矩阵,在C#实现一下,并且类似法线贴图把数值范围处理到[0,1]。
工具:模型平均法线写入顶点颜色
[MenuItem("Tools/模型平均法线写入顶点颜色")]
public static void WriteToColor()
{
MeshFilter[] meshFilters = Selection.activeGameObject.GetComponentsInChildren<MeshFilter>();
foreach (var meshFilter in meshFilters)
{
Mesh mesh = Object.Instantiate(meshFilter.sharedMesh);
ToColor(mesh);
AssetDatabase.CreateAsset(mesh, "Assets/" + meshFilter.name + "New.mesh");
}
SkinnedMeshRenderer[] skinMeshRenders = Selection.activeGameObject.GetComponentsInChildren<SkinnedMeshRenderer>();
foreach (var skinMeshRender in skinMeshRenders)
{
Mesh mesh = Object.Instantiate(skinMeshRender.sharedMesh);
ToColor(mesh);
AssetDatabase.CreateAsset(mesh, "Assets/" + skinMeshRender.name + "New.mesh");
}
}
private static void ToColor(Mesh mesh)
{
var averageNormalHash = new Dictionary<Vector3, Vector3>();
for (var j = 0; j < mesh.vertexCount; j++)
{
if (!averageNormalHash.ContainsKey(mesh.vertices[j]))
{
averageNormalHash.Add(mesh.vertices[j], mesh.normals[j]);
}
else
{
averageNormalHash[mesh.vertices[j]] =
(averageNormalHash[mesh.vertices[j]] + mesh.normals[j]).normalized;
}
}
var averageNormals = new Vector3[mesh.vertexCount];
for (var j = 0; j < mesh.vertexCount; j++)
{
averageNormals[j] = averageNormalHash[mesh.vertices[j]];
//转到切线空间 并且设置成[0,1]范围
var mNormal = mesh.normals[j];
var mTangent = mesh.tangents[j];
var mBinormal = Vector3.Cross(mNormal, new Vector3(mTangent.x, mTangent.y, mTangent.z)) * mTangent.w;
//构造是按列,此处需要按行
Matrix4x4 matrix = new Matrix4x4(new Vector3(mTangent.x, mTangent.y, mTangent.z).normalized, mBinormal.normalized, mNormal.normalized, Vector4.zero);
Matrix4x4 tmatrix = Matrix4x4.Transpose(matrix);
//[-1,1]=>[0,2]=>[0,1]
averageNormals[j] = (tmatrix.MultiplyVector(averageNormals[j]).normalized + Vector3.one) / 2.0f;
}
var colors = new Color[mesh.vertexCount];
for (var j = 0; j < mesh.vertexCount; j++)
{
colors[j] = new Color(averageNormals[j].x, averageNormals[j].y, averageNormals[j].z, 0);
}
mesh.colors = colors;
}
在Shader是类似的,将矩阵转置一下,就是切线空间转模型空间的矩阵
v2f vert(appdata v)
{
v2f o;
float4 pos = UnityObjectToClipPos(v.vertex);
TANGENT_SPACE_ROTATION;
float3 anormal = v.colors.rgb * 2 - 1;
//这里再转置(逆)一下,因为rotation是模型空间转切线空间的矩阵
anormal = normalize(mul(transpose(rotation), anormal));
float3 viewNormal = mul((float3x3)UNITY_MATRIX_IT_MV, anormal.xyz);
float3 ndcNormal = normalize(TransformViewToProjection(viewNormal.xyz)) ;//将法线变换到NDC空间
float aspect = _ScreenParams.x / _ScreenParams.y;//求得屏幕宽高比
ndcNormal.x /= aspect;
//pos.w跟距离有关 裁剪后会除掉,进行近大远小缩放,但我们希望远处的描边也一样大,所以乘回去,避免被除没了
pos.xy += 0.01 * _OutlineScale * ndcNormal.xy * pos.w;
o.vertex = pos;
return o;
}
TANGENT_SPACE_ROTATION; 会得到模型空间转切线空间矩阵rotation,再做一下转置就是切线转模型。
anormal就是还原好的模型空间下的平均法线
为了在屏幕上,无论远近描边大小都不变,也就是说要还原“透视除法”的处理,将w分量乘回去。
另外,我们在屏幕上沿着发现扩展的x增量,在视口变换后,会根据屏幕分辨率调整,横屏变得更大竖屏更小,因此也要消除掉这个影响。