單純只想讓物件們看起來不一樣
我們今天做了一個物件,它是白色的。但我想另外建立 10 個物件,但只改顏色讓它們看起來不一樣,我們可能有那些做法?
- 編輯時,建立 10 個不同顏色的材質。
- 遊戲時,動態複製材質,對材質改顏色。
- 寫一個能改顏色的 Shader 。
- 把 Mesh 頂點出取修改 vertex color。
由上面幾個看起來
- 比較無腦,但東西做起來有點麻煩。
- Code 不會太難寫。
- Shader 耶!看起來好屌,但 code 難寫了點。
- 要懂 Mesh 結構,不過因為是改 Mesh,就像是你建了很多不同顏色的 Mesh 再丟到埸景裡,結果就跟做了很多 Mesh 一樣,
本文跳過這方法。
大家會想挑那一個來實作呢?很難選是吧?那就全部送給你!(除了第 4 個,小聲)
建立多個不同顏色的材質
這個方法太簡單了!
在 Assets 視窗裡按右鍵 -> Create -> Material
然後再挑個能設定顏色的 Shader ,挑個自己想要的顏色,這個材質就完成了。接下來把材質拉進物件的 render 的 materials 裡。
完成後,把 10 個不同顏色的物件做成 Prefab,供日後使用。
動態複製材質
這方法也不難。寫一個腳本,在裡面建立顏色變數 color ,然後拉進物件身上。在編輯時設定顏色、在 Start() 時複製材質及修改顏色。
using UnityEngine; using System.Collections; public class wallsimp : MonoBehaviour { public Color color; void OnDestroy() { GameObject.Destroy(GetComponent<Renderer>().material); } // Use this for initialization void Start () { GetComponent<Renderer>().material.color = color; } // Update is called once per frame void Update () { } }
用這方法,我們應該只要建立一個 Prefab,要用時拉進 Scene 裡,再各別設定顏色即可。若要做成 10 個不同色 Prefab 我也是不反對啦。
寫一個能改顏色的 Shader
這個做法需要二個東西:
- Shader
- Script
首先建立一個 Shader
在 Assets 視窗裡按右鍵 -> Create -> Shader
在 Shader 內除了貼圖外,再加一個顏色的屬性 _MainColor。取出貼圖顏色後再乘上 _MainColor 即可。
Shader "Custom/TexturedColor" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _MainColor ("MainColor", Color) = (1,1,1,1) } SubShader { Tags { "RenderType"="Opaque" } LOD 200 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" uniform sampler2D _MainTex; uniform float4 _MainColor; struct vertexInput { float4 vertex : POSITION; // The vertex position in model space. float3 normal : NORMAL; // The vertex normal in model space. float4 color : COLOR; // Per-vertex color float4 texcoord : TEXCOORD0; // The first UV coordinate. }; struct vertexOutput { float4 pos : SV_POSITION; float4 color : COLOR; float2 tex : TEXCOORD0; float3 normal : TEXCOORD1; }; vertexOutput vert(vertexInput v) { vertexOutput o; o.pos = mul(UNITY_MATRIX_MVP, v.vertex); o.color = v.color; o.tex = v.texcoord; o.normal = v.normal; return o; } float4 frag (vertexOutput input ) : COLOR { float4 outcolor = float4(0 , 0, 0, 0); float4 lMainTex = tex2D(_MainTex, input.tex) * _MainColor; outcolor = outcolor + lMainTex ; return outcolor; } ENDCG } } FallBack "Diffuse" }
再來建立一個腳本,加入一個 color 變數,在編輯時設定顏色,在 Start() 時設定 Shader 裡的顏色的屬性。
using UnityEngine; using System.Collections; public class ShaderColor : MonoBehaviour { public Color color; // Use this for initialization void Start () { GetComponent<Renderer>().material.SetVector("_MainColor", color); } // Update is called once per frame void Update () { } }
然後建立一個材質,指定它使用 Custom/TexturedColor shader,再來把這個材質拉進物件的 render 的 materials 裡即可。
用這方法跟上個方法一樣,我們應該只要建立一個 Prefab,要用時拉進 Scene 裡,再各別設定顏色即可。若要做成 10 個不同色 Prefab 我也是再次不反對啦。
效能上出了什麼事?
好棒!我們完成了三種可以改顏色的做法了!但若我們用在大量的物件上時問題就浮現出來了。現在我們在場景上有 1000 個物件(不開影子,比較單純好分析),談一下它們各自會是什麼狀況。
首先,動態複製材質的這個做法,因為材質是複製出來的,所以當我們有 1000 個物件在場景時,不管裡面顏色有沒有重複,一律會產生 1000 個材質,每加一個物件會增加 2 個 setpass call、增加 2 個 draw call。所以有 2000 setpass call、2000 draw call。嚇到了吧!哈哈。
再來看一下我們辛苦寫 Shader 的做法,因為只要有存取到 GetComponent().material,Unity 一樣會很貼心的幫我們複製一份材質。所以結果跟動態複製材質的做法一樣會有 2000 setpass call、2000 draw call。
最後是無腦的做法,建立 10 個不同顏色的材質。別嚇到了哦。在 1000 個物件時,10 種顏色都出現在場景裡,材質會只會有 10 個(就是我們在編輯時建出來的那 10 個),每增加一種不同色的物件會增加 2 個 setpass call、增加 2 個 draw call。只有 10 種顏色,再因 Unity 的 DynamicBatching 技術,整個場景只會有 20 setpass call、20 draw call。
以下附上表格:
環境:1000 個 3D BOX 物件、10 種顏色、沒有影子。
setpass call | draw call | |
建立個不同顏色的材質 | 20 | 20 |
動態複製材質改顏色 | 2000 | 2000 |
寫一個能改顏色的 Shader | 2000 | 2000 |
最後,我們看到這結果時,你會想說什麼呢?我會想說:
傻人有傻福
最後一個做法
把 Mesh 頂點出取修改 vertex color,這方法就像是重製一份 Mesh 那樣,只是這份工作由美術身上移至程式身上。我們可以寫編輯功能在編輯時就把改過頂資料的 Mesh 存出,但這個方法跟由美術製作的感覺一樣,我就不教大家怎麼寫工具了。
這裡要提的做法是執行時動態改 Mesh 的顏色,這做法需要二個東西:
- Shader
- Script
我們可以寫一個簡單有頂點色的 Shader(或挑一個有計算頂點色的 Shader ),這個 Shader 與上面提供的 Shader 很像,只差在現在這一個在 Pixel Shader 裡把頂點色算進來,上面的沒有。
Shader "Custom/TexturedColor2" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 200 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" uniform sampler2D _MainTex; struct vertexInput { float4 vertex : POSITION; // The vertex position in model space. float3 normal : NORMAL; // The vertex normal in model space. float4 color : COLOR; // Per-vertex color float4 texcoord : TEXCOORD0; // The first UV coordinate. }; struct vertexOutput { float4 pos : SV_POSITION; float4 color : COLOR; float2 tex : TEXCOORD0; float3 normal : TEXCOORD1; }; vertexOutput vert(vertexInput v) { vertexOutput o; o.pos = mul(UNITY_MATRIX_MVP, v.vertex); o.color = v.color; o.tex = v.texcoord; o.normal = v.normal; return o; } float4 frag (vertexOutput input ) : COLOR { float4 outcolor = input.color; float4 lMainTex = tex2D(_MainTex, input.tex); outcolor = outcolor * lMainTex ; return outcolor; } ENDCG } } FallBack "Diffuse" }
再來寫一個腳本,裡面有一個顏色變數 color ,在編輯器選顏色,start() 時設定顏色。
using UnityEngine; using System.Collections; public class MeshColor : MonoBehaviour { public Color color; // Use this for initialization void Start () { MeshFilter mf = GetComponent<MeshFilter>(); Color [] nc = new Color[mf.mesh.vertices.Length]; for (int i = 0; i < nc.Length; i++) { nc[i] = color; } mf.mesh.colors = nc; } // Update is called once per frame void Update () { } }
然後建立一個材質,指定它使用 Custom/TexturedColor2 shader,再來把這個材質拉進物件的 render 的 materials 裡即可。由於是程式改色,所以我們應只要建一個 Prefab 即可,然後要使用時拉進場景中再挑選我們要的顏色。
效能有問題嗎?
其實在執行前,我們應就能猜出 setpass/draw call 的結果了,因為它們共用了一個材質,它用量一定是最少的。 馬上來與前三個做法比較一下:
環境:1000 個 3D BOX 物件、10 種顏色、沒有影子。
MacBookPro 2.6 GHz Intel Core i5 8 GB 1600 MHz DDR3 Intel Iris 1536 MB |
setpass call | draw call | start() 時間 ms | 記憶體用量 mesh/material |
建立個不同顏色的材質 | 20 | 20 | 小到找不到 | 450 KB / 28.9 KB |
動態複製材質(default-material) | 2000 | 2000 | 小到找不到 | 232 KB / 1.7 MB |
寫一個能改顏色的 Shader | 2000 | 2000 | 小到找不到 | 459 KB / 0.8 MB |
改頂點色 | 2 | 4 | 39.64 ms | 3.1 MB /16.2 KB |
修改頂點的做法果然跟我們預測的一樣效能最好,使用一個材質增加 2 個 setpass call、增加 2 個 draw call,但隨著物件數量增至 1000 時,draw call 微上升變成 4 個。
只不過,動態去改頂點使它在 start() 時多花了點時間,也因為每個模型都要有不同的頂點色,所以多建立出來的 colors buffer 使記憶體用量多了幾 MB(不管你原模型有沒有頂點色資訊),它成敗的因素就是頂點的數量。
也許有人會問:
"那就一開始由美術做 10 種不同頂點色的物件不就好了,不用寫 code 、不用寫工具、也不會卡在 start()?"
沒錯!這其實是我一開始不想實作改 Mesh 的原因,它的最終效能是能改進的!由美術來做可以解決 start() 卡住問題!與本文一開的共用一物件只想讓顏色不同的初衷不一樣了。
但如果有美術真的要做 10 種不同頂點色的物件給我,我一定會哭著求他別這樣對待我,因為使用硬碟、記憶體容量會變大,錯誤、修正不好維護(比如:模型改變設計或有 Bug 了,另外 9 個都要記得一起改,最後會變成叫程式寫工具來改)。我沒遇過想做 10 種頂點不同色的美術,大家都會選用製作 10 種不同材質或由程式寫 Shader 。
四種套餐,吃什麼好?
以上把所有的方法及比較表格都提共給大家了,在記憶體、效能、或製作工法自己做個平衡後,相信大家應能依不同的情況選用不同的做法吧?
比如:
- 建立材質:適合物件量多,遊戲中不會一直變化顏色的。
- 動態複製材質、寫 Shader:適合物件量少,遊戲進行中顏色需多變化的。
- 改頂點色:適合頂點量少,遊戲中不會一直變化顏色。
以前使用非 Unity 引擎時,改頂點色的情況我只用在 GUI 及自己用程式建出來的Mesh(頂點少),美術建出來的模型(頂點多)我大多用是使用預建材質、動態換材質及 Shader。在 Unity 中,GUI 不用我自己寫,所以改頂點色的方法我是還沒讓它出埸過。
最最最後,大家看了改頂點色的做法是不是心中也有句話想講?我是想說:
雖然改 Mesh 要考量的東西不少,但大家若能清楚明白的酌量使用它其實是很好的 (比如用時間、記憶體用量換執行時的效能)。
參考:
DrawCallBatching http://docs.unity3d.com/Manual/DrawCallBatching.html