标签:fse class fragment 法线 tin eve second 公式 tor
体积光,这个名称是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函数非常easy。事实上ShadowGun中非常多就是都是通过网格来模拟光照效果,它们的frag函数一般非常easy,而大部分计算都在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;
当中,顶点的透明通道决定了这个顶点能否够移动(这是因为,体积光往往有一边是不能够移动的,想象一下从窗户投进来的光,起点用于在和窗户的衔接处是不会动的)。而_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);
这里并非和上面一样是为了收缩顶点,相反是为了扩大顶点区域。
这主要是为了模拟射灯的效果,我们在远离光源的过程中会感觉好像光照范围范围变大了。
distScale和上面的ffadeout类似,它相同是一个范围在(0,1)之间的值,有两个參数控制顶点增长的起始和终止位置,_SizeGrowStartDist和_SizeGrowEndDist。
在dist小于_SizeGrowStartDist时,distScale返回0;在dist逐渐大于_SizeGrowStartDist时,逐渐从0增大。当dist(实际是dist-_SizeGrowStartDist,但_SizeGrowStartDist通常都非常小)大于_SizeGrowEndDist时。返回1。表示已经达到了最大的扩张大小。当中,_MaxGrowSize用于控制扩张的大小,而顶点的透明度决定了该点是否会移动(和上面的类似的)。
因为这里是扩张和非收缩,因此移动方向是朝着顶点法线的方向正向移动。
比較复杂的是顶点颜色的计算。
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;
主要的淡出效果已经通过上面的nfadeout实现了,因此这里主要是为了模拟闪烁的效果。闪烁本质上就是一种动画效果。这里给出了两种动画模拟的方式:一种是均匀跳跃的脉冲波模拟。一种是非均匀的噪声模拟。这是用过_NoiseAmount參数实现的,它的面板凝视中也说明了这一点,“Noise amount (when zero, pulse wave is used)”,即当噪声非常小时,就会使用均匀跳跃的脉冲波模拟。否则就使用_NoiseAmount模拟非均匀的闪烁动画。
我们首先来看怎样模拟均匀的脉冲波闪烁。其计算式例如以下:
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)); }
这个函数决定了图像中平滑上升的区域。代码中的系数决定,左右两边平滑上升(下降)区域的长度占总体高频区域的25%。当fracTime全然大于_TimeOnDuration后,就进入低频区,即输出是0。
以下分析非均匀的噪声闪烁模拟。
主要代码例如以下:
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));
有时。我们并不希望让平板全然垂直与视角的观察方向。而是仅想得到在XZ平面上的方向(想象一个仅仅能够左右摆头,但不能够向上仰头或向下低头的向日葵)。因此,这里能够使用_VerticalBillboarding參数对这样的垂直程度进行调整,_VerticalBillboarding为0表示全然舍弃Y方向上的信息,仅能够左右摆头;而_VerticalBillboarding为1则表示平板要全然垂直与观察视角的方向。对于水箱这样的精巧的对象,一般_VerticalBillboarding为0。
然后将其正则化后的结果作为CalcOrthonormalBasis的输入。输出的是该网格中心点面对视角的右手方向rightLocal和正上方向upLocal。这两个方向决定了该shader像向日葵一样的行为。
得到当前的右手方向和正上方向后。distScale和之前的shader一样。相同是依据距离观察点的距离来计算顶点的扩张程度的。BBNormal则是为了更新旋转后顶点的法线方向,它的计算简单明了,就是使用新的右手/正上方向又一次定义法线,这里z方向是不须要考虑的。应该它就是个平板。
BBLocalPos一行,首先使用新的右手/正上方向还原该角度下该顶点的相应位置,然后再使用新的法线方向BBNormal和distScale进行扩张。最后。使用參数_ViewerOffset对BBLocalPos进行最后的偏移。_ViewerOffset表示将平板像视角方向进行偏移的量。一般设为0就可以。
这篇有点长,数学公式也非常多,我尽量用函数图像来解释了。
这里总结一下上面的各种技术:
比如,在对顶点进行收缩和扩张时。须要使用顶点颜色的透明度来决定该点能否够移动;在最后一篇计算网格中心点的位置时,每一个顶点相对于网格中心点的偏移也提前存储在了顶点的颜色和纹理坐标中。
在GodRays中,淡出的同一时候还会收缩顶点。
_Multiplier:光照颜色的乘数。能够用来调亮/调暗最后的模拟光照。
_Bias:模拟闪烁时。波形的偏移,能够理解成把波形图像向Y方向的移动量。
_TimeOnDuration:模拟闪烁时。波形中高频区域的长度。能够理解为闪烁时亮着的时间。
_TimeOffDuration:模拟闪烁时,波形中低频区域的长度,能够理解为闪烁时暗着的时间。
_BlinkingTimeOffsScale:模拟闪烁时,指定闪烁在波形中的開始位置。
_SizeGrowStartDist:大于这个距离时,会開始对顶点进行扩展。即从0開始增长。
_SizeGrowEndDist:达到这个距离时,扩张达到最大程度,即扩展程度为1。
_MaxGrowSize:扩张的最大大小。
_NoiseAmount:模拟闪烁时。噪声的程度。用于混合均匀的脉冲波和噪声波。
_VerticalBillboarding:在Blinking GodRays Billboarded中。平板的垂直程度。返回为(0, 1)。0表示不须要垂直与视角方向,仅仅在XZ平面旋转。
_ViewerOffset:在Blinking GodRays Billboarded中。将网格向视角方向移动的偏移量。
_Color:用于改变光照颜色。
写这一篇心好累。
。。
【Unity Shaders】ShadowGun系列之二——雾和体积光
标签:fse class fragment 法线 tin eve second 公式 tor
原文地址:http://www.cnblogs.com/yfceshi/p/6877780.html