package com.seibel.lod.render;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import org.lwjgl.opengl.GL11;
import com.mojang.blaze3d.matrix.MatrixStack;
import com.mojang.blaze3d.platform.GlStateManager;
import com.mojang.blaze3d.systems.RenderSystem;
import com.seibel.lod.builders.LodBufferBuilder;
import com.seibel.lod.enums.FogDistance;
import com.seibel.lod.enums.FogQuality;
import com.seibel.lod.handlers.ReflectionHandler;
import com.seibel.lod.objects.LodChunk;
import com.seibel.lod.objects.LodDimension;
import com.seibel.lod.objects.NearFarBuffer;
import com.seibel.lod.objects.NearFarFogSetting;
import com.seibel.lod.util.LodConfig;
import net.minecraft.client.Minecraft;
import net.minecraft.client.entity.player.ClientPlayerEntity;
import net.minecraft.client.renderer.ActiveRenderInfo;
import net.minecraft.client.renderer.BufferBuilder;
import net.minecraft.client.renderer.FogRenderer;
import net.minecraft.client.renderer.GameRenderer;
import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
import net.minecraft.client.renderer.vertex.VertexBuffer;
import net.minecraft.client.renderer.vertex.VertexFormat;
import net.minecraft.potion.Effects;
import net.minecraft.profiler.IProfiler;
import net.minecraft.util.math.MathHelper;
import net.minecraft.util.math.vector.Matrix4f;
import net.minecraft.util.math.vector.Vector3d;
import net.minecraft.util.math.vector.Vector3f;
/**
* This is where all the magic happens.
* This is where LODs are draw to the world.
*
* @author James Seibel
* @version 05-08-2021
*/
public class LodRender
{
/** this is the light used when rendering the LODs,
* it should be something different than what is used by Minecraft */
private static final int LOD_GL_LIGHT_NUMBER = GL11.GL_LIGHT2;
/** 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 GameRenderer gameRender;
private IProfiler profiler;
private float farPlaneDistance;
private ReflectionHandler reflectionHandler;
/** This is used to generate the buildable buffers */
private LodBufferBuilder lodBufferBuilder;
/** The buffers that are used to draw LODs using near fog */
private volatile BufferBuilder drawableNearBuffer;
/** The buffers that are used to draw LODs using far fog */
private volatile BufferBuilder drawableFarBuffer;
/** This is the VertexBuffer used to draw any LODs that use near fog */
private volatile VertexBuffer nearVbo;
/** This is the VertexBuffer used to draw any LODs that use far fog */
private volatile VertexBuffer farVbo;
public static final VertexFormat LOD_VERTEX_FORMAT = DefaultVertexFormats.POSITION_COLOR;
/** 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 LOD buffers should be regenerated,
* provided they aren't already being regenerated. */
private boolean regen = false;
public LodRender(LodBufferBuilder newLodBufferBuilder)
{
mc = Minecraft.getInstance();
gameRender = mc.gameRenderer;
reflectionHandler = new ReflectionHandler();
lodBufferBuilder = newLodBufferBuilder;
}
/**
* Besides drawing the LODs this method also starts
* the async process of generating the Buffers that hold those LODs.
*
* @param newDimension The dimension to draw, if null doesn't replace the current dimension.
* @param partialTicks how far into the current tick this method was called.
*/
@SuppressWarnings("deprecation")
public void drawLODs(LodDimension lodDim, float partialTicks, IProfiler newProfiler)
{
if (lodDim == null)
{
// if there aren't any loaded LodChunks
// don't try drawing anything
return;
}
//===============//
// initial setup //
//===============//
profiler = newProfiler;
profiler.startSection("LOD setup");
ClientPlayerEntity player = mc.player;
// should LODs be regenerated?
if ((int)player.getPosX() / LodChunk.WIDTH != prevChunkX ||
(int)player.getPosZ() / LodChunk.WIDTH != prevChunkZ ||
previousChunkRenderDistance != mc.gameSettings.renderDistanceChunks ||
prevFogDistance != LodConfig.CLIENT.fogDistance.get())
{
// yes
regen = true;
prevChunkX = (int)player.getPosX() / LodChunk.WIDTH;
prevChunkZ = (int)player.getPosZ() / LodChunk.WIDTH;
prevFogDistance = LodConfig.CLIENT.fogDistance.get();
}
else
{
// nope, the player hasn't moved, the
// render distance hasn't changed, and
// the dimension is the same
}
// did the user change the debug setting?
if (LodConfig.CLIENT.debugMode.get() != debugging)
{
debugging = LodConfig.CLIENT.debugMode.get();
regen = true;
}
// 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 * LodConfig.CLIENT.lodChunkRadiusMultiplier.get() * 2;
int numbChunksWide = (totalLength / LodChunk.WIDTH);
//=================//
// create the LODs //
//=================//
// only regenerate the LODs if:
// 1. we want to regenerate LODs
// 2. we aren't already regenerating the LODs
// 3. we aren't waiting for the build and draw buffers to swap
// (this is to prevent thread conflicts)
if (regen && !lodBufferBuilder.generatingBuffers && !lodBufferBuilder.newBuffersAvaliable())
{
// this will mainly happen when the view distance is changed
if (drawableNearBuffer == null || drawableFarBuffer == null ||
previousChunkRenderDistance != mc.gameSettings.renderDistanceChunks)
setupBuffers(numbChunksWide);
// generate the LODs on a separate thread to prevent stuttering or freezing
lodBufferBuilder.generateLodBuffersAsync(this, lodDim, player.getPosX(), player.getPosZ(), numbChunksWide);
// the regen process has been started,
// it will be done when lodBufferBuilder.newBuffersAvaliable
// is true
regen = false;
}
// replace the buffers used to draw and build,
// this is only done when the createLodBufferGenerationThread
// has finished executing on a parallel thread.
if (lodBufferBuilder.newBuffersAvaliable())
{
swapBuffers();
}
//===========================//
// GL settings for rendering //
//===========================//
// set the required open GL settings
GL11.glPolygonMode(GL11.GL_FRONT_AND_BACK, GL11.GL_FILL);
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
GL11.glDisable(GL11.GL_TEXTURE_2D);
GL11.glEnable(GL11.GL_CULL_FACE);
GL11.glEnable(GL11.GL_COLOR_MATERIAL);
GL11.glEnable(GL11.GL_DEPTH_TEST);
// disable the lights Minecraft uses
GL11.glDisable(GL11.GL_LIGHT0);
GL11.glDisable(GL11.GL_LIGHT1);
// get the default projection matrix so we can
// reset it after drawing the LODs
float[] defaultProjMatrix = new float[16];
GL11.glGetFloatv(GL11.GL_PROJECTION_MATRIX, defaultProjMatrix);
Matrix4f modelViewMatrix = generateModelViewMatrix(partialTicks);
setupProjectionMatrix(partialTicks);
setupLighting(lodDim, partialTicks);
NearFarFogSetting fogSetting = determineFogSettings();
// determine the current fog settings so they can be
// reset after drawing the LODs
float defaultFogStartDist = GL11.glGetFloat(GL11.GL_FOG_START);
float defaultFogEndDist = GL11.glGetFloat(GL11.GL_FOG_END);
int defaultFogMode = GL11.glGetInteger(GL11.GL_FOG_MODE);
//===========//
// rendering //
//===========//
profiler.endStartSection("LOD draw");
setupFog(fogSetting.nearFogSetting, reflectionHandler.getFogQuality());
sendLodsToGpuAndDraw(nearVbo, modelViewMatrix);
setupFog(fogSetting.farFogSetting, reflectionHandler.getFogQuality());
sendLodsToGpuAndDraw(farVbo, modelViewMatrix);
//=========//
// cleanup //
//=========//
profiler.endStartSection("LOD cleanup");
GL11.glPolygonMode(GL11.GL_FRONT_AND_BACK, GL11.GL_FILL);
GL11.glEnable(GL11.GL_TEXTURE_2D);
GL11.glDisable(LOD_GL_LIGHT_NUMBER);
// re-enable the lights Minecraft uses
GL11.glEnable(GL11.GL_LIGHT0);
GL11.glEnable(GL11.GL_LIGHT1);
RenderSystem.disableLighting();
// 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;
// reset the fog settings so the normal chunks
// will be drawn correctly
RenderSystem.fogStart(defaultFogStartDist);
RenderSystem.fogEnd(defaultFogEndDist);
RenderSystem.fogMode(defaultFogMode);
// reset the projection matrix so anything drawn after
// the LODs will use the correct projection matrix
Matrix4f mvm = new Matrix4f(defaultProjMatrix);
mvm.transpose();
gameRender.resetProjectionMatrix(mvm);
// clear the depth buffer so anything drawn is drawn
// over the LODs
GL11.glClear(GL11.GL_DEPTH_BUFFER_BIT);
// end of internal LOD profiling
profiler.endSection();
}
/**
* This is where the actual drawing happens.
*
* @param buffers the buffers sent to the GPU to draw
*/
private void sendLodsToGpuAndDraw(VertexBuffer vbo, Matrix4f modelViewMatrix)
{
if (vbo == null)
return;
vbo.bindBuffer();
// 0L is the starting pointer
LOD_VERTEX_FORMAT.setupBufferState(0L);
vbo.draw(modelViewMatrix, GL11.GL_QUADS);
VertexBuffer.unbindBuffer();
LOD_VERTEX_FORMAT.clearBufferState();
}
//=================//
// Setup Functions //
//=================//
@SuppressWarnings("deprecation")
private void setupFog(FogDistance fogDistance, FogQuality fogQuality)
{
if(fogQuality == FogQuality.OFF)
{
FogRenderer.resetFog();
RenderSystem.disableFog();
return;
}
if(fogDistance == FogDistance.NEAR_AND_FAR)
{
throw new IllegalArgumentException("setupFog doesn't accept the NEAR_AND_FAR fog distance.");
}
// 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)
{
RenderSystem.fogEnd(farPlaneDistance * 1.75f);
RenderSystem.fogStart(farPlaneDistance * 1.95f);
}
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
RenderSystem.fogEnd(farPlaneDistance * 1.5f);
RenderSystem.fogStart(farPlaneDistance * 2.0f);
}
}
else if(fogDistance == FogDistance.FAR)
{
if (fogQuality == FogQuality.FANCY)
{
RenderSystem.fogStart(farPlaneDistance * 0.85f * LodConfig.CLIENT.lodChunkRadiusMultiplier.get());
RenderSystem.fogEnd(farPlaneDistance * 1.0f * LodConfig.CLIENT.lodChunkRadiusMultiplier.get());
}
else if(fogQuality == FogQuality.FAST)
{
RenderSystem.fogStart(farPlaneDistance * 0.5f * LodConfig.CLIENT.lodChunkRadiusMultiplier.get());
RenderSystem.fogEnd(farPlaneDistance * 0.75f * LodConfig.CLIENT.lodChunkRadiusMultiplier.get());
}
}
RenderSystem.fogMode(GlStateManager.FogMode.LINEAR);
RenderSystem.enableFog();
}
/**
* Create the model view matrix to move the LODs
* from object space into world space.
*/
private Matrix4f generateModelViewMatrix(float partialTicks)
{
// get all relevant camera info
ActiveRenderInfo renderInfo = mc.gameRenderer.getActiveRenderInfo();
Vector3d projectedView = renderInfo.getProjectedView();
// generate the model view matrix
MatrixStack matrixStack = new MatrixStack();
matrixStack.push();
// translate and rotate to the current camera location
matrixStack.rotate(Vector3f.XP.rotationDegrees(renderInfo.getPitch()));
matrixStack.rotate(Vector3f.YP.rotationDegrees(renderInfo.getYaw() + 180));
matrixStack.translate(-projectedView.x, -projectedView.y, -projectedView.z);
return matrixStack.getLast().getMatrix();
}
/**
* create a new projection matrix and send it over to the GPU
*
* A lot of this code is copied from renderWorld (line 578)
* in the GameRender class. The code copied is anything with
* a matrixStack and is responsible for making sure the LOD
* objects distort correctly relative to the rest of the world.
* Distortions are caused by: standing in a nether portal,
* nausea potion effect, walking bobbing.
*
* @param partialTicks how many ticks into the frame we are
*/
private void setupProjectionMatrix(float partialTicks)
{
// Note: if the LOD objects don't distort correctly
// compared to regular minecraft terrain, make sure
// all the transformations in renderWorld are here too
MatrixStack matrixStack = new MatrixStack();
matrixStack.push();
gameRender.hurtCameraEffect(matrixStack, partialTicks);
if (this.mc.gameSettings.viewBobbing) {
gameRender.applyBobbing(matrixStack, partialTicks);
}
// potion and nausea effects
float f = MathHelper.lerp(partialTicks, mc.player.prevTimeInPortal, mc.player.timeInPortal) * mc.gameSettings.screenEffectScale * mc.gameSettings.screenEffectScale;
if (f > 0.0F) {
int i = this.mc.player.isPotionActive(Effects.NAUSEA) ? 7 : 20;
float f1 = 5.0F / (f * f + 5.0F) - f * 0.04F;
f1 = f1 * f1;
Vector3f vector3f = new Vector3f(0.0F, MathHelper.SQRT_2 / 2.0F, MathHelper.SQRT_2 / 2.0F);
matrixStack.rotate(vector3f.rotationDegrees((gameRender.rendererUpdateCount + partialTicks) * i));
matrixStack.scale(1.0F / f1, 1.0F, 1.0F);
float f2 = -(gameRender.rendererUpdateCount + partialTicks) * i;
matrixStack.rotate(vector3f.rotationDegrees(f2));
}
// this projection matrix allows us to see past the normal
// world render distance
Matrix4f projectionMatrix =
Matrix4f.perspective(
getFov(partialTicks, true),
(float)this.mc.getMainWindow().getFramebufferWidth() / (float)this.mc.getMainWindow().getFramebufferHeight(),
0.5F,
this.farPlaneDistance * LodConfig.CLIENT.lodChunkRadiusMultiplier.get() * 2);
// add the screen space distortions
projectionMatrix.mul(matrixStack.getLast().getMatrix());
gameRender.resetProjectionMatrix(projectionMatrix);
return;
}
/**
* setup the lighting to be used for the LODs
*/
@SuppressWarnings("deprecation")
private void setupLighting(LodDimension lodDimension, float partialTicks)
{
float sunBrightness = lodDimension.dimension.hasSkyLight() ? mc.world.getSunBrightness(partialTicks) : 0.2f;
float gammaMultiplyer = (float)mc.gameSettings.gamma - 0.5f;
float lightStrength = sunBrightness - 0.4f + (gammaMultiplyer * 0.2f);
float lightAmbient[] = {lightStrength, lightStrength, lightStrength, 1.0f};
ByteBuffer temp = ByteBuffer.allocateDirect(16);
temp.order(ByteOrder.nativeOrder());
GL11.glLightfv(LOD_GL_LIGHT_NUMBER, GL11.GL_AMBIENT, (FloatBuffer) temp.asFloatBuffer().put(lightAmbient).flip());
GL11.glEnable(LOD_GL_LIGHT_NUMBER); // Enable the above lighting
RenderSystem.enableLighting();
}
/**
* Create all buffers that will be used.
*/
private void setupBuffers(int numbChunksWide)
{
// calculate the max amount of storage needed (in bytes)
int bufferMaxCapacity = (numbChunksWide * numbChunksWide * (6 * 4 * (3 + 4)));
// (numbChunksWide * numbChunksWide *
// (sidesOnACube * pointsInASquare * (positionPoints + colorPoints)))
// TODO complain or do something when memory is too low
// currently the VM will just crash and complain there is no more memory
// issue #4
drawableNearBuffer = new BufferBuilder(bufferMaxCapacity);
drawableFarBuffer = new BufferBuilder(bufferMaxCapacity);
lodBufferBuilder.setupBuffers(bufferMaxCapacity);
}
//======================//
// Other Misc Functions //
//======================//
/**
* If this is called then the next time "drawLODs" is called
* the LODs will be regenerated; the same as if the player moved.
*/
public void regenerateLODsNextFrame()
{
regen = true;
}
/**
* Replace the current drawable buffers with the newly
* created buffers from the lodBufferBuilder.
*/
private void swapBuffers()
{
// replace the drawable buffers with
// the newly created buffers from the lodBufferBuilder
NearFarBuffer newBuffers = lodBufferBuilder.swapBuffers(drawableNearBuffer, drawableFarBuffer);
drawableNearBuffer = newBuffers.nearBuffer;
drawableFarBuffer = newBuffers.farBuffer;
// bind the buffers with their respective VBOs
if (nearVbo != null)
nearVbo.close();
nearVbo = new VertexBuffer(LOD_VERTEX_FORMAT);
nearVbo.upload(drawableNearBuffer);
if (farVbo != null)
farVbo.close();
farVbo = new VertexBuffer(LOD_VERTEX_FORMAT);
farVbo.upload(drawableFarBuffer);
}
private double getFov(float partialTicks, boolean useFovSetting)
{
return mc.gameRenderer.getFOVModifier(mc.gameRenderer.getActiveRenderInfo(), partialTicks, useFovSetting);
}
/**
* Based on the fogDistance setting and
* optifine's fogQuality setting return what fog
* settings should be used when rendering.
*/
private NearFarFogSetting determineFogSettings()
{
NearFarFogSetting fogSetting = new NearFarFogSetting();
switch(reflectionHandler.getFogQuality())
{
case FANCY:
switch(LodConfig.CLIENT.fogDistance.get())
{
case NEAR_AND_FAR:
fogSetting.nearFogSetting = FogDistance.NEAR;
fogSetting.farFogSetting = FogDistance.FAR;
break;
case NEAR:
fogSetting.nearFogSetting = FogDistance.NEAR;
fogSetting.farFogSetting = FogDistance.NEAR;
break;
case FAR:
fogSetting.nearFogSetting = FogDistance.FAR;
fogSetting.farFogSetting = FogDistance.FAR;
break;
}
break;
case FAST:
// fast fog setting should only have one type of
// fog, since the LODs are separated into a near
// and far portion; and fast fog is rendered from the
// frustrum's perspective instead of the camera
switch(LodConfig.CLIENT.fogDistance.get())
{
case NEAR_AND_FAR:
fogSetting.nearFogSetting = FogDistance.NEAR;
fogSetting.farFogSetting = FogDistance.NEAR;
break;
case NEAR:
fogSetting.nearFogSetting = FogDistance.NEAR;
fogSetting.farFogSetting = FogDistance.NEAR;
break;
case FAR:
fogSetting.nearFogSetting = FogDistance.FAR;
fogSetting.farFogSetting = FogDistance.FAR;
break;
}
break;
case OFF:
break;
}
return fogSetting;
}
}