转自

http://blog.csdn.net/xoyojank/article/details/5734537

by José María Méndez

原文链接: http://www.gamedev.net/reference/programming/features/simpleSSAO/


绪论

全局照明(global illumination, GI)是一个计算机图形学术语, 它指的是所有表面之间相互作用的光照现象(光线来回跳动, 折射, 或者被遮挡), 例如: 渗色(color bleeding), 焦散(caustics), 和阴影. 很多情况下, GI这个术语代表的只是渗色和逼真的环境光照(ambient lighting).

直接照明– 光线直接来自光源– 对于今天的硬件来说已经非常容易计算, 但这对于GI并不成立, 因为我们需要收集场景中每个面的邻近面信息, 这样的复杂度很快就会失控. 不过, 也有一些容易控制的GI近似模拟方式. 当光线在场景中传播和跳动时, 有一些地方是不容易被照到的: 角落, 物体之间紧密的缝隙, 折缝, 等等. 这就导致了这些区域看起来比它们周围要暗一些.

这个现象被称为环境遮蔽(ambient occlusion, AO), 一般用于模拟这种区域变暗的方法是: 对于每个面, 测试它被其它面”阻挡”了多少. 这样的计算比起全局光照来说要快得多, 但大多数现有的AO算法还没法实时地运行.

实时AO在屏幕空间环境遮蔽(Screen Space Ambient Occlusion, SSAO)出现之前一直被认为是达不成的目标. 它的第一次应用是在Crytek的”Crysis”这款游戏中, 之后的很多其它游戏也使用了这项技术. 在这篇文章中, 我会讲解一种简单明了, 但效果又好于传统实现的SSAO方法.

Crysis中的SSAO

准备工作 

最初Crytek的实现是用一个深度缓冲做为输入, 粗暴地进行这样的工作: 对于每个深度缓冲中的像素, 采样周围3D空间中的一些点, 投影回屏幕空间并比较采样点和深度缓冲中相同位置的深度值, 以此判断采样点是在面前(没被遮挡)还是在面后(遇到一个遮挡体). 这样经过对深度缓冲的采样, 平均遮挡体的距离后得出就得到了一个遮闭缓冲. 但是这种方式存在一些问题(如自遮闭, 光环), 之后我会说明.

这里我叙述的算法的所有计算都是在2D空间中进行, 不需要进行投影变换. 它用到了每个像素的位置和法线缓冲, 所以如果你已经使用了延迟渲染的话, 一半的工作已经完成了. 如果没有, 你可以从深度缓冲中重建位置信息, 或者直接把每个像素的位置保存到浮点缓冲中去. 如果你是第一次实现SSAO, 那么我建议后者, 因为在这里我不会讲解如何从深度缓冲中去重建位置信息. 无论是哪种方式, 在接下来的文章中, 我会假设你已经有这两个缓冲可用. 另外, 位置和法线需要是视图空间的.

接下来我们要做的事情就是: 使用位置和法线缓冲生成一个每像素对应一个分量的遮闭缓冲. 怎么使用遮闭信息的决定权在你; 通常的方法是从场景的环境光照中减去它, 但是如果你愿意的话, 也可以用来做一些非真实(NPR, non-photorealistic)渲染效果.


算法

对场景中的任意像素, 可以这么计算它的环境遮闭: 把所有周围的像素当做小球, 计算它们的贡献度之和. 为了简单起见, 我们把所有的小球当成点: 遮挡者仅仅是没有朝向的点, 那么被遮挡者(接受遮闭的像素)只是一个<位置, 法线>对.

因此, 每个遮挡者的遮闭贡献度取决于两个因素:

  • 到被遮挡者的距离“d”.
  • 被遮挡者的法线”N”与两者(遮挡者与被遮挡者)之间向量”V”的夹角.

有了这两个因素, 一个计算遮闭的简单公式就出来了:

Occlusion = max( 0.0, dot( N, V) ) * ( 1.0 / ( 1.0 + d ) ) 

第一项max( 0.0, dot( N,V ) ), 直觉上来说就是位于被遮挡者正上方的的点比其它点的贡献度更大. 第二项的作用是按距离线性衰减效果, 当然你也可以选择使用平方衰减或其它衰减函数, 但凭个人喜好了.

这个算法非常简单: 从当前像素周围采样一些邻近点, 用上面的公式统计出遮闭贡献度. 为了收集遮闭, 我使用45o和90o时旋转的4次采样 (<1,0>,<-1,0>,<0,1>,<0,-1>), 并且使用一张随机法线纹理做镜像.

一些小技巧可以加速计算: 如使用一半大小的位置和法线缓存, 当然如果你愿意的话, 同时也可以对最后的SSAO缓存应用一个双向的模糊以减少采样产生的噪点. 注意这两个技巧是可以应用于任何SSAO算法的.

下面是应用于屏幕矩形的HLSL pixel shader代码:

[cpp] view plain copy print ?
  1. sampler g_buffer_norm;
  2. sampler g_buffer_pos;
  3. sampler g_random;
  4. float random_size;
  5. float g_sample_rad;
  6. float g_intensity;
  7. float g_scale;
  8. float g_bias;
  9. struct PS_INPUT
  10. {
  11. float2 uv : TEXCOORD0;
  12. };
  13. struct PS_OUTPUT
  14. {
  15. float4 color : COLOR0;
  16. };
  17. float3 getPosition(in float2 uv)
  18. {
  19. return tex2D(g_buffer_pos,uv).xyz;
  20. }
  21. float3 getNormal(in float2 uv)
  22. {
  23. return normalize(tex2D(g_buffer_norm, uv).xyz * 2.0f - 1.0f);
  24. }
  25. float2 getRandom(in float2 uv)
  26. {
  27. return normalize(tex2D(g_random, g_screen_size * uv / random_size).xy * 2.0f - 1.0f);
  28. }
  29. float doAmbientOcclusion(in float2 tcoord,in float2 uv, in float3 p, in float3 cnorm)
  30. {
  31. float3 diff = getPosition(tcoord + uv) - p;
  32. const float3 v = normalize(diff);
  33. const float d = length(diff)*g_scale;
  34. return max(0.0,dot(cnorm,v)-g_bias)*(1.0/(1.0+d))*g_intensity;
  35. }
  36. PS_OUTPUT main(PS_INPUT i)
  37. {
  38. PS_OUTPUT o = (PS_OUTPUT)0;
  39. o.color.rgb = 1.0f;
  40. const float2 vec[4] = {float2(1,0),float2(-1,0),
  41. float2(0,1),float2(0,-1)};
  42. float3 p = getPosition(i.uv);
  43. float3 n = getNormal(i.uv);
  44. float2 rand = getRandom(i.uv);
  45. float ao = 0.0f;
  46. float rad = g_sample_rad/p.z;
  47. //**SSAO Calculation**//
  48. int iterations = 4;
  49. for (int j = 0; j < iterations; ++j)
  50. {
  51. float2 coord1 = reflect(vec[j],rand)*rad;
  52. float2 coord2 = float2(coord1.x*0.707 - coord1.y*0.707, coord1.x*0.707 + coord1.y*0.707);
  53. ao += doAmbientOcclusion(i.uv,coord1*0.25, p, n);
  54. ao += doAmbientOcclusion(i.uv,coord2*0.5, p, n);
  55. ao += doAmbientOcclusion(i.uv,coord1*0.75, p, n);
  56. ao += doAmbientOcclusion(i.uv,coord2, p, n);
  57. }
  58. ao/=(float)iterations*4.0;
  59. //**END**//
  60. //Do stuff here with your occlusion value “ao”: modulate ambient lighting, write it to a buffer for later //use, etc.
  61. return o;
  62. }

sampler g_buffer_norm; sampler g_buffer_pos; sampler g_random; float random_size; float g_sample_rad; float g_intensity; float g_scale; float g_bias; struct PS_INPUT { float2 uv : TEXCOORD0; }; struct PS_OUTPUT { float4 color : COLOR0; }; float3 getPosition(in float2 uv) { return tex2D(g_buffer_pos,uv).xyz; } float3 getNormal(in float2 uv) { return normalize(tex2D(g_buffer_norm, uv).xyz * 2.0f - 1.0f); } float2 getRandom(in float2 uv) { return normalize(tex2D(g_random, g_screen_size * uv / random_size).xy * 2.0f - 1.0f); } float doAmbientOcclusion(in float2 tcoord,in float2 uv, in float3 p, in float3 cnorm) { float3 diff = getPosition(tcoord + uv) - p; const float3 v = normalize(diff); const float d = length(diff)*g_scale; return max(0.0,dot(cnorm,v)-g_bias)*(1.0/(1.0+d))*g_intensity; } PS_OUTPUT main(PS_INPUT i) { PS_OUTPUT o = (PS_OUTPUT)0; o.color.rgb = 1.0f; const float2 vec[4] = {float2(1,0),float2(-1,0), float2(0,1),float2(0,-1)}; float3 p = getPosition(i.uv); float3 n = getNormal(i.uv); float2 rand = getRandom(i.uv); float ao = 0.0f; float rad = g_sample_rad/p.z; //**SSAO Calculation**// int iterations = 4; for (int j = 0; j < iterations; ++j) { float2 coord1 = reflect(vec[j],rand)*rad; float2 coord2 = float2(coord1.x*0.707 - coord1.y*0.707, coord1.x*0.707 + coord1.y*0.707); ao += doAmbientOcclusion(i.uv,coord1*0.25, p, n); ao += doAmbientOcclusion(i.uv,coord2*0.5, p, n); ao += doAmbientOcclusion(i.uv,coord1*0.75, p, n); ao += doAmbientOcclusion(i.uv,coord2, p, n); } ao/=(float)iterations*4.0; //**END**// //Do stuff here with your occlusion value “ao”: modulate ambient lighting, write it to a buffer for later //use, etc. return o; }

这个屏幕空间的方案与 “Hardware Accelerated Ambient Occlusion Techniques on GPUs” [1]十分相似, 主要是采样模式和AO函数的不同. 另外也可以理解成“Dynamic Ambient Occlusion and Indirect Lighting” [2]的图像空间版本.

代码中有些值得提下的细节:

  • 半径除以p.z, 按到摄像机的距离进行了缩放. 如果你忽略这个除法, 所有的屏幕上的像素会使用同样的采样半径, 输出的结果就失去了透视感. 
  • 在for循环中, coord1是位于90o的原始采样坐标, coord2是相同的坐标, 只不过旋转了45o. 
  • 随机纹理包含了随机的法线向量, 所以这是你的平均法线贴图. 下面这张是我使用的随机法线纹理: 

它被平铺到整个屏幕, 被每个像素使用下面的纹理坐标采样:

g_screen_size * uv / random_size

“g_screen_size” 包含了屏幕的宽和高(像素单位), “random_size”是随机纹理的大小(我使用的是64x64). 采样出的法线用来镜像for循环中的采样向量, 以此获得每个屏幕像素各不相同的采样模式. (详见参考文献中的“interleaved sampling”)

最后, shader减少到只需要遍历几个遮挡者, 为它们调用我们的AO函数, 累积出最后的结果. 其中共有4个artist变量:

  • g_scale: 缩放遮挡者和被遮挡者之间的距离.
  • g_bias: 控制被遮挡者所受的遮挡圆锥宽度.
  • g_sample_rad: 采样半径.
  • g_intensity: AO强度.

当你调节它们同时观察效果的变化, 可以很直观地达到想要的效果.


结果

a) 直接输出, 1个pass16次采样 b) 直接输出, 1个pass8次采样 c) 只有直接光照d) 直接光照– ao, 2个pass 每pass16次采样.

如你所见, 代码既短小又简单, 结果也没有自遮闭, 只有很微弱的光环. 这两个现象也是使用深度缓冲作为输入的算法的主要问题, 可以从下面的图片中看出来:

自遮闭出现的原因是传统算法是在每个像素周围的球体上采样的, 所以没有被遮挡的平面上至少有一半的采样被标记成”被遮挡”. 这就导致了整体的遮闭效果是偏灰色的. 光环是物体周围出现的白色软边, 因为这些区域自遮闭是没有起作用的. 所认, 避免自遮闭的同时也能减弱光环问题.

这个方法在你移动摄像机时会产生今人惊呀的效果. 如果你对效果的追求高于速度, 可以使用两个或更多不同半径的pass(复制代码中的for循环), 一个用于采集更多的全局AO, 其它的用于消除小裂缝. 在光照或纹理应用之后, 采样产生的瑕疵几乎看不出来, 也正是因为这个原因, 通常你不需要额外的模糊pass.


进阶

上面我已经叙述了一个简单实用的, 非常适合游戏使用的SSAO实现. 但是, 如果能把背离摄像机的面也考虑在内, 这样就可以获得更好的质量. 一般这需要三个缓冲: 两个位置/深度缓冲, 和一个法线缓冲.

不过你也可以用两个缓冲来实现: 把正面和背面的深度分别保存在一个缓冲的红绿分量里, 然后再从每个分量中重建位置. 这样你就可以第一个缓冲用于保存”位置”, 第二个缓冲用于保存法线了.

下面是每个位置缓冲采样16次的结果:

正面遮蔽, 右: 背面遮蔽

实现它只需要在搜索遮挡者时, 在循环中调用“doAmbientOcclusion()”采样背面的位置缓冲. 显然, 背面的贡献度很小, 却使得采样的数目增加了一倍, 几乎把渲染时间变成了原来的两倍. 虽然你可以减小背面的采样, 但这仍然不太实用.

这是需要增加的额外代码:

在循环内部加入下面的调用:

[cpp] view plain copy print ?
  1. ao += doAmbientOcclusionBack(i.uv,coord1*(0.25+0.125), p, n);
  2. ao += doAmbientOcclusionBack(i.uv,coord2*(0.5+0.125), p, n);
  3. ao += doAmbientOcclusionBack(i.uv,coord1*(0.75+0.125), p, n);
  4. ao += doAmbientOcclusionBack(i.uv,coord2*1.125, p, n);

ao += doAmbientOcclusionBack(i.uv,coord1*(0.25+0.125), p, n); ao += doAmbientOcclusionBack(i.uv,coord2*(0.5+0.125), p, n); ao += doAmbientOcclusionBack(i.uv,coord1*(0.75+0.125), p, n); ao += doAmbientOcclusionBack(i.uv,coord2*1.125, p, n);

把这两个函数加入shader:

[cpp] view plain copy print ?
  1. float3 getPositionBack(in float2 uv)
  2. {
  3. return tex2D(g_buffer_posb,uv).xyz;
  4. }
  5. float doAmbientOcclusionBack(in float2 tcoord,in float2 uv, in float3 p, in float3 cnorm)
  6. {
  7. float3 diff = getPositionBack(tcoord + uv) - p;
  8. const float3 v = normalize(diff);
  9. const float d = length(diff)*g_scale;
  10. return max(0.0,dot(cnorm,v)-g_bias)*(1.0/(1.0+d));
  11. }

float3 getPositionBack(in float2 uv) { return tex2D(g_buffer_posb,uv).xyz; } float doAmbientOcclusionBack(in float2 tcoord,in float2 uv, in float3 p, in float3 cnorm) { float3 diff = getPositionBack(tcoord + uv) - p; const float3 v = normalize(diff); const float d = length(diff)*g_scale; return max(0.0,dot(cnorm,v)-g_bias)*(1.0/(1.0+d)); }

增加一个保存了背面位置的sampler “g_buffer_posb”. (开启正面剔除绘制场景来生成它)

另一个可以做的更改(这次我们改进的是速度而不是效果)是在我们的shader中增加一个简单的LOD (level of detail) 系统. 把固定次数的采样改成这样:

int iterations = lerp(6.0,2.0,p.z/g_far_clip);

变量“g_far_clip” 是远裁剪面的距离, 必须做为参数传入shader. 现在每个像素应用的迭代次数取决于到摄像机的距离, 因此远处的像素只进行了粗糙的采样, 这就以不明显的质量下降换来了效率的提高. 不过, 在下面的性能衡量中我没有使用这个技巧.


总结和性能衡量

文章开头我提到过, 这个方法非常适用于延迟光照的游戏, 因为它需要的两个缓冲已经具备了. 它的实现很直接, 质量也不错, 又解决了自遮闭问题并减弱了光环现象. 不过除了这些, 它也跟其它SSAO技术一样有着共同的缺陷:

缺点:

  • 没有把隐藏的几何体考虑在内(特别是视锥体外的).
  • 性能很大程度上决定于采样半径和到摄像机的距离, 因为近裁剪面附近的物体使用的半径从远处的大.
  • 输出有噪点.

权衡一下速度, 可以对16次采样的实现做4x4的高斯模糊, 因为每次采样只采样了一个纹理, 并且AO函数十分简单, 不过实际应用的话还是有点慢. 这里有一个表格显示900x650的包含Hebe模型的场景, 没有模糊的情况下nVidia8800GT下的速度:

设定

FPS

SSAO时间(ms)

高 (32次正/反采样)

150

3.3

中 (16次正采样)

290

0.27

低 (8 次正采样)

310

0.08

最后的这些截图你可以看到这个算法下不同模型的效果. 最高质量 (32 次正反采样, 较大的半径, 3x3 双向模糊):

最低质量(8次正采样, 无模糊, 小半径):

对比一下这项技术和光线追踪的AO也很有用. 比较的目的是看一下这项技术在有多少采样的情况下可以逼近实际的AO.

左: SSAO每像素48次采样(32 正面 16 背面), 没有模糊. 右: Mental Ray中的光线追踪AO. 32 次采样, spread = 2.0, maxdistance = 1.0; falloff = 1.0.

最后的一个建议: 不要想着把shader插入到你的管线中就能自动得到逼真的效果. 尽管这个实现有着很好的性能/质量比, 但SSAO是一项很耗费时间的效果, 你需要精心地调整它来达到尽可能高的性能. 如加减采样次数, 增加一个双向模糊, 改变强度, 等等. 另外, 你需要考虑SSAO是不是适合你. 除非你有很多动态物件在你的场景中, 要不然根本不需要SSAO; 可能light map对你来说已经足够了, 而且可以为静态场景提供更好的质量.

希望你能够从这篇文章中获益. 所有包含在这篇文章中的代码都遵循 MIT license


关于作者

José María Méndez 是一个23岁的计算机工程学生. 他业余写游戏已经有6年, 现在在一家刚起步的Minimal Drama Game Studio公司任首席程序员.


参考资料

[1] Hardware Accelerated Ambient Occlusion Techniques on GPUs
(Perumaal Shanmugam)

[2] Dynamic Ambient Occlusion and Indirect Lighting 
(Michael Bunnell)

[3] Image-Based Proxy Accumulation for Real-Time Soft Global Illumination 
(Peter-Pike Sloan, Naga K. Govindaraju, Derek Nowrouzezahrai, John Snyder)

[4] Interleaved Sampling
(Alexander Keller, Wolfgang Heidrich)

1024x768下渲染Crytek的 Sponza, 175 fps, 有一个方向光.


1024x768同样的场景, 110 fps, 使用 SSAO中级设置: 16次采样, 正面(front faces), 没有模糊. 环境光已经乘了(1.0-AO).

一个简单实用的SSAO实现相关推荐

  1. html+css+小图标,HTML+CSS入门 一个简单实用的CSS loading图标

    本篇教程介绍了HTML+CSS入门 一个简单实用的CSS loading图标,希望阅读本篇文章以后大家有所收获,帮助大家HTML+CSS入门.< 在web开发中,为了提高用户体验,在加载数据的时 ...

  2. 一个简单实用的,基于EF的三层架构

    到底什么样的框架才是好框架呢?或许不同人有不同的看法.我个人觉一个好的框架,最重要的要是简单实用,能快速适开发,可维护性高(不会出现复制黏贴的代码),并能快速响应各种业务场景的变化的框架,同时性能不会 ...

  3. easyeda,一个简单实用的探索性数据分析工具

    在算法工程师的日常工作中,探索性数据分析(Exploratory Data Analysis)是一种常见的任务.通过分析数据的缺失情况,分布情况,以及和标签的相关性等,数据EDA可以帮助算法工程师评估 ...

  4. WinForm_2一个简单实用的小应用——桌面时钟

    对于WinForm编程呢,我准备采用编写不同的小应用或小游戏这种方式--因此,可能每一个程序会包揽相对较多的东西,毕竟WinForm编程还是挺简单的^_^ 今天先来做一个挺实用的小应用--桌面时钟,也 ...

  5. 【tkinter】的使用详解,做一个简单实用的万能可视化界面!

    文章目录 一.tkinter类介绍 二.在例子中学会用法! 1.一个简单的开始 2.学会使用button 3.Entry窗口部件 1.简单使用 2.动态改变entry内部的值 4.Text窗口部件 ( ...

  6. Kotlin自定义一个简单实用的标题栏

    标题栏是每个APP必不可少的一部分,通过它我们可以实现导航以及添加一些操作事件等等.下面分享一个我常用的标题栏控件. 先来说一下大概的思路吧,考虑到标题栏一般具有比较多的控件,例如左右的图标按钮或者文 ...

  7. 基于gulp编写的一个简单实用的前端开发环境

    自从Node.js出现以来,基于其的前端开发的工具框架也越来越多了,从Grunt到Gulp再到现在很火的WebPack,所有的这些新的东西的出现都极大的解放了我们在前端领域的开发,作为一个在前端领域里 ...

  8. python简易图形-python图形用户界面(四):教你实现一个简单实用的计时器

    前言 本系列课程是针对无基础的,争取用简单明了的语言来讲解,学习前需要具备基本的电脑操作能力,准备一个已安装python环境的电脑.如果觉得好可以分享转发,有问题的地方也欢迎指出,在此先行谢过. 今天 ...

  9. 封装一个简单实用的朋友圈

    前言 2014年过的那么快,过年又那么块,2015年又是飞快地节奏,真尼玛感觉上帝是不是无聊使用了变速外挂开启了加速模式~到现在博主都无法接受已 经上班的事实--在地铁脸被挤在玻璃上的时候只能用眼神写 ...

最新文章

  1. ES5新增的方法——数组的方法
  2. android app自动更新界面_Android自定义view之模仿登录界面文本输入框(华为云APP)...
  3. tensorboard的可视化及模型可视化
  4. 汇编语言(二十六)之自然数求和
  5. 使用Sqlmap对dvwa进行sql注入测试(初级阶段)
  6. Flask的状态保持和上下文管理
  7. 人魔比妖都恶的时代...
  8. AngularJS基础入门初探
  9. [Ext JS6实战] Ajax获取Tree Store
  10. 论文阅读|How Does Batch Normalizetion Help Optimization
  11. 数据结构与算法学习笔记
  12. diabetes影响因子2017_2017年SCI影响因子发布,几家欢喜几家愁
  13. adb 切换默认桌面_android tv 模拟器默认桌面修改 Alternate Launcher开机自动启动app...
  14. 推荐一个好看且实用的火狐浏览器新标签页插件【火狐浏览器新标签页自定义美化】
  15. Cisco Packet Tracer教程
  16. 【zigbee无线通信模块步步详解】ZigBee3.0模块建立远程网络控制方法
  17. $.closest()
  18. 计算机控制系统设计题例题,计算机控制系统练习题..doc
  19. 【转】Coherence Step by Step
  20. uniCloud更新APP

热门文章

  1. Activity 生命周期 A打开B界面生命周期变化
  2. DM达梦数据库用户管理
  3. Godaddy教程:支付宝/信用卡续费域名(图文)
  4. 俄罗斯成功断开全球互联网背后,电力物联网已成为大国斗争“新战场”!
  5. centos 6.5 p2v virt-p2v过程详解之一
  6. ServletPath()与ContextPath()的区别
  7. 2021年度工作总结
  8. python中dns库用法详解(DNS处理模块)
  9. 用mediapipe定位人脸轮廓
  10. Could not fetch URL https://pypi.python.org/simple/: connection error: HTTPSConnectionPool(host='pyp