面板渲染管线
[!INFO] 源码
client/PanelFboManager.java、client/RecipePanelRenderer.java、client/RecipePanelItemRenderer.java、client/RecipePanelWaila.java、client/PanelColors.java
NEI 的配方绘制是 GUI 代码,不能直接丢进世界里跑(物品模型会戳出来、图层会 z-fight)。本 mod 的做法是:把每块面板的配方用 NEI 自己的绘制代码渲染进一张离屏 framebuffer,再由 TileEntity 特殊渲染器把这一张纹理当作单个平面四边形 blit 到方块面上。
帧调度
PanelFboManager.INSTANCE 注册在 FML 事件总线上,@SubscribeEvent onRenderTick(RenderTickEvent) 只在 phase == START 且有缓存面板时工作,且要求 OpenGlHelper.framebufferSupported。
| 缓存结构 | 类型 | 说明 |
|---|---|---|
panels |
IdentityHashMap<RecipePanelTile, Panel> |
按 TileEntity 身份索引,不走 TileEntity 的 equals |
lastSeen |
HashMap<RecipePanelTile, Integer> |
记录上次被 TESR 画到的帧号 |
frame |
int | 每个渲染 tick 自增 |
重绘条件(due):
panel.isStale(snapshot, settings)——renderedFor != snapshot || renderedSettings != settings,用 NBT 标签对象身份比较,所以放置后第一次、改设置后都会立刻重绘- 或面板在屏上(
frame - seen <= 2)且frame - panel.lastRenderFrame >= ANIM_INTERVAL_FRAMES(3)—— 定期重绘让 GregTech 的动画贴图继续走动
本帧有任何面板重绘过,结束时统一 Minecraft.getMinecraft().getFramebuffer().bindFramebuffer(true) 恢复主 framebuffer。
超过 EVICT_AFTER_FRAMES(200 帧)没被看到的面板会被 dispose() 掉 FBO 并从两个 map 移除,避免离屏缓冲无限增长。
ClientProxy.init 还向 IReloadableResourceManager 注册了一个重载监听器,资源包重载时调 PanelFboManager.reload(),丢弃全部缓存让面板按新资源重建(主题色、贴图集都会变)。
画布尺寸
Panel.resolve 先取 GuiRecipeTab.getHandlerInfo(handler) 的 getWidth / getHeight(拿不到就用 HandlerInfo.DEFAULT_WIDTH / DEFAULT_HEIGHT)与 getYShift(),然后把每个已解析槽位和产物坐标都并进包围盒——注释说明这是因为 GregTech 机器配方会画到 HandlerInfo 声明的框之外。
| 常量 | 值 | 含义 |
|---|---|---|
INNER_PAD |
14 | 方形 framebuffer 内四周留的留白(配方像素) |
MIN_SIDE |
176 | framebuffer 边长下限 |
MAX_SIDE |
360 | framebuffer 边长上限 |
SUPERSAMPLE |
3 | framebuffer 纹理边长 = 边长 × 3 |
边长 = clamp(max(bw, bh) + 2 × 14, 176, 360),并且强制取正方形(width = height = side),这样所有面板在墙上看起来一样大。居中偏移 originX / originY 把配方的包围盒摆到正中。
framebuffer 用 new Framebuffer(texW, texH, true) 创建——带深度缓冲,因为 GuiContainerManager.drawItem 会开 GL_DEPTH_TEST,GregTech 的多遍图标与 3D 方块模型需要深度来做合成与自遮挡。
投影与绘制顺序
- 正交投影保持在配方像素单位:
glOrtho(0, fw, fh, 0, 1000, 3000),再glTranslatef(0, 0, -2000) - 视图口放大到 3 倍来做超采样,刻意不用
glScalef——注释写明 GregTech 的自定义物品渲染器在非单位 modelview 缩放下会出错 - 渲染完调
buildMipmaps(),让面板远看或斜看时文字不爬行;驱动不支持glGenerateMipmap时回落到纯GL_LINEAR
绘制顺序:
- 关闭
GL_LIGHTING/GL_CULL_FACE/GL_DEPTH_TEST - 除非透明,画九宫格背景 —— 拉伸原版
textures/gui/demo_background.png(BG_TEX = 256,源区域 248×166,BORDER = 4) - 画标题名牌
- 平移到
originX, originY + yShift,调handler.drawBackground(recipeIndex) - 逐个画冻结的材料/副产物/产物栈
- 画角标(概率、NC)
handler.drawForeground(recipeIndex)
第 5 步画的是 ResolvedRecipe 里的 slot.stack(刻印时定下的那个循环变体),所以排列不会自己跳;但贴图集层面的动画照常播放——RecipeSnapshot 注释说明这正是"变体冻结、动画继续"的原因。
α 修复:GT 的物品渲染器可能把 alpha 通道清零,直接 blit 就会透出世界。所以绘制末尾用 glColorMask(false, false, false, true) 只重写 alpha:透明面板时把每个槽位矩形回填为不透明(保留背景可透视),不透明面板时直接重画一遍九宫格背景(保住它可能是圆角的形状)。
标题名牌取 customName 或 frozen.recipeName,用 font.trimStringToWidth(text, fw - 2*BORDER - 8) 截断后水平居中画在 y = 5。
TESR — RecipePanelRenderer
| 常量 | 值 | 含义 |
|---|---|---|
PANEL_RGB |
0xC6C6C6 |
还没渲染好时画的纯色占位 |
MAX_EXTENT |
0.92 |
面板四边形占方块面的比例 |
REACH |
0.5 - 0.01 |
四边形离方块中心、朝自己那一面的偏移 |
GL_LIGHTING关闭 +OpenGlHelper.setLightmapTextureCoords(lightmapTexUnit, 240F, 240F)全亮——它镜像的是 GUI,方块光/天空光与火把色不该染色panel.ready()为真才绑纹理;panel.transparent()为真才开GL_BLEND(SRC_ALPHA/ONE_MINUS_SRC_ALPHA),否则关掉- 还没 ready 就画一个
0xC6C6C6的实心四边形——所以刚放下的面板不会凭空消失,而是先出现灰块 orientOutward按ForgeDirection旋转,六个朝向全覆盖
物品栏图标替换
RecipePanelItemRenderer 实现 Forge 的 IItemRenderer,通过 ClientProxy.init 的 MinecraftForgeClient.registerItemRenderer(ModItems.recipePanel, ...) 绑定。
handleRenderType 只在同时满足三个条件时接管:渲染类型是 INVENTORY、玩家正按住 Shift、且 ItemRecipePanel.getResult(item) != null。也就是在物品栏里按住 Shift 查看时,已刻印的面板显示成它那条配方的产物图标,而不是那张纸。
renderItem 用 RenderItem.getInstance().renderItemAndEffectIntoGUI(...) 画产物,整个调用包在 try/catch (Throwable) 里,失败只 LOG.warn,不会让物品栏渲染崩掉。
Waila 悬停
RecipePanelWaila 实现 Waila 的 IWailaDataProvider,通过 ClientProxy.preInit 的 IMC 注册:
FMLInterModComms.sendMessage("Waila", "register", "com.neirecipepanels.client.RecipePanelWaila.callbackRegister")- 回调里
registrar.registerStackProvider(new RecipePanelWaila(), RecipePanelTile.class)
getWailaHead / getWailaBody / getWailaTail / getNBTData 全是原样返回的直通实现,唯一有逻辑的是 getWailaStack:它把命中位置反投影回配方像素坐标,交给 PanelFboManager.Panel.stackAt 查出光标下那个格子的物品。这样悬停面板时 Waila 报的是画面上的材料/产物,而不是面板方块本身。
stackAt 的命中判定是 16×16 像素的矩形(inSlot 先加 originX / originY + yShift + rely),依次匹配材料格、副产物格,最后是产物格;都不中返回 null。
hoveredStack 里的 v 翻转有注释解释:正交投影把配方像素 y=0 放在顶部,而 FBO 纹理的 v=0 采到的是最底一行,所以要 fy = (int)(panel.height * (1F - v))。u/v 越界(u < 0 || u > 1 || v < 0 || v > 1)返回 null。
兼容性处理
GregTech
| 位置 | 处理 |
|---|---|
PanelColors |
Loader.isModLoaded("gregtech") 为真时通过 GUIColorOverride.get(new ResourceLocation("gregtech", "textures/gui/background/nei_single_recipe.png")) 读资源包主题色,否则返回写死的回退值 |
PanelFboManager.useFlatIcon |
见下 |
ResolvedRecipe |
调 handler.getRecipeName() 顺带触发 GT handler 懒加载它的主题化 NEI 文字颜色(GuiRecipe 每帧都调,所以这里也顺带预热) |
PanelColors 用的两个键与回退值:
| 用途 | 键 | 回退 |
|---|---|---|
| 标题(不透明背景) | title |
0x404040 |
| 标题(透明背景,带阴影) | text_white |
0xFFFFFF |
PanelColors 每次调用都重读而不缓存,注释说明这样资源包变更(会让 GregTech 共享缓存失效)无需重启即可生效。
useFlatIcon 判定 GT 的 meta 生成物品(齿轮、板、粉等):若物品是 IGT_ItemWithMaterialRenderer 且不是 IFluidContainerItem,且 NBT 里没有 mFluidDisplayAmount / mFluidDisplayHeat,且注册了 INVENTORY 渲染器,且 getIcon(stack, 0) 非 null —— 就改画普通的多遍平面图标。注释强调范围刻意收窄到 GT:别的 mod(AE2 线缆、GT 流体罐)走标准路径渲染正常,应该保留它们本来的外观。
绘制时按 getSpriteNumber() 决定绑方块贴图集还是物品贴图集(AE2 部件这类图标来自方块集),并逐遍应用 getColorFromItemStack 上色。
Waila
见上一节。Waila 缺席时 dependencies.gradle 里它是 devOnlyNonPublishable(仅开发期依赖,不作为本 mod 的发布依赖),那条 IMC 消息在缺席时是空操作。