自定义模型加载器
模型本质上就是形状,可以是立方体、一组立方体、一组三角形,或其他几何形状及其组合。在多数场景中,定义方式并不重要,因为它最终都会成为内存中的 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。
创建自定义模型加载器
需要三个类和一个事件处理器:
模型加载流程说明了它们之间的关系:
- 加载模型时,
loader指向自定义加载器的 JSON 会交给几何加载器。它读取属性并返回几何对象。 - 烘焙模型时,几何对象被烘焙为动态烘焙模型。
- 渲染模型时,使用该动态模型。
以下基本结构分别使用 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。