NeoForge-1.20.4Mod开发教程之烘焙模型

什么是烘焙模型

未烘焙模型(Unbaked)

它描述的是“这个模型应该长什么样、用哪些纹理、有哪些部件、要不要套父模型、display 怎么变换”等——但它还不是能直接拿去渲染的东西。

在 Forge/NeoForge 里,未烘焙来源可能是:

  • 原版的 UnbakedModel(从 JSON 读出来)

  • 自定义加载器(比如 OBJ loader)得到的 IUnbakedGeometry(从 obj/mtl 读出来)

烘焙模型(BakedModel)

它是把未烘焙模型在加载资源阶段执行一次 bake() 之后得到的结果: 模型的形状、纹理引用、各种规则都会被解析/展开成更接近直接渲染的数据,通常就是一堆 Quads(四边形面片),渲染时几乎可以直接交给 GPU

 

“烘焙”具体做了什么

假设你有一个 models/item/fire_axe.json

  • 加载阶段读 JSON:得到“未烘焙信息”(路径、材质名、纹理名、面数据、uv……)

  • 烘焙阶段 bake() 会做这些事:

    1. 把父模型/引用关系展开(JSON 继承 parent 的那种)

    2. 把纹理引用解析成真正可用的纹理对象(从资源定位符变成可渲染用的贴图)

    3. 把几何形状“转换成 Quads 列表”(最常见:getQuads() 里返回这些四边形面片)

    4. 把一些能提前算的东西提前算好

    5. (可选)决定不同渲染通道/RenderType、不同 pass 怎么画(比如透明、cutout、多层)

烘焙模型就是“渲染前的预处理结果”

 

现在我们制作一个伪装方块,它会根据它下面的方块改变渲染的模型

 

创建HiddenBlockModel类

public class HiddenBlockModel implements BakedModel {
     BakedModel defaultModel;//保存原始模型
     public static ModelProperty<BlockState> COPIED_BLOCK = new ModelProperty<>();//储存要伪装成的方块 BlockState
 ​
     public HiddenBlockModel(BakedModel existingModel) {
         this.defaultModel = existingModel;
    }
 ​
     @Override
     public @NotNull List<BakedQuad> getQuads(@Nullable BlockState state, @Nullable Direction side, @NotNull RandomSource rand, @NotNull ModelData data, @Nullable RenderType renderType) {
         BakedModel renderModel = defaultModel;//先默认使用 defaultModel
         if (data.has(COPIED_BLOCK)) {
             BlockState copiedBlock = data.get(COPIED_BLOCK);//如果data包含COPIED_BLOCK:取出 copiedBlock
             if (copiedBlock != null) {
                 Minecraft mc = Minecraft.getInstance();//获取客户端
                 BlockRenderDispatcher blockRendererDispatcher = mc.getBlockRenderer();//拿到方块渲染总管
                 renderModel = blockRendererDispatcher.getBlockModel(copiedBlock);//通过getBlockModel(copiedBlock)取得对应方块的 BakedModel,作为实际渲染模型
            }
        }
         return renderModel.getQuads(state,side,rand,data,renderType);
    }
 ​
     @Override
     public @NotNull ModelData getModelData(@NotNull BlockAndTintGetter level, @NotNull BlockPos pos, @NotNull BlockState state, @NotNull ModelData modelData) {//用于在渲染前构造/更新 ModelData
         BlockState downBlockState = level.getBlockState(pos.below());//获取下方方块
         ModelData modelDataMap = modelData.derive().with(COPIED_BLOCK,null).build();
 ​
         if(downBlockState.getBlock() == Blocks.AIR || downBlockState.getBlock() == ModBlocks.HIDDEN_BLOCK.get()){//如果下方是空气或下方也是隐藏方块
             return modelDataMap;//直接返回清空后的 modelDataMap,表示“不伪装”
        }
 ​
         return modelDataMap.derive().with(COPIED_BLOCK,downBlockState).build();
    }
 ​
     @Override
     public @NotNull List<BakedQuad> getQuads(@Nullable BlockState pState, @Nullable Direction pDirection, @NotNull RandomSource pRandom) {//旧接口签名,强制抛异常
         throw new AssertionError("IBakedModel::getQuads should never be called, only IForgeBakedModel::getQuads");//只希望走NeoForge扩展接口版本,即上面的getQuads
    }
 ​
     @Override
     public boolean useAmbientOcclusion() {
         return defaultModel.useAmbientOcclusion();
    }
 ​
     @Override
     public boolean isGui3d() {
         return defaultModel.isGui3d();
    }
 ​
     @Override
     public boolean usesBlockLight() {
         return defaultModel.usesBlockLight();
    }
 ​
     @Override
     public boolean isCustomRenderer() {
         return defaultModel.isCustomRenderer();
    }
 ​
     @Override
     public @NotNull TextureAtlasSprite getParticleIcon() {
         return defaultModel.getParticleIcon();
    }
 ​
     @Override
     public @NotNull ItemOverrides getOverrides() {
         return defaultModel.getOverrides();
    }
 }

ModelProperty<T> 是NeoForge渲染系统里给ModelData 做键的对象,相当于一个带类型的key

我们可以把ModelData理解成一个Map,而ModelProperty<T> 就是这个Map的 key,并且固定了value的类型T ModelProperty<T> 只是“标识符”,不存数据

数据实际存放在ModelData里,通过 modelData.get(COPIED_BLOCK) / modelData.with(COPIED_BLOCK, value) 访问 因为它是 key,所以一般都声明为 public static final,确保全局唯一且稳定

这里 ModelProperty<BlockState> COPIED_BLOCK 表示:这个键对应的值类型必须是 BlockState

简化类比:

ModelProperty<BlockState> ≈ MapKey<BlockState>

ModelData ≈ Map<ModelProperty<?>, Object>(但内部有类型检查)

在HiddenBlockModel里,它用来在渲染时告诉模型“我应该伪装成哪个方块”

 

ModelData可以把它理解成:“给模型渲染用的额外参数包(键值对)”

普通的方块/物品模型只靠 BlockState 往往不够(比如:方块实体里存了“复制的方块状态”“里面装了什么”“朝向之外的自定义状态”),这时就用 ModelData 把这些额外信息BlockEntity(或世界上下文)传给模型,让模型在 getQuads(...) 渲染时能根据这些数据决定“画哪一套quads/用哪张纹理/显示哪一部分”

NeoForge 文档明确说:当模型需要依赖BlockEntity来决定渲染内容时,可以在BlockEntity#getModelData 返回数据,并在需要时调用BlockEntity#requestModelDataUpdate让客户端更新,然后渲染侧通过世界/位置取到这份ModelData来使用。

ModelData 存的是“键 → 值”(上面已经说过了)

它常用的方法就是has(...)get(...)builder()/derive().with(...)这类,用来检查/读取/构建这份额外数据

 

创建 HiddenBlock类

public class HiddenBlock extends Block {
     public HiddenBlock() {
         super(Properties.ofFullCopy(Blocks.STONE).noOcclusion());
    }
 }

 

创建ModEvent类

public class ModEvent {
     @Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD,value = Dist.CLIENT)
 ​
     public static class ModEventsBus{
         @SubscribeEvent
         public static void onModelBaked(ModelEvent.ModifyBakingResult event) {//用我们写的模型重新包装替换默认的模型
             for (BlockState blockstate : ModBlocks.HIDDEN_BLOCK.get().getStateDefinition().getPossibleStates()) {//遍历hiddenblock所有的blockstate,确保都被替换
                 ModelResourceLocation modelResourceLocation = BlockModelShaper.stateToModelLocation(blockstate);//把BlockState转成模型键(MRL)
                 BakedModel existingModel = event.getModels().get(modelResourceLocation);//从烘焙结果里取出现有模型
                 if (existingModel == null) {
                     throw new RuntimeException("Did not find Hidden in registry");
                } else if (existingModel instanceof HiddenBlockModel) {
                     throw new RuntimeException("Tried to replace Hidden twice");
                } else {
                     HiddenBlockModel HiddenBlockModel = new HiddenBlockModel(existingModel);
                     event.getModels().put(modelResourceLocation, HiddenBlockModel);
                }
            }
        }
    }
 }

模型键(MRL)一般指 ModelResourceLocation(缩写常写 MRL),它就是在模型表里定位某一个烘焙模型(BakedModel)的键

 

以下是整体渲染流程:

1. 初始化与注册阶段(启动时)

1.1 ExampleMod 构造函数注册方块等内容

  • 代码位置:E:\NeoForgeExample\src\main\java\com\fanxien\examplemod\ExampleMod.java

  • 这里调用 ModBlocks.register(modEventBus),把 hidden_block 注册进游戏

1.2 HiddenBlock 本身没有自定义渲染逻辑

  • 代码位置:E:\NeoForgeExample\src\main\java\com\fanxien\examplemod\block\custom\HiddenBlock.java

  • 只是 Properties.ofFullCopy(Blocks.STONE).noOcclusion() 等于一个普通方块,默认渲染形态仍是 MODEL

2. 资源加载与模型烘焙阶段(客户端资源加载)

2.1 默认流程:blockstate → model JSON → 烘焙为 BakedModel

  • 这是 Minecraft/NeoForge 的默认流程

  • hidden_block 来说,会生成一个“默认烘焙模型”existingModel

2.2 烘焙完成后:把默认模型替换成 HiddenBlockModel

  • 代码位置:E:\NeoForgeExample\src\main\java\com\fanxien\examplemod\event\ModEvent.java

  • 触发点:ModelEvent.ModifyBakingResult

  • 关键逻辑:

    • 遍历 hidden_block 的所有可能 BlockState

    • BlockModelShaper.stateToModelLocation 找到对应 ModelResourceLocation

    • event.getModels() 取出已经烘焙好的 existingModel

    • new HiddenBlockModel(existingModel) 包装

    • 再放回 event.getModels(),完成替换

  • 结果:后续所有渲染拿到的模型都变成 HiddenBlockModel

3. 世界渲染阶段(渲染每个方块时)

3.1 BlockRenderDispatcher 获取模型

  • 调用:BlockRenderDispatcher.getBlockModel(state)

  • 返回的已经是你替换后的 HiddenBlockModel

3.2 BakedModel#getModelData 被调用(准备模型数据,通常在区块重建时)

  • 调用方法:HiddenBlockModel.getModelData(...)

  • 代码位置:E:\NeoForgeExample\src\main\java\com\fanxien\examplemod\client\model\HiddenBlockModel.java

  • 在该方法里:

    • 读取 pos.below()BlockState

    • 如果下方是空气或也是隐藏方块 → 不伪装

    • 否则把下方 BlockState 放进 ModelDataCOPIED_BLOCK

3.3 BakedModel#getQuads 被调用(最终决定渲染成什么样子)

  • 调用方法:HiddenBlockModel.getQuads(..., ModelData data, RenderType renderType)

  • 逻辑:

    • 默认 renderModel = defaultModel

    • 如果 data 里带了 COPIED_BLOCK

      • 取出 BlockState copiedBlock

      • Minecraft.getInstance().getBlockRenderer().getBlockModel(copiedBlock) 拿到“下方方块”的模型

      • 把它作为真正的渲染模型

    • 最后返回 renderModel.getQuads(...) 的结果

  • 结果:渲染出来的几何数据是“下方方块”的模型

5. 代码里的“最关键节点”

  1. 模型替换入口

    • 位置:E:\NeoForgeExample\src\main\java\com\fanxien\examplemod\event\ModEvent.java

    • 事件:ModelEvent.ModifyBakingResult

  2. 伪装决策点

    • 位置:E:\NeoForgeExample\src\main\java\com\fanxien\examplemod\client\model\HiddenBlockModel.java

    • 方法:getModelData(...)

  3. 渲染输出点

    • 位置:E:\NeoForgeExample\src\main\java\com\fanxien\examplemod\client\model\HiddenBlockModel.java

    • 方法:getQuads(..., ModelData data, RenderType)

© 版权声明
THE END
喜欢就支持一下吧
点赞6赞赏 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容