当前位置: 首页 > news >正文

游戏引擎学习第239天:通过 OpenGL 渲染游戏

回顾并为今天的内容做准备

今天,我想继续完成这部分内容,因为实际上我们已经完成了大部分工作,剩下的部分并不复杂。我计划今天完成这部分实现,至少是那些不涉及纹理的部分。正如昨天所说,纹理部分才是唯一比较复杂的地方。

当我说复杂时,我并不是指绘制的部分——绘制带有纹理的物体其实非常简单,正如你们看到的那样。这里的复杂之处主要是如何将纹理以流式方式传输到显卡,这才是我们真正面临的问题。

所以今天的目标是尽量完成这部分内容,到今天结束时,我们希望只剩下纹理相关的问题,这样我们明天可以专注讨论纹理部分。

game_opengl.cpp: 查看新的硬件渲染代码,并考虑如何使渲染器同时支持软件和硬件渲染

目前,我们已经完成了硬件渲染器的基本设置,并且创建了一个OpenGL文件,在该文件中,我们初步布局了为了通过OpenGL渲染所需要的内容。到目前为止,这部分代码非常基础,只有极少的内容。虽然未来可能会逐渐增加一些功能,但整体来说,实现硬件渲染的工作量并不大。事实上,制作2D游戏的软件渲染器也没有比这个复杂多少,3D游戏相较而言要复杂得多,因为需要处理更多的管理工作和更多的细节问题。

对于我们的项目,目前并不需要担心3D的复杂问题,因为目前游戏还是2D的,或者说是“2D半”的状态。所以,所有的问题通常都出现在渲染管线的上游部分,我们需要考虑如何处理数据流而不是专注于渲染本身。在这个过程中,渲染的任务基本上是把精灵(Sprite)放到屏幕上,这样一来,实际的渲染代码并不会非常复杂。

在我们编写的渲染代码中,大家记得我们做了一些工作以确保可以在我们的“屏幕空间”中进行渲染。昨天我们通过设置投影矩阵来完成了这一点。我们测试了这一点并确保它正常工作,因此到现在为止,我们所需要做的就是将当前尚未连接的代码部分连起来。

基本上,我已经有了可以在OpenGL中运行的代码架构,我知道如果仅仅用这些代码去渲染屏幕是没问题的。但是目前我缺少的,是从实际调用渲染输出的地方,搭建一个良好的桥梁。具体来说,这里有一些地方会调用渲染输出函数,像是地面块的渲染部分就是这样调用的。所以问题的关键是,如何将这些代码部分整合起来,使得渲染既能在OpenGL上运行,也能在软件渲染上运行。

接下来,我们可以采用几种不同的方法来解决这个问题。首先,我打算以一种常见的方式来做,可能这并不是最优解,但我会先尝试这个方法。然后,我们可以讨论一下为什么这种做法并不是最佳方案,进一步探讨如何改进它。

有时候,当我有比较明确的想法时,我可能直接选择一种我认为是最有效的解决方案。这样做的好处是能够迅速解决问题,但也可能导致大家无法看到“不太好的方案”是如何引出更好的解决方案的。所以,如果是我不太熟悉的情况,我希望能和大家一起探讨并从中学习。但是如果是我已经有了比较清晰思路的方案,那么就可以直接实现,这样大家就可以从中看到更多的细节和解决问题的方法。

总结来说,我先展示一个大多数人可能会选择的方法,然后我们再一起讨论如何进一步改进,以达到更好的效果。

game_render_group.cpp: 引入 RenderToOutput,选择使用 OpenGLRenderGroupToOutput 或 TiledRenderGroupToOutput 渲染

我们目前的渲染流程中,只有在调用 TiledRenderGroupToOutput 函数时,才会真正进行渲染输出,除此之外,仅在调试系统中会调用这个函数。我们观察代码后发现,当前的结构是将渲染数据以瓦片(tile)形式输出到最终的渲染目标。

为了增强系统的灵活性和可扩展性,我们计划在这个流程中加入一个中间层,也就是一个中间的“shim”函数,用于在不同渲染路径之间做选择:根据具体情况决定是走 OpenGL 渲染路径,还是走软件渲染路径。

我们会在 RenderGroup 内部插入这个中间函数。具体来说,就是不再直接调用 TiledRenderGroupToOutput,而是调用一个新的函数,比如叫 RenderToOutput,这个函数内部再根据情况转调不同的实现:

  • 如果当前使用的是硬件渲染(如 OpenGL),就调用 OpenGLRenderGroupToOutput
  • 如果不是,则继续走现有的 TiledRenderGroupToOutput 路径

这个中间函数将根据 RenderGroup 的设置(例如一个标志字段,可能叫 IsHardware)来判断当前的渲染模式。

此外,我们注意到排序操作(SortRenderEntries)目前是在两个地方调用的:TiledRenderGroupToOutputRenderGroupToOutput,无论走哪条路径都需要排序操作。因此,我们准备将排序逻辑统一提前到中间层的入口处,在做实际渲染之前无论走哪条分支都先进行排序,以保证渲染顺序正确。

关于 RenderGroupToOutput(非瓦片版本)的保留与否,尚未决定。这取决于我们是否保留现有的 ground chunk 系统,或者将其移除,暂时还没有做出明确选择。

在实现 OpenGL 路径时,渲染所需的参数与软件路径略有不同:

  • 对于软件渲染,我们需要传入 RenderQueueRenderGroupDrawBufferTemporaryMemory
  • 而对于 OpenGL 渲染,目前并不需要 RenderQueue,因为当前版本中并不涉及多线程处理,只需 RenderGroup 和可能的 TemporaryMemory 即可,甚至 TemporaryMemory 可能都不需要

最后,我们希望避免在其他模块中引入 OpenGL 的头文件和依赖,以防止污染项目结构和增加耦合,因此需要特别注意模块间的隔离和接口设计。整个方案的目标是提升渲染架构的灵活性,并为将来支持更多渲染后端(例如 Vulkan 或其他软件渲染器)打下基础。
在这里插入图片描述

在这里插入图片描述

game_platform.h: typedef platform_opengl_render 并将 *RenderToOpenGL 添加到 platform_api

我们打算对当前的系统结构进行一个非常小、非常轻量的改动,主要是针对平台层的渲染回调进行拓展。

game_platform 中,我们原本已经定义了一系列平台相关的回调函数,这些回调数量目前保持得相对精简,结构简洁、易于维护。我们希望继续保持这种简洁性,但现在计划在此基础上增加一个新的回调接口,用于后续的讨论和扩展。

我们将会新增一个平台回调函数,暂时命名为 PlatformOpenGLRender,这个函数的主要目的是为 OpenGL 渲染提供平台层面的入口。虽然目前这个功能不会立即实现,但我们需要先为它留出接口。

为此,我们会:

  1. 在平台层的回调结构中添加一个新成员用于指向 PlatformOpenGLRender
  2. 为这个回调定义一个对应的 typedef 类型别名,确保类型安全和代码清晰。
  3. 不会为它定义宏(macro),因为我们并不打算立刻将它整合进宏系统中,也暂时不打算对其进行展开或具体实现。

这个接口的引入是为后续可能的功能留好空间,比如我们之后可能会根据平台调用这个函数来进行基于 OpenGL 的渲染操作。但现阶段我们只是先把这个结构安插进去,以便未来讨论、测试或者扩展用途。

总之,这一改动目的是在不破坏现有系统架构的前提下,预留一个针对 OpenGL 渲染的标准化入口点,为后续多渲染路径支持打下基础。
在这里插入图片描述

留下一个小小的雏形

我们现在决定先将这个新的 OpenGL 渲染接口保留在系统中,也就是说它暂时不会被实际调用或实现完整逻辑,而是作为未来扩展的起点静态存在。

目前的结构中,我们已经有了 RenderGroup 的定义,这是整个渲染流程中的核心数据结构之一。我们在保持现有结构不变的基础上,进一步在平台层中引入了 PlatformOpenGLRender 回调。

这个新的平台回调函数现在已经可以被调用,它作为渲染接口的一部分,提供了一个调用 OpenGL 渲染的统一入口点。这使得在后续设计中,可以根据运行时的渲染方式(如软件或硬件)动态决定走哪一条路径,同时也为平台层对接 OpenGL 提供了清晰的切入点。

当前这个回调虽然处于未激活状态,但已经具备了基本的结构:

  • 它被定义在平台回调接口集合中
  • 已创建了对应的类型定义
  • 可以在渲染逻辑中通过判定条件调用它

整体来说,这一步为未来引入完整的 OpenGL 渲染流程奠定了结构基础,也为系统的模块化、可移植性和可扩展性提供了良好的前提。我们可以根据实际需要逐步丰富其实现逻辑,而不影响现有软件渲染路径的稳定性。

game_opengl.cpp: 让 RenderGroupToOutput 只接受 *RenderGroup

我们现在准备将 OpenGL 渲染逻辑与平台层代码正式连接起来。在当前的 OpenGL 渲染实现中,我们并没有使用输出目标(Output Target)或者剪裁矩形(Clipping Rect),因此这个渲染路径已经天然符合我们之前定义的平台回调函数的签名要求。

从结构上看,理论上只需要做的事情就是注册好 IsHardware 标志位,并据此决定走哪条渲染路径即可。也就是说,只要设置好是否使用硬件渲染的标识,就可以实现平台层对 OpenGL 渲染函数的自动调用。

接下来,我们查看了 OpenGLRenderGroupToOutput 这个函数,它原本是直接由 OpenGL 路径使用的函数。现在我们要把它调整为通过平台层回调调用。

我们会将该函数接入平台回调系统,调用方式统一从平台入口进行。这一步的具体做法是将调用接口切换到 Platform 层的结构中,比如调用类似 Platform->OpenGLRender 的方式来代替之前的直接调用。

为了判断是否使用 OpenGL 渲染,我们还需要一个方法来判定当前是否处于“硬件渲染模式”。目前我们会先临时硬编码设置为“是”(即 IsHardware = true),后续会再进一步完善这个判断逻辑,使其能根据系统状态或配置自动决定。

这一步的意义在于:通过统一从平台接口调用 OpenGL 渲染函数,我们可以将渲染路径更加抽象、模块化,使得软件渲染和硬件渲染可以在同一逻辑结构中灵活切换。这个改动不仅减少了耦合性,还为未来支持多种渲染后端提供了良好的结构基础。
在这里插入图片描述

运行游戏并惊讶于事情是如何发生的

我们现在如果直接运行游戏,理论上应该在调用渲染回调的地方立即崩溃。因为此时我们并没有为 PlatformOpenGLRender 指定任何函数实现,它的值是空的(即为 0 指针),所以程序应该会在尝试跳转调用这个空指针时崩溃。

预期是程序会在执行到该回调时直接跳转到地址 0,导致段错误或者非法访问,从而使游戏立刻崩溃。

但实际上,程序并没有崩溃。这就非常令人意外。

这说明当前调用的那一部分代码并没有真正执行到 PlatformOpenGLRender,或者说该回调没有被触发。可能的原因包括:

  1. 条件判断未进入
    有可能我们在调用这个回调之前设置了某种判断条件,比如检查 IsHardware 是否为真,而该标志尚未启用或被正确设置,所以压根没有进入 OpenGL 渲染路径。

  2. 渲染路径未激活
    当前帧根本没有执行走向 OpenGL 的代码路径,可能仍然走的是旧的 tile 渲染逻辑,所以自然不会调用未绑定的 OpenGL 渲染函数。

  3. 函数指针未实际调用
    即使进入到了相关逻辑,可能由于代码中仍然缺少实际对该回调的调用,比如只是做了注册,但尚未完成调用机制的连接。

  4. 编译器优化或冗余结构
    也不排除某些路径在当前编译环境下被优化掉或绕开了,使得原本认为应该执行的代码实际上并未执行。

不管是哪种情况,这都说明当前结构还没有真正触发我们设想中的“跳转到空函数导致崩溃”的代码路径。这也是调试过程中的重要线索,提示我们回头检查实际是否已经调用到了 OpenGL 的回调逻辑。下一步应重点检查:

  • IsHardware 是否正确设置为 true
  • 渲染路径是否确实走到了 PlatformOpenGLRender
  • 是否在调用前做了空指针保护
  • 结构体中该函数指针是否已经初始化

通过这些验证,就可以定位为什么程序在理论上应该崩溃的地方没有出错的具体原因。
在这里插入图片描述

调试器: 步入 GameUpdate 并意识到我们使用了旧的函数

我们现在回到运行流程中,尝试验证在没有绑定平台层 OpenGL 渲染回调的情况下,程序是否会崩溃。

在最初的设想中,我们认为程序应该会在调用这个空函数指针时立即跳转到非法地址,导致崩溃。但实际运行时却没有发生崩溃,这让人感到困惑。

于是我们进一步分析执行流程,首先设置了断点,开始排查:

  1. 在主函数处设置断点,观察执行路径。
  2. 查阅主循环流程,发现 GameUpdateAndRender 是实际执行逻辑所在。
  3. 沿着调用链继续往下查看代码结构:从 WinMain 进入,处理系统消息后调用游戏更新与渲染函数。

然后定位到 GameUpdateAndRender 函数,理论上在此函数中应当已经改为调用新的 PlatformOpenGLRender 接口。

但实际检查时,我们发现:

  • 调用处仍然是旧的 TiledRenderGroupToOutput
  • 新的函数从未被调用
  • 原因是我们忘记替换调用位置,也就是“调用现场”(call site)仍然是旧的函数

所以真正的问题是,虽然我们定义并注册了新的平台回调,但由于没有在实际逻辑中把它接入替换旧有的渲染函数,所以新函数根本没有被调用,自然也不会出现跳转空指针导致崩溃的情况。

这是一种非常典型的“逻辑未连通”问题 —— 接口写好了、结构改好了,但忘了改掉原来真正用的地方。

总结:

  • 当前执行路径仍然使用旧的渲染调用,未跳转至新的平台 OpenGL 渲染接口
  • 因此空指针未被触发调用,程序未崩溃
  • 需要将调用点替换为调用 PlatformOpenGLRender,才能验证该接口是否安全、是否有效绑定

这个过程也揭示了测试时要特别注意“调用路径是否真正接通”,否则很容易误以为某段逻辑正常或异常,其实根本没被执行。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

再次运行游戏并触发第一次异常

我们决定重新尝试一遍,目的是验证新引入的 OpenGL 平台渲染接口是否能正确触发,并观察程序是否会因为未绑定导致崩溃。

在这次尝试中,我们确保修改了调用现场,真正将原本调用旧渲染函数的地方,替换为了调用新的平台层回调函数。这样一来,程序执行到这一步时就会尝试跳转执行 PlatformOpenGLRender

因为我们目前尚未对该函数指针进行绑定(仍为 null 或 0),所以程序在执行到这里时,理论上就会出现预期中的崩溃——跳转到空指针所指的地址,进入非法内存区域。

运行程序后,结果与预期一致:程序崩溃了。这正是我们想要看到的现象,说明这条新的执行路径现在确实已经打通,系统已经尝试调用新的平台渲染接口,只不过由于当前权限不足或指针未绑定,因此导致了异常终止。

这也说明:

  1. 渲染流程已经真正接入新的平台回调结构;
  2. 接口调用逻辑是有效的,只是未绑定时会导致崩溃;
  3. 后续需要确保在平台初始化阶段,正确将 PlatformOpenGLRender 指针赋值为实际的渲染函数,以防止空调用;
  4. 目前的崩溃行为证明新的接口路径没有遗漏逻辑,后续调试具备明确方向。

总的来说,这次崩溃是一个好现象,证明我们已经将结构接通,现在需要做的就是赋予这个接口正确的实现函数,保证其在运行时拥有合法的调用目标。

win32_game.cpp: #include “game_opengl.cpp”

现在我们可以开始将 OpenGL 渲染逻辑正式接入到平台层。在 Win32 层通过 game 的代码路径,我们可以包含 game 的接口定义,并将具体的 OpenGL 渲染函数注册到平台结构中去。

具体步骤如下:

  1. 在 Win32 平台代码中包含 game 的相关头文件
    这一步是为了让平台代码能够访问到我们定义的渲染接口类型和结构体,比如 PlatformOpenGLRender

  2. 将实际的 OpenGL 渲染函数赋值给平台结构中的函数指针
    在平台初始化阶段,我们填写这个平台结构体中的所有函数指针字段,现在包括了一个新的字段 RenderToOpenGL(即 PlatformOpenGLRender)。这个字段的值就是我们自己定义的 OpenGL 渲染函数,比如 OpenGLRenderGroupToOutput

  3. 处理编译器报错
    在代码整合过程中会遇到一些编译错误,比如头文件包含顺序、函数签名不匹配、类型不明等问题。我们需要逐一修复这些错误,直到编译通过。

  4. 一旦编译通过,整套逻辑就完成对接
    这样一来,游戏在运行时就可以通过平台接口调用 OpenGL 渲染逻辑,而不再依赖硬编码调用方式,也不会跳转到空指针导致崩溃。

这一步完成之后,平台结构就具备了完整的渲染调用能力。我们实现了以下目标:

  • 游戏模块和平台层之间通过清晰的接口进行沟通;
  • 渲染逻辑可以在运行时灵活切换(如 OpenGL 或软件渲染);
  • 系统结构更加模块化、解耦,易于后期维护和拓展。

接下来的任务是确保这个函数调用在正确的地方被触发,并对实际渲染结果进行验证,从而保证平台层 OpenGL 渲染的工作路径完全通畅。

这个地方我直接引入头文件
在这里插入图片描述

在这里插入图片描述

显示未定义
win32_game.cpp.obj 中未定义OpenGLRenderGroupToOutput
在这里插入图片描述

在这里插入图片描述

奇怪怎么还是未定义
在这里插入图片描述

在这里插入图片描述

函数名字不对
在这里插入图片描述

game_opengl.cpp: 使 OpenGLRenderGroupToOutput 接受 *OutputTarget

目前的工作重点是让整个渲染流程的整合部分顺利通过编译,并修复由于接口拼接和类型定义所引发的一系列问题。整体过程如下:


目标:让新加入的平台 OpenGL 渲染流程能够正确编译通过

  1. 处理未定义类型问题
    在当前代码中,因为没有包含 game_render_group 头文件,导致编译器无法识别与渲染相关的数据结构和函数声明。解决方式是直接在当前使用这些结构的文件中包含对应头文件。

  2. 关于 OutputTarget 的必要性
    一开始预期在新渲染函数中不需要 OutputTarget,但很快发现,实际渲染需要知道输出区域的宽度和高度。由于这些信息储存在 LoadedBitmap 结构中,所以必须传入该对象才能访问这些关键尺寸信息。为了暂时解决这个问题,决定仍然保留 OutputTarget 的传递。

  3. 添加参数以匹配函数签名
    将 OutputTarget 添加进函数调用参数中,确保在调用渲染接口时,传递了正确的上下文数据,避免在渲染流程中出现信息缺失。


编译器报错及修复过程

  1. OpenGL 渲染函数中对向量结构的误用
    在 OpenGL 的矩形绘制函数中,试图访问一个二维向量(如 v2)的 .x.y 成员,但实际传入的结构是一个四维向量(v4)。这就导致了类型不匹配,编译器报错。

  2. 追踪报错源头并确认类型定义错误
    进一步检查发现,这些本应是 v2 类型的变量在定义时误用了 v4,是一个明显的逻辑失误。这个问题的根源在于变量声明时的粗心操作,与结构使用逻辑不符。

  3. 修复错误类型定义
    将相关变量重新声明为 v2 类型,使得 .x.y 成员的访问成为合法操作,从而解决这类编译错误。


后续计划与思考

  1. 未来结构精简与渲染通道逻辑优化
    当前之所以需要额外传入 OutputTarget,是因为结构尚未统一,未来可能通过调整平台层和渲染结构的关系,使得输出尺寸信息能统一封装,无需显式传入。

  2. 渲染接口参数逐步明确
    当前阶段通过临时修正和显式传参解决问题,但随着接口结构稳定,还需进一步清理接口、重构参数传递路径,使得渲染函数既灵活又不冗余。


当前成果

  • 渲染相关头文件已经正确包含,类型可识别;
  • OutputTarget 参数已正确传入;
  • 向量类型错误已修复;
  • 编译器错误基本清除;
  • 整体渲染流程已具备连接平台调用与游戏渲染之间的桥梁。

接下来的重点是运行验证新流程是否正确绘制,并检查是否存在运行时错误。整体渲染结构的接入已经迈出了决定性的一步。

这部分之前好像没做

win32_game.cpp: 为了 OpenGL 的需要重新编码 Proj 数组

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

运行游戏并只看到我们的调试信息

现在我们已经完成了 OpenGL 渲染输出版本的接入,这意味着理论上整个渲染流程已经具备运行条件,并且在没有明显崩溃的前提下可以开始测试。


当前阶段的实现状态:

  1. OpenGL 渲染路径成功接入
    已经实现了一个新的 RenderToOpenGL 函数版本,并将其挂接到了平台接口中,现在可以在游戏运行时调用该路径完成图形输出。

  2. 理论上可以开始运行程序
    在没有严重逻辑错误或空指针的情况下,程序应该可以运行并不再崩溃。当前测试的重点不再是结构拼接或调用流程,而是渲染输出的实际表现。


当前功能受限:

  1. 尚未启用纹理功能
    目前尚未在 OpenGL 渲染路径中启用纹理处理,因此屏幕上不会显示任何基于纹理的图像数据。这也意味着大部分场景元素仍然无法正确显示。

  2. 只有纯色填充可以显示
    在当前状态下,只有通过纯色填充绘制的图形(solid rects)才可能出现在屏幕上,因为这些操作不依赖纹理,只依赖基础颜色设置和几何形状。


后续验证计划:

  1. 尝试运行并观察输出
    现在可以尝试运行程序并观察是否有任何图形显示,特别是是否能看到纯色矩形或 UI 元素,这将验证 OpenGL 路径是否真正生效。

  2. 纹理支持需后续补充
    要完整还原软件渲染中的效果,必须后续实现并接入 OpenGL 的纹理上传与绑定机制,包括图像解码后上传为 OpenGL 纹理、纹理坐标设置、着色器启用等。

  3. 测试重点转向图像管线正确性
    如果纯色矩形能够显示,那么说明 OpenGL 的基础绘图逻辑(顶点上传、颜色填充)是正常工作的,后续可以在这个基础上继续完善更复杂的图像效果。


当前收获总结:

  • OpenGL 渲染逻辑成功嵌入平台层;
  • 已可运行测试,无重大崩溃;
  • 基础渲染结构已具备;
  • 已知限制:纹理尚未处理。

下一步是继续观察输出验证绘制是否生效,然后逐步补上纹理相关支持,让图像完整显示。整个 OpenGL 接入工作已经完成了关键节点,逐步迈入功能扩展阶段。

win32_game.cpp: #if0 注释掉缓冲区绘制代码

目前的渲染系统中有两个关键问题需要处理:


当前情况确认:

  1. 虽然 OpenGL 渲染路径已实现,但还未真正切换到该路径
    渲染流程中虽然已经准备好了 RenderToOpenGL 的调用和结构支持,但实际运行时仍旧执行的是原来的旧路径(可能是软件渲染路径或后台缓冲清除逻辑)。换句话说,游戏并没有“走”进 OpenGL 渲染分支。

  2. 仍在运行旧的缓冲区绘制逻辑
    当前仍然存在调用旧的缓冲区清除和绘制逻辑(例如原先软件渲染流程中的 backbuffer 清除与绘制),而这些缓冲现在已经不再更新内容,因此继续显示的话将会是无效画面或残影。


所需操作:

  1. 需要清除原有的软件渲染输出逻辑
    将原有负责绘制 backbuffer 或输出未更新内容的代码移除,避免干扰新的 OpenGL 渲染输出。

  2. 显式切换到 OpenGL 渲染路径
    确保在主循环或者渲染入口处,已经正确调用到 RenderToOpenGL 函数。可以通过设置标志位如 IsHardware 或平台回调是否存在来判断并调用正确的渲染分支。


预期效果:

  1. 应当可以看到一些“纯色的垃圾图形”
    在正确切换到 OpenGL 渲染路径之后,由于当前没有纹理,绘制内容仅包括基本的纯色矩形或几何图形,因此在屏幕上可能会出现一些“无意义但可视”的纯色块。这个现象说明 OpenGL 绘制路径已经起效。

后续计划:

  • 完善纹理加载与绑定;
  • 替换所有旧的渲染路径调用为统一的调度逻辑;
  • 优化平台层判断机制;
  • 移除无效的 backbuffer 绘制流程。

整体上,正在逐步完成从软件渲染到硬件渲染(OpenGL)的平滑迁移。当前阶段主要是扫尾旧逻辑并验证 OpenGL 路径的有效性,接下来便是逐步构建图形细节功能。
在这里插入图片描述

在这里插入图片描述

运行游戏并看到一堆无意义的内容

目前的渲染系统表现正符合预期阶段的效果:


当前状态总结:

  1. 屏幕上确实只能看到一些基本的纯色图形
    由于当前还未集成纹理处理功能,因此大多数画面内容(如贴图、字体、图标等)都无法正确渲染。只要不是纯色矩形或简单形状,现阶段都不会显示。

  2. 说明系统确实进入了 OpenGL 渲染路径
    屏幕显示虽然简单、甚至是“垃圾图形”,但这恰好验证了渲染流程已经进入新的 OpenGL 路径。图形输出来自 OpenGL 绘制,而不是旧的回退路径或缓冲区残留。

  3. 目前是正确的阶段性结果
    虽然画面无法完整展现游戏图形,但这是过渡阶段的正常表现。结构已搭好,后续只需补全 OpenGL 所需的纹理上传与绑定机制,即可恢复完整图像渲染。


当前实现的核心逻辑:

  • 已成功跳过旧的后备缓冲绘制逻辑;
  • 已设置合适的判断分支,调用 OpenGL 渲染函数;
  • 平台接口中的回调机制开始生效;
  • 屏幕输出结果来自新路径,未出现渲染崩溃。

接下来需要完成的部分:

  • 实现纹理上传到 GPU(通过 glTexImage2D);
  • 设置纹理坐标与着色器处理;
  • 在渲染队列中恢复带纹理元素的绘制;
  • 检查是否所有绘制命令都成功映射到 OpenGL 接口。

目前的目标阶段已经达成,即渲染结构从软件路径平稳迁移至硬件路径,并成功生成可视输出。虽然功能有限,但验证了系统架构无误,后续只需逐步补完具体渲染细节。

game_opengl.cpp: 每帧发送纹理到 OpenGL

目前的目标是让纹理贴图简单地在 OpenGL 路径中运作起来,哪怕是以一种非常低效但可运行的方式,以便尽快看到效果并据此做进一步设计与优化。


当前任务目标:

实现一个最简单粗暴的纹理绑定和上传流程,哪怕效率极低,只要能跑出结果用于观察和测试。


操作步骤说明:

  1. 识别当前主要渲染缺失点
    主要问题集中在无法渲染位图(Bitmap),即纹理未上传到 GPU,也未绑定。

  2. 采用低效但可行的策略
    在每一帧绘制前都重新上传整张位图纹理并绑定:

    • 使用 glBindTexture 绑定纹理;
    • 使用 glTexImage2D 把 bitmap 数据传入 GPU;
    • 启用文本uring 模式;
    • 再绘制对应的矩形(quad)进行显示。
  3. 无需提前分配纹理 ID 池
    可以直接在使用时动态生成纹理对象(OpenGL 不强制要求纹理 ID 必须预分配)。


注意点与已知问题:

  • 效率极差:每帧上传完整纹理严重浪费带宽与性能;
  • 测试目的合理:当前阶段只是为了验证 OpenGL 路径纹理功能是否正常工作;
  • 一切临时,非最终方案:后续需要引入纹理缓存与管理系统,避免重复上传;
  • 依赖清晰绑定顺序:必须先上传纹理数据,再绑定使用,再调用绘制函数。

当前代码逻辑大致结构如下:

glBindTexture(GL_TEXTURE_2D, texture_id); // 绑定纹理 ID
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0,GL_RGBA, GL_UNSIGNED_BYTE, bitmap_memory); // 上传纹理数据
glEnable(GL_TEXTURE_2D); // 启用纹理

目的达成后再做的事:

  • 建立一个 GPU 纹理池,避免每帧重复上传;
  • 添加 bitmap 改变侦测机制,只有在更新后才重新上传;
  • 设计 bitmap ID 与 OpenGL 纹理对象的绑定逻辑;
  • 后续可优化为使用 PBO(Pixel Buffer Object)或纹理 atlasing 等高效策略。

通过这种最简单的方式,可以快速测试渲染管线是否贯通,为后续更复杂的集成和优化打下基础。
在这里插入图片描述

在这里插入图片描述

运行游戏并看到它几乎正常工作

目前已经实现了在 OpenGL 中渲染纹理贴图的最基础功能,但使用的方式极其低效,仅作为测试用途。


已完成部分:

  • 成功通过 OpenGL 将 bitmap 图像渲染到屏幕;
  • 图像内容(如 Cosine 图形)已经可以显示,说明纹理管线已经打通;
  • 渲染过程中已经可以看出纹理确实被上传并显示在屏幕上。

存在严重问题:

  1. 极其缓慢的性能表现
    每一帧都将整张纹理上传到 GPU(通过总线传输),导致极大延迟与卡顿。

  2. 未开启 Alpha 混合
    当前渲染结果缺乏透明度信息,即使贴图本身有 alpha 通道,也不会被正确混合进背景;

    • 屏幕上的所有图像都以完全不透明的方式显示;
    • 没有半透明或叠加效果。

当前观察总结:

  • 虽然整体做法效率极差,但验证了以下几个核心目标:
    • OpenGL 路径确实可以渲染纹理;
    • bitmap 数据成功传入 GPU 并被使用;
    • 代码流程可以顺利从平台层调用到渲染函数并完成绘制;
    • 渲染输出已进入实际游戏画面,但因为每帧都上传整个贴图,效率极低;
    • 在某些画面(如 Cosine)中可以看到贴图逐渐显示出来,说明流程正确但极慢。

下一步需要解决的问题:

  1. 启用 Alpha 混合

    • OpenGL 默认关闭 alpha 混合,需要手动打开;
    • 打开后才能让含透明通道的贴图正确显示(实现淡入、遮罩等效果);
    • 方法非常简单,将通过 glEnable(GL_BLEND)glBlendFunc 实现。
  2. 避免每帧重复上传纹理

    • 应当为每张 bitmap 分配 GPU 纹理,只在 bitmap 变化时上传;
    • 可引入纹理缓存机制,将 CPU bitmap 映射到 OpenGL 纹理对象,重复使用;
    • 优化上传流程,避免占用总线传输资源;
  3. 正确处理纹理尺寸与格式转换

    • GPU 需要对上传图像做格式匹配与内存映射,这一过程也会带来性能损耗;
    • 提前转换格式,或使用 GPU 友好的布局可以降低开销。

总结:

虽然目前渲染非常缓慢,但已经迈出了验证 OpenGL 渲染路径的重要一步。下一步将聚焦于性能优化和基础功能支持(如透明度渲染),逐步构建稳定高效的 GPU 渲染系统。
在这里插入图片描述

黑板:混合

目前正在解决纹理渲染中缺失 Alpha 混合(透明度混合) 的问题,这是在构建游戏图形系统中不可或缺的一步。当前虽然已经实现了基础的贴图渲染,但由于缺少混合功能,所有图像都是完全不透明的,因此必须补充这一关键渲染功能。


已确认的问题背景

  • 当前 OpenGL 管线使用的是固定功能(Fixed Function)版本,不支持自定义 shader;
  • 使用的是最基础的纹理渲染,无法自行控制像素与帧缓冲的混合方式;
  • 因此所有图像绘制都是覆盖式的,缺乏透明度处理。

GPU 渲染管线结构回顾(简化)

GPU 的管线大致可以分为以下几个阶段:

  1. 顶点处理(Vertex Stage):几何体的处理、变换;
  2. 片元处理(Fragment/Pixel Stage):负责每个像素的着色;
  3. 混合阶段(Blending):将当前片元输出与帧缓冲已有内容进行合成。

目前使用的是固定功能的 OpenGL 渲染流程,因此前两个阶段使用的是 OpenGL 内置逻辑,不支持自定义(未来可能升级到支持 Shader 的现代 OpenGL)。

混合阶段通常在绝大多数硬件上仍是固定功能的,即使其他部分可编程,这一部分仍然受限。也就是说:

  • 即使未来使用 Shader 进行像素计算;
  • 最终的颜色输出如何与已有的帧缓冲内容混合,依然受限于 OpenGL 提供的少量可选模式。

Alpha Blending 的理论基础

在帧缓冲上进行混合绘制时,需要将新绘制的像素颜色(Source)与帧缓冲已有像素(Destination)结合,计算出最终颜色(D’):

D' = Source × A + Destination × B

其中 A 和 B 是两个权重,可以通过 OpenGL 提供的混合函数设置。


预乘 Alpha(Premultiplied Alpha)混合公式

由于图像数据使用了预乘 Alpha格式,因此混合公式简化为:

Result = Source + (1 - SourceAlpha) × Destination

这是当前图像系统中使用的标准预乘 Alpha 混合,适用于所有 UI 和精灵绘制。


OpenGL 实现方法概述

OpenGL 提供了简单的接口设置混合方式,只需两步:

  1. 启用混合功能

    glEnable(GL_BLEND);
    
  2. 设置混合函数

    glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
    

这正是实现预乘 Alpha 混合的设置,其中:

  • GL_ONE 表示保留 Source 全部分量;
  • GL_ONE_MINUS_SRC_ALPHA 表示按剩余透明度混合目标色。

这样每个像素的输出色值就会按照预乘 Alpha 混合进入帧缓冲。


限制说明

由于当前还使用的是 OpenGL 固定功能管线,意味着没有自定义 Shader 的能力:

  • 无法在混合阶段访问帧缓冲已有内容进行任意操作;
  • 混合阶段只能通过 glBlendFunc 等固定函数控制,无法扩展更复杂逻辑;
  • 若未来需要更灵活的图像处理,如动态阴影、HDR、后期处理等,将必须迁移到支持 Shader 的现代 OpenGL。

总结

  • OpenGL 渲染已经初步成功;
  • 当前缺失 Alpha 混合导致所有图像不透明;
  • 通过启用 glEnable(GL_BLEND) 和设置 glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA) 可实现正确的预乘 Alpha 混合;
  • 尽管 GPU 的其他阶段已高度可编程,混合阶段依然受限,后续若需更多自由度需要引入 Framebuffer 或 Shader。

这是实现完整图像系统、精灵合成与 UI 绘制的关键一步。

game_opengl.cpp: 启用 GL_BLEND 并使用 glBlendFunc 指定混合方程

现在正在设置 OpenGL 的 Alpha 混合功能,以实现图像透明度的正确显示效果。在默认情况下,OpenGL 是关闭混合功能的,因此绘制出来的图像完全不透明,看不到任何透明区域。为了解决这个问题,需要启用并配置正确的混合方程。


当前设置背景

  • 默认状态下 GL_TEXTURE_2D 是开启的;
  • 大多数绘制操作都会使用纹理,因此默认保持开启状态;
  • 在不使用纹理时,可以临时关闭再绘制,以保持灵活性。

设置透明度混合的步骤

1. 启用混合功能

混合功能默认是关闭的,因此需要显式开启:

glEnable(GL_BLEND);

这一步是关键,否则所有图像都会以不透明形式绘制,不管纹理中是否包含透明区域。


2. 配置混合方程

通过 OpenGL 的函数 glBlendFunc 可以设置混合方程中的两个重要参数:源因子(Source Factor)和目标因子(Destination Factor)。这个函数的调用方式如下:

glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
参数解释:
  • GL_ONE:表示源颜色(即当前正在绘制的像素颜色)保持其全部值,不进行缩放;
  • GL_ONE_MINUS_SRC_ALPHA:表示目标颜色(即帧缓冲中已有的颜色)按 1 减去源像素的透明度进行缩放。

这就完美地实现了**预乘 Alpha 混合(Premultiplied Alpha Blending)**的公式:

结果色 = 源色 + (1 - 源透明度) × 目标色

这种混合方式可以正确地表现出图像中的半透明、阴影、淡入淡出等视觉效果,是大多数 2D 游戏、UI 绘制的标准做法。


重要说明

  • glBlendFunc 的设置需要在开启混合之后进行;
  • 只需要设置一次即可,全局有效,直到显式更改;
  • 设置后的混合效果会应用于所有后续绘制调用,直到再次修改。

整体效果

通过启用 GL_BLEND 并使用 glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA)

  • 透明区域会正确显示;
  • 精灵图像之间可以实现自然的叠加;
  • 显示效果与设计图保持一致;
  • 可以为后续的光影、特效打下基础。

小结

  • 启用 OpenGL 的混合功能后,图像不再不透明;
  • 使用正确的混合函数参数,可以实现符合预期的透明叠加;
  • OpenGL 提供的 GL_ONEGL_ONE_MINUS_SRC_ALPHA 恰好能满足预乘 Alpha 的需求;
  • 混合配置是渲染流程中必须的一个部分,关系到最终显示效果的真实性与美观度。

至此,基础的透明度渲染已经完成,后续可以继续扩展更复杂的图像渲染逻辑。

在这里插入图片描述

在这里插入图片描述

https://registry.khronos.org/OpenGL-Refpages/gl4/html/glBlendFunc.xhtml
在这里插入图片描述

在这里插入图片描述

搞错了是GL_ONE_MINUS_SRC_ALPHA 不是GL_ONE_MINUS_DST_ALPHA
在这里插入图片描述

GL_ONE_MINUS_SRC_ALPHAGL_ONE_MINUS_DST_ALPHA 是 OpenGL 中用在混合函数 glBlendFunc 里的两个常量,它们代表了不同的混合因子,分别对应源图像和目标图像的透明度反比(即 1 - alpha)。它们的区别在于它们引用的 alpha 值来源不同


概念对比

常量名中文名称计算值alpha 值来源用途说明
GL_ONE_MINUS_SRC_ALPHA源透明度反比1 - src_alpha来自当前要绘制的像素(源图像)通常用于控制目标图像保留多少
GL_ONE_MINUS_DST_ALPHA目标透明度反比1 - dst_alpha来自已经绘制在 framebuffer 中的像素(目标图像)一般用于更复杂的后处理合成,例如图像渐隐、蒙版等

实例说明(假设 alpha 值为 0.6)

GL_ONE_MINUS_SRC_ALPHA
  • 公式是 1 - 0.6 = 0.4
  • 通常与 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) 搭配使用
  • 这是最常见的混合模式,用于普通透明图像混合:
    最终颜色 = src_color × src_alpha + dst_color × (1 - src_alpha)
    
GL_ONE_MINUS_DST_ALPHA
  • 如果 framebuffer 中已有图像的 alpha 为 0.3,那就是 1 - 0.3 = 0.7
  • 常用于 更特殊的合成效果,比如源图像的影响根据目标图像的透明度来调整
  • 示例:
    最终颜色 = src_color × src_alpha + dst_color × (1 - dst_alpha)
    

应用场景

场景推荐使用的常量
正常的透明度混合(2D 游戏精灵)GL_ONE_MINUS_SRC_ALPHA
特殊合成需求,如多层滤镜或后处理GL_ONE_MINUS_DST_ALPHA

小结

  • GL_ONE_MINUS_SRC_ALPHA:更常用,处理源图像与背景混合;
  • GL_ONE_MINUS_DST_ALPHA:处理与已有内容(目标图像)相关的混合需求;
  • 混合效果由 glBlendFunc(srcFactor, dstFactor) 控制;
  • 使用时需根据图像合成目标谨慎选择,否则可能出现视觉错误或完全不透明。

“简直就像硬件已经为此设置好了!”

几乎可以说,硬件天生就是为实现这种操作而设计的——这是因为这正是图形渲染中最常见、最核心的需求之一。我们设置透明度混合的整个过程几乎被硬件“默认支持”,原因就是它在图形渲染场景中扮演着极其关键的角色。


背后原理详解:

现代 GPU 虽然在很多部分支持高度可编程的渲染管线(如顶点着色器、片元着色器等),但在像素混合(blending)这个阶段却仍然保留了固定功能(fixed-function)的逻辑,这是因为:

  • 混合运算是所有图像渲染中最普遍的操作之一,例如半透明、图层叠加、阴影、光晕等;
  • 性能要求极高,为了确保混合操作的执行速度,硬件直接对这一功能进行专门优化;
  • 操作模式相对固定,虽然支持一些参数配置,但实际使用中大多数混合行为都是类似的,因此没必要将其设计成完全可编程;
  • 便于统一处理多种场景,如 UI 渲染、2D 游戏、粒子系统、GUI 框架等几乎都依赖于这种标准的 alpha 混合逻辑。

操作设置符合预期

我们启用 GL_BLEND,并调用 glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA),可以直接调用硬件预设的混合路径,这种路径效率极高,而且效果标准,基本不需要再手动处理底层像素运算。

这让整个渲染过程变得非常自然和高效,好像硬件就是“为这一刻”量身定做的一样。


实际表现

  • 图像边缘过渡自然;
  • 半透明叠加真实可信;
  • 混合效果无需手动实现,只需设置一次,即可由硬件全权接管;
  • 即使在复杂画面中,也能保持高帧率和流畅度。

总结

整个渲染流程中,alpha 混合几乎是 GPU 内建并高度优化的标准操作。我们所做的设置并不是在“定制”混合行为,而是在“唤醒”硬件为我们准备好的通道。正因为这个功能如此常用,所以它几乎像是显卡专门为它而生的。只需调用接口,就能发挥其全部潜力。

运行游戏并看到我们成功地重现了图像

现在如果我们启用了混合并运行程序,就可以明显看到我们已经成功地将图像正确地重新显示出来了。从效果上来看,图像的透明度、颜色叠加等视觉表现都和预期一致,说明混合逻辑的配置是有效的。


当前效果总结:

  • 图像正确还原:可以清楚地看到原始图像的重构结果,显示效果趋近于真实的绘制目标。
  • 透明度表现正常:经过启用混合后,图像不再是完全不透明的,而是实现了正确的 alpha 混合,像素叠加效果自然。
  • 色彩混合合理:混合出的像素色值与原始预期一致,预乘 alpha 的混合逻辑成功复现。
  • 显示流程完整:尽管渲染效率较低,但整个管线从上传贴图到绘制完成的路径是闭环的。

性能问题依旧存在:

尽管功能上已经跑通,但仍存在明显的性能瓶颈:

  • 每一帧都重新将整张纹理上传到 GPU,这是极其低效的行为;
  • 显存带宽和 PCI-E 总线资源被大量浪费,导致帧率严重下降;
  • 这种方式不适合用在实际项目中,尤其是画面元素复杂或纹理数据较大的情况下。

几个注意点和问题:

  • 虽然现在显示看起来是“正确的”,但其实仍然存在一些小问题,例如:
    • 部分图像区域可能还存在色彩精度偏差;
    • 混合结果在不同显卡上可能略有差异,特别是处理 premultiplied alpha 的具体方式上;
    • 如果贴图数据本身更新较频繁,就不能用这种方式长久运行,必须优化上传逻辑。

总结:

我们现在已经实现了透明混合并正确显示出图像,是渲染管线的重要进展。虽然还存在性能和细节上的问题,但当前结果已经是一个非常接近目标的近似版本,验证了整个流程是合理的。接下来只需优化贴图上传策略,就可以将性能和效果统一提升上去。
在这里插入图片描述

game_render_group.h: 向 loaded_bitmap 添加 Handle,以存储位图是否已提交

为了减少程序运行的慢速问题,提出了一个简单的解决办法。这个解决方案是通过修改资产加载部分来优化,特别是在处理位图时。具体来说,资产加载时会涉及到一个名为“加载位图”的概念,位图会被保存在资产内存的头文件中。通过将这个位图的句柄设置为一个简单的值,我们可以避免每次都重新加载位图,从而提升性能。

为了实现这个目标,可以在加载位图时,给它设置一个默认的句柄值。例如,在代码中检查加载位图时,如果句柄尚未被提交(即它的值为零),就可以直接跳过提交操作,避免每帧都传输位图数据。通过这种方式,避免了不必要的性能开销。

这种方法虽然是一种临时的应急方案,但它有助于暂时缓解当前的性能问题,尤其是在开发初期。当涉及到 OpenGL 和纹理的复杂度增加时,未来会有更完善的解决方案来优化渲染流程。

在这里插入图片描述

game_opengl.cpp: 根据条件绑定纹理或设置句柄

为了优化纹理提交和减少不必要的性能开销,可以通过检查纹理句柄是否已存在来决定是否需要提交纹理。具体来说,当处理一个位图时,首先要检查它是否已经有一个已设置的句柄。如果已经有句柄,就直接绑定并使用该句柄;如果没有句柄,则需要为其生成一个新的句柄,并在第一次提交时将其传递给图形卡。

一旦提交了纹理,并将句柄绑定到图形卡之后,就不需要在之后的帧中再次提交该纹理。这样,可以大大减少每帧的计算负担,因为每个纹理只需在第一次使用时提交一次。为实现这一点,可以使用全局变量来跟踪提交的纹理句柄,甚至可以通过简单的计数器来管理这些句柄。

通过这种方式,可以有效避免每次渲染都重新提交纹理的问题,从而提升性能和减少不必要的操作。
在这里插入图片描述

运行游戏并看到它现在运行得更快

在完成上述纹理句柄缓存与一次性提交逻辑后,纹理在首次使用时被正确提交给图形卡,并在后续渲染中重用,从而解决了之前每一帧都重复提交导致的性能瓶颈问题。现在渲染速度明显提升,整体运行效率大幅改善,不再存在严重的卡顿或延迟现象。

然而,尽管性能问题得到解决,屏幕上的实际图像效果依然存在明显问题:当前通过OpenGL渲染的图像显示效果远不如之前使用软件渲染器(software renderer)时的效果。图像的质量和视觉呈现明显下降,显示结果与预期有较大偏差。

对比两种渲染方式后,发现出现这种情况的原因可能不是出在纹理处理流程,而是排序关键字(sort key)逻辑存在错误。排序关键字的作用是决定图元(如精灵)在绘制时的前后顺序,它直接影响透明图层的叠加和显示效果。如果排序逻辑有误,可能导致图层顺序错乱、遮挡关系错误,进而导致最终图像出现异常,无法正确还原软件渲染时的效果。

因此,当前需要重点检查并修复排序关键字的生成和处理逻辑,确保图元在渲染管线中按照正确顺序进行绘制,以恢复图像的正确显示和层级关系。只有这样,视觉质量才能和软件渲染器保持一致。

运行游戏并指出图像有闪烁的现象

我们在调试过程中发现了一个新的视觉问题。虽然之前的问题修复得比较顺利,但现在屏幕上呈现的图像效果非常糟糕,尤其在原生查看时,画面有种“闪烁”的感觉,细节看起来很模糊、不清晰。

这种画面质量问题并不是由于程序逻辑错误或渲染流程崩溃导致的,而是跟图像的采样方式有关。之前在实现双线性过滤(bilinear filtering)时,已经分析过相关问题。双线性过滤的作用就是在图像缩放时进行平滑插值,避免锯齿和不规则的像素跳变,从而使图像看起来更加柔和自然,细节也更加清晰。

而当前的问题是:虽然使用了现代图形硬件进行渲染,但显示效果却明显不如之前的软件渲染,图像存在锯齿、抖动或色彩跳跃的问题。这说明当前图形管线中没有正确启用双线性过滤,或者在绑定纹理后,没有设置合适的过滤模式。

这种现象表明,图形硬件虽然强大,但如果我们没有正确配置其参数,比如纹理过滤方式,它依然会产生非常粗糙、难看的输出。默认的过滤模式可能是“最近点采样”(nearest-neighbor sampling),也就是一个像素一个颜色,导致图像边缘生硬、易抖动,尤其在有缩放或运动时更明显。

因此,为了解决这个问题,需要:

  • 在绑定纹理后,显式设置纹理过滤模式为双线性过滤(GL_LINEAR);
  • 确保在缩小/放大情况下分别使用 GL_LINEAR_MIPMAP_LINEAR 或相关的 MIPMAP 过滤方式;
  • 检查纹理坐标是否正确计算,避免像素中心错位。

最终目标是让当前的图形渲染效果至少达到甚至超过之前的软件渲染质量,毕竟我们现在使用的是专用图形硬件。图像模糊或闪烁的问题不能被接受,必须通过合理配置图形管线来解决。

game_opengl.cpp: 重新启用双线性过滤使用 glTexParameteri

之所以当前画面效果不理想,问题出在我们设置纹理过滤方式时的选项。

在设置纹理参数时,我们用了 GL_NEAREST 作为过滤方式。这是最基础、最原始的采样方式,也就是最近点采样:无论纹理被放大还是缩小,都会直接选取最接近的一个 texel(纹理像素)进行绘制。这种方式渲染速度快、实现简单,但会带来严重的锯齿、像素跳变、边缘闪烁等问题。

在纹理渲染中会遇到两种情况:

  • 放大(Magnification):原始纹理比屏幕上显示的尺寸小,导致需要对纹理进行放大来填满区域。
  • 缩小(Minification):原始纹理比实际显示区域大,需进行缩小处理。

OpenGL 允许我们分别指定这两种情况时使用的过滤方式。在我们的设置中,两者都使用了 GL_NEAREST,所以效果会非常粗糙。

我们需要使用的是 GL_LINEAR,这个选项实际上就是双线性过滤(Bilinear Filtering)。双线性过滤会在目标像素对应的纹理坐标周围取四个 texel,然后根据其距离进行插值计算,得出一个更平滑的像素值。这样可以显著提升画面的柔和度,消除闪烁和锯齿现象。

虽然 OpenGL 这里用了 GL_LINEAR 这个通用名字,它适用于一维、二维甚至三维纹理:

  • 对 1D 纹理来说是线性插值;
  • 对 2D 纹理来说是双线性插值;
  • 对 3D 纹理来说是三线性插值(有时也称体积插值)。

我们当前使用的是二维纹理,因此 GL_LINEAR 实际就代表了 双线性过滤。虽然名字通用,但其含义会根据纹理的维度自适应调整。

要解决当前画面模糊和锯齿的问题,只需修改纹理参数设置,将过滤模式从 GL_NEAREST 改为 GL_LINEAR 即可。这一步操作很简单,却能显著提升渲染质量,使硬件渲染效果真正达到应有水平,不再逊色于软件渲染。

在这里插入图片描述

再次运行游戏并注意到现在更像软件渲染器,但我们的地面块现在有问题

现在画面显示效果已经明显改善,看起来更接近软件渲染的结果了,不再有那些难看的闪烁和抖动,整体表现回到了一个可以接受的状态。至此,硬件渲染器基本已经搭建完成,效果还算不错。

不过目前还有个问题——地面块(ground chunks)渲染存在一些异常,看起来像是出错了。虽然这种情况在一定程度上可以预期,因为地面块在渲染逻辑上比较特殊:它们复用了纹理句柄,并且在管理和提交数据时也采用了非常规的方式,所以自然容易出问题。

除了地面块,其他部分的表现都比较正常。角色、界面、其他场景元素看起来都没什么问题,可以自由行走和浏览,硬件渲染运行良好。虽然地面块的问题似乎是一个提交逻辑的 Bug,但目前还不确定具体原因,可能需要之后单独深入分析。

可以注意到,出现这个问题的纹理呈现出诡异的紫色,但具体原因暂时还不明确,显然值得调试和查明原因。不过当前阶段可能不是处理这个问题的最佳时机,所以可以暂时放一放。

总的来说,目前游戏已经完全运行在硬件渲染器上,软件光栅器已经被彻底关闭了。值得注意的是,即便这块显卡是 2010 年发布的,已经有 6 年历史了(大约是 2016 年购买的),但它依然毫无压力地完成了全部渲染任务。这也证明了一个观点:对 2D 游戏来说,图形硬件的性能是极其充足的。

很多人讨论是否需要用到 VBO(Vertex Buffer Object)或各种高级特性,其实在 2D 游戏里,如果不做非常不合理的操作,完全没有必要过度优化。只要结构清晰、逻辑合理,哪怕是很老的显卡也能轻松胜任。

当然,如果做一些非常极端的操作,比如过度重叠绘制(overdraw)等,是有可能压榨出显卡性能瓶颈的。但对一般的 2D 游戏场景,哪怕是市面上用 Unity 随便开发的游戏,只要不是设计失误,显卡都可以毫不费力地运行这些内容。

事实上当前我们的渲染代码极其简单,甚至几分钟就能写完的主渲染循环就已经足够应对所有渲染任务,并且运行良好,这说明我们已经有了一个稳定的硬件渲染基础。

不过要注意,目前仍然有一个我们即将面对的核心问题:纹理提交的异步性问题。由于我们的游戏架构采用的是渐进式资源加载(asset streaming),而图形硬件在处理纹理提交时往往不能很好地异步执行,这就成了接下来我们必须重点解决的问题。

在这里插入图片描述

描述当前架构的问题,并在过程中启用调试系统

我们接下来想要讨论的重点,是当前这种架构模式存在的一些问题,特别是为什么我们并不完全满意目前的实现方式。

尽管当前的渲染函数本身是没问题的,除了绑定纹理部分使用了全局变量这种不优雅的手段,这部分是一个临时性 hack,我们之后肯定得将纹理绑定的逻辑抽离出去,整合进资产管理系统中,而这将是最具挑战性的部分。但暂时先不考虑纹理绑定的问题,其余部分功能上都没有太大问题。

真正的问题出在这个渲染函数是如何被调用的。目前我们在调用方式上已经开始显露出一些架构性缺陷。一个最直观的例子是调试代码现在散落在各处,调试 overlay 层的位置不清晰,比如之前为了找调试渲染的入口还得沿着调用链往下翻,看最后是哪个地方在调用 RenderGroup,这种方式让维护和扩展变得困难。

同时,我们在调用中还传入了一个位图(bitmap)作为输出目标,但这个做法本质上是有问题的。渲染目标并不应该仅仅是一个具体的 bitmap,这实际上是对渲染抽象理解不清导致的设计问题。我们需要一个更抽象的“渲染目标”概念,而不是直接使用 bitmap 对象。虽然这点目前影响不大,但它预示了更深层的架构问题。

更严重的是,这种架构方式使得游戏逻辑与平台层之间的职责边界开始模糊。现在的流程是,游戏逻辑通过某种方式再次“绕回”平台层去完成 OpenGL 的实际渲染,这样一来,平台层就不得不以一种通用且开放的方式支持来自游戏逻辑的 OpenGL 渲染调用。这就破坏了之前非常清晰的分工模式。

我们原先的架构是:平台层只负责将一块内存交给游戏逻辑,游戏逻辑渲染完了再交还给平台层,平台层想怎么用这块内存都可以,比如用来截图、录像、做测试回归比较、缓冲存储、显示到屏幕等等,非常灵活。而现在的架构导致渲染行为被固定死了,游戏逻辑必须直接调用平台层的 OpenGL 逻辑,失去了原有的抽象和灵活性。

更重要的是,之前的设计里,游戏完全不需要知道平台层的任何细节,而现在它必须知道自己在用 OpenGL 渲染,并且还需要配合平台层的上下文,这种耦合会导致扩展性变差、移植困难、调试变复杂等一系列问题。

因此,我们想要思考一种方式,能够重新恢复那种解耦的架构优势。一种可能的思路是将“渲染列表(render list)”作为输出结果,而不是最终像素位图。也就是说,让游戏逻辑输出一份完整的渲染指令列表,然后平台层可以决定如何处理这些指令,比如提交给 OpenGL 渲染,或者用于其他目的(测试、调试、截屏等)。

这会让整个架构重新回到数据驱动的风格,而不是行为驱动,从而让平台层重新获得对渲染流程的掌控权,同时也让游戏逻辑不必承担平台相关的责任,实现职责分离和良好的模块化设计。这就是我们接下来想要探索和实现的方向。
在这里插入图片描述

在这里插入图片描述

黑板:之前和新的数据流

目前我们面临的问题在于渲染流程架构的变化。之前我们使用的是一种非常清晰、顺畅的流程:

平台层完成自己的准备工作后,调用游戏逻辑,并传入一个位图作为渲染目标。游戏逻辑只负责在这块位图上绘制图像,渲染完成后返回平台层,然后平台可以自由地对这个位图进行处理,比如显示在屏幕上、保存、比较测试等等。这种设计很清晰,平台控制权明确、渲染行为透明。

但现在的架构发生了变化。现在的平台层同样调用游戏逻辑,但游戏逻辑在内部却又需要绕回平台层,发起实际的 OpenGL 渲染调用。渲染不仅发生在游戏逻辑中,而且调用路径呈现出“回旋式”的流程:平台 → 游戏 → 再次平台 → 执行渲染。整个过程中不只一次往返,还可能涉及多次,比如游戏渲染一次,调试信息渲染再来一次。

这就带来了一个核心问题:平台层失去了对渲染时机的控制权。比如出于性能优化、安全同步或多窗口支持等场景下,平台可能需要对渲染时机做出调度。但现在的结构下,所有时机都由游戏逻辑主导,平台完全无法插手。这种“反客为主”的调用方式,会使平台代码变得复杂,为了支持灵活渲染还可能不得不引入异步提交、延迟执行等复杂机制。

我们更倾向于恢复原来那种清晰的架构思路,但将输出从“位图”改为“渲染指令列表”(也可以称为渲染组或渲染缓冲区)。也就是说:

  • 平台仍然负责初始化调用;
  • 游戏逻辑收到一个“渲染组缓冲区”;
  • 游戏逻辑只向这个缓冲区填入渲染指令,而不直接进行渲染调用
  • 游戏逻辑完成后返回;
  • 平台层再读取渲染组,决定如何处理:提交给 OpenGL、Direct3D、保存为图像、做回归测试等。

这样架构的优势是显而易见的:

  1. 隔离性强:渲染细节全部由平台负责,游戏逻辑不需要关心底层 API。
  2. 更易测试:平台可以捕获渲染指令,做离线分析和对比。
  3. 更灵活扩展:支持多种渲染路径(如立体渲染、多视角)、甚至导出中间数据。
  4. 便于移植:将渲染从游戏逻辑中抽离,使得平台移植变得轻松。
  5. 结构更清晰:流程中没有回调、也没有上下文切换的隐含依赖,维护性更好。

我们目前的实现中,在渲染部分已经出现一些不必要的复杂性,比如平台层中有太多细节被迫暴露给游戏逻辑,这显然违背了良好的分层原则。我们希望逐步清理这些部分,最终达到一个更干净、职责明确的架构。理想状态下,平台只提供必要的接口,比如用于添加渲染项、完成渲染、分配/释放缓冲区等基本操作即可,调试功能的接口则可以保留,但不应影响核心架构的清晰性。

我们打算在接下来的工作中开始着手做这些改进,逐步调整架构,使其更加健壮、灵活,并易于维护和扩展。

Q&A

为什么选择 OpenGL 而不是 DirectX?

我们选择 OpenGL 而不是 DirectX 的原因有两个层面。

第一个是非常直接的现实考量:跨平台兼容性。OpenGL 是一个跨平台图形 API,可以在 Windows、Linux、macOS 上运行,也可以配合 SDL 等库使用。这意味着无论用户使用哪种系统,都可以跟随进度一起学习和开发,而无需另学一套不同的图形接口。如果我们使用的是 DirectX,那么非 Windows 用户就无法直接参考和实现这些渲染部分的内容,会大大降低教程或代码的可移植性和受众覆盖率。OpenGL 在这一点上是“天然优势”,在做跨平台项目时是一个非常自然、合理的选择。

第二个是从开发者技术选择的角度来看。虽然 DirectX 主要在 Windows 上使用,并且在 PC 游戏市场中占据主导地位(大约占据了 97% 的份额),所以在只面向 Windows 的商业游戏项目中使用 DirectX 是完全合理的。但如果项目对跨平台有任何需求,或者想尽量减少平台依赖,OpenGL 的优势就非常明显了。

此外,OpenGL 也提供了一些 DirectX 没有的灵活性,尤其是结合厂商扩展时。例如 NVIDIA 的一些专有扩展可以带来更直接的硬件访问能力,允许实现一些高级的图形特效或者优化路径,这些往往在 DirectX 中是做不到的(即便是 DirectX 12 也不一定能完全覆盖)。

虽然 DirectX 12 和 OpenGL 在架构层面差异很大,但在实际项目中,大多数开发者仍然在使用 DirectX 11,这与 OpenGL 相比其实没有本质上的难度差异。而且如果确实希望用 DirectX,也可以通过某些封装层或者中间件来实现跨平台运行,例如通过一些兼容层在 macOS 上运行 DirectX 代码,这样的方式虽然性能略有损耗,但也是可行的。

我们个人倾向使用 OpenGL,并不是因为 DirectX 不好,而是因为 OpenGL 的灵活性、可移植性以及与现有代码结构更契合,同时也更适合教育和分享场景——读者可以轻松在自己设备上构建和运行。

总结来说:

  • OpenGL 跨平台能力强,对所有主要桌面系统都有良好支持;
  • 适合教学和参考,方便他人跟进学习;
  • 不依赖特定平台生态,便于长期维护和扩展;
  • 灵活性高,通过扩展支持更多底层特性;
  • 在实际项目中没有遇到明显限制或问题

当然,如果目标非常明确,只在 Windows 上发布游戏,DirectX 也是完全合理的选择。这并没有对错,只是根据需求做出的不同决策。我们更倾向于保持通用性和灵活性。

你注意到引入排序时地面瓦片的变化了吗?

我们注意到地面瓦片在调用新设施时出现了一些变化,主要是因为这些瓦片现在被进行了排序,而之前并没有做任何排序处理。我们在现场也讨论过这一点。瓦片的排序状态发生了变化,但我们暂时还不确定是否真的在意这个变化。

目前看来,这种排序的变化可能对我们没有太大影响,原因在于从当前的游戏设计角度来看,地面瓦片这个系统可能根本就不会被真正使用。也就是说,虽然它现在出现在系统中并有调用行为,但按照目前的设计路线,地面瓦片可能会被完全移除。这个判断基于我们目前对游戏整体结构和流程的规划,而且这个设计已经趋于稳定,基本不会再做大调整。

当然也不能完全排除未来可能会再次评估和改动设计,但在现阶段,地面瓦片的存在感和功能价值都非常低,很有可能最终会作为一个不必要的系统被裁剪掉。因此,关于它们现在是否进行了排序,其实并不重要,属于一个独立的、次要的问题。我们当前的重点还是聚焦在整体架构的清晰性和功能的核心实现上,而不是这种潜在会被淘汰的部分。

我之前问过这个问题,但我想再次听听你的看法,为什么基于数据协议的接口比基于函数调用的接口更好?

这是一个相对复杂的问题,我们并不认为基于数据协议的接口一定优于基于函数调用的接口,或者反过来。它们各自有明确的优势与劣势,需要根据具体场景权衡使用。

基于数据协议的接口的主要优势在于其提供了非线性访问重复访问的能力。也就是说,当我们将所有的操作记录成一个数据结构,而不是立刻执行这些操作时,就可以在之后任意地读取这些记录,并且不止一次地使用这些记录。举一个具体的例子,比如我们在渲染系统中,将所有需要绘制的精灵和它们的位置收集起来后并不立刻渲染,而是先缓存在一个缓冲区中,这样我们就可以对它们进行排序处理,然后再统一进行绘制。这样的处理方式在基于函数调用的接口中是非常困难的,除非我们自己先做一轮记录工作——也就是说,最终也得实现类似的数据结构。

因此,非线性访问与多次访问同一信息是基于数据协议接口的一个巨大优势。它也天然具备更容易记录操作历史的能力,例如我们可以将整个帧的渲染指令保存下来,之后可以用来进行离线分析、回放、测试等用途,这也算是一种附加的便利性。当然,这些功能理论上在函数调用模式下也可以通过增加相应记录逻辑来实现,但会更复杂和更不自然。

我们倾向于使用基于数据协议的接口,主要是因为它在“立即模式(Immediate Mode)”和“保留模式(Retained Mode)”之间取得了一种很好的平衡。我们不喜欢保留模式,因为它强制需要在调用者与内部状态之间进行同步,而这种同步往往很复杂又容易出错。但我们也不喜欢纯粹的立即模式,因为它太线性,不够灵活,无法进行如排序这类优化,也难以对渲染命令进行重新组织与控制。

所以我们更倾向使用一种“看起来是立即模式”的设计:对于调用者来说,他们只需要调用类似 Draw 的函数,完全不需要关心底层的状态同步、资源管理、内存释放等问题,就像调用一个立即模式接口一样轻松。但实际内部实现是将这些操作记录下来,在适当时机统一处理(比如排序后再渲染),实现效果上则接近保留模式的能力。

这种方式不仅优化灵活性、提升效率,也让接口使用者拥有更轻松的调用体验,无需了解任何同步细节。这种“对调用者是立即模式、对内部是数据记录”的设计思想,在很多地方都可以发挥作用,尤其适合对性能与可控性都有要求的场景。

最后一点补充,很多人容易误解我们对“立即模式 GUI”的看法。我们从未否定状态管理的重要性,相反,状态管理非常关键。而我们强调的是,让调用者从状态同步的痛苦中解放出来,才能真正实现高效、简洁的接口使用体验。正如在我们的渲染系统中,调用者调用 Draw 后并不用关心它会不会被排序或者什么时候才被绘制,只知道最终屏幕上会呈现出想要的结果,这才是我们追求的接口设计理念。

假设:渲染系统

  1. 基于函数调用的接口(传统方式)

在基于函数调用的接口中,渲染操作会是按顺序执行的。假设你有一个函数,叫做 DrawSprite(x, y),每次调用它时,你传入一个精灵的位置信息,它就会立刻在屏幕上绘制出这个精灵。

// 立即绘制
DrawSprite(100, 200);
DrawSprite(200, 300);
DrawSprite(300, 400);

每次调用 DrawSprite 时,精灵都会立即绘制到屏幕上。这个过程是线性的,不会被改变——它会按照传入的顺序一个接一个地执行。如果你想对这些精灵做一些排序(比如根据它们的层级排序),你就必须在每次调用时手动进行排序处理,这就变得非常麻烦。

  1. 基于数据协议的接口

在基于数据协议的接口中,渲染操作的调用不是立即执行的,而是记录到一个数据结构(比如缓冲区或队列)中,稍后统一处理。这就让你能够在执行渲染时进行更多的灵活操作,比如排序、筛选或者延迟渲染。

// 创建一个渲染缓冲区
RenderBuffer buffer;// 把渲染命令添加到缓冲区,而不是立刻执行
buffer.AddCommand(DrawSpriteCommand(100, 200));
buffer.AddCommand(DrawSpriteCommand(200, 300));
buffer.AddCommand(DrawSpriteCommand(300, 400));// 执行渲染之前,你可以在缓冲区内对命令进行排序
buffer.SortCommandsByLayer();// 最后一次性绘制所有精灵
buffer.ExecuteCommands();

在这个例子中,我们并不是在调用 DrawSprite 时立刻绘制,而是先把渲染命令(DrawSpriteCommand)存入 RenderBuffer 缓冲区。然后,我们可以对这些命令进行各种操作,比如排序(比如根据层级或者其它属性排序)。最终,我们一次性执行所有命令,这样可以减少不必要的重复计算,还能更灵活地优化渲染流程。

优势对比:

  • 基于函数调用的接口

    • 操作线性、简单,但缺乏灵活性。
    • 不能进行复杂的操作,比如排序或修改渲染顺序。
    • 每个函数调用时都会立即执行,无法进行优化或延迟执行。
  • 基于数据协议的接口

    • 可以缓存多个操作,之后统一执行,可以在执行前对数据进行复杂处理(如排序、筛选、分组等)。
    • 提供了非线性、重复访问的能力,可以在多个时间点对渲染命令进行处理,甚至在渲染前对数据进行多次修改。
    • 适合需要优化、延迟渲染或者在渲染前进行大量操作的复杂场景。

总结:

基于数据协议的接口提供了一种更加灵活、可控的方式来管理渲染命令,它允许你像立即模式一样简单地发出调用,但又像保留模式一样能做大量的优化和操作。这样,可以在复杂的渲染场景中(如动态排序、延迟渲染等)提供更好的控制和性能。而基于函数调用的接口则更适合简单、直接的应用场景,虽然它在执行时简单,但缺乏灵活性和扩展性。

V-Sync 是如何控制的?

在讨论OpenGL的控制方式时,需要提到一个关键点:OpenGL的控制实际上是通过扩展调用(extension call)来实现的。当前还没有详细讲解扩展的部分,因为在后续的几天里,还会有更多内容来讨论如何获取扩展和如何启用这些扩展。

扩展是OpenGL提供的一种机制,允许开发者在标准OpenGL的基础上,使用额外的功能或者提升性能的技术。通过扩展,OpenGL的功能可以得到扩展,这使得开发者能够在不同的硬件和平台上使用特定的特性,来达到更高的渲染效率或者实现一些特别的图形效果。

目前,虽然扩展的讨论还没有展开,但一旦掌握了如何获取和使用扩展,就可以在OpenGL中启用更多特性,比如Vulkan风格的控制。这意味着,在完成其他基础部分的讲解之后,将会深入讲解如何通过扩展调用来控制OpenGL的行为,并能够充分利用这些额外的功能。

GPU 是否自动进行 sRGB 校正?

关于RGB校正的问题,目前的处理方式是“有”与“没有”自动校正,这取决于如何理解“自动”。目前,系统中的RGB校正并不是自动完成的,实际上它可能存在问题,特别是在sRGB(标准RGB)方面,校正并没有做到正确。如果理解为“自动”的话,当前的实现并不符合自动校正的标准。

当开始处理扩展功能时,情况可能会发生变化。为了确保不遗忘重要的部分,有必要记录需要处理的扩展功能。至今尚未深入讨论硬件渲染的部分,但已经实现了基本的操作,比如使用sRGB帧缓冲和纹理处理等。

此外,还涉及到一些同步和纹理下载等操作,这些功能可能在未来会使用,但目前不属于必须实现的特性。因为在软件渲染中并未涉及这些特性,因此暂时没有强烈需求去实现它们。这些功能只是潜在的未来改进,当前不属于实际需求的范畴。

我没怎么注意,但 OpenGL 的纹理名称问题是否破坏了热加载?

关于OpenGL纹理名称的问题,当前的实现存在一个问题,主要是由于使用了静态变量,这种做法是我们之前决定不再使用的,所以这种方式应该会导致问题,尤其是热重载时可能会出现错误。

除了这个静态变量的问题,其他部分应该没有问题。在这种情况下,唯一需要注意的是,使用静态变量可能会导致热重载失败,因此需要考虑替换这种实现方式,避免出现类似的问题。

关于sRGB校正的问题,是的,目前已经处理了sRGB相关的内容,并且已经考虑到如何改进或扩展相关的功能。如果接下来需要处理其他语言或功能,会根据实际需求进行调整。

选择兼容 OpenGL API vs Core 有没有性能上的原因,还是仅仅因为我们可以在 Core 中使用可编程着色器?

关于OpenGL的兼容性版本(Compatibility)与核心版本(Core)之间的选择,实际上并没有特别强烈的性能差异,它们在性能上的区别并不大。无论是兼容性版本还是核心版本,都支持可编程着色器(programmable shaders),这意味着无论选择哪种版本,都可以使用现代图形编程功能。

选择兼容性版本的主要原因是为了支持老旧的固定功能管线(fixed function pipeline)以及更广泛的硬件兼容性。兼容性版本在支持旧版图形API和硬件上可能更好,但并不是所有驱动程序都强制支持这个版本,因此选择兼容性版本时,可能会遇到某些驱动不完全支持的问题。

相比之下,核心版本(Core)不再支持固定功能管线,这意味着它更专注于现代图形编程和可编程着色器。如果没有特殊需求,核心版本更加现代化,且未来的驱动程序可能会优先优化这一版本,因此从长期来看,选择核心版本更为安全和稳妥。

总的来说,核心版本的好处是避免了对过时功能的依赖,同时也避免了不完全支持兼容性版本的驱动程序问题。因此,如果没有特别需要兼容老旧硬件的需求,选择核心版本更符合现代开发的趋势。

桌面 GPU 没有可编程混合模式的理由是什么?因为我看到大多数移动 GPU 都有这个功能

关于桌面GPU不具备可编程混合模式的问题,其实有几个原因。首先,大部分移动GPU(尤其是现代的移动设备)可能会实现可编程混合模式,因为这些设备通常需要更强的灵活性和可定制性来适应不同的渲染需求。相较之下,桌面GPU的设计更注重性能优化和处理效率,而混合模式的灵活性可能会导致性能上的损耗。

桌面GPU往往是为了处理高性能图形渲染而设计的,可能会有更强的固定功能支持和硬件级优化,以确保在大多数常见渲染任务中保持高效。为了保持这些性能优势,桌面GPU可能选择将混合模式和其他复杂的图形操作固定为硬件加速方式,而不是让开发者自定义。

从硬件设计的角度来看,增加可编程混合模式可能会使得GPU的设计更加复杂,也可能对计算性能产生负面影响,尤其是在处理大量三角形和像素时。这种复杂性可能导致性能上的损失,尤其是在需要高效渲染的桌面级应用中,因此,很多桌面GPU选择优化固定功能管线,而不是实现可编程的混合模式。

总的来说,桌面GPU之所以没有广泛支持可编程混合模式,主要是出于性能优化和硬件设计的考虑。这并不意味着不可行,而是因为大多数桌面GPU的应用场景对可编程混合模式的需求相对较少,更多的是依赖于固定功能的高效处理。

黑板:分块(移动)与非分块(桌面)帧缓冲

关于桌面GPU和移动GPU的渲染方式,主要的区别在于“分块渲染”(tiled rendering)和“非分块渲染”(non-tiled rendering)。在移动GPU中,通常采用分块渲染,即将屏幕划分成多个小块,每个核心负责渲染一个小块。这样做的原因与移动设备的资源限制有关,尤其是内存带宽和功耗的限制。由于移动设备通常没有足够强大的内存控制器和大容量的专用内存,因此每个渲染核心只能快速读取和写入自己负责的小块内存,避免了频繁访问全局内存,从而提高了效率。

在桌面GPU中,通常采用非分块渲染的方式,因为桌面GPU的内存带宽和计算资源通常要比移动设备更强大。这使得桌面GPU可以支持更复杂的图形操作,包括不规则的访问模式(例如无序访问视图,UAVs),并且能够在渲染过程中执行更多类型的计算。尽管非分块渲染的灵活性较高,但也会带来性能上的一些挑战,特别是在多个核心需要同时访问相同资源时,会发生竞争和冲突,影响整体效率。

因此,桌面GPU在处理不需要进行分块的渲染时,需要更加复杂的机制来解决资源争用问题。比如,在渲染过程中,如果多个核心同时尝试读取或写入同一帧缓冲的不同部分,可能会导致冲突和性能下降。为了避免这种问题,桌面GPU往往会将一些关键的渲染步骤(如混合和纹理解包)硬件化,以确保更高的执行效率。

此外,在GPU设计中,某些硬件功能(如可编程混合模式和纹理解压)通常会被优化为专用硬件模块,而不是完全交由可编程流水线处理。这是因为这些操作在图形渲染过程中占用了大量带宽,必须在硬件中进行高效处理,以避免性能瓶颈。例如,纹理解压本身就是一个带宽密集型的操作,因此通常会通过硬件专门优化,而不是通过在着色器中编写解压代码。

总的来说,桌面GPU的设计选择更多的是为了保证在不需要分块渲染的情况下,能够高效地处理复杂的图形任务。这种设计允许GPU支持更广泛的操作,但也带来了更多的资源管理和性能优化挑战。对于混合模式和纹理解压等操作,硬件专用的优化可以确保更高的效率,同时避免在渲染过程中执行任意代码所带来的性能问题。

现在我们得到的渲染是否自动进行了伽马修正?

目前在OpenGL中,默认情况下并不支持伽马校正渲染。要实现伽马校正渲染,需要使用扩展功能。基本的OpenGL并没有内置支持sRGB(标准红绿蓝)格式,因此必须显式地启用相关的扩展。当启用了这些扩展之后,图形卡通常会提供伽马校正功能,并且这通常不会带来额外的性能开销,因为大多数显卡已经内建了查找表(LUT)等硬件支持来处理伽马校正。所以,在使用扩展之后,伽马校正渲染就可以“免费”获得,并且大部分硬件都支持这种功能,能够高效地处理色彩空间转换问题。

操作系统是否会自动启用 V-Sync,还是只有在窗口模式下才适用?

在OpenGL中,默认情况下并不会自动启用垂直同步(V-Sync)。OpenGL允许画面撕裂(tearing),除非显式地要求它禁用撕裂。因此,除非特别设置,否则目前不会启用垂直同步,OpenGL会允许画面撕裂的发生。尤其是在窗口模式下,可能会有不同的行为,但在常规模式下并不会自动进行垂直同步。

V-Sync(垂直同步)是一种显示技术,旨在消除画面撕裂(tearing),提高图像的稳定性和流畅度。它的原理是将图形渲染的帧率与显示器的刷新率同步。具体来说,V-Sync 会确保图形卡输出的帧数与显示器的刷新频率(通常为60Hz、120Hz等)一致,从而避免画面在更新时出现不完整的图像,导致画面分裂或撕裂的现象。

V-Sync 工作原理:

  • 显示器的刷新率是指屏幕每秒钟显示的帧数,例如 60Hz 则表示每秒显示 60 帧图像。
  • 图形卡(GPU)会生成一帧帧的图像,然后送到显示器。若图形卡生成的帧数超过显示器的刷新率(例如图形卡每秒渲染 100 帧,而显示器每秒只刷新 60 次),就会出现画面撕裂,即显示的图像被不完整地切分成两部分。
  • 启用 V-Sync 后,图形卡会将渲染帧数限制为显示器的刷新率,确保每一帧都完整地显示出来,避免撕裂现象。

V-Sync 的优缺点:

  • 优点

    • 消除了画面撕裂,使画面更加平滑和稳定。
    • 对于静止图像或较慢运动的场景,能够显著提升视觉体验。
  • 缺点

    • 启用 V-Sync 时,图形卡的帧率会被限制在显示器的刷新率,可能会导致一些性能下降,尤其是在图形要求较高的场景中。
    • 有时会引入输入延迟,导致控制响应不如关闭 V-Sync 时那样迅速,尤其是在需要精确操作的游戏中。
    • 在低帧率的情况下,可能会出现“卡顿”现象,因为 GPU 渲染的帧数不足以与显示器刷新率同步。

V-Sync 是一种常见的优化技术,但它并不总是最适合所有情况。在一些需要极高流畅度和快速响应的游戏中,可能会选择禁用 V-Sync 或使用其他技术(如 G-Sync 或 FreeSync)来提高性能和视觉体验。

所以伽马曲线也是 OpenGL 扩展吗?

Gamma 曲线是显示图像亮度和对比度的一个重要因素,它在图形渲染中用于调整颜色的非线性映射。通常来说,显示器或图形卡都会使用 Gamma 曲线来处理图像的亮度,使其更符合人眼的感知方式。Gamma 曲线的作用是将图像的像素亮度值转换成显示器可以显示的实际亮度,从而使画面更加自然。

在 OpenGL 中,Gamma 曲线的处理并不是自动启用的。需要使用特定的扩展或功能来启用 Gamma 校正。基本的 OpenGL 本身并不提供直接的 Gamma 曲线处理功能,必须通过扩展来开启相关的功能。这就意味着,Gamma 校正通常需要额外的配置或调用才能正确地进行,才能确保图像的显示效果更加符合视觉需求。

主要内容:

  • Gamma 曲线是显示图像时调节亮度的方式,可以帮助图像的显示更符合人眼的视觉习惯。
  • OpenGL 本身并不自动处理 Gamma 曲线,需要通过扩展(Extensions)来启用 Gamma 校正。
  • 启用后,Gamma 校正可以提升图像的质量,尤其在色彩和亮度的显示上,让图像更自然。

我知道你可能已经厌烦了所有 Vulkan 的问题,但这里又有一个。很多地方把 Vulkan 看作是 OpenGL 的下一个版本,认为一旦 Vulkan 发布,OpenGL 的开发/采用将会逐渐减少,最终导致显卡厂商不再开发 OpenGL 驱动。这种情况会发生吗?如果发生了,你有什么计划?

关于 Vulkan 和 OpenGL 的问题,有一种观点认为,随着 Vulkan 的发布,OpenGL 的发展和使用会逐渐衰退,最终可能会被 Vulkan 所取代,甚至可能导致图形硬件制造商仅支持 Vulkan。这种观点的背后认为 Vulkan 是下一代图形接口,具有更好的性能和更低的抽象层次,更适合现代硬件的需求。

然而,实际上,OpenGL 不太可能完全消失。即便 Vulkan 获得更广泛的采用,OpenGL 仍然会存在并且继续使用,至少在短期内不会消失。可能的情况是,Vulkan 会被作为底层技术来实现 OpenGL,类似于通过 Vulkan 层来支持 OpenGL 功能。换句话说,即使 Vulkan 成为主流技术,OpenGL 仍然能够通过某种方式存在,并且仍然是开发者们选择的图形接口之一。

就目前来看,Vulkan 的普及不会立即导致 OpenGL 的消失。OpenGL 仍然会继续存在,而且它可能会与 Vulkan 共存一段时间,特别是在短期内。至于是否有可能在更远的未来,Vulkan 完全取代 OpenGL,那也许需要好几年,甚至十年后的事情。

总之,OpenGL 在可预见的未来不会消失,即使 Vulkan 是一个非常有前景的图形API。

你说 API 非常相似,但有没有某种视觉效果在 DirectX 上能做到,而 OpenGL 做不到,或者反之?

关于视觉效果的问题,可能是问 OpenGL 和 DirectX 之间是否存在一些特定的视觉效果或图形指令,OpenGL 能做到而 DirectX 做不到,或者反之。从这个角度来说,OpenGL 和 DirectX 在硬件特性和功能访问上确实有所不同。虽然二者都有强大的图形处理能力,但 OpenGL 提供了一些特定的功能,这些功能在 DirectX 中可能没有直接的对应。

具体来说,OpenGL 在某些硬件特性上有更高的访问权限,这意味着一些特定的图形效果可能只能在 OpenGL 中实现,或者在 OpenGL 中的实现方式可能更加灵活。不过,OpenGL 和 DirectX 作为两种不同的图形接口,各自有其独特的优势和功能,因此在不同的开发需求下,选择的工具和方法可能会有所不同。

总的来说,OpenGL 和 DirectX 都有自己的强项,某些硬件的特性可能只能通过 OpenGL 访问,这使得某些视觉效果在 OpenGL 中实现更为方便。

为什么 Windows 上的 OpenGL 头文件只提供旧版遗留内容,而其他平台提供新内容?

Windows上的OpenGL头文件只提供旧的遗留功能,而其他平台则会添加新特性,这背后的原因是微软曾经尝试推动DirectX,甚至希望完全淘汰OpenGL。微软的目标是迫使开发者使用DirectX,这也是为什么他们不支持更新的OpenGL特性或扩展的原因。虽然他们的计划未能成功,但依然采取了这种做法,限制了OpenGL在Windows平台上的发展和更新。

这种做法被认为是缺乏支持和不合理的,但现实就是如此,开发者在Windows平台上遇到OpenGL时,往往只能使用旧版的遗留特性,而其他平台则能利用更现代的功能和扩展。这一现象反映了微软在图形API方面的策略,也让OpenGL在Windows平台上的发展变得更加受限。

能否讲解一下不同图形 API 的优缺点?

不同的图形API有其优缺点,主要差别体现在以下几个方面:

  1. OpenGL

    • 优点:

      • 提供了对硬件特性更好的访问。OpenGL允许驱动程序开发者更容易地暴露扩展,使得能够访问更多的硬件特性。
      • 跨平台兼容性更好,尤其是在Windows平台上,OpenGL能够支持较旧版本的操作系统,不需要为不同版本的操作系统编写不同的代码。
      • OpenGL的兼容性更强,允许在不同的Windows版本上运行,开发者只需要一个代码库即可支持多个Windows版本,不需要像DirectX那样为不同版本的DirectX编写代码。
    • 缺点:

      • 驱动程序的支持和优化可能不如DirectX。
      • 性能上可能存在差距,尤其是在一些特定硬件上,OpenGL的驱动程序支持可能不如DirectX稳定或优化好。
  2. DirectX

    • 优点:

      • 与Windows操作系统的集成性更好,尤其是在Windows平台上,DirectX通常能更好地支持Windows特性。
      • DirectX 12更接近Vulkan,在性能优化和对低层次硬件控制方面具有优势。
    • 缺点:

      • DirectX的版本兼容性问题较为严重,例如DirectX 12只能运行在Windows 10及更高版本上,而DirectX 11只支持Windows 7及以上版本,这会导致开发者在跨多个Windows版本时需要管理不同的代码路径。
      • 相比OpenGL,DirectX的跨平台支持较差,无法在非Windows平台上运行。
  3. Vulkan

    • 虽然Vulkan没有直接被提及,但它与DirectX 12类似,提供了更低级别的硬件控制,适用于性能需求更高的应用。Vulkan通常与DirectX 12作比较,因为它们都提供了类似的功能和性能优化。

总结来说,如果只是在Windows平台上开发,选择DirectX与OpenGL差别不大。OpenGL提供了更好的跨平台兼容性,而DirectX在某些机器上可能会表现得更好,尤其是在AMD的驱动程序支持上。对于开发者而言,选择OpenGL或DirectX更多取决于对特定硬件和平台的支持需求,基本没有绝对的理由去强烈倾向于使用其中一个API。

相关文章:

  • Unity Paint In 3D 入门
  • Python线程全面详解:从基础概念到高级应用
  • 鸿蒙生态新利器:华为ArkUI-X混合开发框架深度解析
  • android contentProvider 踩坑日记
  • uniapp 上传二进制流图片
  • 鸿蒙生态:鸿蒙生态校园行心得
  • Windows下Golang与Nuxt项目宝塔部署指南
  • L1-4、如何写出清晰有目标的 Prompt
  • vscode python 代码无法函数跳转的问题
  • 55、Spring Boot 详细讲义(十一 项目实战)springboot应用的登录功能和权限认证
  • 小刚说C语言刷题——1031 温度转化
  • Ubuntu-Linux中vi / vim编辑文件,保存并退出
  • 云账号安全事件分析:黑客利用RAM子账户发起ECS命令执行攻击
  • 联易融科技:以科技赋能驱动经营反转与价值重估
  • 可吸收聚合物:医疗科技与绿色未来的交汇点
  • K8s:概念、特点、核心组件与简单应用
  • 方案精读:华为智慧园区解决方案【附全文阅读】
  • [创业之路-380]:企业法务 - 企业经营中,企业为什么会虚开増值税发票?哪些是虚开増值税发票的行为?示例?风险?
  • SpringCloud组件—Eureka
  • 【sylar-webserver】重构 增加内存池
  • 董明珠卸任格力电器总裁,张伟接棒
  • 证券时报:金价再创历史新高,“避险”黄金不应异化为投机工具
  • 告别国泰海通,黄燕铭下一站将加盟东方证券,负责研究业务
  • 日媒:日本公明党党首将访华,并携带石破茂亲笔信
  • 动力电池、风光电设备退役潮来袭,国家队即将推出“再生计划”
  • 商务部:试点示范已形成9批190多项创新成果向全国推广