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.
First, what existing vanilla content uses color providers? A few examples include:
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.
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 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:
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 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<? 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; } }
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<ClientPlayPacketListener> 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<ColorBlockEntity> 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:
/data
command, the color should change.Ctrl
pressed, and place the block, it should display as the expected color.
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.
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.