🇬🇧 English
🇬🇧 English
Appearance
🇬🇧 English
🇬🇧 English
Appearance
This page is written for version:
1.21.10
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.
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.
public class CounterBlockEntityRenderState extends BlockEntityRenderState {
private int clicks = 0;
public int getClicks() {
return clicks;
}
public void setClicks(int clicks) {
this.clicks = clicks;
}
}
Then we create a BlockEntityRenderer
for our CounterBlockEntity
.
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) {
}
}
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:
public class ExampleModBlockEntityRenderer implements ClientModInitializer {
@Override
public void onInitializeClient() {
BlockEntityRendererFactories.register(ModBlockEntities.COUNTER_BLOCK_ENTITY, CounterBlockEntityRenderer::new);
}
}
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.
@Override
public CounterBlockEntityRenderState createRenderState() {
return new CounterBlockEntityRenderState();
}
updateRenderState
can be used to update the render state with entity data.
@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
}
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
.
Now that we have a renderer, we can draw. The render
method is called every frame, and it's where the rendering magic happens.
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:
matrices.translate(0.5, 1, 0.5);
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
:
matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(90));
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:
matrices.scale(1/18f, 1/18f, 1/18f);
Now, the whole transformation looks like this:
matrices.push();
matrices.translate(0.5, 1, 0.5);
matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(90));
matrices.scale(1/18f, 1/18f, 1/18f);
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.
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
);
The submitText
method takes a lot of parameters, but the most important ones are:
OrderedText
to draw;x
and y
coordinates;color
value;Matrix4f
describing how it should be transformed (to get one from a MatrixStack
, we can use .peek().getPositionMatrix()
to get the Matrix4f
for the topmost entry).And after all this work, here's the result: