mirror of
https://github.com/PixelGuys/Cubyz.git
synced 2025-08-03 11:17:05 -04:00
1300 lines
48 KiB
Zig
1300 lines
48 KiB
Zig
const std = @import("std");
|
|
const Allocator = std.mem.Allocator;
|
|
|
|
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 Fog = graphics.Fog;
|
|
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 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;
|
|
const zNear = 0.1;
|
|
const zFar = 65536.0; // TODO: Fix z-fighting problems.
|
|
|
|
var fogShader: graphics.Shader = undefined;
|
|
var fogUniforms: struct {
|
|
fog_activ: c_int,
|
|
fog_color: c_int,
|
|
fog_density: c_int,
|
|
color: c_int,
|
|
} = undefined;
|
|
var deferredRenderPassShader: graphics.Shader = undefined;
|
|
var deferredUniforms: struct {
|
|
color: c_int,
|
|
} = undefined;
|
|
|
|
pub var activeFrameBuffer: c_uint = 0;
|
|
|
|
pub fn init() !void {
|
|
fogShader = try Shader.initAndGetUniforms("assets/cubyz/shaders/fog_vertex.vs", "assets/cubyz/shaders/fog_fragment.fs", &fogUniforms);
|
|
deferredRenderPassShader = try Shader.initAndGetUniforms("assets/cubyz/shaders/deferred_render_pass.vs", "assets/cubyz/shaders/deferred_render_pass.fs", &deferredUniforms);
|
|
buffers.init();
|
|
try Bloom.init();
|
|
try MeshSelection.init();
|
|
try MenuBackGround.init();
|
|
}
|
|
|
|
pub fn deinit() void {
|
|
fogShader.deinit();
|
|
deferredRenderPassShader.deinit();
|
|
buffers.deinit();
|
|
Bloom.deinit();
|
|
MeshSelection.deinit();
|
|
MenuBackGround.deinit();
|
|
}
|
|
|
|
const buffers = struct {
|
|
var buffer: c_uint = undefined;
|
|
var colorTexture: c_uint = undefined;
|
|
var depthBuffer: c_uint = undefined;
|
|
fn init() void {
|
|
c.glGenFramebuffers(1, &buffer);
|
|
c.glGenRenderbuffers(1, &depthBuffer);
|
|
c.glGenTextures(1, &colorTexture);
|
|
|
|
updateBufferSize(Window.width, Window.height);
|
|
|
|
c.glBindFramebuffer(c.GL_FRAMEBUFFER, buffer);
|
|
|
|
c.glFramebufferTexture2D(c.GL_FRAMEBUFFER, c.GL_COLOR_ATTACHMENT0, c.GL_TEXTURE_2D, colorTexture, 0);
|
|
|
|
c.glFramebufferRenderbuffer(c.GL_FRAMEBUFFER, c.GL_DEPTH_STENCIL_ATTACHMENT, c.GL_RENDERBUFFER, depthBuffer);
|
|
|
|
c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0);
|
|
}
|
|
|
|
fn deinit() void {
|
|
c.glDeleteFramebuffers(1, &buffer);
|
|
c.glDeleteRenderbuffers(1, &depthBuffer);
|
|
c.glDeleteTextures(1, &colorTexture);
|
|
}
|
|
|
|
fn regenTexture(texture: c_uint, internalFormat: c_int, format: c_uint, width: u31, height: u31) void {
|
|
c.glBindTexture(c.GL_TEXTURE_2D, texture);
|
|
c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MIN_FILTER, c.GL_NEAREST);
|
|
c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MAG_FILTER, c.GL_NEAREST);
|
|
c.glTexImage2D(c.GL_TEXTURE_2D, 0, internalFormat, width, height, 0, format, c.GL_UNSIGNED_BYTE, null);
|
|
c.glBindTexture(c.GL_TEXTURE_2D, 0);
|
|
}
|
|
|
|
fn updateBufferSize(width: u31, height: u31) void {
|
|
c.glBindFramebuffer(c.GL_FRAMEBUFFER, buffer);
|
|
|
|
regenTexture(colorTexture, c.GL_RGB10_A2, c.GL_RGB, width, height);
|
|
|
|
c.glBindRenderbuffer(c.GL_RENDERBUFFER, depthBuffer);
|
|
c.glRenderbufferStorage(c.GL_RENDERBUFFER, c.GL_DEPTH24_STENCIL8, width, height);
|
|
c.glBindRenderbuffer(c.GL_RENDERBUFFER, 0);
|
|
|
|
const attachments = [_]c_uint{c.GL_COLOR_ATTACHMENT0};
|
|
c.glDrawBuffers(attachments.len, &attachments);
|
|
|
|
c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0);
|
|
}
|
|
|
|
fn bindTextures() void {
|
|
c.glActiveTexture(c.GL_TEXTURE3);
|
|
c.glBindTexture(c.GL_TEXTURE_2D, colorTexture);
|
|
}
|
|
|
|
fn bind() void {
|
|
c.glBindFramebuffer(c.GL_FRAMEBUFFER, buffer);
|
|
}
|
|
|
|
fn unbind() void {
|
|
c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0);
|
|
}
|
|
|
|
fn clearAndBind(clearColor: Vec4f) void {
|
|
c.glBindFramebuffer(c.GL_FRAMEBUFFER, buffer);
|
|
c.glClearColor(clearColor[0], clearColor[1], clearColor[2], 1);
|
|
c.glClear(c.GL_DEPTH_BUFFER_BIT | c.GL_STENCIL_BUFFER_BIT | c.GL_COLOR_BUFFER_BIT);
|
|
// Clears the position separately to prevent issues with default value.
|
|
const positionClearColor = [_]f32 {0, 0, 6.55e4, 1}; // z value corresponds to the highest 16-bit float value.
|
|
c.glClearBufferfv(c.GL_COLOR, 1, &positionClearColor);
|
|
}
|
|
};
|
|
|
|
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);
|
|
// TODO: Transformation.updateProjectionMatrix(frustumProjectionMatrix, (float)Math.toRadians(fov), width, height, Z_NEAR, Z_FAR_LOD); // Need to combine both for frustum intersection
|
|
buffers.updateBufferSize(width, height);
|
|
}
|
|
|
|
pub fn render(playerPosition: Vec3d) !void {
|
|
var startTime = std.time.milliTimestamp();
|
|
// TODO: BlockMeshes.loadMeshes(); // Loads all meshes that weren't loaded yet
|
|
// 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;
|
|
// }
|
|
//
|
|
// while (!Cubyz.renderDeque.isEmpty()) {
|
|
// Cubyz.renderDeque.pop().run();
|
|
// }
|
|
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);
|
|
var skyColor = vec.xyz(world.clearColor);
|
|
game.fog.color = skyColor;
|
|
// TODO:
|
|
// Cubyz.fog.setActive(ClientSettings.FOG_COEFFICIENT != 0);
|
|
// Cubyz.fog.setDensity(1 / (ClientSettings.EFFECTIVE_RENDER_DISTANCE*ClientSettings.FOG_COEFFICIENT));
|
|
skyColor *= @splat(0.25);
|
|
|
|
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 {
|
|
_ = world;
|
|
buffers.clearAndBind(Vec4f{skyColor[0], skyColor[1], skyColor[2], 1});
|
|
game.camera.updateViewMatrix();
|
|
|
|
// Uses FrustumCulling on the chunks.
|
|
var frustum = Frustum.init(Vec3f{0, 0, 0}, game.camera.viewMatrix, lastFov, zFar, lastWidth, lastHeight);
|
|
|
|
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};
|
|
|
|
// Update the uniforms. The uniforms are needed to render the replacement meshes.
|
|
chunk.meshing.bindShaderAndUniforms(game.projectionMatrix, ambientLight, time);
|
|
|
|
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]);
|
|
var meshes = &struct {
|
|
var meshes = std.ArrayList(*chunk.meshing.ChunkMesh).init(main.globalAllocator);
|
|
}.meshes;
|
|
defer meshes.clearRetainingCapacity();
|
|
|
|
try RenderStructure.updateAndGetRenderChunks(game.world.?.conn, playerPos, settings.renderDistance, settings.LODFactor, frustum, meshes);
|
|
|
|
try sortChunks(meshes.items, playerPos);
|
|
|
|
// 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);
|
|
// }
|
|
// }
|
|
MeshSelection.select(playerPos, game.camera.direction);
|
|
MeshSelection.render(game.projectionMatrix, game.camera.viewMatrix, playerPos);
|
|
|
|
// Render the far away ReducedChunks:
|
|
chunk.meshing.bindShaderAndUniforms(game.projectionMatrix, ambientLight, time);
|
|
c.glUniform1i(chunk.meshing.uniforms.@"waterFog.activ", if(waterFog.active) 1 else 0);
|
|
c.glUniform3fv(chunk.meshing.uniforms.@"waterFog.color", 1, @ptrCast(&waterFog.color));
|
|
c.glUniform1f(chunk.meshing.uniforms.@"waterFog.density", waterFog.density);
|
|
|
|
for(meshes.items) |mesh| {
|
|
mesh.render(playerPos);
|
|
}
|
|
|
|
// for(int i = 0; i < visibleReduced.size; i++) {
|
|
// ReducedChunkMesh mesh = visibleReduced.array[i];
|
|
// mesh.render(playerPosition);
|
|
// }
|
|
|
|
entity.ClientEntityManager.render(game.projectionMatrix, ambientLight, .{1, 0.5, 0.25}, playerPos);
|
|
|
|
try itemdrop.ItemDropRenderer.renderItemDrops(game.projectionMatrix, ambientLight, playerPos, time);
|
|
|
|
// Render transparent chunk meshes:
|
|
|
|
chunk.meshing.bindTransparentShaderAndUniforms(game.projectionMatrix, ambientLight, time);
|
|
c.glUniform1i(chunk.meshing.transparentUniforms.@"waterFog.activ", if(waterFog.active) 1 else 0);
|
|
c.glUniform3fv(chunk.meshing.transparentUniforms.@"waterFog.color", 1, @ptrCast(&waterFog.color));
|
|
c.glUniform1f(chunk.meshing.transparentUniforms.@"waterFog.density", waterFog.density);
|
|
|
|
c.glBlendFunc(c.GL_SRC_ALPHA, c.GL_SRC1_COLOR);
|
|
{
|
|
var i: usize = meshes.items.len;
|
|
while(true) {
|
|
if(i == 0) break;
|
|
i -= 1;
|
|
meshes.items[i].renderTransparent(playerPos);
|
|
}
|
|
}
|
|
c.glBlendFunc(c.GL_SRC_ALPHA, c.GL_ONE_MINUS_SRC_ALPHA);
|
|
// NormalChunkMesh.bindTransparentShader(ambientLight, directionalLight.getDirection(), time);
|
|
|
|
buffers.bindTextures();
|
|
|
|
// NormalChunkMesh.transparentShader.setUniform(NormalChunkMesh.TransparentUniforms.loc_waterFog_activ, waterFog.isActive());
|
|
// NormalChunkMesh.transparentShader.setUniform(NormalChunkMesh.TransparentUniforms.loc_waterFog_color, waterFog.getColor());
|
|
// NormalChunkMesh.transparentShader.setUniform(NormalChunkMesh.TransparentUniforms.loc_waterFog_density, waterFog.getDensity());
|
|
|
|
// NormalChunkMesh[] meshes = sortChunks(visibleChunks.toArray(), x0/Chunk.chunkSize - 0.5f, y0/Chunk.chunkSize - 0.5f, z0/Chunk.chunkSize - 0.5f);
|
|
// for (NormalChunkMesh mesh : meshes) {
|
|
// NormalChunkMesh.transparentShader.setUniform(NormalChunkMesh.TransparentUniforms.loc_drawFrontFace, false);
|
|
// glCullFace(GL_FRONT);
|
|
// mesh.renderTransparent(playerPosition);
|
|
|
|
// NormalChunkMesh.transparentShader.setUniform(NormalChunkMesh.TransparentUniforms.loc_drawFrontFace, true);
|
|
// glCullFace(GL_BACK);
|
|
// mesh.renderTransparent(playerPosition);
|
|
// }
|
|
|
|
// if(selected != null && Blocks.transparent(selected.getBlock())) {
|
|
// BlockBreakingRenderer.render(selected, playerPosition);
|
|
// glActiveTexture(GL_TEXTURE0);
|
|
// Meshes.blockTextureArray.bind();
|
|
// glActiveTexture(GL_TEXTURE1);
|
|
// Meshes.emissionTextureArray.bind();
|
|
// }
|
|
|
|
fogShader.bind();
|
|
// Draw the water fog if the player is underwater:
|
|
// Player player = Cubyz.player;
|
|
// int block = Cubyz.world.getBlock((int)Math.round(player.getPosition().x), (int)(player.getPosition().y + player.height), (int)Math.round(player.getPosition().z));
|
|
// if (block != 0 && !Blocks.solid(block)) {
|
|
// if (Blocks.id(block).toString().equals("cubyz:water")) {
|
|
// fogShader.setUniform(FogUniforms.loc_fog_activ, waterFog.isActive());
|
|
// fogShader.setUniform(FogUniforms.loc_fog_color, waterFog.getColor());
|
|
// fogShader.setUniform(FogUniforms.loc_fog_density, waterFog.getDensity());
|
|
// glUniform1i(FogUniforms.loc_color, 3);
|
|
|
|
// glBindVertexArray(Graphics.rectVAO);
|
|
// glDisable(GL_DEPTH_TEST);
|
|
// glDisable(GL_CULL_FACE);
|
|
// glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
|
// }
|
|
// }
|
|
if(settings.bloom) {
|
|
Bloom.render(lastWidth, lastHeight);
|
|
}
|
|
buffers.unbind();
|
|
buffers.bindTextures();
|
|
deferredRenderPassShader.bind();
|
|
c.glUniform1i(deferredUniforms.color, 3);
|
|
|
|
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);
|
|
}
|
|
|
|
/// Sorts the chunks based on their distance from the player to reduce overdraw.
|
|
fn sortChunks(toSort: []*chunk.meshing.ChunkMesh, playerPos: Vec3d) !void {
|
|
const distances = try main.threadAllocator.alloc(f64, toSort.len);
|
|
defer main.threadAllocator.free(distances);
|
|
|
|
for(distances, 0..) |*dist, i| {
|
|
dist.* = vec.length(playerPos - Vec3d{
|
|
@floatFromInt(toSort[i].pos.wx),
|
|
@floatFromInt(toSort[i].pos.wy),
|
|
@floatFromInt(toSort[i].pos.wz),
|
|
});
|
|
}
|
|
// Insert sort them:
|
|
var i: u32 = 1;
|
|
while(i < toSort.len) : (i += 1) {
|
|
var j: u32 = i - 1;
|
|
while(true) {
|
|
@setRuntimeSafety(false);
|
|
if(distances[j] > distances[j+1]) {
|
|
// Swap them:
|
|
{
|
|
const swap = distances[j];
|
|
distances[j] = distances[j+1];
|
|
distances[j+1] = swap;
|
|
} {
|
|
const swap = toSort[j];
|
|
toSort[j] = toSort[j+1];
|
|
toSort[j+1] = swap;
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
|
|
if(j == 0) break;
|
|
j -= 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 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 upscaleShader: graphics.Shader = undefined;
|
|
|
|
pub fn init() !void {
|
|
buffer1.init(false);
|
|
buffer2.init(false);
|
|
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.init("assets/cubyz/shaders/bloom/color_extractor_downsample.vs", "assets/cubyz/shaders/bloom/color_extractor_downsample.fs");
|
|
upscaleShader = try graphics.Shader.init("assets/cubyz/shaders/bloom/upscale.vs", "assets/cubyz/shaders/bloom/upscale.fs");
|
|
}
|
|
|
|
pub fn deinit() void {
|
|
buffer1.deinit();
|
|
buffer2.deinit();
|
|
firstPassShader.deinit();
|
|
secondPassShader.deinit();
|
|
upscaleShader.deinit();
|
|
}
|
|
|
|
fn extractImageDataAndDownsample() void {
|
|
colorExtractAndDownsampleShader.bind();
|
|
buffers.bindTextures();
|
|
buffer1.bind();
|
|
c.glBindVertexArray(graphics.draw.rectVAO);
|
|
c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4);
|
|
}
|
|
|
|
fn firstPass() void {
|
|
firstPassShader.bind();
|
|
c.glActiveTexture(c.GL_TEXTURE3);
|
|
c.glBindTexture(c.GL_TEXTURE_2D, buffer1.texture);
|
|
buffer2.bind();
|
|
c.glBindVertexArray(graphics.draw.rectVAO);
|
|
c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4);
|
|
}
|
|
|
|
fn secondPass() void {
|
|
secondPassShader.bind();
|
|
c.glActiveTexture(c.GL_TEXTURE3);
|
|
c.glBindTexture(c.GL_TEXTURE_2D, buffer2.texture);
|
|
buffer1.bind();
|
|
c.glBindVertexArray(graphics.draw.rectVAO);
|
|
c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4);
|
|
}
|
|
|
|
fn upscale() void {
|
|
upscaleShader.bind();
|
|
c.glActiveTexture(c.GL_TEXTURE3);
|
|
c.glBindTexture(c.GL_TEXTURE_2D, buffer1.texture);
|
|
buffers.bind();
|
|
c.glBindVertexArray(graphics.draw.rectVAO);
|
|
c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4);
|
|
}
|
|
|
|
pub fn render(currentWidth: u31, currentHeight: u31) void {
|
|
if(width != currentWidth or height != currentHeight) {
|
|
width = currentWidth;
|
|
height = currentHeight;
|
|
buffer1.updateSize(width/2, height/2, c.GL_NEAREST, c.GL_CLAMP_TO_EDGE);
|
|
std.debug.assert(buffer1.validate());
|
|
buffer2.updateSize(width/2, height/2, c.GL_NEAREST, c.GL_CLAMP_TO_EDGE);
|
|
std.debug.assert(buffer2.validate());
|
|
}
|
|
c.glDisable(c.GL_DEPTH_TEST);
|
|
c.glDisable(c.GL_CULL_FACE);
|
|
|
|
c.glViewport(0, 0, width/2, height/2);
|
|
extractImageDataAndDownsample();
|
|
firstPass();
|
|
secondPass();
|
|
c.glViewport(0, 0, width, height);
|
|
c.glBlendFunc(c.GL_ONE, c.GL_ONE);
|
|
upscale();
|
|
|
|
c.glEnable(c.GL_DEPTH_TEST);
|
|
c.glEnable(c.GL_CULL_FACE);
|
|
c.glBlendFunc(c.GL_SRC_ALPHA, c.GL_ONE_MINUS_SRC_ALPHA);
|
|
}
|
|
};
|
|
|
|
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.threadAllocator);
|
|
defer walker.deinit();
|
|
var fileList = std.ArrayList([]const u8).init(main.threadAllocator);
|
|
defer {
|
|
for(fileList.items) |fileName| {
|
|
main.threadAllocator.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.threadAllocator.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.threadAllocator, "assets/backgrounds/{s}", .{fileList.items[theChosenOne]});
|
|
defer main.threadAllocator.free(theChosenPath);
|
|
texture = try graphics.Texture.initFromFile(theChosenPath);
|
|
}
|
|
|
|
pub fn deinit() void {
|
|
shader.deinit();
|
|
c.glDeleteVertexArrays(1, &vao);
|
|
c.glDeleteBuffers(2, &vbos);
|
|
}
|
|
|
|
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;
|
|
var viewMatrix = Mat4f.rotationY(angle);
|
|
shader.bind();
|
|
|
|
c.glUniformMatrix4fv(uniforms.viewMatrix, 1, c.GL_FALSE, @ptrCast(&viewMatrix));
|
|
c.glUniformMatrix4fv(uniforms.projectionMatrix, 1, c.GL_FALSE, @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.
|
|
var pixels: []u32 = try main.threadAllocator.alloc(u32, size*size);
|
|
defer main.threadAllocator.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);
|
|
defer buffer.deinit();
|
|
buffer.updateSize(size, size, c.GL_NEAREST, c.GL_REPEAT);
|
|
|
|
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.
|
|
var image = try graphics.Image.init(main.threadAllocator, 4*size, size);
|
|
defer image.deinit(main.threadAllocator);
|
|
|
|
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.threadAllocator, "assets/backgrounds/{s}_{}.png", .{game.world.?.name, game.world.?.gameTime.load(.Monotonic)});
|
|
defer main.threadAllocator.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: [5]Plane, // Who cares about the near plane anyways?
|
|
|
|
pub fn init(cameraPos: Vec3f, rotationMatrix: Mat4f, fovY: f32, _zFar: f32, width: u31, height: u31) Frustum {
|
|
var invRotationMatrix = rotationMatrix.transpose();
|
|
var cameraDir = vec.xyz(invRotationMatrix.mulVec(Vec4f{0, 0, 1, 1}));
|
|
var cameraUp = vec.xyz(invRotationMatrix.mulVec(Vec4f{0, 1, 0, 1}));
|
|
var cameraRight = vec.xyz(invRotationMatrix.mulVec(Vec4f{1, 0, 0, 1}));
|
|
|
|
const halfVSide = _zFar*std.math.tan(std.math.degreesToRadians(f32, fovY)*0.5);
|
|
const halfHSide = halfVSide*@as(f32, @floatFromInt(width))/@as(f32, @floatFromInt(height));
|
|
const frontMultFar = cameraDir*@as(Vec3f, @splat(_zFar));
|
|
|
|
var self: Frustum = undefined;
|
|
self.planes[0] = Plane{.pos = cameraPos + frontMultFar, .norm=-cameraDir}; // far
|
|
self.planes[1] = Plane{.pos = cameraPos, .norm=vec.cross(cameraUp, frontMultFar + cameraRight*@as(Vec3f, @splat(halfHSide)))}; // right
|
|
self.planes[2] = Plane{.pos = cameraPos, .norm=vec.cross(frontMultFar - cameraRight*@as(Vec3f, @splat(halfHSide)), cameraUp)}; // left
|
|
self.planes[3] = Plane{.pos = cameraPos, .norm=vec.cross(cameraRight, frontMultFar - cameraUp*@as(Vec3f, @splat(halfVSide)))}; // top
|
|
self.planes[4] = Plane{.pos = cameraPos, .norm=vec.cross(frontMultFar + 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 += @max(0, dim[0]*plane.norm[0]);
|
|
dist += @max(0, dim[1]*plane.norm[1]);
|
|
dist += @max(0, dim[2]*plane.norm[2]);
|
|
if(dist < 128) 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);
|
|
|
|
var 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,
|
|
};
|
|
var 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 selectedBlockPos: ?Vec3i = null;
|
|
pub fn select(_pos: Vec3d, _dir: Vec3f) void {
|
|
var pos = _pos;
|
|
// TODO: pos.y += Player.cameraHeight;
|
|
var dir = vec.floatCast(f64, _dir);
|
|
//TODO:
|
|
// intersection.set(0, 0, 0, dir.x, dir.y, dir.z);
|
|
|
|
// 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 = vec.intFromFloat(i32, std.math.sign(dir));
|
|
const invDir = @as(Vec3d, @splat(1))/dir;
|
|
const tDelta = @fabs(invDir);
|
|
var tMax = (@floor(pos) - pos)*invDir;
|
|
tMax = @max(tMax, tMax + tDelta*vec.floatFromInt(f64, step));
|
|
tMax = @select(f64, dir == @as(Vec3d, @splat(0)), @as(Vec3d, @splat(std.math.inf(f64))), tMax);
|
|
var voxelPos = vec.intFromFloat(i32, @floor(pos));
|
|
|
|
var total_tMax: f64 = 0;
|
|
|
|
selectedBlockPos = null;
|
|
|
|
while(total_tMax < closestDistance) {
|
|
const block = RenderStructure.getBlock(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];
|
|
var transformedMin = model.permutation.transform(voxelModel.min - @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 max = @max(transformedMin ,transformedMax);
|
|
const t1 = (vec.floatFromInt(f64, voxelPos) + vec.floatFromInt(f64, min)/@as(Vec3d, @splat(16.0)) - pos)*invDir;
|
|
const t2 = (vec.floatFromInt(f64, voxelPos) + vec.floatFromInt(f64, 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;
|
|
}
|
|
}
|
|
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
|
|
}
|
|
// TODO(requires inventory):
|
|
// /**
|
|
// * Places a block in the world.
|
|
// * @param stack
|
|
// * @param world
|
|
// */
|
|
// public void placeBlock(ItemStack stack, World world) {
|
|
// if (selectedSpatial != null && selectedSpatial instanceof BlockInstance) {
|
|
// BlockInstance bi = (BlockInstance)selectedSpatial;
|
|
// IntWrapper block = new IntWrapper(bi.getBlock());
|
|
// Vector3d relativePos = new Vector3d(pos);
|
|
// relativePos.sub(bi.x, bi.y, bi.z);
|
|
// int b = stack.getBlock();
|
|
// if (b != 0) {
|
|
// Vector3i neighborDir = new Vector3i();
|
|
// // Check if stuff can be added to the block itself:
|
|
// if (b == bi.getBlock()) {
|
|
// if (Blocks.mode(b).generateData(Cubyz.world, bi.x, bi.y, bi.z, relativePos, dir, neighborDir, block, false)) {
|
|
// world.updateBlock(bi.x, bi.y, bi.z, block.data);
|
|
// stack.add(-1);
|
|
// return;
|
|
// }
|
|
// }
|
|
// // Get the next neighbor:
|
|
// Vector3i neighbor = new Vector3i();
|
|
// getEmptyPlace(neighbor, neighborDir);
|
|
// relativePos.set(pos);
|
|
// relativePos.sub(neighbor.x, neighbor.y, neighbor.z);
|
|
// boolean dataOnlyUpdate = world.getBlock(neighbor.x, neighbor.y, neighbor.z) == b;
|
|
// if (dataOnlyUpdate) {
|
|
// block.data = world.getBlock(neighbor.x, neighbor.y, neighbor.z);
|
|
// if (Blocks.mode(b).generateData(Cubyz.world, neighbor.x, neighbor.y, neighbor.z, relativePos, dir, neighborDir, block, false)) {
|
|
// world.updateBlock(neighbor.x, neighbor.y, neighbor.z, block.data);
|
|
// stack.add(-1);
|
|
// }
|
|
// } else {
|
|
// // Check if the block can actually be placed at that point. There might be entities or other blocks in the way.
|
|
// if (Blocks.solid(world.getBlock(neighbor.x, neighbor.y, neighbor.z)))
|
|
// return;
|
|
// 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.data = b;
|
|
// if (Blocks.mode(b).generateData(Cubyz.world, neighbor.x, neighbor.y, neighbor.z, relativePos, dir, neighborDir, block, true)) {
|
|
// world.updateBlock(neighbor.x, neighbor.y, neighbor.z, block.data);
|
|
// stack.add(-1);
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
// TODO: Check how this is needed.
|
|
// /**
|
|
// * Returns the free block right next to the currently selected block.
|
|
// * @param pos selected block position
|
|
// * @param dir camera direction
|
|
// */
|
|
// private void getEmptyPlace(Vector3i pos, Vector3i dir) {
|
|
// int dirX = CubyzMath.nonZeroSign(this.dir.x);
|
|
// int dirY = CubyzMath.nonZeroSign(this.dir.y);
|
|
// int dirZ = CubyzMath.nonZeroSign(this.dir.z);
|
|
// pos.set(((BlockInstance)selectedSpatial).x, ((BlockInstance)selectedSpatial).y, ((BlockInstance)selectedSpatial).z);
|
|
// pos.add(-dirX, 0, 0);
|
|
// dir.add(dirX, 0, 0);
|
|
// min.set(pos.x, pos.y, pos.z).sub(this.pos);
|
|
// max.set(min);
|
|
// max.add(1, 1, 1); // scale, scale, scale
|
|
// if (!intersection.test((float)min.x, (float)min.y, (float)min.z, (float)max.x, (float)max.y, (float)max.z)) {
|
|
// pos.add(dirX, -dirY, 0);
|
|
// dir.add(-dirX, dirY, 0);
|
|
// min.set(pos.x, pos.y, pos.z).sub(this.pos);
|
|
// max.set(min);
|
|
// max.add(1, 1, 1); // scale, scale, scale
|
|
// if (!intersection.test((float)min.x, (float)min.y, (float)min.z, (float)max.x, (float)max.y, (float)max.z)) {
|
|
// pos.add(0, dirY, -dirZ);
|
|
// dir.add(0, -dirY, dirZ);
|
|
// }
|
|
// }
|
|
// }
|
|
pub fn render(projectionMatrix: Mat4f, viewMatrix: Mat4f, playerPos: Vec3d) void {
|
|
if(selectedBlockPos) |_selectedBlockPos| {
|
|
var block = RenderStructure.getBlock(_selectedBlockPos[0], _selectedBlockPos[1], _selectedBlockPos[2]) orelse return;
|
|
const model = blocks.meshes.model(block);
|
|
const voxelModel = &models.voxelModels.items[model.modelIndex];
|
|
var transformedMin = model.permutation.transform(voxelModel.min - @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 max = @max(transformedMin ,transformedMax);
|
|
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(@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.
|
|
}
|
|
}
|
|
};
|
|
|
|
pub const RenderStructure = struct {
|
|
const ChunkMeshNode = struct {
|
|
mesh: chunk.meshing.ChunkMesh,
|
|
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.
|
|
};
|
|
var storageLists: [settings.highestLOD + 1][]?*ChunkMeshNode = [1][]?*ChunkMeshNode{&.{}} ** (settings.highestLOD + 1);
|
|
var storageListsSwap: [settings.highestLOD + 1][]?*ChunkMeshNode = [1][]?*ChunkMeshNode{&.{}} ** (settings.highestLOD + 1);
|
|
var updatableList: std.ArrayList(chunk.ChunkPosition) = undefined;
|
|
var updatableListSwap: std.ArrayList(chunk.ChunkPosition) = undefined;
|
|
var clearList: std.ArrayList(*ChunkMeshNode) = undefined;
|
|
var lastRD: i32 = 0;
|
|
var lastFactor: f32 = 0;
|
|
var lastX: [settings.highestLOD + 1]i32 = [_]i32{0} ** (settings.highestLOD + 1);
|
|
var lastY: [settings.highestLOD + 1]i32 = [_]i32{0} ** (settings.highestLOD + 1);
|
|
var lastZ: [settings.highestLOD + 1]i32 = [_]i32{0} ** (settings.highestLOD + 1);
|
|
var lastSize: [settings.highestLOD + 1]i32 = [_]i32{0} ** (settings.highestLOD + 1);
|
|
var lodMutex: [settings.highestLOD + 1]std.Thread.Mutex = [_]std.Thread.Mutex{std.Thread.Mutex{}} ** (settings.highestLOD + 1);
|
|
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;
|
|
lastFactor = 0;
|
|
updatableList = std.ArrayList(chunk.ChunkPosition).init(main.globalAllocator);
|
|
blockUpdateList = std.ArrayList(BlockUpdate).init(main.globalAllocator);
|
|
clearList = std.ArrayList(*ChunkMeshNode).init(main.globalAllocator);
|
|
for(&storageLists) |*storageList| {
|
|
storageList.* = try main.globalAllocator.alloc(?*ChunkMeshNode, 0);
|
|
}
|
|
}
|
|
|
|
pub fn deinit() void {
|
|
for(storageLists) |storageList| {
|
|
for(storageList) |nullChunkMesh| {
|
|
if(nullChunkMesh) |chunkMesh| {
|
|
chunkMesh.mesh.deinit();
|
|
main.globalAllocator.destroy(chunkMesh);
|
|
}
|
|
}
|
|
main.globalAllocator.free(storageList);
|
|
}
|
|
updatableList.deinit();
|
|
for(clearList.items) |chunkMesh| {
|
|
chunkMesh.mesh.deinit();
|
|
main.globalAllocator.destroy(chunkMesh);
|
|
}
|
|
blockUpdateList.deinit();
|
|
clearList.deinit();
|
|
}
|
|
|
|
fn _getNode(pos: chunk.ChunkPosition) ?*ChunkMeshNode {
|
|
var lod = std.math.log2_int(u31, pos.voxelSize);
|
|
lodMutex[lod].lock();
|
|
defer lodMutex[lod].unlock();
|
|
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)]; // TODO: Wait for #12205 to be fixed and remove the weird (&...).* workaround.
|
|
}
|
|
|
|
pub fn getChunk(x: i32, y: i32, z: i32) ?*chunk.Chunk {
|
|
const node = RenderStructure._getNode(.{.wx = x, .wy = y, .wz = z, .voxelSize=1}) orelse return null;
|
|
return node.mesh.chunk.load(.Monotonic);
|
|
}
|
|
|
|
pub fn getBlock(x: i32, y: i32, z: i32) ?blocks.Block {
|
|
const node = RenderStructure._getNode(.{.wx = x, .wy = y, .wz = z, .voxelSize=1}) orelse return null;
|
|
const block = (node.mesh.chunk.load(.Monotonic) orelse return null).getBlock(x & chunk.chunkMask, y & chunk.chunkMask, z & chunk.chunkMask);
|
|
return block;
|
|
}
|
|
|
|
pub fn getNeighbor(_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;
|
|
var node = _getNode(pos) orelse return null;
|
|
return &node.mesh;
|
|
}
|
|
|
|
pub fn updateAndGetRenderChunks(conn: *network.Connection, playerPos: Vec3d, renderDistance: i32, LODFactor: f32, frustum: Frustum, meshes: *std.ArrayList(*chunk.meshing.ChunkMesh)) !void {
|
|
if(lastRD != renderDistance and lastFactor != LODFactor) {
|
|
try network.Protocols.genericUpdate.sendRenderDistance(conn, renderDistance, LODFactor);
|
|
}
|
|
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.threadAllocator);
|
|
defer meshRequests.deinit();
|
|
|
|
for(0..storageLists.len) |_lod| {
|
|
const lod: u5 = @intCast(_lod);
|
|
var maxRenderDistance = renderDistance*chunk.chunkSize << lod;
|
|
if(lod != 0) maxRenderDistance = @intFromFloat(@ceil(@as(f32, @floatFromInt(maxRenderDistance))*LODFactor));
|
|
var sizeShift = chunk.chunkShift + lod;
|
|
const size: u31 = @intCast(chunk.chunkSize << lod);
|
|
const mask: i32 = size - 1;
|
|
const invMask: i32 = ~mask;
|
|
|
|
const maxSideLength: u31 = @intCast(@divFloor(2*maxRenderDistance + size-1, size) + 2);
|
|
var newList = storageListsSwap[_lod];
|
|
if(newList.len != maxSideLength*maxSideLength*maxSideLength) {
|
|
main.globalAllocator.free(newList);
|
|
newList = try main.globalAllocator.alloc(?*ChunkMeshNode, maxSideLength*maxSideLength*maxSideLength);
|
|
}
|
|
@memset(newList, null);
|
|
|
|
const startX = size*(@divFloor(px, size) -% maxSideLength/2);
|
|
const startY = size*(@divFloor(py, size) -% maxSideLength/2);
|
|
const startZ = size*(@divFloor(pz, size) -% maxSideLength/2);
|
|
|
|
const minX = px-%maxRenderDistance-%1 & invMask;
|
|
const maxX = px+%maxRenderDistance+%size & invMask;
|
|
var x = minX;
|
|
while(x != maxX): (x +%= size) {
|
|
const xIndex = @divExact(x -% startX, size);
|
|
var deltaX: i64 = std.math.absInt(@as(i64, x +% size/2 -% px)) catch unreachable;
|
|
deltaX = @max(0, deltaX - size/2);
|
|
const maxYRenderDistanceSquare: f64 = @floatFromInt(@as(i64, maxRenderDistance)*@as(i64, maxRenderDistance) - deltaX*deltaX);
|
|
const maxYRenderDistance: i32 = @intFromFloat(@ceil(@sqrt(maxYRenderDistanceSquare)));
|
|
|
|
const minY = py-%maxYRenderDistance-%1 & invMask;
|
|
const maxY = py+%maxYRenderDistance+%size & invMask;
|
|
var y = minY;
|
|
while(y != maxY): (y +%= size) {
|
|
const yIndex = @divExact(y -% startY, size);
|
|
var deltaY: i64 = std.math.absInt(@as(i64, y +% size/2 -% py)) catch unreachable;
|
|
deltaY = @max(0, deltaY - size/2);
|
|
const maxZRenderDistanceSquare: f64 = @floatFromInt(@as(i64, maxYRenderDistance)*@as(i64, maxYRenderDistance) - deltaY*deltaY);
|
|
const maxZRenderDistance: i32 = @intFromFloat(@ceil(@sqrt(maxZRenderDistanceSquare)));
|
|
|
|
const minZ = pz-%maxZRenderDistance-%1 & invMask;
|
|
const maxZ = pz+%maxZRenderDistance+%size & invMask;
|
|
var z = minZ;
|
|
while(z != maxZ): (z +%= size) {
|
|
const zIndex = @divExact(z -% startZ, size);
|
|
const index = (xIndex*maxSideLength + yIndex)*maxSideLength + zIndex;
|
|
const pos = chunk.ChunkPosition{.wx=x, .wy=y, .wz=z, .voxelSize=@as(u31, 1)<<lod};
|
|
var node = _getNode(pos);
|
|
if(node) |_node| {
|
|
_node.shouldBeRemoved = false;
|
|
} else {
|
|
node = try main.globalAllocator.create(ChunkMeshNode);
|
|
node.?.mesh = chunk.meshing.ChunkMesh.init(main.globalAllocator, pos);
|
|
node.?.shouldBeRemoved = true; // Might be removed in the next iteration.
|
|
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 meshes.append(&node.?.mesh);
|
|
}
|
|
if(lod+1 != storageLists.len and node.?.mesh.generated) {
|
|
if(_getNode(.{.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);
|
|
parent.mesh.visibilityMask &= ~(@as(u8, 1) << octantIndex);
|
|
}
|
|
}
|
|
node.?.drawableChildren = 0;
|
|
newList[@intCast(index)] = node;
|
|
}
|
|
}
|
|
}
|
|
|
|
var oldList = storageLists[lod];
|
|
{
|
|
lodMutex[lod].lock();
|
|
defer lodMutex[lod].unlock();
|
|
lastX[lod] = startX;
|
|
lastY[lod] = startY;
|
|
lastZ[lod] = startZ;
|
|
lastSize[lod] = maxSideLength;
|
|
storageLists[lod] = newList;
|
|
storageListsSwap[lod] = oldList;
|
|
}
|
|
for(oldList) |nullMesh| {
|
|
if(nullMesh) |mesh| {
|
|
if(mesh.shouldBeRemoved) {
|
|
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| {
|
|
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;
|
|
}
|
|
}
|
|
// Update the neighbors, so we don't get cracks when we look back:
|
|
for(chunk.Neighbors.iterable) |neighbor| {
|
|
if(getNeighbor(mesh.mesh.pos, mesh.mesh.pos.voxelSize, neighbor)) |neighborMesh| {
|
|
if(neighborMesh.generated) {
|
|
neighborMesh.mutex.lock();
|
|
defer neighborMesh.mutex.unlock();
|
|
try neighborMesh.uploadDataAndFinishNeighbors();
|
|
}
|
|
}
|
|
}
|
|
if(mesh.mesh.mutex.tryLock()) { // Make sure there is no task currently running on the thing.
|
|
mesh.mesh.mutex.unlock();
|
|
mesh.mesh.deinit();
|
|
main.globalAllocator.destroy(mesh);
|
|
} else {
|
|
try clearList.append(mesh);
|
|
}
|
|
} else {
|
|
mesh.shouldBeRemoved = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var i: usize = 0;
|
|
while(i < clearList.items.len) {
|
|
const mesh = clearList.items[i];
|
|
if(mesh.mesh.mutex.tryLock()) { // Make sure there is no task currently running on the thing.
|
|
mesh.mesh.mutex.unlock();
|
|
mesh.mesh.deinit();
|
|
main.globalAllocator.destroy(mesh);
|
|
_ = clearList.swapRemove(i);
|
|
} else {
|
|
i += 1;
|
|
}
|
|
}
|
|
|
|
lastRD = renderDistance;
|
|
lastFactor = LODFactor;
|
|
// Make requests after updating the, to avoid concurrency issues and reduce the number of requests:
|
|
try network.Protocols.chunkRequest.sendRequest(conn, meshRequests.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};
|
|
if(_getNode(pos)) |node| {
|
|
try node.mesh.updateBlock(blockUpdate.x, blockUpdate.y, blockUpdate.z, blockUpdate.newBlock);
|
|
}
|
|
}
|
|
blockUpdateList.clearRetainingCapacity();
|
|
}
|
|
mutex.lock();
|
|
defer mutex.unlock();
|
|
while(updatableList.items.len != 0) {
|
|
// TODO: Find a faster solution than going through the entire list.
|
|
var closestPriority: f32 = -std.math.floatMax(f32);
|
|
var closestIndex: usize = 0;
|
|
const playerPos = game.Player.getPosBlocking();
|
|
for(updatableList.items, 0..) |pos, i| {
|
|
const priority = pos.getPriority(playerPos);
|
|
if(priority > closestPriority) {
|
|
closestPriority = priority;
|
|
closestIndex = i;
|
|
}
|
|
}
|
|
const pos = updatableList.orderedRemove(closestIndex);
|
|
mutex.unlock();
|
|
defer mutex.lock();
|
|
const nullNode = _getNode(pos);
|
|
if(nullNode) |node| {
|
|
node.mesh.mutex.lock();
|
|
defer node.mesh.mutex.unlock();
|
|
node.mesh.uploadDataAndFinishNeighbors() catch |err| {
|
|
if(err == error.LODMissing) {
|
|
mutex.lock();
|
|
defer mutex.unlock();
|
|
try updatableList.append(pos);
|
|
} else {
|
|
return err;
|
|
}
|
|
};
|
|
}
|
|
if(std.time.milliTimestamp() >= targetTime) break; // Update at least one 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 {
|
|
var 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 {
|
|
var 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;
|
|
if(self.mesh.pos.voxelSize != 1) maxRenderDistance = @intFromFloat(@ceil(@as(f32, @floatFromInt(maxRenderDistance))*settings.LODFactor));
|
|
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 nullNode = _getNode(pos);
|
|
if(nullNode) |node| {
|
|
{
|
|
node.mesh.mutex.lock();
|
|
defer node.mesh.mutex.unlock();
|
|
try node.mesh.regenerateMainMesh(self.mesh);
|
|
}
|
|
mutex.lock();
|
|
defer mutex.unlock();
|
|
updatableList.append(pos) 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);
|
|
};
|
|
} else {
|
|
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);
|
|
}
|
|
};
|