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

news2024/12/22 16:59:49

前言

参照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对象上有基础颜色纹理,可以通过键盘和鼠标来控制相机的移动和旋转。

在这里插入图片描述

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

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

相关文章

Vue3 + Vite + TypeScript + Element-Plus:从零到一构建企业级后台管理系统(前后端开源)

vue3-element-admin 是基于 vue-element-admin 升级的 Vue3 Element Plus 版本的后台管理前端解决方案,技术栈为 Vue3 Vite4 TypeScript Element Plus Pinia Vue Router 等当前主流框架。 相较于其他管理前端框架,vue3-element-admin 的优势在于一…

Python 面向对象之多态和鸭子类型

Python 面向对象之多态和鸭子类型 【一】多态 【1】概念 多态是面向对象的三大特征之一多态:允许不同的对象对同一操作做出不同的反应多态可以提高代码的灵活性,可扩展性,简化代码逻辑 【2】代码解释 在植物大战僵尸中,有寒冰…

vite4项目中,vant兼容750适配

一般非vite项目,使用postcss-px-to-viewport。在设计稿为750时候,可使用以下配置兼容vant。 在vite4项目中,以上配置不行。需要调整下,使用postcss-px-to-viewport-8-plugin,并修改viewportWidth,具体如下…

2 @RequestMapping 注解

1. RequestMapping 概念 SpringMVC 使用RequestMapping 注解为控制器指定可以处理哪些 URL 请求在控制器的类定义及方法定义处都可标注 RequestMapping 标记在类上:提供初步的请求映射信息。相对于 WEB 应用的根目录标记在方法上:提供进一步的细分映射信…

大创项目推荐 深度学习图像风格迁移 - opencv python

文章目录 0 前言1 VGG网络2 风格迁移3 内容损失4 风格损失5 主代码实现6 迁移模型实现7 效果展示8 最后 0 前言 🔥 优质竞赛项目系列,今天要分享的是 🚩 深度学习图像风格迁移 - opencv python 该项目较为新颖,适合作为竞赛课题…

遇见狂神说 Spring学习笔记(完整笔记+代码)

简介 Spring是一个开源的免费的框架(容器)Spring是一个轻量级的、非入侵式的框架控制反转(IOC),面向切面编程 (AOP)支持事务的处理,支持对框架进行整合 Spring就是一个轻量级的控制反转(IOC)和…

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

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

01-线程池项目背景:C++的数据库操作

从0开始学习C与数据库的联动 1.原始方式-使用MySQL Connector/C 提供的API查询 1.1 数据库预操作 我的本地电脑上有mysql数据库,里面预先创建了一个database名叫chat,用户名root,密码password。 1.2 Visual Studio预操作 在Windows上使用…

Linux与C/C++服务器开发:深入探索网络编程与实用技术(文末送书)

🎥 屿小夏 : 个人主页 🔥个人专栏 : 书籍推荐 🌄 莫道桑榆晚,为霞尚满天! 文章目录 📑前言一. 构建高性能Linux C/C服务器1.1 优化服务器性能1.2 处理并发和并行性1.3 高效管理内存1…

dnSpy调试工具二次开发1-新增菜单

测试环境: window 10 visual studio 2019 版本号:16.11.15 .net framework 4.8 开发者工具包 下载 .NET Framework 4.8 | 免费官方下载 .net 5开发者工具包 下载 .NET 5.0 (Linux、macOS 和 Windows) 利用git拉取代码(源码地址:Gi…

TypeError: loaderUtils.getOptions is not a function

webpack 版本:^5.89.0 但是直接 pnpm add loader-utils 安装的版本比较新,会报错:TypeError: loaderUtils.getOptions is not a function。 解决方案:将低 loader-utils 版本,我这里使用 ^2.0.0 就不会再报这个错误了 …

Pandas DataFrame中将True/False映射到1/0

在本文中,我们将看到如何在Pandas DataFrame中将True/False映射到1/0。True/False到1/0的转换在执行计算时至关重要,并且可以轻松分析数据。 1. replace方法 在这个例子中,我们使用Pandas replace()方法将True/False映射到1/0。在这里&…

十大性能测试工具

这篇关于“性能测试工具”的文章将按以下顺序让您了解不同的软件测试工具: 什么是性能测试? 为什么我们需要性能测试? 性能测试的优势 性能测试的类型 十大性能测试工具 什么是性能测试? 性能测试是一种软件测试,可确…

25考研经验贴之准备篇三

Hello各位小伙伴又见面了,今天要给大家分享一些大家在备考中可以用到的软件。 另外前两次分享的一些择校什么的也不够全面,今天又为大家找到了一个全面的考研常识讲解视频,有需要的可以关注公众号,在后台回复:考研常识…

Python自动点击器

一、如何制作一个Python自动点击器? 当用户单击开始键时,代码将从键盘获取输入,并在用户单击退出键时终止自动点击器,自动点击器开始单击指针放置在屏幕上的任何位置。我们将在这里使用pynput模块。 二、什么是自动点击器&#…

Traffic Flow Prediction via Spatial Temporal Graph NeuralNetwork

KEYWORDS Traffic Prediction, Graph Neural Networks, Spatial Temporal Model, Dynamic, Recurrent Neural Network, Transformer This paper is published under the Creative Commons Attribution 4.0 International (CC-BY 4.0) license ABSTRACT 交通流分析、预测和管理…

呕心沥血总结的Python自动化测试面试题

📢专注于分享软件测试干货内容,欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!📢软件测试面试题分享: 1000道软件测试面试题及答案📢软件测试实战项目分享: 纯接口项目-完…

JavaScrip-初识JavaScript-知识点

初识JavaScript 编程基础编程计算机语言标记语言编译器&解释器 计算机基础计算机组成数据存储数据存储单位程序运行 认识JavaScript什么是JavaScriptJavaScript作用HTML&CSS&JavaScript的关系浏览器执行JavaScript过程JavaScript的组成JavaScript初体验 JavaScript…

八大在线项目实习 2024年第一期即将开班

八大项目: 某实习网站招聘信息采集与分析(Python数据采集与分析) 股票价格形态聚类与收益分析(Python金融分析) 某平台网络入侵用户自动识别(Python机器学习) 某平台广东省区采购数据分析&#…

企业微信开发:自建应用:接收消息(企业内部服务器)/回调配置

概述 在企业微信的自建应用中,用户触发了某些行为(发送消息、进行菜单操作或者外部联系人变更等),要发送相关信息给企业内部服务器。 备注:接收消息 和 回调,在本文中指代相同的行为,即企业微信…