什么是烘焙模型
未烘焙模型(Unbaked)
它描述的是“这个模型应该长什么样、用哪些纹理、有哪些部件、要不要套父模型、display 怎么变换”等——但它还不是能直接拿去渲染的东西。
在 Forge/NeoForge 里,未烘焙来源可能是:
-
原版的
UnbakedModel(从 JSON 读出来) -
自定义加载器(比如 OBJ loader)得到的
IUnbakedGeometry(从 obj/mtl 读出来)
烘焙模型(BakedModel)
它是把未烘焙模型在加载资源阶段执行一次 bake() 之后得到的结果: 模型的形状、纹理引用、各种规则都会被解析/展开成更接近直接渲染的数据,通常就是一堆 Quads(四边形面片),渲染时几乎可以直接交给 GPU
“烘焙”具体做了什么
假设你有一个 models/item/fire_axe.json:
-
加载阶段读 JSON:得到“未烘焙信息”(路径、材质名、纹理名、面数据、uv……)
-
烘焙阶段 bake() 会做这些事:
-
把父模型/引用关系展开(JSON 继承 parent 的那种)
-
把纹理引用解析成真正可用的纹理对象(从资源定位符变成可渲染用的贴图)
-
把几何形状“转换成 Quads 列表”(最常见:
getQuads()里返回这些四边形面片) -
把一些能提前算的东西提前算好
-
(可选)决定不同渲染通道/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放进ModelData的COPIED_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. 代码里的“最关键节点”
-
模型替换入口
-
位置:
E:\NeoForgeExample\src\main\java\com\fanxien\examplemod\event\ModEvent.java -
事件:
ModelEvent.ModifyBakingResult
-
-
伪装决策点
-
位置:
E:\NeoForgeExample\src\main\java\com\fanxien\examplemod\client\model\HiddenBlockModel.java -
方法:
getModelData(...)
-
-
渲染输出点
-
位置:
E:\NeoForgeExample\src\main\java\com\fanxien\examplemod\client\model\HiddenBlockModel.java -
方法:
-







暂无评论内容