模型数据生成

模型数据生成

与大多数 JSON 数据一样,方块和物品模型可以通过数据生成创建。两者有一些共通内容,数据生成代码也因此共享部分实现。

模型数据生成类

ModelBuilder

每个模型都从某种 ModelBuilder 开始,通常根据生成目标使用 BlockModelBuilder 或 ItemModelBuilder。它包含模型的父模型、纹理、元素、变换、加载器等所有属性,可通过方法设置:

方法 作用
#texture(String key, ResourceLocation texture) 添加指定键与纹理位置的纹理变量。第二个参数也有字符串重载。
#renderType(ResourceLocation renderType) 设置渲染类型,也有字符串重载。有效值见 RenderType 类。
#ao(boolean ao) 设置是否使用环境光遮蔽。
#guiLight(GuiLight light) 设置 GUI 光照,可为 GuiLight.FRONT 或 GuiLight.SIDE。
#element() 添加并返回新的 ElementBuilder,相当于向模型添加元素,可继续修改。
#transforms() 返回 TransformVecBuilder,用于设置模型的 display。
#customLoader(BiFunction customLoaderFactory) 通过给定工厂使用自定义加载器及其构建器。这会改变构建器类型,具体可用方法由加载器决定。NeoForge 提供若干内置加载器,含数据生成说明,详见链接文章。

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

ModelProvider

方块和物品模型数据生成分别使用 ModelProvider 子类 BlockModelProvider 与 ItemModelProvider。物品模型直接继承后者;方块模型则使用 BlockStateProvider,其内部 BlockModelProvider 可通过 BlockStateProvider#models() 访问。它还包含内部 ItemModelProvider,可通过 BlockStateProvider#itemModels() 访问。最重要的方法是 getBuilder(String path),返回指定位置的 BlockModelBuilder(或 ItemModelBuilder)。

ModelProvider 还有多种辅助方法。其中最重要的可能是 withExistingParent(String name, ResourceLocation parent):通过 getBuilder(name) 返回新构建器,并将指定资源位置设为父模型。另两个常用方法是 mcLoc(String name) 与 modLoc(String name),前者创建 minecraft 命名空间和给定路径的资源位置,后者改用提供器的 Mod ID(通常是自己的 Mod ID)。此外还为台阶、楼梯、栅栏、门等提供 #withExistingParent 的快捷方法。

ModelFile

最后一个重要类是 ModelFile,表示磁盘上的模型 JSON。它是抽象类,有两个内部子类:ExistingModelFile 会通过 ExistingFileHelper 验证文件存在;UncheckedModelFile 则不检查,直接假定文件存在。ModelBuilder 本身也属于 ModelFile。

方块模型数据生成

要生成方块状态和方块模型文件,继承 BlockStateProvider 并重写 registerStatesAndModels()。方块模型始终放在 models/block 子文件夹,但引用相对于 models,所以必须带 block/ 前缀。大多数情况下可选用预定义辅助方法:

public class MyBlockStateProvider extends BlockStateProvider {
    // Parameter values are provided by GatherDataEvent.
    public MyBlockStateProvider(PackOutput output, ExistingFileHelper existingFileHelper) {
        // Replace "examplemod" with your own mod id.
        super(output, "examplemod", existingFileHelper);
    }
    
    @Override
    protected void registerStatesAndModels() {
        // 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.
        ModelFile exampleModel = models().withExistingParent("example_model", this.mcLoc("block/cobblestone"));
        Block block = MyBlocksClass.EXAMPLE_BLOCK.get();
        ResourceLocation exampleTexture = modLoc("block/example_texture");
        ResourceLocation bottomTexture = modLoc("block/example_texture_bottom");
        ResourceLocation topTexture = modLoc("block/example_texture_top");
        ResourceLocation sideTexture = modLoc("block/example_texture_front");
        ResourceLocation frontTexture = modLoc("block/example_texture_front");

        // 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.
        simpleBlock(block);
        // Overload that accepts a model file to use.
        simpleBlock(block, exampleModel);
        // Overload that accepts one or multiple (vararg) ConfiguredModel objects.
        // See below for more info about ConfiguredModel.
        simpleBlock(block, ConfiguredModel.builder().build());
        // Adds an item model file with the block's name, parenting the given model file, for a block item to pick up.
        simpleBlockItem(block, exampleModel);
        // Shorthand for calling #simpleBlock() (model file overload) and #simpleBlockItem.
        simpleBlockWithItem(block, exampleModel);
        
        // 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.
        logBlock(block);
        // Like #logBlock, but the textures are named <path>_side.png and <path>_end.png instead of
        // <path>.png and <path>_top.png, respectively. Used by quartz pillars and similar blocks.
        // Has an overload that allow you to specify a different texture base name, that is then suffixed
        // with _side and _end as needed, an overload that allows you to specify two resource locations
        // for the side and end textures, and an overload that allows specifying side and end model files.
        axisBlock(block);
        // Variants of #logBlock and #axisBlock that additionally allow for render types to be specified.
        // Comes in string and resource location variants for the render type,
        // in all combinations with all variants of #logBlock and #axisBlock.
        logBlockWithRenderType(block, "minecraft:cutout");
        axisBlockWithRenderType(block, mcLoc("cutout_mipped"));
        
        // 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.
        horizontalBlock(block, sideTexture, frontTexture, topTexture);
        // Specifies a horizontally-rotatable block model with a model file that will be rotated as needed.
        // Has an overload that instead of a model file accepts a Function<BlockState, ModelFile>,
        // allowing for different rotations to use different models. Used e.g. by the stonecutter.
        horizontalBlock(block, exampleModel);
        // Specifies a horizontally-rotatable block model that is attached to a face, e.g. for buttons or levers.
        // Accounts for placing the block on the ground and on the ceiling, and rotates them accordingly.
        // Like #horizontalBlock, has an overload that accepts a Function<BlockState, ModelFile> instead.
        horizontalFaceBlock(block, exampleModel);
        // Similar to #horizontalBlock, but for blocks that are rotatable in all directions, including up and down.
        // Again, has an overload that accepts a Function<BlockState, ModelFile> instead.
        directionalBlock(block, exampleModel);
    }
}

BlockStateProvider 还为以下常见方块模型提供辅助方法:楼梯、台阶、按钮、压力板、告示牌、栅栏、栅栏门、墙、玻璃板、门和活板门。

有时方块状态不需要特殊处理,但模型需要。此时可使用 BlockStateProvider#models() 返回的 BlockModelProvider 中的额外方法。它们的首个参数都是名称,大多与完整立方体有关,通常作为 simpleBlock 等方法的模型参数使用。除了配合 BlockStateProvider 的方法外,还包括:

  • withExistingParent:返回带有指定父模型的新构建器。父模型必须已存在,或先于当前模型创建。
  • getExistingFile:在提供器的 ExistingFileHelper 中查找并返回 ModelFile,不存在则抛出 IllegalStateException。
  • singleTexture:接受父模型和一个纹理位置,设置父模型及 texture 纹理变量。
  • sideBottomTop:接受父模型与侧面、底面、顶面三个纹理位置。
  • cube:接受六个面的纹理位置,返回对应纹理的完整立方体。
  • cubeAll:接受一个纹理位置,将它应用到完整立方体六面,可视为 singleTexture 与 cube 的组合。
  • cubeTop:接受两个纹理位置,第一个用于侧面和底面,第二个用于顶面。
  • cubeBottomTop:接受侧面、底面和顶面三个纹理位置,可视为 cube 与 sideBottomTop 的组合。
  • cubeColumn 与 cubeColumnHorizontal:接受侧面与端面两个纹理,返回竖直或水平柱状立方体,供 #logBlock、#axisBlock 及其变体使用。
  • orientable:接受侧面、正面、顶面三个纹理,返回带独立正面纹理的立方体。
  • orientableVertical:省略顶面参数,顶面也使用侧面纹理。
  • orientableWithBottom:在正面与顶面参数之间增加底面纹理参数。
  • crop:使用指定纹理返回作物状模型,如原版四种作物。
  • cross:使用指定纹理返回交叉模型,如花、树苗和许多植物。
  • torch:使用指定纹理返回火把模型。
  • wall_torch:使用指定纹理返回墙上火把模型(墙上火把与地面火把是不同方块)。
  • carpet:使用指定纹理返回地毯模型。

最后,在事件中注册方块状态提供器:

@SubscribeEvent // on the mod event bus
public static void gatherData(GatherDataEvent event) {
    DataGenerator generator = event.getGenerator();
    PackOutput output = generator.getPackOutput();
    ExistingFileHelper existingFileHelper = event.getExistingFileHelper();

    // other providers here
    generator.addProvider(
        event.includeClient(),
        new MyBlockStateProvider(output, existingFileHelper)
    );
}

ConfiguredModel.Builder

如果默认辅助方法无法满足需求,可以直接通过 ConfiguredModel.Builder 构建模型对象,再交给 VariantBlockStateBuilder 创建 variants 状态文件,或交给 MultiPartBlockStateBuilder 创建 multipart 状态文件:

// Create a ConfiguredModel.Builder. Alternatively, you can use one of the ways demonstrated below
// (VariantBlockStateBuilder.PartialBlockstate#modelForState or MultiPartBlockStateBuilder#part) where applicable.
ConfiguredModel.Builder<?> builder = ConfiguredModel.builder()
// Use a model file. As mentioned previously, can either be an ExistingModelFile, an UncheckedModelFile,
// or some sort of ModelBuilder. See above for how to use ModelBuilder.
        .modelFile(models().withExistingParent("example_model", this.mcLoc("block/cobblestone")))
        // Set rotations around the x and y axes.
        .rotationX(90)
        .rotationY(180)
        // Set a uvlock.
        .uvlock(true)
        // Set a weight.
        .weight(5);
// Build the configured model. The return type is an array
// to account for multiple possible models in the same blockstate.
ConfiguredModel[] model = builder.build();

// Get a variant block state builder.
VariantBlockStateBuilder variantBuilder = getVariantBuilder(MyBlocksClass.EXAMPLE_BLOCK.get());
// Create a partial state and set properties on it.
VariantBlockStateBuilder.PartialBlockstate partialState = variantBuilder.partialState();
// Add one or multiple models for a partial blockstate. The models are a vararg parameter.
variantBuilder.addModels(partialState,
    // Specify at least one ConfiguredModel.Builder, as seen above. Create through #modelForState().
    partialState.modelForState()
        .modelFile(models().withExistingParent("example_variant_model", this.mcLoc("block/cobblestone")))
        .uvlock(true)
);
// Alternatively, forAllStates(Function<BlockState, ConfiguredModel[]>) creates a model for every state.
// The passed function will be called once for each possible state.
variantBuilder.forAllStates(state -> {
    // Return a ConfiguredModel depending on the state's properties.
    // For example, the following code will rotate the model depending on the horizontal rotation of the block.
    return ConfiguredModel.builder()
        .modelFile(models().withExistingParent("example_variant_model", this.mcLoc("block/cobblestone")))
        .rotationY((int) state.getValue(BlockStateProperties.HORIZONTAL_FACING).toYRot())
        .build();
});

// Get a multipart block state builder.
MultiPartBlockStateBuilder multipartBuilder = getMultipartBuilder(MyBlocksClass.EXAMPLE_BLOCK.get());
// Add a new part. Starts with .part() and ends with .end().
multipartBuilder.part()
    // Step one: Build the model. multipartBuilder.part() returns a ConfiguredModel.Builder,
    // meaning that all methods seen above can be used here as well.
    .modelFile(models().withExistingParent("example_multipart_model", this.mcLoc("block/cobblestone")))
    // Call .addModel(). Now that the model is built, we can proceed to step two: add the part data.
    .addModel()
    // Add a condition for the part. Requires a property
    // and at least one property value; property values are a vararg.
    .condition(BlockStateProperties.FACING, Direction.NORTH, Direction.SOUTH)
    // Set the multipart conditions to be ORed instead of the default ANDing.
    .useOr()
    // Creates a nested condition group.
    .nestedGroup()
    // Adds a condition to the nested group.
    .condition(BlockStateProperties.FACING, Direction.NORTH)
    // Sets only this condition group to be ORed instead of ANDed.
    .useOr()
    // Creates yet another nested condition group. There is no limit on how many groups can be nested.
    .nestedGroup()
    // Ends the nested condition group, returning to the owning part builder or condition group level.
    // Called twice here since we currently have two nested groups.
    .endNestedGroup()
    .endNestedGroup()
    // End the part builder and add the resulting part to the multipart builder.
    .end();

物品模型数据生成

物品模型生成简单得多,主要因为可以直接操作 ItemModelProvider,而无需 BlockStateProvider 这样的中间类。物品没有对应的方块状态文件,而是直接使用模型。

同样先创建类并继承基础提供器,此处为 ItemModelProvider。因为已经直接位于 ModelProvider 子类中,所有 models() 调用都改为 this,或直接省略。

public class MyItemModelProvider extends ItemModelProvider {
    public MyItemModelProvider(PackOutput output, ExistingFileHelper existingFileHelper) {
        super(output, "examplemod", existingFileHelper);
    }
    
    @Override
    protected void registerModels() {
        // Block items generally use their corresponding block models as parent.
        withExistingParent(MyItemsClass.EXAMPLE_BLOCK_ITEM.getId().toString(), modLoc("block/example_block"));
        // Items generally use a simple parent and one texture. The most common parents are item/generated and item/handheld.
        // In this example, the item texture would be located at assets/examplemod/textures/item/example_item.png.
        // If you want a more complex model, you can use getBuilder() and then work from that, like you would with block models.
        withExistingParent(MyItemsClass.EXAMPLE_ITEM.getId().toString(), mcLoc("item/generated")).texture("layer0", "item/example_item");
        // The above line is so common that there is a shortcut for it. Note that the item registry name and the
        // texture path, relative to textures/item, must match.
        basicItem(MyItemsClass.EXAMPLE_ITEM.get());
    }
}

与其他数据提供器一样,记得在事件中注册:

@SubscribeEvent // on the mod event bus
public static void gatherData(GatherDataEvent event) {
    DataGenerator generator = event.getGenerator();
    PackOutput output = generator.getPackOutput();
    ExistingFileHelper existingFileHelper = event.getExistingFileHelper();

    // other providers here
    generator.addProvider(
        event.includeClient(),
        new MyItemModelProvider(output, existingFileHelper)
    );
}

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