From 17cdb0f745063f3572b864aecd85b9155b31472d Mon Sep 17 00:00:00 2001 From: James Seibel Date: Mon, 9 Mar 2026 16:35:17 -0500 Subject: [PATCH] re-add some core rendering handlers --- .../core/render/RenderParams.java | 221 +++++++ .../render/renderer/BeaconRenderHandler.java | 333 ++++++++++ .../render/renderer/CloudRenderHandler.java | 619 ++++++++++++++++++ .../cullingFrustum/DhFrustumBounds.java | 69 ++ .../cullingFrustum/NeverCullFrustum.java | 41 ++ 5 files changed, 1283 insertions(+) create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/render/RenderParams.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/render/renderer/BeaconRenderHandler.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/render/renderer/CloudRenderHandler.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/render/renderer/cullingFrustum/DhFrustumBounds.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/render/renderer/cullingFrustum/NeverCullFrustum.java diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/RenderParams.java b/core/src/main/java/com/seibel/distanthorizons/core/render/RenderParams.java new file mode 100644 index 000000000..98ce8e75f --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/RenderParams.java @@ -0,0 +1,221 @@ +package com.seibel.distanthorizons.core.render; + +import com.seibel.distanthorizons.api.enums.rendering.EDhApiRenderPass; +import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam; +import com.seibel.distanthorizons.core.api.internal.SharedApi; +import com.seibel.distanthorizons.core.api.internal.rendering.DhRenderState; +import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; +import com.seibel.distanthorizons.core.jar.EPlatform; +import com.seibel.distanthorizons.core.level.IDhClientLevel; +import com.seibel.distanthorizons.core.util.RenderUtil; +import com.seibel.distanthorizons.core.util.math.Mat4f; +import com.seibel.distanthorizons.core.util.math.Vec3d; +import com.seibel.distanthorizons.core.util.threading.PriorityTaskPicker; +import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; +import com.seibel.distanthorizons.core.world.IDhClientWorld; +import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; +import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper; +import com.seibel.distanthorizons.core.wrapperInterfaces.misc.ILightMapWrapper; +import com.seibel.distanthorizons.core.wrapperInterfaces.modAccessor.AbstractOptifineAccessor; +import com.seibel.distanthorizons.core.wrapperInterfaces.render.IMcGenericRenderer; +import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; + +/** + * An extension of {@link DhApiRenderParam} + * that allows additional validation and putting all + * rendering variables in a single place. + */ +public class RenderParams extends DhApiRenderParam +{ + private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); + private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class); + + private static final long TIME_FOR_MAC_TO_FINISH_COMPILING_IN_MS = 10_000; + private static boolean initialLoadingComplete = false; + + + public IDhClientWorld dhClientWorld; + public IDhClientLevel dhClientLevel; + /** more specific override of the API value {@link DhApiRenderParam#clientLevelWrapper} */ + public IClientLevelWrapper clientLevelWrapper; + public ILightMapWrapper lightmap; + public RenderBufferHandler renderBufferHandler; + public IMcGenericRenderer genericRenderer; + public Vec3d exactCameraPosition; + /** @see DhRenderState#vanillaFogEnabled */ + public boolean vanillaFogEnabled; + + public boolean validationRun = false; + + + + //=============// + // constructor // + //=============// + //region + + public RenderParams(EDhApiRenderPass renderPass, DhRenderState renderState) + { + this(renderPass, + renderState.partialTickTime, + renderState.mcProjectionMatrix, renderState.mcModelViewMatrix, + renderState.clientLevelWrapper, + renderState.vanillaFogEnabled + ); + } + private RenderParams( + EDhApiRenderPass renderPass, + float newPartialTicks, + Mat4f newMcProjectionMatrix, Mat4f newMcModelViewMatrix, + IClientLevelWrapper clientLevelWrapper, + boolean vanillaFogEnabled + ) + { + super(renderPass, + newPartialTicks, + RenderUtil.getNearClipPlaneInBlocks(), RenderUtil.getFarClipPlaneDistanceInBlocks(), + newMcProjectionMatrix, newMcModelViewMatrix, + RenderUtil.createLodProjectionMatrix(newMcProjectionMatrix), RenderUtil.createLodModelViewMatrix(newMcModelViewMatrix), + clientLevelWrapper.getMinHeight(), + clientLevelWrapper); + + + this.dhClientWorld = SharedApi.tryGetDhClientWorld(); + if (this.dhClientWorld != null) + { + this.dhClientLevel = (IDhClientLevel) this.dhClientWorld.getLevel(clientLevelWrapper); + if (this.dhClientLevel != null) + { + this.renderBufferHandler = this.dhClientLevel.getRenderBufferHandler(); + this.genericRenderer = this.dhClientLevel.getGenericRenderer(); + } + } + + this.clientLevelWrapper = clientLevelWrapper; + this.lightmap = MC_RENDER.getLightmapWrapper(this.clientLevelWrapper); + + if (MC_CLIENT.playerExists()) + { + this.exactCameraPosition = MC_RENDER.getCameraExactPosition(); + } + + this.vanillaFogEnabled = vanillaFogEnabled; + + } + + //endregion + + + + //======================// + // parameter validation // + //======================// + //region + + /** + * Should be called before rendering is done. + * @return a message if LODs shouldn't be rendered, null if the LODs can render + */ + public String getValidationErrorMessage(long firstRenderTimeMs) + { + // Note: all strings here should be constants to prevent String allocations + + this.validationRun = true; + + + if (!MC_CLIENT.playerExists()) + { + return "No Player Exists"; + } + + if (this.dhClientWorld == null) + { + return "No DH Client World Loaded"; + } + + if (this.dhClientLevel == null) + { + return "No DH Client Level Loaded"; + } + + if (this.clientLevelWrapper == null) + { + return "No Client Level Wrapper Loaded"; + } + + if (this.lightmap == null) + { + return "No Lightmap Loaded"; + } + + if (this.renderBufferHandler == null) + { + return "No RenderBufferHandler Present"; + } + + if (this.genericRenderer == null) + { + return "No Generic Renderer Present"; + } + + if (this.dhModelViewMatrix == null + || this.mcModelViewMatrix == null) + { + return "No MVM or Proj Matrix Given"; + } + + if (AbstractOptifineAccessor.optifinePresent() + && MC_RENDER.getTargetFramebuffer() == -1) + { + // wait for MC to finish setting up their renderer + return "Optifine Target Frame Buffer not set"; + } + + + // potential fix for a segfault when + // Sodium and DH are running together + if (EPlatform.get() == EPlatform.MACOS + && !initialLoadingComplete) + { + // Once MC starts rendering, wait a few seconds so + // MC/Sodium can finish their shader compiling before DH does its own. + // This will allow DH to compile its own shaders after Sodium finishes + // compiling its own. + long nowMs = System.currentTimeMillis(); + long firstAllowedRenderTimeMs = firstRenderTimeMs + TIME_FOR_MAC_TO_FINISH_COMPILING_IN_MS; + if (nowMs < firstAllowedRenderTimeMs) + { + return "Waiting for initial MC compile..."; + } + + + // null shouldn't happen, but just in case + PriorityTaskPicker.Executor renderLoadExecutor = ThreadPoolUtil.getRenderLoadingExecutor(); + if (renderLoadExecutor == null) + { + return "Waiting for DH Threadpool..."; + } + + // wait for DH to finish loading, by the time that's done + // java should have finished all of DH's JIT compiling, + // which will hopefully mean less concurrency and thus a lower + // chance of breaking + // (plus this gives Sodium/vanill a bit longer to finish their setup) + int taskCount = renderLoadExecutor.getQueueSize(); + if (taskCount > 0) + { + return "Waiting for DH JIT compiling..."; + } + + initialLoadingComplete = true; + } + + + return null; + } + + //endregion + + + +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/BeaconRenderHandler.java b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/BeaconRenderHandler.java new file mode 100644 index 000000000..aef4e9bd1 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/BeaconRenderHandler.java @@ -0,0 +1,333 @@ +/* + * This file is part of the Distant Horizons mod + * licensed under the GNU LGPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.seibel.distanthorizons.core.render.renderer; + +import com.seibel.distanthorizons.api.enums.rendering.EDhApiBlockMaterial; +import com.seibel.distanthorizons.api.interfaces.render.IDhApiCustomRenderObjectFactory; +import com.seibel.distanthorizons.api.interfaces.render.IDhApiRenderableBoxGroup; +import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam; +import com.seibel.distanthorizons.api.objects.math.DhApiVec3d; +import com.seibel.distanthorizons.api.objects.render.DhApiRenderableBox; +import com.seibel.distanthorizons.api.objects.render.DhApiRenderableBoxGroupShading; +import com.seibel.distanthorizons.core.config.Config; +import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; +import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; +import com.seibel.distanthorizons.core.pos.DhSectionPos; +import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos; +import com.seibel.distanthorizons.core.sql.dto.BeaconBeamDTO; +import com.seibel.distanthorizons.core.util.LodUtil; +import com.seibel.distanthorizons.core.util.RenderUtil; +import com.seibel.distanthorizons.core.util.math.Vec3d; +import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; +import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper; +import com.seibel.distanthorizons.core.wrapperInterfaces.render.IMcGenericRenderer; +import com.seibel.distanthorizons.coreapi.ModInfo; +import com.seibel.distanthorizons.core.logging.DhLogger; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Predicate; + +public class BeaconRenderHandler +{ + private static final DhLogger LOGGER = new DhLoggerBuilder().build(); + private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class); + private static final IDhApiCustomRenderObjectFactory GENERIC_OBJECT_FACTORY = SingletonInjector.INSTANCE.get(IDhApiCustomRenderObjectFactory.class); + + /** how often should we check if a beacon should be culled? */ + private static final int MAX_CULLING_FREQUENCY_IN_MS = 1_000; + + private static final Comparator NEGATIVE_BLOCKPOS_COMPARATOR = new NegativeInfiniteBlockPosComparator(); + + + + private final ReentrantLock updateLock = new ReentrantLock(); + + /** only contains the beacons currently being rendered (culled beacons will be missing) */ + private final IDhApiRenderableBoxGroup activeBeaconBoxRenderGroup; + /** contains all beacons that could be rendered (including those that are being culled) */ + private final ArrayList fullBeaconBoxList = new ArrayList<>(); + /** contains all beacons that could be rendered */ + private final HashSet fullBeaconBlockPosSet = new HashSet<>(); + + private boolean cullingThreadRunning = false; + private boolean updateRenderDataNextFrame = false; + + + + //=============// + // constructor // + //=============// + //region + + public BeaconRenderHandler(@NotNull IMcGenericRenderer renderer) + { + this.activeBeaconBoxRenderGroup = GENERIC_OBJECT_FACTORY.createAbsolutePositionedGroup(ModInfo.NAME+":Beacons", new ArrayList<>(0)); + this.activeBeaconBoxRenderGroup.setBlockLight(LodUtil.MAX_MC_LIGHT); + this.activeBeaconBoxRenderGroup.setSkyLight(LodUtil.MAX_MC_LIGHT); + this.activeBeaconBoxRenderGroup.setSsaoEnabled(false); + this.activeBeaconBoxRenderGroup.setShading(DhApiRenderableBoxGroupShading.getUnshaded()); + this.activeBeaconBoxRenderGroup.setPreRenderFunc(this::beforeRender); + + renderer.add(this.activeBeaconBoxRenderGroup); + } + + //endregion + + + + //=================// + // render handling // + //=================// + //region + + public void startRenderingBeacons(ArrayList beaconList, byte detailLevel) + { + try + { + this.updateLock.lock(); + + + // how wide should each beacon be? + int beaconBlockWidth = 1; + if (Config.Client.Advanced.Graphics.GenericRendering.expandDistantBeacons.get()) + { + beaconBlockWidth = DhSectionPos.getBlockWidth(detailLevel); + } + + + ArrayList sortedBeaconList = new ArrayList<>(beaconList); + + // merge distant beams if requested + if (Config.Client.Advanced.Graphics.GenericRendering.expandDistantBeacons.get()) + { + // sort beacons from neg inf -> pos inf + // so we can consistently merge adjacent beacons + sortedBeaconList.sort(NEGATIVE_BLOCKPOS_COMPARATOR); + + // go through each beacon... + for (int outerIndex = 0; outerIndex < sortedBeaconList.size(); outerIndex++) + { + BeaconBeamDTO outerBeacon = sortedBeaconList.get(outerIndex); + DhBlockPos outerBlockPos = outerBeacon.blockPos; + + // ...and remove any beacons that are within the block width to prevent overlaps + for (int mergeIndex = outerIndex + 1; mergeIndex < sortedBeaconList.size(); mergeIndex++) + { + BeaconBeamDTO beaconToMerge = sortedBeaconList.get(mergeIndex); + DhBlockPos mergeBlockPos = beaconToMerge.blockPos; + + int xDiff = mergeBlockPos.getX() - outerBlockPos.getX(); + int zDiff = mergeBlockPos.getZ() - outerBlockPos.getZ(); + + // merge (remove) this beacon if + // it's close to the outer beacon + if (xDiff < beaconBlockWidth + && zDiff < beaconBlockWidth) + { + sortedBeaconList.remove(mergeIndex); + mergeIndex--; // minus 1 so we don't go past the end of the array when incrementing in the for loop up top + } + } + } + } + + + //LOGGER.info("startRenderingBeacons ["+sortedBeaconList+"]"); + + // add each beacon to the renderer + for (int i = 0; i < sortedBeaconList.size(); i++) + { + BeaconBeamDTO beacon = sortedBeaconList.get(i); + if (!this.fullBeaconBlockPosSet.add(beacon.blockPos)) + { + // skip already present beacons + continue; + } + + + int maxBeaconBeamHeight = Config.Client.Advanced.Graphics.GenericRendering.beaconRenderHeight.get(); + DhApiRenderableBox beaconBox = new DhApiRenderableBox( + new DhApiVec3d(beacon.blockPos.getX(), beacon.blockPos.getY() + 1, beacon.blockPos.getZ()), + new DhApiVec3d(beacon.blockPos.getX() + beaconBlockWidth, maxBeaconBeamHeight, beacon.blockPos.getZ() + beaconBlockWidth), + beacon.color, + EDhApiBlockMaterial.ILLUMINATED + ); + + this.activeBeaconBoxRenderGroup.add(beaconBox); + this.fullBeaconBoxList.add(beaconBox); + this.activeBeaconBoxRenderGroup.triggerBoxChange(); + } + } + finally + { + this.updateLock.unlock(); + } + } + + public void stopRenderingBeaconsInRange(long pos) + { + try + { + this.updateLock.lock(); + + Predicate removeBoxPredicate = (DhApiRenderableBox box) -> + { + DhBlockPos blockPos = new DhBlockPos((int)box.minPos.x, (int)box.minPos.y, (int)box.minPos.z); + boolean contains = DhSectionPos.contains(pos, blockPos); + //if (contains) + //{ + // LOGGER.info("stopRenderingBeaconsInRange ["+DhSectionPos.toString(pos)+"] ["+blockPos+"]"); + //} + return contains; + }; + this.activeBeaconBoxRenderGroup.removeIf(removeBoxPredicate); + this.fullBeaconBoxList.removeIf(removeBoxPredicate); + + this.fullBeaconBlockPosSet.removeIf((DhBlockPos blockPos) -> DhSectionPos.contains(pos, blockPos)); + + this.activeBeaconBoxRenderGroup.triggerBoxChange(); + } + finally + { + this.updateLock.unlock(); + } + } + + + private void beforeRender(DhApiRenderParam renderEventParam) + { + if (Config.Client.Advanced.Graphics.Culling.disableBeaconDistanceCulling.get()) + { + // this could be called only when the player moves, but it's an extremely cheap check, + // so there isn't much of a reason to bother + this.tryUpdateBeaconCullingAsync(); + } + + + // this must be called on the render thread to prevent concurrency issues + if (this.updateRenderDataNextFrame) + { + this.activeBeaconBoxRenderGroup.triggerBoxChange(); + this.updateRenderDataNextFrame = false; + } + this.activeBeaconBoxRenderGroup.setActive(Config.Client.Advanced.Graphics.GenericRendering.enableBeaconRendering.get()); + } + /** does nothing if the culling thread is already running */ + private void tryUpdateBeaconCullingAsync() + { + ThreadPoolExecutor executor = ThreadPoolUtil.getBeaconCullingExecutor(); + if (executor != null + && !this.cullingThreadRunning) + { + this.cullingThreadRunning = true; + + try + { + executor.execute(() -> + { + try + { + Thread.sleep(MAX_CULLING_FREQUENCY_IN_MS); + } + catch (InterruptedException ignore) { } + + try + { + // lock to make sure we don't try adding beacons to the arrays while processing them + this.updateLock.lock(); + + Vec3d cameraPos = MC_RENDER.getCameraExactPosition(); + + // fading by the overdraw prevention amount helps reduce beacons from rendering strangely + // on the border of DH's render distance + float dhFadeDistance = RenderUtil.getNearClipPlaneInBlocks(); + + + // Clear the existing box group so we can re-populate it. + // Since the box group is only used when we trigger an update, clearing it here + // and repopulating it is fine. + this.activeBeaconBoxRenderGroup.clear(); + + // While iterating over every beacon isn't a great way of doing this, + // when 940 beacons were tested this only took ~0.9 Milliseconds, so as long as + // we aren't freezing the render thread this method of culling works just fine. + for (DhApiRenderableBox box : this.fullBeaconBoxList) + { + // if a beacon is outside the vanilla render distance render it + double distance = Vec3d.getHorizontalDistance(cameraPos, box.minPos); + if (distance > dhFadeDistance) + { + this.activeBeaconBoxRenderGroup.add(box); + } + } + + this.updateRenderDataNextFrame = true; + } + catch (Exception e) + { + LOGGER.error("Unexpected issue while updating beacon culling. Error: " + e.getMessage(), e); + } + finally + { + this.updateLock.unlock(); + this.cullingThreadRunning = false; + } + }); + } + catch (RejectedExecutionException ignore) + { /* If this happens that means everything is already shut down and no culling is necessary */ } + } + } + + //endregion + + + + //================// + // helper classes // + //================// + //region + + private static class NegativeInfiniteBlockPosComparator implements Comparator + { + @Override + public int compare(BeaconBeamDTO beacon1, BeaconBeamDTO beacon2) + { + DhBlockPos blockPos1 = beacon1.blockPos; + DhBlockPos blockPos2 = beacon2.blockPos; + + // sort by X, then by Z + if (blockPos1.getX() != blockPos2.getX()) + { + return Integer.compare(blockPos1.getX(), blockPos2.getX()); + } + return Integer.compare(blockPos1.getZ(), blockPos2.getZ()); + } + } + + //endregion + + + +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/CloudRenderHandler.java b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/CloudRenderHandler.java new file mode 100644 index 000000000..76e18d55b --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/CloudRenderHandler.java @@ -0,0 +1,619 @@ +/* + * This file is part of the Distant Horizons mod + * licensed under the GNU LGPL v3 License. + * + * Copyright (C) 2020 James Seibel + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.seibel.distanthorizons.core.render.renderer; + +import com.seibel.distanthorizons.api.enums.rendering.EDhApiBlockMaterial; +import com.seibel.distanthorizons.api.interfaces.render.IDhApiCustomRenderObjectFactory; +import com.seibel.distanthorizons.api.interfaces.render.IDhApiRenderableBoxGroup; +import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam; +import com.seibel.distanthorizons.api.objects.math.DhApiVec3d; +import com.seibel.distanthorizons.api.objects.render.DhApiRenderableBox; +import com.seibel.distanthorizons.api.objects.render.DhApiRenderableBoxGroupShading; +import com.seibel.distanthorizons.core.config.Config; +import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; +import com.seibel.distanthorizons.core.level.IDhClientLevel; +import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; +import com.seibel.distanthorizons.core.util.LodUtil; +import com.seibel.distanthorizons.core.util.math.Vec3d; +import com.seibel.distanthorizons.core.util.math.Vec3f; +import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper; +import com.seibel.distanthorizons.core.wrapperInterfaces.render.IMcGenericRenderer; +import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; +import com.seibel.distanthorizons.coreapi.ModInfo; +import com.seibel.distanthorizons.core.logging.DhLogger; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.*; + +public class CloudRenderHandler +{ + private static final DhLogger LOGGER = new DhLoggerBuilder().build(); + private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class); + private static final IDhApiCustomRenderObjectFactory GENERIC_OBJECT_FACTORY = SingletonInjector.INSTANCE.get(IDhApiCustomRenderObjectFactory.class); + + private static final String CLOUD_RESOURCE_TEXTURE_PATH = "assets/distanthorizons/textures/clouds.png"; + + private static final boolean DEBUG_BORDER_COLORS = false; + + /** + * How wide an individual box is.
+ * Measured in blocks. + */ + private static final int CLOUD_BOX_WIDTH = 128; + /** measured in blocks */ + private static final int CLOUD_BOX_THICKNESS = 32; + + /** + * How many cloud groups wide can we render at maximum?
+ * 1 = 3x3 or 9 total
+ * 2 = 5x5 or 25 total

+ * + * 5 seems like a good count since it can cover up to around 2048 render distance. + */ + private static final int CLOUD_INSTANCE_RADIUS_COUNT = 5; + + private static final float MOVE_SPEED_IN_BLOCKS_PER_SECOND = 6.0f; + + + private final IDhApiRenderableBoxGroup[][] boxGroupByOffset + // radius * 2 to get the diameter + // + 1 so we get an odd number wide (needed so we can have a center position) + = new IDhApiRenderableBoxGroup[(CLOUD_INSTANCE_RADIUS_COUNT * 2) + 1][(CLOUD_INSTANCE_RADIUS_COUNT * 2) + 1]; + + private final IDhClientLevel level; + private final IMcGenericRenderer renderer; + + /** cached array so we don't need to re-create it each frame for each cloud group */ + private final Vec3d[] cullingCorners = new Vec3d[] + { + // the values of each will be overwritten during the culling pass + new Vec3d(), + new Vec3d(), + new Vec3d(), + new Vec3d(), + }; + + + private boolean disabledWarningLogged = false; + + + + //=============// + // constructor // + //=============// + //region + + public CloudRenderHandler(IDhClientLevel level, IMcGenericRenderer renderer) + { + this.level = level; + this.renderer = renderer; + + + + //=======================// + // get the cloud texture // + //=======================// + //region + + // default to a single empty slot in case the texture is broken + boolean[][] cloudLocations = new boolean[1][1]; + try + { + cloudLocations = getCloudsFromTexture(); + } + catch (FileNotFoundException e) + { + LOGGER.error(e.getMessage(), e); + } + catch (IOException e) + { + LOGGER.error("Unexpected issue getting cloud texture, error: ["+e.getMessage()+"].", e); + } + + if (cloudLocations.length != 0 && + cloudLocations.length != cloudLocations[0].length) + { + LOGGER.warn("Non-square cloud texture found, some parts of the texture will be clipped off."); + } + + //endregion + + + + //===================// + // parse the texture // + //===================// + //region + + int textureWidth = cloudLocations.length; + ArrayList boxList = new ArrayList<>(512); + for (int x = 0; x < textureWidth; x ++) + { + for (int z = 0; z < textureWidth; z ++) + { + if (cloudLocations[x][z]) + { + // start a new box in Z direction + int startZ = z; + int startX = x; + int endZ = startZ; + int endX = x+1; + + + + //==========================// + // merge in the Z direction // + //==========================// + + // Find the cloud's length in the Z direction + while (endZ < textureWidth + && cloudLocations[x][endZ]) + { + endZ++; + } + // update the z iterator so we can skip over everything included in this cloud + z = endZ - 1; + + + + //==========================// + // merge in the X direction // + //==========================// + + for (int currentX = startX + 1; currentX < textureWidth; currentX++) + { + boolean canMergeInXDir = true; + + // check if all locations in this column are true + for (int adjacentZ = startZ; adjacentZ < endZ; adjacentZ++) + { + if (!cloudLocations[currentX][adjacentZ]) + { + // at least one pixel in the texture is false, + // so we can't merge in this direction + canMergeInXDir = false; + break; + } + } + + + if (canMergeInXDir) + { + // mark the adjacent column as processed + for (int currentZ = startZ; currentZ < endZ; currentZ++) + { + // by flipping all the pixels in the adjacent column to false, + // we don't have to worry about adding another cloud + cloudLocations[currentX][currentZ] = false; + } + + endX = (currentX + 1); + } + else + { + break; + } + } + + + + //============================// + // Create the renderable box // + //============================// + + // endZ contains the last cloud index + // so the cloud now goes from startZ to endZ (inclusive) + int minXBlockPos = startX * CLOUD_BOX_WIDTH; + int minZBlockPos = startZ * CLOUD_BOX_WIDTH; + int maxXBlockPos = endX * CLOUD_BOX_WIDTH; + int maxZBlockPos = endZ * CLOUD_BOX_WIDTH; + + // this color is changed at render time based on the level time + Color color = new Color(255,255,255,255); + if (DEBUG_BORDER_COLORS) + { + // equals is included so the boarder is 2 blocks wide, making it easier to see + if (x <= 1) { color = Color.RED; } + else if (x >= textureWidth - 2) { color = Color.GREEN; } + if (z <= 1) { color = Color.BLUE; } + else if (z >= textureWidth - 2) { color = Color.BLACK; } + } + + DhApiRenderableBox box = new DhApiRenderableBox( + new DhApiVec3d(minXBlockPos, 0, minZBlockPos), + new DhApiVec3d(maxXBlockPos, CLOUD_BOX_THICKNESS, maxZBlockPos), + color, + EDhApiBlockMaterial.UNKNOWN + ); + boxList.add(box); + } + } + } + + //endregion + + + + //========================// + // create the renderables // + //========================// + //region + + // slightly lighter shading than the default + DhApiRenderableBoxGroupShading cloudShading = DhApiRenderableBoxGroupShading.getUnshaded(); + cloudShading.north = cloudShading.south = 0.9f; + cloudShading.east = cloudShading.west = 0.8f; + cloudShading.top = 1.0f; + cloudShading.bottom = 0.7f; + + + for (int x = -CLOUD_INSTANCE_RADIUS_COUNT; x <= CLOUD_INSTANCE_RADIUS_COUNT; x++) + { + for (int z = -CLOUD_INSTANCE_RADIUS_COUNT; z <= CLOUD_INSTANCE_RADIUS_COUNT; z++) + { + IDhApiRenderableBoxGroup boxGroup = GENERIC_OBJECT_FACTORY.createRelativePositionedGroup( + ModInfo.NAME + ":Clouds", + new DhApiVec3d(0, 0, 0), // the offset will be set during rendering + boxList); + + // since cloud colors are set by the level based on the time of day lighting should affect it + boxGroup.setBlockLight(LodUtil.MAX_MC_LIGHT); + boxGroup.setSkyLight(LodUtil.MAX_MC_LIGHT); + boxGroup.setSsaoEnabled(false); + boxGroup.setShading(cloudShading); + + CloudParams cloudParams = new CloudParams(textureWidth, x, z); + boxGroup.setPreRenderFunc((renderParam) -> this.preRender(renderParam, cloudParams)); + + renderer.add(boxGroup); + this.boxGroupByOffset[x+CLOUD_INSTANCE_RADIUS_COUNT][z+CLOUD_INSTANCE_RADIUS_COUNT] = boxGroup; + } + } + } + + //endregion + + + + //===========// + // rendering // + //===========// + //region + + private void preRender(DhApiRenderParam renderParam, CloudParams cloudParams) + { + IDhApiRenderableBoxGroup boxGroup = this.boxGroupByOffset[cloudParams.instanceOffsetX+CLOUD_INSTANCE_RADIUS_COUNT][cloudParams.instanceOffsetZ+CLOUD_INSTANCE_RADIUS_COUNT]; + + + + //===================// + // should we render? // + //===================// + + boolean renderClouds = Config.Client.Advanced.Graphics.GenericRendering.enableCloudRendering.get(); + boxGroup.setActive(renderClouds); + if(!renderClouds) + { + return; + } + + //if (!this.renderer.getInstancedRenderingAvailable()) + //{ + // if (!this.disabledWarningLogged) + // { + // this.disabledWarningLogged = true; + // LOGGER.warn("Instanced rendering unavailable, cloud rendering disabled."); + // } + // boxGroup.setActive(false); + // return; + //} + + IClientLevelWrapper clientLevelWrapper = this.level.getClientLevelWrapper(); + if (clientLevelWrapper == null) + { + return; + } + + + + //================// + // cloud movement // + //================// + + long currentTime = System.currentTimeMillis(); + float deltaTime = (currentTime - cloudParams.lastFrameTime) / 1000.0f; // Delta time in seconds + cloudParams.lastFrameTime = currentTime; + + float deltaX = MOVE_SPEED_IN_BLOCKS_PER_SECOND * deltaTime; + // negative delta is to match vanilla's cloud movement + cloudParams.deltaOffsetX -= deltaX; + // wrap the cloud around after reaching the edge + cloudParams.deltaOffsetX %= cloudParams.widthInBlocks; + + + + //============================// + // camera movement and offset // + //============================// + + // camera position + int cameraPosX = (int)MC_RENDER.getCameraExactPosition().x; + int cameraPosZ = (int)MC_RENDER.getCameraExactPosition().z; + // offset the camera position by negative 1 width when below zero to fix off-by-one errors in the negative direction + if (cameraPosX < 0) { cameraPosX -= cloudParams.widthInBlocks; } + if (cameraPosZ < 0) { cameraPosZ -= cloudParams.widthInBlocks; } + + // determine how many cloud instances away from the origin we are + int cloudInstanceOffsetCountX = (cameraPosX / cloudParams.widthInBlocks); + int cloudInstanceOffsetCountZ = (cameraPosZ / cloudParams.widthInBlocks); + // calculate the new offset + float instanceOffsetX = (cloudInstanceOffsetCountX * cloudParams.widthInBlocks); + float instanceOffsetZ = (cloudInstanceOffsetCountZ * cloudParams.widthInBlocks); + + + float newMinPosX = + cloudParams.deltaOffsetX + + (cloudParams.instanceOffsetX * cloudParams.widthInBlocks) + + instanceOffsetX + cloudParams.halfWidthInBlocks; + float newMinPosY = this.level.getLevelWrapper().getMaxHeight() + 200; + float newMinPosZ = cloudParams.deltaOffsetZ + + (cloudParams.instanceOffsetZ * cloudParams.widthInBlocks) + + instanceOffsetZ + cloudParams.halfWidthInBlocks; + + boolean cullCloud = this.shouldCloudBeCulled( + newMinPosX, newMinPosY, newMinPosZ, + cloudParams + ); + if(cullCloud) + { + boxGroup.setActive(false); + } + + + + //===========================// + // update color and position // + //===========================// + + // if debug colors are enabled don't change them + if (!DEBUG_BORDER_COLORS + // don't modify cloud groups that aren't active + && boxGroup.isActive()) + { + // cloud color changes based on the time of day and weather so we need to get it from the level + Color newCloudColor = clientLevelWrapper.getCloudColor(renderParam.partialTicks); + + + // all boxes should have the same color, so we can get their current color + // via the first box + DhApiRenderableBox firstBox = boxGroup.get(0); + Color currentBoxColor = firstBox.color; + + // update the boxes if their color should be changed + if (!newCloudColor.equals(currentBoxColor)) + { + // Note: cloud instances may share boxes + // because of that this method may only need to be called once per all clouds + for (DhApiRenderableBox box : boxGroup) + { + box.color = newCloudColor; + } + } + + + // trigger an update if this cloud section has a different color + if (!cloudParams.previousColor.equals(newCloudColor)) + { + cloudParams.previousColor = newCloudColor; + + boxGroup.triggerBoxChange(); + } + } + + boxGroup.setOriginBlockPos(new DhApiVec3d(newMinPosX, newMinPosY, newMinPosZ)); + } + private boolean shouldCloudBeCulled( + float minPosX, float minPosY, float minPosZ, + CloudParams cloudParams) + { + //========================// + // skip center 3x3 clouds // + //========================// + + // always render the center 3x3 clouds, otherwise we may see + // an un-rendered border + if (cloudParams.instanceOffsetX >= -1 && cloudParams.instanceOffsetX <= 1 + && cloudParams.instanceOffsetZ >= -1 && cloudParams.instanceOffsetZ <= 1) + { + return false; + } + + + + //==============// + // culling prep // + //==============// + + // we need all 4 corners since we want to draw any clouds that + // could potentially be within render distance + this.cullingCorners[0].x = minPosX; + this.cullingCorners[0].y = minPosY; + this.cullingCorners[0].z = minPosZ; + + this.cullingCorners[1].x = minPosX; + this.cullingCorners[1].y = minPosY; + this.cullingCorners[1].z = minPosZ + cloudParams.widthInBlocks; + + this.cullingCorners[2].x = minPosX + cloudParams.widthInBlocks; + this.cullingCorners[2].y = minPosY; + this.cullingCorners[2].z = minPosZ; + + this.cullingCorners[3].x = minPosX + cloudParams.widthInBlocks; + this.cullingCorners[3].y = minPosY; + this.cullingCorners[3].z = minPosZ + cloudParams.widthInBlocks; + + Vec3d cameraPos = MC_RENDER.getCameraExactPosition(); + Vec3f cameraLookAtVector = MC_RENDER.getLookAtVector(); + cameraLookAtVector.normalize(); + + double renderDistance = Config.Client.Advanced.Graphics.Quality.lodChunkRenderDistanceRadius.get() + // * 1.5 is so we have a little extra buffer where clouds will render further than + // necessary to prevent seeing the cloud border + * LodUtil.CHUNK_WIDTH * 1.5; + + + + //===================// + // check each corner // + //===================// + + boolean allOutsideRenderDistance = true; + boolean allBehindCamera = true; + + for (Vec3d corner : this.cullingCorners) + { + // Check if the corner is within the render distance + // (ignoring height, since LODs also ignore height) + + Vec3d cornerNoHeight = new Vec3d(corner); + cornerNoHeight.y = 0; + Vec3d cameraPosNoHeight = new Vec3d(cameraPos); + cameraPosNoHeight.y = 0; + + double cornerDistance = cornerNoHeight.getDistance(cameraPosNoHeight); + if (cornerDistance <= renderDistance) + { + allOutsideRenderDistance = false; + } + + + // Check if the corner is in front of the camera (dot product > 0 means in front) + Vec3f toCorner = new Vec3f( + (float) (corner.x - cameraPos.x), + (float) (corner.y - cameraPos.y), + (float) (corner.z - cameraPos.z)); + toCorner.normalize(); + + if (cameraLookAtVector.dotProduct(toCorner) > 0) + { + allBehindCamera = false; + } + } + + // Cull if all corners are either behind the camera or outside the render distance + return allOutsideRenderDistance || allBehindCamera; + } + + //endregion + + + + //==================// + // texture handling // + //==================// + //region + + private static boolean[][] getCloudsFromTexture() throws FileNotFoundException, IOException + { + final ClassLoader loader = CloudRenderHandler.class.getClassLoader(); + + boolean[][] whitePixels = null; + try(InputStream imageInputStream = loader.getResourceAsStream(CLOUD_RESOURCE_TEXTURE_PATH)) + { + if (imageInputStream == null) + { + throw new FileNotFoundException("Unable to find cloud texture at resource path: ["+CLOUD_RESOURCE_TEXTURE_PATH+"]."); + } + + BufferedImage image = ImageIO.read(imageInputStream); + + int width = image.getWidth(); + int height = image.getHeight(); + + whitePixels = new boolean[width][height]; + + for (int x = 0; x < width; x ++) + { + for (int z = 0; z < width; z ++) + { + Color color = new Color(image.getRGB(x,z)); + whitePixels[x][z] = color.equals(Color.WHITE); + } + } + } + + return whitePixels; + } + + //endregion + + + + //================// + // helper classes // + //================// + //region + + private static class CloudParams + { + public final int textureWidth; + public final int widthInBlocks; + public final int halfWidthInBlocks; + + public final int instanceOffsetX; + public final int instanceOffsetZ; + + + /** how far this cloud group has moved in the X direction based on time */ + public float deltaOffsetX = 0; + /** how far this cloud group has moved in the Z direction based on time */ + public float deltaOffsetZ = 0; + + public long lastFrameTime = System.currentTimeMillis(); + + /** used so we can trigger a VBO update when necessary */ + public Color previousColor = Color.WHITE; + + + + // constructor // + + public CloudParams(int textureWidth, int instanceOffsetX, int instanceOffsetZ) + { + this.textureWidth = textureWidth; + this.widthInBlocks = (this.textureWidth * CLOUD_BOX_WIDTH); + this.halfWidthInBlocks = this.widthInBlocks / 2; + + this.instanceOffsetX = instanceOffsetX; + this.instanceOffsetZ = instanceOffsetZ; + } + + } + + //endregion + + + +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/cullingFrustum/DhFrustumBounds.java b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/cullingFrustum/DhFrustumBounds.java new file mode 100644 index 000000000..5fe059b34 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/cullingFrustum/DhFrustumBounds.java @@ -0,0 +1,69 @@ +package com.seibel.distanthorizons.core.render.renderer.cullingFrustum; + +import com.seibel.distanthorizons.api.interfaces.override.rendering.IDhApiCullingFrustum; +import com.seibel.distanthorizons.api.objects.math.DhApiMat4f; +import com.seibel.distanthorizons.coreapi.interfaces.dependencyInjection.IOverrideInjector; +import com.seibel.distanthorizons.core.util.math.Mat4f; +import org.joml.FrustumIntersection; +import org.joml.Matrix4f; +import org.joml.Matrix4fc; +import org.joml.Vector3f; + + +public class DhFrustumBounds implements IDhApiCullingFrustum +{ + private final FrustumIntersection frustum; + private final Vector3f boundsMin = new Vector3f(); + private final Vector3f boundsMax = new Vector3f(); + public float worldMinY; + public float worldMaxY; + + + + //=============// + // constructor // + //=============// + + public DhFrustumBounds() + { + this.frustum = new FrustumIntersection(); + } + + + + //=========// + // methods // + //=========// + + @Override + public void update(int worldMinBlockY, int worldMaxBlockY, DhApiMat4f dhWorldViewProjection) + { + this.worldMinY = worldMinBlockY; + this.worldMaxY = worldMaxBlockY; + + Matrix4f worldViewProjection = new Matrix4f(Mat4f.createJomlMatrix(dhWorldViewProjection)); + this.frustum.set(worldViewProjection); + + Matrix4fc matWorldViewProjectionInv = new Matrix4f(worldViewProjection).invert(); + matWorldViewProjectionInv.frustumAabb(this.boundsMin, this.boundsMax); + } + + @Override + public boolean intersects(int lodBlockPosMinX, int lodBlockPosMinZ, int lodBlockWidth, int lodDetailLevel) + { + Vector3f lodMin = new Vector3f(lodBlockPosMinX, this.worldMinY, lodBlockPosMinZ); + Vector3f lodMax = new Vector3f(lodBlockPosMinX + lodBlockWidth, this.worldMaxY, lodBlockPosMinZ + lodBlockWidth); + + return this.frustum.testAab(lodMin, lodMax); + } + + + + //=====================// + // overridable methods // + //=====================// + + @Override + public int getPriority() { return IOverrideInjector.CORE_PRIORITY; } + +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/cullingFrustum/NeverCullFrustum.java b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/cullingFrustum/NeverCullFrustum.java new file mode 100644 index 000000000..e6ca60d54 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/cullingFrustum/NeverCullFrustum.java @@ -0,0 +1,41 @@ +package com.seibel.distanthorizons.core.render.renderer.cullingFrustum; + +import com.seibel.distanthorizons.api.interfaces.override.rendering.IDhApiCullingFrustum; +import com.seibel.distanthorizons.api.interfaces.override.rendering.IDhApiShadowCullingFrustum; +import com.seibel.distanthorizons.api.objects.math.DhApiMat4f; +import com.seibel.distanthorizons.coreapi.interfaces.dependencyInjection.IOverrideInjector; + +/** + * Dummy {@link IDhApiCullingFrustum} that allows everything through.
+ * Useful when a frustum is required, but culling shouldn't be done. + */ +public class NeverCullFrustum implements IDhApiCullingFrustum, IDhApiShadowCullingFrustum +{ + //=============// + // constructor // + //=============// + + public NeverCullFrustum() { } + + + + //=========// + // methods // + //=========// + + @Override + public void update(int worldMinBlockY, int worldMaxBlockY, DhApiMat4f dhWorldViewProjection) { /* update isn't needed */ } + + @Override + public boolean intersects(int lodBlockPosMinX, int lodBlockPosMinZ, int lodBlockWidth, int lodDetailLevel) { return true; } + + + + //=====================// + // overridable methods // + //=====================// + + @Override + public int getPriority() { return IOverrideInjector.CORE_PRIORITY; } + +}