游戏蛮牛学习群(纯技术交流,不闲聊):539178957
游戏蛮牛 手机端
开启辅助访问
 找回密码
 注册帐号

扫一扫,访问微社区

开发者专栏

关注:2260

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

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

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

[士郎] 高级干货——屏幕空间环境光遮蔽(SSAO)

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

6578

主题

7096

帖子

2万

积分

Rank: 16

UID
1231
好友
185
蛮牛币
8403
威望
30
注册时间
2013-7-29
在线时间
3375 小时
最后登录
2018-7-23

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

发表于 2018-1-12 12:04:59 | 显示全部楼层 |阅读模式

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

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

x
延续了我对Depth Buffer的一贯兴趣, 本文将介绍SSAO的基本原理及包括Temporal Coherence SSAO, Selective Temporal Filtering SSAO在内的优化算法.
何为"SSAO"
Screen Space Ambient Occlusion (以下简称SSAO), 屏幕空间环境光遮蔽. 在具体介绍SSAO之前, 本文先介绍更加广义的Ambient Occlusion (AO).
简单来说, Ambient Occlusion(以下简称"AO")是一种基于全局照明中的环境光(Ambient Light)参数和环境几何信息来计算场景中任何一点的光照强度系数的算法. AO描述了表面上的任何一点所接受到的环境光被周围几何体所遮蔽的百分比, 因此使得渲染的结果更加富有层次感, 对比度更高.


1.jpg



图片来自Wiki. 因为老人的皱纹处对外界暴露的部分较少, 使用AO后被遮蔽的部分较多, 渲染后显得更加暗一些, 增加了皱纹的层次感和质感.

AO的计算公式如下:



代表点的法线, 代表点切平面正方向的任意单位向量, 是可见函数, 如果点方向被遮挡则为1, 否则为0.

由此可见, 计算AO系数是一个颇为昂贵的操作. 一般离线渲染器都会采用Ray-Tracing(光线追踪)或是简化的Ray-Marching(所谓光线行进)算法, 模拟若干条射线以计算遮蔽百分比. 很明显这种方式不可能应用到实时图形渲染中. 尽管目前有一些实时计算AO的新技术, 但是其性能距离普及还有很长的路要走.

3.jpg

上图为基于Ray-Tracing的AO计算模型. 红色的射线表示V = 1, 绿色的射线表示V = 0.

那么我们能否Trade Off, 用差一点的渲染结果来获得更高的运行效率呢? 答案是肯定的, 而且方法还远不止一种. 本文将重点放在SSAO上.

顾名思义, "Screen Space"意味着SSAO并不是场景的预处理, 而是屏幕后期处理. 其原理是在片元着色器中对于屏幕上的每个像素模拟若干个位置随机的采样点, 用被遮蔽的采样点数量百分比来近似表示光照强度系数.

SSAO的实现
SSAO的实现可分为三个步骤: 计算AO, 模糊/滤波, 与Color Buffer混合.

1. 计算AO

计算AO的核心问题在于如何取采样点并判断这些采样点是否被遮蔽. 我们首先解决第一个问题. 在此我们使用一种指向法线方向的半球形采样块(Sample Kernel), 并在采样块中生成采样点. 距离原点越远的点, AO贡献越小. 采样块如下图所示:

4.jpg


那么我们转入第二个问题: 如何判断下图中的采样点遮蔽情况呢?


5.jpg


(涂黑的点处在几何体表面内部, 因此判定为被遮蔽)
一种方法是将采样点全部投影到View Plane上, 相当于获取采样点的UV坐标, 并同时获取Depth Buffer中该UV坐标处的深度值. 随后比较采样点的深度和场景中该点的深度. 如果采样点的深度更大, 说明其被场景遮蔽. 最终将所有采样点的AO贡献求和, 即是该点的AO值. 计算公式如下:


其中, 函数V前面已经介绍过. 函数D是一个[0, 1]之间的单调递减函数, 距离原点越近的采样点对AO的贡献越大. 一般使用指数函数.

6.jpg

上图为原图

7.jpg



上图为求得的AO值. 颜色越深代表AO越大.


以下是循环采样部分代码(ii为循环变量):

[AppleScript] 纯文本查看 复制代码
half3 randomDirection = RandomSample[ ii ];
float2 uv_offset = randomDirection.xy * scale;

float randomDepth = depth - ( randomDirection.z * _Radius );

float sampleDepth;
float3 sampleNormal;

DecodeDepthNormal ( tex2D ( _CameraDepthNormalsTexture, i.uv + uv_offset ), sampleDepth, sampleNormal );

sampleDepth *= _ProjectionParams.z;

float diff =  saturate( randomDepth - sampleDepth );
if ( diff > _ZDiff )
        occlusionAmount += pow (1 - diff, _Attenuation);

到此我们发现了一个问题: 上面求得的AO结果非常不理想. 图中有非常明显的条带状阴影, 给人的感觉像是在图上轻轻地抹了一层均匀的油漆. 产生这种现象的原因很简单 - 为了满足实时渲染的性能要求, 我们必须限制采样点的数目.

但是, 对于这种现象我们有一个Trick ---- 可以引入噪声, 将每个采样点以原点法线方向为旋转轴旋转随机的角度. 这样的新采样点会变得极其不规则, 更加离散化. 将低频的条纹转化成高频的噪声.

[AppleScript] 纯文本查看 复制代码
half3 randomVector = tex2D ( _RandomTexture, i.uv_random ).xyz * 2.0 - 1.0;
half3 randomDirection = reflect ( RandomSample[ ii ], randomVector );


8.jpg


上图为引入随机噪声后的采样结果. 我们发现"油漆"变成了"沙子".

2. 模糊/滤波

"油漆"好还是"沙子"好?

都不好!

"油漆"显得平淡无奇, "沙子"让人眼花缭乱. 中国人讲究中庸之道, 也就是说 ---- 我们需要一个"中频"的AO!

在此介绍两种方法. 第一种方法是直接模糊. 比较常用的是高斯模糊. 关于高斯模糊的资料有很多, 本文不再赘述.

第二种方法在采样原理上和高斯模糊别无二致, 只是采样系数由静态变为动态: 原点与采样点的UV坐标距离, 法线和深度关系共同决定采样系数, 距离越远采样系数越小, 法线和深度的差距越大则采样系数也越大. 这样的模糊使得结果更加趋近于中频, 进一步减弱了闪烁(Flickering)的效果.

3. 与Color Buffer混合.

一般加入Gamma Correction使得阴影更有层次感, 即最终结果为:

[AppleScript] 纯文本查看 复制代码
tex2D ( _MainTex, i.uv ) * pow ( ( 1 - occlusion ), 2.2 );


9.jpg
上图为SSAO处理后的最终结果.

SSAO的问题与优化策略
SSAO技术的基本原理已经介绍完了, 下面我们来谈谈SSAO可能遇到的问题, 以及相应的解决方案:

1. 采样块的问题

上文的SSAO实现方案其实是假定了使用Deferred Rendering, 深度和法线都可以非常容易得获取到, 因此我们的半球形采样块可以沿着顶点的法线方向摆放. 但是如果获取法线比较困难, 我们可以将半球形退化成球形, 这也正是2007年Crysis中SSAO的实现方案.

10.jpg



如上图所示, 采样块的摆放与法线无关, 采样点遍布整个球形中. 这种方法的效果自然不如半球形采样块.

2. 重复计算的问题

SSAO最为耗时的操作是模拟多个采样点并计算其AO贡献值, 因此我们应该想办法避免重复计算, 尽量使用以前的结果. 这里可以使用Reverse Reprojection(反向二次投影), 保存上一帧的AO计算信息, 使得当前帧中相对上一帧没有变化的点可以利用旧的AO信息, 避免重复计算. 这种方式称为SSAO with Temporal Coherence(时间相干性), 简称为TSSAO. 具体的实现方式将在下文中进行阐述.

3. 滤波与精确计算之间的矛盾

这也是一个Trade Off ---- 反正运算结果都是要套用滤波来过滤掉噪点, 那么最开始计算的时候就可以想办法在保证质量不受太大影响的前提下, 尽量提升效率. 举一个例子:

假设有两个面饼师傅, 第一个师傅的工作是揉出来5个直径为1.2cm的面球, 第二个师傅的工作是把这5个面球放在一起拍成一个面饼.
我们不讨论这种工作方式是否合理, 只是在此情况下第一个师傅确实不用对1.2cm吹毛求疵, 只要差不多就行了.
明白了这个道理, 就会发现可以在计算AO的时候使用降采样, 这样能成平方倍地降低SSAO的时间复杂度. 关于降采样网上有很多资料, 我的专栏第一篇文章也对此有过介绍.


TSSAO
1. Reverse Reprojection

交替使用两张Render Texture, 一张代表当前帧, 另一张代表上一帧. 对于当前帧上任何一个Pixel都可以根据其UV坐标重建其世界坐标, 然后根据上一帧的View-Projection矩阵的逆矩阵来转化成上一帧的相应UV坐标. 如果两帧上对应的Pixel的Depth与世界坐标差距不大, 那么当前帧就可以利用上一帧对应Pixel的信息, 免去重复计算.

对于静态场景我们有如下公式:



t表示NDC坐标, P表示Projection矩阵, V表示View矩阵.

[AppleScript] 纯文本查看 复制代码
inline float4 UV2WorldPos(float2 uv, float4x4 iv)
{
        float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv);        
        float2 invClipSize = float2(_CurP._11, _CurP._22);
        float2 spos = (uv * 2 - 1) / invClipSize;
        float depthEye = LinearEyeDepth(depth);        
    float3 vpos = float3(spos, -1) * depthEye;
        return mul(iv, float4(vpos, 1.0));
}

inline float2 WorldPos2UV(float4 worldPos, float4x4 vp)
{
        float4 vpos = mul(vp, worldPos);
        float2 projOffset = float2(_CurP._13, _CurP._23);
        float2 screenPos = vpos.xy / vpos.w + projOffset;
        return 0.5 * screenPos + 0.5;
}

inline float2 GetMotionVector(float2 uv)
{
        float4 worldPos = UV2WorldPos(uv, _CurIV);
        float2 curUv = WorldPos2UV(worldPos, _CurVP);
        float2 preUv = WorldPos2UV(worldPos, _PrevVP);
        return  curUv - preUv;
}


但是对于动态场景上述公式不再成立. 因为SSAO要做到Scene Independent, 同时还必须记录前后两帧的关联, 因此在此我们可以用一个Render Texture单独记录每个像素在前一帧与当前帧的世界坐标的差. 具体的原因详见下文.

2. 新的AO计算公式

在TSSAO中我们使用两个Render Texture分别作为上一帧和当前帧的AO Buffer. 首先计算当前帧的AO贡献值:


表示的是之前已经计算过的采样点数量, k表示每一帧应该计算的采样点数量.

随后, 利用当前帧和上一帧的AO贡献值共同计算当前帧的AO值:


最后, 更新的值:



在此说明一下为什么要设置n的上限. 这里主要有两个原因, 第一是如果之前的计算结果不老化, 当前帧的AO贡献值会越来越小, 算法的反应会越来越慢. 第二是反向二次投影本身是有误差的, 随着投影次数的增加误差会变得非常大, 因此必须限制被使用的结果数量, 适当舍弃掉过老的数据.

3. 检测不合法像素


很容易想到的一个判定条件是深度检测: 如果新旧两个像素的深度差距过大, 那么说明场景 已经改变, 当前像素的AO值已经不正确, 必须全部舍弃. 相对深度关系检测条件如下:

但是, SSAO考虑的不仅仅是当前点, 还有它周边的环境. 举一个例子: 在一个静态的地面上放置着一个动态的立方体. 这个立方体随着时间不规则运动. 地面与立方体地面棱边的外交界处的AO值自然明显高出地面上其他点的值, 但是立方体的移动会使得地面上相应区域的AO值不再有效 ---- 虽然地面没有在动, 地面上的点能够通过深度关系检测.

11.jpg



这里我们用到了另一个Trick ---- 我们在计算当前帧AO的循环中可以同时做出以下判定:


只要有一个采样点不满足上述条件, 则说明其对应原点的周边环境已经发生改变, 其AO值自然也应该重新计算. 对于的计算方法也很简单: 根据和在第一步中记录的世界坐标的差就可以直接得出了.


只要像素被判定为不合法, 则其会被重置为0, 即之前的所有AO运算结果全部舍弃.


[AppleScript] 纯文本查看 复制代码
int ww = w;

if ( abs ( 1 - texelDepth / preDepth ) >= EPS )
        ww = 0;

float scale = _Parameters.x / depth;

for ( int ii = 0; ii < _SampleCount; ii++ )
{
        float3 randomDirection = RAND_SAMPLES [ w + ii ];
        randomDirection *= -sign ( dot ( texelNormal, randomDirection ) );
        randomDirection += texelNormal * 0.3;

        float2 uv_offset = randomDirection.xy * scale;

        float randomDepth = texelDepth - randomDirection.z * _Parameters.x;

        float sampleDepth;
        float3 sampleNormal;

        GetDepthNormal ( i.uv + uv_offset, sampleDepth, sampleNormal );

        sampleDepth *= _ProjectionParams.z;

        float diff = randomDepth - sampleDepth;

        if ( diff > _Parameters.y )
                occlusion += saturate ( cos ( dot ( randomDirection, texelNormal ) ) ) * pow ( 1 - diff, _Parameters.z );

        if ( abs ( randomDirection - abs ( randomDirection + GetMotionVector ( i.uv + uv_offset ) - GetMotionVector ( i.uv ) - texelPosition ) ) >= EPS )
}



4. 滤波

可以在SSAO的滤波基础上加上收敛度的条件. 收敛度定义为:



收敛度越大, 说明当前像素越为"安全", 随着时间的改变越小, 因此采样系数也越大.

Selective Temporal Filtering (STF)
这项技术应用在了BattleField3中. 首先它是基于TSSAO的, 不同的是其AO计算方式: 在当前帧的AO贡献值和历史数据中间做插值.

这样做带来的一个小问题就是"老化" ---- 旧的数据不能及时清理出去, 这就导致场景移动比较快的时候, AO Buffer会存在鬼影的问题.


但是一个更为严重的问题是"闪烁" ---- BattleField3的场景中有大量花草树木, 树叶的晃动使得大量像素被频繁检测为失效, 重新计算AO, 这与未失效的部分构成了鲜明的对比.


DICE的解决方案非常Trick: 他们发现存在鬼影的像素和存在闪烁的像素是互斥的. 因此他们想办法甄别这两种像素, 并对于可能产生鬼影的像素关掉Temporal Filtering. 因此这项技术被称为Selective Temporal Filtering.


具体的方法是检测连续性: 对于任何一个Pixel, 连续在x或y方向选择两个像素, 判断这三个像素的深度是否连续. 如果连续则可能产生鬼影, 否则可能产生闪烁.


12.jpg

最后将所有闪烁的像素按照4x4放大, 圈定进行Temporal Filter的区域:


13.jpg

14.jpg




后记
SSAO原理并不复杂, 只是在实际应用场景中会有各种各样的Trick以应对个性化的需要. 文中主要讲解了SSAO的基本原理与TSSAO的优化原理, 并举了BattleField3的STF为例.

评分

参与人数 1鲜花 +2 收起 理由
staryy + 2 很给力!

查看全部评分


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

使用道具 举报

排名
239
昨日变化

34

主题

862

帖子

4184

积分

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

UID
6108
好友
17
蛮牛币
4434
威望
0
注册时间
2013-10-22
在线时间
1302 小时
最后登录
2018-7-23

社区QQ达人七夕浪漫情人

QQ
发表于 2018-1-12 13:34:58 | 显示全部楼层
虽然看不懂 但是点个赞吧

回复 支持 反对

使用道具 举报

6蛮牛粉丝
1478/1500
排名
3021
昨日变化
19

5

主题

636

帖子

1478

积分

Rank: 6Rank: 6Rank: 6

UID
239879
好友
1
蛮牛币
2679
威望
0
注册时间
2017-8-26
在线时间
397 小时
最后登录
2018-7-23

迈向小康

发表于 2018-1-12 13:49:16 | 显示全部楼层
虽然看不懂 但是点个赞吧
[发帖际遇]: lzklzklzk 捡了钱没交公 蛮牛币 降了 1 . 幸运榜 / 衰神榜

回复 支持 反对

使用道具 举报

5熟悉之中
909/1000
排名
3588
昨日变化
13

2

主题

155

帖子

909

积分

Rank: 5Rank: 5

UID
237394
好友
0
蛮牛币
42
威望
0
注册时间
2017-8-13
在线时间
374 小时
最后登录
2018-7-23
发表于 2018-1-12 14:02:59 | 显示全部楼层
虽然看不懂 但是点个赞吧

回复 支持 反对

使用道具 举报

5熟悉之中
767/1000
排名
6462
昨日变化
2

2

主题

306

帖子

767

积分

Rank: 5Rank: 5

UID
238701
好友
1
蛮牛币
643
威望
0
注册时间
2017-8-21
在线时间
275 小时
最后登录
2018-7-14
发表于 2018-1-12 14:36:02 | 显示全部楼层
清风有这个Advanced ANT+插件耶?

回复 支持 反对

使用道具 举报

4四处流浪
363/500
排名
9077
昨日变化
3

2

主题

71

帖子

363

积分

Rank: 4

UID
155434
好友
0
蛮牛币
78
威望
0
注册时间
2016-7-6
在线时间
176 小时
最后登录
2018-6-20
发表于 2018-1-12 15:08:11 | 显示全部楼层
完全看不懂- -!

回复

使用道具 举报

3偶尔光临
254/300
排名
9726
昨日变化
3

0

主题

41

帖子

254

积分

Rank: 3Rank: 3Rank: 3

UID
261576
好友
0
蛮牛币
355
威望
0
注册时间
2017-12-28
在线时间
112 小时
最后登录
2018-6-11
发表于 2018-1-12 16:00:30 | 显示全部楼层
大神的帖子 真是各种高端 但是看不懂哇

回复 支持 反对

使用道具 举报

7日久生情
2556/5000
排名
360
昨日变化
2

0

主题

231

帖子

2556

积分

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

UID
14494
好友
0
蛮牛币
2204
威望
0
注册时间
2014-2-15
在线时间
725 小时
最后登录
2018-7-14

VIP

发表于 2018-1-12 16:10:42 | 显示全部楼层
虽然看不懂 但是点个赞吧

回复 支持 反对

使用道具 举报

排名
60174
昨日变化
13

0

主题

38

帖子

72

积分

Rank: 2Rank: 2

UID
5356
好友
0
蛮牛币
1
威望
0
注册时间
2013-10-10
在线时间
31 小时
最后登录
2018-1-12
发表于 2018-1-12 16:26:10 | 显示全部楼层
RE: Gesture Recognizer 2.1.unitypackage [修改
[发帖际遇]: chenshiran13 被钱袋砸中进医院,看病花了 1 蛮牛币. 幸运榜 / 衰神榜

回复 支持 反对

使用道具 举报

5熟悉之中
557/1000
排名
37024
昨日变化
7

0

主题

321

帖子

557

积分

Rank: 5Rank: 5

UID
134576
好友
0
蛮牛币
228
威望
0
注册时间
2016-1-13
在线时间
230 小时
最后登录
2018-7-18
发表于 2018-1-12 16:40:41 | 显示全部楼层

虽然看不懂 但是点个赞吧

回复 支持 反对

使用道具 举报

6蛮牛粉丝
1044/1500
排名
2427
昨日变化
3

1

主题

252

帖子

1044

积分

Rank: 6Rank: 6Rank: 6

UID
147717
好友
0
蛮牛币
1303
威望
0
注册时间
2016-5-5
在线时间
259 小时
最后登录
2018-7-23
发表于 2018-1-12 16:51:41 | 显示全部楼层
虽然看不懂 但是点个赞吧

回复 支持 反对

使用道具 举报

7日久生情
1682/5000
排名
2663
昨日变化
13

40

主题

721

帖子

1682

积分

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

UID
219600
好友
7
蛮牛币
2401
威望
0
注册时间
2017-4-27
在线时间
431 小时
最后登录
2018-7-23
发表于 2018-1-12 17:58:38 | 显示全部楼层
感谢分享

回复

使用道具 举报

7日久生情
4094/5000
排名
145
昨日变化
1

18

主题

310

帖子

4094

积分

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

UID
67323
好友
1
蛮牛币
3187
威望
0
注册时间
2015-1-10
在线时间
1622 小时
最后登录
2018-7-23
发表于 2018-1-12 21:32:45 | 显示全部楼层
这个有点难啃,让我先舔软

回复 支持 反对

使用道具 举报

7日久生情
2535/5000
排名
3453
昨日变化
2

2

主题

1780

帖子

2535

积分

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

UID
241666
好友
0
蛮牛币
10199
威望
0
注册时间
2017-9-6
在线时间
362 小时
最后登录
2018-6-7
发表于 2018-1-13 10:26:39 来自Mobile--- | 显示全部楼层
感谢分享

回复

使用道具 举报

7日久生情
2535/5000
排名
3453
昨日变化
2

2

主题

1780

帖子

2535

积分

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

UID
241666
好友
0
蛮牛币
10199
威望
0
注册时间
2017-9-6
在线时间
362 小时
最后登录
2018-6-7
发表于 2018-1-13 10:45:29 来自Mobile--- | 显示全部楼层
感谢分享

回复

使用道具 举报

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

本版积分规则

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