re-add some core rendering handlers

This commit is contained in:
James Seibel
2026-03-09 16:35:17 -05:00
parent 67b2467bee
commit 17cdb0f745
5 changed files with 1283 additions and 0 deletions
@@ -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
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<BeaconBeamDTO> 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<DhApiRenderableBox> fullBeaconBoxList = new ArrayList<>();
/** contains all beacons that could be rendered */
private final HashSet<DhBlockPos> 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<BeaconBeamDTO> 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<BeaconBeamDTO> 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<DhApiRenderableBox> 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<BeaconBeamDTO>
{
@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
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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. <br>
* 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? <br>
* 1 = 3x3 or 9 total <br>
* 2 = 5x5 or 25 total <br> <br>
*
* 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<DhApiRenderableBox> 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
}
@@ -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; }
}
@@ -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. <br>
* 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; }
}