Unity Shader-GodRay,体积光(BillBoard,Volume Shadow,Raidal Blur,Ray-Marching)

前言

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


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


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


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

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


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


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


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


简介

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

下面附上一段代码:

//puppet_master
//2018.4.15
//Shadow Gun中贴片方式实现GodRay代码,升级unity2017.3,增长一些注释
Shader "GodRay/ShadowGunSimple" 
{

	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
		//增长一个颜色控制(仅RGB生效)
		_Color("Color", Color) = (1,1,1,1)
	}

	SubShader 
	{	
		Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
		
		//叠加方式Blend
		Blend One One
		Cull Off 
		Lighting Off 
		ZWrite Off 
		Fog { Color (0,0,0,0) }
		
		CGINCLUDE	
		#include "UnityCG.cginc"
		sampler2D _MainTex;
		
		float _FadeOutDistNear;
		float _FadeOutDistFar;
		float _Multiplier;
		float _ContractionAmount;
		float4 _Color;

		struct v2f {
			float4	pos	: SV_POSITION;
			float2	uv		: TEXCOORD0;
			fixed4	color	: TEXCOORD1;
		};
		
		v2f vert (appdata_full v)
		{
			v2f 		o;
			//update mul(UNITY_MATRIX_MV, v.vertex) 根据UNITY_USE_PREMULTIPLIED_MATRICES宏控制,能够预计算矩阵,减小逐顶点计算
			float3		viewPos		= UnityObjectToViewPos(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;
			//沿normal反方向根据fade系数控制顶点位置缩进,刷了顶点色控制哪些顶点须要缩进
			//黑科技:mesh是特制的,normal方向是沿着面片方向的,而非正常的垂直于面片
			vpos.xyz -=   v.normal * saturate(1 - nfadeout) * v.color.a * _ContractionAmount;
							
			o.uv	= v.texcoord.xy;
			o.pos	= UnityObjectToClipPos(vpos);
			//直接在vert中计算淡出效果
			o.color	= nfadeout * v.color * _Multiplier* _Color;
							
			return o;
		}
		
		fixed4 frag (v2f i) : COLOR
		{			
				return tex2D (_MainTex, i.uv.xy) * i.color ;
		}
		ENDCG

		Pass 
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#pragma fragmentoption ARB_precision_hint_fastest			
			ENDCG 
		}	
	}
}

效果以下:


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

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


Volume Shadow光方向挤出

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

Shader代码以下:

//puppet_master
//2018.4.15
//GodRay,体积阴影扩展,沿光方向挤出顶点实现
Shader "GodRay/VolumeShadow" 
{

	Properties 
	{
		_Color("Color", Color) = (1,1,1,0.002)
		_MainTex ("Base texture", 2D) = "white" {}
		_ExtrusionFactor("Extrusion", Range(0, 2)) = 0.1
		_Intensity("Intensity", Range(0, 10)) = 1
		_WorldLightPos("LightPos", Vector) = (0,0,0,0)
	}

	SubShader 
	{	
		Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent + 1" }
		
		Blend SrcAlpha OneMinusSrcAlpha
		Cull Off 
		ZWrite Off 
		Fog { Color (0,0,0,0) }
		
		CGINCLUDE	
		#include "UnityCG.cginc"
		
		float4 _Color;
		float4 _WorldLightPos;
		sampler2D _MainTex;
		float _ExtrusionFactor;
		float _Intensity;

		struct v2f {
			float4	pos		: SV_POSITION;
			float2	uv		: TEXCOORD0;
			float distance : TEXCOORD1;
		};
		
		v2f vert (appdata_base v)
		{
			v2f o;
			//转化到物体空间计算
			float3 objectLightPos = mul(unity_WorldToObject, _WorldLightPos.xyz).xyz;
			float3 objectLightDir = objectLightPos - v.vertex.xyz;
			float dotValue = dot(objectLightDir, v.normal);
			//light dot normal,*0.5+0.5转化为0,1控制变量,控制受光面挤出
			float controlValue = sign(dotValue) * 0.5 + 0.5;
			float4 vpos = v.vertex;
			//受光面沿法线反方向挤出顶点
			vpos.xyz -= objectLightDir * _ExtrusionFactor * controlValue;
							
			o.uv	= v.texcoord.xy;
			o.pos	= UnityObjectToClipPos(vpos);
			o.distance = length(objectLightDir);
							
			return o;
		}
		
		fixed4 frag (v2f i) : COLOR
		{	
			fixed4 tex = tex2D(_MainTex, i.uv);
			//顶点到光的距离与物体到光的距离控制一个衰减值
			float att = i.distance / _WorldLightPos.w;
			return _Color * tex * att * _Intensity;
		}
		ENDCG

		Pass 
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#pragma fragmentoption ARB_precision_hint_fastest			
			ENDCG 
		}	
	}
}

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

/********************************************************************
 FileName: GodRayVolumeHelper.cs
 Description:
 Created: 2018/04/20
 history: 20:4:2018 0:24 by zhangjian
*********************************************************************/
using UnityEngine;

[ExecuteInEditMode]
public class GodRayVolumeHelper : MonoBehaviour {

    public Transform lightTransform;
    private Material godRayVolumeMateril;

    void Awake()
    {
        var renderer = GetComponentInChildren<Renderer>();
        foreach(var mat in renderer.sharedMaterials)
        {
            if (mat.shader.name.Contains("VolumeShadow"))
                godRayVolumeMateril = mat;
        }
    }
	
	// Update is called once per frame
	void Update ()
    {
        if (lightTransform == null || godRayVolumeMateril == null)
            return;
        float distance = Vector3.Distance(lightTransform.position, transform.position);
        godRayVolumeMateril.SetVector("_WorldLightPos", new Vector4(lightTransform.position.x, lightTransform.position.y, lightTransform.position.z, distance));
    }
}

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


简单分析一下这个效果的实现。首先,咱们须要肯定只有受光面才沿着光方向挤出,因此这个时候就要想起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代码:

//puppet_master
//2018.4.20
//后处理方式实现GodRay
Shader "GodRay/PostEffect" {

	Properties{
		_MainTex("Base (RGB)", 2D) = "white" {}
		_BlurTex("Blur", 2D) = "white"{}
	}

	CGINCLUDE
	#define RADIAL_SAMPLE_COUNT 6
	#include "UnityCG.cginc"
	
	//用于阈值提取高亮部分
	struct v2f_threshold
	{
		float4 pos : SV_POSITION;
		float2 uv : TEXCOORD0;
	};

	//用于blur
	struct v2f_blur
	{
		float4 pos : SV_POSITION;
		float2 uv  : TEXCOORD0;
		float2 blurOffset : TEXCOORD1;
	};

	//用于最终融合
	struct v2f_merge
	{
		float4 pos : SV_POSITION;
		float2 uv  : TEXCOORD0;
		float2 uv1 : TEXCOORD1;
	};

	sampler2D _MainTex;
	float4 _MainTex_TexelSize;
	sampler2D _BlurTex;
	float4 _BlurTex_TexelSize;
	float4 _ViewPortLightPos;
	
	float4 _offsets;
	float4 _ColorThreshold;
	float4 _LightColor;
	float _LightFactor;
	float _PowFactor;
	float _LightRadius;

	//高亮部分提取shader
	v2f_threshold vert_threshold(appdata_img v)
	{
		v2f_threshold o;
		o.pos = UnityObjectToClipPos(v.vertex);
		o.uv = v.texcoord.xy;
		
		//dx中纹理从左上角为初始坐标,须要反向
#if UNITY_UV_STARTS_AT_TOP
		if (_MainTex_TexelSize.y < 0)
			o.uv.y = 1 - o.uv.y;
#endif	
		return o;
	}

	fixed4 frag_threshold(v2f_threshold i) : SV_Target
	{
		fixed4 color = tex2D(_MainTex, i.uv);
		float distFromLight = length(_ViewPortLightPos.xy - i.uv);
		float distanceControl = saturate(_LightRadius - distFromLight);
		//仅当color大于设置的阈值的时候才输出
		float4 thresholdColor = saturate(color - _ColorThreshold) * distanceControl;
		float luminanceColor = Luminance(thresholdColor.rgb);
		luminanceColor = pow(luminanceColor, _PowFactor);
		return fixed4(luminanceColor, luminanceColor, luminanceColor, 1);
	}

	//径向模糊 vert shader
	v2f_blur vert_blur(appdata_img v)
	{
		v2f_blur o;
		o.pos = UnityObjectToClipPos(v.vertex);
		o.uv = v.texcoord.xy;
		//径向模糊采样偏移值*沿光的方向权重
		o.blurOffset = _offsets * (_ViewPortLightPos.xy - o.uv);
		return o;
	}

	//径向模拟pixel shader
	fixed4 frag_blur(v2f_blur i) : SV_Target
	{
		half4 color = half4(0,0,0,0);
		for(int j = 0; j < RADIAL_SAMPLE_COUNT; j++)   
		{	
			color += tex2D(_MainTex, i.uv.xy);
			i.uv.xy += i.blurOffset; 	
		}
		return color / RADIAL_SAMPLE_COUNT;
	}

	//融合vertex shader
	v2f_merge vert_merge(appdata_img v)
	{
		v2f_merge o;
		//mvp矩阵变换
		o.pos = UnityObjectToClipPos(v.vertex);
		//uv坐标传递
		o.uv.xy = v.texcoord.xy;
		o.uv1.xy = o.uv.xy;
#if UNITY_UV_STARTS_AT_TOP
		if (_MainTex_TexelSize.y < 0)
			o.uv.y = 1 - o.uv.y;
#endif	
		return o;
	}

	fixed4 frag_merge(v2f_merge i) : SV_Target
	{
		fixed4 ori = tex2D(_MainTex, i.uv1);
		fixed4 blur = tex2D(_BlurTex, i.uv);
		//输出= 原始图像,叠加体积光贴图
		return ori + _LightFactor * blur * _LightColor;
	}

		ENDCG

	SubShader
	{
		//pass 0: 提取高亮部分
		Pass
		{
			ZTest Off
			Cull Off
			ZWrite Off
			Fog{ Mode Off }

			CGPROGRAM
			#pragma vertex vert_threshold
			#pragma fragment frag_threshold
			ENDCG
		}

		//pass 1: 径向模糊
		Pass
		{
			ZTest Off
			Cull Off
			ZWrite Off
			Fog{ Mode Off }

			CGPROGRAM
			#pragma vertex vert_blur
			#pragma fragment frag_blur
			ENDCG
		}

		//pass 2: 将体积光模糊图与原图融合
		Pass
		{

			ZTest Off
			Cull Off
			ZWrite Off
			Fog{ Mode Off }

			CGPROGRAM
			#pragma vertex vert_merge
			#pragma fragment frag_merge
			ENDCG
		}
	}
}

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

using UnityEngine;
using System.Collections;

[ExecuteInEditMode]
public class GodRayPostEffect : PostEffectBase
{
    //高亮部分提取阈值
    public Color colorThreshold = Color.gray;
    //体积光颜色
    public Color lightColor = Color.white;
    //光强度
    [Range(0.0f, 20.0f)]
    public float lightFactor = 0.5f;
    //径向模糊uv采样偏移值
    [Range(0.0f, 10.0f)]
    public float samplerScale = 1;
    //Blur迭代次数
    [Range(1,3)]
    public int blurIteration = 2;
    //下降分辨率倍率
    [Range(0, 3)]
    public int downSample = 1;
    //光源位置
    public Transform lightTransform;
    //产生体积光的范围
    [Range(0.0f, 5.0f)]
    public float lightRadius = 2.0f;
    //提取高亮结果Pow倍率,适当下降颜色过亮的状况
    [Range(1.0f, 4.0f)]
    public float lightPowFactor = 3.0f; 

    private Camera targetCamera = null;

    void Awake()
    {
        targetCamera = GetComponent<Camera>();
    }

    void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        if (_Material && targetCamera)
        {
            int rtWidth = source.width >> downSample;
            int rtHeight = source.height >> downSample;
            //RT分辨率按照downSameple下降
            RenderTexture temp1 = RenderTexture.GetTemporary(rtWidth, rtHeight, 0, source.format);

            //计算光源位置从世界空间转化到视口空间
            Vector3 viewPortLightPos = lightTransform == null ? new Vector3(.5f, .5f, 0) : targetCamera.WorldToViewportPoint(lightTransform.position);
          
            //将shader变量改成PropertyId,以及将float放在Vector中一块儿传递给Material会更省一些,but,我懒
            _Material.SetVector("_ColorThreshold", colorThreshold);
            _Material.SetVector("_ViewPortLightPos", new Vector4(viewPortLightPos.x, viewPortLightPos.y, viewPortLightPos.z, 0));
            _Material.SetFloat("_LightRadius", lightRadius);
            _Material.SetFloat("_PowFactor", lightPowFactor);
            //根据阈值提取高亮部分,使用pass0进行高亮提取,比Bloom多一步计算光源距离剔除光源范围外的部分
            Graphics.Blit(source, temp1, _Material, 0);

            _Material.SetVector("_ViewPortLightPos", new Vector4(viewPortLightPos.x, viewPortLightPos.y, viewPortLightPos.z, 0));
            _Material.SetFloat("_LightRadius", lightRadius);
            //径向模糊的采样uv偏移值
            float samplerOffset = samplerScale / source.width;
            //径向模糊,两次一组,迭代进行
            for (int i = 0; i < blurIteration; i++)
            {
                RenderTexture temp2 = RenderTexture.GetTemporary(rtWidth, rtHeight, 0, source.format);
                float offset = samplerOffset * (i * 2 + 1);
                _Material.SetVector("_offsets", new Vector4(offset, offset, 0, 0));
                Graphics.Blit(temp1, temp2, _Material, 1);

                offset = samplerOffset * (i * 2 + 2);
                _Material.SetVector("_offsets", new Vector4(offset, offset, 0, 0));
                Graphics.Blit(temp2, temp1, _Material, 1);
                RenderTexture.ReleaseTemporary(temp2);
            }
           
            _Material.SetTexture("_BlurTex", temp1);
            _Material.SetVector("_LightColor", lightColor);
            _Material.SetFloat("_LightFactor", lightFactor);
            //最终混合,将体积光径向模糊图与原始图片混合,pass2
            Graphics.Blit(source, destination, _Material, 2);

            //释放申请的RT
            RenderTexture.ReleaseTemporary(temp1);
        }
        else
        {
            Graphics.Blit(source, destination);
        }
    }
}

效果以下:


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


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

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


but,这个效果尚未完,由于还有一个很严重的问题,提取高亮部分采用的是颜色提取的方式,那么,无论远近,若是镜头前自己就有很亮的东西,那瞬间就会被闪瞎眼,好比上面的模型加了一个自发光效果的话,就真的变成God了:


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

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

//puppet_master
//2018.4.20
//后处理方式实现GodRay,使用深度剔除无需产生光源的部分
Shader "GodRay/PostEffect" {

	Properties{
		_MainTex("Base (RGB)", 2D) = "white" {}
		_BlurTex("Blur", 2D) = "white"{}
	}

	CGINCLUDE
	#define RADIAL_SAMPLE_COUNT 6
	#include "UnityCG.cginc"
	
	//用于阈值提取高亮部分
	struct v2f_threshold
	{
		float4 pos : SV_POSITION;
		float2 uv : TEXCOORD0;
	};

	//用于blur
	struct v2f_blur
	{
		float4 pos : SV_POSITION;
		float2 uv  : TEXCOORD0;
		float2 blurOffset : TEXCOORD1;
	};

	//用于最终融合
	struct v2f_merge
	{
		float4 pos : SV_POSITION;
		float2 uv  : TEXCOORD0;
		float2 uv1 : TEXCOORD1;
	};

	sampler2D _CameraDepthTexture;
	sampler2D _MainTex;
	float4 _MainTex_TexelSize;
	sampler2D _BlurTex;
	float4 _BlurTex_TexelSize;
	float4 _ViewPortLightPos;
	
	float4 _offsets;
	float4 _ColorThreshold;
	float4 _LightColor;
	float _LightFactor;
	float _PowFactor;
	float _LightRadius;
	float _DepthThreshold;

	//高亮部分提取shader
	v2f_threshold vert_threshold(appdata_img v)
	{
		v2f_threshold o;
		o.pos = UnityObjectToClipPos(v.vertex);
		o.uv = v.texcoord.xy;
		
		//dx中纹理从左上角为初始坐标,须要反向
#if UNITY_UV_STARTS_AT_TOP
		if (_MainTex_TexelSize.y < 0)
			o.uv.y = 1 - o.uv.y;
#endif	
		return o;
	}

	fixed4 frag_threshold(v2f_threshold i) : SV_Target
	{
		fixed4 color = tex2D(_MainTex, i.uv);
		float distFromLight = length(_ViewPortLightPos.xy - i.uv);
		float distanceControl = saturate(_LightRadius - distFromLight);
		//仅当color大于设置的阈值的时候才输出
		float4 thresholdColor = saturate(color - _ColorThreshold) * distanceControl;
		float luminanceColor = Luminance(thresholdColor.rgb);
		luminanceColor = pow(luminanceColor, _PowFactor);
		//采样深度贴图
		float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
		//转换回01区间
		depth = Linear01Depth (depth);
		//将深度小于阈值的部分直接变为0做为系数乘原来的结果,剃掉近处的内容
		luminanceColor *= sign(saturate(depth - _DepthThreshold));
		return fixed4(luminanceColor, luminanceColor, luminanceColor, 1);
	}

	//径向模糊 vert shader
	v2f_blur vert_blur(appdata_img v)
	{
		v2f_blur o;
		o.pos = UnityObjectToClipPos(v.vertex);
		o.uv = v.texcoord.xy;
		//径向模糊采样偏移值*沿光的方向权重
		o.blurOffset = _offsets * (_ViewPortLightPos.xy - o.uv);
		return o;
	}

	//径向模拟pixel shader
	fixed4 frag_blur(v2f_blur i) : SV_Target
	{
		half4 color = half4(0,0,0,0);
		for(int j = 0; j < RADIAL_SAMPLE_COUNT; j++)   
		{	
			color += tex2D(_MainTex, i.uv.xy);
			i.uv.xy += i.blurOffset; 	
		}
		return color / RADIAL_SAMPLE_COUNT;
	}

	//融合vertex shader
	v2f_merge vert_merge(appdata_img v)
	{
		v2f_merge o;
		//mvp矩阵变换
		o.pos = UnityObjectToClipPos(v.vertex);
		//uv坐标传递
		o.uv.xy = v.texcoord.xy;
		o.uv1.xy = o.uv.xy;
#if UNITY_UV_STARTS_AT_TOP
		if (_MainTex_TexelSize.y < 0)
			o.uv.y = 1 - o.uv.y;
#endif	
		return o;
	}

	fixed4 frag_merge(v2f_merge i) : SV_Target
	{
		fixed4 ori = tex2D(_MainTex, i.uv1);
		fixed4 blur = tex2D(_BlurTex, i.uv);
		
		//输出= 原始图像,叠加体积光贴图
		fixed4 lightColor =  _LightFactor * blur * _LightColor;
		return lightColor + ori;
	}

		ENDCG

	SubShader
	{
		//pass 0: 提取高亮部分
		Pass
		{
			ZTest Off
			Cull Off
			ZWrite Off
			Fog{ Mode Off }

			CGPROGRAM
			#pragma vertex vert_threshold
			#pragma fragment frag_threshold
			ENDCG
		}

		//pass 1: 径向模糊
		Pass
		{
			ZTest Off
			Cull Off
			ZWrite Off
			Fog{ Mode Off }

			CGPROGRAM
			#pragma vertex vert_blur
			#pragma fragment frag_blur
			ENDCG
		}

		//pass 2: 将体积光模糊图与原图融合
		Pass
		{

			ZTest Off
			Cull Off
			ZWrite Off
			Fog{ Mode Off }

			CGPROGRAM
			#pragma vertex vert_merge
			#pragma fragment frag_merge
			ENDCG
		}

	}
}
C#部分,增长了一个在激活时开启 相机深度的操做,并添加了一个深度阈值的系数:
/********************************************************************
 FileName: GodRayPostEffect.cs
 Description:
 Created: 2018/04/24
 history: 24:4:2018 0:11 by zhangjian
*********************************************************************/
using UnityEngine;

[ExecuteInEditMode]
public class GodRayPostEffect : PostEffectBase
{
    //深度控制阈值
    [Range(0.0f, 1.0f)]
    public float depthThreshold = 0.8f;
    //高亮部分提取阈值
    public Color colorThreshold = Color.gray;
    //体积光颜色
    public Color lightColor = Color.white;
    //光强度
    [Range(0.0f, 20.0f)]
    public float lightFactor = 0.5f;
    //径向模糊uv采样偏移值
    [Range(0.0f, 10.0f)]
    public float samplerScale = 1;
    //Blur迭代次数
    [Range(1,3)]
    public int blurIteration = 2;
    //下降分辨率倍率
    [Range(0, 3)]
    public int downSample = 1;
    //光源位置
    public Transform lightTransform;
    //产生体积光的范围
    [Range(0.0f, 5.0f)]
    public float lightRadius = 2.0f;
    //提取高亮结果Pow倍率,适当下降颜色过亮的状况
    [Range(1.0f, 4.0f)]
    public float lightPowFactor = 3.0f;

    private Camera targetCamera = null;

    void Awake()
    {
        targetCamera = GetComponent<Camera>();
    }

    void OnEnable()
    {
        targetCamera.depthTextureMode = DepthTextureMode.Depth;
    }

    void OnDistable()
    {
        targetCamera.depthTextureMode = DepthTextureMode.None;
    }

    void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        if (_Material && targetCamera)
        {
            int rtWidth = source.width >> downSample;
            int rtHeight = source.height >> downSample;
            //RT分辨率按照downSameple下降
            RenderTexture temp1 = RenderTexture.GetTemporary(rtWidth, rtHeight, 0, source.format);

            //计算光源位置从世界空间转化到视口空间
            Vector3 viewPortLightPos = lightTransform == null ? new Vector3(.5f, .5f, 0) : targetCamera.WorldToViewportPoint(lightTransform.position);
          
            //将shader变量改成PropertyId,以及将float放在Vector中一块儿传递给Material会更省一些,but,我懒
            _Material.SetVector("_ColorThreshold", colorThreshold);
            _Material.SetVector("_ViewPortLightPos", new Vector4(viewPortLightPos.x, viewPortLightPos.y, viewPortLightPos.z, 0));
            _Material.SetFloat("_LightRadius", lightRadius);
            _Material.SetFloat("_PowFactor", lightPowFactor);
            _Material.SetFloat("_DepthThreshold", depthThreshold);
            //根据阈值提取高亮部分,使用pass0进行高亮提取,比Bloom多一步计算光源距离剔除光源范围外的部分
            Graphics.Blit(source, temp1, _Material, 0);

            _Material.SetVector("_ViewPortLightPos", new Vector4(viewPortLightPos.x, viewPortLightPos.y, viewPortLightPos.z, 0));
            _Material.SetFloat("_LightRadius", lightRadius);
            //径向模糊的采样uv偏移值
            float samplerOffset = samplerScale / source.width;
            //径向模糊,两次一组,迭代进行
            for (int i = 0; i < blurIteration; i++)
            {
                RenderTexture temp2 = RenderTexture.GetTemporary(rtWidth, rtHeight, 0, source.format);
                float offset = samplerOffset * (i * 2 + 1);
                _Material.SetVector("_offsets", new Vector4(offset, offset, 0, 0));
                Graphics.Blit(temp1, temp2, _Material, 1);

                offset = samplerOffset * (i * 2 + 2);
                _Material.SetVector("_offsets", new Vector4(offset, offset, 0, 0));
                Graphics.Blit(temp2, temp1, _Material, 1);
                RenderTexture.ReleaseTemporary(temp2);
            }
           
            _Material.SetTexture("_BlurTex", temp1);
            _Material.SetVector("_LightColor", lightColor);
            _Material.SetFloat("_LightFactor", lightFactor);
            //最终混合,将体积光径向模糊图与原始图片混合,pass2
            Graphics.Blit(source, destination, _Material, 2);

            //释放申请的RT
            RenderTexture.ReleaseTemporary(temp1);
        }
        else
        {
            Graphics.Blit(source, destination);
        }
    }
}

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


but,上面的状况其实也有必定局限,若是咱们的光源并非在最远,近处光源仍然想表现这个效果,或者这个超级亮的对象自己不会写入深度图,加之开启深度自己也是有很大消耗的。因此,咱们能够换一种方式,本身去生成一张Mask图进行抠图操做。比较方便的一个方法是额外生成一个和当前相机同样的相机,使用RenderWithShader方式生成一张Mask图。

Replace Shader代码以下,只把Geometry的替换了,其余类型看我的喜爱咯:

//puppet_master
//2018.4.24
//后处理Mask生成ReplaceMentShader
Shader "GodRay/MaskGen" {

	CGINCLUDE
	#include "UnityCG.cginc"
	fixed4 frag_maskGen(v2f_img i) : SV_Target
	{
		return fixed4(0, 0, 0, 1.0f);
	}
	ENDCG

	SubShader
	{
		Tags 
		{
			"Queue" = "Geometry"
			"RenderType" = "Opaque"
		}
		
		Pass
		{
			ZTest Off
			Cull Off
			ZWrite Off
			Lighting Off
			Fog{ Mode Off }

			CGPROGRAM
			#pragma vertex vert_img
			#pragma fragment frag_maskGen
			ENDCG
		}
	}
}

GodRay Shader基本和Depth方式没什么区别,只是改成采样Mask贴图了:

//puppet_master
//2018.4.20
//后处理方式实现GodRay,使用Mask剔除不显示光的部分
Shader "GodRay/PostEffectMask" {

	Properties{
		_MainTex("Base (RGB)", 2D) = "white" {}
		_BlurTex("Blur", 2D) = "white"{}
		_MainTex("Mask", 2D) = "white"{}
	}

	CGINCLUDE
	#define RADIAL_SAMPLE_COUNT 6
	#include "UnityCG.cginc"
	
	//用于阈值提取高亮部分
	struct v2f_threshold
	{
		float4 pos : SV_POSITION;
		float2 uv : TEXCOORD0;
	};

	//用于blur
	struct v2f_blur
	{
		float4 pos : SV_POSITION;
		float2 uv  : TEXCOORD0;
		float2 blurOffset : TEXCOORD1;
	};

	//用于最终融合
	struct v2f_merge
	{
		float4 pos : SV_POSITION;
		float2 uv  : TEXCOORD0;
		float2 uv1 : TEXCOORD1;
	};

	sampler2D _MaskTexture;
	sampler2D _MainTex;
	float4 _MainTex_TexelSize;
	sampler2D _BlurTex;
	float4 _BlurTex_TexelSize;
	float4 _ViewPortLightPos;
	
	float4 _offsets;
	float4 _ColorThreshold;
	float4 _LightColor;
	float _LightFactor;
	float _PowFactor;
	float _LightRadius;

	//高亮部分提取shader
	v2f_threshold vert_threshold(appdata_img v)
	{
		v2f_threshold o;
		o.pos = UnityObjectToClipPos(v.vertex);
		o.uv = v.texcoord.xy;
		
		//dx中纹理从左上角为初始坐标,须要反向
#if UNITY_UV_STARTS_AT_TOP
		if (_MainTex_TexelSize.y < 0)
			o.uv.y = 1 - o.uv.y;
#endif	
		return o;
	}

	fixed4 frag_threshold(v2f_threshold i) : SV_Target
	{
		fixed4 color = tex2D(_MainTex, i.uv);
		float distFromLight = length(_ViewPortLightPos.xy - i.uv);
		float distanceControl = saturate(_LightRadius - distFromLight);
		//仅当color大于设置的阈值的时候才输出
		float4 thresholdColor = saturate(color - _ColorThreshold) * distanceControl;
		float luminanceColor = Luminance(thresholdColor.rgb);
		luminanceColor = pow(luminanceColor, _PowFactor);
		//采样深度贴图
		float depth = tex2D(_MaskTexture, i.uv);
		//转换回01区间
		depth = Linear01Depth (depth);
		//将深度小于阈值的部分直接变为0做为系数乘原来的结果,剃掉近处的内容
		luminanceColor *= depth;
		return fixed4(luminanceColor, luminanceColor, luminanceColor, 1);
	}

	//径向模糊 vert shader
	v2f_blur vert_blur(appdata_img v)
	{
		v2f_blur o;
		o.pos = UnityObjectToClipPos(v.vertex);
		o.uv = v.texcoord.xy;
		//径向模糊采样偏移值*沿光的方向权重
		o.blurOffset = _offsets * (_ViewPortLightPos.xy - o.uv);
		return o;
	}

	//径向模拟pixel shader
	fixed4 frag_blur(v2f_blur i) : SV_Target
	{
		half4 color = half4(0,0,0,0);
		for(int j = 0; j < RADIAL_SAMPLE_COUNT; j++)   
		{	
			color += tex2D(_MainTex, i.uv.xy);
			i.uv.xy += i.blurOffset; 	
		}
		
		return color / RADIAL_SAMPLE_COUNT;
	}

	//融合vertex shader
	v2f_merge vert_merge(appdata_img v)
	{
		v2f_merge o;
		//mvp矩阵变换
		o.pos = UnityObjectToClipPos(v.vertex);
		//uv坐标传递
		o.uv.xy = v.texcoord.xy;
		o.uv1.xy = o.uv.xy;
#if UNITY_UV_STARTS_AT_TOP
		if (_MainTex_TexelSize.y < 0)
			o.uv.y = 1 - o.uv.y;
#endif	
		return o;
	}

	fixed4 frag_merge(v2f_merge i) : SV_Target
	{
		fixed4 ori = tex2D(_MainTex, i.uv1);
		fixed4 blur = tex2D(_BlurTex, i.uv);
		//输出= 原始图像,叠加体积光贴图
		fixed4 lightColor =  _LightFactor * blur * _LightColor;
		return lightColor + ori;
	}

		ENDCG

	SubShader
	{
		//pass 0: 提取高亮部分
		Pass
		{
			ZTest Off
			Cull Off
			ZWrite Off
			Fog{ Mode Off }

			CGPROGRAM
			#pragma vertex vert_threshold
			#pragma fragment frag_threshold
			ENDCG
		}

		//pass 1: 径向模糊
		Pass
		{
			ZTest Off
			Cull Off
			ZWrite Off
			Fog{ Mode Off }

			CGPROGRAM
			#pragma vertex vert_blur
			#pragma fragment frag_blur
			ENDCG
		}

		//pass 2: 将体积光模糊图与原图融合
		Pass
		{

			ZTest Off
			Cull Off
			ZWrite Off
			Fog{ Mode Off }

			CGPROGRAM
			#pragma vertex vert_merge
			#pragma fragment frag_merge
			ENDCG
		}

	}
}

C#脚本部分变化较大:

/********************************************************************
 FileName: GodRayPostEffectMask.cs
 Description:
 Created: 2018/04/24
 history: 24:4:2018 0:11 by puppet_master
*********************************************************************/
using UnityEngine;
using UnityEngine.Rendering;

[ExecuteInEditMode]
public class GodRayPostEffectMask : PostEffectBase
{
    //高亮部分提取阈值
    public Color colorThreshold = Color.gray;
    //体积光颜色
    public Color lightColor = Color.white;
    //光强度
    [Range(0.0f, 20.0f)]
    public float lightFactor = 15.0f;
    //径向模糊uv采样偏移值
    [Range(0.0f, 10.0f)]
    public float samplerScale = 10.0f;
    //Blur迭代次数
    [Range(1,3)]
    public int blurIteration = 2;
    //下降分辨率倍率
    [Range(0, 3)]
    public int downSample = 1;
    //光源位置
    public Transform lightTransform;
    //产生体积光的范围
    [Range(0.0f, 5.0f)]
    public float lightRadius = 2.0f;
    //提取高亮结果Pow倍率,适当下降颜色过亮的状况
    [Range(1.0f, 4.0f)]
    public float lightPowFactor = 3.0f;

    public Shader replaceMentShader = null;

    private Camera targetCamera = null;
    private Camera maskCamera = null;

    public RenderTexture maskTexture = null;

    void Start()
    {
        targetCamera = GetComponent<Camera>();
        if (maskTexture == null)
            maskTexture = new RenderTexture(512, 512, 0);
        //建立一个相机渲染Mask
        var maskCamTransform = transform.Find("maskCameraGo");
        if (maskCamTransform == null)
        {
            GameObject go = new GameObject("maskCameraGo");
            go.transform.parent = transform;
            maskCamera = go.AddComponent<Camera>();
        }
        else
        {
            maskCamera = maskCamTransform.GetComponent<Camera>();
            if (maskCamera == null)
                maskCamera = maskCamTransform.gameObject.AddComponent<Camera>();
        }
        maskCamera.CopyFrom(targetCamera);
        maskCamera.targetTexture = maskTexture;
        maskCamera.depth = -999;
        //默认Mask图为白色
        maskCamera.clearFlags = CameraClearFlags.Color;
        maskCamera.backgroundColor = Color.white;
    }

    void OnEnable()
    {
        if (maskCamera != null)
        {
            maskCamera.targetTexture = maskTexture;
            maskCamera.enabled = true;
        }
    }

    void OnDisable()
    {
        if (maskCamera != null)
        {
            maskCamera.targetTexture = null;
            maskCamera.enabled = false;
        }
    }
  
    void OnDestroy()
    {
        if (maskCamera != null)
        {
            DestroyImmediate(maskCamera.gameObject);
            maskCamera = null;
        }
        if (maskTexture != null)
        {
            DestroyImmediate(maskTexture);
            maskTexture = null;
        }
    }

    void OnPreRender()
    {
        if (maskCamera != null)
        {
            //使用RenderWithShader绘制Mask图
            maskCamera.RenderWithShader(replaceMentShader, "RenderType");
        }
    }

    void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        if (_Material && targetCamera)
        {
            int rtWidth = source.width >> downSample;
            int rtHeight = source.height >> downSample;
            //RT分辨率按照downSameple下降
            RenderTexture temp1 = RenderTexture.GetTemporary(rtWidth, rtHeight, 0, source.format);

            //计算光源位置从世界空间转化到视口空间
            Vector3 viewPortLightPos = lightTransform == null ? new Vector3(.5f, .5f, 0) : targetCamera.WorldToViewportPoint(lightTransform.position);
          
            //将shader变量改成PropertyId,以及将float放在Vector中一块儿传递给Material会更省一些,but,我懒
            _Material.SetVector("_ColorThreshold", colorThreshold);
            _Material.SetVector("_ViewPortLightPos", new Vector4(viewPortLightPos.x, viewPortLightPos.y, viewPortLightPos.z, 0));
            _Material.SetFloat("_LightRadius", lightRadius);
            _Material.SetFloat("_PowFactor", lightPowFactor);
            _Material.SetTexture("_MaskTexture", maskTexture);
            //根据阈值提取高亮部分,使用pass0进行高亮提取,比Bloom多一步计算光源距离剔除光源范围外的部分
            Graphics.Blit(source, temp1, _Material, 0);

            _Material.SetVector("_ViewPortLightPos", new Vector4(viewPortLightPos.x, viewPortLightPos.y, viewPortLightPos.z, 0));
            _Material.SetFloat("_LightRadius", lightRadius);
            //径向模糊的采样uv偏移值
            float samplerOffset = samplerScale / source.width;
            //径向模糊,两次一组,迭代进行
            for (int i = 0; i < blurIteration; i++)
            {
                RenderTexture temp2 = RenderTexture.GetTemporary(rtWidth, rtHeight, 0, source.format);
                float offset = samplerOffset * (i * 2 + 1);
                _Material.SetVector("_offsets", new Vector4(offset, offset, 0, 0));
                Graphics.Blit(temp1, temp2, _Material, 1);

                offset = samplerOffset * (i * 2 + 2);
                _Material.SetVector("_offsets", new Vector4(offset, offset, 0, 0));
                Graphics.Blit(temp2, temp1, _Material, 1);
                RenderTexture.ReleaseTemporary(temp2);
            }
           
            _Material.SetTexture("_BlurTex", temp1);
            _Material.SetVector("_LightColor", lightColor);
            _Material.SetFloat("_LightFactor", lightFactor);
            //最终混合,将体积光径向模糊图与原始图片混合,pass2
            Graphics.Blit(source, destination, _Material, 2);

            //释放申请的RT
            RenderTexture.ReleaseTemporary(temp1);
        }
        else
        {
            Graphics.Blit(source, destination);
        }
    }
}

效果与以前一致,左侧为最终效果,右侧为Mask图:


用后处理方式实现体积光效果,还有一个比较硬性的要求就是屏幕内至少应该能看到所谓的光源,也就是高亮点,不然提取高亮失败的话,再怎么径向模糊都生成不了光线效果。不过既然要这样用了,没有光的地方,强行加个高光亮点也不是不能够哈。


RayMarching光线追踪

ShaderToy上大部分都是基于RayMarching的,每次去看都给这些大神跪了,不由惊叹一声,“我去,还有这种操做???”。Ray-Marching简单地来讲就是用数学来绘图,纯数学的方式去计算物体的颜色,比光栅化渲染更容易模拟一些云,雾等特殊效果。关于RayMarching我不作太多介绍了,下文中的RayMarching与正常的SDF等不太同样,用Ray-Marching实现体积光仍是挺好玩的,如今部分国外大型PC游戏已经采用这种方式进行体积光的渲染了,好比《Inside》,《KillZone》。关于RayMarching实现体积光效果,能够参考《GPU Pro 5-Volumetric Light Effects in Killzone: Shadow Fall》,里面也引用了一些连接,包括原论文,下文中公式以及部分原理图也来自《KillZone》的技术分享,另外知乎上的大佬也写过一篇很好的关于体积光的文章。原理以下图所示:


首先,体积光不是一个相似正常物体只有表面有颜色,而是一个介质(假设是均匀介质),光线在这个介质中传播时,通过的每一个路径点上都会对最终进入咱们眼睛的光强有影响,因此咱们计算时,从视线点开始,每次沿着视线方向推动一点,采样每一个采样点的亮度,全部通过的采样点上的亮度和就是最终的亮度值,如上图(c)所示。

体积光的实现,Git上有一个很好的开源项目,Star也不少,下文中的实现有参考该项目,不过为了方便(就是懒),我决定作一个比较简单的PointLight的体积光效果,Directironal Light的经过相机视锥体边角反推全屏幕像素点对应的世界位置再进行Ray-Marching,实现也差很少,最麻烦的是SpotLight,形状不是很方便完美地用数学贴合光照值,不过也能够用一个贴近的载体mesh去计算,等过一阵子闲下来再来实现一发。

首先,咱们要知道的是点光源在某一点上的光强度值,由于点光源自己是有一个衰减的,虽然我很想直接除以距离的平方做为衰减值,可是无心中发现乐乐大佬分享过一个关于Unity中点光源衰减计算的帖子,再去看了一下Unity cginc里面那些计算,发现事情没有这么简单:Unity的点光源衰减计算,其实是采样了一张衰减贴图,首先计算当前位置距离点光源的距离平方,而后除以光源范围的平方,结果去采样衰减图,附上Untiy中计算:

float3 tolight = wpos - _LightPos.xyz;
half3 lightDir = -normalize (tolight);
float att = dot(tolight, tolight) * _LightPos.w;
float atten = tex2D (_LightTextureB0, att.rr).UNITY_ATTEN_CHANNEL;
atten *= UnityDeferredComputeShadow (tolight, fadeDist, uv);
		
#if defined (POINT_COOKIE)
atten *= texCUBEbias(_LightTexture0, float4(mul(unity_WorldToLight, half4(wpos,1)).xyz, -8)).w;
#endif //POINT_COOKIE

因此,根据上面的结果,最基本的Ray-Marching实现以下:

//点光源体积光RayMarching
float4 RayMarching(float3 rayOri, float3 rayDir, float rayLength)
{
	//ori:相机位置
	//raydir:从相机到当前像素点对应世界坐标值的方向
	//rayLength:长度
	
	//步进值
	float delta = rayLength / RAYMARCHING_STEP_COUNT;
	float3 step = rayDir * delta;
	float3 curPos = rayOri + step;
	
	float totalAtten = 0;
	for(int t = 0; t < RAYMARCHING_STEP_COUNT; t++)
	{
		float3 tolight = (curPos - _VolumeLightPos.xyz);
		float att = dot(tolight, tolight) * _MieScatteringFactor.w;
		float atten = tex2D(_LightTextureB0, att.rr).UNITY_ATTEN_CHANNEL;
		//每次步进增长该点光强
		totalAtten += atten;
		curPos += step;
	}
	
	float4 color = float4(totalAtten, totalAtten, totalAtten, totalAtten);
	return color * _TintColor;
}

这样,咱们能够获得一个边缘虚化的球:


可是,这并非咱们想要的体积光,这就是一个体积...再来分析一下体积光的造成,使用Ray-Marching方式实现体积光,相比于其余三种方式,稍微“基于物理”了一些,因此也就有必要说一下光的物理现象。当光透过一些介质时,一般是灰尘之类的,会形成光的反射,这个反射是四面八方的,若是都反射到咱们的眼睛里,那么大概就是上面的这个效果,but,并非,因此光线是朝向四面八方反射的,以灰尘为球心的球体每一个方向均可能反射,而且反射的几率不一样,而且须要知足能量守恒定律,这个现象简单称之为光的散射。另外一个现象就是光在传播过程当中会被吸取一部分,剩下的才会传递到咱们眼中。数学很差的我,先无耻地抄俩关于光散射和吸取的公式:

第一个名字很是萌Mie-Scattering HG(咩-散射公式,喵喵喵?)


简单来讲,g所表示的就是光的散射系数,g越大,光束越集中,散射越少,g越小,散射越强,θ为光线方向和视线方向的夹角。

第二个是Beer-Lambert法则,表现为入射光强和透光强度的比:OutLight = InLight * exp(- c * d), c为物质密度,d为距离。咱们可使用3D噪声纹理+uv流动来动态采样每一个点的密度值,这样就能够模拟灰尘在灯光下流动的效果。此处我假设密度值相同。

在Shader里面增长Mie-Scattering散射和Beer-Lambert衰减:

float MieScatteringFunc(float3 lightDir, float3 rayDir)
{
	//MieScattering公式
	// (1 - g ^2) / (4 * pi * (1 + g ^2 - 2 * g * cosθ) ^ 1.5 )
	//_MieScatteringFactor.x = (1 - g ^ 2) / 4 * pai
	//_MieScatteringFactor.y =  1 + g ^ 2
	//_MieScatteringFactor.z =  2 * g
	float lightCos = dot(lightDir, -rayDir);
	return _MieScatteringFactor.x / pow((_MieScatteringFactor.y - _MieScatteringFactor.z * lightCos), 1.5);
}

//Beer-Lambert法则
float ExtingctionFunc(float stepSize, inout float extinction)
{
	float density = 1.0; //密度,暂且认为为1吧,能够采样3DNoise贴图
	float scattering = _ScatterFactor * stepSize * density;
	extinction += _ExtictionFactor * stepSize * density;
	return scattering * exp(-extinction);
}

float4 RayMarching(float3 rayOri, float3 rayDir, float rayLength)
{	
	float delta = rayLength / RAYMARCHING_STEP_COUNT;
	float3 step = rayDir * delta;
	float3 curPos = rayOri + step;
	
	float totalAtten = 0;
	float extinction = 0;
	for(int t = 0; t < RAYMARCHING_STEP_COUNT; t++)
	{
		float3 tolight = (curPos - _VolumeLightPos.xyz);
		float atten = 1.0;
		float att = dot(tolight, tolight) * _MieScatteringFactor.w;
		atten *= tex2D(_LightTextureB0, att.rr).UNITY_ATTEN_CHANNEL;
		atten *= MieScatteringFunc(normalize(-tolight), rayDir);
		atten *= ExtingctionFunc(delta, extinction);
		
		totalAtten += atten;
		curPos += step;
	}
	
	float4 color = float4(totalAtten, totalAtten, totalAtten, totalAtten);
	return color * _TintColor;
}

增长了散射以后的光,便有了一点光的感受,调整Mie-Scattering的g值就能够改变散射值:


下面一个问题是这个对象自己的渲染,咱们是把这个模型做为一个载体进行渲染的,为了更好的表现效果,不至于被其余东西遮挡住,因此开了ZTest Always,Transparent的Queue是最好。可是咱们关闭了ZTest,换句话说,这个东东就不会被遮住了,这也是不现实的,因此,咱们要本身进行一个深度测试,相似原理图(b)所示,在进行Ray-Marching的过程当中,先采样深度值,计算ray的距离,在当前像素对应点的世界坐标距离初始点的距离和当前深度值计算出来的视空间距离比较,用一个更近的距离做为RayMarching的最终距离,这样就能够处理遮挡相关的问题。

另外一方面,咱们须要处理体积光对于对象的阴影,没有体积光的阴影,体积光就变成了雾。为了让这个对象有照射到其余对象上阴影的效果,咱们再采样一下ShadowMap。由于直接受该对象自身上的点光阴影,因此这里我直接把这个shader改成AddPass,不过这里有一个很严重的问题,Unity对于透明的物体是不接受(Dither能够投射,可是不接受!)阴影的,因此没有办法,我暂且把这个对象放在AlphaTest队列里,可是这样还会有对象被其余后渲染的内容遮挡或者天空盒遮挡的问题,问题老是不少,不过解决办法老是有的,下文再说。

Shader代码以下:

//puppet_master
//2018.4.28
//体积光:RayMarching方式实现点光源体积光效果
Shader "GodRay/RayMarchingPointLight" 
{

Properties 
{
	_TintColor ("Tint Color", Color) = (0.5,0.5,0.5,0.5)
	_ExtictionFactor("ExtictionFactor", Range(0, 0.1)) = 0.01
	_ScatterFactor("ScatterFactor", Range(0, 1)) = 1
}

Category {
	//受自身Add点光阴影
	Name "FORWARD_DELTA"
	Tags { "LightMode" = "ForwardAdd"  "RenderType"="Opaque" "Queue" = "AlphaTest"}
	Blend SrcAlpha One
	Cull Off 
	Lighting Off 
	//不写深度,永远经过ZTest,本身作检测
	ZWrite Off 
	ZTest Always
	Fog { Color (0,0,0,0) }
	
	SubShader {
		Pass {
		
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#pragma fragmentoption ARB_precision_hint_fastest
			
			#include "UnityCG.cginc"
			#include "UnityDeferredLibrary.cginc"
			//RayMarching步进次数
			#define RAYMARCHING_STEP_COUNT 64
			
			#pragma shader_feature SHADOWS_CUBE
			#pragma shader_feature POINT

			fixed4 _TintColor;
			sampler2D _DitherMap;
			float4x4 _LightMatrix;
			float4 _VolumeLightPos;
			float4 _MieScatteringFactor;
			float _ExtictionFactor;
			float _ScatterFactor;

			struct v2f {
				float4 pos : POSITION;
				float3 worldNormal : TEXCOORD0;
				float3 worldPos : TEXCOORD1;
				float4 screenUV : TEXCOORD2;
			};
			
			float MieScatteringFunc(float3 lightDir, float3 rayDir)
			{
				//MieScattering公式
				// (1 - g ^2) / (4 * pi * (1 + g ^2 - 2 * g * cosθ) ^ 1.5 )
				//_MieScatteringFactor.x = (1 - g ^ 2) / 4 * pai
				//_MieScatteringFactor.y =  1 + g ^ 2
				//_MieScatteringFactor.z =  2 * g
				float lightCos = dot(lightDir, -rayDir);
				return _MieScatteringFactor.x / pow((_MieScatteringFactor.y - _MieScatteringFactor.z * lightCos), 1.5);
			}
			
			//Beer-Lambert法则
			float ExtingctionFunc(float stepSize, inout float extinction)
			{
				float density = 1.0; //密度,暂且认为为1吧,能够采样3DNoise贴图获得
				float scattering = _ScatterFactor * stepSize * density;
				extinction += _ExtictionFactor * stepSize * density;
				return scattering * exp(-extinction);
			}
			
			float4 RayMarching(float3 rayOri, float3 rayDir, float rayLength)
			{	
				float delta = rayLength / RAYMARCHING_STEP_COUNT;
				float3 step = rayDir * delta;
				float3 curPos = rayOri + step;
				
				float totalAtten = 0;
				float extinction = 0;
				for(int t = 0; t < RAYMARCHING_STEP_COUNT; t++)
				{
					
					float3 tolight = (curPos - _VolumeLightPos.xyz);
					//光源衰减
					float atten = 2.0;
					float att = dot(tolight, tolight) * _MieScatteringFactor.w;
					atten *= tex2D(_LightTextureB0, att.rr).UNITY_ATTEN_CHANNEL;
					//Mie散射
					atten *= MieScatteringFunc(normalize(-tolight), rayDir);
					//传播过程当中吸取
					atten *= ExtingctionFunc(delta, extinction);
					#if defined (SHADOWS_CUBE)
					//阴影
					atten *= UnityDeferredComputeShadow(tolight, 0, float2(0, 0));
					#endif
					totalAtten += atten;
					curPos += step;
				}
				
				float4 color = float4(totalAtten, totalAtten, totalAtten, totalAtten);
				return color * _TintColor;
			}

			v2f vert (appdata_base v)
			{
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;  
				o.screenUV = ComputeScreenPos(o.pos);
				return o;
			}

			fixed4 frag (v2f i) : COLOR
			{
				float3 worldPos = i.worldPos;
				float3 worldCamPos = _WorldSpaceCameraPos.xyz;
				float rayDis = length(worldCamPos - worldPos);
				
				float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.screenUV.xy / i.screenUV.w);
				float linearEyeDepth = LinearEyeDepth(depth);
				rayDis = min(rayDis, linearEyeDepth);
				
				return RayMarching(worldCamPos, normalize(worldPos - worldCamPos), rayDis);
			}
			ENDCG 
		}
	} 	
}
}

C#代码以下:

/********************************************************************
 FileName: VolumeRayMarchingLight.cs
 Description:
 Created: 2018/04/28
 history: 28:4:2018 20:33 by puppet_master
*********************************************************************/
using UnityEngine;

[ExecuteInEditMode]
public class VolumeRayMarchingLight : MonoBehaviour {

    private Material lightMaterial = null;

    private Light lightComponent = null;

    //Mie-Scattering g 参数
    [Range(0.0f, 0.99f)]
    public float MieScatteringG = 0.5f;

    void OnEnable()
    {
        if (Camera.main != null)
            Camera.main.depthTextureMode = DepthTextureMode.Depth;
        InitVolumeLight();
    }

    void OnDisable()
    {
        if (Camera.main != null)
            Camera.main.depthTextureMode = DepthTextureMode.None;
    }

    private void InitVolumeLight()
    {
        var render = GetComponent<Renderer>();
        //sharedMaterial方便一点...
        lightMaterial = render.sharedMaterial;
        lightComponent = GetComponent<Light>();
        if (lightComponent == null)
        {
            lightComponent = gameObject.AddComponent<Light>();
        }
    }

    void Update ()
    {
        if (lightMaterial == null || lightComponent == null)
            return;
        //世界->光源矩阵
        Matrix4x4 lightMatrix = Matrix4x4.TRS(transform.position, transform.rotation, Vector3.one).inverse;
        transform.localScale = new Vector3(lightComponent.range * 2.0f, lightComponent.range * 2.0f, lightComponent.range * 2.0f);

        lightMaterial.EnableKeyword("POINT");
        if (lightComponent.shadows == LightShadows.None)
        {
            lightMaterial.DisableKeyword("SHADOWS_CUBE");
        }
        else
        {
            lightMaterial.EnableKeyword("SHADOWS_CUBE");
        }

        float g2 = MieScatteringG * MieScatteringG;
        float lightRange = lightComponent.range;
        lightMaterial.SetMatrix("_LightMatrix", lightMatrix);
        lightMaterial.SetVector("_VolumeLightPos", transform.position);
        lightMaterial.SetVector("_MieScatteringFactor", new Vector4((1 - g2) * 0.25f / Mathf.PI, 1 + g2, 2 * MieScatteringG, 1.0f / (lightRange * lightRange)));
	}
}
效果以下:


再来一张:


经过Ray-Marching实现的体积光,进行了N(上图为64)次射线步进的操做,这个若是在性能差一点的机器上,基本就挂掉了。因此,下面来看看Ray-Marching的优化。

第一点,也是最重要的优化方式,下降RayMarching的步进次数,可是这个数若是特别低的话,会出现比较明显的穿帮现象,以下,左侧是4次步进,右侧是64次步进的效果,光强度较低,阴影部分比较明显:


有一个很好的方式来解决这个问题,就是Dither,简单来讲就是随机采样,更直白点说就是运用噪声。俗话说得好,有什么解决不了的问题?那就加个噪声试试吧!

所谓Dither,就是一个噪声格子,咱们能够定义一个4*4的贴图,这个就直接从《KillZone》的分享里面摘抄一下啦:


这样,起始点的时候咱们给每一个点增长一个偏移值,让采样更加随机:

//dither
float2 offsetUV = (fmod(floor(ditherUV), 4.0));
float ditherValue = tex2D(_DitherMap, offsetUV / 4.0).a;				
float delta = rayLength / RAYMARCHING_STEP_COUNT;
float3 step = rayDir * delta;
float3 curPos = rayOri + step * ditherValue;


左侧为没有Dither的Ray-Marching,右侧为有Dither的Ray-Marching。增长了Dither以后,咱们发现原来断层的效果又变得连续了,Magic!!!感受想出这个办法的人简直是个天才,用这个方法,性能直接好几十倍的提高。


不过仔细看上图,虽然用了Dither让效果变得连续了,可是图像中出现了一堆相似蜂巢的小格子,咱们须要把这个处理掉,Dither须要配合Blur使用,这个东西须要套餐,单独用很差使。 既然要Blur,咱们天然是须要将体积光自己抽取出来单独进行Blur,相似径向模糊中提取高亮部分,不然整个画面就模糊了。那么,最好的方法,咱们就直接把体积光渲染到一张RT上,而后进行Blur最后在叠加回原图。

说道单独渲染到RT上, 此处又能够进行第二个优化,Ray-Marching的大部分操做是在Pixel阶段进行的,因此咱们若是能下降Pixel运行的次数,就能够大大下降消耗了。因此,这里渲染体积光Mesh的RT咱们就能够下降分辨率,下降为一半甚至四分之一。

要把对象渲染到RT上,这时候咱们就想到了最方便的东东:CommandBuffer,简直是一些奇怪效果的救星,自从有了CommandBuffer,单首创建摄像机的事情就少了好多。咱们要找一个合适的时机来渲染这个对象,一个比较麻烦的问题在于咱们须要采样ShadowMap,上文咱们也提到了,以前让对象在Geomerty队列或者AlphaTest队列才能采样ShadowMap,放在Transparent队列的话,ShadowMap就变成了一个默认的贴图了,直接用CommandBuffer渲染,放到Before Opaque也不行(也多是我姿式不对,但愿各位遇到一样问题的大佬赐教)。那么咱们强行把渲染的时机插入到ShadowMap结束的阶段,此时激活的RT仍然是ShadowMap,而后咱们把当前渲染的RT手动设置到ShadowMap中,而后再将CommandBuffer中设置当前相机的RT为渲染体积光的RT就能够实现了。可是这样作又出了一个新问题,此时相机的MVP矩阵并非咱们正常渲染的MVP,而是渲染ShadowMap时相机的矩阵,咱们直接用Unity内置的矩阵进行顶点变换,结果是错误的,因此,须要手动设置一个正常的相机MVP矩阵传给shader。(上面的过程简直就是为达目的,无所不用其极....)

体积光shader:

//puppet_master
//2018.4.28
//体积光:RayMarching方式实现点光源体积光效果
Shader "GodRay/RayMarchingPointLight" 
{

Properties 
{
	_TintColor ("Tint Color", Color) = (1.0,1.0,1.0,1.0)
	_ExtictionFactor("ExtictionFactor", Range(0, 0.1)) = 0.01
	_ScatterFactor("ScatterFactor", Range(0, 1)) = 1
}

Category {
	//受自身Add点光阴影
	//Name "FORWARD_DELTA"
	Tags {   "RenderType"="Opaque" "Queue" = "AlphaTest"}
	Blend SrcAlpha One
	Cull Off 
	Lighting Off 
	//不写深度,永远经过ZTest,本身作检测
	ZWrite Off 
	ZTest Always
	Fog { Color (0,0,0,0) }
	
	SubShader {
		Pass {
		
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#pragma fragmentoption ARB_precision_hint_fastest
			
			#include "UnityCG.cginc"
			#include "UnityDeferredLibrary.cginc"
			//RayMarching步进次数
			#define RAYMARCHING_STEP_COUNT 4
			
			#pragma shader_feature SHADOWS_CUBE
			#pragma shader_feature POINT

			fixed4 _TintColor;
			sampler2D _DitherMap;
			float4x4 _LightMatrix;
			float4x4 _CustomMVP;
			float4 _VolumeLightPos;
			float4 _MieScatteringFactor;
			float _ExtictionFactor;
			float _ScatterFactor;

			struct v2f {
				float4 pos : POSITION;
				float3 worldNormal : TEXCOORD0;
				float3 worldPos : TEXCOORD1;
				float4 screenUV : TEXCOORD2;
			};
			
			float MieScatteringFunc(float3 lightDir, float3 rayDir)
			{
				//MieScattering公式
				// (1 - g ^2) / (4 * pi * (1 + g ^2 - 2 * g * cosθ) ^ 1.5 )
				//_MieScatteringFactor.x = (1 - g ^ 2) / 4 * pai
				//_MieScatteringFactor.y =  1 + g ^ 2
				//_MieScatteringFactor.z =  2 * g
				float lightCos = dot(lightDir, -rayDir);
				return _MieScatteringFactor.x / pow((_MieScatteringFactor.y - _MieScatteringFactor.z * lightCos), 1.5);
			}
			
			//Beer-Lambert法则
			float ExtingctionFunc(float stepSize, inout float extinction)
			{
				float density = 1.0; //密度,暂且认为为1吧,能够采样3DNoise贴图获得
				float scattering = _ScatterFactor * stepSize * density;
				extinction += _ExtictionFactor * stepSize * density;
				return scattering * exp(-extinction);
			}
			
			float4 RayMarching(float3 rayOri, float3 rayDir, float rayLength, float2 ditherUV)
			{	
				//dither
				float2 offsetUV = (fmod(floor(ditherUV), 4.0));
				float ditherValue = tex2D(_DitherMap, offsetUV / 4.0).a;
				
				float delta = rayLength / RAYMARCHING_STEP_COUNT;
				float3 step = rayDir * delta;
				float3 curPos = rayOri + step * ditherValue;
				
				float totalAtten = 0;
				float extinction = 0;
				for(int t = 0; t < RAYMARCHING_STEP_COUNT; t++)
				{
					
					float3 tolight = (curPos - _VolumeLightPos.xyz);
					//光源衰减
					float atten = 2.0;
					float att = dot(tolight, tolight) * _MieScatteringFactor.w;
					atten *= tex2D(_LightTextureB0, att.rr).UNITY_ATTEN_CHANNEL;
					//Mie散射
					atten *= MieScatteringFunc(normalize(-tolight), rayDir);
					//传播过程当中吸取
					atten *= ExtingctionFunc(delta, extinction);
					#if defined (SHADOWS_CUBE)
					//阴影
					atten *= UnityDeferredComputeShadow(tolight, 0, float2(0, 0));
					#endif
					totalAtten += atten;
					curPos += step;
				}
				//totalAtten = 0.1;
				float4 color = float4(totalAtten, totalAtten, totalAtten, totalAtten);
				return color * _TintColor;
			}

			v2f vert (appdata_base v)
			{
				v2f o;
				//o.pos = UnityObjectToClipPos(v.vertex);
				o.pos = mul(_CustomMVP, v.vertex);
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;  
				o.screenUV = ComputeScreenPos(o.pos);
				return o;
			}

			fixed4 frag (v2f i) : COLOR
			{
				float3 worldPos = i.worldPos;
				float3 worldCamPos = _WorldSpaceCameraPos.xyz;
				float rayDis = length(worldCamPos - worldPos);
				
				float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.screenUV.xy / i.screenUV.w);
				float linearEyeDepth = LinearEyeDepth(depth);
				rayDis = min(rayDis, linearEyeDepth);
				
				return RayMarching(worldCamPos, normalize(worldPos - worldCamPos), rayDis, i.pos.xy);
			}
			ENDCG 
		}
	} 	
}
}

体积光脚本:

/********************************************************************
 FileName: VolumeRayMarchingLight.cs
 Description:
 Created: 2018/04/28
 history: 28:4:2018 20:33 by puppet_master
*********************************************************************/
using UnityEngine;
using UnityEngine.Rendering;

[ExecuteInEditMode]
public class VolumeRayMarchingLight : MonoBehaviour {

    private Material lightMaterial = null;

    private Light lightComponent = null;

    private Texture2D ditherMap = null;

    private CommandBuffer commandBuffer = null;

    public static RenderTexture volumeLightRT = null;

    private Renderer lightRenderer = null;

    //Mie-Scattering g 参数
    [Range(0.0f, 0.99f)]
    public float MieScatteringG = 0.0f;

    void OnEnable()
    {
        if (Camera.main != null)
            Camera.main.depthTextureMode = DepthTextureMode.Depth;

        Init();
        lightComponent.AddCommandBuffer(LightEvent.AfterShadowMap, commandBuffer);
    }

    void OnDisable()
    {
        lightComponent.RemoveCommandBuffer(LightEvent.AfterShadowMap, commandBuffer);
        if (Camera.main != null)
            Camera.main.depthTextureMode = DepthTextureMode.None;
    }

    private void Init()
    {
        InitVolumeLight();
        InitCommandBuffer();
        InitPostEffectComponent();
    }

    private void InitVolumeLight()
    {
        lightRenderer = GetComponent<Renderer>();
        lightMaterial = lightRenderer.sharedMaterial;
        lightComponent = GetComponent<Light>();
        if (lightComponent == null)
            lightComponent = gameObject.AddComponent<Light>();
        lightComponent.shadows = LightShadows.Hard;
        lightRenderer.enabled = false;
        if (ditherMap == null)
            ditherMap = GenerateDitherMap();
        if (volumeLightRT == null)
            volumeLightRT = new RenderTexture(512, 512, 16);
    }

    private void InitCommandBuffer()
    {
        if (commandBuffer == null)
            commandBuffer = new CommandBuffer();
        commandBuffer.Clear();
        commandBuffer.name = "RayMarchingVolumePointLight";
        commandBuffer.SetGlobalTexture("_ShadowMapTexture", BuiltinRenderTextureType.CurrentActive);
        commandBuffer.SetRenderTarget(volumeLightRT);
        commandBuffer.ClearRenderTarget(true, true, Color.black);
        commandBuffer.DrawRenderer(lightRenderer, lightMaterial);
    }

    private void InitPostEffectComponent()
    {
        if (Camera.main == null)
            return;
       var postEffect = Camera.main.gameObject.GetComponent<VolumeRayMarchingPostEffect>();
       if (postEffect == null)
           postEffect = Camera.main.gameObject.AddComponent<VolumeRayMarchingPostEffect>();
        postEffect.RegistVolumeLightRT(volumeLightRT);
        postEffect.shader = Shader.Find("GodRay/VolumeLightRayMarchingPostEffect");
    }

    void Update ()
    {
        if (lightMaterial == null || lightComponent == null)
            return;
        //世界->光源矩阵
        Matrix4x4 lightMatrix = Matrix4x4.TRS(transform.position, transform.rotation, Vector3.one).inverse;
        transform.localScale = new Vector3(lightComponent.range * 2.0f, lightComponent.range * 2.0f, lightComponent.range * 2.0f);

        lightMaterial.EnableKeyword("POINT");
        if (lightComponent.shadows == LightShadows.None)
        {
            lightMaterial.DisableKeyword("SHADOWS_CUBE");
        }
        else
        {
            lightMaterial.EnableKeyword("SHADOWS_CUBE");
        }

        float g2 = MieScatteringG * MieScatteringG;
        float lightRange = lightComponent.range;
        lightMaterial.SetMatrix("_LightMatrix", lightMatrix);
        lightMaterial.SetVector("_VolumeLightPos", transform.position);
        lightMaterial.SetVector("_MieScatteringFactor", new Vector4((1 - g2) * 0.25f / Mathf.PI, 1 + g2, 2 * MieScatteringG, 1.0f / (lightRange * lightRange)));
        lightMaterial.SetTexture("_DitherMap", ditherMap);
        //本身计算MVP矩阵传给shader,用Camera.main可能致使编辑器Scene窗口显示有问题
        Matrix4x4 world = transform.localToWorldMatrix;
        Matrix4x4 proj = GL.GetGPUProjectionMatrix(Camera.main.projectionMatrix, true);
        Matrix4x4 mat =  proj * Camera.main.worldToCameraMatrix * world;
        lightMaterial.SetMatrix("_CustomMVP", mat);
    }

    private Texture2D GenerateDitherMap()
    {
        int texSize = 4;
        var ditherMap = new Texture2D(texSize, texSize, TextureFormat.Alpha8, false, true);
        ditherMap.filterMode = FilterMode.Point;
        Color32[] colors = new Color32[texSize * texSize];

        colors[0] = GetDitherColor(0.0f);
        colors[1] = GetDitherColor(8.0f);
        colors[2] = GetDitherColor(2.0f);
        colors[3] = GetDitherColor(10.0f);

        colors[4] = GetDitherColor(12.0f);
        colors[5] = GetDitherColor(4.0f);
        colors[6] = GetDitherColor(14.0f);
        colors[7] = GetDitherColor(6.0f);

        colors[8] = GetDitherColor(3.0f);
        colors[9] = GetDitherColor(11.0f);
        colors[10] = GetDitherColor(1.0f);
        colors[11] = GetDitherColor(9.0f);

        colors[12] = GetDitherColor(15.0f);
        colors[13] = GetDitherColor(7.0f);
        colors[14] = GetDitherColor(13.0f);
        colors[15] = GetDitherColor(5.0f);

        ditherMap.SetPixels32(colors);
        ditherMap.Apply();
        return ditherMap;
    }

    private Color32 GetDitherColor(float value)
    {
        byte byteValue = (byte)(value / 16.0f * 255);
        return new Color32(byteValue, byteValue, byteValue, byteValue);
    }
}

后处理脚本,PS 后处理基类在上面提到过啦!!!!!!:

/********************************************************************
 FileName: VolumeRayMarchingPostEffect.cs
 Description:体积光后处理脚本,用于模糊+叠加
 Created: 2018/04/29
 history: 29:4:2018 1:47 by puppet_master
*********************************************************************/
using UnityEngine;

public class VolumeRayMarchingPostEffect : PostEffectBase
{
    //分辨率
    public int downSample = 1;
    //采样率
    public int samplerScale = 1;

    private RenderTexture volumeLightRT = null;

    public void RegistVolumeLightRT(RenderTexture rt)
    {
        volumeLightRT = rt;
    }

    void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        if (_Material && volumeLightRT)
        {
            Graphics.Blit(volumeLightRT, destination);
            //申请RT,而且分辨率按照downSameple下降
            RenderTexture tempRT = RenderTexture.GetTemporary(volumeLightRT.width >> downSample, volumeLightRT.height >> downSample, 0, source.format);

            //高斯模糊,两次模糊,横向纵向,使用pass1进行高斯模糊
            _Material.SetVector("_offsets", new Vector4(0, samplerScale, 0, 0));
            Graphics.Blit(volumeLightRT, tempRT, _Material, 0);
            _Material.SetVector("_offsets", new Vector4(samplerScale, 0, 0, 0));
            Graphics.Blit(tempRT, volumeLightRT, _Material, 0);

            _Material.SetTexture("_VolumeLightTex", volumeLightRT);
            Graphics.Blit(source, destination, _Material, 1);

            //释放申请的RT
            RenderTexture.ReleaseTemporary(tempRT);
        }
        else
        {
            Graphics.Blit(source, destination);
        }
        
    }
}

模糊以及最终叠加shader:

//puppet_master
//2018.4.29
//体积光:RayMarching方式实现点光源体积光效果配合后处理shader,实现高斯模糊+叠加效果
Shader "GodRay/VolumeLightRayMarchingPostEffect" {

	Properties{
		_MainTex("Base (RGB)", 2D) = "white" {}
		_VolumeLightTex("Volume", 2D) = "white"{}
	}

	CGINCLUDE
	#include "UnityCG.cginc"

	//用于blur
	struct v2f_blur
	{
		float4 pos : SV_POSITION;
		float2 uv  : TEXCOORD0;
		float4 uv01 : TEXCOORD1;
		float4 uv23 : TEXCOORD2;
		float4 uv45 : TEXCOORD3;
	};

	//用于叠加
	struct v2f_add
	{
		float4 pos : SV_POSITION;
		float2 uv  : TEXCOORD0;
		float2 uv1 : TEXCOORD1;
	};

	sampler2D _MainTex;
	float4 _MainTex_TexelSize;
	sampler2D _VolumeLightTex;
	float4 _VolumeLightTex_TexelSize;
	float4 _offsets;
	float4 _colorThreshold;

	//高斯模糊 vert shader
	v2f_blur vert_blur(appdata_img v)
	{
		v2f_blur o;
		_offsets *= _MainTex_TexelSize.xyxy;
		o.pos = UnityObjectToClipPos(v.vertex);
		o.uv = v.texcoord.xy;

		o.uv01 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1);
		o.uv23 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1) * 2.0;
		o.uv45 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1) * 3.0;

		return o;
	}

	//高斯模糊 pixel shader
	fixed4 frag_blur(v2f_blur i) : SV_Target
	{
		fixed4 color = fixed4(0,0,0,0);
		color += 0.40 * tex2D(_MainTex, i.uv);
		color += 0.15 * tex2D(_MainTex, i.uv01.xy);
		color += 0.15 * tex2D(_MainTex, i.uv01.zw);
		color += 0.10 * tex2D(_MainTex, i.uv23.xy);
		color += 0.10 * tex2D(_MainTex, i.uv23.zw);
		color += 0.05 * tex2D(_MainTex, i.uv45.xy);
		color += 0.05 * tex2D(_MainTex, i.uv45.zw);
		return color;
	}

	v2f_add vert_add(appdata_img v)
	{
		v2f_add o;
		//mvp矩阵变换
		o.pos = UnityObjectToClipPos(v.vertex);
		//uv坐标传递
		o.uv.xy = v.texcoord.xy;
		o.uv1.xy = o.uv.xy;
#if UNITY_UV_STARTS_AT_TOP
		if (_MainTex_TexelSize.y < 0)
			o.uv.y = 1 - o.uv.y;
#endif	
		return o;
	}

	fixed4 frag_add(v2f_add i) : SV_Target
	{
		fixed4 ori = tex2D(_MainTex, i.uv1);
		fixed4 light = tex2D(_VolumeLightTex, i.uv);
		return ori + light;
	}

	ENDCG

	SubShader
	{
		//pass 0: 高斯模糊
		Pass
		{
			ZTest Off
			Cull Off
			ZWrite Off
			Fog{ Mode Off }

			CGPROGRAM
			#pragma vertex vert_blur
			#pragma fragment frag_blur
			ENDCG
		}

		//pass 1: 叠加效果
		Pass
		{

			ZTest Off
			Cull Off
			ZWrite Off
			Fog{ Mode Off }

			CGPROGRAM
			#pragma vertex vert_add
			#pragma fragment frag_add
			ENDCG
		}

	}
}

模糊的话,我就直接沿用了以前写的高斯模糊,横向+纵向两遍模糊,最终直接把颜色值叠加回原始贴图,与Bloom效果或者上文的Raial Blur后处理体积光相似。

咱们把体积光渲染到RT上,效果以下:


把上图进行降分辨率(1/2)+高斯模糊后效果:


下面用几张图对比一下效果,第一张128次步进,原始效果:


4次步进效果,彻底乱套了:


4次步进,Dither效果,有网格:


4次步进,Dither,RT下降1/2分辨率高斯模糊后叠加效果:


固然,我只是简单粗暴地加上去了,也能够尝试一些更好玩的叠加方式。

经过Dither技术,能够大大下降Ray-Marching的消耗,不过这个效果的总体计算量仍是蛮大的,尤为对于阴影方面的处理,也比较麻烦,若是不考虑阴影,单纯计算体积光,可能会容易许多。不知道这个效果能不能在移动设备上跑得动,我也没敢尝试,哈哈。

总结

本篇文章尝试了四种目前实时渲染中常见的体积光实现的方式:BillBoard贴片,Volume Shadow体积阴影沿光方向挤出顶点,Radial Blur PostProcessing径向模糊后处理,Ray-Marching光线追踪体积光。四种实现,效果,耗费,实现困难度都是逐渐递增的。