ShadowGun 圖形技術分析
ShadowGun雖然是2011年的移動平台的遊戲demo,但是裡面的很多優化技巧到現在來看都是很值得學習的,畢竟是上過西瓜大會的。
網上現存的兩份代碼一個是shadow gun sample level,一個遊戲場景,沒法玩,只有一個攝像機動畫,asset store上已經找不到了,另外一個是Shadowgun: Deadzone GM"s Kit,帶伺服器,可以玩,asset store上還可以下載到。
下面就通過閱讀demo中的代碼來一起學習下。
飄動的旗幟
用的就是GPUGems裡面的技術Vegetation Procedural Animation and Shading in Crysis,基本原理就是在mesh的頂點色中刷入權重,利用GPU頂點動畫來模擬布料被風吹的效果。
在maya里看下mash的頂點色
Shader裡面
輸入的參數
Properties { _MainTex ("Base (RGB) Gloss (A)", 2D) = "white" {} //颳風的方向(世界坐標系下) _Wind("Wind params",Vector) = (1,1,1,1) //風的頻率 _WindEdgeFlutter("Wind edge fultter factor", float) = 0.5 //風的頻率的縮放 _WindEdgeFlutterFreqScale("Wind edge fultter freq scale",float) = 0.5}
_Time是Unity的一個內置 float4變數(t/20, t,t*2, t*3),專門用來做shader動畫的,
看下vert裡面的關鍵代碼
//計算風的一些參數
//計算風的一些參數float4 windParams = float4(0,_WindEdgeFlutter,bendingFact.xx);float2 windTime = _Time.y * float2(_WindEdgeFlutterFreqScale,1);float4 mdlPos = AnimateVertex2(v.vertex,v.normal,windParams,wind,windTime);//mvp矩陣變換o.pos = mul(UNITY_MATRIX_MVP,mdlPos);
所以最核心的函數就是AnimateVertex2,看下它是怎麼將模型裡面的位置v.vertex轉換到被風吹動的mdlPos。
inline float4 AnimateVertex2(float4 pos, float3 normal, float4 animParams,float4 wind,float2 time){ // animParams stored in color // animParams.x = branch phase // animParams.y = edge flutter factor // animParams.z = primary factor // animParams.w = secondary factor float fDetailAmp = 0.1f; float fBranchAmp = 0.3f; // Phases (object, vertex, branch) float fObjPhase = dot(_Object2World[3].xyz, 1); float fBranchPhase = fObjPhase + animParams.x; float fVtxPhase = dot(pos.xyz, animParams.y + fBranchPhase); // x is used for edges; y is used for branches float2 vWavesIn = time.yy + float2(fVtxPhase, fBranchPhase ); // 1.975, 0.793, 0.375, 0.193 are good frequencies float4 vWaves = (frac( vWavesIn.xxyy * float4(1.975, 0.793, 0.375, 0.193) ) * 2.0 - 1.0); vWaves = SmoothTriangleWave( vWaves ); float2 vWavesSum = vWaves.xz + vWaves.yw; // Edge (xz) and branch bending (y) float3 bend = animParams.y * fDetailAmp * normal.xyz; bend.y = animParams.w * fBranchAmp; pos.xyz += ((vWavesSum.xyx * bend) + (wind.xyz * vWavesSum.y * animParams.w)) * wind.w; // Primary bending // Displace position pos.xyz += animParams.z * wind.xyz; return pos;}
關鍵思想是分層blend,首先計算了由主體到枝幹再到頂點的震動係數,edge指旗子的邊緣和自身xz方向的震動,branch指的是旗子整體的y方向的上下移動,接下來用了一些很trick的方法算出了一個float2的位移值,這個值就是頂點的位置,然後是將頂點的位移blend到主幹上去,接著是主幹上的位移blend到代碼有點不講道理,最後再把結果在風的方向上位移一定係數的距離。
UVAnimation
UVAnimation可以分為三個講,滾滾濃煙,分層滾動天空盒,水面波紋
先說最簡單的天空盒,就是兩套UV速度,以不同的速率變化
o.uv = TRANSFORM_TEX(v.texcoord.xy,_MainTex) + frac(float2(_ScrollX, _ScrollY) * _Time);o.uv2 = TRANSFORM_TEX(v.texcoord.xy,_DetailTex) + frac(float2(_Scroll2X, _Scroll2Y) * _Time);
最後又疊了一個顏色用來調節明暗關係。
fixed4 frag (v2f i) : COLOR{ fixed4 o; fixed4 tex = tex2D (_MainTex, i.uv); fixed4 tex2 = tex2D (_DetailTex, i.uv2); o = (tex * tex2) * i.color; return o;}
晚上竟然又月亮。
滾滾濃煙
還是用了頂點色
看下Mesh
地下的紅色,和煙的顏色疊起來,表現火焰的感覺。
貼圖是兩張不同的煙,用來表現層次感。
Shader和天空盒的基本一致。
不要覺得上面兩個shader比較簡單就沒人用了,可以自習對比下cfm的運輸船
「Volumetric」 effects
所謂的體效果包括了Glow,Light Shafts,Fog Plane,Emissive BillBoards
為了模擬光從窗戶投射進來,用了一個透明的片來表現
但不是單純地半透明片,它是View distance based fade out,有下面兩個特點
1) 隨著視角的接近,透明的程度變大,離得特別遠得時候,透明度也會變大
2) Mesh的位置會隨著攝像機的位置變化,接近的時候有一種推開的感覺(減少overdraw)
減少overdraw的同時,規避了透明片插在攝像機里的問題。
都是vertex shader 乾的
核心的代碼
float3 viewPos = mul(UNITY_MATRIX_MV,v.vertex);float dist = length(viewPos);float nfadeout = saturate(dist / _FadeOutDistNear);float ffadeout = 1 - saturate(max(dist - _FadeOutDistFar,0) * 0.2);
關於saturate函數:camps the specified value within the range of 0 to 1.
簡單的說就是跟據面片到攝像機的距離計算出淡入淡出的係數。具體計算可以參考這裡
面片塗了頂點色
在計算位置的時候會根據alpha值來計算推開的距離
float4 vpos = v.vertex;vpos.xyz -= v.normal * saturate(1 - nfadeout) * v.color.a * _ContractionAmount;
官方的說法是這樣
Vertex color alpha determines which vertices are moveable and which are not (in our case, vertices with black alpha stays, those with white alpha moves).
Vertex normal determines the direction of movement.
The shader then evaluates distance to the viewer and handles surface fade in/out appropriately.
為了實現這些效果,渲染了一大推的半透明物體,在移動平台上,會引起嚴重的overdraw。為了解決overdraw的問題,做了下面幾點
1. 使用最簡單的fragmentshader,基本上就只採樣一張貼圖。如果插值的結果不太好就用密一些的網格。
2. 減少半透明的面積,這個在shader裡面已經體現了
還有幾個用來模擬燈的地方
特點是會隨機閃動。
Mesh方面還是刷了頂點色
插在面片上的兩個長條三角形目測是為了防止在攝像機靠近的時候被culling掉。
閃動的原理是在vertexshader中利用sin函數計算出一個隨機係數乘以o.color.
具體的計算代碼如下
float fracTime = fmod(time,_TimeOnDuration + _TimeOffDuration);float wave = smoothstep(0,_TimeOnDuration * 0.25,fracTime) * (1 - smoothstep(_TimeOnDuration * 0.75,_TimeOnDuration,fracTime));float noiseTime = time * (6.2831853f / _TimeOnDuration);float noise = sin(noiseTime) * (0.5f * cos(noiseTime * 0.6366f + 56.7272f) + 0.5f);float noiseWave = _NoiseAmount * noise + (1 - _NoiseAmount);wave = _NoiseAmount < 0.01f ? wave : noiseWave;o.color = nfadeout * _Color * _Multiplier * wave;
具體的原理可以參考這一篇的分析
Billboarding
用來表現Glow的感覺,用了兩個片來模擬
Shader方面,除了前面的View distancebased fade out和閃動特性之外,有加了billboarding。
float3 centerOffs = float3(float(0.5).xx - v.color.rg,0) * v.texcoord1.xyy;float3 centerLocal = v.vertex.xyz + centerOffs.xyz;float3 viewerLocal = mul(_World2Object,float4(_WorldSpaceCameraPos,1)); float3 localDir = viewerLocal - centerLocal; localDir[1] = lerp(0,localDir[1],_VerticalBillboarding);float localDirLength=length(localDir);float3 rightLocal;float3 upLocal;CalcOrthonormalBasis(localDir / localDirLength,rightLocal,upLocal);float distScale = CalcDistScale(localDirLength) * v.color.a; float3 BBNormal = rightLocal * v.normal.x + upLocal * v.normal.y;float3 BBLocalPos = centerLocal - (rightLocal * centerOffs.x + upLocal * centerOffs.y) + BBNormal * distScale;BBLocalPos += _ViewerOffset * localDir;
在Mesh裡面的頂點色是這樣的
大概的思路是通過頂點色構建一個坐標系,然後算頂點的偏移。具體的實現可以參考這裡
角色陰影
實現方法是在腳下放一個面片,render queue是 transparent – 15,基本是再所有透明物體的之前渲染。然後在面片的vertex shader中算人的AO。
在計算AO的時候,將人近似成球體
Shader裡面的代碼也很簡單
#if 1 // quite suprisinly this looks better (probably there is some error in AO calculation) ao = 1 - saturate(SphereAO(_Sphere0,wrldPos,wrldNormal) + SphereAO(_Sphere1,wrldPos,wrldNormal) + SphereAO(_Sphere2,wrldPos,wrldNormal));#else ao = 1 - max(max(SphereAO(_Sphere0,wrldPos,wrldNormal),SphereAO(_Sphere1,wrldPos,wrldNormal)),SphereAO(_Sphere2,wrldPos,wrldNormal));#endif ao = max(ao,1 - _Intensity) + (1 - v.color.r); o.color = fixed4(ao,ao,ao,ao);#endif
_Sphere0;_Sphere1;_Sphere2;是由外面傳進來的三個近似球體的位置,關鍵看下SphereAO函數
float SphereAO(float4 sphere,float3 pos,float3 normal){ float3 dir = sphere.xyz - pos; float d = length(dir); float v; dir /= d; v = (sphere.w / d); return dot(normal,dir) * v * v;}
就是跟據頂點的位置,法線以及球體的中心計算出一個ao值,具體原理參見大神的文章sphere ambient occlusion
參考
Rendering techniques and optimizationchallenges
Fast Mobile Shaders
ShadowGun: Optimizing for Mobile Sample Level
sphere ambient occlusion
【Unity Shaders】ShadowGun系列
ShadowGun 的學習筆記 - GodRays
推薦閱讀: