范围:
fill、text、outline之外的自绘内容。
前置:元素系统 中 Element.extractRenderState 的调用时机,以及 工具类 中 TextDrawUtil 的缩放矩阵写法。
自绘相关的四个类都位于 ui.render 包:VertexPainter、CustomElementRenderState、CustomDrawUtil、GradientPainter。
三层分工
自绘能力由三个类组成,职责分别是:
| 类 | 身份 | 职责 |
|---|---|---|
VertexPainter | 函数式接口 | 定义顶点写法。方法签名是 paint(VertexConsumer, Matrix3x2fc) |
CustomElementRenderState | 清单条目 | 实现 GuiElementRenderState,汇集管线、纹理、矩阵快照、矩形、裁剪与 bounds,在渲染期将顶点生成转交给 VertexPainter |
CustomDrawUtil | 门面 | 业务代码的唯一入口,负责获取矩阵快照与裁剪矩形、构造条目并提交到清单 |
GradientPainter 是可选的第四层,将常用图案(渐变)编译为 VertexPainter,避免每个调用方重复编写顶点循环。
划分为三层而非单一工具类,理由是形状范围没有上限:渐变、圆角、裁剪过的图片、对角分界的双色块、任意网格采样都归属于「矩形内任意图案」这一范围。形状纳入工具类会使工具类随形状持续扩张;形状交由调用方的 lambda 处理时,工具类无需改动。
相关 Minecraft 方法
public final ScissorStack scissorStack = new ScissorStack(); // 字段
public final GuiRenderState guiRenderState; // 字段
public Matrix3x2fStack pose() // 方法
SCISSOR 栈与 GuiRenderState 是原版自带的 public 字段。
CustomElementRenderState 清单条目
GuiGraphicsExtractor 在 MC 中只做一件事:向 GuiRenderState 写入清单。GuiElementRenderState 是清单条目,GuiRenderer 是执行者。自绘扩展只在「写清单」这一层插入,其余全部复用。
与原版一致的形态
CustomElementRenderState 的 record 分量与原版的 ColoredRectangleRenderState 逐项对应:管线、纹理设置、矩阵快照、四个坐标、两色、裁剪矩形、bounds。只有一处不同:顶点不固定为两色四边形,而是交给 painter。
沿用原版结构的好处是 bounds() 的算法与裁剪区自洽方式都与 vanilla 一致,排序契约无需额外论证。
bounds 排序契约
GuiRenderState.addGuiElement 首先获取 screenArea.bounds(),为 null 时直接返回 false,该元素被静默丢弃,没有报错也没有日志。因此 bounds 必须是准确值。
CustomElementRenderState 在构造时算一次并缓存进 record 分量:
ScreenRectangle bounds = new ScreenRectangle(x0, y0, x1 - x0, y1 - y0).transformMaxBounds(pose);
return scissorArea != null ? scissorArea.intersection(bounds) : bounds;
- 缓存的原因:排序每帧查询上百次
bounds(),现算等于每次查询重跑一次矩阵变换。 - 与裁剪区求交的原因:落在裁剪区外的部分不算可见。不求交时,裁剪掉的元素仍然占着 bounds,会让后续层级将其误判为重叠元素而改变排序结果。
- 求交后为 null 属正常情况:整个元素被裁掉时交集为空,
addGuiElement会将其丢弃,效果正是「裁掉后不绘制」。
pose 快照
buildVertices 到 GuiRenderer 阶段才被调用,此时 extractor 的矩阵栈已改变。因此 record 存储的是 new Matrix3x2f(e.pose()) 复制出的值,而非活引用。
VertexPainter 顶点写法
QUADS 四顶点约束
RenderPipelines.GUI 的顶点格式是 DefaultVertexFormat.POSITION_COLOR 配 VertexFormat.Mode.QUADS,顶点总数必须是四的倍数。网格细分时每格输出四个顶点,三角形数据会被视为坏数据丢弃。
winding 顺序
顶点顺序沿用 vanilla 的矩形条目:
vc.addVertexWith2DPose(pose, x0, y0).setColor(col1);
vc.addVertexWith2DPose(pose, x0, y1).setColor(col2);
vc.addVertexWith2DPose(pose, x1, y1).setColor(col2);
vc.addVertexWith2DPose(pose, x1, y0).setColor(col1);
即左上、左下、右下、右上。QUADS 会被分解为两个三角形,两种拆法都让两个三角形共用一条内部对角线,但存活三角形的绕行方向由此确定。绕行方向与 vanilla 相反时,在开启背面剔除的管线上整块图形会消失,且现象隐蔽:清单中存在条目、排序照常参与、画面无输出。
CustomDrawUtil 门面
| 方法 | 说明 |
|---|---|
draw(e, x0, y0, x1, y1, painter) | 走 RenderPipelines.GUI 管线,无纹理 |
draw(e, pipeline, textureSetup, x0, y0, x1, y1, painter) | 通用形态,处理贴图与非默认混合 |
drawBilinear(e, x0, y0, x1, y1, topLeft, topRight, bottomLeft, bottomRight) | 横纵同时渐变,交给光栅器插值 |
drawBilinearMeshed(e, x0, y0, x1, y1, topLeft, topRight, bottomLeft, bottomRight, cells) | 横纵同时渐变,CPU 上算严格双线性 |
坐标写在矩阵变换之前,与 vanilla 的 fill 同一套口径,矩阵由门面快照。矩形区间是左闭右开的 [x0, x1) × [y0, y1)。
GradientPainter 渐变
四个角的颜色按屏幕方位给出:左上、右上、左下、右下。单向渐变不必另写方法,横向渐变把上下两行的颜色各取成同一个值,纵向渐变把左右两列各取成同一个值,四角插值即退化为该方向。
bilinear 四顶点模式
return (vc, pose) -> {
vc.addVertexWith2DPose(pose, x0, y0).setColor(topLeft);
vc.addVertexWith2DPose(pose, x0, y1).setColor(bottomLeft);
vc.addVertexWith2DPose(pose, x1, y1).setColor(bottomRight);
vc.addVertexWith2DPose(pose, x1, y0).setColor(topRight);
};
CPU 侧只负责顶点组装,插值在 GPU 上完成。处理链路如下:
| 阶段 | 执行者 | 操作 |
|---|---|---|
buildVertices | CPU | 四次 setColor(原始色值),不做任何颜色运算 |
排序与 GuiRenderer | CPU | 上传顶点缓冲,按 bounds 决定绘制顺序 |
core/gui 顶点着色器 | GPU | 每个顶点算一次裁剪坐标,把颜色原样传出 |
| 光栅化 | GPU | 把颜色插值到每个片元 |
core/gui 片元着色器 | GPU | 逐片元乘 ColorModulator |
插值这一步发生在固定功能光栅化阶段,既不在片元着色器里,也不在 CPU 里。MC 的 GUI 着色器即为例证:
// core/gui.vsh
in vec4 Color;
out vec4 vertexColor;
void main() {
gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0);
vertexColor = Color;
}
// core/gui.fsh
in vec4 vertexColor;
out vec4 fragColor;
void main() {
vec4 color = vertexColor;
if (color.a == 0.0) {
discard;
}
fragColor = color * ColorModulator;
}
vertexColor 在顶点着色器是 out、在片元着色器是 in,两边都没有 flat 限定符。GLSL 330 的 varying 默认使用平滑插值,逐片元恒定必须显式写 flat。片元着色器对颜色全程零运算,既无 mix 也无 smoothstep,说明插值发生在光栅化阶段而非着色器内。
vanilla 的 fillGradient 依赖同一机制:ColoredRectangleRenderState 在两色不同时把色值分给四个角,中间值同样由光栅器计算。
bilinearMeshed 严格双线性
bilinearMeshed 在 CPU 上按双线性权重计算每个网格顶点的颜色,再划分为 cells × cells 个小矩形,网格坐标使用 col / (float) cells 而不是累加步长,保证末格正好落在 1.0,边缘不产生浮点累积误差。DEFAULT_CELLS = 8,即 64 格 256 个顶点。
两种口径的数学差异
四个顶点版本得到的是分片仿射:每个三角形各自线性。它与数学意义上的双线性不同:双线性含 uv 交叉项,三角形重心插值无法表达。
四角取 0 / 100 / 200 / 255 时,中心点处严格双线性得到 138.75,四顶点版本得到 127.5。
接缝不可见:两三角形共用的那条对角线两端颜色相同、沿边线性一致,拼接处颜色连续。差异集中在中间区域的渐变偏差,且四角颜色越接近,差异越小。
两种口径的选择:默认使用四顶点版本,一个矩形仅四个顶点;需要与外部计算结果严格对齐时使用 bilinearMeshed,其顶点数量为前者的平方。
零 alpha 片元丢弃
片元着色器中有 if (color.a == 0.0) discard;。alpha 恰好为零的片元会被丢弃,四角颜色以 0x00 开头时这些角上会出现镂空。渐变到透明的边缘通常需要该效果;若为叠加在底图上的半透明蒙版,应保留少量 alpha 余量。
不变量
bounds()必须非 null 且准确。为 null 会被addGuiElement静默丢弃,没有异常也没有日志。改动矩形或矩阵的处理方式时,这个值必须跟着重算。pose必须存快照。存活引用会使顶点使用渲染期已变更的矩阵。- 顶点总数必须是四的倍数。QUADS 模式下数量不对会被视为坏数据丢弃。
- 绕行方向必须对齐 vanilla。方向相反时会在开启剔除的管线上整块消失。
- 不得为获取
scissorStack或guiRenderState编写 Mixin。两者都是 vanilla 自带的 public 字段。 - 渐变不新增
RenderPipeline。自绘能力复用RenderPipelines.GUI与GUI_TEXTURED,不注册新管线。
连带影响
- 改
CustomElementRenderState的 record 分量会波及CustomDrawUtil的构造调用与所有传painter的调用点。 - 改
bounds的算法会同时影响排序结果与裁剪行为,两者由同一处代码决定。 - 改
VertexPainter的方法签名会波及所有自绘调用点与GradientPainter返回的 lambda。 - 改
GradientPainter.bilinear的顶点顺序会同时改变渐变结果与绕行方向,两者相互关联。