-
Notifications
You must be signed in to change notification settings - Fork 1
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 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
);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);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.
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.
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 caseappendsshapeonly whenconditionis true.caseis an infix function onVoxelShapewithin the builder, sox case flagreads 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);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.
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.