从零开始的OpenGL光栅化渲染器构建5-阴影

前言

阴影是光线被阻挡的结果;当一个光源的光线由于其他物体的阻挡不能够达到一个物体的表面的时候,那么这个物体就在阴影中了。阴影能够使场景看起来真实得多,并且可以让观察者获得物体之间的空间位置关系。

直接阴影

阴影映射(Shadow Mapping)背后的思路非常简单:我们以光的位置为视角进行渲染,我们能看到的东西都将被点亮,看不见的一定是在阴影之中了。

阴影映射的过程是,首先从光的视角渲染出一张深度图(利用帧缓冲),然后再进行正常渲染,根据深度图判断当前片段是否在阴影中。阴影映射的伪代码如下:

// 1. 首先渲染深度贴图
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glClear(GL_DEPTH_BUFFER_BIT);
ConfigureShaderAndMatrices();
RenderScene();
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// 2. 像往常一样渲染场景,但这次使用深度贴图
glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
ConfigureShaderAndMatrices();
glBindTexture(GL_TEXTURE_2D, depthMap);
RenderScene();

阴影映射的可视化结果如下:

在这里插入图片描述

如何检查一个片段是否在阴影中:

首先把变换到光源视角空间的片段位置转换为裁切空间的标准化设备坐标。然后将当前片段的深度值与深度图中存储的深度值做比较,决定此时是否处在阴影之中。

此时渲染出的效果图如下图所示:
在这里插入图片描述

对阴影进行改进

我们可以看到,此时在图片中可以看到明显的线条样式,这种阴影贴图的不真实感叫做阴影失真(Shadow Acne),下图解释了成因:

在这里插入图片描述

因为阴影贴图受限于分辨率,在距离光源比较远的情况下,多个片段可能从深度贴图的同一个值中去采样。图片每个斜坡代表深度贴图一个单独的纹理像素。你可以看到,多个片段从同一个深度值进行采样。

虽然很多时候没问题,但是当光源以一个角度朝向表面的时候就会出问题,这种情况下深度贴图也是从一个角度下进行渲染的。多个片段就会从同一个斜坡的深度纹理像素中采样,有些在地板上面,有些在地板下面;这样我们所得到的阴影就有了差异。因为这个,有些片段被认为是在阴影之中,有些不在,由此产生了图片中的条纹样式。

我们可以用一个叫做阴影偏移(shadow bias)的技巧来解决这个问题,我们简单的对表面的深度(或深度贴图)应用一个偏移量,这样片段就不会被错误地认为在表面之下了。

在这里插入图片描述

使用了偏移量后,所有采样点都获得了比表面深度更小的深度值,这样整个表面就正确地被照亮,没有任何阴影。实现的代码为:

float bias = 0.005;
float shadow = currentDepth - bias > closestDepth  ? 1.0 : 0.0;

有些表面坡度很大,仍然会产生阴影失真。有一个更加可靠的办法能够根据表面朝向光线的角度更改偏移量:使用点乘:

float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);

这里我们有一个偏移量的最大值0.05,和一个最小值0.005,它们是基于表面法线和光照方向的。这样像地板这样的表面几乎与光源垂直,得到的偏移就很小,而比如立方体的侧面这种表面得到的偏移就更大。

除此之外,还有一个正面剔除的步骤,为了解决阴影偏移问题,不过这个步骤我还没搞清楚,暂时不贴出来了。

此时的阴影效果如下:

在这里插入图片描述

除此之外,还需要对采样进行一些额外的限制。目前的采样渲染过程,光的视锥不可见的区域一律被认为是处于阴影中,不管它真的处于阴影之中。出现这个状况是因为超出光的视锥的投影坐标比1.0大,这样采样的深度纹理就会超出他默认的0到1的范围。为了处理这个问题,我们让所有超出深度贴图的坐标的深度范围是1.0,这样超出的坐标将永远不在阴影之中。另外,还有一部分区域的坐标超出了光的正交视锥的远平面。对于这一点,只要投影向量的z坐标大于1.0,我们就把shadow的值强制设为0.0。

这样,此时的渲染结果如下图所示:

在这里插入图片描述

PCF

阴影中锯齿边很严重,需要处理一下这个问题。

可以通过增加深度贴图的分辨率的方式来降低锯齿块,也可以尝试尽可能的让光的视锥接近场景。

另一个(并不完整的)解决方案叫做PCF(percentage-closer filtering),这是一种多个不同过滤方式的组合,它产生柔和阴影,使它们出现更少的锯齿块和硬边。核心思想是从深度贴图中多次采样,每一次采样的纹理坐标都稍有不同。每个独立的样本可能在也可能不再阴影中。所有的次生结果接着结合在一起,进行平均化,我们就得到了柔和阴影。

一个简单的PCF的实现是简单的从纹理像素四周对深度贴图采样,然后把结果平均起来:

float shadow = 0.0;
vec2 texelSize = 1.0 / textureSize(shadowMap, 0);
for(int x = -1; x <= 1; ++x)
{
    for(int y = -1; y <= 1; ++y)
    {
        float pcfDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r; 
        shadow += currentDepth - bias > pcfDepth ? 1.0 : 0.0;        
    }    
}
shadow /= 9.0;

运用了PCF进行边缘软化后,此时的渲染结果如下:

在这里插入图片描述

点阴影

点阴影与定向阴影映射类似,只不过点阴影需要将深度信息渲染到立方体贴图上,这个阴影贴图也可以叫万向阴影贴图。下图是点阴影的示意图:

在这里插入图片描述

点阴影的渲染有两个渲染阶段:首先我们生成深度贴图,然后我们正常使用深度贴图渲染,在场景中创建阴影。帧缓冲对象和立方体贴图的处理看起是这样的:

// 1. 首先将场景深度信息渲染到立方体贴图上
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glClear(GL_DEPTH_BUFFER_BIT);
ConfigureShaderAndMatrices();
RenderScene();
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// 2. 之后利用阴影贴图,像往常一样渲染场景
glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
ConfigureShaderAndMatrices();
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubemap);
RenderScene();

深度着色器的构建

为了避免在渲染立方体贴图时,将场景渲染6次,我们可以采用一个几何着色器。在几何着色器中,我们可以把一个三角形变换到上下左后前后六个方向的光源观察空间,绘制出六个三角形。因此在处理时,顶点着色器简单地将顶点变换到世界空间,然后直接发送到几何着色器中。

#version 330 core
layout (location = 0) in vec3 position;

uniform mat4 model;

void main()
{
    gl_Position = model * vec4(position, 1.0);
}

紧接着几何着色器以3个三角形的顶点作为输入,它还有一个光空间变换矩阵的uniform数组。几何着色器接下来会负责将顶点变换到光空间;这里它开始变得有趣了。

几何着色器有一个内建变量叫做gl_Layer,它指定发散出基本图形送到立方体贴图的哪个面。当不管它时,几何着色器就会像往常一样把它的基本图形发送到输送管道的下一阶段,但当我们更新这个变量就能控制每个基本图形将渲染到立方体贴图的哪一个面。当然这只有当我们有了一个附加到激活的帧缓冲的立方体贴图纹理才有效:

#version 330 core
layout (triangles) in;
layout (triangle_strip, max_vertices=18) out;

uniform mat4 shadowMatrices[6];

out vec4 FragPos; // FragPos from GS (output per emitvertex)

void main()
{
    for(int face = 0; face < 6; ++face)
    {
        gl_Layer = face; // built-in variable that specifies to which face we render.
        for(int i = 0; i < 3; ++i) // for each triangle's vertices
        {
            FragPos = gl_in[i].gl_Position;
            gl_Position = shadowMatrices[face] * FragPos;
            EmitVertex();
        }    
        EndPrimitive();
    }
}

几何着色器相对简单。我们输入一个三角形,输出总共6个三角形(6*3顶点,所以总共18个顶点)。在main函数中,我们遍历立方体贴图的6个面,我们每个面指定为一个输出面,把这个面的interger(整数)存到gl_Layer。然后,我们通过把面的光空间变换矩阵乘以FragPos,将每个世界空间顶点变换到相关的光空间,生成每个三角形。注意,我们还要将最后的FragPos变量发送给像素着色器,我们需要计算一个深度值。

最后是片元着色器,我们将在这里计算每一个fragment的深度,这个深度就是每个fragment位置和光源位置之间的线性距离。

#version 330 core
in vec4 FragPos;

uniform vec3 lightPos;
uniform float far_plane;

void main()
{
    // get distance between fragment and light source
    float lightDistance = length(FragPos.xyz - lightPos);

    // map to [0;1] range by dividing by far_plane
    lightDistance = lightDistance / far_plane;

    // write this as modified depth
    gl_FragDepth = lightDistance;
}

之后,我们便可以利用深度图,来渲染万向阴影了。点阴影的渲染结果如下所示:

在这里插入图片描述

如果把立方体贴图深度缓冲渲染出来,结果如下:

在这里插入图片描述

PCF

与定向阴影映射类似,我们可以用PCF来对阴影的边缘进行软化,得出更自然的阴影渲染效果:

在这里插入图片描述

参考

https://learnopengl-cn.github.io/05%20Advanced%20Lighting/03%20Shadows/01%20Shadow%20Mapping/

https://learnopengl-cn.github.io/05%20Advanced%20Lighting/03%20Shadows/02%20Point%20Shadows/

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

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

相关文章

C语言/c++指针详细讲解【超详细】【由浅入深】

指针用法简单介绍 指针&#xff0c;是内存单元的编号。 内存条分好多好多小单元&#xff0c;一个小单元有 8 位&#xff0c;可以存放 8 个 0 或 1&#xff1b;也就是说&#xff0c;内存的编号不是以位算的&#xff0c;而是以字节算的&#xff0c;不是一个 0 或 1 是一个编号&…

【题目】2023年国赛信息安全管理与评估正式赛任务书-模块3 CTF

全国职业院校技能大赛 高等职业教育组 信息安全管理与评估 任务书 模块三 网络安全渗透、理论技能与职业素养 竞赛相关资源资料可在文末关注公众号获得 比赛时间及注意事项 本阶段比赛时长为180分钟&#xff0c;时间为9:00-12:00。 【注意事项】 &#xff08;1&#xf…

做一个简单的倒计时

<div>距离过年还有:<span></span></div><script>let div document.querySelector("div");let span document.querySelector("span");// 获取未来时间戳let future new Date("2024-2-10 00:00:00");// 获取当下…

深度解析Python关键字:掌握核心语法的基石(新版本35+4)

目录 关键字 keyword 关键字列表 kwlist softkwlist 关键字分类 数据类型 True、False None 运算类型 and、or、not in is 模块导入 import 辅助关键字 from、as 上下文管理 with 占位语句 pass 流程控制 if、elif、else for while break、continue…

Android学习之路(23)组件化框架ARouter的使用

一、功能介绍 支持直接解析标准URL进行跳转&#xff0c;并自动注入参数到目标页面中支持多模块工程使用支持添加多个拦截器&#xff0c;自定义拦截顺序支持依赖注入&#xff0c;可单独作为依赖注入框架使用支持InstantRun支持MultiDex(Google方案)映射关系按组分类、多级管理&…

计组原理:系统概论与基本组成

系统概论与基本组成 系统概论硬件软件 计算机系统的层次结构系统复杂性的管理方法1&#xff1a;抽象 计算机的基本组成冯诺依曼计算机系统复杂性的管理方法 2&#xff1a;&#xff08;3’Y&#xff09; 计算机的工作步骤上机前的准备&#xff1a;计算机的解题过程存储器的基本组…

Java List集合使用 Comparator.comparing 排序报空指针异常问题

前言 有时候对一个List集合的某个字段进行排序的时候会报错&#xff0c;问题就是排序的那个字段可能是个空值&#xff0c;那么下面就是处理这种问题的方式&#xff0c;亲自测试有效。 参考 点击可跳转&#xff1a;Java List集合使用 Comparator.comparing 排序报空指针异常问…

TPM模拟器安装

目录 TPM模拟器安装 1&#xff09;安装配置所需依赖 2&#xff09;从官网下载TPM模拟器程序ibmtpm1332.tar.gz 3&#xff09;创建安装目录并将源码解压到对应目录 4&#xff09;进入解压后的目录&#xff0c;然后执行安装命令 5&#xff09;将tpm服务器添到Linux系统执行目…

gradle各版本下载地址

IDEA如何配置 Gradle&#xff08;详细版&#xff09;_idea gradle配置-CSDN博客 Gradle | Releases 参考以上文档

Python中函数的4种参数形式

默认参数的特点是在声明函数时使用“”来指定默认值。缺省参数指因为程序使用了默认值&#xff0c;使得函数调用时不必写出全部参数。 关键字参数可以摆脱位置匹配的限制&#xff0c;直接用变量名匹配。可变参数用于处理任意数量的参数&#xff0c;形参中带一个*&#xff0c;将…

小蓝和小桥的挑战*

题目 import java.util.Scanner;public class Main {public static void main(String[] args) {Scanner sc new Scanner(System.in);int t sc.nextInt();sc.nextLine();while(t-- > 0) {int n sc.nextInt();sc.nextLine();int[] a new int[n];for(int i0;i<n;i)a[i…

Web server failed to start.Port xxxx was already in use.

目录 一、报错截图&#xff1a;二、解决方式 一、报错截图&#xff1a; 某端口被占用,导致出现如下报错&#xff1a; 二、解决方式 windowsR 输入cmd—>回车 如下图所示 查看被占用的端口的进程&#xff0c;如下图&#xff1a; netstat -ano |findstr 端口号结束这个进程…

恭喜CSDN,www.csdn.com域名买回来了!

摘要&#xff1a;www.csdn.com已经可以访问了&#xff0c;恭喜CSDN&#xff0c;迈出国际化的重要一步。 主页面浓浓的国际简约范 静态展示特点 著名的1024 day day up OpenStack & Open-Source 最后附上的是report 已经有众多支持者了&#xff0c;看排序。 博客不多&#…

重拾计网-第三弹 你不知道的计算机的秘密

&#x1f389;欢迎您来到我的MySQL基础复习专栏 ☆* o(≧▽≦)o *☆哈喽~我是小小恶斯法克&#x1f379; ✨博客主页&#xff1a;小小恶斯法克的博客 &#x1f388;该系列文章专栏&#xff1a;重拾计算机网络 &#x1f379;文章作者技术和水平很有限&#xff0c;如果文中出现错…

【MATLAB源码-第120期】基于matlab的GFSK系统调制解调仿真,输出各个节点的波形以及功率谱。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 GFSK&#xff08;高斯频移键控&#xff09;是一种数字调制技术&#xff0c;广泛应用于无线通信领域&#xff0c;尤其是在低功率和短距离通信系统中&#xff0c;例如蓝牙技术。GFSK是频移键控&#xff08;FSK&#xff09;的一…

Vue3+ElementUI 多选框中复选框和名字点击方法效果分离

现在的需求为 比如我点击了Option A &#xff0c;触发点击Option A的方法&#xff0c;并且复选框不会取消勾选&#xff0c;分离的方法。 <el-checkbox-group v-model"mapWork.model_checkArray.value"> <div class"naipTypeDom" v-for"item …

AI+量化02_金融市场的基础概念

文章目录 问答之纯小白 vs GPT4Q1. 请用尽可能简短的语句或例子&#xff0c;给小白讲解宏观经济Q2. 给小白讲解资本边际效率Q3. 如果证券没有风险是否意味着没有收益&#xff1f;Q4. 芒格&#xff1a;教人炒股&#xff0c;犹如引人吸毒Q5. 给小白快速讲解&#xff1a;卡房统计量…

IP 地址如何进行动态分配?

由于 IP 地址资源的有限性&#xff0c;大部分用户上网都是使用动态 IP 地址&#xff0c;而不是静态 IP 地址。动态 IP 地址指的是在需要的时候才进行 IP 地址分配的方式&#xff0c;而静态 IP 地址是固定分配一个 IP 地址&#xff0c;每次都用这一个地址。因此&#xff0c;IP 地…

docker常用基础命令

文章目录 1、Docker 环境信息命令1.1、docker info1.2、docker version 2、系统日志信息常用命令2.1、docker events2.2、docker logs2.3、docker history 3、容器的生命周期管理命令3.1、docker create3.2、docker run 总结 1、Docker 环境信息命令 1.1、docker info 显示 D…

硬件-11-服务器的基础知识

参考服务器基础知识大科普 1 电视剧背景 服务器被誉为互联网之魂。 电视剧《创业年代》是一部有冯绍峰和袁姗姗等人联手主演的一部讲述我国第一批科技创业者创业故事的电视剧&#xff0c;可以说是他们铲下了建设中关村的第一捧土。 电视剧《创业年代》中的潮信公司并没有…