Skip to main content
Version: 1.21.4

客户端物品(Client Items)

客户端物品(Client Items)是在代码中描述 ItemStack 在游戏内应如何渲染的方式,具体指定了在不同状态下应使用哪些模型。客户端物品文件位于 assets 文件夹 下的 items 子目录中,其相对路径由 DataComponents#ITEM_MODEL 指定。默认情况下,这个路径就是对象的注册表名称(registry name)(例如,minecraft:apple 默认会对应于 assets/minecraft/items/apple.json)。

客户端物品数据被存储在 ModelManager 中,可以通过 Minecraft.getInstance().modelManager 进行访问。然后,你可以调用 ModelManager#getItemModelgetItemProperties,通过其 ResourceLocation 获取客户端物品的信息。

warning

不要将客户端物品与 烘焙模型(baked models) 混淆,后者定义了实际在游戏中渲染的模型及其四边形(quads)。

概览(Overview)

客户端物品的 JSON 文件可以分为两部分:由 model 定义的模型部分,以及由 properties 定义的属性部分。model 用于指定在特定渲染上下文下应使用哪些模型 JSON 文件。另一方面,properties 用于设置渲染器使用的相关参数。

// 针对某个物品 'examplemod:example_item'
// JSON 文件位于 'assets/examplemod/items/example_item.json'
{
// 定义要渲染的模型
"model": {
"type": "minecraft:model",
// 指向 'models' 目录下的模型 JSON 文件的路径
// 实际路径为 'assets/examplemod/models/item/example_item.json'
"model": "examplemod:item/example_item"
},
// 定义渲染过程中使用的一些设置
"properties": {
// 当为 false 时,禁用物品切换时
// 物品向其常规位置上抬起的动画效果
"hand_animation_on_swap": false
}
}

关于物品模型渲染的更多信息可以在下文中找到。

一个基础模型(A Basic Model)

model 字段中的 type 决定了如何选择要渲染的物品模型。最简单的类型由 minecraft:model(或 BlockModelWrapper)处理,其功能是定义要渲染的模型 JSON,相对于 models 目录(例如 assets/<namespace>/models/<path>.json)。

// 针对某个物品 'examplemod:example_item'
// JSON 位于 'assets/examplemod/items/example_item.json'
{
"model": {
"type": "minecraft:model",
// 指向相对于 'models' 目录的模型 JSON
// 路径为 'assets/examplemod/models/item/example_item.json'
"model": "examplemod:item/example_item"
}
}

着色(Tinting)

和大多数模型一样,客户端物品模型可以根据物品堆(stack)的属性来改变指定纹理的颜色。因此,minecraft:model 类型拥有 tints 字段,用于定义要应用的不透明颜色。这些被称为 ItemTintSource(原文),定义在 ItemTintSources(原文)中。它们同样拥有一个 type 字段,用于指定使用哪种着色源。应用到的 tintindex 由它们在列表中的索引决定。

// 对于某个物品 'examplemod:example_item'
// JSON 文件位于 'assets/examplemod/items/example_item.json'
{
"model": {
"type": "minecraft:model",
// 指向 'assets/examplemod/models/item/example_item.json'
"model": "examplemod:item/example_item",
// 要应用的染色列表
"tints": [
{
// 当 tintindex: 0 时使用
"type": "minecraft:constant",
// 0x00FF00(纯绿色)
"value": 65280
},
{
// 当 tintindex: 1 时使用
"type": "minecraft:dye",
// 0x0000FF(纯蓝色)
// 仅当未设置 `DataComponents#DYED_COLOR` 时才会调用
"default": 255
}
]
}
}

创建你自己的 ItemTintSource 与其他基于 codec 的注册表对象类似。你需要编写一个实现了 ItemTintSource 的类,创建一个用于编码和解码该对象的 MapCodec,并通过 mod 事件总线 上的 RegisterColorHandlersEvent.ItemTintSources 方法将该 codec 注册到其注册表(Registry)中。ItemTintSource 只包含一个方法 calculate,它接受当前的 ItemStack、该物品堆所在的世界(level)以及持有该物品堆的实体(entity),返回一个 ARGB 格式的不透明颜色,其中高 8 位为 0xFF。

public record DamageBar(int defaultColor) implements ItemTintSource {

// 需要注册的 map 编解码器
public static final MapCodec<DamageBar> MAP_CODEC = ExtraCodecs.RGB_COLOR_CODEC.fieldOf("default")
.xmap(DamageBar::new, DamageBar::defaultColor);

public DamageBar(int defaultColor) {
// 确保传入的颜色是完全不透明的
this.defaultColor = ARGB.opaque(defaultColor);
}

@Override
public int calculate(ItemStack stack, @Nullable ClientLevel level, @Nullable LivingEntity entity) {
// 如果物品已受损,则返回其耐久条颜色,否则使用默认颜色
return stack.isDamaged() ? ARGB.opaque(stack.getBarColor()) : defaultColor;
}

@Override
public MapCodec<DamageBar> type() {
return MAP_CODEC;
}
}

// 在某个客户端类中,将事件注册到模组事件总线
@SubscribeEvent
public static void registerItemTintSources(RegisterColorHandlersEvent.ItemTintSources event) {
event.register(
// 用于作为类型引用的名称
ResourceLocation.fromNamespaceAndPath("examplemod", "damage_bar"),
// map 编解码器
DamageBar.MAP_CODEC
)
}
// 针对某个物品 'examplemod:example_item'
// JSON 文件位于 'assets/examplemod/items/example_item.json'
{
"model": {
"type": "minecraft:model",
// 指向 'assets/examplemod/models/item/example_item.json'
"model": "examplemod:item/example_item",
// 要应用的染色列表
"tints": [
{
// 当 tintindex: 0 时生效
"type": "examplemod:damage_bar",
// 0x00FF00(纯绿色)
"default": 65280
}
]
}
}

复合模型(Composite Models)

有时候,你可能希望为同一个物品注册多个模型。虽然可以直接使用 复合模型加载器 实现,但对于物品模型,有一个自定义的 minecraft:composite 类型,可以接受一个模型列表用于渲染。

// 对于某个物品 'examplemod:example_item'
// JSON 文件位于 'assets/examplemod/items/example_item.json'
{
"model": {
"type": "minecraft:composite",

// 要渲染的模型
// 将按照列表中出现的顺序进行渲染
"models": [
{
"type": "minecraft:model",
// 指向 'assets/examplemod/models/item/example_item_1.json'
"model": "examplemod:item/example_item_1"
},
{
"type": "minecraft:model",
// 指向 'assets/examplemod/models/item/example_item_2.json'
"model": "examplemod:item/example_item_2"
}
]
}
}

属性模型(Property Models)

有些物品会根据其物品堆(stack)中存储的数据改变自身状态(例如,拉弓、损坏的鞘翅、时钟在不同维度下的显示等)。为了让模型能够根据状态变化,物品模型可以指定一个属性(property)来追踪,并根据该条件选择不同的模型。属性模型主要有三种类型:区间分发(range dispatch)、选择(select)和条件(conditional)。它们分别对应 float 表达式、switch case 和 boolean 判断。

区间分发模型(Range Dispatch Models)

范围分派模型(Range dispatch models)要求类型定义某个 RangeSelectItemModelProperty,以获取一个浮点数用于切换模型。每个条目都包含一个阈值(threshold),只有当该属性值大于该阈值时,对应的模型才会被渲染。最终选择的模型是所有阈值中不超过属性值、且最接近该属性值的那一个(例如,如果属性值为 4,阈值有 35,则会渲染与 3 关联的模型;如果属性值为 6,则渲染与 5 关联的模型)。可用的 RangeSelectItemModelProperty 可在 RangeSelectItemModelProperties 中找到。

// 针对某个物品 'examplemod:example_item'
// JSON 文件位于 'assets/examplemod/items/example_item.json'
{
"model": {
"type": "minecraft:range_dispatch",

// 要使用的 `RangeSelectItemModelProperty`
"property": "minecraft:count",
// 用于对计算得到的属性值进行缩放的系数
// 如果 count 为 0.3,scale 为 0.2,则最终检查的阈值为 0.3*0.2=0.06
"scale": 1,
"fallback": {
// 当没有任何阈值匹配时使用的回退模型
// 可以是任何未烘焙(unbaked)的模型类型
"type": "minecraft:model",
// 指向 'assets/examplemod/models/item/example_item.json'
"model": "examplemod:item/example_item"
},

// 由 `Count` 定义的属性
// 若为 true,则使用最大堆叠数量对 count 进行归一化
"normalize": true,

// 带有阈值信息的条目
"entries": [
{
// 当 count 为当前最大堆叠数量的三分之一时
"threshold": 0.33,
"model": {
// 可以是任何未烘焙(unbaked)的模型类型
"type": "minecraft:model",
// 指向 'assets/examplemod/models/item/example_item_1.json'
"model": "examplemod:item/example_item_1"
}
},
{
// 当 count 为当前最大堆叠数量的三分之二时
"threshold": 0.66,
"model": {
// 可以是任何未烘焙(unbaked)的模型类型
"type": "minecraft:model",
// 指向 'assets/examplemod/models/item/example_item_2.json'
"model": "examplemod:item/example_item_2"
}
}
]
}
}

自定义 RangeSelectItemModelProperty 的方法与其他基于 codec 的注册表对象类似。你需要创建一个实现了 RangeSelectItemModelProperty 的类,编写一个 MapCodec 用于编解码该对象,并通过 mod 事件总线 上的 RegisterRangeSelectItemModelPropertyEvent 将该 codec 注册到其注册表(Registry)中。RangeSelectItemModelProperty 只包含一个方法 get,它接收当前的 ItemStack、该堆叠所在的等级(level)、持有该堆叠的实体,以及一个用于返回任意 float 值的种子值,供区间分派模型进行解释。

public record AppliedEnchantments() implements RangeSelectItemModelProperty {

public static final MapCodec<AppliedEnchantments> MAP_CODEC = MapCodec.unit(new AppliedEnchantments());

@Override
public float get(ItemStack stack, @Nullable ClientLevel level, @Nullable LivingEntity entity, int seed) {
return (float) stack.getEnchantments().size();
}

@Override
public MapCodec<AppliedEnchantments> type() {
return MAP_CODEC;
}
}

// 在某个客户端类中,将事件注册到模组事件总线
@SubscribeEvent
public static void registerRangeProperties(RegisterRangeSelectItemModelPropertyEvent event) {
event.register(
// 用作类型引用的名称
ResourceLocation.fromNamespaceAndPath("examplemod", "applied_enchantments"),
// map 编解码器
AppliedEnchantments.MAP_CODEC
)
}
// 针对某个物品 'examplemod:example_item'
// JSON 文件位于 'assets/examplemod/items/example_item.json'
{
"model": {
"type": "minecraft:range_dispatch",

// 要使用的 `RangeSelectItemModelProperty`
"property": "examplemod:applied_enchantments",
// 用于乘以计算所得属性值的缩放系数
// 如果计数为 0.3,scale 为 0.2,则阈值判断为 0.3*0.2=0.06
"scale": 0.5,
"fallback": {
// 当没有任何阈值匹配时使用的备用模型
// 可以是任意未烘焙模型类型
"type": "minecraft:model",
// 指向 'assets/examplemod/models/item/example_item.json'
"model": "examplemod:item/example_item"
},

// 带有阈值信息的条目
"entries": [
{
// 至少存在一个附魔时
// 因为 1 * 缩放系数 0.5 = 0.5
"threshold": 0.5,
"model": {
// 可以是任意未烘焙模型类型
"type": "minecraft:model",
// 指向 'assets/examplemod/models/item/example_item_1.json'
"model": "examplemod:item/example_item_1"
}
},
{
// 至少存在两个附魔时
// 因为 2 * 缩放系数 0.5 = 1
"threshold": 1,
"model": {
// 可以是任意未烘焙模型类型
"type": "minecraft:model",
// 指向 'assets/examplemod/models/item/example_item_2.json'
"model": "examplemod:item/example_item_2"
}
}
]
}
}

选择模型(Select Models)

选择模型(Select models)与区间分派模型(range dispatch models)类似,但它们根据由 SelectItemModelProperty 定义的某个值进行切换,有点类似于针对枚举的 switch 语句。被选中的模型是属性值与 case 语句中值完全匹配的那一个。可用的 SelectItemModelProperty 可在 SelectItemModelProperties 中找到。

// 针对某个物品 'examplemod:example_item'
// JSON 文件位于 'assets/examplemod/items/example_item.json'
{
"model": {
"type": "minecraft:select",

// 要使用的 `SelectItemModelProperty`
"property": "minecraft:display_context",
"fallback": {
// 如果没有匹配的情况,则使用该备用模型
// 可以是任意未烘焙(unbaked)的模型类型
"type": "minecraft:model",
"model": "examplemod:item/example_item"
},

// 基于可选属性(Selectable Property)进行分支判断
"cases": [
{
// 当显示上下文为 `ItemDisplayContext#GUI` 时
"when": "gui",
"model": {
// 可以是任意未烘焙(unbaked)的模型类型
"type": "minecraft:model",
// 指向 'assets/examplemod/models/item/example_item_1.json'
"model": "examplemod:item/example_item_1"
}
},
{
// 当显示上下文为 `ItemDisplayContext#FIRST_PERSON_RIGHT_HAND` 时
"when": "firstperson_righthand",
"model": {
// 可以是任意未烘焙(unbaked)的模型类型
"type": "minecraft:model",
// 指向 'assets/examplemod/models/item/example_item_2.json'
"model": "examplemod:item/example_item_2"
}
}
]
}
}

创建你自己的 SelectItemModelProperty 与基于编解码器(codec-based)的注册表对象(Registry object)类似。你需要实现一个 SelectItemModelProperty<T> 的类,创建一个用于序列化和反序列化属性值的 Codec,再创建一个用于编码和解码对象的 MapCodec,并通过 mod 事件总线 上的 RegisterSelectItemModelPropertyEvent 注册该编解码器到其注册表(Registry)。SelectItemModelProperty 拥有一个泛型 T,用于表示切换的值。它只包含一个方法 get,该方法接收当前的 ItemStack、该物品栈所在的关卡、持有该物品栈的实体、一个种子值,以及物品的显示上下文,并返回一个任意的 T,供 select model 解释使用。

// 选择属性类
public record StackRarity() implements SelectItemModelProperty<Rarity> {

// 待注册的对象,包含相关的编解码器
public static final SelectItemModelProperty.Type<StackRarity, Rarity> TYPE = SelectItemModelProperty.Type.create(
// 此属性的 map 编解码器
MapCodec.unit(new StackRarity()),
// 被选择对象的编解码器
// 用于序列化 case 条目("when": <property value>)
Rarity.CODEC
);

@Nullable
@Override
public Rarity get(ItemStack stack, @Nullable ClientLevel level, @Nullable LivingEntity entity, int seed, ItemDisplayContext displayContext) {
// 返回 null 时,使用回退模型
return stack.get(DataComponents.RARITY);
}

@Override
public SelectItemModelProperty.Type<StackRarity, Rarity> type() {
return TYPE;
}
}

// 在某个客户端类中,事件注册到 mod 事件总线
@SubscribeEvent
public static void registerSelectProperties(RegisterSelectItemModelPropertyEvent event) {
event.register(
// 作为类型引用的名称
ResourceLocation.fromNamespaceAndPath("examplemod", "rarity"),
// 属性类型
StackRarity.TYPE
)
}
// 对于某个物品 'examplemod:example_item'
// 位于 'assets/examplemod/items/example_item.json' 的 JSON 配置
{
"model": {
"type": "minecraft:select",

// 要使用的 `SelectItemModelProperty`
"property": "examplemod:rarity",
"fallback": {
// 如果没有匹配的 case,则使用此回退模型
// 可以是任意未烘焙(unbaked)的模型类型
"type": "minecraft:model",
"model": "examplemod:item/example_item"
},

// 根据可选属性(Selectable Property)进行分支
"cases": [
{
// 当稀有度为 `Rarity#UNCOMMON` 时
"when": "uncommon",
"model": {
// 可以是任意未烘焙(unbaked)的模型类型
"type": "minecraft:model",
// 指向 'assets/examplemod/models/item/example_item_1.json'
"model": "examplemod:item/example_item_1"
}
},
{
// 当稀有度为 `Rarity#RARE` 时
"when": "rare",
"model": {
// 可以是任意未烘焙(unbaked)的模型类型
"type": "minecraft:model",
// 指向 'assets/examplemod/models/item/example_item_2.json'
"model": "examplemod:item/example_item_2"
}
}
]
}
}

条件模型(Conditional Models)

条件模型(Conditional models)是三种模型类型中最简单的一种。该类型定义了某个 ConditionalItemModelProperty,用于获取一个布尔值(boolean),以决定是否切换模型。模型的选择取决于返回的布尔值是 true 还是 false。可用的 ConditionalItemModelProperty 可以在 ConditionalItemModelProperties 中找到。

// 对于某个物品 'examplemod:example_item'
// 位于 'assets/examplemod/items/example_item.json' 的 JSON 文件
{
"model": {
"type": "minecraft:condition",

// 要使用的 `ConditionalItemModelProperty`
"property": "minecraft:damaged",

// 布尔结果为 true 时的处理
"on_true": {
// 可以是任意未烘焙(unbaked)模型类型
"type": "minecraft:model",
// 指向 'assets/examplemod/models/item/example_item_1.json'
"model": "examplemod:item/example_item_1"

},
"on_false": {
// 可以是任意未烘焙(unbaked)模型类型
"type": "minecraft:model",
// 指向 'assets/examplemod/models/item/example_item_2.json'
"model": "examplemod:item/example_item_2"
}
}
}

创建你自己的 ConditionalItemModelProperty 与其他基于编解码器(codec-based)注册对象类似。你需要实现一个 ConditionalItemModelProperty 接口的类,创建一个用于编码和解码该对象的 MapCodec,并通过 mod 事件总线 上的 RegisterConditionalItemModelPropertyEvent 将该编解码器注册到其注册表(registry)中。RangeSelectItemModelProperty 只包含一个方法 get,它接受当前的 ItemStack,该物品堆所在的世界(level),持有该物品堆的实体(entity)、某个带种子的值(seeded value),以及物品的显示上下文(display context),并返回一个任意布尔值,该值将被条件模型(conditional model)用于判断选择 on_true 还是 on_false

public record BarVisible() implements ConditionalItemModelProperty {

public static final MapCodec<BarVisible> MAP_CODEC = MapCodec.unit(new BarVisible());

@Override
public boolean get(ItemStack stack, @Nullable ClientLevel level, @Nullable LivingEntity entity, int seed, ItemDisplayContext context) {
return stack.isBarVisible();
}

@Override
public MapCodec<BarVisible> type() {
return MAP_CODEC;
}
}

// 在某个客户端类中,将事件注册到模组事件总线
@SubscribeEvent
public static void registerConditionalProperties(RegisterConditionalItemModelPropertyEvent event) {
event.register(
// 用作类型引用的名称
ResourceLocation.fromNamespaceAndPath("examplemod", "bar_visible"),
// Map codec
BarVisible.MAP_CODEC
)
}
// 针对某个物品 'examplemod:example_item'
// JSON 文件位于 'assets/examplemod/items/example_item.json'
{
"model": {
"type": "minecraft:condition",

// 要使用的 `ConditionalItemModelProperty`
"property": "examplemod:bar_visible",

// 布尔结果为 true 时的模型
"on_true": {
// 可以是任意未烘焙(unbaked)模型类型
"type": "minecraft:model",
// 指向 'assets/examplemod/models/item/example_item_1.json'
"model": "examplemod:item/example_item_1"

},
"on_false": {
// 可以是任意未烘焙(unbaked)模型类型
"type": "minecraft:model",
// 指向 'assets/examplemod/models/item/example_item_2.json'
"model": "examplemod:item/example_item_2"
}
}
}

特殊模型(Special Models)

并非所有模型都能通过基础的模型 JSON 进行渲染。有些模型可以动态渲染,或者复用为 BlockEntityRenderer 创建的现有模型。在这些情况下,有一种特殊的模型类型,允许开发者自定义渲染逻辑。这类模型被称为 SpecialModelRenderer,定义在 SpecialModelRenderers 内。

// 针对某个物品 'examplemod:example_item'
// JSON 文件位于 'assets/examplemod/items/example_item.json'
{
"model": {
"type": "minecraft:special",

// 读取粒子贴图和显示变换的父模型
// 指向 'assets/minecraft/models/item/template_skull.json'
"base": "minecraft:item/template_skull",
"model": {
// 要使用的特殊模型渲染器
"type": "minecraft:head",

// 由 `SkullSpecialRenderer.Unbaked` 定义的属性
// 骷髅方块的类型
"kind": "wither_skeleton",
// 渲染头部时使用的贴图
// 指向 'assets/examplemod/textures/entity/heads/skeleton_override.png'
"texture": "examplemod:heads/skeleton_override",
// 用于头部模型动画的浮点数
"animation": 0.5
}
}
}

自定义 SpecialModelRenderer 主要分为三个部分:用于渲染物品的 SpecialModelRenderer 实例、用于 JSON 读写的 SpecialModelRenderer.Unbaked 实例,以及注册逻辑(用于物品渲染,必要时也用于方块渲染)。 首先,有一个 SpecialModelRenderer。它的工作方式与其他渲染器类(例如方块实体渲染器、实体渲染器)类似。它应该接收在渲染过程中需要用到的静态数据(比如 Model 实例、纹理的 Material 等)。需要注意两个方法。首先是 extractArgument,它用于从 ItemStack 中提取必要的数据,只将渲染所需的数据传递给 render 方法,从而限制 render 方法可以访问的数据量。

note

如果你不确定会需要哪些数据,可以让这个方法直接返回对应的 ItemStack。如果你不需要从堆叠中获取任何数据,可以使用 NoDataSpecialModelRenderer,它已经帮你实现了这个方法。

接下来是 render 方法。它接收 extractArgument 返回的值、物品的显示上下文、用于渲染的 pose stack、可用的 buffer sources、打包光照值、叠加纹理,以及一个布尔值表示该物品堆叠是否带有箔片效果(例如附魔)。所有的渲染逻辑都应该在这个方法中完成。

public record ExampleSpecialRenderer(Model model, Material material) implements SpecialModelRenderer<Boolean> {

@Nullable
public Boolean extractArgument(ItemStack stack) {
// 提取要使用的数据
return stack.isBarVisible();
}

// 渲染模型
@Override
public void render(@Nullable Boolean barVisible, ItemDisplayContext displayContext, PoseStack pose, MultiBufferSource bufferSource, int light, int overlay, boolean hasFoil) {
this.model.renderToBuffer(pose, this.material.buffer(bufferSource, barVisible ? RenderType::entityCutout : RenderType::entitySolid), light, overlay);
}
}

接下来是 SpecialModelRenderer.Unbaked 实例。它应该包含可以从文件中读取的数据,用于决定传递哪些内容给特殊渲染器。它同样包含两个方法:bake 用于构建特殊渲染器实例;type 用于定义编码/解码文件时使用的 MapCodec

public record ExampleSpecialRenderer(Model model, Material material) implements SpecialModelRenderer<Boolean> {

// ...

public record Unbaked(ResourceLocation texture) implements SpecialModelRenderer.Unbaked {

public static final MapCodec<ExampleSpecialRenderer.Unbaked> MAP_CODEC = ResourceLocation.CODEC.fieldOf("texture")
.xmap(ExampleSpecialRenderer.Unbaked::new, ExampleSpecialRenderer.Unbaked::texture);

@Override
public MapCodec<ExampleSpecialRenderer.Unbaked> type() {
return MAP_CODEC;
}

@Override
public SpecialModelRenderer<?> bake(EntityModelSet modelSet) {
// 将资源位置解析为绝对路径
ResourceLocation textureLoc = this.texture.withPath(path -> "textures/entity/" + path + ".png");

// 获取需要渲染的模型和材质
return new ExampleSpecialRenderer(...);
}
}
}

最后,我们需要将这些对象注册到它们各自需要的位置。对于客户端物品,这一步是通过 mod 事件总线(mod event bus) 上的 RegisterSpecialModelRendererEvent 完成的。如果你希望这个特殊渲染器(special renderer)也被用于 BlockEntityRenderer,比如在某些物品样式的上下文中渲染方块(例如末影人手持该方块时),那么就需要通过 mod 事件总线 上的 RegisterSpecialBlockModelRendererEvent 注册一个方块的 Unbaked 版本。

// 在某个客户端类中,将该事件注册到 mod 事件总线
@SubscribeEvent
public static void registerSpecialRenderers(RegisterSpecialModelRendererEvent event) {
event.register(
// 用作类型引用的名称
ResourceLocation.fromNamespaceAndPath("examplemod", "example_special"),
// map 编解码器
ExampleSpecialRenderer.Unbaked.MAP_CODEC
)
}

// 用于在物品样式上下文中渲染方块
// 假设有一个 DeferredBlock<ExampleBlock> EXAMPLE_BLOCK
@SubscribeEvent
public static void registerSpecialBlockRenderers(RegisterSpecialBlockModelRendererEvent event) {
event.register(
// 需要渲染的方块
EXAMPLE_BLOCK.get()
// 要使用的 unbaked 实例
new ExampleSpecialRenderer.Unbaked(ResourceLocation.fromNamespaceAndPath("examplemod", "entity/example_special"))
)
}
// 针对某个物品 'examplemod:example_item'
// 位于 'assets/examplemod/items/example_item.json' 的 JSON 文件
{
"model": {
"type": "minecraft:special",

// 父模型,用于读取粒子材质和显示变换
// 指向 'assets/minecraft/models/item/template_skull.json'
"base": "minecraft:item/template_skull",
"model": {
// 要使用的特殊模型渲染器
"type": "examplemod:example_special",

// 由 `ExampleSpecialRenderer.Unbaked` 定义的属性
// 渲染时使用的材质
// 指向 'assets/examplemod/textures/entity/example/example_texture.png'
"texture": "examplemod:example/example_texture"
}
}
}

动态流体容器(Dynamic Fluid Container)

NeoForge 增加了一种物品模型,可以构建动态流体容器(dynamic fluid container),能够在运行时根据内部所含流体动态更换贴图。

note

要使流体色调(fluid tint)应用到流体贴图上,相关物品必须附加有 Capabilities.FluidHandler.ITEM。如果你的物品不是直接使用 BucketItem(也不是其子类),那么你需要为你的物品注册该能力

// 对于某个物品 'examplemod:example_item'
// 位于 'assets/examplemod/items/example_item.json' 的 JSON
{
"model": {
"type": "neoforge:fluid_container",

// 用于构建容器的贴图
// 这些贴图是基于方块图集(block atlas)的,因此它们的路径是相对于 `textures` 目录的
"textures": {
// 设置模型的粒子贴图(particle sprite)
// 如果未设置,则使用第一个非空贴图,顺序如下:
// - 流体的静止贴图
// - 容器的基础贴图
// - 容器的覆盖贴图(如果未作为遮罩使用)
// 指向 'assets/minecraft/textures/item/bucket.png'
"particle": "minecraft:item/bucket",
// 设置第一层使用的贴图,通常用于流体的容器本体
// 如果未设置,则不会添加该层
// 指向 'assets/minecraft/textures/item/bucket.png'
"base": "minecraft:item/bucket",
// 设置作为静止流体贴图遮罩(mask)使用的贴图
// 显示流体的区域应为纯白色
// 如果未设置,或流体为空,则不会渲染该层
// 指向 'assets/neoforge/textures/item/mask/bucket_fluid.png'
"fluid": "neoforge:item/mask/bucket_fluid",
// 设置以下用途的贴图:
// - 当 'cover_is_mask' 为 false 时,作为覆盖层贴图
// - 当 'cover_is_mask' 为 true 时,作为应用于基础贴图的遮罩(纯白色区域可见)
// 如果未设置,或当 'cover_is_mask' 为 true 且未设置基础贴图,则不会渲染该层
// 指向 'assets/neoforge/textures/item/mask/bucket_fluid_cover.png'
"cover": "neoforge:item/mask/bucket_fluid_cover",
},

// 若为 true,则对于密度小于等于零的流体,将模型旋转 180 度
// 默认为 false
"flip_gas": true,
// 若为 true,则将覆盖贴图作为基础贴图的遮罩使用
// 默认为 true
"cover_is_mask": true,
// 若为 true,则对于光照等级大于零的流体,将流体贴图层的光照贴图设置为最大值
// 默认为 true
"apply_fluid_luminosity": false
}
}

手动渲染物品(Manually Rendering an Item)

如果你需要自行渲染一个物品(item),比如在某些 BlockEntityRendererEntityRenderer 中,可以通过三个步骤实现。首先,相关的渲染器会创建一个 ItemStackRenderState 来保存物品堆栈的渲染信息。接着,ItemModelResolver 会通过其方法之一更新 ItemStackRenderState,将其状态切换为当前需要渲染的物品。最后,使用渲染状态的 render 方法来完成物品的渲染。

ItemStackRenderState 负责追踪渲染所需的数据。每一个“模型(model)”都会拥有自己的 ItemStackRenderState.LayerRenderState,其中包含了要渲染的 BakedModel,渲染类型、是否有闪烁特效(foil status)、染色信息(tint information)以及任何特殊的渲染器。你可以通过 newLayer 方法创建新的图层(layer),并通过 clear 方法在渲染前清空图层。如果你使用的是预定义数量的图层,可以调用 ensureCapacity 方法,确保有足够数量的 LayerRenderStates 以正确渲染。ItemStackRenderState 还包含了一些方法,用于获取第一层(layer)的 BakedModel 属性,除了 pickParticleIcon,它会为某个随机图层获取粒子贴图(particle texture)。

ItemModelResolver 负责更新 ItemStackRenderState。这可以通过 updateForLiving(用于被生物实体持有的物品)、updateForNonLiving(用于被其他类型实体持有的物品)以及 updateForTopItem(用于所有其他情况)来实现。这些方法会接收渲染状态、要渲染的物品堆栈以及当前的显示上下文(display context)。其他参数则用于更新关于持有手、世界(level)、实体以及种子值(seeded value)等信息。每个方法都会在调用 update 之前,先调用 ItemStackRenderState#clear,而 update 方法则来自于通过 DataComponents#ITEM_MODEL 获取到的 ItemModel。如果你不在某个渲染器上下文(比如 BlockEntityRendererEntityRenderer)中,总是可以通过 Minecraft#getItemModelResolver 获取到 ItemModelResolver

自定义物品模型定义(Custom Item Model Defintions)

创建你自己的 ItemModel 通常分为三部分:用于更新渲染状态的 ItemModel 实例、用于读写 JSON 的 ItemModel.Unbaked 实例,以及注册以便使用该 ItemModel

warning

请务必确认你需要的物品模型无法通过上面已有的系统创建。在大多数情况下,并不需要自定义 ItemModel

首先是 ItemModel。它负责更新 ItemStackRenderState,以保证物品被正确渲染。它应当接收在渲染过程中使用的静态数据(例如 BakedModel 实例、属性信息等)。唯一的方法是 update,它会接收渲染状态、物品堆栈、模型解析器(model resolver)、显示上下文、世界(level)、持有实体和一些种子值(seeded value),用于更新 ItemStackRenderState。在这些参数中,只有 ItemStackRenderState 应被修改,其余参数都应作为只读数据处理。

public record RenderTypeModelWrapper(BakedModel model, RenderType type) implements ItemModel {

// 更新渲染状态
@Override
public void update(ItemStackRenderState state, ItemStack stack, ItemModelResolver resolver, ItemDisplayContext displayContext, @Nullable ClientLevel level, @Nullable LivingEntity entity, int seed) {
// 创建一个新的渲染层用于渲染模型
ItemStackRenderState.LayerRenderState layerState = state.newLayer();
if (stack.hasFoil()) {
layerState.setFoilType(ItemStackRenderState.FoilType.STANDARD);
}
layerState.setupBlockModel(this.model, this.type);
}
}

接下来是 ItemModel.Unbaked 实例。它应包含可以从文件中读取的数据,用于确定传递给物品模型(item model)的内容。该实例还包含两个方法:bake 用于构造 ItemModel 实例;type 定义了用于文件编码/解码的 MapCodec

public record RenderTypeModelWrapper(BakedModel model, RenderType type) implements ItemModel {

// ...

public record Unbaked(ResourceLocation model, RenderType type) implements ItemModel.Unbaked {
// 为 codec 创建渲染类型映射
private static final BiMap<String, RenderType> RENDER_TYPES = Util.make(HashBiMap.create(), map -> {
map.put("translucent_item", Sheets.translucentItemSheet());
map.put("cutout_block", Sheets.cutoutBlockSheet());
});
private static final Codec<RenderType> RENDER_TYPE_CODEC = ExtraCodecs.idResolverCodec(Codec.STRING, RENDER_TYPES::get, RENDER_TYPES.inverse()::get);

// 要注册的 map codec
public static final MapCodec<RenderTypeModelWrapper.Unbaked> MAP_CODEC = RecordCodecBuilder.mapCodec(instance ->
instance.group(
ResourceLocation.CODEC.fieldOf("model").forGetter(RenderTypeModelWrapper.Unbaked::model),
RENDER_TYPE_CODEC.fieldOf("render_type").forGetter(RenderTypeModelWrapper.Unbaked::type)
)
.apply(instance, RenderTypeModelWrapper.Unbaked::new)
);

@Override
public void resolveDependencies(ResolvableModel.Resolver resolver) {
// 解析模型依赖,因此传入所有已知的资源位置
resolver.resolve(this.model);
}

@Override
public ItemModel bake(ItemModel.BakingContext context) {
// 获取已烘焙的模型并返回
BakedModel baked = context.bake(this.model);
return new RenderTypeModelWrapper(baked, this.type);
}

@Override
public MapCodec<RenderTypeModelWrapper.Unbaked> type() {
return MAP_CODEC;
}
}
}

然后,我们通过 mod 事件总线 上的 RegisterItemModelsEvent 注册 map codec。

// 在某个客户端类中,将事件注册到 mod 事件总线(mod event bus)
@SubscribeEvent
public static void registerItemModels(RegisterItemModelsEvent event) {
event.register(
// 作为类型引用的名称
ResourceLocation.fromNamespaceAndPath("examplemod", "render_type"),
// Map 编解码器(map codec)
RenderTypeModelWrapper.Unbaked.MAP_CODEC
)
}

最后,我们可以在 JSON 文件中或数据生成(datagen)流程中使用这个 ItemModel

// 对于某个物品 'examplemod:example_item'
// JSON 文件位于 'assets/examplemod/items/example_item.json'
{
"model": {
"type": "examplemod:render_type",
// 指向 'assets/examplemod/models/item/example_item.json'
"model": "examplemod:item/example_item",
// 设置渲染时使用的渲染类型
"render_type": "cutout_block"
}
}

资源包 已烘焙模型 方块实体渲染器 能力系统 复合模型 手动渲染物品 mod 事件总线 资源定位符