游戏引擎学习第282天:Z轴移动与摄像机运动

运行游戏,展示目前进展

我们目前正在进行一个游戏开发项目。昨天,我们实现了基于房间的角色移动系统,并且加入了摄像机的跟随滚动功能。这是我们首次进入"游戏逻辑设计"阶段,也就是说,我们开始构建游戏本身的行为和玩法,而不仅仅是底层引擎的开发。

我们当前的任务是整理并清理已有的代码,使结构更清晰,并为下一步开发奠定基础。现在,角色已经可以在房间之间移动,这种移动方式非常流畅且快速,对于一款节奏明快的动作类游戏而言,这种迅速的转换可能是合适的。尽管如此,我们之后仍然可以根据需要调整移动的节奏。

接下来,我们的目标是进一步完善结构化房间布局:不仅实现水平相邻的房间,还要支持垂直堆叠的房间。我们希望让角色可以从高处跳下,进入另一个垂直空间,就像是穿越到下一层。我们曾经用平滑坡道模拟过这种高低差移动,现在希望能更正式地实现成"跳跃到新高度层级"的形式。这种结构在一些游戏(例如《以撒的结合》)中是单向的(只能向下),而我们希望实现更复杂的双向跳跃与层级间穿越。

关于碰撞系统,我们仍在探索阶段。当前的实现只是粗略地处理实体的放置,并未进行任何真正的实体打包处理。我们会在后续具体开发中,再决定碰撞逻辑的实现方式。为了避免过度设计,我们选择保留简单的实现,等到需要时再扩展功能。

此外,为了增加地形的变化感,我们开始尝试给房间中的可通行点添加高度的随机偏移。例如,在创建房间时,我们可以给地板元素增加一个随机的 z 轴高度变化,使得游戏世界看起来更加自然。实现方式是将随机偏移量传入实体的放置位置,并通过 entity offset 设置其在世界中的位置偏移。我们一度错误地将这个偏移应用在墙体而非地板上,导致测试结果不准确。在修复这个错误之后,我们能够正确观察到这些高度偏移生效,并在渲染排序上也能正确体现。

我们计划将这个高度偏移的功能整合进标准房间的生成中。目前正在调整代码,把高度偏移逻辑从墙体中移除,转而应用在地板元素上。初步测试结果表明系统是可行的,只是之前查看错了变量,导致一度误以为无效。

我们接下来的重点将是构建更完整的三维房间系统,支持房间的垂直堆叠、上下跳跃,并完善物理碰撞和视觉渲染效果,为后续的玩法机制提供坚实的基础。

修改 game_world_mode.cpp:随机生成可通行点的高度

我们探讨了在场景中加入Z轴高度变化的可行性,目的是让场地元素的高度具备一定的随机性,从而带来更自然的视觉效果。我们尝试通过在添加场景元素(如墙或地板)时,赋予其一个小幅度的随机Z轴偏移来实现这个目标。具体过程如下:

我们在添加元素时使用了一个add_wall_abs的函数,它接受坐标参数(x, y, z),我们发现可以给这些参数添加一定的随机值,例如在±0.25米之间生成的随机数,作为Z轴的微小扰动。

为实现这个想法,我们检查了当前的随机数生成机制,发现系统中有一个名为act_series的随机数序列,并考虑使用它来生成偏移值。然后尝试在添加元素时传入这些随机偏移,使得它们的位置在Z轴上产生微小差异。

随后,我们注意到在添加这些实体到世界中的过程中,可以为实体设置一个偏移(offset),这个偏移值在加入世界时会被考虑进去。特别地,这个偏移可以包括Z轴分量,从而达到我们想要的高度错落效果。

接着我们进行了验证,发现在视觉排序上,这些实体确实表现出像是考虑了Z偏移的样子,但在实际渲染时并没有真的按Z轴偏移显示出来,这引起了一些困惑。这可能是渲染或实体处理中的一个bug,我们打算进一步调查。

为了验证这个问题,我们将偏移量加大到1米,使效果更明显,并通过这种极端值来判断是否Z轴偏移真的被应用到了可视化表现中。

然而,在测试过程中出现了一个错误:我们不小心把偏移应用到了墙体而不是地面上,这并不是预期的目标。意识到这个错误后,我们立刻进行了修正,将随机偏移正确地应用到地板上,同时从墙体的代码中移除这些干扰性的改动。

此外,为了让偏移控制更加系统化,我们修改了创建房间时使用的函数add_standard_room,传入当前使用的随机数序列series,确保在生成房间内容时可以使用统一的随机性参数。

总的来说,这一过程探索了如何将Z轴的随机扰动整合进当前的世界构建系统中,识别了潜在的渲染问题,修复了应用目标错误,并进一步理清了偏移应用机制,确保视觉排序与物理表现的一致性,为今后更复杂的空间构建与高度机制奠定了良好基础。

运行游戏,穿越房间

目前已经成功实现了物体在Z轴上的偏移,这是我们预期中的效果。虽然这些物体被偏移了,但角色依然可以顺利跳跃其上,基本的交互依旧保持正常。

在测试过程中注意到一个很有趣的现象:地板具有碰撞体积,这意味着角色不能直接跳进一些被偏移的位置,必须绕过某些区域后再跳上去。这正是我们想要验证的核心点------Z轴的高度变化对交互和移动逻辑有真实影响。

接下来我们要更深入地考虑这些元素的Z轴坐标,并开始对其进行更加系统化、规范化的处理。目前在渲染排序方面仍存在一些问题,比如当某些对象处于较高位置时,可能会出现排序错误的现象(曾经为角色渲染处理过类似的问题)。因此,我们有必要将所有与Z轴相关的逻辑统一管理,不再采取零散、临时的处理方式。

重点在于:一旦进入正式开发阶段,就会有大量功能代码依赖这些基础系统。如果这些系统设计得不严谨,后续开发中就会一直被这些糟糕的基础拖累。所以我们应当在此阶段明确解决方案,把显而易见的问题优先修复,减少潜在的技术债。

目前看起来整体效果很好,跳跃逻辑非常稳定,程序化动画的优势也开始显现------比如它不像传统动画那样死板,总能根据环境自动适应,无需担心脚滑或接触点偏差等问题。

还观察到了一个很有意思的现象:当我们角色跳得够高,甚至能触发相机向上切换到更高楼层的视角,这是出乎意料但很酷的效果。

不过,这也引发了一个新的好奇点:是否可以人为地增加偏移,让角色"更容易"进入下一层楼?想测试一下如果始终向上偏移一点会发生什么。

之后继续检查了相机与Z轴的关系,发现我们当前好像没有明确检查Z轴高度与相机层级之间的对应逻辑。进一步排查中发现,相机的Z轴跟随是直接设置为角色所处chunk的Z值的,这种做法显然太粗糙,不利于实现平滑过渡。

所以接下来计划的改进方向是:将Z轴的位移与相机切换机制之间建立更细致、平滑的逻辑,使得在多楼层、复杂地形场景中,Z轴处理既能保持交互准确性,也能确保视觉体验一致、顺畅。目标是系统化、模块化这些机制,避免到处都是临时补丁代码,从源头上减少维护负担。

修改 game_sim_region.cpp:添加摄像机高度插值功能

目前的系统中,Z轴的设置是直接硬编码到chunk的Z值,这种做法已经不再适用,需要被替换。我们真正想要的是一种更灵活的系统,能够根据高度差进行跳跃、移动,并且支持在这些高度之间实现平滑的插值过渡。

我们开始查看这套机制的实际运行方式。首先想确认的是在world代码中,每个tile是如何定义的。我们使用的是chunk维度,允许整个世界空间以完整的长方体(矩形立方体)形式构建。

初始化world时,传入了这些维度的参数。比如我们设定了每个tile的边长,以及整个chunk的范围,像是"tile边长的两倍"这样的配置。这个设置在我们实现地面缓冲区系统时曾发挥过重要作用,但现在已经变得无关紧要,因为我们不再依赖地面缓冲来对齐任何东西。

所以,world的维度也需要调整。其中"典型楼层高度"设置为三米,这是个重要值,因为它将用于控制摄像机在Z轴方向上的移动步长。

因此,我们的目标是把这层逻辑统一起来。我们会将"楼层高度"视为一个通用的"房间间距",即每次Z轴变动时,角色或摄像机都将按这个固定步长移动。我们可以在world的配置中直接读取这个值,并将它作为room delta(房间间距)进行处理。

此外,和apron(可能是边缘区域的高度处理)相关的逻辑也基本可以照搬,不需要太多修改。我们计划将这些逻辑进行统一,保持一致性。

然后我们进入代码部分。当前这段代码结构非常相似,冗余度高,因此有必要做一次重构,把它简化。我们打算用一个更通用的循环来替代重复的X、Y、Z分别处理的代码块。

举例来说,我们不再使用 .x, .y, .z 这样的字段访问方式,而是通过循环处理每一个维度,令代码逻辑统一且易于维护。

我们还注意到,在做Z轴跳跃处理(例如"上升一个房间"或"下降一个房间")时,这一套逻辑也可以被映射到chunk空间内,因此无论是地图更新,还是视觉表现,都是一种结构化、可拓展的处理方法。

总的来说,我们正在把所有与房间跳跃、高度偏移、相机运动等相关的Z轴处理逻辑规范化,并准备进行代码结构的优化,减少重复,提升清晰度与维护性。这将为后续功能开发打下更稳固的基础。

我们在使用Tudor编辑器的过程中,发现一个奇怪的现象:有时候在打字时,输入左中括号 [ 后会自动插入一个空格,而且这个行为似乎不是持续发生,而是偶尔出现,难以稳定复现。初步怀疑可能是我们自定义配置文件中的某项设置导致的,也有可能是多个设置项共同作用的结果。目前还没能找到明确的原因,因为还没时间深入排查相关配置。

暂时先搁置这个问题,我们继续回到Z轴运动的实现部分。

我们为地图和chunk空间引入了新的相机机制,并将Z方向的delta应用到相机上,这样做的好处是可以使相机在垂直方向上也能平滑跟随角色或变化。经过检查,这部分的逻辑是完整并且连贯的,可以认为该部分问题已经解决。

接下来要处理的是Z轴上具体行为的细节。我们在某个逻辑分支中尝试引入Z值变动,但要注意的是,像"反弹"(bounce)这一类逻辑在Z方向是不能成立的------因为弹跳逻辑通常是为了阻止继续朝某个方向移动,但如果我们正打算朝Z方向"上升"或"下降",那种阻止就没有意义了。因此这一部分逻辑需要做特殊处理或被剔除。

虽然如此,我们仍然可以把这段代码结构做一些简化和统一处理,不必重复写多个条件分支,这不是大问题,只需要后续注意调整即可。

我们接着进行编译,现在代码已经合并好,并且Z轴的运动逻辑也已经加入了。如果想实际测试跳跃到更高层级的功能,我们还需要构造一个"坡道"或者高度逐渐上升的区域,这样才能看到角色从一个平台跳到另一个高层平台时的真实效果。

这个测试将帮助我们确认相机追踪、Z值插值、跳跃逻辑是否真正协调一致。整体来看,目前的修改已为进一步完善Z轴高度系统打下良好基础。

修改 game_sim_region.cpp:让 AddStandardRoom 生成斜坡

我们决定在标准房间中直接构建一个斜坡结构,来更直观地测试Z轴高度变化的表现。具体实现上,我们不再使用之前的随机Z轴偏移逻辑,而是依据房间内的位置坐标(x和y)来生成规律性的Z偏移,从而让整个房间呈现出"斜坡"或"地形起伏"的状态。

我们在添加标准房间时引入了如下逻辑:

  • 如果某个元素的 offset_x 大于零,那么它的 z 值会被提升;
  • 同理,如果 offset_y 大于零,我们也会提升它的 z 值;
  • 这样从左下角到右上角,Z高度会逐渐升高,形成一个斜向上的坡面;
  • 最终我们将 z 的偏移值设置为 offset_x + offset_y,简单明了地构成一个"对角线"方向上逐渐升高的地形。

在实现过程中遇到一个细节问题:代码的静态分析器在评估这个 offset_x + offset_y 的值时,发出类型转换的警告。虽然我们知道这个值始终处于$$-12, 12]的范围之内,并不会超过浮点数表示的限制,但分析器似乎无法准确推导出表达式的值域,导致它错误地认为这个值"可能"无法安全转换为 real 类型。不过这并不会影响实际运行,我们直接忽略这个警告。

完成这些设定之后,我们执行代码运行整个场景。这次运行的目的是测试如下几个核心点:

  • 地图上地形高度是否按设定规律上升;
  • 角色在不同Z层之间移动时,跳跃逻辑是否正常;
  • 相机能否正确跟随高度变化并平滑过渡;
  • 高度变化是否影响到碰撞检测和交互行为。

通过这种统一规律的"坡道式"结构,便于我们清晰观察系统在Z轴方向的表现效果,有助于发现问题和调整细节。整体来说,这是为Z轴支持系统打基础和进行验证的关键步骤。

运行游戏,尝试跳上斜坡

我们开始测试角色是否可以顺利地沿着创建的斜坡向上移动。然而,在尝试向上跳跃时,发现角色似乎无法跳到更高的位置,尽管这些位置在视觉上是可以到达的。这引发了对跳跃逻辑的深入检查。

目前的跳跃系统似乎有一个限制------角色只能跳到比"头部位置"更低或相等的目标点,如果目标位置在角色头部之上,就不会考虑跳跃过去。这种设定可能是出于防止跳跃到不可达高处的考虑,但也会导致某些应当可跳跃的点被误判为不可达。

为了验证当前逻辑,我们开始分析代码,发现跳跃并没有明确"禁止"向上跳跃的逻辑。但通过观察行为,发现系统总是选择靠近地面的目标,而不会选择更高处的区域作为跳跃目标。于是我们怀疑核心问题可能在于"寻找可跳跃目标"的算法部分。

分析 get_closest_traversable 这一方法后发现,跳跃逻辑的核心判断就埋藏在这里。这个方法负责从周围区域中找到"最近的可跳跃目标"。如果高度差异过大或者因为Z轴的影响被计算为"距离过远",那么即便理论上可以跳跃,也可能被系统忽略。

进一步推测,问题的可能原因有以下几点:

  1. 高度因素影响距离计算:Z轴高度差纳入了"最近点"计算中,导致本应被跳到的点因为"距离不够近"而被排除。
  2. 没有引入合适的跳跃高度容差:系统没有明确设定一个"最大可跳高度"的范围来允许跳跃到略高的位置。
  3. 优先选择更低的目标点:在多个可达点中,算法可能默认选择Z值更小(即更低)的区域,而忽略高处。

因此,虽然逻辑上跳跃机制没有硬性限制向上跳,但由于目标选择算法的偏差,使得向上的跳跃行为实际上无法发生。

总结来说,这一部分暴露了跳跃目标选择逻辑在面对有高度差异的环境时的不足。需要进一步完善"可跳跃判断"的标准,例如:

  • 明确设定最大允许跳跃高度;
  • 在目标选择中允许适当优先考虑更高位置(如果满足其他跳跃条件);
  • 修正Z轴高度对"最近目标"的影响方式,避免高处被误判为"距离远"。

下一步可以着手调整 get_closest_traversable 的判断规则,确保在面对有高度差的地图时,角色能够智能地跳跃到合理的目标位置。

修改 game_world_mode.cpp:将 HeadToPoint.z 设为 0

我们不确定具体原因,但可以验证一个假设:目前跳跃目标的选择是基于三维空间中角色头部与目标点之间的完整距离(包括X、Y和Z轴)。如果我们把Z轴的高度差(Z值)忽略,设为零,那么目标选择就只会基于二维平面上的距离(X和Y轴)。这样做可以测试,是否高度差导致系统总是选取更低的点,避免跳到更高的位置。通过这种方式,我们能更明确地判断当前的跳跃目标选择逻辑是否因为考虑了高度差而导致了问题。

运行游戏,发现现在可以向上跳跃了

我们发现如果忽略高度差,可以跳到其他物体上,说明之前跳不上去的原因是那些目标点离得更远。这个现象提示我们,这些物体的高度比预期的要高很多,可能代码中设置的高度值有误,导致实体的最大速度设得比应有的高。实际上,这些高度确实很高,不太合理,因此我们应该把这些高度调低一些,避免在一次跳跃中跳得过高。总体来说,这并不是一个异常问题,而是符合预期的表现,不过后续还需要继续调整和优化。

修改 game_world_mode.cpp:让 AddStandardRoom 生成更缓的斜坡

首先,我们希望调整"添加标准房间"的高度变化,不让它爬升得那么多。计划是把之前的高度调整比例降低,比如改成原来的十分之一,或者四分之一左右,这样爬升幅度不会太大。之前的高度加起来大约有十二米,这个数值太高了,显得不合理。我们觉得一米太少了,但大约五米的爬升可能更合适,因为目标是做一个斜坡效果。接下来会测试这个调整,看看能否让高度变化变得更平缓。

运行游戏,穿越更缓的斜坡

这样调整后高度变得更加合理,效果也挺不错的。整体表现符合预期,没有什么问题。现在这些部分都运行良好,可以继续去处理接下来想要做的其他工作了。

注释掉 HeadToPoint.z 的设定,再次穿越缓坡

现在我们关注的是在计算最近可跳跃点时,如何处理高度差异的问题。考虑到如果允许角色跳到距离很近但高度差很大的位置,可能会导致不合理的跳跃行为,因此想暂时去掉高度限制,看看在高度差不大的情况下,仅用二维距离判断最近点是否仍然有效。实验结果显示,在高度差不大时,使用总距离判断最近点效果还是挺好的,角色可以正常跳跃。

不过,若出现较大的跳跃高度差,这种简单的距离判断可能不够合理,需要进一步优化。此外还注意到角色有时会跳到比预期稍低的位置,这可能与摄像机代码有关,之后需要进一步修正。

总体来看,现阶段这种做法在多数情况下是可行的,但仍需留意在更复杂高度差场景下的表现,后续可能需要调整跳跃判定逻辑,以确保跳跃体验的合理性和连贯性。

修改 game_world_mode.cpp:重新计算 HeadToPoint,优先选择 XY 平面上的可通行点

现在考虑如何在计算最近点时更合理地处理高度差(z轴方向)的影响。想法是,不完全忽略z轴距离,而是对z轴距离做一定的"压缩"或"削减",以避免高度差过小就导致点被排除,也不会因为高度差过大而选错点。

具体有两种方案:

  1. 整体压缩z轴距离:把头部到目标点的z轴距离乘以一个系数,使得z轴距离变得更小,相当于在计算距离时z轴的权重降低,但这种做法会影响整个空间的判断。

  2. 局部压缩z轴距离:只针对头部到目标点的z轴距离减去一个固定值(比如1.5米或者2米),相当于忽略一定范围内的高度差。减去后,如果结果为负,则将其限制为0。这种做法更像是设定一个"允许跳跃的最大高度差门槛",只有超过这个高度差才会影响点的选择。

这样做的目的就是让二维距离的判断更优先,z轴高度差在合理范围内不会过多干扰最近点的选取,只有当高度差超过允许范围时才真正开始影响判断。整体上,这种方式可以更准确地模拟跳跃时角色对高度的判断,避免不合理的跳跃路径选择。

目前还不确定具体应该用多少作为允许跳跃的高度差门槛,可能会根据实际需求调整到楼层高度的一半或其他合适值。

运行游戏,穿越世界

我们在考虑处理跳跃时的高度差问题时,发现这确实是个比较复杂的问题,目前还不能确定哪种方案是最优的。不过可以大致看到这种做法的效果和可能出现的问题。主要的问题在于,如果允许在相对较高的平台之间跳跃,角色头部的高度(z轴)需要像其他位置一样进行插值处理,这增加了复杂度。

另外还存在一个相关问题,就是是否需要在x轴方向上也做空间扭曲(warp),还是只需要在y轴方向上处理。因为当前实现是完整的透视变换,可能会影响游戏玩法,实际上我们可能更希望x和y轴的处理更为一致和合理,避免过度扭曲。

虽然我们能在场景里自由移动角色,也能测试跳跃效果,但这也带来了开发中的一个问题------喜欢不断自己操作角色去试,反而耽误了实际的开发进度。

接下来需要优先解决的问题是z轴(高度)的处理,想要有一个统一且简洁的方案,避免系统中出现太多不同的z值定义和混乱。目标是找到一个能够一次性解决高度问题的方案,这样以后就不需要反复调整和担心。

整体来说,虽然这会是一个有些困难的过程,但为了让系统更稳定和易维护,我们决定投入时间去做这项整理和统一,争取彻底解决z轴处理的问题。

黑板笔记:关于最终 Z 轴位置的计划

我们对最终的z轴处理有了一个清晰的计划。经过各种权衡和在斜坡上的试验,我们认为实际的z轴透视变换在这种二维游戏里并不太适用。虽然上下斜坡的效果很酷,但它带来了很多问题,也让玩家难以理解画面中的透视关系,整体感觉比较混乱。

不过,我们很喜欢那种在游戏中"上升"和"下降"楼层的感觉,以及伴随的缩放过渡效果,所以我们打算采用一种明确的区分方案:分成"楼层z"和"实际z"。

其中,"实际z"将完全采用正交投影的方式处理,始终保持二维的纯粹性,而"楼层z"则代表玩家在世界中的楼层高度,是基于地图块的离散层级。玩家在不同楼层之间切换时,会触发相应的视觉变换。

在渲染时,会将z轴拆分成两个部分------"实际z"和"楼层z",两者都会存在,但"实际z"会被统一应用,不再用透视变换,而楼层z则用来区分玩家所处的楼层。实体会根据楼层z被分类到不同的"桶"中,这些桶不仅决定渲染顺序,还会和alpha渐隐效果挂钩。

alpha渐隐的作用是随着高度升高,远处的物体逐渐消失,这样可以改善视觉层次和游戏体验。目前的实现还不是特别完善,特别是对低楼层物体的渐隐效果不明显,但这是未来想要完善的部分。

总之,这个方案既保留了层级感和高度变化带来的游戏乐趣,又避免了复杂的透视变换带来的混乱,更便于维护和理解。

修改 game_world_mode.cpp:允许斜坡低于 0,并尝试穿越以观察 alpha 淡入淡出效果

我们尝试让渐隐效果(alpha fade)在高度变高和变低两个方向都生效,这样视野里的物体随着高度变化都会出现渐隐效果。然而,实际测试时发现虽然代码逻辑是双向的,但渐隐效果并没有按预期显示,感觉很奇怪。

于是我们意识到有两个任务要完成:第一是恢复渐隐功能,因为之前可能在修改代码时不小心把它删除了。渐隐是通过一个全局alpha值控制的,我们怀疑当前的问题是渐隐的起始和结束高度值设置不对。

我们检查了渐隐的起始和结束位置,发现顶部渐隐开始于典型楼层高度的五分之一处,结束于三分之四处。全局alpha值是根据这些位置计算的。

为了验证全局alpha是否正常工作,我们尝试直接设置它为0.1,结果证明全局alpha是正常起作用的。因此问题出在渐隐开始和结束的高度值设置上,可能在之前的某次调整中忘了更新这些参数,导致渐隐效果没有正确触发。

调查为什么 alpha 淡入淡出没有发生

我们在处理渐隐顶部(fade top)的时候,查看了用来计算渐隐的具体数值,发现它是基于实体在地面上的位置(ground point)相对于摄像机的位置来计算的。具体来说,是用实体的地面点减去摄像机的位置,得出一个相对高度。

这个计算看起来合理,但对于某些变量,比如"camera relative ground p z"的具体含义不太确定,特别是它对渐隐效果的实际影响不太清楚。为了更好理解,我们尝试暂时不使用这个值,而改用实体的p值来做计算,看看效果如何。

接下来,我们还注意到在计算时会对z值进行限制映射(clamp mapped to range),这意味着z值被映射到一定范围内,但这个z值可能并不是我们最初预期的那种z值。为了弄清楚摄像机的位置到底是怎样的,我们决定用调试手段一步步跟踪代码,查看摄像机的位置和相关数值的具体情况。

使用调试器:在 CameraRelativeGroupP 初始化处断点,检查变量值

我们观察到摄像机相对于实体的高度(camera relative ground p)确实低于摄像机自身的位置,而摄像机的位置是原点(0,0,0),这符合预期。地面下有一个实体,这很可能是角落的那个实体,它的高度是零,也符合预期。

虽然大部分实体的高度都是零,但我们注意到确实有些实体的高度高于摄像机,应该会产生渐隐效果。这个渐隐的开始高度(fade top start)是1.5,结束高度(fade top nz)是2.25,符合渐隐的预期范围。

通过这些信息,推测当前设置下,当摄像机移动向下时,渐隐效果的计算方式导致了某些现象。我们打算进一步研究这个过程,看看具体发生了什么,所以准备继续深入调试和观察摄像机与实体间的关系。

修改 game_world_mode.cpp:只创建一个房间

我们计划创建一个标准房间,想要将实体放置在足够高的位置,以便观察高度变化的效果。为了简化场景,我们打算减少实体数量,只生成一个房间,这样不会有太多房间干扰视线。

接着我们将摄像机位置设置在两个点之间,尝试观察高度变化。我们注意到摄像机的位置(camera p)实际上是画布起点的位置,而我们在将世界物体定位时,会对位置应用摄像机位置的负值作为偏移。虽然理论上这应该不会造成任何奇怪的误差,但实际结果让我们感到困惑。

怀疑摄像机位置(camera p)是否已经正确考虑了所有平滑偏移(smooth offset),经确认它确实包含了这些偏移,因此应该是保持一致的,不会产生错误。但依然感觉有些细节没有弄明白,不确定遗漏了什么相对明显的东西。

修改 game_world_mode.cpp:为 HeroHead 添加 CameraRelativeGroundP 的调试值(DEBUG_VALUE)

我们考虑在代码中增加一个调试值,用来专门监测主角头部的位置变化,这样有助于更清楚地观察和分析相关数据。只在主角头部时启用这个调试值,方便针对性地排查问题。

运行游戏,穿越斜坡,观察 CameraRelativeGroundP 的数值

在运行时,我们观察到摄像机与主角头部的相对高度关系。当角色向下移动时,摄像机会在一定距离内跟随角色,这符合预期。起初角色跳下去时,还未达到摄像机需要跟随的位置,但一旦达到那个临界点,摄像机就会开始拉回视角以保持跟随状态。角色再次落地后,这个跟随机制依然表现合理,整体表现较为正常和稳定。

修改 game_world_mode.cpp:给 Monstar 也添加同样的调试值

现在我们关注这里的一个对象,特别是那个"Monestar"怪物,因为场景中只有一个,所以可以明确知道它是哪一个。接着显示出该怪物的调试数值,从调试数据中可以清楚看到它的状态和相关信息。

运行游戏,注意我们没有加载上方一层的世界

问题其实不在于程序有bug,而是在于我们并没有加载玩家所在楼层上一层的世界区域,所以怪物看起来就像是消失了,实际上是因为它被卸载了。可以观察到怪物确实不见了,没被保留在内存中。显然还有其他实体被驱逐出场景,否则程序还会打印相关信息,但现在打印出来的内容基本为空,没有任何数据。

修改 game_world_mode.cpp:将 SimBoundsExpansion 增加十五层

我们需要弄清楚敌人到底发生了什么。首先,要检查一下在z轴方向上,我们希望加载的范围到底有多大。是否应该包括上一层楼的区域,答案是现在看来应该包括。于是暂时调整设置,把范围扩大到包括上面的楼层,然后看看这是否能解决敌人消失的问题。

运行游戏,发现 alpha 淡入淡出效果生效了

现在一切恢复正常,透明度渐变效果也正常工作了。问题主要是之前没有保留上一层楼的数据,导致那部分内容被清理掉了,不知道为什么之前没有这样做。接下来关注的重点是透明度渐变是否应该更早开始,我倾向于认为是的,渐变的触发时机可能需要调整得更早一些。

修改 game_world_mode.cpp:给房间增加一列树

我们讨论了树木重叠时会出现的问题,这是之前开始制作时就注意到的。为了更清楚地观察这个情况,我们决定在标准房间中添加一些元素来帮助展示。具体来说,我们计划添加一些墙壁,并通过条件判断在特定坐标位置放置这些墙壁。虽然不清楚为什么还在用这种方式处理,但还是打算在x等于2和-2,或者x等于-3的地方加墙,可能还会把它们放得更靠近一些,甚至换到另一侧。这些调整旨在更好地呈现树木重叠时的视觉效果和相关问题。

运行游戏,触发淡入淡出效果,解释多个实体同时淡出时 alpha 混合的问题

我们观察到在物体淡出时存在一个明显的问题,尤其是当多个物体重叠绘制时,透明度(alpha值)的表现变得不一致。具体来说,每个树或物体都有自己的淡出效果,当它们叠加时,重叠区域的透明度会叠加,导致视觉效果不统一,看起来像是整体透明度被降低了,这样显得很业余、不自然,也不符合最终想要的艺术效果。

理想的效果是让这些重叠的物体作为一个整体,统一以一个透明度值淡出,而不是各自独立计算透明度然后叠加。为实现这一点,渲染需要分层处理:将属于同一层的所有实体一次性渲染到一个缓冲区中,再对另一层做同样操作,最后将这些缓冲区进行合成,统一控制透明度。这意味着需要分多遍(passes)渲染,而不是逐个实体独立渲染。

如果没有这种分层渲染,只靠简单叠加,透明度的叠加效果会随着叠加层数增多而变得难以控制,尤其是当场景中存在多层需要淡入淡出的物体时,传统方法很难避免透明度表现混乱。

简单来说,解决方案是将所有同一层的物体先渲染到一个纹理或缓冲中,再整体以统一的透明度进行淡出,这样可以保证视觉上的一致性和美观。实现起来需要对渲染流程做一些调整和优化,稍微复杂但必要。

修改 game_render_group.h:考虑不同的 alpha 混合方法

我们开始考虑如何处理渲染组的问题,特别是针对透明度分层的渲染方式。现有的排序机制已经包含了对排序键(sort key)的调整,我们可以在这个基础上扩展,让渲染列表支持不同的透明度层次或"分区"(tranches)。也就是说,在排序过程中插入某种信息,指明当前渲染的对象透明度会发生变化。

目前还不确定是否需要完全分开渲染路径------比如说为不同透明度层设立独立的渲染目标(render target)------因为这样做可能带来复杂性,特别是当其他对象也需要多透明度路径时,这些对象的深度排序可能不相关,若强行合并会引发混乱。因此,我们需要一种机制,能让不同的对象根据需求分配到不同的渲染"分区"或渲染目标。

有一点比较有用的是,当前的排序过程已经支持"裁剪矩形索引"(clip rectangle index),我们可以考虑扩展它,让它不仅包含裁剪矩形信息,还能携带渲染目标的信息。这样我们就能把渲染任务分配到不同的渲染目标上,之后再把这些渲染目标上的内容按顺序合成(blit)在一起,形成最终画面。

这个方案的好处是灵活且已有部分支持,能基于现有机制进行改造;但具体实现还需进一步思考和实验。总体来说,明天的重点就是想清楚这个Z轴深度排序和多透明度层渲染的机制,争取让渲染流程更加自洽和合理。

问答环节

你说摄像机上下移动看起来很酷,希望房间的地形高度能很好地利用这个效果

新的蓝色"Uptown Camera Room"看起来非常酷,希望房间的高度设计能很好地利用这个效果。我们可以考虑让摄像机随着玩家向上移动时,画面稍微跟随上升,即使玩家没有离开当前房间,也能感觉到一定的高度变化。这种设计能让场景显得更有层次感和动态感。

同时,跳跃时的场景转换希望做得更明显,比如跳进一个洞,进入下一个房间时会有一个明显的下落过渡效果,让玩家感受到空间的垂直变化。这种大幅度的过渡有助于强化空间感和玩家体验。

除此之外,这个摄像机高度的变化机制还可以应用在其他场景或者功能上,增加更多有趣的视觉表现和交互效果。整体来说,摄像机的动态调整和房间高度的设计都还有很多发挥的空间。

你能用 GL_MIN / GL_MAX 混合方程来处理 alpha 吗?

这个问题其实本质上是和"顺序无关透明度"(Order Independent Transparency)的问题是一样的。核心难点在于,当有一个表面透过另一个表面时,颜色的混合必须考虑透过层的影响。具体来说,就是我们看到的颜色是被前面的透明物体按一定比例"染色"后的颜色,如果中间又有另一层透明物体,它们之间的叠加关系必须被正确处理。

因为这些透明物体的叠加顺序影响最终的显示结果,即使绘制顺序固定(比如从前到后或从后到前),在绘制每一层时,都需要知道当前层的颜色和透明度如何影响下面层的颜色。比如绘制背景时,背景是完全不透明的颜色,接着绘制一个半透明的树,再绘制另一个树,如何正确计算最终颜色,使得结果看起来就像是在没有中间那棵树的情况下,前后两棵树正确叠加。

想通过在 alpha 通道或者颜色通道做一些数学处理,比如用最大混合方程(max blend equation)来修正 alpha 或颜色值,试图在绘制过程中补偿已有的透明度影响,是非常复杂甚至不可能的。因为绘制时不知道之前颜色值的具体状态,无法反推出正确的颜色贡献比例,也就无法准确地从颜色中"扣除"被遮挡部分的颜色。

总结来说,这个问题是一个非常困难的图形渲染难题,需要特殊的技术(比如深度剥离、片段列表等)来解决,单纯依靠传统的混合方程和 alpha 通道是不够的,缺乏对每个像素透明层次的完整信息,导致无法正确复原颜色。

你能做一个魔兽世界游戏吗?

我们设想一个游戏,名字叫做"World of Warcraft",但不是传统的角色扮演游戏,而是一款类似"Bookworm"或者"Letter Linker"这样的文字拼字游戏。游戏中所有出现的词汇都必须来自于魔兽世界的宇宙背景。玩家通过拼写这些魔兽世界相关的词汇来击败敌人,就像用单词作武器一样战斗。这种设定非常有趣,也很有创意,玩起来肯定会很搞笑且富有挑战性。

如果实体轮廓没有抗锯齿,可以用深度缓冲区和反向渲染顺序(从前到后)来渲染淡出实体,但因为抗锯齿,alpha 混合会比较难处理

如果实体的轮廓不是透明的,理论上可以利用深度缓冲区,并反转实体的渲染顺序,从前到后进行绘制,从而解决一些渲染问题。但实际上,使用深度缓冲区配合透明度混合会比较复杂,也不太理想。

首先,我们并不想真正用深度缓冲区来做这个,主要是因为如果只是为了解决透明度混合的问题而加一个深度缓冲区,会导致内存带宽使用翻倍,既有读操作又有写操作,资源消耗非常大,而且可能没有必要。

不过,如果最终需要额外生成一个单独的颜色缓冲区,也许利用深度缓冲区反而是一个更好的选择。对于反转实体的渲染顺序,前面提到的从前到后绘制,可以借助深度缓冲区先绘制较近的实体,这样后面的实体不会覆盖前面的。

具体做法是:使用深度缓冲区后,在绘制下一层时先清理深度缓冲区,然后再继续绘制。这样在多层渲染中,可以确保每一层的透明效果和深度关系正确呈现。总的来说,这种方法是可行的,但需要额外的渲染步骤和资源。

你提到用离屏缓冲渲染然后合成来实现理想的 alpha 效果,还提到过"片段列表"这个方法,可以详细讲讲吗?

在讨论通过渲染到多个缓冲区再合成来实现理想的透明效果时,也提到了使用片段列表(fragment list)的方法。关于这种方法,其实原理相当简单,但如果不了解现代GPU的工作原理,很难想到。

现代GPU本质上是基于并行计算单元(类似CPU)的结构,但它们在编程模型上有很多限制和不便。片段列表技术就是通过为每个像素维护一个片段的链表,记录所有经过该像素的片段信息,而不是简单地一次性覆盖或混合。

这样,在最终合成时,可以根据链表中所有片段的顺序和透明度,准确计算出每个像素的最终颜色,实现真正的顺序无关透明(order-independent transparency)。这种方法克服了传统透明度混合在重叠和排序上的不足,能够更精确地表现多层透明物体的叠加效果。

不过实现起来比较复杂,需要支持链表数据结构和较多的显存访问,且对GPU资源的要求较高,因此在实际应用中不常见,但对于高质量透明效果来说是一个非常有效的方案。

黑板笔记:片段原子列表(Per-fragment atomic lists)

我们讨论了解决顺序无关透明(order-independent transparency)问题的一种方法,可以称之为"每个片段的原子链表",虽然这个名字只是为了方便表达,实际上它就是一种常见的技术。

传统渲染是前向渲染过程,每个像素会被多个三角形覆盖,按顺序处理每个三角形对像素颜色和透明度的影响,状态依次传递更新。问题是如果想知道后续覆盖的片段情况,传统方法无法提前得知,导致透明物体(比如重叠的树木)渲染时会出现错误的深度和透明叠加。

片段链表方法的核心思想是:帧缓冲区不再简单存储一个像素的颜色状态,而是存储一个指向链表的指针。每当一个三角形覆盖该像素时,就生成一个片段,并将其加入该像素对应的链表中。最终通过遍历链表,所有片段的信息都能被完整保存,并在后续阶段统一排序和混合,准确计算出最终颜色。这样就能解决传统渲染中顺序错误带来的透明度问题。

这种链表可以是单链表结构,或者是固定大小的数组,根据实现需求不同。片段链表的维护涉及内存管理和多线程同步问题,因为多个片段可能同时写入同一个像素的链表。现代GPU支持原子操作(atomic operations),能够安全地分配链表节点和更新指针,避免并发冲突。

这项技术依赖于现代GPU的计算能力和原子操作特性,五六年前很难实现,但现在大多数PC级GPU都支持这些复杂操作。移动设备和一些较老硬件可能还不能很好支持。实现时需要额外的计算资源和显存空间,成本较高,但如果愿意投入,能够得到高质量的透明渲染效果。

总之,利用每个像素的片段链表,将所有覆盖该像素的片段保存起来,最后统一排序和混合,能彻底解决透明物体渲染中的排序和遮挡问题。这是一种非常强大但计算量较大的透明度解决方案。

这听起来很疯狂,有现代游戏用这个技术吗?

我们谈到了"cubiculum"(房间摄像机)的概念,讨论它听起来非常疯狂,也问及是否有现代游戏使用这种技术。对此我们坦率地承认其实并不清楚具体哪些游戏采用了,但合理推测现代游戏中应该会有类似应用。比如可以去问一下像《战地》系列的开发团队,或是像DICE这样的公司,他们可能会用到这种技术。总体感觉这类技术适合那些对镜头和空间感控制有较高需求的大型游戏,因此很可能已经被现代游戏采用。

你能不能先正常预渲染到离屏缓冲,然后用淡出 alpha 合成到主帧缓冲?

我们讨论了游戏中快速处理透明度问题的一个方法,就是先将画面预先渲染到一个辅助缓冲区(austrian buffer),然后再和主帧缓冲区进行合成。我们认为这可能是最实际的做法,也是之前提到的方案。虽然不能排除还有其他选择,但综合来看,这种先渲染到辅助缓冲区再合成的方式是最合理的解决方案。

如果没有抗锯齿问题,可以一直用深度缓冲区的前到后渲染方法,同时减少过度绘制带来的带宽消耗

我们讨论了使用深度缓冲区从前到后渲染来减少过度绘制的问题。虽然这样做可以减少一些额外的绘制开销,但如果想用深度缓冲区,还可以尝试对边缘部分使用alpha覆盖(alpha coverage)技术来处理透明度边缘。然而,这样做可能会增加带宽的使用,造成额外负担,可能比我们实际需要的还要多。因此,虽然可行,但在带宽成本上可能并不划算。

这听起来会有很大性能开销

性能影响确实很大,可能会导致渲染速度变慢十倍甚至二十倍,但这取决于目标硬件和想实现的效果。如果目标硬件非常高端,比如需要NVIDIA 980这样的显卡,那就不用担心老旧机器的性能问题,专注于发挥强大GPU的计算能力。

相比于浪费大量资源做无意义的过度绘制,花费更多性能去实现"每片元原子列表"(per-fragment atomic lists)这种技术,虽然计算量增加很多,但它让很多之前做不到的效果成为可能。比如能够处理多层深度的烟雾效果,光线穿透并累积不同深度的物体,实现更精准的边缘处理和遮挡效果。

现代GPU提供了巨大的计算能力,关键在于如何合理利用这些性能,而不是单纯去避免额外开销。很多高级特性看似代价高昂,但如果能够带来创新和视觉上的提升,这些额外开销是值得的。简言之,技术的选择和性能消耗应当和最终想实现的视觉效果以及目标硬件平台相匹配,才能达到最佳的效果和效率平衡。

说到烟雾的例子,你看到 David Rosen 今天发布的内容了吗?

提到烟雾效果,看到今天有人发的内容,感觉那种烟雾看起来挺基础的,比较像普通的颗粒系统,效果比较模糊、不够真实。相比之下,战地1预告片里的绿色芥末气体那种烟雾效果才真的让人震撼,看起来非常酷炫,尤其是结尾部分的那个场景,烟雾的表现非常细腻和逼真,给人强烈的视觉冲击,让人很想了解他们是怎么做出来的。

我以为大多数游戏只针对主机硬件做最大化优化

大多数游戏设计都是为了最大限度地利用主机硬件,但需要记住,主机硬件本身也支持原子操作,比如PlayStation 4就具备原子操作功能。虽然具体细节可能不完全清楚,但可以确定这类硬件架构(类似于"推土机"架构)支持这些功能,因此利用原子操作在主机上实现复杂的图形计算是可行的。

相关推荐
LVerrrr1 小时前
Missashe考研日记—Day37-Day43
学习·考研
uyeonashi1 小时前
【Boost搜索引擎】构建Boost站内搜索引擎实践
开发语言·c++·搜索引擎
TIF星空2 小时前
【使用 C# 获取 USB 设备信息及进行通信】
开发语言·经验分享·笔记·学习·microsoft·c#
Smile丶凉轩4 小时前
Qt 界面优化(绘图)
开发语言·数据库·c++·qt
AI算法工程师Moxi5 小时前
什么时候可以开始学习深度学习?
人工智能·深度学习·学习
small_wh1te_coder6 小时前
从经典力扣题发掘DFS与记忆化搜索的本质 -从矩阵最长递增路径入手 一步步探究dfs思维优化与编程深度思考
c语言·数据结构·c++·stm32·算法·leetcode·深度优先
Magnum Lehar6 小时前
3d游戏引擎的math矩阵实现
线性代数·矩阵·游戏引擎
hjjdebug8 小时前
constexpr 关键字的意义(入门)
c++·constexpr
丰锋ff8 小时前
操作系统学习笔记第3章 内存管理(灰灰题库)
笔记·学习
jackson凌9 小时前
【Java学习笔记】equals方法
java·笔记·学习