模型数据生成

模型数据生成

与多数 JSON 数据一样,方块模型、物品模型,以及必需的方块状态文件和客户端物品,都可以通过数据生成创建。相关工作由原版 ModelProvider 处理,NeoForge 则通过 ExtendedModelTemplateBuilder 提供扩展。由于方块和物品模型的 JSON 相似,它们的数据生成代码也比较接近。

模型模板

每个模型都从 ModelTemplate 开始。原版模板以预先生成的模型文件为基础,定义父模型、必需的纹理槽和文件名后缀。NeoForge 的 ExtendedModelTemplate 通过 ExtendedModelTemplateBuilder 构建,可以一直生成到底层元素和面,并使用 NeoForge 新增功能。

可以调用 ModelTemplates 中的方法或构造函数创建模板。构造函数接受相对于 models 目录的可选父模型 Identifier、追加到路径末尾的可选字符串(例如按下的按钮使用 _pressed),以及生成时必须定义的可变数量 TextureSlot。纹理槽本质上是 textures 映射中的键,也可以指定父槽,在自身未指定纹理时回退。例如 TextureSlot#PARTICLE 先查 particle,再查 texture,最后查 all。如果槽及其父槽都没有定义,数据生成会崩溃。

// Assumes there is a texture referenced as '#base'
// Can be resolved by either specifying 'base' or 'all'
public static final TextureSlot BASE = TextureSlot.create("base", TextureSlot.ALL);

// Assume there exists some model 'examplemod:block/example_template'
public static final ModelTemplate EXAMPLE_TEMPLATE = new ModelTemplate(
    // The parent model location
    Optional.of(
        ModelLocationUtils.decorateBlockModelLocation("examplemod:example_template")
    ),
    // The suffix to apply to the end of any model that uses this template
    Optional.of("_example"),
    // All texture slots that must be defined
    // Should be as specific as possible based on what's undefined in the parent model
    TextureSlot.PARTICLE,
    BASE
);

NeoForge 的扩展模板可以用 ExtendedModelTemplateBuilder#builder 创建,也可以对已有原版模板调用 ModelTemplate#extend,最后调用 #build 生成模板。构建器提供完整的模型 JSON 控制:

方法 效果
#parent(Identifier parent) 设置相对于 models 目录的父模型位置。
#suffix(String suffix) 将字符串追加到模型文件路径末尾。
#requiredTextureSlot(TextureSlot slot) 添加生成时必须在 TextureMapping 定义的纹理槽。
transform(ItemDisplayContext type, Consumer<TransformVecBuilder> action) 通过 consumer 配置 TransformVecBuilder,用于设置模型 display。
#ambientOcclusion(boolean ambientOcclusion) 设置是否使用环境光遮蔽。
#guiLight(UnbakedModel.GuiLight light) 设置 GUI 光照,可为 GuiLight.FRONT 或 GuiLight.SIDE。
#element(Consumer<ElementBuilder> action) 通过 consumer 配置并添加新 ElementBuilder,相当于添加模型元素。
#customLoader(Supplier customLoaderFactory, Consumer action) 用工厂指定自定义加载器及其构建器,再由 consumer 配置。此操作会改变构建器类型,因此可用方法取决于加载器实现。NeoForge 内置若干加载器,详见链接文档,包含数据生成说明。
#rootTransforms(Consumer<RootTransformsBuilder> action) 配置在物品显示变换和方块状态变换之前应用的根变换。

建议 数据生成虽能创建复杂模型,但更建议使用 Blockbench 等建模软件制作复杂模型,再直接使用导出的文件,或将其作为其他模型的父模型。

创建模型实例

有了模板后,调用某个 ModelTemplate#create* 方法即可生成模型。各方法参数不同,但核心都包括:表示文件名的 Identifier、将 TextureSlot 映射到相对于 textures 目录的纹理标识符的 TextureMapping,以及作为模型输出的 BiConsumer<Identifier, ModelInstance>。方法创建用于生成模型的 JsonObject,若有重复则报错。

提示 基础 create 方法不会应用已保存的后缀。只有接受方块或物品的 create* 方法会应用后缀。

// Given some BiConsumer<Identifier, ModelInstance> modelOutput
// Assume there is a DeferredBlock<Block> EXAMPLE_BLOCK
EXAMPLE_TEMPLATE.create(
    // Creates the model at 'assets/minecraft/models/block/example_block_example.json'
    EXAMPLE_BLOCK.get(),
    // Define textures in slots
    new TextureMapping()
        // "particle": "examplemod:item/example_block"
        .put(TextureSlot.PARTICLE, TextureMapping.getBlockTexture(EXAMPLE_BLOCK.get()))
        // "base": "examplemod:item/example_block_base"
        .put(TextureSlot.BASE, TextureMapping.getBlockTexture(EXAMPLE_BLOCK.get(), "_base")),
    // The consumer of the generated model json
    modelOutput
);

一些生成模型使用相似模板及纹理命名规则,例如普通方块纹理直接使用方块名。这时可以创建 TexturedModel.Provider 减少重复。它是接收 Block、返回 TexturedModel(模板与纹理映射的组合)的函数式接口。通过 TexturedModel#createDefault 创建,传入从方块到纹理映射的函数以及模板,再用 TexturedModel.Provider#create 传入目标方块生成模型。

public static final TexturedModel.Provider EXAMPLE_TEMPLATE_PROVIDER = TexturedModel.createDefault(
    // Block to texture mapping
    block -> new TextureMapping()
        .put(TextureSlot.PARTICLE, TextureMapping.getBlockTexture(block))
        .put(TextureSlot.BASE, TextureMapping.getBlockTexture(block, "_base")),
    // The template to generate from
    EXAMPLE_TEMPLATE
);

// Given some BiConsumer<Identifier, ModelInstance> modelOutput
// Assume there is a DeferredBlock<Block> EXAMPLE_BLOCK
EXAMPLE_TEMPLATE_PROVIDER.create(
    // Creates the model at 'assets/minecraft/models/block/example_block_example.json'
    EXAMPLE_BLOCK.get(),
    // The consumer of the generated model json
    modelOutput
);

ModelProvider

方块和物品的数据生成分别使用 registerModels 提供的 BlockModelGenerators 和 ItemModelGenerators。它们同时生成模型 JSON 及方块状态、客户端物品等所需文件,并提供便捷方法,将一整组文件的构建合为一次调用。例如,ItemModelGenerators#generateFlatItem 配合 ModelTemplates#FLAT_ITEM 可以创建基础 item/generated 模型;BlockModelGenerators#createTrivialCube 可以创建基础 block/cube_all 模型。

public class ExampleModelProvider extends ModelProvider {

    public ExampleModelProvider(PackOutput output) {
        // Replace "examplemod" with your own mod id.
        super(output, "examplemod");
    }

    @Override
    protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerators itemModels) {
        // Generate models and associated files here
    }
}

与其他数据提供器一样,别忘了通过事件注册:

@SubscribeEvent // on the mod event bus
public static void gatherData(GatherDataEvent.Client event) {
    event.createProvider(ExampleModelProvider::new);
}

方块模型数据生成

实际生成方块状态和模型时,可以在 ModelProvider#registerModels 中调用 BlockModelGenerators 的各种公共方法,也可以自行把生成内容传给对应输出:方块状态使用 blockStateOutput,非简单客户端物品使用 itemModelOutput,模型 JSON 使用 modelOutput。

提示 若方块注册了关联的 BlockItem,但没有生成客户端物品,ModelProvider 会自动创建,使用默认方块模型 assets/<namespace>/models/block/<path>.json。

public class ExampleModelProvider extends ModelProvider {

    public ExampleModelProvider(PackOutput output) {
        // Replace "examplemod" with your own mod id.
        super(output, "examplemod");
    }

    @Override
    protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerators itemModels) {
        // Placeholders, their usages should be replaced with real values. See above for how to use the model builder,
        // and below for the helpers the model builder offers.
        Block block = MyBlocksClass.EXAMPLE_BLOCK.get();

        // Create a simple block model with the same texture on each side.
        // The texture must be located at assets/<namespace>/textures/block/<path>.png, where
        // <namespace> and <path> are the block's registry name's namespace and path, respectively.
        // Used by the majority of (full) blocks, such as planks, cobblestone or bricks.
        blockModels.createTrivialCube(block);

        // Overload that accepts a `TexturedModel.Provider` to use.
        blockModels.createTrivialBlock(block, EXAMPLE_TEMPLATE_PROVIDER);

        // Block items have a model generated automatically
        // But let's assume you want to generate a different item, such as a flat item
        blockModels.registerSimpleFlatItemModel(block);

        // Adds a log block model. Requires two textures at assets/<namespace>/textures/block/<path>.png and
        // assets/<namespace>/textures/block/<path>_top.png, referencing the side and top texture, respectively.
        // Note that the block input here is limited to RotatedPillarBlock, which is the class vanilla logs use.
        blockModels.woodProvider(block).log(block);
        
        // Like WoodProvider#logWithHorizontal. Used by quartz pillars and similar blocks.
        blockModels.createRotatedPillarWithHorizontalVariant(block, TexturedModel.COLUMN_ALT, TexturedModel.COLUMN_HORIZONTAL_ALT);

        // Using the `ExtendedModelTemplate` to specify the render type to use.
        blockModels.createRotatedPillarWithHorizontalVariant(block,
            TexturedModel.COLUMN_ALT.updateTemplate(template ->
                template.extend().renderType("minecraft:cutout").build()
            ),
            TexturedModel.COLUMN_HORIZONTAL_ALT.updateTemplate(template ->
                template.extend().renderType(this.mcLocation("cutout_mipped")).build()
            )
        );

        // Specifies a horizontally-rotatable block model with a side texture, a front texture, and a top texture.
        // The bottom will use the side texture as well. If you don't need the front or top texture,
        // just pass in the side texture twice. Used by e.g. furnaces and similar blocks.
        blockModels.createHorizontallyRotatedBlock(
            block,
            TexturedModel.Provider.ORIENTABLE_ONLY_TOP.updateTexture(mapping ->
                mapping.put(TextureSlot.SIDE, this.modLocation("block/example_texture_side"))
                .put(TextureSlot.FRONT, this.modLocation("block/example_texture_front"))
                .put(TextureSlot.TOP, this.modLocation("block/example_texture_top"))
            )
        );

        // Specifies a horizontally-rotatable block model that is attached to a face, e.g. for buttons.
        // Accounts for placing the block on the ground and on the ceiling, and rotates them accordingly.
        blockModels.familyWithExistingFullBlock(block).button(block);

        // Create a model to use for blockstatefiles
        Identifier modelLoc = TexturedModel.CUBE.create(block, blockModels.modelOutput);

        // Create a common variant to transform
        Variant variant = new Variant(modelLoc);

        // Basic single variant model
        blockModels.blockStateOutput.accept(
            MultiVariantGenerator.dispatch(
                block,
                new MultiVariant(
                    WeightedList.of(
                        new Weighted<>(
                            // Set model
                            variant
                                // Set rotations around the x and y axes
                                .with(VariantMutator.X_ROT.withValue(Quadrant.R90))
                                .with(VariantMutator.Y_ROT.withValue(Quadrant.R180))
                                // Set a uvlock
                                .with(VariantMutator.UV_LOCK.withValue(true)),
                            // Set a weight
                            5
                        )
                    )
                )
            )
        );

        // Add one or multiple models based on the block state properties
        blockModels.blockStateOutput.accept(
            MultiVariantGenerator.dispatch(
                block,
                // Create the basic multi-variant
                BlockModelGenerators.variant(variant)
            ).with(
                // Apply a property dispatch
                // Will mutate the variant based on the provided mutators
                PropertyDispatch.modify(BlockStateProperties.AXIS)
                    .select(Direction.Axis.Y, BlockModelGenerators.NOP)
                    .select(Direction.Axis.Z, BlockModelGenerators.X_ROT_90)
                    .select(Direction.Axis.X, BlockModelGenerators.X_ROT_90.then(BlockModelGenerators.Y_ROT_90))
            )
        );

        // Generate a multipart
        blockModels.blockStateOutput.accept(
            MultiPartGenerator.multiPart(block)
                // Provide the base model
                .with(BlockModelGenerators.variant(variant))
                // Add conditions for variant to appear
                .with(
                    // Add conditions to apply
                    new CombinedCondition(
                        CombinedCondition.Operation.OR,
                        List.of(
                            // Where at least one of the conditions are true
                            BlockModelGenerators.condition().term(BlockStateProperties.FACING, Direction.NORTH, Direction.SOUTH)
                            // Can nest as many conditions or groups as necessary
                            new CombinedCondition(
                                CombinedCondition.Operation.AND,
                                List.of(
                                    BlockModelGenerators.condition().term(BlockStateProperties.FACING, Direction.NORTH)
                                )
                            )
                        )
                    ),
                    // Supply variant to mutate
                    BlockModelGenerators.variant(variant)
                )
        );
    }
}

物品模型数据生成

由于 ItemModelGenerators 和处理属性信息的 ItemModelUtils 提供了许多便捷方法,物品模型生成简单得多。与上文类似,可以在 ModelProvider#registerModels 中调用生成器的公共方法,也可以自行把非简单客户端物品输出给 itemModelOutput,把模型 JSON 输出给 modelOutput。

public class ExampleModelProvider extends ModelProvider {

    public ExampleModelProvider(PackOutput output) {
        // Replace "examplemod" with your own mod id.
        super(output, "examplemod");
    }

    @Override
    protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerators itemModels) {
        // The most common item
        // item/generated with the layer0 texture as the item name
        itemModels.generateFlatItem(MyItemsClass.EXAMPLE_ITEM.get(), ModelTemplates.FLAT_ITEM);

        // A bow-like item
        ItemModel.Unbaked bow = ItemModelUtils.plainModel(ModelLocationUtils.getModelLocation(MyItemsClass.EXAMPLE_ITEM.get()));
        ItemModel.Unbaked pullingBow0 = ItemModelUtils.plainModel(this.createFlatItemModel(MyItemsClass.EXAMPLE_ITEM.get(), "_pulling_0", ModelTemplates.BOW));
        ItemModel.Unbaked pullingBow1 = ItemModelUtils.plainModel(this.createFlatItemModel(MyItemsClass.EXAMPLE_ITEM.get(), "_pulling_1", ModelTemplates.BOW));
        ItemModel.Unbaked pullingBow2 = ItemModelUtils.plainModel(this.createFlatItemModel(MyItemsClass.EXAMPLE_ITEM.get(), "_pulling_2", ModelTemplates.BOW));
        this.itemModelOutput.accept(
            MyItemsClass.EXAMPLE_ITEM.get(),
            // Conditional model for item
            ItemModelUtils.conditional(
                // Checks if item is being used
                ItemModelUtils.isUsingItem(),
                // When true, select model based on use duration
                ItemModelUtils.rangeSelect(
                    new UseDuration(false),
                    // Scalar to apply to the thresholds
                    0.05F,
                    pullingBow0,
                    // Threshold when 0.65
                    ItemModelUtils.override(pullingBow1, 0.65F),
                    // Threshold when 0.9
                    ItemModelUtils.override(pullingBow2, 0.9F)
                ),
                // When false, use the base bow model
                bow
            ),
            // Some settings to use during the rendering process
            new ClientItem.Properties(
                // When false, disables the animation where the item is raised
                // up towards its normal position on item swap
                false,
                // When true, allows the model to render outside its defined
                // slot bounds (defined in GuiItemRenderState#bounds) in a GUI
                // instead of being scissored
                false,
                // Applies the scalar to the height of the hand when swapping
                1.0F
            )
        );
    }
}

来源: NeoForged/Documentation(26.1)。中文翻译依据提交 4a680cdd4d15。