From f3c9a4ad92b1b8fc23f2c23a9db3506ebe0019f2 Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Thu, 7 Jul 2022 21:12:34 +0200 Subject: [PATCH] Added improved physics, and optimized chunk mesh generation. --- .../java/nl/andrewl/aos2_client/Client.java | 12 +- .../andrewl/aos2_client/render/ChunkMesh.java | 8 +- .../render/ChunkMeshGenerator.java | 109 ++++++++++++------ .../java/nl/andrewl/aos_core/model/Chunk.java | 20 +++- .../java/nl/andrewl/aos_core/model/World.java | 39 +++++++ .../nl/andrewl/aos_core/model/WorldTest.java | 33 ++++++ server/pom.xml | 27 +++++ .../java/nl/andrewl/aos2_server/Server.java | 12 +- .../nl/andrewl/aos2_server/WorldUpdater.java | 61 ++++++++-- 9 files changed, 261 insertions(+), 60 deletions(-) create mode 100644 core/src/test/java/nl/andrewl/aos_core/model/WorldTest.java diff --git a/client/src/main/java/nl/andrewl/aos2_client/Client.java b/client/src/main/java/nl/andrewl/aos2_client/Client.java index ec0d2c9..654373b 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/Client.java +++ b/client/src/main/java/nl/andrewl/aos2_client/Client.java @@ -1,6 +1,7 @@ package nl.andrewl.aos2_client; import nl.andrewl.aos2_client.render.ChunkMesh; +import nl.andrewl.aos2_client.render.ChunkMeshGenerator; import nl.andrewl.aos2_client.render.ChunkRenderer; import nl.andrewl.aos2_client.render.WindowUtils; import nl.andrewl.aos_core.model.World; @@ -22,10 +23,10 @@ public class Client implements Runnable { client.run(); } - private InetAddress serverAddress; - private int serverPort; - private String username; - private CommunicationHandler communicationHandler; + private final InetAddress serverAddress; + private final int serverPort; + private final String username; + private final CommunicationHandler communicationHandler; private ChunkRenderer chunkRenderer; private int clientId; @@ -46,6 +47,7 @@ public class Client implements Runnable { var windowInfo = WindowUtils.initUI(); long windowHandle = windowInfo.windowHandle(); chunkRenderer = new ChunkRenderer(windowInfo.width(), windowInfo.height()); + ChunkMeshGenerator meshGenerator = new ChunkMeshGenerator(); try { this.clientId = communicationHandler.establishConnection(serverAddress, serverPort, username); @@ -62,7 +64,7 @@ public class Client implements Runnable { e.printStackTrace(); } for (var chunk : world.getChunkMap().values()) { - chunkRenderer.addChunkMesh(new ChunkMesh(chunk)); + chunkRenderer.addChunkMesh(new ChunkMesh(chunk, meshGenerator)); } glfwSetCursorPosCallback(windowHandle, cam); diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/ChunkMesh.java b/client/src/main/java/nl/andrewl/aos2_client/render/ChunkMesh.java index 98f0e44..23517b2 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/render/ChunkMesh.java +++ b/client/src/main/java/nl/andrewl/aos2_client/render/ChunkMesh.java @@ -17,7 +17,7 @@ public class ChunkMesh { private final int[] positionData; private final Chunk chunk; - public ChunkMesh(Chunk chunk) { + public ChunkMesh(Chunk chunk, ChunkMeshGenerator meshGenerator) { this.chunk = chunk; this.positionData = new int[]{chunk.getPosition().x, chunk.getPosition().y, chunk.getPosition().z}; @@ -25,7 +25,7 @@ public class ChunkMesh { this.eboId = glGenBuffers(); this.vaoId = glGenVertexArrays(); - loadMesh(); + loadMesh(meshGenerator); initVertexArrayAttributes(); } @@ -37,9 +37,9 @@ public class ChunkMesh { /** * Generates and loads this chunk's mesh into the allocated OpenGL buffers. */ - private void loadMesh() { + private void loadMesh(ChunkMeshGenerator meshGenerator) { long start = System.nanoTime(); - var meshData = ChunkMeshGenerator.generateMesh(chunk); + var meshData = meshGenerator.generateMesh(chunk); double dur = (System.nanoTime() - start) / 1_000_000.0; this.indexCount = meshData.indexBuffer().limit(); // Print some debug information. diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/ChunkMeshGenerator.java b/client/src/main/java/nl/andrewl/aos2_client/render/ChunkMeshGenerator.java index 8bceb91..6758bdd 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/render/ChunkMeshGenerator.java +++ b/client/src/main/java/nl/andrewl/aos2_client/render/ChunkMeshGenerator.java @@ -2,22 +2,35 @@ package nl.andrewl.aos2_client.render; import nl.andrewl.aos_core.model.Chunk; import org.joml.Vector3f; +import org.joml.Vector3i; import org.lwjgl.BufferUtils; import java.nio.FloatBuffer; import java.nio.IntBuffer; -import java.util.List; - +/** + * Highly-optimized class for generating chunk meshes, without any heap + * allocations at runtime. Not thread safe. + */ public final class ChunkMeshGenerator { - private ChunkMeshGenerator() {} + private final FloatBuffer vertexBuffer; + private final IntBuffer indexBuffer; - public static ChunkMeshData generateMesh(Chunk chunk) { - FloatBuffer vertexBuffer = BufferUtils.createFloatBuffer(300000); - IntBuffer indexBuffer = BufferUtils.createIntBuffer(100000); + private final Vector3i pos = new Vector3i(); + private final Vector3f color = new Vector3f(); + private final Vector3f norm = new Vector3f(); + + public ChunkMeshGenerator() { + vertexBuffer = BufferUtils.createFloatBuffer(300_000); + indexBuffer = BufferUtils.createIntBuffer(100_000); + } + + public ChunkMeshData generateMesh(Chunk chunk) { + vertexBuffer.clear(); + indexBuffer.clear(); int idx = 0; for (int i = 0; i < Chunk.TOTAL_SIZE; i++) { - var pos = Chunk.idxToXyz(i); + Chunk.idxToXyz(i, pos); int x = pos.x; int y = pos.y; int z = pos.z; @@ -26,52 +39,82 @@ public final class ChunkMeshGenerator { continue; // Don't render empty blocks. } - Vector3f color = Chunk.getColor(block); + Chunk.getColor(block, color); // See /design/block_rendering.svg for a diagram of how these vertices are defined. - var a = new Vector3f(x, y + 1, z + 1); - var b = new Vector3f(x, y + 1, z); - var c = new Vector3f(x, y, z); - var d = new Vector3f(x, y, z + 1); - var e = new Vector3f(x + 1, y + 1, z); - var f = new Vector3f(x + 1, y + 1, z + 1); - var g = new Vector3f(x + 1, y, z + 1); - var h = new Vector3f(x + 1, y, z); +// var a = new Vector3f(x, y + 1, z + 1); +// var b = new Vector3f(x, y + 1, z); +// var c = new Vector3f(x, y, z); +// var d = new Vector3f(x, y, z + 1); +// var e = new Vector3f(x + 1, y + 1, z); +// var f = new Vector3f(x + 1, y + 1, z + 1); +// var g = new Vector3f(x + 1, y, z + 1); +// var h = new Vector3f(x + 1, y, z); // Top if (chunk.getBlockAt(x, y + 1, z) == 0) { - var norm = new Vector3f(0, 1, 0); - genFace(vertexBuffer, indexBuffer, idx, color, norm, List.of(a, f, e, b)); + norm.set(0, 1, 0); + genFace(idx, + x, y+1, z+1, // a + x+1, y+1, z+1, // f + x+1, y+1, z, // e + x, y+1, z // b + ); idx += 4; } // Bottom if (chunk.getBlockAt(x, y - 1, z) == 0) { - var norm = new Vector3f(0, -1, 0); - genFace(vertexBuffer, indexBuffer, idx, color, norm, List.of(c, h, g, d)); + norm.set(0, -1, 0);// c h g d + genFace(idx, + x, y, z, // c + x+1, y, z, // h + x+1, y, z+1, // g + x, y, z+1 // d + ); idx += 4; } // Positive z if (chunk.getBlockAt(x, y, z + 1) == 0) { - var norm = new Vector3f(0, 0, 1); - genFace(vertexBuffer, indexBuffer, idx, color, norm, List.of(f, a, d, g)); + norm.set(0, 0, 1); + genFace(idx, + x+1, y+1, z+1, // f + x, y+1, z+1, // a + x, y, z+1, // d + x+1, y, z+1 // g + ); idx += 4; } // Negative z if (chunk.getBlockAt(x, y, z - 1) == 0) { - var norm = new Vector3f(0, 0, -1); - genFace(vertexBuffer, indexBuffer, idx, color, norm, List.of(b, e, h, c)); + norm.set(0, 0, -1); + genFace(idx, + x, y+1, z, // b + x+1, y+1, z, // e + x+1, y, z, // h + x, y, z // c + ); idx += 4; } // Positive x if (chunk.getBlockAt(x + 1, y, z) == 0) { - var norm = new Vector3f(1, 0, 0); - genFace(vertexBuffer, indexBuffer, idx, color, norm, List.of(e, f, g, h)); + norm.set(1, 0, 0); + genFace(idx, + x+1, y+1, z, // e + x+1, y+1, z+1, // f + x+1, y, z+1, // g + x+1, y, z // h + ); idx += 4; } // Negative x if (chunk.getBlockAt(x - 1, y, z) == 0) { - var norm = new Vector3f(-1, 0, 0); - genFace(vertexBuffer, indexBuffer, idx, color, norm, List.of(a, b, c, d)); + norm.set(-1, 0, 0); + genFace(idx, + x, y+1, z+1, // a + x, y+1, z, // b + x, y, z, // c + x, y, z+1 // d + ); idx += 4; } } @@ -79,11 +122,11 @@ public final class ChunkMeshGenerator { return new ChunkMeshData(vertexBuffer.flip(), indexBuffer.flip()); } - private static void genFace(FloatBuffer vertexBuffer, IntBuffer indexBuffer, int currentIndex, Vector3f color, Vector3f norm, List vertices) { - for (var vertex : vertices) { - vertexBuffer.put(vertex.x); - vertexBuffer.put(vertex.y); - vertexBuffer.put(vertex.z); + private void genFace(int currentIndex, float... vertices) { + for (int i = 0; i < 12; i += 3) { + vertexBuffer.put(vertices[i]); + vertexBuffer.put(vertices[i+1]); + vertexBuffer.put(vertices[i+2]); vertexBuffer.put(color.x); vertexBuffer.put(color.y); vertexBuffer.put(color.z); diff --git a/core/src/main/java/nl/andrewl/aos_core/model/Chunk.java b/core/src/main/java/nl/andrewl/aos_core/model/Chunk.java index 07973d3..1478b7d 100644 --- a/core/src/main/java/nl/andrewl/aos_core/model/Chunk.java +++ b/core/src/main/java/nl/andrewl/aos_core/model/Chunk.java @@ -67,12 +67,17 @@ public class Chunk { * @return The 3D coordinate, or -1, -1, -1 if the index is out of bounds. */ public static Vector3i idxToXyz(int idx) { - if (idx < 0 || idx >= TOTAL_SIZE) return new Vector3i(-1, -1, -1); - int x = idx / (SIZE * SIZE); + Vector3i vec = new Vector3i(-1, -1, -1); + idxToXyz(idx, vec); + return vec; + } + + public static void idxToXyz(int idx, Vector3i vec) { + if (idx < 0 || idx >= TOTAL_SIZE) return; + vec.x = idx / (SIZE * SIZE); int remainder = idx % (SIZE * SIZE); - int y = remainder / SIZE; - int z = remainder % SIZE; - return new Vector3i(x, y, z); + vec.y = remainder / SIZE; + vec.z = remainder % SIZE; } public byte getBlockAt(int x, int y, int z) { @@ -126,4 +131,9 @@ public class Chunk { float v = blockValue / 127.0f; return new Vector3f(v); } + + public static void getColor(byte blockValue, Vector3f vec) { + float v = blockValue / 127.0f; + vec.set(v); + } } diff --git a/core/src/main/java/nl/andrewl/aos_core/model/World.java b/core/src/main/java/nl/andrewl/aos_core/model/World.java index bb9fb42..050d7f6 100644 --- a/core/src/main/java/nl/andrewl/aos_core/model/World.java +++ b/core/src/main/java/nl/andrewl/aos_core/model/World.java @@ -1,5 +1,7 @@ package nl.andrewl.aos_core.model; +import org.joml.Math; +import org.joml.Vector3f; import org.joml.Vector3i; import org.joml.Vector3ic; @@ -17,6 +19,30 @@ public class World { return chunkMap; } + public byte getBlockAt(Vector3f pos) { + Vector3i chunkPos = getChunkPosAt(pos); + Chunk chunk = chunkMap.get(chunkPos); + if (chunk == null) return 0; + Vector3i blockPos = new Vector3i( + (int) Math.floor(pos.x - chunkPos.x * Chunk.SIZE), + (int) Math.floor(pos.y - chunkPos.y * Chunk.SIZE), + (int) Math.floor(pos.z - chunkPos.z * Chunk.SIZE) + ); + return chunk.getBlockAt(blockPos); + } + + public void setBlockAt(Vector3f pos, byte block) { + Vector3i chunkPos = getChunkPosAt(pos); + Chunk chunk = chunkMap.get(chunkPos); + if (chunk == null) return; + Vector3i blockPos = new Vector3i( + (int) Math.floor(pos.x - chunkPos.x * Chunk.SIZE), + (int) Math.floor(pos.y - chunkPos.y * Chunk.SIZE), + (int) Math.floor(pos.z - chunkPos.z * Chunk.SIZE) + ); + chunk.setBlockAt(blockPos.x, blockPos.y, blockPos.z, block); + } + public byte getBlockAt(int x, int y, int z) { int chunkX = x / Chunk.SIZE; int localX = x % Chunk.SIZE; @@ -33,4 +59,17 @@ public class World { public Chunk getChunkAt(Vector3i chunkPos) { return chunkMap.get(chunkPos); } + + /** + * Gets the coordinates of a chunk at a given world position. + * @param worldPos The world position. + * @return The chunk position. Note that this may not correspond to any existing chunk. + */ + public static Vector3i getChunkPosAt(Vector3f worldPos) { + return new Vector3i( + (int) Math.floor(worldPos.x / Chunk.SIZE), + (int) Math.floor(worldPos.y / Chunk.SIZE), + (int) Math.floor(worldPos.z / Chunk.SIZE) + ); + } } diff --git a/core/src/test/java/nl/andrewl/aos_core/model/WorldTest.java b/core/src/test/java/nl/andrewl/aos_core/model/WorldTest.java new file mode 100644 index 0000000..f8f7d86 --- /dev/null +++ b/core/src/test/java/nl/andrewl/aos_core/model/WorldTest.java @@ -0,0 +1,33 @@ +package nl.andrewl.aos_core.model; + +import org.joml.Vector3f; +import org.joml.Vector3i; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class WorldTest { + @Test + public void testGetBlockAt() { + Chunk chunk = new Chunk(0, 0, 0); + chunk.setBlockAt(1, 0, 0, (byte) 1); + World world = new World(); + world.addChunk(chunk); + assertEquals(1, world.getBlockAt(new Vector3f(1, 0, 0))); + assertEquals(1, world.getBlockAt(new Vector3f(1.9f, 0, 0))); + assertEquals(1, world.getBlockAt(new Vector3f(1.5f, 0.7f, 0.3f))); + } + + @Test + public void testGetChunkPosAt() { + assertEquals(new Vector3i(0, 0, 0), World.getChunkPosAt(new Vector3f(0, 0, 0))); + assertEquals(new Vector3i(0, 0, 0), World.getChunkPosAt(new Vector3f(1, 0, 0))); + assertEquals(new Vector3i(0, 0, 0), World.getChunkPosAt(new Vector3f(0, 0.5f, 0))); + assertEquals(new Vector3i(0, 0, 0), World.getChunkPosAt(new Vector3f(Chunk.SIZE - 1, 0, 0))); + assertEquals(new Vector3i(1, 0, 0), World.getChunkPosAt(new Vector3f(Chunk.SIZE, 0, 0))); + assertEquals(new Vector3i(0, 0, -1), World.getChunkPosAt(new Vector3f(0, 0, -0.0001f))); + assertEquals(new Vector3i(0, 0, 0), World.getChunkPosAt(new Vector3f(Chunk.SIZE / 2f, Chunk.SIZE / 2f, Chunk.SIZE / 2f))); + assertEquals(new Vector3i(1, 1, 1), World.getChunkPosAt(new Vector3f(Chunk.SIZE, Chunk.SIZE, Chunk.SIZE))); + assertEquals(new Vector3i(4, 4, 4), World.getChunkPosAt(new Vector3f(Chunk.SIZE * 5 - 1, Chunk.SIZE * 5 - 1, Chunk.SIZE * 5 - 1))); + } +} diff --git a/server/pom.xml b/server/pom.xml index 288e8a8..a2a44f9 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -23,4 +23,31 @@ ${parent.version} + + + + + maven-assembly-plugin + + + + nl.andrewl.aos2_server.Server + + + + jar-with-dependencies + + + + + make-assembly + package + + single + + + + + + \ No newline at end of file diff --git a/server/src/main/java/nl/andrewl/aos2_server/Server.java b/server/src/main/java/nl/andrewl/aos2_server/Server.java index 84459b9..850d4a5 100644 --- a/server/src/main/java/nl/andrewl/aos2_server/Server.java +++ b/server/src/main/java/nl/andrewl/aos2_server/Server.java @@ -8,6 +8,7 @@ import nl.andrewl.aos_core.net.udp.ClientOrientationState; import nl.andrewl.aos_core.net.udp.DatagramInit; import nl.andrewl.aos_core.net.udp.PlayerUpdateMessage; import nl.andrewl.record_net.Message; +import org.joml.Vector3f; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,16 +40,21 @@ public class Server implements Runnable { Random rand = new Random(1); this.world = new World(); for (int x = -5; x <= 5; x++) { - for (int y = 0; y <= 3; y++) { + for (int y = 0; y <= 5; y++) { for (int z = -3; z <= 3; z++) { Chunk chunk = new Chunk(x, y, z); - for (int i = 0; i < Chunk.TOTAL_SIZE; i++) { - chunk.getBlocks()[i] = (byte) rand.nextInt(20, 40); + if (y <= 3) { + for (int i = 0; i < Chunk.TOTAL_SIZE; i++) { + chunk.getBlocks()[i] = (byte) rand.nextInt(20, 40); + } } world.addChunk(chunk); } } } + world.setBlockAt(new Vector3f(5, 64, 5), (byte) 50); + world.setBlockAt(new Vector3f(5, 65, 6), (byte) 50); + world.setBlockAt(new Vector3f(5, 66, 7), (byte) 50); } @Override diff --git a/server/src/main/java/nl/andrewl/aos2_server/WorldUpdater.java b/server/src/main/java/nl/andrewl/aos2_server/WorldUpdater.java index 96b663c..f688bba 100644 --- a/server/src/main/java/nl/andrewl/aos2_server/WorldUpdater.java +++ b/server/src/main/java/nl/andrewl/aos2_server/WorldUpdater.java @@ -1,6 +1,8 @@ package nl.andrewl.aos2_server; +import nl.andrewl.aos_core.model.World; import nl.andrewl.aos_core.net.udp.PlayerUpdateMessage; +import org.joml.Math; import org.joml.Matrix4f; import org.joml.Vector3f; import org.slf4j.Logger; @@ -60,37 +62,76 @@ public class WorldUpdater implements Runnable { private void updatePlayerMovement(ServerPlayer player) { boolean updated = false; var v = player.getVelocity(); + var hv = new Vector3f(v.x, 0, v.z); var p = player.getPosition(); - // Apply deceleration to the player before computing any input-derived acceleration. - if (v.length() > 0) { - Vector3f deceleration = new Vector3f(v).negate().normalize().mul(0.1f); - v.add(deceleration); - if (v.length() < 0.1f) { - v.set(0); + // Check if we have a negative velocity that will cause us to fall through a block next tick. + float nextTickY = p.y + v.y; + if (server.getWorld().getBlockAt(new Vector3f(p.x, nextTickY, p.z)) != 0) { + // Find the first block we'll hit and set the player down on that. + int floorY = (int) Math.floor(p.y) - 1; + while (true) { + if (server.getWorld().getBlockAt(new Vector3f(p.x, floorY, p.z)) != 0) { + p.y = floorY + 1f; + v.y = 0; + break; + } else { + floorY--; + } } + } + + // Check if the player is on the ground. + boolean grounded = (Math.floor(p.y) == p.y && server.getWorld().getBlockAt(new Vector3f(p.x, p.y - 0.0001f, p.z)) != 0); + + if (!grounded) { + v.y -= 0.1f; + } + + // Apply horizontal deceleration to the player before computing any input-derived acceleration. + if (hv.length() > 0) { + Vector3f deceleration = new Vector3f(hv).negate().normalize().mul(0.1f); + hv.add(deceleration); + if (hv.length() < 0.1f) { + hv.set(0); + } + v.x = hv.x; + v.z = hv.z; updated = true; } Vector3f a = new Vector3f(); var inputState = player.getLastInputState(); + if (inputState.jumping() && grounded) { + v.y = 0.6f; + } + + // Compute horizontal motion separately. if (inputState.forward()) a.z -= 1; if (inputState.backward()) a.z += 1; if (inputState.left()) a.x -= 1; if (inputState.right()) a.x += 1; - if (inputState.jumping()) a.y += 1; // TODO: check if on ground. - if (inputState.crouching()) a.y -= 1; // TODO: do crouching instead of down. +// if (inputState.crouching()) a.y -= 1; // TODO: do crouching instead of down. if (a.lengthSquared() > 0) { a.normalize(); Matrix4f moveTransform = new Matrix4f(); moveTransform.rotate(player.getOrientation().x, new Vector3f(0, 1, 0)); moveTransform.transformDirection(a); - v.add(a); + hv.add(a); + final float maxSpeed = 0.25f; // Blocks per tick. - if (v.length() > maxSpeed) v.normalize(maxSpeed); + if (hv.length() > maxSpeed) { + hv.normalize(maxSpeed); + } + v.x = hv.x; + v.z = hv.z; updated = true; } + // Check if the player is colliding with the world. + + + // Apply velocity to the player's position. if (v.lengthSquared() > 0) { p.add(v); updated = true;