0%

绝区零角色实现分析(蕾米截帧)

由 astra 生成 · 更新于 2026-09-27 18:44(北京时间)

分析日期:2026-09-27。分析蕾米所在大厅中的角色变形、脸部着色、眼睛合成、阴影分类和运动输出。

本文独立展开角色实现。公共光照、反射、雾和整帧后处理见 绝区零场景分析;完成单游戏分析后的横向结论见 角色实现对比。

配图直接来自真实回放,关键效果用同一区域的阶段前后图解释。局部图已统一到便于阅读的方向,HDR 配对使用一致显示范围;最终画面另作外观参照。公式和通道按实际读写解释,未确认的部件归属明确保留。

1. 几何准备把变形、细分与材质求值分开

形态混合从稀疏增量库读取受影响顶点,把位置、法线和切线增量累加到可变顶点数组。后续几何阶段消费这个结果,材质求值不必重新遍历每个形态目标。

翅膀另有计算细分路径,拆为三个职责:先按距离与拓扑确定细分强度,再生成/约束边上的新位置,最后按被拆分的边生成三角形连接关系。这样得到的是可以直接交给绘制的顶点和索引,而不是让材质像素程序改变轮廓。

人群动画和其他几何准备也与主着色分开;目前尚未还原所有骨骼字段和每个网格的变形归属。可明确移植的稀疏记录、边规则及输出分配见第 5–6 节。

2. 脸、眼和透明部件承担不同的着色职责

部件或路径 生成的数据 为什么单独处理
脸部主着色 阴影和材质调色后的 HDR 颜色、控制量、法线与运动 脸部明暗已按专用规则求值,后续不能把它当作普通未着色底色
脸部阴影几何 光源方向的深度 向其他受光表面提供遮挡,不生成当前视图颜色
阴影分类写入 只更新指定模板位 在保留已有材质分类的同时,附加后续遮蔽需要的状态
眼睛合成 预乘覆盖率的颜色及受控的材质输出 覆盖眼部颜色时保留所需底层数据,避免把法线和其他材质量一并抹掉
透明部件 在已有场景颜色上追加可见贡献 合成时要分别管理颜色、深度与运动,不能照搬不透明身体的状态

角色还参与镜像视图,因此反射中的可见形状也需要与变形结果一致。主视图和镜像视图分别求值各自的可见性与着色。

3. 脸、眼和 stencil caster 的管线状态不同

处理职责 深度行为 模板分类规则 颜色行为
脸部主着色 GreaterEqual,写深度 Always / Replace,ref=128(0x80),writeMask=255 写入角色颜色、材质数据与法线,混合关闭
阴影分类写入 GreaterEqual,不写深度 Equal,compareMask=128,ref=132(0x84),Replace,writeMask=4 所有颜色目标保留原值,只改变允许写入的模板位
眼睛合成 GreaterEqual,不写深度 关闭 HDR 颜色按 One / InvSrcAlpha 合成;第一材质图直接写;第二材质图和法线保留原值

阴影分类路径只检查模板 bit 7,并只更新 bit 2:先确认像素属于允许处理的角色类,再叠加局部状态,其他分类位保留。眼睛则对颜色附件分别设置混合和写掩码,覆盖可见颜色时保留底层表面数据。

这里根据状态确认合成与分类行为,不从 stencil 值进一步猜测所有材质位域。恢复角色路径时,shader、附件顺序、独立 blend、深度写入和 stencil 掩码需要一起恢复。

4. 逐对象阴影与胶囊遮蔽怎样接入

对象级阴影解析包含分类准备与屏幕结果处理。分类步骤可以只写模板、不写颜色,先确定后续允许影响的像素范围;不能把分类准备本身理解成已经生成了阴影强度。

胶囊遮蔽读取颜色、相机深度、法线与实体几何数据。它先从深度恢复表面位置,再计算该位置与胶囊线段、半径的关系,用简化几何近似物体造成的局部遮挡。屏幕 AO 主要沿可见深度搜索邻居,胶囊路径则额外知道遮挡物的近似空间形状。

两者可以为同一表面提供不同来源的遮蔽。胶囊与每个骨骼/实例的对应仍未完全解码;公共 AO 与阴影链见 绝区零场景分析。

5. 形态混合的稀疏顶点布局与原地累加

蕾米的形态混合计算不是把整个网格乘一个变换。每条输入记录长 40 bytes,只描述一个受影响顶点及三组增量;输出顶点步长也为 40 bytes。

输入记录中的内容 字节布局 实际操作
目标顶点索引 开头 4 bytes 决定修改输出顶点数组中的哪一项
位置增量 随后的 3 个 float 加到输出顶点的位置字段,输出偏移 0
法线增量 下一组 3 个 float 加到输出顶点的方向字段,输出偏移 12
切线方向增量 最后一组 3 个 float 加到输出顶点的另一方向字段,输出偏移 24;此 kernel 不改其后的第 4 分量

用语义变量表示实际指令:

1
2
3
4
5
6
if threadIndex >= affectedVertexCount: return
delta = morphDeltas[startRecord + threadIndex]
vertex = deformedVertices[delta.vertexIndex]
vertex.position += morphWeight * delta.positionDelta
vertex.normal += morphWeight * delta.normalDelta
vertex.tangent += morphWeight * delta.tangentDelta

工作量按受影响顶点数分派,越界线程立即退出;每个形态目标的权重直接缩放其增量。此 kernel 内没有把累加后的法线或切线归一化,也没有原子浮点累加。由此可知:底层顶点必须先有基础值,重复命中同一输出顶点的更新需要由数据组织或调用顺序保证;不能把多个会冲突的形态任务任意合成一个并行写入。

证据:稀疏形态混合程序、形态混合常量。位置/方向字段的命名结合记录布局和后续顶点用途,确切 loader 结构体名并未保留。

6. 翅膀细分:距离控制、边处理与 GPU 生成索引

细分主计算实际读取蕾米翅膀顶点,结合边/邻接表生成新顶点。输入顶点按 10 个标量,即 40 bytes 索引,输出则按每顶点 3 个 float4,即 48 bytes组织;不能复用原顶点 stride 读取细分结果。

这三个 kernel 各有独立职责:

  1. 细分主计算复制已有法线/切线,生成边顶点方向,并按空间距离控制细分强度。边端点索引会从一个 32-bit 整数的低/高 16 位拆出,部分附加数据用半精度浮点解码。
  2. 边位置更新先算端点中点;有完整邻接且需要平滑时,生成与 Loop 边规则一致的候选:平滑点 = 3/8 × (端点甲 + 端点乙) + 1/8 × (对面点甲 + 对面点乙)。随后按边强度在中点与平滑点之间插值。
  3. 索引生成根据一条原三角形有几条边需要拆分,输出 1–4 个三角形;额外索引数为 3 × 被拆边数。

距离控制部分可以提炼为:

1
2
3
4
距离 = length(顶点位置 - 参考位置)
投影尺度 = 半精度几何尺度 / 距离 * 投影比例
距离权重 = saturate((投影尺度 - 起始阈值) * 过渡斜率)
允许细分强度 = min(6-bit 强度值 / 63, 距离权重 * 分支倍率)

分支倍率可取 1 或 3;本帧代表调用中的投影比例约 1978.1837、起始阈值约 0.7、过渡斜率约 5.0。因此它有屏幕尺度/距离相关控制,不能只称为“网格离线预细分”。这些量的距离单位是捕获引擎单位,不自行换算为米。

边 kernel 还有一条受位图控制的约束分支:将“平滑候选减中点”的位移投影到归一化方向后再加回中点。它会改变细分点移动方向,不能删掉此分支只保留标准 Loop 公式。

索引分配采用两级计数:每组 64 线程先在共享内存累加额外索引数,由组内一个线程对全局计数器做原子加法,取得整组输出区间;每个线程再用组内偏移写入。程序有明确的共享内存同步点。这样减少全局原子操作,并允许结果直接供间接绘制使用;本帧未还原所有间接参数生成步骤,不能把这一个计数器当成完整的 GPU 场景剔除系统。

证据:细分主计算、边更新、三角形索引生成、本帧细分参数。

7. 脸部的材质输出与场景材质并非同一通道定义

脸部程序一开始就计算当前与前一位置的屏幕差,并将结果写进第二材质参数图的 RG;场景普通材质在相同槽位写的却是材质参数。后续延迟阶段必须根据分类重新组织最终运动/身份图。

脸部输出 通道内容 后续解释
HDR 颜色图 RGB 为阴影、实体光照与材质调色之后的脸部颜色;A=1 后续继续累积/合成颜色
第一材质控制图 RGB 写入同一个经平方根压缩的材质控制量;A=0 不是脸部底色贴图
第二材质参数图 RG 为有符号平方根编码的屏幕位移;B 为四个布尔条件组合后的低位标志/255;A 根据分支为 0 或约 0.34 需要分类解码,不能按普通场景材质参数解释
法线图 RGB=0.5×单位法线+0.5;A=1 RGB 用于方向计算,A 与其他材质分支区分

当前/前一裁剪坐标先除以 w,屏幕差乘 (0.5,-0.5) 转为纹理位移,再做有符号平方根编码,零点约 0.498039。当动态条件关闭时,RG 写接近零运动的常量。这里的 y 翻转是生产者的一部分,不能额外再重复翻转一次。

脸部颜色还包含一段可确认的 HSV 型处理:求最大/最小通道得到色相和饱和度,按明度与饱和度改变阴影用颜色,再重建 RGB。它不是简单 底色 × 固定阴影色。角色叠加贴图也有开关;本帧材质 的该开关为 0,说明“程序绑定叠加纹理”不等于本帧这条分支对颜色有贡献。

证据:脸部程序、材质参数与开关。

8. 脸部阴影:法线偏移、九点 PCF 与级联采样

脸部同时接入一张附加阴影深度图和主相机阴影缓存数组。附加阴影路径先把表面沿法线偏移,再变换到光源空间、映射到阴影子区域,最后做比较采样。这里“比较采样”返回遮挡通过率,不是直接把深度值当作明暗。

附加深度图的核有 9 次比较采样,以一个 texel 为基础偏移:

1
2
3
4
中心
四个对角:(±1, ±1)
四个轴向:(±√2, 0)、(0, ±√2)
阴影通过率 ≈ 0.1111 × 九个比较结果之和

本帧纹理尺寸为 2048²,基础偏移是 1/2048;程序中的 0.1111 保留捕获近似值,不能在要求逐像素一致时随意替换常量。通过率再与材质遮蔽量、强度和开关组合;阴影不是在计算完后无条件乘到所有脸部颜色上。

主相机阴影路径通过表面到四个参考中心的平方距离和范围阈值选择层级,随后对数组中的对应层做采样。采样偏移使用从投影位置生成的扰动角;这会影响边缘的噪声/软化模式。附加阴影图与级联缓存是两种输入,不能只恢复其中一张图便认定脸部阴影完整。

证据:同一 脸部程序 的附加阴影与级联分支;本帧阴影 texel 参数。

9. 眼睛:预乘颜色合成与独立角色色彩查找表

眼睛合成之前 眼睛合成之后 最终脸部效果
眼睛合成之前 眼睛合成之后 最终脸部效果

直接看两眼的眼白区域。 前两图取相同 HDR 区域和显示设置:合成后出现紫色虹膜、瞳孔与眼内亮点,脸部其他明暗基本不变,说明眼睛作为独立颜色贡献接入。第三图包含后续着色、抗锯齿和颜色处理,用于对照最终外观;不能把它相对中图的全部变化都归因于眼睛程序。

眼睛输出为 RGB = 眼睛颜色 × 覆盖率、A = 覆盖率,与管线上的 One / InvSrcAlpha 配合,形成预乘透明合成:

新颜色 = 已预乘眼睛颜色 + 原场景颜色 × (1 - 眼睛覆盖率)。

如果改成普通 SrcAlpha / InvSrcAlpha,颜色会再乘一次覆盖率,边缘就会偏暗。眼睛对其他材质目标的写入策略也不同:法线和第二材质图保留原结果,而第一材质图的本次输出是零,不能统一开启所有 MRT 的 alpha blend。

眼睛程序还读取 1024×32 RGBA16F 角色 LUT。这是铺成二维条带的 32 级颜色立方体:一个颜色轴选相邻切片,另外两个轴在切片内定位,执行两次纹理采样后按小数部分插值。在查表之前还有压缩变换:

1
2
查表输入 = saturate(log2(曝光缩放后的颜色 × 5.555556 + 0.047996)
× 0.0735 + 0.386036)

因此“角色有单独 LUT”意味着角色颜色在最终整帧后处理之前就可有专用颜色变换,而不是给最终画面再套一遍统一滤镜。该查表位于条件分支内,不把 shader 中存在的能力等同于每个像素必然执行。

证据:眼睛着色与查表程序。

10. 分析边界与证据

已展开稀疏形态记录、翅膀边规则、脸部阴影与眼睛查表;其余准备数据到每个骨骼/网格的完整对应、其他脸部与透明变体的全部公式仍待还原。不能把翅膀细分规则推广为每个角色的统一几何实现。

角色绑定证据 保存原始状态,资源语义索引 对照数据用途;捕获编号和提交统计只用于附录定位。