面板渲染管线

[!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

绘制顺序:

  1. 关闭 GL_LIGHTING / GL_CULL_FACE / GL_DEPTH_TEST
  2. 除非透明,画九宫格背景 —— 拉伸原版 textures/gui/demo_background.png(BG_TEX = 256,源区域 248×166,BORDER = 4)
  3. 画标题名牌
  4. 平移到 originX, originY + yShift,调 handler.drawBackground(recipeIndex)
  5. 逐个画冻结的材料/副产物/产物栈
  6. 画角标(概率、NC)
  7. 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 消息在缺席时是空操作。

相关条目