Gap-free object rotation

- Rotations off the right angles fill every target cell from the nearest source block
- Source blocks that no target cell samples still place at their rotated position
- Right-angle rotations keep the direct transform
- Markers, canopy and tile data follow their resampled source blocks
- Datapack export bakes off-angle variants with the same resampler
- The unused rotateCopy paths are removed
- Placement goldens match the resampled output
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-31 19:23:25 -04:00
1 parent 2462330787
commit b0860f94fc
14 files changed
+870 -185

No files matched your search

@@ -6,6 +6,7 @@ import art.arcane.iris.pack.datapack.export.TemplateVolume;
import art.arcane.iris.pack.datapack.export.VanillaBlockState;
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
import art.arcane.iris.pack.datapack.export.compile.WeightedState;
import art.arcane.iris.structure.object.RotationResampler;
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
import java.util.ArrayList;
@@ -16,6 +17,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.ToIntFunction;
public final class ObjectVariantBaker {
private static final Set<String> AIR = Set.of("minecraft:air", "minecraft:cave_air", "minecraft:void_air");
@@ -35,6 +37,10 @@ public final class ObjectVariantBaker {
.comparingInt(Voxel::y)
.thenComparingInt(Voxel::x)
.thenComparingInt(Voxel::z);
private static final Comparator<Voxel> PLACEMENT_ORDER = Comparator
.comparingInt(Voxel::x)
.thenComparingInt(Voxel::y)
.thenComparingInt(Voxel::z);
public static String templateId(String namespace, String objectPath, VariantSpec spec, DataVersion target) {
VanillaResourceIdentifier.requireNamespace(namespace, "template namespace");
@@ -59,7 +65,7 @@ public final class ObjectVariantBaker {
sizeZ = scaledSize(sizeZ, spec.scale());
voxels = scale(voxels, spec.scale(), sizeX, sizeY, sizeZ);
}
voxels = anchor(voxels, spec.rotation(), sizeX / 2, sizeY / 2, sizeZ / 2);
voxels = anchor(templateId, voxels, spec.rotation(), sizeX / 2, sizeY / 2, sizeZ / 2);
if (spec.warp() != null) {
voxels = warp(voxels, spec.warp());
}
@@ -243,11 +249,19 @@ public final class ObjectVariantBaker {
return new ArrayList<>(scaled.values());
}
private static List<Voxel> anchor(List<Voxel> voxels, VariantSpec.Rotation rotation, int centerX, int centerY, int centerZ) {
private static List<Voxel> anchor(String templateId, List<Voxel> voxels, VariantSpec.Rotation rotation, int centerX,
int centerY, int centerZ) {
BlockStateRotation states = BlockStateRotation.of(rotation.x(), rotation.y(), rotation.z());
double[] lift = rotate(0, centerY, 0, rotation);
int rotatedCenterY = (int) Math.floor(lift[1]);
List<Voxel> sorted = new ArrayList<>(voxels);
if (!rotation.rightAngles()) {
sorted.sort(PLACEMENT_ORDER);
RotationResampler.Targets targets = RotationResampler.of(templateId, offsets(sorted, Voxel::x, centerX),
offsets(sorted, Voxel::y, centerY), offsets(sorted, Voxel::z, centerZ))
.resample(rotation.x(), rotation.y(), rotation.z());
return resampled(sorted, targets, states, rotatedCenterY);
}
sorted.sort(VOXEL_ORDER);
Map<Long, Voxel> anchored = new LinkedHashMap<>();
for (Voxel voxel : sorted) {
@@ -260,6 +274,35 @@ public final class ObjectVariantBaker {
return new ArrayList<>(anchored.values());
}
private static int[] offsets(List<Voxel> voxels, ToIntFunction<Voxel> axis, int center) {
int[] offsets = new int[voxels.size()];
for (int index = 0; index < offsets.length; index++) {
offsets[index] = axis.applyAsInt(voxels.get(index)) - center;
}
return offsets;
}
private static List<Voxel> resampled(List<Voxel> sources, RotationResampler.Targets targets, BlockStateRotation states,
int liftY) {
int[] x = targets.x();
int[] y = targets.y();
int[] z = targets.z();
int[] origins = targets.sources();
VanillaBlockState[] turned = new VanillaBlockState[sources.size()];
List<Voxel> anchored = new ArrayList<>(origins.length);
for (int index = 0; index < origins.length; index++) {
int origin = origins[index];
Voxel source = sources.get(origin);
VanillaBlockState state = turned[origin];
if (state == null) {
state = states.apply(source.state());
turned[origin] = state;
}
anchored.add(new Voxel(x[index], y[index] + liftY, z[index], state, source.nbt(), source.root()));
}
return anchored;
}
private static double[] rotate(double x, double y, double z, VariantSpec.Rotation rotation) {
double rx = x;
double ry = y;
@@ -44,6 +44,10 @@ public record VariantSpec(double scale, Rotation rotation, VariantWarp warp, dou
public boolean none() {
return x == 0 && y == 0 && z == 0;
}
public boolean rightAngles() {
return x % 90 == 0 && y % 90 == 0 && z % 90 == 0;
}
}
public static final class Builder {
@@ -386,10 +386,6 @@ public class IrisObject extends IrisRegistrant {
});
}
public IrisObject rotateCopy(IrisObjectRotation rt) {
return IrisObjectTransforms.rotateCopy(this, rt);
}
public IrisObject scaled(double scale, IrisObjectPlacementScaleInterpolator interpolation) {
return IrisObjectTransforms.scaled(this, scale, interpolation);
}
@@ -168,14 +168,6 @@ public class IrisObjectRotation {
return getRotation(spin, zAxis);
}
public IrisObject rotateCopy(IrisObject e) {
if (e == null) {
return null;
}
return e.rotateCopy(this);
}
public IrisBlockVector rotate(IrisBlockVector direction) {
return rotate(direction, 0, 0, 0);
}
@@ -43,72 +43,12 @@ import java.util.Map;
import java.util.Set;
/**
* Geometric transforms for {@link IrisObject}: rotation, scaling and the interpolated upscalers.
* Geometric transforms for {@link IrisObject}: scaling and the interpolated upscalers.
*/
final class IrisObjectTransforms {
private IrisObjectTransforms() {
}
static IrisObject rotateCopy(IrisObject self, IrisObjectRotation rt) {
IrisObject copy = self.copy();
rotate(copy, rt, 0, 0, 0);
return copy;
}
static void rotate(IrisObject self, IrisObjectRotation r, int spinx, int spiny, int spinz) {
self.writeLock.lock();
try {
VectorMap<NativeBlockState> d = new VectorMap<>();
VectorMap<TileData> dx = new VectorMap<>();
TreeGrounding grounding = self.getTreeGrounding();
Set<TreeBlockCanvas.Vec> canopy = grounding == null ? null : new HashSet<>();
for (Map.Entry<IrisBlockVector, NativeBlockState> entry : self.blocks) {
NativeBlockState rotated = r.rotate(entry.getValue(), spinx, spiny, spinz);
if (rotated == null) {
continue;
}
IrisBlockVector destination = r.rotate(entry.getKey(), spinx, spiny, spinz);
d.put(destination, rotated);
if (canopy != null) {
TreeBlockCanvas.Vec cell = new TreeBlockCanvas.Vec(destination.getBlockX(),
destination.getBlockY(), destination.getBlockZ());
IrisBlockVector source = entry.getKey();
if (grounding.isCanopy(source.getBlockX(), source.getBlockY(), source.getBlockZ())) {
canopy.add(cell);
} else {
canopy.remove(cell);
}
}
TileData tile = self.states.get(entry.getKey());
if (tile == null) {
dx.remove(destination);
} else {
dx.put(destination, tile);
}
}
self.blocks = d;
self.states = dx;
if (grounding != null) {
List<TreeGrounding.Root> roots = new ArrayList<>();
for (TreeGrounding.Root root : grounding.roots()) {
IrisBlockVector position = r.rotate(new IrisBlockVector(root.x(), root.y(), root.z()), spinx, spiny, spinz);
NativeBlockState state = d.get(position);
if (state != null) {
roots.add(new TreeGrounding.Root(position.getBlockX(), position.getBlockY(), position.getBlockZ(), state));
}
}
self.setTreeGrounding(TreeGrounding.create(roots, grounding.maxExtension(), canopy));
}
IrisObjectShaping.shrinkwrap(self);
self.surfaceSupportOffsets.reset();
self.floatingFootprint.reset();
} finally {
self.writeLock.unlock();
}
}
static IrisObject scaled(IrisObject self, double scale, IrisObjectPlacementScaleInterpolator interpolation) {
return scaled(self, scale, interpolation, ScaleOrigin.DEFAULT);
}
@@ -67,12 +67,12 @@ final class ObjectMarkerPlacement {
}
}
String[] resolved = new String[bake.count()];
String[] bySource = new String[bake.sourceCount()];
VectorMap<NativeBlockState>.Cursor cursor = blocks.cursor();
int index = 0;
while (index < resolved.length && cursor.next()) {
resolved[index++] = markers.get(cursor.key());
while (index < bySource.length && cursor.next()) {
bySource[index++] = markers.get(cursor.key());
}
return resolved;
return bake.markers(bySource);
}
}
@@ -8,6 +8,7 @@ import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
@@ -21,15 +22,25 @@ final class ObjectPlacementBake {
private final int[] offsetY;
private final int[] offsetZ;
private final int[] layerY;
private final short[] paletteIndexes;
private final long[] canopy;
private final int[] tileBlocks;
private final TileData[] tileValues;
private final int[] sources;
private volatile double[] exact;
private ObjectPlacementBake(Layout layout, Key key, int[] offsetX, int[] offsetY, int[] offsetZ, int[] layerY) {
private ObjectPlacementBake(Layout layout, Key key, Cells cells) {
this.layout = layout;
this.key = key;
this.offsetX = offsetX;
this.offsetY = offsetY;
this.offsetZ = offsetZ;
this.layerY = layerY;
this.offsetX = cells.offsetX();
this.offsetY = cells.offsetY();
this.offsetZ = cells.offsetZ();
this.layerY = cells.layerY();
this.paletteIndexes = cells.paletteIndexes();
this.canopy = cells.canopy();
this.tileBlocks = cells.tileBlocks();
this.tileValues = cells.tileValues();
this.sources = cells.sources();
}
static ObjectPlacementBake of(IrisObject object, SpinKernel spin, boolean ceilingHang, IrisBlockVector translateOffset) {
@@ -42,7 +53,10 @@ final class ObjectPlacementBake {
if (cached != null && cached.layout == layout) {
return cached;
}
ObjectPlacementBake built = bake(object, layout, key, spin, ceilingHang, translateOffset);
Cells cells = spin.axisAligned
? transformed(object, layout, spin, ceilingHang, translateOffset)
: resampled(object, layout, spin, ceilingHang, translateOffset);
ObjectPlacementBake built = new ObjectPlacementBake(layout, key, cells);
if (cached != null) {
variants.replace(key, cached, built);
} else if (variants.size() < MAXIMUM_VARIANTS_PER_OBJECT) {
@@ -54,7 +68,7 @@ final class ObjectPlacementBake {
}
}
private static ObjectPlacementBake bake(IrisObject object, Layout layout, Key key, SpinKernel spin, boolean ceilingHang,
private static Cells transformed(IrisObject object, Layout layout, SpinKernel spin, boolean ceilingHang,
IrisBlockVector translateOffset) {
int count = layout.count;
int[] x = new int[count];
@@ -66,7 +80,7 @@ final class ObjectPlacementBake {
VectorMap<NativeBlockState>.Cursor cursor = object.blocks.cursor();
int index = 0;
while (cursor.next() && index < count) {
transform(cursor.key(), rotated, spin, ceilingHang, translateOffset);
RotationResampler.transform(cursor.key(), rotated, spin, ceilingHang, translateOffset);
int roundedY = (int) Math.round(rotated.getY());
int floorY = rotated.getBlockY();
x[index] = (int) Math.round(rotated.getX());
@@ -79,24 +93,46 @@ final class ObjectPlacementBake {
if (index != count) {
throw new IllegalStateException("Object " + object.getLoadKey() + " changed while its placement bake was built");
}
return new ObjectPlacementBake(layout, key, x, y, z, layerDiffers ? layer : null);
return new Cells(x, y, z, layerDiffers ? layer : null, layout.paletteIndexes, layout.canopy, layout.tileBlocks,
layout.tileValues, null);
}
private static void transform(IrisBlockVector source, IrisBlockVector target, SpinKernel spin, boolean ceilingHang,
private static Cells resampled(IrisObject object, Layout layout, SpinKernel spin, boolean ceilingHang,
IrisBlockVector translateOffset) {
target.setX(source.getX());
target.setY(source.getY());
target.setZ(source.getZ());
spin.rotate(target);
if (ceilingHang) {
target.setY(-target.getBlockY());
RotationResampler.Targets targets = layout.resampler(object).resample(spin, ceilingHang, translateOffset);
return gather(layout, targets.x(), targets.y(), targets.z(), targets.sources());
}
if (translateOffset != null) {
target.add(translateOffset);
private static Cells gather(Layout layout, int[] x, int[] y, int[] z, int[] sources) {
int count = sources.length;
short[] paletteIndexes = new short[count];
long[] canopy = layout.canopy == null ? null : new long[(count + 63) >>> 6];
IntArrayList tileBlocks = layout.tileBlocks == null ? null : new IntArrayList();
List<TileData> tileValues = layout.tileBlocks == null ? null : new ArrayList<>();
for (int index = 0; index < count; index++) {
int source = sources[index];
paletteIndexes[index] = layout.paletteIndexes[source];
if (canopy != null && (layout.canopy[source >>> 6] & (1L << (source & 63))) != 0L) {
canopy[index >>> 6] |= 1L << (index & 63);
}
if (tileBlocks != null) {
int slot = Arrays.binarySearch(layout.tileBlocks, source);
if (slot >= 0) {
tileBlocks.add(index);
tileValues.add(layout.tileValues[slot]);
}
}
}
boolean tiled = tileBlocks != null && !tileBlocks.isEmpty();
return new Cells(x, y, z, null, paletteIndexes, canopy, tiled ? tileBlocks.toIntArray() : null,
tiled ? tileValues.toArray(new TileData[0]) : null, sources);
}
int count() {
return offsetX.length;
}
int sourceCount() {
return layout.count;
}
@@ -117,7 +153,7 @@ final class ObjectPlacementBake {
}
int paletteIndex(int index) {
return layout.paletteIndexes[index];
return paletteIndexes[index];
}
NativeBlockState[] palette() {
@@ -125,32 +161,51 @@ final class ObjectPlacementBake {
}
int[] tileBlocks() {
return layout.tileBlocks;
return tileBlocks;
}
TileData[] tileValues() {
return layout.tileValues;
return tileValues;
}
boolean canopy(int index) {
long[] canopy = layout.canopy;
return canopy != null && (canopy[index >>> 6] & (1L << (index & 63))) != 0L;
}
String[] markers(String[] sourceMarkers) {
if (sources == null) {
return sourceMarkers;
}
String[] unclaimed = sourceMarkers.clone();
String[] resolved = new String[sources.length];
for (int index = 0; index < sources.length; index++) {
int source = sources[index];
resolved[index] = unclaimed[source];
unclaimed[source] = null;
}
return resolved;
}
double[] exact(IrisObject object, SpinKernel spin) {
double[] resolved = exact;
if (resolved != null) {
return resolved;
}
resolved = sources == null ? transformedExact(object, spin) : targetExact();
exact = resolved;
return resolved;
}
private double[] transformedExact(IrisObject object, SpinKernel spin) {
int count = layout.count;
object.readLock.lock();
try {
resolved = new double[count * 3];
double[] resolved = new double[count * 3];
IrisBlockVector rotated = new IrisBlockVector(0, 0, 0);
VectorMap<NativeBlockState>.Cursor cursor = object.blocks.cursor();
int index = 0;
while (cursor.next() && index < count) {
transform(cursor.key(), rotated, spin, key.ceilingHang(), key.translateOffset());
RotationResampler.transform(cursor.key(), rotated, spin, key.ceilingHang(), key.translateOffset());
resolved[index * 3] = rotated.getX();
resolved[index * 3 + 1] = rotated.getY();
resolved[index * 3 + 2] = rotated.getZ();
@@ -159,10 +214,19 @@ final class ObjectPlacementBake {
if (index != count) {
throw new IllegalStateException("Object " + object.getLoadKey() + " changed while its placement bake was read");
}
return resolved;
} finally {
object.readLock.unlock();
}
exact = resolved;
}
private double[] targetExact() {
double[] resolved = new double[offsetX.length * 3];
for (int index = 0; index < offsetX.length; index++) {
resolved[index * 3] = offsetX[index];
resolved[index * 3 + 1] = offsetY[index];
resolved[index * 3 + 2] = offsetZ[index];
}
return resolved;
}
@@ -176,6 +240,7 @@ final class ObjectPlacementBake {
private final int[] tileBlocks;
private final TileData[] tileValues;
private final long[] canopy;
private volatile RotationResampler resampler;
private Layout(long blocksRevision, long statesRevision, TreeGrounding grounding, int count, short[] paletteIndexes,
NativeBlockState[] palette, int[] tileBlocks, TileData[] tileValues, long[] canopy) {
@@ -270,6 +335,15 @@ final class ObjectPlacementBake {
&& object.states.modificationRevision() == statesRevision
&& object.getTreeGrounding() == grounding;
}
private RotationResampler resampler(IrisObject object) {
RotationResampler resolved = resampler;
if (resolved == null) {
resolved = RotationResampler.of(object, count);
resampler = resolved;
}
return resolved;
}
}
record Key(boolean rotates, int xMode, int yMode, int zMode, long xCos, long xSin, long yCos, long ySin,
@@ -291,4 +365,8 @@ final class ObjectPlacementBake {
Double.longBitsToDouble(translateY), Double.longBitsToDouble(translateZ)) : null;
}
}
private record Cells(int[] offsetX, int[] offsetY, int[] offsetZ, int[] layerY, short[] paletteIndexes, long[] canopy,
int[] tileBlocks, TileData[] tileValues, int[] sources) {
}
}
@@ -0,0 +1,205 @@
package art.arcane.iris.structure.object;
import art.arcane.iris.generation.block.VectorMap;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import it.unimi.dsi.fastutil.ints.IntArrayList;
public final class RotationResampler {
private final int[] sourceX;
private final int[] sourceY;
private final int[] sourceZ;
private final Box sourceBox;
private final int[] sourceCells;
private RotationResampler(int[] sourceX, int[] sourceY, int[] sourceZ, Box sourceBox, int[] sourceCells) {
this.sourceX = sourceX;
this.sourceY = sourceY;
this.sourceZ = sourceZ;
this.sourceBox = sourceBox;
this.sourceCells = sourceCells;
}
public static RotationResampler of(String owner, int[] x, int[] y, int[] z) {
int count = x.length;
if (y.length != count || z.length != count) {
throw new IllegalArgumentException(owner + " has " + x.length + "/" + y.length + "/" + z.length
+ " source coordinates on its x/y/z axes");
}
if (count == 0) {
return new RotationResampler(x, y, z, new Box(0, 0, 0, 0, 0, 0), new int[1]);
}
int minX = Integer.MAX_VALUE;
int minY = Integer.MAX_VALUE;
int minZ = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE;
int maxY = Integer.MIN_VALUE;
int maxZ = Integer.MIN_VALUE;
for (int index = 0; index < count; index++) {
minX = Math.min(minX, x[index]);
minY = Math.min(minY, y[index]);
minZ = Math.min(minZ, z[index]);
maxX = Math.max(maxX, x[index]);
maxY = Math.max(maxY, y[index]);
maxZ = Math.max(maxZ, z[index]);
}
Box box = new Box(minX, maxX, minY, maxY, minZ, maxZ);
if (box.volume() > Integer.MAX_VALUE - 8) {
throw new IllegalStateException(owner + " spans " + (maxX - minX + 1) + "x" + (maxY - minY + 1) + "x"
+ (maxZ - minZ + 1) + " blocks, too large to resample for a non-right-angle rotation");
}
int[] cells = new int[(int) box.volume()];
for (int index = 0; index < count; index++) {
cells[(int) box.cell(x[index], y[index], z[index])] = index + 1;
}
return new RotationResampler(x, y, z, box, cells);
}
static RotationResampler of(IrisObject object, int count) {
int[] x = new int[count];
int[] y = new int[count];
int[] z = new int[count];
VectorMap<NativeBlockState>.Cursor cursor = object.blocks.cursor();
int index = 0;
while (index < count && cursor.next()) {
IrisBlockVector position = cursor.key();
x[index] = position.getBlockX();
y[index] = position.getBlockY();
z[index] = position.getBlockZ();
index++;
}
if (index != count) {
throw new IllegalStateException("Object " + object.getLoadKey() + " changed while its placement grid was built");
}
return of("Object " + object.getLoadKey(), x, y, z);
}
public Targets resample(double xDegrees, double yDegrees, double zDegrees) {
return resample(new SpinKernel(IrisObjectRotation.of(xDegrees, yDegrees, zDegrees), 0, 0, 0), false, null);
}
Targets resample(SpinKernel spin, boolean ceilingHang, IrisBlockVector translateOffset) {
int count = sourceX.length;
Box bounds = targetBounds(spin, ceilingHang, translateOffset);
long[] occupied = new long[(int) ((bounds.volume() + 63L) >>> 6)];
long[] sampled = new long[(count + 63) >>> 6];
IntArrayList x = new IntArrayList(count);
IntArrayList y = new IntArrayList(count);
IntArrayList z = new IntArrayList(count);
IntArrayList sources = new IntArrayList(count);
IrisBlockVector probe = new IrisBlockVector(0, 0, 0);
for (int targetX = bounds.minX(); targetX <= bounds.maxX(); targetX++) {
for (int targetY = bounds.minY(); targetY <= bounds.maxY(); targetY++) {
for (int targetZ = bounds.minZ(); targetZ <= bounds.maxZ(); targetZ++) {
probe.setX(targetX);
probe.setY(targetY);
probe.setZ(targetZ);
if (translateOffset != null) {
probe.subtract(translateOffset);
}
if (ceilingHang) {
probe.setY(-probe.getY());
}
spin.inverseRotate(probe);
int source = source((int) Math.round(probe.getX()), (int) Math.round(probe.getY()),
(int) Math.round(probe.getZ()));
if (source < 0) {
continue;
}
long cell = bounds.cell(targetX, targetY, targetZ);
occupied[(int) (cell >>> 6)] |= 1L << (cell & 63L);
sampled[source >>> 6] |= 1L << (source & 63);
x.add(targetX);
y.add(targetY);
z.add(targetZ);
sources.add(source);
}
}
}
for (int source = 0; source < count; source++) {
if ((sampled[source >>> 6] & (1L << (source & 63))) != 0L) {
continue;
}
probe.setX(sourceX[source]);
probe.setY(sourceY[source]);
probe.setZ(sourceZ[source]);
transform(probe, probe, spin, ceilingHang, translateOffset);
int targetX = Math.clamp(Math.round(probe.getX()), bounds.minX(), bounds.maxX());
int targetY = Math.clamp(Math.round(probe.getY()), bounds.minY(), bounds.maxY());
int targetZ = Math.clamp(Math.round(probe.getZ()), bounds.minZ(), bounds.maxZ());
long cell = bounds.cell(targetX, targetY, targetZ);
if ((occupied[(int) (cell >>> 6)] & (1L << (cell & 63L))) == 0L) {
occupied[(int) (cell >>> 6)] |= 1L << (cell & 63L);
x.add(targetX);
y.add(targetY);
z.add(targetZ);
sources.add(source);
}
}
return new Targets(x.toIntArray(), y.toIntArray(), z.toIntArray(), sources.toIntArray());
}
static void transform(IrisBlockVector source, IrisBlockVector target, SpinKernel spin, boolean ceilingHang,
IrisBlockVector translateOffset) {
target.setX(source.getX());
target.setY(source.getY());
target.setZ(source.getZ());
spin.rotate(target);
if (ceilingHang) {
target.setY(-target.getBlockY());
}
if (translateOffset != null) {
target.add(translateOffset);
}
}
private int source(int x, int y, int z) {
return sourceBox.contains(x, y, z) ? sourceCells[(int) sourceBox.cell(x, y, z)] - 1 : -1;
}
private Box targetBounds(SpinKernel spin, boolean ceilingHang, IrisBlockVector translateOffset) {
int minX = Integer.MAX_VALUE;
int minY = Integer.MAX_VALUE;
int minZ = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE;
int maxY = Integer.MIN_VALUE;
int maxZ = Integer.MIN_VALUE;
IrisBlockVector corner = new IrisBlockVector(0, 0, 0);
for (int index = 0; index < 8; index++) {
corner.setX((index & 1) == 0 ? sourceBox.minX() : sourceBox.maxX());
corner.setY((index & 2) == 0 ? sourceBox.minY() : sourceBox.maxY());
corner.setZ((index & 4) == 0 ? sourceBox.minZ() : sourceBox.maxZ());
transform(corner, corner, spin, ceilingHang, translateOffset);
int x = (int) Math.round(corner.getX());
int y = (int) Math.round(corner.getY());
int z = (int) Math.round(corner.getZ());
minX = Math.min(minX, x);
minY = Math.min(minY, y);
minZ = Math.min(minZ, z);
maxX = Math.max(maxX, x);
maxY = Math.max(maxY, y);
maxZ = Math.max(maxZ, z);
}
return new Box(minX, maxX, minY, maxY, minZ, maxZ);
}
public record Targets(int[] x, int[] y, int[] z, int[] sources) {
public int count() {
return sources.length;
}
}
private record Box(int minX, int maxX, int minY, int maxY, int minZ, int maxZ) {
private long volume() {
return (long) (maxX - minX + 1) * (maxY - minY + 1) * (maxZ - minZ + 1);
}
private boolean contains(int x, int y, int z) {
return x >= minX && x <= maxX && y >= minY && y <= maxY && z >= minZ && z <= maxZ;
}
private long cell(int x, int y, int z) {
return ((long) (x - minX) * (maxY - minY + 1) + (y - minY)) * (maxZ - minZ + 1) + (z - minZ);
}
}
}
@@ -17,8 +17,10 @@ final class SpinKernel {
static final int MODE_QUARTER = 2;
static final int MODE_THREE_QUARTER = 3;
static final int MODE_ANGLE = 4;
private static final double AXIS_ALIGNED_EPSILON = 1.0E-9D;
final boolean rotates;
final boolean axisAligned;
final int xMode;
final int yMode;
final int zMode;
@@ -44,6 +46,9 @@ final class SpinKernel {
zSin = Math.sin(zAngle);
yCos = Math.cos(yAngle);
ySin = Math.sin(yAngle);
axisAligned = Math.abs(xCos * xSin) < AXIS_ALIGNED_EPSILON
&& Math.abs(zCos * zSin) < AXIS_ALIGNED_EPSILON
&& Math.abs(yCos * ySin) < AXIS_ALIGNED_EPSILON;
}
private static int modeOf(IrisAxisRotationClamp clamp) {
@@ -149,4 +154,85 @@ final class SpinKernel {
}
}
}
void inverseRotate(IrisBlockVector v) {
if (!rotates) {
return;
}
switch (yMode) {
case MODE_FLIP -> {
v.setX(-v.getX());
v.setZ(-v.getZ());
}
case MODE_QUARTER -> {
double x = v.getX();
v.setX(-v.getZ());
v.setZ(x);
}
case MODE_THREE_QUARTER -> {
double x = v.getX();
v.setX(v.getZ());
v.setZ(-x);
}
case MODE_ANGLE -> {
double x = yCos * v.getX() - ySin * v.getZ();
double z = ySin * v.getX() + yCos * v.getZ();
v.setX(x);
v.setZ(z);
}
default -> {
}
}
switch (zMode) {
case MODE_FLIP -> {
v.setY(-v.getY());
v.setX(-v.getX());
}
case MODE_QUARTER -> {
double y = v.getY();
v.setY(-v.getX());
v.setX(y);
}
case MODE_THREE_QUARTER -> {
double y = v.getY();
v.setY(v.getX());
v.setX(-y);
}
case MODE_ANGLE -> {
double x = zCos * v.getX() + zSin * v.getY();
double y = -zSin * v.getX() + zCos * v.getY();
v.setX(x);
v.setY(y);
}
default -> {
}
}
switch (xMode) {
case MODE_FLIP -> {
v.setZ(-v.getZ());
v.setY(-v.getY());
}
case MODE_QUARTER -> {
double z = v.getZ();
v.setZ(-v.getY());
v.setY(z);
}
case MODE_THREE_QUARTER -> {
double z = v.getZ();
v.setZ(v.getY());
v.setY(-z);
}
case MODE_ANGLE -> {
double y = xCos * v.getY() + xSin * v.getZ();
double z = -xSin * v.getY() + xCos * v.getZ();
v.setY(y);
v.setZ(z);
}
default -> {
}
}
}
}
@@ -6,12 +6,15 @@ import art.arcane.iris.pack.datapack.export.TemplateBlock;
import art.arcane.iris.pack.datapack.export.TemplateVolume;
import art.arcane.iris.pack.datapack.export.VanillaBlockState;
import art.arcane.iris.pack.datapack.export.compile.WeightedState;
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
import org.junit.Test;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.junit.Assert.assertArrayEquals;
@@ -22,6 +25,8 @@ import static org.junit.Assert.assertTrue;
public class ObjectVariantBakerTest {
private static final String NAMESPACE = "iris_export";
private static final String LEAVES = "minecraft:oak_leaves[distance=1,persistent=true,waterlogged=false]";
private static final long RIGHT_ANGLE_GOLDEN = 0x5db739a5b940bf1dL;
@Test
public void variantBakingIsDeterministic() throws IOException {
@@ -245,6 +250,171 @@ public class ObjectVariantBakerTest {
assertEquals(2, lifted.roots().reach());
}
@Test
public void diagonalTurnsFillSolidCanopiesWithoutHoles() {
for (double degrees : new double[]{45, 135}) {
assertSolid(leafDisc(6, 3), degrees, (x, z) -> x * x + z * z <= 25D);
assertSolid(leafBox(4, 3), degrees, (x, z) -> Math.abs(x) <= 3.75D && Math.abs(z) <= 3.75D);
}
}
@Test
public void diagonalTurnsKeepEveryThinTrunkColumn() {
int[][] columns = {{2, 0}, {0, 2}, {-2, 0}, {3, 1}};
List<TemplateBlock> blocks = new ArrayList<>();
for (int[] column : columns) {
for (int y = 0; y < 6; y++) {
blocks.add(block(column[0] + 3, y, column[1] + 3, "minecraft:oak_log[axis=y]"));
}
}
TemplateVolume trunks = new TemplateVolume(7, 6, 7, blocks);
for (double degrees : new double[]{45, 135, 225, 315}) {
BakedVariant variant = turned(trunks, new VariantSpec.Rotation(0, degrees, 0));
Map<Long, int[]> heights = new HashMap<>();
for (TemplateBlock block : variant.volume().blocks()) {
int x = anchoredX(variant, block);
int z = anchoredZ(variant, block);
heights.computeIfAbsent(cell(x, 0, z), ignored -> new int[]{x, z, 0})[2]++;
}
double cos = Math.cos(Math.toRadians(degrees));
double sin = Math.sin(Math.toRadians(degrees));
for (int[] column : columns) {
long forward = cell((int) Math.round(cos * column[0] + sin * column[1]), 0,
(int) Math.round(-sin * column[0] + cos * column[1]));
boolean kept = false;
for (Map.Entry<Long, int[]> output : heights.entrySet()) {
int[] value = output.getValue();
assertEquals(degrees + " degrees cut a trunk column short", 6, value[2]);
boolean samples = (int) Math.round(cos * value[0] - sin * value[1]) == column[0]
&& (int) Math.round(sin * value[0] + cos * value[1]) == column[1];
kept |= samples || output.getKey() == forward;
}
assertTrue(degrees + " degrees lost the trunk column at " + column[0] + "," + column[1], kept);
}
}
}
@Test
public void rightAngleTurnsKeepTheirExactLayout() {
CompoundTag chest = new CompoundTag();
chest.putString("id", "minecraft:chest");
TemplateVolume volume = new TemplateVolume(4, 4, 4, List.of(
block(0, 0, 0, "minecraft:stone"),
block(3, 0, 1, "minecraft:stone"),
block(1, 1, 2, "minecraft:oak_log[axis=x]"),
block(2, 2, 0, "minecraft:oak_stairs[facing=north,half=bottom,shape=outer_left,waterlogged=false]"),
new TemplateBlock(3, 1, 3, VanillaBlockState.parse("minecraft:chest[facing=east,type=single,waterlogged=false]"), chest),
block(1, 3, 1, "minecraft:oak_leaves[distance=1,persistent=false,waterlogged=false]"),
block(0, 2, 3, "minecraft:oak_slab[type=bottom,waterlogged=false]")));
TreeRoots roots = new TreeRoots(List.of(block(0, 0, 0, "minecraft:stone")), 2);
StringBuilder layout = new StringBuilder();
for (double scale : new double[]{1, 1.5}) {
for (int x = 0; x < 360; x += 90) {
for (int y = 0; y < 360; y += 90) {
for (int z = 0; z < 360; z += 90) {
VariantSpec spec = VariantSpec.builder().scale(scale).rotation(new VariantSpec.Rotation(x, y, z)).build();
describe(layout, new ObjectVariantBaker().bake("iris_export:object/turn/a", new ObjectSource(volume, -1, roots), spec));
}
}
}
}
assertEquals(RIGHT_ANGLE_GOLDEN, fnv(layout));
}
private static void assertSolid(TemplateVolume source, double degrees, Interior interior) {
BakedVariant variant = turned(source, new VariantSpec.Rotation(0, degrees, 0));
Set<Long> covered = new HashSet<>();
for (TemplateBlock block : variant.volume().blocks()) {
assertTrue(degrees + " degrees wrote a cell twice",
covered.add(cell(anchoredX(variant, block), anchoredY(variant, block), anchoredZ(variant, block))));
}
double cos = Math.cos(Math.toRadians(degrees));
double sin = Math.sin(Math.toRadians(degrees));
int reach = Math.max(source.sizeX(), source.sizeZ());
for (int y = 0; y < source.sizeY(); y++) {
for (int x = -reach; x <= reach; x++) {
for (int z = -reach; z <= reach; z++) {
if (interior.contains(cos * x - sin * z, sin * x + cos * z)) {
assertTrue(degrees + " degrees left a hole at " + x + "," + y + "," + z, covered.contains(cell(x, y, z)));
}
}
}
}
}
private static BakedVariant turned(TemplateVolume source, VariantSpec.Rotation rotation) {
return new ObjectVariantBaker().bake("iris_export:object/turn/b", new ObjectSource(source, 0, TreeRoots.NONE),
VariantSpec.builder().rotation(rotation).build());
}
private static int anchoredX(BakedVariant variant, TemplateBlock block) {
return block.x() - (variant.volume().sizeX() - 1) / 2;
}
private static int anchoredY(BakedVariant variant, TemplateBlock block) {
return block.y() + variant.baseY();
}
private static int anchoredZ(BakedVariant variant, TemplateBlock block) {
return block.z() - (variant.volume().sizeZ() - 1) / 2;
}
private static TemplateVolume leafDisc(int radius, int height) {
List<TemplateBlock> blocks = new ArrayList<>();
for (int y = 0; y < height; y++) {
for (int x = -radius; x <= radius; x++) {
for (int z = -radius; z <= radius; z++) {
if (x * x + z * z <= radius * radius) {
blocks.add(block(x + radius, y, z + radius, LEAVES));
}
}
}
}
return new TemplateVolume(radius * 2 + 1, height, radius * 2 + 1, blocks);
}
private static TemplateVolume leafBox(int half, int height) {
List<TemplateBlock> blocks = new ArrayList<>();
for (int y = 0; y < height; y++) {
for (int x = -half; x <= half; x++) {
for (int z = -half; z <= half; z++) {
blocks.add(block(x + half, y, z + half, LEAVES));
}
}
}
return new TemplateVolume(half * 2 + 1, height, half * 2 + 1, blocks);
}
private static void describe(StringBuilder layout, BakedVariant variant) {
TemplateVolume volume = variant.volume();
layout.append(variant.id()).append(' ').append(volume.sizeX()).append('x').append(volume.sizeY()).append('x')
.append(volume.sizeZ()).append(" base=").append(variant.baseY()).append('\n');
for (TemplateBlock block : volume.blocks()) {
layout.append(block.x()).append(',').append(block.y()).append(',').append(block.z()).append(' ')
.append(block.state().canonical()).append(' ')
.append(block.nbt() == null ? "-" : block.nbt().getString("id")).append('\n');
}
layout.append("roots reach=").append(variant.roots().reach()).append('\n');
for (TemplateBlock cell : variant.roots().cells()) {
layout.append(cell.x()).append(',').append(cell.y()).append(',').append(cell.z()).append(' ')
.append(cell.state().canonical()).append('\n');
}
}
private static long fnv(CharSequence text) {
long hash = 0xcbf29ce484222325L;
for (int index = 0; index < text.length(); index++) {
hash ^= text.charAt(index);
hash *= 0x100000001b3L;
}
return hash;
}
private static long cell(int x, int y, int z) {
return (((long) x & 0x1FFFFFL) << 42) | (((long) y & 0x1FFFFFL) << 21) | ((long) z & 0x1FFFFFL);
}
private static TemplateVolume tree() {
List<TemplateBlock> blocks = new ArrayList<>();
for (int y = 0; y < 5; y++) {
@@ -271,4 +441,8 @@ public class ObjectVariantBakerTest {
}
return states;
}
private interface Interior {
boolean contains(double x, double z);
}
}
@@ -56,66 +56,6 @@ public class IrisObjectTransformsTileTest {
IrisPlatforms.unbind();
}
@Test
public void rotationOmitsUnsupportedBlocksAndTheirTiles() {
IrisObject source = new IrisObject(3, 1, 1);
NativeBlockState unsupported = state("minecraft:wall_torch");
source.setUnsigned(0, 0, 0, unsupported);
source.setUnsignedTile(0, 0, 0, tile("minecraft:wall_torch", "omitted"));
source.setUnsigned(1, 0, 0, state("minecraft:chest"));
source.setUnsignedTile(1, 0, 0, tile("minecraft:chest", "kept"));
source.setUnsigned(2, 0, 0, state("minecraft:stone"));
IrisObjectRotation.StateRotator previous = IrisObjectRotation.bindPlatformRotator(
(rotation, block, x, y, z) -> block == unsupported ? null : block);
IrisObject rotated;
try {
rotated = source.rotateCopy(IrisObjectRotation.of(0, 90, 0));
} finally {
IrisObjectRotation.restorePlatformRotator(previous);
}
assertEquals(2, rotated.getBlocks().size());
assertEquals(1, rotated.getStates().size());
for (Map.Entry<IrisBlockVector, TileData> entry : rotated.getStates()) {
assertEquals("minecraft:chest", rotated.getBlocks().get(entry.getKey()).materialKey());
assertEquals("kept", entry.getValue().getProperties().get("name"));
}
assertEquals(3, source.getBlocks().size());
assertEquals(2, source.getStates().size());
}
@Test
public void fractionalRotationKeepsTileDataOnlyForTheWinningBlock() {
IrisObject source = new IrisObject(9, 1, 9);
for (int x = 0; x < 9; x++) {
for (int z = 0; z < 9; z++) {
boolean chest = (x + z) % 2 == 0;
source.setUnsigned(x, 0, z, state(chest ? "minecraft:chest" : "minecraft:stone"));
if (chest) {
source.setUnsignedTile(x, 0, z, tile("minecraft:chest", x + ":" + z));
}
}
}
IrisObjectRotation.StateRotator previous = IrisObjectRotation.bindPlatformRotator(
(rotation, block, x, y, z) -> block);
IrisObject rotated;
try {
rotated = source.rotateCopy(IrisObjectRotation.of(0, 45, 0));
} finally {
IrisObjectRotation.restorePlatformRotator(previous);
}
assertTrue(rotated.getBlocks().size() < source.getBlocks().size());
for (Map.Entry<IrisBlockVector, TileData> entry : rotated.getStates()) {
assertEquals("minecraft:chest", rotated.getBlocks().get(entry.getKey()).materialKey());
}
for (Map.Entry<IrisBlockVector, NativeBlockState> entry : rotated.getBlocks()) {
if (entry.getValue().materialKey().equals("minecraft:chest")) {
assertNotNull(rotated.getStates().get(entry.getKey()));
}
}
}
@Test
public void scalingWaitsForMatchingGeometryAndVolume() throws Exception {
assertConsistentScaling(false);
@@ -1,11 +1,23 @@
package art.arcane.iris.structure.object;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import art.arcane.iris.pack.datapack.export.TemplateBlock;
import art.arcane.iris.pack.datapack.export.TemplateVolume;
import art.arcane.iris.pack.datapack.export.VanillaBlockState;
import art.arcane.iris.pack.datapack.export.compile.object.BakedVariant;
import art.arcane.iris.pack.datapack.export.compile.object.ObjectSource;
import art.arcane.iris.pack.datapack.export.compile.object.ObjectVariantBaker;
import art.arcane.iris.pack.datapack.export.compile.object.TreeRoots;
import art.arcane.iris.pack.datapack.export.compile.object.VariantSpec;
import art.arcane.iris.testsupport.KeyedBlockState;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
@@ -91,7 +103,11 @@ public class ObjectPlacementBakeTest {
distinct.add(ObjectPlacementBake.Key.of(spin, false, translate));
assertSame(first.palette(), bake.palette());
assertTrue(object.placementBakes.size() <= 32);
assertTransformed(object, spin, translate, bake);
if (spin.axisAligned) {
assertTransformed(object, spin, false, translate, bake);
} else {
assertResampled(object, spin, translate, bake);
}
if (placement == 2000) {
settled = new HashMap<>(object.placementBakes);
}
@@ -104,13 +120,240 @@ public class ObjectPlacementBakeTest {
}
}
private static void assertTransformed(IrisObject object, SpinKernel spin, IrisBlockVector translate, ObjectPlacementBake bake) {
@Test
public void diagonalYawTurnsFillEverySolidInteriorCellExactlyOnce() {
IrisObject disc = leafDisc(6, 3);
IrisObject box = leafBox(4, 3);
IrisObjectRotation diagonal = yawRotation(45D);
for (int degrees : new int[]{45, 135}) {
SpinKernel spin = new SpinKernel(diagonal, 0, degrees, 0);
assertSolidCoverage(ObjectPlacementBake.of(disc, spin, false, null), degrees, 6, 3,
(x, z) -> x * x + z * z <= 25D);
assertSolidCoverage(ObjectPlacementBake.of(box, spin, false, null), degrees, 6, 3,
(x, z) -> Math.abs(x) <= 3.75D && Math.abs(z) <= 3.75D);
}
}
@Test
public void diagonalYawTurnsKeepEveryThinTrunkColumn() {
IrisObject object = new IrisObject(7, 6, 7);
KeyedBlockState log = new KeyedBlockState("minecraft:oak_log[axis=y]");
int[][] columns = {{2, 0}, {0, 2}, {-2, 0}, {3, 1}};
for (int[] column : columns) {
for (int y = 0; y < 6; y++) {
object.getBlocks().put(new IrisBlockVector(column[0], y, column[1]), log);
}
}
IrisObjectRotation diagonal = yawRotation(45D);
for (int degrees : new int[]{45, 135, 225, 315}) {
SpinKernel spin = new SpinKernel(diagonal, 0, degrees, 0);
ObjectPlacementBake bake = ObjectPlacementBake.of(object, spin, false, null);
Set<Long> covered = new HashSet<>();
Set<Long> sampled = new HashSet<>();
for (int index = 0; index < bake.count(); index++) {
assertTrue(covered.add(cell(bake.offsetX(index), bake.offsetY(index), bake.offsetZ(index))));
IrisBlockVector inverse = new IrisBlockVector(bake.offsetX(index), bake.offsetY(index), bake.offsetZ(index));
spin.inverseRotate(inverse);
sampled.add(cell((int) Math.round(inverse.getX()), (int) Math.round(inverse.getY()), (int) Math.round(inverse.getZ())));
}
for (IrisBlockVector source : object.getBlocks().keys()) {
IrisBlockVector forward = source.clone();
spin.rotate(forward);
boolean represented = sampled.contains(cell(source.getBlockX(), source.getBlockY(), source.getBlockZ()))
|| covered.contains(cell((int) Math.round(forward.getX()), (int) Math.round(forward.getY()),
(int) Math.round(forward.getZ())));
assertTrue(degrees + " degrees dropped trunk block " + source, represented);
}
}
}
@Test
public void quarterTurnsKeepTheForwardTransform() {
IrisObject object = leafDisc(6, 3);
object.getBlocks().put(new IrisBlockVector(7, 1, -2), new KeyedBlockState("minecraft:oak_log[axis=x]"));
IrisBlockVector translate = new IrisBlockVector(1.5D, -2D, 0.25D);
IrisObjectRotation[] rotations = {new IrisObjectRotation(), yawRotation(45D)};
for (IrisObjectRotation rotation : rotations) {
for (int degrees = 0; degrees < 360; degrees += 90) {
SpinKernel spin = new SpinKernel(rotation, 0, degrees, 0);
assertTransformed(object, spin, false, null, ObjectPlacementBake.of(object, spin, false, null));
assertTransformed(object, spin, true, translate, ObjectPlacementBake.of(object, spin, true, translate));
}
}
for (int degrees = 90; degrees < 360; degrees += 90) {
SpinKernel spin = new SpinKernel(IrisObjectRotation.of(0D, degrees, 0D), 0, 0, 0);
assertTransformed(object, spin, false, null, ObjectPlacementBake.of(object, spin, false, null));
}
}
@Test
public void datapackVariantsMatchPlacementAtNonRightAngles() {
IrisObject object = new IrisObject(13, 5, 13);
KeyedBlockState leaves = new KeyedBlockState("minecraft:oak_leaves[distance=1,persistent=true]");
for (int y = 0; y <= 2; y++) {
for (int x = -6; x <= 6; x++) {
for (int z = -6; z <= 6; z++) {
if (x * x + z * z <= 36) {
object.getBlocks().put(new IrisBlockVector(x, y, z), leaves);
}
}
}
}
for (int y = -2; y <= 1; y++) {
object.getBlocks().put(new IrisBlockVector(0, y, 0), new KeyedBlockState("minecraft:oak_log[axis=y]"));
object.getBlocks().put(new IrisBlockVector(4, y, -3), new KeyedBlockState("minecraft:birch_log[axis=y]"));
}
object.getBlocks().put(new IrisBlockVector(-5, 2, 2), new KeyedBlockState("minecraft:gold_block"));
object.getBlocks().put(new IrisBlockVector(2, -2, 5), new KeyedBlockState("minecraft:stone"));
List<TemplateBlock> blocks = new ArrayList<>();
for (IrisBlockVector key : object.getBlocks().keys()) {
blocks.add(new TemplateBlock(key.getBlockX() + object.getCenter().getX(), key.getBlockY() + object.getCenter().getY(),
key.getBlockZ() + object.getCenter().getZ(), VanillaBlockState.parse(object.getBlocks().get(key).key()), null));
}
ObjectSource source = new ObjectSource(new TemplateVolume(object.getW(), object.getH(), object.getD(), blocks), 0,
TreeRoots.NONE);
double[][] angles = {{0D, 45D, 0D}, {0D, 135D, 0D}, {0D, 30D, 0D}, {20D, 45D, 0D}, {0D, 60D, 15D}};
for (double[] angle : angles) {
IrisObjectRotation rotation = IrisObjectRotation.of(angle[0], angle[1], angle[2]);
SpinKernel spin = new SpinKernel(rotation, 0, 0, 0);
ObjectPlacementBake bake = ObjectPlacementBake.of(object, spin, false, null);
int lift = rotation.rotate(new IrisBlockVector(0, object.getCenter().getY(), 0), 0, 0, 0).getBlockY();
Set<String> placed = new HashSet<>();
for (int index = 0; index < bake.count(); index++) {
String key = bake.palette()[bake.paletteIndex(index)].key();
placed.add(bake.offsetX(index) + "," + (bake.offsetY(index) + lift) + "," + bake.offsetZ(index) + " "
+ key.substring(0, key.indexOf('[') < 0 ? key.length() : key.indexOf('[')));
}
BakedVariant variant = new ObjectVariantBaker().bake("iris_export:object/parity/a", source,
VariantSpec.builder().rotation(new VariantSpec.Rotation(angle[0], angle[1], angle[2])).build());
Set<String> exported = new HashSet<>();
TemplateVolume volume = variant.volume();
for (TemplateBlock block : volume.blocks()) {
exported.add((block.x() - (volume.sizeX() - 1) / 2) + "," + (block.y() + variant.baseY()) + ","
+ (block.z() - (volume.sizeZ() - 1) / 2) + " " + block.state().name());
}
assertEquals(Arrays.toString(angle), placed, exported);
}
}
private static void assertResampled(IrisObject object, SpinKernel spin, IrisBlockVector translate, ObjectPlacementBake bake) {
double[] exact = bake.exact(object, spin);
Set<Long> forward = new HashSet<>();
for (IrisBlockVector source : object.getBlocks().keys()) {
IrisBlockVector moved = source.clone();
spin.rotate(moved);
moved.add(translate);
forward.add(cell((int) Math.round(moved.getX()), (int) Math.round(moved.getY()), (int) Math.round(moved.getZ())));
}
Set<Long> covered = new HashSet<>();
Set<Long> sampled = new HashSet<>();
for (int index = 0; index < bake.count(); index++) {
long target = cell(bake.offsetX(index), bake.offsetY(index), bake.offsetZ(index));
assertTrue(covered.add(target));
IrisBlockVector inverse = new IrisBlockVector(bake.offsetX(index), bake.offsetY(index), bake.offsetZ(index));
inverse.subtract(translate);
spin.inverseRotate(inverse);
IrisBlockVector nearest = new IrisBlockVector((int) Math.round(inverse.getX()), (int) Math.round(inverse.getY()),
(int) Math.round(inverse.getZ()));
NativeBlockState state = bake.palette()[bake.paletteIndex(index)];
NativeBlockState nearestState = object.getBlocks().get(nearest);
if (nearestState != null) {
assertSame(nearestState, state);
sampled.add(cell(nearest.getBlockX(), nearest.getBlockY(), nearest.getBlockZ()));
} else {
assertTrue(forward.contains(target));
}
assertEquals(bake.offsetY(index), bake.layerY(index));
assertEquals(bake.offsetX(index), exact[index * 3], 0D);
assertEquals(bake.offsetY(index), exact[index * 3 + 1], 0D);
assertEquals(bake.offsetZ(index), exact[index * 3 + 2], 0D);
}
for (IrisBlockVector source : object.getBlocks().keys()) {
IrisBlockVector moved = source.clone();
spin.rotate(moved);
moved.add(translate);
assertTrue(sampled.contains(cell(source.getBlockX(), source.getBlockY(), source.getBlockZ()))
|| covered.contains(cell((int) Math.round(moved.getX()), (int) Math.round(moved.getY()),
(int) Math.round(moved.getZ()))));
}
}
private static void assertSolidCoverage(ObjectPlacementBake bake, int degrees, int radius, int height, Interior interior) {
Map<Long, Integer> covered = new HashMap<>();
for (int index = 0; index < bake.count(); index++) {
covered.merge(cell(bake.offsetX(index), bake.offsetY(index), bake.offsetZ(index)), 1, Integer::sum);
}
double cos = Math.cos(Math.toRadians(degrees));
double sin = Math.sin(Math.toRadians(degrees));
int reach = radius * 2;
for (int y = 0; y < height; y++) {
for (int x = -reach; x <= reach; x++) {
for (int z = -reach; z <= reach; z++) {
int writes = covered.getOrDefault(cell(x, y, z), 0);
assertTrue(degrees + " degrees wrote cell " + x + "," + y + "," + z + " " + writes + " times", writes <= 1);
if (interior.contains(cos * x - sin * z, sin * x + cos * z)) {
assertEquals(degrees + " degrees left a hole at " + x + "," + y + "," + z, 1, writes);
}
}
}
}
}
private static long cell(int x, int y, int z) {
return (((long) x & 0x1FFFFFL) << 42) | (((long) y & 0x1FFFFFL) << 21) | ((long) z & 0x1FFFFFL);
}
private static IrisObjectRotation yawRotation(double interval) {
IrisObjectRotation rotation = new IrisObjectRotation();
rotation.setYAxis(new IrisAxisRotationClamp(true, false, 0D, 0D, interval));
return rotation;
}
private static IrisObject leafDisc(int radius, int height) {
IrisObject object = new IrisObject(radius * 2 + 1, height, radius * 2 + 1);
KeyedBlockState leaves = new KeyedBlockState("minecraft:oak_leaves[distance=1,persistent=true]");
for (int y = 0; y < height; y++) {
for (int x = -radius; x <= radius; x++) {
for (int z = -radius; z <= radius; z++) {
if (x * x + z * z <= radius * radius) {
object.getBlocks().put(new IrisBlockVector(x, y, z), leaves);
}
}
}
}
return object;
}
private static IrisObject leafBox(int half, int height) {
IrisObject object = new IrisObject(half * 2 + 1, height, half * 2 + 1);
KeyedBlockState leaves = new KeyedBlockState("minecraft:oak_leaves[distance=1,persistent=true]");
for (int y = 0; y < height; y++) {
for (int x = -half; x <= half; x++) {
for (int z = -half; z <= half; z++) {
object.getBlocks().put(new IrisBlockVector(x, y, z), leaves);
}
}
}
return object;
}
private interface Interior {
boolean contains(double x, double z);
}
private static void assertTransformed(IrisObject object, SpinKernel spin, boolean ceilingHang, IrisBlockVector translate,
ObjectPlacementBake bake) {
double[] exact = bake.exact(object, spin);
int index = 0;
for (IrisBlockVector source : object.getBlocks().keys()) {
IrisBlockVector expected = source.clone();
spin.rotate(expected);
if (ceilingHang) {
expected.setY(-expected.getBlockY());
}
if (translate != null) {
expected.add(translate);
}
assertEquals((int) Math.round(expected.getX()), bake.offsetX(index));
assertEquals((int) Math.round(expected.getY()), bake.offsetY(index));
assertEquals((int) Math.round(expected.getZ()), bake.offsetZ(index));
@@ -63,9 +63,9 @@ public class ObjectPlacementGoldenTest {
private static final int OBJECTS = 200;
private static final int SEEDS = 5;
private static final long GOLDEN = 0x0b870e7696a055b0L;
private static final long MARKER_GOLDEN = 0x60b4bbf1144b239cL;
private static final long HIGH_VARIANT_GOLDEN = 0x998044b16f7f1f6fL;
private static final long GOLDEN = 0x930e130dc51b6f67L;
private static final long MARKER_GOLDEN = 0x67ae55d574c3894eL;
private static final long HIGH_VARIANT_GOLDEN = 0xdd61791b0f7e4d37L;
private static final List<String> PALETTE = List.of(
"minecraft:stone",
@@ -297,21 +297,6 @@ public class TreeGroundingPlacementTest {
}
}
@Test
public void rotatedCopiesKeepMetadataOnTheirActualRootBlocks() {
IrisObject source = tree(true);
Map<Position, NativeBlockState> original = snapshot(source);
TreeGrounding grounding = source.getTreeGrounding();
for (int angle : new int[]{45, 90, 180, 270}) {
IrisObject rotated = source.rotateCopy(IrisObjectRotation.of(0, angle, 0));
assertGroundingMatchesBlocks(rotated);
assertEquals(3, rotated.getTreeGrounding().maxExtension());
assertEquals(original, snapshot(source));
assertSame(grounding, source.getTreeGrounding());
}
}
@Test
public void shrinkwrapMovesRootMetadataAndInvalidatesItsFootprint() {
IrisObject source = tree(true);
@@ -397,7 +382,6 @@ public class TreeGroundingPlacementTest {
IrisObject shrunk = source.copy();
shrunk.shrinkwrap();
for (IrisObject transformed : List.of(source.copy(), shrunk,
source.rotateCopy(IrisObjectRotation.of(0, 90, 0)),
source.scaled(2, IrisObjectPlacementScaleInterpolator.NONE),
source.scaledAroundOrigin(2, IrisObjectPlacementScaleInterpolator.NONE))) {
assertGroundingMatchesBlocks(transformed);