好久没有更新博客了,经历了不少事情,好在最近回归了一点正轨,决定继续Unity Shader的学习之路。作为回归的第一篇,来玩一个比较酷炫的效果(当然废话也比较多),一般称之为GodRay(圣光),也有人叫它云隙光,还有人叫它体积光(探照灯)。这几个名字对应几种类似的效果,但是实现方式相差甚远。先来几张照片以及其他游戏的截图看一下:

ps:这张图片是一张照片哈,是本屌丝看别人的云南游记发现的,哎呀,看着好美好想去>_<

ps:这张截图是《耻辱-外魔之死》的一张截图,窗缝中透过的光形成了一道道光束。也不知道《耻辱》系列还有没有后续了,超级喜欢的一个系列,最近才买的这一部,一共五关,还剩一关就通关了,我竟然有点舍不得玩了...

ps:这张图是《罗马之子》中的一个截图,抬头看太阳会发现一个很耀眼的光束,啥时候能自带个这样的光效哈,CryEngine渲染就是棒。这个游戏玩得有点心酸,感觉主角好悲剧。

ps:《剑灵》中云隙光的效果,很明显,很给力!

ps:来张《天涯明月刀》中的动态效果,天刀人模的渲染和天气系统太给力了,技能也很流畅,对,还有萌萌哒萝莉,萝莉,萝莉!!!本来是想着去看看有啥效果可以玩一下的,结果一不小心沉迷了好几个月,差点玩成《天涯上班刀》。

ps:《Inside》打水怪的一关,潜艇探照灯的效果;这是个人很喜欢的一部游戏,当初只是感觉这个游戏玩法很好,直到看了他们GDC的分享,反过来再玩这个游戏的时候,才意识到这个游戏的渲染技术竟然也如此超前,可能游戏本身的玩法太好玩,以至于我第一遍玩的时候,完全没注意这些效果相关的东东。

额,赶脚我是一个写游戏评测的的...回归正题,GodRay效果对游戏的画面提升很大,也成了当今各种大型游戏中很常见的一个效果,所以今天本人打算把上面的这几个效果用四种不同的方式实现一遍,当然,上面的都是3A大作,我这个小菜鸟只能简单模拟一下,权当实践一遍当今游戏中常见的体积光实现的技术,疏漏之处,还望各位高手不吝赐教。

简介

首先得了解一下真实世界中GodRay现象的原理,然后我们再去模拟(虽然大多数情况实现跟原理相差十万八千里)。这种光的现象是中学物理学过的一个东东,叫丁达尔效应。胶体中粒子对光线进行了散射形成光亮的通路。自然界中,云,雾,空气中的烟尘等都是胶体,所以当光照射过去的时候,发生散射,就形成了我们看到的GodRay了。

我们要在游戏中模拟这种现象,·当然不太可能完全按照现实世界中的方式去做,如果真的按照现实方式去渲染体积光,可能需要非常非常大量的粒子,这在PC端实时计算都很困难,在目前的移动设备上就更不可能了。对于游戏中我们所要的,就是在需要的地方,能显示出一道光线就好了。今天主要介绍以下几种实现方式,BillBoard特效贴片,Volume Shadow沿光方向挤出顶点,Raidal Blur Postprocessing基于后处理的实现,Ray-Marching基于光线追踪的实现。几种方式殊途同归,都是尽可能用最省的消耗来近似模拟这一酷炫的现象。

BillBoard特效贴片

最简单的方法,直接在需要有GodRay的地方,放一个特效片,模拟一个光效,就完成啦!

通过Unity自带的粒子系统,控制粒子贴图采样uv变换,以及颜色的alpha变换,模拟灯光摇曳的状态(今天找到了一个Gif录屏软件,GifCam,感觉还不错,终于摆脱了先录视频再转Gif的费劲工作流...):

这是最简单粗暴的方法,不过往往也是最行之有效的,同时也是性能最好的。对于场景中的一些简单装饰性的效果,其实用这种方式就可以满足了,这也是最适合手游的一种方案。《耻辱-外魔之死》的窗缝中透光的效果,如果不考虑近处穿帮的问题,其实就可以使用这种方式进行近似模拟。

不过,这个方式过于简单了点儿,远景效果还可以,如果离近了可能会显得不是很真实,所以就有很多针对这个效果的变种,最著名的应该就是Shadow Gun里面的实现了,Shadow Gun确实是一个好东东,里面很多效果的实现都很经典,下面来分析一下ShadowGun的体积光效果。

Shadow Gun中的体积光有两个重要的特性,第一个是根据距离远近 ,动态调整体积光的颜色及透明度,来达到更加真实的体积光的效果,在远距离看不清体积光,距离近些时逐渐清晰,当距离很近时,降低强度,使之更容易看清背后物体。第二个特性是动态调整体积光网格的位置,当摄像机贴近体积光时,避免了相机与半透穿插,同时也避免了因半透占屏比高导致的像素计算暴涨的性能问题。

下面附上一段代码:

  1. //puppet_master
  2. //2018.4.15
  3. //Shadow Gun中贴片方式实现GodRay代码,升级unity2017.3,增加一些注释
  4. Shader "GodRay/ShadowGunSimple"
  5. {
  6. Properties
  7. {
  8. _MainTex ("Base texture", 2D) =www.120xh.cn   "white" {}
  9. _FadeOutDistNear ("Near fadeout dist", float) = 10
  10. _FadeOutDistFar ("Far fadeout dist", float) = 10000
  11. _Multiplier("Multiplier", float) = 1
  12. _ContractionAmount("Near www.taohuayuan178.com   contraction amount", float) = 5
  13. //增加一个颜色控制(仅RGB生效)
  14. _Color("Color", Color) = (1,1,1,1)
  15. }
  16. SubShader
  17. {
  18. Tags { "Queue"="Transparent" "IgnoreProjector"=www.qinlinyule.cn "True" "RenderType"="Transparent" }
  19. //叠加方式Blend
  20. Blend One www.taohuayuangw.com One
  21. Cull Off
  22. Lighting Off
  23. ZWrite Off
  24. Fog { Color (0,0,0,0) }
  25. CGINCLUDE
  26. #include "UnityCG.cginc"
  27. sampler2D _www.thd178.com MainTex;
  28. float _FadeOutDistNear;
  29. float _FadeOutDistFar;
  30. float _Multiplier;
  31. float _ContractionAmount;
  32. float4 _Color;
  33. struct v2f {
  34. float4  pos : SV_POSITION;
  35. float2  uv      : TEXCOORD0;
  36. fixed4  color   : TEXCOORD1;
  37. };
  38. v2f vert (appdata_full v)
  39. {
  40. v2f         o;
  41. //update mul(UNITY_MATRIX_MV, v.vertex) 根据UNITY_USE_PREMULTIPLIED_MATRICES宏控制,可以预计算矩阵,减少逐顶点计算
  42. float3      viewPos     = www.baohuayule.net  UnityObjectToViewPos(v.vertex);
  43. float       dist        = length(viewPos);
  44. float       nfadeout    = saturate(dist / _FadeOutDistNear);
  45. float       ffadeout    = 1 - saturate(max(dist - _FadeOutDistFar,0) * 0.2);
  46. //乘方扩大影响
  47. ffadeout *= ffadeout;
  48. nfadeout *= nfadeout;
  49. nfadeout *= nfadeout;
  50. nfadeout *= ffadeout;
  51. float4 vpos = v.vertex;
  52. //沿normal反方向根据fade系数控制顶点位置缩进,刷了顶点色控制哪些顶点需要缩进
  53. //黑科技:mesh是特制的,normal方向是沿着面片方向的,而非正常的垂直于面片
  54. vpos.xyz -=   v.normal * saturate(1 -www.thd178.com/  nfadeout) * v.color.a * _ContractionAmount;
  55. o.uv    = v.texcoord.xy;
  56. o.pos   = UnityObjectToClipPos(vpos);
  57. //直接在vert中计算淡出效果
  58. o.color = nfadeout * v.color * _Multiplier* _Color;
  59. return o;
  60. }
  61. fixed4 frag (v2f i) : COLOR
  62. {
  63. return tex2D (_MainTex, i.uv.xy) * i.color ;
  64. }
  65. ENDCG
  66. Pass
  67. {
  68. CGPROGRAM
  69. #pragma vertex vert
  70. #pragma fragment frag
  71. #pragma fragmentoption ARB_precision_hint_fastest
  72. ENDCG
  73. }
  74. }
  75. }

效果如下:

简单分析一下:根据远近控制淡入淡出比较简单,只要设置两个距离的系数,根据距离去计算即可,如果感觉效果不够强,就乘方一下,这个也是shader中比较常用的一个提高某个属性对效果影响强度的手段。另一点,根据距离去动态调整顶点的位置,本身这个思想就比较有想法,但是实现更加惊艳到我了。首先刷顶点色这个也是比较常用的控制模型不同位置不同表现的一个方法,但是Shadow Gun不光刷了顶点色,还把法线的内容改了(本身不需要光照计算,没有法线的需求),直接在制作模型的时候将面片的法线改为沿着面片的方向,而不是正常的垂直于面片的方向,这样在计算时,就可以很容易地让模型的缩进方向改为沿着面片。所以这个shader必须结合特制的mesh来使用,并且model设置的法线必须为Import方式,如果改为calculate方式,Unity自己计算出的法线的话,效果就完全不对了。

Shadow Gun中还有一个稍微复杂一些的GodRay Shader,除上面的效果外,又增加了一个根据正弦波等模拟的灯光忽明忽暗的效果,与最上面粒子的控制效果大同小异。这种shader的变种其实可以模拟做一个聚光灯的效果,用一个圆筒形的Mesh,根据菲涅尔计算一个柔和的边缘,然后光柱本身采样一下噪声图,做一个UV滚动,也可以刷一下顶点数控制一下光渐变,就有一个比较好的探照灯效果啦。

Volume Shadow光方向挤出

这个方案也是一个相对比较省的方案,但是效果的局限性很大,只是某些特殊情况下可以出比较好的效果,主要的思想是阴影的一种实现-体积阴影的扩展。这个效果在《黑魂2》里面我曾经见过一次,然而这个游戏我实在没有兴趣再被虐一遍,所以木有找到游戏截图,另外天刀的神威职业选人界面的效果与这个有些类似。

Shader代码如下:

  1. //puppet_master
  2. //2018.4.15
  3. //GodRay,体积阴影扩展,沿光方向挤出顶点实现
  4. Shader "GodRay/VolumeShadow"
  5. {
  6. Properties
  7. {
  8. _Color("Color", Color) = (1,1,1,0.002)
  9. _MainTex ("Base texture", 2D) = "white" {}
  10. _ExtrusionFactor("Extrusion", Range(0, 2)) = 0.1
  11. _Intensity("Intensity", Range(0, 10)) = 1
  12. _WorldLightPos("LightPos", Vector) = (0,0,0,0)
  13. }
  14. SubShader
  15. {
  16. Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent + 1" }
  17. Blend SrcAlpha OneMinusSrcAlpha
  18. Cull Off
  19. ZWrite Off
  20. Fog { Color (0,0,0,0) }
  21. CGINCLUDE
  22. #include "www.douniu178.com   UnityCG.cginc"
  23. float4 _Color;
  24. float4 _WorldLightPos;
  25. sampler2D _MainTex;
  26. float _ExtrusionFactor;
  27. float _Intensity;
  28. struct v2f {
  29. float4  pos     : SV_POSITION;
  30. float2  uv      : TEXCOORD0;
  31. float distance : TEXCOORD1;
  32. };
  33. v2f vert (appdata_base v)
  34. {
  35. v2f o;
  36. //转化到物体空间计算
  37. float3 objectLightPos = mul(unity_WorldToObject, _WorldLightPos.xyz).xyz;
  38. float3 objectLightDir = objectLightPos - v.vertex.xyz;
  39. float dotValue = dot(objectLightDir, v.normal);
  40. //light dot normal,*0.5+0.5转化为0,1控制变量,控制受光面挤出
  41. float controlValue www.leyouzaixan.cn = sign(dotValue) * 0.5 + 0.5;
  42. float4 vpos = v.vertex;
  43. //受光面沿法线反方向挤出顶点
  44. vpos.xyz -= objectLightDir * _www.leyouzxgw.com ExtrusionFactor * controlValue;
  45. o.uv    = v.texcoord.xy;
  46. o.pos   = UnityObjectToClipPos(vpos);
  47. o.distance = length(objectLightDir);
  48. return o;
  49. }
  50. fixed4 frag (v2f i) : COLOR
  51. {
  52. fixed4 tex = tex2D(_MainTex, i.uv);
  53. //顶点到光的距离与物体到光的距离控制一个衰减值
  54. float att = i.distance / _WorldLightPos.w;
  55. return _Color * tex *www.wmyl88.com  att * _Intensity;
  56. }
  57. ENDCG
  58. Pass
  59. {
  60. CGPROGRAM
  61. #pragma vertex vert
  62. #pragma fragment frag
  63. #pragma fragmentoption ARB_precision_hint_fastest
  64. ENDCG
  65. }
  66. }
  67. }

另外,这里没有使用真正的光源位置,而是自己控制了一个光源的位置,这样比较灵活,不过需要一个脚本把光源位置传递给shader。另外,如果要渲染体积光,除了体积光,还需要渲染对象本身,可以用RenderWithShader,Command Buffer,Graphics.DrawMesh等等,不过,我直接用了最简单偷懒的方法,直接给对象加了个材质,一个正常渲染,一个渲染体积光。脚本如下:

  1. /********************************************************************
  2. FileName: GodRayVolumeHelper.cs
  3. Description:
  4. Created: 2018/04/20
  5. history: 20:4:2018 0:24 by zhangjian
  6. *********************************************************************/
  7. using UnityEngine;
  8. [ExecuteInEditMode]
  9. public class GodRayVolumeHelper : MonoBehaviour {
  10. public Transform lightTransform;
  11. private Material godRayVolumeMateril;
  12. void Awake()
  13. {
  14. var renderer = GetComponentInChildren<Renderer>();
  15. foreach(var mat in renderer.sharedMaterials)
  16. {
  17. if (mat.shader.name.Contains("VolumeShadow"))
  18. godRayVolumeMateril = mat;
  19. }
  20. }
  21. // Update is called once per frame
  22. void Update ()
  23. {
  24. if (lightTransform == null || godRayVolumeMateril == null)
  25. return;
  26. float distance = Vector3.Distance(lightTransform.position, transform.position);
  27. godRayVolumeMateril.SetVector("_WorldLightPos", new Vector4(lightTransform.position.x, lightTransform.position.y, lightTransform.position.z, distance));
  28. }
  29. }

效果如下(恩,参数调的猛了点,不过我喜欢!):

简单分析一下这个效果的实现。首先,我们需要确定只有受光面才沿着光方向挤出,所以这个时候就要想起diffuse的计算方式,直接用法线方向点乘光线方向,这里我们直接把世界空间光位置转到模型空间进而计算了模型空间的光方向。点乘的结果就代表了光方向与法线方向的贴合程度,我们通过sign函数直接把这个值变成一个-1,1的控制值,然后再进行一个最常见的*0.5+0.5变换,-1,1变化为0,1。这样这个点乘结果就可以作为我们判断是受光面还是背光面的控制值了。然后我们将物体受光面的每个顶点沿着光的反方向增加一个偏移值,就达到了“挤出”的效果,关于顶点偏移,在描边效果以及溶解效果也都有使用。上面的操作都是在vertex阶段进行,在pixel阶段,我们只需要采样一下贴图,个人感觉还是直接采样对象本身的贴图就好了,有一种对象自身的颜色被光“照”出来的感觉(恩,这么说非常不专业,然而我也没有想好要怎么解释这个现象)。为了让效果好一些,可以适当控制一下光线沿距离的衰减等等。

Raidal Blur Postprocessing径向模糊后处理

哇,终于到了后处理了,我还是这个观点,后处理是最能提升游戏画面效果的方式之一,所以我也是最喜欢后处理的,哈哈。GodRay的后处理实现的效果也是要比前两种更加真实,也适用于更多情况,当然也比前两者耗费更多。文章开头截图中除了《Inside》和《耻辱-外魔之死》外的几个圣光效果,个人感觉应该是用这种方式实现的。径向模糊后处理方式实现GodRay,可以参考《GPU Gems 3 -Volumetric Light Scattering as a Post-Process》这篇文章。

在后处理中,我们只有一张屏幕的RT。所以,我们需要用图像的方式来进行处理。首先,我们要找到光点,最简单的方式,就是直接用颜色阈值提取高亮部分,这个就是我们在Bloom效果中使用的方法,通过亮度提取出一张所谓高光点的部分;然后将这张图进行径向模糊,把亮度部分向一个方向延伸,迭代几次之后我们就能够得到一个光束的效果;最终我们再将这个光束图与屏幕原始图像叠加就得到了体积光的效果。

比如一个原始的天空效果:

经过提取高亮=>径向模糊=>增大模糊半径再次径向模糊=>与原图叠加的效果分别如下图:

下面附上shader代码:

  1. //puppet_master
  2. //2018.4.20
  3. //后处理方式实现GodRay
  4. Shader "GodRay/PostEffect" {
  5. Properties{
  6. _MainTex("Base (RGB)", 2D) = "white" {}
  7. _BlurTex("Blur", 2D) = "white"{}
  8. }
  9. CGINCLUDE
  10. #define RADIAL_SAMPLE_COUNT 6
  11. #include "UnityCG.cginc"
  12. //用于阈值提取高亮部分
  13. struct v2f_threshold
  14. {
  15. float4 pos : SV_POSITION;
  16. float2 uv : TEXCOORD0;
  17. };
  18. //用于blur
  19. struct v2f_blur
  20. {
  21. float4 pos : SV_POSITION;
  22. float2 uv  : TEXCOORD0;
  23. float2 blurOffset : TEXCOORD1;
  24. };
  25. //用于最终融合
  26. struct v2f_merge
  27. {
  28. float4 pos : SV_POSITION;
  29. float2 uv  : TEXCOORD0;
  30. float2 uv1 : TEXCOORD1;
  31. };
  32. sampler2D _MainTex;
  33. float4 _MainTex_TexelSize;
  34. sampler2D _BlurTex;
  35. float4 _BlurTex_TexelSize;
  36. float4 _ViewPortLightPos;
  37. float4 _offsets;
  38. float4 _ColorThreshold;
  39. float4 _LightColor;
  40. float _LightFactor;
  41. float _PowFactor;
  42. float _LightRadius;
  43. //高亮部分提取shader
  44. v2f_threshold vert_threshold(appdata_img v)
  45. {
  46. v2f_threshold o;
  47. o.pos = UnityObjectToClipPos(v.vertex);
  48. o.uv = v.texcoord.xy;
  49. //dx中纹理从左上角为初始坐标,需要反向
  50. #if UNITY_UV_STARTS_AT_TOP
  51. if (_MainTex_TexelSize.y < 0)
  52. o.uv.y = 1 - o.uv.y;
  53. #endif
  54. return o;
  55. }
  56. fixed4 frag_threshold(v2f_threshold i) : SV_Target
  57. {
  58. fixed4 color = tex2D(_MainTex, i.uv);
  59. float distFromLight = length(_ViewPortLightPos.xy - i.uv);
  60. float distanceControl = saturate(_LightRadius - distFromLight);
  61. //仅当color大于设置的阈值的时候才输出
  62. float4 thresholdColor = saturate(color - _ColorThreshold) * distanceControl;
  63. float luminanceColor = Luminance(thresholdColor.rgb);
  64. luminanceColor = pow(luminanceColor, _PowFactor);
  65. return fixed4(luminanceColor, luminanceColor, luminanceColor, 1);
  66. }
  67. //径向模糊 vert shader
  68. v2f_blur vert_blur(appdata_img v)
  69. {
  70. v2f_blur o;
  71. o.pos = UnityObjectToClipPos(v.vertex);
  72. o.uv = v.texcoord.xy;
  73. //径向模糊采样偏移值*沿光的方向权重
  74. o.blurOffset = _offsets * (_ViewPortLightPos.xy - o.uv);
  75. return o;
  76. }
  77. //径向模拟pixel shader
  78. fixed4 frag_blur(v2f_blur i) : SV_Target
  79. {
  80. half4 color = half4(0,0,0,0);
  81. for(int j = 0; j < RADIAL_SAMPLE_COUNT; j++)
  82. {
  83. color += tex2D(_MainTex, i.uv.xy);
  84. i.uv.xy += i.blurOffset;
  85. }
  86. return color / RADIAL_SAMPLE_COUNT;
  87. }
  88. //融合vertex shader
  89. v2f_merge vert_merge(appdata_img v)
  90. {
  91. v2f_merge o;
  92. //mvp矩阵变换
  93. o.pos = UnityObjectToClipPos(v.vertex);
  94. //uv坐标传递
  95. o.uv.xy = v.texcoord.xy;
  96. o.uv1.xy = o.uv.xy;
  97. #if UNITY_UV_STARTS_AT_TOP
  98. if (_MainTex_TexelSize.y < 0)
  99. o.uv.y = 1 - o.uv.y;
  100. #endif
  101. return o;
  102. }
  103. fixed4 frag_merge(v2f_merge i) : SV_Target
  104. {
  105. fixed4 ori = tex2D(_MainTex, i.uv1);
  106. fixed4 blur = tex2D(_BlurTex, i.uv);
  107. //输出= 原始图像,叠加体积光贴图
  108. return ori + _LightFactor * blur * _LightColor;
  109. }
  110. ENDCG
  111. SubShader
  112. {
  113. //pass 0: 提取高亮部分
  114. Pass
  115. {
  116. ZTest Off
  117. Cull Off
  118. ZWrite Off
  119. Fog{ Mode Off }
  120. CGPROGRAM
  121. #pragma vertex vert_threshold
  122. #pragma fragment frag_threshold
  123. ENDCG
  124. }
  125. //pass 1: 径向模糊
  126. Pass
  127. {
  128. ZTest Off
  129. Cull Off
  130. ZWrite Off
  131. Fog{ Mode Off }
  132. CGPROGRAM
  133. #pragma vertex vert_blur
  134. #pragma fragment frag_blur
  135. ENDCG
  136. }
  137. //pass 2: 将体积光模糊图与原图融合
  138. Pass
  139. {
  140. ZTest Off
  141. Cull Off
  142. ZWrite Off
  143. Fog{ Mode Off }
  144. CGPROGRAM
  145. #pragma vertex vert_merge
  146. #pragma fragment frag_merge
  147. ENDCG
  148. }
  149. }
  150. }

C#部分代码如下,PostEffectBase基类在屏幕校色这篇文章里(>_<半年没更新。。。发现最多的评论就是问这个类在哪的。。。唉,我记得我应该每篇文章都有注明的呀。。。):

  1. using UnityEngine;
  2. using System.Collections;
  3. [ExecuteInEditMode]
  4. public class GodRayPostEffect : PostEffectBase
  5. {
  6. //高亮部分提取阈值
  7. public Color colorThreshold = Color.gray;
  8. //体积光颜色
  9. public Color lightColor = Color.white;
  10. //光强度
  11. [Range(0.0f, 20.0f)]
  12. public float lightFactor = 0.5f;
  13. //径向模糊uv采样偏移值
  14. [Range(0.0f, 10.0f)]
  15. public float samplerScale = 1;
  16. //Blur迭代次数
  17. [Range(1,3)]
  18. public int blurIteration = 2;
  19. //降低分辨率倍率
  20. [Range(0, 3)]
  21. public int downSample = 1;
  22. //光源位置
  23. public Transform lightTransform;
  24. //产生体积光的范围
  25. [Range(0.0f, 5.0f)]
  26. public float lightRadius = 2.0f;
  27. //提取高亮结果Pow倍率,适当降低颜色过亮的情况
  28. [Range(1.0f, 4.0f)]
  29. public float lightPowFactor = 3.0f;
  30. private Camera targetCamera = null;
  31. void Awake()
  32. {
  33. targetCamera = GetComponent<Camera>();
  34. }
  35. void OnRenderImage(RenderTexture source, RenderTexture destination)
  36. {
  37. if (_Material && targetCamera)
  38. {
  39. int rtWidth = source.width >> downSample;
  40. int rtHeight = source.height >> downSample;
  41. //RT分辨率按照downSameple降低
  42. RenderTexture temp1 = RenderTexture.GetTemporary(rtWidth, rtHeight, 0, source.format);
  43. //计算光源位置从世界空间转化到视口空间
  44. Vector3 viewPortLightPos = lightTransform == null ? new Vector3(.5f, .5f, 0) : targetCamera.WorldToViewportPoint(lightTransform.position);
  45. //将shader变量改为PropertyId,以及将float放在Vector中一起传递给Material会更省一些,but,我懒
  46. _Material.SetVector("_ColorThreshold", colorThreshold);
  47. _Material.SetVector("_ViewPortLightPos", new Vector4(viewPortLightPos.x, viewPortLightPos.y, viewPortLightPos.z, 0));
  48. _Material.SetFloat("_LightRadius", lightRadius);
  49. _Material.SetFloat("_PowFactor", lightPowFactor);
  50. //根据阈值提取高亮部分,使用pass0进行高亮提取,比Bloom多一步计算光源距离剔除光源范围外的部分
  51. Graphics.Blit(source, temp1, _Material, 0);
  52. _Material.SetVector("_ViewPortLightPos", new Vector4(viewPortLightPos.x, viewPortLightPos.y, viewPortLightPos.z, 0));
  53. _Material.SetFloat("_LightRadius", lightRadius);
  54. //径向模糊的采样uv偏移值
  55. float samplerOffset = samplerScale / source.width;
  56. //径向模糊,两次一组,迭代进行
  57. for (int i = 0; i < blurIteration; i++)
  58. {
  59. RenderTexture temp2 = RenderTexture.GetTemporary(rtWidth, rtHeight, 0, source.format);
  60. float offset = samplerOffset * (i * 2 + 1);
  61. _Material.SetVector("_offsets", new Vector4(offset, offset, 0, 0));
  62. Graphics.Blit(temp1, temp2, _Material, 1);
  63. offset = samplerOffset * (i * 2 + 2);
  64. _Material.SetVector("_offsets", new Vector4(offset, offset, 0, 0));
  65. Graphics.Blit(temp2, temp1, _Material, 1);
  66. RenderTexture.ReleaseTemporary(temp2);
  67. }
  68. _Material.SetTexture("_BlurTex", temp1);
  69. _Material.SetVector("_LightColor", lightColor);
  70. _Material.SetFloat("_LightFactor", lightFactor);
  71. //最终混合,将体积光径向模糊图与原始图片混合,pass2
  72. Graphics.Blit(source, destination, _Material, 2);
  73. //释放申请的RT
  74. RenderTexture.ReleaseTemporary(temp1);
  75. }
  76. else
  77. {
  78. Graphics.Blit(source, destination);
  79. }
  80. }
  81. }

效果如下:

上面的效果在提取高亮部分有一个将高亮部分阈值转灰度的操作,不过个人感觉不转灰度也挺好看的,直接用带颜色信息的体积光进行Blur在叠回去,有一种散射的赶脚,不过就是颜色不好控制了:

恩,阳光透过云层洒下万丈光芒,瞬间整个人心情都变好啦,哈哈哈哈哈。

再来一张动态的类似文章开头天刀截图的那种效果:

but,这个效果还没有完,因为还有一个很严重的问题,提取高亮部分采用的是颜色提取的方式,那么,不管远近,如果镜头前本身就有很亮的东西,那瞬间就会被闪瞎眼,比如上面的模型加了一个自发光效果的话,就真的变成God了:

所以,这是直接用颜色提取高亮部分的弊端,因为仅靠一张RT图像信息,我们没有办法分辨哪些才真正的光。我们需要做的是在提取高亮或者最终混合阶段,剔除掉不应该显示高光的部分,这样遮挡效果会更好,并且不会出现上图所示的情况。

所以下面要搞得就是用一个Mask,剔除掉不应该显示为高亮的部分。首先来个实现上最简单的,但是可能比较费的方法,我们可以直接用深度进行剔除,因为所谓的GodRay大部分应该都是天空部分的光源,而天空盒的深度为最大值,在计算时把深度很小的部分直接剔除掉。不多说,上代码:

  1. //puppet_master
  2. //2018.4.20
  3. //后处理方式实现GodRay,使用深度剔除无需产生光源的部分
  4. Shader "GodRay/PostEffect" {
  5. Properties{
  6. _MainTex("Base (RGB)", 2D) = "white" {}
  7. _BlurTex("Blur", 2D) = "white"{}
  8. }
  9. CGINCLUDE
  10. #define RADIAL_SAMPLE_COUNT 6
  11. #include "UnityCG.cginc"
  12. //用于阈值提取高亮部分
  13. struct v2f_threshold
  14. {
  15. float4 pos : SV_POSITION;
  16. float2 uv : TEXCOORD0;
  17. };
  18. //用于blur
  19. struct v2f_blur
  20. {
  21. float4 pos : SV_POSITION;
  22. float2 uv  : TEXCOORD0;
  23. float2 blurOffset : TEXCOORD1;
  24. };
  25. //用于最终融合
  26. struct v2f_merge
  27. {
  28. float4 pos : SV_POSITION;
  29. float2 uv  : TEXCOORD0;
  30. float2 uv1 : TEXCOORD1;
  31. };
  32. sampler2D _CameraDepthTexture;
  33. sampler2D _MainTex;
  34. float4 _MainTex_TexelSize;
  35. sampler2D _BlurTex;
  36. float4 _BlurTex_TexelSize;
  37. float4 _ViewPortLightPos;
  38. float4 _offsets;
  39. float4 _ColorThreshold;
  40. float4 _LightColor;
  41. float _LightFactor;
  42. float _PowFactor;
  43. float _LightRadius;
  44. float _DepthThreshold;
  45. //高亮部分提取shader
  46. v2f_threshold vert_threshold(appdata_img v)
  47. {
  48. v2f_threshold o;
  49. o.pos = UnityObjectToClipPos(v.vertex);
  50. o.uv = v.texcoord.xy;
  51. //dx中纹理从左上角为初始坐标,需要反向
  52. #if UNITY_UV_STARTS_AT_TOP
  53. if (_MainTex_TexelSize.y < 0)
  54. o.uv.y = 1 - o.uv.y;
  55. #endif
  56. return o;
  57. }
  58. fixed4 frag_threshold(v2f_threshold i) : SV_Target
  59. {
  60. fixed4 color = tex2D(_MainTex, i.uv);
  61. float distFromLight = length(_ViewPortLightPos.xy - i.uv);
  62. float distanceControl = saturate(_LightRadius - distFromLight);
  63. //仅当color大于设置的阈值的时候才输出
  64. float4 thresholdColor = saturate(color - _ColorThreshold) * distanceControl;
  65. float luminanceColor = Luminance(thresholdColor.rgb);
  66. luminanceColor = pow(luminanceColor, _PowFactor);
  67. //采样深度贴图
  68. float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
  69. //转换回01区间
  70. depth = Linear01Depth (depth);
  71. //将深度小于阈值的部分直接变为0作为系数乘原来的结果,剃掉近处的内容
  72. luminanceColor *= sign(saturate(depth - _DepthThreshold));
  73. return fixed4(luminanceColor, luminanceColor, luminanceColor, 1);
  74. }
  75. //径向模糊 vert shader
  76. v2f_blur vert_blur(appdata_img v)
  77. {
  78. v2f_blur o;
  79. o.pos = UnityObjectToClipPos(v.vertex);
  80. o.uv = v.texcoord.xy;
  81. //径向模糊采样偏移值*沿光的方向权重
  82. o.blurOffset = _offsets * (_ViewPortLightPos.xy - o.uv);
  83. return o;
  84. }
  85. //径向模拟pixel shader
  86. fixed4 frag_blur(v2f_blur i) : SV_Target
  87. {
  88. half4 color = half4(0,0,0,0);
  89. for(int j = 0; j < RADIAL_SAMPLE_COUNT; j++)
  90. {
  91. color += tex2D(_MainTex, i.uv.xy);
  92. i.uv.xy += i.blurOffset;
  93. }
  94. return color / RADIAL_SAMPLE_COUNT;
  95. }
  96. //融合vertex shader
  97. v2f_merge vert_merge(appdata_img v)
  98. {
  99. v2f_merge o;
  100. //mvp矩阵变换
  101. o.pos = UnityObjectToClipPos(v.vertex);
  102. //uv坐标传递
  103. o.uv.xy = v.texcoord.xy;
  104. o.uv1.xy = o.uv.xy;
  105. #if UNITY_UV_STARTS_AT_TOP
  106. if (_MainTex_TexelSize.y < 0)
  107. o.uv.y = 1 - o.uv.y;
  108. #endif
  109. return o;
  110. }
  111. fixed4 frag_merge(v2f_merge i) : SV_Target
  112. {
  113. fixed4 ori = tex2D(_MainTex, i.uv1);
  114. fixed4 blur = tex2D(_BlurTex, i.uv);
  115. //输出= 原始图像,叠加体积光贴图
  116. fixed4 lightColor =  _LightFactor * blur * _LightColor;
  117. return lightColor + ori;
  118. }
  119. ENDCG
  120. SubShader
  121. {
  122. //pass 0: 提取高亮部分
  123. Pass
  124. {
  125. ZTest Off
  126. Cull Off
  127. ZWrite Off
  128. Fog{ Mode Off }
  129. CGPROGRAM
  130. #pragma vertex vert_threshold
  131. #pragma fragment frag_threshold
  132. ENDCG
  133. }
  134. //pass 1: 径向模糊
  135. Pass
  136. {
  137. ZTest Off
  138. Cull Off
  139. ZWrite Off
  140. Fog{ Mode Off }
  141. CGPROGRAM
  142. #pragma vertex vert_blur
  143. #pragma fragment frag_blur
  144. ENDCG
  145. }
  146. //pass 2: 将体积光模糊图与原图融合
  147. Pass
  148. {
  149. ZTest Off
  150. Cull Off
  151. ZWrite Off
  152. Fog{ Mode Off }
  153. CGPROGRAM
  154. #pragma vertex vert_merge
  155. #pragma fragment frag_merge
  156. ENDCG
  157. }
  158. }
  159. }

C#部分,增加了一个在激活时开启相机深度的操作,并添加了一个深度阈值的系数:

  1. /********************************************************************
  2. FileName: GodRayPostEffect.cs
  3. Description:
  4. Created: 2018/04/24
  5. history: 24:4:2018 0:11 by zhangjian
  6. *********************************************************************/
  7. using UnityEngine;
  8. [ExecuteInEditMode]
  9. public class GodRayPostEffect : PostEffectBase
  10. {
  11. //深度控制阈值
  12. [Range(0.0f, 1.0f)]
  13. public float depthThreshold = 0.8f;
  14. //高亮部分提取阈值
  15. public Color colorThreshold = Color.gray;
  16. //体积光颜色
  17. public Color lightColor = Color.white;
  18. //光强度
  19. [Range(0.0f, 20.0f)]
  20. public float lightFactor = 0.5f;
  21. //径向模糊uv采样偏移值
  22. [Range(0.0f, 10.0f)]
  23. public float samplerScale = 1;
  24. //Blur迭代次数
  25. [Range(1,3)]
  26. public int blurIteration = 2;
  27. //降低分辨率倍率
  28. [Range(0, 3)]
  29. public int downSample = 1;
  30. //光源位置
  31. public Transform lightTransform;
  32. //产生体积光的范围
  33. [Range(0.0f, 5.0f)]
  34. public float lightRadius = 2.0f;
  35. //提取高亮结果Pow倍率,适当降低颜色过亮的情况
  36. [Range(1.0f, 4.0f)]
  37. public float lightPowFactor = 3.0f;
  38. private Camera targetCamera = null;
  39. void Awake()
  40. {
  41. targetCamera = GetComponent<Camera>();
  42. }
  43. void OnEnable()
  44. {
  45. targetCamera.depthTextureMode = DepthTextureMode.Depth;
  46. }
  47. void OnDistable()
  48. {
  49. targetCamera.depthTextureMode = DepthTextureMode.None;
  50. }
  51. void OnRenderImage(RenderTexture source, RenderTexture destination)
  52. {
  53. if (_Material && targetCamera)
  54. {
  55. int rtWidth = source.width >> downSample;
  56. int rtHeight = source.height >> downSample;
  57. //RT分辨率按照downSameple降低
  58. RenderTexture temp1 = RenderTexture.GetTemporary(rtWidth, rtHeight, 0, source.format);
  59. //计算光源位置从世界空间转化到视口空间
  60. Vector3 viewPortLightPos = lightTransform == null ? new Vector3(.5f, .5f, 0) : targetCamera.WorldToViewportPoint(lightTransform.position);
  61. //将shader变量改为PropertyId,以及将float放在Vector中一起传递给Material会更省一些,but,我懒
  62. _Material.SetVector("_ColorThreshold", colorThreshold);
  63. _Material.SetVector("_ViewPortLightPos", new Vector4(viewPortLightPos.x, viewPortLightPos.y, viewPortLightPos.z, 0));
  64. _Material.SetFloat("_LightRadius", lightRadius);
  65. _Material.SetFloat("_PowFactor", lightPowFactor);
  66. _Material.SetFloat("_DepthThreshold", depthThreshold);
  67. //根据阈值提取高亮部分,使用pass0进行高亮提取,比Bloom多一步计算光源距离剔除光源范围外的部分
  68. Graphics.Blit(source, temp1, _Material, 0);
  69. _Material.SetVector("_ViewPortLightPos", new Vector4(viewPortLightPos.x, viewPortLightPos.y, viewPortLightPos.z, 0));
  70. _Material.SetFloat("_LightRadius", lightRadius);
  71. //径向模糊的采样uv偏移值
  72. float samplerOffset = samplerScale / source.width;
  73. //径向模糊,两次一组,迭代进行
  74. for (int i = 0; i < blurIteration; i++)
  75. {
  76. RenderTexture temp2 = RenderTexture.GetTemporary(rtWidth, rtHeight, 0, source.format);
  77. float offset = samplerOffset * (i * 2 + 1);
  78. _Material.SetVector("_offsets", new Vector4(offset, offset, 0, 0));
  79. Graphics.Blit(temp1, temp2, _Material, 1);
  80. offset = samplerOffset * (i * 2 + 2);
  81. _Material.SetVector("_offsets", new Vector4(offset, offset, 0, 0));
  82. Graphics.Blit(temp2, temp1, _Material, 1);
  83. RenderTexture.ReleaseTemporary(temp2);
  84. }
  85. _Material.SetTexture("_BlurTex", temp1);
  86. _Material.SetVector("_LightColor", lightColor);
  87. _Material.SetFloat("_LightFactor", lightFactor);
  88. //最终混合,将体积光径向模糊图与原始图片混合,pass2
  89. Graphics.Blit(source, destination, _Material, 2);
  90. //释放申请的RT
  91. RenderTexture.ReleaseTemporary(temp1);
  92. }
  93. else
  94. {
  95. Graphics.Blit(source, destination);
  96. }
  97. }
  98. }

这样,即使我们贴脸一个闪瞎眼的模型,也不会出现上面那种情况,因为我们在提取阈值的时候,就把它扣掉啦,下图左侧是抠图的Pass,右图是最终结果:

Unity Shader-GodRay,体积光(BillBoard,Volume Shadow,Raidal Blur,Ray-Marching)的更多相关文章

  1. Unity Shader 入门精要学习 (冯乐乐 著)

    第1篇 基础篇 第1章 欢迎来到Shader的世界 第2章 渲染流水线 第3章 Unity Shader 基础 第4章 学习Shader所需的数学基础 第2篇 初级篇 第5章 开始Unity Shad ...

  2. 【Unity Shaders】ShadowGun系列之二——雾和体积光

    写在前面 体积光,这个名称是God Rays的中文翻译,感觉不是很形象.God Rays其实是Crepuscular rays在图形学中的说法,而Crepuscular rays的意思是云隙光.曙光. ...

  3. 【Unity Shader】(七) ------ 复杂的光照(下)

    笔者使用的是 Unity 2018.2.0f2 + VS2017,建议读者使用与 Unity 2018 相近的版本,避免一些因为版本不一致而出现的问题.              [Unity Sha ...

  4. 【转】《Unity Shader入门精要》冯乐乐著 书中彩图

    为方便个人手机学习时候查阅,从网上转来这些彩图. 如属过当行为,联系本人删除. 勘错表 http://candycat1992.github.io/unity_shaders_book/unity_s ...

  5. Unity Shader入门精要学习笔记 - 第9章 更复杂的光照

    转载自 冯乐乐的<Unity Shader入门精要> Unity 的渲染路径 在Unity里,渲染路径决定了光照是如何应该到Unity Shader 中的.因此,如果要和光源打交道,我们需 ...

  6. 【Unity Shader】Unity Chan的卡通材质

    写在前面 时隔两个月我终于来更新博客了,之前一直在学东西,做一些项目,感觉没什么可以分享的就一直没写.本来之前打算写云彩渲染或是Compute Shader的,觉得时间比较长所以打算先写个简单的. 今 ...

  7. Unity3D学习(六):《Unity Shader入门精要》——Unity的基础光照

    前言 光学中,我们是用辐射度来量化光. 光照按照不同的散射方向分为:漫反射(diffuse)和高光反射(specular).高光反射描述物体是如何反射光线的,漫反射则表示有多少光线会被折射.吸收和散射 ...

  8. Unity Shader 基础(3) 获取深度纹理

    Unity提供了很多Image Effect效果,包含Global Fog.DOF.Boom.Blur.Edge Detection等等,这些效果里面都会使用到摄像机深度或者根据深度还原世界坐标实现各 ...

  9. 【Unity Shader】(三) ------ 光照模型原理及漫反射和高光反射的实现

    [Unity Shader](三) ---------------- 光照模型原理及漫反射和高光反射的实现 [Unity Shader](四) ------ 纹理之法线纹理.单张纹理及遮罩纹理的实现 ...

随机推荐

  1. Friendly Date Ranges-freecodecamp算法题目

    Friendly Date Ranges 1.要求 把常见的日期格式如:YYYY-MM-DD 转换成一种更易读的格式. 易读格式应该是用月份名称代替月份数字,用序数词代替数字来表示天 (1st 代替 ...

  2. zeppelin ERROR总结

    ERROR [2017-03-23 20:01:50,799] ({qtp331657670-221} NotebookServer.java[onMessage]:221) - Can't hand ...

  3. linux面试集

    shell:1.$# 和 $*之类的特殊变量 特殊变量列表 变量 含义 $0 当前脚本的文件名 $n 传递给脚本或函数的参数.n是一个数字,表示第几个参数.例如,第一个参数就是$1 $# 传递给脚本或 ...

  4. 微信小程序插件内页面跳转和参数传递

    在此以插件开发中文章列表跳传文章详情为例. 1.首先在插件中的文章列表页面wxml中绑定跳转事件. bindtap='url' data-id="{{item.article_id}}&qu ...

  5. Ado访问sqlserver 端口号非1433时 连接串的写法

    Provider=SQLOLEDB.;Persist Security Info=False;Data Source=hostName,Port //注意用 逗号分隔主机名与端口号

  6. Choosing Capital for Treeland CodeForces - 219D (树形DP)

    传送门 The country Treeland consists of n cities, some pairs of them are connected with unidirectional  ...

  7. POJ:3685-Matrix

    Matrix Time Limit: 6000MS Memory Limit: 65536K Total Submissions: 7879 Accepted: 2374 Description Gi ...

  8. POJ:2010-Moo University - Financial Aid

    Moo University - Financial Aid Time Limit: 1000MS Memory Limit: 30000K Total Submissions: 10894 Acce ...

  9. 笔记-redis深入学习-1

    笔记-redis深入学习-1 redis的基本使用已经会了,但存储和读取只是数据库系统最基础的功能: 数据库系统还得为可靠实现这两者提供一系列保证: 数据.操作备份和恢复,主要是持久化: 高可用:主要 ...

  10. contest0 from codechef

    A  CodeChef - KSPHERES 中文题意  Mandarin Chinese Eugene has a sequence of upper hemispheres and another ...