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; }
+
+}