9
0
mirror of https://github.com/Xiao-MoMi/craft-engine.git synced 2025-12-28 19:39:11 +00:00

优化实体剔除

This commit is contained in:
XiaoMoMi
2025-11-29 01:46:31 +08:00
parent 399a01cd1b
commit c59478e891
16 changed files with 270 additions and 50 deletions

View File

@@ -31,6 +31,7 @@ import net.momirealms.craftengine.core.plugin.context.Context;
import net.momirealms.craftengine.core.plugin.context.event.EventFunctions;
import net.momirealms.craftengine.core.plugin.context.event.EventTrigger;
import net.momirealms.craftengine.core.plugin.context.function.Function;
import net.momirealms.craftengine.core.plugin.entityculling.CullingData;
import net.momirealms.craftengine.core.plugin.locale.LocalizedException;
import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigException;
import net.momirealms.craftengine.core.plugin.logger.Debugger;
@@ -38,6 +39,7 @@ import net.momirealms.craftengine.core.registry.BuiltInRegistries;
import net.momirealms.craftengine.core.registry.Holder;
import net.momirealms.craftengine.core.registry.WritableRegistry;
import net.momirealms.craftengine.core.util.*;
import net.momirealms.craftengine.core.world.collision.AABB;
import net.momirealms.sparrow.nbt.CompoundTag;
import org.incendo.cloud.suggestion.Suggestion;
import org.jetbrains.annotations.NotNull;
@@ -52,6 +54,7 @@ import java.util.concurrent.ExecutionException;
public abstract class AbstractBlockManager extends AbstractModelGenerator implements BlockManager {
private static final JsonElement EMPTY_VARIANT_MODEL = MiscUtils.init(new JsonObject(), o -> o.addProperty("model", "minecraft:block/empty"));
private static final AABB DEFAULT_BLOCK_ENTITY_AABB = new AABB(-.5, -.5, -.5, .5, .5, .5);
protected final BlockParser blockParser;
protected final BlockStateMappingParser blockStateMappingParser;
// 根据id获取自定义方块
@@ -603,7 +606,7 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
BlockStateAppearance blockStateAppearance = new BlockStateAppearance(
visualBlockState,
parseBlockEntityRender(appearanceSection.get("entity-renderer")),
ResourceConfigUtils.getAsAABB(appearanceSection.getOrDefault("aabb", 1), "aabb")
parseCullingData(appearanceSection.get("entity-culling"))
);
appearances.put(appearanceName, blockStateAppearance);
if (anyAppearance == null) {
@@ -643,7 +646,7 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
}
for (ImmutableBlockState possibleState : possibleStates) {
possibleState.setVisualBlockState(appearance.blockState());
possibleState.setEstimatedBoundingBox(appearance.estimateAABB());
possibleState.setCullingData(appearance.cullingData());
appearance.blockEntityRenderer().ifPresent(possibleState::setConstantRenderers);
}
}
@@ -667,7 +670,7 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
if (visualState == null) {
visualState = anyAppearance.blockState();
state.setVisualBlockState(visualState);
state.setEstimatedBoundingBox(anyAppearance.estimateAABB());
state.setCullingData(anyAppearance.cullingData());
anyAppearance.blockEntityRenderer().ifPresent(state::setConstantRenderers);
}
int appearanceId = visualState.registryId();
@@ -707,6 +710,21 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
}, () -> GsonHelper.get().toJson(section)));
}
private CullingData parseCullingData(Object arguments) {
if (arguments instanceof Boolean b && !b) {
return null;
}
if (!(arguments instanceof Map)) {
return new CullingData(DEFAULT_BLOCK_ENTITY_AABB, Config.entityCullingViewDistance(), 0.5);
}
Map<String, Object> argumentsMap = ResourceConfigUtils.getAsMap(arguments, "entity-culling");
return new CullingData(
ResourceConfigUtils.getAsAABB(argumentsMap.getOrDefault("aabb", 1), "aabb"),
ResourceConfigUtils.getAsInt(argumentsMap.getOrDefault("view-distance", Config.entityCullingViewDistance()), "view-distance"),
ResourceConfigUtils.getAsDouble(argumentsMap.getOrDefault("aabb-expansion", 0.5), "aabb-expansion")
);
}
@SuppressWarnings("unchecked")
private Optional<BlockEntityElementConfig<? extends BlockEntityElement>[]> parseBlockEntityRender(Object arguments) {
if (arguments == null) return Optional.empty();

View File

@@ -2,11 +2,12 @@ package net.momirealms.craftengine.core.block;
import net.momirealms.craftengine.core.block.entity.render.element.BlockEntityElement;
import net.momirealms.craftengine.core.block.entity.render.element.BlockEntityElementConfig;
import net.momirealms.craftengine.core.world.collision.AABB;
import net.momirealms.craftengine.core.plugin.entityculling.CullingData;
import org.jetbrains.annotations.Nullable;
import java.util.Optional;
public record BlockStateAppearance(BlockStateWrapper blockState,
Optional<BlockEntityElementConfig<? extends BlockEntityElement>[]> blockEntityRenderer,
AABB estimateAABB) {
@Nullable CullingData cullingData) {
}

View File

@@ -13,11 +13,11 @@ import net.momirealms.craftengine.core.item.Item;
import net.momirealms.craftengine.core.loot.LootTable;
import net.momirealms.craftengine.core.plugin.context.ContextHolder;
import net.momirealms.craftengine.core.plugin.context.parameter.DirectContextParameters;
import net.momirealms.craftengine.core.plugin.entityculling.CullingData;
import net.momirealms.craftengine.core.registry.Holder;
import net.momirealms.craftengine.core.util.MiscUtils;
import net.momirealms.craftengine.core.world.CEWorld;
import net.momirealms.craftengine.core.world.World;
import net.momirealms.craftengine.core.world.collision.AABB;
import net.momirealms.sparrow.nbt.CompoundTag;
import net.momirealms.sparrow.nbt.NBT;
import net.momirealms.sparrow.nbt.Tag;
@@ -43,7 +43,8 @@ public final class ImmutableBlockState {
private BlockEntityType<? extends BlockEntity> blockEntityType;
@Nullable
private BlockEntityElementConfig<? extends BlockEntityElement>[] renderers;
private AABB estimatedBoundingBox;
@Nullable
private CullingData cullingData;
ImmutableBlockState(
Holder.Reference<CustomBlock> owner,
@@ -89,12 +90,13 @@ public final class ImmutableBlockState {
this.renderers = renderers;
}
public void setEstimatedBoundingBox(AABB aabb) {
this.estimatedBoundingBox = aabb;
@Nullable
public CullingData cullingData() {
return cullingData;
}
public AABB estimatedBoundingBox() {
return estimatedBoundingBox;
public void setCullingData(@Nullable CullingData cullingData) {
this.cullingData = cullingData;
}
public boolean hasBlockEntity() {

View File

@@ -2,18 +2,19 @@ package net.momirealms.craftengine.core.block.entity.render;
import net.momirealms.craftengine.core.block.entity.render.element.BlockEntityElement;
import net.momirealms.craftengine.core.entity.player.Player;
import net.momirealms.craftengine.core.plugin.entityculling.CullingData;
import net.momirealms.craftengine.core.world.Cullable;
import net.momirealms.craftengine.core.world.collision.AABB;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
@ApiStatus.Experimental
public class ConstantBlockEntityRenderer implements Cullable {
private final BlockEntityElement[] elements;
public final AABB aabb;
public final CullingData cullingData;
public ConstantBlockEntityRenderer(BlockEntityElement[] elements, AABB aabb) {
public ConstantBlockEntityRenderer(BlockEntityElement[] elements, @Nullable CullingData cullingData) {
this.elements = elements;
this.aabb = aabb;
this.cullingData = cullingData;
}
@Override
@@ -55,7 +56,11 @@ public class ConstantBlockEntityRenderer implements Cullable {
}
@Override
public AABB aabb() {
return this.aabb;
public CullingData cullingData() {
return this.cullingData;
}
public boolean canCull() {
return this.cullingData != null;
}
}

View File

@@ -203,7 +203,7 @@ public abstract class Player extends AbstractEntity implements NetWorkUser {
public abstract void addTrackedBlockEntities(Map<BlockPos, ConstantBlockEntityRenderer> renders);
public abstract void addTrackedBlockEntity(BlockPos blockPos, ConstantBlockEntityRenderer renderer);
public abstract VirtualCullableObject addTrackedBlockEntity(BlockPos blockPos, ConstantBlockEntityRenderer renderer);
public abstract VirtualCullableObject getTrackedBlockEntity(BlockPos blockPos);

View File

@@ -204,6 +204,7 @@ public class Config {
protected int emoji$max_emojis_per_parse;
protected boolean client_optimization$entity_culling$enable;
protected int client_optimization$entity_culling$view_distance;
public Config(CraftEngine plugin) {
this.plugin = plugin;
@@ -566,6 +567,7 @@ public class Config {
// client optimization
client_optimization$entity_culling$enable = config.getBoolean("client-optimization.entity-culling.enable", false);
client_optimization$entity_culling$view_distance = config.getInt("client-optimization.entity-culling.view-distance", 64);
firstTime = false;
}
@@ -1161,6 +1163,10 @@ public class Config {
return instance.client_optimization$entity_culling$enable;
}
public static int entityCullingViewDistance() {
return instance.client_optimization$entity_culling$view_distance;
}
public YamlDocument loadOrCreateYamlData(String fileName) {
Path path = this.plugin.dataFolderPath().resolve(fileName);
if (!Files.exists(path)) {

View File

@@ -0,0 +1,27 @@
package net.momirealms.craftengine.core.plugin.entityculling;
import net.momirealms.craftengine.core.world.collision.AABB;
public final class CullingData {
public final AABB aabb;
public final int maxDistance;
public final double aabbExpansion;
public CullingData(AABB aabb, int maxDistance, double aabbExpansion) {
this.aabb = aabb;
this.maxDistance = maxDistance;
this.aabbExpansion = aabbExpansion;
}
public AABB aabb() {
return aabb;
}
public int maxDistance() {
return maxDistance;
}
public double aabbExpansion() {
return aabbExpansion;
}
}

View File

@@ -13,38 +13,42 @@ import java.util.Arrays;
public final class EntityCulling {
public static final int MAX_SAMPLES = 14;
private final Player player;
private final int maxDistance;
private final double aabbExpansion;
private final boolean[] dotSelectors = new boolean[MAX_SAMPLES];
private final MutableVec3d[] targetPoints = new MutableVec3d[MAX_SAMPLES];
private final int[] lastHitBlock = new int[MAX_SAMPLES * 3];
private final boolean[] canCheckLastHitBlock = new boolean[MAX_SAMPLES];
// 相机附近的点位大概率会多次重复获取
private final long[] occlusionCache = new long[32 * 32 * 32 / 32];
private int hitBlockCount = 0;
private int lastVisitChunkX = Integer.MAX_VALUE;
private int lastVisitChunkZ = Integer.MAX_VALUE;
private ClientChunk lastVisitChunk = null;
public EntityCulling(Player player, int maxDistance, double aabbExpansion) {
public EntityCulling(Player player) {
this.player = player;
this.maxDistance = maxDistance;
this.aabbExpansion = aabbExpansion;
for (int i = 0; i < MAX_SAMPLES; i++) {
this.targetPoints[i] = new MutableVec3d(0,0,0);
}
}
public boolean isVisible(AABB aabb, Vec3d cameraPos) {
public void resetCache() {
Arrays.fill(this.occlusionCache, 0);
}
public boolean isVisible(CullingData cullable, Vec3d cameraPos, Vec3d playerHeadPos) {
// 情空标志位
Arrays.fill(this.canCheckLastHitBlock, false);
this.hitBlockCount = 0;
AABB aabb = cullable.aabb;
double aabbExpansion = cullable.aabbExpansion;
// 根据AABB获取能包裹此AABB的最小长方体
int minX = MiscUtils.floor(aabb.minX - this.aabbExpansion);
int minY = MiscUtils.floor(aabb.minY - this.aabbExpansion);
int minZ = MiscUtils.floor(aabb.minZ - this.aabbExpansion);
int maxX = MiscUtils.ceil(aabb.maxX + this.aabbExpansion);
int maxY = MiscUtils.ceil(aabb.maxY + this.aabbExpansion);
int maxZ = MiscUtils.ceil(aabb.maxZ + this.aabbExpansion);
int minX = MiscUtils.floor(aabb.minX - aabbExpansion);
int minY = MiscUtils.floor(aabb.minY - aabbExpansion);
int minZ = MiscUtils.floor(aabb.minZ - aabbExpansion);
int maxX = MiscUtils.ceil(aabb.maxX + aabbExpansion);
int maxY = MiscUtils.ceil(aabb.maxY + aabbExpansion);
int maxZ = MiscUtils.ceil(aabb.maxZ + aabbExpansion);
double cameraX = cameraPos.x;
double cameraY = cameraPos.y;
@@ -60,7 +64,8 @@ public final class EntityCulling {
}
// 如果设置了最大距离
if (this.maxDistance > 0) {
int maxDistance = cullable.maxDistance;
if (maxDistance > 0) {
// 计算AABB到相机的最小距离
double distanceSq = 0.0;
// 计算XYZ轴方向的距离
@@ -68,7 +73,7 @@ public final class EntityCulling {
distanceSq += distanceSq(minY, maxY, cameraY, relY);
distanceSq += distanceSq(minZ, maxZ, cameraZ, relZ);
// 检查距离是否超过最大值
double maxDistanceSq = this.maxDistance * this.maxDistance;
double maxDistanceSq = maxDistance * maxDistance;
// 超过最大距离,剔除
if (distanceSq > maxDistanceSq) {
return false;
@@ -277,21 +282,76 @@ public final class EntityCulling {
return false;
}
private boolean stepRay(int currentBlockX, int currentBlockY, int currentBlockZ,
// 0 = 没缓存
// 1 = 没遮挡
// 2 = 有遮挡
private int getCacheState(int index) {
int arrayIndex = index / 32; // 获取数组下标
int offset = (index % 32) * 2; // 每个状态占2bit计算位偏移
if (arrayIndex >= 0 && arrayIndex < this.occlusionCache.length) {
long cacheValue = this.occlusionCache[arrayIndex];
// 提取2bit的状态值 (0-3)
return (int) ((cacheValue >> offset) & 0b11);
}
return 0; // 索引越界时返回默认状态
}
private void setCacheState(int index, int state) {
long[] cache = this.occlusionCache;
int arrayIndex = index / 32;
int offset = (index % 32) * 2;
if (arrayIndex >= 0 && arrayIndex < cache.length) {
long mask = ~(0b11L << offset); // 创建清除掩码
long newValue = (cache[arrayIndex] & mask) | ((state & 0b11L) << offset);
cache[arrayIndex] = newValue;
}
}
private boolean stepRay(int startingX, int startingY, int startingZ,
double stepSizeX, double stepSizeY, double stepSizeZ,
int remainingSteps, int stepDirectionX, int stepDirectionY,
int stepDirectionZ, double nextStepTimeY, double nextStepTimeX,
double nextStepTimeZ) {
int remainingSteps,
int stepDirectionX, int stepDirectionY, int stepDirectionZ,
double nextStepTimeY, double nextStepTimeX, double nextStepTimeZ) {
int currentBlockX = startingX;
int currentBlockY = startingY;
int currentBlockZ = startingZ;
// 遍历射线路径上的所有方块(跳过最后一个目标方块)
for (; remainingSteps > 1; remainingSteps--) {
int cacheIndex = getCacheIndex(currentBlockX, currentBlockY, currentBlockZ, startingX, startingY, startingZ);
if (cacheIndex != -1) {
int cacheState = getCacheState(cacheIndex);
// 没遮挡
if (cacheState == 1) {
continue;
}
// 有遮挡
else if (cacheState == 2) {
this.lastHitBlock[this.hitBlockCount * 3] = currentBlockX;
this.lastHitBlock[this.hitBlockCount * 3 + 1] = currentBlockY;
this.lastHitBlock[this.hitBlockCount * 3 + 2] = currentBlockZ;
return false;
}
}
// 检查当前方块是否遮挡视线
if (isOccluding(currentBlockX, currentBlockY, currentBlockZ)) {
this.lastHitBlock[this.hitBlockCount * 3] = currentBlockX;
this.lastHitBlock[this.hitBlockCount * 3 + 1] = currentBlockY;
this.lastHitBlock[this.hitBlockCount * 3 + 2] = currentBlockZ;
// 设置缓存
if (cacheIndex != -1) {
setCacheState(cacheIndex, 2);
}
return false; // 视线被遮挡,立即返回
} else {
// 设置缓存
if (cacheIndex != -1) {
setCacheState(cacheIndex, 1);
}
}
// 基于时间参数选择下一个要遍历的方块方向
@@ -315,6 +375,22 @@ public final class EntityCulling {
return true;
}
private int getCacheIndex(int x, int y, int z, int startX, int startY, int startZ) {
int deltaX = startX + 16 - x;
if (deltaX < 0 || deltaX >= 32) {
return -1;
}
int deltaY = startY + 16 - y;
if (deltaY < 0 || deltaY >= 32) {
return -1;
}
int deltaZ = startZ + 16 - z;
if (deltaZ < 0 || deltaZ >= 32) {
return -1;
}
return deltaX + 32 * deltaY + 32 * 32 * deltaZ;
}
private double distanceSq(int min, int max, double camera, Relative rel) {
if (rel == Relative.NEGATIVE) {
double dx = camera - max;

View File

@@ -6,6 +6,7 @@ import java.lang.Thread.UncaughtExceptionHandler;
import java.util.Arrays;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@@ -46,6 +47,13 @@ public abstract class AbstractJavaScheduler<T> implements SchedulerAdapter<T> {
return new AsyncTask(future);
}
@Override
public SchedulerTask asyncRepeating(Consumer<SchedulerTask> task, long delay, long interval, TimeUnit unit) {
LazyAsyncTask asyncTask = new LazyAsyncTask();
asyncTask.future = this.scheduler.scheduleAtFixedRate(() -> this.worker.execute(() -> task.accept(asyncTask)), delay, interval, unit);
return asyncTask;
}
@Override
public void shutdownScheduler() {
this.scheduler.shutdown();

View File

@@ -0,0 +1,21 @@
package net.momirealms.craftengine.core.plugin.scheduler;
import java.util.concurrent.ScheduledFuture;
public class LazyAsyncTask implements SchedulerTask {
public ScheduledFuture<?> future;
@Override
public void cancel() {
if (future != null) {
future.cancel(false);
}
}
@Override
public boolean cancelled() {
if (future == null) return false;
return future.isCancelled();
}
}

View File

@@ -2,6 +2,7 @@ package net.momirealms.craftengine.core.plugin.scheduler;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
public interface SchedulerAdapter<W> {
@@ -25,6 +26,8 @@ public interface SchedulerAdapter<W> {
SchedulerTask asyncRepeating(Runnable task, long delay, long interval, TimeUnit unit);
SchedulerTask asyncRepeating(Consumer<SchedulerTask> task, long delay, long interval, TimeUnit unit);
void shutdownScheduler();
void shutdownExecutor();

View File

@@ -1,13 +1,15 @@
package net.momirealms.craftengine.core.world;
import net.momirealms.craftengine.core.entity.player.Player;
import net.momirealms.craftengine.core.world.collision.AABB;
import net.momirealms.craftengine.core.plugin.entityculling.CullingData;
import org.jetbrains.annotations.Nullable;
public interface Cullable {
AABB aabb();
void show(Player player);
void hide(Player player);
@Nullable
CullingData cullingData();
}

View File

@@ -12,6 +12,7 @@ import net.momirealms.craftengine.core.block.entity.render.element.BlockEntityEl
import net.momirealms.craftengine.core.block.entity.tick.*;
import net.momirealms.craftengine.core.entity.player.Player;
import net.momirealms.craftengine.core.plugin.config.Config;
import net.momirealms.craftengine.core.plugin.entityculling.CullingData;
import net.momirealms.craftengine.core.plugin.logger.Debugger;
import net.momirealms.craftengine.core.world.*;
import net.momirealms.craftengine.core.world.chunk.client.VirtualCullableObject;
@@ -139,7 +140,12 @@ public class CEChunk {
BlockEntityElementConfig<? extends BlockEntityElement>[] renderers = state.constantRenderers();
if (renderers != null && renderers.length > 0) {
BlockEntityElement[] elements = new BlockEntityElement[renderers.length];
ConstantBlockEntityRenderer renderer = new ConstantBlockEntityRenderer(elements, state.estimatedBoundingBox().move(pos));
ConstantBlockEntityRenderer renderer = new ConstantBlockEntityRenderer(
elements,
Optional.ofNullable(state.cullingData())
.map(data -> new CullingData(data.aabb.move(pos), data.maxDistance, data.aabbExpansion))
.orElse(null)
);
World wrappedWorld = this.world.world();
List<Player> trackedBy = getTrackedBy();
boolean hasTrackedBy = trackedBy != null && !trackedBy.isEmpty();
@@ -159,7 +165,7 @@ public class CEChunk {
// 如果启用实体剔除,那么只对已经渲染的进行变换
if (Config.enableEntityCulling()) {
for (Player player : trackedBy) {
VirtualCullableObject trackedBlockEntity = player.getTrackedBlockEntity(pos);
VirtualCullableObject trackedBlockEntity = player.addTrackedBlockEntity(pos, renderer);
if (trackedBlockEntity == null || trackedBlockEntity.isShown()) {
element.transform(player);
}
@@ -207,7 +213,7 @@ public class CEChunk {
// 如果启用实体剔除,那么只对已经渲染的进行变换
if (Config.enableEntityCulling()) {
for (Player player : trackedBy) {
VirtualCullableObject trackedBlockEntity = player.getTrackedBlockEntity(pos);
VirtualCullableObject trackedBlockEntity = player.addTrackedBlockEntity(pos, renderer);
if (trackedBlockEntity == null || trackedBlockEntity.isShown()) {
newElement.transform(player);
}