From 92fb91fb08f344ca09e004ebebe5190c095506fd Mon Sep 17 00:00:00 2001 From: MrHua269 Date: Wed, 5 Feb 2025 15:30:33 +0800 Subject: [PATCH] Raytracing entity tracker Based on the framework of EntityCulling((((((( --- .../0042-Raytracing-tracker-experiment.patch | 166 +++ .../0037-Raytracing-tracker-experiment.patch | 1043 +++++++++++++++++ 2 files changed, 1209 insertions(+) create mode 100644 luminol-server/minecraft-patches/features/0042-Raytracing-tracker-experiment.patch create mode 100644 luminol-server/paper-patches/features/0037-Raytracing-tracker-experiment.patch diff --git a/luminol-server/minecraft-patches/features/0042-Raytracing-tracker-experiment.patch b/luminol-server/minecraft-patches/features/0042-Raytracing-tracker-experiment.patch new file mode 100644 index 0000000..abbe575 --- /dev/null +++ b/luminol-server/minecraft-patches/features/0042-Raytracing-tracker-experiment.patch @@ -0,0 +1,166 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Wed, 5 Feb 2025 15:22:19 +0800 +Subject: [PATCH] Raytracing tracker experiment + +Based on the framework of EntityCulling((((((( + + +diff --git a/net/minecraft/server/level/ChunkMap.java b/net/minecraft/server/level/ChunkMap.java +index 7eff847790394aecd058e7a61905da86163b4c6e..9099457f55a2829297ac1db8a69a98ff717d9a86 100644 +--- a/net/minecraft/server/level/ChunkMap.java ++++ b/net/minecraft/server/level/ChunkMap.java +@@ -1208,7 +1208,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + double d1 = vec3_dx * vec3_dx + vec3_dz * vec3_dz; // Paper + double d2 = d * d; + // Paper start - Configurable entity tracking range by Y +- boolean flag = d1 <= d2; ++ boolean flag = d1 <= d2 && !entity.isCulled(); // Luminol - Ray tracing entity tracker + if (flag && level.paperConfig().entities.trackingRangeY.enabled) { + double rangeY = level.paperConfig().entities.trackingRangeY.get(this.entity, -1); + if (rangeY != -1) { +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index 6f9cfd8c72853d9cb30c9731a96e7f1e8f0644c4..b1ded501047f75d2e7af775bf8866fd6a5400a91 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -136,7 +136,7 @@ import net.minecraft.world.scores.ScoreHolder; + import net.minecraft.world.scores.Team; + import org.slf4j.Logger; + +-public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess, ScoreHolder, ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity, ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity { // Paper - rewrite chunk system // Paper - optimise entity tracker ++public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess, ScoreHolder, ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity, ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity, dev.tr7zw.entityculling.versionless.access.Cullable { // Paper - rewrite chunk system // Paper - optimise entity tracker // Luminol - Ray tracing entity tracker + + // CraftBukkit start + private static final int CURRENT_LEVEL = 2; +@@ -6050,4 +6050,46 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + // Paper end - Expose entity id counter + + public boolean shouldTickHot() { return this.tickCount > 20 * 10 && this.isAlive(); } // KioCG ++ ++ private long lasttime = 0; ++ private boolean culled = false; ++ private boolean outOfCamera = false; ++ ++ @Override ++ public void setTimeout() { ++ this.lasttime = System.currentTimeMillis() + 1000; ++ } ++ ++ @Override ++ public boolean isForcedVisible() { ++ return this.lasttime > System.currentTimeMillis(); ++ } ++ ++ @Override ++ public void setCulled(boolean value) { ++ this.culled = value; ++ if (!value) { ++ setTimeout(); ++ } ++ } ++ ++ @Override ++ public boolean isCulled() { ++ if (!me.earthme.luminol.config.modules.experiment.RayTrackingEntityTrackerConfig.enabled) ++ return false; ++ return this.culled; ++ } ++ ++ @Override ++ public void setOutOfCamera(boolean value) { ++ this.outOfCamera = value; ++ } ++ ++ @Override ++ public boolean isOutOfCamera() { ++ if (!me.earthme.luminol.config.modules.experiment.RayTrackingEntityTrackerConfig.enabled) ++ return false; ++ return this.outOfCamera; ++ } ++ + } +diff --git a/net/minecraft/world/entity/EntityType.java b/net/minecraft/world/entity/EntityType.java +index d9cc1d7e56c37d5ce92544edc10e89dbc89dd15d..39e7689be243b9c99b507d665f6591359115287b 100644 +--- a/net/minecraft/world/entity/EntityType.java ++++ b/net/minecraft/world/entity/EntityType.java +@@ -1097,6 +1097,9 @@ public class EntityType implements FeatureElement, EntityTypeT + public final int passengerTickTimerId; + public final int passengerInactiveTickTimerId; + // Folia end - profiler ++ // Luminol - Raytracing entity tracker ++ public boolean skipRaytracningCheck = false; ++ // Luminol end + + public EntityType( + EntityType.EntityFactory factory, +diff --git a/net/minecraft/world/entity/player/Player.java b/net/minecraft/world/entity/player/Player.java +index 0e020dfebe06dce4c19beb10c961ea9e8a35a415..320f2131d3c5aee68d5eda43db0d5745ec9358db 100644 +--- a/net/minecraft/world/entity/player/Player.java ++++ b/net/minecraft/world/entity/player/Player.java +@@ -210,6 +210,25 @@ public abstract class Player extends LivingEntity { + return (org.bukkit.craftbukkit.entity.CraftHumanEntity) super.getBukkitEntity(); + } + // CraftBukkit end ++ // Luminol start - Raytracing entity tracker ++ public dev.tr7zw.entityculling.CullTask cullTask; ++ { ++ if (!me.earthme.luminol.config.modules.experiment.RayTrackingEntityTrackerConfig.enabled) { ++ this.cullTask = null; ++ }else { ++ final com.logisticscraft.occlusionculling.OcclusionCullingInstance culling = new com.logisticscraft.occlusionculling.OcclusionCullingInstance( ++ me.earthme.luminol.config.modules.experiment.RayTrackingEntityTrackerConfig.tracingDistance, ++ new dev.tr7zw.entityculling.DefaultChunkDataProvider(this.level()) ++ ); ++ ++ this.cullTask = new dev.tr7zw.entityculling.CullTask( ++ culling, this, ++ me.earthme.luminol.config.modules.experiment.RayTrackingEntityTrackerConfig.hitboxLimit, ++ me.earthme.luminol.config.modules.experiment.RayTrackingEntityTrackerConfig.checkIntervalMs ++ ); ++ } ++ } ++ // Luminol end + + public Player(Level level, BlockPos pos, float yRot, GameProfile gameProfile) { + super(EntityType.PLAYER, level); +@@ -262,6 +281,24 @@ public abstract class Player extends LivingEntity { + + @Override + public void tick() { ++ // Luminol start - Ray tracing entity tracker ++ if (!me.earthme.luminol.config.modules.experiment.RayTrackingEntityTrackerConfig.enabled) { ++ this.cullTask = null; ++ }else { ++ final com.logisticscraft.occlusionculling.OcclusionCullingInstance culling = new com.logisticscraft.occlusionculling.OcclusionCullingInstance( ++ me.earthme.luminol.config.modules.experiment.RayTrackingEntityTrackerConfig.tracingDistance, ++ new dev.tr7zw.entityculling.DefaultChunkDataProvider(this.level()) ++ ); ++ ++ this.cullTask = new dev.tr7zw.entityculling.CullTask( ++ culling, this, ++ me.earthme.luminol.config.modules.experiment.RayTrackingEntityTrackerConfig.hitboxLimit, ++ me.earthme.luminol.config.modules.experiment.RayTrackingEntityTrackerConfig.checkIntervalMs ++ ); ++ } ++ if (this.cullTask != null) this.cullTask.setup(); ++ // Luminol end ++ + this.noPhysics = this.isSpectator(); + if (this.isSpectator() || this.isPassenger()) { + this.setOnGround(false); +@@ -345,6 +382,7 @@ public abstract class Player extends LivingEntity { + if (this.currentImpulseContextResetGraceTime > 0) { + this.currentImpulseContextResetGraceTime--; + } ++ if (this.cullTask != null) this.cullTask.requestCullSignal(); // Luminol - Ray tracing entity tracker + } + + @Override +@@ -1502,6 +1540,7 @@ public abstract class Player extends LivingEntity { + if (this.containerMenu != null && this.hasContainerOpen()) { + this.doCloseContainer(); + } ++ if (this.cullTask != null) this.cullTask.signalStop(); // Luminol - Ray tracing entity tracker + } + + // Folia start - region threading diff --git a/luminol-server/paper-patches/features/0037-Raytracing-tracker-experiment.patch b/luminol-server/paper-patches/features/0037-Raytracing-tracker-experiment.patch new file mode 100644 index 0000000..e078f4b --- /dev/null +++ b/luminol-server/paper-patches/features/0037-Raytracing-tracker-experiment.patch @@ -0,0 +1,1043 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Wed, 5 Feb 2025 15:22:21 +0800 +Subject: [PATCH] Raytracing tracker experiment + +Based on the framework of EntityCulling((((((( + + +diff --git a/src/main/java/com/logisticscraft/occlusionculling/DataProvider.java b/src/main/java/com/logisticscraft/occlusionculling/DataProvider.java +new file mode 100644 +index 0000000000000000000000000000000000000000..5ab4600554e8dcb2a2ddc22dd1c1015c89ba5b82 +--- /dev/null ++++ b/src/main/java/com/logisticscraft/occlusionculling/DataProvider.java +@@ -0,0 +1,34 @@ ++package com.logisticscraft.occlusionculling; ++ ++import com.logisticscraft.occlusionculling.util.Vec3d; ++ ++public interface DataProvider { ++ ++ /** ++ * Prepares the requested chunk. Returns true if the chunk is ready, false when ++ * not loaded. Should not reload the chunk when the x and y are the same as the ++ * last request! ++ * ++ * @param chunkX ++ * @param chunkZ ++ * @return ++ */ ++ boolean prepareChunk(int chunkX, int chunkZ); ++ ++ /** ++ * Location is inside the chunk. ++ * ++ * @param x ++ * @param y ++ * @param z ++ * @return ++ */ ++ boolean isOpaqueFullCube(int x, int y, int z); ++ ++ default void cleanup() { ++ } ++ ++ default void checkingPosition(Vec3d[] targetPoints, int size, Vec3d viewerPosition) { ++ } ++ ++} +diff --git a/src/main/java/com/logisticscraft/occlusionculling/OcclusionCullingInstance.java b/src/main/java/com/logisticscraft/occlusionculling/OcclusionCullingInstance.java +new file mode 100644 +index 0000000000000000000000000000000000000000..30efa77d8ed337e9f044c41a10c6ecfd5ec162e2 +--- /dev/null ++++ b/src/main/java/com/logisticscraft/occlusionculling/OcclusionCullingInstance.java +@@ -0,0 +1,515 @@ ++package com.logisticscraft.occlusionculling; ++ ++import java.util.Arrays; ++import java.util.BitSet; ++ ++import com.logisticscraft.occlusionculling.cache.ArrayOcclusionCache; ++import com.logisticscraft.occlusionculling.cache.OcclusionCache; ++import com.logisticscraft.occlusionculling.util.MathUtilities; ++import com.logisticscraft.occlusionculling.util.Vec3d; ++ ++public class OcclusionCullingInstance { ++ ++ private static final int ON_MIN_X = 0x01; ++ private static final int ON_MAX_X = 0x02; ++ private static final int ON_MIN_Y = 0x04; ++ private static final int ON_MAX_Y = 0x08; ++ private static final int ON_MIN_Z = 0x10; ++ private static final int ON_MAX_Z = 0x20; ++ ++ private final int reach; ++ private final double aabbExpansion; ++ private final DataProvider provider; ++ private final OcclusionCache cache; ++ ++ // Reused allocated data structures ++ private final BitSet skipList = new BitSet(); // Grows bigger in case some mod introduces giant hitboxes ++ private final Vec3d[] targetPoints = new Vec3d[15]; ++ private final Vec3d targetPos = new Vec3d(0, 0, 0); ++ private final int[] cameraPos = new int[3]; ++ private final boolean[] dotselectors = new boolean[14]; ++ private boolean allowRayChecks = false; ++ private final int[] lastHitBlock = new int[3]; ++ private boolean allowWallClipping = false; ++ ++ ++ public OcclusionCullingInstance(int maxDistance, DataProvider provider) { ++ this(maxDistance, provider, new ArrayOcclusionCache(maxDistance), 0.5); ++ } ++ ++ public OcclusionCullingInstance(int maxDistance, DataProvider provider, OcclusionCache cache, double aabbExpansion) { ++ this.reach = maxDistance; ++ this.provider = provider; ++ this.cache = cache; ++ this.aabbExpansion = aabbExpansion; ++ for(int i = 0; i < targetPoints.length; i++) { ++ targetPoints[i] = new Vec3d(0, 0, 0); ++ } ++ } ++ ++ public boolean isAABBVisible(Vec3d aabbMin, Vec3d aabbMax, Vec3d viewerPosition) { ++ try { ++ int maxX = MathUtilities.floor(aabbMax.x ++ + aabbExpansion); ++ int maxY = MathUtilities.floor(aabbMax.y ++ + aabbExpansion); ++ int maxZ = MathUtilities.floor(aabbMax.z ++ + aabbExpansion); ++ int minX = MathUtilities.floor(aabbMin.x ++ - aabbExpansion); ++ int minY = MathUtilities.floor(aabbMin.y ++ - aabbExpansion); ++ int minZ = MathUtilities.floor(aabbMin.z ++ - aabbExpansion); ++ ++ cameraPos[0] = MathUtilities.floor(viewerPosition.x); ++ cameraPos[1] = MathUtilities.floor(viewerPosition.y); ++ cameraPos[2] = MathUtilities.floor(viewerPosition.z); ++ ++ Relative relX = Relative.from(minX, maxX, cameraPos[0]); ++ Relative relY = Relative.from(minY, maxY, cameraPos[1]); ++ Relative relZ = Relative.from(minZ, maxZ, cameraPos[2]); ++ ++ if(relX == Relative.INSIDE && relY == Relative.INSIDE && relZ == Relative.INSIDE) { ++ return true; // We are inside of the AABB, don't cull ++ } ++ ++ skipList.clear(); ++ ++ // Just check the cache first ++ int id = 0; ++ for (int x = minX; x <= maxX; x++) { ++ for (int y = minY; y <= maxY; y++) { ++ for (int z = minZ; z <= maxZ; z++) { ++ int cachedValue = getCacheValue(x, y, z); ++ ++ if (cachedValue == 1) { ++ // non-occluding ++ return true; ++ } ++ ++ if (cachedValue != 0) { ++ // was checked and it wasn't visible ++ skipList.set(id); ++ } ++ id++; ++ } ++ } ++ } ++ ++ // only after the first hit wall the cache becomes valid. ++ allowRayChecks = false; ++ ++ // since the cache wasn't helpfull ++ id = 0; ++ for (int x = minX; x <= maxX; x++) { ++ byte visibleOnFaceX = 0; ++ byte faceEdgeDataX = 0; ++ faceEdgeDataX |= (x == minX) ? ON_MIN_X : 0; ++ faceEdgeDataX |= (x == maxX) ? ON_MAX_X : 0; ++ visibleOnFaceX |= (x == minX && relX == Relative.POSITIVE) ? ON_MIN_X : 0; ++ visibleOnFaceX |= (x == maxX && relX == Relative.NEGATIVE) ? ON_MAX_X : 0; ++ for (int y = minY; y <= maxY; y++) { ++ byte faceEdgeDataY = faceEdgeDataX; ++ byte visibleOnFaceY = visibleOnFaceX; ++ faceEdgeDataY |= (y == minY) ? ON_MIN_Y : 0; ++ faceEdgeDataY |= (y == maxY) ? ON_MAX_Y : 0; ++ visibleOnFaceY |= (y == minY && relY == Relative.POSITIVE) ? ON_MIN_Y : 0; ++ visibleOnFaceY |= (y == maxY && relY == Relative.NEGATIVE) ? ON_MAX_Y : 0; ++ for (int z = minZ; z <= maxZ; z++) { ++ byte faceEdgeData = faceEdgeDataY; ++ byte visibleOnFace = visibleOnFaceY; ++ faceEdgeData |= (z == minZ) ? ON_MIN_Z : 0; ++ faceEdgeData |= (z == maxZ) ? ON_MAX_Z : 0; ++ visibleOnFace |= (z == minZ && relZ == Relative.POSITIVE) ? ON_MIN_Z : 0; ++ visibleOnFace |= (z == maxZ && relZ == Relative.NEGATIVE) ? ON_MAX_Z : 0; ++ if(skipList.get(id)) { // was checked and it wasn't visible ++ id++; ++ continue; ++ } ++ ++ if (visibleOnFace != 0) { ++ targetPos.set(x, y, z); ++ if (isVoxelVisible(viewerPosition, targetPos, faceEdgeData, visibleOnFace)) { ++ return true; ++ } ++ } ++ id++; ++ } ++ } ++ } ++ ++ return false; ++ } catch (Throwable t) { ++ // Failsafe ++ t.printStackTrace(); ++ } ++ return true; ++ } ++ ++ /** ++ * @param viewerPosition ++ * @param position ++ * @param faceData contains rather this Block is on the outside for a given face ++ * @param visibleOnFace contains rather a face should be concidered ++ * @return ++ */ ++ private boolean isVoxelVisible(Vec3d viewerPosition, Vec3d position, byte faceData, byte visibleOnFace) { ++ int targetSize = 0; ++ Arrays.fill(dotselectors, false); ++ if((visibleOnFace & ON_MIN_X) == ON_MIN_X){ ++ dotselectors[0] = true; ++ if((faceData & ~ON_MIN_X) != 0) { ++ dotselectors[1] = true; ++ dotselectors[4] = true; ++ dotselectors[5] = true; ++ } ++ dotselectors[8] = true; ++ } ++ if((visibleOnFace & ON_MIN_Y) == ON_MIN_Y){ ++ dotselectors[0] = true; ++ if((faceData & ~ON_MIN_Y) != 0) { ++ dotselectors[3] = true; ++ dotselectors[4] = true; ++ dotselectors[7] = true; ++ } ++ dotselectors[9] = true; ++ } ++ if((visibleOnFace & ON_MIN_Z) == ON_MIN_Z){ ++ dotselectors[0] = true; ++ if((faceData & ~ON_MIN_Z) != 0) { ++ dotselectors[1] = true; ++ dotselectors[4] = true; ++ dotselectors[5] = true; ++ } ++ dotselectors[10] = true; ++ } ++ if((visibleOnFace & ON_MAX_X) == ON_MAX_X){ ++ dotselectors[4] = true; ++ if((faceData & ~ON_MAX_X) != 0) { ++ dotselectors[5] = true; ++ dotselectors[6] = true; ++ dotselectors[7] = true; ++ } ++ dotselectors[11] = true; ++ } ++ if((visibleOnFace & ON_MAX_Y) == ON_MAX_Y){ ++ dotselectors[1] = true; ++ if((faceData & ~ON_MAX_Y) != 0) { ++ dotselectors[2] = true; ++ dotselectors[5] = true; ++ dotselectors[6] = true; ++ } ++ dotselectors[12] = true; ++ } ++ if((visibleOnFace & ON_MAX_Z) == ON_MAX_Z){ ++ dotselectors[2] = true; ++ if((faceData & ~ON_MAX_Z) != 0) { ++ dotselectors[3] = true; ++ dotselectors[6] = true; ++ dotselectors[7] = true; ++ } ++ dotselectors[13] = true; ++ } ++ ++ if (dotselectors[0])targetPoints[targetSize++].setAdd(position, 0.05, 0.05, 0.05); ++ if (dotselectors[1])targetPoints[targetSize++].setAdd(position, 0.05, 0.95, 0.05); ++ if (dotselectors[2])targetPoints[targetSize++].setAdd(position, 0.05, 0.95, 0.95); ++ if (dotselectors[3])targetPoints[targetSize++].setAdd(position, 0.05, 0.05, 0.95); ++ if (dotselectors[4])targetPoints[targetSize++].setAdd(position, 0.95, 0.05, 0.05); ++ if (dotselectors[5])targetPoints[targetSize++].setAdd(position, 0.95, 0.95, 0.05); ++ if (dotselectors[6])targetPoints[targetSize++].setAdd(position, 0.95, 0.95, 0.95); ++ if (dotselectors[7])targetPoints[targetSize++].setAdd(position, 0.95, 0.05, 0.95); ++ // middle points ++ if (dotselectors[8])targetPoints[targetSize++].setAdd(position, 0.05, 0.5, 0.5); ++ if (dotselectors[9])targetPoints[targetSize++].setAdd(position, 0.5, 0.05, 0.5); ++ if (dotselectors[10])targetPoints[targetSize++].setAdd(position, 0.5, 0.5, 0.05); ++ if (dotselectors[11])targetPoints[targetSize++].setAdd(position, 0.95, 0.5, 0.5); ++ if (dotselectors[12])targetPoints[targetSize++].setAdd(position, 0.5, 0.95, 0.5); ++ if (dotselectors[13])targetPoints[targetSize++].setAdd(position, 0.5, 0.5, 0.95); ++ ++ return isVisible(viewerPosition, targetPoints, targetSize); ++ } ++ ++ private boolean rayIntersection(int[] b, Vec3d rayOrigin, Vec3d rayDir) { ++ Vec3d rInv = new Vec3d(1, 1, 1).div(rayDir); ++ ++ double t1 = (b[0] - rayOrigin.x) * rInv.x; ++ double t2 = (b[0] + 1 - rayOrigin.x) * rInv.x; ++ double t3 = (b[1] - rayOrigin.y) * rInv.y; ++ double t4 = (b[1] + 1 - rayOrigin.y) * rInv.y; ++ double t5 = (b[2] - rayOrigin.z) * rInv.z; ++ double t6 = (b[2] + 1 - rayOrigin.z) * rInv.z; ++ ++ double tmin = Math.max(Math.max(Math.min(t1, t2), Math.min(t3, t4)), Math.min(t5, t6)); ++ double tmax = Math.min(Math.min(Math.max(t1, t2), Math.max(t3, t4)), Math.max(t5, t6)); ++ ++ // if tmax > 0, ray (line) is intersecting AABB, but the whole AABB is behind us ++ if (tmax > 0) { ++ return false; ++ } ++ ++ // if tmin > tmax, ray doesn't intersect AABB ++ if (tmin > tmax) { ++ return false; ++ } ++ ++ return true; ++ } ++ ++ /** ++ * returns the grid cells that intersect with this Vec3d
++ * http://playtechs.blogspot.de/2007/03/raytracing-on-grid.html ++ *

++ * Caching assumes that all Vec3d's are inside the same block ++ */ ++ private boolean isVisible(Vec3d start, Vec3d[] targets, int size) { ++ // start cell coordinate ++ int x = cameraPos[0]; ++ int y = cameraPos[1]; ++ int z = cameraPos[2]; ++ ++ for (int v = 0; v < size; v++) { ++ // ray-casting target ++ Vec3d target = targets[v]; ++ ++ double relativeX = start.x - target.getX(); ++ double relativeY = start.y - target.getY(); ++ double relativeZ = start.z - target.getZ(); ++ ++ if(allowRayChecks && rayIntersection(lastHitBlock, start, new Vec3d(relativeX, relativeY, relativeZ).normalize())) { ++ continue; ++ } ++ ++ // horizontal and vertical cell amount spanned ++ double dimensionX = Math.abs(relativeX); ++ double dimensionY = Math.abs(relativeY); ++ double dimensionZ = Math.abs(relativeZ); ++ ++ // distance between horizontal intersection points with cell border as a ++ // fraction of the total Vec3d length ++ double dimFracX = 1f / dimensionX; ++ // distance between vertical intersection points with cell border as a fraction ++ // of the total Vec3d length ++ double dimFracY = 1f / dimensionY; ++ double dimFracZ = 1f / dimensionZ; ++ ++ // total amount of intersected cells ++ int intersectCount = 1; ++ ++ // 1, 0 or -1 ++ // determines the direction of the next cell (horizontally / vertically) ++ int x_inc, y_inc, z_inc; ++ ++ // the distance to the next horizontal / vertical intersection point with a cell ++ // border as a fraction of the total Vec3d length ++ double t_next_y, t_next_x, t_next_z; ++ ++ if (dimensionX == 0f) { ++ x_inc = 0; ++ t_next_x = dimFracX; // don't increment horizontally because the Vec3d is perfectly vertical ++ } else if (target.x > start.x) { ++ x_inc = 1; // target point is horizontally greater than starting point so increment every ++ // step by 1 ++ intersectCount += MathUtilities.floor(target.x) - x; // increment total amount of intersecting cells ++ t_next_x = (float) ((x + 1 - start.x) * dimFracX); // calculate the next horizontal ++ // intersection ++ // point based on the position inside ++ // the first cell ++ } else { ++ x_inc = -1; // target point is horizontally smaller than starting point so reduce every step ++ // by 1 ++ intersectCount += x - MathUtilities.floor(target.x); // increment total amount of intersecting cells ++ t_next_x = (float) ((start.x - x) ++ * dimFracX); // calculate the next horizontal ++ // intersection point ++ // based on the position inside ++ // the first cell ++ } ++ ++ if (dimensionY == 0f) { ++ y_inc = 0; ++ t_next_y = dimFracY; // don't increment vertically because the Vec3d is perfectly horizontal ++ } else if (target.y > start.y) { ++ y_inc = 1; // target point is vertically greater than starting point so increment every ++ // step by 1 ++ intersectCount += MathUtilities.floor(target.y) - y; // increment total amount of intersecting cells ++ t_next_y = (float) ((y + 1 - start.y) ++ * dimFracY); // calculate the next vertical ++ // intersection ++ // point based on the position inside ++ // the first cell ++ } else { ++ y_inc = -1; // target point is vertically smaller than starting point so reduce every step ++ // by 1 ++ intersectCount += y - MathUtilities.floor(target.y); // increment total amount of intersecting cells ++ t_next_y = (float) ((start.y - y) ++ * dimFracY); // calculate the next vertical intersection ++ // point ++ // based on the position inside ++ // the first cell ++ } ++ ++ if (dimensionZ == 0f) { ++ z_inc = 0; ++ t_next_z = dimFracZ; // don't increment vertically because the Vec3d is perfectly horizontal ++ } else if (target.z > start.z) { ++ z_inc = 1; // target point is vertically greater than starting point so increment every ++ // step by 1 ++ intersectCount += MathUtilities.floor(target.z) - z; // increment total amount of intersecting cells ++ t_next_z = (float) ((z + 1 - start.z) ++ * dimFracZ); // calculate the next vertical ++ // intersection ++ // point based on the position inside ++ // the first cell ++ } else { ++ z_inc = -1; // target point is vertically smaller than starting point so reduce every step ++ // by 1 ++ intersectCount += z - MathUtilities.floor(target.z); // increment total amount of intersecting cells ++ t_next_z = (float) ((start.z - z) ++ * dimFracZ); // calculate the next vertical intersection ++ // point ++ // based on the position inside ++ // the first cell ++ } ++ ++ boolean finished = stepRay(start, x, y, z, ++ dimFracX, dimFracY, dimFracZ, intersectCount, x_inc, y_inc, ++ z_inc, t_next_y, t_next_x, t_next_z); ++ provider.cleanup(); ++ if (finished) { ++ cacheResult(targets[0], true); ++ return true; ++ } else { ++ allowRayChecks = true; ++ } ++ } ++ cacheResult(targets[0], false); ++ return false; ++ } ++ ++ private boolean stepRay(Vec3d start, int currentX, int currentY, ++ int currentZ, double distInX, double distInY, ++ double distInZ, int n, int x_inc, int y_inc, ++ int z_inc, double t_next_y, double t_next_x, ++ double t_next_z) { ++ allowWallClipping = true; // initially allow rays to go through walls till they are on the outside ++ // iterate through all intersecting cells (n times) ++ for (; n > 1; n--) { // n-1 times because we don't want to check the last block ++ // towards - where from ++ ++ ++ // get cached value, 0 means uncached (default) ++ int cVal = getCacheValue(currentX, currentY, currentZ); ++ ++ if (cVal == 2 && !allowWallClipping) { ++ // block cached as occluding, stop ray ++ lastHitBlock[0] = currentX; ++ lastHitBlock[1] = currentY; ++ lastHitBlock[2] = currentZ; ++ return false; ++ } ++ ++ if (cVal == 0) { ++ // save current cell ++ int chunkX = currentX >> 4; ++ int chunkZ = currentZ >> 4; ++ ++ if (!provider.prepareChunk(chunkX, chunkZ)) { // Chunk not ready ++ return false; ++ } ++ ++ if (provider.isOpaqueFullCube(currentX, currentY, currentZ)) { ++ if (!allowWallClipping) { ++ cache.setLastHidden(); ++ lastHitBlock[0] = currentX; ++ lastHitBlock[1] = currentY; ++ lastHitBlock[2] = currentZ; ++ return false; ++ } ++ } else { ++ // outside of wall, now clipping is not allowed ++ allowWallClipping = false; ++ cache.setLastVisible(); ++ } ++ } ++ ++ if(cVal == 1) { ++ // outside of wall, now clipping is not allowed ++ allowWallClipping = false; ++ } ++ ++ ++ if (t_next_y < t_next_x && t_next_y < t_next_z) { // next cell is upwards/downwards because the distance to ++ // the next vertical ++ // intersection point is smaller than to the next horizontal intersection point ++ currentY += y_inc; // move up/down ++ t_next_y += distInY; // update next vertical intersection point ++ } else if (t_next_x < t_next_y && t_next_x < t_next_z) { // next cell is right/left ++ currentX += x_inc; // move right/left ++ t_next_x += distInX; // update next horizontal intersection point ++ } else { ++ currentZ += z_inc; // move right/left ++ t_next_z += distInZ; // update next horizontal intersection point ++ } ++ ++ } ++ return true; ++ } ++ ++ // -1 = invalid location, 0 = not checked yet, 1 = visible, 2 = occluding ++ private int getCacheValue(int x, int y, int z) { ++ x -= cameraPos[0]; ++ y -= cameraPos[1]; ++ z -= cameraPos[2]; ++ if (Math.abs(x) > reach - 2 || Math.abs(y) > reach - 2 ++ || Math.abs(z) > reach - 2) { ++ return -1; ++ } ++ ++ // check if target is already known ++ return cache.getState(x + reach, y + reach, z + reach); ++ } ++ ++ ++ private void cacheResult(int x, int y, int z, boolean result) { ++ int cx = x - cameraPos[0] + reach; ++ int cy = y - cameraPos[1] + reach; ++ int cz = z - cameraPos[2] + reach; ++ if (result) { ++ cache.setVisible(cx, cy, cz); ++ } else { ++ cache.setHidden(cx, cy, cz); ++ } ++ } ++ ++ private void cacheResult(Vec3d vector, boolean result) { ++ int cx = MathUtilities.floor(vector.x) - cameraPos[0] + reach; ++ int cy = MathUtilities.floor(vector.y) - cameraPos[1] + reach; ++ int cz = MathUtilities.floor(vector.z) - cameraPos[2] + reach; ++ if (result) { ++ cache.setVisible(cx, cy, cz); ++ } else { ++ cache.setHidden(cx, cy, cz); ++ } ++ } ++ ++ public void resetCache() { ++ this.cache.resetCache(); ++ } ++ ++ private enum Relative { ++ INSIDE, POSITIVE, NEGATIVE; ++ ++ public static Relative from(int min, int max, int pos) { ++ if (max > pos && min > pos) { ++ return POSITIVE; ++ } else if (min < pos && max < pos) { ++ return NEGATIVE; ++ } ++ return INSIDE; ++ } ++ } ++ ++} +diff --git a/src/main/java/com/logisticscraft/occlusionculling/cache/ArrayOcclusionCache.java b/src/main/java/com/logisticscraft/occlusionculling/cache/ArrayOcclusionCache.java +new file mode 100644 +index 0000000000000000000000000000000000000000..08f0ddb343c12ac781ee33f1a7da1462573405b9 +--- /dev/null ++++ b/src/main/java/com/logisticscraft/occlusionculling/cache/ArrayOcclusionCache.java +@@ -0,0 +1,57 @@ ++package com.logisticscraft.occlusionculling.cache; ++ ++import java.util.Arrays; ++ ++public class ArrayOcclusionCache implements OcclusionCache { ++ ++ private final int reachX2; ++ private final byte[] cache; ++ private int positionKey; ++ private int entry; ++ private int offset; ++ ++ public ArrayOcclusionCache(int reach) { ++ this.reachX2 = reach * 2; ++ this.cache = new byte[(reachX2 * reachX2 * reachX2) / 4]; ++ } ++ ++ @Override ++ public void resetCache() { ++ Arrays.fill(cache, (byte) 0); ++ } ++ ++ @Override ++ public void setVisible(int x, int y, int z) { ++ positionKey = x + y * reachX2 + z * reachX2 * reachX2; ++ entry = positionKey / 4; ++ offset = (positionKey % 4) * 2; ++ cache[entry] |= 1 << offset; ++ } ++ ++ @Override ++ public void setHidden(int x, int y, int z) { ++ positionKey = x + y * reachX2 + z * reachX2 * reachX2; ++ entry = positionKey / 4; ++ offset = (positionKey % 4) * 2; ++ cache[entry] |= 1 << offset + 1; ++ } ++ ++ @Override ++ public int getState(int x, int y, int z) { ++ positionKey = x + y * reachX2 + z * reachX2 * reachX2; ++ entry = positionKey / 4; ++ offset = (positionKey % 4) * 2; ++ return cache[entry] >> offset & 3; ++ } ++ ++ @Override ++ public void setLastVisible() { ++ cache[entry] |= 1 << offset; ++ } ++ ++ @Override ++ public void setLastHidden() { ++ cache[entry] |= 1 << offset + 1; ++ } ++ ++} +diff --git a/src/main/java/com/logisticscraft/occlusionculling/cache/OcclusionCache.java b/src/main/java/com/logisticscraft/occlusionculling/cache/OcclusionCache.java +new file mode 100644 +index 0000000000000000000000000000000000000000..2a2e5620fb2b1a71fbca2068fc049bfa89a47737 +--- /dev/null ++++ b/src/main/java/com/logisticscraft/occlusionculling/cache/OcclusionCache.java +@@ -0,0 +1,17 @@ ++package com.logisticscraft.occlusionculling.cache; ++ ++public interface OcclusionCache { ++ ++ void resetCache(); ++ ++ void setVisible(int x, int y, int z); ++ ++ void setHidden(int x, int y, int z); ++ ++ int getState(int x, int y, int z); ++ ++ void setLastHidden(); ++ ++ void setLastVisible(); ++ ++} +diff --git a/src/main/java/com/logisticscraft/occlusionculling/util/MathUtilities.java b/src/main/java/com/logisticscraft/occlusionculling/util/MathUtilities.java +new file mode 100644 +index 0000000000000000000000000000000000000000..fb28265631b079eb5da8ae3a7ecca6e343f8c0b6 +--- /dev/null ++++ b/src/main/java/com/logisticscraft/occlusionculling/util/MathUtilities.java +@@ -0,0 +1,25 @@ ++package com.logisticscraft.occlusionculling.util; ++ ++/** ++ * Contains MathHelper methods ++ */ ++public final class MathUtilities { ++ ++ private MathUtilities() { ++ } ++ ++ public static int floor(double d) { ++ int i = (int) d; ++ return d < (double) i ? i - 1 : i; ++ } ++ ++ public static int fastFloor(double d) { ++ return (int) (d + 1024.0) - 1024; ++ } ++ ++ public static int ceil(double d) { ++ int i = (int) d; ++ return d > (double) i ? i + 1 : i; ++ } ++ ++} +diff --git a/src/main/java/com/logisticscraft/occlusionculling/util/Vec3d.java b/src/main/java/com/logisticscraft/occlusionculling/util/Vec3d.java +new file mode 100644 +index 0000000000000000000000000000000000000000..7e3cb9e11f65af0627164c8dbd6aad3e5690d36c +--- /dev/null ++++ b/src/main/java/com/logisticscraft/occlusionculling/util/Vec3d.java +@@ -0,0 +1,87 @@ ++package com.logisticscraft.occlusionculling.util; ++ ++public class Vec3d { ++ ++ public double x; ++ public double y; ++ public double z; ++ ++ public Vec3d(double x, double y, double z) { ++ this.x = x; ++ this.y = y; ++ this.z = z; ++ } ++ ++ public double getX() { ++ return x; ++ } ++ ++ public double getY() { ++ return y; ++ } ++ ++ public double getZ() { ++ return z; ++ } ++ ++ public void set(double x, double y, double z) { ++ this.x = x; ++ this.y = y; ++ this.z = z; ++ } ++ ++ public void setAdd(Vec3d vec, double x, double y, double z) { ++ this.x = vec.x + x; ++ this.y = vec.y + y; ++ this.z = vec.z + z; ++ } ++ ++ public Vec3d div(Vec3d rayDir) { ++ this.x /= rayDir.x; ++ this.z /= rayDir.z; ++ this.y /= rayDir.y; ++ return this; ++ } ++ ++ public Vec3d normalize() { ++ double mag = Math.sqrt(x*x+y*y+z*z); ++ this.x /= mag; ++ this.y /= mag; ++ this.z /= mag; ++ return this; ++ } ++ ++ public boolean equals(Object other) { ++ if (this == other) { ++ return true; ++ } ++ if (!(other instanceof Vec3d)) { ++ return false; ++ } ++ Vec3d vec3d = (Vec3d) other; ++ if (Double.compare(vec3d.x, x) != 0) { ++ return false; ++ } ++ if (Double.compare(vec3d.y, y) != 0) { ++ return false; ++ } ++ return Double.compare(vec3d.z, z) == 0; ++ } ++ ++ @Override ++ public int hashCode() { ++ long l = Double.doubleToLongBits(x); ++ int i = (int) (l ^ l >>> 32); ++ l = Double.doubleToLongBits(y); ++ i = 31 * i + (int) (l ^ l >>> 32); ++ l = Double.doubleToLongBits(z); ++ i = 31 * i + (int) (l ^ l >>> 32); ++ return i; ++ } ++ ++ @Override ++ public String toString() { ++ return "(" + x + ", " + y + ", " + z + ")"; ++ } ++ ++} +diff --git a/src/main/java/dev/tr7zw/entityculling/CullTask.java b/src/main/java/dev/tr7zw/entityculling/CullTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..57e36d4a2c770a3d2ae72a78e69802dd119e6999 +--- /dev/null ++++ b/src/main/java/dev/tr7zw/entityculling/CullTask.java +@@ -0,0 +1,152 @@ ++package dev.tr7zw.entityculling; ++ ++import java.util.concurrent.CompletableFuture; ++import java.util.concurrent.Executor; ++import java.util.concurrent.Executors; ++import java.util.concurrent.TimeUnit; ++ ++import ca.spottedleaf.moonrise.common.util.TickThread; ++import com.logisticscraft.occlusionculling.OcclusionCullingInstance; ++import com.logisticscraft.occlusionculling.util.Vec3d; ++ ++import dev.tr7zw.entityculling.versionless.access.Cullable; ++import me.earthme.luminol.config.modules.experiment.RayTrackingEntityTrackerConfig; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.entity.decoration.ArmorStand; ++import net.minecraft.world.entity.player.Player; ++import net.minecraft.world.phys.AABB; ++import net.minecraft.world.phys.Vec3; ++ ++public class CullTask implements Runnable { ++ ++ private volatile boolean requestCull = false; ++ private volatile boolean scheduleNext = true; ++ private volatile boolean inited = false; ++ ++ private final OcclusionCullingInstance culling; ++ private final Player checkTarget; ++ ++ private final int hitboxLimit; ++ ++ public long lastCheckedTime = 0; ++ ++ // reused preallocated vars ++ private final Vec3d lastPos = new Vec3d(0, 0, 0); ++ private final Vec3d aabbMin = new Vec3d(0, 0, 0); ++ private final Vec3d aabbMax = new Vec3d(0, 0, 0); ++ ++ private static final Executor backgroundWorker = Executors.newCachedThreadPool(task -> { ++ final TickThread worker = new TickThread("EntityCulling") { ++ @Override ++ public void run() { ++ task.run(); ++ } ++ }; ++ ++ worker.setDaemon(true); ++ ++ return worker; ++ }); ++ ++ private final Executor worker; ++ ++ public CullTask( ++ OcclusionCullingInstance culling, ++ Player checkTarget, ++ int hitboxLimit, ++ long checkIntervalMs ++ ) { ++ this.culling = culling; ++ this.checkTarget = checkTarget; ++ this.hitboxLimit = hitboxLimit; ++ this.worker = CompletableFuture.delayedExecutor(checkIntervalMs, TimeUnit.MILLISECONDS, backgroundWorker); ++ } ++ ++ public void requestCullSignal() { ++ this.requestCull = true; ++ } ++ ++ public void signalStop() { ++ this.scheduleNext = false; ++ } ++ ++ public void setup() { ++ if (!this.inited) ++ this.inited = true; ++ else ++ return; ++ this.worker.execute(this); ++ } ++ ++ @Override ++ public void run() { ++ try { ++ if (this.checkTarget.tickCount > 10) { ++ // getEyePosition can use a fixed delta as its debug only anyway ++ Vec3 cameraMC = this.checkTarget.getEyePosition(0); ++ if (requestCull || !(cameraMC.x == lastPos.x && cameraMC.y == lastPos.y && cameraMC.z == lastPos.z)) { ++ long start = System.currentTimeMillis(); ++ ++ requestCull = false; ++ ++ lastPos.set(cameraMC.x, cameraMC.y, cameraMC.z); ++ culling.resetCache(); ++ ++ cullEntities(cameraMC, lastPos); ++ ++ lastCheckedTime = (System.currentTimeMillis() - start); ++ } ++ } ++ }finally { ++ if (this.scheduleNext) { ++ this.worker.execute(this); ++ } ++ } ++ } ++ ++ private void cullEntities(Vec3 cameraMC, Vec3d camera) { ++ for (Entity entity : this.checkTarget.level().getEntities().getAll()) { ++ if (!(entity instanceof Cullable cullable)) { ++ continue; // Not sure how this could happen outside from mixin screwing up the inject into ++ // Entity ++ } ++ ++ if (entity.getType().skipRaytracningCheck) { ++ continue; ++ } ++ ++ if (!cullable.isForcedVisible()) { ++ if (entity.isCurrentlyGlowing() || isSkippableArmorstand(entity)) { ++ cullable.setCulled(false); ++ continue; ++ } ++ ++ if (!entity.position().closerThan(cameraMC, me.earthme.luminol.config.modules.experiment.RayTrackingEntityTrackerConfig.tracingDistance)) { ++ cullable.setCulled(false); // If your entity view distance is larger than tracingDistance just ++ // render it ++ continue; ++ } ++ ++ AABB boundingBox = entity.getBoundingBox(); ++ if (boundingBox.getXsize() > hitboxLimit || boundingBox.getYsize() > hitboxLimit ++ || boundingBox.getZsize() > hitboxLimit) { ++ cullable.setCulled(false); // To big to bother to cull ++ continue; ++ } ++ ++ aabbMin.set(boundingBox.minX, boundingBox.minY, boundingBox.minZ); ++ aabbMax.set(boundingBox.maxX, boundingBox.maxY, boundingBox.maxZ); ++ ++ boolean visible = culling.isAABBVisible(aabbMin, aabbMax, camera); ++ ++ cullable.setCulled(!visible); ++ } ++ } ++ } ++ ++ private boolean isSkippableArmorstand(Entity entity) { ++ if (!RayTrackingEntityTrackerConfig.skipMarkerArmorStands) ++ return false; ++ return entity instanceof ArmorStand && entity.isInvisible(); ++ } ++} +diff --git a/src/main/java/dev/tr7zw/entityculling/DefaultChunkDataProvider.java b/src/main/java/dev/tr7zw/entityculling/DefaultChunkDataProvider.java +new file mode 100644 +index 0000000000000000000000000000000000000000..aadf7cdc4a28ecf92853e147fa115d43c89f6fe8 +--- /dev/null ++++ b/src/main/java/dev/tr7zw/entityculling/DefaultChunkDataProvider.java +@@ -0,0 +1,43 @@ ++package dev.tr7zw.entityculling; ++ ++import com.logisticscraft.occlusionculling.DataProvider; ++import net.minecraft.core.BlockPos; ++import net.minecraft.world.level.Level; ++import net.minecraft.world.level.block.Blocks; ++import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.status.ChunkStatus; ++ ++public class DefaultChunkDataProvider implements DataProvider { ++ private final Level level; ++ ++ public DefaultChunkDataProvider(Level level) { ++ this.level = level; ++ } ++ ++ @Override ++ public boolean prepareChunk(int chunkX, int chunkZ) { ++ return this.level.getChunkIfLoaded(chunkX, chunkZ) != null; ++ } ++ ++ @Override ++ public boolean isOpaqueFullCube(int x, int y, int z) { ++ BlockPos pos = new BlockPos(x, y, z); ++ ++ final ChunkAccess access = this.level.getChunkIfLoaded(pos); ++ if (access == null) { ++ return false; ++ } ++ ++ if (this.level.isOutsideBuildHeight(pos)) { ++ return Blocks.VOID_AIR.defaultBlockState().isSolidRender(); ++ } else { ++ return access.getBlockState(pos).isSolidRender();// 好孩子不要学坏叔叔这样绕过异步拦截() ++ } ++ } ++ ++ @Override ++ public void cleanup() { ++ DataProvider.super.cleanup(); ++ } ++ ++} +diff --git a/src/main/java/dev/tr7zw/entityculling/versionless/access/Cullable.java b/src/main/java/dev/tr7zw/entityculling/versionless/access/Cullable.java +new file mode 100644 +index 0000000000000000000000000000000000000000..948e8e934793532704161e0a129ebf86355ba5a8 +--- /dev/null ++++ b/src/main/java/dev/tr7zw/entityculling/versionless/access/Cullable.java +@@ -0,0 +1,17 @@ ++package dev.tr7zw.entityculling.versionless.access; ++ ++public interface Cullable { ++ ++ public void setTimeout(); ++ ++ public boolean isForcedVisible(); ++ ++ public void setCulled(boolean value); ++ ++ public boolean isCulled(); ++ ++ public void setOutOfCamera(boolean value); ++ ++ public boolean isOutOfCamera(); ++ ++} +diff --git a/src/main/java/me/earthme/luminol/config/modules/experiment/RayTrackingEntityTrackerConfig.java b/src/main/java/me/earthme/luminol/config/modules/experiment/RayTrackingEntityTrackerConfig.java +new file mode 100644 +index 0000000000000000000000000000000000000000..1e3e6a500c9eb26a06f5441f9e542dc9b1ad0cf4 +--- /dev/null ++++ b/src/main/java/me/earthme/luminol/config/modules/experiment/RayTrackingEntityTrackerConfig.java +@@ -0,0 +1,28 @@ ++package me.earthme.luminol.config.modules.experiment; ++ ++import me.earthme.luminol.config.ConfigInfo; ++import me.earthme.luminol.config.EnumConfigCategory; ++import me.earthme.luminol.config.IConfigModule; ++ ++public class RayTrackingEntityTrackerConfig implements IConfigModule { ++ @ConfigInfo(baseName = "enabled") ++ public static boolean enabled = false; ++ @ConfigInfo(baseName = "skip_marker_armor_stands") ++ public static boolean skipMarkerArmorStands = true; ++ @ConfigInfo(baseName = "check_interval_ms") ++ public static int checkIntervalMs = 10; ++ @ConfigInfo(baseName = "tracing_distance") ++ public static int tracingDistance = 48; ++ @ConfigInfo(baseName = "hitbox_limit") ++ public static int hitboxLimit = 50; ++ ++ @Override ++ public EnumConfigCategory getCategory() { ++ return EnumConfigCategory.EXPERIMENT; ++ } ++ ++ @Override ++ public String getBaseName() { ++ return "ray_tracking_entity_tracker"; ++ } ++}