Table of Contents

颜色提供器

有没有想过,草和树叶如何根据生物群系而改变色调的?皮革盔甲如何具有看似无限的颜色模式?答案是颜色提供器,这个可以允许你根据位置、NBT、方块状态等属性为方块或物品的模型纹理设置色调或者着色。

原版例子

首先,现有的哪些原版内容使用颜色提供器?一些示例包括:

颜色提供器功能强大,但是 Mojang 选择对混凝土、羊毛和玻璃等有色方块坚持使用单独的纹理。此时的主要用例是针对受生物群系的方块,以及对现有纹理的细微调整,例如药箭的彩色末端。

颜色提供器背后的概念很简单。你为之注册方块和物品,并在渲染该方块或物品的模型时,颜色提供器对纹理的每一层应用色调调整。两个提供器都可以访问模型的层,这意味着您可以分别对模型的每个部分进行色调设置,皮革盔甲和药箭就是这种情况。当您只想更改几个像素而不是整个纹理时,这很有用。

请记住,颜色提供器是客户端机制。确保将与其相关的所有代码放入客户端初始化器中。

方块颜色提供器

要将方块注册到方块颜色提供器,您需要使用 Fabric 的 ColorProviderRegistry。此类中有一个 BLOCKITEM 提供器的实例,可以其调用 registerregister 方法接受颜色提供器的一个实例,以及想要使用该提供器进行着色的每个方块的变量。

首先,先在 TutoriaoBlocks 类创建方块,关于如何创建方块,见 blocks

TutorialBlocks.class
public final class TutorialBlocks {
  [...]
 
  public static final Block COLOR_BLOCK = register("color_block", new Block(AbstractBlock.Settings.create()));
}

简单添加个方块状态文件:

src/main/resources/assets/tutorial/blockstates/color_block.json
{
  "variants": {
    "": {
      "model": "tutorial:block/color_block"
    }
  }
}

在你的 ClientModInitializer

ExampleModClient.java
@Environment(EnvType.CLIENT)
public class ExampleModClient implements ClientModInitializer {
  @Override
  public void onInitializeClient() {
    // ...
 
    ColorProviderRegistry.BLOCK.register((state, view, pos, tintIndex) -> 0x3495eb, TutorialBlocks.COLOR_BLOCK);
  }
}

如果还没有的话,记得在你的 fabric.mod.json 中注册:

{
  // ...
  "entrypoints": {
    // ...
    "client": [
      "net.fabricmc.example.ExampleModClient"
    ]
  },
  // ...
}

然后创建带有 tintindex 的模型:模型也很重要,这里需要注意的是,你一定要为模型的每一个你需要着色的部分定义tintindex。如要查看这个的例子,请参考 leaves.json,这是原版树叶使用的基本模型。这里是我们方块使用的模型:

src/main/resources/assets/tutorial/models/block/color_block.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 的方块。所以可以将上面的模型替换为:

src/main/resources/assets/tutorial/models/block/color_block.json
{
  "parent": "block/leaves",
  "textures": {
    "all": "block/white_concrete"
  }
}
注意:方块颜色是有缓存的,如果提供了随时间改变的颜色,这个改变不会直接生效,除非附近有方块更新。

这是最终结果——请注意,原始模型使用了 white_concrete(白色混凝土)纹理(下图使用了 imgur 图床):

带有颜色提供器的方块实体

如果你需要在颜色提供器中访问 BlockEntity 数据,你需要实现来自 RenderDataBlockEntitygetRenderData() 方法,这是 Fabric API 的接口,不过注入到的 BlockEntity 中。如果使用的是旧版本,尝试实现 RenderAttachmentBlockEntity 并返回你需要的数据。

这是因为方块可以在单独的线程渲染,所以直接访问数据并不安全。而且,如果使用 getBlockState 查询方块,你无法查看整个世界——确保你只查询当前位置的 ±2 方块范围内的位置。

在这个例子中,我们创建一个 ColorBlock 类和 ColorBlockEntity 类,并连接方块与方块实体(更多信息见方块实体教程)。

ColorBlock.java
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);
  }
 
  @Override
  protected BlockRenderType getRenderType(BlockState state) {
    return BlockRenderType.MODEL;
  }
}
ColorBlockEntity.java
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);
    color = nbt.getInt("color");
 
    // 当数据通过“/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<ClientPlayPacketListener> 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

  public static final ColorBlock COLOR_BLOCK = register("color_block", new ColorBlock(AbstractBlock.Settings.create()));

TutorialBlockEntityTypes 类中:

  public static final BlockEntityType<ColorBlockEntity> COLOR_BLOCK = register("color_block",
      BlockEntityType.Builder.create(ColorBlockEntity::new, TutorialBlocks.COLOR_BLOCK).build());

现在我们修改 onUseWithItem 方法,这样颜色会在我们使用染料与方块接触时改变:

ColorBlock.java
public class ColorBlock extends BlockWithEntity {
  [...]
 
  @Override
  protected ItemActionResult 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.Argb.averageArgb(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 以确保线程安全和数据连贯。

ExampleModClient.java
@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);
  }
}

搞定!现在你可以检查下面这些是否都正常起作用:

物品颜色提供器

物品是类似的,区别在于提供的上下文。不访问状态、世界和位置,而是访问 ItemStack

物品模型可以直接继承使用 tintindex 的方块模型:

ExampleModClient.java
@Environment(EnvType.CLIENT)
public class ExampleModClient implements ClientModInitializer {
  @Override
  public void onInitializeClient() {
    // ...
 
    ColorProviderRegistry.ITEM.register((stack, tintIndex) -> 0x3495eb, TutorialBlocks.COLOR_BLOCK);
  }
}

这会以像方块那样的方法为你物品栏中的物品提供色相。

限制

使用颜色提供器的一个关键问题是物品的提供器中缺少上下文。这就是为什么原版草不会根据您站立的位置改变物品栏中的颜色的原因。为了实现诸如方块的颜色变体(混凝土、玻璃、羊毛等)之类的东西,建议您为每个版本简单地提供单独的纹理。