From f0757296f8f25ab08519519446530a50b0675631 Mon Sep 17 00:00:00 2001 From: James Seibel Date: Sat, 17 Jan 2026 16:12:06 -0600 Subject: [PATCH] Add dynamic overdraw distance based on camera speed --- .../core/api/internal/ClientApi.java | 87 ++++++++++++++++--- .../distanthorizons/core/config/Config.java | 35 +++++--- .../core/render/RenderBufferHandler.java | 1 - .../distanthorizons/core/util/RenderUtil.java | 51 +++++++++-- .../assets/distanthorizons/lang/en_us.json | 6 +- core/src/main/resources/shaders/fog/fog.frag | 10 ++- 6 files changed, 157 insertions(+), 33 deletions(-) diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientApi.java b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientApi.java index 362621d6a..5a2dde41f 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientApi.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientApi.java @@ -32,8 +32,9 @@ import com.seibel.distanthorizons.core.pos.DhChunkPos; import com.seibel.distanthorizons.core.render.DhApiRenderProxy; import com.seibel.distanthorizons.core.render.renderer.*; import com.seibel.distanthorizons.core.util.TimerUtil; +import com.seibel.distanthorizons.core.util.math.Vec3d; import com.seibel.distanthorizons.core.util.objects.Pair; -import com.seibel.distanthorizons.core.util.threading.PriorityTaskPicker; +import com.seibel.distanthorizons.core.util.objects.RollingAverage; import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper; import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector; @@ -93,6 +94,13 @@ public class ClientApi */ public static final DhRenderState RENDER_STATE = new DhRenderState(); + /** + * 50ms = 20 FPS + * @link https://fpstoms.com/ + * @see ClientApi#cameraSpeedRollingAverage + */ + private static final long MIN_MS_BETWEEN_SPEED_CHECKS = 50; + private boolean isDevBuildMessagePrinted = false; private boolean lowMemoryWarningPrinted = false; @@ -120,6 +128,21 @@ public class ClientApi public String lastRenderParamValidationMessage = null; + /** + * measured in blocks/second
+ * + * The number of points tracked here is related + * to the rate at which we check for speed. + * So if the ms_between is changed the number of points + * tracked should also be to keep the ratio roughly the same. + * @see ClientApi#MIN_MS_BETWEEN_SPEED_CHECKS + */ + public RollingAverage cameraSpeedRollingAverage = new RollingAverage(40); + private Vec3d lastCameraPosForSpeedCheck = new Vec3d(); + private long msSinceLastSpeedCheck = 0L; + + + //==============// // constructors // @@ -384,12 +407,6 @@ public class ClientApi private void renderLodLayer(boolean renderingDeferredLayer) { - //=========// - // logging // - //=========// - - this.sendQueuedChatMessages(); - IProfilerWrapper profiler = MC_CLIENT.getProfiler(); profiler.pop(); // get out of "terrain" profiler.push("DH-RenderLevel"); @@ -399,12 +416,27 @@ public class ClientApi //=====================// // render thread tasks // //=====================// + ///region // only run these tasks once per frame if (!renderingDeferredLayer) { profiler.push("DH render thread tasks"); + + + //===============// + // chat messages // + //===============// + + this.sendQueuedChatMessages(); + + + + //======================// + // GL Proxy queued jobs // + //======================// + try { // make sure the GLProxy is created for future use @@ -418,14 +450,41 @@ public class ClientApi LOGGER.error("Unexpected issue running render thread tasks, error: [" + e.getMessage() + "].", e); } + + + //==============// + // camera speed // + //==============// + + long nowMs = System.currentTimeMillis(); + if (this.msSinceLastSpeedCheck + MIN_MS_BETWEEN_SPEED_CHECKS < nowMs) + { + // calc time since last check + double secSinceLastCheck = (nowMs - this.msSinceLastSpeedCheck) / 1_000.0; + this.msSinceLastSpeedCheck = nowMs; + + // get the distance traveled since last frame + Vec3d camPos = MC_RENDER.getCameraExactPosition(); + double distanceInBlocks = camPos.getDistance(this.lastCameraPosForSpeedCheck); + double speed = distanceInBlocks / secSinceLastCheck; + + // record new values for next check + this.cameraSpeedRollingAverage.add(speed); + this.lastCameraPosForSpeedCheck = camPos; + } + + profiler.pop(); } + ///endregion + //=================// // parameter setup // //=================// + ///region EDhApiRenderPass renderPass; if (DhApiRenderProxy.INSTANCE.getDeferTransparentRendering()) @@ -451,16 +510,19 @@ public class ClientApi RenderParams renderParams = new RenderParams( renderPass, - RENDER_STATE.frameTime, + RENDER_STATE.partialTickTime, RENDER_STATE.mcProjectionMatrix, RENDER_STATE.mcModelViewMatrix, RENDER_STATE.clientLevelWrapper ); + ///endregion + //============// // validation // //============// + ///region // TODO write this message to the F3 menu so people can see when a different mod screws with the lightmap String validationMessage = renderParams.getValidationErrorMessage(); @@ -487,11 +549,14 @@ public class ClientApi return; } + ///endregion + //===========// // rendering // //===========// + ///region try { @@ -545,6 +610,8 @@ public class ClientApi MC_CLIENT.sendChatMessage(MinecraftTextFormat.DARK_RED + "Error: " + MinecraftTextFormat.CLEAR_FORMATTING + e); } + ///endregion + profiler.pop(); // end LOD @@ -578,7 +645,7 @@ public class ClientApi // don't fade when Iris shaders are active, otherwise the rendering can get weird && !DhApiRenderProxy.INSTANCE.getDeferTransparentRendering()) { - VanillaFadeRenderer.INSTANCE.render(RENDER_STATE.mcModelViewMatrix, RENDER_STATE.mcProjectionMatrix, RENDER_STATE.frameTime, RENDER_STATE.clientLevelWrapper); + VanillaFadeRenderer.INSTANCE.render(RENDER_STATE.mcModelViewMatrix, RENDER_STATE.mcProjectionMatrix, RENDER_STATE.partialTickTime, RENDER_STATE.clientLevelWrapper); } } /** @@ -602,7 +669,7 @@ public class ClientApi && !DhApiRenderProxy.INSTANCE.getDeferTransparentRendering(); if (renderFade) { - VanillaFadeRenderer.INSTANCE.render(RENDER_STATE.mcModelViewMatrix, RENDER_STATE.mcProjectionMatrix, RENDER_STATE.frameTime, RENDER_STATE.clientLevelWrapper); + VanillaFadeRenderer.INSTANCE.render(RENDER_STATE.mcModelViewMatrix, RENDER_STATE.mcProjectionMatrix, RENDER_STATE.partialTickTime, RENDER_STATE.clientLevelWrapper); } } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/config/Config.java b/core/src/main/java/com/seibel/distanthorizons/core/config/Config.java index 6da68d6e6..f53049ca1 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/config/Config.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/config/Config.java @@ -713,21 +713,32 @@ public class Config public static ConfigUIComment cullingHeader = new ConfigUIComment.Builder().setParentConfigClass(Culling.class).build(); public static ConfigEntry overdrawPrevention = new ConfigEntry.Builder() - .setMinDefaultMax(0.0, 0.0, 1.0) // TODO change -1 to auto + .setMinDefaultMax(-1.0, -1.0, 1.0) .comment("" - + "Determines how far from the camera Distant Horizons will start rendering. \n" - + "Measured as a percentage of the vanilla render distance.\n" - + "\n" - + "0 = auto, overdraw will change based on the vanilla render distance.\n" - + "\n" - + "Higher values will prevent LODs from rendering behind vanilla blocks at a higher distance,\n" - + "but may cause holes in the world. \n" - + "Holes are most likely to appear when flying through unloaded terrain. \n" - + "\n" - + "Increasing the vanilla render distance increases the effectiveness of this setting." - + "") + + "Determines how far from the camera Distant Horizons will start rendering. \n" + + "Measured as a percentage of the vanilla render distance.\n" + + "\n" + + "-1 = auto, overdraw will change based on the vanilla render distance.\n" + + "\n" + + "Higher values will prevent LODs from rendering behind vanilla blocks at a higher distance,\n" + + "but may cause holes in the world. \n" + + "Holes are most likely to appear when flying through unloaded terrain. \n" + + "\n" + + "Increasing the vanilla render distance increases the effectiveness of this setting." + + "") .build(); + public static ConfigEntry reduceOverdrawWithFastMovement = new ConfigEntry.Builder() + .set(true) + .comment("" + + "If set to true the overdraw prevention radius will get closer\n" + + "to the camera when flying/moving quickly.\n" + + "\n" + + "This helps reduce issues where Minecraft can't load or\n" + + "generate chunks fast enough to keep up with DH.\n" + + "") + .build(); + public static ConfigEntry enableCaveCulling = new ConfigEntry.Builder() .set(true) .comment("" diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/RenderBufferHandler.java b/core/src/main/java/com/seibel/distanthorizons/core/render/RenderBufferHandler.java index 46624efe1..643a9d791 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/RenderBufferHandler.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/RenderBufferHandler.java @@ -42,7 +42,6 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.modAccessor.IIrisAccess import com.seibel.distanthorizons.coreapi.interfaces.dependencyInjection.IOverrideInjector; import com.seibel.distanthorizons.core.util.math.Mat4f; import com.seibel.distanthorizons.core.util.math.Vec3d; -import org.jetbrains.annotations.Nullable; import org.joml.Matrix4f; import org.joml.Matrix4fc; diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/RenderUtil.java b/core/src/main/java/com/seibel/distanthorizons/core/util/RenderUtil.java index 39ae264c5..f03496cb1 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/util/RenderUtil.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/RenderUtil.java @@ -19,6 +19,7 @@ package com.seibel.distanthorizons.core.util; +import com.seibel.distanthorizons.core.api.internal.ClientApi; import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; @@ -36,6 +37,19 @@ public class RenderUtil private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class); + /** all speeds are measured in blocks per second */ + private static class DynamicOverdraw + { + /** + * A walking player moves around 4.1 blocks/sec + * A sprint jumping player around 7.1 blocks/sec + */ + public static final float MIN_SPEED = 10.0f; + /** a max speed spectator player can move just shy of 100 blocks/sec */ + public static final float MAX_SPEED = 100.0f; + public static final float MIN_OVERDRAW_RATIO = 0.2f; + } + //=====================// @@ -95,11 +109,12 @@ public class RenderUtil public static float getAutoOverdrawPrevention() { float overdraw = Config.Client.Advanced.Graphics.Culling.overdrawPrevention.get().floatValue(); - - // 0 or less - if (overdraw <= 0) + if (overdraw < 0) { - // at low render distances this hides the vanilla RD border + // automatic mode, + // get overdraw based on vanilla render distance. + // At low render distances this hides the vanilla RD border + int chunkRenderDistance = MC_RENDER.getRenderDistance(); if (chunkRenderDistance <= 2) { @@ -122,12 +137,38 @@ public class RenderUtil overdraw = 0.9f; } } - + else + { + // prevent setting an overdraw of 0 + // since that will cause rendering issues + overdraw = MathUtil.clamp(0.05f, overdraw, 1.0f); + } + return overdraw; } public static float getNearClipPlaneInBlocksForFading(float partialTicks) { float overdraw = getAutoOverdrawPrevention(); + + if (Config.Client.Advanced.Graphics.Culling.reduceOverdrawWithFastMovement.get()) + { + double avgSpeed = ClientApi.INSTANCE.cameraSpeedRollingAverage.getAverage(); + if (avgSpeed >= DynamicOverdraw.MIN_SPEED) + { + // if the player is moving fast enough, + // smoothly decrease the fade distance + // to give MC have a chance to load/generate. + + // convert the speed into a range of 0.0 - 1.0 + float speedRange = (float)((DynamicOverdraw.MAX_SPEED - avgSpeed) / DynamicOverdraw.MAX_SPEED); + // if math.max isn't done here we could completely + // remove vanilla rendering at high speeds + speedRange = Math.max(speedRange, DynamicOverdraw.MIN_OVERDRAW_RATIO); + + overdraw *= speedRange; + } + } + return getNearClipPlaneDistanceInBlocks(partialTicks, overdraw); } private static float getNearClipPlaneDistanceInBlocks(float partialTicks, float overdrawPreventionPercent) diff --git a/core/src/main/resources/assets/distanthorizons/lang/en_us.json b/core/src/main/resources/assets/distanthorizons/lang/en_us.json index 4357d439c..99b50f9d8 100644 --- a/core/src/main/resources/assets/distanthorizons/lang/en_us.json +++ b/core/src/main/resources/assets/distanthorizons/lang/en_us.json @@ -363,7 +363,11 @@ "distanthorizons.config.client.advanced.graphics.culling.overdrawPrevention": "Overdraw Prevention", "distanthorizons.config.client.advanced.graphics.culling.overdrawPrevention.@tooltip": - "Determines how far from the camera Distant Horizons will start rendering. \nMeasured as a percentage of the vanilla render distance.\n0 = Auto, overdraw will change based on the vanilla render distance.\n\nHigher values will prevent LODs from rendering behind vanilla blocks at a higher distance,\nbut may cause holes to appear in the LODs. \nHoles are most likely to appear when flying through unloaded terrain. \n\nIncreasing the vanilla render distance increases the effectiveness of this setting.", + "Determines how far from the camera Distant Horizons will start rendering. \nMeasured as a percentage of the vanilla render distance.\n-1 = Auto, overdraw will change based on the vanilla render distance.\n\nHigher values will prevent LODs from rendering behind vanilla blocks at a higher distance,\nbut may cause holes to appear in the LODs. \nHoles are most likely to appear when flying through unloaded terrain. \n\nIncreasing the vanilla render distance increases the effectiveness of this setting.", + "distanthorizons.config.client.advanced.graphics.culling.reduceOverdrawWithFastMovement": + "Reduce Overdraw With Fast Movement", + "distanthorizons.config.client.advanced.graphics.culling.reduceOverdrawWithFastMovement.@tooltip": + "If set to true the overdraw prevention radius will get closer \nto the camera when flying/moving quickly. \n\nThis helps reduce issues where Minecraft can't load or \ngenerate chunks fast enough to keep up with DH.", "distanthorizons.config.client.advanced.graphics.culling.enableCaveCulling": "Cave Culling", "distanthorizons.config.client.advanced.graphics.culling.enableCaveCulling.@tooltip": diff --git a/core/src/main/resources/shaders/fog/fog.frag b/core/src/main/resources/shaders/fog/fog.frag index d4982674d..607dd0740 100644 --- a/core/src/main/resources/shaders/fog/fog.frag +++ b/core/src/main/resources/shaders/fog/fog.frag @@ -158,15 +158,17 @@ float linearFog(float worldDist, float fogStart, float fogLength, float fogMin, return fogMin + fogRange * worldDist; } -float exponentialFog(float x, float fogStart, float fogLength, -float fogMin, float fogRange, float fogDensity) +float exponentialFog( + float x, float fogStart, float fogLength, + float fogMin, float fogRange, float fogDensity) { x = max((x-fogStart)/fogLength, 0.0) * fogDensity; return fogMin + fogRange - fogRange/exp(x); } -float exponentialSquaredFog(float x, float fogStart, float fogLength, -float fogMin, float fogRange, float fogDensity) +float exponentialSquaredFog( + float x, float fogStart, float fogLength, + float fogMin, float fogRange, float fogDensity) { x = max((x-fogStart)/fogLength, 0.0) * fogDensity; return fogMin + fogRange - fogRange/exp(x*x);