User Tools

Site Tools


tutorial:colorprovider

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:

TutorialBlocks.class
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:

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

In your 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);
  }
}

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:

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" }
      }
    }
  ]
}

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:

src/main/resources/assets/tutorial/models/block/color_block.json
{
  "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:

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 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).

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);
  }
 
  // 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:

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);
  }
 
  [...]
}

Finally, modify the color provider to use the render data. We call FabricBlockView.getBlockEntityRenderData to ensure thread-safety and data-consistency.

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);
  }
}

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:

src/main/resources/assets/tutorial/models/item/color_block.json
{
  "parent": "tutorial:block/color_block"
}
ExampleModClient.java
@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.

tutorial/colorprovider.txt · Last modified: 2024/08/27 03:13 by solidblock