游戏引擎学习第296天:层的雾效和透明度
总结并为今天的工作定下基调
我们正在进行多层平面渲染的工作,具体是处理 Z 轴方向上的相关问题。众所周知,在 2D 游戏中不可避免地需要做出一些妥协,因为我们试图表现出一定的空间深度,但所有图像都是二维的,不支持真实的旋转操作等,因此深度只是虚构的。我们通过不断实验、搭建场景和测试不同方案,逐步摸索出了一种比较可靠的方式来处理游戏中的深度问题。
为了解决这个问题,我们对渲染器的提交方式做了一些必要的调整,以便能够更准确地表达“深度”在游戏世界中的意义。我们昨天搭建了一个示例的分层场景,这个场景集中展现了当前我们在处理 Z 轴深度时遇到的所有问题,目的是逐一解决这些问题,最终让渲染接口能够准确且方便地表达游戏在深度层面的需求。
查看当前的场景可以发现,一开始就有几个明显的问题。首先是排序系统出现了混乱,这主要是由于我们在处理 Z 轴时方式不当导致的。实际问题可能看起来比实际情况严重一些,根本原因是我们现在没有标记一个图形是“平放”在地面上还是“竖直”立起来的。
这又回到了我们以前在渲染器中讨论的老问题:有些物体是贴地的(平放),有些是竖立的,这两类物体在排序上必须采用不同的方式处理。当前我们并没有处理好这两类表面之间的排序逻辑。我们还没有添加相关机制去标记这些图形是否是平放的。
目前看到的问题,比如角色跳跃的平台(绘制成矩形)出现在人物前面而不是后面,就是因为这些矩形没有标记为“平放”类型,导致它们参与了错误的排序流程。这是当前深度处理没有完善所造成的直接后果。
我们接下来的目标就是解决这些排序和渲染的问题,使渲染接口更准确、更合理地反映游戏逻辑中物体在深度上的关系。
修改 game_world_mode.cpp
:在 UpdateAndRenderWorld
中设置 Volumes 不再保持直立(Upright)
我们不太确定,但有可能之前设置的某个行为其实是可以对所有渲染项生效的,而不仅仅局限于位图。需要进一步确认,但初步判断是,那种“躺倒”或“平放”的状态,可能是通过变换矩阵(transform)中某个字段控制的,比如是否是“upright”(竖立)的。
在之前的实现中,例如 world_node
结构里,可能并没有对某些元素设置这个标志,比如道路块(road chunks)现在可能还没设置,但对于某些可通行区域、体积区域等内容,我们本应设置它,却没有设置。
我们可以做的改进是,参考已有的逻辑,比如 entity_transform
,我们之前在这个结构中处理了对象的变换信息。当我们开始绘制这些体积或者地形(比如小山、坡道等)的时候,可以主动在代码中指定:
EntityTransform.Upright = false;
这行代码的作用是明确告诉渲染系统,这个对象不是竖立的,而是平放在地面上的。
通过这样的设定,渲染器就能准确地区分哪些对象应该采用“平面排序”,哪些应该采用“竖直排序”,从而修正因排序逻辑错误导致的图层错乱问题,例如地面矩形盖住角色的问题。这是我们改进深度处理逻辑、提升渲染准确性的关键一步。
运行游戏,观察改动带来的效果
我们可以看到,当设置了某些对象为“平躺”时,确实能在排序行为上产生一些效果,使得渲染顺序更合理。这种设置能一定程度上解决深度层级的问题,比如让贴图理解为是贴在地面上而不是立在地面上。但即使如此,仍然存在一些没有解决的问题,排序仍然不正确。这不是排序逻辑的错误,而是我们表达“什么东西应该画在什么位置”这件事情本身存在局限。
如果场景中所有元素的位置和层级是恒定的,排序会非常简单。但现实是,位置和层级可能会动态变化,比如一个角色从楼梯底下走到楼梯上面,某个时刻在地板下,接着又处于地板上。这样的复杂场景让我们更难精确地表达每个对象应该处于哪个层。
此外,另一个更严重的问题是,目前 Z 值(深度值)并不支持位移效果。也就是说,虽然我们可以设置对象的 Z 值来影响排序,但这个值并不改变对象在画面中的实际位置。比如角色头部上下晃动,我们希望能通过 Z 值实现这种悬浮或抬高的效果,但目前 Z 值只影响排序,不影响实际的视觉偏移。
我们希望改进这一点,使得 Z 值可以真正实现“悬浮”或“高度偏移”的效果。同时,还希望 Z 值能用于处理“透视变化”——比如角色走上楼梯,我们希望整个楼层向下压缩或拉伸,让上层楼变小,下层楼显现,这是一种类透视的表现。
但这里有个重点是:这种透视式的变化,只希望发生在完整楼层之间,不希望发生在楼梯或斜坡等细节元素上。因为我们的美术风格是正交投影(orthographic projection),如果有些物体发生透视变化,有些没有,会让玩家感到混乱,破坏空间理解。
所以我们的做法是:让一个完整楼层作为整体向上或向下移动,表现层级关系;但在同一楼层内,所有物体都保持正交关系,不做透视变化,确保画面稳定一致。
还有一点是远处楼层的视觉处理。目前已经初步实现了楼层的远近淡出效果,但目前的处理是通过 alpha 渐隐,也就是变得透明。但这并不是我们真正想要的效果。我们更希望表现为“雾化”处理,也就是说楼层远了之后不是变透明,而是变暗、有雾感,模拟空气颗粒、远景灰度等效果,让玩家感受到“远处难以看清”的感觉,而不是直接“看穿”物体。
总结起来,我们需要解决以下几个关键问题:
- 明确定义对象是否是“平面”还是“立起”,以便正确排序;
- 允许 Z 值影响对象实际位置,实现真实的垂直偏移和漂浮效果;
- 在渲染器中加入支持完整楼层整体升降,表达透视式的上下层切换;
- 保持单一楼层内部使用正交视图,防止视觉混乱;
- 实现雾化远处楼层的视觉处理,而非简单透明化;
- 梳理清楚向渲染系统传递 Z 值的方式,让其能理解开发意图并做出正确渲染。
这些都是当前 Z 层级渲染系统面临的问题,我们需要一一解决,以完善整个深度表达机制。
修改 game_render_group.h
:将 render_group
中的 GlobalAlpha
变为 v4
类型,并重命名为 GlobalModulate
值得注意的是,虽然目前我们使用的是 Global Alpha(全局透明度)机制来实现某些视觉效果,比如楼层之间的雾化淡出效果,但这种方式未必是最理想的方案。现在的做法是在渲染颜色时将颜色乘以一个 Global Alpha 值,从而使整体变得更透明,以达到远近区分或视觉淡化的效果。
我们现在考虑的一种改进方式是,不再单独使用 Global Alpha,而是引入一种更通用的“全局调制”(Global Modulate)机制。这个机制不仅可以控制 alpha(透明度),还可以同时调制 RGB 颜色通道,也就是说可以整体地影响颜色的各个分量,而不是只影响透明度。
目前的方式是将每个颜色直接乘以一个 alpha 值,这只会降低颜色强度而不会改变颜色本身。而通过使用 Global Modulate 向量(v4,包含 RGBA 四个分量),我们可以同时对颜色的红绿蓝和 alpha 进行调制。这样我们不仅可以让物体变得更透明,还可以让它变暗、变蓝、变灰等,实现更多种类的视觉效果,比如雾化、环境色影响、光照模拟等。
这种方式对于我们目前的问题会更通用也更灵活。比如在实现远处楼层的雾化时,我们不只是希望它们变得透明,而是希望颜色整体变得朦胧、发灰、带有空间感。用 Global Modulate 可以在统一的框架下实现这一点,而不需要手动处理每个颜色的改变。
综上所述,计划中的调整是:
- 不再单独使用 Global Alpha,而是引入 Global Modulate 向量;
- 这个向量包含 RGBA 四个分量,可以全面调制渲染颜色;
- 应用时将整个颜色与 Global Modulate 做乘法,即每个颜色分量都被调制;
- 这样既可以控制透明度,也可以整体变色,实现更自然的雾化或远近视觉表现;
- 这一机制将提升我们对渲染系统的控制能力,避免局限于只用透明度处理视觉层次的问题。
通过这个改动,我们可以构建更丰富、更合理的渲染体系,尤其是在多楼层、多视角、多深度的游戏世界中,这种机制会带来更强的表现力。
考虑清晰地区分使用预乘 Alpha(premultiplied alpha)与非预乘 Alpha 的颜色数据
我们目前在处理颜色值时,存在一些不够规范和一致的问题,尤其是在是否使用“预乘 Alpha(Pre-multiplied Alpha)”这一点上做得不够明确和统一。这种不一致会导致颜色渲染行为不确定、难以维护,并可能引发视觉错误。
具体问题体现在以下几个方面:
-
颜色值是否是预乘 Alpha 不清楚:
我们在代码中传入颜色值时,并没有明确标注这些颜色是否已经与 Alpha 分量相乘(即预乘 Alpha)。这会导致后续渲染流程中无法正确判断该如何混合颜色。 -
各个模块之间处理方式不统一:
比如,在 OpenGL 路径中,我们直接指定了颜色值,但没有明确说明这些颜色是否是预乘的;而在我们自己写的渲染器中,比如DrawRectangleQuickly
函数内部,可以看到我们在这里执行了预乘操作,即把颜色乘上了 Alpha 值再去渲染。这种差异使得系统变得混乱。 -
我们没有形成统一的约定和执行路径:
目前看来,我们更倾向于在稍后的阶段再去做预乘处理(也就是传入的是非预乘的颜色,之后再乘 Alpha),但代码中并未统一遵循这一原则,这增加了维护难度。
因此,我们需要进行一次统一的清理和规范化工作,来解决以下问题:
目标和改进方案:
-
确立统一的颜色处理约定:
统一规定颜色传入时为非预乘 Alpha(即 RGBA 的 RGB 分量未乘 Alpha),所有预乘运算将在进入实际渲染流程前完成。 -
集中式预乘处理:
在进入渲染器之前的某个明确步骤中,对所有颜色做一次标准化的预乘处理,保证进入底层渲染调用时的数据是统一格式。 -
清理所有现有路径中的不一致逻辑:
检查所有使用颜色的路径,包括 OpenGL 渲染、软件渲染、位图混合、矩形绘制等,确保它们遵循新的统一约定。 -
明确文档说明及代码注释:
在颜色处理相关的函数和结构体中标注是否为预乘 Alpha,以便日后维护和理解。 -
考虑加入调试模式验证颜色格式:
可以加入简单的调试断言或验证机制,确保传入颜色数据时符合预设规范,比如 RGB 不应大于 Alpha 的值等。
通过以上整理和修复,我们的渲染系统在颜色处理上将变得更加健壮、清晰,并且更容易调试和维护。这对于今后加入复杂视觉效果(如半透明、雾化、光照)是非常重要的基础工作。
修改 game_render_group.cpp
:引入 StoreColor
函数,并在 PushBitmap
、PushRect
和 Clear
中调用它
我们决定暂时不处理“人员分布”的相关问题,虽然这个问题很重要,但现在先暂停一下,然后之后会尽快回到这部分。
目前的重点是对条目的渲染元素进行统一的颜色处理方式。我们计划引入一个名为 store_color
的函数,用于在每次我们需要为条目传递颜色值时进行统一的处理。这些颜色值将作为“重要颜色”存储起来,虽然暂时还不知道具体用途,但目标是将颜色的处理逻辑集中管理。
具体做法如下:
-
定义一个
store_color
函数,接收源颜色和目标颜色参数,目前这个函数只是将颜色值直接传递过去。 -
在系统中所有需要打包颜色进入条目的地方,都统一调用这个
store_color
函数。 -
检查代码中所有使用
push
操作和传递颜色的部分,确保它们都经过store_color
处理。- 比如,在处理位图的
push
操作时,虽然不会传递颜色,但应该确保其结构兼容。 - 当执行
push_rect
时,也要统一调用store_color
。 push_rect_outline
可能不经过同样的路径,所以不需要修改。clear
操作中也应调用store_color
,保持一致。
- 比如,在处理位图的
-
这样做的结果是所有与颜色相关的传递操作都集中到了一个统一的函数中进行处理,为后续添加颜色处理逻辑打好基础。
接下来,如果我们要渲染圆形或者其他图形时,也可以按照同样的约定使用 store_color
。而这项约定是,每次调用 store_color
时,源颜色会和全局颜色值进行 Hadamard 乘积(即逐元素相乘),因此我们将需要获取当前的渲染上下文(render group)信息。
这样设计的好处是,所有颜色的处理都被规范化和模块化,便于统一调整和增强颜色混合逻辑。
黑板讲解:Hadamard 积(哈达玛积)
我们决定在颜色渲染流程中引入一种全局调制(global modulate)机制。该机制的核心思想是对源颜色进行每分量的乘法操作,也就是所谓的 Hadamard 乘积(有时称为“元素乘”或“分量乘”)。这是一种非常直观的向量乘法方式——对两个向量的每一个对应分量分别相乘,例如:
如果我们有两个向量:
- 第一个是源颜色
(x, y, z)
- 第二个是全局调制颜色
(a, b, c)
那么它们的 Hadamard 乘积就是:
(x * a, y * b, z * c)
每个分量单独相乘。这种方式和我们在图形编程中常用的内积(dot product)或叉积(cross product)不同,它没有涉及向量间的方向关系,仅仅是逐个分量的乘法,非常适合用于颜色调制。
在渲染中使用 Hadamard 乘积的场景主要是颜色混合,比如实现雾效(fog)、全局光照调整(global tinting)或透明度控制(alpha blending)。引入这个机制后,我们可以通过设置一个全局调制颜色值来统一调控所有渲染对象的颜色表现。
例如:
- 设置一个偏暗的调制颜色,可以让所有图形整体变暗,实现夜晚效果。
- 设置一个较低 alpha 值的调制颜色,可以整体控制透明度,达到淡入淡出的目的。
目前我们使用这种方式可以很好地实现颜色向黑色的渐变,比如淡出或远处消失的效果。但当我们希望颜色向白色或其他颜色淡出时,仅仅使用 Hadamard 乘积就不够了。因为 Hadamard 乘积的本质是压低颜色值(分量相乘只能变小),所以它无法模拟颜色“漂白”或变亮的情况。
为了解决这个问题,我们意识到还需要引入“雾颜色”的概念(fog color)。这意味着不仅仅压低原始颜色,还要向某个目标颜色进行过渡,即:
- 除了调制外,还要定义一个目标颜色,向其进行插值。
- 可以控制混合比例,比如根据距离或透明度渐变。
这样的机制比单纯的 Hadamard 乘积更加灵活,能够适应更多样化的视觉效果需求,比如淡出到白色、褪色到蓝色等。
因此,下一步除了继续使用 Hadamard 乘积进行基础调制,还将考虑引入目标颜色插值机制,使得颜色过渡处理更具表现力和控制力。
修改 game_render_group.h
:为 render_group
添加 tGlobalColor
和 GlobalColor
,以实现线性混合与调制(modulate)
我们进一步扩展了颜色处理的方案,在原有的全局调制(global modulate)机制基础上,加入了一个全局颜色(global color)变量,用以实现更灵活的颜色混合控制。整体目标是构建一种既能进行线性混合,也能进行调制的机制,以适应更复杂的渲染需求,例如环境变化、特效渲染等。
核心机制构建
我们引入两个全局变量:
- 全局颜色(global color):表示希望向其混合的目标颜色。
- 全局调制值(global t):表示混合强度或调制强度的因子(每个颜色通道独立控制)。
每次渲染颜色时,都会调用颜色存储函数(store color),在这里根据传入的源颜色、全局颜色和混合因子进行计算处理。
颜色处理的计算逻辑
处理流程如下:
-
对于每个颜色通道(R、G、B)分别处理;
-
获取源颜色分量值
src.x
(也可能是src.r
); -
获取目标颜色分量
global_color.x
; -
获取对应通道的混合系数
t
值(来自全局调制向量); -
应用线性插值公式:
d s t . x = ( 1 − t ) ⋅ s r c . x + t ⋅ g l o b a l _ c o l o r . x dst.x = (1 - t) \cdot src.x + t \cdot global\_color.x dst.x=(1−t)⋅src.x+t⋅global_color.x
-
重复上述操作处理 G 和 B 通道。
这样我们可以实现:
- 当
t = 0
时,输出颜色保持原样; - 当
t = 1
时,输出完全为目标颜色; - 当
t
介于 0 和 1 之间时,实现平滑渐变; - 当目标颜色为全 0 且
t > 0
时,表现为“变暗”或“调制”。
反向调制的设计选择
考虑到初始化问题和一致性,我们决定采用反向调制方式:t
值越大,表示越倾向于目标颜色,反之则越倾向于保留原色。
- 这样做的好处是:默认情况下(所有
t=0
),不会对原始颜色产生任何影响; - 目标颜色(global color)为全 0 时,将不会造成任何不必要的颜色偏移;
- 游戏初始化阶段只需设置一次全局调制和颜色值,后续使用更灵活。
用法和灵活性
这个机制具备高度灵活性:
- 想要调制(modulate):设置目标颜色为全 0,仅通过
t
控制亮度; - 想要淡入淡出:设置一个具体的目标颜色,并调整
t
实现过渡; - 想要染色:设置非黑的目标颜色,加上一定的
t
值,即可给物体叠加色调; - 所有参数可以被游戏逻辑动态设置,例如根据时间、距离、状态或事件变化来控制颜色过渡效果。
总结
我们构建了一套统一的颜色处理机制,结合全局颜色与全局调制值,实现了颜色调制与混合的融合式处理方案。该系统结构清晰、逻辑自洽、初始化安全,且便于游戏逻辑进行动态控制。每次渲染前只需做一小段数学计算,即可根据当前全局状态影响所有渲染条目的颜色行为,大大增强了渲染系统的表现力和扩展性。
修改 game_render_group.cpp
:将 Group
传递给 StoreColor
我们在整体渲染系统中做了进一步的优化和结构性调整,目标是实现更灵活、更清晰的全局颜色与透明度控制机制,以适应不同方向上的淡出与雾化效果。
组传递机制
在所有的颜色存储逻辑(store color)中,我们统一要求传入“组(group)”参数。这一点非常合理,因为在以往的所有存储操作中,本身就已经包含了实体所属的分组信息,因此传递这个额外参数并不会增加复杂性或负担。
初始化自动化
全局调制(global modulate)值现在默认在初始化时为零。这意味着不再需要手动初始化这些值,系统会自动设置为默认无效状态(即对颜色无影响),使得初始化逻辑更简洁,也避免了潜在的遗漏。
全局 Alpha 设置优化
在处理全局透明度(global alpha)时,我们对每个实体分别进行设置。在新的逻辑中,不再像之前那样统一进行渐隐(fade),而是根据不同位置或方向采用不同的处理方式:
-
不再始终渐隐(not always fading):过去的处理方式是在所有方向上统一执行透明度渐变;
-
改为按方向控制:现在我们只对一部分区域进行渐隐,例如:
- 上方区域(从顶部下降的物体)进行渐隐处理;
- 下方区域(视野底部)则采用雾化(fogging out)的方式。
这样的处理更加符合视觉体验和游戏氛围需求,也允许更精准地控制不同区域的显示效果。
三种状态的判断
由于存在多种透明度或颜色混合的需求,我们引入逻辑判断来区分当前渲染对象处于哪一种状态:
- 渐隐状态(如:从顶部进入视野);
- 雾化状态(如:从底部逐渐模糊消失);
- 常规状态(完全不受影响,保留原色);
根据判断结果分别应用不同的颜色处理和混合策略。
整体目标与效果
通过这一系列的优化:
- 我们实现了按方向分离的视觉过渡控制;
- 简化了初始化和颜色传递逻辑;
- 保持了渲染路径的一致性;
- 并为未来更多的视觉特效处理(如区域遮罩、距离衰减等)打下了基础。
这样的设计更灵活,更高效,同时也更易于维护和扩展。
黑板讲解:Fade(渐变)与 Haze(雾化)区别
为了更清楚地区分“渐隐(fade)”和“雾化(fog)”之间的差异,我们进一步明确了它们在渲染系统中的用途和视觉表现方式:
渐隐(Fade)
-
核心特性:是透明度变化,使物体逐渐变得“可穿透”,即可以看穿某个图层,看到其背后的内容;
-
应用场景:主要用于角色与摄像机之间的遮挡图层;
-
视觉效果:这些图层不会完全遮挡视线,而是部分透明,比如 50% 的不透明度;
-
使用方式:在实际渲染中,这些图层设置了非全透明的 alpha 值,通过 alpha blending 实现逐渐消隐;
-
举例说明:
- 视角从上向下看时,人物正好处在某个建筑层后面;
- 为了不遮挡玩家视线,该层会以一定透明度显示;
- 使玩家可以“看到”人物并理解其所处空间位置。
雾化(Fog)
-
核心特性:不是“透明”,而是颜色混合,让图像“模糊、灰暗、失真”;
-
应用场景:用于视野下方(如地牢的更深层)或远处的视觉区域;
-
视觉效果:混合向一种指定的“雾气颜色”或“远景颜色”,营造出深邃、神秘、看不清的氛围;
-
使用方式:将原始颜色与雾颜色做线性插值,不涉及 alpha 透明度;
-
特点区别:
- 不会看穿到底层内容;
- 只是“失真”并趋近某种统一的颜色(例如深灰、蓝黑);
-
举例说明:
- 玩家视角向下望向地牢更底层时,底部区域的物体不会透明;
- 而是逐渐被一种迷雾色覆盖,使远处看上去模糊并带有氛围感;
- 给人以深远、未知、危险的视觉暗示。
总结对比
特性 | 渐隐(Fade) | 雾化(Fog) |
---|---|---|
是否透明 | 是(可以看穿) | 否(不会透视) |
使用方式 | 设置 alpha 值进行透明度混合 | 颜色插值到指定的雾色 |
视觉目标 | 保证遮挡物后面的内容仍可辨认 | 营造深远、神秘、模糊的空间感 |
典型场景 | 摄像机前方遮挡图层 | 远处/地下视野的模糊区域 |
渲染实现 | alpha blending | color lerp(颜色插值) |
通过这种明确的划分,我们可以在不同场景下选择合适的渲染方式,既不混淆透明与雾效的逻辑,又能统一整套渲染管线的数据结构与计算路径,从而实现更具表现力、更细腻的视觉过渡与层次感。
有问题
# 修改 game_world_mode.cpp
:在 UpdateAndRenderWorld
中条件性地计算 tGlobalColor
和 GlobalColor
,实现渐变和雾化
我们现在的目标是确保在渲染系统中,不同情境下的颜色混合和透明度处理行为清晰、统一,并且可以通过配置全局渲染组参数来灵活控制。
默认情况下(无任何混合效果):
- 渲染组的
global_color
设为不重要的值; - 更关键的是,颜色混合的目标色(即 fade color 或 haze color)全部设为
0
,代表当前不参与颜色混合; - 这时渲染出来的颜色就是原始颜色,完全不做任何颜色处理;
- 所有混合度参数(t 值)也设为
0
,保证既不混合颜色也不影响 alpha。
情况一:向下层雾化(Fog)
global_color
设置为我们希望的雾效颜色,例如vec4(0.5, 0.5, 0.5, 0.0)
表示 RGB 都为灰色,alpha 为 0;- t 值用于控制混合强度,只用于 RGB 分量,alpha 分量设置为
0
; - 目的是 只混合颜色,不影响透明度,使下层区域变得灰暗、模糊,而不穿透;
- alpha 不变,保持不透明。
情况二:向上层透明(Fade)
global_color
中的 RGB 分量设为0
(不做颜色变化),alpha 设为0
;- t 值用于控制透明度变化,RGB 分量的 t 设为
0
,alpha 分量设为某个混合值; - 目的是 只影响透明度,让上层逐渐透明以看到下层内容;
- 颜色不变,仅仅变透明。
具体实现逻辑:
-
每次设置混合参数时,只需设定
global_color
和global_t
两个向量(分别控制目标颜色和混合强度); -
当我们设置的是雾化效果:
global_color.rgb = 雾颜色
global_t.rgb = 雾混合强度
global_t.a = 0
(不变透明度)
-
当我们设置的是渐隐效果:
global_color = 任意
(因为 RGB 不参与混合)global_t.rgb = 0
global_t.a = alpha 混合强度
清除混合效果:
- 恢复默认时,只需把
global_color
和global_t
全部设为0
; - 这样系统会自动忽略所有混合逻辑,恢复正常渲染。
关于 t 值使用一致性的思考:
- 当前每次设置混合效果时,我们对 RGB 和 alpha 使用了 相同的 t 值向量;
- 理论上可以把颜色和透明度的混合权重拆成两个独立值(例如
color_t
和alpha_t
),从而实现更细粒度的控制; - 但目前为止所有使用场景都只涉及“要么混合颜色,要么混合透明度”的简单二选一结构,因此保持一个统一的
global_t
是合理且足够的; - 如果未来遇到需要同时混合颜色和 alpha 的需求,可以考虑扩展。
整体总结:
通过使用统一的全局参数 global_color
和 global_t
,我们实现了灵活控制颜色混合(雾化)和透明度渐隐(fade)的方法。该设计清晰、易于扩展,便于后续在渲染管线中增加更多图层处理或特效控制逻辑。
运行游戏,发现 Alpha 乘法处理不正确
我们目前仍有大量工作尚未完成,主要集中在 Alpha 通道的处理上,尤其是 alpha 的乘法(modulation)还没有完全正确地实现。当前的实现存在 bug,尤其在 OpenGL 渲染层中,alpha 的预乘处理似乎并未如预期执行,这是导致渲染异常的原因之一。
当前存在的问题:
-
Alpha 乘法处理不正确
在混合处理时,alpha 通道需要进行正确的乘法处理,特别是当我们执行 fade 操作(如从顶部向下 fade)时,我们希望目标 alpha 值为 0,即完全透明。但在实际过程中,alpha 通道的值未正确乘入渲染管线中。 -
t 值计算逻辑未完善
当前我们使用的 t 值计算逻辑,在理论上是正确的,例如 fade top 的时候,我们确实是计算一个趋向于 0 的 alpha 值。但问题在于,后续使用这些 t 值进行颜色与透明度混合时,管线中并未严格按照这个 t 值进行预乘或插值。 -
OpenGL 层未处理预乘 alpha
在具体渲染接口层,尤其是 OpenGL 的相关部分,似乎并未在合适的位置处理 premultiplied alpha(预乘 alpha),即 RGB 值应当乘以 alpha 值之后再参与混合。当前可能是直接将 RGB 输出,而没有考虑其应根据 alpha 进行预乘,从而导致渲染效果不正确,特别是在多层半透明重叠时。
下一步计划:
-
彻查所有相关代码路径,确保在渲染前将颜色值正确进行 alpha 预乘;
-
在设置
t
和global_color
时,确认逻辑上的目标是正确的,确保在 fade 相关的场景中,alpha 正确下降; -
确保 OpenGL 层在 shader 或 pipeline 设置中支持并执行 alpha 预乘;
-
针对 fade 和 fog 两种场景,分别验证:
- fade:alpha 应趋于 0,RGB 不变;
- fog:alpha 不变,RGB 逐渐变为 haze 色;
-
对所有使用了 alpha 的渲染路径,加入调试输出或可视化验证,帮助我们检查实际渲染值与理论预期是否一致;
-
最终确认混合公式是否为:
color_out.rgb = lerp(src.rgb, global_color.rgb, t.rgb) color_out.a = lerp(src.a, global_color.a, t.a) 然后 color_out.rgb *= color_out.a (预乘)
总结:
当前的核心问题出在 alpha 的预乘未被正确实现,导致 fade 效果表现异常。必须在 shader 或渲染流程中修复这一点,并统一所有相关逻辑中的 t 值使用与混合方式。同时,需要从逻辑定义和底层渲染两个层面同时处理,才能保证颜色与透明度的混合行为符合预期。
修改 game_render.cpp
:让 DrawRectangle
、DrawRectangleSlowly
和 DrawRectangleQuickly
不再预乘 Alpha
我们决定不再在渲染管线中对颜色值进行预乘(premultiplied alpha),转而将这一逻辑上移至调用代码中进行处理。这意味着颜色的乘法逻辑不再内置于底层渲染代码中,而是由调用方在必要时显式地执行。这种做法带来了更高的灵活性,并避免了多次重复乘法带来的性能浪费和混淆。
调整策略概述:
-
移除预乘逻辑
在render_entry
中原本执行的预乘处理被去除。之前在渲染路径中对颜色值(RGB)乘以 alpha 的代码已经明确地被删除。 -
预乘责任上移
原本在底层自动进行的预乘逻辑,将由调用方显式执行。调用代码在需要透明度混合时,应自行将 RGB 值乘以 alpha 值。这样可以清晰控制在哪些地方执行乘法,避免重复或错误的混合。 -
清理代码路径
所有涉及到p-multiply
(预乘)的函数调用均已清理,相关位置被标记,确保不会再隐式进行乘法处理。包括多个位置,如:- 原始渲染管线处理位置
- 打包或填充渲染数据的函数(pack function)
-
结构增强与状态标记
在render_group
或类似结构中添加了标识位,用于明确指示颜色值是否已经被预乘。这使得后续逻辑在处理渲染数据时能够根据标志位判断是否还需要做预乘处理。 -
记录与追踪
将预乘状态显式记录下来,在调试或数据传递过程中可以轻松检查颜色状态,保证渲染路径正确处理已预乘和未预乘的值。
带来的好处:
- 更清晰的逻辑边界:渲染层专注于绘制,不再干涉颜色逻辑;
- 更高的灵活性:允许不同的颜色混合策略按需配置,不受固定预乘策略限制;
- 减少冗余计算:避免重复对颜色进行乘法处理,提升性能;
- 调试更容易:显式记录预乘状态,便于分析渲染异常时追溯问题来源。
下一步建议:
- 系统性检查所有调用渲染接口的地方,确保在需要时显式执行颜色与 alpha 的乘法;
- 根据
render_group
中是否标记为已预乘,动态决定是否跳过乘法逻辑; - 保持清晰注释,特别是在传递颜色数据时注明其是否预乘,避免后续误用。
通过将预乘 alpha 的责任移出渲染核心代码,并加入明确的状态管理机制,可以更清晰、更可控地处理所有与透明度和颜色混合相关的渲染逻辑。此举对整个渲染系统的健壮性和可维护性具有积极意义。
修改 game_render_group.h
:在合适的位置将 Color
重命名为 PremulColor
我们对渲染管线中颜色的处理逻辑进行了系统性的调整,明确采用**预乘 alpha(premultiplied alpha)**的策略,并在整个渲染过程中保持一致。这不仅提升了渲染精度,也让处理逻辑更具一致性和可维护性。
主要调整内容如下:
-
颜色值统一为预乘格式
- 所有涉及颜色传递的地方统一转换为预乘格式,即每个 RGB 分量在存储或传递之前都与对应的 alpha 分量相乘。
- 这一规则从颜色产生之处开始执行,确保进入渲染流程的所有颜色值已完成预乘。
-
明确标识与命名
-
将原有变量统一改名为
premul_color
(预乘颜色),使得含义明确,不容易混淆。例如:color
→premul_color
group.color
→group.premul_color
-
在涉及颜色计算或传递的地方都加上标注或重命名,清晰表示该值已预乘,避免误用。
-
-
渲染流程保持一致性
- 无论是清屏(clear)、填充(fill)、矩形渲染(rectangle)等操作,使用的颜色值一律要求为预乘格式。
- 即便某些操作(如清除)对颜色值无实际影响,也统一以预乘格式表示,确保管线中每一部分的语义一致性。
-
颜色预乘执行逻辑
-
在调用存储颜色信息的接口(如
store_color
)时,一旦得出 alpha 值,就立即执行预乘:premul_color.r = color.r * alpha; premul_color.g = color.g * alpha; premul_color.b = color.b * alpha; premul_color.a = alpha;
-
该逻辑保证所有颜色值进入渲染阶段前,已完成与透明度的乘法运算。
-
-
OpenGL 层同步更新
- 在 OpenGL 的矩形绘制等相关部分也同步进行了调整,确保使用的颜色值与上层逻辑一致,均为已预乘的颜色。
- 并对相关变量添加注释或重命名,如
OpenGLRectColor → premul_color
,确保上下层含义匹配。
目的与好处:
- 统一处理逻辑:无论是哪一层调用代码,颜色处理规则始终一致,避免逻辑分歧。
- 渲染结果更准确:避免因为未预乘导致的透明混合异常或边缘发黑等视觉问题。
- 便于调试和维护:通过明确的命名与注释,可以迅速判断当前值的处理状态,提升维护效率。
- 便于批量更新:大量逻辑可以通过批量搜索替换进行重构,节省时间且风险低。
下一步工作:
- 系统性检查所有颜色处理相关模块,统一为
premul_color
并移除非预乘处理逻辑; - 在所有调用渲染接口的地方补充预乘处理,确保不遗漏;
- 检查中间结构和缓存,确认不会出现非预乘颜色值混入渲染路径;
- 补充文档说明或注释,以便后续开发者理解该规范。
通过这一系列处理,渲染系统在处理颜色与透明度混合时的表现将更稳定可靠,同时为后续特效扩展、图层管理等打下良好基础。
运行游戏,注意两个渲染器绘制的实心矩形颜色不同,并调查原因
我们目前已经对渲染流程中的颜色处理进行了一系列调整,确保颜色值在整个渲染管线中始终以预乘 alpha(premultiplied alpha)的形式存在,但从实际观察来看,仍然出现了一些问题,尤其是填充矩形时颜色异常发暗,这促使我们开始调查 OpenGL 的具体行为,尤其是与颜色混合和 gamma 校正相关的处理细节。
当前问题观察:
-
颜色异常仅出现在填充矩形上
说明问题可能集中在使用 OpenGL 绘制非纹理图元时,而不是在纹理路径中。 -
填充颜色看起来明显偏暗
这通常是颜色 gamma 未正确处理的表现之一。 -
纹理渲染路径(位图)显示一切正常
所有带纹理的路径表现正常,因此排除了 alpha 预乘或基本颜色数据的问题,更有可能是渲染 API 使用上的差异。
调试分析过程:
-
验证 OpenGL 中颜色是否被预乘处理
- 在 OpenGL 中使用
glColor4f
设置颜色,传入的颜色是否自动被预乘 alpha?答案是不会,OpenGL 默认不预乘颜色。 - 所以,传入已经预乘的颜色是正确做法。
- 在 OpenGL 中使用
-
Blend 状态是启用的,使用了
(GL_ONE, GL_ONE_MINUS_SRC_ALPHA)
- 这与我们预期的预乘 alpha 模式匹配,确认这一点没有问题。
-
疑点集中在 gamma 校正上
- 在其他路径中我们手动对颜色值进行 gamma 反变换(例如使用平方来还原线性空间)再执行混合,然后再重新 gamma 编码。
- 而 OpenGL 颜色设置路径(无 shader)可能根本没有处理 gamma,这就导致了颜色亮度的一致性差异。
-
代码中执行了颜色平方(即线性化处理)
- 这意味着我们在 RGB 输入前就对其做了 gamma 反变换(例如将 0.5 变成 0.25),目的是在做颜色混合之前进入线性空间进行处理,这符合正确的图像处理逻辑。
- 但 OpenGL 在直接绘制路径(未使用 shader)中,颜色值并未被自动线性化,导致颜色显示明显偏暗。
-
怀疑点集中在 OpenGL 渲染管线中的“输出 gamma 编码”
- OpenGL 默认 framebuffer 是 gamma 编码的,但输入颜色不会自动转换到线性空间,除非开启 sRGB framebuffer 或使用 shader 手动处理。
- 所以现在的问题很可能是:我们在一边对颜色手动做了 gamma 反变换,而 OpenGL 并没有在另一边做同样的处理,从而导致颜色偏暗。
分析结论:
- 填充矩形颜色偏暗的根本原因是:我们手动 gamma 反变换(squaring)了 RGB 值,目的是进入线性空间混合,但 OpenGL 默认并没有做匹配的“gamma 编码”输出,因此颜色结果比预期暗。
- 纹理路径没有问题是因为纹理采样、混合流程,以及 shader 处理都保持一致且我们控制完全。
- OpenGL 渲染命令中的
glColor4f
并不会进行 gamma 处理,更不会自动进行任何线性化或重新编码。
解决方向建议:
-
对非纹理绘制路径禁用手动 gamma 校正
- 不再对
glColor4f
设置的颜色进行平方处理,确保其处于默认 gamma 空间,这样结果会与 OpenGL 默认输出行为一致。
- 不再对
-
理想方案是统一启用 sRGB framebuffer
- 如果启用
GL_FRAMEBUFFER_SRGB
,OpenGL 将会自动对最终输出进行 gamma 编码,这样我们可以确保颜色在所有路径下统一线性处理,再统一输出。
- 如果启用
-
未来考虑全部切换至 shader 路径控制
- 使用 fragment shader 替代
glColor4f
的固定功能路径,自行控制颜色混合和 gamma 处理,提升一致性和可控性。
- 使用 fragment shader 替代
总结:
我们已经明确当前填充矩形颜色偏暗的根本原因是由于gamma 处理流程不一致导致的。尽管渲染路径中颜色值已进行预乘,但由于 OpenGL 在非纹理绘制中没有进行 gamma 编码,手动线性化处理反而让颜色变得更暗。下一步应统一 gamma 处理策略,并考虑启用 OpenGL 的 sRGB framebuffer 或完全采用 shader 渲染流程,以获得更准确和一致的视觉表现。
修改 game_render.cpp
:尝试在最开始就对 Color
进行平方操作
我们目前处理的是 textile_rgb
的颜色混合流程,具体问题集中在 gamma 校正(gamma correction)和颜色平方(squaring)操作上。以下是对当前状态的详细总结:
一、颜色平方的位置和逻辑困惑
我们有一个输入颜色 rgb
,这本身是不变的。然而,在某些地方我们对它进行了平方操作(mm_square
),理论上这是为了进行 gamma 解码(即将 gamma 空间颜色转换为线性空间以便混合)。但问题在于:
- 源颜色(incoming color)本身没有变化,我们却对它进行平方,显得逻辑不太合理。
- 如果只是单纯地拷贝源颜色,那平方没有意义,除非后续有混合操作依赖于这个线性化结果。
- 推测当初添加平方逻辑可能是为了统一处理混合中的 gamma 校正问题,但当前实际使用场景中并未生效。
二、颜色乘以 255 的混乱位置
我们对颜色值进行了乘以 255 的操作以将其从 0–1 浮点空间映射到 0–255 整数空间:
- 然而,在乘以 255 之前就执行平方是有问题的。
- 正确的顺序应是:先乘以 255,再平方,才能模拟 OpenGL 在整数颜色空间中进行 gamma 解码的效果。
- 当前实现中,如果先平方再乘 255,会导致颜色值严重偏暗或失真。
三、颜色处理结果的观察反馈
通过渲染结果可以观察到:
- 位图(bitmap)路径经过相同处理流程后,颜色几乎没有变化,说明这一部分处理是合理的。
- 但纯色填充矩形(solid filled rectangles)颜色却发生了明显的偏差,说明两者在管线中经历了不同的处理路径或阶段。
- 这表明某个地方存在非一致性,很可能是 OpenGL 对颜色的处理(例如是否自动 gamma 编码/解码)与当前 CPU 路径存在差异。
四、对 OpenGL 行为的疑惑
我们目前尚不明确 OpenGL 在接收到颜色(如 glColor4fv
)时是否自动执行了某种颜色空间变换:
-
如果 OpenGL 在写入帧缓冲时自动执行 gamma 编码,而我们没有进行配套的解码/编码处理,就会造成颜色不一致。
-
此外,OpenGL 渲染状态下是否启用了 sRGB 帧缓冲(GL_FRAMEBUFFER_SRGB)也会影响颜色输出。
-
不清楚的部分主要包括:
- OpenGL 的 modulation color 是否会被 gamma 编码。
- 我们当前手动进行的 gamma 校正是否与 OpenGL 内部行为重复或冲突。
五、临时实验尝试与推理
在当前逻辑中尝试了以下方法:
- 取消平方操作,观察颜色变化,发现并无显著改善。
- 修改平方次方函数为
pow(color, 2.2)
,尝试更接近真实 gamma 曲线,效果有所改善但性能变差。 - 得出推论:OpenGL 可能自动处理了一部分 gamma 编码流程,而我们手动进行的 gamma 校正可能与其行为重复,造成过度校正。
- 进一步猜测当前不一致性的根源在于:OpenGL 和自定义渲染路径中,颜色处理流程未完全对齐,尤其在 gamma 解码的阶段。
六、总结与后续建议
-
统一处理流程:
- 所有颜色在进入混合前必须统一处理空间(全部转换为线性空间),混合后再统一编码为 gamma 空间。
-
修正操作顺序:
- 若在整数空间中进行平方,应先乘 255,再平方,不能反过来。
-
调查 OpenGL 行为:
- 核实是否启用了
GL_FRAMEBUFFER_SRGB
。 - 明确
glColor4fv
是否参与了 gamma 编码。 - 检查驱动是否有相关自动转换行为。
- 核实是否启用了
-
建议添加调试标志:
- 在渲染路径中加入明确标识(是否平方、是否 gamma 编码),方便后续排查。
-
逐步验证每一段路径:
- 分别对 bitmap 路径、solid fill 路径进行颜色插入与渲染对比,验证是否是特定路径的行为不一致。
我们当前最大的问题是 OpenGL 与 CPU 路径之间颜色空间处理未能对齐,导致视觉效果偏差严重。要解决这个问题,需要更系统地校准颜色处理流程,并搞清楚 OpenGL 渲染时实际应用了哪些隐式变换。
修改 game_render.cpp
:尝试将所有颜色乘以 255
当前讨论的是在颜色混合过程中,目标颜色(destination)被处理为平方值,这个平方是在 0 到 255 的空间中完成的。这引出了一个非常关键的问题:为了使混合效果与 OpenGL 保持一致,输入颜色(incoming color)也必须符合一定的前提条件。以下是详细的中文总结:
一、颜色平方空间的统一
当前我们对目标颜色(destination)做了平方处理,这表示:
- 混合操作是在“线性空间”中进行的。
- 但这个线性空间是建立在 0–255 的整数空间上,也就是说,平方操作是在已经乘以 255 的基础上完成的。
二、输入颜色的处理推论
既然目标颜色是在 0–255 空间平方的,那么:
- 如果希望输入颜色与之匹配,就必须也处于类似的“平方后”的线性空间。
- 这意味着:输入颜色必须先乘以 255,再进行平方,才能和目标颜色匹配。
- 同时,为了避免重复 gamma 校正,输入颜色不应再进行 gamma 编码,否则会发生偏差。
也就是说:输入颜色需要处于“已乘255但未 gamma 编码”的状态。
三、结论与推测
这种逻辑虽然看起来有些反直觉(“crazy”),但从技术上是合理的:
- OpenGL 中如果目标颜色自动做了 gamma 编码(或解码),而我们手动处理输入颜色,就需要确保两者的处理路径完全对齐。
- 如果目标颜色是在一个平方空间中进行混合操作(例如
linear = srgb^2.0
),那么输入颜色也必须被平方才能保持一致性。 - 而由于我们当前对目标颜色的平方发生在 0–255 空间,因此输入颜色也应该以乘以 255 后的数值进入同样的处理流程。
四、现象说明
当前的猜想解释了为什么颜色在混合后看起来“怪异”或“偏暗”:
- 如果输入颜色没有先乘以 255 再平方,而目标颜色是这样处理的,二者之间的混合就不是线性空间下的正确结果。
- 也解释了为什么在 bitmap 路径中变化不大,但在 solid fill 路径中颜色差异显著:路径中的处理方式和预期不一致。
五、下一步建议
-
确认输入颜色的处理顺序:
- 是否在乘 255 后进行了平方?
- 是否进行了 gamma 编码,是否有必要取消?
-
统一处理模型:
- 明确所有路径中颜色处理的空间:线性 vs gamma,整数 vs 浮点。
- 目标和源颜色处理流程保持一致。
-
可视化验证:
- 通过输出中间颜色值、混合前后值,验证实际运行的处理路径。
-
参考 OpenGL 默认行为:
- 查询 OpenGL 是否启用了
GL_FRAMEBUFFER_SRGB
或默认进行 gamma 编码。 - 明确 GL 的颜色插值阶段是在 gamma 空间还是线性空间。
- 查询 OpenGL 是否启用了
整体来看,虽然结论可能有些“反直觉”,但从匹配渲染结果出发,这种输入颜色需要“乘以 255 后平方但不 gamma 编码”的做法是有可能正确的。要验证这一点,需要更系统地测试和对比 OpenGL 与自定义渲染路径的差异。
运行游戏,发现显示结果几乎完全正确
我们刚才注意到一个非常微妙但关键的渲染行为差异,而且几乎完全命中了渲染效果的误差来源。
这是一个非常细致的问题,在这类底层图形渲染代码中非常常见,但又极其容易忽略。我们之前一直在处理颜色值的线性化和 gamma 校正问题,而刚才的结果显示:我们的修正使渲染输出和预期几乎完全一致。
这是因为之前在处理颜色数据时,存在一个看似无害但实际上影响深远的错误:
- 我们的颜色值在处理时没有在正确的空间中进行操作,也就是说,我们没有统一好在 gamma 空间和线性空间之间的转换。
- 渲染中某个 subtle(微妙)的问题隐藏得很深,比如平方的位置、是否已乘 255、是否在进行 gamma 解码或编码,这些如果稍有偏差,最终输出的视觉效果就会明显不同。
- 在调试中,我们终于发现:原来问题出在了颜色在进入混合阶段前的处理方式不一致,导致和 OpenGL 的默认行为有所偏差。
我们自己不是传统图形渲染体系出身的,所以对这类“老派渲染路径”的隐含逻辑并不熟悉,这让我们在调试这类问题时往往比较吃力。
总之,通过这次修正:
- 我们已经非常接近 OpenGL 的渲染结果了;
- 而这背后的根本原因是颜色处理流程中的一个细节没有对齐;
- 这个细节最终被捕捉到了,这种细致的观察和验证是解决图形底层 bug 的关键。
这是一个很好的经验积累,也提醒我们在颜色处理和混合时要保持极高的精度和一致性。
黑板讲解:确保保持在同一色彩空间内(Color Space)
我们在用零到255的空间来进行混合操作,因为我们加载的是已经是8位的数值,不想无谓地转换成浮点数,所以保持在这个范围内比较方便。颜色值本来是0到1的范围,我们先乘以255,把它们转换到0到255之间,再进行平方操作实现gamma校正。
平方操作实际上把数值范围变成了0到(255的平方),这就是我们现在处理的范围。入色值先乘以255,再平方,理论上就达到了正确的范围。
问题在于,我们还需要将这个范围映射到0到(255平方)之间,而平方操作原本就是为了完成这个映射。但如果不想做gamma校正(也就是不平方),我们就得再额外乘以255一次,才能让数值进入和帧缓冲读取的数据相同的空间范围,而帧缓冲读取的颜色是已经经过gamma校正的。
这样一来我们就清楚了:OpenGL其实并不对颜色值做gamma校正,它只是在颜色上做乘法处理。换句话说,OpenGL传入的颜色值就是直接乘以alpha或者其它系数,没有额外的gamma校正。
接下来需要弄明白的是为什么我们没有正确设置这个值,明显是哪里出了问题。我们打算在渲染组里设置断点,检查哪里搞错了。因为我们现在是对每个颜色分量乘以alpha,得到一个预乘alpha的目标颜色值,但现实中渲染结果却没有达到预期。
我们期望这些颜色值能正确影响渲染实体,尤其是在“世界模式”下,根据摄像机位置对它们进行设置,并且这些颜色信息应该会传递给所有位图绘制调用。可是实际上这些颜色好像根本没有任何影响,说明我们的处理流程或设置环节中出现了严重错误。
总结来说:
- 我们使用了0到255的范围进行混合处理,避免不必要的浮点转换;
- 颜色值要经过乘以255,然后平方进行gamma校正,或者如果不校正,就需要额外乘以255来匹配帧缓冲读取的数据空间;
- OpenGL本身不做gamma校正,只做乘法;
- 当前代码在设置颜色预乘alpha值时存在问题,导致颜色未正确作用于渲染;
- 需要通过断点调试具体找出错误的环节,特别是在渲染组中颜色的设置和传递部分。
调试器:在 CameraRelativeP.z > FadeTopStartZ
时单步进入 UpdateAndRenderWorld
,检查颜色值
我们在调试代码时,决定在一个看起来最可能出现问题的地方设置断点,重点观察颜色和alpha混合相关的代码。我们特别关注了“fade top”这个alpha渐变的部分,因为它最明显地体现出问题。
调试时,发现传入的t值(代表alpha渐变的程度)大多数时间是零,这意味着许多对象可能处于视野之外或者根本没被渲染。为了调试方便,我们在代码循环里设置了条件断点,只在t值大于0.1时中断,以便观察有效的alpha渐变处理。
通过观察绘制调用,发现颜色和alpha的相关值都符合预期:
- t值为0.3,符合想象的alpha渐变;
- 目标颜色(destination color)的alpha值低于1,表示有透明度变化;
- 颜色通道(r、g、b)经过了正确的调制,反映了目标alpha的影响;
- 最终颜色被存储在预期的变量里。
但是,虽然代码中颜色和alpha的处理都符合预期,实际渲染结果却没有表现出任何alpha渐变效果。所有对象看起来都是完全不透明、满色显示,没有出现应该有的淡出效果。
我们传递的alpha值和颜色值都是正确的,理论上应该会看到逐渐变淡的视觉效果,但现实中完全没有。
因此问题在于渲染流程中虽然计算正确,但最终渲染结果没有体现这些变化。下一步需要进一步深入调试渲染流程,尤其是颜色和alpha值是如何被传递和应用到最终绘制中的,才能找到具体的错误原因。
总结:
- 设置了断点,重点看alpha渐变的t值和颜色混合值;
- 发现t值大多数是零,设置条件断点过滤有效数据;
- 颜色和alpha混合计算符合预期,目标alpha和颜色正确调制;
- 实际渲染中没有体现任何alpha渐变效果,所有东西都是不透明的;
- 表明渲染中某个环节没有正确使用计算出的alpha值;
- 需要更深入调试渲染管线,确认颜色和alpha的传递与应用流程,找出具体问题所在。
修改 game_opengl.cpp
:尝试以 50% 不透明度渲染所有内容
我们确认在渲染流程中没有做非常愚蠢的操作,比如OpenGL中直接用 glColor
设置颜色时出现的问题。为了测试,我们尝试将所有颜色的alpha值乘以0.5,理论上所有图形应该变成半透明。
测试结果显示,当我们手动将颜色乘以0.5时,矩形确实会变得半透明,这说明基础的颜色混合和透明度处理是有效的,没有根本性的错误。
接下来疑问就来了:为什么实体(entity)上用到的primal color
(基本颜色)仍然显示不正确?有可能是代码中某个地方忘记调用store color
函数来正确存储颜色值,这种遗漏在代码里很容易发生。
另外一个怀疑是颜色值可能被过早清空或重置了。比如在某些部分代码中我们有清空或重设t global
(全局透明度值)的操作,这可能影响后续渲染的透明度表现,但目前看起来这些操作不应该导致问题。
还尝试检查了draw hit points
部分,确认那里没有异常行为,也没发现颜色值被异常改动。
总结目前分析:
- 手动将颜色alpha乘0.5能正确显示半透明,说明颜色混合机制本身正常;
- 实体渲染中颜色显示错误,可能是某些地方漏掉了调用存储颜色的函数;
- 颜色值可能在某些阶段被过早清零或重置,但目前看起来这不是主要问题;
- 相关的渲染函数都用到了正常的颜色值,没有发现异常;
- 整体来看,问题很可能出在渲染流程中某些关键点的颜色存储或传递细节,需要进一步排查代码中颜色赋值和存储的完整性。
调试器:进入 OpenGLRectangle
,查看 Entry->PremulColor
的值
我们准备在OpenGL渲染流程中设置断点,特别是在处理“entity primo color”(实体基本颜色)的部分。目的就是观察这些基本颜色到底长什么样,为什么它们总是保持相同的状态。
通过调试,我们发现这些颜色没有多少被调制过(modulated),也就是说它们看起来几乎没有变化,这让人觉得有些奇怪。不清楚为什么所有颜色看起来都差不多。
我们推测这些颜色可能是一些已经绘制过的内容,因为渲染顺序是先绘制远处的元素,然后再绘制上层的内容,这可能影响颜色的显示情况。
总体来看,目前的重点是通过断点观察,试图理解为什么这些颜色没有出现预期中的变化,尤其是为什么颜色没有被正确调制,这可能是导致渲染结果异常的关键线索。
修改 game_render_group.h
:在render_group_entry_header
和 render_group
添加 DebugTag
在调试渲染流程时,为了更容易定位问题,可以采用一种技巧:给渲染组中的每个条目(entry)加上一个标签(tag),这样就能追踪某个特定条目从进入渲染管线到输出的整个过程。具体做法是在渲染条目的结构中添加一个调试用的标签字段,比如叫“debug tag”。
在渲染过程中,如果遇到某个条目的标签等于我们想要观察的特定值,就可以触发断点,这样就能暂停程序,检查这个条目当前的状态。推入渲染元素(render element)时,也会把这个标签赋给它的渲染组,这样整个渲染流程就能跟踪带标签的条目。
通过这个办法,我们可以很清楚地看到特定渲染条目的状态,确认它是否经过了预期的操作,或者是否在某个阶段出现了异常,从而大大简化调试难度,尤其是在缺乏完善调试工具的环境下,这种手工打标签、打断点的方法非常实用。
修改 game_world_mode.cpp
:当 CameraRelativeP.z > FadeTopStartZ
时设置 DebugTag
为 1
现在,为了调试那些带有淡入淡出效果的渲染项(例如 fade top start),我们打算使用一种更方便的方法,避免每次调试都必须手动追踪大量渲染条目。我们可以在推入这些特定渲染项时,为它们设置一个特定的调试标签值,比如设置为 1
,而其他渲染项则默认设置为 0
或不设置。
具体来说,在执行 push_bitmap
或相关的渲染推入函数时,如果当前处理的是带有淡出属性的 traversable,我们就将其渲染组的调试标签设置为特定值,例如:
render_group.debug_tag = 1;
这样,只有我们感兴趣的、带有淡出处理逻辑的 traversable 会被标记。当这些带有标记的条目进入渲染流程后,如果程序检测到某条渲染项的标签为 1
,我们可以在这时打断点或输出状态信息,从而只关注这些特定渲染项的行为。
最终,这种方法可以帮助我们更高效地定位问题。通过只关注那些拥有特殊属性(如淡出效果)且我们主动标记过的渲染条目,我们可以跳过无关信息,快速验证颜色是否被正确设置、alpha 混合是否生效、渲染输出是否如预期。这种方式不仅提高了调试效率,也为之后类似问题的排查提供了便利手段。
调试器:在 OpenGLRenderCommands
的 BreakHere
位置打断点
现在我们可以在 opengl_sympathy
中设置断点,观察特定路径下进入的渲染项,确认它们是否具有我们设置的预乘颜色值(pre-multiplied color),并且是否已经实现淡出(fade out)效果。例如我们看到其中一个颜色值为 0.666666...
,这表明它确实处于逐渐淡出的状态,表现是符合预期的。
但是,目前的问题在于:尽管部分颜色值表现正常,能够正确衰减,但仍然有不少渲染项进入该路径后颜色值看起来被“压扁”或钳制了,换句话说,它们没有如预期地渐变淡出,甚至可能直接失效或被处理成全黑、全白、或其他异常值。
目前的观察结果显示,某些颜色值看起来并不让人放心,可能存在以下几种可能性:
-
颜色值提前被钳制(clamp):在进入实际混合阶段前,颜色通道可能被钳制到了 0 或 1,导致 alpha 衰减不起作用。
-
预乘操作顺序不当:可能颜色值与 alpha 值的乘法在错误的阶段进行,或者乘法之后又有其他操作影响了它的值。
-
渲染路径存在多个分支:并非所有路径都应用了统一的颜色处理方式,某些渲染路径可能跳过了预乘、淡出处理或调试标签检测。
-
渲染后续过程覆盖了预期效果:例如绘制顺序或 z-order 排序错误,导致正确渲染的对象被错误覆盖。
我们可以进一步做的是:
- 检查所有进入该渲染路径的颜色值,确认是否每个都得到了我们期望的衰减值。
- 在设置预乘颜色的代码段中增加更多输出信息,记录每一个颜色值和 alpha 值的计算过程。
- 检查是否有地方在设置完颜色之后又错误地重写了颜色值,或是否有全局状态污染。
- 确认 OpenGL 状态(例如 blend 模式)是否在渲染中被正确设置。
这种现象说明我们可能只是部分解决了问题,还有一些边缘情况或隐藏路径尚未处理完全。需要继续细致排查。
修改 game_world_mode.cpp
:为渐变效果取 tGlobalColor
的 Alpha 值的相反数(负值)
我们在处理这个问题时,最终发现关键点出在对 t
值的使用上。这个 t
原本是用于控制颜色混合或透明度渐变的参数,但我们之前忽略了它的实际调制方向——它的变化是反过来的。
也就是说,t
实际上是随着物体靠近摄像机而逐渐减小的,因此我们用它来做透明度混合的时候,结果是:物体越靠近镜头,反而越淡出,远离时反而变得不透明。这正好与预期效果相反,造成了非常突兀的视觉跳变。
同时,由于这个变化过程非常迅速,靠近摄像机的物体淡出速度过快,我们在调试或视觉观察时很难察觉,造成了“为什么没有 fade”的错觉。实际上 fade 是发生了的,只是时机和方向都错了。
所以总结如下:
- 使用的
t
值控制透明度的逻辑是反的,fade 的方向设置错误; - 渲染系统本身可能已经按逻辑执行 fade 操作;
- 错误的方向导致靠近时物体淡出而不是出现;
- 视觉上看不到变化,是因为 fade 过程过快导致观察不到;
- 修复思路应当是:反转
t
的计算或使用方式,使其随着靠近而增加,从而达到靠近时变得清晰、远离时淡出的预期视觉效果。
这是一个典型的“逻辑正确但方向错误”问题。现在我们可以有的放矢地去调整 t
的生成或使用方式来实现正确的视觉表现。
1-t 反而不对
运行游戏,渐变效果正确显示
我们现在已经修复了物体在图层之间快速跳变时导致的显示问题,确保了不同层级之间渲染行为的正确性。接下来需要处理另一个方向上的淡出逻辑,这部分也存在类似的问题。
我们意识到,这部分的 t
值控制逻辑同样是反向的,也就是说,淡出行为目前依然与预期相反。为了修复这个问题,最直接的做法就是在使用 t
值时进行反转,即使用 1 - t
,以达到随着距离增加而逐渐透明的效果。
不过,我们考虑了当前使用的 clamp_map_to_range
方法,这个方法本身就可以接受一个“反向范围”,即开始值大于结束值的情况。在这种情况下,它可以自动生成一个从 1 到 0 的过渡值,而不需要我们手动去做 1 - t
的运算。
因此我们决定不手动反转 t
,而是直接通过调整 clamp_map_to_range
的参数顺序,利用其内部逻辑产生一个反向的渐变值,从而使得淡出行为自动完成正确的过渡。
总结如下:
- 修复了图层跳变的问题;
- 另一个方向上的淡出效果也有反向的
t
值问题; - 本可以使用
1 - t
修正,但我们采用了更优的方式; - 直接通过
clamp_map_to_range
的参数调整,生成反向渐变; - 这样避免了冗余计算,同时保持逻辑清晰;
- 现在淡出行为也能随着距离变化正确执行。
这一部分的修正进一步完善了我们的渲染逻辑,使透明度和图层行为更加自然和连贯。
调查为何下层区域什么也看不到
我们现在的任务是解决底层楼面在渲染中出现的问题。具体来说,底层在进行淡出时,本应朝着全局颜色(global color)渐变,但实际呈现出的效果却是朝黑色渐变,这显然不合理。
首先我们检查了用于控制淡出的 t
值的逻辑。这个 t
值应当是从 bottom_start_z 向 bottom_end_z 递增,也就是说随着高度的上升,t
值应该逐渐增大,这符合预期的渐变方向。因此,fade 的方向逻辑是正确的。
然而,尽管逻辑上看起来正常,实际渲染的结果却是错误的。我们观察到颜色逐渐变黑,而不是淡向预期的全局颜色。因此推测这里可能并没有使用正确的目标颜色(global color),而是默认地淡向了黑色。
为排查问题,我们首先验证了是否存在明显的数据错误,比如所有颜色值是否都为零。然而我们没有看到下层有任何有效颜色值,这让人不太安心。接下来我们尝试排除是否是平台层代码的问题,但平台相关代码本身是可靠的,不太可能是原因所在。
为了进一步确认淡出逻辑是否在正常运行,我们做了一个测试——如果我们将 t
设置为 0.0、0.5 或 1.0,然后完全关闭目标颜色(即不指定目标色),按照我们的理解,颜色应当保持不变,不应产生任何额外的混合效果。
因为这种情况下,唯一的处理逻辑就是用源颜色乘以源 alpha,这本来就是我们希望的预乘效果,因此颜色应该和未处理时一致,不会导致变暗或其他异常情况。
最终我们意识到,看起来“下方没有颜色”的原因,其实并不是渲染错了,而是这些区域本身根本没有任何有效内容。因此并非淡出逻辑或颜色混合出错,而是那些区域仅仅是被全局颜色清屏了,这种情况下看到的自然就是黑色。
所以总结如下:
- 底层淡出逻辑使用的
t
值方向正确; - 实际渲染结果却表现为渐变至黑色;
- 初步怀疑为目标颜色未正确设置;
- 测试排除了混合公式的错误,预乘计算是合理的;
- 发现底层区域并无有效图元渲染,因此显示为清屏颜色;
- 渲染结果其实是正确的,问题出在区域本身没有内容;
- 结论是无需修复,这种表现符合预期。
这一过程说明淡出机制整体运作是正确的,底层变黑仅仅是因为缺少可渲染内容而显示了默认背景颜色。我们可以放心继续后续开发工作。
修改 game_world_mode.cpp
:在 UpdateAndRenderWorld
中启用正确的混合方式
现在我们已经实现了一个向下俯视的视角,并成功开启了正确的混合(blending)模式。从目前的渲染效果来看,图像内容已经开始逐渐淡出,呈现出某种灰色的混合结果。
不过目前仍存在一个不确定性:淡出的目标颜色到底应该是什么?现在画面中淡出的颜色似乎是灰色,但并不确定这是否是最终期望的视觉表现。目标颜色的选择尚未明确,可能需要根据具体的美术需求、游戏氛围或其他上下文逻辑进一步调整。
目前的状态说明:
- 混合功能已正确启用;
- 渲染内容能够在视野中逐渐淡出;
- 当前的混合效果是从内容淡出到灰色;
- 淡出目标颜色暂未确定,可能需后续调试或设计决定。
整体来看,渲染流程已经进入一个正确的阶段,但最终的视觉细节仍需进一步确认和优化。
修改 game_world_mode.cpp
:初始化 BackgroundColor
,并将其传递给 Clear
和底层楼层的 GlobalColor
现在的目标是让画面淡出的颜色与屏幕清除时使用的背景色一致,以确保视觉上过渡自然。然而目前仍不完全确定最终想要的效果。
当前的处理情况如下:
- 清屏操作已经在特定位置执行;
- 清屏颜色(背景色)需要设置成我们希望淡出时趋近的颜色;
- 此外,需要将目标颜色的 Alpha 值设置为 1,以实现一种“雾气”或“薄雾”的视觉效果;
- 目前这个设置看起来可能有些太亮,不确定是否符合理想中的“雾气”表现,但视觉上稍微好看一些;
- 接下来需要在渲染中指定一个明确的背景颜色,并作为淡出目标色,这样在做颜色渐变(淡出)的时候,可以统一过渡到这个颜色;
- 淡出逻辑也需要基于这个背景色进行调整,确保最终效果自然且连贯。
整体来说,已经基本实现雾化或淡出的渲染流程,但还需要根据背景色进一步调整清屏参数和目标色调,以达到预期的美术效果。
运行游戏,观察雾化(Haze)效果显现
现在已经成功实现了雾化(haze)效果,并且视觉表现接近预期。
具体情况如下:
- 当前画面在远处逐渐出现雾化感,颜色向背景色过渡,看起来非常自然;
- 渐变效果生效之后,之前存在的半透明问题也已经解决,画面中的元素不再是不透明地叠加;
- 远景部分逐渐消失在背景色之中,整体过渡平滑;
- 整体视觉效果令人满意,远处的画面逐渐褪色的感觉清晰可见,符合设想;
- 当前的效果被认为是合理且可接受的,没有出现明显的偏差或bug;
- 渲染管线中的雾化处理已经初步定型,可以作为后续进一步完善和美化的基础。
总之,现在的雾化处理已经达到了预期目标,整体表现令人满意,基本可以认为阶段性目标完成。
问答环节
可以尝试将透视投影的中心设为玩家的位置,这样不同层之间的元素就会对齐
可以尝试的一种方式是将透视投影的中心设定为玩家的位置,这样其他楼层的元素可能会在视觉上对齐。但这个方法存在明显的问题:
- 虽然这样可以让场景中其他楼层与玩家当前所处位置在某一时刻对齐,但这种对齐是动态变化的,完全依赖于玩家的位置;
- 一旦玩家开始移动,后方的楼层也会跟着“移动”,因为透视中心随着玩家变化而变化;
- 这种移动会导致后方楼层在视觉上产生不稳定感,像是在漂浮或晃动,缺乏视觉参考的恒定性;
- 对于一个分层结构的场景,这种动态变化的对齐效果不仅不能提供清晰的空间感,反而可能会造成混乱与干扰;
- 从整体体验来看,这种方式并不理想,会带来违和感。
因此,尽管这种做法在技术上可行,但它带来的视觉副作用并不符合稳定、清晰的多层级透视展示需求,最终结论是这个方法不可取。
Hadamard 变换好像能做很多事,它在 game_world.cpp
中的 subtract
函数里具体做了什么?
Hadamard transform
这个结构似乎在多个功能场景下都有广泛用途。从用途来看,它看起来是用于非均匀缩放的变换操作。具体在 game_world.cpp
的 Subtract
函数中,它扮演的角色可能是执行某种坐标变换或缩放计算。
结合上下文,可以详细总结如下:
- 这个
Hadamard transform
在视觉变换系统中可能是一个核心工具,用于执行一些通用的空间变换; - 当前判断是在
Subtract
函数中它可能用来进行坐标值的缩放处理; - 特别是执行非均匀(即各个轴上比例不一致)的缩放时,这种结构会非常有用;
- 推测可能的用法是:给定两个世界位置或者坐标,想要计算它们在某种变换(例如缩放或旋转)下的差异,使用这个变换结构就能统一处理;
- 此结构未来可能还适用于更多需要局部坐标变换或投影计算的场景,例如摄像机视锥调整、碰撞检测中的空间映射等;
- 目前还需要查看具体实现细节以确认其精确用途,但其设计显然考虑了通用性和扩展性。
因此,Hadamard transform
并不是仅服务于单一功能的工具,而是一个具备通用变换能力的系统模块,在视觉层级、坐标处理等多个子系统中都具备潜在价值。
黑板讲解:Hadamard 积 ≈ 对角矩阵的乘法
在图形学中,经常需要对向量进行缩放操作。假设有一个向量,想让它在保持方向不变的情况下变长或变短,这时可以通过一个标量乘法来实现,比如将向量乘以一个标量c。如果c=2,向量长度变成原来的两倍。
但是,有时候希望对向量做非均匀缩放,比如只在x轴方向上变长两倍,而y轴方向保持不变,这样向量就会被“拉伸”成一个斜向的向量。这时,不能用简单的标量乘法,而是需要用矩阵来表示非均匀缩放。
非均匀缩放矩阵是一个对角矩阵,对角线上元素分别代表各轴的缩放系数,比如x轴是cx,y轴是cy,矩阵乘以向量后就实现了对不同轴不同的缩放。矩阵乘法就是将矩阵行和向量列对应元素相乘求和,最终得到缩放后的向量。
这里提到的Hadamard乘积(元素乘积)其实是矩阵对角线向量与输入向量的逐元素乘法。换句话说,Hadamard乘积等价于把一个向量当作对角矩阵的对角线元素,再与另一个向量相乘。这个操作在数学上不常见,但在计算机图形中用于表示非均匀缩放非常方便。
总结来说:
- 普通缩放是用标量乘法实现的向量长度变化;
- 非均匀缩放则是用对角矩阵实现的各轴不同缩放;
- Hadamard乘积是逐元素乘法,等同于用一个向量构造的对角矩阵与向量相乘;
- 这种方式在图形渲染代码中经常出现,虽然数学上少见,但因方便表示非均匀缩放而被使用。
顺便说一句,带 X 的圆圈表示张量积(Tensor Product)
在讨论符号“×”时,通常有人认为它代表张量积(tensor product),但其实并不一定如此。回忆来看,常见的符号“×”更多是用来表示某种算子(operator),而不是特指张量积。
很多教材中,引入任意算子时会使用类似的符号,所以“×”并没有一个固定的、唯一的数学含义,它的具体意义往往取决于所处的数学分支和上下文环境。
虽然“×”符号很可能被用来表示张量积,但也可能表示其他很多不同的操作,具体含义需要结合上下文才能确定。因此,不能简单地认为带圈的“×”一定是张量积符号,而是一个多义的符号,含义依赖于具体情况。
实现带纹理或不同颜色的雾是否困难?比如能不能做出渐暗边缘效果(Vignetted Fog)?
实现带纹理或者不同雾颜色的效果,比如说做一个晕影雾效(vignetted fog),技术上并不难,但实现方式和当前的做法会有明显不同。
目前的代码中,比如在渲染房间的部分,雾的颜色计算只是在初始化时运行一次,通过几行代码完成,这样做效率高且简单。
如果要实现根据位置或其他因素动态调节雾颜色,甚至实现两种颜色之间的渐变,就不能只在初始化时设置单一颜色了。需要在真正渲染阶段进行计算,动态修改颜色。
这就需要使用额外的纹理单元,或者编写带有程序化效果的着色器(shader),比如说晕影效果可以通过程序计算而不是纹理实现。
具体来说,不再只是简单地设置一个颜色用来调节纹理颜色,而是要在渲染时根据屏幕空间的某些数据(比如额外的纹理或计算结果),动态地调整混合系数或颜色。
这在技术上不是特别困难,但会增加渲染流程的复杂度,需要多一层屏幕空间的数据源和处理,整体实现会比现有方案复杂不少。
是怎么发现 gamma 校正错误的?是哪一个线索让你恍然大悟?
发现伽马校正问题的过程并不是瞬间灵光一现的时刻,而是通过一系列细致排查逐步缩小问题范围的结果。最初对整体情况有一个大致的预期,但会忽略一些细节,比如范围被平方处理了。在测试时关闭伽马校正后,结果和预期不符,感觉非常奇怪,明明应该匹配的两个数值却不匹配。这种异常促使进一步深入调查。
当明确了范围平方的问题后,重新用正确的范围且不进行伽马校正,结果才正确呈现出来。这个发现过程是渐进的,通过一步步排除错误和不匹配,最后留下了那个明显的线索,让问题变得一目了然。整个经历是一连串缩小怀疑范围的步骤,直到真正的问题浮现出来,而不是靠某个单一瞬间的灵感解决。