护甲 / 闪光 / 玩家反射(armor & glint)
基本信息
| 属性 | 值 |
|---|---|
| 包 | com.gtnewhorizons.angelica.rendering |
| 本条目覆盖 | 6 个文件 |
| 总行数 | 385 + 72 + 45 + 38 + 31 + 5 = 576 |
| 主题 | 护甲贴图路径、护甲层几何、凋灵/爬行者光效叠加层、附魔闪光矩阵、玩家世界空间反射数据 |
这 6 个文件没有共同基类,但共享同一个 mixin 家族:early/angelica/entity/ 与 early/angelica/bugfixes/ 下的护甲/闪光相关 mixin。它们解决的是同一类问题 —— 1.7.10 的实体渲染缺少现代版(Minecraft 1.13+)着色器包假定存在的结构。
| # | 文件 | 行数 | 可见性 |
|---|---|---|---|
| 1 | PlayerReflectionCapture.java |
385 | public final |
| 2 | GlintClock.java |
72 | public final |
| 3 | SkippedGlintBlock.java |
45 | public final |
| 4 | ArmorTexturePaths.java |
38 | public final |
| 5 | ModelHorseArmor.java |
31 | public |
| 6 | WitherArmorState.java |
5 | public |
PlayerReflectionCapture:合成 12 盒玩家模型
问题陈述(源码注释 :42-45)
Feeds the shaderpack’s world-space player reflection, which expects the modern 12-box player model: 288 vertices over a 64x64 skin layout. 1.7.10 has 7 boxes and a 64x32 layout, so both are synthesized here.
即:着色器包的「世界空间玩家反射」假定玩家模型是现代版的 12 盒、288 顶点、64×64 皮肤;1.7.10 原版是 7 盒、64×32。这个类同时合成几何和贴图布局。
常量(源码实值 :48-57)
| 常量 | 值 | 行 | 推导 |
|---|---|---|---|
VERTEX_COUNT |
288 | :48 |
给定 |
FLOATS_PER_VERTEX |
5 | :49 |
x, y, z, u, v |
PART_COUNT |
12 | :50 |
给定 |
VERTICES_PER_PART |
24 | :51 |
288 / 12 |
RAW_INTS_PER_VERTEX |
8 | :53 |
5 个 float 位 + 3 个打包通道 |
ATLAS_LAYOUT |
64 | :55 |
目标皮肤图集边长 |
VERTEX_FLAGS |
VertexFlags.convertToFlags(true, false, true, false) |
:57 |
有位置、有纹理 |
VERTICES_PER_PART = 24 = 6 面 × 4 角(emitBox :188-206 的双层循环 q < 6 × c < 4)。
12 槽 = 6 部件 × 2 层
partOrder(ModelBiped, ModelRenderer[])(:141-155)硬编码 12 个槽位:
| 槽 | 字段 | 槽 | 字段 |
|---|---|---|---|
| 0 | bipedHead |
6 | bipedLeftArm |
| 1 | bipedHeadwear |
7 | bipedLeftArm |
| 2 | bipedRightArm |
8 | bipedRightLeg |
| 3 | bipedRightArm |
9 | bipedRightLeg |
| 4 | bipedLeftLeg |
10 | bipedBody |
| 5 | bipedLeftLeg |
11 | bipedBody |
同名字段被故意填两次。 6 个唯一部件:head / headwear / rightArm / leftArm / rightLeg / leftLeg / body 中的 5 个被填两次(bipedHead 只填一次 —— 槽 0、1 分别是 bipedHead 和 bipedHeadwear,是不同字段)。这对应现代版的独立护甲层:帽子、外套、左右袖、左右裤。1.7.10 的 ModelBiped 没有这些独立字段,所以复用同一个 ModelRenderer 引用占两个槽。
⚠️ partOrder 的映射不是从任何常量推导的,是手写数组。1.7.10 的 ModelBiped 字段若被改名或语义调整,这里会静默产出错误布局 —— 编译期无保护。
12 个常量槽的填充值(flushInner :226-240)
rawData 首次分配时,每顶点的第 5/6/7 个 int 被写成固定魔数:
| 索引 | 值 | 语义(据源码内注释) |
|---|---|---|
d + 5 |
0xFFFFFFFF |
color |
d + 6 |
0x00008000 |
normal |
d + 7 |
0x00F000F0 |
light |
normal = 0x00008000 是「Y 轴正向单位向量」打包成 3 个 10-bit 分量;light = 0x00F000F0 把 sky/block light 都设成满值。这两个是占位值,不是计算结果 —— 玩家反射不需要真实法线/光照。
顶点格式转换
flush()(:206-224)的三道前置检查,任一不满足就静默返回:
| 检查 | 行 | 作用 |
|---|---|---|
Iris.enabled |
:209 |
Iris 全局开关 |
pipeline instanceof DeferredWorldRenderingPipeline |
:215 |
必须延迟管线 |
GLStateManager.getActiveProgram() == pipeline.getActivePassProgramId() |
:216 |
当前激活 program 必须就是该管线的 pass program |
第 3 条意味着:若在错误的 program 下调用 flush(),数据被静默丢弃。flush() 由 mixin 在正确时机调用;调用点写错不会报错,只会没效果。
flush 全程包在 Tracy zone Z_PLAYER_REFLECTION_FLUSH(:59,颜色 COLOR_IRIS)里,且用 try/finally 保证 endZone(:218-223)。
状态机
| 字段 | 语义 |
|---|---|
active |
begin() 找到 ModelBiped 目标才为 true |
captured |
emitAndSubmit() 已跑过 |
pending |
已提交待 flush |
shouldCapture() |
active && !captured(:124-126)—— 每帧只捕获一次 |
begin(:100-115)要求 RenderManager.instance.getEntityRenderObject(player) 是 RendererLivingEntity 且 living.mainModel instanceof ModelBiped。非 biped 模型(村民、盔甲架)直接 active = false。
emitAndSubmit(:128-140)在填充 vertexScratch 后把 modelview、当前皮肤纹理 id、当前实体 id、当前手持物 id 快照到 pending*,然后置 pending = true。这一段不在 GL 上下文里执行,真正的 GL 调用推迟到 flush()。
直接内存
类用 gtnhlib MemoryUtilities 的 memAlloc / memFree / memAddress0 静态导入(:38-40),配 repackAddress / repackCapacity(:91-92)和 ensureRepackCapacity 做容量复用。
⚠️ memFree 只在容量不足时释放旧块重分配(ensureRepackCapacity)。类里没有 shutdown 钩子释放 repackBuffer / skinPixels 的直接内存 —— 它们是 static final/长期存活的,生命周期等于游戏进程。这是刻意的(避免每帧分配),但也意味着 64×64 皮肤副本(skinCopyWidth × skinCopyHeight,:88-89)会常驻显存/堆外。
GlintClock:附魔闪光的帧级矩阵
附魔闪光(enchantment glint)在 1.7.10 是每帧用「当前时间 + 纹理矩阵」算出来的 UV 偏移。GlintClock 把它每帧算一次,供护甲层、手持物层、TESR 批处理复用。
常量(:9-14)
| 常量 | 值 |
|---|---|
SPEED0 |
0.001 |
SPEED1 |
0.001 + 0.003 = 0.004 |
SIN0 / COS0 |
sin/cos(30.0°) |
SIN1 / COS1 |
sin/cos(-30.0°) |
两个 30° 旋转、两个速度(0.001 与 0.004),是 1.7.10 原版 RenderManager 闪光纹理矩阵的双层结构的复刻。SPEED1 写成 0.001 + 0.003 而不是 0.004,是为了让两个速度的差值关系在源码里可见。
beginFrame 的换算(:20-37)
final double ticks = nowMillis / 50.0; // ms → 20 tick/s 的「刻」
final double f0 = ticks * SPEED0 * 20.0;
final double du0 = frac(-f0 * SIN0 / 3.0);
final double dv0 = frac(f0 * COS0 / 3.0);
armorU0 = (float) (3.0 * (COS0 * du0 + SIN0 * dv0));
armorV0 = (float) (3.0 * (-SIN0 * du0 + COS0 * dv0));
frac(:67-69)是 x - floor(x),把结果压到 [0,1)。3.0 的乘子对应原版 1.7.10 闪光贴图的 3×3 图集。
两张矩阵在 :35-36:
armorFirst.scaling(0.33333334f).rotateZ(30°).translate(armorU0, armorV0, 0);
armorSecond.scaling(0.33333334f).rotateZ(-30°).translate(armorU1, armorV1, 0);
⚠️ 0.33333334f 不是 1/3。1.0f/3.0f 在 float 下恰好是 0.33333334,但这里写的是字面量。若有人把它「修正」成 1.0f/3.0f 结果相同;若改成精确的 0.3333333f(7 位有效数字)则矩阵不再精确是 1/3 缩放,会引入极小的 UV 误差。别动。
关键 API
| 方法 | 行 | 语义 |
|---|---|---|
beginFrame(long nowMillis) |
:20 |
每帧一次,nowMillis / 50.0 换算 |
secondArmorMatrix(Matrix4fc first) |
:39-41 |
若 first 与 armorFirst 相等(eps = 0.000001f)返回 armorSecond,否则 null |
firstArmorMatrix() / secondArmorMatrix() |
:43-49 |
直接取矩阵引用(不复制,调用方不得改) |
secondArmorLayer(Matrix4fc) |
:55-57 |
textureMatrix.m01() < 0f —— 用矩阵的一个元素判层 |
translateArmorGlint() |
:59-65 |
调 GLStateManager.glTranslatef(armorU0/U1, armorV0/V1, 0f) |
⚠️ secondArmorMatrix(Matrix4fc) 的返回值可能是 null。调用方必须判空,否则 NPE。这是本类最容易踩的坑。
⚠️ secondArmorLayer 用 m01() < 0f 判层 —— 依赖 JOML 的 m01 是「第 0 列第 1 行」这一布局约定。这不是语义化判断,是数值技巧。
firstArmorMatrix() 返回的是静态可变对象。任何调用方就地修改都会污染全局状态。
SkippedGlintBlock:跳过闪光时的状态补偿
语义(源码注释 :9-11):> The state a vanilla glint block leaves behind, for callers that skip the block. GLSM is only called where its effective state differs.
即:批量渲染时如果跳过了某个闪光块,调用方必须手动补齐它本来会留下的 GL 状态。applyExitState(:26-44)的每个 GLSM 调用都先判「实际状态是否真的不同」,不同才调 —— 避免无谓的 GL 调用。
两个预置出口状态
| 方法 | 行 | RGBA | 混合 RGB | 混合 Alpha |
|---|---|---|---|---|
applyArmorExitState() |
:18-20 |
1,1,1,1 |
SRC_COLOR, ONE |
SRC_COLOR, ONE |
applyHeldItemExitState() |
:22-24 |
0.5×0.76, 0.25×0.76, 0.8×0.76, 1 |
SRC_COLOR, ONE |
ONE, ZERO |
手持物出口状态的 RGB 是 0.76 乘过的颜色(紫红色),不是原版常量。0.76 是源码里的字面量(:23),其来源是 1.7.10 ItemRenderer 的闪光染色,无注释说明来历。
出口动作清单(按源码执行顺序)
| # | 条件 | 动作 | 行 |
|---|---|---|---|
| 1 | 颜色与目标 4 分量任一不等 | glColor4f |
:27-30 |
| 2 | resetTextureMatrix 且单位 0 矩阵非单位 |
glMatrixMode(GL_TEXTURE) + glLoadIdentity() |
:31-34 |
| 3 | 矩阵模式非 GL_MODELVIEW |
glMatrixMode(GL_MODELVIEW) |
:35 |
| 4 | restoreDepthMask 且 depth mask 无效 |
glDepthMask(true) |
:36 |
| 5 | 光照未启用 | glEnable(GL_LIGHTING) |
:37 |
| 6 | 有效混合态未知或 4 个混合因子不等 | tryBlendFuncSeparate(...) |
:38-41 |
| 7 | 有效混合已启用 | glDisable(GL_BLEND) |
:42 |
| 8 | 深度函数非 GL_LEQUAL |
glDepthFunc(GL_LEQUAL) |
:43 |
第 1 条无条件检查颜色(4 分量 != 逐个比),第 2/4 条按调用方传入的布尔决定。applyArmorExitState 传 resetTextureMatrix = true, restoreDepthMask = true;applyHeldItemExitState 传 false, false。
第 6 条的判据含 getBlendState().isFuncUnknown() —— 这是 GLSM 状态跟踪里「尚未查询过 GL 混合函数」的哨兵。未查询过就重设一次,属于自愈。
blend 字段(:14)是 private static final BlendState,复用避免每帧分配。
ArmorTexturePaths:护甲贴图路径缓存
38 行,纯字符串拼接 + 一层手写缓存。目标是消除渲染热路径上的字符串拼接分配。
private static final int STRIDE = 5; // :6
private static String[] table = new String[0]; // :7
缓存布局(:24 的下标算式):
k = base + 1 + (slot == 2 ? 1 : 0) + (type == null ? 0 : 2)
base = renderIndex * STRIDE
| 槽位偏移 | 含义 |
|---|---|
+0 |
该 renderIndex 的 prefix(作为有效性标记) |
+1 |
layer 1(slot != 2) |
+2 |
layer 2(slot == 2) |
+3 |
layer 1 + overlay |
+4 |
layer 2 + overlay |
STRIDE = 5 正好是 1 个标记 + 4 个组合。slot == 2 是魔数(对应 1.7.10 EntityEquipmentSlot 的脚部),不是枚举比较。
缓存失效逻辑(:17-23)
if (t[base] != prefix) {
t[base + 1] = t[base + 2] = t[base + 3] = t[base + 4] = null;
t[base] = prefix;
}
用 t[base] != prefix(引用比较,不是 equals)判断该 renderIndex 是否换过贴图前缀。换过就作废 4 个组合。引用比较在这里是安全的:调用方传的是常量字符串(见 MixinRenderBiped_ArmorResource),同一资源前缀是同一常量对象。
⚠️ 若调用方每次都 new String(prefix),缓存永远失效,退化成每次都拼字符串 —— 不会出错,但优化完全失效且无任何日志。
路径格式(:33-35)
"textures/models/armor/" + prefix + (slot == 2 ? "_layer_2" : "_layer_1") + (type == null ? ".png" : "_" + type + ".png")
只有一个 overlay 类型会走缓存(:10 的 type != null && !"overlay".equals(type) 直接 format 不缓存)。任意其它 type 字符串也能进,只是不缓存。
prefix == null 也直接 format(:10),会拼出 textures/models/armor/null_layer_1.png —— 不抛异常,只是一条坏路径。
ModelHorseArmor:膨胀的马铠模型
public class ModelHorseArmor extends ModelHorse { // :16
public ModelHorseArmor(float inflate) { // :18
super();
for (ModelRenderer renderer : this.boxList) {
List<ModelBox> originalBoxes = new ArrayList<>(renderer.cubeList);
renderer.cubeList.clear();
for (ModelBox box : originalBoxes) {
int width = Math.round(box.posX2 - box.posX1);
int height = Math.round(box.posY2 - box.posY1);
int depth = Math.round(box.posZ2 - box.posZ1);
renderer.addBox(box.posX1, box.posY1, box.posZ1, width, height, depth, inflate);
}
}
}
}
策略(源码注释 :10-15):继承 ModelHorse 拿到全部动画与渲染逻辑,构造器先建默认几何,然后用 inflate 重建每一个 box,作为独立护甲层用。
Math.round(posX2 - posX1)把float坐标差取整。若原 box 坐标非整数,取整会引入最多 0.5 的误差。addBox(..., inflate)是 1.7.10ModelRenderer的原版 API,inflate语义是「四边各向外扩inflate格」。renderer.cubeList.clear()前先拷贝到originalBoxes—— 避免迭代时修改同一个 list。
⚠️ 重建会丢失 ModelBox 上的非几何状态(若 mixin 在 ModelBox 上挂了额外字段)。addBox 产生的是全新 ModelBox,原对象被丢弃。Angelica 自己在 mixins/interfaces/ModelBoxQuads 上给 ModelBox 挂了 angelica$getQuadList()(见 PlayerReflectionCapture 的 quadsOf,:165-170)—— 新 box 上没有这个 quad 列表,需要 mixin 侧补齐。这是本文件与 PlayerReflectionCapture 之间一条源码里没写出来的隐式耦合。
boxList 是 ModelHorse 的字段(1.7.10 是 public List<ModelRenderer> boxList)。构造器直接遍历继承来的可变字段,而 super() 刚跑完 —— 顺序正确。
WitherArmorState:5 行的跨 mixin 标志
public class WitherArmorState { // :3
public static boolean pendingInflate = false; // :4
}
整个类只有 1 个 public 静态 boolean,5 行(含 package + import 占位)。
public static 可变字段,无 volatile、无封装、无注释。两个使用者:
| 使用者 | 文件 |
|---|---|
| 写/读侧 | mixins/early/angelica/bugfixes/MixinRenderWither_ArmorCentering.java |
| 另一侧 | mixins/early/angelica/bugfixes/MixinModelWither_ArmorCentering.java |
「 ArmorCentering 」(凋灵护甲居中)是把 ModelWither 的护甲 box 相对本体平移的 bug 修复。这两个 mixin 之间需要传一个「本次是否要施加 inflate」的一次性标志,WitherArmorState 就是那个跨类传参槽。
⚠️ 这是典型的「用 static 字段当方法参数」的 mixin 惯用法,有三个实际风险:
- 无 volatile。若两个 mixin 在不同线程(渲染线程 vs 其它)执行,可见性无保证。实际都在渲染线程,风险低但按 JMM 仍是未同步的。
- 不自动复位。写
true的一侧若在读侧读到之前抛异常,标志残留true,下一次调用会被误判。需要读侧用完手动清 —— 源码里无try/finally证据。 - 不重入。若渲染过程中递归进入(如凋灵实体被反射效果二次渲染),标志会被内层覆盖。
这三点在本仓库内无法从该文件确认是否被正确处理(该文件本身只有 1 个字段),需要看两个 mixin 的实现才能判定。
已知问题 / 风险
PlayerReflectionCapture依赖内嵌 Iris 三重前置检查,任一不满足就静默返回(:209、:215、:216)。排查「玩家反射没效果」时,三处都是无声失败点。PlayerReflectionCapture.partOrder的 12 槽映射是手写数组(:141-155),不随ModelBiped字段变化而更新,编译期无保护。GlintClock.secondArmorMatrix(Matrix4fc)可能返回null(:40),调用方不判空即 NPE。GlintClock的矩阵 getter 返回共享可变对象(:43-49),就地修改污染全局。GlintClock.secondArmorLayer用m01() < 0f判层(:56),依赖 JOML 布局约定而非语义。ArmorTexturePaths用引用比较t[base] != prefix判缓存有效性(:17)。调用方若每次 new String,优化静默失效。prefix == null会拼出含字面量null的路径且不报错(:10、:33-35)。ModelHorseArmor重建 box 会丢弃ModelBox上的 mixin 挂载字段(ModelBoxQuads的 quad 列表),本文件内无补救代码。WitherArmorState.pendingInflate是裸 public static 字段,非 volatile、不自动复位、不防重入,且该文件没有任何注释说明它的语义与生命周期。5 行代码承载跨 mixin 传参,审计成本极高。SkippedGlintBlock的0.76与0.5染色系数无出处注释(:23),是 1.7.10ItemRenderer的魔数复刻,改动会直接改变手持物闪光颜色。
相关条目
- 渲染崩溃恢复 - 同属
rendering/顶层 - 帧节流内核 - 同属
rendering/顶层 - Iris(内嵌) -
PlayerReflectionCapture的 3 个前置条件来源;net/coderbot/iris是上游代码不是 Angelica 的 - Celeritas(内嵌地形渲染引擎) -
GlintClock的调用方之一 - Angelica mixin 分组 - 上述 6 个类的全部注入点
- Shaders mixin 分组 -
PlayerReflectionCapture的MixinModelBiped注入点 - 资源与语言键 - 若护甲路径涉及本地化