游戏引擎学习第313天:回到 Z 层级的工作

回顾并为今天的内容定下基调

昨天我们新增了每个元素级别的排序功能,并且采用了一种我们认为挺有意思的方法。原本计划采用一个更复杂的实现方式,但在中途实现的过程中,突然意识到其实有个更简单的做法,于是我们就改用了这个简单的方式,并且运行效果也很好我们还用上了加入的循环检测机制,确认排序逻辑里并没有出现错误,因此循环检测也派上了用场。整体来说昨天的工作还是挺顺利的,对结果也很满意。

不过,眼下我们已经到了需要继续完善 Z 轴分层逻辑的阶段了。当前我们已经实现了在单一图层内的正确排序,但图层之间的分层系统还没有做好。这也就意味着如果现在运行游戏,会发现当前只有一个房间能够按照我们期望的方式进行排序渲染,这是正常的。

但之前做的楼梯部分仍然保留了旧的透视变换逻辑,也就是物体随着高度上升会向一侧偏移或放大,而这不是我们想要的。我们希望的是:在一个房间内部,只是简单地向上排列(也就是 Y 值变大),而不应该带有这种透视拉伸效果。因此这部分显然还需要修复。

另外,当尝试添加多个房间时,也还存在明显的问题。当前引擎的结构还没有处理多个房间之间的图层关系,因此我们还需要回过头来继续完成图层系统的实现。这也正是接下来需要重点处理的部分。

修改 game_world_mode.cpp:让 AddStandardRoom() 生成多层内容,运行游戏观察排序混乱的情况

如果我们开始让多个房间以垂直叠加的方式添加进场景中,很快就会发现当前的排序系统完全无法正常工作。因为目前的排序逻辑只是为单个房间设计的,它只考虑了一个房间内部的规则。原因是我们采用的是"二维半"的排序方式,所以为了实现简单,它必须做出一些妥协。

但显而易见,这样做在当前情形下根本行不通。多个房间叠在一起时,排序混乱,完全没有逻辑可言,物体在渲染时的前后关系也变得毫无依据。原因很清楚,就是因为我们的排序规则并没有考虑跨层的 Z 切片。

虽然我们理论上可以尝试去改造排序算法以适应这种多层的情况,但这并不是我们的目标。我们的目标从来都不是让一个排序系统去处理所有情况,而是将整个世界切分成多个 Z 切片(Z slices),然后对每一个切片独立进行排序,这样我们就能避免跨层的复杂依赖问题。而这个方法我们已经知道是有效的,这一直是我们设定的方向。

所以现在必须做的,就是回过头来,把这个世界切片的系统实现出来。我们要按照预定方案将场景划分成一个个图层切片,然后让渲染器对这些图层单独处理。这样我们才能验证,在这种切分模式下,原有的排序规则是否真的有效。

目前的情况是,所有对象都混杂地渲染在一起,排序混乱无序,结果无法理解也无法接受。如果不实现图层切分,那么排序逻辑就根本没有可行性。因此我们必须暂停当前的操作,回到图层划分的实现上来,这是我们接下来面临的一项艰巨工作。

鼓励性的发言:谈论探索式编程,以及顺应架构演进的流程

接下来要进行的工作会比较困难。一方面是因为这类任务通常需要我们集中精力,在连续的几小时甚至一两天内完成,比如在那种完全沉浸的开发状态中,把所有细节打磨到位。而现在我们是分段进行,并且一边做一边讲解,这让整个过程的节奏被打散,也更难专注,思路容易被打断。

这不仅对我们来说是挑战,对其他人理解起来也不容易。因为我们要频繁地移动代码,进行结构调整,很多内容是"感觉上的",是对系统组织方式的直觉和理解过程。这种过程很难用清晰、线性的语言完全解释清楚,我们也为此感到抱歉,但这是开发过程中不可避免的一部分。

我们一直在采用探索式架构设计的方式推进,这种方式强调的是在实践中"摸索出"合适的架构。而不是一开始就设想出一个完美的系统结构图,然后按图纸建造。从来都没有所谓"架构全知者",任何优秀架构的形成都是在实际开发过程中不断调整、试错、发现问题并修正之后逐步凝练出来的。

因此,这种架构上的调整、重组,是我们必须时不时面对并接受的。它不是失败,而是一种成长,是朝着更清晰、更合理结构演进的必经之路。越是深入项目,就越能看清哪些原来的设计不足以支撑后续需求,而哪些新的设计思路更契合当前的发展方向。

如果我们抱着一开始就要把一切都设计完美的想法去开发软件,那最终只会得到一个僵化的、难以维护的系统。而如果我们相信架构是一个逐步"退火"与优化的过程,那么就应该乐于接受这种在开发中不断演进的结构变化。

所以我们要做的,是保持代码库的灵活性,让它能够随着我们对项目理解的加深而不断演化。不要害怕重构,不要排斥改动,而是积极地拥抱这种前行中的架构演化。因为最终只有那些可以流动、可以适应、可以成长的系统,才是真正高效、优雅且长久可维护的。

修改 game_entity.cpp:让 UpdateAndRenderEntities() 回到排序改动前的样子,运行游戏看看当前效果

现在我们要开始处理的任务,是关于图层系统的进一步构建。之前我们已经开始了这一部分的设计和探索,但在中途发现需要先完成排序系统,于是暂时中断了对图层的开发,先去实现了排序。现在排序系统已经实现并测试得差不多了,我们回到之前未完成的图层概念。

当前的系统中,已经存在一个叫做"转换为相对图层"的功能,它会根据一个位置返回一个相对图层的数值,用来表示这个元素应该被视为在"世界"的哪一层。它还会修改传入的位置,使其变成相对于那个图层的偏移。为了临时跳过这个机制做测试,之前我们只是传入一个临时变量,没有真正使用转换结果,这样就避免了偏移修改。

现在如果我们恢复回"没有排序机制之前"所使用的图层系统,可以看到场景中确实出现了两个房间,但它们被渲染在了同一个平面上,图形出现闪烁,就是因为两个图层叠在一起,深度冲突,没有正确分层。这种行为其实正好说明了我们接下来要实现的目标------我们希望所有房间的图元在渲染时都被压扁在一个统一的空间中,但是渲染器知道它们所属的是不同的层,然后在实际绘制时,通过不同的透视偏移把它们展示为"上下叠放"的结构。

这也正是我们设想的"图层切片"渲染方式的核心------逻辑上世界是分层的,但物理渲染时所有图元都压进同一个Z空间里,之后再通过相对Z值的偏移来渲染出深度感。我们不再让渲染器试图在三维空间中自行解决可见性排序的问题,而是人为地提前把世界切成若干"图层切片",每个切片内做局部排序,然后分层绘制,从而让渲染变得简单明确。

黑板讲解:Z轴切片与"向上"这一概念的两种不同理解

我们现在再次澄清并统一对"图层切片"概念的理解,以便后续开发保持一致思路。

当前的世界结构中存在多个区域(如房间或地形块),它们可以堆叠在彼此之上。每个区域可能被树木等装饰物包围,不同层之间是垂直叠加的。

我们需要引入"向上"的两个不同概念,也就是说,"向上"这个词在不同上下文中代表不同的意思:


第一种"向上"的概念:在同一个切片层内部的相对Z位移

在一个切片层(即一个房间或一个平台)内部,我们希望将所有Z轴上的高度变化,仅仅表现为在Y轴上的偏移,而不再有缩放或X方向的透视偏移。这意味着:

  • 例如一段楼梯,它的每一级高度不再被拉伸或放大,而是以相同的大小逐级向上排布。
  • 视觉上,这种楼梯应该是垂直排列的,没有透视效果,就像是从侧面看的一条笔直的Y轴方向延伸。
  • 这种处理方式,属于一种"倾斜正交视角":将Z的变化直接转换为Y轴偏移,不做其他处理。

这样可以达到我们想要的视觉风格,使得在同一个房间中,不同高度的物体看起来是平等的,不会因为视角透视而大小不同。


第二种"向上"的概念:切片之间的垂直堆叠关系

当我们从一个房间或区域走向另一个在上方的房间(即从一层走向二层),这种Z的变化代表的是不同的切片层。这里的"向上"意味着跨越了切片边界,进入了一个新的世界切片。

对于这种"跨切片"的垂直关系,我们希望呈现出透视变化,即:

  • 越高的层级,其内容会放大 (因为它离相机更近),会出现X、Y轴上的偏移(体现为透视缩放和平移)。
  • 整个切片作为一个整体进行变换,比如放大或偏移,而不是对其中单独元素进行Z方向位置的直接操作。

目标总结:

我们的渲染逻辑将按如下方式进行:

  1. 将整个世界分割为多个"切片"(每个代表一个房间或楼层)。
  2. 每个切片内部,元素根据相对Z值映射到Y轴偏移,不进行缩放或透视。
  3. 每个切片作为整体,根据它在世界中的高度位置进行统一的缩放、偏移,体现出它在整个世界中的上下关系。
  4. 渲染器对切片分开排序,每个切片内部进行局部排序(之前已实现)。

通过这种方式,我们就可以得到既有清晰逻辑层次,又符合艺术风格的视觉效果,使玩家可以准确识别不同高度上的对象,同时保持画面的整洁和层次分明。这也是我们引入"图层切片"架构的根本目的。

黑板讲解:分层的 Alpha 混合

我们在进行图层切片和渲染重构的过程中,还需要同步修复一个关键的视觉问题:透明度(Alpha)混合不正确的问题。

这个问题表面上不太明显,但实际上对整体视觉品质有着重大影响。很多游戏中都会出现这种情况,看起来很不专业。


问题描述:

当两个半透明的图像(例如两棵树)部分重叠时,我们期望看到的效果是:前面那棵树挡住后面一部分区域,叠加的区域在视觉上保持一致的透明度(例如都设定为 50%)。但由于渲染顺序和混合逻辑不当,叠加区域实际看上去比应有的更不透明

举例说明:

  • 假设背景为 M,然后树A在前,树B更前,两者都设置为 50% 透明度。

  • 我们希望最后看到的像素颜色是:

    0.5 × M + 0.5 × N

    (N 是前景树图层的合成结果)

然而,由于我们当前是逐个绘制精灵(sprites),每个精灵独立执行一次混合计算,结果就不是我们期望的那样。


实际发生了什么:

  1. 首先绘制背景 M,显存中是 M。

  2. 然后绘制精灵 A(透明度 0.5):

    • 当前缓冲区变为:0.5 × A + 0.5 × M
  3. 接着绘制精灵 B(同样透明度 0.5):

    • 它会再与上一层结果混合,而不是和原始 M 或 N 整体混合。
    • 计算结果为:0.5 × B + 0.25 × A + 0.25 × M

此时的最终像素颜色中:

  • 来自树图层(N层)的贡献是 A 和 B 加起来,共 0.75(0.5 B + 0.25 A)
  • 背景(M)的贡献只剩下 0.25
  • 总透明度已经偏离了原设定的 50%

每增加一个精灵,这种偏差会更严重,画面越来越不透明,不再真实。


本质问题:

多次执行的 Alpha 混合是逐层进行的,每一层都是线性叠加,这导致前层对后层产生了过多的遮挡,叠加后的透明度呈指数式变化,远远偏离了预期的视觉表现。


理想目标:

我们期望达到的是对某一个完整图层进行整体混合,也就是说:

  • 图层内部的多个精灵,应该先合成为一个"图层图像"
  • 再把这个图层图像作为一个整体,用透明度(例如 50%)和背景进行一次混合

只有这样,才能实现 真正的图层透明效果,不会因局部重复混合而产生透明度累加问题。


初步方案方向:

为了解决这个问题,我们需要:

  1. 将每个切片图层的所有内容先合成为一个临时缓冲图像(offscreen render target)
  2. 对这个合成图层应用统一的 Alpha 混合逻辑,再与背景图层合成
  3. 避免图层内精灵直接逐个向主缓冲区进行 Alpha 混合渲染

这样才能实现像:

  • 楼梯淡出
  • 房间随镜头远近渐隐
  • 整体场景雾化、模糊

等特效,并且保持透明度一致,画面清晰真实,不会"越重叠越黑"。


这个修复虽然麻烦,但非常必要,是提升画面质量、避免渲染逻辑错误的关键一步。我们将会在进行图层切片和分层渲染的同时,把这个正确的 Alpha 混合机制也一并设计进去。

黑板讲解:如何解决当前问题

我们决定采用的解决方案是将整个场景按"切片"的方式来进行渲染,从而彻底解决多精灵混合导致透明度叠加错误的问题。虽然这个问题理论上可以通过一些复杂的渲染技巧解决,例如使用目标帧缓冲区中的 alpha 通道、单独的中间缓冲、深度预通道(depth pass)以及各种比较操作等,但我们不打算走那种"疯狂 shader 技术栈"的路线。我们选择更简单、直接、可控的方式来处理这个问题。


解决方案:按图层切片进行渲染

我们将整个场景划分为不同的切片(slice),每个切片作为一个独立的图层进行处理。每个切片中包含多个 sprite 精灵,我们先将这些精灵全部绘制到一个离屏缓冲(offscreen buffer)中,生成该切片的完整图像。

之后,我们再将这个切片图像与其它切片或者背景图层进行合成。在这个合成阶段,我们只需要应用一次标准的透明度混合公式即可:

P = (1 - α) × N + α × M

其中:

  • N 表示之前已经绘制好的缓冲内容(背景或其它切片)
  • M 是当前图层(切片)渲染结果
  • α 是整个图层的透明度(例如用于逐渐淡出)

这个方式的关键点在于:

  • 图层内所有精灵先合成为一张完整的图像,内部再怎么复杂,外部只看最终合成的图层图像
  • 合成操作只执行一次,因此不会因为精灵之间的重叠而产生透明度叠加问题
  • 保留了精灵原始的透明度(例如玻璃、幽灵等对象仍可以是半透明的),不会影响单个精灵的表现
  • 可以对图层整体应用动态效果,比如整个图层淡入淡出、模糊、雾化等

操作流程回顾:

  1. 对每一个切片图层:

    • 所有属于该图层的 sprite 精灵全部渲染到一张临时缓冲图像中
    • 这些精灵保留自己的 alpha 值,不进行额外的混合
  2. 完成图层合成后:

    • 将整个切片图层的图像,作为一个整体,与其它图层按指定透明度混合
    • 混合操作只进行一次,确保 alpha 计算正确
  3. 最终得到准确的视觉效果,无论有多少精灵,透明度表现始终如预期


这种分层合成的方式不仅解决了透明度问题,还为后续的渲染优化和效果控制打下了坚实基础。我们可以在图层层面做更多效果控制,而不用在精灵层面做复杂的逻辑,极大地简化了系统的复杂度和维护成本。

黑板讲解:渲染缓冲区

我们最终实现图层渲染和合成的方式,可能在细节上还有一定的复杂度,但整体思路是可行的。如果操作得当,甚至可能不需要为每个图层都分别渲染,只要在关键图层上使用额外处理即可。即便采用最保守的实现方式,最坏情况下我们也只是需要增加一些渲染缓冲区(render buffer),并不是非常昂贵的开销。


渲染缓冲与操作数量的关系

我们需要的渲染缓冲区数量,与我们要进行的特殊图层混合操作的次数成正比。回顾之前的设计,我们实际上不需要太多这样的操作,具体如下:

1. 雾效(Fog)不受该问题影响:
  • 雾效是颜色效果,而不是 alpha 混合;
  • 雾不是通过多个 sprite 重叠绘制出来的,而是通过颜色叠加实现;
  • 所以雾效图层不会因为重复渲染而产生透明度累积的问题;
  • 因此,从角色所处的层开始,向下的所有图层(即更底层的场景)都可以合并在一个渲染缓冲区中,统一渲染处理,无需额外操作。
2. 仅需单独处理高于角色的那一层:
  • 真正需要使用我们提出的"切片图层单独合成再统一混合"的处理方式的,仅有角色上方的那一层;
  • 因为那一层会由于摄像机视角靠近而被淡出,所以会涉及 alpha 混合;
  • 如果直接按 sprite 分别进行透明度渲染,会导致重叠区域的透明度错误叠加问题;
  • 因此这一层必须单独渲染,最终统一做一次合成,确保视觉正确。

总结:

  • 实际上我们不需要为所有图层都使用这种额外缓冲和混合方式;
  • 只需要为角色上方的一个图层使用特殊处理;
  • 角色所在层及其下方的所有图层,可以直接合成并渲染,无需担心透明度错误;
  • 雾效等纯颜色操作不涉及 sprite 重叠透明度问题,因此也不需额外处理;
  • 整体的资源开销很小,仅需一到两个额外的 render buffer 即可实现正确视觉效果;
  • 保持系统简单的同时,实现了专业级别的图层混合表现。

这一实现方式既高效又易于维护,能为后续扩展视觉效果和图层控制提供良好的基础。

黑板讲解:简单估算我们可用的图形内存

我们评估了一下在最坏情况下所需的渲染缓冲区数量,即便是这样,整体的内存开销也是完全可以接受的。假设我们渲染的分辨率为 1920×1080,每个像素占用 4 字节,那么单个渲染缓冲区的内存需求约为:

  • 1920 × 1080 × 4 = 8.29 MB

如果我们需要两个这样的缓冲区,总共也只是大约 16 MB 的显存占用。


显存使用的合理性:

这个显存占用从两个角度来看都没有问题:

1. 内存分配层面完全可接受
  • 当前主流的显卡大多配备至少 512MB 显存
  • 普遍的配置是 1GB ,甚至 2GB 或以上
  • 而我们只占用了 16MB,这远低于最低配显卡的显存上限,基本可以忽略;
  • 即使是中低端显卡,在运行其他正常图形任务的同时,也完全可以承受这个开销;
  • 更何况我们不是频繁创建销毁,而是只在初始化时分配几块缓冲区长期使用。
2. Steam 硬件调查也验证了用户硬件足以支撑
  • 从硬件调查数据可以看出:

    • 拥有 512MB 显存 的用户比例大约在 10% 左右;
    • 显存为 1GB 的用户占比接近 1/3
    • 显存为 2GB 及以上 的用户占比甚至超过 1/3
  • 也就是说,绝大多数玩家的显卡配置都远高于我们的最低需求;

  • 我们只需 16MB,甚至最老旧的显卡(如 256MB 显存)也能容纳;


对弱性能设备的适配考虑:

  • 如果是性能极弱的平台,比如 Raspberry Pi:

    • 本身就不适合跑 1080p 分辨率的游戏;
    • 因此我们可以通过降低分辨率来规避显存问题;
    • 比如运行在 720p 或 480p 下,对显存的需求就会大幅下降;

总结:

  • 即便采用最保守的策略,在视觉处理上使用两个完整的 1080p 渲染缓冲区,显存开销也只是约 16MB
  • 在现代主流或中低端显卡配置下,这个开销几乎可以忽略;
  • 只有在极端弱性能设备上才可能需要对分辨率或处理方式进行优化;
  • 所以我们完全可以放心采用这种方案,不仅效果正确,性能成本也极低。

黑板讲解:简单估算我们可用的内存带宽

我们分析了在进行额外的图像合成(blit)操作时所需的内存带宽,发现这种开销非常小,不会对整体性能产生显著影响。


具体分析:

  • 以一个 8MB 的内存拷贝为例,这样的操作非常轻量;
  • 参考一个比较低端的显卡型号(比如GeForce 600系列的移动版),其显存带宽大约是 14.4 GB/s
  • 如果按 60 帧每秒计算,显卡每帧能处理约 240 MB 的内存带宽;
  • 8MB 的额外内存拷贝只占用非常小的比例,远远在显卡的带宽承载范围内;

对整体渲染流程影响:

  • 游戏渲染本身需要读取和写入大量数据,包括:

    • 贴图加载
    • 深度缓冲的读写
    • 颜色缓冲的读写
    • 各种着色器计算
  • 这些操作本身已经消耗了显卡大量内存带宽;

  • 增加一个额外的合成步骤,就是将一个缓冲区的内容拷贝到另一个缓冲区进行 alpha 混合,这实际上只增加了大约一个屏幕大小的内存拷贝(即上文的8MB);

  • 这种额外的带宽需求非常小,属于"鸡毛蒜皮"级别;


现实情况复杂性:

  • 实际上的内存带宽开销并非精确数值,受以下多种因素影响:

    • 显卡的具体内存子系统架构
    • 着色器执行和压缩机制(如颜色压缩、深度压缩等)
    • 是否能提前剔除不必要的像素计算(early-out)
    • 驱动和硬件优化机制
  • 由于这些因素的复杂性,很难用纯数学公式准确计算内存带宽消耗;

  • 但从经验和估算来看,当前的方案是合理且可行的;


性能决策的参考原则:

  • 了解内存带宽和显存使用的基本量级,有助于在性能优化时做出合理判断;
  • 如果发现需要额外带宽达到数百兆字节每帧(比如 300MB/帧)这种量级,就必须警惕,因为这会严重拖垮性能,甚至让某些硬件完全无法运行;
  • 目前的额外带宽需求仅为几兆字节,远远低于硬件承受上限;
  • 这样能保证即使在较低端硬件上,游戏运行也不会因为额外的渲染缓冲操作出现明显的性能瓶颈;

总结:

  • 额外的合成操作带来的内存带宽负担极小,完全在现代显卡的负载范围内;
  • 这种开销对性能影响可忽略不计,属于性能可接受的范围;
  • 保持对硬件资源的合理估算和认知,是做出性能优化决策的重要基础;
  • 因此,我们可以安心采用这种分层渲染和一次性合成的方案,既保证了视觉效果的准确,又不会显著增加性能负担。

思考接下来该如何推进

我们现在需要推进两个核心问题的解决:

第一,必须理清变换(transform)的处理方式。当前我们对Z轴有两种不同的含义,需要明确这两种Z值如何协调运作,确保变换逻辑正确无误。

第二,在处理变换之前,我们可以先让渲染器支持"分层渲染"的概念,也就是把场景分成两个不同的切片(slice),然后让渲染器能够正确地对这两个切片进行alpha混合,达到预期的叠加效果。

此外,可以考虑暂时先恢复之前的方案,分别对这两个部分进行开发和调试,先解决渲染层的切片和混合,再进一步理清变换的实现细节。这样分步骤推进,减少复杂度,更容易把控整体流程。

修改 game_entity.cpp:在 UpdateAndRenderEntities() 中引入 TestAlpha

我们发现alpha值没有被正确设置,首先要查明为什么alpha没有被赋值。看起来是因为我们已经对场景进行了分区处理,导致没有给实体(entity)单独设置alpha。

我们需要验证这一点,确认是否确实没有给实体设置全局alpha。假如想要设置实体alpha,应该依据当前的"层级索引"(level index)来决定alpha值。

具体做法是,在裁剪矩形(cliprect)处理的地方,也就是推入cliprect的函数里,尝试获取并保存每个层级的alpha值。为了方便调试,可以新建一个"测试alpha"数组,存储每个层级对应的alpha。

alpha值的计算方式是:如果层级处于淡出区域(fade top)之上,alpha等于1减去那个颜色通道的透明度值(T值)。保存了这个测试alpha后,在后续渲染时可以根据层级查找对应的alpha值。

在渲染时,先拿到对应层级的test alpha,然后将它应用到颜色的alpha通道上。具体就是将颜色的alpha通道乘以这个test alpha,从而实现正确的透明度混合效果。

整体思路是:先把每个层级的alpha保存下来,然后在渲染时根据层级查找这个alpha,正确设置实体的透明度,确保分层渲染时的透明处理正确无误。

运行游戏观察结果

当角色上楼梯时,预期应该看到逐渐淡入的效果,但实际看到的是效果瞬间切换,没有渐变,这显得很奇怪。为确认问题,首先验证了当前的状态,发现level index的变化似乎没有按照预期工作。虽然设定了层级索引来控制透明度变化,但在实际运行中,这个索引并没有正确导致alpha的渐变,反而是突然切换了状态。因此,需要进一步排查为什么level index没有正确驱动淡入效果,导致透明度没有平滑变化。

使用调试器:中断进入 UpdateAndRenderEntities() 并检查 fade(淡化)值

我们检查了当前的透明度(T值),发现当没有任何层级超过fade top起始高度时,T值是1,符合预期,也就是完全显示。但当角色逐渐上楼梯时,fade top高度是2.25,而当前层级的相对位置是3.0,看起来是正确的,意味着应该开始淡出。奇怪的是,当角色上楼梯时,应该看到淡入效果,但实际情况并非如此,可能是因为场景的世界坐标在某些时刻发生了偏移或滑动,导致fade效果还未生效就被位置调整所覆盖了。我们推测淡入的触发点需要设置得更高一点,才能在切换为新的层级之前完成淡入效果。但根据典型楼层高度的设置,fade开始和结束的高度比例看起来有些异常,似乎fade效果的时间点和层级变化的实际位置没有很好地匹配。整体来看,当前的fade逻辑和层级切换存在时间和空间上的不协调,需要调整fade起止位置,确保淡入淡出效果能正确展现。

修改 game_entity.cpp:调整 FadeTopEndZFadeTopStartZ 值,再次使用调试器中断查看

目前我们发现没有任何层级超过fade top起始高度(3),具体来看层级高度依次是负12、负9、负6、负3、当前层级0、上层3,而fade top起始高度也是3。虽然fade的起始位置看似合理,应该能触发透明度的渐变,但实际上并没有按预期那样工作,透明度没有按范围映射正常变化,表现得非常混乱,不像是正确的过渡效果。我们感觉这里一定遗漏了什么关键细节,整体逻辑有些模糊不清,需要进一步深入排查和分析才能找到问题的根源。

修改 game_opengl.cpp:将 Entry->PremulColor.a 传给 OpenGLRenderCommands() 中绘制矩形的 glColor4f() 调用

我们对当前的效果不满意,怀疑是计算逻辑出了问题,觉得自己可能犯了低级错误。为了更清楚地观察,我们在绘制矩形时给它加上了边框,这样更容易看出问题所在。同时,我们想要确保透明度的计算更加准确,希望能够尊重主颜色的Alpha值,这样所有元素才能同步淡出,避免矩形边框还在但内容已经消失的情况。现在发现元素的透明度表现异常,似乎计算出的范围有问题,怀疑计算公式不对,尤其是关于相机相对地面Z的部分,感觉应该减去相机相对地面Z的值,但实际代码中并没有这么写,所以打算修改这部分逻辑,尝试修正这个错误。

我们对当前的效果不满意,怀疑是计算逻辑出了问题,觉得自己可能犯了低级错误。为了更清楚地观察,我们在绘制矩形时给它加上了边框,这样更容易看出问题所在。同时,我们想要确保透明度的计算更加准确,希望能够尊重主颜色的Alpha值,这样所有元素才能同步淡出,避免矩形边框还在但内容已经消失的情况。现在发现元素的透明度表现异常,似乎计算出的范围有问题,怀疑计算公式不对,尤其是关于相机相对地面Z的部分,感觉应该减去相机相对地面Z的值,但实际代码中并没有这么写,所以打算修改这部分逻辑,尝试修正这个错误。

修改 game_entity.cpp:在 UpdateAndRenderEntities() 中对 CameraRelativeGroundZ 减去 WorldMode->CameraOffset.z

我们意识到,如果我们真正想知道某个物体相对于相机的位置,就必须从其位置中减去相机在世界空间中的偏移量(world_mode_camera_offset_z)。而我们之前在计算中并没有进行这个减法操作,导致当前的代码存在问题,是不正确的。

之前可能是因为其他部分的逻辑刚好掩盖了这个错误,所以才"看起来能用"。现在我们修正了这个问题,确保在计算位置时正确地减去了相机的Z轴偏移值。这样一来,我们得到的相对相机的高度数据才是准确的,渲染和Alpha渐变等操作才能基于正确的高度信息来执行。完成这一步后,整个系统的运行表现开始变得正常,显示一切就绪,我们可以继续推进其他部分的开发。

运行游戏并确认现在效果恢复正常

现在一切恢复正常,整体状态良好。我们终于可以看到预期的淡入淡出效果重新发挥作用,视觉过渡也变得顺畅自然。当我们下降到较低区域时,画面也能正确呈现,没有异常现象发生。整体运行看起来更加完善、令人满意。

我们再次检查了"开始淡入"和"开始淡出"的逻辑,发现这些看起来都没有问题。目前这些值大致是合理的,不过之后还可以进一步调整以获得更好的视觉体验。虽然现在的过渡还不够理想,不够平滑,但这属于后期微调的部分,留待之后再优化即可。

总之,这一阶段我们已经顺利完成了关键的修复和校正工作。现在的系统能够正确识别不同Z层级之间的Alpha变化,并在渲染中按照相对相机高度进行合理的淡入淡出处理。后续可以更专注于美术表现上的细节调节,比如让过渡更柔和、调整范围更宽等,确保整体效果更自然流畅。当前这个基础已经是可靠且可扩展的。

黑板讲解:RecanonicalizeCoord 的作用

现在我们的目标是要让物体更加正确地处于各自的层级中,但目前存在一个问题,就是图层选择的逻辑并不完全正确。举个例子来说,楼梯的各个台阶理应被视为当前层的一部分,但从当前的行为来看,它们并没有被包含在正确的图层中。也就是说,我们希望保持在"当前楼层"的图层中进行绘制,而现在这个逻辑显然有偏差。

目前我们使用了 recanonicalize_coordinate 这个方法来处理坐标,但对于 Z 轴来说,这种处理是不合适的。这个函数中使用的是四舍五入的方式,它假定我们想要以坐标的几何中心来判断所处的图层。然而我们现在的目标是明确每个对象属于哪个楼层,而不是根据其中心点来推算所属图层。因此,这种"居中计算"的逻辑,在三维扩展之后就显得不合时宜。

回忆我们最初的设计思路,之前我们将物体位置定义为相对于 tile 的中心点进行存储,而不是 tile 的边角。这两种做法在二维中差别不大,但一旦扩展到三维空间,层级划分问题就变得明显了。我们现在需要的是更偏向"截断(truncate)"的行为,而不是"居中(centroid)"行为。

换句话说,我们要做的是:如果一个物体落在某一层之下,那就把它归入该层,而不是考虑它中心点是否穿越边界。在这种分层逻辑下,我们就需要重新审视 tile 的放置方式。如果我们坚持继续用"居中计算"的策略,那么地面 tile 本身的位置就需要微调(要么向下平移一些,要么向上),以便让包围盒的划分符合我们想要的图层逻辑。

当前我们的思考方向是:将底层的 tile 完全视为这一层的一部分,而将上方空间划入下一层。如果我们继续使用居中计算的方法,就必须人为地调整每个 tile 的放置高度,以便让它刚好落在我们划分图层的规则之内。否则,我们就得彻底切换思路,采用截断而不是居中的逻辑来决定物体属于哪个图层。

因此,这一问题的本质在于,我们需要明确并统一一个分层策略:是基于 tile 中心点来判定归属层,还是采用类似向下取整的逻辑,以确保每个 tile 和物体都出现在正确的层级中。一旦这个决定做出,其余的层级系统和渲染逻辑才会更加可靠和一致。

黑板讲解:Z轴偏移

当前的问题在于我们对楼层的划分逻辑存在不一致,特别是Z轴方向上各实体所属楼层的判断方式导致了一些渲染和逻辑错误。

具体来说,现在我们有两层楼的结构,存在一群带有Z轴偏移的角色或对象。当前系统的规则是这样划分楼层的:比如Z值在某个范围内的属于楼层A,而在另一个范围内的则属于楼层B。这个规则初看合理,但在实际中出现了问题。我们从侧面来分析场景,比如楼梯间的情况:

  • 一些角色位于较低位置,我们希望它们被归类到下层;
  • 另一些角色虽然视觉上应该还在下层,但由于他们的Z轴偏移,他们被错误地归入了上层。

用图形方式来理解的话,假设画出侧视图,楼梯所在的位置有一个坡度,角色们根据其在这个坡道上的位置有不同的Z值。而我们的图层判断逻辑是根据对象的中心点落在哪个高度范围内来决定它属于哪个楼层。结果是,明明视觉上应该属于下层的对象,却因其Z偏移刚好超过了当前层级的上界,被系统错误地识别为上层成员。

为了解决这个问题,我们得调整"楼层的参考面"。目前,我们是以tile的几何中心为参照点来定义每层的位置,但这种"居中判断"的方式不适合当前的三维图层划分。正确的做法应该是"把每一层的地板整体向下平移一个固定值",即每层的Z值基准点应当低于其中心值。

这个偏移调整的目标,是确保所有附着在某个楼层tile上的对象,即便有一定的Z轴偏移,也不会被误判为属于更高一层。所有的地面层也将处于层级定义范围的上半部分,而不是居中对齐。

这其实可以通过简单的修改来实现,例如在 world mode 中定义每层的实际Z值时,加上一个统一的负偏移,让地板位于该楼层tile中心点之下一个适当的距离。这样一来,角色的Z偏移仍然不会超出本楼层范围,所有的划分将更加合理。

这个调整不会引入复杂的逻辑,也不会改变tile或角色的世界坐标,仅仅是改变我们判断"对象属于哪层"的依据,从"中心对齐"变成"底边对齐",更贴合我们在视觉上和逻辑上对楼层的期望划分方式。

修改 game_world_mode.cpp:在 ChunkPositionFromTilePosition() 中让实体的 Z 轴位置向下偏移

我们目前正在处理的是楼层的Z轴偏移逻辑,重点在于如何正确地将房间或对象放置在合理的楼层范围中,以便避免出现视觉或逻辑上的错层错误。

在执行 AddStandardRoom 逻辑时,可以观察到我们通过某个 P.offsetZ 的值来决定房间(或对象)在Z轴上的位置。而在调用 ChunkPositionFromTilePosition 时,这个函数内部默认采用的是将tile的位置直接映射为某一"楼层"的概念,它根据tile的位置直接确定了归属的层级。

然而,我们想要实现的效果并不是这种默认的对齐方式。我们希望的是,所有对象的Z轴偏移应该是从tile几何中心向下有一个合适的偏移量。换句话说,每层的"可视区域"应该从其tile中心点往下延伸一个距离,这个距离应基于tile的深度(例如以tile的米数为单位),用于表示该层的地板区域。这么做的目的是让具有Z轴偏移的对象不会轻易越界"蹦"到上一层去。

我们在代码中尝试实现这一点:在添加标准房间时,将其Z轴偏移值设置得更低一些,希望借此让所有实体相对tile的Z值都往下偏移,从而避免不正确地被分类到上层。

理论上讲,完成了上述偏移调整后,所有对象的位置都应该被"拉"到正确的楼层范围中,不再跨越层级被错误渲染。然而,实际运行中我们发现这种调整并未完全生效。举例来说,仍然有两个对象显示在不应出现的地方,也就是说,它们的Z判断逻辑依然被归入了错误的层级。

这种现象说明,尽管我们已经修改了Z轴偏移的逻辑,但渲染或分层的判断机制中可能还有其他遗漏。可能是在计算Z层级归属的函数中还有未使用新的偏移规则,或者某些地方仍然使用了原本未修正的tile中心点作为判断依据。

这说明接下来还需要进一步排查Z轴分层逻辑的完整性,确认是否所有参与渲染和层级归属的逻辑路径都采用了统一的偏移准则,确保所有对象能够在视觉上和逻辑上正确归属于其所在的楼层。

修改 game_world_mode.cpp:在 ChunkPositionFromTilePosition() 中以不同方式计算 TileDepthInMeters

我们遇到的问题是某段代码无法正常工作,目前还不清楚是因为逻辑错误,还是我们当初设计时就犯了某种错误。情况相当复杂,需要仔细分析。

目前系统中存在多个"堆栈"(stacks),而在这些堆栈中的某个位置是模拟区域(simulation region)的原点。大多数情况下,这个模拟原点会刚好落在某一个切片(slice)上,也就是说它和切片是对齐的。

问题可能出现在相机的位置和模拟区域的位置之间存在偏差。虽然我们以为模拟原点就在我们观察的位置上,但实际上相机的位置是不一样的,相机会有自己的偏移。

模拟区域的原点是按固定步长(step)移动的,而相机却有一个额外的偏移量,也就是说我们不能简单地把相机的位置等同于模拟原点。相机有一个"world mode camera offset"(世界模式相机偏移),这是一个额外的因素,可能正是导致我们之前逻辑出错的关键。

因此,模拟区域在移动时是按步进更新的,而相机的位置是独立的,并且带有额外的偏移,造成了两者之间的不一致。这种不一致可能就是导致我们观察到问题的根本原因。我们需要重新考虑相机和模拟区域的位置关系,明确它们在空间中的对应方式,从而修正现有的计算或渲染逻辑。

使用调试器:中断进入 ChunkPositionFromTilePosition() 并检查偏移值

我们正在检查某个区域的创建过程,重点是观察从顶部位置映射到块(chunk)空间时的坐标变换情况,以确认是否符合预期。

首先,我们注意到Z轴方向上有一个位移是-1.2,这是符合预期的结果,因为这个值正好是某个数除以0.4得出的,表示模拟空间中的Z轴位置。接下来我们将这个值映射到块空间中,结果也如预期一致,说明目前的映射过程本身没有问题。

然后我们查看了一个标准房间的代码,其中有一个offset_y + 2的操作,这个是将整体向上移动的逻辑。我们暂时不明白这个操作是否合理,但从结果来看,它似乎确实完成了我们想要做的事情。然而也可能存在某种逻辑错误。

进一步分析我们发现,如果offset_y的值在-2到2之间,那么加上2之后的范围就是0到4。这种情况下,理论上Z轴偏移(p_offset_z)应该不会越界,仍然在允许的范围之内。这种判断基于对偏移后的数值的理解,预期结果仍在合法范围内。

我们随后尝试测试最高的一个偏移情况,具体坐标是0、1、2,并观察此时的p_offset_z值。在这种情况下,我们看到初始偏移是-1.2,然后叠加上楼梯最高层的位移,结果得出0.8。

这个结果在数值上是合理的,因为0.8仍然会被归类到原本的那一层chunk中,也就是说它的坐标在处理时仍会"对齐"到同一层chunk。因此,从理论上看,所有角色应该都还在同一个chunk之内。

但实际观察发现情况并非如此------最终效果并没有达到我们预期的"所有人都在同一个chunk"这一结果,说明某处逻辑可能仍存在问题,尽管看起来计算是正确的。可能是由于某个细节没有被正确考虑或执行导致的偏差,还需进一步深入验证。

使用调试器:中断进入 ConvertToLayerRelative() 并查看 Z 值

我们当前的问题是,尽管在逻辑上将对象正确地分配到了对应的chunk中,但在结果上却没有得到预期的渲染效果。我们开始进一步追踪Z轴坐标的数值,以寻找偏差的来源。

我们注意到,实际获取到的Z值频繁为0,这显然不符合我们之前的偏移计算结果。推测可能是相机的位置被设置到某个特定对象上,从而抵消了我们原本对位置所做的位移。这意味着虽然对象在模拟阶段正确地归类到了chunk中,但在渲染阶段这些位移被抵消了,导致显示结果出现偏差。

理想情况下,渲染过程本应直接依据chunk信息来处理对象的坐标,而不是依赖当前相机的偏移,这也是接下来需要优化的部分。

我们进一步查看了实体变换的处理逻辑,发现其使用了默认的"直立变换"方式,并通过get_ground_point_entity减去camera_p的方式进行坐标换算。但是这一处理方式存在问题,因为并未使用绝对坐标。我们真正需要的应该是实体在全局空间中的绝对位置,而不是相对于当前模拟区域的偏移值。

进一步推断,get_ground_point_entity可能本身是相对于模拟区域(sim region)的位置,而非绝对世界坐标。这意味着如果我们只看相对坐标,在相机已经发生位置抵消的情况下,可能会导致渲染出的层级不准确。

因此,我们实际想要知道的应该是两个chunk之间在Z轴上的差值:一个是实体所属chunk,另一个是当前模拟区域的原点所处的chunk。这才是影响渲染排序或层级归类的真正关键值。

虽然当前的处理流程中存在一些杂音和干扰因素,但从逻辑上我们已经逐步理清楚了核心问题所在。接下来需要调整的是坐标变换和渲染时所依据的基准,使其基于绝对chunk位置进行判断,而非相对坐标,这样才能得到一致的渲染效果。我们还在理清思路的过程中,很多细节需要逐步推敲和确认。

修改 game_entity.cpp:临时计算实体应处于的相对图层

我们的目标是正确判断实体所在的层级,以用于层级之间的透明度(alpha)查找。为此,我们需要基于实体在世界中的实际位置来进行计算。

具体而言,我们需要获取实体在世界空间中的全局坐标,尤其是其所在的chunk的Z轴索引。然后,将这个Z索引减去模拟区域(sim region)原点所在chunk的Z索引,得到它们之间的Z轴chunk差值。这个差值就是我们用于判断实体所属"相对层级"的关键数据。这个计算结果将用于渲染系统中确定透明度查找表所使用的层索引。

目前的一些旧逻辑,比如使用实体Z位置与相机Z位置之间的差值来计算层级,已经不再适用,因为现在的系统中,相机Z的差异不再影响层内的变换。每个层是独立渲染的,内部不再有基于Z轴的比例变换。

因此,之前那些依赖相机Z距离的判断逻辑可以被废弃,我们只关心chunk之间的层级偏移。为了将这个思路落实,接下来的工作是:

  • 建立获取实体在世界空间中位置的基础逻辑;
  • 提取chunk Z索引并进行差值计算;
  • 基于这个差值决定该实体处于哪个相对渲染层;
  • 将旧的、基于相机Z差的判断逻辑移除。

此外,我们记录了一个待办事项,即明确指出:层级判断依赖的是实体和模拟区域原点在chunk Z方向上的差值。当前有一个与多行双斜杠注释相关的bug也已经报告,预计将在后续版本中修复。

总体而言,现在我们已经清晰了下一步的修正方向,尽管还有许多工作需要完成,但路径已经明确,后续只需按计划逐步推进即可。

问答环节

你预计最终游戏会有足够多的 sprite 需要使用纹理图集,以减少每帧的纹理绑定次数吗?

我们讨论了游戏中是否有必要使用纹理图集(texture atlas)来减少每帧的纹理绑定次数。结论是,这主要取决于具体情况,但总体来说可能性不大。

现在硬件对纹理切换的开销已经非常小,几乎不存在性能惩罚。尤其是在Nvidia显卡上,这一点非常明显,因为它们支持纹理绑定列表(bind lists),几乎不会有纹理切换的性能问题。换句话说,现代硬件基本不需要担心频繁切换纹理的性能影响。

即使是Intel显卡,纹理切换的开销也已经微乎其微。唯一可能有些差异的是AMD硬件,但总体来说,这不是一个普遍存在的瓶颈。

如果目标是非常老旧的硬件,那么纹理切换的开销可能才会成为问题。但对于当前和未来的硬件平台,完全可以忽略这点。

此外,即便游戏最终需要使用纹理图集,这也不必在开发初期就强制实施。可以在游戏完成或发布后,再进行纹理图集的优化。这不会影响整体架构设计,只是要求资源管理器更智能,能够更好地处理资源的分页加载和管理。

总结来说,纹理图集优化是一个后期优化步骤,不必过早考虑,不同硬件平台有不同表现,但现代主流硬件几乎不受影响。我们的重点应放在功能和架构的实现上,性能优化可以留到后期针对具体硬件进行。

我不明白为什么每层楼的"基准"Z值是负的,这看起来太刻意了。从逻辑上讲,让 0 作为底部不是更合理吗?是不是和渲染有关?我最近没跟进直播内容

关于楼层的基准值为什么是负数,而不是从零开始,这其实并没有绝对的对错,这种设计非常灵活,可以根据需求自由决定。

有观点认为基准值为零更合逻辑,尤其是作为楼层的最低点,但实际上这主要取决于渲染和空间划分的需求。这个问题本质上是一个空间分区的问题,我们把场景分成了"块"(chunk),每个chunk代表空间中的一个区域。

选择负数作为基准值并不奇怪,也不是勉强的做法,只是空间划分的一种合理方式。没有所谓"唯一正确"的答案,完全可以根据具体需求调整。

另外,在调试过程中遇到了屏幕保护程序或屏幕锁定自动启动的问题,影响了观察和操作,需要关闭自动锁屏功能以便更好地查看调试信息。这是一个额外的环境配置问题,不影响核心逻辑的讨论。

总结来说,空间分区的基准值设计是灵活且可调整的,没有唯一标准。当前遇到的屏幕锁定问题已经通过关闭自动锁屏功能得以解决,方便后续调试和观察。

黑板讲解:区块的规范化点

我们讨论了在三维空间中如何定义一个"块"(chunk)内的标准参考点位置的问题。虽然游戏中使用的是二维精灵,但底层的空间表示实际上是三维的。

对于一个块来说,有几种选择来确定其标准参考点的位置:

  1. 选择块的一个角落作为参考点,比如左下角。
  2. 选择块的中心作为参考点。
  3. 选择块在X和Y方向的中心位置,但在Z方向是底部。

这个选择会直接影响到相对位置的基准值,比如楼层的Z值。如果选择角落或者中心底部作为参考点,楼层的Z值可能接近零,但实际上通常不会是整零,因为处于边界会导致排序和舍入的问题,可能会希望偏移一点(比如0.1),以避免计算上的麻烦。

如果选择的是立方体中心作为参考点,那么楼层的相对Z值会是一个负数,比如-0.5或者其他数值,取决于楼层距离中心的高度。这个选择也是完全可以的,没有绝对对错。

但是,如果选择楼层为零作为基准,而X和Y方向又以中心为参考点,这会导致坐标系在处理上变得不一致。因为这样X和Y是以中心计算,而Z是以底部计算,导致计算时的参考点不统一,逻辑上会显得混乱,需要在代码中分开处理。

尽管渲染系统中因为某些效果必须对Z轴做特殊处理,但空间分区本身不应该引入这种人为的区别。空间分区的三维坐标系统最好保持一致性,要么全部用中心点,要么全部用角落(最小点)。

因此,如果坚持要让楼层的Z值基准为零,那么X和Y也应该改为以块的最小角作为基准点,而不是中心。这样三维空间的参考点统一,代码处理也更简单清晰。

总结来说,定义块内参考点的位置没有唯一标准,完全是设计选择,但为了代码的简洁和一致性,建议X、Y和Z方向采用统一的基准点,避免不同轴用不同基准点导致的复杂度增加。

相关推荐
liuyang-neu14 分钟前
力扣 155.最小栈
java·算法·leetcode
心软且酷丶19 分钟前
leetcode:2160. 拆分数位后四位数字的最小和(python3解法,数学相关算法题)
python·算法·leetcode
Musennn1 小时前
leetcode98.验证二叉搜索树:递归法中序遍历的递增性验证之道
java·数据结构·算法·leetcode
互联网搬砖老肖1 小时前
React的单向数据绑定
前端·javascript·react.js
NoneCoder1 小时前
React 生命周期与 Hook 理解解析
前端·react.js·面试
reduceanxiety2 小时前
机试 | vector/array Minimum Glutton C++
数据结构·c++·算法
盛夏绽放2 小时前
Python常用高阶函数全面解析:通俗易懂的指南
前端·windows·python
2301_794461572 小时前
力扣-最大连续一的个数
数据结构·算法·leetcode
深空数字孪生3 小时前
前端性能优化:如何让网页加载更快?
前端·性能优化
阿乐今天敲代码没3 小时前
echarts实现项目进度甘特图
前端·echarts·甘特图