Chapter 15, True Shapes

A beta block is one box and it lies about itself. Stairs select as a full cube, a fence collides as a solid pillar, an L-shape claims to be a square. Here is how RetroAPI gives a block a list of boxes that the outline, the raytracer, collision, and even the server all agree on.

Open Beta 1.7.3, look at a staircase, and move your crosshair across it. The selection box that snaps around it is a full cube, top to bottom, corner to corner, because in this game a block has exactly one bounding box and the stair's box is the whole cube. Walk into a fence and you stop a meter away from the rail, because the fence's single box is a fat pillar that fills its footprint. Anything that should be L-shaped or hollow or stepped, in vanilla beta, simply lies: it outlines, collides, and raytraces as the one box it was given, and there is no second box to be had.

RetroAPI's answer is VoxelShapes, ported into RetroAPI from matthewperiut/voxelshapes (credit and full source there). A VoxelShape is not one box, it is a list of boxes, and that list is honored by every system that cares about a block's geometry: the mouse-over selection outline (drawn per edge, with the interior edges where boxes meet removed), raytracing (so you can mine the block sitting behind the gap of a wall corner instead of hitting empty air), entity collision, and the dedicated server's movement verification (so a shaped block never false-flags a legitimate move). One interface on your block, and all four fall into line.

This is the geometry half of a block. The look of a block, its model and textures, is Chapter 16. VoxelShapes is collision and selection: pure code, no assets. The two are independent, a block can have a fancy model and a single vanilla box, or a plain model and a precise multi-box shape, or both.

The API, exactly

You opt in by implementing one interface on your Block. Here it is in full:

com/periut/retroapi/voxelshapes/HasVoxelShape.java
package com.periut.retroapi.voxelshapes;

import net.minecraft.world.World;

/**
 * Implement on a Block to give it a multi-box SELECTION shape: the mouse-over outline,
 * raytracing (you can mine through the gaps), and, unless {@link HasCollisionVoxelShape}
 * is also implemented, entity collision. Return null to fall back to vanilla handling
 * for that position. Ported from matthewperiut/voxelshapes.
 */
public interface HasVoxelShape {
    VoxelShape getVoxelShape(World world, int x, int y, int z);
}

One method, getVoxelShape(world, x, y, z). It runs per query, per block position, so you can return a different shape depending on the world (a fence reading its neighbors, a wall reading its connection state). Return null for any position where you want the ordinary vanilla single-box treatment, the lazy escape hatch when you do not have a special shape to offer.

Sometimes the shape you collide with should not match the shape you select. A fence and a wall stand only one block tall visually, but they are meant to be unjumpable: real Minecraft makes their collision box half a block taller than the model so a player cannot hop over them. That second, taller shape is a separate interface:

com/periut/retroapi/voxelshapes/HasCollisionVoxelShape.java
package com.periut.retroapi.voxelshapes;

import net.minecraft.world.World;

/**
 * Implement alongside {@link HasVoxelShape} when the COLLISION shape differs from the
 * selection shape (fences and walls collide 1.5 blocks tall but select at their visual
 * height). Return null to fall back. Ported from matthewperiut/voxelshapes.
 */
public interface HasCollisionVoxelShape {
    VoxelShape getCollisionVoxelShape(World world, int x, int y, int z);
}

Implement HasCollisionVoxelShape alongside HasVoxelShape only when collision differs from selection. If you implement only HasVoxelShape, the same shape does both jobs. The outline you see and the wall you bump into are then literally the same boxes.

Building a shape

A shape is built from two small types. A VoxelBox is one box in block space, the unit cube runs 0 to 1 on each axis (values outside are allowed), with a min corner and a max corner. A VoxelData is the immutable list of boxes plus a precomputable set of outline edges. Here is VoxelBox in full, it is a record, so it is just six doubles and a couple of helpers:

com/periut/retroapi/voxelshapes/VoxelBox.java
package com.periut.retroapi.voxelshapes;

import net.minecraft.util.math.Box;

import java.util.Arrays;

/**
 * One immutable box of a {@link VoxelShape}, coordinates in block space (0-1, values
 * outside are allowed). Ported from matthewperiut/voxelshapes.
 */
public record VoxelBox(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) {

	public static VoxelBox voxelify(Box box) {
		return new VoxelBox(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ);
	}

	public static VoxelBox[] voxelify(Box[] boxes) {
		return Arrays.stream(boxes).map(VoxelBox::voxelify).toArray(VoxelBox[]::new);
	}

	public static Box devoxelify(VoxelBox voxelBox) {
		return Box.create(voxelBox.minX, voxelBox.minY, voxelBox.minZ, voxelBox.maxX, voxelBox.maxY, voxelBox.maxZ);
	}

	public static Box[] devoxelify(VoxelBox[] voxelBoxes) {
		return Arrays.stream(voxelBoxes).map(VoxelBox::devoxelify).toArray(Box[]::new);
	}

	public VoxelBox add(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) {
		return new VoxelBox(this.minX + minX, this.minY + minY, this.minZ + minZ,
			this.maxX + maxX, this.maxY + maxY, this.maxZ + maxZ);
	}

	public VoxelBox add(VoxelBox other) {
		return new VoxelBox(this.minX + other.minX, this.minY + other.minY, this.minZ + other.minZ,
			this.maxX + other.maxX, this.maxY + other.maxY, this.maxZ + other.maxZ);
	}
}

Note the two add overloads. They shift and grow a box, every corner is moved by the amount you pass, so adding a box that is zero on most axes and 0.5 on maxY is exactly how the fence and wall raise their collision height by half a block, leaving the other five corners untouched. voxelify and devoxelify convert to and from the game's native Box, which is why you can build a shape out of vanilla Box objects when you already have those.

VoxelData is the list. You build one, then derive the rest, every method returns a new instance because the boxes are immutable:

com/periut/retroapi/voxelshapes/VoxelData.java, constructors + builders
public class VoxelData {
    private final VoxelVec3d center;
    private final VoxelBox[] boxes;
    private Line[] lines = null;

    public VoxelData(Box... boxes) {
        this(VoxelBox.voxelify(boxes));
    }

    public VoxelData(VoxelBox... boxes) {
        this(new VoxelVec3d(0.5, 0.5, 0.5), boxes);
    }

    public VoxelData(Vec3d center, Box[] boxes) {
        this(VoxelVec3d.voxelify(center), VoxelBox.voxelify(boxes));
    }

    public VoxelData(VoxelVec3d center, VoxelBox[] boxes) {
        this.center = center;
        this.boxes = boxes;
    }

    public VoxelData withBox(VoxelBox voxelBox) {
        VoxelBox[] newBoxes = new VoxelBox[boxes.length + 1];
        System.arraycopy(boxes, 0, newBoxes, 0, boxes.length);
        newBoxes[boxes.length] = voxelBox;
        return new VoxelData(center, newBoxes);
    }

    public VoxelData withBox(Box box) {
        return withBox(VoxelBox.voxelify(box));
    }

    public VoxelData preCache() {
        computeLines();
        return this;
    }

    public VoxelShape withOffset(int x, int y, int z) {
        return withOffset(new VoxelVec3d(x, y, z));
    }

    // ... withOffset(VoxelVec3d) / withOffset(Vec3d), accessors, computeLines, getLines ...
}

The lifecycle reads top to bottom:

CallWhat it does
new VoxelData(VoxelBox...)a shape from one or more block-space boxes (centered at 0.5, 0.5, 0.5)
new VoxelData(Box...)same, built from vanilla Box objects you already have
data.withBox(box)appends one box, returns a new VoxelData, the original is untouched
data.preCache()computes the outline edges once, up front, and returns the same data for chaining
data.withOffset(x, y, z)positions the shape at a block, producing the per-query VoxelShape

The pattern that falls out of this is the whole performance story: build the VoxelData once per distinct shape (a static field, a small array indexed by metadata, or a map keyed by state) and call withOffset per query to slide that cached template onto the block being asked about. The boxes and the precomputed outline edges are shared across every position; only the cheap offset wrapper is created each time. Never rebuild the box list inside getVoxelShape.

Coordinates are block-space, 0 to 1, not world coordinates. A box from 0.25 to 0.75 is the centered post of any block. withOffset(x, y, z) is what moves it to the actual position. Build templates in 0-1 space and never bake a world coordinate into a box.

Free upgrades: vanilla stairs and fences

Because the interfaces are implemented by mixin, the moment VoxelShapes is in the game, vanilla stairs and fences get real shapes with zero work on your part. Stairs are the perfect minimal example, the entire mixin is a base slab, four upper-half boxes, and a metadata lookup:

com/periut/retroapi/mixin/voxelshapes/StairsVoxelShapeMixin.java
package com.periut.retroapi.mixin.voxelshapes;

import com.periut.retroapi.voxelshapes.HasVoxelShape;
import com.periut.retroapi.voxelshapes.VoxelData;
import com.periut.retroapi.voxelshapes.VoxelShape;
import com.periut.retroapi.voxelshapes.VoxelBox;
import net.minecraft.block.StairsBlock;
import net.minecraft.world.World;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;

@Mixin(StairsBlock.class)
public class StairsVoxelShapeMixin implements HasVoxelShape {
    @Unique
    private static final VoxelData retroapi$BASE_VOXEL_DATA = new VoxelData(new VoxelBox(0, 0, 0, 1, 0.5, 1));
    @Unique
    private static final VoxelData[] retroapi$VOXEL_DATUM = new VoxelData[4];

    static {
        retroapi$VOXEL_DATUM[0] = retroapi$BASE_VOXEL_DATA.withBox(new VoxelBox(.5, .5, 0, 1, 1, 1)).preCache();
        retroapi$VOXEL_DATUM[1] = retroapi$BASE_VOXEL_DATA.withBox(new VoxelBox(0, .5, 0, .5, 1, 1)).preCache();
        retroapi$VOXEL_DATUM[2] = retroapi$BASE_VOXEL_DATA.withBox(new VoxelBox(0, .5, .5, 1, 1, 1)).preCache();
        retroapi$VOXEL_DATUM[3] = retroapi$BASE_VOXEL_DATA.withBox(new VoxelBox(0, .5, 0, 1, 1, .5)).preCache();
    }

    @Override
    public VoxelShape getVoxelShape(World world, int x, int y, int z) {
        int meta = world.getBlockMeta(x, y, z);
        if (meta < 0 || meta > 3) return null;
        return retroapi$VOXEL_DATUM[meta].withOffset(x, y, z);
    }
}

Walk it through, because this is the canonical recipe. retroapi$BASE_VOXEL_DATA is the bottom slab, the lower half of the stair, shared by all four facings. The static block builds four VoxelData, one per facing, each the base slab plus the upper-half box for that orientation, and calls preCache() on each so the outline edges are computed exactly once at class load. The four go into a static array. Then getVoxelShape reads the stair's metadata (0 to 3, the facing), bails to null if it is somehow out of range, indexes the right precomputed VoxelData, and calls withOffset to land it on this block. Build four shapes forever, index in, offset out. Now stairs select and raytrace as two stacked boxes, and you can mine the block tucked under the overhang.

Fences do exactly the same thing with more entries. There are 16 connection combinations (four neighbor directions, each present or not), so the fence mixin builds a 16-entry array of VoxelData, a center post plus one rail box per connected side, and a parallel 16-entry array for collision where every box is grown half a block taller with that add trick so the fence cannot be hopped. The query computes a 4-bit index from the four neighbors and offsets the matching template, the same index-in-offset-out shape as stairs.

The wall: connection-state-driven shapes

The stair indexed by four metadata values. The fence indexed by sixteen connection bits. The showcase wall is the full version of the idea: its shape is a pure function of its blockstate, the 162-state connection state you met in Chapter 14, and it caches one VoxelData per interned state in a map. Here is the entire voxel-shape section of ExampleWallBlock:

src/main/java/com/example/example_mod/ExampleWallBlock.java, voxel shapes
	// ------------------------------------------------------------ voxel shapes --
	// The wall's REAL shape: a box per visible piece, built from the same state the
	// models render from. The post box appears when up=true, and every connected
	// side contributes its own segment box (mirroring the wall_side model geometry,
	// 14px tall for low and 16px for tall segments). Selection outlines hug each
	// box, you can mine through the gaps, and entities collide with the true shape.
	// The collision variant adds the fence-style +0.5 height so walls can't be
	// hopped over, while the selection shape stays at the visual height.

	private static final VoxelBox POST = new VoxelBox(0.25, 0, 0.25, 0.75, 1, 0.75);
	private static final VoxelBox SIDE_NORTH_LOW = new VoxelBox(0.3125, 0, 0, 0.6875, 0.875, 0.5);
	private static final VoxelBox SIDE_NORTH_TALL = new VoxelBox(0.3125, 0, 0, 0.6875, 1, 0.5);
	private static final VoxelBox CLIMB_GUARD = new VoxelBox(0, 0, 0, 0, 0.5, 0);

	/** Shapes are pure functions of the state; cache one VoxelData per interned state. */
	private static final Map<RetroBlockState, VoxelData> SHAPES = new HashMap<>();
	private static final Map<RetroBlockState, VoxelData> COLLISION_SHAPES = new HashMap<>();

	private VoxelData shapeData(RetroBlockState state, boolean collision) {
		Map<RetroBlockState, VoxelData> cache = collision ? COLLISION_SHAPES : SHAPES;
		VoxelData cached = cache.get(state);
		if (cached != null) {
			return cached;
		}
		VoxelData data = new VoxelData(new VoxelBox[0]);
		if (state.get(UP)) {
			data = data.withBox(collision ? POST.add(CLIMB_GUARD) : POST);
		}
		data = withSide(data, state.get(NORTH), 0, collision);
		data = withSide(data, state.get(EAST), 1, collision);
		data = withSide(data, state.get(SOUTH), 2, collision);
		data = withSide(data, state.get(WEST), 3, collision);
		if (!collision) {
			data = data.preCache(); // outline edges computed once per state
		}
		cache.put(state, data);
		return data;
	}

	/** Adds one side segment box, rotated from the north template in 90 degree steps. */
	private static VoxelData withSide(VoxelData data, WallShape shape, int rotation, boolean collision) {
		if (shape == WallShape.NONE) {
			return data;
		}
		VoxelBox box = shape == WallShape.TALL ? SIDE_NORTH_TALL : SIDE_NORTH_LOW;
		for (int i = 0; i < rotation; i++) {
			box = rotateY(box);
		}
		if (collision) {
			box = box.add(CLIMB_GUARD);
		}
		return data.withBox(box);
	}

	/** Rotates a box 90 degrees clockwise about the block center (x,z) -> (1-z, x). */
	private static VoxelBox rotateY(VoxelBox box) {
		return new VoxelBox(1 - box.maxZ(), box.minY(), box.minX(), 1 - box.minZ(), box.maxY(), box.maxX());
	}

	@Override
	public VoxelShape getVoxelShape(World world, int x, int y, int z) {
		RetroBlockState state = RetroStates.get(world, x, y, z);
		if (state == null) {
			return null;
		}
		return shapeData(state, false).withOffset(x, y, z);
	}

	@Override
	public VoxelShape getCollisionVoxelShape(World world, int x, int y, int z) {
		RetroBlockState state = RetroStates.get(world, x, y, z);
		if (state == null) {
			return null;
		}
		return shapeData(state, true).withOffset(x, y, z);
	}

Read it as the same recipe, scaled up to a real state space. There is one VoxelBox per visible piece: the centered POST (a quarter-to-three-quarters pillar, full height), a low north segment (SIDE_NORTH_LOW, 14 pixels tall, the 0.875 on its maxY) and a tall north segment (SIDE_NORTH_TALL, the full 16 pixels), both matching the wall_side model geometry exactly. CLIMB_GUARD is the now-familiar half-block lift, zero on every axis except maxY.

shapeData is the cache. It keys a HashMap on the RetroBlockState itself, which is safe and fast because states are interned (Chapter 14): there is exactly one object per distinct state, so it is a perfect map key with identity hashing. On a cache miss it builds the shape from the state: a post box when up=true, then one segment box per connected side. It calls preCache() for the selection shape so the outline edges are computed once, stores it, and returns it. Every later query for that state is a single map lookup plus a cheap withOffset.

withSide is where the rotation lives. There is only one template, the north segment; the other three faces are that template spun by rotateY in 90-degree steps, exactly the way the wall's blockstate JSON y-rotates the single side model to face east, south, and west. The collision build path adds CLIMB_GUARD to every box (and to the post), which is why a wall is the unjumpable 1.5 blocks tall to an entity while still drawing its outline at the visual height. The two public methods are tiny: read the state, return null if there is none, otherwise pull the cached VoxelData and offset it onto the block.

The payoff in game is immediate. The selection outline hugs the wall's true silhouette, post plus whichever arms are connected, instead of snapping to a cube. You can mine a block hiding behind the gap of a wall corner, because the raytracer tests each box and finds the empty space between them. And entities collide with the real shape, stopped by the arms and the post, not by a phantom full cube. The connection state drives all of it, and the moment RetroStates.set reshapes the wall, the next query rebuilds (or re-fetches) the matching cached shape automatically.

The SHAPES map is bounded by the number of states the wall is actually placed in, not by 162. It fills lazily, one entry the first time each distinct connection state is queried, and every position sharing that state then shares the same precomputed boxes and outline edges.

How it hooks in

Your block code never touches collision math, the raytracer, the outline renderer, or the server's move check. Five mixins do all of it, keyed off the two interfaces, in package com.periut.retroapi.mixin.voxelshapes:

MixinInterceptsWhat it does with your shape
BlockVoxelShapeMixinBlock.addIntersectingBoundingBox and Block.raycastcollision: feeds your boxes (collision variant if present) into the entity-collision list. Raycast: tests each box's six faces and returns the nearest one hit, so the ray passes through the gaps and lands on whatever box is actually in the way
VoxelOutlineRendererMixin (client)WorldRenderer.renderBlockOutlinedraws the selection outline as tessellated per-edge lines from the precomputed edge list, each expanded slightly from the box center so it never z-fights with the block face
VoxelMoveCheckMixin (dedicated server)ServerPlayNetworkHandler.onPlayerMovereplaces the anti-cheat teleport-back check: verifies the player's move against your real boxes so a shaped block does not false-flag a legitimate movement and rubber-band the player
StairsVoxelShapeMixinStairsBlockimplements HasVoxelShape on vanilla stairs, for free
FenceVoxelShapeMixinFenceBlockimplements both interfaces on vanilla fences, with the taller collision variant, for free

The outline is drawn per edge, not per face, with the interior edges (where two boxes meet) already removed by the edge precompute, and every line is pushed about 0.002 out from the center so it floats just clear of the block surface and never flickers against it. The raycast walks your boxes and, for each, interpolates where the ray crosses each of the six faces, keeps only crossings that actually land inside the box, and picks the face nearest the ray's start, which is what lets you target a back box through the gap of a front one. The server move check rebuilds the player's bounding box, scans the blocks it overlaps, and accepts the move if it intersects any of your real boxes, so the shaped block participates in collision verification instead of being treated as a full cube the player is illegally inside.

None of those names appear in ExampleWallBlock. You implement getVoxelShape (and optionally getCollisionVoxelShape), build a cached VoxelData per shape, and offset it per query. The mixins find your interface and wire the rest.

What we built

A beta block had one box and lied about its shape. Now a block can carry a list of boxes that the outline, the raytracer, entity collision, and the dedicated server all honor: one interface for selection, an optional second when collision should stand taller. The recipe is always the same, build a VoxelData once per distinct shape, preCache its outline edges, and withOffset it onto the block per query, whether you index by metadata like the stairs, by connection bits like the fence, or by interned blockstate like the wall.

The wall now has its true silhouette, but it is still wearing no clothes: collision is code, the look is a model and a texture, and we keep mentioning the multipart blockstate JSON that turns those 162 states into post-plus-rotated-side geometry without drawing it. That, JSON block and item models, render layers, tints, and animated textures, all running on a 2011 game, is the last building chapter. The states of the wall came from Chapter 14; now let's give it pixels in Chapter 16.