自定义模型加载器
模型就是形状,可以是一个立方体、多个立方体、三角形或其他几何形状的集合。多数情况下,模型如何定义并不重要,因为最终都会烘焙为 QuadCollection。因此,NeoForge 允许注册自定义加载器,将任意模型转换为游戏使用的烘焙格式。
模型加载器
方块模型仍从模型 JSON 文件开始加载,但可以在根对象指定 loader,用自己的加载器替换默认实现。自定义加载器可以忽略默认加载器所要求的全部字段。
除了默认加载器,NeoForge 还内置了多种不同用途的加载器。
组合模型
组合模型可以在父模型中指定不同部分,再由子模型只启用其中一些。例如,以下父模型位于 examplemod:example_composite_model:
{
"loader": "neoforge:composite",
// Specify model parts.
"children": {
// These can either be references to another model or a model itself.
"part_1": {
"parent": "examplemod:some_model_1"
},
"part_2": {
"parent": "examplemod:some_model_2"
}
},
"visibility": {
// Disable part 2 by default.
"part_2": false
}
}
随后,在继承 examplemod:example_composite_model 的子模型中分别启用或禁用各部分:
{
"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。
空模型
空模型不渲染任何内容。
{
"loader": "neoforge:empty"
}
OBJ 模型
OBJ 加载器允许在游戏中使用 Wavefront .obj 三维模型,从而支持三角形、圆形等任意形状。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。
创建自定义模型加载器
需要四个类及一个事件处理器:
UnbakedModelLoader类。UnbakedGeometry类,通常实现ExtendedUnbakedGeometry。UnbakedModel类,通常继承AbstractUnbakedModel。- 保存已烘焙四边形的
QuadCollection,通常直接使用该类。 - 客户端的
ModelEvent.RegisterLoaders事件处理器,用于注册加载器。 - 可选:
AddClientReloadListenersEvent的客户端处理器,适用于需要缓存已加载数据的加载器。
各部分按以下流程协作:
- 加载时,
loader指向你的加载器的模型 JSON 会被传给加载器。加载器读取属性,并结合含有未烘焙四边形的UnbakedGeometry,返回UnbakedModel。 - 烘焙时,调用
UnbakedGeometry#bake,得到QuadCollection。 - 渲染时,四边形集合与客户端物品或方块状态定义所需的其他信息共同参与渲染。
提示 如果模型用于物品或方块状态,有时更适合创建新的
ItemModel或BlockStateModel。例如直接使用或生成四边形集合的模型,更适合作为物品或方块状态模型;解析 OBJ 等不同数据格式时,则应使用新加载器。
下面给出基础类结构:加载器名为 MyUnbakedModelLoader,未烘焙模型为 MyUnbakedModel,几何体为 MyUnbakedGeometry,并假定加载器需要缓存:
// This is the class used to load the model into its unbaked format
public class MyUnbakedModelLoader implements UnbakedModelLoader<MyUnbakedModel>, ResourceManagerReloadListener {
// It is highly recommended to use a singleton pattern for unbaked model loaders, as all models can be loaded through one loader.
public static final MyUnbakedModelLoader INSTANCE = new MyUnbakedModelLoader();
// The id we will use to register this loader. Also used in the loader datagen class.
public static final Identifier ID = Identifier.fromNamespaceAndPath("examplemod", "my_custom_loader");
// In accordance with the singleton pattern, make the constructor private.
private MyUnbakedModelLoader() {}
@Override
public void onResourceManagerReload(ResourceManager resourceManager) {
// Handle any cache clearing logic
}
@Override
public MyUnbakedModel read(JsonObject obj, JsonDeserializationContext context) throws JsonParseException {
// Use the given JsonObject and, if needed, the JsonDeserializationContext to get properties from the model JSON.
// The MyUnbakedModel constructor may have constructor parameters (see below).
// Read the data used to create the quads
MyUnbakedGeometry geometry;
// For the basic parameters provided by vanilla and NeoForge, you can use the StandardModelParameters
StandardModelParameters params = StandardModelParameters.parse(obj, context);
return new MyUnbakedModel(params, geometry);
}
}
// Holds the unbaked quads to render
// Other information that is stored in the unbaked model should be passed to the context map
public class MyUnbakedGeometry implements ExtendedUnbakedGeometry {
public MyUnbakedGeometry(...) {
// Store the unbaked quads to bake
}
// Method responsible for model baking, returning the quad collection. Parameters in this method are:
// - The map of texture names to their associated materials.
// - The model baker. Can be used for getting sub-models to bake and getting sprites from the texture slots.
// - The model state. This holds the transformations from the blockstate file, typically from rotations and the uvlock.
// - The name of the model.
// - A ContextMap of settings provided by NeoForge and your unbaked model. See the 'NeoForgeModelProperties' class for all available properties.
@Override
public QuadCollection bake(TextureSlots textureSlots, ModelBaker baker, ModelState state, ModelDebugName debugName, ContextMap additionalProperties) {
// The builder to create the collection
var builder = new QuadCollection.Builder();
// Build the quads for baking
builder.addUnculledFace(...); // or addCulledFace(Direction, BakedQuad)
// Create the quad collection
return builder.build();
}
}
// The unbaked model contains all the information read from the JSON.
// It provides the basic settings and geometry.
// Using AbstractUnbakedModel sets the Vanilla and NeoForge properties methods
public class MyUnbakedModel extends AbstractUnbakedModel {
private final MyUnbakedGeometry geometry;
public MyUnbakedModel(StandardModelParameters params, MyUnbakedGeometry geometry) {
super(params);
this.geometry = geometry;
}
@Override
public UnbakedGeometry geometry() {
// The geometry to used to construct the baked quads
return this.geometry;
}
@Override
public void fillAdditionalProperties(ContextMap.Builder propertiesBuilder) {
super.fillAdditionalProperties(propertiesBuilder);
// Add additional properties below by calling withParameter(ContextKey<T>, T)
// They can then be accessed in the ContextMap provided in UnbakedGeometry#bake
}
}
完成后,别忘了注册加载器:
@SubscribeEvent // on the mod event bus only on the physical client
public static void registerLoaders(ModelEvent.RegisterLoaders event) {
event.register(MyUnbakedModelLoader.ID, MyUnbakedModelLoader.INSTANCE);
}
// If you are caching data in the model loader:
@SubscribeEvent // on the mod event bus only on the physical client
public static void addClientResourceListeners(AddClientReloadListenersEvent event) {
// Register the listener with our id
event.addListener(MyUnbakedModelLoader.ID, MyUnbakedModelLoader.INSTANCE);
// Add a dependency for our model loader to run before models are loaded
// Allows the cache to be cleared before the new data is populated
event.addDependency(MyUnbakedModelLoader.ID, VanillaClientListeners.MODELS);
}
模型加载器的数据生成
自定义模型同样可以生成。需要一个继承 CustomLoaderBuilder 的类:
public class MyLoaderBuilder extends CustomLoaderBuilder {
public MyLoaderBuilder() {
super(
// Your model loader's id.
MyUnbakedModelLoader.ID,
// 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.
@Override
protected CustomLoaderBuilder copyInternal() {
// Create a new instance of your loader builder and copy the properties from this builder
// to the new instance.
MyLoaderBuilder builder = new MyLoaderBuilder();
// builder.<field> = this.<field>;
return builder;
}
// 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 an extension of ModelProvider and a DeferredBlock<Block> EXAMPLE_BLOCK.
// The parameter for customLoader() is a Supplier to construct the builder and a Consumer to set to associated properties.
@Override
protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerators itemModels) {
blockModels.createTrivialBlock(
// The block to generate the model for
EXAMPLE_BLOCK.get(),
TexturedModel.createDefault(
// A mapping used to get the textures
block -> new TextureMapping().put(
TextureSlot.ALL, TextureMapping.getBlockTexture(block)
),
// The model template builder used to create the JSON
ExtendedModelTemplateBuilder.builder()
// Say we are using a custom model loader
.customLoader(MyLoaderBuilder::new, loader -> {
// Set any required fields here
})
// Textures required by the model
.requiredTextureSlot(TextureSlot.ALL)
// Call build once complete
.build()
)
);
}
可见性
CustomLoaderBuilder 的默认实现提供了应用可见性的方法。加载器可以自行决定是否使用 visibility 属性。目前只有组合模型加载器和 OBJ 加载器使用它。
方块状态模型加载器
方块状态模型独立于模型 JSON,因此 NeoForge 还提供相应的自定义加载器,通过在 variant 或 multipart 中指定 type 使用。自定义加载器可以忽略默认加载器要求的全部字段。
组合方块状态模型
组合方块状态模型用于同时渲染多个 BlockStateModel。
{
"variants": {
"": {
"type": "neoforge:composite",
// Specify model parts.
"models": [
// These must be inlined block state models
{
"variants": {
// ...
}
},
{
"multipart": [
// ...
]
}
// ...
]
}
}
}
要生成它,使用 CompositeBlockStateModelBuilder。
复用默认模型加载器
有时保留原版加载器、只在其基础上添加逻辑更合适,而无需完全替换。可以在自定义加载器中删除 loader 属性,再将 JSON 交回模型反序列化器,使其按普通未烘焙模型处理。随后,就可以在烘焙前修改模型或几何体。
public class MyUnbakedModelLoader implements UnbakedModelLoader<MyUnbakedModel> {
public static final MyUnbakedModelLoader INSTANCE = new MyUnbakedModelLoader();
public static final Identifier ID = Identifier.fromNamespaceAndPath("examplemod", "my_custom_loader");
private MyUnbakedModelLoader() {}
@Override
public MyUnbakedModel read(JsonObject jsonObject, JsonDeserializationContext context) throws JsonParseException {
// Trick the deserializer into thinking this is a normal model by removing the loader field
// Then, pass it to the deserializer.
jsonObject.remove("loader");
UnbakedModel model = context.deserialize(jsonObject, UnbakedModel.class);
return new MyUnbakedModel(model, /* other parameters here */);
}
}
// We extend the delegate class as that stores the wrapped model
public class MyUnbakedModel extends DelegateUnbakedModel {
// Store the model for use below
public MyUnbakedModel(UnbakedModel model, /* other parameters here */) {
super(model);
}
}
创建自定义方块状态模型加载器
需要五个类及一个事件处理器:
- 加载方块状态模型的
CustomUnbakedBlockStateModel。 - 表示烘焙模型的
BlockStateModel,通常实现DynamicBlockStateModel。 - 加载模型 JSON 的
BlockStateModelPart.Unbaked。 - 为指定面或模型应用变换的
ModelState。 - 保存四边形、环境光遮蔽及粒子纹理的
BlockStateModelPart,通常使用SimpleModelWrapper。 - 客户端
RegisterBlockStateModels处理器,注册未烘焙加载器的 codec。
加载流程如下:
- 定义加载时,variant、multipart 或自定义定义中,将
type设为该加载器的模型被解码为CustomUnbakedBlockStateModel。 - 烘焙时,调用其
#bake,返回包含模型部分列表的BlockStateModel。 - 渲染时,
BlockStateModel#collectParts收集需要渲染的部分。
下面示例中,烘焙模型名为 MyBlockStateModel,其内部 record MyBlockStateModel.Unbaked 表示未烘焙模型。模型部分为 MyBlockStateModelPart,未烘焙部分是内部 record MyBlockStateModelPart.Unbaked,模型状态名为 MyModelState:
// The model state used to apply the necessary transformations
// If you are using an intermediate object to hold the model state, it must be transformable to a ModelState
public class MyModelState implements ModelState {
// Used for the unbaked block model part
public static final Codec<MyModelState> CODEC = Codec.unit(new MyModelState());
public MyModelState() {}
@Override
public Transformation transformation() {
// Returns the model rotation to apply to the baking vertices
return Transformation.identity();
}
@Override
public Matrix4fc faceTransformation(Direction direction) {
// Returns the matrix that is applied to a given face on the model after the transformation
// This is currently unused in Vanilla
return NO_TRANSFORM;
}
@Override
public Matrix4fc inverseFaceTransformation(Direction direction) {
// Returns the inverse of faceTransformation that is applied to a given face on the model
// This is passed to the FaceBakery
return NO_TRANSFORM;
}
}
// The model part representing a baked model
// useAmbientOcclusion and particleMaterial are implemented as part of the record
public record MyBlockStateModelPart(QuadCollection quads, boolean useAmbientOcclusion, Material.Baked particleMaterial) implements BlockStateModelPart {
// Get the baked quads to render
@Override
List<BakedQuad> getQuads(@Nullable Direction direction) {
return this.quads.getQuads(direction);
}
// The flags of the materials backing the quads.
@Override
public int materialFlags() {
return this.quads.materialFlags();
}
// The unbaked model that is read from the block state json
public record Unbaked(Identifier modelLocation, MyModelState modelState) implements BlockStateModelPart.Unbaked {
// Used for the unbaked block state model
public static final MapCodec<MyBlockStateModelPart.Unbaked> CODEC = RecordCodecBuilder.mapCodec(
instance -> instance.group(
Identifier.CODEC.fieldOf("model").forGetter(MyBlockStateModelPart.Unbaked::modelLocation),
MyModelState.CODEC.fieldOf("state").forGetter(MyBlockStateModelPart.Unbaked::modelState)
).apply(instance, MyBlockStateModelPart.Unbaked::new)
);
@Override
public void resolveDependencies(ResolvableModel.Resolver resolver) {
// Mark any models used by the model part
resolver.markDependency(this.modelLocation);
}
@Override
public BlockStateModelPart bake(ModelBaker baker) {
// Get the model to bake
ResolvedModel resolvedModel = baker.getModel(this.modelLocation);
// Get the necessary settings for the model part
TextureSlots slots = resolvedModel.getTopTextureSlots();
boolean ao = resolvedModel.getTopAmbientOcclusion();
Material.Baked particle = resolvedModel.resolveParticleMaterial(slots, baker);
QuadCollection quads = resolvedModel.bakeTopGeometry(slots, baker, this.modelState);
// Return the baked part
return new MyBlockStateModelPart(quads, ao, particle);
}
}
}
// The state model representing the baked block state
public record MyBlockStateModel(MyBlockStateModelPart model) implements DynamicBlockStateModel {
// Sets the particle material
// While it needs to be implemented, any actual logic should be delegated to the level-aware version
@Override
public Material.Baked particleMaterial() {
return this.model.particleMaterial();
}
// The flags of the materials backing the quads.
// While it needs to be implemented, any actual logic should be delegated to the level-aware version
@Override
public int materialFlags() {
return this.quads.materialFlags();
}
// This effectively acts as a key to reuse geometry previous produced. This should generally be as deterministic as possible.
@Override
public Object createGeometryKey(BlockAndTintGetter level, BlockPos pos, BlockState state, RandomSource random) {
return this;
}
// Method responsible for collecting the parts to be rendered. Parameters in this method are:
// - The getter for the blocks and tints, usually the level.
// - The position of the block to render.
// - The state of the block.
// - A random instance.
// - This list of model parts to be rendered. Add your model parts here.
@Override
public void collectParts(BlockAndTintGetter level, BlockPos pos, BlockState state, RandomSource random, List<BlockStateModelPart> parts) {
// If you want the block rendered to be dependent on the block entity (e.g., your block entity implements `BlockEntity#getModelData`)
// You can call `BlockAndTintGetter#getModelData` with the block position
// You can read the property using `get` with the `ModelProperty` key
// Remember that your block entity should call `BlockEntity#requestModelDataUpdate` to sync the model data to the client
ModelData data = level.getModelData(pos);
// Add the model to be rendered
parts.add(this.model);
}
@Override
public Material.Baked particleMaterial(BlockAndTintGetter level, BlockPos pos, BlockState state) {
// Override this if you want to use the level to determine what particle to render
return self().particleMaterial();
}
@Override
public int materialFlags(BlockAndTintGetter level, BlockPos pos, BlockState state) {
// Override this if you want to use the level to determine what material flags the model has
return self().materialFlags();
}
// The unbaked model that is read from the block state json
public record Unbaked(MyBlockStateModelPart.Unbaked model) implements CustomUnbakedBlockStateModel {
// The codec to register
public static final MapCodec<MyBlockStateModel.Unbaked> CODEC = MyBlockStateModelPart.Unbaked.CODEC.xmap(
MyBlockStateModel.Unbaked::new, MyBlockStateModel.Unbaked::model
);
public static final Identifier ID = Identifier.fromNamespaceAndPath("examplemod", "my_custom_model_loader");
@Override
public void resolveDependencies(ResolvableModel.Resolver resolver) {
// Mark any models used by the state model
this.model.resolveDependencies(resolver);
}
@Override
public BlockStateModel bake(ModelBaker baker) {
// Bake the model parts and pass into the block state model
return new MyBlockStateModel(this.model.bake(baker));
}
}
}
完成后,注册加载器:
@SubscribeEvent // on the mod event bus only on the physical client
public static void registerDefinitions(RegisterBlockStateModels event) {
event.registerModel(MyBlockStateModel.Unbaked.ID, MyBlockStateModel.Unbaked.CODEC);
}
状态模型加载器的数据生成
模型也可以生成,需要继承 CustomBlockStateModelBuilder:
// The builder used to construct the block state JSON
public class MyBlockStateModelBuilder extends CustomBlockStateModelBuilder {
private MyBlockStateModelPart.Unbaked model;
public MyBlockStateModelBuilder() {}
// Add fields and setters for the fields here. The fields can then be used below.
@Override
public MyBlockStateModelBuilder with(VariantMutator variantMutator) {
// If you want to apply any mutators that assumes your unbaked model part is a `Variant`
// If not, this should do nothing
return this;
}
// This is for generalized unbaked blockstate models
@Override
public MyBlockStateModelBuilder with(UnbakedMutator unbakedMutator) {
var result = new MyBlockStateModelBuilder();
if (this.model != null) {
result.model = unbakedMutator.apply(this.model);
}
return result;
}
// Converts the builder to its unbaked variant to encode
@Override
public CustomUnbakedBlockStateModel toUnbaked() {
return new MyBlockStateModel.Unbaked(this.model);
}
}
在方块或物品模型数据生成中,如下使用该构建器:
// This assumes an extension of ModelProvider and a DeferredBlock<Block> EXAMPLE_BLOCK.
@Override
protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerators itemModels) {
blockModels.blockStateOutput.accept(
MultiVariantGenerator.dispatch(
// The block to generate the model for
EXAMPLE_BLOCK.get(),
// Our custom block state builder
MultiVariant.of(new CustomBlockStateModelBuilder().with(...))
)
);
}
生成结果如下:
{
"variants": {
"": {
"type": "examplemod:my_custom_model_loader"
// Other fields
}
}
}
方块状态定义加载器
单个状态模型处理某个状态的加载,而定义加载器处理整个方块状态文件,通过 neoforge:definition_type 指定。自定义定义加载器可以忽略默认加载器要求的所有字段。
创建自定义方块状态定义加载器
需要两个类和一个事件处理器:
- 加载定义的
CustomBlockModelDefinition。 - 将方块状态烘焙为
BlockStateModel的BlockStateModel.UnbakedRoot。 - 客户端
RegisterBlockStateModels处理器,注册对应 codec。
流程如下:
- 定义加载时,
neoforge:definition_type指向加载器的定义被解码为CustomBlockModelDefinition。 - 随后调用
CustomBlockModelDefinition#instantiate,将所有可能状态映射到相应BlockStateModel.UnbakedRoot。简单情况可通过BlockStateModel.Unbaked#asRoot构造,复杂情况则创建自己的根对象。 - 烘焙时调用
BlockStateModel.UnbakedRoot#bake,为某个BlockState返回BlockStateModel。
以下示例的定义名为 MyBlockModelDefinition,复用 BlockStateModel.Unbaked#asRoot 构造根对象:
public record MyBlockModelDefinition(MyBlockStateModel.Unbaked model) implements CustomBlockModelDefinition {
// The codec to register
public static final MapCodec<MyBlockModelDefinition> CODEC = MyBlockStateModel.Unbaked.CODEC.xmap(
MyBlockModelDefinition::new, MyBlockModelDefinition::model
);
public static final Identifier ID = Identifier.fromNamespaceAndPath("examplemod", "my_custom_definition_loader");
// This method maps all possible states to some unbaked root
// As the root will generally share block states models, they are typically operated using a `ModelBaker.SharedOperationKey` to cache the loading model
@Override
public Map<BlockState, BlockStateModel.UnbakedRoot> instantiate(StateDefinition<Block, BlockState> states, Supplier<String> sourceSupplier) {
Map<BlockState, BlockStateModel.UnbakedRoot> result = new HashMap<>();
// Handle for all possible states
var unbakedRoot = this.model.asRoot();
states.getPossibleStates().forEach(state -> result.put(state, unbakedRoot));
return result;
}
@Override
public MapCodec<? extends CustomBlockModelDefinition> codec() {
return CODEC;
}
}
完成后,注册加载器:
@SubscribeEvent // on the mod event bus only on the physical client
public static void registerDefinitions(RegisterBlockStateModels event) {
event.registerDefinition(MyBlockModelDefinition.ID, MyBlockModelDefinition.CODEC);
}
状态定义加载器的数据生成
定义也可以生成,需要实现 BlockModelDefinitionGenerator:
public class MyBlockModelDefinitionGenerator implements BlockModelDefinitionGenerator {
private final Block block;
private final MyBlockStateModelBuilder builder;
private MyBlockModelDefinitionGenerator(Block block, MyBlockStateModelBuilder builder) {
this.block = block;
this.builder = builder;
}
public static MyBlockModelDefinitionGenerator dispatch(Block block, MyBlockStateModelBuilder builder) {
return new MyBlockModelDefinitionGenerator(block, builder);
}
@Override
public Block block() {
// Returns the block you are generating the definition file for
return this.block;
}
@Override
public BlockModelDefinition create() {
// Creates the block model definition used to encode and decode the file
return new MyBlockModelDefinition(this.builder.toUnbaked());
}
}
在方块或物品的模型数据生成中,如下使用该构建器:
// This assumes a DeferredBlock<Block> EXAMPLE_BLOCK.
@Override
protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerators itemModels) {
blockModels.blockStateOutput.accept(
MyBlockModelDefinitionGenerator.dispatch(
// The block to generate the model for
EXAMPLE_BLOCK.get(),
new CustomBlockStateModelBuilder(...)
)
);
}
生成结果如下:
{
"neoforge:definition_type": "examplemod:my_custom_definition_loader"
// Other fields
}
来源: NeoForged/Documentation(26.1)。中文翻译依据提交
4a680cdd4d15。