====== 颜色提供器 ======
有没有想过,草和树叶如何根据生物群系而改变色调的?皮革盔甲如何具有看似无限的颜色模式?答案是**颜色提供器**,这个可以允许你根据位置、NBT、方块状态等属性为方块或物品的模型纹理设置色调或者着色。
===== 原版例子 =====
首先,现有的哪些原版内容使用颜色提供器?一些示例包括:
*草
*树叶
*皮革盔甲着色
*红石线
*西瓜、甘蔗和睡莲等植物
*药箭
颜色提供器功能强大,但是 Mojang 选择对混凝土、羊毛和玻璃等有色方块坚持使用单独的纹理。此时的主要用例是针对受生物群系的方块,以及对现有纹理的细微调整,例如药箭的彩色末端。
颜色提供器背后的概念很简单。你为之注册方块和物品,并在渲染该方块或物品的模型时,颜色提供器对纹理的每一层应用色调调整。两个提供器都可以访问模型的层,这意味着您可以分别对模型的每个部分进行色调设置,皮革盔甲和药箭就是这种情况。当您只想更改几个像素而不是整个纹理时,这很有用。
请记住,颜色提供器是客户端机制。确保将与其相关的所有代码放入客户端初始化器中。
===== 方块颜色提供器 =====
要将方块注册到方块颜色提供器,您需要使用 Fabric 的 ''ColorProviderRegistry''。此类中有一个 ''BLOCK'' 和 ''ITEM'' 提供器的实例,可以其调用 ''register''。''register'' 方法接受颜色提供器的一个实例,以及想要使用该提供器进行着色的每个方块的变量。
首先,先在 ''TutoriaoBlocks'' 类创建方块,关于如何创建方块,见 [[blocks]]:
public final class TutorialBlocks {
[...]
// 1.21.2 之前:
public static final Block COLOR_BLOCK = register("color_block", new Block(AbstractBlock.Settings.create()));
// 1.21.2 及之后:
public static final Block COLOR_BLOCK = register("color_block", Block::new, AbstractBlock.Settings.create());
}
简单添加个方块状态文件:
{
"variants": {
"": {
"model": "tutorial:block/color_block"
}
}
}
在你的 ''ClientModInitializer'':
@Environment(EnvType.CLIENT)
public class ExampleModClient implements ClientModInitializer {
@Override
public void onInitializeClient() {
// ...
ColorProviderRegistry.BLOCK.register((state, view, pos, tintIndex) -> 0x3495eb, TutorialBlocks.COLOR_BLOCK);
}
}
如果还没有的话,记得在你的 [[documentation:fabric_mod_json|fabric.mod.json]] 中注册:
{
// ...
"entrypoints": {
// ...
"client": [
"net.fabricmc.example.ExampleModClient"
]
},
// ...
}
然后创建带有 //tintindex// 的模型:模型也很重要,这里需要注意的是,你//一定要//为模型的每一个你需要着色的部分定义tintindex。如要查看这个的例子,请参考 ''leaves.json'',这是原版树叶使用的基本模型。这里是我们方块使用的模型:
{
"parent": "block/block",
"textures": {
"all": "block/white_concrete",
"particle": "#all"
},
"elements": [
{ "from": [ 0, 0, 0 ],
"to": [ 16, 16, 16 ],
"faces": {
"down": { "uv": [ 0, 0, 16, 16 ], "texture": "#all", "tintindex": 0, "cullface": "down" },
"up": { "uv": [ 0, 0, 16, 16 ], "texture": "#all", "tintindex": 0, "cullface": "up" },
"north": { "uv": [ 0, 0, 16, 16 ], "texture": "#all", "tintindex": 0, "cullface": "north" },
"south": { "uv": [ 0, 0, 16, 16 ], "texture": "#all", "tintindex": 0, "cullface": "south" },
"west": { "uv": [ 0, 0, 16, 16 ], "texture": "#all", "tintindex": 0, "cullface": "west" },
"east": { "uv": [ 0, 0, 16, 16 ], "texture": "#all", "tintindex": 0, "cullface": "east" }
}
}
]
}
在这个实例里面,我们添加了单一的 tintindex,出现在 ''tintIndex'' 参数中(着色索引 0)。事实上,方块可以直接继承 ''minecraft:block/leaves'' 模型,因为也使用的带有 tintindex 的方块。所以可以将上面的模型替换为:
{
"parent": "block/leaves",
"textures": {
"all": "block/white_concrete"
}
}
> **注意:**方块颜色是有缓存的,如果提供了随时间改变的颜色,这个改变不会直接生效,除非附近有方块更新。
这是最终结果——请注意,原始模型使用了 ''white_concrete''(白色混凝土)纹理(下图使用了 imgur 图床):
{{https://i.imgur.com/fZLS10g.png}}
===== 带有颜色提供器的方块实体 =====
如果你需要在颜色提供器中访问 ''BlockEntity'' 数据,你需要实现来自 ''RenderDataBlockEntity'' 的 ''getRenderData()'' 方法,这是 Fabric API 的接口,不过[[interface_injection|注入]]到的 ''BlockEntity'' 中。如果使用的是旧版本,尝试实现 ''RenderAttachmentBlockEntity'' 并返回你需要的数据。
这是因为方块可以在单独的线程渲染,所以直接访问数据并不安全。而且,如果使用 ''getBlockState'' 查询方块,你无法查看整个世界——确保你只查询当前位置的 ±2 方块范围内的位置。
在这个例子中,我们创建一个 ''ColorBlock'' 类和 ''ColorBlockEntity'' 类,并连接方块与方块实体(更多信息见[[blockentity|方块实体]]教程)。
public class ColorBlock extends BlockWithEntity {
public ColorBlock(Settings settings) {
super(settings);
}
@Override
protected MapCodec extends ColorBlock> getCodec() {
return createCodec(ColorBlock::new);
}
@Nullable
@Override
public BlockEntity createBlockEntity(BlockPos pos, BlockState state) {
return new ColorBlockEntity(pos, state);
}
// 自从 1.21.4 开始,不再需要这个方法,因为所有方块实体都默认使用方块模型。
@Override
protected BlockRenderType getRenderType(BlockState state) {
return BlockRenderType.MODEL;
}
}
public class ColorBlockEntity extends BlockEntity {
public int color = 0x3495eb;
public ColorBlockEntity(BlockPos pos, BlockState state) {
super(TutorialBlockEntityTypes.COLOR_BLOCK, pos, state);
}
// 以下两个方块指定了颜色数据的序列化。
@Override
protected void readNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) {
super.readNbt(nbt, registryLookup);
// 对于 1.21.5 之前的版本,请直接使用 nbt.getInt("color");
color = nbt.getInt("color", 0);
// 当数据通过“/data”命令修改,或者放置了有“block_entity_data”组件的物品时,
// 需要同步更新。
if (world != null) {
world.updateListeners(pos, getCachedState(), getCachedState(), 0);
}
}
@Override
protected void writeNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) {
super.writeNbt(nbt, registryLookup);
nbt.putInt("color", color);
}
@Nullable
@Override
public Packet toUpdatePacket() {
return BlockEntityUpdateS2CPacket.create(this);
}
@Override
public NbtCompound toInitialChunkDataNbt(RegistryWrapper.WrapperLookup registryLookup) {
return createNbt(registryLookup);
}
@Override
public @Nullable Object getRenderData() {
// 这是来自 `RenderDataBlockEntity` 类的方法。
return color;
}
}
在 ''TutorialBlocks'' 类中,将 ''new Block'' 替换为 ''new ColorBlock'':
// 1.21.2 之前:
public static final ColorBlock COLOR_BLOCK = register("color_block", new ColorBlock(AbstractBlock.Settings.create()));
// 1.21.2 及之后:
public static final Block COLOR_BLOCK = register("color_block", ColorBlock::new, AbstractBlock.Settings.create());
在 ''TutorialBlockEntityTypes'' 类中:
public static final BlockEntityType COLOR_BLOCK = register("color_block",
BlockEntityType.Builder.create(ColorBlockEntity::new, TutorialBlocks.COLOR_BLOCK).build());
> 不要忘了在你的模组初始化器中,静态加载 ''TutorialBlocks'' 和 ''TutorialBlockEntityTypes'' 类。
现在我们修改 ''onUseWithItem'' 方法,这样颜色会在我们使用染料与方块接触时改变:
public class ColorBlock extends BlockWithEntity {
[...]
@Override
protected ActionResult onUseWithItem(ItemStack stack, BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockHitResult hit) {
if (stack.getItem() instanceof DyeItem dyeItem) {
if (world.getBlockEntity(pos) instanceof ColorBlockEntity colorBlockEntity) {
final int newColor = dyeItem.getColor().getEntityColor();
final int originalColor = colorBlockEntity.color;
colorBlockEntity.color = ColorHelper.average(newColor, originalColor);
stack.decrementUnlessCreative(1, player);
colorBlockEntity.markDirty();
world.updateListeners(pos, state, state, 0);
}
}
return super.onUseWithItem(stack, state, world, pos, player, hand, hit);
}
[...]
}
最后,修改颜色提供器以使用渲染数据。我们调用 ''FabricBlockView.getBlockEntityRenderData'' 以确保线程安全和数据连贯。
@Environment(EnvType.CLIENT)
public class ExampleModClient implements ClientModInitializer {
[...]
@Override
public void onInitializeClient() {
[...]
ColorProviderRegistry.BLOCK.register((state, view, pos, tintIndex) -> view != null && view.getBlockEntityRenderData(pos) instanceof Integer integer ? integer : 0x3495eb, TutorialBlocks.COLOR_BLOCK);
}
}
搞定!现在你可以检查下面这些是否都正常起作用:
* 使用染料交互,颜色应该改变。
* 通过 ''/data'' 命令修改颜色时,颜色应该改变。
* 按下 ''Ctrl'' 同时拾取(按下鼠标中键)并放置方块,应该显示为预期的颜色。
* 离开世界重进,颜色应该保留。
===== 自定义物品着色(1.21.4 及之后) =====
从 1.21.4 开始,物品的着色是由物品模型映射指定的。原版提供了几种常见的着色来源的类型,参见 [[https://zh.minecraft.wiki/w/物品模型映射|Minecraft Wiki]]。在这个例子中,我们需要直接指定物品的颜色,因此可以将物品模型映射像这样写:
{
"model": {
"type": "model",
"model": "tutorial:block/color_block",
"tints": [
{
"type": "constant",
"value": 3446251
}
]
}
}
如果需要指定自定义的着色来源,可以使用原版提供的 ''TintResourceTypes.//ID_MAPPER//.put(...)'' 进行注册。注意是在客户端环境下完成的。注册时使用的 ID 将是上面的物品模型映射中的 ''%%"tints"%%'' 中的 ''%%"type"%%'' 的值。
> 如果着色不生效,检查下使用的模型中的 tintindex 的值,应该与物品模型映射中的 ''%%"tints"%%'' 列表中的元素下标对应。例如,如果 tintindex 为 2,表示将使用物品模型映射中的第三个着色来源。
===== 物品颜色提供器(1.21.4 之前) =====
在 1.21.3 以及之前的版本,物品的颜色提供器也是可通过 Fabric API 注册的。与方块不同,物品的颜色提供器提供的上下文不访问状态、世界和位置,而是访问 ''ItemStack''。
物品模型可以直接继承使用 tintindex 的方块模型:
首先,物品需要一个直接继承方块模型的物品模型:
{
"parent": "tutorial:block/color_block"
}
然后,再在客户端环境中,注册颜色提供器。
@Environment(EnvType.CLIENT)
public class ExampleModClient implements ClientModInitializer {
@Override
public void onInitializeClient() {
// ...
ColorProviderRegistry.ITEM.register((stack, tintIndex) -> 0x3495eb, TutorialBlocks.COLOR_BLOCK);
}
}
这会以像方块那样的方法为你物品栏中的物品提供色相。
===== 限制 =====
使用颜色提供器的一个关键问题是物品的提供器中缺少上下文。这就是为什么原版草不会根据您站立的位置改变物品栏中的颜色的原因。为了实现诸如方块的颜色变体(混凝土、玻璃、羊毛等)之类的东西,建议您为每个版本简单地提供单独的纹理。