特殊物品模型与自定义渲染
并非所有模型都能用基础 JSON 表示。有些包含动态部分,或复用 BlockEntityRenderer 创建的 Model。这时可使用特殊模型类型,指定提交渲染的特征。这些渲染器称为 SpecialModelRenderer,已有类型定义在 SpecialModelRenderers 中。
JSON
// For some item 'examplemod:example_item'
// JSON at 'assets/examplemod/items/example_item.json'
{
"model": {
"type": "minecraft:special",
// The parent model to read the particle texture and display transformation from
// Points to 'assets/minecraft/models/item/template_skull.json'
"base": "minecraft:item/template_skull",
"model": {
// The special model renderer to use
"type": "minecraft:head",
// Properties defined by `SkullSpecialRenderer.Unbaked`
// The type of the skull block
"kind": "wither_skeleton",
// The texture to use when rendering the head
// Points to 'assets/examplemod/textures/entity/heads/skeleton_override.png'
"texture": "examplemod:heads/skeleton_override",
// The animation float used to animate the head model
"animation": 0.5
}
}
}
数据生成
// Assume there is some DeferredItem<Item> EXAMPLE_ITEM
// Within an extended ModelProvider
@Override
protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerators itemModels) {
itemModels.itemModelOutput.accept(
EXAMPLE_ITEM.get(),
new SpecialModelWrapper.Unbaked(
// The parent model to read the particle texture and display transformation from
// Points to 'assets/minecraft/models/item/template_skull.json'
Identifier.fromNamespaceAndPath("minecraft", "item/template_skull"),
// The special model renderer to use
new SkullSpecialRenderer.Unbaked(
// The type of the skull block
SkullBlock.Types.WITHER_SKELETON,
// The texture to use when rendering the head
// Points to 'assets/examplemod/textures/entity/heads/skeleton_override.png'
Optional.of(
Identifier.fromNamespaceAndPath("examplemod", "heads/skeleton_override")
),
// The animation float used to animate the head model
0.5f
)
)
);
}
自定义特殊模型渲染器分三部分:负责提交渲染特征的 SpecialModelRenderer 实例、负责 JSON 读写的 SpecialModelRenderer.Unbaked 实例,以及将其用于物品、必要时用于方块的注册步骤。
首先是渲染器本体,工作方式与方块实体、实体等其他渲染器类似。构造时传入提交过程使用的静态数据,例如 Model 子类、纹理 SpriteId 等。需要关注两个方法:extractArgument 从 ItemStack 中只提取必要数据,限制 submit 可见的信息量。
提示 不确定需要哪些数据时,可以直接返回当前
ItemStack。如果完全不需要堆叠数据,可使用已实现此方法的NoDataSpecialModelRenderer。
submit 接收提取的数据、姿态栈、用于提交特征的收集器、打包光照、叠加纹理、是否具有闪光(例如附魔),以及轮廓颜色。全部特征提交都应在此方法中完成。
public record ExampleSpecialRenderer(SpriteGetter spriteGetter, Model.Simple model, SpriteId sprite) implements SpecialModelRenderer<Boolean> {
@Nullable
public Boolean extractArgument(ItemStack stack) {
// Extract the data to be used
return stack.isBarVisible();
}
// Submit the features of the model
@Override
public void submit(Boolean argument, PoseStack poseStack, SubmitNodeCollector collector, int lightCoords, int overlayCoords, boolean hasFoil, int outlineColor) {
collector.submitModel(
this.model, Unit.INSTANCE,
poseStack, this.sprite.renderType(barVisible ? RenderType::entityCutout : RenderType::entitySolid),
lightCoords, overlayCoords, -1, this.spriteGetter.get(this.sprite), outlineColor, null
);
}
}
接着是 SpecialModelRenderer.Unbaked,保存可从文件读取、用于确定渲染器构造参数的数据。它有两个方法:bake 构建实际渲染器实例,type 提供文件编码和解码使用的 MapCodec。
public record ExampleSpecialRenderer(SpriteGetter spriteGetter, Model.Simple model, SpriteId sprite) implements SpecialModelRenderer<Boolean> {
// ...
public record Unbaked(Identifier texture) implements SpecialModelRenderer.Unbaked {
public static final MapCodec<ExampleSpecialRenderer.Unbaked> MAP_CODEC = Identifier.CODEC.fieldOf("texture")
.xmap(ExampleSpecialRenderer.Unbaked::new, ExampleSpecialRenderer.Unbaked::texture);
@Override
public MapCodec<ExampleSpecialRenderer.Unbaked> type() {
return MAP_CODEC;
}
@Override
public SpecialModelRenderer<?> bake(SpecialModelRenderer.BakingContext ctx) {
// Resolve resource location to absolute path
Identifier textureLoc = this.texture.withPath(path -> "textures/entity/" + path + ".png");
// Get the model and the sprites to render
return new ExampleSpecialRenderer(ctx.sprites(), ...);
}
}
}
最后注册对象。客户端物品通过 Mod 事件总线上的 RegisterSpecialModelRendererEvent 注册。如果渲染器也应作为 BlockEntityRenderer 的一部分,用于末影人搬运方块等类似物品的渲染上下文,还应在同一总线的 RegisterBlockModelsEvent 中注册方块对应的 Unbaked 版本。
// In some event handler class
@SubscribeEvent // on the mod event bus only on the physical client
public static void registerSpecialRenderers(RegisterSpecialModelRendererEvent event) {
event.register(
// The name to reference as the type
Identifier.fromNamespaceAndPath("examplemod", "example_special"),
// The map codec
ExampleSpecialRenderer.Unbaked.MAP_CODEC
);
}
// For rendering a block in an item-like context
// Assume some DeferredBlock<ExampleBlock> EXAMPLE_BLOCK
@SubscribeEvent // on the mod event bus only on the physical client
public static void registerSpecialBlockRenderers(RegisterBlockModelsEvent event) {
event.register(
// The unbaked instance to use
new SpecialBlockModelWrapper.Unbaked(
new ExampleSpecialRenderer.Unbaked(Identifier.fromNamespaceAndPath("examplemod", "entity/example_special")),
Optional.empty()
),
// The block to render for
EXAMPLE_BLOCK.get()
);
}
JSON
// For some item 'examplemod:example_item'
// JSON at 'assets/examplemod/items/example_item.json'
{
"model": {
"type": "minecraft:special",
// The parent model to read the particle texture and display transformation from
// Points to 'assets/minecraft/models/item/template_skull.json'
"base": "minecraft:item/template_skull",
"model": {
// The special model renderer to use
"type": "examplemod:example_special",
// Properties defined by `ExampleSpecialRenderer.Unbaked`
// The texture to use
// Points to 'assets/examplemod/textures/entity/example/example_texture.png'
"texture": "examplemod:example/example_texture"
}
}
}
数据生成
// Assume there is some DeferredItem<Item> EXAMPLE_ITEM
// Within an extended ModelProvider
@Override
protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerators itemModels) {
itemModels.itemModelOutput.accept(
EXAMPLE_ITEM.get(),
new SpecialModelWrapper.Unbaked(
// The parent model to read the particle texture and display transformation from
// Points to 'assets/minecraft/models/item/template_skull.json'
Identifier.fromNamespaceAndPath("minecraft", "item/template_skull"),
// The special model renderer to use
new ExampleSpecialRenderer.Unbaked(
// The texture to use
// Points to 'assets/examplemod/textures/entity/example/example_texture.png'
Identifier.fromNamespaceAndPath("examplemod", "example/example_texture")
)
)
);
}
动态流体容器
NeoForge 添加了动态流体容器物品模型,可在运行时根据内部流体重新设置纹理。
提示 要让流体颜色应用到纹理,物品必须具有
Capabilities.FluidHandler.ITEM。若物品既不是BucketItem本身,也不是其子类,就需要注册该能力。
JSON
// For some item 'examplemod:example_item'
// JSON at 'assets/examplemod/items/example_item.json'
{
"model": {
"type": "neoforge:fluid_container",
// The textures used to construct the container
// These are in reference to the block atlas, so they are relative to the `textures` directory
"textures": {
// Sets the model particle sprite
// If not set, uses the first texture that is not null:
// - Fluid still texture
// - Container base texture
// - Container cover texture, if not used as a mask
// Points to 'assets/minecraft/textures/item/bucket.png'
"particle": "minecraft:item/bucket",
// Sets the texture to use on the first layer, generally the container of the fluid
// If not set, the layer will not be added
// Points to 'assets/minecraft/textures/item/bucket.png'
"base": "minecraft:item/bucket",
// Sets the texture to use as the mask for the still fluid texture
// Areas where the fluid is seen should be pure white
// If not set or the fluid is empty, then the layer is not drawn
// Points to 'assets/neoforge/textures/item/mask/bucket_fluid.png'
"fluid": "neoforge:item/mask/bucket_fluid",
// Sets the texture to use as either
// - The overlay texture when 'cover_is_mask' is false
// - The mask to apply to the base texture (should be pure white to see) when 'cover_is_mask' is true
// If not set or no base texture is set when 'cover_is_mask' is true, then the layer is not drawn
// Points to 'assets/neoforge/textures/item/mask/bucket_fluid_cover.png'
"cover": "neoforge:item/mask/bucket_fluid_cover",
},
// When true, rotates the model 180 degrees for fluids whose density is negative or zero
// Defaults to false
"flip_gas": true,
// When true, uses the cover texture as a mask for the base texture
// Defaults to true
"cover_is_mask": true,
// When true, sets the lightmap of the fluid texture layer to its max value
// for fluids whose light level is greater than zero
// Defaults to true
"apply_fluid_luminosity": false
}
}
数据生成
// Assume there is some DeferredItem<ExampleFluidContainerItem> EXAMPLE_ITEM
// Within an extended ModelProvider
@Override
protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerators itemModels) {
itemModels.itemModelOutput.accept(
EXAMPLE_ITEM.get(),
new DynamicFluidContainerModel.Unbaked(
// The textures used to construct the container
// These are in reference to the block atlas, so they are relative to the `textures` directory
new DynamicFluidContainerModel.Textures(
// Sets the model particle sprite
// If not set, uses the first texture that is not null:
// - Fluid still texture
// - Container base texture
// - Container cover texture, if not used as a mask
// Points to 'assets/minecraft/textures/item/bucket.png'
Optional.of(Identifier.withDefaultNamespace("item/bucket")),
// Sets the texture to use on the first layer, generally the container of the fluid
// If not set, the layer will not be added
// Points to 'assets/minecraft/textures/item/bucket.png'
Optional.of(Identifier.withDefaultNamespace("item/bucket")),
// Sets the texture to use as the mask for the still fluid texture
// Areas where the fluid is seen should be pure white
// If not set or the fluid is empty, then the layer is not rendered
// Points to 'assets/neoforge/textures/item/mask/bucket_fluid.png'
Optional.of(Identifier.fromNamespaceAndPath("neoforge", "item/mask/bucket_fluid")),
// Sets the texture to use as either
// - The overlay texture when 'cover_is_mask' is false
// - The mask to apply to the base texture (should be pure white to see) when 'cover_is_mask' is true
// If not set or no base texture is set when 'cover_is_mask' is true, then the layer is not rendered
// Points to 'assets/neoforge/textures/item/mask/bucket_fluid_cover.png'
Optional.of(Identifier.fromNamespaceAndPath("neoforge", "item/mask/bucket_fluid_cover"))
),
// When true, rotates the model 180 degrees
// Defaults to false
true,
// When true, uses the cover texture as a mask for the base texture
// Defaults to true
true,
// When true, sets the lightmap of the fluid texture layer to its max value
// Defaults to true
false
)
);
}
手动提交物品渲染
若需要在 BlockEntityRenderer 或 EntityRenderer 等位置提交物品渲染特征,可分三步:先由渲染器创建 ItemStackRenderState 保存堆叠状态,再通过 ItemModelResolver 的方法将其更新为当前物品,最后调用 ItemStackRenderState#submit 提交。
渲染状态保存绘制数据。每个“模型”都有自己的 ItemStackRenderState.LayerRenderState,包含待渲染四边形、渲染类型、闪光状态、着色信息、动画标志、边界及特殊渲染器。用 newLayer 创建层,用 clear 清理渲染状态。如果层数固定,可调用 ensureCapacity 保证有足够层数。
提示 界面使用子类
TrackingItemStackRenderState,保存模型身份信息,以跨帧缓存已渲染状态。
ItemModelResolver 负责更新状态。生物持有的物品使用 updateForLiving,其他实体持有的物品使用 updateForNonLiving,其余情况使用 updateForTopItem。这些方法接收渲染状态、堆叠及显示上下文,其他参数提供手、世界、物品所有者及种子值。每个方法先调用 ItemStackRenderState#clear,再根据 DataComponents#ITEM_MODEL 获取模型并调用其 update。如果不在方块实体或实体渲染器等上下文中,可始终通过 Minecraft#getItemModelResolver 获取解析器。
自定义物品模型定义
自定义 ItemModel 分三部分:更新渲染状态的实例、负责 JSON 读写的 ItemModel.Unbaked,以及注册模型。
警告 先确认前面介绍的现有系统无法创建所需模型。大多数情况下无需自定义
ItemModel。
ItemModel 负责更新 ItemStackRenderState,让物品正确绘制。构造时传入提交过程使用的静态数据,例如四边形列表、属性信息等。唯一方法 update 接收渲染状态、堆叠、解析器、显示上下文、世界、物品所有者和种子值。只应修改 ItemStackRenderState,其余参数均视为只读。
public record ExampleModelWrapper(QuadCollection quads, List<ItemTintSource> tints, ModelRenderProperties properties, Matrix4fc transformation) implements ItemModel {
// Update the render state
@Override
public void update(ItemStackRenderState state, ItemStack stack, ItemModelResolver resolver, ItemDisplayContext displayContext, @Nullable ClientLevel level, @Nullable ItemOwner owner, int seed) {
// Set the identity used by the model
state.appendModelIdentityElement(this);
// Create a new layer
ItemStackRenderState.LayerRenderState layerState = state.newLayer();
// Sets the foil to use
if (stack.hasFoil()) {
layerState.setFoilType(ItemStackRenderState.FoilType.STANDARD);
state.appendModelIdentityElement(ItemStackRenderState.FoilType.STANDARD);
}
// Apply the tint sources
int tintSize = this.tints.size();
int[] tintLayers = layerState.prepareTintLayers(tintSize);
for (int idx = 0; idx < tintSize; idx++) {
int tintColor = this.tints.get(idx).calculate(stack, level, owner.asLivingEntity());
tintLayers[idx] = tintColor;
state.appendModelIdentityElement(tintColor);
}
// Computes the bounds of the model
// Used for GUI render bounds (when oversized) and item entity bobbing
layerState.setExtents(CuboidItemModelWrapper.computeExtents(this.quads.getAll()));
// Set the local transforms to apply for the client item
layerState.setLocalTransform(this.transformation);
// Set other common model properties
this.properties.applyToLayer(layerState, displayContext);
// Adds the quads to submit
layerState.prepareQuadList().addAll(this.quads.getAll());
// Set animated if it has the associated material flag
if (this.quads.hasMaterialFlag(BakedQuad.FLAG_ANIMATED)) {
layerState.setAnimated();
}
}
}
接着创建 ItemModel.Unbaked,保存从文件读取、用于构造物品模型的数据。它有两个方法:bake 构造 ItemModel 实例,type 提供文件编码和解码的 MapCodec。
public record ExampleModelWrapper(QuadCollection quads, List<ItemTintSource> tints, ModelRenderProperties properties, Matrix4fc transformation) implements ItemModel {
// ...
public record Unbaked(Identifier model, List<ItemTintSource> tints, Optional<Transformation> transformation) implements ItemModel.Unbaked {
// The map codec to register
public static final MapCodec<ExampleModelWrapper.Unbaked> MAP_CODEC = RecordCodecBuilder.mapCodec(instance ->
instance.group(
Identifier.CODEC.fieldOf("model").forGetter(ExampleModelWrapper.Unbaked::model),
ItemTintSources.CODEC.listOf().optionalFieldOf("tints", List.of()).forGetter(ExampleModelWrapper.Unbaked::tints)
Transformation.EXTENDED_CODEC.optionalFieldOf("transformation").forGetter(ExampleModelWrapper.Unbaked::transformation)
)
.apply(instance, ExampleModelWrapper.Unbaked::new)
);
@Override
public void resolveDependencies(ResolvableModel.Resolver resolver) {
// Mark all dependencies for this item model
resolver.markDependency(this.model);
}
@Override
public ItemModel bake(ItemModel.BakingContext context, Matrix4fc parentTransform) {
// Get the baked quads and return
ModelBaker baker = context.blockModelBaker();
ResolvedModel resolvedModel = baker.getModel(this.model);
TextureSlots slots = resolvedModel.getTopTextureSlots();
return new ExampleModelWrapper(
resolvedModel.bakeTopGeometry(slots, baker, BlockModelRotation.IDENTITY),
this.tints,
ModelRenderProperties.fromResolvedModel(baker, resolvedModel, slots),
Transformation.compose(parentTransform, this.transformation)
);
}
@Override
public MapCodec<ExampleModelWrapper.Unbaked> type() {
return MAP_CODEC;
}
}
}
随后通过 Mod 事件总线上的 RegisterItemModelsEvent 注册 map codec。
// In some event handler class
@SubscribeEvent // on the mod event bus only on the physical client
public static void registerItemModels(RegisterItemModelsEvent event) {
event.register(
// The name to reference as the type
Identifier.fromNamespaceAndPath("examplemod", "render_type"),
// The map codec
ExampleModelWrapper.Unbaked.MAP_CODEC
)
}
最后,即可在 JSON 或数据生成中使用该模型。
JSON
// For some item 'examplemod:example_item'
// JSON at 'assets/examplemod/items/example_item.json'
{
"model": {
"type": "examplemod:render_type",
// Points to 'assets/examplemod/models/item/example_item.json'
"model": "examplemod:item/example_item",
// Any tints to apply to the model texture
"tints": []
}
}
数据生成
// Assume there is some DeferredItem<Item> EXAMPLE_ITEM
// Within an extended ModelProvider
@Override
protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerators itemModels) {
itemModels.itemModelOutput.accept(
EXAMPLE_ITEM.get(),
new ExampleModelWrapper.Unbaked(
// Points to 'assets/examplemod/models/item/example_item.json'
ModelLocationUtils.getModelLocation(EXAMPLE_ITEM.get()),
// Any tints to apply to the model texture
List.of(),
// The transformations to apply after the model JSON transforms
Optional.empty()
)
);
}
来源: NeoForged/Documentation(26.1,原文第 1135–1641 行)。中文翻译依据提交
4a680cdd4d15。