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

news2025/1/23 15:18:02

一、简介

本文介绍了 屏幕空间环境光遮蔽(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)

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

KubeSphere 开源社区 2024 年度回顾与致谢

随着 2024 年圆满落幕&#xff0c;我们回顾 KubeSphere 社区这一年走过的每一步&#xff0c;感慨万千。2024 年&#xff0c;KubeSphere 继续领跑云原生技术的创新与发展&#xff0c;推动开源文化的传播&#xff0c;致力于为全球开发者和企业用户提供更强大的平台和解决方案。感…

ToDesk云电脑、顺网云、网易云、易腾云、极云普惠云横测对比:探寻电竞最佳拍档

一、云电脑&#xff1a;电竞新宠崛起 在电竞游戏不断发展的今天&#xff0c;硬件性能成为了决定游戏体验的关键因素。为了追求极致的游戏画面与流畅度&#xff0c;玩家们往往需要投入大量资金购置高性能电脑。然而&#xff0c;云电脑技术的出现&#xff0c;为玩家们提供了一种…

GitCode 助力 AutoTable:共创 MyBatis 生态的自动表格管理新篇章

项目仓库https://gitcode.com/dromara/auto-table 解放双手&#xff0c;专注业务&#xff1a;MyBatis 生态的“自动表格”创新 AutoTable 是一款致力于为 MyBatis 生态赋予“自动表格”功能的创新插件。其核心理念是通过 Java 实体类自动生成和维护数据库的表结构&#xff0c…

【useLayoutEffect Hook】在浏览器完成布局和绘制之前执行副作用

目录 前言语法useLayoutEffect 对比 useEffect&#xff1a;示例 前言 useLayoutEffect 是 React 中的一个 Hook&#xff0c; 类似于 useEffect&#xff0c;但有一个关键的区别&#xff1a;它会在所有的 DOM 变更之后同步调用 effect。这意味着它可以读取 DOM 布局并同步重新渲…

vue3-sfc-loader 加载远程.vue文件(sfc)案例

注意事项 style标签如果增加了lang比如&#xff1a;lang“scss”&#xff0c;需要提供scss-loader的处理器&#xff0c;这个暂时没研究&#xff0c;我的处理方式是将动态模版的css放在了全局打包暂时还没有测试&#xff0c;后面测试了会同步更新 安装vue3-sfc-loader npm inst…

AIGC的企业级解决方案架构及成本效益分析

AIGC的企业级解决方案架构及成本效益分析 一,企业级解决方案架构 AIGC(人工智能生成内容)的企业级解决方案架构是一个多层次、多维度的复杂系统,旨在帮助企业实现智能化转型和业务创新。以下是总结的企业级AIGC解决方案架构的主要组成部分: 1. 技术架构 企业级AIGC解决方…

NSIS系统制作Windows下的简易的安装程序

一. 前言 NSIS&#xff08;Nullsoft Scriptable Install System&#xff09;是一个专业的开源系统&#xff0c;用于创建 Windows 安装程序。拥有小巧而灵活的特点&#xff0c;受到很多用户的赞赏。 NSIS 基于脚本语言&#xff0c;允许您创建逻辑来处理比较复杂的安装任务。 官…

leetcode-不同路径问题

一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角&#xff08;在下图中标记为 “Finish” &#xff09;。 问总共有多少条不同的路径&#xff1f; 看见题目…

前端开发中的模拟后端与MVVM架构实践[特殊字符][特殊字符][特殊字符]

平时&#xff0c;后端可能不能及时给接口给前端进行数据调用和读取。这时候&#xff0c;前端想到进行模拟后端接口。本文将介绍如何通过vite-plugin-mock插件模拟后端接口&#xff0c;并探讨MVVM架构在前端开发中的应用。此外&#xff0c;我们还将讨论Vue2与Vue3的区别&#xf…

【Tool】沉浸式翻译 DeepLX

效果对比 对比一下四个常用的翻译工具的效果 不难看出只有Deepl算是在讲人话 如何配置 DeepLX 安装沉浸式翻译插件 获取APIKEY 从这获取: https://linux.do/t/topic/111737 配置 参考官方教程: https://linux.do/t/topic/111911

登录认证(3):会话跟踪技术:Session

Session概览 上文提到了&#xff0c;为了在同一个会话中共享数据&#xff08;比如用户的登录状态&#xff09;&#xff0c;我们需要使用会话跟踪技术&#xff0c;Cookie是客户端的会话跟踪技术&#xff0c;是存储在本地浏览器中的。而本文介绍另外一种会话跟踪技术Session&…

2024年博客之星年度评选|第一步——创作影响力评审入围Top300名单 | 博客之星陪跑指南

2024年博客之星年度评选&#xff5c;第一步——创作影响力评审入围Top300名单 | 博客之星陪跑指南 2024年博客之星年度评选正在如火如荼地进行中&#xff01;作为博客圈最具影响力的评选活动之一&#xff0c;今年的评选吸引了众多优秀博主的参与。现在&#xff0c;距离Top300入…

ui文件转py程序的工具

源博客连接&#xff1a; PyCharm中利用外部工具uic转成的py文件&#xff0c;里面全是C代码&#xff0c;并非python类型的代码&#xff0c;导致大量报错。。。_pyside6-uic为什么把ui转为了c-CSDN博客 如果想把ui文件转为py文件&#xff0c;首先设置pycharm的外部工具&#xf…

【喜讯】海云安荣获“数字安全产业贡献奖”

近日&#xff0c;国内领先的数字化领域独立第三方调研咨询机构数世咨询主办的“2025数字安全市场年度大会”在北京成功举办。在此次大会上&#xff0c;海云安的高敏捷信创白盒产品凭借其在AI大模型技术方面的卓越贡献和突出的技术创新能力&#xff0c;荣获了“数字安全产业贡献…

FluentCMS:基于 ASP.NET Core 和 Blazor 技术构建的开源CMS内容管理系统

推荐一个基于 ASP.NET Core 和 Blazor 技术构建的、功能完善的开源CMS内容管理系统。 01 项目简介 FluentCMS 是一个基于强大的 ASP.NET Core 和创新的 Blazor 技术构建的现代内容管理系统&#xff08;CMS&#xff09;。 FluentCMS 设计为快速、灵活且用户友好&#xff0c;它…

Java实现简易银行账户管理系统

目录 1、项目概述 1.1 项目结构 1.2 技术栈 2、核心功能说明 2.1 账户管理 2.2 异常处理体系 3、设计理念解析 3.1 面向对象设计 3.2 关键设计点 4、使用指南 4.1 运行流程 4.2 注意事项 5、扩展建议 5.1增加功能 5.2优化方向 6、主要的功能模块代码说明 6.1exception 6.2main …

深度学习系列75:sql大模型工具vanna

1. 概述 vanna是一个可以将自然语言转为sql的工具。简单的demo如下&#xff1a; !pip install vanna import vanna from vanna.remote import VannaDefault vn VannaDefault(modelchinook, api_keyvanna.get_api_key(my-emailexample.com)) vn.connect_to_sqlite(https://va…

C# 网络协议第三方库Protobuf的使用

为什么要使用二进制数据 通常我们写一个简单的网络通讯软件可能使用的最多的是字符串类型&#xff0c;比较简单&#xff0c;例如发送格式为(head)19|Msg:Heart|100,x,y,z…&#xff0c;在接收端会解析收到的socket数据。 这样通常是完全可行的&#xff0c;但是随着数据量变大&…

网络安全 | 什么是正向代理和反向代理?

关注&#xff1a;CodingTechWork 引言 在现代网络架构中&#xff0c;代理服务器扮演着重要的角色。它们在客户端和服务器之间充当中介&#xff0c;帮助管理、保护和优化数据流。根据代理的工作方向和用途&#xff0c;代理服务器可分为正向代理和反向代理。本文将深入探讨这两种…

Python 预训练:打通视觉与大语言模型应用壁垒——Python预训练视觉和大语言模型

大语言模型是一种由包含数百亿甚至更多参数的深度神经网络构建的语言模型&#xff0c;通常使用自监督学习方法通过大量无标签文本进行训练&#xff0c;是深度学习之后的又一大人工智能技术革命。 大语言模型的发展主要经历了基础模型阶段(2018 年到2021年)、能力探索阶段(2019年…