游戏引擎学习第297天:将实体分离到Z层中
回顾并为今天的内容做准备
昨天我们做了雾效混合(fog blend)和透明度混合(alpha blending)的尝试,现在正在进行渲染部分的深度(Z)清理工作。今天的重点是把“切片”(slices)变成一个核心的系统,也就是让切片成为第一类对象,这将是一个很大的改动,需要花一些时间和精力。我们现在基本准备好了,接下来就是进入代码,真正实现这个功能。
我们做了一个测试,把瓦片放置在远处并逐渐淡出。测试结果显示有一些深度排序的问题,这也是昨天提到的。同时,还有另外一些问题,比如目前并没有把这些切片当成独立的对象处理,所有的对象都是一视同仁地传入渲染流程,所以会出现一些问题。
举个例子,当我执行透明度淡出时,重叠的对象会各自单独淡出,而不是一同淡出。虽然淡出的效果不太明显,但这种分开处理透明度的方式并不是我们想要的。理想的情况是,重叠对象能一起进行正确的透明度混合。
演示单独处理带有透明度混合纹理的问题
可以把动画的淡出过程放慢很多,比如放慢到原速度的10%,这样更容易观察问题。比如看怪物的躯干在跳到下面一层时,它和下面的物体是如何淡出的。现在它自己在淡出,同时它所在的瓦片也在淡出,导致你可以看到它背后的瓦片,这种表现并不是我们想要的。
我们不希望处于上层瓦片上的物体和瓦片各自单独淡出,而是希望整个图像——包括物体和瓦片——作为一个整体一起淡出,保持整体连贯性,让人感觉像是在穿过一个整体逐渐变得朦胧的场景。
此外,还有一个后续要处理的问题是光照。我们之前已经做了一些光照相关的工作,之后还会继续做。光照的实现很大程度上依赖于对二维切片的理解,也就是说,需要知道每个切片里的光照情况。
因此,我们需要让渲染器能够接收关于切片的信息,这样它才能判断每个切片应该作为独立单元来渲染。为了实现这一点,需要设计一定的通信机制,让渲染系统知道如何处理这些切片。
game_render_group.h:考虑在 render_entry_cliprect 中添加切片信息
我们已经有了 clip rect(剪裁矩形)的概念,于是我们考虑能否将关于 slice(切片)的信息直接放到 clip rect 中。因为每个条目都会关联一个特定的 clip rect,这样做显得非常合理。我们可以为每个 slice 设置一个对应的 clip rect,这些 clip rect 实际上并不需要执行额外的剪裁操作,它们甚至可以共用同一个剪裁矩形,但每一个都包含关于该 slice 的额外信息。
这些额外信息可以包括:
- 该 slice 是否需要雾化处理;
- 是否需要淡入淡出效果;
- 渲染顺序;
- 其他与渲染相关的控制信息。
通过这种方式,我们可以把控制逻辑直接绑定在 clip rect 上,管理起来会更加集中和简单。这样不需要额外引入一个新的概念来描述这些切片的分组或特性。如果不将这些信息附加在 clip rect 上,我们就需要新增一个关于 Z slices 的抽象结构,用于将条目聚集成切片,并作为独立的渲染单元处理。这会引入更多复杂度。
我们需要这些切片能够作为独立对象被渲染,例如渲染到各自的缓冲区,以实现像透明度淡出这样的效果。
还有一种可选做法是:为每个 slice 分别创建一个 render group(渲染组),每个渲染组只负责渲染它自己的 slice。这样可以依次进行多个渲染操作,每个 slice 单独处理。这种方法的可行性还有待评估,虽然它带来了灵活性,但可能会增加性能开销或系统复杂性。
目前尚不清楚哪种方法是最优解,因此需要逐一分析每种方法涉及的工作量与利弊。同时也可能存在完全不同于上述方案的替代路径,需要进一步探索与验证。总的来说,我们已经掌握了部分基础信息,并需要在此基础上评估最合适的架构设计。
game_world_mode.cpp:考虑指定相对的楼层层级
我们正在讨论关于如何将实体划分进不同的 slice(切片)的问题。我们当前已经有每个实体的 Z 值,它标识了该实体的空间位置,但目前这些 Z 值与楼层(floor)之间的关系并不明确,这导致我们难以准确地知道一个实体属于哪一层或哪一个 slice。
虽然我们的游戏表面上是 2D 的,但实际上我们允许在 3D 空间中任意放置物体。因此目前是通过 3D 空间查询来定位实体,而不是通过明确定义的楼层系统。这会让 slice 的判断变得模糊。
我们认为一个合理的方式是:因为整个世界在存储时已经被平均切分成一个个 slice 区块(类似体素或格子),所以我们可以直接用实体的 Z 坐标进行 flooring 操作(向下取整)来推断其所在 slice。这种方式可以简单而稳定地将实体分配到正确的切片中。
另外,我们可以引入一个明确的 slice 编号系统。例如:
- 0 表示玩家当前所处的楼层;
- 正数(1、2、3…)表示玩家上方的楼层;
- 负数(-1、-2、-3…)表示玩家下方的楼层。
考虑到渲染需求,我们实际上只需要关注当前楼层、上面一层(用于渐变淡出效果)以及下面几层(用于从洞口往下看的远景)。这意味着我们不需要为所有楼层保留状态,只需跟踪有限几层即可。
在代码结构方面,我们已经有了 entity_transform
结构体,它包含了实体的空间偏移 offset_p
,其中包含了 Z 值。我们完全可以在这个结构体中加入一个 slice_index
字段来表示当前实体所属的 slice。
为了保持结构精简,可以利用原有的空间,比如:
- 使用
uint8
类型的字段存储slice_index
; - 将原有的
uint16
类型字段拆分为两个uint8
字段,分别表示类型和 slice 索引; - 这样可以将该结构体保持在仅 32 字节以内,同时附加必要的 slice 信息。
从渲染角度来看,我们有两个方向可选:
-
将 slice 信息单独存储于实体结构中
这种方式不依赖clip_rect
,而是让每个实体记录自己的 slice index。渲染系统在绘制时使用该 index 归类渲染对象,实现逐层渲染和混合。 -
将 slice 信息合并进
render_entry_cliprect
中
在这种方式下,clip_rect
除了剪裁信息之外,还包含:- slice index;
- 渲染效果参数(如全局颜色、是否淡出、是否雾化等);
- 是否为一个独立渲染的 slice 单元,是否需要单独缓存再合成。
后者提供了更强的封装性,把所有关于 slice 渲染的信息集中在一起,使得渲染逻辑更集中、更清晰。
目前还未决定采用哪一种方式,我们需要进一步评估哪种方案更具可扩展性、更符合整体架构,并考虑后续功能如混合、排序、遮罩、缓存等的实现复杂度。整体思路是尽可能简化结构、保持渲染系统的高效性和可维护性。
game_world_mode.cpp:让 UpdateAndRenderWorld 循环遍历 LevelIndex,使用 PushClipRect 设置 ClipRectIndex
我们正在探索如何将 clip rect(剪裁矩形)用作 slice(切片)管理的载体,从而为渲染系统中的多层结构提供统一的管理方式。核心思路是在渲染初始化阶段,预先创建好所有需要的 clip rect,并将其存入数组中,便于后续渲染阶段按需引用。
目前系统中尚未实际分配或使用 clip rect,这是首次将其引入并与多层渲染逻辑结合。在渲染开始时,我们需要确定总共要处理多少层,假设当前玩家视角在第 0 层,我们打算支持从第 1 层往上、到第 -4 层往下的渲染,意味着总共需要 6 层。
我们会通过以下方式处理这些 clip rect:
-
确定层级范围:
min_level_index
为 -4;max_level_index
为 1;- 则总层数为
max_level_index - min_level_index + 1
,即 6。
-
创建 clip rect 数组:
- 用于存储各个层级对应的剪裁矩形索引;
- 每个剪裁矩形都会通过调用
PushClipRect
接口压入渲染组(render group); - 裁剪区域设置为整个屏幕区域(因为暂不做区域裁剪),使用 render group 的默认宽度和高度即可。
-
初始化流程:
- 使用循环遍历所有层级索引,依次生成 clip rect;
- 建议循环时直接用数组长度而非预设常量,以避免潜在出错。
-
实体渲染时关联对应 slice:
- 每个实体在渲染时会根据其 slice index 选择合适的 clip rect;
- 具体实现方式是将
render_group.current_clip_rect_index
设置为该实体所属层的 clip rect 索引; - 当前逻辑中我们是直接赋值
current_clip_rect_index
,之后可能需要引入一个工具函数来封装设置与恢复操作,使流程更安全简洁。
-
未来优化方向:
- 当前 clip rect 索引是手动管理的,建议设计一个 push/pop 栈式管理接口,以简化状态管理并减少手动恢复的出错风险;
- 同时 clip rect 可扩展支持更多的渲染参数,如透明度、颜色变换等,为后续渲染合成打好基础。
总的来说,这一套机制的目的是在渲染系统中以最少的结构改动,引入对多层视图和 slice 渲染的支持,借助现有 clip rect 系统来承载新的 slice 管理逻辑,实现逻辑上的分层渲染,并为未来的效果扩展(如雾化、透明渐变、视野遮挡等)提供统一的入口。
game_render_group.h:引入 transient_clip_rect
我们打算引入一个临时的 clip rect(剪裁矩形)管理结构,用于简化当前的剪裁状态保存与恢复逻辑,避免在渲染流程中频繁手动设置和恢复 clip_rect_index
,从而提升代码的可读性和可维护性。
核心思想是构建一个具备构造函数与析构函数的轻量级结构,在创建时自动保存当前状态,在生命周期结束时自动恢复之前的状态,相当于一个局部作用域内的状态保护机制。
具体实现逻辑如下:
1. 结构命名与作用
- 命名为
TransientClipRect
(暂名,可替换); - 构造函数用于保存当前
clip_rect_index
并设置新的; - 析构函数自动恢复原先的
clip_rect_index
; - 作用域结束时自动回退,不需要手动恢复,减少出错风险。
2. 构造函数行为
- 接收
render_group
的引用; - 接收一个新的
clip_rect_index
; - 在构造时保存当前
render_group.current_clip_rect_index
; - 然后将其设置为新的 index;
- 如果没有传入新的 index,也允许只保存当前状态不做修改,方便外部灵活控制。
3. 析构函数行为
- 在对象销毁时将
current_clip_rect_index
恢复为构造时保存的值; - 自动化的状态回滚机制,保证渲染流程中 clip rect 状态始终一致。
4. 细节注意点
- 因为 C++ 的作用域规则限制,构造体中无法直接访问外部变量,必须通过构造函数传入;
- 实现中临时存储旧的 clip rect index 是必要的;
- 可以考虑设置默认构造方式,在构造时不强制设置新的 clip rect,而是允许仅保存当前状态,以供后续代码自行设置;
- 这种结构非常适合像渲染组这样的状态类资源,在需要局部修改状态时使用。
5. 示例结构
struct TransientClipRect {RenderGroup *Group;u32 OldClipRectIndex;TransientClipRect(RenderGroup *Group, u32 NewIndex) : Group(Group), OldClipRectIndex(Group->CurrentClipRectIndex) {Group->CurrentClipRectIndex = NewIndex;}// 重载构造函数:仅保存当前状态,不设置新的 indexTransientClipRect(RenderGroup *Group): Group(Group), OldClipRectIndex(Group->CurrentClipRectIndex){// 不修改当前 index,用户手动控制设置}~TransientClipRect() {Group->CurrentClipRectIndex = OldClipRectIndex;}
};
6. 使用方式示例
{TransientClipRect scope_clip(&RenderGroup, SomeSliceClipIndex);// 此处的渲染使用新的 clip rect// ...
} // 作用域结束自动恢复
这种机制非常适合局部使用 clip rect,比如在渲染一个 slice、一个 UI 面板,或临时切换状态时。它提供了更强的安全性与封装性,使得我们可以避免全局状态污染,并减少手动管理带来的潜在问题。
game_world_mode.cpp:让 UpdateAndRenderWorld 将三个主要层放入不同的 ClipRectIndex
我们打算引入一个 TransientClipRect
(临时剪裁矩形)机制,自动保存并恢复当前的剪裁状态,使得我们在设置新的 clip rect(剪裁区域)时不必担心手动还原之前的状态。这种方式允许我们更加自由地对不同实体或渲染批次设置所需的剪裁矩形,简化逻辑,提高灵活性和可维护性。
实际操作逻辑如下:
1. 创建多个剪裁矩形
我们打算在渲染开始前预设多个不同的 clip rect(如 clip_rect_index 0、1、2),将它们分别推送至 RenderGroup
中,表示渲染中的不同 slice(图层、楼层或深度区段)。这样可以把场景分成不同区域进行处理,实现比如不同楼层的独立渲染与后期合成(如 Alpha 淡出)。
2. 使用 TransientClipRect 自动管理状态
为了避免频繁保存和恢复 clip rect 状态,我们使用 TransientClipRect
来自动完成状态的推入和恢复:
- 每当我们需要切换到一个新的剪裁矩形时,就创建一个
TransientClipRect
实例; - 构造函数中保存当前的
clip_rect_index
并设置新的; - 析构函数中自动恢复之前保存的 index;
- 保证外部调用者状态不被污染。
这种封装方式让我们可以“肆无忌惮”地在任何需要设置剪裁矩形的位置进行设置,而不用担心清理或恢复逻辑。
3. 测试用例逻辑
我们暂时手动创建三个不同的 clip rect 并分配给不同的 slice,验证 clip rect 切换是否正确生效:
- 每个 clip rect 都使用相同的大小,覆盖整个渲染缓冲区;
- 创建一个
clip_rect_indices
数组,储存所有的 index; - 在每个 slice 渲染过程中,使用
TransientClipRect
绑定对应的 clip rect index; - 在作用域结束后 clip rect 状态会被自动恢复。
4. 渲染结构的调整建议
为了使渲染逻辑更清晰,打算将目前包含模拟与渲染的大块代码拆分成独立函数:
- 当前
SimulateEntities
函数既包含实体的更新,也包括实体的渲染,逻辑臃肿; - 可以将“模拟 + 渲染”流程分成
SimulateEntities
和RenderEntities
,或类似命名; - 在新的渲染函数中进行剪裁矩形切换以及实体绘制逻辑,这样更贴近我们的目的,也利于未来维护或扩展如分层渲染、模糊、光照等操作。
总结
我们实现了一个临时剪裁矩形机制,使剪裁状态的切换更加可靠和自动化,并基于此设计思路初步验证了通过预设多个 clip rect 来实现不同 slice 的分离渲染。下一步我们准备将模拟与渲染逻辑解耦,进一步清理渲染结构,使其更便于扩展和维护。这个机制为后续实现类似多楼层、深度雾效、独立透明度等视觉效果打下基础。
game_entity.cpp:引入 UpdateAndRenderEntities
我们正在将渲染与更新实体的逻辑提取到一个新的函数中,比如叫 UpdateAndRenderEntities
,使得主流程更加清晰且更易维护。这个新函数需要接受一系列参数,因此我们开始梳理所有它需要的输入内容,并将原先的大块代码搬迁进去。以下是我们整理出的详细内容和设计决策:
1. 基础参数提取
我们将以下关键对象作为参数传入该函数:
RenderGroup
:渲染命令集合,是渲染所有实体的核心;SimRegion
:模拟区域,包含当前帧需要更新和渲染的所有实体;TimeFunction
:时间曲线函数,用于时间相关的插值或过渡;CameraPosition
/FadeTopStartZ
等摄像机参数:这些可能来自 world 模式,用于决定视角切换、淡出等效果;DrawBuffer
的宽高:用于初始化 ClipRect;虽然理论上可以复用当前的剪裁矩形,但由于当前实现没有保存剪裁矩形的逻辑,因此我们决定暂时继续传入 draw buffer 信息。
2. 剪裁矩形的传递与生成
我们观察到剪裁矩形(ClipRect)其实是在 RenderGroup
中以链式结构存储的,每次调用 PushClipRect
都会在链表中插入一个新的剪裁矩形命令节点。由于这些节点尚未被线性化(即没有形成一个可查询数组),想在之后获取某个 clip_rect_index
实际对应的剪裁区域并不容易。
因此,当前阶段我们决定采取更直接的方式:直接传入 DrawBuffer
的宽度和高度来生成剪裁矩形,避免深入重构这一部分机制。这种方式能快速落地并保证功能正确运行,待后期优化时再考虑为剪裁矩形提供明确的查询与复用机制。
3. 其他参数的整理
我们还识别出了一些额外的参数或状态信息:
- 背景色
BackgroundColor
:当前清屏颜色,虽然将来可能从更合理的配置来源中获取,但现阶段仍需显式传入; dtForFrame
(帧时间):已在外部调用处可用,因此无需在新函数内部重复计算;TranslateAsset
(资产位置转换器):用于处理资产加载和坐标变换,是必要参数;DrawHitPoints
:是否绘制角色血条或类似调试信息,也被提取至新函数;MouseP
(鼠标位置):目前处于未使用状态,暂时传入但用途有限,只在调试场景中使用。
4. 结构优化的方向
由于原来的渲染和模拟逻辑都混杂在一个函数中,现在我们将其分离为 SimulateEntities
和 UpdateAndRenderEntities
两个模块式函数,分别负责:
- 实体的逻辑模拟、行为处理;
- 渲染剪裁管理、可视实体绘制。
这样做的好处包括:
- 模块职责更清晰;
- 更便于未来替换、重构或并行处理;
- 可在不同上下文中复用
UpdateAndRenderEntities
,如不同的相机视角、不同层级的渲染等。
总结
我们将渲染与更新实体的逻辑提取到一个新函数中,并理清了所有参数的来源与用途。剪裁矩形的管理暂时使用简化逻辑,后续可进一步优化为可查询的结构。整个过程为渲染结构的模块化、清晰化打下基础,也为将来的功能扩展(如图层淡入淡出、摄像机切换、多视口支持等)预留了灵活空间。
game_world_mode.cpp:让 UpdateAndRenderWorld 调用 UpdateAndRenderEntities
我们继续将主函数中与实体渲染有关的部分移入新的函数中。由于我们已经在前一步整理好了所需的所有参数,并且这些参数在原有代码中命名一致,因此我们直接将这些变量作为参数传入新函数即可。下面是这一步的具体操作和细节说明:
1. 直接传参实现替换
由于原始变量名和新函数参数名保持一致,我们无需重命名或重写调用逻辑。我们可以直接将旧逻辑中的变量声明部分删除,直接调用新函数。例如:
UpdateAndRenderEntities(RenderGroup,SimRegion,CameraP,DrawBuffer,BackgroundColor,dtForFrame,TranslateAsset,MouseP
);
这样可以保证逻辑的一致性和替换的最小化风险。
2. 清理冗余代码
在完成新函数调用替代原逻辑后,我们清理了不再需要的旧变量定义。由于新函数已经统一接收参数,原函数体中原本的参数初始化代码可以完全移除,减少了代码冗余并提高了可读性。
3. 确保一致性与正确性
我们检查函数签名与调用处的一致性,确认所有参数都已经在调用位置正确传入,类型和顺序都符合要求。理论上这样应该能直接通过编译器的类型检查。某些中间调试或临时代码(例如忘记删除的测试变量或标记)也已被一并清除。
总结
我们完成了将实体更新和渲染的逻辑提取到独立函数中的收尾工作。通过保持参数名一致,我们实现了无缝替换,避免了重复代码和潜在的命名冲突。同时,结构更加清晰,便于后期维护和功能拓展。下一步可以专注于优化该函数内部的实现细节,或进一步拆分细粒度逻辑以提升灵活性与可重用性。
game_entity.cpp:通过将当前层的屏幕尺寸减半来测试裁剪
我们现在实际上已经完成了剪裁区域(clip rect)的设置,并且可以通过人为缩小某个剪裁区域来测试它是否工作正常。具体操作和逻辑如下:
1. 准备测试剪裁区域效果
我们在实体绘制相关的代码中进行处理,比如在 PushClipRect
的设置过程中。原本的代码会为每一个渲染层级设置一个完整大小的剪裁矩形,而我们现在尝试通过人为缩小其中某一个来验证是否确实发生了剪裁。
2. 判断当前的剪裁区域是哪一层级
由于我们当前的层级设置是一个范围,比如从 -4 到 1,所以我们实际使用的索引是 最大层级索引 - 最小层级索引
。我们用这个索引来定位具体是哪一层剪裁区域被设置。比如,当前的正常可视层级是索引为 2 的那一个(因为总共是 6 层,从 -4 到 1,0 层对应的是索引 4)。
我们判断:
if (level_index == 2) {// 默认的全屏剪裁区域
} else {// 用于测试的缩小剪裁区域
}
3. 缩小剪裁区域实现方式
我们在设置 PushClipRect
的时候,通常会传入一个完整的 width
和 height
。现在为了测试,我们对某一层级的剪裁矩形宽度进行人为缩小。实现方式很简单:
Width = Width / 2;
这个修改只针对测试层级生效,因此我们可以观察到这个层级的渲染效果会被“裁掉”一半,从而验证剪裁功能是否正常工作。
4. 硬编码 vs 通用处理
虽然当前是通过硬编码的方式来处理特定层级的剪裁行为,但这是临时测试用途。后续可以考虑建立一个更通用的逻辑结构来决定各层的可视区域,比如根据视差或距离比例来自动调整剪裁区域大小,以提高系统的灵活性和扩展性。
总结
我们通过人为缩小一个剪裁区域(clip rect)来验证当前的图形系统是否确实对每一层级应用了正确的裁剪。通过判断层级索引并在特定层级上调整剪裁区域宽度,我们可以清晰地看到渲染内容是否被限制在该区域内。这种方式是验证图形剪裁是否生效的一个直观有效手段,同时也为未来做更复杂的视图管理打下了基础。
运行游戏,发现画面被裁剪到了屏幕下半部分
现在我们应该能够看到一个清晰的效果:
画面中被设置剪裁区域的那一层完全限制在了屏幕的下半部分渲染,而其他层则没有受到影响。
1. 剪裁区域测试成功
通过人为缩小某一层级的剪裁区域,我们验证了该层的内容确实只被渲染到了屏幕的下部。说明每一层对应的剪裁区域已经成功地生效,并且各自独立。
2. 上层不受限制
因为我们只对某一特定层做了剪裁,而其他层保持默认全屏剪裁,所以可以看到:
- 上层(如雾化渐变层、Alpha层)的内容没有受到任何限制,仍然被正常渲染在整个画面范围内;
- 只有中间层(我们缩小剪裁的那一层)被限制到了屏幕的下半部分,这进一步说明层级的剪裁是分开控制的。
3. 测试方法进一步验证
为了更直观,我们可以尝试跳跃角色或镜头视角,让不同层级的内容进入视野,比如进入雾化区域观察:
- 当我们进入有雾的区域时,可以看到处于雾化效果控制下的对象不受剪裁影响;
- 而中间层的对象依然被限制在屏幕下部;
- 若还原剪裁设置,这些中间层对象会再次铺满整个区域,说明之前的限制确实是剪裁造成的。
4. 层级划分与剪裁逻辑验证成功
这种测试方式有效验证了我们对每一层使用不同剪裁区域的系统逻辑是正确的:
- 每层拥有独立的剪裁区域;
- 每个实体被正确地分配到相应的渲染层;
- 我们可以按需控制每层的渲染区域,实现灵活的分层渲染与可视控制(如视差、雾化、Alpha 混合等)。
总结
我们已经验证了剪裁系统的独立性和有效性:不同层级拥有各自的剪裁区域,而实体也正确地被归类到对应的层中。通过人为控制其中一层的剪裁范围,我们清晰看到了剪裁效果对特定图层的影响,而其他层依然保持正常渲染。这一过程帮助我们确认了图形管线的层级渲染与区域裁剪机制是正确运行的,并为后续更精细的图形效果打下基础。
game_entity.cpp:让 UpdateAndRenderEntities 为每个 LevelIndex 计算 CameraRelativeGroundZ
我们现在需要更严谨地处理每一层的地面高度(camera_relative_ground_p
)等与相机相关的数值计算,因为接下来我们要开始给这些层分配楼层编号(floor numbers
)。虽然当前我们不打算改变剪裁区域(Clip Rect)的行为,但这些和相机高度相关的信息现在应该跟剪裁区域绑定,或者说应该与某个与剪裁层级有关的结构挂钩。
1. 将相机相关的地面高度信息与剪裁区域绑定
我们原来只计算一次 camera_relative_ground_p
,但现在我们希望每个剪裁层(或者叫“层级”)各自有自己版本的这个值。为此我们计划:
- 不再在每个对象(实体)层面上计算
camera_relative_ground_p
; - 而是每一个剪裁层都单独计算一次;
- 每个实体通过其所在的剪裁层使用那一层的对应信息。
2. ClipRect 附带效果信息结构
为了支持每层独立处理混合(如透明度或颜色叠加)相关的数据,我们考虑添加一个与 ClipRect 关联的“效果信息”结构,临时命名为 ClipRectEffects
或 ClipRectBlending
:
- 其中可以包含如
blend_color
和tint_color
等字段; - 对于没有混合需求的层,可设置为默认值或空值;
PushClipRect
操作将额外接受一个参数来绑定这个特效结构。
这样我们就能够把透明渐变、颜色调整这些逻辑封装在特定剪裁区域内部,而不是全局使用一个颜色。
3. ClipRectEffects 的应用逻辑
一旦 ClipRectEffects
结构存在并挂接:
- 每次渲染时,系统通过当前剪裁区域索引获取对应效果;
- 渲染管线可以按需使用其颜色信息、透明度等;
- 避免出现多个实体在不同高度层次共享错误颜色状态的问题。
4. 按 Z 值分配剪裁层级
为了实现上述目标,我们还必须能将任意一个实体的 Z 坐标正确地映射到其所在的剪裁层。这需要一个严格的、可预测的分层逻辑:
- 例如定义层级间距为固定值
Z_SLICE_STEP
; - 使用
(z - z_min) / Z_SLICE_STEP
之类的方式将Z
值转为level_index
; - 显然这个过程要求我们在渲染开始前明确定义所有层级的 Z 分布范围(即
z_min
,z_max
); - 该映射逻辑需要避免“误差临界点”问题,例如 Z 处于两层之间时如何归属等。
5. 当前测试验证
我们当前已经注释掉 camera_relative_ground_p
的具体赋值代码,观察整个系统运行是否会出错。测试结果表明:
- 所有内容都走了默认剪裁路径;
- 因此当前所有实体都共享了默认的渲染层,不再区分透明层、渐隐层等;
- 渐隐与 Alpha 混合效果也失效,这再次印证了我们需要将相关逻辑与剪裁层级解耦。
6. 后续工作规划
为了构建更合理的多层渲染系统,下一步将包括:
- 建立
ClipRectEffects
并将其集成到剪裁系统中; - 明确每层剪裁区域所对应的 Z 层级及其计算方式;
- 实现将实体准确归类到对应剪裁层(即层级渲染桶);
- 完善 ClipRect 分层渲染效果,实现每层独立控制渲染样式(例如雾化、透明、颜色等)。
总结
我们已经明确了剪裁区域不仅要管理渲染区域,还要承载对应层级的视觉特效信息(如颜色、透明度)。通过构建 ClipRectEffects
结构,并将 camera_relative_ground_p
等数据与剪裁层级挂钩,我们将实现一个真正分层、可控、可混合的渲染系统。下一阶段的关键是实现实体 Z 值与层级索引的精确映射,从而使所有渲染逻辑按层生效。
可以看到没有透明的alph
game_entity.cpp:在渲染之前立即在 UpdateAndRenderEntities 中设置 CameraRelativeGroundP
我们知道每个实体的 Z 值是相对于摄像机的位置的,因此可以继续使用这个信息来进行剪裁层级的划分(也就是将实体归入某个渲染桶中),虽然我们之前在上层计算过这个值,但那不是正确的位置。现在我们可以修复之前的一个 Bug——正确地在实体即将渲染前,利用变换完成后的坐标来做分层处理。
1. Z 层级划分逻辑放到渲染阶段
- 每个实体在准备渲染时,已经拥有了变换后的世界坐标;
- 这时候通过
camera_relative_ground_p.z
可以获取相对于摄像机的 Z 坐标; - 根据这个值,我们可以将实体归入合适的 Z 层级桶中(ClipRect Index);
- 所以此处我们应实现一个函数或逻辑,诸如
GetClipRectIndexFromRelativeZ()
,用来将 Z 值转成整数层级索引,例如-4
到1
这样的数值; - 使用该索引判断当前实体是否在有效可见范围内,如果不在,直接跳过该实体的渲染处理,避免浪费 GPU 或 CPU 资源。
2. 避免无效的实体进入渲染流程
- 若实体所在的层级(ClipRectIndex)不在设定的可见层级范围
min_clip_rect_index ~ max_clip_rect_index
内,就完全跳过渲染; - 这样可以避免不必要地去查找图像资源(Bitmaps)、做渲染准备等;
- 相当于实现了一个粗粒度的剔除系统,提高了整体效率。
3. 现有逻辑中已有的位移计算可以复用
- 当前已经在代码中计算了
offset_p
,它本身就是实体位置相对于摄像机的位置; - 因此不需要再次计算
camera_relative_ground_p.z
,而是可以直接利用offset_p.z
来完成层级判断; - 这样可以避免重复计算,提升效率。
4. Z 层级划分核心逻辑
最终的判断核心类似如下:
relative_layer_index = GetLayerIndexFromZ(offset_p.z);if (relative_layer_index < min_layer || relative_layer_index > max_layer) {// 跳过不在可视范围内的实体continue;
}
其中 GetLayerIndexFromZ()
是一个需要实现的函数,它把相对 Z 坐标映射为一个整数层级值。比如可能通过某种步进值(如每层 1.0 或自定义步长)来进行划分。
5. 命名的一致性问题
当前我们在部分地方使用了“layer”,而在其他地方使用了“level”,需要统一命名,避免混淆。这在系统逻辑上非常重要,因为同一变量的不同命名容易导致逻辑出错。
6. 情绪性反思与节奏把控
在处理中作者表达出了一种“虽然工作繁忙但编程本身还是很享受”的情绪,只是担心不能把事情做对、做完整。说明当前技术问题本身不难,关键在于工程节奏与系统架构的合理性。
总结
我们已经建立起一个合理的做法来根据实体的相对 Z 值将其分配到正确的剪裁层中。这个过程将在实体即将被渲染前进行,避免了过早或重复计算,同时也通过层级过滤有效剔除了不需渲染的实体。下一步就是实现 GetLayerIndexFromZ()
函数,并统一命名风格,进一步清理逻辑和数据结构,使得整个渲染层级系统更加稳定、清晰、高效。
game_entity.cpp:在 UpdateAndRenderEntities 中从 OffsetP.z 中提取 RelativeLayer
我们现在明确了一个关键点:offset_p.z
是我们真正需要的信息,但它必须被拆解为两个部分,因为我们正打算将 Z 的概念一分为二,以满足更合理的渲染和图层处理。
一、Z 的双重职责:分片与位移
Z 值现在需要承担两种不同的角色:
-
确定实体属于哪个“层片(slice)”
- 这个层片决定了实体在哪一个剪裁区(ClipRect);
- 它会影响透视缩放等渲染效果;
- 是渲染时确定缩放、裁剪、Z-Sorting 的关键。
-
确定在该层片中的相对 Z 偏移
- 这个值只用于 Y 方向的视觉位移,不参与透视缩放或剪裁;
- 可用于制造深度层次感,例如轻微的上下浮动、堆叠效果;
- 主要影响实体在画面中上下移动的具体位置。
这两个信息必须独立表示,因为它们逻辑功能不同。
二、实体变换流程的改造
为了支持上述逻辑,我们将在实体的变换逻辑中(如 EntityTransformOffsetP
)做如下改造:
-
输入:一个 Z 值;
-
输出:两个信息:
RelativeLayerIndex
:该实体应该归入的层片;ModifiedZ
:Z 值去除层片信息后剩余的偏移量;
这可通过一个新的函数来实现,例如:
int ConvertToLayerRelativeZ(float &z)
{// 假设层间距是固定的,例如 1.0int layerIndex = floor(z / LayerHeight);z = z - layerIndex * LayerHeight; // 提取出偏移return layerIndex;
}
调用时原始 Z 会被替换为偏移量,同时我们获得了层片索引。
三、Z 处理逻辑的集成应用
-
绘制前将
offset_p.z
拆解成两部分:- 得到用于
ClipRect
索引的层片编号; - 得到用于位置位移的内部偏移;
- 得到用于
-
选择剪裁区域(ClipRect):
- 将相对层片索引
RelativeLayer
减去最小层片索引MinLayer
; - 得到当前实体所归属的剪裁数组索引;
- 从
ClipRects[RelativeLayer - MinLayer]
中读取对应信息;
- 将相对层片索引
-
避免非可视层渲染:
- 如果实体所在层片超出了预设范围,跳过渲染;
- 节省资源,提升效率。
四、对当前系统架构的优化
这套结构是建立在“假 3D / 伪 Z 深度”系统上的,是对传统 2D 渲染系统中进行层次模拟的一种策略:
- Z 分片用于整体缩放(伪 3D 层次);
- 片内 Z 偏移用于垂直位移(模拟堆叠);
- 这种处理方式在不引入真正 3D 几何体的前提下,兼顾了视觉层次感与性能;
- 通过将复杂度抽象到分片+偏移,提升了系统的表达力和控制力。
五、总结
我们最终形成了一种结构清晰、职责分明的 Z 值处理机制:
- 利用
offset_p.z
拆出 层级索引(用于剪裁、排序) 和 片内位移(用于绘制位置); - 层级索引用于从剪裁区数组中选择正确区域;
- 位移值用于偏移实体在画面中的最终渲染位置;
- 这种结构让我们在保持 2D 图像基础的同时,获得类似 3D 场景的层级感与管理方式;
- 系统架构也因此更可扩展、更高效、更具组织性。
最终,只需要实现一个“Z 拆解函数”,就可以在多个地方共享使用这一机制,极大简化了层次处理逻辑。
game_entity.cpp:让 UpdateAndRenderEntities 根据 Origin 设置 CameraRelativeGroundZ
我们现在要处理的核心问题是 Z 值层片的离散映射 ——这是一个涉及离散数学的环节,容易出错,需要精确计算而不是“差不多就行”。
一、目标:构建相对层级索引
我们要做的事情是在每一帧渲染时,将一个实体的 Z 值转换为它在屏幕上的相对层片索引(Relative Layer Index),以便用于图层排序和裁剪区域选择。这涉及以下几部分计算:
二、相对层片计算的依据:摄像机位置与地面偏移
1. 摄像机的世界空间位置
摄像机在世界空间的绝对位置是关键,它包含两部分:
AbsoluteTileZ
:摄像机所在的整块地图“楼层”;OffsetZ
:摄像机相对该楼层的偏移量;
这意味着摄像机在世界空间的精确 Z 位置是:
CameraZ = AbsoluteTileZ × TileHeight + OffsetZ
2. 如何理解“地面是零层”?
我们约定**地面层(floor index = 0)**的参考点是摄像机正好处于整层(offset 为 0)的位置,因此:
CameraRelativeGroundZ = -CameraOffsetZ
也就是说:
- 如果摄像机向上偏移,我们就认为地面“低”了;
- 如果摄像机向下偏移,地面就“高”了。
三、Z 值与层级索引的转换逻辑
假设我们有一组分片,例如从层级 -4 到 +1,总共 6 个分片。我们想要知道一个实体的 Z 值落入哪一个分片,做法如下:
1. 得到相对于摄像机的实体 Z:
RelativeZ = EntityZ - CameraZ
2. 将该值除以“典型楼层高度”,向下取整得到所在层片索引:
LayerIndex = floor(RelativeZ / TypicalFloorHeight)
3. 转换为数组中的有效索引(从 0 开始):
ClipRectIndex = LayerIndex - MinLayerIndex
例如,如果分片范围是从 -4 到 +1,那么:
- MinLayerIndex = -4;
- 若 LayerIndex = -3,则 ClipRectIndex = 1;
这样就能正确映射到 ClipRects[1]
。
四、Z 值剩余偏移(片内位移)
在将 Z 分成片后,还需要计算实体在当前片内的剩余偏移,作为最终 Y 坐标修正:
ZRemainder = RelativeZ % TypicalFloorHeight
这个剩余值就是我们用于模拟“在当前层中上下浮动”的 Y 偏移量,视觉效果上不会引起缩放或透视变化。
五、完整的分层计算过程
以下是整个计算过程的伪代码结构:
// 输入:实体的世界空间 Z 值,摄像机位置
float relativeZ = entityZ - cameraZ;// 计算所在的层级
int layerIndex = floor(relativeZ / typicalFloorHeight);// 获取片内偏移
float zRemainder = relativeZ - (layerIndex * typicalFloorHeight);// 映射为 ClipRect 数组索引
int clipRectIndex = layerIndex - minLayerIndex;
六、总结
这一部分的实现关键在于:
- 用摄像机的偏移作为基准零点,建立相对 Z;
- 将相对 Z 按照典型楼层高度划分为离散层片;
- 提取剩余偏移用于片内位置位移;
- 通过
MinLayerIndex
将层片索引转换为数组索引。
这个机制为我们建立了一个稳定、可控、易于管理的伪 3D 层级系统,为后续所有的剪裁、排序、雾化等效果提供了基础。
game_entity.cpp:引入 ConvertToLayerRelative
我们在实现 convert_to_layer_relative
时,目标是将 Z 值进行分片归类,和之前构建的分层逻辑保持完全一致,也就是将某个相对于摄像机的 Z 值正确归入一个离散层级中,并得到在该层级内部的相对偏移。
一、问题背景与思路重申
我们要把实体的 Z 值映射到正确的层(slice)里,然后获得:
- 该层的索引(例如相对于摄像机是第几层);
- 该实体在这个层级内部的相对偏移。
此过程本质上就是对空间坐标进行一个“离散化 + 残余偏移”的处理,这个逻辑在我们已有的代码中已经实现过,比如 MapIntoChunkSpace
或 RecanonicalizeCoordinate
,这两个函数可以直接拿来用。
二、可重用的已有函数
我们意识到,在世界空间转块坐标(chunk space)的时候,其实已经有类似的操作:
-
MapIntoChunkSpace
本质上会调用RecanonicalizeCoordinate
; -
这些函数会将一个绝对坐标分解为:
- 整数部分(即块索引、楼层索引);
- 小数部分(即块内偏移、片内偏移);
我们可以直接调用 RecanonicalizeCoordinate
来完成我们 Z 值的分层处理。
三、实际操作步骤
1. 确定 Z 轴上的块大小
我们约定楼层的高度(即分片间距)是一个固定值,例如:
#define FLOOR_HEIGHT 3.0f
这个值就是我们离散切片的基础。
2. 对 Z 值调用 RecanonicalizeCoordinate
我们以摄像机的世界空间位置为基准点,把当前实体的 Z 位置(相对于摄像机)进行“重正化”,如下:
// 输入:实体的相对 Z 值(例如 worldZ - cameraZ)
float relativeZ = entityZ - cameraZ;// 重正化:获得“Z层索引”和“层内偏移”
int layerIndex;
float zOffsetInLayer;RecanonicalizeCoordinate(relativeZ, FLOOR_HEIGHT, &layerIndex, &zOffsetInLayer);
在这个函数中,layerIndex
表示实体在第几层,而 zOffsetInLayer
表示在该层内的偏移量。
四、几点需要注意的细节处理
1. 地板高度常量化
TypicalFloorHeight
(常见楼层高度)不再是“建议值”或可变值,而是被固定为用于层级划分的基础单位。由于所有切片操作都依赖于这个单位,它需要被视为一个“语义常量”。
2. 可跳跃层但不可变层高
我们可以跳过某些层(例如设置一个实体直接渲染在第 2 层),但不能任意设置每层的高度,因为这会打破整个分层的离散逻辑。
3. 相对坐标的参照点
实体的相对 Z 是以摄像机的世界位置为基础进行计算的,偏移也是相对于摄像机所在的参考地板层计算的,这一点尤为重要。
五、最终行为实现总结
通过对 Z 值调用已有的 RecanonicalizeCoordinate
:
- 我们获得一个离散的层片索引(相对摄像机的第几层);
- 同时也获得了当前片内的浮动偏移值,用于位移计算;
- 最终可以映射到
ClipRects[]
中的具体索引,用于裁剪和渲染处理;
我们在逻辑上构建了一个离散片层系统,它依赖:
- 固定的楼层高度(统一分片单位);
- 摄像机世界位置作为参照;
- 通过函数复用减少重复逻辑,保证一致性。
这样,我们的层级系统就具备了严谨、统一且可维护的基础,可以在渲染、混合、排序等方面展开更复杂的处理。
game_render_group.h:引入 clip_rect_fx
我们现在继续完成另外一部分工作,那就是给每个渲染组提供一个非常重要的信息:clip_rect_fx。
一、clip_rect_fx 的引入
我们在设置 RenderGroup
的时候,除了之前已经有的 RenderOntoClipRect
之外,现在引入了 clip_rect_fx
结构。这其实是将原来存在 RenderGroup
中的一些全局状态信息“剥离”出来,变成每个裁剪区域(clip rekt)自己独有的状态数据。
换句话说,原先所有渲染实体都共享的一些渲染效果参数(例如混合模式、模糊等),现在变成了跟随每一个裁剪区域单独设定的私有参数。
二、对渲染器的影响
这样的变化对渲染器造成了一定的影响,需要做一些额外的处理。这是因为:
- 原先渲染器只需要处理一组全局的状态;
- 现在每一个
ClipRekt
都要在渲染末尾合并上自己的clip_rect_fx
; - 渲染器逻辑必须在处理每个
ClipRekt
的时候,将这些新的效果状态考虑进去。
也就是说,clip_rect_fx 必须在渲染流程结束阶段手动合并,不能依赖之前的全局状态机制。
三、引入 clip_rect_fx 的目的和好处
虽然这会导致渲染器实现上多一些复杂度,但它的好处也非常明显:
-
更细粒度的控制
每个裁剪区可以拥有独立的渲染效果,避免了不同对象间共享状态引发的问题。 -
未来拓展性更强
如果我们未来要在不同层做不同的后期处理(例如模糊、颜色偏移、变形),这种机制可以直接支持,而不需要全局切换状态。 -
更高的渲染效率
在剔除(culling)和分层渲染时可以只对实际影响的区域进行处理,不用渲染无效区域。
四、渲染器接下来的工作
为了适应这套机制,我们的渲染器接下来要做以下几件事:
- 在每个 clip rekt 渲染完成后,读取其对应的
clip_rect_fx
; - 将这些效果参数应用到当前的帧缓冲或渲染目标中;
- 清除不必要的全局状态依赖,确保每个 clip rekt 的状态互不干扰;
- 结构上更清晰地标明“在哪个裁剪区域、使用了哪些渲染效果”。
五、小结
我们将一些全局性的渲染状态移到了 ClipRekt
局部中作为 clip_rect_fx
,这样做的主要目的在于提升渲染系统的模块化、独立性和可控性。虽然这对渲染器结构提出了更高的要求,但也为未来的图像效果处理和扩展提供了更强的灵活性与清晰的架构基础。
考虑将系统减少到两个切片
我们在处理颜色相关的数据时,最初是将颜色信息存储起来以便后续使用,比如用于一些效果处理(比如alpha渐隐)。但现在发现,这个流程需要调整:
- 颜色数据不能直接在一开始就应用,而是在实际渲染时,根据对应的裁剪区域(clip rect)去应用这些颜色效果。
- 这样做是为了避免提前应用带来的效率问题,尤其是对于需要分开处理的效果,比如alpha渐隐效果。这个效果通常需要单独渲染成一个独立的图层,之后再进行混合,而不是和实体对象一块渲染。
针对雾效(fog)处理:
- 雾效可以全部放在同一个裁剪区域内,不需要过度拆分层级。
- 只需要几个层级,通常是当前层、上一层和远处未渐隐层,基本满足需求。
- 把雾效存储在每个层的独立位置只是为了实验和保证操作的准确性,实际应用中可以简化。
关于Z偏移和切片(slice)分配:
- 必须保证对象根据Z偏移正确分配到对应的切片,这样才能保证透视效果的正确实现。
- 第一次遍历所有对象时,先给每个对象正确分配切片,保证后续渲染中颜色和效果应用的准确性。
- 后续可以减少切片数目,比如只保留alpha渐隐切片和其他普通切片,因为多数情况下只需要这些切片来处理透视和效果。
- 不一定需要在渲染时再等待切片的分配,可以提前完成。
对于全局颜色状态的处理:
- 仍然希望能支持按层存储颜色数据,以便不同对象能拥有不同的雾效强度,保持灵活性。
- 但这可能不是处理雾效最有效的方式,也许基于Z深度的雾效会更合理和高效。
- 颜色存储逻辑需要足够灵活,能够支持存放各种效果(特别是雾效)相关的数据。
总结:
- 颜色效果的应用时机从“提前”改成“渲染时按裁剪区域应用”,避免效率和逻辑上的问题。
- 雾效不需要复杂拆分层级,可以简化处理。
- Z偏移和切片分配必须准确,确保透视正确。
- 保持颜色存储的灵活性,既支持雾效也为未来扩展做好准备。
- 目前保留部分代码用于过渡,未来可能会简化或者去掉部分冗余逻辑。
game_render_group.cpp:让 PushClipRect 接受一个 clip_rect_fx 参数
我们还没有修改相关代码来支持传入剪裁区域效果(clip rect effects)参数。
- 计划是让“push clip rect”这个函数能够接受一个指向剪裁区域效果的指针。
- 在调用“push”时,如果没有效果,则传入0;有效果时则传入对应的效果数据指针。
- 这样可以方便地将效果信息与剪裁区域绑定,保证渲染时能正确应用这些效果。
另外,“convert to layer relative”函数需要增加一个参数,表示当前的世界模式(world mode),以便根据世界坐标正确转换层级索引。
总结:
- 增加剪裁区域效果的传递和管理机制。
- 优化函数接口,支持世界模式参数,提高坐标转换的准确性。
- 这些改动是为了让渲染流程更加灵活,能够正确处理不同效果与层级关系。
段错误
运行游戏,发现切片没有进行透视变换
目前系统的绘制表现虽然不是错误的,但存在预期中的问题:我们确实按照设定将索引取整(floor)来划分层级,但由于当前尚未加入透视变换的处理,因此每一层所代表的“切片”并没有呈现出应有的透视效果。
详细总结如下:
- 当前系统通过对 Z 值取整来进行切片分层,这部分逻辑是按照设想工作的。
- 然而,我们并没有在渲染过程中引入任何透视变换的计算或应用,因此虽然物体被划分到了不同的层,但它们并没有产生纵深(Z 轴)上的透视缩放效果。
- 换句话说,这些切片只是简单地被平铺在屏幕上,没有体现出距离相机远近时应有的大小差异或视觉上的压缩效果。
- 这是因为用于生成透视效果的变换矩阵或缩放逻辑尚未添加到渲染路径中。
- 从当前的测试或初步渲染结果来看,虽然系统已经正确地将物体按照层级划分,但视觉效果上依然是“正交的”,缺乏立体感。
最终结论:
我们当前的层级分配逻辑已经具备功能,但需要尽快补上透视变换的部分,确保切片在 Z 方向上的视觉表现符合预期,从而完成“伪3D”的最终目标。
问答环节
昨天你在切换软件和硬件渲染器时,为什么看起来颜色还是有些微差别?
我们注意到在软件渲染器和硬件渲染器(OpenGL)之间切换时,颜色表现上存在非常细微的差异。我们虽然没有专门对这一差异进行深入调查,但已经预期到会出现这种现象。以下是我们目前的理解和分析:
-
颜色差异的根本原因在于我们使用的是一种近似的伽马校正方式。我们在软件渲染器中对颜色进行处理时,采用的是简单的平方与开平方方法,相当于使用了γ=2.0的曲线进行校正。
-
相比之下,OpenGL(硬件渲染器)采用的是更加精确的伽马校正,通常是符合标准的γ≈2.2曲线,或者更接近标准的真实色彩校正。
-
由于这两种伽马值之间存在差异,我们就会观察到轻微的颜色偏差。这种偏差在视觉上是可以察觉的,但通常不是非常明显,不会造成“严重”的色差。
-
我们在调试时曾遇到过一次非常明显的颜色错误,但那次并不是由于伽马差异导致的,而是另有真正的 Bug 存在。我们确认这一点是因为γ=2.0和γ=2.2之间的差异不会造成那么剧烈的视觉效果。所以,我们继续排查,最终找到了实际问题所在。
-
尽管现在的伽马差异会带来细微的色彩不一致,但这是预期之内的现象,属于合理范畴。未来如果有需要,也可以通过更准确的γ曲线模拟来消除这个偏差,但目前没有必要。
总结:
- 软件渲染器使用γ=2.0近似计算;
- 硬件渲染器使用更精确的γ≈2.2;
- 轻微的颜色差异是正常、可预期的;
- 显著的色差说明存在其他 Bug,已被查出并修复;
- 当前的色彩表现被认为是可接受的,不构成问题。