Opengl SSAO

本文探讨

主要是探讨,如何在法线半球上进行采样。

  1. 首先我们还是沿用延迟渲染的方式存储需要的信息。
  2. 然后我们根据法线的gbuffer(世界坐标)创建一个shaderPoint的切线空间,这个切线空间是随机生成的(通过随机生成tangent向量)然后在随机生成的切线空间中随机生成点。这里的生成切线空间的纹理是4*4的。利用gl_repeat来进行重复性使用随机生成切线空间的纹理
unsigned int noiseTexture; 
glGenTextures(1, &noiseTexture);
glBindTexture(GL_TEXTURE_2D, noiseTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, 4, 4, 0, GL_RGB, GL_FLOAT, &ssaoNoise[0]);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);  
#version 330 core
out float FragColor;
  
in vec2 TexCoords;

uniform sampler2D gPosition;
uniform sampler2D gNormal;
uniform sampler2D texNoise;

uniform vec3 samples[64];
uniform mat4 projection;

// tile noise texture over screen, based on screen dimensions divided by noise size
const vec2 noiseScale = vec2(800.0/4.0, 600.0/4.0); // screen = 800x600

void main()
{
    [...]
}
  1. 根据纹理(gbuffer生成的)生成随机的切线空间和采样点
vec3 fragPos   = texture(gPosition, TexCoords).xyz;
vec3 normal    = texture(gNormal, TexCoords).rgb;
vec3 randomVec = texture(texNoise, TexCoords * noiseScale).xyz; 
vec3 tangent   = normalize(randomVec - normal * dot(randomVec, normal));
vec3 bitangent = cross(normal, tangent);
mat3 TBN       = mat3(tangent, bitangent, normal);  
float occlusion = 0.0;
for(int i = 0; i < kernelSize; ++i)
{
    // get sample position
    vec3 samplePos = TBN * samples[i]; // from tangent to view-space
    samplePos = fragPos + samplePos * radius; 
    
    [...]
}  

这里 kernelSize 和 radius 是我们可以用来调整效果的变量;在本例中,值分别为 64 和 0.5。对于每次迭代,我们首先将相应的样本转换为视图空间。然后,我们将偏移样本乘以半径,以增加(或减少)SSAO 的有效样本半径。

  1. 然后我们需要根据在半球内的采样值转换到透视之后的空间中,与zbuffer中的深度值比较(这里的深度值就是在初次渲染场景的时候,每个片段的得到的gposition.z的深度值)
vec4 offset = vec4(samplePos, 1.0);
offset      = projection * offset;    // from view to clip-space
offset.xyz /= offset.w;               // perspective divide
offset.xyz  = offset.xyz * 0.5 + 0.5; // transform to range 0.0 - 1.0 
float sampleDepth = texture(gPosition, offset.xy).z; 
occlusion += (sampleDepth >= samplePos.z + bias ? 1.0 : 0.0);  

这里有一个很大的误区,就是在片段着色器中,我们认为1.0代表无限远,但是实际上这是映射后的结果,也就是zbuffer中的结果。

但是gPosition存储的是实际的世界坐标,也就是说这里的depth是越小越远(因为z在opengl中是向外的)

所以当采样点的z比深度图上的值小的时候,证明被遮挡了。

  1. 然后就是计算可见度
occlusion = 1.0 - (occlusion / kernelSize);
FragColor = occlusion; 

这样就计算出,该点的环境光可见度,也就是从该点射出的射线有多少可以被view接收

初步结果

  1. 生成半球随机采样点
//samples
int kernelSize = 64;
float radius = 0.5f;
std::vector<glm::vec3> samples;
for(int i = 0; i < kernelSize; i++)
{
    glm::vec3 samplePoint = glm::vec3(
        randNumber(engine) * 2.0f - 1.0f,
        randNumber(engine) * 2.0f - 1.0f,
        randNumber(engine)
    );
    samplePoint = glm::normalize(samplePoint);
    //more random not only on the face
    samplePoint *= randNumber(engine);
    //then we want the samplepoint get more nums close the shader point
    //so we need to scale the samplePoint
    //but we not want all of the point inside a range
    auto ourlerp = [](float a, float b, float t){ return a + (b - a) * t; };
    float scale = (float) i / (float) kernelSize;
    scale = ourlerp(0.0, 1.0, scale * scale);
    samplePoint *= scale;

    //now the samplepoint has been more distribute,however, more points close the shader point
    samples.push_back(samplePoint);
  1. 生成法线空间的随机T向量

我个人感觉这不是传统的切线空间,因为这里我设置的是以物体本来的总体法线求切线空间,然后求法线贴图的法线转移到世界坐标中。这里我认为用以shader point的法线为z轴的局部坐标系,我认为更加贴切

//random way
std::default_random_engine engine;
std::uniform_real_distribution<float> randNumber(0.0f, 1.0f);
//texNoise
std::vector<glm::vec3> texNoise;
for(int i = 0; i < 16; i++)
{
    glm::vec3 noise(randNumber(engine) * 2.0f - 1.0f, 
                        randNumber(engine) * 2.0f - 1.0f,
                        0.0f);
    texNoise.emplace_back(noise);  
}
//create Tex
uint32_t texNoiseTex;
{
    glGenTextures(1, &texNoiseTex);
    glBindTexture(GL_TEXTURE_2D, texNoiseTex);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, 4, 4, 0, GL_RGB, GL_FLOAT, &texNoise[0]);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glBindTexture(GL_TEXTURE_2D, 0);
}
  1. 然后就是shader通过采样得到环境光的visibility
#version 420 core

layout(location = 0) out float FragColor;
layout(location = 1) out vec3 debugColor;
in vec2 vTexCoord;

uniform sampler2D gPosition;
uniform sampler2D gNormal;
uniform sampler2D texNoise;

uniform vec3 samples[64];

uniform mat4 view;
uniform mat4 perspective;

uniform float ScreenWidth;
uniform float ScreenHeight;

uniform int kernelSize;
uniform float radius;
uniform float bias;

void main()
{
    vec2 noiseScale = vec2(ScreenWidth / 4.0, ScreenHeight / 4.0);
    vec2 texCoords = vTexCoord * noiseScale;

    //get information
    vec3 texRandomVec = normalize(texture(texNoise, texCoords).xyz);
    vec3 FragPos = texture(gPosition, vTexCoord).xyz;
    vec3 Normal = normalize(texture(gNormal, vTexCoord).xyz);
    
    //create TBN
    vec3 T = normalize(texRandomVec - dot(texRandomVec, Normal) * Normal);
    vec3 B = cross(Normal, T);
    mat3 TBN = mat3(T, B, Normal);

    //base the local coordinate normal system to sample point then get the world coordinate by TBN
    //calculate the occulusion
    float occulusion = 0.0;
    for(int i = 0; i < kernelSize; i++)
    {
        vec3 samplePoint = TBN * samples[i];
        samplePoint = FragPos + samplePoint * radius;
        vec4 offset = perspective * view * vec4(samplePoint, 1.0);
        offset /= offset.w;
        vec2 offsetuv = offset.xy * 0.5 + 0.5;
        float sampleDepth = texture(gPosition, offsetuv).z;

        occulusion += (sampleDepth >= samplePoint.z + bias ? 1.0 : 0.0);
    }
    FragColor = 1.0 - (occulusion / kernelSize);
    debugColor = texture(texNoise, texCoords).xyz;
}

初步的效果:

优化

每当测试靠近表面边缘对齐的片段的环境光遮蔽时,它还会考虑远离测试表面后面的表面的深度值;这些值将(错误地)影响 Occlusion Factor。我们可以通过引入范围检查来解决这个问题。下图可以看到,和尚的头部,不应该有如此黑边,因为理论上他不是和物体的接触处,不应该具有如此强烈的层次感,其它与后面环境接触的人体部分同理。

造成这种现象的原因是,当处理该片段的时候,该片段的gposition落到后面的背景上,那么采样的深度很大部分在人体上,导致可见度很低

于是我们需要去降低远端物体的影响,就是它不能给1.0的因子。可以思考一个问题,ssao是将无限远的地方当作间接光照,那么当你的深度和我的shader point深度差值太大,我就不应该考虑你对我的影响,而如果你离我很近遮挡我,证明我的反射光照无法传递出去,那么影响就大。

于是可以使用glsl的smoothstep函数进行平滑的插值

float smoothstep(float a, float b, float t)
{
    return a + (b - a) * t;
}

可以看到在书包头部的影响非常大,也符合常理,那就是头部的位置是悬停在地板之上的,不应该有层次。而其它和地板接触的位置,应该具备层次。

此时还有一个问题,可以注意到,因为我们随机采样局部坐标系的T是ever 4 * 4,可以很清楚的看到重复模式

优化方式就是对ssao的值做一次空间滤波

这里我们就采用普通的3 * 3的方式进行滤波求平均

#version 420 core

layout(location = 0) out float ssaoRate;

in vec2 vTexCoord;

uniform sampler2D ssaoTexture;

void main()
{
    float offset = 1.0 / (textureSize(ssaoTexture, 0));

    float result = 0.0;

    for(int i = -1; i <= 1; i++)
    {
        for(int j = -1; j <= 1; j++)
        {
            vec2 offsetuv = vec2(float(i), float(j)) * offset;
            result += texture(ssaoTexture, vTexCoord + offsetuv).r;
        }
    }
    ssaoRate = result / 9.0;
}

同时创建一个frambuffer去得到blur以后的值

一个bug问题

我相信有些朋友和我一样就是用世界坐标系去求tbn和物体的坐标,但是opengl教程写的是转为view空间。我发现如果是存储的世界坐标系,会出现逐渐的黑边效果。也就是计算ssao的值的时候会出现可见度随视角移动而变动。

这里我思考了一下,是这样的,为什么需要转为view空间呢,因为ssao的本质就是测试到camera的深度,如果采样点的深度浅则看得见。

但是如果用世界坐标有一个很严重的问题,那就是我们比较深度是用z的分量比较,这是世界坐标系,也就是你用z轴比较深度(到相机的距离)这是完全不正确的

为什么会随着camera的位置移动,阴影就会跟着移动呢,就是越靠近ssao小的地方,就会越亮。这里唯一变动就是view,那么也就是说采样点的z首先是一定不会变的,因为物体没有变动,也就是采样点的xy在屏幕上的对应物体不同。

anyway应该用view坐标系下,这样摄像机从0出发看向空间,那么深度就是z的坐标。

float occulusion = 0.0;
for(int i = 0; i < kernelSize; i++)
{
    vec3 samplePoint = TBN * samples[i];
    samplePoint = FragPos + samplePoint * radius;
    vec4 offset = perspective * vec4(samplePoint, 1.0);//view space to clip space
    offset /= offset.w;
    vec2 offsetuv = offset.xy * 0.5 + 0.5;
    float sampleDepth = texture(gPosition, offsetuv).z;

    float rangeCheck = smoothstep(0.0, 1.0, radius / abs(FragPos.z - sampleDepth));
    occulusion += (sampleDepth >= samplePoint.z + bias ? 1.0 : 0.0) * rangeCheck;
}
FragColor = 1.0 - (occulusion / kernelSize);
debugColor = texture(texNoise, texCoords).xyz;
#version 420 core

layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNormal;
layout(std140, binding = 2) uniform PMatrix
{
    mat4 view;
    mat4 perspective;
};
uniform bool inverseNormal;
uniform mat4 model;

out VS_OUT
{
    vec3 vNormal;
    vec3 vFragPos;
}vs_out;

void main()
{
    vs_out.vFragPos = ((view * model * vec4(aPos, 1.0)) / (view * model * vec4(aPos, 1.0)).w).xyz; // view space
    
    vec3 pasNormal = mat3(transpose(inverse(view * model))) * (inverseNormal ? -aNormal : aNormal); // view space
    vs_out.vNormal = normalize(pasNormal);
    gl_Position = perspective * view * model * vec4(aPos, 1.0);
}
#version 420 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNormal;
layout(location = 2) in vec3 aTangent;
layout(location = 3) in vec2 aTexCoord;
layout(std140, binding = 2) uniform PMatrix
{
    mat4 view;
    mat4 perspective;
};

out VS_OUT
{
    mat3 TBN;
    vec3 vFragPos;
    vec2 vTexCoord;
}vs_out;
uniform mat4 model;
void main()
{
    mat3 tbn_matrix = mat3(transpose(inverse(view * model))); // view space 
    vec3 T = tbn_matrix * aTangent;
    vec3 N = tbn_matrix * aNormal;
    T = T - dot(T, N) * N;
    vec3 B = cross(N, T);
    vs_out.TBN = mat3(T, B, N);

    vec4 pasPos = view * model * vec4(aPos, 1.0); // view space
    vs_out.vFragPos = (pasPos / pasPos.w).xyz;
    vs_out.vTexCoord = aTexCoord;
    gl_Position = perspective * view * model * vec4(aPos, 1.0);
}
glm::vec4 viewPos = camera.GetViewMatrix() * glm::vec4(lightPos, 1.0f);
glm::vec3 v = glm::vec3(viewPos / viewPos.w);
m_totalShader.SetValue("light.Position", v);
m_totalShader.SetValue("light.Color", lightColor);

.....
m_totalShader.SetValue("uCameraPos", glm::vec3(0.0f,0.0f,0.0f));

最终效果

不使用法线纹理贴图的时候

没有高光和衰减的时候

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值