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

本文介绍了Unity中实现GodRay(体积光)的四种方法:BillBoard特效贴片、Volume Shadow光方向挤出、径向模糊后处理以及Ray-Marching光线追踪。通过实例展示了每种方法的实现原理、代码示例和效果对比,探讨了性能与效果的平衡,适合Unity开发者学习参考。
摘要由CSDN通过智能技术生成

前言

好久没有更新博客了,经历了不少事情,好在最近回归了一点正轨,决定继续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中的体积光有两个重要的特性,第一个是根据距离远近 ,动态调整体积光的颜色及透明度,来达到更加真实的体积光的效果,在远距离看不清体积光,距离近些时逐渐清晰,当距离很近时,降低强度,使之更容易看清背后物体。第二个特性是动态调整体积光网格的位置,当摄像机贴近体积光时,避免了相机与半透穿插,同时也避免了因半透占屏比高导致的像素计算暴涨的性能问题。

下面附上一段代码:

//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 dist
  • 84
    点赞
  • 201
    收藏
    觉得还不错? 一键收藏
  • 27
    评论
评论 27
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值