Unity渲染Lowpoly风格的模型硬边

Last modified date

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"
}

直接在建模的时候做硬边,美术比较好控制最终效果。制作角色道具之类的比较讲究美观的模型的时候适合用这个方式.

其他做法都是自动把所有边都变成硬边。总之以上上几种方法各有优缺点,要根据不同项目不同平台选择合理的用法。