From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Spottedleaf 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 TEMP_COLLISION_LIST = new UnsafeList<>(1024); + static boolean tempCollisionListInUse; + + public static UnsafeList getTempCollisionList() { + if (!Bukkit.isPrimaryThread() || tempCollisionListInUse) { + return new UnsafeList<>(16); + } + tempCollisionListInUse = true; + return TEMP_COLLISION_LIST; + } + + public static void returnTempCollisionList(List list) { + if (list != TEMP_COLLISION_LIST) { + return; + } + ((UnsafeList)list).setSize(0); + tempCollisionListInUse = false; + } + static final UnsafeList TEMP_GET_ENTITIES_LIST = new UnsafeList<>(1024); + static boolean tempGetEntitiesListInUse; + + public static UnsafeList getTempGetEntitiesList() { + if (!Bukkit.isPrimaryThread() || tempGetEntitiesListInUse) { + return new UnsafeList<>(16); + } + tempGetEntitiesListInUse = true; + return TEMP_GET_ENTITIES_LIST; + } + + public static void returnTempGetEntitiesList(List 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 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 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 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 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 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 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 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 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 voxels, + final List 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 intoVoxel, final List intoAABB, + final int collisionFlags, final BiPredicate 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 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 into, final int collisionFlags, final Predicate 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 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 intoVoxel, final List intoAABB, final int collisionFlags, + final BiPredicate blockPredicate, + final Predicate 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 aabbs, + boolean isOffset, + double offX, double offY, double offZ +) { + + public CachedToAABBs removeOffset() { + final List toOffset = this.aabbs; + final double offX = this.offX; + final double offY = this.offY; + final double offZ = this.offZ; + + final List 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 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 potentialCollisionsBB = new java.util.ArrayList<>(); + final List 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 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 list = this.level().getEntities((Entity) this, this.getBoundingBox(), ArmorStand.RIDABLE_MINECARTS); + List 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 extends AbstractIterator { // 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 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 List getEntitiesOfClass(Class entityClass, AABB box) { @@ -67,23 +83,41 @@ public interface EntityGetter { } default List 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 entities; + if (entity != null && entity.hardCollides()) { + entities = this.getEntities(entity, box, null); } else { - Predicate predicate = entity == null ? EntitySelector.CAN_BE_COLLIDED_WITH : EntitySelector.NO_SPECTATORS.and(entity::canCollideWith); - List list = this.getEntities(entity, box.inflate(1.0E-7), predicate); - if (list.isEmpty()) { - return List.of(); - } else { - Builder builder = ImmutableList.builderWithExpectedSize(list.size()); - - for (Entity entity2 : list) { - builder.add(Shapes.create(entity2.getBoundingBox())); - } + entities = this.getHardCollidingEntities(entity, box, null); + } + + final List 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 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 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 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 aabbs = new java.util.ArrayList<>(); + final List 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 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> 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 datapaletteblock, PalettedContainer> 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 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 toAabbsUncached() { + final List 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 toAabbs() { - List 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 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 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) {