package com.backsun.lod.renderer; import java.awt.Color; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import org.lwjgl.opengl.GL11; import org.lwjgl.util.glu.Project; import com.backsun.lod.objects.LodChunk; import com.backsun.lod.objects.LodDimension; import com.backsun.lod.util.LodConfig; import com.backsun.lod.util.ReflectionHandler; import com.backsun.lod.util.enums.ColorDirection; import com.backsun.lod.util.enums.FogDistance; import com.backsun.lod.util.enums.FogQuality; import com.backsun.lod.util.enums.LodLocation; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.BufferBuilder; import net.minecraft.client.renderer.GlStateManager; import net.minecraft.client.renderer.Tessellator; import net.minecraft.client.renderer.vertex.DefaultVertexFormats; import net.minecraft.entity.Entity; import net.minecraft.util.math.AxisAlignedBB; import net.minecraft.util.math.MathHelper; /** * @author James Seibel * @version 2-13-2021 */ public class LodRenderer { /** If true the LODs colors will be replaced with * a checkerboard, this can be used for debugging. */ public boolean debugging = false; private Minecraft mc; private float farPlaneDistance; // make sure this is an even number, or else it won't align with the chunk grid /** this is the total width of the LODs (I.E the diameter, not the radius) */ private static final int LOD_CHUNK_DISTANCE_RADIUS = 6; private Tessellator tessellator; private BufferBuilder bufferBuilder; /** * This is an array of 0's used to clear old * ByteBuffers when they need to be rebuilt. */ byte[] clearBytes; private ReflectionHandler reflectionHandler; public LodDimension dimension = null; private int maxThreads = Runtime.getRuntime().availableProcessors(); /** How many threads should be used for building the render buffer. */ private int numbBufferThreads = maxThreads; private ArrayList bufferThreads = new ArrayList(); private volatile ByteBuffer[] nearBuffers = new ByteBuffer[maxThreads]; private volatile ByteBuffer[] farBuffers = new ByteBuffer[maxThreads]; private ExecutorService threadPool = Executors.newFixedThreadPool(maxThreads); /* * this is the maximum number of bytes a buffer * would ever have to hold at once (this prevents the buffer * from having to resize and thus save performance) */ private int bufferMaxCapacity = 0; /** This is used to determine if the LODs should be regenerated */ private int previousChunkRenderDistance = 0; /** This is used to determine if the LODs should be regenerated */ private int prevChunkX = 0; /** This is used to determine if the LODs should be regenerated */ private int prevChunkZ = 0; /** This is used to determine if the LODs should be regenerated */ private FogDistance prevFogDistance = FogDistance.NEAR_AND_FAR; /** if this is true the LODs should be regenerated */ private boolean regen = false; public LodRenderer() { mc = Minecraft.getMinecraft(); // for some reason "Tessellator.getInstance()" won't work here, we have to create a new one tessellator = new Tessellator(2097152); bufferBuilder = tessellator.getBuffer(); reflectionHandler = new ReflectionHandler(); } public void drawLODs(LodDimension newDimension, float partialTicks) { if (reflectionHandler.fovMethod == null) { // don't continue if we can't get the // user's FOV return; } if (reflectionHandler.fovMethod == null) { // we aren't able to get the user's // FOV, don't render anything return; } // should the LODs be regenerated? if ((int)Minecraft.getMinecraft().player.posX / LodChunk.WIDTH != prevChunkX || (int)Minecraft.getMinecraft().player.posZ / LodChunk.WIDTH != prevChunkZ || previousChunkRenderDistance != mc.gameSettings.renderDistanceChunks || prevFogDistance != LodConfig.fogDistance || dimension != newDimension) { regen = true; prevChunkX = (int)Minecraft.getMinecraft().player.posX / LodChunk.WIDTH; prevChunkZ = (int)Minecraft.getMinecraft().player.posZ / LodChunk.WIDTH; prevFogDistance = LodConfig.fogDistance; } else { // nope, the player hasn't moved, the // render distance hasn't changed, and // the dimension is the same regen = false; } dimension = newDimension; if (dimension == null) { // if there aren't any loaded LodChunks // don't try drawing anything return; } // used for debugging and viewing how long different processes take mc.mcProfiler.endSection(); mc.mcProfiler.startSection("LOD"); mc.mcProfiler.startSection("LOD setup"); @SuppressWarnings("unused") long startTime = System.nanoTime(); if (LodConfig.drawCheckerBoard) { if (debugging != LodConfig.drawCheckerBoard) regen = true; debugging = true; } else { if (debugging != LodConfig.drawCheckerBoard) regen = true; debugging = false; } // color setup int alpha = 255; // 0 - 255 Color red = new Color(255, 0, 0, alpha); Color black = new Color(0, 0, 0, alpha); Color white = new Color(255, 255, 255, alpha); @SuppressWarnings("unused") Color invisible = new Color(0,0,0,0); @SuppressWarnings("unused") Color error = new Color(255, 0, 225, alpha); // bright pink // get the camera location Entity player = mc.player; double cameraX = player.lastTickPosX + (player.posX - player.lastTickPosX) * partialTicks; double cameraY = player.lastTickPosY + (player.posY - player.lastTickPosY) * partialTicks; double cameraZ = player.lastTickPosZ + (player.posZ - player.lastTickPosZ) * partialTicks; // determine how far the game's render distance is currently set int renderDistWidth = mc.gameSettings.renderDistanceChunks; farPlaneDistance = renderDistWidth * LodChunk.WIDTH; // set how big the LODs will be and how far they will go int totalLength = (int) farPlaneDistance * LOD_CHUNK_DISTANCE_RADIUS * 2; int numbChunksWide = (totalLength / LodChunk.WIDTH); // this seemingly useless math is required, // just using (int) camera doesn't work int playerXChunkOffset = ((int) cameraX / LodChunk.WIDTH) * LodChunk.WIDTH; int playerZChunkOffset = ((int) cameraZ / LodChunk.WIDTH) * LodChunk.WIDTH; // this where we will start drawing squares // (exactly half the total width) int startX = (-LodChunk.WIDTH * (numbChunksWide / 2)) + playerXChunkOffset; int startZ = (-LodChunk.WIDTH * (numbChunksWide / 2)) + playerZChunkOffset; // this is where we store the LOD objects AxisAlignedBB lodArray[][] = new AxisAlignedBB[numbChunksWide][numbChunksWide]; // this is where we store the color for each LOD object Color colorArray[][] = new Color[numbChunksWide][numbChunksWide]; //=================// // create the LODs // //=================// // TODO create a worker thread to do this if (regen) { mc.mcProfiler.endStartSection("LOD generation"); // x axis for (int i = 0; i < numbChunksWide; i++) { // z axis for (int j = 0; j < numbChunksWide; j++) { // skip the middle // (As the player moves some chunks will overlap or be missing, // this is just how chunk loading/unloading works. This can hopefully // be hidden with careful use of fog) int middle = (numbChunksWide / 2); if (RenderUtil.isCoordinateInLoadedArea(i, j, middle)) { continue; } // set where this square will be drawn in the world double xOffset = (LodChunk.WIDTH * i) + // offset by the number of LOD blocks startX; // offset so the center LOD block is centered underneath the player double yOffset = 0; double zOffset = (LodChunk.WIDTH * j) + startZ; int chunkX = i + (startX / LodChunk.WIDTH); int chunkZ = j + (startZ / LodChunk.WIDTH); LodChunk lod = dimension.getLodFromCoordinates(chunkX, chunkZ); // new LodChunk(); // if (lod == null) { // note: for some reason if any color or lod object are set here // it causes the game to use 100% gpu, all of it undefined in the debug menu // and drop to ~6 fps. // colorArray[i][j] = null; // lodArray[i][j] = null; continue; } Color c = new Color( (lod.colors[ColorDirection.TOP.value].getRed()), (lod.colors[ColorDirection.TOP.value].getGreen()), (lod.colors[ColorDirection.TOP.value].getBlue()), lod.colors[ColorDirection.TOP.value].getAlpha()); if (!debugging) { // add the color to the array colorArray[i][j] = c; } else { // if debugging draw the squares as a black and white checker board if ((chunkX + chunkZ) % 2 == 0) c = white; else c = black; // draw the first square as red if (i == 0 && j == 0) c = red; colorArray[i][j] = c; } // add the new box to the array int topPoint = getLodHeightPoint(lod.top); int bottomPoint = getLodHeightPoint(lod.bottom); // don't draw an LOD if it is empty if (topPoint == -1 && bottomPoint == -1) continue; lodArray[i][j] = new AxisAlignedBB(0, bottomPoint, 0, LodChunk.WIDTH, topPoint, LodChunk.WIDTH).offset(xOffset, yOffset, zOffset); } } } //===========================// // GL settings for rendering // //===========================// // set the required open GL settings GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); GL11.glLineWidth(2.0f); GL11.glDisable(GL11.GL_TEXTURE_2D); GL11.glEnable(GL11.GL_CULL_FACE); GL11.glPolygonMode(GL11.GL_FRONT_AND_BACK, GL11.GL_FILL); GlStateManager.translate(-cameraX, -cameraY, -cameraZ); setProjectionMatrix(partialTicks); setupLighting(partialTicks); setupBufferThreads(lodArray); //===========// // rendering // //===========// mc.mcProfiler.endStartSection("LOD build buffer"); if (regen) generateLodBuffers(lodArray, colorArray, LodConfig.fogDistance); switch(LodConfig.fogDistance) { case NEAR_AND_FAR: mc.mcProfiler.endStartSection("LOD draw setup"); setupFog(FogDistance.NEAR, reflectionHandler.getFogQuality()); sendLodsToGpuAndDraw(nearBuffers); mc.mcProfiler.endStartSection("LOD draw setup"); setupFog(FogDistance.FAR, reflectionHandler.getFogQuality()); sendLodsToGpuAndDraw(farBuffers); break; case NEAR: mc.mcProfiler.endStartSection("LOD draw setup"); setupFog(FogDistance.NEAR, reflectionHandler.getFogQuality()); sendLodsToGpuAndDraw(nearBuffers); break; case FAR: mc.mcProfiler.endStartSection("LOD draw setup"); setupFog(FogDistance.FAR, reflectionHandler.getFogQuality()); sendLodsToGpuAndDraw(farBuffers); break; } //=========// // cleanup // //=========// mc.mcProfiler.endStartSection("LOD cleanup"); // this must be done otherwise other parts of the screen may be drawn with a fog effect // IE the GUI GlStateManager.disableFog(); GL11.glPolygonMode(GL11.GL_FRONT_AND_BACK, GL11.GL_FILL); GL11.glEnable(GL11.GL_TEXTURE_2D); GL11.glDisable(GL11.GL_LIGHT1); GL11.glDisable(GL11.GL_COLOR_MATERIAL); // change the perspective matrix back to prevent incompatibilities // with other mods that may render during forgeRenderLast Project.gluPerspective(reflectionHandler.getFov(mc, partialTicks, true), (float) this.mc.displayWidth / (float) this.mc.displayHeight, 0.05F, this.farPlaneDistance * MathHelper.SQRT_2); // this can't be called until after the buffers are built // because otherwise the buffers may be set to the wrong size previousChunkRenderDistance = mc.gameSettings.renderDistanceChunks; // This is about how long this whole process should take // 16 ms = 60 hz @SuppressWarnings("unused") long endTime = System.nanoTime(); // end of profiler tracking mc.mcProfiler.endSection(); } /** * draw an array of cubes (or squares) with the given colors. * @param lods bounding boxes to draw * @param colors color of each box to draw */ private void generateLodBuffers(AxisAlignedBB[][] lods, Color[][] colors, FogDistance fogDistance) { List> bufferFutures = new ArrayList<>(); bufferMaxCapacity = (lods.length * lods.length * (6 * 4 * ((3 * 4) + (4 * 4)))) / numbBufferThreads; // TODO this should change based on whether we are using near/far or both fog settings for(int i = 0; i < numbBufferThreads; i++) { if (nearBuffers[i] == null || previousChunkRenderDistance != mc.gameSettings.renderDistanceChunks) { nearBuffers[i] = ByteBuffer.allocateDirect(bufferMaxCapacity); nearBuffers[i].order(ByteOrder.LITTLE_ENDIAN); farBuffers[i] = ByteBuffer.allocateDirect(bufferMaxCapacity); farBuffers[i].order(ByteOrder.LITTLE_ENDIAN); clearBytes = new byte[bufferMaxCapacity]; } if (regen) { // this is the best way I could find to // overwrite the old data // (which needs to be done otherwise old // LODs may be drawn) nearBuffers[i].clear(); nearBuffers[i].put(clearBytes); nearBuffers[i].clear(); farBuffers[i].clear(); farBuffers[i].put(clearBytes); farBuffers[i].clear(); } int pos = bufferBuilder.getByteBuffer().position(); nearBuffers[i].position(pos); farBuffers[i].position(pos); bufferThreads.get(i).setNewData(nearBuffers[i], farBuffers[i], fogDistance, lods, colors, i, numbBufferThreads); } try { bufferFutures = threadPool.invokeAll(bufferThreads); } catch (InterruptedException e) { // this should never happen, but just in case e.printStackTrace(); } for(int i = 0; i < numbBufferThreads; i++) { try { nearBuffers[i] = bufferFutures.get(i).get().nearBuffer; farBuffers[i] = bufferFutures.get(i).get().farBuffer; } catch(CancellationException | ExecutionException| InterruptedException e) { // this should never happen, but just in case e.printStackTrace(); } } } private void sendLodsToGpuAndDraw(ByteBuffer[] buffers) { for(int i = 0; i < numbBufferThreads; i++) { int pos = bufferBuilder.getByteBuffer().position(); buffers[i].position(pos); bufferBuilder.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_COLOR); bufferBuilder.getByteBuffer().clear(); bufferBuilder.putBulkData(buffers[i]); mc.mcProfiler.endStartSection("LOD draw"); tessellator.draw(); mc.mcProfiler.endStartSection("LOD draw setup"); bufferBuilder.getByteBuffer().clear(); // this is required otherwise nothing is drawn } } //=================// // Setup Functions // //=================// private void setupFog(FogDistance fogDistance, FogQuality fogQuality) { if(fogQuality == FogQuality.OFF) { GlStateManager.disableFog(); return; } if(fogDistance == FogDistance.NEAR_AND_FAR) { throw new IllegalArgumentException("setupFog only accepts NEAR or FAR fog distances."); } // the multipliers are percentages // of the regular view distance. if(fogDistance == FogDistance.NEAR) { // the reason that I wrote fogEnd then fogStart backwards // is because we are using fog backwards to how // it is normally used, with it hiding near objects // instead of far objects. if (fogQuality == FogQuality.FANCY) { GlStateManager.setFogEnd(farPlaneDistance * 0.3f * LOD_CHUNK_DISTANCE_RADIUS); GlStateManager.setFogStart(farPlaneDistance * 0.35f * LOD_CHUNK_DISTANCE_RADIUS); } else if(fogQuality == FogQuality.FAST) { // for the far fog of the normal chunks // to start right where the LODs' end use: // end = 0.8f, start = 1.5f GlStateManager.setFogEnd(farPlaneDistance * 1.5f); GlStateManager.setFogStart(farPlaneDistance * 2.0f); } } else if(fogDistance == FogDistance.FAR) { if (fogQuality == FogQuality.FANCY) { GlStateManager.setFogStart(farPlaneDistance * 0.78f * LOD_CHUNK_DISTANCE_RADIUS); GlStateManager.setFogEnd(farPlaneDistance * 1.0f * LOD_CHUNK_DISTANCE_RADIUS); } else if(fogQuality == FogQuality.FAST) { GlStateManager.setFogStart(farPlaneDistance * 0.5f * LOD_CHUNK_DISTANCE_RADIUS); GlStateManager.setFogEnd(farPlaneDistance * 0.75f * LOD_CHUNK_DISTANCE_RADIUS); } } GlStateManager.setFogDensity(0.1f); GlStateManager.enableFog(); } /** * create a new projection matrix and send it over to the GPU * @param partialTicks how many ticks into the frame we are * @return true if the matrix was successfully created and sent to the GPU, false otherwise */ private void setProjectionMatrix(float partialTicks) { // create a new view frustum so that the squares can be drawn outside the normal view distance GlStateManager.matrixMode(GL11.GL_PROJECTION); GlStateManager.loadIdentity(); // only continue if we can get the FOV if (reflectionHandler.fovMethod != null) { Project.gluPerspective(reflectionHandler.getFov(mc, partialTicks, true), (float) mc.displayWidth / (float) mc.displayHeight, 0.5F, farPlaneDistance * 12); } // we weren't able to set up the projection matrix return; } /** * setup the lighting to be used for the LODs */ private void setupLighting(float partialTicks) { GL11.glEnable(GL11.GL_COLOR_MATERIAL); // set the color to be used as the material (this allows lighting to be enabled) // this isn't perfect right now, but it looks pretty good at 50% brightness float sunBrightness = mc.world.getSunBrightness(partialTicks) * mc.world.provider.getSunBrightnessFactor(partialTicks); float skyHasLight = mc.world.provider.hasSkyLight()? 1.0f : 0.15f; float gammaMultiplyer = (mc.gameSettings.gammaSetting * 0.5f + 0.5f); float lightStrength = sunBrightness * skyHasLight * gammaMultiplyer; float lightAmbient[] = {lightStrength, lightStrength, lightStrength, 1.0f}; ByteBuffer temp = ByteBuffer.allocateDirect(16); temp.order(ByteOrder.nativeOrder()); GL11.glLight(GL11.GL_LIGHT1, GL11.GL_AMBIENT, (FloatBuffer) temp.asFloatBuffer().put(lightAmbient).flip()); GL11.glEnable(GL11.GL_LIGHT1); // Enable the above lighting GlStateManager.enableLighting(); } private void setupBufferThreads(AxisAlignedBB[][] lods) { if (numbBufferThreads != bufferThreads.size()) { bufferMaxCapacity = (lods.length * lods.length * (6 * 4 * ((3 * 4) + (4 * 4)))) / numbBufferThreads; clearBytes = new byte[bufferMaxCapacity]; bufferThreads.clear(); for(int i = 0; i < numbBufferThreads; i++) bufferThreads.add(new BuildBufferThread()); regen = true; for(int i = 0; i < maxThreads; i++) { nearBuffers[i] = ByteBuffer.allocateDirect(bufferMaxCapacity); nearBuffers[i].order(ByteOrder.LITTLE_ENDIAN); farBuffers[i] = ByteBuffer.allocateDirect(bufferMaxCapacity); farBuffers[i].order(ByteOrder.LITTLE_ENDIAN); } } } /** * Returns -1 if there are no valid points */ private int getLodHeightPoint(short[] heightPoints) { if (heightPoints[LodLocation.NE.value] != -1) return heightPoints[LodLocation.NE.value]; if (heightPoints[LodLocation.NW.value] != -1) return heightPoints[LodLocation.NW.value]; if (heightPoints[LodLocation.SE.value] != -1) return heightPoints[LodLocation.NE.value]; return heightPoints[LodLocation.NE.value]; } }