package net.rustnotmc.client; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; import net.minecraft.client.Minecraft; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import net.minecraft.client.renderer.GlStateManager; import net.rustnotmc.protocol.Packets; import org.lwjgl.BufferUtils; import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL15; import org.lwjgl.opengl.GL20; /** * The far view of the world. * * The chunks around a player hold the whole detail, but a player sees much * further than that. The far view is the ground of the world seen from far * away: a grid of heights and colours, one sample every four blocks. The * server sends it a region at a time, and the client turns each region into * a mesh and draws it beyond the loaded chunks. * * The mesh is drawn with its own shader, because the far view has no blocks * and no textures: only the shape of the ground and its colour. */ public final class RsxLod { /** The regions that came from the server, by the corner of the region. */ private static final java.util.Map REGIONS = new java.util.concurrent.ConcurrentHashMap(); /** The meshes that were built, by the corner of the region. */ private static final java.util.Map MESHES = new java.util.concurrent.ConcurrentHashMap(); /** Where the far view writes down what went wrong. */ private static final Logger LOGGER = LogManager.getLogger(); /** Whether the far view is drawn at all. */ private static boolean enabled = true; /** The shader of the far view, built when it is first needed. */ private static Program program; /** How many meshes were drawn the last time. */ private static int drawn; /** Whether the drawing of the far view was already said out loud. */ private static boolean announced; private RsxLod() { } /** Takes a region from the server and builds its mesh. */ public static void accept(Packets.LodRegion region) { Long key = Long.valueOf(key(region.originX, region.originZ)); REGIONS.put(key, region); Mesh old = MESHES.remove(key); if (old != null) { old.free(); } if (REGIONS.size() == 1) { LOGGER.info("RustNotMC: the far view came: region at " + region.originX + "," + region.originZ + " with " + region.size + " samples"); } } /** Throws the far view away, for when the player leaves the world. */ public static void clear() { REGIONS.clear(); for (Mesh mesh : MESHES.values()) { mesh.free(); } MESHES.clear(); } /** Whether the far view is drawn. */ public static boolean enabled() { return enabled; } public static void enabled(boolean on) { enabled = on; } /** How many meshes were drawn the last time. */ public static int drawn() { return drawn; } /** How many regions came from the server. */ public static int regions() { return REGIONS.size(); } /** The key of a region: its corner, folded into one number. */ private static long key(int originX, int originZ) { return ((long) originX << 32) | (originZ & 0xFFFFFFFFL); } /** * Draws the far view. * * The far view is drawn before the chunks, so that the chunks, which are * nearer and have all the detail, cover it where they overlap. */ public static void render(Minecraft mc) { drawn = 0; if (!enabled || REGIONS.isEmpty() || mc.player == null) { return; } if (mc.gameSettings.renderDistanceChunks <= 0) { return; } Program prog = program(); if (prog == null) { return; } FloatBuffer projection = BufferUtils.createFloatBuffer(16); FloatBuffer model = BufferUtils.createFloatBuffer(16); GL11.glGetFloat(GL11.GL_PROJECTION_MATRIX, projection); GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, model); float[] projectionArray = new float[16]; float[] modelArray = new float[16]; projection.get(projectionArray); model.get(modelArray); double camX = mc.getRenderViewEntity().lastTickPosX + (mc.getRenderViewEntity().posX - mc.getRenderViewEntity().lastTickPosX) * (double) mc.getRenderPartialTicks(); double camY = mc.getRenderViewEntity().lastTickPosY + (mc.getRenderViewEntity().posY - mc.getRenderViewEntity().lastTickPosY) * (double) mc.getRenderPartialTicks(); double camZ = mc.getRenderViewEntity().lastTickPosZ + (mc.getRenderViewEntity().posZ - mc.getRenderViewEntity().lastTickPosZ) * (double) mc.getRenderPartialTicks(); // the colour the far view fades into: the colour of the sky. The far // view reaches further than the chunks do, so its fog sits at the // edge of the far view and not at the edge of the chunks; the two // kinds of ground fade into the same sky where they meet. float chunkEdge = (float) (mc.gameSettings.renderDistanceChunks * 16.0D); float farEdge = 1600.0F; float fogStart = Math.max(chunkEdge * 0.8F, 128.0F); float fogEnd = chunkEdge + farEdge; float fogRed = 0.47F; float fogGreen = 0.65F; float fogBlue = 1.0F; GlStateManager.pushMatrix(); GlStateManager.disableAlpha(); GlStateManager.disableLighting(); GlStateManager.depthMask(true); GlStateManager.enableDepth(); GlStateManager.disableCull(); GlStateManager.shadeModel(7425); prog.use(); GL20.glUniformMatrix4(prog.uniform("uProj"), false, projection); GL20.glUniformMatrix4(prog.uniform("uModel"), false, model); GL20.glUniform3f(prog.uniform("uCam"), (float) camX, (float) camY, (float) camZ); GL20.glUniform4f(prog.uniform("uFogColour"), fogRed, fogGreen, fogBlue, 1.0F); GL20.glUniform1f(prog.uniform("uFogStart"), fogStart); GL20.glUniform1f(prog.uniform("uFogEnd"), fogEnd); for (java.util.Map.Entry entry : REGIONS.entrySet()) { Long key = entry.getKey(); Mesh mesh = MESHES.get(key); if (mesh == null) { mesh = build(entry.getValue()); MESHES.put(key, mesh); } mesh.draw(); drawn++; } if (drawn > 0 && !announced) { announced = true; LOGGER.info("RustNotMC: the far view is drawn: " + drawn + " regions"); } prog.unuse(); GlStateManager.shadeModel(7424); GlStateManager.enableCull(); GlStateManager.enableLighting(); GlStateManager.enableAlpha(); GlStateManager.popMatrix(); } /** Builds the mesh of one region. */ private static Mesh build(Packets.LodRegion region) { int size = region.size; int step = region.step; int cells = (size - 1) * (size - 1); int vertices = cells * 6; ByteBuffer data = ByteBuffer.allocateDirect(vertices * 16).order(ByteOrder.nativeOrder()); // the sun of the far view: the same direction from the north-west // and above, so that the far ground is not flat colour float sunX = -0.4629F; float sunY = 0.8165F; float sunZ = -0.3492F; for (int z = 0; z < size - 1; z++) { for (int x = 0; x < size - 1; x++) { int at = z * size + x; int h00 = region.heights[at] & 0xFF; int h10 = region.heights[at + 1] & 0xFF; int h01 = region.heights[at + size] & 0xFF; int h11 = region.heights[at + size + 1] & 0xFF; float x0 = region.originX + x * step; float x1 = x0 + step; float z0 = region.originZ + z * step; float z1 = z0 + step; int c = at * 3; float r = (region.colors[c] & 0xFF) / 255.0F; float g = (region.colors[c + 1] & 0xFF) / 255.0F; float b = (region.colors[c + 2] & 0xFF) / 255.0F; // the slope of the cell: the client works out its own light // here, because the far view has no blocks for the game to // light, only the shape of the ground it has already got float dx = (h10 + h11 - h00 - h01) / (2.0F * step); float dz = (h01 + h11 - h00 - h10) / (2.0F * step); float length = (float) Math.sqrt(dx * dx + dz * dz + 1.0F); float facing = (-dx * sunX + sunY - dz * sunZ) / length; float light = Math.max(0.42F, (facing - 0.35F) * 0.95F + 0.72F); float lr = Math.min(1.0F, r * light); float lg = Math.min(1.0F, g * light); float lb = Math.min(1.0F, b * light); // two triangles: the first one along the diagonal, the second // one the other way, so that the ground is never torn vertex(data, x0, h00, z0, lr, lg, lb); vertex(data, x1, h10, z0, lr, lg, lb); vertex(data, x1, h11, z1, lr, lg, lb); vertex(data, x0, h00, z0, lr, lg, lb); vertex(data, x1, h11, z1, lr, lg, lb); vertex(data, x0, h01, z1, lr, lg, lb); } } data.flip(); int id = GL15.glGenBuffers(); GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, id); GL15.glBufferData(GL15.GL_ARRAY_BUFFER, data, GL15.GL_STATIC_DRAW); GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0); return new Mesh(id, vertices); } /** One vertex of the far view: a place and a colour. */ private static void vertex(ByteBuffer data, float x, float y, float z, float r, float g, float b) { data.putFloat(x); data.putFloat(y); data.putFloat(z); data.put((byte) (r * 255.0F)); data.put((byte) (g * 255.0F)); data.put((byte) (b * 255.0F)); data.put((byte) 255); } /** What the driver says about a shader that did not build. */ private static String infoLog(int shader) { java.nio.IntBuffer length = BufferUtils.createIntBuffer(1); java.nio.ByteBuffer text = BufferUtils.createByteBuffer(8192); GL20.glGetShaderInfoLog(shader, length, text); byte[] bytes = new byte[Math.max(0, length.get(0))]; text.get(bytes); return new String(bytes, java.nio.charset.StandardCharsets.UTF_8); } /** What the driver says about a program that did not link. */ private static String programLog(int program) { java.nio.IntBuffer length = BufferUtils.createIntBuffer(1); java.nio.ByteBuffer text = BufferUtils.createByteBuffer(8192); GL20.glGetProgramInfoLog(program, length, text); byte[] bytes = new byte[Math.max(0, length.get(0))]; text.get(bytes); return new String(bytes, java.nio.charset.StandardCharsets.UTF_8); } /** Builds the shader of the far view. */ private static Program program() { if (program != null) { return program.ok ? program : null; } String vertex = "#version 330\n" + "in vec3 aPos;\n" + "in vec4 aColour;\n" + "uniform mat4 uProj;\n" + "uniform mat4 uModel;\n" + "uniform vec3 uCam;\n" + "uniform vec4 uFogColour;\n" + "uniform float uFogStart;\n" + "uniform float uFogEnd;\n" + "out vec4 vColour;\n" + "out float vDistance;\n" + "void main() {\n" + " vColour = aColour;\n" + " vDistance = length(aPos - uCam);\n" + " gl_Position = uProj * uModel * vec4(aPos, 1.0);\n" + "}\n"; String fragment = "#version 330\n" + "in vec4 vColour;\n" + "in float vDistance;\n" + "uniform vec4 uFogColour;\n" + "uniform float uFogStart;\n" + "uniform float uFogEnd;\n" + "out vec4 fragColour;\n" + "void main() {\n" + " float f = clamp((vDistance - uFogStart) / max(uFogEnd - uFogStart, 0.001)," + " 0.0, 1.0);\n" + " fragColour = vec4(mix(vColour.rgb, uFogColour.rgb, f), 1.0);\n" + "}\n"; program = Program.build(vertex, fragment); if (!program.ok) { LOGGER.error("RustNotMC: LOD: the shader would not build"); } return program.ok ? program : null; } /** The mesh of one region. */ private static final class Mesh { /** The buffer of the vertices. */ final int buffer; /** How many vertices there are. */ final int count; Mesh(int buffer, int count) { this.buffer = buffer; this.count = count; } /** Draws the mesh. */ void draw() { GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, this.buffer); GL20.glEnableVertexAttribArray(0); GL20.glEnableVertexAttribArray(1); GL20.glVertexAttribPointer(0, 3, GL11.GL_FLOAT, false, 16, 0L); GL20.glVertexAttribPointer(1, 4, GL11.GL_UNSIGNED_BYTE, true, 16, 12L); GL11.glDrawArrays(GL11.GL_TRIANGLES, 0, this.count); GL20.glDisableVertexAttribArray(1); GL20.glDisableVertexAttribArray(0); GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0); } /** Throws the mesh away. */ void free() { GL15.glDeleteBuffers(this.buffer); } } /** A shader program of the far view. */ private static final class Program { /** The program itself. */ final int id; /** Whether the program is good. */ final boolean ok; /** The names of the uniforms. */ final java.util.Map uniforms = new java.util.HashMap(); private Program(int id, boolean ok) { this.id = id; this.ok = ok; } /** Builds a program, or says why it did not build. */ static Program build(String vertex, String fragment) { int vs = GL20.glCreateShader(GL20.GL_VERTEX_SHADER); GL20.glShaderSource(vs, vertex); GL20.glCompileShader(vs); if (GL20.glGetShaderi(vs, GL20.GL_COMPILE_STATUS) == GL11.GL_FALSE) { String log = infoLog(vs); GL20.glDeleteShader(vs); LOGGER.error("RustNotMC: LOD: the vertex shader: " + log); return new Program(0, false); } int fs = GL20.glCreateShader(GL20.GL_FRAGMENT_SHADER); GL20.glShaderSource(fs, fragment); GL20.glCompileShader(fs); if (GL20.glGetShaderi(fs, GL20.GL_COMPILE_STATUS) == GL11.GL_FALSE) { String log = infoLog(fs); GL20.glDeleteShader(fs); GL20.glDeleteShader(vs); LOGGER.error("RustNotMC: LOD: the fragment shader: " + log); return new Program(0, false); } int program = GL20.glCreateProgram(); GL20.glAttachShader(program, vs); GL20.glAttachShader(program, fs); GL20.glBindAttribLocation(program, 0, "aPos"); GL20.glBindAttribLocation(program, 1, "aColour"); GL20.glLinkProgram(program); if (GL20.glGetProgrami(program, GL20.GL_LINK_STATUS) == GL11.GL_FALSE) { String log = programLog(program); GL20.glDeleteProgram(program); LOGGER.error("RustNotMC: LOD: the program: " + log); return new Program(0, false); } return new Program(program, true); } /** Uses the program. */ void use() { GL20.glUseProgram(this.id); } /** Stops using the program. */ void unuse() { GL20.glUseProgram(0); } /** The place of a uniform. */ int uniform(String name) { Integer known = this.uniforms.get(name); if (known != null) { return known.intValue(); } int at = GL20.glGetUniformLocation(this.id, name); this.uniforms.put(name, Integer.valueOf(at)); return at; } } }