【OpenGL】Shader例項分析(九)- AngryBots中的主角受傷特效
AngryBots是Unity官方的一個非常棒的例子,很有研究價值。以前研究的時候,由於其內容豐富,一時間不知道從哪入手寫文章分析。這一段時間研究shader技術比較多一些,就從shader的這一方面開始吧。首先分析其中的一個螢幕特效:當主角受到攻擊時會出現的全屏效果(postScreenEffect),效果如下:
其實這是一種的Bloom效果,相關檔案有:MobileBloom.js 和 MobileBloom.shader;關於如何檢視這兩個檔案,請參考下圖:
JS程式碼分析
MobileBloom.js部分程式碼如下:
function OnRenderImage (source : RenderTexture, destination : RenderTexture) { #if UNITY_EDITOR FindShaders (); CheckSupport (); CreateMaterials (); #endif agonyTint = Mathf.Clamp01 (agonyTint - Time.deltaTime * 2.75f); var tempRtLowA : RenderTexture = RenderTexture.GetTemporary (source.width / 4, source.height / 4, rtFormat); var tempRtLowB : RenderTexture = RenderTexture.GetTemporary (source.width / 4, source.height / 4, rtFormat); // prepare data apply.SetColor ("_ColorMix", colorMix); apply.SetVector ("_Parameter", Vector4 (colorMixBlend * 0.25f, 0.0f, 0.0f, 1.0f - intensity - agonyTint)); // downsample & blur Graphics.Blit (source, tempRtLowA, apply, agonyTint < 0.5f ? 1 : 5); Graphics.Blit (tempRtLowA, tempRtLowB, apply, 2); Graphics.Blit (tempRtLowB, tempRtLowA, apply, 3); // apply apply.SetTexture ("_Bloom", tempRtLowA); Graphics.Blit (source, destination, apply, QualityManager.quality > Quality.Medium ? 4 : 0); RenderTexture.ReleaseTemporary (tempRtLowA); RenderTexture.ReleaseTemporary (tempRtLowB); }
知識點準備
這是一個回撥函式,是MonoBehaviour的生命週期的一部分,每一幀都會被呼叫;當這個函式被呼叫時,所有的3d渲染已經完成,渲染結果以引數source傳入到函式中,後期效果的實現就是對source的處理,並把結果整合到destination中。這個函式所在的指令碼一般繫結在Camera上。此函式只有在Unity Pro版本中才能夠使用。
static void Blit(Texture source, RenderTexture dest); static void Blit(Texture source, RenderTexture dest, Material mat, int pass = -1); static void Blit(Texture source, Material mat, int pass = -1);
這個函式就像過轉化器一樣,source圖片經過它的處理變成了dest圖片,其中材質物件mat負責演算法實施(更準確的說法:演算法是繫結到該mat上的shader來實現的。shader可以有多個pass,可以通過pass引數指定特定的shader,-1表示執行這個shader上所有的pass)。
GetTemporary獲取臨時的RenderTexture。ReleaseTemporary用來釋放指定的RenderTexture;
RenderTexture一般在GPU中實現,速度快但資源稀缺。unity內部對RenderTexture做了池化操作,以便複用之。對GetTemporary函式的呼叫其實就是獲取unity中RenderTexture的引用;當處理完之後,使用ReleaseTemporary來釋放對此RenderTexture的引用,達到複用的目的,提高效能。
JS程式碼分析
瞭解了三個知識點,上面程式碼的功能就非常清晰了,分析如下:
- a)獲取兩個渲染貼圖tempRtLowA和tempRtLowB(長寬都是原圖的1/4,用以加快渲染速度)
- b)設定Mat中Shader的引數
- c)通過Mat來處理貼圖,最終渲染到destination貼圖中,用來顯示
- d)釋放臨時的貼圖。
這裡先解釋a和c;
【步驟a】,獲取兩個貼圖,並縮小到原來的1/16(長寬都縮小為原來的1/4,面積為原來的1/16),節約了GPU記憶體,同時提高渲染速度;由於接下來的步驟是對圖片進行模糊處理(對質量要求不高),這樣做是可行的。
【步驟c】(注:呼叫Blit函式來過濾貼圖,其中最後一個數字引數是用來指代shader的pass的)
pass1 或者 pass5, 提取顏色中最亮的部分;pass2 對高亮圖片進行縱向模糊;pass3 對高亮圖片進行橫向模糊;pass0或pass4;把模糊的圖片疊加到原圖片上。
一個亮點,先經過橫向模糊,再經過縱向模糊的過程,如下圖所示(可以把這理解為“使一個點向周圍擴散的演算法”):
圖解演算法
現在的重點是【步驟c】中的shader演算法是怎麼實現的了,先圖解一下演算法:
圖1 原圖
圖2【初始化】原圖縮放成原來的1/16
圖3【步驟1】擴大高亮區域
圖4 【步驟2】縱向模糊
圖5 【步驟3】橫向模糊
圖6 【步驟4a】(原圖 + 步驟3的效果)最終疊加的效果,這個效果稱之為glow或者bloom。
圖7 【步驟4b】(原圖 + 步驟3的效果)最終疊加的效果 《===(注意:這個效果需要在步驟1中新增紅色成份)
調節步驟1中的圖片顏色強度,可以形成相應的動畫,如下圖所示:
Shader分析
接下來,我將按照上圖的序列來分析shader開始。
圖3【步驟1】擴大高亮區域
js程式碼:
Graphics.Blit (source, tempRtLowA, apply, 1);
shader程式碼:
struct v2f_withMaxCoords {
half4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
half2 uv2[4] : TEXCOORD1;
};
v2f_withMaxCoords vertMax (appdata_img v)
{
v2f_withMaxCoords o;
o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
o.uv = v.texcoord;
o.uv2[0] = v.texcoord + _MainTex_TexelSize.xy * half2(1.5,1.5);
o.uv2[1] = v.texcoord + _MainTex_TexelSize.xy * half2(-1.5,1.5);
o.uv2[2] = v.texcoord + _MainTex_TexelSize.xy * half2(-1.5,-1.5);
o.uv2[3] = v.texcoord + _MainTex_TexelSize.xy * half2(1.5,-1.5);
return o;
}
fixed4 fragMax ( v2f_withMaxCoords i ) : COLOR
{
fixed4 color = tex2D(_MainTex, i.uv.xy);
color = max(color, tex2D (_MainTex, i.uv2[0]));
color = max(color, tex2D (_MainTex, i.uv2[1]));
color = max(color, tex2D (_MainTex, i.uv2[2]));
color = max(color, tex2D (_MainTex, i.uv2[3]));
return saturate(color - ONE_MINUS_INTENSITY);
}
// 1
Pass {
CGPROGRAM
#pragma vertex vertMax
#pragma fragment fragMax
#pragma fragmentoption ARB_precision_hint_fastest
ENDCG
}
這段程式碼的作用可以描述為:當渲染某一點時,在這一點及其周圍四點(左上、右上、左下、右下)中,選取最亮的一點作為該點的顏色。具體解釋為:在vertMax的程式碼中,構造了向四個方向偏移的uv座標,結合本身uv,共5個uv,一起提交給openGL,光柵化後傳給fragmentShader使用。在fragMax中從5個uv所對應的畫素中,選取其中最大的作為顏色輸出。結果如圖3所示。
圖4 【步驟2】縱向模糊
js端
Graphics.Blit (tempRtLowA, tempRtLowB, apply, 2);
Shader端程式碼:struct v2f_withBlurCoords {
half4 pos : SV_POSITION;
half2 uv2[4] : TEXCOORD0;
};
v2f_withBlurCoords vertBlurVertical (appdata_img v)
{
v2f_withBlurCoords o;
o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
o.uv2[0] = v.texcoord + _MainTex_TexelSize.xy * half2(0.0, -1.5);
o.uv2[1] = v.texcoord + _MainTex_TexelSize.xy * half2(0.0, -0.5);
o.uv2[2] = v.texcoord + _MainTex_TexelSize.xy * half2(0.0, 0.5);
o.uv2[3] = v.texcoord + _MainTex_TexelSize.xy * half2(0.0, 1.5);
return o;
}
fixed4 fragBlurForFlares ( v2f_withBlurCoords i ) : COLOR
{
fixed4 color = tex2D (_MainTex, i.uv2[0]);
color += tex2D (_MainTex, i.uv2[1]);
color += tex2D (_MainTex, i.uv2[2]);
color += tex2D (_MainTex, i.uv2[3]);
return color * 0.25;
}
// 2
Pass {
CGPROGRAM
#pragma vertex vertBlurVertical
#pragma fragment fragBlurForFlares
#pragma fragmentoption ARB_precision_hint_fastest
ENDCG
}
這段程式碼的作用可以描述為:當渲染某一點時,在豎直方向上距其0.5和1.5個單位的四個點(上下各兩個)的顏色疊加起來,作為該點的顏色。結果如圖4所示。
圖5 【步驟3】橫向模糊 (同圖四的描述)
圖6 【步驟4a】最終疊加的效果
(原圖 + 步驟3的效果)最終疊加的效果,這個效果稱之為glow或者bloom。
js段程式碼:
apply.SetTexture ("_Bloom", tempRtLowA);
Graphics.Blit (source, destination, apply, 0);
Shader端程式碼:
struct v2f_simple {
half4 pos : SV_POSITION;
half4 uv : TEXCOORD0;
};
v2f_simple vertBloom (appdata_img v)
{
v2f_simple o;
o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
o.uv = v.texcoord.xyxy;
#if SHADER_API_D3D9
if (_MainTex_TexelSize.y < 0.0)
o.uv.w = 1.0 - o.uv.w;
#endif
return o;
}
fixed4 fragBloom ( v2f_simple i ) : COLOR
{
fixed4 color = tex2D(_MainTex, i.uv.xy);
return color + tex2D(_Bloom, i.uv.zw);
}
// 0
Pass {
CGPROGRAM
#pragma vertex vertBloom
#pragma fragment fragBloom
#pragma fragmentoption ARB_precision_hint_fastest
ENDCG
}
這段程式碼的作用可以描述為:把圖5的結果疊加到原圖上。結果如圖6所示。
Shader的完整程式碼
MobileBloom.shader:
Shader "Hidden/MobileBloom" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_Bloom ("Bloom (RGB)", 2D) = "black" {}
}
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
sampler2D _Bloom;
uniform fixed4 _ColorMix;
uniform half4 _MainTex_TexelSize;
uniform fixed4 _Parameter;
#define ONE_MINUS_INTENSITY _Parameter.w
struct v2f_simple {
half4 pos : SV_POSITION;
half4 uv : TEXCOORD0;
};
struct v2f_withMaxCoords {
half4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
half2 uv2[4] : TEXCOORD1;
};
struct v2f_withBlurCoords {
half4 pos : SV_POSITION;
half2 uv2[4] : TEXCOORD0;
};
v2f_simple vertBloom (appdata_img v)
{
v2f_simple o;
o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
o.uv = v.texcoord.xyxy;
#if SHADER_API_D3D9
if (_MainTex_TexelSize.y < 0.0)
o.uv.w = 1.0 - o.uv.w;
#endif
return o;
}
v2f_withMaxCoords vertMax (appdata_img v)
{
v2f_withMaxCoords o;
o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
o.uv = v.texcoord;
o.uv2[0] = v.texcoord + _MainTex_TexelSize.xy * half2(1.5,1.5);
o.uv2[1] = v.texcoord + _MainTex_TexelSize.xy * half2(-1.5,1.5);
o.uv2[2] = v.texcoord + _MainTex_TexelSize.xy * half2(-1.5,-1.5);
o.uv2[3] = v.texcoord + _MainTex_TexelSize.xy * half2(1.5,-1.5);
return o;
}
v2f_withBlurCoords vertBlurVertical (appdata_img v)
{
v2f_withBlurCoords o;
o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
o.uv2[0] = v.texcoord + _MainTex_TexelSize.xy * half2(0.0, -1.5);
o.uv2[1] = v.texcoord + _MainTex_TexelSize.xy * half2(0.0, -0.5);
o.uv2[2] = v.texcoord + _MainTex_TexelSize.xy * half2(0.0, 0.5);
o.uv2[3] = v.texcoord + _MainTex_TexelSize.xy * half2(0.0, 1.5);
return o;
}
v2f_withBlurCoords vertBlurHorizontal (appdata_img v)
{
v2f_withBlurCoords o;
o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
o.uv2[0] = v.texcoord + _MainTex_TexelSize.xy * half2(-1.5, 0.0);
o.uv2[1] = v.texcoord + _MainTex_TexelSize.xy * half2(-0.5, 0.0);
o.uv2[2] = v.texcoord + _MainTex_TexelSize.xy * half2(0.5, 0.0);
o.uv2[3] = v.texcoord + _MainTex_TexelSize.xy * half2(1.5, 0.0);
return o;
}
fixed4 fragBloom ( v2f_simple i ) : COLOR
{
fixed4 color = tex2D(_MainTex, i.uv.xy);
return color + tex2D(_Bloom, i.uv.zw);
}
fixed4 fragBloomWithColorMix ( v2f_simple i ) : COLOR
{
fixed4 color = tex2D(_MainTex, i.uv.xy);
half colorDistance = Luminance(abs(color.rgb-_ColorMix.rgb));
color = lerp(color, _ColorMix, (_Parameter.x*colorDistance));
color += tex2D(_Bloom, i.uv.zw);
return color;
}
fixed4 fragMaxWithPain ( v2f_withMaxCoords i ) : COLOR
{
fixed4 color = tex2D(_MainTex, i.uv.xy);
color = max(color, tex2D (_MainTex, i.uv2[0]));
color = max(color, tex2D (_MainTex, i.uv2[1]));
color = max(color, tex2D (_MainTex, i.uv2[2]));
color = max(color, tex2D (_MainTex, i.uv2[3]));
return saturate(color + half4(0.25,0,0,0) - ONE_MINUS_INTENSITY);
}
fixed4 fragMax ( v2f_withMaxCoords i ) : COLOR
{
fixed4 color = tex2D(_MainTex, i.uv.xy);
color = max(color, tex2D (_MainTex, i.uv2[0]));
color = max(color, tex2D (_MainTex, i.uv2[1]));
color = max(color, tex2D (_MainTex, i.uv2[2]));
color = max(color, tex2D (_MainTex, i.uv2[3]));
return saturate(color - ONE_MINUS_INTENSITY);
}
fixed4 fragBlurForFlares ( v2f_withBlurCoords i ) : COLOR
{
fixed4 color = tex2D (_MainTex, i.uv2[0]);
color += tex2D (_MainTex, i.uv2[1]);
color += tex2D (_MainTex, i.uv2[2]);
color += tex2D (_MainTex, i.uv2[3]);
return color * 0.25;
}
ENDCG
SubShader {
ZTest Always Cull Off ZWrite Off Blend Off
Fog { Mode off }
// 0
Pass {
CGPROGRAM
#pragma vertex vertBloom
#pragma fragment fragBloom
#pragma fragmentoption ARB_precision_hint_fastest
ENDCG
}
// 1
Pass {
CGPROGRAM
#pragma vertex vertMax
#pragma fragment fragMax
#pragma fragmentoption ARB_precision_hint_fastest
ENDCG
}
// 2
Pass {
CGPROGRAM
#pragma vertex vertBlurVertical
#pragma fragment fragBlurForFlares
#pragma fragmentoption ARB_precision_hint_fastest
ENDCG
}
// 3
Pass {
CGPROGRAM
#pragma vertex vertBlurHorizontal
#pragma fragment fragBlurForFlares
#pragma fragmentoption ARB_precision_hint_fastest
ENDCG
}
// 4
Pass {
CGPROGRAM
#pragma vertex vertBloom
#pragma fragment fragBloomWithColorMix
#pragma fragmentoption ARB_precision_hint_fastest
ENDCG
}
// 5
Pass {
CGPROGRAM
#pragma vertex vertMax
#pragma fragment fragMaxWithPain
#pragma fragmentoption ARB_precision_hint_fastest
ENDCG
}
}
FallBack Off
}
參考文獻
《Unity Shaders and Effects Cookbook》的章節: