306 changes: 166 additions & 140 deletions Shared/src/main/java/dev/tr7zw/entityculling/CullTask.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,150 +23,176 @@

public class CullTask implements Runnable {

public boolean requestCull = false;
public boolean requestCull = false;
public boolean disableEntityCulling = false;
public boolean disableBlockEntityCulling = false;

private final OcclusionCullingInstance culling;
private final OcclusionCullingInstance culling;
private final Minecraft client = Minecraft.getInstance();
private final int sleepDelay = EntityCullingModBase.instance.config.sleepDelay;
private final int hitboxLimit = EntityCullingModBase.instance.config.hitboxLimit;
private final Set<BlockEntityType<?>> blockEntityWhitelist;
private final Set<EntityType<?>> entityWhistelist;
public long lastTime = 0;

// reused preallocated vars
private Vec3d lastPos = new Vec3d(0, 0, 0);
private Vec3d aabbMin = new Vec3d(0, 0, 0);
private Vec3d aabbMax = new Vec3d(0, 0, 0);

public CullTask(OcclusionCullingInstance culling, Set<BlockEntityType<?>> blockEntityWhitelist, Set<EntityType<?>> entityWhistelist) {
this.culling = culling;
this.blockEntityWhitelist = blockEntityWhitelist;
this.entityWhistelist = entityWhistelist;
}

@Override
public void run() {
while (client.getGame() != null) { // client.isRunning() returns false at the start?!?
try {
Thread.sleep(sleepDelay);
if (EntityCullingModBase.enabled && client.level != null && client.player != null && client.player.tickCount > 10) {
Vec3 cameraMC = EntityCullingModBase.instance.config.debugMode
private final int sleepDelay = EntityCullingModBase.instance.config.sleepDelay;
private final int hitboxLimit = EntityCullingModBase.instance.config.hitboxLimit;
private final Set<BlockEntityType<?>> blockEntityWhitelist;
private final Set<EntityType<?>> entityWhistelist;
public long lastTime = 0;

// reused preallocated vars
private Vec3d lastPos = new Vec3d(0, 0, 0);
private Vec3d aabbMin = new Vec3d(0, 0, 0);
private Vec3d aabbMax = new Vec3d(0, 0, 0);

public CullTask(OcclusionCullingInstance culling, Set<BlockEntityType<?>> blockEntityWhitelist,
Set<EntityType<?>> entityWhistelist) {
this.culling = culling;
this.blockEntityWhitelist = blockEntityWhitelist;
this.entityWhistelist = entityWhistelist;
}

@Override
public void run() {
while (client.getGame() != null) { // client.isRunning() returns false at the start?!?
try {
Thread.sleep(sleepDelay);
if (EntityCullingModBase.enabled && client.level != null && client.player != null
&& client.player.tickCount > 10) {
Vec3 cameraMC = EntityCullingModBase.instance.config.debugMode
? client.player.getEyePosition(client.getDeltaFrameTime())
: client.gameRenderer.getMainCamera().getPosition();

if (requestCull || !(cameraMC.x == lastPos.x && cameraMC.y == lastPos.y && cameraMC.z == lastPos.z)) {
long start = System.currentTimeMillis();
requestCull = false;
lastPos.set(cameraMC.x, cameraMC.y, cameraMC.z);
Vec3d camera = lastPos;
culling.resetCache();
boolean spectator = client.player.isSpectator();
for (int x = -8; x <= 8; x++) {
for (int z = -8; z <= 8; z++) {
LevelChunk chunk = client.level.getChunk(client.player.chunkPosition().x + x,
client.player.chunkPosition().z + z);
Iterator<Entry<BlockPos, BlockEntity>> iterator = chunk.getBlockEntities().entrySet().iterator();
Entry<BlockPos, BlockEntity> entry;
while(iterator.hasNext()) {
try {
entry = iterator.next();
}catch(NullPointerException | ConcurrentModificationException ex) {
break; // We are not synced to the main thread, so NPE's/CME are allowed here and way less
// overhead probably than trying to sync stuff up for no really good reason
}
if(blockEntityWhitelist.contains(entry.getValue().getType())) {
continue;
}
if(EntityCullingModBase.instance.isDynamicWhitelisted(entry.getValue())) {
continue;
}
Cullable cullable = (Cullable) entry.getValue();
if (!cullable.isForcedVisible()) {
if (spectator) {
cullable.setCulled(false);
continue;
}
BlockPos pos = entry.getKey();
if(closerThan(pos, cameraMC, 64)) { // 64 is the fixed max tile view distance
AABB boundingBox = EntityCullingModBase.instance.setupAABB(entry.getValue(), pos);
if(boundingBox.getXsize() > hitboxLimit || boundingBox.getYsize() > hitboxLimit || boundingBox.getZsize() > hitboxLimit) {
cullable.setCulled(false); // To big to bother to cull
continue;
}
aabbMin.set(boundingBox.minX, boundingBox.minY, boundingBox.minZ);
aabbMax.set(boundingBox.maxX, boundingBox.maxY, boundingBox.maxZ);
boolean visible = culling.isAABBVisible(aabbMin, aabbMax, camera);
cullable.setCulled(!visible);
}
}
}

}
}
Entity entity = null;
Iterator<Entity> iterable = client.level.entitiesForRendering().iterator();
while (iterable.hasNext()) {
try {
entity = iterable.next();
} catch (NullPointerException | ConcurrentModificationException ex) {
break; // We are not synced to the main thread, so NPE's/CME are allowed here and way less
// overhead probably than trying to sync stuff up for no really good reason
}
if(entity == null || !(entity instanceof Cullable)) {
continue; // Not sure how this could happen outside from mixin screwing up the inject into Entity
}
if(entityWhistelist.contains(entity.getType())) {
continue;
}
if(EntityCullingModBase.instance.isDynamicWhitelisted(entity)) {

if (requestCull
|| !(cameraMC.x == lastPos.x && cameraMC.y == lastPos.y && cameraMC.z == lastPos.z)) {
long start = System.currentTimeMillis();
requestCull = false;
lastPos.set(cameraMC.x, cameraMC.y, cameraMC.z);
Vec3d camera = lastPos;
culling.resetCache();
boolean spectator = client.player.isSpectator();
cullBlockEntities(cameraMC, camera, spectator);
cullEntities(cameraMC, camera, spectator);
lastTime = (System.currentTimeMillis() - start);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println("Shutting down culling task!");
}

private void cullEntities(Vec3 cameraMC, Vec3d camera, boolean spectator) {
if(disableEntityCulling) {
return;
}
Entity entity = null;
Iterator<Entity> iterable = client.level.entitiesForRendering().iterator();
while (iterable.hasNext()) {
try {
entity = iterable.next();
} catch (NullPointerException | ConcurrentModificationException ex) {
break; // We are not synced to the main thread, so NPE's/CME are allowed here and way
// less
// overhead probably than trying to sync stuff up for no really good reason
}
if (entity == null || !(entity instanceof Cullable)) {
continue; // Not sure how this could happen outside from mixin screwing up the inject into
// Entity
}
if (entityWhistelist.contains(entity.getType())) {
continue;
}
if (EntityCullingModBase.instance.isDynamicWhitelisted(entity)) {
continue;
}
Cullable cullable = (Cullable) entity;
if (!cullable.isForcedVisible()) {
if (spectator || entity.isCurrentlyGlowing() || isSkippableArmorstand(entity)) {
cullable.setCulled(false);
continue;
}
if (!entity.position().closerThan(cameraMC, EntityCullingModBase.instance.config.tracingDistance)) {
cullable.setCulled(false); // If your entity view distance is larger than tracingDistance just
// render it
continue;
}
AABB boundingBox = entity.getBoundingBoxForCulling();
if (boundingBox.getXsize() > hitboxLimit || boundingBox.getYsize() > hitboxLimit
|| boundingBox.getZsize() > hitboxLimit) {
cullable.setCulled(false); // To big to bother to cull
continue;
}
aabbMin.set(boundingBox.minX, boundingBox.minY, boundingBox.minZ);
aabbMax.set(boundingBox.maxX, boundingBox.maxY, boundingBox.maxZ);
boolean visible = culling.isAABBVisible(aabbMin, aabbMax, camera);
cullable.setCulled(!visible);
}
}
}

private void cullBlockEntities(Vec3 cameraMC, Vec3d camera, boolean spectator) {
if(disableBlockEntityCulling) {
return;
}
for (int x = -8; x <= 8; x++) {
for (int z = -8; z <= 8; z++) {
LevelChunk chunk = client.level.getChunk(client.player.chunkPosition().x + x,
client.player.chunkPosition().z + z);
Iterator<Entry<BlockPos, BlockEntity>> iterator = chunk.getBlockEntities().entrySet().iterator();
Entry<BlockPos, BlockEntity> entry;
while (iterator.hasNext()) {
try {
entry = iterator.next();
} catch (NullPointerException | ConcurrentModificationException ex) {
break; // We are not synced to the main thread, so NPE's/CME are allowed here and way
// less
// overhead probably than trying to sync stuff up for no really good reason
}
if (blockEntityWhitelist.contains(entry.getValue().getType())) {
continue;
}
if (EntityCullingModBase.instance.isDynamicWhitelisted(entry.getValue())) {
continue;
}
Cullable cullable = (Cullable) entry.getValue();
if (!cullable.isForcedVisible()) {
if (spectator) {
cullable.setCulled(false);
continue;
}
BlockPos pos = entry.getKey();
if (closerThan(pos, cameraMC, 64)) { // 64 is the fixed max tile view distance
AABB boundingBox = EntityCullingModBase.instance.setupAABB(entry.getValue(), pos);
if (boundingBox.getXsize() > hitboxLimit || boundingBox.getYsize() > hitboxLimit
|| boundingBox.getZsize() > hitboxLimit) {
cullable.setCulled(false); // To big to bother to cull
continue;
}
Cullable cullable = (Cullable) entity;
if (!cullable.isForcedVisible()) {
if (spectator || entity.isCurrentlyGlowing() || isSkippableArmorstand(entity)) {
cullable.setCulled(false);
continue;
}
if(!entity.position().closerThan(cameraMC, EntityCullingModBase.instance.config.tracingDistance)) {
cullable.setCulled(false); // If your entity view distance is larger than tracingDistance just render it
continue;
}
AABB boundingBox = entity.getBoundingBoxForCulling();
if(boundingBox.getXsize() > hitboxLimit || boundingBox.getYsize() > hitboxLimit || boundingBox.getZsize() > hitboxLimit) {
cullable.setCulled(false); // To big to bother to cull
continue;
}
aabbMin.set(boundingBox.minX, boundingBox.minY, boundingBox.minZ);
aabbMax.set(boundingBox.maxX, boundingBox.maxY, boundingBox.maxZ);
boolean visible = culling.isAABBVisible(aabbMin, aabbMax, camera);
cullable.setCulled(!visible);
}
}
lastTime = (System.currentTimeMillis()-start);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println("Shutting down culling task!");
}

private boolean isSkippableArmorstand(Entity entity) {
if(!EntityCullingModBase.instance.config.skipMarkerArmorStands)return false;
return entity instanceof ArmorStand && ((ArmorStand) entity).isMarker();
}

// Vec3i forward compatibility functions
private static boolean closerThan(BlockPos blockPos, Position position, double d) {
return distSqr(blockPos, position.x(), position.y(), position.z(), true) < d * d;
}

private static double distSqr(BlockPos blockPos, double d, double e, double f, boolean bl) {
double g = bl ? 0.5D : 0.0D;
double h = (double)blockPos.getX() + g - d;
double i = (double)blockPos.getY() + g - e;
double j = (double)blockPos.getZ() + g - f;
return h * h + i * i + j * j;
}
aabbMin.set(boundingBox.minX, boundingBox.minY, boundingBox.minZ);
aabbMax.set(boundingBox.maxX, boundingBox.maxY, boundingBox.maxZ);
boolean visible = culling.isAABBVisible(aabbMin, aabbMax, camera);
cullable.setCulled(!visible);
}
}
}

}
}
}

private boolean isSkippableArmorstand(Entity entity) {
if (!EntityCullingModBase.instance.config.skipMarkerArmorStands)
return false;
return entity instanceof ArmorStand && ((ArmorStand) entity).isMarker();
}

// Vec3i forward compatibility functions
private static boolean closerThan(BlockPos blockPos, Position position, double d) {
return distSqr(blockPos, position.x(), position.y(), position.z(), true) < d * d;
}

private static double distSqr(BlockPos blockPos, double d, double e, double f, boolean bl) {
double g = bl ? 0.5D : 0.0D;
double h = (double) blockPos.getX() + g - d;
double i = (double) blockPos.getY() + g - e;
double j = (double) blockPos.getZ() + g - f;
return h * h + i * i + j * j;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dev.tr7zw.entityculling.mixin;

import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
Expand All @@ -11,14 +12,25 @@
import dev.tr7zw.entityculling.access.Cullable;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderDispatcher;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
import net.minecraft.world.level.block.entity.BlockEntity;

@Mixin(BlockEntityRenderDispatcher.class)
public class BlockEntityRenderDispatcherMixin {
public abstract class BlockEntityRenderDispatcherMixin {

@Inject(method = "Lnet/minecraft/client/renderer/blockentity/BlockEntityRenderDispatcher;render(Lnet/minecraft/world/level/block/entity/BlockEntity;FLcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;)V", at = @At("HEAD"), cancellable = true)
public <E extends BlockEntity> void render(E blockEntity, float f, PoseStack poseStack,
MultiBufferSource multiBufferSource, CallbackInfo info) {
if(EntityCullingModBase.instance.config.skipBlockEntityCulling) {
return;
}
BlockEntityRenderer<E> blockEntityRenderer = getRenderer(blockEntity);
// respect the "shouldRenderOffScreen" method
if (blockEntityRenderer != null && blockEntityRenderer.shouldRenderOffScreen(blockEntity)) {
EntityCullingModBase.instance.renderedBlockEntities++;
return;
}

if (!((Cullable) blockEntity).isForcedVisible() && ((Cullable) blockEntity).isCulled()) {
EntityCullingModBase.instance.skippedBlockEntities++;
info.cancel();
Expand All @@ -27,4 +39,7 @@ public <E extends BlockEntity> void render(E blockEntity, float f, PoseStack pos
EntityCullingModBase.instance.renderedBlockEntities++;
}

@Shadow
public abstract <E extends BlockEntity> BlockEntityRenderer<E> getRenderer(E blockEntity);

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,39 @@
import dev.tr7zw.entityculling.access.Cullable;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.monster.warden.AngerLevel;
import net.minecraft.world.entity.monster.warden.Warden;
import net.minecraft.world.entity.vehicle.AbstractMinecart;

@Mixin(ClientLevel.class)
public class ClientWorldMixin {

private Minecraft mc = Minecraft.getInstance();

@Inject(method = "tickNonPassenger", at = @At("HEAD"), cancellable = true)
public void tickEntity(Entity entity, CallbackInfo info) {
if(!EntityCullingModBase.instance.config.tickCulling) {
if (!EntityCullingModBase.instance.config.tickCulling || EntityCullingModBase.instance.config.skipEntityCulling) {
EntityCullingModBase.instance.tickedEntities++;
return; // disabled
}
// Use abstract minecart instead of whitelist to also catch modded Minecarts
if(entity == mc.player || entity == mc.cameraEntity || entity.isPassenger() || entity.isVehicle() || (entity instanceof AbstractMinecart)) {
if (entity.noCulling || entity == mc.player || entity == mc.cameraEntity || entity.isPassenger() || entity.isVehicle()
|| (entity instanceof AbstractMinecart)) {
EntityCullingModBase.instance.tickedEntities++;
return; // never skip the client tick for the player or entities in vehicles/with passengers
return; // never skip the client tick for the player or entities in vehicles/with
// passengers. Also respect the "noCulling" flag
}
if(EntityCullingModBase.instance.entityWhistelist.contains(entity.getType())) {
if (EntityCullingModBase.instance.entityWhistelist.contains(entity.getType())) {
EntityCullingModBase.instance.tickedEntities++;
return; // whitelisted, don't skip that tick
}
if(entity instanceof Cullable) {
if (entity instanceof Cullable) {
Cullable cull = (Cullable) entity;
if(cull.isCulled() || cull.isOutOfCamera()) {
if (cull.isCulled() || cull.isOutOfCamera()) {
basicTick(entity);
EntityCullingModBase.instance.skippedEntityTicks++;
info.cancel();
Expand All @@ -46,16 +52,35 @@ public void tickEntity(Entity entity, CallbackInfo info) {
}
EntityCullingModBase.instance.tickedEntities++;
}

private void basicTick(Entity entity) {
entity.setOldPosAndRot();
++entity.tickCount;
if(entity instanceof LivingEntity living) {
if (entity instanceof LivingEntity living) {
living.aiStep();
if (living.hurtTime > 0)
living.hurtTime--;
}

// the warden sounds are generated clientside instead of serverside, so simulate that part of the code here.
if (entity instanceof Warden warden) {
if (mc.level.isClientSide() && !warden.isSilent()
&& warden.tickCount % getWardenHeartBeatDelay(warden) == 0) {
mc.level.playLocalSound(warden.getX(), warden.getY(), warden.getZ(), SoundEvents.WARDEN_HEARTBEAT,
warden.getSoundSource(), 5.0F, warden.getVoicePitch(), false);
}
}
}


/**
* Copy of that method, since it's private. No need to use an access widener for
* this
*
* @param warden
* @return
*/
private int getWardenHeartBeatDelay(Warden warden) {
float f = warden.getClientAngerLevel() / AngerLevel.ANGRY.getMinimumAnger();
return 40 - Mth.floor(Mth.clamp(f, 0.0F, 1.0F) * 30.0F);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,13 @@ public List<String> getLeftText(CallbackInfoReturnable<List<String>> callback) {
}
List<String> list = callback.getReturnValue();
list.add("[Culling] Last pass: " + EntityCullingModBase.instance.cullTask.lastTime + "ms");
list.add("[Culling] Rendered Block Entities: " + EntityCullingModBase.instance.renderedBlockEntities + " Skipped: " + EntityCullingModBase.instance.skippedBlockEntities);
list.add("[Culling] Rendered Entities: " + EntityCullingModBase.instance.renderedEntities + " Skipped: " + EntityCullingModBase.instance.skippedEntities);
list.add("[Culling] Ticked Entities: " + lastTickedEntities + " Skipped: " + lastSkippedEntityTicks);
if(!EntityCullingModBase.instance.config.skipBlockEntityCulling) {
list.add("[Culling] Rendered Block Entities: " + EntityCullingModBase.instance.renderedBlockEntities + " Skipped: " + EntityCullingModBase.instance.skippedBlockEntities);
}
if(!EntityCullingModBase.instance.config.skipEntityCulling) {
list.add("[Culling] Rendered Entities: " + EntityCullingModBase.instance.renderedEntities + " Skipped: " + EntityCullingModBase.instance.skippedEntities);
list.add("[Culling] Ticked Entities: " + lastTickedEntities + " Skipped: " + lastSkippedEntityTicks);
}

EntityCullingModBase.instance.renderedBlockEntities = 0;
EntityCullingModBase.instance.skippedBlockEntities = 0;
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,11 @@ public class WorldRendererMixin {
@Inject(at = @At("HEAD"), method = "renderEntity", cancellable = true)
private void renderEntity(Entity entity, double cameraX, double cameraY, double cameraZ, float tickDelta,
PoseStack matrices, MultiBufferSource vertexConsumers, CallbackInfo info) {
if(EntityCullingModBase.instance.config.skipEntityCulling) {
return;
}
Cullable cullable = (Cullable) entity;
if (!cullable.isForcedVisible() && cullable.isCulled()) {
if (!cullable.isForcedVisible() && cullable.isCulled() && !entity.noCulling) {
@SuppressWarnings("unchecked")
EntityRenderer<Entity> entityRenderer = (EntityRenderer<Entity>) entityRenderDispatcher.getRenderer(entity);
@SuppressWarnings("unchecked")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"text.entityculling.title": "EntityCulling",
"key.entityculling.toggle": "Debug toggle culling"
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"text.entityculling.title": "EntityCulling",
"key.entityculling.toggle": "Alternância da depuração do Culling"
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"modmenu.descriptionTranslation.entityculling": "Мод, использующий параллельную трассировку путей для определения видимости сущностей и блоков-сущностей, скрытых за блоками.",
"text.entityculling.title": "EntityCulling",
"key.entityculling.toggle": "Переключить отбраковку"
"text.entityculling.title": "EntityCulling",
"key.entityculling.toggle": "Вкл/выкл отладку отбраковки"
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"text.entityculling.title": "EntityCulling",
"key.entityculling.toggle": "Перемкнути вибракування"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"text.entityculling.title": "EntityCulling",
"key.entityculling.toggle": "開關剔除除錯"
}
3 changes: 1 addition & 2 deletions Shared/src/main/resources/entityculling.mixins.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
"EntityRendererMixin",
"CullableMixin",
"DebugHudMixin",
"ClientWorldMixin",
"DonorAbstractClientPlayerMixin"
"ClientWorldMixin"
],
"injectors": {
"defaultRequire": 1
Expand Down
18 changes: 12 additions & 6 deletions gradle-compose.yml
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
version: '0.0.1'
source: "https://github.com/tr7zw/ForgeFabricComposeTemplate/tree/1.19.3-fabriconly-mcpublish/"
version: '0.0.2'
source: "https://github.com/tr7zw/ModComposeTemplate/tree/1.19.3"
replacements:
group: "dev.tr7zw"
name: "EntityCulling"
id: "entityculling"
version: "1.5.2"
version: "1.6.0"
author: "tr7zw"
relocationpackage: "dev.tr7zw.entityculling"
dependencies: '
include "com.logisticscraft:occlusionculling:0.0.6-SNAPSHOT"
inc "com.logisticscraft:occlusionculling:0.0.7-SNAPSHOT"
'
licensename: "LICENSE-EntityCulling"
modrinthid: NNAgCjsB
curseforgeid: 448233
enabledFlags:
- autopublish
- publishFabric
- publishForge
- modrinth
- curseforge
rootProject:
template: "."
subProjects:
EntityCulling-Fabric:
template: "Fabric"
# EntityCulling-Forge:
# template: "Forge"
EntityCulling-Forge:
template: "Forge"
Binary file modified gradle/gradle-compose.jar
Binary file not shown.