模型数据生成(Model Datagen)
和大多数 JSON 数据一样,方块与物品的模型(model)、以及它们所需的方块状态(blockstate)文件和 客户端物品,都可以通过 数据生成(datagen) 自动生成。所有这些流程都由原版的 ModelProvider 处理,NeoForge 通过 ExtendedModelTemplateBuilder 提供了扩展功能。由于方块模型和物品模型的 JSON 结构非常相似,所以数据生成的代码也基本一致。
模型模板(Model Templates)
每一个模型都以 ModelTemplate 作为起点。对于原版来说,ModelTemplate 充当某个预生成模型文件的父级,定义了父模型、所需的纹理槽(texture slot),以及需要添加的文件后缀。在 NeoForge 中,ExtendedModelTemplate 通过 ExtendedModelTemplateBuilder 构建,允许开发者精确地生成模型的基础元素和各个面(face),同时支持 NeoForge 新增的功能。
可以通过 ModelTemplates 提供的方法或直接调用构造函数来创建一个 ModelTemplate。构造函数参数包括:父模型在 models 目录下的可选 ResourceLocation,应用于文件路径末尾的可选字符串后缀(例如按钮被按下时会加上 _pressed),以及一个或多个必须定义的 TextureSlot,否则数据生成过程会崩溃。TextureSlot 本质上是一个字符串,定义了 textures 映射中的纹理“键”。每个键还可以有一个父级 TextureSlot,如果当前槽没有指定纹理,则会向上查找。例如,TextureSlot#PARTICLE 会优先查找定义的 particle 纹理,如果没有,则查找 texture,最后查找 all。如果该槽及其父级都未定义,数据生成时会直接崩溃。
// 假设存在一个名为 '#base' 的纹理引用
// 可以通过指定 'base' 或 'all' 来解析
public static final TextureSlot BASE = TextureSlot.create("base", TextureSlot.ALL);
// 假设存在一个模型 'examplemod:block/example_template'
public static final ModelTemplate EXAMPLE_TEMPLATE = new ModelTemplate(
// 父模型的位置
Optional.of(
ModelLocationUtils.decorateBlockModelLocation("examplemod:example_template")
),
// 应用于所有使用该模板模型文件末尾的后缀
Optional.of("_example"),
// 必须定义的所有纹理槽
// 应尽可能具体,依据父模型中未定义的部分指定
TextureSlot.PARTICLE,
BASE
);
NeoForge 新增的 ExtendedModelTemplate 可以通过 ExtendedModelTemplateBuilder#builder 创建,也可以对已有的原版模板调用 ModelTemplate#extend 进行扩展。通过 #build 方法将 builder 构建为模板。builder 的方法可以让你完全控制模型 JSON 的生成过程:
| 方法(Method) | 效果(Effect) |
|---|---|
#parent(ResourceLocation parent) | 设置父模型的位置,相对于 models 目录。 |
#suffix(String suffix) | 在模型文件路径的末尾追加该字符串。 |
#requiredTextureSlot(TextureSlot slot) | 添加一个必须在 TextureMapping 中定义的纹理槽,用于生成模型。 |
#renderType(ResourceLocation renderType) | 设置渲染类型。该方法有一个重载版本,参数为 String。可用值请参阅 RenderType 类。 |
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) | 添加一个新的 ElementBuilder(相当于向模型添加一个新的元素),通过 consumer 进行配置。 |
#customLoader(Supplier customLoaderFactory, Consumer action) | 使用指定的 factory,让此模型使用自定义加载器,并因此获得一个通过 consumer 配置的自定义加载器构建器。这会改变构建器的类型,因此可能会根据加载器的实现使用不同的方法。NeoForge 默认提供了几个自定义加载器,更多信息(包括数据生成)请参阅相关文档。 |
rootTransforms(Consumer<RootTransformsBuilder> action) | 配置模型在物品显示和方块状态变换前应用的变换,通过 consumer 进行设置。 |
虽然可以通过数据生成(datagen)创建复杂的模型,但更推荐使用像 Blockbench 这样的建模软件来制作复杂模型,然后将导出的模型直接使用,或作为其他模型的父模型。
创建模型实例(Creating the Model Instance)
现在我们已经有了一个 ModelTemplate,可以通过调用 ModelTemplate#create* 方法之一来生成模型本身。虽然每个 create 方法接受的参数略有不同,但它们的核心参数都是:一个表示文件名的 ResourceLocation,一个将 TextureSlot 映射到相对于 textures 目录的某个 ResourceLocation 的 TextureMapping,以及作为模型输出的 BiConsumer<ResourceLocation, ModelInstance>。然后,该方法会实际创建用于生成模型的 JsonObject,如果有重复项会抛出错误。
调用基础的 create 方法不会应用存储的后缀。只有那些接收方块或物品作为参数的 create* 方法才会应用后缀。
// 给定一个 BiConsumer<ResourceLocation, ModelInstance> modelOutput
// 假设有一个 DeferredBlock<Block> EXAMPLE_BLOCK
EXAMPLE_TEMPLATE.create(
// 在 'assets/minecraft/models/block/example_block_example.json' 处创建模型
EXAMPLE_BLOCK.get(),
// 在各个槽位定义贴图
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")),
// 生成的模型 json 的消费者
modelOutput
);
有时,生成的模型会使用类似的模型模板和贴图命名模式(例如,一个普通方块的贴图就是方块的名字)。在这种情况下,可以创建一个 TextureModel.Provider 来帮助消除冗余。这个 provider 实际上是一个函数式接口 ,接收一个 Block 并返回一个 TexturedModel(即 ModelTemplate 和 TextureMapping 的组合),用于生成模型。该接口通过 TexturedModel#createDefault 构造,它接收一个将 Block 映射为其 TextureMapping 的函数,以及要使用的 ModelTemplate。然后可以通过调用 TexturedModel.Provider#create 并传入要生成的 Block,来生成模型。
public static final TexturedModel.Provider EXAMPLE_TEMPLATE_PROVIDER = TexturedModel.createDefault(
// 方块到材质的映射
block -> new TextureMapping()
.put(TextureSlot.PARTICLE, TextureMapping.getBlockTexture(block))
.put(TextureSlot.BASE, TextureMapping.getBlockTexture(block, "_base")),
// 用于生成的模板
EXAMPLE_TEMPLATE
);
// 假设有一个 BiConsumer<ResourceLocation, ModelInstance> modelOutput
// 假设存在一个 DeferredBlock<Block> EXAMPLE_BLOCK
EXAMPLE_TEMPLATE_PROVIDER.create(
// 在 'assets/minecraft/models/block/example_block_example.json' 位置创建模型
EXAMPLE_BLOCK.get(),
// 生成的模型 json 的消费者
modelOutput
)
ModelProvider
无论是方块(block)还是物品(item)的模型数据生成(datagen),都依赖于 registerModels 方法提供的生成器(generator),分别名为 BlockModelGenerators 和 ItemModelGenerators。每个生成器不仅会生成模型 JSON,还会生成任何额外所需的文件(如 blockstate、客户端物品等)。这些生成器内置了多种辅助方法,可以将所有相关文件的构建批量合并为一个易于使用的方法,例如 ItemModelGenerators#generateFlatItem 用于创建一个基础的 item/generated 模型,或 BlockModelGenerators#createTrivialCube 用于生成一个基础的 block/cube_all 模型。
public class ExampleModelProvider extends ModelProvider {
public ExampleModelProvider(PackOutput output) {
// 将 "examplemod" 替换为你自己的 mod id
super(output, "examplemod");
}
@Override
protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerators itemModels) {
// 在这里生成模型及其相关文件
}
}
和所有数据提供器(data provider)一样,别忘了在事件中注册你的 provider:
@SubscribeEvent
public static void gatherData(GatherDataEvent.Client event) {
event.createProvider(ExampleModelProvider::new);
}
方块模型数据生成(Block Model Datagen)
现在,要实际生成 blockstate 和方块模型文件,你可以在 ModelProvider#registerModels 中调用 BlockModelGenerators 里的众多公共方法之一,或者你也可以自己将生成的文件传递给 blockStateOutput(用于 blockstate 文件)、itemModelOutput(用于非基础客户端物品)、以及 modelOutput(用于模型 JSON)。
如果你的方块有一个已注册的 BlockItem,但没有生成对应的客户端物品(client item),ModelProvider 会自动使用默认的方块模型路径 assets/<namespace>/models/block/<path>.json 作为其模型来生成客户端物品。
public class ExampleModelProvider extends ModelProvider {
public ExampleModelProvider(PackOutput output) {
// 将 "examplemod" 替换为你自己的模组 id。
super(output, "examplemod");
}
@Override
protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerators itemModels) {
// 占位符,实际使用时应替换为真实的值。上文已介绍如何使用模型构建器,
// 下文会介绍模型构建器提供的辅助方法。
Block block = MyBlocksClass.EXAMPLE_BLOCK.get();
// 创建一个每一面都使用相同纹理的简单方块模型。
// 纹理文件必须放在 assets/<namespace>/textures/block/<path>.png,
// 其中 <namespace> 和 <path> 分别是方块注册名的命名空间和路径。
// 适用于大多数(完整 )方块,例如木板、圆石或砖块。
blockModels.createTrivialCube(block);
// 接收 `TexturedModel.Provider` 的重载版本。
blockModels.createTrivialBlock(block, EXAMPLE_TEMPLATE_PROVIDER);
// 方块物品会自动生成模型
// 但如果你想为其他物品(如扁平物品)生成模型,可以这样做
blockModels.registerSimpleFlatItemModel(block);
// 添加一个原木方块模型。需要两个纹理文件,分别位于 assets/<namespace>/textures/block/<path>.png 和
// assets/<namespace>/textures/block/<path>_top.png,分别对应侧面和顶部纹理。
// 注意,这里的方块参数必须是 RotatedPillarBlock 类 型,这也是原版原木使用的类。
blockModels.woodProvider(block).log(block);
// 类似于 WoodProvider#logWithHorizontal,用于石英柱子等类似方块。
blockModels.createRotatedPillarWithHorizontalVariant(block, TexturedModel.COLUMN_ALT, TexturedModel.COLUMN_HORIZONTAL_ALT);
// 使用 `ExtendedModelTemplate` 指定渲染类型。
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()
)
);
// 指定一个可以水平旋转的方块模型,包含侧面纹理、正面纹理和顶部纹理。
// 底部同样会使用侧面纹理。如果你不需要正面或顶部纹理,
// 可以多次传入侧面纹理。例如用于熔炉等类似方块。
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"))
)
);
// 指定一个可水平旋转且依附于某个面的方块模型,例如按钮。
// 会考虑将方块放置在地面或天花板上,并相应地旋转模型。
blockModels.familyWithExistingFullBlock(block).button(block);
// 创建一个用于 blockstate 文件的模型
ResourceLocation modelLoc = TexturedModel.CUBE.create(block, blockModels.modelOutput);
// 基础的单一变体模型
blockModels.blockStateOutput.accept(
MultiVariantGenerator.multiVariant(
block,
Variant.variant()
// 设置并生成模型
.with(VariantProperties.MODEL, modelLoc)
// 设置绕 x 轴和 y 轴的旋转
.with(VariantProperties.X_ROT, VariantProperties.Rotation.R90)
.with(VariantProperties.Y_ROT, VariantProperties.Rotation.R180)
// 启用 uvlock
.with(VariantProperties.UV_LOCK, true)
// 设置权重
.with(VariantProperties.WEIGHT, 5)
)
);
// 根据方块状态属性添加一个或多个模型
blockModels.blockStateOutput.accept(
MultiVariantGenerator.multiVariant(block)
.with(
// 使用属性分派,可针对多个属性选择
PropertyDispatch.property(BlockStateProperties.AXIS)
// 选择属性并应用模型
.select(Direction.Axis.Y, Variant.variant().with(VariantProperties.MODEL, modelLoc))
.select(
Direction.Axis.Z,
Variant.variant().with(VariantProperties.MODEL, modelLoc)
.with(VariantProperties.X_ROT, VariantProperties.Rotation.R90)
)
.select(
Direction.Axis.X,
Variant.variant()
.with(VariantProperties.MODEL, modelLoc)
.with(VariantProperties.X_ROT, VariantProperties.Rotation.R90)
.with(VariantProperties.Y_ROT, VariantProperties.Rotation.R90)
)
)
);
// 通过属性分派修改简单模型设置
// 例如:根据方块的水平朝向旋转模型
blockModels.blockStateOutput.accept(
MultiVariantGenerator.multiVariant(
block,
Variant.variant().with(VariantProperties.MODEL, modelLoc)
.with(BlockModelGenerators.createHorizontalFacingDispatch())
)
);
// 生成一个多部件(multipart)模型
blockModels.blockStateOutput.accept(
MultiPartGenerator.multiPart(block)
// 提供基础模型
.with(Variant.variant().with(VariantProperties.MODEL, modelLoc))
// 添加变体出现的条件
.with(
// 添加应用条件
Condition.or(
// 至少有一个条件为 true 时应用
Condition.condition().term(BlockStateProperties.FACING, Direction.NORTH, Direction.SOUTH)
// 可以根据需要嵌套任意多的条件或组合
Condition.and(
Condition.condition().term(BlockStateProperties.FACING, Direction.NORTH)
)
),
// 提供要生成的变体
Variant.variant().with(VariantProperties.MODEL, modelLoc)
)
);
}
}
物品模型数据生成(Item Model Datagen)
生成物品模型(item models)要简单得多,这主要归功于 ItemModelGenerators 和 ItemModelUtils 中用于属性信息的各种辅助方法。与前文类似,你可以在 ModelProvider#registerModels 方法中调用 ItemModelGenerators 提供的众多公共方法之一,或者对于更复杂的客户端物品,也可以自行将生成的文件传递给 itemModelOutput,模型 JSON 则传递给 modelOutput。
public class ExampleModelProvider extends ModelProvider {
public ExampleModelProvider(PackOutput output) {
// 将 "examplemod" 替换为你自己的 mod id。
super(output, "examplemod");
}
@Override
protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerators itemModels) {
// 最常见的物品
// 使用 item/generated,layer0 纹理为物品名
itemModels.generateFlatItem(MyItemsClass.EXAMPLE_ITEM.get(), ModelTemplates.FLAT_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(),
// 针对物品的条件模型
ItemModelUtils.conditional(
// 检查物品是否正在被使用
ItemModelUtils.isUsingItem(),
// 如果为真,根据使用时长选择模型
ItemModelUtils.rangeSelect(
new UseDuration(false),
// 应用于阈值的比例因子
0.05F,
pullingBow0,
// 阈值为 0.65 时
ItemModelUtils.override(pullingBow1, 0.65F),
// 阈值为 0.9 时
ItemModelUtils.override(pullingBow2, 0.9F)
),
// 如果为假,使用基础弓模型
bow
)
);
}
}
环境光遮蔽 Blockbench 编辑器 物品(items) 自定义模型加载 器(Custom Model Loader) 数据生成(Data Generation) 元素(elements)