SSAO

SSAO

引入SSAO

  • 要想渲染出逼真的效果,模拟真实世界的光照必不可少,这种光照模型有两部分组成:直接光照和间接光照,他有个高大上的名字,全局光照
  • 全局光照虽然可以渲染出逼真的效果,但开销巨大,不适合应用到实时渲染
  • 在实时渲染中,为了效率和效果达到一种平衡,只能将光照模型简化、变形甚至省略
  • 之前,提到的Blinn-Phong光照模型是对直接光照的模拟
  • 而AO(Ambient Occlusion,环境光遮蔽)就是对间接光照的模拟
  • 间接光照是指:经过周围物体多次反射后来到物体表面的光线;光线每被反射一次都会有一定程度的衰减
  • AO模拟间接光照的思路是:通过将褶皱、孔洞、面与面相交的小片区域变暗的方法近似模拟出间接光照
  • 可见AO是要使用场景中物体的几何体,再加上空间中的光线来计算遮蔽量,这会带来很大的性能开销
  • SSAO(Screen-Space Ambient Occlusion,屏幕空间环境光遮蔽,2007年由Crytek公司发布)使用了屏幕空间场景的深度而不是真实的几何体数据来确定遮蔽量。这一做法相对于AO不但速度快,而且效果好

原理

  • 首先将场景渲染到一副带有深度值的贴图,对于这个贴图中的每个片段,都会根据他周围的深度值计算一个遮蔽因子,这个因子会在之后用于减弱环境光照

带有深度值的贴图

  • 这个贴图不仅要提供深度值,用于计算遮蔽因子;还要提供顶点坐标,用于之后的光照计算
  • 在生成这个贴图时,如果用在片段着色器中用gl_FragCoord.z得到深度值,则是非线性的,还需要根据近远裁剪面转为线性的深度值
  • 线性的深度值在摄像机空间,或者说摄像机空间的顶点坐标Z值,就是线性的深度值
  • 既然这个纹理既要坐标又要线性深度,那么就将摄像机空间的坐标输出到纹理,一举两得
  • 此后再使用该纹理时就要注意了,用的都是摄像机空间中的坐标

确定遮蔽因子

  • 通过深度测试的采样点,遮蔽因子加一
  • 采样点是指:以片段为圆心的球型核心中,均匀的指定N个采样点
  • 深度测试时,用的深度缓冲就是前面提到的“带有深度值的纹理”
  • 当采样点的深度值小于纹理中的值时,即通过深度测试,遮蔽因子加一
  • 遮蔽因子越大,说明片段接收到的间接光照越少,因此在应用时要用1.0 - 遮蔽因子/采样点个数
  • 注意:使用这种算法产生的AO是不正确的,但Crytek公司认为渲染效果和效率都令人满意,而且一经发布快速被工业界广泛应用,使得这种方式逐渐成为模拟AO的标准
  • 小结一下:遮蔽因子不是判断片段是否被遮挡,也不是判断采样点是否被遮挡,而是通过统计采样点的数量,来减弱片段的环境光照,统计的是通过深度测试的采样点

实现

  • 原理不复杂,但实现起来多处需要注意;大致流程如下:
  • 渲染需要的纹理
  • 统计遮蔽因子,并用其生成ssao纹理
  • 利用前面生成四幅的纹理,计算光照

渲染需要的纹理

  • 由原理和大致流程可知,我们需要的不只一个纹理;一个摄像机空间的坐标纹理、一个法线纹理用于光照计算、一个颜色纹理
  • 一次场景渲染,为后续工作提供多个纹理支持,这正是延迟渲染的灵魂,所以这一步用延迟渲染实现

统计遮蔽因子,生成ssao纹理

  • 要统计遮蔽因子,首先要有采样点
  • 原理中说,采样点在球型核心中,但实际上使用半球采样核心效果更好,而且半球是在表面法线的方向上
  • 那么问题来了:要根据每个法线方向生成半球采样核心吗?问题先放这里,把具体流程理完,最后再说
  • 假设现在半球型采样核心定义完毕,接下来就要在靠近圆心的附近均匀地生成N个采样点
  • 问题2:生成多少采样点即能保证效果又能保持效率?
  • 有了采样点,就可以通过判断每个采样点是否通过深度测试来统计遮蔽因子
  • 有了遮蔽因子,用其生成ssao纹理
定义半球采样核心
  • 由于对每个表面法线方向生成采样核心非常困难,也不合实际,我们将在切线空间中生成采样核心,法向量将指向正z方向
  • 由此可知,采样点也是在切线空间中,在进行深度测试前一定要变换到摄像机空间,因为使用的坐标纹理是在摄像机空间
  • 定义采样核心中的采样点时要注意,先在半球范围内获取均匀的采样点,再将采样点往圆心偏移
std::uniform_real_distribution<GLfloat> randomFloats(0.0, 1.0); // 均匀的获取0.0到1.0之间的随机数
std::default_random_engine generator;
std::vector<glm::vec3> ssaoKernel;
for (GLuint i = 0; i < N; ++i)
{
    glm::vec3 sample( randomFloats(generator) * 2.0 - 1.0, randomFloats(generator) * 2.0 - 1.0, randomFloats(generator) );
    sample = glm::normalize(sample);

    GLfloat scale = GLfloat(i) / N;
    scale = lerp(0.1f, 1.0f, scale * scale);
    sample *= scale;//往圆心偏移
    ssaoKernel.push_back(sample);
}
GLfloat lerp(GLfloat a, GLfloat b, GLfloat f)
{
    return a + f * (b - a);
}
采样点数量
  • 渲染效果一定与采样点的数量有直接关系,如果采样点数量太低,渲染的精度会急剧减少;如果采样点数量太高,会影响性能;
  • 我们可以通过引入随机性到采样核心,从而减少采样点的数量;
  • 具体做法是:随机旋转采样核心,就能在有限样本数量中得到高质量的结果
  • 但是, 随机性又引入以下两个问题:
性能问题
  • 如果对场景中每个片段创建一个随机旋转向量,那么很快将内存耗尽
  • 解决办法是创建一个小的随机旋转向量纹理texNoise,平铺在屏幕上
  • 由于纹理坐标的取值在0.0和1.0之间,texNoise纹理将不会平铺。可以通过屏幕分辨率除以噪声纹理大小的方式计算纹理坐标的缩放因子,使用这个缩放因子使texNoise达到平铺的效果
//创建一个4x4朝向切线空间平面法线的随机旋转向量数组
std::vector<glm::vec3> ssaoNoise;
for (GLuint i = 0; i < 16; i++)
{
    glm::vec3 noise( random(), random(), 0.0f); //采样核心是沿着正z方向在切线空间内旋转,所以设定z分量为0.0,random()的取值是-1.0到1.0
    ssaoNoise.push_back(noise);
}
//ssaoNoise数组创建noiseTexture...
  • 这个随机旋转向量的具体用法是:与法向量结合计算切线空间的TBN矩阵
vec3 normal = texture(gNormal, TexCoords).rgb;
vec3 randomVec = texture(texNoise, TexCoords * noiseScale).xyz;
vec3 tangent = normalize(randomVec - normal * dot(randomVec, normal));//施密特正交化,由randowVec和normal,求一个与normal垂直的向量
vec3 bitangent = cross(normal, tangent);
mat3 TBN = mat3(tangent, bitangent, normal);
//将采样点转换到摄像机空间,统计遮蔽因子...
  • 统计遮蔽因子,输出ssao纹理
float occlusion = 0.0;
for(int i = 0; i < kernelSize; ++i)
{
    vec3 samplePos = TBN * samples[i]; //将采样点转换到摄像机空间
    samplePos = fragPos + samplePos * radius; 
    vec4 offset = vec4(samplePos, 1.0);
    offset = projection * offset;
    offset.xyz /= offset.w; 
    offset.xyz = offset.xyz * 0.5 + 0.5; 
    
    float sampleDepth = texture(gPosition, offset.xy).z; 
    occlusion += sampleDepth >= samplePos.z ? 1.0 : 0.0; 
}
occlusion = 1.0 - (occlusion / kernelSize);
FragColor = occlusion;
噪声图像
  • 解决噪声图像需要将ssao纹理进行模糊
  • 上面的随机旋转向量纹理使得ssao纹理在生成时,保持了一致的随机性,我们可以使用这一性质来创建一个简单的模糊着色器:
uniform sampler2D ssao;
void main() 
{
    vec2 texelSize = 1.0 / vec2(textureSize(ssaoInput, 0));
    float result = 0.0;
    for (int x = -2; x < 2; ++x) 
    {
        for (int y = -2; y < 2; ++y) 
        {
            vec2 offset = vec2(float(x), float(y)) * texelSize;
            result += texture(ssao, TexCoords + offset).r;
        }
    }
    FragColor = result / (4.0 * 4.0);
}  
  • ssao纹理,他是一个灰度图,灰度图不是说一定得是灰色,而是说他的三个颜色通道的值相同,因此在创建这种纹理时,纹理内部格式设置为只有一个颜色通道,这里设置为GL_RED,所以就得到了下面红色的结果
    在这里插入图片描述

  • 上面红色的看着不习惯,下面白色的也许会更容易体会啥是AO;白色是因为纹理的内部格式设置为了GL_RGB,在实际应用时不建议这么做
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zE1IJ6GH-1629472511173)(en-resource://database/6077:1)]

利用四幅纹理,计算光照

  • 前面延迟渲染的几何着色阶段生成了三个纹理:坐标纹理、法线纹理、颜色纹理;接下来又生成了ssao纹理,用这四幅纹理进行光照计算
  • 在计算光照时,ssao纹理只用来减弱环境光照
  • 漫反射光和镜面高光用衰减方程减弱
vec3 Diffuse = texture(gAlbedo, TexCoords).rgb;
float AmbientOcclusion = texture(ssao, TexCoords).r;
vec3 ambient = vec3(0.3 * Diffuse * AmbientOcclusion);
vec3 diffuse = max(dot(Normal, lightDir), 0.0) * Diffuse * light.Color;
//...
vec3 specular = light.Color * spec;
//...
float attenuation = 1.0 / (1.0 + light.Linear * distance + light.Quadratic * distance * 
distance);//light.Linear线形衰减因子,light.Quadratic二次衰减因子
diffuse *= attenuation;
specular *= attenuation;
lighting += diffuse + specular;

FragColor = vec4(lighting, 1.0);
效果对比
  • 使用ssao
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SkW8yo8E-1629472511175)(en-resource://database/6076:1)]

  • 没有使用ssao
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dLn5vvgd-1629472511178)(en-resource://database/6075:1)]

  • ssao的效果本身就不是很明显,再加上背包的颜色又比较重,在背包上很难看处差别;

  • 在图的右上角,三个面两两相交处,可以看到少许差异

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值