Make flying through caves fun again (removes CPU-side occlusion culling)

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
This commit is contained in:
IntegratedQuantum 2024-09-15 17:26:56 +02:00
parent d7055f6998
commit e9fdad732d
3 changed files with 23 additions and 204 deletions

View File

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

View File

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

View File

@ -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
@ -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);
@ -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,
@ -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{
@ -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(.{
@ -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 {
@ -824,13 +683,8 @@ 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,
@ -838,7 +692,7 @@ pub noinline fn updateAndGetRenderChunks(conn: *network.Connection, playerPos: V
}) catch unreachable;
node.rendered = true;
}
break :continueNeighborLoop;
continue :neighborLoop;
}
}
}
@ -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);