自定义模型加载器

自定义模型加载器

模型本质上就是形状,可以是立方体、一组立方体、一组三角形,或其他几何形状及其组合。在多数场景中,定义方式并不重要,因为它最终都会成为内存中的 BakedModel。因此,NeoForge 允许注册自定义模型加载器,将任意模型转换成供游戏使用的 BakedModel。

方块模型仍以模型 JSON 为入口。不过,可在根对象指定 loader 字段,将默认加载器替换为自己的加载器。自定义加载器可以忽略默认加载器要求的全部字段。

内置模型加载器

除默认加载器外,NeoForge 提供若干各有用途的内置加载器。

组合模型

组合模型可在父模型中定义多个部件,再在子模型中只应用其中一部分。以位于 examplemod:example_composite_model 的父模型为例:

{
    "loader": "neoforge:composite",
    // Specify model parts.
    "children": {
        "part_1": {
            "parent": "examplemod:some_model_1"
        },
        "part_2": {
            "parent": "examplemod:some_model_2"
        }
    },
    "visibility": {
        // Disable part 2 by default.
        "part_2": false
    }
}

随后可在其子模型中分别启用或禁用部件:

{
    "parent": "examplemod:example_composite_model",
    // Override visibility. If a part is missing, it will use the parent model's visibility value.
    "visibility": {
        "part_1": false,
        "part_2": true
    }
}

要通过数据生成创建此模型,使用自定义加载器构建类 CompositeModelBuilder。

动态流体容器模型

动态流体容器模型常用于桶,因此也称动态桶模型。它适用于代表桶、储罐等流体容器,并希望在模型中显示流体的物品。这仅适用于可用流体种类固定的情况(例如只有熔岩和细雪)。若可使用任意流体,应改用 BlockEntityWithoutLevelRenderer。

{
    "loader": "neoforge:fluid_container",
    // Required. Must be a valid fluid id.
    "fluid": "minecraft:water",
    // The loader generally expects two textures: base and fluid.
    "textures": {
        // The base container texture, i.e. the equivalent of an empty bucket.
        "base": "examplemod:item/custom_container",
        // The fluid texture, i.e. the equivalent of water in a bucket.
        "fluid": "examplemod:item/custom_container_fluid"
    },
    // Optional, defaults to false. Whether to flip the model upside down, for gaseous fluids.
    "flip_gas": true,
    // Optional, defaults to true. Whether to have the cover act as the mask.
    "cover_is_mask": false,
    // Optional, defaults to true. Whether to apply the fluid's luminosity to the item model.
    "apply_fluid_luminosity": false,
}

动态流体容器通常直接使用桶模型,可指定 neoforge:item_bucket 父模型,如下例(实际父路径为 neoforge:item/bucket):

{
    "loader": "neoforge:fluid_container",
    "parent": "neoforge:item/bucket",
    // Replace with your own fluid.
    "fluid": "minecraft:water"
    // Optional properties here. Note that the textures are handled by the parent.
}

要生成此模型,使用 DynamicFluidContainerModelBuilder。注意,出于旧版兼容原因,该类仍有设置 apply_tint 的方法,但此属性已不再使用。

元素模型

元素模型由方块模型元素和可选的根变换组成,主要用于常规模型渲染之外的场景,例如方块实体渲染器。

{
    "loader": "neoforge:elements",
    "elements": [...],
    "transform": {...}
}

空模型

空模型不渲染任何内容。

{
    "loader": "neoforge:empty"
}

物品图层模型

物品图层模型是标准 item/generated 的变体,额外支持无限图层(默认上限为 5),以及逐图层的渲染类型。

{
    "loader": "neoforge:item_layers",
    "textures": {
        "layer0": "...",
        "layer1": "...",
        "layer2": "...",
        "layer3": "...",
        "layer4": "...",
        "layer5": "...",
    },
    "render_types": {
        // Map render types to layer numbers. For example, layers 0, 2 and 4 will use cutout.
        "minecraft:cutout": [0, 2, 4],
        "minecraft:cutout_mipped": [1, 3],
        "minecraft:translucent": [5]
    },
    // other stuff the default loader allows here
}

要生成此模型,使用 ItemLayerModelBuilder。

OBJ 模型

OBJ 加载器支持在游戏中使用 Wavefront .obj 三维模型,允许任意形状,包括三角形、圆形等。文件必须放在 models 或其子文件夹,且需提供同名 .mtl(或手动指定)。例如,models/block/example.obj 对应 models/block/example.mtl。

{
    "loader": "neoforge:obj",
    // Required. Reference to the model file. Note that this is relative to the namespace root, not the model folder.
    "model": "examplemod:models/example.obj",
    // Normally, .mtl files must be put into the same location as the .obj file, with only the file ending differing.
    // This will cause the loader to automatically pick them up. However, you can also set the location
    // of the .mtl file manually if needed.
    "mtl_override": "examplemod:models/example_other_name.mtl",
    // These textures can be referenced in the .mtl file as #texture0, #particle, etc.
    // This usually requires manual editing of the .mtl file.
    "textures": {
        "texture0": "minecraft:block/cobblestone",
        "particle": "minecraft:block/stone"
    },
    // Enable or disable automatic culling of the model. Optional, defaults to true.
    "automatic_culling": false,
    // Whether to shade the model or not. Optional, defaults to true.
    "shade_quads": false,
    // Some modeling programs will assume V=0 to be bottom instead of the top. This property flips the Vs upside-down.
    // Optional, defaults to false.
    "flip_v": true,
    // Whether to enable emissivity or not. Optional, defaults to true.
    "emissive_ambient": false
}

要生成此模型,使用 ObjModelBuilder。

独立变换模型

独立变换模型根据视角切换不同模型,视角与普通模型的 display 区块相同。先指定用于回退的基础模型,再为各视角指定覆盖模型。每一项都可以是完整模型,但通常最简单的方式是使用另一个模型的子模型作为引用:

{
    "loader": "neoforge:separate_transforms",
    // Use the cobblestone model normally.
    "base": {
        "parent": "minecraft:block/cobblestone"
    },
    // Use the stone model only when dropped.
    "perspectives": {
        "ground": {
            "parent": "minecraft:block/stone"
        }
    }
}

要生成此模型,使用 SeparateTransformsModelBuilder。

创建自定义模型加载器

需要三个类和一个事件处理器:

模型加载流程说明了它们之间的关系:

  1. 加载模型时,loader 指向自定义加载器的 JSON 会交给几何加载器。它读取属性并返回几何对象。
  2. 烘焙模型时,几何对象被烘焙为动态烘焙模型。
  3. 渲染模型时,使用该动态模型。

以下基本结构分别使用 MyGeometryLoader、MyGeometry 和 MyDynamicModel:

public class MyGeometryLoader implements IGeometryLoader<MyGeometry> {
    // It is highly recommended to use a singleton pattern for geometry loaders, as all models can be loaded through one loader.
    public static final MyGeometryLoader INSTANCE = new MyGeometryLoader();
    // The id we will use to register this loader. Also used in the loader datagen class.
    public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath("examplemod", "my_custom_loader");
    
    // In accordance with the singleton pattern, make the constructor private.        
    private MyGeometryLoader() {}
    
    @Override
    public MyGeometry read(JsonObject jsonObject, JsonDeserializationContext context) throws JsonParseException {
        // Use the given JsonObject and, if needed, the JsonDeserializationContext to get properties from the model JSON.
        // The MyGeometry constructor may have constructor parameters (see below).
        return new MyGeometry();
    }
}

public class MyGeometry implements IUnbakedGeometry<MyGeometry> {
    // The constructor may have any parameters you need, and store them in fields for further usage below.
    // If the constructor has parameters, the constructor call in MyGeometryLoader#read must match them.
    public MyGeometry() {}

    // Method responsible for model baking, returning our dynamic model. Parameters in this method are:
    // - The geometry baking context. Contains many properties that we will pass into the model, e.g. light and ao values.
    // - The model baker. Can be used for baking sub-models.
    // - The sprite getter. Maps materials (= texture variables) to TextureAtlasSprites. Materials can be obtained from the context.
    //   For example, to get a model's particle texture, call spriteGetter.apply(context.getMaterial("particle"));
    // - The model state. This holds the properties from the blockstate file, e.g. rotations and the uvlock boolean.
    // - The item overrides. This is the code representation of an "overrides" block in an item model.
    @Override
    public BakedModel bake(IGeometryBakingContext context, ModelBaker baker, Function<Material, TextureAtlasSprite> spriteGetter, ModelState modelState, ItemOverrides overrides) {
        // See info on the parameters below.
        return new MyDynamicModel(context.useAmbientOcclusion(), context.isGui3d(), context.useBlockLight(),
            spriteGetter.apply(context.getMaterial("particle")), overrides);
    }

    // Method responsible for correctly resolving parent properties. Required if this model loads any nested models or reuses the vanilla loader on itself (see below).
    @Override
    public void resolveParents(Function<ResourceLocation, UnbakedModel> modelGetter, IGeometryBakingContext context) {
        // UnbakedModel#resolveParents
    }
}

// BakedModelWrapper can be used as well to return default values for most methods, allowing you to only override what actually needs to be overridden.
public class MyDynamicModel implements IDynamicBakedModel {
    // Material of the missing texture. Its sprite can be used as a fallback when needed.
    private static final Material MISSING_TEXTURE = 
        new Material(TextureAtlas.LOCATION_BLOCKS, MissingTextureAtlasSprite.getLocation());

    // Attributes for use in the methods below. Optional, the methods may also use constant values if applicable.
    private final boolean useAmbientOcclusion;
    private final boolean isGui3d;
    private final boolean usesBlockLight;
    private final TextureAtlasSprite particle;
    private final ItemOverrides overrides;

    // The constructor does not require any parameters other than the ones for instantiating the final fields.
    // It may specify any additional parameters to store in fields you deem necessary for your model to work.
    public MyDynamicModel(boolean useAmbientOcclusion, boolean isGui3d, boolean usesBlockLight, TextureAtlasSprite particle, ItemOverrides overrides) {
        this.useAmbientOcclusion = useAmbientOcclusion;
        this.isGui3d = isGui3d;
        this.usesBlockLight = usesBlockLight;
        this.particle = particle;
        this.overrides = overrides;
    }

    // Use our attributes. Refer to the article on baked models for more information on the method's effects.
    @Override
    public boolean useAmbientOcclusion() {
        return useAmbientOcclusion;
    }

    @Override
    public boolean isGui3d() {
        return isGui3d;
    }

    @Override
    public boolean usesBlockLight() {
        return usesBlockLight;
    }

    @Override
    public TextureAtlasSprite getParticleIcon() {
        // Return MISSING_TEXTURE.sprite() if you don't need a particle, e.g. when in an item model context.
        return particle;
    }

    @Override
    public ItemOverrides getOverrides() {
        // Return ItemOverrides.EMPTY when in a block model context.
        return overrides;
    }

    // Override this to true if you want to use a custom block entity renderer instead of the default renderer.
    @Override
    public boolean isCustomRenderer() {
        return false;
    }

    // This is where the magic happens. Return a list of the quads to render here. Parameters are:
    // - The blockstate being rendered. May be null if rendering an item.
    // - The side being culled against. May be null, which means quads that cannot be occluded should be returned.
    // - A client-bound random source you can use for randomizing stuff.
    // - The extra data to use. Originates from a block entity (if present), or from BakedModel#getModelData().
    // - The render type for which quads are being requested.
    // NOTE: This may be called many times in quick succession, up to several times per block.
    // This should be as fast as possible and use caching wherever applicable.
    @Override
    public List<BakedQuad> getQuads(@Nullable BlockState state, @Nullable Direction side, RandomSource rand, ModelData extraData, @Nullable RenderType renderType) {
        List<BakedQuad> quads = new ArrayList<>();
        // add elements to the quads list as needed here
        return quads;
    }
}

最后务必注册加载器,否则它不会生效:

@SubscribeEvent // on the mod event bus only on the physical client
public static void registerGeometryLoaders(ModelEvent.RegisterGeometryLoaders event) {
    event.register(MyGeometryLoader.ID, MyGeometryLoader.INSTANCE);
}

数据生成

也可以生成这些模型,需要继承 CustomLoaderBuilder:

// This assumes a block model. Use ItemModelBuilder as the generic parameter instead 
// if you're making a custom item model.
public class MyLoaderBuilder extends CustomLoaderBuilder<BlockModelBuilder> {
    public MyLoaderBuilder(BlockModelBuilder parent, ExistingFileHelper existingFileHelper) {
        super(
            // Your model loader's id.
            MyGeometryLoader.ID,
            // The parent builder we use. This is always the first constructor parameter.
            parent,
            // The existing file helper we use. This is always the second constructor parameter.
            existingFileHelper,
            // Whether the loader allows inline vanilla elements as a fallback if the loader is absent.
            false
        );
    }
    
    // Add fields and setters for the fields here. The fields can then be used below.
    
    // Serialize the model to JSON.
    @Override
    public JsonObject toJson(JsonObject json) {
        // Add your fields to the given JsonObject.
        // Then call super, which adds the loader property and some other things.
        return super.toJson(json);
    }
}

在方块或物品的模型数据生成过程中,使用该构建器:

// This assumes a BlockStateProvider. Use getBuilder("my_cool_block") directly in an ItemModelProvider.
// The parameter for customLoader() is a BiFunction. The parameters of the BiFunction
// are the result of the getBuilder() call and the provider's ExistingFileHelper.
MyLoaderBuilder loaderBuilder = models().getBuilder("my_cool_block").customLoader(MyLoaderBuilder::new);

然后在 loaderBuilder 上调用自定义字段的 setter。

可见性

CustomLoaderBuilder 默认实现包含设置可见性的方法。加载器可选择使用或忽略 visibility。目前只有组合模型加载器使用此属性。

复用默认加载器

有时,在原版加载器基础上添加逻辑比完全替换更合适。可以在加载器中移除 loader 属性,将 JSON 再交给模型反序列化器,让它按普通模型处理。随后把模型传给几何类,像默认处理器一样烘焙,再传给动态模型。这样就可按需操作基础模型的四边形:

public class MyGeometryLoader implements IGeometryLoader<MyGeometry> {
    public static final MyGeometryLoader INSTANCE = new MyGeometryLoader();
    public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath(...);
    
    private MyGeometryLoader() {}
    
    @Override
    public MyGeometry read(JsonObject jsonObject, JsonDeserializationContext context) throws JsonParseException {
        // Trick the deserializer into thinking this is a normal model by removing the loader field and then passing it back into the deserializer.
        jsonObject.remove("loader");
        BlockModel base = context.deserialize(jsonObject, BlockModel.class);
        // other stuff here if needed
        return new MyGeometry(base);
    }
}

public class MyGeometry implements IUnbakedGeometry<MyGeometry> {
    private final BlockModel base;

    // Store the block model for usage below.            
    public MyGeometry(BlockModel base) {
        this.base = base;
    }

    @Override
    public BakedModel bake(IGeometryBakingContext context, ModelBaker baker, Function<Material, TextureAtlasSprite> spriteGetter, ModelState modelState, ItemOverrides overrides) {
        BakedModel bakedBase = new ElementsModel(base.getElements()).bake(context, baker, spriteGetter, modelState, overrides);
        return new MyDynamicModel(bakedBase, /* other parameters here */);
    }

    @Override
    public void resolveParents(Function<ResourceLocation, UnbakedModel> modelGetter, IGeometryBakingContext context) {
        base.resolveParents(modelGetter);
    }
}

public class MyDynamicModel implements IDynamicBakedModel {
    private final BakedModel base;
    // other fields here

    public MyDynamicModel(BakedModel base, /* other parameters here */) {
        this.base = base;
        // set other fields here
    }

    // other override methods here

    @Override
    public List<BakedQuad> getQuads(@Nullable BlockState state, @Nullable Direction side, RandomSource rand, ModelData extraData, @Nullable RenderType renderType) {
        List<BakedQuad> quads = new ArrayList<>();
        // Add the base model's quads. Can also do something different with the quads here, depending on what you need.
        quads.addAll(base.getQuads(state, side, rand, extraData, renderType));
        // add other elements to the quads list as needed here
        return quads;
    }
    
    // Apply the base model's transforms to our model as well.
    @Override
    public BakedModel applyTransform(ItemDisplayContext transformType, PoseStack poseStack, boolean applyLeftHandTransform) {
        return base.applyTransform(transformType, poseStack, applyLeftHandTransform);
    }
}

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