======= Color Providers ======= Ever wonder how grass and leaves change hues depending on the biome, or how leather armor can have seemingly infinite color patterns? Meet **color providers**, which allow you to hue and tint block & item model textures based on properties such as location, NBT, or block states. ===== Vanilla Examples ===== First, what existing vanilla content uses color providers? A few examples include: * grass * leaves * leather armor dying * redstone wire * plants such as melons, sugarcane, and lily pads * tipped arrows The color provider is powerful, but Mojang has opted to stick with individual textures for colored blocks such as concrete, wool, and glass. The primary use case at this point is for biome shaded blocks and small tweaks to existing textures, such as the colored end of a tipped arrow. The concept behind color providers is simple. You register a block or item to them, and when the block or item's model is rendered, the color provider applies a hue tweak to each layer of the texture. Both providers give you access to the layer of the model, which means you can hue each portion of a model separately, which is the case in leather armor & tipped arrows. This is useful for when you only want to change a few pixels, but not the entire texture. Remember that the color provider is a client-side mechanic. Make sure to put any code related to it inside a client initializer. ===== Block Color Provider ===== To register a block to the block color provider, you'll need to use Fabric's ''ColorProviderRegistry''. There is an instance of the ''BLOCK'' and ''ITEM'' provider inside this class, which you can call ''register'' on. The ''register'' method takes an instance of your color provider and a varargs of every block you want to color with the provider. At first, we create the block in ''TutorialBlocks'' class. For how to create the block, see [[blocks]]: public final class TutorialBlocks { [...] public static final Block COLOR_BLOCK = register("color_block", new Block(AbstractBlock.Settings.create())); } Then add a simple block states file: { "variants": { "": { "model": "tutorial:block/color_block" } } } In your ''ClientModInitializer'': @Environment(EnvType.CLIENT) public class ExampleModClient implements ClientModInitializer { @Override public void onInitializeClient() { // ... ColorProviderRegistry.BLOCK.register((state, view, pos, tintIndex) -> 0x3495eb, TutorialBlocks.COLOR_BLOCK); } } If you haven't do so, remember to register it in your [[documentation:fabric_mod_json|fabric.mod.json]]: { // ... "entrypoints": { // ... "client": [ "net.fabricmc.example.ExampleModClient" ] }, // ... } Then we create the block model with //tintindex//. The model is also important: the main note here is that you are //required// to define a tintindex for each portion of the model you want to hue. To see an example of this, check out ''leaves.json'', which is the base model used for vanilla leaves. Here's the model used for our block: { "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" } } } ] } In this instance, we're adding a single tintindex, which is what would appear in the ''tintIndex'' parameter (tint index 0). Actually, we can directly inherit the ''minecraft:block/leaves'' model because it also uses a cube with tintindex. So you can also replace the model above, with: { "parent": "block/leaves", "textures": { "all": "block/white_concrete" } } > **Note:** the color of the block is cached. If you privded a color that changes among time, the changes will not take effect immediately unless there are block updates nearby. Here's the final result-- note that the original model used the ''white_concrete'' texture: {{https://i.imgur.com/fZLS10g.png}} ===== Block Entity with Color Provider ===== If you need to access ''BlockEntity'' data in the color provider, you'll want to override ''getRenderData()'' method from the ''RenderDataBlockEntity'', which is an interface of Fabric API but [[interface_injection|injected]] to ''BlockEntity''. If you're using old versions, try implementing ''RenderAttachmentBlockEntity'' and returning the data you need. This is because blocks can be rendered on separate threads, so accessing the data directly is not safe. Additionally, if you query blocks with ''getBlockState'' you won't be able to view the entire world - make sure you only query within ±2 blocks x/y/z of the current position. In this case, we create a ''ColorBlock'' class and a ''ColorBlockEntity'' class, and connect the block with block entity (more information see [[blockentity]]). public class ColorBlock extends BlockWithEntity { public ColorBlock(Settings settings) { super(settings); } @Override protected MapCodec 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; } } public class ColorBlockEntity extends BlockEntity { public int color = 0x3495eb; public ColorBlockEntity(BlockPos pos, BlockState state) { super(TutorialBlockEntityTypes.COLOR_BLOCK, pos, state); } // The following two methods specify serialization of color data. @Override protected void readNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) { super.readNbt(nbt, registryLookup); color = nbt.getInt("color"); // When the data is modified through "/data" command, // or placed by an item with "block_entity_data" component, // the render color will be updated. 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() { // this is the method from `RenderDataBlockEntity` class. return color; } } In the ''TutorialBlocks'' class, replace ''new Block'' with ''new ColorBlock'': public static final ColorBlock COLOR_BLOCK = register("color_block", new ColorBlock(AbstractBlock.Settings.create())); In the ''TutorialBlockEntityTypes'' class: public static final BlockEntityType COLOR_BLOCK = register("color_block", BlockEntityType.Builder.create(ColorBlockEntity::new, TutorialBlocks.COLOR_BLOCK).build()); Now we modify ''onUseWithItem'' method so that the color changes when you interact the block with a dye: 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); } [...] } Finally, modify the color provider to use the render data. We call ''FabricBlockView.getBlockEntityRenderData'' to ensure thread-safety and data-consistency. @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); } } Now done! Then you can check whether the following work correctly: * When you interact the block with a dye, the color should change. * When you modify the color through ''/data'' command, the color should change. * When you pick (press mouse wheel) the block with ''Ctrl'' pressed, and place the block, it should display as the expected color. * When you leave the world and re-enter, the color should be kept. ===== Item Color Provider ===== Items are similar; the difference is the context provided. Instead of having a state, world, or position, you have access to the ''ItemStack''. For item models, we can directly inherite the block model that uses 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); } } This would hue the item in our inventory in the same fashion as the block. ===== Limitations ===== One key issue with using the color provider is the lack of context in the item provider. This is why vanilla grass doesn't change colors in your inventory depending on where you stand. For implementing things such as color variants of blocks (concrete, glass, wool, etc.), you're encouraged to simply provide an individual texture for each version.