[OpenGL]实现屏幕空间环境光遮蔽(Screen-Space Ambient Occlusion, SSAO)

一、简介

本文介绍了 屏幕空间环境光遮蔽(Screen-Space Ambient Occlusion, SSAO) 的基本概念,实现流程和简单的代码实现。实现 SSAO 时使用到了 OpenGL 中的延迟着色 (Deferred shading)技术。
按照本文代码实现后,可以实现以下效果:

渲染结果

二、SSAO介绍以及实现流程

1. SSAO 介绍

(1). 什么是 Ambient Occlusion, AO

简单来说 Ambient Occlusion(AO) 是一种全局照明(Global Illumination,GI)中的根据环境光(Ambient Light)参数和环境几何信息来计算场景中任何一点的光照强度系数的算法。
AO 描述了表面上的任何一点所接受到的环境光被周围几何体所遮蔽的百分比, 因此使得渲染的结果更加富有层次感,对比度更高。
例如:对于下图中的车辆缝隙处(红色),会受到周边模型面片的遮挡,导致接收到的环境光 (Ambient light)较少,因此这些地方会更暗。而在车辆的边缘处(绿色),几乎不会受到周围模型面片的遮挡,接收到的环境光较多,因此这些地方会更亮。
AO示意图

(2). 什么是 Screen-Space Ambient Occlusion, SSAO

为了计算准确的 AO,可以使用光线跟踪算法。但是光线跟踪消耗计算资源太大,而 屏幕空间环境光遮蔽 (Screen-Space Ambient Occlusion, SSAO) 是一种仅仅基于屏幕信息(例如,屏幕上各像素对应 片元 的空间位置信息)快速估计 AO 的算法。

SSAO 算法的基本思想为:
对于目标着色点,在其周围的一个球(或者面向相机方向的半球)内采样多个采样点,如果采样点大多被模型的其他面片遮挡,那么说明该目标着色点的 Ambien Occlusion 比较大,因此该着色点理应较暗些。而反之,目标着色点周围得到的采样点大部分并不会被模型的其他面片遮挡,那么说明该着色点的 Ambient Occlusion 更小,因此会更亮。

以下图为例:
在图中的 红色着色点 附近的一个球内采样,得到 8 个采样点,其中只有两个采样点(白色采样点)相比模型中的其他面片更靠近相机,不会被模型面片遮挡。而其他 6 个采样点(灰色采样点)都会被模型中的其他面片遮挡,则红色目标着色点的 Ambient Occlusion 更大,渲染结果中此着色点会更暗。
而图中的 绿色着色点 附近的大多数采样点(白色采样点)都不会被模型面片遮挡,只有两个采样点(灰色采样点)会被遮挡,则绿色目标着色点的 Ambient Occlusion 更小,渲染结果中此着色点会更亮。

SSAO 示意图
在实现时也可以在朝向相机的半球内采样,理论上这样的结果会更加准确,而不是上图中所示的整个球内采样。在计算得到 各点的 AO 值后,也可以使用 滤波 操作对屏幕上各点的 AO 值进行滤波操作,平滑遮蔽效果,消除噪点。

2. SSAO 实现流程

实现 SSAO 主要分为 4 趟 pass。

(1). Geometry Pass

该 pass 对输入场景模型进行处理,将屏幕各像素对应片元的 texture_color, positon (in world space), normal 和 position (in view space) 输出到 GBuffer 中;

(2). Cal SSAO Pass

该 pass 根据 屏幕各像素对应片元(目标着色点)的 position (in view space) 信息,在各 片元 周围采样,得到采样点,根据采样点 是否会被模型遮挡计算目标着色点的 AO 值;

(3.) Blur SSAO Pass

该 pass 对 上趟流程中计算得到的 AO 进行滤波操作,的到滤波后的 blurredAO;

(4). Lighting (Shading) Pass

该 pass 根据 pass (1) 中得到的 texture_color, positon (in world space), normal 和 pass (3) 中得到的 blurredAO 计算各着色点的颜色值。使用 Phong 着色模型,公式如下:
I = I a ∗ b l u r r e d A O + I d + I s I=Ia * blurredAO + Id + Is I=IablurredAO+Id+Is

3. 主要代码讲解

(1). Geometry Pass Shader

geometryPassShader.vert:

#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNor;
layout(location = 2) in vec2 aTexCoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

out vec3 vertexPos;
out vec3 vertexNor;
out vec2 textureCoord;
out vec4 vertexViewPos;

void main() {
  textureCoord = aTexCoord;
  // 裁剪空间坐标系 (clip space) 中 点的位置
  gl_Position = projection * view * model * vec4(aPos, 1.0f);
  // 世界坐标系 (world space) 中 点的位置
  vertexPos = (model * vec4(aPos, 1.0f)).xyz;
  // 世界坐标系 (world space) 中 点的法向
  vertexNor = mat3(transpose(inverse(model))) * aNor;
  // 视图坐标系 (view space) 中 点的位置
  vertexViewPos = view * model * vec4(aPos, 1.0f);
}

geometryPassShader.frag:

#version 330 core
layout(location = 0) out vec4 FragColor;   // diffuse color
layout(location = 1) out vec3 FragPos;     // position in world space
layout(location = 2) out vec3 FragNor;     // normal in world space
layout(location = 3) out vec4 FragViewPos; // position in view space

in vec3 vertexPos;
in vec3 vertexNor;
in vec2 textureCoord;
in vec4 vertexViewPos;

uniform sampler2D texture0;

void main() {
  FragPos = vertexPos;
  FragNor = vertexNor;
  FragColor = texture(texture0, textureCoord);
  FragViewPos = vertexViewPos;
}

(2). Cal SSAO Pass Shader

calSSAOPassShader.vert

#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNor;
layout(location = 2) in vec2 aTexCoord;
out vec2 textureCoord;
void main() {
  gl_Position = vec4(aPos, 1.0f);
  textureCoord = aTexCoord;
}

calSSAOPassShader.frag:

#version 330 core
out float AO;
in vec2 textureCoord;

uniform mat4 projection;

uniform sampler2D textureViewPos; // position (in view space)

uniform vec3 gKernel[64]; // random position offset

// 计算目标片段的 Ambient Occlusion (AO) 值
// AO in [0,1]
// 为了便于后续计算 代码中的 AO 规定为:
// AO 越接近 0,说明该片段被遮挡的越多(越暗)
// AO 越接近 1,说明该片段被遮挡的越多(越亮)
void main() {

  vec3 shadeViewPos = texture(textureViewPos, textureCoord)
                          .xyz; // 目标片段在 view space 中的坐标

  AO = 0.0;
  float gSampleRad = 1.5f;
  for (int i = 0; i < 64; i++) {
    vec3 sampleViewPos = shadeViewPos + gKernel[i]; // 在目标片段周围随机采样
    vec4 sampleProPos =
        vec4(sampleViewPos, 1.0); // 采样点 在 view space 中的坐标

    sampleProPos = projection * sampleProPos;
    sampleProPos.xy /= sampleProPos.w;
    // 采样点 投影到屏幕,再归一化到[0,1]的 xy 坐标 (即采样点对应的 uv 坐标)
    sampleProPos.xy = sampleProPos.xy * 0.5 + vec2(0.5, 0.5);

    // 相机-采样点 射线与场景相交点(场景表面点)对应的 z 值(在 view space 中)
    float surfaceDepth = texture(textureViewPos, sampleProPos.xy).z;

    if (abs(shadeViewPos.z - surfaceDepth) < gSampleRad) {
      // step(a,b) = if (a<b) return 1.0 else return 0.0;
      // 在 view sapce 中, camera position 为 (0,0,0)
      // 假如 abs(surfaceDepth) < abs(sampleViewPos.z) 说明 场景表面点 比
      // 采样点距离相机更近,那么 AO += 1
      // 假如 abs(surfaceDepth) >= abs(sampleViewPos.z) 说明 采样点 比
      // 场景表面点 距离相机更近,那么 AO += 0
      AO += step(abs(surfaceDepth), abs(sampleViewPos.z));
    }
  }
  // 前面 AO 记录的是 '采样点 被 场景表面 遮挡的次数'
  // 因此需要 令 AO = 1.0 - AO / (采样次数)
  // 最后得到的 AO 才是目标片段的 AO 值
  AO = 1.0 - AO / 64;
}

(3). Blur SSAO Pass Shader

blurSSAOPassShader.vert:

#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNor;
layout(location = 2) in vec2 aTexCoord;
out vec2 textureCoord;
void main() {
  gl_Position = vec4(aPos, 1.0f);
  textureCoord = aTexCoord;
}

blurSSAOPassShader.frag:

#version 330 core
// out vec4 FragColor;
out float blurredAO;

in vec2 textureCoord;

uniform sampler2D textureAO;

void main() {
  blurredAO = 0.0;
  float Offsets[4] = float[](-1.5, -0.5, 0.5, 1.5);
  float originAO = texture(textureAO, textureCoord).x;

  for (int i = 0; i < 4; i++) {
    for (int j = 0; j < 4; j++) {
      float AO = texture(textureAO, textureCoord).r;
      vec2 tc = textureCoord;
      tc.x = textureCoord.x + Offsets[j] / textureSize(textureAO, 0).x;
      tc.y = textureCoord.y + Offsets[i] / textureSize(textureAO, 0).y;
      blurredAO += texture(textureAO, tc).x;
    }
  }

  blurredAO /= 16.0;
}

(4). Lighting (Shading) Pass Shader

lightingSSAOPassShader.vert:

#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNor;
layout(location = 2) in vec2 aTexCoord;
out vec2 textureCoord;
void main() {
  gl_Position = vec4(aPos, 1.0f);
  textureCoord = aTexCoord;
}

lightingSSAOPassShader.frag:

#version 330 core
out vec4 FragColor;

in vec2 textureCoord;

uniform int state;

uniform vec3 lightPos;

uniform vec3 cameraPos;
uniform vec3 k;

uniform sampler2D textureColor;     // color
uniform sampler2D texturePos;       // position (in world space)
uniform sampler2D textureNor;       // normal (in world space)
uniform sampler2D textureBlurredAO; // blurredAO

void main() {
  vec3 vertexPos = texture(texturePos, textureCoord).xyz;
  vec3 vertexNor = texture(textureNor, textureCoord).xyz;
  vec3 lightColor = vec3(1.0f, 1.0f, 1.0f);

  // Ambient
  // Ia = ka * La
  float ambientStrenth = k[0];
  vec3 ambient = ambientStrenth * lightColor;
  float blurredAO = texture(textureBlurredAO, textureCoord).x;

  if (state == 0) {
    // Rendering scene with SSAO on.
    ambient = ambient * vec3(blurredAO);
  } else if (state == 1) {
    // Rendering scene with SSAO off.
  } else {
    // Rendering AO.
    FragColor = vec4(blurredAO);
    return;
  }
  vec3 diffuse = vec3(0, 0, 0);
  vec3 specular = vec3(0, 0, 0);
  // Diffuse
  // Id = kd * max(0, normal dot light) * Ld
  float diffuseStrenth = k[1];
  vec3 normalDir = normalize(vertexNor);

  vec3 lightDir = normalize(lightPos - vertexPos);
  diffuse = diffuseStrenth * max(dot(normalDir, lightDir), 0.0) * lightColor;

  // Specular (Phong)
  // Is = ks * (view dot reflect)^s * Ls

  float specularStrenth = k[2];
  vec3 viewDir = normalize(cameraPos - vertexPos);
  vec3 reflectDir = reflect(-lightDir, normalDir);
  specular = specularStrenth * pow(max(dot(viewDir, reflectDir), 0.0f), 2) *
             lightColor;

  // Specular (Blinn-Phong)
  // Is = ks * (normal dot halfway)^s Ls
  // float specularStrenth = k[2];
  // vec3 viewDir = normalize(cameraPos - vertexPos);
  // vec3 halfwayDir = normalize(lightDir + viewDir);
  // vec3 temp_specular = specularStrenth *
  //                      pow(max(dot(normalDir, halfwayDir), 0.0f), 2) *
  //                      lightColor;

  diffuse = clamp(diffuse, 0.0, 1.0);
  specular = clamp(specular, 0.0, 1.0);

  // Obejct color
  vec3 objectColor = texture(textureColor, textureCoord).xyz;

  // Color = Ambient + Diffuse + Specular
  // I = Ia + Id + Is
  FragColor = vec4((ambient + diffuse + specular) * objectColor, 1.0f);
}

4. 全部代码及模型文件

使用OpenGL实现 屏幕空间环境光遮蔽(Screen-Space Ambient Occlusion, SSAO) 的全部代码以及模型文件可以在 OpenGL实现屏幕空间环境光遮蔽(Screen-Space Ambient Occlusion, SSAO) 中下载。程序运行后按下 空格 键在使用SSAO渲染场景直接渲染场景渲染AO值三种模式中切换。
渲染结果如下:
渲染结果

三、参考

[1].ogl-tutorial45 Screen Space Ambient Occlusion
[2].游戏后期特效第四发 – 屏幕空间环境光遮蔽(SSAO)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/957352.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

c++ 与 Matlab 程序的数据比对

文章目录 背景环境数据保存数据加载 背景 ***避免数据精度误差&#xff0c;快速对比变量 *** 环境 c下载 https://github.com/BlueBrain/HighFive 以及hdf5库 在vs 中配置库 数据保存 #include <highfive/highfive.hpp> using namespace HighFive;std::string fil…

Java基础——概念和常识(语言特点、JVM、JDK、JRE、AOT/JIT等介绍)

我是一个计算机专业研0的学生卡蒙Camel&#x1f42b;&#x1f42b;&#x1f42b;&#xff08;刚保研&#xff09; 记录每天学习过程&#xff08;主要学习Java、python、人工智能&#xff09;&#xff0c;总结知识点&#xff08;内容来自&#xff1a;自我总结网上借鉴&#xff0…

Java设计模式:创建型模式→建造者模式

Java 建造者模式详解 1. 定义 建造者模式&#xff08;Builder Pattern&#xff09;是一种创建型设计模式&#xff0c;允许使用多个简单的对象一步步构建一个复杂的对象。该模式使用一个建造者对象来构造一个最终的对象&#xff0c;提供清晰的分步构建流程&#xff0c;从而使得…

从CRUD到高级功能:EF Core在.NET Core中全面应用(三)

目录 IQueryable使用 原生SQL使用 实体状态跟踪 全局查询筛选器 并发控制使用 IQueryable使用 在EFCore中IQueryable是一个接口用于表示可查询的集合&#xff0c;它继承自IEnumerable但具有一些关键的区别&#xff0c;使得它在处理数据库查询时非常有用&#xff0c;普通集…

C语言之小型成绩管理系统

&#x1f31f; 嗨&#xff0c;我是LucianaiB&#xff01; &#x1f30d; 总有人间一两风&#xff0c;填我十万八千梦。 &#x1f680; 路漫漫其修远兮&#xff0c;吾将上下而求索。 C语言之小型成绩管理系统 目录 设计题目设计目的设计任务描述设计要求输入和输出要求验收要…

Linux中DataX使用第一期

简介 DataX 是阿里云 DataWorks数据集成 的开源版本&#xff0c;在阿里巴巴集团内被广泛使用的离线数据同步工具/平台。DataX 实现了包括 MySQL、Oracle、OceanBase、SqlServer、Postgre、HDFS、Hive、ADS、HBase、TableStore(OTS)、MaxCompute(ODPS)、Hologres、DRDS, databen…

Windows配置frp内网穿透实现远程连接

仅个人记录 本文仅介绍客户端的配置 1. 开始 frp分为服务端和客户端&#xff0c;为实现内网穿透需要同时配置服务端和客户端&#xff0c;并且版本保持一致&#xff0c;可以前往 frp github下载 本文使用 0.51.2 版本&#xff0c;从GitHub下载并解压&#xff0c;得到如下文件…

PHP同城配送小程序

&#x1f680; 同城极速达——您生活中的极速配送大师 &#x1f4f1; 一款专为现代都市快节奏生活量身打造的同城配送小程序&#xff0c;同城极速达&#xff0c;集高效、便捷、智能于一身&#xff0c;依托ThinkPHPGatewayWorkerUniapp的强大架构&#xff0c;巧妙融合用户端、骑…

ESP32云开发二( http + led + lcd)

文章目录 前言先上效果图platformio.iniwokwi.tomldiagram.json源代码编译编译成功上传云端完结撒花⭐⭐⭐⭐⭐ 前言 阅读此篇前建议先看 此片熟悉下wokwi https://blog.csdn.net/qq_20330595/article/details/144289986 先上效果图 Column 1Column 2 platformio.ini wokwi…

分布式搜索引擎02

1. DSL查询文档 elasticsearch的查询依然是基于JSON风格的DSL来实现的。 1.1. DSL查询分类 Elasticsearch提供了基于JSON的DSL&#xff08;Domain Specific Language&#xff09;来定义查询。常见的查询类型包括&#xff1a; 查询所有&#xff1a;查询出所有数据&#xff0c…

reactor框架使用时,数据流请求流程

1. 我们在Flux打开时&#xff0c;可以看到 public abstract class Flux<T> implements CorePublisher<T> { 2. public interface CorePublisher<T> extends Publisher<T> {void subscribe(CoreSubscriber<? super T> subscriber); } Publish…

E-Prime2实现List嵌套

用E-Prime实现一个简单的List嵌套&#xff0c;实验流程基于斯特鲁程序&#xff08;色词一致/不一致实验&#xff09;。 首先File-New&#xff0c;新建一个空白项目 此时生成流程如下 Experiment Object是实验中被用到的流程或者控件对象&#xff0c;SessionProc是总流程&#x…

web-view环境下,H5页面打开其他小程序

在Web-view环境下&#xff0c;H5页面无法直接打开其他小程序。正确的实现方式是先从H5页面跳转回当前小程序&#xff0c;再由当前小程序跳转到目标小程序。具体实现方法如下&#xff1a; H5页面跳转回小程序时&#xff0c;调用wx.miniProgram.navigateTo()方法。 小程序跳转到…

ChatGPT 摘要,以 ESS 作为你的私有数据存储

作者&#xff1a;来自 Elastic Ryan_Earle 本教程介绍如何设置 Elasticsearch 网络爬虫&#xff0c;将网站索引到 Elasticsearch 中&#xff0c;然后利用 ChatGPT 使用我们的私人数据来总结对其提出的问题。 Python 脚本的 Github Repo&#xff1a;https://github.com/Gunner…

Linux系统的第一个进程是什么?

Linux进程的生命周期从创建开始&#xff0c;直至终止&#xff0c;贯穿了一个进程的整个存在过程。我们可以通过系统调用fork()或vfork()来创建一个新的子进程&#xff0c;这标志着一个新进程的诞生。 实际上&#xff0c;Linux系统中的所有进程都是由其父进程创建的。 既然所有…

《冲动》V1.6官方学习版

《冲动》官方版 https://pan.xunlei.com/s/VODiYvUAE1lECHcq66BR1np_A1?pwdfxc6# 具有侦探小说、戏剧和恐怖元素的惊悚片。 主角结束了漫长的商务旅行回到家&#xff0c;他的妻子和年幼的儿子热切地等待着他。然而&#xff0c;当他到达时&#xff0c;他发现有些不对劲&#x…

【Java计算机毕业设计】基于SSM圣宠宠物领养网站【源代码+数据库+LW文档+开题报告+答辩稿+部署教程+代码讲解】

源代码数据库LW文档&#xff08;1万字以上&#xff09;开题报告答辩稿 部署教程代码讲解代码时间修改教程 一、开发工具、运行环境、开发技术 开发工具 1、操作系统&#xff1a;Window操作系统 2、开发工具&#xff1a;IntelliJ IDEA或者Eclipse 3、数据库存储&#xff1a…

【微机原理与接口技术】定时控制接口

文章目录 8253的引脚和工作方式内部结构和引脚工作方式方式0&#xff1a;计数结束中断方式1&#xff1a;可编程单稳脉冲方式2&#xff1a;周期性负脉冲输出方式3&#xff1a;方波发生器方式4&#xff1a;软件触发的单次负脉冲输出方式5&#xff1a;硬件触发的单次负脉冲输出各种…

场馆预定平台高并发时间段预定实现V2

&#x1f3af; 本文档介绍了场馆预订系统接口V2的设计与实现&#xff0c;旨在解决V1版本中库存数据不一致及性能瓶颈的问题。通过引入令牌机制确保缓存和数据库库存的最终一致性&#xff0c;避免因服务器故障导致的库存错误占用问题。同时&#xff0c;采用消息队列异步处理库存…

从零到上线:Node.js 项目的完整部署流程(包含 Docker 和 CICD)

从零到上线&#xff1a;Node.js 项目的完整部署流程&#xff08;包含 Docker 和 CI/CD&#xff09; 目录 项目初始化&#xff1a;构建一个简单的 Node.js 应用设置 Docker 环境&#xff1a;容器化你的应用配置 CI/CD&#xff1a;自动化构建与部署上线前的最后检查&#xff1a;…