花瓣小跑 发表于 前天 00:31

游戏引擎学习第235天:在 Windows 上初始化 OpenGL

希奇有问题 之前没注意到

这个问题是Count == 0
https://i-blog.csdnimg.cn/direct/8a3c629241144c529d65db79e45f2448.png#pic_center
https://i-blog.csdnimg.cn/direct/33d99497ecc140d88defc76d79a6a237.png#pic_center
https://i-blog.csdnimg.cn/direct/2c76882e82764a23a76b0c9bdc322bab.png#pic_center
GlobalConstants_Renderer_UsedDebugCamer 打开的话会有Bug
https://i-blog.csdnimg.cn/direct/c0dabe9fb5e243a78e216d54c0acc980.png#pic_center
https://i-blog.csdnimg.cn/direct/210093137d36434d905a413414211110.png#pic_center
https://i-blog.csdnimg.cn/direct/d23be1d063164250838fea9ecfa9ba2b.png#pic_center
https://i-blog.csdnimg.cn/direct/99683afd1fa44936a66f157458a885f8.png#pic_center
Count是零的话就不让排序了
https://i-blog.csdnimg.cn/direct/6598a4097b6c46648e4c75facb6914e0.png#pic_center
game.h: 查阅 TODO 列表

大家好,接待来到 game Hero,这是一档我们在直播中一起编写完备游戏的节目。不幸的是,我们昨天完成了之前的工作。完成任务固然好,但这也意味着本日我们并不清楚接下来应该做什么。我们并不知道接下来最符合的任务是什么。因此,我现在需要检察一下任务清单,看看我们本日有什么事变可以做。我们有很多选择,做的事变不在少数,虽然不能说我们已经完成了所有的工作,但确实有很多事变还可以继承做。
我们已经做了一些事变,包括去掉了偶数扫描线标记法,并且添加了排序功能。这个排序功能因为某些原因好像一直没有在代码中完成,因此现在的问题是我们该如何继承推进。这里是我们可以做的所有任务的清单,这个清单是我们想到的一些需要做的事变,虽然它不完全,但它展示了我们想到的一些点:我们想做这个,想做那个。因此,接下来我们可以选择去做的一些事变。比如说我们可以归去美满调试代码,因为我确实想在某个时间点把它做完。另有一些小任务,比如调试音频相关的代码、处理线程等。
其实我本身也不确定接下来要做什么,以是我在想,也许是时候让整个项目朝着更接近发布的状态发展了,至少是在一些清理工作上。因为实际上距离我们制作一个完备游戏已经不远了,很多任务根本上都是围绕游戏本身的内容,更多的是游戏的开辟和内容,而不再是底层的基础架构。因此,我有一个疑问,也许现在是时候将任务聚焦到 Win32 平台层,参加 OpenGL 支持。这样一来,我们就能有一个符合的合成层,通过 Windows 来获得垂直同步(vsync),从而确保我们在 60 帧或 30 帧的固定帧率下运行游戏,而不用再担心计时精度等问题。这样,我们就能在制作游戏时不再被非固定帧率的问题困扰。
win32_game.cpp: 将硬件加速提到 TODO 列表的顶部并开始实施

我们本日继承举行排序的相关工作,虽然根本已经完成了,但照旧想收尾一下,让整个流程更完备。其实这次排序只是个捏词,主要是想顺便聊一聊一些计算机科学的知识点,这些内容在我们项目中平常不会特别讲,更多是得当做一些自学延伸的内容。
回顾上周五的进度,当时我们实验实现就地归并排序(merge sort in-place),结果发现这个做法并不可行。原因是操作成本太高,为了实现就地排序,我们必须执行大量的数据移动,而这些操作虽然逻辑上不复杂,但代价很大。
因此我们最终放弃了就地排序的实验,转而接纳了非就地的方式来实现归并排序。这个版本非常轻易实现,我们在短短几分钟内就完成了代码编写,而且运行结果精良。
现在这部分排序已经可以正常工作,接下来就可以继承在已有基础上推进后续的开辟了。
运行游戏并注意到希奇的淡出结果

现在我们有一个有点希奇的淡入淡出结果,这个机制是后来加上的,当时是出于一个人的想法,想让画面从 Windows 桌面上淡入淡出,我们也没特别的理由,就是觉得可以做一下,于是就实现了这个结果。
现在的逻辑是,画面可以实现双向的淡入淡出,比如启动时从黑屏淡入,退出时再淡出回到桌面。这个结果其实不是直接在主窗口上做的,我们实际上是创建了一个专门用于淡入淡出的窗口,它的唯一作用就是把整个屏幕盖住,用黑色举行淡入淡出处理。
而真正用来渲染游戏内容的窗口是另一个,我们创建这个窗口时,让它的尺寸和整个屏幕一样大,也就是说它占满了全屏,但一开始是不可见的。等准备好后,我们再把这个窗口设置为可见状态,然后通过 BitBlt(图像块传输)等方式将画面绘制到这个窗口中,从而实现游戏画面的表现。
以是,如果我们之后还需要对这个表现流程做调整,比如修改启动流程或添加更多的渲染逻辑,就需要注意我们现在是用了两个窗口:一个用于处理淡入淡出的视觉结果,另一个是实际负责表现游戏内容的主窗口。这个计划使得我们可以把视觉过渡和实际游戏逻辑解耦,方便管理和优化。
为 OpenGL 设置环境

我们接下来无论是使用 OpenGL 照旧 Direct3D,都需要做一些额外的准备工作,才气让 Windows 系统知道我们将要使用 3D 图形硬件举行渲染。
第一步其实相对简单,就是在项目中链接符合的系统库。之前我们已经做过类似的事变,比如列出我们所依赖的 Windows 系统服务。需要明确一点,这些库并不是我们平常所明白的那种“包含实际函数实现的库”,而是“导入库(import libraries)”。这些导入库的作用是资助我们毗连到操作系统提供的功能接口上。
举例来说:


[*]user32.lib 里面包含的是窗口相关的服务,例如 CreateWindow 这样的函数;
[*]gdi32.lib 提供图形界面相关的一些基础功能,比如 GetDeviceCaps 等;
[*]winmm.lib 则是我们用来设置定时器分辨率的,这个用于调整窗口调度系统的精度。
https://i-blog.csdnimg.cn/direct/097136a296d043038a6f0410c1c561b9.png#pic_center
到现在为止,我们的步伐中只用了这些库,也仅仅调用了一些基础的窗口和定时器相关功能。
但接下来我们要和 3D 图形硬件打交道,而这些功能并不包含在前面这三个库中。因为我们运行在 Windows 平台上,不可能直接越过操作系统去访问显卡硬件,必须通过系统提供的接口来操作。
以是,接下来的任务就是:


[*]找到系统中用于访问 3D 图形硬件的接口;
[*]链接这些新的导入库;
[*]正确地初始化图形上下文,让 Windows 知道我们将会使用硬件加速。
这一步是使用硬件加速渲染的基础,无论选择 OpenGL 照旧 Direct3D,走的门路都类似。我们不直接控制硬件,只能通过操作系统提供的通道举行访问和控制。后续的工作就是在这个基础上一步步搭建起图形渲染管线。
链接 opengl32.lib

我们接下来需要添加一个新的导入库,以便可以大概访问系统的 3D 图形接口。在 Windows 上,我们有两个主要选择:DirectX 或 OpenGL。
这里我们选择添加 OpenGL,而不是 DirectX,原因是 OpenGL 具有更强的跨平台能力。虽然我们在 Windows 上学习如何初始化 OpenGL,这一部分代码本身不会直接在其他平台使用,但一旦我们通过 OpenGL 实现了渲染逻辑,这部分渲染逻辑就可以通用于其他平台,比如 macOS 和 Linux。对于教学来说,选择更具通用性的 OpenGL 显然更符合,它能让大家在差别的操作系统下都受益。
根据定名风俗,我们添加的库名是 opengl32.lib。这个库就是用来链接并调用 OpenGL 提供的系统接口,答应我们访问并使用 3D 图形硬件。
在添加了这个库之后,编译步伐会成功,链接器不会报错,但此时还不会有什么明显变化,因为我们还没有真正调用任何 OpenGL 的功能。以是运行游戏后,表现跟之前完全一样,什么都没有变。
换句话说,此时唯一发生的变化就是在构建系统中额外链接了 opengl32.lib,为后续使用 OpenGL 做好准备。之后我们会开始编写代码,使用 OpenGL 初始化渲染上下文,开始举行图形渲染。
https://i-blog.csdnimg.cn/direct/99bfd399963b4888b81c57700412f867.png#pic_center
https://i-blog.csdnimg.cn/direct/3df04caa6d7e4180833f3ead949015f5.png#pic_center
添加库没报错分析找到了
win32_game.cpp: #include <opengl32.h>

现在我们已经具备了调用操作系统中 OpenGL 相关函数的能力,就像之前为了使用 DirectSound、XInput 等系统服务所做的一样,我们也需要包含一个 OpenGL 的头文件。在这里,我们引入的是 gl.h,这个头文件中包含了我们可能需要用到的所有 OpenGL 函数声明。
参加这个头文件之后,我们就可以像调用其他平台 API 一样调用 OpenGL 的函数了。就像我们使用 windows.h 来调用 Windows API,使用 xinput.h 来调用 XInput 接口一样。
不过,值得注意的是,对于 XInput 和 DirectSound,我们接纳了“延迟绑定”(late binding)的方式,也就是在运行时通过 GetProcAddress 来获取函数地点,而不是在链接阶段就绑定到这些库。这么做的原因是,这些库在某些用户机器上可能根本不存在,如果我们在编译时就将它们绑定到可执行文件中,一旦用户系统没有这些库,步伐就会在启动时瓦解,这是我们不希望看到的。
相比之下,OpenGL 则不需要这么谨慎。OpenGL 从 Windows NT 3.51 开始就随系统一起提供,也就是说在所有当代 Windows 系统上,都一定会存在一个根本版本的 OpenGL。我们当前所使用的 Windows 属于 NT 分支(而不是 Windows 95/98/ME 那种老旧的分支),以是我们可以放心地直接链接 opengl32.lib 并使用它提供的根本功能。
当然,这并不意味着所有当代 OpenGL 的功能都可以随意使用。OpenGL 的发展历程中出现了很多版本和扩展,从 1.0 到 4.x,每个版本新增了许多新特性。这些高级特性是否可用,取决于显卡驱动的更新环境和显卡本身的支持水平。
也就是说,虽然根本的 OpenGL 1.x 函数是可以直接使用的,但如果想使用更高级的 OpenGL 功能(比如 OpenGL 3.0 以后的东西),我们仍然需要像调用 XInput 那样,通过动态加载的方式获取函数指针,这就需要我们写类似 wglGetProcAddress 这样的加载代码。
https://i-blog.csdnimg.cn/direct/5bf3d723e1f04bba9e80a46ac3a7360f.png#pic_center
现在,我们只需要最基础的 OpenGL 功能,比如把图像绘制到屏幕上的能力,而这些基础功能在任何支持 OpenGL 的系统中都是可以直接调用的。以是现在只需要包含 gl.h,之后就能开始初始化和使用 OpenGL 渲染器了。后续如果我们决定更早地切换渲染器到 OpenGL,那么这些基础知识也将派上用场。
https://i-blog.csdnimg.cn/direct/5935b15ff7a942e3b9827a1633e36865.png#pic_center
添加gl/GL.h头文件
https://i-blog.csdnimg.cn/direct/a9b557fd0ae44269af7fc834b37f2b68.png#pic_center
#include <gl/gl.h>
https://i-blog.csdnimg.cn/direct/c2f0a91b8f8d4695ba4c782eb8ab4b3d.png#pic_center
#include <gl/GL.h>
https://i-blog.csdnimg.cn/direct/20cbc05d22854296bcc4852d677cfd84.png#pic_center
是一个文件
Windows 头文件之以是不区分大小写,根本原因在于 Windows 文件系统的特性,以及 C/C++ 编译器在 Windows 上的行为。下面我们详细解释这个问题:
一、Windows 文件系统是大小写不敏感的

Windows 常用的文件系统(如 NTFS、FAT32)默认是:


[*]大小写不敏感(case-insensitive)
[*]但它是大小写保存的(case-preserving)
什么意思?
举个例子:
windows 会认为 these 都是同一个文件:
    Windows.h
    windows.h
    WINDOWS.H
但它会保存你最初写入的大小写形式,比如你创建了 Windows.h,之后访问 windows.h 也能找到它,但文件名会仍然表现为 Windows.h。
二、C/C++ 编译器遵循操作系统行为

Windows 下的主流编译器(如 MSVC、Clang、GCC)在处理 #include 指令时:
#include "windows.h"
#include "Windows.h"
#include "WINDOWS.H"
这些它都会成功找到,因为它最终依赖的是 Windows 文件系统的查找机制,而不是本身去实现一套区分大小写的逻辑。
三、与 Linux/Mac 的区别

而在 Linux 或 macOS 上就差别了:


[*]大多数默认的文件系统(如 ext4、APFS)是 大小写敏感的(case-sensitive)
[*]以是在这些系统上,#include "Windows.h" 和 #include "windows.h" 就可能是完全差别的文件,找不到就会报错。
总结一句话:

   Windows 的头文件不区分大小写,是因为 Windows 文件系统默认大小写不敏感,编译器顺应这一行为。
如果你在写跨平台代码,发起照旧始终使用正确的大小写拼写,这样可以避免在 Linux/macOS 上编译失败。需要更严谨的做法时,也可以在 Windows 上使用大小写敏感的子系统(比如 WSL 的 ext4 挂载)。
win32_game.cpp: 引入 Win32InitOpenGL

我们现在需要做的是让窗口具备使用 OpenGL 的能力。就像之前初始化 DirectSound 一样,我们需要经历一个初始化流程,不过这次针对的是 OpenGL。
虽然 DirectSound 需要动态加载库(比如使用 LoadLibrary 和 GetProcAddress),但 OpenGL 不需要这样处理。原因在于 OpenGL 是 Windows 自带支持的系统服务,就像 user32 那样是一直存在的,因此我们可以直接静态链接 OpenGL32.lib 并调用其中的函数,而不必担心目标机器上缺失该功能。唯一的不确定是其支持的 OpenGL 版本,因为这取决于显卡和驱动步伐的版本,但至少最基础的一部分(OpenGL 1.x)是一直存在的。
接下来,我们要做的事变和初始化 DirectSound 时的流程类似,我们需要经历几个具体的步骤。这些步骤虽然不算多,但确实比力“诡异”和不直观,很轻易出错,因此也欠好完备记住,以是我们可能需要检察一些示例代码作为参考。毕竟,这种初始化过程不像一般的 API 调用那么通例和清楚。
为此,我们打算写一个初始化 OpenGL 的函数,比如叫 Win32InitOpenGL。现在为了避免打乱已有结构,我们打算把这个函数先单独放进当前文件,避免和其他逻辑混在一起。
我们当前的目标很明确:我们已经有一个窗口,现在需要让这个窗口附加一个可以与 OpenGL 一起工作的上下文。也就是说,我们要为这个窗口创建一个 OpenGL 上下文,使得它能支持 OpenGL 渲染。
在写具体的代码之前,我们要先了解 OpenGL 的模型,毕竟设置过程的诡异之处多半也是由于这个模型本身的汗青遗留计划所导致的。我们需要理清楚整个初始化流程,包括像素格式设置、设备上下文创建、渲染上下文创建与绑定等操作。
总之,现在的任务是为当前窗口创建一个 OpenGL 渲染环境,接下来将从 OpenGL 的团体模型入手,逐步实现这个目标。
https://i-blog.csdnimg.cn/direct/c383e99d77164a089ec0ba35c210a063.png#pic_center
https://i-blog.csdnimg.cn/direct/1ab7cccd900b4ee48a4ef7a3da365a5b.png#pic_center
Blackboard: Windows 上的 OpenGL

我们现在来了解一下 OpenGL 在 Windows 上的运作方式。
OpenGL 是一个图形 API,最初来源于一个叫 Silicon Graphics(简称 SGI)的公司。在个人电脑还没有图形加速卡的年代,SGI 专门制造拥有硬件加速能力的高端图形工作站。这些设备可以完成在当时看来险些不可思议的图形任务,比如以硬件方式快速绘制实心三角形或举行纹理映射等。
SGI 开辟了一个专门用于控制其图形硬件的 API,最初叫 GL,后期也被称为 Iris GL(Iris 是他们的一个产品线)。这个 API 后来被标准化成了 OpenGL,成为一个更开放的平台标准,以便于各个平台都可以实现并运行这个图形接口。这样一来,开辟者编写的图形步伐可以直接在 SGI 的设备上获得硬件加速,从而提升运行服从。
OpenGL 最初的功能非常基础,仅包含诸如绘制线条、填充多边形、举行基础纹理映射等操作,没有当代 GPU 的高级特性,如着色器或并行计算功能。这些高级功能都是后来通过 OpenGL 的扩展机制连续参加的。
随着时间推移,OpenGL 渐渐演变为两部分:

[*] 平台无关部分:这部分是我们通常以为的 OpenGL,比如函数 glVertex3f、glClear 等,它们用于形貌和控制图形渲染过程,这部分由 OpenGL 的标准委员会(如 Khronos Group)统一维护和规范。
[*] 平台相关部分:这部分处理如何在具体平台上启动和运行 OpenGL。每个平台由于架构和窗口系统的差别,初始化和集成 OpenGL 的方式也差别。例如在 Windows 上,我们必须处理窗口创建、像素格式设置、设备上下文与渲染上下文的绑定等。而这些流程没有在 OpenGL 的标准中定义,完全依赖于平台自身提供的机制。
在 Windows 上,这些平台相关操作通过一组以 wgl 前缀开头的 API 实现(意为 Windows OpenGL),例如:


[*]wglCreateContext:创建一个 OpenGL 渲染上下文;
[*]wglMakeCurrent:将渲染上下文绑定到当前线程;
[*]wglDeleteContext:删除渲染上下文;
[*]wglGetProcAddress:获取扩展函数地点。
这些函数不属于 OpenGL 的焦点规范,而是 Windows 提供的专门用于支持 OpenGL 的机制。
但需要注意的是,并不是所有 OpenGL 的相关调用都遵循 wgl 前缀的定名。例如 SwapBuffers 就没有前缀,它属于 GDI(图形设备接口)的一部分,用于在双缓冲时互换前背景缓冲区。
总结起来:


[*]OpenGL 的标准部分处理图形渲染本身;
[*]初始化和集成 OpenGL 到应用步伐中,依赖平台提供的接口(如 Windows 提供的 wgl 系列);
[*]Windows 上运行 OpenGL 步伐必须与其窗口系统深度共同,设置像素格式、创建渲染上下文等;
[*]OpenGL 的最初版本非常基础,当代许多高级特性依赖于扩展机制;
[*]SGI 是 OpenGL 背后的起源,NVIDIA、3dfx 等后来的图形硬件厂商很多也都源自 SGI 的工程师。
现在我们要做的,就是围绕 Windows 的这些平台特有部分,实现一套 OpenGL 初始化流程,为窗口提供渲染支持。
Blackboard: “DC” -> 设备上下文

在 Windows 编程中,我们之前接触过一个叫做 设备上下文(Device Context, DC) 的概念。它是 Windows 中用于绘图操作的一个状态聚集,用来形貌当前绘图的相关信息,比如坐标变换、当前画笔颜色、绘图模式等。而我们通过 HDC(Handle to Device Context)来获取对设备上下文的访问权限,并举行图形操作,比如用 FillRect 这样的函数绘制矩形。
OpenGL 中也有一个非常类似的机制,叫做 渲染上下文(Rendering Context, RC),也就是 OpenGL 的 RC(HGLRC 表示)。这个 RC 是用来生存 OpenGL 当前状态的对象,比如当前使用的着色器、当前绑定的纹理、清除颜色等。当我们在 OpenGL 中调用函数时,这些操作并不会直接传入绘图目标,而是默认作用于当前线程绑定的渲染上下文。
Windows 中 DC 与 OpenGL 中 RC 的关系

我们在 Windows 中绘制图形,比如调用 FillRect,需要传入目标的 HDC,系统就知道我们要对哪个窗口举行绘制。而在 OpenGL 中,比如调用 glClear(用于清除颜色缓冲区),并没有任何与目标窗口相关的参数,这是因为:


[*]OpenGL 的状态是绑定到线程的。
[*]我们需要手动将一个 HGLRC(OpenGL 渲染上下文)绑定到当前线程,这样后续的 OpenGL 下令才会知道作用于哪个窗口。
这就是 wglMakeCurrent 的作用:将某个渲染上下文与当前线程绑定。只有绑定成功之后,线程才可以正常执行 OpenGL 调用。
渲染流程的初始化目标

我们当前的目标是用 OpenGL 在窗口中做一件简单的事 —— 清屏操作。即我们实验用 OpenGL 把窗口配景清成某种醒目标颜色,比如粉红色。这一步的目标是确认 OpenGL 渲染管线在 Windows 中被正确初始化和设置了。
为了做到这一点,我们需要完成以下步骤:

[*]获取设备上下文 HDC:从窗口中取得 Windows 图形系统的设备上下文,用于与窗口表面举行交互。
[*]设置像素格式:告诉系统我们想用什么样的像素格式(比如支持 OpenGL、颜色深度、双缓冲等)。
[*]创建 OpenGL 渲染上下文 HGLRC:基于我们设置好的像素格式和 HDC 创建一个渲染上下文。
[*]绑定渲染上下文到当前线程:通过 wglMakeCurrent 将 HGLRC 和 HDC 绑定到当前线程中,这样这个线程发出的 OpenGL 下令就知道该作用在哪个窗口上。
[*]执行 OpenGL 下令举行绘制:比如 glClearColor 设置清除颜色,glClear 执行清屏。
这种机制与 Windows 自身的设备上下文系统是相互协作的,我们可以明白为在 Windows 的图形子系统上层,叠加了一个 OpenGL 渲染子系统,它拥有本身的状态管理方式并通过渲染上下文来毗连系统图形资源与 OpenGL 状态。
这个架构是线程相关的,也就是说:


[*]每个线程只能有一个活跃的 OpenGL 渲染上下文;
[*]一个渲染上下文也只能在一个线程中活跃使用;
[*]如果要在多个线程中使用 OpenGL,需要举行显式的绑定和切换。
总体来看,这些概念虽然听起来复杂,但本质上是:Windows 管系统的设备上下文,OpenGL 管图形渲染的状态,而我们需要在它们之间创建绑定桥梁,才气在窗口上使用 OpenGL 绘图功能。
win32_game.cpp: 编写 Win32InitOpenGL

在 Windows 中使用 OpenGL 时,创建和启用一个 OpenGL 渲染上下文的流程看起来简单,但实际上另有很多隐藏的细节。首先,我们要调用 wglCreateContext 来创建一个 OpenGL 渲染上下文(Rendering Context,简称 RC)。这一步需要传入一个设备上下文(HDC),这个 HDC 通常是从一个窗口中通过 GetDC 获取到的。
这个 RC 是 Windows 系统中对 OpenGL 渲染状态的封装,类似于一个句柄(handle),范例为 HGLRC。调用 wglCreateContext 成功之后,我们就拥有了一个 OpenGL 渲染上下文。
不过,仅仅创建出来还不够。创建一个 RC 并不意味着当前线程正在使用它。为了让 OpenGL 知道当前线程要使用哪个渲染上下文,我们需要调用 wglMakeCurrent,把我们创建的 RC 绑定到线程上。这个函数需要传入两个参数:

[*]一个 HDC(设备上下文),用于指定这个 RC 是和哪个窗口表面关联的;
[*]一个 HGLRC(渲染上下文),就是我们刚才用 wglCreateContext 创建的。
HANDLE GL Render Context
一旦调用 wglMakeCurrent 成功,这个线程就可以开始使用 OpenGL 下令了,所有的 OpenGL 状态都会隐式关联到当前线程。
这统统听起来好像非常简单,但实际上,这只是表层工作。仅靠 wglCreateContext 和 wglMakeCurrent 并不足以让 OpenGL 在窗口中正确工作。虽然代码可能可以编译通过,流程也看似完备,但如果实验运行,很可能无法在窗口中看到任何渲染结果。
这背后的原因是 OpenGL 的使用依赖一系列更复杂的准备工作,比如:


[*]必须先设置一个符合的像素格式(Pixel Format);
[*]必须确保窗口支持 OpenGL 渲染;
[*]必须处理双缓冲机制、上下文版本要求、多平台兼容性等问题。
因此,虽然我们看起来只写了几行代码就完成了 RC 的创建和绑定,但实际上这远远不够。之后我们还需要深入处理这些基础设施部分,才气让 OpenGL 正常地在窗口中举行渲染操作。
总结一下当前流程:

[*]从窗口获取 HDC;
[*]调用 wglCreateContext 创建 OpenGL RC;
[*]使用 wglMakeCurrent 把 RC 绑定到当前线程;
[*]此时理论上就可以调用 OpenGL 的函数了。
但这仅是启动流程的一部分,背面还会逐步处理像素格式、渲染目标设置等复杂问题,才气真正让 OpenGL 正常工作。表面简单,实则隐含了许多平台依赖与细节处理。
https://i-blog.csdnimg.cn/direct/afbb88937b2949509fa2480338c41036.png#pic_center
win32_game.cpp: 禁用 Win32DisplayBufferInWindow

我们现在要做的是,先临时移除原来将离屏缓冲区内容表现到屏幕上的流程,具体就是把现有的那段使用 StretchDIBits 表现画面的代码临时禁用掉。这段代码原来是将 CPU 渲染好的图像拷贝到窗口上,也就是所谓的软件渲染方式。
禁用掉这些表现逻辑之后,当运行步伐时,整个游戏画面就会消散,不再表现我们原来渲染好的内容。屏幕上可能会出现一片黑,或者是一些残留数据、未定义内容,总之看不到之前的游戏界面了。
这是因为,我们现在不再使用那套软件方式来把画面刷到窗口,而是准备转向使用 OpenGL 来处理图像的呈现。也就是说,我们开始切换渲染管线,不再从 CPU 拷贝图像到窗口,而是通过 GPU 的 OpenGL 来直接控制表现过程。
这一步是为接下来的 OpenGL 初始化和测试作准备——只有当原来的表现方式被移除,才气确认 OpenGL 是否能正确接管图像输出。以是这一步操作的意义就是清空旧的表现路径,让我们可以大概干净地测试新创建的 OpenGL 渲染流程是否可以大概成功把图像输出到窗口。
https://i-blog.csdnimg.cn/direct/5fcdacbbb619452fa635fa7bc9cf2865.png#pic_center
https://i-blog.csdnimg.cn/direct/c4c0fe79472d4243911f37250ec3ca79.png#pic_center
win32_game.cpp: 在该函数中插入一些 OpenGL 代码

我们现在要在现有的步伐中插入一些基础的 OpenGL 渲染操作,这样就可以测试 OpenGL 是否真的已经正确初始化并且可以工作了。整个过程其实非常简单,主要分为三个步骤:
第一步:设置清屏颜色(glClearColor)

我们调用 glClearColor 来设置清屏时要使用的颜色。这个函数担当四个浮点数参数,分别表示红、绿、蓝、以及 alpha(透明度)分量。例如,如果我们传入 (1.0f, 0.0f, 1.0f, 0.0f),那就是一个非常美丽的紫色,alpha 为 0。
虽然我们设置的是浮点数颜色,但最终 OpenGL 会主动将这些浮点颜色转换成帧缓冲区支持的格式,比如 8 位色深等,以是不用担心底层的存储格式问题。
第二步:清除颜色缓冲区(glClear)

设置好清屏颜色之后,我们使用 glClear 来清除指定的缓冲区。在这个例子中,我们只需要清除颜色缓冲区即可,以是传入的是 GL_COLOR_BUFFER_BIT。
其他的缓冲区像 GL_DEPTH_BUFFER_BIT(Z 缓冲)、GL_STENCIL_BUFFER_BIT(模板缓冲)、GL_ACCUM_BUFFER_BIT(累积缓冲)临时都用不到,以是不用剖析。
调用 glClear 之后,OpenGL 会用我们刚刚设置好的颜色去清空整个颜色缓冲区,也就是把后备缓冲区填满指定的颜色。
第三步:互换前后缓冲区(SwapBuffers)

虽然我们已经在缓冲区里绘制了内容(例如清屏),但还没有真正表现在窗口上。OpenGL 默认使用双缓冲,也就是“前缓冲区”表现在屏幕上,“后缓冲区”用于绘制。绘制完成后,要调用 SwapBuffers,把后缓冲区的内容切换到屏幕上表现。
这时候需要传入一个 HDC(设备上下文句柄),也就是窗口对应的 Windows 设备上下文,用于告诉系统具体要在哪个窗口表现结果。
补充:设置视口(glViewport)

另有一个小细节是设置视口(viewport),即告诉 OpenGL 当前要渲染的区域在窗口上的哪一块。这个通过调用 glViewport 实现,参数是左上角位置 (x, y) 和宽高 (width, height)。通常我们会设置为窗口的整个区域,比如 (0, 0, width, height),表示从左上角开始,覆盖整个窗口。
小结:

以是我们插入的 OpenGL 渲染流程是这样的:

[*]glViewport(0, 0, width, height):设置绘制区域为整个窗口。
[*]glClearColor(r, g, b, a):设置清屏颜色。
[*]glClear(GL_COLOR_BUFFER_BIT):清空颜色缓冲区。
[*]SwapBuffers(hdc):将结果表现到窗口上。
这样,每一帧我们就能看到一个由 OpenGL 渲染出来的纯色画面,这也是验证 OpenGL 初始化是否成功的最简单方式。虽然这个渲染操作非常基础,但它创建了后续更复杂渲染流程的基础。
https://i-blog.csdnimg.cn/direct/9b134a0b75344320a54fb87953f87ed3.png#pic_center
https://i-blog.csdnimg.cn/direct/b071bae2366e4e4ca1258697081d3849.png#pic_center
https://i-blog.csdnimg.cn/direct/80e27933c9294190818ca83d43098b1a.png#pic_center
https://i-blog.csdnimg.cn/direct/6d21d63cd4534cab8ceedb8dd60135f0.png#pic_center
https://i-blog.csdnimg.cn/direct/d257d08e63a248ceb1c14dfcb2fdea93.png#pic_center
https://i-blog.csdnimg.cn/direct/ca84300a4a7e43c59d6b42bf72d7dfb6.png#pic_center
https://i-blog.csdnimg.cn/direct/18a621b481c345ce98f9b105b0cce6c4.png#pic_center
https://i-blog.csdnimg.cn/direct/7e593a3a4c8143e1974c3328fbb23ed4.png#pic_center
运行游戏并注意到 “没有出现粉色屏幕”

尽管按照我们刚才的步骤设置了 OpenGL 清屏颜色为粉色,但实际上我们并没有看到粉色屏幕。这是因为,尽管我们按照正确的步骤举行了设置,但仍然缺少一些必要的操作来确保屏幕能正确表现。
问题的根源在于,虽然我们做了 glClearColor 和 glClear 来设置和清空颜色缓冲区,但 OpenGL 渲染并不主动表现内容,特别是在双缓冲的环境下。我们已经设置了清屏颜色,并且清空了缓冲区,但并没有真正把缓冲区的内容展示出来。
为了办理这个问题,除了清除颜色缓冲区外,还需要确保我们使用 SwapBuffers 来将渲染内容从背景缓冲区(后缓冲)切换到前台缓冲区(即屏幕表现的部分)。如果没有调用 SwapBuffers,屏幕上就不会表现任何内容,尽管背景已经完成了渲染操作。
以是,虽然我们做了 glClear 和 glClearColor 的设置,结果却没有表现预期的粉色屏幕。这分析我们在渲染的过程中仍然缺少了关键步骤——互换缓冲区操作(SwapBuffers)。如果这些步骤没做好,OpenGL 渲染的结果就不会正确表现在屏幕上。
https://i-blog.csdnimg.cn/direct/bc09226f0dd6484799667216aa970458.png#pic_center
win32_game.cpp: 调用 Win32InitOpenGL,运行游戏并碰到无效代码路径

我们在步伐中设置好 OpenGL 渲染流程,包括创建渲染上下文、设置清屏颜色、调用清除缓冲区以及举行缓冲区互换(SwapBuffers),理论上应该可以在窗口中看到一块粉色的清屏颜色。
但实际上,步伐连渲染的那一步都没有走到,乃至在初始化阶段就失败了。进一步检查发现,是 OpenGL 渲染上下文(RC)创建失败了。也就是说,wglCreateContext 这一关键步骤没有成功执行,从而导致后续的 OpenGL 渲染都无法举行。
这比“屏幕没有变成粉色”还要更直接地分析问题存在,因为步伐根本没有成功完成渲染上下文的创建,天然也不会进入到真正的绘制阶段。
因此问题出在更早的地方,而不是渲染逻辑本身。这提示我们需要进一步检查为什么渲染上下文创建失败。接下来的任务就是找到导致渲染上下文创建失败的根本原因,通常这与设备上下文(DC)未设置正确的像素格式有关,也可能是窗口本身尚未准备好举行 OpenGL 初始化。
总之,当前的问题并不是绘制逻辑失败,而是根本没有成功初始化 OpenGL,这才导致窗口中什么都没有表现出来。我们接下来要办理的就是为什么 RC 创建失败的问题。
https://i-blog.csdnimg.cn/direct/bc0558b6402c4b8da2e58c4d7a517c1b.png#pic_center
https://i-blog.csdnimg.cn/direct/ab9e50b61be441b1884b24fd85ea4179.png#pic_center
网上搜刮: PIXELFORMATDESCRIPTOR1

为了让 OpenGL 成功在窗口上举行渲染,我们不能只创建一个渲染上下文(Rendering Context)然后直接使用。必须先做一件非常关键的初始化操作:为设备上下文(DC)设置一个符合的像素格式(Pixel Format)。如果不做这一步,OpenGL 渲染上下文的创建是一定会失败的。
这是一个遗留计划造成的问题,起因可以追溯到早年图形系统资源极度受限的期间。当时候,显存稀缺,带宽昂贵,不像现在可以任意用 32 位真彩色。系统乃至支持运行在调色板模式(如 8 位颜色,每个像素代表调色板中的一个颜色索引)或 16 色模式。因此,在举行图形渲染时,操作系统需要明确知道我们盼望的像素格式。
为此,我们需要调用 SetPixelFormat,为窗口关联的设备上下文(DC)设置一个支持 OpenGL 渲染的像素格式。然而,这并不是直接“指定”一个格式,而是一个“协商过程”。
我们需要构造一个 PIXELFORMATDESCRIPTOR 结构体,这个结构体里填满了各种参数,比如希望使用的颜色深度(如 32 位 RGBA)、是否支持双缓冲、是否支持 OpenGL、是否需要深度缓冲、模板缓冲等等。
但我们不能就这样任意填一个 PIXELFORMATDESCRIPTOR 然后直接使用。因为 OpenGL 的实现和底层的硬件驱动只支持一部分具体的像素格式。我们必须从系统实际支持的格式中选择一个最接近我们盼望的。
为此有两个方法:

[*] 穷举法:调用 DescribePixelFormat,一个一个地查询系统支持的所有像素格式,直到找到一个和我们盼望匹配的为止。这种方式繁琐,而且系统可能支持上百种格式。
[*] 选择法(推荐):构造一个希望的 PIXELFORMATDESCRIPTOR,然后调用 ChoosePixelFormat 让操作系统在它支持的格式中选出一个最接近我们盼望的。这种方式更高效,也更可靠。
完成上述步骤后,还必须用 SetPixelFormat 将这个选中的像素格式正式设置到我们的设备上下文上。只有这样,后续创建 OpenGL 渲染上下文的调用才会成功。
总结:


[*]初始化 OpenGL 前,必须为窗口的 DC 设置像素格式。
[*]设置像素格式涉及到一个“协商”过程,操作系统和硬件会选择一个它们支持的格式。
[*]使用 ChoosePixelFormat + SetPixelFormat 是最常见、最稳妥的方式。
[*]忽略这一步会导致 OpenGL 无法工作,即使后续的渲染代码写得完全正确,依然无法看到任何输出。
这一步是使用 OpenGL 在 Windows 平台上渲染的一个重要前提。
https://i-blog.csdnimg.cn/direct/1085a634692d43f09d887c8293b9d344.png#pic_center
https://i-blog.csdnimg.cn/direct/b70e513ab0114d969bd5fff76414a0f0.png#pic_center
https://i-blog.csdnimg.cn/direct/e33e336ad0a84966a88191efbd0699d8.png#pic_center
win32_game.cpp: 初始化该结构体

我们在初始化 OpenGL 时,还需要完成一个非常关键的步骤:设置像素格式(Pixel Format)。这部分看起来非常琐碎,但它是整个 OpenGL 启动过程中不可或缺的一步。我们不能直接使用 OpenGL 渲染上下文,必须先告诉 Windows,我们希望以什么样的像素格式在窗口中举行渲染。
首先,我们要准备一个 PIXELFORMATDESCRIPTOR 结构体,用于形貌我们希望使用的像素格式。这个结构体的设置要非常小心:


[*]nSize 是结构体的大小,这个字段虽然没什么用,但 Windows 要求必须设置。它的存在是为了“兼容将来的扩展”。
[*]nVersion 是版本号,现在只存在一个版本,设为 1。
[*]dwFlags 是标志位,我们必须启用以下几个标志:

[*]PFD_DRAW_TO_WINDOW:表示可以渲染到窗口上。
[*]PFD_SUPPORT_OPENGL:表示支持 OpenGL 渲染。
[*]PFD_DOUBLEBUFFER:表示使用双缓冲(back buffer + front buffer)。

然后是像素格式具体参数的设置:


[*]iPixelType 应设为 PFD_TYPE_RGBA,表示使用 RGBA 颜色。
[*]cColorBits 设置为 32,表示使用 32 位颜色深度(24 位颜色 + 8 位 Alpha 通道)。
[*]cAlphaBits 设置为 8,表示我们需要 8 位 Alpha 通道。
其他如深度缓冲、模板缓冲、累积缓冲等,我们当前不需要,因此设置为 0。
我们并不能直接用这个结构体调用 SetPixelFormat,因为 Windows 和底层硬件并不一定支持我们指定的格式。我们需要通过 ChoosePixelFormat 让系统“推荐”一个最接近我们设定的像素格式。
这个函数会返回一个索引(整数),表示一个系统支持的像素格式。然后我们用这个索引调用 DescribePixelFormat,获取该像素格式的完备形貌结构体(系统实际支持的那一个)。这一步非常重要,因为我们不能直接使用本身构造的结构体作为最终设定的格式,必须使用系统返回的版本。
最后,调用 SetPixelFormat,把获取到的像素格式应用到窗口的设备上下文(DC)上。这一步成功之后,我们才气继承使用 wglCreateContext 创建 OpenGL 渲染上下文,并用 wglMakeCurrent 将其设置为当前上下文。
在实际执行时我们发现:


[*]系统返回的格式确实是 32 位颜色,但包含了我们没指定的缓冲区,比如 24 位的深度缓冲、8 位模板缓冲等。这可能是硬件默认的格式,只能担当有这些缓冲的版本,我们也不能清除或控制这些额外部分。
[*]Windows 的文档中提到 cColorBits 不包含 Alpha 位,但我们实际测试时发现包含了 Alpha 位。分析文档和实现并不一致。虽然文档写的是“excluding alpha”,但返回的是 32 位(含 Alpha)。我们根据实际行为把 cColorBits 设置为 32 位,确保拿到我们想要的结果。
[*]设置完成后我们成功创建了 OpenGL 上下文,这代表像素格式协商成功,设备上下文可以正常举行 OpenGL 渲染。
额外注意事项:


[*]启用 OpenGL 后,原来使用 GDI 实现的窗口殊效(如淡入淡出)会失效。这是因为 OpenGL 接管了渲染流程,不再相应原有的窗口层叠或过渡结果。
[*]如果之后需要使用 OpenGL 的 3.0 或 4.0 高级特性,还需要使用扩展方式创建上下文(如 wglCreateContextAttribsARB),这是更高版本的上下文创建方式,我们可以以后再添加这部分。
总结:

[*]初始化 PIXELFORMATDESCRIPTOR 并设置关键参数。
[*]使用 ChoosePixelFormat 获取最匹配的像素格式索引。
[*]使用 DescribePixelFormat 获取真实格式形貌。
[*]使用 SetPixelFormat 将其应用到设备上下文上。
[*]创建 OpenGL 上下文并绑定。
完成以上步骤后,就可以开始使用 OpenGL 举行渲染。虽然过程繁琐,但每一步都是确保 OpenGL 成功运行的关键。
https://i-blog.csdnimg.cn/direct/5158d34a1916472a83188457649e48cd.png#pic_center
https://i-blog.csdnimg.cn/direct/ad39d3d41c944af9bf5381196a83a0b0.png#pic_center
https://i-blog.csdnimg.cn/direct/7281aab9711748db8fa699a1c82a7783.png#pic_center
https://i-blog.csdnimg.cn/direct/b3483d2c14974040a11d4ce3a968e391.png#pic_center
https://i-blog.csdnimg.cn/direct/dd18d1437d18471ab4da63fe193d69aa.png#pic_center
https://i-blog.csdnimg.cn/direct/0fc9cd19b6f64c1a8472d4343fd4807c.png#pic_center
https://i-blog.csdnimg.cn/direct/3f35e03cc1254335be4f77ac8affa3aa.png#pic_center
https://i-blog.csdnimg.cn/direct/3e33f64d61e94dbeb3f6bbdad0810e80.png#pic_center
https://i-blog.csdnimg.cn/direct/48eb8297e31947a8babda4023f09c8ba.png#pic_center
https://i-blog.csdnimg.cn/direct/8f0e3446c984473398e95e2445d794d6.png#pic_center
https://i-blog.csdnimg.cn/direct/a66f12caa2e040f79f3fc495d7c9faf9.png#pic_center
Windows 下初始化 OpenGL 的关键步骤之一:设置像素格式(Pixel Format),我们逐行解释这段代码是做什么的,以及它为什么这么做。
HDC WindowDC = GetDC(Window);
作用:


[*]获取窗口的设备上下文(DC, Device Context),用于之后的图形绘制。
[*]Window 是一个窗口句柄(HWND),通过 GetDC 获取其对应的绘图上下文。
[*]所有后续关于像素格式、OpenGL 上下文的创建都要用到这个 DC。
PIXELFORMATDESCRIPTOR DesiredPixelFormat = {};
DesiredPixelFormat.nSize = sizeof(DesiredPixelFormat);
DesiredPixelFormat.nVersion = 1;
DesiredPixelFormat.dwFlags = PFD_SUPPORT_OPENGL | PFD_DRAW_TO_WINDOW | PFD_DOUBLEBUFFER;
DesiredPixelFormat.cColorBits = 32;
DesiredPixelFormat.cAlphaBits = 8;
DesiredPixelFormat.iLayerType = PFD_MAIN_PLANE;
作用:


[*]创建一个我们希望使用的像素格式形貌结构体(PIXELFORMATDESCRIPTOR)。
[*]设置了我们对这个像素格式的要求,包括:
字段寄义nSize结构体大小,必须设定,Windows 要求nVersion固定为 1,现在只存在这一版本dwFlags设置三种标志:支持 OpenGL、可以绘制到窗口、使用双缓冲cColorBits希望的颜色深度,这里是 32 位(通常包含 RGB 和 Alpha)cAlphaBitsAlpha 通道的位数,8 位iLayerType设置为主图层(PFD_MAIN_PLANE),也就是正常表现层 int SuggestedPixelFormatIndex = ChoosePixelFormat(WindowDC, &DesiredPixelFormat);
作用:


[*]调用系统函数 ChoosePixelFormat,告诉它我们“抱负”的像素格式是什么。
[*]系统会返回一个最接近我们要求的像素格式的索引,因为我们不能直接使用本身的设定。
[*]这个返回值 SuggestedPixelFormatIndex 是后续操作的关键。
PIXELFORMATDESCRIPTOR SuggestedPixelFormat;

DescribePixelFormat(WindowDC, SuggestedPixelFormatIndex, sizeof(SuggestedPixelFormat),
                  &SuggestedPixelFormat);
作用:


[*]用 DescribePixelFormat 获取上一步系统发起的像素格式的真实形貌。
[*]因为我们只拿到了一个索引,但并不知道它具体长什么样。
[*]通过这个函数,我们拿到系统真实支持的 PIXELFORMATDESCRIPTOR,生存在 SuggestedPixelFormat 中。
SetPixelFormat(WindowDC, SuggestedPixelFormatIndex, &SuggestedPixelFormat);
作用:


[*]真正把我们选定的像素格式应用到窗口的设备上下文中。
[*]这一步是必须的,不设置像素格式就不能创建 OpenGL 上下文。
[*]必须传入我们获取到的格式索引和完备的格式形貌结构体。
总结流程图:

[ GetDC ] → 获取窗口设备上下文(HDC)
   ↓
[ 填写 DesiredPixelFormat ] → 想要的像素格式
   ↓
[ ChoosePixelFormat ] → 让系统挑选最匹配的格式(返回索引)
   ↓
[ DescribePixelFormat ] → 获取索引对应的实际格式描述
   ↓
[ SetPixelFormat ] → 应用格式到窗口 DC
完成这一步之后,我们的窗口就具备了 OpenGL 渲染的基础条件,下一步就可以创建 OpenGL 上下文了。这个过程是所有 Windows OpenGL 步伐的通用启动流程。
https://i-blog.csdnimg.cn/direct/78377e2ad69f4a7a8c1a2012ad00aefd.png#pic_center
问答环节

很有趣,粉色屏幕没有出现在直播中,是 OBS 问题吗?

我们在运行步伐时碰到一个问题:在屏幕上确实渲染出了粉红色的内容,但在通过 OBS 举行直播推流时,粉红色部分完全没有表现,观众看到的画面就是一片黑。这分析 OBS 并没有成功捕获到 OpenGL 渲染出来的图像。
我们初步推测问题出在 OBS 和 OpenGL 的兼容性上。OBS 默认使用的是一种屏幕捕获方式,它可能无法正确读取由 OpenGL 绘制的图像,尤其是如果 OpenGL 使用了某些特别上下文、窗口设置或者硬件加速路径。也有可能 OBS 在捕获图像的时候没有处理好双缓冲机制或者上下文切换,导致帧缓冲区的内容没有被读取。
更具体一点:


[*]OpenGL 在窗口中直接渲染图像,它可能并不会走标准的 GDI 或 DWM 渲染路径;
[*]OBS 捕获的是系统合成之后的画面,如果我们用的窗口不是标准合成层,或者渲染时用了某种“独占”方式,OBS 可能根本捕获不到;
[*]一些显卡或驱动(尤其是独显/集显混用的系统)会把 OpenGL 渲染做在一个无法被系统正常捕获的离屏表面上;
[*]在 OBS 的设置中,如果没有选择正确的捕获模式(例如“游戏捕获”、“窗口捕获” vs “表现捕获”),OpenGL 内容经常会丢失或黑屏;
[*]某些系统下,使用硬件加速或全屏独占窗口模式也会导致 OBS 捕获不到任何内容。
最闭幕果就是:我们确实成功渲染了 OpenGL 图像,但在 OBS 推流里却什么都没有表现出来,看上去就像黑屏一样。这种征象其实很常见,在调试 OpenGL 应用直播或录制时需要特别注意工具之间的兼容性。可能需要切换 OBS 捕获模式、关闭全屏独占,乃至使用插件或逼迫启用兼容性捕获才气办理。
根据这个,你应该为 ColorBit2 使用 32

我们根据实际运行的结果观察,发现文档中关于 cColorBits 的形貌好像是错误的。文档中明确写着 cColorBits 不包含 alpha 通道的位数,意思是如果我们希望获得 24 位颜色和 8 位 alpha,那应该设置 cColorBits = 24 和 cAlphaBits = 8。
但是,实际环境是我们传入 cColorBits = 24 后,系统返回的像素格式结构中,颜色位数是 32,alpha 位是 8。也就是说,系统实际上把 alpha 包含在了 color bits 里。分析 cColorBits 的真实寄义在实际运行中是包括 alpha 的,或者说系统的行为根本没遵循文档所说的逻辑。
进一步判定,在当代图形硬件上,没有哪块显卡会默认给出“32 位颜色再加 8 位 alpha”的设置,也就是说,所谓的 40 位颜色缓冲险些不可能是默认选项。因此可以确认,返回的 32 位颜色其实是包括了 8 位 alpha 的,也就是 RGBA 各 8 位。这是现在硬件最常用也最公道的格式。
总结如下:


[*]系统返回的像素格式表明 cColorBits = 32 实际包括了 alpha;
[*]设置为 24 位 color + 8 位 alpha 并不会返回 24,而是 32,分析系统行为和文档不一致;
[*]主流显卡不会默认使用超过 32 位的颜色缓冲,因此可以确认 alpha 是包含在 color bits 中的;
[*]因此在设置 PIXELFORMATDESCRIPTOR 时,cColorBits 应该直接设为 32,不需要再额外思量 alpha 是否被计入。
这分析在处理 OpenGL 初始化时,不能完全依赖文档的说法,还必须根据实际返回的像素格式去验证系统行为。我们最终接纳的战略是直接设为 32,以确保得到我们盼望的 RGBA 8888 格式。
想知道是 CPU 照旧 GPU 实际大将信息传输到屏幕上。我记得你提到过这个,但我忘了

现在的渲染流程大致如下:
我们在 CPU 端打包一系列的渲染下令,这些下令并不会在 CPU 上被实际执行,而是通过 PCI 总线传输到 GPU。当下令传输过去后,GPU 吸取到这些指令并按照其中的形貌去执行实际的图形操作,比如清屏、绘制几何体、贴图等等。
以“清屏为粉色”为例:


[*]我们在 CPU 端构建了一个“清屏并设为粉色”的下令;
[*]这个下令通过总线被发送到 GPU;
[*]清屏操作并不在 CPU 上执行,而是由 GPU 在吸取到下令之后举行;
[*]以是最终屏幕呈现出粉色配景,是 GPU 实际完成了这一绘制任务。
换句话说,我们通过 CPU 指定“要做什么”,但真正“做这件事”的,是 GPU。两者通过显卡驱动和硬件接口举行协同工作。只要明白这一点,就可以清楚为什么某些操作在视觉上看起来“延迟”或和 CPU 无关,因为它们是异步在 GPU 上完成的。这种计划可以充实使用 GPU 的并行计算能力,进步团体渲染服从。
你对 Vulkan 有什么看法?

现在我们对 Vulkan 的态度偏向不喜欢,但由于某些限制,临时还无法对其举行具体讨论,可能是因为还未正式发布或者处于某种保密阶段。因此我们需要等待符合的时机,才气公开讨论 Vulkan 的具体内容或细节。
如果你有心做一个完全无关的教程直播,IO 完成端口会很好,因为我懒得看这方面的资料

我们以为 IO 完成端口(IO Completion Ports)是 Windows 系统中为数不多计划精良的 API 之一,非常值得了解和使用。虽然现在事变较多,可能没有机会专门制作相关的教学,但我们仍然推荐花时间学习这套机制。
IO 完成端口是一种高效的异步 I/O 机制,实用于需要处理大量并发 I/O 请求的步伐,特别是在服务器场景下表现精彩。它答应我们将多个 I/O 请求与一个端口关联起来,操作系统在请求完成时会关照我们,可以极大减少线程切换和上下文开销,从而提升性能和可扩展性。
这套 API 提供了一种线程池模型,由我们控制线程的最大数量,而不是为每一个毗连或请求都创建一个线程,这避免了线程爆炸的问题。团体上,它是 Windows 平台上举行高性能网络编程或文件处理的推荐方案之一。
你怎么看待 Nvidia GeForce 不清除内存的做法?

关于 NVIDIA GeForce 显卡,如果说它不清除内存的意思,可能是指显卡在使用过程中并不会主动清理显存中的数据。这通常意味着显卡会继承保存数据在显存中,直到系统或驱动步伐决定清理它。
显卡的内存管理是由 GPU 驱动控制的,一般来说,GPU 会在渲染过程中动态分配和使用显存,但并不会主动清理显存中的数据,尤其是当 GPU 还需要这些数据时。直到有新的渲染任务或系统需要释放显存时,显存中的数据才会被清除或覆盖。
这可能影响一些步伐的表现,尤其是在内存压力较大的环境下。如果显卡的内存没有被及时清理,可能会导致显存的不足,从而影响图形渲染性能或步伐的稳定性。以是,在开辟图形步伐时,需要思量如何有用地管理显存,避免过多无用数据占用显存资源。
进得晚了。我们现在加载了本身的 GL 函数指针吗?

现在并没有加载 OpenGL 的函数指针,因为当前仅使用 OpenGL 1.x 的功能。在 Windows 上,OpenGL 1.x 的根本功能已经直接内建,以是只需要调用这些内建的功能就可以举行渲染,不需要额外的函数指针。
然而,如果将来需要使用 OpenGL 3.0 或 4.0 的功能,比如更先进的图形渲染特性,就需要加载相应的函数指针。这是因为这些版本的 OpenGL 引入了更多的功能和扩展,需要通过动态加载函数指针来访问这些功能,而不是依赖操作系统自带的旧版本函数。
实验一种差别于粉色的颜色?OBS 可能把那个当作透明处理

思量到捕获问题,可能会将其处理为透明,但由于无法通过捕获工具捕获 OpenGL 输出,因此如果将其设置为 OpenGL 捕获模式就能办理这个问题。然而,最终决定使用捕获卡来办理表现问题,这样就不再需要思量捕获相关的问题。这样做的好处是,不仅办理了捕获无法正常工作的难题,还能避免 CPU 始终处于 11% 负载的环境,从而优化了性能。
你知道为什么他们弃用了 GL_ALPHA 吗?

在讨论时提到的“GL_ALPHA”好像是指某个特定的 OpenGL API。具体来说,这可能与 OpenGL 中的 alpha 渲染或透明度相关功能有关。不过,关于为什么这个 API 被弃用并没有明确分析,可能是因为它在实际应用中的使用不再广泛,或者由于技术进步和新的 API 替换了旧的实现方式。
如果是在谈论 OpenGL 或相关图形库的变动,通常是因为旧的功能存在局限性,或者已经被更高效、更当代的方式所代替。因此,开辟者通常会选择弃用不再必要或被更好技术所替换的部分,以进步团体性能或简化 API 的使用。
你不需要调用 DescribePixelFormat,因为当你使用 ChoosePixelFormat 时,它会主动修改 DesiredPixelFormat 中的内容

在使用 ChoosePixelFormat 函数时,它并不会修改我们传入的 DesiredPixelFormat 结构体的内容。我们传递给它的是一个像素格式形貌符(PIXELFORMATDESCRIPTOR),它只是用于指定我们盼望的像素格式参数。ChoosePixelFormat 函数会根据这些参数从系统中选择一个符合的像素格式,并返回一个发起的像素格式索引。这个返回的索引指向系统推荐的像素格式,但它不会直接修改我们传入的 DesiredPixelFormat 结构体本身。
因此,调用 ChoosePixelFormat 后,我们需要通过 DescribePixelFormat 函数来获取具体的像素格式形貌符,这时它才会填充我们传入的 PIXELFORMATDESCRIPTOR 结构体,这个结构体才会包含实际的、被系统推荐的像素格式的详细信息。
既然我们将使用深度缓冲区,那 Z 排序还重要吗?

排序 Z 缓冲区(Z-sort)在渲染过程中仍然非常重要,原因有两个:

[*] 透明结果的正确性:如果想要透明物体正确渲染,必须举行排序。透明物体需要按照从远到近的次序举行绘制,以确保它们可以大概正确地与配景举行混合,避免渲染错误。
[*] 性能提升:即使使用 Z 缓冲区,通过前到后的绘制次序可以优化渲染过程。在绘制时,可以使用早期的 Z 测试(early Z),跳过那些已经被遮挡的物体,从而节省 GPU 的计算资源,进步渲染速度。
此外,不使用 Z 缓冲区还能节省带宽,因为没有必要举行 Z 缓冲区的读写操作,从而减少内存访问的开销。不过,即使如此,仍然有可能开启 Z 缓冲区来使用早期遮挡检测(early occlusion),进一步优化性能。
总的来说,虽然在某些环境下可以不使用 Z 缓冲区,但在渲染透明物体时,排序 Z 缓冲区仍然黑白常重要的,且能带来性能的提升。
抱歉!看起来 OpenGL 文档又在哄人

OpenGL 的文档好像经常禁绝确,已经多次验证了这一点。尽管文档中有很多形貌,但在实际应用中,碰到的环境常常与文档所说的不一致,这让办理问题变得更加困难。
你会使用多个 OpenGL 版本,照旧只用最小的版本以兼容一般的 Windows XP 机器?(大多数机器上用的是什么版本?)

思量到目标平台为较老的 Windows XP 系统,选择符合的 OpenGL 版本是关键。由于我们开辟的是一款 2D 游戏,并不需要太多扩展功能,因此可以选择一个公道的 OpenGL 版本,乃至可能仅使用 OpenGL 2.0,这样就能满足大部分需求,同时保持系统的轻量化。总体来说,尽量保持最小化,避免引入不必要的复杂功能,因为对于这个项目而言,并不需要太多额外的 OpenGL 功能。
对于像这样的游戏,使用 OpenGL 的新版本会有什么好处吗?

使用新的 OpenGL 版本对于像这种游戏可能会有一些好处,尤其是涉及到图形渲染时。我们可能需要使用一些着色器(Shaders),至少会用到一些基础的着色器。虽然不使用着色器也能运行,但为了实现更丰富的结果,比如特别的视觉结果或性能优化,使用着色器会更符合。这样可以提升游戏的视觉表现和机动性,使得渲染结果更加多样化。
调用 OpenGL 函数(比如 glClear())时,opengl32.lib 和显卡驱动 DLL 之间有区别吗?

在调用像 GL_Clear 这样的函数时,无论是使用 OpenGL 32 库中的版本,照旧图形驱动 DLL 中的版本,它们是完全一样的。没有任何区别。这意味着两者的功能和结果是相同的,都是通过图形驱动提供的 OpenGL 接口来执行的。
代码库与导入库

当看到 .lib 文件时,它可能有两种寄义。它可以是一个导入库(import library),也可以是一个代码库(code library)。例如,如果你链接的是像 ZLib 这样的库,那它是一个代码库,包含了一些实际的代码,如压缩代码,它将被链接到游戏中。而 OpenGL32.lib 不是这种代码库,它是一个导入库,类似于 user32.lib,它不包含实际的代码实现。
OpenGL32.lib 只是提供了与操作系统的绑定点。通过链接到 OpenGL32.lib,你实际上是调用操作系统提供的 OpenGL 接口,而这个接口在步伐运行时被加载。这个库本身并不包含 OpenGL 的实际代码,它只是将调用传递给操作系统的服务,然后操作系统会决定是否直接将调用转发给图形驱动步伐,或者先经过一些额外的步骤,例如检查或举行环转移等操作。
因此,OpenGL32.lib 的作用只是提供与操作系统交互的绑定点,从而通过操作系统与图形驱动步伐举行通讯。
既然我们使用 OpenGL,那么你会修改我们的 2.5D 殊效,照旧保持代码不变?

代码将保持不变,只是在将来会有一个硬件路径。
对于 ChoosePixelFormat(),我们是不是不应该指定 iPixelType 并将其设置为 PFD_TYPE_RGBA?

在选择像素格式时,可能没有指定像素范例为PFD_TYPE_RGBA,结果可能是运气好,默认值为0。如果没有显式指定,它可能会默认设置为0。
https://i-blog.csdnimg.cn/direct/06161e4affa5492a9489bd1be8585bcf.png#pic_center
win32_game.cpp: 设置 DesiredPixelFormat.iPixelType

虽然没有指定像素范例为PFD_TYPE_RGBA是 technically 正确的,因为默认值是0,并且这样可以得到预期的结果,但为了代码更清楚和符合规范,照旧将其显式写出会更好。这样可以确保代码更加符合标准,也更易于明白。
以是通过链接 opengl32.lib,我们就不需要使用 GetProcAddress 来加载 OpenGL 吗?

通过链接OpenGL32.lib并不意味着不需要使用GetProcAddress。原因是,早期Windows系统自带的OpenGL版本过旧。如果只使用OpenGL 1.x版本,确实不需要调用GetProcAddress,类似于不需要为CreateWindow调用它,因为Windows会直接提供这些功能。但如果使用较新的OpenGL版本,就必须通过GetProcAddress来获取相关函数指针。
Blackboard: 动态链接表

在编译和链接步伐时,有两种常见的函数调用方式。第一种是直接调用本身定义的函数,比如myFunction。这种函数在编译后会被替换为一个相对地点的调用,编译后的可执行文件中不再保存函数本身的代码。第二种是调用操作系统提供的函数,如CreateWindow。这种函数的调用不会直接指向实际地点,而是通过一个动态链接表(DLL)举行管理。在加载步伐时,操作系统会将CreateWindow的实际地点映射到步伐的地点空间中,动态链接表会更新所有指向该函数的调用,替换为实际的函数地点。
GetProcAddress是用来手动完成这种过程的工具。通常,如果是操作系统内置的函数(如CreateWindow),步伐可以依赖动态链接表来举行调用,但如果是可选的外部库函数(如XInput或OpenGL的扩展函数),则需要使用GetProcAddress来查找这些函数的地点,以便在步伐运行时检查并动态加载它们。这是为了确保步伐能在差别的机器上运行,即使某些库可能不存在。
例如,Windows系统上的OpenGL 1.x版本是固定的,不需要使用GetProcAddress来调用,但对于更高版本的OpenGL(如OpenGL 2.0及以上),其中的新函数则需要通过GetProcAddress来查询和加载。
总结来说,GetProcAddress和类似的机制答应步伐在运行时动态加载函数和库,这样可以确保步伐在没有预先依赖某些库的环境下也能正确运行。
当然可以,下面我们用实际代码来举几个例子资助明白上面讲的内容,尤其是 GetProcAddress 在 OpenGL 和其他 Windows API 场景中的用途。
示例 1:平凡函数调用(静态链接)

void MyFunction() {
    // 执行一些逻辑
}

int main() {
    MyFunction();// 编译器在编译时已知地址,直接生成调用指令
    return 0;
}
分析:这是一个平凡的静态链接函数,编译后会直接在可执行文件中生成指向 MyFunction 的地点,不涉及动态链接。
示例 2:调用 Windows API(通过 import lib)

#include <windows.h>

int main() {
    HWND hwnd = CreateWindowA("STATIC", "Title", WS_OVERLAPPEDWINDOW,
                              0, 0, 800, 600, NULL, NULL, NULL, NULL);
    return 0;
}
分析:CreateWindowA 是 Windows API,通过 user32.lib 链接。这类调用在步伐启动时由操作系统装载并修复函数地点(通过 IAT - Import Address Table)。不需要手动调用 GetProcAddress。
示例 3:手动加载函数(用于可选组件)

#include <windows.h>
#include <stdio.h>

typedef DWORD (WINAPI *LPXInputGetState)(DWORD, void*);

int main() {
    HMODULE xinput = LoadLibraryA("xinput1_4.dll");
    if (xinput) {
      LPXInputGetState XInputGetState = (LPXInputGetState)GetProcAddress(xinput, "XInputGetState");
      if (XInputGetState) {
            printf("XInputGetState 加载成功,可以使用手柄功能\n");
      } else {
            printf("XInputGetState 加载失败,禁用手柄功能\n");
      }
    } else {
      printf("XInput DLL 未加载,系统不支持 XInput\n");
    }
    return 0;
}
分析:使用 GetProcAddress 是因为某些 Windows 系统可能没有安装 XInput。这样可以让步伐在没有 XInput 的机器上仍然正常运行(只是不支持手柄)。
示例 4:加载 OpenGL 高版本函数(OpenGL 扩展)

#include <windows.h>
#include <GL/gl.h>
#include <stdio.h>

typedef void (APIENTRY *PFNGLGENBUFFERSPROC)(GLsizei, GLuint*);
PFNGLGENBUFFERSPROC glGenBuffers;

void LoadOpenGLFunctions() {
    glGenBuffers = (PFNGLGENBUFFERSPROC)wglGetProcAddress("glGenBuffers");
    if (glGenBuffers) {
      printf("glGenBuffers 加载成功\n");
    } else {
      printf("glGenBuffers 加载失败,可能不支持 OpenGL 1.5+\n");
    }
}
分析:glGenBuffers 是 OpenGL 1.5 中引入的函数,Windows 系统只内置了 OpenGL 1.1,以是必须使用 wglGetProcAddress 动态加载。否则会导致链接失败或运行时报错。
页: [1]
查看完整版本: 游戏引擎学习第235天:在 Windows 上初始化 OpenGL