【ShaderLab实例笔记】Overwatch Shield - 守望先锋护盾特效制作笔记
教程鏈接:Overwatch Shield
項目鏈接:OverwatchShieldTemplate
Pipeline & Shader:Built-in,Unlit
本文是對 Overwatch Shield 學(xué)習(xí)過程的記錄和總結(jié),不是完全的翻譯,更多的細(xì)節(jié)和圖文建議跳轉(zhuǎn)原博
效果分析
使用守望先鋒中Reinhardt的護(hù)盾作為參考
整體觀察
可以看出護(hù)盾是半透明的,帶有一層基色,可以微微照亮周邊區(qū)域
繞著護(hù)盾走可以發(fā)現(xiàn),護(hù)盾的背面也可以顯示(關(guān)閉Cull),并且有明顯的厚度
蜂窩紋理
蜂窩會從中心沿x軸方向搏動,仔細(xì)觀察還可以發(fā)現(xiàn)蜂窩并不是同時搏動的
蜂窩狀電流
電流沿著蜂窩狀邊緣向外擴(kuò)散,不能難發(fā)現(xiàn)擴(kuò)散的形狀一個菱形
所以這個效果其實是一個從護(hù)盾中心,從點變大的菱形框點亮蜂窩邊緣的過程
固定邊緣
仔細(xì)看就會發(fā)現(xiàn)邊緣效果由兩部分組成:固定邊緣、相交邊緣
相交邊緣
相交邊緣的效果基本和固定邊緣一樣
效果總結(jié)
項目設(shè)置
場景方面,模擬了視頻(圖片)中的物體:
護(hù)盾與地面相交,有物體穿過護(hù)盾,方便測試效果
另外項目的色彩空間使用了線性空間,因為線性空間的精度更高
更多關(guān)于顏色空間的內(nèi)容,可以查看GAMMA AND LINEAR SPACE - WHAT THEY ARE AND HOW THEY DIFFER和Unity User Manual Color space
Shader編寫
基礎(chǔ)效果
目標(biāo)效果:整體透明,帶有基色,Cull Off
沒啥東西,改 Tag,關(guān) Cull,設(shè)置 Blend,完事
基礎(chǔ)效果圖
?
蜂窩搏動
目標(biāo)效果:蜂窩紋理從護(hù)盾中心沿x軸搏動,帶有一定先后順序
效果拆分:
1.蜂窩紋理 + 整體呼吸效果
fixed4 frag (v2f i) : SV_Target {// 蜂窩圖案fixed4 pulseTex = tex2D(_PulseTex, i.uv);fixed4 pulseTerm = pulseTex * _Color * _PulseIntensity;// 呼吸效果pulseTerm *= abs(sin(_Time.y * _PulseTimeScale));return fixed4(_Color.rgb + pulseTerm.rgb, _Color.a); }
?
2.打亂呼吸順序
打亂順序的方法有很多,因為這里使用的蜂窩紋理剛好有深淺變化,可以直接用來作為決定呼吸順序的因子
這里的代碼會讓 r 通道越大的地方亮的越早
fixed4 frag (v2f i) : SV_Target {// 蜂窩圖案fixed4 pulseTex = tex2D(_PulseTex, i.uv);fixed4 pulseTerm = pulseTex * _Color * _PulseIntensity;float breath = _Time.y * _PulseTimeScale;float pulseOffset = pulseTex.r * _PulseTexOffsetScale;// 隨機(jī)呼吸效果pulseTerm *= abs(sin(breath + pulseOffset));return fixed4(_Color.rgb + pulseTerm.rgb, _Color.a); }為了防止 sin() 的負(fù)值導(dǎo)致奇怪的結(jié)果出現(xiàn),需要濾去負(fù)值
同時也不希望負(fù)值直接被處理為常數(shù)導(dǎo)致圖案無變化,使用了 abs()
?
3.呼吸效果沿x軸擴(kuò)散
由于制作模型時,將模型的中心點設(shè)置在了正中心(默認(rèn)是底部中心),可以直接取模型空間的x坐標(biāo)來確定頂點與中心軸的距離
fixed4 frag (v2f i) : SV_Target {// 蜂窩圖案fixed4 pulseTex = tex2D(_PulseTex, i.uv);fixed4 pulseTerm = pulseTex * _Color * _PulseIntensity;float breath = _Time.y * _PulseTimeScale;float pulseOffset = pulseTex.r * _PulseTexOffsetScale;float horizontalDist = abs(i.posOS.x);float xOffset = horizontalDist * _PulsePosScale;// 沿x軸擴(kuò)散的隨機(jī)呼吸效果pulseTerm *= abs(sin(breath + pulseOffset - xOffset));return fixed4(_Color.rgb + pulseTerm.rgb, _Color.a); }?
電流脈沖
目標(biāo)效果:蜂窩電流從護(hù)盾中心按菱形擴(kuò)散
大體上思路和蜂窩搏動效果有很多相似的地方
效果拆分:
1.蜂窩電流 + 整體呼吸效果
fixed4 frag (v2f i) : SV_Target {// 蜂窩搏動// ...// 蜂窩電流fixed4 hexEdgeTex = tex2D(_HexEdgeTex, i.uv);fixed4 hexEdgeTerm = hexEdgeTex * _HexEdgeColor * _HexEdgeIntensity;// 呼吸float edgeBreath = _Time.y * _HexEdgeTimeScale;hexEdgeTerm *= saturate(sin(edgeBreath));// 只看電流// return fixed4(_Color.rgb + pulseTerm.rgb + hexEdgeTerm.rgb, _Color.a);return fixed4(hexEdgeTerm.rgb, _Color.a); }1.5呼吸周期調(diào)整
現(xiàn)在的呼吸周期使用的是 saturate(sin(x)) 函數(shù),呼吸效果如下圖:
圖中當(dāng)曲線位于 x 軸上方時,電流才會出現(xiàn),到達(dá) 1 時達(dá)到最亮,隨后變暗直到消失,開始下一輪循環(huán)
由于現(xiàn)在的曲線是一個單純的 sin 曲線,電流亮和不亮的時間是相同的
將曲線向下平移,就會讓亮的時間變短,暗的時間變長
可以將這個下移量設(shè)置為變量 _HexEdgeWidthModifier,方便調(diào)試
但是根據(jù)實現(xiàn)效果看,電流只是淡淡的閃一下就滅了,效果非常不明顯
這是因為在 x 軸上方的部分因為被下移,而無法到達(dá) 1 值,導(dǎo)致了電流顏色一直無法到達(dá)飽和的效果
解決方法是把它拉長到 1:
(1 - _HexEdgeWidthModifier)/ edgeNormalizer = 1,
edgeNormalizer = 1 - _HexEdgeWidthModifier
?
2.菱形光從護(hù)盾中心擴(kuò)散
在蜂窩搏動效果中,使用的是 sin 沿 x 軸擴(kuò)散
這里要使用一個菱形擴(kuò)散(“ 菱形 ” 雖然不準(zhǔn)確,方便起見先這么叫)
由于 x 對應(yīng)的 y 不唯一,這個菱形無法使用函數(shù)表示
但是,它可以表示為這個式子:abs(y) + abs(x) = 1,等式右邊的數(shù)字越大,菱形就越大
這意味著如果計算 sin(abs(x)+abs(y)),就可以構(gòu)造一個邊長不斷在一定區(qū)間變化的菱形的圖案,只要加入時間參數(shù)就可以讓它動起來
補(bǔ)充
上面的加粗字是這篇筆記全文唯一一個完完全全的翻譯,沒有一點自己的理解
因為坦白說,這句話壓根就沒理解…
想了好久也想不明白這到底是什么從 0 突變到 2048 的因果聯(lián)系啊…???
?
Desmos 沒辦法表示二維圖像,好在還有Gooogle計算器:
有了圖果然就好理解多了,這波啊,絕對是先有圖,后有粗體字
這里的verticalDist取的是z方向的值,因為在這個模型的坐標(biāo)系中,上方向是z
把最后的return修改一下,電流這里也算是完成了
?
輪廓線
目標(biāo)效果:越邊緣顏色越實
這個效果相對于前面的兩個很好實現(xiàn),只要使用漸變邊緣的遮罩紋理,再進(jìn)行一些調(diào)整就可以了
大粗邊緣太傻太悶了,要想辦法把邊緣變薄
如果不想改貼圖的話,就需要進(jìn)行一些數(shù)學(xué)計算,比如pow()
需要注意的是,因為顏色的值域是[0, 1],_EdgeExponent越大意味著邊緣遮罩將更快的達(dá)到1并保持恒定,導(dǎo)致邊緣看起來缺乏過渡
?
相交邊緣
目標(biāo)效果:與其他物體相交處有高亮
雖然把固定邊緣和相交邊緣分成了不同的效果制作,為了讓最終的效果不突兀,還是需要保證固定邊緣和相交邊緣看起來就像是護(hù)盾的連續(xù)邊界一樣
把它們分成兩部制作,只是因為兩個邊緣的形成原因不一樣,需要使用不同的方法來判定邊緣:
一個是護(hù)盾原有的邊緣,固定存在(邊緣紋理);一個是護(hù)盾與其他物體相交形成的邊緣,需要根據(jù)相交情況進(jìn)行判斷(深度紋理)
?
原理分析
為什么判斷相交情況需要深度紋理呢?
首先,當(dāng)兩個物體相交時,它們之間的距離會變成0
而深度值指的是點到相機(jī)(屏幕)的距離
那么只要護(hù)盾上某點的深度值和環(huán)境中其他物體上的點的深度值差值足夠小,就會表明這兩個點足夠接近,足以產(chǎn)生相交線
護(hù)盾的深度值可以在相機(jī)空間計算獲得,環(huán)境的深度值可以通過采樣相機(jī)渲染的深度紋理獲得
補(bǔ)充
根據(jù)之前學(xué)過的陰影章節(jié)可以知道,如果 Shader (和它的 FallBack)中沒有 ShadowCaster Pass,它所在的物體就不會出現(xiàn)在深度紋理中
本例的 Shader 中不包含 默認(rèn)的 ShadowCaster Pass 和 FallBack,所以深度紋理中沒有存儲護(hù)盾的深度值
?
攝像機(jī)深度紋理獲取以及采樣
想要獲得相機(jī)的深度紋理,需要在相機(jī)上掛一個腳本,確保它渲染深度紋理:
void OnEnable() {GetComponent<Camera>().depthTextureMode = DepthTextureMode.DepthNormals; }如果設(shè)置正確,在相機(jī)組件的Inspector面板上,會出現(xiàn)下面的提示:
然后就可以在Shader中使用這個紋理了
這里需要注意的是,此時相機(jī)渲染出來的深度紋理是在屏幕空間中得到的,因此也需要在屏幕空間進(jìn)行采樣
o.posSS = ComputeScreenPos(o.pos);// UnityCG.cginc中的ComputeScreenPos大意 inline float4 ComputeScreenPos (float4 pos) {float4 o = pos * 0.5f;o.xy = float2(o.x, o.y*_ProjectionParams.x) + o.w;o.zw = pos.zw;return o; }其中 _ProjectionParams.x 取值為 1.0 或 -1.0,取決于當(dāng)前平臺的屏幕坐標(biāo)系的 y 軸是向上還是向下,其他的具體原理可以見之前的計算機(jī)圖形學(xué)筆記,齊次裁剪空間到屏幕空間的變換
?
護(hù)盾深度值計算
想要計算深度值,必須先計算相機(jī)空間的頂點位置,它的 z 值就是該點的深度
因為在片元著色器中進(jìn)行比較時,兩個深度值都為正更方便,所以把這個深度值乘以 -1(相機(jī)空間中 z 的反方向指向相機(jī)的朝向,在相機(jī)面前的點的 z 都為負(fù)數(shù))
o.depth = -mul(UNITY_MATRIX_MV, v.vertex).z;現(xiàn)在,這個深度值是頂點到相機(jī)的實際距離
但是深度紋理存儲的值是顏色,值域是 [0 ,1](在近平面上為 0,在遠(yuǎn)平面上為 1)
所以為了能正確比較兩個深度值,將頂點深度值重映射到 [0,1],需要除以相機(jī)到遠(yuǎn)平面的距離(_ProjectionParams.w = 1 / FarPlane)
補(bǔ)充
其實這里數(shù)學(xué)上并不嚴(yán)謹(jǐn),頂點到相機(jī)的實際距離值域應(yīng)該是 [near,far],重映射的結(jié)果應(yīng)該是 (z - near) / (far - near),而不是簡單的 z / far
但是由于本例中相機(jī)的近平面為 0.1,所以在這里沒有太大影響,不如簡化計算直接取 z / far
?
相交效果制作
將上面兩步得到的屏幕空間坐標(biāo)、護(hù)盾深度值傳遞到 Fragment Shader,然后就開始制作效果
首先是求深度紋理中環(huán)境深度值,和護(hù)盾深度值的差值
float diff = tex2D(_CameraDepthNormalsTexture, i.posSS.xy).r - i.depth;然而事情并沒有這么簡單,還有兩個問題要處理:
解決方法倒是很簡單:
// DecodeFloatRG為UnityCG.cginc中自帶的輔助函數(shù) float diff = DecodeFloatRG(tex2D(_CameraDepthNormalsTexture, i.posSS.xy / i.posSS.w).zw) - i.depth;補(bǔ)充
注意 diff 的值是非負(fù)的
雖然兩個距離做差應(yīng)該有三種情況(>、<、= 0),但被減項是 “ 場景中不透明物體的深度值 ”,意味著當(dāng) diff < 0 時(即護(hù)盾深度值較大),護(hù)盾本身被不透明物體遮擋,不會被渲染;只有在 diff > 0 時(即護(hù)盾深度值較小),護(hù)盾才會被渲染。因此,從渲染結(jié)果的角度看,diff ≥ 0,是非負(fù)的
?
關(guān)于透視除法,可以復(fù)習(xí)之前的計算機(jī)圖形學(xué)筆記,或者畫個側(cè)面的視錐體切一切
理論上講,現(xiàn)在已經(jīng)得到了環(huán)境深度值和護(hù)盾深度值的非負(fù)差值(值域 [0,1]),只要取 1 - diff,就可以得到一個相交邊緣的遮罩
fixed4 frag (v2f i) : SV_Target {// 蜂窩搏動、電流脈沖、固定邊緣// ...float diff = DecodeFloatRG(tex2D(_CameraDepthNormalsTexture, i.posSS.xy / i.posSS.w).zw) - i.depth;float intersectGradient = 1 - diff;return intersectGradient; }但是事情好像沒有這么簡單:
這是因為 diff 的值太小了,沒能將非相交邊緣的部分減到 0,可以通過乘上一個常量來增大這個值,比如 20
這里將 intersectGradient 的值改成了 1 - min(diff, 1.0);,是因為1 - diff; 無法保證 diff 乘了 20 后 intersectGradient 依舊 > 0
雖然負(fù)值在普通 Shader 中會顯示為黑色,但是在修改過 BlendMode 的 Shader 中,負(fù)值顏色依然可以顯示,因此必須防止負(fù)值出現(xiàn)
修改后問題看起來解決了
但是這個方法并不通用,因為當(dāng)改變相機(jī)的遠(yuǎn)平面時,這個遮罩也會跟著改變
這其實是因為在一開始計算護(hù)盾深度值的時候,做了一個 “ 除以相機(jī)到遠(yuǎn)平面的距離 ” 的操作
當(dāng)時這么做的理由是,z 值是距離,紋理采樣是顏色,值域不同
既然現(xiàn)在發(fā)現(xiàn),統(tǒng)一成顏色值域的話會出問題,那干脆就都整成距離吧…
所以解決方法是給差值 “ 乘以相機(jī)到遠(yuǎn)平面的距離 ”
fixed4 frag (v2f i) : SV_Target {// 蜂窩搏動、電流脈沖、固定邊緣// ...float diff = DecodeFloatRG(tex2D(_CameraDepthNormalsTexture, i.posSS.xy / i.posSS.w).zw) - i.depth;diff *= _ProjectionParams.z;float intersectGradient = 1 - min(diff, 1.0);return intersectGradient; }這下就完全不受相機(jī)設(shè)置的影響了,因為它就是一個絕對的距離,不是一個基于遠(yuǎn)平面參數(shù)的 Range(0, 1)
推薦直接從前面就改成使用距離計算,可以節(jié)省一點計算量…
前面的筆記就不改了,反正栽坑爬坑也是思考的一部分…
雖然想要兩種邊緣無縫連接,計算相交線的一些參數(shù)(指數(shù)、強(qiáng)度)還是設(shè)置成了與固定邊緣不一樣的變量參數(shù)
圖中護(hù)盾材質(zhì)的固定邊緣和相交邊緣的強(qiáng)度、指數(shù)使用的是同樣的值
?
自發(fā)光
目標(biāo)效果:護(hù)盾可以微微照亮周邊
因為護(hù)盾的自發(fā)光是整體效果,可以把它交給后處理
給相機(jī)添加 Post Process Layer 和 Post Process Volume
在 Post Process Layer 中,選擇 Default Layer,反走樣選擇 FXAA
補(bǔ)充
正常情況下應(yīng)該對需要進(jìn)行不同后處理的物體在 Inspector 設(shè)置 Layer 進(jìn)行分層,但是這里只是為了實現(xiàn)一個效果,而且還是整體的效果,所以可以忽略
在 Post Process Volume 中,勾選 is Global,創(chuàng)建一個新的 Profile,添加Bloom效果,調(diào)節(jié) Bloom 強(qiáng)度
?
優(yōu)化
將使用同一套UV的多張灰度紋理存儲進(jìn)一張紋理,通過.r、.g、b、.a使用對應(yīng)通道的紋理
一張紋理有RGBA四個通道,所以一張紋理最多可以存4張灰度圖
R:蜂窩邊緣,G:蜂窩紋理,B:護(hù)盾邊緣
這是一個很常見的優(yōu)化手段,有很多優(yōu)點:
簡單來說就是節(jié)省空間,提升速度,簡化操作
?
總結(jié)
完整 Shader
Shader "Lexdev/CaseStudies/OverwatchShield" {Properties{_HexTex("R:HexEdge G:HexPulse B:Edge", 2D) = "white" {}_Color ("Color", Color) = (1,1,1,1)[Header(Hex Pulse)] _PulseIntensity ("Hex Pulse Intensity", float) = 3.0_PulseTimeScale("Hex Pulse Time Scale", float) = 2.0_PulsePosScale("Hex Pulse Position Scale", float) = 50.0_PulseTexOffsetScale("Hex Pulse Texture Offset Scale", float) = 1.5[Header(Electronic Pulse)]_HexEdgeColor("Hex Edge Color", COLOR) = (0,0,0,0)_HexEdgeIntensity("Hex Edge Intensity", float) = 2.0_HexEdgeTimeScale("Hex Edge Time Scale", float) = 2.0_HexEdgeWidthModifier("Hex Edge Width Modifier", Range(0,1)) = 0.8_HexEdgePosScale("Hex Edge Position Scale", float) = 80.0[Header(Edge)]_EdgeIntensity("Edge Intensity", float) = 10.0_EdgeExponent("Edge Falloff Exponent", float) = 6.0_IntersectIntensity("Intersection Intensity", float) = 10.0_IntersectExponent("Intersection Falloff Exponent", float) = 6.0}SubShader{Pass{Tags {"RenderType" = "Transparent" "Queue" = "Transparent"}Cull offBlend SrcAlpha oneHLSLPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"sampler2D _HexTex; float4 _HexTex_ST;float4 _Color;float _PulseIntensity;float _PulseTimeScale;float _PulsePosScale;float _PulseTexOffsetScale;float4 _HexEdgeColor;float _HexEdgeIntensity;float _HexEdgeTimeScale;float _HexEdgeWidthModifier;float _HexEdgePosScale;float _EdgeIntensity;float _EdgeExponent;// Camera rendered depth texture, must use the namesampler2D _CameraDepthNormalsTexture;float _IntersectIntensity;float _IntersectExponent;struct appdata{float4 vertex : POSITION;float2 uv : TEXCOORD0;};struct v2f{float4 pos : SV_POSITION;float2 uv : TEXCOORD0;float4 posOS : TEXCOORD1;float4 posSS : TEXCOORD2;float depth : TEXCOORD3;};v2f vert (appdata v){v2f o;o.pos = UnityObjectToClipPos(v.vertex);o.uv = TRANSFORM_TEX(v.uv, _HexTex);o.posOS = v.vertex;o.posSS = ComputeScreenPos(o.pos);// Z-Distanceo.depth = - mul(UNITY_MATRIX_MV, v.vertex).z;return o;}fixed4 frag (v2f i) : SV_Target{fixed4 hexTex = tex2D(_HexTex, i.uv);fixed pulseTex = hexTex.g;fixed hexEdgeTex = hexTex.r;fixed edgeTex = hexTex.b;// Hex pulsefixed3 pulseTerm = pulseTex * _Color * _PulseIntensity;float breath = _Time.y * _PulseTimeScale;float pulseOffset = pulseTex * _PulseTexOffsetScale;float horizontalDist = abs(i.posOS.x);float xOffset = horizontalDist * _PulsePosScale;pulseTerm *= abs(sin(breath + pulseOffset - xOffset));// Electronic pulsefixed3 hexEdgeTerm = hexEdgeTex * _HexEdgeColor * _HexEdgeIntensity;float edgeBreath = _Time.y * _HexEdgeTimeScale;float edgeNormalizer = 1 - _HexEdgeWidthModifier;float verticalDist = abs(i.posOS.z); // Z-Up in Object-Spacefloat diamondPattern = (horizontalDist + verticalDist) * _HexEdgePosScale;hexEdgeTerm *= saturate(sin(diamondPattern - edgeBreath) - _HexEdgeWidthModifier) / edgeNormalizer;// Z-Distancefloat diff = DecodeFloatRG(tex2D(_CameraDepthNormalsTexture, i.posSS.xy / i.posSS.w).zw) * _ProjectionParams.z - i.depth;float intersectGradient = 1 - min(diff, 1.0f);// Edge & Intersectionfixed3 edgeTerm = pow(edgeTex, _EdgeExponent) * _Color * _EdgeIntensity;fixed3 intersectTerm = pow(intersectGradient, _IntersectExponent) * _Color * _IntersectIntensity;return fixed4(_Color.rgb + pulseTerm + hexEdgeTerm + edgeTerm + intersectTerm, _Color.a);}ENDHLSL}} }基本思路總結(jié)
總的來說效果可以分為兩大部分:圖案和邊緣
圖案部分可以拆解為沿 x 軸擴(kuò)散的蜂窩圖案,和以菱形向外擴(kuò)大的電流
邊緣部分可以拆分為護(hù)盾的固有邊緣,和護(hù)盾與其他物體相交產(chǎn)生的邊緣
圖案部分的計算是先用顏色、紋理、強(qiáng)度因子得到基礎(chǔ)圖形,再通過對時間進(jìn)行數(shù)學(xué)運(yùn)算獲得圖形遮罩,二者相乘得到動畫
邊緣部分的計算是采樣紋理獲得遮罩,再通過對顏色進(jìn)行數(shù)學(xué)計算,二者相乘得到邊緣
?
雜七雜八總結(jié)
平移 sin 函數(shù)可以打算 > 0 和 < 0 的長度平衡;可能需要對平移后的值做矯正
abs(x) + abs(y) = n(常)是中心在原點的斜放正方形
sin(abs(x) + abs(y)) 是s&fja@inc*ud^ishd(#svd%abaabaaba
有奇奇怪怪的非二維函數(shù)可以扔到Google里看圖像
采樣相機(jī)渲染的深度圖時,需要先做一個透視除法矯正透視導(dǎo)致的歪斜
?
一點題外話
感覺撿到寶了,這個博主真的好良心啊
博客里一共有5個教程,目前算是跟著學(xué)完了其中兩個最感興趣的效果(Gears Hammer of Dawn、Overwatch Shield),受益良多,慢慢的開始明白拆解思路、參數(shù)設(shè)置思路了
雖然依然是數(shù)學(xué)渣一個,但是還是很開心終于開始慢慢體會到數(shù)學(xué)的神奇了
哎…失去了 gif 感覺放圖的樂趣都沒了QWQ
總結(jié)
以上是生活随笔為你收集整理的【ShaderLab实例笔记】Overwatch Shield - 守望先锋护盾特效制作笔记的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 我同意 三江方士 对 哥德巴赫猜想 的
- 下一篇: 第三方士大夫特人以及法国恢复规划