Added improved physics, and optimized chunk mesh generation.

This commit is contained in:
Andrew Lalis 2022-07-07 21:12:34 +02:00
parent f2b0e09979
commit f3c9a4ad92
9 changed files with 261 additions and 60 deletions

View File

@ -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);

View File

@ -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.

View File

@ -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<Vector3f> 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);

View File

@ -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);
}
}

View File

@ -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)
);
}
}

View File

@ -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)));
}
}

View File

@ -23,4 +23,31 @@
<version>${parent.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>nl.andrewl.aos2_server.Server</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -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);
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

View File

@ -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;