Skip to content

Matrices

Matrices provide an easy way to transform rendered elements. They also allow content to be transformed into world space properly.

Assuming you're coming from the Introduction To Rendering page, you've already met them. In fact, they're everywhere, which is a good thing, since that means that we can transform about anything.

The Minecraft Implementation

Minecraft has a wrapper around this entire mechanism, called the MatrixStack. It's a stack of matrices, which means that you can push() and pop() from it. These 2 methods will come in very handy later, since they essentially allow you to back up (push()) the current state, and then restore the correct backup later (pop()).

Using a MatrixStack

When rendering anything, you usually have to pass in a MatrixStack from somewhere else. Since we also have access to the MatrixStack, we can make a backup of it (push()), transform it in any way we want, render with it, then restore it before passing it along to the next component (pop()).

Let's take our old example from the Introduction To Rendering page for this example:

java
HudRenderCallback.EVENT.register((drawContext, tickDelta) -> {
    MatrixStack matrixStack = drawContext.getMatrices();
    Matrix4f positionMatrix = matrixStack.peek().getPositionMatrix();
    Tessellator tessellator = Tessellator.getInstance();
    BufferBuilder buffer = tessellator.getBuffer();

    buffer.begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION_COLOR_TEXTURE);
    buffer.vertex(positionMatrix, 20, 20, 0).color(1f, 1f, 1f, 1f).texture(0f, 0f).next();
    buffer.vertex(positionMatrix, 20, 60, 0).color(1f, 0f, 0f, 1f).texture(0f, 1f).next();
    buffer.vertex(positionMatrix, 60, 60, 0).color(0f, 1f, 0f, 1f).texture(1f, 1f).next();
    buffer.vertex(positionMatrix, 60, 20, 0).color(0f, 0f, 1f, 1f).texture(1f, 0f).next();

    RenderSystem.setShader(GameRenderer::getPositionColorTexProgram);
    RenderSystem.setShaderTexture(0, new Identifier("examplemod", "icon.png"));
    RenderSystem.setShaderColor(1f, 1f, 1f, 1f);

    tessellator.draw();
});

We can already see a Matrix4f being made from our MatrixStack we got from the event, so let's try modifying it.

We have 3 ways to modify a MatrixStack: translate(x, y, z), scale(x, y, z) and multiply(Quaternion). multiply is just a fancy word for "rotate the entire stack with the given quaternion".

INFO

If you're struggling to understand what quaternions are, checkout this video by 3Blue1Brown

Scaling & Translating

Before anything, let's wrap our code in matrixStack.push() and .pop():

java
// ...
matrixStack.push();
Matrix4f positionMatrix = matrixStack.peek().getPositionMatrix();
// ...
tessellator.draw();
matrixStack.pop();
// ...

Then, let's try scaling the matrixStack on the x axis, to stretch the image out:

java
// ...
matrixStack.push();
matrixStack.scale(2, 1, 1);
// ...

Doing this results in the following render:

If you're looking really close, you might've noticed that the spacing on the left looks a little... off?

You're correct, scaling all of the vertecies has the side effect that any precomputed padding is ruined. To fix this, we need to either

  1. Translate the Matrix to position the element correctly (positioning the element itself at 0, 0, 0), or
  2. Translate the Matrix to account for the added padding

Let's look at option 2 for now.

Since we have doubled the scale on the x axis, we have added 20 additional pixels to the padding. To remove them, we just need to translate the matrix 20 px to the left: translate(-20, 0, 0).

It's important to do this before scaling the matrix, since matrices will apply previous transformations to new transformations as well.

Adding the translate call before the scale call results in this result:

Since that looks good, let's try to rotate it instead.

Rotation

Assuming you know what a quaternion is, we can use them to rotate our drawn content. Lucky for us, minecraft has an utility to help with quaternions: RotationAxis.

We'll be using RotationAxis.POSITIVE_Z to rotate the image clockwise on the screen. The reason we're using POSITIVE_Z, is because RotationAxis generates quaternions based on the axis we rotate around, which is the Z axis in our case:

By calling multiply(RotationAxis.POSITIVE_Z.rotationDegrees(45)), we can effectively rotate the entire drawn content by 45 degrees clockwise, but something goes horribly wrong:

Looking at the previous attempt, we can easily connect the dots here: We're rotating the element positioned at (20, 20) from the origin (0, 0), which will rotate the element in an orbit $\sqrt{20^2 + 20^2}$ pixels from the origin.

To fix this, we have to

  1. Translate the Matrix to shift the entire element into the right position
  2. Rotate it
  3. Translate the Matrix to shift the entire element onto (0, 0), where we want to rotate it from

Remember: previous transformations apply to the future ones, so the 3rd step is rotated from the 2nd, which is translated from the 1st

All in all, an example of this would look like this:

java
matrixStack.translate(40, 40, 0); // shift the entire image back
matrixStack.multiply(RotationAxis.POSITIVE_Z.rotationDegrees(45)); // rotate the image
matrixStack.translate(-40, -40, 0); // shift the entire element -40 pixels to the left and top, so the center of the image is at (0, 0)

After doing this, we finally get a proper result:

And since this is rendered each frame, we can easily animate it:

Full Example

java
HudRenderCallback.EVENT.register((drawContext, tickDelta) -> {
    MatrixStack matrixStack = drawContext.getMatrices();
    matrixStack.push();

    matrixStack.translate(40, 40, 0);
    matrixStack.multiply(RotationAxis.POSITIVE_Z.rotationDegrees((System.currentTimeMillis() % 5000) / 5000f * 360f));
    matrixStack.translate(-40, -40, 0);

    Matrix4f positionMatrix = matrixStack.peek().getPositionMatrix();
    Tessellator tessellator = Tessellator.getInstance();
    BufferBuilder buffer = tessellator.getBuffer();

    buffer.begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION_COLOR_TEXTURE);
    buffer.vertex(positionMatrix, 20, 20, 0).color(1f, 1f, 1f, 1f).texture(0f, 0f).next();
    buffer.vertex(positionMatrix, 20, 60, 0).color(1f, 0f, 0f, 1f).texture(0f, 1f).next();
    buffer.vertex(positionMatrix, 60, 60, 0).color(0f, 1f, 0f, 1f).texture(1f, 1f).next();
    buffer.vertex(positionMatrix, 60, 20, 0).color(0f, 0f, 1f, 1f).texture(1f, 0f).next();

    RenderSystem.setShader(GameRenderer::getPositionColorTexProgram);
    RenderSystem.setShaderTexture(0, new Identifier("examplemod", "icon.png"));
    RenderSystem.setShaderColor(1f, 1f, 1f, 1f);

    tessellator.draw();
    matrixStack.pop();
});

Not affiliated with Mojang Studios or the Fabric Project.