Voxel Engine Prototype in Java

Introduction

A voxel engine is a 3D graphics engine that uses volumetric pixels (voxels) as the fundamental building blocks for creating and rendering 3D worlds. This prototype demonstrates core voxel engine concepts including chunk-based world management, mesh generation, and basic rendering.

Core Architecture

Voxel Data Structure

public class Voxel {
public static final byte TYPE_AIR = 0;
public static final byte TYPE_GRASS = 1;
public static final byte TYPE_DIRT = 2;
public static final byte TYPE_STONE = 3;
public static final byte TYPE_WATER = 4;
public static final byte TYPE_WOOD = 5;
public static final byte TYPE_LEAVES = 6;
private byte type;
private boolean transparent;
private boolean solid;
public Voxel(byte type) {
this.type = type;
updateProperties();
}
private void updateProperties() {
switch (type) {
case TYPE_AIR:
transparent = true;
solid = false;
break;
case TYPE_WATER:
transparent = true;
solid = false;
break;
case TYPE_LEAVES:
transparent = true;
solid = true;
break;
default:
transparent = false;
solid = true;
break;
}
}
// Getters
public byte getType() { return type; }
public boolean isTransparent() { return transparent; }
public boolean isSolid() { return solid; }
public void setType(byte type) {
this.type = type;
updateProperties();
}
// Static factory methods
public static Voxel air() { return new Voxel(TYPE_AIR); }
public static Voxel grass() { return new Voxel(TYPE_GRASS); }
public static Voxel dirt() { return new Voxel(TYPE_DIRT); }
public static Voxel stone() { return new Voxel(TYPE_STONE); }
public static Voxel water() { return new Voxel(TYPE_WATER); }
public static Voxel wood() { return new Voxel(TYPE_WOOD); }
public static Voxel leaves() { return new Voxel(TYPE_LEAVES); }
}

Chunk System

public class Chunk {
public static final int CHUNK_SIZE = 16;
public static final int CHUNK_HEIGHT = 256;
private final Voxel[][][] voxels;
private final int chunkX, chunkZ;
private boolean needsRebuild;
private ChunkMesh mesh;
public Chunk(int chunkX, int chunkZ) {
this.chunkX = chunkX;
this.chunkZ = chunkZ;
this.voxels = new Voxel[CHUNK_SIZE][CHUNK_HEIGHT][CHUNK_SIZE];
this.needsRebuild = true;
initializeVoxels();
}
private void initializeVoxels() {
// Fill with air initially
for (int x = 0; x < CHUNK_SIZE; x++) {
for (int y = 0; y < CHUNK_HEIGHT; y++) {
for (int z = 0; z < CHUNK_SIZE; z++) {
voxels[x][y][z] = Voxel.air();
}
}
}
}
public Voxel getVoxel(int x, int y, int z) {
if (isWithinBounds(x, y, z)) {
return voxels[x][y][z];
}
return Voxel.air();
}
public void setVoxel(int x, int y, int z, Voxel voxel) {
if (isWithinBounds(x, y, z)) {
voxels[x][y][z] = voxel;
needsRebuild = true;
// Notify neighboring chunks if on border
if (x == 0) notifyNeighbor(-1, 0);
if (x == CHUNK_SIZE - 1) notifyNeighbor(1, 0);
if (z == 0) notifyNeighbor(0, -1);
if (z == CHUNK_SIZE - 1) notifyNeighbor(0, 1);
}
}
private void notifyNeighbor(int dx, int dz) {
// This would notify the world to mark neighboring chunk for rebuild
// Implementation depends on world management
}
private boolean isWithinBounds(int x, int y, int z) {
return x >= 0 && x < CHUNK_SIZE && 
y >= 0 && y < CHUNK_HEIGHT && 
z >= 0 && z < CHUNK_SIZE;
}
public boolean isSolid(int x, int y, int z) {
Voxel voxel = getVoxel(x, y, z);
return voxel != null && voxel.isSolid();
}
public boolean isTransparent(int x, int y, int z) {
Voxel voxel = getVoxel(x, y, z);
return voxel != null && voxel.isTransparent();
}
public void generateMesh() {
this.mesh = new ChunkMesh(this);
this.needsRebuild = false;
}
public ChunkMesh getMesh() {
if (needsRebuild) {
generateMesh();
}
return mesh;
}
// World position conversion
public int getWorldX(int localX) {
return chunkX * CHUNK_SIZE + localX;
}
public int getWorldZ(int localZ) {
return chunkZ * CHUNK_SIZE + localZ;
}
// Getters
public int getChunkX() { return chunkX; }
public int getChunkZ() { return chunkZ; }
public boolean needsRebuild() { return needsRebuild; }
}

Mesh Generation

Chunk Mesh Generation

public class ChunkMesh {
private static final int VERTEX_SIZE = 8; // x, y, z, u, v, r, g, b
private static final int INDICES_PER_FACE = 6;
private FloatBuffer vertexBuffer;
private IntBuffer indexBuffer;
private int vertexCount;
private int indexCount;
public ChunkMesh(Chunk chunk) {
generateMesh(chunk);
}
private void generateMesh(Chunk chunk) {
List<Float> vertices = new ArrayList<>();
List<Integer> indices = new ArrayList<>();
for (int x = 0; x < Chunk.CHUNK_SIZE; x++) {
for (int y = 0; y < Chunk.CHUNK_HEIGHT; y++) {
for (int z = 0; z < Chunk.CHUNK_SIZE; z++) {
Voxel voxel = chunk.getVoxel(x, y, z);
if (voxel.isSolid()) {
generateVoxelMesh(chunk, x, y, z, voxel, vertices, indices);
}
}
}
}
createBuffers(vertices, indices);
}
private void generateVoxelMesh(Chunk chunk, int x, int y, int z, Voxel voxel, 
List<Float> vertices, List<Integer> indices) {
// Check each face for visibility
if (shouldRenderFace(chunk, x, y - 1, z)) { // Bottom face
addFace(x, y, z, Face.BOTTOM, voxel, vertices, indices);
}
if (shouldRenderFace(chunk, x, y + 1, z)) { // Top face
addFace(x, y, z, Face.TOP, voxel, vertices, indices);
}
if (shouldRenderFace(chunk, x - 1, y, z)) { // West face
addFace(x, y, z, Face.WEST, voxel, vertices, indices);
}
if (shouldRenderFace(chunk, x + 1, y, z)) { // East face
addFace(x, y, z, Face.EAST, voxel, vertices, indices);
}
if (shouldRenderFace(chunk, x, y, z - 1)) { // North face
addFace(x, y, z, Face.NORTH, voxel, vertices, indices);
}
if (shouldRenderFace(chunk, x, y, z + 1)) { // South face
addFace(x, y, z, Face.SOUTH, voxel, vertices, indices);
}
}
private boolean shouldRenderFace(Chunk chunk, int x, int y, int z) {
// Check if the neighboring voxel exists and is transparent
if (x < 0 || x >= Chunk.CHUNK_SIZE || 
z < 0 || z >= Chunk.CHUNK_SIZE || 
y < 0 || y >= Chunk.CHUNK_HEIGHT) {
return true; // Face at chunk border is always visible
}
Voxel neighbor = chunk.getVoxel(x, y, z);
return !neighbor.isSolid() || neighbor.isTransparent();
}
private void addFace(int x, int y, int z, Face face, Voxel voxel,
List<Float> vertices, List<Integer> indices) {
int baseVertex = vertexCount;
float[] faceVertices = face.getVertices(x, y, z);
float[] texCoords = getTextureCoordinates(voxel, face);
float[] color = getColor(voxel);
// Add vertices with texture coordinates and color
for (int i = 0; i < 4; i++) {
// Position
vertices.add(faceVertices[i * 3]);
vertices.add(faceVertices[i * 3 + 1]);
vertices.add(faceVertices[i * 3 + 2]);
// Texture coordinates
vertices.add(texCoords[i * 2]);
vertices.add(texCoords[i * 2 + 1]);
// Color
vertices.add(color[0]);
vertices.add(color[1]);
vertices.add(color[2]);
}
// Add indices (two triangles)
int[] faceIndices = face.getIndices();
for (int index : faceIndices) {
indices.add(baseVertex + index);
}
vertexCount += 4;
indexCount += 6;
}
private float[] getTextureCoordinates(Voxel voxel, Face face) {
// Simple texture coordinates - in real implementation, 
// this would look up from a texture atlas
switch (voxel.getType()) {
case Voxel.TYPE_GRASS:
if (face == Face.TOP) return new float[]{0, 0, 1, 0, 1, 1, 0, 1};
else if (face == Face.BOTTOM) return new float[]{0, 0, 1, 0, 1, 1, 0, 1};
else return new float[]{0, 0, 1, 0, 1, 1, 0, 1};
default:
return new float[]{0, 0, 1, 0, 1, 1, 0, 1};
}
}
private float[] getColor(Voxel voxel) {
// Simple color mapping
switch (voxel.getType()) {
case Voxel.TYPE_GRASS: return new float[]{0.2f, 0.8f, 0.2f};
case Voxel.TYPE_DIRT: return new float[]{0.5f, 0.3f, 0.1f};
case Voxel.TYPE_STONE: return new float[]{0.5f, 0.5f, 0.5f};
case Voxel.TYPE_WATER: return new float[]{0.2f, 0.2f, 0.8f};
case Voxel.TYPE_WOOD: return new float[]{0.5f, 0.3f, 0.1f};
case Voxel.TYPE_LEAVES: return new float[]{0.1f, 0.6f, 0.1f};
default: return new float[]{1.0f, 1.0f, 1.0f};
}
}
private void createBuffers(List<Float> vertices, List<Integer> indices) {
vertexBuffer = BufferUtils.createFloatBuffer(vertices.size());
for (Float v : vertices) {
vertexBuffer.put(v);
}
vertexBuffer.flip();
indexBuffer = BufferUtils.createIntBuffer(indices.size());
for (Integer i : indices) {
indexBuffer.put(i);
}
indexBuffer.flip();
}
public void render() {
if (vertexCount == 0) return;
// Enable vertex arrays
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);
// Set pointers
glVertexPointer(3, GL_FLOAT, VERTEX_SIZE * Float.BYTES, vertexBuffer);
glTexCoordPointer(2, GL_FLOAT, VERTEX_SIZE * Float.BYTES, 
vertexBuffer.position(3));
glColorPointer(3, GL_FLOAT, VERTEX_SIZE * Float.BYTES, 
vertexBuffer.position(5));
// Draw
glDrawElements(GL_TRIANGLES, indexBuffer);
// Disable vertex arrays
glDisableClientState(GL_VERTEX_ARRAY);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
glDisableClientState(GL_COLOR_ARRAY);
}
public void cleanup() {
// Clean up buffers
if (vertexBuffer != null) {
vertexBuffer.clear();
}
if (indexBuffer != null) {
indexBuffer.clear();
}
}
}
enum Face {
TOP, BOTTOM, NORTH, SOUTH, EAST, WEST;
public float[] getVertices(float x, float y, float z) {
switch (this) {
case TOP:
return new float[]{
x, y + 1, z,        // 0
x + 1, y + 1, z,    // 1
x + 1, y + 1, z + 1,// 2
x, y + 1, z + 1     // 3
};
case BOTTOM:
return new float[]{
x, y, z,            // 0
x, y, z + 1,        // 1
x + 1, y, z + 1,    // 2
x + 1, y, z         // 3
};
case NORTH:
return new float[]{
x, y, z,            // 0
x, y + 1, z,        // 1
x + 1, y + 1, z,    // 2
x + 1, y, z         // 3
};
case SOUTH:
return new float[]{
x, y, z + 1,        // 0
x + 1, y, z + 1,    // 1
x + 1, y + 1, z + 1,// 2
x, y + 1, z + 1     // 3
};
case EAST:
return new float[]{
x + 1, y, z,        // 0
x + 1, y + 1, z,    // 1
x + 1, y + 1, z + 1,// 2
x + 1, y, z + 1     // 3
};
case WEST:
return new float[]{
x, y, z,            // 0
x, y, z + 1,        // 1
x, y + 1, z + 1,    // 2
x, y + 1, z         // 3
};
default:
return new float[12];
}
}
public int[] getIndices() {
return new int[]{0, 1, 2, 0, 2, 3};
}
}

World Management

World Class

public class VoxelWorld {
private final Map<Long, Chunk> chunks;
private final int renderDistance;
private final WorldGenerator worldGenerator;
public VoxelWorld(int renderDistance) {
this.chunks = new ConcurrentHashMap<>();
this.renderDistance = renderDistance;
this.worldGenerator = new WorldGenerator();
}
public Chunk getChunk(int chunkX, int chunkZ) {
long key = getChunkKey(chunkX, chunkZ);
return chunks.computeIfAbsent(key, k -> {
Chunk chunk = new Chunk(chunkX, chunkZ);
worldGenerator.generateChunk(chunk);
return chunk;
});
}
public Voxel getVoxel(int worldX, int worldY, int worldZ) {
int chunkX = worldX >> 4; // Divide by 16
int chunkZ = worldZ >> 4;
int localX = worldX & 0xF; // Modulo 16
int localZ = worldZ & 0xF;
Chunk chunk = getChunk(chunkX, chunkZ);
return chunk.getVoxel(localX, worldY, localZ);
}
public void setVoxel(int worldX, int worldY, int worldZ, Voxel voxel) {
int chunkX = worldX >> 4;
int chunkZ = worldZ >> 4;
int localX = worldX & 0xF;
int localZ = worldZ & 0xF;
Chunk chunk = getChunk(chunkX, chunkZ);
chunk.setVoxel(localX, worldY, localZ, voxel);
}
public void updateChunks(int playerChunkX, int playerChunkZ) {
// Load chunks within render distance
for (int x = -renderDistance; x <= renderDistance; x++) {
for (int z = -renderDistance; z <= renderDistance; z++) {
int chunkX = playerChunkX + x;
int chunkZ = playerChunkZ + z;
getChunk(chunkX, chunkZ); // This will generate if needed
}
}
// Unload chunks outside render distance
chunks.entrySet().removeIf(entry -> {
long key = entry.getKey();
int chunkX = (int)(key >> 32);
int chunkZ = (int)(key & 0xFFFFFFFF);
int distX = Math.abs(chunkX - playerChunkX);
int distZ = Math.abs(chunkZ - playerChunkZ);
return distX > renderDistance || distZ > renderDistance;
});
}
public void render() {
for (Chunk chunk : chunks.values()) {
ChunkMesh mesh = chunk.getMesh();
if (mesh != null) {
glPushMatrix();
glTranslatef(chunk.getChunkX() * Chunk.CHUNK_SIZE, 0, 
chunk.getChunkZ() * Chunk.CHUNK_SIZE);
mesh.render();
glPopMatrix();
}
}
}
private long getChunkKey(int x, int z) {
return ((long)x << 32) | (z & 0xFFFFFFFFL);
}
public void cleanup() {
for (Chunk chunk : chunks.values()) {
ChunkMesh mesh = chunk.getMesh();
if (mesh != null) {
mesh.cleanup();
}
}
chunks.clear();
}
}

World Generation

Procedural World Generator

public class WorldGenerator {
private final SimplexNoise terrainNoise;
private final SimplexNoise caveNoise;
private final Random random;
public WorldGenerator() {
this.random = new Random();
this.terrainNoise = new SimplexNoise(12345);
this.caveNoise = new SimplexNoise(67890);
}
public void generateChunk(Chunk chunk) {
int chunkX = chunk.getChunkX();
int chunkZ = chunk.getChunkZ();
for (int x = 0; x < Chunk.CHUNK_SIZE; x++) {
for (int z = 0; z < Chunk.CHUNK_SIZE; z++) {
int worldX = chunk.getWorldX(x);
int worldZ = chunk.getWorldZ(z);
generateColumn(chunk, x, z, worldX, worldZ);
}
}
}
private void generateColumn(Chunk chunk, int x, int z, int worldX, int worldZ) {
// Generate terrain height
double noise = terrainNoise.noise(worldX * 0.01, worldZ * 0.01);
double roughness = terrainNoise.noise(worldX * 0.05, worldZ * 0.05) * 0.3;
int height = (int)((noise * 0.5 + 0.5) * 30 + roughness * 10 + 50);
// Generate caves
boolean hasCave = caveNoise.noise(worldX * 0.1, worldZ * 0.1) > 0.2;
for (int y = 0; y < Chunk.CHUNK_HEIGHT; y++) {
Voxel voxel = determineVoxel(y, height, hasCave, worldX, worldZ, y);
chunk.setVoxel(x, y, z, voxel);
}
}
private Voxel determineVoxel(int y, int surfaceHeight, boolean hasCave, 
int worldX, int worldZ, int localY) {
if (y > surfaceHeight) {
if (y < 60) return Voxel.water();
return Voxel.air();
}
// Generate caves
if (hasCave && y < surfaceHeight - 5 && y > 10) {
double caveNoise = caveNoise.noise(worldX * 0.2, localY * 0.2, worldZ * 0.2);
if (caveNoise > 0.1) {
return Voxel.air();
}
}
// Surface layers
if (y == surfaceHeight) {
return Voxel.grass();
} else if (y > surfaceHeight - 5) {
return Voxel.dirt();
} else if (y > surfaceHeight - 15) {
// Occasionally generate ore veins
if (random.nextFloat() < 0.02f) {
return Voxel.stone(); // In real implementation, this would be an ore type
}
return Voxel.stone();
} else {
return Voxel.stone();
}
}
public void generateTree(Chunk chunk, int x, int y, int z) {
// Generate trunk
int trunkHeight = 4 + random.nextInt(3);
for (int i = 0; i < trunkHeight; i++) {
if (y + i < Chunk.CHUNK_HEIGHT) {
chunk.setVoxel(x, y + i, z, Voxel.wood());
}
}
// Generate leaves
int leafStart = y + trunkHeight - 2;
for (int dx = -2; dx <= 2; dx++) {
for (int dz = -2; dz <= 2; dz++) {
for (int dy = 0; dy < 3; dy++) {
if (Math.abs(dx) + Math.abs(dz) + Math.abs(dy) <= 3) {
int leafX = x + dx;
int leafY = leafStart + dy;
int leafZ = z + dz;
if (leafX >= 0 && leafX < Chunk.CHUNK_SIZE &&
leafZ >= 0 && leafZ < Chunk.CHUNK_SIZE &&
leafY < Chunk.CHUNK_HEIGHT) {
chunk.setVoxel(leafX, leafY, leafZ, Voxel.leaves());
}
}
}
}
}
}
}
// Simple Simplex Noise implementation
public class SimplexNoise {
private final PermutationTable permutation;
public SimplexNoise(long seed) {
this.permutation = new PermutationTable(seed);
}
public double noise(double x, double y) {
// 2D Simplex noise implementation
// Simplified version - full implementation would be more complex
double s = (x + y) * 0.366025403784439; // F2
int i = (int)Math.floor(x + s);
int j = (int)Math.floor(y + s);
double t = (i + j) * 0.211324865405187; // G2
double X0 = i - t;
double Y0 = j - t;
double x0 = x - X0;
double y0 = y - Y0;
// Determine which simplex we're in
int i1, j1;
if (x0 > y0) {
i1 = 1; j1 = 0;
} else {
i1 = 0; j1 = 1;
}
double x1 = x0 - i1 + 0.211324865405187;
double y1 = y0 - j1 + 0.211324865405187;
double x2 = x0 - 1.0 + 0.422649730810374;
double y2 = y0 - 1.0 + 0.422649730810374;
// Calculate contributions
double n0, n1, n2;
double t0 = 0.5 - x0 * x0 - y0 * y0;
if (t0 < 0) n0 = 0;
else {
t0 *= t0;
n0 = t0 * t0 * grad(permutation.perm(i, j), x0, y0);
}
double t1 = 0.5 - x1 * x1 - y1 * y1;
if (t1 < 0) n1 = 0;
else {
t1 *= t1;
n1 = t1 * t1 * grad(permutation.perm(i + i1, j + j1), x1, y1);
}
double t2 = 0.5 - x2 * x2 - y2 * y2;
if (t2 < 0) n2 = 0;
else {
t2 *= t2;
n2 = t2 * t2 * grad(permutation.perm(i + 1, j + 1), x2, y2);
}
return 70.0 * (n0 + n1 + n2);
}
public double noise(double x, double y, double z) {
// 3D Simplex noise - similar structure but more complex
// Implementation would follow similar pattern to 2D but with 3 dimensions
return noise(x, y); // Simplified for prototype
}
private double grad(int hash, double x, double y) {
int h = hash & 7;
double u = h < 4 ? x : y;
double v = h < 4 ? y : x;
return ((h & 1) != 0 ? -u : u) + ((h & 2) != 0 ? -2.0 * v : 2.0 * v);
}
private static class PermutationTable {
private final int[] perm;
public PermutationTable(long seed) {
perm = new int[512];
Random random = new Random(seed);
int[] p = new int[256];
for (int i = 0; i < 256; i++) p[i] = i;
// Shuffle array
for (int i = 0; i < 256; i++) {
int j = random.nextInt(256 - i) + i;
int temp = p[i];
p[i] = p[j];
p[j] = temp;
}
// Duplicate for overflow
for (int i = 0; i < 512; i++) {
perm[i] = p[i & 255];
}
}
public int perm(int x, int y) {
return perm[perm[x] + y];
}
public int perm(int x, int y, int z) {
return perm[perm[perm[x] + y] + z];
}
}
}

Player and Camera System

First-Person Camera

public class Player {
private Vector3f position;
private Vector3f velocity;
private float yaw, pitch;
private boolean onGround;
private final AABB boundingBox;
public Player() {
this.position = new Vector3f(0, 70, 0);
this.velocity = new Vector3f(0, 0, 0);
this.boundingBox = new AABB(-0.3f, 0, -0.3f, 0.3f, 1.8f, 0.3f);
}
public void update(float deltaTime, VoxelWorld world) {
// Apply gravity
if (!onGround) {
velocity.y -= 20.0f * deltaTime; // Gravity
}
// Update position
Vector3f newPosition = new Vector3f(position);
newPosition.x += velocity.x * deltaTime;
newPosition.y += velocity.y * deltaTime;
newPosition.z += velocity.z * deltaTime;
// Check collisions
handleCollisions(world, newPosition);
// Apply friction
velocity.x *= 0.9f;
velocity.z *= 0.9f;
}
private void handleCollisions(VoxelWorld world, Vector3f newPosition) {
AABB newBoundingBox = boundingBox.translate(newPosition);
// Check Y collision (vertical)
if (velocity.y < 0) { // Falling
if (checkCollision(world, newBoundingBox.offset(0, -0.1f, 0))) {
velocity.y = 0;
onGround = true;
newPosition.y = (float)Math.floor(newPosition.y);
} else {
onGround = false;
}
} else if (velocity.y > 0) { // Jumping
if (checkCollision(world, newBoundingBox.offset(0, 0.1f, 0))) {
velocity.y = 0;
}
}
// Check X collision
if (checkCollision(world, newBoundingBox.offset(velocity.x * 0.1f, 0, 0))) {
velocity.x = 0;
}
// Check Z collision
if (checkCollision(world, newBoundingBox.offset(0, 0, velocity.z * 0.1f))) {
velocity.z = 0;
}
position.set(newPosition);
}
private boolean checkCollision(VoxelWorld world, AABB box) {
int minX = (int)Math.floor(box.minX);
int maxX = (int)Math.ceil(box.maxX);
int minY = (int)Math.floor(box.minY);
int maxY = (int)Math.ceil(box.maxY);
int minZ = (int)Math.floor(box.minZ);
int maxZ = (int)Math.ceil(box.maxZ);
for (int x = minX; x <= maxX; x++) {
for (int y = minY; y <= maxY; y++) {
for (int z = minZ; z <= maxZ; z++) {
Voxel voxel = world.getVoxel(x, y, z);
if (voxel.isSolid()) {
AABB voxelBox = new AABB(x, y, z, x + 1, y + 1, z + 1);
if (box.intersects(voxelBox)) {
return true;
}
}
}
}
}
return false;
}
public void move(float forward, float strafe) {
float cosYaw = (float)Math.cos(Math.toRadians(yaw));
float sinYaw = (float)Math.sin(Math.toRadians(yaw));
velocity.x = forward * sinYaw + strafe * cosYaw;
velocity.z = -forward * cosYaw + strafe * sinYaw;
// Normalize if moving diagonally
if (forward != 0 && strafe != 0) {
velocity.x *= 0.707f;
velocity.z *= 0.707f;
}
// Apply speed
float speed = 5.0f;
velocity.x *= speed;
velocity.z *= speed;
}
public void jump() {
if (onGround) {
velocity.y = 8.0f;
onGround = false;
}
}
public void setOrientation(float yaw, float pitch) {
this.yaw = yaw;
this.pitch = Math.max(-89, Math.min(89, pitch));
}
public Matrix4f getViewMatrix() {
Matrix4f matrix = new Matrix4f();
matrix.setIdentity();
matrix.rotate((float)Math.toRadians(pitch), new Vector3f(1, 0, 0));
matrix.rotate((float)Math.toRadians(yaw), new Vector3f(0, 1, 0));
matrix.translate(new Vector3f(-position.x, -position.y, -position.z));
return matrix;
}
// Getters
public Vector3f getPosition() { return position; }
public float getYaw() { return yaw; }
public float getPitch() { return pitch; }
}
public class AABB {
public final float minX, minY, minZ;
public final float maxX, maxY, maxZ;
public AABB(float minX, float minY, float minZ, float maxX, float maxY, float maxZ) {
this.minX = minX;
this.minY = minY;
this.minZ = minZ;
this.maxX = maxX;
this.maxY = maxY;
this.maxZ = maxZ;
}
public AABB translate(Vector3f translation) {
return new AABB(
minX + translation.x, minY + translation.y, minZ + translation.z,
maxX + translation.x, maxY + translation.y, maxZ + translation.z
);
}
public AABB offset(float dx, float dy, float dz) {
return new AABB(minX + dx, minY + dy, minZ + dz,
maxX + dx, maxY + dy, maxZ + dz);
}
public boolean intersects(AABB other) {
return minX < other.maxX && maxX > other.minX &&
minY < other.maxY && maxY > other.minY &&
minZ < other.maxZ && maxZ > other.minZ;
}
}

Main Game Loop

Voxel Engine Main Class

public class VoxelEngine {
private VoxelWorld world;
private Player player;
private boolean running;
private int windowWidth = 800;
private int windowHeight = 600;
public VoxelEngine() {
try {
initialize();
run();
} finally {
cleanup();
}
}
private void initialize() {
// Initialize GLFW
if (!glfwInit()) {
throw new IllegalStateException("Unable to initialize GLFW");
}
// Create window
long window = glfwCreateWindow(windowWidth, windowHeight, "Voxel Engine", 0, 0);
if (window == 0) {
throw new RuntimeException("Failed to create GLFW window");
}
glfwMakeContextCurrent(window);
GL.createCapabilities();
// Initialize systems
world = new VoxelWorld(8); // 8 chunk render distance
player = new Player();
// Set up OpenGL
glEnable(GL_DEPTH_TEST);
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
// Set up callbacks
setupCallbacks(window);
running = true;
}
private void setupCallbacks(long window) {
glfwSetKeyCallback(window, (win, key, scancode, action, mods) -> {
if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE) {
running = false;
}
});
glfwSetCursorPosCallback(window, (win, xpos, ypos) -> {
handleMouseInput(xpos, ypos);
});
glfwSetMouseButtonCallback(window, (win, button, action, mods) -> {
handleMouseClick(button, action);
});
}
private void handleMouseInput(double xpos, double ypos) {
// Calculate mouse movement
float deltaX = (float)(xpos - windowWidth / 2);
float deltaY = (float)(ypos - windowHeight / 2);
// Update player orientation
player.setOrientation(
player.getYaw() + deltaX * 0.1f,
player.getPitch() + deltaY * 0.1f
);
// Reset cursor to center
glfwSetCursorPos(glfwGetCurrentContext(), windowWidth / 2, windowHeight / 2);
}
private void handleMouseClick(int button, int action) {
if (action == GLFW_PRESS) {
if (button == GLFW_MOUSE_BUTTON_LEFT) {
// Break block
breakBlock();
} else if (button == GLFW_MOUSE_BUTTON_RIGHT) {
// Place block
placeBlock();
}
}
}
private void breakBlock() {
Vector3f position = player.getPosition();
Vector3f direction = getLookDirection();
// Raycast to find targeted block
for (float t = 0; t < 5; t += 0.1f) {
int x = (int)Math.floor(position.x + direction.x * t);
int y = (int)Math.floor(position.y + direction.y * t);
int z = (int)Math.floor(position.z + direction.z * t);
Voxel voxel = world.getVoxel(x, y, z);
if (voxel.isSolid()) {
world.setVoxel(x, y, z, Voxel.air());
break;
}
}
}
private void placeBlock() {
Vector3f position = player.getPosition();
Vector3f direction = getLookDirection();
// Raycast to find placement position
Integer lastX = null, lastY = null, lastZ = null;
for (float t = 0; t < 5; t += 0.1f) {
int x = (int)Math.floor(position.x + direction.x * t);
int y = (int)Math.floor(position.y + direction.y * t);
int z = (int)Math.floor(position.z + direction.z * t);
Voxel voxel = world.getVoxel(x, y, z);
if (voxel.isSolid()) {
if (lastX != null) {
// Place block at previous position (adjacent to hit block)
world.setVoxel(lastX, lastY, lastZ, Voxel.stone());
}
break;
}
lastX = x;
lastY = y;
lastZ = z;
}
}
private Vector3f getLookDirection() {
float yaw = (float)Math.toRadians(player.getYaw());
float pitch = (float)Math.toRadians(player.getPitch());
return new Vector3f(
(float)(Math.sin(yaw) * Math.cos(pitch)),
(float)(-Math.sin(pitch)),
(float)(-Math.cos(yaw) * Math.cos(pitch))
);
}
private void run() {
long window = glfwGetCurrentContext();
long lastTime = System.nanoTime();
while (running && !glfwWindowShouldClose(window)) {
long currentTime = System.nanoTime();
float deltaTime = (currentTime - lastTime) / 1_000_000_000.0f;
lastTime = currentTime;
// Handle input
handleKeyboardInput(window, deltaTime);
// Update
update(deltaTime);
// Render
render();
glfwSwapBuffers(window);
glfwPollEvents();
}
}
private void handleKeyboardInput(long window, float deltaTime) {
float forward = 0, strafe = 0;
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) forward += 1;
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) forward -= 1;
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) strafe -= 1;
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) strafe += 1;
if (glfwGetKey(window, GLFW_KEY_SPACE) == GLFW_PRESS) player.jump();
player.move(forward, strafe);
}
private void update(float deltaTime) {
player.update(deltaTime, world);
// Update chunks around player
Vector3f playerPos = player.getPosition();
int playerChunkX = (int)Math.floor(playerPos.x) >> 4;
int playerChunkZ = (int)Math.floor(playerPos.z) >> 4;
world.updateChunks(playerChunkX, playerChunkZ);
}
private void render() {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Set up projection matrix
Matrix4f projection = new Matrix4f();
projection.perspective((float)Math.toRadians(70), 
(float)windowWidth / windowHeight, 0.1f, 1000.0f);
// Set up view matrix from player
Matrix4f view = player.getViewMatrix();
// Set up model-view-projection matrix
Matrix4f mvp = new Matrix4f(projection);
mvp.mul(view);
// Render world
glLoadMatrixf(mvp.get(new float[16]));
world.render();
}
private void cleanup() {
if (world != null) {
world.cleanup();
}
glfwTerminate();
}
public static void main(String[] args) {
new VoxelEngine();
}
}

Performance Optimizations

Frustum Culling and LOD

public class ChunkManager {
private final VoxelWorld world;
private final Map<Long, Chunk> loadedChunks;
private final List<Chunk> visibleChunks;
public ChunkManager(VoxelWorld world) {
this.world = world;
this.loadedChunks = new HashMap<>();
this.visibleChunks = new ArrayList<>();
}
public void updateVisibility(Frustum frustum, Vector3f cameraPos) {
visibleChunks.clear();
for (Chunk chunk : loadedChunks.values()) {
AABB chunkBounds = getChunkAABB(chunk);
if (frustum.intersects(chunkBounds)) {
// Calculate distance for LOD
float distance = distanceToChunk(cameraPos, chunk);
// Simple LOD: skip meshing for distant chunks
if (distance < 100) { // Only mesh chunks within 100 units
visibleChunks.add(chunk);
}
}
}
}
private AABB getChunkAABB(Chunk chunk) {
float minX = chunk.getChunkX() * Chunk.CHUNK_SIZE;
float minZ = chunk.getChunkZ() * Chunk.CHUNK_SIZE;
float maxX = minX + Chunk.CHUNK_SIZE;
float maxZ = minZ + Chunk.CHUNK_SIZE;
return new AABB(minX, 0, minZ, maxX, Chunk.CHUNK_HEIGHT, maxZ);
}
private float distanceToChunk(Vector3f cameraPos, Chunk chunk) {
float chunkCenterX = (chunk.getChunkX() + 0.5f) * Chunk.CHUNK_SIZE;
float chunkCenterZ = (chunk.getChunkZ() + 0.5f) * Chunk.CHUNK_SIZE;
float dx = cameraPos.x - chunkCenterX;
float dz = cameraPos.z - chunkCenterZ;
return (float)Math.sqrt(dx * dx + dz * dz);
}
}
public class Frustum {
private final Plane[] planes;
public Frustum() {
this.planes = new Plane[6];
for (int i = 0; i < 6; i++) {
planes[i] = new Plane();
}
}
public void update(Matrix4f mvp) {
// Extract frustum planes from model-view-projection matrix
// Left plane
planes[0].set(mvp.m03 + mvp.m00, mvp.m13 + mvp.m10, mvp.m23 + mvp.m20, mvp.m33 + mvp.m30);
// Right plane
planes[1].set(mvp.m03 - mvp.m00, mvp.m13 - mvp.m10, mvp.m23 - mvp.m20, mvp.m33 - mvp.m30);
// Bottom plane
planes[2].set(mvp.m03 + mvp.m01, mvp.m13 + mvp.m11, mvp.m23 + mvp.m21, mvp.m33 + mvp.m31);
// Top plane
planes[3].set(mvp.m03 - mvp.m01, mvp.m13 - mvp.m11, mvp.m23 - mvp.m21, mvp.m33 - mvp.m31);
// Near plane
planes[4].set(mvp.m03 + mvp.m02, mvp.m13 + mvp.m12, mvp.m23 + mvp.m22, mvp.m33 + mvp.m32);
// Far plane
planes[5].set(mvp.m03 - mvp.m02, mvp.m13 - mvp.m12, mvp.m23 - mvp.m22, mvp.m33 - mvp.m32);
// Normalize all planes
for (Plane plane : planes) {
plane.normalize();
}
}
public boolean intersects(AABB aabb) {
for (Plane plane : planes) {
if (plane.distanceToPoint(getPositiveVertex(aabb, plane)) < 0) {
return false;
}
}
return true;
}
private Vector3f getPositiveVertex(AABB aabb, Plane plane) {
Vector3f vertex = new Vector3f();
vertex.x = plane.normal.x > 0 ? aabb.maxX : aabb.minX;
vertex.y = plane.normal.y > 0 ? aabb.maxY : aabb.minY;
vertex.z = plane.normal.z > 0 ? aabb.maxZ : aabb.minZ;
return vertex;
}
}
public class Plane {
public float a, b, c, d;
public void set(float a, float b, float c, float d) {
this.a = a;
this.b = b;
this.c = c;
this.d = d;
}
public void normalize() {
float length = (float)Math.sqrt(a * a + b * b + c * c);
if (length > 0) {
float invLength = 1.0f / length;
a *= invLength;
b *= invLength;
c *= invLength;
d *= invLength;
}
}
public float distanceToPoint(Vector3f point) {
return a * point.x + b * point.y + c * point.z + d;
}
}

This voxel engine prototype provides a solid foundation for building Minecraft-like games or other voxel-based applications. Key features include chunk-based world management, greedy meshing for efficient rendering, procedural world generation, collision detection, and basic player interaction. The architecture is designed to be extensible for adding features like lighting, fluids, and more advanced world generation.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper