Table of Contents

模型生成

我们知道,几乎每个物品都需要对应的物品烘焙模型(item baked model,以下简称物品模型),从 1.21.4 开始还需要物品模型映射(item models definition)。每个方块都需要方块烘焙模型(block baked model,以下简称方块模型)和方块状态映射(block states definition),还需要对应的物品烘焙模型或者物品模型映射。一个方块就有这么多大大小小复杂的 JSON 要文件。显然,一个个去制作太费神了。因此,我们使用数据生成器为方块和物品生成模型。

在数据生成器中,模型和映射通常是一起生成的,通常来说,生成模型时返回一个模型 id,然后利用这个模型 id 去生成方块状态映射或物品模型映射。我们将从简单到复杂逐个开始。

:!: 注意:我们在之前的教程中,在 resources 文件夹内可能已经手动创建了一些 JSON 文件。当数据生成器生成同名的 JSON 文件时,会与我们已经手动创建的 JSON 冲突,因此在运行数据生成前,请删除它们。
:!: 注意:从 1.21.4 开始,数据生成会区分客户端和服务器,原版中与模型有关的类都会被加上 @Environment(EnvType.CLIENT),仅在客户端环境下可用。如果你在实际运行数据生成时,发现是服务器环境从而报错,请参考数据生成入门教程的手动数据生成部分,在 build.gradle 中修改 fabricApi 块以启用客户端环境,然后重新生成 IDE 的运行配置。

准备

首先,先创建一个类,继承 FabricModelProvider,并在数据生成的入口点注册它:

TutorialModelGenerator.java
public static class TutorialModelGenerator extends FabricModelProvider {
  public TutorialModelGenerator(FabricDataOutput output) {
    super(output);
  }
 
 
  @Override
  public void generateBlockStateModels(BlockStateModelGenerator blockStateModelGenerator) {
    // ...
  }
 
 
  @Override
  public void generateItemModels(ItemModelGenerator itemModelGenerator) {
    // ...
  }
}
ExampleModDataGenerator.java
public class ExampleModDataGenerator implements DataGeneratorEntrypoint {
  @Override
  public void onInitializeDataGenerator(FabricDataGenerator generator) {
    // ...
 
    pack.addProvider(TutorialModelGenerator::new);
  }
}

简单的方块模型

我们在 blocks 的教程中创建过一个示例方块。那么在这里,我们使用短短几行代码,为它创建方块状态映射和方块模型:

TutorialModelGenerator.java
  @Override
  public void generateBlockStateModels(BlockStateModelGenerator blockStateModelGenerator) {
    blockStateModelGenerator.registerSimpleCubeAll(TutorialBlocks.EXAMPLE_BLOCK);
  }

这行代码会创建一个最简单的方块模型,该模型是一个完整的方块,各面都使用与其 id 一致的纹理:tutorial:block/example_block。注意,纹理是图片,一般不会使用数据生成器生成。同时还会创建一个最简单的方块状态映射,直接使用其方块模型。

或者也可以这样(下面的代码仍是在 generateBlockStateModels 方法中的),这种方法就可以指定不同的方块模型类型,第一行返回的是模型 id,而在生成方块状态映射时会使用此模型 id:

    final Identifier exampleBlockModelId = TexturedModel.CUBE_ALL.upload(TutorialBlocks.EXAMPLE_BLOCK, blockStateModelGenerator.modelCollector);
    blockStateModelGenerator.registerParentedItemModel(TutorialBlocks.EXAMPLE_BLOCK, exampleBlockModelId);

如果需要指定不同的纹理,可以手动创建方块模型(例如六面都使用红树原木的顶部纹理):

    final Identifier exampleBlockModelId = Models.CUBE_ALL.upload(TutorialBlocks.EXAMPLE_BLOCK, TextureMap.all(Identifier.ofVanilla("block/mangrove_log_top")), blockStateModelGenerator.modelCollector);
    blockStateModelGenerator.registerParentedItemModel(TutorialBlocks.EXAMPLE_BLOCK, exampleBlockModelId);

简单的物品模型(1.21.4 之后)

生成物品模型也很简单:

TutorialModelGenerator.java
  @Override
  public void generateItemModels(ItemModelGenerator itemModelGenerator) {
    itemModelGenerator.register(TutorialItems.CUSTOM_ITEM, Models.GENERATED);
  }

这个物品模型将使用最基本的物品模型映射,物品模型为 item/generated,并使用与其 id 一致的纹理(tutorial:item/custom_item)。当然,你也可以根据需要,将 Models.GENERATED 修改成其他的值。

如果 itemModelGenerator.register 没有指定第二个参数,那么就只会生成物品模型映射,不会生成物品模型。

如果想要指定不同的纹理(例如在这个例子中,直接使用原版的白色羊毛纹理),那么也可以手动创建模型和模型映射(下面的代码仍是在 generateItemModels 方法中的)。

TutorialModelGenerator.java
    itemModelGenerator.register(TutorialItems.CUSTOM_ITEM, Models.GENERATED);
    final Identifier modelId = Models.GENERATED.upload(TutorialItems.CUSTOM_ITEM, TextureMap.layer0(Identifier.of("block/white_wool")), itemModelGenerator.modelCollector);
    itemModelGenerator.output.accept(TutorialItems.CUSTOM_ITEM, ItemModels.basic(modelId));

常见的原版方块模型(以楼梯和台阶为例)

有时方块模型并不简单,因为涉及多种复杂的方块状态。例如,楼梯就有多种朝向,可以是东南西北,可以是正放或倒放,而且还可以是角落的。我们自己去为这些方块创建模型和方块状态映射显然会很费神。好在这些常用的方块,原版已经给我们写好了,我们只需要使用就可以。这里以楼梯和台阶为例,我们为钻石块创建相应的楼梯和台阶。首先,我们先快速地创建这些方块:

对于 1.21.2 及之后的版本:

TutorialBlocks.json
  public static final Block DIAMOND_STAIRS = register("diamond_stairs",
      settings -> new StairsBlock(Blocks.DIAMOND_BLOCK.getDefaultState(), settings),
      AbstractBlock.Settings.copy(Blocks.DIAMOND_BLOCK));
  public static final Block DIAMOND_SLAB = register("diamond_slab",
      SlabBlock::new,
      AbstractBlock.Settings.copy(Blocks.DIORITE_SLAB));

对于 1.21.2 之前的版本:

TutorialBlocks.json
  public static final Block DIAMOND_STAIRS = register("diamond_stairs",
      new StairsBlock(Blocks.DIAMOND_BLOCK.getDefaultState(),
      AbstractBlock.Settings.copy(Blocks.DIAMOND_BLOCK)));
  public static final Block DIAMOND_SLAB = register("diamond_slab",
      new SlabBlock(AbstractBlock.Settings.copy(Blocks.DIORITE_SLAB)));

然后,我们先分别创建常规楼梯模型、内角落楼梯模型、外角落楼梯模型、底台阶模型、顶台阶模型。双台阶模型我们直接使用原版的钻石块模型。创建这些模型时,会返回模型的 id,然后使用 BlockStateModelGenerator 中的各个实用方法去创建方块状态映射,会使用到之前返回的模型 id。我们还需要使用常规楼梯模型和底台阶模型作为物品模型。

TutorialModelGenerator.json
    final TextureMap diamondTexture = TextureMap.all(Identifier.ofVanilla("block/diamond_block"));
 
    final Identifier stairsModelId = Models.STAIRS.upload(TutorialBlocks.DIAMOND_STAIRS, diamondTexture, blockStateModelGenerator.modelCollector);
    final Identifier innerStairsModelId = Models.INNER_STAIRS.upload(TutorialBlocks.DIAMOND_STAIRS, diamondTexture, blockStateModelGenerator.modelCollector);
    final Identifier outerStairsModelId = Models.OUTER_STAIRS.upload(TutorialBlocks.DIAMOND_STAIRS, diamondTexture, blockStateModelGenerator.modelCollector);
    blockStateModelGenerator.blockStateCollector.accept(
        BlockStateModelGenerator.createStairsBlockState(TutorialBlocks.DIAMOND_STAIRS,
            BlockStateModelGenerator.createWeightedVariant(innerStairsModelId),
            BlockStateModelGenerator.createWeightedVariant(stairsModelId),
            BlockStateModelGenerator.createWeightedVariant(outerStairsModelId)));
    blockStateModelGenerator.registerParentedItemModel(TutorialBlocks.DIAMOND_STAIRS, stairsModelId);
 
    final Identifier slabBottomModelId = Models.SLAB.upload(TutorialBlocks.DIAMOND_SLAB, diamondTexture, blockStateModelGenerator.modelCollector);
    final Identifier slabTopModelId = Models.SLAB_TOP.upload(TutorialBlocks.DIAMOND_SLAB, diamondTexture, blockStateModelGenerator.modelCollector);
    blockStateModelGenerator.blockStateCollector.accept(
        BlockStateModelGenerator.createSlabBlockState(TutorialBlocks.DIAMOND_SLAB,
            BlockStateModelGenerator.createWeightedVariant(slabBottomModelId),
            BlockStateModelGenerator.createWeightedVariant(slabTopModelId),
            BlockStateModelGenerator.createWeightedVariant(Identifier.ofVanilla("block/diamond_block")))
    );
    blockStateModelGenerator.registerParentedItemModel(TutorialBlocks.DIAMOND_SLAB, slabBottomModelId);
在 1.21.4 及之前的版本中,不需要调用 BlockStateModelGenerator.createWeightedVariant 方法中,直接将 id 传作参数即可。

大功告成!我们就仅仅通过这么几行代码,成功地添加了楼梯和台阶所需要的一切模型、方块状态映射和物品模型映射!

带有朝向的方块(以竖直台阶为例)

带有朝向的方块通常仅使用一个方块模型,但是在方块模型映射中,会映射到不同的模型变种,例如不同的 x 旋转、y 旋转以及 uvlock。(这些词是不是在之前手动写方块状态映射的 JSON 时有些熟悉?)

我们以 directionalblock 中创建的竖直台阶为例,通过数据生成器为其生成模型和方块状态映射。

自定义模型

我们之前创建过一个 tutorial:block/vertical_slab 的模型,作为竖直方块模型的模板。这种模板模型通常不需要使用数据生成器生成。然后我们为竖直的磨制安山岩台阶方块创建了名为 tutorial:block/polished_andesite_vertical_slab,继承前面提到的模板模型,并提供纹理变量。

为了在数据生成器中继承此模板模型,我们需要为此模板模型创建 Model 对象。该模型的 JSON 中会使用 bottomtopside 这三个纹理变量,因此我们在这里的代码中也依赖这三个纹理变量,数据生成器将在生成模型时为这几个变量赋值。

TutorialModelGenerator.java
public class TutorialModelGenerator extends FabricModelProvider {
  public static final Model VERTICAL_SLAB = new Model(
      Optional.of(Identifier.of("tutorial", "block/vertical_slab")),
      Optional.empty(),
      TextureKey.BOTTOM, TextureKey.TOP, TextureKey.SIDE);
 
  // ...
}

然后再调用 upload 方法即可。该方法将生成方块模型,并返回该模型的 id。在这里,我们一并把物品模型映射做好,让物品也直接使用此方块模型。

TutorialModelGenerator.java
  @Override
  public void generateBlockStateModels(BlockStateModelGenerator blockStateModelGenerator) {
    // ...
 
    final Identifier verticalSlabModelId = VERTICAL_SLAB.upload(TutorialBlocks.POLISHED_ANDESITE_VERTICAL_SLAB, TextureMap.all(Identifier.ofVanilla("block/polished_andesite")), blockStateModelGenerator.modelCollector);
    blockStateModelGenerator.registerParentedItemModel(TutorialBlocks.POLISHED_ANDESITE_VERTICAL_SLAB, verticalSlabModelId);
  }

自定义方块状态映射(1.21.5 之后)

重头戏来了——现在我们要为竖直台阶方块创建方块状态映射。其实没那么复杂,因为我们知道模型都是一样的,只需要有不同的旋转而已。

在 1.21.5 中,方块状态的对象是 BlockModelDefinitionCreator,分为两种:

对于 VariantsBlockModelDefinitionCreator,我们需要指定方块状态与方块模型变种的关系,也就是要说清楚:什么方块状态使用什么模型变种。多个方块状态可以使用同一模型变种(例如含水和不含水的方块,模型都是一样的),同一方块状态也可以使用多个变种(例如泥土、沙子等方块的随机旋转)——但必须不重不漏:不一定每个方块状态属性都要考虑到,但每个可能的方块状态都要被包括,且不能重复,否则就会有错误。在 1.21.5 中,有两种方法指定方块状态与模型变种的关系:

我们的竖直台阶有两个方块状态属性:facingwaterloggedwaterlogged 不影响其模型,所以我们只看 facing。因为模型 id 不受朝向影响,所以这里我们使用方法一。

TutorialModelGenerator.java
  @Override
  public void generateBlockStateModels(BlockStateModelGenerator blockStateModelGenerator) {
    // ...
 
    blockStateModelGenerator.blockStateCollector.accept(
        VariantsBlockModelDefinitionCreator.of(TutorialBlocks.POLISHED_ANDESITE_VERTICAL_SLAB,
                BlockStateModelGenerator.createWeightedVariant(verticalSlabModelId))
            .apply(BlockStateModelGenerator.UV_LOCK)
            .coordinate(BlockStateVariantMap.operations(VerticalSlabBlock.FACING)
                .register(Direction.NORTH, BlockStateModelGenerator.NO_OP)
                .register(Direction.EAST, BlockStateModelGenerator.ROTATE_Y_90)
                .register(Direction.SOUTH, BlockStateModelGenerator.ROTATE_Y_180)
                .register(Direction.WEST, BlockStateModelGenerator.ROTATE_Y_270)));
  }

这里的 apply 方法会将所有方块状态都应用同一操作(例如全部添加 uvlock),而 coordinate 方法则会根据不同的方块状态应用不同的操作(例如设置不同的 y 旋转值),BlockStateVariantMap.operations 只管变种,不管模型 id。

可以使用方法二吗?当然可以,代码如下:
TutorialModelGenerator.java
    blockStateModelGenerator.blockStateCollector.accept(
        VariantsBlockModelDefinitionCreator.of(
            TutorialBlocks.POLISHED_ANDESITE_VERTICAL_SLAB)
            .with(BlockStateVariantMap.models(VerticalSlabBlock.FACING)
                .register(Direction.NORTH, BlockStateModelGenerator.createWeightedVariant(verticalSlabModelId))
                .register(Direction.EAST, BlockStateModelGenerator.createWeightedVariant(verticalSlabModelId).apply(BlockStateModelGenerator.ROTATE_Y_90))
                .register(Direction.SOUTH, BlockStateModelGenerator.createWeightedVariant(verticalSlabModelId).apply(BlockStateModelGenerator.ROTATE_Y_180))
                .register(Direction.WEST, BlockStateModelGenerator.createWeightedVariant(verticalSlabModelId).apply(BlockStateModelGenerator.ROTATE_X_270))
            )
            .apply(BlockStateModelGenerator.UV_LOCK)
    );


我们发现,在方法二中,调用 VariantsBlockModelDefinitionCreator.of 时没有将 WeightedVariant 作为第二个参数,而是在后面每个方块状态提供一次,我们也注意到方法一中调用的是 BlockStateVariantMap.operations,每次注册的也仅是对模型变种的操作,而在方法二中,调用的是 BlockStateVariantMap.models,每次注册的都是个完整的模型变种。通过调用 with 分配模型变种之后,就可以继续像方法一那样使用 applycoordinate 方法修改变种。

在上面的方法一和方法二中,除了通过 register 方法逐个注册模型操作或模型变种之外,也可以使用 generate 方法,通过 lambda 的方式指定模型操作或者模型变种,这个 lambda 是一个函数,接收对应的属性值作为参数,返回的是模型操作或模型变种。

如果方块状态有多个属性,多个属性都会影响到模型变种,那么可以直接在 BlockStateVariantMap.operationsBlockStateVariantMap.models 中提供多个属性,然后在后面调用 registergenerate 时同时指定或使用多个值,也可以调用两次 coordinate,让每个属性各自决定对方块状态的修改。