Skip to content

Commit

Permalink
Make flying through caves fun again (removes CPU-side occlusion culling)
Browse files Browse the repository at this point in the history
This is not quite the performance improvement I had planned.
On the GPU-side the raster occlusion culling step got more expensive, since there now are more chunks checked on average.
On the CPU-side the cost of traversing all the occluded chunks is roughly equal to the cost of the previous occlusion culling heuristic.
On the bright side the cost is now constant, whereas previously looking into the sky was twice as expensive.

fixes #526
fixes #514
fixes #259
  • Loading branch information
IntegratedQuantum committed Sep 15, 2024
1 parent d7055f6 commit e9fdad7
Show file tree
Hide file tree
Showing 3 changed files with 23 additions and 204 deletions.
3 changes: 1 addition & 2 deletions src/renderer.zig
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,6 @@ pub fn renderWorld(world: *World, ambientLight: Vec3f, skyColor: Vec3f, playerPo

// Uses FrustumCulling on the chunks.
const frustum = Frustum.init(Vec3f{0, 0, 0}, game.camera.viewMatrix, lastFov, lastWidth, lastHeight);
_ = frustum;

const time: u32 = @intCast(std.time.milliTimestamp() & std.math.maxInt(u32));

Expand All @@ -196,7 +195,7 @@ pub fn renderWorld(world: *World, ambientLight: Vec3f, skyColor: Vec3f, playerPo

chunk_meshing.quadsDrawn = 0;
chunk_meshing.transparentQuadsDrawn = 0;
const meshes = mesh_storage.updateAndGetRenderChunks(world.conn, playerPos, settings.renderDistance);
const meshes = mesh_storage.updateAndGetRenderChunks(world.conn, &frustum, playerPos, settings.renderDistance);

gpu_performance_measuring.startQuery(.chunk_rendering_preparation);
const direction = crosshairDirection(game.camera.viewMatrix, lastFov, lastWidth, lastHeight);
Expand Down
26 changes: 0 additions & 26 deletions src/renderer/chunk_meshing.zig
Original file line number Diff line number Diff line change
Expand Up @@ -658,17 +658,6 @@ pub const ChunkMesh = struct { // MARK: ChunkMesh
self.distance = @abs(fullDx) + @abs(fullDy) + @abs(fullDz);
}
};
const BoundingRectToNeighborChunk = struct {
min: Vec3i = @splat(std.math.maxInt(i32)),
max: Vec3i = @splat(0),

fn adjustToBlock(self: *BoundingRectToNeighborChunk, block: Block, pos: Vec3i, neighbor: chunk.Neighbor) void {
if(block.viewThrough()) {
self.min = @min(self.min, pos);
self.max = @max(self.max, pos + neighbor.orthogonalComponents());
}
}
};
pos: chunk.ChunkPosition,
size: i32,
chunk: *chunk.Chunk,
Expand All @@ -695,8 +684,6 @@ pub const ChunkMesh = struct { // MARK: ChunkMesh
min: Vec3f = undefined,
max: Vec3f = undefined,

chunkBorders: [6]BoundingRectToNeighborChunk = [1]BoundingRectToNeighborChunk{.{}} ** 6,

pub fn init(self: *ChunkMesh, pos: chunk.ChunkPosition, ch: *chunk.Chunk) void {
self.* = ChunkMesh{
.pos = pos,
Expand Down Expand Up @@ -1184,19 +1171,6 @@ pub const ChunkMesh = struct { // MARK: ChunkMesh
}
}

// Check out the borders:
var x: u8 = 0;
while(x < chunk.chunkSize): (x += 1) {
var y: u8 = 0;
while(y < chunk.chunkSize): (y += 1) {
self.chunkBorders[chunk.Neighbor.dirNegX.toInt()].adjustToBlock(self.chunk.data.getValue(chunk.getIndex(0, x, y)), .{0, x, y}, chunk.Neighbor.dirNegX);
self.chunkBorders[chunk.Neighbor.dirPosX.toInt()].adjustToBlock(self.chunk.data.getValue(chunk.getIndex(chunk.chunkSize-1, x, y)), .{chunk.chunkSize, x, y}, chunk.Neighbor.dirPosX);
self.chunkBorders[chunk.Neighbor.dirNegY.toInt()].adjustToBlock(self.chunk.data.getValue(chunk.getIndex(x, 0, y)), .{x, 0, y}, chunk.Neighbor.dirNegY);
self.chunkBorders[chunk.Neighbor.dirPosY.toInt()].adjustToBlock(self.chunk.data.getValue(chunk.getIndex(x, chunk.chunkSize-1, y)), .{x, chunk.chunkSize, y}, chunk.Neighbor.dirPosY);
self.chunkBorders[chunk.Neighbor.dirDown.toInt()].adjustToBlock(self.chunk.data.getValue(chunk.getIndex(x, y, 0)), .{x, y, 0}, chunk.Neighbor.dirDown);
self.chunkBorders[chunk.Neighbor.dirUp.toInt()].adjustToBlock(self.chunk.data.getValue(chunk.getIndex(x, y, chunk.chunkSize-1)), .{x, y, chunk.chunkSize}, chunk.Neighbor.dirUp);
}
}
self.mutex.unlock();

self.finishNeighbors(lightRefreshList);
Expand Down
198 changes: 22 additions & 176 deletions src/renderer/mesh_storage.zig
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ const chunk_meshing = @import("chunk_meshing.zig");
const ChunkMeshNode = struct {
mesh: ?*chunk_meshing.ChunkMesh = null,
lod: u3 = undefined,
min: Vec2f = undefined,
max: Vec2f = undefined,
active: bool = false,
rendered: bool = false,
finishedMeshing: bool = false, // Must be synced with mesh.finishedMeshing
Expand Down Expand Up @@ -536,7 +534,7 @@ fn createNewMeshes(olderPx: i32, olderPy: i32, olderPz: i32, olderRD: i32, meshR
}
}

pub noinline fn updateAndGetRenderChunks(conn: *network.Connection, playerPos: Vec3d, renderDistance: i32) []*chunk_meshing.ChunkMesh { // MARK: updateAndGetRenderChunks()
pub noinline fn updateAndGetRenderChunks(conn: *network.Connection, frustum: *const main.renderer.Frustum, playerPos: Vec3d, renderDistance: i32) []*chunk_meshing.ChunkMesh { // MARK: updateAndGetRenderChunks()
meshList.clearRetainingCapacity();
if(lastRD != renderDistance) {
network.Protocols.genericUpdate.sendRenderDistance(conn, renderDistance);
Expand Down Expand Up @@ -565,9 +563,9 @@ pub noinline fn updateAndGetRenderChunks(conn: *network.Connection, playerPos: V
network.Protocols.lightMapRequest.sendRequest(conn, mapRequests.items);
network.Protocols.chunkRequest.sendRequest(conn, meshRequests.items, .{lastPx, lastPy, lastPz});

// Does occlusion using a breadth-first search that caches an on-screen visibility rectangle.
// Finds all visible chunks and lod chunks using a breadth-first search.

const OcclusionData = struct {
const SearchData = struct {
node: *ChunkMeshNode,
distance: f64,

Expand All @@ -578,8 +576,7 @@ pub noinline fn updateAndGetRenderChunks(conn: *network.Connection, playerPos: V
}
};

// TODO: Is there a way to combine this with minecraft's approach?
var searchList = std.PriorityQueue(OcclusionData, void, OcclusionData.compare).init(main.stackAllocator.allocator, {});
var searchList = std.PriorityQueue(SearchData, void, SearchData.compare).init(main.stackAllocator.allocator, {});
defer searchList.deinit();
{
var firstPos = chunk.ChunkPosition{
Expand All @@ -597,8 +594,6 @@ pub noinline fn updateAndGetRenderChunks(conn: *network.Connection, playerPos: V
const hasMesh = node.finishedMeshing;
if(hasMesh) {
node.lod = lod;
node.min = @splat(-1);
node.max = @splat(1);
node.active = true;
node.rendered = true;
searchList.add(.{
Expand All @@ -615,168 +610,32 @@ pub noinline fn updateAndGetRenderChunks(conn: *network.Connection, playerPos: V
}
var nodeList = main.List(*ChunkMeshNode).init(main.stackAllocator);
defer nodeList.deinit();
const projRotMat = game.projectionMatrix.mul(game.camera.viewMatrix);
while(searchList.removeOrNull()) |data| {
std.debug.assert(data.node.finishedMeshing);
nodeList.append(data.node);
data.node.active = false;

mutex.lock();
const mesh = data.node.mesh orelse {
mutex.unlock();
continue;
};
mesh.increaseRefCount();
defer mesh.decreaseRefCount();
mutex.unlock();
const mesh = data.node.mesh.?; // No need to lock the mutex, since no other thread is allowed to overwrite the mesh (unless it's null).
const pos = mesh.pos;

mesh.visibilityMask = 0xff;
const relPos: Vec3d = @as(Vec3d, @floatFromInt(Vec3i{mesh.pos.wx, mesh.pos.wy, mesh.pos.wz})) - playerPos;
const relPos: Vec3d = @as(Vec3d, @floatFromInt(Vec3i{pos.wx, pos.wy, pos.wz})) - playerPos;
const relPosFloat: Vec3f = @floatCast(relPos);
var isNeighborLod: [6]bool = .{false} ** 6;
for(chunk.Neighbor.iterable) |neighbor| continueNeighborLoop: {
neighborLoop: for(chunk.Neighbor.iterable) |neighbor| {
const component = neighbor.extractDirectionComponent(relPosFloat);
if(neighbor.isPositive() and component + @as(f32, @floatFromInt(chunk.chunkSize*mesh.pos.voxelSize)) <= 0) continue;
if(neighbor.isPositive() and component + @as(f32, @floatFromInt(chunk.chunkSize*pos.voxelSize)) <= 0) continue;
if(!neighbor.isPositive() and component >= 0) continue;
if(@reduce(.Or, mesh.chunkBorders[neighbor.toInt()].max < mesh.chunkBorders[neighbor.toInt()].min)) continue; // There was not a single transparent block along the chunk border. TODO: Find a better solution.
const minVec: Vec3f = @floatFromInt(mesh.chunkBorders[neighbor.toInt()].min*@as(Vec3i, @splat(mesh.pos.voxelSize)));
const maxVec: Vec3f = @floatFromInt(mesh.chunkBorders[neighbor.toInt()].max*@as(Vec3i, @splat(mesh.pos.voxelSize)));
var xyMin: Vec2f = .{10, 10};
var xyMax: Vec2f = .{-10, -10};
var numberOfNegatives: u8 = 0;
var corners: [5]Vec4f = undefined;
var curCorner: usize = 0;
for(0..2) |a| {
for(0..2) |b| {

var cornerVector: Vec3f = undefined;
switch(neighbor.vectorComponent()) {
.x => {
cornerVector = @select(f32, @Vector(3, bool){true, a == 0, b == 0}, minVec, maxVec);
},
.y => {
cornerVector = @select(f32, @Vector(3, bool){a == 0, true, b == 0}, minVec, maxVec);
},
.z => {
cornerVector = @select(f32, @Vector(3, bool){a == 0, b == 0, true}, minVec, maxVec);
},
}
corners[curCorner] = projRotMat.mulVec(vec.combine(relPosFloat + cornerVector, 1));
if(corners[curCorner][3] < 0) {
numberOfNegatives += 1;
}
curCorner += 1;
}
}
switch(numberOfNegatives) { // Oh, so complicated. But this should only trigger very close to the player.
4 => continue,
0 => {},
1 => {
// Needs to duplicate the problematic corner and move it onto the projected plane.
var problematicOne: usize = 0;
for(0..curCorner) |i| {
if(corners[i][3] < 0) {
problematicOne = i;
break;
}
}
const problematicVector = corners[problematicOne];
// The two neighbors of the quad:
const neighborA = corners[problematicOne ^ 1];
const neighborB = corners[problematicOne ^ 2];
// Move the problematic point towards the neighbor:
const one: Vec4f = @splat(1);
const weightA: Vec4f = @splat(problematicVector[3]/(problematicVector[3] - neighborA[3]));
var towardsA = neighborA*weightA + problematicVector*(one - weightA);
towardsA[3] = 0; // Prevent inaccuracies
const weightB: Vec4f = @splat(problematicVector[3]/(problematicVector[3] - neighborB[3]));
var towardsB = neighborB*weightB + problematicVector*(one - weightB);
towardsB[3] = 0; // Prevent inaccuracies
corners[problematicOne] = towardsA;
corners[curCorner] = towardsB;
curCorner += 1;
},
2 => {
// Needs to move the two problematic corners onto the projected plane.
var problematicOne: usize = undefined;
for(0..curCorner) |i| {
if(corners[i][3] < 0) {
problematicOne = i;
break;
}
}
const problematicVectorOne = corners[problematicOne];
var problematicTwo: usize = undefined;
for(problematicOne+1..curCorner) |i| {
if(corners[i][3] < 0) {
problematicTwo = i;
break;
}
}
const problematicVectorTwo = corners[problematicTwo];

const commonDirection = problematicOne ^ problematicTwo;
const projectionDirection = commonDirection ^ 0b11;
// The respective neighbors:
const neighborOne = corners[problematicOne ^ projectionDirection];
const neighborTwo = corners[problematicTwo ^ projectionDirection];
// Move the problematic points towards the neighbor:
const one: Vec4f = @splat(1);
const weightOne: Vec4f = @splat(problematicVectorOne[3]/(problematicVectorOne[3] - neighborOne[3]));
var towardsOne = neighborOne*weightOne + problematicVectorOne*(one - weightOne);
towardsOne[3] = 0; // Prevent inaccuracies
corners[problematicOne] = towardsOne;

const weightTwo: Vec4f = @splat(problematicVectorTwo[3]/(problematicVectorTwo[3] - neighborTwo[3]));
var towardsTwo = neighborTwo*weightTwo + problematicVectorTwo*(one - weightTwo);
towardsTwo[3] = 0; // Prevent inaccuracies
corners[problematicTwo] = towardsTwo;
},
3 => {
// Throw away the far problematic vector, move the other two onto the projection plane.
var neighborIndex: usize = undefined;
for(0..curCorner) |i| {
if(corners[i][3] >= 0) {
neighborIndex = i;
break;
}
}
const neighborVector = corners[neighborIndex];
const problematicVectorOne = corners[neighborIndex ^ 1];
const problematicVectorTwo = corners[neighborIndex ^ 2];
// Move the problematic points towards the neighbor:
const one: Vec4f = @splat(1);
const weightOne: Vec4f = @splat(problematicVectorOne[3]/(problematicVectorOne[3] - neighborVector[3]));
var towardsOne = neighborVector*weightOne + problematicVectorOne*(one - weightOne);
towardsOne[3] = 0; // Prevent inaccuracies

const weightTwo: Vec4f = @splat(problematicVectorTwo[3]/(problematicVectorTwo[3] - neighborVector[3]));
var towardsTwo = neighborVector*weightTwo + problematicVectorTwo*(one - weightTwo);
towardsTwo[3] = 0; // Prevent inaccuracies

corners[0] = neighborVector;
corners[1] = towardsOne;
corners[2] = towardsTwo;
curCorner = 3;
},
else => unreachable,
}

for(0..curCorner) |i| {
const projected = corners[i];
const xy = vec.xy(projected)/@as(Vec2f, @splat(@max(0, projected[3])));
xyMin = @min(xyMin, xy);
xyMax = @max(xyMax, xy);
}
const min = @max(xyMin, data.node.min);
const max = @min(xyMax, data.node.max);
if(@reduce(.Or, min >= max)) continue; // Nothing to render.
var neighborPos = chunk.ChunkPosition{
.wx = mesh.pos.wx +% neighbor.relX()*chunk.chunkSize*mesh.pos.voxelSize,
.wy = mesh.pos.wy +% neighbor.relY()*chunk.chunkSize*mesh.pos.voxelSize,
.wz = mesh.pos.wz +% neighbor.relZ()*chunk.chunkSize*mesh.pos.voxelSize,
.voxelSize = mesh.pos.voxelSize,
.wx = pos.wx +% neighbor.relX()*chunk.chunkSize*pos.voxelSize,
.wy = pos.wy +% neighbor.relY()*chunk.chunkSize*pos.voxelSize,
.wz = pos.wz +% neighbor.relZ()*chunk.chunkSize*pos.voxelSize,
.voxelSize = pos.voxelSize,
};
if(!getNodePointer(neighborPos).active) { // Don't repeat the same frustum check all the time.
if(!frustum.testAAB(relPosFloat + @as(Vec3f, @floatFromInt(Vec3i{neighbor.relX()*chunk.chunkSize*pos.voxelSize, neighbor.relY()*chunk.chunkSize*pos.voxelSize, neighbor.relZ()*chunk.chunkSize*pos.voxelSize})), @splat(@floatFromInt(chunk.chunkSize*pos.voxelSize))))
continue;
}
var lod: u3 = data.node.lod;
lodLoop: while(lod <= settings.highestLOD) : (lod += 1) {
defer {
Expand Down Expand Up @@ -824,21 +683,16 @@ pub noinline fn updateAndGetRenderChunks(conn: *network.Connection, playerPos: V
if(lod != data.node.lod) {
isNeighborLod[neighbor.toInt()] = true;
}
if(node.active) {
node.min = @min(node.min, min);
node.max = @max(node.max, max);
} else {
if(!node.active) {
node.lod = lod;
node.min = min;
node.max = max;
node.active = true;
searchList.add(.{
.node = node,
.distance = neighborPos.getMaxDistanceSquared(playerPos),
}) catch unreachable;
node.rendered = true;
}
break :continueNeighborLoop;
continue :neighborLoop;
}
}
}
Expand All @@ -848,20 +702,12 @@ pub noinline fn updateAndGetRenderChunks(conn: *network.Connection, playerPos: V
node.rendered = false;
if(!node.finishedMeshing) continue;

node.mutex.lock();
const mesh = node.mesh orelse {
node.mutex.unlock();
continue;
};
mesh.increaseRefCount();
defer mesh.decreaseRefCount();
node.mutex.unlock();
const mesh = node.mesh.?; // No need to lock the mutex, since no other thread is allowed to overwrite the mesh (unless it's null).

if(mesh.pos.voxelSize != @as(u31, 1) << settings.highestLOD) {
const parent = getNodePointer(.{.wx=mesh.pos.wx, .wy=mesh.pos.wy, .wz=mesh.pos.wz, .voxelSize=mesh.pos.voxelSize << 1});
parent.mutex.lock();
defer parent.mutex.unlock();
if(parent.mesh) |parentMesh| {
if(parent.finishedMeshing) {
const parentMesh = parent.mesh.?; // No need to lock the mutex, since no other thread is allowed to overwrite the mesh (unless it's null).
const sizeShift = chunk.chunkShift + @ctz(mesh.pos.voxelSize);
const octantIndex: u3 = @intCast((mesh.pos.wx>>sizeShift & 1) | (mesh.pos.wy>>sizeShift & 1)<<1 | (mesh.pos.wz>>sizeShift & 1)<<2);
parentMesh.visibilityMask &= ~(@as(u8, 1) << octantIndex);
Expand Down

0 comments on commit e9fdad7

Please sign in to comment.