使用 OpenGL 和 GLSL 的 SSAO 算法出现奇怪的性能行为
Strange performance behaviour with SSAO algorithm using OpenGL and GLSL
我正在使用 Oriented-Hemisphere 渲染技术研究 SSAO(屏幕-Space 环境遮挡)算法。
I)算法
此算法需要输入:
- 1 个包含预计算样本的数组(在主循环之前加载 -> 在我的示例中,我使用 64 个样本 根据 z 轴定向。
- 1 个包含归一化旋转矢量的噪声纹理也根据 z 轴定向(此纹理生成一次)。
- 2 个来自 GBuffer 的纹理:'PositionSampler' 和 'NormalSampler' 包含视图中的位置和法向量 space。
这是我使用的片段着色器源代码:
#version 400
/*
** Output color value.
*/
layout (location = 0) out vec4 FragColor;
/*
** Vertex inputs.
*/
in VertexData_VS
{
vec2 TexCoords;
} VertexData_IN;
/*
** Inverse Projection Matrix.
*/
uniform mat4 ProjMatrix;
/*
** GBuffer samplers.
*/
uniform sampler2D PositionSampler;
uniform sampler2D NormalSampler;
/*
** Noise sampler.
*/
uniform sampler2D NoiseSampler;
/*
** Noise texture viewport.
*/
uniform vec2 NoiseTexOffset;
/*
** Ambient light intensity.
*/
uniform vec4 AmbientIntensity;
/*
** SSAO kernel + size.
*/
uniform vec3 SSAOKernel[64];
uniform uint SSAOKernelSize;
uniform float SSAORadius;
/*
** Computes Orientation matrix.
*/
mat3 GetOrientationMatrix(vec3 normal, vec3 rotation)
{
vec3 tangent = normalize(rotation - normal * dot(rotation, normal)); //Graham Schmidt process
vec3 bitangent = cross(normal, tangent);
return (mat3(tangent, bitangent, normal)); //Orientation according to the normal
}
/*
** Fragment shader entry point.
*/
void main(void)
{
float OcclusionFactor = 0.0f;
vec3 gNormal_CS = normalize(texture(
NormalSampler, VertexData_IN.TexCoords).xyz * 2.0f - 1.0f); //Normal vector in view space from GBuffer
vec3 rotationVec = normalize(texture(NoiseSampler,
VertexData_IN.TexCoords * NoiseTexOffset).xyz * 2.0f - 1.0f); //Rotation vector required for Graham Schmidt process
vec3 Origin_VS = texture(PositionSampler, VertexData_IN.TexCoords).xyz; //Origin vertex in view space from GBuffer
mat3 OrientMatrix = GetOrientationMatrix(gNormal_CS, rotationVec);
for (int idx = 0; idx < SSAOKernelSize; idx++) //For each sample (64 iterations)
{
vec4 Sample_VS = vec4(Origin_VS + OrientMatrix * SSAOKernel[idx], 1.0f); //Sample translated in view space
vec4 Sample_HS = ProjMatrix * Sample_VS; //Sample in homogeneus space
vec3 Sample_CS = Sample_HS.xyz /= Sample_HS.w; //Perspective dividing (clip space)
vec2 texOffset = Sample_CS.xy * 0.5f + 0.5f; //Recover sample texture coordinates
vec3 SampleDepth_VS = texture(PositionSampler, texOffset).xyz; //Sample depth in view space
if (Sample_VS.z < SampleDepth_VS.z)
if (length(Sample_VS.xyz - SampleDepth_VS) <= SSAORadius)
OcclusionFactor += 1.0f; //Occlusion accumulation
}
OcclusionFactor = 1.0f - (OcclusionFactor / float(SSAOKernelSize));
FragColor = vec4(OcclusionFactor);
FragColor *= AmbientIntensity;
}
这是结果(没有模糊渲染通道):
到这里为止一切似乎都是正确的。
II) 性能
我注意到 NSight 调试器 一个关于性能的非常奇怪的行为:
如果我将相机越来越靠近龙,性能会受到严重影响。
但是,在我看来,情况应该不是这样,因为SSAO算法适用于Screen-Space并且不依赖于例如龙的图元数量。
这里有 3 个屏幕截图,有 3 个不同的相机位置(在这 3 个情况下,所有 1024*768 像素着色器都使用相同的算法执行):
a) GPU 空闲:40%(受影响的像素:100%)
b) GPU 空闲:25%(受影响的像素:100%)
c) GPU 空闲:2%! (受影响的像素:100%)
我的渲染引擎在我的示例中使用了 exaclly 2 个渲染通道:
- Material Pass(填充 position 和 normal 采样器)
- Ambient pass(填充SSAO纹理)
我认为问题出在这两个过程的执行上,但事实并非如此,因为我在我的客户端代码中添加了一个条件,如果相机是静止的。所以当我拍摄上面的这 3 张图片时,只执行了 Ambient Pass。因此,这种性能不足与 material 传球无关。我可以给你的另一个论点是,如果我删除龙网格(只有飞机的场景),结果是一样的:我的相机越靠近飞机,性能就越差!
对我来说这种行为是不合逻辑的!就像我上面说的,在这 3 种情况下,所有像素着色器都是应用完全相同的像素着色器代码执行的!
现在,如果我直接在片段着色器中更改一小段代码,我会注意到另一个奇怪的行为:
如果我替换行:
FragColor = vec4(OcclusionFactor);
线下:
FragColor = vec4(1.0f, 1.0f, 1.0f, 1.0f);
性能不足消失!
意思是如果SSAO代码执行正确(我在执行过程中尝试打断点检查)并且我没有在OcclusionFactor处使用end 来填充最终输出的颜色,所以不乏表现力!
我想我们可以得出结论,问题不是出在 "FragColor = vec4(OcclusionFactor);" 行之前的着色器代码...我想。
你如何解释这样的行为?
我在客户端代码和片段着色器代码中尝试了很多代码组合,但我找不到解决这个问题的方法!我真的迷路了。
非常感谢您的帮助!
简短的回答是缓存效率。
为了理解这一点,让我们看一下内部循环中的以下几行:
vec4 Sample_VS = vec4(Origin_VS + OrientMatrix * SSAOKernel[idx], 1.0f); //Sample translated in view space
vec4 Sample_HS = ProjMatrix * Sample_VS; //Sample in homogeneus space
vec3 Sample_CS = Sample_HS.xyz /= Sample_HS.w; //Perspective dividing (clip space)
vec2 texOffset = Sample_CS.xy * 0.5f + 0.5f; //Recover sample texture coordinates
vec3 SampleDepth_VS = texture(PositionSampler, texOffset).xyz; //Sample depth in view space
你在这里做的是:
- 翻译原始观点space
- 将其转换为剪辑 space
- 采样纹理
那么这与缓存效率有何对应关系?
缓存在访问相邻像素时运行良好。例如,如果您使用的是高斯模糊,则您只会访问邻居,这些邻居很可能已经加载到缓存中。
假设您的物体现在离得很远。那么clipspace中采样的像素也非常接近原始点->高局部性->良好的缓存性能。
如果相机离你的物体很近,生成的样本点会更远(在剪辑 space 中),你会得到一个随机的内存访问模式。这会大大降低你的表现,尽管你实际上并没有做更多的操作。
编辑:
为了提高性能,您可以从上一次传递的深度缓冲区重建视图 space 位置。
如果您使用 32 位深度缓冲区,将一个样本所需的数据量从 12 字节减少到 4 字节。
位置重构是这样的:
vec4 reconstruct_vs_pos(vec2 tc){
float depth = texture(depthTexture,tc).x;
vec4 p = vec4(tc.x,tc.y,depth,1) * 2.0f + 1.0f; //tranformed to unit cube [-1,1]^3
vec4 p_cs = invProj * p; //invProj: inverse projection matrix (pass this by uniform)
return p_cs / p_cs.w;
}
同时,您可以进行的另一项优化是以缩小的尺寸渲染 SSAO 纹理,最好是主视口尺寸的一半。如果你这样做,一定要将你的深度纹理复制到另一个半尺寸纹理(glBlitFramebuffer)并从中采样你的位置。我希望这会将性能提高一个数量级,尤其是在您给出的最坏情况下。
我正在使用 Oriented-Hemisphere 渲染技术研究 SSAO(屏幕-Space 环境遮挡)算法。
I)算法
此算法需要输入:
- 1 个包含预计算样本的数组(在主循环之前加载 -> 在我的示例中,我使用 64 个样本 根据 z 轴定向。
- 1 个包含归一化旋转矢量的噪声纹理也根据 z 轴定向(此纹理生成一次)。
- 2 个来自 GBuffer 的纹理:'PositionSampler' 和 'NormalSampler' 包含视图中的位置和法向量 space。
这是我使用的片段着色器源代码:
#version 400
/*
** Output color value.
*/
layout (location = 0) out vec4 FragColor;
/*
** Vertex inputs.
*/
in VertexData_VS
{
vec2 TexCoords;
} VertexData_IN;
/*
** Inverse Projection Matrix.
*/
uniform mat4 ProjMatrix;
/*
** GBuffer samplers.
*/
uniform sampler2D PositionSampler;
uniform sampler2D NormalSampler;
/*
** Noise sampler.
*/
uniform sampler2D NoiseSampler;
/*
** Noise texture viewport.
*/
uniform vec2 NoiseTexOffset;
/*
** Ambient light intensity.
*/
uniform vec4 AmbientIntensity;
/*
** SSAO kernel + size.
*/
uniform vec3 SSAOKernel[64];
uniform uint SSAOKernelSize;
uniform float SSAORadius;
/*
** Computes Orientation matrix.
*/
mat3 GetOrientationMatrix(vec3 normal, vec3 rotation)
{
vec3 tangent = normalize(rotation - normal * dot(rotation, normal)); //Graham Schmidt process
vec3 bitangent = cross(normal, tangent);
return (mat3(tangent, bitangent, normal)); //Orientation according to the normal
}
/*
** Fragment shader entry point.
*/
void main(void)
{
float OcclusionFactor = 0.0f;
vec3 gNormal_CS = normalize(texture(
NormalSampler, VertexData_IN.TexCoords).xyz * 2.0f - 1.0f); //Normal vector in view space from GBuffer
vec3 rotationVec = normalize(texture(NoiseSampler,
VertexData_IN.TexCoords * NoiseTexOffset).xyz * 2.0f - 1.0f); //Rotation vector required for Graham Schmidt process
vec3 Origin_VS = texture(PositionSampler, VertexData_IN.TexCoords).xyz; //Origin vertex in view space from GBuffer
mat3 OrientMatrix = GetOrientationMatrix(gNormal_CS, rotationVec);
for (int idx = 0; idx < SSAOKernelSize; idx++) //For each sample (64 iterations)
{
vec4 Sample_VS = vec4(Origin_VS + OrientMatrix * SSAOKernel[idx], 1.0f); //Sample translated in view space
vec4 Sample_HS = ProjMatrix * Sample_VS; //Sample in homogeneus space
vec3 Sample_CS = Sample_HS.xyz /= Sample_HS.w; //Perspective dividing (clip space)
vec2 texOffset = Sample_CS.xy * 0.5f + 0.5f; //Recover sample texture coordinates
vec3 SampleDepth_VS = texture(PositionSampler, texOffset).xyz; //Sample depth in view space
if (Sample_VS.z < SampleDepth_VS.z)
if (length(Sample_VS.xyz - SampleDepth_VS) <= SSAORadius)
OcclusionFactor += 1.0f; //Occlusion accumulation
}
OcclusionFactor = 1.0f - (OcclusionFactor / float(SSAOKernelSize));
FragColor = vec4(OcclusionFactor);
FragColor *= AmbientIntensity;
}
这是结果(没有模糊渲染通道):
到这里为止一切似乎都是正确的。
II) 性能
我注意到 NSight 调试器 一个关于性能的非常奇怪的行为:
如果我将相机越来越靠近龙,性能会受到严重影响。
但是,在我看来,情况应该不是这样,因为SSAO算法适用于Screen-Space并且不依赖于例如龙的图元数量。
这里有 3 个屏幕截图,有 3 个不同的相机位置(在这 3 个情况下,所有 1024*768 像素着色器都使用相同的算法执行):
a) GPU 空闲:40%(受影响的像素:100%)
b) GPU 空闲:25%(受影响的像素:100%)
c) GPU 空闲:2%! (受影响的像素:100%)
我的渲染引擎在我的示例中使用了 exaclly 2 个渲染通道:
- Material Pass(填充 position 和 normal 采样器)
- Ambient pass(填充SSAO纹理)
我认为问题出在这两个过程的执行上,但事实并非如此,因为我在我的客户端代码中添加了一个条件,如果相机是静止的。所以当我拍摄上面的这 3 张图片时,只执行了 Ambient Pass。因此,这种性能不足与 material 传球无关。我可以给你的另一个论点是,如果我删除龙网格(只有飞机的场景),结果是一样的:我的相机越靠近飞机,性能就越差!
对我来说这种行为是不合逻辑的!就像我上面说的,在这 3 种情况下,所有像素着色器都是应用完全相同的像素着色器代码执行的!
现在,如果我直接在片段着色器中更改一小段代码,我会注意到另一个奇怪的行为:
如果我替换行:
FragColor = vec4(OcclusionFactor);
线下:
FragColor = vec4(1.0f, 1.0f, 1.0f, 1.0f);
性能不足消失!
意思是如果SSAO代码执行正确(我在执行过程中尝试打断点检查)并且我没有在OcclusionFactor处使用end 来填充最终输出的颜色,所以不乏表现力!
我想我们可以得出结论,问题不是出在 "FragColor = vec4(OcclusionFactor);" 行之前的着色器代码...我想。
你如何解释这样的行为?
我在客户端代码和片段着色器代码中尝试了很多代码组合,但我找不到解决这个问题的方法!我真的迷路了。
非常感谢您的帮助!
简短的回答是缓存效率。
为了理解这一点,让我们看一下内部循环中的以下几行:
vec4 Sample_VS = vec4(Origin_VS + OrientMatrix * SSAOKernel[idx], 1.0f); //Sample translated in view space
vec4 Sample_HS = ProjMatrix * Sample_VS; //Sample in homogeneus space
vec3 Sample_CS = Sample_HS.xyz /= Sample_HS.w; //Perspective dividing (clip space)
vec2 texOffset = Sample_CS.xy * 0.5f + 0.5f; //Recover sample texture coordinates
vec3 SampleDepth_VS = texture(PositionSampler, texOffset).xyz; //Sample depth in view space
你在这里做的是:
- 翻译原始观点space
- 将其转换为剪辑 space
- 采样纹理
那么这与缓存效率有何对应关系?
缓存在访问相邻像素时运行良好。例如,如果您使用的是高斯模糊,则您只会访问邻居,这些邻居很可能已经加载到缓存中。
假设您的物体现在离得很远。那么clipspace中采样的像素也非常接近原始点->高局部性->良好的缓存性能。
如果相机离你的物体很近,生成的样本点会更远(在剪辑 space 中),你会得到一个随机的内存访问模式。这会大大降低你的表现,尽管你实际上并没有做更多的操作。
编辑:
为了提高性能,您可以从上一次传递的深度缓冲区重建视图 space 位置。
如果您使用 32 位深度缓冲区,将一个样本所需的数据量从 12 字节减少到 4 字节。
位置重构是这样的:
vec4 reconstruct_vs_pos(vec2 tc){
float depth = texture(depthTexture,tc).x;
vec4 p = vec4(tc.x,tc.y,depth,1) * 2.0f + 1.0f; //tranformed to unit cube [-1,1]^3
vec4 p_cs = invProj * p; //invProj: inverse projection matrix (pass this by uniform)
return p_cs / p_cs.w;
}
同时,您可以进行的另一项优化是以缩小的尺寸渲染 SSAO 纹理,最好是主视口尺寸的一半。如果你这样做,一定要将你的深度纹理复制到另一个半尺寸纹理(glBlitFramebuffer)并从中采样你的位置。我希望这会将性能提高一个数量级,尤其是在您给出的最坏情况下。