標籤:

X-Ray Vision

上圖出自遊戲《恥辱2》。顯而易見,這個效果是:當存在遮擋物時,開啟寫輪眼(Och!!應該是透視眼)可以看到遮擋物之後的敵人(剩下的就是潛伏過去干翻他們)。這是一個很有趣的效果。現在讓我們開始在Unity裡面複製(簡易的)這個效果。

要完成X-Ray Vision有下面幾個要點:

  • 非透視狀態下的Shader以及透視狀態下的Shader
  • 利用Stencil處理遮擋
  • Unity的Rendering with Replaced Shaders的使用

P1 — 兩種狀態下的Shader

這兩個Shader其實很簡單(當然這裡只是為了簡潔,如果讀者有其他自己的想法都可以加到Shader中)。

非透視狀態下的Shader — 就是正常渲染人物的Shader,這裡我使用了最簡單的Phong光照模型(當然這裡可以使用更好的光照模型,甚至是PBR)。

透視狀態下的Shader — 這也就是一個簡單的外輪廓描邊,僅僅只有外輪廓區域的顏色。

代碼如下:

Passnt{nttCGPROGRAMntt#pragma vertex vertntt#pragma fragment fragntttntt#include "UnityCG.cginc"ntt#include "Lighting.cginc"nnttstruct appdatantt{ntttfloat4 vertex : POSITION;ntttfloat2 uv : TEXCOORD0;ntttfloat3 normal : NORMAL;ntt};nnttstruct v2fntt{ntttfloat2 uv : TEXCOORD0;ntttfloat4 vertex : SV_POSITION;ntttfloat4 worldPos : TEXCOORD1;ntttfloat3 worldNormal : TEXCOORD2;ntt};nnttsampler2D _MainTex;nttfloat4 _MainTex_ST;nnttfixed4 _XRayCol;nttfloat _XRayWidth;nnttv2f vert (appdata v)ntt{ntttv2f o;nttto.vertex = UnityObjectToClipPos(v.vertex);nttto.uv = TRANSFORM_TEX(v.uv, _MainTex);nttto.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);nttto.worldPos = mul(unity_ObjectToWorld, v.vertex);nntttreturn o;ntt}ntttnttfixed4 frag (v2f i) : SV_Targetntt{nttt// sample the texturentttfixed4 albedo = tex2D(_MainTex, i.uv);nntttfixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo.rgb;nntttfixed3 N = normalize(i.worldNormal);ntttfixed3 L = normalize(UnityWorldSpaceLightDir(i.worldPos));ntttfixed3 V = normalize(UnityWorldSpaceViewDir(i.worldPos));nntttfixed NdotL = saturate(dot(N, L));ntttfixed NdotV = (1 - saturate(dot(N, V))) * _XRayWidth;nntttfixed3 diffuse = _LightColor0.rgb * albedo * NdotL;nntttfixed3 xRayCol = _XRayCol.rgb * NdotV;nntttreturn fixed4(xRayCol, 1);ntt}nttENDCGnt}n

效果如下:

P2 — 利用Stencil處理遮擋

Stencil Test — 模板測試

模板測試是一個很有意思的東西。模板緩存(Stencil Buffer)是一個和屏幕大小相同的區域,對應每個像素保存了每個像素的模板值。默認狀態下模板值是0。

模板測試所做的就是,和已經存在的模板值進行比較,如果達到了我們給定的條件就能輸出這個片段,否則拋棄這個片段。

關於模板緩存和模板測試的更多資料可以參考下面:

Learn-OpenGL Stencil Test

Unity ShaderLab — Stencil

那麼利用模板測試來處理遮擋就分為兩部分:

  1. 為遮擋物對應的像素設置模板值,以確定遮擋區域(也就是需要透視的區域)
  2. 為被遮擋物提供模板值和遮擋物的模板值比較,以確保在遮擋區域顯示透視效果

遮擋物的Shader,這裡也是利用一個很簡單的Phong光照模型,只是在SubShader中加上了Stencil塊,代碼如下:

Stencilnt{nttRef 1nttComp AlwaysnttPass ReplacenttZFail Keepnt}n

這裡的意思就是,提供1與模板緩存中的模板值比較,比較總是通過並且將當前像素的模板值替換成1(通常默認是0)。

被遮擋物的Shader,我們上面已經提供了(外輪廓描邊)。這裡也是只需要調價一下Stencil塊,以及幾個特定的開關就行:

Stencilnt{nttRef 0nttComp NotEqualnt}nntZWrite OffntZTest AlwaysntBlend One Onen

這裡的意思是,提供0與模板緩衝中的模板值比較,比較的方式是不等於0時則通過模板測試。所以當碰到遮擋區域的時候,人物的模板測試會通過。然後經過不寫入深度、總是通過深度測試,以及混合模式是疊加的方式,將人物在遮擋的區域繪製出來。最終的效果如下:

P3 — Unity的Rendering with Replaced Shaders

什麼是Rendering with Replaced Shaders?

從字面上理解就是利用替換Shader來進行渲染。具體的怎麼使用Replaced Shader,讓我們一步步來了解。

首先按常規用一台相機渲染正常(不帶透視效果)的人物。

然後我們需要另一台相機來渲染透視效果的人物。

那麼問題就來了!!!怎麼用另一台相機來渲染透視效果呢?

反正肯定不是再用一個同樣的模型使用不同的材質(shader)來渲染。那麼Replaced Shader的用處就浮現出來了!!!

Replaced Shader的用處就是 — 對同一個物體使用不同的Shader進行渲染。

那麼Replaced Shader是如何找到我們需要渲染的物體從而替換他的shader的呢?

答案是:利用Tags。我們知道Unity中有許多Tags,甚至可以自定義Tags。利用Tags就能找出我們需要替換的物體!!!

關於Replaced Shader的具體說明請參考Unity官方文檔:

Rendering with Replaced Shaders

這裡我們進行簡述:

  1. 創建一台主相機正常渲染場景,其中包括我們需要透視的人物。
  2. 創建第二台相機用來渲染透視效果。這裡需要添加腳本。代碼如下:

using System.Collections;nusing System.Collections.Generic;nusing UnityEngine;nnpublic class XRay : MonoBehaviour {nntpublic Shader XRayShader;nntvoid OnEnable()nt{nttGetComponent<Camera>().SetReplacementShader(XRayShader, "XRay");nt}n}n

這裡調用了Camera類中的SetReplacementShader方法,它會看我們所提供的XRayShader中是否有"XRay"標籤。如果有,它會去找場景中的物體是否帶有和提供的XRayShader一樣的"XRay"標籤。如果找到了,那麼就替換掉!!!

所以實際上我們就是利用同一個模型,兩台不同的相機渲染出了XRay的效果。當透視Shader的模板值通過的時候,這個時候就會顯示出透視的效果。否則透視效果的渲染就會被丟棄。按正常的渲染顯示模型。

關於Replaced Shader有一個需要注意的點:

就是用來替換的Shader和被替換的Shader的屬性必須匹配,不然是渲染不出效果的!!

也就是說比如:替換的Shader中有一個Color屬性,而被替換的Shader中並沒有。這個時候是渲染不出效果的!!

有一個效率上的問題

這種方式,如果場景中有n個複雜的效果,那麼就需要多添加n個對應的渲染相機,這樣子會增加渲染壓力。不知道這是否有可行的解決方案?

希望有好想法的小夥伴們給我指點一二,十分感謝。


第一次更新

我仔細想了一想,上面的做法好像有點蠢了。其實有一種更簡單的做法。

利用兩個Pass,一個用來渲染深度大於當前像素的透視狀態,一個用來渲染深度小於等於當前像素的正常狀態!!!

雖然兩個Pass會增加DC。但是在我的場景中兩種方式最終所佔用的總DC是相同的,所以我更新了這個方式。當然隨著場景的擴大,物體的增多。這個DC量還是有待商榷的!

Shader很簡單,就是將正常和透視效果分別寫入到一個Pass中,使用不同的狀態開關。

SubShadernt{nttPassntt{ntttZTest GreaterntttZWrite OffntttBlend One OnenntttTags{ntttt"Queue" = "Transparent"ntttt"RenderType" = "Transparent"nttt}nntttCGPROGRAMnttt#pragma vertex vertnttt#pragma fragment fragntttnttt#include "UnityCG.cginc"nttt#include "Lighting.cginc"nntttstruct appdatanttt{nttttfloat4 vertex : POSITION;nttttfloat2 uv : TEXCOORD0;nttttfloat3 normal : NORMAL;nttt};nntttstruct v2fnttt{nttttfloat2 uv : TEXCOORD0;nttttfloat4 vertex : SV_POSITION;nttttfloat4 worldPos : TEXCOORD1;nttttfloat3 worldNormal : TEXCOORD2;nttt};nntttsampler2D _MainTex;ntttfloat4 _MainTex_ST;nntttfixed4 _XRayCol;ntttfloat _XRayWidth;nntttv2f vert (appdata v)nttt{nttttv2f o;ntttto.vertex = UnityObjectToClipPos(v.vertex);ntttto.uv = TRANSFORM_TEX(v.uv, _MainTex);ntttto.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);ntttto.worldPos = mul(unity_ObjectToWorld, v.vertex);nnttttreturn o;nttt}ntttntttfixed4 frag (v2f i) : SV_Targetnttt{ntttt// sample the texturenttttfixed4 albedo = tex2D(_MainTex, i.uv);nnttttfixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo.rgb;nnttttfixed3 N = normalize(i.worldNormal);nttttfixed3 L = normalize(UnityWorldSpaceLightDir(i.worldPos));nttttfixed3 V = normalize(UnityWorldSpaceViewDir(i.worldPos));nnttttfixed NdotL = saturate(dot(N, L));nttttfixed NdotV = (1 - saturate(dot(N, V))) * _XRayWidth;nnttttfixed3 diffuse = _LightColor0.rgb * albedo * NdotL;nnttttfixed3 xRayCol = _XRayCol.rgb * NdotV;nnttttreturn fixed4(xRayCol, 1);nttt}ntttENDCGntt}nnttPassntt{ntttTagsnttt{ntttt"Queue" = "Geometry-1"ntttt"RenderType" = "Opaque"ntttt"LightMode"="ForwardBase"nttt}nntttCGPROGRAMnttt#pragma vertex vertnttt#pragma fragment fragntttnttt#include "UnityCG.cginc"nttt#include "Lighting.cginc"nntttstruct appdatanttt{nttttfloat4 vertex : POSITION;nttttfloat2 uv : TEXCOORD0;nttttfloat3 normal : NORMAL;nttt};nntttstruct v2fnttt{nttttfloat2 uv : TEXCOORD0;nttttfloat4 vertex : SV_POSITION;nttttfloat3 worldNormal : TEXCOORD1;nttttfloat3 worldPos : TEXCOORD2;nttt};nntttsampler2D _MainTex;ntttfloat4 _MainTex_ST;ntttntttv2f vert (appdata v)nttt{nttttv2f o;ntttto.vertex = UnityObjectToClipPos(v.vertex);ntttto.uv = TRANSFORM_TEX(v.uv, _MainTex);ntttto.worldNormal = mul(v.normal, unity_WorldToObject);ntttto.worldPos = mul(unity_ObjectToWorld, v.vertex);nnttttreturn o;nttt}ntttntttfixed4 frag (v2f i) : SV_Targetnttt{ntttt// sample the texturenttttfixed4 albedo = tex2D(_MainTex, i.uv);nttttfixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;nnttttfixed3 N = normalize(i.worldNormal);nttttfixed3 L = normalize(UnityWorldSpaceLightDir(i.worldPos));nnttttfixed3 diffuse = _LightColor0.rgb * albedo.rgb * saturate(dot(N, L));nnttttfixed4 finalCol = fixed4(ambient + diffuse , 1);nnttttreturn finalCol;nttt}ntttENDCGntt}n

推薦閱讀:

木地板材質真沒你想像的那麼難啊
Unity5 PBR如何實現天氣系統的雪景效果
為什麼我看壞arnold渲染器的發展
FFT Ocean

TAG:渲染 | shader |