Cubyz/src/chunk.zig
2023-01-21 19:05:02 +01:00

835 lines
30 KiB
Zig

const std = @import("std");
const Allocator = std.mem.Allocator;
const blocks = @import("blocks.zig");
const Block = blocks.Block;
const game = @import("game.zig");
const graphics = @import("graphics.zig");
const c = graphics.c;
const Shader = graphics.Shader;
const SSBO = graphics.SSBO;
const main = @import("main.zig");
const models = @import("models.zig");
const renderer = @import("renderer.zig");
const settings = @import("settings.zig");
const vec = @import("vec.zig");
const Vec2f = vec.Vec2f;
const Vec3f = vec.Vec3f;
const Vec3d = vec.Vec3d;
const Mat4f = vec.Mat4f;
pub const chunkShift: u5 = 5;
pub const chunkShift2: u5 = chunkShift*2;
pub const chunkSize: i32 = 1 << chunkShift;
pub const chunkSizeIterator: [chunkSize]u0 = undefined;
pub const chunkVolume: u31 = 1 << 3*chunkShift;
pub const chunkMask: i32 = chunkSize - 1;
/// Contains a bunch of constants used to describe neighboring blocks.
pub const Neighbors = struct {
/// How many neighbors there are.
pub const neighbors: u3 = 6;
/// Directions → Index
pub const dirUp: u3 = 0;
/// Directions → Index
pub const dirDown: u3 = 1;
/// Directions → Index
pub const dirPosX: u3 = 2;
/// Directions → Index
pub const dirNegX: u3 = 3;
/// Directions → Index
pub const dirPosZ: u3 = 4;
/// Directions → Index
pub const dirNegZ: u3 = 5;
/// Index to relative position
pub const relX = [_]i32 {0, 0, 1, -1, 0, 0};
/// Index to relative position
pub const relY = [_]i32 {1, -1, 0, 0, 0, 0};
/// Index to relative position
pub const relZ = [_]i32 {0, 0, 0, 0, 1, -1};
/// Index to bitMask for bitmap direction data
pub const bitMask = [_]u6 {0x01, 0x02, 0x04, 0x08, 0x10, 0x20};
/// To iterate over all neighbors easily
pub const iterable = [_]u3 {0, 1, 2, 3, 4, 5};
};
/// Gets the index of a given position inside this chunk.
fn getIndex(x: i32, y: i32, z: i32) u32 {
std.debug.assert((x & chunkMask) == x and (y & chunkMask) == y and (z & chunkMask) == z);
return (@intCast(u32, x) << chunkShift) | (@intCast(u32, y) << chunkShift2) | @intCast(u32, z);
}
/// Gets the x coordinate from a given index inside this chunk.
fn extractXFromIndex(index: usize) i32 {
return @intCast(i32, index >> chunkShift & chunkMask);
}
/// Gets the y coordinate from a given index inside this chunk.
fn extractYFromIndex(index: usize) i32 {
return @intCast(i32, index >> chunkShift2 & chunkMask);
}
/// Gets the z coordinate from a given index inside this chunk.
fn extractZFromIndex(index: usize) i32 {
return @intCast(i32, index & chunkMask);
}
pub const ChunkPosition = struct {
wx: i32,
wy: i32,
wz: i32,
voxelSize: u31,
// TODO(mabye?):
// public int hashCode() {
// int shift = Math.min(Integer.numberOfTrailingZeros(wx), Math.min(Integer.numberOfTrailingZeros(wy), Integer.numberOfTrailingZeros(wz)));
// return (((wx >> shift) * 31 + (wy >> shift)) * 31 + (wz >> shift)) * 31 + voxelSize;
// }
pub fn getMinDistanceSquared(self: ChunkPosition, playerPosition: Vec3d) f64 {
var halfWidth = @intToFloat(f64, self.voxelSize*@divExact(chunkSize, 2));
var dx = @fabs(@intToFloat(f64, self.wx) + halfWidth - playerPosition[0]);
var dy = @fabs(@intToFloat(f64, self.wy) + halfWidth - playerPosition[1]);
var dz = @fabs(@intToFloat(f64, self.wz) + halfWidth - playerPosition[2]);
dx = @max(0, dx - halfWidth);
dy = @max(0, dy - halfWidth);
dz = @max(0, dz - halfWidth);
return dx*dx + dy*dy + dz*dz;
}
pub fn getPriority(self: ChunkPosition, playerPos: Vec3d) f32 {
return -@floatCast(f32, self.getMinDistanceSquared(playerPos))/@intToFloat(f32, self.voxelSize*self.voxelSize) + 2*@intToFloat(f32, std.math.log2_int(u31, self.voxelSize)*chunkSize*chunkSize);
}
};
pub const Chunk = struct {
pos: ChunkPosition,
blocks: [chunkVolume]Block = undefined,
wasChanged: bool = false,
/// When a chunk is cleaned, it won't be saved by the ChunkManager anymore, so following changes need to be saved directly.
wasCleaned: bool = false,
generated: bool = false,
width: i32,
voxelSizeShift: u5,
voxelSizeMask: i32,
widthShift: u5,
mutex: std.Thread.Mutex,
pub fn init(self: *Chunk, pos: ChunkPosition) void {
std.debug.assert((pos.voxelSize - 1 & pos.voxelSize) == 0);
std.debug.assert(@mod(pos.wx, pos.voxelSize) == 0 and @mod(pos.wy, pos.voxelSize) == 0 and @mod(pos.wz, pos.voxelSize) == 0);
const voxelSizeShift = @intCast(u5, std.math.log2_int(u31, pos.voxelSize));
self.* = Chunk {
.pos = pos,
.width = pos.voxelSize*chunkSize,
.voxelSizeShift = voxelSizeShift,
.voxelSizeMask = pos.voxelSize - 1,
.widthShift = voxelSizeShift + chunkShift,
.mutex = std.Thread.Mutex{},
};
}
pub fn setChanged(self: *Chunk) void {
self.wasChanged = true;
{
self.mutex.lock();
if(self.wasCleaned) {
self.save();
}
self.mutex.unlock();
}
}
pub fn clean(self: *Chunk) void {
{
self.mutex.lock();
self.wasCleaned = true;
self.save();
self.mutex.unlock();
}
}
pub fn unclean(self: *Chunk) void {
{
self.mutex.lock();
self.wasCleaned = false;
self.save();
self.mutex.unlock();
}
}
/// Checks if the given relative coordinates lie within the bounds of this chunk.
pub fn liesInChunk(self: *const Chunk, x: i32, y: i32, z: i32) bool {
return x >= 0 and x < self.width
and y >= 0 and y < self.width
and z >= 0 and z < self.width;
}
/// This is useful to convert for loops to work for reduced resolution:
/// Instead of using
/// for(int x = start; x < end; x++)
/// for(int x = chunk.startIndex(start); x < end; x += chunk.getVoxelSize())
/// should be used to only activate those voxels that are used in Cubyz's downscaling technique.
pub fn startIndex(self: *const Chunk, start: i32) i32 {
return start+self.voxelSizeMask & ~self.voxelSizeMask; // Rounds up to the nearest valid voxel coordinate.
}
/// Updates a block if current value is air or the current block is degradable.
/// Does not do any bound checks. They are expected to be done with the `liesInChunk` function.
pub fn updateBlockIfDegradable(self: *Chunk, x: i32, y: i32, z: i32, newBlock: Block) void {
x >>= self.voxelSizeShift;
y >>= self.voxelSizeShift;
z >>= self.voxelSizeShift;
var index = getIndex(x, y, z);
if (self.blocks[index] == 0 || self.blocks[index].degradable()) {
self.blocks[index] = newBlock;
}
}
/// Updates a block if it is inside this chunk.
/// Does not do any bound checks. They are expected to be done with the `liesInChunk` function.
pub fn updateBlock(self: *Chunk, x: i32, y: i32, z: i32, newBlock: Block) void {
x >>= self.voxelSizeShift;
y >>= self.voxelSizeShift;
z >>= self.voxelSizeShift;
var index = getIndex(x, y, z);
self.blocks[index] = newBlock;
}
/// Updates a block if it is inside this chunk. Should be used in generation to prevent accidently storing these as changes.
/// Does not do any bound checks. They are expected to be done with the `liesInChunk` function.
pub fn updateBlockInGeneration(self: *Chunk, x: i32, y: i32, z: i32, newBlock: Block) void {
x >>= self.voxelSizeShift;
y >>= self.voxelSizeShift;
z >>= self.voxelSizeShift;
var index = getIndex(x, y, z);
self.blocks[index] = newBlock;
}
/// Gets a block if it is inside this chunk.
/// Does not do any bound checks. They are expected to be done with the `liesInChunk` function.
pub fn getBlock(self: *const Chunk, _x: i32, _y: i32, _z: i32) Block {
var x = _x >> self.voxelSizeShift;
var y = _y >> self.voxelSizeShift;
var z = _z >> self.voxelSizeShift;
var index = getIndex(x, y, z);
return self.blocks[index];
}
pub fn getNeighbors(self: *const Chunk, x: i32, y: i32, z: i32, neighborsArray: *[6]Block) void {
std.debug.assert(neighborsArray.length == 6);
x &= chunkMask;
y &= chunkMask;
z &= chunkMask;
for(Neighbors.relX) |_, i| {
var xi = x + Neighbors.relX[i];
var yi = y + Neighbors.relY[i];
var zi = z + Neighbors.relZ[i];
if (xi == (xi & chunkMask) and yi == (yi & chunkMask) and zi == (zi & chunkMask)) { // Simple double-bound test for coordinates.
neighborsArray[i] = self.getBlock(xi, yi, zi);
} else {
// TODO: What about other chunks?
// NormalChunk ch = world.getChunk(xi + wx, yi + wy, zi + wz);
// if (ch != null) {
// neighborsArray[i] = ch.getBlock(xi & chunkMask, yi & chunkMask, zi & chunkMask);
// } else {
// neighborsArray[i] = 1; // Some solid replacement, in case the chunk isn't loaded. TODO: Properly choose a solid block.
// }
}
}
}
pub fn updateFromLowerResolution(self: *Chunk, other: *const Chunk) void {
const xOffset = if(other.wx != self.wx) chunkSize/2 else 0; // Offsets of the lower resolution chunk in this chunk.
const yOffset = if(other.wy != self.wy) chunkSize/2 else 0;
const zOffset = if(other.wz != self.wz) chunkSize/2 else 0;
var x: i32 = 0;
while(x < chunkSize/2): (x += 1) {
var y: i32 = 0;
while(y < chunkSize/2): (y += 1) {
var z: i32 = 0;
while(z < chunkSize/2): (z += 1) {
// Count the neighbors for each subblock. An transparent block counts 5. A chunk border(unknown block) only counts 1.
var neighborCount: [8]u32 = undefined;
var octantBlocks: [8]Block = undefined;
var maxCount: u32 = 0;
var dx: i32 = 0;
while(dx <= 1): (dx += 1) {
var dy: i32 = 0;
while(dy <= 1): (dy += 1) {
var dz: i32 = 0;
while(dz <= 1): (dz += 1) {
const index = getIndex(x*2 + dx, y*2 + dy, z*2 + dz);
const i = dx*4 + dz*2 + dy;
octantBlocks[i] = other.blocks[index];
if(octantBlocks[i] == 0) continue; // I don't care about air blocks.
var count: u32 = 0;
for(Neighbors.iterable) |n| {
const nx = x*2 + dx + Neighbors.relX[n];
const ny = y*2 + dy + Neighbors.relY[n];
const nz = z*2 + dz + Neighbors.relZ[n];
if((nx & chunkMask) == nx and (ny & chunkMask) == ny and (nz & chunkMask) == nz) { // If it's inside the chunk.
const neighborIndex = getIndex(nx, ny, nz);
if(other.blocks[neighborIndex].transparent()) {
count += 5;
}
} else {
count += 1;
}
}
maxCount = @max(maxCount, count);
neighborCount[i] = count;
}
}
}
// Uses a specific permutation here that keeps high resolution patterns in lower resolution.
const permutationStart = (x & 1)*4 + (z & 1)*2 + (y & 1);
const block = Block{.typ = 0, .data = 0};
for(neighborCount) |_, i| {
const appliedPermutation = permutationStart ^ i;
if(neighborCount[appliedPermutation] >= maxCount - 1) { // Avoid pattern breaks at chunk borders.
block = blocks[appliedPermutation];
}
}
// Update the block:
const thisIndex = getIndex(x + xOffset, y + yOffset, z + zOffset);
self.blocks[thisIndex] = block;
}
}
}
// Create updated meshes and send to client:
// TODO:
//for(int x = 0; x <= 2*xOffset; x += chunkSize) {
// for(int y = 0; y <= 2*yOffset; y += chunkSize) {
// for(int z = 0; z <= 2*zOffset; z += chunkSize) {
// int wx = this.wx + x*voxelSize - Chunk.chunkSize;
// int wy = this.wy + y*voxelSize - Chunk.chunkSize;
// int wz = this.wz + z*voxelSize - Chunk.chunkSize;
// if(voxelSize == 32) {
// wx -= chunkSize*voxelSize/2;
// wy -= chunkSize*voxelSize/2;
// wz -= chunkSize*voxelSize/2;
// }
// world.queueChunks(new ChunkData[] {new ChunkData(wx, wy, wz, voxelSize)});
// }
// }
//}
self.setChanged();
}
// TODO: Move this outside.
// /**
// * Generates this chunk.
// * If the chunk was already saved it is loaded from file instead.
// * @param seed
// * @param terrainGenerationProfile
// */
// public void generate(World world, long seed, TerrainGenerationProfile terrainGenerationProfile) {
// assert !generated : "Seriously, why would you generate this chunk twice???";
// if(!ChunkIO.loadChunkFromFile(world, this)) {
// CaveMap caveMap = new CaveMap(this);
// CaveBiomeMap biomeMap = new CaveBiomeMap(this);
//
// for (Generator g : terrainGenerationProfile.generators) {
// g.generate(seed ^ g.getGeneratorSeed(), wx, wy, wz, this, caveMap, biomeMap);
// }
// }
// generated = true;
// }
// TODO:
pub fn save(chunk: *const Chunk) void {
_ = chunk;
// /**
// * Saves this chunk.
// */
// public void save(World world) {
// if(wasChanged) {
// ChunkIO.storeChunkToFile(world, this);
// wasChanged = false;
// // Update the next lod chunk:
// if(voxelSize != 1 << Constants.HIGHEST_LOD) {
// if(world instanceof ServerWorld) {
// ReducedChunk chunk = ((ServerWorld)world).chunkManager.getOrGenerateReducedChunk(wx, wy, wz, voxelSize*2);
// chunk.updateFromLowerResolution(this);
// } else {
// Logger.error("Not implemented: ");
// Logger.error(new Exception());
// }
// }
// }
}
// TODO: Check if/how they are needed:
//
// public Vector3d getMin() {
// return new Vector3d(wx, wy, wz);
// }
//
// public Vector3d getMax() {
// return new Vector3d(wx + width, wy + width, wz + width);
// }
};
pub const meshing = struct {
var shader: Shader = undefined;
pub var uniforms: struct {
projectionMatrix: c_int,
viewMatrix: c_int,
modelPosition: c_int,
screenSize: c_int,
ambientLight: c_int,
@"fog.activ": c_int,
@"fog.color": c_int,
@"fog.density": c_int,
texture_sampler: c_int,
emissionSampler: c_int,
@"waterFog.activ": c_int,
@"waterFog.color": c_int,
@"waterFog.density": c_int,
time: c_int,
visibilityMask: c_int,
voxelSize: c_int,
} = undefined;
var vao: c_uint = undefined;
var vbo: c_uint = undefined;
var faces: std.ArrayList(u32) = undefined;
var faceBuffer: graphics.LargeBuffer = undefined;
pub fn init() !void {
shader = try Shader.create("assets/cubyz/shaders/chunks/chunk_vertex.vs", "assets/cubyz/shaders/chunks/chunk_fragment.fs");
uniforms = shader.bulkGetUniformLocation(@TypeOf(uniforms));
var rawData: [6*3 << (3*chunkShift)]u32 = undefined; // 6 vertices per face, maximum 3 faces/block
const lut = [_]u32{0, 1, 2, 2, 1, 3};
for(rawData) |_, i| {
rawData[i] = @intCast(u32, i)/6*4 + lut[i%6];
}
c.glGenVertexArrays(1, &vao);
c.glBindVertexArray(vao);
c.glGenBuffers(1, &vbo);
c.glBindBuffer(c.GL_ELEMENT_ARRAY_BUFFER, vbo);
c.glBufferData(c.GL_ELEMENT_ARRAY_BUFFER, rawData.len*@sizeOf(f32), &rawData, c.GL_STATIC_DRAW);
c.glVertexAttribPointer(0, 2, c.GL_FLOAT, c.GL_FALSE, 2*@sizeOf(f32), null);
c.glBindVertexArray(0);
faces = try std.ArrayList(u32).initCapacity(std.heap.page_allocator, 65536);
try faceBuffer.init(main.globalAllocator, 128 << 20, 3);
}
pub fn deinit() void {
shader.delete();
c.glDeleteVertexArrays(1, &vao);
c.glDeleteBuffers(1, &vbo);
faces.deinit();
faceBuffer.deinit();
}
pub fn bindShaderAndUniforms(projMatrix: Mat4f, ambient: Vec3f, time: u32) void {
shader.bind();
c.glUniform1i(uniforms.@"fog.activ", if(game.fog.active) 1 else 0);
c.glUniform3fv(uniforms.@"fog.color", 1, @ptrCast([*c]f32, &game.fog.color));
c.glUniform1f(uniforms.@"fog.density", game.fog.density);
c.glUniformMatrix4fv(uniforms.projectionMatrix, 1, c.GL_FALSE, @ptrCast([*c]const f32, &projMatrix));
c.glUniform1i(uniforms.texture_sampler, 0);
c.glUniform1i(uniforms.emissionSampler, 1);
c.glUniformMatrix4fv(uniforms.viewMatrix, 1, c.GL_FALSE, @ptrCast([*c]f32, &game.camera.viewMatrix));
c.glUniform3f(uniforms.ambientLight, ambient[0], ambient[1], ambient[2]);
c.glUniform1i(uniforms.time, @truncate(u31, time));
c.glBindVertexArray(vao);
}
const FaceData = switch(@import("builtin").target.cpu.arch.endian()) {
.Little => packed struct {
position: u32,
blockAndModel: u32,
},
.Big => packed struct {
blockAndModel: u32,
position: u32,
},
};
pub const ChunkMesh = struct {
pos: ChunkPosition,
size: i32,
chunk: std.atomic.Atomic(?*Chunk),
faces: std.ArrayList(FaceData),
bufferAllocation: graphics.LargeBuffer.Allocation = .{.start = 0, .len = 0},
coreCount: u31 = 0,
neighborStart: [7]u31 = [_]u31{0} ** 7,
vertexCount: u31 = 0,
generated: bool = false,
mutex: std.Thread.Mutex = std.Thread.Mutex{},
visibilityMask: u8 = 0xff,
pub fn init(allocator: Allocator, pos: ChunkPosition) ChunkMesh {
return ChunkMesh{
.pos = pos,
.size = chunkSize*pos.voxelSize,
.faces = std.ArrayList(FaceData).init(allocator),
.chunk = std.atomic.Atomic(?*Chunk).init(null),
};
}
pub fn deinit(self: *ChunkMesh) void {
faceBuffer.free(self.bufferAllocation) catch unreachable;
self.faces.deinit();
if(self.chunk.load(.Monotonic)) |ch| {
renderer.RenderStructure.allocator.destroy(ch);
}
}
fn canBeSeenThroughOtherBlock(block: Block, other: Block, neighbor: u3) bool {
const rotatedModel = blocks.meshes.model(block);
const model = &models.voxelModels.items[rotatedModel.modelIndex];
const freestandingModel = switch(rotatedModel.permutation.permuteNeighborIndex(neighbor)) {
Neighbors.dirNegX => model.min[0] != 0,
Neighbors.dirPosX => model.max[0] != 16,
Neighbors.dirDown => model.min[1] != 0,
Neighbors.dirUp => model.max[1] != 16,
Neighbors.dirNegZ => model.min[2] != 0,
Neighbors.dirPosZ => model.max[2] != 16,
else => unreachable,
};
return block.typ != 0 and (
freestandingModel
or other.typ == 0
or (!std.meta.eql(block, other) and other.viewThrough())
or blocks.meshes.model(other).modelIndex != 0 // TODO: make this more strict to avoid overdraw.
);
}
pub fn regenerateMainMesh(self: *ChunkMesh, chunk: *Chunk) !void {
std.debug.assert(!self.mutex.tryLock()); // The mutex should be locked when calling this function.
self.faces.clearRetainingCapacity();
var n: u32 = 0;
var x: u8 = 0;
while(x < chunkSize): (x += 1) {
var y: u8 = 0;
while(y < chunkSize): (y += 1) {
var z: u8 = 0;
while(z < chunkSize): (z += 1) {
const block = (&chunk.blocks)[getIndex(x, y, z)]; // ← a temporary fix to a compiler performance bug. TODO: check if this was fixed.
if(block.typ == 0) continue;
// Check all neighbors:
for(Neighbors.iterable) |i| {
n += 1;
const x2 = x + Neighbors.relX[i];
const y2 = y + Neighbors.relY[i];
const z2 = z + Neighbors.relZ[i];
if(x2&chunkMask != x2 or y2&chunkMask != y2 or z2&chunkMask != z2) continue; // Neighbor is outside the chunk.
const neighborBlock = (&chunk.blocks)[getIndex(x2, y2, z2)]; // ← a temporary fix to a compiler performance bug. TODO: check if this was fixed.
if(canBeSeenThroughOtherBlock(block, neighborBlock, i)) {
try self.faces.append(constructFaceData(block, i, @intCast(u8, x2), @intCast(u8, y2), @intCast(u8, z2)));
}
}
}
}
}
if(self.chunk.load(.Monotonic)) |oldChunk| {
renderer.RenderStructure.allocator.destroy(oldChunk);
}
self.chunk.store(chunk, .Monotonic);
self.coreCount = @intCast(u31, self.faces.items.len);
self.neighborStart = [_]u31{self.coreCount} ** 7;
}
fn addFace(self: *ChunkMesh, faceData: FaceData, fromNeighborChunk: ?u3) !void {
std.debug.assert(!self.mutex.tryLock()); // The mutex should be locked when calling this function.
var insertionIndex: u31 = undefined;
if(fromNeighborChunk) |neighbor| {
insertionIndex = self.neighborStart[neighbor];
for(self.neighborStart[neighbor+1..]) |*start| {
start.* += 1;
}
} else {
insertionIndex = self.coreCount;
self.coreCount += 1;
for(self.neighborStart) |*start| {
start.* += 1;
}
}
try self.faces.insert(insertionIndex, faceData);
}
fn removeFace(self: *ChunkMesh, faceData: FaceData, fromNeighborChunk: ?u3) void {
std.debug.assert(!self.mutex.tryLock()); // The mutex should be locked when calling this function.
var searchStart: u32 = undefined;
var searchEnd: u32 = undefined;
if(fromNeighborChunk) |neighbor| {
searchStart = self.neighborStart[neighbor];
searchEnd = self.neighborStart[neighbor+1];
for(self.neighborStart[neighbor+1..]) |*start| {
start.* -= 1;
}
} else {
searchStart = 0;
searchEnd = self.coreCount;
self.coreCount -= 1;
for(self.neighborStart) |*start| {
start.* -= 1;
}
}
var i: u32 = searchStart;
while(i < searchEnd): (i += 1) {
if(std.meta.eql(self.faces.items[i], faceData)) {
_ = self.faces.orderedRemove(i);
return;
}
}
@panic("Couldn't find the face to remove. This case is not handled.");
}
fn changeFace(self: *ChunkMesh, oldFaceData: FaceData, newFaceData: FaceData, fromNeighborChunk: ?u3) void {
std.debug.assert(!self.mutex.tryLock()); // The mutex should be locked when calling this function.
var searchRange: []FaceData = undefined;
if(fromNeighborChunk) |neighbor| {
searchRange = self.faces.items[self.neighborStart[neighbor]..self.neighborStart[neighbor+1]];
} else {
searchRange = self.faces.items[0..self.coreCount];
}
var i: u32 = 0;
while(i < searchRange.len): (i += 2) {
if(std.meta.eql(self.faces.items[i], oldFaceData)) {
searchRange[i] = newFaceData;
return;
}
}
std.log.err("Couldn't find the face to replace.", .{});
}
pub fn updateBlock(self: *ChunkMesh, _x: i32, _y: i32, _z: i32, newBlock: Block) !void { // TODO: Investigate bug when placing blocks.
const x = _x & chunkMask;
const y = _y & chunkMask;
const z = _z & chunkMask;
self.mutex.lock();
defer self.mutex.unlock();
if(!self.generated) return;
const oldBlock = self.chunk.load(.Monotonic).?.blocks[getIndex(x, y, z)];
for(Neighbors.iterable) |neighbor| {
var neighborMesh = self;
var nx = x + Neighbors.relX[neighbor];
var ny = y + Neighbors.relY[neighbor];
var nz = z + Neighbors.relZ[neighbor];
if(nx & chunkMask != nx or ny & chunkMask != ny or nz & chunkMask != nz) { // Outside this chunk.
neighborMesh = renderer.RenderStructure.getNeighbor(self.pos, self.pos.voxelSize, neighbor) orelse continue;
if(!neighborMesh.generated) continue;
neighborMesh.mutex.lock();
}
defer if(neighborMesh != self) neighborMesh.mutex.unlock();
nx &= chunkMask;
ny &= chunkMask;
nz &= chunkMask;
const neighborBlock = neighborMesh.chunk.load(.Monotonic).?.blocks[getIndex(nx, ny, nz)];
{
{ // The face of the changed block
const newVisibility = canBeSeenThroughOtherBlock(newBlock, neighborBlock, neighbor);
const newFaceData = constructFaceData(newBlock, neighbor, @intCast(u8, nx), @intCast(u8, ny), @intCast(u8, nz));
const oldFaceData = constructFaceData(oldBlock, neighbor, @intCast(u8, nx), @intCast(u8, ny), @intCast(u8, nz));
if(canBeSeenThroughOtherBlock(oldBlock, neighborBlock, neighbor) != newVisibility) {
if(newVisibility) { // Adding the face
if(neighborMesh == self) {
try self.addFace(newFaceData, null);
} else {
try neighborMesh.addFace(newFaceData, neighbor);
}
} else { // Removing the face
if(neighborMesh == self) {
self.removeFace(oldFaceData, null);
} else {
neighborMesh.removeFace(oldFaceData, neighbor);
}
}
} else if(newVisibility) { // Changing the face
if(neighborMesh == self) {
self.changeFace(oldFaceData, newFaceData, null);
} else {
neighborMesh.changeFace(oldFaceData, newFaceData, neighbor);
}
}
}
{ // The face of the neighbor block
const newVisibility = canBeSeenThroughOtherBlock(neighborBlock, newBlock, neighbor ^ 1);
const newFaceData = constructFaceData(neighborBlock, neighbor ^ 1, @intCast(u8, x), @intCast(u8, y), @intCast(u8, z));
const oldFaceData = constructFaceData(neighborBlock, neighbor ^ 1, @intCast(u8, x), @intCast(u8, y), @intCast(u8, z));
if(canBeSeenThroughOtherBlock(neighborBlock, oldBlock, neighbor ^ 1) != newVisibility) {
if(newVisibility) { // Adding the face
if(neighborMesh == self) {
try self.addFace(newFaceData, null);
} else {
try self.addFace(newFaceData, neighbor);
}
} else { // Removing the face
if(neighborMesh == self) {
self.removeFace(oldFaceData, null);
} else {
self.removeFace(oldFaceData, neighbor);
}
}
} else if(newVisibility) { // Changing the face
if(neighborMesh == self) {
self.changeFace(oldFaceData, newFaceData, null);
} else {
self.changeFace(oldFaceData, newFaceData, neighbor);
}
}
}
}
if(neighborMesh != self) try neighborMesh.uploadData();
}
self.chunk.load(.Monotonic).?.blocks[getIndex(x, y, z)] = newBlock;
try self.uploadData();
}
fn uploadData(self: *ChunkMesh) !void {
self.vertexCount = @intCast(u31, 6*self.faces.items.len);
try faceBuffer.realloc(&self.bufferAllocation, @intCast(u31, 8*self.faces.items.len));
faceBuffer.bufferSubData(self.bufferAllocation.start, FaceData, self.faces.items);
}
inline fn constructFaceData(block: Block, normal: u32, x: u32, y: u32, z: u32) FaceData {
const model = blocks.meshes.model(block);
return FaceData {
.position = @as(u32, x) | @as(u32, y)<<5 | @as(u32, z)<<10 | normal<<20 | @as(u32, model.permutation.toInt())<<23,
.blockAndModel = block.typ | @as(u32, model.modelIndex)<<16,
};
}
pub fn uploadDataAndFinishNeighbors(self: *ChunkMesh) !void {
std.debug.assert(!self.mutex.tryLock()); // The mutex should be locked when calling this function.
const chunk = self.chunk.load(.Monotonic) orelse return; // In the mean-time the mesh was discarded and recreated and all the data was lost.
self.faces.shrinkRetainingCapacity(self.coreCount);
for(Neighbors.iterable) |neighbor| {
self.neighborStart[neighbor] = @intCast(u31, self.faces.items.len);
var nullNeighborMesh = renderer.RenderStructure.getNeighbor(self.pos, self.pos.voxelSize, neighbor);
if(nullNeighborMesh) |neighborMesh| {
std.debug.assert(neighborMesh != self);
neighborMesh.mutex.lock();
defer neighborMesh.mutex.unlock();
if(neighborMesh.generated) {
var additionalNeighborFaces = std.ArrayList(FaceData).init(main.threadAllocator);
defer additionalNeighborFaces.deinit();
var x3: u8 = if(neighbor & 1 == 0) @intCast(u8, chunkMask) else 0;
var x1: u8 = 0;
while(x1 < chunkSize): (x1 += 1) {
var x2: u8 = 0;
while(x2 < chunkSize): (x2 += 1) {
var x: u8 = undefined;
var y: u8 = undefined;
var z: u8 = undefined;
if(Neighbors.relX[neighbor] != 0) {
x = x3;
y = x1;
z = x2;
} else if(Neighbors.relY[neighbor] != 0) {
x = x1;
y = x3;
z = x2;
} else {
x = x2;
y = x1;
z = x3;
}
var otherX = @intCast(u8, x+%Neighbors.relX[neighbor] & chunkMask);
var otherY = @intCast(u8, y+%Neighbors.relY[neighbor] & chunkMask);
var otherZ = @intCast(u8, z+%Neighbors.relZ[neighbor] & chunkMask);
var block = (&chunk.blocks)[getIndex(x, y, z)]; // ← a temporary fix to a compiler performance bug. TODO: check if this was fixed.
var otherBlock = (&neighborMesh.chunk.load(.Monotonic).?.blocks)[getIndex(otherX, otherY, otherZ)]; // ← a temporary fix to a compiler performance bug. TODO: check if this was fixed.
if(canBeSeenThroughOtherBlock(block, otherBlock, neighbor)) {
try additionalNeighborFaces.append(constructFaceData(block, neighbor, otherX, otherY, otherZ));
}
if(canBeSeenThroughOtherBlock(otherBlock, block, neighbor ^ 1)) {
try self.faces.append(constructFaceData(otherBlock, neighbor ^ 1, x, y, z));
}
}
}
var rangeStart = neighborMesh.neighborStart[neighbor ^ 1];
var rangeEnd = neighborMesh.neighborStart[(neighbor ^ 1)+1];
try neighborMesh.faces.replaceRange(rangeStart, rangeEnd - rangeStart, additionalNeighborFaces.items);
for(neighborMesh.neighborStart[1+(neighbor ^ 1)..]) |*neighborStart| {
neighborStart.* = neighborStart.* - (rangeEnd - rangeStart) + @intCast(u31, additionalNeighborFaces.items.len);
}
try neighborMesh.uploadData();
continue;
}
}
// lod border:
if(self.pos.voxelSize == 1 << settings.highestLOD) continue;
var neighborMesh = renderer.RenderStructure.getNeighbor(self.pos, 2*self.pos.voxelSize, neighbor) orelse return error.LODMissing;
neighborMesh.mutex.lock();
defer neighborMesh.mutex.unlock();
if(neighborMesh.generated) {
const x3: u8 = if(neighbor & 1 == 0) @intCast(u8, chunkMask) else 0;
const offsetX = @divExact(self.pos.wx, self.pos.voxelSize) & chunkSize;
const offsetY = @divExact(self.pos.wy, self.pos.voxelSize) & chunkSize;
const offsetZ = @divExact(self.pos.wz, self.pos.voxelSize) & chunkSize;
var x1: u8 = 0;
while(x1 < chunkSize): (x1 += 1) {
var x2: u8 = 0;
while(x2 < chunkSize): (x2 += 1) {
var x: u8 = undefined;
var y: u8 = undefined;
var z: u8 = undefined;
if(Neighbors.relX[neighbor] != 0) {
x = x3;
y = x1;
z = x2;
} else if(Neighbors.relY[neighbor] != 0) {
x = x1;
y = x3;
z = x2;
} else {
x = x2;
y = x1;
z = x3;
}
var otherX = @intCast(u8, (x+%Neighbors.relX[neighbor]+%offsetX >> 1) & chunkMask);
var otherY = @intCast(u8, (y+%Neighbors.relY[neighbor]+%offsetY >> 1) & chunkMask);
var otherZ = @intCast(u8, (z+%Neighbors.relZ[neighbor]+%offsetZ >> 1) & chunkMask);
var block = (&chunk.blocks)[getIndex(x, y, z)]; // ← a temporary fix to a compiler performance bug. TODO: check if this was fixed.
var otherBlock = (&neighborMesh.chunk.load(.Monotonic).?.blocks)[getIndex(otherX, otherY, otherZ)]; // ← a temporary fix to a compiler performance bug. TODO: check if this was fixed.
if(canBeSeenThroughOtherBlock(otherBlock, block, neighbor ^ 1)) {
try self.faces.append(constructFaceData(otherBlock, neighbor ^ 1, x, y, z));
}
}
}
} else {
return error.LODMissing;
}
}
self.neighborStart[6] = @intCast(u31, self.faces.items.len);
try self.uploadData();
self.generated = true;
}
pub fn render(self: *ChunkMesh, playerPosition: Vec3d) void {
if(!self.generated) {
return;
}
if(self.vertexCount == 0) return;
c.glUniform3f(
uniforms.modelPosition,
@floatCast(f32, @intToFloat(f64, self.pos.wx) - playerPosition[0]),
@floatCast(f32, @intToFloat(f64, self.pos.wy) - playerPosition[1]),
@floatCast(f32, @intToFloat(f64, self.pos.wz) - playerPosition[2])
);
c.glUniform1i(uniforms.visibilityMask, self.visibilityMask);
c.glUniform1i(uniforms.voxelSize, self.pos.voxelSize);
c.glDrawElementsBaseVertex(c.GL_TRIANGLES, self.vertexCount, c.GL_UNSIGNED_INT, null, self.bufferAllocation.start/8*4);
}
};
};