diff options
Diffstat (limited to 'src/main/java/makamys/neodymium')
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); + } +} |