一个简单的SSAO

2023-12-27 17:10
文章标签 简单 ssao

本文主要是介绍一个简单的SSAO,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!



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
  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.    
  40.  o.color.rgb = 1.0f;  
  41.  const float2 vec[4] = {float2(1,0),float2(-1,0),  
  42.             float2(0,1),float2(0,-1)};  
  43.  float3 p = getPosition(i.uv);  
  44.  float3 n = getNormal(i.uv);  
  45.  float2 rand = getRandom(i.uv);  
  46.  float ao = 0.0f;  
  47.  float rad = g_sample_rad/p.z;  
  48.  //**SSAO Calculation**//   
  49.  int iterations = 4;  
  50.  for (int j = 0; j < iterations; ++j)  
  51.  {  
  52.   float2 coord1 = reflect(vec[j],rand)*rad;  
  53.   float2 coord2 = float2(coord1.x*0.707 - coord1.y*0.707, coord1.x*0.707 + coord1.y*0.707);  
  54.     
  55.   ao += doAmbientOcclusion(i.uv,coord1*0.25, p, n);  
  56.   ao += doAmbientOcclusion(i.uv,coord2*0.5, p, n);  
  57.   ao += doAmbientOcclusion(i.uv,coord1*0.75, p, n);  
  58.   ao += doAmbientOcclusion(i.uv,coord2, p, n);  
  59.  }   
  60.  ao/=(float)iterations*4.0;  
  61.  //**END**//   
  62. //Do stuff here with your occlusion value “ao”: modulate ambient lighting, write it to a buffer for later //use, etc.   
  63.  return o;  
  64. }  
[cpp]  view plain copy
  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.    
  40.  o.color.rgb = 1.0f;  
  41.  const float2 vec[4] = {float2(1,0),float2(-1,0),  
  42.             float2(0,1),float2(0,-1)};  
  43.  float3 p = getPosition(i.uv);  
  44.  float3 n = getNormal(i.uv);  
  45.  float2 rand = getRandom(i.uv);  
  46.  float ao = 0.0f;  
  47.  float rad = g_sample_rad/p.z;  
  48.  //**SSAO Calculation**//  
  49.  int iterations = 4;  
  50.  for (int j = 0; j < iterations; ++j)  
  51.  {  
  52.   float2 coord1 = reflect(vec[j],rand)*rad;  
  53.   float2 coord2 = float2(coord1.x*0.707 - coord1.y*0.707, coord1.x*0.707 + coord1.y*0.707);  
  54.     
  55.   ao += doAmbientOcclusion(i.uv,coord1*0.25, p, n);  
  56.   ao += doAmbientOcclusion(i.uv,coord2*0.5, p, n);  
  57.   ao += doAmbientOcclusion(i.uv,coord1*0.75, p, n);  
  58.   ao += doAmbientOcclusion(i.uv,coord2, p, n);  
  59.  }   
  60.  ao/=(float)iterations*4.0;  
  61.  //**END**//  
  62. //Do stuff here with your occlusion value “ao”: modulate ambient lighting, write it to a buffer for later //use, etc.  
  63.  return o;  
  64. }  
 

 

这个屏幕空间的方案与 “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
  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);  
[cpp]  view plain copy
  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);  
 

把这两个函数加入shader

[cpp]  view plain copy
  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. }  
[cpp]  view plain copy
  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. }  
 

增加一个保存了背面位置的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). 

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
  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.    
  40.  o.color.rgb = 1.0f;  
  41.  const float2 vec[4] = {float2(1,0),float2(-1,0),  
  42.             float2(0,1),float2(0,-1)};  
  43.  float3 p = getPosition(i.uv);  
  44.  float3 n = getNormal(i.uv);  
  45.  float2 rand = getRandom(i.uv);  
  46.  float ao = 0.0f;  
  47.  float rad = g_sample_rad/p.z;  
  48.  //**SSAO Calculation**//   
  49.  int iterations = 4;  
  50.  for (int j = 0; j < iterations; ++j)  
  51.  {  
  52.   float2 coord1 = reflect(vec[j],rand)*rad;  
  53.   float2 coord2 = float2(coord1.x*0.707 - coord1.y*0.707, coord1.x*0.707 + coord1.y*0.707);  
  54.     
  55.   ao += doAmbientOcclusion(i.uv,coord1*0.25, p, n);  
  56.   ao += doAmbientOcclusion(i.uv,coord2*0.5, p, n);  
  57.   ao += doAmbientOcclusion(i.uv,coord1*0.75, p, n);  
  58.   ao += doAmbientOcclusion(i.uv,coord2, p, n);  
  59.  }   
  60.  ao/=(float)iterations*4.0;  
  61.  //**END**//   
  62. //Do stuff here with your occlusion value “ao”: modulate ambient lighting, write it to a buffer for later //use, etc.   
  63.  return o;  
  64. }  
[cpp]  view plain copy
  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.    
  40.  o.color.rgb = 1.0f;  
  41.  const float2 vec[4] = {float2(1,0),float2(-1,0),  
  42.             float2(0,1),float2(0,-1)};  
  43.  float3 p = getPosition(i.uv);  
  44.  float3 n = getNormal(i.uv);  
  45.  float2 rand = getRandom(i.uv);  
  46.  float ao = 0.0f;  
  47.  float rad = g_sample_rad/p.z;  
  48.  //**SSAO Calculation**//  
  49.  int iterations = 4;  
  50.  for (int j = 0; j < iterations; ++j)  
  51.  {  
  52.   float2 coord1 = reflect(vec[j],rand)*rad;  
  53.   float2 coord2 = float2(coord1.x*0.707 - coord1.y*0.707, coord1.x*0.707 + coord1.y*0.707);  
  54.     
  55.   ao += doAmbientOcclusion(i.uv,coord1*0.25, p, n);  
  56.   ao += doAmbientOcclusion(i.uv,coord2*0.5, p, n);  
  57.   ao += doAmbientOcclusion(i.uv,coord1*0.75, p, n);  
  58.   ao += doAmbientOcclusion(i.uv,coord2, p, n);  
  59.  }   
  60.  ao/=(float)iterations*4.0;  
  61.  //**END**//  
  62. //Do stuff here with your occlusion value “ao”: modulate ambient lighting, write it to a buffer for later //use, etc.  
  63.  return o;  
  64. }  
 

 

这个屏幕空间的方案与 “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
  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);  
[cpp]  view plain copy
  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);  
 

把这两个函数加入shader

[cpp]  view plain copy
  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. }  
[cpp]  view plain copy
  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. }  
 

增加一个保存了背面位置的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的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/543833

相关文章

csu 1446 Problem J Modified LCS (扩展欧几里得算法的简单应用)

这是一道扩展欧几里得算法的简单应用题,这题是在湖南多校训练赛中队友ac的一道题,在比赛之后请教了队友,然后自己把它a掉 这也是自己独自做扩展欧几里得算法的题目 题意:把题意转变下就变成了:求d1*x - d2*y = f2 - f1的解,很明显用exgcd来解 下面介绍一下exgcd的一些知识点:求ax + by = c的解 一、首先求ax + by = gcd(a,b)的解 这个

hdu2289(简单二分)

虽说是简单二分,但是我还是wa死了  题意:已知圆台的体积,求高度 首先要知道圆台体积怎么求:设上下底的半径分别为r1,r2,高为h,V = PI*(r1*r1+r1*r2+r2*r2)*h/3 然后以h进行二分 代码如下: #include<iostream>#include<algorithm>#include<cstring>#include<stack>#includ

usaco 1.3 Prime Cryptarithm(简单哈希表暴搜剪枝)

思路: 1. 用一个 hash[ ] 数组存放输入的数字,令 hash[ tmp ]=1 。 2. 一个自定义函数 check( ) ,检查各位是否为输入的数字。 3. 暴搜。第一行数从 100到999,第二行数从 10到99。 4. 剪枝。 代码: /*ID: who jayLANG: C++TASK: crypt1*/#include<stdio.h>bool h

uva 10387 Billiard(简单几何)

题意是一个球从矩形的中点出发,告诉你小球与矩形两条边的碰撞次数与小球回到原点的时间,求小球出发时的角度和小球的速度。 简单的几何问题,小球每与竖边碰撞一次,向右扩展一个相同的矩形;每与横边碰撞一次,向上扩展一个相同的矩形。 可以发现,扩展矩形的路径和在当前矩形中的每一段路径相同,当小球回到出发点时,一条直线的路径刚好经过最后一个扩展矩形的中心点。 最后扩展的路径和横边竖边恰好组成一个直

poj 1113 凸包+简单几何计算

题意: 给N个平面上的点,现在要在离点外L米处建城墙,使得城墙把所有点都包含进去且城墙的长度最短。 解析: 韬哥出的某次训练赛上A出的第一道计算几何,算是大水题吧。 用convexhull算法把凸包求出来,然后加加减减就A了。 计算见下图: 好久没玩画图了啊好开心。 代码: #include <iostream>#include <cstdio>#inclu

uva 10130 简单背包

题意: 背包和 代码: #include <iostream>#include <cstdio>#include <cstdlib>#include <algorithm>#include <cstring>#include <cmath>#include <stack>#include <vector>#include <queue>#include <map>

JAVA用最简单的方法来构建一个高可用的服务端,提升系统可用性

一、什么是提升系统的高可用性 JAVA服务端,顾名思义就是23体验网为用户提供服务的。停工时间,就是不能向用户提供服务的时间。高可用,就是系统具有高度可用性,尽量减少停工时间。如何用最简单的方法来搭建一个高效率可用的服务端JAVA呢? 停工的原因一般有: 服务器故障。例如服务器宕机,服务器网络出现问题,机房或者机架出现问题等;访问量急剧上升,导致服务器压力过大导致访问量急剧上升的原因;时间和

简单的角色响应鼠标而移动

actor类 //处理移动距离,核心是找到角色坐标在世界坐标的向量的投影(x,y,z),然后在世界坐标中合成,此CC是在地面行走,所以Y轴投影始终置为0; using UnityEngine; using System.Collections; public class actor : MonoBehaviour { public float speed=0.1f; CharacterCo

docker-compose安装和简单使用

本文介绍docker-compose的安装和使用 新版docker已经默认安装了docker-compose 可以使用docker-compose -v 查看docker-compose版本 如果没有的话可以使用以下命令直接安装 sudo curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-c

JavaFX环境的搭建和一个简单的例子

之前在网上搜了很多与javaFX相关的资料,都说要在Eclepse上要安装sdk插件什么的,反正就是乱七八糟的一大片,最后还是没搞成功,所以我在这里写下我搭建javaFX成功的环境给大家做一个参考吧。希望能帮助到你们! 1.首先要保证你的jdk版本能够支持JavaFX的开发,jdk-7u25版本以上的都能支持,最好安装jdk8吧,因为jdk8对支持JavaFX有新的特性了,比如:3D等;