440 lines
17 KiB
Java
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;
|
|
}
|
|
}
|
|
}
|