Skip to main content

Episode 15

Back: Index

Introduction

In the first part of this tutorial we continue with some basic rendering tasks. The second part of this tutorial is an update on the data generation tutorial for blockstates.

Part one: rendering

In this part of the tutorial we implement a feature where all mobs are marked with a red (for hostile) or green line when the player is holding a ghast tear in his or her hand.

Custom RenderType

The first thing we have to do is to make a custom render type. We could have used the standard lines() render type, but we want a thicker line so that's why we made a custom one:

public class MyRenderType extends RenderType {

    // Dummy
    public MyRenderType(String name, VertexFormat format, int p_i225992_3_, int p_i225992_4_, boolean p_i225992_5_, boolean p_i225992_6_, Runnable runnablePre, Runnable runnablePost) {
        super(name, format, p_i225992_3_, p_i225992_4_, p_i225992_5_, p_i225992_6_, runnablePre, runnablePost);
    }

    private static final LineState THICK_LINES = new LineState(OptionalDouble.of(3.0D));

    public static final RenderType OVERLAY_LINES = get("overlay_lines",
        DefaultVertexFormats.POSITION_COLOR, GL11.GL_LINES, 256,
        RenderType.State.builder().line(THICK_LINES)
            .layer(PROJECTION_LAYERING)
            .transparency(TRANSLUCENT_TRANSPARENCY)
            .texture(NO_TEXTURE)
            .depthTest(DEPTH_ALWAYS)
            .cull(CULL_DISABLED)
            .lightmap(LIGHTMAP_DISABLED)
            .writeMask(COLOR_WRITE)
            .build(false));
}

To make a custom render type you need to make a class that extends from RenderType. The reason for this is that many essential things in RenderType are protected, so we can only access them from a subclass. However, we don't actually need to make an instance of this RenderType. To make a custom render type the only thing that you have to do is the code above. There is no need to register it. You just define it like this, and then you can use it.

We want our lines to render on top of all geometry. Even if it would be covered by a block that is normally obscuring the line. This is done by the combination of depthTest(DEPTH_ALWAYS) (i.e. always render, ignoring the depth buffer) and writeMask(COLOR_WRITE) (only write color, don't write depth information).

The RenderLivingEvent renderer

To render things in the world independent of an entity or block and even for entities or blocks that are not from your mod, Forge provides a number of events that you can subscribe too. In this example we will subscribe to RenderLivingEvent.Post which is called after an entity is rendered. There is also a RenderLivingEvent.Pre that can be canceled. You can use that to make entities invisible for example.

public class AfterLivingRenderer {

    public static void render(RenderLivingEvent.Post event) {
        ClientPlayerEntity player = Minecraft.getInstance().player;

        if (player.getHeldItemMainhand().getItem() == Items.GHAST_TEAR) {
            showMobs(event.getMatrixStack(), event.getEntity());
        }
    }

    private static void greenLine(IVertexBuilder builder, Matrix4f positionMatrix, float dx1, float dy1, float dz1, float dx2, float dy2, float dz2) {
        builder.pos(positionMatrix, dx1, dy1, dz1)
                .color(0.0f, 1.0f, 0.0f, 1.0f)
                .endVertex();
        builder.pos(positionMatrix, dx2, dy2, dz2)
                .color(0.0f, 1.0f, 0.0f, 1.0f)
                .endVertex();
    }

    private static void redLine(IVertexBuilder builder, Matrix4f positionMatrix, float dx1, float dy1, float dz1, float dx2, float dy2, float dz2) {
        builder.pos(positionMatrix, dx1, dy1, dz1)
                .color(1.0f, 0.0f, 0.0f, 1.0f)
                .endVertex();
        builder.pos(positionMatrix, dx2, dy2, dz2)
                .color(1.0f, 0.0f, 0.0f, 1.0f)
                .endVertex();
    }


    private static void showMobs(MatrixStack matrixStack, LivingEntity entity) {
        IRenderTypeBuffer.Impl buffer = Minecraft.getInstance().getRenderTypeBuffers().getBufferSource();
        IVertexBuilder builder = buffer.getBuffer(MyRenderType.OVERLAY_LINES);

        Matrix4f positionMatrix = matrixStack.getLast().getPositionMatrix();

        if (entity instanceof IMob) {
            redLine(builder, positionMatrix, 0, .5f, 0, 0, 6, 0);
        } else {
            greenLine(builder, positionMatrix, 0, .5f, 0, 0, 6, 0);
        }
    }
}

The main method in this class is render. That's the actual listener. But before it can work we also need to add it to the event bus. Add this code to ClientSetup:

public static void init(final FMLClientSetupEvent event) {
    ...
    MinecraftForge.EVENT_BUS.addListener(AfterLivingRenderer::render);
}

The rendering itself is pretty simple. We get a matrixstack from the event and the IRenderTypeBuffer we currently have to get from the main Minecraft instance. In the future this will probably be added to the event as well but for now you can do it as the code above does. The matrixStack that is given in the event will already be set at the position/orientation of the entity that has just been rendered. So we don't have to manipulate it in this case. The rest of the code is very simple. We create a vertex builder for our custom render type and add a red or green line depending on the type of mob.

The RenderWorldLastEvent renderer

Another useful event is RenderWorldLastEvent. This happens after all other things have been rendered. In this example we use it to implement a feature where all nearby tile entities are marked with a blue outline when the player is holding a netherstar.

public class InWorldRenderer {

    public static void render(RenderWorldLastEvent event) {
        ClientPlayerEntity player = Minecraft.getInstance().player;

        if (player.getHeldItemMainhand().getItem() == Items.NETHER_STAR) {
            locateTileEntities(player, event.getMatrixStack());
        }
    }

    private static void blueLine(IVertexBuilder builder, Matrix4f positionMatrix, BlockPos pos, float dx1, float dy1, float dz1, float dx2, float dy2, float dz2) {
        builder.pos(positionMatrix, pos.getX()+dx1, pos.getY()+dy1, pos.getZ()+dz1)
                .color(0.0f, 0.0f, 1.0f, 1.0f)
                .endVertex();
        builder.pos(positionMatrix, pos.getX()+dx2, pos.getY()+dy2, pos.getZ()+dz2)
                .color(0.0f, 0.0f, 1.0f, 1.0f)
                .endVertex();
    }

    private static void locateTileEntities(ClientPlayerEntity player, MatrixStack matrixStack) {
        IRenderTypeBuffer.Impl buffer = Minecraft.getInstance().getRenderTypeBuffers().getBufferSource();
        IVertexBuilder builder = buffer.getBuffer(MyRenderType.OVERLAY_LINES);

        BlockPos playerPos = player.getPosition();
        int px = playerPos.getX();
        int py = playerPos.getY();
        int pz = playerPos.getZ();
        World world = player.getEntityWorld();

        matrixStack.push();

        Vec3d projectedView = Minecraft.getInstance().gameRenderer.getActiveRenderInfo().getProjectedView();
        matrixStack.translate(-projectedView.x, -projectedView.y, -projectedView.z);

        Matrix4f positionMatrix = matrixStack.getLast().getPositionMatrix();

        BlockPos.Mutable pos = new BlockPos.Mutable();
        for (int dx = -10; dx <= 10; dx++) {
            for (int dy = -10; dy <= 10; dy++) {
                for (int dz = -10; dz <= 10; dz++) {
                    pos.setPos(px + dx, py + dy, pz + dz);
                    if (world.getTileEntity(pos) != null) {
                        blueLine(builder, positionMatrix, pos, 0, 0, 0, 1, 0, 0);
                        blueLine(builder, positionMatrix, pos, 0, 1, 0, 1, 1, 0);
                        blueLine(builder, positionMatrix, pos, 0, 0, 1, 1, 0, 1);
                        blueLine(builder, positionMatrix, pos, 0, 1, 1, 1, 1, 1);

                        blueLine(builder, positionMatrix, pos, 0, 0, 0, 0, 0, 1);
                        blueLine(builder, positionMatrix, pos, 1, 0, 0, 1, 0, 1);
                        blueLine(builder, positionMatrix, pos, 0, 1, 0, 0, 1, 1);
                        blueLine(builder, positionMatrix, pos, 1, 1, 0, 1, 1, 1);

                        blueLine(builder, positionMatrix, pos, 0, 0, 0, 0, 1, 0);
                        blueLine(builder, positionMatrix, pos, 1, 0, 0, 1, 1, 0);
                        blueLine(builder, positionMatrix, pos, 0, 0, 1, 0, 1, 1);
                        blueLine(builder, positionMatrix, pos, 1, 0, 1, 1, 1, 1);
                    }
                }
            }
        }

        matrixStack.pop();

        RenderSystem.disableDepthTest();
        buffer.finish(MyRenderType.OVERLAY_LINES);
    }
}

This example is a bit more complicated. When this event is called the given MatrixStack will be set to the position of the player in discrete coordinates (i.e. the BlockPos location at the feet of the player). However, when the player is moving there is actually smooth movement of the camera location at any given time. To calculate the actual camera position at any given point in time we need this call:

Vec3d projectedView = Minecraft.getInstance().gameRenderer.getActiveRenderInfo().getProjectedView();
matrixStack.translate(-projectedView.x, -projectedView.y, -projectedView.z);

We use this position to translate our view backwards. After this translation we can render things in the world with their actual world position (for example the actual BlockPos of a tile entity in the world).

The rest of the code is straightforward: we scan the area for tile entities and then render 12 blue lines around every tile entity using our custom render type.

The final two lines of this render function are a bit special. The reason they are needed may have to do with a bug in Forge but the problem is that the OpenGL state at this time is apparently not predictable and so it happened that the color (for example) was not correct. To force it to be correct we call buffer.finish which will actually flush all geometry that was put on this render type. We also forcibly disable the depth test before doing that to be sure that it is correct.

Part two: multipart model and blockstate generation

In this part of the tutorial we take a more complex multipart model. The model is similar to the RFToolsPower dimensional cell. It is a block that has six configurable sides. Every side can have three modes: none, input, and output.

The block

public class ComplexMultipartBlock extends Block {

    public static final EnumProperty<ComplexMultipartTile.Mode> NORTH = EnumProperty.create("north", ComplexMultipartTile.Mode.class);
    public static final EnumProperty<ComplexMultipartTile.Mode> SOUTH = EnumProperty.create("south", ComplexMultipartTile.Mode.class);
    public static final EnumProperty<ComplexMultipartTile.Mode> WEST = EnumProperty.create("west", ComplexMultipartTile.Mode.class);
    public static final EnumProperty<ComplexMultipartTile.Mode> EAST = EnumProperty.create("east", ComplexMultipartTile.Mode.class);
    public static final EnumProperty<ComplexMultipartTile.Mode> UP = EnumProperty.create("up", ComplexMultipartTile.Mode.class);
    public static final EnumProperty<ComplexMultipartTile.Mode> DOWN = EnumProperty.create("down", ComplexMultipartTile.Mode.class);

    private final static VoxelShape RENDER_SHAPE = VoxelShapes.create(0.1, 0.1, 0.1, 0.9, 0.9, 0.9);

    public ComplexMultipartBlock() {
        super(Properties.create(Material.IRON)
                .sound(SoundType.METAL)
                .hardnessAndResistance(2.0f)
        );
    }

    @Override
    public boolean hasTileEntity(BlockState state) {
        return true;
    }

    @Nullable
    @Override
    public TileEntity createTileEntity(BlockState state, IBlockReader world) {
        return new ComplexMultipartTile();
    }

    @Override
    public VoxelShape getRenderShape(BlockState state, IBlockReader reader, BlockPos pos) {
        return RENDER_SHAPE;
    }

    @Override
    public ActionResultType onBlockActivated(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockRayTraceResult result) {
        if (!world.isRemote) {
            TileEntity te = world.getTileEntity(pos);
            if (te instanceof ComplexMultipartTile) {
                ComplexMultipartTile dimensionalCellTileEntity = (ComplexMultipartTile) te;
                dimensionalCellTileEntity.toggleMode(result.getFace());
            }
        }
        return super.onBlockActivated(state, world, pos, player, hand, result);
    }

    @Override
    protected void fillStateContainer(StateContainer.Builder<Block, BlockState> builder) {
        super.fillStateContainer(builder);
        builder.add(NORTH, SOUTH, WEST, EAST, DOWN, UP);
    }
}

The block code is pretty straightforward. We have six properties (one for every side) for the Mode enum (which will be defined later). When the block is activated we toggle the mode of the face that is being selected by the player. Note the render shape which is slightly smaller than a full block. That's needed because our block is transparent, so we don't want to cull faces of adjacent blocks.

To render our block on the translucent layer we need to add this to ClientSetup:

    public static void init(final FMLClientSetupEvent event) {
        ...
        RenderTypeLookup.setRenderLayer(Registration.COMPLEX_MULTIPART.get(), RenderType.translucent());
    }

The tile entity

public class ComplexMultipartTile extends TileEntity {

    private Mode modes[] = new Mode[]{Mode.MODE_NONE, Mode.MODE_NONE, Mode.MODE_NONE, Mode.MODE_NONE, Mode.MODE_NONE, Mode.MODE_NONE};

    public ComplexMultipartTile() {
        super(Registration.COMPLEX_MULTIPART_TILE.get());
    }

    public void toggleMode(Direction side) {
        switch (modes[side.ordinal()]) {
            case MODE_NONE:
                modes[side.ordinal()] = Mode.MODE_INPUT;
                break;
            case MODE_INPUT:
                modes[side.ordinal()] = Mode.MODE_OUTPUT;
                break;
            case MODE_OUTPUT:
                modes[side.ordinal()] = Mode.MODE_NONE;
                break;
        }
        updateState();
    }

    public Mode getMode(Direction side) {
        return modes[side.ordinal()];
    }

    private void updateState() {
        Mode north = getMode(Direction.NORTH);
        Mode south = getMode(Direction.SOUTH);
        Mode west = getMode(Direction.WEST);
        Mode east = getMode(Direction.EAST);
        Mode up = getMode(Direction.UP);
        Mode down = getMode(Direction.DOWN);
        BlockState state = world.getBlockState(pos);
        world.setBlockState(pos, state.with(NORTH, north).with(SOUTH, south).with(WEST, west).with(EAST, east).with(UP, up).with(DOWN, down),
                Constants.BlockFlags.BLOCK_UPDATE + Constants.BlockFlags.NOTIFY_NEIGHBORS);
        markDirty();
    }

    @Override
    public void read(CompoundNBT compound) {
        super.read(compound);
        modes[0] = Mode.values()[compound.getByte("m0")];
        modes[1] = Mode.values()[compound.getByte("m1")];
        modes[2] = Mode.values()[compound.getByte("m2")];
        modes[3] = Mode.values()[compound.getByte("m3")];
        modes[4] = Mode.values()[compound.getByte("m4")];
        modes[5] = Mode.values()[compound.getByte("m5")];
    }

    @Override
    public CompoundNBT write(CompoundNBT compound) {
        compound.putByte("m0", (byte) modes[0].ordinal());
        compound.putByte("m1", (byte) modes[1].ordinal());
        compound.putByte("m2", (byte) modes[2].ordinal());
        compound.putByte("m3", (byte) modes[3].ordinal());
        compound.putByte("m4", (byte) modes[4].ordinal());
        compound.putByte("m5", (byte) modes[5].ordinal());
        return super.write(compound);
    }

    public enum Mode implements IStringSerializable {
        MODE_NONE("none"),
        MODE_INPUT("input"),   // Blue
        MODE_OUTPUT("output"); // Yellow

        private final String name;

        Mode(String name) {
            this.name = name;
        }

        @Override
        public String getName() {
            return name;
        }


        @Override
        public String toString() {
            return getName();
        }
    }
}

The tile entity keeps track of the six modes. Note that this is actually not really needed as the TileEntity could get this data from the blockstate itself. This is just for demonstration purposes.

Registration

Add this code to the Registration class:

    public static final RegistryObject<ComplexMultipartBlock> COMPLEX_MULTIPART = BLOCKS.register("complex_multipart", ComplexMultipartBlock::new);
    public static final RegistryObject<Item> COMPLEX_MULTIPART_ITEM = ITEMS.register("complex_multipart", () -> new BlockItem(COMPLEX_MULTIPART.get(), new Item.Properties().group(ModSetup.ITEM_GROUP)));
    public static final RegistryObject<TileEntityType<ComplexMultipartTile>> COMPLEX_MULTIPART_TILE = TILES.register("complex_multipart", () -> TileEntityType.Builder.create(ComplexMultipartTile::new, COMPLEX_MULTIPART.get()).build(null));

Data generator

We extend our data generator to also have support for generating blockstates:

@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD)
public class DataGenerators {

    @SubscribeEvent
    public static void gatherData(GatherDataEvent event) {
        DataGenerator generator = event.getGenerator();
        if (event.includeServer()) {
            generator.addProvider(new Recipes(generator));
            generator.addProvider(new LootTables(generator));
        }
        if (event.includeClient()) {
            generator.addProvider(new BlockStates(generator, event.getExistingFileHelper()));
        }
    }
}

BlockState generator

The BlockState generator is a bit more complex in this situation:

public class BlockStates extends BlockStateProvider {

    public BlockStates(DataGenerator gen, ExistingFileHelper exFileHelper) {
        super(gen, MyTutorial.MODID, exFileHelper);
    }

    @Override
    protected void registerStatesAndModels() {
        BlockModelBuilder dimCellFrame = models().getBuilder("block/complex/main");

        floatingCube(dimCellFrame, 0f, 0f, 0f, 1f, 16f, 1f);
        floatingCube(dimCellFrame, 15f, 0f, 0f, 16f, 16f, 1f);
        floatingCube(dimCellFrame, 0f, 0f, 15f, 1f, 16f, 16f);
        floatingCube(dimCellFrame, 15f, 0f, 15f, 16f, 16f, 16f);

        floatingCube(dimCellFrame, 1f, 0f, 0f, 15f, 1f, 1f);
        floatingCube(dimCellFrame, 1f, 15f, 0f, 15f, 16f, 1f);
        floatingCube(dimCellFrame, 1f, 0f, 15f, 15f, 1f, 16f);
        floatingCube(dimCellFrame, 1f, 15f, 15f, 15f, 16f, 16f);

        floatingCube(dimCellFrame, 0f, 0f, 1f, 1f, 1f, 15f);
        floatingCube(dimCellFrame, 15f, 0f, 1f, 16f, 1f, 15f);
        floatingCube(dimCellFrame, 0f, 15f, 1f, 1f, 16f, 15f);
        floatingCube(dimCellFrame, 15f, 15f, 1f, 16f, 16f, 15f);

        floatingCube(dimCellFrame, 1f, 1f, 1f, 15f, 15f, 15f);

        dimCellFrame.texture("window", modLoc("block/complex_window"));

        createDimensionalCellModel(Registration.COMPLEX_MULTIPART.get(), dimCellFrame);
    }

    private void floatingCube(BlockModelBuilder builder, float fx, float fy, float fz, float tx, float ty, float tz) {
        builder.element().from(fx, fy, fz).to(tx, ty, tz).allFaces((direction, faceBuilder) -> faceBuilder.texture("#window")).end();
    }

    private void createDimensionalCellModel(Block block, BlockModelBuilder dimCellFrame) {
        BlockModelBuilder singleNone = models().getBuilder("block/complex/singlenone")
                .element().from(3, 3, 3).to(13, 13, 13).face(Direction.DOWN).texture("#single").end().end()
                .texture("single", modLoc("block/complex"));
        BlockModelBuilder singleIn = models().getBuilder("block/complex/singlein")
                .element().from(3, 3, 3).to(13, 13, 13).face(Direction.DOWN).texture("#single").end().end()
                .texture("single", modLoc("block/complex_in"));
        BlockModelBuilder singleOut = models().getBuilder("block/complex/singleout")
                .element().from(3, 3, 3).to(13, 13, 13).face(Direction.DOWN).texture("#single").end().end()
                .texture("single", modLoc("block/complex_out"));

        MultiPartBlockStateBuilder bld = getMultipartBuilder(block);

        bld.part().modelFile(dimCellFrame).addModel();

        BlockModelBuilder[] models = new BlockModelBuilder[] { singleNone, singleIn, singleOut };
        for (ComplexMultipartTile.Mode mode : ComplexMultipartTile.Mode.values()) {
            bld.part().modelFile(models[mode.ordinal()]).addModel().condition(ComplexMultipartBlock.DOWN, mode);
            bld.part().modelFile(models[mode.ordinal()]).rotationX(180).addModel().condition(ComplexMultipartBlock.UP, mode);
            bld.part().modelFile(models[mode.ordinal()]).rotationX(90).addModel().condition(ComplexMultipartBlock.SOUTH, mode);
            bld.part().modelFile(models[mode.ordinal()]).rotationX(270).addModel().condition(ComplexMultipartBlock.NORTH, mode);
            bld.part().modelFile(models[mode.ordinal()]).rotationY(90).rotationX(90).addModel().condition(ComplexMultipartBlock.WEST, mode);
            bld.part().modelFile(models[mode.ordinal()]).rotationY(270).rotationX(90).addModel().condition(ComplexMultipartBlock.EAST, mode);
        }
    }
}

This code will generate the multipart blockstate file as well as four models:

  • main.json: the outer frame of the block and the transparent windows
  • singlein.json: the model representing the down quad for input mode
  • singleout.json: the model representing the down quad for output mode
  • singlenone.json: the model representing the down quad for none mode

The multipart model will (depending on the DOWN, UP, SOUTH, NORTH, WEST, or EAST properties in the blockstate) select various parts. It will also rotate the single quad model to face the correct way depending on orientation.