【游戏技术群】959392658  【游戏出海群】12067810
游戏蛮牛 手机端
开启辅助访问
 找回密码
 注册帐号

扫一扫,访问微社区

开发者专栏

关注:2373

当前位置:游戏蛮牛 技术专区 开发者专栏

__________________________________________________________________________________
开发者干货区版块规则:

  1、文章必须是图文形式。(至少2幅图)
      2、文章字数必须保持在1500字节以上。(编辑器右下角有字数检查)
      3、本版块只支持在游戏蛮牛原创首发,不支持转载。
      4、本版块回复不得无意义,如:顶、呵呵、不错......【真的会扣分的哦】
      5、......
__________________________________________________________________________________
查看: 1466|回复: 21

[士郎] Unity3D实时体积光

[复制链接]  [移动端链接]
排名
3
昨日变化

7085

主题

7612

帖子

2万

积分

Rank: 16

UID
1231
好友
185
蛮牛币
11082
威望
30
注册时间
2013-7-29
在线时间
3640 小时
最后登录
2018-12-14

社区QQ达人活力之星原创精华达人突出贡献奖财富之证游戏蛮牛QQ群会员蛮牛妹VIP

发表于 2018-9-26 15:35:37 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有帐号?注册帐号

x
体积光是现实中常见的因丁达尔效应而产生的一种大气现象,文人墨客常用“慵懒的阳光泄下”描绘该现象带来的美感。笔者在一次旅游后见到了这种神奇的自然现象,遂决定在游戏中实现并使用这样的效果。
2.jpg
青藏高原上雨后初晴的体积光
体积光是由空气中的水蒸气和灰尘对光线的散射造成的,显然在实时渲染中我们无法模拟庞杂的微小粒子,只能用近似的方法获得,实现的原理其他的大神已经解释的非常详细,我们这里将更加着重于实现。

我们将使用Unity自带的Shadowmap实现体积光,众所周知,实时渲染中的阴影是通过将像素点的世界坐标(在fragment shader中获得或由像素深度反推获得)乘灯光的viewProjectionMatrix获得该像素点在shadowmap下的uv,并与shadowmap记录的像素点深度进行比较,若shadowmap记录的深度大于等于该点,则表示该点并没有在阴影下,应该受到光照,反之则表示该点受到遮挡,不应该受到光照。Unity中目前支持的实时光照有Directional Light, Spot Light, Point Light,这三种光照类型的实现大同小异。

另外,希望读者在阅读之前,确保自己对Unity渲染管线,官方提供的.cginc文件以及CommandBuffer, GL等API比较熟悉,接下来的分析将不会出现对此类基础的详解。

效果的实现全部在本人的开源里,注意,本开源项目并非完全原创,在经过Slightly Mad大神的同意后,对其开源原型进行魔改,增减.

3.jpg

Directional light常用来模拟太阳光,月光等自然光照,由于所有物体都会受到directional light的影响,因此我们的可以直接使用后处理来实现。正如链接里的解释,raymarch的基本原理就是将一条线段按比例分成多段,然后将每个顶点上的采样结果相加,所以首先需要获得射线的起点,我们通过以下代码获得renderTexture上的4个角的近裁面位置,然后在后处理shader中通过fragment shader的线性插值即可获得每个像素点在近裁面上的位置:
[AppleScript] 纯文本查看 复制代码
cam = GetComponent<Camera>();
Matrix4x4 inverseViewProjectionMatrix = GL.GetGPUProjectionMatrix(cam.projectionMatrix, true);
inverseViewProjectionMatrix *= cam.worldToCameraMatrix;
inverseViewProjectionMatrix = inverseViewProjectionMatrix.inverse;
Vector3 leftBottom = inverseViewProjectionMatrix.MultiplyPoint(new Vector3(-1, -1, 1));
Vector3 rightBottom = inverseViewProjectionMatrix.MultiplyPoint(new Vector3(1, -1, 1));
Vector3 leftTop = inverseViewProjectionMatrix.MultiplyPoint(new Vector3(-1, 1, 1));
Vector3 rightTop = inverseViewProjectionMatrix.MultiplyPoint(new Vector3(1, 1, 1));


这样我们就拥有了摄像机近裁面的四个角的世界坐标,同理可以通过这样的方法取得摄像机远裁面的四个角:

[AppleScript] 纯文本查看 复制代码
cam = GetComponent<Camera>();
Matrix4x4 inverseViewProjectionMatrix = GL.GetGPUProjectionMatrix(cam.projectionMatrix, true);
inverseViewProjectionMatrix *= cam.worldToCameraMatrix;
inverseViewProjectionMatrix = inverseViewProjectionMatrix.inverse;
Vector3 leftBottom = inverseViewProjectionMatrix.MultiplyPoint(new Vector3(-1, -1, 0));
Vector3 rightBottom = inverseViewProjectionMatrix.MultiplyPoint(new Vector3(1, -1, 0));
Vector3 leftTop = inverseViewProjectionMatrix.MultiplyPoint(new Vector3(-1, 1, 0));
Vector3 rightTop = inverseViewProjectionMatrix.MultiplyPoint(new Vector3(1, 1, 0));


使用其他引擎或DirectX的朋友请注意,是使用OpenGL标准的投影,所以我们这里需要经过GL.GetGPUProjectionMatrix函数获得确实的投影矩阵,并且远裁面深度为0,近裁面深度为1。


以近裁面坐标作为线段起点,以近裁面坐标到远裁面坐标为线段方向,我们还需要确定线段的终点,显然,直接取远裁面是非常愚蠢的,假设摄像机会渲染1000米,而我们的显卡性能中等偏良好,大概可以承受64次采样的性能消耗,那每一步采样之间将会相隔1000 / 64 = 15.625米,也就是说在15米左右之内的物体变化都不会体积光产生任何影响,而且还会导致采样点出现在着色像素之后,产生严重的bug。


因此,我们需要通过深度图,获得屏幕像素的深度并以此为线段的终点,同时限制线段最大长度,将线段长度设置到一个精度与视觉效果的相对平衡,确定终点的示例代码大致如下:

[AppleScript] 纯文本查看 复制代码
// _InverseViewProjectionMatrix UNITY_MATRIX_VP矩阵的逆矩阵
// _SampleCount  采样率
// _VolumetricIntensity 体积光强度
// _MaxLength 最远采样距离
float depth = tex2D(_CameraDepthTexture, i.uv).r;   //像素深度
float worldPos = mul(_InverseViewProjectionMatrix, float3(i.uv * 2 - 1, depth));   //通过NDC坐标反推世界坐标
float3 startPos = ...    //已经取得的世界空间近裁面坐标
float3 direction = normalize(worldPos - startPos);    //线段归一化方向
float m_length = min(_MaxLength, length(worldPos - startPos));      //获取线段的目标长度
float perNodeLength = m_length / _SampleCount;          //每两个采样点之间的距离
float3 currentPoint = startPos;    //记录当前采样点的位置
float intensity = 0;
for (int i = 0; i < _SampleCount; ++i){    //进行固定次数的采样
    currentPoint += direction * perNodeLength;   //更新当前采样点
    intensity += GetShadow(currentPoint);      //获得当前坐标的阴影遮挡信息
}
intensity *= _VolumetricIntensity * m_Length;      //对体积光的强度进行控制
return intensity;


通过类似实现,可以获得一个简易的体积光效果,如何获得阴影采样的实现,在UnityCG.cginc和UnityShadowLibrary.cginc中有详细的实现,文件目录在(Unity Folder)/Editor/Data/CGIncludes/,其中平行光的阴影采样涉及到cascadeShadowMap的计算,需要对采样进行由近及远分成4层,有兴趣的朋友可以自行研究一下,这里不再赘述。


2. Point Light
不同光源之间其实唯一的不同就是计算ray march线段的起点与终点的方法不同,point light的照射范围其实是一个球形,所以我们可以简单的直接在灯光的位置放一个球,然后通过直线与球交点的计算方法,然后其他诸如深度比较等操作,与Directional Light并无不同,示例代码大致如下:

[AppleScript] 纯文本查看 复制代码
// _Center 球心坐标
// _Radius 球半径
// _InverseViewProjectionMatrix UNITY_MATRIX_VP矩阵的逆矩阵
// _SampleCount  采样率
// _VolumetricIntensity 体积光强度
float depth = tex2D(_CameraDepthTexture, i.uv).r;   //像素深度
float pixelWorldPos= mul(_InverseViewProjectionMatrix, float3(i.uv * 2 - 1, depth));   //通过NDC坐标反推世界坐标
float3 wpos = ...   //球表面位置
float3 wNormal = ... //归一化的球表面法线
float3 startPos = wpos;   //确定线段起始点
float3 camToSurface = wpos - _WorldSpaceCameraPos;//从摄像机到表面的方向,即ray march的迭代方向
float camToSurfaceLength = length(camToSurface);//摄像机到球表面的距离
float3 surfaceToCenter = -wNormal * _Radius;   //球表面到球心
float3 direction = normalize(camToSurface);    //线段方向
float3 desiredEndPoint = startPos + dot(normalize(camToSurface), surfaceToCenter) * 2 * camToSurface;
//这里通过简单的立体几何知识,求得射线与球的前后两个相交点
float m_length = length(startPos - pixelWorldPos);   //从起点到屏幕像素点的距离
m_length = min(m_length, length(startPos - desiredEndPoint));//获得最小线段距离
float intensity = 0;
float3 currentPoint = startPos;
float perNodeLength = m_length / _SampleCount;          //每两个采样点之间的距离
for(int i = 0; i < _SampleCount; ++i){
    currentPoint += direction * perNodeLength;     //更新当前采样点
    intensity += GetShadow(currentPoint - _Center);  
    //注意,point light使用cubemap作为shadowmap,因此我们需要传入球心到采样点的方向
}
intensity *= _VolumetricIntensity * m_Length;      //对体积光的强度进行控制
return intensity;



3. Spot Light
原理与点光源其实大致相似,只不过我们这里为了性(tou)能(lan),就不在shader中判断角度,而是直接使用CommandBuffer渲染两次锥形mesh,然后通过两次渲染的深度的差别,计算线段的起点和终点,示例代码大致如下(注意,这段代码在背面深度已渲染到指定的RenderTarget之后,被渲染的像素应该在Mesh的正面):

[AppleScript] 纯文本查看 复制代码
// _InverseViewProjectionMatrix UNITY_MATRIX_VP矩阵的逆矩阵
// _SampleCount  采样率
// _VolumetricIntensity 体积光强度
// _BackDepthTexture  光源mesh的背面深度
float backDepth = tex2D(_BackDepthTexture, i.uv).r; //背面深度
float backWorldPos= mul(_InverseViewProjectionMatrix, float3(i.uv * 2 - 1, backDepth ));   //通过NDC坐标反推世界坐标
float depth = tex2D(_CameraDepthTexture, i.uv).r; //深度图像素深度
float pixelWorldPos= mul(_InverseViewProjectionMatrix, float3(i.uv * 2 - 1, depth ));   //通过NDC坐标反推世界坐标
float3 direction = normalize(backWorldPos - _WorldSpaceCameraPos);
float3 startPos = wpos; // 起点位置即为Mesh的正面像素位置
float m_Length = min(length(backWorldPos - startPos), length(pixelWorldPos - startPos)); //线段长度
float intensity = 0;
float3 currentPoint = startPos;
float perNodeLength = m_length / _SampleCount;          //每两个采样点之间的距离
for(int i = 0; i < _SampleCount; ++i){
    currentPoint += direction * perNodeLength;     //更新当前采样点
    intensity += GetShadow(currentPoint);
}
intensity *= _VolumetricIntensity * m_Length;      //对体积光的强度进行控制
return intensity;



直接渲染两次mesh这个方法确实比较丑陋,不过我们两者相权取其轻,多一个drawcall一般来讲比在shader中进行复杂的运算更为节省性能,而且还要考虑到lighting cookie的使用,在许多次时代渲染中,场景地编常常使用cookie改变灯光形状,增加灯光真实度,因此我们必须保证GetShadow传入的值确实的存在于shadowmap中。
4. 优化性能:
由于多次迭代,体积光的消耗实际非常高昂,即使是在性能强大的PC平台也是如此,因此我们可以考虑许多优化方法大幅度降低性能消耗,而降采样就是其中非常好的一种方法。我们将目标贴图的分辨率设置为摄像机Render Target的分辨率的一半,也就是直接将计算量缩小到了之前的1/4,但是降采样后的图像马赛克化严重,我们可以使用高斯模糊,将其通过正态分布的模糊采样放回到原分辨率的贴图中,降采样不仅不会降低画质,反而会给体积光带来一种朦胧感与意境。
4.jpg
降采样模糊后的太阳体积光效果



5. 进一步提高质量:
细心观察的朋友已经注意到,这个体积光虽然已经实现,但是极其生硬,单调,仿佛蒙了一层布一样,实在谈不上美感,原因很简单,空气中的雾气,尘埃并不是简单的发光,而是折射太阳光,因此我们需要考虑透光时光线强弱的变化。


首先,既然是透射,那么当视线与太阳射入角度越小时,阳光应该越发强劲,这种算法称之为Mie Scattering,即通过视线与光源夹角计算强度值,有时会在次表面透射材质中使用到。加上Mie Scattering后效果为:
5.jpg
加上Mie Scattering后已经初步拥有了真实感
大气的情况复杂多变,有时雾并不是静止不动而是在风的影响下有缓慢的飘动,但是若是使用3D贴图进行二次raymarch,将会导致性能消耗非常高昂,这是无论如何也无法接受的,因此我们这里使用一个比较Trick的方法,使用一张2D贴图在屏幕空间进行采样,影响体积光的强度。(Demo中这么使用确实没什么问题,但是有时场景复杂可能会穿帮)。
6.jpg
水雾的密度大于空气,因此会缓慢下沉,由于我们的ray march全程使用统一的世界坐标,因此想要基于采样点的高度进行强度累计非常容易,这里就不赘述了,直接给出最终实现效果:

7.jpg
拥有了高度雾与扰动贴图后的效果有了较大的提升
唔,比较核心的技术细节大概就这些了,想到了再加,想到了再加,嘿嘿。


知乎@MaxwellGeng




1.png

跟我念“站长妹纸萌萌哒!”我说站长,你说YO!爱你们么么哒~
回复

使用道具 举报

6蛮牛粉丝
1369/1500
排名
3808
昨日变化
3

0

主题

807

帖子

1369

积分

Rank: 6Rank: 6Rank: 6

UID
210390
好友
0
蛮牛币
1646
威望
0
注册时间
2017-3-7
在线时间
182 小时
最后登录
2018-12-15
发表于 2018-9-26 18:02:21 | 显示全部楼层

回复

使用道具 举报

7日久生情
2855/5000
排名
2481
昨日变化
1

2

主题

1848

帖子

2855

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
241666
好友
0
蛮牛币
10740
威望
0
注册时间
2017-9-6
在线时间
443 小时
最后登录
2018-12-9
发表于 2018-9-27 06:28:04 | 显示全部楼层
感谢分享代码

回复

使用道具 举报

5熟悉之中
871/1000
排名
2796
昨日变化
3

1

主题

101

帖子

871

积分

Rank: 5Rank: 5

UID
248064
好友
1
蛮牛币
1308
威望
0
注册时间
2017-10-10
在线时间
265 小时
最后登录
2018-12-10
发表于 2018-9-27 08:30:50 | 显示全部楼层
顶一个!!!!!!!!!!!!
[发帖际遇]: 雾霾 在论坛发帖时没有注意,被小偷偷去了 2 蛮牛币. 幸运榜 / 衰神榜

回复

使用道具 举报

3偶尔光临
299/300
排名
6836
昨日变化
53

0

主题

45

帖子

299

积分

Rank: 3Rank: 3Rank: 3

UID
200493
好友
0
蛮牛币
1301
威望
0
注册时间
2017-1-11
在线时间
68 小时
最后登录
2018-12-15
发表于 2018-9-27 08:50:20 | 显示全部楼层
666666666666666

回复 支持 反对

使用道具 举报

7日久生情
1617/5000
排名
1739
昨日变化
1

0

主题

398

帖子

1617

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
136635
好友
0
蛮牛币
1685
威望
0
注册时间
2016-2-15
在线时间
485 小时
最后登录
2018-12-14
发表于 2018-9-27 09:21:43 | 显示全部楼层
感谢分享,学习了

回复

使用道具 举报

5熟悉之中
620/1000
排名
4848
昨日变化
29

1

主题

138

帖子

620

积分

Rank: 5Rank: 5

UID
204004
好友
0
蛮牛币
448
威望
0
注册时间
2017-1-22
在线时间
191 小时
最后登录
2018-12-15
发表于 2018-9-27 09:37:44 | 显示全部楼层

回复

使用道具 举报

4四处流浪
320/500
排名
7067
昨日变化
3

0

主题

62

帖子

320

积分

Rank: 4

UID
250263
好友
0
蛮牛币
441
威望
0
注册时间
2017-10-23
在线时间
80 小时
最后登录
2018-12-14
发表于 2018-9-27 09:49:02 | 显示全部楼层
{:104:}{:104:}

回复

使用道具 举报

7日久生情
3180/5000
排名
311
昨日变化
2

1

主题

736

帖子

3180

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
27356
好友
1
蛮牛币
4720
威望
0
注册时间
2014-5-30
在线时间
627 小时
最后登录
2018-12-14
发表于 2018-9-27 09:49:17 | 显示全部楼层
请教下:
这个效果和“上帝射线”有什么本质上的区别吗?

回复 支持 反对

使用道具 举报

5熟悉之中
633/1000
排名
5350
昨日变化
4

0

主题

213

帖子

633

积分

Rank: 5Rank: 5

UID
192389
好友
1
蛮牛币
942
威望
0
注册时间
2016-12-14
在线时间
162 小时
最后登录
2018-12-12
发表于 2018-9-27 09:54:07 | 显示全部楼层
真的很牛,太厉害了大大

回复 支持 反对

使用道具 举报

5熟悉之中
685/1000
排名
4164
昨日变化
21

0

主题

188

帖子

685

积分

Rank: 5Rank: 5

UID
267103
好友
0
蛮牛币
1699
威望
0
注册时间
2018-1-31
在线时间
157 小时
最后登录
2018-12-14
发表于 2018-9-27 10:38:17 | 显示全部楼层
[发帖际遇]: 学习ingNice 在网吧通宵,花了 1 蛮牛币. 幸运榜 / 衰神榜

回复

使用道具 举报

0

主题

3

帖子

9

积分

Rank: 1

UID
69696
好友
0
蛮牛币
17
威望
0
注册时间
2015-1-19
在线时间
6 小时
最后登录
2018-9-27
发表于 2018-9-27 20:46:06 | 显示全部楼层
tf107 发表于 2018-9-27 09:49
请教下:
这个效果和“上帝射线”有什么本质上的区别吗?

实际上就是God Ray

回复 支持 反对

使用道具 举报

7日久生情
1665/5000
排名
2312
昨日变化
4

17

主题

656

帖子

1665

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
214924
好友
3
蛮牛币
5103
威望
0
注册时间
2017-3-28
在线时间
396 小时
最后登录
2018-12-15
发表于 2018-9-28 13:28:49 | 显示全部楼层
膜拜ing~

回复

使用道具 举报

3偶尔光临
215/300
排名
12741
昨日变化
10

0

主题

66

帖子

215

积分

Rank: 3Rank: 3Rank: 3

UID
235994
好友
2
蛮牛币
28
威望
0
注册时间
2017-8-5
在线时间
82 小时
最后登录
2018-11-30
发表于 2018-10-9 09:36:42 | 显示全部楼层
卧槽好牛B,先看看效果

回复 支持 反对

使用道具 举报

4四处流浪
354/500
排名
8778
昨日变化
92

4

主题

80

帖子

354

积分

Rank: 4

UID
135780
好友
0
蛮牛币
688
威望
0
注册时间
2016-1-28
在线时间
136 小时
最后登录
2018-12-14
发表于 2018-10-12 09:13:54 | 显示全部楼层
好牛 我看看就好了

回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册帐号

本版积分规则

快速回复 返回顶部 返回列表