现代Minecraft渲染导论 第四章:后现代Minecraft渲染架构(26.1+)
从 RenderSystem + ShaderInstance + BufferBuilder.end() 时代,走到 GuiGraphics + GuiRenderState + RenderPipeline + FrameGraph 时代
版本:
- Minecraft
1.21.11(截至目前26.1与其相比渲染架构变更不大)- NeoForge 平台(Fabric暂不考虑,但本章大部分内容不涉及Mod加载器平台)
- Java
21
迁移警告
Mojang正在积极地将Blaze3D迁移至VK,故本章讲述的内容仅适用于26年夏季VK渲染后端更新前的版本(1.21.11/26.1,相关接口和代码不保证之后不发生语义漂移
4.0 前言
如果你在 1.20.1 甚至更老的版本里写过渲染代码,那么你大概率熟悉这样一套路径:
kotlin
RenderSystem.setShader(...)
RenderSystem.setShaderTexture(0, texture)
val builder = Tesselator.getInstance().builder
builder.begin(...)
// vertex(...)
val mesh = builder.end()
BufferUploader.drawWithShader(mesh)
这套思维在旧版 Minecraft 里当然成立,而且在很多地方确实是主路径。
但到了 1.21.11,你如果还把它当作整个客户端渲染系统的中心,那就会产生严重误判。
为什么?
因为高版本变化的不是某个函数,而是整套渲染架构的组织方式:
- GUI 渲染已经明显转向
GuiGraphics + GuiRenderState。 - GUI 2D 变换已经不再默认建立在
PoseStack上,而是JOML提供的Matrix3x2fStack。 - pipeline 已经被提升为显式对象,
RenderPipeline不再只是“底层内部细节”。 - GPU 资源的表达已经不再只是一个
texture id,而是更靠近现代图形 API 的资源对象与视图对象。 FrameGraphBuilder这种现代引擎的资源生命周期与 pass 依赖管理工具已经进入 Mojang 的正式实现。- NeoForge 也明确提供了
RegisterRenderPipelinesEvent、RegisterParticleGroupsEvent这类高版本扩展点。
所以这一章不是给高版本 API 速查表,而是一次渲染心智模型的重建。
4.0.1 这一章到底要解决什么问题
本章要解决的是下面这几个问题:
1.21.11的 GUI 渲染到底是如何从“调用 helper”变成“提交 render state”的。GuiGraphics为什么不是PoseStack的新名字。GuiRenderState为什么说明 Mojang 自己也在做“先收集,再提交”。RenderPipeline为什么比旧版ShaderInstance更接近高版本的真实中心。TextureTarget、GpuTextureView、FrameGraphBuilder为什么说明离屏、后处理、资源生命周期都必须重新理解。- 为什么粒子、自定义 GUI、高级特效、offscreen composition、world supplemental render 不能再沿用旧版统一抽象。
4.0.2 本章的阅读方式
- 先接受一个事实:高版本渲染 API 不是旧 API 的别名。
- 先看 Mojang 自己的源码组织,再看我们自己的封装应该怎么落。
- 先分清 GUI path 和 World path,再谈统一抽象。
- 先理解资源与提交模型,再谈性能与优化。
如果你跳过前两步,直接去看自己的 mod 代码,通常会产生一种错觉:
“为什么新版写起来这么别扭?我只想像以前那样直接画一个矩形。”
问题不在于 Mojang 故意把事情复杂化,而在于旧版那套“直接设置状态、直接推顶点、直接 draw”的上层暴露方式,本来就不适合继续承载现在的渲染系统,而且不利于后期MC向Vulkan迁移
4.0.3 本章与前几章的关系
前几章讲的图形学内容仍然是基础,但它们不再足以直接指导 1.21.11 开发。
这一章会把旧内容“吸收并升级”:
- 第一章里关于状态机、纹理、FBO、混合、矩阵的知识,仍然需要。
- 第二章里关于 Blaze3D 的讨论,仍然有用,但语义必须更新。
- 第三章里关于 MRT、HDR、后处理、离屏渲染的内容,在这里会和
TextureTarget、GpuTextureView、FrameGraph重新结合。
也就是说,本章不是附录,而是整个系列在高版本语境下的核心章节。
4.1 从旧世界进入新世界:渲染心智模型先更新
4.1.1 旧模型:状态驱动、立即感很强
旧版 Minecraft 渲染的常见心智模型一直是“立即味”很重,因为在MC 1.14正式Introduce Blaze3D前MC一直在使用GL立即模式绘图,即使在后期迁移到了核心模式,Mojang依然保留了类立即模式的绘图接口,在以前的核心模式(再早的立即模式就不再描述了,纯GlStateManager+Tesselator)写渲染,流程大致是这样的:
- 先切 shader。
- 先切 texture。
- 先切 blend/depth/cull/scissor 等状态。
- 拿到
BufferBuilder。 - 开始塞顶点。
end()。- 上传并绘制。
这套模型最大的特点,是“渲染动作”和“状态修改”耦合得很紧。
你可以把它画成这样:
这个模型在旧版本里之所以有效,是因为上层 API 暴露出来的抽象,本来就非常接近立即绘制。
4.1.2 新模型:提交驱动、资源驱动、管线驱动
到 1.21.11,至少在 GUI 与新式 pipeline 语义上,更合理的心智模型应该是:
- 我不是直接“画”,而是在提交一个渲染描述。
- 这个渲染描述会进入某个 render state 容器。
- 它最终会在合适的阶段按正确的顺序、正确的资源绑定、正确的 pipeline 被统一提交。
- 离屏与后处理不再只是“手动 bind 一个 FBO 再画”,而是越来越偏向 pass + resource lifetime 的组织方式。
简化后可以画成这样:
可以看到高版本的绘制流程正在逐渐变得规范。
4.1.3 为什么“状态机思维”不再够用
OpenGL 状态机知识当然没有过时,但它在高版本里已经不是上层最重要的组织原则了。
原因很简单:
GuiGraphics上层接口已经在主动替你构造 GUI render state。RenderPipeline已经把大批“渲染状态组合”收敛为显式对象。TextureSetup里已经不再只是 texture slot,而是GpuTextureView + GpuSampler。FrameGraphBuilder开始显式管理 pass 顺序与资源生命周期。
所以现在更准确的说法是:
::: align-center
旧版强调“当前 GPU 状态是什么”,新版越来越强调“我要提交什么描述,以及这个描述依赖哪些资源和管线”。
:::
4.1.4 为什么 GUI 和 World 不能继续混在一起
旧版很多项目有一个经典问题:
- GUI 画法和世界画法混用同一套 helper。
- 上层统一拿着
PoseStack到处传。 - 只要能跑,就把二维、三维、离屏、粒子、世界额外渲染都塞进一套抽象里。
到了 1.21.11,这条路会越来越危险。
因为 Mojang 在MC源码里给出了明显信号:
- GUI 2D 主要围绕
GuiGraphics与Matrix3x2fStack。 - World / 3D 仍然主要围绕
PoseStack、世界渲染通路、对应的 pipeline 和 buffer 体系。 - 粒子路径也有自己的 extract / submit 方向。
如果还强行维持“统一渲染上下文”,最终结果通常是:
- GUI 2D 变换语义被三维抽象污染。
- 世界渲染被 GUI helper 牵着走。
- 粒子和离屏逻辑都开始借道错误路径。
4.1.5 我们应该如何更新认知
可以先把高版本渲染理解成三层:
| 层级 | 关注点 | 典型对象 |
|---|---|---|
| 上层语义层 | 我想画什么、我要表达什么效果 | GuiGraphics、项目自己的 retained UI、特效语义对象 |
| 中层提交层 | 以什么 render state / pipeline / pass 提交 | GuiRenderState、RenderPipeline、TextureSetup |
| 底层资源层 | GPU 资源、视图、命令编码、生命周期 | GpuTextureView、TextureTarget、FrameGraphBuilder |
一旦这样分层,很多迁移决策就会清晰得多:
- 上层 UI 语义该保留。
- 中层旧 replay backend 该重写。
- 底层 OpenGL 细节该下沉到 backend boundary 后面。
4.2 Mojang 自己在做什么:1.21.11 渲染栈总览
如果你想真正理解新版渲染系统,第一步不是搜教程(准确地说,也搜不到,MC的渲染教程多年来一直严重匮乏),而是先看源码包结构。
4.2.1 关键包结构
在 1.21.11 里,至少要先关注下面这些位置:
| 包 / 类 | 关注重点 |
|---|---|
net.minecraft.client.gui.GuiGraphics |
GUI 绘制入口 |
net.minecraft.client.gui.render.state.* |
GUI render state 的数据结构 |
net.minecraft.client.renderer.RenderPipelines |
Mojang 预注册 pipeline |
com.mojang.blaze3d.pipeline.RenderPipeline |
pipeline 本体 |
com.mojang.blaze3d.pipeline.TextureTarget |
离屏 render target |
com.mojang.blaze3d.textures.GpuTextureView |
纹理视图 |
com.mojang.blaze3d.framegraph.FrameGraphBuilder |
pass / resource graph |
net.neoforged.neoforge.client.event.RegisterRenderPipelinesEvent |
NeoForge 的 pipeline 注册入口 |
net.neoforged.neoforge.client.event.RegisterParticleGroupsEvent |
NeoForge 的粒子组注册入口 |
这已经足够说明Mojang 的新渲染系统,不再只是“RenderSystem 下面一堆 OpenGL 函数封装”。
4.2.2 com.mojang.blaze3d 现在分别负责什么
现在可以粗略把 Blaze3D 的相关子模块理解成:
| 模块 | 作用 |
|---|---|
pipeline |
描述渲染管线、render target 等对象 |
textures |
纹理对象、view、sampler |
framegraph |
pass 依赖、资源生命周期、执行图 |
systems |
一些仍然存在的系统级渲染入口,例如 RenderSystem |
注意,这里的关键词已经不是“立即模式式 helper”,而是:
PipelineTextureViewFrameGraphResource
这几个词本身就已经比旧版更接近现代图形 API 的语义了。
4.2.3 net.minecraft.client.gui 的新 GUI 渲染路径
GUI 路径现在的主角不是 AbstractGui,也不是自己手搓 BufferBuilder。
而是 GuiGraphics,当然这个GuiGraphics也和1.20.X不一样了
它内部直接持有:
Matrix3x2fStack poseGuiRenderState guiRenderStateScissorStack- GUI atlas / material 相关对象
这已经清楚地表明:
Mojang 现在把 GUI 绘制理解成“收集 GUI 元素状态,再统一处理”的流程,而不是简单的 helper 调用集合。
4.2.4 net.minecraft.client.renderer 的新管线组织方式
旧版很多人说“shader”,新版必须开始谈“pipeline”。
RenderPipelines 中预先注册了大量 pipeline / snippet,这些对象不再只是 shader 程序名,而是:
- 顶点格式
- 顶点模式
- blend
- depth test
- cull
- sampler
- uniform
- shader define
也就是说,新版真正重要的不只是“用哪个 shader”,而是“这个绘制属于哪个 pipeline”。
4.2.5 NeoForge 在高版本里提供了什么扩展点
NeoForge 也在跟着高版本渲染语义走,典型例子就是:
RegisterRenderPipelinesEventRegisterParticleGroupsEvent
这说明高版本扩展的正确方向是:
- 在 pipeline 层接入。
- 在 particle group / render order 层接入。
而不是继续到处 patch 一些旧时代的 begin/end 或 render-time mixin。
4.3 GUI 渲染入口已经换了:GuiGraphics
如果你只记住本章一个类,那大概率应该先记住 GuiGraphics。
因为它是 GUI 新路径里最显眼、也是最容易被误解的入口。
很多人第一次看到它,会下意识觉得:
“这不就是新版 AbstractGui 吗?”
或者:
“这不还是1.20.X那个GuiGraphics吗?”
这只说对了一半。
更准确地说,现在的GuiGraphics 已经是:
- GUI 绘制入口。
- GUI 2D 变换入口。
- GUI render state 提交入口。
- GUI 分层与 scissor 语义的承载入口。
4.3.1 构造和成员
Mojang 源码:
java
public class GuiGraphics implements net.neoforged.neoforge.client.extensions.IGuiGraphicsExtension {
final Minecraft minecraft;
private final Matrix3x2fStack pose;
private final GuiGraphics.ScissorStack scissorStack = new GuiGraphics.ScissorStack();
private final MaterialSet materials;
private final TextureAtlas guiSprites;
final GuiRenderState guiRenderState;
private GuiGraphics(Minecraft minecraft, Matrix3x2fStack pose, GuiRenderState guiRenderState, int mouseX, int mouseY) {
this.minecraft = minecraft;
this.pose = pose;
this.guiRenderState = guiRenderState;
}
public GuiGraphics(Minecraft minecraft, GuiRenderState guiRenderState, int mouseX, int mouseY) {
this(minecraft, new Matrix3x2fStack(16), guiRenderState, mouseX, mouseY);
}
}
这段代码已经足够说明三个关键变化:
- GUI 变换栈已经是
Matrix3x2fStack,不是PoseStack。 GuiGraphics从一开始就绑定着GuiRenderState。- scissor 也不再只是“顺手调一个 GL scissor”,而是 GUI 语义的一部分。
4.3.2 pose() 为什么是 Matrix3x2fStack
源码非常直接:
java
public Matrix3x2fStack pose() {
return this.pose;
}
Mojang 已经明确把 GUI 2D 变换从旧式三维 PoseStack 语义中拆出来了。
因为 GUI 绝大多数绘制,本质上只需要二维仿射变换:
- 平移
- 旋转
- 缩放
- 剪裁后的轴对齐边界计算
对于这类工作,Matrix3x2fStack 更轻,也更接近真实需求。
所以如果你的项目在 GUI 层还到处默认传 PoseStack,那必须立即迁移。
4.3.3 fill(...) 不是“立刻画矩形”,而是“提交矩形状态”
看 fill 的实现:
java
public void fill(int minX, int minY, int maxX, int maxY, int color) {
this.fill(RenderPipelines.GUI, minX, minY, maxX, maxY, color);
}
public void fill(RenderPipeline pipeline, int minX, int minY, int maxX, int maxY, int color) {
this.submitColoredRectangle(pipeline, TextureSetup.noTexture(), minX, minY, maxX, maxY, color, null);
}
private void submitColoredRectangle(
RenderPipeline pipeline,
TextureSetup textureSetup,
int minX,
int minY,
int maxX,
int maxY,
int colorFrom,
@Nullable Integer colorTo
) {
this.guiRenderState.submitGuiElement(
new ColoredRectangleRenderState(
pipeline,
textureSetup,
new Matrix3x2f(this.pose),
minX,
minY,
maxX,
maxY,
colorFrom,
colorTo != null ? colorTo : colorFrom,
this.scissorStack.peek()
)
);
}
这段代码它表达的是:
“把这个矩形的 pipeline、texture setup、pose、几何范围、颜色、scissor,一次性打包成 ColoredRectangleRenderState,然后提交给 GuiRenderState。”
这就是新版 GUI 主路径的核心语义。
4.3.4 drawString(...) 也不是直接画,而是提交文本状态
源码同样很直白:
java
public void drawString(Font font, FormattedCharSequence text, int x, int y, int color, boolean drawShadow) {
if (ARGB.alpha(color) != 0) {
this.guiRenderState.submitText(
new GuiTextRenderState(
font, text, new Matrix3x2f(this.pose), x, y, color, 0, drawShadow, false, this.scissorStack.peek()
)
);
}
}
注意这里依然没有直接“绘制文本”,而是在提交 GuiTextRenderState。
而 GuiTextRenderState 自己还会延迟准备:
java
public Font.PreparedText ensurePrepared() {
if (this.preparedText == null) {
this.preparedText = this.font.prepareText(this.text, this.x, this.y, this.color, this.dropShadow, this.includeEmpty, this.backgroundColor);
ScreenRectangle screenrectangle = this.preparedText.bounds();
if (screenrectangle != null) {
screenrectangle = screenrectangle.transformMaxBounds(this.pose);
this.bounds = this.scissor != null ? this.scissor.intersection(screenrectangle) : screenrectangle;
}
}
return this.preparedText;
}
这说明新版 GUI 文本路径也更偏向“状态对象 + 延迟准备 + 统一调度”。
4.3.5 renderItem(...) 走的是 item render state 路径
看物品渲染:
java
private void renderItem(@Nullable LivingEntity entity, @Nullable Level level, ItemStack stack, int x, int y, int seed) {
if (!stack.isEmpty()) {
TrackingItemStackRenderState trackingitemstackrenderstate = new TrackingItemStackRenderState();
this.minecraft
.getItemModelResolver()
.updateForTopItem(trackingitemstackrenderstate, stack, ItemDisplayContext.GUI, level, entity, seed);
this.guiRenderState.submitItem(
new GuiItemRenderState(
stack.getItem().getName().toString(),
new Matrix3x2f(this.pose),
trackingitemstackrenderstate,
x,
y,
this.scissorStack.peek()
)
);
}
}
这同样不是“这里直接把 item 画出来”。
而是:
- 先解析 item model state。
- 再打包成
GuiItemRenderState。 - 再交给
GuiRenderState。
所以读者现在应该很清楚了:
GuiGraphics 的主要职责,不是自己完成所有绘制,而是把 GUI 调用翻译成统一的 GUI render state。
4.3.6 scissor 在新模型里也带着变换语义
源码:
java
public void enableScissor(int minX, int minY, int maxX, int maxY) {
ScreenRectangle screenrectangle = new ScreenRectangle(minX, minY, maxX - minX, maxY - minY)
.transformAxisAligned(this.pose);
this.scissorStack.push(screenrectangle);
}
注意这里不是直接把参数塞给底层 scissor。
它先做了:
- 根据当前
pose进行轴对齐变换。 - 再把结果压入
scissorStack。
这意味着在高版本 GUI 路径里,scissor 已经是和当前 2D 变换绑定的语义对象,而不是单纯的“底层状态开关”。
4.3.7 blitSprite(...) 说明 GUI atlas 与 sprite metadata 也被正式纳入 GUI 渲染模型
再看一段很关键的逻辑:
java
public void blitSprite(RenderPipeline pipeline, Identifier sprite, int x, int y, int width, int height, int color) {
TextureAtlasSprite textureatlassprite = this.guiSprites.getSprite(sprite);
GuiSpriteScaling guispritescaling = getSpriteScaling(textureatlassprite);
switch (guispritescaling) {
case GuiSpriteScaling.Stretch guispritescaling$stretch:
this.blitSprite(pipeline, textureatlassprite, x, y, width, height, color);
break;
case GuiSpriteScaling.Tile guispritescaling$tile:
this.blitTiledSprite(...);
break;
case GuiSpriteScaling.NineSlice guispritescaling$nineslice:
this.blitNineSlicedSprite(...);
break;
default:
}
}
这说明新版 GUI 渲染不仅仅是“贴一张纹理”,而是已经把:
- atlas sprite
- GUI metadata
- nine-slice / tile / stretch 这些 GUI 特化语义
都作为正式路径的一部分。
所以如果你的 GUI 框架还在上层自己重复造一套九宫格、tile、atlas 逻辑,就要重新评估是否和 Mojang 已有语义冲突了。
4.4 GUI 不是立刻画上去的:GuiRenderState
理解了 GuiGraphics,下一步就必须理解 GuiRenderState。
因为如果你不理解它,就会一直误以为:
“GuiGraphics.fill(...) 只是内部偷偷帮我调了一个更复杂的 draw call。”
实际上不是。
更接近真实情况的说法是:
GuiGraphics 负责生产 GUI render state,GuiRenderState 负责组织这些 state。
4.4.1 先看它最关键的字段
Mojang 源码:
java
public class GuiRenderState {
private final List<GuiRenderState.Node> strata = new ArrayList<>();
private int firstStratumAfterBlur = Integer.MAX_VALUE;
private GuiRenderState.Node current;
private final Set<Object> itemModelIdentities = new HashSet<>();
private @Nullable ScreenRectangle lastElementBounds;
public GuiRenderState() {
this.nextStratum();
}
}
这里最重要的不是字段名,而是它暗示出来的结构:
- 有 strata,也就是分层。
- 有 current,也就是当前节点。
- 有 blur 的分界点。
- 有上一个元素的 bounds。
这说明 GuiRenderState 关心的不是“有没有 draw call”,而是:
- 元素如何分层。
- 元素如何根据屏幕边界关系组织。
- 哪些内容在 blur 前,哪些内容在 blur 后。
4.4.2 nextStratum() 与 up():Mojang 在显式组织 GUI 层级
看两个核心方法:
java
public void nextStratum() {
this.current = new GuiRenderState.Node(null);
this.strata.add(this.current);
}
public void up() {
if (this.current.up == null) {
this.current.up = new GuiRenderState.Node(this.current);
}
this.current = this.current.up;
}
这已经说明 GUI 渲染在内部并不是一个扁平列表。
它至少有两层结构:
- 横向的
strata - 纵向的
up节点链
可以先把它理解成:
stratum解决大层级问题。up解决局部覆盖与嵌套层级问题。
4.4.3 为什么要根据 bounds 自动调整节点位置
看最重要的一段逻辑:
java
private boolean findAppropriateNode(ScreenArea screenArea) {
ScreenRectangle screenrectangle = screenArea.bounds();
if (screenrectangle == null) {
return false;
} else {
if (this.lastElementBounds != null && this.lastElementBounds.encompasses(screenrectangle)) {
this.up();
} else {
this.navigateToAboveHighestElementWithIntersectingBounds(screenrectangle);
}
this.lastElementBounds = screenrectangle;
return true;
}
}
它说明 Mojang 在提交 GUI 元素时,并不是简单 append 到列表尾部,而是在根据元素边界关系决定应该放到哪个 node。
这背后的意思是:
::: align-center
GUI layering 在高版本里,不只是调用顺序问题,而是屏幕区域关系也参与了渲染组织。
:::
再看 navigateToAboveHighestElementWithIntersectingBounds(...):
java
private void navigateToAboveHighestElementWithIntersectingBounds(ScreenRectangle rectangle) {
GuiRenderState.Node node = this.strata.getLast();
while (node.up != null) {
node = node.up;
}
boolean flag = false;
while (!flag) {
flag = this.hasIntersection(rectangle, node.elementStates)
|| this.hasIntersection(rectangle, node.itemStates)
|| this.hasIntersection(rectangle, node.textStates)
|| this.hasIntersection(rectangle, node.picturesInPictureStates);
if (node.parent == null) {
break;
}
if (!flag) {
node = node.parent;
}
}
this.current = node;
if (flag) {
this.up();
}
}
这段逻辑等价于:
- 先找到当前最高的 node。
- 向下检查有没有和当前元素 bounds 相交的内容。
- 如果有,就上升一层放置。
- 如果没有,就回退到父节点继续判断。
也就是说,Mojang 在用一套“基于 bounds 相交关系的层级组织逻辑”来帮助 GUI 元素正确分层。
4.4.4 Node 里存的不是一种东西,而是多类 GUI state
Node 的结构也很能说明问题:
java
static class Node {
public @Nullable List<GuiElementRenderState> elementStates;
public @Nullable List<GuiElementRenderState> glyphStates;
public @Nullable List<GuiItemRenderState> itemStates;
public @Nullable List<GuiTextRenderState> textStates;
public @Nullable List<PictureInPictureRenderState> picturesInPictureStates;
}
这说明 GUI 渲染在 Mojang 内部已经明确拆分成不同类别:
- 普通 GUI 元素
- glyph
- item
- text
- 画中画类状态
这和旧版“都转成顶点然后一股脑儿画掉”的上层观感差异非常大。
因为现在不同类别的 GUI 内容,在提交阶段完全可以走不同的处理路径。
4.4.5 它是怎么遍历这些状态的
看遍历逻辑:
java
public void forEachElement(Consumer<GuiElementRenderState> action, GuiRenderState.TraverseRange traverseRange) { ... }
public void forEachItem(Consumer<GuiItemRenderState> action) { ... }
public void forEachText(Consumer<GuiTextRenderState> action) { ... }
public void forEachPictureInPicture(Consumer<PictureInPictureRenderState> action) { ... }
这说明后续提交阶段可以按类别遍历,也可以按 blur 前后遍历。
于是我们就能看出 GuiRenderState 的真正角色:
它不是某个“中间变量”。
它实际上是 GUI 帧内渲染调度的核心数据结构之一。
4.4.6 用图看 GuiRenderState=
再把 strata + up 看成层级图,大致像这样:
这当然不是完整内部结构图,但已经足够建立正确印象:
GuiRenderState不是扁平列表。- 它在主动组织 GUI layering。
- 它是状态容器,不是立即绘制工具。
4.5 GUI 2D 变换系统重做:Matrix3x2fStack
前面我们已经看到,GuiGraphics 直接持有的是 Matrix3x2fStack,而不是 PoseStack。
这一节要解决的问题是:
为什么 Mojang 要这样改,以及这对我们自己的 GUI 框架意味着什么。
核心结论
在1.21.11里,GUI 2D 已经不该继续默认建立在PoseStack心智上。
这不是代码风格问题,而是语义边界已经被 Mojang 明确拆开了。
4.5.1 为什么 GUI 不再继续沿用 PoseStack
旧时代把 PoseStack 到处传,有一个很大的便利:
- GUI 也能用。
- 世界渲染也能用。
- 粒子也能用。
- 看起来所有变换都统一了。
但这个“统一”其实很虚。
因为 GUI 2D 真正关心的是:
- 二维平移
- 二维缩放
- 二维旋转
- 变换后的屏幕边界
- 裁剪区域的传播
而 PoseStack 的语义中心是三维。
当你把 GUI 也绑在三维栈上时,长期后果通常是:
- 上层 API 总觉得
mulPoseMatrix(Matrix4f)是理所当然的。 - 很多 GUI helper 会偷偷依赖旧时代 model-view 的残留习惯。
- scissor、bounds、命中测试在二维坐标语义上会越来越混乱。
4.5.2 ScreenRectangle :Mojang 的真实意图
ScreenRectangle 在 1.21.11 中有两个关键方法:
java
public ScreenRectangle transformAxisAligned(Matrix3x2fc pose) {
Vector2f v0 = pose.transformPosition(this.left(), this.top(), new Vector2f());
Vector2f v1 = pose.transformPosition(this.right(), this.bottom(), new Vector2f());
return new ScreenRectangle(Mth.floor(v0.x), Mth.floor(v0.y), Mth.floor(v1.x - v0.x), Mth.floor(v1.y - v0.y));
}
public ScreenRectangle transformMaxBounds(Matrix3x2fc pose) {
Vector2f v0 = pose.transformPosition(this.left(), this.top(), new Vector2f());
Vector2f v1 = pose.transformPosition(this.right(), this.top(), new Vector2f());
Vector2f v2 = pose.transformPosition(this.left(), this.bottom(), new Vector2f());
Vector2f v3 = pose.transformPosition(this.right(), this.bottom(), new Vector2f());
float minX = Math.min(Math.min(v0.x(), v2.x()), Math.min(v1.x(), v3.x()));
float maxX = Math.max(Math.max(v0.x(), v2.x()), Math.max(v1.x(), v3.x()));
float minY = Math.min(Math.min(v0.y(), v2.y()), Math.min(v1.y(), v3.y()));
float maxY = Math.max(Math.max(v0.y(), v2.y()), Math.max(v1.y(), v3.y()));
return new ScreenRectangle(Mth.floor(minX), Mth.floor(minY), Mth.ceil(maxX - minX), Mth.ceil(maxY - minY));
}
这说明 Mojang 已经把 GUI 的边界计算明确建立在二维仿射变换上。
其中:
transformAxisAligned(...)更适合本身保持轴对齐的语义,例如部分 scissor 处理。transformMaxBounds(...)则更适合算“经过 2D 变换后的最大包围盒”。
这正是 GUI 最常见的需求。
4.5.3 GuiGraphics.enableScissor(...)
前面我们已经看过:
java
public void enableScissor(int minX, int minY, int maxX, int maxY) {
ScreenRectangle screenrectangle = new ScreenRectangle(minX, minY, maxX - minX, maxY - minY)
.transformAxisAligned(this.pose);
this.scissorStack.push(screenrectangle);
}
也就是说:
- GUI 当前变换栈是二维的。
- scissor 也跟着二维变换一起走。
- bounds 计算同样跟着二维变换一起走。
如果你的 GUI 抽象还在假设“只要传一个 PoseStack,之后再慢慢约定哪些操作只在二维使用”,那就已经落后于 Mojang 当前设计了。
4.5.4 对项目迁移意味着什么
如果你在迁移自己的 GUI 系统,那么至少应该做这几件事:
- GUIContext 级别引入 GUI 专用 2D 栈,而不是继续默认持有
PoseStack。 - GUI DSL 与 World DSL 拆开,不要再试图做一个“全场通用”的变换 DSL。
- 任何依赖
Matrix4f、mulPoseMatrix(...)、旧 model-view 假设的 GUI 代码,都应该被视为迁移风险点。 - GUI replay / retained flush 时,应该优先输出到
GuiGraphics能理解的二维语义对象。
4.5.5 GUI 2D 与 World 3D 的边界
可以先把边界总结成一句话:
::: align-center
GUI 2D 用 Matrix3x2fStack,World / 3D 继续主要用 PoseStack
:::
4.6 渲染状态不再靠手搓:RenderPipeline
如果说 GuiGraphics 和 GuiRenderState 是高版本 GUI 路径的入口,那么 RenderPipeline 就是高版本渲染架构里最关键的中层对象之一。
旧时代很多人做渲染设计时,脑子里的中心对象是:
ShaderInstanceRenderSystem当前状态- 顶点格式
- 当前纹理槽
到了 1.21.11,这个中心必须往 RenderPipeline 迁移。
4.6.1 RenderPipeline 到底在描述什么
先看 Mojang 的字段:
java
public class RenderPipeline {
private final Identifier location;
private final Identifier vertexShader;
private final Identifier fragmentShader;
private final ShaderDefines shaderDefines;
private final List<String> samplers;
private final List<RenderPipeline.UniformDescription> uniforms;
private final DepthTestFunction depthTestFunction;
private final PolygonMode polygonMode;
private final boolean cull;
private final Optional<BlendFunction> blendFunction;
private final boolean writeColor;
private final boolean writeAlpha;
private final boolean writeDepth;
private final VertexFormat vertexFormat;
private final VertexFormat.Mode vertexFormatMode;
private final float depthBiasScaleFactor;
private final float depthBiasConstant;
private final int sortKey;
}
这已经足够说明:
RenderPipeline 不是“shader 句柄”。
它至少同时描述了:
- shader
- define
- sampler
- uniform
- depth test
- blend
- cull
- color/depth write
- vertex format
- vertex topology
也就是说,Mojang 正在把“一个绘制到底属于哪种状态组合”显式对象化。
4.6.2 Builder 说明 pipeline 是正式的组合单位
再看 Builder 的接口:
java
public static class Builder {
public RenderPipeline.Builder withLocation(String location) { ... }
public RenderPipeline.Builder withFragmentShader(String fragmentShader) { ... }
public RenderPipeline.Builder withVertexShader(String vertexShader) { ... }
public RenderPipeline.Builder withShaderDefine(String flag) { ... }
public RenderPipeline.Builder withShaderDefine(String key, float value) { ... }
public RenderPipeline.Builder withSampler(String sampler) { ... }
public RenderPipeline.Builder withUniform(String uniform, UniformType type) { ... }
public RenderPipeline.Builder withDepthTestFunction(DepthTestFunction depthTestFunction) { ... }
public RenderPipeline.Builder withCull(boolean cull) { ... }
public RenderPipeline.Builder withBlend(BlendFunction blendFunction) { ... }
public RenderPipeline.Builder withDepthWrite(boolean writeDepth) { ... }
public RenderPipeline.Builder withVertexFormat(VertexFormat vertexFormat, VertexFormat.Mode vertexFormatMode) { ... }
}
读者会发现,高版本里真正稳定的组合单位已经变成:
“某一类绘制所需的整组渲染状态”
而不是:
“先拿一个 shader,然后在调用点临时拼剩下的状态。”
这就是为什么高版本不应该再以 ShaderInstance 为架构中心。
4.6.3 Snippet 在解决什么问题
Mojang 没有让每条 pipeline 从零开始声明,而是引入了 Snippet。
源码:
java
public static RenderPipeline.Builder builder(RenderPipeline.Snippet... snippets) {
RenderPipeline.Builder builder = new RenderPipeline.Builder();
for (RenderPipeline.Snippet snippet : snippets) {
builder.withSnippet(snippet);
}
return builder;
}
Snippet 的价值在于:
- 把公共状态片段复用出来。
- 让不同 pipeline 可以基于同一组基础约束拼装。
- 避免每个 pipeline 手动抄一遍所有状态。
这是一种非常典型的高版本设计信号:
Mojang 正在把 pipeline 当成正式架构层,而不是零散配置。
4.6.4 先看 Mojang 预定义的 GUI / 粒子 / Outline 片段
看 RenderPipelines:
java
public static final RenderPipeline.Snippet PARTICLE_SNIPPET = RenderPipeline.builder(MATRICES_FOG_SNIPPET)
.withVertexShader("core/particle")
.withFragmentShader("core/particle")
.withSampler("Sampler0")
.withSampler("Sampler2")
.withVertexFormat(DefaultVertexFormat.PARTICLE, VertexFormat.Mode.QUADS)
.buildSnippet();
public static final RenderPipeline.Snippet GUI_SNIPPET = RenderPipeline.builder(MATRICES_PROJECTION_SNIPPET)
.withVertexShader("core/gui")
.withFragmentShader("core/gui")
.withBlend(BlendFunction.TRANSLUCENT)
.withVertexFormat(DefaultVertexFormat.POSITION_COLOR, VertexFormat.Mode.QUADS)
.withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
.buildSnippet();
public static final RenderPipeline.Snippet GUI_TEXTURED_SNIPPET = RenderPipeline.builder(MATRICES_PROJECTION_SNIPPET)
.withVertexShader("core/position_tex_color")
.withFragmentShader("core/position_tex_color")
.withSampler("Sampler0")
.withBlend(BlendFunction.TRANSLUCENT)
.withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, VertexFormat.Mode.QUADS)
.withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
.buildSnippet();
public static final RenderPipeline.Snippet GUI_TEXT_SNIPPET = RenderPipeline.builder(TEXT_SNIPPET)
.withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
.buildSnippet();
public static final RenderPipeline.Snippet OUTLINE_SNIPPET = RenderPipeline.builder(MATRICES_PROJECTION_SNIPPET)
.withVertexShader("core/rendertype_outline")
.withFragmentShader("core/rendertype_outline")
.withSampler("Sampler0")
.withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
.withDepthWrite(false)
.withVertexFormat(DefaultVertexFormat.POSITION_TEX_COLOR, VertexFormat.Mode.QUADS)
.buildSnippet();
这几段非常有代表性。
它说明 Mojang 已经在显式区分:
- GUI 纯色
- GUI 纹理
- GUI 文本
- 粒子
- Outline
这些东西不再只是“同一个 shader 改几个参数”。
4.6.5 再看真正注册出的 pipeline
再看下面这些:
java
public static final RenderPipeline GUI = register(RenderPipeline.builder(GUI_SNIPPET).withLocation("pipeline/gui").build());
public static final RenderPipeline GUI_TEXTURED = register(RenderPipeline.builder(GUI_TEXTURED_SNIPPET).withLocation("pipeline/gui_textured").build());
public static final RenderPipeline GUI_INVERT = register(
RenderPipeline.builder(GUI_SNIPPET).withLocation("pipeline/gui_invert").withBlend(BlendFunction.INVERT).build()
);
public static final RenderPipeline GUI_TEXT_HIGHLIGHT = register(
RenderPipeline.builder(GUI_SNIPPET).withLocation("pipeline/gui_text_highlight").withBlend(BlendFunction.ADDITIVE).build()
);
这再次证明:
- pipeline 是正式注册对象。
- Mojang 通过 snippet 派生出具体 pipeline。
- 同一类基础语义上,可以再做少量状态变体。
4.6.6 这对我们自己的架构意味着什么
如果你自己还在设计一个“Shader管理器 统治一切”的体系,那么高版本里它的地位应该下降。
更合适的设计应该是:
UiPipelineRegistryWorldRenderPipelineRegistry- 必要时保留很小的 shader handle 作为过渡桥
因为真正稳定的上层边界已经变成:
- 这个绘制属于哪条路径
- 它应该落在哪个 pipeline
- 它需要哪些 sampler / uniform / vertex format
不要再把 RenderPipeline 当成 ShaderInstance 的皮肤
如果只是把旧设计里所有 ShaderInstance 字段机械替换成 RenderPipeline,
你会得到一个“看起来高版本、实际上还是旧架构”的系统。
4.6.7 高版本应该如何理解 pipeline
可以先用一句话总结:
::: align-center
高版本里,pipeline 是一类绘制的正式渲染契约
:::
这个契约不仅决定 shader,还决定这类绘制的资源绑定约束与状态边界。
4.7 NeoForge 的新注册入口:RegisterRenderPipelinesEvent
既然 pipeline 已经是正式架构对象,那么 mod 侧自然也需要一个正式注册入口。
NeoForge 给出了RegisterRenderPipelinesEvent
4.7.1 先看事件本身
源码很短,但很重要:
java
/**
* Fired to allow mods to register custom {@linkplain RenderPipeline pipelines}.
* This event is fired after the default Minecraft pipelines have been registered.
*/
public class RegisterRenderPipelinesEvent extends Event implements IModBusEvent {
private final Consumer<RenderPipeline> registrar;
public void registerPipeline(RenderPipeline pipeline) {
registrar.accept(pipeline);
}
}
这段话至少明确了三件事:
- 这是给 mod 注册自定义
RenderPipeline用的正式入口。 - 它发生在 Mojang 默认 pipeline 注册完成之后。
- NeoForge 没有让你去 patch 内部列表,而是给了显式 registrar。
4.7.2 这意味着什么
这意味着高版本的正确扩展方式是:
- 在 mod bus 上注册你自己的 pipeline。
- 让上层渲染语义显式依赖这些 pipeline。
- 不要再靠一堆 runtime hack 去模拟旧版 shader registration。
如果放到项目设计上,这就是为什么我们需要:
UiPipelineRegistryWorldRenderPipelineRegistry
而不是一个“大一统 ShaderRegistry”。
4.7.3 什么应该进入 UiPipelineRegistry
GUI pipeline 的典型候选包括:
- 标准 2D 纯色 / 纹理路径
- 自定义 GUI 贴图效果
- SDF / distance field 类矩形与边框
- GUI blur / mask / composite 需要的 pipeline
- ShaderTexture 一类 GUI 特化效果
这些 pipeline 有几个共同点:
- 它们属于 GUI path。
- 默认应建立在 GUI 2D 坐标与 GUI 资源绑定约束上。
- 不应该混入 world supplemental render 的假设。
4.7.4 什么应该进入 WorldRenderPipelineRegistry
World pipeline 的典型候选包括:
- Outliner
- world supplemental render
- 特定世界特效的补充渲染
- 未来需要接入世界通路的特殊 mesh / line / overlay
这些 pipeline 的约束和 GUI 完全不同:
- 它们通常需要 world / camera / fog / depth 的语义。
- 它们的 transform 主体不是 GUI 2D。
- 它们不能借用 GUI pipeline “先跑通再说”。
4.8 离屏渲染与资源生命周期:TextureTarget
如果说前面几节主要在重建 GUI 与 pipeline 语义,从这里开始,我们进入高版本离屏与资源生命周期的核心变化。
旧时代很多人一说离屏渲染,脑子里的第一反应是:
- 创建 FBO
- 绑颜色纹理
- 绑深度纹理
bindWrite()unbindWrite()- 拿
colorTextureId - 再手动画一个全屏 quad 合成
这套流程并没有“错”,但它已经不是 1.21.11 最值得依赖的上层心智了。
4.8.1 TextureTarget 是什么
源码很短:
java
public class TextureTarget extends RenderTarget {
public TextureTarget(@Nullable String name, int width, int height, boolean useDepth) {
this(name, width, height, useDepth, false);
}
public TextureTarget(@Nullable String name, int width, int height, boolean useDepth, boolean enableStencil) {
super(name, useDepth, enableStencil);
RenderSystem.assertOnRenderThread();
this.resize(width, height);
}
}
它本身不复杂,重点在于它继承的 RenderTarget。
4.8.2 RenderTarget 的内部已经不是旧 FBO 心智
看 RenderTarget 的核心字段:
java
public abstract class RenderTarget {
public int width;
public int height;
public final boolean useDepth;
public final boolean useStencil;
protected @Nullable GpuTexture colorTexture;
protected @Nullable GpuTextureView colorTextureView;
protected @Nullable GpuTexture depthTexture;
protected @Nullable GpuTextureView depthTextureView;
}
注意这里的关键词:
GpuTextureGpuTextureView
而不是:
- color attachment id
- depth attachment id
- framebuffer handle
这说明高版本在离屏资源表达上,已经明显转向现代 GPU 资源对象模型。
4.8.3 看看它是怎么创建资源的
java
public void createBuffers(int width, int height) {
RenderSystem.assertOnRenderThread();
GpuDevice device = RenderSystem.getDevice();
if (this.useDepth) {
this.depthTexture = device.createTexture(() -> this.label + " / Depth", 15, format, width, height, 1, 1);
this.depthTextureView = device.createTextureView(this.depthTexture);
}
this.colorTexture = device.createTexture(() -> this.label + " / Color", 15, TextureFormat.RGBA8, width, height, 1, 1);
this.colorTextureView = device.createTextureView(this.colorTexture);
}
这里的重点不是参数细节,而是资源创建的结构:
- 先通过
GpuDevice创建 texture - 再创建 texture view
- RenderTarget 持有的是资源对象与 view
这已经和旧时代“拿一堆 GL id 自己管理”完全不是一个抽象层次。
4.8.4 销毁语义也已经是资源对象生命周期
再看销毁:
java
public void destroyBuffers() {
RenderSystem.assertOnRenderThread();
if (this.depthTexture != null) {
this.depthTexture.close();
this.depthTexture = null;
}
if (this.depthTextureView != null) {
this.depthTextureView.close();
this.depthTextureView = null;
}
if (this.colorTexture != null) {
this.colorTexture.close();
this.colorTexture = null;
}
if (this.colorTextureView != null) {
this.colorTextureView.close();
this.colorTextureView = null;
}
}
这再次说明:
高版本在离屏层的核心语义已经变成资源生命周期管理,而不是手动记住几个 id。
4.8.5 copyDepthFrom(...) 与 blitAndBlendToTexture(...) 说明什么
看两段方法:
java
public void copyDepthFrom(RenderTarget otherTarget) {
RenderSystem.getDevice()
.createCommandEncoder()
.copyTextureToTexture(otherTarget.depthTexture, this.depthTexture, 0, 0, 0, 0, 0, this.width, this.height);
}
java
public void blitAndBlendToTexture(GpuTextureView textureView) {
try (RenderPass renderpass = RenderSystem.getDevice()
.createCommandEncoder()
.createRenderPass(() -> "Blit render target", textureView, OptionalInt.empty())) {
renderpass.setPipeline(RenderPipelines.ENTITY_OUTLINE_BLIT);
RenderSystem.bindDefaultUniforms(renderpass);
renderpass.bindTexture("InSampler", this.colorTextureView, RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST));
renderpass.draw(0, 3);
}
}
这两段非常关键。
它说明:
- 离屏 target 之间的资源复制已经通过 command encoder 表达。
- 合成也开始显式走 render pass + pipeline,而不是默认“绑回主 FBO 画个 quad”。
4.9 GPU资源不再只是一个texture id:GpuTexture与GpuTextureView
前一节里我们已经看到,RenderTarget内部保存的不是旧式colorTextureId,而是:
1.GpuTexture
2.GpuTextureView
这不是命名变化,而是资源模型变化。
4.9.1先看GpuTextureView本体
源码非常短:
java
public abstract class GpuTextureView implements AutoCloseable {
private final GpuTexture texture;
private final int baseMipLevel;
private final int mipLevels;
public GpuTexture texture() {
return this.texture;
}
public int baseMipLevel() {
return this.baseMipLevel;
}
public int mipLevels() {
return this.mipLevels;
}
public int getWidth(int mipLevel) {
return this.texture.getWidth(mipLevel + this.baseMipLevel);
}
public int getHeight(int mipLevel) {
return this.texture.getHeight(mipLevel + this.baseMipLevel);
}
public abstract boolean isClosed();
}
这段代码有几个非常关键的信号:
1.view不是纹理本身。
2.view引用一个底层GpuTexture。
3.view可以只看这个纹理的一部分mip层级。
4.view自己也有生命周期,需要close()。
4.9.2为什么高版本开始强调view
旧时代很多上层代码的默认想法是:
“我有一张纹理,所以我就有一个纹理句柄。”
这在现代GPU资源模型里太粗糙了。
因为你真正需要表达的经常不是:
“我拥有整个资源。”
而是:
“我现在要以某种方式访问这个资源的某一部分。”
GpuTextureView正是在表达这个语义。
它允许高层把“资源本体”和“当前访问视图”分开。
4.9.3这对离屏与合成为什么很重要
如果你继续沿用旧式colorTextureId心智,那么你会天然假设:
1.离屏目标暴露给上层的就是一张完整纹理。
2.采样、复制、合成都默认对整张纹理进行。
3.mip、子区域、后续视图变化都不是上层关心的问题。
但现在高版本已经把这些语义显式对象化了。
这意味着:
1.离屏输出更适合以GpuTextureView向后续pass暴露。
2.上层不该再直接依赖“某个整数纹理id”。
3.UIVisualLayer、视频上传、外部纹理接入等系统,都应该围绕view而不是裸资源句柄设计。
4.9.4GpuTextureView为什么说明资源所有权必须更严谨
再看RenderTarget.destroyBuffers()时对view的处理:
java
if (this.colorTextureView != null) {
this.colorTextureView.close();
this.colorTextureView = null;
}
这件事很值得强调。
因为它表明:
1.view不是随便借一下就完了的临时数据。
2.view和texture一样属于需要明确释放的GPU资源对象。
3.如果你的上层代码把view到处缓存却不管理生命周期,就会进入错误状态。
一个重要迁移结论
高版本里上层特效代码不应该继续把“纹理资源”抽象成一个整数id。
更稳定的边界应该是“能力导向接口+GpuTextureView或更高层资源句柄”。
4.10FrameGraph:真正的“新渲染系统骨架”
到了这里,我们终于可以进入高版本渲染系统里最像“架构中枢”的对象之一:FrameGraphBuilder
如果前面的GuiGraphics、GuiRenderState、RenderPipeline、TextureTarget是在告诉你“渲染对象和资源表达变了”,那么FrameGraphBuilder是在告诉你:
“pass调度与资源生命周期管理本身,也已经正式进入架构层。”
4.10.1什么是FrameGraph
可以先用最简单的话来定义:
FrameGraph就是一张描述“这一帧有哪些pass、这些pass依赖哪些资源、这些资源什么时候分配和释放”的图。
它要解决的核心问题不是“怎么画一个东西”,而是:
1.这一帧到底需要执行哪些pass。
2.哪些pass其实可以裁掉。
3.哪些资源在哪些pass之间共享。
4.这些资源应该何时获取、何时释放。
5.pass顺序如何根据依赖自动决定。
这和旧式“我先bind这个FBO,再手动调用下一步”是完全不同的思路。
4.10.2先看最关键的入口
源码:
java
public class FrameGraphBuilder {
private final List<FrameGraphBuilder.InternalVirtualResource<?>> internalResources = new ArrayList<>();
private final List<FrameGraphBuilder.ExternalResource<?>> externalResources = new ArrayList<>();
private final List<FrameGraphBuilder.Pass> passes = new ArrayList<>();
public FramePass addPass(String name) {
FrameGraphBuilder.Pass pass = new FrameGraphBuilder.Pass(this.passes.size(), name);
this.passes.add(pass);
return pass;
}
public <T> ResourceHandle<T> importExternal(String name, T resource) {
...
}
public <T> ResourceHandle<T> createInternal(String name, ResourceDescriptor<T> descriptor) {
...
}
}
这里已经能看出FrameGraph的三个基础元素:
1.pass
2.external resource
3.internal resource
4.10.3importExternal(...)与createInternal(...)分别表达什么
这两个入口非常关键。
importExternal(...)表达的是:
“这个资源不是由当前frame graph创建的,但本帧图会引用它。”
例如:
1.主屏幕目标
2.已有的离屏目标
3.从别处传进来的纹理资源
createInternal(...)表达的是:
“这个资源是当前frame graph内部产生并管理生命周期的资源。”
例如:
1.某个临时后处理target
2.某个中间mask纹理
3.某个只在数个pass之间存在的缓冲
这条边界非常重要,因为它决定资源生命周期是否由frame graph接管。
4.10.4ResourceHandle为什么比“直接传对象”更重要
FrameGraph没有让你把对象直接塞进pass,而是引入了ResourceHandle。
源码里Handle大致是这样:
java
static class Handle<T> implements ResourceHandle<T> {
final FrameGraphBuilder.VirtualResource<T> holder;
private final int version;
final FrameGraphBuilder.@Nullable Pass createdBy;
final BitSet readBy = new BitSet();
private FrameGraphBuilder.@Nullable Handle<T> aliasedBy;
@Override
public T get() {
return this.holder.get();
}
}
为什么这很重要?
因为handle表达的不只是“拿到对象”,而是:
1.这个资源是哪个虚拟资源的哪个版本。
2.它是谁创建的。
3.哪些pass读了它。
4.它是否已经被后续写入alias成新版。
也就是说,handle是资源数据流的一部分,不是普通引用。
4.10.5FrameGraph如何决定执行哪些pass
看execute(...)里的主流程:
java
public void execute(GraphicsResourceAllocator allocator, FrameGraphBuilder.Inspector inspector) {
BitSet keep = this.identifyPassesToKeep();
List<FrameGraphBuilder.Pass> ordered = new ArrayList<>(keep.cardinality());
BitSet visiting = new BitSet(this.passes.size());
for (FrameGraphBuilder.Pass pass : this.passes) {
this.resolvePassOrder(pass, keep, visiting, ordered);
}
this.assignResourceLifetimes(ordered);
for (FrameGraphBuilder.Pass pass : ordered) {
...
pass.task.run();
...
}
}
这里至少做了三件事:
1.识别哪些pass真的需要保留。
2.解析pass顺序。
3.根据最终顺序分配资源生命周期。
这就是为什么FrameGraph不是“高级任务列表”。它是在真正管理一帧渲染图。
4.10.6pass裁剪意味着什么
先看identifyPassesToKeep():
java
private BitSet identifyPassesToKeep() {
for (FrameGraphBuilder.VirtualResource<?> virtualresource : this.externalResources) {
FrameGraphBuilder.Pass pass = virtualresource.handle.createdBy;
if (pass != null) {
this.discoverAllRequiredPasses(pass, bitset, deque);
}
}
for (FrameGraphBuilder.Pass pass : this.passes) {
if (pass.disableCulling) {
this.discoverAllRequiredPasses(pass, bitset, deque);
}
}
}
这说明不是所有你“加进来”的pass最后都会执行。
只有那些:
- 会影响外部资源产出
- 或者被明确标记不能裁剪的pass
才会被保留下来。这是一种很现代的图式优化思维。
4.10.7资源生命周期是怎么被自动安排的
再看assignResourceLifetimes(...):
java
private void assignResourceLifetimes(Collection<FrameGraphBuilder.Pass> passes) {
FrameGraphBuilder.Pass[] lastUsers = new FrameGraphBuilder.Pass[this.internalResources.size()];
for (FrameGraphBuilder.Pass pass : passes) {
for (int i = pass.requiredResourceIds.nextSetBit(0); i >= 0; i = pass.requiredResourceIds.nextSetBit(i + 1)) {
FrameGraphBuilder.InternalVirtualResource<?> resource = this.internalResources.get(i);
FrameGraphBuilder.Pass previous = lastUsers[i];
lastUsers[i] = pass;
if (previous == null) {
pass.resourcesToAcquire.add(resource);
} else {
previous.resourcesToRelease.clear(i);
}
pass.resourcesToRelease.set(i);
}
}
}
这段代码表达的语义非常清楚:
1.资源第一次被用到时获取。
2.资源最后一次被用到的pass负责释放。
这就是资源生命周期自动化。
所以以后再谈高版本离屏、后处理、多pass合成时,正确方向应该是:
1.把pass关系表达清楚。
2.把资源流表达清楚。
3.让图自己决定分配和释放。
而不是继续手动写一串bind/unbind流程。
4.10.8用图看FrameGraph
这个图当然简化了很多,但它足够帮助你建立正确印象:
1.pass不是孤立函数。
2.资源不是任意全局对象。
3.输出关系决定pass是否需要被保留。
不要把FrameGraph当成“更好看的后处理链表”
后处理链表只是在描述顺序。而FrameGraph描述的是顺序、依赖、资源版本和生命周期。
4.11CommandEncoder/RenderPass/GpuDevice:更底层的新GPU语义
如果说RenderPipeline在定义“我要以什么渲染契约绘制”,FrameGraphBuilder在定义“这一帧有哪些pass和资源关系”,那么再往下一层,你就会看到真正的GPU操作接口:
1.GpuDevice
2.CommandEncoder
3.RenderPass
这几个对象非常重要,因为它们说明Mojang正在继续把底层后端抽象往现代图形API方向推进。
4.11.1先看GpuDevice
源码:
java
public interface GpuDevice {
CommandEncoder createCommandEncoder();
GpuSampler createSampler(...);
GpuTexture createTexture(...);
GpuTextureView createTextureView(GpuTexture texture);
GpuTextureView createTextureView(GpuTexture texture, int baseMipLevel, int mipLevels);
GpuBuffer createBuffer(...);
CompiledRenderPipeline precompilePipeline(RenderPipeline pipeline, @Nullable ShaderSource shaderSource);
}
从这个接口你就能看出,GpuDevice已经是一个非常典型的设备抽象了。
它负责:
1.创建命令编码器
2.创建纹理
3.创建视图
4.创建buffer
5.预编译pipeline
所以高版本里真正的资源创建中心,不应该再是你自己散落在各处的OpenGL调用。
4.11.2CommandEncoder在表达什么
再看CommandEncoder:
java
public interface CommandEncoder {
RenderPass createRenderPass(Supplier<String> debugGroup, GpuTextureView colorTexture, OptionalInt clearColor);
void clearColorTexture(GpuTexture texture, int color);
void writeToBuffer(GpuBufferSlice slice, ByteBuffer buffer);
void writeToTexture(GpuTexture texture, NativeImage image);
void copyTextureToTexture(GpuTexture source, GpuTexture destination, int mipLevel, int x, int y, int sourceX, int sourceY, int width, int height);
void presentTexture(GpuTextureView texture);
}
这个接口非常像现代GPUAPI里的命令录制层。
它关心的不是“当前全局状态是什么”,而是:
1.我现在要创建一个render pass。
2.我要清理哪个资源。
3.我要把哪段数据写入哪个buffer或texture。
4.我要把哪个纹理复制到另一个纹理。
5.我要把哪个最终view呈现到屏幕。
这已经和旧式RenderSystem状态机思维有本质区别了。
4.11.3RenderPass在表达什么
再看RenderPass:
java
public interface RenderPass extends AutoCloseable {
void setPipeline(RenderPipeline pipeline);
void bindTexture(String name, @Nullable GpuTextureView textureView, @Nullable GpuSampler sampler);
void setUniform(String name, GpuBuffer buffer);
void setViewport(int x, int y, int width, int height);
void enableScissor(int x, int y, int width, int height);
void setVertexBuffer(int index, GpuBuffer buffer);
void setIndexBuffer(GpuBuffer indexBuffer, VertexFormat.IndexType indexType);
void drawIndexed(int firstIndex, int index, int indexCount, int primCount);
void draw(int firstIndex, int indexCount);
}
这说明真正的绘制提交通常发生在pass内部。
而且pass内部的关键动作已经被明确成:
- 选择pipeline
- 绑定texture与sampler
- 绑定uniform buffer
- 设置viewport/scissor
- 绑定vertex/index buffer
- 发起draw
这套语义非常接近现代图形API的正式提交流程。
4.11.4为什么这说明上层不该继续直接依赖OpenGL
因为如果Mojang已经在提供:
- 设备抽象
- 资源抽象
- 视图抽象
- pass抽象
- pipeline抽象
那么上层UI/特效/世界补充渲染继续直接依赖原始OpenGL调用,就等于在主动绕过高版本正式架构。而且在之后迁移到VK时会造成巨大的迁移灾难
4.11.5把这三层关系串起来
现在我们可以把高版本渲染提交主线画成这样:
本章的介绍到此结束
全部评论 (0)
暂无评论,快来抢沙发吧~