9
0
mirror of https://github.com/Xiao-MoMi/craft-engine.git synced 2026-01-04 15:41:38 +00:00

完成第三人称光线追踪,修复眼睛位置

This commit is contained in:
XiaoMoMi
2025-11-29 21:28:02 +08:00
parent 0b3d52b39b
commit 8d9be05f11
12 changed files with 152 additions and 75 deletions

View File

@@ -8,21 +8,11 @@ import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.command.CraftEngineCommandManager;
import net.momirealms.craftengine.core.plugin.command.FlagKeys;
import net.momirealms.craftengine.core.plugin.locale.MessageConstants;
import net.momirealms.craftengine.core.plugin.locale.TranslationManager;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.incendo.cloud.Command;
import org.incendo.cloud.bukkit.parser.PlayerParser;
import org.incendo.cloud.context.CommandContext;
import org.incendo.cloud.context.CommandInput;
import org.incendo.cloud.parser.standard.DoubleParser;
import org.incendo.cloud.parser.standard.StringParser;
import org.incendo.cloud.suggestion.Suggestion;
import org.incendo.cloud.suggestion.SuggestionProvider;
import java.util.Locale;
import java.util.concurrent.CompletableFuture;
public class SetEntityViewDistanceScaleCommand extends BukkitCommandFeature<CommandSender> {

View File

@@ -84,7 +84,6 @@ import net.momirealms.craftengine.core.plugin.context.NetworkTextReplaceContext;
import net.momirealms.craftengine.core.plugin.context.PlayerOptionalContext;
import net.momirealms.craftengine.core.plugin.context.event.EventTrigger;
import net.momirealms.craftengine.core.plugin.context.parameter.DirectContextParameters;
import net.momirealms.craftengine.core.plugin.entityculling.EntityCullingThread;
import net.momirealms.craftengine.core.plugin.locale.TranslationManager;
import net.momirealms.craftengine.core.plugin.logger.Debugger;
import net.momirealms.craftengine.core.plugin.network.*;
@@ -2036,7 +2035,7 @@ public class BukkitNetworkManager implements NetworkManager, Listener, PluginMes
boolean hasGlobalPalette = false;
// 创建客户端侧世界(只在开启实体情况下创建)
ClientSection[] clientSections = Config.enableEntityCulling() ? new ClientSection[count] : null;
ClientSection[] clientSections = Config.entityCullingRayTracing() ? new ClientSection[count] : null;
for (int i = 0; i < count; i++) {
MCSection mcSection = new MCSection(user.clientBlockList(), this.blockList, this.biomeList);
@@ -2215,7 +2214,7 @@ public class BukkitNetworkManager implements NetworkManager, Listener, PluginMes
// 获取客户端侧区域
ClientSection clientSection = null;
if (Config.enableEntityCulling()) {
if (Config.entityCullingRayTracing()) {
SectionPos sectionPos = SectionPos.of(sPos);
ClientChunk trackedChunk = user.getTrackedChunk(sectionPos.asChunkPos().longKey);
clientSection = trackedChunk.sectionById(sectionPos.y);
@@ -2261,7 +2260,7 @@ public class BukkitNetworkManager implements NetworkManager, Listener, PluginMes
FriendlyByteBuf buf = event.getBuffer();
BlockPos pos = buf.readBlockPos();
int before = buf.readVarInt();
if (Config.enableEntityCulling()) {
if (Config.entityCullingRayTracing()) {
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));
@@ -2441,7 +2440,7 @@ public class BukkitNetworkManager implements NetworkManager, Listener, PluginMes
BlockPos blockPos = buf.readBlockPos();
int state = buf.readInt();
// 移除不透明设置
if (Config.enableEntityCulling()) {
if (Config.entityCullingRayTracing()) {
ClientChunk trackedChunk = user.getTrackedChunk(ChunkPos.asLong(blockPos.x >> 4, blockPos.z >> 4));
if (trackedChunk != null) {
trackedChunk.setOccluding(blockPos.x, blockPos.y, blockPos.z, false);

View File

@@ -52,6 +52,7 @@ import org.bukkit.block.Block;
import org.bukkit.damage.DamageSource;
import org.bukkit.damage.DamageType;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.inventory.EquipmentSlot;
@@ -116,8 +117,6 @@ public class BukkitServerPlayer extends Player {
private IntIdentityList blockList = new IntIdentityList(BlockStateUtils.vanillaBlockStateCount());
// cache if player can break blocks
private boolean clientSideCanBreak = true;
// prevent AFK players from consuming too much CPU resource on predicting
private Location previousEyeLocation;
// a cooldown for better breaking experience
private int lastSuccessfulBreak;
// player's game tick
@@ -145,6 +144,10 @@ public class BukkitServerPlayer extends Player {
// 跟踪到的方块实体渲染器
private final Map<BlockPos, VirtualCullableObject> trackedBlockEntityRenderers = new ConcurrentHashMap<>();
private final EntityCulling culling;
private Vec3d firstPersonCameraVec3;
private Vec3d thirdPersonCameraVec3;
// 玩家眼睛所在位置
private Location eyeLocation;
public BukkitServerPlayer(BukkitCraftEngine plugin, @Nullable Channel channel) {
this.channel = channel;
@@ -512,10 +515,11 @@ public class BukkitServerPlayer extends Player {
@Override
public void tick() {
// not fully online
if (serverPlayer() == null) return;
Object serverPlayer = serverPlayer();
if (serverPlayer == null) return;
org.bukkit.entity.Player bukkitPlayer = platformPlayer();
if (VersionHelper.isFolia()) {
try {
Object serverPlayer = serverPlayer();
Object gameMode = FastNMS.INSTANCE.field$ServerPlayer$gameMode(serverPlayer);
this.gameTicks = (int) CoreReflections.field$ServerPlayerGameMode$gameTicks.get(gameMode);
} catch (ReflectiveOperationException e) {
@@ -527,12 +531,30 @@ public class BukkitServerPlayer extends Player {
if (this.gameTicks % 20 == 0) {
this.updateGUI();
}
// 更新眼睛位置
{
Location unsureEyeLocation = bukkitPlayer.getEyeLocation();
Entity vehicle = bukkitPlayer.getVehicle();
if (vehicle != null) {
Object mountPos = FastNMS.INSTANCE.method$Entity$getPassengerRidingPosition(FastNMS.INSTANCE.method$CraftEntity$getHandle(vehicle), serverPlayer);
unsureEyeLocation.set(FastNMS.INSTANCE.field$Vec3$x(mountPos), FastNMS.INSTANCE.field$Vec3$y(mountPos) + bukkitPlayer.getEyeHeight(), FastNMS.INSTANCE.field$Vec3$z(mountPos));
}
if (Config.predictBreaking() && !this.isDestroyingCustomBlock && !unsureEyeLocation.equals(this.eyeLocation)) {
// if it's not destroying blocks, we do predict
if ((gameTicks() + entityID()) % Config.predictBreakingInterval() == 0) {
this.predictNextBlockToMine();
}
}
this.eyeLocation = unsureEyeLocation;
}
if (hasSwingHand()) {
if (this.isDestroyingBlock) {
this.tickBlockDestroy();
} else if (this.lastStopMiningPos != null && this.gameTicks - this.lastStopMiningTick <= 5) {
double range = getCachedInteractionRange();
RayTraceResult result = platformPlayer().rayTraceBlocks(range, FluidCollisionMode.NEVER);
RayTraceResult result = rayTrace(this.eyeLocation, range, FluidCollisionMode.NEVER);
if (result != null) {
Block hitBlock = result.getHitBlock();
if (hitBlock != null) {
@@ -555,36 +577,57 @@ public class BukkitServerPlayer extends Player {
this.isHackedBreak = false;
}
}
if (Config.predictBreaking() && !this.isDestroyingCustomBlock) {
// if it's not destroying blocks, we do predict
if ((gameTicks() + entityID()) % Config.predictBreakingInterval() == 0) {
Location eyeLocation = platformPlayer().getEyeLocation();
if (eyeLocation.equals(this.previousEyeLocation)) {
return;
if (Config.entityCullingRayTracing()) {
org.bukkit.entity.Player player = platformPlayer();
Location eyeLocation = this.eyeLocation.clone();
this.firstPersonCameraVec3 = LocationUtils.toVec3d(eyeLocation);
int distance = 4;
if (VersionHelper.isOrAbove1_21_6()) {
Entity vehicle = player.getVehicle();
if (vehicle != null && vehicle.getType() == EntityType.HAPPY_GHAST) {
distance = 8;
}
this.previousEyeLocation = eyeLocation;
this.predictNextBlockToMine();
}
this.thirdPersonCameraVec3 = LocationUtils.toVec3d(eyeLocation.subtract(eyeLocation.getDirection().multiply(distance)));
}
}
@Override
public void entityCullingTick() {
this.culling.restoreTokenOnTick();
boolean useRayTracing = Config.entityCullingRayTracing();
for (VirtualCullableObject cullableObject : this.trackedBlockEntityRenderers.values()) {
CullingData cullingData = cullableObject.cullable.cullingData();
if (cullingData != null) {
Vec3d vec3d = LocationUtils.toVec3d(platformPlayer().getEyeLocation());
boolean visible = this.culling.isVisible(cullingData, vec3d);
if (visible != cullableObject.isShown) {
if (Config.enableEntityCullingRateLimiting() && visible) {
if (this.culling.takeToken()) {
cullableObject.setShown(this, true);
}
} else {
cullableObject.setShown(this, visible);
boolean firstPersonVisible = this.culling.isVisible(cullingData, this.firstPersonCameraVec3, useRayTracing);
// 之前可见
if (cullableObject.isShown) {
boolean thirdPersonVisible = this.culling.isVisible(cullingData, this.thirdPersonCameraVec3, useRayTracing);
if (!firstPersonVisible && !thirdPersonVisible) {
cullableObject.setShown(this, false);
}
}
// 之前不可见
else {
// 但是第一人称可见了
if (firstPersonVisible) {
// 下次再说
if (Config.enableEntityCullingRateLimiting() && !this.culling.takeToken()) {
continue;
}
cullableObject.setShown(this, true);
continue;
}
if (this.culling.isVisible(cullingData, this.thirdPersonCameraVec3, useRayTracing)) {
// 下次再说
if (Config.enableEntityCullingRateLimiting() && !this.culling.takeToken()) {
continue;
}
cullableObject.setShown(this, true);
}
// 仍然不可见
}
} else {
cullableObject.setShown(this, true);
}
@@ -607,17 +650,12 @@ public class BukkitServerPlayer extends Player {
public boolean canInteractWithBlock(BlockPos pos, double distance) {
double d = this.getCachedInteractionRange() + distance;
return (new AABB(pos)).distanceToSqr(this.getEyePosition()) < d * d;
return (new AABB(pos)).distanceToSqr(this.getEyePos()) < d * d;
}
public boolean canInteractPoint(Vec3d pos, double distance) {
double d = this.getCachedInteractionRange() + distance;
return Vec3d.distanceToSqr(this.getEyePosition(), pos) < d * d;
}
public final Vec3d getEyePosition() {
Location eyeLocation = this.platformPlayer().getEyeLocation();
return new Vec3d(eyeLocation.getX(), eyeLocation.getY(), eyeLocation.getZ());
return Vec3d.distanceToSqr(this.getEyePos(), pos) < d * d;
}
@Override
@@ -777,7 +815,7 @@ public class BukkitServerPlayer extends Player {
try {
org.bukkit.entity.Player player = platformPlayer();
double range = getCachedInteractionRange();
RayTraceResult result = player.rayTraceBlocks(range, FluidCollisionMode.NEVER);
RayTraceResult result = rayTrace(this.eyeLocation, range, FluidCollisionMode.NEVER);
if (result == null) return;
Block hitBlock = result.getHitBlock();
if (hitBlock == null) return;
@@ -1217,7 +1255,7 @@ public class BukkitServerPlayer extends Player {
@Override
public void removeTrackedChunk(long chunkPos) {
this.trackedChunks.remove(chunkPos);
if (Config.enableEntityCulling()) {
if (Config.entityCullingRayTracing()) {
this.culling.removeLastVisitChunkIfMatches((int) chunkPos, (int) (chunkPos >> 32));
}
}
@@ -1367,4 +1405,36 @@ public class BukkitServerPlayer extends Player {
public void clearTrackedBlockEntities() {
this.trackedBlockEntityRenderers.clear();
}
@Override
public WorldPosition eyePosition() {
return LocationUtils.toWorldPosition(this.getEyeLocation());
}
public Location getEyeLocation() {
org.bukkit.entity.Player player = platformPlayer();
Location eyeLocation = player.getEyeLocation();
Entity vehicle = player.getVehicle();
if (vehicle != null) {
Object mountPos = FastNMS.INSTANCE.method$Entity$getPassengerRidingPosition(FastNMS.INSTANCE.method$CraftEntity$getHandle(vehicle), serverPlayer());
eyeLocation.set(FastNMS.INSTANCE.field$Vec3$x(mountPos), FastNMS.INSTANCE.field$Vec3$y(mountPos) + player.getEyeHeight(), FastNMS.INSTANCE.field$Vec3$z(mountPos));
}
return eyeLocation;
}
public Vec3d getEyePos() {
org.bukkit.entity.Player player = platformPlayer();
Entity vehicle = player.getVehicle();
if (vehicle != null) {
Object mountPos = FastNMS.INSTANCE.method$Entity$getPassengerRidingPosition(FastNMS.INSTANCE.method$CraftEntity$getHandle(vehicle), serverPlayer());
return new Vec3d(FastNMS.INSTANCE.field$Vec3$x(mountPos), FastNMS.INSTANCE.field$Vec3$y(mountPos) + player.getEyeHeight(), FastNMS.INSTANCE.field$Vec3$z(mountPos));
} else {
Location location = player.getLocation();
return new Vec3d(location.getX(), location.getY() + player.getEyeHeight(), location.getZ());
}
}
private RayTraceResult rayTrace(Location start, double range, FluidCollisionMode mode) {
return start.getWorld().rayTraceBlocks(start, start.getDirection(), range, mode);
}
}

View File

@@ -554,11 +554,13 @@ chunk-system:
# [Premium Exclusive]
client-optimization:
# Using server-side ray tracing algorithms to hide block entities/furniture and reduce client-side rendering pressure.
# Requires a restart to fully apply.
entity-culling:
enable: true
view-distance: 64
# Using server-side ray tracing algorithms to hide block entities/furniture and reduce client-side rendering pressure.
ray-tracing: true
# Cull entities based on distance
view-distance: 64 # -1 = no limit
# Determining the number of threads to execute these raytrace operations
threads: 1
# Limit the maximum number of entities with visibility changes per tick for one player

View File

@@ -715,13 +715,14 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
return null;
}
if (!(arguments instanceof Map)) {
return new CullingData(DEFAULT_BLOCK_ENTITY_AABB, Config.entityCullingViewDistance(), 0.5);
return new CullingData(DEFAULT_BLOCK_ENTITY_AABB, Config.entityCullingViewDistance(), 0.5, true);
}
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")
ResourceConfigUtils.getAsDouble(argumentsMap.getOrDefault("aabb-expansion", 0.5), "aabb-expansion"),
ResourceConfigUtils.getAsBoolean(argumentsMap.getOrDefault("ray-tracing", true), "ray-tracing")
);
}

View File

@@ -218,4 +218,6 @@ public abstract class Player extends AbstractEntity implements NetWorkUser {
@Override
public void remove() {
}
public abstract WorldPosition eyePosition();
}

View File

@@ -208,6 +208,7 @@ public class Config {
protected boolean client_optimization$entity_culling$enable;
protected int client_optimization$entity_culling$view_distance;
protected int client_optimization$entity_culling$threads;
protected boolean client_optimization$entity_culling$ray_tracing;
protected boolean client_optimization$entity_culling$rate_limiting$enable;
protected int client_optimization$entity_culling$rate_limiting$bucket_size;
protected int client_optimization$entity_culling$rate_limiting$restore_per_tick;
@@ -579,6 +580,7 @@ public class Config {
}
client_optimization$entity_culling$view_distance = config.getInt("client-optimization.entity-culling.view-distance", 64);
client_optimization$entity_culling$threads = config.getInt("client-optimization.entity-culling.threads", 1);
client_optimization$entity_culling$ray_tracing = client_optimization$entity_culling$enable && config.getBoolean("client-optimization.entity-culling.ray-tracing", true);
client_optimization$entity_culling$rate_limiting$enable = config.getBoolean("client-optimization.entity-culling.rate-limiting.enable", true);
client_optimization$entity_culling$rate_limiting$bucket_size = config.getInt("client-optimization.entity-culling.rate-limiting.bucket-size", 300);
client_optimization$entity_culling$rate_limiting$restore_per_tick = config.getInt("client-optimization.entity-culling.rate-limiting.restore-per-tick", 5);
@@ -1197,6 +1199,10 @@ public class Config {
return instance.client_optimization$entity_culling$rate_limiting$restore_per_tick;
}
public static boolean entityCullingRayTracing() {
return instance.client_optimization$entity_culling$ray_tracing;
}
public YamlDocument loadOrCreateYamlData(String fileName) {
Path path = this.plugin.dataFolderPath().resolve(fileName);
if (!Files.exists(path)) {

View File

@@ -6,22 +6,28 @@ public final class CullingData {
public final AABB aabb;
public final int maxDistance;
public final double aabbExpansion;
public final boolean rayTracing;
public CullingData(AABB aabb, int maxDistance, double aabbExpansion) {
public CullingData(AABB aabb, int maxDistance, double aabbExpansion, boolean rayTracing) {
this.aabb = aabb;
this.maxDistance = maxDistance;
this.aabbExpansion = aabbExpansion;
this.rayTracing = rayTracing;
}
public AABB aabb() {
return aabb;
return this.aabb;
}
public int maxDistance() {
return maxDistance;
return this.maxDistance;
}
public double aabbExpansion() {
return aabbExpansion;
return this.aabbExpansion;
}
public boolean rayTracing() {
return this.rayTracing;
}
}

View File

@@ -16,8 +16,8 @@ public final class EntityCulling {
private final Player player;
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 int[] lastHitBlock = new int[3];
private boolean canCheckLastHitBlock = false;
private int hitBlockCount = 0;
private int lastVisitChunkX = Integer.MAX_VALUE;
private int lastVisitChunkZ = Integer.MAX_VALUE;
@@ -52,9 +52,9 @@ public final class EntityCulling {
return false;
}
public boolean isVisible(CullingData cullable, Vec3d cameraPos) {
public boolean isVisible(CullingData cullable, Vec3d cameraPos, boolean rayTracing) {
// 情空标志位
Arrays.fill(this.canCheckLastHitBlock, false);
this.canCheckLastHitBlock = false;
this.hitBlockCount = 0;
AABB aabb = cullable.aabb;
double aabbExpansion = cullable.aabbExpansion;
@@ -97,6 +97,10 @@ public final class EntityCulling {
}
}
if (!rayTracing || !cullable.rayTracing) {
return true;
}
// 清空之前的缓存
Arrays.fill(this.dotSelectors, false);
if (relX == Relative.POSITIVE) {
@@ -190,7 +194,7 @@ public final class EntityCulling {
int startBlockZ = MiscUtils.floor(start.z);
// 遍历所有目标点进行视线检测
outer: for (int targetIndex = 0; targetIndex < targetCount; targetIndex++) {
for (int targetIndex = 0; targetIndex < targetCount; targetIndex++) {
MutableVec3d currentTarget = targets[targetIndex];
// 计算起点到目标的相对向量(世界坐标差)
@@ -199,14 +203,9 @@ public final class EntityCulling {
double deltaZ = start.z - currentTarget.z;
// 检查之前命中的方块,大概率还是命中
for (int i = 0; i < MAX_SAMPLES; i++) {
if (this.canCheckLastHitBlock[i]) {
int offset = i * 3;
if (rayIntersection(this.lastHitBlock[offset], this.lastHitBlock[offset + 1], this.lastHitBlock[offset + 2], start, new MutableVec3d(deltaX, deltaY, deltaZ).normalize())) {
continue outer;
}
} else {
break;
if (this.canCheckLastHitBlock) {
if (rayIntersection(this.lastHitBlock[0], this.lastHitBlock[1], this.lastHitBlock[2], start, new MutableVec3d(deltaX, deltaY, deltaZ).normalize())) {
continue;
}
}
@@ -292,7 +291,7 @@ public final class EntityCulling {
if (isLineOfSightClear) {
return true;
} else {
this.canCheckLastHitBlock[this.hitBlockCount++] = true;
this.canCheckLastHitBlock = true;
}
}

View File

@@ -2,6 +2,7 @@ package net.momirealms.craftengine.core.plugin.entityculling;
import net.momirealms.craftengine.core.entity.player.Player;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.config.Config;
import net.momirealms.craftengine.core.plugin.logger.Debugger;
import java.util.concurrent.Executors;
@@ -14,6 +15,7 @@ public class EntityCullingThread {
private final AtomicBoolean isRunning = new AtomicBoolean(false);
private final int id;
private final int threads;
private int timer;
public EntityCullingThread(int id, int threads) {
this.id = id;
@@ -35,7 +37,7 @@ public class EntityCullingThread {
this.scheduler.execute(() -> {
try {
int processed = 0;
long startTime = System.currentTimeMillis();
long startTime = System.nanoTime();
for (Player player : CraftEngine.instance().networkManager().onlineUsers()) {
// 使用绝对值确保非负,使用 threads 而不是 threads-1 确保均匀分布
@@ -45,10 +47,10 @@ public class EntityCullingThread {
}
}
long duration = System.currentTimeMillis() - startTime;
if (duration > 45) {
String value = String.format("EntityCullingThread-%d processed %d players in %dms (over 45ms)",
this.id, processed, duration);
long duration = System.nanoTime() - startTime;
if (Config.debugEntityCulling() && this.timer++ % 20 == 0) {
String value = String.format("EntityCullingThread-%d processed %d players in %sms",
this.id, processed, String.format("%.2f", duration / 1_000_000.0));
Debugger.ENTITY_CULLING.debug(() -> value);
}
} catch (Exception e) {

View File

@@ -143,7 +143,7 @@ public class CEChunk {
ConstantBlockEntityRenderer renderer = new ConstantBlockEntityRenderer(
elements,
Optional.ofNullable(state.cullingData())
.map(data -> new CullingData(data.aabb.move(pos), data.maxDistance, data.aabbExpansion))
.map(data -> new CullingData(data.aabb.move(pos), data.maxDistance, data.aabbExpansion, data.rayTracing))
.orElse(null)
);
World wrappedWorld = this.world.world();

View File

@@ -48,7 +48,7 @@ byte_buddy_version=1.18.1
ahocorasick_version=0.6.3
snake_yaml_version=2.5
anti_grief_version=1.0.5
nms_helper_version=1.0.137
nms_helper_version=1.0.138
evalex_version=3.5.0
reactive_streams_version=1.0.4
amazon_awssdk_version=2.38.7