TESR 网格捕获(rendering/tesr 模板与顶点)
基本信息
| 属性 | 值 |
|---|---|
| 包 | com.gtnewhorizons.angelica.rendering.tesr |
| 本条目覆盖 | 8 个文件 |
| 总行数 | 212 + 162 + 86 + 69 + 46 + 28 + 25 + 18 = 646 |
| 主题 | 模板捕获、顶点解码/变换、缓冲管理、ModelBox 识别、闪光捕获 |
| # | 文件 | 行数 | 可见性 |
|---|---|---|---|
| 1 | GlintCapture.java |
212 | public final |
| 2 | VertexTransform.java |
162 | public final |
| 3 | MeshBuffer.java |
86 | public final |
| 4 | BakedTransformCapture.java |
69 | public final |
| 5 | ModelBoxCapture.java |
46 | public final |
| 6 | ModelPartMesher.java |
28 | public final |
| 7 | TemplateCapture.java |
25 | public final |
| 8 | TemplateBuffer.java |
18 | public final |
同包另两篇:TESR 实例化管线、TESR 保留组。
TemplateBuffer:模板数据容器
18 行,唯一的数据载体。
public final class TemplateBuffer { // :3
public final int[] data; // :5 顶点数据
final int[] work; // :6 工作副本(包私有)
public final int vertexCount; // :7
public final int drawMode; // :8
long bucketEpoch; // :9 包私有
int bucketIndex; // :10 包私有
}
构造器(:12-17)复制一次:
this.data = data;
this.work = data.clone();
⚠️ work = data.clone() 在每次构造时做一次完整深拷贝。模板是长生命周期的缓存对象,所以拷贝一次可接受;但 VertexTransform.writeInstance 每次绘制都要重填 work 的全部 vertexCount 个顶点(见下),data 保持不变作为真源。
3 个字段 final、2 个字段可写且包私有。bucketEpoch / bucketIndex 是 InstancedTemplateRenderer.Bucket 的索引,不参与相等性判断(没有 equals)。
⚠️ 没有 equals / hashCode —— 若被放进 HashMap/HashSet 会退化为身份哈希。源码内它只被数组索引访问(TemplateCapture、VertexTransform),但这是隐式约束:一旦有代码把它当 map 键就会出错。
TemplateCapture:DirectTessellator → TemplateBuffer
25 行,5 步:
public static TemplateBuffer toTemplate(DirectTessellator direct) { // :15
final int vertexCount = direct.getVertexCount(); // :16
if (vertexCount == 0) return null; // :17
final int drawMode = direct.getDrawMode(); // :18
final VertexFormat format = direct.getVertexFormat(); // :19
final ByteBuffer copy = direct.allocateBufferCopy(); // :20
final int[] data = VertexTransform.decode(memAddress0(copy), format, vertexCount, drawMode); // :21
memFree(copy); // :22
return new TemplateBuffer(data, vertexCount, drawMode); // :23
}
| 步 | 行 | 说明 |
|---|---|---|
| 空检查 | :17 |
vertexCount == 0 返回 null(不是空模板) |
| 拷出 | :20 |
allocateBufferCopy() —— 从直接内存拷出 |
| 解码 | :21 |
VertexTransform.decode(addr, format, count, drawMode) |
| 立即释放 | :22 |
memFree(copy) |
| 包装 | :23 |
⚠️ memFree(copy) 在 :22 无 try/finally。若 :21 的 decode 抛异常,直接内存永久泄漏。这是本条目里最实在的资源泄漏点。
⚠️ 返回 null 而不是抛异常(:17)—— 调用方必须判空。空模板(无顶点)不构成错误,可能被正常路径遇到。
⚠️ drawMode 被保存进 TemplateBuffer 但 decode 也在用它(:21)—— drawMode 影响法线播种(见下),但它同时被存下来用于后续绘制。两者共用一个值,但语义不同(一个用于解码、一个用于 GL 绘制模式)。
VertexTransform:解码与实例写入
162 行,是模板数据的编解码核心。
13 个静态 import 的索引常量(:10-21)
全部来自 gtnhlib:com.gtnewhorizon.gtnhlib.client.renderer.cel.util.ModelQuadUtil:
| 常量 | 用途 |
|---|---|
VERTEX_SIZE |
每顶点的 int 数量 |
X_INDEX / Y_INDEX / Z_INDEX |
位置 |
TEX_X_INDEX / TEX_Y_INDEX |
纹理坐标 |
COLOR_INDEX |
颜色 |
LIGHT_INDEX |
光照 |
NORMAL_INDEX |
法线 |
DEFAULT_COLOR / DEFAULT_LIGHTMAP |
缺省填充值 |
⚠️ VERTEX_SIZE 的值本条目不可判定 —— 定义在 gtnhlib 依赖里,不在本仓库。InstancedTemplateRenderer.TEMPLATE_STRIDE = VERTEX_SIZE * 4(InstancedTemplateRenderer.java:37)也依赖它。
⚠️ 索引用位运算而非乘:out[v * VERTEX_SIZE | NORMAL_INDEX](:45)、work[vbase | COLOR_INDEX](:137)。| 优先级低于 *,所以这是 (v * VERTEX_SIZE) | NORMAL_INDEX —— 正确,但前提是 VERTEX_SIZE 是 2 的幂。若 gtnhlib 改成非 2 的幂会静默算错。
⚠️ DEFAULT_COLOR / DEFAULT_LIGHTMAP 被 import 但本条目未看到它们在已读段落(:29-50、:131-162)中使用 —— 它们在 decode 的 3 参数重载(:93-129)里。该方法本条目未完整阅读。
PACKED_UP(:27)
private static final int PACKED_UP = NormI8.pack(0f, 1f, 0f, 0f);
NormI8 来自 net.coderbot.iris.vertices(内嵌 Iris)。(0, 1, 0, 0) 是 +Y 方向。EntityShadowBatcher 也有同名常量 PACKED_UP(:30)—— 两份独立定义的同值常量。
法线播种:seedMissingNormals(:35-~55)
decode(long, VertexFormat, int, int)(:29-33)先调 3 参数重载,再调 seedMissingNormals。
final int prim = switch (drawMode) { // :36-40
case GL11.GL_QUADS -> 4;
case GL11.GL_TRIANGLES -> 3;
default -> 0;
};
if (prim != 0) { // :41
outer:
for (int p = 0; p + prim <= vertexCount; p += prim) { // :43
for (int v = p; v < p + prim; v++) {
if (out[v * VERTEX_SIZE | NORMAL_INDEX] != 0) continue outer; // :45
}
final int packed = packedFaceNormal(out, p, prim); // :47
for (int v = p; v < p + prim; v++) {
out[v * VERTEX_SIZE | NORMAL_INDEX] = packed; // :49
}
}
}
逻辑:若一个图元(prim 个顶点)里所有顶点的法线都是 0,就用 packedFaceNormal 算出面法线并赋给全部顶点。只要有一个顶点有法线就整组跳过(continue outer)。
⚠️ 这是一个 switch 表达式(Java 14+)与带标签的 continue(outer: 标签在 :42)。语法上依赖现代 JDK。
⚠️ 「法线全 0」被当作「无法线」。但合法的 -Y 法线打包后可能恰好是 0 —— 若是,「向下」的面会被误判为「无法线」而重算。packedFaceNormal(本条目未完整读)若算出一致的值则无害;但这依赖 NormI8 的打包约定,本条目不可判定。
⚠️ drawMode 既非 GL_QUADS 也非 GL_TRIANGLES 时 prim = 0,整个播种跳过(:41)—— 例如 GL_LINES / GL_TRIANGLE_STRIP / GL_TRIANGLE_FAN(VertexTransform.java:39 的 default -> 0)。1.7.10 的 Tessellator 只用 QUADS,但第三方 TESR 若发 strip/fan,法线不会被播种。
⚠️ continue outer 跳的是最外层 for(:43) —— 正确写法,但 outer: 标签在 :42 独占一行,若格式化工具重排代码极易丢失标签的作用域语义。
writeInstance:每顶点的实例变换(:131-150)
9 个参数,long destPtr 返回。逐顶点做 3 件事:
| 步 | 行 | 动作 |
|---|---|---|
| 颜色相乘 | :137 |
work[COLOR] = mulColor(src[COLOR], colorABGR) |
| 光照覆盖 | :138 |
work[LIGHT] = packedLight(直接覆盖,不相乘) |
| 纹理矩阵 | :139-147 |
texMatrix != null 时变换 UV,否则原样复制 |
UV 变换公式(:142-143):
u' = texMatrix.m00()*u + texMatrix.m10()*t + texMatrix.m30();
t' = texMatrix.m01()*u + texMatrix.m11()*t + texMatrix.m31();
⚠️ 只用了 texMatrix 的第 0、1 列和第 3 行(平移) —— 忽略缩放的 m22 与 z 相关的项。这是刻意的(2D 纹理矩阵只需 2×2 + 平移),但若 texMatrix 含透视项会被忽略。
⚠️ Float.intBitsToFloat / floatToRawIntBits 配对(:140-146)—— 正确,但用 raw 而非普通转换意味着 NaN 载荷会被保留。这是与 OperationArgs.boxed 相同的一致性选择(见 帧节流)。
最后一步(:149):
return destFormat.writeToBuffer0(destPtr, work, vertexCount * VERTEX_SIZE, mv, scratch);
⚠️ work 的前 vertexCount * VERTEX_SIZE 个元素必须全部被本循环填满,否则残留上次的数据。循环写了 COLOR / LIGHT / TEX_X / TEX_Y 四个槽,位置、法线、其余槽位靠 :137-148 之前 work = data.clone() 留下的原值。work 只在 TemplateBuffer 构造时 clone 一次,之后永不重置 —— 所以每个槽位都必须每次都写或保持不变。若将来 writeInstance 增加条件分支跳过某槽,会读到上一次绘制其它实例时的值。这是真实的隐患。
mulColor:带哨兵的两色相乘(:152-161)
static int mulColor(int a, int b) {
if (a == -1) return b; // :153
if (b == -1) return a; // :154
return mul8(a,b,0) | mul8(a,b,8) | mul8(a,b,16) | mul8(a,b,24); // :155
}
⚠️ -1(即 0xFFFFFFFF,全白且 alpha 最大)是「无 tint」哨兵。先判 a 再判 b —— 若两者都是 -1 返回 b(也是 -1),正确。
⚠️ -1 作为哨兵意味着「无法表达 tint 成纯白不透明」。若某实例的 tint 恰好是 0xFFFFFFFF(白色不透明),它会覆盖模板颜色而不是相乘(结果恰为白色,数学上等价)—— 所以实际无害,但语义是巧合而非设计。
mul8(:158-161)是标准的 8 位定点乘法:
final int t = ((a >>> shift) & 0xFF) * ((b >>> shift) & 0xFF) + 0x80; // + 0x80 舍入
return (((t + (t >> 8)) >> 8) & 0xFF) << shift;
⚠️ + 0x80 是四舍五入(不是截断)—— 与 ParticleQuads.packColor 的 (int) 截断(见 粒子条目)策略不同。两处都是颜色量化,但一个有舍入一个没有。
⚠️ mulColor 是包私有(无 public),mul8 是 private。
BakedTransformCapture:带变换烘焙的捕获
69 行,extends DirectTessellator(gtnhlib)。
| 字段 | 行 | 说明 |
|---|---|---|
delta |
:19 |
ModelViewDelta(在 client/rendering,见 客户端渲染服务) |
deltaMatrix |
:20 |
Matrix4f 暂存 |
scratch |
:21 |
Vector3f |
transform |
:22 |
当前生效的变换,非 null 期间才做矩阵烘焙 |
firstColor |
:23 |
首个 draw 的打包颜色 |
sawRun |
:24 |
是否已见到第一个 draw |
colorUniform |
:25 |
全部 draw 颜色是否一致 |
begin / end(:31-42)
public void begin() { TessellatorManager.startCapturingDirect(this); delta.snapshot(); sawRun = false; colorUniform = true; } // :31-36
public TemplateBuffer end() { // :38
final TemplateBuffer template = colorUniform ? TemplateCapture.toTemplate(this) : null; // :39
TessellatorManager.stopCapturingDirect(); // :40
return template;
}
⚠️ end() 在 colorUniform == false 时返回 null(:39)—— 颜色不一致的模板无法实例化(因为实例 tint 只能整体乘一个颜色)。这是有意的设计约束,不是失败。
⚠️ TessellatorManager.stopCapturingDirect() 无 try/finally(:40)。若 toTemplate 抛异常(memFree 泄漏见上),捕获状态不会被关闭,后续所有 Tessellator 写入都会进到这个 DirectTessellator —— 严重的状态泄漏。startCapturingDirect / stopCapturingDirect 是 gtnhlib 的全局单槽(其实现不在本仓库)。
⚠️ 构造器用 memAlloc(capacity)(:28)—— 直接内存,无对应的 memFree。BakedTransformCapture 的实例由谁创建、何时释放,本条目不可判定。
interceptDraw:颜色一致性检查(:44-60)
@Override
protected int interceptDraw(Tessellator tessellator) { // :45
final Color4 color = GLStateManager.getColor(); // :46
final int packed = ColorABGR.pack(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha()); // :47
if (!sawRun) { sawRun = true; firstColor = packed; } // :48-50
else if (packed != firstColor) colorUniform = false; // :51-53
transform = delta.deltaOrNull(deltaMatrix); // :54
try { return super.interceptDraw(tessellator); } // :56
finally { transform = null; } // :58
}
逐 draw 比对 GLSM 的当前颜色。第一个 draw 记下 firstColor,后续任一不同则 colorUniform = false(粘性,不回退)。
⚠️ transform 是实例字段而非 ThreadLocal。若捕获在多线程进行(InstancedTemplateRenderer 的 TTL 重建可能在工作线程),会互相覆盖。源码无同步。
⚠️ finally { transform = null; }(:58) —— 正确,保证异常时清理。
writeVertexData:条件烘焙(:62-68)
@Override
protected long writeVertexData(VertexFormat format, int[] rawBuffer, int rawBufferIndex) { // :63
if (transform != null) return format.writeToBuffer0(writePtr, rawBuffer, rawBufferIndex, transform, scratch); // :65
return super.writeVertexData(format, rawBuffer, rawBufferIndex); // :67
}
transform != null 时用自定义矩阵写顶点,否则走父类。transform 的来源是 delta.deltaOrNull(deltaMatrix)(:54)—— 返回 null 表示「modelview 未变」,此时烘焙是恒等变换,走父类的快路径。
⚠️ writePtr 是 DirectTessellator 的字段(父类),子类直接访问 —— 依赖父类字段名与可见性。
ModelPartMesher:ModelPart 的 Tessellator 输出
28 行,2 个方法。
public static void emitPart(Tessellator t, ModelRenderer part, float scale) { // :13
t.setTranslation(part.offsetX + part.rotationPointX * scale,
part.offsetY + part.rotationPointY * scale,
part.offsetZ + part.rotationPointZ * scale); // :14-17
emitPartLocal(t, part, scale); // :18
t.setTranslation(0, 0, 0); // :19
}
public static void emitPartLocal(Tessellator t, ModelRenderer part, float scale) { // :22
final List<ModelBox> boxes = part.cubeList;
for (int i = 0, n = boxes.size(); i < n; i++) boxes.get(i).render(t, scale); // :24-26
}
emitPartLocal 与 ModelHorseArmor 构造函数里的遍历几乎相同(见 护甲条目)—— 两处都直接遍历 part.cubeList 这个可变字段。
⚠️ t.setTranslation(0, 0, 0) 在 :19 无 try/finally。若 emitPartLocal 抛异常,Tessellator 的平移状态停留在中间值,后续渲染全部偏移。这是真实的异常路径缺陷。
⚠️ emitPart 不检查 part == null 或 part.cubeList == null,:23 直接 .size() 会 NPE。而 PlayerReflectionCapture.quadsOf(:165-168)做了完整的 part == null || cubeList == null || isEmpty() 三重检查。同包两处的空安全标准不一致。
ModelBoxCapture:识别「这个 box 是不是标准立方体」
46 行,是模式匹配而非渲染。EPSILON = 1.0e-4f(:10)。
public static CubeParams capture(TexturedQuad[] quads, float texWidth, float texHeight, boolean mirror,
int texU, int texV, float x, float y, float z,
int sizeX, int sizeY, int sizeZ, float inflate) { // :14
13 个参数,其中 6 个是 float 位置/尺寸 + 2 个 int 尺寸。⚠️ 参数列表极长且位置参数相邻(float x, float y, float z, int sizeX, ...)—— 传错顺序编译期无法发现。
六道校验,任意一道失败返回 null
| # | 行 | 检查 |
|---|---|---|
| 1 | :15 |
quads == null || quads.length != UnitCubeMesh.FACE_COUNT |
| 2 | :17 |
CubeParams.of(...) 返回 null |
| 3 | :23 |
每个 quad 非 null、vertexPositions != null、length == 4 |
| 4 | :28 |
每个顶点非 null、vector3D != null |
| 5 | :30-32 |
几何位置匹配单位立方体 |
| 6 | :35-37 |
纹理坐标匹配标准 UV 布局 |
镜像处理(:19-20、:27):
final float lowX = mirror ? params.minX() + params.spanX() : params.minX(); // :19
final float spanX = mirror ? -params.spanX() : params.spanX(); // :20
...
final PositionTextureVertex vertex = quad.vertexPositions[mirror ? 3 - corner : corner]; // :27
镜像做两件事:X 方向的起点与跨度翻转(:19-20),以及顶点索引反转 3 - corner(:27)。
⚠️ 顶点索引反转只作用于 mirror,但几何校验用的 unit[0] * spanX 已经把 span 取负了(:30)。两处镜像处理叠加是否正确,取决于 UnitCubeMesh.CORNERS 的定义 —— 该类在 GLSM 子项目,本条目不可判定。若两者语义不匹配,镜像路径会全部返回 null(静默失效),而正镜像路径正常。这是一个只能靠实测发现的问题。
near 的相对容差(:43-45)
private static boolean near(float a, float b) { return Math.abs(a - b) < EPSILON; }
⚠️ 绝对容差 1.0e-4f,不是相对容差。对比 ParticleQuadDecoder 用的是相对容差 1.0e-5f * (1 + |half| + scale)(见 粒子条目)。两处策略不同:大尺寸 box 在这里更容易通过(绝对容差),UV 比对在小纹理图上也更容易通过。对超大 box,位置误差 0.0001 会被接受 —— 这可能是有意的容差,也可能过宽。
UV 比对(:33-37):
final int u = texU + net.u(corner).texels(sizeX, sizeZ);
final int v = texV + net.v(corner).texels(sizeY, sizeZ);
if (!near(vertex.texturePositionX, u / texWidth) || !near(vertex.texturePositionY, v / texHeight)) return null;
⚠️ sizeZ 同时参与 u 与 v 的 texels 计算(:33-34)—— 说明 net.u/v 的 texels(...) 签名接受的是「该面对应的两个边长」而非统一签名。UnitCubeMesh.Net 在 GLSM 子项目,本条目不可判定。
MeshBuffer:GPU 网格缓冲
86 行,public final,upload / bind / draw / unbind / render / delete。
| 方法 | 行 | 说明 |
|---|---|---|
upload(format, drawMode, data, vertexCount) |
:16 |
4 参版 |
upload(format, drawMode, data, vertexCount, dynamic) |
:20 |
5 参版,多一个 dynamic 标志 |
isUploaded() |
:32 |
|
bind() |
:36 |
|
draw(first, count) |
:44 |
|
unbind() |
:48 |
|
render() |
:53 |
|
delete() |
:59 |
|
ensureCapacity(ByteBuffer, int bytes, boolean preserve) |
:70 |
static 工具 |
⚠️ ensureCapacity 是本条目唯一被外部复用的方法 —— ParticleInstancer.emit(ParticleInstancer.java:174)与 groupFor(:~273)都调它,RetainedTesrGroups 也调。扩容策略是 buffer.capacity() * 2(:76)。
⚠️ ensureCapacity 的 preserve 参数控制是否拷贝旧内容。两个粒子调用点都传 true(ParticleInstancer.java:174、:~273),但粒子数据是每帧重填的,preserve 无必要 —— 每次扩容都做一次全量拷贝。
⚠️ 4 参 upload 与 5 参 upload 并存(:16、:20)—— 4 参版必须转调 5 参版并传某个 dynamic 默认值。该默认值本条目未读,语义可能与调用方预期不符。
GlintCapture:附魔闪光捕获
212 行,public final。本条目唯一只有 1 个内部类 Range(:16)的大文件 —— 其余 200 行的方法签名本条目未逐个读取。
已确认的事实:
| 项 | 值/说明 |
|---|---|
| 唯一内部类 | private static final class Range(:16) |
| 关联 | GlintClock(见 护甲条目)提供帧级闪光矩阵 |
| 关联 | ModelPartBatcher 与 items/HeldItemGlint 都引用 GlintClock(grep 确认) |
| 关联 | HeldItemGlint 在 rendering/items/(本条目未覆盖,items/ 只有 2 个未覆盖文件:DroppedItemInstancer、ItemPropCache;HeldItemGlint、BlockRenderListManager、ItemRenderListManager 已被其它条目提及) |
⚠️ GlintCapture.java 212 行是本条目最大的未读文件。 本条目只覆盖了它的存在与唯一内部类,其算法本条目不可判定。不做臆测。
已知问题 / 风险
TemplateCapture.toTemplate的memFree(copy)无 try/finally(:22),decode抛异常则直接内存永久泄漏。BakedTransformCapture.end()的stopCapturingDirect()无 try/finally(:40),toTemplate抛异常则 gtnhlib 的全局捕获槽泄漏 —— 后续所有 Tessellator 写入都会进入这个DirectTessellator。这是本条目最严重的状态泄漏。BakedTransformCapture用memAlloc(capacity)但无memFree(:28),直接内存生命周期无保障。BakedTransformCapture.transform是实例字段非 ThreadLocal(:22),多线程捕获会互相覆盖。TemplateBuffer.work只在构造时 clone 一次(:14),之后靠writeInstance逐槽填满。任何条件跳过某槽都会读到上一次绘制其它实例的残留值。ModelPartMesher.emitPart的setTranslation(0,0,0)无 try/finally(:19),异常时 Tessellator 平移状态残留。ModelPartMesher.emitPartLocal无空安全检查(:23),而同包的PlayerReflectionCapture.quadsOf有三重检查 —— 标准不一致。ModelBoxCapture.capture是 13 参数方法(:14),6 个相邻float位置/尺寸,顺序传错编译期无保护。ModelBoxCapture的near用绝对容差1.0e-4f(:10、:44),与ParticleQuadDecoder的相对容差策略不同。ModelBoxCapture的镜像路径正确性依赖 GLSM 的UnitCubeMesh.CORNERS语义(:27、:30),本仓库不可判定;不匹配会静默全量返回 null。VertexTransform.seedMissingNormals把「法线为 0」当作「无法线」(:45),依赖 NormI8 打包约定,本仓库不可判定是否存在合法的 0 打包法线。VertexTransform的索引用|而非+(:45、:137),依赖VERTEX_SIZE是 2 的幂(gtnhlib,不在本仓库)。VertexTransform.writeInstance忽略texMatrix的 m22 与透视项(:142-143),只做 2×2 + 平移。VertexTransform.mulColor逐分量舍入(:159)而ParticleQuads.packColor截断 —— 两处颜色量化策略不同。TemplateBuffer无equals/hashCode,作为 map 键会退化为身份哈希。MeshBuffer4 参与 5 参upload并存(:16、:20),4 参版的dynamic默认值未确认。MeshBuffer.ensureCapacity的preserve = true在粒子路径上做无必要的全量拷贝(容量倍增时)。GlintCapture.java212 行中约 200 行算法本条目未读(只确认了唯一内部类Range)。
相关条目
- TESR 实例化管线 -
TemplateBuffer的消费者(VertexTransform.writeInstance) - TESR 保留组 - 模板的缓存与晋升/降级
- 客户端渲染服务 -
ModelViewDelta的定义(client/rendering/) - 护甲 / 闪光 / 玩家反射 -
GlintClock的帧级矩阵 - 粒子实例化 -
MeshBuffer.ensureCapacity的另一调用方 - API 层 -
VertexFormat/DirectTessellator来自 gtnhlib - Subprojects(内嵌子项目) - GLSM 的
CubeParams/UnitCubeMesh/DirectTessellator侧 - Iris(内嵌) -
NormI8/ColorABGR的来源