← 返回索引

范围: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 上完成。处理链路如下:

阶段执行者操作
buildVerticesCPU四次 setColor(原始色值),不做任何颜色运算
排序与 GuiRendererCPU上传顶点缓冲,按 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 的顶点顺序会同时改变渐变结果与绕行方向,两者相互关联。

相关文档

  • 工具类:TextDrawUtil 的缩放矩阵写法,与自绘扩展同属绘制工具,可对照理解矩阵压栈的时机
  • 元素系统:extractRenderState 的调用时机决定 pose 为何要存快照
  • 根树与层级系统:渲染流水线,确认自绘条目在哪一段被消费
  • 测试源集:GradientBox 组件与 DevGallery 的「Gradient」行