标签:
体积光,这个名称是God Rays的中文翻译,感觉不是很形象。God Rays其实是Crepuscular rays在图形学中的说法,而Crepuscular rays的意思是云隙光、曙光、曙暮辉的意思。在现实生活中,它的样子大概是下面这样:
体积光的翻译大概就是由于这种光可见好像有体积似的。这些光一般是由于强烈的阳光从一些缝隙,如云间缝隙、窗户的缝隙中,透到较暗的环境中所造成的。如果要真实模拟体积光,可能需要很复杂的粒子渲染。但这在移动设备上基本是不可能实现的。
ShadowGun中把体积光归于是雾的一种应用。雾和体积光有很多相似的地方,一个大面积的体积光从视觉上来看和雾很像,它们有一个共性的感性认识,就是其可见度和距离视角的远近有关。因此,ShadowGun用简单网格+Alpha Blending的方法来模拟雾和体积光。由于ShadowGun中体积光是雾的一种应用,因此下面统一称为雾效。
ShadowGun其实最开始是2011年的一个移动平台的第三人称射击游戏。当然,也是用Unity开发的。当年,由于在画面上的出色表现赢得了很多眼球~更难能可贵的是,在2012的时候,它的开发者放出了示例场景,来让更多的开发者学习如何优化移动平台上的shader。下载地址请戳官方博客。看不懂英文的可以看这篇(写得很不错)。项目里共包含了将近20个优化后的shader。关于使用许可的问题,项目里的shader都是可以免费使用的,而贴图和模型是不可用于商业用途的呦~
虽然ShadowGun的出场时间有点久远了,但很多技术还是可以借鉴滴~而且它现在仍然在更新,并且价格为高昂的¥30,可见其对自信程度。
ShadowGun里包含了几个比较重要的shader,例如非常有名的旗帜飘动的shader,动态效果的天空盒子的shader,环境高光纹理映射等等。
这里的雾效不是指那种真的全局环境都受影响的大雾,而是一种现象:在视角逐渐接近它的时候,视野逐渐清晰。例如对于体积光,从远处看它可能会感觉很亮,但越接近亮度越小,越能看清后面的物体。这种效果可以很好地让玩家感觉到深度的变化。ShadowGun的解决方法是使用一个简单的网格(Fog planes)+透明纹理来模拟。一旦玩家靠近时,通过减淡颜色+使网格顶点移开(需要移开的原因是因为,即使是完全透明的alpha面也会消耗很多渲染时间,而这里的移开一般是把网格收缩变小,减少透明区域)的方法来模拟这个效果。
而如果要使用这个Shader,就需要在三维软件中处理那么Fog planes:
在ShadowGun中,有三个shaders使用了这个技术:GodRays,Blinking GodRays和Blinking GodRays Billboarded。
其中GodRays用于模拟体积光。
Blinking GodRays用于各种blingbling的光效,它也是这三个中应用最广的一个shader,包括了光锥的闪烁、水面反光、仪表盘的灯光闪烁(图中的绿色发光部分)、金属表面的反光闪烁、顶棚的阳光闪烁、火焰及火光的闪烁(地面上的火光闪烁和飞船后发射器的火焰)、光雾效果(背景的蓝绿色光雾)等等。
Blinking GodRays Billboarded用于水箱中的blingbling灯光效果(下图中罐体周围的绿色发光部分)。
可以看出来,场景里几乎任何看起来会发光的物体都是靠这种技术模拟的。
GodRays是其中最简单、最基本的shader。
代码如下:
Shader "MADFINGER/Transparent/GodRays" { Properties { _MainTex ("Base texture", 2D) = "white" {} _FadeOutDistNear ("Near fadeout dist", float) = 10 _FadeOutDistFar ("Far fadeout dist", float) = 10000 _Multiplier("Multiplier", float) = 1 _ContractionAmount("Near contraction amount", float) = 5 } SubShader { Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" } Blend One One // Blend One OneMinusSrcColor Cull Off Lighting Off ZWrite Off Fog { Color (0,0,0,0) } LOD 100 CGINCLUDE #include "UnityCG.cginc" sampler2D _MainTex; float _FadeOutDistNear; float _FadeOutDistFar; float _Multiplier; float _ContractionAmount; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; fixed4 color : TEXCOORD1; }; v2f vert (appdata_full v) { v2f o; 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); ffadeout *= ffadeout; nfadeout *= nfadeout; nfadeout *= nfadeout; nfadeout *= ffadeout; float4 vpos = v.vertex; vpos.xyz -= v.normal * saturate(1 - nfadeout) * v.color.a * _ContractionAmount; o.uv = v.texcoord.xy; o.pos = mul(UNITY_MATRIX_MVP, vpos); o.color = nfadeout * v.color * _Multiplier; return o; } ENDCG Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma fragmentoption ARB_precision_hint_fastest fixed4 frag (v2f i) : COLOR { return tex2D (_MainTex, i.uv.xy) * i.color; } ENDCG } } }
可以看出来,frag函数非常简单。其实ShadowGun中很多就是都是通过网格来模拟光照效果,它们的frag函数一般非常简单,而大部分计算都在vert函数中。这是可以理解的,因为逐顶点永远比逐像素的效率更高。vert里负责计算三个部分:一个是顶点位置,一个是纹理坐标,一个是传递给fragment的颜色信息(这里还包含了重要的透明度信息)。frag函数里就可以通过简单的纹理采样和颜色相乘来得到最终的效果。
我们来看最重要的vert函数。这个shader中没有对纹理坐标做什么更改,因此,这个vert函数的关键只有两个部分,一个是顶点位置,一个的颜色信息。
我们先来看颜色的计算过程。vert在输入的顶点颜色的基础(这意味着在建模时就要给顶点赋予合适的体积光颜色)上,对其还乘以了一个乘数_Multiplier和一个衰减值nfadeout。乘数_Multiplier很好理解,就是用于改变亮度而已。关键在于nfadeout。nfadeout是一个范围在(0,1)之间的淡化系数,它用于模拟淡入或淡出效果。和它相关的有两个属性:_FadeOutDistNear和_FadeOutDistFar。玩家由无限远开始接近这个物体的过程中,一开始是远大于_FadeOutDistFar,那么是看不到这个体积光的;然后逐渐接近_FadeOutDistFar后,开始出现淡入效果;如果小于了_FadeOutDistNear,那么就会开始模拟淡出的效果。与其相关的是下面两句:
float nfadeout = saturate(dist / _FadeOutDistNear); float ffadeout = 1 - saturate(max(dist - _FadeOutDistFar,0) * 0.2);
其中dist是在View Space中距离原点的远近,也就是距离视角的远近。nfadeout负责计算“如果小于了_FadeOutDistNear,那么就会开始模拟淡出的效果”这一效果。可以看出,当dist大于_FadeOutDistNear时,总是返回1,从而不会产生任何影响;而一旦小于_FadeOutDistNear后,就会产生一个线性的衰减。ffadeout的计算看起来复杂也难懂一点。我们希望ffadeout的结果是,在dist远大于_FadeOutDistFar时返回0;在dist逐渐接近_FadeOutDistFar时,逐渐从0增加到1;在dist小于_FadeOutDistFar时,返回1。从函数图像来看,其实就是个分段函数。上面的写法只是通过max和saturate函数来实现这种分段的目的,其中0.2是模拟了淡入的速率。下面就是这句计算表达式的函数图像:
对于一般的射灯模拟,_FadeOutDistNear的值都比较大。在计算完nfadeout和ffadeout后,并没有直接相乘,而是各自进行了指数操作。这里感觉是感性的计算,即希望淡入/淡出的速率更快或者更慢等。
下面是顶点位置的计算。与其相关的语句是:
float4 vpos = v.vertex; vpos.xyz -= v.normal * saturate(1 - nfadeout) * v.color.a * _ContractionAmount;
剩下的部分就没什么难的了。
Blinking GodRays只更改了vert部分,而且涉及到更多的参数和变量。代码如下:
v2f vert (appdata_full v) { v2f o; float time = _Time.y + _BlinkingTimeOffsScale * v.color.b; 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); 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); float distScale = min(max(dist - _SizeGrowStartDist,0) / _SizeGrowEndDist,1); wave = _NoiseAmount < 0.01f ? wave : noiseWave; distScale = distScale * distScale * _MaxGrowSize * v.color.a; wave += _Bias; ffadeout *= ffadeout; nfadeout *= nfadeout; nfadeout *= nfadeout; nfadeout *= ffadeout; float4 mdlPos = v.vertex; mdlPos.xyz += distScale * v.normal; o.uv = v.texcoord.xy; o.pos = mul(UNITY_MATRIX_MVP, mdlPos); o.color = nfadeout * _Color * _Multiplier * wave; return o; }
我们先来看顶点位置。顶点位置的计算如下:
float distScale = min(max(dist - _SizeGrowStartDist,0) / _SizeGrowEndDist,1); distScale = distScale * distScale * _MaxGrowSize * v.color.a; float4 mdlPos = v.vertex; mdlPos.xyz += distScale * v.normal; o.pos = mul(UNITY_MATRIX_MVP, mdlPos);
比较复杂的是顶点颜色的计算。
o.color = nfadeout * _Color * _Multiplier * wave
首先,这里没有使用原来的顶点颜色进行计算,而是允许用户在面板中调整_Color参数。这是可以理解的,因为体积光的颜色基本不变,通常都是偏黄的,因此在上一个shader中可以直接使用原来的顶点颜色进行计算,节约空间。而这里的用途很广泛,让用户自定义颜色是更好的选择。上面的nfadeout和_Multiplier与之前的计算无异,不再赘述。复杂的是wave的计算:
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; wave += _Bias;
我们首先来看如何模拟均匀的脉冲波闪烁。其计算式如下:
float time = _Time.y + _BlinkingTimeOffsScale * v.color.b; float fracTime = fmod(time,_TimeOnDuration + _TimeOffDuration); float wave = smoothstep(0, _TimeOnDuration * 0.25, fracTime) * (1 - smoothstep(_TimeOnDuration * 0.75, _TimeOnDuration, fracTime));
这里面涉及了三个参数:_BlinkingTimeOffsScale,_TimeOnDuration,_TimeOffDuration。我们还是从wave的函数图像出发来看这里面的猫腻(注意其中的参数):
可以看出来_TimeOnDuration和_TimeOffDuration两个参数负责控制脉冲波的高频区域的时间长度和低频区域的时间长度。而_BlinkingTimeOffsScale代码里的说明是,“Blinking time offset scale (seconds)”,从第二幅图像上可以看出来其实就是制定从哪个位置开始模拟脉冲闪烁,需要注意的是,_BlinkingTimeOffsScale的取值范围在(0, _TimeOnDuration + _TimeOffDuratio),如果大于这个范围也会相当于对_TimeOnDuration + _TimeOffDuratio取模。
图像直观了解后,我们再来看代码。fracTime反应了当前处于一个周期中的那个阶段,因此需要使用当前的时间time对整个循环周期_TimeOnDuration + _TimeOffDuratio取模。smoothstep函数将返回一个范围在(0, 1)之间的值,这个值由第三个参数相对于前两个参数的位置来平滑插值决定的。Nvidia文档里是这样给出它的参考代码的:
float smoothstep(float a, float b, float x) { float t = saturate((x - a)/(b - a)); return t*t*(3.0 - (2.0*t)); }
下面分析非均匀的噪声闪烁模拟。主要代码如下:
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);
从代码看,噪声模拟主要依靠一个正弦函数和余弦函数的乘积来实现。
终于到了最后了,真累。Blinking GodRays Billboarded和上一篇有相通的地方,它们都是blingbling的!但区别在于,Billboarded的意思是它总是会面朝着观察者的方向,它的网格其实是一些平板(像广告板一样),但是由于它总是会根据我们的观察方向来随时旋转,让我们感觉它是立体的一样。Billboarded的行为就跟向日葵总是会朝着太阳一样!它在ShadowGun中用于模拟水箱中的灯光效果。
主要代码如下:
v2f vert (appdata_full v) { v2f o; #if 0 // cheap view space billboarding float3 centerOffs = float3(float(0.5).xx - v.color.rg,0) * v.texcoord1.xyy; float3 BBCenter = v.vertex + centerOffs.xyz; float3 viewPos = mul(UNITY_MATRIX_MV,float4(BBCenter,1)) - centerOffs; #else 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; #endif float time = _Time.y + _BlinkingTimeOffsScale * v.color.b; 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; wave += _Bias; o.uv = v.texcoord.xy; o.pos = mul(UNITY_MATRIX_MVP, float4(BBLocalPos,1)); o.color = CalcFadeOutFactor(localDirLength) * _Color * _Multiplier * wave; return o; }
我们先来看顶点颜色的计算。这一部分和上一个shader几乎完全一样,稍有不同的是,它把计算淡入淡出的工作封装到了一个函数中CalcFadeOutFactor。对于上一个shader来说,CalcFadeOutFactor函数的输入是在View Space中顶点的距离,但在这里不可以直接使用顶点的原始位置v.vertex,而是修改后的网格中心位置距离localDirLength。
下面是最关键的顶点位置的计算。它的相关代码如下:
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; #endif o.pos = mul(UNITY_MATRIX_MVP, float4(BBLocalPos,1));
这篇有点长,数学公式也很多,我尽量用函数图像来解释了。这里总结一下上面的各种技术:
写这一篇心好累。。。
【Unity Shaders】ShadowGun系列之二——雾和体积光
标签:
原文地址:http://blog.csdn.net/candycat1992/article/details/42061701