什麼!改個物件顏色,效能就不見了!Shader V.S. Material V.S. Vertex

單純只想讓物件們看起來不一樣

我們今天做了一個物件,它是白色的。但我想另外建立 10 個物件,但只改顏色讓它們看起來不一樣,我們可能有那些做法?

  1. 編輯時,建立 10 個不同顏色的材質。
  2. 遊戲時,動態複製材質,對材質改顏色。
  3. 寫一個能改顏色的 Shader 。
  4. 把 Mesh 頂點出取修改 vertex color。

由上面幾個看起來

  1. 比較無腦,但東西做起來有點麻煩。
  2. Code 不會太難寫。
  3. Shader 耶!看起來好屌,但 code 難寫了點。
  4. 要懂 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

發表留言