4726 lines
214 KiB
Diff
4726 lines
214 KiB
Diff
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
|
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
|
|
Date: Mon, 4 May 2020 10:06:24 -0700
|
|
Subject: [PATCH] Collision optimisations
|
|
|
|
The collision patch has been designed with the assumption that
|
|
most shapes are either a single AABB or an ArrayVoxelShape
|
|
(typical voxel bitset representation). Like previously,
|
|
single AABB shapes are treated as AABBs. Unlike previously, the
|
|
VoxelShape class has been changed to carry shape data that
|
|
ArrayVoxelShape would, except in a discrete manner rather
|
|
than abstracted away (not hidden behind DoubleList and
|
|
the poorly named DiscreteVoxelShape).
|
|
|
|
VoxelShape now carries three important states:
|
|
1. The voxel bitset + its sizes for the X, Y, and Z axis
|
|
2. The voxel coordinates (represented as an array and an offset per axis)
|
|
3. Single AABB representation, if possible
|
|
|
|
Note that if the single AABB representation is present,
|
|
it is used instead of the voxel bitset representation as
|
|
the single AABB representation is a special case of the
|
|
voxel bitset representation and can be optimised as such.
|
|
|
|
This effectively turns every VoxelShape instance, regardless of
|
|
actual class, into a typical voxel bitset representation.
|
|
This allows all VoxelShape operations to be optimised
|
|
for voxel bitset representations without dealing with the
|
|
abstraction and indirection that was imposed on VoxelShape
|
|
by Mojang. The patch now effectively optimises all VoxelShape
|
|
operations. Below is a list of some of the operations optimised:
|
|
- Shape merging/ORing
|
|
- Shape optimisation
|
|
- Occlusion checking
|
|
- Non-single AABB VoxelShape collisions/intersection
|
|
- Shape raytracing
|
|
- Empty VoxelShape testing
|
|
|
|
This patch also includes optimisations for raytracing,
|
|
which mostly boil down to removing indirection caused by the
|
|
interface BlockGetter which allows chunk caching.
|
|
|
|
diff --git a/src/main/java/io/papermc/paper/util/CachedLists.java b/src/main/java/io/papermc/paper/util/CachedLists.java
|
|
index be668387f65a633c6ac497fca632a4767a1bf3a2..e08f4e39db4ee3fed62e37364d17dcc5c5683504 100644
|
|
--- a/src/main/java/io/papermc/paper/util/CachedLists.java
|
|
+++ b/src/main/java/io/papermc/paper/util/CachedLists.java
|
|
@@ -1,8 +1,57 @@
|
|
package io.papermc.paper.util;
|
|
|
|
+import net.minecraft.world.entity.Entity;
|
|
+import net.minecraft.world.phys.AABB;
|
|
+import org.bukkit.Bukkit;
|
|
+import org.bukkit.craftbukkit.util.UnsafeList;
|
|
+import java.util.List;
|
|
+
|
|
public final class CachedLists {
|
|
|
|
- public static void reset() {
|
|
+ // Paper start - optimise collisions
|
|
+ static final UnsafeList<AABB> TEMP_COLLISION_LIST = new UnsafeList<>(1024);
|
|
+ static boolean tempCollisionListInUse;
|
|
+
|
|
+ public static UnsafeList<AABB> getTempCollisionList() {
|
|
+ if (!Bukkit.isPrimaryThread() || tempCollisionListInUse) {
|
|
+ return new UnsafeList<>(16);
|
|
+ }
|
|
+ tempCollisionListInUse = true;
|
|
+ return TEMP_COLLISION_LIST;
|
|
+ }
|
|
+
|
|
+ public static void returnTempCollisionList(List<AABB> list) {
|
|
+ if (list != TEMP_COLLISION_LIST) {
|
|
+ return;
|
|
+ }
|
|
+ ((UnsafeList)list).setSize(0);
|
|
+ tempCollisionListInUse = false;
|
|
+ }
|
|
|
|
+ static final UnsafeList<Entity> TEMP_GET_ENTITIES_LIST = new UnsafeList<>(1024);
|
|
+ static boolean tempGetEntitiesListInUse;
|
|
+
|
|
+ public static UnsafeList<Entity> getTempGetEntitiesList() {
|
|
+ if (!Bukkit.isPrimaryThread() || tempGetEntitiesListInUse) {
|
|
+ return new UnsafeList<>(16);
|
|
+ }
|
|
+ tempGetEntitiesListInUse = true;
|
|
+ return TEMP_GET_ENTITIES_LIST;
|
|
+ }
|
|
+
|
|
+ public static void returnTempGetEntitiesList(List<Entity> list) {
|
|
+ if (list != TEMP_GET_ENTITIES_LIST) {
|
|
+ return;
|
|
+ }
|
|
+ ((UnsafeList)list).setSize(0);
|
|
+ tempGetEntitiesListInUse = false;
|
|
+ }
|
|
+ // Paper end - optimise collisions
|
|
+
|
|
+ public static void reset() {
|
|
+ // Paper start - optimise collisions
|
|
+ TEMP_COLLISION_LIST.completeReset();
|
|
+ TEMP_GET_ENTITIES_LIST.completeReset();
|
|
+ // Paper end - optimise collisions
|
|
}
|
|
}
|
|
diff --git a/src/main/java/io/papermc/paper/util/CollisionUtil.java b/src/main/java/io/papermc/paper/util/CollisionUtil.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..ee0331a6bc40cdde08d926fd8eb1dc642630c2e5
|
|
--- /dev/null
|
|
+++ b/src/main/java/io/papermc/paper/util/CollisionUtil.java
|
|
@@ -0,0 +1,1851 @@
|
|
+package io.papermc.paper.util;
|
|
+
|
|
+import io.papermc.paper.util.collisions.CachedShapeData;
|
|
+import it.unimi.dsi.fastutil.doubles.DoubleArrayList;
|
|
+import it.unimi.dsi.fastutil.doubles.DoubleList;
|
|
+import net.minecraft.core.BlockPos;
|
|
+import net.minecraft.core.Direction;
|
|
+import net.minecraft.server.level.ServerChunkCache;
|
|
+import net.minecraft.util.Mth;
|
|
+import net.minecraft.world.entity.Entity;
|
|
+import net.minecraft.world.item.Item;
|
|
+import net.minecraft.world.level.CollisionGetter;
|
|
+import net.minecraft.world.level.EntityGetter;
|
|
+import net.minecraft.world.level.Level;
|
|
+import net.minecraft.world.level.block.Blocks;
|
|
+import net.minecraft.world.level.block.state.BlockState;
|
|
+import net.minecraft.world.level.border.WorldBorder;
|
|
+import net.minecraft.world.level.chunk.ChunkAccess;
|
|
+import net.minecraft.world.level.chunk.ChunkStatus;
|
|
+import net.minecraft.world.level.chunk.LevelChunkSection;
|
|
+import net.minecraft.world.level.chunk.PalettedContainer;
|
|
+import net.minecraft.world.level.material.FluidState;
|
|
+import net.minecraft.world.phys.AABB;
|
|
+import net.minecraft.world.phys.Vec3;
|
|
+import net.minecraft.world.phys.shapes.ArrayVoxelShape;
|
|
+import net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape;
|
|
+import net.minecraft.world.phys.shapes.BooleanOp;
|
|
+import net.minecraft.world.phys.shapes.CollisionContext;
|
|
+import net.minecraft.world.phys.shapes.DiscreteVoxelShape;
|
|
+import net.minecraft.world.phys.shapes.EntityCollisionContext;
|
|
+import net.minecraft.world.phys.shapes.OffsetDoubleList;
|
|
+import net.minecraft.world.phys.shapes.Shapes;
|
|
+import net.minecraft.world.phys.shapes.VoxelShape;
|
|
+import java.util.Arrays;
|
|
+import java.util.List;
|
|
+import java.util.function.BiPredicate;
|
|
+import java.util.function.Predicate;
|
|
+
|
|
+public final class CollisionUtil {
|
|
+
|
|
+ public static final double COLLISION_EPSILON = 1.0E-7;
|
|
+ public static final DoubleArrayList ZERO_ONE = DoubleArrayList.wrap(new double[] { 0.0, 1.0 });
|
|
+
|
|
+ public static boolean isSpecialCollidingBlock(final net.minecraft.world.level.block.state.BlockBehaviour.BlockStateBase block) {
|
|
+ return block.hasLargeCollisionShape() || block.getBlock() == Blocks.MOVING_PISTON;
|
|
+ }
|
|
+
|
|
+ public static boolean isEmpty(final AABB aabb) {
|
|
+ return (aabb.maxX - aabb.minX) < COLLISION_EPSILON || (aabb.maxY - aabb.minY) < COLLISION_EPSILON || (aabb.maxZ - aabb.minZ) < COLLISION_EPSILON;
|
|
+ }
|
|
+
|
|
+ public static boolean isEmpty(final double minX, final double minY, final double minZ,
|
|
+ final double maxX, final double maxY, final double maxZ) {
|
|
+ return (maxX - minX) < COLLISION_EPSILON || (maxY - minY) < COLLISION_EPSILON || (maxZ - minZ) < COLLISION_EPSILON;
|
|
+ }
|
|
+
|
|
+ public static AABB getBoxForChunk(final int chunkX, final int chunkZ) {
|
|
+ double x = (double)(chunkX << 4);
|
|
+ double z = (double)(chunkZ << 4);
|
|
+ // use a bounding box bigger than the chunk to prevent entities from entering it on move
|
|
+ return new AABB(x - 3*COLLISION_EPSILON, Double.NEGATIVE_INFINITY, z - 3*COLLISION_EPSILON,
|
|
+ x + (16.0 + 3*COLLISION_EPSILON), Double.POSITIVE_INFINITY, z + (16.0 + 3*COLLISION_EPSILON), false);
|
|
+ }
|
|
+
|
|
+ /*
|
|
+ A couple of rules for VoxelShape collisions:
|
|
+ Two shapes only intersect if they are actually more than EPSILON units into each other. This also applies to movement
|
|
+ checks.
|
|
+ If the two shapes strictly collide, then the return value of a collide call will return a value in the opposite
|
|
+ direction of the source move. However, this value will not be greater in magnitude than EPSILON. Collision code
|
|
+ will automatically round it to 0.
|
|
+ */
|
|
+
|
|
+ public static boolean voxelShapeIntersect(final double minX1, final double minY1, final double minZ1, final double maxX1,
|
|
+ final double maxY1, final double maxZ1, final double minX2, final double minY2,
|
|
+ final double minZ2, final double maxX2, final double maxY2, final double maxZ2) {
|
|
+ return (minX1 - maxX2) < -COLLISION_EPSILON && (maxX1 - minX2) > COLLISION_EPSILON &&
|
|
+ (minY1 - maxY2) < -COLLISION_EPSILON && (maxY1 - minY2) > COLLISION_EPSILON &&
|
|
+ (minZ1 - maxZ2) < -COLLISION_EPSILON && (maxZ1 - minZ2) > COLLISION_EPSILON;
|
|
+ }
|
|
+
|
|
+ public static boolean voxelShapeIntersect(final AABB box, final double minX, final double minY, final double minZ,
|
|
+ final double maxX, final double maxY, final double maxZ) {
|
|
+ return (box.minX - maxX) < -COLLISION_EPSILON && (box.maxX - minX) > COLLISION_EPSILON &&
|
|
+ (box.minY - maxY) < -COLLISION_EPSILON && (box.maxY - minY) > COLLISION_EPSILON &&
|
|
+ (box.minZ - maxZ) < -COLLISION_EPSILON && (box.maxZ - minZ) > COLLISION_EPSILON;
|
|
+ }
|
|
+
|
|
+ public static boolean voxelShapeIntersect(final AABB box1, final AABB box2) {
|
|
+ return (box1.minX - box2.maxX) < -COLLISION_EPSILON && (box1.maxX - box2.minX) > COLLISION_EPSILON &&
|
|
+ (box1.minY - box2.maxY) < -COLLISION_EPSILON && (box1.maxY - box2.minY) > COLLISION_EPSILON &&
|
|
+ (box1.minZ - box2.maxZ) < -COLLISION_EPSILON && (box1.maxZ - box2.minZ) > COLLISION_EPSILON;
|
|
+ }
|
|
+
|
|
+ // assume !isEmpty(target) && abs(source_move) >= COLLISION_EPSILON
|
|
+ public static double collideX(final AABB target, final AABB source, final double source_move) {
|
|
+ if ((source.minY - target.maxY) < -COLLISION_EPSILON && (source.maxY - target.minY) > COLLISION_EPSILON &&
|
|
+ (source.minZ - target.maxZ) < -COLLISION_EPSILON && (source.maxZ - target.minZ) > COLLISION_EPSILON) {
|
|
+ if (source_move >= 0.0) {
|
|
+ final double max_move = target.minX - source.maxX; // < 0.0 if no strict collision
|
|
+ if (max_move < -COLLISION_EPSILON) {
|
|
+ return source_move;
|
|
+ }
|
|
+ return Math.min(max_move, source_move);
|
|
+ } else {
|
|
+ final double max_move = target.maxX - source.minX; // > 0.0 if no strict collision
|
|
+ if (max_move > COLLISION_EPSILON) {
|
|
+ return source_move;
|
|
+ }
|
|
+ return Math.max(max_move, source_move);
|
|
+ }
|
|
+ }
|
|
+ return source_move;
|
|
+ }
|
|
+
|
|
+ // assume !isEmpty(target) && abs(source_move) >= COLLISION_EPSILON
|
|
+ public static double collideY(final AABB target, final AABB source, final double source_move) {
|
|
+ if ((source.minX - target.maxX) < -COLLISION_EPSILON && (source.maxX - target.minX) > COLLISION_EPSILON &&
|
|
+ (source.minZ - target.maxZ) < -COLLISION_EPSILON && (source.maxZ - target.minZ) > COLLISION_EPSILON) {
|
|
+ if (source_move >= 0.0) {
|
|
+ final double max_move = target.minY - source.maxY; // < 0.0 if no strict collision
|
|
+ if (max_move < -COLLISION_EPSILON) {
|
|
+ return source_move;
|
|
+ }
|
|
+ return Math.min(max_move, source_move);
|
|
+ } else {
|
|
+ final double max_move = target.maxY - source.minY; // > 0.0 if no strict collision
|
|
+ if (max_move > COLLISION_EPSILON) {
|
|
+ return source_move;
|
|
+ }
|
|
+ return Math.max(max_move, source_move);
|
|
+ }
|
|
+ }
|
|
+ return source_move;
|
|
+ }
|
|
+
|
|
+ // assume !isEmpty(target) && abs(source_move) >= COLLISION_EPSILON
|
|
+ public static double collideZ(final AABB target, final AABB source, final double source_move) {
|
|
+ if ((source.minX - target.maxX) < -COLLISION_EPSILON && (source.maxX - target.minX) > COLLISION_EPSILON &&
|
|
+ (source.minY - target.maxY) < -COLLISION_EPSILON && (source.maxY - target.minY) > COLLISION_EPSILON) {
|
|
+ if (source_move >= 0.0) {
|
|
+ final double max_move = target.minZ - source.maxZ; // < 0.0 if no strict collision
|
|
+ if (max_move < -COLLISION_EPSILON) {
|
|
+ return source_move;
|
|
+ }
|
|
+ return Math.min(max_move, source_move);
|
|
+ } else {
|
|
+ final double max_move = target.maxZ - source.minZ; // > 0.0 if no strict collision
|
|
+ if (max_move > COLLISION_EPSILON) {
|
|
+ return source_move;
|
|
+ }
|
|
+ return Math.max(max_move, source_move);
|
|
+ }
|
|
+ }
|
|
+ return source_move;
|
|
+ }
|
|
+
|
|
+ // startIndex and endIndex inclusive
|
|
+ // assumes indices are in range of array
|
|
+ private static int findFloor(final double[] values, final double value, int startIndex, int endIndex) {
|
|
+ do {
|
|
+ final int middle = (startIndex + endIndex) >>> 1;
|
|
+ final double middleVal = values[middle];
|
|
+
|
|
+ if (value < middleVal) {
|
|
+ endIndex = middle - 1;
|
|
+ } else {
|
|
+ startIndex = middle + 1;
|
|
+ }
|
|
+ } while (startIndex <= endIndex);
|
|
+
|
|
+ return startIndex - 1;
|
|
+ }
|
|
+
|
|
+ public static boolean voxelShapeIntersectNoEmpty(final VoxelShape voxel, final AABB aabb) {
|
|
+ if (voxel.isEmpty()) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true
|
|
+
|
|
+ // offsets that should be applied to coords
|
|
+ final double off_x = voxel.offsetX();
|
|
+ final double off_y = voxel.offsetY();
|
|
+ final double off_z = voxel.offsetZ();
|
|
+
|
|
+ final double[] coords_x = voxel.rootCoordinatesX();
|
|
+ final double[] coords_y = voxel.rootCoordinatesY();
|
|
+ final double[] coords_z = voxel.rootCoordinatesZ();
|
|
+
|
|
+ final CachedShapeData cached_shape_data = voxel.getCachedVoxelData();
|
|
+
|
|
+ // note: size = coords.length - 1
|
|
+ final int size_x = cached_shape_data.sizeX();
|
|
+ final int size_y = cached_shape_data.sizeY();
|
|
+ final int size_z = cached_shape_data.sizeZ();
|
|
+
|
|
+ // note: voxel bitset with set index (x, y, z) indicates that
|
|
+ // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1])
|
|
+ // is collidable. this is the fundamental principle of operation for the voxel collision operation
|
|
+
|
|
+ // note: we should be offsetting coords, but we can also just subtract from source as well - which is
|
|
+ // a win in terms of ops / simplicity (see findFloor, allows us to not modify coords for that)
|
|
+ // note: for intersection, one we find the floor of the min we can use that as the start index
|
|
+ // for the next check as source max >= source min
|
|
+ // note: we can fast check intersection on the two other axis by seeing if the min index is >= size,
|
|
+ // as this implies that coords[coords.length - 1] < source min
|
|
+ // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max
|
|
+
|
|
+ final int floor_min_x = Math.max(
|
|
+ 0,
|
|
+ findFloor(coords_x, (aabb.minX - off_x) + COLLISION_EPSILON, 0, size_x)
|
|
+ );
|
|
+ if (floor_min_x >= size_x) {
|
|
+ // cannot intersect
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ final int ceil_max_x = Math.min(
|
|
+ size_x,
|
|
+ findFloor(coords_x, (aabb.maxX - off_x) - COLLISION_EPSILON, floor_min_x, size_x) + 1
|
|
+ );
|
|
+ if (floor_min_x >= ceil_max_x) {
|
|
+ // cannot intersect
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ final int floor_min_y = Math.max(
|
|
+ 0,
|
|
+ findFloor(coords_y, (aabb.minY - off_y) + COLLISION_EPSILON, 0, size_y)
|
|
+ );
|
|
+ if (floor_min_y >= size_y) {
|
|
+ // cannot intersect
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ final int ceil_max_y = Math.min(
|
|
+ size_y,
|
|
+ findFloor(coords_y, (aabb.maxY - off_y) - COLLISION_EPSILON, floor_min_y, size_y) + 1
|
|
+ );
|
|
+ if (floor_min_y >= ceil_max_y) {
|
|
+ // cannot intersect
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ final int floor_min_z = Math.max(
|
|
+ 0,
|
|
+ findFloor(coords_z, (aabb.minZ - off_z) + COLLISION_EPSILON, 0, size_z)
|
|
+ );
|
|
+ if (floor_min_z >= size_z) {
|
|
+ // cannot intersect
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ final int ceil_max_z = Math.min(
|
|
+ size_z,
|
|
+ findFloor(coords_z, (aabb.maxZ - off_z) - COLLISION_EPSILON, floor_min_z, size_z) + 1
|
|
+ );
|
|
+ if (floor_min_z >= ceil_max_z) {
|
|
+ // cannot intersect
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ final long[] bitset = cached_shape_data.voxelSet();
|
|
+
|
|
+ // check bitset to check if any shapes in range are full
|
|
+
|
|
+ final int mul_x = size_y*size_z;
|
|
+ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) {
|
|
+ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) {
|
|
+ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) {
|
|
+ final int index = curr_z + curr_y*size_z + curr_x*mul_x;
|
|
+ // note: JLS states long shift operators ANDS shift by 63
|
|
+ if ((bitset[index >>> 6] & (1L << index)) != 0L) {
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ // assume !target.isEmpty() && abs(source_move) >= COLLISION_EPSILON
|
|
+ public static double collideX(final VoxelShape target, final AABB source, final double source_move) {
|
|
+ final AABB single_aabb = target.getSingleAABBRepresentation();
|
|
+ if (single_aabb != null) {
|
|
+ return collideX(single_aabb, source, source_move);
|
|
+ }
|
|
+ // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true
|
|
+
|
|
+ // offsets that should be applied to coords
|
|
+ final double off_x = target.offsetX();
|
|
+ final double off_y = target.offsetY();
|
|
+ final double off_z = target.offsetZ();
|
|
+
|
|
+ final double[] coords_x = target.rootCoordinatesX();
|
|
+ final double[] coords_y = target.rootCoordinatesY();
|
|
+ final double[] coords_z = target.rootCoordinatesZ();
|
|
+
|
|
+ final CachedShapeData cached_shape_data = target.getCachedVoxelData();
|
|
+
|
|
+ // note: size = coords.length - 1
|
|
+ final int size_x = cached_shape_data.sizeX();
|
|
+ final int size_y = cached_shape_data.sizeY();
|
|
+ final int size_z = cached_shape_data.sizeZ();
|
|
+
|
|
+ // note: voxel bitset with set index (x, y, z) indicates that
|
|
+ // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1])
|
|
+ // is collidable. this is the fundamental principle of operation for the voxel collision operation
|
|
+
|
|
+
|
|
+ // note: we should be offsetting coords, but we can also just subtract from source as well - which is
|
|
+ // a win in terms of ops / simplicity (see findFloor, allows us to not modify coords for that)
|
|
+ // note: for intersection, one we find the floor of the min we can use that as the start index
|
|
+ // for the next check as source max >= source min
|
|
+ // note: we can fast check intersection on the two other axis by seeing if the min index is >= size,
|
|
+ // as this implies that coords[coords.length - 1] < source min
|
|
+ // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max
|
|
+
|
|
+ final int floor_min_y = Math.max(
|
|
+ 0,
|
|
+ findFloor(coords_y, (source.minY - off_y) + COLLISION_EPSILON, 0, size_y)
|
|
+ );
|
|
+ if (floor_min_y >= size_y) {
|
|
+ // cannot intersect
|
|
+ return source_move;
|
|
+ }
|
|
+
|
|
+ final int ceil_max_y = Math.min(
|
|
+ size_y,
|
|
+ findFloor(coords_y, (source.maxY - off_y) - COLLISION_EPSILON, floor_min_y, size_y) + 1
|
|
+ );
|
|
+ if (floor_min_y >= ceil_max_y) {
|
|
+ // cannot intersect
|
|
+ return source_move;
|
|
+ }
|
|
+
|
|
+ final int floor_min_z = Math.max(
|
|
+ 0,
|
|
+ findFloor(coords_z, (source.minZ - off_z) + COLLISION_EPSILON, 0, size_z)
|
|
+ );
|
|
+ if (floor_min_z >= size_z) {
|
|
+ // cannot intersect
|
|
+ return source_move;
|
|
+ }
|
|
+
|
|
+ final int ceil_max_z = Math.min(
|
|
+ size_z,
|
|
+ findFloor(coords_z, (source.maxZ - off_z) - COLLISION_EPSILON, floor_min_z, size_z) + 1
|
|
+ );
|
|
+ if (floor_min_z >= ceil_max_z) {
|
|
+ // cannot intersect
|
|
+ return source_move;
|
|
+ }
|
|
+
|
|
+ // index = z + y*size_z + x*(size_z*size_y)
|
|
+
|
|
+ final long[] bitset = cached_shape_data.voxelSet();
|
|
+
|
|
+ if (source_move > 0.0) {
|
|
+ final double source_max = source.maxX - off_x;
|
|
+ final int ceil_max_x = findFloor(
|
|
+ coords_x, source_max - COLLISION_EPSILON, 0, size_x
|
|
+ ) + 1; // add one, we are not interested in (coords[i] + COLLISION_EPSILON) < max
|
|
+
|
|
+ // note: only the order of the first loop matters
|
|
+
|
|
+ // note: we cannot collide with the face at index size on the collision axis for forward movement
|
|
+
|
|
+ final int mul_x = size_y*size_z;
|
|
+ for (int curr_x = ceil_max_x; curr_x < size_x; ++curr_x) {
|
|
+ double max_dist = coords_x[curr_x] - source_max;
|
|
+ if (max_dist >= source_move) {
|
|
+ // if we reach here, then we will never have a case where
|
|
+ // coords[curr + n] - source_max < source_move, as coords[curr + n] < coords[curr + n + 1]
|
|
+ // thus, we can return immediately
|
|
+
|
|
+ // this optimization is important since this loop is bounded by size, and _not_ by
|
|
+ // a calculated max index based off of source_move - so it would be possible to check
|
|
+ // the whole intersected shape for collisions when we didn't need to!
|
|
+ return source_move;
|
|
+ }
|
|
+ if (max_dist >= -COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON
|
|
+ max_dist = Math.min(max_dist, source_move);
|
|
+ }
|
|
+ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) {
|
|
+ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) {
|
|
+ final int index = curr_z + curr_y*size_z + curr_x*mul_x;
|
|
+ // note: JLS states long shift operators ANDS shift by 63
|
|
+ if ((bitset[index >>> 6] & (1L << index)) != 0L) {
|
|
+ return max_dist;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return source_move;
|
|
+ } else {
|
|
+ final double source_min = source.minX - off_x;
|
|
+ final int floor_min_x = findFloor(
|
|
+ coords_x, source_min + COLLISION_EPSILON, 0, size_x
|
|
+ );
|
|
+
|
|
+ // note: only the order of the first loop matters
|
|
+
|
|
+ // note: we cannot collide with the face at index 0 on the collision axis for backwards movement
|
|
+
|
|
+ // note: we offset the collision axis by - 1 for the voxel bitset index, but use + 1 for the
|
|
+ // coordinate index as the voxelset stores whether the shape is solid for [index, index + 1]
|
|
+ // thus, we need to use the voxel index i-1 if we want to check that the face at index i is solid
|
|
+ final int mul_x = size_y*size_z;
|
|
+ for (int curr_x = floor_min_x - 1; curr_x >= 0; --curr_x) {
|
|
+ double max_dist = coords_x[curr_x + 1] - source_min;
|
|
+ if (max_dist <= source_move) {
|
|
+ // if we reach here, then we will never have a case where
|
|
+ // coords[curr + n] - source_max > source_move, as coords[curr + n] > coords[curr + n - 1]
|
|
+ // thus, we can return immediately
|
|
+
|
|
+ // this optimization is important since this loop is possibly bounded by size, and _not_ by
|
|
+ // a calculated max index based off of source_move - so it would be possible to check
|
|
+ // the whole intersected shape for collisions when we didn't need to!
|
|
+ return source_move;
|
|
+ }
|
|
+ if (max_dist <= COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON
|
|
+ max_dist = Math.max(max_dist, source_move);
|
|
+ }
|
|
+ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) {
|
|
+ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) {
|
|
+ final int index = curr_z + curr_y*size_z + curr_x*mul_x;
|
|
+ // note: JLS states long shift operators ANDS shift by 63
|
|
+ if ((bitset[index >>> 6] & (1L << index)) != 0L) {
|
|
+ return max_dist;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return source_move;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static double collideY(final VoxelShape target, final AABB source, final double source_move) {
|
|
+ final AABB single_aabb = target.getSingleAABBRepresentation();
|
|
+ if (single_aabb != null) {
|
|
+ return collideY(single_aabb, source, source_move);
|
|
+ }
|
|
+ // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true
|
|
+
|
|
+ // offsets that should be applied to coords
|
|
+ final double off_x = target.offsetX();
|
|
+ final double off_y = target.offsetY();
|
|
+ final double off_z = target.offsetZ();
|
|
+
|
|
+ final double[] coords_x = target.rootCoordinatesX();
|
|
+ final double[] coords_y = target.rootCoordinatesY();
|
|
+ final double[] coords_z = target.rootCoordinatesZ();
|
|
+
|
|
+ final CachedShapeData cached_shape_data = target.getCachedVoxelData();
|
|
+
|
|
+ // note: size = coords.length - 1
|
|
+ final int size_x = cached_shape_data.sizeX();
|
|
+ final int size_y = cached_shape_data.sizeY();
|
|
+ final int size_z = cached_shape_data.sizeZ();
|
|
+
|
|
+ // note: voxel bitset with set index (x, y, z) indicates that
|
|
+ // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1])
|
|
+ // is collidable. this is the fundamental principle of operation for the voxel collision operation
|
|
+
|
|
+
|
|
+ // note: we should be offsetting coords, but we can also just subtract from source as well - which is
|
|
+ // a win in terms of ops / simplicity (see findFloor, allows us to not modify coords for that)
|
|
+ // note: for intersection, one we find the floor of the min we can use that as the start index
|
|
+ // for the next check as source max >= source min
|
|
+ // note: we can fast check intersection on the two other axis by seeing if the min index is >= size,
|
|
+ // as this implies that coords[coords.length - 1] < source min
|
|
+ // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max
|
|
+
|
|
+ final int floor_min_x = Math.max(
|
|
+ 0,
|
|
+ findFloor(coords_x, (source.minX - off_x) + COLLISION_EPSILON, 0, size_x)
|
|
+ );
|
|
+ if (floor_min_x >= size_x) {
|
|
+ // cannot intersect
|
|
+ return source_move;
|
|
+ }
|
|
+
|
|
+ final int ceil_max_x = Math.min(
|
|
+ size_x,
|
|
+ findFloor(coords_x, (source.maxX - off_x) - COLLISION_EPSILON, floor_min_x, size_x) + 1
|
|
+ );
|
|
+ if (floor_min_x >= ceil_max_x) {
|
|
+ // cannot intersect
|
|
+ return source_move;
|
|
+ }
|
|
+
|
|
+ final int floor_min_z = Math.max(
|
|
+ 0,
|
|
+ findFloor(coords_z, (source.minZ - off_z) + COLLISION_EPSILON, 0, size_z)
|
|
+ );
|
|
+ if (floor_min_z >= size_z) {
|
|
+ // cannot intersect
|
|
+ return source_move;
|
|
+ }
|
|
+
|
|
+ final int ceil_max_z = Math.min(
|
|
+ size_z,
|
|
+ findFloor(coords_z, (source.maxZ - off_z) - COLLISION_EPSILON, floor_min_z, size_z) + 1
|
|
+ );
|
|
+ if (floor_min_z >= ceil_max_z) {
|
|
+ // cannot intersect
|
|
+ return source_move;
|
|
+ }
|
|
+
|
|
+ // index = z + y*size_z + x*(size_z*size_y)
|
|
+
|
|
+ final long[] bitset = cached_shape_data.voxelSet();
|
|
+
|
|
+ if (source_move > 0.0) {
|
|
+ final double source_max = source.maxY - off_y;
|
|
+ final int ceil_max_y = findFloor(
|
|
+ coords_y, source_max - COLLISION_EPSILON, 0, size_y
|
|
+ ) + 1; // add one, we are not interested in (coords[i] + COLLISION_EPSILON) < max
|
|
+
|
|
+ // note: only the order of the first loop matters
|
|
+
|
|
+ // note: we cannot collide with the face at index size on the collision axis for forward movement
|
|
+
|
|
+ final int mul_x = size_y*size_z;
|
|
+ for (int curr_y = ceil_max_y; curr_y < size_y; ++curr_y) {
|
|
+ double max_dist = coords_y[curr_y] - source_max;
|
|
+ if (max_dist >= source_move) {
|
|
+ // if we reach here, then we will never have a case where
|
|
+ // coords[curr + n] - source_max < source_move, as coords[curr + n] < coords[curr + n + 1]
|
|
+ // thus, we can return immediately
|
|
+
|
|
+ // this optimization is important since this loop is bounded by size, and _not_ by
|
|
+ // a calculated max index based off of source_move - so it would be possible to check
|
|
+ // the whole intersected shape for collisions when we didn't need to!
|
|
+ return source_move;
|
|
+ }
|
|
+ if (max_dist >= -COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON
|
|
+ max_dist = Math.min(max_dist, source_move);
|
|
+ }
|
|
+ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) {
|
|
+ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) {
|
|
+ final int index = curr_z + curr_y*size_z + curr_x*mul_x;
|
|
+ // note: JLS states long shift operators ANDS shift by 63
|
|
+ if ((bitset[index >>> 6] & (1L << index)) != 0L) {
|
|
+ return max_dist;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return source_move;
|
|
+ } else {
|
|
+ final double source_min = source.minY - off_y;
|
|
+ final int floor_min_y = findFloor(
|
|
+ coords_y, source_min + COLLISION_EPSILON, 0, size_y
|
|
+ );
|
|
+
|
|
+ // note: only the order of the first loop matters
|
|
+
|
|
+ // note: we cannot collide with the face at index 0 on the collision axis for backwards movement
|
|
+
|
|
+ // note: we offset the collision axis by - 1 for the voxel bitset index, but use + 1 for the
|
|
+ // coordinate index as the voxelset stores whether the shape is solid for [index, index + 1]
|
|
+ // thus, we need to use the voxel index i-1 if we want to check that the face at index i is solid
|
|
+ final int mul_x = size_y*size_z;
|
|
+ for (int curr_y = floor_min_y - 1; curr_y >= 0; --curr_y) {
|
|
+ double max_dist = coords_y[curr_y + 1] - source_min;
|
|
+ if (max_dist <= source_move) {
|
|
+ // if we reach here, then we will never have a case where
|
|
+ // coords[curr + n] - source_max > source_move, as coords[curr + n] > coords[curr + n - 1]
|
|
+ // thus, we can return immediately
|
|
+
|
|
+ // this optimization is important since this loop is possibly bounded by size, and _not_ by
|
|
+ // a calculated max index based off of source_move - so it would be possible to check
|
|
+ // the whole intersected shape for collisions when we didn't need to!
|
|
+ return source_move;
|
|
+ }
|
|
+ if (max_dist <= COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON
|
|
+ max_dist = Math.max(max_dist, source_move);
|
|
+ }
|
|
+ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) {
|
|
+ for (int curr_z = floor_min_z; curr_z < ceil_max_z; ++curr_z) {
|
|
+ final int index = curr_z + curr_y*size_z + curr_x*mul_x;
|
|
+ // note: JLS states long shift operators ANDS shift by 63
|
|
+ if ((bitset[index >>> 6] & (1L << index)) != 0L) {
|
|
+ return max_dist;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return source_move;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static double collideZ(final VoxelShape target, final AABB source, final double source_move) {
|
|
+ final AABB single_aabb = target.getSingleAABBRepresentation();
|
|
+ if (single_aabb != null) {
|
|
+ return collideZ(single_aabb, source, source_move);
|
|
+ }
|
|
+ // note: this function assumes that for any i in coords that coord[i + 1] - coord[i] > COLLISION_EPSILON is true
|
|
+
|
|
+ // offsets that should be applied to coords
|
|
+ final double off_x = target.offsetX();
|
|
+ final double off_y = target.offsetY();
|
|
+ final double off_z = target.offsetZ();
|
|
+
|
|
+ final double[] coords_x = target.rootCoordinatesX();
|
|
+ final double[] coords_y = target.rootCoordinatesY();
|
|
+ final double[] coords_z = target.rootCoordinatesZ();
|
|
+
|
|
+ final CachedShapeData cached_shape_data = target.getCachedVoxelData();
|
|
+
|
|
+ // note: size = coords.length - 1
|
|
+ final int size_x = cached_shape_data.sizeX();
|
|
+ final int size_y = cached_shape_data.sizeY();
|
|
+ final int size_z = cached_shape_data.sizeZ();
|
|
+
|
|
+ // note: voxel bitset with set index (x, y, z) indicates that
|
|
+ // an AABB(coords_x[x], coords_y[y], coords_z[z], coords_x[x + 1], coords_y[y + 1], coords_z[z + 1])
|
|
+ // is collidable. this is the fundamental principle of operation for the voxel collision operation
|
|
+
|
|
+
|
|
+ // note: we should be offsetting coords, but we can also just subtract from source as well - which is
|
|
+ // a win in terms of ops / simplicity (see findFloor, allows us to not modify coords for that)
|
|
+ // note: for intersection, one we find the floor of the min we can use that as the start index
|
|
+ // for the next check as source max >= source min
|
|
+ // note: we can fast check intersection on the two other axis by seeing if the min index is >= size,
|
|
+ // as this implies that coords[coords.length - 1] < source min
|
|
+ // we can also fast check by seeing if max index is < 0, as this implies that coords[0] > source max
|
|
+
|
|
+ final int floor_min_x = Math.max(
|
|
+ 0,
|
|
+ findFloor(coords_x, (source.minX - off_x) + COLLISION_EPSILON, 0, size_x)
|
|
+ );
|
|
+ if (floor_min_x >= size_x) {
|
|
+ // cannot intersect
|
|
+ return source_move;
|
|
+ }
|
|
+
|
|
+ final int ceil_max_x = Math.min(
|
|
+ size_x,
|
|
+ findFloor(coords_x, (source.maxX - off_x) - COLLISION_EPSILON, floor_min_x, size_x) + 1
|
|
+ );
|
|
+ if (floor_min_x >= ceil_max_x) {
|
|
+ // cannot intersect
|
|
+ return source_move;
|
|
+ }
|
|
+
|
|
+ final int floor_min_y = Math.max(
|
|
+ 0,
|
|
+ findFloor(coords_y, (source.minY - off_y) + COLLISION_EPSILON, 0, size_y)
|
|
+ );
|
|
+ if (floor_min_y >= size_y) {
|
|
+ // cannot intersect
|
|
+ return source_move;
|
|
+ }
|
|
+
|
|
+ final int ceil_max_y = Math.min(
|
|
+ size_y,
|
|
+ findFloor(coords_y, (source.maxY - off_y) - COLLISION_EPSILON, floor_min_y, size_y) + 1
|
|
+ );
|
|
+ if (floor_min_y >= ceil_max_y) {
|
|
+ // cannot intersect
|
|
+ return source_move;
|
|
+ }
|
|
+
|
|
+ // index = z + y*size_z + x*(size_z*size_y)
|
|
+
|
|
+ final long[] bitset = cached_shape_data.voxelSet();
|
|
+
|
|
+ if (source_move > 0.0) {
|
|
+ final double source_max = source.maxZ - off_z;
|
|
+ final int ceil_max_z = findFloor(
|
|
+ coords_z, source_max - COLLISION_EPSILON, 0, size_z
|
|
+ ) + 1; // add one, we are not interested in (coords[i] + COLLISION_EPSILON) < max
|
|
+
|
|
+ // note: only the order of the first loop matters
|
|
+
|
|
+ // note: we cannot collide with the face at index size on the collision axis for forward movement
|
|
+
|
|
+ final int mul_x = size_y*size_z;
|
|
+ for (int curr_z = ceil_max_z; curr_z < size_z; ++curr_z) {
|
|
+ double max_dist = coords_z[curr_z] - source_max;
|
|
+ if (max_dist >= source_move) {
|
|
+ // if we reach here, then we will never have a case where
|
|
+ // coords[curr + n] - source_max < source_move, as coords[curr + n] < coords[curr + n + 1]
|
|
+ // thus, we can return immediately
|
|
+
|
|
+ // this optimization is important since this loop is bounded by size, and _not_ by
|
|
+ // a calculated max index based off of source_move - so it would be possible to check
|
|
+ // the whole intersected shape for collisions when we didn't need to!
|
|
+ return source_move;
|
|
+ }
|
|
+ if (max_dist >= -COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON
|
|
+ max_dist = Math.min(max_dist, source_move);
|
|
+ }
|
|
+ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) {
|
|
+ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) {
|
|
+ final int index = curr_z + curr_y*size_z + curr_x*mul_x;
|
|
+ // note: JLS states long shift operators ANDS shift by 63
|
|
+ if ((bitset[index >>> 6] & (1L << index)) != 0L) {
|
|
+ return max_dist;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return source_move;
|
|
+ } else {
|
|
+ final double source_min = source.minZ - off_z;
|
|
+ final int floor_min_z = findFloor(
|
|
+ coords_z, source_min + COLLISION_EPSILON, 0, size_z
|
|
+ );
|
|
+
|
|
+ // note: only the order of the first loop matters
|
|
+
|
|
+ // note: we cannot collide with the face at index 0 on the collision axis for backwards movement
|
|
+
|
|
+ // note: we offset the collision axis by - 1 for the voxel bitset index, but use + 1 for the
|
|
+ // coordinate index as the voxelset stores whether the shape is solid for [index, index + 1]
|
|
+ // thus, we need to use the voxel index i-1 if we want to check that the face at index i is solid
|
|
+ final int mul_x = size_y*size_z;
|
|
+ for (int curr_z = floor_min_z - 1; curr_z >= 0; --curr_z) {
|
|
+ double max_dist = coords_z[curr_z + 1] - source_min;
|
|
+ if (max_dist <= source_move) {
|
|
+ // if we reach here, then we will never have a case where
|
|
+ // coords[curr + n] - source_max > source_move, as coords[curr + n] > coords[curr + n - 1]
|
|
+ // thus, we can return immediately
|
|
+
|
|
+ // this optimization is important since this loop is possibly bounded by size, and _not_ by
|
|
+ // a calculated max index based off of source_move - so it would be possible to check
|
|
+ // the whole intersected shape for collisions when we didn't need to!
|
|
+ return source_move;
|
|
+ }
|
|
+ if (max_dist <= COLLISION_EPSILON) { // only push out by up to COLLISION_EPSILON
|
|
+ max_dist = Math.max(max_dist, source_move);
|
|
+ }
|
|
+ for (int curr_x = floor_min_x; curr_x < ceil_max_x; ++curr_x) {
|
|
+ for (int curr_y = floor_min_y; curr_y < ceil_max_y; ++curr_y) {
|
|
+ final int index = curr_z + curr_y*size_z + curr_x*mul_x;
|
|
+ // note: JLS states long shift operators ANDS shift by 63
|
|
+ if ((bitset[index >>> 6] & (1L << index)) != 0L) {
|
|
+ return max_dist;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return source_move;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // does not use epsilon
|
|
+ public static boolean strictlyContains(final VoxelShape voxel, final Vec3 point) {
|
|
+ return strictlyContains(voxel, point.x, point.y, point.z);
|
|
+ }
|
|
+
|
|
+ // does not use epsilon
|
|
+ public static boolean strictlyContains(final VoxelShape voxel, double x, double y, double z) {
|
|
+ final AABB single_aabb = voxel.getSingleAABBRepresentation();
|
|
+ if (single_aabb != null) {
|
|
+ return single_aabb.contains(x, y, z);
|
|
+ }
|
|
+
|
|
+ if (voxel.isEmpty()) {
|
|
+ // bitset is clear, no point in searching
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ // offset input
|
|
+ x -= voxel.offsetX();
|
|
+ y -= voxel.offsetY();
|
|
+ z -= voxel.offsetZ();
|
|
+
|
|
+ final double[] coords_x = voxel.rootCoordinatesX();
|
|
+ final double[] coords_y = voxel.rootCoordinatesY();
|
|
+ final double[] coords_z = voxel.rootCoordinatesZ();
|
|
+
|
|
+ final CachedShapeData cached_shape_data = voxel.getCachedVoxelData();
|
|
+
|
|
+ // note: size = coords.length - 1
|
|
+ final int size_x = cached_shape_data.sizeX();
|
|
+ final int size_y = cached_shape_data.sizeY();
|
|
+ final int size_z = cached_shape_data.sizeZ();
|
|
+
|
|
+ // note: should mirror AABB#contains, which is that for any point X that X >= min and X < max.
|
|
+ // specifically, it cannot collide on the max bounds of the shape
|
|
+
|
|
+ final int index_x = findFloor(coords_x, x, 0, size_x);
|
|
+ if (index_x < 0 || index_x >= size_x) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ final int index_y = findFloor(coords_y, y, 0, size_y);
|
|
+ if (index_y < 0 || index_y >= size_y) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ final int index_z = findFloor(coords_z, z, 0, size_z);
|
|
+ if (index_z < 0 || index_z >= size_z) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ // index = z + y*size_z + x*(size_z*size_y)
|
|
+
|
|
+ final int index = index_z + index_y*size_z + index_x*(size_z*size_y);
|
|
+
|
|
+ final long[] bitset = cached_shape_data.voxelSet();
|
|
+
|
|
+ return (bitset[index >>> 6] & (1L << index)) != 0L;
|
|
+ }
|
|
+
|
|
+ private static int makeBitset(final boolean ft, final boolean tf, final boolean tt) {
|
|
+ // idx ff -> 0
|
|
+ // idx ft -> 1
|
|
+ // idx tf -> 2
|
|
+ // idx tt -> 3
|
|
+ return ((ft ? 1 : 0) << 1) | ((tf ? 1 : 0) << 2) | ((tt ? 1 : 0) << 3);
|
|
+ }
|
|
+
|
|
+ private static BitSetDiscreteVoxelShape merge(final CachedShapeData shapeDataFirst, final CachedShapeData shapeDataSecond,
|
|
+ final MergedVoxelCoordinateList mergedX, final MergedVoxelCoordinateList mergedY,
|
|
+ final MergedVoxelCoordinateList mergedZ,
|
|
+ final int booleanOp) {
|
|
+ final int sizeX = mergedX.voxels;
|
|
+ final int sizeY = mergedY.voxels;
|
|
+ final int sizeZ = mergedZ.voxels;
|
|
+
|
|
+ final long[] s1Voxels = shapeDataFirst.voxelSet();
|
|
+ final long[] s2Voxels = shapeDataSecond.voxelSet();
|
|
+
|
|
+ final int s1Mul1 = shapeDataFirst.sizeZ();
|
|
+ final int s1Mul2 = s1Mul1 * shapeDataFirst.sizeY();
|
|
+
|
|
+ final int s2Mul1 = shapeDataSecond.sizeZ();
|
|
+ final int s2Mul2 = s2Mul1 * shapeDataSecond.sizeY();
|
|
+
|
|
+ // note: indices may contain -1, but nothing > size
|
|
+ final BitSetDiscreteVoxelShape ret = new BitSetDiscreteVoxelShape(sizeX, sizeY, sizeZ);
|
|
+
|
|
+ boolean empty = true;
|
|
+
|
|
+ int mergedIdx = 0;
|
|
+ for (int idxX = 0; idxX < sizeX; ++idxX) {
|
|
+ final int s1x = mergedX.firstIndices[idxX];
|
|
+ final int s2x = mergedX.secondIndices[idxX];
|
|
+ boolean setX = false;
|
|
+ for (int idxY = 0; idxY < sizeY; ++idxY) {
|
|
+ final int s1y = mergedY.firstIndices[idxY];
|
|
+ final int s2y = mergedY.secondIndices[idxY];
|
|
+ boolean setY = false;
|
|
+ for (int idxZ = 0; idxZ < sizeZ; ++idxZ) {
|
|
+ final int s1z = mergedZ.firstIndices[idxZ];
|
|
+ final int s2z = mergedZ.secondIndices[idxZ];
|
|
+
|
|
+ int idx;
|
|
+
|
|
+ final int isS1Full = (s1x | s1y | s1z) < 0 ? 0 : (int)((s1Voxels[(idx = s1z + s1y*s1Mul1 + s1x*s1Mul2) >>> 6] >>> idx) & 1L);
|
|
+ final int isS2Full = (s2x | s2y | s2z) < 0 ? 0 : (int)((s2Voxels[(idx = s2z + s2y*s2Mul1 + s2x*s2Mul2) >>> 6] >>> idx) & 1L);
|
|
+
|
|
+ // idx ff -> 0
|
|
+ // idx ft -> 1
|
|
+ // idx tf -> 2
|
|
+ // idx tt -> 3
|
|
+
|
|
+ final boolean res = (booleanOp & (1 << (isS2Full | (isS1Full << 1)))) != 0;
|
|
+ setY |= res;
|
|
+ setX |= res;
|
|
+
|
|
+ if (res) {
|
|
+ empty = false;
|
|
+ // inline and optimize fill operation
|
|
+ ret.zMin = Math.min(ret.zMin, idxZ);
|
|
+ ret.zMax = Math.max(ret.zMax, idxZ + 1);
|
|
+ ret.storage.set(mergedIdx);
|
|
+ }
|
|
+
|
|
+ ++mergedIdx;
|
|
+ }
|
|
+ if (setY) {
|
|
+ ret.yMin = Math.min(ret.yMin, idxY);
|
|
+ ret.yMax = Math.max(ret.yMax, idxY + 1);
|
|
+ }
|
|
+ }
|
|
+ if (setX) {
|
|
+ ret.xMin = Math.min(ret.xMin, idxX);
|
|
+ ret.xMax = Math.max(ret.xMax, idxX + 1);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return empty ? null : ret;
|
|
+ }
|
|
+
|
|
+ private static boolean isMergeEmpty(final CachedShapeData shapeDataFirst, final CachedShapeData shapeDataSecond,
|
|
+ final MergedVoxelCoordinateList mergedX, final MergedVoxelCoordinateList mergedY,
|
|
+ final MergedVoxelCoordinateList mergedZ,
|
|
+ final int booleanOp) {
|
|
+ final int sizeX = mergedX.voxels;
|
|
+ final int sizeY = mergedY.voxels;
|
|
+ final int sizeZ = mergedZ.voxels;
|
|
+
|
|
+ final long[] s1Voxels = shapeDataFirst.voxelSet();
|
|
+ final long[] s2Voxels = shapeDataSecond.voxelSet();
|
|
+
|
|
+ final int s1Mul1 = shapeDataFirst.sizeZ();
|
|
+ final int s1Mul2 = s1Mul1 * shapeDataFirst.sizeY();
|
|
+
|
|
+ final int s2Mul1 = shapeDataSecond.sizeZ();
|
|
+ final int s2Mul2 = s2Mul1 * shapeDataSecond.sizeY();
|
|
+
|
|
+ // note: indices may contain -1, but nothing > size
|
|
+ for (int idxX = 0; idxX < sizeX; ++idxX) {
|
|
+ final int s1x = mergedX.firstIndices[idxX];
|
|
+ final int s2x = mergedX.secondIndices[idxX];
|
|
+ for (int idxY = 0; idxY < sizeY; ++idxY) {
|
|
+ final int s1y = mergedY.firstIndices[idxY];
|
|
+ final int s2y = mergedY.secondIndices[idxY];
|
|
+ for (int idxZ = 0; idxZ < sizeZ; ++idxZ) {
|
|
+ final int s1z = mergedZ.firstIndices[idxZ];
|
|
+ final int s2z = mergedZ.secondIndices[idxZ];
|
|
+
|
|
+ int idx;
|
|
+
|
|
+ final int isS1Full = (s1x | s1y | s1z) < 0 ? 0 : (int)((s1Voxels[(idx = s1z + s1y*s1Mul1 + s1x*s1Mul2) >>> 6] >>> idx) & 1L);
|
|
+ final int isS2Full = (s2x | s2y | s2z) < 0 ? 0 : (int)((s2Voxels[(idx = s2z + s2y*s2Mul1 + s2x*s2Mul2) >>> 6] >>> idx) & 1L);
|
|
+
|
|
+ // idx ff -> 0
|
|
+ // idx ft -> 1
|
|
+ // idx tf -> 2
|
|
+ // idx tt -> 3
|
|
+
|
|
+ final boolean res = (booleanOp & (1 << (isS2Full | (isS1Full << 1)))) != 0;
|
|
+
|
|
+ if (res) {
|
|
+ return false;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ public static VoxelShape joinOptimized(final VoxelShape first, final VoxelShape second, final BooleanOp operator) {
|
|
+ return joinUnoptimized(first, second, operator).optimize();
|
|
+ }
|
|
+
|
|
+ public static VoxelShape joinUnoptimized(final VoxelShape first, final VoxelShape second, final BooleanOp operator) {
|
|
+ final boolean ff = operator.apply(false, false);
|
|
+ if (ff) {
|
|
+ // technically, should be an infinite box but that's clearly an error
|
|
+ throw new UnsupportedOperationException("Ambiguous operator: (false, false) -> true");
|
|
+ }
|
|
+
|
|
+ final boolean tt = operator.apply(true, true);
|
|
+
|
|
+ if (first == second) {
|
|
+ return tt ? first : Shapes.empty();
|
|
+ }
|
|
+
|
|
+ final boolean ft = operator.apply(false, true);
|
|
+ final boolean tf = operator.apply(true, false);
|
|
+
|
|
+ if (first.isEmpty()) {
|
|
+ return ft ? second : Shapes.empty();
|
|
+ }
|
|
+ if (second.isEmpty()) {
|
|
+ return tf ? first : Shapes.empty();
|
|
+ }
|
|
+
|
|
+ if (!tt) {
|
|
+ // try to check for no intersection, since tt = false
|
|
+ final AABB aabbF = first.getSingleAABBRepresentation();
|
|
+ final AABB aabbS = second.getSingleAABBRepresentation();
|
|
+
|
|
+ final boolean intersect;
|
|
+
|
|
+ final boolean hasAABBF = aabbF != null;
|
|
+ final boolean hasAABBS = aabbS != null;
|
|
+ if (hasAABBF | hasAABBS) {
|
|
+ if (hasAABBF & hasAABBS) {
|
|
+ intersect = voxelShapeIntersect(aabbF, aabbS);
|
|
+ } else if (hasAABBF) {
|
|
+ intersect = voxelShapeIntersectNoEmpty(second, aabbF);
|
|
+ } else {
|
|
+ intersect = voxelShapeIntersectNoEmpty(first, aabbS);
|
|
+ }
|
|
+ } else {
|
|
+ // expect cached bounds
|
|
+ intersect = voxelShapeIntersect(first.bounds(), second.bounds());
|
|
+ }
|
|
+
|
|
+ if (!intersect) {
|
|
+ if (!tf & !ft) {
|
|
+ return Shapes.empty();
|
|
+ }
|
|
+ if (!tf | !ft) {
|
|
+ return tf ? first : second;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ final MergedVoxelCoordinateList mergedX = MergedVoxelCoordinateList.merge(
|
|
+ first.rootCoordinatesX(), first.offsetX(),
|
|
+ second.rootCoordinatesX(), second.offsetX(),
|
|
+ ft, tf
|
|
+ );
|
|
+ if (mergedX == MergedVoxelCoordinateList.EMPTY) {
|
|
+ return Shapes.empty();
|
|
+ }
|
|
+ final MergedVoxelCoordinateList mergedY = MergedVoxelCoordinateList.merge(
|
|
+ first.rootCoordinatesY(), first.offsetY(),
|
|
+ second.rootCoordinatesY(), second.offsetY(),
|
|
+ ft, tf
|
|
+ );
|
|
+ if (mergedY == MergedVoxelCoordinateList.EMPTY) {
|
|
+ return Shapes.empty();
|
|
+ }
|
|
+ final MergedVoxelCoordinateList mergedZ = MergedVoxelCoordinateList.merge(
|
|
+ first.rootCoordinatesZ(), first.offsetZ(),
|
|
+ second.rootCoordinatesZ(), second.offsetZ(),
|
|
+ ft, tf
|
|
+ );
|
|
+ if (mergedZ == MergedVoxelCoordinateList.EMPTY) {
|
|
+ return Shapes.empty();
|
|
+ }
|
|
+
|
|
+ final CachedShapeData shapeDataFirst = first.getCachedVoxelData();
|
|
+ final CachedShapeData shapeDataSecond = second.getCachedVoxelData();
|
|
+
|
|
+ final BitSetDiscreteVoxelShape mergedShape = merge(
|
|
+ shapeDataFirst, shapeDataSecond,
|
|
+ mergedX, mergedY, mergedZ,
|
|
+ makeBitset(ft, tf, tt)
|
|
+ );
|
|
+
|
|
+ if (mergedShape == null) {
|
|
+ return Shapes.empty();
|
|
+ }
|
|
+
|
|
+ return new ArrayVoxelShape(
|
|
+ mergedShape, mergedX.wrapCoords(), mergedY.wrapCoords(), mergedZ.wrapCoords()
|
|
+ );
|
|
+ }
|
|
+
|
|
+ public static boolean isJoinNonEmpty(final VoxelShape first, final VoxelShape second, final BooleanOp operator) {
|
|
+ final boolean ff = operator.apply(false, false);
|
|
+ if (ff) {
|
|
+ // technically, should be an infinite box but that's clearly an error
|
|
+ throw new UnsupportedOperationException("Ambiguous operator: (false, false) -> true");
|
|
+ }
|
|
+ final boolean firstEmpty = first.isEmpty();
|
|
+ final boolean secondEmpty = second.isEmpty();
|
|
+ if (firstEmpty | secondEmpty) {
|
|
+ return operator.apply(!firstEmpty, !secondEmpty);
|
|
+ }
|
|
+
|
|
+ final boolean tt = operator.apply(true, true);
|
|
+
|
|
+ if (first == second) {
|
|
+ return tt;
|
|
+ }
|
|
+
|
|
+ final boolean ft = operator.apply(false, true);
|
|
+ final boolean tf = operator.apply(true, false);
|
|
+
|
|
+ // try to check intersection
|
|
+ final AABB aabbF = first.getSingleAABBRepresentation();
|
|
+ final AABB aabbS = second.getSingleAABBRepresentation();
|
|
+
|
|
+ final boolean intersect;
|
|
+
|
|
+ final boolean hasAABBF = aabbF != null;
|
|
+ final boolean hasAABBS = aabbS != null;
|
|
+ if (hasAABBF | hasAABBS) {
|
|
+ if (hasAABBF & hasAABBS) {
|
|
+ intersect = voxelShapeIntersect(aabbF, aabbS);
|
|
+ } else if (hasAABBF) {
|
|
+ intersect = voxelShapeIntersectNoEmpty(second, aabbF);
|
|
+ } else {
|
|
+ // hasAABBS -> true
|
|
+ intersect = voxelShapeIntersectNoEmpty(first, aabbS);
|
|
+ }
|
|
+
|
|
+ if (!intersect) {
|
|
+ // is only non-empty if we take from first or second, as there is no overlap AND both shapes are non-empty
|
|
+ return tf | ft;
|
|
+ } else if (tt) {
|
|
+ // intersect = true && tt = true -> non-empty merged shape
|
|
+ return true;
|
|
+ }
|
|
+ } else {
|
|
+ // expect cached bounds
|
|
+ intersect = voxelShapeIntersect(first.bounds(), second.bounds());
|
|
+ if (!intersect) {
|
|
+ // is only non-empty if we take from first or second, as there is no intersection
|
|
+ return tf | ft;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ final MergedVoxelCoordinateList mergedX = MergedVoxelCoordinateList.merge(
|
|
+ first.rootCoordinatesX(), first.offsetX(),
|
|
+ second.rootCoordinatesX(), second.offsetX(),
|
|
+ ft, tf
|
|
+ );
|
|
+ if (mergedX == MergedVoxelCoordinateList.EMPTY) {
|
|
+ return false;
|
|
+ }
|
|
+ final MergedVoxelCoordinateList mergedY = MergedVoxelCoordinateList.merge(
|
|
+ first.rootCoordinatesY(), first.offsetY(),
|
|
+ second.rootCoordinatesY(), second.offsetY(),
|
|
+ ft, tf
|
|
+ );
|
|
+ if (mergedY == MergedVoxelCoordinateList.EMPTY) {
|
|
+ return false;
|
|
+ }
|
|
+ final MergedVoxelCoordinateList mergedZ = MergedVoxelCoordinateList.merge(
|
|
+ first.rootCoordinatesZ(), first.offsetZ(),
|
|
+ second.rootCoordinatesZ(), second.offsetZ(),
|
|
+ ft, tf
|
|
+ );
|
|
+ if (mergedZ == MergedVoxelCoordinateList.EMPTY) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ final CachedShapeData shapeDataFirst = first.getCachedVoxelData();
|
|
+ final CachedShapeData shapeDataSecond = second.getCachedVoxelData();
|
|
+
|
|
+ return !isMergeEmpty(
|
|
+ shapeDataFirst, shapeDataSecond,
|
|
+ mergedX, mergedY, mergedZ,
|
|
+ makeBitset(ft, tf, tt)
|
|
+ );
|
|
+ }
|
|
+
|
|
+ private static final class MergedVoxelCoordinateList {
|
|
+
|
|
+ private static final int[][] SIMPLE_INDICES_CACHE = new int[64][];
|
|
+ static {
|
|
+ for (int i = 0; i < SIMPLE_INDICES_CACHE.length; ++i) {
|
|
+ SIMPLE_INDICES_CACHE[i] = getIndices(i);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private static final MergedVoxelCoordinateList EMPTY = new MergedVoxelCoordinateList(
|
|
+ new double[] { 0.0 }, 0.0, new int[0], new int[0], 0
|
|
+ );
|
|
+
|
|
+ private static int[] getIndices(final int length) {
|
|
+ final int[] ret = new int[length];
|
|
+
|
|
+ for (int i = 1; i < length; ++i) {
|
|
+ ret[i] = i;
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ // indices above voxel size are always set to -1
|
|
+ public final double[] coordinates;
|
|
+ public final double coordinateOffset;
|
|
+ public final int[] firstIndices;
|
|
+ public final int[] secondIndices;
|
|
+ public final int voxels;
|
|
+
|
|
+ private MergedVoxelCoordinateList(final double[] coordinates, final double coordinateOffset,
|
|
+ final int[] firstIndices, final int[] secondIndices, final int voxels) {
|
|
+ this.coordinates = coordinates;
|
|
+ this.coordinateOffset = coordinateOffset;
|
|
+ this.firstIndices = firstIndices;
|
|
+ this.secondIndices = secondIndices;
|
|
+ this.voxels = voxels;
|
|
+ }
|
|
+
|
|
+ public DoubleList wrapCoords() {
|
|
+ if (this.coordinateOffset == 0.0) {
|
|
+ return DoubleArrayList.wrap(this.coordinates, this.voxels + 1);
|
|
+ }
|
|
+ return new OffsetDoubleList(DoubleArrayList.wrap(this.coordinates, this.voxels + 1), this.coordinateOffset);
|
|
+ }
|
|
+
|
|
+ // assume coordinates.length > 1
|
|
+ public static MergedVoxelCoordinateList getForSingle(final double[] coordinates, final double offset) {
|
|
+ final int voxels = coordinates.length - 1;
|
|
+ final int[] indices = voxels < SIMPLE_INDICES_CACHE.length ? SIMPLE_INDICES_CACHE[voxels] : getIndices(voxels);
|
|
+
|
|
+ return new MergedVoxelCoordinateList(coordinates, offset, indices, indices, voxels);
|
|
+ }
|
|
+
|
|
+ // assume coordinates.length > 1
|
|
+ public static MergedVoxelCoordinateList merge(final double[] firstCoordinates, final double firstOffset,
|
|
+ final double[] secondCoordinates, final double secondOffset,
|
|
+ final boolean ft, final boolean tf) {
|
|
+ if (firstCoordinates == secondCoordinates && firstOffset == secondOffset) {
|
|
+ return getForSingle(firstCoordinates, firstOffset);
|
|
+ }
|
|
+
|
|
+ final int firstCount = firstCoordinates.length;
|
|
+ final int secondCount = secondCoordinates.length;
|
|
+
|
|
+ final int voxelsFirst = firstCount - 1;
|
|
+ final int voxelsSecond = secondCount - 1;
|
|
+
|
|
+ final int maxCount = firstCount + secondCount;
|
|
+
|
|
+ final double[] coordinates = new double[maxCount];
|
|
+ final int[] firstIndices = new int[maxCount];
|
|
+ final int[] secondIndices = new int[maxCount];
|
|
+
|
|
+ final boolean notTF = !tf;
|
|
+ final boolean notFT = !ft;
|
|
+
|
|
+ int firstIndex = 0;
|
|
+ int secondIndex = 0;
|
|
+ int resultSize = 0;
|
|
+
|
|
+ // note: operations on NaN are false
|
|
+ double last = Double.NaN;
|
|
+
|
|
+ for (;;) {
|
|
+ final boolean noneLeftFirst = firstIndex >= firstCount;
|
|
+ final boolean noneLeftSecond = secondIndex >= secondCount;
|
|
+
|
|
+ if ((noneLeftFirst & noneLeftSecond) | (noneLeftSecond & notTF) | (noneLeftFirst & notFT)) {
|
|
+ break;
|
|
+ }
|
|
+
|
|
+ final boolean firstZero = firstIndex == 0;
|
|
+ final boolean secondZero = secondIndex == 0;
|
|
+
|
|
+ final double select;
|
|
+
|
|
+ if (noneLeftFirst) {
|
|
+ // noneLeftSecond -> false
|
|
+ // notFT -> false
|
|
+ select = secondCoordinates[secondIndex] + secondOffset;
|
|
+ ++secondIndex;
|
|
+ } else if (noneLeftSecond) {
|
|
+ // noneLeftFirst -> false
|
|
+ // notTF -> false
|
|
+ select = firstCoordinates[firstIndex] + firstOffset;
|
|
+ ++firstIndex;
|
|
+ } else {
|
|
+ // noneLeftFirst | noneLeftSecond -> false
|
|
+ // notTF -> ??
|
|
+ // notFT -> ??
|
|
+ final boolean breakFirst = notTF & secondZero;
|
|
+ final boolean breakSecond = notFT & firstZero;
|
|
+
|
|
+ final double first = firstCoordinates[firstIndex] + firstOffset;
|
|
+ final double second = secondCoordinates[secondIndex] + secondOffset;
|
|
+ final boolean useFirst = first < (second + COLLISION_EPSILON);
|
|
+ final boolean cont = (useFirst & breakFirst) | (!useFirst & breakSecond);
|
|
+
|
|
+ select = useFirst ? first : second;
|
|
+ firstIndex += useFirst ? 1 : 0;
|
|
+ secondIndex += 1 ^ (useFirst ? 1 : 0);
|
|
+
|
|
+ if (cont) {
|
|
+ continue;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ int prevFirst = firstIndex - 1;
|
|
+ prevFirst = prevFirst >= voxelsFirst ? -1 : prevFirst;
|
|
+ int prevSecond = secondIndex - 1;
|
|
+ prevSecond = prevSecond >= voxelsSecond ? -1 : prevSecond;
|
|
+
|
|
+ if (last >= (select - COLLISION_EPSILON)) {
|
|
+ // note: any operations on NaN is false
|
|
+ firstIndices[resultSize - 1] = prevFirst;
|
|
+ secondIndices[resultSize - 1] = prevSecond;
|
|
+ } else {
|
|
+ firstIndices[resultSize] = prevFirst;
|
|
+ secondIndices[resultSize] = prevSecond;
|
|
+ coordinates[resultSize] = select;
|
|
+
|
|
+ ++resultSize;
|
|
+ last = select;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return resultSize <= 1 ? EMPTY : new MergedVoxelCoordinateList(coordinates, 0.0, firstIndices, secondIndices, resultSize - 1);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static AABB offsetX(final AABB box, final double dx) {
|
|
+ return new AABB(box.minX + dx, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ, false);
|
|
+ }
|
|
+
|
|
+ public static AABB offsetY(final AABB box, final double dy) {
|
|
+ return new AABB(box.minX, box.minY + dy, box.minZ, box.maxX, box.maxY + dy, box.maxZ, false);
|
|
+ }
|
|
+
|
|
+ public static AABB offsetZ(final AABB box, final double dz) {
|
|
+ return new AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.maxZ + dz, false);
|
|
+ }
|
|
+
|
|
+ public static AABB expandRight(final AABB box, final double dx) { // dx > 0.0
|
|
+ return new AABB(box.minX, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ, false);
|
|
+ }
|
|
+
|
|
+ public static AABB expandLeft(final AABB box, final double dx) { // dx < 0.0
|
|
+ return new AABB(box.minX - dx, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ);
|
|
+ }
|
|
+
|
|
+ public static AABB expandUpwards(final AABB box, final double dy) { // dy > 0.0
|
|
+ return new AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY + dy, box.maxZ, false);
|
|
+ }
|
|
+
|
|
+ public static AABB expandDownwards(final AABB box, final double dy) { // dy < 0.0
|
|
+ return new AABB(box.minX, box.minY - dy, box.minZ, box.maxX, box.maxY, box.maxZ, false);
|
|
+ }
|
|
+
|
|
+ public static AABB expandForwards(final AABB box, final double dz) { // dz > 0.0
|
|
+ return new AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ + dz, false);
|
|
+ }
|
|
+
|
|
+ public static AABB expandBackwards(final AABB box, final double dz) { // dz < 0.0
|
|
+ return new AABB(box.minX, box.minY, box.minZ - dz, box.maxX, box.maxY, box.maxZ, false);
|
|
+ }
|
|
+
|
|
+ public static AABB cutRight(final AABB box, final double dx) { // dx > 0.0
|
|
+ return new AABB(box.maxX, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ, false);
|
|
+ }
|
|
+
|
|
+ public static AABB cutLeft(final AABB box, final double dx) { // dx < 0.0
|
|
+ return new AABB(box.minX + dx, box.minY, box.minZ, box.minX, box.maxY, box.maxZ, false);
|
|
+ }
|
|
+
|
|
+ public static AABB cutUpwards(final AABB box, final double dy) { // dy > 0.0
|
|
+ return new AABB(box.minX, box.maxY, box.minZ, box.maxX, box.maxY + dy, box.maxZ, false);
|
|
+ }
|
|
+
|
|
+ public static AABB cutDownwards(final AABB box, final double dy) { // dy < 0.0
|
|
+ return new AABB(box.minX, box.minY + dy, box.minZ, box.maxX, box.minY, box.maxZ, false);
|
|
+ }
|
|
+
|
|
+ public static AABB cutForwards(final AABB box, final double dz) { // dz > 0.0
|
|
+ return new AABB(box.minX, box.minY, box.maxZ, box.maxX, box.maxY, box.maxZ + dz, false);
|
|
+ }
|
|
+
|
|
+ public static AABB cutBackwards(final AABB box, final double dz) { // dz < 0.0
|
|
+ return new AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.minZ, false);
|
|
+ }
|
|
+
|
|
+ public static double performAABBCollisionsX(final AABB currentBoundingBox, double value, final List<AABB> potentialCollisions) {
|
|
+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
|
|
+ if (Math.abs(value) < COLLISION_EPSILON) {
|
|
+ return 0.0;
|
|
+ }
|
|
+ final AABB target = potentialCollisions.get(i);
|
|
+ value = collideX(target, currentBoundingBox, value);
|
|
+ }
|
|
+
|
|
+ return value;
|
|
+ }
|
|
+
|
|
+ public static double performAABBCollisionsY(final AABB currentBoundingBox, double value, final List<AABB> potentialCollisions) {
|
|
+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
|
|
+ if (Math.abs(value) < COLLISION_EPSILON) {
|
|
+ return 0.0;
|
|
+ }
|
|
+ final AABB target = potentialCollisions.get(i);
|
|
+ value = collideY(target, currentBoundingBox, value);
|
|
+ }
|
|
+
|
|
+ return value;
|
|
+ }
|
|
+
|
|
+ public static double performAABBCollisionsZ(final AABB currentBoundingBox, double value, final List<AABB> potentialCollisions) {
|
|
+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
|
|
+ if (Math.abs(value) < COLLISION_EPSILON) {
|
|
+ return 0.0;
|
|
+ }
|
|
+ final AABB target = potentialCollisions.get(i);
|
|
+ value = collideZ(target, currentBoundingBox, value);
|
|
+ }
|
|
+
|
|
+ return value;
|
|
+ }
|
|
+
|
|
+ public static double performVoxelCollisionsX(final AABB currentBoundingBox, double value, final List<VoxelShape> potentialCollisions) {
|
|
+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
|
|
+ if (Math.abs(value) < COLLISION_EPSILON) {
|
|
+ return 0.0;
|
|
+ }
|
|
+ final VoxelShape target = potentialCollisions.get(i);
|
|
+ value = collideX(target, currentBoundingBox, value);
|
|
+ }
|
|
+
|
|
+ return value;
|
|
+ }
|
|
+
|
|
+ public static double performVoxelCollisionsY(final AABB currentBoundingBox, double value, final List<VoxelShape> potentialCollisions) {
|
|
+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
|
|
+ if (Math.abs(value) < COLLISION_EPSILON) {
|
|
+ return 0.0;
|
|
+ }
|
|
+ final VoxelShape target = potentialCollisions.get(i);
|
|
+ value = collideY(target, currentBoundingBox, value);
|
|
+ }
|
|
+
|
|
+ return value;
|
|
+ }
|
|
+
|
|
+ public static double performVoxelCollisionsZ(final AABB currentBoundingBox, double value, final List<VoxelShape> potentialCollisions) {
|
|
+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
|
|
+ if (Math.abs(value) < COLLISION_EPSILON) {
|
|
+ return 0.0;
|
|
+ }
|
|
+ final VoxelShape target = potentialCollisions.get(i);
|
|
+ value = collideZ(target, currentBoundingBox, value);
|
|
+ }
|
|
+
|
|
+ return value;
|
|
+ }
|
|
+
|
|
+ public static Vec3 performVoxelCollisions(final Vec3 moveVector, AABB axisalignedbb, final List<VoxelShape> potentialCollisions) {
|
|
+ double x = moveVector.x;
|
|
+ double y = moveVector.y;
|
|
+ double z = moveVector.z;
|
|
+
|
|
+ if (y != 0.0) {
|
|
+ y = performVoxelCollisionsY(axisalignedbb, y, potentialCollisions);
|
|
+ if (y != 0.0) {
|
|
+ axisalignedbb = offsetY(axisalignedbb, y);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ final boolean xSmaller = Math.abs(x) < Math.abs(z);
|
|
+
|
|
+ if (xSmaller && z != 0.0) {
|
|
+ z = performVoxelCollisionsZ(axisalignedbb, z, potentialCollisions);
|
|
+ if (z != 0.0) {
|
|
+ axisalignedbb = offsetZ(axisalignedbb, z);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (x != 0.0) {
|
|
+ x = performVoxelCollisionsX(axisalignedbb, x, potentialCollisions);
|
|
+ if (!xSmaller && x != 0.0) {
|
|
+ axisalignedbb = offsetX(axisalignedbb, x);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (!xSmaller && z != 0.0) {
|
|
+ z = performVoxelCollisionsZ(axisalignedbb, z, potentialCollisions);
|
|
+ }
|
|
+
|
|
+ return new Vec3(x, y, z);
|
|
+ }
|
|
+
|
|
+ public static Vec3 performAABBCollisions(final Vec3 moveVector, AABB axisalignedbb, final List<AABB> potentialCollisions) {
|
|
+ double x = moveVector.x;
|
|
+ double y = moveVector.y;
|
|
+ double z = moveVector.z;
|
|
+
|
|
+ if (y != 0.0) {
|
|
+ y = performAABBCollisionsY(axisalignedbb, y, potentialCollisions);
|
|
+ if (y != 0.0) {
|
|
+ axisalignedbb = offsetY(axisalignedbb, y);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ final boolean xSmaller = Math.abs(x) < Math.abs(z);
|
|
+
|
|
+ if (xSmaller && z != 0.0) {
|
|
+ z = performAABBCollisionsZ(axisalignedbb, z, potentialCollisions);
|
|
+ if (z != 0.0) {
|
|
+ axisalignedbb = offsetZ(axisalignedbb, z);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (x != 0.0) {
|
|
+ x = performAABBCollisionsX(axisalignedbb, x, potentialCollisions);
|
|
+ if (!xSmaller && x != 0.0) {
|
|
+ axisalignedbb = offsetX(axisalignedbb, x);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (!xSmaller && z != 0.0) {
|
|
+ z = performAABBCollisionsZ(axisalignedbb, z, potentialCollisions);
|
|
+ }
|
|
+
|
|
+ return new Vec3(x, y, z);
|
|
+ }
|
|
+
|
|
+ public static Vec3 performCollisions(final Vec3 moveVector, AABB axisalignedbb,
|
|
+ final List<VoxelShape> voxels,
|
|
+ final List<AABB> aabbs) {
|
|
+ if (voxels.isEmpty()) {
|
|
+ // fast track only AABBs
|
|
+ return performAABBCollisions(moveVector, axisalignedbb, aabbs);
|
|
+ }
|
|
+
|
|
+ double x = moveVector.x;
|
|
+ double y = moveVector.y;
|
|
+ double z = moveVector.z;
|
|
+
|
|
+ if (y != 0.0) {
|
|
+ y = performAABBCollisionsY(axisalignedbb, y, aabbs);
|
|
+ y = performVoxelCollisionsY(axisalignedbb, y, voxels);
|
|
+ if (y != 0.0) {
|
|
+ axisalignedbb = offsetY(axisalignedbb, y);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ final boolean xSmaller = Math.abs(x) < Math.abs(z);
|
|
+
|
|
+ if (xSmaller && z != 0.0) {
|
|
+ z = performAABBCollisionsZ(axisalignedbb, z, aabbs);
|
|
+ z = performVoxelCollisionsZ(axisalignedbb, z, voxels);
|
|
+ if (z != 0.0) {
|
|
+ axisalignedbb = offsetZ(axisalignedbb, z);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (x != 0.0) {
|
|
+ x = performAABBCollisionsX(axisalignedbb, x, aabbs);
|
|
+ x = performVoxelCollisionsX(axisalignedbb, x, voxels);
|
|
+ if (!xSmaller && x != 0.0) {
|
|
+ axisalignedbb = offsetX(axisalignedbb, x);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (!xSmaller && z != 0.0) {
|
|
+ z = performAABBCollisionsZ(axisalignedbb, z, aabbs);
|
|
+ z = performVoxelCollisionsZ(axisalignedbb, z, voxels);
|
|
+ }
|
|
+
|
|
+ return new Vec3(x, y, z);
|
|
+ }
|
|
+
|
|
+ public static boolean isCollidingWithBorder(final WorldBorder worldborder, final AABB boundingBox) {
|
|
+ return isCollidingWithBorder(worldborder, boundingBox.minX, boundingBox.maxX, boundingBox.minZ, boundingBox.maxZ);
|
|
+ }
|
|
+
|
|
+ public static boolean isCollidingWithBorder(final WorldBorder worldborder, final double boxMinX, final double boxMaxX,
|
|
+ final double boxMinZ, final double boxMaxZ) {
|
|
+ // border size is rounded like the collide voxel shape of the border
|
|
+ final double borderMinX = Math.floor(worldborder.getMinX()); // -X
|
|
+ final double borderMaxX = Math.ceil(worldborder.getMaxX()); // +X
|
|
+
|
|
+ final double borderMinZ = Math.floor(worldborder.getMinZ()); // -Z
|
|
+ final double borderMaxZ = Math.ceil(worldborder.getMaxZ()); // +Z
|
|
+
|
|
+ // inverted check for world border enclosing the specified box expanded by -EPSILON
|
|
+ return (borderMinX - boxMinX) > CollisionUtil.COLLISION_EPSILON || (borderMaxX - boxMaxX) < -CollisionUtil.COLLISION_EPSILON ||
|
|
+ (borderMinZ - boxMinZ) > CollisionUtil.COLLISION_EPSILON || (borderMaxZ - boxMaxZ) < -CollisionUtil.COLLISION_EPSILON;
|
|
+ }
|
|
+
|
|
+ /* Math.max/min specify that any NaN argument results in a NaN return, unlike these functions */
|
|
+ private static double min(final double x, final double y) {
|
|
+ return x < y ? x : y;
|
|
+ }
|
|
+
|
|
+ private static double max(final double x, final double y) {
|
|
+ return x > y ? x : y;
|
|
+ }
|
|
+
|
|
+ public static final int COLLISION_FLAG_LOAD_CHUNKS = 1 << 0;
|
|
+ public static final int COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS = 1 << 1;
|
|
+ public static final int COLLISION_FLAG_CHECK_BORDER = 1 << 2;
|
|
+ public static final int COLLISION_FLAG_CHECK_ONLY = 1 << 3;
|
|
+
|
|
+ public static boolean getCollisionsForBlocksOrWorldBorder(final Level world, final Entity entity, final AABB aabb,
|
|
+ final List<VoxelShape> intoVoxel, final List<AABB> intoAABB,
|
|
+ final int collisionFlags, final BiPredicate<BlockState, BlockPos> predicate) {
|
|
+ final boolean checkOnly = (collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0;
|
|
+ boolean ret = false;
|
|
+
|
|
+ if ((collisionFlags & COLLISION_FLAG_CHECK_BORDER) != 0) {
|
|
+ final WorldBorder worldBorder = world.getWorldBorder();
|
|
+ if (CollisionUtil.isCollidingWithBorder(worldBorder, aabb) && entity != null && worldBorder.isInsideCloseToBorder(entity, aabb)) {
|
|
+ if (checkOnly) {
|
|
+ return true;
|
|
+ } else {
|
|
+ final VoxelShape borderShape = worldBorder.getCollisionShape();
|
|
+ intoVoxel.add(borderShape);
|
|
+ ret = true;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ final int minSection = world.minSection;
|
|
+
|
|
+ final int minBlockX = Mth.floor(aabb.minX - COLLISION_EPSILON) - 1;
|
|
+ final int maxBlockX = Mth.floor(aabb.maxX + COLLISION_EPSILON) + 1;
|
|
+
|
|
+ final int minBlockY = Math.max((minSection << 4) - 1, Mth.floor(aabb.minY - COLLISION_EPSILON) - 1);
|
|
+ final int maxBlockY = Math.min((world.maxSection << 4) + 16, Mth.floor(aabb.maxY + COLLISION_EPSILON) + 1);
|
|
+
|
|
+ final int minBlockZ = Mth.floor(aabb.minZ - COLLISION_EPSILON) - 1;
|
|
+ final int maxBlockZ = Mth.floor(aabb.maxZ + COLLISION_EPSILON) + 1;
|
|
+
|
|
+ final BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos();
|
|
+ final CollisionContext collisionShape = new LazyEntityCollisionContext(entity);
|
|
+
|
|
+ // special cases:
|
|
+ if (minBlockY > maxBlockY) {
|
|
+ // no point in checking
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ final int minChunkX = minBlockX >> 4;
|
|
+ final int maxChunkX = maxBlockX >> 4;
|
|
+
|
|
+ final int minChunkY = minBlockY >> 4;
|
|
+ final int maxChunkY = maxBlockY >> 4;
|
|
+
|
|
+ final int minChunkZ = minBlockZ >> 4;
|
|
+ final int maxChunkZ = maxBlockZ >> 4;
|
|
+
|
|
+ final boolean loadChunks = (collisionFlags & COLLISION_FLAG_LOAD_CHUNKS) != 0;
|
|
+ final ServerChunkCache chunkSource = (ServerChunkCache)world.getChunkSource();
|
|
+
|
|
+ for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) {
|
|
+ for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) {
|
|
+ final ChunkAccess chunk = loadChunks ? chunkSource.getChunk(currChunkX, currChunkZ, ChunkStatus.FULL, true) : chunkSource.getChunkAtIfLoadedImmediately(currChunkX, currChunkZ);
|
|
+
|
|
+ if (chunk == null) {
|
|
+ if ((collisionFlags & COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS) != 0) {
|
|
+ if (checkOnly) {
|
|
+ return true;
|
|
+ } else {
|
|
+ intoAABB.add(getBoxForChunk(currChunkX, currChunkZ));
|
|
+ ret = true;
|
|
+ }
|
|
+ }
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ final LevelChunkSection[] sections = chunk.getSections();
|
|
+
|
|
+ // bound y
|
|
+ for (int currChunkY = minChunkY; currChunkY <= maxChunkY; ++currChunkY) {
|
|
+ final int sectionIdx = currChunkY - minSection;
|
|
+ if (sectionIdx < 0 || sectionIdx >= sections.length) {
|
|
+ continue;
|
|
+ }
|
|
+ final LevelChunkSection section = sections[sectionIdx];
|
|
+ if (section == null || section.hasOnlyAir()) {
|
|
+ // empty
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ final boolean hasSpecial = section.getSpecialCollidingBlocks() != 0;
|
|
+ final int sectionAdjust = !hasSpecial ? 1 : 0;
|
|
+
|
|
+ final PalettedContainer<BlockState> blocks = section.states;
|
|
+
|
|
+ final int minXIterate = currChunkX == minChunkX ? (minBlockX & 15) + sectionAdjust : 0;
|
|
+ final int maxXIterate = currChunkX == maxChunkX ? (maxBlockX & 15) - sectionAdjust : 15;
|
|
+ final int minZIterate = currChunkZ == minChunkZ ? (minBlockZ & 15) + sectionAdjust : 0;
|
|
+ final int maxZIterate = currChunkZ == maxChunkZ ? (maxBlockZ & 15) - sectionAdjust : 15;
|
|
+ final int minYIterate = currChunkY == minChunkY ? (minBlockY & 15) + sectionAdjust : 0;
|
|
+ final int maxYIterate = currChunkY == maxChunkY ? (maxBlockY & 15) - sectionAdjust : 15;
|
|
+
|
|
+ for (int currY = minYIterate; currY <= maxYIterate; ++currY) {
|
|
+ final int blockY = currY | (currChunkY << 4);
|
|
+ for (int currZ = minZIterate; currZ <= maxZIterate; ++currZ) {
|
|
+ final int blockZ = currZ | (currChunkZ << 4);
|
|
+ for (int currX = minXIterate; currX <= maxXIterate; ++currX) {
|
|
+ final int localBlockIndex = (currX) | (currZ << 4) | ((currY) << 8);
|
|
+ final int blockX = currX | (currChunkX << 4);
|
|
+
|
|
+ final int edgeCount = hasSpecial ? ((blockX == minBlockX || blockX == maxBlockX) ? 1 : 0) +
|
|
+ ((blockY == minBlockY || blockY == maxBlockY) ? 1 : 0) +
|
|
+ ((blockZ == minBlockZ || blockZ == maxBlockZ) ? 1 : 0) : 0;
|
|
+ if (edgeCount == 3) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ final BlockState blockData = blocks.get(localBlockIndex);
|
|
+
|
|
+ if (blockData.emptyCollisionShape()) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ if (edgeCount == 0 || ((edgeCount != 1 || blockData.hasLargeCollisionShape()) && (edgeCount != 2 || blockData.getBlock() == Blocks.MOVING_PISTON))) {
|
|
+ VoxelShape blockCollision = blockData.getConstantCollisionShape();
|
|
+
|
|
+ if (blockCollision == null) {
|
|
+ mutablePos.set(blockX, blockY, blockZ);
|
|
+ blockCollision = blockData.getCollisionShape(world, mutablePos, collisionShape);
|
|
+ }
|
|
+
|
|
+ AABB singleAABB = blockCollision.getSingleAABBRepresentation();
|
|
+ if (singleAABB != null) {
|
|
+ singleAABB = singleAABB.move((double)blockX, (double)blockY, (double)blockZ);
|
|
+ if (!voxelShapeIntersect(aabb, singleAABB)) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ if (predicate != null) {
|
|
+ mutablePos.set(blockX, blockY, blockZ);
|
|
+ if (!predicate.test(blockData, mutablePos)) {
|
|
+ continue;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (checkOnly) {
|
|
+ return true;
|
|
+ } else {
|
|
+ ret = true;
|
|
+ intoAABB.add(singleAABB);
|
|
+ continue;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (blockCollision.isEmpty()) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ final VoxelShape blockCollisionOffset = blockCollision.move((double)blockX, (double)blockY, (double)blockZ);
|
|
+
|
|
+ if (!voxelShapeIntersectNoEmpty(blockCollisionOffset, aabb)) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ if (predicate != null) {
|
|
+ mutablePos.set(blockX, blockY, blockZ);
|
|
+ if (!predicate.test(blockData, mutablePos)) {
|
|
+ continue;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (checkOnly) {
|
|
+ return true;
|
|
+ } else {
|
|
+ ret = true;
|
|
+ intoVoxel.add(blockCollisionOffset);
|
|
+ continue;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ public static boolean getEntityHardCollisions(final CollisionGetter getter, final Entity entity, AABB aabb,
|
|
+ final List<AABB> into, final int collisionFlags, final Predicate<Entity> predicate) {
|
|
+ final boolean checkOnly = (collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0;
|
|
+ if (!(getter instanceof EntityGetter entityGetter)) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ boolean ret = false;
|
|
+
|
|
+ // to comply with vanilla intersection rules, expand by -epsilon so that we only get stuff we definitely collide with.
|
|
+ // Vanilla for hard collisions has this backwards, and they expand by +epsilon but this causes terrible problems
|
|
+ // specifically with boat collisions.
|
|
+ aabb = aabb.inflate(-COLLISION_EPSILON, -COLLISION_EPSILON, -COLLISION_EPSILON);
|
|
+ final List<Entity> entities;
|
|
+ if (entity != null && entity.hardCollides()) {
|
|
+ entities = entityGetter.getEntities(entity, aabb, predicate);
|
|
+ } else {
|
|
+ entities = entityGetter.getHardCollidingEntities(entity, aabb, predicate);
|
|
+ }
|
|
+
|
|
+ for (int i = 0, len = entities.size(); i < len; ++i) {
|
|
+ final Entity otherEntity = entities.get(i);
|
|
+
|
|
+ if (otherEntity.isSpectator()) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ if ((entity == null && otherEntity.canBeCollidedWith()) || (entity != null && entity.canCollideWith(otherEntity))) {
|
|
+ if (checkOnly) {
|
|
+ return true;
|
|
+ } else {
|
|
+ into.add(otherEntity.getBoundingBox());
|
|
+ ret = true;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ public static boolean getCollisions(final Level world, final Entity entity, final AABB aabb,
|
|
+ final List<VoxelShape> intoVoxel, final List<AABB> intoAABB, final int collisionFlags,
|
|
+ final BiPredicate<BlockState, BlockPos> blockPredicate,
|
|
+ final Predicate<Entity> entityPredicate) {
|
|
+ if ((collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0) {
|
|
+ return getCollisionsForBlocksOrWorldBorder(world, entity, aabb, intoVoxel, intoAABB, collisionFlags, blockPredicate)
|
|
+ || getEntityHardCollisions(world, entity, aabb, intoAABB, collisionFlags, entityPredicate);
|
|
+ } else {
|
|
+ return getCollisionsForBlocksOrWorldBorder(world, entity, aabb, intoVoxel, intoAABB, collisionFlags, blockPredicate)
|
|
+ | getEntityHardCollisions(world, entity, aabb, intoAABB, collisionFlags, entityPredicate);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static final class LazyEntityCollisionContext extends EntityCollisionContext {
|
|
+
|
|
+ private CollisionContext delegate;
|
|
+ private boolean delegated;
|
|
+
|
|
+ public LazyEntityCollisionContext(final Entity entity) {
|
|
+ super(false, 0.0, null, null, entity);
|
|
+ }
|
|
+
|
|
+ public boolean isDelegated() {
|
|
+ final boolean delegated = this.delegated;
|
|
+ this.delegated = false;
|
|
+ return delegated;
|
|
+ }
|
|
+
|
|
+ public CollisionContext getDelegate() {
|
|
+ this.delegated = true;
|
|
+ final Entity entity = this.getEntity();
|
|
+ return this.delegate == null ? this.delegate = (entity == null ? CollisionContext.empty() : CollisionContext.of(entity)) : this.delegate;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean isDescending() {
|
|
+ return this.getDelegate().isDescending();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean isAbove(final VoxelShape shape, final BlockPos pos, final boolean defaultValue) {
|
|
+ return this.getDelegate().isAbove(shape, pos, defaultValue);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean isHoldingItem(final Item item) {
|
|
+ return this.getDelegate().isHoldingItem(item);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean canStandOnFluid(final FluidState state, final FluidState fluidState) {
|
|
+ return this.getDelegate().canStandOnFluid(state, fluidState);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private CollisionUtil() {
|
|
+ throw new RuntimeException();
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/io/papermc/paper/util/collisions/CachedShapeData.java b/src/main/java/io/papermc/paper/util/collisions/CachedShapeData.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..1cb96b09375770f92f3e494ce2f28d9ff8699581
|
|
--- /dev/null
|
|
+++ b/src/main/java/io/papermc/paper/util/collisions/CachedShapeData.java
|
|
@@ -0,0 +1,10 @@
|
|
+package io.papermc.paper.util.collisions;
|
|
+
|
|
+public record CachedShapeData(
|
|
+ int sizeX, int sizeY, int sizeZ,
|
|
+ long[] voxelSet,
|
|
+ int minFullX, int minFullY, int minFullZ,
|
|
+ int maxFullX, int maxFullY, int maxFullZ,
|
|
+ boolean isEmpty, boolean hasSingleAABB
|
|
+) {
|
|
+}
|
|
diff --git a/src/main/java/io/papermc/paper/util/collisions/CachedToAABBs.java b/src/main/java/io/papermc/paper/util/collisions/CachedToAABBs.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..85c448a775f60ca4b4a4f2baf17487ef45bdd383
|
|
--- /dev/null
|
|
+++ b/src/main/java/io/papermc/paper/util/collisions/CachedToAABBs.java
|
|
@@ -0,0 +1,39 @@
|
|
+package io.papermc.paper.util.collisions;
|
|
+
|
|
+import net.minecraft.world.phys.AABB;
|
|
+import java.util.ArrayList;
|
|
+import java.util.List;
|
|
+
|
|
+public record CachedToAABBs(
|
|
+ List<AABB> aabbs,
|
|
+ boolean isOffset,
|
|
+ double offX, double offY, double offZ
|
|
+) {
|
|
+
|
|
+ public CachedToAABBs removeOffset() {
|
|
+ final List<AABB> toOffset = this.aabbs;
|
|
+ final double offX = this.offX;
|
|
+ final double offY = this.offY;
|
|
+ final double offZ = this.offZ;
|
|
+
|
|
+ final List<AABB> ret = new ArrayList<>(toOffset.size());
|
|
+
|
|
+ for (int i = 0, len = toOffset.size(); i < len; ++i) {
|
|
+ ret.add(toOffset.get(i).move(offX, offY, offZ));
|
|
+ }
|
|
+
|
|
+ return new CachedToAABBs(ret, false, 0.0, 0.0, 0.0);
|
|
+ }
|
|
+
|
|
+ public static CachedToAABBs offset(final CachedToAABBs cache, final double offX, final double offY, final double offZ) {
|
|
+ if (offX == 0.0 && offY == 0.0 && offZ == 0.0) {
|
|
+ return cache;
|
|
+ }
|
|
+
|
|
+ final double resX = cache.offX + offX;
|
|
+ final double resY = cache.offY + offY;
|
|
+ final double resZ = cache.offZ + offZ;
|
|
+
|
|
+ return new CachedToAABBs(cache.aabbs, true, resX, resY, resZ);
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/io/papermc/paper/util/collisions/FlatBitsetUtil.java b/src/main/java/io/papermc/paper/util/collisions/FlatBitsetUtil.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..ff9d2dad39dcc02b2371458b7b5f64c6090e8012
|
|
--- /dev/null
|
|
+++ b/src/main/java/io/papermc/paper/util/collisions/FlatBitsetUtil.java
|
|
@@ -0,0 +1,109 @@
|
|
+package io.papermc.paper.util.collisions;
|
|
+
|
|
+import java.util.Objects;
|
|
+
|
|
+public final class FlatBitsetUtil {
|
|
+
|
|
+ private static final int LOG2_LONG = 6;
|
|
+ private static final long ALL_SET = -1L;
|
|
+ private static final int BITS_PER_LONG = Long.SIZE;
|
|
+
|
|
+ // from inclusive
|
|
+ // to exclusive
|
|
+ public static int firstSet(final long[] bitset, final int from, final int to) {
|
|
+ if ((from | to | (to - from)) < 0) {
|
|
+ throw new IndexOutOfBoundsException();
|
|
+ }
|
|
+
|
|
+ int bitsetIdx = from >>> LOG2_LONG;
|
|
+ int bitIdx = from & ~(BITS_PER_LONG - 1);
|
|
+
|
|
+ long tmp = bitset[bitsetIdx] & (ALL_SET << from);
|
|
+ for (;;) {
|
|
+ if (tmp != 0L) {
|
|
+ final int ret = bitIdx | Long.numberOfTrailingZeros(tmp);
|
|
+ return ret >= to ? -1 : ret;
|
|
+ }
|
|
+
|
|
+ bitIdx += BITS_PER_LONG;
|
|
+
|
|
+ if (bitIdx >= to) {
|
|
+ return -1;
|
|
+ }
|
|
+
|
|
+ tmp = bitset[++bitsetIdx];
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // from inclusive
|
|
+ // to exclusive
|
|
+ public static int firstClear(final long[] bitset, final int from, final int to) {
|
|
+ if ((from | to | (to - from)) < 0) {
|
|
+ throw new IndexOutOfBoundsException();
|
|
+ }
|
|
+ // like firstSet, but invert the bitset
|
|
+
|
|
+ int bitsetIdx = from >>> LOG2_LONG;
|
|
+ int bitIdx = from & ~(BITS_PER_LONG - 1);
|
|
+
|
|
+ long tmp = (~bitset[bitsetIdx]) & (ALL_SET << from);
|
|
+ for (;;) {
|
|
+ if (tmp != 0L) {
|
|
+ final int ret = bitIdx | Long.numberOfTrailingZeros(tmp);
|
|
+ return ret >= to ? -1 : ret;
|
|
+ }
|
|
+
|
|
+ bitIdx += BITS_PER_LONG;
|
|
+
|
|
+ if (bitIdx >= to) {
|
|
+ return -1;
|
|
+ }
|
|
+
|
|
+ tmp = ~bitset[++bitsetIdx];
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // from inclusive
|
|
+ // to exclusive
|
|
+ public static void clearRange(final long[] bitset, final int from, int to) {
|
|
+ if ((from | to | (to - from)) < 0) {
|
|
+ throw new IndexOutOfBoundsException();
|
|
+ }
|
|
+
|
|
+ if (from == to) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ --to;
|
|
+
|
|
+ final int fromBitsetIdx = from >>> LOG2_LONG;
|
|
+ final int toBitsetIdx = to >>> LOG2_LONG;
|
|
+
|
|
+ final long keepFirst = ~(ALL_SET << from);
|
|
+ final long keepLast = ~(ALL_SET >>> ((BITS_PER_LONG - 1) ^ to));
|
|
+
|
|
+ Objects.checkFromToIndex(fromBitsetIdx, toBitsetIdx, bitset.length);
|
|
+
|
|
+ if (fromBitsetIdx == toBitsetIdx) {
|
|
+ // special case: need to keep both first and last
|
|
+ bitset[fromBitsetIdx] &= (keepFirst | keepLast);
|
|
+ } else {
|
|
+ bitset[fromBitsetIdx] &= keepFirst;
|
|
+
|
|
+ for (int i = fromBitsetIdx + 1; i < toBitsetIdx; ++i) {
|
|
+ bitset[i] = 0L;
|
|
+ }
|
|
+
|
|
+ bitset[toBitsetIdx] &= keepLast;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // from inclusive
|
|
+ // to exclusive
|
|
+ public static boolean isRangeSet(final long[] bitset, final int from, final int to) {
|
|
+ return firstClear(bitset, from, to) == -1;
|
|
+ }
|
|
+
|
|
+
|
|
+ private FlatBitsetUtil() {}
|
|
+}
|
|
diff --git a/src/main/java/io/papermc/paper/util/collisions/MergedORCache.java b/src/main/java/io/papermc/paper/util/collisions/MergedORCache.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..1f42bdfdb052056e62a939ab0d1944f8a064fe6c
|
|
--- /dev/null
|
|
+++ b/src/main/java/io/papermc/paper/util/collisions/MergedORCache.java
|
|
@@ -0,0 +1,10 @@
|
|
+package io.papermc.paper.util.collisions;
|
|
+
|
|
+import net.minecraft.world.phys.shapes.VoxelShape;
|
|
+
|
|
+public record MergedORCache(
|
|
+ VoxelShape key,
|
|
+ VoxelShape result
|
|
+) {
|
|
+
|
|
+}
|
|
diff --git a/src/main/java/net/minecraft/core/Direction.java b/src/main/java/net/minecraft/core/Direction.java
|
|
index 75694cfd7d8adde6b9246518c20fe75774297a57..84a760fdc50bdafc9150f977e9c5d557a30ee220 100644
|
|
--- a/src/main/java/net/minecraft/core/Direction.java
|
|
+++ b/src/main/java/net/minecraft/core/Direction.java
|
|
@@ -53,6 +53,21 @@ public enum Direction implements StringRepresentable {
|
|
private final int adjY;
|
|
private final int adjZ;
|
|
// Paper end - Perf: Inline shift direction fields
|
|
+ // Paper start - optimise collisions
|
|
+ private static final int RANDOM_OFFSET = 2017601568;
|
|
+ private Direction opposite;
|
|
+ static {
|
|
+ for (final Direction direction : VALUES) {
|
|
+ direction.opposite = from3DDataValue(direction.oppositeIndex);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private final int id = it.unimi.dsi.fastutil.HashCommon.murmurHash3(it.unimi.dsi.fastutil.HashCommon.murmurHash3(this.ordinal() + RANDOM_OFFSET) + RANDOM_OFFSET);
|
|
+
|
|
+ public final int uniqueId() {
|
|
+ return this.id;
|
|
+ }
|
|
+ // Paper end - optimise collisions
|
|
|
|
private Direction(int id, int idOpposite, int idHorizontal, String name, Direction.AxisDirection direction, Direction.Axis axis, Vec3i vector) {
|
|
this.data3d = id;
|
|
diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
|
|
index acc1751324f040accc4fc18914ed281e572358eb..17a6d43685f35a6978c2d941876a1f8a9a2c8b42 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
|
|
@@ -496,7 +496,7 @@ public class ServerPlayer extends Player {
|
|
|
|
if (blockposition1 != null) {
|
|
this.moveTo(blockposition1, world.getSharedSpawnAngle(), 0.0F); // Paper - MC-200092 - fix first spawn pos yaw being ignored
|
|
- if (world.noCollision((Entity) this)) {
|
|
+ if (world.noCollision(this, this.getBoundingBox(), true)) { // Paper - make sure this loads chunks, we default to NOT loading now
|
|
break;
|
|
}
|
|
}
|
|
@@ -504,7 +504,7 @@ public class ServerPlayer extends Player {
|
|
} else {
|
|
this.moveTo(blockposition, world.getSharedSpawnAngle(), 0.0F); // Paper - MC-200092 - fix first spawn pos yaw being ignored
|
|
|
|
- while (!world.noCollision((Entity) this) && this.getY() < (double) (world.getMaxBuildHeight() - 1)) {
|
|
+ while (!world.noCollision(this, this.getBoundingBox(), true) && this.getY() < (double) (world.getMaxBuildHeight() - 1)) { // Paper - make sure this loads chunks, we default to NOT loading now
|
|
this.setPos(this.getX(), this.getY() + 1.0D, this.getZ());
|
|
}
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
|
|
index 594cb6ce4bfa6c42212000a1ed983ea95ee2c4bf..97b0119ac71284b3a223c089bec26d87a01d3b25 100644
|
|
--- a/src/main/java/net/minecraft/server/players/PlayerList.java
|
|
+++ b/src/main/java/net/minecraft/server/players/PlayerList.java
|
|
@@ -936,7 +936,7 @@ public abstract class PlayerList {
|
|
entityplayer1.forceSetPositionRotation(location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch());
|
|
|
|
worldserver1.getChunkSource().addRegionTicket(net.minecraft.server.level.TicketType.POST_TELEPORT, new net.minecraft.world.level.ChunkPos(location.getBlockX() >> 4, location.getBlockZ() >> 4), 1, entityplayer.getId()); // Paper
|
|
- while (avoidSuffocation && !worldserver1.noCollision((Entity) entityplayer1) && entityplayer1.getY() < (double) worldserver1.getMaxBuildHeight()) {
|
|
+ while (avoidSuffocation && !worldserver1.noCollision(entityplayer1, entityplayer1.getBoundingBox(), true) && entityplayer1.getY() < (double) worldserver1.getMaxBuildHeight()) { // Paper - make sure this loads chunks, we default to NOT loading now
|
|
// CraftBukkit end
|
|
entityplayer1.setPos(entityplayer1.getX(), entityplayer1.getY() + 1.0D, entityplayer1.getZ());
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java
|
|
index 9a01eff5a93c68edd45f98e9a6f8d24656650fb6..7992375dc55492aeb6defb204b28dd267be4a6e7 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/Entity.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/Entity.java
|
|
@@ -1250,9 +1250,44 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, S
|
|
float f = this.getBlockSpeedFactor();
|
|
|
|
this.setDeltaMovement(this.getDeltaMovement().multiply((double) f, 1.0D, (double) f));
|
|
- if (this.level().getBlockStatesIfLoaded(this.getBoundingBox().deflate(1.0E-6D)).noneMatch((iblockdata2) -> {
|
|
- return iblockdata2.is(BlockTags.FIRE) || iblockdata2.is(Blocks.LAVA);
|
|
- })) {
|
|
+ // Paper start - remove expensive streams from here
|
|
+ boolean noneMatch = true;
|
|
+ AABB fireSearchBox = this.getBoundingBox().deflate(1.0E-6D);
|
|
+ {
|
|
+ int minX = Mth.floor(fireSearchBox.minX);
|
|
+ int minY = Mth.floor(fireSearchBox.minY);
|
|
+ int minZ = Mth.floor(fireSearchBox.minZ);
|
|
+ int maxX = Mth.floor(fireSearchBox.maxX);
|
|
+ int maxY = Mth.floor(fireSearchBox.maxY);
|
|
+ int maxZ = Mth.floor(fireSearchBox.maxZ);
|
|
+ fire_search_loop:
|
|
+ for (int fz = minZ; fz <= maxZ; ++fz) {
|
|
+ for (int fx = minX; fx <= maxX; ++fx) {
|
|
+ for (int fy = minY; fy <= maxY; ++fy) {
|
|
+ net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk)this.level.getChunkIfLoadedImmediately(fx >> 4, fz >> 4);
|
|
+ if (chunk == null) {
|
|
+ // Vanilla rets an empty stream if all the chunks are not loaded, so noneMatch will be true
|
|
+ // even if we're in lava/fire
|
|
+ noneMatch = true;
|
|
+ break fire_search_loop;
|
|
+ }
|
|
+ if (!noneMatch) {
|
|
+ // don't do get type, we already know we're in fire - we just need to check the chunks
|
|
+ // loaded state
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ BlockState type = chunk.getBlockStateFinal(fx, fy, fz);
|
|
+ if (type.is(BlockTags.FIRE) || type.is(Blocks.LAVA)) {
|
|
+ noneMatch = false;
|
|
+ // can't break, we need to retain vanilla behavior by ensuring ALL chunks are loaded
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ if (noneMatch) {
|
|
+ // Paper end - remove expensive streams from here
|
|
if (this.remainingFireTicks <= 0) {
|
|
this.setRemainingFireTicks(-this.getFireImmuneTicks());
|
|
}
|
|
@@ -1432,32 +1467,82 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, S
|
|
}
|
|
|
|
private Vec3 collide(Vec3 movement) {
|
|
- AABB axisalignedbb = this.getBoundingBox();
|
|
- List<VoxelShape> list = this.level().getEntityCollisions(this, axisalignedbb.expandTowards(movement));
|
|
- Vec3 vec3d1 = movement.lengthSqr() == 0.0D ? movement : Entity.collideBoundingBox(this, movement, axisalignedbb, this.level(), list);
|
|
- boolean flag = movement.x != vec3d1.x;
|
|
- boolean flag1 = movement.y != vec3d1.y;
|
|
- boolean flag2 = movement.z != vec3d1.z;
|
|
- boolean flag3 = this.onGround() || flag1 && movement.y < 0.0D;
|
|
+ // Paper start - optimise collisions
|
|
+ final boolean xZero = movement.x == 0.0;
|
|
+ final boolean yZero = movement.y == 0.0;
|
|
+ final boolean zZero = movement.z == 0.0;
|
|
+ if (xZero & yZero & zZero) {
|
|
+ return movement;
|
|
+ }
|
|
+
|
|
+ final Level world = this.level;
|
|
+ final AABB currBoundingBox = this.getBoundingBox();
|
|
+
|
|
+ if (io.papermc.paper.util.CollisionUtil.isEmpty(currBoundingBox)) {
|
|
+ return movement;
|
|
+ }
|
|
+
|
|
+ final List<AABB> potentialCollisionsBB = new java.util.ArrayList<>();
|
|
+ final List<VoxelShape> potentialCollisionsVoxel = new java.util.ArrayList<>();
|
|
+ final double stepHeight = (double)this.maxUpStep();
|
|
+ final AABB collisionBox;
|
|
+ final boolean onGround = this.onGround;
|
|
+
|
|
+ if (xZero & zZero) {
|
|
+ if (movement.y > 0.0) {
|
|
+ collisionBox = io.papermc.paper.util.CollisionUtil.cutUpwards(currBoundingBox, movement.y);
|
|
+ } else {
|
|
+ collisionBox = io.papermc.paper.util.CollisionUtil.cutDownwards(currBoundingBox, movement.y);
|
|
+ }
|
|
+ } else {
|
|
+ // note: xZero == false or zZero == false
|
|
+ if (stepHeight > 0.0 && (onGround || (movement.y < 0.0))) {
|
|
+ // don't bother getting the collisions if we don't need them.
|
|
+ if (movement.y <= 0.0) {
|
|
+ collisionBox = io.papermc.paper.util.CollisionUtil.expandUpwards(currBoundingBox.expandTowards(movement.x, movement.y, movement.z), stepHeight);
|
|
+ } else {
|
|
+ collisionBox = currBoundingBox.expandTowards(movement.x, Math.max(stepHeight, movement.y), movement.z);
|
|
+ }
|
|
+ } else {
|
|
+ collisionBox = currBoundingBox.expandTowards(movement.x, movement.y, movement.z);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ io.papermc.paper.util.CollisionUtil.getCollisions(
|
|
+ world, this, collisionBox, potentialCollisionsVoxel, potentialCollisionsBB,
|
|
+ io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_BORDER,
|
|
+ null, null
|
|
+ );
|
|
+
|
|
+ if (potentialCollisionsVoxel.isEmpty() && potentialCollisionsBB.isEmpty()) {
|
|
+ return movement;
|
|
+ }
|
|
|
|
- if (this.maxUpStep() > 0.0F && flag3 && (flag || flag2)) {
|
|
- Vec3 vec3d2 = Entity.collideBoundingBox(this, new Vec3(movement.x, (double) this.maxUpStep(), movement.z), axisalignedbb, this.level(), list);
|
|
- Vec3 vec3d3 = Entity.collideBoundingBox(this, new Vec3(0.0D, (double) this.maxUpStep(), 0.0D), axisalignedbb.expandTowards(movement.x, 0.0D, movement.z), this.level(), list);
|
|
+ final Vec3 limitedMoveVector = io.papermc.paper.util.CollisionUtil.performCollisions(movement, currBoundingBox, potentialCollisionsVoxel, potentialCollisionsBB);
|
|
|
|
- if (vec3d3.y < (double) this.maxUpStep()) {
|
|
- Vec3 vec3d4 = Entity.collideBoundingBox(this, new Vec3(movement.x, 0.0D, movement.z), axisalignedbb.move(vec3d3), this.level(), list).add(vec3d3);
|
|
+ if (stepHeight > 0.0
|
|
+ && (onGround || (limitedMoveVector.y != movement.y && movement.y < 0.0))
|
|
+ && (limitedMoveVector.x != movement.x || limitedMoveVector.z != movement.z)) {
|
|
+ Vec3 vec3d2 = io.papermc.paper.util.CollisionUtil.performCollisions(new Vec3(movement.x, stepHeight, movement.z), currBoundingBox, potentialCollisionsVoxel, potentialCollisionsBB);
|
|
+ final Vec3 vec3d3 = io.papermc.paper.util.CollisionUtil.performCollisions(new Vec3(0.0, stepHeight, 0.0), currBoundingBox.expandTowards(movement.x, 0.0, movement.z), potentialCollisionsVoxel, potentialCollisionsBB);
|
|
+
|
|
+ if (vec3d3.y < stepHeight) {
|
|
+ final Vec3 vec3d4 = io.papermc.paper.util.CollisionUtil.performCollisions(new Vec3(movement.x, 0.0D, movement.z), currBoundingBox.move(vec3d3), potentialCollisionsVoxel, potentialCollisionsBB).add(vec3d3);
|
|
|
|
if (vec3d4.horizontalDistanceSqr() > vec3d2.horizontalDistanceSqr()) {
|
|
vec3d2 = vec3d4;
|
|
}
|
|
}
|
|
|
|
- if (vec3d2.horizontalDistanceSqr() > vec3d1.horizontalDistanceSqr()) {
|
|
- return vec3d2.add(Entity.collideBoundingBox(this, new Vec3(0.0D, -vec3d2.y + movement.y, 0.0D), axisalignedbb.move(vec3d2), this.level(), list));
|
|
+ if (vec3d2.horizontalDistanceSqr() > limitedMoveVector.horizontalDistanceSqr()) {
|
|
+ return vec3d2.add(io.papermc.paper.util.CollisionUtil.performCollisions(new Vec3(0.0D, -vec3d2.y + movement.y, 0.0D), currBoundingBox.move(vec3d2), potentialCollisionsVoxel, potentialCollisionsBB));
|
|
}
|
|
- }
|
|
|
|
- return vec3d1;
|
|
+ return limitedMoveVector;
|
|
+ } else {
|
|
+ return limitedMoveVector;
|
|
+ }
|
|
+ // Paper end - optimise collisions
|
|
}
|
|
|
|
public static Vec3 collideBoundingBox(@Nullable Entity entity, Vec3 movement, AABB entityBoundingBox, Level world, List<VoxelShape> collisions) {
|
|
@@ -2707,11 +2792,70 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, S
|
|
float f = this.dimensions.width * 0.8F;
|
|
AABB axisalignedbb = AABB.ofSize(this.getEyePosition(), (double) f, 1.0E-6D, (double) f);
|
|
|
|
- return BlockPos.betweenClosedStream(axisalignedbb).anyMatch((blockposition) -> {
|
|
- BlockState iblockdata = this.level().getBlockState(blockposition);
|
|
+ // Paper start - optimise collisions
|
|
+ if (io.papermc.paper.util.CollisionUtil.isEmpty(axisalignedbb)) {
|
|
+ return false;
|
|
+ }
|
|
|
|
- return !iblockdata.isAir() && iblockdata.isSuffocating(this.level(), blockposition) && Shapes.joinIsNotEmpty(iblockdata.getCollisionShape(this.level(), blockposition).move((double) blockposition.getX(), (double) blockposition.getY(), (double) blockposition.getZ()), Shapes.create(axisalignedbb), BooleanOp.AND);
|
|
- });
|
|
+ final BlockPos.MutableBlockPos tempPos = new BlockPos.MutableBlockPos();
|
|
+
|
|
+ final int minX = Mth.floor(axisalignedbb.minX);
|
|
+ final int minY = Mth.floor(axisalignedbb.minY);
|
|
+ final int minZ = Mth.floor(axisalignedbb.minZ);
|
|
+ final int maxX = Mth.floor(axisalignedbb.maxX);
|
|
+ final int maxY = Mth.floor(axisalignedbb.maxY);
|
|
+ final int maxZ = Mth.floor(axisalignedbb.maxZ);
|
|
+
|
|
+ final net.minecraft.server.level.ServerChunkCache chunkProvider = (net.minecraft.server.level.ServerChunkCache)this.level.getChunkSource();
|
|
+
|
|
+ long lastChunkKey = ChunkPos.INVALID_CHUNK_POS;
|
|
+ net.minecraft.world.level.chunk.LevelChunk lastChunk = null;
|
|
+ for (int fz = minZ; fz <= maxZ; ++fz) {
|
|
+ tempPos.setZ(fz);
|
|
+ for (int fx = minX; fx <= maxX; ++fx) {
|
|
+ final int newChunkX = fx >> 4;
|
|
+ final int newChunkZ = fz >> 4;
|
|
+ final net.minecraft.world.level.chunk.LevelChunk chunk = lastChunkKey == (lastChunkKey = io.papermc.paper.util.CoordinateUtils.getChunkKey(newChunkX, newChunkZ)) ?
|
|
+ lastChunk : (lastChunk = chunkProvider.getChunkAtIfLoadedImmediately(newChunkX, newChunkZ));
|
|
+ tempPos.setX(fx);
|
|
+ if (chunk == null) {
|
|
+ continue;
|
|
+ }
|
|
+ for (int fy = minY; fy <= maxY; ++fy) {
|
|
+ tempPos.setY(fy);
|
|
+
|
|
+ final BlockState state = chunk.getBlockState(tempPos);
|
|
+
|
|
+ if (state.emptyCollisionShape() || !state.isSuffocating(this.level, tempPos)) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ // Yes, it does not use the Entity context stuff.
|
|
+ final VoxelShape collisionShape = state.getCollisionShape(this.level, tempPos);
|
|
+
|
|
+ if (collisionShape.isEmpty()) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ final AABB toCollide = axisalignedbb.move(-(double)fx, -(double)fy, -(double)fz);
|
|
+
|
|
+ final AABB singleAABB = collisionShape.getSingleAABBRepresentation();
|
|
+ if (singleAABB != null) {
|
|
+ if (io.papermc.paper.util.CollisionUtil.voxelShapeIntersect(singleAABB, toCollide)) {
|
|
+ return true;
|
|
+ }
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ if (io.papermc.paper.util.CollisionUtil.voxelShapeIntersectNoEmpty(collisionShape, toCollide)) {
|
|
+ return true;
|
|
+ }
|
|
+ continue;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ // Paper end - optimise collisions
|
|
+ return false;
|
|
}
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java b/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java
|
|
index bbe299afd361a107e3936c8ea1a62067fcac9b7e..eadcebd7845ee716e33c0ac0544502da1a6c5941 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java
|
|
@@ -354,7 +354,7 @@ public class ArmorStand extends LivingEntity {
|
|
@Override
|
|
protected void pushEntities() {
|
|
if (!this.level().paperConfig().entities.armorStands.doCollisionEntityLookups) return; // Paper - Option to prevent armor stands from doing entity lookups
|
|
- List<Entity> list = this.level().getEntities((Entity) this, this.getBoundingBox(), ArmorStand.RIDABLE_MINECARTS);
|
|
+ List<AbstractMinecart> list = this.level().getEntitiesOfClass(AbstractMinecart.class, this.getBoundingBox(), ArmorStand.RIDABLE_MINECARTS); // Paper - optimise collisions
|
|
Iterator iterator = list.iterator();
|
|
|
|
while (iterator.hasNext()) {
|
|
diff --git a/src/main/java/net/minecraft/world/entity/monster/Spider.java b/src/main/java/net/minecraft/world/entity/monster/Spider.java
|
|
index ffa4f34d964fbcc53e2dfe11677832db21a6eb93..7618364e5373fe17cfe45a5a4ee9ab25e591581c 100644
|
|
--- a/src/main/java/net/minecraft/world/entity/monster/Spider.java
|
|
+++ b/src/main/java/net/minecraft/world/entity/monster/Spider.java
|
|
@@ -86,7 +86,7 @@ public class Spider extends Monster {
|
|
public void tick() {
|
|
super.tick();
|
|
if (!this.level().isClientSide) {
|
|
- this.setClimbing(this.horizontalCollision && (this.level().paperConfig().entities.behavior.allowSpiderWorldBorderClimbing)); // Paper - Add config option for spider worldborder climbing
|
|
+ this.setClimbing(this.horizontalCollision && (this.level().paperConfig().entities.behavior.allowSpiderWorldBorderClimbing || !io.papermc.paper.util.CollisionUtil.isCollidingWithBorder(this.level().getWorldBorder(), this.getBoundingBox().inflate(io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)))); // Paper - Add config option for spider worldborder climbing & Inflate by +EPSILON as collision will just barely place us outside border
|
|
}
|
|
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/level/BlockCollisions.java b/src/main/java/net/minecraft/world/level/BlockCollisions.java
|
|
index cd89623a44f02d7db77f0d0f87545cf80841f403..48710a60561824a3670ebef3601f284dd7089481 100644
|
|
--- a/src/main/java/net/minecraft/world/level/BlockCollisions.java
|
|
+++ b/src/main/java/net/minecraft/world/level/BlockCollisions.java
|
|
@@ -99,7 +99,7 @@ public class BlockCollisions<T> extends AbstractIterator<T> {
|
|
// Paper end
|
|
VoxelShape voxelShape = blockState.getCollisionShape(this.collisionGetter, this.pos, this.context);
|
|
if (voxelShape == Shapes.block()) {
|
|
- if (this.box.intersects((double)i, (double)j, (double)k, (double)i + 1.0, (double)j + 1.0, (double)k + 1.0)) {
|
|
+ if (io.papermc.paper.util.CollisionUtil.voxelShapeIntersect(this.box, i, j, k, i + 1.0, j + 1.0, k + 1.0)) { // Paper - keep vanilla behavior for voxelshape intersection - See comment in CollisionUtil
|
|
return this.resultProvider.apply(this.pos, voxelShape.move((double)i, (double)j, (double)k));
|
|
}
|
|
} else {
|
|
diff --git a/src/main/java/net/minecraft/world/level/ClipContext.java b/src/main/java/net/minecraft/world/level/ClipContext.java
|
|
index 86a4f30c8784c602436ecf1c78efb0bdca4b7089..b0bea28e9261767c60d30fb0e76f4f3af8a5634e 100644
|
|
--- a/src/main/java/net/minecraft/world/level/ClipContext.java
|
|
+++ b/src/main/java/net/minecraft/world/level/ClipContext.java
|
|
@@ -17,8 +17,8 @@ public class ClipContext {
|
|
|
|
private final Vec3 from;
|
|
private final Vec3 to;
|
|
- private final ClipContext.Block block;
|
|
- private final ClipContext.Fluid fluid;
|
|
+ public final ClipContext.Block block; // Paper - optimise collisions - public
|
|
+ public final ClipContext.Fluid fluid; // Paper - optimise collisions - public
|
|
private final CollisionContext collisionContext;
|
|
|
|
public ClipContext(Vec3 start, Vec3 end, ClipContext.Block shapeType, ClipContext.Fluid fluidHandling, Entity entity) {
|
|
diff --git a/src/main/java/net/minecraft/world/level/CollisionGetter.java b/src/main/java/net/minecraft/world/level/CollisionGetter.java
|
|
index 1ad0c976c6e2d6d31397dff850a9de7c16d16fba..dc877fe2e3c53b353baa59c125232e425fee67d7 100644
|
|
--- a/src/main/java/net/minecraft/world/level/CollisionGetter.java
|
|
+++ b/src/main/java/net/minecraft/world/level/CollisionGetter.java
|
|
@@ -35,6 +35,12 @@ public interface CollisionGetter extends BlockGetter {
|
|
return this.isUnobstructed(entity, Shapes.create(entity.getBoundingBox()));
|
|
}
|
|
|
|
+ // Paper start - optimise collisions
|
|
+ default boolean noCollision(Entity entity, AABB box, boolean loadChunks) {
|
|
+ return this.noCollision(entity, box);
|
|
+ }
|
|
+ // Paper end - optimise collisions
|
|
+
|
|
default boolean noCollision(AABB box) {
|
|
return this.noCollision(null, box);
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/level/EntityGetter.java b/src/main/java/net/minecraft/world/level/EntityGetter.java
|
|
index 9a28912f52824acdc80a62243b136e6f365bf567..21843501355a0c0c8d594e3e5312e97861c9a777 100644
|
|
--- a/src/main/java/net/minecraft/world/level/EntityGetter.java
|
|
+++ b/src/main/java/net/minecraft/world/level/EntityGetter.java
|
|
@@ -46,20 +46,36 @@ public interface EntityGetter {
|
|
}
|
|
|
|
default boolean isUnobstructed(@Nullable Entity except, VoxelShape shape) {
|
|
+ // Paper start - optimise collisions
|
|
if (shape.isEmpty()) {
|
|
- return true;
|
|
- } else {
|
|
- for (Entity entity : this.getEntities(except, shape.bounds())) {
|
|
- if (!entity.isRemoved()
|
|
- && entity.blocksBuilding
|
|
- && (except == null || !entity.isPassengerOfSameVehicle(except))
|
|
- && Shapes.joinIsNotEmpty(shape, Shapes.create(entity.getBoundingBox()), BooleanOp.AND)) {
|
|
- return false;
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ final AABB singleAABB = shape.getSingleAABBRepresentation();
|
|
+ final List<Entity> entities = this.getEntities(
|
|
+ except,
|
|
+ singleAABB == null ? shape.bounds() : singleAABB.inflate(-io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON, -io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON, -io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)
|
|
+ );
|
|
+
|
|
+ for (int i = 0, len = entities.size(); i < len; ++i) {
|
|
+ final Entity otherEntity = entities.get(i);
|
|
+
|
|
+ if (otherEntity.isRemoved() || !otherEntity.blocksBuilding || (except != null && otherEntity.isPassengerOfSameVehicle(except))) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ if (singleAABB == null) {
|
|
+ final AABB entityBB = otherEntity.getBoundingBox();
|
|
+ if (io.papermc.paper.util.CollisionUtil.isEmpty(entityBB) || !io.papermc.paper.util.CollisionUtil.voxelShapeIntersectNoEmpty(shape, entityBB)) {
|
|
+ continue;
|
|
}
|
|
}
|
|
|
|
- return true;
|
|
+ return false;
|
|
}
|
|
+
|
|
+ return true;
|
|
+ // Paper end - optimise collisions
|
|
}
|
|
|
|
default <T extends Entity> List<T> getEntitiesOfClass(Class<T> entityClass, AABB box) {
|
|
@@ -67,23 +83,41 @@ public interface EntityGetter {
|
|
}
|
|
|
|
default List<VoxelShape> getEntityCollisions(@Nullable Entity entity, AABB box) {
|
|
- if (box.getSize() < 1.0E-7) {
|
|
- return List.of();
|
|
+ // Paper start - optimise collisions
|
|
+ // first behavior change is to correctly check for empty AABB
|
|
+ if (io.papermc.paper.util.CollisionUtil.isEmpty(box)) {
|
|
+ // reduce indirection by always returning type with same class
|
|
+ return new java.util.ArrayList<>();
|
|
+ }
|
|
+
|
|
+ // to comply with vanilla intersection rules, expand by -epsilon so that we only get stuff we definitely collide with.
|
|
+ // Vanilla for hard collisions has this backwards, and they expand by +epsilon but this causes terrible problems
|
|
+ // specifically with boat collisions.
|
|
+ box = box.inflate(-io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON, -io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON, -io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON);
|
|
+
|
|
+ final List<Entity> entities;
|
|
+ if (entity != null && entity.hardCollides()) {
|
|
+ entities = this.getEntities(entity, box, null);
|
|
} else {
|
|
- Predicate<Entity> predicate = entity == null ? EntitySelector.CAN_BE_COLLIDED_WITH : EntitySelector.NO_SPECTATORS.and(entity::canCollideWith);
|
|
- List<Entity> list = this.getEntities(entity, box.inflate(1.0E-7), predicate);
|
|
- if (list.isEmpty()) {
|
|
- return List.of();
|
|
- } else {
|
|
- Builder<VoxelShape> builder = ImmutableList.builderWithExpectedSize(list.size());
|
|
-
|
|
- for (Entity entity2 : list) {
|
|
- builder.add(Shapes.create(entity2.getBoundingBox()));
|
|
- }
|
|
+ entities = this.getHardCollidingEntities(entity, box, null);
|
|
+ }
|
|
+
|
|
+ final List<VoxelShape> ret = new java.util.ArrayList<>(Math.min(25, entities.size()));
|
|
|
|
- return builder.build();
|
|
+ for (int i = 0, len = entities.size(); i < len; ++i) {
|
|
+ final Entity otherEntity = entities.get(i);
|
|
+
|
|
+ if (otherEntity.isSpectator()) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ if ((entity == null && otherEntity.canBeCollidedWith()) || (entity != null && entity.canCollideWith(otherEntity))) {
|
|
+ ret.add(Shapes.create(otherEntity.getBoundingBox()));
|
|
}
|
|
}
|
|
+
|
|
+ return ret;
|
|
+ // Paper end - optimise collisions
|
|
}
|
|
|
|
// Paper start - Affects Spawning API
|
|
diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
|
|
index 47e83adf64df673bc40077335baf786f865411e8..bb57f97dbc2fcc7c28ebfb54ff00796fc7f51efe 100644
|
|
--- a/src/main/java/net/minecraft/world/level/Level.java
|
|
+++ b/src/main/java/net/minecraft/world/level/Level.java
|
|
@@ -294,6 +294,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
this.entityLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.entityMaxTickTime);
|
|
this.tileLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.tileMaxTickTime);
|
|
this.chunkPacketBlockController = this.paperConfig().anticheat.antiXray.enabled ? new com.destroystokyo.paper.antixray.ChunkPacketBlockControllerAntiXray(this, executor) : com.destroystokyo.paper.antixray.ChunkPacketBlockController.NO_OPERATION_INSTANCE; // Paper - Anti-Xray
|
|
+ // Paper start - optimise collisions
|
|
+ this.minSection = io.papermc.paper.util.WorldUtil.getMinSection(this);
|
|
+ this.maxSection = io.papermc.paper.util.WorldUtil.getMaxSection(this);
|
|
+ // Paper end - optimise collisions
|
|
}
|
|
|
|
// Paper start - Cancel hit for vanished players
|
|
@@ -335,6 +339,366 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
return true;
|
|
}
|
|
// Paper end - Cancel hit for vanished players
|
|
+ // Paper start - optimise collisions
|
|
+ public final int minSection;
|
|
+ public final int maxSection;
|
|
+
|
|
+ @Override
|
|
+ public final boolean isUnobstructed(final Entity entity) {
|
|
+ final AABB boundingBox = entity.getBoundingBox();
|
|
+ if (io.papermc.paper.util.CollisionUtil.isEmpty(boundingBox)) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ final List<Entity> entities = this.getEntities(
|
|
+ entity,
|
|
+ boundingBox.inflate(-io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON, -io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON, -io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON),
|
|
+ null
|
|
+ );
|
|
+
|
|
+ for (int i = 0, len = entities.size(); i < len; ++i) {
|
|
+ final Entity otherEntity = entities.get(i);
|
|
+
|
|
+ if (otherEntity.isSpectator() || otherEntity.isRemoved() || !otherEntity.blocksBuilding || otherEntity.isPassengerOfSameVehicle(entity)) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ private static net.minecraft.world.phys.BlockHitResult miss(final ClipContext clipContext) {
|
|
+ final Vec3 to = clipContext.getTo();
|
|
+ final Vec3 from = clipContext.getFrom();
|
|
+
|
|
+ return net.minecraft.world.phys.BlockHitResult.miss(to, Direction.getNearest(from.x - to.x, from.y - to.y, from.z - to.z), BlockPos.containing(to.x, to.y, to.z));
|
|
+ }
|
|
+
|
|
+ private static final FluidState AIR_FLUIDSTATE = Fluids.EMPTY.defaultFluidState();
|
|
+
|
|
+ private static net.minecraft.world.phys.BlockHitResult fastClip(final Vec3 from, final Vec3 to, final Level level,
|
|
+ final ClipContext clipContext) {
|
|
+ final double adjX = io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON * (from.x - to.x);
|
|
+ final double adjY = io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON * (from.y - to.y);
|
|
+ final double adjZ = io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON * (from.z - to.z);
|
|
+
|
|
+ if (adjX == 0.0 && adjY == 0.0 && adjZ == 0.0) {
|
|
+ return miss(clipContext);
|
|
+ }
|
|
+
|
|
+ final double toXAdj = to.x - adjX;
|
|
+ final double toYAdj = to.y - adjY;
|
|
+ final double toZAdj = to.z - adjZ;
|
|
+ final double fromXAdj = from.x + adjX;
|
|
+ final double fromYAdj = from.y + adjY;
|
|
+ final double fromZAdj = from.z + adjZ;
|
|
+
|
|
+ int currX = Mth.floor(fromXAdj);
|
|
+ int currY = Mth.floor(fromYAdj);
|
|
+ int currZ = Mth.floor(fromZAdj);
|
|
+
|
|
+ final BlockPos.MutableBlockPos currPos = new BlockPos.MutableBlockPos();
|
|
+
|
|
+ final double diffX = toXAdj - fromXAdj;
|
|
+ final double diffY = toYAdj - fromYAdj;
|
|
+ final double diffZ = toZAdj - fromZAdj;
|
|
+
|
|
+ final double dxDouble = Math.signum(diffX);
|
|
+ final double dyDouble = Math.signum(diffY);
|
|
+ final double dzDouble = Math.signum(diffZ);
|
|
+
|
|
+ final int dx = (int)dxDouble;
|
|
+ final int dy = (int)dyDouble;
|
|
+ final int dz = (int)dzDouble;
|
|
+
|
|
+ final double normalizedDiffX = diffX == 0.0 ? Double.MAX_VALUE : dxDouble / diffX;
|
|
+ final double normalizedDiffY = diffY == 0.0 ? Double.MAX_VALUE : dyDouble / diffY;
|
|
+ final double normalizedDiffZ = diffZ == 0.0 ? Double.MAX_VALUE : dzDouble / diffZ;
|
|
+
|
|
+ double normalizedCurrX = normalizedDiffX * (diffX > 0.0 ? (1.0 - Mth.frac(fromXAdj)) : Mth.frac(fromXAdj));
|
|
+ double normalizedCurrY = normalizedDiffY * (diffY > 0.0 ? (1.0 - Mth.frac(fromYAdj)) : Mth.frac(fromYAdj));
|
|
+ double normalizedCurrZ = normalizedDiffZ * (diffZ > 0.0 ? (1.0 - Mth.frac(fromZAdj)) : Mth.frac(fromZAdj));
|
|
+
|
|
+ net.minecraft.world.level.chunk.LevelChunkSection[] lastChunk = null;
|
|
+ net.minecraft.world.level.chunk.PalettedContainer<BlockState> lastSection = null;
|
|
+ int lastChunkX = Integer.MIN_VALUE;
|
|
+ int lastChunkY = Integer.MIN_VALUE;
|
|
+ int lastChunkZ = Integer.MIN_VALUE;
|
|
+
|
|
+ final int minSection = level.minSection;
|
|
+ final net.minecraft.server.level.ServerChunkCache chunkProvider = (net.minecraft.server.level.ServerChunkCache)level.getChunkSource();
|
|
+
|
|
+ for (;;) {
|
|
+ currPos.set(currX, currY, currZ);
|
|
+
|
|
+ final int newChunkX = currX >> 4;
|
|
+ final int newChunkY = currY >> 4;
|
|
+ final int newChunkZ = currZ >> 4;
|
|
+
|
|
+ final int chunkDiff = ((newChunkX ^ lastChunkX) | (newChunkZ ^ lastChunkZ));
|
|
+ final int chunkYDiff = newChunkY ^ lastChunkY;
|
|
+
|
|
+ if ((chunkDiff | chunkYDiff) != 0) {
|
|
+ if (chunkDiff != 0) {
|
|
+ LevelChunk chunk = chunkProvider.getChunkAtIfLoadedImmediately(newChunkX, newChunkZ);
|
|
+ lastChunk = chunk == null ? null : chunk.getSections(); // diff: don't load chunks for this
|
|
+ }
|
|
+ final int sectionY = newChunkY - minSection;
|
|
+ lastSection = lastChunk != null && sectionY >= 0 && sectionY < lastChunk.length ? lastChunk[sectionY].states : null;
|
|
+
|
|
+ lastChunkX = newChunkX;
|
|
+ lastChunkY = newChunkY;
|
|
+ lastChunkZ = newChunkZ;
|
|
+ }
|
|
+
|
|
+ final BlockState blockState;
|
|
+ if (lastSection != null && !(blockState = lastSection.get((currX & 15) | ((currZ & 15) << 4) | ((currY & 15) << (4+4)))).isAir()) {
|
|
+ final net.minecraft.world.phys.shapes.VoxelShape blockCollision = clipContext.getBlockShape(blockState, level, currPos);
|
|
+
|
|
+ final net.minecraft.world.phys.BlockHitResult blockHit = blockCollision.isEmpty() ? null : level.clipWithInteractionOverride(from, to, currPos, blockCollision, blockState);
|
|
+
|
|
+ final net.minecraft.world.phys.shapes.VoxelShape fluidCollision;
|
|
+ final FluidState fluidState;
|
|
+ if (clipContext.fluid != ClipContext.Fluid.NONE && (fluidState = blockState.getFluidState()) != AIR_FLUIDSTATE) {
|
|
+ fluidCollision = clipContext.getFluidShape(fluidState, level, currPos);
|
|
+
|
|
+ final net.minecraft.world.phys.BlockHitResult fluidHit = fluidCollision.clip(from, to, currPos);
|
|
+
|
|
+ if (fluidHit != null) {
|
|
+ if (blockHit == null) {
|
|
+ return fluidHit;
|
|
+ }
|
|
+
|
|
+ return from.distanceToSqr(blockHit.getLocation()) <= from.distanceToSqr(fluidHit.getLocation()) ? blockHit : fluidHit;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (blockHit != null) {
|
|
+ return blockHit;
|
|
+ }
|
|
+ } // else: usually fall here
|
|
+
|
|
+ if (normalizedCurrX > 1.0 && normalizedCurrY > 1.0 && normalizedCurrZ > 1.0) {
|
|
+ return miss(clipContext);
|
|
+ }
|
|
+
|
|
+ // inc the smallest normalized coordinate
|
|
+
|
|
+ if (normalizedCurrX < normalizedCurrY) {
|
|
+ if (normalizedCurrX < normalizedCurrZ) {
|
|
+ currX += dx;
|
|
+ normalizedCurrX += normalizedDiffX;
|
|
+ } else {
|
|
+ // x < y && x >= z <--> z < y && z <= x
|
|
+ currZ += dz;
|
|
+ normalizedCurrZ += normalizedDiffZ;
|
|
+ }
|
|
+ } else if (normalizedCurrY < normalizedCurrZ) {
|
|
+ // y <= x && y < z
|
|
+ currY += dy;
|
|
+ normalizedCurrY += normalizedDiffY;
|
|
+ } else {
|
|
+ // y <= x && z <= y <--> z <= y && z <= x
|
|
+ currZ += dz;
|
|
+ normalizedCurrZ += normalizedDiffZ;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public final net.minecraft.world.phys.BlockHitResult clip(final ClipContext clipContext) {
|
|
+ // can only do this in this class, as not everything that implements BlockGetter can retrieve chunks
|
|
+ return fastClip(clipContext.getFrom(), clipContext.getTo(), this, clipContext);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public final boolean noCollision(final Entity entity, final AABB box, final boolean loadChunks) {
|
|
+ int flags = io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_ONLY;
|
|
+ if (entity != null) {
|
|
+ flags |= io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_BORDER;
|
|
+ }
|
|
+ if (loadChunks) {
|
|
+ flags |= io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_LOAD_CHUNKS;
|
|
+ }
|
|
+ if (io.papermc.paper.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this, entity, box, null, null, flags, null)) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ return !io.papermc.paper.util.CollisionUtil.getEntityHardCollisions(this, entity, box, null, flags, null);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public final boolean collidesWithSuffocatingBlock(final Entity entity, final AABB box) {
|
|
+ return io.papermc.paper.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this, entity, box, null, null,
|
|
+ io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_ONLY,
|
|
+ (final BlockState state, final BlockPos pos) -> {
|
|
+ return state.isSuffocating(Level.this, pos);
|
|
+ }
|
|
+ );
|
|
+ }
|
|
+
|
|
+ private static net.minecraft.world.phys.shapes.VoxelShape inflateAABBToVoxel(final AABB aabb, final double x, final double y, final double z) {
|
|
+ return net.minecraft.world.phys.shapes.Shapes.create(
|
|
+ aabb.minX - x,
|
|
+ aabb.minY - y,
|
|
+ aabb.minZ - z,
|
|
+
|
|
+ aabb.maxX + x,
|
|
+ aabb.maxY + y,
|
|
+ aabb.maxZ + z
|
|
+ );
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public final java.util.Optional<Vec3> findFreePosition(final Entity entity, final net.minecraft.world.phys.shapes.VoxelShape boundsShape, final Vec3 fromPosition,
|
|
+ final double rangeX, final double rangeY, final double rangeZ) {
|
|
+ if (boundsShape.isEmpty()) {
|
|
+ return java.util.Optional.empty();
|
|
+ }
|
|
+
|
|
+ final double expandByX = rangeX * 0.5;
|
|
+ final double expandByY = rangeY * 0.5;
|
|
+ final double expandByZ = rangeZ * 0.5;
|
|
+
|
|
+ // note: it is useless to look at shapes outside of range / 2.0
|
|
+ final AABB collectionVolume = boundsShape.bounds().inflate(expandByX, expandByY, expandByZ);
|
|
+
|
|
+ final List<AABB> aabbs = new java.util.ArrayList<>();
|
|
+ final List<net.minecraft.world.phys.shapes.VoxelShape> voxels = new java.util.ArrayList<>();
|
|
+
|
|
+ io.papermc.paper.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(
|
|
+ this, entity, collectionVolume, voxels, aabbs,
|
|
+ io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_BORDER,
|
|
+ null
|
|
+ );
|
|
+
|
|
+ // push voxels into aabbs
|
|
+ for (int i = 0, len = voxels.size(); i < len; ++i) {
|
|
+ aabbs.addAll(voxels.get(i).toAabbs());
|
|
+ }
|
|
+
|
|
+ // expand AABBs
|
|
+ final net.minecraft.world.phys.shapes.VoxelShape first = aabbs.isEmpty() ? net.minecraft.world.phys.shapes.Shapes.empty() : inflateAABBToVoxel(aabbs.get(0), expandByX, expandByY, expandByZ);
|
|
+ final net.minecraft.world.phys.shapes.VoxelShape[] rest = new net.minecraft.world.phys.shapes.VoxelShape[Math.max(0, aabbs.size() - 1)];
|
|
+
|
|
+ for (int i = 1, len = aabbs.size(); i < len; ++i) {
|
|
+ rest[i - 1] = inflateAABBToVoxel(aabbs.get(i), expandByX, expandByY, expandByZ);
|
|
+ }
|
|
+
|
|
+ // use optimized implementation of ORing the shapes together
|
|
+ final net.minecraft.world.phys.shapes.VoxelShape joined = net.minecraft.world.phys.shapes.Shapes.or(first, rest);
|
|
+
|
|
+ // find free space
|
|
+ // can use unoptimized join here (instead of join()), as closestPointTo uses toAabbs()
|
|
+ final net.minecraft.world.phys.shapes.VoxelShape freeSpace = net.minecraft.world.phys.shapes.Shapes.joinUnoptimized(
|
|
+ boundsShape, joined, net.minecraft.world.phys.shapes.BooleanOp.ONLY_FIRST
|
|
+ );
|
|
+
|
|
+ return freeSpace.closestPointTo(fromPosition);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public final java.util.Optional<BlockPos> findSupportingBlock(final Entity entity, final AABB aabb) {
|
|
+ final int minBlockX = Mth.floor(aabb.minX - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) - 1;
|
|
+ final int maxBlockX = Mth.floor(aabb.maxX + io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) + 1;
|
|
+
|
|
+ final int minBlockY = Mth.floor(aabb.minY - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) - 1;
|
|
+ final int maxBlockY = Mth.floor(aabb.maxY + io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) + 1;
|
|
+
|
|
+ final int minBlockZ = Mth.floor(aabb.minZ - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) - 1;
|
|
+ final int maxBlockZ = Mth.floor(aabb.maxZ + io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) + 1;
|
|
+
|
|
+ io.papermc.paper.util.CollisionUtil.LazyEntityCollisionContext collisionContext = null;
|
|
+
|
|
+ final BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
|
+ BlockPos selected = null;
|
|
+ double selectedDistance = Double.MAX_VALUE;
|
|
+
|
|
+ final Vec3 entityPos = entity.position();
|
|
+
|
|
+ LevelChunk lastChunk = null;
|
|
+ int lastChunkX = Integer.MIN_VALUE;
|
|
+ int lastChunkZ = Integer.MIN_VALUE;
|
|
+
|
|
+ final net.minecraft.server.level.ServerChunkCache chunkProvider = (net.minecraft.server.level.ServerChunkCache)this.getChunkSource();
|
|
+
|
|
+ for (int currZ = minBlockZ; currZ <= maxBlockZ; ++currZ) {
|
|
+ pos.setZ(currZ);
|
|
+ for (int currX = minBlockX; currX <= maxBlockX; ++currX) {
|
|
+ pos.setX(currX);
|
|
+
|
|
+ final int newChunkX = currX >> 4;
|
|
+ final int newChunkZ = currZ >> 4;
|
|
+
|
|
+ final int chunkDiff = ((newChunkX ^ lastChunkX) | (newChunkZ ^ lastChunkZ));
|
|
+
|
|
+ if (chunkDiff != 0) {
|
|
+ lastChunk = chunkProvider.getChunkAtIfLoadedImmediately(newChunkX, newChunkZ);
|
|
+ }
|
|
+
|
|
+ if (lastChunk == null) {
|
|
+ continue;
|
|
+ }
|
|
+ for (int currY = minBlockY; currY <= maxBlockY; ++currY) {
|
|
+ int edgeCount = ((currX == minBlockX || currX == maxBlockX) ? 1 : 0) +
|
|
+ ((currY == minBlockY || currY == maxBlockY) ? 1 : 0) +
|
|
+ ((currZ == minBlockZ || currZ == maxBlockZ) ? 1 : 0);
|
|
+ if (edgeCount == 3) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ pos.setY(currY);
|
|
+
|
|
+ final double distance = pos.distToCenterSqr(entityPos);
|
|
+ if (distance > selectedDistance || (distance == selectedDistance && selected.compareTo(pos) >= 0)) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ final BlockState state = lastChunk.getBlockState(currX, currY, currZ);
|
|
+ if (state.emptyCollisionShape()) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ if ((edgeCount != 1 || state.hasLargeCollisionShape()) && (edgeCount != 2 || state.getBlock() == Blocks.MOVING_PISTON)) {
|
|
+ if (collisionContext == null) {
|
|
+ collisionContext = new io.papermc.paper.util.CollisionUtil.LazyEntityCollisionContext(entity);
|
|
+ }
|
|
+ final net.minecraft.world.phys.shapes.VoxelShape blockCollision = state.getCollisionShape(lastChunk, pos, collisionContext);
|
|
+ if (blockCollision.isEmpty()) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ // avoid VoxelShape#move by shifting the entity collision shape instead
|
|
+ final AABB shiftedAABB = aabb.move(-(double)currX, -(double)currY, -(double)currZ);
|
|
+
|
|
+ final AABB singleAABB = blockCollision.getSingleAABBRepresentation();
|
|
+ if (singleAABB != null) {
|
|
+ if (!io.papermc.paper.util.CollisionUtil.voxelShapeIntersect(singleAABB, shiftedAABB)) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ selected = pos.immutable();
|
|
+ selectedDistance = distance;
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ if (!io.papermc.paper.util.CollisionUtil.voxelShapeIntersectNoEmpty(blockCollision, shiftedAABB)) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ selected = pos.immutable();
|
|
+ selectedDistance = distance;
|
|
+ continue;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return java.util.Optional.ofNullable(selected);
|
|
+ }
|
|
+ // Paper end - optimise collisions
|
|
@Override
|
|
public boolean isClientSide() {
|
|
return this.isClientSide;
|
|
@@ -958,7 +1322,17 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
|
|
@Override
|
|
public boolean noCollision(@Nullable Entity entity, AABB box) {
|
|
if (entity instanceof net.minecraft.world.entity.decoration.ArmorStand && !entity.level().paperConfig().entities.armorStands.doCollisionEntityLookups) return false;
|
|
- return LevelAccessor.super.noCollision(entity, box);
|
|
+ // Paper start - optimise collisions
|
|
+ int flags = io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_ONLY;
|
|
+ if (entity != null) {
|
|
+ flags |= io.papermc.paper.util.CollisionUtil.COLLISION_FLAG_CHECK_BORDER;
|
|
+ }
|
|
+ if (io.papermc.paper.util.CollisionUtil.getCollisionsForBlocksOrWorldBorder(this, entity, box, null, null, flags, null)) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ return !io.papermc.paper.util.CollisionUtil.getEntityHardCollisions(this, entity, box, null, flags, null);
|
|
+ // Paper end - optimise collisions
|
|
}
|
|
// Paper end - Option to prevent armor stands from doing entity lookups
|
|
|
|
diff --git a/src/main/java/net/minecraft/world/level/block/Block.java b/src/main/java/net/minecraft/world/level/block/Block.java
|
|
index b60a52788e73de3dcb086c1a4628466b25c9d3ef..22036ed3ea0629bc12981a8d91a03e55cc2117d6 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/Block.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/Block.java
|
|
@@ -284,7 +284,7 @@ public class Block extends BlockBehaviour implements ItemLike {
|
|
}
|
|
|
|
public static boolean isShapeFullBlock(VoxelShape shape) {
|
|
- return (Boolean) Block.SHAPE_FULL_BLOCK_CACHE.getUnchecked(shape);
|
|
+ return shape.isFullBlock(); // Paper - optimise collisions
|
|
}
|
|
|
|
public boolean propagatesSkylightDown(BlockState state, BlockGetter world, BlockPos pos) {
|
|
diff --git a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java
|
|
index e493b34aa8726ed48f8e5db2ae8ea561cc5b1f75..2892e586146cbc560f0bcf4b9af6d0575cb0a82e 100644
|
|
--- a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java
|
|
+++ b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java
|
|
@@ -882,6 +882,10 @@ public abstract class BlockBehaviour implements FeatureElement {
|
|
this.instrument = blockbase_info.instrument;
|
|
this.replaceable = blockbase_info.replaceable;
|
|
this.conditionallyFullOpaque = this.canOcclude & this.useShapeForLightOcclusion; // Paper
|
|
+ // Paper start - optimise collisions
|
|
+ this.id1 = it.unimi.dsi.fastutil.HashCommon.murmurHash3(it.unimi.dsi.fastutil.HashCommon.murmurHash3(ID_GENERATOR.getAndIncrement() + RANDOM_OFFSET) + RANDOM_OFFSET);
|
|
+ this.id2 = it.unimi.dsi.fastutil.HashCommon.murmurHash3(it.unimi.dsi.fastutil.HashCommon.murmurHash3(ID_GENERATOR.getAndIncrement() + RANDOM_OFFSET) + RANDOM_OFFSET);
|
|
+ // Paper end - optimise collisions
|
|
}
|
|
// Paper start - Perf: impl cached craft block data, lazy load to fix issue with loading at the wrong time
|
|
private org.bukkit.craftbukkit.block.data.CraftBlockData cachedCraftBlockData;
|
|
@@ -930,6 +934,52 @@ public abstract class BlockBehaviour implements FeatureElement {
|
|
return this.conditionallyFullOpaque;
|
|
}
|
|
// Paper end - starlight
|
|
+ // Paper start - optimise collisions
|
|
+ private static final int RANDOM_OFFSET = 704237939;
|
|
+ private static final Direction[] DIRECTIONS_CACHED = Direction.values();
|
|
+ private static final java.util.concurrent.atomic.AtomicInteger ID_GENERATOR = new java.util.concurrent.atomic.AtomicInteger();
|
|
+ private final int id1, id2;
|
|
+ private boolean occludesFullBlock;
|
|
+ private boolean emptyCollisionShape;
|
|
+ private VoxelShape constantCollisionShape;
|
|
+ private AABB constantAABBCollision;
|
|
+ private static void initCaches(final VoxelShape shape) {
|
|
+ shape.isFullBlock();
|
|
+ shape.occludesFullBlock();
|
|
+ shape.toAabbs();
|
|
+ if (!shape.isEmpty()) {
|
|
+ shape.bounds();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public final boolean hasCache() {
|
|
+ return this.cache != null;
|
|
+ }
|
|
+
|
|
+ public final boolean occludesFullBlock() {
|
|
+ return this.occludesFullBlock;
|
|
+ }
|
|
+
|
|
+ public final boolean emptyCollisionShape() {
|
|
+ return this.emptyCollisionShape;
|
|
+ }
|
|
+
|
|
+ public final int uniqueId1() {
|
|
+ return this.id1;
|
|
+ }
|
|
+
|
|
+ public final int uniqueId2() {
|
|
+ return this.id2;
|
|
+ }
|
|
+
|
|
+ public final VoxelShape getConstantCollisionShape() {
|
|
+ return this.constantCollisionShape;
|
|
+ }
|
|
+
|
|
+ public final AABB getConstantCollisionAABB() {
|
|
+ return this.constantAABBCollision;
|
|
+ }
|
|
+ // Paper end - optimise collisions
|
|
|
|
public void initCache() {
|
|
this.fluidState = ((Block) this.owner).getFluidState(this.asState());
|
|
@@ -941,6 +991,39 @@ public abstract class BlockBehaviour implements FeatureElement {
|
|
this.opacityIfCached = this.cache == null || this.isConditionallyFullOpaque() ? -1 : this.cache.lightBlock; // Paper - starlight - cache opacity for light
|
|
|
|
this.legacySolid = this.calculateSolid();
|
|
+ // Paper start - optimise collisions
|
|
+ if (this.cache != null) {
|
|
+ final VoxelShape collisionShape = this.cache.collisionShape;
|
|
+ try {
|
|
+ this.constantCollisionShape = this.getCollisionShape(null, null, null);
|
|
+ this.constantAABBCollision = this.constantCollisionShape == null ? null : this.constantCollisionShape.getSingleAABBRepresentation();
|
|
+ } catch (final Throwable throwable) {
|
|
+ this.constantCollisionShape = null;
|
|
+ this.constantAABBCollision = null;
|
|
+ }
|
|
+ this.occludesFullBlock = collisionShape.occludesFullBlock();
|
|
+ this.emptyCollisionShape = collisionShape.isEmpty();
|
|
+ // init caches
|
|
+ initCaches(collisionShape);
|
|
+ if (collisionShape != Shapes.empty() && collisionShape != Shapes.block()) {
|
|
+ for (final Direction direction : DIRECTIONS_CACHED) {
|
|
+ // initialise the directional face shape cache as well
|
|
+ final VoxelShape shape = Shapes.getFaceShape(collisionShape, direction);
|
|
+ initCaches(shape);
|
|
+ }
|
|
+ }
|
|
+ if (this.cache.occlusionShapes != null) {
|
|
+ for (final VoxelShape shape : this.cache.occlusionShapes) {
|
|
+ initCaches(shape);
|
|
+ }
|
|
+ }
|
|
+ } else {
|
|
+ this.occludesFullBlock = false;
|
|
+ this.emptyCollisionShape = false;
|
|
+ this.constantCollisionShape = null;
|
|
+ this.constantAABBCollision = null;
|
|
+ }
|
|
+ // Paper end - optimise collisions
|
|
}
|
|
|
|
public Block getBlock() {
|
|
diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
|
|
index eb05c01e85825cbd5b7cf43bc6d261db0b871b92..796bbef3544e06b8e7aac7e8ac5f740a2613f4bd 100644
|
|
--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
|
|
+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
|
|
@@ -26,6 +26,22 @@ public class LevelChunkSection {
|
|
// CraftBukkit start - read/write
|
|
private PalettedContainer<Holder<Biome>> biomes;
|
|
public final com.destroystokyo.paper.util.maplist.IBlockDataList tickingList = new com.destroystokyo.paper.util.maplist.IBlockDataList(); // Paper
|
|
+ // Paper start - optimise collisions
|
|
+ private int specialCollidingBlocks;
|
|
+
|
|
+ private void updateBlockCallback(final int x, final int y, final int z, final BlockState oldState, final BlockState newState) {
|
|
+ if (io.papermc.paper.util.CollisionUtil.isSpecialCollidingBlock(newState)) {
|
|
+ ++this.specialCollidingBlocks;
|
|
+ }
|
|
+ if (io.papermc.paper.util.CollisionUtil.isSpecialCollidingBlock(oldState)) {
|
|
+ --this.specialCollidingBlocks;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public final int getSpecialCollidingBlocks() {
|
|
+ return this.specialCollidingBlocks;
|
|
+ }
|
|
+ // Paper end - optimise collisions
|
|
|
|
public LevelChunkSection(PalettedContainer<BlockState> datapaletteblock, PalettedContainer<Holder<Biome>> palettedcontainerro) {
|
|
// CraftBukkit end
|
|
@@ -62,8 +78,8 @@ public class LevelChunkSection {
|
|
return this.setBlockState(x, y, z, state, true);
|
|
}
|
|
|
|
- public BlockState setBlockState(int x, int y, int z, BlockState state, boolean lock) {
|
|
- BlockState iblockdata1;
|
|
+ public BlockState setBlockState(int x, int y, int z, BlockState state, boolean lock) { // Paper - state -> new state
|
|
+ BlockState iblockdata1; // Paper - iblockdata1 -> oldState
|
|
|
|
if (lock) {
|
|
iblockdata1 = (BlockState) this.states.getAndSet(x, y, z, state);
|
|
@@ -102,6 +118,7 @@ public class LevelChunkSection {
|
|
++this.tickingFluidCount;
|
|
}
|
|
|
|
+ this.updateBlockCallback(x, y, z, iblockdata1, state); // Paper - optimise collisions
|
|
return iblockdata1;
|
|
}
|
|
|
|
@@ -147,6 +164,11 @@ public class LevelChunkSection {
|
|
}
|
|
}
|
|
|
|
+ // Paper start - optimise collisions
|
|
+ if (io.papermc.paper.util.CollisionUtil.isSpecialCollidingBlock(iblockdata)) {
|
|
+ ++this.specialCollidingBlocks;
|
|
+ }
|
|
+ // Paper end - optimise collisions
|
|
});
|
|
}
|
|
// Paper end
|
|
diff --git a/src/main/java/net/minecraft/world/level/material/FlowingFluid.java b/src/main/java/net/minecraft/world/level/material/FlowingFluid.java
|
|
index a98ab20814cc29a25e9d29adfbb7e70d46768df2..6d8ff6c06af5545634f255ed17dc1e489ece2548 100644
|
|
--- a/src/main/java/net/minecraft/world/level/material/FlowingFluid.java
|
|
+++ b/src/main/java/net/minecraft/world/level/material/FlowingFluid.java
|
|
@@ -240,6 +240,17 @@ public abstract class FlowingFluid extends Fluid {
|
|
}
|
|
|
|
private boolean canPassThroughWall(Direction face, BlockGetter world, BlockPos pos, BlockState state, BlockPos fromPos, BlockState fromState) {
|
|
+ // Paper start - optimise collisions
|
|
+ if (state.emptyCollisionShape() & fromState.emptyCollisionShape()) {
|
|
+ // don't even try to cache simple cases
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ if (state.occludesFullBlock() | fromState.occludesFullBlock()) {
|
|
+ // don't even try to cache simple cases
|
|
+ return false;
|
|
+ }
|
|
+ // Paper end - optimise collisions
|
|
Object2ByteLinkedOpenHashMap object2bytelinkedopenhashmap;
|
|
|
|
if (!state.getBlock().hasDynamicShape() && !fromState.getBlock().hasDynamicShape()) {
|
|
diff --git a/src/main/java/net/minecraft/world/phys/AABB.java b/src/main/java/net/minecraft/world/phys/AABB.java
|
|
index 62752e28a68400f0e1a44f0196f0e51e3dd702b8..92394960fc76886f393cba02ac33c57739a4b383 100644
|
|
--- a/src/main/java/net/minecraft/world/phys/AABB.java
|
|
+++ b/src/main/java/net/minecraft/world/phys/AABB.java
|
|
@@ -25,6 +25,17 @@ public class AABB {
|
|
this.maxZ = Math.max(z1, z2);
|
|
}
|
|
|
|
+ // Paper start
|
|
+ public AABB(double minX, double minY, double minZ, double maxX, double maxY, double maxZ, boolean dummy) {
|
|
+ this.minX = minX;
|
|
+ this.minY = minY;
|
|
+ this.minZ = minZ;
|
|
+ this.maxX = maxX;
|
|
+ this.maxY = maxY;
|
|
+ this.maxZ = maxZ;
|
|
+ }
|
|
+ // Paper end
|
|
+
|
|
public AABB(BlockPos pos) {
|
|
this((double)pos.getX(), (double)pos.getY(), (double)pos.getZ(), (double)(pos.getX() + 1), (double)(pos.getY() + 1), (double)(pos.getZ() + 1));
|
|
}
|
|
@@ -321,7 +332,7 @@ public class AABB {
|
|
}
|
|
|
|
@Nullable
|
|
- private static Direction getDirection(
|
|
+ public static Direction getDirection( // Paper - optimise collisions - public
|
|
AABB box, Vec3 intersectingVector, double[] traceDistanceResult, @Nullable Direction approachDirection, double deltaX, double deltaY, double deltaZ
|
|
) {
|
|
if (deltaX > 1.0E-7) {
|
|
diff --git a/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java
|
|
index fc7f986812bdf74e0aea3bd09a1d53ba6def697f..0583d40a235aaecd9d6081486bbfb7355709a5ac 100644
|
|
--- a/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java
|
|
+++ b/src/main/java/net/minecraft/world/phys/shapes/ArrayVoxelShape.java
|
|
@@ -20,7 +20,7 @@ public class ArrayVoxelShape extends VoxelShape {
|
|
);
|
|
}
|
|
|
|
- ArrayVoxelShape(DiscreteVoxelShape shape, DoubleList xPoints, DoubleList yPoints, DoubleList zPoints) {
|
|
+ public ArrayVoxelShape(DiscreteVoxelShape shape, DoubleList xPoints, DoubleList yPoints, DoubleList zPoints) { // Paper - optimise collisions - public
|
|
super(shape);
|
|
int i = shape.getXSize() + 1;
|
|
int j = shape.getYSize() + 1;
|
|
@@ -34,6 +34,7 @@ public class ArrayVoxelShape extends VoxelShape {
|
|
new IllegalArgumentException("Lengths of point arrays must be consistent with the size of the VoxelShape.")
|
|
);
|
|
}
|
|
+ this.initCache(); // Paper - optimise collisions
|
|
}
|
|
|
|
@Override
|
|
@@ -49,4 +50,5 @@ public class ArrayVoxelShape extends VoxelShape {
|
|
throw new IllegalArgumentException();
|
|
}
|
|
}
|
|
+
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java
|
|
index 31b570517c1047e8e1cd5280baf80977af2b6121..d8b80632f6186641ee2ddaef9eba7ba998b09136 100644
|
|
--- a/src/main/java/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java
|
|
+++ b/src/main/java/net/minecraft/world/phys/shapes/BitSetDiscreteVoxelShape.java
|
|
@@ -4,13 +4,13 @@ import java.util.BitSet;
|
|
import net.minecraft.core.Direction;
|
|
|
|
public final class BitSetDiscreteVoxelShape extends DiscreteVoxelShape {
|
|
- private final BitSet storage;
|
|
- private int xMin;
|
|
- private int yMin;
|
|
- private int zMin;
|
|
- private int xMax;
|
|
- private int yMax;
|
|
- private int zMax;
|
|
+ public final BitSet storage; // Paper - optimise collisions - public
|
|
+ public int xMin; // Paper - optimise collisions - public
|
|
+ public int yMin; // Paper - optimise collisions - public
|
|
+ public int zMin; // Paper - optimise collisions - public
|
|
+ public int xMax; // Paper - optimise collisions - public
|
|
+ public int yMax; // Paper - optimise collisions - public
|
|
+ public int zMax; // Paper - optimise collisions - public
|
|
|
|
public BitSetDiscreteVoxelShape(int sizeX, int sizeY, int sizeZ) {
|
|
super(sizeX, sizeY, sizeZ);
|
|
@@ -151,45 +151,106 @@ public final class BitSetDiscreteVoxelShape extends DiscreteVoxelShape {
|
|
}
|
|
|
|
protected static void forAllBoxes(DiscreteVoxelShape voxelSet, DiscreteVoxelShape.IntLineConsumer callback, boolean coalesce) {
|
|
- BitSetDiscreteVoxelShape bitSetDiscreteVoxelShape = new BitSetDiscreteVoxelShape(voxelSet);
|
|
+ // Paper start - optimise collisions
|
|
+ // called with the shape of a VoxelShape, so we can expect the cache to exist
|
|
+ final io.papermc.paper.util.collisions.CachedShapeData cache = voxelSet.getOrCreateCachedShapeData();
|
|
|
|
- for (int i = 0; i < bitSetDiscreteVoxelShape.ySize; i++) {
|
|
- for (int j = 0; j < bitSetDiscreteVoxelShape.xSize; j++) {
|
|
- int k = -1;
|
|
+ final int sizeX = cache.sizeX();
|
|
+ final int sizeY = cache.sizeY();
|
|
+ final int sizeZ = cache.sizeZ();
|
|
|
|
- for (int l = 0; l <= bitSetDiscreteVoxelShape.zSize; l++) {
|
|
- if (bitSetDiscreteVoxelShape.isFullWide(j, i, l)) {
|
|
- if (coalesce) {
|
|
- if (k == -1) {
|
|
- k = l;
|
|
- }
|
|
- } else {
|
|
- callback.consume(j, i, l, j + 1, i + 1, l + 1);
|
|
+ int indexX;
|
|
+ int indexY = 0;
|
|
+ int indexZ;
|
|
+
|
|
+ int incY = sizeZ;
|
|
+ int incX = sizeZ*sizeY;
|
|
+
|
|
+ long[] bitset = cache.voxelSet();
|
|
+
|
|
+ // index = z + y*size_z + x*(size_z*size_y)
|
|
+
|
|
+ if (!coalesce) {
|
|
+ // due to the odd selection of loop order (which does affect behavior, unfortunately) we can't simply
|
|
+ // increment an index in the Z loop, and have to perform this trash (keeping track of 3 counters) to avoid
|
|
+ // the multiplication
|
|
+ for (int y = 0; y < sizeY; ++y, indexY += incY) {
|
|
+ indexX = indexY;
|
|
+ for (int x = 0; x < sizeX; ++x, indexX += incX) {
|
|
+ indexZ = indexX;
|
|
+ for (int z = 0; z < sizeZ; ++z, ++indexZ) {
|
|
+ if ((bitset[indexZ >>> 6] & (1L << indexZ)) != 0L) {
|
|
+ callback.consume(x, y, z, x + 1, y + 1, z + 1);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ } else {
|
|
+ // same notes about loop order as the above
|
|
+ // this branch is actually important to optimise, as it affects uncached toAabbs() (which affects optimize())
|
|
+
|
|
+ // only clone when we may write to it
|
|
+ bitset = bitset.clone();
|
|
+
|
|
+ for (int y = 0; y < sizeY; ++y, indexY += incY) {
|
|
+ indexX = indexY;
|
|
+ for (int x = 0; x < sizeX; ++x, indexX += incX) {
|
|
+ for (int zIdx = indexX, endIndex = indexX + sizeZ; zIdx < endIndex;) {
|
|
+ final int firstSetZ = io.papermc.paper.util.collisions.FlatBitsetUtil.firstSet(bitset, zIdx, endIndex);
|
|
+
|
|
+ if (firstSetZ == -1) {
|
|
+ break;
|
|
}
|
|
- } else if (k != -1) {
|
|
- int m = j;
|
|
- int n = i;
|
|
- bitSetDiscreteVoxelShape.clearZStrip(k, l, j, i);
|
|
-
|
|
- while (bitSetDiscreteVoxelShape.isZStripFull(k, l, m + 1, i)) {
|
|
- bitSetDiscreteVoxelShape.clearZStrip(k, l, m + 1, i);
|
|
- m++;
|
|
+
|
|
+ int lastSetZ = io.papermc.paper.util.collisions.FlatBitsetUtil.firstClear(bitset, firstSetZ, endIndex);
|
|
+ if (lastSetZ == -1) {
|
|
+ lastSetZ = endIndex;
|
|
}
|
|
|
|
- while (bitSetDiscreteVoxelShape.isXZRectangleFull(j, m + 1, k, l, n + 1)) {
|
|
- for (int o = j; o <= m; o++) {
|
|
- bitSetDiscreteVoxelShape.clearZStrip(k, l, o, n + 1);
|
|
+ io.papermc.paper.util.collisions.FlatBitsetUtil.clearRange(bitset, firstSetZ, lastSetZ);
|
|
+
|
|
+ // try to merge neighbouring on the X axis
|
|
+ int endX = x + 1; // exclusive
|
|
+ for (int neighbourIdxStart = firstSetZ + incX, neighbourIdxEnd = lastSetZ + incX;
|
|
+ endX < sizeX && io.papermc.paper.util.collisions.FlatBitsetUtil.isRangeSet(bitset, neighbourIdxStart, neighbourIdxEnd);
|
|
+ neighbourIdxStart += incX, neighbourIdxEnd += incX) {
|
|
+
|
|
+ ++endX;
|
|
+ io.papermc.paper.util.collisions.FlatBitsetUtil.clearRange(bitset, neighbourIdxStart, neighbourIdxEnd);
|
|
+ }
|
|
+
|
|
+ // try to merge neighbouring on the Y axis
|
|
+
|
|
+ int endY; // exclusive
|
|
+ int firstSetZY, lastSetZY;
|
|
+ y_merge:
|
|
+ for (endY = y + 1, firstSetZY = firstSetZ + incY, lastSetZY = lastSetZ + incY; endY < sizeY;
|
|
+ firstSetZY += incY, lastSetZY += incY) {
|
|
+
|
|
+ // test the whole XZ range
|
|
+ for (int testX = x, start = firstSetZY, end = lastSetZY; testX < endX;
|
|
+ ++testX, start += incX, end += incX) {
|
|
+ if (!io.papermc.paper.util.collisions.FlatBitsetUtil.isRangeSet(bitset, start, end)) {
|
|
+ break y_merge;
|
|
+ }
|
|
}
|
|
|
|
- n++;
|
|
+ ++endY;
|
|
+
|
|
+ // passed, so we can clear it
|
|
+ for (int testX = x, start = firstSetZY, end = lastSetZY; testX < endX;
|
|
+ ++testX, start += incX, end += incX) {
|
|
+ io.papermc.paper.util.collisions.FlatBitsetUtil.clearRange(bitset, start, end);
|
|
+ }
|
|
}
|
|
|
|
- callback.consume(j, i, k, m + 1, n + 1, l);
|
|
- k = -1;
|
|
+ callback.consume(x, y, firstSetZ - indexX, endX, endY, lastSetZ - indexX);
|
|
+ zIdx = lastSetZ;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
+ // Paper end - optimise collisions
|
|
}
|
|
|
|
private boolean isZStripFull(int z1, int z2, int x, int y) {
|
|
diff --git a/src/main/java/net/minecraft/world/phys/shapes/CubeVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/CubeVoxelShape.java
|
|
index 32632368f06b79f53342fde060bbcd1b7c64767a..b9af1d14c7815c99273bce8165cf384d669c1a75 100644
|
|
--- a/src/main/java/net/minecraft/world/phys/shapes/CubeVoxelShape.java
|
|
+++ b/src/main/java/net/minecraft/world/phys/shapes/CubeVoxelShape.java
|
|
@@ -7,6 +7,7 @@ import net.minecraft.util.Mth;
|
|
public final class CubeVoxelShape extends VoxelShape {
|
|
protected CubeVoxelShape(DiscreteVoxelShape voxels) {
|
|
super(voxels);
|
|
+ this.initCache(); // Paper - optimise collisions
|
|
}
|
|
|
|
@Override
|
|
diff --git a/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java
|
|
index 87a8f12dc3d47fb093115030e0222f065f1dcb1c..44b62f1f6685084c0cff02bd31eb5a7c2ef9eead 100644
|
|
--- a/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java
|
|
+++ b/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java
|
|
@@ -9,6 +9,71 @@ public abstract class DiscreteVoxelShape {
|
|
protected final int ySize;
|
|
protected final int zSize;
|
|
|
|
+ // Paper start - optimise collisions
|
|
+ private io.papermc.paper.util.collisions.CachedShapeData cachedShapeData;
|
|
+
|
|
+ public final io.papermc.paper.util.collisions.CachedShapeData getOrCreateCachedShapeData() {
|
|
+ if (this.cachedShapeData != null) {
|
|
+ return this.cachedShapeData;
|
|
+ }
|
|
+
|
|
+ final DiscreteVoxelShape discreteVoxelShape = (DiscreteVoxelShape)(Object)this;
|
|
+
|
|
+ final int sizeX = discreteVoxelShape.getXSize();
|
|
+ final int sizeY = discreteVoxelShape.getYSize();
|
|
+ final int sizeZ = discreteVoxelShape.getZSize();
|
|
+
|
|
+ final int maxIndex = sizeX * sizeY * sizeZ; // exclusive
|
|
+
|
|
+ final int longsRequired = (maxIndex + (Long.SIZE - 1)) >>> 6;
|
|
+ long[] voxelSet;
|
|
+
|
|
+ final boolean isEmpty = discreteVoxelShape.isEmpty();
|
|
+
|
|
+ if (discreteVoxelShape instanceof BitSetDiscreteVoxelShape bitsetShape) {
|
|
+ voxelSet = bitsetShape.storage.toLongArray();
|
|
+ if (voxelSet.length < longsRequired) {
|
|
+ // happens when the later long values are 0L, so we need to resize
|
|
+ voxelSet = java.util.Arrays.copyOf(voxelSet, longsRequired);
|
|
+ }
|
|
+ } else {
|
|
+ voxelSet = new long[longsRequired];
|
|
+ if (!isEmpty) {
|
|
+ final int mulX = sizeZ * sizeY;
|
|
+ for (int x = 0; x < sizeX; ++x) {
|
|
+ for (int y = 0; y < sizeY; ++y) {
|
|
+ for (int z = 0; z < sizeZ; ++z) {
|
|
+ if (discreteVoxelShape.isFull(x, y, z)) {
|
|
+ // index = z + y*size_z + x*(size_z*size_y)
|
|
+ final int index = z + y * sizeZ + x * mulX;
|
|
+
|
|
+ voxelSet[index >>> 6] |= 1L << index;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ final boolean hasSingleAABB = sizeX == 1 && sizeY == 1 && sizeZ == 1 && !isEmpty && discreteVoxelShape.isFull(0, 0, 0);
|
|
+
|
|
+ final int minFullX = discreteVoxelShape.firstFull(Direction.Axis.X);
|
|
+ final int minFullY = discreteVoxelShape.firstFull(Direction.Axis.Y);
|
|
+ final int minFullZ = discreteVoxelShape.firstFull(Direction.Axis.Z);
|
|
+
|
|
+ final int maxFullX = discreteVoxelShape.lastFull(Direction.Axis.X);
|
|
+ final int maxFullY = discreteVoxelShape.lastFull(Direction.Axis.Y);
|
|
+ final int maxFullZ = discreteVoxelShape.lastFull(Direction.Axis.Z);
|
|
+
|
|
+ return this.cachedShapeData = new io.papermc.paper.util.collisions.CachedShapeData(
|
|
+ sizeX, sizeY, sizeZ, voxelSet,
|
|
+ minFullX, minFullY, minFullZ,
|
|
+ maxFullX, maxFullY, maxFullZ,
|
|
+ isEmpty, hasSingleAABB
|
|
+ );
|
|
+ }
|
|
+ // Paper end - optimise collisions
|
|
+
|
|
protected DiscreteVoxelShape(int sizeX, int sizeY, int sizeZ) {
|
|
if (sizeX >= 0 && sizeY >= 0 && sizeZ >= 0) {
|
|
this.xSize = sizeX;
|
|
diff --git a/src/main/java/net/minecraft/world/phys/shapes/OffsetDoubleList.java b/src/main/java/net/minecraft/world/phys/shapes/OffsetDoubleList.java
|
|
index 7ec02a7849437a18860aa0df7d9ddd71b2447d4c..5e45e49ab09344cb95736f4124b1c6e002ef5b82 100644
|
|
--- a/src/main/java/net/minecraft/world/phys/shapes/OffsetDoubleList.java
|
|
+++ b/src/main/java/net/minecraft/world/phys/shapes/OffsetDoubleList.java
|
|
@@ -4,8 +4,8 @@ import it.unimi.dsi.fastutil.doubles.AbstractDoubleList;
|
|
import it.unimi.dsi.fastutil.doubles.DoubleList;
|
|
|
|
public class OffsetDoubleList extends AbstractDoubleList {
|
|
- private final DoubleList delegate;
|
|
- private final double offset;
|
|
+ public final DoubleList delegate; // Paper - optimise collisions - public
|
|
+ public final double offset; // Paper - optimise collisions - public
|
|
|
|
public OffsetDoubleList(DoubleList oldList, double offset) {
|
|
this.delegate = oldList;
|
|
diff --git a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java
|
|
index 86df4ef44d0a5107ee929dfd40d8ccb0779e8bfc..fbf1a559aefe444410b63a773374e011e4964e16 100644
|
|
--- a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java
|
|
+++ b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java
|
|
@@ -16,9 +16,15 @@ public final class Shapes {
|
|
public static final double EPSILON = 1.0E-7;
|
|
public static final double BIG_EPSILON = 1.0E-6;
|
|
private static final VoxelShape BLOCK = Util.make(() -> {
|
|
- DiscreteVoxelShape discreteVoxelShape = new BitSetDiscreteVoxelShape(1, 1, 1);
|
|
- discreteVoxelShape.fill(0, 0, 0);
|
|
- return new CubeVoxelShape(discreteVoxelShape);
|
|
+ // Paper start - optimise collisions - force arrayvoxelshape
|
|
+ final DiscreteVoxelShape shape = new BitSetDiscreteVoxelShape(1, 1, 1);
|
|
+ shape.fill(0, 0, 0);
|
|
+
|
|
+ return new ArrayVoxelShape(
|
|
+ shape,
|
|
+ io.papermc.paper.util.CollisionUtil.ZERO_ONE, io.papermc.paper.util.CollisionUtil.ZERO_ONE, io.papermc.paper.util.CollisionUtil.ZERO_ONE
|
|
+ );
|
|
+ // Paper end - optimise collisions - force arrayvoxelshape
|
|
});
|
|
public static final VoxelShape INFINITY = box(
|
|
Double.NEGATIVE_INFINITY,
|
|
@@ -35,6 +41,30 @@ public final class Shapes {
|
|
new DoubleArrayList(new double[]{0.0})
|
|
);
|
|
|
|
+ // Paper start - optimise collisions - force arrayvoxelshape
|
|
+ private static final DoubleArrayList[] PARTS_BY_BITS = new DoubleArrayList[] {
|
|
+ DoubleArrayList.wrap(generateCubeParts(1 << 0)),
|
|
+ DoubleArrayList.wrap(generateCubeParts(1 << 1)),
|
|
+ DoubleArrayList.wrap(generateCubeParts(1 << 2)),
|
|
+ DoubleArrayList.wrap(generateCubeParts(1 << 3))
|
|
+ };
|
|
+
|
|
+ private static double[] generateCubeParts(final int parts) {
|
|
+ // note: parts is a power of two, so we do not need to worry about loss of precision here
|
|
+ // note: parts is from [2^0, 2^3]
|
|
+ final double inc = 1.0 / (double)parts;
|
|
+
|
|
+ final double[] ret = new double[parts + 1];
|
|
+ double val = 0.0;
|
|
+ for (int i = 0; i <= parts; ++i) {
|
|
+ ret[i] = val;
|
|
+ val += inc;
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+ // Paper end - optimise collisions - force arrayvoxelshape
|
|
+
|
|
public static VoxelShape empty() {
|
|
return EMPTY;
|
|
}
|
|
@@ -53,35 +83,39 @@ public final class Shapes {
|
|
|
|
public static VoxelShape create(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) {
|
|
if (!(maxX - minX < 1.0E-7) && !(maxY - minY < 1.0E-7) && !(maxZ - minZ < 1.0E-7)) {
|
|
- int i = findBits(minX, maxX);
|
|
- int j = findBits(minY, maxY);
|
|
- int k = findBits(minZ, maxZ);
|
|
- if (i < 0 || j < 0 || k < 0) {
|
|
- return new ArrayVoxelShape(
|
|
- BLOCK.shape,
|
|
- DoubleArrayList.wrap(new double[]{minX, maxX}),
|
|
- DoubleArrayList.wrap(new double[]{minY, maxY}),
|
|
- DoubleArrayList.wrap(new double[]{minZ, maxZ})
|
|
- );
|
|
- } else if (i == 0 && j == 0 && k == 0) {
|
|
- return block();
|
|
+ // Paper start - optimise collisions
|
|
+ // force ArrayVoxelShape in every case
|
|
+ final int bitsX = findBits(minX, maxX);
|
|
+ final int bitsY = findBits(minY, maxY);
|
|
+ final int bitsZ = findBits(minZ, maxZ);
|
|
+ if (bitsX >= 0 && bitsY >= 0 && bitsZ >= 0) {
|
|
+ if (bitsX == 0 && bitsY == 0 && bitsZ == 0) {
|
|
+ return BLOCK;
|
|
+ } else {
|
|
+ final int sizeX = 1 << bitsX;
|
|
+ final int sizeY = 1 << bitsY;
|
|
+ final int sizeZ = 1 << bitsZ;
|
|
+ final BitSetDiscreteVoxelShape shape = BitSetDiscreteVoxelShape.withFilledBounds(
|
|
+ sizeX, sizeY, sizeZ,
|
|
+ (int)Math.round(minX * (double)sizeX), (int)Math.round(minY * (double)sizeY), (int)Math.round(minZ * (double)sizeZ),
|
|
+ (int)Math.round(maxX * (double)sizeX), (int)Math.round(maxY * (double)sizeY), (int)Math.round(maxZ * (double)sizeZ)
|
|
+ );
|
|
+ return new ArrayVoxelShape(
|
|
+ shape,
|
|
+ PARTS_BY_BITS[bitsX],
|
|
+ PARTS_BY_BITS[bitsY],
|
|
+ PARTS_BY_BITS[bitsZ]
|
|
+ );
|
|
+ }
|
|
} else {
|
|
- int l = 1 << i;
|
|
- int m = 1 << j;
|
|
- int n = 1 << k;
|
|
- BitSetDiscreteVoxelShape bitSetDiscreteVoxelShape = BitSetDiscreteVoxelShape.withFilledBounds(
|
|
- l,
|
|
- m,
|
|
- n,
|
|
- (int)Math.round(minX * (double)l),
|
|
- (int)Math.round(minY * (double)m),
|
|
- (int)Math.round(minZ * (double)n),
|
|
- (int)Math.round(maxX * (double)l),
|
|
- (int)Math.round(maxY * (double)m),
|
|
- (int)Math.round(maxZ * (double)n)
|
|
+ return new ArrayVoxelShape(
|
|
+ BLOCK.shape,
|
|
+ minX == 0.0 && maxX == 1.0 ? io.papermc.paper.util.CollisionUtil.ZERO_ONE : DoubleArrayList.wrap(new double[] { minX, maxX }),
|
|
+ minY == 0.0 && maxY == 1.0 ? io.papermc.paper.util.CollisionUtil.ZERO_ONE : DoubleArrayList.wrap(new double[] { minY, maxY }),
|
|
+ minZ == 0.0 && maxZ == 1.0 ? io.papermc.paper.util.CollisionUtil.ZERO_ONE : DoubleArrayList.wrap(new double[] { minZ, maxZ })
|
|
);
|
|
- return new CubeVoxelShape(bitSetDiscreteVoxelShape);
|
|
}
|
|
+ // Paper end - optimise collisions
|
|
} else {
|
|
return empty();
|
|
}
|
|
@@ -120,79 +154,53 @@ public final class Shapes {
|
|
}
|
|
|
|
public static VoxelShape or(VoxelShape first, VoxelShape... others) {
|
|
- return Arrays.stream(others).reduce(first, Shapes::or);
|
|
+ // Paper start - optimise collisions
|
|
+ int size = others.length;
|
|
+ if (size == 0) {
|
|
+ return first;
|
|
+ }
|
|
+
|
|
+ // reduce complexity of joins by splitting the merges
|
|
+
|
|
+ // add extra slot for first shape
|
|
+ ++size;
|
|
+ final VoxelShape[] tmp = Arrays.copyOf(others, size);
|
|
+ // insert first shape
|
|
+ tmp[size - 1] = first;
|
|
+
|
|
+ while (size > 1) {
|
|
+ int newSize = 0;
|
|
+ for (int i = 0; i < size; i += 2) {
|
|
+ final int next = i + 1;
|
|
+ if (next >= size) {
|
|
+ // nothing to merge with, so leave it for next iteration
|
|
+ tmp[newSize++] = tmp[i];
|
|
+ break;
|
|
+ } else {
|
|
+ // merge with adjacent
|
|
+ final VoxelShape one = tmp[i];
|
|
+ final VoxelShape second = tmp[next];
|
|
+
|
|
+ tmp[newSize++] = Shapes.or(one, second);
|
|
+ }
|
|
+ }
|
|
+ size = newSize;
|
|
+ }
|
|
+
|
|
+ return tmp[0];
|
|
+ // Paper end - optimise collisions
|
|
}
|
|
|
|
public static VoxelShape join(VoxelShape first, VoxelShape second, BooleanOp function) {
|
|
- return joinUnoptimized(first, second, function).optimize();
|
|
+ return io.papermc.paper.util.CollisionUtil.joinOptimized(first, second, function); // Paper - optimise collisions
|
|
}
|
|
|
|
public static VoxelShape joinUnoptimized(VoxelShape one, VoxelShape two, BooleanOp function) {
|
|
- if (function.apply(false, false)) {
|
|
- throw (IllegalArgumentException)Util.pauseInIde(new IllegalArgumentException());
|
|
- } else if (one == two) {
|
|
- return function.apply(true, true) ? one : empty();
|
|
- } else {
|
|
- boolean bl = function.apply(true, false);
|
|
- boolean bl2 = function.apply(false, true);
|
|
- if (one.isEmpty()) {
|
|
- return bl2 ? two : empty();
|
|
- } else if (two.isEmpty()) {
|
|
- return bl ? one : empty();
|
|
- } else {
|
|
- IndexMerger indexMerger = createIndexMerger(1, one.getCoords(Direction.Axis.X), two.getCoords(Direction.Axis.X), bl, bl2);
|
|
- IndexMerger indexMerger2 = createIndexMerger(indexMerger.size() - 1, one.getCoords(Direction.Axis.Y), two.getCoords(Direction.Axis.Y), bl, bl2);
|
|
- IndexMerger indexMerger3 = createIndexMerger(
|
|
- (indexMerger.size() - 1) * (indexMerger2.size() - 1), one.getCoords(Direction.Axis.Z), two.getCoords(Direction.Axis.Z), bl, bl2
|
|
- );
|
|
- BitSetDiscreteVoxelShape bitSetDiscreteVoxelShape = BitSetDiscreteVoxelShape.join(
|
|
- one.shape, two.shape, indexMerger, indexMerger2, indexMerger3, function
|
|
- );
|
|
- return (VoxelShape)(indexMerger instanceof DiscreteCubeMerger
|
|
- && indexMerger2 instanceof DiscreteCubeMerger
|
|
- && indexMerger3 instanceof DiscreteCubeMerger
|
|
- ? new CubeVoxelShape(bitSetDiscreteVoxelShape)
|
|
- : new ArrayVoxelShape(bitSetDiscreteVoxelShape, indexMerger.getList(), indexMerger2.getList(), indexMerger3.getList()));
|
|
- }
|
|
- }
|
|
+ return io.papermc.paper.util.CollisionUtil.joinUnoptimized(one, two, function); // Paper - optimise collisions
|
|
}
|
|
|
|
public static boolean joinIsNotEmpty(VoxelShape shape1, VoxelShape shape2, BooleanOp predicate) {
|
|
- if (predicate.apply(false, false)) {
|
|
- throw (IllegalArgumentException)Util.pauseInIde(new IllegalArgumentException());
|
|
- } else {
|
|
- boolean bl = shape1.isEmpty();
|
|
- boolean bl2 = shape2.isEmpty();
|
|
- if (!bl && !bl2) {
|
|
- if (shape1 == shape2) {
|
|
- return predicate.apply(true, true);
|
|
- } else {
|
|
- boolean bl3 = predicate.apply(true, false);
|
|
- boolean bl4 = predicate.apply(false, true);
|
|
-
|
|
- for (Direction.Axis axis : AxisCycle.AXIS_VALUES) {
|
|
- if (shape1.max(axis) < shape2.min(axis) - 1.0E-7) {
|
|
- return bl3 || bl4;
|
|
- }
|
|
-
|
|
- if (shape2.max(axis) < shape1.min(axis) - 1.0E-7) {
|
|
- return bl3 || bl4;
|
|
- }
|
|
- }
|
|
-
|
|
- IndexMerger indexMerger = createIndexMerger(1, shape1.getCoords(Direction.Axis.X), shape2.getCoords(Direction.Axis.X), bl3, bl4);
|
|
- IndexMerger indexMerger2 = createIndexMerger(
|
|
- indexMerger.size() - 1, shape1.getCoords(Direction.Axis.Y), shape2.getCoords(Direction.Axis.Y), bl3, bl4
|
|
- );
|
|
- IndexMerger indexMerger3 = createIndexMerger(
|
|
- (indexMerger.size() - 1) * (indexMerger2.size() - 1), shape1.getCoords(Direction.Axis.Z), shape2.getCoords(Direction.Axis.Z), bl3, bl4
|
|
- );
|
|
- return joinIsNotEmpty(indexMerger, indexMerger2, indexMerger3, shape1.shape, shape2.shape, predicate);
|
|
- }
|
|
- } else {
|
|
- return predicate.apply(!bl, !bl2);
|
|
- }
|
|
- }
|
|
+ return io.papermc.paper.util.CollisionUtil.isJoinNonEmpty(shape1, shape2, predicate); // Paper - optimise collisions
|
|
}
|
|
|
|
private static boolean joinIsNotEmpty(
|
|
@@ -220,69 +228,119 @@ public final class Shapes {
|
|
}
|
|
|
|
public static boolean blockOccudes(VoxelShape shape, VoxelShape neighbor, Direction direction) {
|
|
- if (shape == block() && neighbor == block()) {
|
|
+ // Paper start - optimise collisions
|
|
+ final boolean firstBlock = shape == BLOCK;
|
|
+ final boolean secondBlock = neighbor == BLOCK;
|
|
+
|
|
+ if (firstBlock & secondBlock) {
|
|
return true;
|
|
- } else if (neighbor.isEmpty()) {
|
|
+ }
|
|
+
|
|
+ if (shape.isEmpty() | neighbor.isEmpty()) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ // we optimise getOpposite, so we can use it
|
|
+ // secondly, use our cache to retrieve sliced shape
|
|
+ final VoxelShape newFirst = shape.getFaceShapeClamped(direction);
|
|
+ if (newFirst.isEmpty()) {
|
|
return false;
|
|
- } else {
|
|
- Direction.Axis axis = direction.getAxis();
|
|
- Direction.AxisDirection axisDirection = direction.getAxisDirection();
|
|
- VoxelShape voxelShape = axisDirection == Direction.AxisDirection.POSITIVE ? shape : neighbor;
|
|
- VoxelShape voxelShape2 = axisDirection == Direction.AxisDirection.POSITIVE ? neighbor : shape;
|
|
- BooleanOp booleanOp = axisDirection == Direction.AxisDirection.POSITIVE ? BooleanOp.ONLY_FIRST : BooleanOp.ONLY_SECOND;
|
|
- return DoubleMath.fuzzyEquals(voxelShape.max(axis), 1.0, 1.0E-7)
|
|
- && DoubleMath.fuzzyEquals(voxelShape2.min(axis), 0.0, 1.0E-7)
|
|
- && !joinIsNotEmpty(new SliceShape(voxelShape, axis, voxelShape.shape.getSize(axis) - 1), new SliceShape(voxelShape2, axis, 0), booleanOp);
|
|
}
|
|
+ final VoxelShape newSecond = neighbor.getFaceShapeClamped(direction.getOpposite());
|
|
+ if (newSecond.isEmpty()) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ return !joinIsNotEmpty(newFirst, newSecond, BooleanOp.ONLY_FIRST);
|
|
+ // Paper end - optimise collisions
|
|
}
|
|
|
|
public static VoxelShape getFaceShape(VoxelShape shape, Direction direction) {
|
|
- if (shape == block()) {
|
|
- return block();
|
|
- } else {
|
|
- Direction.Axis axis = direction.getAxis();
|
|
- boolean bl;
|
|
- int i;
|
|
- if (direction.getAxisDirection() == Direction.AxisDirection.POSITIVE) {
|
|
- bl = DoubleMath.fuzzyEquals(shape.max(axis), 1.0, 1.0E-7);
|
|
- i = shape.shape.getSize(axis) - 1;
|
|
- } else {
|
|
- bl = DoubleMath.fuzzyEquals(shape.min(axis), 0.0, 1.0E-7);
|
|
- i = 0;
|
|
- }
|
|
+ return shape.getFaceShapeClamped(direction); // Paper - optimise collisions
|
|
+ }
|
|
|
|
- return (VoxelShape)(!bl ? empty() : new SliceShape(shape, axis, i));
|
|
- }
|
|
+ // Paper start - optimise collisions
|
|
+ private static boolean mergedMayOccludeBlock(final VoxelShape shape1, final VoxelShape shape2) {
|
|
+ // if the combined bounds of the two shapes cannot occlude, then neither can the merged
|
|
+ final AABB bounds1 = shape1.bounds();
|
|
+ final AABB bounds2 = shape2.bounds();
|
|
+
|
|
+ final double minX = Math.min(bounds1.minX, bounds2.minX);
|
|
+ final double minY = Math.min(bounds1.minY, bounds2.minY);
|
|
+ final double minZ = Math.min(bounds1.minZ, bounds2.minZ);
|
|
+
|
|
+ final double maxX = Math.max(bounds1.maxX, bounds2.maxX);
|
|
+ final double maxY = Math.max(bounds1.maxY, bounds2.maxY);
|
|
+ final double maxZ = Math.max(bounds1.maxZ, bounds2.maxZ);
|
|
+
|
|
+ return (minX <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && maxX >= (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)) &&
|
|
+ (minY <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && maxY >= (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)) &&
|
|
+ (minZ <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && maxZ >= (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON));
|
|
}
|
|
+ // Paper end - optimise collisions
|
|
|
|
public static boolean mergedFaceOccludes(VoxelShape one, VoxelShape two, Direction direction) {
|
|
- if (one != block() && two != block()) {
|
|
- Direction.Axis axis = direction.getAxis();
|
|
- Direction.AxisDirection axisDirection = direction.getAxisDirection();
|
|
- VoxelShape voxelShape = axisDirection == Direction.AxisDirection.POSITIVE ? one : two;
|
|
- VoxelShape voxelShape2 = axisDirection == Direction.AxisDirection.POSITIVE ? two : one;
|
|
- if (!DoubleMath.fuzzyEquals(voxelShape.max(axis), 1.0, 1.0E-7)) {
|
|
- voxelShape = empty();
|
|
- }
|
|
+ // Paper start - optimise collisions
|
|
+ // see if any of the shapes on their own occludes, only if cached
|
|
+ if (one.occludesFullBlockIfCached() || two.occludesFullBlockIfCached()) {
|
|
+ return true;
|
|
+ }
|
|
|
|
- if (!DoubleMath.fuzzyEquals(voxelShape2.min(axis), 0.0, 1.0E-7)) {
|
|
- voxelShape2 = empty();
|
|
- }
|
|
+ if (one.isEmpty() & two.isEmpty()) {
|
|
+ return false;
|
|
+ }
|
|
|
|
- return !joinIsNotEmpty(
|
|
- block(),
|
|
- joinUnoptimized(new SliceShape(voxelShape, axis, voxelShape.shape.getSize(axis) - 1), new SliceShape(voxelShape2, axis, 0), BooleanOp.OR),
|
|
- BooleanOp.ONLY_FIRST
|
|
- );
|
|
- } else {
|
|
+ // we optimise getOpposite, so we can use it
|
|
+ // secondly, use our cache to retrieve sliced shape
|
|
+ final VoxelShape newFirst = one.getFaceShapeClamped(direction);
|
|
+ final VoxelShape newSecond = two.getFaceShapeClamped(direction.getOpposite());
|
|
+
|
|
+ // see if any of the shapes on their own occludes, only if cached
|
|
+ if (newFirst.occludesFullBlockIfCached() || newSecond.occludesFullBlockIfCached()) {
|
|
return true;
|
|
}
|
|
+
|
|
+ final boolean firstEmpty = newFirst.isEmpty();
|
|
+ final boolean secondEmpty = newSecond.isEmpty();
|
|
+
|
|
+ if (firstEmpty & secondEmpty) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ if (firstEmpty | secondEmpty) {
|
|
+ return secondEmpty ? newFirst.occludesFullBlock() : newSecond.occludesFullBlock();
|
|
+ }
|
|
+
|
|
+ if (newFirst == newSecond) {
|
|
+ return newFirst.occludesFullBlock();
|
|
+ }
|
|
+
|
|
+ return mergedMayOccludeBlock(newFirst, newSecond) && newFirst.orUnoptimized(newSecond).occludesFullBlock();
|
|
+ // Paper end - optimise collisions
|
|
}
|
|
|
|
public static boolean faceShapeOccludes(VoxelShape one, VoxelShape two) {
|
|
- return one == block()
|
|
- || two == block()
|
|
- || (!one.isEmpty() || !two.isEmpty()) && !joinIsNotEmpty(block(), joinUnoptimized(one, two, BooleanOp.OR), BooleanOp.ONLY_FIRST);
|
|
+ // Paper start - optimise collisions
|
|
+ if (one.occludesFullBlockIfCached() || two.occludesFullBlockIfCached()) {
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ final boolean s1Empty = one.isEmpty();
|
|
+ final boolean s2Empty = two.isEmpty();
|
|
+ if (s1Empty & s2Empty) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ if (s1Empty | s2Empty) {
|
|
+ return s2Empty ? one.occludesFullBlock() : two.occludesFullBlock();
|
|
+ }
|
|
+
|
|
+ if (one == two) {
|
|
+ return one.occludesFullBlock();
|
|
+ }
|
|
+
|
|
+ return mergedMayOccludeBlock(one, two) && (one.orUnoptimized(two)).occludesFullBlock();
|
|
+ // Paper end - optimise collisions
|
|
}
|
|
|
|
@VisibleForTesting
|
|
diff --git a/src/main/java/net/minecraft/world/phys/shapes/SliceShape.java b/src/main/java/net/minecraft/world/phys/shapes/SliceShape.java
|
|
index 53aa193f33a1a15376a59b8d6dd8cbc6cbec168b..a745ff8d115e1d0da6138e4f06726e0737bb1600 100644
|
|
--- a/src/main/java/net/minecraft/world/phys/shapes/SliceShape.java
|
|
+++ b/src/main/java/net/minecraft/world/phys/shapes/SliceShape.java
|
|
@@ -12,6 +12,7 @@ public class SliceShape extends VoxelShape {
|
|
super(makeSlice(shape.shape, axis, sliceWidth));
|
|
this.delegate = shape;
|
|
this.axis = axis;
|
|
+ this.initCache(); // Paper - optimise collisions
|
|
}
|
|
|
|
private static DiscreteVoxelShape makeSlice(DiscreteVoxelShape voxelSet, Direction.Axis axis, int sliceWidth) {
|
|
diff --git a/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java
|
|
index 2936c56e5690b42518010698e5177755422e4c5d..e6b17f32f2b6930739a98c6139442383c1847add 100644
|
|
--- a/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java
|
|
+++ b/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java
|
|
@@ -16,37 +16,438 @@ import net.minecraft.world.phys.BlockHitResult;
|
|
import net.minecraft.world.phys.Vec3;
|
|
|
|
public abstract class VoxelShape {
|
|
- protected final DiscreteVoxelShape shape;
|
|
+ public final DiscreteVoxelShape shape; // Paper - optimise collisions - public
|
|
@Nullable
|
|
private VoxelShape[] faces;
|
|
|
|
- VoxelShape(DiscreteVoxelShape voxels) {
|
|
+ // Paper start - optimise collisions
|
|
+ private double offsetX;
|
|
+ private double offsetY;
|
|
+ private double offsetZ;
|
|
+ @Nullable private AABB singleAABBRepresentation;
|
|
+ private double[] rootCoordinatesX;
|
|
+ private double[] rootCoordinatesY;
|
|
+ private double[] rootCoordinatesZ;
|
|
+
|
|
+ private io.papermc.paper.util.collisions.CachedShapeData cachedShapeData;
|
|
+ private boolean isEmpty;
|
|
+
|
|
+ private io.papermc.paper.util.collisions.CachedToAABBs cachedToAABBs;
|
|
+ private AABB cachedBounds;
|
|
+
|
|
+ private Boolean isFullBlock;
|
|
+
|
|
+ private Boolean occludesFullBlock;
|
|
+
|
|
+ // must be power of two
|
|
+ private static final int MERGED_CACHE_SIZE = 16;
|
|
+
|
|
+ private io.papermc.paper.util.collisions.MergedORCache[] mergedORCache;
|
|
+
|
|
+ public final double offsetX() {
|
|
+ return this.offsetX;
|
|
+ }
|
|
+
|
|
+ public final double offsetY() {
|
|
+ return this.offsetY;
|
|
+ }
|
|
+
|
|
+ public final double offsetZ() {
|
|
+ return this.offsetZ;
|
|
+ }
|
|
+
|
|
+ public final AABB getSingleAABBRepresentation() {
|
|
+ return this.singleAABBRepresentation;
|
|
+ }
|
|
+
|
|
+ public final double[] rootCoordinatesX() {
|
|
+ return this.rootCoordinatesX;
|
|
+ }
|
|
+
|
|
+ public final double[] rootCoordinatesY() {
|
|
+ return this.rootCoordinatesY;
|
|
+ }
|
|
+
|
|
+ public final double[] rootCoordinatesZ() {
|
|
+ return this.rootCoordinatesZ;
|
|
+ }
|
|
+
|
|
+ private static double[] extractRawArray(final DoubleList list) {
|
|
+ if (list instanceof it.unimi.dsi.fastutil.doubles.DoubleArrayList rawList) {
|
|
+ final double[] raw = rawList.elements();
|
|
+ final int expected = rawList.size();
|
|
+ if (raw.length == expected) {
|
|
+ return raw;
|
|
+ } else {
|
|
+ return java.util.Arrays.copyOf(raw, expected);
|
|
+ }
|
|
+ } else {
|
|
+ return list.toDoubleArray();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public final void initCache() {
|
|
+ this.cachedShapeData = this.shape.getOrCreateCachedShapeData();
|
|
+ this.isEmpty = this.cachedShapeData.isEmpty();
|
|
+
|
|
+ final DoubleList xList = this.getCoords(Direction.Axis.X);
|
|
+ final DoubleList yList = this.getCoords(Direction.Axis.Y);
|
|
+ final DoubleList zList = this.getCoords(Direction.Axis.Z);
|
|
+
|
|
+ if (xList instanceof OffsetDoubleList offsetDoubleList) {
|
|
+ this.offsetX = offsetDoubleList.offset;
|
|
+ this.rootCoordinatesX = extractRawArray(offsetDoubleList.delegate);
|
|
+ } else {
|
|
+ this.rootCoordinatesX = extractRawArray(xList);
|
|
+ }
|
|
+
|
|
+ if (yList instanceof OffsetDoubleList offsetDoubleList) {
|
|
+ this.offsetY = offsetDoubleList.offset;
|
|
+ this.rootCoordinatesY = extractRawArray(offsetDoubleList.delegate);
|
|
+ } else {
|
|
+ this.rootCoordinatesY = extractRawArray(yList);
|
|
+ }
|
|
+
|
|
+ if (zList instanceof OffsetDoubleList offsetDoubleList) {
|
|
+ this.offsetZ = offsetDoubleList.offset;
|
|
+ this.rootCoordinatesZ = extractRawArray(offsetDoubleList.delegate);
|
|
+ } else {
|
|
+ this.rootCoordinatesZ = extractRawArray(zList);
|
|
+ }
|
|
+
|
|
+ if (this.cachedShapeData.hasSingleAABB()) {
|
|
+ this.singleAABBRepresentation = new AABB(
|
|
+ this.rootCoordinatesX[0] + this.offsetX, this.rootCoordinatesY[0] + this.offsetY, this.rootCoordinatesZ[0] + this.offsetZ,
|
|
+ this.rootCoordinatesX[1] + this.offsetX, this.rootCoordinatesY[1] + this.offsetY, this.rootCoordinatesZ[1] + this.offsetZ
|
|
+ );
|
|
+ this.cachedBounds = this.singleAABBRepresentation;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public final io.papermc.paper.util.collisions.CachedShapeData getCachedVoxelData() {
|
|
+ return this.cachedShapeData;
|
|
+ }
|
|
+
|
|
+ private VoxelShape[] faceShapeClampedCache;
|
|
+
|
|
+ public final VoxelShape getFaceShapeClamped(final Direction direction) {
|
|
+ if (this.isEmpty) {
|
|
+ return (VoxelShape)(Object)this;
|
|
+ }
|
|
+ if ((VoxelShape)(Object)this == Shapes.block()) {
|
|
+ return (VoxelShape)(Object)this;
|
|
+ }
|
|
+
|
|
+ VoxelShape[] cache = this.faceShapeClampedCache;
|
|
+ if (cache != null) {
|
|
+ final VoxelShape ret = cache[direction.ordinal()];
|
|
+ if (ret != null) {
|
|
+ return ret;
|
|
+ }
|
|
+ }
|
|
+
|
|
+
|
|
+ if (cache == null) {
|
|
+ this.faceShapeClampedCache = cache = new VoxelShape[6];
|
|
+ }
|
|
+
|
|
+ final Direction.Axis axis = direction.getAxis();
|
|
+
|
|
+ final VoxelShape ret;
|
|
+
|
|
+ if (direction.getAxisDirection() == Direction.AxisDirection.POSITIVE) {
|
|
+ if (DoubleMath.fuzzyEquals(this.max(axis), 1.0, io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)) {
|
|
+ ret = tryForceBlock(new SliceShape((VoxelShape)(Object)this, axis, this.shape.getSize(axis) - 1));
|
|
+ } else {
|
|
+ ret = Shapes.empty();
|
|
+ }
|
|
+ } else {
|
|
+ if (DoubleMath.fuzzyEquals(this.min(axis), 0.0, io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)) {
|
|
+ ret = tryForceBlock(new SliceShape((VoxelShape)(Object)this, axis, 0));
|
|
+ } else {
|
|
+ ret = Shapes.empty();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ cache[direction.ordinal()] = ret;
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ private static VoxelShape tryForceBlock(final VoxelShape other) {
|
|
+ if (other == Shapes.block()) {
|
|
+ return other;
|
|
+ }
|
|
+
|
|
+ final AABB otherAABB = other.getSingleAABBRepresentation();
|
|
+ if (otherAABB == null) {
|
|
+ return other;
|
|
+ }
|
|
+
|
|
+ if (Shapes.block().getSingleAABBRepresentation().equals(otherAABB)) {
|
|
+ return Shapes.block();
|
|
+ }
|
|
+
|
|
+ return other;
|
|
+ }
|
|
+
|
|
+ private boolean computeOccludesFullBlock() {
|
|
+ if (this.isEmpty) {
|
|
+ this.occludesFullBlock = Boolean.FALSE;
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ if (this.isFullBlock()) {
|
|
+ this.occludesFullBlock = Boolean.TRUE;
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ final AABB singleAABB = this.singleAABBRepresentation;
|
|
+ if (singleAABB != null) {
|
|
+ // check if the bounding box encloses the full cube
|
|
+ final boolean ret =
|
|
+ (singleAABB.minY <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && singleAABB.maxY >= (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)) &&
|
|
+ (singleAABB.minX <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && singleAABB.maxX >= (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON)) &&
|
|
+ (singleAABB.minZ <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON && singleAABB.maxZ >= (1 - io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON));
|
|
+ this.occludesFullBlock = Boolean.valueOf(ret);
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ final boolean ret = !Shapes.joinIsNotEmpty(Shapes.block(), ((VoxelShape)(Object)this), BooleanOp.ONLY_FIRST);
|
|
+ this.occludesFullBlock = Boolean.valueOf(ret);
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ public final boolean occludesFullBlock() {
|
|
+ final Boolean ret = this.occludesFullBlock;
|
|
+ if (ret != null) {
|
|
+ return ret.booleanValue();
|
|
+ }
|
|
+
|
|
+ return this.computeOccludesFullBlock();
|
|
+ }
|
|
+
|
|
+ public final boolean occludesFullBlockIfCached() {
|
|
+ final Boolean ret = this.occludesFullBlock;
|
|
+ return ret != null ? ret.booleanValue() : false;
|
|
+ }
|
|
+
|
|
+ private static int hash(final VoxelShape key) {
|
|
+ return it.unimi.dsi.fastutil.HashCommon.mix(System.identityHashCode(key));
|
|
+ }
|
|
+
|
|
+ public final VoxelShape orUnoptimized(final VoxelShape other) {
|
|
+ // don't cache simple cases
|
|
+ if (((VoxelShape)(Object)this) == other) {
|
|
+ return other;
|
|
+ }
|
|
+
|
|
+ if (this.isEmpty) {
|
|
+ return other;
|
|
+ }
|
|
+
|
|
+ if (other.isEmpty()) {
|
|
+ return (VoxelShape)(Object)this;
|
|
+ }
|
|
+
|
|
+ // try this cache first
|
|
+ final int thisCacheKey = hash(other) & (MERGED_CACHE_SIZE - 1);
|
|
+ final io.papermc.paper.util.collisions.MergedORCache cached = this.mergedORCache == null ? null : this.mergedORCache[thisCacheKey];
|
|
+ if (cached != null && cached.key() == other) {
|
|
+ return cached.result();
|
|
+ }
|
|
+
|
|
+ // try other cache
|
|
+ final int otherCacheKey = hash(this) & (MERGED_CACHE_SIZE - 1);
|
|
+ final io.papermc.paper.util.collisions.MergedORCache otherCache = other.mergedORCache == null ? null : other.mergedORCache[otherCacheKey];
|
|
+ if (otherCache != null && otherCache.key() == this) {
|
|
+ return otherCache.result();
|
|
+ }
|
|
+
|
|
+ // note: unsure if joinUnoptimized(1, 2, OR) == joinUnoptimized(2, 1, OR) for all cases
|
|
+ final VoxelShape result = Shapes.joinUnoptimized(this, other, BooleanOp.OR);
|
|
+
|
|
+ if (cached != null && otherCache == null) {
|
|
+ // try to use second cache instead of replacing an entry in this cache
|
|
+ if (other.mergedORCache == null) {
|
|
+ other.mergedORCache = new io.papermc.paper.util.collisions.MergedORCache[MERGED_CACHE_SIZE];
|
|
+ }
|
|
+ other.mergedORCache[otherCacheKey] = new io.papermc.paper.util.collisions.MergedORCache(this, result);
|
|
+ } else {
|
|
+ // line is not occupied or other cache line is full
|
|
+ // always bias to replace this cache, as this cache is the first we check
|
|
+ if (this.mergedORCache == null) {
|
|
+ this.mergedORCache = new io.papermc.paper.util.collisions.MergedORCache[MERGED_CACHE_SIZE];
|
|
+ }
|
|
+ this.mergedORCache[thisCacheKey] = new io.papermc.paper.util.collisions.MergedORCache(other, result);
|
|
+ }
|
|
+
|
|
+ return result;
|
|
+ }
|
|
+
|
|
+ private boolean computeFullBlock() {
|
|
+ Boolean ret;
|
|
+ if (this.isEmpty) {
|
|
+ ret = Boolean.FALSE;
|
|
+ } else if ((VoxelShape)(Object)this == Shapes.block()) {
|
|
+ ret = Boolean.TRUE;
|
|
+ } else {
|
|
+ final AABB singleAABB = this.singleAABBRepresentation;
|
|
+ if (singleAABB == null) {
|
|
+ final io.papermc.paper.util.collisions.CachedShapeData shapeData = this.cachedShapeData;
|
|
+ final int sMinX = shapeData.minFullX();
|
|
+ final int sMinY = shapeData.minFullY();
|
|
+ final int sMinZ = shapeData.minFullZ();
|
|
+
|
|
+ final int sMaxX = shapeData.maxFullX();
|
|
+ final int sMaxY = shapeData.maxFullY();
|
|
+ final int sMaxZ = shapeData.maxFullZ();
|
|
+
|
|
+ if (Math.abs(this.rootCoordinatesX[sMinX] + this.offsetX) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON &&
|
|
+ Math.abs(this.rootCoordinatesY[sMinY] + this.offsetY) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON &&
|
|
+ Math.abs(this.rootCoordinatesZ[sMinZ] + this.offsetZ) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON &&
|
|
+
|
|
+ Math.abs(1.0 - (this.rootCoordinatesX[sMaxX] + this.offsetX)) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON &&
|
|
+ Math.abs(1.0 - (this.rootCoordinatesY[sMaxY] + this.offsetY)) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON &&
|
|
+ Math.abs(1.0 - (this.rootCoordinatesZ[sMaxZ] + this.offsetZ)) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) {
|
|
+
|
|
+ // index = z + y*sizeZ + x*(sizeZ*sizeY)
|
|
+
|
|
+ final int sizeY = shapeData.sizeY();
|
|
+ final int sizeZ = shapeData.sizeZ();
|
|
+
|
|
+ final long[] bitset = shapeData.voxelSet();
|
|
+
|
|
+ ret = Boolean.TRUE;
|
|
+
|
|
+ check_full:
|
|
+ for (int x = sMinX; x < sMaxX; ++x) {
|
|
+ for (int y = sMinY; y < sMaxY; ++y) {
|
|
+ final int baseIndex = y*sizeZ + x*(sizeZ*sizeY);
|
|
+ if (!io.papermc.paper.util.collisions.FlatBitsetUtil.isRangeSet(bitset, baseIndex + sMinZ, baseIndex + sMaxZ)) {
|
|
+ ret = Boolean.FALSE;
|
|
+ break check_full;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ } else {
|
|
+ ret = Boolean.FALSE;
|
|
+ }
|
|
+ } else {
|
|
+ ret = Boolean.valueOf(
|
|
+ Math.abs(singleAABB.minX) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON &&
|
|
+ Math.abs(singleAABB.minY) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON &&
|
|
+ Math.abs(singleAABB.minZ) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON &&
|
|
+
|
|
+ Math.abs(1.0 - singleAABB.maxX) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON &&
|
|
+ Math.abs(1.0 - singleAABB.maxY) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON &&
|
|
+ Math.abs(1.0 - singleAABB.maxZ) <= io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON
|
|
+ );
|
|
+ }
|
|
+ }
|
|
+
|
|
+ this.isFullBlock = ret;
|
|
+
|
|
+ return ret.booleanValue();
|
|
+ }
|
|
+
|
|
+ public boolean isFullBlock() {
|
|
+ final Boolean ret = this.isFullBlock;
|
|
+
|
|
+ if (ret != null) {
|
|
+ return ret.booleanValue();
|
|
+ }
|
|
+
|
|
+ return this.computeFullBlock();
|
|
+ }
|
|
+ // Paper end - optimise collisions
|
|
+
|
|
+ protected VoxelShape(DiscreteVoxelShape voxels) { // Paper - protected
|
|
this.shape = voxels;
|
|
}
|
|
|
|
public double min(Direction.Axis axis) {
|
|
- int i = this.shape.firstFull(axis);
|
|
- return i >= this.shape.getSize(axis) ? Double.POSITIVE_INFINITY : this.get(axis, i);
|
|
+ // Paper start - optimise collisions
|
|
+ final io.papermc.paper.util.collisions.CachedShapeData shapeData = this.cachedShapeData;
|
|
+ switch (axis) {
|
|
+ case X: {
|
|
+ final int idx = shapeData.minFullX();
|
|
+ return idx >= shapeData.sizeX() ? Double.POSITIVE_INFINITY : (this.rootCoordinatesX[idx] + this.offsetX);
|
|
+ }
|
|
+ case Y: {
|
|
+ final int idx = shapeData.minFullY();
|
|
+ return idx >= shapeData.sizeY() ? Double.POSITIVE_INFINITY : (this.rootCoordinatesY[idx] + this.offsetY);
|
|
+ }
|
|
+ case Z: {
|
|
+ final int idx = shapeData.minFullZ();
|
|
+ return idx >= shapeData.sizeZ() ? Double.POSITIVE_INFINITY : (this.rootCoordinatesZ[idx] + this.offsetZ);
|
|
+ }
|
|
+ default: {
|
|
+ // should never get here
|
|
+ return Double.POSITIVE_INFINITY;
|
|
+ }
|
|
+ }
|
|
+ // Paper end - optimise collisions
|
|
}
|
|
|
|
public double max(Direction.Axis axis) {
|
|
- int i = this.shape.lastFull(axis);
|
|
- return i <= 0 ? Double.NEGATIVE_INFINITY : this.get(axis, i);
|
|
+ // Paper start - optimise collisions
|
|
+ final io.papermc.paper.util.collisions.CachedShapeData shapeData = this.cachedShapeData;
|
|
+ switch (axis) {
|
|
+ case X: {
|
|
+ final int idx = shapeData.maxFullX();
|
|
+ return idx <= 0 ? Double.NEGATIVE_INFINITY : (this.rootCoordinatesX[idx] + this.offsetX);
|
|
+ }
|
|
+ case Y: {
|
|
+ final int idx = shapeData.maxFullY();
|
|
+ return idx <= 0 ? Double.NEGATIVE_INFINITY : (this.rootCoordinatesY[idx] + this.offsetY);
|
|
+ }
|
|
+ case Z: {
|
|
+ final int idx = shapeData.maxFullZ();
|
|
+ return idx <= 0 ? Double.NEGATIVE_INFINITY : (this.rootCoordinatesZ[idx] + this.offsetZ);
|
|
+ }
|
|
+ default: {
|
|
+ // should never get here
|
|
+ return Double.NEGATIVE_INFINITY;
|
|
+ }
|
|
+ }
|
|
+ // Paper end - optimise collisions
|
|
}
|
|
|
|
public AABB bounds() {
|
|
- if (this.isEmpty()) {
|
|
- throw (UnsupportedOperationException)Util.pauseInIde(new UnsupportedOperationException("No bounds for empty shape."));
|
|
- } else {
|
|
- return new AABB(
|
|
- this.min(Direction.Axis.X),
|
|
- this.min(Direction.Axis.Y),
|
|
- this.min(Direction.Axis.Z),
|
|
- this.max(Direction.Axis.X),
|
|
- this.max(Direction.Axis.Y),
|
|
- this.max(Direction.Axis.Z)
|
|
- );
|
|
+ // Paper start - optimise collisions
|
|
+ if (this.isEmpty) {
|
|
+ throw Util.pauseInIde(new UnsupportedOperationException("No bounds for empty shape."));
|
|
+ }
|
|
+ AABB cached = this.cachedBounds;
|
|
+ if (cached != null) {
|
|
+ return cached;
|
|
}
|
|
+
|
|
+ final io.papermc.paper.util.collisions.CachedShapeData shapeData = this.cachedShapeData;
|
|
+
|
|
+ final double[] coordsX = this.rootCoordinatesX;
|
|
+ final double[] coordsY = this.rootCoordinatesY;
|
|
+ final double[] coordsZ = this.rootCoordinatesZ;
|
|
+
|
|
+ final double offX = this.offsetX;
|
|
+ final double offY = this.offsetY;
|
|
+ final double offZ = this.offsetZ;
|
|
+
|
|
+ // note: if not empty, then there is one full AABB so no bounds checks are needed on the minFull/maxFull indices
|
|
+ cached = new AABB(
|
|
+ coordsX[shapeData.minFullX()] + offX,
|
|
+ coordsY[shapeData.minFullY()] + offY,
|
|
+ coordsZ[shapeData.minFullZ()] + offZ,
|
|
+
|
|
+ coordsX[shapeData.maxFullX()] + offX,
|
|
+ coordsY[shapeData.maxFullY()] + offY,
|
|
+ coordsZ[shapeData.maxFullZ()] + offZ
|
|
+ );
|
|
+
|
|
+ this.cachedBounds = cached;
|
|
+ return cached;
|
|
+ // Paper end - optimise collisions
|
|
}
|
|
|
|
public VoxelShape singleEncompassing() {
|
|
@@ -69,28 +470,106 @@ public abstract class VoxelShape {
|
|
protected abstract DoubleList getCoords(Direction.Axis axis);
|
|
|
|
public boolean isEmpty() {
|
|
- return this.shape.isEmpty();
|
|
+ return this.isEmpty; // Paper - optimise collisions
|
|
+ }
|
|
+
|
|
+ // Paper start - optimise collisions
|
|
+ private static DoubleList offsetList(final DoubleList src, final double by) {
|
|
+ if (src instanceof OffsetDoubleList offsetDoubleList) {
|
|
+ return new OffsetDoubleList(offsetDoubleList.delegate, by + offsetDoubleList.offset);
|
|
+ }
|
|
+ return new OffsetDoubleList(src, by);
|
|
}
|
|
+ // Paper end - optimise collisions
|
|
|
|
public VoxelShape move(double x, double y, double z) {
|
|
- return (VoxelShape)(this.isEmpty()
|
|
- ? Shapes.empty()
|
|
- : new ArrayVoxelShape(
|
|
+ // Paper start - optimise collisions
|
|
+ if (this.isEmpty) {
|
|
+ return Shapes.empty();
|
|
+ }
|
|
+
|
|
+ final ArrayVoxelShape ret = new ArrayVoxelShape(
|
|
this.shape,
|
|
- new OffsetDoubleList(this.getCoords(Direction.Axis.X), x),
|
|
- new OffsetDoubleList(this.getCoords(Direction.Axis.Y), y),
|
|
- new OffsetDoubleList(this.getCoords(Direction.Axis.Z), z)
|
|
- ));
|
|
+ offsetList(this.getCoords(Direction.Axis.X), x),
|
|
+ offsetList(this.getCoords(Direction.Axis.Y), y),
|
|
+ offsetList(this.getCoords(Direction.Axis.Z), z)
|
|
+ );
|
|
+
|
|
+ final io.papermc.paper.util.collisions.CachedToAABBs cachedToAABBs = this.cachedToAABBs;
|
|
+ if (cachedToAABBs != null) {
|
|
+ ((VoxelShape)ret).cachedToAABBs = io.papermc.paper.util.collisions.CachedToAABBs.offset(cachedToAABBs, x, y, z);
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ // Paper end - optimise collisions
|
|
}
|
|
|
|
public VoxelShape optimize() {
|
|
- VoxelShape[] voxelShapes = new VoxelShape[]{Shapes.empty()};
|
|
- this.forAllBoxes(
|
|
- (minX, minY, minZ, maxX, maxY, maxZ) -> voxelShapes[0] = Shapes.joinUnoptimized(
|
|
- voxelShapes[0], Shapes.box(minX, minY, minZ, maxX, maxY, maxZ), BooleanOp.OR
|
|
- )
|
|
- );
|
|
- return voxelShapes[0];
|
|
+ // Paper start - optimise collisions
|
|
+ // Optimise merge strategy to increase the number of simple joins, and additionally forward the toAabbs cache
|
|
+ // to result
|
|
+ if (this.isEmpty) {
|
|
+ return Shapes.empty();
|
|
+ }
|
|
+
|
|
+ if (this.singleAABBRepresentation != null) {
|
|
+ // note: the isFullBlock() is fuzzy, and Shapes.create() is also fuzzy which would return block()
|
|
+ return this.isFullBlock() ? Shapes.block() : this;
|
|
+ }
|
|
+
|
|
+ final List<AABB> aabbs = this.toAabbs();
|
|
+
|
|
+ if (aabbs.size() == 1) {
|
|
+ final AABB singleAABB = aabbs.get(0);
|
|
+ final VoxelShape ret = Shapes.create(singleAABB);
|
|
+
|
|
+ // forward AABB cache
|
|
+ if (ret.cachedToAABBs == null) {
|
|
+ ret.cachedToAABBs = this.cachedToAABBs;
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ } else {
|
|
+ // reduce complexity of joins by splitting the merges (old complexity: n^2, new: nlogn)
|
|
+
|
|
+ // set up flat array so that this merge is done in-place
|
|
+ final VoxelShape[] tmp = new VoxelShape[aabbs.size()];
|
|
+
|
|
+ // initialise as unmerged
|
|
+ for (int i = 0, len = aabbs.size(); i < len; ++i) {
|
|
+ tmp[i] = Shapes.create(aabbs.get(i));
|
|
+ }
|
|
+
|
|
+ int size = aabbs.size();
|
|
+ while (size > 1) {
|
|
+ int newSize = 0;
|
|
+ for (int i = 0; i < size; i += 2) {
|
|
+ final int next = i + 1;
|
|
+ if (next >= size) {
|
|
+ // nothing to merge with, so leave it for next iteration
|
|
+ tmp[newSize++] = tmp[i];
|
|
+ break;
|
|
+ } else {
|
|
+ // merge with adjacent
|
|
+ final VoxelShape first = tmp[i];
|
|
+ final VoxelShape second = tmp[next];
|
|
+
|
|
+ tmp[newSize++] = Shapes.joinUnoptimized(first, second, BooleanOp.OR);
|
|
+ }
|
|
+ }
|
|
+ size = newSize;
|
|
+ }
|
|
+
|
|
+ final VoxelShape ret = tmp[0];
|
|
+
|
|
+ // forward AABB cache
|
|
+ if (ret.cachedToAABBs == null) {
|
|
+ ret.cachedToAABBs = this.cachedToAABBs;
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+ // Paper end - optimise collisions
|
|
}
|
|
|
|
public void forAllEdges(Shapes.DoubleLineConsumer consumer) {
|
|
@@ -126,10 +605,43 @@ public abstract class VoxelShape {
|
|
);
|
|
}
|
|
|
|
+ // Paper start - optimise collisions
|
|
+ private List<AABB> toAabbsUncached() {
|
|
+ final List<AABB> ret = new java.util.ArrayList<>();
|
|
+ if (this.singleAABBRepresentation != null) {
|
|
+ ret.add(this.singleAABBRepresentation);
|
|
+ } else {
|
|
+ this.forAllBoxes((minX, minY, minZ, maxX, maxY, maxZ) -> {
|
|
+ ret.add(new AABB(minX, minY, minZ, maxX, maxY, maxZ));
|
|
+ });
|
|
+ }
|
|
+
|
|
+ // cache result
|
|
+ this.cachedToAABBs = new io.papermc.paper.util.collisions.CachedToAABBs(ret, false, 0.0, 0.0, 0.0);
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+ // Paper end - optimise collisions
|
|
+
|
|
public List<AABB> toAabbs() {
|
|
- List<AABB> list = Lists.newArrayList();
|
|
- this.forAllBoxes((x1, y1, z1, x2, y2, z2) -> list.add(new AABB(x1, y1, z1, x2, y2, z2)));
|
|
- return list;
|
|
+ // Paper start - optimise collisions
|
|
+ io.papermc.paper.util.collisions.CachedToAABBs cachedToAABBs = this.cachedToAABBs;
|
|
+ if (cachedToAABBs != null) {
|
|
+ if (!cachedToAABBs.isOffset()) {
|
|
+ return cachedToAABBs.aabbs();
|
|
+ }
|
|
+
|
|
+ // all we need to do is offset the cache
|
|
+ cachedToAABBs = cachedToAABBs.removeOffset();
|
|
+ // update cache
|
|
+ this.cachedToAABBs = cachedToAABBs;
|
|
+
|
|
+ return cachedToAABBs.aabbs();
|
|
+ }
|
|
+
|
|
+ // make new cache
|
|
+ return this.toAabbsUncached();
|
|
+ // Paper end - optimise collisions
|
|
}
|
|
|
|
public double min(Direction.Axis axis, double from, double to) {
|
|
@@ -154,43 +666,85 @@ public abstract class VoxelShape {
|
|
return Mth.binarySearch(0, this.shape.getSize(axis) + 1, i -> coord < this.get(axis, i)) - 1;
|
|
}
|
|
|
|
+ // Paper start - optimise collisions
|
|
+ /**
|
|
+ * Copy of AABB#clip but for one AABB
|
|
+ */
|
|
+ private static BlockHitResult clip(final AABB aabb, final Vec3 from, final Vec3 to, final BlockPos offset) {
|
|
+ final double[] minDistanceArr = new double[] { 1.0 };
|
|
+ final double diffX = to.x - from.x;
|
|
+ final double diffY = to.y - from.y;
|
|
+ final double diffZ = to.z - from.z;
|
|
+
|
|
+ final Direction direction = AABB.getDirection(aabb.move(offset), from, minDistanceArr, null, diffX, diffY, diffZ);
|
|
+
|
|
+ if (direction == null) {
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ final double minDistance = minDistanceArr[0];
|
|
+ return new BlockHitResult(from.add(minDistance * diffX, minDistance * diffY, minDistance * diffZ), direction, offset, false);
|
|
+ }
|
|
+ // Paper end - optimise collisions
|
|
+
|
|
@Nullable
|
|
public BlockHitResult clip(Vec3 start, Vec3 end, BlockPos pos) {
|
|
- if (this.isEmpty()) {
|
|
+ // Paper start - optimise collisions
|
|
+ if (this.isEmpty) {
|
|
return null;
|
|
- } else {
|
|
- Vec3 vec3 = end.subtract(start);
|
|
- if (vec3.lengthSqr() < 1.0E-7) {
|
|
- return null;
|
|
- } else {
|
|
- Vec3 vec32 = start.add(vec3.scale(0.001));
|
|
- return this.shape
|
|
- .isFullWide(
|
|
- this.findIndex(Direction.Axis.X, vec32.x - (double)pos.getX()),
|
|
- this.findIndex(Direction.Axis.Y, vec32.y - (double)pos.getY()),
|
|
- this.findIndex(Direction.Axis.Z, vec32.z - (double)pos.getZ())
|
|
- )
|
|
- ? new BlockHitResult(vec32, Direction.getNearest(vec3.x, vec3.y, vec3.z).getOpposite(), pos, true)
|
|
- : AABB.clip(this.toAabbs(), start, end, pos);
|
|
+ }
|
|
+
|
|
+ final Vec3 directionOpposite = end.subtract(start);
|
|
+ if (directionOpposite.lengthSqr() < io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) {
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ final Vec3 fromBehind = start.add(directionOpposite.scale(0.001));
|
|
+ final double fromBehindOffsetX = fromBehind.x - (double)pos.getX();
|
|
+ final double fromBehindOffsetY = fromBehind.y - (double)pos.getY();
|
|
+ final double fromBehindOffsetZ = fromBehind.z - (double)pos.getZ();
|
|
+
|
|
+ final AABB singleAABB = this.singleAABBRepresentation;
|
|
+ if (singleAABB != null) {
|
|
+ if (singleAABB.contains(fromBehindOffsetX, fromBehindOffsetY, fromBehindOffsetZ)) {
|
|
+ return new BlockHitResult(fromBehind, Direction.getNearest(directionOpposite.x, directionOpposite.y, directionOpposite.z).getOpposite(), pos, true);
|
|
}
|
|
+ return clip(singleAABB, start, end, pos);
|
|
}
|
|
+
|
|
+ if (io.papermc.paper.util.CollisionUtil.strictlyContains(this, fromBehindOffsetX, fromBehindOffsetY, fromBehindOffsetZ)) {
|
|
+ return new BlockHitResult(fromBehind, Direction.getNearest(directionOpposite.x, directionOpposite.y, directionOpposite.z).getOpposite(), pos, true);
|
|
+ }
|
|
+
|
|
+ return AABB.clip(this.toAabbs(), start, end, pos);
|
|
+ // Paper end - optimise collisions
|
|
}
|
|
|
|
public Optional<Vec3> closestPointTo(Vec3 target) {
|
|
- if (this.isEmpty()) {
|
|
+ // Paper start - optimise collisions
|
|
+ if (this.isEmpty) {
|
|
return Optional.empty();
|
|
- } else {
|
|
- Vec3[] vec3s = new Vec3[1];
|
|
- this.forAllBoxes((minX, minY, minZ, maxX, maxY, maxZ) -> {
|
|
- double d = Mth.clamp(target.x(), minX, maxX);
|
|
- double e = Mth.clamp(target.y(), minY, maxY);
|
|
- double f = Mth.clamp(target.z(), minZ, maxZ);
|
|
- if (vec3s[0] == null || target.distanceToSqr(d, e, f) < target.distanceToSqr(vec3s[0])) {
|
|
- vec3s[0] = new Vec3(d, e, f);
|
|
- }
|
|
- });
|
|
- return Optional.of(vec3s[0]);
|
|
}
|
|
+
|
|
+ Vec3 ret = null;
|
|
+ double retDistance = Double.MAX_VALUE;
|
|
+
|
|
+ final List<AABB> aabbs = this.toAabbs();
|
|
+ for (int i = 0, len = aabbs.size(); i < len; ++i) {
|
|
+ final AABB aabb = aabbs.get(i);
|
|
+ final double x = Mth.clamp(target.x, aabb.minX, aabb.maxX);
|
|
+ final double y = Mth.clamp(target.y, aabb.minY, aabb.maxY);
|
|
+ final double z = Mth.clamp(target.z, aabb.minZ, aabb.maxZ);
|
|
+
|
|
+ double dist = target.distanceToSqr(x, y, z);
|
|
+ if (dist < retDistance) {
|
|
+ ret = new Vec3(x, y, z);
|
|
+ retDistance = dist;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return Optional.ofNullable(ret);
|
|
+ // Paper end - optimise collisions
|
|
}
|
|
|
|
public VoxelShape getFaceShape(Direction facing) {
|
|
@@ -227,7 +781,28 @@ public abstract class VoxelShape {
|
|
}
|
|
|
|
public double collide(Direction.Axis axis, AABB box, double maxDist) {
|
|
- return this.collideX(AxisCycle.between(axis, Direction.Axis.X), box, maxDist);
|
|
+ // Paper start - optimise collisions
|
|
+ if (this.isEmpty) {
|
|
+ return maxDist;
|
|
+ }
|
|
+ if (Math.abs(maxDist) < io.papermc.paper.util.CollisionUtil.COLLISION_EPSILON) {
|
|
+ return 0.0;
|
|
+ }
|
|
+ switch (axis) {
|
|
+ case X: {
|
|
+ return io.papermc.paper.util.CollisionUtil.collideX(this, box, maxDist);
|
|
+ }
|
|
+ case Y: {
|
|
+ return io.papermc.paper.util.CollisionUtil.collideY(this, box, maxDist);
|
|
+ }
|
|
+ case Z: {
|
|
+ return io.papermc.paper.util.CollisionUtil.collideZ(this, box, maxDist);
|
|
+ }
|
|
+ default: {
|
|
+ throw new RuntimeException("Unknown axis: " + axis);
|
|
+ }
|
|
+ }
|
|
+ // Paper end - optimise collisions
|
|
}
|
|
|
|
protected double collideX(AxisCycle axisCycle, AABB box, double maxDist) {
|