理解模型系统
Minecraft 模型本质上是附带纹理的四边形列表。建模流程的各部分有独立实现,底层模型 JSON 会被反序列化为 UnbakedModel。最终,各条渲染管线接收 List<BakedQuad> 以及自身所需的属性。一些方块实体渲染器也使用这些模型。模型复杂度没有限制。
模型保存在 ModelManager 中,可通过 Minecraft.getInstance().getModelManager() 访问。物品管线中,可以向 ModelManager#getItemModel 传入 Identifier 获取对应 ItemModel;方块状态管线中,则向 ModelManager.getBlockStateModelSet().get() 传入 BlockState 获取 BlockStateModel。Mod 基本都会复用已自动加载并烘焙的模型。
通用模型与几何体
assets/<namespace>/models 中的基础模型 JSON 反序列化为 UnbakedModel。它通常距离烘焙结果只差一步,以通用形式包含各种属性。其中最重要的是 UnbakedModel#geometry 提供的 UnbakedGeometry,表示将要转换成 BakedQuad 的数据。物品和方块状态模型最终调用 UnbakedGeometry#bake,把这些四边形内联到自身。它通常构造 QuadCollection,保存四边形列表;某些四边形始终渲染,另一些只有指定方向未被剔除时才渲染。这里的四边形类似建模软件及多数游戏中的三角形,但 Minecraft 以方块为主,因此开发者选择用四顶点四边形,而非三顶点三角形渲染。
UnbakedModel 中的信息可能由方块状态定义、物品模型或两者使用。例如 useAmbientOcclusion 仅供方块状态定义使用,guiLight 和 transforms 仅供物品模型使用,textureSlots 和 parent 则两者共用。
烘焙期间,每个 UnbakedModel 都包装为 ResolvedModel,由物品或方块状态的 ModelBaker 获取。顾名思义,它是已解析全部引用的未烘焙模型。通过 getTop* 方法可以计算并取得当前模型及其父模型的属性与几何体。通常在这里调用 ResolvedModel#bakeTopGeometry,将其烘焙为 QuadCollection。
方块状态定义
assets/<namespace>/blockstates 中的方块状态定义 JSON 会为每个 BlockState 编译并烘焙出 BlockStateModel,过程如下:
- 加载阶段:
- 将 JSON 加载为
BlockStateModel.UnbakedRoot。根对象是共用缓存系统,用于将状态关联到一组模型。 - 根对象加载
BlockStateModel.Unbaked,准备将其关联到适当状态。 - 未烘焙模型加载
BlockStateModelPart.Unbaked,后者用于获取通用UnbakedModel,更准确地说是ResolvedModel。
- 将 JSON 加载为
- 烘焙阶段:
- 为每个
BlockState调用BlockStateModel.UnbakedRoot#bake。 - 为指定状态调用
BlockStateModel.Unbaked#bake,创建BlockStateModel。 - 为模型中的各部分调用
BlockStateModelPart.Unbaked#bake,把ResolvedModel内联为QuadCollection,默认还会取得环境光遮蔽设置、粒子图标和渲染类型。
- 为每个
BlockStateModel 最重要的方法是 collectParts,它向待渲染列表追加 BlockStateModelPart。每个部分通过 BlockStateModelPart#getQuads 提供自己的四边形列表,再上传到顶点消费者并渲染。collectParts 有五个参数:
BlockAndTintGetter:表示渲染该状态的世界。BlockPos:渲染位置。BlockState:正在渲染的方块状态;可为 null,表示渲染物品。RandomSource:绑定到客户端的随机源,可用于随机化。List<BlockStateModelPart>:用于接收待渲染部分的列表。
模型数据
有时模型需要依赖 BlockEntity 才能在 collectParts 中确定选择哪些部分。NeoForge 的 ModelData 系统用于从方块实体同步并传递数据。方块实体应实现 getModelData 并返回所需数据,再调用 BlockEntity#requestModelDataUpdate 请求更新。在 collectParts 中,可用方块位置调用 BlockAndTintGetter 的 getModelData 来取得数据。
物品模型
assets/<namespace>/items 中的客户端物品 JSON 会为指定 Item 编译并烘焙出供 ItemStack 使用的 ItemModel:
- 加载阶段:
- 将 JSON 加载为
ClientItem,其中保存物品模型及一些通用渲染属性。 ClientItem加载ItemModel.Unbaked。
- 将 JSON 加载为
- 烘焙阶段:
- 为每个物品调用
ItemModel.Unbaked#bake,将ResolvedModel内联为List<BakedQuad>,并取得通用ModelRenderProperties;如果物品是BlockItem,还会取得渲染类型。
- 为每个物品调用
物品渲染的具体信息见手动渲染物品。
显示视角
Minecraft 渲染引擎支持下表列出的物品显示视角,其中包括货架使用的 ON_SHELF。这些类型用于模型 JSON 的 display 段,在代码中由 ItemDisplayContext 表示。通常从 UnbakedModel 传给物品模型的 ModelRenderProperties,再由 ModelRenderProperties#applyToLayer 应用到 ItemStackRenderState。
| 枚举值 | JSON 键 | 用途 |
|---|---|---|
THIRD_PERSON_RIGHT_HAND |
"thirdperson_righthand" |
第三人称右手,包括 F5 视角和其他玩家。 |
THIRD_PERSON_LEFT_HAND |
"thirdperson_lefthand" |
第三人称左手。 |
FIRST_PERSON_RIGHT_HAND |
"firstperson_righthand" |
第一人称右手。 |
FIRST_PERSON_LEFT_HAND |
"firstperson_lefthand" |
第一人称左手。 |
HEAD |
"head" |
放在玩家头部盔甲槽位,通常只能通过命令实现。 |
GUI |
"gui" |
物品栏、快捷栏。 |
GROUND |
"ground" |
掉落物;旋转由掉落物渲染器处理,而非模型。 |
FIXED |
"fixed" |
物品展示框。 |
ON_SHELF |
"on_shelf" |
货架方块。 |
NONE |
"none" |
代码中的回退,不应在 JSON 中使用。 |
NeoForge 允许扩展 ItemDisplayContext,用于自定义渲染调用。新增类型可指定回退变换,在模型未指定相应变换时使用,其他行为与原版相同。
修改烘焙结果
在代码中修改已有方块状态模型或物品堆叠模型,通常可以用委托包装模型。方块状态已有 DelegateBlockStateModel,物品模型则没有现成实现。包装类只需重写所需方法:
// For block states
public class MyDelegateBlockStateModel extends DelegateBlockStateModel {
// Pass the original model to super.
public MyDelegateBlockStateModel(BlockStateModel originalModel) {
super(originalModel);
}
// Override whatever methods you want here. You may also access originalModel if needed.
}
// For item models
public class MyDelegateItemModel implements ItemModel {
private final ItemModel originalModel;
public MyDelegateItemModel(ItemModel originalModel) {
this.originalModel = originalModel;
}
// Override whatever methods you want here. You may also access originalModel if needed.
@Override
public void update(ItemStackRenderState renderState, ItemStack stack, ItemModelResolver resolver, ItemDisplayContext displayContext, @Nullable ClientLevel level, @Nullable ItemOwner owner, int seed
) {
this.originalModel.update(renderState, stack, resolver, displayContext, level, owner, seed);
}
}
写好包装类后,需要将它应用到目标模型。在客户端 Mod 事件总线上,为 ModelEvent.ModifyBakingResult 注册事件处理器:
@SubscribeEvent // on the mod event bus only on the physical client
public static void modifyBakingResult(ModelEvent.ModifyBakingResult event) {
// For block state models
event.getBakingResult().blockStateModels().computeIfPresent(
// The block state of the model to modify.
MyBlocksClass.EXAMPLE_BLOCK.get().defaultBlockState(),
// A BiFunction with the location and the original models as parameters, returning the new model.
(location, model) -> new MyDelegateBakedModel(model);
);
// For item models
event.getBakingResult().itemStackModels().computeIfPresent(
// The resource location the model to modify.
// Typically the item registry name; however, can be anything due to the ITEM_MODEL data component
MyItemsClass.EXAMPLE_ITEM.getKey().identifier(),
// A BiFunction with the location and the original models as parameters, returning the new model.
(location, model) -> new MyDelegateItemModel(model);
);
}
警告 条件允许时,通常更推荐自定义模型加载器,而不是在
ModelEvent.ModifyBakingResult中包装已烘焙模型。自定义加载器也可以按需使用委托模型。
来源: NeoForged/Documentation(26.1)。中文翻译依据提交
4a680cdd4d15。