轉自http://blog.csdn.net/candycat1992/article/details/51511433
原理
Low polygon風格的渲染也被稱爲flat shading。雖然把這篇文章歸到Shader類別裏,但其實是完全可以用非Shader的方法來解決的。下面兩張圖片,左邊是我們不希望得到的結果,而右邊是我們想要得到的效果。
做美術的同學都知道上面模型的區別就是“硬邊”和“軟邊”的問題。左圖裏就是軟邊的效果,軟邊意味着相鄰三角形之間共用頂點,這些被共用頂點的法線(藍線)通常是根據毗連三角形的面法線加權平均計算得到的,如下圖所示(圖片來源):
由於共用頂點的存在,光柵化階段的插值使得這些三角面片中間每個點的點法線都是有三個頂點的法線插值得到的,而這三個法線各不相同, 使得面片有平滑過渡的效果,從而形成軟邊。在其他風格的渲染中,模型大多都會使用軟邊,這不僅可以減少頂點數目提高性能,還能讓渲染效果更加平滑。
但flat shading顯然不想要平滑的渲染效果,我們希望就算光柵化插值後同一個三角面片中每個點的法線都應該是相同的(都等於面法線),只有這樣它們光照計算的結果纔會是相同的。如下圖所示(圖片來源):
要實現這樣的效果就是把所有相連的邊在建模軟件中設置成硬邊,Hard Edge。這樣軟件背後就會進行拆頂點的工作,每個三角形有各自屬於自己的三個頂點,不與他人共用,這三個頂點的點法線計算不會受毗連三角形的影響,而僅僅是由該三角面片的面法線決定。
因此,我們有了解決方案一:在建模軟件中把邊設置成硬邊。
直接使用Unity
值得高興的是,就算你在建模軟件裏忘記了設置硬邊,Unity也是可以直接幫我們做到軟硬邊的轉換的。它的原理其實就是重新拆點、重新計算點法線。
在我們導入一個模型後,可以在模型的導入面板中的Normals & Tangents塊中,把Normals設置成Calculate模式、把Smoothing Angle設置成0就可以得到硬邊的效果。如上圖所示。
這樣的模型可以直接使用任何普通的Shader進行渲染,就會得到flat shading的效果。
程序產生的網格
之前羣裏有人問程序產生的網格怎麼實現這種效果。原理也是一樣的,我們只需要保證不要共用頂點、正確計算頂點法線即可。在我的NPR項目裏,有這樣一個例子,把任何模型強制轉化成沒有共用頂點的模型。其中關鍵代碼是長這個樣子的:
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();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
因此,如果你是通過程序方法產生網格的,可以使用類似上面的代碼來進行網格轉換,再使用普通的Shader即可。
更復雜的方法:Geometry Shader
上面轉化硬邊的方法會增加頂點數目,有一種完全不需要對網格進行任何處理的方法就是使用Geometry Shader。這種方法的原理就是在光柵化前、在Geometry Shader裏給每個頂點增加一個屬性,面法線faceNormal。由於Geometry 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);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
這種方法有點小題大做,因爲geometry shader是SM 4.0中的特性,移動終端大多達不到這樣的要求。
參考鏈接: