diff options
Diffstat (limited to 'src')
24 files changed, 2357 insertions, 30 deletions
diff --git a/src/main/java/makamys/lodmod/LODMod.java b/src/main/java/makamys/lodmod/LODMod.java index c31a5d7..ab4fb07 100644 --- a/src/main/java/makamys/lodmod/LODMod.java +++ b/src/main/java/makamys/lodmod/LODMod.java @@ -24,7 +24,6 @@ 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.ClientTickEvent; -import makamys.lodmod.mixin.IRenderGlobal; @Mod(modid = LODMod.MODID, version = LODMod.VERSION) public class LODMod diff --git a/src/main/java/makamys/lodmod/ducks/ITessellator.java b/src/main/java/makamys/lodmod/ducks/ITessellator.java new file mode 100644 index 0000000..5a57741 --- /dev/null +++ b/src/main/java/makamys/lodmod/ducks/ITessellator.java @@ -0,0 +1,10 @@ +package makamys.lodmod.ducks; + +import org.spongepowered.asm.mixin.Mixin; + +import makamys.lodmod.renderer.ChunkMesh; +import net.minecraft.client.renderer.Tessellator; + +public interface ITessellator { + public ChunkMesh toChunkMesh(); +} diff --git a/src/main/java/makamys/lodmod/ducks/IWorldRenderer.java b/src/main/java/makamys/lodmod/ducks/IWorldRenderer.java new file mode 100644 index 0000000..83857d6 --- /dev/null +++ b/src/main/java/makamys/lodmod/ducks/IWorldRenderer.java @@ -0,0 +1,12 @@ +package makamys.lodmod.ducks; + +import java.util.List; + +import org.spongepowered.asm.mixin.Mixin; + +import makamys.lodmod.renderer.ChunkMesh; +import net.minecraft.client.renderer.WorldRenderer; + +public interface IWorldRenderer { + public List<ChunkMesh> getChunkMeshes(); +} diff --git a/src/main/java/makamys/lodmod/mixin/IRenderGlobal.java b/src/main/java/makamys/lodmod/mixin/IRenderGlobal.java deleted file mode 100644 index d5ac050..0000000 --- a/src/main/java/makamys/lodmod/mixin/IRenderGlobal.java +++ /dev/null @@ -1,26 +0,0 @@ -package makamys.lodmod.mixin; - -import java.nio.IntBuffer; -import java.util.ArrayList; -import java.util.List; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.gen.Accessor; - -import net.minecraft.client.Minecraft; -import net.minecraft.client.multiplayer.WorldClient; -import net.minecraft.client.renderer.RenderBlocks; -import net.minecraft.client.renderer.RenderGlobal; -import net.minecraft.client.renderer.WorldRenderer; -import net.minecraft.client.renderer.texture.TextureManager; -import net.minecraft.entity.Entity; -import net.minecraft.util.ResourceLocation; -import net.minecraft.world.IWorldAccess; - -@Mixin(RenderGlobal.class) -public interface IRenderGlobal { - @Accessor("cloudTickCounter") - int cloudTickCounter(); -}
\ No newline at end of file diff --git a/src/main/java/makamys/lodmod/mixin/MixinChunkCache.java b/src/main/java/makamys/lodmod/mixin/MixinChunkCache.java new file mode 100644 index 0000000..bbfcca1 --- /dev/null +++ b/src/main/java/makamys/lodmod/mixin/MixinChunkCache.java @@ -0,0 +1,28 @@ +package makamys.lodmod.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import makamys.lodmod.renderer.FarChunkCache; +import makamys.lodmod.renderer.MyRenderer; +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(FarChunkCache.class.isInstance(this.getClass()) && chunk.isEmpty()) { + Chunk myChunk = MyRenderer.getChunkFromChunkCoords(p1, p2); + if(myChunk != null) { + chunk = myChunk; + } + } + return chunk; + } + +} diff --git a/src/main/java/makamys/lodmod/mixin/MixinEntityRenderer.java b/src/main/java/makamys/lodmod/mixin/MixinEntityRenderer.java index 933b577..544d131 100644 --- a/src/main/java/makamys/lodmod/mixin/MixinEntityRenderer.java +++ b/src/main/java/makamys/lodmod/mixin/MixinEntityRenderer.java @@ -8,6 +8,7 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import makamys.lodmod.renderer.MyRenderer; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.EntityRenderer; import net.minecraft.entity.EntityLivingBase; @@ -27,7 +28,7 @@ abstract class MixinEntityRenderer { @Inject(method = "renderWorld", at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glAlphaFunc(IF)V", shift = At.Shift.AFTER, ordinal = 1)) private void afterSortAndRender(float alpha, long something, CallbackInfo ci) { Minecraft.getMinecraft().entityRenderer.enableLightmap((double)alpha); - //MyRenderer.beforeRenderTerrain(); + MyRenderer.beforeRenderTerrain(); Minecraft.getMinecraft().entityRenderer.disableLightmap((double)alpha); } diff --git a/src/main/java/makamys/lodmod/mixin/MixinMinecraftServer.java b/src/main/java/makamys/lodmod/mixin/MixinMinecraftServer.java new file mode 100644 index 0000000..747cbb8 --- /dev/null +++ b/src/main/java/makamys/lodmod/mixin/MixinMinecraftServer.java @@ -0,0 +1,30 @@ +package makamys.lodmod.mixin; + +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.callback.CallbackInfo; + +import makamys.lodmod.renderer.MyRenderer; +import net.minecraft.server.MinecraftServer; + +@Mixin(MinecraftServer.class) +abstract class MixinMinecraftServer { + + @Shadow + boolean worldIsBeingDeleted; + + @Inject(method = "stopServer", at = @At("HEAD")) + public void stopServer(CallbackInfo ci) { + if(!worldIsBeingDeleted) { + MyRenderer.onStopServer(); + } + } + + @Inject(method = "updateTimeLightAndEntities", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/NetworkSystem;networkTick()V")) + public void preServerTick(CallbackInfo ci) { + MyRenderer.serverTick(); + } + +} diff --git a/src/main/java/makamys/lodmod/mixin/MixinRenderGlobal.java b/src/main/java/makamys/lodmod/mixin/MixinRenderGlobal.java new file mode 100644 index 0000000..f2b89fd --- /dev/null +++ b/src/main/java/makamys/lodmod/mixin/MixinRenderGlobal.java @@ -0,0 +1,19 @@ +package makamys.lodmod.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import makamys.lodmod.renderer.MyRenderer; +import net.minecraft.client.renderer.RenderGlobal; + +@Mixin(RenderGlobal.class) +abstract class MixinRenderGlobal { + + @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(MyRenderer.renderWorld) { + thiz.renderAllRenderLists(p1, p2); + } + } +} diff --git a/src/main/java/makamys/lodmod/mixin/MixinTessellator.java b/src/main/java/makamys/lodmod/mixin/MixinTessellator.java new file mode 100644 index 0000000..544190d --- /dev/null +++ b/src/main/java/makamys/lodmod/mixin/MixinTessellator.java @@ -0,0 +1,161 @@ +package makamys.lodmod.mixin; + +import java.util.ArrayList; +import java.util.List; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import makamys.lodmod.ducks.ITessellator; +import makamys.lodmod.renderer.ChunkMesh; +import makamys.lodmod.renderer.MeshQuad; +import makamys.lodmod.renderer.MeshQuad.QuadPlaneComparator; +import net.minecraft.client.renderer.Tessellator; + +@Mixin(Tessellator.class) +abstract class MixinTessellator implements ITessellator { + + @Shadow + private int vertexCount; + + @Shadow + private int[] rawBuffer; + + @Shadow + private double xOffset; + @Shadow + private double yOffset; + @Shadow + private double zOffset; + + @Shadow + private boolean hasTexture; + @Shadow + private boolean hasBrightness; + @Shadow + private boolean hasColor; + @Shadow + private boolean hasNormals; + + private static int totalOriginalQuadCount = 0; + private static int totalSimplifiedQuadCount = 0; + /* + public static void endSave() { + if(out == null) { + return; + } + try { + out.close(); + String nameList = String.join("\n", ((TextureMap)Minecraft.getMinecraft().getTextureManager().getTexture(TextureMap.locationBlocksTexture)).mapUploadedSprites.keySet()); + Files.write(nameList, new File("tessellator_strings.txt"), Charset.forName("UTF-8")); + out = null; + } catch (IOException e) { + e.printStackTrace(); + } + }*/ + + public ChunkMesh toChunkMesh() { + if(this.vertexCount % 4 != 0) { + System.out.println("Error: Vertex count is not a multiple of 4"); + return null; + } + + List<MeshQuad> quads = new ArrayList<>(); + + List<Integer> spriteIndexes = new ArrayList<>(); + List<Byte> xs = new ArrayList<>(); + List<Byte> ys = new ArrayList<>(); + List<Byte> zs = new ArrayList<>(); + List<Byte> relUs = new ArrayList<>(); + List<Byte> relVs = new ArrayList<>(); + List<Byte> bUs = new ArrayList<>(); + List<Byte> bVs = new ArrayList<>(); + List<Integer> cs = new ArrayList<>(); + + for(int quadI = 0; quadI < this.vertexCount / 4; quadI++) { + MeshQuad quad = new MeshQuad(rawBuffer, quadI * 32, new ChunkMesh.Flags(hasTexture, hasBrightness, hasColor, hasNormals)); + 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); + } + boolean optimize = true; + if(optimize) { + 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(hasTexture, hasBrightness, hasColor, hasNormals), + quadCount, quads); + } else { + return null; + } + } + + private 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 int countValidQuads(List<MeshQuad> quads) { + int quadCount = 0; + for(MeshQuad quad : quads) { + if(!quad.deleted) { + quadCount++; + } + } + return quadCount; + } +} diff --git a/src/main/java/makamys/lodmod/mixin/MixinWorldRenderer.java b/src/main/java/makamys/lodmod/mixin/MixinWorldRenderer.java new file mode 100644 index 0000000..abfcc4e --- /dev/null +++ b/src/main/java/makamys/lodmod/mixin/MixinWorldRenderer.java @@ -0,0 +1,117 @@ +package makamys.lodmod.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 makamys.lodmod.ducks.ITessellator; +import makamys.lodmod.ducks.IWorldRenderer; +import makamys.lodmod.renderer.ChunkMesh; +import makamys.lodmod.renderer.FarChunkCache; +import makamys.lodmod.renderer.FarWorldRenderer; +import makamys.lodmod.renderer.MyRenderer; +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.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; + + public List<ChunkMesh> chunkMeshes = new ArrayList<>(); + + @Redirect(method = "setPosition", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/entity/Render;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) { + chunkMeshes.clear(); + } + + @Inject(method = "updateRenderer", at = @At(value = "TAIL")) + private void postUpdateRenderer(CallbackInfo ci) { + MyRenderer.onWorldRendererPost(WorldRenderer.class.cast(this)); + chunkMeshes.clear(); + } + + @Inject(method = "postRenderBlocks", at = @At(value = "HEAD")) + private void prePostRenderBlocks(CallbackInfo ci) { + chunkMeshes.add(((ITessellator)Tessellator.instance).toChunkMesh()); + } + + @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) { + MyRenderer.onDontDraw(WorldRenderer.class.cast(this)); + } + + @Override + public List<ChunkMesh> getChunkMeshes() { + return chunkMeshes; + } +} diff --git a/src/main/java/makamys/lodmod/renderer/ChunkMesh.java b/src/main/java/makamys/lodmod/renderer/ChunkMesh.java new file mode 100644 index 0000000..4175b9f --- /dev/null +++ b/src/main/java/makamys/lodmod/renderer/ChunkMesh.java @@ -0,0 +1,352 @@ +package makamys.lodmod.renderer; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.EOFException; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.ShortBuffer; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +import org.lwjgl.BufferUtils; + +import makamys.lodmod.ducks.IWorldRenderer; +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.tileentity.TileEntity; + +public class ChunkMesh extends Mesh { + + int x; + int y; + int z; + Flags flags; + + private ChunkMesh(int x, int y, int z, Flags flags, int quadCount, byte[] data, List<String> stringTable) { + this.x = x; + this.y = y; + this.z = z; + this.flags = flags; + this.quadCount = quadCount; + + this.buffer = createBuffer(data, stringTable); + } + + public ChunkMesh(int x, int y, int z, Flags flags, int quadCount, List<MeshQuad> quads) { + this.x = x; + this.y = y; + this.z = z; + this.flags = flags; + this.quadCount = quadCount; + + this.buffer = createBuffer(quads); + } + + private ByteBuffer createBuffer(List<MeshQuad> quads) { + 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; + } + + ByteBuffer buffer = BufferUtils.createByteBuffer(quadCount * 6 * getStride()); + FloatBuffer floatBuffer = buffer.asFloatBuffer(); + ShortBuffer shortBuffer = buffer.asShortBuffer(); + IntBuffer intBuffer = buffer.asIntBuffer(); + + try { + for(MeshQuad quad : quads) { + if(quad.deleted) { + continue; + } + String spriteName = quad.spriteName; + + 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 simpleX = quad.xs[vi]; + if(simpleX == 255) simpleX = 256; + int simpleY = quad.ys[vi]; + if(simpleY == 255) simpleY = 256; + int simpleZ = quad.zs[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 + + int relU = quad.relUs[vi]; + int relV = quad.relVs[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)quad.bUs[vi]); // bU + shortBuffer.put((short)quad.bVs[vi]); // bV + + intBuffer.position(shortBuffer.position() / 2); + + intBuffer.put(quad.cs[vi]); // c + + floatBuffer.position(intBuffer.position()); + } + } + } catch(Exception e) { + e.printStackTrace(); + } + + buffer.position(floatBuffer.position() * 4); + buffer.flip(); + + return buffer; + } + + 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); + + DataInputStream in = new DataInputStream(new ByteArrayInputStream(data)); + + 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(in, 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); + + intBuffer.put(readIntAt(in, colorOffset + 4 * 4 * quadI + 4 * vi)); // c + + floatBuffer.position(intBuffer.position()); + } + } + } catch(Exception e) { + e.printStackTrace(); + } + + buffer.position(floatBuffer.position() * 4); + buffer.flip(); + + return buffer; + } + + // 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 int readIntAt(DataInputStream in, int offset) { + try { + in.reset(); + in.skip(offset); + return in.readInt(); + } catch(IOException e) { + return -1; + } + } + + public int getStride() { + return (3 * 4 + (flags.hasTexture ? 8 : 0) + (flags.hasBrightness ? 4 : 0) + (flags.hasColor ? 4 : 0) + (flags.hasNormals ? 4 : 0)); + } + + static ChunkMesh loadChunkMesh(int x, int y, int z) { + try(DataInputStream in = new DataInputStream(new FileInputStream("tessellator_dump.dat"))){ + List<String> stringTable = Files.readAllLines(Paths.get("tessellator_strings.txt")); + + while(true) { + int xOffset = in.readInt(); + int yOffset = in.readInt(); + int zOffset = in.readInt(); + Flags flags = new Flags(in.readByte()); + + int quadSize = 2 + 4 * (3 + (flags.hasTexture ? 2 : 0) + (flags.hasBrightness ? 2 : 0) + + (flags.hasColor ? 4 : 0) + (flags.hasNormals ? 4 : 0)); + + + int quadCount = in.readInt(); + + if(xOffset == x && yOffset == y && zOffset == z) { + byte[] data = new byte[quadSize * quadCount]; + in.read(data, 0, data.length); + + return new ChunkMesh(x, y, z, flags, quadCount, data, stringTable); + } else { + in.skip(quadSize * quadCount); + } + } + } catch(EOFException eof) { + System.out.println("end??"); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + // TODO don't repeat yourself + static List<ChunkMesh> loadAll(){ + List<ChunkMesh> list = new ArrayList<ChunkMesh>(); + try(DataInputStream in = new DataInputStream(new FileInputStream("tessellator_dump.dat"))){ + List<String> stringTable = Files.readAllLines(Paths.get("tessellator_strings.txt")); + + while(true) { + int xOffset = in.readInt(); + int yOffset = in.readInt(); + int zOffset = in.readInt(); + Flags flags = new Flags(in.readByte()); + + int quadSize = 2 + 4 * (3 + (flags.hasTexture ? 2 : 0) + (flags.hasBrightness ? 2 : 0) + + (flags.hasColor ? 4 : 0) + (flags.hasNormals ? 4 : 0)); + + + int quadCount = in.readInt(); + + if(quadCount > 0) { + byte[] data = new byte[quadSize * quadCount]; + in.read(data, 0, data.length); + + list.add(new ChunkMesh(xOffset, yOffset, zOffset, flags, quadCount, data, stringTable)); + } + } + } catch(EOFException eof) { + System.out.println("end??"); + } catch (IOException e) { + e.printStackTrace(); + } + return list; + } + + 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 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/lodmod/renderer/FarChunkCache.java b/src/main/java/makamys/lodmod/renderer/FarChunkCache.java new file mode 100644 index 0000000..f4e3cca --- /dev/null +++ b/src/main/java/makamys/lodmod/renderer/FarChunkCache.java @@ -0,0 +1,12 @@ +package makamys.lodmod.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/lodmod/renderer/FarWorldRenderer.java b/src/main/java/makamys/lodmod/renderer/FarWorldRenderer.java new file mode 100644 index 0000000..72eb4fd --- /dev/null +++ b/src/main/java/makamys/lodmod/renderer/FarWorldRenderer.java @@ -0,0 +1,14 @@ +package makamys.lodmod.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/lodmod/renderer/LODChunk.java b/src/main/java/makamys/lodmod/renderer/LODChunk.java new file mode 100644 index 0000000..bae235c --- /dev/null +++ b/src/main/java/makamys/lodmod/renderer/LODChunk.java @@ -0,0 +1,67 @@ +package makamys.lodmod.renderer; + +import java.util.List; + +import net.minecraft.entity.Entity; +import net.minecraft.world.chunk.Chunk; + +public class LODChunk { + + int x, z; + Chunk chunk; + public boolean waitingForData = false; + int lod = 0; + boolean visible; + + SimpleChunkMesh simpleMesh; + ChunkMesh[] chunkMeshes = new ChunkMesh[32]; + + public LODChunk(int x, int z) { + this.x = x; + this.z = z; + } + + @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++) { + if(chunkMeshes[cy * 2 + i] != null) { + MyRenderer.setMeshVisible(chunkMeshes[cy * 2 + i], false); + chunkMeshes[cy * 2 + i] = null; + } + } + + for(int i = 0; i < newChunkMeshes.size(); i++) { + chunkMeshes[cy * 2 + i] = newChunkMeshes.get(i); + MyRenderer.sendMeshToGPU(newChunkMeshes.get(i)); + } + } + + public boolean hasChunkMeshes() { + for(ChunkMesh cm : chunkMeshes) { + if(cm != null) { + return true; + } + } + return false; + } + + public void tick(Entity player) { + double distSq = distSq(player); + if(distSq < Math.pow(32 * 16, 2)) { + MyRenderer.setLOD(this, 2); + } else if(distSq < Math.pow(64 * 16, 2)) { + MyRenderer.setLOD(this, 1); + } else { + MyRenderer.setVisible(this, false); + } + } + +} diff --git a/src/main/java/makamys/lodmod/renderer/LODRegion.java b/src/main/java/makamys/lodmod/renderer/LODRegion.java new file mode 100644 index 0000000..695202d --- /dev/null +++ b/src/main/java/makamys/lodmod/renderer/LODRegion.java @@ -0,0 +1,62 @@ +package makamys.lodmod.renderer; + +import net.minecraft.entity.Entity; +import net.minecraft.world.chunk.Chunk; + +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 static LODRegion load(int regionX, int regionZ) { + return new LODRegion(regionX, regionZ); // TODO + } + + 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].chunk = chunk; + data[relX][relZ].waitingForData = false; + return data[relX][relZ]; + } + return null; + } + + public void tick(Entity player) { + 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); + } + } + } + } + +} diff --git a/src/main/java/makamys/lodmod/renderer/Mesh.java b/src/main/java/makamys/lodmod/renderer/Mesh.java new file mode 100644 index 0000000..be379f6 --- /dev/null +++ b/src/main/java/makamys/lodmod/renderer/Mesh.java @@ -0,0 +1,15 @@ +package makamys.lodmod.renderer; + +import java.nio.ByteBuffer; +import java.nio.IntBuffer; + +public abstract class Mesh { + + public ByteBuffer buffer; + public int quadCount; + public boolean visible; + public int iFirst, iCount; + + public abstract int getStride(); + +} diff --git a/src/main/java/makamys/lodmod/renderer/MeshQuad.java b/src/main/java/makamys/lodmod/renderer/MeshQuad.java new file mode 100644 index 0000000..a48815d --- /dev/null +++ b/src/main/java/makamys/lodmod/renderer/MeshQuad.java @@ -0,0 +1,424 @@ +package makamys.lodmod.renderer; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Comparator; +import java.util.Locale; +import java.util.Map; + +import makamys.lodmod.renderer.MeshQuad.QuadPlaneComparator; +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 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) { + 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 = MyRenderer.getSpriteIndexForUV(avgU, avgV); + sprite = MyRenderer.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]); + float y = Float.intBitsToFloat(rawBuffer[i + 1]); + float z = Float.intBitsToFloat(rawBuffer[i + 2]); + + 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/lodmod/renderer/MyRenderer.java b/src/main/java/makamys/lodmod/renderer/MyRenderer.java new file mode 100644 index 0000000..a1ce086 --- /dev/null +++ b/src/main/java/makamys/lodmod/renderer/MyRenderer.java @@ -0,0 +1,748 @@ +package makamys.lodmod.renderer; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.particle.EntityFX; +import net.minecraft.client.renderer.WorldRenderer; +import net.minecraft.client.renderer.texture.ITextureObject; +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.tileentity.TileEntity; +import net.minecraft.util.ChunkCoordinates; +import net.minecraft.util.ResourceLocation; +import net.minecraft.world.ChunkCoordIntPair; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.gen.ChunkProviderServer; + +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.file.Files; +import java.nio.file.Paths; +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.ArrayBlockingQueue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.SynchronousQueue; +import java.util.stream.Collectors; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.lwjgl.BufferUtils; +import org.lwjgl.Sys; +import org.lwjgl.input.Keyboard; +import org.lwjgl.input.Mouse; +import org.lwjgl.opengl.ARBDebugOutputCallback; +import org.lwjgl.opengl.ARBShaderObjects; +import org.lwjgl.opengl.Display; +import org.lwjgl.opengl.DisplayMode; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GLContext; +import org.lwjgl.util.vector.Matrix4f; + +import makamys.lodmod.ducks.IWorldRenderer; +import makamys.lodmod.util.Util; + +import static org.lwjgl.opengl.ARBBufferObject.*; +import static org.lwjgl.opengl.ARBVertexBufferObject.*; +import static org.lwjgl.opengl.GL11.*; +import static org.lwjgl.opengl.GL13.*; +import static org.lwjgl.opengl.GL14.*; +import static org.lwjgl.opengl.GL15.*; +import static org.lwjgl.opengl.GL20.*; +import static org.lwjgl.opengl.GL30.*; +import static org.lwjgl.util.glu.GLU.*; + +public class MyRenderer { + + private static boolean hasInited = false; + + private static boolean[] wasDown = new boolean[256]; + private static int renderQuads = 0; + + public static boolean renderWorld = true; + public static boolean renderLOD = true; + + //private static int[] spriteIndexMap; + public static List<TextureAtlasSprite> sprites; + + static private Map<Long, Integer> uv2spriteIndex = new HashMap<>(); + + //static ChunkMesh mesh; + + private static void destroy() { + glDeleteProgram(shaderProgram); + glDeleteVertexArrays(VAO); + glDeleteBuffers(VBO); + } + + private static void mainLoop() { + if(Keyboard.isKeyDown(Keyboard.KEY_F) && !wasDown[Keyboard.KEY_F]) { + renderLOD = !renderLOD; + } + if(Keyboard.isKeyDown(Keyboard.KEY_V) && !wasDown[Keyboard.KEY_V]) { + renderWorld = !renderWorld; + } + if(hasInited) { + /*if(Keyboard.isKeyDown(Keyboard.KEY_X) && !wasDown[Keyboard.KEY_X]) { + renderQuads--; + if(renderQuads < 0) { + renderQuads = 0; + } + } + if(Keyboard.isKeyDown(Keyboard.KEY_C) && !wasDown[Keyboard.KEY_C]) { + renderQuads++; + if(renderQuads > mesh.quadCount) { + renderQuads = mesh.quadCount; + } + }*/ + } + + + for(int i = 0; i < 256; i++) { + wasDown[i] = Keyboard.isKeyDown(i); + } + } + + private static void render() { + GL11.glPushAttrib(GL11.GL_ENABLE_BIT); + //GL11.glDisable(GL11.GL_CULL_FACE); + //GL11.glDisable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_TEXTURE_2D); + + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + //GL11.glColor4f(0, 1, 0, 1); // change this for your colour + //GL11.glLineWidth(9.0F); + //GL11.glDepthMask(false); + float z = 0; + + /*GL11.glBegin(GL11.GL_TRIANGLES); + GL11.glVertex3f(-0.9f,-0.9f,0); + GL11.glVertex3f(0.9f,0.9f,0); + GL11.glVertex3f(-0.9f,0.9f,0); + GL11.glEnd();*/ + + //glClear(GL_COLOR_BUFFER_BIT); + + 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 { + FloatBuffer modelView = BufferUtils.createFloatBuffer(16); + glGetFloat(GL_MODELVIEW_MATRIX, modelView); + + FloatBuffer projBuf = BufferUtils.createFloatBuffer(16); + glGetFloat(GL_PROJECTION_MATRIX, projBuf); + //modelView.flip(); + + IntBuffer viewportBuf = BufferUtils.createIntBuffer(16); + glGetInteger(GL_VIEWPORT, viewportBuf); + viewportBuf.limit(4); + + FloatBuffer projInvBuf = BufferUtils.createFloatBuffer(16); + Matrix4f m = new Matrix4f(); + m.load(projBuf); + projBuf.flip(); + m.invert(); + m.store(projInvBuf); + projInvBuf.flip(); + + FloatBuffer fogColorBuf = BufferUtils.createFloatBuffer(16); + glGetFloat(GL_FOG_COLOR, fogColorBuf); + fogColorBuf.limit(4); + + FloatBuffer fogStartEnd = BufferUtils.createFloatBuffer(2); + 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); + //glUniform4(u_viewport, viewportBuf); + 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; + + glUniform3f(u_playerPos, (float)EntityFX.interpPosX - originX, (float)EntityFX.interpPosY - originY, (float)EntityFX.interpPosZ - originZ); + + glUniform1i(u_light, 1); + } + + glBindVertexArray(VAO); + //glEnableVertexAttribArray(0); + //glDrawArrays(GL_TRIANGLES, 0, 3); + //glActiveTexture(GL_TEXTURE1); + //ITextureObject hmm = Minecraft.getMinecraft().getTextureManager().getTexture(new ResourceLocation("dynamic/lightMap_1")); + //glBindTexture(GL_TEXTURE_2D, hmm.getGlTextureId()); + //glDrawArrays(GL_TRIANGLES, 0, 6*renderQuads); + glMultiDrawArrays(GL_TRIANGLES, piFirst, piCount); + + //glDisableVertexAttribArray(0); + glBindVertexArray(0); + glUseProgram(0); + + GL11.glDepthMask(true); + GL11.glPopAttrib(); + + + } + + private static int VAO, VBO, EBO, shaderProgram; + private static IntBuffer piFirst, piCount; + + private static String readFile(String path){ + try { + return new String(Files.readAllBytes(Util.getResourcePath(path))); + } catch (IOException e) { + e.printStackTrace(); + } + return ""; + } + + private static byte[] byteBufferToArray(ByteBuffer buffer) { + byte[] dst = new byte[buffer.remaining()]; + buffer.get(dst); + buffer.flip(); + return dst; + } + + private static int[] intBufferToArray(IntBuffer buffer) { + int[] dst = new int[buffer.remaining()]; + buffer.get(dst); + buffer.flip(); + return dst; + } + + 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; + } + } + + private static boolean init() { + Map<String, TextureAtlasSprite> uploadedSprites = ((TextureMap)Minecraft.getMinecraft().getTextureManager().getTexture(TextureMap.locationBlocksTexture)).mapUploadedSprites; + sprites = uploadedSprites.values().stream().collect(Collectors.toList()); + + int vertexShader; + vertexShader = glCreateShader(GL_VERTEX_SHADER); + + glShaderSource(vertexShader, 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, readFile("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); + + VAO = glGenVertexArrays(); + glBindVertexArray(VAO); + + VBO = glGenBuffers(); + glBindBuffer(GL_ARRAY_BUFFER, VBO); + + EBO = glGenBuffers(); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); + boolean rough = false; + + /* + List<Integer> chunkCoords = new ArrayList<>();*/ + /*chunkCoords.add(-19); + chunkCoords.add(4); + chunkCoords.add(15);*/ + /*List<Mesh> meshes = new ArrayList<Mesh>(); + for(Chunk chunk : myChunks) { + Entity player = Minecraft.getMinecraft().thePlayer; + int minRadius = 0; + int radius = 10; + if(minRadius <= Math.abs(chunk.xPosition - player.chunkCoordX) && Math.abs(chunk.xPosition - player.chunkCoordX) < radius && minRadius <= Math.abs(chunk.zPosition - player.chunkCoordZ) && Math.abs(chunk.zPosition - player.chunkCoordZ) < radius) { + if(!rough) { + for(int y = 0; y < 16; y++) { + chunkCoords.add(chunk.xPosition); + chunkCoords.add(y); + chunkCoords.add(chunk.zPosition); + } + } else { + meshes.add(new SimpleChunkMesh(chunk)); + } + } + + } + long t0 = System.nanoTime(); + if(!rough) { + ChunkMesh.saveChunks(chunkCoords); + + meshes.addAll(ChunkMesh.loadAll()); + } + System.out.println("loaded " + meshes.size() + " cchunks in " + ((System.nanoTime() - t0) / 1000000000f) + "s"); + //meshes = meshes.subList(2, 3); + int bufferSize = 0; + int quadCount = 0; + Mesh firstMesh = meshes.get(0); + int stride = -1; + for(Mesh mesh : meshes) { + bufferSize += mesh.buffer.limit(); + quadCount += mesh.quadCount; + } + + if(Runtime.getRuntime().freeMemory() < BUFFER_SIZE) { + System.out.println("not enough memory"); + // TODO optimize memory usage + return false; + } + ByteBuffer buffer = BufferUtils.createByteBuffer(BUFFER_SIZE); + + for(Mesh mesh : meshes) { + int verticesSoFar = buffer.position() / firstMesh.getStride(); + buffer.put(mesh.buffer); + } + buffer.flip();*/ + + glBufferData(GL_ARRAY_BUFFER, BUFFER_SIZE, GL_STATIC_DRAW); + + 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); + + //renderQuads = quadCount; + + int nextTri = 0; + piFirst = BufferUtils.createIntBuffer(MAX_MESHES); + piFirst.flip(); + piCount = BufferUtils.createIntBuffer(MAX_MESHES); + piCount.flip(); + /*for(int i = 0; i < meshes.size(); i++) { + Mesh mesh = meshes.get(i); + + piFirst.put(nextTri); + piCount.put(mesh.quadCount * 6); + nextTri += mesh.quadCount * 6; + }*/ + + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); + //glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + + return true; + } + + public static boolean doRender = true; + private static boolean hasInited2 = false; + + private static int BUFFER_SIZE = 1024 * 1024 * 1024; + private static int MAX_MESHES = 100000; + + public static void beforeRenderTerrain() { + init2(); + + if(doRender) { + mainLoop(); + if(hasInited && renderLOD) { + render(); + } + } + } + + public static void onWorldRendererPost(WorldRenderer wr) { + LODChunk lodChunk = getLODChunk(Math.floorDiv(wr.posX, 16), Math.floorDiv(wr.posZ, 16)); + lodChunk.putChunkMeshes(Math.floorDiv(wr.posY, 16), ((IWorldRenderer)wr).getChunkMeshes()); + setVisible(lodChunk, false); + } + + public static void onDontDraw(WorldRenderer wr) { + int chunkX = Math.floorDiv(wr.posX, 16); + int chunkY = Math.floorDiv(wr.posY, 16); + int chunkZ = Math.floorDiv(wr.posZ, 16); + + Entity player = (Entity)Minecraft.getMinecraft().getIntegratedServer().getConfigurationManager().playerEntityList.get(0); + LODChunk lodChunk = getLODChunk(chunkX, chunkZ); + + if(lodChunk.hasChunkMeshes()) { + setLOD(lodChunk, 2);//(lodChunk.distSq(player) < (16 * 16 * 16 * 16)) ? 1 : 2); + } + setVisible(lodChunk, true); + } + + private static void init2() { + if(!hasInited2) { + hasInited = init(); + hasInited2 = true; + } else { + while(!farChunks.isEmpty()) { + LODChunk lodChunk = receiveFarChunk(farChunks.remove()); + sendChunkToGPU(lodChunk); + } + + List<Object> players = Minecraft.getMinecraft().getIntegratedServer().getConfigurationManager().playerEntityList; + if(!players.isEmpty()) { + Entity player = (Entity)players.get(0); + + 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); + + int range = 64; + for(int x = -range; x <= range; x++) { + for(int z = -range; z <= range; z++) { + int chunkX = centerX + x; + int chunkZ = centerZ + z; + + if(getLODChunk(chunkX, chunkZ).chunk == null) { + newServerChunkLoadQueue.add(new ChunkCoordIntPair(chunkX, chunkZ)); + } + } + } + Collections.sort(newServerChunkLoadQueue, new ChunkCoordDistanceComparator(player)); + setServerChunkLoadQueue(newServerChunkLoadQueue); + hasInited2 = true; + + lastSortX = player.posX; + lastSortY = player.posY; + lastSortZ = player.posZ; + + loadedRegionsMap.forEach((k, v) -> v.tick(player)); + } + } + } + } + + private static double getLastSortDistanceSq(Entity player) { + return Math.pow(lastSortX - player.posX, 2) + Math.pow(lastSortZ - player.posZ, 2); + } + + private static synchronized void setServerChunkLoadQueue(List<ChunkCoordIntPair> coords) { + serverChunkLoadQueue = coords; + } + + private static LODChunk receiveFarChunk(Chunk chunk) { + LODRegion region = getRegionContaining(chunk.xPosition, chunk.zPosition); + myChunks.add(chunk); + return region.putChunk(chunk); + } + + private static LODChunk getLODChunk(int chunkX, int chunkZ) { + return getRegionContaining(chunkX, chunkZ).getChunkAbsolute(chunkX, chunkZ); + } + + static List<Chunk> myChunks = new ArrayList<Chunk>(); + static List<LODChunk> pendingLODChunks = new ArrayList<>(); + + private static boolean hasServerInited = false; + private static HashMap<ChunkCoordIntPair, LODRegion> loadedRegionsMap = new HashMap<>(); + + // TODO make these packets to make this work on dedicated servers + static Queue<Chunk> farChunks = new ConcurrentLinkedQueue<>(); + + static List<ChunkCoordIntPair> serverChunkLoadQueue = new ArrayList<>(); + + private static double lastSortX = Double.NaN; + private static double lastSortY = Double.NaN; + private static double lastSortZ = Double.NaN; + + public static void onStopServer() { + + } + + public static synchronized void serverTick() { + int chunkLoadsRemaining = 64; + while(!serverChunkLoadQueue.isEmpty() && chunkLoadsRemaining-- > 0) { + ChunkCoordIntPair coords = serverChunkLoadQueue.remove(0); + ChunkProviderServer chunkProviderServer = Minecraft.getMinecraft().getIntegratedServer().worldServers[0].theChunkProviderServer; + Chunk chunk = chunkProviderServer.currentChunkProvider.provideChunk(coords.chunkXPos, coords.chunkZPos); + farChunks.add(chunk); + } + } + + private static 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(Math.floorDiv(chunkX , 32), Math.floorDiv(chunkZ , 32)); + loadedRegionsMap.put(key, region); + } + return region; + } + + private static void loadChunk(int chunkX, int chunkZ) { + LODRegion region = getRegionContaining(chunkX, chunkZ); + LODChunk lodChunk = region.getChunkAbsolute(chunkX, chunkZ); + if(lodChunk == null) { + ChunkProviderServer chunkProviderServer = Minecraft.getMinecraft().getIntegratedServer().worldServers[0].theChunkProviderServer; + //Chunk chunk = chunkProviderServer.loadChunk(chunkX, chunkZ); + Chunk chunk = chunkProviderServer.currentChunkProvider.provideChunk(chunkX, chunkZ); + /*Chunk chunk = chunkProviderServer.safeLoadChunk(chunkX, chunkZ); + if(chunk == null) { + chunk = chunkProviderServer.currentChunkProvider.provideChunk(chunkX, chunkZ); + } + if(chunk != null) { + chunk.populateChunk(chunkProviderServer, chunkProviderServer, chunkX, chunkZ); + myChunks.add(chunk); + }*/ + if(chunk != null) { + myChunks.add(chunk); + } + //lodChunk = region.putChunk(new LODChunk(chunk)); + } + sendChunkToGPU(lodChunk); + } + + private static void sendChunkToGPU(LODChunk lodChunk) { + lodChunk.simpleMesh = new SimpleChunkMesh(lodChunk.chunk); + /*for(int cy = 0; cy < 16; cy++) { + lodChunk.chunkMeshes[cy] = ChunkMesh.getChunkMesh(lodChunk.x, cy, lodChunk.z); + sendMeshToGPU(lodChunk.chunkMeshes[cy]); + }*/ + + sendMeshToGPU(lodChunk.simpleMesh); + + Entity player = (Entity) Minecraft.getMinecraft().getIntegratedServer().getConfigurationManager().playerEntityList.get(0); + + setLOD(lodChunk, 1);//lodChunk.distSq(player) < 16 * 16 * 16 * 16 ? 2 : 1); + setVisible(lodChunk, true); + } + + public static void setLOD(LODChunk lodChunk, int lod) { + if(lod == lodChunk.lod) return; + + lodChunk.lod = lod; + lodChunkChanged(lodChunk); + } + + public static void setVisible(LODChunk lodChunk, boolean visible) { + if(visible == lodChunk.visible) return; + + lodChunk.visible = visible; + lodChunkChanged(lodChunk); + } + + public static void lodChunkChanged(LODChunk lodChunk) { + int newLOD = (!lodChunk.hasChunkMeshes() && lodChunk.lod == 2) ? 1 : lodChunk.lod; + if(lodChunk.simpleMesh != null) { + if(lodChunk.visible && newLOD == 1) { + if(!lodChunk.simpleMesh.visible) { + setMeshVisible(lodChunk.simpleMesh, true); + } + } else { + if(lodChunk.simpleMesh.visible) { + setMeshVisible(lodChunk.simpleMesh, false); + } + } + } + for(ChunkMesh cm : lodChunk.chunkMeshes) { + if(cm != null) { + if(lodChunk.visible && newLOD == 2) { + if(!cm.visible) { + setMeshVisible(cm, true); + } + } else { + if(cm.visible) { + setMeshVisible(cm, false); + } + } + } + } + } + + private static int nextTri; + private static int nextMeshOffset; + private static int nextMesh; + + protected static void sendMeshToGPU(Mesh mesh) { + if(mesh == null) { + return; + } + glBindVertexArray(VAO); + glBindBuffer(GL_ARRAY_BUFFER, VBO); + + glBufferSubData(GL_ARRAY_BUFFER, nextMeshOffset, mesh.buffer); + mesh.iFirst = nextTri; + mesh.iCount = mesh.quadCount * 6; + + nextTri += mesh.quadCount * 6; + nextMeshOffset += mesh.buffer.limit(); + + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); + } + + protected static void setMeshVisible(Mesh mesh, boolean visible) { + if(mesh == null) return; + + if(mesh.visible != visible) { + if(!visible) { + piFirst.position(0); + int[] piFirstArr = new int[piFirst.limit()]; + piFirst.get(piFirstArr); + int index = ArrayUtils.indexOf(piFirstArr, mesh.iFirst); + piFirstArr = ArrayUtils.remove(piFirstArr, index); + piFirst.position(0); + piFirst.put(piFirstArr); + piFirst.position(0); + piFirst.limit(piFirst.limit() - 1); + + piCount.position(0); + int[] piCountArr = new int[piCount.limit()]; + piCount.get(piCountArr); + piCountArr = ArrayUtils.remove(piCountArr, index); + piCount.position(0); + piCount.put(piCountArr); + piCount.position(0); + piCount.limit(piCount.limit() - 1); + nextMesh--; + } else if(visible) { + piFirst.limit(piFirst.limit() + 1); + piFirst.put(nextMesh, mesh.iFirst); + piCount.limit(piCount.limit() + 1); + piCount.put(nextMesh, mesh.iCount); + nextMesh++; + } + mesh.visible = visible; + } + } + + public static Chunk getChunkFromChunkCoords(int x, int z) { + for(Chunk chunk : myChunks) { + if(chunk.xPosition == x && chunk.zPosition == z) { + return chunk; + } + } + return null; + } + + 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> { + Entity player; + + public ChunkCoordDistanceComparator(Entity player) { + this.player = player; + } + + @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) - player.posX), 2) + + Math.pow(((p.chunkZPos * 16) - player.posZ), 2) + ); + } + } +} diff --git a/src/main/java/makamys/lodmod/renderer/SimpleChunkMesh.java b/src/main/java/makamys/lodmod/renderer/SimpleChunkMesh.java new file mode 100644 index 0000000..190a398 --- /dev/null +++ b/src/main/java/makamys/lodmod/renderer/SimpleChunkMesh.java @@ -0,0 +1,150 @@ +package makamys.lodmod.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 org.lwjgl.BufferUtils; + +import net.minecraft.block.Block; +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.util.IIcon; +import net.minecraft.world.chunk.Chunk; + +public class SimpleChunkMesh extends Mesh { + + private FloatBuffer vertices; + + public SimpleChunkMesh(Chunk target) { + int divisions = 4; + quadCount = divisions * divisions * 5; + + buffer = BufferUtils.createByteBuffer(4 * 6 * 7 * quadCount); + vertices = buffer.asFloatBuffer(); + + 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; + for(y = 255; y > 0; y--) { + int xOff = divX * size; + int zOff = divZ * size; + Block block = target.getBlock(xOff, y, zOff); + + float offX = target.xPosition * 16 + divX * size; + float offY = y; + float offZ = target.zPosition * 16 + divZ * size; + + if(!foundWater && block.getMaterial() == Material.water) { + // TODO just add a face here, and keep the seabed + foundWater = true; + int meta = target.getBlockMetadata(xOff, y, zOff); + IIcon waterIcon = block.getIcon(1, meta); + int waterColor = block.colorMultiplier(Minecraft.getMinecraft().theWorld, target.xPosition * 16 + xOff, y, target.zPosition * 16 + zOff); + waterColor |= 0xFF000000; + + addCube(offX, offY, offZ, size, size, size*4, waterIcon, waterColor); + break; + } + + if(block.isBlockNormalCube() && block.isOpaqueCube() && block.renderAsNormalBlock()) { + int meta = target.getBlockMetadata(xOff, y, zOff); + icon = block.getIcon(1, meta); + color = block.colorMultiplier(Minecraft.getMinecraft().theWorld, target.xPosition * 16 + xOff, y, target.zPosition * 16 + zOff); + color = (0xFF << 24) | ((color >> 16 & 0xFF) << 0) | ((color >> 8 & 0xFF) << 8) | ((color >> 0 & 0xFF) << 16); + + addCube(offX, offY, offZ, size, size, offY, icon, color); + break; + } + } + } + } + vertices.flip(); + } + + private void addCube(float x, float y, float z, float sizeX, float sizeZ, float sizeY, IIcon icon, int color) { + 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, 240 + ); + 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, 180 + ); + addFace( + x + sizeX, y - sizeY, z + sizeZ, + x + sizeX, y + 0, z + sizeZ, + x + 0, y + 0, z + sizeZ, + x + 0, y - sizeY, z + sizeZ, + icon, color, 180 + ); + addFace( + x + sizeX, y - sizeY, z + 0, + x + sizeX, y + 0, z + 0, + x + sizeX, y + 0, z + sizeZ, + x + sizeX, y - sizeY, z + sizeZ, + icon, color, 120 + ); + 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, 120 + ); + } + + 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); + + } + + public int getStride() { + return (3 * 4 + 8 + 4 + 4); + } + +} diff --git a/src/main/java/makamys/lodmod/util/Util.java b/src/main/java/makamys/lodmod/util/Util.java new file mode 100644 index 0000000..92cc5cd --- /dev/null +++ b/src/main/java/makamys/lodmod/util/Util.java @@ -0,0 +1,30 @@ +package makamys.lodmod.util; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.nio.file.FileSystems; +import java.nio.file.Path; + +public class Util { + public static Path getResourcePath(String relPath) { + 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; + } + } +} diff --git a/src/main/resources/META-INF/LODMod_at.cfg b/src/main/resources/META-INF/LODMod_at.cfg new file mode 100644 index 0000000..ff15140 --- /dev/null +++ b/src/main/resources/META-INF/LODMod_at.cfg @@ -0,0 +1,2 @@ +public net.minecraft.client.renderer.texture.TextureMap field_94252_e #mapUploadedSprites +public net.minecraft.client.renderer.Tessellator func_78379_d()V #reset
\ No newline at end of file diff --git a/src/main/resources/lodmod.mixin.json b/src/main/resources/lodmod.mixin.json index 8ded54d..d8bfed1 100644 --- a/src/main/resources/lodmod.mixin.json +++ b/src/main/resources/lodmod.mixin.json @@ -5,8 +5,13 @@ "refmap": "lodmod.mixin.refmap.json", "compatibilityLevel": "JAVA_8", "mixins": [ - "MixinSoundManager", + "MixinChunkCache", "MixinEntityRenderer", - "IRenderGlobal" + "MixinMinecraftServer", + "MixinRenderGlobal", + "MixinSoundManager", + "MixinSoundManager", + "MixinTessellator", + "MixinWorldRenderer" ] } diff --git a/src/main/resources/shaders/chunk.frag b/src/main/resources/shaders/chunk.frag new file mode 100644 index 0000000..ae187cb --- /dev/null +++ b/src/main/resources/shaders/chunk.frag @@ -0,0 +1,61 @@ +#version 330 core +out vec4 FragColor; + +in vec2 TexCoord; +in vec2 BTexCoord; +in vec4 DaColor; +in vec4 Viewport; +in mat4 ProjInv; +in vec4 FogColor; +in vec2 FogStartEnd; + +uniform sampler2D atlas; +uniform sampler2D lightTex; + +void main() +{ + //FragColor = texture(lightTex, TexCoord); + //FragColor = texture(lightTex, vec2(8.0/256.0,8.0/256.0)); + //FragColor = texture(lightTex, vec2(0, 0)); + //FragColor = vec4(texture(lightTex, vec2(0, 0)).xyz,1); + //FragColor = vec4(BTexCoord.xy / 256, 0, 1); + //FragColor = texture(lightTex, (BTexCoord + 8.0) / 256.0); + //FragColor = texture(light, vec2(16,16)); + //FragColor = vec4(1,0,0,1); + //FragColor = DaColor/256.0; + + vec4 texColor = texture(atlas, TexCoord); + vec4 colorMult = DaColor/256.0; + + vec4 lightyColor = texture(lightTex, (BTexCoord + 8.0) / 256.0); + //vec4 lightyMultColor = vec4(lightyColor.x * colorMult.x, lightyColor.y * colorMult.y, lightyColor.z * colorMult.z, lightyColor.w * colorMult.w); + + //FragColor = vec4(texColor.x * colorMult.x, texColor.y * colorMult.y, texColor.z * colorMult.z, texColor.w * colorMult.w); + //vec4 texMultColor = texColor * colorMult; + + //FragColor = texMultColor;// + (lightyColor - vec4(0.5,0.5,0.5,0.0)); + //FragColor = vec4(lightyColor.xyz, 1); + //FragColor = ((texColor * colorMult) * lightyColor); + vec4 rasterColor = ((texColor * colorMult) * lightyColor); + float s = FogStartEnd.x; + float e = FogStartEnd.y; + + vec4 ndcPos; + ndcPos.xy = ((2.0 * gl_FragCoord.xy) - (2.0 * Viewport.xy)) / (Viewport.zw) - 1; + ndcPos.z = (2.0 * gl_FragCoord.z - gl_DepthRange.near - gl_DepthRange.far) / + (gl_DepthRange.far - gl_DepthRange.near); + ndcPos.w = 1.0; + + vec4 clipPos = ndcPos / gl_FragCoord.w; + vec4 eyePos = ProjInv * clipPos; + + float z = length(eyePos); + /* + if(z < 200){ + discard; + } + */ + float f = (e - z) / (e - s); + f = clamp(f, 0, 1); + FragColor = mix(FogColor, rasterColor, f); +}
\ No newline at end of file diff --git a/src/main/resources/shaders/chunk.vert b/src/main/resources/shaders/chunk.vert new file mode 100644 index 0000000..3f2ad1c --- /dev/null +++ b/src/main/resources/shaders/chunk.vert @@ -0,0 +1,34 @@ +#version 330 core +layout (location = 0) in vec3 aPos; +layout (location = 1) in vec2 aTexCoord; +layout (location = 2) in vec2 aBTexCoord; +layout (location = 3) in vec4 aDaColor; + +uniform mat4 modelView; +uniform mat4 proj; +uniform mat4 projInv; +uniform vec4 viewport; +uniform vec4 fogColor; +uniform vec2 fogStartEnd; + +uniform vec3 playerPos; + +out vec2 TexCoord; +out vec2 BTexCoord; +out vec4 DaColor; +out vec4 Viewport; +out mat4 ProjInv; +out vec4 FogColor; +out vec2 FogStartEnd; + +void main() +{ + gl_Position = proj * modelView * vec4(aPos - playerPos, 1.0); + TexCoord = aTexCoord; + BTexCoord = aBTexCoord; + DaColor = aDaColor; + Viewport = viewport; + ProjInv = projInv; + FogColor = fogColor; + FogStartEnd = fogStartEnd; +}
\ No newline at end of file |