Unity Shader-Matcap(材質捕獲)

前言

最近通關了《Alan Wake》(心靈殺手),整體感覺很不錯,遊戲雖然是2010年發行的,但是畫面至今看來也還是不錯的,尤其是遊戲內的體積光效果,貫穿了整個遊戲,因爲光本身就是這個遊戲中最強力的武器。

主人公是一位小說家,但是當你發現你所寫的小說都變成了現實的時候,這肯定是一件細思極恐的事情。故事情節一波三折,頗有些美劇的感覺。

每次看到電鋸,不由得想起被《生化危機》和《惡靈附身》支配的恐懼,好在這款遊戲的電鋸不太抗揍。其實《惡靈附身》跟這款遊戲有不少相似的地方,都是在“意念”的世界中,所以遊戲中經常出現走着走着天上掉下來輛火車啦,走着走着橋飛啦之類的情況。

遊戲還是很給力的,我就不多劇透了,下面是本文的正題。之前的blog裏面,大多是一些特殊效果以及屏幕後處理相關的內容,但是關於模型本身光照效果的比較少。今天剛好來玩一個簡單,但是卻很有意思的東東,叫做Matcap。

簡介

Matcap,全稱爲Material Capture,翻譯過來就是材質捕獲。簡單來說就是預先生成的一種存儲了光照和反射等信息的貼圖,運行時使用法線方向進行採樣。Matcap的好處就是可以用很低的消耗來實現很多特殊風格的效果,但是Matcap也有一些缺陷,在於Matcap僅對於固定相機視角的情況較好,這也是Matcap的原理決定的。

Matcap主要是在Zbrush,Mudbox這些軟件裏面使用的,ZBrush裏面雕模的時候有時候雖然沒有貼圖,但是效果看起來也挺好的,實際上就是Matcap的作用,而且這些美術大佬們不斷在擴展這些matcap,積累了很多好玩的matcap效果,而使用這些效果卻非常容易,只需要換一下貼圖。

Matcap的原理

先來看一下Matcap的原理。先可以考慮一下CubeMap的原理,CubeMap是一個六面體,正常的反射採樣時使用的反射對應方向上投影最近的面上進行採樣,更簡單一點的(應該算是一個Trick了),甚至可以直接使用法線方向進行採樣,對於天空盒類型的反射也可以得到不錯的效果,即不同法線所指向的方向有不同的效果。而Matcap將這一點發揮得更加淋漓盡致,因爲Matcap只用一張圖就可以。所以我們需要考慮怎樣將這個法線轉換到一個合適的區間來採樣Matcap。

由於物體的法線是一個三維的朝向,但是我們最終採樣到二維貼圖也只有兩個維度。我們需要去掉一個維度,在相機空間下的物體法線向量,我們可以不考慮Z軸的指向,即不考慮Z軸本身對屏幕空間的XY平面的貢獻,因爲法線是一個單位向量,如果法線在XY方向上的投影權重很大,那麼說明在Z方向的權重就很小,對應到二維的Matcap上採樣的位置越靠近邊緣;而如果法線在XY方向的投影權重很小,那麼Z方向的權重就很大,對應到二維的Matcap上的採樣位置就越靠近中心。

接下來我們需要考慮的是在哪一個空間計算Matcap,如果是物體空間或者是世界空間,由於相機位置朝向不確定,不好確定法線的Z方向與屏幕空間的關係,我們不好確定捨棄哪一個維度。而相機空間下的物體在屏幕上的方位都已經大致確定,相機空間下相對運算要比屏幕空間更少一些。所以最合適的實際上就是相機空間下進行計算。

我們可以通過Unity的內置矩陣將法線從物體空間轉化到視空間,然後對於是空間的法線方向,由於方向是(-1,1)區間,我們需要再將其*0.5 + 0.5變化到(0,1)區間,就可以實現使用這個xy方向採樣Matcap貼圖了。

Matcap的實現

上面瞭解了Matcap的原理,下面直接上代碼:

/********************************************************************
 FileName: Matcap.shader
 Description: Matcap效果
 history: 4:11:2018 by puppet_master
 https://blog.csdn.net/puppet_master
*********************************************************************/
Shader "Unlit/Matcap"
{
	Properties
	{
		_MatCap ("Matcap", 2D) = "white" {}
	}
	SubShader
	{
		Tags { "RenderType"="Opaque" }
		LOD 100

		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"

			struct v2f
			{
				float2 matcapuv : TEXCOORD0;
				float4 vertex : SV_POSITION;
			};

			sampler2D _MatCap;
			
			v2f vert (appdata_base v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				//乘以逆轉置矩陣將normal變換到視空間
				float3 viewnormal = mul(UNITY_MATRIX_IT_MV, v.normal);
				//需要normalize一下,否則保證normal處在(-1,1)區間,否則有scale的object效果不對
				viewnormal = normalize(viewnormal);
				o.matcapuv = viewnormal.xy * 0.5 + 0.5;			
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				fixed4 mat = tex2D(_MatCap, i.matcapuv);
				return mat;
			}
			ENDCG
		}
	}
}

我們使用了UNITY_MATRIX_IT_MV,即正常矩陣的逆轉置進行計算,原因在於直接用ModelView矩陣變換法線時,對於非uniform變換可能導致法線與平面不垂直的問題。關於法線的變換,可以參考本人之前的blog《Unity Shader-描邊效果》

我們使用一張Matcap,在Matcap中,我們最終採樣的是一個法線方向變換到(0,1)區間的結果,實際上有效的區域就只是貼圖中心周圍的圓形範圍內有效。而Matcap比較直白的效果就是,在當前視角方向看,Matcap的上下左右方向的顏色就對應模型在當前視角方向對應的法線所指向的上下左右方向。

比如我們用下面一張Matcap貼圖:

在不同的模型上的效果如下:

從上圖中,我們很容易看出,對於一個球體,球體上顯示的,實際上就是Matcap上對應的內容,不論我們從哪個視角看,球體上都是這樣的一個表現。Matcap左上角的高光兩點,對應到模型上左上角的部分也會有高光亮點。

另外,還有一點需要注意,在我們把Normal變換到視空間後,我們進行了一次Normalize操作。其實我在網上看到了絕大部分的版本的Matcap的uv計算實現是這樣的:

o.matcapuv.x = mul(UNITY_MATRIX_IT_MV[0], v.normal);
o.matcapuv.y = mul(UNITY_MATRIX_IT_MV[1], v.normal);

可能是出於性能的考慮,直接使用UNITY_MATRIX_IT_MV的x和y軸作爲基,直接求Normal在xy軸的投影,雖然可以減少矩陣的計算,但是這樣有一個比較嚴重的問題。如果對象的Scale是1,那麼效果沒有問題,但是如果對象的Scale非1,那麼得到的法線並非在(-1,1)區間,轉化之後的uv值肯定也跑偏了,結果自然就不對了,如下圖。

上圖中,中間的球體Scale爲1,效果正常;左側Scale爲0.5,邊緣的採樣效果不對;右側Scale爲2,只採樣了一部分Matcap。所以,在計算後一定要保證ViewNormal處在(-1,1)區間,否則最終(0,1)區間採樣Matcap的結果肯定不對。所以,建議還是使用更加保證效果正確的方式,直接Normalize之後,就可以保證Scale之後效果也正常啦。

Matcap的效果

因爲Matcap本身的原理很簡單,我們使用的Shader就相當於是一個模板了,任意的效果都是通過替換這張Matcap圖來實現的。下面就是來看一下Matcap效果的時候啦,我從ZBrush內置的Matcap以及提供的Matcap庫中選取了幾個好玩的Matcap圖,ZBrush中的材質是ZMT格式的,我們直接可以預覽的時候把Matcap求截個圖使用(感覺方法low了點,不過這也是最簡單的方法啦)。注:對應Matcap圖實際上就與場景中的球體表現一致。

先來個小金人效果,最簡單的模擬金屬的效果:

玉石次表面效果:

另一種玉石的效果,有點大理石的趕腳:

Matcap也可以實現邊緣光效果,甚至畫一個半球也可以實現一部分的邊緣光效果:

簡單模擬反射的效果:

一種挺好玩的風格,ZBrush裏面叫fish skin(魚皮效果是什麼鬼):

Matcap優化

雖然直接使用Matcap可以用很省的消耗做出各種特殊效果,但是實際上Matcap也有一些限制,這也是Matcap的原理導致的。我們看下面的圖片:

在比較圓潤的對象上,我們可以看到較好的效果,但是在小獅子的底座上平面,還有右側的正方體上,在同樣的一個平面內,我們看不到任何變化,整個平面都是平的;而且在傳統的Matcap上有一個問題,整體的渲染效果與視角關係不算很大,有區別的情況僅在於平面的法線分佈不同導致表面效果不同,這導致了Matcap在視角方面有一些限制,僅對於固定的相機視角較好。

我們考慮一下上面我們採樣matcap的操作應該就可以瞭解這種情況的原因啦,當遇到一個平面時,這個平面上的所有的法線方向都是相同的,轉化到視空間後,方向也是相同的,再*0.5 + 0.5之後的uv值也是相同的,最終就導致了一個平面上所有的像素點採樣matcap得到是值都是相同的。爲了緩解這種情況,我們就需要不僅僅考慮視方向的問題,還需要考慮一下位置的問題,換句話說,我們可以把相機的位置也加入計算,物體相對於相機的方向也作爲matcap採樣的影響因子之一。

最簡單的,我們可以在計算方向的時候,不直接使用Normal計算,而是根據當前像素點的相機空間位置,相機空間法線,計算一個反射的方向,再用這個反射的方向進行matcap採樣即可:

float3 viewnormal = mul(UNITY_MATRIX_IT_MV, v.normal);
float3 viewPos = UnityObjectToViewPos(v.vertex);
float3 r = reflect(viewPos, viewnormal);
r = normalize(r);
o.matcapuv = r.xy * 0.5 + 0.5;

還有一種方式,是我從一個老外的blog裏面發現的,也是使用了反射方向進行計算,但是不是直接用反射方向的xy,而是通過一個公式將xy按照一個權重進行調製,得到的效果更好。公式如下:

優化後的Shader如下:

/********************************************************************
 FileName: Matcap.shader
 Description: Matcap效果
 history: 5:11:2018 by puppet_master
 https://blog.csdn.net/puppet_master
*********************************************************************/
Shader "Unlit/MatcapReflect"
{
	Properties
	{
		_MatCap ("Matcap", 2D) = "white" {}
	}
	SubShader
	{
		Tags { "RenderType"="Opaque" }
		LOD 100
 
		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"
 
			struct v2f
			{
				float2 matcapuv : TEXCOORD0;
				float4 vertex : SV_POSITION;
			};
 
			sampler2D _MatCap;
			sampler2D _GlobalMatcap;
			
			v2f vert (appdata_base v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				//乘以逆轉置矩陣將normal變換到視空間
				float3 viewnormal = mul(UNITY_MATRIX_IT_MV, v.normal);
				viewnormal = normalize(viewnormal);
				float3 viewPos = UnityObjectToViewPos(v.vertex);
				float3 r = reflect(viewPos, viewnormal);
				float m = 2.0 * sqrt(r.x * r.x + r.y * r.y + (r.z + 1) * (r.z + 1));
				o.matcapuv = r.xy / m + 0.5;
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				fixed4 mat = tex2D(_MatCap, i.matcapuv);
				return mat;
			}
			ENDCG
		}
	}
}

最終效果如下,可見,在正方體的平面上,也出現了類似次表面玉石的效果變化:

在換一張反射效果的貼圖,在Cube上也能顯示出比較好的效果,來張動圖:

動態生成Matcap

Matcap目前主要還是用於實現一些好玩的特殊效果,但是對於動態光照等效果,Matcap着實是做不到的。畢竟Matcap本身就是一種預計算好的特殊光照效果貼圖,要想隨着場景的光進行動態變化,目前本人想到的就兩種方式。第一,僅把Matcap作爲一個輸入的參數,額外按照光照計算一個權重來調製Matcap效果;另一種就是直接渲染一個球體,作爲動態的Matcap,這個球體可以使用很複雜的計算Shader,然後場景裏面其他的對象採樣Matcap。這個idea也是源自另一篇老外的blog《World Space MatCap Shading》(不過該blog作者使用的是世界空間的Normal進行的計算,結果遇到了一堆問題,個人並不是很看好這種方式)。這種方式可能會節省一些光照的計算,但是本人沒有具體測試過,所以就當玩一下啦。

下面,我們實現一下動態生成一張Matcap進行一個最簡單的diffuse計算效果,首先我們使用一個簡單的Shader如下:

	
Shader "Unlit/SimpleLight"
{

	SubShader
	{
		Tags { "RenderType"="Opaque" }
		LOD 100

		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#define UNITY_PASS_FORWARDBASE
			#pragma multi_compile_fwdbase
			
			#include "UnityCG.cginc"

			struct v2f
			{
				float4 vertex : SV_POSITION;
				float3 worldNormal : NORMAL;
			};

			
			v2f vert (appdata_base v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				
				o.worldNormal = UnityObjectToWorldNormal(v.normal);

				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				float3 lightDir = _WorldSpaceLightPos0.xyz;
				float3 normal = normalize(i.worldNormal);
				float ndotl = saturate(dot(lightDir, normal));
				return fixed4(ndotl,ndotl,ndotl,1);
			}
			ENDCG
		}
	}
}

然後我們用CommandBuffer在相機前面繪製一個Sphere到RT上作爲動態的Matcap:

var cam = GetComponent<Camera>();
matcap = RenderTexture.GetTemporary(512, 512, 24, RenderTextureFormat.Default, RenderTextureReadWrite.Default, 4);
var commandbuffer = new CommandBuffer();
commandbuffer.ClearRenderTarget(true, true, Color.black);
commandbuffer.SetRenderTarget(matcap);
commandbuffer.DrawRenderer(sphereRenderer, sphereRenderer.sharedMaterial);
cam.AddCommandBuffer(CameraEvent.BeforeForwardOpaque, commandbuffer);
Shader.SetGlobalTexture("_GlobalMatcap", matcap);

我們在場景中放置兩個模型,左側爲Matcap效果,右側爲SimpleLight效果,上面的Sphere顯示動態的Matcap,可見Matcap效果與SimpleLight效果類似:

通過這樣的一個方式,如果計算方式很複雜的光照計算,我們就可以通過Matcap進行預計算一次,然後其他所有對象採樣Matcap來達到動態的效果。

總結

本文主要實現了基本的Matcap效果,基於反射的Matcap效果,以及動態生成Matcap效果。在正式使用時Matcap可能並不會直接作爲結果輸出使用,有可能是用於某些特殊光照效果,或者特殊材質效果,配合正常的貼圖,Mask貼圖等使用。

週末又通關了一個小遊戲《12 is better than 6》,流程很短,但是很硬核,而且很有特點,下一篇的開頭又有東西寫啦!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章