Files

440 lines
17 KiB
Java

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<Long, Packets.LodRegion> REGIONS =
new java.util.concurrent.ConcurrentHashMap<Long, Packets.LodRegion>();
/** The meshes that were built, by the corner of the region. */
private static final java.util.Map<Long, Mesh> MESHES =
new java.util.concurrent.ConcurrentHashMap<Long, Mesh>();
/** 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<Long, Packets.LodRegion> 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<String, Integer> uniforms =
new java.util.HashMap<String, Integer>();
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;
}
}
}