Unity渲染Lowpoly风格的模型硬边
Lowpoly风格的一大特色是模型硬边,这片文章探讨一下几种不同的实现方式。
做法1:拆分Mesh顶点
把polygon的edge设成硬边,导入到Unity里,硬边的Polygon会自动变成拆分顶点的网格(每个三角面完全独立)。渲染的时候每个三角面会有独立的法线信息,所以最终显示的边缘是硬边。
这样做的缺点就是会增加顶点数。基本上翻了3倍。在早期的低端智能手机上跑2万顶点就会开始卡了,现在当然好很多了,10万顶点也毫无压力。
有以下几种方式实现:
1,在3D软件里把需要硬边的部分的Edge设置成HardEdge
2,导入到unity里,在导入设置里“Normals & Tangents”里面,“Normals”设成“Caculate”,角度改成0(小于此角度的都变成硬边)
3,通过代码把网格转换成拆成独立三角面的网格。这个适合用在动态生成的Mesh上。
代码片段如下:
Vector3[] oldVerts = mesh.vertices;
Vector4[] oldTangents = mesh.tangents;
Vector2[] oldUVs = mesh.uv;
int[] triangles = mesh.triangles;
Vector3[] newVerts = new Vector3[triangles.Length];
Vector4[] newTangents = new Vector4[triangles.Length];
Vector2[] newUVs = new Vector2[triangles.Length];
for (int i = 0; i < triangles.Length; i++) {
newVerts[i] = oldVerts[triangles[i]];
newTangents[i] = oldTangents[triangles[i]];
newUVs[i] = oldUVs[triangles[i]];
triangles[i] = i;
}
mesh.vertices = newVerts;
mesh.tangents = newTangents;
mesh.triangles = triangles;
mesh.uv = newUVs;
mesh.RecalculateBounds();
mesh.RecalculateNormals();
做法2:用面法线FaceNormal
上面转化硬边的方法会增加顶点数目,有一种完全不需要拆分网格的方法就是使用Geometry Shader。这种方法的原理就是在光栅化前,给每个顶点增加一个属性,面法线faceNormal。由于Geometry Shader中可以知道同一个三角面片中的所有三个顶点的信息,因此我们可以为它们计算一个相应的面法线值。这样,即便在经过光栅化插值后,同一个三角面片中的面法线也是一样的。
但是faceNormal要SM4.0才支持,手机是跑不了的。
Shader代码片段如下:
[maxvertexcount(3)]
void geom(triangle v2g IN[3], inout TriangleStream<g2f> triStream) {
float3 A = IN[1].worldPos.xyz - IN[0].worldPos.xyz;
float3 B = IN[2].worldPos.xyz - IN[0].worldPos.xyz;
float3 fn = normalize(cross(A, B));
g2f o;
o.pos = IN[0].pos;
o.uv = IN[0].uv;
o.worldPos = IN[0].worldPos;
o.faceNormal = fn;
triStream.Append(o);
o.pos = IN[1].pos;
o.uv = IN[1].uv;
o.worldPos = IN[1].worldPos;
o.faceNormal = fn;
triStream.Append(o);
o.pos = IN[2].pos;
o.uv = IN[2].uv;
o.worldPos = IN[2].worldPos;
o.faceNormal = fn;
triStream.Append(o);
}
做法3:计算每个三角面的法线信息,计算出其光影信息
在fragment部分,计算出每个渲染单位所在三角面的法线信息,再通过光线方向和视野方向,计算出该三角面的受光效果。这个方式其实就是自己实现做法2里的FaceNormal。这个可以在手机上跑,但性能还有待测试。
以下是完整的shader代码:
Shader "WalkingFat/DiffuseLowpoly" {
Properties {
_Color ("Color", Color) = (1,1,1,1)
}
SubShader {
Pass {
// FOLLOWING LINE IS NEW
Tags {"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "AutoLight.cginc"
#pragma multi_compile_fwdbase
#pragma multi_compile_fog
uniform float4 _LightColor0;
uniform float4 _Color;
struct VertexInput {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};
struct VertexOutput {
float4 pos : SV_POSITION;
float4 posWorld : TEXCOORD0;
float3 normalDir : TEXCOORD1;
float3 tangentDir : TEXCOORD2;
float3 bitangentDir : TEXCOORD3;
LIGHTING_COORDS(4,5)
UNITY_FOG_COORDS(6)
};
VertexOutput vert (VertexInput v) {
VertexOutput o = (VertexOutput)0;
o.normalDir = UnityObjectToWorldNormal(v.normal);
o.tangentDir = normalize( mul( unity_ObjectToWorld, float4( v.tangent.xyz, 0.0 ) ).xyz );
o.bitangentDir = normalize(cross(o.normalDir, o.tangentDir) * v.tangent.w);
o.posWorld = mul(unity_ObjectToWorld, v.vertex);
float3 lightColor = _LightColor0.rgb;
o.pos = UnityObjectToClipPos( v.vertex );
UNITY_TRANSFER_FOG(o,o.pos);
TRANSFER_VERTEX_TO_FRAGMENT(o)
return o;
}
float4 frag(VertexOutput i) : COLOR {
i.normalDir = normalize(i.normalDir);
float3x3 tangentTransform = float3x3( i.tangentDir, i.bitangentDir, i.normalDir);
float3 viewDirection = normalize(_WorldSpaceCameraPos.xyz - i.posWorld.xyz);
float3 normalLocal = mul( tangentTransform, cross(normalize(ddy(i.posWorld.rgb)),normalize(ddx(i.posWorld.rgb))) ).xyz.rgb;
float3 normalDirection = normalize(mul( normalLocal, tangentTransform )); // Perturbed normals
float3 lightDirection = normalize(_WorldSpaceLightPos0.xyz);
float3 lightColor = _LightColor0.rgb;
////// Lighting:
float attenuation = LIGHT_ATTENUATION(i);
float3 attenColor = attenuation * _LightColor0.xyz;
/////// Diffuse:
float NdotL = max(0.0,dot( normalDirection, lightDirection ));
float3 directDiffuse = max( 0.0, NdotL) * attenColor;
float3 indirectDiffuse = float3(0,0,0);
indirectDiffuse += UNITY_LIGHTMODEL_AMBIENT.rgb; // Ambient Light
float3 diffuseColor = _Color.rgb;
float3 diffuse = (directDiffuse + indirectDiffuse) * diffuseColor;
/// Final Color:
float3 finalColor = diffuse;
fixed4 finalRGBA = fixed4(finalColor,1);
UNITY_APPLY_FOG(i.fogCoord, finalRGBA);
return finalRGBA;
}
ENDCG
}
}
Fallback "VertexLit"
}
直接在建模的时候做硬边,美术比较好控制最终效果。制作角色道具之类的比较讲究美观的模型的时候适合用这个方式.
其他做法都是自动把所有边都变成硬边。总之以上上几种方法各有优缺点,要根据不同项目不同平台选择合理的用法。