本篇教程的视频

本篇教程的源代码

GitHub地址:TutorialMod-2D3D-Mixin-1.21

介绍

本期教程是Mixin的第二个实例,我们将仿写类似于三叉戟或者望远镜的物品,这类物品它在物品栏的GUI中显示的是二维的,但当我们拿在手上的时候是三维

这里的三维模型,并不是指之前那种简单的加厚一层的三维模型,而是真正的三维模型,这里我们也要用到三维的建模软件,比如说BlockBench

BlockBench官网:BlockBench

BlockBench可以用来制作物品、方块、实体等等的模型,但要注意的是,Java版的模型和Bedrock(基岩)版的模型是不一样的,Java的限制得注意,比如不能超过3×3×3大小;只支持单轴旋转,并以22.5°为一个旋转单位

关于BlockBench的使用,这里就不多说了,可以自行搜索教程。本篇教程请先自行简单做一个模型,也可以到Github的源代码中下载

另外,在BlockBench的显示模式中,记得调整一下,包括GUI的模式、第一人称左右手、第三人称左右手等等

编写Mixin

查看源代码

这里我们先来看SPYGLASS(望远镜)这个物品的源代码

1
public static final Item SPYGLASS = register("spyglass", new SpyglassItem(new Item.Settings().maxCount(1)));

这个是注册的,我们可以看到这个物品是SpyglassItem类的实例,另外给它设置了最大堆叠数量为1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class SpyglassItem extends Item {
public static final int MAX_USE_TIME = 1200;
public static final float FOV_MULTIPLIER = 0.1F;

public SpyglassItem(Item.Settings settings) {
super(settings);
}

@Override
public int getMaxUseTime(ItemStack stack, LivingEntity user) {
return 1200;
}

@Override
public UseAction getUseAction(ItemStack stack) {
return UseAction.SPYGLASS;
}

@Override
public TypedActionResult<ItemStack> use(World world, PlayerEntity user, Hand hand) {
user.playSound(SoundEvents.ITEM_SPYGLASS_USE, 1.0F, 1.0F);
user.incrementStat(Stats.USED.getOrCreateStat(this));
return ItemUsage.consumeHeldItem(world, user, hand);
}

@Override
public ItemStack finishUsing(ItemStack stack, World world, LivingEntity user) {
this.playStopUsingSound(user);
return stack;
}

@Override
public void onStoppedUsing(ItemStack stack, World world, LivingEntity user, int remainingUseTicks) {
this.playStopUsingSound(user);
}

private void playStopUsingSound(LivingEntity user) {
user.playSound(SoundEvents.ITEM_SPYGLASS_STOP_USING, 1.0F, 1.0F);
}
}

但是我们去这个类中查找的时候,并没有看到什么渲染的方法,只有一些使用的方法,还有一些音效的播放

所以现在,我们得换个地方找。这里我们点击Items中的SPYGLASS进行跳转,在跳出来的预选栏中,我们可以看到用到了SPYGLASS的地方

在这一堆类中,我们只要找有关渲染(Render)的类即可。这里我们可以看到PlayerHeldItemFeatureRendererItemRenderer这两个渲染类用到了SPYGLASS

这里我们要看的是ItemRenderer的东西,前者是用于渲染玩家使用望远镜的时候,把望远镜怼到脸上的这个动作的

1
2
3
4
5
6
7
8
9
10
11
12
...
if (!stack.isEmpty()) {
matrices.push();
boolean bl = renderMode == ModelTransformationMode.GUI || renderMode == ModelTransformationMode.GROUND || renderMode == ModelTransformationMode.FIXED;
if (bl) {
if (stack.isOf(Items.TRIDENT)) {
model = this.models.getModelManager().getModel(TRIDENT);
} else if (stack.isOf(Items.SPYGLASS)) {
model = this.models.getModelManager().getModel(SPYGLASS);
}
}
...

这里的代码是在renderItem方法中,我们可以看到,当renderModeGUIGROUND或者FIXED的时候,会调用this.models.getModelManager().getModel(SPYGLASS)来获取模型

这里传的SPYGLASS是望远镜的二维贴图的路径

也就是说,在GUI(包括物品栏等等)、GROUND(掉落物形式)或者FIXED(?这个我还真不清楚)的时候,会调用SPYGLASS的二维贴图(这个看游戏里面)

另外,这里的TRIDENT是三叉戟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public BakedModel getModel(ItemStack stack, @Nullable World world, @Nullable LivingEntity entity, int seed) {
BakedModel bakedModel;
if (stack.isOf(Items.TRIDENT)) {
bakedModel = this.models.getModelManager().getModel(TRIDENT_IN_HAND);
} else if (stack.isOf(Items.SPYGLASS)) {
bakedModel = this.models.getModelManager().getModel(SPYGLASS_IN_HAND);
}
...
}

...

BakedModel bakedModel = this.getModel(item, world, entity, seed);
this.renderItem(item, renderMode, leftHanded, matrices, vertexConsumers, light, overlay, bakedModel);

...

其实这个类里面还有一个getModel方法也用到了SPYGLASS,但这个并不用看,顺着这些方法看一下,我们就可以看到这玩意是给renderItem方法传递BakedModel

当然,这里它传的是SPYGLASS_IN_HAND,这个是望远镜的三维模型路径

结合上面的那一堆方法,我们可以理解为,当我们在物品栏中看到的是SPYGLASS的二维贴图,但当我们拿在手上的时候,是SPYGLASS_IN_HAND的三维模型

最后,显然易见,不论是望远镜还是三叉戟,这些东西都是硬编码的,所以我们要仿写的时候,得用Mixin

设计Mixin

首先我们来看看如何进行Mixin的编写,我们再看看源代码

1
2
3
4
5
6
7
8
boolean bl = renderMode == ModelTransformationMode.GUI || renderMode == ModelTransformationMode.GROUND || renderMode == ModelTransformationMode.FIXED;
if (bl) {
...
} else if (stack.isOf(Items.SPYGLASS)) {
model = this.models.getModelManager().getModel(SPYGLASS);
}
}
...

这里的bl判断的是渲染模式

而后,对model进行了赋值,类型是BakedModel

所以我们要使用Mixin改变这里的model值,即当stack是我们的物品时,我们将model赋值为我们的物品模型

再看赋值的语句,这里的this.models是这个类中的一个私有变量,类型是ItemModels

所以我们还得先访问这个变量,才能进行下面的操作

创建ItemRenderAccessor接口

这里我们先创建一个接口,用于访问ItemRenderer中的ItemModels变量

1
2
3
4
5
6
@Mixin(ItemRenderer.class)
public interface ItemRenderAccessor {
// 使用@Accessor注解获取ItemRenderer的models字段
@Accessor("models")
ItemModels getModels();
}

这里我们使用@Accessor注解来获取ItemRenderermodels字段,这个注解在前面也讲到过,这里不再赘述

创建ItemRendererMixin类

现在,我们来创建用于修改这个变量的类

1
2
3
4
@Mixin(ItemRenderer.class)
public class ItemRendererMixin {

}

那么这里,我们是修改其中的一个变量,所以我们使用@ModifyVariable注解

1
2
3
4
5
6
7
8
9
10
11
12
@ModifyVariable(method = "renderItem(Lnet/minecraft/item/ItemStack;Lnet/minecraft/client/render/model/json/ModelTransformationMode;ZLnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;IILnet/minecraft/client/render/model/BakedModel;)V", 
at = @At("HEAD"), argsOnly = true)
public BakedModel usePlateModel(BakedModel model, ItemStack stack,
ModelTransformationMode renderMode,
boolean leftHanded,
MatrixStack matrices,
VertexConsumerProvider vertexConsumers,
int light,
int overlay
) {

}

这里的methodrenderItem方法,因为它有一堆参数,所以后面的引用相当长

atHEAD,表示在方法的头部进行修改,原本的这个renderItem方法是没有返回值的,这个位置随意,也可以自己测试一下

argsOnlytrue,表示只有参数才会被修改

下面是我们创建的方法,因为修改的是其中的model变量,而它的变量类型是BakedModel,所以我们的方法返回值的类型是BakedModel

并且要注意,不论方法的形参有多少、是什么,BakedModel model这个形参必须是所有形参的第一个,否则会报错(即和返回值有关的形参放第一个)

后面的参数其实是renderItem方法的参数,这里我是直接搬过来的,用不到的参数其实可以不写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@ModifyVariable(method = "renderItem(Lnet/minecraft/item/ItemStack;Lnet/minecraft/client/render/model/json/ModelTransformationMode;ZLnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;IILnet/minecraft/client/render/model/BakedModel;)V", at = @At("HEAD"), argsOnly = true)
public BakedModel usePlateModel(BakedModel model, ItemStack stack,
ModelTransformationMode renderMode,
boolean leftHanded,
MatrixStack matrices,
VertexConsumerProvider vertexConsumers,
int light,
int overlay
) {
boolean bl = renderMode != ModelTransformationMode.GUI;
if (bl) {
if (stack.isOf(ModItems.PLATE)) {
return ((ItemRendererAccessor) this).getModels().getModelManager().getModel(
ModelIdentifier.ofInventoryVariant(Identifier.of(TutorialMod.MOD_ID, "plate_3d")));
}
}
return model;
}

这里我们同样判断了renderMode,但这里是!=,这里我们想实现的是,物品仅在GUI模式下显示二维贴图,其他模式下均显示三维模型

然后指定我们的模型文件的路径,Identifier记得改

当然,如果不是我们的模型,就返回原本的model

不过,这里的ModItems.PLATE暂时会报错,因为我们还没有注册这个物品

物品注册

1
public static final Item PLATE = registerItems("plate", new Item(new Item.Settings()));

物品注册就使用最简单的方法,实例化一个Item即可,当然如果你有自己的物品类,则实例化对应的类即可

而后,假设说你已经做好了模型,放在了对应的位置,贴图也都搞好了。然而,当你进入游戏的时候,你还是会见到一个大大的黑紫块,这是为什么呢?

因为我们还有东西没写

PS:其实后面我自己再试了一遍,如果at的位置在HEAD,那么会变成黑紫块(包括物品栏和手持状态);但是at的位置在TAIL,则返回的是二维的贴图模型(包括物品栏和手持状态)(可以自己试试)

继续观察源代码

我们再来看一下ItemRenderer中的一些东西,比如说SPYGLASS_IN_HAND

1
public static final ModelIdentifier SPYGLASS_IN_HAND = ModelIdentifier.ofInventoryVariant(Identifier.ofVanilla("spyglass_in_hand"));

这个字段我们上面提到过,这个是望远镜的三维模型的路径,当我们点击它进行跳转的时候,我们可以看到另外一个类ModelLoader也调用了它

1
this.loadItemModel(ItemRenderer.SPYGLASS_IN_HAND);

看这个类的名字,我们就知道这个类是用来加载模型的,我们再看一下这个类的loadItemModel方法

这里的这个语句便是用来加载模型的

然后嘞,我们还是得通过Mixin来修改这个方法

创建ModelLoaderMixin类

1
2
3
4
@Mixin(ModelLoader.class)
public abstract class ModelLoaderMixin {

}

然后我们通过@Inject注解来注入我们的语句

1
2
3
4
5
6
@Shadow protected abstract void loadItemModel(ModelIdentifier id);

@Inject(method = "<init>", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/model/ModelLoader;loadItemModel(Lnet/minecraft/client/util/ModelIdentifier;)V", ordinal = 1, shift = At.Shift.AFTER))
public void addPlate(CallbackInfo ci) {
this.loadItemModel(ModelIdentifier.ofInventoryVariant(Identifier.of(TutorialMod.MOD_ID, "plate_3d")));
}

这里的<init>ModelLoader的构造方法,我们在这个方法中注入我们的语句

@At中的targetloadItemModel方法的调用

ordinal是第几次调用的索引(从0开始),这里1的话就是在我们上面那个语句执行完以后再执行我们的语句

shift是在这个调用之后进行注入

而后我们这里的方法里面要调用loadItemModel方法,所以得利用@Shadow注解来引入这个方法

在这个方法中,我们调用了loadItemModel方法,传入了我们的模型路径,这里传的是物品的三维模型

在此之后,假设其他东西都设置对了,我们进入游戏就不再是黑紫块了

其他文件

3D模型文件

这里我们要在resources文件夹下的assets/tutorialmod/models/item文件夹中放入我们的模型文件plate_3d.json,这个是BlockBench导出的,不是数据生成的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
{
"credit": "Made with Blockbench",
"texture_size": [32, 32],
"textures": {
"0": "tutorialmod:item/plate_3d",
"particle": "tutorialmod:item/plate_3d"
},
"elements": [
{
"from": [5, 0, 5],
"to": [11, 1, 11],
"faces": {
"north": {"uv": [4, 6, 7, 6.5], "texture": "#0"},
"east": {"uv": [6, 4, 9, 4.5], "texture": "#0"},
"south": {"uv": [6, 4.5, 9, 5], "texture": "#0"},
"west": {"uv": [4, 6.5, 7, 7], "texture": "#0"},
"up": {"uv": [3, 3, 0, 0], "texture": "#0"},
"down": {"uv": [3, 3, 0, 6], "texture": "#0"}
}
},
{
"from": [5, 1, 11],
"to": [11, 2, 13],
"faces": {
"north": {"uv": [0, 7, 3, 7.5], "texture": "#0"},
"east": {"uv": [7, 6, 8, 6.5], "texture": "#0"},
"south": {"uv": [4, 7, 7, 7.5], "texture": "#0"},
"west": {"uv": [7, 6.5, 8, 7], "texture": "#0"},
"up": {"uv": [7, 6, 4, 5], "texture": "#0"},
"down": {"uv": [3, 6, 0, 7], "texture": "#0"}
}
},
{
"from": [5, 1, 3],
"to": [11, 2, 5],
"faces": {
"north": {"uv": [7, 5, 10, 5.5], "texture": "#0"},
"east": {"uv": [7, 7, 8, 7.5], "texture": "#0"},
"south": {"uv": [7, 5.5, 10, 6], "texture": "#0"},
"west": {"uv": [0, 7.5, 1, 8], "texture": "#0"},
"up": {"uv": [9, 1, 6, 0], "texture": "#0"},
"down": {"uv": [9, 1, 6, 2], "texture": "#0"}
}
},
{
"from": [11, 1, 3],
"to": [13, 2, 13],
"faces": {
"north": {"uv": [1, 7.5, 2, 8], "texture": "#0"},
"east": {"uv": [6, 2, 11, 2.5], "texture": "#0"},
"south": {"uv": [2, 7.5, 3, 8], "texture": "#0"},
"west": {"uv": [6, 2.5, 11, 3], "texture": "#0"},
"up": {"uv": [4, 5, 3, 0], "texture": "#0"},
"down": {"uv": [5, 0, 4, 5], "texture": "#0"}
}
},
{
"from": [3, 1, 3],
"to": [5, 2, 13],
"faces": {
"north": {"uv": [4, 7.5, 5, 8], "texture": "#0"},
"east": {"uv": [6, 3, 11, 3.5], "texture": "#0"},
"south": {"uv": [5, 7.5, 6, 8], "texture": "#0"},
"west": {"uv": [6, 3.5, 11, 4], "texture": "#0"},
"up": {"uv": [6, 5, 5, 0], "texture": "#0"},
"down": {"uv": [4, 5, 3, 10], "texture": "#0"}
}
}
],
"display": {
"thirdperson_righthand": {
"rotation": [75, 45, 0],
"translation": [0, 2.5, 2.25],
"scale": [0.375, 0.375, 0.375]
},
"thirdperson_lefthand": {
"rotation": [75, 45, 0],
"translation": [0, 2.5, 2.75],
"scale": [0.375, 0.375, 0.375]
},
"firstperson_righthand": {
"rotation": [0, 45, 0],
"translation": [0, 4.25, 0],
"scale": [0.4, 0.4, 0.4]
},
"firstperson_lefthand": {
"rotation": [0, 225, 0],
"translation": [0, 3.75, 0],
"scale": [0.4, 0.4, 0.4]
},
"ground": {
"translation": [0, 3, 0],
"scale": [0.25, 0.25, 0.25]
},
"gui": {
"rotation": [30, 225, 0],
"scale": [0.625, 0.625, 0.625]
},
"head": {
"rotation": [0, 180, 0],
"translation": [0, 13, 7]
},
"fixed": {
"scale": [0.5, 0.5, 0.5]
}
}
}

如果你不想自己做,可以复制这里的

2D模型文件

我们可以使用数据生成来生成这个plate.json文件,也可以手写

1
2
3
4
5
6
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "tutorialmod:item/plate"
}
}

如果用数据生成,则是这样写

1
itemModelGenerator.register(ModItems.PLATE, Models.GENERATED);

语言数据生成

1
translationBuilder.add(ModItems.PLATE, "Plate");

贴图文件

这里因为我们对应的是两个模型,所以我们需要两个贴图文件,一个是plate.png,一个是plate_3d.png

后面那个也是BlockBench导出保存的

测试

在这些东西都设置完成以后,我们就可以进入游戏看看我们的物品了

如果都正确的话,那么我们的物品在物品栏中是二维的,但是拿在手上的时候是三维的,扔在地上的时候也是三维的