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.TextureAtlasSprite; import net.minecraft.client.renderer.texture.TextureMap; import net.minecraft.entity.Entity; import net.minecraft.world.ChunkCoordIntPair; import net.minecraft.world.World; import net.minecraft.world.chunk.Chunk; import net.minecraft.world.gen.ChunkProviderServer; 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.lodmod.ducks.IWorldRenderer; import makamys.lodmod.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 { private boolean hasInited = false; private boolean[] wasDown = new boolean[256]; private int renderQuads = 0; public boolean renderWorld = true; public boolean renderLOD = true; private static int BUFFER_SIZE = 1024 * 1024 * 1024; private static int MAX_MESHES = 100000; private int VAO, VBO, shaderProgram; private IntBuffer piFirst, piCount; List myChunks = new ArrayList(); List pendingLODChunks = new ArrayList<>(); private boolean hasServerInited = false; private Map loadedRegionsMap = new HashMap<>(); private List sentMeshes = new ArrayList<>(); // TODO make these packets to make this work on dedicated servers Queue farChunks = new ConcurrentLinkedQueue<>(); List serverChunkLoadQueue = new ArrayList<>(); private double lastSortX = Double.NaN; private double lastSortY = Double.NaN; private double lastSortZ = Double.NaN; private long lastGCTime = -1; private long gcInterval = 60 * 1000; public LODRenderer(){ hasInited = init(); } public void beforeRenderTerrain() { if(hasInited) { mainLoop(); handleKeyboard(); gcInterval = 10 * 1000; if(lastGCTime == -1 || (System.currentTimeMillis() - lastGCTime) > gcInterval) { runGC(); lastGCTime = System.currentTimeMillis(); } if(renderLOD) { render(); } } } private void mainLoop() { while(!farChunks.isEmpty()) { LODChunk lodChunk = receiveFarChunk(farChunks.remove()); sendChunkToGPU(lodChunk); } List players = Minecraft.getMinecraft().getIntegratedServer().getConfigurationManager().playerEntityList; if(!players.isEmpty()) { Entity player = (Entity)players.get(0); List 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); lastSortX = player.posX; lastSortY = player.posY; lastSortZ = player.posZ; loadedRegionsMap.forEach((k, v) -> v.tick(player)); } } } private void handleKeyboard() { if(Keyboard.isKeyDown(Keyboard.KEY_F) && !wasDown[Keyboard.KEY_F]) { renderLOD = !renderLOD; } if(Keyboard.isKeyDown(Keyboard.KEY_V) && !wasDown[Keyboard.KEY_V]) { renderWorld = !renderWorld; } for(int i = 0; i < 256; i++) { wasDown[i] = Keyboard.isKeyDown(i); } } private void runGC() { nextMeshOffset = 0; nextTri = 0; nextMesh = 0; piFirst.limit(0); piCount.limit(0); glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); int deletedNum = 0; int deletedRAM = 0; for(Iterator it = sentMeshes.iterator(); it.hasNext(); ) { Mesh mesh = it.next(); if(!mesh.pendingGPUDelete) { if(mesh.offset != nextMeshOffset) { glBufferSubData(GL_ARRAY_BUFFER, nextMeshOffset, mesh.buffer); } mesh.iFirst = nextTri; mesh.offset = nextMeshOffset; nextMeshOffset += mesh.buffer.limit(); nextTri += mesh.quadCount * 6; piFirst.limit(piFirst.limit() + 1); piFirst.put(nextMesh, mesh.iFirst); piCount.limit(piCount.limit() + 1); piCount.put(nextMesh, mesh.iCount); nextMesh++; } else { mesh.iFirst = mesh.offset = -1; mesh.visible = false; mesh.pendingGPUDelete = false; it.remove(); deletedNum++; deletedRAM += mesh.buffer.limit(); } } System.out.println("Deleted " + deletedNum + " meshes, freeing up " + (deletedRAM / 1024 / 1024) + "MB of VRAM"); glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); } private void render() { GL11.glPushAttrib(GL11.GL_ENABLE_BIT); GL11.glDisable(GL11.GL_TEXTURE_2D); GL11.glEnable(GL11.GL_BLEND); GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); 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); 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); 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); glMultiDrawArrays(GL_TRIANGLES, piFirst, piCount); glBindVertexArray(0); glUseProgram(0); GL11.glDepthMask(true); GL11.glPopAttrib(); } public boolean init() { Map uploadedSprites = ((TextureMap)Minecraft.getMinecraft().getTextureManager().getTexture(TextureMap.locationBlocksTexture)).mapUploadedSprites; 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("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); 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); piFirst = BufferUtils.createIntBuffer(MAX_MESHES); piFirst.flip(); piCount = BufferUtils.createIntBuffer(MAX_MESHES); piCount.flip(); glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); return true; } public void destroy() { glDeleteProgram(shaderProgram); glDeleteVertexArrays(VAO); glDeleteBuffers(VBO); SimpleChunkMesh.instances = 0; SimpleChunkMesh.usedRAM = 0; ChunkMesh.instances = 0; ChunkMesh.usedRAM = 0; } public 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 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 double getLastSortDistanceSq(Entity player) { return Math.pow(lastSortX - player.posX, 2) + Math.pow(lastSortZ - player.posZ, 2); } private synchronized void setServerChunkLoadQueue(List coords) { serverChunkLoadQueue = coords; } private LODChunk receiveFarChunk(Chunk chunk) { LODRegion region = getRegionContaining(chunk.xPosition, chunk.zPosition); myChunks.add(chunk); 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 = 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 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 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 void sendChunkToGPU(LODChunk lodChunk) { lodChunk.simpleMesh = new SimpleChunkMesh(lodChunk.chunk); 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 void setLOD(LODChunk lodChunk, int lod) { if(lod == lodChunk.lod) return; lodChunk.lod = lod; lodChunkChanged(lodChunk); } public void setVisible(LODChunk lodChunk, boolean visible) { if(visible == lodChunk.visible) return; lodChunk.visible = visible; lodChunkChanged(lodChunk); } public 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 int nextTri; private int nextMeshOffset; private int nextMesh; protected void setMeshVisible(Mesh mesh, boolean visible) { if(mesh == null) return; if(mesh.visible != visible) { if(!visible) { if(mesh.iFirst == -1) { System.out.println("uh"); } 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); deleteMeshFromGPU(mesh); 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) { sendMeshToGPU(mesh); piFirst.limit(piFirst.limit() + 1); piFirst.put(nextMesh, mesh.iFirst); piCount.limit(piCount.limit() + 1); piCount.put(nextMesh, mesh.iCount); nextMesh++; } mesh.visible = visible; } } private void sendMeshToGPU(Mesh mesh) { if(mesh == null) { return; } if(mesh.pendingGPUDelete) { mesh.pendingGPUDelete = false; return; } glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferSubData(GL_ARRAY_BUFFER, nextMeshOffset, mesh.buffer); mesh.iFirst = nextTri; mesh.iCount = mesh.quadCount * 6; mesh.offset = nextMeshOffset; nextTri += mesh.quadCount * 6; nextMeshOffset += mesh.buffer.limit(); sentMeshes.add(mesh); glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); } private void deleteMeshFromGPU(Mesh mesh) { if(mesh == null) { return; } mesh.pendingGPUDelete = true; } public Chunk getChunkFromChunkCoords(int x, int z) { for(Chunk chunk : myChunks) { if(chunk.xPosition == x && chunk.zPosition == z) { return chunk; } } return null; } public List getDebugText() { return Arrays.asList( "VRAM: " + (nextMeshOffset / 1024 / 1024) + "MB / " + (BUFFER_SIZE / 1024 / 1024) + "MB", "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" ); } public void onSave(World world) { String worldName = world.getWorldInfo().getWorldName(); Path saveDir = Minecraft.getMinecraft().mcDataDir.toPath().resolve("lodmod").resolve(worldName); loadedRegionsMap.forEach((k, v) -> v.save(saveDir)); } public static class LODChunkComparator implements Comparator { 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 { 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) ); } } }