自定义模型加载器(Custom Model Loaders)
模型(model)本质上就是一个形状。它可以是一个立方体、一组立方体、一组三角形,或者任何其他几何形状(或几何形状的组合)。在大多数情况下,模型是如何定义的并不重要,因为最终所 有内容都会以 BakedModel 的形式加载到内存中。因此,NeoForge 提供了注册自定义模型加载器(custom model loaders)的能力,你可以将任意模型转换为游戏可用的 BakedModel。
方块模型的入口依然是模型 JSON 文件。不过,你可以在 JSON 的根部指定一个 loader 字段,用于替换默认加载器为你自己的加载器。自定义模型加载器可以忽略默认加载器所需的所有字段。
内置模型加载器(Builtin Model Loaders)
除了默认的模型加载器之外,NeoForge 还提供了几种内置加载器,每种都适用于不同的场景。
复合模型(Composite Model)
复合模型可以用来在父模型中指定不同的模型部件(model part),并且只在子模型中应用其中的一部分。下面通过一个示例来说明。在 examplemod:example_composite_model 这个父模型中:
{
"loader": "neoforge:composite",
// 指定模型部件。
"children": {
"part_1": {
"parent": "examplemod:some_model_1"
},
"part_2": {
"parent": "examplemod:some_model_2"
}
},
"visibility": {
// 默认禁用 part_2。
"part_2": false
}
}
接下来,我们可以在 examplemod:example_composite_model 的子模型中分别启用或禁用各个部件:
{
"parent": "examplemod:example_composite_model",
// 覆盖可见性。如果某个部件缺失,则会使用父模型的可见性设置。
"visibility": {
"part_1": false,
"part_2": true
}
}
要为这个模型进行 数据生成,请使用自定义加载器类 CompositeModelBuilder。
复合模型加载器不应用于 客户端物品 所使用的模型。对于这些 情况,应在定义中使用专门的 composite model。
空模型(Empty Model)
空模型不会渲染任何内容。
{
"loader": "neoforge:empty"
}
OBJ 模型(OBJ Model)
OBJ 模型加载器允许你在游戏中使用 Wavefront .obj 三维模型,从而可以在模型中包含任意形状(包括三角形、圆等)。.obj 模型文件必须放在 models 文件夹(或其子文件夹)下,并且需要有一个同名的 .mtl 文件(也可以手动指定)。例如,放在 models/block/example.obj 的 OBJ 模型,必须有一个对应的 MTL 文件 models/block/example.mtl。
{
"loader": "neoforge:obj",
// 必须字段。模型文 件的引用。注意,这里的路径是相对于命名空间根目录,而不是 model 文件夹。
"model": "examplemod:models/example.obj",
// 通常,.mtl 文件需要和 .obj 文件放在同一位置,只有文件扩展名不同。
// 这样加载器会自动识别并加载对应的 .mtl 文件。当然,你也可以手动指定 .mtl 文件的位置。
"mtl_override": "examplemod:models/example_other_name.mtl",
// 这些纹理可以在 .mtl 文件中通过 #texture0、#particle 等引 用。
// 通常需要手动编辑 .mtl 文件来完成引用。
"textures": {
"texture0": "minecraft:block/cobblestone",
"particle": "minecraft:block/stone"
},
// 启用或禁用模型的自动剔除。可选,默认值为 true。
"automatic_culling": false,
// 是否对模型进行明暗处理。可选,默认值为 true。
"shade_quads": false,
// 一些建模程序会将 V=0 视为底部而不是顶部。此属性会将 V 坐标上下翻转。
// 可选,默认值为 false。
"flip_v": true,
// 是否启用自发光。可选,默认值为 true。
"emissive_ambient": false
}
要为该模型进行 数据生成,请使用自定义加载器类 ObjModelBuilder。
创建自定义模型加载器(Creating Custom Model Loaders)
要创建你自己的模型加载器,需要准备三个类以及一个事件处理器:
- 一个
UnbakedModelLoader类 - 一个
UnbakedModel类 - 一个 已烘焙模型 类,通常是
SimpleBakedModel实例,或者如果需要ModelData,则使用IDynamicBakedModel - 一个 客户端 事件处理器,用于
ModelEvent.RegisterLoaders,负责注册未烘焙模型加载器 - 可选:一个 客户端 事件处理器,用于
RegisterClientReloadListenersEvent,适用于那些需要缓存加载数据的模型加载器
为了说明这些类之间的关系,我们跟踪一个模型的加载过程:
- 在模型加载阶段,带有
loader属性的模型 JSON 会被传递给你的未烘焙模型加载器。加载器读取模型 JSON,并根据 JSON 属性返回一个未烘焙对象。 - 在模型烘焙阶段,该对象会被烘焙,返回一个已烘焙模型。
- 在模型渲染阶段,已烘焙模型会被用于渲染。
如果你要为物品(item)创建自定义模型加载器,根据实际需求,也许直接创建一个新的 ItemModel 会更合适。例如,如果你的模型需要使用或生成 BakedModel,那么用 ItemModel 更合理;而如果你的模型需要渲染不同的数据格式(比如 .obj),则应该创建新的模型加载器。
让我们通过一个基础的类结构来进一步说明这一点。加载器类命名为 MyUnbakedModelLoader,未烘焙模型类命名为 MyUnbakedModel,我们将构造一个 SimpleBakedModel 实例。同时,我们假设模型加载器需要使用某些缓存:
public class MyUnbakedModelLoader implements UnbakedModelLoader<MyUnbakedModel>, ResourceManagerReloadListener {
// 强烈推荐对未烘焙模型加载器(unbaked model loaders)使用单例模式(singleton pattern),因为所有模型都可以通过一个加载器进行加载。
public static final MyUnbakedModelLoader INSTANCE = new MyUnbakedModelLoader();
// 我们用于注册此加载器的 id,同时也会在数据生成(datagen)类中使用。
public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath("examplemod", "my_custom_loader");
// 根据单例模式,将构造函数设为私有。
private MyUnbakedModelLoader() {}
@Override
public void onResourceManagerReload(ResourceManager resourceManager) {
// 处理任何缓存清理的逻辑
}
@Override
public MyUnbakedModel read(JsonObject jsonObject, JsonDeserializationContext context) throws JsonParseException {
// 使用传入的 JsonObject,如果需要还可以使用 JsonDeserializationContext,从模型 JSON 中获取属性。
// MyUnbakedModel 构造函数可以有参数(见下文)。
return new MyUnbakedModel();
}
}
// AbstractUnbakedModel 用作自定义模型的基础未烘焙模型。
// 支持原版(vanilla)和 NeoForge 的属性,但 bake 方法留给模组开发者实现。
public class MyUnbakedModel extends AbstractUnbakedModel {
// 构造函数可以包含你需要的任何参数,并将它们存储到字段中以便后续使用。
// 如果构造函数有参数,MyUnbakedModelLoader#read 中的构造调用必须与之匹配。
public MyUnbakedModel() {}
// 负责模型烘焙(baking)的方法,返回我们烘焙后的模型。该方法的参数如下:
// - 纹理名称到其关联材质的映射(map)。
// - 模型烘焙器(model baker)。可用于烘焙子模型和从纹理槽获取精灵(sprite)。
// - 模型状态(model state)。包含 blockstate 文件中的属性,例如旋转(rotations)和 uvlock 布尔值。
// - 渲染模型时是否使用环境光遮蔽(ambient occlusion)的布尔值。
// - 渲染模型时是否使用方块光照(block light)的布尔值。
// - 此模型在指定 ItemDisplayContext 下应如何显示的物品变换(item transforms)。
// - 由 NeoForge 提供的设置 ContextMap。可用属性详见 'NeoForgeModelProperties' 类。
@Override
public BakedModel bake(TextureSlots textures, ModelBaker baker, ModelState modelState, boolean useAmbientOcclusion, boolean usesBlockLight, ItemTransforms itemTransforms, ContextMap additionalProperties) {
// true 布尔值表示模型在 GUI 中为 3D
var builder = new SimpleBakedModel.Builder(useAmbientOcclusion, usesBlockLight, true, itemTransforms);
// 设置粒子纹理(particle texture)
builder.particle(baker.findSprite(textures, TextureSlot.PARTICLE.getId()));
// 添加烘焙四边形(baked quads)(可根据需要多次调用)
builder.addUnculledFace(...) // 或者 addCulledFace(Direction, BakedQuad)
// 创建烘焙模型
return builder.build(additionalProperties.getOrDefault(NeoForgeModelProperties.RENDER_TYPE, RenderTypeGroup.EMPTY));
}
// 负责正确解析父级属性的方法。如果该模型加载了任何嵌套模型,或对自身使用了原版加载器,则必须实现(见下文)。
@Override
public void resolveDependencies(ResolvableModel.Resolver resolver) {
// ResolvableModel.Resolver#resolve
}
// 向用于烘焙的 context map 添加属性
@Override
public void fillAdditionalProperties(ContextMap.Builder propertiesBuilder) {
super.fillAdditionalProperties(propertiesBuilder);
// 通过调用 withParameter(ContextKey<T>, T) 在下方添加额外属性
}
}
当一切准备就绪后,别忘了实际注册你的加载器(loader),否则之前的努力都将白费:
// 客户端 mod 总线事件处理器
@SubscribeEvent
public static void registerGeometryLoaders(ModelEvent.RegisterLoaders event) {
event.register(MyUnbakedModelLoader.ID, MyUnbakedModelLoader.INSTANCE);
}
// 如果你在模型加载器中缓存了数据:
// 客户端 mod 总线事件处理器
@SubscribeEvent
public static void onRegisterReloadListeners(RegisterClientReloadListenersEvent event) {
event.registerReloadListener(MyUnbakedModelLoader.INSTANCE);
}
数据生成(Datagen)
当然,我们也可以为我们的模型进行 数据生成。为此,需要一个继承自 CustomLoaderBuilder 的类:
public class MyLoaderBuilder extends CustomLoaderBuilder {
public MyLoaderBuilder() {
super(
// 你的模型加载器的 id。
MyUnbakedModelLoader.ID,
// 如果加载器不存在,是否允许内联 vanilla 元素作为回退。
false
);
}
// 在这里添加字段以及字段的 setter。之后可以在下方使用这些字段。
@Override
protected CustomLoaderBuilder copyInternal() {
// 创建你的加载器 builder 的新实例,并将当前 builder 的属性复制到新实例中。
MyLoaderBuilder builder = new MyLoaderBuilder();
// builder.<field> = this.<field>;
return builder;
}
// 将模型序列化为 JSON。
@Override
public JsonObject toJson(JsonObject json) {
// 将你的字段添加到传入的 JsonObject 中。
// 然后调用 super 方法,它会添加 loader 属性以及其他一些内容。
return super.toJson(json);
}
}
要使用这个加载器 builder,请在方块(block)或物品(item)模型数据生成时按如下操作:
// 这里假设你已经继承了 ModelProvider,并且有一个 DeferredBlock<Block> EXAMPLE_BLOCK。
// customLoader() 的参数是一个用于构建 builder 的 Supplier,以及一个用于设置相关属性的 Consumer。
@Override
protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerators itemModels) {
blockModels.createTrivialBlock(
// 要为其生成模型的方块
EXAMPLE_BLOCK.get(),
TexturedModel.createDefault(
// 用于获取纹理的映射
block -> new TextureMapping().put(
TextureSlot.ALL, TextureMapping.getBlockTexture(block)
),
// 用于生成 JSON 的模型模板构建器
ExtendedModelTemplateBuilder.builder()
// 指定我们正在使用自定义模型加载器
.customLoader(MyLoaderBuilder::new, loader -> {
// 在这里设置任何所需的字段
})
// 模型所需的纹理
.requiredTextureSlot(TextureSlot.ALL)
// 完成后调用 build
.build()
)
)
}
可见性(Visibility)
CustomLoaderBuilder 的默认实现包含了用于设置可见性的方法。在你的模型加载器中,你可以选择使用或忽略 visibility 属性。目前,只有 复合模型加载器 和 OBJ 加载器 会用到这个属性。
复用默认模型加载器(Reusing the Default Model Loader)
在某些情况下,直接在原版模型加载器(vanilla model loader)基础上进行扩展,而不是完全替换它,会更加合理。我们可以用一个巧妙的方法实现这一点:在模型加载器中,简单地移除 loader 属性,然后将其交回给模型反序列化器(model deserializer),这样它就会被当作常规的未烘焙模型(unbaked model)处理。接着,在烘焙(bake)过程中,我们将模型传递给烘焙后的模型(baked model),这样我们就可以以任何想要的方式使用模型的四边形(quads)。
下列示例仅适用于文件只包含一个模型 JSON(无论是在顶层还是嵌套在某个对象内)的情况。如果需要加载多个模型,那么 JSON 应该包含对其他 JSON 文件的引用,或者将子对象反序列化为 UnbakedModel 并通过 UnbakedModel#bakeWithTopModelValues 进行烘焙。推荐使用引用的方式。
public class MyUnbakedModelLoader implements UnbakedModelLoader<MyUnbakedModel> {
public static final MyUnbakedModelLoader INSTANCE = new MyUnbakedModelLoader();
public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath("examplemod", "my_custom_loader");
private MyUnbakedModelLoader() {}
@Override
public MyUnbakedModel read(JsonObject jsonObject, JsonDeserializationContext context) throws JsonParseException {
// 通过移除 loader 字段,欺骗反序列化器让其认为这是一个普通模型
// 然后,将其传递给反序列化器
jsonObject.remove("loader");
UnbakedModel model = context.deserialize(jsonObject, UnbakedModel.class);
return new MyUnbakedModel(model, /* 这里填写其他参数 */);
}
}
// 继承委托类,因为它存储了被包装的模型
public class MyUnbakedModel extends DelegateUnbakedModel {
// 存储模型,供下方使用
public MyUnbakedModel(UnbakedModel model, /* 这里填写其他参数 */) {
super(model);
}
@Override
public BakedModel bake(TextureSlots textures, ModelBaker baker, ModelState modelState, boolean useAmbientOcclusion, boolean usesBlockLight, ItemTransforms itemTransforms, ContextMap additionalProperties) {
BakedModel base = super.bake(textures, baker, modelState, useAmbientOcclusion, usesBlockLight, itemTransforms, additionalProperties);
return new MyBakedModel(base, /* 这里填写其他参数 */);
}
}
// 继承委托类,因为它存储了被包装的模型
public class MyDynamicModel extends DelegateBakedModel {
// 这里填写其他字段
public MyDynamicModel(BakedModel base, /* 这里填写其他参数 */) {
super(base);
// 在这里设置其他字段
}
// 这里覆盖其他方法
@Override
public List<BakedQuad> getQuads(@Nullable BlockState state, @Nullable Direction side, RandomSource rand, ModelData extraData, @Nullable RenderType renderType) {
List<BakedQuad> quads = new ArrayList<>();
// 添加基础模型的四边形。你也可以根据需要对这些四边形进行不同的处理
quads.addAll(this.parent.getQuads(state, side, rand, extraData, renderType));
// 根据需要向 quads 列表中添加其他元素
return quads;
}
}