aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/makamys/neodymium
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/makamys/neodymium')
-rw-r--r--src/main/java/makamys/neodymium/LODMod.java239
-rw-r--r--src/main/java/makamys/neodymium/MixinConfigPlugin.java72
-rw-r--r--src/main/java/makamys/neodymium/ducks/IWorldRenderer.java13
-rw-r--r--src/main/java/makamys/neodymium/mixin/MixinChunkCache.java29
-rw-r--r--src/main/java/makamys/neodymium/mixin/MixinEntityRenderer.java35
-rw-r--r--src/main/java/makamys/neodymium/mixin/MixinRenderBlocks.java29
-rw-r--r--src/main/java/makamys/neodymium/mixin/MixinRenderGlobal.java40
-rw-r--r--src/main/java/makamys/neodymium/mixin/MixinRenderGlobal_OptiFine.java25
-rw-r--r--src/main/java/makamys/neodymium/mixin/MixinWorldRenderer.java181
-rw-r--r--src/main/java/makamys/neodymium/renderer/ChunkMesh.java455
-rw-r--r--src/main/java/makamys/neodymium/renderer/FarChunkCache.java12
-rw-r--r--src/main/java/makamys/neodymium/renderer/FarWorldRenderer.java14
-rw-r--r--src/main/java/makamys/neodymium/renderer/GPUMemoryManager.java215
-rw-r--r--src/main/java/makamys/neodymium/renderer/LODChunk.java184
-rw-r--r--src/main/java/makamys/neodymium/renderer/LODRegion.java195
-rw-r--r--src/main/java/makamys/neodymium/renderer/LODRenderer.java730
-rw-r--r--src/main/java/makamys/neodymium/renderer/Mesh.java44
-rw-r--r--src/main/java/makamys/neodymium/renderer/MeshQuad.java426
-rw-r--r--src/main/java/makamys/neodymium/renderer/SimpleChunkMesh.java350
-rw-r--r--src/main/java/makamys/neodymium/util/BufferWriter.java46
-rw-r--r--src/main/java/makamys/neodymium/util/GuiHelper.java61
-rw-r--r--src/main/java/makamys/neodymium/util/MCUtil.java20
-rw-r--r--src/main/java/makamys/neodymium/util/SpriteUtil.java55
-rw-r--r--src/main/java/makamys/neodymium/util/Util.java88
24 files changed, 3558 insertions, 0 deletions
diff --git a/src/main/java/makamys/neodymium/LODMod.java b/src/main/java/makamys/neodymium/LODMod.java
new file mode 100644
index 0000000..47c948b
--- /dev/null
+++ b/src/main/java/makamys/neodymium/LODMod.java
@@ -0,0 +1,239 @@
+package makamys.neodymium;
+
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.FontRenderer;
+import net.minecraft.client.gui.ScaledResolution;
+import net.minecraft.client.renderer.entity.RenderManager;
+import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.world.World;
+import net.minecraftforge.client.event.EntityViewRenderEvent;
+import net.minecraftforge.client.event.RenderGameOverlayEvent;
+import net.minecraftforge.client.event.RenderGameOverlayEvent.ElementType;
+import net.minecraftforge.common.MinecraftForge;
+import net.minecraftforge.common.config.Configuration;
+import net.minecraftforge.event.world.ChunkEvent;
+import net.minecraftforge.event.world.WorldEvent;
+
+import java.io.File;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.lwjgl.input.Keyboard;
+import org.lwjgl.opengl.GL11;
+
+import cpw.mods.fml.common.FMLCommonHandler;
+import cpw.mods.fml.common.Mod;
+import cpw.mods.fml.common.Mod.EventHandler;
+import cpw.mods.fml.common.event.FMLInitializationEvent;
+import cpw.mods.fml.common.event.FMLPreInitializationEvent;
+import cpw.mods.fml.common.eventhandler.SubscribeEvent;
+import cpw.mods.fml.common.gameevent.TickEvent;
+import cpw.mods.fml.relauncher.Side;
+import cpw.mods.fml.relauncher.SideOnly;
+import makamys.neodymium.renderer.LODRenderer;
+import makamys.neodymium.util.SpriteUtil;
+
+@Mod(modid = LODMod.MODID, version = LODMod.VERSION)
+public class LODMod
+{
+ public static final String MODID = "lodmod";
+ public static final String VERSION = "0.0";
+
+ public static final Logger LOGGER = LogManager.getLogger("lodmod");
+
+ public static LODRenderer renderer;
+
+ public static boolean enabled;
+ public static int chunkLoadsPerTick;
+ public static List<Class> blockClassBlacklist;
+ public static double fogStart;
+ public static double fogEnd;
+ public static double farPlaneDistanceMultiplier;
+ public static float maxSimpleMeshHeight;
+ public static boolean forceVanillaBiomeTemperature;
+ public static boolean hideUnderVanillaChunks;
+ public static boolean disableChunkMeshes;
+ public static boolean disableSimpleMeshes;
+ public static boolean saveMeshes;
+ public static boolean optimizeChunkMeshes;
+ public static int maxMeshesPerFrame;
+ public static int sortFrequency;
+ public static int gcRate;
+ public static int VRAMSize;
+ public static int debugPrefix;
+
+ private File configFile;
+
+ public static boolean fogEventWasPosted;
+
+ public static boolean ofFastRender;
+ public static boolean enableFog;
+
+ @EventHandler
+ public void preInit(FMLPreInitializationEvent event)
+ {
+ configFile = event.getSuggestedConfigurationFile();
+ reloadConfig();
+ }
+
+ private void reloadConfig() {
+ Configuration config = new Configuration(configFile);
+
+ config.load();
+ enabled = config.get("General", "enabled", true).getBoolean();
+ chunkLoadsPerTick = config.get("General", "chunkLoadsPerTick", 64).getInt();
+ blockClassBlacklist = Arrays.stream(config.get("General", "blockClassBlacklist", "net.minecraft.block.BlockRotatedPillar;biomesoplenty.common.blocks.BlockBOPLog;gregapi.block.multitileentity.MultiTileEntityBlock").getString().split(";"))
+ .map(className -> {
+ try {
+ return Class.forName(className);
+ } catch (ClassNotFoundException e) {
+ return null;
+ }
+ })
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+ fogStart = config.get("Fog", "fogStart", "0.4").getDouble();
+ fogEnd = config.get("Fog", "fogEnd", "0.8").getDouble();
+ farPlaneDistanceMultiplier = config.get("Fog", "farPlaneDistanceMultiplier", "1.0").getDouble();
+
+ maxSimpleMeshHeight = (float)config.get("Debug", "maxSimpleMeshHeight", 1000.0).getDouble();
+
+ forceVanillaBiomeTemperature = config.get("Simple mesh generation", "forceVanillaBiomeTemperature", true).getBoolean();
+
+ hideUnderVanillaChunks = config.getBoolean("hideUnderVanillaChunks", "render", true, "");
+ disableChunkMeshes = config.getBoolean("disableChunkMeshes", "render", true, "");
+ disableSimpleMeshes = config.getBoolean("disableSimpleMeshes", "render", false, "");
+ optimizeChunkMeshes = config.getBoolean("optimizeChunkMeshes", "render", true, "");
+ saveMeshes = config.getBoolean("saveMeshes", "render", false, "");
+ maxMeshesPerFrame = config.getInt("maxMeshesPerFrame", "render", -1, -1, Integer.MAX_VALUE, "");
+ sortFrequency = config.getInt("sortFrequency", "render", 1, 1, Integer.MAX_VALUE, "");
+ gcRate = config.getInt("gcRate", "render", 1, 1, Integer.MAX_VALUE, "Maximum number of meshes to relocate each frame.");
+ VRAMSize = config.getInt("VRAMSize", "render", 1024, 1, Integer.MAX_VALUE, "VRAM buffer size (MB).");
+ enableFog = config.getBoolean("enableFog", "render", true, "");
+ debugPrefix = config.getInt("debugPrefix", "debug", Keyboard.KEY_F4, -1, Integer.MAX_VALUE, "This key has to be held down while pressing the debug keybinds. LWJGL keycode. Setting this to 0 will make the keybinds usable without holding anything else down. Setting this to -1 will disable debug keybinds entirely.");
+
+ if(config.hasChanged()) {
+ config.save();
+ }
+ }
+
+ @EventHandler
+ public void init(FMLInitializationEvent event)
+ {
+ FMLCommonHandler.instance().bus().register(this);
+ MinecraftForge.EVENT_BUS.register(this);
+ }
+
+ private void onPlayerWorldChanged(World newWorld) {
+ if(getRendererWorld() == null && newWorld != null) {
+ reloadConfig();
+ if(enabled) {
+ SpriteUtil.init();
+ }
+ }
+ if(renderer != null) {
+ renderer.destroy();
+ renderer = null;
+ }
+ if(enabled && newWorld != null) {
+ renderer = new LODRenderer(newWorld);
+ }
+ }
+
+ @SubscribeEvent
+ @SideOnly(Side.CLIENT)
+ public void onWorldUnload(WorldEvent.Unload event) {
+ if(event.world == getRendererWorld()) {
+ onPlayerWorldChanged(null);
+ }
+ }
+
+ @SubscribeEvent
+ public void onChunkLoad(ChunkEvent.Load event) {
+ if(!event.world.isRemote) return;
+
+ if(isActive()) {
+ renderer.onChunkLoad(event);
+ }
+ }
+
+ public static boolean isActive() {
+ return renderer != null && renderer.hasInited && !renderer.destroyPending;
+ }
+
+ private World getRendererWorld() {
+ return renderer != null ? renderer.world : null;
+ }
+
+ @SubscribeEvent
+ public void onClientTick(TickEvent.ClientTickEvent event) {
+ if(event.phase == TickEvent.Phase.START) {
+ EntityPlayer player = Minecraft.getMinecraft().thePlayer;
+ World world = player != null ? player.worldObj : null;
+ if(world != getRendererWorld()) {
+ onPlayerWorldChanged(world);
+ }
+
+ if(MixinConfigPlugin.isOptiFinePresent()) {
+ try {
+ ofFastRender = (boolean)Class.forName("Config").getMethod("isFastRender").invoke(null);
+ } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException
+ | SecurityException | ClassNotFoundException e) {
+ // oops
+ }
+ }
+ }
+ }
+
+ @SubscribeEvent
+ public void onServerTick(TickEvent.ServerTickEvent event) {
+ if(event.phase == TickEvent.Phase.START) {
+ if(isActive()) {
+ renderer.serverTick();
+ }
+ }
+ }
+
+ @SubscribeEvent
+ public void onRenderTick(TickEvent.RenderTickEvent event) {
+ if(event.phase == TickEvent.Phase.END) {
+ if(isActive()) {
+ renderer.onRenderTickEnd();
+ }
+ }
+ }
+
+ @SubscribeEvent
+ public void onRenderOverlay(RenderGameOverlayEvent event) {
+ FontRenderer fontRenderer = RenderManager.instance.getFontRenderer();
+ if(isActive() && event.type == ElementType.TEXT && fontRenderer != null && Minecraft.getMinecraft().gameSettings.showDebugInfo)
+ {
+ Minecraft mc = Minecraft.getMinecraft();
+ ScaledResolution scaledresolution = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
+ int w = scaledresolution.getScaledWidth();
+ int h = scaledresolution.getScaledHeight();
+
+ int yOffset = 0;
+ for(String s : renderer.getDebugText()) {
+ fontRenderer.drawStringWithShadow(s, w - fontRenderer.getStringWidth(s) - 10, 80 + yOffset, 0xFFFFFF);
+ yOffset += 10;
+ }
+ }
+ }
+
+
+ @SubscribeEvent
+ public void onRenderFog(EntityViewRenderEvent.RenderFogEvent event) {
+ fogEventWasPosted = true;
+ }
+
+ public static boolean shouldRenderVanillaWorld() {
+ return !isActive() || (isActive() && renderer.renderWorld && !renderer.rendererActive);
+ }
+
+}
diff --git a/src/main/java/makamys/neodymium/MixinConfigPlugin.java b/src/main/java/makamys/neodymium/MixinConfigPlugin.java
new file mode 100644
index 0000000..cc4bc30
--- /dev/null
+++ b/src/main/java/makamys/neodymium/MixinConfigPlugin.java
@@ -0,0 +1,72 @@
+package makamys.neodymium;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+
+import org.spongepowered.asm.lib.tree.ClassNode;
+import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin;
+import org.spongepowered.asm.mixin.extensibility.IMixinInfo;
+
+public class MixinConfigPlugin implements IMixinConfigPlugin {
+
+ private static boolean isOptiFinePresent = MixinConfigPlugin.class.getResource("/optifine/OptiFineTweaker.class") != null;
+
+ @Override
+ public void onLoad(String mixinPackage) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public String getRefMapperConfig() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public boolean shouldApplyMixin(String targetClassName, String mixinClassName) {
+ return true;
+ }
+
+ @Override
+ public void acceptTargets(Set<String> myTargets, Set<String> otherTargets) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public List<String> getMixins() {
+ List<String> mixins = new ArrayList<>();
+ mixins.addAll(Arrays.asList("MixinChunkCache",
+ "MixinEntityRenderer",
+ "MixinRenderGlobal",
+ "MixinWorldRenderer",
+ "MixinRenderBlocks"));
+
+ if (isOptiFinePresent()) {
+ System.out.println("Detected OptiFine");
+ mixins.add("MixinRenderGlobal_OptiFine");
+ }
+
+ return mixins;
+ }
+
+ @Override
+ public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
+ // TODO Auto-generated method stub
+
+ }
+
+ public static boolean isOptiFinePresent() {
+ return isOptiFinePresent;
+ }
+
+}
diff --git a/src/main/java/makamys/neodymium/ducks/IWorldRenderer.java b/src/main/java/makamys/neodymium/ducks/IWorldRenderer.java
new file mode 100644
index 0000000..4525cda
--- /dev/null
+++ b/src/main/java/makamys/neodymium/ducks/IWorldRenderer.java
@@ -0,0 +1,13 @@
+package makamys.neodymium.ducks;
+
+import java.util.List;
+
+import org.spongepowered.asm.mixin.Mixin;
+
+import makamys.neodymium.renderer.ChunkMesh;
+import net.minecraft.client.renderer.WorldRenderer;
+
+public interface IWorldRenderer {
+ public List<ChunkMesh> getChunkMeshes();
+ public boolean isDrawn();
+}
diff --git a/src/main/java/makamys/neodymium/mixin/MixinChunkCache.java b/src/main/java/makamys/neodymium/mixin/MixinChunkCache.java
new file mode 100644
index 0000000..b4a7368
--- /dev/null
+++ b/src/main/java/makamys/neodymium/mixin/MixinChunkCache.java
@@ -0,0 +1,29 @@
+package makamys.neodymium.mixin;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Redirect;
+
+import makamys.neodymium.LODMod;
+import makamys.neodymium.renderer.FarChunkCache;
+import makamys.neodymium.renderer.LODRenderer;
+import net.minecraft.world.ChunkCache;
+import net.minecraft.world.World;
+import net.minecraft.world.chunk.Chunk;
+
+@Mixin(ChunkCache.class)
+abstract class MixinChunkCache {
+
+ @Redirect(method = "<init>*", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/World;getChunkFromChunkCoords(II)Lnet/minecraft/world/chunk/Chunk;"))
+ private Chunk redirectGetChunkFromChunkCoords(World world, int p1, int p2) {
+ Chunk chunk = world.getChunkFromChunkCoords(p1, p2);
+ if(LODMod.isActive() && FarChunkCache.class.isInstance(this.getClass()) && chunk.isEmpty()) {
+ Chunk myChunk = LODMod.renderer.getChunkFromChunkCoords(p1, p2);
+ if(myChunk != null) {
+ chunk = myChunk;
+ }
+ }
+ return chunk;
+ }
+
+}
diff --git a/src/main/java/makamys/neodymium/mixin/MixinEntityRenderer.java b/src/main/java/makamys/neodymium/mixin/MixinEntityRenderer.java
new file mode 100644
index 0000000..66d95ae
--- /dev/null
+++ b/src/main/java/makamys/neodymium/mixin/MixinEntityRenderer.java
@@ -0,0 +1,35 @@
+package makamys.neodymium.mixin;
+
+import org.lwjgl.opengl.GL11;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.Redirect;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+import makamys.neodymium.LODMod;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.EntityRenderer;
+import net.minecraft.entity.EntityLivingBase;
+
+@Mixin(EntityRenderer.class)
+abstract class MixinEntityRenderer {
+
+ @Shadow
+ private float farPlaneDistance;
+
+ @Inject(method = "setupCameraTransform", at = @At(value = "FIELD", target = "Lnet/minecraft/client/renderer/EntityRenderer;farPlaneDistance:F", shift = At.Shift.AFTER, ordinal = 1))
+ private void onConstructed(CallbackInfo ci) {
+ if(LODMod.isActive()) {
+ farPlaneDistance *= LODMod.renderer.getFarPlaneDistanceMultiplier();
+ }
+ }
+
+ @Inject(method = "setupFog", at = @At(value = "RETURN"))
+ private void afterSetupFog(int mode, float alpha, CallbackInfo ci) {
+ if(LODMod.isActive()) {
+ LODMod.renderer.afterSetupFog(mode, alpha, farPlaneDistance);
+ }
+ }
+}
diff --git a/src/main/java/makamys/neodymium/mixin/MixinRenderBlocks.java b/src/main/java/makamys/neodymium/mixin/MixinRenderBlocks.java
new file mode 100644
index 0000000..0f8ff41
--- /dev/null
+++ b/src/main/java/makamys/neodymium/mixin/MixinRenderBlocks.java
@@ -0,0 +1,29 @@
+package makamys.neodymium.mixin;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Redirect;
+
+import cpw.mods.fml.relauncher.Side;
+import cpw.mods.fml.relauncher.SideOnly;
+import makamys.neodymium.LODMod;
+import makamys.neodymium.renderer.FarChunkCache;
+import net.minecraft.block.Block;
+import net.minecraft.client.renderer.RenderBlocks;
+import net.minecraft.world.IBlockAccess;
+import net.minecraft.world.World;
+import net.minecraft.world.chunk.Chunk;
+
+@Mixin(RenderBlocks.class)
+abstract class MixinRenderBlocks {
+
+ @Redirect(method = "renderBlockLiquid", at = @At(value = "INVOKE", target = "Lnet/minecraft/block/Block;shouldSideBeRendered(Lnet/minecraft/world/IBlockAccess;IIII)Z"))
+ public boolean shouldSideBeRendered(Block block, IBlockAccess ba, int x, int y, int z, int w) {
+ if(LODMod.isActive()) {
+ return LODMod.renderer.shouldSideBeRendered(block, ba, x, y, z, w);
+ } else {
+ return block.shouldSideBeRendered(ba, x, y, z, w);
+ }
+ }
+
+}
diff --git a/src/main/java/makamys/neodymium/mixin/MixinRenderGlobal.java b/src/main/java/makamys/neodymium/mixin/MixinRenderGlobal.java
new file mode 100644
index 0000000..dc561a2
--- /dev/null
+++ b/src/main/java/makamys/neodymium/mixin/MixinRenderGlobal.java
@@ -0,0 +1,40 @@
+package makamys.neodymium.mixin;
+
+import java.nio.IntBuffer;
+
+import org.lwjgl.opengl.GL11;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.ModifyVariable;
+import org.spongepowered.asm.mixin.injection.Redirect;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+import makamys.neodymium.LODMod;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.RenderGlobal;
+import net.minecraft.client.renderer.WorldRenderer;
+import net.minecraft.entity.Entity;
+
+@Mixin(RenderGlobal.class)
+abstract class MixinRenderGlobal {
+
+ @Shadow
+ private WorldRenderer[] sortedWorldRenderers;
+
+ @Redirect(method = "renderSortedRenderers", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/RenderGlobal;renderAllRenderLists(ID)V"))
+ private void redirectRenderAllRenderLists(RenderGlobal thiz, int p1, double p2) {
+ if(LODMod.shouldRenderVanillaWorld()) {
+ thiz.renderAllRenderLists(p1, p2);
+ }
+ }
+
+ @Inject(method = "renderSortedRenderers", at = @At(value = "HEAD"))
+ public void preRenderSortedRenderers(int startRenderer, int numRenderers, int renderPass, double partialTickTime, CallbackInfoReturnable cir) {
+ if(LODMod.isActive()) {
+ LODMod.renderer.preRenderSortedRenderers(renderPass, partialTickTime, sortedWorldRenderers);
+ }
+ }
+}
diff --git a/src/main/java/makamys/neodymium/mixin/MixinRenderGlobal_OptiFine.java b/src/main/java/makamys/neodymium/mixin/MixinRenderGlobal_OptiFine.java
new file mode 100644
index 0000000..dad164c
--- /dev/null
+++ b/src/main/java/makamys/neodymium/mixin/MixinRenderGlobal_OptiFine.java
@@ -0,0 +1,25 @@
+package makamys.neodymium.mixin;
+
+import java.nio.IntBuffer;
+
+import org.lwjgl.opengl.GL11;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Redirect;
+
+import makamys.neodymium.LODMod;
+import net.minecraft.client.renderer.RenderBlocks;
+import net.minecraft.client.renderer.RenderGlobal;
+
+@Mixin(RenderGlobal.class)
+abstract class MixinRenderGlobal_OptiFine {
+
+ // for OptiFine's Fast Render option
+ @Redirect(method = "renderSortedRenderersFast", at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glCallLists(Ljava/nio/IntBuffer;)V"), remap=false)
+ private void redirectRenderAllRenderLists(IntBuffer buffer) {
+ if(LODMod.shouldRenderVanillaWorld()) {
+ GL11.glCallLists(buffer);
+ }
+ }
+
+}
diff --git a/src/main/java/makamys/neodymium/mixin/MixinWorldRenderer.java b/src/main/java/makamys/neodymium/mixin/MixinWorldRenderer.java
new file mode 100644
index 0000000..47450c3
--- /dev/null
+++ b/src/main/java/makamys/neodymium/mixin/MixinWorldRenderer.java
@@ -0,0 +1,181 @@
+package makamys.neodymium.mixin;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.lwjgl.opengl.GL11;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.Redirect;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+import makamys.neodymium.LODMod;
+import makamys.neodymium.ducks.IWorldRenderer;
+import makamys.neodymium.renderer.ChunkMesh;
+import makamys.neodymium.renderer.FarChunkCache;
+import makamys.neodymium.renderer.FarWorldRenderer;
+import makamys.neodymium.renderer.LODRenderer;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.Tessellator;
+import net.minecraft.client.renderer.WorldRenderer;
+import net.minecraft.client.renderer.entity.RenderItem;
+import net.minecraft.entity.EntityLivingBase;
+import net.minecraft.util.AxisAlignedBB;
+import net.minecraft.world.ChunkCache;
+import net.minecraft.world.World;
+
+@Mixin(WorldRenderer.class)
+abstract class MixinWorldRenderer implements IWorldRenderer {
+
+ @Shadow
+ public int posX;
+ @Shadow
+ public int posY;
+ @Shadow
+ public int posZ;
+
+ @Shadow
+ private boolean isInFrustum;
+ @Shadow
+ public boolean[] skipRenderPass;
+ @Shadow
+ private int glRenderList;
+
+ @Shadow
+ public boolean needsUpdate;
+
+ boolean savedDrawnStatus;
+
+ public List<ChunkMesh> chunkMeshes;
+
+ @Redirect(method = "setPosition", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/entity/RenderItem;renderAABB(Lnet/minecraft/util/AxisAlignedBB;)V"))
+ private void redirectRenderAABB(AxisAlignedBB p1) {
+ if(!FarWorldRenderer.class.isInstance(this.getClass())) {
+ RenderItem.renderAABB(p1);
+ }
+ }
+
+ @Redirect(method = "updateRenderer", at = @At(value = "NEW", target = "Lnet/minecraft/world/ChunkCache;"))
+ private ChunkCache redirectConstructChunkCache(World p1, int p2, int p3, int p4, int p5, int p6, int p7, int p8) {
+ if(!FarWorldRenderer.class.isInstance(this.getClass())) {
+ return new ChunkCache(p1, p2, p3, p4, p5, p6, p7, p8);
+ } else {
+ return new FarChunkCache(p1, p2, p3, p4, p5, p6, p7, p8);
+ }
+ }
+
+ @Inject(method = "updateRenderer", at = @At(value = "HEAD"))
+ private void preUpdateRenderer(CallbackInfo ci) {
+ saveDrawnStatus();
+
+ if(LODMod.isActive()) {
+ if(needsUpdate) {
+ if(chunkMeshes != null) {
+ chunkMeshes.clear();
+ } else {
+ chunkMeshes = new ArrayList<>();
+ }
+ } else {
+ chunkMeshes = null;
+ }
+ }
+ }
+
+ @Inject(method = "updateRenderer", at = @At(value = "RETURN"))
+ private void postUpdateRenderer(CallbackInfo ci) {
+ notifyIfDrawnStatusChanged();
+
+ if(LODMod.isActive()) {
+ if(chunkMeshes != null) {
+ LODMod.renderer.onWorldRendererPost(WorldRenderer.class.cast(this));
+ chunkMeshes.clear();
+ }
+ }
+ }
+
+ @Inject(method = "postRenderBlocks", at = @At(value = "HEAD"))
+ private void prePostRenderBlocks(int pass, EntityLivingBase entity, CallbackInfo ci) {
+ if(LODMod.isActive() && !LODMod.disableChunkMeshes) {
+ if(chunkMeshes != null) {
+ chunkMeshes.add(ChunkMesh.fromTessellator(pass, WorldRenderer.class.cast(this), Tessellator.instance));
+ }
+ }
+ }
+
+ @Redirect(method = "postRenderBlocks", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/Tessellator;draw()I"))
+ private int redirectPostRenderBlocksDraw() {
+ if(!FarWorldRenderer.class.isInstance(this.getClass())) {
+ return Tessellator.instance.draw();
+ } else {
+ Tessellator.instance.reset();
+ return 0;
+ }
+ }
+
+ // There's probably a nicer way to do this
+
+ @Redirect(method = "postRenderBlocks", at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glPopMatrix()V"))
+ private void redirectPostRenderBlocksGL1() {
+ if(!FarWorldRenderer.class.isInstance(this.getClass())) {
+ GL11.glPopMatrix();
+ }
+ }
+
+ @Redirect(method = "postRenderBlocks", at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glEndList()V"))
+ private void redirectPostRenderBlocksGL2() {
+ if(!FarWorldRenderer.class.isInstance(this.getClass())) {
+ GL11.glEndList();
+ }
+ }
+
+ // XXX this is inconsistent, Forge callbacks are preserved in postRenderBlocks but not preRenderBlocks
+
+ @Inject(method = "preRenderBlocks", at = @At(value = "HEAD"))
+ private void preRenderBlocksInjector(CallbackInfo ci) {
+ if(FarWorldRenderer.class.isInstance(this.getClass())) {
+ Tessellator.instance.setTranslation((double)(-this.posX), (double)(-this.posY), (double)(-this.posZ));
+ ci.cancel();
+ }
+ }
+
+ @Inject(method = "setDontDraw", at = @At(value = "HEAD"))
+ private void preSetDontDraw(CallbackInfo ci) {
+ if(LODMod.isActive()) {
+ LODMod.renderer.onWorldRendererChanged(WorldRenderer.class.cast(this), LODRenderer.WorldRendererChange.DELETED);
+ }
+ }
+
+ @Override
+ public List<ChunkMesh> getChunkMeshes() {
+ return chunkMeshes;
+ }
+
+ @Inject(method = "updateInFrustum", at = @At(value = "HEAD"))
+ private void preUpdateInFrustum(CallbackInfo ci) {
+ saveDrawnStatus();
+ }
+
+ @Inject(method = "updateInFrustum", at = @At(value = "RETURN"))
+ private void postUpdateInFrustum(CallbackInfo ci) {
+ notifyIfDrawnStatusChanged();
+ }
+
+ private void saveDrawnStatus() {
+ savedDrawnStatus = isDrawn();
+ }
+
+ private void notifyIfDrawnStatusChanged() {
+ boolean drawn = isDrawn();
+ if(LODMod.isActive() && drawn != savedDrawnStatus) {
+ LODMod.renderer.onWorldRendererChanged(WorldRenderer.class.cast(this), drawn ? LODRenderer.WorldRendererChange.VISIBLE : LODRenderer.WorldRendererChange.INVISIBLE);
+ }
+ }
+
+ @Override
+ public boolean isDrawn() {
+ return isInFrustum && (!skipRenderPass[0] || !skipRenderPass[1]);
+ }
+}
diff --git a/src/main/java/makamys/neodymium/renderer/ChunkMesh.java b/src/main/java/makamys/neodymium/renderer/ChunkMesh.java
new file mode 100644
index 0000000..6c4cd59
--- /dev/null
+++ b/src/main/java/makamys/neodymium/renderer/ChunkMesh.java
@@ -0,0 +1,455 @@
+package makamys.neodymium.renderer;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.nio.ShortBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.lwjgl.BufferUtils;
+
+import makamys.neodymium.LODMod;
+import makamys.neodymium.MixinConfigPlugin;
+import makamys.neodymium.ducks.IWorldRenderer;
+import makamys.neodymium.util.BufferWriter;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.Tessellator;
+import net.minecraft.client.renderer.WorldRenderer;
+import net.minecraft.client.renderer.texture.TextureAtlasSprite;
+import net.minecraft.client.renderer.texture.TextureMap;
+import net.minecraft.entity.Entity;
+import net.minecraft.nbt.NBTBase;
+import net.minecraft.nbt.NBTTagByteArray;
+import net.minecraft.tileentity.TileEntity;
+
+public class ChunkMesh extends Mesh {
+
+ Flags flags;
+
+ // TODO move this somewhere else
+ List<String> nameList = (List<String>) ((TextureMap)Minecraft.getMinecraft().getTextureManager().getTexture(TextureMap.locationBlocksTexture)).mapUploadedSprites.keySet().stream().collect(Collectors.toList());
+
+ public static int usedRAM = 0;
+ public static int instances = 0;
+
+ public ChunkMesh(int x, int y, int z, Flags flags, int quadCount, ByteBuffer buffer, int pass) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ this.flags = flags;
+ this.quadCount = quadCount;
+ this.pass = pass;
+
+ this.buffer = buffer;
+ usedRAM += buffer.limit();
+ instances++;
+ }
+
+ public ChunkMesh(int x, int y, int z, Flags flags, int quadCount, List<MeshQuad> quads, int pass) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ this.flags = flags;
+ this.quadCount = quadCount;
+ this.pass = pass;
+
+ NBTBase nbtData = toNBT(quads, quadCount);
+ buffer = createBuffer(((NBTTagByteArray)nbtData).func_150292_c(), nameList);
+ usedRAM += buffer.limit();
+ instances++;
+ }
+
+ private static int totalOriginalQuadCount = 0;
+ private static int totalSimplifiedQuadCount = 0;
+
+ public static ChunkMesh fromTessellator(int pass, WorldRenderer wr, Tessellator t) {
+ if(t.vertexCount % 4 != 0) {
+ System.out.println("Error: Vertex count is not a multiple of 4");
+ return null;
+ }
+
+ int xOffset = wr.posX;
+ int yOffset = wr.posY;
+ int zOffset = wr.posZ;
+
+ boolean fr = MixinConfigPlugin.isOptiFinePresent() && LODMod.ofFastRender;
+ int tessellatorXOffset = fr ? xOffset : 0;
+ int tessellatorYOffset = fr ? yOffset : 0;
+ int tessellatorZOffset = fr ? zOffset : 0;
+
+ boolean optimize = LODMod.optimizeChunkMeshes;
+
+ ChunkMesh.Flags flags = new ChunkMesh.Flags(t.hasTexture, t.hasBrightness, t.hasColor, t.hasNormals);
+
+ if(optimize) {
+ List<MeshQuad> quads = new ArrayList<>();
+
+ for(int quadI = 0; quadI < t.vertexCount / 4; quadI++) {
+ MeshQuad quad = new MeshQuad(t.rawBuffer, quadI * 32, flags, tessellatorXOffset, tessellatorYOffset, tessellatorZOffset);
+ //if(quad.bUs[0] == quad.bUs[1] && quad.bUs[1] == quad.bUs[2] && quad.bUs[2] == quad.bUs[3] && quad.bUs[3] == quad.bVs[0] && quad.bVs[0] == quad.bVs[1] && quad.bVs[1] == quad.bVs[2] && quad.bVs[2] == quad.bVs[3] && quad.bVs[3] == 0) {
+ // quad.deleted = true;
+ //}
+ if(quad.plane == quad.PLANE_XZ && !quad.isClockwiseXZ()) {
+ // water hack
+ quad.deleted = true;
+ }
+ quads.add(quad);
+ }
+
+ ArrayList<ArrayList<MeshQuad>> quadsByPlaneDir = new ArrayList<>(); // XY, XZ, YZ
+ for(int i = 0; i < 3; i++) {
+ quadsByPlaneDir.add(new ArrayList<MeshQuad>());
+ }
+ for(MeshQuad quad : quads) {
+ if(quad.plane != MeshQuad.PLANE_NONE) {
+ quadsByPlaneDir.get(quad.plane).add(quad);
+ }
+ }
+ for(int plane = 0; plane < 3; plane++) {
+ quadsByPlaneDir.get(plane).sort(MeshQuad.QuadPlaneComparator.quadPlaneComparators[plane]);
+ }
+
+ for(int plane = 0; plane < 3; plane++) {
+ List<MeshQuad> planeDirQuads = quadsByPlaneDir.get(plane);
+ int planeStart = 0;
+ for(int quadI = 0; quadI < planeDirQuads.size(); quadI++) {
+ MeshQuad quad = planeDirQuads.get(quadI);
+ MeshQuad nextQuad = quadI == planeDirQuads.size() - 1 ? null : planeDirQuads.get(quadI + 1);
+ if(!quad.onSamePlaneAs(nextQuad)) {
+ simplifyPlane(planeDirQuads.subList(planeStart, quadI));
+ planeStart = quadI + 1;
+ }
+ }
+ }
+
+ int quadCount = countValidQuads(quads);
+
+ totalOriginalQuadCount += quads.size();
+ totalSimplifiedQuadCount += quadCount;
+ //System.out.println("simplified quads " + totalOriginalQuadCount + " -> " + totalSimplifiedQuadCount + " (ratio: " + ((float)totalSimplifiedQuadCount / (float)totalOriginalQuadCount) + ") totalMergeCountByPlane: " + Arrays.toString(totalMergeCountByPlane));
+
+ if(quadCount > 0) {
+ return new ChunkMesh(
+ (int)(xOffset / 16), (int)(yOffset / 16), (int)(zOffset / 16),
+ new ChunkMesh.Flags(t.hasTexture, t.hasBrightness, t.hasColor, t.hasNormals),
+ quadCount, quads, pass);
+ } else {
+ return null;
+ }
+ } else {
+ int quadCount = t.vertexCount / 4;
+ ByteBuffer buffer = BufferUtils.createByteBuffer(quadCount * 6 * 7 * 4);
+ BufferWriter out = new BufferWriter(buffer);
+
+ try {
+ for(int i = 0; i < quadCount; i++) {
+ writeBufferQuad(t, i * 32, out, -tessellatorXOffset + xOffset, -tessellatorYOffset + yOffset, -tessellatorZOffset + zOffset);
+ }
+ } catch(IOException e) {
+ e.printStackTrace();
+ }
+ buffer.flip();
+
+ if(quadCount > 0) {
+ return new ChunkMesh(
+ (int)(xOffset / 16), (int)(yOffset / 16), (int)(zOffset / 16),
+ flags,
+ quadCount, buffer, pass);
+ } else {
+ return null;
+ }
+ }
+ }
+
+ private static void writeBufferQuad(Tessellator t, int offset, BufferWriter out, float offsetX, float offsetY, float offsetZ) throws IOException {
+ for(int vertexI = 0; vertexI < 6; vertexI++) {
+
+ int vi = new int[]{0, 1, 2, 0, 2, 3}[vertexI];
+
+ int i = offset + vi * 8;
+
+ float x = Float.intBitsToFloat(t.rawBuffer[i + 0]) + offsetX;
+ float y = Float.intBitsToFloat(t.rawBuffer[i + 1]) + offsetY;
+ float z = Float.intBitsToFloat(t.rawBuffer[i + 2]) + offsetZ;
+
+ out.writeFloat(x);
+ out.writeFloat(y);
+ out.writeFloat(z);
+
+ float u = Float.intBitsToFloat(t.rawBuffer[i + 3]);
+ float v = Float.intBitsToFloat(t.rawBuffer[i + 4]);
+
+ out.writeFloat(u);
+ out.writeFloat(v);
+
+ int brightness = t.rawBuffer[i + 7];
+ out.writeInt(brightness);
+
+ int color = t.rawBuffer[i + 5];
+ out.writeInt(color);
+
+ i += 8;
+ }
+ }
+
+ private static void simplifyPlane(List<MeshQuad> planeQuads) {
+ MeshQuad lastQuad = null;
+ // Pass 1: merge quads to create rows
+ for(MeshQuad quad : planeQuads) {
+ if(lastQuad != null) {
+ lastQuad.tryToMerge(quad);
+ }
+ if(quad.isValid(quad)) {
+ lastQuad = quad;
+ }
+ }
+
+ // Pass 2: merge rows to create rectangles
+ // TODO optimize?
+ for(int i = 0; i < planeQuads.size(); i++) {
+ for(int j = i + 1; j < planeQuads.size(); j++) {
+ planeQuads.get(i).tryToMerge(planeQuads.get(j));
+ }
+ }
+ }
+
+ private static int countValidQuads(List<MeshQuad> quads) {
+ int quadCount = 0;
+ for(MeshQuad quad : quads) {
+ if(!quad.deleted) {
+ quadCount++;
+ }
+ }
+ return quadCount;
+ }
+
+ private NBTBase toNBT(List<? extends MeshQuad> quads, int quadCount) {
+ ByteArrayOutputStream byteOut = new ByteArrayOutputStream(quadCount * (2 + 4 * (3 + 2 + 2 + 4)));
+ DataOutputStream out = new DataOutputStream(byteOut);
+ try {
+ for(int pass = 0; pass <= 9; pass++){
+ for(MeshQuad quad : quads) {
+ quad.writeToDisk(out, pass);
+ }
+ }
+ } catch(IOException e) {}
+
+ NBTTagByteArray arr = new NBTTagByteArray(byteOut.toByteArray());
+ usedRAM += arr.func_150292_c().length;
+ return arr;
+ }
+
+ void destroy() {
+ if(buffer != null) {
+ usedRAM -= buffer.limit();
+ instances--;
+ buffer = null;
+
+ if(gpuStatus == Mesh.GPUStatus.SENT) {
+ gpuStatus = Mesh.GPUStatus.PENDING_DELETE;
+ }
+ }
+ }
+
+ @Override
+ public void destroyBuffer() {
+ destroy();
+ }
+
+ private ByteBuffer createBuffer(byte[] data, List<String> stringTable) {
+ if(!(flags.hasTexture && flags.hasColor && flags.hasBrightness && !flags.hasNormals)) {
+ // for simplicity's sake we just assume this setup
+ System.out.println("invalid mesh properties, expected a chunk");
+ return null;
+ }
+ int coordsOffset = quadCount * 2;
+ int textureOffset = quadCount * (2 + 4 + 4 + 4);
+ int brightnessOffset = quadCount * (2 + 4 + 4 + 4 + 4 + 4);
+ int colorOffset = quadCount * (2 + 4 + 4 + 4 + 4 + 4 + 4 + 4);
+
+ ByteBuffer buffer = BufferUtils.createByteBuffer(quadCount * 6 * getStride());
+ FloatBuffer floatBuffer = buffer.asFloatBuffer();
+ ShortBuffer shortBuffer = buffer.asShortBuffer();
+ IntBuffer intBuffer = buffer.asIntBuffer();
+
+ try {
+ for(int quadI = 0; quadI < quadCount; quadI++) {
+ short spriteIndex = readShortAt(data, quadI * 2);
+ String spriteName = stringTable.get(spriteIndex);
+
+ TextureAtlasSprite tas = ((TextureMap)Minecraft.getMinecraft().getTextureManager().getTexture(TextureMap.locationBlocksTexture)).getAtlasSprite(spriteName);
+
+ for (int vertexNum = 0; vertexNum < 6; vertexNum++) {
+ int vi = new int[]{0, 1, 3, 1, 2, 3}[vertexNum];
+ int vertexI = 4 * quadI + vi;
+ int offset = vertexI * getStride();
+ int simpleX = Byte.toUnsignedInt(data[coordsOffset + 0 * 4 * quadCount + 4 * quadI + vi]);
+ if(simpleX == 255) simpleX = 256;
+ int simpleY = Byte.toUnsignedInt(data[coordsOffset + 1 * 4 * quadCount + 4 * quadI + vi]);
+ if(simpleY == 255) simpleY = 256;
+ int simpleZ = Byte.toUnsignedInt(data[coordsOffset + 2 * 4 * quadCount + 4 * quadI + vi]);
+ if(simpleZ == 255) simpleZ = 256;
+ floatBuffer.put(x * 16 + simpleX / 16f); // x
+ floatBuffer.put(y * 16 + simpleY / 16f); // y
+ floatBuffer.put(z * 16 + simpleZ / 16f); // z
+
+ byte relU = data[textureOffset + 0 * 4 * quadCount + 4 * quadI + vi];
+ byte relV = data[textureOffset + 1 * 4 * quadCount + 4 * quadI + vi];
+
+ floatBuffer.put(tas.getMinU() + (tas.getMaxU() - tas.getMinU()) * (relU / 16f)); // u
+ floatBuffer.put(tas.getMinV() + (tas.getMaxV() - tas.getMinV()) * (relV / 16f)); // v
+
+ shortBuffer.position(floatBuffer.position() * 2);
+
+ shortBuffer.put((short)Byte.toUnsignedInt(data[brightnessOffset + 0 * 4 * quadCount + 4 * quadI + vi])); // bU
+ shortBuffer.put((short)Byte.toUnsignedInt(data[brightnessOffset + 1 * 4 * quadCount + 4 * quadI + vi])); // bV
+
+ intBuffer.position(shortBuffer.position() / 2);
+
+ int integet = readIntAt(data, colorOffset + 4 * 4 * quadI + 4 * vi);
+ intBuffer.put(integet); // c
+
+ floatBuffer.position(intBuffer.position());
+ }
+ }
+ } catch(Exception e) {
+ e.printStackTrace();
+ }
+
+ buffer.position(floatBuffer.position() * 4);
+ buffer.flip();
+
+ usedRAM += buffer.limit();
+
+ return buffer;
+ }
+
+ public void update() {
+ }
+
+ // Java is weird.
+ public static short readShortAt(DataInputStream in, int offset) {
+ try {
+ in.reset();
+ in.skip(offset);
+ return in.readShort();
+ } catch(IOException e) {
+ return -1;
+ }
+ }
+
+ public static short readShortAt(byte[] data, int offset) {
+ return (short)(Byte.toUnsignedInt(data[offset]) << 8 | Byte.toUnsignedInt(data[offset + 1]));
+ }
+
+ public static int readIntAt(DataInputStream in, int offset) {
+ try {
+ in.reset();
+ in.skip(offset);
+ return in.readInt();
+ } catch(IOException e) {
+ return -1;
+ }
+ }
+
+ public static int readIntAt(byte[] data, int offset) {
+ return (int)(Byte.toUnsignedLong(data[offset]) << 24 | Byte.toUnsignedLong(data[offset + 1]) << 16 | Byte.toUnsignedLong(data[offset + 2]) << 8 | Byte.toUnsignedLong(data[offset + 3]));
+ }
+
+ public int getStride() {
+ return (3 * 4 + (flags.hasTexture ? 8 : 0) + (flags.hasBrightness ? 4 : 0) + (flags.hasColor ? 4 : 0) + (flags.hasNormals ? 4 : 0));
+ }
+
+ static void saveChunks(List<Integer> coords) {
+ System.out.println("saving " + (coords.size() / 3) + " cchunks");
+ for(int i = 0; i < coords.size(); i += 3) {
+ if(i % 300 == 0) {
+ System.out.println((i / 3) + " / " + (coords.size() / 3));
+ }
+ int theX = coords.get(i);
+ int theY = coords.get(i + 1);
+ int theZ = coords.get(i + 2);
+
+ WorldRenderer wr = new WorldRenderer(Minecraft.getMinecraft().theWorld, new ArrayList<TileEntity>(), theX * 16, theY * 16, theZ * 16, 100000);
+ /*
+ if (this.occlusionEnabled)
+ {
+ this.worldRenderers[(var6 * this.renderChunksTall + var5) * this.renderChunksWide + var4].glOcclusionQuery = this.glOcclusionQueryBase.get(var3);
+ }*/
+
+ wr.isWaitingOnOcclusionQuery = false;
+ wr.isVisible = true;
+ wr.isInFrustum = true;
+ wr.chunkIndex = 0;
+ wr.markDirty();
+ wr.updateRenderer(Minecraft.getMinecraft().thePlayer);
+ }
+ //Tessellator.endSave();
+ }
+
+ static List<ChunkMesh> getChunkMesh(int theX, int theY, int theZ) {
+ WorldRenderer wr = new WorldRenderer(Minecraft.getMinecraft().theWorld, new ArrayList<TileEntity>(), theX * 16, theY * 16, theZ * 16, 100000);
+
+ wr.isWaitingOnOcclusionQuery = false;
+ wr.isVisible = true;
+ wr.isInFrustum = true;
+ wr.chunkIndex = 0;
+ wr.markDirty();
+ wr.updateRenderer(Minecraft.getMinecraft().thePlayer);
+ return ((IWorldRenderer)wr).getChunkMeshes();
+ }
+
+ public double distSq(Entity player) {
+ int centerX = x * 16 + 8;
+ int centerY = y * 16 + 8;
+ int centerZ = z * 16 + 8;
+
+ return player.getDistanceSq(centerX, centerY, centerZ);
+ }
+
+ public static class Flags {
+ boolean hasTexture;
+ boolean hasBrightness;
+ boolean hasColor;
+ boolean hasNormals;
+
+ public Flags(byte flags) {
+ hasTexture = (flags & 1) != 0;
+ hasBrightness = (flags & 2) != 0;
+ hasColor = (flags & 4) != 0;
+ hasNormals = (flags & 8) != 0;
+ }
+
+ public Flags(boolean hasTexture, boolean hasBrightness, boolean hasColor, boolean hasNormals) {
+ this.hasTexture = hasTexture;
+ this.hasBrightness = hasBrightness;
+ this.hasColor = hasColor;
+ this.hasNormals = hasNormals;
+ }
+
+ public byte toByte() {
+ byte flags = 0;
+ if(hasTexture) {
+ flags |= 1;
+ }
+ if(hasBrightness) {
+ flags |= 2;
+ }
+ if(hasColor) {
+ flags |= 4;
+ }
+ if(hasNormals) {
+ flags |= 8;
+ }
+ return flags;
+ }
+ }
+
+}
+
diff --git a/src/main/java/makamys/neodymium/renderer/FarChunkCache.java b/src/main/java/makamys/neodymium/renderer/FarChunkCache.java
new file mode 100644
index 0000000..21dd5a6
--- /dev/null
+++ b/src/main/java/makamys/neodymium/renderer/FarChunkCache.java
@@ -0,0 +1,12 @@
+package makamys.neodymium.renderer;
+
+import net.minecraft.world.ChunkCache;
+import net.minecraft.world.World;
+
+public class FarChunkCache extends ChunkCache {
+
+ public FarChunkCache(World p1, int p2, int p3, int p4, int p5, int p6, int p7, int p8) {
+ super(p1, p2, p3, p4, p5, p6, p7, p8);
+ }
+
+}
diff --git a/src/main/java/makamys/neodymium/renderer/FarWorldRenderer.java b/src/main/java/makamys/neodymium/renderer/FarWorldRenderer.java
new file mode 100644
index 0000000..a15d8b9
--- /dev/null
+++ b/src/main/java/makamys/neodymium/renderer/FarWorldRenderer.java
@@ -0,0 +1,14 @@
+package makamys.neodymium.renderer;
+
+import java.util.List;
+
+import net.minecraft.client.renderer.WorldRenderer;
+import net.minecraft.world.World;
+
+public class FarWorldRenderer extends WorldRenderer {
+
+ public FarWorldRenderer(World p1, List p2, int p3, int p4, int p5, int p6) {
+ super(p1, p2, p3, p4, p5, p6);
+ }
+
+}
diff --git a/src/main/java/makamys/neodymium/renderer/GPUMemoryManager.java b/src/main/java/makamys/neodymium/renderer/GPUMemoryManager.java
new file mode 100644
index 0000000..75b1f64
--- /dev/null
+++ b/src/main/java/makamys/neodymium/renderer/GPUMemoryManager.java
@@ -0,0 +1,215 @@
+package makamys.neodymium.renderer;
+
+import static org.lwjgl.opengl.GL15.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+
+import makamys.neodymium.LODMod;
+import makamys.neodymium.renderer.Mesh.GPUStatus;
+import makamys.neodymium.util.GuiHelper;
+
+public class GPUMemoryManager {
+
+ private int bufferSize;
+
+ public int VBO;
+
+ private int nextMesh;
+
+ private List<Mesh> sentMeshes = new ArrayList<>();
+
+ public GPUMemoryManager() {
+ VBO = glGenBuffers();
+
+ bufferSize = LODMod.VRAMSize * 1024 * 1024;
+
+ glBindBuffer(GL_ARRAY_BUFFER, VBO);
+
+ glBufferData(GL_ARRAY_BUFFER, bufferSize, GL_DYNAMIC_DRAW);
+
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ }
+
+ public void runGC(boolean full) {
+ glBindBuffer(GL_ARRAY_BUFFER, VBO);
+
+ int moved = 0;
+ int timesReachedEnd = 0;
+ int checksLeft = sentMeshes.size();
+
+ while((!full && (moved < 4 && checksLeft-- > 0)) || (full && timesReachedEnd < 2) && !sentMeshes.isEmpty()) {
+ nextMesh++;
+ if(nextMesh >= sentMeshes.size()) {
+ nextMesh = 0;
+ timesReachedEnd++;
+ }
+ Mesh mesh = sentMeshes.get(nextMesh);
+
+ if(mesh.gpuStatus == GPUStatus.SENT) {
+ int offset = nextMesh == 0 ? 0 : sentMeshes.get(nextMesh - 1).getEnd();
+ if(mesh.offset != offset) {
+ glBufferSubData(GL_ARRAY_BUFFER, offset, mesh.buffer);
+ moved++;
+ }
+ mesh.iFirst = offset / mesh.getStride();
+ mesh.offset = offset;
+ } else if(mesh.gpuStatus == GPUStatus.PENDING_DELETE) {
+ mesh.iFirst = mesh.offset = -1;
+ mesh.visible = false;
+ mesh.gpuStatus = GPUStatus.UNSENT;
+
+ sentMeshes.remove(nextMesh);
+
+ mesh.destroyBuffer();
+
+ if(nextMesh > 0) {
+ nextMesh--;
+ }
+ }
+ }
+
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ }
+
+ private int malloc(int size) {
+ int nextBase = 0;
+ if(!sentMeshes.isEmpty()) {
+ if(nextMesh < sentMeshes.size() - 1) {
+ Mesh next = sentMeshes.get(nextMesh);
+ Mesh nextnext = sentMeshes.get(nextMesh + 1);
+ if(nextnext.offset - next.getEnd() >= size) {
+ return next.getEnd();
+ }
+ }
+
+ nextBase = sentMeshes.get(sentMeshes.size() - 1).getEnd();
+ }
+
+ if(nextBase + size >= bufferSize) {
+ return -1;
+ } else {
+ return nextBase;
+ }
+ }
+
+ private int end() {
+ return (sentMeshes.isEmpty() ? 0 : sentMeshes.get(sentMeshes.size() - 1).getEnd());
+ }
+
+ public void sendMeshToGPU(Mesh mesh) {
+ if(mesh == null || mesh.buffer == null) {
+ return;
+ }
+
+ if(end() + mesh.bufferSize() >= bufferSize) {
+ runGC(true);
+ }
+
+ if(end() + mesh.bufferSize() >= bufferSize) {
+ System.out.println("VRAM is full! Try increasing the allocated VRAM in the config, if possible. Reverting to vanilla renderer.");
+ LODMod.renderer.destroyPending = true;
+ // TODO restart renderer with more VRAM allocated when this happens.
+ return;
+ }
+
+ int size = mesh.bufferSize();
+ int insertIndex = -1;
+
+ int nextBase = -1;
+ if(!sentMeshes.isEmpty()) {
+ if(nextMesh < sentMeshes.size() - 1) {
+ Mesh next = sentMeshes.get(nextMesh);
+ Mesh nextnext = null;
+ for(int i = nextMesh + 1; i < sentMeshes.size(); i++) {
+ Mesh m = sentMeshes.get(i);
+ if(m.gpuStatus == Mesh.GPUStatus.SENT) {
+ nextnext = m;
+ break;
+ }
+ }
+ if(nextnext != null && nextnext.offset - next.getEnd() >= size) {
+ nextBase = next.getEnd();
+ insertIndex = nextMesh + 1;
+ }
+ }
+
+ if(nextBase == -1) {
+ nextBase = sentMeshes.get(sentMeshes.size() - 1).getEnd();
+ }
+ }
+ if(nextBase == -1) nextBase = 0;
+
+
+ if(mesh.gpuStatus == GPUStatus.UNSENT) {
+ mesh.prepareBuffer();
+
+ glBindBuffer(GL_ARRAY_BUFFER, VBO);
+
+ glBufferSubData(GL_ARRAY_BUFFER, nextBase, mesh.buffer);
+ mesh.iFirst = nextBase / mesh.getStride();
+ mesh.iCount = mesh.quadCount * 6;
+ mesh.offset = nextBase;
+
+ if(insertIndex == -1) {
+ sentMeshes.add(mesh);
+ } else {
+ sentMeshes.add(insertIndex, mesh);
+ nextMesh = insertIndex;
+ }
+
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ }
+
+ mesh.gpuStatus = GPUStatus.SENT;
+ }
+
+ public void deleteMeshFromGPU(Mesh mesh) {
+ if(mesh == null || mesh.gpuStatus == GPUStatus.UNSENT) {
+ return;
+ }
+ mesh.gpuStatus = GPUStatus.PENDING_DELETE;
+ }
+
+ public void destroy() {
+ glDeleteBuffers(VBO);
+ }
+
+ public List<String> getDebugText() {
+ return Arrays.asList("VRAM: " + (end() / 1024 / 1024) + "MB / " + (bufferSize / 1024 / 1024) + "MB");
+ }
+
+ public void drawInfo() {
+ int scale = 10000;
+ int rowLength = 512;
+ int yOff = 20;
+
+ int height = (bufferSize / scale) / rowLength;
+ GuiHelper.drawRectangle(0, yOff, rowLength, height, 0x000000, 50);
+
+ int meshI = 0;
+ for(Mesh mesh : sentMeshes) {
+
+ int o = mesh.offset / 10000;
+ int o2 = (mesh.offset + mesh.bufferSize()) / 10000;
+ if(o / rowLength == o2 / rowLength) {
+ if(mesh.gpuStatus != Mesh.GPUStatus.PENDING_DELETE) {
+ GuiHelper.drawRectangle(o % rowLength, o / rowLength + yOff, mesh.buffer.limit() / scale + 1, 1, meshI == nextMesh ? 0x00FF00 : 0xFFFFFF);
+ }
+ } else {
+ for(int i = o; i < o2; i++) {
+ int x = i % rowLength;
+ int y = i / rowLength;
+ if(mesh.gpuStatus != Mesh.GPUStatus.PENDING_DELETE) {
+ GuiHelper.drawRectangle(x, y + yOff, 1, 1, 0xFFFFFF);
+ }
+ }
+ }
+ meshI++;
+ }
+ GuiHelper.drawRectangle(0 % rowLength, 0 + yOff, 4, 4, 0x00FF00);
+ GuiHelper.drawRectangle((bufferSize / scale) % rowLength, (bufferSize / scale) / rowLength + yOff, 4, 4, 0xFF0000);
+ }
+
+}
diff --git a/src/main/java/makamys/neodymium/renderer/LODChunk.java b/src/main/java/makamys/neodymium/renderer/LODChunk.java
new file mode 100644
index 0000000..5dd3762
--- /dev/null
+++ b/src/main/java/makamys/neodymium/renderer/LODChunk.java
@@ -0,0 +1,184 @@
+package makamys.neodymium.renderer;
+
+import java.util.List;
+
+import makamys.neodymium.LODMod;
+import net.minecraft.entity.Entity;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.nbt.NBTTagEnd;
+import net.minecraft.nbt.NBTTagList;
+import net.minecraft.world.chunk.Chunk;
+
+public class LODChunk {
+
+ int x, z;
+ public boolean needsChunk = true;
+ int lod = 0;
+ boolean visible;
+ boolean dirty;
+ boolean discardedMesh;
+
+ SimpleChunkMesh[] simpleMeshes = new SimpleChunkMesh[2];
+ ChunkMesh[] chunkMeshes = new ChunkMesh[32];
+
+ public boolean[] isSectionVisible = new boolean[16];
+
+ LODRenderer renderer = LODMod.renderer;
+
+ public LODChunk(int x, int z) {
+ this.x = x;
+ this.z = z;
+ }
+ /*
+ public LODChunk(NBTTagCompound nbt, List<String> spriteList) {
+ this.x = nbt.getInteger("x");
+ this.z = nbt.getInteger("z");
+
+ loadChunkMeshesNBT(nbt.getCompoundTag("chunkMeshes"), spriteList);
+ }
+
+ private void loadChunkMeshesNBT(NBTTagCompound chunkMeshesCompound, List<String> spriteList) {
+ for(Object o : chunkMeshesCompound.func_150296_c()) {
+ String key = (String)o;
+ int keyInt = Integer.parseInt(key);
+
+ byte[] data = chunkMeshesCompound.getByteArray(key);
+
+ chunkMeshes[keyInt] = new ChunkMesh(x, keyInt / 2, z, new ChunkMesh.Flags(true, true, true, false), data.length / (2 + 4 * (3 + 2 + 2 + 4)), data, spriteList, keyInt % 2);
+ }
+ }
+ */
+ @Override
+ public String toString() {
+ return "LODChunk(" + x + ", " + z + ")";
+ }
+
+ public double distSq(Entity entity) {
+ return Math.pow(entity.posX - x * 16, 2) + Math.pow(entity.posZ - z * 16, 2);
+ }
+
+ public void putChunkMeshes(int cy, List<ChunkMesh> newChunkMeshes) {
+ for(int i = 0; i < 2; i++) {
+ ChunkMesh newChunkMesh = newChunkMeshes.size() > i ? newChunkMeshes.get(i) : null;
+ if(chunkMeshes[cy * 2 + i] != null) {
+ if(newChunkMesh != null) {
+ newChunkMesh.pass = i;
+ }
+
+ renderer.removeMesh(chunkMeshes[cy * 2 + i]);
+ chunkMeshes[cy * 2 + i].destroy();
+ }
+ chunkMeshes[cy * 2 + i] = newChunkMesh;
+ }
+ LODMod.renderer.lodChunkChanged(this);
+ dirty = true;
+ discardedMesh = false;
+ }
+
+ // nice copypasta
+ public void putSimpleMeshes(List<SimpleChunkMesh> newSimpleMeshes) {
+ for(int i = 0; i < 2; i++) {
+ SimpleChunkMesh newSimpleMesh = newSimpleMeshes.size() > i ? newSimpleMeshes.get(i) : null;
+ if(simpleMeshes[i] != null) {
+ if(newSimpleMesh != null) {
+ newSimpleMesh.pass = i;
+ }
+
+ renderer.setMeshVisible(simpleMeshes[i], false);
+ simpleMeshes[i].destroy();
+ }
+ simpleMeshes[i] = newSimpleMesh;
+ }
+ LODMod.renderer.lodChunkChanged(this);
+ }
+
+ public boolean hasChunkMeshes() {
+ for(ChunkMesh cm : chunkMeshes) {
+ if(cm != null) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public void tick(Entity player) {
+ double distSq = distSq(player);
+ if(LODMod.disableSimpleMeshes || distSq < Math.pow((LODMod.renderer.renderRange / 2) * 16, 2)) {
+ setLOD(2);
+ } else if(distSq < Math.pow((LODMod.renderer.renderRange) * 16, 2)) {
+ setLOD(1);
+ } else {
+ setLOD(0);
+ }
+ }
+
+ public void setLOD(int lod) {
+ if(lod == this.lod) return;
+
+ this.lod = lod;
+ LODMod.renderer.lodChunkChanged(this);
+ if(!dirty) {
+ if(lod < 2) {
+ for(int i = 0; i < chunkMeshes.length; i++) {
+ if(chunkMeshes[i] != null) {
+ chunkMeshes[i].destroy();
+ chunkMeshes[i] = null;
+ discardedMesh = true;
+ }
+ }
+ }
+ }
+ }
+ /*
+ public NBTTagCompound saveToNBT(NBTTagCompound oldNbt, List<String> oldStringTable) {
+ NBTTagCompound nbt = new NBTTagCompound();
+ nbt.setInteger("x", x);
+ nbt.setInteger("z", z);
+
+ NBTTagCompound chunkMeshesCompound = oldNbt == null ? new NBTTagCompound() : oldNbt.getCompoundTag("chunkMeshes");
+ if(!discardedMesh) {
+ for(int i = 0; i < chunkMeshes.length; i++) {
+ if(chunkMeshes[i] != null) {
+ chunkMeshesCompound.setTag(String.valueOf(i), chunkMeshes[i].nbtData);
+ }
+ }
+ } else if(oldNbt != null && discardedMesh && lod == 2) {
+ loadChunkMeshesNBT(chunkMeshesCompound, oldStringTable);
+ LODMod.renderer.lodChunkChanged(this);
+ }
+ nbt.setTag("chunkMeshes", chunkMeshesCompound);
+ dirty = false;
+ return nbt;
+ }
+ */
+ public void destroy() {
+ for(SimpleChunkMesh scm: simpleMeshes) {
+ if(scm != null) {
+ scm.destroy();
+ }
+ }
+ for(ChunkMesh cm: chunkMeshes) {
+ if(cm != null) {
+ cm.destroy();
+ }
+ }
+ LODMod.renderer.setVisible(this, false);
+ }
+
+ public void receiveChunk(Chunk chunk) {
+ if(!LODMod.disableSimpleMeshes) {
+ putSimpleMeshes(SimpleChunkMesh.generateSimpleMeshes(chunk));
+ }
+ }
+
+ public boolean isFullyVisible() {
+ if(!visible) return false;
+ for(boolean b : isSectionVisible) {
+ if(!b) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+}
diff --git a/src/main/java/makamys/neodymium/renderer/LODRegion.java b/src/main/java/makamys/neodymium/renderer/LODRegion.java
new file mode 100644
index 0000000..22316f7
--- /dev/null
+++ b/src/main/java/makamys/neodymium/renderer/LODRegion.java
@@ -0,0 +1,195 @@
+package makamys.neodymium.renderer;
+
+import java.io.BufferedOutputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import makamys.neodymium.LODMod;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.texture.TextureMap;
+import net.minecraft.entity.Entity;
+import net.minecraft.nbt.CompressedStreamTools;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.nbt.NBTTagList;
+import net.minecraft.world.chunk.Chunk;
+import net.minecraftforge.common.util.Constants.NBT;
+
+public class LODRegion {
+
+ private LODChunk[][] data = new LODChunk[32][32];
+
+ int regionX, regionZ;
+
+ public LODRegion(int regionX, int regionZ) {
+ this.regionX = regionX;
+ this.regionZ = regionZ;
+
+ for(int i = 0; i < 32; i++) {
+ for(int j = 0; j < 32; j++) {
+ data[i][j] = new LODChunk(regionX * 32 + i, regionZ * 32 + j);
+ }
+ }
+ }
+ /*
+ public LODRegion(int regionX, int regionZ, NBTTagCompound nbt) {
+ this.regionX = regionX;
+ this.regionZ = regionZ;
+
+ NBTTagList list = nbt.getTagList("chunks", NBT.TAG_COMPOUND);
+ List<String> stringTable = Arrays.asList(nbt.getString("stringTable").split("\\n"));
+
+ int idx = 0;
+ for(int i = 0; i < 32; i++) {
+ for(int j = 0; j < 32; j++) {
+ data[i][j] = new LODChunk(list.getCompoundTagAt(idx++), stringTable);
+ if(data[i][j].hasChunkMeshes()) {
+ LODMod.renderer.setVisible(data[i][j], true);
+ }
+ }
+ }
+ }
+ */
+ public static LODRegion load(Path saveDir, int regionX, int regionZ) {
+ /*if(!(LODMod.disableChunkMeshes || !LODMod.saveMeshes)) {
+ File saveFile = getSavePath(saveDir, regionX, regionZ).toFile();
+ if(saveFile.exists()) {
+ try {
+ NBTTagCompound nbt = CompressedStreamTools.readCompressed(new FileInputStream(saveFile));
+ return new LODRegion(regionX, regionZ, nbt);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }*/
+ return new LODRegion(regionX, regionZ);
+ }
+ /*
+ private static Path getSavePath(Path saveDir, int regionX, int regionZ) {
+ return saveDir.resolve("lod").resolve(regionX + "," + regionZ + ".lod");
+ }
+
+ public void save(Path saveDir) {
+ if(LODMod.disableChunkMeshes || !LODMod.saveMeshes) return;
+
+ try {
+ File saveFile = getSavePath(saveDir, regionX, regionZ).toFile();
+ saveFile.getParentFile().mkdirs();
+
+ NBTTagCompound oldNbt = null;
+ NBTTagList oldList = null;
+ List<String> oldStringTable = null;
+ if(saveFile.exists()) {
+ oldNbt = CompressedStreamTools.readCompressed(new FileInputStream(saveFile));
+ oldList = oldNbt.getTagList("chunks", NBT.TAG_COMPOUND);;
+ oldStringTable = Arrays.asList(oldNbt.getString("stringTable").split("\\n"));
+ }
+
+ NBTTagCompound nbt = new NBTTagCompound();
+ nbt.setByte("V", (byte)0);
+ nbt.setString("stringTable", String.join("\n", (List<String>) ((TextureMap)Minecraft.getMinecraft().getTextureManager().getTexture(TextureMap.locationBlocksTexture)).mapUploadedSprites.keySet().stream().collect(Collectors.toList())));
+
+ NBTTagList list = new NBTTagList();
+
+ int idx = 0;
+ for(int i = 0; i < 32; i++) {
+ for(int j = 0; j < 32; j++) {
+ list.appendTag(data[i][j].saveToNBT(oldNbt == null ? null : oldList.getCompoundTagAt(idx++),
+ oldNbt == null? null : oldStringTable));
+ }
+ }
+ nbt.setTag("chunks", list);
+
+ new Thread(
+ new Runnable() {
+
+ @Override
+ public void run() {
+ try {
+ CompressedStreamTools.writeCompressed(nbt, new FileOutputStream(saveFile));
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }).start();
+
+ } catch (Exception e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+ */
+ public LODChunk getChunkAbsolute(int chunkXAbs, int chunkZAbs) {
+ return getChunk(chunkXAbs - regionX * 32, chunkZAbs - regionZ * 32);
+ }
+
+ public LODChunk getChunk(int x, int z) {
+ if(x >= 0 && x < 32 && z >= 0 && z < 32) {
+ return data[x][z];
+ } else {
+ return null;
+ }
+ }
+
+ public LODChunk putChunk(Chunk chunk) {
+ int relX = chunk.xPosition - regionX * 32;
+ int relZ = chunk.zPosition - regionZ * 32;
+
+ if(relX >= 0 && relX < 32 && relZ >= 0 && relZ < 32) {
+ data[relX][relZ].receiveChunk(chunk);
+ return data[relX][relZ];
+ }
+ return null;
+ }
+
+ public boolean tick(Entity player) {
+ int visibleChunks = 0;
+ for(int i = 0; i < 32; i++) {
+ for(int j = 0; j < 32; j++) {
+ LODChunk chunk = data[i][j];
+ if(chunk != null) {
+ chunk.tick(player);
+ if(chunk.visible) {
+ visibleChunks++;
+ }
+ }
+ }
+ }
+ return visibleChunks > 0;
+ }
+
+ public void destroy(Path saveDir) {
+ //save(saveDir);
+ for(int i = 0; i < 32; i++) {
+ for(int j = 0; j < 32; j++) {
+ LODChunk chunk = data[i][j];
+ if(chunk != null) {
+ chunk.destroy();
+ }
+ }
+ }
+ }
+
+ public double distanceTaxicab(Entity entity) {
+ double centerX = ((regionX * 32) + 16) * 16;
+ double centerZ = ((regionZ * 32) + 16) * 16;
+
+ return Math.max(Math.abs(centerX - entity.posX), Math.abs(centerZ - entity.posZ));
+
+ }
+
+ @Override
+ public String toString() {
+ return "LODRegion(" + regionX + ", " + regionZ + ")";
+ }
+
+}
diff --git a/src/main/java/makamys/neodymium/renderer/LODRenderer.java b/src/main/java/makamys/neodymium/renderer/LODRenderer.java
new file mode 100644
index 0000000..974c408
--- /dev/null
+++ b/src/main/java/makamys/neodymium/renderer/LODRenderer.java
@@ -0,0 +1,730 @@
+package makamys.neodymium.renderer;
+
+import net.minecraft.block.Block;
+import net.minecraft.block.material.Material;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.particle.EntityFX;
+import net.minecraft.client.renderer.WorldRenderer;
+import net.minecraft.client.renderer.texture.TextureAtlasSprite;
+import net.minecraft.client.renderer.texture.TextureMap;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.EntityLivingBase;
+import net.minecraft.util.EnumFacing;
+import net.minecraft.world.ChunkCoordIntPair;
+import net.minecraft.world.IBlockAccess;
+import net.minecraft.world.World;
+import net.minecraft.world.biome.BiomeGenBase;
+import net.minecraft.world.chunk.Chunk;
+import net.minecraft.world.gen.ChunkProviderServer;
+import net.minecraftforge.event.world.ChunkEvent;
+
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.lwjgl.BufferUtils;
+import org.lwjgl.input.Keyboard;
+import org.lwjgl.opengl.GL11;
+import org.lwjgl.util.vector.Matrix4f;
+
+import makamys.neodymium.LODMod;
+import makamys.neodymium.ducks.IWorldRenderer;
+import makamys.neodymium.renderer.Mesh.GPUStatus;
+import makamys.neodymium.util.GuiHelper;
+import makamys.neodymium.util.Util;
+
+import static org.lwjgl.opengl.GL11.*;
+import static org.lwjgl.opengl.GL14.*;
+import static org.lwjgl.opengl.GL15.*;
+import static org.lwjgl.opengl.GL20.*;
+import static org.lwjgl.opengl.GL30.*;
+
+public class LODRenderer {
+
+ public boolean hasInited = false;
+ public boolean destroyPending;
+
+ private boolean[] wasDown = new boolean[256];
+ private int renderQuads = 0;
+
+ public boolean renderWorld;
+ public boolean rendererActive;
+ private boolean showMemoryDebugger;
+
+ private static int MAX_MESHES = 100000;
+
+ private int VAO, shaderProgram;
+ private IntBuffer[] piFirst = new IntBuffer[2];
+ private IntBuffer[] piCount = new IntBuffer[2];
+ private List<Mesh>[] sentMeshes = (List<Mesh>[])new ArrayList[] {new ArrayList<Mesh>(), new ArrayList<Mesh>()};
+ GPUMemoryManager mem;
+
+ List<Chunk> myChunks = new ArrayList<Chunk>();
+ List<LODChunk> pendingLODChunks = new ArrayList<>();
+
+ private boolean hasServerInited = false;
+ private Map<ChunkCoordIntPair, LODRegion> loadedRegionsMap = new HashMap<>();
+
+ public World world;
+
+ // TODO make these packets to make this work on dedicated servers
+ Queue<Chunk> farChunks = new ConcurrentLinkedQueue<>();
+
+ List<ChunkCoordIntPair> serverChunkLoadQueue = new ArrayList<>();
+
+ private double lastSortX = Double.NaN;
+ private double lastSortY = Double.NaN;
+ private double lastSortZ = Double.NaN;
+
+ private long lastGCTime = -1;
+ private long lastSaveTime = -1;
+ private long gcInterval = 10 * 1000;
+ private long saveInterval = 60 * 1000;
+
+ private int renderedMeshes;
+ private int frameCount;
+
+ public int renderRange = 48;
+
+ private boolean freezeMeshes;
+
+ public LODRenderer(World world){
+ this.world = world;
+ if(shouldRenderInWorld(world)) {
+ hasInited = init();
+ }
+
+ renderWorld = true;
+ rendererActive = true;
+ }
+
+ public void preRenderSortedRenderers(int renderPass, double alpha, WorldRenderer[] sortedWorldRenderers) {
+ if(renderPass != 0) return;
+
+ LODMod.fogEventWasPosted = false;
+
+ renderedMeshes = 0;
+
+ Minecraft.getMinecraft().entityRenderer.enableLightmap((double)alpha);
+
+ if(hasInited) {
+ mainLoop();
+ if(Minecraft.getMinecraft().currentScreen == null) {
+ handleKeyboard();
+ }
+ if(frameCount % 2 == 0) {
+ mem.runGC(false);
+ }
+ lastGCTime = System.currentTimeMillis();
+ if(lastSaveTime == -1 || (System.currentTimeMillis() - lastSaveTime) > saveInterval && LODMod.saveMeshes) {
+ onSave();
+ lastSaveTime = System.currentTimeMillis();
+ }
+
+ if(rendererActive && renderWorld) {
+ if(frameCount % LODMod.sortFrequency == 0) {
+ sort();
+ }
+
+ updateMeshes();
+ initIndexBuffers();
+ render(alpha);
+ }
+ }
+
+ frameCount++;
+
+ Minecraft.getMinecraft().entityRenderer.disableLightmap((double)alpha);
+ }
+
+ public void onRenderTickEnd() {
+ if(destroyPending) {
+ LODMod.renderer = null;
+ return;
+ }
+ if(showMemoryDebugger && mem != null) {
+ GuiHelper.begin();
+ mem.drawInfo();
+ GuiHelper.end();
+ }
+ }
+
+ private void sort() {
+ Entity player = Minecraft.getMinecraft().renderViewEntity;
+ for(List<Mesh> list : sentMeshes) {
+ list.sort(new MeshDistanceComparator(player.posX / 16, player.posY / 16, player.posZ / 16));
+ }
+ }
+
+ private void updateMeshes() {
+ for(List<Mesh> list : sentMeshes) {
+ for(Mesh mesh : list) {
+ mesh.update();
+ }
+ }
+ }
+
+ private void initIndexBuffers() {
+ for(int i = 0; i < 2; i++) {
+ piFirst[i].limit(sentMeshes[i].size());
+ piCount[i].limit(sentMeshes[i].size());
+ for(Mesh mesh : sentMeshes[i]) {
+ if(mesh.visible && (LODMod.maxMeshesPerFrame == -1 || renderedMeshes < LODMod.maxMeshesPerFrame)) {
+ renderedMeshes++;
+ piFirst[i].put(mesh.iFirst);
+ piCount[i].put(mesh.iCount);
+ }
+ }
+ piFirst[i].flip();
+ piCount[i].flip();
+ }
+ }
+
+ private void mainLoop() {
+ while(!farChunks.isEmpty()) {
+ LODChunk lodChunk = receiveFarChunk(farChunks.remove());
+ sendChunkToGPU(lodChunk);
+ }
+
+ if(Minecraft.getMinecraft().playerController.netClientHandler.doneLoadingTerrain) {
+ Entity player = Minecraft.getMinecraft().renderViewEntity;
+
+ List<ChunkCoordIntPair> newServerChunkLoadQueue = new ArrayList<>();
+
+ if(Double.isNaN(lastSortX) || getLastSortDistanceSq(player) > 16 * 16) {
+ int centerX = (int)Math.floor(player.posX / 16.0);
+ int centerZ = (int)Math.floor(player.posZ / 16.0);
+
+ for(int x = -renderRange; x <= renderRange; x++) {
+ for(int z = -renderRange; z <= renderRange; z++) {
+ if(x * x + z * z < renderRange * renderRange) {
+ int chunkX = centerX + x;
+ int chunkZ = centerZ + z;
+
+ if(getLODChunk(chunkX, chunkZ).needsChunk) {
+ newServerChunkLoadQueue.add(new ChunkCoordIntPair(chunkX, chunkZ));
+ getLODChunk(chunkX, chunkZ).needsChunk = false;
+ }
+ }
+ }
+ }
+ Collections.sort(newServerChunkLoadQueue, new ChunkCoordDistanceComparator(player.posX, player.posY, player.posZ));
+ addToServerChunkLoadQueue(newServerChunkLoadQueue);
+
+ lastSortX = player.posX;
+ lastSortY = player.posY;
+ lastSortZ = player.posZ;
+ for(Iterator<ChunkCoordIntPair> it = loadedRegionsMap.keySet().iterator(); it.hasNext();) {
+ ChunkCoordIntPair k = it.next();
+ LODRegion v = loadedRegionsMap.get(k);
+
+ if(v.distanceTaxicab(player) > renderRange * 16 + 16 * 16) {
+ System.out.println("unloading " + v);
+ v.destroy(getSaveDir());
+ it.remove();
+ } else {
+ v.tick(player);
+ }
+ }
+ }
+ }
+ }
+
+ public float getFarPlaneDistanceMultiplier() {
+ return (float)LODMod.farPlaneDistanceMultiplier;
+ }
+
+ public void afterSetupFog(int mode, float alpha, float farPlaneDistance) {
+ EntityLivingBase entity = Minecraft.getMinecraft().renderViewEntity;
+ if(LODMod.fogEventWasPosted && !Minecraft.getMinecraft().theWorld.provider.doesXZShowFog((int)entity.posX, (int)entity.posZ)) {
+ GL11.glFogf(GL11.GL_FOG_START, mode < 0 ? 0 : farPlaneDistance * (float)LODMod.fogStart);
+ GL11.glFogf(GL11.GL_FOG_END, mode < 0 ? farPlaneDistance/4 : farPlaneDistance * (float)LODMod.fogEnd);
+ }
+ }
+
+ private void handleKeyboard() {
+ if(LODMod.debugPrefix == 0 || (LODMod.debugPrefix != -1 && Keyboard.isKeyDown(LODMod.debugPrefix))) {
+ if(Keyboard.isKeyDown(Keyboard.KEY_F) && !wasDown[Keyboard.KEY_F]) {
+ rendererActive = !rendererActive;
+ }
+ if(Keyboard.isKeyDown(Keyboard.KEY_V) && !wasDown[Keyboard.KEY_V]) {
+ renderWorld = !renderWorld;
+ }
+ if(Keyboard.isKeyDown(Keyboard.KEY_R) && !wasDown[Keyboard.KEY_R]) {
+ loadShader();
+ }
+ if(Keyboard.isKeyDown(Keyboard.KEY_M) && !wasDown[Keyboard.KEY_M]) {
+ showMemoryDebugger = !showMemoryDebugger;
+ //LODChunk chunk = getLODChunk(9, -18);
+ //setMeshVisible(chunk.chunkMeshes[7], false, true);
+ //freezeMeshes = false;
+ //chunk.chunkMeshes[7].quadCount = 256;
+ //setMeshVisible(chunk.chunkMeshes[7], true, true);
+ }
+ }
+ for(int i = 0; i < 256; i++) {
+ wasDown[i] = Keyboard.isKeyDown(i);
+ }
+ }
+
+ FloatBuffer modelView = BufferUtils.createFloatBuffer(16);
+ FloatBuffer projBuf = BufferUtils.createFloatBuffer(16);
+ IntBuffer viewportBuf = BufferUtils.createIntBuffer(16);
+ FloatBuffer projInvBuf = BufferUtils.createFloatBuffer(16);
+ FloatBuffer fogColorBuf = BufferUtils.createFloatBuffer(16);
+ FloatBuffer fogStartEnd = BufferUtils.createFloatBuffer(2);
+ Matrix4f projMatrix = new Matrix4f();
+
+ private void render(double alpha) {
+ GL11.glPushAttrib(GL11.GL_ENABLE_BIT);
+ GL11.glDisable(GL11.GL_TEXTURE_2D);
+
+ glUseProgram(shaderProgram);
+
+ int u_modelView = glGetUniformLocation(shaderProgram, "modelView");
+ int u_proj = glGetUniformLocation(shaderProgram, "proj");
+ int u_playerPos = glGetUniformLocation(shaderProgram, "playerPos");
+ int u_light = glGetUniformLocation(shaderProgram, "lightTex");
+ int u_viewport = glGetUniformLocation(shaderProgram, "viewport");
+ int u_projInv = glGetUniformLocation(shaderProgram, "projInv");
+ int u_fogColor = glGetUniformLocation(shaderProgram, "fogColor");
+ int u_fogStartEnd = glGetUniformLocation(shaderProgram, "fogStartEnd");
+
+ if(false && (u_modelView == -1 || u_proj == -1 || u_playerPos == -1 || u_light == -1 || u_viewport == -1 || u_projInv == -1 || u_fogColor == -1 || u_fogStartEnd == -1)) {
+ System.out.println("failed to get the uniform");
+ } else {
+ glGetFloat(GL_MODELVIEW_MATRIX, modelView);
+
+ glGetFloat(GL_PROJECTION_MATRIX, projBuf);
+
+ glGetInteger(GL_VIEWPORT, viewportBuf);
+
+ projMatrix.load(projBuf);
+ projBuf.flip();
+ projMatrix.invert();
+ projMatrix.store(projInvBuf);
+ projInvBuf.flip();
+
+ fogColorBuf.limit(16);
+ glGetFloat(GL_FOG_COLOR, fogColorBuf);
+ fogColorBuf.limit(4);
+
+ fogStartEnd.put(glGetFloat(GL_FOG_START));
+ fogStartEnd.put(glGetFloat(GL_FOG_END));
+ fogStartEnd.flip();
+
+ glUniformMatrix4(u_modelView, false, modelView);
+ glUniformMatrix4(u_proj, false, projBuf);
+ glUniformMatrix4(u_projInv, false, projInvBuf);
+ glUniform4f(u_viewport, viewportBuf.get(0),viewportBuf.get(1),viewportBuf.get(2),viewportBuf.get(3));
+ glUniform4(u_fogColor, fogColorBuf);
+ glUniform2(u_fogStartEnd, fogStartEnd);
+
+ float originX = 0;
+ float originY = 0;
+ float originZ = 0;
+
+ Entity rve = Minecraft.getMinecraft().renderViewEntity;
+ double interpX = rve.lastTickPosX + (rve.posX - rve.lastTickPosX) * alpha;
+ double interpY = rve.lastTickPosY + (rve.posY - rve.lastTickPosY) * alpha + rve.getEyeHeight();
+ double interpZ = rve.lastTickPosZ + (rve.posZ - rve.lastTickPosZ) * alpha;
+
+ glUniform3f(u_playerPos, (float)interpX - originX, (float)interpY - originY, (float)interpZ - originZ);
+
+ glUniform1i(u_light, 1);
+
+ modelView.position(0);
+ projBuf.position(0);
+ viewportBuf.position(0);
+ projInvBuf.position(0);
+ fogColorBuf.position(0);
+ fogStartEnd.position(0);
+ }
+
+ glBindVertexArray(VAO);
+ GL11.glDisable(GL11.GL_BLEND);
+ glMultiDrawArrays(GL_TRIANGLES, piFirst[0], piCount[0]);
+ GL11.glEnable(GL11.GL_BLEND);
+ GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
+ glMultiDrawArrays(GL_TRIANGLES, piFirst[1], piCount[1]);
+
+ glBindVertexArray(0);
+ glUseProgram(0);
+
+ GL11.glDepthMask(true);
+ GL11.glPopAttrib();
+
+
+ }
+
+ public boolean init() {
+ Map<String, TextureAtlasSprite> uploadedSprites = ((TextureMap)Minecraft.getMinecraft().getTextureManager().getTexture(TextureMap.locationBlocksTexture)).mapUploadedSprites;
+
+ loadShader();
+
+ VAO = glGenVertexArrays();
+ glBindVertexArray(VAO);
+
+ mem = new GPUMemoryManager();
+
+ glBindBuffer(GL_ARRAY_BUFFER, mem.VBO);
+
+ int stride = 7 * 4;
+
+ glVertexAttribPointer(0, 3, GL_FLOAT, false, stride, 0);
+ glVertexAttribPointer(1, 2, GL_FLOAT, false, stride, 3 * 4);
+ glVertexAttribPointer(2, 2, GL_SHORT, false, stride, 5 * 4);
+ glVertexAttribPointer(3, 4, GL_UNSIGNED_BYTE, false, stride, 6 * 4);
+
+ glEnableVertexAttribArray(0);
+ glEnableVertexAttribArray(1);
+ glEnableVertexAttribArray(2);
+ glEnableVertexAttribArray(3);
+
+ for(int i = 0; i < 2; i++) {
+ piFirst[i] = BufferUtils.createIntBuffer(MAX_MESHES);
+ piFirst[i].flip();
+ piCount[i] = BufferUtils.createIntBuffer(MAX_MESHES);
+ piCount[i].flip();
+ }
+
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glBindVertexArray(0);
+
+ return true;
+ }
+
+ private void loadShader() {
+ int vertexShader;
+ vertexShader = glCreateShader(GL_VERTEX_SHADER);
+
+ glShaderSource(vertexShader, Util.readFile("shaders/chunk.vert"));
+ glCompileShader(vertexShader);
+
+ if(glGetShaderi(vertexShader, GL_COMPILE_STATUS) == 0) {
+ System.out.println("Error compiling vertex shader: " + glGetShaderInfoLog(vertexShader, 256));
+ }
+
+ int fragmentShader;
+ fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
+
+ glShaderSource(fragmentShader, Util.readFile(LODMod.enableFog ? "shaders/chunk_fog.frag" : "shaders/chunk.frag"));
+ glCompileShader(fragmentShader);
+
+ if(glGetShaderi(fragmentShader, GL_COMPILE_STATUS) == 0) {
+ System.out.println("Error compiling fragment shader: " + glGetShaderInfoLog(fragmentShader, 256));
+ }
+
+ shaderProgram = glCreateProgram();
+ glAttachShader(shaderProgram, vertexShader);
+ glAttachShader(shaderProgram, fragmentShader);
+ glLinkProgram(shaderProgram);
+
+ if(glGetProgrami(shaderProgram, GL_LINK_STATUS) == 0) {
+ System.out.println("Error linking shader: " + glGetShaderInfoLog(shaderProgram, 256));
+ }
+
+ glDeleteShader(vertexShader);
+ glDeleteShader(fragmentShader);
+ }
+
+ public void destroy() {
+ onSave();
+
+ glDeleteProgram(shaderProgram);
+ glDeleteVertexArrays(VAO);
+ mem.destroy();
+
+ SimpleChunkMesh.instances = 0;
+ SimpleChunkMesh.usedRAM = 0;
+ ChunkMesh.instances = 0;
+ ChunkMesh.usedRAM = 0;
+ }
+
+ public void onWorldRendererChanged(WorldRenderer wr, WorldRendererChange change) {
+ int x = Math.floorDiv(wr.posX, 16);
+ int y = Math.floorDiv(wr.posY, 16);
+ int z = Math.floorDiv(wr.posZ, 16);
+ LODChunk lodChunk = getLODChunk(x, z);
+
+ lodChunk.isSectionVisible[y] = change == WorldRendererChange.VISIBLE;
+ if(change == WorldRendererChange.DELETED) {
+ removeMesh(lodChunk.chunkMeshes[y]);
+ }
+ lodChunkChanged(lodChunk);
+ }
+
+ public void onWorldRendererPost(WorldRenderer wr) {
+ if(LODMod.disableChunkMeshes) return;
+
+ int x = Math.floorDiv(wr.posX, 16);
+ int y = Math.floorDiv(wr.posY, 16);
+ int z = Math.floorDiv(wr.posZ, 16);
+
+ if(Minecraft.getMinecraft().theWorld.getChunkFromChunkCoords(x, z).isChunkLoaded) {
+ LODChunk lodChunk = getLODChunk(x, z);
+ lodChunk.isSectionVisible[y] = ((IWorldRenderer)wr).isDrawn();
+ lodChunk.putChunkMeshes(y, ((IWorldRenderer)wr).getChunkMeshes());
+ }
+ }
+
+ private double getLastSortDistanceSq(Entity player) {
+ return Math.pow(lastSortX - player.posX, 2) + Math.pow(lastSortZ - player.posZ, 2);
+ }
+
+ private synchronized void addToServerChunkLoadQueue(List<ChunkCoordIntPair> coords) {
+ serverChunkLoadQueue.addAll(coords);
+ }
+
+ private LODChunk receiveFarChunk(Chunk chunk) {
+ LODRegion region = getRegionContaining(chunk.xPosition, chunk.zPosition);
+ return region.putChunk(chunk);
+ }
+
+ private LODChunk getLODChunk(int chunkX, int chunkZ) {
+ return getRegionContaining(chunkX, chunkZ).getChunkAbsolute(chunkX, chunkZ);
+ }
+
+ public void onStopServer() {
+
+ }
+
+ public synchronized void serverTick() {
+ int chunkLoadsRemaining = LODMod.chunkLoadsPerTick;
+ while(!serverChunkLoadQueue.isEmpty() && chunkLoadsRemaining-- > 0) {
+ ChunkCoordIntPair coords = serverChunkLoadQueue.remove(0);
+ ChunkProviderServer chunkProviderServer = Minecraft.getMinecraft().getIntegratedServer().worldServerForDimension(world.provider.dimensionId).theChunkProviderServer;
+ Chunk chunk = chunkProviderServer.currentChunkProvider.provideChunk(coords.chunkXPos, coords.chunkZPos);
+ SimpleChunkMesh.prepareFarChunkOnServer(chunk);
+ farChunks.add(chunk);
+ }
+ }
+
+ private LODRegion getRegionContaining(int chunkX, int chunkZ) {
+ ChunkCoordIntPair key = new ChunkCoordIntPair(Math.floorDiv(chunkX , 32), Math.floorDiv(chunkZ, 32));
+ LODRegion region = loadedRegionsMap.get(key);
+ if(region == null) {
+ region = LODRegion.load(getSaveDir(), Math.floorDiv(chunkX , 32), Math.floorDiv(chunkZ , 32));
+ loadedRegionsMap.put(key, region);
+ }
+ return region;
+ }
+
+ private void sendChunkToGPU(LODChunk lodChunk) {
+ Entity player = Minecraft.getMinecraft().renderViewEntity;
+
+ lodChunk.tick(player);
+ setVisible(lodChunk, true, true);
+ }
+
+ public void setVisible(LODChunk chunk, boolean visible) {
+ setVisible(chunk, visible, false);
+ }
+
+ public void setVisible(LODChunk lodChunk, boolean visible, boolean forceCheck) {
+ if(!forceCheck && visible == lodChunk.visible) return;
+
+ lodChunk.visible = visible;
+ lodChunkChanged(lodChunk);
+ }
+
+ public void lodChunkChanged(LODChunk lodChunk) {
+ int newLOD = (!lodChunk.hasChunkMeshes() && lodChunk.lod == 2) ? (LODMod.disableSimpleMeshes ? 0 : 1) : lodChunk.lod;
+ for(SimpleChunkMesh sm : lodChunk.simpleMeshes) {
+ if(sm != null) {
+ if(lodChunk.isFullyVisible() && newLOD == 1) {
+ if(!sm.visible) {
+ setMeshVisible(sm, true);
+ }
+ } else {
+ if(sm.visible) {
+ setMeshVisible(sm, false);
+ }
+ }
+ }
+ }
+ for(int y = 0; y < 16; y++) {
+ for(int pass = 0; pass < 2; pass++) {
+ ChunkMesh cm = lodChunk.chunkMeshes[y * 2 + pass];
+ if(cm != null) {
+ if(lodChunk.isSectionVisible[y] && newLOD == 2) {
+ if(!cm.visible) {
+ setMeshVisible(cm, true);
+ }
+ } else {
+ if(cm.visible) {
+ setMeshVisible(cm, false);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ protected void setMeshVisible(Mesh mesh, boolean visible) {
+ setMeshVisible(mesh, visible, false);
+ }
+
+ protected void setMeshVisible(Mesh mesh, boolean visible, boolean force) {
+ if((!force && freezeMeshes) || mesh == null) return;
+
+ if(mesh.visible != visible) {
+ mesh.visible = visible;
+
+ if(mesh.gpuStatus == GPUStatus.UNSENT) {
+ mem.sendMeshToGPU(mesh);
+ sentMeshes[mesh.pass].add(mesh);
+ }
+ }
+ }
+
+ public void removeMesh(Mesh mesh) {
+ if(mesh == null) return;
+
+ mem.deleteMeshFromGPU(mesh);
+ sentMeshes[mesh.pass].remove(mesh);
+ setMeshVisible(mesh, false);
+ }
+
+ public Chunk getChunkFromChunkCoords(int x, int z) {
+ for(Chunk chunk : myChunks) {
+ if(chunk.xPosition == x && chunk.zPosition == z) {
+ return chunk;
+ }
+ }
+ return null;
+ }
+
+ public boolean shouldSideBeRendered(Block block, IBlockAccess ba, int x, int y, int z, int w) {
+ EnumFacing facing = EnumFacing.values()[w];
+ if(block.getMaterial() == Material.water && facing != EnumFacing.UP && facing != EnumFacing.DOWN && !Minecraft.getMinecraft().theWorld.getChunkFromBlockCoords(x, z).isChunkLoaded) {
+ return false;
+ } else {
+ return block.shouldSideBeRendered(ba, x, y, z, w);
+ }
+ }
+
+ public List<String> getDebugText() {
+ List<String> text = new ArrayList<>();
+ text.addAll(mem.getDebugText());
+ text.addAll(Arrays.asList(
+ "Simple meshes: " + SimpleChunkMesh.instances + " (" + SimpleChunkMesh.usedRAM / 1024 / 1024 + "MB)",
+ "Full meshes: " + ChunkMesh.instances + " (" + ChunkMesh.usedRAM / 1024 / 1024 + "MB)",
+ "Total RAM used: " + ((SimpleChunkMesh.usedRAM + ChunkMesh.usedRAM) / 1024 / 1024) + " MB",
+ "Rendered: " + renderedMeshes
+ ));
+ return text;
+ }
+
+ public void onSave() {
+ System.out.println("Saving LOD regions...");
+ long t0 = System.currentTimeMillis();
+ //loadedRegionsMap.forEach((k, v) -> v.save(getSaveDir()));
+ System.out.println("Finished saving LOD regions in " + ((System.currentTimeMillis() - t0) / 1000.0) + "s");
+ }
+
+ public void onChunkLoad(ChunkEvent.Load event) {
+ farChunks.add(event.getChunk());
+ }
+
+ private Path getSaveDir(){
+ return Minecraft.getMinecraft().mcDataDir.toPath().resolve("lodmod").resolve(Minecraft.getMinecraft().getIntegratedServer().getFolderName());
+ }
+
+ private boolean shouldRenderInWorld(World world) {
+ return world != null && !world.provider.isHellWorld;
+ }
+
+ public static class LODChunkComparator implements Comparator<LODChunk> {
+ Entity player;
+
+ public LODChunkComparator(Entity player) {
+ this.player = player;
+ }
+
+ @Override
+ public int compare(LODChunk p1, LODChunk p2) {
+ int distSq1 = distSq(p1);
+ int distSq2 = distSq(p2);
+ return distSq1 < distSq2 ? -1 : distSq1 > distSq2 ? 1 : 0;
+ }
+
+ int distSq(LODChunk p) {
+ return (int)(
+ Math.pow(((p.x * 16) - player.chunkCoordX), 2) +
+ Math.pow(((p.z * 16) - player.chunkCoordZ), 2)
+ );
+ }
+ }
+
+ public static class ChunkCoordDistanceComparator implements Comparator<ChunkCoordIntPair> {
+ double x, y, z;
+
+ public ChunkCoordDistanceComparator(double x, double y, double z) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ }
+
+ @Override
+ public int compare(ChunkCoordIntPair p1, ChunkCoordIntPair p2) {
+ int distSq1 = distSq(p1);
+ int distSq2 = distSq(p2);
+ return distSq1 < distSq2 ? -1 : distSq1 > distSq2 ? 1 : 0;
+ }
+
+ int distSq(ChunkCoordIntPair p) {
+ return (int)(
+ Math.pow(((p.chunkXPos * 16) - x), 2) +
+ Math.pow(((p.chunkZPos * 16) - z), 2)
+ );
+ }
+ }
+
+ public static class MeshDistanceComparator implements Comparator<Mesh> {
+ double x, y, z;
+
+ MeshDistanceComparator(double x, double y, double z){
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ }
+
+ @Override
+ public int compare(Mesh a, Mesh b) {
+ if(a.pass < b.pass) {
+ return -1;
+ } else if(a.pass > b.pass) {
+ return 1;
+ } else {
+ double distSqA = a.distSq(x, y, z);
+ double distSqB = b.distSq(x, y, z);
+ if(distSqA > distSqB) {
+ return 1;
+ } else if(distSqA < distSqB) {
+ return -1;
+ } else {
+ return 0;
+ }
+ }
+ }
+
+ }
+
+ public static enum WorldRendererChange {
+ VISIBLE, INVISIBLE, DELETED
+ }
+} \ No newline at end of file
diff --git a/src/main/java/makamys/neodymium/renderer/Mesh.java b/src/main/java/makamys/neodymium/renderer/Mesh.java
new file mode 100644
index 0000000..dea5fc1
--- /dev/null
+++ b/src/main/java/makamys/neodymium/renderer/Mesh.java
@@ -0,0 +1,44 @@
+package makamys.neodymium.renderer;
+
+import java.nio.ByteBuffer;
+import java.nio.IntBuffer;
+
+import makamys.neodymium.util.Util;
+import net.minecraft.entity.Entity;
+import net.minecraft.nbt.NBTBase;
+
+public abstract class Mesh {
+
+ /** Can be null, unless gpuStatus is SENT */
+ public ByteBuffer buffer;
+ public int quadCount;
+ public boolean visible;
+ public GPUStatus gpuStatus = GPUStatus.UNSENT;
+ public int iFirst = -1, iCount = -1;
+ public int offset = -1;
+ public int pass;
+ int x, y, z;
+
+ public abstract int getStride();
+
+ public double distSq(double x2, double y2, double z2) {
+ return Util.distSq(x, y, z, x2, y2, z2);
+ }
+
+ public int bufferSize() {
+ return buffer == null ? 0 : buffer.limit();
+ }
+
+ public int getEnd() {
+ return offset + bufferSize();
+ }
+
+ public void prepareBuffer() {}
+ public void destroyBuffer() {}
+
+ public void update() {}
+
+ public static enum GPUStatus {
+ UNSENT, SENT, PENDING_DELETE
+ }
+}
diff --git a/src/main/java/makamys/neodymium/renderer/MeshQuad.java b/src/main/java/makamys/neodymium/renderer/MeshQuad.java
new file mode 100644
index 0000000..427355f
--- /dev/null
+++ b/src/main/java/makamys/neodymium/renderer/MeshQuad.java
@@ -0,0 +1,426 @@
+package makamys.neodymium.renderer;
+
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.Comparator;
+import java.util.Locale;
+import java.util.Map;
+
+import makamys.neodymium.renderer.MeshQuad.QuadPlaneComparator;
+import makamys.neodymium.util.SpriteUtil;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.texture.TextureAtlasSprite;
+import net.minecraft.client.renderer.texture.TextureMap;
+import net.minecraft.util.EnumFacing;
+
+public class MeshQuad {
+ public int spriteIndex;
+ public String spriteName;
+ public int[] xs = new int[4];
+ public int[] ys = new int[4];
+ public int[] zs = new int[4];
+ public int baseX = -1, baseY, baseZ;
+ public int minX = Integer.MAX_VALUE;
+ public int minY = Integer.MAX_VALUE;
+ public int minZ = Integer.MAX_VALUE;
+ public int maxX = Integer.MIN_VALUE;
+ public int maxY = Integer.MIN_VALUE;
+ public int maxZ = Integer.MIN_VALUE;
+ public int[] relUs = new int[4];
+ public int[] relVs = new int[4];
+ public int[] bUs = new int[4];
+ public int[] bVs = new int[4];
+ public int[] cs = new int[4];
+ public int[] normals = new int[4];
+ public boolean deleted;
+ public boolean isFullQuad;
+
+ public static final int PLANE_NONE = -1, PLANE_XY = 0, PLANE_XZ = 1, PLANE_YZ = 2;
+ public int plane = PLANE_NONE;
+ public int offset;
+ public ChunkMesh.Flags flags;
+
+ public static int[] totalMergeCountByPlane = new int[3];
+
+ private int minPositive(int a, int b) {
+ if(a == -1) {
+ return b;
+ } else {
+ return a < b ? a : b;
+ }
+ }
+ private int maxPositive(int a, int b) {
+ if(a == -1) {
+ return b;
+ } else {
+ return a > b ? a : b;
+ }
+ }
+
+ public MeshQuad(int[] rawBuffer, int offset, ChunkMesh.Flags flags, int offsetX, int offsetY, int offsetZ) {
+ this.offset = offset;
+ this.flags = flags;
+ int i = offset;
+ float[] us = new float[4];
+ float uSum = 0;
+ float[] vs = new float[4];
+ float vSum = 0;
+ for(int vertexI = 0; vertexI < 4; vertexI++) {
+ float u = Float.intBitsToFloat(rawBuffer[vertexI * 8 + i + 3]);
+ float v = Float.intBitsToFloat(rawBuffer[vertexI * 8 + i + 4]);
+
+ us[vertexI] = u;
+ vs[vertexI] = v;
+
+ uSum += u;
+ vSum += v;
+ }
+
+ float avgU = uSum / 4f;
+ float avgV = vSum / 4f;
+
+ TextureAtlasSprite sprite = null;
+ Map<String, TextureAtlasSprite> uploadedSprites = ((TextureMap)Minecraft.getMinecraft().getTextureManager().getTexture(TextureMap.locationBlocksTexture)).mapUploadedSprites;
+
+ spriteIndex = SpriteUtil.getSpriteIndexForUV(avgU, avgV);
+ sprite = SpriteUtil.getSprite(spriteIndex);
+
+ if(sprite == null) {
+ System.out.println("Error: couldn't find sprite");
+ } else {
+ spriteName = sprite.getIconName();
+ for(int vertexI = 0; vertexI < 4; vertexI++) {
+ float x = Float.intBitsToFloat(rawBuffer[i + 0]) - offsetX;
+ float y = Float.intBitsToFloat(rawBuffer[i + 1]) - offsetY;
+ float z = Float.intBitsToFloat(rawBuffer[i + 2]) - offsetZ;
+
+ int simpleX = (int)(x * 16);
+ //if(simpleX == 256) simpleX = 255;
+ int simpleY = (int)(y * 16);
+ //if(simpleY == 256) simpleY = 255;
+ int simpleZ = (int)(z * 16);
+ //if(simpleZ == 256) simpleZ = 255;
+
+ xs[vertexI] = simpleX;
+ ys[vertexI] = simpleY;
+ zs[vertexI] = simpleZ;
+
+ // hasTexture
+ float u = us[vertexI];
+ float v = vs[vertexI];
+
+ int simpleRelU = (int)((u - sprite.getMinU()) / (sprite.getMaxU() - sprite.getMinU()) * 16);
+ int simpleRelV = (int)((v - sprite.getMinV()) / (sprite.getMaxV() - sprite.getMinV()) * 16);
+ if(flags.hasTexture) {
+ relUs[vertexI] = simpleRelU;
+ relVs[vertexI] = simpleRelV;
+ }
+
+ // hasBrightness
+ int brightness = rawBuffer[i + 7];
+ int brightnessU = brightness & 0xFFFF;
+ int brightnessV = (brightness >> 16) & 0xFFFF;
+ if(flags.hasBrightness) {
+ bUs[vertexI] = (int)brightnessU;
+ bVs[vertexI] = (int)brightnessV;
+ }
+
+ // hasColor
+ int color = rawBuffer[i + 5];
+ if(flags.hasColor) {
+ cs[vertexI] = color;
+ }
+
+ // hasNormals
+ int normal = rawBuffer[i + 6];
+ if(flags.hasNormals) {
+ normals[vertexI] = normal;
+ }
+
+ i += 8;
+ }
+ }
+
+ updateMinMaxXYZ();
+
+ if(ys[0] == ys[1] && ys[1] == ys[2] && ys[2] == ys[3]) {
+ plane = PLANE_XZ;
+ } else if(xs[0] == xs[1] && xs[1] == xs[2] && xs[2] == xs[3]) {
+ plane = PLANE_YZ;
+ } else if(zs[0] == zs[1] && zs[1] == zs[2] && zs[2] == zs[3]) {
+ plane = PLANE_XY;
+ } else {
+ plane = PLANE_NONE;
+ }
+
+ boolean equalToAABB = true;
+ for(int minOrMaxX = 0; minOrMaxX < 2; minOrMaxX++) {
+ for(int minOrMaxY = 0; minOrMaxY < 2; minOrMaxY++) {
+ for(int minOrMaxZ = 0; minOrMaxZ < 2; minOrMaxZ++) {
+ if(getCornerVertex(minOrMaxX == 1, minOrMaxY == 1, minOrMaxZ == 1) == -1) {
+ equalToAABB = false;
+ break;
+ }
+ }
+ }
+ }
+
+ switch(plane) {
+ case PLANE_XY:
+ isFullQuad = equalToAABB && (maxX - minX) == 16 && (maxY - minY) == 16;
+ break;
+ case PLANE_XZ:
+ isFullQuad = equalToAABB && (maxX - minX) == 16 && (maxZ - minZ) == 16;
+ break;
+ case PLANE_YZ:
+ isFullQuad = equalToAABB && (maxY - minY) == 16 && (maxZ - minZ) == 16;
+ break;
+ default:
+ isFullQuad = false;
+ }
+
+ for(int c = 0; c < 3; c++) {
+ if(getMin(c) < 0 || getMax(c) < 0 || getMax(c) - getMin(c) > 16 || getMin(c) > 256 || getMax(c) > 256) {
+ this.deleted = true;
+ // TODO handle weirdness more gracefully
+ }
+ }
+ }
+
+ // yeah this is kinda unoptimal
+ private int getCornerVertex(boolean minOrMaxX, boolean minOrMaxY, boolean minOrMaxZ) {
+ int aabbCornerX = !minOrMaxX ? minX : maxX;
+ int aabbCornerY = !minOrMaxY ? minY : maxY;
+ int aabbCornerZ = !minOrMaxZ ? minZ : maxZ;
+
+ for(int vi = 0; vi < 4; vi++) {
+ if(xs[vi] == aabbCornerX && ys[vi] == aabbCornerY && zs[vi] == aabbCornerZ) {
+ return vi;
+ }
+ }
+ return -1;
+ }
+
+ public void tryToMerge(MeshQuad o) {
+ if(isValid(this) && isValid(o) && plane == o.plane
+ && spriteIndex == o.spriteIndex && isFullQuad && o.isFullQuad) {
+ int numVerticesTouching = 0;
+ for(int i = 0; i < 4; i++) {
+ for(int j = 0; j < 4; j++) {
+ if(xs[i] == o.xs[j] && ys[i] == o.ys[j] && zs[i] == o.zs[j]) {
+ numVerticesTouching++;
+ }
+ }
+ }
+ if(numVerticesTouching == 2) {
+ mergeWithQuad(o);
+
+ totalMergeCountByPlane[plane]++;
+
+ o.deleted = true;
+ }
+ }
+ }
+
+ private void mergeWithQuad(MeshQuad o) {
+ if(minX < o.minX) {
+ copyEdgeFrom(o, EnumFacing.EAST);
+ } else if(minX > o.minX) {
+ copyEdgeFrom(o, EnumFacing.WEST);
+ } else if(minY < o.minY) {
+ copyEdgeFrom(o, EnumFacing.UP);
+ } else if(minY > o.minY) {
+ copyEdgeFrom(o, EnumFacing.DOWN);
+ } else if(minZ < o.minZ) {
+ copyEdgeFrom(o, EnumFacing.NORTH);
+ } else if(minX > o.minX) {
+ copyEdgeFrom(o, EnumFacing.SOUTH);
+ }
+ }
+
+ private void copyEdgeFrom(MeshQuad o, EnumFacing side) {
+ int whichX, whichY, whichZ;
+ whichX = whichY = whichZ = -1;
+
+ switch(plane) {
+ case PLANE_XY:
+ whichZ = 0;
+ break;
+ case PLANE_XZ:
+ whichY = 0;
+ break;
+ case PLANE_YZ:
+ whichX = 0;
+ break;
+ }
+
+ switch(side) {
+ case EAST:
+ copyCornerVertexFrom(o, 1, whichY, whichZ);
+ break;
+ case WEST:
+ copyCornerVertexFrom(o, 0, whichY, whichZ);
+ break;
+ case UP:
+ copyCornerVertexFrom(o, whichX, 1, whichZ);
+ break;
+ case DOWN:
+ copyCornerVertexFrom(o, whichX, 0, whichZ);
+ break;
+ case NORTH:
+ copyCornerVertexFrom(o, whichX, whichY, 1);
+ break;
+ case SOUTH:
+ copyCornerVertexFrom(o, whichX, whichY, 0);
+ break;
+ }
+
+ updateMinMaxXYZ();
+ }
+
+ private void updateMinMaxXYZ() {
+ for(int i = 0; i < 4; i++) {
+ minX = Math.min(minX, xs[i]);
+ minY = Math.min(minY, ys[i]);
+ minZ = Math.min(minZ, zs[i]);
+ maxX = Math.max(maxX, xs[i]);
+ maxY = Math.max(maxY, ys[i]);
+ maxZ = Math.max(maxZ, zs[i]);
+ }
+ }
+
+ private void copyCornerVertexFrom(MeshQuad o, int whichX, int whichY, int whichZ) {
+ int whichXMin, whichXMax, whichYMin, whichYMax, whichZMin, whichZMax;
+ whichXMin = whichYMin = whichZMin = 0;
+ whichXMax = whichYMax = whichZMax = 1;
+
+ if(whichX != -1) whichXMin = whichXMax = whichX;
+ if(whichY != -1) whichYMin = whichYMax = whichY;
+ if(whichZ != -1) whichZMin = whichZMax = whichZ;
+
+ for(int minOrMaxX = whichXMin; minOrMaxX <= whichXMax; minOrMaxX++) {
+ for(int minOrMaxY = whichYMin; minOrMaxY <= whichYMax; minOrMaxY++) {
+ for(int minOrMaxZ = whichZMin; minOrMaxZ <= whichZMax; minOrMaxZ++) {
+ copyVertexFrom(o,
+ o.getCornerVertex(minOrMaxX == 1, minOrMaxY == 1, minOrMaxZ == 1),
+ getCornerVertex(minOrMaxX == 1, minOrMaxY == 1, minOrMaxZ == 1));
+ }
+ }
+ }
+ }
+
+ private void copyVertexFrom(MeshQuad o, int src, int dest) {
+ xs[dest] = o.xs[src];
+ ys[dest] = o.ys[src];
+ zs[dest] = o.zs[src];
+ relUs[dest] = o.relUs[src];
+ relVs[dest] = o.relVs[src];
+ bUs[dest] = o.bUs[src];
+ bVs[dest] = o.bVs[src];
+ cs[dest] = o.cs[src];
+ normals[dest] = o.normals[src];
+ }
+
+ public void writeToDisk(DataOutputStream out, int pass) throws IOException {
+ if(deleted) {
+ return;
+ }
+
+ if(flags.hasTexture) {
+ if(pass == 0) out.writeShort(spriteIndex);
+ }
+ for (int vertexI = 0; vertexI < 4; vertexI++) {
+ if(pass == 1) out.writeByte(xs[vertexI] == 256 ? 255 : xs[vertexI]);
+ if(pass == 2) out.writeByte(ys[vertexI] == 256 ? 255 : ys[vertexI]);
+ if(pass == 3) out.writeByte(zs[vertexI] == 256 ? 255 : zs[vertexI]);
+
+ if (flags.hasTexture) {
+ if(pass == 4) out.writeByte(relUs[vertexI]);
+ if(pass == 5) out.writeByte(relVs[vertexI]);
+ }
+
+ if (flags.hasBrightness) {
+ if(pass == 6) out.writeByte(bUs[vertexI]);
+ if(pass == 7) out.writeByte(bVs[vertexI]);
+ }
+
+ if (flags.hasColor) {
+ if(pass == 8) out.writeInt(cs[vertexI]);
+ }
+
+ if (flags.hasNormals) {
+ if(pass == 9) out.writeInt(normals[vertexI]);
+ }
+ }
+ }
+
+ // maybe minXYZ and maxXYZ should be arrays instead
+ public int getMin(int coord) {
+ return coord == 0 ? minX : coord == 1 ? minY : coord == 2 ? minZ : -1;
+ }
+
+ public int getMax(int coord) {
+ return coord == 0 ? maxX : coord == 1 ? maxY : coord == 2 ? maxZ : -1;
+ }
+
+ public boolean onSamePlaneAs(MeshQuad o) {
+ return isValid(this) && isValid(o) && plane == o.plane &&
+ ((plane == PLANE_XY && minZ == o.minZ) ||
+ (plane == PLANE_XZ && minY == o.minY) ||
+ (plane == PLANE_YZ && minX == o.minX));
+ }
+
+ // this should be static..
+ public boolean isValid(MeshQuad q) {
+ return q != null && !q.deleted;
+ }
+
+ public boolean isClockwiseXZ() {
+ return (xs[1] - xs[0]) * (zs[2] - zs[0]) - (xs[2] - xs[0]) * (zs[1] - zs[0]) < 0;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(Locale.ENGLISH, "%s(%.1f, %.1f, %.1f -- %.1f, %.1f, %.1f) %s", deleted ? "XXX " : "", minX/16f, minY/16f, minZ/16f, maxX/16f, maxY/16f, maxZ/16f, spriteName);
+ }
+
+ public static class QuadPlaneComparator implements Comparator<MeshQuad> {
+
+ public static final QuadPlaneComparator[] quadPlaneComparators = new QuadPlaneComparator[]{
+ new QuadPlaneComparator(2, 1, 0), // PLANE_XY -> ZYX
+ new QuadPlaneComparator(1, 2, 0), // PLANE_XZ -> YZX
+ new QuadPlaneComparator(0, 2, 1) // PLANE_YZ -> XZY
+ };
+
+ private int c0, c1, c2;
+
+ public QuadPlaneComparator(int firstCoordToCompare, int secondCoordToCompare, int thirdCoordToCompare) {
+ this.c0 = firstCoordToCompare;
+ this.c1 = secondCoordToCompare;
+ this.c2 = thirdCoordToCompare;
+ }
+
+ @Override
+ public int compare(MeshQuad a, MeshQuad b) {
+ if(a.getMin(c0) < b.getMin(c0)) {
+ return -1;
+ } else if(a.getMin(c0) > b.getMin(c0)) {
+ return 1;
+ } else {
+ if(a.getMin(c1) < b.getMin(c1)) {
+ return -1;
+ } else if(a.getMin(c1) > b.getMin(c1)) {
+ return 1;
+ } else {
+ if(a.getMin(c2) < b.getMin(c2)) {
+ return -1;
+ } else if(a.getMin(c2) > b.getMin(c2)) {
+ return 1;
+ } else {
+ return (int)Math.signum(a.offset - b.offset);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/makamys/neodymium/renderer/SimpleChunkMesh.java b/src/main/java/makamys/neodymium/renderer/SimpleChunkMesh.java
new file mode 100644
index 0000000..964dec8
--- /dev/null
+++ b/src/main/java/makamys/neodymium/renderer/SimpleChunkMesh.java
@@ -0,0 +1,350 @@
+package makamys.neodymium.renderer;
+
+import static org.lwjgl.opengl.GL11.GL_FLOAT;
+import static org.lwjgl.opengl.GL15.GL_ARRAY_BUFFER;
+import static org.lwjgl.opengl.GL15.GL_ELEMENT_ARRAY_BUFFER;
+import static org.lwjgl.opengl.GL15.GL_STATIC_DRAW;
+import static org.lwjgl.opengl.GL15.glBufferData;
+import static org.lwjgl.opengl.GL20.glEnableVertexAttribArray;
+import static org.lwjgl.opengl.GL20.glVertexAttribPointer;
+
+import java.nio.ByteBuffer;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.util.Arrays;
+import java.util.List;
+
+import org.lwjgl.BufferUtils;
+
+import makamys.neodymium.LODMod;
+import makamys.neodymium.util.MCUtil;
+import net.minecraft.block.Block;
+import net.minecraft.block.BlockGrass;
+import net.minecraft.block.BlockLeaves;
+import net.minecraft.block.material.Material;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.texture.TextureAtlasSprite;
+import net.minecraft.client.renderer.texture.TextureMap;
+import net.minecraft.entity.Entity;
+import net.minecraft.init.Blocks;
+import net.minecraft.nbt.NBTTagString;
+import net.minecraft.util.IIcon;
+import net.minecraft.world.biome.BiomeGenBase;
+import net.minecraft.world.chunk.Chunk;
+
+public class SimpleChunkMesh extends Mesh {
+
+ private FloatBuffer vertices;
+
+ public static int usedRAM;
+ public static int instances;
+ public static int divisions = 4;
+
+ private static boolean isSolid(Block block) {
+ return block.isBlockNormalCube() && block.isOpaqueCube() && block.renderAsNormalBlock();
+ }
+
+ private static boolean isBad(Block block) {
+ for(Class clazz : LODMod.blockClassBlacklist) {
+ if(clazz.isInstance(block)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static List<SimpleChunkMesh> generateSimpleMeshes(Chunk target){
+ SimpleChunkMesh pass1 = new SimpleChunkMesh(target.xPosition, target.zPosition, divisions * divisions * 25, 0);
+ SimpleChunkMesh pass2 = new SimpleChunkMesh(target.xPosition, target.zPosition, divisions * divisions * 25, 1);
+
+ SimpleChunkMeshBuilder builder = new SimpleChunkMeshBuilder();
+
+ for(int divX = 0; divX < divisions; divX++) {
+ for(int divZ = 0; divZ < divisions; divZ++) {
+ IIcon icon = null;
+ int color = 0xFFFFFFFF;
+ int size = 16 / divisions;
+ int y = 255;
+ boolean foundWater = false;
+
+ int xOff = divX * size;
+ int zOff = divZ * size;
+
+ int biomeId = target.getBiomeArray()[xOff << 4 | zOff] & 255;
+ if(biomeId == 255) {
+ System.out.println("Missing biome data for chunk " + target.xPosition + ", " + target.zPosition);
+ }
+ BiomeGenBase biome = BiomeGenBase.getBiome(biomeId) == null ? BiomeGenBase.plains : BiomeGenBase.getBiome(biomeId);
+
+ for(y = 255; y > 0; y--) {
+ Block block = target.getBlock(xOff, y, zOff);
+
+ int worldX = target.xPosition * 16 + divX * size;
+ int worldY = y;
+ int worldZ = target.zPosition * 16 + divZ * size;
+
+ if(!foundWater && block.getMaterial() == Material.water) {
+ foundWater = true;
+ int meta = target.getBlockMetadata(xOff, y, zOff);
+ IIcon waterIcon = block.getIcon(1, meta);
+
+ int waterColor = biome.getWaterColorMultiplier();
+ waterColor |= 0xFF000000;
+ pass2.addFaceYPos(worldX, worldY, worldZ, size, size, waterIcon, waterColor, 1);
+ }
+
+ if(isSolid(block) && isBad(block)) {
+ for(int dx = -1; dx <= 1; dx++) {
+ for(int dz = -1; dz <= 1; dz++) {
+ int newX = xOff + dx;
+ int newZ = zOff + dz;
+ if(newX >= 0 && newX < 16 && newZ >= 0 && newZ < 16) {
+ Block newBlock = target.getBlock(newX, y, newZ);
+ if(!isBad(newBlock)) {
+ xOff += dx;
+ zOff += dz;
+ worldX += dx;
+ worldZ += dz;
+ block = newBlock;
+ }
+ }
+ }
+ }
+ }
+ if(isSolid(block)) {
+
+ float brightnessMult = foundWater ? 0.2f : 1f;
+ int meta = target.getBlockMetadata(xOff, y, zOff);
+ icon = block.getIcon(1, meta);
+
+ if(block instanceof BlockGrass) {
+ color = biome.getBiomeGrassColor(worldX, y, worldZ);
+ } else if(block instanceof BlockLeaves) {
+ color = biome.getBiomeFoliageColor(worldX, y, worldZ);
+ } else {
+ color = block.colorMultiplier(Minecraft.getMinecraft().theWorld, worldX, y, worldZ);
+ }
+ color = (0xFF << 24) | ((color >> 16 & 0xFF) << 0) | ((color >> 8 & 0xFF) << 8) | ((color >> 0 & 0xFF) << 16);
+
+ if((LODMod.forceVanillaBiomeTemperature ? MCUtil.getBiomeTemperatureVanilla(biome, worldX, y, worldZ)
+ : biome.getFloatTemperature(worldX, y, worldZ)) < 0.15f) {
+
+ builder.addCube(divX, divZ, worldY + 0.2f, 1f, Blocks.snow_layer.getIcon(1, 0), 0xFFFFFFFF, brightnessMult);
+ builder.addCube(divX, divZ, worldY - 0.8f, -1, icon, color, brightnessMult);
+ } else {
+ builder.addCube(divX, divZ, worldY, -1, icon, color, brightnessMult);
+ }
+
+
+ break;
+ }
+ }
+ }
+ }
+
+ builder.render(pass1, target.xPosition, target.zPosition);
+
+ pass1.finish();
+ pass2.finish();
+
+ return Arrays.asList(new SimpleChunkMesh[] {pass1.quadCount != 0 ? pass1 : null, pass2.quadCount != 0 ? pass2 : null});
+ }
+
+ private static class SimpleChunkMeshBuilder {
+ int maxIconsPerColumn = 2;
+ float[][][] heights = new float[divisions][divisions][maxIconsPerColumn];
+ float[][][] depths = new float[divisions][divisions][maxIconsPerColumn];
+ IIcon[][][] icons = new IIcon[divisions][divisions][maxIconsPerColumn];
+ int[][][] colors = new int[divisions][divisions][maxIconsPerColumn];
+ float[][][] brightnessMults = new float[divisions][divisions][maxIconsPerColumn];
+
+ public void addCube(int x, int z, float height, float depth, IIcon icon, int color, float brightnessMult) {
+ IIcon[] iconz = icons[x][z];
+ int i = iconz[0] == null ? 0 : 1;
+ if(iconz[0] != null && iconz[1] != null) {
+ throw new IllegalStateException("Too many icons in column");
+ }
+
+ heights[x][z][i] = height;
+ depths[x][z][i] = depth;
+ icons[x][z][i] = icon;
+ colors[x][z][i] = color;
+ brightnessMults[x][z][i] = brightnessMult;
+ }
+
+ public void render(SimpleChunkMesh mesh, int chunkX, int chunkZ) {
+ float size = 16 / divisions;
+
+ for(int x = 0; x < divisions; x++) {
+ for(int z = 0; z < divisions; z++) {
+ float worldX = chunkX * 16 + x * size;
+ float worldZ = chunkZ * 16 + z * size;
+ for(int i = 0; i < maxIconsPerColumn; i++) {
+ IIcon icon = icons[x][z][i];
+ if(icon != null) {
+ float height = heights[x][z][i];
+ float depthValue = depths[x][z][i];
+ float depth = depthValue == -1 ? height : depthValue;
+ int color = colors[x][z][i];
+ float brightnessMult = brightnessMults[x][z][i];
+
+ if(i == 0) {
+ mesh.addFaceYPos(worldX, height, worldZ, size, size, icon, color, brightnessMult);
+ }
+ float heightX0 = x > 0 ? heights[x - 1][z][0] : 0;
+ if(heightX0 < height) {
+ mesh.addFaceX2(worldX, height, worldZ, Math.min(depth, height - heightX0), size, icon, color, brightnessMult);
+ }
+
+ float heightX1 = x < divisions - 1 ? heights[x + 1][z][0] : 0;
+ if(heightX1 < height) {
+ mesh.addFaceX1(worldX + size, height, worldZ, Math.min(depth, height - heightX1), size, icon, color, brightnessMult);
+ }
+
+ float heightZ0 = z > 0 ? heights[x][z - 1][0] : 0;
+ if(heightZ0 < height) {
+ mesh.addFaceZ1(worldX, height, worldZ, size, Math.min(depth, height - heightZ0), icon, color, brightnessMult);
+ }
+
+ float heightZ1 = z < divisions - 1 ? heights[x][z + 1][0] : 0;
+ if(heightZ1 < height) {
+ mesh.addFaceZ2(worldX, height, worldZ + size, size, Math.min(depth, height - heightZ1), icon, color, brightnessMult);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ public SimpleChunkMesh(int x, int z, int maxQuads, int pass) {
+ this.x = x;
+ this.y = 64;
+ this.z = z;
+ this.pass = pass;
+
+ buffer = BufferUtils.createByteBuffer(4 * 6 * 7 * maxQuads);
+ vertices = buffer.asFloatBuffer();
+ }
+
+ public void finish() {
+ vertices.flip();
+ buffer.limit(vertices.limit() * 4);
+
+ // may want to shrink the buffers to actual size to not waste memory
+
+ usedRAM += buffer.limit();
+ instances++;
+ }
+
+ private void addCube(float x, float y, float z, float sizeX, float sizeZ, float sizeY, IIcon icon, int color, float brightnessMult) {
+ addFaceYPos(x, y, z, sizeX, sizeZ, icon, color, brightnessMult);
+ addFaceZ1(x, y, z, sizeX, sizeY, icon, color, brightnessMult);
+ addFaceZ2(x, y, z + sizeZ, sizeX, sizeY, icon, color, brightnessMult);
+ addFaceX1(x + sizeX, y, z, sizeX, sizeY, icon, color, brightnessMult);
+ addFaceX2(x, y, z, sizeX, sizeY, icon, color, brightnessMult);
+ }
+
+ private void addFaceZ1(float x, float y, float z, float sizeX, float sizeY, IIcon icon, int color, float brightnessMult) {
+ addFace(
+ x + 0, y - sizeY, z + 0,
+ x + 0, y + 0, z + 0,
+ x + sizeX, y + 0, z + 0,
+ x + sizeX, y - sizeY, z + 0,
+ icon, color, (int)(200 * brightnessMult)
+ );
+ }
+
+ private void addFaceZ2(float x, float y, float z, float sizeX, float sizeY, IIcon icon, int color, float brightnessMult) {
+ addFace(
+ x + sizeX, y - sizeY, z,
+ x + sizeX, y + 0, z,
+ x + 0, y + 0, z,
+ x + 0, y - sizeY, z,
+ icon, color, (int)(200 * brightnessMult)
+ );
+ }
+
+ private void addFaceX1(float x, float y, float z, float sizeY, float sizeZ, IIcon icon, int color, float brightnessMult) {
+ addFace(
+ x, y - sizeY, z + 0,
+ x, y + 0, z + 0,
+ x, y + 0, z + sizeZ,
+ x, y - sizeY, z + sizeZ,
+ icon, color, (int)(160 * brightnessMult)
+ );
+ }
+
+ private void addFaceX2(float x, float y, float z, float sizeY, float sizeZ, IIcon icon, int color, float brightnessMult) {
+ addFace(
+ x + 0, y - sizeY, z + sizeZ,
+ x + 0, y + 0, z + sizeZ,
+ x + 0, y + 0, z + 0,
+ x + 0, y - sizeY, z + 0,
+ icon, color, (int)(160 * brightnessMult)
+ );
+ }
+
+ private void addFaceYPos(float x, float y, float z, float sizeX, float sizeZ, IIcon icon, int color, float brightnessMult) {
+ addFace(
+ x + 0, y + 0, z + 0,
+ x + 0, y + 0, z + sizeZ,
+ x + sizeX, y + 0, z + sizeZ,
+ x + sizeX, y + 0, z + 0,
+ icon, color, (int)(240 * brightnessMult)
+ );
+ }
+
+ private void addFace(float p1x, float p1y, float p1z,
+ float p2x, float p2y, float p2z,
+ float p3x, float p3y, float p3z,
+ float p4x, float p4y, float p4z,
+ IIcon icon, int color, int brightness) {
+ int off = vertices.position() * 4;
+ vertices.put(new float[] {
+ p1x, p1y, p1z, icon.getMinU(), icon.getMaxV(), 0, 0,
+ p2x, p2y, p2z, icon.getMinU(), icon.getMinV(), 0, 0,
+ p4x, p4y, p4z, icon.getMaxU(), icon.getMaxV(), 0, 0,
+ p2x, p2y, p2z, icon.getMinU(), icon.getMinV(), 0, 0,
+ p3x, p3y, p3z, icon.getMaxU(), icon.getMinV(), 0, 0,
+ p4x, p4y, p4z, icon.getMaxU(), icon.getMaxV(), 0, 0
+ });
+ buffer.putInt(off + 0 * getStride() + 6 * 4, color);
+ buffer.putShort(off + 0 * getStride() + 5 * 4 + 2, (short)brightness);
+ buffer.putInt(off + 1 * getStride() + 6 * 4, color);
+ buffer.putShort(off + 1 * getStride() + 5 * 4 + 2, (short)brightness);
+ buffer.putInt(off + 2 * getStride() + 6 * 4, color);
+ buffer.putShort(off + 2 * getStride() + 5 * 4 + 2, (short)brightness);
+ buffer.putInt(off + 3 * getStride() + 6 * 4, color);
+ buffer.putShort(off + 3 * getStride() + 5 * 4 + 2, (short)brightness);
+ buffer.putInt(off + 4 * getStride() + 6 * 4, color);
+ buffer.putShort(off + 4 * getStride() + 5 * 4 + 2, (short)brightness);
+ buffer.putInt(off + 5 * getStride() + 6 * 4, color);
+ buffer.putShort(off + 5 * getStride() + 5 * 4 + 2, (short)brightness);
+
+ quadCount++;
+ }
+
+ public int getStride() {
+ return (3 * 4 + 8 + 4 + 4);
+ }
+
+ public void destroy() {
+ usedRAM -= buffer.limit();
+ instances--;
+ }
+
+ public static void prepareFarChunkOnServer(Chunk chunk) {
+ for(int divX = 0; divX < divisions; divX++) {
+ for(int divZ = 0; divZ < divisions; divZ++) {
+ int size = 16 / divisions;
+
+ int xOff = divX * size;
+ int zOff = divZ * size;
+
+ chunk.getBiomeGenForWorldCoords(xOff, zOff, chunk.worldObj.getWorldChunkManager());
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/makamys/neodymium/util/BufferWriter.java b/src/main/java/makamys/neodymium/util/BufferWriter.java
new file mode 100644
index 0000000..901f5dc
--- /dev/null
+++ b/src/main/java/makamys/neodymium/util/BufferWriter.java
@@ -0,0 +1,46 @@
+package makamys.neodymium.util;
+
+import java.nio.ByteBuffer;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.nio.ShortBuffer;
+
+public class BufferWriter {
+
+ private ByteBuffer buf;
+
+ private FloatBuffer floatBuffer;
+ private ShortBuffer shortBuffer;
+ private IntBuffer intBuffer;
+
+ public BufferWriter(ByteBuffer buf) {
+ this.buf = buf;
+ this.floatBuffer = buf.asFloatBuffer();
+ this.shortBuffer = buf.asShortBuffer();
+ this.intBuffer = buf.asIntBuffer();
+ }
+
+ private void incrementPosition(int add) {
+ buf.position(buf.position() + add);
+ floatBuffer.position(buf.position() / 4);
+ shortBuffer.position(buf.position() / 2);
+ intBuffer.position(buf.position() / 4);
+ }
+
+ public void writeFloat(float x) {
+ try {
+ floatBuffer.put(x);
+
+ incrementPosition(4);
+ } catch(Exception e){
+ e.printStackTrace();
+ }
+ }
+
+ public void writeInt(int x) {
+ intBuffer.put(x);
+
+ incrementPosition(4);
+ }
+
+} \ No newline at end of file
diff --git a/src/main/java/makamys/neodymium/util/GuiHelper.java b/src/main/java/makamys/neodymium/util/GuiHelper.java
new file mode 100644
index 0000000..1b56d63
--- /dev/null
+++ b/src/main/java/makamys/neodymium/util/GuiHelper.java
@@ -0,0 +1,61 @@
+package makamys.neodymium.util;
+
+import org.lwjgl.opengl.GL11;
+
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.Tessellator;
+
+public class GuiHelper {
+
+ public static void begin() {
+ GL11.glDisable(GL11.GL_TEXTURE_2D);
+
+ Minecraft mc = Minecraft.getMinecraft();
+
+ //GL11.glClear(GL11.GL_DEPTH_BUFFER_BIT);
+ GL11.glMatrixMode(GL11.GL_PROJECTION);
+ //GL11.glEnable(GL11.GL_COLOR_MATERIAL);
+ GL11.glLoadIdentity();
+ GL11.glOrtho(0.0D, (double)mc.displayWidth, (double)mc.displayHeight, 0.0D, 1000.0D, 3000.0D);
+ GL11.glMatrixMode(GL11.GL_MODELVIEW);
+ GL11.glLoadIdentity();
+ GL11.glTranslatef(0.0F, 0.0F, -2000.0F);
+ //GL11.glLineWidth(1.0F);
+ //GL11.glDisable(GL11.GL_TEXTURE_2D);
+ }
+
+ public static void drawRectangle(int x, int y, int w, int h, int color) {
+ Tessellator tessellator = Tessellator.instance;
+ tessellator.startDrawingQuads();
+ tessellator.setColorOpaque_I(color);
+ tessellator.addVertex(x, y, 0);
+ tessellator.addVertex(x, y+h, 0);
+ tessellator.addVertex(x+w, y+h, 0);
+ tessellator.addVertex(x+w, y, 0);
+
+ tessellator.draw();
+ }
+
+ public static void drawRectangle(int x, int y, int w, int h, int color, int opacity) {
+ GL11.glEnable(GL11.GL_BLEND);
+
+ Tessellator tessellator = Tessellator.instance;
+ tessellator.startDrawingQuads();
+ tessellator.setColorRGBA_I(color, opacity);
+ tessellator.addVertex(x, y, 0);
+ tessellator.addVertex(x, y+h, 0);
+ tessellator.addVertex(x+w, y+h, 0);
+ tessellator.addVertex(x+w, y, 0);
+
+ tessellator.draw();
+
+ GL11.glDisable(GL11.GL_BLEND);
+ }
+
+ public static void end() {
+ //GL11.glDisable(GL11.GL_BLEND);
+
+ //GL11.glEnable(GL11.GL_TEXTURE_2D);
+ }
+
+}
diff --git a/src/main/java/makamys/neodymium/util/MCUtil.java b/src/main/java/makamys/neodymium/util/MCUtil.java
new file mode 100644
index 0000000..7a2694d
--- /dev/null
+++ b/src/main/java/makamys/neodymium/util/MCUtil.java
@@ -0,0 +1,20 @@
+package makamys.neodymium.util;
+
+import net.minecraft.world.biome.BiomeGenBase;
+
+public class MCUtil {
+
+ public static float getBiomeTemperatureVanilla(BiomeGenBase biome, int p_150564_1_, int p_150564_2_, int p_150564_3_){
+ if (p_150564_2_ > 64)
+ {
+ float f = (float)BiomeGenBase.temperatureNoise
+ .func_151601_a((double)p_150564_1_ * 1.0D / 8.0D, (double)p_150564_3_ * 1.0D / 8.0D) * 4.0F;
+ return biome.temperature - (f + (float)p_150564_2_ - 64.0F) * 0.05F / 30.0F;
+ }
+ else
+ {
+ return biome.temperature;
+ }
+ }
+
+}
diff --git a/src/main/java/makamys/neodymium/util/SpriteUtil.java b/src/main/java/makamys/neodymium/util/SpriteUtil.java
new file mode 100644
index 0000000..4219802
--- /dev/null
+++ b/src/main/java/makamys/neodymium/util/SpriteUtil.java
@@ -0,0 +1,55 @@
+package makamys.neodymium.util;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.texture.TextureAtlasSprite;
+import net.minecraft.client.renderer.texture.TextureMap;
+import net.minecraft.world.ChunkCoordIntPair;
+
+public class SpriteUtil {
+
+ private static int[] spriteIndexMap;
+ public static List<TextureAtlasSprite> sprites;
+
+ private static Map<Long, Integer> uv2spriteIndex = new HashMap<>();
+
+ private static int findSpriteIndexForUV(float u, float v) {
+ Map<String, TextureAtlasSprite> uploadedSprites = ((TextureMap)Minecraft.getMinecraft().getTextureManager().getTexture(TextureMap.locationBlocksTexture)).mapUploadedSprites;
+
+ int spriteIndex = 0;
+ for(TextureAtlasSprite tas : uploadedSprites.values()) {
+ if(tas.getMinU() <= u && u <= tas.getMaxU() && tas.getMinV() <= v && v <= tas.getMaxV()) {
+ break;
+ }
+ spriteIndex++;
+ }
+ return spriteIndex;
+ }
+
+ public static int getSpriteIndexForUV(float u, float v){
+ long key = ChunkCoordIntPair.chunkXZ2Int((int)(u * Integer.MAX_VALUE), (int)(v * Integer.MAX_VALUE));
+ int index = uv2spriteIndex.getOrDefault(key, -1);
+ if(index == -1) {
+ index = findSpriteIndexForUV(u, v);
+ uv2spriteIndex.put(key, index);
+ }
+ return index;
+ }
+
+ public static TextureAtlasSprite getSprite(int i){
+ if(i >= 0 && i < sprites.size()) {
+ return sprites.get(i);
+ } else {
+ return null;
+ }
+ }
+
+ public static void init() {
+ Map<String, TextureAtlasSprite> uploadedSprites = ((TextureMap)Minecraft.getMinecraft().getTextureManager().getTexture(TextureMap.locationBlocksTexture)).mapUploadedSprites;
+ sprites = uploadedSprites.values().stream().collect(Collectors.toList());
+ }
+}
diff --git a/src/main/java/makamys/neodymium/util/Util.java b/src/main/java/makamys/neodymium/util/Util.java
new file mode 100644
index 0000000..2507a78
--- /dev/null
+++ b/src/main/java/makamys/neodymium/util/Util.java
@@ -0,0 +1,88 @@
+package makamys.neodymium.util;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URL;
+import java.nio.ByteBuffer;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import net.minecraft.launchwrapper.Launch;
+
+public class Util {
+
+ private static boolean allowResourceOverrides = Boolean.parseBoolean(System.getProperty("lodmod.allowResourceOverrides", "false"));
+
+ public static Path getResourcePath(String relPath) {
+ if(allowResourceOverrides) {
+ File overrideFile = new File(new File(Launch.minecraftHome, "lodmod/resources"), relPath);
+ if(overrideFile.exists()) {
+ return overrideFile.toPath();
+ }
+ }
+
+ try {
+ URL resourceURL = Util.class.getClassLoader().getResource(relPath);
+
+ switch(resourceURL.getProtocol()) {
+ case "jar":
+ String urlString = resourceURL.getPath();
+ int lastExclamation = urlString.lastIndexOf('!');
+ String newURLString = urlString.substring(0, lastExclamation);
+ return FileSystems.newFileSystem(new File(URI.create(newURLString)).toPath(), null).getPath(relPath);
+ case "file":
+ return new File(URI.create(resourceURL.toString())).toPath();
+ default:
+ return null;
+ }
+ } catch(IOException e) {
+ return null;
+ }
+ }
+
+ public static String readFile(String path){
+ try {
+ return new String(Files.readAllBytes(Util.getResourcePath(path)));
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return "";
+ }
+
+ public static byte[] byteBufferToArray(ByteBuffer buffer) {
+ byte[] dst = new byte[buffer.limit()];
+ int pos = buffer.position();
+ buffer.position(0);
+ buffer.get(dst);
+ buffer.position(pos);
+ return dst;
+ }
+
+ public static int[] intBufferToArray(IntBuffer buffer) {
+ int[] dst = new int[buffer.limit()];
+ int pos = buffer.position();
+ buffer.position(0);
+ buffer.get(dst);
+ buffer.position(pos);
+ return dst;
+ }
+
+ public static float[] floatBufferToArray(FloatBuffer buffer) {
+ float[] dst = new float[buffer.limit()];
+ int pos = buffer.position();
+ buffer.position(0);
+ buffer.get(dst);
+ buffer.position(pos);
+ return dst;
+ }
+
+ public static double distSq(double x1, double y1, double z1, double x2, double y2, double z2) {
+ return Math.pow(x1 - x2, 2) +
+ Math.pow(y1 - y2, 2) +
+ Math.pow(z1 - z2, 2);
+ }
+}