从零开始的OpenGL光栅化渲染器构建1

前言

参照Learnopengl,我开始回顾OpenGL中的内容,最终目标是构建一个玩具级的光栅化渲染器,最好还能和之前做的光线追踪渲染器相结合,希望能够有所收获吧~

包管理

之前我用CMake配置过OpenGL的环境,这样做出来的项目比较利于跨平台,但是很麻烦。这里我为了偷懒,使用vcpkg进行C++的包管理。vcpkg的使用教程可以参考这篇博客。因为vcpkg和visual studio都是微软家的,因此,想在visual studio中使用vcpkg中安装的包,只需要一句 ./vcpkg.exe integrate install便可集成,非常方便。

目前需要用到的包:

glfw3: 配合OpenGL使用的轻量级工具程序库,主要功能是创建并管理窗口和 OpenGL 上下文,同时还提供了处理手柄、键盘、鼠标输入的功能。

glad : 用来访问OpenGL规范接口的第三方库。

glm: 一个数据库,可以用来进行向量计算、矩阵计算等运算。

stb: 目前用到的是里面的stb_image.h头文件,用来读取各种图片格式。

渲染器的构建记录

这篇博客里,我将记录下我搭建基础渲染器过程中,遇到的各个知识点。在这个渲染器中,我可以渲染出一个3D场景,场景中包含多个box对象,每个box对象上有基础颜色纹理,可以通过键盘和鼠标来控制相机的移动和旋转。

这篇博客主要参考了LearnOpenGL CN教程中的入门部分。

你好,三角形

在这里插入图片描述

上图是OpenGL中的渲染管线,其中蓝色的部分表示我们可以编辑的部分。在当前阶段,我们主要关注顶点着色器和片段着色器。在渲染中,我们要做的事就是将各种图形数据通过约定的方式输入顶点着色器,然后渲染管线会对输入的数据进行各种计算,最终输出到屏幕上。

那么如何将数据输入顶点着色器呢?

我们可以将输入数据存入顶点缓冲对象(Vertex Buffer Objects, VBO),这个对象会在GPU内存(通常称为显存)中存储大量顶点数据。之后,我们通过指针的方式告诉顶点着色器,顶点中每个属性的位置,这样图形数据就能够顺利输入顶点着色器了。

绘制每一个物体时,我们都需要重复这一过程:将输入存入VBO,通过指针确定每个属性的位置。这看起来可能不多,但是如果有超过5个顶点属性,上百个不同物体呢(这其实并不罕见)。绑定正确的缓冲对象,为每个物体配置所有顶点属性很快就变成一件麻烦事。这时,我们可以利用另一个OpenGL对象:顶点数组对象(Vertex Array Object, VAO)。VAO可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中。

除了上述提到的两个OpenGL对象,我们还有最后一个需要讨论的东西——元素缓冲对象(Element Buffer Object,EBO),也叫索引缓冲对象(Index Buffer Object,IBO)。要解释元素缓冲对象的工作方式最好还是举个例子:假设我们不再绘制一个三角形而是绘制一个矩形。我们可以绘制两个三角形来组成一个矩形(OpenGL主要处理三角形)。这会生成下面的顶点的集合:

float vertices[] = {
    // 第一个三角形
    0.5f, 0.5f, 0.0f,   // 右上角
    0.5f, -0.5f, 0.0f,  // 右下角
    -0.5f, 0.5f, 0.0f,  // 左上角
    // 第二个三角形
    0.5f, -0.5f, 0.0f,  // 右下角
    -0.5f, -0.5f, 0.0f, // 左下角
    -0.5f, 0.5f, 0.0f   // 左上角
};

可以看到,有几个顶点叠加了。我们指定了右下角左上角两次!一个矩形只有4个而不是6个顶点,这样就产生50%的额外开销。当我们有包括上千个三角形的模型之后这个问题会更糟糕,这会产生一大堆浪费。更好的解决方案是只储存不同的顶点,并设定绘制这些顶点的顺序。这样子我们只要储存4个顶点就能绘制矩形了,之后只要指定绘制的顺序就行了。如果OpenGL提供这个功能就好了,对吧?

值得庆幸的是,元素缓冲区对象的工作方式正是如此。 EBO是一个缓冲区,就像一个顶点缓冲区对象一样,它存储 OpenGL 用来决定要绘制哪些顶点的索引。这种所谓的索引绘制(Indexed Drawing)正是我们问题的解决方案。我们可以利用(不重复的)顶点和索引信息,来绘制矩形。在输入时,EBO和VBO类似,也是提前将索引数据复制到缓冲里去。

三种OpenGL对象的关系图如下所示:

在这里插入图片描述

着色器

在OpenGL中,着色器是使用GLSL(OpenGL Shading Language)编写的,为了绘制我们的图形,我们必须要创建一个着色器程序。一个着色器程序至少包含一个顶点着色器和一个片段着色器。在实际编写的过程中,我们需要分别编译顶点着色器和片段着色器,再将两个编译好的着色器链接起来,最终得到我们的着色器程序。

更详细的内容可以参照原教程页面,这里我不再继续贴了。可以关注一下着色器程序中的uniform属性,带uniform记号的属性值是从着色器外部传递的,我们可以在主程序代码中编写传递。

纹理

为了能够把纹理映射(Map)到三角形上,我们需要指定三角形的每个顶点各自对应纹理的哪个部分。这样每个顶点就会关联着一个纹理坐标(Texture Coordinate),用来标明该从纹理图像的哪个部分采样(译注:采集片段颜色)。之后在图形的其它片段上进行片段插值(Fragment Interpolation)。将纹理映射到三角形的示例如下:

textures

纹理环绕方式

纹理坐标的范围通常是从(0, 0)到(1, 1),那如果我们把纹理坐标设置在范围之外会发生什么?OpenGL默认的行为是重复这个纹理图像(我们基本上忽略浮点纹理坐标的整数部分),但OpenGL提供了更多的选择:

环绕方式描述
GL_REPEAT对纹理的默认行为。重复纹理图像。
GL_MIRRORED_REPEAT和GL_REPEAT一样,但每次重复图片是镜像放置的。
GL_CLAMP_TO_EDGE纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。
GL_CLAMP_TO_BORDER超出的坐标为用户指定的边缘颜色。

在这里插入图片描述

纹理过滤

纹理坐标不依赖于分辨率(Resolution),它可以是任意浮点值,所以OpenGL需要知道怎样将纹理像素(Texture Pixel,也叫Texel,译注1)映射到纹理坐标。当你有一个很大的物体但是纹理的分辨率很低的时候这就变得很重要了。

如何将纹理像素映射到纹理坐标,常用的有两种方式,邻近过滤和线性过滤。

GL_NEAREST(也叫邻近过滤,Nearest Neighbor Filtering)是OpenGL默认的纹理过滤方式。当设置为GL_NEAREST的时候,OpenGL会选择中心点最接近纹理坐标的那个像素。下图中你可以看到四个像素,加号代表纹理坐标。左上角那个纹理像素的中心距离纹理坐标最近,所以它会被选择为样本颜色:

在这里插入图片描述

GL_LINEAR(也叫线性过滤,(Bi)linear Filtering)它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。下图中你可以看到返回的颜色是邻近像素的混合色:

在这里插入图片描述

这两种纹理过滤方式的视觉效果如图下图所示:

在这里插入图片描述

多级渐远纹理

想象一下,假设我们有一个包含着上千物体的大房间,每个物体上都有纹理。有些物体会很远,但其纹理会拥有与近处物体同样高的分辨率。由于远处的物体可能只产生很少的片段,OpenGL从高分辨率纹理中为这些片段获取正确的颜色值就很困难,因为它需要对一个跨过纹理很大部分的片段只拾取一个纹理颜色。在小物体上这会产生不真实的感觉,更不用说对它们使用高分辨率纹理浪费内存的问题了。

penGL使用一种叫做多级渐远纹理(Mipmap)的概念来解决这个问题,它简单来说就是一系列的纹理图像,后一个纹理图像是前一个的二分之一。多级渐远纹理背后的理念很简单:距观察者的距离超过一定的阈值,OpenGL会使用不同的多级渐远纹理,即最适合物体的距离的那个。由于距离远,解析度不高也不会被用户注意到。同时,多级渐远纹理另一加分之处是它的性能非常好。让我们看一下多级渐远纹理是什么样子的:

在这里插入图片描述

我个人的理解,当物体距离相机近或者远时,物体上每个顶点的纹理坐标是不变的,多级渐远纹理为我们生成了多个具有不同分辨率的纹理。距离越远,纹理的分辨率就越低。如下图所示,随着物体变远,纹理的分辨率不断降低。

在这里插入图片描述

坐标系统

OpenGL希望在每次顶点着色器运行后,我们可见的所有顶点都为标准化设备坐标(Normalized Device Coordinate, NDC),也就是说,每个顶点的 x , y , z x, y, z x,y,z坐标都应该在-1.0到1.0之间,超过这个坐标范围内的顶点都将不可见。我们通常会自己设定一个坐标范围,之后再再顶点着色器中将这些坐标变换为标准化设备坐标。然后将这些标准化设备坐标传入光栅器,将它们变换为屏幕上的二维坐标或像素。

将坐标变换为标准化设备坐标,接着再转化为屏幕坐标的过程通常是分步进行的,类似于流水线。在物体顶点最终转化为屏幕坐标之前会变换到多个坐标系统,对于我们来说总共有5个比较重要的坐标系统:

  • 局部空间(Local Space, 或者称为物体空间(Object Space))
  • 世界空间(World Space)
  • 观察空间(View Space, 或者称为视觉空间(Eye Space))
  • 裁剪空间(Clip Space)
  • 屏幕空间(Screen Space)

在这里插入图片描述

  1. 局部坐标是对象相对于局部原点的坐标,也是物体起始的坐标。
  2. 下一步是将局部坐标变换为世界空间坐标,世界空间坐标是处于一个更大的空间范围的。这些坐标相对于世界的全局原点,它们会和其它物体一起相对于世界的原点进行摆放。
  3. 接下来我们将世界坐标变换为观察空间坐标,使得每个坐标都是从摄像机或者说观察者的角度进行观察的。
  4. 坐标到达观察空间之后,我们需要将其投影到裁剪坐标。裁剪坐标会被处理至-1.0到1.0的范围内,并判断哪些顶点将会出现在屏幕上。
  5. 最后,我们将裁剪坐标变换为屏幕坐标,我们将使用一个叫做视口变换(Viewport Transform)的过程。视口变换将位于-1.0到1.0范围的坐标变换到由glViewport函数所定义的坐标范围内。最后变换出来的坐标将会送到光栅器,将其转化为片段。

一个顶点坐标将会根据以下过程被变换到裁剪坐标:
V c l i p = M p r o j e c t i o n ⋅ M v i e w ⋅ M m o d e l ⋅ V l o c a l \mathbf V_{clip} = \mathbf M_{projection}\cdot \mathbf M_{view}\cdot \mathbf M_{model}\cdot \mathbf V_{local} Vclip=MprojectionMviewMmodelVlocal

其中投影矩阵的推导过程可以参考我之前发布的博客:正交投影矩阵与透视投影矩阵的推导。

PS: 渲染一个正方体,需要开启深度测试。

相机

OpenGL本身没有相机的概念,我们通过将场景中的所有物体往相反的方向移动和旋转来模拟出摄像机,产生一种我们在移动的感觉,而不是场景在移动。在实际编写过程中,我们通过交互,来影响 M v i e w \mathbf M_{view} Mview这个矩阵,以及 M p r o j e c t i o n \mathbf M_{projection} Mprojection矩阵中 f o v fov fov的值,产生相机的作用。

除了教程中提供的相机的操作设计之外,我增加对相机的平滑处理,包括移动平滑、旋转平滑、zoom平滑。

实现的思路就是保存一组相机移动、旋转、zoom上一步的delta值,在传入对应的移动、旋转、zoom平滑的交互操作时,将获得的delta值与上一步的delta值做线性插值处理。

举个例子,对于移动来说,我首先设置一个 p r e D e l t a P o s preDeltaPos preDeltaPos,初值为0。当我按下了 W \mathbf W W键之后,意味着相机向前方移动,此时我们可以获得一个 c u r D e l t a P o s curDeltaPos curDeltaPos。如果不进行平滑处理的话,我们将相机的位置直接加上这个 c u r D e l t a P o s curDeltaPos curDeltaPos。如果进行平滑处理的话,我们让相机的位置加上 t ∗ p r e D e l t a P o s + ( 1 − t ) ∗ c u r D e l t a P o s t * preDeltaPos + (1 - t) * curDeltaPos tpreDeltaPos+(1t)curDeltaPos的值,即 ( 1 − t ) ∗ c u r D e l t a P o s (1 - t) * curDeltaPos (1t)curDeltaPos p r e D e l t a P o s preDeltaPos preDeltaPos等于0)。这样的话,在启动时不至于太突兀,相机会有一种缓慢启动的效果。

最终渲染结果

正如我上面提到的,在这个渲染器中,我可以渲染出一个3D场景,场景中包含多个box对象,每个box对象上有基础颜色纹理,可以通过键盘和鼠标来控制相机的移动和旋转。

在这里插入图片描述

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

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

相关文章

【Java EE初阶六】多线程案例(单例模式)

1. 单例模式 单例模式是一种设计模式,设计模式是我们必须要掌握的一个技能; 1.1 关于框架和设计模式 设计模式是软性的规定,且框架是硬性的规定,这些都是技术大佬已经设计好的; 一般来说设计模式有很多种,…

抖音字幕视频怎么做能滚动 抖音个性字幕怎么做 抖音短视频用什么软件剪辑

不管是抖音短视频,还是其他影视网站的影视剧,字幕基本都是必不可少的,字幕本身就能加强观众对视频的理解,而且像一些滚动字幕,会更加吸引观众的注意力,那抖音字幕视频怎么做能滚动?抖音个性字幕…

Hierarchical Clusting模型

介绍: Hierarchical Clustering 是一种常用的聚类方法,它通过构建一个层次化的聚类树(或者称为聚类图),将数据点逐步合并组成不同的聚类簇。 Hierarchical Clustering 的主要思想是将相似的数据点归为一类&#xff0c…

动态规划(不同路径1,不同路径2,整数拆分)

62.不同路径 力扣题目链接(opens new window) 一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。…

有什么安全处理方案可以有效防护恶意爬虫

常见的爬虫 有百度爬虫、谷歌爬虫、必应爬虫等搜索引擎类爬虫,此类爬虫经常被企业用于提高站点在搜索引擎内的自然排名,使得站点在各大搜索引擎中的排名能够提高,进一步通过搜索引擎来进行引流为企业增加业务流量。 恶意爬虫与合法、合规的搜…

资源类的使用(MFC)

文章目录 1.预备知识1.菜单1.创建菜单在系统自动生成的菜单资源中添加一个主菜单命令菜单属性 2.编辑菜单过程中所涉及的操作3.菜单设计步骤4.菜单的响应和消息路由5.CMenu类获取菜单指针添加菜单项删除菜单项获取菜单项数目获取菜单ID号对菜单项属性的修改显示快捷菜单 2.工具…

Reids原理及简单命令

目录 1.关系数据库与非关系型数据库 关系型数据库 非关系型数据库 关系型数据库和非关系型数据库区别 数据存储方式不同 扩展方式不同 对事务性的支持不同 总结: 2. Redis简介 什么是redis reids优点 reids使用场景: reids快的原因 Redis数…

Ubuntu 虚拟机挂接 Windows 目录

Windows 共享目录 首先 Windows 下共享目录 我这里偷懒直接直接 Everyone ,也可以指定用户啥的 Ubuntu 挂接 挂接命令,类似如下: sudo mount -o usernamefananchong,passwordxxxx,uid1000,gid1000,file_mode0644,dir_mode0755,dynperm //…

不要告诉别人的passwd

文章目录 不要告诉别人的passwd修改或更新密码删除用户密码查看密码的状态更多信息 不要告诉别人的passwd passwd用于创建或者更新用户密码,是管理员必备的命令之一。 这个命令最终的实现是通过调用Linux-PAM 和Libuser API来实现的。 官方的定义为: …

uniapp微信小程序投票系统实战 (SpringBoot2+vue3.2+element plus ) -小程序首页实现

锋哥原创的uniapp微信小程序投票系统实战: uniapp微信小程序投票系统实战课程 (SpringBoot2vue3.2element plus ) ( 火爆连载更新中... )_哔哩哔哩_bilibiliuniapp微信小程序投票系统实战课程 (SpringBoot2vue3.2element plus ) ( 火爆连载更新中... )共计21条视频…

【2023 CCF 大数据与计算智能大赛】基于TPU平台实现超分辨率重建模型部署 基于QuickRNet的TPU超分模型部署

2023 CCF 大数据与计算智能大赛 《赛题名称》 基于QuickRNet的TPU超分模型部署 巴黎欧莱雅 林松 智能应用业务部算法工程师 中信科移动 中国-北京 gpu163.com 团队简介 巴黎欧莱雅团队包含一个队长和零个队员。 队长林松,研究生学历,2019-202…

【C++】内存对齐

本篇文章介绍C中的内存对齐,后续介绍C的union和C的variant的时候,需要用到这部分的知识。 占用内存 先回忆下C各个数据类型占用的内存大小: int:所占内存大小:4byte 32bit;char:所占内存大小…

视频智能分析支持摄像头异常位移检测,监测摄像机异常位移变化,保障监控状态

我们经常在生产场景中会遇到摄像头经过风吹日晒,或者异常的触碰,导致了角度或者位置的变化,这种情况下,如果不及时做出调整,会导致原本的监控条件被破坏,发生事件需要追溯的时候,查不到对应位置…

Kubernetes复习总结(一):Kubernetes内置资源、Device Plugin机制

1、Kubernetes内置资源 1)、Pod Pod是Kubernetes进行管理的最小单元,程序要运行必须部署在容器中,而容器必须存在于Pod中 Pod可以认为是容器的封装,一个Pod中可以存在一个或者多个容器 1)Pod进程组 在Kubernetes里面…

cookie和session、请求转发和重定向

会话 分为有状态会话和无状态会话 在HTML中,"会话"一般指的是Web服务器与客户端(通常是浏览器)之间进行的一系列请求和响应。它是一种在网络上模拟人与人之间通信的方式,常见于Web应用程序中。 会话、Cookie和Sessio…

Vue电商后端管理API接口测试

引言 最近有人在学习接口自动化测试时没有接口练手,其实接口的话,要么找第三方提供的,要么自己开发。第三方在线API需要认证,并且普通的话每天调用次数有一定的限制。自己开发的话,只要不停电,想怎么用就怎…

Jmeter接口自动化测试 :Jmeter变量的使用

在使用jmeter进行接口测试时,我们难免会遇到需要从上下文中获取测试数据的情况,这个时候就需要引入变量了。 定义变量 添加->配置元件->用户自定义的变量 添加->配置元件->CSV 数据文件设置 变量的调用方式:${变量名} 变量的作…

低代码平台的崛起:探索火爆背后的因素

文章目录 前言低代码开发平台优缺点有哪些?速度稳定性赋能一致性安全简单低代码为什么能火?由哪些因素导致? 低代码的优势后记 前言 在当前科技发展快速的时代,低代码开发平台越来越受到关注和推崇。与传统的软件开发方式相比&am…

C++学习笔记——类作用域和抽象数据类型

目录 一、C类作用域 类内作用域 类外作用域 二、类作用域案列详细的解释说明 三、抽象数据类型 四、总结 类作用域 抽象数据类型(ADT) 五、图书馆管理系统 一、C类作用域 在C中,类作用域是指类定义中声明的标识符(成员变…

我建立了一个资源分享群

我建立了一个资源分享群 在为寻找资源犯愁? 在为分享资源犯愁? 一起加入分享资源群(是wx群哦)吧!你可以分享自己的资源帮助他人。你可以在群组里需求资源获取别人的帮助。发广告请绕行,会被拉黑哦 微信…