人气 257

[综合技术] Shadow Gun 图形技术分析 [复制链接]

九艺网 2018-10-13 18:53:28

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?注册

x
ShadowGun虽然是2011年的移动平台的游戏demo,但是里面的很多优化技巧到现在来看都是很值得学习的,毕竟是上过西瓜大会的。
网上现存的两份代码一个是shadow gun sample level,一个游戏场景,没法玩,只有一个摄像机动画,asset store上已经找不到了,另外一个是Shadowgun: Deadzone GM's Kit,带服务器,可以玩,asset store上还可以下载到。
下面就通过阅读demo中的代码来一起学习下。

飘动的旗帜

v2-a629a74f8594d5775ed492275c3f8ef1_hd.jpg
用的就是GPUGems里面的技术Vegetation Procedural Animation and Shading in Crysis,基本原理就是在mesh的顶点色中刷入权重,利用GPU顶点动画来模拟布料被风吹的效果。
在maya里看下mash的顶点色

v2-e5b5a6cbdf8d7677d309b7189dd576c4_hd.jpg
Shader里面
输入的参数
  1. Properties {
  2.         _MainTex ("Base (RGB) Gloss (A)", 2D) = "white" {}
  3.         //刮风的方向(世界坐标系下)
  4.         _Wind("Wind params",Vector) = (1,1,1,1)
  5.         //风的频率
  6.         _WindEdgeFlutter("Wind edge fultter factor", float) = 0.5
  7.         //风的频率的缩放
  8.         _WindEdgeFlutterFreqScale("Wind edge fultter freq scale",float) = 0.5
  9. }
复制代码


_Time是Unity的一个内置 float4变量(t/20, t,t*2, t*3),专门用来做shader动画的,

看下vert里面的关键代码
//计算风的一些参数
  1. //计算风的一些参数
  2. float4        windParams        = float4(0,_WindEdgeFlutter,bendingFact.xx);
  3. float2        windTime = _Time.y * float2(_WindEdgeFlutterFreqScale,1);
  4. float4        mdlPos        = AnimateVertex2(v.vertex,v.normal,windParams,wind,windTime);
  5. //mvp矩阵变换
  6. o.pos         = mul(UNITY_MATRIX_MVP,mdlPos);
复制代码

所以最核心的函数就是AnimateVertex2,看下它是怎么将模型里面的位置v.vertex转换到被风吹动的mdlPos。
  1. inline float4 AnimateVertex2(float4 pos, float3 normal, float4 animParams,float4 wind,float2 time)
  2. {        
  3.         // animParams stored in color
  4.         // animParams.x = branch phase
  5.         // animParams.y = edge flutter factor
  6.         // animParams.z = primary factor
  7.         // animParams.w = secondary factor

  8.         float fDetailAmp = 0.1f;
  9.         float fBranchAmp = 0.3f;
  10.         
  11.         // Phases (object, vertex, branch)
  12.         float fObjPhase = dot(_Object2World[3].xyz, 1);
  13.         float fBranchPhase = fObjPhase + animParams.x;
  14.         
  15.         float fVtxPhase = dot(pos.xyz, animParams.y + fBranchPhase);
  16.         
  17.         // x is used for edges; y is used for branches
  18.         float2 vWavesIn = time.yy  + float2(fVtxPhase, fBranchPhase );
  19.         
  20.         // 1.975, 0.793, 0.375, 0.193 are good frequencies
  21.         float4 vWaves = (frac( vWavesIn.xxyy * float4(1.975, 0.793, 0.375, 0.193) ) * 2.0 - 1.0);
  22.         
  23.         vWaves = SmoothTriangleWave( vWaves );
  24.         float2 vWavesSum = vWaves.xz + vWaves.yw;

  25.         // Edge (xz) and branch bending (y)
  26.         float3 bend = animParams.y * fDetailAmp * normal.xyz;
  27.         bend.y = animParams.w * fBranchAmp;
  28.         pos.xyz += ((vWavesSum.xyx * bend) + (wind.xyz * vWavesSum.y * animParams.w)) * wind.w;

  29.         // Primary bending
  30.         // Displace position
  31.         pos.xyz += animParams.z * wind.xyz;
  32.         
  33.         return pos;
  34. }
复制代码

关键思想是分层blend,首先计算了由主体到枝干再到顶点的震动系数,edge指旗子的边缘和自身xz方向的震动,branch指的是旗子整体的y方向的上下移动,接下来用了一些很trick的方法算出了一个float2的位移值,这个值就是顶点的位置,然后是将顶点的位移blend到主干上去,接着是主干上的位移blend到代码有点不讲道理,最后再把结果在风的方向上位移一定系数的距离。

UVAnimation
UVAnimation可以分为三个讲,滚滚浓烟,分层滚动天空盒,水面波纹

先说最简单的天空盒,就是两套UV速度,以不同的速率变化
  1. <font color="rgb(26, 26, 26)"><font face="-apple-system, BlinkMacSystemFont, " "=""><font size="3">o.uv = TRANSFORM_TEX(v.texcoord.xy,_MainTex) + frac(float2(_ScrollX, _ScrollY) * _Time);</font></font></font><div><font color="rgb(26, 26, 26)"><font face="-apple-system, BlinkMacSystemFont, " "=""><font size="3">o.uv2 = TRANSFORM_TEX(v.texcoord.xy,_DetailTex) + frac(float2(_Scroll2X, _Scroll2Y) * _Time);</font></font></font></div>
复制代码


v2-7adbca0f112cb736ff2f62c93b9a94c7_hd.jpg
最后又叠了一个颜色用来调节明暗关系。
  1. fixed4 frag (v2f i) : COLOR
  2. {
  3.         fixed4 o;
  4.         fixed4 tex = tex2D (_MainTex, i.uv);
  5.         fixed4 tex2 = tex2D (_DetailTex, i.uv2);
  6.         
  7.         o = (tex * tex2) * i.color;
  8.         
  9.         return o;
  10. }
复制代码

晚上竟然又月亮。

v2-6630c43cc3a031acda5a50ab08f23efc_hd.jpg

滚滚浓烟
还是用了顶点色
看下Mesh

v2-ca5f1a44a4de7c672e3827620459785f_hd.jpg
地下的红色,和烟的颜色叠起来,表现火焰的感觉。

贴图是两张不同的烟,用来表现层次感。

v2-d182bfd0bc5760eb7414dcc596f747b9_hd.jpg

Shader和天空盒的基本一致。

不要觉得上面两个shader比较简单就没人用了,可以自习对比下cfm的运输船
v2-7738a185f23b94479c8d52adf37d1bd3_hd.jpg
“Volumetric” effects
所谓的体效果包括了Glow,Light Shafts,Fog Plane,Emissive BillBoards

为了模拟光从窗户投射进来,用了一个透明的片来表现

v2-479c28b0d137dd7348d24bcaf550b867_hd.jpg

但不是单纯地半透明片,它是View distance based fade out,有下面两个特点
1) 随着视角的接近,透明的程度变大,离得特别远得时候,透明度也会变大
2) Mesh的位置会随着摄像机的位置变化,接近的时候有一种推开的感觉(减少overdraw)
减少overdraw的同时,规避了透明片插在摄像机里的问题。

都是vertex shader 干的
核心的代码
  1. <font color="rgb(26, 26, 26)"><font face="-apple-system, BlinkMacSystemFont, " "=""><font size="3">float3        viewPos        = mul(UNITY_MATRIX_MV,v.vertex);</font></font></font><div><font color="rgb(26, 26, 26)"><font face="-apple-system, BlinkMacSystemFont, " "=""><font size="3">float                dist                = length(viewPos);</font></font></font></div><div><font color="rgb(26, 26, 26)"><font face="-apple-system, BlinkMacSystemFont, " "=""><font size="3">float                nfadeout        = saturate(dist / _FadeOutDistNear);</font></font></font></div><div><font color="rgb(26, 26, 26)"><font face="-apple-system, BlinkMacSystemFont, " "=""><font size="3">float                ffadeout        = 1 - saturate(max(dist - _FadeOutDistFar,0) * 0.2);
  2. </font></font></font></div>
复制代码

关于saturate函数:camps the specified value within the range of 0 to 1.
简单的说就是跟据面片到摄像机的距离计算出淡入淡出的系数。具体计算可以参考这里

面片涂了顶点色
v2-345d9cc14910bc99dbc3fbc876c03345_hd.jpg

在计算位置的时候会根据alpha值来计算推开的距离
  1. <div><font color="rgb(26, 26, 26)"><font face="-apple-system, BlinkMacSystemFont, " "=""><font size="3">float4 vpos = v.vertex;</font></font></font></div><div><font color="rgb(26, 26, 26)"><font face="-apple-system, BlinkMacSystemFont, " "=""><font size="3">vpos.xyz -=   v.normal * saturate(1 - nfadeout) * v.color.a * _ContractionAmount;</font></font></font></div>
复制代码




官方的说法是这样
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里面已经体现了

还有几个用来模拟灯的地方

v2-326bf55dcb2f9a790b3da5e204c891c6_hd.jpg
v2-6013817be06dbb557f7954fc857d4ba9_hd.jpg

特点是会随机闪动。

Mesh方面还是刷了顶点色

v2-221c12b3649abca9818ab7d640a1b7c6_hd.jpg
插在面片上的两个长条三角形目测是为了防止在摄像机靠近的时候被culling掉。

闪动的原理是在vertexshader中利用sin函数计算出一个随机系数乘以o.color.
具体的计算代码如下
  1. <div><font color="#006000" face="-apple-system, BlinkMacSystemFont">float<span style="white-space: pre;">        </span>fracTime<span style="white-space: pre;">        </span>= fmod(time,_TimeOnDuration + _TimeOffDuration);</font></div><div><font color="#006000" face="-apple-system, BlinkMacSystemFont">float<span style="white-space:pre">        </span>wave<span style="white-space:pre">                </span>= smoothstep(0,_TimeOnDuration * 0.25,fracTime)  * (1 - smoothstep(_TimeOnDuration * 0.75,_TimeOnDuration,fracTime));</font></div><div><font color="#006000" face="-apple-system, BlinkMacSystemFont">float<span style="white-space:pre">        </span>noiseTime<span style="white-space:pre">        </span>= time *  (6.2831853f / _TimeOnDuration);</font></div><div><font color="#006000" face="-apple-system, BlinkMacSystemFont">float<span style="white-space:pre">        </span>noise<span style="white-space:pre">                </span>= sin(noiseTime) * (0.5f * cos(noiseTime * 0.6366f + 56.7272f) + 0.5f);</font></div><div><font color="#006000" face="-apple-system, BlinkMacSystemFont">float<span style="white-space:pre">        </span>noiseWave<span style="white-space:pre">        </span>= _NoiseAmount * noise + (1 - _NoiseAmount);</font></div><div><font color="#006000" face="-apple-system, BlinkMacSystemFont">
  2. </font></div><div><font color="#006000" face="-apple-system, BlinkMacSystemFont">wave = _NoiseAmount < 0.01f ? wave : noiseWave;</font></div><div><font color="#006000" face="-apple-system, BlinkMacSystemFont">o.color<span style="white-space: pre;">        </span>= nfadeout * _Color * _Multiplier * wave;</font></div>
复制代码



具体的原理可以参考这一篇的分析

Billboarding

v2-a5f6ea47f02c24ddab9247e9c3687a3e_hd.jpg
用来表现Glow的感觉,用了两个片来模拟


v2-e9f41c7f8429ac6ca1ecec0b64597959_hd.jpg

Shader方面,除了前面的View distance based fade out和闪动特性之外,有加了billboarding。
  1. <div><font color="#1a1a1a">float3<span style="white-space:pre">        </span>centerOffs<span style="white-space:pre">                </span>= float3(float(0.5).xx - v.color.rg,0) * v.texcoord1.xyy;</font></div><div><font color="#1a1a1a">float3<span style="white-space:pre">        </span>centerLocal<span style="white-space:pre">        </span>= v.vertex.xyz + centerOffs.xyz;</font></div><div><font color="#1a1a1a">float3<span style="white-space:pre">        </span>viewerLocal<span style="white-space:pre">        </span>= mul(_World2Object,float4(_WorldSpaceCameraPos,1));<span style="white-space:pre">                        </span></font></div><div><font color="#1a1a1a">float3<span style="white-space:pre">        </span>localDir<span style="white-space:pre">                        </span>= viewerLocal - centerLocal;</font></div><div><span style="white-space:pre"><font color="#1a1a1a">                </font></span></div><div><font color="#1a1a1a">localDir[1] = lerp(0,localDir[1],_VerticalBillboarding);</font></div><div><font color="#1a1a1a">
  2. </font></div><div><font color="#1a1a1a">float<span style="white-space:pre">                </span>localDirLength=length(localDir);</font></div><div><font color="#1a1a1a">float3<span style="white-space:pre">        </span>rightLocal;</font></div><div><font color="#1a1a1a">float3<span style="white-space:pre">        </span>upLocal;</font></div><div><font color="#1a1a1a">
  3. </font></div><div><font color="#1a1a1a">CalcOrthonormalBasis(localDir / localDirLength,rightLocal,upLocal);</font></div><div><font color="#1a1a1a">
  4. </font></div><div><font color="#1a1a1a">float<span style="white-space:pre">                </span>distScale<span style="white-space:pre">                </span>= CalcDistScale(localDirLength) * v.color.a;<span style="white-space:pre">                </span></font></div><div><font color="#1a1a1a">float3<span style="white-space:pre">        </span>BBNormal<span style="white-space:pre">                </span>= rightLocal * v.normal.x + upLocal * v.normal.y;</font></div><div><font color="#1a1a1a">float3<span style="white-space:pre">        </span>BBLocalPos<span style="white-space:pre">        </span>= centerLocal - (rightLocal * centerOffs.x + upLocal * centerOffs.y) + BBNormal * distScale;</font></div><div><font color="#1a1a1a">
  5. </font></div><div><font color="#1a1a1a">BBLocalPos += _ViewerOffset * localDir;</font></div>
复制代码



在Mesh里面的顶点色是这样的

v2-2b6ee4ff4282d0253e06a44d14f9b1b7_hd.jpg
大概的思路是通过顶点色构建一个坐标系,然后算顶点的偏移。具体的实现可以参考这里

角色阴影

v2-ef0d88e9f73ffccf9c946318c31f9bbd_hd.jpg

实现方法是在脚下放一个面片,render queue是 transparent – 15,基本是再所有透明物体的之前渲染。然后在面片的vertex shader中算人的AO。

在计算AO的时候,将人近似成球体

v2-238e1647eb192fd93a80766b83f64b63_hd.jpg

Shader里面的代码也很简单

  1. <div><font color="#1a1a1a">#if 1</font></div><div><font color="#1a1a1a"><span style="white-space:pre">                </span>// quite suprisinly this looks better (probably there is some error in AO calculation)</font></div><div><font color="#1a1a1a"><span style="white-space:pre">                </span>ao = 1 - saturate(SphereAO(_Sphere0,wrldPos,wrldNormal) + SphereAO(_Sphere1,wrldPos,wrldNormal) + SphereAO(_Sphere2,wrldPos,wrldNormal));</font></div><div><font color="#1a1a1a">#else</font></div><div><font color="#1a1a1a"><span style="white-space:pre">                </span>ao = 1 - max(max(SphereAO(_Sphere0,wrldPos,wrldNormal),SphereAO(_Sphere1,wrldPos,wrldNormal)),SphereAO(_Sphere2,wrldPos,wrldNormal));</font></div><div><font color="#1a1a1a">#endif</font></div><div><font color="#1a1a1a">
  2. </font></div><div><font color="#1a1a1a"><span style="white-space:pre">                </span>ao = max(ao,1 - _Intensity) + (1 - v.color.r);</font></div><div><font color="#1a1a1a"><span style="white-space:pre">                </span>o.color = fixed4(ao,ao,ao,ao);</font></div><div><font color="#1a1a1a">
  3. </font></div><div><font color="#1a1a1a">#endif</font></div>
复制代码


_Sphere0;_Sphere1;_Sphere2;是由外面传进来的三个近似球体的位置,关键看下SphereAO函数
  1. float SphereAO(float4 sphere,float3 pos,float3 normal)
  2. {
  3.         float3        dir = sphere.xyz - pos;
  4.         float        d        = length(dir);
  5.         float        v;

  6.         dir /= d;

  7.         v = (sphere.w / d);

  8.         return dot(normal,dir) * v * v;
  9. }
复制代码

就是跟据顶点的位置,法线以及球体的中心计算出一个ao值,具体原理参见大神的文章sphere ambient occlusion


v2-3976935776290cfd8044ff875f97eed5_hd.jpg
参考
Rendering techniques and optimization challenges\
Fast Mobile Shaders\
ShadowGun: Optimizing for Mobile Sample Level\
【Unity Shaders】ShadowGun系列\


回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

QQ|手机版|小黑屋|九艺游戏动画论坛 ( 津ICP备2022000452号-1 )

GMT+8, 2024-3-29 20:02 , Processed in 0.119409 second(s), 29 queries .

Powered by Discuz! X3.4  © 2001-2017 Discuz Team.