Skip to content

Combining Shapes

Mystery2099 edited this page Sep 18, 2026 · 3 revisions

Combining shapes

Most custom block shapes are just several boxes joined together. VoxLib gives you a few ways to do that, ranging from the + operator to a small conditional builder. Use whichever one keeps the shape easiest to understand later.

The + operator

The + operator unions two shapes:

import com.github.mystery2099.voxlib.combination.VoxelAssembly.createCuboidShape
import com.github.mystery2099.voxlib.combination.VoxelAssembly.plus

val base = createCuboidShape(0, 0, 0, 16, 1, 16)
val post = createCuboidShape(7, 1, 7, 9, 15, 9)
val top  = createCuboidShape(6, 15, 6, 10, 16, 10)

val tableShape = base + post + top

+ returns a new shape. += also works because plus is a Kotlin operator function; it rebinds the variable to the new shape and does not mutate the original.

Importing VoxelAssembly.plus brings the operator into scope. If you import com.github.mystery2099.voxlib.combination.VoxelAssembly.*, all extensions come with it.

Java has no operator overloading. VoxLib's API lives in a Kotlin object, so call it through INSTANCE:

VoxelShape tableShape = VoxelAssembly.INSTANCE.plus(
    VoxelAssembly.INSTANCE.plus(base, post),
    top
);

union for many shapes

When you already have several shapes, VoxelAssembly.union(vararg shapes) is cleaner than a long chain of + calls. It skips empty inputs and combines large collections in a balanced order, which keeps the intermediate shapes smaller and cheaper.

import com.github.mystery2099.voxlib.combination.VoxelAssembly.union

val fence = union(
    CommonShapes.createFencePost(),
    CommonShapes.createFenceConnections(north = true, south = true)
)

unifyWith extends VoxelShape and takes the other shapes as varargs. Spread an existing collection into the call:

val combined = firstShape.unifyWith(*rest.toTypedArray())

Empty and single-element cases return sensible values: an empty vararg returns Shapes.empty(), and one shape returns it unchanged.

Java:

VoxelShape combined = VoxelAssembly.INSTANCE.union(a, b, c);

Other BooleanOp combinations

VoxelAssembly.combine(function, shapes) folds shapes with any BooleanOp, including OR, AND, ONLY_FIRST, and ONLY_SECOND. Union is so common that union exists separately for it, with empty-shape skipping and balanced combination. Use combine for the other Boolean operations.

import com.github.mystery2099.voxlib.combination.VoxelAssembly.combine
import net.minecraft.world.phys.shapes.BooleanOp

val overlapOnly = combine(BooleanOp.AND, shapeA, shapeB)

Note that combine uses Shapes.joinUnoptimized, matching vanilla's raw behavior. For the common union case, prefer + or union.

The and alias (deprecated)

Older code may use shape and otherShape. This was always a union, but the name read like intersection, so current versions deprecate it in favor of +. Its behavior is unchanged. Replace a and b with a + b.

Conditional assembly with appendShapes

When parts of a shape depend on blockstate properties, ordinary if statements can make the assembly code noisy. appendShapes keeps those conditions next to the boxes they control:

import com.github.mystery2099.voxlib.combination.VoxelAssembly.appendShapes
import com.github.mystery2099.voxlib.combination.VoxelAssembly.createCuboidShape
import net.minecraft.world.phys.shapes.VoxelShape

fun chairShape(hasBackrest: Boolean): VoxelShape {
    val seat = createCuboidShape(2, 8, 2, 14, 10, 14)
    val legs = createCuboidShape(3, 0, 3, 13, 8, 13)

    return seat appendShapes {
        createCuboidShape(3, 10, 12, 13, 16, 14) case hasBackrest
        append(legs)
    }
}

Inside the lambda:

  • shape condition case appends shape only when condition is true. case is an infix function on VoxelShape within the builder, so x case flag reads like a label.
  • append(shape) appends unconditionally; append(shape, condition) takes an optional condition.
  • The result is the accumulated union.

The receiver of appendShapes (here seat) is the starting shape, not the first operand of the first append; append and case union onto it.

One edge case is worth calling out: on 1.21.1, unions with a full cube preserve geometry outside the block bounds. Earlier releases discarded that extra geometry when a full cube participated in the union.

Java cannot use the DSL, which relies on Kotlin receiver lambdas. Build the same shape with explicit conditionals:

VoxelAssembly voxels = VoxelAssembly.INSTANCE;
VoxelShape seat = voxels.createCuboidShape(2, 8, 2, 14, 10, 14);
VoxelShape legs = voxels.createCuboidShape(3, 0, 3, 13, 8, 13);
VoxelShape chair = hasBackrest
    ? voxels.union(seat, legs, voxels.createCuboidShape(3, 10, 12, 13, 16, 14))
    : voxels.union(seat, legs);

Empty shapes

All VoxLib union entry points short-circuit empties: unioning with Shapes.empty() returns the other shape unchanged, so an empty operand never produces a wasted computation or a cache entry.

Performance notes

Unions go through the bounded cache described in Caching and performance. The practical version is simple: repeating a union with the same shape instances can be fast, but a fixed block shape should still be built once and stored in a constant.

Clone this wiki locally