mirror of
https://github.com/PixelGuys/Cubyz.git
synced 2025-08-03 19:28:49 -04:00
1645 lines
62 KiB
Zig
1645 lines
62 KiB
Zig
const std = @import("std");
|
|
const Allocator = std.mem.Allocator;
|
|
const Atomic = std.atomic.Atomic;
|
|
|
|
const blocks = @import("blocks.zig");
|
|
const chunk = @import("chunk.zig");
|
|
const entity = @import("entity.zig");
|
|
const graphics = @import("graphics.zig");
|
|
const c = graphics.c;
|
|
const Shader = graphics.Shader;
|
|
const game = @import("game.zig");
|
|
const World = game.World;
|
|
const itemdrop = @import("itemdrop.zig");
|
|
const main = @import("main.zig");
|
|
const Window = main.Window;
|
|
const models = @import("models.zig");
|
|
const network = @import("network.zig");
|
|
const settings = @import("settings.zig");
|
|
const utils = @import("utils.zig");
|
|
const vec = @import("vec.zig");
|
|
const gpu_performance_measuring = main.gui.windowlist.gpu_performance_measuring;
|
|
const Vec2f = vec.Vec2f;
|
|
const Vec3i = vec.Vec3i;
|
|
const Vec3f = vec.Vec3f;
|
|
const Vec3d = vec.Vec3d;
|
|
const Vec4f = vec.Vec4f;
|
|
const Mat4f = vec.Mat4f;
|
|
|
|
/// The number of milliseconds after which no more chunk meshes are created. This allows the game to run smoother on movement.
|
|
const maximumMeshTime = 12;
|
|
pub const zNear = 0.1;
|
|
pub const zFar = 65536.0; // TODO: Fix z-fighting problems.
|
|
|
|
var deferredRenderPassShader: graphics.Shader = undefined;
|
|
var deferredUniforms: struct {
|
|
color: c_int,
|
|
depthTexture: c_int,
|
|
@"fog.color": c_int,
|
|
@"fog.density": c_int,
|
|
tanXY: c_int,
|
|
zNear: c_int,
|
|
zFar: c_int,
|
|
} = undefined;
|
|
var fakeReflectionShader: graphics.Shader = undefined;
|
|
var fakeReflectionUniforms: struct {
|
|
normalVector: c_int,
|
|
upVector: c_int,
|
|
rightVector: c_int,
|
|
frequency: c_int,
|
|
reflectionMapSize: c_int,
|
|
} = undefined;
|
|
|
|
pub var activeFrameBuffer: c_uint = 0;
|
|
|
|
pub const reflectionCubeMapSize = 64;
|
|
var reflectionCubeMap: graphics.CubeMapTexture = undefined;
|
|
|
|
pub fn init() !void {
|
|
deferredRenderPassShader = try Shader.initAndGetUniforms("assets/cubyz/shaders/deferred_render_pass.vs", "assets/cubyz/shaders/deferred_render_pass.fs", &deferredUniforms);
|
|
fakeReflectionShader = try Shader.initAndGetUniforms("assets/cubyz/shaders/fake_reflection.vs", "assets/cubyz/shaders/fake_reflection.fs", &fakeReflectionUniforms);
|
|
worldFrameBuffer.init(true, c.GL_NEAREST, c.GL_CLAMP_TO_EDGE);
|
|
worldFrameBuffer.updateSize(Window.width, Window.height, c.GL_RGB16F);
|
|
try Bloom.init();
|
|
try MeshSelection.init();
|
|
try MenuBackGround.init();
|
|
reflectionCubeMap = graphics.CubeMapTexture.init();
|
|
reflectionCubeMap.generate(reflectionCubeMapSize, reflectionCubeMapSize);
|
|
initReflectionCubeMap();
|
|
}
|
|
|
|
pub fn deinit() void {
|
|
deferredRenderPassShader.deinit();
|
|
fakeReflectionShader.deinit();
|
|
worldFrameBuffer.deinit();
|
|
Bloom.deinit();
|
|
MeshSelection.deinit();
|
|
MenuBackGround.deinit();
|
|
reflectionCubeMap.deinit();
|
|
}
|
|
|
|
fn initReflectionCubeMap() void {
|
|
c.glViewport(0, 0, reflectionCubeMapSize, reflectionCubeMapSize);
|
|
var framebuffer: graphics.FrameBuffer = undefined;
|
|
framebuffer.init(false, c.GL_LINEAR, c.GL_CLAMP_TO_EDGE);
|
|
defer framebuffer.deinit();
|
|
framebuffer.bind();
|
|
fakeReflectionShader.bind();
|
|
c.glUniform1f(fakeReflectionUniforms.frequency, 1);
|
|
c.glUniform1f(fakeReflectionUniforms.reflectionMapSize, reflectionCubeMapSize);
|
|
for(0..6) |face| {
|
|
c.glUniform3fv(fakeReflectionUniforms.normalVector, 1, @ptrCast(&graphics.CubeMapTexture.faceNormal(face)));
|
|
c.glUniform3fv(fakeReflectionUniforms.upVector, 1, @ptrCast(&graphics.CubeMapTexture.faceUp(face)));
|
|
c.glUniform3fv(fakeReflectionUniforms.rightVector, 1, @ptrCast(&graphics.CubeMapTexture.faceRight(face)));
|
|
reflectionCubeMap.bindToFramebuffer(framebuffer, @intCast(face));
|
|
c.glBindVertexArray(graphics.draw.rectVAO);
|
|
c.glDisable(c.GL_DEPTH_TEST);
|
|
c.glDisable(c.GL_CULL_FACE);
|
|
c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4);
|
|
}
|
|
}
|
|
|
|
var worldFrameBuffer: graphics.FrameBuffer = undefined;
|
|
|
|
var lastWidth: u31 = 0;
|
|
var lastHeight: u31 = 0;
|
|
var lastFov: f32 = 0;
|
|
pub fn updateViewport(width: u31, height: u31, fov: f32) void {
|
|
lastWidth = width;
|
|
lastHeight = height;
|
|
lastFov = fov;
|
|
c.glViewport(0, 0, width, height);
|
|
game.projectionMatrix = Mat4f.perspective(std.math.degreesToRadians(f32, fov), @as(f32, @floatFromInt(width))/@as(f32, @floatFromInt(height)), zNear, zFar);
|
|
worldFrameBuffer.updateSize(width, height, c.GL_RGB16F);
|
|
worldFrameBuffer.unbind();
|
|
}
|
|
|
|
pub fn render(playerPosition: Vec3d) !void {
|
|
const startTime = std.time.milliTimestamp();
|
|
// TODO:
|
|
// if (Cubyz.player != null) {
|
|
// if (Cubyz.playerInc.x != 0 || Cubyz.playerInc.z != 0) { // while walking
|
|
// if (bobbingUp) {
|
|
// playerBobbing += 0.005f;
|
|
// if (playerBobbing >= 0.05f) {
|
|
// bobbingUp = false;
|
|
// }
|
|
// } else {
|
|
// playerBobbing -= 0.005f;
|
|
// if (playerBobbing <= -0.05f) {
|
|
// bobbingUp = true;
|
|
// }
|
|
// }
|
|
// }
|
|
// if (Cubyz.playerInc.y != 0) {
|
|
// Cubyz.player.vy = Cubyz.playerInc.y;
|
|
// }
|
|
// if (Cubyz.playerInc.x != 0) {
|
|
// Cubyz.player.vx = Cubyz.playerInc.x;
|
|
// }
|
|
// playerPosition.y += Player.cameraHeight + playerBobbing;
|
|
// }
|
|
if(game.world) |world| {
|
|
// // TODO: Handle colors and sun position in the world.
|
|
var ambient: Vec3f = undefined;
|
|
ambient[0] = @max(0.1, world.ambientLight);
|
|
ambient[1] = @max(0.1, world.ambientLight);
|
|
ambient[2] = @max(0.1, world.ambientLight);
|
|
const skyColor = vec.xyz(world.clearColor);
|
|
game.fog.color = skyColor;
|
|
|
|
try renderWorld(world, ambient, skyColor, playerPosition);
|
|
try RenderStructure.updateMeshes(startTime + maximumMeshTime);
|
|
} else {
|
|
// TODO:
|
|
// clearColor.y = clearColor.z = 0.7f;
|
|
// clearColor.x = 0.1f;@import("main.zig")
|
|
//
|
|
// Window.setClearColor(clearColor);
|
|
MenuBackGround.render();
|
|
}
|
|
}
|
|
|
|
pub fn renderWorld(world: *World, ambientLight: Vec3f, skyColor: Vec3f, playerPos: Vec3d) !void {
|
|
worldFrameBuffer.bind();
|
|
gpu_performance_measuring.startQuery(.clear);
|
|
worldFrameBuffer.clear(Vec4f{skyColor[0], skyColor[1], skyColor[2], 1});
|
|
gpu_performance_measuring.stopQuery();
|
|
game.camera.updateViewMatrix();
|
|
|
|
// 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));
|
|
|
|
gpu_performance_measuring.startQuery(.animation);
|
|
blocks.meshes.preProcessAnimationData(time);
|
|
gpu_performance_measuring.stopQuery();
|
|
|
|
|
|
// Update the uniforms. The uniforms are needed to render the replacement meshes.
|
|
chunk.meshing.bindShaderAndUniforms(game.projectionMatrix, ambientLight);
|
|
|
|
reflectionCubeMap.bindTo(2);
|
|
c.glActiveTexture(c.GL_TEXTURE0);
|
|
blocks.meshes.blockTextureArray.bind();
|
|
c.glActiveTexture(c.GL_TEXTURE1);
|
|
blocks.meshes.emissionTextureArray.bind();
|
|
|
|
// SimpleList<NormalChunkMesh> visibleChunks = new SimpleList<NormalChunkMesh>(new NormalChunkMesh[64]);
|
|
// SimpleList<ReducedChunkMesh> visibleReduced = new SimpleList<ReducedChunkMesh>(new ReducedChunkMesh[64]);
|
|
|
|
chunk.meshing.quadsDrawn = 0;
|
|
chunk.meshing.transparentQuadsDrawn = 0;
|
|
const meshes = try RenderStructure.updateAndGetRenderChunks(world.conn, playerPos, settings.renderDistance);
|
|
|
|
// for (ChunkMesh mesh : Cubyz.chunkTree.getRenderChunks(frustumInt, x0, y0, z0)) {
|
|
// if (mesh instanceof NormalChunkMesh) {
|
|
// visibleChunks.add((NormalChunkMesh)mesh);
|
|
//
|
|
// mesh.render(playerPosition);
|
|
// } else if (mesh instanceof ReducedChunkMesh) {
|
|
// visibleReduced.add((ReducedChunkMesh)mesh);
|
|
// }
|
|
// }
|
|
gpu_performance_measuring.startQuery(.chunk_rendering);
|
|
MeshSelection.select(playerPos, game.camera.direction);
|
|
MeshSelection.render(game.projectionMatrix, game.camera.viewMatrix, playerPos);
|
|
|
|
try chunk.meshing.beginRender();
|
|
chunk.meshing.bindShaderAndUniforms(game.projectionMatrix, ambientLight);
|
|
|
|
for(meshes) |mesh| {
|
|
mesh.render(playerPos);
|
|
}
|
|
gpu_performance_measuring.stopQuery();
|
|
|
|
gpu_performance_measuring.startQuery(.voxel_model_rendering);
|
|
chunk.meshing.bindVoxelShaderAndUniforms(game.projectionMatrix, ambientLight);
|
|
|
|
for(meshes) |mesh| {
|
|
mesh.renderVoxelModels(playerPos);
|
|
}
|
|
gpu_performance_measuring.stopQuery();
|
|
|
|
// for(int i = 0; i < visibleReduced.size; i++) {
|
|
// ReducedChunkMesh mesh = visibleReduced.array[i];
|
|
// mesh.render(playerPosition);
|
|
// }
|
|
|
|
gpu_performance_measuring.startQuery(.entity_rendering);
|
|
entity.ClientEntityManager.render(game.projectionMatrix, ambientLight, .{1, 0.5, 0.25}, playerPos);
|
|
|
|
try itemdrop.ItemDropRenderer.renderItemDrops(game.projectionMatrix, ambientLight, playerPos, time);
|
|
gpu_performance_measuring.stopQuery();
|
|
|
|
// Render transparent chunk meshes:
|
|
worldFrameBuffer.bindDepthTexture(c.GL_TEXTURE3);
|
|
|
|
gpu_performance_measuring.startQuery(.transparent_rendering);
|
|
c.glTextureBarrier();
|
|
chunk.meshing.bindTransparentShaderAndUniforms(game.projectionMatrix, ambientLight);
|
|
|
|
c.glBlendEquation(c.GL_FUNC_ADD);
|
|
c.glBlendFunc(c.GL_ONE, c.GL_SRC1_COLOR);
|
|
c.glDepthFunc(c.GL_LEQUAL);
|
|
c.glDepthMask(c.GL_FALSE);
|
|
{
|
|
var i: usize = meshes.len;
|
|
while(true) {
|
|
if(i == 0) break;
|
|
i -= 1;
|
|
try meshes[i].renderTransparent(playerPos);
|
|
}
|
|
}
|
|
c.glDepthMask(c.GL_TRUE);
|
|
c.glDepthFunc(c.GL_LESS);
|
|
c.glBlendFunc(c.GL_SRC_ALPHA, c.GL_ONE_MINUS_SRC_ALPHA);
|
|
chunk.meshing.endRender();
|
|
gpu_performance_measuring.stopQuery();
|
|
// NormalChunkMesh.bindTransparentShader(ambientLight, directionalLight.getDirection(), time);
|
|
|
|
worldFrameBuffer.bindTexture(c.GL_TEXTURE3);
|
|
|
|
// if(selected != null && Blocks.transparent(selected.getBlock())) {
|
|
// BlockBreakingRenderer.render(selected, playerPosition);
|
|
// glActiveTexture(GL_TEXTURE0);
|
|
// Meshes.blockTextureArray.bind();
|
|
// glActiveTexture(GL_TEXTURE1);
|
|
// Meshes.emissionTextureArray.bind();
|
|
// }
|
|
|
|
const playerBlock = RenderStructure.getBlockFromAnyLodFromRenderThread(@intFromFloat(@floor(playerPos[0])), @intFromFloat(@floor(playerPos[1])), @intFromFloat(@floor(playerPos[2])));
|
|
|
|
if(settings.bloom) {
|
|
Bloom.render(lastWidth, lastHeight, playerBlock);
|
|
} else {
|
|
Bloom.bindReplacementImage();
|
|
}
|
|
gpu_performance_measuring.startQuery(.final_copy);
|
|
worldFrameBuffer.bindTexture(c.GL_TEXTURE3);
|
|
worldFrameBuffer.bindDepthTexture(c.GL_TEXTURE4);
|
|
worldFrameBuffer.unbind();
|
|
deferredRenderPassShader.bind();
|
|
c.glUniform1i(deferredUniforms.color, 3);
|
|
c.glUniform1i(deferredUniforms.depthTexture, 4);
|
|
if(playerBlock.typ == 0) {
|
|
c.glUniform3fv(deferredUniforms.@"fog.color", 1, @ptrCast(&game.fog.color));
|
|
c.glUniform1f(deferredUniforms.@"fog.density", game.fog.density);
|
|
} else {
|
|
const fogColor = blocks.meshes.fogColor(playerBlock);
|
|
c.glUniform3f(deferredUniforms.@"fog.color", @as(f32, @floatFromInt(fogColor >> 16 & 255))/255.0, @as(f32, @floatFromInt(fogColor >> 8 & 255))/255.0, @as(f32, @floatFromInt(fogColor >> 0 & 255))/255.0);
|
|
c.glUniform1f(deferredUniforms.@"fog.density", blocks.meshes.fogDensity(playerBlock));
|
|
}
|
|
c.glUniform1f(deferredUniforms.zNear, zNear);
|
|
c.glUniform1f(deferredUniforms.zFar, zFar);
|
|
c.glUniform2f(deferredUniforms.tanXY, 1.0/game.projectionMatrix.rows[0][0], 1.0/game.projectionMatrix.rows[1][1]);
|
|
|
|
c.glBindFramebuffer(c.GL_FRAMEBUFFER, activeFrameBuffer);
|
|
|
|
c.glBindVertexArray(graphics.draw.rectVAO);
|
|
c.glDisable(c.GL_DEPTH_TEST);
|
|
c.glDisable(c.GL_CULL_FACE);
|
|
c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4);
|
|
|
|
c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0);
|
|
|
|
try entity.ClientEntityManager.renderNames(game.projectionMatrix, playerPos);
|
|
gpu_performance_measuring.stopQuery();
|
|
}
|
|
|
|
// private float playerBobbing;
|
|
// private boolean bobbingUp;
|
|
//
|
|
// private Vector3f ambient = new Vector3f();
|
|
// private Vector4f clearColor = new Vector4f(0.1f, 0.7f, 0.7f, 1f);
|
|
// private DirectionalLight light = new DirectionalLight(new Vector3f(1.0f, 1.0f, 1.0f), new Vector3f(0.0f, 1.0f, 0.0f).mul(0.1f));
|
|
|
|
const Bloom = struct {
|
|
var buffer1: graphics.FrameBuffer = undefined;
|
|
var buffer2: graphics.FrameBuffer = undefined;
|
|
var emptyBuffer: graphics.Texture = undefined;
|
|
var width: u31 = std.math.maxInt(u31);
|
|
var height: u31 = std.math.maxInt(u31);
|
|
var firstPassShader: graphics.Shader = undefined;
|
|
var secondPassShader: graphics.Shader = undefined;
|
|
var colorExtractAndDownsampleShader: graphics.Shader = undefined;
|
|
var colorExtractUniforms: struct {
|
|
depthTexture: c_int,
|
|
zNear: c_int,
|
|
zFar: c_int,
|
|
tanXY: c_int,
|
|
@"fog.color": c_int,
|
|
@"fog.density": c_int,
|
|
} = undefined;
|
|
|
|
pub fn init() !void {
|
|
buffer1.init(false, c.GL_LINEAR, c.GL_CLAMP_TO_EDGE);
|
|
buffer2.init(false, c.GL_LINEAR, c.GL_CLAMP_TO_EDGE);
|
|
emptyBuffer = graphics.Texture.init();
|
|
emptyBuffer.generate(graphics.Image.emptyImage);
|
|
firstPassShader = try graphics.Shader.init("assets/cubyz/shaders/bloom/first_pass.vs", "assets/cubyz/shaders/bloom/first_pass.fs");
|
|
secondPassShader = try graphics.Shader.init("assets/cubyz/shaders/bloom/second_pass.vs", "assets/cubyz/shaders/bloom/second_pass.fs");
|
|
colorExtractAndDownsampleShader = try graphics.Shader.initAndGetUniforms("assets/cubyz/shaders/bloom/color_extractor_downsample.vs", "assets/cubyz/shaders/bloom/color_extractor_downsample.fs", &colorExtractUniforms);
|
|
}
|
|
|
|
pub fn deinit() void {
|
|
buffer1.deinit();
|
|
buffer2.deinit();
|
|
firstPassShader.deinit();
|
|
secondPassShader.deinit();
|
|
}
|
|
|
|
fn extractImageDataAndDownsample(playerBlock: blocks.Block) void {
|
|
colorExtractAndDownsampleShader.bind();
|
|
worldFrameBuffer.bindTexture(c.GL_TEXTURE3);
|
|
worldFrameBuffer.bindDepthTexture(c.GL_TEXTURE4);
|
|
buffer1.bind();
|
|
c.glUniform1i(colorExtractUniforms.depthTexture, 4);
|
|
if(playerBlock.typ == 0) {
|
|
c.glUniform3fv(colorExtractUniforms.@"fog.color", 1, @ptrCast(&game.fog.color));
|
|
c.glUniform1f(colorExtractUniforms.@"fog.density", game.fog.density);
|
|
} else {
|
|
const fogColor = blocks.meshes.fogColor(playerBlock);
|
|
c.glUniform3f(colorExtractUniforms.@"fog.color", @as(f32, @floatFromInt(fogColor >> 16 & 255))/255.0, @as(f32, @floatFromInt(fogColor >> 8 & 255))/255.0, @as(f32, @floatFromInt(fogColor >> 0 & 255))/255.0);
|
|
c.glUniform1f(colorExtractUniforms.@"fog.density", blocks.meshes.fogDensity(playerBlock));
|
|
}
|
|
c.glUniform1f(colorExtractUniforms.zNear, zNear);
|
|
c.glUniform1f(colorExtractUniforms.zFar, zFar);
|
|
c.glUniform2f(colorExtractUniforms.tanXY, 1.0/game.projectionMatrix.rows[0][0], 1.0/game.projectionMatrix.rows[1][1]);
|
|
c.glBindVertexArray(graphics.draw.rectVAO);
|
|
c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4);
|
|
}
|
|
|
|
fn firstPass() void {
|
|
firstPassShader.bind();
|
|
buffer1.bindTexture(c.GL_TEXTURE3);
|
|
buffer2.bind();
|
|
c.glBindVertexArray(graphics.draw.rectVAO);
|
|
c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4);
|
|
}
|
|
|
|
fn secondPass() void {
|
|
secondPassShader.bind();
|
|
buffer2.bindTexture(c.GL_TEXTURE3);
|
|
buffer1.bind();
|
|
c.glBindVertexArray(graphics.draw.rectVAO);
|
|
c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4);
|
|
}
|
|
|
|
fn render(currentWidth: u31, currentHeight: u31, playerBlock: blocks.Block) void {
|
|
if(width != currentWidth or height != currentHeight) {
|
|
width = currentWidth;
|
|
height = currentHeight;
|
|
buffer1.updateSize(width/4, height/4, c.GL_RGB16F);
|
|
std.debug.assert(buffer1.validate());
|
|
buffer2.updateSize(width/4, height/4, c.GL_RGB16F);
|
|
std.debug.assert(buffer2.validate());
|
|
}
|
|
gpu_performance_measuring.startQuery(.bloom_extract_downsample);
|
|
c.glDisable(c.GL_DEPTH_TEST);
|
|
c.glDisable(c.GL_CULL_FACE);
|
|
c.glDepthMask(c.GL_FALSE);
|
|
|
|
c.glViewport(0, 0, width/4, height/4);
|
|
extractImageDataAndDownsample(playerBlock);
|
|
gpu_performance_measuring.stopQuery();
|
|
gpu_performance_measuring.startQuery(.bloom_first_pass);
|
|
firstPass();
|
|
gpu_performance_measuring.stopQuery();
|
|
gpu_performance_measuring.startQuery(.bloom_second_pass);
|
|
secondPass();
|
|
|
|
c.glViewport(0, 0, width, height);
|
|
buffer1.bindTexture(c.GL_TEXTURE5);
|
|
|
|
c.glDepthMask(c.GL_TRUE);
|
|
c.glEnable(c.GL_DEPTH_TEST);
|
|
c.glEnable(c.GL_CULL_FACE);
|
|
c.glBlendFunc(c.GL_SRC_ALPHA, c.GL_ONE_MINUS_SRC_ALPHA);
|
|
gpu_performance_measuring.stopQuery();
|
|
}
|
|
|
|
fn bindReplacementImage() void {
|
|
emptyBuffer.bindTo(5);
|
|
}
|
|
};
|
|
|
|
pub const MenuBackGround = struct {
|
|
var shader: Shader = undefined;
|
|
var uniforms: struct {
|
|
image: c_int,
|
|
viewMatrix: c_int,
|
|
projectionMatrix: c_int,
|
|
} = undefined;
|
|
|
|
var vao: c_uint = undefined;
|
|
var vbos: [2]c_uint = undefined;
|
|
var texture: graphics.Texture = undefined;
|
|
|
|
var angle: f32 = 0;
|
|
var lastTime: i128 = undefined;
|
|
|
|
fn init() !void {
|
|
lastTime = std.time.nanoTimestamp();
|
|
shader = try Shader.initAndGetUniforms("assets/cubyz/shaders/background/vertex.vs", "assets/cubyz/shaders/background/fragment.fs", &uniforms);
|
|
shader.bind();
|
|
c.glUniform1i(uniforms.image, 0);
|
|
// 4 sides of a simple cube with some panorama texture on it.
|
|
const rawData = [_]f32 {
|
|
-1, -1, -1, 1, 1,
|
|
-1, 1, -1, 1, 0,
|
|
-1, -1, 1, 0.75, 1,
|
|
-1, 1, 1, 0.75, 0,
|
|
1, -1, 1, 0.5, 1,
|
|
1, 1, 1, 0.5, 0,
|
|
1, -1, -1, 0.25, 1,
|
|
1, 1, -1, 0.25, 0,
|
|
-1, -1, -1, 0, 1,
|
|
-1, 1, -1, 0, 0,
|
|
};
|
|
|
|
const indices = [_]c_int {
|
|
0, 1, 2,
|
|
2, 3, 1,
|
|
2, 3, 4,
|
|
4, 5, 3,
|
|
4, 5, 6,
|
|
6, 7, 5,
|
|
6, 7, 8,
|
|
8, 9, 7,
|
|
};
|
|
|
|
c.glGenVertexArrays(1, &vao);
|
|
c.glBindVertexArray(vao);
|
|
c.glGenBuffers(2, &vbos);
|
|
c.glBindBuffer(c.GL_ARRAY_BUFFER, vbos[0]);
|
|
c.glBufferData(c.GL_ARRAY_BUFFER, @intCast(rawData.len*@sizeOf(f32)), &rawData, c.GL_STATIC_DRAW);
|
|
c.glVertexAttribPointer(0, 3, c.GL_FLOAT, c.GL_FALSE, 5*@sizeOf(f32), null);
|
|
c.glVertexAttribPointer(1, 2, c.GL_FLOAT, c.GL_FALSE, 5*@sizeOf(f32), @ptrFromInt(3*@sizeOf(f32)));
|
|
c.glEnableVertexAttribArray(0);
|
|
c.glEnableVertexAttribArray(1);
|
|
c.glBindBuffer(c.GL_ELEMENT_ARRAY_BUFFER, vbos[1]);
|
|
c.glBufferData(c.GL_ELEMENT_ARRAY_BUFFER, @intCast(indices.len*@sizeOf(c_int)), &indices, c.GL_STATIC_DRAW);
|
|
|
|
// Load a random texture from the backgrounds folder. The player may make their own pictures which can be chosen as well.
|
|
var dir: std.fs.IterableDir = try std.fs.cwd().makeOpenPathIterable("assets/backgrounds", .{});
|
|
defer dir.close();
|
|
|
|
var walker = try dir.walk(main.globalAllocator);
|
|
defer walker.deinit();
|
|
var fileList = std.ArrayList([]const u8).init(main.globalAllocator);
|
|
defer {
|
|
for(fileList.items) |fileName| {
|
|
main.globalAllocator.free(fileName);
|
|
}
|
|
fileList.deinit();
|
|
}
|
|
|
|
while(try walker.next()) |entry| {
|
|
if(entry.kind == .file and std.ascii.endsWithIgnoreCase(entry.basename, ".png")) {
|
|
try fileList.append(try main.globalAllocator.dupe(u8, entry.path));
|
|
}
|
|
}
|
|
if(fileList.items.len == 0) {
|
|
std.log.warn("Couldn't find any background scene images in \"assets/backgrounds\".", .{});
|
|
texture = .{.textureID = 0};
|
|
return;
|
|
}
|
|
const theChosenOne = main.random.nextIntBounded(u32, &main.seed, @as(u32, @intCast(fileList.items.len)));
|
|
const theChosenPath = try std.fmt.allocPrint(main.stackAllocator, "assets/backgrounds/{s}", .{fileList.items[theChosenOne]});
|
|
defer main.stackAllocator.free(theChosenPath);
|
|
texture = try graphics.Texture.initFromFile(theChosenPath);
|
|
}
|
|
|
|
pub fn deinit() void {
|
|
shader.deinit();
|
|
c.glDeleteVertexArrays(1, &vao);
|
|
c.glDeleteBuffers(2, &vbos);
|
|
}
|
|
|
|
pub fn hasImage() bool {
|
|
return texture.textureID != 0;
|
|
}
|
|
|
|
pub fn render() void {
|
|
if(texture.textureID == 0) return;
|
|
c.glDisable(c.GL_CULL_FACE); // I'm not sure if my triangles are rotated correctly, and there are no triangles facing away from the player anyways.
|
|
|
|
// Use a simple rotation around the y axis, with a steadily increasing angle.
|
|
const newTime = std.time.nanoTimestamp();
|
|
angle += @as(f32, @floatFromInt(newTime - lastTime))/2e10;
|
|
lastTime = newTime;
|
|
const viewMatrix = Mat4f.rotationY(angle);
|
|
shader.bind();
|
|
|
|
c.glUniformMatrix4fv(uniforms.viewMatrix, 1, c.GL_TRUE, @ptrCast(&viewMatrix));
|
|
c.glUniformMatrix4fv(uniforms.projectionMatrix, 1, c.GL_TRUE, @ptrCast(&game.projectionMatrix));
|
|
|
|
texture.bindTo(0);
|
|
|
|
c.glBindVertexArray(vao);
|
|
c.glDrawElements(c.GL_TRIANGLES, 24, c.GL_UNSIGNED_INT, null);
|
|
}
|
|
|
|
pub fn takeBackgroundImage() !void {
|
|
const size: usize = 1024; // Use a power of 2 here, to reduce video memory waste.
|
|
const pixels: []u32 = try main.stackAllocator.alloc(u32, size*size);
|
|
defer main.stackAllocator.free(pixels);
|
|
|
|
// Change the viewport and the matrices to render 4 cube faces:
|
|
|
|
updateViewport(size, size, 90.0);
|
|
defer updateViewport(Window.width, Window.height, settings.fov);
|
|
|
|
var buffer: graphics.FrameBuffer = undefined;
|
|
buffer.init(true, c.GL_NEAREST, c.GL_REPEAT);
|
|
defer buffer.deinit();
|
|
buffer.updateSize(size, size, c.GL_RGBA8);
|
|
|
|
activeFrameBuffer = buffer.frameBuffer;
|
|
defer activeFrameBuffer = 0;
|
|
|
|
const oldRotation = game.camera.rotation;
|
|
defer game.camera.rotation = oldRotation;
|
|
|
|
const angles = [_]f32 {std.math.pi/2.0, std.math.pi, std.math.pi*3/2.0, std.math.pi*2};
|
|
|
|
// All 4 sides are stored in a single image.
|
|
const image = try graphics.Image.init(main.stackAllocator, 4*size, size);
|
|
defer image.deinit(main.stackAllocator);
|
|
|
|
for(0..4) |i| {
|
|
c.glEnable(c.GL_CULL_FACE);
|
|
c.glEnable(c.GL_DEPTH_TEST);
|
|
game.camera.rotation = .{0, angles[i], 0};
|
|
// Draw to frame buffer.
|
|
buffer.bind();
|
|
c.glClear(c.GL_DEPTH_BUFFER_BIT | c.GL_STENCIL_BUFFER_BIT | c.GL_COLOR_BUFFER_BIT);
|
|
try main.renderer.render(game.Player.getPosBlocking());
|
|
// Copy the pixels directly from OpenGL
|
|
buffer.bind();
|
|
c.glReadPixels(0, 0, size, size, c.GL_RGBA, c.GL_UNSIGNED_BYTE, pixels.ptr);
|
|
|
|
for(0..size) |y| {
|
|
for(0..size) |x| {
|
|
const index = x + y*size;
|
|
// Needs to flip the image in y-direction.
|
|
image.setRGB(x + size*i, size - 1 - y, @bitCast(pixels[index]));
|
|
}
|
|
}
|
|
}
|
|
c.glDisable(c.GL_CULL_FACE);
|
|
c.glDisable(c.GL_DEPTH_TEST);
|
|
c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0);
|
|
|
|
const fileName = try std.fmt.allocPrint(main.stackAllocator, "assets/backgrounds/{s}_{}.png", .{game.world.?.name, game.world.?.gameTime.load(.Monotonic)});
|
|
defer main.stackAllocator.free(fileName);
|
|
try image.exportToFile(fileName);
|
|
// TODO: Performance is terrible even with -O3. Consider using qoi instead.
|
|
}
|
|
};
|
|
|
|
pub const Frustum = struct {
|
|
const Plane = struct {
|
|
pos: Vec3f,
|
|
norm: Vec3f,
|
|
};
|
|
planes: [4]Plane, // Who cares about the near/far plane anyways?
|
|
|
|
pub fn init(cameraPos: Vec3f, rotationMatrix: Mat4f, fovY: f32, width: u31, height: u31) Frustum {
|
|
const invRotationMatrix = rotationMatrix.transpose();
|
|
const cameraDir = vec.xyz(invRotationMatrix.mulVec(Vec4f{0, 0, 1, 1}));
|
|
const cameraUp = vec.xyz(invRotationMatrix.mulVec(Vec4f{0, 1, 0, 1}));
|
|
const cameraRight = vec.xyz(invRotationMatrix.mulVec(Vec4f{1, 0, 0, 1}));
|
|
|
|
const halfVSide = std.math.tan(std.math.degreesToRadians(f32, fovY)*0.5);
|
|
const halfHSide = halfVSide*@as(f32, @floatFromInt(width))/@as(f32, @floatFromInt(height));
|
|
|
|
var self: Frustum = undefined;
|
|
self.planes[0] = Plane{.pos = cameraPos, .norm=vec.cross(cameraUp, cameraDir + cameraRight*@as(Vec3f, @splat(halfHSide)))}; // right
|
|
self.planes[1] = Plane{.pos = cameraPos, .norm=vec.cross(cameraDir - cameraRight*@as(Vec3f, @splat(halfHSide)), cameraUp)}; // left
|
|
self.planes[2] = Plane{.pos = cameraPos, .norm=vec.cross(cameraRight, cameraDir - cameraUp*@as(Vec3f, @splat(halfVSide)))}; // top
|
|
self.planes[3] = Plane{.pos = cameraPos, .norm=vec.cross(cameraDir + cameraUp*@as(Vec3f, @splat(halfVSide)), cameraRight)}; // bottom
|
|
return self;
|
|
}
|
|
|
|
pub fn testAAB(self: Frustum, pos: Vec3f, dim: Vec3f) bool {
|
|
inline for(self.planes) |plane| {
|
|
var dist: f32 = vec.dot(pos - plane.pos, plane.norm);
|
|
// Find the most positive corner:
|
|
dist += @reduce(.Add, @max(Vec3f{0, 0, 0}, dim*plane.norm));
|
|
if(dist < 0) return false;
|
|
}
|
|
return true;
|
|
}
|
|
};
|
|
|
|
pub const MeshSelection = struct {
|
|
var shader: Shader = undefined;
|
|
var uniforms: struct {
|
|
projectionMatrix: c_int,
|
|
viewMatrix: c_int,
|
|
modelPosition: c_int,
|
|
lowerBounds: c_int,
|
|
upperBounds: c_int,
|
|
} = undefined;
|
|
|
|
var cubeVAO: c_uint = undefined;
|
|
var cubeVBO: c_uint = undefined;
|
|
var cubeIBO: c_uint = undefined;
|
|
|
|
pub fn init() !void {
|
|
shader = try Shader.initAndGetUniforms("assets/cubyz/shaders/block_selection_vertex.vs", "assets/cubyz/shaders/block_selection_fragment.fs", &uniforms);
|
|
|
|
const rawData = [_]f32 {
|
|
0, 0, 0,
|
|
0, 0, 1,
|
|
0, 1, 0,
|
|
0, 1, 1,
|
|
1, 0, 0,
|
|
1, 0, 1,
|
|
1, 1, 0,
|
|
1, 1, 1,
|
|
};
|
|
const indices = [_]u8 {
|
|
0, 1,
|
|
0, 2,
|
|
0, 4,
|
|
1, 3,
|
|
1, 5,
|
|
2, 3,
|
|
2, 6,
|
|
3, 7,
|
|
4, 5,
|
|
4, 6,
|
|
5, 7,
|
|
6, 7,
|
|
};
|
|
|
|
c.glGenVertexArrays(1, &cubeVAO);
|
|
c.glBindVertexArray(cubeVAO);
|
|
c.glGenBuffers(1, &cubeVBO);
|
|
c.glBindBuffer(c.GL_ARRAY_BUFFER, cubeVBO);
|
|
c.glBufferData(c.GL_ARRAY_BUFFER, rawData.len*@sizeOf(f32), &rawData, c.GL_STATIC_DRAW);
|
|
c.glVertexAttribPointer(0, 3, c.GL_FLOAT, c.GL_FALSE, 3*@sizeOf(f32), null);
|
|
c.glEnableVertexAttribArray(0);
|
|
c.glGenBuffers(1, &cubeIBO);
|
|
c.glBindBuffer(c.GL_ELEMENT_ARRAY_BUFFER, cubeIBO);
|
|
c.glBufferData(c.GL_ELEMENT_ARRAY_BUFFER, indices.len*@sizeOf(u8), &indices, c.GL_STATIC_DRAW);
|
|
|
|
}
|
|
|
|
pub fn deinit() void {
|
|
shader.deinit();
|
|
c.glDeleteBuffers(1, &cubeIBO);
|
|
c.glDeleteBuffers(1, &cubeVBO);
|
|
c.glDeleteVertexArrays(1, &cubeVAO);
|
|
}
|
|
|
|
var posBeforeBlock: Vec3i = undefined;
|
|
var selectedBlockPos: ?Vec3i = null;
|
|
var lastPos: Vec3d = undefined;
|
|
var lastDir: Vec3f = undefined;
|
|
pub fn select(_pos: Vec3d, _dir: Vec3f) void {
|
|
var pos = _pos;
|
|
_ = &pos;// TODO: pos.y += Player.cameraHeight;
|
|
lastPos = pos;
|
|
const dir: Vec3d = @floatCast(_dir);
|
|
lastDir = _dir;
|
|
|
|
// Test blocks:
|
|
const closestDistance: f64 = 6.0; // selection now limited
|
|
// Implementation of "A Fast Voxel Traversal Algorithm for Ray Tracing" http://www.cse.yorku.ca/~amana/research/grid.pdf
|
|
const step: Vec3i = @intFromFloat(std.math.sign(dir));
|
|
const invDir = @as(Vec3d, @splat(1))/dir;
|
|
const tDelta = @abs(invDir);
|
|
var tMax = (@floor(pos) - pos)*invDir;
|
|
tMax = @max(tMax, tMax + tDelta*@as(Vec3f, @floatFromInt(step)));
|
|
tMax = @select(f64, dir == @as(Vec3d, @splat(0)), @as(Vec3d, @splat(std.math.inf(f64))), tMax);
|
|
var voxelPos: Vec3i = @intFromFloat(@floor(pos));
|
|
|
|
var total_tMax: f64 = 0;
|
|
|
|
selectedBlockPos = null;
|
|
|
|
while(total_tMax < closestDistance) {
|
|
const block = RenderStructure.getBlockFromRenderThread(voxelPos[0], voxelPos[1], voxelPos[2]) orelse break;
|
|
if(block.typ != 0) {
|
|
// Check the true bounding box (using this algorithm here: https://tavianator.com/2011/ray_box.html):
|
|
const model = blocks.meshes.model(block);
|
|
const voxelModel = &models.voxelModels.items[model.modelIndex];
|
|
const transformedMin = model.permutation.transform(voxelModel.min - @as(Vec3i, @splat(8))) + @as(Vec3i, @splat(8));
|
|
const transformedMax = model.permutation.transform(voxelModel.max - @as(Vec3i, @splat(8))) + @as(Vec3i, @splat(8));
|
|
const min: Vec3d = @floatFromInt(@min(transformedMin, transformedMax));
|
|
const max: Vec3d = @floatFromInt(@max(transformedMin ,transformedMax));
|
|
const voxelPosFloat: Vec3d = @floatFromInt(voxelPos);
|
|
const t1 = (voxelPosFloat + min/@as(Vec3d, @splat(16.0)) - pos)*invDir;
|
|
const t2 = (voxelPosFloat + max/@as(Vec3d, @splat(16.0)) - pos)*invDir;
|
|
const boxTMin = @reduce(.Max, @min(t1, t2));
|
|
const boxTMax = @reduce(.Min, @max(t1, t2));
|
|
if(boxTMin <= boxTMax and boxTMin <= closestDistance and boxTMax > 0) {
|
|
selectedBlockPos = voxelPos;
|
|
break;
|
|
}
|
|
}
|
|
posBeforeBlock = voxelPos;
|
|
if(tMax[0] < tMax[1]) {
|
|
if(tMax[0] < tMax[2]) {
|
|
voxelPos[0] += step[0];
|
|
total_tMax = tMax[0];
|
|
tMax[0] += tDelta[0];
|
|
} else {
|
|
voxelPos[2] += step[2];
|
|
total_tMax = tMax[2];
|
|
tMax[2] += tDelta[2];
|
|
}
|
|
} else {
|
|
if(tMax[1] < tMax[2]) {
|
|
voxelPos[1] += step[1];
|
|
total_tMax = tMax[1];
|
|
tMax[1] += tDelta[1];
|
|
} else {
|
|
voxelPos[2] += step[2];
|
|
total_tMax = tMax[2];
|
|
tMax[2] += tDelta[2];
|
|
}
|
|
}
|
|
}
|
|
// TODO: Test entities
|
|
}
|
|
|
|
pub fn placeBlock(inventoryStack: *main.items.ItemStack) !void {
|
|
if(selectedBlockPos) |selectedPos| {
|
|
var block = RenderStructure.getBlockFromRenderThread(selectedPos[0], selectedPos[1], selectedPos[2]) orelse return;
|
|
if(inventoryStack.item) |item| {
|
|
switch(item) {
|
|
.baseItem => |baseItem| {
|
|
if(baseItem.block) |itemBlock| {
|
|
const rotationMode = blocks.Block.mode(.{.typ = itemBlock, .data = 0});
|
|
var neighborDir = Vec3i{0, 0, 0};
|
|
// Check if stuff can be added to the block itself:
|
|
if(itemBlock == block.typ) {
|
|
const relPos = lastPos - @as(Vec3d, @floatFromInt(selectedPos));
|
|
if(rotationMode.generateData(main.game.world.?, selectedPos, relPos, lastDir, neighborDir, &block, false)) {
|
|
// TODO: world.updateBlock(bi.x, bi.y, bi.z, block.data); (→ Sending it over the network)
|
|
try RenderStructure.updateBlock(selectedPos[0], selectedPos[1], selectedPos[2], block);
|
|
_ = inventoryStack.add(@as(i32, -1));
|
|
return;
|
|
}
|
|
}
|
|
// Check the block in front of it:
|
|
const neighborPos = posBeforeBlock;
|
|
neighborDir = selectedPos - posBeforeBlock;
|
|
const relPos = lastPos - @as(Vec3d, @floatFromInt(neighborPos));
|
|
block = RenderStructure.getBlockFromRenderThread(neighborPos[0], neighborPos[1], neighborPos[2]) orelse return;
|
|
if(block.typ == itemBlock) {
|
|
if(rotationMode.generateData(main.game.world.?, neighborPos, relPos, lastDir, neighborDir, &block, false)) {
|
|
// TODO: world.updateBlock(bi.x, bi.y, bi.z, block.data); (→ Sending it over the network)
|
|
try RenderStructure.updateBlock(neighborPos[0], neighborPos[1], neighborPos[2], block);
|
|
_ = inventoryStack.add(@as(i32, -1));
|
|
return;
|
|
}
|
|
} else {
|
|
// Check if the block can actually be placed at that point. There might be entities or other blocks in the way.
|
|
if(block.solid()) return;
|
|
// TODO:
|
|
// for(ClientEntity ent : ClientEntityManager.getEntities()) {
|
|
// Vector3d pos = ent.position;
|
|
// // Check if the block is inside:
|
|
// if (neighbor.x < pos.x + ent.width && neighbor.x + 1 > pos.x - ent.width
|
|
// && neighbor.z < pos.z + ent.width && neighbor.z + 1 > pos.z - ent.width
|
|
// && neighbor.y < pos.y + ent.height && neighbor.y + 1 > pos.y)
|
|
// return;
|
|
// }
|
|
block.typ = itemBlock;
|
|
block.data = 0;
|
|
if(rotationMode.generateData(main.game.world.?, neighborPos, relPos, lastDir, neighborDir, &block, true)) {
|
|
// TODO: world.updateBlock(bi.x, bi.y, bi.z, block.data); (→ Sending it over the network)
|
|
try RenderStructure.updateBlock(neighborPos[0], neighborPos[1], neighborPos[2], block);
|
|
_ = inventoryStack.add(@as(i32, -1));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
.tool => |tool| {
|
|
_ = tool; // TODO: Tools might change existing blocks.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn drawCube(projectionMatrix: Mat4f, viewMatrix: Mat4f, relativePositionToPlayer: Vec3d, min: Vec3f, max: Vec3f) void {
|
|
shader.bind();
|
|
|
|
c.glUniformMatrix4fv(uniforms.projectionMatrix, 1, c.GL_TRUE, @ptrCast(&projectionMatrix));
|
|
c.glUniformMatrix4fv(uniforms.viewMatrix, 1, c.GL_TRUE, @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 {
|
|
if(selectedBlockPos) |_selectedBlockPos| {
|
|
c.glEnable(c.GL_POLYGON_OFFSET_LINE);
|
|
defer c.glDisable(c.GL_POLYGON_OFFSET_LINE);
|
|
c.glPolygonOffset(-2, 0);
|
|
const block = RenderStructure.getBlockFromRenderThread(_selectedBlockPos[0], _selectedBlockPos[1], _selectedBlockPos[2]) orelse return;
|
|
const model = blocks.meshes.model(block);
|
|
const voxelModel = &models.voxelModels.items[model.modelIndex];
|
|
const transformedMin = model.permutation.transform(voxelModel.min - @as(Vec3i, @splat(8))) + @as(Vec3i, @splat(8));
|
|
const transformedMax = model.permutation.transform(voxelModel.max - @as(Vec3i, @splat(8))) + @as(Vec3i, @splat(8));
|
|
const min: Vec3f = @floatFromInt(@min(transformedMin, transformedMax));
|
|
const max: Vec3f = @floatFromInt(@max(transformedMin ,transformedMax));
|
|
drawCube(projectionMatrix, viewMatrix, @as(Vec3d, @floatFromInt(_selectedBlockPos)) - playerPos, min/@as(Vec3f, @splat(16.0)), max/@as(Vec3f, @splat(16.0)));
|
|
}
|
|
}
|
|
};
|
|
|
|
pub const RenderStructure = struct {
|
|
const ChunkMeshNode = struct {
|
|
mesh: Atomic(?*chunk.meshing.ChunkMesh),
|
|
lod: u3,
|
|
min: Vec2f,
|
|
max: Vec2f,
|
|
active: bool,
|
|
rendered: bool,
|
|
};
|
|
const storageSize = 32;
|
|
const storageMask = storageSize - 1;
|
|
var storageLists: [settings.highestLOD + 1]*[storageSize*storageSize*storageSize]ChunkMeshNode = undefined;
|
|
var meshList = std.ArrayList(*chunk.meshing.ChunkMesh).init(main.globalAllocator);
|
|
var priorityUpdateList = std.ArrayList(*chunk.meshing.ChunkMesh).init(main.globalAllocator);
|
|
pub var updatableList = std.ArrayList(*chunk.meshing.ChunkMesh).init(main.globalAllocator);
|
|
var clearList = std.ArrayList(*chunk.meshing.ChunkMesh).init(main.globalAllocator);
|
|
var lastPx: i32 = 0;
|
|
var lastPy: i32 = 0;
|
|
var lastPz: i32 = 0;
|
|
var lastRD: i32 = 0;
|
|
var mutex = std.Thread.Mutex{};
|
|
var blockUpdateMutex = std.Thread.Mutex{};
|
|
const BlockUpdate = struct {
|
|
x: i32,
|
|
y: i32,
|
|
z: i32,
|
|
newBlock: blocks.Block,
|
|
};
|
|
var blockUpdateList: std.ArrayList(BlockUpdate) = undefined;
|
|
|
|
pub fn init() !void {
|
|
lastRD = 0;
|
|
blockUpdateList = std.ArrayList(BlockUpdate).init(main.globalAllocator);
|
|
for(&storageLists) |*storageList| {
|
|
storageList.* = try main.globalAllocator.create([storageSize*storageSize*storageSize]ChunkMeshNode);
|
|
for(storageList.*) |*val| {
|
|
val.mesh = Atomic(?*chunk.meshing.ChunkMesh).init(null);
|
|
val.rendered = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn deinit() void {
|
|
freeOldMeshes(0, 0, 0, 0) catch |err| {
|
|
std.log.err("Error while freeing remaining meshes: {s}", .{@errorName(err)});
|
|
};
|
|
for(storageLists) |storageList| {
|
|
main.globalAllocator.destroy(storageList);
|
|
}
|
|
for(updatableList.items) |mesh| {
|
|
mesh.decreaseRefCount() catch unreachable;
|
|
}
|
|
updatableList.deinit();
|
|
for(priorityUpdateList.items) |mesh| {
|
|
mesh.decreaseRefCount() catch unreachable;
|
|
}
|
|
priorityUpdateList.deinit();
|
|
blockUpdateList.deinit();
|
|
meshList.deinit();
|
|
for(clearList.items) |mesh| {
|
|
mesh.deinit();
|
|
main.globalAllocator.destroy(mesh);
|
|
}
|
|
clearList.deinit();
|
|
}
|
|
|
|
fn getNodeFromRenderThread(pos: chunk.ChunkPosition) *ChunkMeshNode {
|
|
const lod = std.math.log2_int(u31, pos.voxelSize);
|
|
var xIndex = pos.wx >> lod+chunk.chunkShift;
|
|
var yIndex = pos.wy >> lod+chunk.chunkShift;
|
|
var zIndex = pos.wz >> lod+chunk.chunkShift;
|
|
xIndex &= storageMask;
|
|
yIndex &= storageMask;
|
|
zIndex &= storageMask;
|
|
const index = (xIndex*storageSize + yIndex)*storageSize + zIndex;
|
|
return &storageLists[lod][@intCast(index)];
|
|
}
|
|
|
|
fn getBlockFromRenderThread(x: i32, y: i32, z: i32) ?blocks.Block {
|
|
const node = RenderStructure.getNodeFromRenderThread(.{.wx = x, .wy = y, .wz = z, .voxelSize=1});
|
|
const mesh = node.mesh.load(.Unordered) orelse return null;
|
|
const block = mesh.chunk.getBlock(x & chunk.chunkMask, y & chunk.chunkMask, z & chunk.chunkMask);
|
|
return block;
|
|
}
|
|
|
|
fn getBlockFromAnyLodFromRenderThread(x: i32, y: i32, z: i32) blocks.Block {
|
|
var lod: u5 = 0;
|
|
while(lod < settings.highestLOD) : (lod += 1) {
|
|
const node = RenderStructure.getNodeFromRenderThread(.{.wx = x, .wy = y, .wz = z, .voxelSize=@as(u31, 1) << lod});
|
|
const mesh = node.mesh.load(.Unordered) orelse continue;
|
|
const block = mesh.chunk.getBlock(x & chunk.chunkMask<<lod, y & chunk.chunkMask<<lod, z & chunk.chunkMask<<lod);
|
|
return block;
|
|
}
|
|
return blocks.Block{.typ = 0, .data = 0};
|
|
}
|
|
|
|
pub fn getMeshFromAnyLodFromRenderThread(wx: i32, wy: i32, wz: i32, voxelSize: u31) ?*chunk.meshing.ChunkMesh {
|
|
var lod: u5 = @ctz(voxelSize);
|
|
while(lod < settings.highestLOD) : (lod += 1) {
|
|
const node = RenderStructure.getNodeFromRenderThread(.{.wx = wx & ~chunk.chunkMask<<lod, .wy = wy & ~chunk.chunkMask<<lod, .wz = wz & ~chunk.chunkMask<<lod, .voxelSize=@as(u31, 1) << lod});
|
|
return node.mesh.load(.Unordered) orelse continue;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
pub fn getNeighborFromRenderThread(_pos: chunk.ChunkPosition, resolution: u31, neighbor: u3) ?*chunk.meshing.ChunkMesh {
|
|
var pos = _pos;
|
|
pos.wx += pos.voxelSize*chunk.chunkSize*chunk.Neighbors.relX[neighbor];
|
|
pos.wy += pos.voxelSize*chunk.chunkSize*chunk.Neighbors.relY[neighbor];
|
|
pos.wz += pos.voxelSize*chunk.chunkSize*chunk.Neighbors.relZ[neighbor];
|
|
pos.voxelSize = resolution;
|
|
const node = getNodeFromRenderThread(pos);
|
|
return node.mesh.load(.Unordered);
|
|
}
|
|
|
|
pub fn getMeshAndIncreaseRefCount(pos: chunk.ChunkPosition) ?*chunk.meshing.ChunkMesh {
|
|
const node = RenderStructure.getNodeFromRenderThread(pos);
|
|
const mesh = node.mesh.load(.Unordered) orelse return null;
|
|
if(mesh.tryIncreaseRefCount()) {
|
|
return mesh;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
fn reduceRenderDistance(fullRenderDistance: i64, reduction: i64) i32 {
|
|
const reducedRenderDistanceSquare: f64 = @floatFromInt(fullRenderDistance*fullRenderDistance - reduction*reduction);
|
|
const reducedRenderDistance: i32 = @intFromFloat(@ceil(@sqrt(@max(0, reducedRenderDistanceSquare))));
|
|
return reducedRenderDistance;
|
|
}
|
|
|
|
fn isInRenderDistance(pos: chunk.ChunkPosition) bool {
|
|
const maxRenderDistance = lastRD*chunk.chunkSize*pos.voxelSize;
|
|
const size: u31 = chunk.chunkSize*pos.voxelSize;
|
|
const mask: i32 = size - 1;
|
|
const invMask: i32 = ~mask;
|
|
|
|
const minX = lastPx-%maxRenderDistance & invMask;
|
|
const maxX = lastPx+%maxRenderDistance+%size & invMask;
|
|
if(pos.wx < minX) return false;
|
|
if(pos.wx >= maxX) return false;
|
|
var deltaX: i64 = @abs(pos.wx +% size/2 -% lastPx);
|
|
deltaX = @max(0, deltaX - size/2);
|
|
|
|
const maxYRenderDistance: i32 = reduceRenderDistance(maxRenderDistance, deltaX);
|
|
const minY = lastPy-%maxYRenderDistance & invMask;
|
|
const maxY = lastPy+%maxYRenderDistance+%size & invMask;
|
|
if(pos.wy < minY) return false;
|
|
if(pos.wy >= maxY) return false;
|
|
var deltaY: i64 = @abs(pos.wy +% size/2 -% lastPy);
|
|
deltaY = @max(0, deltaY - size/2);
|
|
|
|
const maxZRenderDistance: i32 = reduceRenderDistance(maxYRenderDistance, deltaY);
|
|
if(maxZRenderDistance == 0) return false;
|
|
const minZ = lastPz-%maxZRenderDistance & invMask;
|
|
const maxZ = lastPz+%maxZRenderDistance+%size & invMask;
|
|
if(pos.wz < minZ) return false;
|
|
if(pos.wz >= maxZ) return false;
|
|
return true;
|
|
}
|
|
|
|
fn freeOldMeshes(px: i32, py: i32, pz: i32, renderDistance: i32) !void {
|
|
for(0..storageLists.len) |_lod| {
|
|
const lod: u5 = @intCast(_lod);
|
|
const maxRenderDistanceNew = renderDistance*chunk.chunkSize << lod;
|
|
const maxRenderDistanceOld = lastRD*chunk.chunkSize << lod;
|
|
const size: u31 = chunk.chunkSize << lod;
|
|
const mask: i32 = size - 1;
|
|
const invMask: i32 = ~mask;
|
|
|
|
std.debug.assert(@divFloor(2*maxRenderDistanceNew + size-1, size) + 2 <= storageSize);
|
|
|
|
const minX = lastPx-%maxRenderDistanceOld & invMask;
|
|
const maxX = lastPx+%maxRenderDistanceOld+%size & invMask;
|
|
var x = minX;
|
|
while(x != maxX): (x +%= size) {
|
|
const xIndex = @divExact(x, size) & storageMask;
|
|
var deltaXNew: i64 = @abs(x +% size/2 -% px);
|
|
deltaXNew = @max(0, deltaXNew - size/2);
|
|
var deltaXOld: i64 = @abs(x +% size/2 -% lastPx);
|
|
deltaXOld = @max(0, deltaXOld - size/2);
|
|
const maxYRenderDistanceNew: i32 = reduceRenderDistance(maxRenderDistanceNew, deltaXNew);
|
|
const maxYRenderDistanceOld: i32 = reduceRenderDistance(maxRenderDistanceOld, deltaXOld);
|
|
|
|
const minY = lastPy-%maxYRenderDistanceOld & invMask;
|
|
const maxY = lastPy+%maxYRenderDistanceOld+%size & invMask;
|
|
var y = minY;
|
|
while(y != maxY): (y +%= size) {
|
|
const yIndex = @divExact(y, size) & storageMask;
|
|
var deltaYOld: i64 = @abs(y +% size/2 -% lastPy);
|
|
deltaYOld = @max(0, deltaYOld - size/2);
|
|
var deltaYNew: i64 = @abs(y +% size/2 -% py);
|
|
deltaYNew = @max(0, deltaYNew - size/2);
|
|
var maxZRenderDistanceOld: i32 = reduceRenderDistance(maxYRenderDistanceOld, deltaYOld);
|
|
if(maxZRenderDistanceOld == 0) maxZRenderDistanceOld -= size/2;
|
|
var maxZRenderDistanceNew: i32 = reduceRenderDistance(maxYRenderDistanceNew, deltaYNew);
|
|
if(maxZRenderDistanceNew == 0) maxZRenderDistanceNew -= size/2;
|
|
|
|
const minZOld = lastPz-%maxZRenderDistanceOld & invMask;
|
|
const maxZOld = lastPz+%maxZRenderDistanceOld+%size & invMask;
|
|
const minZNew = pz-%maxZRenderDistanceNew & invMask;
|
|
const maxZNew = pz+%maxZRenderDistanceNew+%size & invMask;
|
|
|
|
var zValues: [storageSize]i32 = undefined;
|
|
var zValuesLen: usize = 0;
|
|
if(minZNew -% minZOld > 0) {
|
|
var z = minZOld;
|
|
while(z != minZNew and z != maxZOld): (z +%= size) {
|
|
zValues[zValuesLen] = z;
|
|
zValuesLen += 1;
|
|
}
|
|
}
|
|
if(maxZOld -% maxZNew > 0) {
|
|
var z = minZOld +% @max(0, maxZNew -% minZOld);
|
|
while(z != maxZOld): (z +%= size) {
|
|
zValues[zValuesLen] = z;
|
|
zValuesLen += 1;
|
|
}
|
|
}
|
|
|
|
for(zValues[0..zValuesLen]) |z| {
|
|
const zIndex = @divExact(z, size) & storageMask;
|
|
const index = (xIndex*storageSize + yIndex)*storageSize + zIndex;
|
|
|
|
const node = &storageLists[_lod][@intCast(index)];
|
|
// Update the neighbors, so we don't get cracks when we look back:
|
|
if(node.mesh.load(.Unordered)) |mesh| {
|
|
const pos = mesh.pos;
|
|
try mesh.decreaseRefCount();
|
|
node.mesh.store(null, .Unordered);
|
|
if(renderDistance != 0) {
|
|
for(chunk.Neighbors.iterable) |neighbor| {
|
|
if(getNeighborFromRenderThread(pos, pos.voxelSize, neighbor)) |neighborMesh| {
|
|
neighborMesh.needsNeighborUpdate = true;
|
|
neighborMesh.increaseRefCount();
|
|
try priorityUpdateList.append(neighborMesh);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn createNewMeshes(px: i32, py: i32, pz: i32, renderDistance: i32, meshRequests: *std.ArrayList(chunk.ChunkPosition)) !void {
|
|
for(0..storageLists.len) |_lod| {
|
|
const lod: u5 = @intCast(_lod);
|
|
const maxRenderDistanceNew = renderDistance*chunk.chunkSize << lod;
|
|
const maxRenderDistanceOld = lastRD*chunk.chunkSize << lod;
|
|
const size: u31 = chunk.chunkSize << lod;
|
|
const mask: i32 = size - 1;
|
|
const invMask: i32 = ~mask;
|
|
|
|
std.debug.assert(@divFloor(2*maxRenderDistanceNew + size-1, size) + 2 <= storageSize);
|
|
|
|
const minX = px-%maxRenderDistanceNew & invMask;
|
|
const maxX = px+%maxRenderDistanceNew+%size & invMask;
|
|
var x = minX;
|
|
while(x != maxX): (x +%= size) {
|
|
const xIndex = @divExact(x, size) & storageMask;
|
|
var deltaXNew: i64 = @abs(x +% size/2 -% px);
|
|
deltaXNew = @max(0, deltaXNew - size/2);
|
|
var deltaXOld: i64 = @abs(x +% size/2 -% lastPx);
|
|
deltaXOld = @max(0, deltaXOld - size/2);
|
|
const maxYRenderDistanceNew: i32 = reduceRenderDistance(maxRenderDistanceNew, deltaXNew);
|
|
const maxYRenderDistanceOld: i32 = reduceRenderDistance(maxRenderDistanceOld, deltaXOld);
|
|
|
|
const minY = py-%maxYRenderDistanceNew & invMask;
|
|
const maxY = py+%maxYRenderDistanceNew+%size & invMask;
|
|
var y = minY;
|
|
while(y != maxY): (y +%= size) {
|
|
const yIndex = @divExact(y, size) & storageMask;
|
|
var deltaYOld: i64 = @abs(y +% size/2 -% lastPy);
|
|
deltaYOld = @max(0, deltaYOld - size/2);
|
|
var deltaYNew: i64 = @abs(y +% size/2 -% py);
|
|
deltaYNew = @max(0, deltaYNew - size/2);
|
|
var maxZRenderDistanceNew: i32 = reduceRenderDistance(maxYRenderDistanceNew, deltaYNew);
|
|
if(maxZRenderDistanceNew == 0) maxZRenderDistanceNew -= size/2;
|
|
var maxZRenderDistanceOld: i32 = reduceRenderDistance(maxYRenderDistanceOld, deltaYOld);
|
|
if(maxZRenderDistanceOld == 0) maxZRenderDistanceOld -= size/2;
|
|
|
|
const minZOld = lastPz-%maxZRenderDistanceOld & invMask;
|
|
const maxZOld = lastPz+%maxZRenderDistanceOld+%size & invMask;
|
|
const minZNew = pz-%maxZRenderDistanceNew & invMask;
|
|
const maxZNew = pz+%maxZRenderDistanceNew+%size & invMask;
|
|
|
|
var zValues: [storageSize]i32 = undefined;
|
|
var zValuesLen: usize = 0;
|
|
if(minZOld -% minZNew > 0) {
|
|
var z = minZNew;
|
|
while(z != minZOld and z != maxZNew): (z +%= size) {
|
|
zValues[zValuesLen] = z;
|
|
zValuesLen += 1;
|
|
}
|
|
}
|
|
if(maxZNew -% maxZOld > 0) {
|
|
var z = minZNew +% @max(0, maxZOld -% minZNew);
|
|
while(z != maxZNew): (z +%= size) {
|
|
zValues[zValuesLen] = z;
|
|
zValuesLen += 1;
|
|
}
|
|
}
|
|
|
|
for(zValues[0..zValuesLen]) |z| {
|
|
const zIndex = @divExact(z, size) & storageMask;
|
|
const index = (xIndex*storageSize + yIndex)*storageSize + zIndex;
|
|
const pos = chunk.ChunkPosition{.wx=x, .wy=y, .wz=z, .voxelSize=@as(u31, 1)<<lod};
|
|
|
|
const node = &storageLists[_lod][@intCast(index)];
|
|
std.debug.assert(node.mesh.load(.Unordered) == null);
|
|
try meshRequests.append(pos);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub noinline fn updateAndGetRenderChunks(conn: *network.Connection, playerPos: Vec3d, renderDistance: i32) ![]*chunk.meshing.ChunkMesh {
|
|
meshList.clearRetainingCapacity();
|
|
if(lastRD != renderDistance) {
|
|
try network.Protocols.genericUpdate.sendRenderDistance(conn, renderDistance);
|
|
}
|
|
const px: i32 = @intFromFloat(playerPos[0]);
|
|
const py: i32 = @intFromFloat(playerPos[1]);
|
|
const pz: i32 = @intFromFloat(playerPos[2]);
|
|
|
|
var meshRequests = std.ArrayList(chunk.ChunkPosition).init(main.globalAllocator);
|
|
defer meshRequests.deinit();
|
|
|
|
try freeOldMeshes(px, py, pz, renderDistance);
|
|
try createNewMeshes(px, py, pz, renderDistance, &meshRequests);
|
|
|
|
// 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;
|
|
}
|
|
};
|
|
|
|
// TODO: Is there a way to combine this with minecraft's approach?
|
|
var searchList = std.PriorityQueue(OcclusionData, void, OcclusionData.compare).init(main.globalAllocator, {});
|
|
defer searchList.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) {
|
|
const node = getNodeFromRenderThread(firstPos);
|
|
if(node.mesh.load(.Unordered) != null) {
|
|
node.lod = lod;
|
|
node.min = @splat(-1);
|
|
node.max = @splat(1);
|
|
node.active = true;
|
|
node.rendered = true;
|
|
try searchList.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;
|
|
}
|
|
}
|
|
var nodeList = std.ArrayList(*ChunkMeshNode).init(main.globalAllocator);
|
|
defer nodeList.deinit();
|
|
const projRotMat = game.projectionMatrix.mul(game.camera.viewMatrix);
|
|
while(searchList.removeOrNull()) |data| {
|
|
try nodeList.append(data.node);
|
|
data.node.active = false;
|
|
const mesh = data.node.mesh.load(.Unordered).?;
|
|
mesh.visibilityMask = 0xff;
|
|
const relPos: Vec3d = @as(Vec3d, @floatFromInt(Vec3i{mesh.pos.wx, mesh.pos.wy, mesh.pos.wz})) - playerPos;
|
|
const relPosFloat: Vec3f = @floatCast(relPos);
|
|
var forceNeighborsLod: [6]bool = .{false} ** 6;
|
|
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: Vec3f = @floatFromInt(mesh.chunkBorders[neighbor].min*@as(Vec3i, @splat(mesh.pos.voxelSize)));
|
|
const maxVec: Vec3f = @floatFromInt(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 = @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 + 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) {
|
|
defer {
|
|
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;
|
|
}
|
|
const node = getNodeFromRenderThread(neighborPos);
|
|
if(node.mesh.load(.Unordered)) |neighborMesh| {
|
|
// Ensure that there are no high-to-low lod transitions, which would produce cracks.
|
|
if(lod == data.node.lod and lod != settings.highestLOD and !node.rendered) {
|
|
var isValid: bool = true;
|
|
const relPos2: Vec3d = @as(Vec3d, @floatFromInt(Vec3i{neighborPos.wx, neighborPos.wy, neighborPos.wz})) - playerPos;
|
|
for(chunk.Neighbors.iterable) |neighbor2| {
|
|
const component2 = chunk.Neighbors.extractDirectionComponent(neighbor2, relPos2);
|
|
if(chunk.Neighbors.isPositive[neighbor2] and component2 + @as(f64, @floatFromInt(chunk.chunkSize*neighborMesh.pos.voxelSize)) >= 0) continue;
|
|
if(!chunk.Neighbors.isPositive[neighbor2] and component2 <= 0) continue;
|
|
{ // Check the chunk of same lod:
|
|
const neighborPos2 = chunk.ChunkPosition{
|
|
.wx = neighborPos.wx + chunk.Neighbors.relX[neighbor2]*chunk.chunkSize*neighborPos.voxelSize,
|
|
.wy = neighborPos.wy + chunk.Neighbors.relY[neighbor2]*chunk.chunkSize*neighborPos.voxelSize,
|
|
.wz = neighborPos.wz + chunk.Neighbors.relZ[neighbor2]*chunk.chunkSize*neighborPos.voxelSize,
|
|
.voxelSize = neighborPos.voxelSize,
|
|
};
|
|
const node2 = getNodeFromRenderThread(neighborPos2);
|
|
if(node2.rendered) {
|
|
continue;
|
|
}
|
|
}
|
|
{ // Check the chunk of higher lod
|
|
const neighborPos2 = chunk.ChunkPosition{
|
|
.wx = neighborPos.wx + chunk.Neighbors.relX[neighbor2]*chunk.chunkSize*neighborPos.voxelSize,
|
|
.wy = neighborPos.wy + chunk.Neighbors.relY[neighbor2]*chunk.chunkSize*neighborPos.voxelSize,
|
|
.wz = neighborPos.wz + chunk.Neighbors.relZ[neighbor2]*chunk.chunkSize*neighborPos.voxelSize,
|
|
.voxelSize = neighborPos.voxelSize << 1,
|
|
};
|
|
const node2 = getNodeFromRenderThread(neighborPos2);
|
|
if(node2.rendered) {
|
|
isValid = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if(!isValid) {
|
|
forceNeighborsLod[neighbor] = true;
|
|
continue;
|
|
}
|
|
}
|
|
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 searchList.add(.{
|
|
.node = node,
|
|
.distance = neighborMesh.pos.getMaxDistanceSquared(playerPos),
|
|
});
|
|
node.rendered = true;
|
|
}
|
|
break :continueNeighborLoop;
|
|
}
|
|
}
|
|
}
|
|
try mesh.changeLodBorders(forceNeighborsLod);
|
|
}
|
|
for(nodeList.items) |node| {
|
|
node.rendered = false;
|
|
const mesh = node.mesh.load(.Unordered).?;
|
|
if(mesh.pos.voxelSize != @as(u31, 1) << settings.highestLOD) {
|
|
const parent = getNodeFromRenderThread(.{.wx=mesh.pos.wx, .wy=mesh.pos.wy, .wz=mesh.pos.wz, .voxelSize=mesh.pos.voxelSize << 1});
|
|
if(parent.mesh.load(.Unordered)) |parentMesh| {
|
|
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);
|
|
}
|
|
}
|
|
if(mesh.needsNeighborUpdate) {
|
|
try mesh.uploadDataAndFinishNeighbors();
|
|
mesh.needsNeighborUpdate = false;
|
|
}
|
|
// Remove empty meshes.
|
|
if(mesh.opaqueMesh.vertexCount != 0 or mesh.voxelMesh.vertexCount != 0 or mesh.transparentMesh.vertexCount != 0) {
|
|
try meshList.append(mesh);
|
|
}
|
|
}
|
|
|
|
lastPx = px;
|
|
lastPy = py;
|
|
lastPz = pz;
|
|
lastRD = renderDistance;
|
|
// Make requests after updating the, to avoid concurrency issues and reduce the number of requests:
|
|
try network.Protocols.chunkRequest.sendRequest(conn, meshRequests.items);
|
|
return meshList.items;
|
|
}
|
|
|
|
pub fn updateMeshes(targetTime: i64) !void {
|
|
{ // First of all process all the block updates:
|
|
blockUpdateMutex.lock();
|
|
defer blockUpdateMutex.unlock();
|
|
for(blockUpdateList.items) |blockUpdate| {
|
|
const pos = chunk.ChunkPosition{.wx=blockUpdate.x, .wy=blockUpdate.y, .wz=blockUpdate.z, .voxelSize=1};
|
|
const node = getNodeFromRenderThread(pos);
|
|
if(node.mesh.load(.Unordered)) |mesh| {
|
|
try mesh.updateBlock(blockUpdate.x, blockUpdate.y, blockUpdate.z, blockUpdate.newBlock);
|
|
} // TODO: It seems like we simply ignore the block update if we don't have the mesh yet.
|
|
}
|
|
blockUpdateList.clearRetainingCapacity();
|
|
}
|
|
mutex.lock();
|
|
defer mutex.unlock();
|
|
for(clearList.items) |mesh| {
|
|
mesh.deinit();
|
|
main.globalAllocator.destroy(mesh);
|
|
}
|
|
clearList.clearRetainingCapacity();
|
|
while (priorityUpdateList.items.len != 0) {
|
|
const mesh = priorityUpdateList.orderedRemove(0);
|
|
mutex.unlock();
|
|
defer mutex.lock();
|
|
try mesh.decreaseRefCount();
|
|
if(getNodeFromRenderThread(mesh.pos).mesh.load(.Unordered) != mesh) continue; // This mesh isn't used for rendering anymore.
|
|
if(!mesh.needsNeighborUpdate) continue;
|
|
try mesh.uploadDataAndFinishNeighbors();
|
|
mesh.needsNeighborUpdate = false;
|
|
if(std.time.milliTimestamp() >= targetTime) break; // Update at least one mesh.
|
|
}
|
|
while(updatableList.items.len != 0) {
|
|
// TODO: Find a faster solution than going through the entire list every frame.
|
|
var closestPriority: f32 = -std.math.floatMax(f32);
|
|
var closestIndex: usize = 0;
|
|
const playerPos = game.Player.getPosBlocking();
|
|
{
|
|
var i: usize = 0;
|
|
while(i < updatableList.items.len) {
|
|
const mesh = updatableList.items[i];
|
|
if(!isInRenderDistance(mesh.pos)) {
|
|
_ = updatableList.swapRemove(i);
|
|
mutex.unlock();
|
|
defer mutex.lock();
|
|
try mesh.decreaseRefCount();
|
|
continue;
|
|
}
|
|
const priority = mesh.pos.getPriority(playerPos);
|
|
if(priority > closestPriority) {
|
|
closestPriority = priority;
|
|
closestIndex = i;
|
|
}
|
|
i += 1;
|
|
}
|
|
if(updatableList.items.len == 0) break;
|
|
}
|
|
const mesh = updatableList.swapRemove(closestIndex);
|
|
mutex.unlock();
|
|
defer mutex.lock();
|
|
if(isInRenderDistance(mesh.pos)) {
|
|
const node = getNodeFromRenderThread(mesh.pos);
|
|
try mesh.uploadDataAndFinishNeighbors();
|
|
if(node.mesh.swap(mesh, .Monotonic)) |oldMesh| {
|
|
try oldMesh.decreaseRefCount();
|
|
}
|
|
} else {
|
|
try mesh.decreaseRefCount();
|
|
}
|
|
if(std.time.milliTimestamp() >= targetTime) break; // Update at least one mesh.
|
|
}
|
|
}
|
|
|
|
pub fn addMeshToClearList(mesh: *chunk.meshing.ChunkMesh) !void {
|
|
std.debug.assert(mesh.refCount.load(.Monotonic) == 0);
|
|
mutex.lock();
|
|
defer mutex.unlock();
|
|
try clearList.append(mesh);
|
|
}
|
|
|
|
pub const MeshGenerationTask = struct {
|
|
mesh: *chunk.Chunk,
|
|
|
|
pub const vtable = utils.ThreadPool.VTable{
|
|
.getPriority = @ptrCast(&getPriority),
|
|
.isStillNeeded = @ptrCast(&isStillNeeded),
|
|
.run = @ptrCast(&run),
|
|
.clean = @ptrCast(&clean),
|
|
};
|
|
|
|
pub fn schedule(mesh: *chunk.Chunk) !void {
|
|
const task = try main.globalAllocator.create(MeshGenerationTask);
|
|
task.* = MeshGenerationTask {
|
|
.mesh = mesh,
|
|
};
|
|
try main.threadPool.addTask(task, &vtable);
|
|
}
|
|
|
|
pub fn getPriority(self: *MeshGenerationTask) f32 {
|
|
return self.mesh.pos.getPriority(game.Player.getPosBlocking()); // TODO: This is called in loop, find a way to do this without calling the mutex every time.
|
|
}
|
|
|
|
pub fn isStillNeeded(self: *MeshGenerationTask) bool {
|
|
const distanceSqr = self.mesh.pos.getMinDistanceSquared(game.Player.getPosBlocking()); // TODO: This is called in loop, find a way to do this without calling the mutex every time.
|
|
var maxRenderDistance = settings.renderDistance*chunk.chunkSize*self.mesh.pos.voxelSize;
|
|
maxRenderDistance += 2*self.mesh.pos.voxelSize*chunk.chunkSize;
|
|
return distanceSqr < @as(f64, @floatFromInt(maxRenderDistance*maxRenderDistance));
|
|
}
|
|
|
|
pub fn run(self: *MeshGenerationTask) Allocator.Error!void {
|
|
const pos = self.mesh.pos;
|
|
const mesh = try main.globalAllocator.create(chunk.meshing.ChunkMesh);
|
|
try mesh.init(pos, self.mesh);
|
|
try mesh.regenerateMainMesh();
|
|
mutex.lock();
|
|
defer mutex.unlock();
|
|
updatableList.append(mesh) catch |err| {
|
|
std.log.err("Error while regenerating mesh: {s}", .{@errorName(err)});
|
|
if(@errorReturnTrace()) |trace| {
|
|
std.log.err("Trace: {}", .{trace});
|
|
}
|
|
main.globalAllocator.destroy(self.mesh);
|
|
};
|
|
main.globalAllocator.destroy(self);
|
|
}
|
|
|
|
pub fn clean(self: *MeshGenerationTask) void {
|
|
main.globalAllocator.destroy(self.mesh);
|
|
main.globalAllocator.destroy(self);
|
|
}
|
|
};
|
|
|
|
pub fn updateBlock(x: i32, y: i32, z: i32, newBlock: blocks.Block) !void {
|
|
blockUpdateMutex.lock();
|
|
try blockUpdateList.append(BlockUpdate{.x=x, .y=y, .z=z, .newBlock=newBlock});
|
|
defer blockUpdateMutex.unlock();
|
|
}
|
|
|
|
pub fn updateChunkMesh(mesh: *chunk.Chunk) !void {
|
|
try MeshGenerationTask.schedule(mesh);
|
|
}
|
|
};
|