您的位置:首页 > 文旅 > 旅游 > 常州做网站哪家好_微商城是什么意思_餐饮店如何引流与推广_企业培训十大热门课程

常州做网站哪家好_微商城是什么意思_餐饮店如何引流与推广_企业培训十大热门课程

2025/4/22 19:56:08 来源:https://blog.csdn.net/TM1695648164/article/details/147353154  浏览:    关键词:常州做网站哪家好_微商城是什么意思_餐饮店如何引流与推广_企业培训十大热门课程
常州做网站哪家好_微商城是什么意思_餐饮店如何引流与推广_企业培训十大热门课程

奇怪有问题 之前没注意到

这个问题是Count == 0
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

GlobalConstants_Renderer_UsedDebugCamer 打开的话会有Bug
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

Count是零的话就不让排序了
在这里插入图片描述

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 则是我们用来设置定时器分辨率的,这个用于调整窗口调度系统的精度。
    在这里插入图片描述

到目前为止,我们的程序中只用了这些库,也仅仅调用了一些基础的窗口和定时器相关功能。

但接下来我们要和 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 初始化渲染上下文,开始进行图形渲染。
在这里插入图片描述

在这里插入图片描述

添加库没报错说明找到了

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 这样的加载代码。
在这里插入图片描述

目前,我们只需要最基础的 OpenGL 功能,比如把图像绘制到屏幕上的能力,而这些基础功能在任何支持 OpenGL 的系统中都是可以直接调用的。所以现在只需要包含 gl.h,之后就能开始初始化和使用 OpenGL 渲染器了。后续如果我们决定更早地切换渲染器到 OpenGL,那么这些基础知识也将派上用场。

在这里插入图片描述

添加gl/GL.h头文件
在这里插入图片描述

#include <gl/gl.h>
在这里插入图片描述

#include <gl/GL.h>
在这里插入图片描述

是一个文件

Windows 头文件之所以不区分大小写,根本原因在于 Windows 文件系统的特性,以及 C/C++ 编译器在 Windows 上的行为。下面我们详细解释这个问题:


一、Windows 文件系统是大小写不敏感的

Windows 常用的文件系统(如 NTFSFAT32)默认是:

  • 大小写不敏感(case-insensitive)
  • 但它是大小写保留的(case-preserving)

什么意思?

举个例子:

windows 会认为 these 都是同一个文件:Windows.hwindows.hWINDOWS.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 需要动态加载库(比如使用 LoadLibraryGetProcAddress),但 OpenGL 不需要这样处理。原因在于 OpenGL 是 Windows 自带支持的系统服务,就像 user32 那样是一直存在的,因此我们可以直接静态链接 OpenGL32.lib 并调用其中的函数,而不必担心目标机器上缺失该功能。唯一的不确定是其支持的 OpenGL 版本,因为这取决于显卡和驱动程序的版本,但至少最基础的一部分(OpenGL 1.x)是一直存在的。

接下来,我们要做的事情和初始化 DirectSound 时的流程类似,我们需要经历几个具体的步骤。这些步骤虽然不算多,但确实比较“诡异”和不直观,很容易出错,因此也不好完整记住,所以我们可能需要查看一些示例代码作为参考。毕竟,这种初始化过程不像一般的 API 调用那么常规和清晰。

为此,我们打算写一个初始化 OpenGL 的函数,比如叫 Win32InitOpenGL。目前为了避免打乱已有结构,我们打算把这个函数先单独放进当前文件,避免和其他逻辑混在一起。

我们当前的目标很明确:我们已经有一个窗口,现在需要让这个窗口附加一个可以与 OpenGL 一起工作的上下文。也就是说,我们要为这个窗口创建一个 OpenGL 上下文,使得它能支持 OpenGL 渲染。

在写具体的代码之前,我们要先了解 OpenGL 的模型,毕竟设置过程的诡异之处多半也是由于这个模型本身的历史遗留设计所导致的。我们需要理清楚整个初始化流程,包括像素格式设置、设备上下文创建、渲染上下文建立与绑定等操作。

总之,现在的任务是为当前窗口建立一个 OpenGL 渲染环境,接下来将从 OpenGL 的整体模型入手,逐步实现这个目标。

在这里插入图片描述

在这里插入图片描述

Blackboard: Windows 上的 OpenGL

我们现在来了解一下 OpenGL 在 Windows 上的运作方式。

OpenGL 是一个图形 API,最初来源于一个叫 Silicon Graphics(简称 SGI)的公司。在个人电脑还没有图形加速卡的年代,SGI 专门制造拥有硬件加速能力的高端图形工作站。这些设备可以完成在当时看来几乎不可思议的图形任务,比如以硬件方式快速绘制实心三角形或进行纹理映射等。

SGI 开发了一个专门用于控制其图形硬件的 API,最初叫 GL,后期也被称为 Iris GL(Iris 是他们的一个产品线)。这个 API 后来被标准化成了 OpenGL,成为一个更开放的平台标准,以便于各个平台都可以实现并运行这个图形接口。这样一来,开发者编写的图形程序可以直接在 SGI 的设备上获得硬件加速,从而提升运行效率。

OpenGL 最初的功能非常基础,仅包含诸如绘制线条、填充多边形、进行基础纹理映射等操作,没有现代 GPU 的高级特性,如着色器或并行计算功能。这些高级功能都是后来通过 OpenGL 的扩展机制陆续加入的。

随着时间推移,OpenGL 逐渐演变为两部分:

  1. 平台无关部分:这部分是我们通常认为的 OpenGL,比如函数 glVertex3fglClear 等,它们用于描述和控制图形渲染过程,这部分由 OpenGL 的标准委员会(如 Khronos Group)统一维护和规范。

  2. 平台相关部分:这部分处理如何在具体平台上启动和运行 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 中被正确初始化和设置了。

为了做到这一点,我们需要完成以下步骤:

  1. 获取设备上下文 HDC:从窗口中取得 Windows 图形系统的设备上下文,用于与窗口表面进行交互。
  2. 设置像素格式:告诉系统我们想用什么样的像素格式(比如支持 OpenGL、颜色深度、双缓冲等)。
  3. 创建 OpenGL 渲染上下文 HGLRC:基于我们设置好的像素格式和 HDC 创建一个渲染上下文。
  4. 绑定渲染上下文到当前线程:通过 wglMakeCurrentHGLRCHDC 绑定到当前线程中,这样这个线程发出的 OpenGL 命令就知道该作用在哪个窗口上。
  5. 执行 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 绑定到线程上。这个函数需要传入两个参数:

  1. 一个 HDC(设备上下文),用于指定这个 RC 是和哪个窗口表面关联的;
  2. 一个 HGLRC(渲染上下文),就是我们刚才用 wglCreateContext 创建的。
    HANDLE GL Render Context

一旦调用 wglMakeCurrent 成功,这个线程就可以开始使用 OpenGL 命令了,所有的 OpenGL 状态都会隐式关联到当前线程。

这一切听起来似乎非常简单,但实际上,这只是表层工作。仅靠 wglCreateContextwglMakeCurrent 并不足以让 OpenGL 在窗口中正确工作。虽然代码可能可以编译通过,流程也看似完整,但如果尝试运行,很可能无法在窗口中看到任何渲染结果。

这背后的原因是 OpenGL 的使用依赖一系列更复杂的准备工作,比如:

  • 必须先设置一个合适的像素格式(Pixel Format);
  • 必须确保窗口支持 OpenGL 渲染;
  • 必须处理双缓冲机制、上下文版本要求、多平台兼容性等问题。

因此,虽然我们看起来只写了几行代码就完成了 RC 的创建和绑定,但实际上这远远不够。之后我们还需要深入处理这些基础设施部分,才能让 OpenGL 正常地在窗口中进行渲染操作。

总结一下当前流程:

  1. 从窗口获取 HDC;
  2. 调用 wglCreateContext 创建 OpenGL RC;
  3. 使用 wglMakeCurrent 把 RC 绑定到当前线程;
  4. 此时理论上就可以调用 OpenGL 的函数了。

但这仅是启动流程的一部分,后面还会逐步处理像素格式、渲染目标配置等复杂问题,才能真正让 OpenGL 正常工作。表面简单,实则隐含了许多平台依赖与细节处理。
在这里插入图片描述

win32_game.cpp: 禁用 Win32DisplayBufferInWindow

我们现在要做的是,先暂时移除原本将离屏缓冲区内容显示到屏幕上的流程,具体就是把现有的那段使用 StretchDIBits 显示画面的代码暂时禁用掉。这段代码原本是将 CPU 渲染好的图像拷贝到窗口上,也就是所谓的软件渲染方式。

禁用掉这些显示逻辑之后,当运行程序时,整个游戏画面就会消失,不再显示我们原本渲染好的内容。屏幕上可能会出现一片黑,或者是一些残留数据、未定义内容,总之看不到之前的游戏界面了。

这是因为,我们现在不再使用那套软件方式来把画面刷到窗口,而是准备转向使用 OpenGL 来处理图像的呈现。也就是说,我们开始切换渲染管线,不再从 CPU 拷贝图像到窗口,而是通过 GPU 的 OpenGL 来直接控制显示过程。

这一步是为接下来的 OpenGL 初始化和测试作准备——只有当原来的显示方式被移除,才能确认 OpenGL 是否能正确接管图像输出。所以这一步操作的意义就是清空旧的显示路径,让我们能够干净地测试新建立的 OpenGL 渲染流程是否能够成功把图像输出到窗口。
在这里插入图片描述

在这里插入图片描述

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 渲染流程是这样的:

  1. glViewport(0, 0, width, height):设置绘制区域为整个窗口。
  2. glClearColor(r, g, b, a):设置清屏颜色。
  3. glClear(GL_COLOR_BUFFER_BIT):清空颜色缓冲区。
  4. SwapBuffers(hdc):将结果显示到窗口上。

这样,每一帧我们就能看到一个由 OpenGL 渲染出来的纯色画面,这也是验证 OpenGL 初始化是否成功的最简单方式。虽然这个渲染操作非常基础,但它建立了后续更复杂渲染流程的基础。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

运行游戏并注意到 “没有出现粉色屏幕”

尽管按照我们刚才的步骤设置了 OpenGL 清屏颜色为粉色,但实际上我们并没有看到粉色屏幕。这是因为,尽管我们按照正确的步骤进行了设置,但仍然缺少一些必要的操作来确保屏幕能正确显示。

问题的根源在于,虽然我们做了 glClearColorglClear 来设置和清空颜色缓冲区,但 OpenGL 渲染并不自动显示内容,特别是在双缓冲的情况下。我们已经设置了清屏颜色,并且清空了缓冲区,但并没有真正把缓冲区的内容展示出来。

为了解决这个问题,除了清除颜色缓冲区外,还需要确保我们使用 SwapBuffers 来将渲染内容从后台缓冲区(后缓冲)切换到前台缓冲区(即屏幕显示的部分)。如果没有调用 SwapBuffers,屏幕上就不会显示任何内容,尽管后台已经完成了渲染操作。

所以,虽然我们做了 glClearglClearColor 的设置,结果却没有显示预期的粉色屏幕。这说明我们在渲染的过程中仍然缺少了关键步骤——交换缓冲区操作(SwapBuffers)。如果这些步骤没做好,OpenGL 渲染的结果就不会正确显示在屏幕上。
在这里插入图片描述

win32_game.cpp: 调用 Win32InitOpenGL,运行游戏并遇到无效代码路径

我们在程序中设置好 OpenGL 渲染流程,包括创建渲染上下文、设置清屏颜色、调用清除缓冲区以及进行缓冲区交换(SwapBuffers),理论上应该可以在窗口中看到一块粉色的清屏颜色。

但实际上,程序连渲染的那一步都没有走到,甚至在初始化阶段就失败了。进一步检查发现,是 OpenGL 渲染上下文(RC)创建失败了。也就是说,wglCreateContext 这一关键步骤没有成功执行,从而导致后续的 OpenGL 渲染都无法进行。

这比“屏幕没有变成粉色”还要更直接地说明问题存在,因为程序根本没有成功完成渲染上下文的建立,自然也不会进入到真正的绘制阶段。

因此问题出在更早的地方,而不是渲染逻辑本身。这提示我们需要进一步检查为什么渲染上下文创建失败。接下来的任务就是找到导致渲染上下文创建失败的根本原因,通常这与设备上下文(DC)未配置正确的像素格式有关,也可能是窗口本身尚未准备好进行 OpenGL 初始化。

总之,当前的问题并不是绘制逻辑失败,而是根本没有成功初始化 OpenGL,这才导致窗口中什么都没有显示出来。我们接下来要解决的就是为什么 RC 创建失败的问题。
在这里插入图片描述

在这里插入图片描述

网上搜索: PIXELFORMATDESCRIPTOR1

为了让 OpenGL 成功在窗口上进行渲染,我们不能只创建一个渲染上下文(Rendering Context)然后直接使用。必须先做一件非常关键的初始化操作:为设备上下文(DC)设置一个合适的像素格式(Pixel Format)。如果不做这一步,OpenGL 渲染上下文的创建是一定会失败的。

这是一个遗留设计造成的问题,起因可以追溯到早年图形系统资源极度受限的时代。那时候,显存稀缺,带宽昂贵,不像现在可以随便用 32 位真彩色。系统甚至支持运行在调色板模式(如 8 位颜色,每个像素代表调色板中的一个颜色索引)或 16 色模式。因此,在进行图形渲染时,操作系统需要明确知道我们期望的像素格式。

为此,我们需要调用 SetPixelFormat,为窗口关联的设备上下文(DC)设置一个支持 OpenGL 渲染的像素格式。然而,这并不是直接“指定”一个格式,而是一个“协商过程”。

我们需要构造一个 PIXELFORMATDESCRIPTOR 结构体,这个结构体里填满了各种参数,比如希望使用的颜色深度(如 32 位 RGBA)、是否支持双缓冲、是否支持 OpenGL、是否需要深度缓冲、模板缓冲等等。

但我们不能就这样随便填一个 PIXELFORMATDESCRIPTOR 然后直接使用。因为 OpenGL 的实现和底层的硬件驱动只支持一部分具体的像素格式。我们必须从系统实际支持的格式中选择一个最接近我们期望的。

为此有两个方法:

  1. 穷举法:调用 DescribePixelFormat,一个一个地查询系统支持的所有像素格式,直到找到一个和我们期望匹配的为止。这种方式繁琐,而且系统可能支持上百种格式。

  2. 选择法(推荐):构造一个希望的 PIXELFORMATDESCRIPTOR,然后调用 ChoosePixelFormat 让操作系统在它支持的格式中选出一个最接近我们期望的。这种方式更高效,也更可靠。

完成上述步骤后,还必须用 SetPixelFormat 将这个选中的像素格式正式设置到我们的设备上下文上。只有这样,后续创建 OpenGL 渲染上下文的调用才会成功。

总结:

  • 初始化 OpenGL 前,必须为窗口的 DC 设置像素格式。
  • 设置像素格式涉及到一个“协商”过程,操作系统和硬件会选择一个它们支持的格式。
  • 使用 ChoosePixelFormat + SetPixelFormat 是最常见、最稳妥的方式。
  • 忽略这一步会导致 OpenGL 无法工作,即使后续的渲染代码写得完全正确,依然无法看到任何输出。

这一步是使用 OpenGL 在 Windows 平台上渲染的一个重要前提。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

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),这是更高版本的上下文创建方式,我们可以以后再添加这部分。

总结:

  1. 初始化 PIXELFORMATDESCRIPTOR 并设置关键参数。
  2. 使用 ChoosePixelFormat 获取最匹配的像素格式索引。
  3. 使用 DescribePixelFormat 获取真实格式描述。
  4. 使用 SetPixelFormat 将其应用到设备上下文上。
  5. 创建 OpenGL 上下文并绑定。

完成以上步骤后,就可以开始使用 OpenGL 进行渲染。虽然过程繁琐,但每一步都是确保 OpenGL 成功运行的关键。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

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 程序的通用启动流程。

在这里插入图片描述

问答环节

很有趣,粉色屏幕没有出现在直播中,是 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 = 24cAlphaBits = 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)在渲染过程中仍然非常重要,原因有两个:

  1. 透明效果的正确性:如果想要透明物体正确渲染,必须进行排序。透明物体需要按照从远到近的顺序进行绘制,以确保它们能够正确地与背景进行混合,避免渲染错误。

  2. 性能提升:即使使用 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。
在这里插入图片描述

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 动态加载。否则会导致链接失败或运行时报错。


🔚 小结

情况是否需要 GetProcAddress(或类似)
普通 C 函数❌ 不需要
Windows 标准 API(如 GDI)❌ 不需要
可选 DLL 函数(如 XInput)✅ 需要(GetProcAddress)
OpenGL 扩展函数(1.2+)✅ 需要(wglGetProcAddress)

需要动态加载的函数一般都属于操作系统可选功能第三方库扩展功能,用 GetProcAddress 可以让程序更灵活兼容。

我们来详细讲一下操作系统在加载一个使用了 DLL 的可执行程序时,如何通过 IAT(Import Address Table,导入地址表) 来修复函数地址的机制。


背景知识

在 Windows 中,可执行程序(EXE)和动态链接库(DLL)采用的是 PE(Portable Executable)格式。在编译时,如果我们使用了某个 DLL 的函数(例如 CreateWindow 来自 user32.dll),编译器会把这些函数的符号记录到 PE 文件中,但并不直接写死它的内存地址——因为 DLL 的地址在运行时才知道。


核心概念

🔸 Import Table(导入表)

  • 包含当前可执行文件依赖的 DLL 列表。
  • 每个 DLL 名下列出使用了哪些函数。

🔸 IAT(Import Address Table,导入地址表)

  • 是一张指针表,运行时会被操作系统填充成真实的函数地址
  • 程序中对 DLL 函数的调用,实际上是通过访问 IAT 来进行的。

加载流程(按步骤详细解释)

编译时

假设我们写了如下代码:

#include <windows.h>int main() {MessageBoxA(NULL, "Hello", "Title", MB_OK);return 0;
}

编译后:

  • 编译器知道你用了 MessageBoxA,来自 user32.dll
  • .idata 节中写入:
    • 需要导入的 DLL 名称(如 user32.dll
    • 函数名称列表(如 MessageBoxA
  • .rdata.idata 区域生成一张 IAT 表,最开始每项都是 0 或指向函数名的 thunk。

程序启动时(由 Windows 的 Loader 完成)

  1. 映像映射

    • 操作系统加载 EXE 文件到内存。
    • 找到 .idata 区块,识别出所需的 DLL。
  2. 加载 DLL

    • 加载 user32.dll(若尚未加载)。
    • 记录该 DLL 的加载基地址。
  3. 修复导入地址表(填充 IAT)

    • 对于每一个需要的函数(如 MessageBoxA):
      • 在 DLL 的导出表中查找它的地址。
      • 将找到的地址写入到当前 EXE 的 IAT 表中相应位置。
  4. 跳转调用

    • 当程序调用 MessageBoxA 时,其实是通过 IAT 中的函数指针跳转过去的。

举个实际例子

假设我们写了:

MessageBoxA(...);

实际执行时类似:

CALL DWORD PTR [0x00402000]  ; 假设这是 IAT 中 MessageBoxA 的入口

这个地址在程序运行前是空的,运行时会变成:

0x00402000 => 0x77D4A390  ; 实际指向 user32.dll 中 MessageBoxA 的地址

📎 延迟加载(Delay Load)

一种优化方式:不要在程序启动时就加载所有 DLL,而是等到函数被真正调用前才加载。

编译器支持 /DELAYLOAD:user32.dll,配合内部的跳板代码实现这种延迟修复。


IAT 和安全

优点:

  • 灵活支持 DLL 升级和系统兼容。
  • 可以延迟绑定,减小启动开销。

缺点:

  • 恶意软件可能会篡改 IAT 实现钩子(Hooking),用于注入或监听函数调用(例如键盘钩子等)。

可视化工具推荐

你可以用这些工具来查看一个 EXE 的 IAT:

  • Dependency Walker(depends.exe):经典工具。
  • PE-bear / CFF Explorer:现代 UI,支持查看导入表结构。

既然你经常抱怨 Windows,为什么不使用其他操作系统?

我们经常会抱怨 Windows,但仍然选择使用它的主要原因是作为游戏程序员,几乎是被动地必须使用 Windows。当前大约 95% 的游戏市场都运行在 Windows 平台上,这是一个压倒性的份额。

无论我们是否喜欢这个系统,都无法改变这个现状。在当前的市场结构下,如果目标是开发面向主流用户的游戏,那就必须在 Windows 上进行开发和测试。除非其他操作系统的市场份额总和能达到 70% 甚至更高,否则没有现实的选择空间。

这就是为什么我们仍然坚持使用 Windows,即使它存在很多让人头疼的问题。开发环境、用户基础、驱动支持、图形 API(如 DirectX 和 OpenGL)、兼容性和测试需求等因素,决定了这是游戏开发中难以回避的平台。
https://store.steampowered.com/hwsurvey/Steam-Hardware-Software-Survey-Welcome-to-Steam

你认为为了 OS X 端口,是否需要写一个用 Objective-C 的包装器来使用 OpenGL 并创建窗口?

在 macOS 系统上开发程序时,如果要创建一个行为符合规范的应用程序,通常需要使用 Objective-C 编写启动部分。这并不仅仅是为了使用 OpenGL,而是整个 macOS 应用生命周期的管理都依赖于 Cocoa 框架,而 Cocoa 是基于 Objective-C 的。

要在 macOS 上创建窗口、处理事件、响应系统消息等,往往都需要调用 Objective-C 的类和方法。即便只想做一个简单的图形程序,也很难完全避开 Objective-C,尤其是在处理窗口创建和与操作系统交互时更是如此。

虽然现在也可以通过 Swift 或者 C 接口间接处理部分工作,但从技术实现和兼容性角度出发,Objective-C 仍然是构建原生 macOS 应用程序最常用的方式之一。因此,如果目标是开发一个在 macOS 上行为规范的程序,就必须引入 Objective-C 的启动逻辑和封装。

这可能是个愚蠢的问题,但如果我们使用 OpenCL 将大部分计算任务转移到 GPU 上,而不使用 OpenGL,保持现有的做法会怎样?

关于将现有大量计算任务通过 OpenCL 转移到 GPU 上的设想,这种方式在实际中并不可行。并不是说只要把现有的软件渲染器代码交给 OpenCL 就能获得更高的性能。实际上,GPU 执行通用代码的效率远低于 CPU。如果希望在 GPU 上获得加速,必须有一整套专门为 GPU 架构优化的并行设计。

具体来说,需要将计算任务高度并行化,例如使用类似 32 通道(SIMD)宽度的操作方式,并将所有逻辑组织成适合 GPU 的并行执行单元。当前的架构虽然已经采用了瓦片化的渲染方式,在某种程度上有利于并行化,但为了在 OpenCL 中高效运行,仍需对现有代码结构进行大量改造。

此外,即使付出如此代价,所获得的收益也可能不如直接采用 OpenGL 渲染管线来得直接有效。与其花费大量精力重构原有渲染逻辑以适配 OpenCL,不如将相同的努力用在接入 OpenGL 这样的图形 API,通过图形管线实现硬件加速渲染。这样不仅更符合现代图形渲染的标准流程,而且更稳定、通用性更好。因此,与其试图通过 OpenCL 重构渲染器,不如直接使用 OpenGL 的图形渲染服务来更合理地完成目标。

在休息期间,我写了一个程序,能够通过扫描 cpp/h 文件来生成 OpenGL 函数指针的声明和初始化。你打算在中做类似的事情,还是保持简单?

在项目中,对于 OpenGL 函数指针的初始化,通常可以通过扫描 .cpp.h 文件来自动生成需要的装饰和初始化代码,从而确保所有使用到的 OpenGL 扩展函数都被正确加载。在这个过程中,工具会根据源码中用到的 OpenGL 函数名自动分析并生成对应的函数指针声明和加载逻辑。

不过,在当前这个游戏项目中,我们会选择保持简单,并不会使用这种自动化生成的复杂机制。我们将手动处理这些函数指针的初始化,保持代码清晰和可控。虽然在更复杂或大型的项目中,会更倾向于使用自动工具来处理这类重复性和容易出错的任务,但在这种体量较小、目标明确的项目里,简单直接的做法更加高效实用。

着色器中的 if 语句有多糟糕?

在着色器(shader)中使用 if 语句的效果好坏,取决于执行该着色器的所有像素是否走相同的分支路径。在 GPU 上,这种分支行为可能会导致性能问题,原因如下:

GPU 是高度并行的处理器,多个像素(通常是一组称为“warp”或“wavefront”的并行像素)会同时执行同一个着色器程序。如果这些像素在 if 判断时走了不同的路径,那么 GPU 不能真正做到“并行处理”所有分支,而是要依次执行每一个分支,并通过掩码的方式让某些像素“暂停”等待其他像素执行完当前分支。这被称为“分支分歧”(branch divergence)。

如果所有像素都走了同一个分支,GPU 就可以高效地并行执行,不会产生性能损耗。但如果某些像素进入了 if 的 true 分支,另一些进入 false 分支,就必须分别处理两条路径,执行时间就会变长,效率降低。

因此,在编写着色器时,应尽量避免在片元着色器中出现依赖于像素条件的 if 分支,除非能够保证分支在空间上是一致的(例如整个屏幕区域都会满足某个条件)。否则,这种看似简单的条件判断会对性能造成较大影响。总结就是:if 并不可怕,但在 GPU 上执行的时候,如果不同像素走了不同的路,那就“糟透了”。

你曾经为游戏机编程吗?

曾经为游戏主机编写过程序,不过大多数经验是在较早期的主机平台上。例如 Xbox、PlayStation 2 和 GameCube 等时代的主机。当时主要进行的是底层或跨平台相关的工作。虽然最近也接触过较新的主机,比如在某年十一月曾经短暂地为 PlayStation 4 编写过大约一周的程序,但并不算深入参与。总体来说,主要的开发经验还是集中在早期主机系统的开发流程与技术实现上。

Linux 比 Windows Vista 强多了

在讨论操作系统时,Linux 的使用比例确实在某些方面超越了 Windows Vista。具体来说,虽然 Windows Vista 64 和 32 位的使用占比很小(大约在0.4%或0.5%之间),但整体来看,Linux 在某些情况下能超越 Vista,尤其是当统计所有 Linux 发行版的使用比例时,Linux 的市场份额就超过了 Vista。不过,这个数据看起来有点混乱,似乎有很多操作系统未被包含在统计中,因此最后的对比结果有些难以确认。总结来看,如果算上所有版本的 Linux,它的使用率确实可以击败 Vista,这是一个积极的趋势。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com