Sometimes, using Minecraft's model format is not enough. If you need to add dynamic rendering to your block's visuals, you will need to use a BlockEntityRenderer.
For example, let's make the Counter Block from the Block Entities article show the number of clicks on its top side.
Creating a BlockEntityRenderer
Block entity rendering uses a submit/render system where you first submit the data required to render an object to the screen, the game then renders the object using it's submitted state.
When creating a BlockEntityRenderer for the CounterBlockEntity, it's important to place the class in the appropriate source set, such as src/client/, if your project uses split source sets for client and server. Accessing rendering-related classes directly in the src/main/ source set is not safe because those classes might be loaded on a server.
First, we need to create a BlockEntityRenderState for our CounterBlockEntity to hold the data that will be used for rendering. In this case, we will need the clicks to be available during rendering.
java
public class CounterBlockEntityRenderState extends BlockEntityRenderState {
private int clicks = 0;
public int getClicks() {
return clicks;
}
public void setClicks(int clicks) {
this.clicks = clicks;
}
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
Then we create a BlockEntityRenderer for our CounterBlockEntity.
java
public class CounterBlockEntityRenderer implements BlockEntityRenderer<CounterBlockEntity, CounterBlockEntityRenderState> {
public CounterBlockEntityRenderer(BlockEntityRendererFactory.Context context) {
}
@Override
public CounterBlockEntityRenderState createRenderState() {
return new CounterBlockEntityRenderState();
}
@Override
public void updateRenderState(CounterBlockEntity blockEntity, CounterBlockEntityRenderState state, float tickProgress, Vec3d cameraPos, @Nullable ModelCommandRenderer.CrumblingOverlayCommand crumblingOverlay) {
}
@Override
public void render(CounterBlockEntityRenderState state, MatrixStack matrices, OrderedRenderCommandQueue queue, CameraRenderState cameraState) {
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
The new class has a constructor with BlockEntityRendererFactory.Context as a parameter. The Context has a few useful rendering utilities, like the ItemRenderer or TextRenderer. Also, by including a constructor like this, it becomes possible to use the constructor as the BlockEntityRendererFactory functional interface itself:
java
public class ExampleModBlockEntityRenderer implements ClientModInitializer {
@Override
public void onInitializeClient() {
BlockEntityRendererFactories.register(ModBlockEntities.COUNTER_BLOCK_ENTITY, CounterBlockEntityRenderer::new);
}
}1
2
3
4
5
6
2
3
4
5
6
We will override a few methods to set up the render state along with the render method where the rendering logic will be set up.
createRenderState can be used to initialize the render state.
java
@Override
public CounterBlockEntityRenderState createRenderState() {
return new CounterBlockEntityRenderState();
}1
2
3
4
2
3
4
updateRenderState can be used to update the render state with entity data.
java
@Override
public void updateRenderState(CounterBlockEntity blockEntity, CounterBlockEntityRenderState state, float tickProgress, Vec3d cameraPos, @Nullable ModelCommandRenderer.CrumblingOverlayCommand crumblingOverlay) {
// :::1
BlockEntityRenderer.super.updateRenderState(blockEntity, state, tickProgress, cameraPos, crumblingOverlay);
state.setClicks(blockEntity.getClicks());
// :::1
}1
2
3
4
5
6
7
2
3
4
5
6
7
You should register block entity renderers in your ClientModInitializer class.
BlockEntityRendererFactories is a registry that maps each BlockEntityType with custom rendering code to its respective BlockEntityRenderer.
Drawing on Blocks
Now that we have a renderer, we can draw. The render method is called every frame, and it's where the rendering magic happens.
Moving Around
First, we need to offset and rotate the text so that it's on the block's top side.
INFO
As the name suggests, the MatrixStack is a stack, meaning that you can push and pop transformations. A good rule-of-thumb is to push a new one at the beginning of the render method and pop it at the end, so that the rendering of one block doesn't affect others.
More information about the MatrixStack can be found in the Basic Rendering Concepts article.
To make the translations and rotations needed easier to understand, let's visualize them. In this picture, the green block is where the text would be drawn, by default in the furthest bottom-left point of the block:

So first we need to move the text halfway across the block on the X and Z axes, and then move it up to the top of the block on the Y axis:

This is done with a single translate call:
java
matrices.translate(0.5, 1, 0.5);1
That's the translation done, rotation and scale remain.
By default, the text is drawn on the XY plane, so we need to rotate it 90 degrees around the X axis to make it face upwards on the XZ plane:

The MatrixStack does not have a rotate function, instead we need to use multiply and RotationAxis.POSITIVE_X:
java
matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(90));1
Now the text is in the correct position, but it's too large. The BlockEntityRenderer maps the whole block to a [-0.5, 0.5] cube, while the TextRenderer uses Y coordinates of [0, 9]. As such, we need to scale it down by a factor of 18:
java
matrices.scale(1/18f, 1/18f, 1/18f);1
Now, the whole transformation looks like this:
java
matrices.push();
matrices.translate(0.5, 1, 0.5);
matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(90));
matrices.scale(1/18f, 1/18f, 1/18f);1
2
3
4
2
3
4
Drawing Text
As mentioned earlier, the Context passed into the constructor of our renderer has a TextRenderer that we can use to measure text (getWidth), which is useful for centering.
To draw the text, we will be submitting the necessary data to the render queue. Since we're drawing some text, we can use the submitText method provided through the OrderedRenderCommandQueue instance passed into the render method.
java
String text = state.getClicks() + "";
float width = textRenderer.getWidth(text);
// draw the text. params:
// text, x, y, color, ordered text, shadow, text layer type, light, color, background color, outline color
queue.submitText(
matrices,
-width / 2, -4f,
Text.literal(text).asOrderedText(),
false,
TextRenderer.TextLayerType.SEE_THROUGH,
state.lightmapCoordinates,
0xffffffff,
0,
0
);1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
The submitText method takes a lot of parameters, but the most important ones are:
- the
OrderedTextto draw; - its
xandycoordinates; - the RGB
colorvalue; - the
Matrix4fdescribing how it should be transformed (to get one from aMatrixStack, we can use.peek().getPositionMatrix()to get theMatrix4ffor the topmost entry).
And after all this work, here's the result:


