9
0
mirror of https://github.com/Xiao-MoMi/craft-engine.git synced 2025-12-20 15:39:22 +00:00

初步剔除

This commit is contained in:
XiaoMoMi
2025-11-28 22:06:06 +08:00
parent f460784460
commit 7eb4dc1ff8
30 changed files with 584 additions and 452 deletions

View File

@@ -42,7 +42,7 @@ public class MythicItemDrop extends ItemDrop implements IItemDrop {
context = ItemBuildContext.of(player); context = ItemBuildContext.of(player);
} }
} }
int amountInt = MiscUtils.fastFloor(amount + 0.5F); int amountInt = MiscUtils.floor(amount + 0.5F);
ItemStack itemStack = this.customItem.buildItemStack(context, amountInt); ItemStack itemStack = this.customItem.buildItemStack(context, amountInt);
return adapt(itemStack).amount(amountInt); return adapt(itemStack).amount(amountInt);
} }

View File

@@ -2075,6 +2075,7 @@ public class BukkitNetworkManager implements NetworkManager, Listener, PluginMes
// 两种情况都有,那么需要一个个遍历处理视线遮挡数据 // 两种情况都有,那么需要一个个遍历处理视线遮挡数据
if (hasOcclusions && hasNoOcclusions) { if (hasOcclusions && hasNoOcclusions) {
PackedOcclusionStorage storage = new PackedOcclusionStorage(false); PackedOcclusionStorage storage = new PackedOcclusionStorage(false);
clientSections[i] = new ClientSection(storage);
for (int j = 0; j < 4096; j++) { for (int j = 0; j < 4096; j++) {
int state = container.get(j); int state = container.get(j);
storage.set(j, this.occlusionPredicate.test(state)); storage.set(j, this.occlusionPredicate.test(state));
@@ -2092,6 +2093,7 @@ public class BukkitNetworkManager implements NetworkManager, Listener, PluginMes
PackedOcclusionStorage storage = null; PackedOcclusionStorage storage = null;
if (clientSections != null) { if (clientSections != null) {
storage = new PackedOcclusionStorage(false); storage = new PackedOcclusionStorage(false);
clientSections[i] = new ClientSection(storage);
} }
for (int j = 0; j < 4096; j++) { for (int j = 0; j < 4096; j++) {
@@ -2258,6 +2260,12 @@ public class BukkitNetworkManager implements NetworkManager, Listener, PluginMes
FriendlyByteBuf buf = event.getBuffer(); FriendlyByteBuf buf = event.getBuffer();
BlockPos pos = buf.readBlockPos(); BlockPos pos = buf.readBlockPos();
int before = buf.readVarInt(); int before = buf.readVarInt();
if (Config.enableEntityCulling()) {
ClientChunk trackedChunk = user.getTrackedChunk(ChunkPos.asLong(pos.x >> 4, pos.z >> 4));
if (trackedChunk != null) {
trackedChunk.setOccluding(pos.x, pos.y, pos.z, this.occlusionPredicate.test(before));
}
}
if (user.clientModEnabled() && !BlockStateUtils.isVanillaBlock(before)) { if (user.clientModEnabled() && !BlockStateUtils.isVanillaBlock(before)) {
return; return;
} }
@@ -2270,12 +2278,6 @@ public class BukkitNetworkManager implements NetworkManager, Listener, PluginMes
buf.writeVarInt(event.packetID()); buf.writeVarInt(event.packetID());
buf.writeBlockPos(pos); buf.writeBlockPos(pos);
buf.writeVarInt(state); buf.writeVarInt(state);
if (Config.enableEntityCulling()) {
ClientChunk trackedChunk = user.getTrackedChunk(ChunkPos.asLong(pos.x >> 4, pos.z >> 4));
if (trackedChunk != null) {
trackedChunk.setOccluding(pos.x, pos.y, pos.z, this.occlusionPredicate.test(state));
}
}
} }
} }
@@ -2437,6 +2439,13 @@ public class BukkitNetworkManager implements NetworkManager, Listener, PluginMes
if (eventId != WorldEvents.BLOCK_BREAK_EFFECT) return; if (eventId != WorldEvents.BLOCK_BREAK_EFFECT) return;
BlockPos blockPos = buf.readBlockPos(); BlockPos blockPos = buf.readBlockPos();
int state = buf.readInt(); int state = buf.readInt();
// 移除不透明设置
if (Config.enableEntityCulling()) {
ClientChunk trackedChunk = user.getTrackedChunk(ChunkPos.asLong(blockPos.x >> 4, blockPos.z >> 4));
if (trackedChunk != null) {
trackedChunk.setOccluding(blockPos.x, blockPos.y, blockPos.z, false);
}
}
boolean global = buf.readBoolean(); boolean global = buf.readBoolean();
int newState = user.clientModEnabled() ? modBlockStateMapper[state] : blockStateMapper[state]; int newState = user.clientModEnabled() ? modBlockStateMapper[state] : blockStateMapper[state];
Object blockState = BlockStateUtils.idToBlockState(state); Object blockState = BlockStateUtils.idToBlockState(state);

View File

@@ -32,6 +32,7 @@ import net.momirealms.craftengine.core.item.Item;
import net.momirealms.craftengine.core.plugin.CraftEngine; import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.config.Config; import net.momirealms.craftengine.core.plugin.config.Config;
import net.momirealms.craftengine.core.plugin.context.CooldownData; import net.momirealms.craftengine.core.plugin.context.CooldownData;
import net.momirealms.craftengine.core.plugin.entityculling.EntityCulling;
import net.momirealms.craftengine.core.plugin.locale.TranslationManager; import net.momirealms.craftengine.core.plugin.locale.TranslationManager;
import net.momirealms.craftengine.core.plugin.network.ConnectionState; import net.momirealms.craftengine.core.plugin.network.ConnectionState;
import net.momirealms.craftengine.core.plugin.network.EntityPacketHandler; import net.momirealms.craftengine.core.plugin.network.EntityPacketHandler;
@@ -142,6 +143,8 @@ public class BukkitServerPlayer extends Player {
// 跟踪到的方块实体渲染器 // 跟踪到的方块实体渲染器
private final Map<BlockPos, VirtualCullableObject> trackedBlockEntityRenderers = new ConcurrentHashMap<>(); private final Map<BlockPos, VirtualCullableObject> trackedBlockEntityRenderers = new ConcurrentHashMap<>();
private final EntityCulling culling;
public BukkitServerPlayer(BukkitCraftEngine plugin, @Nullable Channel channel) { public BukkitServerPlayer(BukkitCraftEngine plugin, @Nullable Channel channel) {
this.channel = channel; this.channel = channel;
this.plugin = plugin; this.plugin = plugin;
@@ -154,6 +157,7 @@ public class BukkitServerPlayer extends Player {
} }
} }
} }
this.culling = new EntityCulling(this, 64, 0.5);
} }
public void setPlayer(org.bukkit.entity.Player player) { public void setPlayer(org.bukkit.entity.Player player) {
@@ -559,6 +563,15 @@ public class BukkitServerPlayer extends Player {
this.predictNextBlockToMine(); this.predictNextBlockToMine();
} }
} }
if (Config.enableEntityCulling()) {
long nano1 = System.nanoTime();
for (VirtualCullableObject cullableObject : this.trackedBlockEntityRenderers.values()) {
boolean visible = this.culling.isVisible(cullableObject.cullable.aabb(), LocationUtils.toVec3d(platformPlayer().getEyeLocation()));
cullableObject.setShown(this, visible);
}
long nano2 = System.nanoTime();
//CraftEngine.instance().logger().info("EntityCulling took " + (nano2 - nano1) / 1_000_000d + "ms");
}
} }
private void updateGUI() { private void updateGUI() {
@@ -1303,6 +1316,16 @@ public class BukkitServerPlayer extends Player {
} }
} }
@Override
public void addTrackedBlockEntity(BlockPos blockPos, ConstantBlockEntityRenderer renderer) {
this.trackedBlockEntityRenderers.put(blockPos, new VirtualCullableObject(renderer));
}
@Override
public VirtualCullableObject getTrackedBlockEntity(BlockPos blockPos) {
return this.trackedBlockEntityRenderers.get(blockPos);
}
@Override @Override
public void removeTrackedBlockEntities(Collection<BlockPos> renders) { public void removeTrackedBlockEntities(Collection<BlockPos> renders) {
for (BlockPos render : renders) { for (BlockPos render : renders) {

View File

@@ -60,7 +60,7 @@ public final class EntityUtils {
Object serverLevel = BukkitAdaptors.adapt(player.getWorld()).serverWorld(); Object serverLevel = BukkitAdaptors.adapt(player.getWorld()).serverWorld();
Object serverPlayer = FastNMS.INSTANCE.method$CraftPlayer$getHandle(player); Object serverPlayer = FastNMS.INSTANCE.method$CraftPlayer$getHandle(player);
for (Object pose : List.of(CoreReflections.instance$Pose$STANDING, CoreReflections.instance$Pose$CROUCHING, CoreReflections.instance$Pose$SWIMMING)) { for (Object pose : List.of(CoreReflections.instance$Pose$STANDING, CoreReflections.instance$Pose$CROUCHING, CoreReflections.instance$Pose$SWIMMING)) {
BlockPos pos = new BlockPos(MiscUtils.fastFloor(x), MiscUtils.fastFloor(y), MiscUtils.fastFloor(z)); BlockPos pos = new BlockPos(MiscUtils.floor(x), MiscUtils.floor(y), MiscUtils.floor(z));
try { try {
double floorHeight = (double) CoreReflections.method$BlockGetter$getBlockFloorHeight.invoke(serverLevel, LocationUtils.toBlockPos(pos)); double floorHeight = (double) CoreReflections.method$BlockGetter$getBlockFloorHeight.invoke(serverLevel, LocationUtils.toBlockPos(pos));
if (pos.y() + floorHeight > y + 0.75 || !isBlockFloorValid(floorHeight)) { if (pos.y() + floorHeight > y + 0.75 || !isBlockFloorValid(floorHeight)) {

View File

@@ -178,7 +178,7 @@ templates:
# template: default:loot_table/ore # template: default:loot_table/ore
# arguments: # arguments:
# ore_block: the ore block # ore_block: the ore block
# ore_drop: the drops of the ore # ore_drop: the drops of the ore material
# ore_drop_count: the amount of the ore materials # ore_drop_count: the amount of the ore materials
# exp: the exp to drop # exp: the exp to drop
default:loot_table/ore: default:loot_table/ore:

View File

@@ -16,6 +16,7 @@ public class ConstantBlockEntityRenderer implements Cullable {
this.aabb = aabb; this.aabb = aabb;
} }
@Override
public void show(Player player) { public void show(Player player) {
for (BlockEntityElement element : this.elements) { for (BlockEntityElement element : this.elements) {
if (element != null) { if (element != null) {
@@ -24,6 +25,7 @@ public class ConstantBlockEntityRenderer implements Cullable {
} }
} }
@Override
public void hide(Player player) { public void hide(Player player) {
for (BlockEntityElement element : this.elements) { for (BlockEntityElement element : this.elements) {
if (element != null) { if (element != null) {

View File

@@ -11,6 +11,7 @@ import net.momirealms.craftengine.core.sound.SoundData;
import net.momirealms.craftengine.core.sound.SoundSource; import net.momirealms.craftengine.core.sound.SoundSource;
import net.momirealms.craftengine.core.util.Key; import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.world.*; import net.momirealms.craftengine.core.world.*;
import net.momirealms.craftengine.core.world.chunk.client.VirtualCullableObject;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -202,6 +203,10 @@ public abstract class Player extends AbstractEntity implements NetWorkUser {
public abstract void addTrackedBlockEntities(Map<BlockPos, ConstantBlockEntityRenderer> renders); public abstract void addTrackedBlockEntities(Map<BlockPos, ConstantBlockEntityRenderer> renders);
public abstract void addTrackedBlockEntity(BlockPos blockPos, ConstantBlockEntityRenderer renderer);
public abstract VirtualCullableObject getTrackedBlockEntity(BlockPos blockPos);
public abstract void removeTrackedBlockEntities(Collection<BlockPos> renders); public abstract void removeTrackedBlockEntities(Collection<BlockPos> renders);
public abstract void clearTrackedBlockEntities(); public abstract void clearTrackedBlockEntities();

View File

@@ -44,7 +44,7 @@ public class LootPool<T> {
} }
if (this.compositeCondition.test(context)) { if (this.compositeCondition.test(context)) {
Consumer<Item<T>> consumer = LootFunction.decorate(this.compositeFunction, lootConsumer, context); Consumer<Item<T>> consumer = LootFunction.decorate(this.compositeFunction, lootConsumer, context);
int i = this.rolls.getInt(context) + MiscUtils.fastFloor(this.bonusRolls.getFloat(context) * context.luck()); int i = this.rolls.getInt(context) + MiscUtils.floor(this.bonusRolls.getFloat(context) * context.luck());
for (int j = 0; j < i; ++j) { for (int j = 0; j < i; ++j) {
this.addRandomItem(createFunctionApplier(consumer, context), context); this.addRandomItem(createFunctionApplier(consumer, context), context);
} }

View File

@@ -1,12 +1,13 @@
package net.momirealms.craftengine.core.plugin.context; package net.momirealms.craftengine.core.plugin.context;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedMap;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.*; import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.function.Supplier; import java.util.function.Supplier;
public class ContextHolder { public class ContextHolder {

View File

@@ -40,7 +40,7 @@ public class MatchBlockCondition<CTX extends Context> implements Condition<CTX>
Optional<WorldPosition> optionalWorldPosition = ctx.getOptionalParameter(DirectContextParameters.POSITION); Optional<WorldPosition> optionalWorldPosition = ctx.getOptionalParameter(DirectContextParameters.POSITION);
if (optionalWorldPosition.isPresent()) { if (optionalWorldPosition.isPresent()) {
World world = optionalWorldPosition.get().world(); World world = optionalWorldPosition.get().world();
ExistingBlock blockAt = world.getBlock(MiscUtils.fastFloor(this.x.getDouble(ctx)), MiscUtils.fastFloor(this.y.getDouble(ctx)), MiscUtils.fastFloor(this.z.getDouble(ctx))); ExistingBlock blockAt = world.getBlock(MiscUtils.floor(this.x.getDouble(ctx)), MiscUtils.floor(this.y.getDouble(ctx)), MiscUtils.floor(this.z.getDouble(ctx)));
return MiscUtils.matchRegex(blockAt.id().asString(), this.ids, this.regexMatch); return MiscUtils.matchRegex(blockAt.id().asString(), this.ids, this.regexMatch);
} }
return false; return false;

View File

@@ -28,7 +28,7 @@ public class BreakBlockFunction<CTX extends Context> extends AbstractConditional
@Override @Override
public void runInternal(CTX ctx) { public void runInternal(CTX ctx) {
Optional<Player> optionalPlayer = ctx.getOptionalParameter(DirectContextParameters.PLAYER); Optional<Player> optionalPlayer = ctx.getOptionalParameter(DirectContextParameters.PLAYER);
optionalPlayer.ifPresent(player -> player.breakBlock(MiscUtils.fastFloor(x.getDouble(ctx)), MiscUtils.fastFloor(y.getDouble(ctx)), MiscUtils.fastFloor(z.getDouble(ctx)))); optionalPlayer.ifPresent(player -> player.breakBlock(MiscUtils.floor(x.getDouble(ctx)), MiscUtils.floor(y.getDouble(ctx)), MiscUtils.floor(z.getDouble(ctx))));
} }
@Override @Override

View File

@@ -55,9 +55,9 @@ public class CycleBlockPropertyFunction<CTX extends Context> extends AbstractCon
Optional<WorldPosition> optionalWorldPosition = ctx.getOptionalParameter(DirectContextParameters.POSITION); Optional<WorldPosition> optionalWorldPosition = ctx.getOptionalParameter(DirectContextParameters.POSITION);
if (optionalWorldPosition.isEmpty()) return; if (optionalWorldPosition.isEmpty()) return;
World world = optionalWorldPosition.get().world(); World world = optionalWorldPosition.get().world();
int x = MiscUtils.fastFloor(this.x.getDouble(ctx)); int x = MiscUtils.floor(this.x.getDouble(ctx));
int y = MiscUtils.fastFloor(this.y.getDouble(ctx)); int y = MiscUtils.floor(this.y.getDouble(ctx));
int z = MiscUtils.fastFloor(this.z.getDouble(ctx)); int z = MiscUtils.floor(this.z.getDouble(ctx));
BlockStateWrapper wrapper = updateBlockState(world.getBlock(x, y, z).blockState(), ctx); BlockStateWrapper wrapper = updateBlockState(world.getBlock(x, y, z).blockState(), ctx);
world.setBlockState(x, y, z, wrapper, this.updateFlags.getInt(ctx)); world.setBlockState(x, y, z, wrapper, this.updateFlags.getInt(ctx));
} }

View File

@@ -40,7 +40,7 @@ public class PlaceBlockFunction<CTX extends Context> extends AbstractConditional
Optional<WorldPosition> optionalWorldPosition = ctx.getOptionalParameter(DirectContextParameters.POSITION); Optional<WorldPosition> optionalWorldPosition = ctx.getOptionalParameter(DirectContextParameters.POSITION);
if (optionalWorldPosition.isPresent()) { if (optionalWorldPosition.isPresent()) {
World world = optionalWorldPosition.get().world(); World world = optionalWorldPosition.get().world();
world.setBlockState(MiscUtils.fastFloor(this.x.getDouble(ctx)), MiscUtils.fastFloor(this.y.getDouble(ctx)), MiscUtils.fastFloor(this.z.getDouble(ctx)), this.lazyBlockState.get(), this.updateFlags.getInt(ctx)); world.setBlockState(MiscUtils.floor(this.x.getDouble(ctx)), MiscUtils.floor(this.y.getDouble(ctx)), MiscUtils.floor(this.z.getDouble(ctx)), this.lazyBlockState.get(), this.updateFlags.getInt(ctx));
} }
} }

View File

@@ -48,7 +48,7 @@ public class RunFunction<CTX extends Context> extends AbstractConditionalFunctio
for (Function<CTX> function : functions) { for (Function<CTX> function : functions) {
function.run(ctx); function.run(ctx);
} }
}, delay, pos.world().platformWorld(), MiscUtils.fastFloor(pos.x()) >> 4, MiscUtils.fastFloor(pos.z()) >> 4); }, delay, pos.world().platformWorld(), MiscUtils.floor(pos.x()) >> 4, MiscUtils.floor(pos.z()) >> 4);
} }
} }
} }

View File

@@ -45,9 +45,9 @@ public class TransformBlockFunction<CTX extends Context> extends AbstractConditi
Optional<WorldPosition> optionalWorldPosition = ctx.getOptionalParameter(DirectContextParameters.POSITION); Optional<WorldPosition> optionalWorldPosition = ctx.getOptionalParameter(DirectContextParameters.POSITION);
if (optionalWorldPosition.isPresent()) { if (optionalWorldPosition.isPresent()) {
World world = optionalWorldPosition.get().world(); World world = optionalWorldPosition.get().world();
int x = MiscUtils.fastFloor(this.x.getDouble(ctx)); int x = MiscUtils.floor(this.x.getDouble(ctx));
int y = MiscUtils.fastFloor(this.y.getDouble(ctx)); int y = MiscUtils.floor(this.y.getDouble(ctx));
int z = MiscUtils.fastFloor(this.z.getDouble(ctx)); int z = MiscUtils.floor(this.z.getDouble(ctx));
BlockStateWrapper existingBlockState = world.getBlock(x, y, z).blockState().withProperties(this.properties); BlockStateWrapper existingBlockState = world.getBlock(x, y, z).blockState().withProperties(this.properties);
CompoundTag newProperties = new CompoundTag(); CompoundTag newProperties = new CompoundTag();
for (String propertyName : existingBlockState.getPropertyNames()) { for (String propertyName : existingBlockState.getPropertyNames()) {

View File

@@ -40,9 +40,9 @@ public class UpdateBlockPropertyFunction<CTX extends Context> extends AbstractCo
Optional<WorldPosition> optionalWorldPosition = ctx.getOptionalParameter(DirectContextParameters.POSITION); Optional<WorldPosition> optionalWorldPosition = ctx.getOptionalParameter(DirectContextParameters.POSITION);
if (optionalWorldPosition.isPresent()) { if (optionalWorldPosition.isPresent()) {
World world = optionalWorldPosition.get().world(); World world = optionalWorldPosition.get().world();
int x = MiscUtils.fastFloor(this.x.getDouble(ctx)); int x = MiscUtils.floor(this.x.getDouble(ctx));
int y = MiscUtils.fastFloor(this.y.getDouble(ctx)); int y = MiscUtils.floor(this.y.getDouble(ctx));
int z = MiscUtils.fastFloor(this.z.getDouble(ctx)); int z = MiscUtils.floor(this.z.getDouble(ctx));
ExistingBlock blockAt = world.getBlock(x, y, z); ExistingBlock blockAt = world.getBlock(x, y, z);
BlockStateWrapper wrapper = blockAt.blockState().withProperties(this.properties); BlockStateWrapper wrapper = blockAt.blockState().withProperties(this.properties);
world.setBlockState(x, y, z, wrapper, this.updateFlags.getInt(ctx)); world.setBlockState(x, y, z, wrapper, this.updateFlags.getInt(ctx));

View File

@@ -20,9 +20,9 @@ public class EntityParameterProvider implements ChainParameterProvider<Entity> {
CONTEXT_FUNCTIONS.put(DirectContextParameters.YAW, Entity::xRot); CONTEXT_FUNCTIONS.put(DirectContextParameters.YAW, Entity::xRot);
CONTEXT_FUNCTIONS.put(DirectContextParameters.PITCH, Entity::yRot); CONTEXT_FUNCTIONS.put(DirectContextParameters.PITCH, Entity::yRot);
CONTEXT_FUNCTIONS.put(DirectContextParameters.POSITION, Entity::position); CONTEXT_FUNCTIONS.put(DirectContextParameters.POSITION, Entity::position);
CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_X, p -> MiscUtils.fastFloor(p.x())); CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_X, p -> MiscUtils.floor(p.x()));
CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_Y, p -> MiscUtils.fastFloor(p.y())); CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_Y, p -> MiscUtils.floor(p.y()));
CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_Z, p -> MiscUtils.fastFloor(p.z())); CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_Z, p -> MiscUtils.floor(p.z()));
CONTEXT_FUNCTIONS.put(DirectContextParameters.NAME, Entity::name); CONTEXT_FUNCTIONS.put(DirectContextParameters.NAME, Entity::name);
CONTEXT_FUNCTIONS.put(DirectContextParameters.UUID, Entity::uuid); CONTEXT_FUNCTIONS.put(DirectContextParameters.UUID, Entity::uuid);
CONTEXT_FUNCTIONS.put(DirectContextParameters.WORLD, Entity::world); CONTEXT_FUNCTIONS.put(DirectContextParameters.WORLD, Entity::world);

View File

@@ -21,9 +21,9 @@ public class PlayerParameterProvider implements ChainParameterProvider<Player> {
CONTEXT_FUNCTIONS.put(DirectContextParameters.PITCH, Entity::xRot); CONTEXT_FUNCTIONS.put(DirectContextParameters.PITCH, Entity::xRot);
CONTEXT_FUNCTIONS.put(DirectContextParameters.YAW, Entity::yRot); CONTEXT_FUNCTIONS.put(DirectContextParameters.YAW, Entity::yRot);
CONTEXT_FUNCTIONS.put(DirectContextParameters.POSITION, Entity::position); CONTEXT_FUNCTIONS.put(DirectContextParameters.POSITION, Entity::position);
CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_X, p -> MiscUtils.fastFloor(p.x())); CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_X, p -> MiscUtils.floor(p.x()));
CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_Y, p -> MiscUtils.fastFloor(p.y())); CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_Y, p -> MiscUtils.floor(p.y()));
CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_Z, p -> MiscUtils.fastFloor(p.z())); CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_Z, p -> MiscUtils.floor(p.z()));
CONTEXT_FUNCTIONS.put(DirectContextParameters.FOOD, Player::foodLevel); CONTEXT_FUNCTIONS.put(DirectContextParameters.FOOD, Player::foodLevel);
CONTEXT_FUNCTIONS.put(DirectContextParameters.SATURATION, Player::saturation); CONTEXT_FUNCTIONS.put(DirectContextParameters.SATURATION, Player::saturation);
CONTEXT_FUNCTIONS.put(DirectContextParameters.NAME, Player::name); CONTEXT_FUNCTIONS.put(DirectContextParameters.NAME, Player::name);

View File

@@ -20,9 +20,9 @@ public class PositionParameterProvider implements ChainParameterProvider<WorldPo
CONTEXT_FUNCTIONS.put(DirectContextParameters.Z, WorldPosition::z); CONTEXT_FUNCTIONS.put(DirectContextParameters.Z, WorldPosition::z);
CONTEXT_FUNCTIONS.put(DirectContextParameters.YAW, WorldPosition::xRot); CONTEXT_FUNCTIONS.put(DirectContextParameters.YAW, WorldPosition::xRot);
CONTEXT_FUNCTIONS.put(DirectContextParameters.PITCH, WorldPosition::yRot); CONTEXT_FUNCTIONS.put(DirectContextParameters.PITCH, WorldPosition::yRot);
CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_X, p -> MiscUtils.fastFloor(p.x())); CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_X, p -> MiscUtils.floor(p.x()));
CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_Y, p -> MiscUtils.fastFloor(p.y())); CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_Y, p -> MiscUtils.floor(p.y()));
CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_Z, p -> MiscUtils.fastFloor(p.z())); CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_Z, p -> MiscUtils.floor(p.z()));
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")

View File

@@ -1,453 +1,276 @@
package net.momirealms.craftengine.core.plugin.entityculling; package net.momirealms.craftengine.core.plugin.entityculling;
import net.momirealms.craftengine.core.entity.player.Player;
import net.momirealms.craftengine.core.util.MiscUtils; import net.momirealms.craftengine.core.util.MiscUtils;
import net.momirealms.craftengine.core.world.ChunkPos;
import net.momirealms.craftengine.core.world.MutableVec3d; import net.momirealms.craftengine.core.world.MutableVec3d;
import net.momirealms.craftengine.core.world.Vec3d; import net.momirealms.craftengine.core.world.Vec3d;
import net.momirealms.craftengine.core.world.chunk.client.ClientChunk;
import net.momirealms.craftengine.core.world.collision.AABB;
import java.util.Arrays; import java.util.Arrays;
import java.util.BitSet;
public class EntityCulling { public final class EntityCulling {
private final Player player;
// 面掩码常量 private final int maxDistance;
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 double aabbExpansion;
private final DataProvider provider; private final boolean[] dotSelectors = new boolean[14];
private final OcclusionCache cache; private final MutableVec3d[] targetPoints = new MutableVec3d[14];
// 重用数据结构减少GC压力 public EntityCulling(Player player, int maxDistance, double aabbExpansion) {
private final BitSet skipList = new BitSet(); this.player = player;
private final MutableVec3d[] targetPoints = new MutableVec3d[15]; this.maxDistance = maxDistance;
private final MutableVec3d targetPos = new MutableVec3d(0, 0, 0);
private final int[] cameraPos = new int[3];
private final boolean[] dotselectors = new boolean[14];
private final int[] lastHitBlock = new int[3];
// 状态标志
private boolean allowRayChecks = false;
private boolean allowWallClipping = false;
public EntityCulling(int maxDistance, DataProvider provider) {
this(maxDistance, provider, new ArrayOcclusionCache(maxDistance), 0.5);
}
public EntityCulling(int maxDistance, DataProvider provider, OcclusionCache cache, double aabbExpansion) {
this.reach = maxDistance;
this.provider = provider;
this.cache = cache;
this.aabbExpansion = aabbExpansion; this.aabbExpansion = aabbExpansion;
// 预先初始化点对象 for (int i = 0; i < this.targetPoints.length; i++) {
for(int i = 0; i < targetPoints.length; i++) { this.targetPoints[i] = new MutableVec3d(0,0,0);
targetPoints[i] = new MutableVec3d(0, 0, 0);
} }
} }
public boolean isAABBVisible(Vec3d aabbMin, MutableVec3d aabbMax, MutableVec3d viewerPosition) { public boolean isVisible(AABB aabb, Vec3d cameraPos) {
try { // 根据AABB获取能包裹此AABB的最小长方体
// 计算包围盒范围 int minX = MiscUtils.floor(aabb.minX - this.aabbExpansion);
int maxX = MiscUtils.fastFloor(aabbMax.x + aabbExpansion); int minY = MiscUtils.floor(aabb.minY - this.aabbExpansion);
int maxY = MiscUtils.fastFloor(aabbMax.y + aabbExpansion); int minZ = MiscUtils.floor(aabb.minZ - this.aabbExpansion);
int maxZ = MiscUtils.fastFloor(aabbMax.z + aabbExpansion); int maxX = MiscUtils.ceil(aabb.maxX + this.aabbExpansion);
int minX = MiscUtils.fastFloor(aabbMin.x - aabbExpansion); int maxY = MiscUtils.ceil(aabb.maxY + this.aabbExpansion);
int minY = MiscUtils.fastFloor(aabbMin.y - aabbExpansion); int maxZ = MiscUtils.ceil(aabb.maxZ + this.aabbExpansion);
int minZ = MiscUtils.fastFloor(aabbMin.z - aabbExpansion);
cameraPos[0] = MiscUtils.fastFloor(viewerPosition.x); double cameraX = cameraPos.x;
cameraPos[1] = MiscUtils.fastFloor(viewerPosition.y); double cameraY = cameraPos.y;
cameraPos[2] = MiscUtils.fastFloor(viewerPosition.z); double cameraZ = cameraPos.z;
// 判断是否在包围盒内部 Relative relX = Relative.from(minX, maxX, cameraX);
Relative relX = Relative.from(minX, maxX, cameraPos[0]); Relative relY = Relative.from(minY, maxY, cameraY);
Relative relY = Relative.from(minY, maxY, cameraPos[1]); Relative relZ = Relative.from(minZ, maxZ, cameraZ);
Relative relZ = Relative.from(minZ, maxZ, cameraPos[2]);
// 相机位于实体内部
if (relX == Relative.INSIDE && relY == Relative.INSIDE && relZ == Relative.INSIDE) { if (relX == Relative.INSIDE && relY == Relative.INSIDE && relZ == Relative.INSIDE) {
return true; return true;
} }
skipList.clear(); // 如果设置了最大距离
if (this.maxDistance > 0) {
// 1. 快速检查缓存 // 计算AABB到相机的最小距离
int id = 0; double distanceSq = 0.0;
for (int x = minX; x <= maxX; x++) { // 计算XYZ轴方向的距离
for (int y = minY; y <= maxY; y++) { distanceSq += distanceSq(minX, maxX, cameraX, relX);
for (int z = minZ; z <= maxZ; z++) { distanceSq += distanceSq(minY, maxY, cameraY, relY);
int cachedValue = getCacheValue(x, y, z); distanceSq += distanceSq(minZ, maxZ, cameraZ, relZ);
if (cachedValue == 1) return true; // 缓存显示可见 // 检查距离是否超过最大值
if (cachedValue != 0) skipList.set(id); // 缓存显示不可见或遮挡 double maxDistanceSq = this.maxDistance * this.maxDistance;
id++; // 超过最大距离,剔除
} if (distanceSq > maxDistanceSq) {
}
}
allowRayChecks = false;
id = 0;
// 2. 遍历体素进行光线投射检查
for (int x = minX; x <= maxX; x++) {
// 预计算X轴面的可见性和边缘数据
byte visibleOnFaceX = 0;
byte faceEdgeDataX = 0;
if (x == minX) { faceEdgeDataX |= ON_MIN_X; if (relX == Relative.POSITIVE) visibleOnFaceX |= ON_MIN_X; }
if (x == maxX) { faceEdgeDataX |= ON_MAX_X; if (relX == Relative.NEGATIVE) visibleOnFaceX |= ON_MAX_X; }
for (int y = minY; y <= maxY; y++) {
byte visibleOnFaceY = visibleOnFaceX;
byte faceEdgeDataY = faceEdgeDataX;
if (y == minY) { faceEdgeDataY |= ON_MIN_Y; if (relY == Relative.POSITIVE) visibleOnFaceY |= ON_MIN_Y; }
if (y == maxY) { faceEdgeDataY |= ON_MAX_Y; if (relY == Relative.NEGATIVE) visibleOnFaceY |= ON_MAX_Y; }
for (int z = minZ; z <= maxZ; z++) {
// 如果缓存已标记为不可见,跳过
if(skipList.get(id++)) continue;
byte visibleOnFace = visibleOnFaceY;
byte faceEdgeData = faceEdgeDataY;
if (z == minZ) { faceEdgeData |= ON_MIN_Z; if (relZ == Relative.POSITIVE) visibleOnFace |= ON_MIN_Z; }
if (z == maxZ) { faceEdgeData |= ON_MAX_Z; if (relZ == Relative.NEGATIVE) visibleOnFace |= ON_MAX_Z; }
if (visibleOnFace != 0) {
targetPos.set(x, y, z);
// 检查单个体素是否可见
if (isVoxelVisible(viewerPosition, targetPos, faceEdgeData, visibleOnFace)) {
return true;
}
}
}
}
}
return false; return false;
} catch (Throwable t) {
t.printStackTrace();
return true; // 发生异常默认可见,防止渲染错误
} }
} }
// 接口定义 // 清空之前的缓存
public interface DataProvider { Arrays.fill(this.dotSelectors, false);
boolean prepareChunk(int chunkX, int chunkZ); if (relX == Relative.POSITIVE) {
boolean isOpaqueFullCube(int x, int y, int z); this.dotSelectors[0] = this.dotSelectors[2] = this.dotSelectors[4] = this.dotSelectors[6] = this.dotSelectors[10] = true;
default void cleanup() {} } else if (relX == Relative.NEGATIVE) {
default void checkingPosition(MutableVec3d[] targetPoints, int size, MutableVec3d viewerPosition) {} this.dotSelectors[1] = this.dotSelectors[3] = this.dotSelectors[5] = this.dotSelectors[7] = this.dotSelectors[11] = true;
}
if (relY == Relative.POSITIVE) {
this.dotSelectors[0] = this.dotSelectors[1] = this.dotSelectors[2] = this.dotSelectors[3] = this.dotSelectors[12] = true;
} else if (relY == Relative.NEGATIVE) {
this.dotSelectors[4] = this.dotSelectors[5] = this.dotSelectors[6] = this.dotSelectors[7] = this.dotSelectors[13] = true;
}
if (relZ == Relative.POSITIVE) {
this.dotSelectors[0] = this.dotSelectors[1] = this.dotSelectors[4] = this.dotSelectors[5] = this.dotSelectors[8] = true;
} else if (relZ == Relative.NEGATIVE) {
this.dotSelectors[2] = this.dotSelectors[3] = this.dotSelectors[6] = this.dotSelectors[7] = this.dotSelectors[9] = true;
} }
/** int size = 0;
* 检查单个体素是否对观察者可见 if (this.dotSelectors[0]) targetPoints[size++].set(minX, minY, minZ);
*/ if (this.dotSelectors[1]) targetPoints[size++].set(maxX, minY, minZ);
private boolean isVoxelVisible(MutableVec3d viewerPosition, MutableVec3d position, byte faceData, byte visibleOnFace) { if (this.dotSelectors[2]) targetPoints[size++].set(minX, minY, maxZ);
int targetSize = 0; if (this.dotSelectors[3]) targetPoints[size++].set(maxX, minY, maxZ);
Arrays.fill(dotselectors, false); if (this.dotSelectors[4]) targetPoints[size++].set(minX, maxY, minZ);
if (this.dotSelectors[5]) targetPoints[size++].set(maxX, maxY, minZ);
// 根据相对位置选择需要检测的关键点(角点和面中心点) if (this.dotSelectors[6]) targetPoints[size++].set(minX, maxY, maxZ);
if((visibleOnFace & ON_MIN_X) != 0){ if (this.dotSelectors[7]) targetPoints[size++].set(maxX, maxY, maxZ);
dotselectors[0] = true;
if((faceData & ~ON_MIN_X) != 0) { dotselectors[1] = dotselectors[4] = dotselectors[5] = true; }
dotselectors[8] = true;
}
if((visibleOnFace & ON_MIN_Y) != 0){
dotselectors[0] = true;
if((faceData & ~ON_MIN_Y) != 0) { dotselectors[3] = dotselectors[4] = dotselectors[7] = true; }
dotselectors[9] = true;
}
if((visibleOnFace & ON_MIN_Z) != 0){
dotselectors[0] = true;
if((faceData & ~ON_MIN_Z) != 0) { dotselectors[1] = dotselectors[4] = dotselectors[5] = true; }
dotselectors[10] = true;
}
if((visibleOnFace & ON_MAX_X) != 0){
dotselectors[4] = true;
if((faceData & ~ON_MAX_X) != 0) { dotselectors[5] = dotselectors[6] = dotselectors[7] = true; }
dotselectors[11] = true;
}
if((visibleOnFace & ON_MAX_Y) != 0){
dotselectors[1] = true;
if((faceData & ~ON_MAX_Y) != 0) { dotselectors[2] = dotselectors[5] = dotselectors[6] = true; }
dotselectors[12] = true;
}
if((visibleOnFace & ON_MAX_Z) != 0){
dotselectors[2] = true;
if((faceData & ~ON_MAX_Z) != 0) { dotselectors[3] = dotselectors[6] = dotselectors[7] = true; }
dotselectors[13] = true;
}
// 填充目标点使用偏移量防止Z-Fighting或精度问题
if (dotselectors[0]) targetPoints[targetSize++].add(position, 0.05, 0.05, 0.05);
if (dotselectors[1]) targetPoints[targetSize++].add(position, 0.05, 0.95, 0.05);
if (dotselectors[2]) targetPoints[targetSize++].add(position, 0.05, 0.95, 0.95);
if (dotselectors[3]) targetPoints[targetSize++].add(position, 0.05, 0.05, 0.95);
if (dotselectors[4]) targetPoints[targetSize++].add(position, 0.95, 0.05, 0.05);
if (dotselectors[5]) targetPoints[targetSize++].add(position, 0.95, 0.95, 0.05);
if (dotselectors[6]) targetPoints[targetSize++].add(position, 0.95, 0.95, 0.95);
if (dotselectors[7]) targetPoints[targetSize++].add(position, 0.95, 0.05, 0.95);
// 面中心点 // 面中心点
if (dotselectors[8]) targetPoints[targetSize++].add(position, 0.05, 0.5, 0.5); double averageX = (minX + maxX) / 2.0;
if (dotselectors[9]) targetPoints[targetSize++].add(position, 0.5, 0.05, 0.5); double averageY = (minY + maxY) / 2.0;
if (dotselectors[10]) targetPoints[targetSize++].add(position, 0.5, 0.5, 0.05); double averageZ = (minZ + maxZ) / 2.0;
if (dotselectors[11]) targetPoints[targetSize++].add(position, 0.95, 0.5, 0.5); if (this.dotSelectors[8]) targetPoints[size++].set(averageX, averageY, minZ);
if (dotselectors[12]) targetPoints[targetSize++].add(position, 0.5, 0.95, 0.5); if (this.dotSelectors[9]) targetPoints[size++].set(averageX, averageY, maxZ);
if (dotselectors[13]) targetPoints[targetSize++].add(position, 0.5, 0.5, 0.95); if (this.dotSelectors[10]) targetPoints[size++].set(minX, averageY, averageZ);
if (this.dotSelectors[11]) targetPoints[size++].set(maxX, averageY, averageZ);
if (this.dotSelectors[12]) targetPoints[size++].set(averageX, minY, averageZ);
if (this.dotSelectors[13]) targetPoints[size].set(averageX, maxY, averageZ);
return isVisible(viewerPosition, targetPoints, targetSize); return isVisible(cameraPos, this.targetPoints, size);
}
// 优化:使用基本数据类型代替对象分配
private boolean rayIntersection(int[] b, MutableVec3d rayOrigin, double dirX, double dirY, double dirZ) {
double invX = 1.0 / dirX;
double invY = 1.0 / dirY;
double invZ = 1.0 / dirZ;
double t1 = (b[0] - rayOrigin.x) * invX;
double t2 = (b[0] + 1 - rayOrigin.x) * invX;
double t3 = (b[1] - rayOrigin.y) * invY;
double t4 = (b[1] + 1 - rayOrigin.y) * invY;
double t5 = (b[2] - rayOrigin.z) * invZ;
double t6 = (b[2] + 1 - rayOrigin.z) * invZ;
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));
// tmax > 0: 射线与AABB相交但AABB在身后
// tmin > tmax: 射线不相交
return tmax > 0 && tmin <= tmax;
} }
/** /**
* 基于网格的光线追踪 (DDA算法) * 使用3D DDA算法检测从起点到多个目标点的视线是否通畅
* 算法基于数字微分分析,遍历射线路径上的所有方块
*/ */
private boolean isVisible(MutableVec3d start, MutableVec3d[] targets, int size) { private boolean isVisible(Vec3d start, MutableVec3d[] targets, int targetCount) {
int startX = cameraPos[0]; // 起点所在方块的整数坐标(世界坐标转换为方块坐标)
int startY = cameraPos[1]; int startBlockX = MiscUtils.floor(start.x);
int startZ = cameraPos[2]; int startBlockY = MiscUtils.floor(start.y);
int startBlockZ = MiscUtils.floor(start.z);
for (int v = 0; v < size; v++) { // 遍历所有目标点进行视线检测
MutableVec3d target = targets[v]; for (int targetIndex = 0; targetIndex < targetCount; targetIndex++) {
MutableVec3d currentTarget = targets[targetIndex];
double relX = start.x - target.x; // 计算起点到目标的相对向量(世界坐标差)
double relY = start.y - target.y; double deltaX = start.x - currentTarget.x;
double relZ = start.z - target.z; double deltaY = start.y - currentTarget.y;
double deltaZ = start.z - currentTarget.z;
// 优化避免在此处创建新的Vec3d对象进行归一化 // 计算相对向量的绝对值,用于确定各方向上的距离
if(allowRayChecks) { double absDeltaX = Math.abs(deltaX);
double len = Math.sqrt(relX * relX + relY * relY + relZ * relZ); double absDeltaY = Math.abs(deltaY);
// 传入归一化后的方向分量 double absDeltaZ = Math.abs(deltaZ);
if (rayIntersection(lastHitBlock, start, relX / len, relY / len, relZ / len)) {
continue;
}
}
double dimAbsX = Math.abs(relX); // 预计算每单位距离在各方块边界上的步进增量
double dimAbsY = Math.abs(relY); // 这些值表示射线穿过一个方块所需的时间分数
double dimAbsZ = Math.abs(relZ); double stepIncrementX = 1.0 / (absDeltaX + 1e-10); // 避免除0
double stepIncrementY = 1.0 / (absDeltaY + 1e-10);
double stepIncrementZ = 1.0 / (absDeltaZ + 1e-10);
double dimFracX = 1f / dimAbsX; // 射线将穿过的总方块数量(包括起点和终点)
double dimFracY = 1f / dimAbsY; int totalBlocksToCheck = 1;
double dimFracZ = 1f / dimAbsZ;
int intersectCount = 1; // 各方块坐标的步进方向1: 正向, -1: 反向, 0: 静止)
int x_inc, y_inc, z_inc; int stepDirectionX, stepDirectionY, stepDirectionZ;
double t_next_y, t_next_x, t_next_z;
// 初始化DDA步进参数 // 到下一个方块边界的时间参数(射线参数化表示)
if (dimAbsX == 0f) { double nextStepTimeX, nextStepTimeY, nextStepTimeZ;
x_inc = 0; t_next_x = dimFracX;
} else if (target.x > start.x) { // X方向步进参数计算
x_inc = 1; if (absDeltaX == 0.0) {
intersectCount += MiscUtils.fastFloor(target.x) - startX; // X方向无变化射线平行于YZ平面
t_next_x = (startX + 1 - start.x) * dimFracX; stepDirectionX = 0;
nextStepTimeX = stepIncrementX;
} else if (currentTarget.x > start.x) {
// 目标在起点右侧,向右步进
stepDirectionX = 1;
totalBlocksToCheck += MiscUtils.floor(currentTarget.x) - startBlockX;
nextStepTimeX = (startBlockX + 1 - start.x) * stepIncrementX;
} else { } else {
x_inc = -1; // 目标在起点左侧,向左步进
intersectCount += startX - MiscUtils.fastFloor(target.x); stepDirectionX = -1;
t_next_x = (start.x - startX) * dimFracX; totalBlocksToCheck += startBlockX - MiscUtils.floor(currentTarget.x);
nextStepTimeX = (start.x - startBlockX) * stepIncrementX;
} }
if (dimAbsY == 0f) { // Y方向步进参数计算
y_inc = 0; t_next_y = dimFracY; if (absDeltaY == 0.0) {
} else if (target.y > start.y) { // Y方向无变化射线平行于XZ平面
y_inc = 1; stepDirectionY = 0;
intersectCount += MiscUtils.fastFloor(target.y) - startY; nextStepTimeY = stepIncrementY;
t_next_y = (startY + 1 - start.y) * dimFracY; } else if (currentTarget.y > start.y) {
// 目标在起点上方,向上步进
stepDirectionY = 1;
totalBlocksToCheck += MiscUtils.floor(currentTarget.y) - startBlockY;
nextStepTimeY = (startBlockY + 1 - start.y) * stepIncrementY;
} else { } else {
y_inc = -1; // 目标在起点下方,向下步进
intersectCount += startY - MiscUtils.fastFloor(target.y); stepDirectionY = -1;
t_next_y = (start.y - startY) * dimFracY; totalBlocksToCheck += startBlockY - MiscUtils.floor(currentTarget.y);
nextStepTimeY = (start.y - startBlockY) * stepIncrementY;
} }
if (dimAbsZ == 0f) { // Z方向步进参数计算
z_inc = 0; t_next_z = dimFracZ; if (absDeltaZ == 0.0) {
} else if (target.z > start.z) { // Z方向无变化射线平行于XY平面
z_inc = 1; stepDirectionZ = 0;
intersectCount += MiscUtils.fastFloor(target.z) - startZ; nextStepTimeZ = stepIncrementZ;
t_next_z = (startZ + 1 - start.z) * dimFracZ; } else if (currentTarget.z > start.z) {
// 目标在起点前方,向前步进
stepDirectionZ = 1;
totalBlocksToCheck += MiscUtils.floor(currentTarget.z) - startBlockZ;
nextStepTimeZ = (startBlockZ + 1 - start.z) * stepIncrementZ;
} else { } else {
z_inc = -1; // 目标在起点后方,向后步进
intersectCount += startZ - MiscUtils.fastFloor(target.z); stepDirectionZ = -1;
t_next_z = (start.z - startZ) * dimFracZ; totalBlocksToCheck += startBlockZ - MiscUtils.floor(currentTarget.z);
nextStepTimeZ = (start.z - startBlockZ) * stepIncrementZ;
} }
boolean finished = stepRay(startX, startY, startZ, // 执行DDA步进算法遍历射线路径上的所有方块
dimFracX, dimFracY, dimFracZ, intersectCount, boolean isLineOfSightClear = stepRay(
x_inc, y_inc, z_inc, startBlockX, startBlockY, startBlockZ,
t_next_y, t_next_x, t_next_z); stepIncrementX, stepIncrementY, stepIncrementZ, totalBlocksToCheck,
stepDirectionX, stepDirectionY, stepDirectionZ,
nextStepTimeY, nextStepTimeX, nextStepTimeZ);
provider.cleanup(); // 如果当前目标点可见立即返回
if (finished) { if (isLineOfSightClear) {
cacheResult(targets[0], true);
return true; return true;
} else {
allowRayChecks = true;
} }
} }
cacheResult(targets[0], false);
return false; return false;
} }
private boolean stepRay(int currentX, int currentY, int currentZ, private boolean stepRay(int currentBlockX, int currentBlockY, int currentBlockZ,
double distInX, double distInY, double distInZ, double stepSizeX, double stepSizeY, double stepSizeZ,
int n, int x_inc, int y_inc, int z_inc, int remainingSteps, int stepDirectionX, int stepDirectionY,
double t_next_y, double t_next_x, double t_next_z) { int stepDirectionZ, double nextStepTimeY, double nextStepTimeX,
double nextStepTimeZ) {
allowWallClipping = true; // 初始允许穿墙直到移出起始方块 // 遍历射线路径上的所有方块(跳过最后一个目标方块
for (; remainingSteps > 1; remainingSteps--) {
for (; n > 1; n--) { // 检查当前方块是否遮挡视线
// 检查缓存状态2=遮挡 if (isOccluding(currentBlockX, currentBlockY, currentBlockZ)) {
int cVal = getCacheValue(currentX, currentY, currentZ); return false; // 视线被遮挡,立即返回
if (cVal == 2 && !allowWallClipping) {
lastHitBlock[0] = currentX; lastHitBlock[1] = currentY; lastHitBlock[2] = currentZ;
return false;
} }
if (cVal == 0) { // 基于时间参数选择下一个要遍历的方块方向
// 未缓存查询Provider // 选择距离最近的方块边界作为下一步
int chunkX = currentX >> 4; if (nextStepTimeY < nextStepTimeX && nextStepTimeY < nextStepTimeZ) {
int chunkZ = currentZ >> 4; // Y方向边界最近垂直移动
if (!provider.prepareChunk(chunkX, chunkZ)) return false; currentBlockY += stepDirectionY;
nextStepTimeY += stepSizeY;
if (provider.isOpaqueFullCube(currentX, currentY, currentZ)) { } else if (nextStepTimeX < nextStepTimeY && nextStepTimeX < nextStepTimeZ) {
if (!allowWallClipping) { // X方向边界最近水平移动
cache.setLastHidden(); currentBlockX += stepDirectionX;
lastHitBlock[0] = currentX; lastHitBlock[1] = currentY; lastHitBlock[2] = currentZ; nextStepTimeX += stepSizeX;
return false;
}
} else { } else {
allowWallClipping = false; // Z方向边界最近深度移动
cache.setLastVisible(); currentBlockZ += stepDirectionZ;
nextStepTimeZ += stepSizeZ;
} }
} else if(cVal == 1) {
allowWallClipping = false;
} }
// DDA算法选择下一个体素 // 成功遍历所有中间方块,视线通畅
if (t_next_y < t_next_x && t_next_y < t_next_z) {
currentY += y_inc;
t_next_y += distInY;
} else if (t_next_x < t_next_y && t_next_x < t_next_z) {
currentX += x_inc;
t_next_x += distInX;
} else {
currentZ += z_inc;
t_next_z += distInZ;
}
}
return true; return true;
} }
// 缓存状态:-1=无效, 0=未检查, 1=可见, 2=遮挡 private double distanceSq(int min, int max, double camera, Relative rel) {
private int getCacheValue(int x, int y, int z) { if (rel == Relative.NEGATIVE) {
x -= cameraPos[0]; double dx = camera - max;
y -= cameraPos[1]; return dx * dx;
z -= cameraPos[2]; } else if (rel == Relative.POSITIVE) {
if (Math.abs(x) > reach - 2 || Math.abs(y) > reach - 2 || Math.abs(z) > reach - 2) { double dx = min - camera;
return -1; return dx * dx;
} }
return cache.getState(x + reach, y + reach, z + reach); return 0d;
} }
private void cacheResult(MutableVec3d vector, boolean result) { private boolean isOccluding(int x, int y, int z) {
int cx = MiscUtils.fastFloor(vector.x) - cameraPos[0] + reach; ClientChunk trackedChunk = this.player.getTrackedChunk(ChunkPos.asLong(x >> 4, z >> 4));
int cy = MiscUtils.fastFloor(vector.y) - cameraPos[1] + reach; if (trackedChunk == null) {
int cz = MiscUtils.fastFloor(vector.z) - cameraPos[2] + reach; return false;
if (result) cache.setVisible(cx, cy, cz);
else cache.setHidden(cx, cy, cz);
} }
return trackedChunk.isOccluding(x, y, z);
public void resetCache() {
this.cache.resetCache();
} }
private enum Relative { private enum Relative {
INSIDE, POSITIVE, NEGATIVE; INSIDE, POSITIVE, NEGATIVE;
public static Relative from(int min, int max, int pos) { public static Relative from(int min, int max, double pos) {
if (max > pos && min > pos) return POSITIVE; if (min > pos) return POSITIVE;
else if (min < pos && max < pos) return NEGATIVE; else if (max < pos) return NEGATIVE;
return INSIDE; return INSIDE;
} }
} }
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();
}
// 使用位运算压缩存储状态的缓存实现
public static class ArrayOcclusionCache implements OcclusionCache {
private final int reachX2;
private final byte[] cache;
private int entry, offset;
public ArrayOcclusionCache(int reach) {
this.reachX2 = reach * 2;
// 每一个位置占2位
this.cache = new byte[(reachX2 * reachX2 * reachX2) / 4 + 1];
}
@Override
public void resetCache() {
Arrays.fill(cache, (byte) 0);
}
private void calcIndex(int x, int y, int z) {
int positionKey = x + y * reachX2 + z * reachX2 * reachX2;
entry = positionKey / 4;
offset = (positionKey % 4) * 2;
}
@Override
public void setVisible(int x, int y, int z) {
calcIndex(x, y, z);
cache[entry] |= 1 << offset;
}
@Override
public void setHidden(int x, int y, int z) {
calcIndex(x, y, z);
cache[entry] |= 1 << (offset + 1);
}
@Override
public int getState(int x, int y, int z) {
calcIndex(x, y, z);
return (cache[entry] >> offset) & 3;
}
@Override
public void setLastVisible() {
cache[entry] |= 1 << offset;
}
@Override
public void setLastHidden() {
cache[entry] |= 1 << (offset + 1);
}
}
} }

View File

@@ -0,0 +1,216 @@
package net.momirealms.craftengine.core.plugin.entityculling;
import java.util.Iterator;
import java.util.NoSuchElementException;
// Amanatides, J., & Woo, A. A Fast Voxel Traversal Algorithm for Ray Tracing. http://www.cse.yorku.ca/~amana/research/grid.pdf.
public final class VoxelIterator implements Iterator<int[]> {
private int x;
private int y;
private int z;
private int stepX;
private int stepY;
private int stepZ;
private double tMax;
private double tMaxX;
private double tMaxY;
private double tMaxZ;
private double tDeltaX;
private double tDeltaY;
private double tDeltaZ;
private int[] ref = new int[3]; // This implementation always returns ref or refSwap to avoid garbage. Can easily be changed if needed.
private int[] refSwap = new int[3];
private int[] next;
public VoxelIterator(double startX, double startY, double startZ, double endX, double endY, double endZ) {
initialize(startX, startY, startZ, endX, endY, endZ);
}
public VoxelIterator(int x, int y, int z, double startX, double startY, double startZ, double endX, double endY, double endZ) {
initialize(x, y, z, startX, startY, startZ, endX, endY, endZ);
}
public VoxelIterator(double startX, double startY, double startZ, double directionX, double directionY, double directionZ, double distance) {
initialize(startX, startY, startZ, directionX, directionY, directionZ, distance);
}
public VoxelIterator(int x, int y, int z, double startX, double startY, double startZ, double directionX, double directionY, double directionZ, double distance) {
initialize(x, y, z, startX, startY, startZ, directionX, directionY, directionZ, distance);
}
public VoxelIterator(double startX, double startY, double startZ, double directionX, double directionY, double directionZ, double distance, boolean normalized) {
if (normalized) {
initializeNormalized(startX, startY, startZ, directionX, directionY, directionZ, distance);
} else {
initialize(startX, startY, startZ, directionX, directionY, directionZ, distance);
}
}
public VoxelIterator(int x, int y, int z, double startX, double startY, double startZ, double directionX, double directionY, double directionZ, double distance, boolean normalized) {
if (normalized) {
initializeNormalized(x, y, z, startX, startY, startZ, directionX, directionY, directionZ, distance);
} else {
initialize(x, y, z, startX, startY, startZ, directionX, directionY, directionZ, distance);
}
}
public VoxelIterator initialize(double startX, double startY, double startZ, double endX, double endY, double endZ) {
return initialize(floor(startX), floor(startY), floor(startZ), startX, startY, startZ, endX, endY, endZ);
}
public VoxelIterator initialize(int x, int y, int z, double startX, double startY, double startZ, double endX, double endY, double endZ) {
double directionX = endX - startX;
double directionY = endY - startY;
double directionZ = endZ - startZ;
double distance = Math.sqrt(directionX * directionX + directionY * directionY + directionZ * directionZ);
double fixedDistance = distance == 0. ? Double.NaN : distance;
directionX /= fixedDistance;
directionY /= fixedDistance;
directionZ /= fixedDistance;
return initializeNormalized(x, y, z, startX, startY, startZ, directionX, directionY, directionZ, distance);
}
public VoxelIterator initialize(double startX, double startY, double startZ, double directionX, double directionY, double directionZ, double distance) {
return initialize(floor(startX), floor(startY), floor(startZ), startX, startY, startZ, directionX, directionY, directionZ, distance);
}
public VoxelIterator initialize(int x, int y, int z, double startX, double startY, double startZ, double directionX, double directionY, double directionZ, double distance) {
double signum = Math.signum(distance);
directionX *= signum;
directionY *= signum;
directionZ *= signum;
double length = Math.sqrt(directionX * directionX + directionY * directionY + directionZ * directionZ);
if (length == 0.) {
length = Double.NaN;
}
directionX /= length;
directionY /= length;
directionZ /= length;
return initializeNormalized(x, y, z, startX, startY, startZ, directionX, directionY, directionZ, Math.abs(distance));
}
public VoxelIterator initializeNormalized(double startX, double startY, double startZ, double directionX, double directionY, double directionZ, double distance) {
return initializeNormalized(floor(startX), floor(startY), floor(startZ), startX, startY, startZ, directionX, directionY, directionZ, Math.abs(distance));
}
public VoxelIterator initializeNormalized(int x, int y, int z, double startX, double startY, double startZ, double directionX, double directionY, double directionZ, double distance) {
this.x = x;
this.y = y;
this.z = z;
tMax = distance;
stepX = directionX < 0. ? -1 : 1;
stepY = directionY < 0. ? -1 : 1;
stepZ = directionZ < 0. ? -1 : 1;
tMaxX = directionX == 0. ? Double.POSITIVE_INFINITY : (x + (stepX + 1) / 2 - startX) / directionX;
tMaxY = directionY == 0. ? Double.POSITIVE_INFINITY : (y + (stepY + 1) / 2 - startY) / directionY;
tMaxZ = directionZ == 0. ? Double.POSITIVE_INFINITY : (z + (stepZ + 1) / 2 - startZ) / directionZ;
tDeltaX = 1. / Math.abs(directionX);
tDeltaY = 1. / Math.abs(directionY);
tDeltaZ = 1. / Math.abs(directionZ);
next = ref;
ref[0] = x;
ref[1] = y;
ref[2] = z;
return this;
}
public int[] calculateNext() {
if (tMaxX < tMaxY) {
if (tMaxZ < tMaxX) {
if (tMaxZ <= tMax) {
z += stepZ;
// next = new int[] { x, y, z };
ref[0] = x;
ref[1] = y;
ref[2] = z;
tMaxZ += tDeltaZ;
} else {
next = null;
}
} else {
if (tMaxX <= tMax) {
if (tMaxZ == tMaxX) {
z += stepZ;
tMaxZ += tDeltaZ;
}
x += stepX;
// next = new int[] { x, y, z };
ref[0] = x;
ref[1] = y;
ref[2] = z;
tMaxX += tDeltaX;
} else {
next = null;
}
}
} else if (tMaxY < tMaxZ) {
if (tMaxY <= tMax) {
if (tMaxX == tMaxY) {
x += stepX;
tMaxX += tDeltaX;
}
y += stepY;
// next = new int[] { x, y, z };
ref[0] = x;
ref[1] = y;
ref[2] = z;
tMaxY += tDeltaY;
} else {
next = null;
}
} else {
if (tMaxZ <= tMax) {
if (tMaxX == tMaxZ) {
x += stepX;
tMaxX += tDeltaX;
}
if (tMaxY == tMaxZ) {
y += stepY;
tMaxY += tDeltaY;
}
z += stepZ;
// next = new int[] { x, y, z };
ref[0] = x;
ref[1] = y;
ref[2] = z;
tMaxZ += tDeltaZ;
} else {
next = null;
}
}
return next;
}
@Override
public boolean hasNext() {
return next != null;
}
@Override
public int[] next() {
int[] next = this.next;
if (next == null) {
throw new NoSuchElementException();
}
int[] temp = ref;
ref = refSwap;
refSwap = temp;
this.next = ref;
calculateNext();
return next;
}
private static int floor(double value) {
int i = (int) value;
return value < (double) i ? i - 1 : i;
}
}

View File

@@ -38,7 +38,7 @@ public class Color {
} }
public static Color fromVector3f(Vector3f vec) { public static Color fromVector3f(Vector3f vec) {
return new Color(0 << 24 /*不可省略*/ | MiscUtils.fastFloor(vec.x) << 16 | MiscUtils.fastFloor(vec.y) << 8 | MiscUtils.fastFloor(vec.z)); return new Color(0 << 24 /*不可省略*/ | MiscUtils.floor(vec.x) << 16 | MiscUtils.floor(vec.y) << 8 | MiscUtils.floor(vec.z));
} }
public static int opaque(int color) { public static int opaque(int color) {

View File

@@ -21,19 +21,19 @@ public class MiscUtils {
} }
}); });
public static int fastFloor(double value) { public static int floor(double value) {
int truncated = (int) value; int truncated = (int) value;
return value < (double) truncated ? truncated - 1 : truncated; return value < (double) truncated ? truncated - 1 : truncated;
} }
public static int fastFloor(float value) { public static int floor(float value) {
int truncated = (int) value; int truncated = (int) value;
return value < (double) truncated ? truncated - 1 : truncated; return value < (double) truncated ? truncated - 1 : truncated;
} }
public static int lerpDiscrete(float delta, int start, int end) { public static int lerpDiscrete(float delta, int start, int end) {
int i = end - start; int i = end - start;
return start + fastFloor(delta * (float) (i - 1)) + (delta > 0.0F ? 1 : 0); return start + floor(delta * (float) (i - 1)) + (delta > 0.0F ? 1 : 0);
} }
public static int murmurHash3Mixer(int value) { public static int murmurHash3Mixer(int value) {
@@ -270,7 +270,7 @@ public class MiscUtils {
} }
public static byte packDegrees(float degrees) { public static byte packDegrees(float degrees) {
return (byte) fastFloor(degrees * 256.0F / 360.0F); return (byte) floor(degrees * 256.0F / 360.0F);
} }
public static float unpackDegrees(byte degrees) { public static float unpackDegrees(byte degrees) {

View File

@@ -23,7 +23,7 @@ public class BlockPos extends Vec3i {
} }
public static BlockPos fromVec3d(Vec3d vec) { public static BlockPos fromVec3d(Vec3d vec) {
return new BlockPos(MiscUtils.fastFloor(vec.x), MiscUtils.fastFloor(vec.y), MiscUtils.fastFloor(vec.z)); return new BlockPos(MiscUtils.floor(vec.x), MiscUtils.floor(vec.y), MiscUtils.floor(vec.z));
} }
public static BlockPos of(long packedPos) { public static BlockPos of(long packedPos) {

View File

@@ -23,9 +23,9 @@ public class EntityHitResult {
} }
private BlockPos getBlockPos() { private BlockPos getBlockPos() {
int x = MiscUtils.fastFloor(this.position.x); int x = MiscUtils.floor(this.position.x);
int y = MiscUtils.fastFloor(this.position.y); int y = MiscUtils.floor(this.position.y);
int z = MiscUtils.fastFloor(this.position.z); int z = MiscUtils.floor(this.position.z);
if (this.direction == Direction.UP) { if (this.direction == Direction.UP) {
if (this.position.y % 1 == 0) { if (this.position.y % 1 == 0) {
y--; y--;

View File

@@ -14,9 +14,9 @@ public class MutableVec3d implements Position {
} }
public MutableVec3d toCenter() { public MutableVec3d toCenter() {
this.x = MiscUtils.fastFloor(x) + 0.5; this.x = MiscUtils.floor(x) + 0.5;
this.y = MiscUtils.fastFloor(y) + 0.5; this.y = MiscUtils.floor(y) + 0.5;
this.z = MiscUtils.fastFloor(z) + 0.5; this.z = MiscUtils.floor(z) + 0.5;
return this; return this;
} }

View File

@@ -15,7 +15,7 @@ public class Vec3d implements Position {
} }
public Vec3d toCenter() { public Vec3d toCenter() {
return new Vec3d(MiscUtils.fastFloor(x) + 0.5, MiscUtils.fastFloor(y) + 0.5, MiscUtils.fastFloor(z) + 0.5); return new Vec3d(MiscUtils.floor(x) + 0.5, MiscUtils.floor(y) + 0.5, MiscUtils.floor(z) + 0.5);
} }
public Vec3d add(Vec3d vec) { public Vec3d add(Vec3d vec) {

View File

@@ -14,6 +14,7 @@ import net.momirealms.craftengine.core.entity.player.Player;
import net.momirealms.craftengine.core.plugin.config.Config; import net.momirealms.craftengine.core.plugin.config.Config;
import net.momirealms.craftengine.core.plugin.logger.Debugger; import net.momirealms.craftengine.core.plugin.logger.Debugger;
import net.momirealms.craftengine.core.world.*; import net.momirealms.craftengine.core.world.*;
import net.momirealms.craftengine.core.world.chunk.client.VirtualCullableObject;
import net.momirealms.craftengine.core.world.chunk.serialization.DefaultBlockEntityRendererSerializer; import net.momirealms.craftengine.core.world.chunk.serialization.DefaultBlockEntityRendererSerializer;
import net.momirealms.craftengine.core.world.chunk.serialization.DefaultBlockEntitySerializer; import net.momirealms.craftengine.core.world.chunk.serialization.DefaultBlockEntitySerializer;
import net.momirealms.sparrow.nbt.ListTag; import net.momirealms.sparrow.nbt.ListTag;
@@ -155,22 +156,43 @@ public class CEChunk {
if (element != null) { if (element != null) {
elements[0] = element; elements[0] = element;
if (hasTrackedBy) { if (hasTrackedBy) {
// 如果启用实体剔除,那么只对已经渲染的进行变换
if (Config.enableEntityCulling()) {
for (Player player : trackedBy) {
VirtualCullableObject trackedBlockEntity = player.getTrackedBlockEntity(pos);
if (trackedBlockEntity == null || trackedBlockEntity.isShown()) {
element.transform(player);
}
}
}
// 否则直接变换
else {
for (Player player : trackedBy) { for (Player player : trackedBy) {
element.transform(player); element.transform(player);
} }
} }
}
break outer; break outer;
} }
} }
BlockEntityElement element = config.create(wrappedWorld, pos); BlockEntityElement element = config.create(wrappedWorld, pos);
elements[0] = element; elements[0] = element;
if (hasTrackedBy) { if (hasTrackedBy) {
// 如果启用实体剔除,那么只添加记录
if (Config.enableEntityCulling()) {
for (Player player : trackedBy) {
player.addTrackedBlockEntity(pos, renderer);
}
}
// 否则直接显示
else {
for (Player player : trackedBy) { for (Player player : trackedBy) {
previousElement.hide(player); previousElement.hide(player);
element.show(player); element.show(player);
} }
} }
} }
}
} else { } else {
outer: for (int i = 0; i < elements.length; i++) { outer: for (int i = 0; i < elements.length; i++) {
BlockEntityElementConfig<? extends BlockEntityElement> config = renderers[i]; BlockEntityElementConfig<? extends BlockEntityElement> config = renderers[i];
@@ -182,10 +204,22 @@ public class CEChunk {
previousElements[j] = null; previousElements[j] = null;
elements[i] = newElement; elements[i] = newElement;
if (hasTrackedBy) { if (hasTrackedBy) {
// 如果启用实体剔除,那么只对已经渲染的进行变换
if (Config.enableEntityCulling()) {
for (Player player : trackedBy) {
VirtualCullableObject trackedBlockEntity = player.getTrackedBlockEntity(pos);
if (trackedBlockEntity == null || trackedBlockEntity.isShown()) {
newElement.transform(player);
}
}
}
// 否则直接变换
else {
for (Player player : trackedBy) { for (Player player : trackedBy) {
newElement.transform(player); newElement.transform(player);
} }
} }
}
continue outer; continue outer;
} }
} }
@@ -193,11 +227,17 @@ public class CEChunk {
BlockEntityElement newElement = config.create(wrappedWorld, pos); BlockEntityElement newElement = config.create(wrappedWorld, pos);
elements[i] = newElement; elements[i] = newElement;
if (hasTrackedBy) { if (hasTrackedBy) {
if (Config.enableEntityCulling()) {
for (Player player : trackedBy) {
player.addTrackedBlockEntity(pos, renderer);
}
} else {
for (Player player : trackedBy) { for (Player player : trackedBy) {
newElement.show(player); newElement.show(player);
} }
} }
} }
}
if (hasTrackedBy) { if (hasTrackedBy) {
for (int i = 0; i < previousElements.length; i++) { for (int i = 0; i < previousElements.length; i++) {
BlockEntityElement previousElement = previousElements[i]; BlockEntityElement previousElement = previousElements[i];
@@ -214,11 +254,17 @@ public class CEChunk {
elements[i] = renderers[i].create(wrappedWorld, pos); elements[i] = renderers[i].create(wrappedWorld, pos);
} }
if (hasTrackedBy) { if (hasTrackedBy) {
if (Config.enableEntityCulling()) {
for (Player player : trackedBy) {
player.addTrackedBlockEntity(pos, renderer);
}
} else {
for (Player player : trackedBy) { for (Player player : trackedBy) {
renderer.show(player); renderer.show(player);
} }
} }
} }
}
try { try {
this.renderLock.writeLock().lock(); this.renderLock.writeLock().lock();
this.constantBlockEntityRenderers.put(pos, renderer); this.constantBlockEntityRenderers.put(pos, renderer);
@@ -242,11 +288,17 @@ public class CEChunk {
ConstantBlockEntityRenderer removed = this.constantBlockEntityRenderers.remove(pos); ConstantBlockEntityRenderer removed = this.constantBlockEntityRenderers.remove(pos);
if (removed != null) { if (removed != null) {
if (hide) { if (hide) {
if (Config.enableEntityCulling()) {
for (Player player : getTrackedBy()) {
player.removeTrackedBlockEntities(List.of(pos));
}
} else {
for (Player player : getTrackedBy()) { for (Player player : getTrackedBy()) {
removed.hide(player); removed.hide(player);
} }
} }
} }
}
return removed; return removed;
} finally { } finally {
this.renderLock.writeLock().unlock(); this.renderLock.writeLock().unlock();

View File

@@ -32,7 +32,7 @@ public class ClientChunk {
int index = sectionIndex(SectionPos.blockToSectionCoord(y)); int index = sectionIndex(SectionPos.blockToSectionCoord(y));
ClientSection section = this.sections[index]; ClientSection section = this.sections[index];
if (section == null) return; if (section == null) return;
section.setOccluding((y & 15) << 8 | (z & 15) << 4, occluding); section.setOccluding((y & 15) << 8 | (z & 15) << 4 | x & 15, occluding);
} }
public int sectionIndex(int sectionId) { public int sectionIndex(int sectionId) {

View File

@@ -4,7 +4,7 @@ import net.momirealms.craftengine.core.entity.player.Player;
import net.momirealms.craftengine.core.world.Cullable; import net.momirealms.craftengine.core.world.Cullable;
public class VirtualCullableObject { public class VirtualCullableObject {
private final Cullable cullable; public final Cullable cullable;
private boolean isShown; private boolean isShown;
public VirtualCullableObject(Cullable cullable) { public VirtualCullableObject(Cullable cullable) {
@@ -21,6 +21,7 @@ public class VirtualCullableObject {
} }
public void setShown(Player player, boolean shown) { public void setShown(Player player, boolean shown) {
if (this.isShown == shown) return;
this.isShown = shown; this.isShown = shown;
if (shown) { if (shown) {
this.cullable.show(player); this.cullable.show(player);