TSSAO Temporal Screen-Space Ambient Occlusion (Unity3d 5 示例实现)

前提

环境光(ambient occlusion)是一种GI,其简化形式SSAO可以用“微量高效”来形容,消耗得很少,得到的效果很好。
环 境光遮蔽(ambient occlusion)的本质是计算在一个点的半球面范围内有多少方向被阻塞(如下图1.2.1),然后根据它调整表面颜色。如果实时渲染使用的话非常消 耗,所以在游戏中一般都使用(screen-space ambient occlusion)SSAO。SSAO使用depth buffer来近似一个离散的场景(如下图1.2.2),从而获得固定的开销,而且实现起来比较简单。但是由于SSAO采样数目小,所以产生的效果也不理 想,很容易产生noise。在物体移动时noise会更明显。而且SSAO是low-frequency低分辨率的。所以我们使用时间相干性 (temporal coherence)的SSAO来解决这个问题。

 

 

 
反 向二次投影技术(reverse reprojection)让我们重用上一帧的像素点并且随时间优化精制它们。这可以保持每帧采样点的数量较少,并在短时间有效的累加到上百个采样点。所 以时间相干性很适用于AO。并且与光源和视点无关,AO只与局部邻域(local neighborhood)的几何结构相关,SSAO与像素邻域的场景结构有关。本文主要讲解在延迟渲染戏下使用二次投影技术(reverse reprojection)提升SSAO的质量。

AO(Ambient Occlusion)

从物理的角度来看,AO时sky light 产生的漫反射。计算公式如下:
 
w 是半球上的所有方向,V是二进制可见度函数(binary visibility function),如果可见V(p,w)=1,如果被阻塞的话就为0。D是一个在0-1之间的单调递减函数,随着p到ξ的距离增大而减小,ξ为射线到最 近的表面的交点。简化一下,D是一个阶梯函数。尽管光滑的衰减已经提供了不错的结果,但是采样半径仍是最大的问题

SSAO(Screen Space Ambient Occlusion)

SSAO是一种近似AO的方法,在屏幕空间完成。
我们可以用两种方法判断阻塞:
1.    采样点与当前的depth距离
2.    采样点和当前点的距离

可以用如下公式表示:
 
si为采样点,p为当前点,C为contribution
为了近似公式1.1使用蒙特卡罗积分法,contribution函数如下:
 
AO 的方向射线被替换为p周围的采样点,所以这里的V(p,si)二进制可见度函数(binary visibility function),在p点是否可见采样点si,如果可见则为0,不可见则为1。V是相对于当前z-buffer的。有些SSAO方法省略了depth test,它们的si的contribution是基于p到si的距离和入射角度。D是p到si距离的衰减。

反向二次投影(Reverse Reprojection)

反 向二次投影将当前帧像素与上一帧相同世界空间位置的像素结合起来,可以在当前帧重用上一帧的像素。这个方法可以用在很多技术上,比如shadow mapping, antialiasing, motion blur等等。我们把上一帧的AO信息存在render target中。在静态几何结构中,二次投影在整个帧是一个常量。使用上一帧(f-1)和当前帧(f)的视点(V)和投影矩阵(P),t为像素的 post-perspective位置
 
我 们使用延迟渲染存储的这一帧和上一帧eye-linear depth来求出世界空间坐标,也可以直接存储世界坐标到一张render target中(博主觉得后者比较方便,也免去重复计算)。我们只需要转换当前世界坐标Pf与上一帧的view-projection matrix  Pf-1Vf-1来得到tf-1
我们根据tf-1来计算查找纹理坐标texf-1来查找上一帧的AO buffer。应用透视除法并且缩放结果在0-1范围内。
 

动态物体

如 果在动态场景中方程1.4是无效的,因为二次投影是基于动态物体的转换。解决方案是进行两次顶点转换,一次是使用当前帧转换参数(modeling matrix, skinning.),另一次是使用前一帧的参数。我们无法储存转换参数,所以我们把Pf-1-Pf存到一张render target中,

此处可以使用比depth精度低的render target来存储这个偏移,可以用16bit。


简单讲解一下unity render target format

  

ARGB32

色彩渲染贴图格式,每通道8 bits

Depth

一种depth贴图

ARGBHalf

色彩渲染贴图格式,每通道16 bit

Shadowmap

一种本地系统的 shadowmap渲染贴图格式

RGB565

色彩渲染贴图格式

ARGB4444

色彩渲染贴图格式,每通道4 bit

ARGB1555

色彩渲染贴图格式, rgb通道5 bitAlpha通道1 bit

Default

默认色彩渲染贴图格式, Frame Buffer也是这个格式

ARGB2101010

色彩渲染贴图格式。颜色10 bitsalpha 2 bits

DefaultHDR

默认HDR色彩渲染贴图格式,HDR Frame Buffer也是这个格式

ARGBFloat

色彩渲染贴图格式,每通道32 bit浮点值

RGFloat

两种颜色 (RG)渲染贴图格式, 每通道32 bit浮点值

RGHalf

两种颜色 (RG)渲染贴图格式,每通道16 bit浮点值

RFloat

标量(R)渲染贴图格式,32 bit浮点值

RHalf

标量(R)渲染贴图格式,16 bit浮点值

R8

标量(R)渲染贴图格式, 8 bit fixed point.

ARGBInt

四通道(ARGB)渲染贴图格式, 每通道32 bit带符号整数

RGInt

二通道(RG)渲染贴图格式, 每通道32 bit带符号整数

RInt

标量(R)渲染贴图格式,32 bit带符号整数



随时间精制SSAO

在当前帧f,我们计算新的contribution Cf,k为有效的(可视V>0)采样点个数
 
jf(p)是所有已用的采样点的个数。
结合前一帧的信息我们得到AO的求法
 
权重wf(p)是累积的所有采样点的个数。有一个最大值作为阈值。    
理 论上来讲这种方法可以任意的采样,但是在实践中,是不可以的,因为二次投影并不是准确的,而且重建需要双边过滤,随时间每一次二次投影都使这个误差加剧, 这种误差带来显而易见的模糊,而且会随时间越来越模糊。而且,新的采样点逐渐趋于零,先前的采样永远不会消除。所以我们用先前定义的wmax这个阈值来 clamp wf,使旧的contributions随时间逐渐衰减。此处wmax趋于正无穷。
 
博主把每帧的w值存在了y通道中,另外把最终的AO值存在了x通道中,把depth存在了z通道中。

起始索引jf存在通道a中
从像素中心获取索引值,像素中心根据上面求出的纹理坐标来求得texf-1,res x,y是当前帧buffer的分辨率。
 

检测和处理无效的像素

当我们重投影一个fragment,我们需要判断前一帧是否对应当前像素,也就是说我们储存的上一帧的AO是否有效。我们通过下面三种条件来判断是否有效:1.当前fragment无阻塞2.fragment的采样邻域发生变化3.fragment之前在帧的外面。

判断无阻塞

我们用上一帧和当前帧的depth来判断是否无阻塞,当前帧depth值为df,上一帧为df-1。
 
上式可以在一个大场景中产生一个稳定的结果,有很宽的depth范围,对近面不敏感,对远距离十分敏感。这种情况下,我们discard上一帧,把wf-1设为0,然后计算新的AO。

判断采样邻域的变化

判 断无阻塞这一步骤已经避免了大部分时间相干性的缺陷,但也只局限于固定不动的场景。但是SSAO是从邻域像素收集信息的,通过使用空间sampling kernel。所以,在动态场景我们需要考虑当前像素点邻域的动态移动的物体会影响到当前像素点,即使在无阻塞的像素也是这样。
我们在AO中使用采样做两件事一件是计算当前contribution Cf(p)另一件是检测有效性。检测原理如下图
有效的采样点si和像素p通过采样和像素的相关位置的改变被估算:
 
sif-1通过之前存储的偏移向量(offset vectors)和si来算出(上面提到过的的方法)。只使用tangent 面前面的采样。

 

平滑失效处

上面的方法中我们用一个二进制阈值来检测无阻塞,我们通过 检查所有采样来discard上一帧的值。但是,在变形缓慢的表面,AO改变也是很缓慢,此时我们没有必要全部discard掉上一帧的值,用一种新的方法求出改变的程度,如下公式:
计算了一个在0-1范围内的confidence来表示之前的SSAO的有效程度。
 
S控制无效处的光滑程度在15-30范围内,分布如下图示。如果相关距离没变的话δ(x)=0,随着δ(si)增大confidence趋于零。
 
先前的AO中的所有confidence:
 
把它和前面的权重weight(公式1.6)相乘,为了防止闪烁, convergence小于一定阈值下时我们不重用相同的采样,比如conv(p)<0.5(公式1.7)。
过高的S会移除时间相干性的缺陷,但是会产生很多noise。

处理frame-buffer边界

在帧边界处的信息是不正确的,所以我们检测上一帧中的边界处的采样,在边界不使用上面的平滑失效,直接discard掉,在当前帧计算confidence时也不在超出边界处采样。

自适应Convergence-Aware空间滤波。

SSAO通常使用一个空间滤波来减缓由采样率不足引起的noise。TSSAO也使用一种空间滤波,通过的采样点与当前点在世界空间的距离,这种自适应空间滤波能自动考虑到depth差距,并检测到depth 差距很大的不连续的地方:
 

x是采样点,K(p)是标准化因子,g是空间滤波kernel(比如高斯滤波)。


 给出滤波处代码

<span style="font-size:14px;">			for (int i = 0; i < 3; i++)
			{
				for (int j = 0; j < 3; j++)
				{
					uv_sam = uv + float2(-1 + i, -1 + j) / _Size;
					buffer = tex2D(_AoTex, uv_sam);
					conv = buffer.y;
					c = buffer.x;

					pos = GetPosition(uv,0);
					pos_sam = GetPosition(uv_sam,0);
					g = G(distance(pos,pos_sam));

					Kp += g*conv;
					Ao += Kp*c;
				}

			}</span>



采样方式

采样距离不要过近,判断的阻塞很少,在半球的半径或半径的一半这样的距离之间最好。
可以参考以前写过的这篇文章超级采样 Supersampling 方式汇总,此处推荐Halton sequence方法

2D Halton sequence


 
3D Halton sequence
 
x,z分量计算随机方向的单位球,y分量设置采样半径r。
在Game Programming Gems8使用半随机方法semi-random 3D vectors
在法线周围随机方向向量和距离
这里给出代码:

float3 reflect( float3 vSample, float3 vNormal )
{
return normalize( vSample – 2.0f * dot( vSample, vNormal ) * vNormal );
}
float3x3 MakeRotation( float fAngle, float3 vAxis ) 
{
float fS;
float fC;
sincos( fAngle, fS, fC );
float fXX       = vAxis.x * vAxis.x;
float fYY       = vAxis.y * vAxis.y;
float fZZ       = vAxis.z * vAxis.z;
float fXY       = vAxis.x * vAxis.y;
float fYZ       = vAxis.y * vAxis.z;
float fZX       = vAxis.z * vAxis.x;
float fXS       = vAxis.x * fS;
float fYS       = vAxis.y * fS;
float fZS       = vAxis.z * fS;
float fOneC      = 1.0f - fC;
float3x3 result = float3x3( 
fOneC * fXX +  fC, fOneC * fXY + fZS, fOneC * fZX - fYS,
fOneC * fXY - fZS, fOneC * fYY +  fC, fOneC * fYZ + fXS,
fOneC * fZX + fYS, fOneC * fYZ - fXS, fOneC * fZZ +  fC
);
return result;
}
float4 PostProcessSSAO( float3 i_VPOS ) 
{
...
const float c_scalingConstant = 256.0f;
float3 vRandomNormal = ( normalize( tex2D( p_sSSAONoise, vScreenUV *  
p_vSrcImageSize / c_scalingConstant ).xyz * 2.0f 
– 1.0f ) );
float3x3 rotMatrix = MakeRotation( 1.0f, vNormal );
half fAccumBlock = 0.0f;
for ( int i = 0; i < iSampleCount; i++ ) {
float3 vSamplePointDelta = reflect( p_vSSAOSamplePoints[i], 
vRandomNormal );
float fBlock = TestOcclusion( 
vViewPos, 
vSamplePointDelta,
p_fOcclusionRadius,
p_fFullOcclusionThreshold, 
p_fNoOcclusionThreshold, 
p_fOcclusionPower ) ) {
fAccumBlock += fBlock;
}
...
}



结果

博主实现总共分三步:

1.SSAO

2.TSSAO

3.Filter

给出三步的实现结果

上图为没有滤波的SSAO

 

上图为没有滤波的TSSAO

 

上图为有滤波的TSSAO

上图为Diffuse Color 无光照和阴影

 

上图为Diffuse Color  和有滤波的TSSAO

 


上图为Diffuse Color  和有滤波的TSSAO和光照颜色

原图


原图和有滤波的TSSAO

总结

对 于SSAO本身有一个问题就是对过近或过远的物体处理的不好,因为在固定的采样范围下,过远的物体采样范围偏大忽略了远处细节的阻塞,在近处又由于采样范 围偏小,导致判断几乎在同一个位置上、几乎没有阻塞,解决这个问题的方法就是根据远近动态调节采样范围,算是SSAO的一种优化处理。
SSAO本身是一种低分辨率的处理,因为性能的原因采样点过少,采样率也就过少,但是TSSAO有效的解决了这个问题,使得采样点随时间累加,且能很好的处理动态场景,效果比SSAO好很多,而且更加正确,Noise也很少。

本篇到此结束,

顺便求实习工作,哈哈

近期成果:最近弄的一些渲染

email:  wolf_crixus@sina.cn


                                       ------  by wolf96


原文地址:https://www.cnblogs.com/zhanlang96/p/4863328.html