Skip to main content

Episode 14

Back: Index

Introduction

In this tutorial we will explain how to do rendering in Minecraft 1.15. More in particular how to make a TileEntityRenderer and the concepts behind it.

Adding our magic block

We add a very simple MagicBlock and tile entity like usual. If in doubt check this commit as a reference here

Concepts

Here a few important concepts are explained:

Texture Atlas

Because switching state (like OpenGL settings, texture, ...) on 3D hardware is relatively expensive it is a good idea to batch as many triangles as possible using the same state. That's why Minecraft uses texture atlases in Minecraft which contains all the smaller textures. Check out the following site to see an example of a texture atlas here

Texture Stitching

Texture stitching is the process of putting your small texture on the big atlas. Usually this is done automatically by Minecraft, but sometimes you might want to add your own textures to the atlas manually.

Render Type

All triangles (quads) of the same type are batched together. This is where RenderType comes into play. This indicates the type of the triangle. A few vanilla render types are: SOLID, TRANSLUCENT, CUTOUT, ... but you can also make your own. By rendering your geometry on the correct type it will get rendered together with similar geometry.

Vertex format

A vertex format is basically the information that makes up a single vertex (corner point) of a triangle or quad. The information that is stored with a vertex can include things like:

  • Position
  • Color
  • UV coordinate (texture coordinate on the texture)
  • Normal (vector orthogonal to the surface of the triangle)
  • Lightmap coordinate (for lighting)

DefaultVertexFormats contains many standard vertex formats. The one that is used most is BLOCK.

The renderer

Here we create the MagicRenderer class like this:

public class MagicRenderer extends TileEntityRenderer<MagicTile> {

    public static final ResourceLocation MAGICBLOCK_TEXTURE = new ResourceLocation(MyTutorial.MODID, "block/magicblock");

    public MagicRenderer(TileEntityRendererDispatcher rendererDispatcherIn) {
        super(rendererDispatcherIn);
    }

    private void add(IVertexBuilder renderer, MatrixStack stack, float x, float y, float z, float u, float v) {
        renderer.pos(stack.getLast().getPositionMatrix(), x, y, z)
                .color(1.0f, 1.0f, 1.0f, 1.0f)
                .tex(u, v)
                .lightmap(0, 240)
                .normal(1, 0, 0)
                .endVertex();
    }

    private static float diffFunction(long time, long delta, float scale) {
        long dt = time % (delta * 2);
        if (dt > delta) {
            dt = 2*delta - dt;
        }
        return dt * scale;
    }

    @Override
    public void render(MagicTile tileEntity, float partialTicks, MatrixStack matrixStack, IRenderTypeBuffer buffer, int combinedLight, int combinedOverlay) {

        TextureAtlasSprite sprite = Minecraft.getInstance().getTextureGetter(AtlasTexture.LOCATION_BLOCKS_TEXTURE).apply(MAGICBLOCK_TEXTURE);
        IVertexBuilder builder = buffer.getBuffer(RenderType.translucent());

        long time = System.currentTimeMillis();
        float dx1 = diffFunction(time, 1000, 0.0001f);
        float dx2 = diffFunction(time, 1500, 0.00005f);
        float dx3 = diffFunction(time, 1200, 0.00011f);
        float dx4 = diffFunction(time, 1300, 0.00006f);
        float dy1 = diffFunction(time, 1400, 0.00009f);
        float dy2 = diffFunction(time, 1600, 0.00007f);
        float dy3 = diffFunction(time, 1000, 0.00015f);
        float dy4 = diffFunction(time, 1200, 0.00003f);

        float angle = (time / 100) % 360;
        Quaternion rotation = Vector3f.YP.rotationDegrees(angle);
        float scale = 1.0f + diffFunction(time,1000, 0.001f);

        matrixStack.push();
        matrixStack.translate(.5, .5, .5);
        matrixStack.rotate(rotation);
        matrixStack.scale(scale, scale, scale);
        matrixStack.translate(-.5, -.5, -.5);

        add(builder, matrixStack, 0 + dx1, 0 + dy1, .5f, sprite.getMinU(), sprite.getMinV());
        add(builder, matrixStack, 1 - dx2, 0 + dy2, .5f, sprite.getMaxU(), sprite.getMinV());
        add(builder, matrixStack, 1 - dx3, 1 - dy3, .5f, sprite.getMaxU(), sprite.getMaxV());
        add(builder, matrixStack, 0 + dx4, 1 - dy4, .5f, sprite.getMinU(), sprite.getMaxV());

        add(builder, matrixStack, 0 + dx4, 1 - dy4, .5f, sprite.getMinU(), sprite.getMaxV());
        add(builder, matrixStack, 1 - dx3, 1 - dy3, .5f, sprite.getMaxU(), sprite.getMaxV());
        add(builder, matrixStack, 1 - dx2, 0 + dy2, .5f, sprite.getMaxU(), sprite.getMinV());
        add(builder, matrixStack, 0 + dx1, 0 + dy1, .5f, sprite.getMinU(), sprite.getMinV());

        matrixStack.pop();
    }

    public static void register() {
        ClientRegistry.bindTileEntityRenderer(Registration.MAGICBLOCK_TILE.get(), MagicRenderer::new);
    }
}

The register method we need to call from within our ClientSetup like this:

public static void init(final FMLClientSetupEvent event) {
    ScreenManager.registerFactory(Registration.FIRSTBLOCK_CONTAINER.get(), FirstBlockScreen::new);
    RenderingRegistry.registerEntityRenderingHandler(Registration.WEIRDMOB.get(), WeirdMobRenderer::new);
    ModelLoaderRegistry.registerLoader(new ResourceLocation(MyTutorial.MODID, "fancyloader"), new FancyModelLoader());
    MagicRenderer.register();
}

Because we are using a texture that is not used elsewhere (i.e. not loaded automatically through a JSON model) we also need to listen to the TextureStitchEvent.Pre event:

@SubscribeEvent
public static void onTextureStitch(TextureStitchEvent.Pre event) {
    if (!event.getMap().getBasePath().equals(AtlasTexture.LOCATION_BLOCKS_TEXTURE)) {
        return;
    }

    event.addSprite(MAGICBLOCK_TEXTURE);
}

The renderer step by step

Because we stitched our texture to the main atlas we can get the 'sprite' for it with the line below. This sprite contains information on where our texture can be found on the big atlas. In other words, it contains the minimum U and V coordinates as well as the maximum U and V coordinates:

TextureAtlasSprite sprite = Minecraft.getInstance().getTextureGetter(AtlasTexture.LOCATION_BLOCKS_TEXTURE).apply(MAGICBLOCK_TEXTURE);

When the render method is called the matrixStack that is given will be set to the position of our block. If we want to modify it (translate, scale, or rotate) we first need to remember it using push(). Also don't forget to pop() it later:

matrixStack.push();
matrixStack.translate(.5, .5, .5);
matrixStack.rotate(rotation);
matrixStack.scale(scale, scale, scale);
matrixStack.translate(-.5, -.5, -.5);

...

matrixStack.pop();

To actually render our quad we first have to get a vertex builder for the correct render type, and then we can use that builder to add quads:

        IVertexBuilder builder = buffer.getBuffer(RenderType.translucent());

        add(builder, matrixStack, 0 + dx1, 0 + dy1, .5f, sprite.getMinU(), sprite.getMinV());
        add(builder, matrixStack, 1 - dx2, 0 + dy2, .5f, sprite.getMaxU(), sprite.getMinV());
        add(builder, matrixStack, 1 - dx3, 1 - dy3, .5f, sprite.getMaxU(), sprite.getMaxV());
        add(builder, matrixStack, 0 + dx4, 1 - dy4, .5f, sprite.getMinU(), sprite.getMaxV());

Note that quads are only visible from one side. That's why we also need to add another quad with the vertices reversed. It's the order of the vertices that determines the side from which the quad will be visible.

We use the standard 'translucent' rendertype because our texture has transparency. Note that you can also define your own render types. This will be covered later.

The 'add' method is the method responsible for actually creating a vertex. The implementation of this is as follows:

private void add(IVertexBuilder renderer, MatrixStack stack, float x, float y, float z, float u, float v) {
    renderer.pos(stack.getLast().getPositionMatrix(), x, y, z)
        .color(1.0f, 1.0f, 1.0f, 1.0f)
        .tex(u, v)
        .lightmap(0, 240)
        .normal(1, 0, 0)
        .endVertex();
}

Note that the order in which we add information to the IVertexBuilder is important. Check the format in DefaultVertexFormats (in our case BLOCK) to see what this order should be.

Rendering an ItemStack and BlockState

In addition to rendering your own quads it is also possible to render other items and blocks from within your renderer. That can be useful (for example) to implement some kind of pedestal which shows a visual in-world representation of the item it contains. In this example we just render a hardcoded item:

matrixStack.push();

matrixStack.translate(0.5, 1.5, 0.5);
ItemRenderer itemRenderer = Minecraft.getInstance().getItemRenderer();
ItemStack stack = new ItemStack(Items.DIAMOND);
IBakedModel ibakedmodel = itemRenderer.getItemModelWithOverrides(stack, tileEntity.getWorld(), null);
itemRenderer.renderItem(stack, ItemCameraTransforms.TransformType.FIXED, true, matrixStack, buffer, combinedLight, combinedOverlay, ibakedmodel);

matrixStack.translate(-.5, 1, -.5);
BlockRendererDispatcher blockRenderer = Minecraft.getInstance().getBlockRendererDispatcher();
BlockState state = Blocks.ENDER_CHEST.getDefaultState();
blockRenderer.renderBlock(state, matrixStack, buffer, combinedLight, combinedOverlay, EmptyModelData.INSTANCE);

matrixStack.pop();

Add this code right at the end of the render method (so after matrixStack.pop()). The reason is that we want to work with the original matrixStack again so that we don't have the random scale/rotation that we applied for our quad.