您的位置:首页 > 移动开发 > Unity3D

Unity游戏图形渲染效果系列之SSAO篇

2018-01-19 21:56 309 查看
导言

在传统的MMO中,最常用的提升视觉显示效果的就是环境光遮蔽AO,传统的AO实现方法使用了光线追踪,路径追踪等方法,但是实时渲染效率下,所以现在常用的是屏幕空间环境光遮蔽SSAO,SSAO的效果表现出会让被遮挡的物体显示变暗,没有遮挡的地方显示更加明亮一些,使得整个视觉空间更有层次感。

章节

一.SSAO的视觉表现






如图左边为正常渲染的原图,中间加了SSAO的效果,可以看出石头后边的墙壁明显要比墙壁其他地方要稍微暗一点,右边的就是SSAO渲染出的结果。

二.SSAO的两种实现方式和原理

1.基于法线的半球积分

这是最常用也是效果最好的SSAO方法,在传统MMO中大多采用该方法, 对于场景中的任意像素计算它的环境遮蔽。计算方法如下: 

   假设需要计算的被遮挡像素为P,把所有周围的像素当做小球,计算小球对P的遮挡值的总和,小球的半径足够小,可以看做点。单个遮挡点对P的遮挡值主要取决于两个因素:    
1. 遮挡点到P的距离,假设为d;   
        2. P到遮挡点的向量V与P的法线N之间的夹角。    
根据这两个因素得出一个简单的计算公式:


  
    这个公式很好理解,如图看上方的凸点A,该点周围没有遮挡点,所以周围的采样点都在法向量反方向,比如B,那么AB向量在法线方向的投影(也就是cos值)为负数,说明该点不被遮挡,当和0取max后 就可以过滤这些点,也就是不被遮挡的点的AO值通常为0,。OK 这个时候来看该图下方的凹点,A被两面墙挡住了,那么周围的采样点都类似B点,B在法线方向的投影(cos值)与法相方向同向为正,我们都知道cos值最大值是1,所以该点的被遮挡系数AO值最大为1,所以通常情况下的AO值是为0-1之间的一个遮挡系数。
上述算法比较简单,单纯的公式也不复杂,具体复杂度和效果体现在采样点的选取和参数选取上面,实现时只要在P周围采样一些点,得到这些点的位置,算出点到P的距离和向量,结合P的法线,运用上面公式计算出遮蔽值。这里需要P的位置、P的法线和点的位置,法线可以通过生成法线图得到,位置信息要复杂一点,需要通过深度图获得。    如何通过深度图获得位置信息不是本文的重点,这里大概描述一下方法。根据屏幕空间的纹理坐标可以算出view空间内穿过该屏幕上点的一条射线,即拾取(Pick)的计算方法,结合深度值就可以算出点的位置。
另外关于后边的d公式衰减系数,也可以替换为其他衰减系数公式。
该公式是基于正确的公式计算,也能得到较好的效果,通常用在PC端的MMO游戏,但采样点会比较多效果才能完全体现,但是通常在手机用该方法消耗比较大,而且由于max(0,ao)的操作导致可能至少一半的采样点对AO值计算没有贡献,所以手机上低端SSAO可以采样基于视线方向的半球积分。

2.基于视线方向的球积分

由于法线方向的积分,一半的采样点被浪费,当采样次数不够的时候不能形成很好的效果,为了充分利用每次采样点对AO值贡献的计算,采用如下公式:



如图所以,公式很简单,理解起来就是去掉了max(o,ao)的操作,使得所有的采样点都参与了对AO贡献度的计算,如图可知B点对AV方向的投影(cos值)也就是贡献值为正数,C点对AV方向的投影为负,当采样到足够多的点后进行平均就能的到该点的AO值,至于公式采用了基于视角方向的积分是为了更多情况下采样点都能满足对该点AO贡献度的计算。根据该公式计算cos值得范围通常是-1,1之间,通常我们可以直接映射转换到0-1之间进行参与计算。

两种方式都理解后,下面我们来分别实现两种方式的SSAO算法,以及其效果对比,还有算法中包括选取点灯常规需要注意的地方。

三.SSAO的两种实现方式和效果对比

实现:

1.采样点的选取,如下采样点使用了自定义CROSS[]数组,然后在上下左右方向一定距离处选取采样点,当需要更高质量的时候可用噪声图对采样点方向进行随机采样,得到的效果会更好,当然也可以自定义随机采样点矩阵或者特定的点云模型矩阵参与采样。

2.根据不同的需求给SSAO采样点分级,比如要好点的效果可以采样32次以上,要性能高点可以采样8次就够了.

3.之后计算SSAO值,下面也给了两种计算SSAO的方法,但是得到的Ao贡献值不同方法参与计算要进行不同映射。

4.对于有较高要求的SSAO给结果进行模糊处理 降噪 会使明暗程度更加平滑。

inline half calcAO(half2 tcoord, half2 uvoffset, half3 p, half3 norm,half3 viewdir)
{
half2 t = tcoord + uvoffset;//采样点是UV坐标周围的点 对应的world空间点
float2 XY_Depth = float2(1.0f,0.003921568627451);
float depth = dot(tex2D(_Depth, t).xy, XY_Depth);
float3 diff = float3(t*2-1, 1)*_FarCorner*depth-p;//viewpos offset
half3 v = normalize(diff);
half d = length(diff) *0.11;// _Params1.w/100;//distance

return dot(viewdir, v);

//return max(0.0, dot(norm, v))* (1.0 / (1.0 + d)) ;
//半球积分
}
float4 frag( v2f IN) : COLOR
{

float2   uv=   IN.uv;
float2 XY_Depth = float2(1.0f,0.003921568627451);
float2 sampleuv = uv;
float4 depth_normal = tex2D(_Depth,sampleuv);
float view_depth = dot(depth_normal.xy,XY_Depth);//
float3 normal = DecodeNormal(depth_normal.zw);
float3 view_pos = IN.viewpos*view_depth;
float3 viewdir = normalize(view_pos);

const half2 CROSS[4] = { half2(1.0, 0.0), half2(-1.0, 0.0), half2(0.0, 1.0), half2(0.0, -1.0) };
half eyeDepth =view_depth;// LinearEyeDepth(depth);
half3 position =view_pos;// getWSPosition(uv, depth); // World space
#if defined(SAMPLE_NOISE)
half2 random = normalize(tex2D(_Sample2x2, _ScreenParams.xy * uv / _Params1.x).rg * 2.0 - 1.0);
#endif

half radius =max(_Params1.y/100, 0.005);
//	if(view_pos.z>30)
//		return view_depth;// Skip out of range pixels!!!!!!!!!!!!!!!!!!!!!!!!
half ao = 0;

// Sampling
for (int j = 0; j < 4; j++)
{
half2 coord1;

#if defined(SAMPLE_NOISE)
coord1 = reflect(CROSS[j], random) * radius;//this random important
#else
coord1 = CROSS[j] * radius;
#endif

#if !SAMPLES_VERY_LOW
half2 coord2 = coord1 * 0.707;
coord2 = half2(coord2.x - coord2.y, coord2.x + coord2.y);
#endif

#if SAMPLES_Num==20			// 20
ao += calcAO(uv, coord1 * 0.20, position, normal,viewdir);
ao += calcAO(uv, coord2 * 0.40, position, normal,viewdir);
ao += calcAO(uv, coord1 * 0.60, position, normal,viewdir);
ao += calcAO(uv, coord2 * 0.80, position, normal,viewdir);
ao += calcAO(uv, coord1, position, normal,viewdir);
#elif SAMPLES_Num==16			// 16
ao += calcAO(uv, coord1 * 0.25, position, normal,viewdir);
ao += calcAO(uv, coord2 * 0.50, position, normal,viewdir);
ao += calcAO(uv, coord1 * 0.75, position, normal,viewdir);
ao += calcAO(uv, coord2, position, normal,viewdir);
#elif SAMPLES_Num==12		// 12
ao += calcAO(uv, coord1 * 0.30, position, normal,viewdir);
ao += calcAO(uv, coord2 * 0.60, position, normal,viewdir);
ao += calcAO(uv, coord1 * 0.90, position, normal,viewdir);
#elif SAMPLES_Num==8			// 8
ao += calcAO(uv, coord1 * 0.30, position, normal,viewdir);
ao += calcAO(uv, coord2 * 0.80, position, normal,viewdir);
#elif SAMPLES_Num==4		// 4
ao += calcAO(uv, coord1 * 0.50, position, normal,viewdir);
#endif

}
ao /= SAMPLES_Num;
float3 ret =0.5+ao;
//float3 ret =1-ao;
if (_Debug==1)
return  half4(ret.xyz, 1);

float4 diff = tex2D(_Diffuse, sampleuv);
ret*= diff.xyz*_AmbientColor;
float3 WorldN= mul((float3x3)ViewToWorld, normal);
float3 envDiff = texCUBE(_SkyTexture,WorldN);
ret *= envDiff*envDiff;

return  half4(ret.xyz, 1);
}
下面来看不同SSAO算法,以及不同采样:































总结:从上面的渲染图可以得到一些结论

1.正常情况下,采样次数越越多效果越好

2.在较低次数采样下基于View方向的球积分效果表现更好,在较高次数采样下基于normal方向的半球积分表现效果更好。总的来说基于normal方向的计算效果更佳逼真。

本章讲了SSAO的基本算法, 对于算法中常用的坐标和方向转换不明白的地方,可以好好看看基础,另外本章也提到了采样将SSAO值模糊处理进行平滑得到更好的效果,那么下一张讲最常用的模糊处理吧,模糊处理用在很多地方,比如高斯模糊,Dof,运动模糊,径向模糊,FXAA等等,虽然功能和算法都不一样,但是模糊的思想是互通的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: