计算机图形学 | 实验十一:阴影计算

news2024/11/19 4:42:52

计算机图形学 | 实验十一:阴影计算

  • 计算机图形学 | 实验十一:阴影计算
    • 帧缓冲
      • 创建一个帧缓冲
      • 纹理附件
      • 渲染缓冲对象附件
      • 总结
    • 阴影映射
      • 算法思想
      • 深度贴图
      • 渲染阴影
      • 抗锯齿
    • assimp库
    • 结果

华中科技大学《计算机图形学》课程

MOOC地址:计算机图形学(HUST)

计算机图形学 | 实验十一:阴影计算

帧缓冲

创建一个帧缓冲

和OpenGL其他对象一样,我们使用glGenFramebuffers的函数来创建一个帧缓冲对象:

unsigned int fbo; 
glGenFramebuffers(1, &fbo);
在这里插入代码片

它的使用函数也和其它的对象类似。首先我们创建一个帧缓冲对象,将它绑定为激活的(Active)帧缓冲,做一些操作,之后解绑帧缓冲。我们使用glBindFramebuffer来绑定帧缓冲。

glBindFramebuffer(GL_FRAMEBUFFER, fbo);

在完成上面的操作后,我们还是不能直接使用帧缓冲,因为一个完整的帧缓冲至少需要满足以下条件:

  • 附加至少一个缓冲(颜色、深度或模板缓冲)。
  • 至少有一个附件(Attachment)。
  • 所有的附件都必须是完整的(保留了内存)。

由于我们的帧缓冲不是默认帧缓冲,渲染指令将不会对窗口的视觉输出有任何影响。出于这个原因,渲染到一个不同的帧缓冲被叫做离屏渲染(Off-screen Rendering)。要保证所有的渲染操作在主窗口中有视觉效果,我们需要再次激活默认帧缓冲,将它绑定到0:

glBindFramebuffer(GL_FRAMEBUFFER, fbo);

当然,在完成所有的帧缓冲操作之后,不要忘记删除这个帧缓冲对象:

glDeleteFramebuffers(1, &fbo);

纹理附件

当把一个纹理附加到帧缓冲的时候,所有的渲染指令将会写入到这个纹理中,就想它是一个普通的颜色/深度或模板缓冲一样。使用纹理的优点是,所有渲染操作的结果将会被储存在一个纹理图像中,我们之后可以在着色器中很方便地使用它。

为帧缓冲创建一个纹理和创建一个普通的纹理步骤类似:

unsigned int texture; 
glGenTextures(1, &texture); 
glBindTexture(GL_TEXTURE_2D, texture); 
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 800, 600, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL); 
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); 
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

创建好纹理后,接下来我们把它附加到帧缓冲上:

glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);

glFrameBufferTexture2D有以下的参数:

  • target:帧缓冲的目标(绘制、读取或者两者皆有)
  • attachment:我们想要附加的附件类型。当前我们正在附加一个颜色附件。注意最后的0意味着我们可以附加多个颜色附件。如ATTACHMENT1,2…
  • textarget:你希望附加的纹理类型
  • texture:要附加的纹理本身
  • level:多级渐远纹理的级别。我们将它保留为0。

除了颜色附件之外,我们还可以附加一个深度和模板缓冲纹理到帧缓冲对象中。要附加深度缓冲的话,我们将附件类型设置为GL_DEPTH_ATTACHMENT。注意纹理的格式(Format)和内部格式(Internalformat)类型将变为GL_DEPTH_COMPONENT,来反映深度缓冲的储存格式。要附加模板缓冲的话,你要将第二个参数设置为GL_STENCIL_ATTACHMENT,并将纹理的格式设定为GL_STENCIL_INDEX。

渲染缓冲对象附件

渲染缓冲对象(Renderbuffer Object)是在纹理之后引入到OpenGL中,作为一个可用的帧缓冲附件类型的,所以在过去纹理是唯一可用的附件。和纹理图像一样,渲染缓冲对象是一个真正的缓冲,即一系列的字节、整数、像素等。渲染缓冲对象附加的好处是,它会将数据储存为OpenGL原生的渲染格式,它是为离屏渲染到帧缓冲优化过的。

创建一个渲染缓冲对象和帧缓冲类似:

unsigned int rbo;
glGenRenderbuffers(1, &rbo);

绑定操作:

glBindRenderbuffer(GL_RENDERBUFFER, rbo);

由于渲染缓冲对象通常都是只写的,它们会经常用于深度和模板附件,因为大部分时间我们都不需要从深度和模板缓冲中读取值,只关心深度和模板测试。我们需要深度和模板值用于测试,但不需要对它们进行采样,所以渲染缓冲对象非常适合它们。当我们不需要从这些缓冲中采样的时候,通常都会选择渲染缓冲对象,因为它会更优化一点。

创建一个深度和模板渲染缓冲对象可以通过调用glRenderbufferStorage函数来完成:

glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);

GL_DEPTH24_STENCIL8作为内部格式,它封装了24位的深度和8位的模板缓冲。

最后附加这个渲染缓冲对象:

glFramebufferRenderbuffer(GL_FRAMEBUFFER,GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);

总结

在了解帧缓冲原理后,我们将会将场景渲染到一个附加到帧缓冲对象上的颜色纹理中,之后将在一个横跨整个屏幕的四边形上绘制这个纹理。我们将在第二部分的阴影实验中用到,接下来简单介绍下这个步骤:

  1. 创建并绑定一个帧缓冲;
  2. 创建一个纹理图像并把它作为附件绑定到帧缓冲上;
  3. 将纹理附件(或渲染对象附件)附加到帧缓冲上;
  4. 检查帧缓冲是否完整(检测是否出现异常);
  5. 在帧缓冲上进行绘制;
  6. 解绑 && 删除工作;

阴影映射

算法思想

阴影映射(Shadow Mapping)背后的思路非常简单:具体过程是我们从光源视角出发(以光源所在位置作为摄像机位置),绘制一条射线到达场景上各个片元,得到射线第一次击中的那个物体,然后用这个最近点和射线上其他点进行对比。然后我们将测试一下看看射线上的其他点是否比最近点更远,如果是的话,这个点就在阴影中。我们使用深度缓冲来实现阴影。

深度缓冲里的一个值是摄像机视角下,对应于一个片元的一个0到1之间的深度值。我们从光源的透视图来渲染场景,并把深度值的结果储存到纹理中。通过这种方式,我们就能对光源的透视图所见的最近的深度值进行采样。最终,深度值就会显示从光源的透视图下见到的第一个片元了。我们管储存在纹理中的所有这些深度值,叫做深度贴图(depth map)或阴影贴图。

深度贴图

第一步我们需要生成一张深度贴图(Depth Map)。深度贴图是从光的透视图里渲染的深度纹理,用它计算阴影。因为我们需要将场景的渲染结果储存到一个纹理中,因此需要用到帧缓冲。按照第一节的帧缓冲使用步骤来使用:

首先,我们要为渲染的深度贴图创建一个帧缓冲对象:

GLuint depthMapFBO;
glGenFramebuffers(1, &depthMapFBO);

创建一个2D纹理,提供给帧缓冲的深度缓冲使用:

const GLuint SHADOW_WIDTH = 1024, SHADOW_HEIGHT = 1024;
GLuint depthMap;
glGenTextures(1, &depthMap); 
glBindTexture(GL_TEXTURE_2D, depthMap); 
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT,SHADOW_WIDTH, SHADOW_HEIGHT, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL); 
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);

我们只关心深度值,我们要把纹理格式指定为GL_DEPTH_COMPONENT,1024是我们预先设置的深度贴图的解析度。

绑定帧缓冲,并将纹理附件附加到帧缓冲上:

glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthMap, 0); 
glDrawBuffer(GL_NONE); 
glReadBuffer(GL_NONE); 
glBindFramebuffer(GL_FRAMEBUFFER, 0);

我们需要的只是在从光的透视图下渲染场景的时候深度信息,所以颜色缓冲没有用。然而帧缓冲对象不是完全不包含颜色缓冲的,所以我们需要显式告诉OpenGL我们不适用任何颜色数据进行渲染。调用glDrawBuffer和glReadBuffer把读和绘制缓冲设置为GL_NONE来做这件事。

接下来我们渲染深度贴图,主要分为两个步骤:

  • 光源空间的变换
  • 渲染至深度贴图
  1. 光源空间的变换,这一部分的变换在着色器内完成
    本次实验我们使用的是一个所有光线都平行的定向光。出于这个原因,我们将为光源使用正交投影矩阵:
GLfloat near_plane = 1.0f, far_plane = 7.5f; 
glm::mat4 lightProjection = glm::ortho(-10.0f, 10.0f, -10.0f, 10.0f, near_plane, far_plane);

其次创建一个视图矩阵来变换每个物体,把它们变换到从光源视角可见的空间中,我们将使用glm::lookAt函数;这次从光源的位置看向场景中央:

glm::mat4 lightView = glm::lookAt(glm::vec(-2.0f, 4.0f, -1.0f), glm::vec3(0.0f), glm::vec3(1.0));

二者相结合为我们提供了一个光空间的变换矩阵,它将每个世界空间坐标变换到光源处所见到的那个空间:

glm::mat4 lightSpaceMatrix = lightProjection * lightView;
  1. 进行一次渲染,形成深度贴图
// 渲染阴影深度贴图
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO); 
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT); 
glClear(GL_DEPTH_BUFFER_BIT); 
shadowMap_shader.Use(); 
shadowMap_shader.SetMat4("lightPV", lightPV); 
DrawScene(shadowMap_shader, current_frame); 
glBindFramebuffer(GL_FRAMEBUFFER, 0);

此时顶点着色器内容即为(1)中的空间变换,而片段着色器则什么也不做,如下所示:

void main()
{
	// gl_FragDepth = gl_FragCoord.z;
}

因为其内置变量gl_FragDepth会自动更新深度信息。

在光的透视图视角下,很完美地用每个可见片元的最近深度填充了深度缓冲。通过将这个纹理投射到一个2D四边形上,就能在屏幕上显示出来,显示结果如下:

在这里插入图片描述

渲染阴影

生成深度贴图以后我们就可以开始生成阴影了。这段代码在像素着色器中执行,用来检验一个片元是否在阴影之中,不过我们在顶点着色器中进行光空间的变换:

void main()
{
	//... 
	LightSpaceFragPos = lightPV * vec4(FragPos, 1.0f);
	gl_Position = projection * view * vec4(FragPos, 1.0f);
	//...
}

LightSpaceFragPos这个输出向量。我们用同一个lightSpaceMatrix,把世界空间顶点位置转换为光空间,将这个输出给片段着色器,进行深度的比较。

像素着色器使用Blinn-Phong光照模型渲染场景。我们接着计算出一个shadow值,当fragment在阴影中时是1.0,在阴影外是0.0。

计算阴影:

float ShadowCalculation(vec4 fragPosLightSpace) 
{
	vec3 projCoords = fragPosLightSpace.xyz /fragPosLightSpace.w;
	projCoords = projCoords * 0.5 + 0.5;
	// 光的位置视野下最近的深度
	float closestDepth = texture(depthMap, projCoords.xy).r;
	// 片元的当前深度
	float currentDepth = projCoords.z;
	// 阴影偏移,防止阴影失真
	float bias = 0.005;
	float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0;
	return shadow;
}

上面即为片段着色器的相关代码,在计算阴影中我们要注意几个问题:

  1. 顶点着色器输出一个裁切空间顶点位置到gl_Position时,OpenGL自动进行一个透视除法,将裁切空间坐标的范围-w到w转为-1到1,这要将x、y、z元素除以向量的w元素来实现。由于裁切空间的FragPosLightSpace并不会通过gl_Position传到像素着色器里,我们必须自己做透视除法。
vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;

来自深度贴图的深度在0到1的范围,我们也打算使用projCoords从深度贴图中去采样,所以我们将NDC坐标变换为0到1的范围:

projCoords = projCoords * 0.5 + 0.5;

在渲染阴影时,很容易出现阴影失真的问题,表现为交替黑线,形成原因如下:

在这里插入图片描述

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

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

在这里插入图片描述

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

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

抗锯齿

如果你放大看阴影,在阴影映射边缘你可以看到很明显的锯齿:

在这里插入图片描述

因为深度贴图有一个固定的解析度,多个片元对应于一个纹理像素。结果就是多个片元会从深度贴图的同一个深度值进行采样,这几个片元便得到的是同一个阴影,这就会产生锯齿边;

可以通过增加解析度来降低锯齿。

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

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使用后效果如图:

在这里插入图片描述

assimp库

引擎开发五: Assimp库及使用

Assimp的安装编译及使用过程全纪录(VS2015)(适合菜鸟看的超详细记录)

error LNK2019: 无法解析的外部符号 _stbi_load,该符号在函数 _main 中被引用

结果

在这里插入图片描述

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

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

相关文章

分布式ID解决方案(一)数据库号段方式

一、前言 在一些简单系统中&#xff0c;我们可以直接使用数据库ID自增方式来标识和保存数据&#xff0c;但是随着系统的逐渐复杂&#xff0c;数据量的日益增多&#xff0c;我们可能需要对数据表、数据库实现分库分表。单纯的使用数据库的ID自增无法满足业务场景了&#xff0c;所…

Seata 的可观测实践

作者&#xff1a;察溯 Seata 简介 Seata 的前身是阿里巴巴集团内大规模使用保证分布式事务一致性的中间件&#xff0c;Seata 是其开源产品&#xff0c;由社区维护。在介绍 Seata 前&#xff0c;先与大家讨论下我们业务发展过程中经常遇到的一些问题场景。 业务场景 我们业务…

数据规模缩小 200 倍!指令微调高效指导大模型学习

夕小瑶科技说 原创 作者 | 智商掉了一地、Python 最近大型语言模型&#xff08;LLMs&#xff09;的指令微调备受研究人员的关注&#xff0c;因为它可以开发 LLM 遵循指令的潜力&#xff0c;使其更加符合特定的任务需求。虽然指令微调&#xff08;Instruction Tuning&#xff…

JavaEE-HTTPS的加密流程

目录 对称加密非对称加密证书的引入 对称加密 对称加密就是用同一个密钥把明文进行加密变成密文,也能把密文解密为明文. 理想状态下: 引入对称加密之后, 即使数据被截获, 由于黑客不知道密钥是啥, 因此就无法进行解密, 也就不知道请求的真实内容是啥了. 但同一时刻服务器服务…

数据库规范与SQL调优

数据库设计规范章节&#xff0c;依旧以《阿里巴巴Java开发手册》为原型进行修正和完善。 MySQL规约 (一) 建表规约 (二) 索引规约 (三) SQL规约 (四) ORM规约 (一) 建表规约 1. 【强制】 表达是与否概念的字段&#xff0c;必须使用is_xxx的方式命名&#xff0c;数据类型是…

Windows修改为Mac的字体方法

一: 首先下载字体文件和修改器 Mac字体修改 https://www.aliyundrive.com/s/KKvcRNYkP5p 提取码: 6d3p 点击链接保存&#xff0c;或者复制本段内容&#xff0c;打开「阿里云盘」APP &#xff0c;无需下载极速在线查看&#xff0c;视频原画倍速播放。 二: 设置并修改字体 1:…

服务(第二十八篇)rsync

配置rsync源服务器&#xff1a; #建立/etc/rsyncd.conf 配置文件 vim /etc/rsyncd.conf #添加以下配置项 uid root gid root use chroot yes #禁锢在源目录 address 192.168.80.10 …

浅谈管网抢维修效率对产销差率的影响

1 背景 多年来&#xff0c;漏损治理工作一直围绕检漏、分区计量或压力管理等相关话题&#xff0c;却忽视了抢维修速度与质量对漏损治理成效的影响。实际上&#xff0c;不管是DMA分区计量&#xff0c;还是检漏&#xff0c;最终还是要通过抢维修来修复漏点达到控制漏损的目的。尽…

Vue 3中利用UseStorage轻松实现本地存储功能,释放数据持久化的力量

✅创作者&#xff1a;陈书予 &#x1f389;个人主页&#xff1a;陈书予的个人主页 &#x1f341;陈书予的个人社区&#xff0c;欢迎你的加入: 陈书予的社区 &#x1f31f;专栏地址: 三十天精通 Vue 3 文章目录 一、介绍1.1 什么是本地存储1.2 Vue 3中的UseStorage插件简介 二、…

第18章_MySQL8其它新特性

第18章_MySQL8其它新特性 1. MySQL8新特性概述 MySQL从5.7版本直接跳跃发布了8.0版本&#xff0c;可见这是一个令人兴奋的里程碑版本。MySQL 8版本在功能上做了显著的改进与增强&#xff0c;开发者对MySQL的源代码进行了重构&#xff0c;最突出的一点是多MySQL Optimizer优化器…

MySQL-1-SQL语句的分类、MySQL命令、SQL查询语句

一、SQL语句的分类&#xff08;任何一条sql语句以分号结尾&#xff1b;SQL语句不区分大小写&#xff09; DQL&#xff08;数据查询语言&#xff09;&#xff1a;查询语句&#xff0c;凡是select都是DQL。 DML&#xff08;数据操作语言&#xff09;&#xff1a;insert、delete、…

Midjourney 介绍-AI绘画工具

《Midjourney》是一款2022年3月面世的AI绘画工具&#xff0c;创始人是David Holz。 它一款基于浏览器的在线应用程序&#xff0c;因此你无需安装任何软件&#xff0c;只需在浏览器中访问MidJourney的官方网站即可开始使用。 只要输入想到的文字&#xff0c;就能通过人工智能产出…

一文带你了解MySQL之基于成本的优化

前言 本文章收录在MySQL性能优化原理实战专栏&#xff0c;点击此处查看更多优质内容。 目录 一、什么是成本二、单表查询的成本2.1 准备数据2.2 基于成本的优化步骤2.3 基于索引统计数据的成本计算 三、连接查询的成本2.1 准备数据2.2 Condition filtering介绍2.3 多表连接的成…

『MySQL 实战 45 讲』16 - “order by” 是怎么工作的

“order by” 是怎么工作的 首先创建一个表 CREATE TABLE t ( id int(11) NOT NULL, city varchar(16) NOT NULL, name varchar(16) NOT NULL, age int(11) NOT NULL, addr varchar(128) DEFAULT NULL, PRIMARY KEY (id), KEY city (city) ) ENGINEInnoDB;全字段排序 在 cit…

正确甄别API、REST API、RESTful API和Web Service之间的异同

看到API你会想起什么&#xff1f;是接口、第三方调用、还是API文档&#xff1f;初看你可能会觉得这太熟悉了&#xff0c;这不是系统开发日常系列吗&#xff1f;但你仔细想一想&#xff0c;你会发现API的概念在你脑海里是如此的模糊。如何你通过搜索引擎检索API&#xff0c;你会…

目标检测数据预处理——部件截图,按一定比例进行外扩

本片是截图的篇的升级版本&#xff0c;简单版本的截图请参考根据目标框外扩一定比例进行截图&#xff08;连带标签&#xff09;。 对目标框&#xff08;类别名称&#xff09;进行分类&#xff0c;将同一类的目标框进行截图并分类保存在不同的文件夹中。 在本篇当中&#xff0c;…

Vue3中响应式Reactive的独特之处:它在哪些场景下胜出Ref?

✅创作者&#xff1a;陈书予 &#x1f389;个人主页&#xff1a;陈书予的个人主页 &#x1f341;陈书予的个人社区&#xff0c;欢迎你的加入: 陈书予的社区 &#x1f31f;专栏地址: 三十天精通 Vue 3 文章目录 一、Vue 3中响应式Reactive的独特之处1.1 引言1.2 Vue 3中的响应式…

算法leetcode|51. N 皇后(rust重拳出击)

文章目录 51. N 皇后&#xff1a;样例 1&#xff1a;样例 2&#xff1a;提示&#xff1a; 分析&#xff1a;题解&#xff1a;rust&#xff1a;go&#xff1a;c&#xff1a;python&#xff1a;java&#xff1a; 51. N 皇后&#xff1a; 按照国际象棋的规则&#xff0c;皇后可以…

详解c++STL—STL常用算法

目录 1、常用遍历算法 1.1、for_each 1.2、transform 2、常用查找算法 2.1、find 2.2、find_if 2.3、adjacent_find 2.4、binary_search 2.5、count 2.6、count_if 3、常用排序算法 3.1、sort 3.2、random_shuffle 3.3、merge 3.4、reverse 4、常用拷贝和替换算…

在MyBatis XML文件中处理特殊符号的方法,如“>”、“<”、“>=”、“<=”这些符号XML会报错如何处理

前言 在MyBatis的XML映射文件中&#xff0c;我们经常需要使用特殊符号&#xff0c;比如"大于"、"小于"、"大于等于"、"小于等于"等比较操作符。然而&#xff0c;这些符号在XML中具有特殊的含义&#xff0c;因此需要进行特殊处理&…