护甲 / 闪光 / 玩家反射(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.10 ModelRenderer 的原版 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 惯用法,有三个实际风险:

  1. 无 volatile。若两个 mixin 在不同线程(渲染线程 vs 其它)执行,可见性无保证。实际都在渲染线程,风险低但按 JMM 仍是未同步的。
  2. 不自动复位。写 true 的一侧若在读侧读到之前抛异常,标志残留 true,下一次调用会被误判。需要读侧用完手动清 —— 源码里无 try/finally 证据。
  3. 不重入。若渲染过程中递归进入(如凋灵实体被反射效果二次渲染),标志会被内层覆盖。

这三点在本仓库内无法从该文件确认是否被正确处理(该文件本身只有 1 个字段),需要看两个 mixin 的实现才能判定。

已知问题 / 风险

  1. PlayerReflectionCapture 依赖内嵌 Iris 三重前置检查,任一不满足就静默返回(:209、:215、:216)。排查「玩家反射没效果」时,三处都是无声失败点。
  2. PlayerReflectionCapture.partOrder 的 12 槽映射是手写数组(:141-155),不随 ModelBiped 字段变化而更新,编译期无保护。
  3. GlintClock.secondArmorMatrix(Matrix4fc) 可能返回 null(:40),调用方不判空即 NPE。
  4. GlintClock 的矩阵 getter 返回共享可变对象(:43-49),就地修改污染全局。
  5. GlintClock.secondArmorLayer 用 m01() < 0f 判层(:56),依赖 JOML 布局约定而非语义。
  6. ArmorTexturePaths 用引用比较 t[base] != prefix 判缓存有效性(:17)。调用方若每次 new String,优化静默失效。prefix == null 会拼出含字面量 null 的路径且不报错(:10、:33-35)。
  7. ModelHorseArmor 重建 box 会丢弃 ModelBox 上的 mixin 挂载字段(ModelBoxQuads 的 quad 列表),本文件内无补救代码。
  8. WitherArmorState.pendingInflate 是裸 public static 字段,非 volatile、不自动复位、不防重入,且该文件没有任何注释说明它的语义与生命周期。5 行代码承载跨 mixin 传参,审计成本极高。
  9. SkippedGlintBlock 的 0.76 与 0.5 染色系数无出处注释(:23),是 1.7.10 ItemRenderer 的魔数复刻,改动会直接改变手持物闪光颜色。

相关条目