mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-11-01 08:50:06 +00:00
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:
1 parent
2462330787
commit
b0860f94fc
14 files changed
+874
-189
No files matched your search
+45
-2
@@ -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.VanillaBlockState;
|
||||||
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
|
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
|
||||||
import art.arcane.iris.pack.datapack.export.compile.WeightedState;
|
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 art.arcane.volmlib.util.nbt.tag.CompoundTag;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -16,6 +17,7 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
|
import java.util.function.ToIntFunction;
|
||||||
|
|
||||||
public final class ObjectVariantBaker {
|
public final class ObjectVariantBaker {
|
||||||
private static final Set<String> AIR = Set.of("minecraft:air", "minecraft:cave_air", "minecraft:void_air");
|
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)
|
.comparingInt(Voxel::y)
|
||||||
.thenComparingInt(Voxel::x)
|
.thenComparingInt(Voxel::x)
|
||||||
.thenComparingInt(Voxel::z);
|
.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) {
|
public static String templateId(String namespace, String objectPath, VariantSpec spec, DataVersion target) {
|
||||||
VanillaResourceIdentifier.requireNamespace(namespace, "template namespace");
|
VanillaResourceIdentifier.requireNamespace(namespace, "template namespace");
|
||||||
@@ -59,7 +65,7 @@ public final class ObjectVariantBaker {
|
|||||||
sizeZ = scaledSize(sizeZ, spec.scale());
|
sizeZ = scaledSize(sizeZ, spec.scale());
|
||||||
voxels = scale(voxels, spec.scale(), sizeX, sizeY, sizeZ);
|
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) {
|
if (spec.warp() != null) {
|
||||||
voxels = warp(voxels, spec.warp());
|
voxels = warp(voxels, spec.warp());
|
||||||
}
|
}
|
||||||
@@ -243,11 +249,19 @@ public final class ObjectVariantBaker {
|
|||||||
return new ArrayList<>(scaled.values());
|
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());
|
BlockStateRotation states = BlockStateRotation.of(rotation.x(), rotation.y(), rotation.z());
|
||||||
double[] lift = rotate(0, centerY, 0, rotation);
|
double[] lift = rotate(0, centerY, 0, rotation);
|
||||||
int rotatedCenterY = (int) Math.floor(lift[1]);
|
int rotatedCenterY = (int) Math.floor(lift[1]);
|
||||||
List<Voxel> sorted = new ArrayList<>(voxels);
|
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);
|
sorted.sort(VOXEL_ORDER);
|
||||||
Map<Long, Voxel> anchored = new LinkedHashMap<>();
|
Map<Long, Voxel> anchored = new LinkedHashMap<>();
|
||||||
for (Voxel voxel : sorted) {
|
for (Voxel voxel : sorted) {
|
||||||
@@ -260,6 +274,35 @@ public final class ObjectVariantBaker {
|
|||||||
return new ArrayList<>(anchored.values());
|
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) {
|
private static double[] rotate(double x, double y, double z, VariantSpec.Rotation rotation) {
|
||||||
double rx = x;
|
double rx = x;
|
||||||
double ry = y;
|
double ry = y;
|
||||||
|
|||||||
+4
@@ -44,6 +44,10 @@ public record VariantSpec(double scale, Rotation rotation, VariantWarp warp, dou
|
|||||||
public boolean none() {
|
public boolean none() {
|
||||||
return x == 0 && y == 0 && z == 0;
|
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 {
|
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) {
|
public IrisObject scaled(double scale, IrisObjectPlacementScaleInterpolator interpolation) {
|
||||||
return IrisObjectTransforms.scaled(this, scale, interpolation);
|
return IrisObjectTransforms.scaled(this, scale, interpolation);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,14 +168,6 @@ public class IrisObjectRotation {
|
|||||||
return getRotation(spin, zAxis);
|
return getRotation(spin, zAxis);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IrisObject rotateCopy(IrisObject e) {
|
|
||||||
if (e == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return e.rotateCopy(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IrisBlockVector rotate(IrisBlockVector direction) {
|
public IrisBlockVector rotate(IrisBlockVector direction) {
|
||||||
return rotate(direction, 0, 0, 0);
|
return rotate(direction, 0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,72 +43,12 @@ import java.util.Map;
|
|||||||
import java.util.Set;
|
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 {
|
final class IrisObjectTransforms {
|
||||||
private 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) {
|
static IrisObject scaled(IrisObject self, double scale, IrisObjectPlacementScaleInterpolator interpolation) {
|
||||||
return scaled(self, scale, interpolation, ScaleOrigin.DEFAULT);
|
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();
|
VectorMap<NativeBlockState>.Cursor cursor = blocks.cursor();
|
||||||
int index = 0;
|
int index = 0;
|
||||||
while (index < resolved.length && cursor.next()) {
|
while (index < bySource.length && cursor.next()) {
|
||||||
resolved[index++] = markers.get(cursor.key());
|
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 it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.IdentityHashMap;
|
import java.util.IdentityHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
@@ -21,15 +22,25 @@ final class ObjectPlacementBake {
|
|||||||
private final int[] offsetY;
|
private final int[] offsetY;
|
||||||
private final int[] offsetZ;
|
private final int[] offsetZ;
|
||||||
private final int[] layerY;
|
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 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.layout = layout;
|
||||||
this.key = key;
|
this.key = key;
|
||||||
this.offsetX = offsetX;
|
this.offsetX = cells.offsetX();
|
||||||
this.offsetY = offsetY;
|
this.offsetY = cells.offsetY();
|
||||||
this.offsetZ = offsetZ;
|
this.offsetZ = cells.offsetZ();
|
||||||
this.layerY = layerY;
|
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) {
|
static ObjectPlacementBake of(IrisObject object, SpinKernel spin, boolean ceilingHang, IrisBlockVector translateOffset) {
|
||||||
@@ -42,7 +53,10 @@ final class ObjectPlacementBake {
|
|||||||
if (cached != null && cached.layout == layout) {
|
if (cached != null && cached.layout == layout) {
|
||||||
return cached;
|
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) {
|
if (cached != null) {
|
||||||
variants.replace(key, cached, built);
|
variants.replace(key, cached, built);
|
||||||
} else if (variants.size() < MAXIMUM_VARIANTS_PER_OBJECT) {
|
} else if (variants.size() < MAXIMUM_VARIANTS_PER_OBJECT) {
|
||||||
@@ -54,8 +68,8 @@ 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) {
|
IrisBlockVector translateOffset) {
|
||||||
int count = layout.count;
|
int count = layout.count;
|
||||||
int[] x = new int[count];
|
int[] x = new int[count];
|
||||||
int[] y = new int[count];
|
int[] y = new int[count];
|
||||||
@@ -66,7 +80,7 @@ final class ObjectPlacementBake {
|
|||||||
VectorMap<NativeBlockState>.Cursor cursor = object.blocks.cursor();
|
VectorMap<NativeBlockState>.Cursor cursor = object.blocks.cursor();
|
||||||
int index = 0;
|
int index = 0;
|
||||||
while (cursor.next() && index < count) {
|
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 roundedY = (int) Math.round(rotated.getY());
|
||||||
int floorY = rotated.getBlockY();
|
int floorY = rotated.getBlockY();
|
||||||
x[index] = (int) Math.round(rotated.getX());
|
x[index] = (int) Math.round(rotated.getX());
|
||||||
@@ -79,24 +93,46 @@ final class ObjectPlacementBake {
|
|||||||
if (index != count) {
|
if (index != count) {
|
||||||
throw new IllegalStateException("Object " + object.getLoadKey() + " changed while its placement bake was built");
|
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) {
|
IrisBlockVector translateOffset) {
|
||||||
target.setX(source.getX());
|
RotationResampler.Targets targets = layout.resampler(object).resample(spin, ceilingHang, translateOffset);
|
||||||
target.setY(source.getY());
|
return gather(layout, targets.x(), targets.y(), targets.z(), targets.sources());
|
||||||
target.setZ(source.getZ());
|
}
|
||||||
spin.rotate(target);
|
|
||||||
if (ceilingHang) {
|
private static Cells gather(Layout layout, int[] x, int[] y, int[] z, int[] sources) {
|
||||||
target.setY(-target.getBlockY());
|
int count = sources.length;
|
||||||
}
|
short[] paletteIndexes = new short[count];
|
||||||
if (translateOffset != null) {
|
long[] canopy = layout.canopy == null ? null : new long[(count + 63) >>> 6];
|
||||||
target.add(translateOffset);
|
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() {
|
int count() {
|
||||||
|
return offsetX.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sourceCount() {
|
||||||
return layout.count;
|
return layout.count;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +153,7 @@ final class ObjectPlacementBake {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int paletteIndex(int index) {
|
int paletteIndex(int index) {
|
||||||
return layout.paletteIndexes[index];
|
return paletteIndexes[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
NativeBlockState[] palette() {
|
NativeBlockState[] palette() {
|
||||||
@@ -125,32 +161,51 @@ final class ObjectPlacementBake {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int[] tileBlocks() {
|
int[] tileBlocks() {
|
||||||
return layout.tileBlocks;
|
return tileBlocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
TileData[] tileValues() {
|
TileData[] tileValues() {
|
||||||
return layout.tileValues;
|
return tileValues;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean canopy(int index) {
|
boolean canopy(int index) {
|
||||||
long[] canopy = layout.canopy;
|
|
||||||
return canopy != null && (canopy[index >>> 6] & (1L << (index & 63))) != 0L;
|
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[] exact(IrisObject object, SpinKernel spin) {
|
||||||
double[] resolved = exact;
|
double[] resolved = exact;
|
||||||
if (resolved != null) {
|
if (resolved != null) {
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
resolved = sources == null ? transformedExact(object, spin) : targetExact();
|
||||||
|
exact = resolved;
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double[] transformedExact(IrisObject object, SpinKernel spin) {
|
||||||
int count = layout.count;
|
int count = layout.count;
|
||||||
object.readLock.lock();
|
object.readLock.lock();
|
||||||
try {
|
try {
|
||||||
resolved = new double[count * 3];
|
double[] resolved = new double[count * 3];
|
||||||
IrisBlockVector rotated = new IrisBlockVector(0, 0, 0);
|
IrisBlockVector rotated = new IrisBlockVector(0, 0, 0);
|
||||||
VectorMap<NativeBlockState>.Cursor cursor = object.blocks.cursor();
|
VectorMap<NativeBlockState>.Cursor cursor = object.blocks.cursor();
|
||||||
int index = 0;
|
int index = 0;
|
||||||
while (cursor.next() && index < count) {
|
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] = rotated.getX();
|
||||||
resolved[index * 3 + 1] = rotated.getY();
|
resolved[index * 3 + 1] = rotated.getY();
|
||||||
resolved[index * 3 + 2] = rotated.getZ();
|
resolved[index * 3 + 2] = rotated.getZ();
|
||||||
@@ -159,10 +214,19 @@ final class ObjectPlacementBake {
|
|||||||
if (index != count) {
|
if (index != count) {
|
||||||
throw new IllegalStateException("Object " + object.getLoadKey() + " changed while its placement bake was read");
|
throw new IllegalStateException("Object " + object.getLoadKey() + " changed while its placement bake was read");
|
||||||
}
|
}
|
||||||
|
return resolved;
|
||||||
} finally {
|
} finally {
|
||||||
object.readLock.unlock();
|
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;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,6 +240,7 @@ final class ObjectPlacementBake {
|
|||||||
private final int[] tileBlocks;
|
private final int[] tileBlocks;
|
||||||
private final TileData[] tileValues;
|
private final TileData[] tileValues;
|
||||||
private final long[] canopy;
|
private final long[] canopy;
|
||||||
|
private volatile RotationResampler resampler;
|
||||||
|
|
||||||
private Layout(long blocksRevision, long statesRevision, TreeGrounding grounding, int count, short[] paletteIndexes,
|
private Layout(long blocksRevision, long statesRevision, TreeGrounding grounding, int count, short[] paletteIndexes,
|
||||||
NativeBlockState[] palette, int[] tileBlocks, TileData[] tileValues, long[] canopy) {
|
NativeBlockState[] palette, int[] tileBlocks, TileData[] tileValues, long[] canopy) {
|
||||||
@@ -270,6 +335,15 @@ final class ObjectPlacementBake {
|
|||||||
&& object.states.modificationRevision() == statesRevision
|
&& object.states.modificationRevision() == statesRevision
|
||||||
&& object.getTreeGrounding() == grounding;
|
&& 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,
|
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;
|
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_QUARTER = 2;
|
||||||
static final int MODE_THREE_QUARTER = 3;
|
static final int MODE_THREE_QUARTER = 3;
|
||||||
static final int MODE_ANGLE = 4;
|
static final int MODE_ANGLE = 4;
|
||||||
|
private static final double AXIS_ALIGNED_EPSILON = 1.0E-9D;
|
||||||
|
|
||||||
final boolean rotates;
|
final boolean rotates;
|
||||||
|
final boolean axisAligned;
|
||||||
final int xMode;
|
final int xMode;
|
||||||
final int yMode;
|
final int yMode;
|
||||||
final int zMode;
|
final int zMode;
|
||||||
@@ -44,6 +46,9 @@ final class SpinKernel {
|
|||||||
zSin = Math.sin(zAngle);
|
zSin = Math.sin(zAngle);
|
||||||
yCos = Math.cos(yAngle);
|
yCos = Math.cos(yAngle);
|
||||||
ySin = Math.sin(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) {
|
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 -> {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+174
@@ -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.TemplateVolume;
|
||||||
import art.arcane.iris.pack.datapack.export.VanillaBlockState;
|
import art.arcane.iris.pack.datapack.export.VanillaBlockState;
|
||||||
import art.arcane.iris.pack.datapack.export.compile.WeightedState;
|
import art.arcane.iris.pack.datapack.export.compile.WeightedState;
|
||||||
|
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import static org.junit.Assert.assertArrayEquals;
|
import static org.junit.Assert.assertArrayEquals;
|
||||||
@@ -22,6 +25,8 @@ import static org.junit.Assert.assertTrue;
|
|||||||
|
|
||||||
public class ObjectVariantBakerTest {
|
public class ObjectVariantBakerTest {
|
||||||
private static final String NAMESPACE = "iris_export";
|
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
|
@Test
|
||||||
public void variantBakingIsDeterministic() throws IOException {
|
public void variantBakingIsDeterministic() throws IOException {
|
||||||
@@ -245,6 +250,171 @@ public class ObjectVariantBakerTest {
|
|||||||
assertEquals(2, lifted.roots().reach());
|
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() {
|
private static TemplateVolume tree() {
|
||||||
List<TemplateBlock> blocks = new ArrayList<>();
|
List<TemplateBlock> blocks = new ArrayList<>();
|
||||||
for (int y = 0; y < 5; y++) {
|
for (int y = 0; y < 5; y++) {
|
||||||
@@ -271,4 +441,8 @@ public class ObjectVariantBakerTest {
|
|||||||
}
|
}
|
||||||
return states;
|
return states;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private interface Interior {
|
||||||
|
boolean contains(double x, double z);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -56,66 +56,6 @@ public class IrisObjectTransformsTileTest {
|
|||||||
IrisPlatforms.unbind();
|
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
|
@Test
|
||||||
public void scalingWaitsForMatchingGeometryAndVolume() throws Exception {
|
public void scalingWaitsForMatchingGeometryAndVolume() throws Exception {
|
||||||
assertConsistentScaling(false);
|
assertConsistentScaling(false);
|
||||||
|
|||||||
@@ -1,11 +1,23 @@
|
|||||||
package art.arcane.iris.structure.object;
|
package art.arcane.iris.structure.object;
|
||||||
|
|
||||||
import art.arcane.iris.generation.geometry.IrisBlockVector;
|
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.iris.testsupport.KeyedBlockState;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@@ -91,7 +103,11 @@ public class ObjectPlacementBakeTest {
|
|||||||
distinct.add(ObjectPlacementBake.Key.of(spin, false, translate));
|
distinct.add(ObjectPlacementBake.Key.of(spin, false, translate));
|
||||||
assertSame(first.palette(), bake.palette());
|
assertSame(first.palette(), bake.palette());
|
||||||
assertTrue(object.placementBakes.size() <= 32);
|
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) {
|
if (placement == 2000) {
|
||||||
settled = new HashMap<>(object.placementBakes);
|
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);
|
double[] exact = bake.exact(object, spin);
|
||||||
int index = 0;
|
int index = 0;
|
||||||
for (IrisBlockVector source : object.getBlocks().keys()) {
|
for (IrisBlockVector source : object.getBlocks().keys()) {
|
||||||
IrisBlockVector expected = source.clone();
|
IrisBlockVector expected = source.clone();
|
||||||
spin.rotate(expected);
|
spin.rotate(expected);
|
||||||
expected.add(translate);
|
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.getX()), bake.offsetX(index));
|
||||||
assertEquals((int) Math.round(expected.getY()), bake.offsetY(index));
|
assertEquals((int) Math.round(expected.getY()), bake.offsetY(index));
|
||||||
assertEquals((int) Math.round(expected.getZ()), bake.offsetZ(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 OBJECTS = 200;
|
||||||
private static final int SEEDS = 5;
|
private static final int SEEDS = 5;
|
||||||
private static final long GOLDEN = 0x0b870e7696a055b0L;
|
private static final long GOLDEN = 0x930e130dc51b6f67L;
|
||||||
private static final long MARKER_GOLDEN = 0x60b4bbf1144b239cL;
|
private static final long MARKER_GOLDEN = 0x67ae55d574c3894eL;
|
||||||
private static final long HIGH_VARIANT_GOLDEN = 0x998044b16f7f1f6fL;
|
private static final long HIGH_VARIANT_GOLDEN = 0xdd61791b0f7e4d37L;
|
||||||
|
|
||||||
private static final List<String> PALETTE = List.of(
|
private static final List<String> PALETTE = List.of(
|
||||||
"minecraft:stone",
|
"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
|
@Test
|
||||||
public void shrinkwrapMovesRootMetadataAndInvalidatesItsFootprint() {
|
public void shrinkwrapMovesRootMetadataAndInvalidatesItsFootprint() {
|
||||||
IrisObject source = tree(true);
|
IrisObject source = tree(true);
|
||||||
@@ -397,7 +382,6 @@ public class TreeGroundingPlacementTest {
|
|||||||
IrisObject shrunk = source.copy();
|
IrisObject shrunk = source.copy();
|
||||||
shrunk.shrinkwrap();
|
shrunk.shrinkwrap();
|
||||||
for (IrisObject transformed : List.of(source.copy(), shrunk,
|
for (IrisObject transformed : List.of(source.copy(), shrunk,
|
||||||
source.rotateCopy(IrisObjectRotation.of(0, 90, 0)),
|
|
||||||
source.scaled(2, IrisObjectPlacementScaleInterpolator.NONE),
|
source.scaled(2, IrisObjectPlacementScaleInterpolator.NONE),
|
||||||
source.scaledAroundOrigin(2, IrisObjectPlacementScaleInterpolator.NONE))) {
|
source.scaledAroundOrigin(2, IrisObjectPlacementScaleInterpolator.NONE))) {
|
||||||
assertGroundingMatchesBlocks(transformed);
|
assertGroundingMatchesBlocks(transformed);
|
||||||
|
|||||||
Reference in new issue
Block a user