Compare commits

...

80 Commits

Author SHA1 Message Date
James Seibel bf6db89a4b Add folder for compiled jars
I did this since Gitlab doesn't have an easy way to add release binaries, and I don't want to move everything over to Github right now.
2021-06-01 20:11:41 -05:00
James Seibel 38c644739a Fix the mixin using the old package name 2021-06-01 20:10:27 -05:00
James Seibel a0fe977976 comment out debug lines 2021-05-31 19:31:36 -05:00
James Seibel 739d6d5856 update the version to a1.2 2021-05-31 19:31:12 -05:00
James Seibel 84125735a1 change the packages from com.backsun.lod... to com.seibel.lod... 2021-05-31 19:30:48 -05:00
James Seibel 884f6811a1 Improve how dimension width is determined and add a TODO 2021-05-31 16:43:42 -05:00
James Seibel 402abb0963 add some depreciation supressions and improve the fog slightly 2021-05-31 16:43:21 -05:00
James Seibel 2f7f489e14 Add the ability to count the number of loaded LodChunks 2021-05-31 16:41:27 -05:00
James Seibel 4de5c287fc Close #20 (allow the LOD distance to be changed in the config) 2021-05-31 15:29:26 -05:00
James Seibel ccb58024a1 rename LodRenderer to LodRender 2021-05-31 15:21:58 -05:00
James Seibel 0381d56511 rename method to getFileNameAndPathForRegion 2021-05-31 14:36:11 -05:00
James Seibel 7bbd3fd815 Improve logging 2021-05-31 14:35:34 -05:00
James Seibel 4e243252e9 Simplify the debug render setup 2021-05-31 14:15:24 -05:00
James Seibel b488d21a14 Replace System.out with a Logger 2021-05-31 14:11:58 -05:00
James Seibel 3e8dbf7ac7 Add file versioning logic when writing to a file 2021-05-31 14:11:34 -05:00
James Seibel 695b73f9d3 Fix the CubicLodTemplate taking the average of empty chunks 2021-05-31 11:54:46 -05:00
James Seibel 6e37bce38a Improve how empty LodChunks are determined 2021-05-31 11:54:27 -05:00
James Seibel 06232f65b2 Fix empty chunks not generating
This is most useful in the end, where there are many empty chunks
2021-05-31 11:53:59 -05:00
James Seibel bebe4b7436 Organize the chunk generation modes comment by time 2021-05-31 11:09:38 -05:00
James Seibel 7d9f04d54c Improve wording in the config file 2021-05-31 11:09:03 -05:00
James Seibel 70336acc75 Add a double detail level to the CublicLodTemplate 2021-05-31 10:50:36 -05:00
James Seibel e1ef08a783 Close #24 (only add visible side colors) 2021-05-30 14:06:54 -05:00
James Seibel 648a70097a Add a comment about the LodChunk resolution change 2021-05-30 13:52:38 -05:00
James Seibel 280892f5f4 replace == with .equals 2021-05-29 16:59:10 -05:00
James Seibel 3c57b124d5 Improve profile tracking 2021-05-29 16:59:02 -05:00
James Seibel 04d6bae479 Add several TODOs 2021-05-29 13:47:18 -05:00
James Seibel 98096a5db7 part of 7e4f3a3 2021-05-29 13:23:49 -05:00
James Seibel 7e4f3a3185 Fix chunks not generating out of view range when changing worlds 2021-05-29 13:22:48 -05:00
James Seibel 9fe6be10f6 replace LodGeometryQuality with LodDetail 2021-05-29 13:02:05 -05:00
James Seibel 76b356d38e Add version handling and improve error handling 2021-05-29 13:01:47 -05:00
James Seibel 811e24ee5e Improve and simplify LodChunk 2021-05-29 13:00:43 -05:00
James Seibel 5002db15d6 Remove LodLocation since LodCorner exists 2021-05-29 12:45:46 -05:00
James Seibel a3357c1193 Replace LodGeometryQuality with LodDetail 2021-05-29 12:45:19 -05:00
James Seibel af36224a57 Remove unneeded object creation 2021-05-29 12:42:39 -05:00
James Seibel 630ff361d3 in ColorDirection replace N,S,E,W with NORTH, SOUTH, EAST, WEST 2021-05-19 12:51:23 -05:00
James Seibel 34c6f29a18 Add file versioning to the file handler 2021-05-19 12:34:02 -05:00
James Seibel 909718e491 Remove the readyToReadAndWrite method 2021-05-19 12:10:36 -05:00
James Seibel 8a3d199247 Implement Single and Quad geometry qualities for CubicLod Rendering 2021-05-19 08:47:22 -05:00
James Seibel fa13c981b7 Add a missing empty line in the config file 2021-05-18 21:56:33 -05:00
James Seibel 815b00c3ca Add multiple color modes and set up for geometry quality 2021-05-08 16:41:55 -05:00
James Seibel ae9144a6c4 Add the basis for different LOD drawing modes (aka templates) 2021-05-08 14:58:41 -05:00
James Seibel 359fde3b6b Remove an unneeded line 2021-05-08 14:37:05 -05:00
James Seibel 92fa904cc6 Setup the config and enum for multiple LOD drawing modes 2021-05-05 16:55:01 -05:00
James Seibel 2583ae34d4 Remove DrawMode since it wasn't supported anyway 2021-05-05 16:53:39 -05:00
James Seibel e36b3394f4 Remove an unneeded comment 2021-05-05 16:35:23 -05:00
James Seibel 7af38df92c Remove some outdated TODOs 2021-04-18 21:23:30 -05:00
James Seibel 51add24110 Merge branch '1.16.4' of gitlab.com:jeseibel/minecraft-lod-mod into 1.16.4 2021-04-03 12:11:32 -05:00
James Seibel 9e5aac3bf7 Close #18 (allow client use on servers) 2021-04-03 12:11:20 -05:00
James Seibel 5738a5b7cd Close #8 (allow client use on servers) 2021-04-03 12:05:16 -05:00
James Seibel e1216966a3 Add error checking 2021-04-02 22:56:13 -05:00
James Seibel 222c0de7f1 Only try generating chunks on a local server 2021-04-02 22:50:33 -05:00
James Seibel b4f1fb6d28 Improve a TODO
related to issue #13
2021-04-01 21:05:45 -05:00
James Seibel a32082ad20 Remove the need for a World object in LodBuilder
The world hasn't been needed for a while, I just never got around to removing it.
2021-04-01 13:57:47 -05:00
James Seibel eeb5fb6c3c Improve the generateLodChunkAsync method 2021-04-01 13:56:52 -05:00
James Seibel cb50f24c86 Improve how a unloaded LodWorld is handled 2021-04-01 13:13:14 -05:00
James Seibel 5ca5764c0e Improve how LodWorld, LodBuilder, and LodRenderer objects are handled to prevent multiple references
There was a problem where upon changing worlds the LodWorld wouldn't reflect the commit along with 6c515350 and fccd1db0 should fix that problem.
2021-03-31 14:22:35 -05:00
James Seibel fccd1db045 Add the ability to select and deselect worlds 2021-03-31 14:18:11 -05:00
James Seibel 6c515350bc Clean up references to LodDimension objects 2021-03-31 14:15:49 -05:00
James Seibel 50aee9dfb2 Move getCurrentWorldID into the LodUtils class 2021-03-31 10:56:50 -05:00
James Seibel 0649504770 Fix #16 (stop placeholder chunks from being saved to disk) 2021-03-30 14:55:12 -05:00
James Seibel 29068f9550 Fix the projection matrix not being reset after rendering LODs
Regressive fix for 18c08ccd.
2021-03-30 08:07:15 -05:00
James Seibel 18c08ccd88 Fix #8 and #9 (inaccurate lighting and rendering behind transparent objects)
Instead of using a stencil and rendering in the forgelastdraw event, we now you a mixin to render right before the sold block layer.

The main purpose of this was to allow for LODs to be drawn behind transparent objects; however as a happy accident it seems to have also improved the lighting, I'm not sure if it is perfect, but it is much better.
2021-03-28 22:39:58 -05:00
James Seibel b71d6a5e3f Closes issue #10 (Regen LODs on block change) 2021-03-27 21:49:23 -05:00
James Seibel 8f619f3fa1 Closes issue #12 (improve world change detection) 2021-03-27 21:28:03 -05:00
James Seibel eab16ff20a Move all buffer building into the LodBufferBuilder and improve chunk generation logic
Chunks generation requests should no longer stack exponentially (before whenever one chunk was generated the LODs would be regenerated, causing more chunks to generate so if more than one chunk was ever generated at a time they would stack).
2021-03-25 23:04:48 -05:00
James Seibel d913ed9621 Fix issue #11 (generate chunks closer to the player first) 2021-03-25 16:31:11 -05:00
James Seibel a649cf179f comment out a debug statement 2021-03-25 16:04:02 -05:00
James Seibel cf5de39250 Rename SingleLodChunkGenWorker to LodChunkGenWorker 2021-03-25 16:03:44 -05:00
James Seibel bab3cd9656 Move SingleLodChunkGenWorker into the builders package 2021-03-24 21:59:32 -05:00
James Seibel cafd4f0c47 Move all LodChunk generation code to the LodBuilder object 2021-03-24 21:59:06 -05:00
James Seibel e20833225f Slightly improve LOD generation speed and add code related to heightmap
The slight speed increase is done by only generating the chunk to the "FEATURES" status instead of "FULL".

The code related to the heightmap is currently unused, since the LOD color generation requires blocks. Although it may have some use in the future so I will keep it in, albeit commented out.
2021-03-24 19:09:30 -05:00
James Seibel 14a06c220b Move the enums into their own package 2021-03-24 17:50:17 -05:00
James Seibel e5a5ba327e Add a TODO 2021-03-19 20:36:11 -05:00
James Seibel fedc8f7b66 Add LODs generating outside the player's view distance
It isn't fast enough to keep up with flying creating or spectator players; but it does function without causing heavy server or client lag.
LODs are generated in lines starting far away from the player and moving towards them, in the future they should be generated close to the player first.

Also add a RegionPos object and a way to convert from ChunkPos objects in LodUtils
2021-03-19 20:25:23 -05:00
James Seibel 7a3497d44c Update and improve Access Transformer
Remove old unneeded transformations and add useful comments.
2021-03-04 21:55:26 -06:00
James Seibel 873034f7e4 Merge branch '1.16.4' of gitlab.com:jeseibel/minecraft-lod-mod into 1.16.4 2021-03-04 21:30:33 -06:00
James Seibel a151054b48 Fix issue #7 (screen space distortions not be applied) 2021-03-04 21:30:19 -06:00
James Seibel 14c69971f6 Fix a bug with holding grass blocks
For some reason hold grass blocks (and presumably other biome colored blocks)  would look gray if GL_COLOR_MATERIAL is disabled
2021-03-04 21:16:41 -06:00
James Seibel edc3858699 Add the youtube demo video 2021-03-03 16:16:47 +00:00
James Seibel bdaf33b80b Add a simplified description to the readme 2021-03-03 03:48:13 +00:00
53 changed files with 3338 additions and 2335 deletions
+4
View File
@@ -2,6 +2,10 @@ This mod adds a Level Of Detail (LOD) system to Minecraft.
This implementation renders simplified chunks outside the normal render distance
allowing for an increased view distance without harming performance.
Or in other words: this mod let's you see farther without turning your game into a slide show.
If you want to see a quick demo, check out the video I made here:
https://youtu.be/v61iOYZQWCs
Forge version: 1.16.4-35.1.4
Binary file not shown.
+4 -2
View File
@@ -3,7 +3,9 @@ buildscript {
maven { url = 'https://files.minecraftforge.net/maven' }
jcenter()
mavenCentral()
maven { url='https://dist.creeper.host/Sponge/maven' }
maven { url = 'https://repo.spongepowered.org/maven/' }
// potential replacement in case of problems:
// https://dist.creeper.host/Sponge/maven
}
dependencies {
classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '3.+', changing: true
@@ -16,7 +18,7 @@ apply plugin: 'org.spongepowered.mixin'
apply plugin: 'eclipse'
apply plugin: 'maven-publish'
version = 'a1'
version = 'a1.2'
group = 'com.backsun.lod'
archivesBaseName = 'lod_1.16.4'
@@ -1,189 +0,0 @@
package com.backsun.lod.builders;
import java.awt.Color;
import org.lwjgl.opengl.GL11;
import com.backsun.lod.objects.NearFarBuffer;
import com.backsun.lod.renderer.LodRenderer;
import com.backsun.lod.util.enums.FogDistance;
import net.minecraft.client.renderer.BufferBuilder;
import net.minecraft.util.math.AxisAlignedBB;
/**
* This object is used to create NearFarBuffer objects.
*
* @author James Seibel
* @version 02-27-2021
*/
public class LodBufferBuilder
{
public BufferBuilder nearBuffer;
public BufferBuilder farBuffer;
public FogDistance distanceMode;
public AxisAlignedBB[][] lods;
public Color[][] colors;
public LodBufferBuilder()
{
}
public NearFarBuffer createBuffers(
BufferBuilder newNearBufferBuilder, BufferBuilder newFarBufferBuilder,
FogDistance newDistanceMode,
AxisAlignedBB[][] newLods, Color[][] newColors)
{
nearBuffer = newNearBufferBuilder;
farBuffer = newFarBufferBuilder;
distanceMode = newDistanceMode;
lods = newLods;
colors = newColors;
nearBuffer.begin(GL11.GL_QUADS, LodRenderer.LOD_VERTEX_FORMAT);
farBuffer.begin(GL11.GL_QUADS, LodRenderer.LOD_VERTEX_FORMAT);
int numbChunksWide = lods.length;
BufferBuilder currentBuffer;
AxisAlignedBB bb;
int red;
int green;
int blue;
int alpha;
// x axis
for (int i = 0; i < numbChunksWide; i++)
{
// z axis
for (int j = 0; j < numbChunksWide; j++)
{
if (lods[i][j] == null || colors[i][j] == null)
continue;
bb = lods[i][j];
// get the color of this LOD object
red = colors[i][j].getRed();
green = colors[i][j].getGreen();
blue = colors[i][j].getBlue();
alpha = colors[i][j].getAlpha();
if (isCoordinateInNearFogArea(i, j, numbChunksWide / 2))
currentBuffer = nearBuffer;
else
currentBuffer = farBuffer;
if (bb.minY != bb.maxY)
{
// top (facing up)
addPosAndColor(currentBuffer, bb.minX, bb.maxY, bb.minZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.minX, bb.maxY, bb.maxZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.maxX, bb.maxY, bb.maxZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.maxX, bb.maxY, bb.minZ, red, green, blue, alpha);
// bottom (facing down)
addPosAndColor(currentBuffer, bb.maxX, bb.minY, bb.minZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.maxX, bb.minY, bb.maxZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.minX, bb.minY, bb.maxZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.minX, bb.minY, bb.minZ, red, green, blue, alpha);
// south (facing -Z)
addPosAndColor(currentBuffer, bb.maxX, bb.minY, bb.maxZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.maxX, bb.maxY, bb.maxZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.minX, bb.maxY, bb.maxZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.minX, bb.minY, bb.maxZ, red, green, blue, alpha);
// north (facing +Z)
addPosAndColor(currentBuffer, bb.minX, bb.minY, bb.minZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.minX, bb.maxY, bb.minZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.maxX, bb.maxY, bb.minZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.maxX, bb.minY, bb.minZ, red, green, blue, alpha);
// west (facing -X)
addPosAndColor(currentBuffer, bb.minX, bb.minY, bb.minZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.minX, bb.minY, bb.maxZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.minX, bb.maxY, bb.maxZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.minX, bb.maxY, bb.minZ, red, green, blue, alpha);
// east (facing +X)
addPosAndColor(currentBuffer, bb.maxX, bb.maxY, bb.minZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.maxX, bb.maxY, bb.maxZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.maxX, bb.minY, bb.maxZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.maxX, bb.minY, bb.minZ, red, green, blue, alpha);
}
else
{
// render this LOD as one block thick
// top (facing up)
addPosAndColor(currentBuffer, bb.minX, bb.minY+1, bb.minZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.minX, bb.minY+1, bb.maxZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.maxX, bb.minY+1, bb.maxZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.maxX, bb.minY+1, bb.minZ, red, green, blue, alpha);
// bottom (facing down)
addPosAndColor(currentBuffer, bb.maxX, bb.minY, bb.minZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.maxX, bb.minY, bb.maxZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.minX, bb.minY, bb.maxZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.minX, bb.minY, bb.minZ, red, green, blue, alpha);
// south (facing -Z)
addPosAndColor(currentBuffer, bb.maxX, bb.minY, bb.maxZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.maxX, bb.minY+1, bb.maxZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.minX, bb.minY+1, bb.maxZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.minX, bb.minY, bb.maxZ, red, green, blue, alpha);
// north (facing +Z)
addPosAndColor(currentBuffer, bb.minX, bb.minY, bb.minZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.minX, bb.minY+1, bb.minZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.maxX, bb.minY+1, bb.minZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.maxX, bb.minY, bb.minZ, red, green, blue, alpha);
// west (facing -X)
addPosAndColor(currentBuffer, bb.minX, bb.minY, bb.minZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.minX, bb.minY, bb.maxZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.minX, bb.minY+1, bb.maxZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.minX, bb.minY+1, bb.minZ, red, green, blue, alpha);
// east (facing +X)
addPosAndColor(currentBuffer, bb.maxX, bb.minY+1, bb.minZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.maxX, bb.minY+1, bb.maxZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.maxX, bb.minY, bb.maxZ, red, green, blue, alpha);
addPosAndColor(currentBuffer, bb.maxX, bb.minY, bb.minZ, red, green, blue, alpha);
}
} // z axis
} // x axis
nearBuffer.finishDrawing();
farBuffer.finishDrawing();
return new NearFarBuffer(nearBuffer, farBuffer);
}
private void addPosAndColor(BufferBuilder buffer, double x, double y, double z, int red, int green, int blue, int alpha)
{
buffer.pos(x, y, z).color(red, green, blue, alpha).endVertex();
}
/**
* Find the coordinates that are in the center half of the given
* 2D matrix, starting at (0,0) and going to (2 * lodRadius, 2 * lodRadius).
*/
private static boolean isCoordinateInNearFogArea(int chunkX, int chunkZ, int lodRadius)
{
int halfRadius = lodRadius / 2;
return (chunkX >= lodRadius - halfRadius
&& chunkX <= lodRadius + halfRadius)
&&
(chunkZ >= lodRadius - halfRadius
&& chunkZ <= lodRadius + halfRadius);
}
}
@@ -1,124 +0,0 @@
package com.backsun.lod.builders;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import com.backsun.lod.handlers.LodDimensionFileHandler;
import com.backsun.lod.objects.LodChunk;
import com.backsun.lod.objects.LodDimension;
import com.backsun.lod.objects.LodWorld;
import com.backsun.lod.util.LodUtils;
import net.minecraft.world.DimensionType;
import net.minecraft.world.chunk.Chunk;
import net.minecraft.world.chunk.ChunkSection;
import net.minecraft.world.server.ServerWorld;
/**
* This object is in charge of creating Lod
* related objects.
* (specifically: Lod World, Dimension, Region, and Chunk objects)
*
* @author James Seibel
* @version 2-22-2021
*/
public class LodBuilder
{
private ExecutorService lodGenThreadPool = Executors.newSingleThreadExecutor();
public volatile LodWorld lodWorld;
/** Default size of any LOD regions we use */
public int regionWidth = 5;
public LodBuilder()
{
}
/**
* Returns LodWorld so that it can be passed
* to the LodRenderer.
*/
public LodWorld generateLodChunkAsync(Chunk chunk)
{
if (lodWorld != null)
// is this chunk from the same world as the lodWorld?
if (!lodWorld.worldName.equals(LodDimensionFileHandler.getCurrentWorldID()))
// we are not in the same world anymore,
// remove the old world so it can be recreated later
lodWorld = null;
// don't try to create an LOD object
// if for some reason we aren't
// given a valid chunk object
// (Minecraft often gives back empty
// or null chunks in this method)
if (chunk == null || !isValidChunk(chunk))
return lodWorld;
DimensionType dim = chunk.getWorld().getDimensionType();
ServerWorld world = LodUtils.getServerWorldFromDimension(dim);
if (world == null)
return lodWorld;
Thread thread = new Thread(() ->
{
try
{
LodChunk lod = new LodChunk(chunk, world);
LodDimension lodDim;
if (lodWorld == null)
{
lodWorld = new LodWorld(LodDimensionFileHandler.getCurrentWorldID());
}
if (lodWorld.getLodDimension(dim) == null)
{
lodDim = new LodDimension(dim, regionWidth);
lodWorld.addLodDimension(lodDim);
}
else
{
lodDim = lodWorld.getLodDimension(dim);
}
lodDim.addLod(lod);
}
catch(IllegalArgumentException | NullPointerException e)
{
// if the world changes while LODs are being generated
// they will throw errors as they try to access things that no longer
// exist.
}
});
lodGenThreadPool.execute(thread);
return lodWorld;
}
/**
* Return whether the given chunk
* has any data in it.
*/
public boolean isValidChunk(Chunk chunk)
{
ChunkSection[] blockStorage = chunk.getSections();
for(ChunkSection section : blockStorage)
{
if(section != null && !section.isEmpty())
{
return true;
}
}
return false;
}
}
@@ -1,289 +0,0 @@
package com.backsun.lod.handlers;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import com.backsun.lod.objects.LodChunk;
import com.backsun.lod.objects.LodDimension;
import com.backsun.lod.objects.LodRegion;
import com.backsun.lod.util.LodUtils;
import net.minecraft.client.Minecraft;
import net.minecraft.world.server.ServerChunkProvider;
import net.minecraft.world.server.ServerWorld;
/**
* This object handles creating LodRegions
* from files and saving LodRegion objects
* to file.
*
* @author James Seibel
* @version 01-30-2021
*/
public class LodDimensionFileHandler
{
private LodDimension loadedDimension = null;
public long regionLastWriteTime[][];
private File dimensionDataSaveFolder;
private final String FILE_NAME_PREFIX = "lod";
private final String FILE_EXTENSION = ".txt";
private ExecutorService fileWritingThreadPool = Executors.newFixedThreadPool(1);
public LodDimensionFileHandler(File newSaveFolder, LodDimension newLoadedDimension)
{
dimensionDataSaveFolder = newSaveFolder;
loadedDimension = newLoadedDimension;
// these two variable are used in sync with the LodDimension
regionLastWriteTime = new long[loadedDimension.getWidth()][loadedDimension.getWidth()];
for(int i = 0; i < loadedDimension.getWidth(); i++)
for(int j = 0; j < loadedDimension.getWidth(); j++)
regionLastWriteTime[i][j] = -1;
}
//================//
// read from file //
//================//
/**
* Return the LodRegion at the given coordinates.
* (null if the file doesn't exist)
*/
public LodRegion loadRegionFromFile(int regionX, int regionZ)
{
// we don't currently support reading or writing
// files when connected to a server
if (!Minecraft.getInstance().isIntegratedServerRunning())
return null;
if (!readyToReadAndWrite())
return null;
String fileName = getFileNameForRegion(regionX, regionZ);
File f = new File(fileName);
if (!f.exists())
{
// there wasn't a file, don't
// return anything
return null;
}
LodRegion region = new LodRegion(regionX, regionZ);
try
{
BufferedReader br = new BufferedReader(new FileReader(f));
String s = br.readLine();
while(s != null && !s.isEmpty())
{
try
{
// convert each line into an LOD object and add it to the region
LodChunk lod = new LodChunk(s);
region.addLod(lod);
}
catch(IllegalArgumentException e)
{
// we were unable to create this chunk
// for whatever reason.
// skip to the next chunk
}
s = br.readLine();
}
br.close();
}
catch (IOException e)
{
// File not found
// or the buffered reader encountered a
// problem reading the file
return null;
}
return region;
}
//==============//
// Save to File //
//==============//
/**
* Save all dirty regions in this LodDimension to file.
*/
public synchronized void saveDirtyRegionsToFileAsync()
{
// we don't currently support reading or writing
// files when connected to a server
if (!Minecraft.getInstance().isIntegratedServerRunning())
return;
if (!readyToReadAndWrite())
// we aren't ready to read and write yet
return;
fileWritingThreadPool.execute(saveDirtyRegionsThread);
}
private Thread saveDirtyRegionsThread = new Thread(() ->
{
for(int i = 0; i < loadedDimension.getWidth(); i++)
{
for(int j = 0; j < loadedDimension.getWidth(); j++)
{
if(loadedDimension.isRegionDirty[i][j])
{
saveRegionToDisk(loadedDimension.regions[i][j]);
loadedDimension.isRegionDirty[i][j] = false;
}
}
}
});
/**
* Save a specific region to disk.<br>
* Note: it will save to the LodDimension that this
* handler is associated with.
*/
private void saveRegionToDisk(LodRegion region)
{
if (!readyToReadAndWrite() || region == null)
return;
// convert chunk coordinates to region
// coordinates
int x = region.x;
int z = region.z;
File f = new File(getFileNameForRegion(x, z));
try
{
// make sure the file and folder exists
if (!f.exists())
if(!f.getParentFile().exists())
f.getParentFile().mkdirs();
f.createNewFile();
FileWriter fw = new FileWriter(f);
for(LodChunk[] chunkArray : region.getAllLods())
for(LodChunk chunk : chunkArray)
if(chunk != null)
fw.write(chunk.toData() + "\n");
fw.close();
}
catch(Exception e)
{
System.err.println("LOD file write error: " + e.getMessage());
}
}
//================//
// helper methods //
//================//
/**
* Return the name of the file that should contain the
* region at the given x and z. <br>
* Returns null if this object isn't ready to read and write.
*/
private String getFileNameForRegion(int regionX, int regionZ)
{
if (!readyToReadAndWrite())
return null;
try
{
// saveFolder is something like
// ".\Super Flat\DIM-1\data"
// or
// ".\Super Flat\data"
return dimensionDataSaveFolder.getCanonicalPath() + "\\lod\\" +
FILE_NAME_PREFIX + "." + regionX + "." + regionZ + FILE_EXTENSION;
}
catch(IOException e)
{
return null;
}
}
/**
* Returns if this FileHandler is ready to read
* and write files.
* <br>
* This returns true when the world save directory is known.
*/
public boolean readyToReadAndWrite()
{
return dimensionDataSaveFolder != null;
}
/**
* If on single player this will return the name of the user's
* world, if in multiplayer it will return the server name
* and game version.
*/
public static String getCurrentWorldID()
{
Minecraft mc = Minecraft.getInstance();
if(mc.isIntegratedServerRunning())
{
ServerWorld serverWorld = LodUtils.getFirstValidServerWorld();
if (serverWorld == null)
return "";
ServerChunkProvider provider = serverWorld.getChunkProvider();
if(provider != null)
return provider.getSavedData().folder.toString();
return "";
}
else
{
return mc.getCurrentServerData().serverName + "_version_" + mc.getCurrentServerData().gameVersion;
}
}
}
@@ -1,22 +0,0 @@
package com.backsun.lod.mixin;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import com.backsun.lod.renderer.RenderGlobalHook;
import com.mojang.blaze3d.matrix.MatrixStack;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.WorldRenderer;
@Mixin(WorldRenderer.class)
public class MixinWorldRenderer
{
@Inject(at = @At("HEAD"), method = "renderBlockLayer(Lnet/minecraft/client/renderer/RenderType;Lcom/mojang/blaze3d/matrix/MatrixStack;DDD)V", cancellable = false)
private void renderBlockLayer(RenderType blockLayerIn, MatrixStack matrixStackIn, double xIn, double yIn, double zIn, CallbackInfo callback)
{
RenderGlobalHook.startRenderingStencil(blockLayerIn);
}
}
@@ -1,776 +0,0 @@
package com.backsun.lod.objects;
import java.awt.Color;
import com.backsun.lod.util.enums.ColorDirection;
import com.backsun.lod.util.enums.LodCorner;
import com.backsun.lod.util.enums.LodLocation;
import net.minecraft.block.Blocks;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.color.BlockColors;
import net.minecraft.world.World;
import net.minecraft.world.chunk.Chunk;
import net.minecraft.world.chunk.ChunkSection;
/**
* This object contains position
* and color data for an LOD object.
*
* @author James Seibel
* @version 02-13-2021
*/
public class LodChunk
{
/** how many different pieces of data are in one line */
private static final int DATA_DELIMITER_COUNT = 28;
/** This is what separates each piece of data in the toData method */
public static final char DATA_DELIMITER = ',';
public static final int WIDTH = 16;
private static final int CHUNK_DATA_WIDTH = WIDTH;
private static final int CHUNK_DATA_HEIGHT = WIDTH;
/**
* This is how many blocks are
* required at a specific y-value
* to constitute a LOD point
*/
private final int LOD_BLOCK_REQ = 16;
// the max number of blocks per layer = 64 (8*8)
// since each layer is 1/4 the chunk
/** The x coordinate of the chunk. */
public int x;
/** The z coordinate of the chunk. */
public int z;
// each short is the height of
// that 8th of the chunk.
public short top[];
public short bottom[];
/** The average color of each 6 cardinal directions */
public Color colors[];
//==============//
// constructors //
//==============//
/**
* Create an empty invisible LodChunk at (0,0)
*/
public LodChunk()
{
x = 0;
z = 0;
top = new short[4];
bottom = new short[4];
colors = new Color[6];
// by default have the colors invisible
for(ColorDirection dir : ColorDirection.values())
{
colors[dir.value] = new Color(0, 0, 0, 0);
}
}
/**
* Creates an LodChunk from the string
* generated by the toData method.
*
* @throws IllegalArgumentException if the data isn't valid to create a LodChunk
* @throws NumberFormatException if the data can't be converted into an int at any point
*/
public LodChunk(String data) throws IllegalArgumentException, NumberFormatException
{
/*
* data format:
* x, z, top data, bottom data, rgb color data
*
* example:
* 5,8, 4,4,4,4, 0,0,0,0, 255,255,255, 255,255,255, 255,255,255, 255,255,255, 255,255,255, 255,255,255,
*/
// make sure there are the correct number of entries
// in the data string (28)
int count = 0;
for(int i = 0; i < data.length(); i++)
{
if(data.charAt(i) == DATA_DELIMITER)
{
count++;
}
}
if(count != DATA_DELIMITER_COUNT)
{
throw new IllegalArgumentException("LodChunk constructor givin an invalid string. The data given had " + count + " delimiters when it should have had " + DATA_DELIMITER_COUNT + ".");
}
// index we will use when going through the String
int index = 0;
int lastIndex = 0;
// x and z position
index = data.indexOf(DATA_DELIMITER, 0);
x = Integer.parseInt(data.substring(0,index));
lastIndex = index;
index = data.indexOf(DATA_DELIMITER, lastIndex + 1);
z = Integer.parseInt(data.substring(lastIndex+1,index));
// top
top = new short[4];
for(LodLocation loc : LodLocation.values())
{
lastIndex = index;
index = data.indexOf(DATA_DELIMITER, lastIndex + 1);
top[loc.value] = Short.parseShort(data.substring(lastIndex+1,index));
}
// bottom
bottom = new short[4];
for(LodLocation loc : LodLocation.values())
{
lastIndex = index;
index = data.indexOf(DATA_DELIMITER, lastIndex + 1);
bottom[loc.value] = Short.parseShort(data.substring(lastIndex+1,index));
}
// color
colors = new Color[6];
for(ColorDirection dir : ColorDirection.values())
{
int red = 0;
int green = 0;
int blue = 0;
// get r,g,b
for(int i = 0; i < 3; i++)
{
lastIndex = index;
index = data.indexOf(DATA_DELIMITER, lastIndex + 1);
String raw = "";
switch(i)
{
case 0:
raw = data.substring(lastIndex+1,index);
red = Short.parseShort(raw);
break;
case 1:
raw = data.substring(lastIndex+1,index);
green = Short.parseShort(raw);
break;
case 2:
raw = data.substring(lastIndex+1,index);
blue = Short.parseShort(raw);
break;
}
}
colors[dir.value] = new Color(red, green, blue);
}
}
/**
* Creates a LodChunk for a chunk in the given world. <br>
* Note: The world is required to determine each block's color
*
* @throws IllegalArgumentException
* thrown if either the chunk or world is null.
*/
public LodChunk(Chunk chunk, World world) throws IllegalArgumentException
{
if(chunk == null)
{
throw new IllegalArgumentException("LodChunk constructor given a null chunk");
}
if(world == null)
{
throw new IllegalArgumentException("LodChunk constructor given a null world");
}
x = chunk.getPos().x;
z = chunk.getPos().z;
top = new short[4];
bottom = new short[4];
colors = new Color[6];
// generate the top and bottom points of this LOD
for(LodLocation loc : LodLocation.values())
{
top[loc.value] = generateLodCorner(chunk, SectionGenerationMode.GENERATE_TOP, loc);
bottom[loc.value] = generateLodCorner(chunk, SectionGenerationMode.GENERATE_BOTTOM, loc);
}
// determine the average color for each direction
for(ColorDirection dir : ColorDirection.values())
{
colors[dir.value] = generateLodColorForDirection(chunk, world, dir);
}
}
//=====================//
// constructor helpers //
//=====================//
/**
* Generate the height for the given LodLocation, either the top or bottom.
* <br><br>
* If invalid/null/empty chunks are given
* crashes may occur.
*/
public short generateLodCorner(Chunk chunk, SectionGenerationMode generationMode, LodLocation lodLoc)
{
// should have a length of 16
// (each storage is 16x16x16 and the
// world height is 256)
ChunkSection[] chunkSections = chunk.getSections();
int startX = 0;
int endX = 0;
int startZ = 0;
int endZ = 0;
// determine where we should look in this
// chunk
switch(lodLoc)
{
case NE:
// -N
startZ = 0;
endZ = (CHUNK_DATA_WIDTH / 2) - 1;
// +E
startX = CHUNK_DATA_WIDTH / 2;
endX = CHUNK_DATA_WIDTH - 1;
break;
case SE:
// +S
startZ = CHUNK_DATA_WIDTH / 2;
endZ = CHUNK_DATA_WIDTH;
// +E
startX = CHUNK_DATA_WIDTH / 2;
endX = CHUNK_DATA_WIDTH;
break;
case SW:
// +S
startZ = CHUNK_DATA_WIDTH / 2;
endZ = CHUNK_DATA_WIDTH;
// -W
startX = 0;
endX = (CHUNK_DATA_WIDTH / 2) - 1;
break;
case NW:
// -N
startZ = 0;
endZ = CHUNK_DATA_WIDTH / 2;
// -W
startX = 0;
endX = CHUNK_DATA_WIDTH / 2;
break;
}
if(generationMode == SectionGenerationMode.GENERATE_TOP)
return determineTopPoint(chunkSections, startX, endX, startZ, endZ);
else
return determineBottomPoint(chunkSections, startX, endX, startZ, endZ);
}
/** GENERATE_TOP, GENERATE_BOTTOM */
private enum SectionGenerationMode
{
GENERATE_TOP,
GENERATE_BOTTOM;
}
/**
* Find the lowest valid point from the bottom.
*/
private short determineBottomPoint(ChunkSection[] chunkSections, int startX, int endX, int startZ, int endZ)
{
// search from the bottom up
for(int i = 0; i < chunkSections.length; i++)
{
for(int y = 0; y < CHUNK_DATA_HEIGHT; y++)
{
if(isLayerValidLodPoint(chunkSections, startX, endX, startZ, endZ, i, y))
{
// we found
// enough blocks in this
// layer to count as an
// LOD point
return (short) (y + (i * CHUNK_DATA_HEIGHT));
}
}
}
// we never found a valid LOD point
return -1;
}
/**
* Find the highest valid point from the Top
*/
private short determineTopPoint(ChunkSection[] chunkSections, int startX, int endX, int startZ, int endZ)
{
// search from the top down
for(int i = chunkSections.length - 1; i >= 0; i--)
{
for(int y = CHUNK_DATA_WIDTH - 1; y >= 0; y--)
{
if(isLayerValidLodPoint(chunkSections, startX, endX, startZ, endZ, i, y))
{
// we found
// enough blocks in this
// layer to count as an
// LOD point
return (short) (y + (i * CHUNK_DATA_HEIGHT));
}
}
}
// we never found a valid LOD point
return -1;
}
/**
* Is the layer between the given X, Z, and dataIndex
* values a valid LOD point?
*/
private boolean isLayerValidLodPoint(
ChunkSection[] chunkSections,
int startX, int endX,
int startZ, int endZ,
int sectionIndex, int y)
{
// search through this layer
int layerBlocks = 0;
for(int x = startX; x < endX; x++)
{
for(int z = startZ; z < endZ; z++)
{
if(chunkSections[sectionIndex] == null)
{
// this section doesn't have any blocks,
// it is not a valid section
return false;
}
else
{
if(chunkSections[sectionIndex].getBlockState(x, y, z) != null && chunkSections[sectionIndex].getBlockState(x, y, z).getBlock() != Blocks.AIR)
{
// we found a valid block in
// in this layer
layerBlocks++;
if(layerBlocks >= LOD_BLOCK_REQ)
{
return true;
}
}
}
} // z
} // x
return false;
}
/**
* Generate the color of the given ColorDirection at the given chunk
* in the given world.
*/
private Color generateLodColorForDirection(Chunk chunk, World world, ColorDirection colorDir)
{
Minecraft mc = Minecraft.getInstance();
BlockColors bc = mc.getBlockColors();
switch (colorDir)
{
case TOP:
return generateLodColorVertical(chunk, colorDir, world, bc);
case BOTTOM:
return generateLodColorVertical(chunk, colorDir, world, bc);
case N:
return generateLodColorHorizontal(chunk, colorDir, world, bc);
case S:
return generateLodColorHorizontal(chunk, colorDir, world, bc);
case E:
return generateLodColorHorizontal(chunk, colorDir, world, bc);
case W:
return generateLodColorHorizontal(chunk, colorDir, world, bc);
}
return new Color(0, 0, 0, 0);
}
/**
* Generates the color of the top or bottom of a given chunk in the given world.
*
* @throws IllegalArgumentException if given a ColorDirection other than TOP or BOTTOM
*/
private Color generateLodColorVertical(Chunk chunk, ColorDirection colorDir, World world, BlockColors bc)
{
if(colorDir != ColorDirection.TOP && colorDir != ColorDirection.BOTTOM)
{
throw new IllegalArgumentException("generateLodColorVertical only accepts the ColorDirection TOP or BOTTOM");
}
ChunkSection[] chunkSections = chunk.getSections();
int numbOfBlocks = 0;
int red = 0;
int green = 0;
int blue = 0;
boolean goTopDown = (colorDir == ColorDirection.TOP);
// either go top down or bottom up
int dataStart = goTopDown? chunkSections.length - 1 : 0;
int dataMax = chunkSections.length;
int dataMin = 0;
int dataIncrement = goTopDown? -1 : 1;
int topStart = goTopDown? CHUNK_DATA_HEIGHT - 1 : 0;
int topMax = CHUNK_DATA_HEIGHT;
int topMin = 0;
int topIncrement = goTopDown? -1 : 1;
for(int x = 0; x < CHUNK_DATA_WIDTH; x++)
{
for(int z = 0; z < CHUNK_DATA_WIDTH; z++)
{
boolean foundBlock = false;
for(int i = dataStart; !foundBlock && i >= dataMin && i < dataMax; i += dataIncrement)
{
if(!foundBlock && chunkSections[i] != null)
{
for(int y = topStart; !foundBlock && y >= topMin && y < topMax; y += topIncrement)
{
int ci;
ci = chunkSections[i].getBlockState(x, y, z).materialColor.colorValue;
if(ci == 0)
{
// skip air or invisible blocks
continue;
}
Color c = intToColor(ci);
red += c.getRed();
green += c.getGreen();
blue += c.getBlue();
numbOfBlocks++;
// we found a valid block, skip to the
// next x and z
foundBlock = true;
}
}
}
}
}
if(numbOfBlocks == 0)
numbOfBlocks = 1;
red /= numbOfBlocks;
green /= numbOfBlocks;
blue /= numbOfBlocks;
return new Color(red, green, blue);
}
/**
* Generates the color of the side of a given chunk in the given world for the given ColorDirection.
*
* @throws IllegalArgumentException if given a ColorDirection other than N, S, W, E (North, South, East, West)
*/
private Color generateLodColorHorizontal(Chunk chunk, ColorDirection colorDir, World world, BlockColors bc)
{
if(colorDir != ColorDirection.N && colorDir != ColorDirection.S && colorDir != ColorDirection.E && colorDir != ColorDirection.W)
{
throw new IllegalArgumentException("generateLodColorHorizontal only accepts the ColorDirection N (North), S (South), E (East), or W (West)");
}
ChunkSection[] chunkSections = chunk.getSections();
int numbOfBlocks = 0;
int red = 0;
int green = 0;
int blue = 0;
// these don't change since the over direction doesn't matter
int overStart = 0;
int overIncrement = 1;
// determine which direction is "in"
int inStart = 0;
int inIncrement = 1;
switch (colorDir)
{
case N:
inStart = 0;
inIncrement = 1;
break;
case S:
inStart = CHUNK_DATA_WIDTH - 1;
inIncrement = -1;
break;
case E:
inStart = 0;
inIncrement = 1;
break;
case W:
inStart = CHUNK_DATA_WIDTH - 1;
inIncrement = -1;
break;
default:
// we were given an invalid position, return invisible.
// this shouldn't happen and is mostly here to make the
// compiler happy
return new Color(0,0,0,0);
}
for (int i = 0; i < chunkSections.length; i++)
{
if (chunkSections[i] != null)
{
for (int y = 0; y < CHUNK_DATA_HEIGHT; y++)
{
boolean foundBlock = false;
// over moves "over" the side of the chunk
// in moves "into" the chunk until it finds a block
for (int over = overStart; !foundBlock && over >= 0 && over < CHUNK_DATA_WIDTH; over += overIncrement)
{
for (int in = inStart; !foundBlock && in >= 0 && in < CHUNK_DATA_WIDTH; in += inIncrement)
{
int x = -1;
int z = -1;
// determine which should be X and Z
switch(colorDir)
{
case N:
x = over;
z = in;
break;
case S:
x = over;
z = in;
break;
case E:
x = in;
z = over;
break;
case W:
x = in;
z = over;
break;
default:
// this will never happen, it would have
// been caught by the switch before the loops
break;
}
int ci;
ci = chunkSections[i].getBlockState(x, y, z).getMaterial().getColor().colorValue;
if (ci == 0) {
// skip air or invisible blocks
continue;
}
Color c = intToColor(ci);
red += c.getRed();
green += c.getGreen();
blue += c.getBlue();
numbOfBlocks++;
// we found a valid block, skip to the
// next x and z
foundBlock = true;
}
}
}
}
}
if(numbOfBlocks == 0)
numbOfBlocks = 1;
red /= numbOfBlocks;
green /= numbOfBlocks;
blue /= numbOfBlocks;
return new Color(red, green, blue);
}
/**
* Convert a BlockColors int into a Color object.
*/
private Color intToColor(int num)
{
int filter = 0b11111111;
int red = (num >> 16 ) & filter;
int green = (num >> 8 ) & filter;
int blue = num & filter;
return new Color(red, green, blue);
}
/**
* Convert a Color into a BlockColors object.
*/
@SuppressWarnings("unused")
private int colorToInt(Color color)
{
return color.getRGB();
}
//================//
// misc functions //
//================//
/**
* If this LOD is either invisible from every
* direction or doesn't have a valid height
* it is empty.
*/
public boolean isLodEmpty()
{
for(LodCorner corner : LodCorner.values())
if(top[corner.value] != -1 || bottom[corner.value] != -1)
// at least one corner is valid
return false;
Color invisible = new Color(0,0,0,0);
for(ColorDirection dir : ColorDirection.values())
if(!colors[dir.value].equals(invisible))
// at least one direction has a non-invisible color
return false;
return true;
}
//========//
// output //
//========//
/**
* Outputs all data in csv format
* with the given delimiter.
* <br>
* Exports data in the form:
* <br>
* x, z, top data, bottom data, rgb color data
*
* <br>
* example output:
* <br>
* 5,8, 4,4,4,4, 0,0,0,0 255,255,255, 255,255,255, 255,255,255, 255,255,255, 255,255,255, 255,255,255,
*/
public String toData()
{
String s = "";
s += Integer.toString(x) + DATA_DELIMITER + Integer.toString(z) + DATA_DELIMITER;
for(int i = 0; i < top.length; i++)
{
s += Short.toString(top[i]) + DATA_DELIMITER;
}
for(int i = 0; i < bottom.length; i++)
{
s += Short.toString(bottom[i]) + DATA_DELIMITER;
}
for(int i = 0; i < colors.length; i++)
{
s += Integer.toString(colors[i].getRed()) + DATA_DELIMITER + Integer.toString(colors[i].getGreen()) + DATA_DELIMITER + Integer.toString(colors[i].getBlue()) + DATA_DELIMITER;
}
return s;
}
@Override
public String toString()
{
String s = "";
s += "x: " + x + " z: " + z + "\t";
s += "(" + colors[ColorDirection.TOP.value].getRed() + ", " + colors[ColorDirection.TOP.value].getGreen() + ", " + colors[ColorDirection.TOP.value].getBlue() + ")";
return s;
}
}
@@ -1,65 +0,0 @@
package com.backsun.lod.objects;
import java.util.Hashtable;
import java.util.Map;
import net.minecraft.world.DimensionType;
/**
* This stores all LODs for a given world.
*
* @author James Seibel
* @version 02-22-2021
*/
public class LodWorld
{
public String worldName;
/**
* Key = Dimension id (as an int)
*/
private Map<DimensionType, LodDimension> lodDimensions;
public LodWorld(String newWorldName)
{
worldName = newWorldName;
lodDimensions = new Hashtable<DimensionType, LodDimension>();
}
public void addLodDimension(LodDimension newStorage)
{
lodDimensions.put(newStorage.dimension, newStorage);
}
public LodDimension getLodDimension(DimensionType dimension)
{
return lodDimensions.get(dimension);
}
/**
* Resizes the max width in regions that each LodDimension
* should use.
*/
public void resizeDimensionRegionWidth(int newWidth)
{
for(DimensionType key : lodDimensions.keySet())
lodDimensions.get(key).setRegionWidth(newWidth);
}
@Override
public String toString()
{
String s = "";
s += worldName + "\t - dimensions: ";
for(DimensionType key : lodDimensions.keySet())
s += lodDimensions.get(key).dimension.toString() + ", ";
return s;
}
}
@@ -1,129 +0,0 @@
package com.backsun.lod.proxy;
import org.lwjgl.opengl.GL11;
import com.backsun.lod.builders.LodBuilder;
import com.backsun.lod.handlers.LodDimensionFileHandler;
import com.backsun.lod.objects.LodChunk;
import com.backsun.lod.objects.LodDimension;
import com.backsun.lod.objects.LodRegion;
import com.backsun.lod.objects.LodWorld;
import com.backsun.lod.renderer.LodRenderer;
import com.backsun.lod.renderer.RenderGlobalHook;
import com.backsun.lod.util.LodConfig;
import net.minecraft.client.Minecraft;
import net.minecraft.world.chunk.Chunk;
import net.minecraftforge.client.event.RenderWorldLastEvent;
import net.minecraftforge.event.world.ChunkEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
//TODO Find a way to replace getIntegratedServer so this mod could be used on non-local worlds.
// Minecraft.getMinecraft().getIntegratedServer()
/**
* This handles all events sent to the client,
* and is the starting point for most of this program.
*
* @author James_Seibel
* @version 02-23-2021
*/
public class ClientProxy
{
private LodRenderer renderer;
private LodWorld lodWorld;
private LodBuilder lodBuilder;
Minecraft mc = Minecraft.getInstance();
public ClientProxy()
{
lodBuilder = new LodBuilder();
renderer = new LodRenderer();
}
//==============//
// render event //
//==============//
@SubscribeEvent
public void renderWorldLast(RenderWorldLastEvent event)
{
RenderGlobalHook.endRenderingStencil();
GL11.glStencilFunc(GL11.GL_EQUAL, 0, 0xFF);
if (LodConfig.CLIENT.drawLODs.get())
renderLods(event.getPartialTicks());
GL11.glDisable(GL11.GL_STENCIL_TEST);
}
/**
* Do any setup that is required to draw LODs
* and then tell the LodRenderer to draw.
*/
public void renderLods(float partialTicks)
{
// update the
int newWidth = Math.max(4, (mc.gameSettings.renderDistanceChunks * LodChunk.WIDTH * 2) / LodRegion.SIZE);
if (lodWorld != null && lodBuilder.regionWidth != newWidth)
{
lodWorld.resizeDimensionRegionWidth(newWidth);
lodBuilder.regionWidth = newWidth;
// skip this frame, hopefully the lodWorld
// should have everything set up by then
return;
}
// are we still in the same world?
if (!lodWorld.worldName.equals(LodDimensionFileHandler.getCurrentWorldID()))
// no, don't render the wrong world
return;
if (mc == null || mc.player == null || lodWorld == null)
return;
LodDimension lodDim = lodWorld.getLodDimension(mc.player.world.getDimensionType());
if (lodDim == null)
return;
// offset the regions
double playerX = mc.player.getPosX();
double playerZ = mc.player.getPosZ();
int xOffset = ((int)playerX / (LodChunk.WIDTH * LodRegion.SIZE)) - lodDim.getCenterX();
int zOffset = ((int)playerZ / (LodChunk.WIDTH * LodRegion.SIZE)) - lodDim.getCenterZ();
if (xOffset != 0 || zOffset != 0)
{
lodDim.move(xOffset, zOffset);
}
renderer.drawLODs(lodDim, partialTicks, mc.getProfiler());
}
//=====================//
// lod creation events //
//=====================//
@SubscribeEvent
public void chunkLoadEvent(ChunkEvent.Load event)
{
if (event.getChunk().getClass() == Chunk.class)
lodWorld = lodBuilder.generateLodChunkAsync((Chunk) event.getChunk());
}
}
@@ -1,82 +0,0 @@
package com.backsun.lod.renderer;
import org.lwjgl.opengl.GL11;
import net.minecraft.client.renderer.RenderType;
/**
* This code is used for drawing
* to the stencil buffer.
*
* @author James Seibel
* @version 02-17-2021
*/
public class RenderGlobalHook
{
/**
* this variable should be the same as the method name below.
* It is used when transforming the RenderGlobal class'
* renderBlockLayer method.
*/
public static final String START_STENCIL_METHOD_NAME = "startRenderingStencil";
/**
* This method tells OpenGL to start drawing everything to the stencil.
* This is done to prevent LODs from being rendered on top of the world.
* <br><br>
* Called in the order (as of minecraft 1.16.4): <br>
* RenderType.getSolid() <br>
* RenderType.getCutoutMipped() <br>
* RenderType.getCutout() <br>
* RenderType.getTranslucent() <br>
* RenderType.getTripwire() <br>
*/
public static void startRenderingStencil(RenderType blockLayerIn)
{
// we only enable drawing to the stencil once since
// we want to skip the rendering of the out of world fog
// but catch everything else
if (blockLayerIn == RenderType.getSolid())
{
// solid is the first layer rendered
// clear the buffer so we can start fresh.
// if this isn't cleared first we will have overlap with the fog
// outside the world
GL11.glClearStencil(0);
GL11.glStencilMask(0x11111111);
GL11.glClear(GL11.GL_STENCIL_BUFFER_BIT);
GL11.glEnable(GL11.GL_STENCIL_TEST);
GL11.glStencilFunc(GL11.GL_ALWAYS, 1, 0x11111111);
GL11.glStencilMask(0b11111111);
GL11.glStencilOp(GL11.GL_KEEP, // this doesn't mater since GL_ALWAYS is being used
GL11.GL_KEEP, // stencil test passes
GL11.GL_REPLACE); // stencil + depth pass
}
}
/**
* this variable should be the same as the method name below.
* It is used when transforming the RenderGlobal class'
* renderBlockLayer method.
*/
public static final String END_STENCIL_METHOD_NAME = "endRenderingStencil";
/**
* Currently this method isn't used in any transformations since we end
* the stencil drawing in the ClientProxy right before we draw the LODs.
*/
public static void endRenderingStencil(RenderType blockLayerIn)
{
GL11.glStencilOp(GL11.GL_KEEP, // this doesn't mater since GL_ALWAYS is being used
GL11.GL_KEEP, // stencil test passes
GL11.GL_KEEP); // stencil + depth pass
}
public static void endRenderingStencil()
{
endRenderingStencil(null);
}
}
@@ -1,105 +0,0 @@
package com.backsun.lod.util;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.logging.log4j.LogManager;
import com.backsun.lod.ModInfo;
import com.backsun.lod.util.enums.FogDistance;
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
import com.electronwill.nightconfig.core.io.WritingMode;
import net.minecraftforge.common.ForgeConfigSpec;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.config.ModConfig;
/**
*
* @author James Seibel
* @version 02-27-2021
*/
@Mod.EventBusSubscriber
public class LodConfig
{
public static class Client
{
public ForgeConfigSpec.BooleanValue drawLODs;
public ForgeConfigSpec.EnumValue<FogDistance> fogDistance;
public ForgeConfigSpec.BooleanValue drawCheckerBoard;
Client(ForgeConfigSpec.Builder builder)
{
builder.comment(ModInfo.MODNAME + " configuration settings").push("client");
drawLODs = builder
.comment("If false LODs will not be drawn, \n"
+ "however they will still be generated \n"
+ "and saved to file for later use.")
.define("drawLODs", true);
fogDistance = builder
.comment("\n"
+ "At what distance should Fog be drawn on the LODs? \n"
+ "If fog cuts off ubruptly or you are using Optifine's \"fast\" \n"
+ "fog option set this to " + FogDistance.NEAR.toString() + " or " + FogDistance.FAR.toString() + ".")
.defineEnum("fogDistance", FogDistance.NEAR_AND_FAR);
drawCheckerBoard = builder
.comment("\n"
+ "If false the LODs will draw with their normal world colors. \n"
+ "If true they will draw as a black and white checkerboard. \n"
+ "This can be used for debugging or imagining you are playing a \n"
+ "giant game of chess ;)")
.define("drawCheckerBoard", false);
builder.pop();
}
}
/**
* {@link Path} to the configuration file of this mod
*/
private static final Path CONFIG_PATH =
Paths.get("config", ModInfo.MODID + ".toml");
public static final ForgeConfigSpec clientSpec;
public static final Client CLIENT;
static {
final Pair<Client, ForgeConfigSpec> specPair = new ForgeConfigSpec.Builder().configure(Client::new);
clientSpec = specPair.getRight();
CLIENT = specPair.getLeft();
// setup the config file
CommentedFileConfig config = CommentedFileConfig.builder(CONFIG_PATH)
.writingMode(WritingMode.REPLACE)
.build();
config.load();
config.save();
clientSpec.setConfig(config);
}
@SubscribeEvent
public static void onLoad(final ModConfig.Loading configEvent)
{
LogManager.getLogger().debug(ModInfo.MODNAME, "Loaded forge config file {}", configEvent.getConfig().getFileName());
}
@SubscribeEvent
public static void onFileChange(final ModConfig.Reloading configEvent)
{
LogManager.getLogger().debug(ModInfo.MODNAME, "Forge config just got changed on the file system!");
}
}
@@ -1,63 +0,0 @@
package com.backsun.lod.util;
import net.minecraft.client.Minecraft;
import net.minecraft.server.integrated.IntegratedServer;
import net.minecraft.world.DimensionType;
import net.minecraft.world.server.ServerWorld;
/**
* This class holds methods that may be used in multiple places.
*
* @author James Seibel
* @version 02-26-2021
*/
public class LodUtils
{
private static Minecraft mc = Minecraft.getInstance();
/**
* Gets the first valid ServerWorld.
*
* @return null if there are no ServerWorlds
*/
public static ServerWorld getFirstValidServerWorld()
{
if (mc.getIntegratedServer() == null)
return null;
Iterable<ServerWorld> worlds = mc.getIntegratedServer().getWorlds();
for (ServerWorld world : worlds)
return world;
return null;
}
/**
* Gets the ServerWorld for the relevant dimension.
*
* @return null if there is no ServerWorld for the given dimension
*/
public static ServerWorld getServerWorldFromDimension(DimensionType dimension)
{
IntegratedServer server = mc.getIntegratedServer();
if (server == null)
return null;
Iterable<ServerWorld> worlds = server.getWorlds();
ServerWorld returnWorld = null;
for (ServerWorld world : worlds)
{
if(world.getDimensionType() == dimension)
{
returnWorld = world;
break;
}
}
return returnWorld;
}
}
@@ -1,32 +0,0 @@
package com.backsun.lod.util.enums;
/**
* @author James Seibel
* @version 1-23-2021
*/
public enum DrawMode
{
/** Draw the LOD objects in groups.
* <br>
* <br>
* Fancy fog: render the center and outside LOD
* objects in 2 different groups.
* <br>
* Fast fog: render all LOD objects at one time.
*/
BATCH(0),
/** Draw each LOD objects separately.
* <br>
* <br>
* Not suggested normally since draw calls are GPU expensive.
*/
INDIVIDUAL(5);
public final int value;
private DrawMode(int newValue)
{
value = newValue;
}
}
@@ -1,29 +0,0 @@
package com.backsun.lod.util.enums;
/**
*
* @author James Seibel
* @version 1-20-2020
*
* NE, SE, SW, NW
*/
public enum LodLocation
{
// used for position
/** -Z, +X */
NE(0),
/** +Z, +X */
SE(1),
/** +Z, -X */
SW(2),
/** -Z, -X */
NW(3);
public final int value;
private LodLocation(int newValue)
{
value = newValue;
}
}
@@ -1,7 +1,7 @@
package com.backsun.lod;
package com.seibel.lod;
import com.backsun.lod.proxy.ClientProxy;
import com.backsun.lod.util.LodConfig;
import com.seibel.lod.proxy.ClientProxy;
import com.seibel.lod.util.LodConfig;
import net.minecraft.client.Minecraft;
import net.minecraftforge.common.MinecraftForge;
@@ -1,15 +1,15 @@
package com.backsun.lod;
package com.seibel.lod;
/**
* This file is similar to mcmod.info
*
* @author James Seibel
* @version 02-17-2021
* @version 05-31-2021
*/
public final class ModInfo
{
public static final String MODID = "lod";
public static final String MODNAME = "Levels of Detail";
public static final String MODAPI = "LodAPI";
public static final String VERSION = "1.0";
public static final String VERSION = "a1.2";
}
@@ -0,0 +1,333 @@
package com.seibel.lod.builders;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.lwjgl.opengl.GL11;
import com.seibel.lod.objects.LodChunk;
import com.seibel.lod.objects.LodDimension;
import com.seibel.lod.objects.NearFarBuffer;
import com.seibel.lod.render.LodRender;
import com.seibel.lod.util.LodConfig;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.BufferBuilder;
import net.minecraft.util.math.ChunkPos;
import net.minecraftforge.common.WorldWorkerManager;
/**
* This object is used to create NearFarBuffer objects.
*
* @author James Seibel
* @version 05-06-2021
*/
public class LodBufferBuilder
{
private Minecraft mc;
/** This holds the thread used to generate new LODs off the main thread. */
private ExecutorService genThread = Executors.newSingleThreadExecutor();
private LodBuilder lodBuilder;
/** The buffers that are used to create LODs using near fog */
public volatile BufferBuilder buildableNearBuffer;
/** The buffers that are used to create LODs using far fog */
public volatile BufferBuilder buildableFarBuffer;
/** if this is true the LOD buffers are currently being
* regenerated. */
public volatile boolean generatingBuffers = false;
/** if this is true new LOD buffers have been generated
* and are waiting to be swapped with the drawable buffers*/
private volatile boolean switchBuffers = false;
/** If this is greater than 0 no new chunk generation requests will be made
* this is to prevent chunks from being generated for a long time in an area
* the player is no longer in. */
public int numberOfChunksWaitingToGenerate = 0;
/** how many chunks to generate outside of the player's
* view distance at one time. (or more specifically how
* many requests to make at one time) */
public int maxChunkGenRequests = 8;
public LodBufferBuilder(LodBuilder newLodBuilder)
{
mc = Minecraft.getInstance();
lodBuilder = newLodBuilder;
}
/**
* Create a thread to asynchronously generate LOD buffers
* centered around the given camera X and Z.
* <br>
* This method will write to the drawableNearBuffers and drawableFarBuffers.
* <br>
* After the buildable buffers have been generated they must be
* swapped with the drawable buffers in the LodRenderer to be drawn.
*/
public void generateLodBuffersAsync(LodRender renderer, LodDimension lodDim,
double playerX, double playerZ, int numbChunksWide)
{
// only allow one generation process to happen at a time
if (generatingBuffers)
return;
if (buildableNearBuffer == null || buildableFarBuffer == null)
throw new IllegalStateException("generateLodBuffersAsync was called before the buildableNearBuffer and buildableFarBuffer were created.");
generatingBuffers = true;
// this seemingly useless math is required,
// just using (int) playerX/Z doesn't work
int playerXChunkOffset = ((int) playerX / LodChunk.WIDTH) * LodChunk.WIDTH;
int playerZChunkOffset = ((int) playerZ / LodChunk.WIDTH) * LodChunk.WIDTH;
// this is where we will start drawing squares
// (exactly half the total width)
int startX = (-LodChunk.WIDTH * (numbChunksWide / 2)) + playerXChunkOffset;
int startZ = (-LodChunk.WIDTH * (numbChunksWide / 2)) + playerZChunkOffset;
Thread t = new Thread(()->
{
// index of the chunk currently being added to the
// generation list
int chunkGenIndex = 0;
ChunkPos[] chunksToGen = new ChunkPos[maxChunkGenRequests];
int minChunkDist = Integer.MAX_VALUE;
ChunkPos playerChunkPos = new ChunkPos((int)playerX / LodChunk.WIDTH, (int)playerZ / LodChunk.WIDTH);
// generate our new buildable buffers
buildableNearBuffer.begin(GL11.GL_QUADS, LodRender.LOD_VERTEX_FORMAT);
buildableFarBuffer.begin(GL11.GL_QUADS, LodRender.LOD_VERTEX_FORMAT);
// x axis
for (int i = 0; i < numbChunksWide; i++)
{
// z axis
for (int j = 0; j < numbChunksWide; j++)
{
// skip the middle
// (As the player moves some chunks will overlap or be missing,
// this is just how chunk loading/unloading works. This can hopefully
// be hidden with careful use of fog)
int middle = (numbChunksWide / 2);
if (isCoordInCenterArea(i, j, middle))
{
continue;
}
// set where this square will be drawn in the world
double xOffset = (LodChunk.WIDTH * i) + // offset by the number of LOD blocks
startX; // offset so the center LOD block is centered underneath the player
double yOffset = 0;
double zOffset = (LodChunk.WIDTH * j) + startZ;
int chunkX = i + (startX / LodChunk.WIDTH);
int chunkZ = j + (startZ / LodChunk.WIDTH);
LodChunk lod = lodDim.getLodFromCoordinates(chunkX, chunkZ);
if (lod == null || lod.isLodEmpty())
{
// generate a new chunk if no chunk currently exists
// and we aren't waiting on any other chunks to generate
if (lod == null && numberOfChunksWaitingToGenerate == 0)
{
ChunkPos pos = new ChunkPos(chunkX, chunkZ);
// determine if this position is closer to the player
// than the previous
int newDistance = playerChunkPos.getChessboardDistance(pos);
if (newDistance < minChunkDist)
{
// this chunk is closer, clear any previous
// positions and update the new minimum distance
minChunkDist = newDistance;
chunkGenIndex = 0;
chunksToGen = new ChunkPos[maxChunkGenRequests];
chunksToGen[chunkGenIndex] = pos;
chunkGenIndex++;
}
else if (newDistance <= minChunkDist)
{
// this chunk position is as close or closers than the
// minimum distance
if(chunkGenIndex < maxChunkGenRequests)
{
// we are still under the number of chunks to generate
// add this position to the list
chunksToGen[chunkGenIndex] = pos;
chunkGenIndex++;
}
}
}
// don't render this null chunk
continue;
}
BufferBuilder currentBuffer = null;
if (isCoordinateInNearFogArea(i, j, numbChunksWide / 2))
currentBuffer = buildableNearBuffer;
else
currentBuffer = buildableFarBuffer;
// get the desired LodTemplate and
// add this LOD to the buffer
LodConfig.CLIENT.lodTemplate.get().template.
addLodToBuffer(currentBuffer, lodDim, lod,
xOffset, yOffset, zOffset, renderer.debugging);
}
}
// TODO add a way for a server side mod to generate chunks requested here
if(mc.isIntegratedServerRunning())
{
// start chunk generation
for(ChunkPos chunkPos : chunksToGen)
{
if(chunkPos == null)
break;
numberOfChunksWaitingToGenerate++;
LodChunkGenWorker genWorker = new LodChunkGenWorker(chunkPos, renderer, lodBuilder, this, lodDim);
WorldWorkerManager.addWorker(genWorker);
}
}
// finish the buffer building
buildableNearBuffer.finishDrawing();
buildableFarBuffer.finishDrawing();
// mark that the buildable buffers as ready to swap
generatingBuffers = false;
switchBuffers = true;
});
genThread.execute(t);
return;
}
//====================//
// generation helpers //
//====================//
/**
* Returns if the given coordinate is in the loaded area of the world.
* @param centerCoordinate the center of the loaded world
*/
private boolean isCoordInCenterArea(int i, int j, int centerCoordinate)
{
return (i >= centerCoordinate - mc.gameSettings.renderDistanceChunks
&& i <= centerCoordinate + mc.gameSettings.renderDistanceChunks)
&&
(j >= centerCoordinate - mc.gameSettings.renderDistanceChunks
&& j <= centerCoordinate + mc.gameSettings.renderDistanceChunks);
}
/**
* Find the coordinates that are in the center half of the given
* 2D matrix, starting at (0,0) and going to (2 * lodRadius, 2 * lodRadius).
*/
private static boolean isCoordinateInNearFogArea(int chunkX, int chunkZ, int lodRadius)
{
int halfRadius = lodRadius / 2;
return (chunkX >= lodRadius - halfRadius
&& chunkX <= lodRadius + halfRadius)
&&
(chunkZ >= lodRadius - halfRadius
&& chunkZ <= lodRadius + halfRadius);
}
//===============================//
// BufferBuilder related methods //
//===============================//
/**
* Called from the LodRenderer to create the
* BufferBuilders at the right size.
*
* @param bufferMaxCapacity
*/
public void setupBuffers(int bufferMaxCapacity)
{
buildableNearBuffer = new BufferBuilder(bufferMaxCapacity);
buildableFarBuffer = new BufferBuilder(bufferMaxCapacity);
}
/**
* Swap the drawable and buildable buffers and return
* the old drawable buffers.
* @param drawableNearBuffer
* @param drawableFarBuffer
*/
public NearFarBuffer swapBuffers(BufferBuilder drawableNearBuffer, BufferBuilder drawableFarBuffer)
{
// swap the BufferBuilders
BufferBuilder tmp = buildableNearBuffer;
buildableNearBuffer = drawableNearBuffer;
drawableNearBuffer = tmp;
tmp = buildableFarBuffer;
buildableFarBuffer = drawableFarBuffer;
drawableFarBuffer = tmp;
// the buffers have been swapped
switchBuffers = false;
return new NearFarBuffer(drawableNearBuffer, drawableFarBuffer);
}
/**
* If this is true the buildable near and far
* buffers have been generated and are ready to be
* sent to the LodRenderer.
*/
public boolean newBuffersAvaliable()
{
return switchBuffers;
}
}
@@ -0,0 +1,663 @@
package com.seibel.lod.builders;
import java.awt.Color;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import com.seibel.lod.enums.ColorDirection;
import com.seibel.lod.objects.LodChunk;
import com.seibel.lod.objects.LodDimension;
import com.seibel.lod.objects.LodWorld;
import com.seibel.lod.util.LodUtils;
import net.minecraft.block.BlockState;
import net.minecraft.block.Blocks;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.color.BlockColors;
import net.minecraft.world.DimensionType;
import net.minecraft.world.IWorld;
import net.minecraft.world.chunk.ChunkSection;
import net.minecraft.world.chunk.IChunk;
import net.minecraft.world.gen.Heightmap;
/**
* This object is in charge of creating Lod
* related objects.
* (specifically: Lod World, Dimension, Region, and Chunk objects)
*
* @author James Seibel
* @version 5-29-2021
*/
public class LodBuilder
{
private static final Color INVISIBLE = new Color(0,0,0,0);
private ExecutorService lodGenThreadPool = Executors.newSingleThreadExecutor();
/** Default size of any LOD regions we use */
public int regionWidth = 5;
public static final int CHUNK_DATA_WIDTH = LodChunk.WIDTH;
public static final int CHUNK_SECTION_HEIGHT = LodChunk.WIDTH;
/**
* This is how many blocks are
* required at a specific y-value
* to constitute a LOD point
*/
private final int LOD_BLOCK_REQ = 32;
// the max number of blocks per layer = 64 (8*8)
public LodBuilder()
{
}
public void generateLodChunkAsync(IChunk chunk, LodWorld lodWorld, IWorld world)
{
if (lodWorld == null || !lodWorld.getIsWorldLoaded())
return;
// is this chunk from the same world as the lodWorld?
if (!lodWorld.getWorldName().equals(LodUtils.getWorldID(world)))
// we are not in the same world anymore
// don't add this LOD
return;
// don't try to create an LOD object
// if for some reason we aren't
// given a valid chunk object
if (chunk == null)
return;
Thread thread = new Thread(() ->
{
try
{
DimensionType dim = world.getDimensionType();
LodChunk lod = generateLodFromChunk(chunk);
LodDimension lodDim;
if (lodWorld.getLodDimension(dim) == null)
{
lodDim = new LodDimension(dim, lodWorld, regionWidth);
lodWorld.addLodDimension(lodDim);
}
else
{
lodDim = lodWorld.getLodDimension(dim);
}
lodDim.addLod(lod);
}
catch(IllegalArgumentException | NullPointerException e)
{
// if the world changes while LODs are being generated
// they will throw errors as they try to access things that no longer
// exist.
}
});
lodGenThreadPool.execute(thread);
return;
}
/**
* Creates a LodChunk for a chunk in the given world.
*
* @throws IllegalArgumentException
* thrown if either the chunk or world is null.
*/
public LodChunk generateLodFromChunk(IChunk chunk) throws IllegalArgumentException
{
if(chunk == null)
{
throw new IllegalArgumentException("generateLodFromChunk given a null chunk");
}
Color[] colors = new Color[ColorDirection.values().length];
short height = determineTopPoint(chunk.getSections());
short depth = determineBottomPoint(chunk.getSections());
// determine the average color for each direction
for(ColorDirection dir : ColorDirection.values())
{
colors[dir.value] = generateLodColorForDirection(chunk, dir);
}
// check to see if there are any invisible sides
for(ColorDirection dir : ColorDirection.values())
{
// if there are any invisible sides
// replace them with the top side
// (this is done to make sure oceans and other totally
// flat locations have the correct side colors)
if (dir == ColorDirection.BOTTOM || dir == ColorDirection.TOP)
continue;
if (colors[dir.value] == INVISIBLE)
colors[dir.value] = colors[ColorDirection.TOP.value];
}
return new LodChunk(chunk.getPos(), height, depth, colors);
}
//=====================//
// constructor helpers //
//=====================//
/**
* Find the lowest valid point from the bottom.
*/
private short determineBottomPoint(ChunkSection[] chunkSections)
{
// search from the bottom up
for(int i = 0; i < CHUNK_DATA_WIDTH; i++)
{
for(int y = 0; y < CHUNK_SECTION_HEIGHT; y++)
{
if(isLayerValidLodPoint(chunkSections, i, y))
{
// we found
// enough blocks in this
// layer to count as an
// LOD point
return (short) (y + (i * CHUNK_SECTION_HEIGHT));
}
}
}
// we never found a valid LOD point
return -1;
}
/**
* Find the lowest valid point from the bottom.
*/
@SuppressWarnings("unused")
private short determineBottomPoint(Heightmap heightmap)
{
// the heightmap only shows how high the blocks go, it
// doesn't have any info about how low they go
return 0;
}
/**
* Find the highest valid point from the Top
*/
private short determineTopPoint(ChunkSection[] chunkSections)
{
// search from the top down
for(int section = chunkSections.length - 1; section >= 0; section--)
{
for(int y = CHUNK_DATA_WIDTH - 1; y >= 0; y--)
{
if(isLayerValidLodPoint(chunkSections, section, y))
{
// we found
// enough blocks in this
// layer to count as an
// LOD point
return (short) (y + (section * CHUNK_SECTION_HEIGHT));
}
}
}
// we never found a valid LOD point
return -1;
}
/**
* Find the highest point from the Top
*/
@SuppressWarnings("unused")
private short determineTopPoint(Heightmap heightmap, int endZ)
{
short highest = 0;
for(int x = 0; x < LodChunk.WIDTH; x++)
{
for(int z = 0; z < LodChunk.WIDTH; z++)
{
short newHeight = (short) heightmap.getHeight(x, z);
if (newHeight > highest)
highest = newHeight;
}
}
return highest;
}
/**
* Generate the color for the given chunk in the given ColorDirection.
*/
private Color generateLodColorForDirection(IChunk chunk, ColorDirection colorDir)
{
Minecraft mc = Minecraft.getInstance();
BlockColors bc = mc.getBlockColors();
switch (colorDir)
{
case TOP:
return generateLodColorVertical(chunk, colorDir, bc);
case BOTTOM:
return generateLodColorVertical(chunk, colorDir, bc);
case NORTH:
return generateLodColorHorizontal(chunk, colorDir, bc);
case SOUTH:
return generateLodColorHorizontal(chunk, colorDir, bc);
case EAST:
return generateLodColorHorizontal(chunk, colorDir, bc);
case WEST:
return generateLodColorHorizontal(chunk, colorDir, bc);
}
return INVISIBLE;
}
/**
* Is the layer between the given X, Z, and dataIndex
* values a valid LOD point?
*/
private boolean isLayerValidLodPoint(
ChunkSection[] chunkSections,
int sectionIndex, int y)
{
// search through this layer
int layerBlocks = 0;
for(int x = 0; x < LodChunk.WIDTH; x++)
{
for(int z = 0; z < LodChunk.WIDTH; z++)
{
if(chunkSections[sectionIndex] == null)
{
// this section doesn't have any blocks,
// it is not a valid section
return false;
}
else
{
if(chunkSections[sectionIndex].getBlockState(x, y, z) != null &&
chunkSections[sectionIndex].getBlockState(x, y, z).getBlock() != Blocks.AIR)
{
// we found a valid block in
// in this layer
layerBlocks++;
if(layerBlocks >= LOD_BLOCK_REQ)
{
return true;
}
}
}
} // z
} // x
return false;
}
/**
* Generates the color of the top or bottom of the given chunk.
*
* @throws IllegalArgumentException if given a ColorDirection other than TOP or BOTTOM
*/
private Color generateLodColorVertical(IChunk chunk, ColorDirection colorDir, BlockColors bc)
{
if(colorDir != ColorDirection.TOP && colorDir != ColorDirection.BOTTOM)
{
throw new IllegalArgumentException("generateLodColorVertical only accepts the ColorDirection TOP or BOTTOM");
}
ChunkSection[] chunkSections = chunk.getSections();
int numbOfBlocks = 0;
int red = 0;
int green = 0;
int blue = 0;
boolean goTopDown = (colorDir == ColorDirection.TOP);
// either go top down or bottom up
int dataStart = goTopDown? chunkSections.length - 1 : 0;
int dataMax = chunkSections.length;
int dataMin = 0;
int dataIncrement = goTopDown? -1 : 1;
int topStart = goTopDown? CHUNK_SECTION_HEIGHT - 1 : 0;
int topMax = CHUNK_SECTION_HEIGHT;
int topMin = 0;
int topIncrement = goTopDown? -1 : 1;
for(int x = 0; x < CHUNK_DATA_WIDTH; x++)
{
for(int z = 0; z < CHUNK_DATA_WIDTH; z++)
{
boolean foundBlock = false;
for(int i = dataStart; !foundBlock && i >= dataMin && i < dataMax; i += dataIncrement)
{
if(!foundBlock && chunkSections[i] != null)
{
for(int y = topStart; !foundBlock && y >= topMin && y < topMax; y += topIncrement)
{
int ci;
ci = chunkSections[i].getBlockState(x, y, z).materialColor.colorValue;
if(ci == 0)
{
// skip air or invisible blocks
continue;
}
Color c = intToColor(ci);
red += c.getRed();
green += c.getGreen();
blue += c.getBlue();
numbOfBlocks++;
// we found a valid block, skip to the
// next x and z
foundBlock = true;
}
}
}
}
}
if(numbOfBlocks == 0)
numbOfBlocks = 1;
red /= numbOfBlocks;
green /= numbOfBlocks;
blue /= numbOfBlocks;
return new Color(red, green, blue);
}
/*
* unused variation that can be used with only the heightmap,
* although it just returns the foliage color, so it shouldn't
* be used normally.
Heightmap heightmap = chunk.getHeightmap(Heightmap.Type.WORLD_SURFACE_WG);
int numbOfBlocks = CHUNK_DATA_WIDTH * CHUNK_DATA_WIDTH;
int red = 0;
int green = 0;
int blue = 0;
for(int x = 0; x < CHUNK_DATA_WIDTH; x++)
{
Biome biome = chunk.getBiomes().getNoiseBiome(x,z, heightmap.getHeight(x, z));
Color c = intToColor(biome.getFoliageColor());
red += c.getRed();
green += c.getGreen();
blue += c.getBlue();
}
}
red /= numbOfBlocks;
green /= numbOfBlocks;
blue /= numbOfBlocks;
return new Color(red, green, blue);
*/
/**
* Generates the color for the sides of the given chunk.
*/
private Color generateLodColorHorizontal(IChunk chunk, ColorDirection colorDir, BlockColors bc)
{
if(colorDir != ColorDirection.NORTH && colorDir != ColorDirection.SOUTH && colorDir != ColorDirection.EAST && colorDir != ColorDirection.WEST)
{
throw new IllegalArgumentException("generateLodColorHorizontal only accepts the ColorDirection N (North), S (South), E (East), or W (West)");
}
ChunkSection[] chunkSections = chunk.getSections();
int numbOfBlocks = 0;
int red = 0;
int green = 0;
int blue = 0;
// these don't change since the over direction doesn't matter
int overStart = 0;
int overIncrement = 1;
// determine which direction is "in"
int inStart = 0;
int inIncrement = 1;
switch (colorDir)
{
case NORTH:
inStart = 0;
inIncrement = 1;
break;
case SOUTH:
inStart = CHUNK_DATA_WIDTH - 1;
inIncrement = -1;
break;
case EAST:
inStart = 0;
inIncrement = 1;
break;
case WEST:
inStart = CHUNK_DATA_WIDTH - 1;
inIncrement = -1;
break;
default:
// we were given an invalid position, return invisible.
// this shouldn't happen and is mostly here to make the
// compiler happy
return INVISIBLE;
}
for (int section = 0; section < chunkSections.length; section++)
{
if (chunkSections[section] == null)
continue;
for (int y = 0; y < CHUNK_SECTION_HEIGHT; y++)
{
boolean foundBlock = false;
// over moves "over" the side of the chunk
// in moves "into" the chunk until it finds a block
for (int over = overStart; !foundBlock && over >= 0 && over < CHUNK_DATA_WIDTH; over += overIncrement)
{
for (int in = inStart; !foundBlock && in >= 0 && in < CHUNK_DATA_WIDTH; in += inIncrement)
{
int x = -1;
int z = -1;
// determine which should be X and Z
switch(colorDir)
{
case NORTH:
x = over;
z = in;
break;
case SOUTH:
x = over;
z = in;
break;
case EAST:
x = in;
z = over;
break;
case WEST:
x = in;
z = over;
break;
default:
// here to make the compiler happy
break;
}
// if this block is buried, under other blocks
// don't add it
if(!isBlockPosVisible(chunkSections[section], x,y,z))
{
// go to the next "over" block location,
// don't look at the next "in" location,
// since the next "in" location will more than
// likely still be covered
in = CHUNK_DATA_WIDTH + 2;
continue;
}
int ci;
ci = chunkSections[section].getBlockState(x, y, z).getMaterial().getColor().colorValue;
if (ci == 0) {
// skip air or invisible blocks
continue;
}
Color c = intToColor(ci);
red += c.getRed();
green += c.getGreen();
blue += c.getBlue();
numbOfBlocks++;
// we found a valid block, skip to the
// next x and z
foundBlock = true;
}
}
}
}
// if we didn't find any blocks return invisible
if(numbOfBlocks == 0)
return INVISIBLE;
red /= numbOfBlocks;
green /= numbOfBlocks;
blue /= numbOfBlocks;
return new Color(red, green, blue);
}
private static BlockState airState = Blocks.AIR.getDefaultState();
/**
* returns true if the block at the given coordinates is open to
* air on at least one side.
*/
private boolean isBlockPosVisible(ChunkSection chunkSection, int x, int y, int z)
{
/*
// above
if (y+1 < CHUNK_SECTION_HEIGHT) // don't go over the top
if (chunkSection.getBlockState(x, y+1, z).getBlock() == (Blocks.AIR))
return true;
// below
if (y-1 >= 0) // don't go below the bottom
if (chunkSection.getBlockState(x, y-1, z).getBlock() == (Blocks.AIR))
return true;
*/
// north
if (z-1 > 0)
if (chunkSection.getBlockState(x, y, z-1) == airState)
return true;
// south
if (z+1 < LodChunk.WIDTH)
if (chunkSection.getBlockState(x, y, z+1) == airState)
return true;
// east
if (x+1 <= LodChunk.WIDTH)
if (chunkSection.getBlockState(x+1, y, z) == airState)
return true;
// west
if (x-1 >= 0)
if (chunkSection.getBlockState(x-1, y, z) == airState)
return true;
return false;
}
/**
* Convert a BlockColors int into a Color object.
*/
private Color intToColor(int num)
{
int filter = 0b11111111;
int red = (num >> 16 ) & filter;
int green = (num >> 8 ) & filter;
int blue = num & filter;
return new Color(red, green, blue);
}
/**
* Convert a Color into a BlockColors object.
*/
@SuppressWarnings("unused")
private int colorToInt(Color color)
{
return color.getRGB();
}
}
@@ -0,0 +1,125 @@
package com.seibel.lod.builders;
import com.seibel.lod.objects.LodDimension;
import com.seibel.lod.objects.LodRegion;
import com.seibel.lod.proxy.ClientProxy;
import com.seibel.lod.render.LodRender;
import com.seibel.lod.util.LodUtils;
import net.minecraft.util.math.ChunkPos;
import net.minecraft.world.chunk.ChunkStatus;
import net.minecraft.world.chunk.IChunk;
import net.minecraft.world.server.ServerWorld;
import net.minecraftforge.common.WorldWorkerManager.IWorker;
/**
* This is used to generate a LodChunk at a given ChunkPos.
*
* @author James Seibel
* @version 03-31-2021
*/
public class LodChunkGenWorker implements IWorker
{
private ServerWorld serverWorld;
private ChunkPos pos;
private LodDimension lodDim;
private LodBuilder lodBuilder;
private LodBufferBuilder lodBufferBuilder;
private LodRender lodRender;
public LodChunkGenWorker(ChunkPos newPos, LodRender newLodRenderer,
LodBuilder newLodBuilder, LodBufferBuilder newLodBufferBuilder,
LodDimension newLodDimension)
{
serverWorld = LodUtils.getServerWorldFromDimension(newLodDimension.dimension);
if (serverWorld == null)
throw new IllegalArgumentException("LodChunkGenWorker must have a non-null ServerWorld");
pos = newPos;
lodDim = newLodDimension;
lodBuilder = newLodBuilder;
lodBufferBuilder = newLodBufferBuilder;
lodRender = newLodRenderer;
}
@Override
public boolean doWork()
{
if (pos != null)
{
int x = pos.x;
int z = pos.z;
// only generate LodChunks if they can
// be added to the current LodDimension
if (lodDim.regionIsInRange(pos.x / LodRegion.SIZE, pos.z / LodRegion.SIZE))
{
IChunk chunk;// = serverWorld.getChunk(x, z, ChunkStatus.EMPTY, true);
//long startTime = System.currentTimeMillis();
chunk = serverWorld.getChunk(x, z, ChunkStatus.FEATURES);
//long endTime = System.currentTimeMillis();
//System.out.println(endTime - startTime + "\t" + lodBuilder.hasBlockData(chunk));
lodBuilder.generateLodChunkAsync(chunk, ClientProxy.getLodWorld(), serverWorld);
// this is called so that the new LOD chunk is drawn
// after it is generated
lodRender.regenerateLODsNextFrame();
// useful for debugging
// ClientProxy.LOGGER.info(lodDim.getNumberOfLods());
// if (lodDim.getLodFromCoordinates(x, z) != null)
// ClientProxy.LOGGER.info(x + " " + z + " Success!");
// else
// ClientProxy.LOGGER.info(x + " " + z);
}
// can be used for debugging
//else
//{
// System.out.println("Out of range " + x + " " + z);
//}
lodBufferBuilder.numberOfChunksWaitingToGenerate--;
pos = null;
}
return false;
}
@Override
public boolean hasWork()
{
return pos != null;
}
/*
* performance/generation tests related to
* serverWorld.getChunk(x, z, ChunkStatus. *** )
true/false is whether they generated blocks or not
the time is how long it took to generate
ChunkStatus.EMPTY 0 - 1 ms false (empty, what did you expect? :P)
ChunkStatus.STRUCTURE_REFERENCES 1 - 2 ms false (no height, only generates some chunks)
ChunkStatus.BIOMES 1 - 10 ms false (no height)
ChunkStatus.NOISE 4 - 15 ms true (all blocks are stone)
ChunkStatus.LIQUID_CARVERS 6 - 12 ms true (no snow/trees, just grass)
ChunkStatus.SURFACE 5 - 15 ms true (no snow/trees, just grass)
ChunkStatus.CARVERS 5 - 30 ms true (no snow/trees, just grass)
ChunkStatus.FEATURES 7 - 25 ms true
ChunkStatus.HEIGHTMAPS 20 - 40 ms true
ChunkStatus.LIGHT 20 - 40 ms true
ChunkStatus.FULL 30 - 50 ms true
ChunkStatus.SPAWN 50 - 80 ms true
At this point I would suggest using FEATURES, as it generates snow and trees
(and any other object that is needed to make biomes distinct)
Otherwise if snow/trees aren't necessary SURFACE is the next fastest (although not by much)
*/
}
@@ -0,0 +1,42 @@
package com.seibel.lod.builders.lodTemplates;
import java.awt.Color;
import com.seibel.lod.objects.LodChunk;
import com.seibel.lod.objects.LodDimension;
import net.minecraft.client.renderer.BufferBuilder;
/**
* This is the abstract class used to create different
* BufferBuilders.
*
* @author James Seibel
* @version 05-07-2021
*/
public abstract class AbstractLodTemplate
{
/** alpha used when drawing chunks in debug mode */
protected int debugAlpha = 255; // 0 - 255
protected Color debugBlack = new Color(0, 0, 0, debugAlpha);
protected Color debugWhite = new Color(255, 255, 255, debugAlpha);
public abstract void addLodToBuffer(BufferBuilder buffer,
LodDimension lodDim, LodChunk lod,
double xOffset, double yOffset, double zOffset,
boolean debugging);
/** add the given position and color to the buffer */
protected void addPosAndColor(BufferBuilder buffer,
double x, double y, double z,
int red, int green, int blue, int alpha)
{
buffer.pos(x, y, z).color(red, green, blue, alpha).endVertex();
}
}
@@ -0,0 +1,277 @@
package com.seibel.lod.builders.lodTemplates;
import java.awt.Color;
import java.util.EnumSet;
import com.seibel.lod.enums.ColorDirection;
import com.seibel.lod.enums.RelativeChunkPos;
import com.seibel.lod.objects.LodChunk;
import com.seibel.lod.objects.LodDimension;
import com.seibel.lod.util.LodConfig;
import net.minecraft.client.renderer.BufferBuilder;
import net.minecraft.util.math.AxisAlignedBB;
/**
* Builds LODs as rectangular prisms.
*
* @author James Seibel
* @version 05-31-2021
*/
public class CubicLodTemplate extends AbstractLodTemplate
{
public CubicLodTemplate()
{
}
@Override
public void addLodToBuffer(BufferBuilder buffer,
LodDimension lodDim, LodChunk centerLod,
double xOffset, double yOffset, double zOffset,
boolean debugging)
{
AxisAlignedBB bbox;
// Add this LOD to the BufferBuilder
// using the quality setting set by the config
switch(LodConfig.CLIENT.lodDetail.get())
{
// add a single LOD object for this chunk
case SINGLE:
// returns null if the lod is empty at the given location
bbox = generateBoundingBox(centerLod.getHeight(), centerLod.getDepth(), LodChunk.WIDTH, xOffset, yOffset, zOffset);
if (bbox != null)
{
addBoundingBoxToBuffer(buffer, bbox, generateLodColors(centerLod, false));
}
break;
// add 4 LOD objects for this chunk
case DOUBLE:
/*
* This method generates LODs using the LodChunks that
* are adjacent to create an average quarter and thus
* smooth the transition between chunks.
*/
// get the adjacent LodChunks
LodChunk[] lods = new LodChunk[RelativeChunkPos.values().length];
for(RelativeChunkPos pos : RelativeChunkPos.values())
lods[pos.index] = lodDim.getLodFromCoordinates(centerLod.x + pos.x, centerLod.z + pos.z);
int halfWidth = LodChunk.WIDTH / 2;
// use the adjacent chunks to generate quarter sections
for(EnumSet<RelativeChunkPos> set : RelativeChunkPos.CORNERS)
{
int x = 0;
int z = 0;
// Weight the center LodChunk by this amount
// when taking the average.
// this should be between 3 and 6;
// if set to 1 (no extra weight)
// then the chunks don't appear to be averaged.
int centerWeight = 3;
// how many LodChunks adjacent to the center
// are valid?
int validPoints = centerWeight;
int avgHeight = centerLod.getHeight() * centerWeight;
int avgDepth = centerLod.getDepth() * centerWeight;
int[][] colorAverages = new int[ColorDirection.values().length][3];
Color[] colorToAdd = generateLodColors(centerLod, debugging);
for(int i = 0; i < centerWeight; i++)
colorAverages = addColorToColorAverages(colorAverages, colorToAdd);
for(RelativeChunkPos cornerPos : set)
{
// set the x and y location based on which
// corner we are working on
if (RelativeChunkPos.DIAGONAL.contains(cornerPos))
{
x = Math.min(cornerPos.x, 0) * halfWidth;
z = Math.min(cornerPos.z, 0) * halfWidth;
}
LodChunk cornerLod = lods[cornerPos.index];
if (cornerLod != null && !cornerLod.isLodEmpty())
{
validPoints++;
avgHeight += cornerLod.getHeight();
avgDepth += cornerLod.getDepth();
// only generate average colors if we aren't debugging
// (this is to prevent everything from becoming grey)
if (!debugging)
colorToAdd = generateLodColors(cornerLod, debugging);
else
colorToAdd = generateLodColors(centerLod, debugging);
// add to the running color average
colorAverages = addColorToColorAverages(colorAverages, colorToAdd);
}
}
// convert the heights into actual averages
avgHeight /= validPoints;
avgDepth /= validPoints;
// calculate the average colors
Color[] colors = new Color[ColorDirection.values().length];
for(ColorDirection dir : ColorDirection.values())
{
for(int rgbIndex = 0; rgbIndex < 3; rgbIndex++)
colorAverages[dir.value][rgbIndex] /= validPoints;
colors[dir.value] = new Color(colorAverages[dir.value][0], colorAverages[dir.value][1], colorAverages[dir.value][2]);
}
// returns null if the lod is empty at the given location
bbox = generateBoundingBox(avgHeight, avgDepth, halfWidth, xOffset - (halfWidth / 2) + x + 12, yOffset, zOffset - (halfWidth / 2) + z + 12);
if (bbox != null)
{
addBoundingBoxToBuffer(buffer, bbox, colors);
}
}
break;
} // case
}
private int[][] addColorToColorAverages(int[][] colorAverages, Color[] colorToAdd)
{
for(ColorDirection dir : ColorDirection.values())
{
// convert the colorToAdd to an int array
float[] colorCompoments = new float[4];
colorCompoments = colorToAdd[dir.value].getColorComponents(colorCompoments);
// add each color component to the array
for(int rgbIndex = 0; rgbIndex < 3; rgbIndex++)
{
// * 255 + 0.5 taken from the Color java class
colorAverages[dir.value][rgbIndex] += (int) (colorCompoments[rgbIndex] * 255 + 0.5);
}
}
return colorAverages;
}
private AxisAlignedBB generateBoundingBox(int height, int depth, int width, double xOffset, double yOffset, double zOffset)
{
// don't add an LOD if it is empty
if (height == -1 && depth == -1)
return null;
if (depth == height)
{
// if the top and bottom points are at the same height
// render this LOD as 1 block thick
height++;
}
return new AxisAlignedBB(0, depth, 0, width, height, width).offset(xOffset, yOffset, zOffset);
}
private void addBoundingBoxToBuffer(BufferBuilder buffer, AxisAlignedBB bb, Color[] c)
{
// top (facing up)
addPosAndColor(buffer, bb.minX, bb.maxY, bb.minZ, c[ColorDirection.TOP.value].getRed(), c[ColorDirection.TOP.value].getGreen(), c[ColorDirection.TOP.value].getBlue(), c[ColorDirection.TOP.value].getAlpha());
addPosAndColor(buffer, bb.minX, bb.maxY, bb.maxZ, c[ColorDirection.TOP.value].getRed(), c[ColorDirection.TOP.value].getGreen(), c[ColorDirection.TOP.value].getBlue(), c[ColorDirection.TOP.value].getAlpha());
addPosAndColor(buffer, bb.maxX, bb.maxY, bb.maxZ, c[ColorDirection.TOP.value].getRed(), c[ColorDirection.TOP.value].getGreen(), c[ColorDirection.TOP.value].getBlue(), c[ColorDirection.TOP.value].getAlpha());
addPosAndColor(buffer, bb.maxX, bb.maxY, bb.minZ, c[ColorDirection.TOP.value].getRed(), c[ColorDirection.TOP.value].getGreen(), c[ColorDirection.TOP.value].getBlue(), c[ColorDirection.TOP.value].getAlpha());
// bottom (facing down)
addPosAndColor(buffer, bb.maxX, bb.minY, bb.minZ, c[ColorDirection.BOTTOM.value].getRed(), c[ColorDirection.BOTTOM.value].getGreen(), c[ColorDirection.BOTTOM.value].getBlue(), c[ColorDirection.BOTTOM.value].getAlpha());
addPosAndColor(buffer, bb.maxX, bb.minY, bb.maxZ, c[ColorDirection.BOTTOM.value].getRed(), c[ColorDirection.BOTTOM.value].getGreen(), c[ColorDirection.BOTTOM.value].getBlue(), c[ColorDirection.BOTTOM.value].getAlpha());
addPosAndColor(buffer, bb.minX, bb.minY, bb.maxZ, c[ColorDirection.BOTTOM.value].getRed(), c[ColorDirection.BOTTOM.value].getGreen(), c[ColorDirection.BOTTOM.value].getBlue(), c[ColorDirection.BOTTOM.value].getAlpha());
addPosAndColor(buffer, bb.minX, bb.minY, bb.minZ, c[ColorDirection.BOTTOM.value].getRed(), c[ColorDirection.BOTTOM.value].getGreen(), c[ColorDirection.BOTTOM.value].getBlue(), c[ColorDirection.BOTTOM.value].getAlpha());
// south (facing -Z)
addPosAndColor(buffer, bb.maxX, bb.minY, bb.maxZ, c[ColorDirection.SOUTH.value].getRed(), c[ColorDirection.SOUTH.value].getGreen(), c[ColorDirection.SOUTH.value].getBlue(), c[ColorDirection.SOUTH.value].getAlpha());
addPosAndColor(buffer, bb.maxX, bb.maxY, bb.maxZ, c[ColorDirection.SOUTH.value].getRed(), c[ColorDirection.SOUTH.value].getGreen(), c[ColorDirection.SOUTH.value].getBlue(), c[ColorDirection.SOUTH.value].getAlpha());
addPosAndColor(buffer, bb.minX, bb.maxY, bb.maxZ, c[ColorDirection.SOUTH.value].getRed(), c[ColorDirection.SOUTH.value].getGreen(), c[ColorDirection.SOUTH.value].getBlue(), c[ColorDirection.SOUTH.value].getAlpha());
addPosAndColor(buffer, bb.minX, bb.minY, bb.maxZ, c[ColorDirection.SOUTH.value].getRed(), c[ColorDirection.SOUTH.value].getGreen(), c[ColorDirection.SOUTH.value].getBlue(), c[ColorDirection.SOUTH.value].getAlpha());
// north (facing +Z)
addPosAndColor(buffer, bb.minX, bb.minY, bb.minZ, c[ColorDirection.NORTH.value].getRed(), c[ColorDirection.NORTH.value].getGreen(), c[ColorDirection.NORTH.value].getBlue(), c[ColorDirection.NORTH.value].getAlpha());
addPosAndColor(buffer, bb.minX, bb.maxY, bb.minZ, c[ColorDirection.NORTH.value].getRed(), c[ColorDirection.NORTH.value].getGreen(), c[ColorDirection.NORTH.value].getBlue(), c[ColorDirection.NORTH.value].getAlpha());
addPosAndColor(buffer, bb.maxX, bb.maxY, bb.minZ, c[ColorDirection.NORTH.value].getRed(), c[ColorDirection.NORTH.value].getGreen(), c[ColorDirection.NORTH.value].getBlue(), c[ColorDirection.NORTH.value].getAlpha());
addPosAndColor(buffer, bb.maxX, bb.minY, bb.minZ, c[ColorDirection.NORTH.value].getRed(), c[ColorDirection.NORTH.value].getGreen(), c[ColorDirection.NORTH.value].getBlue(), c[ColorDirection.NORTH.value].getAlpha());
// west (facing -X)
addPosAndColor(buffer, bb.minX, bb.minY, bb.minZ, c[ColorDirection.WEST.value].getRed(), c[ColorDirection.WEST.value].getGreen(), c[ColorDirection.WEST.value].getBlue(), c[ColorDirection.WEST.value].getAlpha());
addPosAndColor(buffer, bb.minX, bb.minY, bb.maxZ, c[ColorDirection.WEST.value].getRed(), c[ColorDirection.WEST.value].getGreen(), c[ColorDirection.WEST.value].getBlue(), c[ColorDirection.WEST.value].getAlpha());
addPosAndColor(buffer, bb.minX, bb.maxY, bb.maxZ, c[ColorDirection.WEST.value].getRed(), c[ColorDirection.WEST.value].getGreen(), c[ColorDirection.WEST.value].getBlue(), c[ColorDirection.WEST.value].getAlpha());
addPosAndColor(buffer, bb.minX, bb.maxY, bb.minZ, c[ColorDirection.WEST.value].getRed(), c[ColorDirection.WEST.value].getGreen(), c[ColorDirection.WEST.value].getBlue(), c[ColorDirection.WEST.value].getAlpha());
// east (facing +X)
addPosAndColor(buffer, bb.maxX, bb.maxY, bb.minZ, c[ColorDirection.EAST.value].getRed(), c[ColorDirection.EAST.value].getGreen(), c[ColorDirection.EAST.value].getBlue(), c[ColorDirection.EAST.value].getAlpha());
addPosAndColor(buffer, bb.maxX, bb.maxY, bb.maxZ, c[ColorDirection.EAST.value].getRed(), c[ColorDirection.EAST.value].getGreen(), c[ColorDirection.EAST.value].getBlue(), c[ColorDirection.EAST.value].getAlpha());
addPosAndColor(buffer, bb.maxX, bb.minY, bb.maxZ, c[ColorDirection.EAST.value].getRed(), c[ColorDirection.EAST.value].getGreen(), c[ColorDirection.EAST.value].getBlue(), c[ColorDirection.EAST.value].getAlpha());
addPosAndColor(buffer, bb.maxX, bb.minY, bb.minZ, c[ColorDirection.EAST.value].getRed(), c[ColorDirection.EAST.value].getGreen(), c[ColorDirection.EAST.value].getBlue(), c[ColorDirection.EAST.value].getAlpha());
}
/**
* Determine the color for each side of this LOD.
*/
private Color[] generateLodColors(LodChunk lod, boolean debugging)
{
Color[] colors = new Color[ColorDirection.values().length];
if (!debugging)
{
// if NOT debugging, look to the config to determine
// how this LOD should be colored
switch (LodConfig.CLIENT.lodColorStyle.get())
{
case TOP:
// only add the top's color to the array
for(ColorDirection dir : ColorDirection.values())
colors[dir.value] = lod.getColor(ColorDirection.TOP);
break;
case INDIVIDUAL_SIDES:
// add each direction's color to the array
for(ColorDirection dir : ColorDirection.values())
colors[dir.value] = lod.getColor(dir);
break;
}
}
else
{
// if debugging draw the squares as a black and white checker board
if ((lod.x + lod.z) % 2 == 0)
for(ColorDirection dir : ColorDirection.values())
// have each direction be the same
// color if debugging
colors[dir.value] = debugWhite;
else
for(ColorDirection dir : ColorDirection.values())
colors[dir.value] = debugBlack;
}
return colors;
}
}
@@ -0,0 +1,27 @@
package com.seibel.lod.builders.lodTemplates;
import com.seibel.lod.objects.LodChunk;
import com.seibel.lod.objects.LodDimension;
import net.minecraft.client.renderer.BufferBuilder;
/**
* TODO DynamicLodTemplate
* Chunks smoothly transition between
* each other, unless a neighboring chunk
* is at a significantly different height.
*
* @author James Seibel
* @version 05-07-2021
*/
public class DynamicLodTemplate extends AbstractLodTemplate
{
@Override
public void addLodToBuffer(BufferBuilder buffer,
LodDimension lodDim, LodChunk lod,
double xOffset, double yOffset, double zOffset,
boolean debugging)
{
System.err.println("DynamicLodTemplate not implemented!");
}
}
@@ -0,0 +1,25 @@
package com.seibel.lod.builders.lodTemplates;
import com.seibel.lod.objects.LodChunk;
import com.seibel.lod.objects.LodDimension;
import net.minecraft.client.renderer.BufferBuilder;
/**
* TODO TriangularLodTemplate
* Builds each LOD chunk as a singular rectangular prism.
*
* @author James Seibel
* @version 05-07-2021
*/
public class TriangularLodTemplate extends AbstractLodTemplate
{
@Override
public void addLodToBuffer(BufferBuilder buffer,
LodDimension lodDim, LodChunk lod,
double xOffset, double yOffset, double zOffset,
boolean debugging)
{
System.err.println("DynamicLodTemplate not implemented!");
}
}
@@ -1,7 +1,7 @@
package com.backsun.lod.util.enums;
package com.seibel.lod.enums;
/**
* TOP, N, S, E, W, BOTTOM
* TOP, NORTH, SOUTH, EAST, WEST, BOTTOM
*
* @author James Seibel
* @version 10-17-2020
@@ -13,14 +13,14 @@ public enum ColorDirection
TOP(0),
/** -Z */
N(1),
NORTH(1),
/** +Z */
S(2),
SOUTH(2),
/** +X */
E(3),
EAST(3),
/** -X */
W(4),
WEST(4),
/** -Y */
BOTTOM(5);
@@ -1,4 +1,4 @@
package com.backsun.lod.util.enums;
package com.seibel.lod.enums;
/**
* Near, far, or NEAR_AND_FAR.
@@ -1,4 +1,4 @@
package com.backsun.lod.util.enums;
package com.seibel.lod.enums;
/**
* fast, fancy, or off
@@ -0,0 +1,18 @@
package com.seibel.lod.enums;
/**
* top, individual_sides
*
* @author James Seibel
* @version 05-08-2021
*/
public enum LodColorStyle
{
/** Use the color from the top of the LOD chunk
* for all sides */
TOP,
/** For each side of the LOD use the color corresponding
* to that side */
INDIVIDUAL_SIDES;
}
@@ -1,4 +1,4 @@
package com.backsun.lod.util.enums;
package com.seibel.lod.enums;
/**
* NE, SE, SW, NW
@@ -8,8 +8,6 @@ package com.backsun.lod.util.enums;
*/
public enum LodCorner
{
// used for position
/** -Z, +X */
NE(0),
/** +Z, +X */
@@ -0,0 +1,25 @@
package com.seibel.lod.enums;
/**
* single, quad
*
* @author James Seibel
* @version 05-29-2021
*/
public enum LodDetail
{
/** render 1 LOD for each chunk */
SINGLE(1),
/** render 4 LODs for each chunk */
DOUBLE(2);
/** How many data points wide the related
* LodChunk object should contain */
public final int value;
private LodDetail(int newValue)
{
value = newValue;
}
}
@@ -0,0 +1,38 @@
package com.seibel.lod.enums;
import com.seibel.lod.builders.lodTemplates.AbstractLodTemplate;
import com.seibel.lod.builders.lodTemplates.CubicLodTemplate;
import com.seibel.lod.builders.lodTemplates.DynamicLodTemplate;
import com.seibel.lod.builders.lodTemplates.TriangularLodTemplate;
/**
* Cubic, Triangular, Dynamic
*
* @author James Seibel
* @version 05-07-2021
*/
public enum LodTemplate
{
// used for position
/** Chunks are rendered as
* rectangular prisms. */
CUBIC(new CubicLodTemplate()),
/** Chunks smoothly transition between
* each other. */
TRIANGULAR(new TriangularLodTemplate()),
/** Chunks smoothly transition between
* each other, unless a neighboring chunk
* is at a significantly different height. */
DYNAMIC(new DynamicLodTemplate());
public final AbstractLodTemplate template;
private LodTemplate(AbstractLodTemplate newTemplate)
{
template = newTemplate;
}
}
@@ -0,0 +1,69 @@
package com.seibel.lod.enums;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
/**
* NE, SE, SW, NW <br>
* NORTH, SOUTH, EAST, WEST, <br>
* CENTER
*
* @author James Seibel
* @version 05-30-2021
*/
public enum RelativeChunkPos
{
/** +X, -Z */
NE(0, 1,-1),
/** +X, +Z */
SE(1, 1,1),
/** -X, +Z */
SW(2, -1,1),
/** -X, -Z */
NW(3, -1,-1),
/** -Z */
NORTH(4, 0,-1),
/** +Z */
SOUTH(5, 0,1),
/** +X */
EAST(6, 1,0),
/** -X */
WEST(7, -1,0),
CENTER(8, 0,0);
/** index used when referencing objects in an array */
public final int index;
/** position relative to X CENTER */
public final int x;
/** position relative to Z CENTER */
public final int z;
/** NORTH, SOUTH, EAST, WEST */
public static EnumSet<RelativeChunkPos> ADJACENT = EnumSet.of(NORTH, SOUTH, EAST, WEST);
/** NE, NW, SE, SW */
public static EnumSet<RelativeChunkPos> DIAGONAL = EnumSet.of(NE, NW, SE, SW);
public static EnumSet<RelativeChunkPos> NW_CORNER = EnumSet.of(WEST, NW, NORTH);
public static EnumSet<RelativeChunkPos> NE_CORNER = EnumSet.of(EAST, NE, NORTH);
public static EnumSet<RelativeChunkPos> SW_CORNER = EnumSet.of(WEST, SW, SOUTH);
public static EnumSet<RelativeChunkPos> SE_CORNER = EnumSet.of(EAST, SE, SOUTH);
/** NW_CORNER, NE_CORNER, SW_CORNER, SE_CORNER <br>
* Contains the 3 points surrounding a corner. */
public static ArrayList<EnumSet<RelativeChunkPos>> CORNERS = new ArrayList<EnumSet<RelativeChunkPos>>( Arrays.asList(NW_CORNER, NE_CORNER, SW_CORNER, SE_CORNER) );
private RelativeChunkPos(int newIndex, int newX, int newZ)
{
index = newIndex;
x = newX;
z = newZ;
}
}
@@ -0,0 +1,333 @@
package com.seibel.lod.handlers;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import com.seibel.lod.objects.LodChunk;
import com.seibel.lod.objects.LodDimension;
import com.seibel.lod.objects.LodRegion;
import com.seibel.lod.proxy.ClientProxy;
/**
* This object handles creating LodRegions
* from files and saving LodRegion objects
* to file.
*
* @author James Seibel
* @version 05-31-2021
*/
public class LodDimensionFileHandler
{
private LodDimension loadedDimension = null;
public long regionLastWriteTime[][];
private File dimensionDataSaveFolder;
private final String FILE_NAME_PREFIX = "lod";
private final String FILE_EXTENSION = ".txt";
/** This is the file version currently accepted by this
* file handler, older versions (smaller numbers) will be deleted and overwritten,
* newer versions (larger numbers) will be ignored and won't be read. */
public static final int LOD_SAVE_FILE_VERSION = 1;
/** This is the string written before the file version */
private static final String LOD_FILE_VERSION_PREFIX = "lod_save_file_version";
/** Allow saving asynchronously, but never try to save multiple regions
* at a time */
private ExecutorService fileWritingThreadPool = Executors.newSingleThreadExecutor();
public LodDimensionFileHandler(File newSaveFolder, LodDimension newLoadedDimension)
{
if (newSaveFolder == null)
throw new IllegalArgumentException("LodDimensionFileHandler requires a valid File location to read and write to.");
dimensionDataSaveFolder = newSaveFolder;
loadedDimension = newLoadedDimension;
// these two variable are used in sync with the LodDimension
regionLastWriteTime = new long[loadedDimension.getWidth()][loadedDimension.getWidth()];
for(int i = 0; i < loadedDimension.getWidth(); i++)
for(int j = 0; j < loadedDimension.getWidth(); j++)
regionLastWriteTime[i][j] = -1;
}
//================//
// read from file //
//================//
/**
* Return the LodRegion at the given coordinates.
* (null if the file doesn't exist)
*/
public LodRegion loadRegionFromFile(int regionX, int regionZ)
{
String fileName = getFileNameAndPathForRegion(regionX, regionZ);
File f = new File(fileName);
if (!f.exists())
{
// there wasn't a file, don't
// return anything
return null;
}
LodRegion region = new LodRegion(regionX, regionZ);
try
{
BufferedReader br = new BufferedReader(new FileReader(f));
String s = br.readLine();
int fileVersion = -1;
if(s != null && !s.isEmpty())
{
// try to get the file version
try
{
fileVersion = Integer.parseInt(s.substring(s.indexOf(' ')).trim());
}
catch(NumberFormatException | StringIndexOutOfBoundsException e)
{
// this file doesn't have a version
// keep the version as -1
fileVersion = -1;
}
// check if this file can be read by this file handler
if(fileVersion < LOD_SAVE_FILE_VERSION)
{
// the file we are reading is an older version,
// close the reader and delete the file.
br.close();
f.delete();
ClientProxy.LOGGER.info("Outdated LOD region file for region: (" + regionX + "," + regionZ + ") version: " + fileVersion +
", version requested: " + LOD_SAVE_FILE_VERSION +
" File was been deleted.");
return null;
}
else if(fileVersion > LOD_SAVE_FILE_VERSION)
{
// the file we are reading is a newer version,
// close the reader and ignore the file, we don't
// want to accidently delete anything the user may want.
br.close();
ClientProxy.LOGGER.info("Newer LOD region file for region: (" + regionX + "," + regionZ + ") version: " + fileVersion +
", version requested: " + LOD_SAVE_FILE_VERSION +
" this region will not be written to in order to protect the newer file.");
return null;
}
}
else
{
// there is no data in this file
br.close();
return null;
}
// this file is a readable version, begin reading the file
s = br.readLine();
while(s != null && !s.isEmpty())
{
try
{
// convert each line into an LOD object and add it to the region
LodChunk lod = new LodChunk(s);
region.addLod(lod);
}
catch(IllegalArgumentException e)
{
// we were unable to create this chunk
// for whatever reason.
// skip to the next chunk
ClientProxy.LOGGER.warn(e.getMessage());
}
s = br.readLine();
}
br.close();
}
catch (IOException e)
{
// the buffered reader encountered a
// problem reading the file
return null;
}
return region;
}
//==============//
// Save to File //
//==============//
/**
* Save all dirty regions in this LodDimension to file.
*/
public void saveDirtyRegionsToFileAsync()
{
fileWritingThreadPool.execute(saveDirtyRegionsThread);
}
private Thread saveDirtyRegionsThread = new Thread(() ->
{
for(int i = 0; i < loadedDimension.getWidth(); i++)
{
for(int j = 0; j < loadedDimension.getWidth(); j++)
{
if(loadedDimension.isRegionDirty[i][j] && loadedDimension.regions[i][j] != null)
{
saveRegionToDisk(loadedDimension.regions[i][j]);
loadedDimension.isRegionDirty[i][j] = false;
}
}
}
});
/**
* Save a specific region to disk.<br>
* Note: <br>
* 1. If a file already exists for a newer version
* the file won't be written.<br>
* 2. This will save to the LodDimension that this
* handler is associated with.
*/
private void saveRegionToDisk(LodRegion region)
{
// convert chunk coordinates to region
// coordinates
int x = region.x;
int z = region.z;
File f = new File(getFileNameAndPathForRegion(x, z));
try
{
// make sure the file and folder exists
if (!f.exists())
{
// the file doesn't exist,
// create it and the folder if need be
if(!f.getParentFile().exists())
f.getParentFile().mkdirs();
f.createNewFile();
}
else
{
// the file exists, make sure it
// is the correct version.
// (to make sure we don't overwrite a newer
// version file if it exists)
BufferedReader br = new BufferedReader(new FileReader(f));
String s = br.readLine();
int fileVersion = LOD_SAVE_FILE_VERSION;
if(s != null && !s.isEmpty())
{
// try to get the file version
try
{
fileVersion = Integer.parseInt(s.substring(s.indexOf(' ')).trim());
}
catch(NumberFormatException | StringIndexOutOfBoundsException e)
{
// this file doesn't have a correctly formated version
// just overwrite the file
}
}
br.close();
// check if this file can be written to by the file handler
if(fileVersion <= LOD_SAVE_FILE_VERSION)
{
// we are good to continue and overwrite the old file
}
else //if(fileVersion > LOD_SAVE_FILE_VERSION)
{
// the file we are reading is a newer version,
// don't write anything, we don't want to accidently
// delete anything the user may want.
return;
}
}
FileWriter fw = new FileWriter(f);
// add the version of this file
fw.write(LOD_FILE_VERSION_PREFIX + " " + LOD_SAVE_FILE_VERSION + "\n");
// add each LodChunk to the file
for(LodChunk[] chunkArray : region.getAllLods())
for(LodChunk chunk : chunkArray)
if(chunk != null && !chunk.isPlaceholder())
fw.write(chunk.toData() + "\n");
fw.close();
}
catch(Exception e)
{
ClientProxy.LOGGER.error("LOD file write error: " + e.getMessage());
}
}
//================//
// helper methods //
//================//
/**
* Return the name of the file that should contain the
* region at the given x and z. <br>
* Returns null if this object isn't ready to read and write.
*/
private String getFileNameAndPathForRegion(int regionX, int regionZ)
{
try
{
// saveFolder is something like
// ".\Super Flat\DIM-1\data"
// or
// ".\Super Flat\data"
return dimensionDataSaveFolder.getCanonicalPath() + "\\" +
FILE_NAME_PREFIX + "." + regionX + "." + regionZ + FILE_EXTENSION;
}
catch(IOException e)
{
return null;
}
}
}
@@ -1,8 +1,8 @@
package com.backsun.lod.handlers;
package com.seibel.lod.handlers;
import java.lang.reflect.Field;
import com.backsun.lod.util.enums.FogQuality;
import com.seibel.lod.enums.FogQuality;
import net.minecraft.client.Minecraft;
@@ -0,0 +1,45 @@
package com.seibel.lod.mixin;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import com.mojang.blaze3d.matrix.MatrixStack;
import com.seibel.lod.LodMain;
import com.seibel.lod.util.LodConfig;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.WorldRenderer;
/**
* This class is used to mix in my rendering code
* before Minecraft starts rendering blocks.
* If this wasn't done the LODs would render on top
* of the normal terrain.
*
* @author James Seibel
* @version 05-29-2021
*/
@Mixin(WorldRenderer.class)
public class MixinWorldRenderer
{
private static float previousPartialTicks = 0;
@Inject(at = @At("RETURN"), method = "renderSky(Lcom/mojang/blaze3d/matrix/MatrixStack;F)V", cancellable = false)
private void renderSky(MatrixStack matrixStackIn, float partialTicks, CallbackInfo callback)
{
// get the partial ticks since renderBlockLayer doesn't
// have access to them
previousPartialTicks = partialTicks;
}
@Inject(at = @At("HEAD"), method = "renderBlockLayer(Lnet/minecraft/client/renderer/RenderType;Lcom/mojang/blaze3d/matrix/MatrixStack;DDD)V", cancellable = false)
private void renderBlockLayer(RenderType renderType, MatrixStack matrixStackIn, double xIn, double yIn, double zIn, CallbackInfo callback)
{
// only render if LODs are enabled and
// only render before solid blocks
if (LodConfig.CLIENT.drawLODs.get() && renderType.equals(RenderType.getSolid()))
LodMain.client_proxy.renderLods(previousPartialTicks);
}
}
@@ -0,0 +1,313 @@
package com.seibel.lod.objects;
import java.awt.Color;
import com.seibel.lod.enums.ColorDirection;
import net.minecraft.util.math.ChunkPos;
/**
* This object contains position
* and color data for an LOD object.
*
* @author James Seibel
* @version 05-29-2021
*/
public class LodChunk
{
/** how many different pieces of data are in one line */
private static final int DATA_DELIMITER_COUNT = 22;
/** This is what separates each piece of data in the toData method */
public static final char DATA_DELIMITER = ',';
/** Width of a Minecraft Chunk */
public static final int WIDTH = 16;
private static final Color INVISIBLE = new Color(0,0,0,0);
/** The x coordinate of the chunk. */
public int x = 0;
/** The z coordinate of the chunk. */
public int z = 0;
/*
* The reason we are only using 1 height and depth point
* instead of multiple is because:
* 1. more points would drastically increase the amount of
* memory and disk space needed
* 2. more height/depth points require more color points,
* which can get out of hand quickly
* 3. with the increased disk space reading/writing would
* take far too long
* 4. the increased resolution is generally not worth it,
* 4 LODs per chunk is the limit of diminishing returns.
* And some of that could potentially be faked through
* smart LodTemplates
*/
private short height;
private short depth;
/** The average color for the 6 cardinal directions */
public Color colors[];
/** If true then this LodChunk contains no data */
private boolean empty = false;
/**
* Create an empty invisible LodChunk at (0,0)
*/
public LodChunk()
{
empty = true;
x = 0;
z = 0;
height = 0;
depth = 0;
colors = new Color[ColorDirection.values().length];
// by default have the colors invisible
for(ColorDirection dir : ColorDirection.values())
colors[dir.value] = INVISIBLE;
}
/**
* Creates an LodChunk from the string
* generated by the toData method.
*
* @throws IllegalArgumentException if the data isn't valid to create a LodChunk
* @throws NumberFormatException if any piece of data can't be converted at any point
*/
public LodChunk(String data) throws IllegalArgumentException, NumberFormatException
{
/*
* data format:
* x, z, height, depth, rgb color data
*
* example:
* 5,8, 4, 0, 255,255,255, 255,255,255, 255,255,255, 255,255,255, 255,255,255, 255,255,255,
*/
// make sure there are the correct number of entries
// in the data string (28)
int count = 0;
for(int i = 0; i < data.length(); i++)
if(data.charAt(i) == DATA_DELIMITER)
count++;
if(count != DATA_DELIMITER_COUNT)
throw new IllegalArgumentException("LodChunk constructor givin an invalid string. The data given had " + count + " delimiters when it should have had " + DATA_DELIMITER_COUNT + ".");
// index we will use when going through the String
int index = 0;
int lastIndex = 0;
// x and z position
index = data.indexOf(DATA_DELIMITER, 0);
x = Integer.parseInt(data.substring(0,index));
lastIndex = index;
index = data.indexOf(DATA_DELIMITER, lastIndex+1);
z = Integer.parseInt(data.substring(lastIndex+1,index));
// height
lastIndex = index;
index = data.indexOf(DATA_DELIMITER, lastIndex+1);
height = Short.parseShort(data.substring(lastIndex+1,index));
// depth
lastIndex = index;
index = data.indexOf(DATA_DELIMITER, lastIndex+1);
depth = Short.parseShort(data.substring(lastIndex+1,index));
// color
colors = new Color[6];
for(ColorDirection dir : ColorDirection.values())
{
int red = 0;
int green = 0;
int blue = 0;
// get r,g,b
for(int i = 0; i < 3; i++)
{
lastIndex = index;
index = data.indexOf(DATA_DELIMITER, lastIndex + 1);
String raw = "";
switch(i)
{
case 0:
raw = data.substring(lastIndex+1,index);
red = Short.parseShort(raw);
break;
case 1:
raw = data.substring(lastIndex+1,index);
green = Short.parseShort(raw);
break;
case 2:
raw = data.substring(lastIndex+1,index);
blue = Short.parseShort(raw);
break;
}
}
colors[dir.value] = new Color(red, green, blue);
}
// after going through all this
// is this LOD empty?
empty = determineIfEmtpy();
}
/**
* Create a LodChunk from the given values.
*/
public LodChunk(ChunkPos pos, short newHeight, short newDepth, Color[] newColors)
{
x = pos.x;
z = pos.z;
height = newHeight;
depth = newDepth;
colors = newColors;
empty = determineIfEmtpy();
}
//================//
// misc functions //
//================//
/**
* Returns true if this LodChunk is an emptyPlaceholder
*/
public boolean isPlaceholder()
{
return empty;
}
public boolean isLodEmpty()
{
return empty;
}
/**
* Returns true if this LOD is either invisible
* from every direction or doesn't have a valid height.
*/
private boolean determineIfEmtpy()
{
// we don't check the depth since the
// height should always be greater than or equal
// to the depth
if(height >= 0)
{
// the height is valid,
// do we have at least 1 non-invisible color?
for(ColorDirection dir : ColorDirection.values())
if(!colors[dir.value].equals(INVISIBLE))
// at least one direction has a non-invisible color
return false;
return true;
}
else
{
// the height is negative,
// this LodChunk is empty
return true;
}
}
//========//
// output //
//========//
/** Returns the color for the given direction */
public Color getColor(ColorDirection dir)
{
return colors[dir.value];
}
public short getHeight()
{
return height;
}
public short getDepth()
{
return depth;
}
/**
* Outputs all data in csv format
* with the given delimiter.
* <br>
* Exports data in the form:
* <br>
* x, z, height, depth, rgb color data
*
* <br>
* example output:
* <br>
* 5,8, 4, 0, 255,255,255, 255,255,255, 255,255,255, 255,255,255, 255,255,255, 255,255,255,
*/
public String toData()
{
String s = "";
s += Integer.toString(x) + DATA_DELIMITER + Integer.toString(z) + DATA_DELIMITER;
s += Short.toString(height) + DATA_DELIMITER;
s += Short.toString(depth) + DATA_DELIMITER;
for(int i = 0; i < colors.length; i++)
{
s += Integer.toString(colors[i].getRed()) + DATA_DELIMITER + Integer.toString(colors[i].getGreen()) + DATA_DELIMITER + Integer.toString(colors[i].getBlue()) + DATA_DELIMITER;
}
return s;
}
@Override
public String toString()
{
String s = "";
s += "x: " + x + " z: " + z + "\t";
s += "(" + colors[ColorDirection.TOP.value].getRed() + ", " + colors[ColorDirection.TOP.value].getGreen() + ", " + colors[ColorDirection.TOP.value].getBlue() + ")";
return s;
}
}
@@ -1,17 +1,23 @@
package com.backsun.lod.objects;
package com.seibel.lod.objects;
import com.backsun.lod.handlers.LodDimensionFileHandler;
import com.backsun.lod.util.LodUtils;
import java.io.File;
import java.io.IOException;
import com.seibel.lod.handlers.LodDimensionFileHandler;
import com.seibel.lod.util.LodUtils;
import net.minecraft.client.Minecraft;
import net.minecraft.util.math.ChunkPos;
import net.minecraft.world.DimensionType;
import net.minecraft.world.server.ServerChunkProvider;
import net.minecraft.world.server.ServerWorld;
/**
* This object holds all loaded LOD regions
* for a given dimension.
*
* @author James Seibel
* @version 02-23-2021
* @version 03-19-2021
*/
public class LodDimension
{
@@ -29,13 +35,42 @@ public class LodDimension
private LodDimensionFileHandler fileHandler;
public LodDimension(DimensionType newDimension, int newMaxWidth)
public LodDimension(DimensionType newDimension, LodWorld lodWorld, int newMaxWidth)
{
dimension = newDimension;
width = newMaxWidth;
ServerChunkProvider provider = LodUtils.getServerWorldFromDimension(newDimension).getChunkProvider();
fileHandler = new LodDimensionFileHandler(provider.getSavedData().folder, this);
try
{
Minecraft mc = Minecraft.getInstance();
File saveDir;
if(mc.isIntegratedServerRunning())
{
// local world
ServerWorld serverWorld = LodUtils.getServerWorldFromDimension(newDimension);
// provider needs a separate variable to prevent
// the compiler from complaining
ServerChunkProvider provider = serverWorld.getChunkProvider();
saveDir = new File(provider.getSavedData().folder.getCanonicalFile() + "\\lod");
}
else
{
// connected to server
saveDir = new File(mc.gameDir.getCanonicalFile() +
"\\lod server data\\" + LodUtils.getDimensionIDFromWorld(mc.world));
}
fileHandler = new LodDimensionFileHandler(saveDir, this);
}
catch(IOException e)
{
// the file handler wasn't able to be created
// we won't be able to read or write any files
}
regions = new LodRegion[width][width];
isRegionDirty = new boolean[width][width];
@@ -207,39 +242,34 @@ public class LodDimension
*/
public void addLod(LodChunk lod)
{
int regionX = lod.x / LodRegion.SIZE;
int regionZ = lod.z / LodRegion.SIZE;
// prevent issues if X/Z is negative and less than 16
if (lod.x < 0)
{
regionX = (Math.abs(regionX) * -1) - 1;
}
if (lod.z < 0)
{
regionZ = (Math.abs(regionZ) * -1) - 1;
}
RegionPos pos = LodUtils.convertChunkPosToRegionPos(new ChunkPos(lod.x, lod.z));
// don't continue if the region can't be saved
if (!regionIsInRange(regionX, regionZ))
if (!regionIsInRange(pos.x, pos.z))
{
return;
LodRegion region = getRegion(regionX, regionZ);
}
LodRegion region = getRegion(pos.x, pos.z);
if (region == null)
{
// if no region exists, create it
region = new LodRegion(regionX, regionZ);
region = new LodRegion(pos.x, pos.z);
setRegion(region);
}
region.addLod(lod);
// mark the region as dirty so it will be saved to disk
int xIndex = (regionX - centerX) + halfWidth;
int zIndex = (regionZ - centerZ) + halfWidth;
isRegionDirty[xIndex][zIndex] = true;
fileHandler.saveDirtyRegionsToFileAsync();
// don't save empty place holders to disk
if (!lod.isPlaceholder() && fileHandler != null)
{
// mark the region as dirty so it will be saved to disk
int xIndex = (pos.x - centerX) + halfWidth;
int zIndex = (pos.z - centerZ) + halfWidth;
isRegionDirty[xIndex][zIndex] = true;
fileHandler.saveDirtyRegionsToFileAsync();
}
}
/**
@@ -251,20 +281,9 @@ public class LodDimension
*/
public LodChunk getLodFromCoordinates(int chunkX, int chunkZ)
{
int regionX = chunkX / LodRegion.SIZE;
int regionZ = chunkZ / LodRegion.SIZE;
RegionPos pos = LodUtils.convertChunkPosToRegionPos(new ChunkPos(chunkX, chunkZ));
// prevent issues if chunkX/Z is negative and less than width
if (chunkX < 0)
{
regionX = (Math.abs(regionX) * -1) - 1;
}
if (chunkZ < 0)
{
regionZ = (Math.abs(regionZ) * -1) - 1;
}
LodRegion region = getRegion(regionX, regionZ);
LodRegion region = getRegion(pos.x, pos.z);
if(region == null)
return null;
@@ -279,7 +298,10 @@ public class LodDimension
*/
public LodRegion getRegionFromFile(int regionX, int regionZ)
{
return fileHandler.loadRegionFromFile(regionX, regionZ);
if (fileHandler != null)
return fileHandler.loadRegionFromFile(regionX, regionZ);
else
return null;
}
@@ -287,7 +309,7 @@ public class LodDimension
* Returns whether the region at the given X and Z coordinates
* is within the loaded range.
*/
private boolean regionIsInRange(int regionX, int regionZ)
public boolean regionIsInRange(int regionX, int regionZ)
{
int xIndex = (regionX - centerX) + halfWidth;
int zIndex = (regionZ - centerZ) + halfWidth;
@@ -312,6 +334,40 @@ public class LodDimension
}
/**
* Returns how many non-null LodChunks
* are stored in this LodDimension.
*/
public int getNumberOfLods()
{
int numbLods = 0;
for (LodRegion[] regions : regions)
{
if(regions == null)
continue;
for (LodRegion region : regions)
{
if(region == null)
continue;
for(LodChunk[] lods : region.getAllLods())
{
if(lods == null)
continue;
for(LodChunk lod : lods)
{
if (lod != null)
numbLods++;
}
}
}
}
return numbLods;
}
public int getWidth()
{
@@ -1,4 +1,4 @@
package com.backsun.lod.objects;
package com.seibel.lod.objects;
/**
* A LodRegion is the a 32x32
@@ -0,0 +1,110 @@
package com.seibel.lod.objects;
import java.util.Hashtable;
import java.util.Map;
import net.minecraft.world.DimensionType;
/**
* This stores all LODs for a given world.
*
* @author James Seibel
* @version 04-01-2021
*/
public class LodWorld
{
private String worldName;
private Map<DimensionType, LodDimension> lodDimensions;
/** If true then the LOD world is setup and ready to use */
private boolean isWorldLoaded = false;
public static final String NO_WORLD_LOADED = "No world loaded";
public LodWorld()
{
worldName = NO_WORLD_LOADED;
}
/**
* Set up the LodWorld with the given newWorldName. <br>
* This should be done whenever loading a new world.
* @param newWorldName
*/
public void selectWorld(String newWorldName)
{
if(newWorldName.isEmpty())
{
deselectWorld();
return;
}
if (worldName.equals(newWorldName))
// don't recreate everything if we
// didn't actually change worlds
return;
worldName = newWorldName;
lodDimensions = new Hashtable<DimensionType, LodDimension>();
isWorldLoaded = true;
}
/**
* Set the worldName to "No world loaded"
* and clear the lodDimensions Map. <br>
* This should be done whenever unloaded a world.
*/
public void deselectWorld()
{
worldName = NO_WORLD_LOADED;
lodDimensions = null;
isWorldLoaded = false;
}
public void addLodDimension(LodDimension newStorage)
{
if (lodDimensions == null)
throw new IllegalStateException("LodWorld hasn't been given a world yet.");
lodDimensions.put(newStorage.dimension, newStorage);
}
public LodDimension getLodDimension(DimensionType dimension)
{
if (lodDimensions == null)
throw new IllegalStateException("LodWorld hasn't been given a world yet.");
return lodDimensions.get(dimension);
}
/**
* Resizes the max width in regions that each LodDimension
* should use.
*/
public void resizeDimensionRegionWidth(int newWidth)
{
if (lodDimensions == null)
throw new IllegalStateException("LodWorld hasn't been given a world yet.");
for(DimensionType key : lodDimensions.keySet())
lodDimensions.get(key).setRegionWidth(newWidth);
}
public boolean getIsWorldLoaded()
{
return isWorldLoaded;
}
public String getWorldName()
{
return worldName;
}
@Override
public String toString()
{
return "World name: " + worldName;
}
}
@@ -1,4 +1,4 @@
package com.backsun.lod.objects;
package com.seibel.lod.objects;
import net.minecraft.client.renderer.BufferBuilder;
@@ -8,7 +8,7 @@ import net.minecraft.client.renderer.BufferBuilder;
* and BuildBufferThread.
*
* @author James Seibel
* @version 02-21-2021
* @version 03-25-2021
*/
public class NearFarBuffer
{
@@ -16,7 +16,10 @@ public class NearFarBuffer
public BufferBuilder farBuffer;
/**
* @param newNearBuffer
* @param newFarBuffer
*/
public NearFarBuffer(BufferBuilder newNearBuffer, BufferBuilder newFarBuffer)
{
nearBuffer = newNearBuffer;
@@ -1,6 +1,6 @@
package com.backsun.lod.objects;
package com.seibel.lod.objects;
import com.backsun.lod.util.enums.FogDistance;
import com.seibel.lod.enums.FogDistance;
/**
* This object is just a replacement for an array
@@ -0,0 +1,31 @@
package com.seibel.lod.objects;
/**
* This object is similar to ChunkPos or BlockPos.
*
* @author James Seibel
* @version 03-19-2021
*/
public class RegionPos
{
public int x;
public int z;
/**
* Default Constructor <br>
*
* Sets x and z to 0
*/
public RegionPos()
{
x = 0;
z = 0;
}
public RegionPos(int newX, int newZ)
{
x = newX;
z = newZ;
}
}
@@ -0,0 +1,189 @@
package com.seibel.lod.proxy;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.seibel.lod.builders.LodBufferBuilder;
import com.seibel.lod.builders.LodBuilder;
import com.seibel.lod.objects.LodChunk;
import com.seibel.lod.objects.LodDimension;
import com.seibel.lod.objects.LodRegion;
import com.seibel.lod.objects.LodWorld;
import com.seibel.lod.render.LodRender;
import com.seibel.lod.util.LodConfig;
import com.seibel.lod.util.LodUtils;
import net.minecraft.client.Minecraft;
import net.minecraft.profiler.IProfiler;
import net.minecraftforge.event.world.BlockEvent;
import net.minecraftforge.event.world.ChunkEvent;
import net.minecraftforge.event.world.WorldEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
/**
* This handles all events sent to the client,
* and is the starting point for most of this program.
*
* @author James_Seibel
* @version 05-31-2021
*/
public class ClientProxy
{
public static final Logger LOGGER = LogManager.getLogger("LOD");
private static LodWorld lodWorld = new LodWorld();
private static LodBuilder lodBuilder = new LodBuilder();
private static LodBufferBuilder lodBufferBuilder = new LodBufferBuilder(lodBuilder);
private static LodRender renderer = new LodRender(lodBufferBuilder);
Minecraft mc = Minecraft.getInstance();
public ClientProxy()
{
}
//==============//
// render event //
//==============//
/**
* Do any setup that is required to draw LODs
* and then tell the LodRenderer to draw.
*/
public void renderLods(float partialTicks)
{
if (mc == null || mc.player == null || !lodWorld.getIsWorldLoaded())
return;
// update each regions' width to match the new render distance
int newWidth = Math.max(4,
// TODO is this logic good?
(mc.gameSettings.renderDistanceChunks * LodChunk.WIDTH * 2 * LodConfig.CLIENT.lodChunkRadiusMultiplier.get()) / LodRegion.SIZE
);
if (lodBuilder.regionWidth != newWidth)
{
lodWorld.resizeDimensionRegionWidth(newWidth);
lodBuilder.regionWidth = newWidth;
// skip this frame, hopefully the lodWorld
// should have everything set up by then
return;
}
LodDimension lodDim = lodWorld.getLodDimension(mc.player.world.getDimensionType());
if (lodDim == null)
return;
// offset the regions
double playerX = mc.player.getPosX();
double playerZ = mc.player.getPosZ();
int xOffset = ((int)playerX / (LodChunk.WIDTH * LodRegion.SIZE)) - lodDim.getCenterX();
int zOffset = ((int)playerZ / (LodChunk.WIDTH * LodRegion.SIZE)) - lodDim.getCenterZ();
if (xOffset != 0 || zOffset != 0)
{
lodDim.move(xOffset, zOffset);
}
// TODO for testing
// LodConfig.CLIENT.debugMode.set(false);
// LodConfig.CLIENT.lodDetail.set(LodDetail.DOUBLE);
// LodConfig.CLIENT.lodColorStyle.set(LodColorStyle.INDIVIDUAL_SIDES);
// LodConfig.CLIENT.lodChunkRadiusMultiplier.set(12);
// Note to self:
// if "unspecified" shows up in the pie chart, it is
// possibly because the amount of time between sections
// is too small for the profile to measure
IProfiler profiler = mc.getProfiler();
profiler.endSection(); // get out of "terrain"
profiler.startSection("LOD");
renderer.drawLODs(lodDim, partialTicks, mc.getProfiler());
profiler.endSection(); // end LOD
profiler.startSection("terrain"); // restart terrain
}
//==============//
// forge events //
//==============//
@SubscribeEvent
public void chunkLoadEvent(ChunkEvent.Load event)
{
lodBuilder.generateLodChunkAsync(event.getChunk(), lodWorld, event.getWorld());
}
@SubscribeEvent
public void worldLoadEvent(WorldEvent.Load event)
{
// the player just loaded a new world/dimension
lodWorld.selectWorld(LodUtils.getWorldID(event.getWorld()));
// make sure the correct LODs are being rendered
// (if this isn't done the previous world's LODs may be drawn)
renderer.regenerateLODsNextFrame();
}
@SubscribeEvent
public void worldUnloadEvent(WorldEvent.Unload event)
{
// the player just unloaded a world/dimension
if(mc.getConnection().getWorld() == null)
{
lodBufferBuilder.numberOfChunksWaitingToGenerate = 0;
// the player has disconnected from a server
lodWorld.deselectWorld();
}
}
@SubscribeEvent
public void blockChangeEvent(BlockEvent event)
{
if (event.getClass() == BlockEvent.BreakEvent.class ||
event.getClass() == BlockEvent.EntityPlaceEvent.class ||
event.getClass() == BlockEvent.EntityMultiPlaceEvent.class ||
event.getClass() == BlockEvent.FluidPlaceBlockEvent.class ||
event.getClass() == BlockEvent.PortalSpawnEvent.class)
{
// recreate the LOD where the blocks were changed
lodBuilder.generateLodChunkAsync(event.getWorld().getChunk(event.getPos()), lodWorld, event.getWorld());
}
}
//================//
// public getters //
//================//
public static LodWorld getLodWorld()
{
return lodWorld;
}
public static LodBuilder getLodBuilder()
{
return lodBuilder;
}
public static LodRender getRenderer()
{
return renderer;
}
}
@@ -1,28 +1,23 @@
package com.backsun.lod.renderer;
package com.seibel.lod.render;
import java.awt.Color;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.lwjgl.opengl.GL11;
import com.backsun.lod.builders.LodBufferBuilder;
import com.backsun.lod.handlers.ReflectionHandler;
import com.backsun.lod.objects.LodChunk;
import com.backsun.lod.objects.LodDimension;
import com.backsun.lod.objects.NearFarBuffer;
import com.backsun.lod.objects.NearFarFogSetting;
import com.backsun.lod.util.LodConfig;
import com.backsun.lod.util.enums.ColorDirection;
import com.backsun.lod.util.enums.FogDistance;
import com.backsun.lod.util.enums.FogQuality;
import com.backsun.lod.util.enums.LodCorner;
import com.mojang.blaze3d.matrix.MatrixStack;
import com.mojang.blaze3d.platform.GlStateManager;
import com.mojang.blaze3d.systems.RenderSystem;
import com.seibel.lod.builders.LodBufferBuilder;
import com.seibel.lod.enums.FogDistance;
import com.seibel.lod.enums.FogQuality;
import com.seibel.lod.handlers.ReflectionHandler;
import com.seibel.lod.objects.LodChunk;
import com.seibel.lod.objects.LodDimension;
import com.seibel.lod.objects.NearFarBuffer;
import com.seibel.lod.objects.NearFarFogSetting;
import com.seibel.lod.util.LodConfig;
import net.minecraft.client.Minecraft;
import net.minecraft.client.entity.player.ClientPlayerEntity;
@@ -35,7 +30,6 @@ import net.minecraft.client.renderer.vertex.VertexBuffer;
import net.minecraft.client.renderer.vertex.VertexFormat;
import net.minecraft.potion.Effects;
import net.minecraft.profiler.IProfiler;
import net.minecraft.util.math.AxisAlignedBB;
import net.minecraft.util.math.MathHelper;
import net.minecraft.util.math.vector.Matrix4f;
import net.minecraft.util.math.vector.Vector3d;
@@ -43,13 +37,13 @@ import net.minecraft.util.math.vector.Vector3f;
/**
* This is where all the magic happens.
* This is where all the magic happens. <br>
* This is where LODs are draw to the world.
*
* @author James Seibel
* @version 2-27-2021
* @version 05-08-2021
*/
public class LodRenderer
public class LodRender
{
/** this is the light used when rendering the LODs,
* it should be something different than what is used by Minecraft */
@@ -63,36 +57,23 @@ public class LodRenderer
private GameRenderer gameRender;
private IProfiler profiler;
private float farPlaneDistance;
/** this is the radius of the LODs */
private static final int LOD_CHUNK_DISTANCE_RADIUS = 6;
private ReflectionHandler reflectionHandler;
public LodDimension lodDimension = null;
/** This is used to generate the buildable buffers */
private LodBufferBuilder lodBufferBuilder = null;
private LodBufferBuilder lodBufferBuilder;
/** The buffers that are used to draw LODs using near fog */
private volatile BufferBuilder drawableNearBuffer = null;
private volatile BufferBuilder drawableNearBuffer;
/** The buffers that are used to draw LODs using far fog */
private volatile BufferBuilder drawableFarBuffer = null;
/** The buffers that are used to create LODs using near fog */
private volatile BufferBuilder buildableNearBuffer = null;
/** The buffers that are used to create LODs using far fog */
private volatile BufferBuilder buildableFarBuffer = null;
private volatile BufferBuilder drawableFarBuffer;
/** This is the VertexBuffer used to draw any LODs that use near fog */
private volatile VertexBuffer nearVbo = null;
private volatile VertexBuffer nearVbo;
/** This is the VertexBuffer used to draw any LODs that use far fog */
private volatile VertexBuffer farVbo = null;
private volatile VertexBuffer farVbo;
public static final VertexFormat LOD_VERTEX_FORMAT = DefaultVertexFormats.POSITION_COLOR;
/** This holds the thread used to generate new LODs off the main thread. */
private ExecutorService genThread = Executors.newSingleThreadExecutor();
/** This is used to determine if the LODs should be regenerated */
private int previousChunkRenderDistance = 0;
/** This is used to determine if the LODs should be regenerated */
@@ -105,24 +86,18 @@ public class LodRenderer
/** if this is true the LOD buffers should be regenerated,
* provided they aren't already being regenerated. */
private boolean regen = false;
/** if this is true the LOD buffers are currently being
* regenerated. */
private volatile boolean regenerating = false;
/** if this is true new LOD buffers have been generated
* and are waiting to be swapped with the drawable buffers*/
private volatile boolean switchBuffers = false;
public LodRenderer()
public LodRender(LodBufferBuilder newLodBufferBuilder)
{
mc = Minecraft.getInstance();
gameRender = mc.gameRenderer;
reflectionHandler = new ReflectionHandler();
lodBufferBuilder = newLodBufferBuilder;
}
@@ -133,9 +108,10 @@ public class LodRenderer
* @param newDimension The dimension to draw, if null doesn't replace the current dimension.
* @param partialTicks how far into the current tick this method was called.
*/
public void drawLODs(LodDimension newDimension, float partialTicks, IProfiler newProfiler)
@SuppressWarnings("deprecation")
public void drawLODs(LodDimension lodDim, float partialTicks, IProfiler newProfiler)
{
if (lodDimension == null && newDimension == null)
if (lodDim == null)
{
// if there aren't any loaded LodChunks
// don't try drawing anything
@@ -151,11 +127,7 @@ public class LodRenderer
// initial setup //
//===============//
// used for debugging and viewing how long different processes take
profiler = newProfiler;
profiler.endSection();
profiler.startSection("LOD");
profiler.startSection("LOD setup");
ClientPlayerEntity player = mc.player;
@@ -164,8 +136,7 @@ public class LodRenderer
if ((int)player.getPosX() / LodChunk.WIDTH != prevChunkX ||
(int)player.getPosZ() / LodChunk.WIDTH != prevChunkZ ||
previousChunkRenderDistance != mc.gameSettings.renderDistanceChunks ||
prevFogDistance != LodConfig.CLIENT.fogDistance.get() ||
lodDimension != newDimension)
prevFogDistance != LodConfig.CLIENT.fogDistance.get())
{
// yes
regen = true;
@@ -181,42 +152,25 @@ public class LodRenderer
// the dimension is the same
}
lodDimension = newDimension;
if (lodDimension == null)
// did the user change the debug setting?
if (LodConfig.CLIENT.debugMode.get() != debugging)
{
// if there aren't any loaded LodChunks
// don't try drawing anything
return;
debugging = LodConfig.CLIENT.debugMode.get();
regen = true;
}
if (LodConfig.CLIENT.drawCheckerBoard.get())
{
if (debugging != LodConfig.CLIENT.drawCheckerBoard.get())
regen = true;
debugging = true;
}
else
{
if (debugging != LodConfig.CLIENT.drawCheckerBoard.get())
regen = true;
debugging = false;
}
// determine how far the game's render distance is currently set
int renderDistWidth = mc.gameSettings.renderDistanceChunks;
farPlaneDistance = renderDistWidth * LodChunk.WIDTH;
// set how big the LODs will be and how far they will go
int totalLength = (int) farPlaneDistance * LOD_CHUNK_DISTANCE_RADIUS * 2;
int totalLength = (int) farPlaneDistance * LodConfig.CLIENT.lodChunkRadiusMultiplier.get() * 2;
int numbChunksWide = (totalLength / LodChunk.WIDTH);
//=================//
// create the LODs //
//=================//
@@ -226,32 +180,28 @@ public class LodRenderer
// 2. we aren't already regenerating the LODs
// 3. we aren't waiting for the build and draw buffers to swap
// (this is to prevent thread conflicts)
if (regen && !regenerating && !switchBuffers)
if (regen && !lodBufferBuilder.generatingBuffers && !lodBufferBuilder.newBuffersAvaliable())
{
regenerating = true;
if (lodBufferBuilder == null)
lodBufferBuilder = new LodBufferBuilder();
// this will mainly happen when the view distance is changed
if (drawableNearBuffer == null || drawableFarBuffer == null ||
previousChunkRenderDistance != mc.gameSettings.renderDistanceChunks)
setupBuffers(numbChunksWide);
// generate the LODs on a separate thread to prevent stuttering or freezing
generateLodBuffersAsync(player.getPosX(), player.getPosZ(), numbChunksWide);
lodBufferBuilder.generateLodBuffersAsync(this, lodDim, player.getPosX(), player.getPosZ(), numbChunksWide);
// the regen process has been started
// the regen process has been started,
// it will be done when lodBufferBuilder.newBuffersAvaliable
// is true
regen = false;
}
// replace the buffers used to draw and build,
// this is only done when the createLodBufferGenerationThread
// has finished executing on a parallel thread.
if (switchBuffers)
if (lodBufferBuilder.newBuffersAvaliable())
{
swapBuffers();
switchBuffers = false;
}
@@ -270,13 +220,27 @@ public class LodRenderer
GL11.glEnable(GL11.GL_COLOR_MATERIAL);
GL11.glEnable(GL11.GL_DEPTH_TEST);
// disable the lights Minecraft uses
GL11.glDisable(GL11.GL_LIGHT0);
GL11.glDisable(GL11.GL_LIGHT1);
// get the default projection matrix so we can
// reset it after drawing the LODs
float[] defaultProjMatrix = new float[16];
GL11.glGetFloatv(GL11.GL_PROJECTION_MATRIX, defaultProjMatrix);
Matrix4f modelViewMatrix = generateModelViewMatrix(partialTicks);
setupProjectionMatrix(partialTicks);
setupLighting(partialTicks);
setupLighting(lodDim, partialTicks);
NearFarFogSetting fogSetting = determineFogSettings();
// determine the current fog settings so they can be
// reset after drawing the LODs
float defaultFogStartDist = GL11.glGetFloat(GL11.GL_FOG_START);
float defaultFogEndDist = GL11.glGetFloat(GL11.GL_FOG_END);
int defaultFogMode = GL11.glGetInteger(GL11.GL_FOG_MODE);
@@ -287,6 +251,7 @@ public class LodRenderer
//===========//
// rendering //
//===========//
profiler.endStartSection("LOD draw");
setupFog(fogSetting.nearFogSetting, reflectionHandler.getFogQuality());
sendLodsToGpuAndDraw(nearVbo, modelViewMatrix);
@@ -296,73 +261,48 @@ public class LodRenderer
//=========//
// cleanup //
//=========//
profiler.endStartSection("LOD cleanup");
// this must be done otherwise other parts of the screen may be drawn with a fog effect
// IE the GUI
FogRenderer.resetFog();
RenderSystem.disableFog();
GL11.glPolygonMode(GL11.GL_FRONT_AND_BACK, GL11.GL_FILL);
GL11.glEnable(GL11.GL_TEXTURE_2D);
GL11.glDisable(LOD_GL_LIGHT_NUMBER);
GL11.glDisable(GL11.GL_COLOR_MATERIAL);
// re-enable the lights Minecraft uses
GL11.glEnable(GL11.GL_LIGHT0);
GL11.glEnable(GL11.GL_LIGHT1);
RenderSystem.disableLighting();
// this can't be called until after the buffers are built
// because otherwise the buffers may be set to the wrong size
previousChunkRenderDistance = mc.gameSettings.renderDistanceChunks;
// reset the fog settings so the normal chunks
// will be drawn correctly
RenderSystem.fogStart(defaultFogStartDist);
RenderSystem.fogEnd(defaultFogEndDist);
RenderSystem.fogMode(defaultFogMode);
// end of profiler tracking
// reset the projection matrix so anything drawn after
// the LODs will use the correct projection matrix
Matrix4f mvm = new Matrix4f(defaultProjMatrix);
mvm.transpose();
gameRender.resetProjectionMatrix(mvm);
// clear the depth buffer so anything drawn is drawn
// over the LODs
GL11.glClear(GL11.GL_DEPTH_BUFFER_BIT);
// end of internal LOD profiling
profiler.endSection();
}
/**
* Create the model view matrix to move the LODs
* from object space into world space.
*/
private Matrix4f generateModelViewMatrix(float partialTicks)
{
// get all relevant camera info
ActiveRenderInfo renderInfo = mc.gameRenderer.getActiveRenderInfo();
Vector3d projectedView = renderInfo.getProjectedView();
double cameraX = projectedView.x;
double cameraY = projectedView.y;
double cameraZ = projectedView.z;
// generate the model view matrix
MatrixStack matrixStack = new MatrixStack();
matrixStack.push();
// translate and rotate to the current camera location
matrixStack.rotate(Vector3f.XP.rotationDegrees(renderInfo.getPitch()));
matrixStack.rotate(Vector3f.YP.rotationDegrees(renderInfo.getYaw() + 180));
matrixStack.translate(-cameraX, -cameraY, -cameraZ);
// this isn't a great solution to nausea or portal spinning
// but i'm not sure what to do otherwise
float f = 0.23f * MathHelper.lerp(partialTicks, this.mc.player.prevTimeInPortal, this.mc.player.timeInPortal) * this.mc.gameSettings.screenEffectScale * this.mc.gameSettings.screenEffectScale;
if (f > 0.0F) {
int i = this.mc.player.isPotionActive(Effects.NAUSEA) ? 7 : 20;
float f1 = 5.0F / (f * f + 5.0F) - f * 0.04F;
f1 = f1 * f1;
Vector3f vector3f = new Vector3f(0.0F, MathHelper.SQRT_2 / 2.0F, MathHelper.SQRT_2 / 2.0F);
matrixStack.rotate(vector3f.rotationDegrees((mc.gameRenderer.rendererUpdateCount + partialTicks) * i));
matrixStack.scale(1.01F / f1, 1.0F, 1.0F);
float f2 = -(mc.gameRenderer.rendererUpdateCount + partialTicks) * i;
matrixStack.rotate(vector3f.rotationDegrees(f2));
}
return matrixStack.getLast().getMatrix();
}
/**
* This is where the actual drawing happens.
@@ -374,15 +314,12 @@ public class LodRenderer
if (vbo == null)
return;
profiler.endStartSection("LOD draw setup");
vbo.bindBuffer();
// 0L is the starting pointer, and the value doesn't appear to matter
// 0L is the starting pointer
LOD_VERTEX_FORMAT.setupBufferState(0L);
profiler.endStartSection("LOD draw");
vbo.draw(modelViewMatrix, GL11.GL_QUADS);
profiler.endStartSection("LOD draw cleanup");
VertexBuffer.unbindBuffer();
LOD_VERTEX_FORMAT.clearBufferState();
}
@@ -424,8 +361,8 @@ public class LodRenderer
if (fogQuality == FogQuality.FANCY)
{
RenderSystem.fogEnd(farPlaneDistance * 0.3f * LOD_CHUNK_DISTANCE_RADIUS);
RenderSystem.fogStart(farPlaneDistance * 0.35f * LOD_CHUNK_DISTANCE_RADIUS);
RenderSystem.fogEnd(farPlaneDistance * 1.75f);
RenderSystem.fogStart(farPlaneDistance * 1.95f);
}
else if(fogQuality == FogQuality.FAST)
{
@@ -441,13 +378,13 @@ public class LodRenderer
{
if (fogQuality == FogQuality.FANCY)
{
RenderSystem.fogStart(farPlaneDistance * 0.78f * LOD_CHUNK_DISTANCE_RADIUS);
RenderSystem.fogEnd(farPlaneDistance * 1.0f * LOD_CHUNK_DISTANCE_RADIUS);
RenderSystem.fogStart(farPlaneDistance * 0.85f * LodConfig.CLIENT.lodChunkRadiusMultiplier.get());
RenderSystem.fogEnd(farPlaneDistance * 1.0f * LodConfig.CLIENT.lodChunkRadiusMultiplier.get());
}
else if(fogQuality == FogQuality.FAST)
{
RenderSystem.fogStart(farPlaneDistance * 0.5f * LOD_CHUNK_DISTANCE_RADIUS);
RenderSystem.fogEnd(farPlaneDistance * 0.75f * LOD_CHUNK_DISTANCE_RADIUS);
RenderSystem.fogStart(farPlaneDistance * 0.5f * LodConfig.CLIENT.lodChunkRadiusMultiplier.get());
RenderSystem.fogEnd(farPlaneDistance * 0.75f * LodConfig.CLIENT.lodChunkRadiusMultiplier.get());
}
}
@@ -456,21 +393,82 @@ public class LodRenderer
}
/**
* Create the model view matrix to move the LODs
* from object space into world space.
*/
private Matrix4f generateModelViewMatrix(float partialTicks)
{
// get all relevant camera info
ActiveRenderInfo renderInfo = mc.gameRenderer.getActiveRenderInfo();
Vector3d projectedView = renderInfo.getProjectedView();
// generate the model view matrix
MatrixStack matrixStack = new MatrixStack();
matrixStack.push();
// translate and rotate to the current camera location
matrixStack.rotate(Vector3f.XP.rotationDegrees(renderInfo.getPitch()));
matrixStack.rotate(Vector3f.YP.rotationDegrees(renderInfo.getYaw() + 180));
matrixStack.translate(-projectedView.x, -projectedView.y, -projectedView.z);
return matrixStack.getLast().getMatrix();
}
/**
* create a new projection matrix and send it over to the GPU
* <br><br>
* A lot of this code is copied from renderWorld (line 578)
* in the GameRender class. The code copied is anything with
* a matrixStack and is responsible for making sure the LOD
* objects distort correctly relative to the rest of the world.
* Distortions are caused by: standing in a nether portal,
* nausea potion effect, walking bobbing.
*
* @param partialTicks how many ticks into the frame we are
*/
private void setupProjectionMatrix(float partialTicks)
{
// Note: if the LOD objects don't distort correctly
// compared to regular minecraft terrain, make sure
// all the transformations in renderWorld are here too
MatrixStack matrixStack = new MatrixStack();
matrixStack.push();
gameRender.hurtCameraEffect(matrixStack, partialTicks);
if (this.mc.gameSettings.viewBobbing) {
gameRender.applyBobbing(matrixStack, partialTicks);
}
// potion and nausea effects
float f = MathHelper.lerp(partialTicks, mc.player.prevTimeInPortal, mc.player.timeInPortal) * mc.gameSettings.screenEffectScale * mc.gameSettings.screenEffectScale;
if (f > 0.0F) {
int i = this.mc.player.isPotionActive(Effects.NAUSEA) ? 7 : 20;
float f1 = 5.0F / (f * f + 5.0F) - f * 0.04F;
f1 = f1 * f1;
Vector3f vector3f = new Vector3f(0.0F, MathHelper.SQRT_2 / 2.0F, MathHelper.SQRT_2 / 2.0F);
matrixStack.rotate(vector3f.rotationDegrees((gameRender.rendererUpdateCount + partialTicks) * i));
matrixStack.scale(1.0F / f1, 1.0F, 1.0F);
float f2 = -(gameRender.rendererUpdateCount + partialTicks) * i;
matrixStack.rotate(vector3f.rotationDegrees(f2));
}
// this projection matrix allows us to see past the normal
// world render distance
Matrix4f projectionMatrix =
Matrix4f.perspective(
getFov(partialTicks, true),
(float)this.mc.getMainWindow().getFramebufferWidth() / (float)this.mc.getMainWindow().getFramebufferHeight(),
0.5F,
this.farPlaneDistance * LOD_CHUNK_DISTANCE_RADIUS * 2);
this.farPlaneDistance * LodConfig.CLIENT.lodChunkRadiusMultiplier.get() * 2);
// add the screen space distortions
projectionMatrix.mul(matrixStack.getLast().getMatrix());
gameRender.resetProjectionMatrix(projectionMatrix);
return;
}
@@ -478,11 +476,12 @@ public class LodRenderer
/**
* setup the lighting to be used for the LODs
*/
private void setupLighting(float partialTicks)
@SuppressWarnings("deprecation")
private void setupLighting(LodDimension lodDimension, float partialTicks)
{
float sunBrightness = lodDimension.dimension.hasSkyLight() ? mc.world.getSunBrightness(partialTicks) : 0.2f;
float gammaMultiplyer = (float)mc.gameSettings.gamma - 0.5f;
float lightStrength = sunBrightness - 0.7f + (gammaMultiplyer * 0.2f);
float lightStrength = sunBrightness - 0.4f + (gammaMultiplyer * 0.2f);
float lightAmbient[] = {lightStrength, lightStrength, lightStrength, 1.0f};
@@ -494,6 +493,7 @@ public class LodRenderer
RenderSystem.enableLighting();
}
/**
* Create all buffers that will be used.
*/
@@ -510,11 +510,11 @@ public class LodRenderer
drawableNearBuffer = new BufferBuilder(bufferMaxCapacity);
drawableFarBuffer = new BufferBuilder(bufferMaxCapacity);
buildableNearBuffer = new BufferBuilder(bufferMaxCapacity);
buildableFarBuffer = new BufferBuilder(bufferMaxCapacity);
lodBufferBuilder.setupBuffers(bufferMaxCapacity);
}
@@ -524,183 +524,27 @@ public class LodRenderer
// Other Misc Functions //
//======================//
/**
* @Returns -1 if there are no valid points
* If this is called then the next time "drawLODs" is called
* the LODs will be regenerated; the same as if the player moved.
*/
private int getValidHeightPoint(short[] heightPoints)
public void regenerateLODsNextFrame()
{
if (heightPoints[LodCorner.NE.value] != -1)
return heightPoints[LodCorner.NE.value];
if (heightPoints[LodCorner.NW.value] != -1)
return heightPoints[LodCorner.NW.value];
if (heightPoints[LodCorner.SE.value] != -1)
return heightPoints[LodCorner.NE.value];
return heightPoints[LodCorner.NE.value];
regen = true;
}
/**
* Create a thread to asynchronously generate LOD buffers
* centered around the given camera X and Z.
* <br>
* This thread will write to the drawableNearBuffers and drawableFarBuffers.
* <br>
* After the buildable buffers have been generated they must be
* swapped with the drawable buffers to be drawn.
*/
private void generateLodBuffersAsync(double playerX, double playerZ,
int numbChunksWide)
{
// this is where we store the points for each LOD object
AxisAlignedBB lodArray[][] = new AxisAlignedBB[numbChunksWide][numbChunksWide];
// this is where we store the color for each LOD object
Color colorArray[][] = new Color[numbChunksWide][numbChunksWide];
int alpha = 255; // 0 - 255
Color red = new Color(255, 0, 0, alpha);
Color black = new Color(0, 0, 0, alpha);
Color white = new Color(255, 255, 255, alpha);
@SuppressWarnings("unused")
Color invisible = new Color(0,0,0,0);
@SuppressWarnings("unused")
Color error = new Color(255, 0, 225, alpha); // bright pink
// this seemingly useless math is required,
// just using (int) camera doesn't work
int playerXChunkOffset = ((int) playerX / LodChunk.WIDTH) * LodChunk.WIDTH;
int playerZChunkOffset = ((int) playerZ / LodChunk.WIDTH) * LodChunk.WIDTH;
// this is where we will start drawing squares
// (exactly half the total width)
int startX = (-LodChunk.WIDTH * (numbChunksWide / 2)) + playerXChunkOffset;
int startZ = (-LodChunk.WIDTH * (numbChunksWide / 2)) + playerZChunkOffset;
Thread t = new Thread(()->
{
// x axis
for (int i = 0; i < numbChunksWide; i++)
{
// z axis
for (int j = 0; j < numbChunksWide; j++)
{
// skip the middle
// (As the player moves some chunks will overlap or be missing,
// this is just how chunk loading/unloading works. This can hopefully
// be hidden with careful use of fog)
int middle = (numbChunksWide / 2);
if (isCoordInCenterArea(i, j, middle))
{
continue;
}
// set where this square will be drawn in the world
double xOffset = (LodChunk.WIDTH * i) + // offset by the number of LOD blocks
startX; // offset so the center LOD block is centered underneath the player
double yOffset = 0;
double zOffset = (LodChunk.WIDTH * j) + startZ;
int chunkX = i + (startX / LodChunk.WIDTH);
int chunkZ = j + (startZ / LodChunk.WIDTH);
LodChunk lod = lodDimension.getLodFromCoordinates(chunkX, chunkZ);
if (lod == null)
{
// note: for some reason if any color or lod objects are set here
// it causes the game to use 100% gpu;
// undefined in the debug menu
// and drop to ~6 fps.
colorArray[i][j] = null;
lodArray[i][j] = null;
// This is something I would like to have done someday
// but currently world generation is too slow to have it
// here, maybe it could be put in a loop
// that happens every tick for a specific number of chunks?
// LodChunk tmpLod = new LodChunk();
// tmpLod.x = chunkX;
// tmpLod.z = chunkZ;
// lodDimension.addLod(tmpLod);
//
// world.getChunkProvider().getChunk(chunkX, chunkZ, true);
// System.out.println(chunkX + "," + chunkZ);
continue;
}
Color c = new Color(
(lod.colors[ColorDirection.TOP.value].getRed()),
(lod.colors[ColorDirection.TOP.value].getGreen()),
(lod.colors[ColorDirection.TOP.value].getBlue()),
lod.colors[ColorDirection.TOP.value].getAlpha());
if (!debugging)
{
// add the color to the array
colorArray[i][j] = c;
}
else
{
// if debugging draw the squares as a black and white checker board
if ((chunkX + chunkZ) % 2 == 0)
c = white;
else
c = black;
// draw the first square as red
if (i == 0 && j == 0)
c = red;
colorArray[i][j] = c;
}
// add the new box to the array
int topPoint = getValidHeightPoint(lod.top);
int bottomPoint = getValidHeightPoint(lod.bottom);
// don't draw an LOD if it is empty
if (topPoint == -1 && bottomPoint == -1)
continue;
lodArray[i][j] = new AxisAlignedBB(0, bottomPoint, 0, LodChunk.WIDTH, topPoint, LodChunk.WIDTH).offset(xOffset, yOffset, zOffset);
}
}
// generate our new buildable buffers
NearFarBuffer nearFarBuffers = lodBufferBuilder.createBuffers(
buildableNearBuffer, buildableFarBuffer,
LodConfig.CLIENT.fogDistance.get(), lodArray, colorArray);
// update our buffers
buildableNearBuffer = nearFarBuffers.nearBuffer;
buildableFarBuffer = nearFarBuffers.farBuffer;
// mark the buildable buffers as ready to swap
regenerating = false;
switchBuffers = true;
});
genThread.execute(t);
return;
}
/**
* Swap buildable and drawable buffers.
* Replace the current drawable buffers with the newly
* created buffers from the lodBufferBuilder.
*/
private void swapBuffers()
{
// swap the BufferBuilders
BufferBuilder tmp = buildableNearBuffer;
buildableNearBuffer = drawableNearBuffer;
drawableNearBuffer = tmp;
tmp = buildableFarBuffer;
buildableFarBuffer = drawableFarBuffer;
drawableFarBuffer = tmp;
// replace the drawable buffers with
// the newly created buffers from the lodBufferBuilder
NearFarBuffer newBuffers = lodBufferBuilder.swapBuffers(drawableNearBuffer, drawableFarBuffer);
drawableNearBuffer = newBuffers.nearBuffer;
drawableFarBuffer = newBuffers.farBuffer;
// bind the buffers with their respective VBOs
@@ -719,21 +563,7 @@ public class LodRenderer
}
/**
* Returns if the given coordinate is in the loaded area of the world.
* @param centerCoordinate the center of the loaded world
*/
private boolean isCoordInCenterArea(int i, int j, int centerCoordinate)
{
return (i >= centerCoordinate - mc.gameSettings.renderDistanceChunks
&& i <= centerCoordinate + mc.gameSettings.renderDistanceChunks)
&&
(j >= centerCoordinate - mc.gameSettings.renderDistanceChunks
&& j <= centerCoordinate + mc.gameSettings.renderDistanceChunks);
}
public double getFov(float partialTicks, boolean useFovSetting)
private double getFov(float partialTicks, boolean useFovSetting)
{
return mc.gameRenderer.getFOVModifier(mc.gameRenderer.getActiveRenderInfo(), partialTicks, useFovSetting);
}
@@ -748,7 +578,6 @@ public class LodRenderer
{
NearFarFogSetting fogSetting = new NearFarFogSetting();
LodConfig.CLIENT.fogDistance.get();
switch(reflectionHandler.getFogQuality())
{
case FANCY:
@@ -1,4 +1,4 @@
package com.backsun.lod.renderer;
package com.seibel.lod.render;
import net.minecraft.client.Minecraft;
@@ -0,0 +1,149 @@
package com.seibel.lod.util;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.logging.log4j.LogManager;
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
import com.electronwill.nightconfig.core.io.WritingMode;
import com.seibel.lod.ModInfo;
import com.seibel.lod.enums.FogDistance;
import com.seibel.lod.enums.LodColorStyle;
import com.seibel.lod.enums.LodDetail;
import com.seibel.lod.enums.LodTemplate;
import net.minecraftforge.common.ForgeConfigSpec;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.config.ModConfig;
/**
*
* @author James Seibel
* @version 05-31-2021
*/
@Mod.EventBusSubscriber
public class LodConfig
{
public static class Client
{
public ForgeConfigSpec.BooleanValue drawLODs;
public ForgeConfigSpec.EnumValue<FogDistance> fogDistance;
public ForgeConfigSpec.BooleanValue debugMode;
public ForgeConfigSpec.EnumValue<LodTemplate> lodTemplate;
public ForgeConfigSpec.EnumValue<LodDetail> lodDetail;
public ForgeConfigSpec.EnumValue<LodColorStyle> lodColorStyle;
/** this is multiplied by the default view distance
* to determine how far out to generate/render LODs */
public ForgeConfigSpec.IntValue lodChunkRadiusMultiplier;
Client(ForgeConfigSpec.Builder builder)
{
builder.comment(ModInfo.MODNAME + " configuration settings").push("client");
drawLODs = builder
.comment("\n"
+ " If false LODs will not be drawn, \n"
+ " however they will still be generated \n"
+ " and saved to file for later use.")
.define("drawLODs", true);
fogDistance = builder
.comment("\n"
+ " At what distance should Fog be drawn on the LODs? \n"
+ " If the fog cuts off ubruptly or you are using Optifine's \"fast\" \n"
+ " fog option set this to " + FogDistance.NEAR.toString() + " or " + FogDistance.FAR.toString() + ".")
.defineEnum("fogDistance", FogDistance.NEAR_AND_FAR);
debugMode = builder
.comment("\n"
+ " If false the LODs will draw with their normal world colors. \n"
+ " If true they will draw as a black and white checkerboard. \n"
+ " This can be used for debugging or imagining you are playing a \n"
+ " giant game of chess ;)")
.define("drawCheckerBoard", false);
lodTemplate = builder
.comment("\n"
+ " How should the LODs be drawn? \n"
+ " " + LodTemplate.CUBIC.toString() + ": LOD Chunks are drawn as rectangular prisms (boxes). \n"
+ " " + LodTemplate.TRIANGULAR.toString() + ": LOD Chunks smoothly transition between other. \n"
+ " " + LodTemplate.DYNAMIC.toString() + ": LOD Chunks smoothly transition between other, "
+ " " + " unless a neighboring chunk is at a significantly different height. ")
.defineEnum("lodTemplate", LodTemplate.CUBIC);
lodDetail = builder
.comment("\n"
+ " How detailed should the LODs be? \n"
+ " " + LodDetail.SINGLE.toString() + ": render 1 LOD for each Chunk. \n"
+ " " + LodDetail.DOUBLE.toString() + ": render 4 LODs for each Chunk.")
.defineEnum("lodGeometryQuality", LodDetail.SINGLE);
lodColorStyle = builder
.comment("\n"
+ " How should the LODs be colored? \n"
+ " " + LodColorStyle.TOP.toString() + ": Use the color from the top of the LOD chunk for all sides. \n"
+ " " + LodColorStyle.INDIVIDUAL_SIDES.toString() + ": For each side of the LOD use the color corresponding to that side. ")
.defineEnum("lodColorStyle", LodColorStyle.TOP);
lodChunkRadiusMultiplier = builder
.comment("\n"
+ " This is multiplied by the default view distance \n"
+ " to determine how far out to generate/render LODs. \n"
+ " A value of 2 means that there is 1 render distance worth \n"
+ " of LODs in each cardinal direction.")
.defineInRange("lodChunkRadiusMultiplier", 6, 2, 1023);
builder.pop();
}
}
/**
* {@link Path} to the configuration file of this mod
*/
private static final Path CONFIG_PATH =
Paths.get("config", ModInfo.MODID + ".toml");
public static final ForgeConfigSpec clientSpec;
public static final Client CLIENT;
static {
final Pair<Client, ForgeConfigSpec> specPair = new ForgeConfigSpec.Builder().configure(Client::new);
clientSpec = specPair.getRight();
CLIENT = specPair.getLeft();
// setup the config file
CommentedFileConfig config = CommentedFileConfig.builder(CONFIG_PATH)
.writingMode(WritingMode.REPLACE)
.build();
config.load();
config.save();
clientSpec.setConfig(config);
}
@SubscribeEvent
public static void onLoad(final ModConfig.Loading configEvent)
{
LogManager.getLogger().debug(ModInfo.MODNAME, "Loaded forge config file {}", configEvent.getConfig().getFileName());
}
@SubscribeEvent
public static void onFileChange(final ModConfig.Reloading configEvent)
{
LogManager.getLogger().debug(ModInfo.MODNAME, "Forge config just got changed on the file system!");
}
}
@@ -0,0 +1,215 @@
package com.seibel.lod.util;
import com.seibel.lod.objects.LodRegion;
import com.seibel.lod.objects.RegionPos;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ServerData;
import net.minecraft.server.integrated.IntegratedServer;
import net.minecraft.util.math.ChunkPos;
import net.minecraft.world.DimensionType;
import net.minecraft.world.IWorld;
import net.minecraft.world.chunk.ChunkSection;
import net.minecraft.world.chunk.IChunk;
import net.minecraft.world.server.ServerChunkProvider;
import net.minecraft.world.server.ServerWorld;
/**
* This class holds methods that may be used in multiple places.
*
* @author James Seibel
* @version 04-01-2021
*/
public class LodUtils
{
private static Minecraft mc = Minecraft.getInstance();
/**
* Gets the first valid ServerWorld.
*
* @return null if there are no ServerWorlds
*/
public static ServerWorld getFirstValidServerWorld()
{
if (mc.getIntegratedServer() == null)
return null;
Iterable<ServerWorld> worlds = mc.getIntegratedServer().getWorlds();
for (ServerWorld world : worlds)
return world;
return null;
}
/**
* Gets the ServerWorld for the relevant dimension.
*
* @return null if there is no ServerWorld for the given dimension
*/
public static ServerWorld getServerWorldFromDimension(DimensionType dimension)
{
IntegratedServer server = mc.getIntegratedServer();
if (server == null)
return null;
Iterable<ServerWorld> worlds = server.getWorlds();
ServerWorld returnWorld = null;
for (ServerWorld world : worlds)
{
if(world.getDimensionType() == dimension)
{
returnWorld = world;
break;
}
}
return returnWorld;
}
/**
* Convert the given ChunkPos into a RegionPos.
*/
public static RegionPos convertChunkPosToRegionPos(ChunkPos pos)
{
RegionPos rPos = new RegionPos();
rPos.x = pos.x / LodRegion.SIZE;
rPos.z = pos.z / LodRegion.SIZE;
// prevent issues if X/Z is negative and less than 16
if (pos.x < 0)
{
rPos.x = (Math.abs(rPos.x) * -1) - 1;
}
if (pos.z < 0)
{
rPos.z = (Math.abs(rPos.z) * -1) - 1;
}
return rPos;
}
/**
* Return whether the given chunk
* has any data in it.
*/
public static boolean chunkHasBlockData(IChunk chunk)
{
ChunkSection[] blockStorage = chunk.getSections();
for(ChunkSection section : blockStorage)
{
if(section != null && !section.isEmpty())
{
return true;
}
}
return false;
}
public static String getCurrentDimensionID()
{
Minecraft mc = Minecraft.getInstance();
if(mc.isIntegratedServerRunning())
{
// this will return the world save location
// and the dimension folder
if(mc.world == null)
return "";
ServerWorld serverWorld = LodUtils.getServerWorldFromDimension(mc.world.getDimensionType());
if(serverWorld == null)
return "";
ServerChunkProvider provider = serverWorld.getChunkProvider();
if(provider == null)
return "";
return provider.getSavedData().folder.toString();
}
else
{
ServerData server = mc.getCurrentServerData();
return server.serverName + ", IP " +
server.serverIP + ", GameVersion " +
server.gameVersion.getString() + "\\"
+ "dim_" + mc.world.getDimensionType().getEffects().getPath() + "\\";
}
}
/**
* If on single player this will return the name of the user's
* world and the dimensional save folder, if in multiplayer
* it will return the server name, game version, and dimension.<br>
* <br>
* This can be used to determine where to save files for a given
* dimension.
*/
public static String getDimensionIDFromWorld(IWorld world)
{
Minecraft mc = Minecraft.getInstance();
if(mc.isIntegratedServerRunning())
{
// this will return the world save location
// and the dimension folder
ServerWorld serverWorld = LodUtils.getServerWorldFromDimension(world.getDimensionType());
if(serverWorld == null)
throw new NullPointerException("getDimensionIDFromWorld wasn't able to get the ServerWorld for the dimension " + world.getDimensionType().getEffects().getPath());
ServerChunkProvider provider = serverWorld.getChunkProvider();
if(provider == null)
throw new NullPointerException("getDimensionIDFromWorld wasn't able to get the ServerChunkProvider for the dimension " + world.getDimensionType().getEffects().getPath());
return provider.getSavedData().folder.toString();
}
else
{
ServerData server = mc.getCurrentServerData();
return server.serverName + ", IP " +
server.serverIP + ", GameVersion " +
server.gameVersion.getString() + "\\"
+ "dim_" + world.getDimensionType().getEffects().getPath() + "\\";
}
}
/**
* If on single player this will return the name of the user's
* world, if in multiplayer it will return the server name
* and game version.
*/
public static String getWorldID(IWorld world)
{
if(mc.isIntegratedServerRunning())
{
// chop off the dimension ID as it is not needed/wanted
String dimId = getDimensionIDFromWorld(world);
// get the world name
int saveIndex = dimId.indexOf("saves") + 1 + "saves".length();
int slashIndex = dimId.indexOf('\\', saveIndex);
dimId = dimId.substring(saveIndex, slashIndex);
return dimId;
}
else
{
ServerData server = mc.getCurrentServerData();
return server.serverName + ", IP " +
server.serverIP + ", GameVersion " +
server.gameVersion.getString();
}
}
}
@@ -1,4 +1,4 @@
package com.backsun.lod.util;
package com.seibel.lod.util;
/**
* This holds meta information about the mod.
@@ -1,8 +1,8 @@
package com.backsun.lod.util;
package com.seibel.lod.util;
import java.lang.reflect.Field;
import com.backsun.lod.util.enums.FogQuality;
import com.seibel.lod.enums.FogQuality;
import net.minecraft.client.Minecraft;
@@ -1,34 +1,23 @@
# Note: to update code in eclipse run the "eclipse" command in graldew
# make public the method getFOVModifier
# used when creating the projection matrix
public net.minecraft.client.renderer.GameRenderer func_215311_a(Lnet/minecraft/client/renderer/ActiveRenderInfo;FZ)D # getFOVModifier
public net.minecraft.client.renderer.GameRenderer field_78529_t # rendererUpdateCount
public net.minecraft.client.renderer.GameRenderer func_228380_a_(Lcom/mojang/blaze3d/matrix/MatrixStack;F)V # hurtCameraEffect
public net.minecraft.client.renderer.GameRenderer func_228383_b_(Lcom/mojang/blaze3d/matrix/MatrixStack;F)V # applyBobbing
# make public the byteBuffer in BufferBuilder
# used when accessing built byteBuffers
public net.minecraft.client.renderer.BufferBuilder field_179001_a # byteBuffer
# make public the cameraZoom in the GameRenderer
public net.minecraft.client.renderer.GameRenderer field_78503_V # cameraZoom
# make public the cameraYaw in the GameRenderer
public net.minecraft.client.renderer.GameRenderer field_228376_w_ # cameraYaw
# make public the cameraPitch in the GameRenderer
public net.minecraft.client.renderer.GameRenderer field_228377_x_ # cameraPitch
# make public the folder in the DimensionSavedDataManager
# used when determining where to save files too
public net.minecraft.world.storage.DimensionSavedDataManager field_215759_d # folder
# make public the ambiantLight field in DimensionType
public net.minecraft.world.DimensionType field_236017_x_ # ambientLight
# make public the renderUpdateCount in GameRenderer
public net.minecraft.client.renderer.GameRenderer field_78529_t # rendererUpdateCount
# make public the materialColor in AbstractBlockState
# used when generating LodChunks
public net.minecraft.block.AbstractBlock$AbstractBlockState field_235704_h_ # materialColor
#=====================#
# Examples from Forge #
#=====================#
+1 -1
View File
@@ -24,7 +24,7 @@ modId="lod" #mandatory
#// The version number of the mod - there's a few well known ${} variables useable here or just hardcode it
#//${file.jarVersion} will substitute the value of the Implementation-Version as read from the mod's JAR file metadata
#// see the associated build.gradle script for how to populate this completely automatically during a build
version="a1" #mandatory
version="a1.2" #mandatory
#// A display name for the mod
displayName="Levels of Detail" #mandatory
+1 -1
View File
@@ -1,6 +1,6 @@
{
"required": true,
"package": "com.backsun.lod.mixin",
"package": "com.seibel.lod.mixin",
"compatibilityLevel": "JAVA_8",
"refmap": "lod.refmap.json",
"mixins": [
+1 -1
View File
@@ -3,7 +3,7 @@
"modid": "lod",
"name": "Level Of Details",
"description": "Generates and renders simplified chunks beyond the normal view distance, at a low performance cost.",
"version": "a1",
"version": "a1.2",
"mcversion": "1.16.4",
"url": "",
"updateUrl": "",