理解模型系统

理解模型系统

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。
  • 烘焙阶段:
    • 为每个 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。
  • 烘焙阶段:
    • 为每个物品调用 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。