mirror of
https://github.com/PixelGuys/Cubyz.git
synced 2025-08-03 19:28:49 -04:00
Occlusion culling
This commit is contained in:
parent
83e8d52120
commit
a703cb6c6a
@ -52,6 +52,27 @@ pub const Neighbors = struct {
|
|||||||
pub const bitMask = [_]u6 {0x01, 0x02, 0x04, 0x08, 0x10, 0x20};
|
pub const bitMask = [_]u6 {0x01, 0x02, 0x04, 0x08, 0x10, 0x20};
|
||||||
/// To iterate over all neighbors easily
|
/// To iterate over all neighbors easily
|
||||||
pub const iterable = [_]u3 {0, 1, 2, 3, 4, 5};
|
pub const iterable = [_]u3 {0, 1, 2, 3, 4, 5};
|
||||||
|
/// Marks the two dimension that are orthogonal
|
||||||
|
pub const orthogonalComponents = [_]Vec3i {
|
||||||
|
.{1, 0, 1},
|
||||||
|
.{1, 0, 1},
|
||||||
|
.{0, 1, 1},
|
||||||
|
.{0, 1, 1},
|
||||||
|
.{1, 1, 0},
|
||||||
|
.{1, 1, 0},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const isPositive = [_]bool {true, false, true, false, true, false};
|
||||||
|
pub const vectorComponent = [_]enum(u2){x = 0, y = 1, z = 2} {.y, .y, .x, .x, .z, .z};
|
||||||
|
|
||||||
|
pub fn extractDirectionComponent(self: u3, in: anytype) @TypeOf(in[0]) {
|
||||||
|
switch(self) {
|
||||||
|
inline else => |val| {
|
||||||
|
if(val >= 6) unreachable;
|
||||||
|
return in[@intFromEnum(vectorComponent[val])];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Gets the index of a given position inside this chunk.
|
/// Gets the index of a given position inside this chunk.
|
||||||
@ -105,6 +126,25 @@ pub const ChunkPosition = struct {
|
|||||||
return dx*dx + dy*dy + dz*dz;
|
return dx*dx + dy*dy + dz*dz;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getMaxDistanceSquared(self: ChunkPosition, playerPosition: Vec3d) f64 {
|
||||||
|
var halfWidth: f64 = @floatFromInt(self.voxelSize*@divExact(chunkSize, 2));
|
||||||
|
var dx = @fabs(@as(f64, @floatFromInt(self.wx)) + halfWidth - playerPosition[0]);
|
||||||
|
var dy = @fabs(@as(f64, @floatFromInt(self.wy)) + halfWidth - playerPosition[1]);
|
||||||
|
var dz = @fabs(@as(f64, @floatFromInt(self.wz)) + halfWidth - playerPosition[2]);
|
||||||
|
dx = dx + halfWidth;
|
||||||
|
dy = dy + halfWidth;
|
||||||
|
dz = dz + halfWidth;
|
||||||
|
return dx*dx + dy*dy + dz*dz;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getCenterDistanceSquared(self: ChunkPosition, playerPosition: Vec3d) f64 {
|
||||||
|
var halfWidth: f64 = @floatFromInt(self.voxelSize*@divExact(chunkSize, 2));
|
||||||
|
var dx = @as(f64, @floatFromInt(self.wx)) + halfWidth - playerPosition[0];
|
||||||
|
var dy = @as(f64, @floatFromInt(self.wy)) + halfWidth - playerPosition[1];
|
||||||
|
var dz = @as(f64, @floatFromInt(self.wz)) + halfWidth - playerPosition[2];
|
||||||
|
return dx*dx + dy*dy + dz*dz;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn getPriority(self: ChunkPosition, playerPos: Vec3d) f32 {
|
pub fn getPriority(self: ChunkPosition, playerPos: Vec3d) f32 {
|
||||||
return -@as(f32, @floatCast(self.getMinDistanceSquared(playerPos)))/@as(f32, @floatFromInt(self.voxelSize*self.voxelSize)) + 2*@as(f32, @floatFromInt(std.math.log2_int(u31, self.voxelSize)*chunkSize*chunkSize));
|
return -@as(f32, @floatCast(self.getMinDistanceSquared(playerPos)))/@as(f32, @floatFromInt(self.voxelSize*self.voxelSize)) + 2*@as(f32, @floatFromInt(std.math.log2_int(u31, self.voxelSize)*chunkSize*chunkSize));
|
||||||
}
|
}
|
||||||
@ -372,6 +412,8 @@ pub const meshing = struct {
|
|||||||
var vbo: c_uint = undefined;
|
var vbo: c_uint = undefined;
|
||||||
var faces: std.ArrayList(u32) = undefined;
|
var faces: std.ArrayList(u32) = undefined;
|
||||||
pub var faceBuffer: graphics.LargeBuffer = undefined;
|
pub var faceBuffer: graphics.LargeBuffer = undefined;
|
||||||
|
pub var quadsDrawn: usize = 0;
|
||||||
|
pub var transparentQuadsDrawn: usize = 0;
|
||||||
|
|
||||||
pub fn init() !void {
|
pub fn init() !void {
|
||||||
shader = try Shader.initAndGetUniforms("assets/cubyz/shaders/chunks/chunk_vertex.vs", "assets/cubyz/shaders/chunks/chunk_fragment.fs", &uniforms);
|
shader = try Shader.initAndGetUniforms("assets/cubyz/shaders/chunks/chunk_vertex.vs", "assets/cubyz/shaders/chunks/chunk_fragment.fs", &uniforms);
|
||||||
@ -585,6 +627,17 @@ pub const meshing = struct {
|
|||||||
index: u32,
|
index: u32,
|
||||||
distance: u32,
|
distance: u32,
|
||||||
};
|
};
|
||||||
|
const BoundingRectToNeighborChunk = struct {
|
||||||
|
min: Vec3i = @splat(std.math.maxInt(i32)),
|
||||||
|
max: Vec3i = @splat(0),
|
||||||
|
|
||||||
|
fn adjustToBlock(self: *BoundingRectToNeighborChunk, block: Block, pos: Vec3i, neighbor: u3) void {
|
||||||
|
if(block.viewThrough()) {
|
||||||
|
self.min = @min(self.min, pos);
|
||||||
|
self.max = @max(self.max, pos + Neighbors.orthogonalComponents[neighbor]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
pos: ChunkPosition,
|
pos: ChunkPosition,
|
||||||
size: i32,
|
size: i32,
|
||||||
chunk: std.atomic.Atomic(?*Chunk),
|
chunk: std.atomic.Atomic(?*Chunk),
|
||||||
@ -599,6 +652,8 @@ pub const meshing = struct {
|
|||||||
currentSortingSwap: []SortingData = &.{},
|
currentSortingSwap: []SortingData = &.{},
|
||||||
lastTransparentUpdatePos: Vec3i = Vec3i{0, 0, 0},
|
lastTransparentUpdatePos: Vec3i = Vec3i{0, 0, 0},
|
||||||
|
|
||||||
|
chunkBorders: [6]BoundingRectToNeighborChunk = [1]BoundingRectToNeighborChunk{.{}} ** 6,
|
||||||
|
|
||||||
pub fn init(allocator: Allocator, pos: ChunkPosition) ChunkMesh {
|
pub fn init(allocator: Allocator, pos: ChunkPosition) ChunkMesh {
|
||||||
return ChunkMesh{
|
return ChunkMesh{
|
||||||
.pos = pos,
|
.pos = pos,
|
||||||
@ -683,6 +738,19 @@ pub const meshing = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Check out the borders:
|
||||||
|
x = 0;
|
||||||
|
while(x < chunkSize): (x += 1) {
|
||||||
|
var y: u8 = 0;
|
||||||
|
while(y < chunkSize): (y += 1) {
|
||||||
|
self.chunkBorders[Neighbors.dirNegX].adjustToBlock((&chunk.blocks)[getIndex(0, x, y)], .{0, x, y}, Neighbors.dirNegX); // TODO: Wait for the compiler bug to get fixed.
|
||||||
|
self.chunkBorders[Neighbors.dirPosX].adjustToBlock((&chunk.blocks)[getIndex(chunkSize-1, x, y)], .{chunkSize, x, y}, Neighbors.dirPosX); // TODO: Wait for the compiler bug to get fixed.
|
||||||
|
self.chunkBorders[Neighbors.dirDown].adjustToBlock((&chunk.blocks)[getIndex(x, 0, y)], .{x, 0, y}, Neighbors.dirDown); // TODO: Wait for the compiler bug to get fixed.
|
||||||
|
self.chunkBorders[Neighbors.dirUp].adjustToBlock((&chunk.blocks)[getIndex(x, chunkSize-1, y)], .{x, chunkSize, y}, Neighbors.dirUp); // TODO: Wait for the compiler bug to get fixed.
|
||||||
|
self.chunkBorders[Neighbors.dirNegZ].adjustToBlock((&chunk.blocks)[getIndex(x, y, 0)], .{x, y, 0}, Neighbors.dirNegZ); // TODO: Wait for the compiler bug to get fixed.
|
||||||
|
self.chunkBorders[Neighbors.dirPosZ].adjustToBlock((&chunk.blocks)[getIndex(x, y, chunkSize-1)], .{x, y, chunkSize}, Neighbors.dirPosZ); // TODO: Wait for the compiler bug to get fixed.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if(self.chunk.swap(chunk, .Monotonic)) |oldChunk| {
|
if(self.chunk.swap(chunk, .Monotonic)) |oldChunk| {
|
||||||
main.globalAllocator.destroy(oldChunk);
|
main.globalAllocator.destroy(oldChunk);
|
||||||
@ -948,6 +1016,7 @@ pub const meshing = struct {
|
|||||||
);
|
);
|
||||||
c.glUniform1i(uniforms.visibilityMask, self.visibilityMask);
|
c.glUniform1i(uniforms.visibilityMask, self.visibilityMask);
|
||||||
c.glUniform1i(uniforms.voxelSize, self.pos.voxelSize);
|
c.glUniform1i(uniforms.voxelSize, self.pos.voxelSize);
|
||||||
|
quadsDrawn += self.opaqueMesh.faces.items.len;
|
||||||
c.glDrawElementsBaseVertex(c.GL_TRIANGLES, self.opaqueMesh.vertexCount, c.GL_UNSIGNED_INT, null, self.opaqueMesh.bufferAllocation.start/8*4);
|
c.glDrawElementsBaseVertex(c.GL_TRIANGLES, self.opaqueMesh.vertexCount, c.GL_UNSIGNED_INT, null, self.opaqueMesh.bufferAllocation.start/8*4);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1046,6 +1115,7 @@ pub const meshing = struct {
|
|||||||
);
|
);
|
||||||
c.glUniform1i(transparentUniforms.visibilityMask, self.visibilityMask);
|
c.glUniform1i(transparentUniforms.visibilityMask, self.visibilityMask);
|
||||||
c.glUniform1i(transparentUniforms.voxelSize, self.pos.voxelSize);
|
c.glUniform1i(transparentUniforms.voxelSize, self.pos.voxelSize);
|
||||||
|
transparentQuadsDrawn += self.transparentMesh.faces.items.len;
|
||||||
c.glDrawElementsBaseVertex(c.GL_TRIANGLES, self.transparentMesh.vertexCount, c.GL_UNSIGNED_INT, null, self.transparentMesh.bufferAllocation.start/8*4);
|
c.glDrawElementsBaseVertex(c.GL_TRIANGLES, self.transparentMesh.vertexCount, c.GL_UNSIGNED_INT, null, self.transparentMesh.bufferAllocation.start/8*4);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -40,8 +40,9 @@ fn flawedRender() !void {
|
|||||||
y += 8;
|
y += 8;
|
||||||
try draw.print("Biome: {s}", .{main.game.world.?.playerBiome.id}, 0, y, 8, .left);
|
try draw.print("Biome: {s}", .{main.game.world.?.playerBiome.id}, 0, y, 8, .left);
|
||||||
y += 8;
|
y += 8;
|
||||||
// TODO: packet loss
|
try draw.print("Opaque faces: {}, Transparent faces: {}", .{main.chunk.meshing.quadsDrawn, main.chunk.meshing.transparentQuadsDrawn}, 0, y, 8, .left);
|
||||||
y += 8;
|
y += 8;
|
||||||
|
// TODO: packet loss
|
||||||
// TODO: Protocol statistics(maybe?)
|
// TODO: Protocol statistics(maybe?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -125,7 +125,7 @@ pub fn onOpen() Allocator.Error!void {
|
|||||||
|
|
||||||
const decodedName = try parseEscapedFolderName(entry.name);
|
const decodedName = try parseEscapedFolderName(entry.name);
|
||||||
defer main.threadAllocator.free(decodedName);
|
defer main.threadAllocator.free(decodedName);
|
||||||
const name = try buttonNameArena.allocator().dupeZ(u8, entry.name); // Null terminate, so we can later recover the string from jsut the pointer.
|
const name = try buttonNameArena.allocator().dupeZ(u8, entry.name); // Null terminate, so we can later recover the string from just the pointer.
|
||||||
const buttonName = try std.fmt.allocPrint(buttonNameArena.allocator(), "Play {s}", .{decodedName});
|
const buttonName = try std.fmt.allocPrint(buttonNameArena.allocator(), "Play {s}", .{decodedName});
|
||||||
|
|
||||||
try row.add(try Button.initText(.{0, 0}, 128, buttonName, .{.callback = &openWorld, .arg = @intFromPtr(name.ptr)}));
|
try row.add(try Button.initText(.{0, 0}, 128, buttonName, .{.callback = &openWorld, .arg = @intFromPtr(name.ptr)}));
|
||||||
|
314
src/renderer.zig
314
src/renderer.zig
@ -18,6 +18,7 @@ const network = @import("network.zig");
|
|||||||
const settings = @import("settings.zig");
|
const settings = @import("settings.zig");
|
||||||
const utils = @import("utils.zig");
|
const utils = @import("utils.zig");
|
||||||
const vec = @import("vec.zig");
|
const vec = @import("vec.zig");
|
||||||
|
const Vec2f = vec.Vec2f;
|
||||||
const Vec3i = vec.Vec3i;
|
const Vec3i = vec.Vec3i;
|
||||||
const Vec3f = vec.Vec3f;
|
const Vec3f = vec.Vec3f;
|
||||||
const Vec3d = vec.Vec3d;
|
const Vec3d = vec.Vec3d;
|
||||||
@ -198,6 +199,7 @@ pub fn renderWorld(world: *World, ambientLight: Vec3f, skyColor: Vec3f, playerPo
|
|||||||
|
|
||||||
// Uses FrustumCulling on the chunks.
|
// Uses FrustumCulling on the chunks.
|
||||||
var frustum = Frustum.init(Vec3f{0, 0, 0}, game.camera.viewMatrix, lastFov, lastWidth, lastHeight);
|
var 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));
|
const time: u32 = @intCast(std.time.milliTimestamp() & std.math.maxInt(u32));
|
||||||
var waterFog = Fog{.active=true, .color=.{0.0, 0.1, 0.2}, .density=0.1};
|
var waterFog = Fog{.active=true, .color=.{0.0, 0.1, 0.2}, .density=0.1};
|
||||||
@ -213,7 +215,9 @@ pub fn renderWorld(world: *World, ambientLight: Vec3f, skyColor: Vec3f, playerPo
|
|||||||
// SimpleList<NormalChunkMesh> visibleChunks = new SimpleList<NormalChunkMesh>(new NormalChunkMesh[64]);
|
// SimpleList<NormalChunkMesh> visibleChunks = new SimpleList<NormalChunkMesh>(new NormalChunkMesh[64]);
|
||||||
// SimpleList<ReducedChunkMesh> visibleReduced = new SimpleList<ReducedChunkMesh>(new ReducedChunkMesh[64]);
|
// SimpleList<ReducedChunkMesh> visibleReduced = new SimpleList<ReducedChunkMesh>(new ReducedChunkMesh[64]);
|
||||||
|
|
||||||
const meshes = try RenderStructure.updateAndGetRenderChunks(game.world.?.conn, playerPos, settings.renderDistance, settings.LODFactor, frustum);
|
chunk.meshing.quadsDrawn = 0;
|
||||||
|
chunk.meshing.transparentQuadsDrawn = 0;
|
||||||
|
const meshes = try RenderStructure.updateAndGetRenderChunks(game.world.?.conn, playerPos, settings.renderDistance, settings.LODFactor);
|
||||||
|
|
||||||
try sortChunks(meshes, playerPos);
|
try sortChunks(meshes, playerPos);
|
||||||
|
|
||||||
@ -865,6 +869,26 @@ pub const MeshSelection = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn drawCube(projectionMatrix: Mat4f, viewMatrix: Mat4f, relativePositionToPlayer: Vec3d, min: Vec3f, max: Vec3f) void {
|
||||||
|
shader.bind();
|
||||||
|
|
||||||
|
c.glUniformMatrix4fv(uniforms.projectionMatrix, 1, c.GL_FALSE, @ptrCast(&projectionMatrix));
|
||||||
|
c.glUniformMatrix4fv(uniforms.viewMatrix, 1, c.GL_FALSE, @ptrCast(&viewMatrix));
|
||||||
|
|
||||||
|
c.glUniform3f(uniforms.modelPosition,
|
||||||
|
@floatCast(relativePositionToPlayer[0]),
|
||||||
|
@floatCast(relativePositionToPlayer[1]),
|
||||||
|
@floatCast(relativePositionToPlayer[2]),
|
||||||
|
);
|
||||||
|
c.glUniform3f(uniforms.lowerBounds, min[0], min[1], min[2]);
|
||||||
|
c.glUniform3f(uniforms.upperBounds, max[0], max[1], max[2]);
|
||||||
|
|
||||||
|
c.glBindVertexArray(cubeVAO);
|
||||||
|
// c.glLineWidth(2); // TODO: Draw thicker lines so they are more visible. Maybe a simple shader + cube mesh is enough.
|
||||||
|
c.glDrawElements(c.GL_LINES, 12*2, c.GL_UNSIGNED_BYTE, null);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render(projectionMatrix: Mat4f, viewMatrix: Mat4f, playerPos: Vec3d) void {
|
pub fn render(projectionMatrix: Mat4f, viewMatrix: Mat4f, playerPos: Vec3d) void {
|
||||||
if(selectedBlockPos) |_selectedBlockPos| {
|
if(selectedBlockPos) |_selectedBlockPos| {
|
||||||
var block = RenderStructure.getBlock(_selectedBlockPos[0], _selectedBlockPos[1], _selectedBlockPos[2]) orelse return;
|
var block = RenderStructure.getBlock(_selectedBlockPos[0], _selectedBlockPos[1], _selectedBlockPos[2]) orelse return;
|
||||||
@ -874,28 +898,7 @@ pub const MeshSelection = struct {
|
|||||||
var transformedMax = model.permutation.transform(voxelModel.max - @as(Vec3i, @splat(8))) + @as(Vec3i, @splat(8));
|
var transformedMax = model.permutation.transform(voxelModel.max - @as(Vec3i, @splat(8))) + @as(Vec3i, @splat(8));
|
||||||
const min = @min(transformedMin, transformedMax);
|
const min = @min(transformedMin, transformedMax);
|
||||||
const max = @max(transformedMin ,transformedMax);
|
const max = @max(transformedMin ,transformedMax);
|
||||||
shader.bind();
|
drawCube(projectionMatrix, viewMatrix, vec.floatFromInt(f64, _selectedBlockPos) - playerPos, vec.floatFromInt(f32, min)/@as(Vec3f, @splat(16.0)), vec.floatFromInt(f32, max)/@as(Vec3f, @splat(16.0)));
|
||||||
|
|
||||||
c.glUniformMatrix4fv(uniforms.projectionMatrix, 1, c.GL_FALSE, @ptrCast(&projectionMatrix));
|
|
||||||
c.glUniformMatrix4fv(uniforms.viewMatrix, 1, c.GL_FALSE, @ptrCast(&viewMatrix));
|
|
||||||
c.glUniform3f(uniforms.modelPosition,
|
|
||||||
@floatCast(@as(f64, @floatFromInt(_selectedBlockPos[0])) - playerPos[0]),
|
|
||||||
@floatCast(@as(f64, @floatFromInt(_selectedBlockPos[1])) - playerPos[1]),
|
|
||||||
@floatCast(@as(f64, @floatFromInt(_selectedBlockPos[2])) - playerPos[2])
|
|
||||||
);
|
|
||||||
c.glUniform3f(uniforms.lowerBounds,
|
|
||||||
@as(f32, @floatFromInt(min[0]))/16.0,
|
|
||||||
@as(f32, @floatFromInt(min[1]))/16.0,
|
|
||||||
@as(f32, @floatFromInt(min[2]))/16.0
|
|
||||||
);
|
|
||||||
c.glUniform3f(uniforms.upperBounds,
|
|
||||||
@as(f32, @floatFromInt(max[0]))/16.0,
|
|
||||||
@as(f32, @floatFromInt(max[1]))/16.0,
|
|
||||||
@as(f32, @floatFromInt(max[2]))/16.0
|
|
||||||
);
|
|
||||||
|
|
||||||
c.glBindVertexArray(cubeVAO);
|
|
||||||
c.glDrawElements(c.GL_LINES, 12*2, c.GL_UNSIGNED_BYTE, null); // TODO: Draw thicker lines so they are more visible. Maybe a simple shader + cube mesh is enough.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -905,6 +908,10 @@ pub const RenderStructure = struct {
|
|||||||
mesh: chunk.meshing.ChunkMesh,
|
mesh: chunk.meshing.ChunkMesh,
|
||||||
shouldBeRemoved: bool, // Internal use.
|
shouldBeRemoved: bool, // Internal use.
|
||||||
drawableChildren: u32, // How many children can be renderer. If this is 8 then there is no need to render this mesh.
|
drawableChildren: u32, // How many children can be renderer. If this is 8 then there is no need to render this mesh.
|
||||||
|
lod: u3,
|
||||||
|
min: Vec2f,
|
||||||
|
max: Vec2f,
|
||||||
|
active: bool,
|
||||||
};
|
};
|
||||||
var storageLists: [settings.highestLOD + 1][]?*ChunkMeshNode = [1][]?*ChunkMeshNode{&.{}} ** (settings.highestLOD + 1);
|
var storageLists: [settings.highestLOD + 1][]?*ChunkMeshNode = [1][]?*ChunkMeshNode{&.{}} ** (settings.highestLOD + 1);
|
||||||
var storageListsSwap: [settings.highestLOD + 1][]?*ChunkMeshNode = [1][]?*ChunkMeshNode{&.{}} ** (settings.highestLOD + 1);
|
var storageListsSwap: [settings.highestLOD + 1][]?*ChunkMeshNode = [1][]?*ChunkMeshNode{&.{}} ** (settings.highestLOD + 1);
|
||||||
@ -963,6 +970,18 @@ pub const RenderStructure = struct {
|
|||||||
meshList.deinit();
|
meshList.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn getNodeFromRenderThread(pos: chunk.ChunkPosition) ?*ChunkMeshNode {
|
||||||
|
var lod = std.math.log2_int(u31, pos.voxelSize);
|
||||||
|
var xIndex = pos.wx-%(&lastX[lod]).* >> lod+chunk.chunkShift;
|
||||||
|
var yIndex = pos.wy-%(&lastY[lod]).* >> lod+chunk.chunkShift;
|
||||||
|
var zIndex = pos.wz-%(&lastZ[lod]).* >> lod+chunk.chunkShift;
|
||||||
|
if(xIndex < 0 or xIndex >= (&lastSize[lod]).*) return null;
|
||||||
|
if(yIndex < 0 or yIndex >= (&lastSize[lod]).*) return null;
|
||||||
|
if(zIndex < 0 or zIndex >= (&lastSize[lod]).*) return null;
|
||||||
|
var index = (xIndex*(&lastSize[lod]).* + yIndex)*(&lastSize[lod]).* + zIndex;
|
||||||
|
return storageLists[lod][@intCast(index)];
|
||||||
|
}
|
||||||
|
|
||||||
fn _getNode(pos: chunk.ChunkPosition) ?*ChunkMeshNode {
|
fn _getNode(pos: chunk.ChunkPosition) ?*ChunkMeshNode {
|
||||||
var lod = std.math.log2_int(u31, pos.voxelSize);
|
var lod = std.math.log2_int(u31, pos.voxelSize);
|
||||||
lodMutex[lod].lock();
|
lodMutex[lod].lock();
|
||||||
@ -998,7 +1017,7 @@ pub const RenderStructure = struct {
|
|||||||
return &node.mesh;
|
return &node.mesh;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn updateAndGetRenderChunks(conn: *network.Connection, playerPos: Vec3d, renderDistance: i32, LODFactor: f32, frustum: Frustum) ![]*chunk.meshing.ChunkMesh {
|
pub fn updateAndGetRenderChunks(conn: *network.Connection, playerPos: Vec3d, renderDistance: i32, LODFactor: f32) ![]*chunk.meshing.ChunkMesh {
|
||||||
meshList.clearRetainingCapacity();
|
meshList.clearRetainingCapacity();
|
||||||
if(lastRD != renderDistance and lastFactor != LODFactor) {
|
if(lastRD != renderDistance and lastFactor != LODFactor) {
|
||||||
try network.Protocols.genericUpdate.sendRenderDistance(conn, renderDistance, LODFactor);
|
try network.Protocols.genericUpdate.sendRenderDistance(conn, renderDistance, LODFactor);
|
||||||
@ -1010,7 +1029,7 @@ pub const RenderStructure = struct {
|
|||||||
var meshRequests = std.ArrayList(chunk.ChunkPosition).init(main.threadAllocator);
|
var meshRequests = std.ArrayList(chunk.ChunkPosition).init(main.threadAllocator);
|
||||||
defer meshRequests.deinit();
|
defer meshRequests.deinit();
|
||||||
|
|
||||||
for(0..storageLists.len) |_lod| {
|
for(0..storageLists.len) |_lod| { // TODO: Can this be done in a more intelligent way?
|
||||||
const lod: u5 = @intCast(_lod);
|
const lod: u5 = @intCast(_lod);
|
||||||
var maxRenderDistance = renderDistance*chunk.chunkSize << lod;
|
var maxRenderDistance = renderDistance*chunk.chunkSize << lod;
|
||||||
if(lod != 0) maxRenderDistance = @intFromFloat(@ceil(@as(f32, @floatFromInt(maxRenderDistance))*LODFactor));
|
if(lod != 0) maxRenderDistance = @intFromFloat(@ceil(@as(f32, @floatFromInt(maxRenderDistance))*LODFactor));
|
||||||
@ -1058,7 +1077,7 @@ pub const RenderStructure = struct {
|
|||||||
const zIndex = @divExact(z -% startZ, size);
|
const zIndex = @divExact(z -% startZ, size);
|
||||||
const index = (xIndex*maxSideLength + yIndex)*maxSideLength + zIndex;
|
const index = (xIndex*maxSideLength + yIndex)*maxSideLength + zIndex;
|
||||||
const pos = chunk.ChunkPosition{.wx=x, .wy=y, .wz=z, .voxelSize=@as(u31, 1)<<lod};
|
const pos = chunk.ChunkPosition{.wx=x, .wy=y, .wz=z, .voxelSize=@as(u31, 1)<<lod};
|
||||||
var node = _getNode(pos);
|
var node = getNodeFromRenderThread(pos);
|
||||||
if(node) |_node| {
|
if(node) |_node| {
|
||||||
_node.shouldBeRemoved = false;
|
_node.shouldBeRemoved = false;
|
||||||
} else {
|
} else {
|
||||||
@ -1067,21 +1086,10 @@ pub const RenderStructure = struct {
|
|||||||
node.?.shouldBeRemoved = true; // Might be removed in the next iteration.
|
node.?.shouldBeRemoved = true; // Might be removed in the next iteration.
|
||||||
try meshRequests.append(pos);
|
try meshRequests.append(pos);
|
||||||
}
|
}
|
||||||
if(frustum.testAAB(Vec3f{
|
|
||||||
@floatCast(@as(f64, @floatFromInt(x)) - playerPos[0]),
|
|
||||||
@floatCast(@as(f64, @floatFromInt(y)) - playerPos[1]),
|
|
||||||
@floatCast(@as(f64, @floatFromInt(z)) - playerPos[2]),
|
|
||||||
}, Vec3f{
|
|
||||||
@floatFromInt(size),
|
|
||||||
@floatFromInt(size),
|
|
||||||
@floatFromInt(size),
|
|
||||||
}) and node.?.mesh.visibilityMask != 0 and !node.?.mesh.isEmpty()) {
|
|
||||||
try meshList.append(&node.?.mesh);
|
|
||||||
}
|
|
||||||
if(lod+1 != storageLists.len and node.?.mesh.generated) {
|
if(lod+1 != storageLists.len and node.?.mesh.generated) {
|
||||||
if(_getNode(.{.wx=x, .wy=y, .wz=z, .voxelSize=@as(u31, 1)<<(lod+1)})) |parent| {
|
if(getNodeFromRenderThread(.{.wx=x, .wy=y, .wz=z, .voxelSize=@as(u31, 1)<<(lod+1)})) |parent| {
|
||||||
const octantIndex: u3 = @intCast((x>>sizeShift & 1) | (y>>sizeShift & 1)<<1 | (z>>sizeShift & 1)<<2);
|
const octantIndex: u3 = @intCast((x>>sizeShift & 1) | (y>>sizeShift & 1)<<1 | (z>>sizeShift & 1)<<2);
|
||||||
parent.mesh.visibilityMask &= ~(@as(u8, 1) << octantIndex);
|
parent.mesh.visibilityMask &= ~(@as(u8, 1) << octantIndex); // TODO: Find a more robust solution, that also works for the new occlusion culling.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
node.?.drawableChildren = 0;
|
node.?.drawableChildren = 0;
|
||||||
@ -1105,7 +1113,7 @@ pub const RenderStructure = struct {
|
|||||||
if(nullMesh) |mesh| {
|
if(nullMesh) |mesh| {
|
||||||
if(mesh.shouldBeRemoved) {
|
if(mesh.shouldBeRemoved) {
|
||||||
if(mesh.mesh.pos.voxelSize != 1 << settings.highestLOD) {
|
if(mesh.mesh.pos.voxelSize != 1 << settings.highestLOD) {
|
||||||
if(_getNode(.{.wx=mesh.mesh.pos.wx, .wy=mesh.mesh.pos.wy, .wz=mesh.mesh.pos.wz, .voxelSize=2*mesh.mesh.pos.voxelSize})) |parent| {
|
if(getNodeFromRenderThread(.{.wx=mesh.mesh.pos.wx, .wy=mesh.mesh.pos.wy, .wz=mesh.mesh.pos.wz, .voxelSize=2*mesh.mesh.pos.voxelSize})) |parent| {
|
||||||
const octantIndex: u3 = @intCast((mesh.mesh.pos.wx>>sizeShift & 1) | (mesh.mesh.pos.wy>>sizeShift & 1)<<1 | (mesh.mesh.pos.wz>>sizeShift & 1)<<2);
|
const octantIndex: u3 = @intCast((mesh.mesh.pos.wx>>sizeShift & 1) | (mesh.mesh.pos.wy>>sizeShift & 1)<<1 | (mesh.mesh.pos.wz>>sizeShift & 1)<<2);
|
||||||
parent.mesh.visibilityMask |= @as(u8, 1) << octantIndex;
|
parent.mesh.visibilityMask |= @as(u8, 1) << octantIndex;
|
||||||
}
|
}
|
||||||
@ -1134,6 +1142,232 @@ pub const RenderStructure = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Does occlusion using a breadth-first search that caches an on-screen visibility rectangle.
|
||||||
|
|
||||||
|
const OcclusionData = struct {
|
||||||
|
node: *ChunkMeshNode,
|
||||||
|
distance: f64,
|
||||||
|
|
||||||
|
pub fn compare(_: void, a: @This(), b: @This()) std.math.Order {
|
||||||
|
if(a.distance < b.distance) return .lt;
|
||||||
|
if(a.distance > b.distance) return .gt;
|
||||||
|
return .eq;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var floodFillList = std.PriorityQueue(OcclusionData, void, OcclusionData.compare).init(main.threadAllocator, {});
|
||||||
|
defer floodFillList.deinit();
|
||||||
|
{
|
||||||
|
var firstPos = chunk.ChunkPosition{
|
||||||
|
.wx = @intFromFloat(@floor(playerPos[0])),
|
||||||
|
.wy = @intFromFloat(@floor(playerPos[1])),
|
||||||
|
.wz = @intFromFloat(@floor(playerPos[2])),
|
||||||
|
.voxelSize = 1,
|
||||||
|
};
|
||||||
|
firstPos.wx &= ~@as(i32, chunk.chunkMask);
|
||||||
|
firstPos.wy &= ~@as(i32, chunk.chunkMask);
|
||||||
|
firstPos.wz &= ~@as(i32, chunk.chunkMask);
|
||||||
|
var lod: u3 = 0;
|
||||||
|
while(lod <= settings.highestLOD) : (lod += 1) {
|
||||||
|
if(getNodeFromRenderThread(firstPos)) |node| if(node.mesh.generated) {
|
||||||
|
node.lod = lod;
|
||||||
|
node.min = @splat(-1);
|
||||||
|
node.max = @splat(1);
|
||||||
|
node.active = true;
|
||||||
|
try floodFillList.add(.{
|
||||||
|
.node = node,
|
||||||
|
.distance = 0,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
firstPos.wx &= ~@as(i32, firstPos.voxelSize*chunk.chunkSize);
|
||||||
|
firstPos.wy &= ~@as(i32, firstPos.voxelSize*chunk.chunkSize);
|
||||||
|
firstPos.wz &= ~@as(i32, firstPos.voxelSize*chunk.chunkSize);
|
||||||
|
firstPos.voxelSize *= 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const projRotMat = game.projectionMatrix.mul(game.camera.viewMatrix);
|
||||||
|
while(floodFillList.removeOrNull()) |data| {
|
||||||
|
data.node.active = false;
|
||||||
|
const mesh = &data.node.mesh;
|
||||||
|
try meshList.append(mesh);
|
||||||
|
const relPos: Vec3d = vec.floatFromInt(f64, Vec3i{mesh.pos.wx, mesh.pos.wy, mesh.pos.wz}) - playerPos;
|
||||||
|
const relPosFloat: Vec3f = vec.floatCast(f32, relPos);
|
||||||
|
for(chunk.Neighbors.iterable) |neighbor| continueNeighborLoop: {
|
||||||
|
const component = chunk.Neighbors.extractDirectionComponent(neighbor, relPos);
|
||||||
|
if(chunk.Neighbors.isPositive[neighbor] and component + @as(f64, @floatFromInt(chunk.chunkSize*mesh.pos.voxelSize)) <= 0) continue;
|
||||||
|
if(!chunk.Neighbors.isPositive[neighbor] and component >= 0) continue;
|
||||||
|
if(@reduce(.Or, @min(mesh.chunkBorders[neighbor].min, mesh.chunkBorders[neighbor].max) != mesh.chunkBorders[neighbor].min)) continue; // There was not a single block in the chunk. TODO: Find a better solution.
|
||||||
|
const minVec = vec.floatFromInt(f32, mesh.chunkBorders[neighbor].min*@as(Vec3i, @splat(mesh.pos.voxelSize)));
|
||||||
|
const maxVec = vec.floatFromInt(f32, mesh.chunkBorders[neighbor].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(chunk.Neighbors.vectorComponent[neighbor]) {
|
||||||
|
.x => {
|
||||||
|
cornerVector[0] = minVec[0];
|
||||||
|
cornerVector[1] = if(a == 0) minVec[1] else maxVec[1];
|
||||||
|
cornerVector[2] = if(b == 0) minVec[2] else maxVec[2];
|
||||||
|
},
|
||||||
|
.y => {
|
||||||
|
cornerVector[1] = minVec[1];
|
||||||
|
cornerVector[0] = if(a == 0) minVec[0] else maxVec[0];
|
||||||
|
cornerVector[2] = if(b == 0) minVec[2] else maxVec[2];
|
||||||
|
},
|
||||||
|
.z => {
|
||||||
|
cornerVector[2] = minVec[2];
|
||||||
|
cornerVector[0] = if(a == 0) minVec[0] else maxVec[0];
|
||||||
|
cornerVector[1] = if(b == 0) minVec[1] else maxVec[1];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
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 + chunk.Neighbors.relX[neighbor]*chunk.chunkSize*mesh.pos.voxelSize,
|
||||||
|
.wy = mesh.pos.wy + chunk.Neighbors.relY[neighbor]*chunk.chunkSize*mesh.pos.voxelSize,
|
||||||
|
.wz = mesh.pos.wz + chunk.Neighbors.relZ[neighbor]*chunk.chunkSize*mesh.pos.voxelSize,
|
||||||
|
.voxelSize = mesh.pos.voxelSize,
|
||||||
|
};
|
||||||
|
var lod: u3 = data.node.lod;
|
||||||
|
while(lod <= settings.highestLOD) : (lod += 1) {
|
||||||
|
if(getNodeFromRenderThread(neighborPos)) |node| if(node.mesh.generated) {
|
||||||
|
if(node.active) {
|
||||||
|
node.min = @min(node.min, min);
|
||||||
|
node.max = @max(node.max, max);
|
||||||
|
} else {
|
||||||
|
node.lod = lod;
|
||||||
|
node.min = min;
|
||||||
|
node.max = max;
|
||||||
|
node.active = true;
|
||||||
|
try floodFillList.add(.{
|
||||||
|
.node = node,
|
||||||
|
.distance = node.mesh.pos.getMaxDistanceSquared(playerPos)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break :continueNeighborLoop;
|
||||||
|
};
|
||||||
|
neighborPos.wx &= ~@as(i32, neighborPos.voxelSize*chunk.chunkSize);
|
||||||
|
neighborPos.wy &= ~@as(i32, neighborPos.voxelSize*chunk.chunkSize);
|
||||||
|
neighborPos.wz &= ~@as(i32, neighborPos.voxelSize*chunk.chunkSize);
|
||||||
|
neighborPos.voxelSize *= 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var i: usize = 0;
|
var i: usize = 0;
|
||||||
while(i < clearList.items.len) {
|
while(i < clearList.items.len) {
|
||||||
const mesh = clearList.items[i];
|
const mesh = clearList.items[i];
|
||||||
|
@ -10,10 +10,18 @@ pub const Vec4i = @Vector(4, i32);
|
|||||||
pub const Vec4f = @Vector(4, f32);
|
pub const Vec4f = @Vector(4, f32);
|
||||||
pub const Vec4d = @Vector(4, f64);
|
pub const Vec4d = @Vector(4, f64);
|
||||||
|
|
||||||
|
pub inline fn combine(pos: Vec3f, w: f32) Vec4f {
|
||||||
|
return .{pos[0], pos[1], pos[2], w};
|
||||||
|
}
|
||||||
|
|
||||||
pub fn xyz(self: anytype) @Vector(3, @typeInfo(@TypeOf(self)).Vector.child) {
|
pub fn xyz(self: anytype) @Vector(3, @typeInfo(@TypeOf(self)).Vector.child) {
|
||||||
return @Vector(3, @typeInfo(@TypeOf(self)).Vector.child){self[0], self[1], self[2]};
|
return @Vector(3, @typeInfo(@TypeOf(self)).Vector.child){self[0], self[1], self[2]};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn xy(self: anytype) @Vector(2, @typeInfo(@TypeOf(self)).Vector.child) {
|
||||||
|
return @Vector(2, @typeInfo(@TypeOf(self)).Vector.child){self[0], self[1]};
|
||||||
|
}
|
||||||
|
|
||||||
pub fn dot(self: anytype, other: @TypeOf(self)) @typeInfo(@TypeOf(self)).Vector.child {
|
pub fn dot(self: anytype, other: @TypeOf(self)) @typeInfo(@TypeOf(self)).Vector.child {
|
||||||
return @reduce(.Add, self*other);
|
return @reduce(.Add, self*other);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user