From 024786085efe9068fec10ad721ed12b8bea94c96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Fernandes?= Date: Tue, 5 May 2026 19:49:41 +0100 Subject: [PATCH 01/14] Add Translate C to build.zig, remove @cImport from graphics.zig --- build.zig | 9 +++++ src/c.h | 24 ++++++++++++ src/graphics.zig | 100 ++++++++++++++++------------------------------- 3 files changed, 67 insertions(+), 66 deletions(-) create mode 100644 src/c.h diff --git a/build.zig b/build.zig index bbbd739b25..e3bbdb740e 100644 --- a/build.zig +++ b/build.zig @@ -48,6 +48,15 @@ fn linkLibraries(b: *std.Build, exe: *std.Build.Step.Compile, useLocalDeps: bool exe.root_module.addObjectFile(subPath.path(b, libName(b, "SPIRV-Tools", t))); exe.root_module.addObjectFile(subPath.path(b, libName(b, "SPIRV-Tools-opt", t))); + const translate_c = b.addTranslateC(.{ + .root_source_file = b.path("src/c.h"), + .target = target, + .optimize = optimize, + }); + translate_c.addIncludePath(headersDeps.path("include")); + + exe.root_module.addImport("c", translate_c.createModule()); + if (t.os.tag == .macos) { const moltenVkLibInstall = b.addInstallFile(subPath.path(b, "libMoltenVK.dylib"), "bin/Cubyz.app/Contents/Frameworks/libMoltenVK.dylib"); const moltenVkJsonInstall = b.addInstallFile(subPath.path(b, "MoltenVK_icd.json"), "bin/Cubyz.app/Contents/Resources/vulkan/icd.d/MoltenVK_icd.json"); diff --git a/src/c.h b/src/c.h new file mode 100644 index 0000000000..d25aead3cf --- /dev/null +++ b/src/c.h @@ -0,0 +1,24 @@ +// graphics.zig +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#ifdef __MACH__ +#include +#include +#else +#include +#endif +#include + +#include +#include diff --git a/src/graphics.zig b/src/graphics.zig index ee02c59cc7..2fcff2e18d 100644 --- a/src/graphics.zig +++ b/src/graphics.zig @@ -3,20 +3,7 @@ const std = @import("std"); const builtin = @import("builtin"); -pub const hbft = @cImport({ - @cDefine("_BITS_STDIO2_H", ""); // TODO: Zig fails to include this header file - @cInclude("freetype/ftadvanc.h"); - @cInclude("freetype/ftbbox.h"); - @cInclude("freetype/ftbitmap.h"); - @cInclude("freetype/ftcolor.h"); - @cInclude("freetype/ftlcdfil.h"); - @cInclude("freetype/ftsizes.h"); - @cInclude("freetype/ftstroke.h"); - @cInclude("freetype/fttrigon.h"); - @cInclude("freetype/ftsynth.h"); - @cInclude("hb.h"); - @cInclude("hb-ft.h"); -}); +pub const c = @import("c"); const vec = @import("vec.zig"); const Mat4f = vec.Mat4f; @@ -36,25 +23,6 @@ pub const ComputePipeline = pipelines.ComputePipeline; pub const Pipeline = pipelines.Pipeline; pub const vulkan = @import("graphics/vulkan.zig"); -pub const c = @cImport({ - @cInclude("glad/gl.h"); - - // NOTE(blackedout): glad is currently not used on macOS, so use Vulkan header from the Vulkan-Headers repository instead - if (builtin.target.os.tag == .macos) { - @cInclude("vulkan/vulkan.h"); - @cInclude("vulkan/vulkan_beta.h"); - } else { - @cInclude("glad/vulkan.h"); - } - @cInclude("GLFW/glfw3.h"); -}); - -pub const stb_image = @cImport({ - @cDefine("_BITS_STDIO2_H", ""); // TODO: Zig fails to include this header file - @cInclude("stb/stb_image.h"); - @cInclude("stb/stb_image_write.h"); -}); - pub const draw = struct { // MARK: draw var color: u32 = 0; var clip: ?Vec4i = null; @@ -596,7 +564,7 @@ pub const TextBuffer = struct { // MARK: TextBuffer alignment: Alignment, width: f32, - buffer: ?*hbft.hb_buffer_t, + buffer: ?*c.hb_buffer_t, glyphs: []GlyphData, lines: main.List(Line), lineBreaks: main.List(LineBreak), @@ -797,19 +765,19 @@ pub const TextBuffer = struct { // MARK: TextBuffer } // Let harfbuzz do its thing: - const buffer = hbft.hb_buffer_create() orelse @panic("Out of Memory while creating harfbuzz buffer"); - defer hbft.hb_buffer_destroy(buffer); - hbft.hb_buffer_add_utf32(buffer, parser.parsedText.items.ptr, @intCast(parser.parsedText.items.len), 0, @intCast(parser.parsedText.items.len)); - hbft.hb_buffer_set_direction(buffer, hbft.HB_DIRECTION_LTR); - hbft.hb_buffer_set_script(buffer, hbft.HB_SCRIPT_COMMON); - hbft.hb_buffer_set_language(buffer, hbft.hb_language_get_default()); - hbft.hb_shape(TextRendering.harfbuzzFont, buffer, null, 0); - var glyphInfos: []hbft.hb_glyph_info_t = undefined; - var glyphPositions: []hbft.hb_glyph_position_t = undefined; + const buffer = c.hb_buffer_create() orelse @panic("Out of Memory while creating harfbuzz buffer"); + defer c.hb_buffer_destroy(buffer); + c.hb_buffer_add_utf32(buffer, parser.parsedText.items.ptr, @intCast(parser.parsedText.items.len), 0, @intCast(parser.parsedText.items.len)); + c.hb_buffer_set_direction(buffer, c.HB_DIRECTION_LTR); + c.hb_buffer_set_script(buffer, c.HB_SCRIPT_COMMON); + c.hb_buffer_set_language(buffer, c.hb_language_get_default()); + c.hb_shape(TextRendering.harfbuzzFont, buffer, null, 0); + var glyphInfos: []c.hb_glyph_info_t = undefined; + var glyphPositions: []c.hb_glyph_position_t = undefined; { var len: c_uint = 0; - glyphInfos.ptr = hbft.hb_buffer_get_glyph_infos(buffer, &len).?; - glyphPositions.ptr = hbft.hb_buffer_get_glyph_positions(buffer, &len).?; + glyphInfos.ptr = c.hb_buffer_get_glyph_infos(buffer, &len).?; + glyphPositions.ptr = c.hb_buffer_get_glyph_positions(buffer, &len).?; glyphInfos.len = len; glyphPositions.len = len; } @@ -1131,10 +1099,10 @@ const TextRendering = struct { // MARK: TextRendering alpha: c_int, } = undefined; - var freetypeLib: hbft.FT_Library = undefined; - var freetypeFace: hbft.FT_Face = undefined; - var harfbuzzFace: ?*hbft.hb_face_t = undefined; - var harfbuzzFont: ?*hbft.hb_font_t = undefined; + var freetypeLib: c.FT_Library = undefined; + var freetypeFace: c.FT_Face = undefined; + var harfbuzzFace: ?*c.hb_face_t = undefined; + var harfbuzzFont: ?*c.hb_font_t = undefined; var glyphMapping: main.List(u31) = undefined; var glyphData: main.List(Glyph) = undefined; var glyphTexture: [2]c_uint = undefined; @@ -1143,9 +1111,9 @@ const TextRendering = struct { // MARK: TextRendering var textureOffset: i32 = 0; var fontUnitsPerPixel: f32 = undefined; - fn ftError(errorCode: hbft.FT_Error) !void { + fn ftError(errorCode: c.FT_Error) !void { if (errorCode == 0) return; - const errorString = hbft.FT_Error_String(errorCode); + const errorString = c.FT_Error_String(errorCode); std.log.err("Got freetype error {s}", .{errorString}); return error.freetype; } @@ -1166,11 +1134,11 @@ const TextRendering = struct { // MARK: TextRendering errdefer pipeline.deinit(); c.glUniform1f(uniforms.alpha, 1.0); c.glUniform2f(uniforms.fontSize, @floatFromInt(textureWidth), @floatFromInt(textureHeight)); - try ftError(hbft.FT_Init_FreeType(&freetypeLib)); - try ftError(hbft.FT_New_Face(freetypeLib, "assets/cubyz/fonts/unscii-16-full.ttf", 0, &freetypeFace)); - try ftError(hbft.FT_Set_Pixel_Sizes(freetypeFace, 0, textureHeight)); - harfbuzzFace = hbft.hb_ft_face_create_referenced(freetypeFace); - harfbuzzFont = hbft.hb_font_create(harfbuzzFace); + try ftError(c.FT_Init_FreeType(&freetypeLib)); + try ftError(c.FT_New_Face(freetypeLib, "assets/cubyz/fonts/unscii-16-full.ttf", 0, &freetypeFace)); + try ftError(c.FT_Set_Pixel_Sizes(freetypeFace, 0, textureHeight)); + harfbuzzFace = c.hb_ft_face_create_referenced(freetypeFace); + harfbuzzFont = c.hb_font_create(harfbuzzFace); fontUnitsPerPixel = @as(f32, @floatFromInt(freetypeFace.*.units_per_EM))/@as(f32, @floatFromInt(textureHeight)); glyphMapping = .init(main.globalAllocator); @@ -1192,11 +1160,11 @@ const TextRendering = struct { // MARK: TextRendering fn deinit() void { pipeline.deinit(); - ftError(hbft.FT_Done_FreeType(freetypeLib)) catch {}; + ftError(c.FT_Done_FreeType(freetypeLib)) catch {}; glyphMapping.deinit(); glyphData.deinit(); c.glDeleteTextures(2, &glyphTexture); - hbft.hb_font_destroy(harfbuzzFont); + c.hb_font_destroy(harfbuzzFont); } fn resizeTexture(newWidth: i32) void { @@ -1212,7 +1180,7 @@ const TextRendering = struct { // MARK: TextRendering c.glUniform2f(uniforms.fontSize, @floatFromInt(textureWidth), @floatFromInt(textureHeight)); } - fn uploadData(bitmap: hbft.FT_Bitmap) void { + fn uploadData(bitmap: c.FT_Bitmap) void { const width: i32 = @bitCast(bitmap.width); const height: i32 = @bitCast(bitmap.rows); const buffer = bitmap.buffer orelse return; @@ -1229,7 +1197,7 @@ const TextRendering = struct { // MARK: TextRendering glyphMapping.appendNTimes(0, index - glyphMapping.items.len + 1); } if (glyphMapping.items[index] == 0) { // glyph was not initialized yet. - try ftError(hbft.FT_Load_Glyph(freetypeFace, index, hbft.FT_LOAD_RENDER)); + try ftError(c.FT_Load_Glyph(freetypeFace, index, c.FT_LOAD_RENDER)); const glyph = freetypeFace.*.glyph; const bitmap = glyph.*.bitmap; const width = bitmap.width; @@ -2122,13 +2090,13 @@ pub const Image = struct { // MARK: Image var channel: c_int = undefined; const nullTerminatedPath = main.stackAllocator.dupeZ(u8, path); // TODO: Find a more zig-friendly image loading library. errdefer main.stackAllocator.free(nullTerminatedPath); - stb_image.stbi_set_flip_vertically_on_load(1); - const data = stb_image.stbi_load(nullTerminatedPath.ptr, @ptrCast(&result.width), @ptrCast(&result.height), &channel, 4) orelse { + c.stbi_set_flip_vertically_on_load(1); + const data = c.stbi_load(nullTerminatedPath.ptr, @ptrCast(&result.width), @ptrCast(&result.height), &channel, 4) orelse { return error.FileNotFound; }; main.stackAllocator.free(nullTerminatedPath); result.imageData = allocator.dupe(Color, @as([*]Color, @ptrCast(data))[0 .. result.width*result.height]); - stb_image.stbi_image_free(data); + c.stbi_image_free(data); return result; } pub fn readUnflippedFromFile(allocator: NeverFailingAllocator, path: []const u8) !Image { @@ -2136,18 +2104,18 @@ pub const Image = struct { // MARK: Image var channel: c_int = undefined; const nullTerminatedPath = main.stackAllocator.dupeZ(u8, path); // TODO: Find a more zig-friendly image loading library. errdefer main.stackAllocator.free(nullTerminatedPath); - const data = stb_image.stbi_load(nullTerminatedPath.ptr, @ptrCast(&result.width), @ptrCast(&result.height), &channel, 4) orelse { + const data = c.stbi_load(nullTerminatedPath.ptr, @ptrCast(&result.width), @ptrCast(&result.height), &channel, 4) orelse { return error.FileNotFound; }; main.stackAllocator.free(nullTerminatedPath); result.imageData = allocator.dupe(Color, @as([*]Color, @ptrCast(data))[0 .. result.width*result.height]); - stb_image.stbi_image_free(data); + c.stbi_image_free(data); return result; } pub fn exportToFile(self: Image, path: []const u8) !void { const nullTerminated = main.stackAllocator.dupeZ(u8, path); defer main.stackAllocator.free(nullTerminated); - _ = stb_image.stbi_write_png(nullTerminated.ptr, self.width, self.height, 4, self.imageData.ptr, self.width*4); // TODO: Handle the return type. + _ = c.stbi_write_png(nullTerminated.ptr, self.width, self.height, 4, self.imageData.ptr, self.width*4); // TODO: Handle the return type. } pub fn getRGB(self: Image, x: usize, y: usize) Color { std.debug.assert(x < self.width); From 91450697c2fcc9d118342427ae6005e3565d8866 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Fernandes?= Date: Wed, 6 May 2026 19:50:00 +0100 Subject: [PATCH 02/14] Add macOS comment to c.h --- src/c.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/c.h b/src/c.h index d25aead3cf..1994643cde 100644 --- a/src/c.h +++ b/src/c.h @@ -12,6 +12,7 @@ #include #include +// NOTE(blackedout): glad is currently not used on macOS, so use Vulkan header from the Vulkan-Headers repository instead #ifdef __MACH__ #include #include From 48d21641c5d0d3535197cc8c5f4f4b0770ab7a5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Fernandes?= Date: Wed, 6 May 2026 20:05:24 +0100 Subject: [PATCH 03/14] Add comments to each set of c.h includes --- src/c.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/c.h b/src/c.h index 1994643cde..e465f1b7f9 100644 --- a/src/c.h +++ b/src/c.h @@ -1,4 +1,4 @@ -// graphics.zig +// used for text rendering #include #include #include @@ -11,6 +11,7 @@ #include #include +// used for rendering graphics and windows #include // NOTE(blackedout): glad is currently not used on macOS, so use Vulkan header from the Vulkan-Headers repository instead #ifdef __MACH__ @@ -21,5 +22,6 @@ #endif #include +// used for exporting and reading images to/from files #include #include From 42dc73919f79afff2df52b92f9adaa19ff5a4438 Mon Sep 17 00:00:00 2001 From: AfmanS <76686739+AfmanS@users.noreply.github.com> Date: Thu, 7 May 2026 15:47:33 +0100 Subject: [PATCH 04/14] Update src/c.h Co-authored-by: IntegratedQuantum <43880493+IntegratedQuantum@users.noreply.github.com> --- src/c.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/c.h b/src/c.h index e465f1b7f9..a0deccc8c9 100644 --- a/src/c.h +++ b/src/c.h @@ -11,7 +11,7 @@ #include #include -// used for rendering graphics and windows +// used for rendering, windowing and inputs #include // NOTE(blackedout): glad is currently not used on macOS, so use Vulkan header from the Vulkan-Headers repository instead #ifdef __MACH__ From f6248e6bdac3cbb8760bc644fb9135a271fa9635 Mon Sep 17 00:00:00 2001 From: AfmanS <76686739+AfmanS@users.noreply.github.com> Date: Thu, 7 May 2026 15:47:41 +0100 Subject: [PATCH 05/14] Update src/c.h Co-authored-by: IntegratedQuantum <43880493+IntegratedQuantum@users.noreply.github.com> --- src/c.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/c.h b/src/c.h index a0deccc8c9..07101aac83 100644 --- a/src/c.h +++ b/src/c.h @@ -1,4 +1,4 @@ -// used for text rendering +// used for text rendering and layouting #include #include #include From 82b1b100c5305909d88f14bc6059e40f899760f3 Mon Sep 17 00:00:00 2001 From: AfmanS <76686739+AfmanS@users.noreply.github.com> Date: Thu, 7 May 2026 15:48:10 +0100 Subject: [PATCH 06/14] Update src/c.h Co-authored-by: IntegratedQuantum <43880493+IntegratedQuantum@users.noreply.github.com> --- src/c.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/c.h b/src/c.h index 07101aac83..abc5c68c0f 100644 --- a/src/c.h +++ b/src/c.h @@ -22,6 +22,6 @@ #endif #include -// used for exporting and reading images to/from files +// used for reading and writing image files #include #include From ee1bb7fa69e5ecb7265f3e7158b79365f9b8aa17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Fernandes?= Date: Thu, 7 May 2026 16:11:02 +0100 Subject: [PATCH 07/14] Replace all occurences of graphics.c and Window.c with @import("c") --- src/block_entity.zig | 2 +- src/blocks.zig | 11 ++++++----- src/client/Entity.zig | 2 +- src/client/entity_manager.zig | 2 +- src/entityComponent/_template.zig | 2 +- src/entityComponent/bag.zig | 2 +- src/entityComponent/model.zig | 2 +- src/entityModel.zig | 2 +- src/entitySystem/_template.zig | 2 +- src/entitySystem/modelRenderer.zig | 2 +- src/graphics.zig | 2 +- src/graphics/Window.zig | 2 +- src/graphics/pipelines.zig | 2 +- src/graphics/vulkan.zig | 2 +- src/gui/gui.zig | 3 ++- src/gui/windows/controls.zig | 2 +- src/gui/windows/crosshair.zig | 2 +- src/gui/windows/gpu_performance_measuring.zig | 2 +- src/gui/windows/main.zig | 3 ++- src/gui/windows/performance_graph.zig | 15 ++++++++------- src/itemdrop.zig | 2 +- src/main.zig | 4 ++-- src/particles.zig | 2 +- src/renderer.zig | 2 +- src/renderer/chunk_meshing.zig | 2 +- 25 files changed, 40 insertions(+), 36 deletions(-) diff --git a/src/block_entity.zig b/src/block_entity.zig index c2b41b612b..1b47b943bf 100644 --- a/src/block_entity.zig +++ b/src/block_entity.zig @@ -6,7 +6,7 @@ const Chunk = main.chunk.Chunk; const ChunkPosition = main.chunk.ChunkPosition; const getIndex = main.chunk.getIndex; const graphics = main.graphics; -const c = graphics.c; +const c = @import("c"); const server = main.server; const User = server.User; const mesh_storage = main.renderer.mesh_storage; diff --git a/src/blocks.zig b/src/blocks.zig index 1eb44b6174..f00f58047a 100644 --- a/src/blocks.zig +++ b/src/blocks.zig @@ -803,10 +803,11 @@ pub const meshes = struct { // MARK: meshes pub fn preProcessAnimationData(time: u32) void { animationComputePipeline.bind(); - graphics.c.glUniform1ui(animationUniforms.time, time); - graphics.c.glUniform1ui(animationUniforms.size, @intCast(animationData.len)); - graphics.c.glDispatchCompute(@intCast(@divFloor(animationData.len + 63, 64)), 1, 1); // TODO: Replace with @divCeil once available - graphics.c.glMemoryBarrier(graphics.c.GL_SHADER_STORAGE_BARRIER_BIT); + const c = @import("c"); + c.glUniform1ui(animationUniforms.time, time); + c.glUniform1ui(animationUniforms.size, @intCast(animationData.len)); + c.glDispatchCompute(@intCast(@divFloor(animationData.len + 63, 64)), 1, 1); // TODO: Replace with @divCeil once available + c.glMemoryBarrier(c.GL_SHADER_STORAGE_BARRIER_BIT); } fn finishTextureLoading() void { @@ -830,7 +831,7 @@ pub const meshes = struct { // MARK: meshes } pub fn generateTextureArray() void { - const c = graphics.c; + const c = @import("c"); blockTextureArray.generate(blockTextures.items, true, true); c.glTexParameterf(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MAX_ANISOTROPY, @floatFromInt(main.settings.anisotropicFiltering)); emissionTextureArray.generate(emissionTextures.items, true, false); diff --git a/src/client/Entity.zig b/src/client/Entity.zig index 82552c91e7..c208fb9b52 100644 --- a/src/client/Entity.zig +++ b/src/client/Entity.zig @@ -4,7 +4,7 @@ const main = @import("main"); const chunk = main.chunk; const game = main.game; const graphics = main.graphics; -const c = graphics.c; +const c = @import("c"); const ZonElement = main.ZonElement; const renderer = main.renderer; const settings = main.settings; diff --git a/src/client/entity_manager.zig b/src/client/entity_manager.zig index 3801d35382..99e0826d91 100644 --- a/src/client/entity_manager.zig +++ b/src/client/entity_manager.zig @@ -4,7 +4,7 @@ const main = @import("main"); const chunk = main.chunk; const game = main.game; const graphics = main.graphics; -const c = graphics.c; +const c = @import("c"); const ZonElement = main.ZonElement; const renderer = main.renderer; const settings = main.settings; diff --git a/src/entityComponent/_template.zig b/src/entityComponent/_template.zig index cbf271e17a..6958ea09d0 100644 --- a/src/entityComponent/_template.zig +++ b/src/entityComponent/_template.zig @@ -4,7 +4,7 @@ const main = @import("main"); const chunk = main.chunk; const game = main.game; const graphics = main.graphics; -const c = graphics.c; +const c = @import("c"); const ZonElement = main.ZonElement; const renderer = main.renderer; const settings = main.settings; diff --git a/src/entityComponent/bag.zig b/src/entityComponent/bag.zig index 81c1a00836..607051bb32 100644 --- a/src/entityComponent/bag.zig +++ b/src/entityComponent/bag.zig @@ -4,7 +4,7 @@ const main = @import("main"); const chunk = main.chunk; const game = main.game; const graphics = main.graphics; -const c = graphics.c; +const c = @import("c"); const ZonElement = main.ZonElement; const renderer = main.renderer; const settings = main.settings; diff --git a/src/entityComponent/model.zig b/src/entityComponent/model.zig index ad81c8e140..4788cd8c9c 100644 --- a/src/entityComponent/model.zig +++ b/src/entityComponent/model.zig @@ -4,7 +4,7 @@ const main = @import("main"); const chunk = main.chunk; const game = main.game; const graphics = main.graphics; -const c = graphics.c; +const c = @import("c"); const ZonElement = main.ZonElement; const renderer = main.renderer; const settings = main.settings; diff --git a/src/entityModel.zig b/src/entityModel.zig index 7927e82d34..4dc7019179 100644 --- a/src/entityModel.zig +++ b/src/entityModel.zig @@ -4,7 +4,7 @@ const main = @import("main"); const chunk = main.chunk; const game = main.game; const graphics = main.graphics; -const c = graphics.c; +const c = @import("c"); const ZonElement = main.ZonElement; const renderer = main.renderer; const settings = main.settings; diff --git a/src/entitySystem/_template.zig b/src/entitySystem/_template.zig index b46aaa17a8..bda016dfa5 100644 --- a/src/entitySystem/_template.zig +++ b/src/entitySystem/_template.zig @@ -4,7 +4,7 @@ const main = @import("main"); const chunk = main.chunk; const game = main.game; const graphics = main.graphics; -const c = graphics.c; +const c = @import("c"); const ZonElement = main.ZonElement; const renderer = main.renderer; const settings = main.settings; diff --git a/src/entitySystem/modelRenderer.zig b/src/entitySystem/modelRenderer.zig index 1ad0de33c5..fa638bb21b 100644 --- a/src/entitySystem/modelRenderer.zig +++ b/src/entitySystem/modelRenderer.zig @@ -4,7 +4,7 @@ const main = @import("main"); const chunk = main.chunk; const game = main.game; const graphics = main.graphics; -const c = graphics.c; +const c = @import("c"); const ZonElement = main.ZonElement; const renderer = main.renderer; const settings = main.settings; diff --git a/src/graphics.zig b/src/graphics.zig index 2fcff2e18d..7b6bdf65bc 100644 --- a/src/graphics.zig +++ b/src/graphics.zig @@ -3,7 +3,7 @@ const std = @import("std"); const builtin = @import("builtin"); -pub const c = @import("c"); +const c = @import("c"); const vec = @import("vec.zig"); const Mat4f = vec.Mat4f; diff --git a/src/graphics/Window.zig b/src/graphics/Window.zig index f3d8b2f7d4..4f3076ce9f 100644 --- a/src/graphics/Window.zig +++ b/src/graphics/Window.zig @@ -9,7 +9,7 @@ const Vec2f = vec.Vec2f; const vulkan = @import("vulkan.zig"); -pub const c = main.graphics.c; +const c = @import("c"); var isFullscreen: bool = false; pub var lastUsedMouse: bool = true; diff --git a/src/graphics/pipelines.zig b/src/graphics/pipelines.zig index e103cbce38..ae8b31670b 100644 --- a/src/graphics/pipelines.zig +++ b/src/graphics/pipelines.zig @@ -2,7 +2,7 @@ const std = @import("std"); const main = @import("main"); const graphics = main.graphics; -const c = graphics.c; +const c = @import("c"); const vulkan = graphics.vulkan; const NeverFailingAllocator = main.heap.NeverFailingAllocator; diff --git a/src/graphics/vulkan.zig b/src/graphics/vulkan.zig index a5867e4ff1..8d56474a01 100644 --- a/src/graphics/vulkan.zig +++ b/src/graphics/vulkan.zig @@ -2,7 +2,7 @@ const std = @import("std"); const builtin = @import("builtin"); const main = @import("main"); -const c = main.graphics.c; +const c = @import("c"); const NeverFailingAllocator = main.heap.NeverFailingAllocator; comptime { diff --git a/src/gui/gui.zig b/src/gui/gui.zig index 769145c205..253d5d36a8 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -556,9 +556,10 @@ pub fn updateAndRenderGui() void { } if (!hideGui) { if (!main.Window.grabbed) { + const c = @import("c"); draw.setColor(0x80000000); GuiWindow.borderPipeline.bind(draw.getScissor()); - graphics.c.glUniform2f(GuiWindow.borderUniforms.effectLength, main.Window.getWindowSize()[0]/6, main.Window.getWindowSize()[1]/6); + c.glUniform2f(GuiWindow.borderUniforms.effectLength, main.Window.getWindowSize()[0]/6, main.Window.getWindowSize()[1]/6); draw.customShadedRect(GuiWindow.borderUniforms, .{0, 0}, main.Window.getWindowSize()); } const oldScale = draw.setScale(scale); diff --git a/src/gui/windows/controls.zig b/src/gui/windows/controls.zig index 8be2435fca..ada83e3965 100644 --- a/src/gui/windows/controls.zig +++ b/src/gui/windows/controls.zig @@ -2,7 +2,7 @@ const std = @import("std"); const main = @import("main"); const Vec2f = main.vec.Vec2f; -const c = main.Window.c; +const c = @import("c"); const gui = @import("../gui.zig"); const GuiComponent = gui.GuiComponent; const GuiWindow = gui.GuiWindow; diff --git a/src/gui/windows/crosshair.zig b/src/gui/windows/crosshair.zig index b57a7e6f77..5eb1482627 100644 --- a/src/gui/windows/crosshair.zig +++ b/src/gui/windows/crosshair.zig @@ -9,7 +9,7 @@ const gui = @import("../gui.zig"); const GuiWindow = gui.GuiWindow; const GuiComponent = gui.GuiComponent; -const c = main.graphics.c; +const c = @import("c"); const size: f32 = 64; pub var window = GuiWindow{ diff --git a/src/gui/windows/gpu_performance_measuring.zig b/src/gui/windows/gpu_performance_measuring.zig index ad917efe0b..530de75471 100644 --- a/src/gui/windows/gpu_performance_measuring.zig +++ b/src/gui/windows/gpu_performance_measuring.zig @@ -1,7 +1,7 @@ const std = @import("std"); const main = @import("main"); -const c = main.Window.c; +const c = @import("c"); const graphics = main.graphics; const draw = graphics.draw; const Texture = graphics.Texture; diff --git a/src/gui/windows/main.zig b/src/gui/windows/main.zig index 49fdfde465..fea68e928e 100644 --- a/src/gui/windows/main.zig +++ b/src/gui/windows/main.zig @@ -17,7 +17,8 @@ pub var window = GuiWindow{ const padding: f32 = 8; fn exitGame() void { - main.Window.c.glfwSetWindowShouldClose(main.Window.window, main.Window.c.GLFW_TRUE); + const c = @import("c"); + c.glfwSetWindowShouldClose(main.Window.window, c.GLFW_TRUE); } pub fn onOpen() void { const list = VerticalList.init(.{padding, 16 + padding}, 300, 16); diff --git a/src/gui/windows/performance_graph.zig b/src/gui/windows/performance_graph.zig index 312899fe8b..8fb7414110 100644 --- a/src/gui/windows/performance_graph.zig +++ b/src/gui/windows/performance_graph.zig @@ -59,6 +59,7 @@ pub fn deinit() void { } pub fn render() void { + const c = @import("c"); lastFrameTime[index] = @floatCast(main.lastFrameTime.load(.monotonic)*1000.0); index = (index + 1)%@as(u31, @intCast(lastFrameTime.len)); draw.setColor(0xffffffff); @@ -71,9 +72,9 @@ pub fn render() void { draw.line(.{border, 56}, .{window.contentSize[0] - border, 56}); draw.setColor(0xffffffff); pipeline.bind(null); - graphics.c.glUniform1i(uniforms.points, lastFrameTime.len); - graphics.c.glUniform1i(uniforms.offset, index); - graphics.c.glUniform3f(uniforms.lineColor, 1, 1, 1); + c.glUniform1i(uniforms.points, lastFrameTime.len); + c.glUniform1i(uniforms.offset, index); + c.glUniform3f(uniforms.lineColor, 1, 1, 1); var pos = Vec2f{border, border}; var dim = window.contentSize - @as(Vec2f, @splat(2*border)); pos *= @splat(draw.setScale(1)); @@ -83,10 +84,10 @@ pub fn render() void { dim = @ceil(dim); pos[1] += dim[1]; - graphics.c.glUniform2f(uniforms.screen, @floatFromInt(main.Window.width), @floatFromInt(main.Window.height)); - graphics.c.glUniform2f(uniforms.start, pos[0], pos[1]); - graphics.c.glUniform2f(uniforms.dimension, dim[0], draw.setScale(1)); + c.glUniform2f(uniforms.screen, @floatFromInt(main.Window.width), @floatFromInt(main.Window.height)); + c.glUniform2f(uniforms.start, pos[0], pos[1]); + c.glUniform2f(uniforms.dimension, dim[0], draw.setScale(1)); ssbo.bufferData(f32, &lastFrameTime); ssbo.bind(5); - graphics.c.glDrawArrays(graphics.c.GL_LINE_STRIP, 0, lastFrameTime.len); + c.glDrawArrays(c.GL_LINE_STRIP, 0, lastFrameTime.len); } diff --git a/src/itemdrop.zig b/src/itemdrop.zig index eb76cdc563..943e14f916 100644 --- a/src/itemdrop.zig +++ b/src/itemdrop.zig @@ -7,7 +7,7 @@ const game = @import("game.zig"); const World = game.World; const ServerWorld = main.server.ServerWorld; const graphics = @import("graphics.zig"); -const c = graphics.c; +const c = @import("c"); const items = @import("items.zig"); const ItemStack = items.ItemStack; const ZonElement = @import("zon.zig").ZonElement; diff --git a/src/main.zig b/src/main.zig index 0444fbddbb..1f5cac60ca 100644 --- a/src/main.zig +++ b/src/main.zig @@ -283,7 +283,7 @@ fn setHotbarSlot(i: comptime_int) *const fn (Window.Key.Modifiers) void { } pub const KeyBoard = struct { // MARK: KeyBoard - const c = Window.c; + const c = @import("c"); pub var keys = [_]Window.Key{ // Gameplay: .{.name = "forward", .key = c.GLFW_KEY_W, .gamepadAxis = .{.axis = c.GLFW_GAMEPAD_AXIS_LEFT_Y, .positive = false}}, @@ -562,7 +562,7 @@ pub fn clientMain() void { // MARK: clientMain() }, } - const c = Window.c; + const c = @import("c"); Window.GLFWCallbacks.framebufferSize(undefined, Window.width, Window.height); var lastBeginRendering = timestamp(); diff --git a/src/particles.zig b/src/particles.zig index 564f2caf6e..82252525a2 100644 --- a/src/particles.zig +++ b/src/particles.zig @@ -8,7 +8,7 @@ const SSBO = graphics.SSBO; const TextureArray = graphics.TextureArray; const Shader = graphics.Shader; const Image = graphics.Image; -const c = graphics.c; +const c = @import("c"); const game = @import("game.zig"); const ZonElement = @import("zon.zig").ZonElement; const random = @import("random.zig"); diff --git a/src/renderer.zig b/src/renderer.zig index 3c7510b405..86bd00595b 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -6,7 +6,7 @@ const chunk = @import("chunk.zig"); const entity = @import("entity.zig"); const graphics = @import("graphics.zig"); const particles = @import("particles.zig"); -const c = graphics.c; +const c = @import("c"); const game = @import("game.zig"); const World = game.World; const itemdrop = @import("itemdrop.zig"); diff --git a/src/renderer/chunk_meshing.zig b/src/renderer/chunk_meshing.zig index aca9f49676..78b9f2447e 100644 --- a/src/renderer/chunk_meshing.zig +++ b/src/renderer/chunk_meshing.zig @@ -11,7 +11,7 @@ const models = main.models; const QuadIndex = models.QuadIndex; const renderer = main.renderer; const graphics = main.graphics; -const c = graphics.c; +const c = @import("c"); const SSBO = graphics.SSBO; const lighting = @import("lighting.zig"); const settings = main.settings; From c2d631cc0081fc104f91da3ffb0a71ee51f77c59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Fernandes?= Date: Thu, 7 May 2026 17:06:49 +0100 Subject: [PATCH 08/14] Reorder imports in Entity.zig --- src/client/Entity.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/Entity.zig b/src/client/Entity.zig index c208fb9b52..49d719ff83 100644 --- a/src/client/Entity.zig +++ b/src/client/Entity.zig @@ -4,11 +4,11 @@ const main = @import("main"); const chunk = main.chunk; const game = main.game; const graphics = main.graphics; -const c = @import("c"); const ZonElement = main.ZonElement; const renderer = main.renderer; const settings = main.settings; const utils = main.utils; +const BinaryReader = utils.BinaryReader; const vec = main.vec; const Mat4f = vec.Mat4f; const Vec3d = vec.Vec3d; @@ -16,7 +16,7 @@ const Vec3f = vec.Vec3f; const Vec4f = vec.Vec4f; const NeverFailingAllocator = main.heap.NeverFailingAllocator; -const BinaryReader = main.utils.BinaryReader; +const c = @import("c"); interpolatedValues: utils.GenericInterpolation(6) = undefined, _interpolationPos: [6]f64 = undefined, From d4eed31f6571a27d71e69dc191be27f7a9574e97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Fernandes?= Date: Thu, 7 May 2026 18:07:18 +0100 Subject: [PATCH 09/14] Relocate C imports to top of files and reorder imports --- src/block_entity.zig | 3 ++- src/blocks.zig | 25 ++++++++++++------- src/client/entity_manager.zig | 4 +-- src/entityComponent/_template.zig | 11 ++++---- src/entityComponent/bag.zig | 11 ++++---- src/entityComponent/model.zig | 4 +-- src/entityModel.zig | 4 +-- src/entitySystem/_template.zig | 11 ++++---- src/entitySystem/modelRenderer.zig | 12 ++++----- src/graphics.zig | 2 +- src/graphics/pipelines.zig | 3 ++- src/graphics/vulkan.zig | 3 ++- src/gui/gui.zig | 4 +-- src/gui/windows/controls.zig | 5 +++- src/gui/windows/gpu_performance_measuring.zig | 3 ++- src/gui/windows/main.zig | 4 ++- src/gui/windows/performance_graph.zig | 3 ++- src/itemdrop.zig | 19 ++++++++++---- src/main.zig | 3 +-- src/particles.zig | 4 +++ src/renderer.zig | 9 +++++-- src/renderer/chunk_meshing.zig | 3 ++- 22 files changed, 90 insertions(+), 60 deletions(-) diff --git a/src/block_entity.zig b/src/block_entity.zig index 1b47b943bf..5e6aa48872 100644 --- a/src/block_entity.zig +++ b/src/block_entity.zig @@ -6,7 +6,6 @@ const Chunk = main.chunk.Chunk; const ChunkPosition = main.chunk.ChunkPosition; const getIndex = main.chunk.getIndex; const graphics = main.graphics; -const c = @import("c"); const server = main.server; const User = server.User; const mesh_storage = main.renderer.mesh_storage; @@ -18,6 +17,8 @@ const Vec3d = vec.Vec3d; const Vec3f = vec.Vec3f; const Vec3i = vec.Vec3i; +const c = @import("c"); + const UpdateEvent = union(enum) { remove: void, update: *BinaryReader, diff --git a/src/blocks.zig b/src/blocks.zig index f00f58047a..25b1e4ec82 100644 --- a/src/blocks.zig +++ b/src/blocks.zig @@ -3,31 +3,40 @@ const std = @import("std"); const main = @import("main"); const Tag = main.Tag; const utils = main.utils; +const ClientBlockCallback = main.callbacks.ClientBlockCallback; +const ServerBlockCallback = main.callbacks.ServerBlockCallback; +const BlockTouchCallback = main.callbacks.BlockTouchCallback; +const sbb = main.server.terrain.structure_building_blocks; +const blueprint = main.blueprint; +const Assets = main.assets.Assets; +const Entity = main.server.Entity; + +const c = @import("c"); + const ZonElement = @import("zon.zig").ZonElement; + const chunk = @import("chunk.zig"); const Neighbor = chunk.Neighbor; const Chunk = chunk.Chunk; + const graphics = @import("graphics.zig"); const SSBO = graphics.SSBO; const Image = graphics.Image; const Color = graphics.Color; const TextureArray = graphics.TextureArray; + const items = @import("items.zig"); const Item = items.Item; + const models = @import("models.zig"); const ModelIndex = models.ModelIndex; + const rotation = @import("rotation.zig"); const RotationMode = rotation.RotationMode; const Degrees = rotation.Degrees; -const Entity = main.server.Entity; + const block_entity = @import("block_entity.zig"); const BlockEntityType = block_entity.BlockEntityType; -const ClientBlockCallback = main.callbacks.ClientBlockCallback; -const ServerBlockCallback = main.callbacks.ServerBlockCallback; -const BlockTouchCallback = main.callbacks.BlockTouchCallback; -const sbb = main.server.terrain.structure_building_blocks; -const blueprint = main.blueprint; -const Assets = main.assets.Assets; pub const maxBlockCount: usize = 65536; // 16 bit limit @@ -803,7 +812,6 @@ pub const meshes = struct { // MARK: meshes pub fn preProcessAnimationData(time: u32) void { animationComputePipeline.bind(); - const c = @import("c"); c.glUniform1ui(animationUniforms.time, time); c.glUniform1ui(animationUniforms.size, @intCast(animationData.len)); c.glDispatchCompute(@intCast(@divFloor(animationData.len + 63, 64)), 1, 1); // TODO: Replace with @divCeil once available @@ -831,7 +839,6 @@ pub const meshes = struct { // MARK: meshes } pub fn generateTextureArray() void { - const c = @import("c"); blockTextureArray.generate(blockTextures.items, true, true); c.glTexParameterf(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MAX_ANISOTROPY, @floatFromInt(main.settings.anisotropicFiltering)); emissionTextureArray.generate(emissionTextures.items, true, false); diff --git a/src/client/entity_manager.zig b/src/client/entity_manager.zig index 99e0826d91..b5e58d1fed 100644 --- a/src/client/entity_manager.zig +++ b/src/client/entity_manager.zig @@ -4,11 +4,11 @@ const main = @import("main"); const chunk = main.chunk; const game = main.game; const graphics = main.graphics; -const c = @import("c"); const ZonElement = main.ZonElement; const renderer = main.renderer; const settings = main.settings; const utils = main.utils; +const BinaryReader = utils.BinaryReader; const vec = main.vec; const Mat4f = vec.Mat4f; const Vec3d = vec.Vec3d; @@ -16,7 +16,7 @@ const Vec3f = vec.Vec3f; const Vec4f = vec.Vec4f; const NeverFailingAllocator = main.heap.NeverFailingAllocator; -const BinaryReader = main.utils.BinaryReader; +const c = @import("c"); var lastTime: i16 = 0; var timeDifference: utils.TimeDifference = utils.TimeDifference{}; diff --git a/src/entityComponent/_template.zig b/src/entityComponent/_template.zig index 6958ea09d0..9f524fb423 100644 --- a/src/entityComponent/_template.zig +++ b/src/entityComponent/_template.zig @@ -3,12 +3,14 @@ const std = @import("std"); const main = @import("main"); const chunk = main.chunk; const game = main.game; +const World = game.World; const graphics = main.graphics; -const c = @import("c"); const ZonElement = main.ZonElement; const renderer = main.renderer; const settings = main.settings; const utils = main.utils; +const BinaryReader = utils.BinaryReader; +const BinaryWriter = utils.BinaryWriter; const vec = main.vec; const Mat4f = vec.Mat4f; const Vec3d = vec.Vec3d; @@ -16,19 +18,16 @@ const Vec3f = vec.Vec3f; const Vec4f = vec.Vec4f; const Vec3i = vec.Vec3i; const NeverFailingAllocator = main.heap.NeverFailingAllocator; - -const BinaryReader = main.utils.BinaryReader; -const BinaryWriter = main.utils.BinaryWriter; - const blocks = main.blocks; const chunk_zig = main.chunk; const ServerChunk = chunk_zig.ServerChunk; -const World = game.World; const ServerWorld = main.server.ServerWorld; const items = main.items; const ItemStack = items.ItemStack; const random = main.random; +const c = @import("c"); + pub var entityComponentID: main.entity.EntityComponentId = undefined; pub const entityComponentVersion = 0; diff --git a/src/entityComponent/bag.zig b/src/entityComponent/bag.zig index 607051bb32..99ecf58cb9 100644 --- a/src/entityComponent/bag.zig +++ b/src/entityComponent/bag.zig @@ -3,12 +3,14 @@ const std = @import("std"); const main = @import("main"); const chunk = main.chunk; const game = main.game; +const World = game.World; const graphics = main.graphics; -const c = @import("c"); const ZonElement = main.ZonElement; const renderer = main.renderer; const settings = main.settings; const utils = main.utils; +const BinaryReader = utils.BinaryReader; +const BinaryWriter = utils.BinaryWriter; const vec = main.vec; const Mat4f = vec.Mat4f; const Vec3d = vec.Vec3d; @@ -16,19 +18,16 @@ const Vec3f = vec.Vec3f; const Vec4f = vec.Vec4f; const Vec3i = vec.Vec3i; const NeverFailingAllocator = main.heap.NeverFailingAllocator; - -const BinaryReader = main.utils.BinaryReader; -const BinaryWriter = main.utils.BinaryWriter; - const blocks = main.blocks; const chunk_zig = main.chunk; const ServerChunk = chunk_zig.ServerChunk; -const World = game.World; const ServerWorld = main.server.ServerWorld; const items = main.items; const ItemStack = items.ItemStack; const random = main.random; +const c = @import("c"); + pub var entityComponentID: main.entity.EntityComponentId = undefined; pub const entityComponentVersion = 0; diff --git a/src/entityComponent/model.zig b/src/entityComponent/model.zig index 4788cd8c9c..56dc2bd809 100644 --- a/src/entityComponent/model.zig +++ b/src/entityComponent/model.zig @@ -4,11 +4,11 @@ const main = @import("main"); const chunk = main.chunk; const game = main.game; const graphics = main.graphics; -const c = @import("c"); const ZonElement = main.ZonElement; const renderer = main.renderer; const settings = main.settings; const utils = main.utils; +const BinaryReader = utils.BinaryReader; const vec = main.vec; const Mat4f = vec.Mat4f; const Vec3d = vec.Vec3d; @@ -16,7 +16,7 @@ const Vec3f = vec.Vec3f; const Vec4f = vec.Vec4f; const NeverFailingAllocator = main.heap.NeverFailingAllocator; -const BinaryReader = main.utils.BinaryReader; +const c = @import("c"); pub var entityComponentID: main.entity.EntityComponentId = undefined; pub const entityComponentVersion = 0; diff --git a/src/entityModel.zig b/src/entityModel.zig index 4dc7019179..cc45054182 100644 --- a/src/entityModel.zig +++ b/src/entityModel.zig @@ -4,11 +4,11 @@ const main = @import("main"); const chunk = main.chunk; const game = main.game; const graphics = main.graphics; -const c = @import("c"); const ZonElement = main.ZonElement; const renderer = main.renderer; const settings = main.settings; const utils = main.utils; +const BinaryReader = utils.BinaryReader; const vec = main.vec; const Mat4f = vec.Mat4f; const Vec3d = vec.Vec3d; @@ -16,7 +16,7 @@ const Vec3f = vec.Vec3f; const Vec4f = vec.Vec4f; const NeverFailingAllocator = main.heap.NeverFailingAllocator; -const BinaryReader = main.utils.BinaryReader; +const c = @import("c"); const gltf = @cImport({ @cInclude("cgltf.h"); diff --git a/src/entitySystem/_template.zig b/src/entitySystem/_template.zig index bda016dfa5..e84932959d 100644 --- a/src/entitySystem/_template.zig +++ b/src/entitySystem/_template.zig @@ -3,12 +3,14 @@ const std = @import("std"); const main = @import("main"); const chunk = main.chunk; const game = main.game; +const World = game.World; const graphics = main.graphics; -const c = @import("c"); const ZonElement = main.ZonElement; const renderer = main.renderer; const settings = main.settings; const utils = main.utils; +const BinaryReader = utils.BinaryReader; +const BinaryWriter = utils.BinaryWriter; const vec = main.vec; const Mat4f = vec.Mat4f; const Vec3d = vec.Vec3d; @@ -16,19 +18,16 @@ const Vec3f = vec.Vec3f; const Vec4f = vec.Vec4f; const Vec3i = vec.Vec3i; const NeverFailingAllocator = main.heap.NeverFailingAllocator; - -const BinaryReader = main.utils.BinaryReader; -const BinaryWriter = main.utils.BinaryWriter; - const blocks = main.blocks; const chunk_zig = main.chunk; const ServerChunk = chunk_zig.ServerChunk; -const World = game.World; const ServerWorld = main.server.ServerWorld; const items = main.items; const ItemStack = items.ItemStack; const random = main.random; +const c = @import("c"); + const entityComponent = main.entityComponent; // ############################# Client only stuff ################################ diff --git a/src/entitySystem/modelRenderer.zig b/src/entitySystem/modelRenderer.zig index fa638bb21b..0cc6633b56 100644 --- a/src/entitySystem/modelRenderer.zig +++ b/src/entitySystem/modelRenderer.zig @@ -3,12 +3,14 @@ const std = @import("std"); const main = @import("main"); const chunk = main.chunk; const game = main.game; +const World = game.World; const graphics = main.graphics; -const c = @import("c"); const ZonElement = main.ZonElement; const renderer = main.renderer; const settings = main.settings; const utils = main.utils; +const BinaryReader = utils.BinaryReader; +const BinaryWriter = utils.BinaryWriter; const vec = main.vec; const Mat4f = vec.Mat4f; const Vec3d = vec.Vec3d; @@ -16,21 +18,17 @@ const Vec3f = vec.Vec3f; const Vec4f = vec.Vec4f; const Vec3i = vec.Vec3i; const NeverFailingAllocator = main.heap.NeverFailingAllocator; - -const BinaryReader = main.utils.BinaryReader; -const BinaryWriter = main.utils.BinaryWriter; - const blocks = main.blocks; const chunk_zig = main.chunk; const ServerChunk = chunk_zig.ServerChunk; -const World = game.World; const ServerWorld = main.server.ServerWorld; const items = main.items; const ItemStack = items.ItemStack; const random = main.random; - const entityComponent = main.entity.components; +const c = @import("c"); + // ############################# Client only stuff ################################ pub const client = struct { var pipeline: graphics.Pipeline = undefined; diff --git a/src/graphics.zig b/src/graphics.zig index 7b6bdf65bc..5291297a36 100644 --- a/src/graphics.zig +++ b/src/graphics.zig @@ -15,12 +15,12 @@ const Vec3f = vec.Vec3f; const main = @import("main"); const Window = main.Window; - const NeverFailingAllocator = main.heap.NeverFailingAllocator; const pipelines = @import("graphics/pipelines.zig"); pub const ComputePipeline = pipelines.ComputePipeline; pub const Pipeline = pipelines.Pipeline; + pub const vulkan = @import("graphics/vulkan.zig"); pub const draw = struct { // MARK: draw diff --git a/src/graphics/pipelines.zig b/src/graphics/pipelines.zig index ae8b31670b..792ccfd1f3 100644 --- a/src/graphics/pipelines.zig +++ b/src/graphics/pipelines.zig @@ -2,10 +2,11 @@ const std = @import("std"); const main = @import("main"); const graphics = main.graphics; -const c = @import("c"); const vulkan = graphics.vulkan; const NeverFailingAllocator = main.heap.NeverFailingAllocator; +const c = @import("c"); + const glslang = @cImport({ @cInclude("glslang/Include/glslang_c_interface.h"); @cInclude("glslang/Public/resource_limits_c.h"); diff --git a/src/graphics/vulkan.zig b/src/graphics/vulkan.zig index 8d56474a01..7b661f6466 100644 --- a/src/graphics/vulkan.zig +++ b/src/graphics/vulkan.zig @@ -2,9 +2,10 @@ const std = @import("std"); const builtin = @import("builtin"); const main = @import("main"); -const c = @import("c"); const NeverFailingAllocator = main.heap.NeverFailingAllocator; +const c = @import("c"); + comptime { std.debug.assert(@as(u1, 1) == c.VK_TRUE and @as(u1, 0) == c.VK_FALSE); // Allows using @intFromBool to convert to vulkan types } diff --git a/src/gui/gui.zig b/src/gui/gui.zig index 253d5d36a8..daeae8efae 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -8,9 +8,10 @@ const settings = main.settings; const vec = main.vec; const Vec2f = vec.Vec2f; const List = main.List; - const NeverFailingAllocator = main.heap.NeverFailingAllocator; +const c = @import("c"); + const Button = @import("components/Button.zig"); const CheckBox = @import("components/CheckBox.zig"); const ItemSlot = @import("components/ItemSlot.zig"); @@ -556,7 +557,6 @@ pub fn updateAndRenderGui() void { } if (!hideGui) { if (!main.Window.grabbed) { - const c = @import("c"); draw.setColor(0x80000000); GuiWindow.borderPipeline.bind(draw.getScissor()); c.glUniform2f(GuiWindow.borderUniforms.effectLength, main.Window.getWindowSize()[0]/6, main.Window.getWindowSize()[1]/6); diff --git a/src/gui/windows/controls.zig b/src/gui/windows/controls.zig index ada83e3965..53c9245723 100644 --- a/src/gui/windows/controls.zig +++ b/src/gui/windows/controls.zig @@ -2,10 +2,13 @@ const std = @import("std"); const main = @import("main"); const Vec2f = main.vec.Vec2f; -const c = @import("c"); + const gui = @import("../gui.zig"); const GuiComponent = gui.GuiComponent; const GuiWindow = gui.GuiWindow; + +const c = @import("c"); + const Button = @import("../components/Button.zig"); const CheckBox = @import("../components/CheckBox.zig"); const HorizontalList = @import("../components/HorizontalList.zig"); diff --git a/src/gui/windows/gpu_performance_measuring.zig b/src/gui/windows/gpu_performance_measuring.zig index 530de75471..066a9145f1 100644 --- a/src/gui/windows/gpu_performance_measuring.zig +++ b/src/gui/windows/gpu_performance_measuring.zig @@ -1,12 +1,13 @@ const std = @import("std"); const main = @import("main"); -const c = @import("c"); const graphics = main.graphics; const draw = graphics.draw; const Texture = graphics.Texture; const Vec2f = main.vec.Vec2f; +const c = @import("c"); + const gui = @import("../gui.zig"); const GuiWindow = gui.GuiWindow; const GuiComponent = gui.GuiComponent; diff --git a/src/gui/windows/main.zig b/src/gui/windows/main.zig index fea68e928e..e62f2c3681 100644 --- a/src/gui/windows/main.zig +++ b/src/gui/windows/main.zig @@ -3,9 +3,12 @@ const std = @import("std"); const main = @import("main"); const Vec2f = main.vec.Vec2f; +const c = @import("c"); + const gui = @import("../gui.zig"); const GuiComponent = gui.GuiComponent; const GuiWindow = gui.GuiWindow; + const Button = @import("../components/Button.zig"); const VerticalList = @import("../components/VerticalList.zig"); @@ -17,7 +20,6 @@ pub var window = GuiWindow{ const padding: f32 = 8; fn exitGame() void { - const c = @import("c"); c.glfwSetWindowShouldClose(main.Window.window, c.GLFW_TRUE); } pub fn onOpen() void { diff --git a/src/gui/windows/performance_graph.zig b/src/gui/windows/performance_graph.zig index 8fb7414110..087d3dc267 100644 --- a/src/gui/windows/performance_graph.zig +++ b/src/gui/windows/performance_graph.zig @@ -6,6 +6,8 @@ const draw = graphics.draw; const Texture = graphics.Texture; const Vec2f = main.vec.Vec2f; +const c = @import("c"); + const gui = @import("../gui.zig"); const GuiWindow = gui.GuiWindow; const GuiComponent = gui.GuiComponent; @@ -59,7 +61,6 @@ pub fn deinit() void { } pub fn render() void { - const c = @import("c"); lastFrameTime[index] = @floatCast(main.lastFrameTime.load(.monotonic)*1000.0); index = (index + 1)%@as(u31, @intCast(lastFrameTime.len)); draw.setColor(0xffffffff); diff --git a/src/itemdrop.zig b/src/itemdrop.zig index 943e14f916..e3b0941966 100644 --- a/src/itemdrop.zig +++ b/src/itemdrop.zig @@ -1,29 +1,38 @@ const std = @import("std"); const blocks = @import("blocks.zig"); + const chunk_zig = @import("chunk.zig"); const ServerChunk = chunk_zig.ServerChunk; + const game = @import("game.zig"); const World = game.World; -const ServerWorld = main.server.ServerWorld; + const graphics = @import("graphics.zig"); -const c = @import("c"); + const items = @import("items.zig"); const ItemStack = items.ItemStack; + const ZonElement = @import("zon.zig").ZonElement; + const main = @import("main"); const physics = main.physics; +const ServerWorld = main.server.ServerWorld; +const BinaryReader = main.utils.BinaryReader; +const BinaryWriter = main.utils.BinaryWriter; +const NeverFailingAllocator = main.heap.NeverFailingAllocator; + const random = @import("random.zig"); const settings = @import("settings.zig"); const utils = @import("utils.zig"); + const vec = @import("vec.zig"); const Mat4f = vec.Mat4f; const Vec3d = vec.Vec3d; const Vec3f = vec.Vec3f; const Vec3i = vec.Vec3i; -const BinaryReader = main.utils.BinaryReader; -const BinaryWriter = main.utils.BinaryWriter; -const NeverFailingAllocator = main.heap.NeverFailingAllocator; + +const c = @import("c"); const ItemDrop = struct { // MARK: ItemDrop pos: Vec3d, diff --git a/src/main.zig b/src/main.zig index 1f5cac60ca..6eb16f5be7 100644 --- a/src/main.zig +++ b/src/main.zig @@ -9,6 +9,7 @@ pub const assets = @import("assets.zig"); pub const block_entity = @import("block_entity.zig"); pub const blocks = @import("blocks.zig"); pub const blueprint = @import("blueprint.zig"); +const c = @import("c"); pub const callbacks = @import("callbacks/callbacks.zig"); pub const chunk = @import("chunk.zig"); pub const client = @import("client.zig"); @@ -283,7 +284,6 @@ fn setHotbarSlot(i: comptime_int) *const fn (Window.Key.Modifiers) void { } pub const KeyBoard = struct { // MARK: KeyBoard - const c = @import("c"); pub var keys = [_]Window.Key{ // Gameplay: .{.name = "forward", .key = c.GLFW_KEY_W, .gamepadAxis = .{.axis = c.GLFW_GAMEPAD_AXIS_LEFT_Y, .positive = false}}, @@ -562,7 +562,6 @@ pub fn clientMain() void { // MARK: clientMain() }, } - const c = @import("c"); Window.GLFWCallbacks.framebufferSize(undefined, Window.width, Window.height); var lastBeginRendering = timestamp(); diff --git a/src/particles.zig b/src/particles.zig index 82252525a2..5c8c970abe 100644 --- a/src/particles.zig +++ b/src/particles.zig @@ -3,16 +3,20 @@ const std = @import("std"); const main = @import("main"); const physics = @import("physics.zig"); const chunk_meshing = @import("renderer/chunk_meshing.zig"); + const graphics = @import("graphics.zig"); const SSBO = graphics.SSBO; const TextureArray = graphics.TextureArray; const Shader = graphics.Shader; const Image = graphics.Image; + const c = @import("c"); const game = @import("game.zig"); const ZonElement = @import("zon.zig").ZonElement; + const random = @import("random.zig"); const RandomRange = random.RandomRange; + const vec = @import("vec.zig"); const Mat4f = vec.Mat4f; const Vec3d = vec.Vec3d; diff --git a/src/renderer.zig b/src/renderer.zig index 86bd00595b..70cd57abba 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -7,17 +7,22 @@ const entity = @import("entity.zig"); const graphics = @import("graphics.zig"); const particles = @import("particles.zig"); const c = @import("c"); + const game = @import("game.zig"); const World = game.World; + const itemdrop = @import("itemdrop.zig"); + const main = @import("main"); +const gpu_performance_measuring = main.gui.windowlist.gpu_performance_measuring; +const crosshair = main.gui.windowlist.crosshair; const Window = main.Window; + const models = @import("models.zig"); const network = @import("network.zig"); const settings = @import("settings.zig"); + const vec = @import("vec.zig"); -const gpu_performance_measuring = main.gui.windowlist.gpu_performance_measuring; -const crosshair = main.gui.windowlist.crosshair; const Vec2f = vec.Vec2f; const Vec3i = vec.Vec3i; const Vec3f = vec.Vec3f; diff --git a/src/renderer/chunk_meshing.zig b/src/renderer/chunk_meshing.zig index 78b9f2447e..df9e386f45 100644 --- a/src/renderer/chunk_meshing.zig +++ b/src/renderer/chunk_meshing.zig @@ -11,7 +11,6 @@ const models = main.models; const QuadIndex = models.QuadIndex; const renderer = main.renderer; const graphics = main.graphics; -const c = @import("c"); const SSBO = graphics.SSBO; const lighting = @import("lighting.zig"); const settings = main.settings; @@ -23,6 +22,8 @@ const Vec3d = vec.Vec3d; const Mat4f = vec.Mat4f; const gpu_performance_measuring = main.gui.windowlist.gpu_performance_measuring; +const c = @import("c"); + const mesh_storage = @import("mesh_storage.zig"); var pipeline: graphics.Pipeline = undefined; From 889833be9f56b8ec6541855cfd9c56a4423375d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Fernandes?= Date: Fri, 8 May 2026 10:06:12 +0100 Subject: [PATCH 10/14] Reorder declarations --- src/blocks.zig | 1472 ++++++----- src/graphics.zig | 4422 ++++++++++++++++----------------- src/gui/windows/controls.zig | 168 +- src/gui/windows/crosshair.zig | 78 +- src/gui/windows/main.zig | 31 +- src/itemdrop.zig | 1672 +++++++------ src/particles.zig | 1043 ++++---- src/renderer.zig | 2114 ++++++++-------- 8 files changed, 5489 insertions(+), 5511 deletions(-) diff --git a/src/blocks.zig b/src/blocks.zig index 25b1e4ec82..4a2b42cbae 100644 --- a/src/blocks.zig +++ b/src/blocks.zig @@ -14,67 +14,61 @@ const Entity = main.server.Entity; const c = @import("c"); const ZonElement = @import("zon.zig").ZonElement; - const chunk = @import("chunk.zig"); const Neighbor = chunk.Neighbor; const Chunk = chunk.Chunk; - const graphics = @import("graphics.zig"); const SSBO = graphics.SSBO; const Image = graphics.Image; const Color = graphics.Color; const TextureArray = graphics.TextureArray; - const items = @import("items.zig"); const Item = items.Item; - const models = @import("models.zig"); const ModelIndex = models.ModelIndex; - const rotation = @import("rotation.zig"); const RotationMode = rotation.RotationMode; const Degrees = rotation.Degrees; - const block_entity = @import("block_entity.zig"); const BlockEntityType = block_entity.BlockEntityType; pub const maxBlockCount: usize = 65536; // 16 bit limit pub const BlockDrop = struct { - items: []const items.ItemStack, - chance: f32, - forbiddenToolTags: []Tag, - allowedToolTags: ?[]Tag = null, - - pub fn isDroppedWhenBrokenWithItem(self: BlockDrop, item: Item) bool { - if (item != .proceduralItem) return self.allowedToolTags == null; - - const proceduralItem = item.proceduralItem; - for (self.forbiddenToolTags) |tag| if (proceduralItem.hasTag(tag)) return false; - if (self.allowedToolTags) |tags| { - for (tags) |tag| if (proceduralItem.hasTag(tag)) return true; - return false; - } - - return true; - } + items: []const items.ItemStack, + chance: f32, + forbiddenToolTags: []Tag, + allowedToolTags: ?[]Tag = null, + + pub fn isDroppedWhenBrokenWithItem(self: BlockDrop, item: Item) bool { + if (item != .proceduralItem) return self.allowedToolTags == null; + + const proceduralItem = item.proceduralItem; + for (self.forbiddenToolTags) |tag| if (proceduralItem.hasTag(tag)) return false; + if (self.allowedToolTags) |tags| { + for (tags) |tag| if (proceduralItem.hasTag(tag)) return true; + return false; + } + + return true; + } }; /// Ores can be found underground in veins. /// TODO: Add support for non-stone ores. pub const Ore = struct { - /// average size of a vein in blocks - size: f32, - /// average density of a vein - density: f32, - /// average veins per chunk - veins: f32, - /// maximum height this ore can be generated - maxHeight: i32, - minHeight: i32, - - blockType: u16, - seed: u64, + /// average size of a vein in blocks + size: f32, + /// average density of a vein + density: f32, + /// average veins per chunk + veins: f32, + /// maximum height this ore can be generated + maxHeight: i32, + minHeight: i32, + + blockType: u16, + seed: u64, }; const SelectionRule = enum { always, toolEffective, never }; @@ -126,759 +120,759 @@ var size: u32 = 0; pub var ores: main.ListUnmanaged(Ore) = .{}; pub fn register(_: []const u8, id: []const u8, zon: ZonElement) u16 { - _id[size] = main.worldArena.dupe(u8, id); - reverseIndices.put(main.worldArena.allocator, _id[size], @intCast(size)) catch unreachable; - - _mode[size] = rotation.getByID(zon.get([]const u8, "rotation", "cubyz:no_rotation")); - _blockHealth[size] = zon.get(f32, "blockHealth", 1); - _blockResistance[size] = zon.get(f32, "blockResistance", 0); - const rotation_tags = _mode[size].getBlockTags(); - const block_tags = Tag.loadTagsFromZon(main.stackAllocator, zon.getChild("tags")); - defer main.stackAllocator.free(block_tags); - _tags[size] = std.mem.concat(main.worldArena.allocator, Tag, &.{rotation_tags, block_tags}) catch unreachable; - - if (_tags[size].len == 0) std.log.err("Block {s} is missing 'tags' field", .{id}); - for (_tags[size]) |tag| { - if (tag == Tag.sbbChild) { - sbb.registerChildBlock(@intCast(size), _id[size]); - break; - } - } - - _light[size] = zon.get(u32, "emittedLight", 0); - _absorption[size] = zon.get(u32, "absorbedLight", 0xffffff); - _degradable[size] = zon.get(bool, "degradable", false); - _selectionRule[size] = zon.get(SelectionRule, "selectionRule", .always); - _replaceable[size] = zon.get(bool, "replaceable", false); - _transparent[size] = zon.get(bool, "transparent", false); - _collide[size] = zon.get(bool, "collide", true); - _alwaysViewThrough[size] = zon.get(bool, "alwaysViewThrough", false); - _viewThrough[size] = zon.get(bool, "viewThrough", false) or _transparent[size] or _alwaysViewThrough[size]; - _hasBackFace[size] = zon.get(bool, "hasBackFace", false); - _friction[size] = zon.get(f32, "friction", 20); - _bounciness[size] = zon.get(f32, "bounciness", 0.0); - _density[size] = zon.get(f32, "density", main.physics.airDensity); - _terminalVelocity[size] = zon.get(f32, "terminalVelocity", 90); - _mobility[size] = zon.get(f32, "mobility", 1.0); - _allowOres[size] = zon.get(bool, "allowOres", false); - - _blockEntity[size] = block_entity.getByID(zon.get(?[]const u8, "blockEntity", null)); - - const oreProperties = zon.getChild("ore"); - if (oreProperties != .null) blk: { - if (!std.mem.eql(u8, zon.get([]const u8, "rotation", "cubyz:no_rotation"), "cubyz:ore")) { - std.log.err("Ore must have rotation mode \"cubyz:ore\"!", .{}); - break :blk; - } - ores.append(main.worldArena, .{ - .veins = oreProperties.get(f32, "veins", 0), - .size = oreProperties.get(f32, "size", 0), - .maxHeight = oreProperties.get(i32, "maxHeight", std.math.maxInt(i32)), - .minHeight = oreProperties.get(i32, "minHeight", std.math.minInt(i32)), - .density = oreProperties.get(f32, "density", 0.5), - .blockType = @intCast(size), - .seed = std.hash.Wyhash.hash(0, id), - }); - } - - defer size += 1; - std.log.debug("Registered block: {d: >5} '{s}'", .{size, id}); - return @intCast(size); + _id[size] = main.worldArena.dupe(u8, id); + reverseIndices.put(main.worldArena.allocator, _id[size], @intCast(size)) catch unreachable; + + _mode[size] = rotation.getByID(zon.get([]const u8, "rotation", "cubyz:no_rotation")); + _blockHealth[size] = zon.get(f32, "blockHealth", 1); + _blockResistance[size] = zon.get(f32, "blockResistance", 0); + const rotation_tags = _mode[size].getBlockTags(); + const block_tags = Tag.loadTagsFromZon(main.stackAllocator, zon.getChild("tags")); + defer main.stackAllocator.free(block_tags); + _tags[size] = std.mem.concat(main.worldArena.allocator, Tag, &.{ rotation_tags, block_tags }) catch unreachable; + + if (_tags[size].len == 0) std.log.err("Block {s} is missing 'tags' field", .{id}); + for (_tags[size]) |tag| { + if (tag == Tag.sbbChild) { + sbb.registerChildBlock(@intCast(size), _id[size]); + break; + } + } + + _light[size] = zon.get(u32, "emittedLight", 0); + _absorption[size] = zon.get(u32, "absorbedLight", 0xffffff); + _degradable[size] = zon.get(bool, "degradable", false); + _selectionRule[size] = zon.get(SelectionRule, "selectionRule", .always); + _replaceable[size] = zon.get(bool, "replaceable", false); + _transparent[size] = zon.get(bool, "transparent", false); + _collide[size] = zon.get(bool, "collide", true); + _alwaysViewThrough[size] = zon.get(bool, "alwaysViewThrough", false); + _viewThrough[size] = zon.get(bool, "viewThrough", false) or _transparent[size] or _alwaysViewThrough[size]; + _hasBackFace[size] = zon.get(bool, "hasBackFace", false); + _friction[size] = zon.get(f32, "friction", 20); + _bounciness[size] = zon.get(f32, "bounciness", 0.0); + _density[size] = zon.get(f32, "density", main.physics.airDensity); + _terminalVelocity[size] = zon.get(f32, "terminalVelocity", 90); + _mobility[size] = zon.get(f32, "mobility", 1.0); + _allowOres[size] = zon.get(bool, "allowOres", false); + + _blockEntity[size] = block_entity.getByID(zon.get(?[]const u8, "blockEntity", null)); + + const oreProperties = zon.getChild("ore"); + if (oreProperties != .null) blk: { + if (!std.mem.eql(u8, zon.get([]const u8, "rotation", "cubyz:no_rotation"), "cubyz:ore")) { + std.log.err("Ore must have rotation mode \"cubyz:ore\"!", .{}); + break :blk; + } + ores.append(main.worldArena, .{ + .veins = oreProperties.get(f32, "veins", 0), + .size = oreProperties.get(f32, "size", 0), + .maxHeight = oreProperties.get(i32, "maxHeight", std.math.maxInt(i32)), + .minHeight = oreProperties.get(i32, "minHeight", std.math.minInt(i32)), + .density = oreProperties.get(f32, "density", 0.5), + .blockType = @intCast(size), + .seed = std.hash.Wyhash.hash(0, id), + }); + } + + defer size += 1; + std.log.debug("Registered block: {d: >5} '{s}'", .{ size, id }); + return @intCast(size); } pub fn loadBlockDrop(blockId: ?[]const u8, zon: ZonElement) []const BlockDrop { - const drops = zon.getChild("drops").toSlice(); - const blockDrops = main.worldArena.alloc(BlockDrop, drops.len); - - for (drops, 0..) |blockDrop, i| { - const itemZons = blockDrop.getChild("items").toSlice(); - var resultItems = main.List(items.ItemStack).initCapacity(main.worldArena, itemZons.len); - - for (itemZons) |itemZon| { - var string = itemZon.as([]const u8, "auto"); - string = std.mem.trim(u8, string, " "); - var iterator = std.mem.splitScalar(u8, string, ' '); - var name = iterator.first(); - var amount: u16 = 1; - while (iterator.next()) |next| { - if (next.len == 0) continue; // skip multiple spaces. - amount = std.fmt.parseInt(u16, name, 0) catch 1; - name = next; - break; - } - - if (std.mem.eql(u8, name, "auto")) { - if (blockId) |id| { - name = id; - } else std.log.err("Cannot use 'auto' in this context", .{}); - } - - const item = items.BaseItemIndex.fromId(name) orelse continue; - resultItems.append(.{.item = .{.baseItem = item}, .amount = amount}); - } - - var allowedToolTags: ?[]Tag = null; - if (blockDrop.getChildOrNull("allowedToolTags")) |tagZon| { - const tags = Tag.loadTagsFromZon(main.worldArena, tagZon); - if (tags.len == 0) { - std.log.err("Field '.allowedToolTags' is an empty array. No tool can drop this blockDrop", .{}); - } - allowedToolTags = tags; - } - - blockDrops[i] = .{ - .items = resultItems.items, - .chance = blockDrop.get(f32, "chance", 1), - .forbiddenToolTags = Tag.loadTagsFromZon(main.worldArena, blockDrop.getChild("forbiddenToolTags")), - .allowedToolTags = allowedToolTags, - }; - } - return blockDrops; + const drops = zon.getChild("drops").toSlice(); + const blockDrops = main.worldArena.alloc(BlockDrop, drops.len); + + for (drops, 0..) |blockDrop, i| { + const itemZons = blockDrop.getChild("items").toSlice(); + var resultItems = main.List(items.ItemStack).initCapacity(main.worldArena, itemZons.len); + + for (itemZons) |itemZon| { + var string = itemZon.as([]const u8, "auto"); + string = std.mem.trim(u8, string, " "); + var iterator = std.mem.splitScalar(u8, string, ' '); + var name = iterator.first(); + var amount: u16 = 1; + while (iterator.next()) |next| { + if (next.len == 0) continue; // skip multiple spaces. + amount = std.fmt.parseInt(u16, name, 0) catch 1; + name = next; + break; + } + + if (std.mem.eql(u8, name, "auto")) { + if (blockId) |id| { + name = id; + } else std.log.err("Cannot use 'auto' in this context", .{}); + } + + const item = items.BaseItemIndex.fromId(name) orelse continue; + resultItems.append(.{ .item = .{ .baseItem = item }, .amount = amount }); + } + + var allowedToolTags: ?[]Tag = null; + if (blockDrop.getChildOrNull("allowedToolTags")) |tagZon| { + const tags = Tag.loadTagsFromZon(main.worldArena, tagZon); + if (tags.len == 0) { + std.log.err("Field '.allowedToolTags' is an empty array. No tool can drop this blockDrop", .{}); + } + allowedToolTags = tags; + } + + blockDrops[i] = .{ + .items = resultItems.items, + .chance = blockDrop.get(f32, "chance", 1), + .forbiddenToolTags = Tag.loadTagsFromZon(main.worldArena, blockDrop.getChild("forbiddenToolTags")), + .allowedToolTags = allowedToolTags, + }; + } + return blockDrops; } fn registerBlockDrop(typ: u16, zon: ZonElement) void { - _blockDrops[typ] = loadBlockDrop(_id[typ], zon); + _blockDrops[typ] = loadBlockDrop(_id[typ], zon); } fn registerLodReplacement(typ: u16, zon: ZonElement) void { - if (zon.get(?[]const u8, "lodReplacement", null)) |replacement| { - _lodReplacement[typ] = getTypeById(replacement); - } else { - _lodReplacement[typ] = typ; - } + if (zon.get(?[]const u8, "lodReplacement", null)) |replacement| { + _lodReplacement[typ] = getTypeById(replacement); + } else { + _lodReplacement[typ] = typ; + } } fn registerOpaqueVariant(typ: u16, zon: ZonElement) void { - if (zon.get(?[]const u8, "opaqueVariant", null)) |replacement| { - _opaqueVariant[typ] = getTypeById(replacement); - } else { - _opaqueVariant[typ] = typ; - } + if (zon.get(?[]const u8, "opaqueVariant", null)) |replacement| { + _opaqueVariant[typ] = getTypeById(replacement); + } else { + _opaqueVariant[typ] = typ; + } } fn registerCallbacks(typ: u16, zon: ZonElement) void { - _onInteract[typ] = blk: { - break :blk ClientBlockCallback.init(zon.getChildOrNull("onInteract") orelse break :blk .noop) orelse { - std.log.err("Failed to load onInteract event for block {s}", .{_id[typ]}); - break :blk .noop; - }; - }; - _onBreak[typ] = blk: { - break :blk ServerBlockCallback.init(zon.getChildOrNull("onBreak") orelse break :blk .noop) orelse { - std.log.err("Failed to load onBreak event for block {s}", .{_id[typ]}); - break :blk .noop; - }; - }; - _onUpdate[typ] = blk: { - break :blk ServerBlockCallback.init(zon.getChildOrNull("onUpdate") orelse break :blk .noop) orelse { - std.log.err("Failed to load onUpdate event for block {s}", .{_id[typ]}); - break :blk .noop; - }; - }; - _onTick[typ] = blk: { - break :blk ServerBlockCallback.init(zon.getChildOrNull("onTick") orelse break :blk .noop) orelse { - std.log.err("Failed to load onTick event for block {s}", .{_id[typ]}); - break :blk .noop; - }; - }; - _onTouch[typ] = blk: { - break :blk BlockTouchCallback.init(zon.getChildOrNull("onTouch") orelse break :blk .noop) orelse { - std.log.err("Failed to load onTouch event for block {s}", .{_id[typ]}); - break :blk .noop; - }; - }; + _onInteract[typ] = blk: { + break :blk ClientBlockCallback.init(zon.getChildOrNull("onInteract") orelse break :blk .noop) orelse { + std.log.err("Failed to load onInteract event for block {s}", .{_id[typ]}); + break :blk .noop; + }; + }; + _onBreak[typ] = blk: { + break :blk ServerBlockCallback.init(zon.getChildOrNull("onBreak") orelse break :blk .noop) orelse { + std.log.err("Failed to load onBreak event for block {s}", .{_id[typ]}); + break :blk .noop; + }; + }; + _onUpdate[typ] = blk: { + break :blk ServerBlockCallback.init(zon.getChildOrNull("onUpdate") orelse break :blk .noop) orelse { + std.log.err("Failed to load onUpdate event for block {s}", .{_id[typ]}); + break :blk .noop; + }; + }; + _onTick[typ] = blk: { + break :blk ServerBlockCallback.init(zon.getChildOrNull("onTick") orelse break :blk .noop) orelse { + std.log.err("Failed to load onTick event for block {s}", .{_id[typ]}); + break :blk .noop; + }; + }; + _onTouch[typ] = blk: { + break :blk BlockTouchCallback.init(zon.getChildOrNull("onTouch") orelse break :blk .noop) orelse { + std.log.err("Failed to load onTouch event for block {s}", .{_id[typ]}); + break :blk .noop; + }; + }; } pub fn finishBlocks(zonElements: Assets.ZonHashMap) void { - var i: u16 = 0; - while (i < size) : (i += 1) { - registerBlockDrop(i, zonElements.get(_id[i]) orelse continue); - registerLodReplacement(i, zonElements.get(_id[i]) orelse continue); - registerOpaqueVariant(i, zonElements.get(_id[i]) orelse continue); - registerCallbacks(i, zonElements.get(_id[i]) orelse continue); - } - blueprint.registerVoidBlock(parseBlock("cubyz:void")); - meshes.finishTextureLoading(); + var i: u16 = 0; + while (i < size) : (i += 1) { + registerBlockDrop(i, zonElements.get(_id[i]) orelse continue); + registerLodReplacement(i, zonElements.get(_id[i]) orelse continue); + registerOpaqueVariant(i, zonElements.get(_id[i]) orelse continue); + registerCallbacks(i, zonElements.get(_id[i]) orelse continue); + } + blueprint.registerVoidBlock(parseBlock("cubyz:void")); + meshes.finishTextureLoading(); } pub fn reset() void { - size = 0; - ores = .{}; - reverseIndices = .{}; - meshes.reset(); + size = 0; + ores = .{}; + reverseIndices = .{}; + meshes.reset(); } pub fn getTypeById(id: []const u8) u16 { - if (reverseIndices.get(id)) |result| { - return result; - } else { - std.log.err("Couldn't find block {s}. Replacing it with air...", .{id}); - return 0; - } + if (reverseIndices.get(id)) |result| { + return result; + } else { + std.log.err("Couldn't find block {s}. Replacing it with air...", .{id}); + return 0; + } } const ParseBlockConfig = struct { - applyMigrations: bool = false, + applyMigrations: bool = false, }; fn parseBlockData(fullBlockId: []const u8, data: []const u8, comptime config: ParseBlockConfig) ?u16 { - if (std.mem.containsAtLeastScalar(u8, data, 1, ':')) { - const oreChild = parseBlockWithOptions(data, config); - if (oreChild.data != 0) { - std.log.warn("Error while parsing ore block data of '{s}': Parent block data must be 0.", .{fullBlockId}); - } - return oreChild.typ; - } - return std.fmt.parseInt(u16, data, 0) catch |err| { - std.log.err("Error while parsing block data of '{s}': {s}", .{fullBlockId, @errorName(err)}); - return null; - }; + if (std.mem.containsAtLeastScalar(u8, data, 1, ':')) { + const oreChild = parseBlockWithOptions(data, config); + if (oreChild.data != 0) { + std.log.warn("Error while parsing ore block data of '{s}': Parent block data must be 0.", .{fullBlockId}); + } + return oreChild.typ; + } + return std.fmt.parseInt(u16, data, 0) catch |err| { + std.log.err("Error while parsing block data of '{s}': {s}", .{ fullBlockId, @errorName(err) }); + return null; + }; } pub fn parseBlock(data: []const u8) Block { - return parseBlockWithOptions(data, .{}); + return parseBlockWithOptions(data, .{}); } pub fn parseBlockWithOptions(data: []const u8, comptime config: ParseBlockConfig) Block { - var id: []const u8 = data; - var blockData: ?u16 = null; - if (std.mem.indexOfScalarPos(u8, data, 1 + (std.mem.indexOfScalar(u8, data, ':') orelse 0), ':')) |pos| { - id = data[0..pos]; - blockData = parseBlockData(data, data[pos + 1 ..], config); - } - if (config.applyMigrations) { - id = main.migrations.applySingle(.block, id); - } - if (reverseIndices.get(id)) |resultType| { - var result: Block = .{.typ = resultType, .data = 0}; - result.data = blockData orelse result.mode().naturalStandard; - return result; - } else { - std.log.err("Couldn't find block {s}. Replacing it with air...", .{id}); - return .{.typ = 0, .data = 0}; - } + var id: []const u8 = data; + var blockData: ?u16 = null; + if (std.mem.indexOfScalarPos(u8, data, 1 + (std.mem.indexOfScalar(u8, data, ':') orelse 0), ':')) |pos| { + id = data[0..pos]; + blockData = parseBlockData(data, data[pos + 1 ..], config); + } + if (config.applyMigrations) { + id = main.migrations.applySingle(.block, id); + } + if (reverseIndices.get(id)) |resultType| { + var result: Block = .{ .typ = resultType, .data = 0 }; + result.data = blockData orelse result.mode().naturalStandard; + return result; + } else { + std.log.err("Couldn't find block {s}. Replacing it with air...", .{id}); + return .{ .typ = 0, .data = 0 }; + } } pub fn getBlockById(idAndData: []const u8) !u16 { - const addonNameSeparatorIndex = std.mem.indexOfScalar(u8, idAndData, ':') orelse return error.MissingAddonNameSeparator; - const blockIdEndIndex = std.mem.indexOfScalarPos(u8, idAndData, 1 + addonNameSeparatorIndex, ':') orelse idAndData.len; - const id = idAndData[0..blockIdEndIndex]; - return reverseIndices.get(id) orelse return error.NotFound; + const addonNameSeparatorIndex = std.mem.indexOfScalar(u8, idAndData, ':') orelse return error.MissingAddonNameSeparator; + const blockIdEndIndex = std.mem.indexOfScalarPos(u8, idAndData, 1 + addonNameSeparatorIndex, ':') orelse idAndData.len; + const id = idAndData[0..blockIdEndIndex]; + return reverseIndices.get(id) orelse return error.NotFound; } pub fn getBlockData(idLikeString: []const u8) !?u16 { - const addonNameSeparatorIndex = std.mem.indexOfScalar(u8, idLikeString, ':') orelse return error.MissingAddonNameSeparator; - const blockIdEndIndex = std.mem.indexOfScalarPos(u8, idLikeString, 1 + addonNameSeparatorIndex, ':') orelse return null; - const dataString = idLikeString[blockIdEndIndex + 1 ..]; - if (dataString.len == 0) return error.EmptyDataString; - return std.fmt.parseInt(u16, dataString, 0) catch return error.InvalidData; + const addonNameSeparatorIndex = std.mem.indexOfScalar(u8, idLikeString, ':') orelse return error.MissingAddonNameSeparator; + const blockIdEndIndex = std.mem.indexOfScalarPos(u8, idLikeString, 1 + addonNameSeparatorIndex, ':') orelse return null; + const dataString = idLikeString[blockIdEndIndex + 1 ..]; + if (dataString.len == 0) return error.EmptyDataString; + return std.fmt.parseInt(u16, dataString, 0) catch return error.InvalidData; } pub fn hasRegistered(id: []const u8) bool { - return reverseIndices.contains(id); + return reverseIndices.contains(id); } pub const Block = packed struct(u32) { // MARK: Block - typ: u16, - data: u16, - - pub const air = Block{.typ = 0, .data = 0}; - - pub fn toInt(self: Block) u32 { - return @as(u32, self.typ) | @as(u32, self.data) << 16; - } - pub fn fromInt(self: u32) Block { - return Block{.typ = @truncate(self), .data = @intCast(self >> 16)}; - } - - pub inline fn transparent(self: Block) bool { - return _transparent[self.typ]; - } - - pub inline fn collide(self: Block) bool { - return _collide[self.typ]; - } - - pub inline fn id(self: Block) []u8 { - return _id[self.typ]; - } - - pub inline fn idAndData(self: Block, list: *main.List(u8)) void { - list.appendSlice(self.id()); - if (self.data == 0) return; - list.append(':'); - self.mode().formatBlockData(self, list); - } - - pub inline fn blockHealth(self: Block) f32 { - return _blockHealth[self.typ]; - } - - pub inline fn blockResistance(self: Block) f32 { - return _blockResistance[self.typ]; - } - - /// Whether you can replace it with another block, mainly used for fluids/gases - pub inline fn replaceable(self: Block) bool { - return _replaceable[self.typ]; - } - - pub inline fn selectionRule(self: Block) SelectionRule { - return _selectionRule[self.typ]; - } - - pub inline fn blockDrops(self: Block) []const BlockDrop { - return _blockDrops[self.typ]; - } - - /// Meaning undegradable parts of trees or other structures can grow through this block. - pub inline fn degradable(self: Block) bool { - return _degradable[self.typ]; - } - - pub inline fn viewThrough(self: Block) bool { - return _viewThrough[self.typ]; - } - - /// shows backfaces even when next to the same block type - pub inline fn alwaysViewThrough(self: Block) bool { - return _alwaysViewThrough[self.typ]; - } - - pub inline fn hasBackFace(self: Block) bool { - return _hasBackFace[self.typ]; - } - - pub inline fn tags(self: Block) []const Tag { - return _tags[self.typ]; - } - - pub inline fn hasTag(self: Block, tag: Tag) bool { - return std.mem.containsAtLeastScalar(Tag, self.tags(), 1, tag); - } - - pub inline fn light(self: Block) u32 { - return _light[self.typ]; - } - - /// How much light this block absorbs if it is transparent. - pub inline fn absorption(self: Block) u32 { - return _absorption[self.typ]; - } - - pub inline fn onInteract(self: Block) ClientBlockCallback { - return _onInteract[self.typ]; - } - pub inline fn onBreak(self: Block) ServerBlockCallback { - return _onBreak[self.typ]; - } - pub inline fn onUpdate(self: Block) ServerBlockCallback { - return _onUpdate[self.typ]; - } - pub inline fn mode(self: Block) *const RotationMode { - return _mode[self.typ]; - } - - pub inline fn modeData(self: Block) u16 { - return _modeData[self.typ]; - } - - pub inline fn rotateZ(self: Block, angle: Degrees) Block { - return .{.typ = self.typ, .data = self.mode().rotateZ(self.data, angle)}; - } - - pub inline fn lodReplacement(self: Block) u16 { - return _lodReplacement[self.typ]; - } - - pub inline fn opaqueVariant(self: Block) u16 { - return _opaqueVariant[self.typ]; - } - - pub inline fn friction(self: Block) f32 { - return _friction[self.typ]; - } - - pub inline fn bounciness(self: Block) f32 { - return _bounciness[self.typ]; - } - - pub inline fn density(self: Block) f32 { - return _density[self.typ]; - } - - pub inline fn terminalVelocity(self: Block) f32 { - return _terminalVelocity[self.typ]; - } - - pub inline fn mobility(self: Block) f32 { - return _mobility[self.typ]; - } - - pub inline fn allowOres(self: Block) bool { - return _allowOres[self.typ]; - } - - pub inline fn onTick(self: Block) ServerBlockCallback { - return _onTick[self.typ]; - } - - pub inline fn onTouch(self: Block) BlockTouchCallback { - return _onTouch[self.typ]; - } - - pub fn blockEntity(self: Block) ?*const BlockEntityType { - return _blockEntity[self.typ]; - } - - pub fn canBeChangedInto(self: Block, newBlock: Block, item: main.items.ItemStack, shouldDropSourceBlockOnSuccess: *bool) main.rotation.RotationMode.CanBeChangedInto { - return newBlock.mode().canBeChangedInto(self, newBlock, item, shouldDropSourceBlockOnSuccess); - } - - pub fn isSelectableByItem(self: Block, item: Item) bool { - if (item == .baseItem) { - const base = item.baseItem; - if (base.block() == self.typ or std.mem.eql(u8, base.id(), "cubyz:selection_wand")) { - return true; - } - } - - if (self.hasTag(.fluid)) { - const fluidPlaceable = item == .baseItem and item.baseItem.hasTag(.fluidPlaceable); - return fluidPlaceable; - } - - return switch (self.selectionRule()) { - .always => true, - .toolEffective => item == .proceduralItem and item.proceduralItem.isEffectiveOn(self), - .never => false, - }; - } + typ: u16, + data: u16, + + pub const air = Block{ .typ = 0, .data = 0 }; + + pub fn toInt(self: Block) u32 { + return @as(u32, self.typ) | @as(u32, self.data) << 16; + } + pub fn fromInt(self: u32) Block { + return Block{ .typ = @truncate(self), .data = @intCast(self >> 16) }; + } + + pub inline fn transparent(self: Block) bool { + return _transparent[self.typ]; + } + + pub inline fn collide(self: Block) bool { + return _collide[self.typ]; + } + + pub inline fn id(self: Block) []u8 { + return _id[self.typ]; + } + + pub inline fn idAndData(self: Block, list: *main.List(u8)) void { + list.appendSlice(self.id()); + if (self.data == 0) return; + list.append(':'); + self.mode().formatBlockData(self, list); + } + + pub inline fn blockHealth(self: Block) f32 { + return _blockHealth[self.typ]; + } + + pub inline fn blockResistance(self: Block) f32 { + return _blockResistance[self.typ]; + } + + /// Whether you can replace it with another block, mainly used for fluids/gases + pub inline fn replaceable(self: Block) bool { + return _replaceable[self.typ]; + } + + pub inline fn selectionRule(self: Block) SelectionRule { + return _selectionRule[self.typ]; + } + + pub inline fn blockDrops(self: Block) []const BlockDrop { + return _blockDrops[self.typ]; + } + + /// Meaning undegradable parts of trees or other structures can grow through this block. + pub inline fn degradable(self: Block) bool { + return _degradable[self.typ]; + } + + pub inline fn viewThrough(self: Block) bool { + return _viewThrough[self.typ]; + } + + /// shows backfaces even when next to the same block type + pub inline fn alwaysViewThrough(self: Block) bool { + return _alwaysViewThrough[self.typ]; + } + + pub inline fn hasBackFace(self: Block) bool { + return _hasBackFace[self.typ]; + } + + pub inline fn tags(self: Block) []const Tag { + return _tags[self.typ]; + } + + pub inline fn hasTag(self: Block, tag: Tag) bool { + return std.mem.containsAtLeastScalar(Tag, self.tags(), 1, tag); + } + + pub inline fn light(self: Block) u32 { + return _light[self.typ]; + } + + /// How much light this block absorbs if it is transparent. + pub inline fn absorption(self: Block) u32 { + return _absorption[self.typ]; + } + + pub inline fn onInteract(self: Block) ClientBlockCallback { + return _onInteract[self.typ]; + } + pub inline fn onBreak(self: Block) ServerBlockCallback { + return _onBreak[self.typ]; + } + pub inline fn onUpdate(self: Block) ServerBlockCallback { + return _onUpdate[self.typ]; + } + pub inline fn mode(self: Block) *const RotationMode { + return _mode[self.typ]; + } + + pub inline fn modeData(self: Block) u16 { + return _modeData[self.typ]; + } + + pub inline fn rotateZ(self: Block, angle: Degrees) Block { + return .{ .typ = self.typ, .data = self.mode().rotateZ(self.data, angle) }; + } + + pub inline fn lodReplacement(self: Block) u16 { + return _lodReplacement[self.typ]; + } + + pub inline fn opaqueVariant(self: Block) u16 { + return _opaqueVariant[self.typ]; + } + + pub inline fn friction(self: Block) f32 { + return _friction[self.typ]; + } + + pub inline fn bounciness(self: Block) f32 { + return _bounciness[self.typ]; + } + + pub inline fn density(self: Block) f32 { + return _density[self.typ]; + } + + pub inline fn terminalVelocity(self: Block) f32 { + return _terminalVelocity[self.typ]; + } + + pub inline fn mobility(self: Block) f32 { + return _mobility[self.typ]; + } + + pub inline fn allowOres(self: Block) bool { + return _allowOres[self.typ]; + } + + pub inline fn onTick(self: Block) ServerBlockCallback { + return _onTick[self.typ]; + } + + pub inline fn onTouch(self: Block) BlockTouchCallback { + return _onTouch[self.typ]; + } + + pub fn blockEntity(self: Block) ?*const BlockEntityType { + return _blockEntity[self.typ]; + } + + pub fn canBeChangedInto(self: Block, newBlock: Block, item: main.items.ItemStack, shouldDropSourceBlockOnSuccess: *bool) main.rotation.RotationMode.CanBeChangedInto { + return newBlock.mode().canBeChangedInto(self, newBlock, item, shouldDropSourceBlockOnSuccess); + } + + pub fn isSelectableByItem(self: Block, item: Item) bool { + if (item == .baseItem) { + const base = item.baseItem; + if (base.block() == self.typ or std.mem.eql(u8, base.id(), "cubyz:selection_wand")) { + return true; + } + } + + if (self.hasTag(.fluid)) { + const fluidPlaceable = item == .baseItem and item.baseItem.hasTag(.fluidPlaceable); + return fluidPlaceable; + } + + return switch (self.selectionRule()) { + .always => true, + .toolEffective => item == .proceduralItem and item.proceduralItem.isEffectiveOn(self), + .never => false, + }; + } }; pub const meshes = struct { // MARK: meshes - const AnimationData = extern struct { - startFrame: u32, - frames: u32, - time: u32, - }; - - const FogData = extern struct { - fogDensity: f32, - fogColor: u32, - }; - var size: u32 = 0; - var _modelIndex: [maxBlockCount]ModelIndex = undefined; - var textureIndices: [maxBlockCount][16]u16 = undefined; - /// Stores the number of textures after each block was added. Used to clean additional textures when the world is switched. - var maxTextureCount: [maxBlockCount]u32 = undefined; - /// Number of loaded meshes. Used to determine if an update is needed. - var loadedMeshes: u32 = 0; - - var textureIds: main.ListUnmanaged([]const u8) = .{}; - var texturePaths: main.ListUnmanaged([]const u8) = .{}; - var animationData: []AnimationData = &.{}; - var blockTextures: main.ListUnmanaged(Image) = .{}; - var emissionTextures: main.ListUnmanaged(Image) = .{}; - var reflectivityTextures: main.ListUnmanaged(Image) = .{}; - var absorptionTextures: main.ListUnmanaged(Image) = .{}; - var textureFogData: main.ListUnmanaged(FogData) = .{}; - pub var textureOcclusionData: []std.atomic.Value(bool) = &.{}; - - pub var blockBreakingTextures: main.ListUnmanaged(u16) = .{}; - - const sideNames = blk: { - var names: [6][]const u8 = undefined; - names[Neighbor.dirDown.toInt()] = "texture_bottom"; - names[Neighbor.dirUp.toInt()] = "texture_top"; - names[Neighbor.dirPosX.toInt()] = "texture_right"; - names[Neighbor.dirNegX.toInt()] = "texture_left"; - names[Neighbor.dirPosY.toInt()] = "texture_front"; - names[Neighbor.dirNegY.toInt()] = "texture_back"; - break :blk names; - }; - - var animationSSBO: ?SSBO = null; - var animatedTextureSSBO: ?SSBO = null; - var fogSSBO: ?SSBO = null; - - var animationComputePipeline: graphics.ComputePipeline = undefined; - var animationUniforms: struct { - time: c_int, - size: c_int, - } = undefined; - - pub var blockTextureArray: TextureArray = undefined; - pub var emissionTextureArray: TextureArray = undefined; - pub var reflectivityAndAbsorptionTextureArray: TextureArray = undefined; - pub var ditherTexture: graphics.Texture = undefined; - - const black: Color = Color{.r = 0, .g = 0, .b = 0, .a = 255}; - const magenta: Color = Color{.r = 255, .g = 0, .b = 255, .a = 255}; - var undefinedTexture = [_]Color{magenta, black, black, magenta}; - const undefinedImage = Image{.width = 2, .height = 2, .imageData = undefinedTexture[0..]}; - var emptyTexture = [_]Color{black}; - const emptyImage = Image{.width = 1, .height = 1, .imageData = emptyTexture[0..]}; - - pub fn init() void { - animationComputePipeline = graphics.ComputePipeline.init("assets/cubyz/shaders/animation_pre_processing.comp", "", &animationUniforms); - blockTextureArray = .init(); - emissionTextureArray = .init(); - reflectivityAndAbsorptionTextureArray = .init(); - ditherTexture = .initFromMipmapFiles("assets/cubyz/blocks/textures/dither/", 64, 0.5); - } - - pub fn deinit() void { - if (animationSSBO) |ssbo| { - ssbo.deinit(); - } - if (animatedTextureSSBO) |ssbo| { - ssbo.deinit(); - } - if (fogSSBO) |ssbo| { - ssbo.deinit(); - } - animationComputePipeline.deinit(); - blockTextureArray.deinit(); - emissionTextureArray.deinit(); - reflectivityAndAbsorptionTextureArray.deinit(); - ditherTexture.deinit(); - } - - pub fn reset() void { - meshes.size = 0; - loadedMeshes = 0; - textureIds = .{}; - texturePaths = .{}; - animationData = &.{}; - blockTextures = .{}; - emissionTextures = .{}; - reflectivityTextures = .{}; - absorptionTextures = .{}; - textureFogData = .{}; - textureOcclusionData = &.{}; - blockBreakingTextures = .{}; - } - - pub inline fn model(block: Block) ModelIndex { - return block.mode().model(block); - } - - pub inline fn modelIndexStart(block: Block) ModelIndex { - return _modelIndex[block.typ]; - } - - pub inline fn fogDensity(block: Block) f32 { - return textureFogData.items[animationData[textureIndices[block.typ][0]].startFrame].fogDensity; - } - - pub inline fn fogColor(block: Block) u32 { - return textureFogData.items[animationData[textureIndices[block.typ][0]].startFrame].fogColor; - } - - pub inline fn hasFog(block: Block) bool { - return fogDensity(block) != 0.0; - } - - pub inline fn textureIndex(block: Block, orientation: usize) u16 { - if (orientation < 16) { - return textureIndices[block.typ][orientation]; - } else { - return textureIndices[block.data][orientation - 16]; - } - } - - fn extendedPath(_allocator: main.heap.NeverFailingAllocator, path: []const u8, ending: []const u8) []const u8 { - return std.fmt.allocPrint(_allocator.allocator, "{s}{s}", .{path, ending}) catch unreachable; - } - - fn readTextureFile(_path: []const u8, ending: []const u8, default: Image) Image { - const path = extendedPath(main.stackAllocator, _path, ending); - defer main.stackAllocator.free(path); - return Image.readFromFile(main.worldArena, path) catch default; - } - - fn extractAnimationSlice(image: Image, frame: usize, frames: usize) Image { - if (image.height < frames) return image; - var startHeight = image.height/frames*frame; - if (image.height%frames > frame) startHeight += frame else startHeight += image.height%frames; - var endHeight = image.height/frames*(frame + 1); - if (image.height%frames > frame + 1) endHeight += frame + 1 else endHeight += image.height%frames; - var result = image; - result.height = @intCast(endHeight - startHeight); - result.imageData = result.imageData[startHeight*image.width .. endHeight*image.width]; - return result; - } - - fn readTextureData(index: usize, _path: []const u8) void { - const path = _path[0 .. _path.len - ".png".len]; - const textureInfoPath = extendedPath(main.stackAllocator, path, ".zig.zon"); - defer main.stackAllocator.free(textureInfoPath); - const textureInfoZon = main.files.cwd().readToZon(main.stackAllocator, textureInfoPath) catch .null; - defer textureInfoZon.deinit(main.stackAllocator); - const animationFrames = textureInfoZon.get(u32, "frames", 1); - const animationTime = textureInfoZon.get(u32, "time", 1); - animationData[index] = .{.startFrame = @intCast(blockTextures.items.len), .frames = animationFrames, .time = animationTime}; - const base = readTextureFile(path, ".png", Image.defaultImage); - const emission = readTextureFile(path, "_emission.png", Image.emptyImage); - const reflectivity = readTextureFile(path, "_reflectivity.png", Image.emptyImage); - const absorption = readTextureFile(path, "_absorption.png", Image.whiteEmptyImage); - for (0..animationFrames) |i| { - blockTextures.append(main.worldArena, extractAnimationSlice(base, i, animationFrames)); - emissionTextures.append(main.worldArena, extractAnimationSlice(emission, i, animationFrames)); - reflectivityTextures.append(main.worldArena, extractAnimationSlice(reflectivity, i, animationFrames)); - absorptionTextures.append(main.worldArena, extractAnimationSlice(absorption, i, animationFrames)); - textureFogData.append(main.worldArena, .{ - .fogDensity = textureInfoZon.get(f32, "fogDensity", 0.0), - .fogColor = textureInfoZon.get(u32, "fogColor", 0xffffff), - }); - } - textureOcclusionData[index].store(textureInfoZon.get(bool, "hasOcclusion", true), .monotonic); - } - - pub fn findTexture(_textureId: ?[]const u8, assetFolder: []const u8) !u16 { - const textureId = _textureId orelse return error.NotFound; - var result: u16 = undefined; - var splitter = std.mem.splitScalar(u8, textureId, ':'); - const mod = splitter.first(); - const id = splitter.rest(); - var path = try std.fmt.allocPrint(main.stackAllocator.allocator, "{s}/{s}/blocks/textures/{s}.png", .{assetFolder, mod, id}); - defer main.stackAllocator.free(path); - // Test if it's already in the list: - for (textureIds.items, 0..) |other, j| { - if (std.mem.eql(u8, other, textureId)) { - result = @intCast(j); - return result; - } - } - const file = main.files.cwd().openFile(path) catch |err| blk: { - if (err != error.FileNotFound) { - std.log.err("Could not open file {s}: {s}", .{path, @errorName(err)}); - } - main.stackAllocator.free(path); - path = try std.fmt.allocPrint(main.stackAllocator.allocator, "assets/{s}/blocks/textures/{s}.png", .{mod, id}); // Default to global assets. - break :blk main.files.cwd().openFile(path) catch |err2| { - std.log.err("File not found. Searched in \"{s}\" and also in the assetFolder \"{s}\"", .{path, assetFolder}); - return err2; - }; - }; - file.close(main.io); // It was only openend to check if it exists. - // Otherwise read it into the list: - result = @intCast(textureIds.items.len); - - textureIds.append(main.worldArena, main.worldArena.dupe(u8, textureId)); - texturePaths.append(main.worldArena, main.worldArena.dupe(u8, path)); - return result; - } - - pub fn getTextureIndices(zon: ZonElement, assetFolder: []const u8, textureIndicesRef: *[16]u16) void { - const defaultIndex = findTexture(zon.get(?[]const u8, "texture", null), assetFolder) catch 0; - inline for (textureIndicesRef, 0..) |*ref, i| { - var textureId = zon.get(?[]const u8, std.fmt.comptimePrint("texture{}", .{i}), null); - if (i < sideNames.len) { - textureId = zon.get(?[]const u8, sideNames[i], textureId); - } - ref.* = findTexture(textureId, assetFolder) catch defaultIndex; - } - } - - pub fn register(assetFolder: []const u8, _: []const u8, zon: ZonElement) void { - _modelIndex[meshes.size] = _mode[meshes.size].createBlockModel(.{.typ = @intCast(meshes.size), .data = 0}, &_modeData[meshes.size], zon.getChild("model")); - - // The actual model is loaded later, in the rendering thread. - // But textures can be loaded here: - - getTextureIndices(zon, assetFolder, &textureIndices[meshes.size]); - - maxTextureCount[meshes.size] = @intCast(textureIds.items.len); - - meshes.size += 1; - } - - pub fn registerBlockBreakingAnimation(assetFolder: []const u8) void { - var i: usize = 0; - while (true) : (i += 1) { - const path1 = std.fmt.allocPrint(main.stackAllocator.allocator, "assets/cubyz/blocks/textures/breaking/{}.png", .{i}) catch unreachable; - defer main.stackAllocator.free(path1); - const path2 = std.fmt.allocPrint(main.stackAllocator.allocator, "{s}/cubyz/blocks/textures/breaking/{}.png", .{assetFolder, i}) catch unreachable; - defer main.stackAllocator.free(path2); - if (!main.files.cwd().hasFile(path1) and !main.files.cwd().hasFile(path2)) break; - - const id = std.fmt.allocPrint(main.stackAllocator.allocator, "cubyz:breaking/{}", .{i}) catch unreachable; - defer main.stackAllocator.free(id); - blockBreakingTextures.append(main.worldArena, findTexture(id, assetFolder) catch break); - } - } - - pub fn preProcessAnimationData(time: u32) void { - animationComputePipeline.bind(); - c.glUniform1ui(animationUniforms.time, time); - c.glUniform1ui(animationUniforms.size, @intCast(animationData.len)); - c.glDispatchCompute(@intCast(@divFloor(animationData.len + 63, 64)), 1, 1); // TODO: Replace with @divCeil once available - c.glMemoryBarrier(c.GL_SHADER_STORAGE_BARRIER_BIT); - } - - fn finishTextureLoading() void { - animationData = main.worldArena.alloc(AnimationData, textureIds.items.len); - textureOcclusionData = main.worldArena.alloc(std.atomic.Value(bool), textureIds.items.len); - for (texturePaths.items, 0..) |path, i| { - readTextureData(i, path); - } - } - - pub fn reloadTextures(_: usize) void { - blockTextures.clearRetainingCapacity(); - emissionTextures.clearRetainingCapacity(); - reflectivityTextures.clearRetainingCapacity(); - absorptionTextures.clearRetainingCapacity(); - textureFogData.clearRetainingCapacity(); - for (texturePaths.items, 0..) |path, i| { - readTextureData(i, path); - } - generateTextureArray(); - } - - pub fn generateTextureArray() void { - blockTextureArray.generate(blockTextures.items, true, true); - c.glTexParameterf(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MAX_ANISOTROPY, @floatFromInt(main.settings.anisotropicFiltering)); - emissionTextureArray.generate(emissionTextures.items, true, false); - c.glTexParameterf(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MAX_ANISOTROPY, @floatFromInt(main.settings.anisotropicFiltering)); - const reflectivityAndAbsorptionTextures = main.stackAllocator.alloc(Image, reflectivityTextures.items.len); - defer main.stackAllocator.free(reflectivityAndAbsorptionTextures); - defer for (reflectivityAndAbsorptionTextures) |texture| { - texture.deinit(main.stackAllocator); - }; - for (reflectivityTextures.items, absorptionTextures.items, reflectivityAndAbsorptionTextures) |reflecitivityTexture, absorptionTexture, *resultTexture| { - const width = @max(reflecitivityTexture.width, absorptionTexture.width); - const height = @max(reflecitivityTexture.height, absorptionTexture.height); - resultTexture.* = Image.init(main.stackAllocator, width, height); - for (0..width) |x| { - for (0..height) |y| { - const reflectivity = reflecitivityTexture.getRGB(x*reflecitivityTexture.width/width, y*reflecitivityTexture.height/height); - const absorption = absorptionTexture.getRGB(x*absorptionTexture.width/width, y*absorptionTexture.height/height); - resultTexture.setRGB(x, y, .{.r = absorption.r, .g = absorption.g, .b = absorption.b, .a = reflectivity.r}); - } - } - } - reflectivityAndAbsorptionTextureArray.generate(reflectivityAndAbsorptionTextures, true, false); - c.glTexParameterf(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MAX_ANISOTROPY, @floatFromInt(main.settings.anisotropicFiltering)); - - // Also generate additional buffers: - if (animationSSBO) |ssbo| { - ssbo.deinit(); - } - if (animatedTextureSSBO) |ssbo| { - ssbo.deinit(); - } - if (fogSSBO) |ssbo| { - ssbo.deinit(); - } - animationSSBO = SSBO.initStatic(AnimationData, animationData); - animationSSBO.?.bind(0); - - animatedTextureSSBO = SSBO.initStaticSize(u32, animationData.len); - animatedTextureSSBO.?.bind(1); - fogSSBO = SSBO.initStatic(FogData, textureFogData.items); - fogSSBO.?.bind(7); - } + const AnimationData = extern struct { + startFrame: u32, + frames: u32, + time: u32, + }; + + const FogData = extern struct { + fogDensity: f32, + fogColor: u32, + }; + var size: u32 = 0; + var _modelIndex: [maxBlockCount]ModelIndex = undefined; + var textureIndices: [maxBlockCount][16]u16 = undefined; + /// Stores the number of textures after each block was added. Used to clean additional textures when the world is switched. + var maxTextureCount: [maxBlockCount]u32 = undefined; + /// Number of loaded meshes. Used to determine if an update is needed. + var loadedMeshes: u32 = 0; + + var textureIds: main.ListUnmanaged([]const u8) = .{}; + var texturePaths: main.ListUnmanaged([]const u8) = .{}; + var animationData: []AnimationData = &.{}; + var blockTextures: main.ListUnmanaged(Image) = .{}; + var emissionTextures: main.ListUnmanaged(Image) = .{}; + var reflectivityTextures: main.ListUnmanaged(Image) = .{}; + var absorptionTextures: main.ListUnmanaged(Image) = .{}; + var textureFogData: main.ListUnmanaged(FogData) = .{}; + pub var textureOcclusionData: []std.atomic.Value(bool) = &.{}; + + pub var blockBreakingTextures: main.ListUnmanaged(u16) = .{}; + + const sideNames = blk: { + var names: [6][]const u8 = undefined; + names[Neighbor.dirDown.toInt()] = "texture_bottom"; + names[Neighbor.dirUp.toInt()] = "texture_top"; + names[Neighbor.dirPosX.toInt()] = "texture_right"; + names[Neighbor.dirNegX.toInt()] = "texture_left"; + names[Neighbor.dirPosY.toInt()] = "texture_front"; + names[Neighbor.dirNegY.toInt()] = "texture_back"; + break :blk names; + }; + + var animationSSBO: ?SSBO = null; + var animatedTextureSSBO: ?SSBO = null; + var fogSSBO: ?SSBO = null; + + var animationComputePipeline: graphics.ComputePipeline = undefined; + var animationUniforms: struct { + time: c_int, + size: c_int, + } = undefined; + + pub var blockTextureArray: TextureArray = undefined; + pub var emissionTextureArray: TextureArray = undefined; + pub var reflectivityAndAbsorptionTextureArray: TextureArray = undefined; + pub var ditherTexture: graphics.Texture = undefined; + + const black: Color = Color{ .r = 0, .g = 0, .b = 0, .a = 255 }; + const magenta: Color = Color{ .r = 255, .g = 0, .b = 255, .a = 255 }; + var undefinedTexture = [_]Color{ magenta, black, black, magenta }; + const undefinedImage = Image{ .width = 2, .height = 2, .imageData = undefinedTexture[0..] }; + var emptyTexture = [_]Color{black}; + const emptyImage = Image{ .width = 1, .height = 1, .imageData = emptyTexture[0..] }; + + pub fn init() void { + animationComputePipeline = graphics.ComputePipeline.init("assets/cubyz/shaders/animation_pre_processing.comp", "", &animationUniforms); + blockTextureArray = .init(); + emissionTextureArray = .init(); + reflectivityAndAbsorptionTextureArray = .init(); + ditherTexture = .initFromMipmapFiles("assets/cubyz/blocks/textures/dither/", 64, 0.5); + } + + pub fn deinit() void { + if (animationSSBO) |ssbo| { + ssbo.deinit(); + } + if (animatedTextureSSBO) |ssbo| { + ssbo.deinit(); + } + if (fogSSBO) |ssbo| { + ssbo.deinit(); + } + animationComputePipeline.deinit(); + blockTextureArray.deinit(); + emissionTextureArray.deinit(); + reflectivityAndAbsorptionTextureArray.deinit(); + ditherTexture.deinit(); + } + + pub fn reset() void { + meshes.size = 0; + loadedMeshes = 0; + textureIds = .{}; + texturePaths = .{}; + animationData = &.{}; + blockTextures = .{}; + emissionTextures = .{}; + reflectivityTextures = .{}; + absorptionTextures = .{}; + textureFogData = .{}; + textureOcclusionData = &.{}; + blockBreakingTextures = .{}; + } + + pub inline fn model(block: Block) ModelIndex { + return block.mode().model(block); + } + + pub inline fn modelIndexStart(block: Block) ModelIndex { + return _modelIndex[block.typ]; + } + + pub inline fn fogDensity(block: Block) f32 { + return textureFogData.items[animationData[textureIndices[block.typ][0]].startFrame].fogDensity; + } + + pub inline fn fogColor(block: Block) u32 { + return textureFogData.items[animationData[textureIndices[block.typ][0]].startFrame].fogColor; + } + + pub inline fn hasFog(block: Block) bool { + return fogDensity(block) != 0.0; + } + + pub inline fn textureIndex(block: Block, orientation: usize) u16 { + if (orientation < 16) { + return textureIndices[block.typ][orientation]; + } else { + return textureIndices[block.data][orientation - 16]; + } + } + + fn extendedPath(_allocator: main.heap.NeverFailingAllocator, path: []const u8, ending: []const u8) []const u8 { + return std.fmt.allocPrint(_allocator.allocator, "{s}{s}", .{ path, ending }) catch unreachable; + } + + fn readTextureFile(_path: []const u8, ending: []const u8, default: Image) Image { + const path = extendedPath(main.stackAllocator, _path, ending); + defer main.stackAllocator.free(path); + return Image.readFromFile(main.worldArena, path) catch default; + } + + fn extractAnimationSlice(image: Image, frame: usize, frames: usize) Image { + if (image.height < frames) return image; + var startHeight = image.height / frames * frame; + if (image.height % frames > frame) startHeight += frame else startHeight += image.height % frames; + var endHeight = image.height / frames * (frame + 1); + if (image.height % frames > frame + 1) endHeight += frame + 1 else endHeight += image.height % frames; + var result = image; + result.height = @intCast(endHeight - startHeight); + result.imageData = result.imageData[startHeight * image.width .. endHeight * image.width]; + return result; + } + + fn readTextureData(index: usize, _path: []const u8) void { + const path = _path[0 .. _path.len - ".png".len]; + const textureInfoPath = extendedPath(main.stackAllocator, path, ".zig.zon"); + defer main.stackAllocator.free(textureInfoPath); + const textureInfoZon = main.files.cwd().readToZon(main.stackAllocator, textureInfoPath) catch .null; + defer textureInfoZon.deinit(main.stackAllocator); + const animationFrames = textureInfoZon.get(u32, "frames", 1); + const animationTime = textureInfoZon.get(u32, "time", 1); + animationData[index] = .{ .startFrame = @intCast(blockTextures.items.len), .frames = animationFrames, .time = animationTime }; + const base = readTextureFile(path, ".png", Image.defaultImage); + const emission = readTextureFile(path, "_emission.png", Image.emptyImage); + const reflectivity = readTextureFile(path, "_reflectivity.png", Image.emptyImage); + const absorption = readTextureFile(path, "_absorption.png", Image.whiteEmptyImage); + for (0..animationFrames) |i| { + blockTextures.append(main.worldArena, extractAnimationSlice(base, i, animationFrames)); + emissionTextures.append(main.worldArena, extractAnimationSlice(emission, i, animationFrames)); + reflectivityTextures.append(main.worldArena, extractAnimationSlice(reflectivity, i, animationFrames)); + absorptionTextures.append(main.worldArena, extractAnimationSlice(absorption, i, animationFrames)); + textureFogData.append(main.worldArena, .{ + .fogDensity = textureInfoZon.get(f32, "fogDensity", 0.0), + .fogColor = textureInfoZon.get(u32, "fogColor", 0xffffff), + }); + } + textureOcclusionData[index].store(textureInfoZon.get(bool, "hasOcclusion", true), .monotonic); + } + + pub fn findTexture(_textureId: ?[]const u8, assetFolder: []const u8) !u16 { + const textureId = _textureId orelse return error.NotFound; + var result: u16 = undefined; + var splitter = std.mem.splitScalar(u8, textureId, ':'); + const mod = splitter.first(); + const id = splitter.rest(); + var path = try std.fmt.allocPrint(main.stackAllocator.allocator, "{s}/{s}/blocks/textures/{s}.png", .{ assetFolder, mod, id }); + defer main.stackAllocator.free(path); + // Test if it's already in the list: + for (textureIds.items, 0..) |other, j| { + if (std.mem.eql(u8, other, textureId)) { + result = @intCast(j); + return result; + } + } + const file = main.files.cwd().openFile(path) catch |err| blk: { + if (err != error.FileNotFound) { + std.log.err("Could not open file {s}: {s}", .{ path, @errorName(err) }); + } + main.stackAllocator.free(path); + path = try std.fmt.allocPrint(main.stackAllocator.allocator, "assets/{s}/blocks/textures/{s}.png", .{ mod, id }); // Default to global assets. + break :blk main.files.cwd().openFile(path) catch |err2| { + std.log.err("File not found. Searched in \"{s}\" and also in the assetFolder \"{s}\"", .{ path, assetFolder }); + return err2; + }; + }; + file.close(main.io); // It was only openend to check if it exists. + // Otherwise read it into the list: + result = @intCast(textureIds.items.len); + + textureIds.append(main.worldArena, main.worldArena.dupe(u8, textureId)); + texturePaths.append(main.worldArena, main.worldArena.dupe(u8, path)); + return result; + } + + pub fn getTextureIndices(zon: ZonElement, assetFolder: []const u8, textureIndicesRef: *[16]u16) void { + const defaultIndex = findTexture(zon.get(?[]const u8, "texture", null), assetFolder) catch 0; + inline for (textureIndicesRef, 0..) |*ref, i| { + var textureId = zon.get(?[]const u8, std.fmt.comptimePrint("texture{}", .{i}), null); + if (i < sideNames.len) { + textureId = zon.get(?[]const u8, sideNames[i], textureId); + } + ref.* = findTexture(textureId, assetFolder) catch defaultIndex; + } + } + + pub fn register(assetFolder: []const u8, _: []const u8, zon: ZonElement) void { + _modelIndex[meshes.size] = _mode[meshes.size].createBlockModel(.{ .typ = @intCast(meshes.size), .data = 0 }, &_modeData[meshes.size], zon.getChild("model")); + + // The actual model is loaded later, in the rendering thread. + // But textures can be loaded here: + + getTextureIndices(zon, assetFolder, &textureIndices[meshes.size]); + + maxTextureCount[meshes.size] = @intCast(textureIds.items.len); + + meshes.size += 1; + } + + pub fn registerBlockBreakingAnimation(assetFolder: []const u8) void { + var i: usize = 0; + while (true) : (i += 1) { + const path1 = std.fmt.allocPrint(main.stackAllocator.allocator, "assets/cubyz/blocks/textures/breaking/{}.png", .{i}) catch unreachable; + defer main.stackAllocator.free(path1); + const path2 = std.fmt.allocPrint(main.stackAllocator.allocator, "{s}/cubyz/blocks/textures/breaking/{}.png", .{ assetFolder, i }) catch unreachable; + defer main.stackAllocator.free(path2); + if (!main.files.cwd().hasFile(path1) and !main.files.cwd().hasFile(path2)) break; + + const id = std.fmt.allocPrint(main.stackAllocator.allocator, "cubyz:breaking/{}", .{i}) catch unreachable; + defer main.stackAllocator.free(id); + blockBreakingTextures.append(main.worldArena, findTexture(id, assetFolder) catch break); + } + } + + pub fn preProcessAnimationData(time: u32) void { + animationComputePipeline.bind(); + c.glUniform1ui(animationUniforms.time, time); + c.glUniform1ui(animationUniforms.size, @intCast(animationData.len)); + c.glDispatchCompute(@intCast(@divFloor(animationData.len + 63, 64)), 1, 1); // TODO: Replace with @divCeil once available + c.glMemoryBarrier(c.GL_SHADER_STORAGE_BARRIER_BIT); + } + + fn finishTextureLoading() void { + animationData = main.worldArena.alloc(AnimationData, textureIds.items.len); + textureOcclusionData = main.worldArena.alloc(std.atomic.Value(bool), textureIds.items.len); + for (texturePaths.items, 0..) |path, i| { + readTextureData(i, path); + } + } + + pub fn reloadTextures(_: usize) void { + blockTextures.clearRetainingCapacity(); + emissionTextures.clearRetainingCapacity(); + reflectivityTextures.clearRetainingCapacity(); + absorptionTextures.clearRetainingCapacity(); + textureFogData.clearRetainingCapacity(); + for (texturePaths.items, 0..) |path, i| { + readTextureData(i, path); + } + generateTextureArray(); + } + + pub fn generateTextureArray() void { + blockTextureArray.generate(blockTextures.items, true, true); + c.glTexParameterf(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MAX_ANISOTROPY, @floatFromInt(main.settings.anisotropicFiltering)); + emissionTextureArray.generate(emissionTextures.items, true, false); + c.glTexParameterf(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MAX_ANISOTROPY, @floatFromInt(main.settings.anisotropicFiltering)); + const reflectivityAndAbsorptionTextures = main.stackAllocator.alloc(Image, reflectivityTextures.items.len); + defer main.stackAllocator.free(reflectivityAndAbsorptionTextures); + defer for (reflectivityAndAbsorptionTextures) |texture| { + texture.deinit(main.stackAllocator); + }; + for (reflectivityTextures.items, absorptionTextures.items, reflectivityAndAbsorptionTextures) |reflecitivityTexture, absorptionTexture, *resultTexture| { + const width = @max(reflecitivityTexture.width, absorptionTexture.width); + const height = @max(reflecitivityTexture.height, absorptionTexture.height); + resultTexture.* = Image.init(main.stackAllocator, width, height); + for (0..width) |x| { + for (0..height) |y| { + const reflectivity = reflecitivityTexture.getRGB(x * reflecitivityTexture.width / width, y * reflecitivityTexture.height / height); + const absorption = absorptionTexture.getRGB(x * absorptionTexture.width / width, y * absorptionTexture.height / height); + resultTexture.setRGB(x, y, .{ .r = absorption.r, .g = absorption.g, .b = absorption.b, .a = reflectivity.r }); + } + } + } + reflectivityAndAbsorptionTextureArray.generate(reflectivityAndAbsorptionTextures, true, false); + c.glTexParameterf(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MAX_ANISOTROPY, @floatFromInt(main.settings.anisotropicFiltering)); + + // Also generate additional buffers: + if (animationSSBO) |ssbo| { + ssbo.deinit(); + } + if (animatedTextureSSBO) |ssbo| { + ssbo.deinit(); + } + if (fogSSBO) |ssbo| { + ssbo.deinit(); + } + animationSSBO = SSBO.initStatic(AnimationData, animationData); + animationSSBO.?.bind(0); + + animatedTextureSSBO = SSBO.initStaticSize(u32, animationData.len); + animatedTextureSSBO.?.bind(1); + fogSSBO = SSBO.initStatic(FogData, textureFogData.items); + fogSSBO.?.bind(7); + } }; diff --git a/src/graphics.zig b/src/graphics.zig index 5291297a36..56a83c6efd 100644 --- a/src/graphics.zig +++ b/src/graphics.zig @@ -3,8 +3,6 @@ const std = @import("std"); const builtin = @import("builtin"); -const c = @import("c"); - const vec = @import("vec.zig"); const Mat4f = vec.Mat4f; const Vec4i = vec.Vec4i; @@ -17,6 +15,8 @@ const main = @import("main"); const Window = main.Window; const NeverFailingAllocator = main.heap.NeverFailingAllocator; +const c = @import("c"); + const pipelines = @import("graphics/pipelines.zig"); pub const ComputePipeline = pipelines.ComputePipeline; pub const Pipeline = pipelines.Pipeline; @@ -24,2269 +24,2269 @@ pub const Pipeline = pipelines.Pipeline; pub const vulkan = @import("graphics/vulkan.zig"); pub const draw = struct { // MARK: draw - var color: u32 = 0; - var clip: ?Vec4i = null; - var translation: Vec2f = Vec2f{0, 0}; - var scale: f32 = 1; - - pub fn setColor(newColor: u32) void { - color = newColor; - } - - pub fn setColorSameAlpha(newColor: u24) void { - color = (color & 0xff000000) | newColor; - } - - /// Returns the previous translation. - pub fn setTranslation(newTranslation: Vec2f) Vec2f { - const oldTranslation = translation; - translation += newTranslation*@as(Vec2f, @splat(scale)); - return oldTranslation; - } - - pub fn restoreTranslation(previousTranslation: Vec2f) void { - translation = previousTranslation; - } - - /// Returns the previous scale. - pub fn setScale(newScale: f32) f32 { - std.debug.assert(newScale >= 0); - const oldScale = scale; - scale *= newScale; - return oldScale; - } - - pub fn restoreScale(previousScale: f32) void { - scale = previousScale; - } - - /// Returns the previous clip. - pub fn setClip(clipRect: Vec2f) ?Vec4i { - std.debug.assert(@reduce(.And, clipRect >= Vec2f{0, 0})); - var viewport: [4]c_int = undefined; - c.glGetIntegerv(c.GL_VIEWPORT, &viewport); - var newClip = Vec4i{ - std.math.lossyCast(i32, translation[0]), - viewport[3] - std.math.lossyCast(i32, translation[1] + clipRect[1]*scale), - std.math.lossyCast(i32, clipRect[0]*scale), - std.math.lossyCast(i32, clipRect[1]*scale), - }; - if (clip) |oldClip| { - if (newClip[0] < oldClip[0]) { - newClip[2] -= oldClip[0] - newClip[0]; - newClip[0] += oldClip[0] - newClip[0]; - } - if (newClip[1] < oldClip[1]) { - newClip[3] -= oldClip[1] - newClip[1]; - newClip[1] += oldClip[1] - newClip[1]; - } - if (newClip[0] + newClip[2] > oldClip[0] + oldClip[2]) { - newClip[2] -= (newClip[0] + newClip[2]) - (oldClip[0] + oldClip[2]); - } - if (newClip[1] + newClip[3] > oldClip[1] + oldClip[3]) { - newClip[3] -= (newClip[1] + newClip[3]) - (oldClip[1] + oldClip[3]); - } - newClip[2] = @max(newClip[2], 0); - newClip[3] = @max(newClip[3], 0); - } - const oldClip = clip; - clip = newClip; - return oldClip; - } - - pub fn getScissor() ?c.VkRect2D { - const clipRect = clip orelse return null; - return .{ - .offset = .{ - .x = clipRect[0], - .y = clipRect[1], - }, - .extent = .{ - .width = @intCast(clipRect[2]), - .height = @intCast(clipRect[3]), - }, - }; - } - - /// Should be used to restore the old clip when leaving the render function. - pub fn restoreClip(previousClip: ?Vec4i) void { - clip = previousClip; - } - - pub const SimpleVertex2D = struct { - pos: [2]f32, - - pub const attributeDescriptions: []const c.VkVertexInputAttributeDescription = &.{ - .{ - .location = 0, - .format = c.VK_FORMAT_R32G32_SFLOAT, - .offset = @offsetOf(@This(), "pos"), - }, - }; - }; - - // ---------------------------------------------------------------------------- - // MARK: fillRect() - var rectUniforms: struct { - screen: c_int, - start: c_int, - size: c_int, - rectColor: c_int, - } = undefined; - var rectPipeline: Pipeline = undefined; - pub var rectVao: VertexArray = undefined; - - fn initRect() void { - rectPipeline = Pipeline.init( - "assets/cubyz/shaders/graphics/Rect.vert", - "assets/cubyz/shaders/graphics/Rect.frag", - "", - &rectUniforms, - SimpleVertex2D, - &.{}, - .{.cullMode = .none}, - .{.depthTest = false, .depthWrite = false}, - .{.attachments = &.{.alphaBlending}}, - ); - const rawData = [_]SimpleVertex2D{ - .{.pos = .{0, 0}}, - .{.pos = .{0, 1}}, - .{.pos = .{1, 0}}, - .{.pos = .{1, 1}}, - }; - - rectVao = .init(SimpleVertex2D, &rawData, null); - } - - fn deinitRect() void { - rectPipeline.deinit(); - rectVao.deinit(); - } - - pub fn rect(_pos: Vec2f, _dim: Vec2f) void { - var pos = _pos; - var dim = _dim; - pos *= @splat(scale); - pos += translation; - dim *= @splat(scale); - - rectPipeline.bind(getScissor()); - - var viewport: [4]c_int = undefined; - c.glGetIntegerv(c.GL_VIEWPORT, &viewport); - c.glUniform2f(rectUniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); - c.glUniform2f(rectUniforms.start, pos[0], pos[1]); - c.glUniform2f(rectUniforms.size, dim[0], dim[1]); - c.glUniform1i(rectUniforms.rectColor, @bitCast(color)); - - rectVao.bind(); - c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); - } - - // ---------------------------------------------------------------------------- - // MARK: fillRectBorder() - var rectBorderUniforms: struct { - screen: c_int, - start: c_int, - size: c_int, - rectColor: c_int, - lineWidth: c_int, - } = undefined; - var rectBorderPipeline: Pipeline = undefined; - var rectBorderVao: VertexArray = undefined; - - fn initRectBorder() void { - const RectBorderVertex = struct { - pos: [4]f32, - - pub const attributeDescriptions: []const c.VkVertexInputAttributeDescription = &.{ - .{ - .location = 0, - .format = c.VK_FORMAT_R32G32B32A32_SFLOAT, - .offset = @offsetOf(@This(), "pos"), - }, - }; - }; - rectBorderPipeline = Pipeline.init( - "assets/cubyz/shaders/graphics/RectBorder.vert", - "assets/cubyz/shaders/graphics/RectBorder.frag", - "", - &rectBorderUniforms, - RectBorderVertex, - &.{}, - .{.cullMode = .none}, - .{.depthTest = false, .depthWrite = false}, - .{.attachments = &.{.alphaBlending}}, - ); - const rawData = [_]RectBorderVertex{ - .{.pos = .{0, 0, 0, 0}}, - .{.pos = .{0, 0, 1, 1}}, - .{.pos = .{0, 1, 0, 0}}, - .{.pos = .{0, 1, 1, -1}}, - .{.pos = .{1, 1, 0, 0}}, - .{.pos = .{1, 1, -1, -1}}, - .{.pos = .{1, 0, 0, 0}}, - .{.pos = .{1, 0, -1, 1}}, - .{.pos = .{0, 0, 0, 0}}, - .{.pos = .{0, 0, 1, 1}}, - }; - - rectBorderVao = .init(RectBorderVertex, &rawData, null); - } - - fn deinitRectBorder() void { - rectBorderPipeline.deinit(); - rectBorderVao.deinit(); - } - - pub fn rectBorder(_pos: Vec2f, _dim: Vec2f, _width: f32) void { - var pos = _pos; - var dim = _dim; - var width = _width; - pos *= @splat(scale); - pos += translation; - dim *= @splat(scale); - width *= scale; - - rectBorderPipeline.bind(getScissor()); - - var viewport: [4]c_int = undefined; - c.glGetIntegerv(c.GL_VIEWPORT, &viewport); - c.glUniform2f(rectBorderUniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); - c.glUniform2f(rectBorderUniforms.start, pos[0], pos[1]); - c.glUniform2f(rectBorderUniforms.size, dim[0], dim[1]); - c.glUniform1i(rectBorderUniforms.rectColor, @bitCast(color)); - c.glUniform1f(rectBorderUniforms.lineWidth, width); - - rectBorderVao.bind(); - c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 10); - } - - // ---------------------------------------------------------------------------- - // MARK: drawLine() - var lineUniforms: struct { - screen: c_int, - start: c_int, - direction: c_int, - lineColor: c_int, - } = undefined; - var linePipeline: Pipeline = undefined; - var lineVao: VertexArray = undefined; - - fn initLine() void { - linePipeline = Pipeline.init( - "assets/cubyz/shaders/graphics/Line.vert", - "assets/cubyz/shaders/graphics/Line.frag", - "", - &lineUniforms, - SimpleVertex2D, - &.{}, - .{.cullMode = .none}, - .{.depthTest = false, .depthWrite = false}, - .{.attachments = &.{.alphaBlending}}, - ); - const rawData = [_]SimpleVertex2D{ - .{.pos = .{0, 0}}, - .{.pos = .{1, 1}}, - }; - - lineVao = .init(SimpleVertex2D, &rawData, null); - } - - fn deinitLine() void { - linePipeline.deinit(); - lineVao.deinit(); - } - - pub fn line(_pos1: Vec2f, _pos2: Vec2f) void { - var pos1 = _pos1; - var pos2 = _pos2; - pos1 *= @splat(scale); - pos1 += translation; - pos2 *= @splat(scale); - pos2 += translation; - - linePipeline.bind(getScissor()); - - var viewport: [4]c_int = undefined; - c.glGetIntegerv(c.GL_VIEWPORT, &viewport); - c.glUniform2f(lineUniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); - c.glUniform2f(lineUniforms.start, pos1[0], pos1[1]); - c.glUniform2f(lineUniforms.direction, pos2[0] - pos1[0], pos2[1] - pos1[1]); - c.glUniform1i(lineUniforms.lineColor, @bitCast(color)); - - lineVao.bind(); - c.glDrawArrays(c.GL_LINE_STRIP, 0, 2); - } - - pub fn rectOutline(_pos: Vec2f, _dim: Vec2f) void { - var pos = _pos; - var dim = _dim; - pos *= @splat(scale); - pos += translation; - dim *= @splat(scale); - - linePipeline.bind(getScissor()); - - var viewport: [4]c_int = undefined; - c.glGetIntegerv(c.GL_VIEWPORT, &viewport); - c.glUniform2f(lineUniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); - c.glUniform2f(lineUniforms.start, pos[0], pos[1]); // Move the coordinates, so they are in the center of a pixel. - c.glUniform2f(lineUniforms.direction, dim[0] - 1, dim[1] - 1); // The height is a lot smaller because the inner edge of the rect is drawn. - c.glUniform1i(lineUniforms.lineColor, @bitCast(color)); - - lineVao.bind(); - c.glDrawArrays(c.GL_LINE_LOOP, 0, 5); - } - - // ---------------------------------------------------------------------------- - // MARK: fillCircle() - var circleUniforms: struct { - screen: c_int, - center: c_int, - radius: c_int, - circleColor: c_int, - } = undefined; - var circlePipeline: Pipeline = undefined; - var circleVao: VertexArray = undefined; - - fn initCircle() void { - circlePipeline = Pipeline.init( - "assets/cubyz/shaders/graphics/Circle.vert", - "assets/cubyz/shaders/graphics/Circle.frag", - "", - &circleUniforms, - SimpleVertex2D, - &.{}, - .{.cullMode = .none}, - .{.depthTest = false, .depthWrite = false}, - .{.attachments = &.{.alphaBlending}}, - ); - const rawData = [_]SimpleVertex2D{ - .{.pos = .{-1, -1}}, - .{.pos = .{-1, 1}}, - .{.pos = .{1, -1}}, - .{.pos = .{1, 1}}, - }; - - circleVao = .init(SimpleVertex2D, &rawData, null); - } - - fn deinitCircle() void { - circlePipeline.deinit(); - circleVao.deinit(); - } - - pub fn circle(_center: Vec2f, _radius: f32) void { - var center = _center; - var radius = _radius; - center *= @splat(scale); - center += translation; - radius *= scale; - circlePipeline.bind(getScissor()); - - var viewport: [4]c_int = undefined; - c.glGetIntegerv(c.GL_VIEWPORT, &viewport); - c.glUniform2f(circleUniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); - c.glUniform2f(circleUniforms.center, center[0], center[1]); // Move the coordinates, so they are in the center of a pixel. - c.glUniform1f(circleUniforms.radius, radius); // The height is a lot smaller because the inner edge of the rect is drawn. - c.glUniform1i(circleUniforms.circleColor, @bitCast(color)); - - circleVao.bind(); - c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); - } - - // ---------------------------------------------------------------------------- - // MARK: drawImage() - // Luckily the vao of the regular rect can used. - var imageUniforms: struct { - screen: c_int, - start: c_int, - size: c_int, - color: c_int, - uvOffset: c_int, - uvDim: c_int, - } = undefined; - var imagePipeline: Pipeline = undefined; - - fn initImage() void { - imagePipeline = Pipeline.init( - "assets/cubyz/shaders/graphics/Image.vert", - "assets/cubyz/shaders/graphics/Image.frag", - "", - &imageUniforms, - SimpleVertex2D, - &.{}, - .{.cullMode = .none}, - .{.depthTest = false, .depthWrite = false}, - .{.attachments = &.{.alphaBlending}}, - ); - } - - fn deinitImage() void { - imagePipeline.deinit(); - } - - pub fn boundImage(_pos: Vec2f, _dim: Vec2f) void { - imagePipeline.bind(getScissor()); - - customShadedImage(&imageUniforms, _pos, _dim); - } - - pub fn boundSubImage(_pos: Vec2f, _dim: Vec2f, uvOffset: Vec2f, uvDim: Vec2f) void { - var pos = _pos; - var dim = _dim; - pos *= @splat(scale); - pos += translation; - dim *= @splat(scale); - pos = @floor(pos); - dim = @ceil(dim); - - imagePipeline.bind(getScissor()); - - var viewport: [4]c_int = undefined; - c.glGetIntegerv(c.GL_VIEWPORT, &viewport); - c.glUniform2f(imageUniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); - c.glUniform2f(imageUniforms.start, pos[0], pos[1]); - c.glUniform2f(imageUniforms.size, dim[0], dim[1]); - c.glUniform1i(imageUniforms.color, @bitCast(color)); - c.glUniform2f(imageUniforms.uvOffset, uvOffset[0], 1 - uvOffset[1] - uvDim[1]); - c.glUniform2f(imageUniforms.uvDim, uvDim[0], uvDim[1]); - - rectVao.bind(); - c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); - } - - pub fn customShadedImage(uniforms: anytype, _pos: Vec2f, _dim: Vec2f) void { - var pos = _pos; - var dim = _dim; - pos *= @splat(scale); - pos += translation; - dim *= @splat(scale); - pos = @floor(pos); - dim = @ceil(dim); - - var viewport: [4]c_int = undefined; - c.glGetIntegerv(c.GL_VIEWPORT, &viewport); - c.glUniform2f(uniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); - c.glUniform2f(uniforms.start, pos[0], pos[1]); - c.glUniform2f(uniforms.size, dim[0], dim[1]); - c.glUniform1i(uniforms.color, @bitCast(color)); - c.glUniform2f(uniforms.uvOffset, 0, 0); - c.glUniform2f(uniforms.uvDim, 1, 1); - - rectVao.bind(); - c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); - } - - // ---------------------------------------------------------------------------- - // MARK: customShadedRect() - - pub fn customShadedRect(uniforms: anytype, _pos: Vec2f, _dim: Vec2f) void { - var pos = _pos; - var dim = _dim; - pos *= @splat(scale); - pos += translation; - dim *= @splat(scale); - pos = @floor(pos); - dim = @ceil(dim); - - var viewport: [4]c_int = undefined; - c.glGetIntegerv(c.GL_VIEWPORT, &viewport); - c.glUniform2f(uniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); - c.glUniform2f(uniforms.start, pos[0], pos[1]); - c.glUniform2f(uniforms.size, dim[0], dim[1]); - c.glUniform1i(uniforms.color, @bitCast(color)); - c.glUniform1f(uniforms.scale, scale); - - rectVao.bind(); - c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); - } - - // ---------------------------------------------------------------------------- - // MARK: text() - - pub fn text(_text: []const u8, x: f32, y: f32, fontSize: f32, alignment: TextBuffer.Alignment) void { - TextRendering.renderText(_text, x, y, fontSize, .{.color = @truncate(@as(u32, @bitCast(color)))}, alignment); - } - - pub inline fn print(comptime format: []const u8, args: anytype, x: f32, y: f32, fontSize: f32, alignment: TextBuffer.Alignment) void { - const string = std.fmt.allocPrint(main.stackAllocator.allocator, format, args) catch unreachable; - defer main.stackAllocator.free(string); - text(string, x, y, fontSize, alignment); - } + var color: u32 = 0; + var clip: ?Vec4i = null; + var translation: Vec2f = Vec2f{ 0, 0 }; + var scale: f32 = 1; + + pub fn setColor(newColor: u32) void { + color = newColor; + } + + pub fn setColorSameAlpha(newColor: u24) void { + color = (color & 0xff000000) | newColor; + } + + /// Returns the previous translation. + pub fn setTranslation(newTranslation: Vec2f) Vec2f { + const oldTranslation = translation; + translation += newTranslation * @as(Vec2f, @splat(scale)); + return oldTranslation; + } + + pub fn restoreTranslation(previousTranslation: Vec2f) void { + translation = previousTranslation; + } + + /// Returns the previous scale. + pub fn setScale(newScale: f32) f32 { + std.debug.assert(newScale >= 0); + const oldScale = scale; + scale *= newScale; + return oldScale; + } + + pub fn restoreScale(previousScale: f32) void { + scale = previousScale; + } + + /// Returns the previous clip. + pub fn setClip(clipRect: Vec2f) ?Vec4i { + std.debug.assert(@reduce(.And, clipRect >= Vec2f{ 0, 0 })); + var viewport: [4]c_int = undefined; + c.glGetIntegerv(c.GL_VIEWPORT, &viewport); + var newClip = Vec4i{ + std.math.lossyCast(i32, translation[0]), + viewport[3] - std.math.lossyCast(i32, translation[1] + clipRect[1] * scale), + std.math.lossyCast(i32, clipRect[0] * scale), + std.math.lossyCast(i32, clipRect[1] * scale), + }; + if (clip) |oldClip| { + if (newClip[0] < oldClip[0]) { + newClip[2] -= oldClip[0] - newClip[0]; + newClip[0] += oldClip[0] - newClip[0]; + } + if (newClip[1] < oldClip[1]) { + newClip[3] -= oldClip[1] - newClip[1]; + newClip[1] += oldClip[1] - newClip[1]; + } + if (newClip[0] + newClip[2] > oldClip[0] + oldClip[2]) { + newClip[2] -= (newClip[0] + newClip[2]) - (oldClip[0] + oldClip[2]); + } + if (newClip[1] + newClip[3] > oldClip[1] + oldClip[3]) { + newClip[3] -= (newClip[1] + newClip[3]) - (oldClip[1] + oldClip[3]); + } + newClip[2] = @max(newClip[2], 0); + newClip[3] = @max(newClip[3], 0); + } + const oldClip = clip; + clip = newClip; + return oldClip; + } + + pub fn getScissor() ?c.VkRect2D { + const clipRect = clip orelse return null; + return .{ + .offset = .{ + .x = clipRect[0], + .y = clipRect[1], + }, + .extent = .{ + .width = @intCast(clipRect[2]), + .height = @intCast(clipRect[3]), + }, + }; + } + + /// Should be used to restore the old clip when leaving the render function. + pub fn restoreClip(previousClip: ?Vec4i) void { + clip = previousClip; + } + + pub const SimpleVertex2D = struct { + pos: [2]f32, + + pub const attributeDescriptions: []const c.VkVertexInputAttributeDescription = &.{ + .{ + .location = 0, + .format = c.VK_FORMAT_R32G32_SFLOAT, + .offset = @offsetOf(@This(), "pos"), + }, + }; + }; + + // ---------------------------------------------------------------------------- + // MARK: fillRect() + var rectUniforms: struct { + screen: c_int, + start: c_int, + size: c_int, + rectColor: c_int, + } = undefined; + var rectPipeline: Pipeline = undefined; + pub var rectVao: VertexArray = undefined; + + fn initRect() void { + rectPipeline = Pipeline.init( + "assets/cubyz/shaders/graphics/Rect.vert", + "assets/cubyz/shaders/graphics/Rect.frag", + "", + &rectUniforms, + SimpleVertex2D, + &.{}, + .{ .cullMode = .none }, + .{ .depthTest = false, .depthWrite = false }, + .{ .attachments = &.{.alphaBlending} }, + ); + const rawData = [_]SimpleVertex2D{ + .{ .pos = .{ 0, 0 } }, + .{ .pos = .{ 0, 1 } }, + .{ .pos = .{ 1, 0 } }, + .{ .pos = .{ 1, 1 } }, + }; + + rectVao = .init(SimpleVertex2D, &rawData, null); + } + + fn deinitRect() void { + rectPipeline.deinit(); + rectVao.deinit(); + } + + pub fn rect(_pos: Vec2f, _dim: Vec2f) void { + var pos = _pos; + var dim = _dim; + pos *= @splat(scale); + pos += translation; + dim *= @splat(scale); + + rectPipeline.bind(getScissor()); + + var viewport: [4]c_int = undefined; + c.glGetIntegerv(c.GL_VIEWPORT, &viewport); + c.glUniform2f(rectUniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); + c.glUniform2f(rectUniforms.start, pos[0], pos[1]); + c.glUniform2f(rectUniforms.size, dim[0], dim[1]); + c.glUniform1i(rectUniforms.rectColor, @bitCast(color)); + + rectVao.bind(); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + } + + // ---------------------------------------------------------------------------- + // MARK: fillRectBorder() + var rectBorderUniforms: struct { + screen: c_int, + start: c_int, + size: c_int, + rectColor: c_int, + lineWidth: c_int, + } = undefined; + var rectBorderPipeline: Pipeline = undefined; + var rectBorderVao: VertexArray = undefined; + + fn initRectBorder() void { + const RectBorderVertex = struct { + pos: [4]f32, + + pub const attributeDescriptions: []const c.VkVertexInputAttributeDescription = &.{ + .{ + .location = 0, + .format = c.VK_FORMAT_R32G32B32A32_SFLOAT, + .offset = @offsetOf(@This(), "pos"), + }, + }; + }; + rectBorderPipeline = Pipeline.init( + "assets/cubyz/shaders/graphics/RectBorder.vert", + "assets/cubyz/shaders/graphics/RectBorder.frag", + "", + &rectBorderUniforms, + RectBorderVertex, + &.{}, + .{ .cullMode = .none }, + .{ .depthTest = false, .depthWrite = false }, + .{ .attachments = &.{.alphaBlending} }, + ); + const rawData = [_]RectBorderVertex{ + .{ .pos = .{ 0, 0, 0, 0 } }, + .{ .pos = .{ 0, 0, 1, 1 } }, + .{ .pos = .{ 0, 1, 0, 0 } }, + .{ .pos = .{ 0, 1, 1, -1 } }, + .{ .pos = .{ 1, 1, 0, 0 } }, + .{ .pos = .{ 1, 1, -1, -1 } }, + .{ .pos = .{ 1, 0, 0, 0 } }, + .{ .pos = .{ 1, 0, -1, 1 } }, + .{ .pos = .{ 0, 0, 0, 0 } }, + .{ .pos = .{ 0, 0, 1, 1 } }, + }; + + rectBorderVao = .init(RectBorderVertex, &rawData, null); + } + + fn deinitRectBorder() void { + rectBorderPipeline.deinit(); + rectBorderVao.deinit(); + } + + pub fn rectBorder(_pos: Vec2f, _dim: Vec2f, _width: f32) void { + var pos = _pos; + var dim = _dim; + var width = _width; + pos *= @splat(scale); + pos += translation; + dim *= @splat(scale); + width *= scale; + + rectBorderPipeline.bind(getScissor()); + + var viewport: [4]c_int = undefined; + c.glGetIntegerv(c.GL_VIEWPORT, &viewport); + c.glUniform2f(rectBorderUniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); + c.glUniform2f(rectBorderUniforms.start, pos[0], pos[1]); + c.glUniform2f(rectBorderUniforms.size, dim[0], dim[1]); + c.glUniform1i(rectBorderUniforms.rectColor, @bitCast(color)); + c.glUniform1f(rectBorderUniforms.lineWidth, width); + + rectBorderVao.bind(); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 10); + } + + // ---------------------------------------------------------------------------- + // MARK: drawLine() + var lineUniforms: struct { + screen: c_int, + start: c_int, + direction: c_int, + lineColor: c_int, + } = undefined; + var linePipeline: Pipeline = undefined; + var lineVao: VertexArray = undefined; + + fn initLine() void { + linePipeline = Pipeline.init( + "assets/cubyz/shaders/graphics/Line.vert", + "assets/cubyz/shaders/graphics/Line.frag", + "", + &lineUniforms, + SimpleVertex2D, + &.{}, + .{ .cullMode = .none }, + .{ .depthTest = false, .depthWrite = false }, + .{ .attachments = &.{.alphaBlending} }, + ); + const rawData = [_]SimpleVertex2D{ + .{ .pos = .{ 0, 0 } }, + .{ .pos = .{ 1, 1 } }, + }; + + lineVao = .init(SimpleVertex2D, &rawData, null); + } + + fn deinitLine() void { + linePipeline.deinit(); + lineVao.deinit(); + } + + pub fn line(_pos1: Vec2f, _pos2: Vec2f) void { + var pos1 = _pos1; + var pos2 = _pos2; + pos1 *= @splat(scale); + pos1 += translation; + pos2 *= @splat(scale); + pos2 += translation; + + linePipeline.bind(getScissor()); + + var viewport: [4]c_int = undefined; + c.glGetIntegerv(c.GL_VIEWPORT, &viewport); + c.glUniform2f(lineUniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); + c.glUniform2f(lineUniforms.start, pos1[0], pos1[1]); + c.glUniform2f(lineUniforms.direction, pos2[0] - pos1[0], pos2[1] - pos1[1]); + c.glUniform1i(lineUniforms.lineColor, @bitCast(color)); + + lineVao.bind(); + c.glDrawArrays(c.GL_LINE_STRIP, 0, 2); + } + + pub fn rectOutline(_pos: Vec2f, _dim: Vec2f) void { + var pos = _pos; + var dim = _dim; + pos *= @splat(scale); + pos += translation; + dim *= @splat(scale); + + linePipeline.bind(getScissor()); + + var viewport: [4]c_int = undefined; + c.glGetIntegerv(c.GL_VIEWPORT, &viewport); + c.glUniform2f(lineUniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); + c.glUniform2f(lineUniforms.start, pos[0], pos[1]); // Move the coordinates, so they are in the center of a pixel. + c.glUniform2f(lineUniforms.direction, dim[0] - 1, dim[1] - 1); // The height is a lot smaller because the inner edge of the rect is drawn. + c.glUniform1i(lineUniforms.lineColor, @bitCast(color)); + + lineVao.bind(); + c.glDrawArrays(c.GL_LINE_LOOP, 0, 5); + } + + // ---------------------------------------------------------------------------- + // MARK: fillCircle() + var circleUniforms: struct { + screen: c_int, + center: c_int, + radius: c_int, + circleColor: c_int, + } = undefined; + var circlePipeline: Pipeline = undefined; + var circleVao: VertexArray = undefined; + + fn initCircle() void { + circlePipeline = Pipeline.init( + "assets/cubyz/shaders/graphics/Circle.vert", + "assets/cubyz/shaders/graphics/Circle.frag", + "", + &circleUniforms, + SimpleVertex2D, + &.{}, + .{ .cullMode = .none }, + .{ .depthTest = false, .depthWrite = false }, + .{ .attachments = &.{.alphaBlending} }, + ); + const rawData = [_]SimpleVertex2D{ + .{ .pos = .{ -1, -1 } }, + .{ .pos = .{ -1, 1 } }, + .{ .pos = .{ 1, -1 } }, + .{ .pos = .{ 1, 1 } }, + }; + + circleVao = .init(SimpleVertex2D, &rawData, null); + } + + fn deinitCircle() void { + circlePipeline.deinit(); + circleVao.deinit(); + } + + pub fn circle(_center: Vec2f, _radius: f32) void { + var center = _center; + var radius = _radius; + center *= @splat(scale); + center += translation; + radius *= scale; + circlePipeline.bind(getScissor()); + + var viewport: [4]c_int = undefined; + c.glGetIntegerv(c.GL_VIEWPORT, &viewport); + c.glUniform2f(circleUniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); + c.glUniform2f(circleUniforms.center, center[0], center[1]); // Move the coordinates, so they are in the center of a pixel. + c.glUniform1f(circleUniforms.radius, radius); // The height is a lot smaller because the inner edge of the rect is drawn. + c.glUniform1i(circleUniforms.circleColor, @bitCast(color)); + + circleVao.bind(); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + } + + // ---------------------------------------------------------------------------- + // MARK: drawImage() + // Luckily the vao of the regular rect can used. + var imageUniforms: struct { + screen: c_int, + start: c_int, + size: c_int, + color: c_int, + uvOffset: c_int, + uvDim: c_int, + } = undefined; + var imagePipeline: Pipeline = undefined; + + fn initImage() void { + imagePipeline = Pipeline.init( + "assets/cubyz/shaders/graphics/Image.vert", + "assets/cubyz/shaders/graphics/Image.frag", + "", + &imageUniforms, + SimpleVertex2D, + &.{}, + .{ .cullMode = .none }, + .{ .depthTest = false, .depthWrite = false }, + .{ .attachments = &.{.alphaBlending} }, + ); + } + + fn deinitImage() void { + imagePipeline.deinit(); + } + + pub fn boundImage(_pos: Vec2f, _dim: Vec2f) void { + imagePipeline.bind(getScissor()); + + customShadedImage(&imageUniforms, _pos, _dim); + } + + pub fn boundSubImage(_pos: Vec2f, _dim: Vec2f, uvOffset: Vec2f, uvDim: Vec2f) void { + var pos = _pos; + var dim = _dim; + pos *= @splat(scale); + pos += translation; + dim *= @splat(scale); + pos = @floor(pos); + dim = @ceil(dim); + + imagePipeline.bind(getScissor()); + + var viewport: [4]c_int = undefined; + c.glGetIntegerv(c.GL_VIEWPORT, &viewport); + c.glUniform2f(imageUniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); + c.glUniform2f(imageUniforms.start, pos[0], pos[1]); + c.glUniform2f(imageUniforms.size, dim[0], dim[1]); + c.glUniform1i(imageUniforms.color, @bitCast(color)); + c.glUniform2f(imageUniforms.uvOffset, uvOffset[0], 1 - uvOffset[1] - uvDim[1]); + c.glUniform2f(imageUniforms.uvDim, uvDim[0], uvDim[1]); + + rectVao.bind(); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + } + + pub fn customShadedImage(uniforms: anytype, _pos: Vec2f, _dim: Vec2f) void { + var pos = _pos; + var dim = _dim; + pos *= @splat(scale); + pos += translation; + dim *= @splat(scale); + pos = @floor(pos); + dim = @ceil(dim); + + var viewport: [4]c_int = undefined; + c.glGetIntegerv(c.GL_VIEWPORT, &viewport); + c.glUniform2f(uniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); + c.glUniform2f(uniforms.start, pos[0], pos[1]); + c.glUniform2f(uniforms.size, dim[0], dim[1]); + c.glUniform1i(uniforms.color, @bitCast(color)); + c.glUniform2f(uniforms.uvOffset, 0, 0); + c.glUniform2f(uniforms.uvDim, 1, 1); + + rectVao.bind(); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + } + + // ---------------------------------------------------------------------------- + // MARK: customShadedRect() + + pub fn customShadedRect(uniforms: anytype, _pos: Vec2f, _dim: Vec2f) void { + var pos = _pos; + var dim = _dim; + pos *= @splat(scale); + pos += translation; + dim *= @splat(scale); + pos = @floor(pos); + dim = @ceil(dim); + + var viewport: [4]c_int = undefined; + c.glGetIntegerv(c.GL_VIEWPORT, &viewport); + c.glUniform2f(uniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); + c.glUniform2f(uniforms.start, pos[0], pos[1]); + c.glUniform2f(uniforms.size, dim[0], dim[1]); + c.glUniform1i(uniforms.color, @bitCast(color)); + c.glUniform1f(uniforms.scale, scale); + + rectVao.bind(); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + } + + // ---------------------------------------------------------------------------- + // MARK: text() + + pub fn text(_text: []const u8, x: f32, y: f32, fontSize: f32, alignment: TextBuffer.Alignment) void { + TextRendering.renderText(_text, x, y, fontSize, .{ .color = @truncate(@as(u32, @bitCast(color))) }, alignment); + } + + pub inline fn print(comptime format: []const u8, args: anytype, x: f32, y: f32, fontSize: f32, alignment: TextBuffer.Alignment) void { + const string = std.fmt.allocPrint(main.stackAllocator.allocator, format, args) catch unreachable; + defer main.stackAllocator.free(string); + text(string, x, y, fontSize, alignment); + } }; pub const TextBuffer = struct { // MARK: TextBuffer - pub const Alignment = enum { - left, - center, - right, - }; - - pub const FontEffect = packed struct(u28) { - color: u24 = 0xffffff, - bold: bool = false, - italic: bool = false, - underline: bool = false, - strikethrough: bool = false, - - fn hasLine(self: FontEffect, comptime isUnderline: bool) bool { - if (isUnderline) return self.underline; - return self.strikethrough; - } - }; - - const Line = struct { - start: f32, - end: f32, - color: u24, - isUnderline: bool, - }; - - const LineBreak = struct { - index: u32, - width: f32, - }; - - const GlyphData = struct { - x_advance: f32, - y_advance: f32, - x_offset: f32, - y_offset: f32, - character: u21, - index: u32, - cluster: u32, - fontEffect: FontEffect, - characterIndex: u32, - }; - - alignment: Alignment, - width: f32, - buffer: ?*c.hb_buffer_t, - glyphs: []GlyphData, - lines: main.List(Line), - lineBreaks: main.List(LineBreak), - - fn addLine(self: *TextBuffer, line: Line) void { - if (line.start != line.end) { - self.lines.append(line); - } - } - - fn initLines(self: *TextBuffer, comptime isUnderline: bool) void { - var line: Line = Line{.start = 0, .end = 0, .color = 0, .isUnderline = isUnderline}; - var lastFontEffect: FontEffect = .{}; - for (self.glyphs) |glyph| { - const fontEffect = glyph.fontEffect; - if (lastFontEffect.hasLine(isUnderline)) { - if (fontEffect.color != lastFontEffect.color) { - self.addLine(line); - line.color = fontEffect.color; - line.start = line.end; - } else if (!fontEffect.hasLine(isUnderline)) { - self.addLine(line); - } - } else if (fontEffect.hasLine(isUnderline)) { - line.start = line.end; - line.color = fontEffect.color; - } - lastFontEffect = fontEffect; - line.end += glyph.x_advance; - } - if (lastFontEffect.hasLine(isUnderline)) { - self.addLine(line); - } - } - - pub const Parser = struct { - unicodeIterator: std.unicode.Utf8Iterator, - currentFontEffect: FontEffect, - parsedText: main.List(u32), - fontEffects: main.List(FontEffect), - characterIndex: main.List(u32), - showControlCharacters: bool, - curChar: u21 = undefined, - curIndex: u32 = 0, - - fn appendControlGetNext(self: *Parser) ?void { - if (self.showControlCharacters) { - self.fontEffects.append(.{.color = 0x808080}); - self.parsedText.append(self.curChar); - self.characterIndex.append(self.curIndex); - } - self.curIndex = @intCast(self.unicodeIterator.i); - self.curChar = self.unicodeIterator.nextCodepoint() orelse return null; - } - - fn appendGetNext(self: *Parser) ?void { - self.fontEffects.append(self.currentFontEffect); - self.parsedText.append(self.curChar); - self.characterIndex.append(self.curIndex); - self.curIndex = @intCast(self.unicodeIterator.i); - self.curChar = self.unicodeIterator.nextCodepoint() orelse return null; - } - - fn peekNextByte(self: *Parser) u8 { - const next = self.unicodeIterator.peek(1); - if (next.len == 0) return 0; - return next[0]; - } - - fn parse(self: *Parser) void { - self.curIndex = @intCast(self.unicodeIterator.i); - self.curChar = self.unicodeIterator.nextCodepoint() orelse return; - while (true) switch (self.curChar) { - '*' => { - self.appendControlGetNext() orelse return; - if (self.curChar == '*') { - self.appendControlGetNext() orelse return; - self.currentFontEffect.bold = !self.currentFontEffect.bold; - } else { - self.currentFontEffect.italic = !self.currentFontEffect.italic; - } - }, - '_' => { - if (self.peekNextByte() == '_') { - self.appendControlGetNext() orelse return; - self.appendControlGetNext() orelse return; - self.currentFontEffect.underline = !self.currentFontEffect.underline; - } else { - self.appendGetNext() orelse return; - } - }, - '~' => { - if (self.peekNextByte() == '~') { - self.appendControlGetNext() orelse return; - self.appendControlGetNext() orelse return; - self.currentFontEffect.strikethrough = !self.currentFontEffect.strikethrough; - } else { - self.appendGetNext() orelse return; - } - }, - '\\' => { - self.appendControlGetNext() orelse return; - self.appendGetNext() orelse return; - }, - '#' => { - self.appendControlGetNext() orelse return; - var shift: u5 = 20; - while (true) : (shift -= 4) { - self.currentFontEffect.color = (self.currentFontEffect.color & ~(@as(u24, 0xf) << shift)) | @as(u24, switch (self.curChar) { - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => self.curChar - '0', - 'a', 'b', 'c', 'd', 'e', 'f' => self.curChar - 'a' + 10, - 'A', 'B', 'C', 'D', 'E', 'F' => self.curChar - 'A' + 10, - else => 0, - }) << shift; - self.appendControlGetNext() orelse return; - if (shift == 0) break; - } - }, - '§' => { - self.currentFontEffect = .{.color = self.currentFontEffect.color}; - self.appendControlGetNext() orelse return; - }, - else => { - self.appendGetNext() orelse return; - }, - }; - } - - pub fn countVisibleCharacters(text: []const u8) usize { - var unicodeIterator = std.unicode.Utf8Iterator{.bytes = text, .i = 0}; - var count: usize = 0; - var curChar = unicodeIterator.nextCodepoint() orelse return count; - outer: while (true) switch (curChar) { - '*' => { - curChar = unicodeIterator.nextCodepoint() orelse break; - }, - '_' => { - curChar = unicodeIterator.nextCodepoint() orelse break; - if (curChar == '_') { - curChar = unicodeIterator.nextCodepoint() orelse break; - } else { - count += 1; - } - }, - '~' => { - curChar = unicodeIterator.nextCodepoint() orelse break; - if (curChar == '~') { - curChar = unicodeIterator.nextCodepoint() orelse break; - } else { - count += 1; - } - }, - '\\' => { - curChar = unicodeIterator.nextCodepoint() orelse break; - curChar = unicodeIterator.nextCodepoint() orelse break; - count += 1; - }, - '#' => { - for (0..7) |_| curChar = unicodeIterator.nextCodepoint() orelse break :outer; - }, - '§' => { - curChar = unicodeIterator.nextCodepoint() orelse break; - }, - else => { - count += 1; - curChar = unicodeIterator.nextCodepoint() orelse break; - }, - }; - return count; - } - }; - - pub fn init(allocator: NeverFailingAllocator, text: []const u8, initialFontEffect: FontEffect, showControlCharacters: bool, alignment: Alignment) TextBuffer { - var self: TextBuffer = .{ - .alignment = alignment, - .width = 1e9, - .buffer = null, - .glyphs = &.{}, - .lines = .init(allocator), - .lineBreaks = .init(allocator), - }; - // Parse the input text: - var parser = Parser{ - .unicodeIterator = std.unicode.Utf8Iterator{.bytes = text, .i = 0}, - .currentFontEffect = initialFontEffect, - .parsedText = .init(main.stackAllocator), - .fontEffects = .init(allocator), - .characterIndex = .init(allocator), - .showControlCharacters = showControlCharacters, - }; - defer parser.fontEffects.deinit(); - defer parser.parsedText.deinit(); - defer parser.characterIndex.deinit(); - parser.parse(); - if (parser.parsedText.items.len == 0) { - self.lineBreaks.append(.{.index = 0, .width = 0}); - return self; - } - - // Let harfbuzz do its thing: - const buffer = c.hb_buffer_create() orelse @panic("Out of Memory while creating harfbuzz buffer"); - defer c.hb_buffer_destroy(buffer); - c.hb_buffer_add_utf32(buffer, parser.parsedText.items.ptr, @intCast(parser.parsedText.items.len), 0, @intCast(parser.parsedText.items.len)); - c.hb_buffer_set_direction(buffer, c.HB_DIRECTION_LTR); - c.hb_buffer_set_script(buffer, c.HB_SCRIPT_COMMON); - c.hb_buffer_set_language(buffer, c.hb_language_get_default()); - c.hb_shape(TextRendering.harfbuzzFont, buffer, null, 0); - var glyphInfos: []c.hb_glyph_info_t = undefined; - var glyphPositions: []c.hb_glyph_position_t = undefined; - { - var len: c_uint = 0; - glyphInfos.ptr = c.hb_buffer_get_glyph_infos(buffer, &len).?; - glyphPositions.ptr = c.hb_buffer_get_glyph_positions(buffer, &len).?; - glyphInfos.len = len; - glyphPositions.len = len; - } - - // Guess the text index from the given cluster indices. Only works if the number of glyphs and the number of characters in a cluster is the same. - const textIndexGuess = main.stackAllocator.alloc(u32, glyphInfos.len); - defer main.stackAllocator.free(textIndexGuess); - for (textIndexGuess, 0..) |*index, i| { - if (i == 0 or glyphInfos[i - 1].cluster != glyphInfos[i].cluster) { - index.* = glyphInfos[i].cluster; - } else { - index.* = @min(textIndexGuess[i - 1] + 1, @as(u32, @intCast(parser.parsedText.items.len - 1))); - for (glyphInfos[i..]) |glyphInfo| { - if (glyphInfo.cluster != glyphInfos[i].cluster) { - index.* = @min(index.*, glyphInfo.cluster - 1); - break; - } - } - } - } - - // Merge it all together: - self.glyphs = allocator.alloc(GlyphData, glyphInfos.len); - for (self.glyphs, 0..) |*glyph, i| { - glyph.x_advance = @as(f32, @floatFromInt(glyphPositions[i].x_advance))/TextRendering.fontUnitsPerPixel; - glyph.y_advance = @as(f32, @floatFromInt(glyphPositions[i].y_advance))/TextRendering.fontUnitsPerPixel; - glyph.x_offset = @as(f32, @floatFromInt(glyphPositions[i].x_offset))/TextRendering.fontUnitsPerPixel; - glyph.y_offset = @as(f32, @floatFromInt(glyphPositions[i].y_offset))/TextRendering.fontUnitsPerPixel; - glyph.character = @intCast(parser.parsedText.items[textIndexGuess[i]]); - glyph.index = glyphInfos[i].codepoint; - glyph.cluster = glyphInfos[i].cluster; - glyph.fontEffect = parser.fontEffects.items[textIndexGuess[i]]; - glyph.characterIndex = parser.characterIndex.items[textIndexGuess[i]]; - } - - // Find the lines: - self.initLines(true); - self.initLines(false); - self.lineBreaks.append(.{.index = 0, .width = 0}); - self.lineBreaks.append(.{.index = @intCast(self.glyphs.len), .width = 0}); - return self; - } - - pub fn deinit(self: TextBuffer) void { - self.lines.allocator.free(self.glyphs); - self.lines.deinit(); - self.lineBreaks.deinit(); - } - - fn getLineOffset(self: TextBuffer, line: usize) f32 { - const factor: f32 = switch (self.alignment) { - .left => 0, - .center => 0.5, - .right => 1, - }; - const diff = self.width - self.lineBreaks.items[line + 1].width; - return diff*factor; - } - - pub fn mousePosToIndex(self: TextBuffer, mousePos: Vec2f, bufferLen: usize) u32 { - var line: usize = @trunc(@max(0, mousePos[1]/16.0)); - line = @min(line, self.lineBreaks.items.len - 2); - var x: f32 = self.getLineOffset(line); - const start = self.lineBreaks.items[line].index; - const end = self.lineBreaks.items[line + 1].index; - for (self.glyphs[start..end]) |glyph| { - if (mousePos[0] < x + glyph.x_advance/2) { - return @intCast(glyph.characterIndex); - } - - x += glyph.x_advance; - } - return @intCast(if (end < self.glyphs.len) self.glyphs[end - 1].characterIndex else bufferLen); - } - - pub fn indexToCursorPos(self: TextBuffer, index: u32) Vec2f { - var x: f32 = 0; - var y: f32 = 0; - var i: usize = 0; - while (true) { - x = self.getLineOffset(i); - for (self.glyphs[self.lineBreaks.items[i].index..self.lineBreaks.items[i + 1].index]) |glyph| { - if (glyph.characterIndex == index) { - return .{x, y}; - } - - x += glyph.x_advance; - y -= glyph.y_advance; - } - i += 1; - if (i >= self.lineBreaks.items.len - 1) { - return .{x, y}; - } - y += 16; - } - } - - /// Returns the calculated dimensions of the text block. - pub fn calculateLineBreaks(self: *TextBuffer, fontSize: f32, maxLineWidth: f32) Vec2f { - self.lineBreaks.clearRetainingCapacity(); - const spaceCharacterWidth = 8; - self.lineBreaks.append(.{.index = 0, .width = 0}); - const scaledMaxWidth = maxLineWidth/fontSize*16.0; - var lineWidth: f32 = 0; - var lastSpaceWidth: f32 = 0; - var lastSpaceIndex: u32 = 0; - for (self.glyphs, 0..) |glyph, i| { - lineWidth += glyph.x_advance; - if (glyph.character == ' ') { - lastSpaceWidth = lineWidth; - lastSpaceIndex = @intCast(i + 1); - } - if (glyph.character == '\n') { - self.lineBreaks.append(.{.index = @intCast(i + 1), .width = lineWidth - spaceCharacterWidth}); - lineWidth = 0; - lastSpaceIndex = 0; - lastSpaceWidth = 0; - } - if (lineWidth > scaledMaxWidth) { - if (lastSpaceIndex != 0) { - lineWidth -= lastSpaceWidth; - self.lineBreaks.append(.{.index = lastSpaceIndex, .width = lastSpaceWidth - spaceCharacterWidth}); - lastSpaceIndex = 0; - lastSpaceWidth = 0; - } else { - self.lineBreaks.append(.{.index = @intCast(i), .width = lineWidth - glyph.x_advance}); - lineWidth = glyph.x_advance; - lastSpaceIndex = 0; - lastSpaceWidth = 0; - } - } - } - self.width = maxLineWidth; - self.lineBreaks.append(.{.index = @intCast(self.glyphs.len), .width = lineWidth}); - return Vec2f{maxLineWidth*fontSize/16.0, @as(f32, @floatFromInt(self.lineBreaks.items.len - 1))*fontSize}; - } - - pub fn drawSelection(self: TextBuffer, pos: Vec2f, selectionStart: u32, selectionEnd: u32) void { - std.debug.assert(selectionStart <= selectionEnd); - var x: f32 = self.getLineOffset(0); - var y: f32 = 0; - var i: usize = 0; - var j: usize = 0; - // Find the start row: - outer: while (i < self.lineBreaks.items.len - 1) : (i += 1) { - x = self.getLineOffset(i); - while (j < self.lineBreaks.items[i + 1].index) : (j += 1) { - const glyph = self.glyphs[j]; - if (glyph.characterIndex >= selectionStart) break :outer; - x += glyph.x_advance; - y -= glyph.y_advance; - } - y += 16; - } - while (i < self.lineBreaks.items.len - 1) { - const startX = x; - while (j < self.lineBreaks.items[i + 1].index and j < selectionEnd) : (j += 1) { - const glyph = self.glyphs[j]; - if (glyph.characterIndex >= selectionEnd) break; - x += glyph.x_advance; - y -= glyph.y_advance; - } - draw.rect(pos + Vec2f{startX, y}, .{x - startX, 16}); - i += 1; - if (i >= self.lineBreaks.items.len - 1) break; - x = self.getLineOffset(i); - y += 16; - } - } - - pub fn render(self: TextBuffer, _x: f32, _y: f32, _fontSize: f32) void { - self.renderShadow(_x, _y, _fontSize); - self.renderTextWithoutShadow(_x, _y, _fontSize); - } - - pub fn renderTextWithoutShadow(self: TextBuffer, _x: f32, _y: f32, _fontSize: f32) void { - const oldTranslation = draw.setTranslation(.{_x, _y}); - defer draw.restoreTranslation(oldTranslation); - const oldScale = draw.setScale(_fontSize/16.0); - defer draw.restoreScale(oldScale); - var x: f32 = 0; - var y: f32 = 0; - TextRendering.pipeline.bind(draw.getScissor()); - var viewport: [4]c_int = undefined; - c.glGetIntegerv(c.GL_VIEWPORT, &viewport); - c.glUniform2f(TextRendering.uniforms.scene, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); - c.glUniform1f(TextRendering.uniforms.ratio, draw.scale); - c.glUniform1f(TextRendering.uniforms.alpha, @as(f32, @floatFromInt(draw.color >> 24))/255.0); - c.glActiveTexture(c.GL_TEXTURE0); - c.glBindTexture(c.GL_TEXTURE_2D, TextRendering.glyphTexture[0]); - draw.rectVao.bind(); - const lineWraps: []f32 = main.stackAllocator.alloc(f32, self.lineBreaks.items.len - 1); - defer main.stackAllocator.free(lineWraps); - var i: usize = 0; - while (i < self.lineBreaks.items.len - 1) : (i += 1) { - x = self.getLineOffset(i); - for (self.glyphs[self.lineBreaks.items[i].index..self.lineBreaks.items[i + 1].index]) |glyph| { - if (glyph.character != '\n') { - const ftGlyph = TextRendering.getGlyph(glyph.index) catch continue; - TextRendering.drawGlyph(ftGlyph, x + glyph.x_offset, y - glyph.y_offset, @bitCast(glyph.fontEffect)); - } - x += glyph.x_advance; - y -= glyph.y_advance; - } - lineWraps[i] = x - self.getLineOffset(i); - x = 0; - y += 16; - } - - for (self.lines.items) |_line| { - var line: Line = _line; - y = 0; - y += if (line.isUnderline) 15 else 8; - const oldColor = draw.color; - draw.setColor(line.color | (@as(u32, 0xff000000) & draw.color)); - defer draw.setColor(oldColor); - for (lineWraps, 0..) |lineWrap, j| { - const lineStart = @max(0, line.start); - const lineEnd = @min(lineWrap, line.end); - if (lineStart < lineEnd) { - const start = Vec2f{lineStart + self.getLineOffset(j), y}; - const dim = Vec2f{lineEnd - lineStart, 1}; - draw.rect(start, dim); - } - line.start -= lineWrap; - line.end -= lineWrap; - y += 16; - } - } - } - - fn shadowColor(color: u24) u24 { - const r: f32 = @floatFromInt(color >> 16); - const g: f32 = @floatFromInt(color >> 8 & 255); - const b: f32 = @floatFromInt(color & 255); - const perceivedBrightness = @sqrt(0.299*r*r + 0.587*g*g + 0.114*b*b); - if (perceivedBrightness < 64) { - return 0xffffff; // Make shadows white for better readability. - } else { - return 0; - } - } - - fn renderShadow(self: TextBuffer, _x: f32, _y: f32, _fontSize: f32) void { // Basically a copy of render with some color and position changes. - const oldTranslation = draw.setTranslation(.{_x + _fontSize/16.0, _y + _fontSize/16.0}); - defer draw.restoreTranslation(oldTranslation); - const oldScale = draw.setScale(_fontSize/16.0); - defer draw.restoreScale(oldScale); - var x: f32 = 0; - var y: f32 = 0; - TextRendering.pipeline.bind(draw.getScissor()); - var viewport: [4]c_int = undefined; - c.glGetIntegerv(c.GL_VIEWPORT, &viewport); - c.glUniform2f(TextRendering.uniforms.scene, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); - c.glUniform1f(TextRendering.uniforms.ratio, draw.scale); - c.glUniform1f(TextRendering.uniforms.alpha, @as(f32, @floatFromInt(draw.color >> 24))/255.0); - c.glActiveTexture(c.GL_TEXTURE0); - c.glBindTexture(c.GL_TEXTURE_2D, TextRendering.glyphTexture[0]); - draw.rectVao.bind(); - const lineWraps: []f32 = main.stackAllocator.alloc(f32, self.lineBreaks.items.len - 1); - defer main.stackAllocator.free(lineWraps); - var i: usize = 0; - while (i < self.lineBreaks.items.len - 1) : (i += 1) { - x = self.getLineOffset(i); - for (self.glyphs[self.lineBreaks.items[i].index..self.lineBreaks.items[i + 1].index]) |glyph| { - if (glyph.character != '\n') { - const ftGlyph = TextRendering.getGlyph(glyph.index) catch continue; - var fontEffect = glyph.fontEffect; - fontEffect.color = shadowColor(fontEffect.color); - TextRendering.drawGlyph(ftGlyph, x + glyph.x_offset, y - glyph.y_offset, @bitCast(fontEffect)); - } - x += glyph.x_advance; - y -= glyph.y_advance; - } - lineWraps[i] = x - self.getLineOffset(i); - x = 0; - y += 16; - } - - for (self.lines.items) |_line| { - var line: Line = _line; - y = 0; - y += if (line.isUnderline) 15 else 8; - const oldColor = draw.color; - draw.setColor(shadowColor(line.color) | (@as(u32, 0xff000000) & draw.color)); - defer draw.setColor(oldColor); - for (lineWraps, 0..) |lineWrap, j| { - const lineStart = @max(0, line.start); - const lineEnd = @min(lineWrap, line.end); - if (lineStart < lineEnd) { - const start = Vec2f{lineStart + self.getLineOffset(j), y}; - const dim = Vec2f{lineEnd - lineStart, 1}; - draw.rect(start, dim); - } - line.start -= lineWrap; - line.end -= lineWrap; - y += 16; - } - } - } + pub const Alignment = enum { + left, + center, + right, + }; + + pub const FontEffect = packed struct(u28) { + color: u24 = 0xffffff, + bold: bool = false, + italic: bool = false, + underline: bool = false, + strikethrough: bool = false, + + fn hasLine(self: FontEffect, comptime isUnderline: bool) bool { + if (isUnderline) return self.underline; + return self.strikethrough; + } + }; + + const Line = struct { + start: f32, + end: f32, + color: u24, + isUnderline: bool, + }; + + const LineBreak = struct { + index: u32, + width: f32, + }; + + const GlyphData = struct { + x_advance: f32, + y_advance: f32, + x_offset: f32, + y_offset: f32, + character: u21, + index: u32, + cluster: u32, + fontEffect: FontEffect, + characterIndex: u32, + }; + + alignment: Alignment, + width: f32, + buffer: ?*c.hb_buffer_t, + glyphs: []GlyphData, + lines: main.List(Line), + lineBreaks: main.List(LineBreak), + + fn addLine(self: *TextBuffer, line: Line) void { + if (line.start != line.end) { + self.lines.append(line); + } + } + + fn initLines(self: *TextBuffer, comptime isUnderline: bool) void { + var line: Line = Line{ .start = 0, .end = 0, .color = 0, .isUnderline = isUnderline }; + var lastFontEffect: FontEffect = .{}; + for (self.glyphs) |glyph| { + const fontEffect = glyph.fontEffect; + if (lastFontEffect.hasLine(isUnderline)) { + if (fontEffect.color != lastFontEffect.color) { + self.addLine(line); + line.color = fontEffect.color; + line.start = line.end; + } else if (!fontEffect.hasLine(isUnderline)) { + self.addLine(line); + } + } else if (fontEffect.hasLine(isUnderline)) { + line.start = line.end; + line.color = fontEffect.color; + } + lastFontEffect = fontEffect; + line.end += glyph.x_advance; + } + if (lastFontEffect.hasLine(isUnderline)) { + self.addLine(line); + } + } + + pub const Parser = struct { + unicodeIterator: std.unicode.Utf8Iterator, + currentFontEffect: FontEffect, + parsedText: main.List(u32), + fontEffects: main.List(FontEffect), + characterIndex: main.List(u32), + showControlCharacters: bool, + curChar: u21 = undefined, + curIndex: u32 = 0, + + fn appendControlGetNext(self: *Parser) ?void { + if (self.showControlCharacters) { + self.fontEffects.append(.{ .color = 0x808080 }); + self.parsedText.append(self.curChar); + self.characterIndex.append(self.curIndex); + } + self.curIndex = @intCast(self.unicodeIterator.i); + self.curChar = self.unicodeIterator.nextCodepoint() orelse return null; + } + + fn appendGetNext(self: *Parser) ?void { + self.fontEffects.append(self.currentFontEffect); + self.parsedText.append(self.curChar); + self.characterIndex.append(self.curIndex); + self.curIndex = @intCast(self.unicodeIterator.i); + self.curChar = self.unicodeIterator.nextCodepoint() orelse return null; + } + + fn peekNextByte(self: *Parser) u8 { + const next = self.unicodeIterator.peek(1); + if (next.len == 0) return 0; + return next[0]; + } + + fn parse(self: *Parser) void { + self.curIndex = @intCast(self.unicodeIterator.i); + self.curChar = self.unicodeIterator.nextCodepoint() orelse return; + while (true) switch (self.curChar) { + '*' => { + self.appendControlGetNext() orelse return; + if (self.curChar == '*') { + self.appendControlGetNext() orelse return; + self.currentFontEffect.bold = !self.currentFontEffect.bold; + } else { + self.currentFontEffect.italic = !self.currentFontEffect.italic; + } + }, + '_' => { + if (self.peekNextByte() == '_') { + self.appendControlGetNext() orelse return; + self.appendControlGetNext() orelse return; + self.currentFontEffect.underline = !self.currentFontEffect.underline; + } else { + self.appendGetNext() orelse return; + } + }, + '~' => { + if (self.peekNextByte() == '~') { + self.appendControlGetNext() orelse return; + self.appendControlGetNext() orelse return; + self.currentFontEffect.strikethrough = !self.currentFontEffect.strikethrough; + } else { + self.appendGetNext() orelse return; + } + }, + '\\' => { + self.appendControlGetNext() orelse return; + self.appendGetNext() orelse return; + }, + '#' => { + self.appendControlGetNext() orelse return; + var shift: u5 = 20; + while (true) : (shift -= 4) { + self.currentFontEffect.color = (self.currentFontEffect.color & ~(@as(u24, 0xf) << shift)) | @as(u24, switch (self.curChar) { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => self.curChar - '0', + 'a', 'b', 'c', 'd', 'e', 'f' => self.curChar - 'a' + 10, + 'A', 'B', 'C', 'D', 'E', 'F' => self.curChar - 'A' + 10, + else => 0, + }) << shift; + self.appendControlGetNext() orelse return; + if (shift == 0) break; + } + }, + '§' => { + self.currentFontEffect = .{ .color = self.currentFontEffect.color }; + self.appendControlGetNext() orelse return; + }, + else => { + self.appendGetNext() orelse return; + }, + }; + } + + pub fn countVisibleCharacters(text: []const u8) usize { + var unicodeIterator = std.unicode.Utf8Iterator{ .bytes = text, .i = 0 }; + var count: usize = 0; + var curChar = unicodeIterator.nextCodepoint() orelse return count; + outer: while (true) switch (curChar) { + '*' => { + curChar = unicodeIterator.nextCodepoint() orelse break; + }, + '_' => { + curChar = unicodeIterator.nextCodepoint() orelse break; + if (curChar == '_') { + curChar = unicodeIterator.nextCodepoint() orelse break; + } else { + count += 1; + } + }, + '~' => { + curChar = unicodeIterator.nextCodepoint() orelse break; + if (curChar == '~') { + curChar = unicodeIterator.nextCodepoint() orelse break; + } else { + count += 1; + } + }, + '\\' => { + curChar = unicodeIterator.nextCodepoint() orelse break; + curChar = unicodeIterator.nextCodepoint() orelse break; + count += 1; + }, + '#' => { + for (0..7) |_| curChar = unicodeIterator.nextCodepoint() orelse break :outer; + }, + '§' => { + curChar = unicodeIterator.nextCodepoint() orelse break; + }, + else => { + count += 1; + curChar = unicodeIterator.nextCodepoint() orelse break; + }, + }; + return count; + } + }; + + pub fn init(allocator: NeverFailingAllocator, text: []const u8, initialFontEffect: FontEffect, showControlCharacters: bool, alignment: Alignment) TextBuffer { + var self: TextBuffer = .{ + .alignment = alignment, + .width = 1e9, + .buffer = null, + .glyphs = &.{}, + .lines = .init(allocator), + .lineBreaks = .init(allocator), + }; + // Parse the input text: + var parser = Parser{ + .unicodeIterator = std.unicode.Utf8Iterator{ .bytes = text, .i = 0 }, + .currentFontEffect = initialFontEffect, + .parsedText = .init(main.stackAllocator), + .fontEffects = .init(allocator), + .characterIndex = .init(allocator), + .showControlCharacters = showControlCharacters, + }; + defer parser.fontEffects.deinit(); + defer parser.parsedText.deinit(); + defer parser.characterIndex.deinit(); + parser.parse(); + if (parser.parsedText.items.len == 0) { + self.lineBreaks.append(.{ .index = 0, .width = 0 }); + return self; + } + + // Let harfbuzz do its thing: + const buffer = c.hb_buffer_create() orelse @panic("Out of Memory while creating harfbuzz buffer"); + defer c.hb_buffer_destroy(buffer); + c.hb_buffer_add_utf32(buffer, parser.parsedText.items.ptr, @intCast(parser.parsedText.items.len), 0, @intCast(parser.parsedText.items.len)); + c.hb_buffer_set_direction(buffer, c.HB_DIRECTION_LTR); + c.hb_buffer_set_script(buffer, c.HB_SCRIPT_COMMON); + c.hb_buffer_set_language(buffer, c.hb_language_get_default()); + c.hb_shape(TextRendering.harfbuzzFont, buffer, null, 0); + var glyphInfos: []c.hb_glyph_info_t = undefined; + var glyphPositions: []c.hb_glyph_position_t = undefined; + { + var len: c_uint = 0; + glyphInfos.ptr = c.hb_buffer_get_glyph_infos(buffer, &len).?; + glyphPositions.ptr = c.hb_buffer_get_glyph_positions(buffer, &len).?; + glyphInfos.len = len; + glyphPositions.len = len; + } + + // Guess the text index from the given cluster indices. Only works if the number of glyphs and the number of characters in a cluster is the same. + const textIndexGuess = main.stackAllocator.alloc(u32, glyphInfos.len); + defer main.stackAllocator.free(textIndexGuess); + for (textIndexGuess, 0..) |*index, i| { + if (i == 0 or glyphInfos[i - 1].cluster != glyphInfos[i].cluster) { + index.* = glyphInfos[i].cluster; + } else { + index.* = @min(textIndexGuess[i - 1] + 1, @as(u32, @intCast(parser.parsedText.items.len - 1))); + for (glyphInfos[i..]) |glyphInfo| { + if (glyphInfo.cluster != glyphInfos[i].cluster) { + index.* = @min(index.*, glyphInfo.cluster - 1); + break; + } + } + } + } + + // Merge it all together: + self.glyphs = allocator.alloc(GlyphData, glyphInfos.len); + for (self.glyphs, 0..) |*glyph, i| { + glyph.x_advance = @as(f32, @floatFromInt(glyphPositions[i].x_advance)) / TextRendering.fontUnitsPerPixel; + glyph.y_advance = @as(f32, @floatFromInt(glyphPositions[i].y_advance)) / TextRendering.fontUnitsPerPixel; + glyph.x_offset = @as(f32, @floatFromInt(glyphPositions[i].x_offset)) / TextRendering.fontUnitsPerPixel; + glyph.y_offset = @as(f32, @floatFromInt(glyphPositions[i].y_offset)) / TextRendering.fontUnitsPerPixel; + glyph.character = @intCast(parser.parsedText.items[textIndexGuess[i]]); + glyph.index = glyphInfos[i].codepoint; + glyph.cluster = glyphInfos[i].cluster; + glyph.fontEffect = parser.fontEffects.items[textIndexGuess[i]]; + glyph.characterIndex = parser.characterIndex.items[textIndexGuess[i]]; + } + + // Find the lines: + self.initLines(true); + self.initLines(false); + self.lineBreaks.append(.{ .index = 0, .width = 0 }); + self.lineBreaks.append(.{ .index = @intCast(self.glyphs.len), .width = 0 }); + return self; + } + + pub fn deinit(self: TextBuffer) void { + self.lines.allocator.free(self.glyphs); + self.lines.deinit(); + self.lineBreaks.deinit(); + } + + fn getLineOffset(self: TextBuffer, line: usize) f32 { + const factor: f32 = switch (self.alignment) { + .left => 0, + .center => 0.5, + .right => 1, + }; + const diff = self.width - self.lineBreaks.items[line + 1].width; + return diff * factor; + } + + pub fn mousePosToIndex(self: TextBuffer, mousePos: Vec2f, bufferLen: usize) u32 { + var line: usize = @trunc(@max(0, mousePos[1] / 16.0)); + line = @min(line, self.lineBreaks.items.len - 2); + var x: f32 = self.getLineOffset(line); + const start = self.lineBreaks.items[line].index; + const end = self.lineBreaks.items[line + 1].index; + for (self.glyphs[start..end]) |glyph| { + if (mousePos[0] < x + glyph.x_advance / 2) { + return @intCast(glyph.characterIndex); + } + + x += glyph.x_advance; + } + return @intCast(if (end < self.glyphs.len) self.glyphs[end - 1].characterIndex else bufferLen); + } + + pub fn indexToCursorPos(self: TextBuffer, index: u32) Vec2f { + var x: f32 = 0; + var y: f32 = 0; + var i: usize = 0; + while (true) { + x = self.getLineOffset(i); + for (self.glyphs[self.lineBreaks.items[i].index..self.lineBreaks.items[i + 1].index]) |glyph| { + if (glyph.characterIndex == index) { + return .{ x, y }; + } + + x += glyph.x_advance; + y -= glyph.y_advance; + } + i += 1; + if (i >= self.lineBreaks.items.len - 1) { + return .{ x, y }; + } + y += 16; + } + } + + /// Returns the calculated dimensions of the text block. + pub fn calculateLineBreaks(self: *TextBuffer, fontSize: f32, maxLineWidth: f32) Vec2f { + self.lineBreaks.clearRetainingCapacity(); + const spaceCharacterWidth = 8; + self.lineBreaks.append(.{ .index = 0, .width = 0 }); + const scaledMaxWidth = maxLineWidth / fontSize * 16.0; + var lineWidth: f32 = 0; + var lastSpaceWidth: f32 = 0; + var lastSpaceIndex: u32 = 0; + for (self.glyphs, 0..) |glyph, i| { + lineWidth += glyph.x_advance; + if (glyph.character == ' ') { + lastSpaceWidth = lineWidth; + lastSpaceIndex = @intCast(i + 1); + } + if (glyph.character == '\n') { + self.lineBreaks.append(.{ .index = @intCast(i + 1), .width = lineWidth - spaceCharacterWidth }); + lineWidth = 0; + lastSpaceIndex = 0; + lastSpaceWidth = 0; + } + if (lineWidth > scaledMaxWidth) { + if (lastSpaceIndex != 0) { + lineWidth -= lastSpaceWidth; + self.lineBreaks.append(.{ .index = lastSpaceIndex, .width = lastSpaceWidth - spaceCharacterWidth }); + lastSpaceIndex = 0; + lastSpaceWidth = 0; + } else { + self.lineBreaks.append(.{ .index = @intCast(i), .width = lineWidth - glyph.x_advance }); + lineWidth = glyph.x_advance; + lastSpaceIndex = 0; + lastSpaceWidth = 0; + } + } + } + self.width = maxLineWidth; + self.lineBreaks.append(.{ .index = @intCast(self.glyphs.len), .width = lineWidth }); + return Vec2f{ maxLineWidth * fontSize / 16.0, @as(f32, @floatFromInt(self.lineBreaks.items.len - 1)) * fontSize }; + } + + pub fn drawSelection(self: TextBuffer, pos: Vec2f, selectionStart: u32, selectionEnd: u32) void { + std.debug.assert(selectionStart <= selectionEnd); + var x: f32 = self.getLineOffset(0); + var y: f32 = 0; + var i: usize = 0; + var j: usize = 0; + // Find the start row: + outer: while (i < self.lineBreaks.items.len - 1) : (i += 1) { + x = self.getLineOffset(i); + while (j < self.lineBreaks.items[i + 1].index) : (j += 1) { + const glyph = self.glyphs[j]; + if (glyph.characterIndex >= selectionStart) break :outer; + x += glyph.x_advance; + y -= glyph.y_advance; + } + y += 16; + } + while (i < self.lineBreaks.items.len - 1) { + const startX = x; + while (j < self.lineBreaks.items[i + 1].index and j < selectionEnd) : (j += 1) { + const glyph = self.glyphs[j]; + if (glyph.characterIndex >= selectionEnd) break; + x += glyph.x_advance; + y -= glyph.y_advance; + } + draw.rect(pos + Vec2f{ startX, y }, .{ x - startX, 16 }); + i += 1; + if (i >= self.lineBreaks.items.len - 1) break; + x = self.getLineOffset(i); + y += 16; + } + } + + pub fn render(self: TextBuffer, _x: f32, _y: f32, _fontSize: f32) void { + self.renderShadow(_x, _y, _fontSize); + self.renderTextWithoutShadow(_x, _y, _fontSize); + } + + pub fn renderTextWithoutShadow(self: TextBuffer, _x: f32, _y: f32, _fontSize: f32) void { + const oldTranslation = draw.setTranslation(.{ _x, _y }); + defer draw.restoreTranslation(oldTranslation); + const oldScale = draw.setScale(_fontSize / 16.0); + defer draw.restoreScale(oldScale); + var x: f32 = 0; + var y: f32 = 0; + TextRendering.pipeline.bind(draw.getScissor()); + var viewport: [4]c_int = undefined; + c.glGetIntegerv(c.GL_VIEWPORT, &viewport); + c.glUniform2f(TextRendering.uniforms.scene, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); + c.glUniform1f(TextRendering.uniforms.ratio, draw.scale); + c.glUniform1f(TextRendering.uniforms.alpha, @as(f32, @floatFromInt(draw.color >> 24)) / 255.0); + c.glActiveTexture(c.GL_TEXTURE0); + c.glBindTexture(c.GL_TEXTURE_2D, TextRendering.glyphTexture[0]); + draw.rectVao.bind(); + const lineWraps: []f32 = main.stackAllocator.alloc(f32, self.lineBreaks.items.len - 1); + defer main.stackAllocator.free(lineWraps); + var i: usize = 0; + while (i < self.lineBreaks.items.len - 1) : (i += 1) { + x = self.getLineOffset(i); + for (self.glyphs[self.lineBreaks.items[i].index..self.lineBreaks.items[i + 1].index]) |glyph| { + if (glyph.character != '\n') { + const ftGlyph = TextRendering.getGlyph(glyph.index) catch continue; + TextRendering.drawGlyph(ftGlyph, x + glyph.x_offset, y - glyph.y_offset, @bitCast(glyph.fontEffect)); + } + x += glyph.x_advance; + y -= glyph.y_advance; + } + lineWraps[i] = x - self.getLineOffset(i); + x = 0; + y += 16; + } + + for (self.lines.items) |_line| { + var line: Line = _line; + y = 0; + y += if (line.isUnderline) 15 else 8; + const oldColor = draw.color; + draw.setColor(line.color | (@as(u32, 0xff000000) & draw.color)); + defer draw.setColor(oldColor); + for (lineWraps, 0..) |lineWrap, j| { + const lineStart = @max(0, line.start); + const lineEnd = @min(lineWrap, line.end); + if (lineStart < lineEnd) { + const start = Vec2f{ lineStart + self.getLineOffset(j), y }; + const dim = Vec2f{ lineEnd - lineStart, 1 }; + draw.rect(start, dim); + } + line.start -= lineWrap; + line.end -= lineWrap; + y += 16; + } + } + } + + fn shadowColor(color: u24) u24 { + const r: f32 = @floatFromInt(color >> 16); + const g: f32 = @floatFromInt(color >> 8 & 255); + const b: f32 = @floatFromInt(color & 255); + const perceivedBrightness = @sqrt(0.299 * r * r + 0.587 * g * g + 0.114 * b * b); + if (perceivedBrightness < 64) { + return 0xffffff; // Make shadows white for better readability. + } else { + return 0; + } + } + + fn renderShadow(self: TextBuffer, _x: f32, _y: f32, _fontSize: f32) void { // Basically a copy of render with some color and position changes. + const oldTranslation = draw.setTranslation(.{ _x + _fontSize / 16.0, _y + _fontSize / 16.0 }); + defer draw.restoreTranslation(oldTranslation); + const oldScale = draw.setScale(_fontSize / 16.0); + defer draw.restoreScale(oldScale); + var x: f32 = 0; + var y: f32 = 0; + TextRendering.pipeline.bind(draw.getScissor()); + var viewport: [4]c_int = undefined; + c.glGetIntegerv(c.GL_VIEWPORT, &viewport); + c.glUniform2f(TextRendering.uniforms.scene, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); + c.glUniform1f(TextRendering.uniforms.ratio, draw.scale); + c.glUniform1f(TextRendering.uniforms.alpha, @as(f32, @floatFromInt(draw.color >> 24)) / 255.0); + c.glActiveTexture(c.GL_TEXTURE0); + c.glBindTexture(c.GL_TEXTURE_2D, TextRendering.glyphTexture[0]); + draw.rectVao.bind(); + const lineWraps: []f32 = main.stackAllocator.alloc(f32, self.lineBreaks.items.len - 1); + defer main.stackAllocator.free(lineWraps); + var i: usize = 0; + while (i < self.lineBreaks.items.len - 1) : (i += 1) { + x = self.getLineOffset(i); + for (self.glyphs[self.lineBreaks.items[i].index..self.lineBreaks.items[i + 1].index]) |glyph| { + if (glyph.character != '\n') { + const ftGlyph = TextRendering.getGlyph(glyph.index) catch continue; + var fontEffect = glyph.fontEffect; + fontEffect.color = shadowColor(fontEffect.color); + TextRendering.drawGlyph(ftGlyph, x + glyph.x_offset, y - glyph.y_offset, @bitCast(fontEffect)); + } + x += glyph.x_advance; + y -= glyph.y_advance; + } + lineWraps[i] = x - self.getLineOffset(i); + x = 0; + y += 16; + } + + for (self.lines.items) |_line| { + var line: Line = _line; + y = 0; + y += if (line.isUnderline) 15 else 8; + const oldColor = draw.color; + draw.setColor(shadowColor(line.color) | (@as(u32, 0xff000000) & draw.color)); + defer draw.setColor(oldColor); + for (lineWraps, 0..) |lineWrap, j| { + const lineStart = @max(0, line.start); + const lineEnd = @min(lineWrap, line.end); + if (lineStart < lineEnd) { + const start = Vec2f{ lineStart + self.getLineOffset(j), y }; + const dim = Vec2f{ lineEnd - lineStart, 1 }; + draw.rect(start, dim); + } + line.start -= lineWrap; + line.end -= lineWrap; + y += 16; + } + } + } }; const TextRendering = struct { // MARK: TextRendering - const Glyph = struct { - textureX: i32, - size: Vec2i, - bearing: Vec2i, - advance: f32, - }; - var pipeline: Pipeline = undefined; - var uniforms: struct { - textureBounds: c_int, - textureRect: c_int, - scene: c_int, - offset: c_int, - ratio: c_int, - fontEffects: c_int, - fontSize: c_int, - alpha: c_int, - } = undefined; - - var freetypeLib: c.FT_Library = undefined; - var freetypeFace: c.FT_Face = undefined; - var harfbuzzFace: ?*c.hb_face_t = undefined; - var harfbuzzFont: ?*c.hb_font_t = undefined; - var glyphMapping: main.List(u31) = undefined; - var glyphData: main.List(Glyph) = undefined; - var glyphTexture: [2]c_uint = undefined; - var textureWidth: i32 = 1024; - const textureHeight: i32 = 16; - var textureOffset: i32 = 0; - var fontUnitsPerPixel: f32 = undefined; - - fn ftError(errorCode: c.FT_Error) !void { - if (errorCode == 0) return; - const errorString = c.FT_Error_String(errorCode); - std.log.err("Got freetype error {s}", .{errorString}); - return error.freetype; - } - - fn init() !void { - pipeline = Pipeline.init( - "assets/cubyz/shaders/graphics/Text.vert", - "assets/cubyz/shaders/graphics/Text.frag", - "", - &uniforms, - draw.SimpleVertex2D, - &.{}, - .{.cullMode = .none}, - .{.depthTest = false, .depthWrite = false}, - .{.attachments = &.{.alphaBlending}}, - ); - pipeline.bind(null); - errdefer pipeline.deinit(); - c.glUniform1f(uniforms.alpha, 1.0); - c.glUniform2f(uniforms.fontSize, @floatFromInt(textureWidth), @floatFromInt(textureHeight)); - try ftError(c.FT_Init_FreeType(&freetypeLib)); - try ftError(c.FT_New_Face(freetypeLib, "assets/cubyz/fonts/unscii-16-full.ttf", 0, &freetypeFace)); - try ftError(c.FT_Set_Pixel_Sizes(freetypeFace, 0, textureHeight)); - harfbuzzFace = c.hb_ft_face_create_referenced(freetypeFace); - harfbuzzFont = c.hb_font_create(harfbuzzFace); - fontUnitsPerPixel = @as(f32, @floatFromInt(freetypeFace.*.units_per_EM))/@as(f32, @floatFromInt(textureHeight)); - - glyphMapping = .init(main.globalAllocator); - glyphData = .init(main.globalAllocator); - glyphData.append(undefined); // 0 is a reserved value. - c.glGenTextures(2, &glyphTexture); - c.glBindTexture(c.GL_TEXTURE_2D, glyphTexture[0]); - c.glTexImage2D(c.GL_TEXTURE_2D, 0, c.GL_R8, textureWidth, textureHeight, 0, c.GL_RED, c.GL_UNSIGNED_BYTE, null); - 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.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT); - c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT); - c.glBindTexture(c.GL_TEXTURE_2D, glyphTexture[1]); - 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.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT); - c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT); - } - - fn deinit() void { - pipeline.deinit(); - ftError(c.FT_Done_FreeType(freetypeLib)) catch {}; - glyphMapping.deinit(); - glyphData.deinit(); - c.glDeleteTextures(2, &glyphTexture); - c.hb_font_destroy(harfbuzzFont); - } - - fn resizeTexture(newWidth: i32) void { - textureWidth = newWidth; - const swap = glyphTexture[1]; - glyphTexture[1] = glyphTexture[0]; - glyphTexture[0] = swap; - c.glActiveTexture(c.GL_TEXTURE0); - c.glBindTexture(c.GL_TEXTURE_2D, glyphTexture[0]); - c.glTexImage2D(c.GL_TEXTURE_2D, 0, c.GL_R8, newWidth, textureHeight, 0, c.GL_RED, c.GL_UNSIGNED_BYTE, null); - c.glCopyImageSubData(glyphTexture[1], c.GL_TEXTURE_2D, 0, 0, 0, 0, glyphTexture[0], c.GL_TEXTURE_2D, 0, 0, 0, 0, textureOffset, textureHeight, 1); - pipeline.bind(draw.getScissor()); - c.glUniform2f(uniforms.fontSize, @floatFromInt(textureWidth), @floatFromInt(textureHeight)); - } - - fn uploadData(bitmap: c.FT_Bitmap) void { - const width: i32 = @bitCast(bitmap.width); - const height: i32 = @bitCast(bitmap.rows); - const buffer = bitmap.buffer orelse return; - if (textureOffset + width > textureWidth) { - resizeTexture(textureWidth*2); - } - c.glPixelStorei(c.GL_UNPACK_ALIGNMENT, 1); - c.glTexSubImage2D(c.GL_TEXTURE_2D, 0, textureOffset, 0, width, height, c.GL_RED, c.GL_UNSIGNED_BYTE, buffer); - textureOffset += width; - } - - fn getGlyph(index: u32) !Glyph { - if (index >= glyphMapping.items.len) { - glyphMapping.appendNTimes(0, index - glyphMapping.items.len + 1); - } - if (glyphMapping.items[index] == 0) { // glyph was not initialized yet. - try ftError(c.FT_Load_Glyph(freetypeFace, index, c.FT_LOAD_RENDER)); - const glyph = freetypeFace.*.glyph; - const bitmap = glyph.*.bitmap; - const width = bitmap.width; - const height = bitmap.rows; - glyphMapping.items[index] = @intCast(glyphData.items.len); - glyphData.addOne().* = Glyph{ - .textureX = textureOffset, - .size = Vec2i{@intCast(width), @intCast(height)}, - .bearing = Vec2i{glyph.*.bitmap_left, 16 - glyph.*.bitmap_top}, - .advance = @as(f32, @floatFromInt(glyph.*.advance.x))/@as(f32, 1 << 6), - }; - uploadData(bitmap); - } - return glyphData.items[glyphMapping.items[index]]; - } - - fn drawGlyph(glyph: Glyph, _x: f32, _y: f32, fontEffects: u28) void { - var x = _x; - var y = _y; - x *= draw.scale; - y *= draw.scale; - x += draw.translation[0]; - y += draw.translation[1]; - x = @floor(x); - y = @ceil(y); - c.glUniform1i(uniforms.fontEffects, fontEffects); - c.glUniform4f(uniforms.textureBounds, @floatFromInt(glyph.textureX), 0, @floatFromInt(glyph.size[0]), @floatFromInt(glyph.size[1])); - if (fontEffects & 0x1000000 != 0) { // bold - c.glUniform2f(uniforms.offset, @as(f32, @floatFromInt(glyph.bearing[0]))*draw.scale + x - 1, @as(f32, @floatFromInt(glyph.bearing[1]))*draw.scale + y - 1); - c.glUniform4f(uniforms.textureRect, @floatFromInt(glyph.textureX - 1), -1, @floatFromInt(glyph.size[0] + 2), @floatFromInt(glyph.size[1] + 2)); - } else { - c.glUniform2f(uniforms.offset, @as(f32, @floatFromInt(glyph.bearing[0]))*draw.scale + x, @as(f32, @floatFromInt(glyph.bearing[1]))*draw.scale + y); - c.glUniform4f(uniforms.textureRect, @floatFromInt(glyph.textureX), 0, @floatFromInt(glyph.size[0]), @floatFromInt(glyph.size[1])); - } - c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); - } - - fn renderText(text: []const u8, x: f32, y: f32, fontSize: f32, initialFontEffect: TextBuffer.FontEffect, alignment: TextBuffer.Alignment) void { - const buf = TextBuffer.init(main.stackAllocator, text, initialFontEffect, false, alignment); - defer buf.deinit(); - - buf.render(x, y, fontSize); - } + const Glyph = struct { + textureX: i32, + size: Vec2i, + bearing: Vec2i, + advance: f32, + }; + var pipeline: Pipeline = undefined; + var uniforms: struct { + textureBounds: c_int, + textureRect: c_int, + scene: c_int, + offset: c_int, + ratio: c_int, + fontEffects: c_int, + fontSize: c_int, + alpha: c_int, + } = undefined; + + var freetypeLib: c.FT_Library = undefined; + var freetypeFace: c.FT_Face = undefined; + var harfbuzzFace: ?*c.hb_face_t = undefined; + var harfbuzzFont: ?*c.hb_font_t = undefined; + var glyphMapping: main.List(u31) = undefined; + var glyphData: main.List(Glyph) = undefined; + var glyphTexture: [2]c_uint = undefined; + var textureWidth: i32 = 1024; + const textureHeight: i32 = 16; + var textureOffset: i32 = 0; + var fontUnitsPerPixel: f32 = undefined; + + fn ftError(errorCode: c.FT_Error) !void { + if (errorCode == 0) return; + const errorString = c.FT_Error_String(errorCode); + std.log.err("Got freetype error {s}", .{errorString}); + return error.freetype; + } + + fn init() !void { + pipeline = Pipeline.init( + "assets/cubyz/shaders/graphics/Text.vert", + "assets/cubyz/shaders/graphics/Text.frag", + "", + &uniforms, + draw.SimpleVertex2D, + &.{}, + .{ .cullMode = .none }, + .{ .depthTest = false, .depthWrite = false }, + .{ .attachments = &.{.alphaBlending} }, + ); + pipeline.bind(null); + errdefer pipeline.deinit(); + c.glUniform1f(uniforms.alpha, 1.0); + c.glUniform2f(uniforms.fontSize, @floatFromInt(textureWidth), @floatFromInt(textureHeight)); + try ftError(c.FT_Init_FreeType(&freetypeLib)); + try ftError(c.FT_New_Face(freetypeLib, "assets/cubyz/fonts/unscii-16-full.ttf", 0, &freetypeFace)); + try ftError(c.FT_Set_Pixel_Sizes(freetypeFace, 0, textureHeight)); + harfbuzzFace = c.hb_ft_face_create_referenced(freetypeFace); + harfbuzzFont = c.hb_font_create(harfbuzzFace); + fontUnitsPerPixel = @as(f32, @floatFromInt(freetypeFace.*.units_per_EM)) / @as(f32, @floatFromInt(textureHeight)); + + glyphMapping = .init(main.globalAllocator); + glyphData = .init(main.globalAllocator); + glyphData.append(undefined); // 0 is a reserved value. + c.glGenTextures(2, &glyphTexture); + c.glBindTexture(c.GL_TEXTURE_2D, glyphTexture[0]); + c.glTexImage2D(c.GL_TEXTURE_2D, 0, c.GL_R8, textureWidth, textureHeight, 0, c.GL_RED, c.GL_UNSIGNED_BYTE, null); + 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.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT); + c.glBindTexture(c.GL_TEXTURE_2D, glyphTexture[1]); + 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.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT); + } + + fn deinit() void { + pipeline.deinit(); + ftError(c.FT_Done_FreeType(freetypeLib)) catch {}; + glyphMapping.deinit(); + glyphData.deinit(); + c.glDeleteTextures(2, &glyphTexture); + c.hb_font_destroy(harfbuzzFont); + } + + fn resizeTexture(newWidth: i32) void { + textureWidth = newWidth; + const swap = glyphTexture[1]; + glyphTexture[1] = glyphTexture[0]; + glyphTexture[0] = swap; + c.glActiveTexture(c.GL_TEXTURE0); + c.glBindTexture(c.GL_TEXTURE_2D, glyphTexture[0]); + c.glTexImage2D(c.GL_TEXTURE_2D, 0, c.GL_R8, newWidth, textureHeight, 0, c.GL_RED, c.GL_UNSIGNED_BYTE, null); + c.glCopyImageSubData(glyphTexture[1], c.GL_TEXTURE_2D, 0, 0, 0, 0, glyphTexture[0], c.GL_TEXTURE_2D, 0, 0, 0, 0, textureOffset, textureHeight, 1); + pipeline.bind(draw.getScissor()); + c.glUniform2f(uniforms.fontSize, @floatFromInt(textureWidth), @floatFromInt(textureHeight)); + } + + fn uploadData(bitmap: c.FT_Bitmap) void { + const width: i32 = @bitCast(bitmap.width); + const height: i32 = @bitCast(bitmap.rows); + const buffer = bitmap.buffer orelse return; + if (textureOffset + width > textureWidth) { + resizeTexture(textureWidth * 2); + } + c.glPixelStorei(c.GL_UNPACK_ALIGNMENT, 1); + c.glTexSubImage2D(c.GL_TEXTURE_2D, 0, textureOffset, 0, width, height, c.GL_RED, c.GL_UNSIGNED_BYTE, buffer); + textureOffset += width; + } + + fn getGlyph(index: u32) !Glyph { + if (index >= glyphMapping.items.len) { + glyphMapping.appendNTimes(0, index - glyphMapping.items.len + 1); + } + if (glyphMapping.items[index] == 0) { // glyph was not initialized yet. + try ftError(c.FT_Load_Glyph(freetypeFace, index, c.FT_LOAD_RENDER)); + const glyph = freetypeFace.*.glyph; + const bitmap = glyph.*.bitmap; + const width = bitmap.width; + const height = bitmap.rows; + glyphMapping.items[index] = @intCast(glyphData.items.len); + glyphData.addOne().* = Glyph{ + .textureX = textureOffset, + .size = Vec2i{ @intCast(width), @intCast(height) }, + .bearing = Vec2i{ glyph.*.bitmap_left, 16 - glyph.*.bitmap_top }, + .advance = @as(f32, @floatFromInt(glyph.*.advance.x)) / @as(f32, 1 << 6), + }; + uploadData(bitmap); + } + return glyphData.items[glyphMapping.items[index]]; + } + + fn drawGlyph(glyph: Glyph, _x: f32, _y: f32, fontEffects: u28) void { + var x = _x; + var y = _y; + x *= draw.scale; + y *= draw.scale; + x += draw.translation[0]; + y += draw.translation[1]; + x = @floor(x); + y = @ceil(y); + c.glUniform1i(uniforms.fontEffects, fontEffects); + c.glUniform4f(uniforms.textureBounds, @floatFromInt(glyph.textureX), 0, @floatFromInt(glyph.size[0]), @floatFromInt(glyph.size[1])); + if (fontEffects & 0x1000000 != 0) { // bold + c.glUniform2f(uniforms.offset, @as(f32, @floatFromInt(glyph.bearing[0])) * draw.scale + x - 1, @as(f32, @floatFromInt(glyph.bearing[1])) * draw.scale + y - 1); + c.glUniform4f(uniforms.textureRect, @floatFromInt(glyph.textureX - 1), -1, @floatFromInt(glyph.size[0] + 2), @floatFromInt(glyph.size[1] + 2)); + } else { + c.glUniform2f(uniforms.offset, @as(f32, @floatFromInt(glyph.bearing[0])) * draw.scale + x, @as(f32, @floatFromInt(glyph.bearing[1])) * draw.scale + y); + c.glUniform4f(uniforms.textureRect, @floatFromInt(glyph.textureX), 0, @floatFromInt(glyph.size[0]), @floatFromInt(glyph.size[1])); + } + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + } + + fn renderText(text: []const u8, x: f32, y: f32, fontSize: f32, initialFontEffect: TextBuffer.FontEffect, alignment: TextBuffer.Alignment) void { + const buf = TextBuffer.init(main.stackAllocator, text, initialFontEffect, false, alignment); + defer buf.deinit(); + + buf.render(x, y, fontSize); + } }; pub fn init() void { // MARK: init() - draw.initCircle(); - draw.initImage(); - draw.initLine(); - draw.initRect(); - draw.initRectBorder(); - TextRendering.init() catch |err| { - std.log.err("Error while initializing TextRendering: {s}", .{@errorName(err)}); - }; - block_texture.init(); - pipelines.init(); - RenderPass.initRenderPasses() catch @panic("Failed to create render passes"); + draw.initCircle(); + draw.initImage(); + draw.initLine(); + draw.initRect(); + draw.initRectBorder(); + TextRendering.init() catch |err| { + std.log.err("Error while initializing TextRendering: {s}", .{@errorName(err)}); + }; + block_texture.init(); + pipelines.init(); + RenderPass.initRenderPasses() catch @panic("Failed to create render passes"); } pub fn deinit() void { - RenderPass.deinitRenderPasses(); - draw.deinitCircle(); - draw.deinitImage(); - draw.deinitLine(); - draw.deinitRect(); - draw.deinitRectBorder(); - TextRendering.deinit(); - block_texture.deinit(); - pipelines.deinit(); + RenderPass.deinitRenderPasses(); + draw.deinitCircle(); + draw.deinitImage(); + draw.deinitLine(); + draw.deinitRect(); + draw.deinitRectBorder(); + TextRendering.deinit(); + block_texture.deinit(); + pipelines.deinit(); } pub const RenderPass = struct { // MARK: RenderPass - renderPass: c.VkRenderPass, - - pub var renderToWindow: RenderPass = undefined; - - fn initRenderPasses() !void { - if (main.settings.launchConfig.vulkanTestingMode) { - renderToWindow = try RenderPass.init(); - } - } - - fn deinitRenderPasses() void { - if (main.settings.launchConfig.vulkanTestingMode) { - renderToWindow.deinit(); - } - } - - pub fn init() !RenderPass { - const colorAttachment = c.VkAttachmentDescription{ - .format = vulkan.SwapChain.imageFormat, // TODO: This needs to be configurable to be able to render to f16 framebuffer - .samples = c.VK_SAMPLE_COUNT_1_BIT, - .loadOp = c.VK_ATTACHMENT_LOAD_OP_CLEAR, - .storeOp = c.VK_ATTACHMENT_STORE_OP_STORE, - .stencilLoadOp = c.VK_ATTACHMENT_LOAD_OP_DONT_CARE, - .stencilStoreOp = c.VK_ATTACHMENT_STORE_OP_DONT_CARE, - .initialLayout = c.VK_IMAGE_LAYOUT_UNDEFINED, - .finalLayout = c.VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, - }; - const colorAttachmentRef = c.VkAttachmentReference{ - .attachment = 0, - .layout = c.VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, - }; - const subpass = c.VkSubpassDescription{ - .pipelineBindPoint = c.VK_PIPELINE_BIND_POINT_GRAPHICS, - .colorAttachmentCount = 1, - .pColorAttachments = &colorAttachmentRef, - }; - const dependency = c.VkSubpassDependency{ - .srcSubpass = c.VK_SUBPASS_EXTERNAL, - .dstSubpass = 0, - .srcStageMask = c.VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, - .srcAccessMask = 0, - .dstStageMask = c.VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, - .dstAccessMask = c.VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, - }; - const renderPassInfo = c.VkRenderPassCreateInfo{ - .sType = c.VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO, - .attachmentCount = 1, - .pAttachments = &colorAttachment, - .subpassCount = 1, - .pSubpasses = &subpass, - .dependencyCount = 1, - .pDependencies = &dependency, - }; - - var self: RenderPass = undefined; - try vulkan.checkResultErr(c.vkCreateRenderPass(vulkan.device, &renderPassInfo, null, &self.renderPass)); - return self; - } - - pub fn deinit(self: RenderPass) void { - c.vkDestroyRenderPass(vulkan.device, self.renderPass, null); - } + renderPass: c.VkRenderPass, + + pub var renderToWindow: RenderPass = undefined; + + fn initRenderPasses() !void { + if (main.settings.launchConfig.vulkanTestingMode) { + renderToWindow = try RenderPass.init(); + } + } + + fn deinitRenderPasses() void { + if (main.settings.launchConfig.vulkanTestingMode) { + renderToWindow.deinit(); + } + } + + pub fn init() !RenderPass { + const colorAttachment = c.VkAttachmentDescription{ + .format = vulkan.SwapChain.imageFormat, // TODO: This needs to be configurable to be able to render to f16 framebuffer + .samples = c.VK_SAMPLE_COUNT_1_BIT, + .loadOp = c.VK_ATTACHMENT_LOAD_OP_CLEAR, + .storeOp = c.VK_ATTACHMENT_STORE_OP_STORE, + .stencilLoadOp = c.VK_ATTACHMENT_LOAD_OP_DONT_CARE, + .stencilStoreOp = c.VK_ATTACHMENT_STORE_OP_DONT_CARE, + .initialLayout = c.VK_IMAGE_LAYOUT_UNDEFINED, + .finalLayout = c.VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, + }; + const colorAttachmentRef = c.VkAttachmentReference{ + .attachment = 0, + .layout = c.VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + }; + const subpass = c.VkSubpassDescription{ + .pipelineBindPoint = c.VK_PIPELINE_BIND_POINT_GRAPHICS, + .colorAttachmentCount = 1, + .pColorAttachments = &colorAttachmentRef, + }; + const dependency = c.VkSubpassDependency{ + .srcSubpass = c.VK_SUBPASS_EXTERNAL, + .dstSubpass = 0, + .srcStageMask = c.VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + .srcAccessMask = 0, + .dstStageMask = c.VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + .dstAccessMask = c.VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + }; + const renderPassInfo = c.VkRenderPassCreateInfo{ + .sType = c.VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO, + .attachmentCount = 1, + .pAttachments = &colorAttachment, + .subpassCount = 1, + .pSubpasses = &subpass, + .dependencyCount = 1, + .pDependencies = &dependency, + }; + + var self: RenderPass = undefined; + try vulkan.checkResultErr(c.vkCreateRenderPass(vulkan.device, &renderPassInfo, null, &self.renderPass)); + return self; + } + + pub fn deinit(self: RenderPass) void { + c.vkDestroyRenderPass(vulkan.device, self.renderPass, null); + } }; pub const VertexArray = struct { // MARK: VertexArray - vao: c_uint, - vbo: c_uint, - ibo: ?c_uint, - - pub const EmptyVertex = struct { - pub const attributeDescriptions: []const c.VkVertexInputAttributeDescription = &.{}; - }; - - pub fn init(T: type, data: []const T, indices_: ?[]const u32) VertexArray { - var result: VertexArray = undefined; - c.glGenVertexArrays(1, &result.vao); - c.glBindVertexArray(result.vao); - c.glGenBuffers(1, &result.vbo); - c.glBindBuffer(c.GL_ARRAY_BUFFER, result.vbo); - c.glBufferData(c.GL_ARRAY_BUFFER, @intCast(data.len*@sizeOf(T)), data.ptr, c.GL_STATIC_DRAW); - if (indices_) |indices| { - result.ibo = 0; - c.glGenBuffers(1, &result.ibo.?); - c.glBindBuffer(c.GL_ELEMENT_ARRAY_BUFFER, result.ibo.?); - c.glBufferData(c.GL_ELEMENT_ARRAY_BUFFER, @intCast(indices.len*@sizeOf(u32)), indices.ptr, c.GL_STATIC_DRAW); - } else { - result.ibo = null; - } - - const attributeDescriptions: []const c.VkVertexInputAttributeDescription = T.attributeDescriptions; - inline for (attributeDescriptions) |desc| { - std.debug.assert(desc.binding == 0); - c.glEnableVertexAttribArray(desc.location); - const glType = comptime switch (desc.format) { - c.VK_FORMAT_R32_SFLOAT => c.GL_FLOAT, - c.VK_FORMAT_R32G32_SFLOAT => c.GL_FLOAT, - c.VK_FORMAT_R32G32B32_SFLOAT => c.GL_FLOAT, - c.VK_FORMAT_R32G32B32A32_SFLOAT => c.GL_FLOAT, - else => @compileError("Unrecognized format"), - }; - const size = comptime switch (desc.format) { - c.VK_FORMAT_R32_SFLOAT => 1, - c.VK_FORMAT_R32G32_SFLOAT => 2, - c.VK_FORMAT_R32G32B32_SFLOAT => 3, - c.VK_FORMAT_R32G32B32A32_SFLOAT => 4, - else => @compileError("Unrecognized format"), - }; - c.glVertexAttribPointer(desc.location, size, glType, c.GL_FALSE, @sizeOf(T), @ptrFromInt(desc.offset)); - } - - c.glBindVertexArray(0); - return result; - } - - pub fn deinit(self: VertexArray) void { - c.glDeleteVertexArrays(1, &self.vao); - c.glDeleteBuffers(1, &self.vbo); - if (self.ibo != null) { - c.glDeleteBuffers(1, &self.ibo.?); - } - } - - pub fn bind(self: VertexArray) void { - c.glBindVertexArray(self.vao); - } + vao: c_uint, + vbo: c_uint, + ibo: ?c_uint, + + pub const EmptyVertex = struct { + pub const attributeDescriptions: []const c.VkVertexInputAttributeDescription = &.{}; + }; + + pub fn init(T: type, data: []const T, indices_: ?[]const u32) VertexArray { + var result: VertexArray = undefined; + c.glGenVertexArrays(1, &result.vao); + c.glBindVertexArray(result.vao); + c.glGenBuffers(1, &result.vbo); + c.glBindBuffer(c.GL_ARRAY_BUFFER, result.vbo); + c.glBufferData(c.GL_ARRAY_BUFFER, @intCast(data.len * @sizeOf(T)), data.ptr, c.GL_STATIC_DRAW); + if (indices_) |indices| { + result.ibo = 0; + c.glGenBuffers(1, &result.ibo.?); + c.glBindBuffer(c.GL_ELEMENT_ARRAY_BUFFER, result.ibo.?); + c.glBufferData(c.GL_ELEMENT_ARRAY_BUFFER, @intCast(indices.len * @sizeOf(u32)), indices.ptr, c.GL_STATIC_DRAW); + } else { + result.ibo = null; + } + + const attributeDescriptions: []const c.VkVertexInputAttributeDescription = T.attributeDescriptions; + inline for (attributeDescriptions) |desc| { + std.debug.assert(desc.binding == 0); + c.glEnableVertexAttribArray(desc.location); + const glType = comptime switch (desc.format) { + c.VK_FORMAT_R32_SFLOAT => c.GL_FLOAT, + c.VK_FORMAT_R32G32_SFLOAT => c.GL_FLOAT, + c.VK_FORMAT_R32G32B32_SFLOAT => c.GL_FLOAT, + c.VK_FORMAT_R32G32B32A32_SFLOAT => c.GL_FLOAT, + else => @compileError("Unrecognized format"), + }; + const size = comptime switch (desc.format) { + c.VK_FORMAT_R32_SFLOAT => 1, + c.VK_FORMAT_R32G32_SFLOAT => 2, + c.VK_FORMAT_R32G32B32_SFLOAT => 3, + c.VK_FORMAT_R32G32B32A32_SFLOAT => 4, + else => @compileError("Unrecognized format"), + }; + c.glVertexAttribPointer(desc.location, size, glType, c.GL_FALSE, @sizeOf(T), @ptrFromInt(desc.offset)); + } + + c.glBindVertexArray(0); + return result; + } + + pub fn deinit(self: VertexArray) void { + c.glDeleteVertexArrays(1, &self.vao); + c.glDeleteBuffers(1, &self.vbo); + if (self.ibo != null) { + c.glDeleteBuffers(1, &self.ibo.?); + } + } + + pub fn bind(self: VertexArray) void { + c.glBindVertexArray(self.vao); + } }; pub const SSBO = struct { // MARK: SSBO - bufferID: c_uint, - pub fn init() SSBO { - var self = SSBO{.bufferID = undefined}; - c.glGenBuffers(1, &self.bufferID); - return self; - } - - pub fn initStatic(comptime T: type, data: []const T) SSBO { - var self = SSBO{.bufferID = undefined}; - c.glGenBuffers(1, &self.bufferID); - c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.bufferID); - c.glBufferStorage(c.GL_SHADER_STORAGE_BUFFER, @intCast(data.len*@sizeOf(T)), data.ptr, 0); - return self; - } - - pub fn initStaticSize(comptime T: type, len: usize) SSBO { - var self = SSBO{.bufferID = undefined}; - c.glGenBuffers(1, &self.bufferID); - c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.bufferID); - c.glBufferStorage(c.GL_SHADER_STORAGE_BUFFER, @intCast(len*@sizeOf(T)), null, 0); - return self; - } - - pub fn deinit(self: SSBO) void { - c.glDeleteBuffers(1, &self.bufferID); - } - - pub fn bind(self: SSBO, binding: c_uint) void { - c.glBindBufferBase(c.GL_SHADER_STORAGE_BUFFER, binding, self.bufferID); - } - - pub fn bufferData(self: SSBO, comptime T: type, data: []const T) void { - c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.bufferID); - c.glBufferData(c.GL_SHADER_STORAGE_BUFFER, @intCast(data.len*@sizeOf(T)), data.ptr, c.GL_STATIC_DRAW); - c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, 0); - } - - pub fn bufferSubData(self: SSBO, comptime T: type, data: []const T, length: usize) void { - c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.bufferID); - c.glBufferSubData(c.GL_SHADER_STORAGE_BUFFER, 0, @intCast(length*@sizeOf(T)), data.ptr); - c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, 0); - } - - pub fn createDynamicBuffer(self: SSBO, comptime T: type, size: usize) void { - c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.bufferID); - c.glBufferData(c.GL_SHADER_STORAGE_BUFFER, @intCast(size*@sizeOf(T)), null, c.GL_DYNAMIC_DRAW); - c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, 0); - } + bufferID: c_uint, + pub fn init() SSBO { + var self = SSBO{ .bufferID = undefined }; + c.glGenBuffers(1, &self.bufferID); + return self; + } + + pub fn initStatic(comptime T: type, data: []const T) SSBO { + var self = SSBO{ .bufferID = undefined }; + c.glGenBuffers(1, &self.bufferID); + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.bufferID); + c.glBufferStorage(c.GL_SHADER_STORAGE_BUFFER, @intCast(data.len * @sizeOf(T)), data.ptr, 0); + return self; + } + + pub fn initStaticSize(comptime T: type, len: usize) SSBO { + var self = SSBO{ .bufferID = undefined }; + c.glGenBuffers(1, &self.bufferID); + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.bufferID); + c.glBufferStorage(c.GL_SHADER_STORAGE_BUFFER, @intCast(len * @sizeOf(T)), null, 0); + return self; + } + + pub fn deinit(self: SSBO) void { + c.glDeleteBuffers(1, &self.bufferID); + } + + pub fn bind(self: SSBO, binding: c_uint) void { + c.glBindBufferBase(c.GL_SHADER_STORAGE_BUFFER, binding, self.bufferID); + } + + pub fn bufferData(self: SSBO, comptime T: type, data: []const T) void { + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.bufferID); + c.glBufferData(c.GL_SHADER_STORAGE_BUFFER, @intCast(data.len * @sizeOf(T)), data.ptr, c.GL_STATIC_DRAW); + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, 0); + } + + pub fn bufferSubData(self: SSBO, comptime T: type, data: []const T, length: usize) void { + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.bufferID); + c.glBufferSubData(c.GL_SHADER_STORAGE_BUFFER, 0, @intCast(length * @sizeOf(T)), data.ptr); + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, 0); + } + + pub fn createDynamicBuffer(self: SSBO, comptime T: type, size: usize) void { + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.bufferID); + c.glBufferData(c.GL_SHADER_STORAGE_BUFFER, @intCast(size * @sizeOf(T)), null, c.GL_DYNAMIC_DRAW); + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, 0); + } }; pub const SubAllocation = struct { - start: u31, - len: u31, + start: u31, + len: u31, }; /// A big SSBO that is able to allocate/free smaller regions. pub fn LargeBuffer(comptime Entry: type) type { // MARK: LargerBuffer - return struct { - ssbo: SSBO, - freeBlocks: main.List(SubAllocation), - fences: [3]c.GLsync, - fencedFreeLists: [3]main.List(SubAllocation), - activeFence: u8, - capacity: u31, - used: u31, - binding: c_uint, - - const Self = @This(); - - fn createBuffer(self: *Self, size: u31) void { - self.ssbo = .init(); - c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.ssbo.bufferID); - const flags = c.GL_MAP_WRITE_BIT | c.GL_DYNAMIC_STORAGE_BIT; - const bytes = @as(c.GLsizeiptr, size)*@sizeOf(Entry); - c.glBufferStorage(c.GL_SHADER_STORAGE_BUFFER, bytes, null, flags); - self.ssbo.bind(self.binding); - self.capacity = size; - } - - pub fn init(self: *Self, allocator: NeverFailingAllocator, size: u31, binding: c_uint) void { - self.used = 0; - self.binding = binding; - self.createBuffer(size); - self.activeFence = 0; - for (&self.fences) |*fence| { - fence.* = c.glFenceSync(c.GL_SYNC_GPU_COMMANDS_COMPLETE, 0); - } - for (&self.fencedFreeLists) |*list| { - list.* = .init(allocator); - } - - self.freeBlocks = .init(allocator); - self.freeBlocks.append(.{.start = 0, .len = size}); - } - - pub fn deinit(self: *Self) void { - for (self.fences) |fence| { - c.glDeleteSync(fence); - } - for (self.fencedFreeLists) |list| { - list.deinit(); - } - self.ssbo.deinit(); - self.freeBlocks.deinit(); - } - - pub fn beginRender(self: *Self) void { - self.activeFence += 1; - if (self.activeFence == self.fences.len) self.activeFence = 0; - const endTime = main.timestamp().addDuration(.fromMilliseconds(5)); - while (self.fencedFreeLists[self.activeFence].popOrNull()) |allocation| { - self.finalFree(allocation); - if (main.timestamp().durationTo(endTime).nanoseconds < 0) break; // TODO: Remove after #1434 - } - _ = c.glClientWaitSync(self.fences[self.activeFence], 0, c.GL_TIMEOUT_IGNORED); // Make sure the render calls that accessed these parts of the buffer have finished. - } - - pub fn endRender(self: *Self) void { - c.glDeleteSync(self.fences[self.activeFence]); - self.fences[self.activeFence] = c.glFenceSync(c.GL_SYNC_GPU_COMMANDS_COMPLETE, 0); - } - - pub fn rawAlloc(self: *Self, size: u31) SubAllocation { - var smallestBlock: ?*SubAllocation = null; - for (self.freeBlocks.items, 0..) |*block, i| { - if (size == block.len) { - self.used += size; - return self.freeBlocks.swapRemove(i); - } - if (size < block.len and if (smallestBlock) |_smallestBlock| block.len < _smallestBlock.len else true) { - smallestBlock = block; - } - } - if (smallestBlock) |block| { - const result = SubAllocation{.start = block.start, .len = size}; - block.start += size; - block.len -= size; - self.used += size; - return result; - } else { - std.log.info("Resizing internal mesh buffer from {} MiB to {} MiB", .{@as(usize, self.capacity)*@sizeOf(Entry) >> 20, (@as(usize, self.capacity)*@sizeOf(Entry) >> 20)*2}); - if (@as(usize, self.capacity)*@sizeOf(Entry)*2 > 1 << 31) @panic("OpenGL 2 GiB buffer size limit reached. Please lower your render distance."); - const oldBuffer = self.ssbo; - defer oldBuffer.deinit(); - const oldCapacity = self.capacity; - self.createBuffer(self.capacity*|2); // TODO: Is there a way to free the old buffer before creating the new one? - self.used += self.capacity - oldCapacity; - self.finalFree(.{.start = oldCapacity, .len = self.capacity - oldCapacity}); - - c.glBindBuffer(c.GL_COPY_READ_BUFFER, oldBuffer.bufferID); - c.glBindBuffer(c.GL_COPY_WRITE_BUFFER, self.ssbo.bufferID); - c.glCopyBufferSubData(c.GL_COPY_READ_BUFFER, c.GL_COPY_WRITE_BUFFER, 0, 0, @as(c.GLsizeiptr, oldCapacity)*@sizeOf(Entry)); - return rawAlloc(self, size); - } - } - - fn finalFree(self: *Self, _allocation: SubAllocation) void { - if (_allocation.len == 0) return; - self.used -= _allocation.len; - var allocation = _allocation; - for (self.freeBlocks.items, 0..) |*block, i| { - if (allocation.start + allocation.len == block.start) { - allocation.len += block.len; - _ = self.freeBlocks.swapRemove(i); - break; - } - } - for (self.freeBlocks.items) |*block| { - if (allocation.start == block.start + block.len) { - block.len += allocation.len; - return; - } - } - self.freeBlocks.append(allocation); - } - - pub fn free(self: *Self, allocation: SubAllocation) void { - if (allocation.len == 0) return; - self.fencedFreeLists[self.activeFence].append(allocation); - } - - /// Must unmap after use! - pub fn allocateAndMapRange(self: *Self, len: usize, allocation: *SubAllocation) []Entry { - self.free(allocation.*); - if (len == 0) { - allocation.len = 0; - return &.{}; - } - allocation.* = self.rawAlloc(@intCast(len)); - c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.ssbo.bufferID); - const ptr: [*]Entry = @ptrCast(@alignCast(c.glMapBufferRange( - c.GL_SHADER_STORAGE_BUFFER, - @as(c.GLintptr, allocation.start)*@sizeOf(Entry), - @as(c.GLsizeiptr, allocation.len)*@sizeOf(Entry), - c.GL_MAP_WRITE_BIT | c.GL_MAP_INVALIDATE_RANGE_BIT, - ))); - return ptr[0..len]; - } - - pub fn unmapRange(self: *Self, range: []Entry) void { - if (range.len == 0) return; - c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.ssbo.bufferID); - std.debug.assert(c.glUnmapBuffer(c.GL_SHADER_STORAGE_BUFFER) == c.GL_TRUE); - } - - pub fn uploadData(self: *Self, data: []const Entry, allocation: *SubAllocation) void { - self.free(allocation.*); - if (data.len == 0) { - allocation.len = 0; - return; - } - allocation.* = self.rawAlloc(@intCast(data.len)); - c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.ssbo.bufferID); - const ptr: [*]Entry = @ptrCast(@alignCast(c.glMapBufferRange( - c.GL_SHADER_STORAGE_BUFFER, - @as(c.GLintptr, allocation.start)*@sizeOf(Entry), - @as(c.GLsizeiptr, allocation.len)*@sizeOf(Entry), - c.GL_MAP_WRITE_BIT | c.GL_MAP_INVALIDATE_RANGE_BIT, - ))); - @memcpy(ptr, data); - std.debug.assert(c.glUnmapBuffer(c.GL_SHADER_STORAGE_BUFFER) == c.GL_TRUE); - } - }; + return struct { + ssbo: SSBO, + freeBlocks: main.List(SubAllocation), + fences: [3]c.GLsync, + fencedFreeLists: [3]main.List(SubAllocation), + activeFence: u8, + capacity: u31, + used: u31, + binding: c_uint, + + const Self = @This(); + + fn createBuffer(self: *Self, size: u31) void { + self.ssbo = .init(); + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.ssbo.bufferID); + const flags = c.GL_MAP_WRITE_BIT | c.GL_DYNAMIC_STORAGE_BIT; + const bytes = @as(c.GLsizeiptr, size) * @sizeOf(Entry); + c.glBufferStorage(c.GL_SHADER_STORAGE_BUFFER, bytes, null, flags); + self.ssbo.bind(self.binding); + self.capacity = size; + } + + pub fn init(self: *Self, allocator: NeverFailingAllocator, size: u31, binding: c_uint) void { + self.used = 0; + self.binding = binding; + self.createBuffer(size); + self.activeFence = 0; + for (&self.fences) |*fence| { + fence.* = c.glFenceSync(c.GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + } + for (&self.fencedFreeLists) |*list| { + list.* = .init(allocator); + } + + self.freeBlocks = .init(allocator); + self.freeBlocks.append(.{ .start = 0, .len = size }); + } + + pub fn deinit(self: *Self) void { + for (self.fences) |fence| { + c.glDeleteSync(fence); + } + for (self.fencedFreeLists) |list| { + list.deinit(); + } + self.ssbo.deinit(); + self.freeBlocks.deinit(); + } + + pub fn beginRender(self: *Self) void { + self.activeFence += 1; + if (self.activeFence == self.fences.len) self.activeFence = 0; + const endTime = main.timestamp().addDuration(.fromMilliseconds(5)); + while (self.fencedFreeLists[self.activeFence].popOrNull()) |allocation| { + self.finalFree(allocation); + if (main.timestamp().durationTo(endTime).nanoseconds < 0) break; // TODO: Remove after #1434 + } + _ = c.glClientWaitSync(self.fences[self.activeFence], 0, c.GL_TIMEOUT_IGNORED); // Make sure the render calls that accessed these parts of the buffer have finished. + } + + pub fn endRender(self: *Self) void { + c.glDeleteSync(self.fences[self.activeFence]); + self.fences[self.activeFence] = c.glFenceSync(c.GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + } + + pub fn rawAlloc(self: *Self, size: u31) SubAllocation { + var smallestBlock: ?*SubAllocation = null; + for (self.freeBlocks.items, 0..) |*block, i| { + if (size == block.len) { + self.used += size; + return self.freeBlocks.swapRemove(i); + } + if (size < block.len and if (smallestBlock) |_smallestBlock| block.len < _smallestBlock.len else true) { + smallestBlock = block; + } + } + if (smallestBlock) |block| { + const result = SubAllocation{ .start = block.start, .len = size }; + block.start += size; + block.len -= size; + self.used += size; + return result; + } else { + std.log.info("Resizing internal mesh buffer from {} MiB to {} MiB", .{ @as(usize, self.capacity) * @sizeOf(Entry) >> 20, (@as(usize, self.capacity) * @sizeOf(Entry) >> 20) * 2 }); + if (@as(usize, self.capacity) * @sizeOf(Entry) * 2 > 1 << 31) @panic("OpenGL 2 GiB buffer size limit reached. Please lower your render distance."); + const oldBuffer = self.ssbo; + defer oldBuffer.deinit(); + const oldCapacity = self.capacity; + self.createBuffer(self.capacity *| 2); // TODO: Is there a way to free the old buffer before creating the new one? + self.used += self.capacity - oldCapacity; + self.finalFree(.{ .start = oldCapacity, .len = self.capacity - oldCapacity }); + + c.glBindBuffer(c.GL_COPY_READ_BUFFER, oldBuffer.bufferID); + c.glBindBuffer(c.GL_COPY_WRITE_BUFFER, self.ssbo.bufferID); + c.glCopyBufferSubData(c.GL_COPY_READ_BUFFER, c.GL_COPY_WRITE_BUFFER, 0, 0, @as(c.GLsizeiptr, oldCapacity) * @sizeOf(Entry)); + return rawAlloc(self, size); + } + } + + fn finalFree(self: *Self, _allocation: SubAllocation) void { + if (_allocation.len == 0) return; + self.used -= _allocation.len; + var allocation = _allocation; + for (self.freeBlocks.items, 0..) |*block, i| { + if (allocation.start + allocation.len == block.start) { + allocation.len += block.len; + _ = self.freeBlocks.swapRemove(i); + break; + } + } + for (self.freeBlocks.items) |*block| { + if (allocation.start == block.start + block.len) { + block.len += allocation.len; + return; + } + } + self.freeBlocks.append(allocation); + } + + pub fn free(self: *Self, allocation: SubAllocation) void { + if (allocation.len == 0) return; + self.fencedFreeLists[self.activeFence].append(allocation); + } + + /// Must unmap after use! + pub fn allocateAndMapRange(self: *Self, len: usize, allocation: *SubAllocation) []Entry { + self.free(allocation.*); + if (len == 0) { + allocation.len = 0; + return &.{}; + } + allocation.* = self.rawAlloc(@intCast(len)); + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.ssbo.bufferID); + const ptr: [*]Entry = @ptrCast(@alignCast(c.glMapBufferRange( + c.GL_SHADER_STORAGE_BUFFER, + @as(c.GLintptr, allocation.start) * @sizeOf(Entry), + @as(c.GLsizeiptr, allocation.len) * @sizeOf(Entry), + c.GL_MAP_WRITE_BIT | c.GL_MAP_INVALIDATE_RANGE_BIT, + ))); + return ptr[0..len]; + } + + pub fn unmapRange(self: *Self, range: []Entry) void { + if (range.len == 0) return; + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.ssbo.bufferID); + std.debug.assert(c.glUnmapBuffer(c.GL_SHADER_STORAGE_BUFFER) == c.GL_TRUE); + } + + pub fn uploadData(self: *Self, data: []const Entry, allocation: *SubAllocation) void { + self.free(allocation.*); + if (data.len == 0) { + allocation.len = 0; + return; + } + allocation.* = self.rawAlloc(@intCast(data.len)); + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.ssbo.bufferID); + const ptr: [*]Entry = @ptrCast(@alignCast(c.glMapBufferRange( + c.GL_SHADER_STORAGE_BUFFER, + @as(c.GLintptr, allocation.start) * @sizeOf(Entry), + @as(c.GLsizeiptr, allocation.len) * @sizeOf(Entry), + c.GL_MAP_WRITE_BIT | c.GL_MAP_INVALIDATE_RANGE_BIT, + ))); + @memcpy(ptr, data); + std.debug.assert(c.glUnmapBuffer(c.GL_SHADER_STORAGE_BUFFER) == c.GL_TRUE); + } + }; } pub const FrameBuffer = struct { // MARK: FrameBuffer - frameBuffer: c_uint, - texture: c_uint, - hasDepthTexture: bool, - depthTexture: c_uint, - - pub fn init(self: *FrameBuffer, hasDepthTexture: bool, textureFilter: c_int, textureWrap: c_int) void { - self.* = FrameBuffer{ - .frameBuffer = undefined, - .texture = undefined, - .depthTexture = undefined, - .hasDepthTexture = hasDepthTexture, - }; - c.glGenFramebuffers(1, &self.frameBuffer); - c.glBindFramebuffer(c.GL_FRAMEBUFFER, self.frameBuffer); - if (hasDepthTexture) { - c.glGenTextures(1, &self.depthTexture); - c.glBindTexture(c.GL_TEXTURE_2D, self.depthTexture); - c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MIN_FILTER, textureFilter); - c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MAG_FILTER, textureFilter); - c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, textureWrap); - c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, textureWrap); - c.glFramebufferTexture2D(c.GL_FRAMEBUFFER, c.GL_DEPTH_ATTACHMENT, c.GL_TEXTURE_2D, self.depthTexture, 0); - } - c.glGenTextures(1, &self.texture); - c.glBindTexture(c.GL_TEXTURE_2D, self.texture); - c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MIN_FILTER, textureFilter); - c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MAG_FILTER, textureFilter); - c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, textureWrap); - c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, textureWrap); - c.glFramebufferTexture2D(c.GL_FRAMEBUFFER, c.GL_COLOR_ATTACHMENT0, c.GL_TEXTURE_2D, self.texture, 0); - - c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0); - } - - pub fn deinit(self: *FrameBuffer) void { - c.glDeleteFramebuffers(1, &self.frameBuffer); - if (self.hasDepthTexture) { - c.glDeleteRenderbuffers(1, &self.depthTexture); - } - c.glDeleteTextures(1, &self.texture); - } - - pub fn updateSize(self: *FrameBuffer, _width: u31, _height: u31, internalFormat: c_int) void { - const width = @max(_width, 1); - const height = @max(_height, 1); - c.glBindFramebuffer(c.GL_FRAMEBUFFER, self.frameBuffer); - if (self.hasDepthTexture) { - c.glBindTexture(c.GL_TEXTURE_2D, self.depthTexture); - c.glTexImage2D(c.GL_TEXTURE_2D, 0, c.GL_DEPTH_COMPONENT32F, width, height, 0, c.GL_DEPTH_COMPONENT, c.GL_FLOAT, null); - } - - c.glBindTexture(c.GL_TEXTURE_2D, self.texture); - c.glTexImage2D(c.GL_TEXTURE_2D, 0, internalFormat, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); - } - - pub fn clear(_: FrameBuffer, clearColor: Vec4f) void { - c.glDepthFunc(c.GL_LESS); - c.glDepthMask(c.GL_TRUE); - c.glDisable(c.GL_SCISSOR_TEST); - c.glClearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]); - c.glClear(c.GL_COLOR_BUFFER_BIT | c.GL_DEPTH_BUFFER_BIT); - } - - pub fn validate(self: *const FrameBuffer) bool { - c.glBindFramebuffer(c.GL_FRAMEBUFFER, self.frameBuffer); - defer c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0); - if (c.glCheckFramebufferStatus(c.GL_FRAMEBUFFER) != c.GL_FRAMEBUFFER_COMPLETE) { - std.log.err("Frame Buffer Object error: {}", .{c.glCheckFramebufferStatus(c.GL_FRAMEBUFFER)}); - return false; - } - return true; - } - - pub fn bindTexture(self: *const FrameBuffer, target: c_uint) void { - c.glActiveTexture(target); - c.glBindTexture(c.GL_TEXTURE_2D, self.texture); - } - - pub fn bindDepthTexture(self: *const FrameBuffer, target: c_uint) void { - std.debug.assert(self.hasDepthTexture); - c.glActiveTexture(target); - c.glBindTexture(c.GL_TEXTURE_2D, self.depthTexture); - } - - pub fn bind(self: *const FrameBuffer) void { - c.glBindFramebuffer(c.GL_FRAMEBUFFER, self.frameBuffer); - } - - pub fn unbind(_: *const FrameBuffer) void { - c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0); - } + frameBuffer: c_uint, + texture: c_uint, + hasDepthTexture: bool, + depthTexture: c_uint, + + pub fn init(self: *FrameBuffer, hasDepthTexture: bool, textureFilter: c_int, textureWrap: c_int) void { + self.* = FrameBuffer{ + .frameBuffer = undefined, + .texture = undefined, + .depthTexture = undefined, + .hasDepthTexture = hasDepthTexture, + }; + c.glGenFramebuffers(1, &self.frameBuffer); + c.glBindFramebuffer(c.GL_FRAMEBUFFER, self.frameBuffer); + if (hasDepthTexture) { + c.glGenTextures(1, &self.depthTexture); + c.glBindTexture(c.GL_TEXTURE_2D, self.depthTexture); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MIN_FILTER, textureFilter); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MAG_FILTER, textureFilter); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, textureWrap); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, textureWrap); + c.glFramebufferTexture2D(c.GL_FRAMEBUFFER, c.GL_DEPTH_ATTACHMENT, c.GL_TEXTURE_2D, self.depthTexture, 0); + } + c.glGenTextures(1, &self.texture); + c.glBindTexture(c.GL_TEXTURE_2D, self.texture); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MIN_FILTER, textureFilter); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MAG_FILTER, textureFilter); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, textureWrap); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, textureWrap); + c.glFramebufferTexture2D(c.GL_FRAMEBUFFER, c.GL_COLOR_ATTACHMENT0, c.GL_TEXTURE_2D, self.texture, 0); + + c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0); + } + + pub fn deinit(self: *FrameBuffer) void { + c.glDeleteFramebuffers(1, &self.frameBuffer); + if (self.hasDepthTexture) { + c.glDeleteRenderbuffers(1, &self.depthTexture); + } + c.glDeleteTextures(1, &self.texture); + } + + pub fn updateSize(self: *FrameBuffer, _width: u31, _height: u31, internalFormat: c_int) void { + const width = @max(_width, 1); + const height = @max(_height, 1); + c.glBindFramebuffer(c.GL_FRAMEBUFFER, self.frameBuffer); + if (self.hasDepthTexture) { + c.glBindTexture(c.GL_TEXTURE_2D, self.depthTexture); + c.glTexImage2D(c.GL_TEXTURE_2D, 0, c.GL_DEPTH_COMPONENT32F, width, height, 0, c.GL_DEPTH_COMPONENT, c.GL_FLOAT, null); + } + + c.glBindTexture(c.GL_TEXTURE_2D, self.texture); + c.glTexImage2D(c.GL_TEXTURE_2D, 0, internalFormat, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); + } + + pub fn clear(_: FrameBuffer, clearColor: Vec4f) void { + c.glDepthFunc(c.GL_LESS); + c.glDepthMask(c.GL_TRUE); + c.glDisable(c.GL_SCISSOR_TEST); + c.glClearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]); + c.glClear(c.GL_COLOR_BUFFER_BIT | c.GL_DEPTH_BUFFER_BIT); + } + + pub fn validate(self: *const FrameBuffer) bool { + c.glBindFramebuffer(c.GL_FRAMEBUFFER, self.frameBuffer); + defer c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0); + if (c.glCheckFramebufferStatus(c.GL_FRAMEBUFFER) != c.GL_FRAMEBUFFER_COMPLETE) { + std.log.err("Frame Buffer Object error: {}", .{c.glCheckFramebufferStatus(c.GL_FRAMEBUFFER)}); + return false; + } + return true; + } + + pub fn bindTexture(self: *const FrameBuffer, target: c_uint) void { + c.glActiveTexture(target); + c.glBindTexture(c.GL_TEXTURE_2D, self.texture); + } + + pub fn bindDepthTexture(self: *const FrameBuffer, target: c_uint) void { + std.debug.assert(self.hasDepthTexture); + c.glActiveTexture(target); + c.glBindTexture(c.GL_TEXTURE_2D, self.depthTexture); + } + + pub fn bind(self: *const FrameBuffer) void { + c.glBindFramebuffer(c.GL_FRAMEBUFFER, self.frameBuffer); + } + + pub fn unbind(_: *const FrameBuffer) void { + c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0); + } }; pub const TextureArray = struct { // MARK: TextureArray - textureID: c_uint, - - pub fn init() TextureArray { - var self: TextureArray = undefined; - c.glGenTextures(1, &self.textureID); - return self; - } - - pub fn deinit(self: TextureArray) void { - c.glDeleteTextures(1, &self.textureID); - } - - pub fn bind(self: TextureArray) void { - c.glBindTexture(c.GL_TEXTURE_2D_ARRAY, self.textureID); - } - - fn lodColorInterpolation(colors: [4]Color, alphaCorrection: bool) Color { - var r: [4]f32 = undefined; - var g: [4]f32 = undefined; - var b: [4]f32 = undefined; - var a: [4]f32 = undefined; - for (0..4) |i| { - r[i] = @floatFromInt(colors[i].r); - g[i] = @floatFromInt(colors[i].g); - b[i] = @floatFromInt(colors[i].b); - a[i] = @floatFromInt(colors[i].a); - } - // Use gamma corrected average(https://stackoverflow.com/a/832314/13082649): - var aSum: f32 = 0; - var rSum: f32 = 0; - var gSum: f32 = 0; - var bSum: f32 = 0; - for (0..4) |i| { - const w = if (alphaCorrection) a[i]*a[i] else 1; - aSum += a[i]*a[i]; - rSum += w*r[i]*r[i]; - gSum += w*g[i]*g[i]; - bSum += w*b[i]*b[i]; - } - aSum = @sqrt(aSum)/2; - rSum = @sqrt(rSum)/2; - gSum = @sqrt(gSum)/2; - bSum = @sqrt(bSum)/2; - if (alphaCorrection and aSum != 0) { - rSum /= aSum; - gSum /= aSum; - bSum /= aSum; - } - return Color{.r = @trunc(rSum), .g = @trunc(gSum), .b = @trunc(bSum), .a = @trunc(aSum)}; - } - - /// (Re-)Generates the GPU buffer. - pub fn generate(self: TextureArray, images: []Image, mipmapping: bool, alphaCorrectMipmapping: bool) void { - var maxWidth: u31 = 1; - var maxHeight: u31 = 1; - for (images) |image| { - maxWidth = @max(maxWidth, image.width); - maxHeight = @max(maxHeight, image.height); - } - // Make sure the width and height use a power of 2: - if (maxWidth - 1 & maxWidth != 0) { - maxWidth = @as(u31, 2) << std.math.log2_int(u31, maxWidth); - } - if (maxHeight - 1 & maxHeight != 0) { - maxHeight = @as(u31, 2) << std.math.log2_int(u31, maxHeight); - } - - std.log.debug("Creating Texture Array of size {}×{} with {} layers.", .{maxWidth, maxHeight, images.len}); - - self.bind(); - - const maxLOD = if (mipmapping) 1 + std.math.log2_int(u31, @max(maxWidth, maxHeight)) else 1; - for (0..maxLOD) |i| { - c.glTexImage3D(c.GL_TEXTURE_2D_ARRAY, @intCast(i), c.GL_RGBA8, @max(1, maxWidth >> @intCast(i)), @max(1, maxHeight >> @intCast(i)), @intCast(images.len), 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); - } - const arena = main.stackAllocator.createArena(); - defer main.stackAllocator.destroyArena(arena); - const lodBuffer: [][]Color = arena.alloc([]Color, maxLOD); - for (lodBuffer, 0..) |*buffer, i| { - buffer.* = arena.alloc(Color, @max(1, maxWidth >> @intCast(i))*@max(1, maxHeight >> @intCast(i))); - } - - for (images, 0..) |image, i| { - // Fill the buffer using nearest sampling. Probably not the best solutions for all textures, but that's what happens when someone doesn't use power of 2 textures... - for (0..maxWidth) |x| { - for (0..maxHeight) |y| { - const index = x + y*maxWidth; - const imageIndex = (x*image.width)/maxWidth + image.width*((y*image.height)/maxHeight); - lodBuffer[0][index] = image.imageData[imageIndex]; - } - } - - // Calculate the mipmap levels: - for (1..lodBuffer.len) |_lod| { - const lod: u5 = @intCast(_lod); - const curWidth = @max(1, maxWidth >> lod); - const curHeight = @max(1, maxHeight >> lod); - const nextWidth = @max(1, maxWidth >> (lod - 1)); - const nextHeight = @max(1, maxHeight >> (lod - 1)); - for (0..curWidth) |x| { - for (0..curHeight) |y| { - const index = x + y*curWidth; - const colors = [4]Color{ - lodBuffer[lod - 1][@min(2*x, nextWidth - 1) + @min(2*y, nextHeight - 1)*nextWidth], - lodBuffer[lod - 1][@min(2*x, nextWidth - 1) + @min(2*y + 1, nextHeight - 1)*nextWidth], - lodBuffer[lod - 1][@min(2*x + 1, nextWidth - 1) + @min(2*y, nextHeight - 1)*nextWidth], - lodBuffer[lod - 1][@min(2*x + 1, nextWidth - 1) + @min(2*y + 1, nextHeight - 1)*nextWidth], - }; - lodBuffer[lod][index] = lodColorInterpolation(colors, alphaCorrectMipmapping); - } - } - } - // Give the correct color to alpha 0 pixels, to avoid dark pixels: - for (1..lodBuffer.len) |_lod| { - const lod: u5 = @intCast(lodBuffer.len - 1 - _lod); - const curWidth = @max(1, maxWidth >> lod); - const curHeight = @max(1, maxHeight >> lod); - for (0..curWidth) |x| { - for (0..curHeight) |y| { - const index = x + y*curWidth; - const index2 = x/2 + y/2*curWidth/2; - if (lodBuffer[lod][index].a == 0) { - lodBuffer[lod][index].r = lodBuffer[lod + 1][index2].r; - lodBuffer[lod][index].g = lodBuffer[lod + 1][index2].g; - lodBuffer[lod][index].b = lodBuffer[lod + 1][index2].b; - } - } - } - } - // Upload: - for (0..lodBuffer.len) |_lod| { - const lod: u5 = @intCast(lodBuffer.len - 1 - _lod); - const curWidth = @max(1, maxWidth >> lod); - const curHeight = @max(1, maxHeight >> lod); - c.glTexSubImage3D(c.GL_TEXTURE_2D_ARRAY, lod, 0, 0, @intCast(i), curWidth, curHeight, 1, c.GL_RGBA, c.GL_UNSIGNED_BYTE, lodBuffer[lod].ptr); - } - } - c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MAX_LOD, maxLOD); - c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MIN_FILTER, c.GL_NEAREST_MIPMAP_LINEAR); - c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MAG_FILTER, c.GL_NEAREST); - c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT); - c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT); - } + textureID: c_uint, + + pub fn init() TextureArray { + var self: TextureArray = undefined; + c.glGenTextures(1, &self.textureID); + return self; + } + + pub fn deinit(self: TextureArray) void { + c.glDeleteTextures(1, &self.textureID); + } + + pub fn bind(self: TextureArray) void { + c.glBindTexture(c.GL_TEXTURE_2D_ARRAY, self.textureID); + } + + fn lodColorInterpolation(colors: [4]Color, alphaCorrection: bool) Color { + var r: [4]f32 = undefined; + var g: [4]f32 = undefined; + var b: [4]f32 = undefined; + var a: [4]f32 = undefined; + for (0..4) |i| { + r[i] = @floatFromInt(colors[i].r); + g[i] = @floatFromInt(colors[i].g); + b[i] = @floatFromInt(colors[i].b); + a[i] = @floatFromInt(colors[i].a); + } + // Use gamma corrected average(https://stackoverflow.com/a/832314/13082649): + var aSum: f32 = 0; + var rSum: f32 = 0; + var gSum: f32 = 0; + var bSum: f32 = 0; + for (0..4) |i| { + const w = if (alphaCorrection) a[i] * a[i] else 1; + aSum += a[i] * a[i]; + rSum += w * r[i] * r[i]; + gSum += w * g[i] * g[i]; + bSum += w * b[i] * b[i]; + } + aSum = @sqrt(aSum) / 2; + rSum = @sqrt(rSum) / 2; + gSum = @sqrt(gSum) / 2; + bSum = @sqrt(bSum) / 2; + if (alphaCorrection and aSum != 0) { + rSum /= aSum; + gSum /= aSum; + bSum /= aSum; + } + return Color{ .r = @trunc(rSum), .g = @trunc(gSum), .b = @trunc(bSum), .a = @trunc(aSum) }; + } + + /// (Re-)Generates the GPU buffer. + pub fn generate(self: TextureArray, images: []Image, mipmapping: bool, alphaCorrectMipmapping: bool) void { + var maxWidth: u31 = 1; + var maxHeight: u31 = 1; + for (images) |image| { + maxWidth = @max(maxWidth, image.width); + maxHeight = @max(maxHeight, image.height); + } + // Make sure the width and height use a power of 2: + if (maxWidth - 1 & maxWidth != 0) { + maxWidth = @as(u31, 2) << std.math.log2_int(u31, maxWidth); + } + if (maxHeight - 1 & maxHeight != 0) { + maxHeight = @as(u31, 2) << std.math.log2_int(u31, maxHeight); + } + + std.log.debug("Creating Texture Array of size {}×{} with {} layers.", .{ maxWidth, maxHeight, images.len }); + + self.bind(); + + const maxLOD = if (mipmapping) 1 + std.math.log2_int(u31, @max(maxWidth, maxHeight)) else 1; + for (0..maxLOD) |i| { + c.glTexImage3D(c.GL_TEXTURE_2D_ARRAY, @intCast(i), c.GL_RGBA8, @max(1, maxWidth >> @intCast(i)), @max(1, maxHeight >> @intCast(i)), @intCast(images.len), 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); + } + const arena = main.stackAllocator.createArena(); + defer main.stackAllocator.destroyArena(arena); + const lodBuffer: [][]Color = arena.alloc([]Color, maxLOD); + for (lodBuffer, 0..) |*buffer, i| { + buffer.* = arena.alloc(Color, @max(1, maxWidth >> @intCast(i)) * @max(1, maxHeight >> @intCast(i))); + } + + for (images, 0..) |image, i| { + // Fill the buffer using nearest sampling. Probably not the best solutions for all textures, but that's what happens when someone doesn't use power of 2 textures... + for (0..maxWidth) |x| { + for (0..maxHeight) |y| { + const index = x + y * maxWidth; + const imageIndex = (x * image.width) / maxWidth + image.width * ((y * image.height) / maxHeight); + lodBuffer[0][index] = image.imageData[imageIndex]; + } + } + + // Calculate the mipmap levels: + for (1..lodBuffer.len) |_lod| { + const lod: u5 = @intCast(_lod); + const curWidth = @max(1, maxWidth >> lod); + const curHeight = @max(1, maxHeight >> lod); + const nextWidth = @max(1, maxWidth >> (lod - 1)); + const nextHeight = @max(1, maxHeight >> (lod - 1)); + for (0..curWidth) |x| { + for (0..curHeight) |y| { + const index = x + y * curWidth; + const colors = [4]Color{ + lodBuffer[lod - 1][@min(2 * x, nextWidth - 1) + @min(2 * y, nextHeight - 1) * nextWidth], + lodBuffer[lod - 1][@min(2 * x, nextWidth - 1) + @min(2 * y + 1, nextHeight - 1) * nextWidth], + lodBuffer[lod - 1][@min(2 * x + 1, nextWidth - 1) + @min(2 * y, nextHeight - 1) * nextWidth], + lodBuffer[lod - 1][@min(2 * x + 1, nextWidth - 1) + @min(2 * y + 1, nextHeight - 1) * nextWidth], + }; + lodBuffer[lod][index] = lodColorInterpolation(colors, alphaCorrectMipmapping); + } + } + } + // Give the correct color to alpha 0 pixels, to avoid dark pixels: + for (1..lodBuffer.len) |_lod| { + const lod: u5 = @intCast(lodBuffer.len - 1 - _lod); + const curWidth = @max(1, maxWidth >> lod); + const curHeight = @max(1, maxHeight >> lod); + for (0..curWidth) |x| { + for (0..curHeight) |y| { + const index = x + y * curWidth; + const index2 = x / 2 + y / 2 * curWidth / 2; + if (lodBuffer[lod][index].a == 0) { + lodBuffer[lod][index].r = lodBuffer[lod + 1][index2].r; + lodBuffer[lod][index].g = lodBuffer[lod + 1][index2].g; + lodBuffer[lod][index].b = lodBuffer[lod + 1][index2].b; + } + } + } + } + // Upload: + for (0..lodBuffer.len) |_lod| { + const lod: u5 = @intCast(lodBuffer.len - 1 - _lod); + const curWidth = @max(1, maxWidth >> lod); + const curHeight = @max(1, maxHeight >> lod); + c.glTexSubImage3D(c.GL_TEXTURE_2D_ARRAY, lod, 0, 0, @intCast(i), curWidth, curHeight, 1, c.GL_RGBA, c.GL_UNSIGNED_BYTE, lodBuffer[lod].ptr); + } + } + c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MAX_LOD, maxLOD); + c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MIN_FILTER, c.GL_NEAREST_MIPMAP_LINEAR); + c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MAG_FILTER, c.GL_NEAREST); + c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT); + c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT); + } }; pub const Texture = struct { // MARK: Texture - textureID: c_uint, - - pub fn init() Texture { - var self: Texture = undefined; - c.glGenTextures(1, &self.textureID); - return self; - } - - pub fn initFromFile(path: []const u8) Texture { - const self = Texture.init(); - const image = Image.readFromFile(main.stackAllocator, path) catch |err| blk: { - std.log.err("Couldn't read image from {s}: {s}", .{path, @errorName(err)}); - break :blk Image.defaultImage; - }; - defer image.deinit(main.stackAllocator); - self.generate(image); - return self; - } - - pub fn initFromMipmapFiles(pathPrefix: []const u8, largestSize: u31, lodBias: f32) Texture { - const self = Texture.init(); - self.bind(); - - const maxLod = std.math.log2_int(u31, largestSize); - - var curSize: u31 = largestSize; - while (curSize != 0) : (curSize /= 2) { - c.glTexImage2D(c.GL_TEXTURE_2D, maxLod - std.math.log2_int(u31, curSize), c.GL_RGBA8, curSize, curSize, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); - } - - curSize = largestSize; - while (curSize != 0) : (curSize /= 2) { - const path = std.fmt.allocPrint(main.stackAllocator.allocator, "{s}{}.png", .{pathPrefix, curSize}) catch unreachable; - defer main.stackAllocator.free(path); - const image = Image.readFromFile(main.stackAllocator, path) catch |err| blk: { - std.log.err("Couldn't read image from {s}: {s}", .{path, @errorName(err)}); - break :blk Image.defaultImage; - }; - defer image.deinit(main.stackAllocator); - c.glTexSubImage2D(c.GL_TEXTURE_2D, maxLod - std.math.log2_int(u31, curSize), 0, 0, curSize, curSize, c.GL_RGBA, c.GL_UNSIGNED_BYTE, image.imageData.ptr); - } - - c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MAX_LOD, maxLod); - c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MIN_FILTER, c.GL_NEAREST_MIPMAP_LINEAR); - c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MAG_FILTER, c.GL_NEAREST); - c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT); - c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT); - c.glTexParameterf(c.GL_TEXTURE_2D, c.GL_TEXTURE_LOD_BIAS, lodBias); - return self; - } - - pub fn deinit(self: Texture) void { - c.glDeleteTextures(1, &self.textureID); - } - - pub fn bindTo(self: Texture, binding: u5) void { - c.glActiveTexture(@intCast(c.GL_TEXTURE0 + binding)); - c.glBindTexture(c.GL_TEXTURE_2D, self.textureID); - } - - pub fn bind(self: Texture) void { - c.glBindTexture(c.GL_TEXTURE_2D, self.textureID); - } - - /// (Re-)Generates the GPU buffer. - pub fn generate(self: Texture, image: Image) void { - self.bind(); - - c.glTexImage2D(c.GL_TEXTURE_2D, 0, c.GL_RGBA8, image.width, image.height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, image.imageData.ptr); - 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.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT); - c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT); - } - - pub fn render(self: Texture, pos: Vec2f, dim: Vec2f) void { - self.bindTo(0); - draw.boundImage(pos, dim); - } - - pub fn size(self: Texture) Vec2i { - self.bind(); - var result: Vec2i = undefined; - c.glGetTexLevelParameteriv(c.GL_TEXTURE_2D, 0, c.GL_TEXTURE_WIDTH, &result[0]); - c.glGetTexLevelParameteriv(c.GL_TEXTURE_2D, 0, c.GL_TEXTURE_HEIGHT, &result[1]); - return result; - } + textureID: c_uint, + + pub fn init() Texture { + var self: Texture = undefined; + c.glGenTextures(1, &self.textureID); + return self; + } + + pub fn initFromFile(path: []const u8) Texture { + const self = Texture.init(); + const image = Image.readFromFile(main.stackAllocator, path) catch |err| blk: { + std.log.err("Couldn't read image from {s}: {s}", .{ path, @errorName(err) }); + break :blk Image.defaultImage; + }; + defer image.deinit(main.stackAllocator); + self.generate(image); + return self; + } + + pub fn initFromMipmapFiles(pathPrefix: []const u8, largestSize: u31, lodBias: f32) Texture { + const self = Texture.init(); + self.bind(); + + const maxLod = std.math.log2_int(u31, largestSize); + + var curSize: u31 = largestSize; + while (curSize != 0) : (curSize /= 2) { + c.glTexImage2D(c.GL_TEXTURE_2D, maxLod - std.math.log2_int(u31, curSize), c.GL_RGBA8, curSize, curSize, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); + } + + curSize = largestSize; + while (curSize != 0) : (curSize /= 2) { + const path = std.fmt.allocPrint(main.stackAllocator.allocator, "{s}{}.png", .{ pathPrefix, curSize }) catch unreachable; + defer main.stackAllocator.free(path); + const image = Image.readFromFile(main.stackAllocator, path) catch |err| blk: { + std.log.err("Couldn't read image from {s}: {s}", .{ path, @errorName(err) }); + break :blk Image.defaultImage; + }; + defer image.deinit(main.stackAllocator); + c.glTexSubImage2D(c.GL_TEXTURE_2D, maxLod - std.math.log2_int(u31, curSize), 0, 0, curSize, curSize, c.GL_RGBA, c.GL_UNSIGNED_BYTE, image.imageData.ptr); + } + + c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MAX_LOD, maxLod); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MIN_FILTER, c.GL_NEAREST_MIPMAP_LINEAR); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MAG_FILTER, c.GL_NEAREST); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT); + c.glTexParameterf(c.GL_TEXTURE_2D, c.GL_TEXTURE_LOD_BIAS, lodBias); + return self; + } + + pub fn deinit(self: Texture) void { + c.glDeleteTextures(1, &self.textureID); + } + + pub fn bindTo(self: Texture, binding: u5) void { + c.glActiveTexture(@intCast(c.GL_TEXTURE0 + binding)); + c.glBindTexture(c.GL_TEXTURE_2D, self.textureID); + } + + pub fn bind(self: Texture) void { + c.glBindTexture(c.GL_TEXTURE_2D, self.textureID); + } + + /// (Re-)Generates the GPU buffer. + pub fn generate(self: Texture, image: Image) void { + self.bind(); + + c.glTexImage2D(c.GL_TEXTURE_2D, 0, c.GL_RGBA8, image.width, image.height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, image.imageData.ptr); + 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.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT); + } + + pub fn render(self: Texture, pos: Vec2f, dim: Vec2f) void { + self.bindTo(0); + draw.boundImage(pos, dim); + } + + pub fn size(self: Texture) Vec2i { + self.bind(); + var result: Vec2i = undefined; + c.glGetTexLevelParameteriv(c.GL_TEXTURE_2D, 0, c.GL_TEXTURE_WIDTH, &result[0]); + c.glGetTexLevelParameteriv(c.GL_TEXTURE_2D, 0, c.GL_TEXTURE_HEIGHT, &result[1]); + return result; + } }; pub const CubeMapTexture = struct { // MARK: CubeMapTexture - textureID: c_uint, - - pub fn init() CubeMapTexture { - var self: CubeMapTexture = undefined; - c.glGenTextures(1, &self.textureID); - return self; - } - - pub fn deinit(self: CubeMapTexture) void { - c.glDeleteTextures(1, &self.textureID); - } - - pub fn bindTo(self: CubeMapTexture, binding: u5) void { - c.glActiveTexture(@intCast(c.GL_TEXTURE0 + binding)); - c.glBindTexture(c.GL_TEXTURE_CUBE_MAP, self.textureID); - } - - pub fn bind(self: CubeMapTexture) void { - c.glBindTexture(c.GL_TEXTURE_CUBE_MAP, self.textureID); - } - - /// (Re-)Generates the GPU buffer. - pub fn generate(self: CubeMapTexture, width: u31, height: u31) void { - self.bind(); - - c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); - c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); - c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_POSITIVE_Y, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); - c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); - c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_POSITIVE_Z, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); - c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); - c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_MIN_FILTER, c.GL_LINEAR); - c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_MAG_FILTER, c.GL_LINEAR); - c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_WRAP_S, c.GL_CLAMP_TO_EDGE); - c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_WRAP_T, c.GL_CLAMP_TO_EDGE); - c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_WRAP_R, c.GL_CLAMP_TO_EDGE); - c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_BASE_LEVEL, 0); - c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_MAX_LEVEL, 0); - } - - pub fn faceNormal(face: usize) Vec3f { - const normals = [_]Vec3f{ - .{1, 0, 0}, // +x - .{-1, 0, 0}, // -x - .{0, 1, 0}, // +y - .{0, -1, 0}, // -y - .{0, 0, 1}, // +z - .{0, 0, -1}, // -z - }; - return normals[face]; - } - - pub fn faceUp(face: usize) Vec3f { - const ups = [_]Vec3f{ - .{0, -1, 0}, // +x - .{0, -1, 0}, // -x - .{0, 0, 1}, // +y - .{0, 0, -1}, // -y - .{0, -1, 0}, // +z - .{0, -1, 0}, // -z - }; - return ups[face]; - } - - pub fn faceRight(face: usize) Vec3f { - comptime var rights: [6]Vec3f = undefined; - inline for (0..6) |i| { - rights[i] = comptime vec.cross(faceNormal(i), faceUp(i)); - } - return rights[face]; - } - - pub fn bindToFramebuffer(self: CubeMapTexture, fb: FrameBuffer, face: c_uint) void { - fb.bind(); - c.glFramebufferTexture2D(c.GL_FRAMEBUFFER, c.GL_COLOR_ATTACHMENT0, @as(c_uint, c.GL_TEXTURE_CUBE_MAP_POSITIVE_X) + face, self.textureID, 0); - } + textureID: c_uint, + + pub fn init() CubeMapTexture { + var self: CubeMapTexture = undefined; + c.glGenTextures(1, &self.textureID); + return self; + } + + pub fn deinit(self: CubeMapTexture) void { + c.glDeleteTextures(1, &self.textureID); + } + + pub fn bindTo(self: CubeMapTexture, binding: u5) void { + c.glActiveTexture(@intCast(c.GL_TEXTURE0 + binding)); + c.glBindTexture(c.GL_TEXTURE_CUBE_MAP, self.textureID); + } + + pub fn bind(self: CubeMapTexture) void { + c.glBindTexture(c.GL_TEXTURE_CUBE_MAP, self.textureID); + } + + /// (Re-)Generates the GPU buffer. + pub fn generate(self: CubeMapTexture, width: u31, height: u31) void { + self.bind(); + + c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); + c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); + c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_POSITIVE_Y, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); + c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); + c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_POSITIVE_Z, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); + c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); + c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_MIN_FILTER, c.GL_LINEAR); + c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_MAG_FILTER, c.GL_LINEAR); + c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_WRAP_S, c.GL_CLAMP_TO_EDGE); + c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_WRAP_T, c.GL_CLAMP_TO_EDGE); + c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_WRAP_R, c.GL_CLAMP_TO_EDGE); + c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_BASE_LEVEL, 0); + c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_MAX_LEVEL, 0); + } + + pub fn faceNormal(face: usize) Vec3f { + const normals = [_]Vec3f{ + .{ 1, 0, 0 }, // +x + .{ -1, 0, 0 }, // -x + .{ 0, 1, 0 }, // +y + .{ 0, -1, 0 }, // -y + .{ 0, 0, 1 }, // +z + .{ 0, 0, -1 }, // -z + }; + return normals[face]; + } + + pub fn faceUp(face: usize) Vec3f { + const ups = [_]Vec3f{ + .{ 0, -1, 0 }, // +x + .{ 0, -1, 0 }, // -x + .{ 0, 0, 1 }, // +y + .{ 0, 0, -1 }, // -y + .{ 0, -1, 0 }, // +z + .{ 0, -1, 0 }, // -z + }; + return ups[face]; + } + + pub fn faceRight(face: usize) Vec3f { + comptime var rights: [6]Vec3f = undefined; + inline for (0..6) |i| { + rights[i] = comptime vec.cross(faceNormal(i), faceUp(i)); + } + return rights[face]; + } + + pub fn bindToFramebuffer(self: CubeMapTexture, fb: FrameBuffer, face: c_uint) void { + fb.bind(); + c.glFramebufferTexture2D(c.GL_FRAMEBUFFER, c.GL_COLOR_ATTACHMENT0, @as(c_uint, c.GL_TEXTURE_CUBE_MAP_POSITIVE_X) + face, self.textureID, 0); + } }; pub const Color = extern struct { // MARK: Color - r: u8, - g: u8, - b: u8, - a: u8, - - pub fn toARBG(self: Color) u32 { - return @as(u32, self.a) << 24 | @as(u32, self.r) << 16 | @as(u32, self.g) << 8 | @as(u32, self.b); - } + r: u8, + g: u8, + b: u8, + a: u8, + + pub fn toARBG(self: Color) u32 { + return @as(u32, self.a) << 24 | @as(u32, self.r) << 16 | @as(u32, self.g) << 8 | @as(u32, self.b); + } }; pub const Image = struct { // MARK: Image - var defaultImageData = [4]Color{ - Color{.r = 0, .g = 0, .b = 0, .a = 255}, - Color{.r = 255, .g = 0, .b = 255, .a = 255}, - Color{.r = 255, .g = 0, .b = 255, .a = 255}, - Color{.r = 0, .g = 0, .b = 0, .a = 255}, - }; - pub const defaultImage = Image{ - .width = 2, - .height = 2, - .imageData = &defaultImageData, - }; - var emptyImageData = [1]Color{ - Color{.r = 0, .g = 0, .b = 0, .a = 0}, - }; - pub const emptyImage = Image{ - .width = 1, - .height = 1, - .imageData = &emptyImageData, - }; - var whiteImageData = [1]Color{ - Color{.r = 255, .g = 255, .b = 255, .a = 255}, - }; - pub const whiteEmptyImage = Image{ - .width = 1, - .height = 1, - .imageData = &whiteImageData, - }; - width: u31, - height: u31, - imageData: []Color, - pub fn init(allocator: NeverFailingAllocator, width: u31, height: u31) Image { - return Image{ - .width = width, - .height = height, - .imageData = allocator.alloc(Color, width*height), - }; - } - pub fn deinit(self: Image, allocator: NeverFailingAllocator) void { - if (self.imageData.ptr == &defaultImageData or self.imageData.ptr == &emptyImageData or self.imageData.ptr == &whiteImageData) return; - allocator.free(self.imageData); - } - pub fn readFromFile(allocator: NeverFailingAllocator, path: []const u8) !Image { - var result: Image = undefined; - var channel: c_int = undefined; - const nullTerminatedPath = main.stackAllocator.dupeZ(u8, path); // TODO: Find a more zig-friendly image loading library. - errdefer main.stackAllocator.free(nullTerminatedPath); - c.stbi_set_flip_vertically_on_load(1); - const data = c.stbi_load(nullTerminatedPath.ptr, @ptrCast(&result.width), @ptrCast(&result.height), &channel, 4) orelse { - return error.FileNotFound; - }; - main.stackAllocator.free(nullTerminatedPath); - result.imageData = allocator.dupe(Color, @as([*]Color, @ptrCast(data))[0 .. result.width*result.height]); - c.stbi_image_free(data); - return result; - } - pub fn readUnflippedFromFile(allocator: NeverFailingAllocator, path: []const u8) !Image { - var result: Image = undefined; - var channel: c_int = undefined; - const nullTerminatedPath = main.stackAllocator.dupeZ(u8, path); // TODO: Find a more zig-friendly image loading library. - errdefer main.stackAllocator.free(nullTerminatedPath); - const data = c.stbi_load(nullTerminatedPath.ptr, @ptrCast(&result.width), @ptrCast(&result.height), &channel, 4) orelse { - return error.FileNotFound; - }; - main.stackAllocator.free(nullTerminatedPath); - result.imageData = allocator.dupe(Color, @as([*]Color, @ptrCast(data))[0 .. result.width*result.height]); - c.stbi_image_free(data); - return result; - } - pub fn exportToFile(self: Image, path: []const u8) !void { - const nullTerminated = main.stackAllocator.dupeZ(u8, path); - defer main.stackAllocator.free(nullTerminated); - _ = c.stbi_write_png(nullTerminated.ptr, self.width, self.height, 4, self.imageData.ptr, self.width*4); // TODO: Handle the return type. - } - pub fn getRGB(self: Image, x: usize, y: usize) Color { - std.debug.assert(x < self.width); - std.debug.assert(y < self.height); - const index = x + y*self.width; - return self.imageData[index]; - } - pub fn setRGB(self: Image, x: usize, y: usize, rgb: Color) void { - std.debug.assert(x < self.width); - std.debug.assert(y < self.height); - const index = x + y*self.width; - self.imageData[index] = rgb; - } + var defaultImageData = [4]Color{ + Color{ .r = 0, .g = 0, .b = 0, .a = 255 }, + Color{ .r = 255, .g = 0, .b = 255, .a = 255 }, + Color{ .r = 255, .g = 0, .b = 255, .a = 255 }, + Color{ .r = 0, .g = 0, .b = 0, .a = 255 }, + }; + pub const defaultImage = Image{ + .width = 2, + .height = 2, + .imageData = &defaultImageData, + }; + var emptyImageData = [1]Color{ + Color{ .r = 0, .g = 0, .b = 0, .a = 0 }, + }; + pub const emptyImage = Image{ + .width = 1, + .height = 1, + .imageData = &emptyImageData, + }; + var whiteImageData = [1]Color{ + Color{ .r = 255, .g = 255, .b = 255, .a = 255 }, + }; + pub const whiteEmptyImage = Image{ + .width = 1, + .height = 1, + .imageData = &whiteImageData, + }; + width: u31, + height: u31, + imageData: []Color, + pub fn init(allocator: NeverFailingAllocator, width: u31, height: u31) Image { + return Image{ + .width = width, + .height = height, + .imageData = allocator.alloc(Color, width * height), + }; + } + pub fn deinit(self: Image, allocator: NeverFailingAllocator) void { + if (self.imageData.ptr == &defaultImageData or self.imageData.ptr == &emptyImageData or self.imageData.ptr == &whiteImageData) return; + allocator.free(self.imageData); + } + pub fn readFromFile(allocator: NeverFailingAllocator, path: []const u8) !Image { + var result: Image = undefined; + var channel: c_int = undefined; + const nullTerminatedPath = main.stackAllocator.dupeZ(u8, path); // TODO: Find a more zig-friendly image loading library. + errdefer main.stackAllocator.free(nullTerminatedPath); + c.stbi_set_flip_vertically_on_load(1); + const data = c.stbi_load(nullTerminatedPath.ptr, @ptrCast(&result.width), @ptrCast(&result.height), &channel, 4) orelse { + return error.FileNotFound; + }; + main.stackAllocator.free(nullTerminatedPath); + result.imageData = allocator.dupe(Color, @as([*]Color, @ptrCast(data))[0 .. result.width * result.height]); + c.stbi_image_free(data); + return result; + } + pub fn readUnflippedFromFile(allocator: NeverFailingAllocator, path: []const u8) !Image { + var result: Image = undefined; + var channel: c_int = undefined; + const nullTerminatedPath = main.stackAllocator.dupeZ(u8, path); // TODO: Find a more zig-friendly image loading library. + errdefer main.stackAllocator.free(nullTerminatedPath); + const data = c.stbi_load(nullTerminatedPath.ptr, @ptrCast(&result.width), @ptrCast(&result.height), &channel, 4) orelse { + return error.FileNotFound; + }; + main.stackAllocator.free(nullTerminatedPath); + result.imageData = allocator.dupe(Color, @as([*]Color, @ptrCast(data))[0 .. result.width * result.height]); + c.stbi_image_free(data); + return result; + } + pub fn exportToFile(self: Image, path: []const u8) !void { + const nullTerminated = main.stackAllocator.dupeZ(u8, path); + defer main.stackAllocator.free(nullTerminated); + _ = c.stbi_write_png(nullTerminated.ptr, self.width, self.height, 4, self.imageData.ptr, self.width * 4); // TODO: Handle the return type. + } + pub fn getRGB(self: Image, x: usize, y: usize) Color { + std.debug.assert(x < self.width); + std.debug.assert(y < self.height); + const index = x + y * self.width; + return self.imageData[index]; + } + pub fn setRGB(self: Image, x: usize, y: usize, rgb: Color) void { + std.debug.assert(x < self.width); + std.debug.assert(y < self.height); + const index = x + y * self.width; + self.imageData[index] = rgb; + } }; pub const Fog = struct { // MARK: Fog - fogColor: Vec3f, - skyColor: Vec3f, - density: f32, - fogLower: f32, - fogHigher: f32, + fogColor: Vec3f, + skyColor: Vec3f, + density: f32, + fogLower: f32, + fogHigher: f32, }; const block_texture = struct { // MARK: block_texture - var uniforms: struct { - transparent: c_int, - } = undefined; - var pipeline: Pipeline = undefined; - var depthTexture: Texture = undefined; - const textureSize = 128; - - fn init() void { - pipeline = Pipeline.init( - "assets/cubyz/shaders/item_texture_post.vert", - "assets/cubyz/shaders/item_texture_post.frag", - "", - &uniforms, - VertexArray.EmptyVertex, - &.{}, - .{.cullMode = .none}, - .{.depthTest = false, .depthWrite = false}, - .{.attachments = &.{.noBlending}}, - ); - depthTexture = .init(); - depthTexture.bind(); - var data: [128*128]f32 = undefined; - - const z: f32 = 134; - const near = main.renderer.zNear; - const far = main.renderer.zFar; - const depth = ((far + near)/(near - far)*-z + 2*near*far/(near - far))/z*0.5 + 0.5; - - @memset(&data, depth); - c.glTexImage2D(c.GL_TEXTURE_2D, 0, c.GL_R32F, textureSize, textureSize, 0, c.GL_RED, c.GL_FLOAT, &data); - 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.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT); - c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT); - } - fn deinit() void { - pipeline.deinit(); - depthTexture.deinit(); - } + var uniforms: struct { + transparent: c_int, + } = undefined; + var pipeline: Pipeline = undefined; + var depthTexture: Texture = undefined; + const textureSize = 128; + + fn init() void { + pipeline = Pipeline.init( + "assets/cubyz/shaders/item_texture_post.vert", + "assets/cubyz/shaders/item_texture_post.frag", + "", + &uniforms, + VertexArray.EmptyVertex, + &.{}, + .{ .cullMode = .none }, + .{ .depthTest = false, .depthWrite = false }, + .{ .attachments = &.{.noBlending} }, + ); + depthTexture = .init(); + depthTexture.bind(); + var data: [128 * 128]f32 = undefined; + + const z: f32 = 134; + const near = main.renderer.zNear; + const far = main.renderer.zFar; + const depth = ((far + near) / (near - far) * -z + 2 * near * far / (near - far)) / z * 0.5 + 0.5; + + @memset(&data, depth); + c.glTexImage2D(c.GL_TEXTURE_2D, 0, c.GL_R32F, textureSize, textureSize, 0, c.GL_RED, c.GL_FLOAT, &data); + 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.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT); + } + fn deinit() void { + pipeline.deinit(); + depthTexture.deinit(); + } }; pub fn generateBlockTexture(blockType: u16) Texture { - const block = main.blocks.Block{.typ = blockType, .data = 0}; // TODO: Use natural standard data. - const textureSize = block_texture.textureSize; - c.glViewport(0, 0, textureSize, textureSize); - - var frameBuffer: FrameBuffer = undefined; - - frameBuffer.init(false, c.GL_NEAREST, c.GL_REPEAT); - defer frameBuffer.deinit(); - frameBuffer.updateSize(textureSize, textureSize, c.GL_RGBA16F); - frameBuffer.bind(); - if (block.transparent()) { - frameBuffer.clear(.{0.683421, 0.6854237, 0.685426, 1}); - } else { - frameBuffer.clear(.{0, 0, 0, 0}); - } - - const projMatrix = Mat4f.perspective(0.013, 1, 64, 256); - const oldViewMatrix = main.game.camera.viewMatrix; - main.game.camera.viewMatrix = Mat4f.identity().mul(Mat4f.rotationX(std.math.pi/4.0)).mul(Mat4f.rotationZ(1.0*std.math.pi/4.0)); - defer main.game.camera.viewMatrix = oldViewMatrix; - const uniforms = if (block.transparent()) &main.renderer.chunk_meshing.transparentUniforms else &main.renderer.chunk_meshing.uniforms; - - var faceData: main.ListUnmanaged(main.renderer.chunk_meshing.FaceData) = .{}; - defer faceData.deinit(main.stackAllocator); - const model = main.blocks.meshes.model(block).model(); - const pos: main.chunk.BlockPos = .fromCoords(1, 1, 1); - if (block.hasBackFace()) { - model.appendInternalQuadsToList(&faceData, main.stackAllocator, block, pos, true); - for (main.chunk.Neighbor.iterable) |neighbor| { - model.appendNeighborFacingQuadsToList(&faceData, main.stackAllocator, block, neighbor, pos, true); - } - } - model.appendInternalQuadsToList(&faceData, main.stackAllocator, block, pos, false); - for (main.chunk.Neighbor.iterable) |neighbor| { - model.appendNeighborFacingQuadsToList(&faceData, main.stackAllocator, block, neighbor, pos.neighbor(neighbor)[0], false); - } - - for (faceData.items) |*face| { - face.position.lightIndex = 0; - } - var allocation: SubAllocation = .{.start = 0, .len = 0}; - main.renderer.chunk_meshing.faceBuffers[0].uploadData(faceData.items, &allocation); - defer main.renderer.chunk_meshing.faceBuffers[0].free(allocation); - var lightAllocation: SubAllocation = .{.start = 0, .len = 0}; - main.renderer.chunk_meshing.lightBuffers[0].uploadData(&.{0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff}, &lightAllocation); - defer main.renderer.chunk_meshing.lightBuffers[0].free(lightAllocation); - - { - const i = 4; // Easily switch between the 8 diagonal coordinates. - var x: f64 = -65.5 + 1.5; - var y: f64 = -65.5 + 1.5; - var z: f64 = -92.631 + 1.5; - if (i & 1 != 0) x = -x + 3; - if (i & 2 != 0) y = -y + 3; - if (i & 4 != 0) z = -z + 3; - var chunkAllocation: SubAllocation = .{.start = 0, .len = 0}; - main.renderer.chunk_meshing.chunkBuffer.uploadData(&.{.{ - .position = .{0, 0, 0}, - .min = undefined, - .max = undefined, - .voxelSize = 1, - .lightStart = lightAllocation.start, - .vertexStartOpaque = undefined, - .faceCountsByNormalOpaque = undefined, - .vertexStartTransparent = undefined, - .vertexCountTransparent = undefined, - .visibilityState = 0, - .oldVisibilityState = 0, - }}, &chunkAllocation); - defer main.renderer.chunk_meshing.chunkBuffer.free(chunkAllocation); - if (block.transparent()) { - c.glBlendEquation(c.GL_FUNC_ADD); - c.glBlendFunc(c.GL_ONE, c.GL_SRC1_COLOR); - main.renderer.chunk_meshing.bindTransparentShaderAndUniforms(projMatrix, .{1, 1, 1}, .{x, y, z}); - } else { - main.renderer.chunk_meshing.bindShaderAndUniforms(projMatrix, .{1, 1, 1}, .{x, y, z}); - } - c.glUniform1f(uniforms.contrast, 0.25); - c.glActiveTexture(c.GL_TEXTURE0); - main.blocks.meshes.blockTextureArray.bind(); - c.glActiveTexture(c.GL_TEXTURE1); - main.blocks.meshes.emissionTextureArray.bind(); - c.glActiveTexture(c.GL_TEXTURE2); - main.blocks.meshes.reflectivityAndAbsorptionTextureArray.bind(); - block_texture.depthTexture.bindTo(5); - c.glDrawElementsInstancedBaseVertexBaseInstance(c.GL_TRIANGLES, @intCast(6*faceData.items.len), c.GL_UNSIGNED_INT, null, 1, allocation.start*4, chunkAllocation.start); - } - - c.glDisable(c.GL_CULL_FACE); - var finalFrameBuffer: FrameBuffer = undefined; - finalFrameBuffer.init(false, c.GL_NEAREST, c.GL_REPEAT); - finalFrameBuffer.updateSize(textureSize, textureSize, c.GL_RGBA8); - finalFrameBuffer.bind(); - const texture = Texture{.textureID = finalFrameBuffer.texture}; - defer c.glDeleteFramebuffers(1, &finalFrameBuffer.frameBuffer); - block_texture.pipeline.bind(null); - c.glUniform1i(block_texture.uniforms.transparent, if (block.transparent()) c.GL_TRUE else c.GL_FALSE); - frameBuffer.bindTexture(c.GL_TEXTURE3); - - draw.rectVao.bind(); - c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); - - c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0); - - c.glViewport(0, 0, main.Window.width, main.Window.height); - c.glBlendFunc(c.GL_SRC_ALPHA, c.GL_ONE_MINUS_SRC_ALPHA); - return texture; + const block = main.blocks.Block{ .typ = blockType, .data = 0 }; // TODO: Use natural standard data. + const textureSize = block_texture.textureSize; + c.glViewport(0, 0, textureSize, textureSize); + + var frameBuffer: FrameBuffer = undefined; + + frameBuffer.init(false, c.GL_NEAREST, c.GL_REPEAT); + defer frameBuffer.deinit(); + frameBuffer.updateSize(textureSize, textureSize, c.GL_RGBA16F); + frameBuffer.bind(); + if (block.transparent()) { + frameBuffer.clear(.{ 0.683421, 0.6854237, 0.685426, 1 }); + } else { + frameBuffer.clear(.{ 0, 0, 0, 0 }); + } + + const projMatrix = Mat4f.perspective(0.013, 1, 64, 256); + const oldViewMatrix = main.game.camera.viewMatrix; + main.game.camera.viewMatrix = Mat4f.identity().mul(Mat4f.rotationX(std.math.pi / 4.0)).mul(Mat4f.rotationZ(1.0 * std.math.pi / 4.0)); + defer main.game.camera.viewMatrix = oldViewMatrix; + const uniforms = if (block.transparent()) &main.renderer.chunk_meshing.transparentUniforms else &main.renderer.chunk_meshing.uniforms; + + var faceData: main.ListUnmanaged(main.renderer.chunk_meshing.FaceData) = .{}; + defer faceData.deinit(main.stackAllocator); + const model = main.blocks.meshes.model(block).model(); + const pos: main.chunk.BlockPos = .fromCoords(1, 1, 1); + if (block.hasBackFace()) { + model.appendInternalQuadsToList(&faceData, main.stackAllocator, block, pos, true); + for (main.chunk.Neighbor.iterable) |neighbor| { + model.appendNeighborFacingQuadsToList(&faceData, main.stackAllocator, block, neighbor, pos, true); + } + } + model.appendInternalQuadsToList(&faceData, main.stackAllocator, block, pos, false); + for (main.chunk.Neighbor.iterable) |neighbor| { + model.appendNeighborFacingQuadsToList(&faceData, main.stackAllocator, block, neighbor, pos.neighbor(neighbor)[0], false); + } + + for (faceData.items) |*face| { + face.position.lightIndex = 0; + } + var allocation: SubAllocation = .{ .start = 0, .len = 0 }; + main.renderer.chunk_meshing.faceBuffers[0].uploadData(faceData.items, &allocation); + defer main.renderer.chunk_meshing.faceBuffers[0].free(allocation); + var lightAllocation: SubAllocation = .{ .start = 0, .len = 0 }; + main.renderer.chunk_meshing.lightBuffers[0].uploadData(&.{ 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff }, &lightAllocation); + defer main.renderer.chunk_meshing.lightBuffers[0].free(lightAllocation); + + { + const i = 4; // Easily switch between the 8 diagonal coordinates. + var x: f64 = -65.5 + 1.5; + var y: f64 = -65.5 + 1.5; + var z: f64 = -92.631 + 1.5; + if (i & 1 != 0) x = -x + 3; + if (i & 2 != 0) y = -y + 3; + if (i & 4 != 0) z = -z + 3; + var chunkAllocation: SubAllocation = .{ .start = 0, .len = 0 }; + main.renderer.chunk_meshing.chunkBuffer.uploadData(&.{.{ + .position = .{ 0, 0, 0 }, + .min = undefined, + .max = undefined, + .voxelSize = 1, + .lightStart = lightAllocation.start, + .vertexStartOpaque = undefined, + .faceCountsByNormalOpaque = undefined, + .vertexStartTransparent = undefined, + .vertexCountTransparent = undefined, + .visibilityState = 0, + .oldVisibilityState = 0, + }}, &chunkAllocation); + defer main.renderer.chunk_meshing.chunkBuffer.free(chunkAllocation); + if (block.transparent()) { + c.glBlendEquation(c.GL_FUNC_ADD); + c.glBlendFunc(c.GL_ONE, c.GL_SRC1_COLOR); + main.renderer.chunk_meshing.bindTransparentShaderAndUniforms(projMatrix, .{ 1, 1, 1 }, .{ x, y, z }); + } else { + main.renderer.chunk_meshing.bindShaderAndUniforms(projMatrix, .{ 1, 1, 1 }, .{ x, y, z }); + } + c.glUniform1f(uniforms.contrast, 0.25); + c.glActiveTexture(c.GL_TEXTURE0); + main.blocks.meshes.blockTextureArray.bind(); + c.glActiveTexture(c.GL_TEXTURE1); + main.blocks.meshes.emissionTextureArray.bind(); + c.glActiveTexture(c.GL_TEXTURE2); + main.blocks.meshes.reflectivityAndAbsorptionTextureArray.bind(); + block_texture.depthTexture.bindTo(5); + c.glDrawElementsInstancedBaseVertexBaseInstance(c.GL_TRIANGLES, @intCast(6 * faceData.items.len), c.GL_UNSIGNED_INT, null, 1, allocation.start * 4, chunkAllocation.start); + } + + c.glDisable(c.GL_CULL_FACE); + var finalFrameBuffer: FrameBuffer = undefined; + finalFrameBuffer.init(false, c.GL_NEAREST, c.GL_REPEAT); + finalFrameBuffer.updateSize(textureSize, textureSize, c.GL_RGBA8); + finalFrameBuffer.bind(); + const texture = Texture{ .textureID = finalFrameBuffer.texture }; + defer c.glDeleteFramebuffers(1, &finalFrameBuffer.frameBuffer); + block_texture.pipeline.bind(null); + c.glUniform1i(block_texture.uniforms.transparent, if (block.transparent()) c.GL_TRUE else c.GL_FALSE); + frameBuffer.bindTexture(c.GL_TEXTURE3); + + draw.rectVao.bind(); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + + c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0); + + c.glViewport(0, 0, main.Window.width, main.Window.height); + c.glBlendFunc(c.GL_SRC_ALPHA, c.GL_ONE_MINUS_SRC_ALPHA); + return texture; } diff --git a/src/gui/windows/controls.zig b/src/gui/windows/controls.zig index 53c9245723..d283110a00 100644 --- a/src/gui/windows/controls.zig +++ b/src/gui/windows/controls.zig @@ -3,12 +3,12 @@ const std = @import("std"); const main = @import("main"); const Vec2f = main.vec.Vec2f; +const c = @import("c"); + const gui = @import("../gui.zig"); const GuiComponent = gui.GuiComponent; const GuiWindow = gui.GuiWindow; -const c = @import("c"); - const Button = @import("../components/Button.zig"); const CheckBox = @import("../components/CheckBox.zig"); const HorizontalList = @import("../components/HorizontalList.zig"); @@ -17,8 +17,8 @@ const VerticalList = @import("../components/VerticalList.zig"); const ContinuousSlider = @import("../components/ContinuousSlider.zig"); pub var window = GuiWindow{ - .contentSize = Vec2f{128, 192}, - .closeIfMouseIsGrabbed = true, + .contentSize = Vec2f{ 128, 192 }, + .closeIfMouseIsGrabbed = true, }; const padding: f32 = 8; @@ -26,121 +26,121 @@ var selectedKey: ?*main.Window.Key = null; var editingKeyboard: bool = true; var needsUpdate: bool = false; fn keyFunction(key: *main.Window.Key) void { - main.Window.setNextKeypressListener(&keypressListener) catch return; - selectedKey = key; - needsUpdate = true; + main.Window.setNextKeypressListener(&keypressListener) catch return; + selectedKey = key; + needsUpdate = true; } fn keypressListener(key: c_int, mouseButton: c_int, scancode: c_int) void { - selectedKey.?.key = key; - selectedKey.?.mouseButton = mouseButton; - selectedKey.?.scancode = scancode; - selectedKey = null; - needsUpdate = true; - main.settings.save(); + selectedKey.?.key = key; + selectedKey.?.mouseButton = mouseButton; + selectedKey.?.scancode = scancode; + selectedKey = null; + needsUpdate = true; + main.settings.save(); } fn gamepadFunction(key: *main.Window.Key) void { - main.Window.setNextGamepadListener(&gamepadListener) catch return; - selectedKey = key; - needsUpdate = true; + main.Window.setNextGamepadListener(&gamepadListener) catch return; + selectedKey = key; + needsUpdate = true; } fn gamepadListener(axis: ?main.Window.GamepadAxis, btn: c_int) void { - selectedKey.?.gamepadAxis = axis; - selectedKey.?.gamepadButton = btn; - selectedKey = null; - needsUpdate = true; - main.settings.save(); + selectedKey.?.gamepadAxis = axis; + selectedKey.?.gamepadButton = btn; + selectedKey = null; + needsUpdate = true; + main.settings.save(); } fn updateSensitivity(sensitivity: f32) void { - if (editingKeyboard) { - main.settings.mouseSensitivity = sensitivity; - } else { - main.settings.controllerSensitivity = sensitivity; - } - main.settings.save(); + if (editingKeyboard) { + main.settings.mouseSensitivity = sensitivity; + } else { + main.settings.controllerSensitivity = sensitivity; + } + main.settings.save(); } fn invertMouseYCallback(newValue: bool) void { - main.settings.invertMouseY = newValue; - main.settings.save(); + main.settings.invertMouseY = newValue; + main.settings.save(); } fn sprintIsToggleCallback(newValue: bool) void { - main.KeyBoard.setIsToggling("sprint", newValue); - main.settings.save(); + main.KeyBoard.setIsToggling("sprint", newValue); + main.settings.save(); } fn updateDeadzone(deadzone: f32) void { - main.settings.controllerAxisDeadzone = deadzone; + main.settings.controllerAxisDeadzone = deadzone; } fn deadzoneFormatter(allocator: main.heap.NeverFailingAllocator, value: f32) []const u8 { - return std.fmt.allocPrint(allocator.allocator, "Deadzone: {d:.0}%", .{value*100}) catch unreachable; + return std.fmt.allocPrint(allocator.allocator, "Deadzone: {d:.0}%", .{value * 100}) catch unreachable; } fn sensitivityFormatter(allocator: main.heap.NeverFailingAllocator, value: f32) []const u8 { - return std.fmt.allocPrint(allocator.allocator, "{s} Sensitivity: {d:.0}%", .{if (editingKeyboard) "Mouse" else "Controller", value*100}) catch unreachable; + return std.fmt.allocPrint(allocator.allocator, "{s} Sensitivity: {d:.0}%", .{ if (editingKeyboard) "Mouse" else "Controller", value * 100 }) catch unreachable; } fn toggleKeyboard() void { - editingKeyboard = !editingKeyboard; - needsUpdate = true; + editingKeyboard = !editingKeyboard; + needsUpdate = true; } fn unbindKey(keyPtr: usize) void { - var key: ?*main.Window.Key = @ptrFromInt(keyPtr); - if (editingKeyboard) { - key.?.key = c.GLFW_KEY_UNKNOWN; - key.?.mouseButton = -1; - key.?.scancode = 0; - } else { - key.?.gamepadAxis = null; - key.?.gamepadButton = -1; - } - needsUpdate = true; + var key: ?*main.Window.Key = @ptrFromInt(keyPtr); + if (editingKeyboard) { + key.?.key = c.GLFW_KEY_UNKNOWN; + key.?.mouseButton = -1; + key.?.scancode = 0; + } else { + key.?.gamepadAxis = null; + key.?.gamepadButton = -1; + } + needsUpdate = true; } pub fn onOpen() void { - const controlsListWidth: u32 = 256; - const keybindButtonWidth: u32 = 160; - const unbindButtonWidth: u32 = 64; - - const list = VerticalList.init(.{padding, 16 + padding}, 364, 8); - list.add(Button.initText(.{0, 0}, keybindButtonWidth, if (editingKeyboard) "Gamepad" else "Keyboard", .init(toggleKeyboard))); - list.add(ContinuousSlider.init(.{0, 0}, controlsListWidth, 0, 5, if (editingKeyboard) main.settings.mouseSensitivity else main.settings.controllerSensitivity, &updateSensitivity, &sensitivityFormatter)); - list.add(CheckBox.init(.{0, 0}, controlsListWidth, "Invert mouse Y", main.settings.invertMouseY, &invertMouseYCallback)); - list.add(CheckBox.init(.{0, 0}, controlsListWidth, "Toggle sprint", main.KeyBoard.key("sprint").isToggling == .yes, &sprintIsToggleCallback)); - - if (!editingKeyboard) { - list.add(ContinuousSlider.init(.{0, 0}, controlsListWidth, 0, 1, main.settings.controllerAxisDeadzone, &updateDeadzone, &deadzoneFormatter)); - } - for (&main.KeyBoard.keys) |*key| { - const label = Label.init(.{0, 0}, keybindButtonWidth, key.name, .left); - const button = if (key == selectedKey) (Button.initText(.{16, 0}, keybindButtonWidth, "...", .{})) else (Button.initText(.{16, 0}, keybindButtonWidth, if (editingKeyboard) key.getName() else key.getGamepadName(), if (editingKeyboard) .initWithPtr(keyFunction, key) else .initWithPtr(gamepadFunction, key))); - const unbindBtn = Button.initText(.{16, 0}, unbindButtonWidth, "Unbind", .initWithPtr(unbindKey, key)); - const row = HorizontalList.init(); - row.add(label); - row.add(button); - row.add(unbindBtn); - row.finish(.{0, 0}, .center); - list.add(row); - } - list.finish(.center); - window.rootComponent = list.toComponent(); - window.contentSize = window.rootComponent.?.pos() + window.rootComponent.?.size() + @as(Vec2f, @splat(padding)); - gui.updateWindowPositions(); + const controlsListWidth: u32 = 256; + const keybindButtonWidth: u32 = 160; + const unbindButtonWidth: u32 = 64; + + const list = VerticalList.init(.{ padding, 16 + padding }, 364, 8); + list.add(Button.initText(.{ 0, 0 }, keybindButtonWidth, if (editingKeyboard) "Gamepad" else "Keyboard", .init(toggleKeyboard))); + list.add(ContinuousSlider.init(.{ 0, 0 }, controlsListWidth, 0, 5, if (editingKeyboard) main.settings.mouseSensitivity else main.settings.controllerSensitivity, &updateSensitivity, &sensitivityFormatter)); + list.add(CheckBox.init(.{ 0, 0 }, controlsListWidth, "Invert mouse Y", main.settings.invertMouseY, &invertMouseYCallback)); + list.add(CheckBox.init(.{ 0, 0 }, controlsListWidth, "Toggle sprint", main.KeyBoard.key("sprint").isToggling == .yes, &sprintIsToggleCallback)); + + if (!editingKeyboard) { + list.add(ContinuousSlider.init(.{ 0, 0 }, controlsListWidth, 0, 1, main.settings.controllerAxisDeadzone, &updateDeadzone, &deadzoneFormatter)); + } + for (&main.KeyBoard.keys) |*key| { + const label = Label.init(.{ 0, 0 }, keybindButtonWidth, key.name, .left); + const button = if (key == selectedKey) (Button.initText(.{ 16, 0 }, keybindButtonWidth, "...", .{})) else (Button.initText(.{ 16, 0 }, keybindButtonWidth, if (editingKeyboard) key.getName() else key.getGamepadName(), if (editingKeyboard) .initWithPtr(keyFunction, key) else .initWithPtr(gamepadFunction, key))); + const unbindBtn = Button.initText(.{ 16, 0 }, unbindButtonWidth, "Unbind", .initWithPtr(unbindKey, key)); + const row = HorizontalList.init(); + row.add(label); + row.add(button); + row.add(unbindBtn); + row.finish(.{ 0, 0 }, .center); + list.add(row); + } + list.finish(.center); + window.rootComponent = list.toComponent(); + window.contentSize = window.rootComponent.?.pos() + window.rootComponent.?.size() + @as(Vec2f, @splat(padding)); + gui.updateWindowPositions(); } pub fn onClose() void { - if (window.rootComponent) |*comp| { - comp.deinit(); - } + if (window.rootComponent) |*comp| { + comp.deinit(); + } } pub fn render() void { - if (needsUpdate) { - needsUpdate = false; - const oldScroll = window.rootComponent.?.verticalList.scrollBar.currentState; - onClose(); - onOpen(); - window.rootComponent.?.verticalList.scrollBar.currentState = oldScroll; - } + if (needsUpdate) { + needsUpdate = false; + const oldScroll = window.rootComponent.?.verticalList.scrollBar.currentState; + onClose(); + onOpen(); + window.rootComponent.?.verticalList.scrollBar.currentState = oldScroll; + } } diff --git a/src/gui/windows/crosshair.zig b/src/gui/windows/crosshair.zig index 5eb1482627..9360b9af83 100644 --- a/src/gui/windows/crosshair.zig +++ b/src/gui/windows/crosshair.zig @@ -5,63 +5,63 @@ const graphics = main.graphics; const Texture = graphics.Texture; const Vec2f = main.vec.Vec2f; +const c = @import("c"); + const gui = @import("../gui.zig"); const GuiWindow = gui.GuiWindow; const GuiComponent = gui.GuiComponent; -const c = @import("c"); - const size: f32 = 64; pub var window = GuiWindow{ - .contentSize = Vec2f{size, size}, - .showTitleBar = false, - .hasBackground = false, - .isHud = true, - .hideIfMouseIsGrabbed = false, - .closeable = false, + .contentSize = Vec2f{ size, size }, + .showTitleBar = false, + .hasBackground = false, + .isHud = true, + .hideIfMouseIsGrabbed = false, + .closeable = false, }; var texture: Texture = undefined; var pipeline: graphics.Pipeline = undefined; var uniforms: struct { - screen: c_int, - start: c_int, - size: c_int, - color: c_int, - uvOffset: c_int, - uvDim: c_int, + screen: c_int, + start: c_int, + size: c_int, + color: c_int, + uvOffset: c_int, + uvDim: c_int, } = undefined; pub fn init() void { - pipeline = graphics.Pipeline.init( - "assets/cubyz/shaders/graphics/Image.vert", - "assets/cubyz/shaders/graphics/Image.frag", - "", - &uniforms, - graphics.draw.SimpleVertex2D, - &.{}, - .{.cullMode = .none}, - .{.depthTest = false, .depthWrite = false}, - .{.attachments = &.{.{ - .srcColorBlendFactor = .one, - .dstColorBlendFactor = .one, - .colorBlendOp = .subtract, - .srcAlphaBlendFactor = .one, - .dstAlphaBlendFactor = .one, - .alphaBlendOp = .subtract, - }}}, - ); - texture = Texture.initFromFile("assets/cubyz/ui/hud/crosshair.png"); + pipeline = graphics.Pipeline.init( + "assets/cubyz/shaders/graphics/Image.vert", + "assets/cubyz/shaders/graphics/Image.frag", + "", + &uniforms, + graphics.draw.SimpleVertex2D, + &.{}, + .{ .cullMode = .none }, + .{ .depthTest = false, .depthWrite = false }, + .{ .attachments = &.{.{ + .srcColorBlendFactor = .one, + .dstColorBlendFactor = .one, + .colorBlendOp = .subtract, + .srcAlphaBlendFactor = .one, + .dstAlphaBlendFactor = .one, + .alphaBlendOp = .subtract, + }} }, + ); + texture = Texture.initFromFile("assets/cubyz/ui/hud/crosshair.png"); } pub fn deinit() void { - pipeline.deinit(); - texture.deinit(); + pipeline.deinit(); + texture.deinit(); } pub fn render() void { - texture.bindTo(0); - graphics.draw.setColor(0xffffffff); - pipeline.bind(graphics.draw.getScissor()); - graphics.draw.customShadedImage(&uniforms, .{0, 0}, .{size, size}); + texture.bindTo(0); + graphics.draw.setColor(0xffffffff); + pipeline.bind(graphics.draw.getScissor()); + graphics.draw.customShadedImage(&uniforms, .{ 0, 0 }, .{ size, size }); } diff --git a/src/gui/windows/main.zig b/src/gui/windows/main.zig index e62f2c3681..ee4d745b6f 100644 --- a/src/gui/windows/main.zig +++ b/src/gui/windows/main.zig @@ -8,34 +8,33 @@ const c = @import("c"); const gui = @import("../gui.zig"); const GuiComponent = gui.GuiComponent; const GuiWindow = gui.GuiWindow; - const Button = @import("../components/Button.zig"); const VerticalList = @import("../components/VerticalList.zig"); pub var window = GuiWindow{ - .contentSize = Vec2f{128, 256}, - .closeable = false, + .contentSize = Vec2f{ 128, 256 }, + .closeable = false, }; const padding: f32 = 8; fn exitGame() void { - c.glfwSetWindowShouldClose(main.Window.window, c.GLFW_TRUE); + c.glfwSetWindowShouldClose(main.Window.window, c.GLFW_TRUE); } pub fn onOpen() void { - const list = VerticalList.init(.{padding, 16 + padding}, 300, 16); - list.add(Button.initText(.{0, 0}, 128, "Singleplayer", gui.openWindowCallback("save_selection"))); - list.add(Button.initText(.{0, 0}, 128, "Multiplayer", gui.openWindowCallback("multiplayer"))); - list.add(Button.initText(.{0, 0}, 128, "Settings", gui.openWindowCallback("settings"))); - list.add(Button.initText(.{0, 0}, 128, "Touch Grass", .init(exitGame))); - list.finish(.center); - window.rootComponent = list.toComponent(); - window.contentSize = window.rootComponent.?.pos() + window.rootComponent.?.size() + @as(Vec2f, @splat(padding)); - gui.updateWindowPositions(); + const list = VerticalList.init(.{ padding, 16 + padding }, 300, 16); + list.add(Button.initText(.{ 0, 0 }, 128, "Singleplayer", gui.openWindowCallback("save_selection"))); + list.add(Button.initText(.{ 0, 0 }, 128, "Multiplayer", gui.openWindowCallback("multiplayer"))); + list.add(Button.initText(.{ 0, 0 }, 128, "Settings", gui.openWindowCallback("settings"))); + list.add(Button.initText(.{ 0, 0 }, 128, "Touch Grass", .init(exitGame))); + list.finish(.center); + window.rootComponent = list.toComponent(); + window.contentSize = window.rootComponent.?.pos() + window.rootComponent.?.size() + @as(Vec2f, @splat(padding)); + gui.updateWindowPositions(); } pub fn onClose() void { - if (window.rootComponent) |*comp| { - comp.deinit(); - } + if (window.rootComponent) |*comp| { + comp.deinit(); + } } diff --git a/src/itemdrop.zig b/src/itemdrop.zig index e3b0941966..caf81156ba 100644 --- a/src/itemdrop.zig +++ b/src/itemdrop.zig @@ -1,31 +1,23 @@ const std = @import("std"); const blocks = @import("blocks.zig"); - const chunk_zig = @import("chunk.zig"); const ServerChunk = chunk_zig.ServerChunk; - const game = @import("game.zig"); const World = game.World; - const graphics = @import("graphics.zig"); - const items = @import("items.zig"); const ItemStack = items.ItemStack; - const ZonElement = @import("zon.zig").ZonElement; - const main = @import("main"); const physics = main.physics; const ServerWorld = main.server.ServerWorld; const BinaryReader = main.utils.BinaryReader; const BinaryWriter = main.utils.BinaryWriter; const NeverFailingAllocator = main.heap.NeverFailingAllocator; - const random = @import("random.zig"); const settings = @import("settings.zig"); const utils = @import("utils.zig"); - const vec = @import("vec.zig"); const Mat4f = vec.Mat4f; const Vec3d = vec.Vec3d; @@ -35,856 +27,856 @@ const Vec3i = vec.Vec3i; const c = @import("c"); const ItemDrop = struct { // MARK: ItemDrop - pos: Vec3d, - vel: Vec3d, - rot: Vec3f, - itemStack: ItemStack, - despawnTime: i32, - pickupCooldown: i32, - - reverseIndex: u16, + pos: Vec3d, + vel: Vec3d, + rot: Vec3f, + itemStack: ItemStack, + despawnTime: i32, + pickupCooldown: i32, + + reverseIndex: u16, }; pub const ItemDropNetworkData = struct { - index: u16, - pos: Vec3d, - vel: Vec3d, + index: u16, + pos: Vec3d, + vel: Vec3d, }; pub const ItemDropManager = struct { // MARK: ItemDropManager - /// Half the side length of all item entities hitboxes as a cube. - pub const radius: f64 = 0.1; - /// Side length of all item entities hitboxes as a cube. - pub const diameter: f64 = 2*radius; - - pub const pickupRange: f64 = 1.0; - - const terminalVelocity = 40.0; - const gravity = 9.81; - - const maxCapacity = 65536; - - allocator: NeverFailingAllocator, - - list: std.MultiArrayList(ItemDrop), - - indices: [maxCapacity]u16 = undefined, - - emptyMutex: main.utils.Mutex = .{}, - isEmpty: std.bit_set.ArrayBitSet(usize, maxCapacity), - - changeQueue: main.utils.ConcurrentQueue(union(enum) { add: struct { u16, ItemDrop }, remove: u16 }), - - world: ?*ServerWorld, - airDragFactor: f64, - - size: u32 = 0, - - pub fn init(self: *ItemDropManager, allocator: NeverFailingAllocator, world: ?*ServerWorld) void { - self.* = ItemDropManager{ - .allocator = allocator, - .list = std.MultiArrayList(ItemDrop){}, - .isEmpty = .initFull(), - .changeQueue = .init(allocator, 16), - .world = world, - .airDragFactor = gravity/terminalVelocity, - }; - self.list.resize(self.allocator.allocator, maxCapacity) catch unreachable; - } - - pub fn deinit(self: *ItemDropManager) void { - self.processChanges(); - self.changeQueue.deinit(); - for (self.indices[0..self.size]) |i| { - self.list.items(.itemStack)[i].item.deinit(); - } - self.list.deinit(self.allocator.allocator); - } - - pub fn loadFrom(self: *ItemDropManager, zon: ZonElement) void { - const zonArray = zon.getChild("array"); - for (zonArray.toSlice()) |elem| { - self.addFromZon(elem); - } - } - - pub fn loadFromBytes(self: *ItemDropManager, reader: *main.utils.BinaryReader) !void { - const version = try reader.readInt(u8); - if (version != 0) return error.UnsupportedVersion; - var i: u16 = 0; - while (reader.remaining.len != 0) : (i += 1) { - try self.addFromBytes(reader, i); - } - } - - pub fn storeToBytes(self: *ItemDropManager, writer: *main.utils.BinaryWriter) void { - const version = 0; - writer.writeInt(u8, version); - for (self.indices[0..self.size]) |i| { - storeSingleToBytes(writer, self.list.get(i)); - } - } - - fn addFromBytes(self: *ItemDropManager, reader: *main.utils.BinaryReader, i: u16) !void { - const despawnTime = try reader.readInt(i32); - const pos = try reader.readVec(Vec3d); - const vel = try reader.readVec(Vec3d); - const itemStack = try items.ItemStack.fromBytes(reader); - self.addWithIndex(i, pos, vel, random.nextFloatVector(3, &main.seed)*@as(Vec3f, @splat(2*std.math.pi)), itemStack, despawnTime, 0); - } - - fn storeSingleToBytes(writer: *main.utils.BinaryWriter, itemdrop: ItemDrop) void { - writer.writeInt(i32, itemdrop.despawnTime); - writer.writeVec(Vec3d, itemdrop.pos); - writer.writeVec(Vec3d, itemdrop.vel); - itemdrop.itemStack.toBytes(writer); - } - - fn addFromZon(self: *ItemDropManager, zon: ZonElement) void { - const item = items.Item.init(zon) catch |err| { - const msg = zon.toStringEfficient(main.stackAllocator, ""); - defer main.stackAllocator.free(msg); - std.log.err("Ignoring invalid item drop {s} which caused {s}", .{msg, @errorName(err)}); - return; - }; - const properties = .{ - zon.get(Vec3d, "pos", .{0, 0, 0}), - zon.get(Vec3d, "vel", .{0, 0, 0}), - random.nextFloatVector(3, &main.seed)*@as(Vec3f, @splat(2*std.math.pi)), - items.ItemStack{.item = item, .amount = zon.get(u16, "amount", 1)}, - zon.get(i32, "despawnTime", 60), - 0, - }; - if (zon.get(?u16, "i", null)) |i| { - @call(.auto, addWithIndex, .{self, i} ++ properties); - } else { - @call(.auto, add, .{self} ++ properties); - } - } - - pub fn getPositionAndVelocityData(self: *ItemDropManager, allocator: NeverFailingAllocator) []ItemDropNetworkData { - const result = allocator.alloc(ItemDropNetworkData, self.size); - for (self.indices[0..self.size], result) |i, *res| { - res.* = .{ - .index = i, - .pos = self.list.items(.pos)[i], - .vel = self.list.items(.vel)[i], - }; - } - return result; - } - - pub fn getInitialList(self: *ItemDropManager, allocator: NeverFailingAllocator) ZonElement { - self.processChanges(); // Make sure all the items from the queue are included. - var list = ZonElement.initArray(allocator); - var ii: u32 = 0; - while (ii < self.size) : (ii += 1) { - const i = self.indices[ii]; - list.array.append(self.storeSingle(allocator, i)); - } - return list; - } - - fn storeDrop(allocator: NeverFailingAllocator, itemDrop: ItemDrop, i: u16) ZonElement { - const obj = ZonElement.initObject(allocator); - obj.put("i", i); - obj.put("pos", itemDrop.pos); - obj.put("vel", itemDrop.vel); - itemDrop.itemStack.storeToZon(allocator, obj); - obj.put("despawnTime", itemDrop.despawnTime); - return obj; - } - - fn storeSingle(self: *ItemDropManager, allocator: NeverFailingAllocator, i: u16) ZonElement { - return storeDrop(allocator, self.list.get(i), i); - } - - pub fn store(self: *ItemDropManager, allocator: NeverFailingAllocator) ZonElement { - const zonArray = ZonElement.initArray(allocator); - for (self.indices[0..self.size]) |i| { - const item = self.storeSingle(allocator, i); - zonArray.array.append(item); - } - const zon = ZonElement.initObject(allocator); - zon.put("array", zonArray); - return zon; - } - - pub fn update(self: *ItemDropManager, deltaTime: f32) void { - std.debug.assert(self.world != null); - self.processChanges(); - const pos = self.list.items(.pos); - const vel = self.list.items(.vel); - const pickupCooldown = self.list.items(.pickupCooldown); - const despawnTime = self.list.items(.despawnTime); - var ii: u32 = 0; - while (ii < self.size) { - const i = self.indices[ii]; - if (self.world.?.getSimulationChunkAndIncreaseRefCount(@trunc(pos[i][0]), @trunc(pos[i][1]), @trunc(pos[i][2]))) |simChunk| { - defer simChunk.decreaseRefCount(); - if (simChunk.getChunk()) |chunk| { - // Check collision with blocks: - self.updateEnt(chunk, &pos[i], &vel[i], deltaTime); - } - } - pickupCooldown[i] -= 1; - despawnTime[i] -= 1; - if (despawnTime[i] < 0) { - self.directRemove(i); - } else { - ii += 1; - } - } - } - - pub fn add(self: *ItemDropManager, pos: Vec3d, vel: Vec3d, rot: Vec3f, itemStack: ItemStack, despawnTime: i32, pickupCooldown: i32) void { - self.emptyMutex.lock(); - const i: u16 = @intCast(self.isEmpty.findFirstSet() orelse { - self.emptyMutex.unlock(); - std.log.err("Item drop capacitiy limit reached. Failed to add itemStack: {}×{s}", .{itemStack.amount, itemStack.item.id() orelse return}); - itemStack.item.deinit(); - return; - }); - self.isEmpty.unset(i); - const drop = ItemDrop{ - .pos = pos, - .vel = vel, - .rot = rot, - .itemStack = itemStack, - .despawnTime = despawnTime, - .pickupCooldown = pickupCooldown, - .reverseIndex = undefined, - }; - if (self.world != null) { - const list = ZonElement.initArray(main.stackAllocator); - defer list.deinit(main.stackAllocator); - list.array.append(.null); - list.array.append(storeDrop(main.stackAllocator, drop, i)); - const updateData = list.toStringEfficient(main.stackAllocator, &.{}); - defer main.stackAllocator.free(updateData); - - const userList = main.server.getUserListAndIncreaseRefCount(main.stackAllocator); - defer main.server.freeUserListAndDecreaseRefCount(main.stackAllocator, userList); - for (userList) |user| { - main.network.protocols.entity.send(user.conn, updateData); - } - } - - self.emptyMutex.unlock(); - self.changeQueue.pushBack(.{.add = .{i, drop}}); - } - - fn addWithIndex(self: *ItemDropManager, i: u16, pos: Vec3d, vel: Vec3d, rot: Vec3f, itemStack: ItemStack, despawnTime: i32, pickupCooldown: i32) void { - self.emptyMutex.lock(); - std.debug.assert(self.isEmpty.isSet(i)); - self.isEmpty.unset(i); - const drop = ItemDrop{ - .pos = pos, - .vel = vel, - .rot = rot, - .itemStack = itemStack, - .despawnTime = despawnTime, - .pickupCooldown = pickupCooldown, - .reverseIndex = undefined, - }; - if (self.world != null) { - const list = ZonElement.initArray(main.stackAllocator); - defer list.deinit(main.stackAllocator); - list.array.append(.null); - list.array.append(storeDrop(main.stackAllocator, drop, i)); - const updateData = list.toStringEfficient(main.stackAllocator, &.{}); - defer main.stackAllocator.free(updateData); - - const userList = main.server.getUserListAndIncreaseRefCount(main.stackAllocator); - defer main.server.freeUserListAndDecreaseRefCount(main.stackAllocator, userList); - for (userList) |user| { - main.network.protocols.entity.send(user.conn, updateData); - } - } - - self.emptyMutex.unlock(); - self.changeQueue.pushBack(.{.add = .{i, drop}}); - } - - fn processChanges(self: *ItemDropManager) void { - while (self.changeQueue.popFront()) |data| { - switch (data) { - .add => |addData| { - self.internalAdd(addData[0], addData[1]); - }, - .remove => |index| { - self.internalRemove(index); - }, - } - } - } - - fn internalAdd(self: *ItemDropManager, i: u16, drop_: ItemDrop) void { - var drop = drop_; - if (self.world == null) { - ClientItemDropManager.clientSideInternalAdd(self, i, drop); - } - drop.reverseIndex = @intCast(self.size); - self.list.set(i, drop); - self.indices[self.size] = i; - self.size += 1; - } - - fn internalRemove(self: *ItemDropManager, i: u16) void { - self.size -= 1; - const ii = self.list.items(.reverseIndex)[i]; - self.list.items(.itemStack)[i].deinit(); - self.list.items(.itemStack)[i] = .{}; - self.indices[ii] = self.indices[self.size]; - self.list.items(.reverseIndex)[self.indices[self.size]] = ii; - } - - fn directRemove(self: *ItemDropManager, i: u16) void { - std.debug.assert(self.world != null); - self.emptyMutex.lock(); - self.isEmpty.set(i); - - const list = ZonElement.initArray(main.stackAllocator); - defer list.deinit(main.stackAllocator); - list.array.append(.null); - list.array.append(.{.int = i}); - const updateData = list.toStringEfficient(main.stackAllocator, &.{}); - defer main.stackAllocator.free(updateData); - - const userList = main.server.getUserListAndIncreaseRefCount(main.stackAllocator); - defer main.server.freeUserListAndDecreaseRefCount(main.stackAllocator, userList); - for (userList) |user| { - main.network.protocols.entity.send(user.conn, updateData); - } - - self.emptyMutex.unlock(); - self.internalRemove(i); - } - - fn updateEnt(self: *ItemDropManager, chunk: *ServerChunk, pos: *Vec3d, vel: *Vec3d, deltaTime: f64) void { - const hitBox = physics.collision.Box{.min = @splat(-radius), .max = @splat(radius)}; - if (physics.collision.collides(.server, .x, 0, pos.*, hitBox) != null) { - self.fixStuckInBlock(chunk, pos, vel, deltaTime); - return; - } - vel.* += Vec3d{0, 0, -gravity*deltaTime}; - inline for (0..3) |i| { - const move = vel.*[i]*deltaTime; // + acceleration[i]*deltaTime; - if (main.physics.collision.collides(.server, @enumFromInt(i), move, pos.*, hitBox)) |box| { - if (move < 0) { - pos.*[i] = box.max[i] + radius; - } else { - pos.*[i] = box.min[i] - radius; - } - vel.*[i] = 0; - } else { - pos.*[i] += move; - } - } - // Apply drag: - vel.* *= @splat(@max(0, 1 - self.airDragFactor*deltaTime)); - } - - fn fixStuckInBlock(self: *ItemDropManager, chunk: *ServerChunk, pos: *Vec3d, vel: *Vec3d, deltaTime: f64) void { - const centeredPos = pos.* - @as(Vec3d, @splat(0.5)); - const pos0: Vec3i = @floor(centeredPos); - - var closestEmptyBlock: Vec3i = @splat(-1); - var closestDist = std.math.floatMax(f64); - var delta = Vec3i{0, 0, 0}; - while (delta[0] <= 1) : (delta[0] += 1) { - delta[1] = 0; - while (delta[1] <= 1) : (delta[1] += 1) { - delta[2] = 0; - while (delta[2] <= 1) : (delta[2] += 1) { - const isSolid = self.checkBlock(chunk, pos, pos0 + delta); - if (!isSolid) { - const dist = vec.lengthSquare(@as(Vec3d, @floatFromInt(pos0 + delta)) - centeredPos); - if (dist < closestDist) { - closestDist = dist; - closestEmptyBlock = delta; - } - } - } - } - } - - vel.* = @splat(0); - const unstuckVelocity: f64 = 1; - if (closestDist == std.math.floatMax(f64)) { - // Surrounded by solid blocks → move upwards - vel.*[2] = unstuckVelocity; - pos.*[2] += vel.*[2]*deltaTime; - } else { - vel.* = @as(Vec3d, @splat(unstuckVelocity))*(@as(Vec3d, @floatFromInt(pos0 + closestEmptyBlock)) - centeredPos); - pos.* += (vel.*)*@as(Vec3d, @splat(deltaTime)); - } - } - - fn checkBlock(self: *ItemDropManager, chunk: *ServerChunk, pos: *Vec3d, blockPos: Vec3i) bool { - // Transform to chunk-relative coordinates: - const chunkPos = blockPos & ~@as(Vec3i, @splat(main.chunk.chunkMask)); - var block: blocks.Block = undefined; - if (chunk.super.pos.wx == chunkPos[0] and chunk.super.pos.wy == chunkPos[1] and chunk.super.pos.wz == chunkPos[2]) { - chunk.mutex.lock(); - defer chunk.mutex.unlock(); - block = chunk.getBlock(blockPos[0] - chunk.super.pos.wx, blockPos[1] - chunk.super.pos.wy, blockPos[2] - chunk.super.pos.wz); - } else { - const otherChunk = self.world.?.getSimulationChunkAndIncreaseRefCount(chunkPos[0], chunkPos[1], chunkPos[2]) orelse return true; - defer otherChunk.decreaseRefCount(); - const ch = otherChunk.getChunk() orelse return true; - ch.mutex.lock(); - defer ch.mutex.unlock(); - block = ch.getBlock(blockPos[0] - ch.super.pos.wx, blockPos[1] - ch.super.pos.wy, blockPos[2] - ch.super.pos.wz); - } - return main.physics.collision.collideWithBlock(block, blockPos[0], blockPos[1], blockPos[2], pos.*, @splat(radius), @splat(0)) != null; - } - - pub fn checkEntity(self: *ItemDropManager, user: *main.server.User) void { - var ii: u32 = 0; - while (ii < self.size) { - const i = self.indices[ii]; - if (self.list.items(.pickupCooldown)[i] > 0) { - ii += 1; - continue; - } - const hitbox = main.game.Player.outerBoundingBox; - const min = user.player().pos + hitbox.min; - const max = user.player().pos + hitbox.max; - const itemPos = self.list.items(.pos)[i]; - const dist = @max(min - itemPos, itemPos - max); - if (@reduce(.Max, dist) < radius + pickupRange) { - const itemStack = &self.list.items(.itemStack)[i]; - main.items.Inventory.ServerSide.tryCollectingToPlayerInventory(user, itemStack); - if (itemStack.amount == 0) { - self.directRemove(i); - continue; - } - } - ii += 1; - } - } + /// Half the side length of all item entities hitboxes as a cube. + pub const radius: f64 = 0.1; + /// Side length of all item entities hitboxes as a cube. + pub const diameter: f64 = 2 * radius; + + pub const pickupRange: f64 = 1.0; + + const terminalVelocity = 40.0; + const gravity = 9.81; + + const maxCapacity = 65536; + + allocator: NeverFailingAllocator, + + list: std.MultiArrayList(ItemDrop), + + indices: [maxCapacity]u16 = undefined, + + emptyMutex: main.utils.Mutex = .{}, + isEmpty: std.bit_set.ArrayBitSet(usize, maxCapacity), + + changeQueue: main.utils.ConcurrentQueue(union(enum) { add: struct { u16, ItemDrop }, remove: u16 }), + + world: ?*ServerWorld, + airDragFactor: f64, + + size: u32 = 0, + + pub fn init(self: *ItemDropManager, allocator: NeverFailingAllocator, world: ?*ServerWorld) void { + self.* = ItemDropManager{ + .allocator = allocator, + .list = std.MultiArrayList(ItemDrop){}, + .isEmpty = .initFull(), + .changeQueue = .init(allocator, 16), + .world = world, + .airDragFactor = gravity / terminalVelocity, + }; + self.list.resize(self.allocator.allocator, maxCapacity) catch unreachable; + } + + pub fn deinit(self: *ItemDropManager) void { + self.processChanges(); + self.changeQueue.deinit(); + for (self.indices[0..self.size]) |i| { + self.list.items(.itemStack)[i].item.deinit(); + } + self.list.deinit(self.allocator.allocator); + } + + pub fn loadFrom(self: *ItemDropManager, zon: ZonElement) void { + const zonArray = zon.getChild("array"); + for (zonArray.toSlice()) |elem| { + self.addFromZon(elem); + } + } + + pub fn loadFromBytes(self: *ItemDropManager, reader: *main.utils.BinaryReader) !void { + const version = try reader.readInt(u8); + if (version != 0) return error.UnsupportedVersion; + var i: u16 = 0; + while (reader.remaining.len != 0) : (i += 1) { + try self.addFromBytes(reader, i); + } + } + + pub fn storeToBytes(self: *ItemDropManager, writer: *main.utils.BinaryWriter) void { + const version = 0; + writer.writeInt(u8, version); + for (self.indices[0..self.size]) |i| { + storeSingleToBytes(writer, self.list.get(i)); + } + } + + fn addFromBytes(self: *ItemDropManager, reader: *main.utils.BinaryReader, i: u16) !void { + const despawnTime = try reader.readInt(i32); + const pos = try reader.readVec(Vec3d); + const vel = try reader.readVec(Vec3d); + const itemStack = try items.ItemStack.fromBytes(reader); + self.addWithIndex(i, pos, vel, random.nextFloatVector(3, &main.seed) * @as(Vec3f, @splat(2 * std.math.pi)), itemStack, despawnTime, 0); + } + + fn storeSingleToBytes(writer: *main.utils.BinaryWriter, itemdrop: ItemDrop) void { + writer.writeInt(i32, itemdrop.despawnTime); + writer.writeVec(Vec3d, itemdrop.pos); + writer.writeVec(Vec3d, itemdrop.vel); + itemdrop.itemStack.toBytes(writer); + } + + fn addFromZon(self: *ItemDropManager, zon: ZonElement) void { + const item = items.Item.init(zon) catch |err| { + const msg = zon.toStringEfficient(main.stackAllocator, ""); + defer main.stackAllocator.free(msg); + std.log.err("Ignoring invalid item drop {s} which caused {s}", .{ msg, @errorName(err) }); + return; + }; + const properties = .{ + zon.get(Vec3d, "pos", .{ 0, 0, 0 }), + zon.get(Vec3d, "vel", .{ 0, 0, 0 }), + random.nextFloatVector(3, &main.seed) * @as(Vec3f, @splat(2 * std.math.pi)), + items.ItemStack{ .item = item, .amount = zon.get(u16, "amount", 1) }, + zon.get(i32, "despawnTime", 60), + 0, + }; + if (zon.get(?u16, "i", null)) |i| { + @call(.auto, addWithIndex, .{ self, i } ++ properties); + } else { + @call(.auto, add, .{self} ++ properties); + } + } + + pub fn getPositionAndVelocityData(self: *ItemDropManager, allocator: NeverFailingAllocator) []ItemDropNetworkData { + const result = allocator.alloc(ItemDropNetworkData, self.size); + for (self.indices[0..self.size], result) |i, *res| { + res.* = .{ + .index = i, + .pos = self.list.items(.pos)[i], + .vel = self.list.items(.vel)[i], + }; + } + return result; + } + + pub fn getInitialList(self: *ItemDropManager, allocator: NeverFailingAllocator) ZonElement { + self.processChanges(); // Make sure all the items from the queue are included. + var list = ZonElement.initArray(allocator); + var ii: u32 = 0; + while (ii < self.size) : (ii += 1) { + const i = self.indices[ii]; + list.array.append(self.storeSingle(allocator, i)); + } + return list; + } + + fn storeDrop(allocator: NeverFailingAllocator, itemDrop: ItemDrop, i: u16) ZonElement { + const obj = ZonElement.initObject(allocator); + obj.put("i", i); + obj.put("pos", itemDrop.pos); + obj.put("vel", itemDrop.vel); + itemDrop.itemStack.storeToZon(allocator, obj); + obj.put("despawnTime", itemDrop.despawnTime); + return obj; + } + + fn storeSingle(self: *ItemDropManager, allocator: NeverFailingAllocator, i: u16) ZonElement { + return storeDrop(allocator, self.list.get(i), i); + } + + pub fn store(self: *ItemDropManager, allocator: NeverFailingAllocator) ZonElement { + const zonArray = ZonElement.initArray(allocator); + for (self.indices[0..self.size]) |i| { + const item = self.storeSingle(allocator, i); + zonArray.array.append(item); + } + const zon = ZonElement.initObject(allocator); + zon.put("array", zonArray); + return zon; + } + + pub fn update(self: *ItemDropManager, deltaTime: f32) void { + std.debug.assert(self.world != null); + self.processChanges(); + const pos = self.list.items(.pos); + const vel = self.list.items(.vel); + const pickupCooldown = self.list.items(.pickupCooldown); + const despawnTime = self.list.items(.despawnTime); + var ii: u32 = 0; + while (ii < self.size) { + const i = self.indices[ii]; + if (self.world.?.getSimulationChunkAndIncreaseRefCount(@trunc(pos[i][0]), @trunc(pos[i][1]), @trunc(pos[i][2]))) |simChunk| { + defer simChunk.decreaseRefCount(); + if (simChunk.getChunk()) |chunk| { + // Check collision with blocks: + self.updateEnt(chunk, &pos[i], &vel[i], deltaTime); + } + } + pickupCooldown[i] -= 1; + despawnTime[i] -= 1; + if (despawnTime[i] < 0) { + self.directRemove(i); + } else { + ii += 1; + } + } + } + + pub fn add(self: *ItemDropManager, pos: Vec3d, vel: Vec3d, rot: Vec3f, itemStack: ItemStack, despawnTime: i32, pickupCooldown: i32) void { + self.emptyMutex.lock(); + const i: u16 = @intCast(self.isEmpty.findFirstSet() orelse { + self.emptyMutex.unlock(); + std.log.err("Item drop capacitiy limit reached. Failed to add itemStack: {}×{s}", .{ itemStack.amount, itemStack.item.id() orelse return }); + itemStack.item.deinit(); + return; + }); + self.isEmpty.unset(i); + const drop = ItemDrop{ + .pos = pos, + .vel = vel, + .rot = rot, + .itemStack = itemStack, + .despawnTime = despawnTime, + .pickupCooldown = pickupCooldown, + .reverseIndex = undefined, + }; + if (self.world != null) { + const list = ZonElement.initArray(main.stackAllocator); + defer list.deinit(main.stackAllocator); + list.array.append(.null); + list.array.append(storeDrop(main.stackAllocator, drop, i)); + const updateData = list.toStringEfficient(main.stackAllocator, &.{}); + defer main.stackAllocator.free(updateData); + + const userList = main.server.getUserListAndIncreaseRefCount(main.stackAllocator); + defer main.server.freeUserListAndDecreaseRefCount(main.stackAllocator, userList); + for (userList) |user| { + main.network.protocols.entity.send(user.conn, updateData); + } + } + + self.emptyMutex.unlock(); + self.changeQueue.pushBack(.{ .add = .{ i, drop } }); + } + + fn addWithIndex(self: *ItemDropManager, i: u16, pos: Vec3d, vel: Vec3d, rot: Vec3f, itemStack: ItemStack, despawnTime: i32, pickupCooldown: i32) void { + self.emptyMutex.lock(); + std.debug.assert(self.isEmpty.isSet(i)); + self.isEmpty.unset(i); + const drop = ItemDrop{ + .pos = pos, + .vel = vel, + .rot = rot, + .itemStack = itemStack, + .despawnTime = despawnTime, + .pickupCooldown = pickupCooldown, + .reverseIndex = undefined, + }; + if (self.world != null) { + const list = ZonElement.initArray(main.stackAllocator); + defer list.deinit(main.stackAllocator); + list.array.append(.null); + list.array.append(storeDrop(main.stackAllocator, drop, i)); + const updateData = list.toStringEfficient(main.stackAllocator, &.{}); + defer main.stackAllocator.free(updateData); + + const userList = main.server.getUserListAndIncreaseRefCount(main.stackAllocator); + defer main.server.freeUserListAndDecreaseRefCount(main.stackAllocator, userList); + for (userList) |user| { + main.network.protocols.entity.send(user.conn, updateData); + } + } + + self.emptyMutex.unlock(); + self.changeQueue.pushBack(.{ .add = .{ i, drop } }); + } + + fn processChanges(self: *ItemDropManager) void { + while (self.changeQueue.popFront()) |data| { + switch (data) { + .add => |addData| { + self.internalAdd(addData[0], addData[1]); + }, + .remove => |index| { + self.internalRemove(index); + }, + } + } + } + + fn internalAdd(self: *ItemDropManager, i: u16, drop_: ItemDrop) void { + var drop = drop_; + if (self.world == null) { + ClientItemDropManager.clientSideInternalAdd(self, i, drop); + } + drop.reverseIndex = @intCast(self.size); + self.list.set(i, drop); + self.indices[self.size] = i; + self.size += 1; + } + + fn internalRemove(self: *ItemDropManager, i: u16) void { + self.size -= 1; + const ii = self.list.items(.reverseIndex)[i]; + self.list.items(.itemStack)[i].deinit(); + self.list.items(.itemStack)[i] = .{}; + self.indices[ii] = self.indices[self.size]; + self.list.items(.reverseIndex)[self.indices[self.size]] = ii; + } + + fn directRemove(self: *ItemDropManager, i: u16) void { + std.debug.assert(self.world != null); + self.emptyMutex.lock(); + self.isEmpty.set(i); + + const list = ZonElement.initArray(main.stackAllocator); + defer list.deinit(main.stackAllocator); + list.array.append(.null); + list.array.append(.{ .int = i }); + const updateData = list.toStringEfficient(main.stackAllocator, &.{}); + defer main.stackAllocator.free(updateData); + + const userList = main.server.getUserListAndIncreaseRefCount(main.stackAllocator); + defer main.server.freeUserListAndDecreaseRefCount(main.stackAllocator, userList); + for (userList) |user| { + main.network.protocols.entity.send(user.conn, updateData); + } + + self.emptyMutex.unlock(); + self.internalRemove(i); + } + + fn updateEnt(self: *ItemDropManager, chunk: *ServerChunk, pos: *Vec3d, vel: *Vec3d, deltaTime: f64) void { + const hitBox = physics.collision.Box{ .min = @splat(-radius), .max = @splat(radius) }; + if (physics.collision.collides(.server, .x, 0, pos.*, hitBox) != null) { + self.fixStuckInBlock(chunk, pos, vel, deltaTime); + return; + } + vel.* += Vec3d{ 0, 0, -gravity * deltaTime }; + inline for (0..3) |i| { + const move = vel.*[i] * deltaTime; // + acceleration[i]*deltaTime; + if (main.physics.collision.collides(.server, @enumFromInt(i), move, pos.*, hitBox)) |box| { + if (move < 0) { + pos.*[i] = box.max[i] + radius; + } else { + pos.*[i] = box.min[i] - radius; + } + vel.*[i] = 0; + } else { + pos.*[i] += move; + } + } + // Apply drag: + vel.* *= @splat(@max(0, 1 - self.airDragFactor * deltaTime)); + } + + fn fixStuckInBlock(self: *ItemDropManager, chunk: *ServerChunk, pos: *Vec3d, vel: *Vec3d, deltaTime: f64) void { + const centeredPos = pos.* - @as(Vec3d, @splat(0.5)); + const pos0: Vec3i = @floor(centeredPos); + + var closestEmptyBlock: Vec3i = @splat(-1); + var closestDist = std.math.floatMax(f64); + var delta = Vec3i{ 0, 0, 0 }; + while (delta[0] <= 1) : (delta[0] += 1) { + delta[1] = 0; + while (delta[1] <= 1) : (delta[1] += 1) { + delta[2] = 0; + while (delta[2] <= 1) : (delta[2] += 1) { + const isSolid = self.checkBlock(chunk, pos, pos0 + delta); + if (!isSolid) { + const dist = vec.lengthSquare(@as(Vec3d, @floatFromInt(pos0 + delta)) - centeredPos); + if (dist < closestDist) { + closestDist = dist; + closestEmptyBlock = delta; + } + } + } + } + } + + vel.* = @splat(0); + const unstuckVelocity: f64 = 1; + if (closestDist == std.math.floatMax(f64)) { + // Surrounded by solid blocks → move upwards + vel.*[2] = unstuckVelocity; + pos.*[2] += vel.*[2] * deltaTime; + } else { + vel.* = @as(Vec3d, @splat(unstuckVelocity)) * (@as(Vec3d, @floatFromInt(pos0 + closestEmptyBlock)) - centeredPos); + pos.* += (vel.*) * @as(Vec3d, @splat(deltaTime)); + } + } + + fn checkBlock(self: *ItemDropManager, chunk: *ServerChunk, pos: *Vec3d, blockPos: Vec3i) bool { + // Transform to chunk-relative coordinates: + const chunkPos = blockPos & ~@as(Vec3i, @splat(main.chunk.chunkMask)); + var block: blocks.Block = undefined; + if (chunk.super.pos.wx == chunkPos[0] and chunk.super.pos.wy == chunkPos[1] and chunk.super.pos.wz == chunkPos[2]) { + chunk.mutex.lock(); + defer chunk.mutex.unlock(); + block = chunk.getBlock(blockPos[0] - chunk.super.pos.wx, blockPos[1] - chunk.super.pos.wy, blockPos[2] - chunk.super.pos.wz); + } else { + const otherChunk = self.world.?.getSimulationChunkAndIncreaseRefCount(chunkPos[0], chunkPos[1], chunkPos[2]) orelse return true; + defer otherChunk.decreaseRefCount(); + const ch = otherChunk.getChunk() orelse return true; + ch.mutex.lock(); + defer ch.mutex.unlock(); + block = ch.getBlock(blockPos[0] - ch.super.pos.wx, blockPos[1] - ch.super.pos.wy, blockPos[2] - ch.super.pos.wz); + } + return main.physics.collision.collideWithBlock(block, blockPos[0], blockPos[1], blockPos[2], pos.*, @splat(radius), @splat(0)) != null; + } + + pub fn checkEntity(self: *ItemDropManager, user: *main.server.User) void { + var ii: u32 = 0; + while (ii < self.size) { + const i = self.indices[ii]; + if (self.list.items(.pickupCooldown)[i] > 0) { + ii += 1; + continue; + } + const hitbox = main.game.Player.outerBoundingBox; + const min = user.player().pos + hitbox.min; + const max = user.player().pos + hitbox.max; + const itemPos = self.list.items(.pos)[i]; + const dist = @max(min - itemPos, itemPos - max); + if (@reduce(.Max, dist) < radius + pickupRange) { + const itemStack = &self.list.items(.itemStack)[i]; + main.items.Inventory.ServerSide.tryCollectingToPlayerInventory(user, itemStack); + if (itemStack.amount == 0) { + self.directRemove(i); + continue; + } + } + ii += 1; + } + } }; pub const ClientItemDropManager = struct { // MARK: ClientItemDropManager - const maxf64Capacity = ItemDropManager.maxCapacity*@sizeOf(Vec3d)/@sizeOf(f64); - - super: ItemDropManager, - - lastTime: i16, - - timeDifference: utils.TimeDifference = .{}, - - interpolation: utils.GenericInterpolation(maxf64Capacity) align(64) = undefined, - - var instance: ?*ClientItemDropManager = null; - - var mutex: main.utils.Mutex = .{}; - - pub fn init(self: *ClientItemDropManager, allocator: NeverFailingAllocator) void { - std.debug.assert(instance == null); // Only one instance allowed. - instance = self; - self.* = .{ - .super = undefined, - .lastTime = @as(i16, @truncate(main.timestamp().toMilliseconds())) -% settings.entityLookback, - }; - self.super.init(allocator, null); - self.interpolation.init( - @ptrCast(self.super.list.items(.pos).ptr), - @ptrCast(self.super.list.items(.vel).ptr), - ); - } - - pub fn deinit(self: *ClientItemDropManager) void { - std.debug.assert(instance != null); // Double deinit. - self.super.deinit(); - instance = null; - } - - pub fn readPosition(self: *ClientItemDropManager, time: i16, itemData: []ItemDropNetworkData) void { - self.timeDifference.addDataPoint(time); - var pos: [ItemDropManager.maxCapacity]Vec3d = undefined; - var vel: [ItemDropManager.maxCapacity]Vec3d = undefined; - for (itemData) |data| { - pos[data.index] = data.pos; - vel[data.index] = data.vel; - } - mutex.lock(); - defer mutex.unlock(); - self.interpolation.updatePosition(@ptrCast(&pos), @ptrCast(&vel), time); // TODO: Only update the ones we actually changed. - } - - pub fn updateInterpolationData(self: *ClientItemDropManager) void { - self.super.processChanges(); - var time = @as(i16, @truncate(main.timestamp().toMilliseconds())) -% settings.entityLookback; - time -%= self.timeDifference.difference.load(.monotonic); - { - mutex.lock(); - defer mutex.unlock(); - self.interpolation.updateIndexed(time, self.lastTime, self.super.indices[0..self.super.size], 4); - } - self.lastTime = time; - } - - fn clientSideInternalAdd(_: *ItemDropManager, i: u16, drop: ItemDrop) void { - mutex.lock(); - defer mutex.unlock(); - for (&instance.?.interpolation.lastVel) |*lastVel| { - @as(*align(8) [ItemDropManager.maxCapacity]Vec3d, @ptrCast(lastVel))[i] = Vec3d{0, 0, 0}; - } - for (&instance.?.interpolation.lastPos) |*lastPos| { - @as(*align(8) [ItemDropManager.maxCapacity]Vec3d, @ptrCast(lastPos))[i] = drop.pos; - } - } - - pub fn remove(self: *ClientItemDropManager, i: u16) void { - self.super.emptyMutex.lock(); - self.super.isEmpty.set(i); - self.super.emptyMutex.unlock(); - self.super.changeQueue.pushBack(.{.remove = i}); - } - - pub fn loadFrom(self: *ClientItemDropManager, zon: ZonElement) void { - self.super.loadFrom(zon); - } - - pub fn addFromZon(self: *ClientItemDropManager, zon: ZonElement) void { - self.super.addFromZon(zon); - } + const maxf64Capacity = ItemDropManager.maxCapacity * @sizeOf(Vec3d) / @sizeOf(f64); + + super: ItemDropManager, + + lastTime: i16, + + timeDifference: utils.TimeDifference = .{}, + + interpolation: utils.GenericInterpolation(maxf64Capacity) align(64) = undefined, + + var instance: ?*ClientItemDropManager = null; + + var mutex: main.utils.Mutex = .{}; + + pub fn init(self: *ClientItemDropManager, allocator: NeverFailingAllocator) void { + std.debug.assert(instance == null); // Only one instance allowed. + instance = self; + self.* = .{ + .super = undefined, + .lastTime = @as(i16, @truncate(main.timestamp().toMilliseconds())) -% settings.entityLookback, + }; + self.super.init(allocator, null); + self.interpolation.init( + @ptrCast(self.super.list.items(.pos).ptr), + @ptrCast(self.super.list.items(.vel).ptr), + ); + } + + pub fn deinit(self: *ClientItemDropManager) void { + std.debug.assert(instance != null); // Double deinit. + self.super.deinit(); + instance = null; + } + + pub fn readPosition(self: *ClientItemDropManager, time: i16, itemData: []ItemDropNetworkData) void { + self.timeDifference.addDataPoint(time); + var pos: [ItemDropManager.maxCapacity]Vec3d = undefined; + var vel: [ItemDropManager.maxCapacity]Vec3d = undefined; + for (itemData) |data| { + pos[data.index] = data.pos; + vel[data.index] = data.vel; + } + mutex.lock(); + defer mutex.unlock(); + self.interpolation.updatePosition(@ptrCast(&pos), @ptrCast(&vel), time); // TODO: Only update the ones we actually changed. + } + + pub fn updateInterpolationData(self: *ClientItemDropManager) void { + self.super.processChanges(); + var time = @as(i16, @truncate(main.timestamp().toMilliseconds())) -% settings.entityLookback; + time -%= self.timeDifference.difference.load(.monotonic); + { + mutex.lock(); + defer mutex.unlock(); + self.interpolation.updateIndexed(time, self.lastTime, self.super.indices[0..self.super.size], 4); + } + self.lastTime = time; + } + + fn clientSideInternalAdd(_: *ItemDropManager, i: u16, drop: ItemDrop) void { + mutex.lock(); + defer mutex.unlock(); + for (&instance.?.interpolation.lastVel) |*lastVel| { + @as(*align(8) [ItemDropManager.maxCapacity]Vec3d, @ptrCast(lastVel))[i] = Vec3d{ 0, 0, 0 }; + } + for (&instance.?.interpolation.lastPos) |*lastPos| { + @as(*align(8) [ItemDropManager.maxCapacity]Vec3d, @ptrCast(lastPos))[i] = drop.pos; + } + } + + pub fn remove(self: *ClientItemDropManager, i: u16) void { + self.super.emptyMutex.lock(); + self.super.isEmpty.set(i); + self.super.emptyMutex.unlock(); + self.super.changeQueue.pushBack(.{ .remove = i }); + } + + pub fn loadFrom(self: *ClientItemDropManager, zon: ZonElement) void { + self.super.loadFrom(zon); + } + + pub fn addFromZon(self: *ClientItemDropManager, zon: ZonElement) void { + self.super.addFromZon(zon); + } }; // Going to handle item animations and other things like - bobbing, interpolation, movement reactions pub const ItemDisplayManager = struct { // MARK: ItemDisplayManager - pub var showItem: bool = true; - var cameraFollow: Vec3f = @splat(0); - var cameraFollowVel: Vec3f = @splat(0); - const damping: Vec3f = @splat(130); + pub var showItem: bool = true; + var cameraFollow: Vec3f = @splat(0); + var cameraFollowVel: Vec3f = @splat(0); + const damping: Vec3f = @splat(130); - pub fn update(deltaTime: f64) void { - if (deltaTime == 0) return; - const dt: f32 = @floatCast(deltaTime); + pub fn update(deltaTime: f64) void { + if (deltaTime == 0) return; + const dt: f32 = @floatCast(deltaTime); - var playerVel: Vec3f = .{@floatCast((game.Player.super.vel[2]*0.009 + game.Player.eye.vel[2]*0.0075)), 0, 0}; - playerVel = vec.clampMag(playerVel, 0.32); + var playerVel: Vec3f = .{ @floatCast((game.Player.super.vel[2] * 0.009 + game.Player.eye.vel[2] * 0.0075)), 0, 0 }; + playerVel = vec.clampMag(playerVel, 0.32); - // TODO: add *smooth* item sway - const n1: Vec3f = cameraFollowVel - (cameraFollow - playerVel)*damping*damping*@as(Vec3f, @splat(dt)); - const n2: Vec3f = @as(Vec3f, @splat(1)) + damping*@as(Vec3f, @splat(dt)); - cameraFollowVel = n1/(n2*n2); + // TODO: add *smooth* item sway + const n1: Vec3f = cameraFollowVel - (cameraFollow - playerVel) * damping * damping * @as(Vec3f, @splat(dt)); + const n2: Vec3f = @as(Vec3f, @splat(1)) + damping * @as(Vec3f, @splat(dt)); + cameraFollowVel = n1 / (n2 * n2); - cameraFollow += cameraFollowVel*@as(Vec3f, @splat(dt)); - } + cameraFollow += cameraFollowVel * @as(Vec3f, @splat(dt)); + } }; pub const ItemDropRenderer = struct { // MARK: ItemDropRenderer - var itemPipeline: graphics.Pipeline = undefined; - var itemUniforms: struct { - projectionMatrix: c_int, - modelMatrix: c_int, - viewMatrix: c_int, - ambientLight: c_int, - modelIndex: c_int, - block: c_int, - reflectionMapSize: c_int, - contrast: c_int, - glDepthRange: c_int, - } = undefined; - - var itemModelSSBO: graphics.SSBO = undefined; - var modelData: main.List(u32) = undefined; - var freeSlots: main.List(*ItemVoxelModel) = undefined; - - const ItemVoxelModel = struct { - index: u31 = undefined, - len: u31 = undefined, - item: items.Item, - - fn getSlot(len: u31) u31 { - for (freeSlots.items, 0..) |potentialSlot, i| { - if (len == potentialSlot.len) { - _ = freeSlots.swapRemove(i); - const result = potentialSlot.index; - main.globalAllocator.destroy(potentialSlot); - return result; - } - } - const result: u31 = @intCast(modelData.items.len); - modelData.resize(result + len); - return result; - } - - fn init(template: ItemVoxelModel) *ItemVoxelModel { - const self = main.globalAllocator.create(ItemVoxelModel); - self.* = ItemVoxelModel{ - .item = template.item, - }; - if (self.item == .baseItem and self.item.baseItem.block() != null and self.item.baseItem.image().imageData.ptr == graphics.Image.defaultImage.imageData.ptr) { - // Find sizes and free index: - var block = blocks.Block{.typ = self.item.baseItem.block().?, .data = 0}; - block.data = block.mode().naturalStandard; - const model = blocks.meshes.model(block).model(); - var data = main.List(u32).init(main.stackAllocator); - defer data.deinit(); - for (model.internalQuads) |quad| { - const textureIndex = blocks.meshes.textureIndex(block, quad.quadInfo().textureSlot); - data.append(@as(u32, @intFromEnum(quad)) << 16 | textureIndex); // modelAndTexture - data.append(0); // offsetByNormal - } - for (model.neighborFacingQuads) |list| { - for (list) |quad| { - const textureIndex = blocks.meshes.textureIndex(block, quad.quadInfo().textureSlot); - data.append(@as(u32, @intFromEnum(quad)) << 16 | textureIndex); // modelAndTexture - data.append(1); // offsetByNormal - } - } - self.len = @intCast(data.items.len); - self.index = getSlot(self.len); - @memcpy(modelData.items[self.index..][0..self.len], data.items); - } else { - // Find sizes and free index: - const img = self.item.getImage(); - const size = Vec3i{img.width, 1, img.height}; - self.len = @intCast(3 + @reduce(.Mul, size)); - self.index = getSlot(self.len); - var dataSection: []u32 = undefined; - dataSection = modelData.items[self.index..][0..self.len]; - dataSection[0] = @intCast(size[0]); - dataSection[1] = @intCast(size[1]); - dataSection[2] = @intCast(size[2]); - var i: u32 = 3; - var z: u32 = 0; - while (z < 1) : (z += 1) { - var x: u32 = 0; - while (x < img.width) : (x += 1) { - var y: u32 = 0; - while (y < img.height) : (y += 1) { - dataSection[i] = img.getRGB(x, y).toARBG(); - i += 1; - } - } - } - } - itemModelSSBO.bufferData(u32, modelData.items); - return self; - } - - fn deinit(self: *ItemVoxelModel) void { - freeSlots.append(self); - } - - pub fn equals(self: ItemVoxelModel, other: ?*ItemVoxelModel) bool { - if (other == null) return false; - return std.meta.eql(self.item, other.?.item); - } - - pub fn hashCode(self: ItemVoxelModel) u32 { - return self.item.hashCode(); - } - }; - - pub fn init() void { - itemPipeline = graphics.Pipeline.init( - "assets/cubyz/shaders/item_drop.vert", - "assets/cubyz/shaders/item_drop.frag", - "", - &itemUniforms, - graphics.VertexArray.EmptyVertex, - &.{}, - .{}, - .{.depthTest = true}, - .{.attachments = &.{.alphaBlending}}, - ); - itemModelSSBO = .init(); - itemModelSSBO.bufferData(i32, &[3]i32{1, 1, 1}); - itemModelSSBO.bind(2); - - modelData = .init(main.globalAllocator); - freeSlots = .init(main.globalAllocator); - } - - pub fn deinit() void { - itemPipeline.deinit(); - itemModelSSBO.deinit(); - modelData.deinit(); - voxelModels.clear(); - for (freeSlots.items) |freeSlot| { - main.globalAllocator.destroy(freeSlot); - } - freeSlots.deinit(); - } - - var voxelModels: utils.Cache(ItemVoxelModel, 32, 32, ItemVoxelModel.deinit) = .{}; - - fn getModel(item: items.Item) *ItemVoxelModel { - const compareObject = ItemVoxelModel{.item = item}; - return voxelModels.findOrCreate(compareObject, ItemVoxelModel.init, null); - } - - fn bindCommonUniforms(projMatrix: Mat4f, viewMatrix: Mat4f, ambientLight: Vec3f) void { - itemPipeline.bind(null); - c.glUniform1f(itemUniforms.reflectionMapSize, main.renderer.reflectionCubeMapSize); - c.glUniformMatrix4fv(itemUniforms.projectionMatrix, 1, c.GL_TRUE, @ptrCast(&projMatrix)); - c.glUniform3fv(itemUniforms.ambientLight, 1, @ptrCast(&ambientLight)); - c.glUniformMatrix4fv(itemUniforms.viewMatrix, 1, c.GL_TRUE, @ptrCast(&viewMatrix)); - c.glUniform1f(itemUniforms.contrast, 0.12); - var depthRange: [2]f32 = undefined; - c.glGetFloatv(c.GL_DEPTH_RANGE, &depthRange); - c.glUniform2fv(itemUniforms.glDepthRange, 1, &depthRange); - } - - fn bindLightUniform(light: [6]u8, ambientLight: Vec3f) void { - const sunLight: Vec3f = ambientLight*@as(Vec3f, @floatFromInt(Vec3i{light[0], light[1], light[2]}))/@as(Vec3f, @splat(255)); - const blockLight: Vec3f = @as(Vec3f, @floatFromInt(Vec3i{light[3], light[4], light[5]}))/@as(Vec3f, @splat(255)); - c.glUniform3fv(itemUniforms.ambientLight, 1, @ptrCast(&@min(@sqrt(sunLight*sunLight + blockLight*blockLight), @as(Vec3f, @splat(1))))); - } - - fn bindModelUniforms(modelIndex: u31, blockType: u16) void { - c.glUniform1i(itemUniforms.modelIndex, modelIndex); - c.glUniform1i(itemUniforms.block, blockType); - } - - fn drawItem(vertices: u31, modelMatrix: Mat4f) void { - c.glUniformMatrix4fv(itemUniforms.modelMatrix, 1, c.GL_TRUE, @ptrCast(&modelMatrix)); - main.renderer.chunk_meshing.vao.bind(); - c.glDrawElements(c.GL_TRIANGLES, vertices, c.GL_UNSIGNED_INT, null); - } - - pub fn renderItemDrops(projMatrix: Mat4f, ambientLight: Vec3f, playerPos: Vec3d) void { - game.world.?.itemDrops.updateInterpolationData(); - - bindCommonUniforms(projMatrix, game.camera.viewMatrix, ambientLight); - const itemDrops = &game.world.?.itemDrops.super; - for (itemDrops.indices[0..itemDrops.size]) |i| { - const item = itemDrops.list.items(.itemStack)[i].item; - if (item != .null) { - var pos = itemDrops.list.items(.pos)[i]; - const rot = itemDrops.list.items(.rot)[i]; - const blockPos: Vec3i = @floor(pos); - const light: [6]u8 = main.renderer.mesh_storage.getLight(blockPos[0], blockPos[1], blockPos[2]) orelse @splat(0); - bindLightUniform(light, ambientLight); - pos -= playerPos; - - const model = getModel(item); - var vertices: u31 = 36; - - var scale: f32 = 0.3; - var blockType: u16 = 0; - if (item == .baseItem and item.baseItem.block() != null and item.baseItem.image().imageData.ptr == graphics.Image.defaultImage.imageData.ptr) { - blockType = item.baseItem.block().?; - vertices = model.len/2*6; - } else { - scale = 0.5; - } - bindModelUniforms(model.index, blockType); - - var modelMatrix = Mat4f.translation(@floatCast(pos)); - modelMatrix = modelMatrix.mul(Mat4f.rotationX(-rot[0])); - modelMatrix = modelMatrix.mul(Mat4f.rotationY(-rot[1])); - modelMatrix = modelMatrix.mul(Mat4f.rotationZ(-rot[2])); - modelMatrix = modelMatrix.mul(Mat4f.scale(@splat(scale))); - modelMatrix = modelMatrix.mul(Mat4f.translation(@splat(-0.5))); - drawItem(vertices, modelMatrix); - } - } - } - - inline fn getIndex(x: u8, y: u8, z: u8) u32 { - return (z*4) + (y*2) + (x); - } - - inline fn blendColors(a: [6]f32, b: [6]f32, t: f32) [6]f32 { - var result: [6]f32 = .{0, 0, 0, 0, 0, 0}; - inline for (0..6) |i| { - result[i] = std.math.lerp(a[i], b[i], t); - } - return result; - } - - pub fn renderDisplayItems(ambientLight: Vec3f, playerPos: Vec3d) void { - if (!ItemDisplayManager.showItem) return; - - const projMatrix: Mat4f = Mat4f.perspective(std.math.degreesToRadians(65), @as(f32, @floatFromInt(main.renderer.lastWidth))/@as(f32, @floatFromInt(main.renderer.lastHeight)), 0.01, 3); - const viewMatrix = Mat4f.identity(); - bindCommonUniforms(projMatrix, viewMatrix, ambientLight); - - const item = game.Player.inventory.getItem(game.Player.selectedSlot); - if (item != .null) { - var pos: Vec3d = Vec3d{0, 0, 0}; - const rot: Vec3f = ItemDisplayManager.cameraFollow; - - const lightPos = @as(Vec3d, @floatCast(playerPos)) - @as(Vec3f, @splat(0.5)); - const blockPos: Vec3i = @floor(lightPos); - const localBlockPos: Vec3f = @floatCast(lightPos - @as(Vec3d, @floatFromInt(blockPos))); - - var samples: [8][6]f32 = @splat(@splat(0)); - inline for (0..2) |z| { - inline for (0..2) |y| { - inline for (0..2) |x| { - const light: [6]u8 = main.renderer.mesh_storage.getLight( - blockPos[0] +% @as(i32, @intCast(x)), - blockPos[1] +% @as(i32, @intCast(y)), - blockPos[2] +% @as(i32, @intCast(z)), - ) orelse @splat(0); - - inline for (0..6) |i| { - samples[getIndex(x, y, z)][i] = @as(f32, @floatFromInt(light[i])); - } - } - } - } - - inline for (0..2) |y| { - inline for (0..2) |x| { - samples[getIndex(x, y, 0)] = blendColors(samples[getIndex(x, y, 0)], samples[getIndex(x, y, 1)], localBlockPos[2]); - } - } - - inline for (0..2) |x| { - samples[getIndex(x, 0, 0)] = blendColors(samples[getIndex(x, 0, 0)], samples[getIndex(x, 1, 0)], localBlockPos[1]); - } - - var result: [6]u8 = .{0, 0, 0, 0, 0, 0}; - inline for (0..6) |i| { - const val = std.math.lerp(samples[getIndex(0, 0, 0)][i], samples[getIndex(1, 0, 0)][i], localBlockPos[0]); - result[i] = @floor(val); - } - - bindLightUniform(result, ambientLight); - - const model = getModel(item); - var vertices: u31 = 36; - - const isBlock: bool = item == .baseItem and item.baseItem.block() != null and item.baseItem.image().imageData.ptr == graphics.Image.defaultImage.imageData.ptr; - var scale: f32 = 0; - var blockType: u16 = 0; - if (isBlock) { - blockType = item.baseItem.block().?; - vertices = model.len/2*6; - scale = 0.3; - pos = Vec3d{0.4, 0.55, -0.32}; - } else { - scale = 0.57; - pos = Vec3d{0.4, 0.65, -0.3}; - } - bindModelUniforms(model.index, blockType); - - var modelMatrix = Mat4f.rotationZ(-rot[2]); - modelMatrix = modelMatrix.mul(Mat4f.rotationY(-rot[1])); - modelMatrix = modelMatrix.mul(Mat4f.rotationX(-rot[0])); - modelMatrix = modelMatrix.mul(Mat4f.translation(@floatCast(pos))); - if (!isBlock) { - if (item == .proceduralItem) { - modelMatrix = modelMatrix.mul(Mat4f.rotationZ(-std.math.pi*0.47)); - modelMatrix = modelMatrix.mul(Mat4f.rotationY(std.math.pi*0.25)); - } else { - modelMatrix = modelMatrix.mul(Mat4f.rotationZ(-std.math.pi*0.45)); - } - } else { - modelMatrix = modelMatrix.mul(Mat4f.rotationZ(-std.math.pi*0.2)); - } - modelMatrix = modelMatrix.mul(Mat4f.scale(@splat(scale))); - modelMatrix = modelMatrix.mul(Mat4f.translation(@splat(-0.5))); - drawItem(vertices, modelMatrix); - } - } + var itemPipeline: graphics.Pipeline = undefined; + var itemUniforms: struct { + projectionMatrix: c_int, + modelMatrix: c_int, + viewMatrix: c_int, + ambientLight: c_int, + modelIndex: c_int, + block: c_int, + reflectionMapSize: c_int, + contrast: c_int, + glDepthRange: c_int, + } = undefined; + + var itemModelSSBO: graphics.SSBO = undefined; + var modelData: main.List(u32) = undefined; + var freeSlots: main.List(*ItemVoxelModel) = undefined; + + const ItemVoxelModel = struct { + index: u31 = undefined, + len: u31 = undefined, + item: items.Item, + + fn getSlot(len: u31) u31 { + for (freeSlots.items, 0..) |potentialSlot, i| { + if (len == potentialSlot.len) { + _ = freeSlots.swapRemove(i); + const result = potentialSlot.index; + main.globalAllocator.destroy(potentialSlot); + return result; + } + } + const result: u31 = @intCast(modelData.items.len); + modelData.resize(result + len); + return result; + } + + fn init(template: ItemVoxelModel) *ItemVoxelModel { + const self = main.globalAllocator.create(ItemVoxelModel); + self.* = ItemVoxelModel{ + .item = template.item, + }; + if (self.item == .baseItem and self.item.baseItem.block() != null and self.item.baseItem.image().imageData.ptr == graphics.Image.defaultImage.imageData.ptr) { + // Find sizes and free index: + var block = blocks.Block{ .typ = self.item.baseItem.block().?, .data = 0 }; + block.data = block.mode().naturalStandard; + const model = blocks.meshes.model(block).model(); + var data = main.List(u32).init(main.stackAllocator); + defer data.deinit(); + for (model.internalQuads) |quad| { + const textureIndex = blocks.meshes.textureIndex(block, quad.quadInfo().textureSlot); + data.append(@as(u32, @intFromEnum(quad)) << 16 | textureIndex); // modelAndTexture + data.append(0); // offsetByNormal + } + for (model.neighborFacingQuads) |list| { + for (list) |quad| { + const textureIndex = blocks.meshes.textureIndex(block, quad.quadInfo().textureSlot); + data.append(@as(u32, @intFromEnum(quad)) << 16 | textureIndex); // modelAndTexture + data.append(1); // offsetByNormal + } + } + self.len = @intCast(data.items.len); + self.index = getSlot(self.len); + @memcpy(modelData.items[self.index..][0..self.len], data.items); + } else { + // Find sizes and free index: + const img = self.item.getImage(); + const size = Vec3i{ img.width, 1, img.height }; + self.len = @intCast(3 + @reduce(.Mul, size)); + self.index = getSlot(self.len); + var dataSection: []u32 = undefined; + dataSection = modelData.items[self.index..][0..self.len]; + dataSection[0] = @intCast(size[0]); + dataSection[1] = @intCast(size[1]); + dataSection[2] = @intCast(size[2]); + var i: u32 = 3; + var z: u32 = 0; + while (z < 1) : (z += 1) { + var x: u32 = 0; + while (x < img.width) : (x += 1) { + var y: u32 = 0; + while (y < img.height) : (y += 1) { + dataSection[i] = img.getRGB(x, y).toARBG(); + i += 1; + } + } + } + } + itemModelSSBO.bufferData(u32, modelData.items); + return self; + } + + fn deinit(self: *ItemVoxelModel) void { + freeSlots.append(self); + } + + pub fn equals(self: ItemVoxelModel, other: ?*ItemVoxelModel) bool { + if (other == null) return false; + return std.meta.eql(self.item, other.?.item); + } + + pub fn hashCode(self: ItemVoxelModel) u32 { + return self.item.hashCode(); + } + }; + + pub fn init() void { + itemPipeline = graphics.Pipeline.init( + "assets/cubyz/shaders/item_drop.vert", + "assets/cubyz/shaders/item_drop.frag", + "", + &itemUniforms, + graphics.VertexArray.EmptyVertex, + &.{}, + .{}, + .{ .depthTest = true }, + .{ .attachments = &.{.alphaBlending} }, + ); + itemModelSSBO = .init(); + itemModelSSBO.bufferData(i32, &[3]i32{ 1, 1, 1 }); + itemModelSSBO.bind(2); + + modelData = .init(main.globalAllocator); + freeSlots = .init(main.globalAllocator); + } + + pub fn deinit() void { + itemPipeline.deinit(); + itemModelSSBO.deinit(); + modelData.deinit(); + voxelModels.clear(); + for (freeSlots.items) |freeSlot| { + main.globalAllocator.destroy(freeSlot); + } + freeSlots.deinit(); + } + + var voxelModels: utils.Cache(ItemVoxelModel, 32, 32, ItemVoxelModel.deinit) = .{}; + + fn getModel(item: items.Item) *ItemVoxelModel { + const compareObject = ItemVoxelModel{ .item = item }; + return voxelModels.findOrCreate(compareObject, ItemVoxelModel.init, null); + } + + fn bindCommonUniforms(projMatrix: Mat4f, viewMatrix: Mat4f, ambientLight: Vec3f) void { + itemPipeline.bind(null); + c.glUniform1f(itemUniforms.reflectionMapSize, main.renderer.reflectionCubeMapSize); + c.glUniformMatrix4fv(itemUniforms.projectionMatrix, 1, c.GL_TRUE, @ptrCast(&projMatrix)); + c.glUniform3fv(itemUniforms.ambientLight, 1, @ptrCast(&ambientLight)); + c.glUniformMatrix4fv(itemUniforms.viewMatrix, 1, c.GL_TRUE, @ptrCast(&viewMatrix)); + c.glUniform1f(itemUniforms.contrast, 0.12); + var depthRange: [2]f32 = undefined; + c.glGetFloatv(c.GL_DEPTH_RANGE, &depthRange); + c.glUniform2fv(itemUniforms.glDepthRange, 1, &depthRange); + } + + fn bindLightUniform(light: [6]u8, ambientLight: Vec3f) void { + const sunLight: Vec3f = ambientLight * @as(Vec3f, @floatFromInt(Vec3i{ light[0], light[1], light[2] })) / @as(Vec3f, @splat(255)); + const blockLight: Vec3f = @as(Vec3f, @floatFromInt(Vec3i{ light[3], light[4], light[5] })) / @as(Vec3f, @splat(255)); + c.glUniform3fv(itemUniforms.ambientLight, 1, @ptrCast(&@min(@sqrt(sunLight * sunLight + blockLight * blockLight), @as(Vec3f, @splat(1))))); + } + + fn bindModelUniforms(modelIndex: u31, blockType: u16) void { + c.glUniform1i(itemUniforms.modelIndex, modelIndex); + c.glUniform1i(itemUniforms.block, blockType); + } + + fn drawItem(vertices: u31, modelMatrix: Mat4f) void { + c.glUniformMatrix4fv(itemUniforms.modelMatrix, 1, c.GL_TRUE, @ptrCast(&modelMatrix)); + main.renderer.chunk_meshing.vao.bind(); + c.glDrawElements(c.GL_TRIANGLES, vertices, c.GL_UNSIGNED_INT, null); + } + + pub fn renderItemDrops(projMatrix: Mat4f, ambientLight: Vec3f, playerPos: Vec3d) void { + game.world.?.itemDrops.updateInterpolationData(); + + bindCommonUniforms(projMatrix, game.camera.viewMatrix, ambientLight); + const itemDrops = &game.world.?.itemDrops.super; + for (itemDrops.indices[0..itemDrops.size]) |i| { + const item = itemDrops.list.items(.itemStack)[i].item; + if (item != .null) { + var pos = itemDrops.list.items(.pos)[i]; + const rot = itemDrops.list.items(.rot)[i]; + const blockPos: Vec3i = @floor(pos); + const light: [6]u8 = main.renderer.mesh_storage.getLight(blockPos[0], blockPos[1], blockPos[2]) orelse @splat(0); + bindLightUniform(light, ambientLight); + pos -= playerPos; + + const model = getModel(item); + var vertices: u31 = 36; + + var scale: f32 = 0.3; + var blockType: u16 = 0; + if (item == .baseItem and item.baseItem.block() != null and item.baseItem.image().imageData.ptr == graphics.Image.defaultImage.imageData.ptr) { + blockType = item.baseItem.block().?; + vertices = model.len / 2 * 6; + } else { + scale = 0.5; + } + bindModelUniforms(model.index, blockType); + + var modelMatrix = Mat4f.translation(@floatCast(pos)); + modelMatrix = modelMatrix.mul(Mat4f.rotationX(-rot[0])); + modelMatrix = modelMatrix.mul(Mat4f.rotationY(-rot[1])); + modelMatrix = modelMatrix.mul(Mat4f.rotationZ(-rot[2])); + modelMatrix = modelMatrix.mul(Mat4f.scale(@splat(scale))); + modelMatrix = modelMatrix.mul(Mat4f.translation(@splat(-0.5))); + drawItem(vertices, modelMatrix); + } + } + } + + inline fn getIndex(x: u8, y: u8, z: u8) u32 { + return (z * 4) + (y * 2) + (x); + } + + inline fn blendColors(a: [6]f32, b: [6]f32, t: f32) [6]f32 { + var result: [6]f32 = .{ 0, 0, 0, 0, 0, 0 }; + inline for (0..6) |i| { + result[i] = std.math.lerp(a[i], b[i], t); + } + return result; + } + + pub fn renderDisplayItems(ambientLight: Vec3f, playerPos: Vec3d) void { + if (!ItemDisplayManager.showItem) return; + + const projMatrix: Mat4f = Mat4f.perspective(std.math.degreesToRadians(65), @as(f32, @floatFromInt(main.renderer.lastWidth)) / @as(f32, @floatFromInt(main.renderer.lastHeight)), 0.01, 3); + const viewMatrix = Mat4f.identity(); + bindCommonUniforms(projMatrix, viewMatrix, ambientLight); + + const item = game.Player.inventory.getItem(game.Player.selectedSlot); + if (item != .null) { + var pos: Vec3d = Vec3d{ 0, 0, 0 }; + const rot: Vec3f = ItemDisplayManager.cameraFollow; + + const lightPos = @as(Vec3d, @floatCast(playerPos)) - @as(Vec3f, @splat(0.5)); + const blockPos: Vec3i = @floor(lightPos); + const localBlockPos: Vec3f = @floatCast(lightPos - @as(Vec3d, @floatFromInt(blockPos))); + + var samples: [8][6]f32 = @splat(@splat(0)); + inline for (0..2) |z| { + inline for (0..2) |y| { + inline for (0..2) |x| { + const light: [6]u8 = main.renderer.mesh_storage.getLight( + blockPos[0] +% @as(i32, @intCast(x)), + blockPos[1] +% @as(i32, @intCast(y)), + blockPos[2] +% @as(i32, @intCast(z)), + ) orelse @splat(0); + + inline for (0..6) |i| { + samples[getIndex(x, y, z)][i] = @as(f32, @floatFromInt(light[i])); + } + } + } + } + + inline for (0..2) |y| { + inline for (0..2) |x| { + samples[getIndex(x, y, 0)] = blendColors(samples[getIndex(x, y, 0)], samples[getIndex(x, y, 1)], localBlockPos[2]); + } + } + + inline for (0..2) |x| { + samples[getIndex(x, 0, 0)] = blendColors(samples[getIndex(x, 0, 0)], samples[getIndex(x, 1, 0)], localBlockPos[1]); + } + + var result: [6]u8 = .{ 0, 0, 0, 0, 0, 0 }; + inline for (0..6) |i| { + const val = std.math.lerp(samples[getIndex(0, 0, 0)][i], samples[getIndex(1, 0, 0)][i], localBlockPos[0]); + result[i] = @floor(val); + } + + bindLightUniform(result, ambientLight); + + const model = getModel(item); + var vertices: u31 = 36; + + const isBlock: bool = item == .baseItem and item.baseItem.block() != null and item.baseItem.image().imageData.ptr == graphics.Image.defaultImage.imageData.ptr; + var scale: f32 = 0; + var blockType: u16 = 0; + if (isBlock) { + blockType = item.baseItem.block().?; + vertices = model.len / 2 * 6; + scale = 0.3; + pos = Vec3d{ 0.4, 0.55, -0.32 }; + } else { + scale = 0.57; + pos = Vec3d{ 0.4, 0.65, -0.3 }; + } + bindModelUniforms(model.index, blockType); + + var modelMatrix = Mat4f.rotationZ(-rot[2]); + modelMatrix = modelMatrix.mul(Mat4f.rotationY(-rot[1])); + modelMatrix = modelMatrix.mul(Mat4f.rotationX(-rot[0])); + modelMatrix = modelMatrix.mul(Mat4f.translation(@floatCast(pos))); + if (!isBlock) { + if (item == .proceduralItem) { + modelMatrix = modelMatrix.mul(Mat4f.rotationZ(-std.math.pi * 0.47)); + modelMatrix = modelMatrix.mul(Mat4f.rotationY(std.math.pi * 0.25)); + } else { + modelMatrix = modelMatrix.mul(Mat4f.rotationZ(-std.math.pi * 0.45)); + } + } else { + modelMatrix = modelMatrix.mul(Mat4f.rotationZ(-std.math.pi * 0.2)); + } + modelMatrix = modelMatrix.mul(Mat4f.scale(@splat(scale))); + modelMatrix = modelMatrix.mul(Mat4f.translation(@splat(-0.5))); + drawItem(vertices, modelMatrix); + } + } }; diff --git a/src/particles.zig b/src/particles.zig index 5c8c970abe..bd2800b5c8 100644 --- a/src/particles.zig +++ b/src/particles.zig @@ -3,20 +3,15 @@ const std = @import("std"); const main = @import("main"); const physics = @import("physics.zig"); const chunk_meshing = @import("renderer/chunk_meshing.zig"); - const graphics = @import("graphics.zig"); const SSBO = graphics.SSBO; const TextureArray = graphics.TextureArray; const Shader = graphics.Shader; const Image = graphics.Image; - -const c = @import("c"); const game = @import("game.zig"); const ZonElement = @import("zon.zig").ZonElement; - const random = @import("random.zig"); const RandomRange = random.RandomRange; - const vec = @import("vec.zig"); const Mat4f = vec.Mat4f; const Vec3d = vec.Vec3d; @@ -26,547 +21,549 @@ const Vec4f = vec.Vec4f; const Vec3i = vec.Vec3i; const Vec2f = vec.Vec2f; +const c = @import("c"); + pub const ParticleManager = struct { - var particleTypesSSBO: SSBO = undefined; - var types: main.ListUnmanaged(ParticleType) = .{}; - var typesLocal: main.ListUnmanaged(ParticleTypeLocal) = .{}; - var textures: main.ListUnmanaged(Image) = .{}; - var emissionTextures: main.ListUnmanaged(Image) = .{}; - - var textureArray: TextureArray = undefined; - var emissionTextureArray: TextureArray = undefined; - - const ParticleIndex = u16; - var particleTypeHashmap: std.StringHashMapUnmanaged(ParticleIndex) = .{}; - - pub fn init() void { - textureArray = .init(); - emissionTextureArray = .init(); - particleTypesSSBO = SSBO.init(); - ParticleSystem.init(); - } - - pub fn deinit() void { - textureArray.deinit(); - emissionTextureArray.deinit(); - ParticleSystem.deinit(); - particleTypesSSBO.deinit(); - } - - pub fn reset() void { - types = .{}; - typesLocal = .{}; - textures = .{}; - emissionTextures = .{}; - particleTypeHashmap = .{}; - ParticleSystem.reset(); - } - - pub fn register(assetsFolder: []const u8, id: []const u8, zon: ZonElement) void { - const textureId = zon.get(?[]const u8, "texture", null) orelse { - std.log.err("Particle texture id was not specified for {s} ({s})", .{id, assetsFolder}); - return; - }; - - const particleType = readTextureDataAndParticleType(assetsFolder, textureId); - var rotVel: RandomRange(f32) = RandomRange(f32).fromZon(zon.getChild("rotationVelocity")) orelse .init(20, 60); - rotVel.min = std.math.degreesToRadians(rotVel.min); - rotVel.max = std.math.degreesToRadians(rotVel.max); - const particleTypeLocal = ParticleTypeLocal{ - .density = RandomRange(f32).fromZon(zon.getChild("density")) orelse .init(2, 3), - .rotVel = rotVel, - .dragCoefficient = RandomRange(f32).fromZon(zon.getChild("dragCoefficient")) orelse .init(0.5, 0.6), - }; - - particleTypeHashmap.put(main.worldArena.allocator, id, @intCast(types.items.len)) catch unreachable; - types.append(main.worldArena, particleType); - typesLocal.append(main.worldArena, particleTypeLocal); - - std.log.debug("Registered particle type: {s}", .{id}); - } - - fn readTextureDataAndParticleType(assetsFolder: []const u8, textureId: []const u8) ParticleType { - var typ: ParticleType = undefined; - - const base = readTexture(assetsFolder, textureId, ".png", Image.defaultImage, .isMandatory); - const emission = readTexture(assetsFolder, textureId, "_emission.png", Image.emptyImage, .isOptional); - const hasEmission = (emission.imageData.ptr != Image.emptyImage.imageData.ptr); - const baseAnimationFrameCount = base.height/base.width; - const emissionAnimationFrameCount = emission.height/emission.width; - - typ.frameCount = @floatFromInt(baseAnimationFrameCount); - typ.startFrame = @floatFromInt(textures.items.len); - typ.size = @as(f32, @floatFromInt(base.width))/16; - - var isBaseBroken = false; - var isEmissionBroken = false; - - if (base.height%base.width != 0) { - std.log.err("Particle base texture has incorrect dimensions ({}x{}) expected height to be multiple of width for {s} ({s})", .{base.width, base.height, textureId, assetsFolder}); - isBaseBroken = true; - } - if (hasEmission and emission.height%emission.width != 0) { - std.log.err("Particle emission texture has incorrect dimensions ({}x{}) expected height to be multiple of width for {s} ({s})", .{base.width, base.height, textureId, assetsFolder}); - isEmissionBroken = true; - } - if (hasEmission and baseAnimationFrameCount != emissionAnimationFrameCount) { - std.log.err("Particle base texture and emission texture frame count mismatch ({} vs {}) for {s} ({s})", .{baseAnimationFrameCount, emissionAnimationFrameCount, textureId, assetsFolder}); - isEmissionBroken = true; - } - - createAnimationFrames(&textures, baseAnimationFrameCount, base, isBaseBroken); - createAnimationFrames(&emissionTextures, baseAnimationFrameCount, emission, isBaseBroken or isEmissionBroken or !hasEmission); - - return typ; - } - - fn readTexture(assetsFolder: []const u8, textureId: []const u8, suffix: []const u8, default: graphics.Image, status: enum { isOptional, isMandatory }) graphics.Image { - var splitter = std.mem.splitScalar(u8, textureId, ':'); - const mod = splitter.first(); - const id = splitter.rest(); - - const gameAssetsPath = std.fmt.allocPrint(main.stackAllocator.allocator, "assets/{s}/particles/textures/{s}{s}", .{mod, id, suffix}) catch unreachable; - defer main.stackAllocator.free(gameAssetsPath); - - const worldAssetsPath = std.fmt.allocPrint(main.stackAllocator.allocator, "{s}/{s}/particles/textures/{s}{s}", .{assetsFolder, mod, id, suffix}) catch unreachable; - defer main.stackAllocator.free(worldAssetsPath); - - return graphics.Image.readFromFile(main.worldArena, worldAssetsPath) catch graphics.Image.readFromFile(main.worldArena, gameAssetsPath) catch { - if (status == .isMandatory) std.log.err("Particle texture not found in {s} and {s}.", .{worldAssetsPath, gameAssetsPath}); - return default; - }; - } - - fn createAnimationFrames(container: *main.ListUnmanaged(Image), frameCount: usize, image: Image, isBroken: bool) void { - for (0..frameCount) |i| { - container.append(main.worldArena, if (isBroken) image else extractAnimationSlice(image, i)); - } - } - - fn extractAnimationSlice(image: Image, frameIndex: usize) Image { - const frameCount = image.height/image.width; - const frameHeight = image.height/frameCount; - const startHeight = frameHeight*frameIndex; - const endHeight = frameHeight*(frameIndex + 1); - var result = image; - result.height = @intCast(frameHeight); - result.imageData = result.imageData[startHeight*image.width .. endHeight*image.width]; - return result; - } - - pub fn generateTextureArray() void { - textureArray.generate(textures.items, true, true); - emissionTextureArray.generate(emissionTextures.items, true, false); - - particleTypesSSBO.bufferData(ParticleType, ParticleManager.types.items); - particleTypesSSBO.bind(14); - } + var particleTypesSSBO: SSBO = undefined; + var types: main.ListUnmanaged(ParticleType) = .{}; + var typesLocal: main.ListUnmanaged(ParticleTypeLocal) = .{}; + var textures: main.ListUnmanaged(Image) = .{}; + var emissionTextures: main.ListUnmanaged(Image) = .{}; + + var textureArray: TextureArray = undefined; + var emissionTextureArray: TextureArray = undefined; + + const ParticleIndex = u16; + var particleTypeHashmap: std.StringHashMapUnmanaged(ParticleIndex) = .{}; + + pub fn init() void { + textureArray = .init(); + emissionTextureArray = .init(); + particleTypesSSBO = SSBO.init(); + ParticleSystem.init(); + } + + pub fn deinit() void { + textureArray.deinit(); + emissionTextureArray.deinit(); + ParticleSystem.deinit(); + particleTypesSSBO.deinit(); + } + + pub fn reset() void { + types = .{}; + typesLocal = .{}; + textures = .{}; + emissionTextures = .{}; + particleTypeHashmap = .{}; + ParticleSystem.reset(); + } + + pub fn register(assetsFolder: []const u8, id: []const u8, zon: ZonElement) void { + const textureId = zon.get(?[]const u8, "texture", null) orelse { + std.log.err("Particle texture id was not specified for {s} ({s})", .{ id, assetsFolder }); + return; + }; + + const particleType = readTextureDataAndParticleType(assetsFolder, textureId); + var rotVel: RandomRange(f32) = RandomRange(f32).fromZon(zon.getChild("rotationVelocity")) orelse .init(20, 60); + rotVel.min = std.math.degreesToRadians(rotVel.min); + rotVel.max = std.math.degreesToRadians(rotVel.max); + const particleTypeLocal = ParticleTypeLocal{ + .density = RandomRange(f32).fromZon(zon.getChild("density")) orelse .init(2, 3), + .rotVel = rotVel, + .dragCoefficient = RandomRange(f32).fromZon(zon.getChild("dragCoefficient")) orelse .init(0.5, 0.6), + }; + + particleTypeHashmap.put(main.worldArena.allocator, id, @intCast(types.items.len)) catch unreachable; + types.append(main.worldArena, particleType); + typesLocal.append(main.worldArena, particleTypeLocal); + + std.log.debug("Registered particle type: {s}", .{id}); + } + + fn readTextureDataAndParticleType(assetsFolder: []const u8, textureId: []const u8) ParticleType { + var typ: ParticleType = undefined; + + const base = readTexture(assetsFolder, textureId, ".png", Image.defaultImage, .isMandatory); + const emission = readTexture(assetsFolder, textureId, "_emission.png", Image.emptyImage, .isOptional); + const hasEmission = (emission.imageData.ptr != Image.emptyImage.imageData.ptr); + const baseAnimationFrameCount = base.height / base.width; + const emissionAnimationFrameCount = emission.height / emission.width; + + typ.frameCount = @floatFromInt(baseAnimationFrameCount); + typ.startFrame = @floatFromInt(textures.items.len); + typ.size = @as(f32, @floatFromInt(base.width)) / 16; + + var isBaseBroken = false; + var isEmissionBroken = false; + + if (base.height % base.width != 0) { + std.log.err("Particle base texture has incorrect dimensions ({}x{}) expected height to be multiple of width for {s} ({s})", .{ base.width, base.height, textureId, assetsFolder }); + isBaseBroken = true; + } + if (hasEmission and emission.height % emission.width != 0) { + std.log.err("Particle emission texture has incorrect dimensions ({}x{}) expected height to be multiple of width for {s} ({s})", .{ base.width, base.height, textureId, assetsFolder }); + isEmissionBroken = true; + } + if (hasEmission and baseAnimationFrameCount != emissionAnimationFrameCount) { + std.log.err("Particle base texture and emission texture frame count mismatch ({} vs {}) for {s} ({s})", .{ baseAnimationFrameCount, emissionAnimationFrameCount, textureId, assetsFolder }); + isEmissionBroken = true; + } + + createAnimationFrames(&textures, baseAnimationFrameCount, base, isBaseBroken); + createAnimationFrames(&emissionTextures, baseAnimationFrameCount, emission, isBaseBroken or isEmissionBroken or !hasEmission); + + return typ; + } + + fn readTexture(assetsFolder: []const u8, textureId: []const u8, suffix: []const u8, default: graphics.Image, status: enum { isOptional, isMandatory }) graphics.Image { + var splitter = std.mem.splitScalar(u8, textureId, ':'); + const mod = splitter.first(); + const id = splitter.rest(); + + const gameAssetsPath = std.fmt.allocPrint(main.stackAllocator.allocator, "assets/{s}/particles/textures/{s}{s}", .{ mod, id, suffix }) catch unreachable; + defer main.stackAllocator.free(gameAssetsPath); + + const worldAssetsPath = std.fmt.allocPrint(main.stackAllocator.allocator, "{s}/{s}/particles/textures/{s}{s}", .{ assetsFolder, mod, id, suffix }) catch unreachable; + defer main.stackAllocator.free(worldAssetsPath); + + return graphics.Image.readFromFile(main.worldArena, worldAssetsPath) catch graphics.Image.readFromFile(main.worldArena, gameAssetsPath) catch { + if (status == .isMandatory) std.log.err("Particle texture not found in {s} and {s}.", .{ worldAssetsPath, gameAssetsPath }); + return default; + }; + } + + fn createAnimationFrames(container: *main.ListUnmanaged(Image), frameCount: usize, image: Image, isBroken: bool) void { + for (0..frameCount) |i| { + container.append(main.worldArena, if (isBroken) image else extractAnimationSlice(image, i)); + } + } + + fn extractAnimationSlice(image: Image, frameIndex: usize) Image { + const frameCount = image.height / image.width; + const frameHeight = image.height / frameCount; + const startHeight = frameHeight * frameIndex; + const endHeight = frameHeight * (frameIndex + 1); + var result = image; + result.height = @intCast(frameHeight); + result.imageData = result.imageData[startHeight * image.width .. endHeight * image.width]; + return result; + } + + pub fn generateTextureArray() void { + textureArray.generate(textures.items, true, true); + emissionTextureArray.generate(emissionTextures.items, true, false); + + particleTypesSSBO.bufferData(ParticleType, ParticleManager.types.items); + particleTypesSSBO.bind(14); + } }; pub const ParticleSystem = struct { - pub const maxCapacity: u32 = 524288; - var particleCount: u32 = 0; - var particles: [maxCapacity]Particle = undefined; - var particlesLocal: [maxCapacity]ParticleLocal = undefined; - var previousPlayerPos: Vec3d = undefined; - - var mutex: main.utils.Mutex = .{}; - var networkCreationQueue: main.ListUnmanaged(struct { emitter: Emitter, pos: Vec3d, count: u32 }) = .{}; - - var particlesSSBO: SSBO = undefined; - - var pipeline: graphics.Pipeline = undefined; - const UniformStruct = struct { - projectionAndViewMatrix: c_int, - billboardMatrix: c_int, - ambientLight: c_int, - }; - var uniforms: UniformStruct = undefined; - - fn init() void { - pipeline = graphics.Pipeline.init( - "assets/cubyz/shaders/particles/particles.vert", - "assets/cubyz/shaders/particles/particles.frag", - "", - &uniforms, - graphics.VertexArray.EmptyVertex, - &.{}, - .{}, - .{.depthTest = true, .depthWrite = true}, - .{.attachments = &.{.noBlending}}, - ); - - particlesSSBO = SSBO.init(); - particlesSSBO.createDynamicBuffer(Particle, maxCapacity); - particlesSSBO.bind(13); - } - - fn deinit() void { - pipeline.deinit(); - particlesSSBO.deinit(); - } - - fn reset() void { - networkCreationQueue = .{}; - } - - pub fn update(deltaTime: f32) void { - mutex.lock(); - if (networkCreationQueue.items.len != 0) { - for (networkCreationQueue.items) |creation| { - creation.emitter.spawnParticles(creation.pos, creation.count); - } - networkCreationQueue.clearRetainingCapacity(); - } - mutex.unlock(); - - const vecDeltaTime: Vec4f = @as(Vec4f, @splat(deltaTime)); - const playerPos = game.Player.getEyePosBlocking(); - const prevPlayerPosDifference: Vec3f = @floatCast(previousPlayerPos - playerPos); - - var i: u32 = 0; - while (i < particleCount) { - const particle = &particles[i]; - const particleLocal = &particlesLocal[i]; - particle.lifeRatio -= particleLocal.lifeVelocity*deltaTime; - if (particle.lifeRatio < 0) { - particleCount -= 1; - particles[i] = particles[particleCount]; - particlesLocal[i] = particlesLocal[particleCount]; - continue; - } - - var pos: Vec3f = particle.pos; - var rot = particle.rot; - const rotVel = particleLocal.velAndRotationVel[3]; - rot += rotVel*deltaTime; - - const airDensity: f32 = physics.airDensity; - const frictionCoefficient = physics.baseGravity/physics.airTerminalVelocity*particleLocal.dragCoefficient; - particleLocal.velAndRotationVel[3] = 0; - const effectiveGravity: f32 = @floatCast(physics.baseGravity*(particleLocal.density - airDensity)/particleLocal.density); - particleLocal.velAndRotationVel[2] -= effectiveGravity*deltaTime; - particleLocal.velAndRotationVel *= @splat(@exp(-frictionCoefficient*deltaTime)); - - if (particleLocal.collides) { - var v3Pos = playerPos + @as(Vec3d, @floatCast(pos + prevPlayerPosDifference)); - const size = ParticleManager.types.items[particle.typ].size; - const hitBox: physics.collision.Box = .{.min = @splat(size*-0.5), .max = @splat(size*0.5)}; - - const posDelta = particleLocal.velAndRotationVel*vecDeltaTime; - - v3Pos[0] += posDelta[0]; - if (physics.collision.collides(.client, .x, -posDelta[0], v3Pos, hitBox)) |box| { - v3Pos[0] = if (posDelta[0] < 0) - box.max[0] - hitBox.min[0] - else - box.min[0] - hitBox.max[0]; - } - v3Pos[1] += posDelta[1]; - if (physics.collision.collides(.client, .y, -posDelta[1], v3Pos, hitBox)) |box| { - v3Pos[1] = if (posDelta[1] < 0) - box.max[1] - hitBox.min[1] - else - box.min[1] - hitBox.max[1]; - } - v3Pos[2] += posDelta[2]; - if (physics.collision.collides(.client, .z, -posDelta[2], v3Pos, hitBox)) |box| { - v3Pos[2] = if (posDelta[2] < 0) - box.max[2] - hitBox.min[2] - else - box.min[2] - hitBox.max[2]; - } - pos = @as(Vec3f, @floatCast(v3Pos - playerPos)); - } else { - const posDelta = particleLocal.velAndRotationVel*vecDeltaTime; - - pos += Vec3f{posDelta[0], posDelta[1], posDelta[2]} + prevPlayerPosDifference; - } - - particle.pos = pos; - particle.rot = rot; - particleLocal.velAndRotationVel[3] = rotVel; - - const positionf64 = @as(Vec3d, @floatCast(pos)) + playerPos; - const intPos: vec.Vec3i = @floor(positionf64); - const light: [6]u8 = main.renderer.mesh_storage.getLight(intPos[0], intPos[1], intPos[2]) orelse @splat(0); - const compressedLight = - @as(u32, light[0] >> 3) << 25 | - @as(u32, light[1] >> 3) << 20 | - @as(u32, light[2] >> 3) << 15 | - @as(u32, light[3] >> 3) << 10 | - @as(u32, light[4] >> 3) << 5 | - @as(u32, light[5] >> 3); - particle.light = compressedLight; - - i += 1; - } - previousPlayerPos = playerPos; - } - - fn addParticle(typ: u32, particleType: ParticleTypeLocal, pos: Vec3d, vel: Vec3f, collides: bool, properties: EmitterProperties) void { - const lifeTime = properties.lifeTime.get(&main.seed); - const density = particleType.density.get(&main.seed); - const rot = if (properties.randomizeRotation) random.nextFloat(&main.seed)*std.math.pi*2 else 0; - const rotVel = particleType.rotVel.get(&main.seed); - const dragCoeff = particleType.dragCoefficient.get(&main.seed); - - particles[particleCount] = Particle{ - .pos = @as(Vec3f, @floatCast(pos - previousPlayerPos)), - .rot = rot, - .typ = typ, - }; - particlesLocal[particleCount] = ParticleLocal{ - .velAndRotationVel = vec.combine(vel, rotVel), - .lifeVelocity = 1/lifeTime, - .density = density, - .dragCoefficient = dragCoeff, - .collides = collides, - }; - particleCount += 1; - } - - pub fn render(projectionMatrix: Mat4f, viewMatrix: Mat4f, ambientLight: Vec3f) void { - particlesSSBO.bufferSubData(Particle, &particles, particleCount); - - pipeline.bind(null); - - const projectionAndViewMatrix = Mat4f.mul(projectionMatrix, viewMatrix); - c.glUniformMatrix4fv(uniforms.projectionAndViewMatrix, 1, c.GL_TRUE, @ptrCast(&projectionAndViewMatrix)); - c.glUniform3fv(uniforms.ambientLight, 1, @ptrCast(&ambientLight)); - - const billboardMatrix = Mat4f.rotationZ(-game.camera.rotation[2] + std.math.pi*0.5) - .mul(Mat4f.rotationY(game.camera.rotation[0] - std.math.pi*0.5)); - c.glUniformMatrix4fv(uniforms.billboardMatrix, 1, c.GL_TRUE, @ptrCast(&billboardMatrix)); - - c.glActiveTexture(c.GL_TEXTURE0); - ParticleManager.textureArray.bind(); - c.glActiveTexture(c.GL_TEXTURE1); - ParticleManager.emissionTextureArray.bind(); - - chunk_meshing.vao.bind(); - - const maxQuads = chunk_meshing.maxQuadsInIndexBuffer; - const count = std.math.divCeil(u32, particleCount, maxQuads) catch unreachable; - for (0..count) |i| { - const particleOffset = (maxQuads*4)*i; - const particleCurrentCount: u32 = @min(maxQuads, particleCount - maxQuads*i); - c.glDrawElementsBaseVertex(c.GL_TRIANGLES, @intCast(particleCurrentCount*6), c.GL_UNSIGNED_INT, null, @intCast(particleOffset)); - } - } - - pub fn getParticleCount() u32 { - return particleCount; - } - - pub fn addParticlesFromNetwork(emitter: Emitter, pos: Vec3d, count: u32) void { - mutex.lock(); - defer mutex.unlock(); - networkCreationQueue.append(main.worldArena, .{.emitter = emitter, .pos = pos, .count = count}); - } + pub const maxCapacity: u32 = 524288; + var particleCount: u32 = 0; + var particles: [maxCapacity]Particle = undefined; + var particlesLocal: [maxCapacity]ParticleLocal = undefined; + var previousPlayerPos: Vec3d = undefined; + + var mutex: main.utils.Mutex = .{}; + var networkCreationQueue: main.ListUnmanaged(struct { emitter: Emitter, pos: Vec3d, count: u32 }) = .{}; + + var particlesSSBO: SSBO = undefined; + + var pipeline: graphics.Pipeline = undefined; + const UniformStruct = struct { + projectionAndViewMatrix: c_int, + billboardMatrix: c_int, + ambientLight: c_int, + }; + var uniforms: UniformStruct = undefined; + + fn init() void { + pipeline = graphics.Pipeline.init( + "assets/cubyz/shaders/particles/particles.vert", + "assets/cubyz/shaders/particles/particles.frag", + "", + &uniforms, + graphics.VertexArray.EmptyVertex, + &.{}, + .{}, + .{ .depthTest = true, .depthWrite = true }, + .{ .attachments = &.{.noBlending} }, + ); + + particlesSSBO = SSBO.init(); + particlesSSBO.createDynamicBuffer(Particle, maxCapacity); + particlesSSBO.bind(13); + } + + fn deinit() void { + pipeline.deinit(); + particlesSSBO.deinit(); + } + + fn reset() void { + networkCreationQueue = .{}; + } + + pub fn update(deltaTime: f32) void { + mutex.lock(); + if (networkCreationQueue.items.len != 0) { + for (networkCreationQueue.items) |creation| { + creation.emitter.spawnParticles(creation.pos, creation.count); + } + networkCreationQueue.clearRetainingCapacity(); + } + mutex.unlock(); + + const vecDeltaTime: Vec4f = @as(Vec4f, @splat(deltaTime)); + const playerPos = game.Player.getEyePosBlocking(); + const prevPlayerPosDifference: Vec3f = @floatCast(previousPlayerPos - playerPos); + + var i: u32 = 0; + while (i < particleCount) { + const particle = &particles[i]; + const particleLocal = &particlesLocal[i]; + particle.lifeRatio -= particleLocal.lifeVelocity * deltaTime; + if (particle.lifeRatio < 0) { + particleCount -= 1; + particles[i] = particles[particleCount]; + particlesLocal[i] = particlesLocal[particleCount]; + continue; + } + + var pos: Vec3f = particle.pos; + var rot = particle.rot; + const rotVel = particleLocal.velAndRotationVel[3]; + rot += rotVel * deltaTime; + + const airDensity: f32 = physics.airDensity; + const frictionCoefficient = physics.baseGravity / physics.airTerminalVelocity * particleLocal.dragCoefficient; + particleLocal.velAndRotationVel[3] = 0; + const effectiveGravity: f32 = @floatCast(physics.baseGravity * (particleLocal.density - airDensity) / particleLocal.density); + particleLocal.velAndRotationVel[2] -= effectiveGravity * deltaTime; + particleLocal.velAndRotationVel *= @splat(@exp(-frictionCoefficient * deltaTime)); + + if (particleLocal.collides) { + var v3Pos = playerPos + @as(Vec3d, @floatCast(pos + prevPlayerPosDifference)); + const size = ParticleManager.types.items[particle.typ].size; + const hitBox: physics.collision.Box = .{ .min = @splat(size * -0.5), .max = @splat(size * 0.5) }; + + const posDelta = particleLocal.velAndRotationVel * vecDeltaTime; + + v3Pos[0] += posDelta[0]; + if (physics.collision.collides(.client, .x, -posDelta[0], v3Pos, hitBox)) |box| { + v3Pos[0] = if (posDelta[0] < 0) + box.max[0] - hitBox.min[0] + else + box.min[0] - hitBox.max[0]; + } + v3Pos[1] += posDelta[1]; + if (physics.collision.collides(.client, .y, -posDelta[1], v3Pos, hitBox)) |box| { + v3Pos[1] = if (posDelta[1] < 0) + box.max[1] - hitBox.min[1] + else + box.min[1] - hitBox.max[1]; + } + v3Pos[2] += posDelta[2]; + if (physics.collision.collides(.client, .z, -posDelta[2], v3Pos, hitBox)) |box| { + v3Pos[2] = if (posDelta[2] < 0) + box.max[2] - hitBox.min[2] + else + box.min[2] - hitBox.max[2]; + } + pos = @as(Vec3f, @floatCast(v3Pos - playerPos)); + } else { + const posDelta = particleLocal.velAndRotationVel * vecDeltaTime; + + pos += Vec3f{ posDelta[0], posDelta[1], posDelta[2] } + prevPlayerPosDifference; + } + + particle.pos = pos; + particle.rot = rot; + particleLocal.velAndRotationVel[3] = rotVel; + + const positionf64 = @as(Vec3d, @floatCast(pos)) + playerPos; + const intPos: vec.Vec3i = @floor(positionf64); + const light: [6]u8 = main.renderer.mesh_storage.getLight(intPos[0], intPos[1], intPos[2]) orelse @splat(0); + const compressedLight = + @as(u32, light[0] >> 3) << 25 | + @as(u32, light[1] >> 3) << 20 | + @as(u32, light[2] >> 3) << 15 | + @as(u32, light[3] >> 3) << 10 | + @as(u32, light[4] >> 3) << 5 | + @as(u32, light[5] >> 3); + particle.light = compressedLight; + + i += 1; + } + previousPlayerPos = playerPos; + } + + fn addParticle(typ: u32, particleType: ParticleTypeLocal, pos: Vec3d, vel: Vec3f, collides: bool, properties: EmitterProperties) void { + const lifeTime = properties.lifeTime.get(&main.seed); + const density = particleType.density.get(&main.seed); + const rot = if (properties.randomizeRotation) random.nextFloat(&main.seed) * std.math.pi * 2 else 0; + const rotVel = particleType.rotVel.get(&main.seed); + const dragCoeff = particleType.dragCoefficient.get(&main.seed); + + particles[particleCount] = Particle{ + .pos = @as(Vec3f, @floatCast(pos - previousPlayerPos)), + .rot = rot, + .typ = typ, + }; + particlesLocal[particleCount] = ParticleLocal{ + .velAndRotationVel = vec.combine(vel, rotVel), + .lifeVelocity = 1 / lifeTime, + .density = density, + .dragCoefficient = dragCoeff, + .collides = collides, + }; + particleCount += 1; + } + + pub fn render(projectionMatrix: Mat4f, viewMatrix: Mat4f, ambientLight: Vec3f) void { + particlesSSBO.bufferSubData(Particle, &particles, particleCount); + + pipeline.bind(null); + + const projectionAndViewMatrix = Mat4f.mul(projectionMatrix, viewMatrix); + c.glUniformMatrix4fv(uniforms.projectionAndViewMatrix, 1, c.GL_TRUE, @ptrCast(&projectionAndViewMatrix)); + c.glUniform3fv(uniforms.ambientLight, 1, @ptrCast(&ambientLight)); + + const billboardMatrix = Mat4f.rotationZ(-game.camera.rotation[2] + std.math.pi * 0.5) + .mul(Mat4f.rotationY(game.camera.rotation[0] - std.math.pi * 0.5)); + c.glUniformMatrix4fv(uniforms.billboardMatrix, 1, c.GL_TRUE, @ptrCast(&billboardMatrix)); + + c.glActiveTexture(c.GL_TEXTURE0); + ParticleManager.textureArray.bind(); + c.glActiveTexture(c.GL_TEXTURE1); + ParticleManager.emissionTextureArray.bind(); + + chunk_meshing.vao.bind(); + + const maxQuads = chunk_meshing.maxQuadsInIndexBuffer; + const count = std.math.divCeil(u32, particleCount, maxQuads) catch unreachable; + for (0..count) |i| { + const particleOffset = (maxQuads * 4) * i; + const particleCurrentCount: u32 = @min(maxQuads, particleCount - maxQuads * i); + c.glDrawElementsBaseVertex(c.GL_TRIANGLES, @intCast(particleCurrentCount * 6), c.GL_UNSIGNED_INT, null, @intCast(particleOffset)); + } + } + + pub fn getParticleCount() u32 { + return particleCount; + } + + pub fn addParticlesFromNetwork(emitter: Emitter, pos: Vec3d, count: u32) void { + mutex.lock(); + defer mutex.unlock(); + networkCreationQueue.append(main.worldArena, .{ .emitter = emitter, .pos = pos, .count = count }); + } }; pub const EmitterProperties = struct { - speed: RandomRange(f32), - lifeTime: RandomRange(f32), - randomizeRotation: bool, - - pub fn parse(zon: ZonElement) EmitterProperties { - return EmitterProperties{ - .speed = RandomRange(f32).fromZon(zon.getChild("speed")) orelse .init(1, 1.5), - .lifeTime = RandomRange(f32).fromZon(zon.getChild("lifeTime")) orelse .init(0.75, 1), - .randomizeRotation = zon.get(bool, "randomRotate", true), - }; - } + speed: RandomRange(f32), + lifeTime: RandomRange(f32), + randomizeRotation: bool, + + pub fn parse(zon: ZonElement) EmitterProperties { + return EmitterProperties{ + .speed = RandomRange(f32).fromZon(zon.getChild("speed")) orelse .init(1, 1.5), + .lifeTime = RandomRange(f32).fromZon(zon.getChild("lifeTime")) orelse .init(0.75, 1), + .randomizeRotation = zon.get(bool, "randomRotate", true), + }; + } }; pub const DirectionMode = union(enum) { - // The particle goes in the direction away from the center - spread: void, - // The particle goes in a random direction - scatter: void, - // The particle goes in the specified direction - direction: Vec3f, - - pub fn parse(zon: ZonElement) !DirectionMode { - const dirModeName = zon.get([]const u8, "mode", @tagName(DirectionMode.spread)); - const dirMode = std.meta.stringToEnum(std.meta.Tag(DirectionMode), dirModeName) orelse return error.InvalidDirectionMode; - return switch (dirMode) { - .direction => .{.direction = zon.get(Vec3f, "direction", .{0, 0, 1})}, - inline else => |mode| @unionInit(DirectionMode, @tagName(mode), {}), - }; - } + // The particle goes in the direction away from the center + spread: void, + // The particle goes in a random direction + scatter: void, + // The particle goes in the specified direction + direction: Vec3f, + + pub fn parse(zon: ZonElement) !DirectionMode { + const dirModeName = zon.get([]const u8, "mode", @tagName(DirectionMode.spread)); + const dirMode = std.meta.stringToEnum(std.meta.Tag(DirectionMode), dirModeName) orelse return error.InvalidDirectionMode; + return switch (dirMode) { + .direction => .{ .direction = zon.get(Vec3f, "direction", .{ 0, 0, 1 }) }, + inline else => |mode| @unionInit(DirectionMode, @tagName(mode), {}), + }; + } }; pub const Emitter = struct { - typ: u16 = 0, - particleType: ParticleTypeLocal, - collides: bool, - spawnShape: SpawnShape, - properties: EmitterProperties, - mode: DirectionMode, - - pub const SpawnShape = union(enum) { - point: SpawnPoint, - sphere: SpawnSphere, - cube: SpawnCube, - - pub fn spawn(self: SpawnShape, pos: Vec3d, properties: EmitterProperties, mode: DirectionMode) struct { Vec3d, Vec3f } { - return switch (self) { - inline else => |shape| shape.spawn(pos, properties, mode), - }; - } - - pub fn parse(zon: ZonElement) !SpawnShape { - const typeZon = zon.get([]const u8, "shape", @tagName(SpawnShape.point)); - const spawnType = std.meta.stringToEnum(std.meta.Tag(SpawnShape), typeZon) orelse return error.InvalidType; - return switch (spawnType) { - inline else => |shape| @unionInit(SpawnShape, @tagName(shape), try @FieldType(SpawnShape, @tagName(shape)).parse(zon)), - }; - } - }; - - pub const SpawnPoint = struct { - pub fn spawn(_: SpawnPoint, pos: Vec3d, properties: EmitterProperties, mode: DirectionMode) struct { Vec3d, Vec3f } { - const particlePos = pos; - const speed: Vec3f = @splat(properties.speed.get(&main.seed)); - const dir: Vec3f = switch (mode) { - .direction => |dir| vec.normalize(dir), - .scatter, .spread => vec.normalize(random.nextFloatVectorSigned(3, &main.seed)), - }; - const particleVel = dir*speed; - - return .{particlePos, particleVel}; - } - - pub fn parse(_: ZonElement) !SpawnPoint { - return SpawnPoint{}; - } - }; - - pub const SpawnSphere = struct { - radius: f32, - - pub fn spawn(self: SpawnSphere, pos: Vec3d, properties: EmitterProperties, mode: DirectionMode) struct { Vec3d, Vec3f } { - const spawnPos: Vec3f = @splat(self.radius); - var offsetPos: Vec3f = undefined; - while (true) { - offsetPos = random.nextFloatVectorSigned(3, &main.seed); - if (vec.lengthSquare(offsetPos) <= 1) break; - } - const particlePos = pos + @as(Vec3d, @floatCast(offsetPos*spawnPos)); - const speed: Vec3f = @splat(properties.speed.get(&main.seed)); - const dir: Vec3f = switch (mode) { - .direction => |dir| vec.normalize(dir), - .scatter => vec.normalize(random.nextFloatVectorSigned(3, &main.seed)), - .spread => @floatCast(offsetPos), - }; - const particleVel = dir*speed; - - return .{particlePos, particleVel}; - } - - pub fn parse(zon: ZonElement) !SpawnSphere { - return SpawnSphere{ - .radius = zon.get(f32, "radius", 1), - }; - } - }; - - pub const SpawnCube = struct { - size: Vec3f, - - pub fn spawn(self: SpawnCube, pos: Vec3d, properties: EmitterProperties, mode: DirectionMode) struct { Vec3d, Vec3f } { - const spawnPos: Vec3f = self.size; - const offsetPos: Vec3f = random.nextFloatVectorSigned(3, &main.seed); - const particlePos = pos + @as(Vec3d, @floatCast(offsetPos*spawnPos)); - const speed: Vec3f = @splat(properties.speed.get(&main.seed)); - const dir: Vec3f = switch (mode) { - .direction => |dir| vec.normalize(dir), - .scatter => vec.normalize(random.nextFloatVectorSigned(3, &main.seed)), - .spread => vec.normalize(@as(Vec3f, @floatCast(offsetPos))), - }; - const particleVel = dir*speed; - - return .{particlePos, particleVel}; - } - - pub fn parse(zon: ZonElement) !SpawnCube { - return SpawnCube{ - .size = zon.get(?Vec3f, "size", null) orelse @splat(zon.get(f32, "size", 1)), - }; - } - }; - - pub fn init(id: []const u8, collides: bool, spawnShape: SpawnShape, properties: EmitterProperties, mode: DirectionMode) Emitter { - const typ = ParticleManager.particleTypeHashmap.get(id) orelse 0; - - return Emitter{ - .typ = typ, - .particleType = ParticleManager.typesLocal.items[typ], - .collides = collides, - .spawnShape = spawnShape, - .properties = properties, - .mode = mode, - }; - } - - pub fn initFromZon(id: []const u8, collides: bool, zon: ZonElement) Emitter { - const typ = ParticleManager.particleTypeHashmap.get(id) orelse 0; - const mode = DirectionMode.parse(zon) catch |err| blk: { - std.log.err("Error while parsing direction mode: \"{s}\"", .{@errorName(err)}); - break :blk .spread; - }; - const spawnShape = Emitter.SpawnShape.parse(zon) catch |err| blk: { - std.log.err("Error while parsing particle spawn data: \"{s}\"", .{@errorName(err)}); - break :blk Emitter.SpawnShape{.point = .{}}; - }; - - return Emitter{ - .typ = typ, - .particleType = ParticleManager.typesLocal.items[typ], - .collides = collides, - .spawnShape = spawnShape, - .properties = EmitterProperties.parse(zon), - .mode = mode, - }; - } - - pub fn spawnParticles(self: Emitter, pos: Vec3d, spawnCount: u32) void { - const count = @min(spawnCount, ParticleSystem.maxCapacity - ParticleSystem.particleCount); - for (0..count) |_| { - const particlePos, const particleVel = self.spawnShape.spawn(pos, self.properties, self.mode); - - ParticleSystem.addParticle(self.typ, self.particleType, particlePos, particleVel, self.collides, self.properties); - } - } + typ: u16 = 0, + particleType: ParticleTypeLocal, + collides: bool, + spawnShape: SpawnShape, + properties: EmitterProperties, + mode: DirectionMode, + + pub const SpawnShape = union(enum) { + point: SpawnPoint, + sphere: SpawnSphere, + cube: SpawnCube, + + pub fn spawn(self: SpawnShape, pos: Vec3d, properties: EmitterProperties, mode: DirectionMode) struct { Vec3d, Vec3f } { + return switch (self) { + inline else => |shape| shape.spawn(pos, properties, mode), + }; + } + + pub fn parse(zon: ZonElement) !SpawnShape { + const typeZon = zon.get([]const u8, "shape", @tagName(SpawnShape.point)); + const spawnType = std.meta.stringToEnum(std.meta.Tag(SpawnShape), typeZon) orelse return error.InvalidType; + return switch (spawnType) { + inline else => |shape| @unionInit(SpawnShape, @tagName(shape), try @FieldType(SpawnShape, @tagName(shape)).parse(zon)), + }; + } + }; + + pub const SpawnPoint = struct { + pub fn spawn(_: SpawnPoint, pos: Vec3d, properties: EmitterProperties, mode: DirectionMode) struct { Vec3d, Vec3f } { + const particlePos = pos; + const speed: Vec3f = @splat(properties.speed.get(&main.seed)); + const dir: Vec3f = switch (mode) { + .direction => |dir| vec.normalize(dir), + .scatter, .spread => vec.normalize(random.nextFloatVectorSigned(3, &main.seed)), + }; + const particleVel = dir * speed; + + return .{ particlePos, particleVel }; + } + + pub fn parse(_: ZonElement) !SpawnPoint { + return SpawnPoint{}; + } + }; + + pub const SpawnSphere = struct { + radius: f32, + + pub fn spawn(self: SpawnSphere, pos: Vec3d, properties: EmitterProperties, mode: DirectionMode) struct { Vec3d, Vec3f } { + const spawnPos: Vec3f = @splat(self.radius); + var offsetPos: Vec3f = undefined; + while (true) { + offsetPos = random.nextFloatVectorSigned(3, &main.seed); + if (vec.lengthSquare(offsetPos) <= 1) break; + } + const particlePos = pos + @as(Vec3d, @floatCast(offsetPos * spawnPos)); + const speed: Vec3f = @splat(properties.speed.get(&main.seed)); + const dir: Vec3f = switch (mode) { + .direction => |dir| vec.normalize(dir), + .scatter => vec.normalize(random.nextFloatVectorSigned(3, &main.seed)), + .spread => @floatCast(offsetPos), + }; + const particleVel = dir * speed; + + return .{ particlePos, particleVel }; + } + + pub fn parse(zon: ZonElement) !SpawnSphere { + return SpawnSphere{ + .radius = zon.get(f32, "radius", 1), + }; + } + }; + + pub const SpawnCube = struct { + size: Vec3f, + + pub fn spawn(self: SpawnCube, pos: Vec3d, properties: EmitterProperties, mode: DirectionMode) struct { Vec3d, Vec3f } { + const spawnPos: Vec3f = self.size; + const offsetPos: Vec3f = random.nextFloatVectorSigned(3, &main.seed); + const particlePos = pos + @as(Vec3d, @floatCast(offsetPos * spawnPos)); + const speed: Vec3f = @splat(properties.speed.get(&main.seed)); + const dir: Vec3f = switch (mode) { + .direction => |dir| vec.normalize(dir), + .scatter => vec.normalize(random.nextFloatVectorSigned(3, &main.seed)), + .spread => vec.normalize(@as(Vec3f, @floatCast(offsetPos))), + }; + const particleVel = dir * speed; + + return .{ particlePos, particleVel }; + } + + pub fn parse(zon: ZonElement) !SpawnCube { + return SpawnCube{ + .size = zon.get(?Vec3f, "size", null) orelse @splat(zon.get(f32, "size", 1)), + }; + } + }; + + pub fn init(id: []const u8, collides: bool, spawnShape: SpawnShape, properties: EmitterProperties, mode: DirectionMode) Emitter { + const typ = ParticleManager.particleTypeHashmap.get(id) orelse 0; + + return Emitter{ + .typ = typ, + .particleType = ParticleManager.typesLocal.items[typ], + .collides = collides, + .spawnShape = spawnShape, + .properties = properties, + .mode = mode, + }; + } + + pub fn initFromZon(id: []const u8, collides: bool, zon: ZonElement) Emitter { + const typ = ParticleManager.particleTypeHashmap.get(id) orelse 0; + const mode = DirectionMode.parse(zon) catch |err| blk: { + std.log.err("Error while parsing direction mode: \"{s}\"", .{@errorName(err)}); + break :blk .spread; + }; + const spawnShape = Emitter.SpawnShape.parse(zon) catch |err| blk: { + std.log.err("Error while parsing particle spawn data: \"{s}\"", .{@errorName(err)}); + break :blk Emitter.SpawnShape{ .point = .{} }; + }; + + return Emitter{ + .typ = typ, + .particleType = ParticleManager.typesLocal.items[typ], + .collides = collides, + .spawnShape = spawnShape, + .properties = EmitterProperties.parse(zon), + .mode = mode, + }; + } + + pub fn spawnParticles(self: Emitter, pos: Vec3d, spawnCount: u32) void { + const count = @min(spawnCount, ParticleSystem.maxCapacity - ParticleSystem.particleCount); + for (0..count) |_| { + const particlePos, const particleVel = self.spawnShape.spawn(pos, self.properties, self.mode); + + ParticleSystem.addParticle(self.typ, self.particleType, particlePos, particleVel, self.collides, self.properties); + } + } }; pub const ParticleType = struct { - frameCount: f32, - startFrame: f32, - size: f32, + frameCount: f32, + startFrame: f32, + size: f32, }; pub const ParticleTypeLocal = struct { - density: RandomRange(f32), - rotVel: RandomRange(f32), - dragCoefficient: RandomRange(f32), + density: RandomRange(f32), + rotVel: RandomRange(f32), + dragCoefficient: RandomRange(f32), }; pub const Particle = extern struct { - pos: [3]f32 align(16), - rot: f32 = 0, - lifeRatio: f32 = 1, - light: u32 = 0, - typ: u32, - // 4 bytes left for use + pos: [3]f32 align(16), + rot: f32 = 0, + lifeRatio: f32 = 1, + light: u32 = 0, + typ: u32, + // 4 bytes left for use }; pub const ParticleLocal = struct { - velAndRotationVel: Vec4f, - lifeVelocity: f32, - density: f32, - dragCoefficient: f32, - collides: bool, + velAndRotationVel: Vec4f, + lifeVelocity: f32, + density: f32, + dragCoefficient: f32, + collides: bool, }; diff --git a/src/renderer.zig b/src/renderer.zig index 70cd57abba..b469c8025e 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -6,22 +6,16 @@ const chunk = @import("chunk.zig"); const entity = @import("entity.zig"); const graphics = @import("graphics.zig"); const particles = @import("particles.zig"); -const c = @import("c"); - const game = @import("game.zig"); const World = game.World; - const itemdrop = @import("itemdrop.zig"); - const main = @import("main"); const gpu_performance_measuring = main.gui.windowlist.gpu_performance_measuring; const crosshair = main.gui.windowlist.crosshair; const Window = main.Window; - const models = @import("models.zig"); const network = @import("network.zig"); const settings = @import("settings.zig"); - const vec = @import("vec.zig"); const Vec2f = vec.Vec2f; const Vec3i = vec.Vec3i; @@ -30,6 +24,8 @@ const Vec3d = vec.Vec3d; const Vec4f = vec.Vec4f; const Mat4f = vec.Mat4f; +const c = @import("c"); + pub const chunk_meshing = @import("renderer/chunk_meshing.zig"); pub const lighting = @import("renderer/lighting.zig"); pub const mesh_storage = @import("renderer/mesh_storage.zig"); @@ -41,24 +37,24 @@ pub const zFar = 65536.0; // TODO: Fix z-fighting problems. var deferredRenderPassPipeline: graphics.Pipeline = undefined; var deferredUniforms: struct { - @"fog.color": c_int, - @"fog.density": c_int, - @"fog.fogLower": c_int, - @"fog.fogHigher": c_int, - tanXY: c_int, - zNear: c_int, - zFar: c_int, - invViewMatrix: c_int, - playerPositionInteger: c_int, - playerPositionFraction: c_int, + @"fog.color": c_int, + @"fog.density": c_int, + @"fog.fogLower": c_int, + @"fog.fogHigher": c_int, + tanXY: c_int, + zNear: c_int, + zFar: c_int, + invViewMatrix: c_int, + playerPositionInteger: c_int, + playerPositionFraction: c_int, } = undefined; var fakeReflectionPipeline: graphics.Pipeline = undefined; var fakeReflectionUniforms: struct { - normalVector: c_int, - upVector: c_int, - rightVector: c_int, - frequency: c_int, - reflectionMapSize: c_int, + normalVector: c_int, + upVector: c_int, + rightVector: c_int, + frequency: c_int, + reflectionMapSize: c_int, } = undefined; pub var activeFrameBuffer: c_uint = 0; @@ -67,71 +63,71 @@ pub const reflectionCubeMapSize = 64; var reflectionCubeMap: graphics.CubeMapTexture = undefined; pub fn init() void { - deferredRenderPassPipeline = graphics.Pipeline.init( - "assets/cubyz/shaders/deferred_render_pass.vert", - "assets/cubyz/shaders/deferred_render_pass.frag", - "", - &deferredUniforms, - graphics.draw.SimpleVertex2D, - &.{}, - .{.cullMode = .none}, - .{.depthTest = false, .depthWrite = false}, - .{.attachments = &.{.noBlending}}, - ); - fakeReflectionPipeline = graphics.Pipeline.init( - "assets/cubyz/shaders/fake_reflection.vert", - "assets/cubyz/shaders/fake_reflection.frag", - "", - &fakeReflectionUniforms, - graphics.draw.SimpleVertex2D, - &.{}, - .{.cullMode = .none}, - .{.depthTest = false, .depthWrite = false}, - .{.attachments = &.{.noBlending}}, - ); - worldFrameBuffer.init(true, c.GL_NEAREST, c.GL_CLAMP_TO_EDGE); - worldFrameBuffer.updateSize(Window.width, Window.height, c.GL_RGB16F); - Bloom.init(); - MeshSelection.init(); - MenuBackGround.init(); - Skybox.init(); - chunk_meshing.init(); - mesh_storage.init(); - reflectionCubeMap = .init(); - reflectionCubeMap.generate(reflectionCubeMapSize, reflectionCubeMapSize); - initReflectionCubeMap(); + deferredRenderPassPipeline = graphics.Pipeline.init( + "assets/cubyz/shaders/deferred_render_pass.vert", + "assets/cubyz/shaders/deferred_render_pass.frag", + "", + &deferredUniforms, + graphics.draw.SimpleVertex2D, + &.{}, + .{ .cullMode = .none }, + .{ .depthTest = false, .depthWrite = false }, + .{ .attachments = &.{.noBlending} }, + ); + fakeReflectionPipeline = graphics.Pipeline.init( + "assets/cubyz/shaders/fake_reflection.vert", + "assets/cubyz/shaders/fake_reflection.frag", + "", + &fakeReflectionUniforms, + graphics.draw.SimpleVertex2D, + &.{}, + .{ .cullMode = .none }, + .{ .depthTest = false, .depthWrite = false }, + .{ .attachments = &.{.noBlending} }, + ); + worldFrameBuffer.init(true, c.GL_NEAREST, c.GL_CLAMP_TO_EDGE); + worldFrameBuffer.updateSize(Window.width, Window.height, c.GL_RGB16F); + Bloom.init(); + MeshSelection.init(); + MenuBackGround.init(); + Skybox.init(); + chunk_meshing.init(); + mesh_storage.init(); + reflectionCubeMap = .init(); + reflectionCubeMap.generate(reflectionCubeMapSize, reflectionCubeMapSize); + initReflectionCubeMap(); } pub fn deinit() void { - deferredRenderPassPipeline.deinit(); - fakeReflectionPipeline.deinit(); - worldFrameBuffer.deinit(); - Bloom.deinit(); - MeshSelection.deinit(); - MenuBackGround.deinit(); - Skybox.deinit(); - mesh_storage.deinit(); - chunk_meshing.deinit(); - reflectionCubeMap.deinit(); + deferredRenderPassPipeline.deinit(); + fakeReflectionPipeline.deinit(); + worldFrameBuffer.deinit(); + Bloom.deinit(); + MeshSelection.deinit(); + MenuBackGround.deinit(); + Skybox.deinit(); + mesh_storage.deinit(); + chunk_meshing.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(); - fakeReflectionPipeline.bind(null); - 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)); - graphics.draw.rectVao.bind(); - c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); - } + 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(); + fakeReflectionPipeline.bind(null); + 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)); + graphics.draw.rectVao.bind(); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + } } var worldFrameBuffer: graphics.FrameBuffer = undefined; @@ -140,1056 +136,1056 @@ pub var lastWidth: u31 = 0; pub var lastHeight: u31 = 0; var lastFov: f32 = 0; pub fn updateFov(fov: f32) void { - if (lastFov != fov) { - lastFov = fov; - game.projectionMatrix = Mat4f.perspective(std.math.degreesToRadians(fov), @as(f32, @floatFromInt(lastWidth))/@as(f32, @floatFromInt(lastHeight)), zNear, zFar); - } + if (lastFov != fov) { + lastFov = fov; + game.projectionMatrix = Mat4f.perspective(std.math.degreesToRadians(fov), @as(f32, @floatFromInt(lastWidth)) / @as(f32, @floatFromInt(lastHeight)), zNear, zFar); + } } pub fn updateViewport(width: u31, height: u31) void { - lastWidth = @trunc(@as(f32, @floatFromInt(width))*main.settings.resolutionScale); - lastHeight = @trunc(@as(f32, @floatFromInt(height))*main.settings.resolutionScale); - game.projectionMatrix = Mat4f.perspective(std.math.degreesToRadians(lastFov), @as(f32, @floatFromInt(lastWidth))/@as(f32, @floatFromInt(lastHeight)), zNear, zFar); - worldFrameBuffer.updateSize(lastWidth, lastHeight, c.GL_RGB16F); - worldFrameBuffer.unbind(); + lastWidth = @trunc(@as(f32, @floatFromInt(width)) * main.settings.resolutionScale); + lastHeight = @trunc(@as(f32, @floatFromInt(height)) * main.settings.resolutionScale); + game.projectionMatrix = Mat4f.perspective(std.math.degreesToRadians(lastFov), @as(f32, @floatFromInt(lastWidth)) / @as(f32, @floatFromInt(lastHeight)), zNear, zFar); + worldFrameBuffer.updateSize(lastWidth, lastHeight, c.GL_RGB16F); + worldFrameBuffer.unbind(); } pub fn render(playerPosition: Vec3d, deltaTime: f64) void { - // TODO: player bobbing - // TODO: Handle colors and sun position in the world. - std.debug.assert(game.world != null); + // TODO: player bobbing + // TODO: Handle colors and sun position in the world. + std.debug.assert(game.world != null); - const nightColor: Vec3f = .{0.3, 0.4, 0.5}; - const ambient = @max(nightColor*@as(Vec3f, @splat(settings.nightBrightness)), @as(Vec3f, @splat(game.world.?.ambientLight))); + const nightColor: Vec3f = .{ 0.3, 0.4, 0.5 }; + const ambient = @max(nightColor * @as(Vec3f, @splat(settings.nightBrightness)), @as(Vec3f, @splat(game.world.?.ambientLight))); - itemdrop.ItemDisplayManager.update(deltaTime); - renderWorld(game.world.?, ambient, game.fog.skyColor, playerPosition); - const startTime = main.timestamp(); - mesh_storage.updateMeshes(startTime.addDuration(maximumMeshTime)); + itemdrop.ItemDisplayManager.update(deltaTime); + renderWorld(game.world.?, ambient, game.fog.skyColor, playerPosition); + const startTime = main.timestamp(); + mesh_storage.updateMeshes(startTime.addDuration(maximumMeshTime)); } pub fn crosshairDirection(rotationMatrix: Mat4f, fovY: f32, width: u31, height: u31) Vec3f { - // stolen code from Frustum.init - const invRotationMatrix = rotationMatrix.transpose(); - const cameraDir = vec.xyz(invRotationMatrix.mulVec(Vec4f{0, 1, 0, 1})); - const cameraUp = vec.xyz(invRotationMatrix.mulVec(Vec4f{0, 0, 1, 1})); - const cameraRight = vec.xyz(invRotationMatrix.mulVec(Vec4f{1, 0, 0, 1})); - - const screenSize = Vec2f{@floatFromInt(width), @floatFromInt(height)}; - const screenCoord = (crosshair.window.pos + crosshair.window.contentSize*Vec2f{0.5, 0.5}*@as(Vec2f, @splat(crosshair.window.scale)))*@as(Vec2f, @splat(main.gui.scale*main.settings.resolutionScale)); - - const halfVSide = std.math.tan(std.math.degreesToRadians(fovY)*0.5); - const halfHSide = halfVSide*screenSize[0]/screenSize[1]; - const sides = Vec2f{halfHSide, halfVSide}; - - const scale = (Vec2f{-1, 1} + Vec2f{2, -2}*screenCoord/screenSize)*sides; - const forwards = cameraDir; - const horizontal = cameraRight*@as(Vec3f, @splat(scale[0])); - const vertical = cameraUp*@as(Vec3f, @splat(scale[1])); // adjust for y coordinate - - const adjusted = forwards + horizontal + vertical; - return adjusted; + // stolen code from Frustum.init + const invRotationMatrix = rotationMatrix.transpose(); + const cameraDir = vec.xyz(invRotationMatrix.mulVec(Vec4f{ 0, 1, 0, 1 })); + const cameraUp = vec.xyz(invRotationMatrix.mulVec(Vec4f{ 0, 0, 1, 1 })); + const cameraRight = vec.xyz(invRotationMatrix.mulVec(Vec4f{ 1, 0, 0, 1 })); + + const screenSize = Vec2f{ @floatFromInt(width), @floatFromInt(height) }; + const screenCoord = (crosshair.window.pos + crosshair.window.contentSize * Vec2f{ 0.5, 0.5 } * @as(Vec2f, @splat(crosshair.window.scale))) * @as(Vec2f, @splat(main.gui.scale * main.settings.resolutionScale)); + + const halfVSide = std.math.tan(std.math.degreesToRadians(fovY) * 0.5); + const halfHSide = halfVSide * screenSize[0] / screenSize[1]; + const sides = Vec2f{ halfHSide, halfVSide }; + + const scale = (Vec2f{ -1, 1 } + Vec2f{ 2, -2 } * screenCoord / screenSize) * sides; + const forwards = cameraDir; + const horizontal = cameraRight * @as(Vec3f, @splat(scale[0])); + const vertical = cameraUp * @as(Vec3f, @splat(scale[1])); // adjust for y coordinate + + const adjusted = forwards + horizontal + vertical; + return adjusted; } pub fn renderWorld(world: *World, ambientLight: Vec3f, skyColor: Vec3f, playerPos: Vec3d) void { // MARK: renderWorld() - worldFrameBuffer.bind(); - c.glViewport(0, 0, lastWidth, lastHeight); - 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); - - const time: u32 = @intCast(main.timestamp().toMilliseconds() & std.math.maxInt(u32)); - - gpu_performance_measuring.startQuery(.skybox); - Skybox.render(); - gpu_performance_measuring.stopQuery(); - - 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, playerPos); - - c.glActiveTexture(c.GL_TEXTURE0); - blocks.meshes.blockTextureArray.bind(); - c.glActiveTexture(c.GL_TEXTURE1); - blocks.meshes.emissionTextureArray.bind(); - c.glActiveTexture(c.GL_TEXTURE2); - blocks.meshes.reflectivityAndAbsorptionTextureArray.bind(); - c.glActiveTexture(c.GL_TEXTURE5); - blocks.meshes.ditherTexture.bind(); - reflectionCubeMap.bindTo(4); - - chunk_meshing.quadsDrawn = 0; - chunk_meshing.transparentQuadsDrawn = 0; - const meshes = mesh_storage.updateAndGetRenderChunks(world.conn, &frustum, playerPos, settings.renderDistance); - - gpu_performance_measuring.startQuery(.chunk_rendering_preparation); - const direction = crosshairDirection(game.camera.viewMatrix, lastFov, lastWidth, lastHeight); - MeshSelection.select(playerPos, direction, game.Player.inventory.getItem(game.Player.selectedSlot)); - - chunk_meshing.beginRender(); - - var chunkLists: [main.settings.highestSupportedLod + 1]main.List(u32) = @splat(main.List(u32).init(main.stackAllocator)); - defer for (chunkLists) |list| list.deinit(); - for (meshes) |mesh| { - mesh.prepareRendering(&chunkLists); - } - gpu_performance_measuring.stopQuery(); - gpu_performance_measuring.startQuery(.chunk_rendering); - chunk_meshing.drawChunksIndirect(&chunkLists, game.projectionMatrix, ambientLight, playerPos, false); - gpu_performance_measuring.stopQuery(); - - gpu_performance_measuring.startQuery(.entity_rendering); - main.entity.client.render(game.projectionMatrix, ambientLight, playerPos, main.lastDeltaTime.load(.monotonic)); - - itemdrop.ItemDropRenderer.renderItemDrops(game.projectionMatrix, ambientLight, playerPos); - gpu_performance_measuring.stopQuery(); - - gpu_performance_measuring.startQuery(.block_entity_rendering); - main.block_entity.renderAll(game.projectionMatrix, ambientLight, playerPos); - gpu_performance_measuring.stopQuery(); - - gpu_performance_measuring.startQuery(.particle_rendering); - particles.ParticleSystem.render(game.projectionMatrix, game.camera.viewMatrix, ambientLight); - gpu_performance_measuring.stopQuery(); - - // Rebind block textures back to their original slots - c.glActiveTexture(c.GL_TEXTURE0); - blocks.meshes.blockTextureArray.bind(); - c.glActiveTexture(c.GL_TEXTURE1); - blocks.meshes.emissionTextureArray.bind(); - - MeshSelection.render(game.projectionMatrix, game.camera.viewMatrix, playerPos); - - // Render transparent chunk meshes: - worldFrameBuffer.bindDepthTexture(c.GL_TEXTURE5); - - gpu_performance_measuring.startQuery(.transparent_rendering_preparation); - c.glTextureBarrier(); - - { - for (&chunkLists) |*list| list.clearRetainingCapacity(); - var i: usize = meshes.len; - while (true) { - if (i == 0) break; - i -= 1; - meshes[i].prepareTransparentRendering(playerPos, &chunkLists); - } - gpu_performance_measuring.stopQuery(); - gpu_performance_measuring.startQuery(.transparent_rendering); - chunk_meshing.drawChunksIndirect(&chunkLists, game.projectionMatrix, ambientLight, playerPos, true); - gpu_performance_measuring.stopQuery(); - } - - c.glDepthRange(0, 0.001); - itemdrop.ItemDropRenderer.renderDisplayItems(ambientLight, playerPos); - c.glDepthRange(0.001, 1); - - chunk_meshing.endRender(); - - worldFrameBuffer.bindTexture(c.GL_TEXTURE3); - - const playerBlock = mesh_storage.getBlockFromAnyLodFromRenderThread(@floor(playerPos[0]), @floor(playerPos[1]), @floor(playerPos[2])); - - if (settings.bloom) { - Bloom.render(lastWidth, lastHeight, playerBlock, playerPos, game.camera.viewMatrix); - } else { - Bloom.bindReplacementImage(); - } - gpu_performance_measuring.startQuery(.final_copy); - if (activeFrameBuffer == 0) c.glViewport(0, 0, main.Window.width, main.Window.height); - worldFrameBuffer.bindTexture(c.GL_TEXTURE3); - worldFrameBuffer.bindDepthTexture(c.GL_TEXTURE4); - worldFrameBuffer.unbind(); - deferredRenderPassPipeline.bind(null); - if (!blocks.meshes.hasFog(playerBlock)) { - c.glUniform3fv(deferredUniforms.@"fog.color", 1, @ptrCast(&game.fog.fogColor)); - c.glUniform1f(deferredUniforms.@"fog.density", game.fog.density); - c.glUniform1f(deferredUniforms.@"fog.fogLower", game.fog.fogLower); - c.glUniform1f(deferredUniforms.@"fog.fogHigher", game.fog.fogHigher); - } 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.@"fog.fogLower", 1e10); - c.glUniform1f(deferredUniforms.@"fog.fogHigher", 1e10); - } - c.glUniformMatrix4fv(deferredUniforms.invViewMatrix, 1, c.GL_TRUE, @ptrCast(&game.camera.viewMatrix.transpose())); - c.glUniform3i(deferredUniforms.playerPositionInteger, @floor(playerPos[0]), @floor(playerPos[1]), @floor(playerPos[2])); - c.glUniform3f(deferredUniforms.playerPositionFraction, @floatCast(@mod(playerPos[0], 1)), @floatCast(@mod(playerPos[1], 1)), @floatCast(@mod(playerPos[2], 1))); - 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][2]); - - c.glBindFramebuffer(c.GL_FRAMEBUFFER, activeFrameBuffer); - - graphics.draw.rectVao.bind(); - c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); - - c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0); - - if (!main.gui.hideGui) main.entity.client.renderHud(game.projectionMatrix, ambientLight, playerPos); - gpu_performance_measuring.stopQuery(); + worldFrameBuffer.bind(); + c.glViewport(0, 0, lastWidth, lastHeight); + 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); + + const time: u32 = @intCast(main.timestamp().toMilliseconds() & std.math.maxInt(u32)); + + gpu_performance_measuring.startQuery(.skybox); + Skybox.render(); + gpu_performance_measuring.stopQuery(); + + 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, playerPos); + + c.glActiveTexture(c.GL_TEXTURE0); + blocks.meshes.blockTextureArray.bind(); + c.glActiveTexture(c.GL_TEXTURE1); + blocks.meshes.emissionTextureArray.bind(); + c.glActiveTexture(c.GL_TEXTURE2); + blocks.meshes.reflectivityAndAbsorptionTextureArray.bind(); + c.glActiveTexture(c.GL_TEXTURE5); + blocks.meshes.ditherTexture.bind(); + reflectionCubeMap.bindTo(4); + + chunk_meshing.quadsDrawn = 0; + chunk_meshing.transparentQuadsDrawn = 0; + const meshes = mesh_storage.updateAndGetRenderChunks(world.conn, &frustum, playerPos, settings.renderDistance); + + gpu_performance_measuring.startQuery(.chunk_rendering_preparation); + const direction = crosshairDirection(game.camera.viewMatrix, lastFov, lastWidth, lastHeight); + MeshSelection.select(playerPos, direction, game.Player.inventory.getItem(game.Player.selectedSlot)); + + chunk_meshing.beginRender(); + + var chunkLists: [main.settings.highestSupportedLod + 1]main.List(u32) = @splat(main.List(u32).init(main.stackAllocator)); + defer for (chunkLists) |list| list.deinit(); + for (meshes) |mesh| { + mesh.prepareRendering(&chunkLists); + } + gpu_performance_measuring.stopQuery(); + gpu_performance_measuring.startQuery(.chunk_rendering); + chunk_meshing.drawChunksIndirect(&chunkLists, game.projectionMatrix, ambientLight, playerPos, false); + gpu_performance_measuring.stopQuery(); + + gpu_performance_measuring.startQuery(.entity_rendering); + main.entity.client.render(game.projectionMatrix, ambientLight, playerPos, main.lastDeltaTime.load(.monotonic)); + + itemdrop.ItemDropRenderer.renderItemDrops(game.projectionMatrix, ambientLight, playerPos); + gpu_performance_measuring.stopQuery(); + + gpu_performance_measuring.startQuery(.block_entity_rendering); + main.block_entity.renderAll(game.projectionMatrix, ambientLight, playerPos); + gpu_performance_measuring.stopQuery(); + + gpu_performance_measuring.startQuery(.particle_rendering); + particles.ParticleSystem.render(game.projectionMatrix, game.camera.viewMatrix, ambientLight); + gpu_performance_measuring.stopQuery(); + + // Rebind block textures back to their original slots + c.glActiveTexture(c.GL_TEXTURE0); + blocks.meshes.blockTextureArray.bind(); + c.glActiveTexture(c.GL_TEXTURE1); + blocks.meshes.emissionTextureArray.bind(); + + MeshSelection.render(game.projectionMatrix, game.camera.viewMatrix, playerPos); + + // Render transparent chunk meshes: + worldFrameBuffer.bindDepthTexture(c.GL_TEXTURE5); + + gpu_performance_measuring.startQuery(.transparent_rendering_preparation); + c.glTextureBarrier(); + + { + for (&chunkLists) |*list| list.clearRetainingCapacity(); + var i: usize = meshes.len; + while (true) { + if (i == 0) break; + i -= 1; + meshes[i].prepareTransparentRendering(playerPos, &chunkLists); + } + gpu_performance_measuring.stopQuery(); + gpu_performance_measuring.startQuery(.transparent_rendering); + chunk_meshing.drawChunksIndirect(&chunkLists, game.projectionMatrix, ambientLight, playerPos, true); + gpu_performance_measuring.stopQuery(); + } + + c.glDepthRange(0, 0.001); + itemdrop.ItemDropRenderer.renderDisplayItems(ambientLight, playerPos); + c.glDepthRange(0.001, 1); + + chunk_meshing.endRender(); + + worldFrameBuffer.bindTexture(c.GL_TEXTURE3); + + const playerBlock = mesh_storage.getBlockFromAnyLodFromRenderThread(@floor(playerPos[0]), @floor(playerPos[1]), @floor(playerPos[2])); + + if (settings.bloom) { + Bloom.render(lastWidth, lastHeight, playerBlock, playerPos, game.camera.viewMatrix); + } else { + Bloom.bindReplacementImage(); + } + gpu_performance_measuring.startQuery(.final_copy); + if (activeFrameBuffer == 0) c.glViewport(0, 0, main.Window.width, main.Window.height); + worldFrameBuffer.bindTexture(c.GL_TEXTURE3); + worldFrameBuffer.bindDepthTexture(c.GL_TEXTURE4); + worldFrameBuffer.unbind(); + deferredRenderPassPipeline.bind(null); + if (!blocks.meshes.hasFog(playerBlock)) { + c.glUniform3fv(deferredUniforms.@"fog.color", 1, @ptrCast(&game.fog.fogColor)); + c.glUniform1f(deferredUniforms.@"fog.density", game.fog.density); + c.glUniform1f(deferredUniforms.@"fog.fogLower", game.fog.fogLower); + c.glUniform1f(deferredUniforms.@"fog.fogHigher", game.fog.fogHigher); + } 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.@"fog.fogLower", 1e10); + c.glUniform1f(deferredUniforms.@"fog.fogHigher", 1e10); + } + c.glUniformMatrix4fv(deferredUniforms.invViewMatrix, 1, c.GL_TRUE, @ptrCast(&game.camera.viewMatrix.transpose())); + c.glUniform3i(deferredUniforms.playerPositionInteger, @floor(playerPos[0]), @floor(playerPos[1]), @floor(playerPos[2])); + c.glUniform3f(deferredUniforms.playerPositionFraction, @floatCast(@mod(playerPos[0], 1)), @floatCast(@mod(playerPos[1], 1)), @floatCast(@mod(playerPos[2], 1))); + 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][2]); + + c.glBindFramebuffer(c.GL_FRAMEBUFFER, activeFrameBuffer); + + graphics.draw.rectVao.bind(); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + + c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0); + + if (!main.gui.hideGui) main.entity.client.renderHud(game.projectionMatrix, ambientLight, playerPos); + gpu_performance_measuring.stopQuery(); } const Bloom = struct { // MARK: Bloom - 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 firstPassPipeline: graphics.Pipeline = undefined; - var secondPassPipeline: graphics.Pipeline = undefined; - var colorExtractAndDownsamplePipeline: graphics.Pipeline = undefined; - var colorExtractUniforms: struct { - zNear: c_int, - zFar: c_int, - tanXY: c_int, - @"fog.color": c_int, - @"fog.density": c_int, - @"fog.fogLower": c_int, - @"fog.fogHigher": c_int, - invViewMatrix: c_int, - playerPositionInteger: c_int, - playerPositionFraction: 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 = .init(); - emptyBuffer.generate(graphics.Image.emptyImage); - firstPassPipeline = graphics.Pipeline.init( - "assets/cubyz/shaders/bloom/first_pass.vert", - "assets/cubyz/shaders/bloom/first_pass.frag", - "", - null, - graphics.draw.SimpleVertex2D, - &.{.{.binding = 3, .count = 1, .type = .combinedImageSampler, .stageFlags = .{.fragment = true}}}, - .{.cullMode = .none}, - .{.depthTest = false, .depthWrite = false}, - .{.attachments = &.{.noBlending}}, - ); - secondPassPipeline = graphics.Pipeline.init( - "assets/cubyz/shaders/bloom/second_pass.vert", - "assets/cubyz/shaders/bloom/second_pass.frag", - "", - null, - graphics.draw.SimpleVertex2D, - &.{.{.binding = 3, .count = 1, .type = .combinedImageSampler, .stageFlags = .{.fragment = true}}}, - .{.cullMode = .none}, - .{.depthTest = false, .depthWrite = false}, - .{.attachments = &.{.noBlending}}, - ); - colorExtractAndDownsamplePipeline = graphics.Pipeline.init( - "assets/cubyz/shaders/bloom/color_extractor_downsample.vert", - "assets/cubyz/shaders/bloom/color_extractor_downsample.frag", - "", - &colorExtractUniforms, - graphics.draw.SimpleVertex2D, - &.{}, - .{.cullMode = .none}, - .{.depthTest = false, .depthWrite = false}, - .{.attachments = &.{.noBlending}}, - ); - } - - pub fn deinit() void { - buffer1.deinit(); - buffer2.deinit(); - firstPassPipeline.deinit(); - secondPassPipeline.deinit(); - colorExtractAndDownsamplePipeline.deinit(); - } - - fn extractImageDataAndDownsample(playerBlock: blocks.Block, playerPos: Vec3d, viewMatrix: Mat4f) void { - colorExtractAndDownsamplePipeline.bind(null); - worldFrameBuffer.bindTexture(c.GL_TEXTURE3); - worldFrameBuffer.bindDepthTexture(c.GL_TEXTURE4); - buffer1.bind(); - if (!blocks.meshes.hasFog(playerBlock)) { - c.glUniform3fv(colorExtractUniforms.@"fog.color", 1, @ptrCast(&game.fog.fogColor)); - c.glUniform1f(colorExtractUniforms.@"fog.density", game.fog.density); - c.glUniform1f(colorExtractUniforms.@"fog.fogLower", game.fog.fogLower); - c.glUniform1f(colorExtractUniforms.@"fog.fogHigher", game.fog.fogHigher); - } 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.@"fog.fogLower", 1e10); - c.glUniform1f(colorExtractUniforms.@"fog.fogHigher", 1e10); - } - - c.glUniformMatrix4fv(colorExtractUniforms.invViewMatrix, 1, c.GL_TRUE, @ptrCast(&viewMatrix.transpose())); - c.glUniform3i(colorExtractUniforms.playerPositionInteger, @floor(playerPos[0]), @floor(playerPos[1]), @floor(playerPos[2])); - c.glUniform3f(colorExtractUniforms.playerPositionFraction, @floatCast(@mod(playerPos[0], 1)), @floatCast(@mod(playerPos[1], 1)), @floatCast(@mod(playerPos[2], 1))); - 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][2]); - graphics.draw.rectVao.bind(); - c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); - } - - fn firstPass() void { - firstPassPipeline.bind(null); - buffer1.bindTexture(c.GL_TEXTURE3); - buffer2.bind(); - graphics.draw.rectVao.bind(); - c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); - } - - fn secondPass() void { - secondPassPipeline.bind(null); - buffer2.bindTexture(c.GL_TEXTURE3); - buffer1.bind(); - graphics.draw.rectVao.bind(); - c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); - } - - fn render(currentWidth: u31, currentHeight: u31, playerBlock: blocks.Block, playerPos: Vec3d, viewMatrix: Mat4f) void { - if (width != currentWidth or height != currentHeight) { - width = currentWidth; - height = currentHeight; - buffer1.updateSize(width/4, height/4, c.GL_R11F_G11F_B10F); - std.debug.assert(buffer1.validate()); - buffer2.updateSize(width/4, height/4, c.GL_R11F_G11F_B10F); - std.debug.assert(buffer2.validate()); - } - gpu_performance_measuring.startQuery(.bloom_extract_downsample); - - c.glViewport(0, 0, width/4, height/4); - extractImageDataAndDownsample(playerBlock, playerPos, viewMatrix); - 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); - - gpu_performance_measuring.stopQuery(); - } - - fn bindReplacementImage() void { - emptyBuffer.bindTo(5); - } + 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 firstPassPipeline: graphics.Pipeline = undefined; + var secondPassPipeline: graphics.Pipeline = undefined; + var colorExtractAndDownsamplePipeline: graphics.Pipeline = undefined; + var colorExtractUniforms: struct { + zNear: c_int, + zFar: c_int, + tanXY: c_int, + @"fog.color": c_int, + @"fog.density": c_int, + @"fog.fogLower": c_int, + @"fog.fogHigher": c_int, + invViewMatrix: c_int, + playerPositionInteger: c_int, + playerPositionFraction: 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 = .init(); + emptyBuffer.generate(graphics.Image.emptyImage); + firstPassPipeline = graphics.Pipeline.init( + "assets/cubyz/shaders/bloom/first_pass.vert", + "assets/cubyz/shaders/bloom/first_pass.frag", + "", + null, + graphics.draw.SimpleVertex2D, + &.{.{ .binding = 3, .count = 1, .type = .combinedImageSampler, .stageFlags = .{ .fragment = true } }}, + .{ .cullMode = .none }, + .{ .depthTest = false, .depthWrite = false }, + .{ .attachments = &.{.noBlending} }, + ); + secondPassPipeline = graphics.Pipeline.init( + "assets/cubyz/shaders/bloom/second_pass.vert", + "assets/cubyz/shaders/bloom/second_pass.frag", + "", + null, + graphics.draw.SimpleVertex2D, + &.{.{ .binding = 3, .count = 1, .type = .combinedImageSampler, .stageFlags = .{ .fragment = true } }}, + .{ .cullMode = .none }, + .{ .depthTest = false, .depthWrite = false }, + .{ .attachments = &.{.noBlending} }, + ); + colorExtractAndDownsamplePipeline = graphics.Pipeline.init( + "assets/cubyz/shaders/bloom/color_extractor_downsample.vert", + "assets/cubyz/shaders/bloom/color_extractor_downsample.frag", + "", + &colorExtractUniforms, + graphics.draw.SimpleVertex2D, + &.{}, + .{ .cullMode = .none }, + .{ .depthTest = false, .depthWrite = false }, + .{ .attachments = &.{.noBlending} }, + ); + } + + pub fn deinit() void { + buffer1.deinit(); + buffer2.deinit(); + firstPassPipeline.deinit(); + secondPassPipeline.deinit(); + colorExtractAndDownsamplePipeline.deinit(); + } + + fn extractImageDataAndDownsample(playerBlock: blocks.Block, playerPos: Vec3d, viewMatrix: Mat4f) void { + colorExtractAndDownsamplePipeline.bind(null); + worldFrameBuffer.bindTexture(c.GL_TEXTURE3); + worldFrameBuffer.bindDepthTexture(c.GL_TEXTURE4); + buffer1.bind(); + if (!blocks.meshes.hasFog(playerBlock)) { + c.glUniform3fv(colorExtractUniforms.@"fog.color", 1, @ptrCast(&game.fog.fogColor)); + c.glUniform1f(colorExtractUniforms.@"fog.density", game.fog.density); + c.glUniform1f(colorExtractUniforms.@"fog.fogLower", game.fog.fogLower); + c.glUniform1f(colorExtractUniforms.@"fog.fogHigher", game.fog.fogHigher); + } 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.@"fog.fogLower", 1e10); + c.glUniform1f(colorExtractUniforms.@"fog.fogHigher", 1e10); + } + + c.glUniformMatrix4fv(colorExtractUniforms.invViewMatrix, 1, c.GL_TRUE, @ptrCast(&viewMatrix.transpose())); + c.glUniform3i(colorExtractUniforms.playerPositionInteger, @floor(playerPos[0]), @floor(playerPos[1]), @floor(playerPos[2])); + c.glUniform3f(colorExtractUniforms.playerPositionFraction, @floatCast(@mod(playerPos[0], 1)), @floatCast(@mod(playerPos[1], 1)), @floatCast(@mod(playerPos[2], 1))); + 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][2]); + graphics.draw.rectVao.bind(); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + } + + fn firstPass() void { + firstPassPipeline.bind(null); + buffer1.bindTexture(c.GL_TEXTURE3); + buffer2.bind(); + graphics.draw.rectVao.bind(); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + } + + fn secondPass() void { + secondPassPipeline.bind(null); + buffer2.bindTexture(c.GL_TEXTURE3); + buffer1.bind(); + graphics.draw.rectVao.bind(); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + } + + fn render(currentWidth: u31, currentHeight: u31, playerBlock: blocks.Block, playerPos: Vec3d, viewMatrix: Mat4f) void { + if (width != currentWidth or height != currentHeight) { + width = currentWidth; + height = currentHeight; + buffer1.updateSize(width / 4, height / 4, c.GL_R11F_G11F_B10F); + std.debug.assert(buffer1.validate()); + buffer2.updateSize(width / 4, height / 4, c.GL_R11F_G11F_B10F); + std.debug.assert(buffer2.validate()); + } + gpu_performance_measuring.startQuery(.bloom_extract_downsample); + + c.glViewport(0, 0, width / 4, height / 4); + extractImageDataAndDownsample(playerBlock, playerPos, viewMatrix); + 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); + + gpu_performance_measuring.stopQuery(); + } + + fn bindReplacementImage() void { + emptyBuffer.bindTo(5); + } }; pub const MenuBackGround = struct { - var pipeline: graphics.Pipeline = undefined; - var uniforms: struct { - viewMatrix: c_int, - projectionMatrix: c_int, - } = undefined; - - var vao: graphics.VertexArray = undefined; - var texture: graphics.Texture = undefined; - - var angle: f32 = 0; - - fn init() void { - const MenuBackgroundVertex = struct { - pos: [3]f32, - uv: [2]f32, - - pub const attributeDescriptions: []const c.VkVertexInputAttributeDescription = &.{ - .{ - .location = 0, - .format = c.VK_FORMAT_R32G32B32_SFLOAT, - .offset = @offsetOf(@This(), "pos"), - }, - .{ - .location = 1, - .format = c.VK_FORMAT_R32G32_SFLOAT, - .offset = @offsetOf(@This(), "uv"), - }, - }; - }; - pipeline = graphics.Pipeline.init( - "assets/cubyz/shaders/background/vertex.vert", - "assets/cubyz/shaders/background/fragment.frag", - "", - &uniforms, - MenuBackgroundVertex, - &.{}, - .{.cullMode = .none}, - .{.depthTest = false, .depthWrite = false}, - .{.attachments = &.{.noBlending}}, - ); - // 4 sides of a simple cube with some panorama texture on it. - const rawData = [_]MenuBackgroundVertex{ - .{.pos = .{-1, 1, -1}, .uv = .{1, 1}}, - .{.pos = .{-1, 1, 1}, .uv = .{1, 0}}, - .{.pos = .{-1, -1, -1}, .uv = .{0.75, 1}}, - .{.pos = .{-1, -1, 1}, .uv = .{0.75, 0}}, - .{.pos = .{1, -1, -1}, .uv = .{0.5, 1}}, - .{.pos = .{1, -1, 1}, .uv = .{0.5, 0}}, - .{.pos = .{1, 1, -1}, .uv = .{0.25, 1}}, - .{.pos = .{1, 1, 1}, .uv = .{0.25, 0}}, - .{.pos = .{-1, 1, -1}, .uv = .{0, 1}}, - .{.pos = .{-1, 1, 1}, .uv = .{0, 0}}, - }; - - const indices = [_]u32{ - 0, 1, 2, - 2, 3, 1, - 2, 3, 4, - 4, 5, 3, - 4, 5, 6, - 6, 7, 5, - 6, 7, 8, - 8, 9, 7, - }; - - vao = .init(MenuBackgroundVertex, &rawData, &indices); - - const backgroundPath = chooseBackgroundImagePath(main.stackAllocator) catch |err| { - std.log.err("Couldn't open background path: {s}", .{@errorName(err)}); - texture = .{.textureID = 0}; - return; - }; - defer main.stackAllocator.free(backgroundPath); - texture = graphics.Texture.initFromFile(backgroundPath); - } - - fn chooseBackgroundImagePath(allocator: main.heap.NeverFailingAllocator) ![]const u8 { - var dir = try main.files.cubyzDir().openIterableDir("backgrounds"); - defer dir.close(); - - // Whenever the version changes copy over the new background image and display it. - if (!std.mem.eql(u8, settings.lastVersionString, settings.version.version)) { - const defaultImageData = try main.files.cwd().read(main.stackAllocator, "assets/cubyz/default_background.png"); - defer main.stackAllocator.free(defaultImageData); - try dir.write("default_background.png", defaultImageData); - - return std.fmt.allocPrint(allocator.allocator, "{s}/backgrounds/default_background.png", .{main.files.cubyzDirStr()}) catch unreachable; - } - - // Otherwise load a random texture from the backgrounds folder. The player may make their own pictures which can be chosen as well. - var walker = dir.walk(main.stackAllocator); - defer walker.deinit(); - var fileList = main.List([]const u8).init(main.stackAllocator); - defer { - for (fileList.items) |fileName| { - main.stackAllocator.free(fileName); - } - fileList.deinit(); - } - - while (try walker.next(main.io)) |entry| { - if (entry.kind == .file and std.ascii.endsWithIgnoreCase(entry.basename, ".png")) { - fileList.append(main.stackAllocator.dupe(u8, entry.path)); - } - } - if (fileList.items.len == 0) { - return error.NoBackgroundImagesFound; - } - const theChosenOne = main.random.nextIntBounded(u32, &main.seed, @as(u32, @intCast(fileList.items.len))); - return std.fmt.allocPrint(allocator.allocator, "{s}/backgrounds/{s}", .{main.files.cubyzDirStr(), fileList.items[theChosenOne]}) catch unreachable; - } - - pub fn deinit() void { - pipeline.deinit(); - vao.deinit(); - } - - pub fn hasImage() bool { - return texture.textureID != 0; - } - - pub fn render(deltaTime: f64) void { - c.glViewport(0, 0, main.Window.width, main.Window.height); - if (texture.textureID == 0) return; - - // Use a simple rotation around the z axis, with a steadily increasing angle. - angle += @as(f32, @floatCast(deltaTime))/20.0; - const viewMatrix = Mat4f.rotationZ(angle); - pipeline.bind(null); - c.glUniformMatrix4fv(uniforms.viewMatrix, 1, c.GL_TRUE, @ptrCast(&viewMatrix)); - c.glUniformMatrix4fv(uniforms.projectionMatrix, 1, c.GL_TRUE, @ptrCast(&game.projectionMatrix)); - - texture.bindTo(0); - - vao.bind(); - 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 = main.stackAllocator.alloc(u32, size*size); - defer main.stackAllocator.free(pixels); - - // Change the viewport and the matrices to render 4 cube faces: - - const oldResolutionScale = main.settings.resolutionScale; - main.settings.resolutionScale = 1; - updateViewport(size, size); - updateFov(90.0); - defer updateFov(main.settings.fov); - main.settings.resolutionScale = oldResolutionScale; - defer updateViewport(Window.width, Window.height); - - 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 = graphics.Image.init(main.stackAllocator, 4*size, size); - defer image.deinit(main.stackAllocator); - - for (0..4) |i| { - c.glDepthFunc(c.GL_LESS); - c.glDepthMask(c.GL_TRUE); - c.glDisable(c.GL_SCISSOR_TEST); - game.camera.rotation = .{0, 0, angles[i]}; - // Draw to frame buffer. - buffer.bind(); - c.glClear(c.GL_DEPTH_BUFFER_BIT | c.GL_STENCIL_BUFFER_BIT | c.GL_COLOR_BUFFER_BIT); - main.renderer.render(game.Player.getEyePosBlocking(), 0); - // 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.glBindFramebuffer(c.GL_FRAMEBUFFER, 0); - - const fileName = std.fmt.allocPrint(main.stackAllocator.allocator, "{s}/backgrounds/{s}_{}.png", .{main.files.cubyzDirStr(), game.world.?.name, game.world.?.gameTime.load(.monotonic)}) catch unreachable; - defer main.stackAllocator.free(fileName); - image.exportToFile(fileName) catch |err| { - std.log.err("Cannot write file {s} due to {s}", .{fileName, @errorName(err)}); - }; - // TODO: Performance is terrible even with -O3. Consider using qoi instead. - } + var pipeline: graphics.Pipeline = undefined; + var uniforms: struct { + viewMatrix: c_int, + projectionMatrix: c_int, + } = undefined; + + var vao: graphics.VertexArray = undefined; + var texture: graphics.Texture = undefined; + + var angle: f32 = 0; + + fn init() void { + const MenuBackgroundVertex = struct { + pos: [3]f32, + uv: [2]f32, + + pub const attributeDescriptions: []const c.VkVertexInputAttributeDescription = &.{ + .{ + .location = 0, + .format = c.VK_FORMAT_R32G32B32_SFLOAT, + .offset = @offsetOf(@This(), "pos"), + }, + .{ + .location = 1, + .format = c.VK_FORMAT_R32G32_SFLOAT, + .offset = @offsetOf(@This(), "uv"), + }, + }; + }; + pipeline = graphics.Pipeline.init( + "assets/cubyz/shaders/background/vertex.vert", + "assets/cubyz/shaders/background/fragment.frag", + "", + &uniforms, + MenuBackgroundVertex, + &.{}, + .{ .cullMode = .none }, + .{ .depthTest = false, .depthWrite = false }, + .{ .attachments = &.{.noBlending} }, + ); + // 4 sides of a simple cube with some panorama texture on it. + const rawData = [_]MenuBackgroundVertex{ + .{ .pos = .{ -1, 1, -1 }, .uv = .{ 1, 1 } }, + .{ .pos = .{ -1, 1, 1 }, .uv = .{ 1, 0 } }, + .{ .pos = .{ -1, -1, -1 }, .uv = .{ 0.75, 1 } }, + .{ .pos = .{ -1, -1, 1 }, .uv = .{ 0.75, 0 } }, + .{ .pos = .{ 1, -1, -1 }, .uv = .{ 0.5, 1 } }, + .{ .pos = .{ 1, -1, 1 }, .uv = .{ 0.5, 0 } }, + .{ .pos = .{ 1, 1, -1 }, .uv = .{ 0.25, 1 } }, + .{ .pos = .{ 1, 1, 1 }, .uv = .{ 0.25, 0 } }, + .{ .pos = .{ -1, 1, -1 }, .uv = .{ 0, 1 } }, + .{ .pos = .{ -1, 1, 1 }, .uv = .{ 0, 0 } }, + }; + + const indices = [_]u32{ + 0, 1, 2, + 2, 3, 1, + 2, 3, 4, + 4, 5, 3, + 4, 5, 6, + 6, 7, 5, + 6, 7, 8, + 8, 9, 7, + }; + + vao = .init(MenuBackgroundVertex, &rawData, &indices); + + const backgroundPath = chooseBackgroundImagePath(main.stackAllocator) catch |err| { + std.log.err("Couldn't open background path: {s}", .{@errorName(err)}); + texture = .{ .textureID = 0 }; + return; + }; + defer main.stackAllocator.free(backgroundPath); + texture = graphics.Texture.initFromFile(backgroundPath); + } + + fn chooseBackgroundImagePath(allocator: main.heap.NeverFailingAllocator) ![]const u8 { + var dir = try main.files.cubyzDir().openIterableDir("backgrounds"); + defer dir.close(); + + // Whenever the version changes copy over the new background image and display it. + if (!std.mem.eql(u8, settings.lastVersionString, settings.version.version)) { + const defaultImageData = try main.files.cwd().read(main.stackAllocator, "assets/cubyz/default_background.png"); + defer main.stackAllocator.free(defaultImageData); + try dir.write("default_background.png", defaultImageData); + + return std.fmt.allocPrint(allocator.allocator, "{s}/backgrounds/default_background.png", .{main.files.cubyzDirStr()}) catch unreachable; + } + + // Otherwise load a random texture from the backgrounds folder. The player may make their own pictures which can be chosen as well. + var walker = dir.walk(main.stackAllocator); + defer walker.deinit(); + var fileList = main.List([]const u8).init(main.stackAllocator); + defer { + for (fileList.items) |fileName| { + main.stackAllocator.free(fileName); + } + fileList.deinit(); + } + + while (try walker.next(main.io)) |entry| { + if (entry.kind == .file and std.ascii.endsWithIgnoreCase(entry.basename, ".png")) { + fileList.append(main.stackAllocator.dupe(u8, entry.path)); + } + } + if (fileList.items.len == 0) { + return error.NoBackgroundImagesFound; + } + const theChosenOne = main.random.nextIntBounded(u32, &main.seed, @as(u32, @intCast(fileList.items.len))); + return std.fmt.allocPrint(allocator.allocator, "{s}/backgrounds/{s}", .{ main.files.cubyzDirStr(), fileList.items[theChosenOne] }) catch unreachable; + } + + pub fn deinit() void { + pipeline.deinit(); + vao.deinit(); + } + + pub fn hasImage() bool { + return texture.textureID != 0; + } + + pub fn render(deltaTime: f64) void { + c.glViewport(0, 0, main.Window.width, main.Window.height); + if (texture.textureID == 0) return; + + // Use a simple rotation around the z axis, with a steadily increasing angle. + angle += @as(f32, @floatCast(deltaTime)) / 20.0; + const viewMatrix = Mat4f.rotationZ(angle); + pipeline.bind(null); + c.glUniformMatrix4fv(uniforms.viewMatrix, 1, c.GL_TRUE, @ptrCast(&viewMatrix)); + c.glUniformMatrix4fv(uniforms.projectionMatrix, 1, c.GL_TRUE, @ptrCast(&game.projectionMatrix)); + + texture.bindTo(0); + + vao.bind(); + 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 = main.stackAllocator.alloc(u32, size * size); + defer main.stackAllocator.free(pixels); + + // Change the viewport and the matrices to render 4 cube faces: + + const oldResolutionScale = main.settings.resolutionScale; + main.settings.resolutionScale = 1; + updateViewport(size, size); + updateFov(90.0); + defer updateFov(main.settings.fov); + main.settings.resolutionScale = oldResolutionScale; + defer updateViewport(Window.width, Window.height); + + 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 = graphics.Image.init(main.stackAllocator, 4 * size, size); + defer image.deinit(main.stackAllocator); + + for (0..4) |i| { + c.glDepthFunc(c.GL_LESS); + c.glDepthMask(c.GL_TRUE); + c.glDisable(c.GL_SCISSOR_TEST); + game.camera.rotation = .{ 0, 0, angles[i] }; + // Draw to frame buffer. + buffer.bind(); + c.glClear(c.GL_DEPTH_BUFFER_BIT | c.GL_STENCIL_BUFFER_BIT | c.GL_COLOR_BUFFER_BIT); + main.renderer.render(game.Player.getEyePosBlocking(), 0); + // 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.glBindFramebuffer(c.GL_FRAMEBUFFER, 0); + + const fileName = std.fmt.allocPrint(main.stackAllocator.allocator, "{s}/backgrounds/{s}_{}.png", .{ main.files.cubyzDirStr(), game.world.?.name, game.world.?.gameTime.load(.monotonic) }) catch unreachable; + defer main.stackAllocator.free(fileName); + image.exportToFile(fileName) catch |err| { + std.log.err("Cannot write file {s} due to {s}", .{ fileName, @errorName(err) }); + }; + // TODO: Performance is terrible even with -O3. Consider using qoi instead. + } }; pub const Skybox = struct { - var starPipeline: graphics.Pipeline = undefined; - var starUniforms: struct { - mvp: c_int, - starOpacity: c_int, - } = undefined; + var starPipeline: graphics.Pipeline = undefined; + var starUniforms: struct { + mvp: c_int, + starOpacity: c_int, + } = undefined; - var starVao: graphics.VertexArray = undefined; + var starVao: graphics.VertexArray = undefined; - var starSsbo: graphics.SSBO = undefined; + var starSsbo: graphics.SSBO = undefined; - const numStars = 10000; + const numStars = 10000; - fn getStarPos(seed: *u64) Vec3f { - const x: f32 = @floatCast(main.random.nextFloatGauss(seed)); - const y: f32 = @floatCast(main.random.nextFloatGauss(seed)); - const z: f32 = @floatCast(main.random.nextFloatGauss(seed)); + fn getStarPos(seed: *u64) Vec3f { + const x: f32 = @floatCast(main.random.nextFloatGauss(seed)); + const y: f32 = @floatCast(main.random.nextFloatGauss(seed)); + const z: f32 = @floatCast(main.random.nextFloatGauss(seed)); - const r = std.math.cbrt(main.random.nextFloat(seed))*5000.0; + const r = std.math.cbrt(main.random.nextFloat(seed)) * 5000.0; - return vec.normalize(Vec3f{x, y, z})*@as(Vec3f, @splat(r)); - } + return vec.normalize(Vec3f{ x, y, z }) * @as(Vec3f, @splat(r)); + } - fn getStarColor(temperature: f32, light: f32, image: graphics.Image) Vec3f { - const rgbCol = image.getRGB(@trunc(std.math.clamp(temperature/15000.0*@as(f32, @floatFromInt(image.width)), 0.0, @as(f32, @floatFromInt(image.width - 1)))), 0); - var rgb: Vec3f = @floatFromInt(Vec3i{rgbCol.r, rgbCol.g, rgbCol.b}); - rgb /= @splat(255.0); + fn getStarColor(temperature: f32, light: f32, image: graphics.Image) Vec3f { + const rgbCol = image.getRGB(@trunc(std.math.clamp(temperature / 15000.0 * @as(f32, @floatFromInt(image.width)), 0.0, @as(f32, @floatFromInt(image.width - 1)))), 0); + var rgb: Vec3f = @floatFromInt(Vec3i{ rgbCol.r, rgbCol.g, rgbCol.b }); + rgb /= @splat(255.0); - rgb *= @as(Vec3f, @splat(light)); + rgb *= @as(Vec3f, @splat(light)); - const m = @reduce(.Max, rgb); - if (m > 1.0) { - rgb /= @as(Vec3f, @splat(m)); - } + const m = @reduce(.Max, rgb); + if (m > 1.0) { + rgb /= @as(Vec3f, @splat(m)); + } - return rgb; - } + return rgb; + } - fn init() void { - const starColorImage = graphics.Image.readFromFile(main.stackAllocator, "assets/cubyz/star.png") catch |err| { - std.log.err("Failed to load star image: {s}", .{@errorName(err)}); - return; - }; - defer starColorImage.deinit(main.stackAllocator); + fn init() void { + const starColorImage = graphics.Image.readFromFile(main.stackAllocator, "assets/cubyz/star.png") catch |err| { + std.log.err("Failed to load star image: {s}", .{@errorName(err)}); + return; + }; + defer starColorImage.deinit(main.stackAllocator); - starPipeline = graphics.Pipeline.init( - "assets/cubyz/shaders/skybox/star.vert", - "assets/cubyz/shaders/skybox/star.frag", - "", - &starUniforms, - graphics.VertexArray.EmptyVertex, - &.{}, - .{.cullMode = .none}, - .{.depthTest = false, .depthWrite = false}, - .{.attachments = &.{.{ - .srcColorBlendFactor = .one, - .dstColorBlendFactor = .one, - .colorBlendOp = .add, - .srcAlphaBlendFactor = .one, - .dstAlphaBlendFactor = .one, - .alphaBlendOp = .add, - }}}, - ); + starPipeline = graphics.Pipeline.init( + "assets/cubyz/shaders/skybox/star.vert", + "assets/cubyz/shaders/skybox/star.frag", + "", + &starUniforms, + graphics.VertexArray.EmptyVertex, + &.{}, + .{ .cullMode = .none }, + .{ .depthTest = false, .depthWrite = false }, + .{ .attachments = &.{.{ + .srcColorBlendFactor = .one, + .dstColorBlendFactor = .one, + .colorBlendOp = .add, + .srcAlphaBlendFactor = .one, + .dstAlphaBlendFactor = .one, + .alphaBlendOp = .add, + }} }, + ); - var starData: [numStars*20]f32 = undefined; + var starData: [numStars * 20]f32 = undefined; - const starDist = 200.0; + const starDist = 200.0; - const off: f32 = @sqrt(3.0)/6.0; + const off: f32 = @sqrt(3.0) / 6.0; - const triVertA = Vec3f{0.5, starDist, -off}; - const triVertB = Vec3f{-0.5, starDist, -off}; - const triVertC = Vec3f{0.0, starDist, @sqrt(3.0)/2.0 - off}; + const triVertA = Vec3f{ 0.5, starDist, -off }; + const triVertB = Vec3f{ -0.5, starDist, -off }; + const triVertC = Vec3f{ 0.0, starDist, @sqrt(3.0) / 2.0 - off }; - var seed: u64 = 0; + var seed: u64 = 0; - for (0..numStars) |i| { - var pos: Vec3f = undefined; + for (0..numStars) |i| { + var pos: Vec3f = undefined; - var radius: f32 = undefined; + var radius: f32 = undefined; - var temperature: f32 = undefined; + var temperature: f32 = undefined; - var light: f32 = 0; + var light: f32 = 0; - while (light < 0.1) { - pos = getStarPos(&seed); + while (light < 0.1) { + pos = getStarPos(&seed); - radius = @floatCast(main.random.nextFloatExp(&seed)*4 + 0.2); + radius = @floatCast(main.random.nextFloatExp(&seed) * 4 + 0.2); - temperature = @floatCast(@abs(main.random.nextFloatGauss(&seed)*3000.0 + 5000.0) + 1000.0); + temperature = @floatCast(@abs(main.random.nextFloatGauss(&seed) * 3000.0 + 5000.0) + 1000.0); - // 3.6e-12 can be modified to change the brightness of the stars - light = (3.6e-12*radius*radius*temperature*temperature*temperature*temperature)/(vec.dot(pos, pos)); - } + // 3.6e-12 can be modified to change the brightness of the stars + light = (3.6e-12 * radius * radius * temperature * temperature * temperature * temperature) / (vec.dot(pos, pos)); + } - pos = vec.normalize(pos)*@as(Vec3f, @splat(starDist)); + pos = vec.normalize(pos) * @as(Vec3f, @splat(starDist)); - const normPos = vec.normalize(pos); + const normPos = vec.normalize(pos); - const color = getStarColor(temperature, light, starColorImage); + const color = getStarColor(temperature, light, starColorImage); - const latitude: f32 = @floatCast(std.math.asin(normPos[2])); - const longitude: f32 = @floatCast(std.math.atan2(-normPos[0], normPos[1])); + const latitude: f32 = @floatCast(std.math.asin(normPos[2])); + const longitude: f32 = @floatCast(std.math.atan2(-normPos[0], normPos[1])); - const mat = Mat4f.rotationZ(longitude).mul(Mat4f.rotationX(latitude)); + const mat = Mat4f.rotationZ(longitude).mul(Mat4f.rotationX(latitude)); - const posA = vec.xyz(mat.mulVec(.{triVertA[0], triVertA[1], triVertA[2], 1.0})); - const posB = vec.xyz(mat.mulVec(.{triVertB[0], triVertB[1], triVertB[2], 1.0})); - const posC = vec.xyz(mat.mulVec(.{triVertC[0], triVertC[1], triVertC[2], 1.0})); + const posA = vec.xyz(mat.mulVec(.{ triVertA[0], triVertA[1], triVertA[2], 1.0 })); + const posB = vec.xyz(mat.mulVec(.{ triVertB[0], triVertB[1], triVertB[2], 1.0 })); + const posC = vec.xyz(mat.mulVec(.{ triVertC[0], triVertC[1], triVertC[2], 1.0 })); - starData[i*20 ..][0..3].* = posA; - starData[i*20 + 4 ..][0..3].* = posB; - starData[i*20 + 8 ..][0..3].* = posC; + starData[i * 20 ..][0..3].* = posA; + starData[i * 20 + 4 ..][0..3].* = posB; + starData[i * 20 + 8 ..][0..3].* = posC; - starData[i*20 + 12 ..][0..3].* = pos; - starData[i*20 + 16 ..][0..3].* = color; - } + starData[i * 20 + 12 ..][0..3].* = pos; + starData[i * 20 + 16 ..][0..3].* = color; + } - starSsbo = graphics.SSBO.initStatic(f32, &starData); + starSsbo = graphics.SSBO.initStatic(f32, &starData); - starVao = .init(graphics.VertexArray.EmptyVertex, &.{}, null); - } + starVao = .init(graphics.VertexArray.EmptyVertex, &.{}, null); + } - pub fn deinit() void { - starPipeline.deinit(); - starSsbo.deinit(); - starVao.deinit(); - } + pub fn deinit() void { + starPipeline.deinit(); + starSsbo.deinit(); + starVao.deinit(); + } - pub fn render() void { - const viewMatrix = game.camera.viewMatrix; + pub fn render() void { + const viewMatrix = game.camera.viewMatrix; - const time = game.world.?.gameTime.load(.monotonic); + const time = game.world.?.gameTime.load(.monotonic); - var starOpacity: f32 = 0; - const dayTime = @abs(@mod(time, game.World.dayCycle) -% game.World.dayCycle/2); - if (dayTime < game.World.dayCycle/4 - game.World.dayCycle/16) { - starOpacity = 1; - } else if (dayTime > game.World.dayCycle/4 + game.World.dayCycle/16) { - starOpacity = 0; - } else { - starOpacity = 1 - @as(f32, @floatFromInt(dayTime - (game.World.dayCycle/4 - game.World.dayCycle/16)))/@as(f32, @floatFromInt(game.World.dayCycle/8)); - } + var starOpacity: f32 = 0; + const dayTime = @abs(@mod(time, game.World.dayCycle) -% game.World.dayCycle / 2); + if (dayTime < game.World.dayCycle / 4 - game.World.dayCycle / 16) { + starOpacity = 1; + } else if (dayTime > game.World.dayCycle / 4 + game.World.dayCycle / 16) { + starOpacity = 0; + } else { + starOpacity = 1 - @as(f32, @floatFromInt(dayTime - (game.World.dayCycle / 4 - game.World.dayCycle / 16))) / @as(f32, @floatFromInt(game.World.dayCycle / 8)); + } - if (starOpacity != 0) { - starPipeline.bind(null); + if (starOpacity != 0) { + starPipeline.bind(null); - const starMatrix = game.projectionMatrix.mul(viewMatrix.mul(Mat4f.rotationX(@as(f32, @floatFromInt(time))/@as(f32, @floatFromInt(main.game.World.dayCycle))))); + const starMatrix = game.projectionMatrix.mul(viewMatrix.mul(Mat4f.rotationX(@as(f32, @floatFromInt(time)) / @as(f32, @floatFromInt(main.game.World.dayCycle))))); - starSsbo.bind(12); + starSsbo.bind(12); - c.glUniform1f(starUniforms.starOpacity, starOpacity); - c.glUniformMatrix4fv(starUniforms.mvp, 1, c.GL_TRUE, @ptrCast(&starMatrix)); + c.glUniform1f(starUniforms.starOpacity, starOpacity); + c.glUniformMatrix4fv(starUniforms.mvp, 1, c.GL_TRUE, @ptrCast(&starMatrix)); - starVao.bind(); - c.glDrawArrays(c.GL_TRIANGLES, 0, numStars*3); + starVao.bind(); + c.glDrawArrays(c.GL_TRIANGLES, 0, numStars * 3); - c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, 0); - } - } + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, 0); + } + } }; pub const Frustum = struct { // MARK: Frustum - 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, 1, 0, 1})); - const cameraUp = vec.xyz(invRotationMatrix.mulVec(Vec4f{0, 0, 1, 1})); - const cameraRight = vec.xyz(invRotationMatrix.mulVec(Vec4f{1, 0, 0, 1})); - - const halfVSide = std.math.tan(std.math.degreesToRadians(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; - } + 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, 1, 0, 1 })); + const cameraUp = vec.xyz(invRotationMatrix.mulVec(Vec4f{ 0, 0, 1, 1 })); + const cameraRight = vec.xyz(invRotationMatrix.mulVec(Vec4f{ 1, 0, 0, 1 })); + + const halfVSide = std.math.tan(std.math.degreesToRadians(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 { // MARK: MeshSelection - var pipeline: graphics.Pipeline = undefined; - var uniforms: struct { - projectionMatrix: c_int, - viewMatrix: c_int, - modelPosition: c_int, - lowerBounds: c_int, - upperBounds: c_int, - lineSize: c_int, - } = undefined; - - pub fn init() void { - pipeline = graphics.Pipeline.init( - "assets/cubyz/shaders/block_selection_vertex.vert", - "assets/cubyz/shaders/block_selection_fragment.frag", - "", - &uniforms, - graphics.VertexArray.EmptyVertex, - &.{}, - .{.cullMode = .none}, - .{.depthTest = true, .depthWrite = true}, - .{.attachments = &.{.alphaBlending}}, - ); - } - - pub fn deinit() void { - pipeline.deinit(); - } - - var posBeforeBlock: Vec3i = undefined; - var neighborOfSelection: chunk.Neighbor = undefined; - pub var selectedBlockPos: ?Vec3i = null; - var lastSelectedBlockPos: Vec3i = undefined; - var currentBlockProgress: f32 = 0; - var currentSwingProgress: f32 = 0; - var currentSwingTime: f32 = 0; - var selectionMin: Vec3f = undefined; - var selectionMax: Vec3f = undefined; - var selectionNormal: Vec3f = undefined; - var lastPos: Vec3d = undefined; - var lastDir: Vec3f = undefined; - pub fn select(pos: Vec3d, _dir: Vec3f, item: main.items.Item) void { - 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 = 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 = @floor(pos); - - var total_tMax: f64 = 0; - - selectedBlockPos = null; - - while (total_tMax < closestDistance) { - const block = mesh_storage.getBlockFromRenderThread(voxelPos[0], voxelPos[1], voxelPos[2]) orelse break; - if (block.typ != 0) blk: { - if (!block.isSelectableByItem(item)) break :blk; - - const relativePlayerPos: Vec3f = @floatCast(pos - @as(Vec3d, @floatFromInt(voxelPos))); - if (block.mode().rayIntersection(block, item, relativePlayerPos, _dir)) |intersection| { - if (intersection.distance <= closestDistance) { - selectedBlockPos = voxelPos; - selectionMin = intersection.min; - selectionMax = intersection.max; - selectionNormal = intersection.normal; - 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]; - neighborOfSelection = if (step[0] == 1) .dirPosX else .dirNegX; - } else { - voxelPos[2] +%= step[2]; - total_tMax = tMax[2]; - tMax[2] += tDelta[2]; - neighborOfSelection = if (step[2] == 1) .dirUp else .dirDown; - } - } else { - if (tMax[1] < tMax[2]) { - voxelPos[1] +%= step[1]; - total_tMax = tMax[1]; - tMax[1] += tDelta[1]; - neighborOfSelection = if (step[1] == 1) .dirPosY else .dirNegY; - } else { - voxelPos[2] +%= step[2]; - total_tMax = tMax[2]; - tMax[2] += tDelta[2]; - neighborOfSelection = if (step[2] == 1) .dirUp else .dirDown; - } - } - } - // TODO: Test entities - } - - fn canPlaceBlock(pos: Vec3i, block: main.blocks.Block) bool { - if (main.physics.collision.collideWithBlock(block, pos[0], pos[1], pos[2], main.game.Player.getPosBlocking() + main.game.Player.outerBoundingBox.center(), main.game.Player.outerBoundingBox.extent(), .{0, 0, 0}) != null) { - return false; - } - return true; // TODO: Check other entities - } - - pub fn placeBlock(inventory: main.items.Inventory.ClientInventory, slot: u32) void { - if (selectedBlockPos) |selectedPos| { - var oldBlock = mesh_storage.getBlockFromRenderThread(selectedPos[0], selectedPos[1], selectedPos[2]) orelse return; - var block = oldBlock; - switch (inventory.getItem(slot)) { - .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: Vec3f = @floatCast(lastPos - @as(Vec3d, @floatFromInt(selectedPos))); - if (rotationMode.generateData(main.game.world.?, selectedPos, relPos, lastDir, neighborDir, null, &block, .{.typ = 0, .data = 0}, false)) { - if (!canPlaceBlock(selectedPos, block)) return; - updateBlockAndSendUpdate(inventory, slot, selectedPos, oldBlock, block); - return; - } - } else { - if (rotationMode.modifyBlock(&block, itemBlock)) { - if (!canPlaceBlock(selectedPos, block)) return; - updateBlockAndSendUpdate(inventory, slot, selectedPos, oldBlock, block); - return; - } - } - // Check the block in front of it: - const neighborPos = posBeforeBlock; - neighborDir = selectedPos - posBeforeBlock; - const relPos: Vec3f = @floatCast(lastPos - @as(Vec3d, @floatFromInt(neighborPos))); - const neighborBlock = block; - oldBlock = mesh_storage.getBlockFromRenderThread(neighborPos[0], neighborPos[1], neighborPos[2]) orelse return; - block = oldBlock; - if (block.typ == itemBlock) { - if (rotationMode.generateData(main.game.world.?, neighborPos, relPos, lastDir, neighborDir, neighborOfSelection, &block, neighborBlock, false)) { - if (!canPlaceBlock(neighborPos, block)) return; - updateBlockAndSendUpdate(inventory, slot, neighborPos, oldBlock, block); - return; - } - } else { - if (!block.replaceable()) return; - block.typ = itemBlock; - block.data = 0; - if (rotationMode.generateData(main.game.world.?, neighborPos, relPos, lastDir, neighborDir, neighborOfSelection, &block, neighborBlock, true)) { - if (!canPlaceBlock(neighborPos, block)) return; - updateBlockAndSendUpdate(inventory, slot, neighborPos, oldBlock, block); - return; - } - } - } - if (std.mem.eql(u8, baseItem.id(), "cubyz:selection_wand")) { - game.Player.selectionPosition2 = selectedPos; - main.network.protocols.genericUpdate.sendWorldEditPos(main.game.world.?.conn, .selectedPos2, selectedPos); - return; - } - }, - .proceduralItem => |proceduralItem| { - _ = proceduralItem; // TODO: Tools might change existing blocks. - }, - .null => {}, - } - } - } - - pub fn breakBlock(inventory: main.items.Inventory.ClientInventory, slot: u32, deltaTime: f64) void { - if (selectedBlockPos) |selectedPos| { - const stack = inventory.getStack(slot); - const isSelectionWand = stack.item == .baseItem and std.mem.eql(u8, stack.item.baseItem.id(), "cubyz:selection_wand"); - if (isSelectionWand) { - game.Player.selectionPosition1 = selectedPos; - main.network.protocols.genericUpdate.sendWorldEditPos(main.game.world.?.conn, .selectedPos1, selectedPos); - return; - } - - if (@reduce(.Or, lastSelectedBlockPos != selectedPos)) { - mesh_storage.removeBreakingAnimation(lastSelectedBlockPos); - currentSwingProgress = 0; - currentSwingTime = 0; - lastSelectedBlockPos = selectedPos; - currentBlockProgress = 0; - } - const block = mesh_storage.getBlockFromRenderThread(selectedPos[0], selectedPos[1], selectedPos[2]) orelse return; - const holdingTargetedBlock = stack.item == .baseItem and stack.item.baseItem.block() == block.typ; - if ((block.hasTag(.fluid) or block.hasTag(.air)) and !holdingTargetedBlock) return; - - const relPos: Vec3f = @floatCast(lastPos - @as(Vec3d, @floatFromInt(selectedPos))); - - main.sync.ClientSide.mutex.lock(); - if (!game.Player.isCreative()) { - var damage: f32 = main.game.Player.defaultBlockDamage; - const isProceduralItem = stack.item == .proceduralItem; - if (isProceduralItem) { - damage = stack.item.proceduralItem.getBlockDamage(block); - } - damage -= block.blockResistance(); - if (damage > 0) { - const swingTime = if (isProceduralItem and stack.item.proceduralItem.isEffectiveOn(block)) 1.0/stack.item.proceduralItem.getProperty(.swingSpeed) else 0.5; - if (currentSwingTime > swingTime) { - currentSwingProgress = 0; - currentSwingTime = 0; - } - if (currentSwingTime == 0) { - const swings = @ceil(block.blockHealth()/damage); - const damagePerSwing = block.blockHealth()/swings; - currentSwingTime = damagePerSwing/damage*swingTime; - } - currentSwingProgress += @floatCast(deltaTime); - while (currentSwingProgress > currentSwingTime) { - currentSwingProgress -= currentSwingTime; - currentBlockProgress += damage*currentSwingTime/swingTime/block.blockHealth(); - if (currentBlockProgress > 0.9999) break; - const swings = @ceil(block.blockHealth()/damage); - const damagePerSwing = block.blockHealth()/swings; - currentSwingTime = damagePerSwing/damage*swingTime; - } - if (currentBlockProgress < 0.9999) { - mesh_storage.removeBreakingAnimation(lastSelectedBlockPos); - if (currentBlockProgress != 0) { - mesh_storage.addBreakingAnimation(lastSelectedBlockPos, currentBlockProgress); - } - main.sync.ClientSide.mutex.unlock(); - - return; - } else { - currentSwingProgress = 0; - mesh_storage.removeBreakingAnimation(lastSelectedBlockPos); - currentBlockProgress = 0; - currentSwingTime = 0; - } - } else { - main.sync.ClientSide.mutex.unlock(); - return; - } - } else { - mesh_storage.removeBreakingAnimation(lastSelectedBlockPos); - } - - var newBlock = block; - block.mode().onBlockBreaking(inventory.getStack(slot).item, relPos, lastDir, &newBlock); - main.sync.ClientSide.mutex.unlock(); - - if (newBlock != block) { - updateBlockAndSendUpdate(inventory, slot, selectedPos, block, newBlock); - } - } - } - - fn updateBlockAndSendUpdate(source: main.items.Inventory.ClientInventory, slot: u32, pos: Vec3i, oldBlock: blocks.Block, newBlock: blocks.Block) void { - main.sync.ClientSide.executeCommand(.{ - .updateBlock = .{ - .source = .{.inv = source.super, .slot = slot}, - .pos = pos, - .dropLocation = .{ - .normalDir = selectionNormal, - .min = selectionMin, - .max = selectionMax, - }, - .oldBlock = oldBlock, - .newBlock = newBlock, - }, - }); - mesh_storage.updateBlock(.{.pos = pos, .newBlock = newBlock, .blockEntityData = &.{}}); - } - - pub fn drawCube(projectionMatrix: Mat4f, viewMatrix: Mat4f, relativePositionToPlayer: Vec3d, min: Vec3f, max: Vec3f) void { - pipeline.bind(null); - - 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.glUniform1f(uniforms.lineSize, 1.0/128.0); - - main.renderer.chunk_meshing.vao.bind(); - c.glDrawElements(c.GL_TRIANGLES, 12*6*6, c.GL_UNSIGNED_INT, null); - } - - pub fn render(projectionMatrix: Mat4f, viewMatrix: Mat4f, playerPos: Vec3d) void { - if (main.gui.hideGui) return; - if (selectedBlockPos) |_selectedBlockPos| { - drawCube(projectionMatrix, viewMatrix, @as(Vec3d, @floatFromInt(_selectedBlockPos)) - playerPos, selectionMin, selectionMax); - } - if (game.Player.selectionPosition1) |pos1| { - if (game.Player.selectionPosition2) |pos2| { - const bottomLeft: Vec3i = @min(pos1, pos2); - const topRight: Vec3i = @max(pos1, pos2); - drawCube(projectionMatrix, viewMatrix, @as(Vec3d, @floatFromInt(bottomLeft)) - playerPos, .{0, 0, 0}, @floatFromInt(topRight - bottomLeft + Vec3i{1, 1, 1})); - } - } - } + var pipeline: graphics.Pipeline = undefined; + var uniforms: struct { + projectionMatrix: c_int, + viewMatrix: c_int, + modelPosition: c_int, + lowerBounds: c_int, + upperBounds: c_int, + lineSize: c_int, + } = undefined; + + pub fn init() void { + pipeline = graphics.Pipeline.init( + "assets/cubyz/shaders/block_selection_vertex.vert", + "assets/cubyz/shaders/block_selection_fragment.frag", + "", + &uniforms, + graphics.VertexArray.EmptyVertex, + &.{}, + .{ .cullMode = .none }, + .{ .depthTest = true, .depthWrite = true }, + .{ .attachments = &.{.alphaBlending} }, + ); + } + + pub fn deinit() void { + pipeline.deinit(); + } + + var posBeforeBlock: Vec3i = undefined; + var neighborOfSelection: chunk.Neighbor = undefined; + pub var selectedBlockPos: ?Vec3i = null; + var lastSelectedBlockPos: Vec3i = undefined; + var currentBlockProgress: f32 = 0; + var currentSwingProgress: f32 = 0; + var currentSwingTime: f32 = 0; + var selectionMin: Vec3f = undefined; + var selectionMax: Vec3f = undefined; + var selectionNormal: Vec3f = undefined; + var lastPos: Vec3d = undefined; + var lastDir: Vec3f = undefined; + pub fn select(pos: Vec3d, _dir: Vec3f, item: main.items.Item) void { + 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 = 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 = @floor(pos); + + var total_tMax: f64 = 0; + + selectedBlockPos = null; + + while (total_tMax < closestDistance) { + const block = mesh_storage.getBlockFromRenderThread(voxelPos[0], voxelPos[1], voxelPos[2]) orelse break; + if (block.typ != 0) blk: { + if (!block.isSelectableByItem(item)) break :blk; + + const relativePlayerPos: Vec3f = @floatCast(pos - @as(Vec3d, @floatFromInt(voxelPos))); + if (block.mode().rayIntersection(block, item, relativePlayerPos, _dir)) |intersection| { + if (intersection.distance <= closestDistance) { + selectedBlockPos = voxelPos; + selectionMin = intersection.min; + selectionMax = intersection.max; + selectionNormal = intersection.normal; + 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]; + neighborOfSelection = if (step[0] == 1) .dirPosX else .dirNegX; + } else { + voxelPos[2] +%= step[2]; + total_tMax = tMax[2]; + tMax[2] += tDelta[2]; + neighborOfSelection = if (step[2] == 1) .dirUp else .dirDown; + } + } else { + if (tMax[1] < tMax[2]) { + voxelPos[1] +%= step[1]; + total_tMax = tMax[1]; + tMax[1] += tDelta[1]; + neighborOfSelection = if (step[1] == 1) .dirPosY else .dirNegY; + } else { + voxelPos[2] +%= step[2]; + total_tMax = tMax[2]; + tMax[2] += tDelta[2]; + neighborOfSelection = if (step[2] == 1) .dirUp else .dirDown; + } + } + } + // TODO: Test entities + } + + fn canPlaceBlock(pos: Vec3i, block: main.blocks.Block) bool { + if (main.physics.collision.collideWithBlock(block, pos[0], pos[1], pos[2], main.game.Player.getPosBlocking() + main.game.Player.outerBoundingBox.center(), main.game.Player.outerBoundingBox.extent(), .{ 0, 0, 0 }) != null) { + return false; + } + return true; // TODO: Check other entities + } + + pub fn placeBlock(inventory: main.items.Inventory.ClientInventory, slot: u32) void { + if (selectedBlockPos) |selectedPos| { + var oldBlock = mesh_storage.getBlockFromRenderThread(selectedPos[0], selectedPos[1], selectedPos[2]) orelse return; + var block = oldBlock; + switch (inventory.getItem(slot)) { + .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: Vec3f = @floatCast(lastPos - @as(Vec3d, @floatFromInt(selectedPos))); + if (rotationMode.generateData(main.game.world.?, selectedPos, relPos, lastDir, neighborDir, null, &block, .{ .typ = 0, .data = 0 }, false)) { + if (!canPlaceBlock(selectedPos, block)) return; + updateBlockAndSendUpdate(inventory, slot, selectedPos, oldBlock, block); + return; + } + } else { + if (rotationMode.modifyBlock(&block, itemBlock)) { + if (!canPlaceBlock(selectedPos, block)) return; + updateBlockAndSendUpdate(inventory, slot, selectedPos, oldBlock, block); + return; + } + } + // Check the block in front of it: + const neighborPos = posBeforeBlock; + neighborDir = selectedPos - posBeforeBlock; + const relPos: Vec3f = @floatCast(lastPos - @as(Vec3d, @floatFromInt(neighborPos))); + const neighborBlock = block; + oldBlock = mesh_storage.getBlockFromRenderThread(neighborPos[0], neighborPos[1], neighborPos[2]) orelse return; + block = oldBlock; + if (block.typ == itemBlock) { + if (rotationMode.generateData(main.game.world.?, neighborPos, relPos, lastDir, neighborDir, neighborOfSelection, &block, neighborBlock, false)) { + if (!canPlaceBlock(neighborPos, block)) return; + updateBlockAndSendUpdate(inventory, slot, neighborPos, oldBlock, block); + return; + } + } else { + if (!block.replaceable()) return; + block.typ = itemBlock; + block.data = 0; + if (rotationMode.generateData(main.game.world.?, neighborPos, relPos, lastDir, neighborDir, neighborOfSelection, &block, neighborBlock, true)) { + if (!canPlaceBlock(neighborPos, block)) return; + updateBlockAndSendUpdate(inventory, slot, neighborPos, oldBlock, block); + return; + } + } + } + if (std.mem.eql(u8, baseItem.id(), "cubyz:selection_wand")) { + game.Player.selectionPosition2 = selectedPos; + main.network.protocols.genericUpdate.sendWorldEditPos(main.game.world.?.conn, .selectedPos2, selectedPos); + return; + } + }, + .proceduralItem => |proceduralItem| { + _ = proceduralItem; // TODO: Tools might change existing blocks. + }, + .null => {}, + } + } + } + + pub fn breakBlock(inventory: main.items.Inventory.ClientInventory, slot: u32, deltaTime: f64) void { + if (selectedBlockPos) |selectedPos| { + const stack = inventory.getStack(slot); + const isSelectionWand = stack.item == .baseItem and std.mem.eql(u8, stack.item.baseItem.id(), "cubyz:selection_wand"); + if (isSelectionWand) { + game.Player.selectionPosition1 = selectedPos; + main.network.protocols.genericUpdate.sendWorldEditPos(main.game.world.?.conn, .selectedPos1, selectedPos); + return; + } + + if (@reduce(.Or, lastSelectedBlockPos != selectedPos)) { + mesh_storage.removeBreakingAnimation(lastSelectedBlockPos); + currentSwingProgress = 0; + currentSwingTime = 0; + lastSelectedBlockPos = selectedPos; + currentBlockProgress = 0; + } + const block = mesh_storage.getBlockFromRenderThread(selectedPos[0], selectedPos[1], selectedPos[2]) orelse return; + const holdingTargetedBlock = stack.item == .baseItem and stack.item.baseItem.block() == block.typ; + if ((block.hasTag(.fluid) or block.hasTag(.air)) and !holdingTargetedBlock) return; + + const relPos: Vec3f = @floatCast(lastPos - @as(Vec3d, @floatFromInt(selectedPos))); + + main.sync.ClientSide.mutex.lock(); + if (!game.Player.isCreative()) { + var damage: f32 = main.game.Player.defaultBlockDamage; + const isProceduralItem = stack.item == .proceduralItem; + if (isProceduralItem) { + damage = stack.item.proceduralItem.getBlockDamage(block); + } + damage -= block.blockResistance(); + if (damage > 0) { + const swingTime = if (isProceduralItem and stack.item.proceduralItem.isEffectiveOn(block)) 1.0 / stack.item.proceduralItem.getProperty(.swingSpeed) else 0.5; + if (currentSwingTime > swingTime) { + currentSwingProgress = 0; + currentSwingTime = 0; + } + if (currentSwingTime == 0) { + const swings = @ceil(block.blockHealth() / damage); + const damagePerSwing = block.blockHealth() / swings; + currentSwingTime = damagePerSwing / damage * swingTime; + } + currentSwingProgress += @floatCast(deltaTime); + while (currentSwingProgress > currentSwingTime) { + currentSwingProgress -= currentSwingTime; + currentBlockProgress += damage * currentSwingTime / swingTime / block.blockHealth(); + if (currentBlockProgress > 0.9999) break; + const swings = @ceil(block.blockHealth() / damage); + const damagePerSwing = block.blockHealth() / swings; + currentSwingTime = damagePerSwing / damage * swingTime; + } + if (currentBlockProgress < 0.9999) { + mesh_storage.removeBreakingAnimation(lastSelectedBlockPos); + if (currentBlockProgress != 0) { + mesh_storage.addBreakingAnimation(lastSelectedBlockPos, currentBlockProgress); + } + main.sync.ClientSide.mutex.unlock(); + + return; + } else { + currentSwingProgress = 0; + mesh_storage.removeBreakingAnimation(lastSelectedBlockPos); + currentBlockProgress = 0; + currentSwingTime = 0; + } + } else { + main.sync.ClientSide.mutex.unlock(); + return; + } + } else { + mesh_storage.removeBreakingAnimation(lastSelectedBlockPos); + } + + var newBlock = block; + block.mode().onBlockBreaking(inventory.getStack(slot).item, relPos, lastDir, &newBlock); + main.sync.ClientSide.mutex.unlock(); + + if (newBlock != block) { + updateBlockAndSendUpdate(inventory, slot, selectedPos, block, newBlock); + } + } + } + + fn updateBlockAndSendUpdate(source: main.items.Inventory.ClientInventory, slot: u32, pos: Vec3i, oldBlock: blocks.Block, newBlock: blocks.Block) void { + main.sync.ClientSide.executeCommand(.{ + .updateBlock = .{ + .source = .{ .inv = source.super, .slot = slot }, + .pos = pos, + .dropLocation = .{ + .normalDir = selectionNormal, + .min = selectionMin, + .max = selectionMax, + }, + .oldBlock = oldBlock, + .newBlock = newBlock, + }, + }); + mesh_storage.updateBlock(.{ .pos = pos, .newBlock = newBlock, .blockEntityData = &.{} }); + } + + pub fn drawCube(projectionMatrix: Mat4f, viewMatrix: Mat4f, relativePositionToPlayer: Vec3d, min: Vec3f, max: Vec3f) void { + pipeline.bind(null); + + 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.glUniform1f(uniforms.lineSize, 1.0 / 128.0); + + main.renderer.chunk_meshing.vao.bind(); + c.glDrawElements(c.GL_TRIANGLES, 12 * 6 * 6, c.GL_UNSIGNED_INT, null); + } + + pub fn render(projectionMatrix: Mat4f, viewMatrix: Mat4f, playerPos: Vec3d) void { + if (main.gui.hideGui) return; + if (selectedBlockPos) |_selectedBlockPos| { + drawCube(projectionMatrix, viewMatrix, @as(Vec3d, @floatFromInt(_selectedBlockPos)) - playerPos, selectionMin, selectionMax); + } + if (game.Player.selectionPosition1) |pos1| { + if (game.Player.selectionPosition2) |pos2| { + const bottomLeft: Vec3i = @min(pos1, pos2); + const topRight: Vec3i = @max(pos1, pos2); + drawCube(projectionMatrix, viewMatrix, @as(Vec3d, @floatFromInt(bottomLeft)) - playerPos, .{ 0, 0, 0 }, @floatFromInt(topRight - bottomLeft + Vec3i{ 1, 1, 1 })); + } + } + } }; From cf0837e6984ca3819a27c24eb1ec48efa8972f14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Fernandes?= Date: Fri, 8 May 2026 10:27:00 +0100 Subject: [PATCH 11/14] formatting --- src/blocks.zig | 1000 ++++---- src/graphics.zig | 4418 ++++++++++++++++----------------- src/gui/windows/controls.zig | 84 +- src/gui/windows/crosshair.zig | 74 +- src/gui/windows/main.zig | 30 +- src/itemdrop.zig | 824 +++--- src/particles.zig | 636 ++--- src/renderer.zig | 2106 ++++++++-------- 8 files changed, 4586 insertions(+), 4586 deletions(-) diff --git a/src/blocks.zig b/src/blocks.zig index 36eaf95e6b..128825fa2f 100644 --- a/src/blocks.zig +++ b/src/blocks.zig @@ -35,40 +35,40 @@ const BlockEntityType = block_entity.BlockEntityType; pub const maxBlockCount: usize = 65536; // 16 bit limit pub const BlockDrop = struct { - items: []const items.ItemStack, - chance: f32, - forbiddenToolTags: []Tag, - allowedToolTags: ?[]Tag = null, - - pub fn isDroppedWhenBrokenWithItem(self: BlockDrop, item: Item) bool { - if (item != .proceduralItem) return self.allowedToolTags == null; - - const proceduralItem = item.proceduralItem; - for (self.forbiddenToolTags) |tag| if (proceduralItem.hasTag(tag)) return false; - if (self.allowedToolTags) |tags| { - for (tags) |tag| if (proceduralItem.hasTag(tag)) return true; - return false; - } - - return true; - } + items: []const items.ItemStack, + chance: f32, + forbiddenToolTags: []Tag, + allowedToolTags: ?[]Tag = null, + + pub fn isDroppedWhenBrokenWithItem(self: BlockDrop, item: Item) bool { + if (item != .proceduralItem) return self.allowedToolTags == null; + + const proceduralItem = item.proceduralItem; + for (self.forbiddenToolTags) |tag| if (proceduralItem.hasTag(tag)) return false; + if (self.allowedToolTags) |tags| { + for (tags) |tag| if (proceduralItem.hasTag(tag)) return true; + return false; + } + + return true; + } }; /// Ores can be found underground in veins. /// TODO: Add support for non-stone ores. pub const Ore = struct { - /// average size of a vein in blocks - size: f32, - /// average density of a vein - density: f32, - /// average veins per chunk - veins: f32, - /// maximum height this ore can be generated - maxHeight: i32, - minHeight: i32, - - blockType: u16, - seed: u64, + /// average size of a vein in blocks + size: f32, + /// average density of a vein + density: f32, + /// average veins per chunk + veins: f32, + /// maximum height this ore can be generated + maxHeight: i32, + minHeight: i32, + + blockType: u16, + seed: u64, }; const SelectionCapabilities = struct { @@ -231,195 +231,195 @@ pub fn register(_: []const u8, id: []const u8, zon: ZonElement) u16 { } pub fn loadBlockDrop(blockId: ?[]const u8, zon: ZonElement) []const BlockDrop { - const drops = zon.getChild("drops").toSlice(); - const blockDrops = main.worldArena.alloc(BlockDrop, drops.len); - - for (drops, 0..) |blockDrop, i| { - const itemZons = blockDrop.getChild("items").toSlice(); - var resultItems = main.List(items.ItemStack).initCapacity(main.worldArena, itemZons.len); - - for (itemZons) |itemZon| { - var string = itemZon.as([]const u8, "auto"); - string = std.mem.trim(u8, string, " "); - var iterator = std.mem.splitScalar(u8, string, ' '); - var name = iterator.first(); - var amount: u16 = 1; - while (iterator.next()) |next| { - if (next.len == 0) continue; // skip multiple spaces. - amount = std.fmt.parseInt(u16, name, 0) catch 1; - name = next; - break; - } - - if (std.mem.eql(u8, name, "auto")) { - if (blockId) |id| { - name = id; - } else std.log.err("Cannot use 'auto' in this context", .{}); - } - - const item = items.BaseItemIndex.fromId(name) orelse continue; - resultItems.append(.{ .item = .{ .baseItem = item }, .amount = amount }); - } - - var allowedToolTags: ?[]Tag = null; - if (blockDrop.getChildOrNull("allowedToolTags")) |tagZon| { - const tags = Tag.loadTagsFromZon(main.worldArena, tagZon); - if (tags.len == 0) { - std.log.err("Field '.allowedToolTags' is an empty array. No tool can drop this blockDrop", .{}); - } - allowedToolTags = tags; - } - - blockDrops[i] = .{ - .items = resultItems.items, - .chance = blockDrop.get(f32, "chance", 1), - .forbiddenToolTags = Tag.loadTagsFromZon(main.worldArena, blockDrop.getChild("forbiddenToolTags")), - .allowedToolTags = allowedToolTags, - }; - } - return blockDrops; + const drops = zon.getChild("drops").toSlice(); + const blockDrops = main.worldArena.alloc(BlockDrop, drops.len); + + for (drops, 0..) |blockDrop, i| { + const itemZons = blockDrop.getChild("items").toSlice(); + var resultItems = main.List(items.ItemStack).initCapacity(main.worldArena, itemZons.len); + + for (itemZons) |itemZon| { + var string = itemZon.as([]const u8, "auto"); + string = std.mem.trim(u8, string, " "); + var iterator = std.mem.splitScalar(u8, string, ' '); + var name = iterator.first(); + var amount: u16 = 1; + while (iterator.next()) |next| { + if (next.len == 0) continue; // skip multiple spaces. + amount = std.fmt.parseInt(u16, name, 0) catch 1; + name = next; + break; + } + + if (std.mem.eql(u8, name, "auto")) { + if (blockId) |id| { + name = id; + } else std.log.err("Cannot use 'auto' in this context", .{}); + } + + const item = items.BaseItemIndex.fromId(name) orelse continue; + resultItems.append(.{.item = .{.baseItem = item}, .amount = amount}); + } + + var allowedToolTags: ?[]Tag = null; + if (blockDrop.getChildOrNull("allowedToolTags")) |tagZon| { + const tags = Tag.loadTagsFromZon(main.worldArena, tagZon); + if (tags.len == 0) { + std.log.err("Field '.allowedToolTags' is an empty array. No tool can drop this blockDrop", .{}); + } + allowedToolTags = tags; + } + + blockDrops[i] = .{ + .items = resultItems.items, + .chance = blockDrop.get(f32, "chance", 1), + .forbiddenToolTags = Tag.loadTagsFromZon(main.worldArena, blockDrop.getChild("forbiddenToolTags")), + .allowedToolTags = allowedToolTags, + }; + } + return blockDrops; } fn registerBlockDrop(typ: u16, zon: ZonElement) void { - _blockDrops[typ] = loadBlockDrop(_id[typ], zon); + _blockDrops[typ] = loadBlockDrop(_id[typ], zon); } fn registerLodReplacement(typ: u16, zon: ZonElement) void { - if (zon.get(?[]const u8, "lodReplacement", null)) |replacement| { - _lodReplacement[typ] = getTypeById(replacement); - } else { - _lodReplacement[typ] = typ; - } + if (zon.get(?[]const u8, "lodReplacement", null)) |replacement| { + _lodReplacement[typ] = getTypeById(replacement); + } else { + _lodReplacement[typ] = typ; + } } fn registerOpaqueVariant(typ: u16, zon: ZonElement) void { - if (zon.get(?[]const u8, "opaqueVariant", null)) |replacement| { - _opaqueVariant[typ] = getTypeById(replacement); - } else { - _opaqueVariant[typ] = typ; - } + if (zon.get(?[]const u8, "opaqueVariant", null)) |replacement| { + _opaqueVariant[typ] = getTypeById(replacement); + } else { + _opaqueVariant[typ] = typ; + } } fn registerCallbacks(typ: u16, zon: ZonElement) void { - _onInteract[typ] = blk: { - break :blk ClientBlockCallback.init(zon.getChildOrNull("onInteract") orelse break :blk .noop) orelse { - std.log.err("Failed to load onInteract event for block {s}", .{_id[typ]}); - break :blk .noop; - }; - }; - _onBreak[typ] = blk: { - break :blk ServerBlockCallback.init(zon.getChildOrNull("onBreak") orelse break :blk .noop) orelse { - std.log.err("Failed to load onBreak event for block {s}", .{_id[typ]}); - break :blk .noop; - }; - }; - _onUpdate[typ] = blk: { - break :blk ServerBlockCallback.init(zon.getChildOrNull("onUpdate") orelse break :blk .noop) orelse { - std.log.err("Failed to load onUpdate event for block {s}", .{_id[typ]}); - break :blk .noop; - }; - }; - _onTick[typ] = blk: { - break :blk ServerBlockCallback.init(zon.getChildOrNull("onTick") orelse break :blk .noop) orelse { - std.log.err("Failed to load onTick event for block {s}", .{_id[typ]}); - break :blk .noop; - }; - }; - _onTouch[typ] = blk: { - break :blk BlockTouchCallback.init(zon.getChildOrNull("onTouch") orelse break :blk .noop) orelse { - std.log.err("Failed to load onTouch event for block {s}", .{_id[typ]}); - break :blk .noop; - }; - }; + _onInteract[typ] = blk: { + break :blk ClientBlockCallback.init(zon.getChildOrNull("onInteract") orelse break :blk .noop) orelse { + std.log.err("Failed to load onInteract event for block {s}", .{_id[typ]}); + break :blk .noop; + }; + }; + _onBreak[typ] = blk: { + break :blk ServerBlockCallback.init(zon.getChildOrNull("onBreak") orelse break :blk .noop) orelse { + std.log.err("Failed to load onBreak event for block {s}", .{_id[typ]}); + break :blk .noop; + }; + }; + _onUpdate[typ] = blk: { + break :blk ServerBlockCallback.init(zon.getChildOrNull("onUpdate") orelse break :blk .noop) orelse { + std.log.err("Failed to load onUpdate event for block {s}", .{_id[typ]}); + break :blk .noop; + }; + }; + _onTick[typ] = blk: { + break :blk ServerBlockCallback.init(zon.getChildOrNull("onTick") orelse break :blk .noop) orelse { + std.log.err("Failed to load onTick event for block {s}", .{_id[typ]}); + break :blk .noop; + }; + }; + _onTouch[typ] = blk: { + break :blk BlockTouchCallback.init(zon.getChildOrNull("onTouch") orelse break :blk .noop) orelse { + std.log.err("Failed to load onTouch event for block {s}", .{_id[typ]}); + break :blk .noop; + }; + }; } pub fn finishBlocks(zonElements: Assets.ZonHashMap) void { - var i: u16 = 0; - while (i < size) : (i += 1) { - registerBlockDrop(i, zonElements.get(_id[i]) orelse continue); - registerLodReplacement(i, zonElements.get(_id[i]) orelse continue); - registerOpaqueVariant(i, zonElements.get(_id[i]) orelse continue); - registerCallbacks(i, zonElements.get(_id[i]) orelse continue); - } - blueprint.registerVoidBlock(parseBlock("cubyz:void")); - meshes.finishTextureLoading(); + var i: u16 = 0; + while (i < size) : (i += 1) { + registerBlockDrop(i, zonElements.get(_id[i]) orelse continue); + registerLodReplacement(i, zonElements.get(_id[i]) orelse continue); + registerOpaqueVariant(i, zonElements.get(_id[i]) orelse continue); + registerCallbacks(i, zonElements.get(_id[i]) orelse continue); + } + blueprint.registerVoidBlock(parseBlock("cubyz:void")); + meshes.finishTextureLoading(); } pub fn reset() void { - size = 0; - ores = .{}; - reverseIndices = .{}; - meshes.reset(); + size = 0; + ores = .{}; + reverseIndices = .{}; + meshes.reset(); } pub fn getTypeById(id: []const u8) u16 { - if (reverseIndices.get(id)) |result| { - return result; - } else { - std.log.err("Couldn't find block {s}. Replacing it with air...", .{id}); - return 0; - } + if (reverseIndices.get(id)) |result| { + return result; + } else { + std.log.err("Couldn't find block {s}. Replacing it with air...", .{id}); + return 0; + } } const ParseBlockConfig = struct { - applyMigrations: bool = false, + applyMigrations: bool = false, }; fn parseBlockData(fullBlockId: []const u8, data: []const u8, comptime config: ParseBlockConfig) ?u16 { - if (std.mem.containsAtLeastScalar(u8, data, 1, ':')) { - const oreChild = parseBlockWithOptions(data, config); - if (oreChild.data != 0) { - std.log.warn("Error while parsing ore block data of '{s}': Parent block data must be 0.", .{fullBlockId}); - } - return oreChild.typ; - } - return std.fmt.parseInt(u16, data, 0) catch |err| { - std.log.err("Error while parsing block data of '{s}': {s}", .{ fullBlockId, @errorName(err) }); - return null; - }; + if (std.mem.containsAtLeastScalar(u8, data, 1, ':')) { + const oreChild = parseBlockWithOptions(data, config); + if (oreChild.data != 0) { + std.log.warn("Error while parsing ore block data of '{s}': Parent block data must be 0.", .{fullBlockId}); + } + return oreChild.typ; + } + return std.fmt.parseInt(u16, data, 0) catch |err| { + std.log.err("Error while parsing block data of '{s}': {s}", .{fullBlockId, @errorName(err)}); + return null; + }; } pub fn parseBlock(data: []const u8) Block { - return parseBlockWithOptions(data, .{}); + return parseBlockWithOptions(data, .{}); } pub fn parseBlockWithOptions(data: []const u8, comptime config: ParseBlockConfig) Block { - var id: []const u8 = data; - var blockData: ?u16 = null; - if (std.mem.indexOfScalarPos(u8, data, 1 + (std.mem.indexOfScalar(u8, data, ':') orelse 0), ':')) |pos| { - id = data[0..pos]; - blockData = parseBlockData(data, data[pos + 1 ..], config); - } - if (config.applyMigrations) { - id = main.migrations.applySingle(.block, id); - } - if (reverseIndices.get(id)) |resultType| { - var result: Block = .{ .typ = resultType, .data = 0 }; - result.data = blockData orelse result.mode().naturalStandard; - return result; - } else { - std.log.err("Couldn't find block {s}. Replacing it with air...", .{id}); - return .{ .typ = 0, .data = 0 }; - } + var id: []const u8 = data; + var blockData: ?u16 = null; + if (std.mem.indexOfScalarPos(u8, data, 1 + (std.mem.indexOfScalar(u8, data, ':') orelse 0), ':')) |pos| { + id = data[0..pos]; + blockData = parseBlockData(data, data[pos + 1 ..], config); + } + if (config.applyMigrations) { + id = main.migrations.applySingle(.block, id); + } + if (reverseIndices.get(id)) |resultType| { + var result: Block = .{.typ = resultType, .data = 0}; + result.data = blockData orelse result.mode().naturalStandard; + return result; + } else { + std.log.err("Couldn't find block {s}. Replacing it with air...", .{id}); + return .{.typ = 0, .data = 0}; + } } pub fn getBlockById(idAndData: []const u8) !u16 { - const addonNameSeparatorIndex = std.mem.indexOfScalar(u8, idAndData, ':') orelse return error.MissingAddonNameSeparator; - const blockIdEndIndex = std.mem.indexOfScalarPos(u8, idAndData, 1 + addonNameSeparatorIndex, ':') orelse idAndData.len; - const id = idAndData[0..blockIdEndIndex]; - return reverseIndices.get(id) orelse return error.NotFound; + const addonNameSeparatorIndex = std.mem.indexOfScalar(u8, idAndData, ':') orelse return error.MissingAddonNameSeparator; + const blockIdEndIndex = std.mem.indexOfScalarPos(u8, idAndData, 1 + addonNameSeparatorIndex, ':') orelse idAndData.len; + const id = idAndData[0..blockIdEndIndex]; + return reverseIndices.get(id) orelse return error.NotFound; } pub fn getBlockData(idLikeString: []const u8) !?u16 { - const addonNameSeparatorIndex = std.mem.indexOfScalar(u8, idLikeString, ':') orelse return error.MissingAddonNameSeparator; - const blockIdEndIndex = std.mem.indexOfScalarPos(u8, idLikeString, 1 + addonNameSeparatorIndex, ':') orelse return null; - const dataString = idLikeString[blockIdEndIndex + 1 ..]; - if (dataString.len == 0) return error.EmptyDataString; - return std.fmt.parseInt(u16, dataString, 0) catch return error.InvalidData; + const addonNameSeparatorIndex = std.mem.indexOfScalar(u8, idLikeString, ':') orelse return error.MissingAddonNameSeparator; + const blockIdEndIndex = std.mem.indexOfScalarPos(u8, idLikeString, 1 + addonNameSeparatorIndex, ':') orelse return null; + const dataString = idLikeString[blockIdEndIndex + 1 ..]; + if (dataString.len == 0) return error.EmptyDataString; + return std.fmt.parseInt(u16, dataString, 0) catch return error.InvalidData; } pub fn hasRegistered(id: []const u8) bool { - return reverseIndices.contains(id); + return reverseIndices.contains(id); } pub const Block = packed struct(u32) { // MARK: Block @@ -585,328 +585,328 @@ pub const Block = packed struct(u32) { // MARK: Block }; pub const meshes = struct { // MARK: meshes - const AnimationData = extern struct { - startFrame: u32, - frames: u32, - time: u32, - }; - - const FogData = extern struct { - fogDensity: f32, - fogColor: u32, - }; - var size: u32 = 0; - var _modelIndex: [maxBlockCount]ModelIndex = undefined; - var textureIndices: [maxBlockCount][16]u16 = undefined; - /// Stores the number of textures after each block was added. Used to clean additional textures when the world is switched. - var maxTextureCount: [maxBlockCount]u32 = undefined; - /// Number of loaded meshes. Used to determine if an update is needed. - var loadedMeshes: u32 = 0; - - var textureIds: main.ListUnmanaged([]const u8) = .{}; - var texturePaths: main.ListUnmanaged([]const u8) = .{}; - var animationData: []AnimationData = &.{}; - var blockTextures: main.ListUnmanaged(Image) = .{}; - var emissionTextures: main.ListUnmanaged(Image) = .{}; - var reflectivityTextures: main.ListUnmanaged(Image) = .{}; - var absorptionTextures: main.ListUnmanaged(Image) = .{}; - var textureFogData: main.ListUnmanaged(FogData) = .{}; - pub var textureOcclusionData: []std.atomic.Value(bool) = &.{}; - - pub var blockBreakingTextures: main.ListUnmanaged(u16) = .{}; - - const sideNames = blk: { - var names: [6][]const u8 = undefined; - names[Neighbor.dirDown.toInt()] = "texture_bottom"; - names[Neighbor.dirUp.toInt()] = "texture_top"; - names[Neighbor.dirPosX.toInt()] = "texture_right"; - names[Neighbor.dirNegX.toInt()] = "texture_left"; - names[Neighbor.dirPosY.toInt()] = "texture_front"; - names[Neighbor.dirNegY.toInt()] = "texture_back"; - break :blk names; - }; - - var animationSSBO: ?SSBO = null; - var animatedTextureSSBO: ?SSBO = null; - var fogSSBO: ?SSBO = null; - - var animationComputePipeline: graphics.ComputePipeline = undefined; - var animationUniforms: struct { - time: c_int, - size: c_int, - } = undefined; - - pub var blockTextureArray: TextureArray = undefined; - pub var emissionTextureArray: TextureArray = undefined; - pub var reflectivityAndAbsorptionTextureArray: TextureArray = undefined; - pub var ditherTexture: graphics.Texture = undefined; - - const black: Color = Color{ .r = 0, .g = 0, .b = 0, .a = 255 }; - const magenta: Color = Color{ .r = 255, .g = 0, .b = 255, .a = 255 }; - var undefinedTexture = [_]Color{ magenta, black, black, magenta }; - const undefinedImage = Image{ .width = 2, .height = 2, .imageData = undefinedTexture[0..] }; - var emptyTexture = [_]Color{black}; - const emptyImage = Image{ .width = 1, .height = 1, .imageData = emptyTexture[0..] }; - - pub fn init() void { - animationComputePipeline = graphics.ComputePipeline.init("assets/cubyz/shaders/animation_pre_processing.comp", "", &animationUniforms); - blockTextureArray = .init(); - emissionTextureArray = .init(); - reflectivityAndAbsorptionTextureArray = .init(); - ditherTexture = .initFromMipmapFiles("assets/cubyz/blocks/textures/dither/", 64, 0.5); - } - - pub fn deinit() void { - if (animationSSBO) |ssbo| { - ssbo.deinit(); - } - if (animatedTextureSSBO) |ssbo| { - ssbo.deinit(); - } - if (fogSSBO) |ssbo| { - ssbo.deinit(); - } - animationComputePipeline.deinit(); - blockTextureArray.deinit(); - emissionTextureArray.deinit(); - reflectivityAndAbsorptionTextureArray.deinit(); - ditherTexture.deinit(); - } - - pub fn reset() void { - meshes.size = 0; - loadedMeshes = 0; - textureIds = .{}; - texturePaths = .{}; - animationData = &.{}; - blockTextures = .{}; - emissionTextures = .{}; - reflectivityTextures = .{}; - absorptionTextures = .{}; - textureFogData = .{}; - textureOcclusionData = &.{}; - blockBreakingTextures = .{}; - } - - pub inline fn model(block: Block) ModelIndex { - return block.mode().model(block); - } - - pub inline fn modelIndexStart(block: Block) ModelIndex { - return _modelIndex[block.typ]; - } - - pub inline fn fogDensity(block: Block) f32 { - return textureFogData.items[animationData[textureIndices[block.typ][0]].startFrame].fogDensity; - } - - pub inline fn fogColor(block: Block) u32 { - return textureFogData.items[animationData[textureIndices[block.typ][0]].startFrame].fogColor; - } - - pub inline fn hasFog(block: Block) bool { - return fogDensity(block) != 0.0; - } - - pub inline fn textureIndex(block: Block, orientation: usize) u16 { - if (orientation < 16) { - return textureIndices[block.typ][orientation]; - } else { - return textureIndices[block.data][orientation - 16]; - } - } - - fn extendedPath(_allocator: main.heap.NeverFailingAllocator, path: []const u8, ending: []const u8) []const u8 { - return std.fmt.allocPrint(_allocator.allocator, "{s}{s}", .{ path, ending }) catch unreachable; - } - - fn readTextureFile(_path: []const u8, ending: []const u8, default: Image) Image { - const path = extendedPath(main.stackAllocator, _path, ending); - defer main.stackAllocator.free(path); - return Image.readFromFile(main.worldArena, path) catch default; - } - - fn extractAnimationSlice(image: Image, frame: usize, frames: usize) Image { - if (image.height < frames) return image; - var startHeight = image.height / frames * frame; - if (image.height % frames > frame) startHeight += frame else startHeight += image.height % frames; - var endHeight = image.height / frames * (frame + 1); - if (image.height % frames > frame + 1) endHeight += frame + 1 else endHeight += image.height % frames; - var result = image; - result.height = @intCast(endHeight - startHeight); - result.imageData = result.imageData[startHeight * image.width .. endHeight * image.width]; - return result; - } - - fn readTextureData(index: usize, _path: []const u8) void { - const path = _path[0 .. _path.len - ".png".len]; - const textureInfoPath = extendedPath(main.stackAllocator, path, ".zig.zon"); - defer main.stackAllocator.free(textureInfoPath); - const textureInfoZon = main.files.cwd().readToZon(main.stackAllocator, textureInfoPath) catch .null; - defer textureInfoZon.deinit(main.stackAllocator); - const animationFrames = textureInfoZon.get(u32, "frames", 1); - const animationTime = textureInfoZon.get(u32, "time", 1); - animationData[index] = .{ .startFrame = @intCast(blockTextures.items.len), .frames = animationFrames, .time = animationTime }; - const base = readTextureFile(path, ".png", Image.defaultImage); - const emission = readTextureFile(path, "_emission.png", Image.emptyImage); - const reflectivity = readTextureFile(path, "_reflectivity.png", Image.emptyImage); - const absorption = readTextureFile(path, "_absorption.png", Image.whiteEmptyImage); - for (0..animationFrames) |i| { - blockTextures.append(main.worldArena, extractAnimationSlice(base, i, animationFrames)); - emissionTextures.append(main.worldArena, extractAnimationSlice(emission, i, animationFrames)); - reflectivityTextures.append(main.worldArena, extractAnimationSlice(reflectivity, i, animationFrames)); - absorptionTextures.append(main.worldArena, extractAnimationSlice(absorption, i, animationFrames)); - textureFogData.append(main.worldArena, .{ - .fogDensity = textureInfoZon.get(f32, "fogDensity", 0.0), - .fogColor = textureInfoZon.get(u32, "fogColor", 0xffffff), - }); - } - textureOcclusionData[index].store(textureInfoZon.get(bool, "hasOcclusion", true), .monotonic); - } - - pub fn findTexture(_textureId: ?[]const u8, assetFolder: []const u8) !u16 { - const textureId = _textureId orelse return error.NotFound; - var result: u16 = undefined; - var splitter = std.mem.splitScalar(u8, textureId, ':'); - const mod = splitter.first(); - const id = splitter.rest(); - var path = try std.fmt.allocPrint(main.stackAllocator.allocator, "{s}/{s}/blocks/textures/{s}.png", .{ assetFolder, mod, id }); - defer main.stackAllocator.free(path); - // Test if it's already in the list: - for (textureIds.items, 0..) |other, j| { - if (std.mem.eql(u8, other, textureId)) { - result = @intCast(j); - return result; - } - } - const file = main.files.cwd().openFile(path) catch |err| blk: { - if (err != error.FileNotFound) { - std.log.err("Could not open file {s}: {s}", .{ path, @errorName(err) }); - } - main.stackAllocator.free(path); - path = try std.fmt.allocPrint(main.stackAllocator.allocator, "assets/{s}/blocks/textures/{s}.png", .{ mod, id }); // Default to global assets. - break :blk main.files.cwd().openFile(path) catch |err2| { - std.log.err("File not found. Searched in \"{s}\" and also in the assetFolder \"{s}\"", .{ path, assetFolder }); - return err2; - }; - }; - file.close(main.io); // It was only openend to check if it exists. - // Otherwise read it into the list: - result = @intCast(textureIds.items.len); - - textureIds.append(main.worldArena, main.worldArena.dupe(u8, textureId)); - texturePaths.append(main.worldArena, main.worldArena.dupe(u8, path)); - return result; - } - - pub fn getTextureIndices(zon: ZonElement, assetFolder: []const u8, textureIndicesRef: *[16]u16) void { - const defaultIndex = findTexture(zon.get(?[]const u8, "texture", null), assetFolder) catch 0; - inline for (textureIndicesRef, 0..) |*ref, i| { - var textureId = zon.get(?[]const u8, std.fmt.comptimePrint("texture{}", .{i}), null); - if (i < sideNames.len) { - textureId = zon.get(?[]const u8, sideNames[i], textureId); - } - ref.* = findTexture(textureId, assetFolder) catch defaultIndex; - } - } - - pub fn register(assetFolder: []const u8, _: []const u8, zon: ZonElement) void { - _modelIndex[meshes.size] = _mode[meshes.size].createBlockModel(.{ .typ = @intCast(meshes.size), .data = 0 }, &_modeData[meshes.size], zon.getChild("model")); - - // The actual model is loaded later, in the rendering thread. - // But textures can be loaded here: - - getTextureIndices(zon, assetFolder, &textureIndices[meshes.size]); - - maxTextureCount[meshes.size] = @intCast(textureIds.items.len); - - meshes.size += 1; - } - - pub fn registerBlockBreakingAnimation(assetFolder: []const u8) void { - var i: usize = 0; - while (true) : (i += 1) { - const path1 = std.fmt.allocPrint(main.stackAllocator.allocator, "assets/cubyz/blocks/textures/breaking/{}.png", .{i}) catch unreachable; - defer main.stackAllocator.free(path1); - const path2 = std.fmt.allocPrint(main.stackAllocator.allocator, "{s}/cubyz/blocks/textures/breaking/{}.png", .{ assetFolder, i }) catch unreachable; - defer main.stackAllocator.free(path2); - if (!main.files.cwd().hasFile(path1) and !main.files.cwd().hasFile(path2)) break; - - const id = std.fmt.allocPrint(main.stackAllocator.allocator, "cubyz:breaking/{}", .{i}) catch unreachable; - defer main.stackAllocator.free(id); - blockBreakingTextures.append(main.worldArena, findTexture(id, assetFolder) catch break); - } - } - - pub fn preProcessAnimationData(time: u32) void { - animationComputePipeline.bind(); - c.glUniform1ui(animationUniforms.time, time); - c.glUniform1ui(animationUniforms.size, @intCast(animationData.len)); - c.glDispatchCompute(@intCast(@divFloor(animationData.len + 63, 64)), 1, 1); // TODO: Replace with @divCeil once available - c.glMemoryBarrier(c.GL_SHADER_STORAGE_BARRIER_BIT); - } - - fn finishTextureLoading() void { - animationData = main.worldArena.alloc(AnimationData, textureIds.items.len); - textureOcclusionData = main.worldArena.alloc(std.atomic.Value(bool), textureIds.items.len); - for (texturePaths.items, 0..) |path, i| { - readTextureData(i, path); - } - } - - pub fn reloadTextures(_: usize) void { - blockTextures.clearRetainingCapacity(); - emissionTextures.clearRetainingCapacity(); - reflectivityTextures.clearRetainingCapacity(); - absorptionTextures.clearRetainingCapacity(); - textureFogData.clearRetainingCapacity(); - for (texturePaths.items, 0..) |path, i| { - readTextureData(i, path); - } - generateTextureArray(); - } - - pub fn generateTextureArray() void { - blockTextureArray.generate(blockTextures.items, true, true); - c.glTexParameterf(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MAX_ANISOTROPY, @floatFromInt(main.settings.anisotropicFiltering)); - emissionTextureArray.generate(emissionTextures.items, true, false); - c.glTexParameterf(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MAX_ANISOTROPY, @floatFromInt(main.settings.anisotropicFiltering)); - const reflectivityAndAbsorptionTextures = main.stackAllocator.alloc(Image, reflectivityTextures.items.len); - defer main.stackAllocator.free(reflectivityAndAbsorptionTextures); - defer for (reflectivityAndAbsorptionTextures) |texture| { - texture.deinit(main.stackAllocator); - }; - for (reflectivityTextures.items, absorptionTextures.items, reflectivityAndAbsorptionTextures) |reflecitivityTexture, absorptionTexture, *resultTexture| { - const width = @max(reflecitivityTexture.width, absorptionTexture.width); - const height = @max(reflecitivityTexture.height, absorptionTexture.height); - resultTexture.* = Image.init(main.stackAllocator, width, height); - for (0..width) |x| { - for (0..height) |y| { - const reflectivity = reflecitivityTexture.getRGB(x * reflecitivityTexture.width / width, y * reflecitivityTexture.height / height); - const absorption = absorptionTexture.getRGB(x * absorptionTexture.width / width, y * absorptionTexture.height / height); - resultTexture.setRGB(x, y, .{ .r = absorption.r, .g = absorption.g, .b = absorption.b, .a = reflectivity.r }); - } - } - } - reflectivityAndAbsorptionTextureArray.generate(reflectivityAndAbsorptionTextures, true, false); - c.glTexParameterf(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MAX_ANISOTROPY, @floatFromInt(main.settings.anisotropicFiltering)); - - // Also generate additional buffers: - if (animationSSBO) |ssbo| { - ssbo.deinit(); - } - if (animatedTextureSSBO) |ssbo| { - ssbo.deinit(); - } - if (fogSSBO) |ssbo| { - ssbo.deinit(); - } - animationSSBO = SSBO.initStatic(AnimationData, animationData); - animationSSBO.?.bind(0); - - animatedTextureSSBO = SSBO.initStaticSize(u32, animationData.len); - animatedTextureSSBO.?.bind(1); - fogSSBO = SSBO.initStatic(FogData, textureFogData.items); - fogSSBO.?.bind(7); - } + const AnimationData = extern struct { + startFrame: u32, + frames: u32, + time: u32, + }; + + const FogData = extern struct { + fogDensity: f32, + fogColor: u32, + }; + var size: u32 = 0; + var _modelIndex: [maxBlockCount]ModelIndex = undefined; + var textureIndices: [maxBlockCount][16]u16 = undefined; + /// Stores the number of textures after each block was added. Used to clean additional textures when the world is switched. + var maxTextureCount: [maxBlockCount]u32 = undefined; + /// Number of loaded meshes. Used to determine if an update is needed. + var loadedMeshes: u32 = 0; + + var textureIds: main.ListUnmanaged([]const u8) = .{}; + var texturePaths: main.ListUnmanaged([]const u8) = .{}; + var animationData: []AnimationData = &.{}; + var blockTextures: main.ListUnmanaged(Image) = .{}; + var emissionTextures: main.ListUnmanaged(Image) = .{}; + var reflectivityTextures: main.ListUnmanaged(Image) = .{}; + var absorptionTextures: main.ListUnmanaged(Image) = .{}; + var textureFogData: main.ListUnmanaged(FogData) = .{}; + pub var textureOcclusionData: []std.atomic.Value(bool) = &.{}; + + pub var blockBreakingTextures: main.ListUnmanaged(u16) = .{}; + + const sideNames = blk: { + var names: [6][]const u8 = undefined; + names[Neighbor.dirDown.toInt()] = "texture_bottom"; + names[Neighbor.dirUp.toInt()] = "texture_top"; + names[Neighbor.dirPosX.toInt()] = "texture_right"; + names[Neighbor.dirNegX.toInt()] = "texture_left"; + names[Neighbor.dirPosY.toInt()] = "texture_front"; + names[Neighbor.dirNegY.toInt()] = "texture_back"; + break :blk names; + }; + + var animationSSBO: ?SSBO = null; + var animatedTextureSSBO: ?SSBO = null; + var fogSSBO: ?SSBO = null; + + var animationComputePipeline: graphics.ComputePipeline = undefined; + var animationUniforms: struct { + time: c_int, + size: c_int, + } = undefined; + + pub var blockTextureArray: TextureArray = undefined; + pub var emissionTextureArray: TextureArray = undefined; + pub var reflectivityAndAbsorptionTextureArray: TextureArray = undefined; + pub var ditherTexture: graphics.Texture = undefined; + + const black: Color = Color{.r = 0, .g = 0, .b = 0, .a = 255}; + const magenta: Color = Color{.r = 255, .g = 0, .b = 255, .a = 255}; + var undefinedTexture = [_]Color{magenta, black, black, magenta}; + const undefinedImage = Image{.width = 2, .height = 2, .imageData = undefinedTexture[0..]}; + var emptyTexture = [_]Color{black}; + const emptyImage = Image{.width = 1, .height = 1, .imageData = emptyTexture[0..]}; + + pub fn init() void { + animationComputePipeline = graphics.ComputePipeline.init("assets/cubyz/shaders/animation_pre_processing.comp", "", &animationUniforms); + blockTextureArray = .init(); + emissionTextureArray = .init(); + reflectivityAndAbsorptionTextureArray = .init(); + ditherTexture = .initFromMipmapFiles("assets/cubyz/blocks/textures/dither/", 64, 0.5); + } + + pub fn deinit() void { + if (animationSSBO) |ssbo| { + ssbo.deinit(); + } + if (animatedTextureSSBO) |ssbo| { + ssbo.deinit(); + } + if (fogSSBO) |ssbo| { + ssbo.deinit(); + } + animationComputePipeline.deinit(); + blockTextureArray.deinit(); + emissionTextureArray.deinit(); + reflectivityAndAbsorptionTextureArray.deinit(); + ditherTexture.deinit(); + } + + pub fn reset() void { + meshes.size = 0; + loadedMeshes = 0; + textureIds = .{}; + texturePaths = .{}; + animationData = &.{}; + blockTextures = .{}; + emissionTextures = .{}; + reflectivityTextures = .{}; + absorptionTextures = .{}; + textureFogData = .{}; + textureOcclusionData = &.{}; + blockBreakingTextures = .{}; + } + + pub inline fn model(block: Block) ModelIndex { + return block.mode().model(block); + } + + pub inline fn modelIndexStart(block: Block) ModelIndex { + return _modelIndex[block.typ]; + } + + pub inline fn fogDensity(block: Block) f32 { + return textureFogData.items[animationData[textureIndices[block.typ][0]].startFrame].fogDensity; + } + + pub inline fn fogColor(block: Block) u32 { + return textureFogData.items[animationData[textureIndices[block.typ][0]].startFrame].fogColor; + } + + pub inline fn hasFog(block: Block) bool { + return fogDensity(block) != 0.0; + } + + pub inline fn textureIndex(block: Block, orientation: usize) u16 { + if (orientation < 16) { + return textureIndices[block.typ][orientation]; + } else { + return textureIndices[block.data][orientation - 16]; + } + } + + fn extendedPath(_allocator: main.heap.NeverFailingAllocator, path: []const u8, ending: []const u8) []const u8 { + return std.fmt.allocPrint(_allocator.allocator, "{s}{s}", .{path, ending}) catch unreachable; + } + + fn readTextureFile(_path: []const u8, ending: []const u8, default: Image) Image { + const path = extendedPath(main.stackAllocator, _path, ending); + defer main.stackAllocator.free(path); + return Image.readFromFile(main.worldArena, path) catch default; + } + + fn extractAnimationSlice(image: Image, frame: usize, frames: usize) Image { + if (image.height < frames) return image; + var startHeight = image.height/frames*frame; + if (image.height%frames > frame) startHeight += frame else startHeight += image.height%frames; + var endHeight = image.height/frames*(frame + 1); + if (image.height%frames > frame + 1) endHeight += frame + 1 else endHeight += image.height%frames; + var result = image; + result.height = @intCast(endHeight - startHeight); + result.imageData = result.imageData[startHeight*image.width .. endHeight*image.width]; + return result; + } + + fn readTextureData(index: usize, _path: []const u8) void { + const path = _path[0 .. _path.len - ".png".len]; + const textureInfoPath = extendedPath(main.stackAllocator, path, ".zig.zon"); + defer main.stackAllocator.free(textureInfoPath); + const textureInfoZon = main.files.cwd().readToZon(main.stackAllocator, textureInfoPath) catch .null; + defer textureInfoZon.deinit(main.stackAllocator); + const animationFrames = textureInfoZon.get(u32, "frames", 1); + const animationTime = textureInfoZon.get(u32, "time", 1); + animationData[index] = .{.startFrame = @intCast(blockTextures.items.len), .frames = animationFrames, .time = animationTime}; + const base = readTextureFile(path, ".png", Image.defaultImage); + const emission = readTextureFile(path, "_emission.png", Image.emptyImage); + const reflectivity = readTextureFile(path, "_reflectivity.png", Image.emptyImage); + const absorption = readTextureFile(path, "_absorption.png", Image.whiteEmptyImage); + for (0..animationFrames) |i| { + blockTextures.append(main.worldArena, extractAnimationSlice(base, i, animationFrames)); + emissionTextures.append(main.worldArena, extractAnimationSlice(emission, i, animationFrames)); + reflectivityTextures.append(main.worldArena, extractAnimationSlice(reflectivity, i, animationFrames)); + absorptionTextures.append(main.worldArena, extractAnimationSlice(absorption, i, animationFrames)); + textureFogData.append(main.worldArena, .{ + .fogDensity = textureInfoZon.get(f32, "fogDensity", 0.0), + .fogColor = textureInfoZon.get(u32, "fogColor", 0xffffff), + }); + } + textureOcclusionData[index].store(textureInfoZon.get(bool, "hasOcclusion", true), .monotonic); + } + + pub fn findTexture(_textureId: ?[]const u8, assetFolder: []const u8) !u16 { + const textureId = _textureId orelse return error.NotFound; + var result: u16 = undefined; + var splitter = std.mem.splitScalar(u8, textureId, ':'); + const mod = splitter.first(); + const id = splitter.rest(); + var path = try std.fmt.allocPrint(main.stackAllocator.allocator, "{s}/{s}/blocks/textures/{s}.png", .{assetFolder, mod, id}); + defer main.stackAllocator.free(path); + // Test if it's already in the list: + for (textureIds.items, 0..) |other, j| { + if (std.mem.eql(u8, other, textureId)) { + result = @intCast(j); + return result; + } + } + const file = main.files.cwd().openFile(path) catch |err| blk: { + if (err != error.FileNotFound) { + std.log.err("Could not open file {s}: {s}", .{path, @errorName(err)}); + } + main.stackAllocator.free(path); + path = try std.fmt.allocPrint(main.stackAllocator.allocator, "assets/{s}/blocks/textures/{s}.png", .{mod, id}); // Default to global assets. + break :blk main.files.cwd().openFile(path) catch |err2| { + std.log.err("File not found. Searched in \"{s}\" and also in the assetFolder \"{s}\"", .{path, assetFolder}); + return err2; + }; + }; + file.close(main.io); // It was only openend to check if it exists. + // Otherwise read it into the list: + result = @intCast(textureIds.items.len); + + textureIds.append(main.worldArena, main.worldArena.dupe(u8, textureId)); + texturePaths.append(main.worldArena, main.worldArena.dupe(u8, path)); + return result; + } + + pub fn getTextureIndices(zon: ZonElement, assetFolder: []const u8, textureIndicesRef: *[16]u16) void { + const defaultIndex = findTexture(zon.get(?[]const u8, "texture", null), assetFolder) catch 0; + inline for (textureIndicesRef, 0..) |*ref, i| { + var textureId = zon.get(?[]const u8, std.fmt.comptimePrint("texture{}", .{i}), null); + if (i < sideNames.len) { + textureId = zon.get(?[]const u8, sideNames[i], textureId); + } + ref.* = findTexture(textureId, assetFolder) catch defaultIndex; + } + } + + pub fn register(assetFolder: []const u8, _: []const u8, zon: ZonElement) void { + _modelIndex[meshes.size] = _mode[meshes.size].createBlockModel(.{.typ = @intCast(meshes.size), .data = 0}, &_modeData[meshes.size], zon.getChild("model")); + + // The actual model is loaded later, in the rendering thread. + // But textures can be loaded here: + + getTextureIndices(zon, assetFolder, &textureIndices[meshes.size]); + + maxTextureCount[meshes.size] = @intCast(textureIds.items.len); + + meshes.size += 1; + } + + pub fn registerBlockBreakingAnimation(assetFolder: []const u8) void { + var i: usize = 0; + while (true) : (i += 1) { + const path1 = std.fmt.allocPrint(main.stackAllocator.allocator, "assets/cubyz/blocks/textures/breaking/{}.png", .{i}) catch unreachable; + defer main.stackAllocator.free(path1); + const path2 = std.fmt.allocPrint(main.stackAllocator.allocator, "{s}/cubyz/blocks/textures/breaking/{}.png", .{assetFolder, i}) catch unreachable; + defer main.stackAllocator.free(path2); + if (!main.files.cwd().hasFile(path1) and !main.files.cwd().hasFile(path2)) break; + + const id = std.fmt.allocPrint(main.stackAllocator.allocator, "cubyz:breaking/{}", .{i}) catch unreachable; + defer main.stackAllocator.free(id); + blockBreakingTextures.append(main.worldArena, findTexture(id, assetFolder) catch break); + } + } + + pub fn preProcessAnimationData(time: u32) void { + animationComputePipeline.bind(); + c.glUniform1ui(animationUniforms.time, time); + c.glUniform1ui(animationUniforms.size, @intCast(animationData.len)); + c.glDispatchCompute(@intCast(@divFloor(animationData.len + 63, 64)), 1, 1); // TODO: Replace with @divCeil once available + c.glMemoryBarrier(c.GL_SHADER_STORAGE_BARRIER_BIT); + } + + fn finishTextureLoading() void { + animationData = main.worldArena.alloc(AnimationData, textureIds.items.len); + textureOcclusionData = main.worldArena.alloc(std.atomic.Value(bool), textureIds.items.len); + for (texturePaths.items, 0..) |path, i| { + readTextureData(i, path); + } + } + + pub fn reloadTextures(_: usize) void { + blockTextures.clearRetainingCapacity(); + emissionTextures.clearRetainingCapacity(); + reflectivityTextures.clearRetainingCapacity(); + absorptionTextures.clearRetainingCapacity(); + textureFogData.clearRetainingCapacity(); + for (texturePaths.items, 0..) |path, i| { + readTextureData(i, path); + } + generateTextureArray(); + } + + pub fn generateTextureArray() void { + blockTextureArray.generate(blockTextures.items, true, true); + c.glTexParameterf(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MAX_ANISOTROPY, @floatFromInt(main.settings.anisotropicFiltering)); + emissionTextureArray.generate(emissionTextures.items, true, false); + c.glTexParameterf(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MAX_ANISOTROPY, @floatFromInt(main.settings.anisotropicFiltering)); + const reflectivityAndAbsorptionTextures = main.stackAllocator.alloc(Image, reflectivityTextures.items.len); + defer main.stackAllocator.free(reflectivityAndAbsorptionTextures); + defer for (reflectivityAndAbsorptionTextures) |texture| { + texture.deinit(main.stackAllocator); + }; + for (reflectivityTextures.items, absorptionTextures.items, reflectivityAndAbsorptionTextures) |reflecitivityTexture, absorptionTexture, *resultTexture| { + const width = @max(reflecitivityTexture.width, absorptionTexture.width); + const height = @max(reflecitivityTexture.height, absorptionTexture.height); + resultTexture.* = Image.init(main.stackAllocator, width, height); + for (0..width) |x| { + for (0..height) |y| { + const reflectivity = reflecitivityTexture.getRGB(x*reflecitivityTexture.width/width, y*reflecitivityTexture.height/height); + const absorption = absorptionTexture.getRGB(x*absorptionTexture.width/width, y*absorptionTexture.height/height); + resultTexture.setRGB(x, y, .{.r = absorption.r, .g = absorption.g, .b = absorption.b, .a = reflectivity.r}); + } + } + } + reflectivityAndAbsorptionTextureArray.generate(reflectivityAndAbsorptionTextures, true, false); + c.glTexParameterf(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MAX_ANISOTROPY, @floatFromInt(main.settings.anisotropicFiltering)); + + // Also generate additional buffers: + if (animationSSBO) |ssbo| { + ssbo.deinit(); + } + if (animatedTextureSSBO) |ssbo| { + ssbo.deinit(); + } + if (fogSSBO) |ssbo| { + ssbo.deinit(); + } + animationSSBO = SSBO.initStatic(AnimationData, animationData); + animationSSBO.?.bind(0); + + animatedTextureSSBO = SSBO.initStaticSize(u32, animationData.len); + animatedTextureSSBO.?.bind(1); + fogSSBO = SSBO.initStatic(FogData, textureFogData.items); + fogSSBO.?.bind(7); + } }; diff --git a/src/graphics.zig b/src/graphics.zig index 56a83c6efd..a9999d7cca 100644 --- a/src/graphics.zig +++ b/src/graphics.zig @@ -24,2269 +24,2269 @@ pub const Pipeline = pipelines.Pipeline; pub const vulkan = @import("graphics/vulkan.zig"); pub const draw = struct { // MARK: draw - var color: u32 = 0; - var clip: ?Vec4i = null; - var translation: Vec2f = Vec2f{ 0, 0 }; - var scale: f32 = 1; - - pub fn setColor(newColor: u32) void { - color = newColor; - } - - pub fn setColorSameAlpha(newColor: u24) void { - color = (color & 0xff000000) | newColor; - } - - /// Returns the previous translation. - pub fn setTranslation(newTranslation: Vec2f) Vec2f { - const oldTranslation = translation; - translation += newTranslation * @as(Vec2f, @splat(scale)); - return oldTranslation; - } - - pub fn restoreTranslation(previousTranslation: Vec2f) void { - translation = previousTranslation; - } - - /// Returns the previous scale. - pub fn setScale(newScale: f32) f32 { - std.debug.assert(newScale >= 0); - const oldScale = scale; - scale *= newScale; - return oldScale; - } - - pub fn restoreScale(previousScale: f32) void { - scale = previousScale; - } - - /// Returns the previous clip. - pub fn setClip(clipRect: Vec2f) ?Vec4i { - std.debug.assert(@reduce(.And, clipRect >= Vec2f{ 0, 0 })); - var viewport: [4]c_int = undefined; - c.glGetIntegerv(c.GL_VIEWPORT, &viewport); - var newClip = Vec4i{ - std.math.lossyCast(i32, translation[0]), - viewport[3] - std.math.lossyCast(i32, translation[1] + clipRect[1] * scale), - std.math.lossyCast(i32, clipRect[0] * scale), - std.math.lossyCast(i32, clipRect[1] * scale), - }; - if (clip) |oldClip| { - if (newClip[0] < oldClip[0]) { - newClip[2] -= oldClip[0] - newClip[0]; - newClip[0] += oldClip[0] - newClip[0]; - } - if (newClip[1] < oldClip[1]) { - newClip[3] -= oldClip[1] - newClip[1]; - newClip[1] += oldClip[1] - newClip[1]; - } - if (newClip[0] + newClip[2] > oldClip[0] + oldClip[2]) { - newClip[2] -= (newClip[0] + newClip[2]) - (oldClip[0] + oldClip[2]); - } - if (newClip[1] + newClip[3] > oldClip[1] + oldClip[3]) { - newClip[3] -= (newClip[1] + newClip[3]) - (oldClip[1] + oldClip[3]); - } - newClip[2] = @max(newClip[2], 0); - newClip[3] = @max(newClip[3], 0); - } - const oldClip = clip; - clip = newClip; - return oldClip; - } - - pub fn getScissor() ?c.VkRect2D { - const clipRect = clip orelse return null; - return .{ - .offset = .{ - .x = clipRect[0], - .y = clipRect[1], - }, - .extent = .{ - .width = @intCast(clipRect[2]), - .height = @intCast(clipRect[3]), - }, - }; - } - - /// Should be used to restore the old clip when leaving the render function. - pub fn restoreClip(previousClip: ?Vec4i) void { - clip = previousClip; - } - - pub const SimpleVertex2D = struct { - pos: [2]f32, - - pub const attributeDescriptions: []const c.VkVertexInputAttributeDescription = &.{ - .{ - .location = 0, - .format = c.VK_FORMAT_R32G32_SFLOAT, - .offset = @offsetOf(@This(), "pos"), - }, - }; - }; - - // ---------------------------------------------------------------------------- - // MARK: fillRect() - var rectUniforms: struct { - screen: c_int, - start: c_int, - size: c_int, - rectColor: c_int, - } = undefined; - var rectPipeline: Pipeline = undefined; - pub var rectVao: VertexArray = undefined; - - fn initRect() void { - rectPipeline = Pipeline.init( - "assets/cubyz/shaders/graphics/Rect.vert", - "assets/cubyz/shaders/graphics/Rect.frag", - "", - &rectUniforms, - SimpleVertex2D, - &.{}, - .{ .cullMode = .none }, - .{ .depthTest = false, .depthWrite = false }, - .{ .attachments = &.{.alphaBlending} }, - ); - const rawData = [_]SimpleVertex2D{ - .{ .pos = .{ 0, 0 } }, - .{ .pos = .{ 0, 1 } }, - .{ .pos = .{ 1, 0 } }, - .{ .pos = .{ 1, 1 } }, - }; - - rectVao = .init(SimpleVertex2D, &rawData, null); - } - - fn deinitRect() void { - rectPipeline.deinit(); - rectVao.deinit(); - } - - pub fn rect(_pos: Vec2f, _dim: Vec2f) void { - var pos = _pos; - var dim = _dim; - pos *= @splat(scale); - pos += translation; - dim *= @splat(scale); - - rectPipeline.bind(getScissor()); - - var viewport: [4]c_int = undefined; - c.glGetIntegerv(c.GL_VIEWPORT, &viewport); - c.glUniform2f(rectUniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); - c.glUniform2f(rectUniforms.start, pos[0], pos[1]); - c.glUniform2f(rectUniforms.size, dim[0], dim[1]); - c.glUniform1i(rectUniforms.rectColor, @bitCast(color)); - - rectVao.bind(); - c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); - } - - // ---------------------------------------------------------------------------- - // MARK: fillRectBorder() - var rectBorderUniforms: struct { - screen: c_int, - start: c_int, - size: c_int, - rectColor: c_int, - lineWidth: c_int, - } = undefined; - var rectBorderPipeline: Pipeline = undefined; - var rectBorderVao: VertexArray = undefined; - - fn initRectBorder() void { - const RectBorderVertex = struct { - pos: [4]f32, - - pub const attributeDescriptions: []const c.VkVertexInputAttributeDescription = &.{ - .{ - .location = 0, - .format = c.VK_FORMAT_R32G32B32A32_SFLOAT, - .offset = @offsetOf(@This(), "pos"), - }, - }; - }; - rectBorderPipeline = Pipeline.init( - "assets/cubyz/shaders/graphics/RectBorder.vert", - "assets/cubyz/shaders/graphics/RectBorder.frag", - "", - &rectBorderUniforms, - RectBorderVertex, - &.{}, - .{ .cullMode = .none }, - .{ .depthTest = false, .depthWrite = false }, - .{ .attachments = &.{.alphaBlending} }, - ); - const rawData = [_]RectBorderVertex{ - .{ .pos = .{ 0, 0, 0, 0 } }, - .{ .pos = .{ 0, 0, 1, 1 } }, - .{ .pos = .{ 0, 1, 0, 0 } }, - .{ .pos = .{ 0, 1, 1, -1 } }, - .{ .pos = .{ 1, 1, 0, 0 } }, - .{ .pos = .{ 1, 1, -1, -1 } }, - .{ .pos = .{ 1, 0, 0, 0 } }, - .{ .pos = .{ 1, 0, -1, 1 } }, - .{ .pos = .{ 0, 0, 0, 0 } }, - .{ .pos = .{ 0, 0, 1, 1 } }, - }; - - rectBorderVao = .init(RectBorderVertex, &rawData, null); - } - - fn deinitRectBorder() void { - rectBorderPipeline.deinit(); - rectBorderVao.deinit(); - } - - pub fn rectBorder(_pos: Vec2f, _dim: Vec2f, _width: f32) void { - var pos = _pos; - var dim = _dim; - var width = _width; - pos *= @splat(scale); - pos += translation; - dim *= @splat(scale); - width *= scale; - - rectBorderPipeline.bind(getScissor()); - - var viewport: [4]c_int = undefined; - c.glGetIntegerv(c.GL_VIEWPORT, &viewport); - c.glUniform2f(rectBorderUniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); - c.glUniform2f(rectBorderUniforms.start, pos[0], pos[1]); - c.glUniform2f(rectBorderUniforms.size, dim[0], dim[1]); - c.glUniform1i(rectBorderUniforms.rectColor, @bitCast(color)); - c.glUniform1f(rectBorderUniforms.lineWidth, width); - - rectBorderVao.bind(); - c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 10); - } - - // ---------------------------------------------------------------------------- - // MARK: drawLine() - var lineUniforms: struct { - screen: c_int, - start: c_int, - direction: c_int, - lineColor: c_int, - } = undefined; - var linePipeline: Pipeline = undefined; - var lineVao: VertexArray = undefined; - - fn initLine() void { - linePipeline = Pipeline.init( - "assets/cubyz/shaders/graphics/Line.vert", - "assets/cubyz/shaders/graphics/Line.frag", - "", - &lineUniforms, - SimpleVertex2D, - &.{}, - .{ .cullMode = .none }, - .{ .depthTest = false, .depthWrite = false }, - .{ .attachments = &.{.alphaBlending} }, - ); - const rawData = [_]SimpleVertex2D{ - .{ .pos = .{ 0, 0 } }, - .{ .pos = .{ 1, 1 } }, - }; - - lineVao = .init(SimpleVertex2D, &rawData, null); - } - - fn deinitLine() void { - linePipeline.deinit(); - lineVao.deinit(); - } - - pub fn line(_pos1: Vec2f, _pos2: Vec2f) void { - var pos1 = _pos1; - var pos2 = _pos2; - pos1 *= @splat(scale); - pos1 += translation; - pos2 *= @splat(scale); - pos2 += translation; - - linePipeline.bind(getScissor()); - - var viewport: [4]c_int = undefined; - c.glGetIntegerv(c.GL_VIEWPORT, &viewport); - c.glUniform2f(lineUniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); - c.glUniform2f(lineUniforms.start, pos1[0], pos1[1]); - c.glUniform2f(lineUniforms.direction, pos2[0] - pos1[0], pos2[1] - pos1[1]); - c.glUniform1i(lineUniforms.lineColor, @bitCast(color)); - - lineVao.bind(); - c.glDrawArrays(c.GL_LINE_STRIP, 0, 2); - } - - pub fn rectOutline(_pos: Vec2f, _dim: Vec2f) void { - var pos = _pos; - var dim = _dim; - pos *= @splat(scale); - pos += translation; - dim *= @splat(scale); - - linePipeline.bind(getScissor()); - - var viewport: [4]c_int = undefined; - c.glGetIntegerv(c.GL_VIEWPORT, &viewport); - c.glUniform2f(lineUniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); - c.glUniform2f(lineUniforms.start, pos[0], pos[1]); // Move the coordinates, so they are in the center of a pixel. - c.glUniform2f(lineUniforms.direction, dim[0] - 1, dim[1] - 1); // The height is a lot smaller because the inner edge of the rect is drawn. - c.glUniform1i(lineUniforms.lineColor, @bitCast(color)); - - lineVao.bind(); - c.glDrawArrays(c.GL_LINE_LOOP, 0, 5); - } - - // ---------------------------------------------------------------------------- - // MARK: fillCircle() - var circleUniforms: struct { - screen: c_int, - center: c_int, - radius: c_int, - circleColor: c_int, - } = undefined; - var circlePipeline: Pipeline = undefined; - var circleVao: VertexArray = undefined; - - fn initCircle() void { - circlePipeline = Pipeline.init( - "assets/cubyz/shaders/graphics/Circle.vert", - "assets/cubyz/shaders/graphics/Circle.frag", - "", - &circleUniforms, - SimpleVertex2D, - &.{}, - .{ .cullMode = .none }, - .{ .depthTest = false, .depthWrite = false }, - .{ .attachments = &.{.alphaBlending} }, - ); - const rawData = [_]SimpleVertex2D{ - .{ .pos = .{ -1, -1 } }, - .{ .pos = .{ -1, 1 } }, - .{ .pos = .{ 1, -1 } }, - .{ .pos = .{ 1, 1 } }, - }; - - circleVao = .init(SimpleVertex2D, &rawData, null); - } - - fn deinitCircle() void { - circlePipeline.deinit(); - circleVao.deinit(); - } - - pub fn circle(_center: Vec2f, _radius: f32) void { - var center = _center; - var radius = _radius; - center *= @splat(scale); - center += translation; - radius *= scale; - circlePipeline.bind(getScissor()); - - var viewport: [4]c_int = undefined; - c.glGetIntegerv(c.GL_VIEWPORT, &viewport); - c.glUniform2f(circleUniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); - c.glUniform2f(circleUniforms.center, center[0], center[1]); // Move the coordinates, so they are in the center of a pixel. - c.glUniform1f(circleUniforms.radius, radius); // The height is a lot smaller because the inner edge of the rect is drawn. - c.glUniform1i(circleUniforms.circleColor, @bitCast(color)); - - circleVao.bind(); - c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); - } - - // ---------------------------------------------------------------------------- - // MARK: drawImage() - // Luckily the vao of the regular rect can used. - var imageUniforms: struct { - screen: c_int, - start: c_int, - size: c_int, - color: c_int, - uvOffset: c_int, - uvDim: c_int, - } = undefined; - var imagePipeline: Pipeline = undefined; - - fn initImage() void { - imagePipeline = Pipeline.init( - "assets/cubyz/shaders/graphics/Image.vert", - "assets/cubyz/shaders/graphics/Image.frag", - "", - &imageUniforms, - SimpleVertex2D, - &.{}, - .{ .cullMode = .none }, - .{ .depthTest = false, .depthWrite = false }, - .{ .attachments = &.{.alphaBlending} }, - ); - } - - fn deinitImage() void { - imagePipeline.deinit(); - } - - pub fn boundImage(_pos: Vec2f, _dim: Vec2f) void { - imagePipeline.bind(getScissor()); - - customShadedImage(&imageUniforms, _pos, _dim); - } - - pub fn boundSubImage(_pos: Vec2f, _dim: Vec2f, uvOffset: Vec2f, uvDim: Vec2f) void { - var pos = _pos; - var dim = _dim; - pos *= @splat(scale); - pos += translation; - dim *= @splat(scale); - pos = @floor(pos); - dim = @ceil(dim); - - imagePipeline.bind(getScissor()); - - var viewport: [4]c_int = undefined; - c.glGetIntegerv(c.GL_VIEWPORT, &viewport); - c.glUniform2f(imageUniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); - c.glUniform2f(imageUniforms.start, pos[0], pos[1]); - c.glUniform2f(imageUniforms.size, dim[0], dim[1]); - c.glUniform1i(imageUniforms.color, @bitCast(color)); - c.glUniform2f(imageUniforms.uvOffset, uvOffset[0], 1 - uvOffset[1] - uvDim[1]); - c.glUniform2f(imageUniforms.uvDim, uvDim[0], uvDim[1]); - - rectVao.bind(); - c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); - } - - pub fn customShadedImage(uniforms: anytype, _pos: Vec2f, _dim: Vec2f) void { - var pos = _pos; - var dim = _dim; - pos *= @splat(scale); - pos += translation; - dim *= @splat(scale); - pos = @floor(pos); - dim = @ceil(dim); - - var viewport: [4]c_int = undefined; - c.glGetIntegerv(c.GL_VIEWPORT, &viewport); - c.glUniform2f(uniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); - c.glUniform2f(uniforms.start, pos[0], pos[1]); - c.glUniform2f(uniforms.size, dim[0], dim[1]); - c.glUniform1i(uniforms.color, @bitCast(color)); - c.glUniform2f(uniforms.uvOffset, 0, 0); - c.glUniform2f(uniforms.uvDim, 1, 1); - - rectVao.bind(); - c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); - } - - // ---------------------------------------------------------------------------- - // MARK: customShadedRect() - - pub fn customShadedRect(uniforms: anytype, _pos: Vec2f, _dim: Vec2f) void { - var pos = _pos; - var dim = _dim; - pos *= @splat(scale); - pos += translation; - dim *= @splat(scale); - pos = @floor(pos); - dim = @ceil(dim); - - var viewport: [4]c_int = undefined; - c.glGetIntegerv(c.GL_VIEWPORT, &viewport); - c.glUniform2f(uniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); - c.glUniform2f(uniforms.start, pos[0], pos[1]); - c.glUniform2f(uniforms.size, dim[0], dim[1]); - c.glUniform1i(uniforms.color, @bitCast(color)); - c.glUniform1f(uniforms.scale, scale); - - rectVao.bind(); - c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); - } - - // ---------------------------------------------------------------------------- - // MARK: text() - - pub fn text(_text: []const u8, x: f32, y: f32, fontSize: f32, alignment: TextBuffer.Alignment) void { - TextRendering.renderText(_text, x, y, fontSize, .{ .color = @truncate(@as(u32, @bitCast(color))) }, alignment); - } - - pub inline fn print(comptime format: []const u8, args: anytype, x: f32, y: f32, fontSize: f32, alignment: TextBuffer.Alignment) void { - const string = std.fmt.allocPrint(main.stackAllocator.allocator, format, args) catch unreachable; - defer main.stackAllocator.free(string); - text(string, x, y, fontSize, alignment); - } + var color: u32 = 0; + var clip: ?Vec4i = null; + var translation: Vec2f = Vec2f{0, 0}; + var scale: f32 = 1; + + pub fn setColor(newColor: u32) void { + color = newColor; + } + + pub fn setColorSameAlpha(newColor: u24) void { + color = (color & 0xff000000) | newColor; + } + + /// Returns the previous translation. + pub fn setTranslation(newTranslation: Vec2f) Vec2f { + const oldTranslation = translation; + translation += newTranslation*@as(Vec2f, @splat(scale)); + return oldTranslation; + } + + pub fn restoreTranslation(previousTranslation: Vec2f) void { + translation = previousTranslation; + } + + /// Returns the previous scale. + pub fn setScale(newScale: f32) f32 { + std.debug.assert(newScale >= 0); + const oldScale = scale; + scale *= newScale; + return oldScale; + } + + pub fn restoreScale(previousScale: f32) void { + scale = previousScale; + } + + /// Returns the previous clip. + pub fn setClip(clipRect: Vec2f) ?Vec4i { + std.debug.assert(@reduce(.And, clipRect >= Vec2f{0, 0})); + var viewport: [4]c_int = undefined; + c.glGetIntegerv(c.GL_VIEWPORT, &viewport); + var newClip = Vec4i{ + std.math.lossyCast(i32, translation[0]), + viewport[3] - std.math.lossyCast(i32, translation[1] + clipRect[1]*scale), + std.math.lossyCast(i32, clipRect[0]*scale), + std.math.lossyCast(i32, clipRect[1]*scale), + }; + if (clip) |oldClip| { + if (newClip[0] < oldClip[0]) { + newClip[2] -= oldClip[0] - newClip[0]; + newClip[0] += oldClip[0] - newClip[0]; + } + if (newClip[1] < oldClip[1]) { + newClip[3] -= oldClip[1] - newClip[1]; + newClip[1] += oldClip[1] - newClip[1]; + } + if (newClip[0] + newClip[2] > oldClip[0] + oldClip[2]) { + newClip[2] -= (newClip[0] + newClip[2]) - (oldClip[0] + oldClip[2]); + } + if (newClip[1] + newClip[3] > oldClip[1] + oldClip[3]) { + newClip[3] -= (newClip[1] + newClip[3]) - (oldClip[1] + oldClip[3]); + } + newClip[2] = @max(newClip[2], 0); + newClip[3] = @max(newClip[3], 0); + } + const oldClip = clip; + clip = newClip; + return oldClip; + } + + pub fn getScissor() ?c.VkRect2D { + const clipRect = clip orelse return null; + return .{ + .offset = .{ + .x = clipRect[0], + .y = clipRect[1], + }, + .extent = .{ + .width = @intCast(clipRect[2]), + .height = @intCast(clipRect[3]), + }, + }; + } + + /// Should be used to restore the old clip when leaving the render function. + pub fn restoreClip(previousClip: ?Vec4i) void { + clip = previousClip; + } + + pub const SimpleVertex2D = struct { + pos: [2]f32, + + pub const attributeDescriptions: []const c.VkVertexInputAttributeDescription = &.{ + .{ + .location = 0, + .format = c.VK_FORMAT_R32G32_SFLOAT, + .offset = @offsetOf(@This(), "pos"), + }, + }; + }; + + // ---------------------------------------------------------------------------- + // MARK: fillRect() + var rectUniforms: struct { + screen: c_int, + start: c_int, + size: c_int, + rectColor: c_int, + } = undefined; + var rectPipeline: Pipeline = undefined; + pub var rectVao: VertexArray = undefined; + + fn initRect() void { + rectPipeline = Pipeline.init( + "assets/cubyz/shaders/graphics/Rect.vert", + "assets/cubyz/shaders/graphics/Rect.frag", + "", + &rectUniforms, + SimpleVertex2D, + &.{}, + .{.cullMode = .none}, + .{.depthTest = false, .depthWrite = false}, + .{.attachments = &.{.alphaBlending}}, + ); + const rawData = [_]SimpleVertex2D{ + .{.pos = .{0, 0}}, + .{.pos = .{0, 1}}, + .{.pos = .{1, 0}}, + .{.pos = .{1, 1}}, + }; + + rectVao = .init(SimpleVertex2D, &rawData, null); + } + + fn deinitRect() void { + rectPipeline.deinit(); + rectVao.deinit(); + } + + pub fn rect(_pos: Vec2f, _dim: Vec2f) void { + var pos = _pos; + var dim = _dim; + pos *= @splat(scale); + pos += translation; + dim *= @splat(scale); + + rectPipeline.bind(getScissor()); + + var viewport: [4]c_int = undefined; + c.glGetIntegerv(c.GL_VIEWPORT, &viewport); + c.glUniform2f(rectUniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); + c.glUniform2f(rectUniforms.start, pos[0], pos[1]); + c.glUniform2f(rectUniforms.size, dim[0], dim[1]); + c.glUniform1i(rectUniforms.rectColor, @bitCast(color)); + + rectVao.bind(); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + } + + // ---------------------------------------------------------------------------- + // MARK: fillRectBorder() + var rectBorderUniforms: struct { + screen: c_int, + start: c_int, + size: c_int, + rectColor: c_int, + lineWidth: c_int, + } = undefined; + var rectBorderPipeline: Pipeline = undefined; + var rectBorderVao: VertexArray = undefined; + + fn initRectBorder() void { + const RectBorderVertex = struct { + pos: [4]f32, + + pub const attributeDescriptions: []const c.VkVertexInputAttributeDescription = &.{ + .{ + .location = 0, + .format = c.VK_FORMAT_R32G32B32A32_SFLOAT, + .offset = @offsetOf(@This(), "pos"), + }, + }; + }; + rectBorderPipeline = Pipeline.init( + "assets/cubyz/shaders/graphics/RectBorder.vert", + "assets/cubyz/shaders/graphics/RectBorder.frag", + "", + &rectBorderUniforms, + RectBorderVertex, + &.{}, + .{.cullMode = .none}, + .{.depthTest = false, .depthWrite = false}, + .{.attachments = &.{.alphaBlending}}, + ); + const rawData = [_]RectBorderVertex{ + .{.pos = .{0, 0, 0, 0}}, + .{.pos = .{0, 0, 1, 1}}, + .{.pos = .{0, 1, 0, 0}}, + .{.pos = .{0, 1, 1, -1}}, + .{.pos = .{1, 1, 0, 0}}, + .{.pos = .{1, 1, -1, -1}}, + .{.pos = .{1, 0, 0, 0}}, + .{.pos = .{1, 0, -1, 1}}, + .{.pos = .{0, 0, 0, 0}}, + .{.pos = .{0, 0, 1, 1}}, + }; + + rectBorderVao = .init(RectBorderVertex, &rawData, null); + } + + fn deinitRectBorder() void { + rectBorderPipeline.deinit(); + rectBorderVao.deinit(); + } + + pub fn rectBorder(_pos: Vec2f, _dim: Vec2f, _width: f32) void { + var pos = _pos; + var dim = _dim; + var width = _width; + pos *= @splat(scale); + pos += translation; + dim *= @splat(scale); + width *= scale; + + rectBorderPipeline.bind(getScissor()); + + var viewport: [4]c_int = undefined; + c.glGetIntegerv(c.GL_VIEWPORT, &viewport); + c.glUniform2f(rectBorderUniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); + c.glUniform2f(rectBorderUniforms.start, pos[0], pos[1]); + c.glUniform2f(rectBorderUniforms.size, dim[0], dim[1]); + c.glUniform1i(rectBorderUniforms.rectColor, @bitCast(color)); + c.glUniform1f(rectBorderUniforms.lineWidth, width); + + rectBorderVao.bind(); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 10); + } + + // ---------------------------------------------------------------------------- + // MARK: drawLine() + var lineUniforms: struct { + screen: c_int, + start: c_int, + direction: c_int, + lineColor: c_int, + } = undefined; + var linePipeline: Pipeline = undefined; + var lineVao: VertexArray = undefined; + + fn initLine() void { + linePipeline = Pipeline.init( + "assets/cubyz/shaders/graphics/Line.vert", + "assets/cubyz/shaders/graphics/Line.frag", + "", + &lineUniforms, + SimpleVertex2D, + &.{}, + .{.cullMode = .none}, + .{.depthTest = false, .depthWrite = false}, + .{.attachments = &.{.alphaBlending}}, + ); + const rawData = [_]SimpleVertex2D{ + .{.pos = .{0, 0}}, + .{.pos = .{1, 1}}, + }; + + lineVao = .init(SimpleVertex2D, &rawData, null); + } + + fn deinitLine() void { + linePipeline.deinit(); + lineVao.deinit(); + } + + pub fn line(_pos1: Vec2f, _pos2: Vec2f) void { + var pos1 = _pos1; + var pos2 = _pos2; + pos1 *= @splat(scale); + pos1 += translation; + pos2 *= @splat(scale); + pos2 += translation; + + linePipeline.bind(getScissor()); + + var viewport: [4]c_int = undefined; + c.glGetIntegerv(c.GL_VIEWPORT, &viewport); + c.glUniform2f(lineUniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); + c.glUniform2f(lineUniforms.start, pos1[0], pos1[1]); + c.glUniform2f(lineUniforms.direction, pos2[0] - pos1[0], pos2[1] - pos1[1]); + c.glUniform1i(lineUniforms.lineColor, @bitCast(color)); + + lineVao.bind(); + c.glDrawArrays(c.GL_LINE_STRIP, 0, 2); + } + + pub fn rectOutline(_pos: Vec2f, _dim: Vec2f) void { + var pos = _pos; + var dim = _dim; + pos *= @splat(scale); + pos += translation; + dim *= @splat(scale); + + linePipeline.bind(getScissor()); + + var viewport: [4]c_int = undefined; + c.glGetIntegerv(c.GL_VIEWPORT, &viewport); + c.glUniform2f(lineUniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); + c.glUniform2f(lineUniforms.start, pos[0], pos[1]); // Move the coordinates, so they are in the center of a pixel. + c.glUniform2f(lineUniforms.direction, dim[0] - 1, dim[1] - 1); // The height is a lot smaller because the inner edge of the rect is drawn. + c.glUniform1i(lineUniforms.lineColor, @bitCast(color)); + + lineVao.bind(); + c.glDrawArrays(c.GL_LINE_LOOP, 0, 5); + } + + // ---------------------------------------------------------------------------- + // MARK: fillCircle() + var circleUniforms: struct { + screen: c_int, + center: c_int, + radius: c_int, + circleColor: c_int, + } = undefined; + var circlePipeline: Pipeline = undefined; + var circleVao: VertexArray = undefined; + + fn initCircle() void { + circlePipeline = Pipeline.init( + "assets/cubyz/shaders/graphics/Circle.vert", + "assets/cubyz/shaders/graphics/Circle.frag", + "", + &circleUniforms, + SimpleVertex2D, + &.{}, + .{.cullMode = .none}, + .{.depthTest = false, .depthWrite = false}, + .{.attachments = &.{.alphaBlending}}, + ); + const rawData = [_]SimpleVertex2D{ + .{.pos = .{-1, -1}}, + .{.pos = .{-1, 1}}, + .{.pos = .{1, -1}}, + .{.pos = .{1, 1}}, + }; + + circleVao = .init(SimpleVertex2D, &rawData, null); + } + + fn deinitCircle() void { + circlePipeline.deinit(); + circleVao.deinit(); + } + + pub fn circle(_center: Vec2f, _radius: f32) void { + var center = _center; + var radius = _radius; + center *= @splat(scale); + center += translation; + radius *= scale; + circlePipeline.bind(getScissor()); + + var viewport: [4]c_int = undefined; + c.glGetIntegerv(c.GL_VIEWPORT, &viewport); + c.glUniform2f(circleUniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); + c.glUniform2f(circleUniforms.center, center[0], center[1]); // Move the coordinates, so they are in the center of a pixel. + c.glUniform1f(circleUniforms.radius, radius); // The height is a lot smaller because the inner edge of the rect is drawn. + c.glUniform1i(circleUniforms.circleColor, @bitCast(color)); + + circleVao.bind(); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + } + + // ---------------------------------------------------------------------------- + // MARK: drawImage() + // Luckily the vao of the regular rect can used. + var imageUniforms: struct { + screen: c_int, + start: c_int, + size: c_int, + color: c_int, + uvOffset: c_int, + uvDim: c_int, + } = undefined; + var imagePipeline: Pipeline = undefined; + + fn initImage() void { + imagePipeline = Pipeline.init( + "assets/cubyz/shaders/graphics/Image.vert", + "assets/cubyz/shaders/graphics/Image.frag", + "", + &imageUniforms, + SimpleVertex2D, + &.{}, + .{.cullMode = .none}, + .{.depthTest = false, .depthWrite = false}, + .{.attachments = &.{.alphaBlending}}, + ); + } + + fn deinitImage() void { + imagePipeline.deinit(); + } + + pub fn boundImage(_pos: Vec2f, _dim: Vec2f) void { + imagePipeline.bind(getScissor()); + + customShadedImage(&imageUniforms, _pos, _dim); + } + + pub fn boundSubImage(_pos: Vec2f, _dim: Vec2f, uvOffset: Vec2f, uvDim: Vec2f) void { + var pos = _pos; + var dim = _dim; + pos *= @splat(scale); + pos += translation; + dim *= @splat(scale); + pos = @floor(pos); + dim = @ceil(dim); + + imagePipeline.bind(getScissor()); + + var viewport: [4]c_int = undefined; + c.glGetIntegerv(c.GL_VIEWPORT, &viewport); + c.glUniform2f(imageUniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); + c.glUniform2f(imageUniforms.start, pos[0], pos[1]); + c.glUniform2f(imageUniforms.size, dim[0], dim[1]); + c.glUniform1i(imageUniforms.color, @bitCast(color)); + c.glUniform2f(imageUniforms.uvOffset, uvOffset[0], 1 - uvOffset[1] - uvDim[1]); + c.glUniform2f(imageUniforms.uvDim, uvDim[0], uvDim[1]); + + rectVao.bind(); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + } + + pub fn customShadedImage(uniforms: anytype, _pos: Vec2f, _dim: Vec2f) void { + var pos = _pos; + var dim = _dim; + pos *= @splat(scale); + pos += translation; + dim *= @splat(scale); + pos = @floor(pos); + dim = @ceil(dim); + + var viewport: [4]c_int = undefined; + c.glGetIntegerv(c.GL_VIEWPORT, &viewport); + c.glUniform2f(uniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); + c.glUniform2f(uniforms.start, pos[0], pos[1]); + c.glUniform2f(uniforms.size, dim[0], dim[1]); + c.glUniform1i(uniforms.color, @bitCast(color)); + c.glUniform2f(uniforms.uvOffset, 0, 0); + c.glUniform2f(uniforms.uvDim, 1, 1); + + rectVao.bind(); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + } + + // ---------------------------------------------------------------------------- + // MARK: customShadedRect() + + pub fn customShadedRect(uniforms: anytype, _pos: Vec2f, _dim: Vec2f) void { + var pos = _pos; + var dim = _dim; + pos *= @splat(scale); + pos += translation; + dim *= @splat(scale); + pos = @floor(pos); + dim = @ceil(dim); + + var viewport: [4]c_int = undefined; + c.glGetIntegerv(c.GL_VIEWPORT, &viewport); + c.glUniform2f(uniforms.screen, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); + c.glUniform2f(uniforms.start, pos[0], pos[1]); + c.glUniform2f(uniforms.size, dim[0], dim[1]); + c.glUniform1i(uniforms.color, @bitCast(color)); + c.glUniform1f(uniforms.scale, scale); + + rectVao.bind(); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + } + + // ---------------------------------------------------------------------------- + // MARK: text() + + pub fn text(_text: []const u8, x: f32, y: f32, fontSize: f32, alignment: TextBuffer.Alignment) void { + TextRendering.renderText(_text, x, y, fontSize, .{.color = @truncate(@as(u32, @bitCast(color)))}, alignment); + } + + pub inline fn print(comptime format: []const u8, args: anytype, x: f32, y: f32, fontSize: f32, alignment: TextBuffer.Alignment) void { + const string = std.fmt.allocPrint(main.stackAllocator.allocator, format, args) catch unreachable; + defer main.stackAllocator.free(string); + text(string, x, y, fontSize, alignment); + } }; pub const TextBuffer = struct { // MARK: TextBuffer - pub const Alignment = enum { - left, - center, - right, - }; - - pub const FontEffect = packed struct(u28) { - color: u24 = 0xffffff, - bold: bool = false, - italic: bool = false, - underline: bool = false, - strikethrough: bool = false, - - fn hasLine(self: FontEffect, comptime isUnderline: bool) bool { - if (isUnderline) return self.underline; - return self.strikethrough; - } - }; - - const Line = struct { - start: f32, - end: f32, - color: u24, - isUnderline: bool, - }; - - const LineBreak = struct { - index: u32, - width: f32, - }; - - const GlyphData = struct { - x_advance: f32, - y_advance: f32, - x_offset: f32, - y_offset: f32, - character: u21, - index: u32, - cluster: u32, - fontEffect: FontEffect, - characterIndex: u32, - }; - - alignment: Alignment, - width: f32, - buffer: ?*c.hb_buffer_t, - glyphs: []GlyphData, - lines: main.List(Line), - lineBreaks: main.List(LineBreak), - - fn addLine(self: *TextBuffer, line: Line) void { - if (line.start != line.end) { - self.lines.append(line); - } - } - - fn initLines(self: *TextBuffer, comptime isUnderline: bool) void { - var line: Line = Line{ .start = 0, .end = 0, .color = 0, .isUnderline = isUnderline }; - var lastFontEffect: FontEffect = .{}; - for (self.glyphs) |glyph| { - const fontEffect = glyph.fontEffect; - if (lastFontEffect.hasLine(isUnderline)) { - if (fontEffect.color != lastFontEffect.color) { - self.addLine(line); - line.color = fontEffect.color; - line.start = line.end; - } else if (!fontEffect.hasLine(isUnderline)) { - self.addLine(line); - } - } else if (fontEffect.hasLine(isUnderline)) { - line.start = line.end; - line.color = fontEffect.color; - } - lastFontEffect = fontEffect; - line.end += glyph.x_advance; - } - if (lastFontEffect.hasLine(isUnderline)) { - self.addLine(line); - } - } - - pub const Parser = struct { - unicodeIterator: std.unicode.Utf8Iterator, - currentFontEffect: FontEffect, - parsedText: main.List(u32), - fontEffects: main.List(FontEffect), - characterIndex: main.List(u32), - showControlCharacters: bool, - curChar: u21 = undefined, - curIndex: u32 = 0, - - fn appendControlGetNext(self: *Parser) ?void { - if (self.showControlCharacters) { - self.fontEffects.append(.{ .color = 0x808080 }); - self.parsedText.append(self.curChar); - self.characterIndex.append(self.curIndex); - } - self.curIndex = @intCast(self.unicodeIterator.i); - self.curChar = self.unicodeIterator.nextCodepoint() orelse return null; - } - - fn appendGetNext(self: *Parser) ?void { - self.fontEffects.append(self.currentFontEffect); - self.parsedText.append(self.curChar); - self.characterIndex.append(self.curIndex); - self.curIndex = @intCast(self.unicodeIterator.i); - self.curChar = self.unicodeIterator.nextCodepoint() orelse return null; - } - - fn peekNextByte(self: *Parser) u8 { - const next = self.unicodeIterator.peek(1); - if (next.len == 0) return 0; - return next[0]; - } - - fn parse(self: *Parser) void { - self.curIndex = @intCast(self.unicodeIterator.i); - self.curChar = self.unicodeIterator.nextCodepoint() orelse return; - while (true) switch (self.curChar) { - '*' => { - self.appendControlGetNext() orelse return; - if (self.curChar == '*') { - self.appendControlGetNext() orelse return; - self.currentFontEffect.bold = !self.currentFontEffect.bold; - } else { - self.currentFontEffect.italic = !self.currentFontEffect.italic; - } - }, - '_' => { - if (self.peekNextByte() == '_') { - self.appendControlGetNext() orelse return; - self.appendControlGetNext() orelse return; - self.currentFontEffect.underline = !self.currentFontEffect.underline; - } else { - self.appendGetNext() orelse return; - } - }, - '~' => { - if (self.peekNextByte() == '~') { - self.appendControlGetNext() orelse return; - self.appendControlGetNext() orelse return; - self.currentFontEffect.strikethrough = !self.currentFontEffect.strikethrough; - } else { - self.appendGetNext() orelse return; - } - }, - '\\' => { - self.appendControlGetNext() orelse return; - self.appendGetNext() orelse return; - }, - '#' => { - self.appendControlGetNext() orelse return; - var shift: u5 = 20; - while (true) : (shift -= 4) { - self.currentFontEffect.color = (self.currentFontEffect.color & ~(@as(u24, 0xf) << shift)) | @as(u24, switch (self.curChar) { - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => self.curChar - '0', - 'a', 'b', 'c', 'd', 'e', 'f' => self.curChar - 'a' + 10, - 'A', 'B', 'C', 'D', 'E', 'F' => self.curChar - 'A' + 10, - else => 0, - }) << shift; - self.appendControlGetNext() orelse return; - if (shift == 0) break; - } - }, - '§' => { - self.currentFontEffect = .{ .color = self.currentFontEffect.color }; - self.appendControlGetNext() orelse return; - }, - else => { - self.appendGetNext() orelse return; - }, - }; - } - - pub fn countVisibleCharacters(text: []const u8) usize { - var unicodeIterator = std.unicode.Utf8Iterator{ .bytes = text, .i = 0 }; - var count: usize = 0; - var curChar = unicodeIterator.nextCodepoint() orelse return count; - outer: while (true) switch (curChar) { - '*' => { - curChar = unicodeIterator.nextCodepoint() orelse break; - }, - '_' => { - curChar = unicodeIterator.nextCodepoint() orelse break; - if (curChar == '_') { - curChar = unicodeIterator.nextCodepoint() orelse break; - } else { - count += 1; - } - }, - '~' => { - curChar = unicodeIterator.nextCodepoint() orelse break; - if (curChar == '~') { - curChar = unicodeIterator.nextCodepoint() orelse break; - } else { - count += 1; - } - }, - '\\' => { - curChar = unicodeIterator.nextCodepoint() orelse break; - curChar = unicodeIterator.nextCodepoint() orelse break; - count += 1; - }, - '#' => { - for (0..7) |_| curChar = unicodeIterator.nextCodepoint() orelse break :outer; - }, - '§' => { - curChar = unicodeIterator.nextCodepoint() orelse break; - }, - else => { - count += 1; - curChar = unicodeIterator.nextCodepoint() orelse break; - }, - }; - return count; - } - }; - - pub fn init(allocator: NeverFailingAllocator, text: []const u8, initialFontEffect: FontEffect, showControlCharacters: bool, alignment: Alignment) TextBuffer { - var self: TextBuffer = .{ - .alignment = alignment, - .width = 1e9, - .buffer = null, - .glyphs = &.{}, - .lines = .init(allocator), - .lineBreaks = .init(allocator), - }; - // Parse the input text: - var parser = Parser{ - .unicodeIterator = std.unicode.Utf8Iterator{ .bytes = text, .i = 0 }, - .currentFontEffect = initialFontEffect, - .parsedText = .init(main.stackAllocator), - .fontEffects = .init(allocator), - .characterIndex = .init(allocator), - .showControlCharacters = showControlCharacters, - }; - defer parser.fontEffects.deinit(); - defer parser.parsedText.deinit(); - defer parser.characterIndex.deinit(); - parser.parse(); - if (parser.parsedText.items.len == 0) { - self.lineBreaks.append(.{ .index = 0, .width = 0 }); - return self; - } - - // Let harfbuzz do its thing: - const buffer = c.hb_buffer_create() orelse @panic("Out of Memory while creating harfbuzz buffer"); - defer c.hb_buffer_destroy(buffer); - c.hb_buffer_add_utf32(buffer, parser.parsedText.items.ptr, @intCast(parser.parsedText.items.len), 0, @intCast(parser.parsedText.items.len)); - c.hb_buffer_set_direction(buffer, c.HB_DIRECTION_LTR); - c.hb_buffer_set_script(buffer, c.HB_SCRIPT_COMMON); - c.hb_buffer_set_language(buffer, c.hb_language_get_default()); - c.hb_shape(TextRendering.harfbuzzFont, buffer, null, 0); - var glyphInfos: []c.hb_glyph_info_t = undefined; - var glyphPositions: []c.hb_glyph_position_t = undefined; - { - var len: c_uint = 0; - glyphInfos.ptr = c.hb_buffer_get_glyph_infos(buffer, &len).?; - glyphPositions.ptr = c.hb_buffer_get_glyph_positions(buffer, &len).?; - glyphInfos.len = len; - glyphPositions.len = len; - } - - // Guess the text index from the given cluster indices. Only works if the number of glyphs and the number of characters in a cluster is the same. - const textIndexGuess = main.stackAllocator.alloc(u32, glyphInfos.len); - defer main.stackAllocator.free(textIndexGuess); - for (textIndexGuess, 0..) |*index, i| { - if (i == 0 or glyphInfos[i - 1].cluster != glyphInfos[i].cluster) { - index.* = glyphInfos[i].cluster; - } else { - index.* = @min(textIndexGuess[i - 1] + 1, @as(u32, @intCast(parser.parsedText.items.len - 1))); - for (glyphInfos[i..]) |glyphInfo| { - if (glyphInfo.cluster != glyphInfos[i].cluster) { - index.* = @min(index.*, glyphInfo.cluster - 1); - break; - } - } - } - } - - // Merge it all together: - self.glyphs = allocator.alloc(GlyphData, glyphInfos.len); - for (self.glyphs, 0..) |*glyph, i| { - glyph.x_advance = @as(f32, @floatFromInt(glyphPositions[i].x_advance)) / TextRendering.fontUnitsPerPixel; - glyph.y_advance = @as(f32, @floatFromInt(glyphPositions[i].y_advance)) / TextRendering.fontUnitsPerPixel; - glyph.x_offset = @as(f32, @floatFromInt(glyphPositions[i].x_offset)) / TextRendering.fontUnitsPerPixel; - glyph.y_offset = @as(f32, @floatFromInt(glyphPositions[i].y_offset)) / TextRendering.fontUnitsPerPixel; - glyph.character = @intCast(parser.parsedText.items[textIndexGuess[i]]); - glyph.index = glyphInfos[i].codepoint; - glyph.cluster = glyphInfos[i].cluster; - glyph.fontEffect = parser.fontEffects.items[textIndexGuess[i]]; - glyph.characterIndex = parser.characterIndex.items[textIndexGuess[i]]; - } - - // Find the lines: - self.initLines(true); - self.initLines(false); - self.lineBreaks.append(.{ .index = 0, .width = 0 }); - self.lineBreaks.append(.{ .index = @intCast(self.glyphs.len), .width = 0 }); - return self; - } - - pub fn deinit(self: TextBuffer) void { - self.lines.allocator.free(self.glyphs); - self.lines.deinit(); - self.lineBreaks.deinit(); - } - - fn getLineOffset(self: TextBuffer, line: usize) f32 { - const factor: f32 = switch (self.alignment) { - .left => 0, - .center => 0.5, - .right => 1, - }; - const diff = self.width - self.lineBreaks.items[line + 1].width; - return diff * factor; - } - - pub fn mousePosToIndex(self: TextBuffer, mousePos: Vec2f, bufferLen: usize) u32 { - var line: usize = @trunc(@max(0, mousePos[1] / 16.0)); - line = @min(line, self.lineBreaks.items.len - 2); - var x: f32 = self.getLineOffset(line); - const start = self.lineBreaks.items[line].index; - const end = self.lineBreaks.items[line + 1].index; - for (self.glyphs[start..end]) |glyph| { - if (mousePos[0] < x + glyph.x_advance / 2) { - return @intCast(glyph.characterIndex); - } - - x += glyph.x_advance; - } - return @intCast(if (end < self.glyphs.len) self.glyphs[end - 1].characterIndex else bufferLen); - } - - pub fn indexToCursorPos(self: TextBuffer, index: u32) Vec2f { - var x: f32 = 0; - var y: f32 = 0; - var i: usize = 0; - while (true) { - x = self.getLineOffset(i); - for (self.glyphs[self.lineBreaks.items[i].index..self.lineBreaks.items[i + 1].index]) |glyph| { - if (glyph.characterIndex == index) { - return .{ x, y }; - } - - x += glyph.x_advance; - y -= glyph.y_advance; - } - i += 1; - if (i >= self.lineBreaks.items.len - 1) { - return .{ x, y }; - } - y += 16; - } - } - - /// Returns the calculated dimensions of the text block. - pub fn calculateLineBreaks(self: *TextBuffer, fontSize: f32, maxLineWidth: f32) Vec2f { - self.lineBreaks.clearRetainingCapacity(); - const spaceCharacterWidth = 8; - self.lineBreaks.append(.{ .index = 0, .width = 0 }); - const scaledMaxWidth = maxLineWidth / fontSize * 16.0; - var lineWidth: f32 = 0; - var lastSpaceWidth: f32 = 0; - var lastSpaceIndex: u32 = 0; - for (self.glyphs, 0..) |glyph, i| { - lineWidth += glyph.x_advance; - if (glyph.character == ' ') { - lastSpaceWidth = lineWidth; - lastSpaceIndex = @intCast(i + 1); - } - if (glyph.character == '\n') { - self.lineBreaks.append(.{ .index = @intCast(i + 1), .width = lineWidth - spaceCharacterWidth }); - lineWidth = 0; - lastSpaceIndex = 0; - lastSpaceWidth = 0; - } - if (lineWidth > scaledMaxWidth) { - if (lastSpaceIndex != 0) { - lineWidth -= lastSpaceWidth; - self.lineBreaks.append(.{ .index = lastSpaceIndex, .width = lastSpaceWidth - spaceCharacterWidth }); - lastSpaceIndex = 0; - lastSpaceWidth = 0; - } else { - self.lineBreaks.append(.{ .index = @intCast(i), .width = lineWidth - glyph.x_advance }); - lineWidth = glyph.x_advance; - lastSpaceIndex = 0; - lastSpaceWidth = 0; - } - } - } - self.width = maxLineWidth; - self.lineBreaks.append(.{ .index = @intCast(self.glyphs.len), .width = lineWidth }); - return Vec2f{ maxLineWidth * fontSize / 16.0, @as(f32, @floatFromInt(self.lineBreaks.items.len - 1)) * fontSize }; - } - - pub fn drawSelection(self: TextBuffer, pos: Vec2f, selectionStart: u32, selectionEnd: u32) void { - std.debug.assert(selectionStart <= selectionEnd); - var x: f32 = self.getLineOffset(0); - var y: f32 = 0; - var i: usize = 0; - var j: usize = 0; - // Find the start row: - outer: while (i < self.lineBreaks.items.len - 1) : (i += 1) { - x = self.getLineOffset(i); - while (j < self.lineBreaks.items[i + 1].index) : (j += 1) { - const glyph = self.glyphs[j]; - if (glyph.characterIndex >= selectionStart) break :outer; - x += glyph.x_advance; - y -= glyph.y_advance; - } - y += 16; - } - while (i < self.lineBreaks.items.len - 1) { - const startX = x; - while (j < self.lineBreaks.items[i + 1].index and j < selectionEnd) : (j += 1) { - const glyph = self.glyphs[j]; - if (glyph.characterIndex >= selectionEnd) break; - x += glyph.x_advance; - y -= glyph.y_advance; - } - draw.rect(pos + Vec2f{ startX, y }, .{ x - startX, 16 }); - i += 1; - if (i >= self.lineBreaks.items.len - 1) break; - x = self.getLineOffset(i); - y += 16; - } - } - - pub fn render(self: TextBuffer, _x: f32, _y: f32, _fontSize: f32) void { - self.renderShadow(_x, _y, _fontSize); - self.renderTextWithoutShadow(_x, _y, _fontSize); - } - - pub fn renderTextWithoutShadow(self: TextBuffer, _x: f32, _y: f32, _fontSize: f32) void { - const oldTranslation = draw.setTranslation(.{ _x, _y }); - defer draw.restoreTranslation(oldTranslation); - const oldScale = draw.setScale(_fontSize / 16.0); - defer draw.restoreScale(oldScale); - var x: f32 = 0; - var y: f32 = 0; - TextRendering.pipeline.bind(draw.getScissor()); - var viewport: [4]c_int = undefined; - c.glGetIntegerv(c.GL_VIEWPORT, &viewport); - c.glUniform2f(TextRendering.uniforms.scene, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); - c.glUniform1f(TextRendering.uniforms.ratio, draw.scale); - c.glUniform1f(TextRendering.uniforms.alpha, @as(f32, @floatFromInt(draw.color >> 24)) / 255.0); - c.glActiveTexture(c.GL_TEXTURE0); - c.glBindTexture(c.GL_TEXTURE_2D, TextRendering.glyphTexture[0]); - draw.rectVao.bind(); - const lineWraps: []f32 = main.stackAllocator.alloc(f32, self.lineBreaks.items.len - 1); - defer main.stackAllocator.free(lineWraps); - var i: usize = 0; - while (i < self.lineBreaks.items.len - 1) : (i += 1) { - x = self.getLineOffset(i); - for (self.glyphs[self.lineBreaks.items[i].index..self.lineBreaks.items[i + 1].index]) |glyph| { - if (glyph.character != '\n') { - const ftGlyph = TextRendering.getGlyph(glyph.index) catch continue; - TextRendering.drawGlyph(ftGlyph, x + glyph.x_offset, y - glyph.y_offset, @bitCast(glyph.fontEffect)); - } - x += glyph.x_advance; - y -= glyph.y_advance; - } - lineWraps[i] = x - self.getLineOffset(i); - x = 0; - y += 16; - } - - for (self.lines.items) |_line| { - var line: Line = _line; - y = 0; - y += if (line.isUnderline) 15 else 8; - const oldColor = draw.color; - draw.setColor(line.color | (@as(u32, 0xff000000) & draw.color)); - defer draw.setColor(oldColor); - for (lineWraps, 0..) |lineWrap, j| { - const lineStart = @max(0, line.start); - const lineEnd = @min(lineWrap, line.end); - if (lineStart < lineEnd) { - const start = Vec2f{ lineStart + self.getLineOffset(j), y }; - const dim = Vec2f{ lineEnd - lineStart, 1 }; - draw.rect(start, dim); - } - line.start -= lineWrap; - line.end -= lineWrap; - y += 16; - } - } - } - - fn shadowColor(color: u24) u24 { - const r: f32 = @floatFromInt(color >> 16); - const g: f32 = @floatFromInt(color >> 8 & 255); - const b: f32 = @floatFromInt(color & 255); - const perceivedBrightness = @sqrt(0.299 * r * r + 0.587 * g * g + 0.114 * b * b); - if (perceivedBrightness < 64) { - return 0xffffff; // Make shadows white for better readability. - } else { - return 0; - } - } - - fn renderShadow(self: TextBuffer, _x: f32, _y: f32, _fontSize: f32) void { // Basically a copy of render with some color and position changes. - const oldTranslation = draw.setTranslation(.{ _x + _fontSize / 16.0, _y + _fontSize / 16.0 }); - defer draw.restoreTranslation(oldTranslation); - const oldScale = draw.setScale(_fontSize / 16.0); - defer draw.restoreScale(oldScale); - var x: f32 = 0; - var y: f32 = 0; - TextRendering.pipeline.bind(draw.getScissor()); - var viewport: [4]c_int = undefined; - c.glGetIntegerv(c.GL_VIEWPORT, &viewport); - c.glUniform2f(TextRendering.uniforms.scene, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); - c.glUniform1f(TextRendering.uniforms.ratio, draw.scale); - c.glUniform1f(TextRendering.uniforms.alpha, @as(f32, @floatFromInt(draw.color >> 24)) / 255.0); - c.glActiveTexture(c.GL_TEXTURE0); - c.glBindTexture(c.GL_TEXTURE_2D, TextRendering.glyphTexture[0]); - draw.rectVao.bind(); - const lineWraps: []f32 = main.stackAllocator.alloc(f32, self.lineBreaks.items.len - 1); - defer main.stackAllocator.free(lineWraps); - var i: usize = 0; - while (i < self.lineBreaks.items.len - 1) : (i += 1) { - x = self.getLineOffset(i); - for (self.glyphs[self.lineBreaks.items[i].index..self.lineBreaks.items[i + 1].index]) |glyph| { - if (glyph.character != '\n') { - const ftGlyph = TextRendering.getGlyph(glyph.index) catch continue; - var fontEffect = glyph.fontEffect; - fontEffect.color = shadowColor(fontEffect.color); - TextRendering.drawGlyph(ftGlyph, x + glyph.x_offset, y - glyph.y_offset, @bitCast(fontEffect)); - } - x += glyph.x_advance; - y -= glyph.y_advance; - } - lineWraps[i] = x - self.getLineOffset(i); - x = 0; - y += 16; - } - - for (self.lines.items) |_line| { - var line: Line = _line; - y = 0; - y += if (line.isUnderline) 15 else 8; - const oldColor = draw.color; - draw.setColor(shadowColor(line.color) | (@as(u32, 0xff000000) & draw.color)); - defer draw.setColor(oldColor); - for (lineWraps, 0..) |lineWrap, j| { - const lineStart = @max(0, line.start); - const lineEnd = @min(lineWrap, line.end); - if (lineStart < lineEnd) { - const start = Vec2f{ lineStart + self.getLineOffset(j), y }; - const dim = Vec2f{ lineEnd - lineStart, 1 }; - draw.rect(start, dim); - } - line.start -= lineWrap; - line.end -= lineWrap; - y += 16; - } - } - } + pub const Alignment = enum { + left, + center, + right, + }; + + pub const FontEffect = packed struct(u28) { + color: u24 = 0xffffff, + bold: bool = false, + italic: bool = false, + underline: bool = false, + strikethrough: bool = false, + + fn hasLine(self: FontEffect, comptime isUnderline: bool) bool { + if (isUnderline) return self.underline; + return self.strikethrough; + } + }; + + const Line = struct { + start: f32, + end: f32, + color: u24, + isUnderline: bool, + }; + + const LineBreak = struct { + index: u32, + width: f32, + }; + + const GlyphData = struct { + x_advance: f32, + y_advance: f32, + x_offset: f32, + y_offset: f32, + character: u21, + index: u32, + cluster: u32, + fontEffect: FontEffect, + characterIndex: u32, + }; + + alignment: Alignment, + width: f32, + buffer: ?*c.hb_buffer_t, + glyphs: []GlyphData, + lines: main.List(Line), + lineBreaks: main.List(LineBreak), + + fn addLine(self: *TextBuffer, line: Line) void { + if (line.start != line.end) { + self.lines.append(line); + } + } + + fn initLines(self: *TextBuffer, comptime isUnderline: bool) void { + var line: Line = Line{.start = 0, .end = 0, .color = 0, .isUnderline = isUnderline}; + var lastFontEffect: FontEffect = .{}; + for (self.glyphs) |glyph| { + const fontEffect = glyph.fontEffect; + if (lastFontEffect.hasLine(isUnderline)) { + if (fontEffect.color != lastFontEffect.color) { + self.addLine(line); + line.color = fontEffect.color; + line.start = line.end; + } else if (!fontEffect.hasLine(isUnderline)) { + self.addLine(line); + } + } else if (fontEffect.hasLine(isUnderline)) { + line.start = line.end; + line.color = fontEffect.color; + } + lastFontEffect = fontEffect; + line.end += glyph.x_advance; + } + if (lastFontEffect.hasLine(isUnderline)) { + self.addLine(line); + } + } + + pub const Parser = struct { + unicodeIterator: std.unicode.Utf8Iterator, + currentFontEffect: FontEffect, + parsedText: main.List(u32), + fontEffects: main.List(FontEffect), + characterIndex: main.List(u32), + showControlCharacters: bool, + curChar: u21 = undefined, + curIndex: u32 = 0, + + fn appendControlGetNext(self: *Parser) ?void { + if (self.showControlCharacters) { + self.fontEffects.append(.{.color = 0x808080}); + self.parsedText.append(self.curChar); + self.characterIndex.append(self.curIndex); + } + self.curIndex = @intCast(self.unicodeIterator.i); + self.curChar = self.unicodeIterator.nextCodepoint() orelse return null; + } + + fn appendGetNext(self: *Parser) ?void { + self.fontEffects.append(self.currentFontEffect); + self.parsedText.append(self.curChar); + self.characterIndex.append(self.curIndex); + self.curIndex = @intCast(self.unicodeIterator.i); + self.curChar = self.unicodeIterator.nextCodepoint() orelse return null; + } + + fn peekNextByte(self: *Parser) u8 { + const next = self.unicodeIterator.peek(1); + if (next.len == 0) return 0; + return next[0]; + } + + fn parse(self: *Parser) void { + self.curIndex = @intCast(self.unicodeIterator.i); + self.curChar = self.unicodeIterator.nextCodepoint() orelse return; + while (true) switch (self.curChar) { + '*' => { + self.appendControlGetNext() orelse return; + if (self.curChar == '*') { + self.appendControlGetNext() orelse return; + self.currentFontEffect.bold = !self.currentFontEffect.bold; + } else { + self.currentFontEffect.italic = !self.currentFontEffect.italic; + } + }, + '_' => { + if (self.peekNextByte() == '_') { + self.appendControlGetNext() orelse return; + self.appendControlGetNext() orelse return; + self.currentFontEffect.underline = !self.currentFontEffect.underline; + } else { + self.appendGetNext() orelse return; + } + }, + '~' => { + if (self.peekNextByte() == '~') { + self.appendControlGetNext() orelse return; + self.appendControlGetNext() orelse return; + self.currentFontEffect.strikethrough = !self.currentFontEffect.strikethrough; + } else { + self.appendGetNext() orelse return; + } + }, + '\\' => { + self.appendControlGetNext() orelse return; + self.appendGetNext() orelse return; + }, + '#' => { + self.appendControlGetNext() orelse return; + var shift: u5 = 20; + while (true) : (shift -= 4) { + self.currentFontEffect.color = (self.currentFontEffect.color & ~(@as(u24, 0xf) << shift)) | @as(u24, switch (self.curChar) { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => self.curChar - '0', + 'a', 'b', 'c', 'd', 'e', 'f' => self.curChar - 'a' + 10, + 'A', 'B', 'C', 'D', 'E', 'F' => self.curChar - 'A' + 10, + else => 0, + }) << shift; + self.appendControlGetNext() orelse return; + if (shift == 0) break; + } + }, + '§' => { + self.currentFontEffect = .{.color = self.currentFontEffect.color}; + self.appendControlGetNext() orelse return; + }, + else => { + self.appendGetNext() orelse return; + }, + }; + } + + pub fn countVisibleCharacters(text: []const u8) usize { + var unicodeIterator = std.unicode.Utf8Iterator{.bytes = text, .i = 0}; + var count: usize = 0; + var curChar = unicodeIterator.nextCodepoint() orelse return count; + outer: while (true) switch (curChar) { + '*' => { + curChar = unicodeIterator.nextCodepoint() orelse break; + }, + '_' => { + curChar = unicodeIterator.nextCodepoint() orelse break; + if (curChar == '_') { + curChar = unicodeIterator.nextCodepoint() orelse break; + } else { + count += 1; + } + }, + '~' => { + curChar = unicodeIterator.nextCodepoint() orelse break; + if (curChar == '~') { + curChar = unicodeIterator.nextCodepoint() orelse break; + } else { + count += 1; + } + }, + '\\' => { + curChar = unicodeIterator.nextCodepoint() orelse break; + curChar = unicodeIterator.nextCodepoint() orelse break; + count += 1; + }, + '#' => { + for (0..7) |_| curChar = unicodeIterator.nextCodepoint() orelse break :outer; + }, + '§' => { + curChar = unicodeIterator.nextCodepoint() orelse break; + }, + else => { + count += 1; + curChar = unicodeIterator.nextCodepoint() orelse break; + }, + }; + return count; + } + }; + + pub fn init(allocator: NeverFailingAllocator, text: []const u8, initialFontEffect: FontEffect, showControlCharacters: bool, alignment: Alignment) TextBuffer { + var self: TextBuffer = .{ + .alignment = alignment, + .width = 1e9, + .buffer = null, + .glyphs = &.{}, + .lines = .init(allocator), + .lineBreaks = .init(allocator), + }; + // Parse the input text: + var parser = Parser{ + .unicodeIterator = std.unicode.Utf8Iterator{.bytes = text, .i = 0}, + .currentFontEffect = initialFontEffect, + .parsedText = .init(main.stackAllocator), + .fontEffects = .init(allocator), + .characterIndex = .init(allocator), + .showControlCharacters = showControlCharacters, + }; + defer parser.fontEffects.deinit(); + defer parser.parsedText.deinit(); + defer parser.characterIndex.deinit(); + parser.parse(); + if (parser.parsedText.items.len == 0) { + self.lineBreaks.append(.{.index = 0, .width = 0}); + return self; + } + + // Let harfbuzz do its thing: + const buffer = c.hb_buffer_create() orelse @panic("Out of Memory while creating harfbuzz buffer"); + defer c.hb_buffer_destroy(buffer); + c.hb_buffer_add_utf32(buffer, parser.parsedText.items.ptr, @intCast(parser.parsedText.items.len), 0, @intCast(parser.parsedText.items.len)); + c.hb_buffer_set_direction(buffer, c.HB_DIRECTION_LTR); + c.hb_buffer_set_script(buffer, c.HB_SCRIPT_COMMON); + c.hb_buffer_set_language(buffer, c.hb_language_get_default()); + c.hb_shape(TextRendering.harfbuzzFont, buffer, null, 0); + var glyphInfos: []c.hb_glyph_info_t = undefined; + var glyphPositions: []c.hb_glyph_position_t = undefined; + { + var len: c_uint = 0; + glyphInfos.ptr = c.hb_buffer_get_glyph_infos(buffer, &len).?; + glyphPositions.ptr = c.hb_buffer_get_glyph_positions(buffer, &len).?; + glyphInfos.len = len; + glyphPositions.len = len; + } + + // Guess the text index from the given cluster indices. Only works if the number of glyphs and the number of characters in a cluster is the same. + const textIndexGuess = main.stackAllocator.alloc(u32, glyphInfos.len); + defer main.stackAllocator.free(textIndexGuess); + for (textIndexGuess, 0..) |*index, i| { + if (i == 0 or glyphInfos[i - 1].cluster != glyphInfos[i].cluster) { + index.* = glyphInfos[i].cluster; + } else { + index.* = @min(textIndexGuess[i - 1] + 1, @as(u32, @intCast(parser.parsedText.items.len - 1))); + for (glyphInfos[i..]) |glyphInfo| { + if (glyphInfo.cluster != glyphInfos[i].cluster) { + index.* = @min(index.*, glyphInfo.cluster - 1); + break; + } + } + } + } + + // Merge it all together: + self.glyphs = allocator.alloc(GlyphData, glyphInfos.len); + for (self.glyphs, 0..) |*glyph, i| { + glyph.x_advance = @as(f32, @floatFromInt(glyphPositions[i].x_advance))/TextRendering.fontUnitsPerPixel; + glyph.y_advance = @as(f32, @floatFromInt(glyphPositions[i].y_advance))/TextRendering.fontUnitsPerPixel; + glyph.x_offset = @as(f32, @floatFromInt(glyphPositions[i].x_offset))/TextRendering.fontUnitsPerPixel; + glyph.y_offset = @as(f32, @floatFromInt(glyphPositions[i].y_offset))/TextRendering.fontUnitsPerPixel; + glyph.character = @intCast(parser.parsedText.items[textIndexGuess[i]]); + glyph.index = glyphInfos[i].codepoint; + glyph.cluster = glyphInfos[i].cluster; + glyph.fontEffect = parser.fontEffects.items[textIndexGuess[i]]; + glyph.characterIndex = parser.characterIndex.items[textIndexGuess[i]]; + } + + // Find the lines: + self.initLines(true); + self.initLines(false); + self.lineBreaks.append(.{.index = 0, .width = 0}); + self.lineBreaks.append(.{.index = @intCast(self.glyphs.len), .width = 0}); + return self; + } + + pub fn deinit(self: TextBuffer) void { + self.lines.allocator.free(self.glyphs); + self.lines.deinit(); + self.lineBreaks.deinit(); + } + + fn getLineOffset(self: TextBuffer, line: usize) f32 { + const factor: f32 = switch (self.alignment) { + .left => 0, + .center => 0.5, + .right => 1, + }; + const diff = self.width - self.lineBreaks.items[line + 1].width; + return diff*factor; + } + + pub fn mousePosToIndex(self: TextBuffer, mousePos: Vec2f, bufferLen: usize) u32 { + var line: usize = @trunc(@max(0, mousePos[1]/16.0)); + line = @min(line, self.lineBreaks.items.len - 2); + var x: f32 = self.getLineOffset(line); + const start = self.lineBreaks.items[line].index; + const end = self.lineBreaks.items[line + 1].index; + for (self.glyphs[start..end]) |glyph| { + if (mousePos[0] < x + glyph.x_advance/2) { + return @intCast(glyph.characterIndex); + } + + x += glyph.x_advance; + } + return @intCast(if (end < self.glyphs.len) self.glyphs[end - 1].characterIndex else bufferLen); + } + + pub fn indexToCursorPos(self: TextBuffer, index: u32) Vec2f { + var x: f32 = 0; + var y: f32 = 0; + var i: usize = 0; + while (true) { + x = self.getLineOffset(i); + for (self.glyphs[self.lineBreaks.items[i].index..self.lineBreaks.items[i + 1].index]) |glyph| { + if (glyph.characterIndex == index) { + return .{x, y}; + } + + x += glyph.x_advance; + y -= glyph.y_advance; + } + i += 1; + if (i >= self.lineBreaks.items.len - 1) { + return .{x, y}; + } + y += 16; + } + } + + /// Returns the calculated dimensions of the text block. + pub fn calculateLineBreaks(self: *TextBuffer, fontSize: f32, maxLineWidth: f32) Vec2f { + self.lineBreaks.clearRetainingCapacity(); + const spaceCharacterWidth = 8; + self.lineBreaks.append(.{.index = 0, .width = 0}); + const scaledMaxWidth = maxLineWidth/fontSize*16.0; + var lineWidth: f32 = 0; + var lastSpaceWidth: f32 = 0; + var lastSpaceIndex: u32 = 0; + for (self.glyphs, 0..) |glyph, i| { + lineWidth += glyph.x_advance; + if (glyph.character == ' ') { + lastSpaceWidth = lineWidth; + lastSpaceIndex = @intCast(i + 1); + } + if (glyph.character == '\n') { + self.lineBreaks.append(.{.index = @intCast(i + 1), .width = lineWidth - spaceCharacterWidth}); + lineWidth = 0; + lastSpaceIndex = 0; + lastSpaceWidth = 0; + } + if (lineWidth > scaledMaxWidth) { + if (lastSpaceIndex != 0) { + lineWidth -= lastSpaceWidth; + self.lineBreaks.append(.{.index = lastSpaceIndex, .width = lastSpaceWidth - spaceCharacterWidth}); + lastSpaceIndex = 0; + lastSpaceWidth = 0; + } else { + self.lineBreaks.append(.{.index = @intCast(i), .width = lineWidth - glyph.x_advance}); + lineWidth = glyph.x_advance; + lastSpaceIndex = 0; + lastSpaceWidth = 0; + } + } + } + self.width = maxLineWidth; + self.lineBreaks.append(.{.index = @intCast(self.glyphs.len), .width = lineWidth}); + return Vec2f{maxLineWidth*fontSize/16.0, @as(f32, @floatFromInt(self.lineBreaks.items.len - 1))*fontSize}; + } + + pub fn drawSelection(self: TextBuffer, pos: Vec2f, selectionStart: u32, selectionEnd: u32) void { + std.debug.assert(selectionStart <= selectionEnd); + var x: f32 = self.getLineOffset(0); + var y: f32 = 0; + var i: usize = 0; + var j: usize = 0; + // Find the start row: + outer: while (i < self.lineBreaks.items.len - 1) : (i += 1) { + x = self.getLineOffset(i); + while (j < self.lineBreaks.items[i + 1].index) : (j += 1) { + const glyph = self.glyphs[j]; + if (glyph.characterIndex >= selectionStart) break :outer; + x += glyph.x_advance; + y -= glyph.y_advance; + } + y += 16; + } + while (i < self.lineBreaks.items.len - 1) { + const startX = x; + while (j < self.lineBreaks.items[i + 1].index and j < selectionEnd) : (j += 1) { + const glyph = self.glyphs[j]; + if (glyph.characterIndex >= selectionEnd) break; + x += glyph.x_advance; + y -= glyph.y_advance; + } + draw.rect(pos + Vec2f{startX, y}, .{x - startX, 16}); + i += 1; + if (i >= self.lineBreaks.items.len - 1) break; + x = self.getLineOffset(i); + y += 16; + } + } + + pub fn render(self: TextBuffer, _x: f32, _y: f32, _fontSize: f32) void { + self.renderShadow(_x, _y, _fontSize); + self.renderTextWithoutShadow(_x, _y, _fontSize); + } + + pub fn renderTextWithoutShadow(self: TextBuffer, _x: f32, _y: f32, _fontSize: f32) void { + const oldTranslation = draw.setTranslation(.{_x, _y}); + defer draw.restoreTranslation(oldTranslation); + const oldScale = draw.setScale(_fontSize/16.0); + defer draw.restoreScale(oldScale); + var x: f32 = 0; + var y: f32 = 0; + TextRendering.pipeline.bind(draw.getScissor()); + var viewport: [4]c_int = undefined; + c.glGetIntegerv(c.GL_VIEWPORT, &viewport); + c.glUniform2f(TextRendering.uniforms.scene, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); + c.glUniform1f(TextRendering.uniforms.ratio, draw.scale); + c.glUniform1f(TextRendering.uniforms.alpha, @as(f32, @floatFromInt(draw.color >> 24))/255.0); + c.glActiveTexture(c.GL_TEXTURE0); + c.glBindTexture(c.GL_TEXTURE_2D, TextRendering.glyphTexture[0]); + draw.rectVao.bind(); + const lineWraps: []f32 = main.stackAllocator.alloc(f32, self.lineBreaks.items.len - 1); + defer main.stackAllocator.free(lineWraps); + var i: usize = 0; + while (i < self.lineBreaks.items.len - 1) : (i += 1) { + x = self.getLineOffset(i); + for (self.glyphs[self.lineBreaks.items[i].index..self.lineBreaks.items[i + 1].index]) |glyph| { + if (glyph.character != '\n') { + const ftGlyph = TextRendering.getGlyph(glyph.index) catch continue; + TextRendering.drawGlyph(ftGlyph, x + glyph.x_offset, y - glyph.y_offset, @bitCast(glyph.fontEffect)); + } + x += glyph.x_advance; + y -= glyph.y_advance; + } + lineWraps[i] = x - self.getLineOffset(i); + x = 0; + y += 16; + } + + for (self.lines.items) |_line| { + var line: Line = _line; + y = 0; + y += if (line.isUnderline) 15 else 8; + const oldColor = draw.color; + draw.setColor(line.color | (@as(u32, 0xff000000) & draw.color)); + defer draw.setColor(oldColor); + for (lineWraps, 0..) |lineWrap, j| { + const lineStart = @max(0, line.start); + const lineEnd = @min(lineWrap, line.end); + if (lineStart < lineEnd) { + const start = Vec2f{lineStart + self.getLineOffset(j), y}; + const dim = Vec2f{lineEnd - lineStart, 1}; + draw.rect(start, dim); + } + line.start -= lineWrap; + line.end -= lineWrap; + y += 16; + } + } + } + + fn shadowColor(color: u24) u24 { + const r: f32 = @floatFromInt(color >> 16); + const g: f32 = @floatFromInt(color >> 8 & 255); + const b: f32 = @floatFromInt(color & 255); + const perceivedBrightness = @sqrt(0.299*r*r + 0.587*g*g + 0.114*b*b); + if (perceivedBrightness < 64) { + return 0xffffff; // Make shadows white for better readability. + } else { + return 0; + } + } + + fn renderShadow(self: TextBuffer, _x: f32, _y: f32, _fontSize: f32) void { // Basically a copy of render with some color and position changes. + const oldTranslation = draw.setTranslation(.{_x + _fontSize/16.0, _y + _fontSize/16.0}); + defer draw.restoreTranslation(oldTranslation); + const oldScale = draw.setScale(_fontSize/16.0); + defer draw.restoreScale(oldScale); + var x: f32 = 0; + var y: f32 = 0; + TextRendering.pipeline.bind(draw.getScissor()); + var viewport: [4]c_int = undefined; + c.glGetIntegerv(c.GL_VIEWPORT, &viewport); + c.glUniform2f(TextRendering.uniforms.scene, @floatFromInt(viewport[2]), @floatFromInt(viewport[3])); + c.glUniform1f(TextRendering.uniforms.ratio, draw.scale); + c.glUniform1f(TextRendering.uniforms.alpha, @as(f32, @floatFromInt(draw.color >> 24))/255.0); + c.glActiveTexture(c.GL_TEXTURE0); + c.glBindTexture(c.GL_TEXTURE_2D, TextRendering.glyphTexture[0]); + draw.rectVao.bind(); + const lineWraps: []f32 = main.stackAllocator.alloc(f32, self.lineBreaks.items.len - 1); + defer main.stackAllocator.free(lineWraps); + var i: usize = 0; + while (i < self.lineBreaks.items.len - 1) : (i += 1) { + x = self.getLineOffset(i); + for (self.glyphs[self.lineBreaks.items[i].index..self.lineBreaks.items[i + 1].index]) |glyph| { + if (glyph.character != '\n') { + const ftGlyph = TextRendering.getGlyph(glyph.index) catch continue; + var fontEffect = glyph.fontEffect; + fontEffect.color = shadowColor(fontEffect.color); + TextRendering.drawGlyph(ftGlyph, x + glyph.x_offset, y - glyph.y_offset, @bitCast(fontEffect)); + } + x += glyph.x_advance; + y -= glyph.y_advance; + } + lineWraps[i] = x - self.getLineOffset(i); + x = 0; + y += 16; + } + + for (self.lines.items) |_line| { + var line: Line = _line; + y = 0; + y += if (line.isUnderline) 15 else 8; + const oldColor = draw.color; + draw.setColor(shadowColor(line.color) | (@as(u32, 0xff000000) & draw.color)); + defer draw.setColor(oldColor); + for (lineWraps, 0..) |lineWrap, j| { + const lineStart = @max(0, line.start); + const lineEnd = @min(lineWrap, line.end); + if (lineStart < lineEnd) { + const start = Vec2f{lineStart + self.getLineOffset(j), y}; + const dim = Vec2f{lineEnd - lineStart, 1}; + draw.rect(start, dim); + } + line.start -= lineWrap; + line.end -= lineWrap; + y += 16; + } + } + } }; const TextRendering = struct { // MARK: TextRendering - const Glyph = struct { - textureX: i32, - size: Vec2i, - bearing: Vec2i, - advance: f32, - }; - var pipeline: Pipeline = undefined; - var uniforms: struct { - textureBounds: c_int, - textureRect: c_int, - scene: c_int, - offset: c_int, - ratio: c_int, - fontEffects: c_int, - fontSize: c_int, - alpha: c_int, - } = undefined; - - var freetypeLib: c.FT_Library = undefined; - var freetypeFace: c.FT_Face = undefined; - var harfbuzzFace: ?*c.hb_face_t = undefined; - var harfbuzzFont: ?*c.hb_font_t = undefined; - var glyphMapping: main.List(u31) = undefined; - var glyphData: main.List(Glyph) = undefined; - var glyphTexture: [2]c_uint = undefined; - var textureWidth: i32 = 1024; - const textureHeight: i32 = 16; - var textureOffset: i32 = 0; - var fontUnitsPerPixel: f32 = undefined; - - fn ftError(errorCode: c.FT_Error) !void { - if (errorCode == 0) return; - const errorString = c.FT_Error_String(errorCode); - std.log.err("Got freetype error {s}", .{errorString}); - return error.freetype; - } - - fn init() !void { - pipeline = Pipeline.init( - "assets/cubyz/shaders/graphics/Text.vert", - "assets/cubyz/shaders/graphics/Text.frag", - "", - &uniforms, - draw.SimpleVertex2D, - &.{}, - .{ .cullMode = .none }, - .{ .depthTest = false, .depthWrite = false }, - .{ .attachments = &.{.alphaBlending} }, - ); - pipeline.bind(null); - errdefer pipeline.deinit(); - c.glUniform1f(uniforms.alpha, 1.0); - c.glUniform2f(uniforms.fontSize, @floatFromInt(textureWidth), @floatFromInt(textureHeight)); - try ftError(c.FT_Init_FreeType(&freetypeLib)); - try ftError(c.FT_New_Face(freetypeLib, "assets/cubyz/fonts/unscii-16-full.ttf", 0, &freetypeFace)); - try ftError(c.FT_Set_Pixel_Sizes(freetypeFace, 0, textureHeight)); - harfbuzzFace = c.hb_ft_face_create_referenced(freetypeFace); - harfbuzzFont = c.hb_font_create(harfbuzzFace); - fontUnitsPerPixel = @as(f32, @floatFromInt(freetypeFace.*.units_per_EM)) / @as(f32, @floatFromInt(textureHeight)); - - glyphMapping = .init(main.globalAllocator); - glyphData = .init(main.globalAllocator); - glyphData.append(undefined); // 0 is a reserved value. - c.glGenTextures(2, &glyphTexture); - c.glBindTexture(c.GL_TEXTURE_2D, glyphTexture[0]); - c.glTexImage2D(c.GL_TEXTURE_2D, 0, c.GL_R8, textureWidth, textureHeight, 0, c.GL_RED, c.GL_UNSIGNED_BYTE, null); - 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.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT); - c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT); - c.glBindTexture(c.GL_TEXTURE_2D, glyphTexture[1]); - 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.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT); - c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT); - } - - fn deinit() void { - pipeline.deinit(); - ftError(c.FT_Done_FreeType(freetypeLib)) catch {}; - glyphMapping.deinit(); - glyphData.deinit(); - c.glDeleteTextures(2, &glyphTexture); - c.hb_font_destroy(harfbuzzFont); - } - - fn resizeTexture(newWidth: i32) void { - textureWidth = newWidth; - const swap = glyphTexture[1]; - glyphTexture[1] = glyphTexture[0]; - glyphTexture[0] = swap; - c.glActiveTexture(c.GL_TEXTURE0); - c.glBindTexture(c.GL_TEXTURE_2D, glyphTexture[0]); - c.glTexImage2D(c.GL_TEXTURE_2D, 0, c.GL_R8, newWidth, textureHeight, 0, c.GL_RED, c.GL_UNSIGNED_BYTE, null); - c.glCopyImageSubData(glyphTexture[1], c.GL_TEXTURE_2D, 0, 0, 0, 0, glyphTexture[0], c.GL_TEXTURE_2D, 0, 0, 0, 0, textureOffset, textureHeight, 1); - pipeline.bind(draw.getScissor()); - c.glUniform2f(uniforms.fontSize, @floatFromInt(textureWidth), @floatFromInt(textureHeight)); - } - - fn uploadData(bitmap: c.FT_Bitmap) void { - const width: i32 = @bitCast(bitmap.width); - const height: i32 = @bitCast(bitmap.rows); - const buffer = bitmap.buffer orelse return; - if (textureOffset + width > textureWidth) { - resizeTexture(textureWidth * 2); - } - c.glPixelStorei(c.GL_UNPACK_ALIGNMENT, 1); - c.glTexSubImage2D(c.GL_TEXTURE_2D, 0, textureOffset, 0, width, height, c.GL_RED, c.GL_UNSIGNED_BYTE, buffer); - textureOffset += width; - } - - fn getGlyph(index: u32) !Glyph { - if (index >= glyphMapping.items.len) { - glyphMapping.appendNTimes(0, index - glyphMapping.items.len + 1); - } - if (glyphMapping.items[index] == 0) { // glyph was not initialized yet. - try ftError(c.FT_Load_Glyph(freetypeFace, index, c.FT_LOAD_RENDER)); - const glyph = freetypeFace.*.glyph; - const bitmap = glyph.*.bitmap; - const width = bitmap.width; - const height = bitmap.rows; - glyphMapping.items[index] = @intCast(glyphData.items.len); - glyphData.addOne().* = Glyph{ - .textureX = textureOffset, - .size = Vec2i{ @intCast(width), @intCast(height) }, - .bearing = Vec2i{ glyph.*.bitmap_left, 16 - glyph.*.bitmap_top }, - .advance = @as(f32, @floatFromInt(glyph.*.advance.x)) / @as(f32, 1 << 6), - }; - uploadData(bitmap); - } - return glyphData.items[glyphMapping.items[index]]; - } - - fn drawGlyph(glyph: Glyph, _x: f32, _y: f32, fontEffects: u28) void { - var x = _x; - var y = _y; - x *= draw.scale; - y *= draw.scale; - x += draw.translation[0]; - y += draw.translation[1]; - x = @floor(x); - y = @ceil(y); - c.glUniform1i(uniforms.fontEffects, fontEffects); - c.glUniform4f(uniforms.textureBounds, @floatFromInt(glyph.textureX), 0, @floatFromInt(glyph.size[0]), @floatFromInt(glyph.size[1])); - if (fontEffects & 0x1000000 != 0) { // bold - c.glUniform2f(uniforms.offset, @as(f32, @floatFromInt(glyph.bearing[0])) * draw.scale + x - 1, @as(f32, @floatFromInt(glyph.bearing[1])) * draw.scale + y - 1); - c.glUniform4f(uniforms.textureRect, @floatFromInt(glyph.textureX - 1), -1, @floatFromInt(glyph.size[0] + 2), @floatFromInt(glyph.size[1] + 2)); - } else { - c.glUniform2f(uniforms.offset, @as(f32, @floatFromInt(glyph.bearing[0])) * draw.scale + x, @as(f32, @floatFromInt(glyph.bearing[1])) * draw.scale + y); - c.glUniform4f(uniforms.textureRect, @floatFromInt(glyph.textureX), 0, @floatFromInt(glyph.size[0]), @floatFromInt(glyph.size[1])); - } - c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); - } - - fn renderText(text: []const u8, x: f32, y: f32, fontSize: f32, initialFontEffect: TextBuffer.FontEffect, alignment: TextBuffer.Alignment) void { - const buf = TextBuffer.init(main.stackAllocator, text, initialFontEffect, false, alignment); - defer buf.deinit(); - - buf.render(x, y, fontSize); - } + const Glyph = struct { + textureX: i32, + size: Vec2i, + bearing: Vec2i, + advance: f32, + }; + var pipeline: Pipeline = undefined; + var uniforms: struct { + textureBounds: c_int, + textureRect: c_int, + scene: c_int, + offset: c_int, + ratio: c_int, + fontEffects: c_int, + fontSize: c_int, + alpha: c_int, + } = undefined; + + var freetypeLib: c.FT_Library = undefined; + var freetypeFace: c.FT_Face = undefined; + var harfbuzzFace: ?*c.hb_face_t = undefined; + var harfbuzzFont: ?*c.hb_font_t = undefined; + var glyphMapping: main.List(u31) = undefined; + var glyphData: main.List(Glyph) = undefined; + var glyphTexture: [2]c_uint = undefined; + var textureWidth: i32 = 1024; + const textureHeight: i32 = 16; + var textureOffset: i32 = 0; + var fontUnitsPerPixel: f32 = undefined; + + fn ftError(errorCode: c.FT_Error) !void { + if (errorCode == 0) return; + const errorString = c.FT_Error_String(errorCode); + std.log.err("Got freetype error {s}", .{errorString}); + return error.freetype; + } + + fn init() !void { + pipeline = Pipeline.init( + "assets/cubyz/shaders/graphics/Text.vert", + "assets/cubyz/shaders/graphics/Text.frag", + "", + &uniforms, + draw.SimpleVertex2D, + &.{}, + .{.cullMode = .none}, + .{.depthTest = false, .depthWrite = false}, + .{.attachments = &.{.alphaBlending}}, + ); + pipeline.bind(null); + errdefer pipeline.deinit(); + c.glUniform1f(uniforms.alpha, 1.0); + c.glUniform2f(uniforms.fontSize, @floatFromInt(textureWidth), @floatFromInt(textureHeight)); + try ftError(c.FT_Init_FreeType(&freetypeLib)); + try ftError(c.FT_New_Face(freetypeLib, "assets/cubyz/fonts/unscii-16-full.ttf", 0, &freetypeFace)); + try ftError(c.FT_Set_Pixel_Sizes(freetypeFace, 0, textureHeight)); + harfbuzzFace = c.hb_ft_face_create_referenced(freetypeFace); + harfbuzzFont = c.hb_font_create(harfbuzzFace); + fontUnitsPerPixel = @as(f32, @floatFromInt(freetypeFace.*.units_per_EM))/@as(f32, @floatFromInt(textureHeight)); + + glyphMapping = .init(main.globalAllocator); + glyphData = .init(main.globalAllocator); + glyphData.append(undefined); // 0 is a reserved value. + c.glGenTextures(2, &glyphTexture); + c.glBindTexture(c.GL_TEXTURE_2D, glyphTexture[0]); + c.glTexImage2D(c.GL_TEXTURE_2D, 0, c.GL_R8, textureWidth, textureHeight, 0, c.GL_RED, c.GL_UNSIGNED_BYTE, null); + 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.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT); + c.glBindTexture(c.GL_TEXTURE_2D, glyphTexture[1]); + 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.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT); + } + + fn deinit() void { + pipeline.deinit(); + ftError(c.FT_Done_FreeType(freetypeLib)) catch {}; + glyphMapping.deinit(); + glyphData.deinit(); + c.glDeleteTextures(2, &glyphTexture); + c.hb_font_destroy(harfbuzzFont); + } + + fn resizeTexture(newWidth: i32) void { + textureWidth = newWidth; + const swap = glyphTexture[1]; + glyphTexture[1] = glyphTexture[0]; + glyphTexture[0] = swap; + c.glActiveTexture(c.GL_TEXTURE0); + c.glBindTexture(c.GL_TEXTURE_2D, glyphTexture[0]); + c.glTexImage2D(c.GL_TEXTURE_2D, 0, c.GL_R8, newWidth, textureHeight, 0, c.GL_RED, c.GL_UNSIGNED_BYTE, null); + c.glCopyImageSubData(glyphTexture[1], c.GL_TEXTURE_2D, 0, 0, 0, 0, glyphTexture[0], c.GL_TEXTURE_2D, 0, 0, 0, 0, textureOffset, textureHeight, 1); + pipeline.bind(draw.getScissor()); + c.glUniform2f(uniforms.fontSize, @floatFromInt(textureWidth), @floatFromInt(textureHeight)); + } + + fn uploadData(bitmap: c.FT_Bitmap) void { + const width: i32 = @bitCast(bitmap.width); + const height: i32 = @bitCast(bitmap.rows); + const buffer = bitmap.buffer orelse return; + if (textureOffset + width > textureWidth) { + resizeTexture(textureWidth*2); + } + c.glPixelStorei(c.GL_UNPACK_ALIGNMENT, 1); + c.glTexSubImage2D(c.GL_TEXTURE_2D, 0, textureOffset, 0, width, height, c.GL_RED, c.GL_UNSIGNED_BYTE, buffer); + textureOffset += width; + } + + fn getGlyph(index: u32) !Glyph { + if (index >= glyphMapping.items.len) { + glyphMapping.appendNTimes(0, index - glyphMapping.items.len + 1); + } + if (glyphMapping.items[index] == 0) { // glyph was not initialized yet. + try ftError(c.FT_Load_Glyph(freetypeFace, index, c.FT_LOAD_RENDER)); + const glyph = freetypeFace.*.glyph; + const bitmap = glyph.*.bitmap; + const width = bitmap.width; + const height = bitmap.rows; + glyphMapping.items[index] = @intCast(glyphData.items.len); + glyphData.addOne().* = Glyph{ + .textureX = textureOffset, + .size = Vec2i{@intCast(width), @intCast(height)}, + .bearing = Vec2i{glyph.*.bitmap_left, 16 - glyph.*.bitmap_top}, + .advance = @as(f32, @floatFromInt(glyph.*.advance.x))/@as(f32, 1 << 6), + }; + uploadData(bitmap); + } + return glyphData.items[glyphMapping.items[index]]; + } + + fn drawGlyph(glyph: Glyph, _x: f32, _y: f32, fontEffects: u28) void { + var x = _x; + var y = _y; + x *= draw.scale; + y *= draw.scale; + x += draw.translation[0]; + y += draw.translation[1]; + x = @floor(x); + y = @ceil(y); + c.glUniform1i(uniforms.fontEffects, fontEffects); + c.glUniform4f(uniforms.textureBounds, @floatFromInt(glyph.textureX), 0, @floatFromInt(glyph.size[0]), @floatFromInt(glyph.size[1])); + if (fontEffects & 0x1000000 != 0) { // bold + c.glUniform2f(uniforms.offset, @as(f32, @floatFromInt(glyph.bearing[0]))*draw.scale + x - 1, @as(f32, @floatFromInt(glyph.bearing[1]))*draw.scale + y - 1); + c.glUniform4f(uniforms.textureRect, @floatFromInt(glyph.textureX - 1), -1, @floatFromInt(glyph.size[0] + 2), @floatFromInt(glyph.size[1] + 2)); + } else { + c.glUniform2f(uniforms.offset, @as(f32, @floatFromInt(glyph.bearing[0]))*draw.scale + x, @as(f32, @floatFromInt(glyph.bearing[1]))*draw.scale + y); + c.glUniform4f(uniforms.textureRect, @floatFromInt(glyph.textureX), 0, @floatFromInt(glyph.size[0]), @floatFromInt(glyph.size[1])); + } + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + } + + fn renderText(text: []const u8, x: f32, y: f32, fontSize: f32, initialFontEffect: TextBuffer.FontEffect, alignment: TextBuffer.Alignment) void { + const buf = TextBuffer.init(main.stackAllocator, text, initialFontEffect, false, alignment); + defer buf.deinit(); + + buf.render(x, y, fontSize); + } }; pub fn init() void { // MARK: init() - draw.initCircle(); - draw.initImage(); - draw.initLine(); - draw.initRect(); - draw.initRectBorder(); - TextRendering.init() catch |err| { - std.log.err("Error while initializing TextRendering: {s}", .{@errorName(err)}); - }; - block_texture.init(); - pipelines.init(); - RenderPass.initRenderPasses() catch @panic("Failed to create render passes"); + draw.initCircle(); + draw.initImage(); + draw.initLine(); + draw.initRect(); + draw.initRectBorder(); + TextRendering.init() catch |err| { + std.log.err("Error while initializing TextRendering: {s}", .{@errorName(err)}); + }; + block_texture.init(); + pipelines.init(); + RenderPass.initRenderPasses() catch @panic("Failed to create render passes"); } pub fn deinit() void { - RenderPass.deinitRenderPasses(); - draw.deinitCircle(); - draw.deinitImage(); - draw.deinitLine(); - draw.deinitRect(); - draw.deinitRectBorder(); - TextRendering.deinit(); - block_texture.deinit(); - pipelines.deinit(); + RenderPass.deinitRenderPasses(); + draw.deinitCircle(); + draw.deinitImage(); + draw.deinitLine(); + draw.deinitRect(); + draw.deinitRectBorder(); + TextRendering.deinit(); + block_texture.deinit(); + pipelines.deinit(); } pub const RenderPass = struct { // MARK: RenderPass - renderPass: c.VkRenderPass, - - pub var renderToWindow: RenderPass = undefined; - - fn initRenderPasses() !void { - if (main.settings.launchConfig.vulkanTestingMode) { - renderToWindow = try RenderPass.init(); - } - } - - fn deinitRenderPasses() void { - if (main.settings.launchConfig.vulkanTestingMode) { - renderToWindow.deinit(); - } - } - - pub fn init() !RenderPass { - const colorAttachment = c.VkAttachmentDescription{ - .format = vulkan.SwapChain.imageFormat, // TODO: This needs to be configurable to be able to render to f16 framebuffer - .samples = c.VK_SAMPLE_COUNT_1_BIT, - .loadOp = c.VK_ATTACHMENT_LOAD_OP_CLEAR, - .storeOp = c.VK_ATTACHMENT_STORE_OP_STORE, - .stencilLoadOp = c.VK_ATTACHMENT_LOAD_OP_DONT_CARE, - .stencilStoreOp = c.VK_ATTACHMENT_STORE_OP_DONT_CARE, - .initialLayout = c.VK_IMAGE_LAYOUT_UNDEFINED, - .finalLayout = c.VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, - }; - const colorAttachmentRef = c.VkAttachmentReference{ - .attachment = 0, - .layout = c.VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, - }; - const subpass = c.VkSubpassDescription{ - .pipelineBindPoint = c.VK_PIPELINE_BIND_POINT_GRAPHICS, - .colorAttachmentCount = 1, - .pColorAttachments = &colorAttachmentRef, - }; - const dependency = c.VkSubpassDependency{ - .srcSubpass = c.VK_SUBPASS_EXTERNAL, - .dstSubpass = 0, - .srcStageMask = c.VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, - .srcAccessMask = 0, - .dstStageMask = c.VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, - .dstAccessMask = c.VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, - }; - const renderPassInfo = c.VkRenderPassCreateInfo{ - .sType = c.VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO, - .attachmentCount = 1, - .pAttachments = &colorAttachment, - .subpassCount = 1, - .pSubpasses = &subpass, - .dependencyCount = 1, - .pDependencies = &dependency, - }; - - var self: RenderPass = undefined; - try vulkan.checkResultErr(c.vkCreateRenderPass(vulkan.device, &renderPassInfo, null, &self.renderPass)); - return self; - } - - pub fn deinit(self: RenderPass) void { - c.vkDestroyRenderPass(vulkan.device, self.renderPass, null); - } + renderPass: c.VkRenderPass, + + pub var renderToWindow: RenderPass = undefined; + + fn initRenderPasses() !void { + if (main.settings.launchConfig.vulkanTestingMode) { + renderToWindow = try RenderPass.init(); + } + } + + fn deinitRenderPasses() void { + if (main.settings.launchConfig.vulkanTestingMode) { + renderToWindow.deinit(); + } + } + + pub fn init() !RenderPass { + const colorAttachment = c.VkAttachmentDescription{ + .format = vulkan.SwapChain.imageFormat, // TODO: This needs to be configurable to be able to render to f16 framebuffer + .samples = c.VK_SAMPLE_COUNT_1_BIT, + .loadOp = c.VK_ATTACHMENT_LOAD_OP_CLEAR, + .storeOp = c.VK_ATTACHMENT_STORE_OP_STORE, + .stencilLoadOp = c.VK_ATTACHMENT_LOAD_OP_DONT_CARE, + .stencilStoreOp = c.VK_ATTACHMENT_STORE_OP_DONT_CARE, + .initialLayout = c.VK_IMAGE_LAYOUT_UNDEFINED, + .finalLayout = c.VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, + }; + const colorAttachmentRef = c.VkAttachmentReference{ + .attachment = 0, + .layout = c.VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + }; + const subpass = c.VkSubpassDescription{ + .pipelineBindPoint = c.VK_PIPELINE_BIND_POINT_GRAPHICS, + .colorAttachmentCount = 1, + .pColorAttachments = &colorAttachmentRef, + }; + const dependency = c.VkSubpassDependency{ + .srcSubpass = c.VK_SUBPASS_EXTERNAL, + .dstSubpass = 0, + .srcStageMask = c.VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + .srcAccessMask = 0, + .dstStageMask = c.VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + .dstAccessMask = c.VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + }; + const renderPassInfo = c.VkRenderPassCreateInfo{ + .sType = c.VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO, + .attachmentCount = 1, + .pAttachments = &colorAttachment, + .subpassCount = 1, + .pSubpasses = &subpass, + .dependencyCount = 1, + .pDependencies = &dependency, + }; + + var self: RenderPass = undefined; + try vulkan.checkResultErr(c.vkCreateRenderPass(vulkan.device, &renderPassInfo, null, &self.renderPass)); + return self; + } + + pub fn deinit(self: RenderPass) void { + c.vkDestroyRenderPass(vulkan.device, self.renderPass, null); + } }; pub const VertexArray = struct { // MARK: VertexArray - vao: c_uint, - vbo: c_uint, - ibo: ?c_uint, - - pub const EmptyVertex = struct { - pub const attributeDescriptions: []const c.VkVertexInputAttributeDescription = &.{}; - }; - - pub fn init(T: type, data: []const T, indices_: ?[]const u32) VertexArray { - var result: VertexArray = undefined; - c.glGenVertexArrays(1, &result.vao); - c.glBindVertexArray(result.vao); - c.glGenBuffers(1, &result.vbo); - c.glBindBuffer(c.GL_ARRAY_BUFFER, result.vbo); - c.glBufferData(c.GL_ARRAY_BUFFER, @intCast(data.len * @sizeOf(T)), data.ptr, c.GL_STATIC_DRAW); - if (indices_) |indices| { - result.ibo = 0; - c.glGenBuffers(1, &result.ibo.?); - c.glBindBuffer(c.GL_ELEMENT_ARRAY_BUFFER, result.ibo.?); - c.glBufferData(c.GL_ELEMENT_ARRAY_BUFFER, @intCast(indices.len * @sizeOf(u32)), indices.ptr, c.GL_STATIC_DRAW); - } else { - result.ibo = null; - } - - const attributeDescriptions: []const c.VkVertexInputAttributeDescription = T.attributeDescriptions; - inline for (attributeDescriptions) |desc| { - std.debug.assert(desc.binding == 0); - c.glEnableVertexAttribArray(desc.location); - const glType = comptime switch (desc.format) { - c.VK_FORMAT_R32_SFLOAT => c.GL_FLOAT, - c.VK_FORMAT_R32G32_SFLOAT => c.GL_FLOAT, - c.VK_FORMAT_R32G32B32_SFLOAT => c.GL_FLOAT, - c.VK_FORMAT_R32G32B32A32_SFLOAT => c.GL_FLOAT, - else => @compileError("Unrecognized format"), - }; - const size = comptime switch (desc.format) { - c.VK_FORMAT_R32_SFLOAT => 1, - c.VK_FORMAT_R32G32_SFLOAT => 2, - c.VK_FORMAT_R32G32B32_SFLOAT => 3, - c.VK_FORMAT_R32G32B32A32_SFLOAT => 4, - else => @compileError("Unrecognized format"), - }; - c.glVertexAttribPointer(desc.location, size, glType, c.GL_FALSE, @sizeOf(T), @ptrFromInt(desc.offset)); - } - - c.glBindVertexArray(0); - return result; - } - - pub fn deinit(self: VertexArray) void { - c.glDeleteVertexArrays(1, &self.vao); - c.glDeleteBuffers(1, &self.vbo); - if (self.ibo != null) { - c.glDeleteBuffers(1, &self.ibo.?); - } - } - - pub fn bind(self: VertexArray) void { - c.glBindVertexArray(self.vao); - } + vao: c_uint, + vbo: c_uint, + ibo: ?c_uint, + + pub const EmptyVertex = struct { + pub const attributeDescriptions: []const c.VkVertexInputAttributeDescription = &.{}; + }; + + pub fn init(T: type, data: []const T, indices_: ?[]const u32) VertexArray { + var result: VertexArray = undefined; + c.glGenVertexArrays(1, &result.vao); + c.glBindVertexArray(result.vao); + c.glGenBuffers(1, &result.vbo); + c.glBindBuffer(c.GL_ARRAY_BUFFER, result.vbo); + c.glBufferData(c.GL_ARRAY_BUFFER, @intCast(data.len*@sizeOf(T)), data.ptr, c.GL_STATIC_DRAW); + if (indices_) |indices| { + result.ibo = 0; + c.glGenBuffers(1, &result.ibo.?); + c.glBindBuffer(c.GL_ELEMENT_ARRAY_BUFFER, result.ibo.?); + c.glBufferData(c.GL_ELEMENT_ARRAY_BUFFER, @intCast(indices.len*@sizeOf(u32)), indices.ptr, c.GL_STATIC_DRAW); + } else { + result.ibo = null; + } + + const attributeDescriptions: []const c.VkVertexInputAttributeDescription = T.attributeDescriptions; + inline for (attributeDescriptions) |desc| { + std.debug.assert(desc.binding == 0); + c.glEnableVertexAttribArray(desc.location); + const glType = comptime switch (desc.format) { + c.VK_FORMAT_R32_SFLOAT => c.GL_FLOAT, + c.VK_FORMAT_R32G32_SFLOAT => c.GL_FLOAT, + c.VK_FORMAT_R32G32B32_SFLOAT => c.GL_FLOAT, + c.VK_FORMAT_R32G32B32A32_SFLOAT => c.GL_FLOAT, + else => @compileError("Unrecognized format"), + }; + const size = comptime switch (desc.format) { + c.VK_FORMAT_R32_SFLOAT => 1, + c.VK_FORMAT_R32G32_SFLOAT => 2, + c.VK_FORMAT_R32G32B32_SFLOAT => 3, + c.VK_FORMAT_R32G32B32A32_SFLOAT => 4, + else => @compileError("Unrecognized format"), + }; + c.glVertexAttribPointer(desc.location, size, glType, c.GL_FALSE, @sizeOf(T), @ptrFromInt(desc.offset)); + } + + c.glBindVertexArray(0); + return result; + } + + pub fn deinit(self: VertexArray) void { + c.glDeleteVertexArrays(1, &self.vao); + c.glDeleteBuffers(1, &self.vbo); + if (self.ibo != null) { + c.glDeleteBuffers(1, &self.ibo.?); + } + } + + pub fn bind(self: VertexArray) void { + c.glBindVertexArray(self.vao); + } }; pub const SSBO = struct { // MARK: SSBO - bufferID: c_uint, - pub fn init() SSBO { - var self = SSBO{ .bufferID = undefined }; - c.glGenBuffers(1, &self.bufferID); - return self; - } - - pub fn initStatic(comptime T: type, data: []const T) SSBO { - var self = SSBO{ .bufferID = undefined }; - c.glGenBuffers(1, &self.bufferID); - c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.bufferID); - c.glBufferStorage(c.GL_SHADER_STORAGE_BUFFER, @intCast(data.len * @sizeOf(T)), data.ptr, 0); - return self; - } - - pub fn initStaticSize(comptime T: type, len: usize) SSBO { - var self = SSBO{ .bufferID = undefined }; - c.glGenBuffers(1, &self.bufferID); - c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.bufferID); - c.glBufferStorage(c.GL_SHADER_STORAGE_BUFFER, @intCast(len * @sizeOf(T)), null, 0); - return self; - } - - pub fn deinit(self: SSBO) void { - c.glDeleteBuffers(1, &self.bufferID); - } - - pub fn bind(self: SSBO, binding: c_uint) void { - c.glBindBufferBase(c.GL_SHADER_STORAGE_BUFFER, binding, self.bufferID); - } - - pub fn bufferData(self: SSBO, comptime T: type, data: []const T) void { - c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.bufferID); - c.glBufferData(c.GL_SHADER_STORAGE_BUFFER, @intCast(data.len * @sizeOf(T)), data.ptr, c.GL_STATIC_DRAW); - c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, 0); - } - - pub fn bufferSubData(self: SSBO, comptime T: type, data: []const T, length: usize) void { - c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.bufferID); - c.glBufferSubData(c.GL_SHADER_STORAGE_BUFFER, 0, @intCast(length * @sizeOf(T)), data.ptr); - c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, 0); - } - - pub fn createDynamicBuffer(self: SSBO, comptime T: type, size: usize) void { - c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.bufferID); - c.glBufferData(c.GL_SHADER_STORAGE_BUFFER, @intCast(size * @sizeOf(T)), null, c.GL_DYNAMIC_DRAW); - c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, 0); - } + bufferID: c_uint, + pub fn init() SSBO { + var self = SSBO{.bufferID = undefined}; + c.glGenBuffers(1, &self.bufferID); + return self; + } + + pub fn initStatic(comptime T: type, data: []const T) SSBO { + var self = SSBO{.bufferID = undefined}; + c.glGenBuffers(1, &self.bufferID); + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.bufferID); + c.glBufferStorage(c.GL_SHADER_STORAGE_BUFFER, @intCast(data.len*@sizeOf(T)), data.ptr, 0); + return self; + } + + pub fn initStaticSize(comptime T: type, len: usize) SSBO { + var self = SSBO{.bufferID = undefined}; + c.glGenBuffers(1, &self.bufferID); + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.bufferID); + c.glBufferStorage(c.GL_SHADER_STORAGE_BUFFER, @intCast(len*@sizeOf(T)), null, 0); + return self; + } + + pub fn deinit(self: SSBO) void { + c.glDeleteBuffers(1, &self.bufferID); + } + + pub fn bind(self: SSBO, binding: c_uint) void { + c.glBindBufferBase(c.GL_SHADER_STORAGE_BUFFER, binding, self.bufferID); + } + + pub fn bufferData(self: SSBO, comptime T: type, data: []const T) void { + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.bufferID); + c.glBufferData(c.GL_SHADER_STORAGE_BUFFER, @intCast(data.len*@sizeOf(T)), data.ptr, c.GL_STATIC_DRAW); + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, 0); + } + + pub fn bufferSubData(self: SSBO, comptime T: type, data: []const T, length: usize) void { + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.bufferID); + c.glBufferSubData(c.GL_SHADER_STORAGE_BUFFER, 0, @intCast(length*@sizeOf(T)), data.ptr); + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, 0); + } + + pub fn createDynamicBuffer(self: SSBO, comptime T: type, size: usize) void { + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.bufferID); + c.glBufferData(c.GL_SHADER_STORAGE_BUFFER, @intCast(size*@sizeOf(T)), null, c.GL_DYNAMIC_DRAW); + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, 0); + } }; pub const SubAllocation = struct { - start: u31, - len: u31, + start: u31, + len: u31, }; /// A big SSBO that is able to allocate/free smaller regions. pub fn LargeBuffer(comptime Entry: type) type { // MARK: LargerBuffer - return struct { - ssbo: SSBO, - freeBlocks: main.List(SubAllocation), - fences: [3]c.GLsync, - fencedFreeLists: [3]main.List(SubAllocation), - activeFence: u8, - capacity: u31, - used: u31, - binding: c_uint, - - const Self = @This(); - - fn createBuffer(self: *Self, size: u31) void { - self.ssbo = .init(); - c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.ssbo.bufferID); - const flags = c.GL_MAP_WRITE_BIT | c.GL_DYNAMIC_STORAGE_BIT; - const bytes = @as(c.GLsizeiptr, size) * @sizeOf(Entry); - c.glBufferStorage(c.GL_SHADER_STORAGE_BUFFER, bytes, null, flags); - self.ssbo.bind(self.binding); - self.capacity = size; - } - - pub fn init(self: *Self, allocator: NeverFailingAllocator, size: u31, binding: c_uint) void { - self.used = 0; - self.binding = binding; - self.createBuffer(size); - self.activeFence = 0; - for (&self.fences) |*fence| { - fence.* = c.glFenceSync(c.GL_SYNC_GPU_COMMANDS_COMPLETE, 0); - } - for (&self.fencedFreeLists) |*list| { - list.* = .init(allocator); - } - - self.freeBlocks = .init(allocator); - self.freeBlocks.append(.{ .start = 0, .len = size }); - } - - pub fn deinit(self: *Self) void { - for (self.fences) |fence| { - c.glDeleteSync(fence); - } - for (self.fencedFreeLists) |list| { - list.deinit(); - } - self.ssbo.deinit(); - self.freeBlocks.deinit(); - } - - pub fn beginRender(self: *Self) void { - self.activeFence += 1; - if (self.activeFence == self.fences.len) self.activeFence = 0; - const endTime = main.timestamp().addDuration(.fromMilliseconds(5)); - while (self.fencedFreeLists[self.activeFence].popOrNull()) |allocation| { - self.finalFree(allocation); - if (main.timestamp().durationTo(endTime).nanoseconds < 0) break; // TODO: Remove after #1434 - } - _ = c.glClientWaitSync(self.fences[self.activeFence], 0, c.GL_TIMEOUT_IGNORED); // Make sure the render calls that accessed these parts of the buffer have finished. - } - - pub fn endRender(self: *Self) void { - c.glDeleteSync(self.fences[self.activeFence]); - self.fences[self.activeFence] = c.glFenceSync(c.GL_SYNC_GPU_COMMANDS_COMPLETE, 0); - } - - pub fn rawAlloc(self: *Self, size: u31) SubAllocation { - var smallestBlock: ?*SubAllocation = null; - for (self.freeBlocks.items, 0..) |*block, i| { - if (size == block.len) { - self.used += size; - return self.freeBlocks.swapRemove(i); - } - if (size < block.len and if (smallestBlock) |_smallestBlock| block.len < _smallestBlock.len else true) { - smallestBlock = block; - } - } - if (smallestBlock) |block| { - const result = SubAllocation{ .start = block.start, .len = size }; - block.start += size; - block.len -= size; - self.used += size; - return result; - } else { - std.log.info("Resizing internal mesh buffer from {} MiB to {} MiB", .{ @as(usize, self.capacity) * @sizeOf(Entry) >> 20, (@as(usize, self.capacity) * @sizeOf(Entry) >> 20) * 2 }); - if (@as(usize, self.capacity) * @sizeOf(Entry) * 2 > 1 << 31) @panic("OpenGL 2 GiB buffer size limit reached. Please lower your render distance."); - const oldBuffer = self.ssbo; - defer oldBuffer.deinit(); - const oldCapacity = self.capacity; - self.createBuffer(self.capacity *| 2); // TODO: Is there a way to free the old buffer before creating the new one? - self.used += self.capacity - oldCapacity; - self.finalFree(.{ .start = oldCapacity, .len = self.capacity - oldCapacity }); - - c.glBindBuffer(c.GL_COPY_READ_BUFFER, oldBuffer.bufferID); - c.glBindBuffer(c.GL_COPY_WRITE_BUFFER, self.ssbo.bufferID); - c.glCopyBufferSubData(c.GL_COPY_READ_BUFFER, c.GL_COPY_WRITE_BUFFER, 0, 0, @as(c.GLsizeiptr, oldCapacity) * @sizeOf(Entry)); - return rawAlloc(self, size); - } - } - - fn finalFree(self: *Self, _allocation: SubAllocation) void { - if (_allocation.len == 0) return; - self.used -= _allocation.len; - var allocation = _allocation; - for (self.freeBlocks.items, 0..) |*block, i| { - if (allocation.start + allocation.len == block.start) { - allocation.len += block.len; - _ = self.freeBlocks.swapRemove(i); - break; - } - } - for (self.freeBlocks.items) |*block| { - if (allocation.start == block.start + block.len) { - block.len += allocation.len; - return; - } - } - self.freeBlocks.append(allocation); - } - - pub fn free(self: *Self, allocation: SubAllocation) void { - if (allocation.len == 0) return; - self.fencedFreeLists[self.activeFence].append(allocation); - } - - /// Must unmap after use! - pub fn allocateAndMapRange(self: *Self, len: usize, allocation: *SubAllocation) []Entry { - self.free(allocation.*); - if (len == 0) { - allocation.len = 0; - return &.{}; - } - allocation.* = self.rawAlloc(@intCast(len)); - c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.ssbo.bufferID); - const ptr: [*]Entry = @ptrCast(@alignCast(c.glMapBufferRange( - c.GL_SHADER_STORAGE_BUFFER, - @as(c.GLintptr, allocation.start) * @sizeOf(Entry), - @as(c.GLsizeiptr, allocation.len) * @sizeOf(Entry), - c.GL_MAP_WRITE_BIT | c.GL_MAP_INVALIDATE_RANGE_BIT, - ))); - return ptr[0..len]; - } - - pub fn unmapRange(self: *Self, range: []Entry) void { - if (range.len == 0) return; - c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.ssbo.bufferID); - std.debug.assert(c.glUnmapBuffer(c.GL_SHADER_STORAGE_BUFFER) == c.GL_TRUE); - } - - pub fn uploadData(self: *Self, data: []const Entry, allocation: *SubAllocation) void { - self.free(allocation.*); - if (data.len == 0) { - allocation.len = 0; - return; - } - allocation.* = self.rawAlloc(@intCast(data.len)); - c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.ssbo.bufferID); - const ptr: [*]Entry = @ptrCast(@alignCast(c.glMapBufferRange( - c.GL_SHADER_STORAGE_BUFFER, - @as(c.GLintptr, allocation.start) * @sizeOf(Entry), - @as(c.GLsizeiptr, allocation.len) * @sizeOf(Entry), - c.GL_MAP_WRITE_BIT | c.GL_MAP_INVALIDATE_RANGE_BIT, - ))); - @memcpy(ptr, data); - std.debug.assert(c.glUnmapBuffer(c.GL_SHADER_STORAGE_BUFFER) == c.GL_TRUE); - } - }; + return struct { + ssbo: SSBO, + freeBlocks: main.List(SubAllocation), + fences: [3]c.GLsync, + fencedFreeLists: [3]main.List(SubAllocation), + activeFence: u8, + capacity: u31, + used: u31, + binding: c_uint, + + const Self = @This(); + + fn createBuffer(self: *Self, size: u31) void { + self.ssbo = .init(); + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.ssbo.bufferID); + const flags = c.GL_MAP_WRITE_BIT | c.GL_DYNAMIC_STORAGE_BIT; + const bytes = @as(c.GLsizeiptr, size)*@sizeOf(Entry); + c.glBufferStorage(c.GL_SHADER_STORAGE_BUFFER, bytes, null, flags); + self.ssbo.bind(self.binding); + self.capacity = size; + } + + pub fn init(self: *Self, allocator: NeverFailingAllocator, size: u31, binding: c_uint) void { + self.used = 0; + self.binding = binding; + self.createBuffer(size); + self.activeFence = 0; + for (&self.fences) |*fence| { + fence.* = c.glFenceSync(c.GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + } + for (&self.fencedFreeLists) |*list| { + list.* = .init(allocator); + } + + self.freeBlocks = .init(allocator); + self.freeBlocks.append(.{.start = 0, .len = size}); + } + + pub fn deinit(self: *Self) void { + for (self.fences) |fence| { + c.glDeleteSync(fence); + } + for (self.fencedFreeLists) |list| { + list.deinit(); + } + self.ssbo.deinit(); + self.freeBlocks.deinit(); + } + + pub fn beginRender(self: *Self) void { + self.activeFence += 1; + if (self.activeFence == self.fences.len) self.activeFence = 0; + const endTime = main.timestamp().addDuration(.fromMilliseconds(5)); + while (self.fencedFreeLists[self.activeFence].popOrNull()) |allocation| { + self.finalFree(allocation); + if (main.timestamp().durationTo(endTime).nanoseconds < 0) break; // TODO: Remove after #1434 + } + _ = c.glClientWaitSync(self.fences[self.activeFence], 0, c.GL_TIMEOUT_IGNORED); // Make sure the render calls that accessed these parts of the buffer have finished. + } + + pub fn endRender(self: *Self) void { + c.glDeleteSync(self.fences[self.activeFence]); + self.fences[self.activeFence] = c.glFenceSync(c.GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + } + + pub fn rawAlloc(self: *Self, size: u31) SubAllocation { + var smallestBlock: ?*SubAllocation = null; + for (self.freeBlocks.items, 0..) |*block, i| { + if (size == block.len) { + self.used += size; + return self.freeBlocks.swapRemove(i); + } + if (size < block.len and if (smallestBlock) |_smallestBlock| block.len < _smallestBlock.len else true) { + smallestBlock = block; + } + } + if (smallestBlock) |block| { + const result = SubAllocation{.start = block.start, .len = size}; + block.start += size; + block.len -= size; + self.used += size; + return result; + } else { + std.log.info("Resizing internal mesh buffer from {} MiB to {} MiB", .{@as(usize, self.capacity)*@sizeOf(Entry) >> 20, (@as(usize, self.capacity)*@sizeOf(Entry) >> 20)*2}); + if (@as(usize, self.capacity)*@sizeOf(Entry)*2 > 1 << 31) @panic("OpenGL 2 GiB buffer size limit reached. Please lower your render distance."); + const oldBuffer = self.ssbo; + defer oldBuffer.deinit(); + const oldCapacity = self.capacity; + self.createBuffer(self.capacity*|2); // TODO: Is there a way to free the old buffer before creating the new one? + self.used += self.capacity - oldCapacity; + self.finalFree(.{.start = oldCapacity, .len = self.capacity - oldCapacity}); + + c.glBindBuffer(c.GL_COPY_READ_BUFFER, oldBuffer.bufferID); + c.glBindBuffer(c.GL_COPY_WRITE_BUFFER, self.ssbo.bufferID); + c.glCopyBufferSubData(c.GL_COPY_READ_BUFFER, c.GL_COPY_WRITE_BUFFER, 0, 0, @as(c.GLsizeiptr, oldCapacity)*@sizeOf(Entry)); + return rawAlloc(self, size); + } + } + + fn finalFree(self: *Self, _allocation: SubAllocation) void { + if (_allocation.len == 0) return; + self.used -= _allocation.len; + var allocation = _allocation; + for (self.freeBlocks.items, 0..) |*block, i| { + if (allocation.start + allocation.len == block.start) { + allocation.len += block.len; + _ = self.freeBlocks.swapRemove(i); + break; + } + } + for (self.freeBlocks.items) |*block| { + if (allocation.start == block.start + block.len) { + block.len += allocation.len; + return; + } + } + self.freeBlocks.append(allocation); + } + + pub fn free(self: *Self, allocation: SubAllocation) void { + if (allocation.len == 0) return; + self.fencedFreeLists[self.activeFence].append(allocation); + } + + /// Must unmap after use! + pub fn allocateAndMapRange(self: *Self, len: usize, allocation: *SubAllocation) []Entry { + self.free(allocation.*); + if (len == 0) { + allocation.len = 0; + return &.{}; + } + allocation.* = self.rawAlloc(@intCast(len)); + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.ssbo.bufferID); + const ptr: [*]Entry = @ptrCast(@alignCast(c.glMapBufferRange( + c.GL_SHADER_STORAGE_BUFFER, + @as(c.GLintptr, allocation.start)*@sizeOf(Entry), + @as(c.GLsizeiptr, allocation.len)*@sizeOf(Entry), + c.GL_MAP_WRITE_BIT | c.GL_MAP_INVALIDATE_RANGE_BIT, + ))); + return ptr[0..len]; + } + + pub fn unmapRange(self: *Self, range: []Entry) void { + if (range.len == 0) return; + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.ssbo.bufferID); + std.debug.assert(c.glUnmapBuffer(c.GL_SHADER_STORAGE_BUFFER) == c.GL_TRUE); + } + + pub fn uploadData(self: *Self, data: []const Entry, allocation: *SubAllocation) void { + self.free(allocation.*); + if (data.len == 0) { + allocation.len = 0; + return; + } + allocation.* = self.rawAlloc(@intCast(data.len)); + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.ssbo.bufferID); + const ptr: [*]Entry = @ptrCast(@alignCast(c.glMapBufferRange( + c.GL_SHADER_STORAGE_BUFFER, + @as(c.GLintptr, allocation.start)*@sizeOf(Entry), + @as(c.GLsizeiptr, allocation.len)*@sizeOf(Entry), + c.GL_MAP_WRITE_BIT | c.GL_MAP_INVALIDATE_RANGE_BIT, + ))); + @memcpy(ptr, data); + std.debug.assert(c.glUnmapBuffer(c.GL_SHADER_STORAGE_BUFFER) == c.GL_TRUE); + } + }; } pub const FrameBuffer = struct { // MARK: FrameBuffer - frameBuffer: c_uint, - texture: c_uint, - hasDepthTexture: bool, - depthTexture: c_uint, - - pub fn init(self: *FrameBuffer, hasDepthTexture: bool, textureFilter: c_int, textureWrap: c_int) void { - self.* = FrameBuffer{ - .frameBuffer = undefined, - .texture = undefined, - .depthTexture = undefined, - .hasDepthTexture = hasDepthTexture, - }; - c.glGenFramebuffers(1, &self.frameBuffer); - c.glBindFramebuffer(c.GL_FRAMEBUFFER, self.frameBuffer); - if (hasDepthTexture) { - c.glGenTextures(1, &self.depthTexture); - c.glBindTexture(c.GL_TEXTURE_2D, self.depthTexture); - c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MIN_FILTER, textureFilter); - c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MAG_FILTER, textureFilter); - c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, textureWrap); - c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, textureWrap); - c.glFramebufferTexture2D(c.GL_FRAMEBUFFER, c.GL_DEPTH_ATTACHMENT, c.GL_TEXTURE_2D, self.depthTexture, 0); - } - c.glGenTextures(1, &self.texture); - c.glBindTexture(c.GL_TEXTURE_2D, self.texture); - c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MIN_FILTER, textureFilter); - c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MAG_FILTER, textureFilter); - c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, textureWrap); - c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, textureWrap); - c.glFramebufferTexture2D(c.GL_FRAMEBUFFER, c.GL_COLOR_ATTACHMENT0, c.GL_TEXTURE_2D, self.texture, 0); - - c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0); - } - - pub fn deinit(self: *FrameBuffer) void { - c.glDeleteFramebuffers(1, &self.frameBuffer); - if (self.hasDepthTexture) { - c.glDeleteRenderbuffers(1, &self.depthTexture); - } - c.glDeleteTextures(1, &self.texture); - } - - pub fn updateSize(self: *FrameBuffer, _width: u31, _height: u31, internalFormat: c_int) void { - const width = @max(_width, 1); - const height = @max(_height, 1); - c.glBindFramebuffer(c.GL_FRAMEBUFFER, self.frameBuffer); - if (self.hasDepthTexture) { - c.glBindTexture(c.GL_TEXTURE_2D, self.depthTexture); - c.glTexImage2D(c.GL_TEXTURE_2D, 0, c.GL_DEPTH_COMPONENT32F, width, height, 0, c.GL_DEPTH_COMPONENT, c.GL_FLOAT, null); - } - - c.glBindTexture(c.GL_TEXTURE_2D, self.texture); - c.glTexImage2D(c.GL_TEXTURE_2D, 0, internalFormat, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); - } - - pub fn clear(_: FrameBuffer, clearColor: Vec4f) void { - c.glDepthFunc(c.GL_LESS); - c.glDepthMask(c.GL_TRUE); - c.glDisable(c.GL_SCISSOR_TEST); - c.glClearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]); - c.glClear(c.GL_COLOR_BUFFER_BIT | c.GL_DEPTH_BUFFER_BIT); - } - - pub fn validate(self: *const FrameBuffer) bool { - c.glBindFramebuffer(c.GL_FRAMEBUFFER, self.frameBuffer); - defer c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0); - if (c.glCheckFramebufferStatus(c.GL_FRAMEBUFFER) != c.GL_FRAMEBUFFER_COMPLETE) { - std.log.err("Frame Buffer Object error: {}", .{c.glCheckFramebufferStatus(c.GL_FRAMEBUFFER)}); - return false; - } - return true; - } - - pub fn bindTexture(self: *const FrameBuffer, target: c_uint) void { - c.glActiveTexture(target); - c.glBindTexture(c.GL_TEXTURE_2D, self.texture); - } - - pub fn bindDepthTexture(self: *const FrameBuffer, target: c_uint) void { - std.debug.assert(self.hasDepthTexture); - c.glActiveTexture(target); - c.glBindTexture(c.GL_TEXTURE_2D, self.depthTexture); - } - - pub fn bind(self: *const FrameBuffer) void { - c.glBindFramebuffer(c.GL_FRAMEBUFFER, self.frameBuffer); - } - - pub fn unbind(_: *const FrameBuffer) void { - c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0); - } + frameBuffer: c_uint, + texture: c_uint, + hasDepthTexture: bool, + depthTexture: c_uint, + + pub fn init(self: *FrameBuffer, hasDepthTexture: bool, textureFilter: c_int, textureWrap: c_int) void { + self.* = FrameBuffer{ + .frameBuffer = undefined, + .texture = undefined, + .depthTexture = undefined, + .hasDepthTexture = hasDepthTexture, + }; + c.glGenFramebuffers(1, &self.frameBuffer); + c.glBindFramebuffer(c.GL_FRAMEBUFFER, self.frameBuffer); + if (hasDepthTexture) { + c.glGenTextures(1, &self.depthTexture); + c.glBindTexture(c.GL_TEXTURE_2D, self.depthTexture); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MIN_FILTER, textureFilter); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MAG_FILTER, textureFilter); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, textureWrap); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, textureWrap); + c.glFramebufferTexture2D(c.GL_FRAMEBUFFER, c.GL_DEPTH_ATTACHMENT, c.GL_TEXTURE_2D, self.depthTexture, 0); + } + c.glGenTextures(1, &self.texture); + c.glBindTexture(c.GL_TEXTURE_2D, self.texture); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MIN_FILTER, textureFilter); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MAG_FILTER, textureFilter); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, textureWrap); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, textureWrap); + c.glFramebufferTexture2D(c.GL_FRAMEBUFFER, c.GL_COLOR_ATTACHMENT0, c.GL_TEXTURE_2D, self.texture, 0); + + c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0); + } + + pub fn deinit(self: *FrameBuffer) void { + c.glDeleteFramebuffers(1, &self.frameBuffer); + if (self.hasDepthTexture) { + c.glDeleteRenderbuffers(1, &self.depthTexture); + } + c.glDeleteTextures(1, &self.texture); + } + + pub fn updateSize(self: *FrameBuffer, _width: u31, _height: u31, internalFormat: c_int) void { + const width = @max(_width, 1); + const height = @max(_height, 1); + c.glBindFramebuffer(c.GL_FRAMEBUFFER, self.frameBuffer); + if (self.hasDepthTexture) { + c.glBindTexture(c.GL_TEXTURE_2D, self.depthTexture); + c.glTexImage2D(c.GL_TEXTURE_2D, 0, c.GL_DEPTH_COMPONENT32F, width, height, 0, c.GL_DEPTH_COMPONENT, c.GL_FLOAT, null); + } + + c.glBindTexture(c.GL_TEXTURE_2D, self.texture); + c.glTexImage2D(c.GL_TEXTURE_2D, 0, internalFormat, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); + } + + pub fn clear(_: FrameBuffer, clearColor: Vec4f) void { + c.glDepthFunc(c.GL_LESS); + c.glDepthMask(c.GL_TRUE); + c.glDisable(c.GL_SCISSOR_TEST); + c.glClearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]); + c.glClear(c.GL_COLOR_BUFFER_BIT | c.GL_DEPTH_BUFFER_BIT); + } + + pub fn validate(self: *const FrameBuffer) bool { + c.glBindFramebuffer(c.GL_FRAMEBUFFER, self.frameBuffer); + defer c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0); + if (c.glCheckFramebufferStatus(c.GL_FRAMEBUFFER) != c.GL_FRAMEBUFFER_COMPLETE) { + std.log.err("Frame Buffer Object error: {}", .{c.glCheckFramebufferStatus(c.GL_FRAMEBUFFER)}); + return false; + } + return true; + } + + pub fn bindTexture(self: *const FrameBuffer, target: c_uint) void { + c.glActiveTexture(target); + c.glBindTexture(c.GL_TEXTURE_2D, self.texture); + } + + pub fn bindDepthTexture(self: *const FrameBuffer, target: c_uint) void { + std.debug.assert(self.hasDepthTexture); + c.glActiveTexture(target); + c.glBindTexture(c.GL_TEXTURE_2D, self.depthTexture); + } + + pub fn bind(self: *const FrameBuffer) void { + c.glBindFramebuffer(c.GL_FRAMEBUFFER, self.frameBuffer); + } + + pub fn unbind(_: *const FrameBuffer) void { + c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0); + } }; pub const TextureArray = struct { // MARK: TextureArray - textureID: c_uint, - - pub fn init() TextureArray { - var self: TextureArray = undefined; - c.glGenTextures(1, &self.textureID); - return self; - } - - pub fn deinit(self: TextureArray) void { - c.glDeleteTextures(1, &self.textureID); - } - - pub fn bind(self: TextureArray) void { - c.glBindTexture(c.GL_TEXTURE_2D_ARRAY, self.textureID); - } - - fn lodColorInterpolation(colors: [4]Color, alphaCorrection: bool) Color { - var r: [4]f32 = undefined; - var g: [4]f32 = undefined; - var b: [4]f32 = undefined; - var a: [4]f32 = undefined; - for (0..4) |i| { - r[i] = @floatFromInt(colors[i].r); - g[i] = @floatFromInt(colors[i].g); - b[i] = @floatFromInt(colors[i].b); - a[i] = @floatFromInt(colors[i].a); - } - // Use gamma corrected average(https://stackoverflow.com/a/832314/13082649): - var aSum: f32 = 0; - var rSum: f32 = 0; - var gSum: f32 = 0; - var bSum: f32 = 0; - for (0..4) |i| { - const w = if (alphaCorrection) a[i] * a[i] else 1; - aSum += a[i] * a[i]; - rSum += w * r[i] * r[i]; - gSum += w * g[i] * g[i]; - bSum += w * b[i] * b[i]; - } - aSum = @sqrt(aSum) / 2; - rSum = @sqrt(rSum) / 2; - gSum = @sqrt(gSum) / 2; - bSum = @sqrt(bSum) / 2; - if (alphaCorrection and aSum != 0) { - rSum /= aSum; - gSum /= aSum; - bSum /= aSum; - } - return Color{ .r = @trunc(rSum), .g = @trunc(gSum), .b = @trunc(bSum), .a = @trunc(aSum) }; - } - - /// (Re-)Generates the GPU buffer. - pub fn generate(self: TextureArray, images: []Image, mipmapping: bool, alphaCorrectMipmapping: bool) void { - var maxWidth: u31 = 1; - var maxHeight: u31 = 1; - for (images) |image| { - maxWidth = @max(maxWidth, image.width); - maxHeight = @max(maxHeight, image.height); - } - // Make sure the width and height use a power of 2: - if (maxWidth - 1 & maxWidth != 0) { - maxWidth = @as(u31, 2) << std.math.log2_int(u31, maxWidth); - } - if (maxHeight - 1 & maxHeight != 0) { - maxHeight = @as(u31, 2) << std.math.log2_int(u31, maxHeight); - } - - std.log.debug("Creating Texture Array of size {}×{} with {} layers.", .{ maxWidth, maxHeight, images.len }); - - self.bind(); - - const maxLOD = if (mipmapping) 1 + std.math.log2_int(u31, @max(maxWidth, maxHeight)) else 1; - for (0..maxLOD) |i| { - c.glTexImage3D(c.GL_TEXTURE_2D_ARRAY, @intCast(i), c.GL_RGBA8, @max(1, maxWidth >> @intCast(i)), @max(1, maxHeight >> @intCast(i)), @intCast(images.len), 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); - } - const arena = main.stackAllocator.createArena(); - defer main.stackAllocator.destroyArena(arena); - const lodBuffer: [][]Color = arena.alloc([]Color, maxLOD); - for (lodBuffer, 0..) |*buffer, i| { - buffer.* = arena.alloc(Color, @max(1, maxWidth >> @intCast(i)) * @max(1, maxHeight >> @intCast(i))); - } - - for (images, 0..) |image, i| { - // Fill the buffer using nearest sampling. Probably not the best solutions for all textures, but that's what happens when someone doesn't use power of 2 textures... - for (0..maxWidth) |x| { - for (0..maxHeight) |y| { - const index = x + y * maxWidth; - const imageIndex = (x * image.width) / maxWidth + image.width * ((y * image.height) / maxHeight); - lodBuffer[0][index] = image.imageData[imageIndex]; - } - } - - // Calculate the mipmap levels: - for (1..lodBuffer.len) |_lod| { - const lod: u5 = @intCast(_lod); - const curWidth = @max(1, maxWidth >> lod); - const curHeight = @max(1, maxHeight >> lod); - const nextWidth = @max(1, maxWidth >> (lod - 1)); - const nextHeight = @max(1, maxHeight >> (lod - 1)); - for (0..curWidth) |x| { - for (0..curHeight) |y| { - const index = x + y * curWidth; - const colors = [4]Color{ - lodBuffer[lod - 1][@min(2 * x, nextWidth - 1) + @min(2 * y, nextHeight - 1) * nextWidth], - lodBuffer[lod - 1][@min(2 * x, nextWidth - 1) + @min(2 * y + 1, nextHeight - 1) * nextWidth], - lodBuffer[lod - 1][@min(2 * x + 1, nextWidth - 1) + @min(2 * y, nextHeight - 1) * nextWidth], - lodBuffer[lod - 1][@min(2 * x + 1, nextWidth - 1) + @min(2 * y + 1, nextHeight - 1) * nextWidth], - }; - lodBuffer[lod][index] = lodColorInterpolation(colors, alphaCorrectMipmapping); - } - } - } - // Give the correct color to alpha 0 pixels, to avoid dark pixels: - for (1..lodBuffer.len) |_lod| { - const lod: u5 = @intCast(lodBuffer.len - 1 - _lod); - const curWidth = @max(1, maxWidth >> lod); - const curHeight = @max(1, maxHeight >> lod); - for (0..curWidth) |x| { - for (0..curHeight) |y| { - const index = x + y * curWidth; - const index2 = x / 2 + y / 2 * curWidth / 2; - if (lodBuffer[lod][index].a == 0) { - lodBuffer[lod][index].r = lodBuffer[lod + 1][index2].r; - lodBuffer[lod][index].g = lodBuffer[lod + 1][index2].g; - lodBuffer[lod][index].b = lodBuffer[lod + 1][index2].b; - } - } - } - } - // Upload: - for (0..lodBuffer.len) |_lod| { - const lod: u5 = @intCast(lodBuffer.len - 1 - _lod); - const curWidth = @max(1, maxWidth >> lod); - const curHeight = @max(1, maxHeight >> lod); - c.glTexSubImage3D(c.GL_TEXTURE_2D_ARRAY, lod, 0, 0, @intCast(i), curWidth, curHeight, 1, c.GL_RGBA, c.GL_UNSIGNED_BYTE, lodBuffer[lod].ptr); - } - } - c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MAX_LOD, maxLOD); - c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MIN_FILTER, c.GL_NEAREST_MIPMAP_LINEAR); - c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MAG_FILTER, c.GL_NEAREST); - c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT); - c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT); - } + textureID: c_uint, + + pub fn init() TextureArray { + var self: TextureArray = undefined; + c.glGenTextures(1, &self.textureID); + return self; + } + + pub fn deinit(self: TextureArray) void { + c.glDeleteTextures(1, &self.textureID); + } + + pub fn bind(self: TextureArray) void { + c.glBindTexture(c.GL_TEXTURE_2D_ARRAY, self.textureID); + } + + fn lodColorInterpolation(colors: [4]Color, alphaCorrection: bool) Color { + var r: [4]f32 = undefined; + var g: [4]f32 = undefined; + var b: [4]f32 = undefined; + var a: [4]f32 = undefined; + for (0..4) |i| { + r[i] = @floatFromInt(colors[i].r); + g[i] = @floatFromInt(colors[i].g); + b[i] = @floatFromInt(colors[i].b); + a[i] = @floatFromInt(colors[i].a); + } + // Use gamma corrected average(https://stackoverflow.com/a/832314/13082649): + var aSum: f32 = 0; + var rSum: f32 = 0; + var gSum: f32 = 0; + var bSum: f32 = 0; + for (0..4) |i| { + const w = if (alphaCorrection) a[i]*a[i] else 1; + aSum += a[i]*a[i]; + rSum += w*r[i]*r[i]; + gSum += w*g[i]*g[i]; + bSum += w*b[i]*b[i]; + } + aSum = @sqrt(aSum)/2; + rSum = @sqrt(rSum)/2; + gSum = @sqrt(gSum)/2; + bSum = @sqrt(bSum)/2; + if (alphaCorrection and aSum != 0) { + rSum /= aSum; + gSum /= aSum; + bSum /= aSum; + } + return Color{.r = @trunc(rSum), .g = @trunc(gSum), .b = @trunc(bSum), .a = @trunc(aSum)}; + } + + /// (Re-)Generates the GPU buffer. + pub fn generate(self: TextureArray, images: []Image, mipmapping: bool, alphaCorrectMipmapping: bool) void { + var maxWidth: u31 = 1; + var maxHeight: u31 = 1; + for (images) |image| { + maxWidth = @max(maxWidth, image.width); + maxHeight = @max(maxHeight, image.height); + } + // Make sure the width and height use a power of 2: + if (maxWidth - 1 & maxWidth != 0) { + maxWidth = @as(u31, 2) << std.math.log2_int(u31, maxWidth); + } + if (maxHeight - 1 & maxHeight != 0) { + maxHeight = @as(u31, 2) << std.math.log2_int(u31, maxHeight); + } + + std.log.debug("Creating Texture Array of size {}×{} with {} layers.", .{maxWidth, maxHeight, images.len}); + + self.bind(); + + const maxLOD = if (mipmapping) 1 + std.math.log2_int(u31, @max(maxWidth, maxHeight)) else 1; + for (0..maxLOD) |i| { + c.glTexImage3D(c.GL_TEXTURE_2D_ARRAY, @intCast(i), c.GL_RGBA8, @max(1, maxWidth >> @intCast(i)), @max(1, maxHeight >> @intCast(i)), @intCast(images.len), 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); + } + const arena = main.stackAllocator.createArena(); + defer main.stackAllocator.destroyArena(arena); + const lodBuffer: [][]Color = arena.alloc([]Color, maxLOD); + for (lodBuffer, 0..) |*buffer, i| { + buffer.* = arena.alloc(Color, @max(1, maxWidth >> @intCast(i))*@max(1, maxHeight >> @intCast(i))); + } + + for (images, 0..) |image, i| { + // Fill the buffer using nearest sampling. Probably not the best solutions for all textures, but that's what happens when someone doesn't use power of 2 textures... + for (0..maxWidth) |x| { + for (0..maxHeight) |y| { + const index = x + y*maxWidth; + const imageIndex = (x*image.width)/maxWidth + image.width*((y*image.height)/maxHeight); + lodBuffer[0][index] = image.imageData[imageIndex]; + } + } + + // Calculate the mipmap levels: + for (1..lodBuffer.len) |_lod| { + const lod: u5 = @intCast(_lod); + const curWidth = @max(1, maxWidth >> lod); + const curHeight = @max(1, maxHeight >> lod); + const nextWidth = @max(1, maxWidth >> (lod - 1)); + const nextHeight = @max(1, maxHeight >> (lod - 1)); + for (0..curWidth) |x| { + for (0..curHeight) |y| { + const index = x + y*curWidth; + const colors = [4]Color{ + lodBuffer[lod - 1][@min(2*x, nextWidth - 1) + @min(2*y, nextHeight - 1)*nextWidth], + lodBuffer[lod - 1][@min(2*x, nextWidth - 1) + @min(2*y + 1, nextHeight - 1)*nextWidth], + lodBuffer[lod - 1][@min(2*x + 1, nextWidth - 1) + @min(2*y, nextHeight - 1)*nextWidth], + lodBuffer[lod - 1][@min(2*x + 1, nextWidth - 1) + @min(2*y + 1, nextHeight - 1)*nextWidth], + }; + lodBuffer[lod][index] = lodColorInterpolation(colors, alphaCorrectMipmapping); + } + } + } + // Give the correct color to alpha 0 pixels, to avoid dark pixels: + for (1..lodBuffer.len) |_lod| { + const lod: u5 = @intCast(lodBuffer.len - 1 - _lod); + const curWidth = @max(1, maxWidth >> lod); + const curHeight = @max(1, maxHeight >> lod); + for (0..curWidth) |x| { + for (0..curHeight) |y| { + const index = x + y*curWidth; + const index2 = x/2 + y/2*curWidth/2; + if (lodBuffer[lod][index].a == 0) { + lodBuffer[lod][index].r = lodBuffer[lod + 1][index2].r; + lodBuffer[lod][index].g = lodBuffer[lod + 1][index2].g; + lodBuffer[lod][index].b = lodBuffer[lod + 1][index2].b; + } + } + } + } + // Upload: + for (0..lodBuffer.len) |_lod| { + const lod: u5 = @intCast(lodBuffer.len - 1 - _lod); + const curWidth = @max(1, maxWidth >> lod); + const curHeight = @max(1, maxHeight >> lod); + c.glTexSubImage3D(c.GL_TEXTURE_2D_ARRAY, lod, 0, 0, @intCast(i), curWidth, curHeight, 1, c.GL_RGBA, c.GL_UNSIGNED_BYTE, lodBuffer[lod].ptr); + } + } + c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MAX_LOD, maxLOD); + c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MIN_FILTER, c.GL_NEAREST_MIPMAP_LINEAR); + c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MAG_FILTER, c.GL_NEAREST); + c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT); + c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT); + } }; pub const Texture = struct { // MARK: Texture - textureID: c_uint, - - pub fn init() Texture { - var self: Texture = undefined; - c.glGenTextures(1, &self.textureID); - return self; - } - - pub fn initFromFile(path: []const u8) Texture { - const self = Texture.init(); - const image = Image.readFromFile(main.stackAllocator, path) catch |err| blk: { - std.log.err("Couldn't read image from {s}: {s}", .{ path, @errorName(err) }); - break :blk Image.defaultImage; - }; - defer image.deinit(main.stackAllocator); - self.generate(image); - return self; - } - - pub fn initFromMipmapFiles(pathPrefix: []const u8, largestSize: u31, lodBias: f32) Texture { - const self = Texture.init(); - self.bind(); - - const maxLod = std.math.log2_int(u31, largestSize); - - var curSize: u31 = largestSize; - while (curSize != 0) : (curSize /= 2) { - c.glTexImage2D(c.GL_TEXTURE_2D, maxLod - std.math.log2_int(u31, curSize), c.GL_RGBA8, curSize, curSize, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); - } - - curSize = largestSize; - while (curSize != 0) : (curSize /= 2) { - const path = std.fmt.allocPrint(main.stackAllocator.allocator, "{s}{}.png", .{ pathPrefix, curSize }) catch unreachable; - defer main.stackAllocator.free(path); - const image = Image.readFromFile(main.stackAllocator, path) catch |err| blk: { - std.log.err("Couldn't read image from {s}: {s}", .{ path, @errorName(err) }); - break :blk Image.defaultImage; - }; - defer image.deinit(main.stackAllocator); - c.glTexSubImage2D(c.GL_TEXTURE_2D, maxLod - std.math.log2_int(u31, curSize), 0, 0, curSize, curSize, c.GL_RGBA, c.GL_UNSIGNED_BYTE, image.imageData.ptr); - } - - c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MAX_LOD, maxLod); - c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MIN_FILTER, c.GL_NEAREST_MIPMAP_LINEAR); - c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MAG_FILTER, c.GL_NEAREST); - c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT); - c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT); - c.glTexParameterf(c.GL_TEXTURE_2D, c.GL_TEXTURE_LOD_BIAS, lodBias); - return self; - } - - pub fn deinit(self: Texture) void { - c.glDeleteTextures(1, &self.textureID); - } - - pub fn bindTo(self: Texture, binding: u5) void { - c.glActiveTexture(@intCast(c.GL_TEXTURE0 + binding)); - c.glBindTexture(c.GL_TEXTURE_2D, self.textureID); - } - - pub fn bind(self: Texture) void { - c.glBindTexture(c.GL_TEXTURE_2D, self.textureID); - } - - /// (Re-)Generates the GPU buffer. - pub fn generate(self: Texture, image: Image) void { - self.bind(); - - c.glTexImage2D(c.GL_TEXTURE_2D, 0, c.GL_RGBA8, image.width, image.height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, image.imageData.ptr); - 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.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT); - c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT); - } - - pub fn render(self: Texture, pos: Vec2f, dim: Vec2f) void { - self.bindTo(0); - draw.boundImage(pos, dim); - } - - pub fn size(self: Texture) Vec2i { - self.bind(); - var result: Vec2i = undefined; - c.glGetTexLevelParameteriv(c.GL_TEXTURE_2D, 0, c.GL_TEXTURE_WIDTH, &result[0]); - c.glGetTexLevelParameteriv(c.GL_TEXTURE_2D, 0, c.GL_TEXTURE_HEIGHT, &result[1]); - return result; - } + textureID: c_uint, + + pub fn init() Texture { + var self: Texture = undefined; + c.glGenTextures(1, &self.textureID); + return self; + } + + pub fn initFromFile(path: []const u8) Texture { + const self = Texture.init(); + const image = Image.readFromFile(main.stackAllocator, path) catch |err| blk: { + std.log.err("Couldn't read image from {s}: {s}", .{path, @errorName(err)}); + break :blk Image.defaultImage; + }; + defer image.deinit(main.stackAllocator); + self.generate(image); + return self; + } + + pub fn initFromMipmapFiles(pathPrefix: []const u8, largestSize: u31, lodBias: f32) Texture { + const self = Texture.init(); + self.bind(); + + const maxLod = std.math.log2_int(u31, largestSize); + + var curSize: u31 = largestSize; + while (curSize != 0) : (curSize /= 2) { + c.glTexImage2D(c.GL_TEXTURE_2D, maxLod - std.math.log2_int(u31, curSize), c.GL_RGBA8, curSize, curSize, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); + } + + curSize = largestSize; + while (curSize != 0) : (curSize /= 2) { + const path = std.fmt.allocPrint(main.stackAllocator.allocator, "{s}{}.png", .{pathPrefix, curSize}) catch unreachable; + defer main.stackAllocator.free(path); + const image = Image.readFromFile(main.stackAllocator, path) catch |err| blk: { + std.log.err("Couldn't read image from {s}: {s}", .{path, @errorName(err)}); + break :blk Image.defaultImage; + }; + defer image.deinit(main.stackAllocator); + c.glTexSubImage2D(c.GL_TEXTURE_2D, maxLod - std.math.log2_int(u31, curSize), 0, 0, curSize, curSize, c.GL_RGBA, c.GL_UNSIGNED_BYTE, image.imageData.ptr); + } + + c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MAX_LOD, maxLod); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MIN_FILTER, c.GL_NEAREST_MIPMAP_LINEAR); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MAG_FILTER, c.GL_NEAREST); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT); + c.glTexParameterf(c.GL_TEXTURE_2D, c.GL_TEXTURE_LOD_BIAS, lodBias); + return self; + } + + pub fn deinit(self: Texture) void { + c.glDeleteTextures(1, &self.textureID); + } + + pub fn bindTo(self: Texture, binding: u5) void { + c.glActiveTexture(@intCast(c.GL_TEXTURE0 + binding)); + c.glBindTexture(c.GL_TEXTURE_2D, self.textureID); + } + + pub fn bind(self: Texture) void { + c.glBindTexture(c.GL_TEXTURE_2D, self.textureID); + } + + /// (Re-)Generates the GPU buffer. + pub fn generate(self: Texture, image: Image) void { + self.bind(); + + c.glTexImage2D(c.GL_TEXTURE_2D, 0, c.GL_RGBA8, image.width, image.height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, image.imageData.ptr); + 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.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT); + } + + pub fn render(self: Texture, pos: Vec2f, dim: Vec2f) void { + self.bindTo(0); + draw.boundImage(pos, dim); + } + + pub fn size(self: Texture) Vec2i { + self.bind(); + var result: Vec2i = undefined; + c.glGetTexLevelParameteriv(c.GL_TEXTURE_2D, 0, c.GL_TEXTURE_WIDTH, &result[0]); + c.glGetTexLevelParameteriv(c.GL_TEXTURE_2D, 0, c.GL_TEXTURE_HEIGHT, &result[1]); + return result; + } }; pub const CubeMapTexture = struct { // MARK: CubeMapTexture - textureID: c_uint, - - pub fn init() CubeMapTexture { - var self: CubeMapTexture = undefined; - c.glGenTextures(1, &self.textureID); - return self; - } - - pub fn deinit(self: CubeMapTexture) void { - c.glDeleteTextures(1, &self.textureID); - } - - pub fn bindTo(self: CubeMapTexture, binding: u5) void { - c.glActiveTexture(@intCast(c.GL_TEXTURE0 + binding)); - c.glBindTexture(c.GL_TEXTURE_CUBE_MAP, self.textureID); - } - - pub fn bind(self: CubeMapTexture) void { - c.glBindTexture(c.GL_TEXTURE_CUBE_MAP, self.textureID); - } - - /// (Re-)Generates the GPU buffer. - pub fn generate(self: CubeMapTexture, width: u31, height: u31) void { - self.bind(); - - c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); - c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); - c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_POSITIVE_Y, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); - c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); - c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_POSITIVE_Z, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); - c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); - c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_MIN_FILTER, c.GL_LINEAR); - c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_MAG_FILTER, c.GL_LINEAR); - c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_WRAP_S, c.GL_CLAMP_TO_EDGE); - c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_WRAP_T, c.GL_CLAMP_TO_EDGE); - c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_WRAP_R, c.GL_CLAMP_TO_EDGE); - c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_BASE_LEVEL, 0); - c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_MAX_LEVEL, 0); - } - - pub fn faceNormal(face: usize) Vec3f { - const normals = [_]Vec3f{ - .{ 1, 0, 0 }, // +x - .{ -1, 0, 0 }, // -x - .{ 0, 1, 0 }, // +y - .{ 0, -1, 0 }, // -y - .{ 0, 0, 1 }, // +z - .{ 0, 0, -1 }, // -z - }; - return normals[face]; - } - - pub fn faceUp(face: usize) Vec3f { - const ups = [_]Vec3f{ - .{ 0, -1, 0 }, // +x - .{ 0, -1, 0 }, // -x - .{ 0, 0, 1 }, // +y - .{ 0, 0, -1 }, // -y - .{ 0, -1, 0 }, // +z - .{ 0, -1, 0 }, // -z - }; - return ups[face]; - } - - pub fn faceRight(face: usize) Vec3f { - comptime var rights: [6]Vec3f = undefined; - inline for (0..6) |i| { - rights[i] = comptime vec.cross(faceNormal(i), faceUp(i)); - } - return rights[face]; - } - - pub fn bindToFramebuffer(self: CubeMapTexture, fb: FrameBuffer, face: c_uint) void { - fb.bind(); - c.glFramebufferTexture2D(c.GL_FRAMEBUFFER, c.GL_COLOR_ATTACHMENT0, @as(c_uint, c.GL_TEXTURE_CUBE_MAP_POSITIVE_X) + face, self.textureID, 0); - } + textureID: c_uint, + + pub fn init() CubeMapTexture { + var self: CubeMapTexture = undefined; + c.glGenTextures(1, &self.textureID); + return self; + } + + pub fn deinit(self: CubeMapTexture) void { + c.glDeleteTextures(1, &self.textureID); + } + + pub fn bindTo(self: CubeMapTexture, binding: u5) void { + c.glActiveTexture(@intCast(c.GL_TEXTURE0 + binding)); + c.glBindTexture(c.GL_TEXTURE_CUBE_MAP, self.textureID); + } + + pub fn bind(self: CubeMapTexture) void { + c.glBindTexture(c.GL_TEXTURE_CUBE_MAP, self.textureID); + } + + /// (Re-)Generates the GPU buffer. + pub fn generate(self: CubeMapTexture, width: u31, height: u31) void { + self.bind(); + + c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); + c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); + c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_POSITIVE_Y, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); + c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); + c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_POSITIVE_Z, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); + c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null); + c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_MIN_FILTER, c.GL_LINEAR); + c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_MAG_FILTER, c.GL_LINEAR); + c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_WRAP_S, c.GL_CLAMP_TO_EDGE); + c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_WRAP_T, c.GL_CLAMP_TO_EDGE); + c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_WRAP_R, c.GL_CLAMP_TO_EDGE); + c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_BASE_LEVEL, 0); + c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_MAX_LEVEL, 0); + } + + pub fn faceNormal(face: usize) Vec3f { + const normals = [_]Vec3f{ + .{1, 0, 0}, // +x + .{-1, 0, 0}, // -x + .{0, 1, 0}, // +y + .{0, -1, 0}, // -y + .{0, 0, 1}, // +z + .{0, 0, -1}, // -z + }; + return normals[face]; + } + + pub fn faceUp(face: usize) Vec3f { + const ups = [_]Vec3f{ + .{0, -1, 0}, // +x + .{0, -1, 0}, // -x + .{0, 0, 1}, // +y + .{0, 0, -1}, // -y + .{0, -1, 0}, // +z + .{0, -1, 0}, // -z + }; + return ups[face]; + } + + pub fn faceRight(face: usize) Vec3f { + comptime var rights: [6]Vec3f = undefined; + inline for (0..6) |i| { + rights[i] = comptime vec.cross(faceNormal(i), faceUp(i)); + } + return rights[face]; + } + + pub fn bindToFramebuffer(self: CubeMapTexture, fb: FrameBuffer, face: c_uint) void { + fb.bind(); + c.glFramebufferTexture2D(c.GL_FRAMEBUFFER, c.GL_COLOR_ATTACHMENT0, @as(c_uint, c.GL_TEXTURE_CUBE_MAP_POSITIVE_X) + face, self.textureID, 0); + } }; pub const Color = extern struct { // MARK: Color - r: u8, - g: u8, - b: u8, - a: u8, - - pub fn toARBG(self: Color) u32 { - return @as(u32, self.a) << 24 | @as(u32, self.r) << 16 | @as(u32, self.g) << 8 | @as(u32, self.b); - } + r: u8, + g: u8, + b: u8, + a: u8, + + pub fn toARBG(self: Color) u32 { + return @as(u32, self.a) << 24 | @as(u32, self.r) << 16 | @as(u32, self.g) << 8 | @as(u32, self.b); + } }; pub const Image = struct { // MARK: Image - var defaultImageData = [4]Color{ - Color{ .r = 0, .g = 0, .b = 0, .a = 255 }, - Color{ .r = 255, .g = 0, .b = 255, .a = 255 }, - Color{ .r = 255, .g = 0, .b = 255, .a = 255 }, - Color{ .r = 0, .g = 0, .b = 0, .a = 255 }, - }; - pub const defaultImage = Image{ - .width = 2, - .height = 2, - .imageData = &defaultImageData, - }; - var emptyImageData = [1]Color{ - Color{ .r = 0, .g = 0, .b = 0, .a = 0 }, - }; - pub const emptyImage = Image{ - .width = 1, - .height = 1, - .imageData = &emptyImageData, - }; - var whiteImageData = [1]Color{ - Color{ .r = 255, .g = 255, .b = 255, .a = 255 }, - }; - pub const whiteEmptyImage = Image{ - .width = 1, - .height = 1, - .imageData = &whiteImageData, - }; - width: u31, - height: u31, - imageData: []Color, - pub fn init(allocator: NeverFailingAllocator, width: u31, height: u31) Image { - return Image{ - .width = width, - .height = height, - .imageData = allocator.alloc(Color, width * height), - }; - } - pub fn deinit(self: Image, allocator: NeverFailingAllocator) void { - if (self.imageData.ptr == &defaultImageData or self.imageData.ptr == &emptyImageData or self.imageData.ptr == &whiteImageData) return; - allocator.free(self.imageData); - } - pub fn readFromFile(allocator: NeverFailingAllocator, path: []const u8) !Image { - var result: Image = undefined; - var channel: c_int = undefined; - const nullTerminatedPath = main.stackAllocator.dupeZ(u8, path); // TODO: Find a more zig-friendly image loading library. - errdefer main.stackAllocator.free(nullTerminatedPath); - c.stbi_set_flip_vertically_on_load(1); - const data = c.stbi_load(nullTerminatedPath.ptr, @ptrCast(&result.width), @ptrCast(&result.height), &channel, 4) orelse { - return error.FileNotFound; - }; - main.stackAllocator.free(nullTerminatedPath); - result.imageData = allocator.dupe(Color, @as([*]Color, @ptrCast(data))[0 .. result.width * result.height]); - c.stbi_image_free(data); - return result; - } - pub fn readUnflippedFromFile(allocator: NeverFailingAllocator, path: []const u8) !Image { - var result: Image = undefined; - var channel: c_int = undefined; - const nullTerminatedPath = main.stackAllocator.dupeZ(u8, path); // TODO: Find a more zig-friendly image loading library. - errdefer main.stackAllocator.free(nullTerminatedPath); - const data = c.stbi_load(nullTerminatedPath.ptr, @ptrCast(&result.width), @ptrCast(&result.height), &channel, 4) orelse { - return error.FileNotFound; - }; - main.stackAllocator.free(nullTerminatedPath); - result.imageData = allocator.dupe(Color, @as([*]Color, @ptrCast(data))[0 .. result.width * result.height]); - c.stbi_image_free(data); - return result; - } - pub fn exportToFile(self: Image, path: []const u8) !void { - const nullTerminated = main.stackAllocator.dupeZ(u8, path); - defer main.stackAllocator.free(nullTerminated); - _ = c.stbi_write_png(nullTerminated.ptr, self.width, self.height, 4, self.imageData.ptr, self.width * 4); // TODO: Handle the return type. - } - pub fn getRGB(self: Image, x: usize, y: usize) Color { - std.debug.assert(x < self.width); - std.debug.assert(y < self.height); - const index = x + y * self.width; - return self.imageData[index]; - } - pub fn setRGB(self: Image, x: usize, y: usize, rgb: Color) void { - std.debug.assert(x < self.width); - std.debug.assert(y < self.height); - const index = x + y * self.width; - self.imageData[index] = rgb; - } + var defaultImageData = [4]Color{ + Color{.r = 0, .g = 0, .b = 0, .a = 255}, + Color{.r = 255, .g = 0, .b = 255, .a = 255}, + Color{.r = 255, .g = 0, .b = 255, .a = 255}, + Color{.r = 0, .g = 0, .b = 0, .a = 255}, + }; + pub const defaultImage = Image{ + .width = 2, + .height = 2, + .imageData = &defaultImageData, + }; + var emptyImageData = [1]Color{ + Color{.r = 0, .g = 0, .b = 0, .a = 0}, + }; + pub const emptyImage = Image{ + .width = 1, + .height = 1, + .imageData = &emptyImageData, + }; + var whiteImageData = [1]Color{ + Color{.r = 255, .g = 255, .b = 255, .a = 255}, + }; + pub const whiteEmptyImage = Image{ + .width = 1, + .height = 1, + .imageData = &whiteImageData, + }; + width: u31, + height: u31, + imageData: []Color, + pub fn init(allocator: NeverFailingAllocator, width: u31, height: u31) Image { + return Image{ + .width = width, + .height = height, + .imageData = allocator.alloc(Color, width*height), + }; + } + pub fn deinit(self: Image, allocator: NeverFailingAllocator) void { + if (self.imageData.ptr == &defaultImageData or self.imageData.ptr == &emptyImageData or self.imageData.ptr == &whiteImageData) return; + allocator.free(self.imageData); + } + pub fn readFromFile(allocator: NeverFailingAllocator, path: []const u8) !Image { + var result: Image = undefined; + var channel: c_int = undefined; + const nullTerminatedPath = main.stackAllocator.dupeZ(u8, path); // TODO: Find a more zig-friendly image loading library. + errdefer main.stackAllocator.free(nullTerminatedPath); + c.stbi_set_flip_vertically_on_load(1); + const data = c.stbi_load(nullTerminatedPath.ptr, @ptrCast(&result.width), @ptrCast(&result.height), &channel, 4) orelse { + return error.FileNotFound; + }; + main.stackAllocator.free(nullTerminatedPath); + result.imageData = allocator.dupe(Color, @as([*]Color, @ptrCast(data))[0 .. result.width*result.height]); + c.stbi_image_free(data); + return result; + } + pub fn readUnflippedFromFile(allocator: NeverFailingAllocator, path: []const u8) !Image { + var result: Image = undefined; + var channel: c_int = undefined; + const nullTerminatedPath = main.stackAllocator.dupeZ(u8, path); // TODO: Find a more zig-friendly image loading library. + errdefer main.stackAllocator.free(nullTerminatedPath); + const data = c.stbi_load(nullTerminatedPath.ptr, @ptrCast(&result.width), @ptrCast(&result.height), &channel, 4) orelse { + return error.FileNotFound; + }; + main.stackAllocator.free(nullTerminatedPath); + result.imageData = allocator.dupe(Color, @as([*]Color, @ptrCast(data))[0 .. result.width*result.height]); + c.stbi_image_free(data); + return result; + } + pub fn exportToFile(self: Image, path: []const u8) !void { + const nullTerminated = main.stackAllocator.dupeZ(u8, path); + defer main.stackAllocator.free(nullTerminated); + _ = c.stbi_write_png(nullTerminated.ptr, self.width, self.height, 4, self.imageData.ptr, self.width*4); // TODO: Handle the return type. + } + pub fn getRGB(self: Image, x: usize, y: usize) Color { + std.debug.assert(x < self.width); + std.debug.assert(y < self.height); + const index = x + y*self.width; + return self.imageData[index]; + } + pub fn setRGB(self: Image, x: usize, y: usize, rgb: Color) void { + std.debug.assert(x < self.width); + std.debug.assert(y < self.height); + const index = x + y*self.width; + self.imageData[index] = rgb; + } }; pub const Fog = struct { // MARK: Fog - fogColor: Vec3f, - skyColor: Vec3f, - density: f32, - fogLower: f32, - fogHigher: f32, + fogColor: Vec3f, + skyColor: Vec3f, + density: f32, + fogLower: f32, + fogHigher: f32, }; const block_texture = struct { // MARK: block_texture - var uniforms: struct { - transparent: c_int, - } = undefined; - var pipeline: Pipeline = undefined; - var depthTexture: Texture = undefined; - const textureSize = 128; - - fn init() void { - pipeline = Pipeline.init( - "assets/cubyz/shaders/item_texture_post.vert", - "assets/cubyz/shaders/item_texture_post.frag", - "", - &uniforms, - VertexArray.EmptyVertex, - &.{}, - .{ .cullMode = .none }, - .{ .depthTest = false, .depthWrite = false }, - .{ .attachments = &.{.noBlending} }, - ); - depthTexture = .init(); - depthTexture.bind(); - var data: [128 * 128]f32 = undefined; - - const z: f32 = 134; - const near = main.renderer.zNear; - const far = main.renderer.zFar; - const depth = ((far + near) / (near - far) * -z + 2 * near * far / (near - far)) / z * 0.5 + 0.5; - - @memset(&data, depth); - c.glTexImage2D(c.GL_TEXTURE_2D, 0, c.GL_R32F, textureSize, textureSize, 0, c.GL_RED, c.GL_FLOAT, &data); - 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.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT); - c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT); - } - fn deinit() void { - pipeline.deinit(); - depthTexture.deinit(); - } + var uniforms: struct { + transparent: c_int, + } = undefined; + var pipeline: Pipeline = undefined; + var depthTexture: Texture = undefined; + const textureSize = 128; + + fn init() void { + pipeline = Pipeline.init( + "assets/cubyz/shaders/item_texture_post.vert", + "assets/cubyz/shaders/item_texture_post.frag", + "", + &uniforms, + VertexArray.EmptyVertex, + &.{}, + .{.cullMode = .none}, + .{.depthTest = false, .depthWrite = false}, + .{.attachments = &.{.noBlending}}, + ); + depthTexture = .init(); + depthTexture.bind(); + var data: [128*128]f32 = undefined; + + const z: f32 = 134; + const near = main.renderer.zNear; + const far = main.renderer.zFar; + const depth = ((far + near)/(near - far)*-z + 2*near*far/(near - far))/z*0.5 + 0.5; + + @memset(&data, depth); + c.glTexImage2D(c.GL_TEXTURE_2D, 0, c.GL_R32F, textureSize, textureSize, 0, c.GL_RED, c.GL_FLOAT, &data); + 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.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT); + c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT); + } + fn deinit() void { + pipeline.deinit(); + depthTexture.deinit(); + } }; pub fn generateBlockTexture(blockType: u16) Texture { - const block = main.blocks.Block{ .typ = blockType, .data = 0 }; // TODO: Use natural standard data. - const textureSize = block_texture.textureSize; - c.glViewport(0, 0, textureSize, textureSize); - - var frameBuffer: FrameBuffer = undefined; - - frameBuffer.init(false, c.GL_NEAREST, c.GL_REPEAT); - defer frameBuffer.deinit(); - frameBuffer.updateSize(textureSize, textureSize, c.GL_RGBA16F); - frameBuffer.bind(); - if (block.transparent()) { - frameBuffer.clear(.{ 0.683421, 0.6854237, 0.685426, 1 }); - } else { - frameBuffer.clear(.{ 0, 0, 0, 0 }); - } - - const projMatrix = Mat4f.perspective(0.013, 1, 64, 256); - const oldViewMatrix = main.game.camera.viewMatrix; - main.game.camera.viewMatrix = Mat4f.identity().mul(Mat4f.rotationX(std.math.pi / 4.0)).mul(Mat4f.rotationZ(1.0 * std.math.pi / 4.0)); - defer main.game.camera.viewMatrix = oldViewMatrix; - const uniforms = if (block.transparent()) &main.renderer.chunk_meshing.transparentUniforms else &main.renderer.chunk_meshing.uniforms; - - var faceData: main.ListUnmanaged(main.renderer.chunk_meshing.FaceData) = .{}; - defer faceData.deinit(main.stackAllocator); - const model = main.blocks.meshes.model(block).model(); - const pos: main.chunk.BlockPos = .fromCoords(1, 1, 1); - if (block.hasBackFace()) { - model.appendInternalQuadsToList(&faceData, main.stackAllocator, block, pos, true); - for (main.chunk.Neighbor.iterable) |neighbor| { - model.appendNeighborFacingQuadsToList(&faceData, main.stackAllocator, block, neighbor, pos, true); - } - } - model.appendInternalQuadsToList(&faceData, main.stackAllocator, block, pos, false); - for (main.chunk.Neighbor.iterable) |neighbor| { - model.appendNeighborFacingQuadsToList(&faceData, main.stackAllocator, block, neighbor, pos.neighbor(neighbor)[0], false); - } - - for (faceData.items) |*face| { - face.position.lightIndex = 0; - } - var allocation: SubAllocation = .{ .start = 0, .len = 0 }; - main.renderer.chunk_meshing.faceBuffers[0].uploadData(faceData.items, &allocation); - defer main.renderer.chunk_meshing.faceBuffers[0].free(allocation); - var lightAllocation: SubAllocation = .{ .start = 0, .len = 0 }; - main.renderer.chunk_meshing.lightBuffers[0].uploadData(&.{ 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff }, &lightAllocation); - defer main.renderer.chunk_meshing.lightBuffers[0].free(lightAllocation); - - { - const i = 4; // Easily switch between the 8 diagonal coordinates. - var x: f64 = -65.5 + 1.5; - var y: f64 = -65.5 + 1.5; - var z: f64 = -92.631 + 1.5; - if (i & 1 != 0) x = -x + 3; - if (i & 2 != 0) y = -y + 3; - if (i & 4 != 0) z = -z + 3; - var chunkAllocation: SubAllocation = .{ .start = 0, .len = 0 }; - main.renderer.chunk_meshing.chunkBuffer.uploadData(&.{.{ - .position = .{ 0, 0, 0 }, - .min = undefined, - .max = undefined, - .voxelSize = 1, - .lightStart = lightAllocation.start, - .vertexStartOpaque = undefined, - .faceCountsByNormalOpaque = undefined, - .vertexStartTransparent = undefined, - .vertexCountTransparent = undefined, - .visibilityState = 0, - .oldVisibilityState = 0, - }}, &chunkAllocation); - defer main.renderer.chunk_meshing.chunkBuffer.free(chunkAllocation); - if (block.transparent()) { - c.glBlendEquation(c.GL_FUNC_ADD); - c.glBlendFunc(c.GL_ONE, c.GL_SRC1_COLOR); - main.renderer.chunk_meshing.bindTransparentShaderAndUniforms(projMatrix, .{ 1, 1, 1 }, .{ x, y, z }); - } else { - main.renderer.chunk_meshing.bindShaderAndUniforms(projMatrix, .{ 1, 1, 1 }, .{ x, y, z }); - } - c.glUniform1f(uniforms.contrast, 0.25); - c.glActiveTexture(c.GL_TEXTURE0); - main.blocks.meshes.blockTextureArray.bind(); - c.glActiveTexture(c.GL_TEXTURE1); - main.blocks.meshes.emissionTextureArray.bind(); - c.glActiveTexture(c.GL_TEXTURE2); - main.blocks.meshes.reflectivityAndAbsorptionTextureArray.bind(); - block_texture.depthTexture.bindTo(5); - c.glDrawElementsInstancedBaseVertexBaseInstance(c.GL_TRIANGLES, @intCast(6 * faceData.items.len), c.GL_UNSIGNED_INT, null, 1, allocation.start * 4, chunkAllocation.start); - } - - c.glDisable(c.GL_CULL_FACE); - var finalFrameBuffer: FrameBuffer = undefined; - finalFrameBuffer.init(false, c.GL_NEAREST, c.GL_REPEAT); - finalFrameBuffer.updateSize(textureSize, textureSize, c.GL_RGBA8); - finalFrameBuffer.bind(); - const texture = Texture{ .textureID = finalFrameBuffer.texture }; - defer c.glDeleteFramebuffers(1, &finalFrameBuffer.frameBuffer); - block_texture.pipeline.bind(null); - c.glUniform1i(block_texture.uniforms.transparent, if (block.transparent()) c.GL_TRUE else c.GL_FALSE); - frameBuffer.bindTexture(c.GL_TEXTURE3); - - draw.rectVao.bind(); - c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); - - c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0); - - c.glViewport(0, 0, main.Window.width, main.Window.height); - c.glBlendFunc(c.GL_SRC_ALPHA, c.GL_ONE_MINUS_SRC_ALPHA); - return texture; + const block = main.blocks.Block{.typ = blockType, .data = 0}; // TODO: Use natural standard data. + const textureSize = block_texture.textureSize; + c.glViewport(0, 0, textureSize, textureSize); + + var frameBuffer: FrameBuffer = undefined; + + frameBuffer.init(false, c.GL_NEAREST, c.GL_REPEAT); + defer frameBuffer.deinit(); + frameBuffer.updateSize(textureSize, textureSize, c.GL_RGBA16F); + frameBuffer.bind(); + if (block.transparent()) { + frameBuffer.clear(.{0.683421, 0.6854237, 0.685426, 1}); + } else { + frameBuffer.clear(.{0, 0, 0, 0}); + } + + const projMatrix = Mat4f.perspective(0.013, 1, 64, 256); + const oldViewMatrix = main.game.camera.viewMatrix; + main.game.camera.viewMatrix = Mat4f.identity().mul(Mat4f.rotationX(std.math.pi/4.0)).mul(Mat4f.rotationZ(1.0*std.math.pi/4.0)); + defer main.game.camera.viewMatrix = oldViewMatrix; + const uniforms = if (block.transparent()) &main.renderer.chunk_meshing.transparentUniforms else &main.renderer.chunk_meshing.uniforms; + + var faceData: main.ListUnmanaged(main.renderer.chunk_meshing.FaceData) = .{}; + defer faceData.deinit(main.stackAllocator); + const model = main.blocks.meshes.model(block).model(); + const pos: main.chunk.BlockPos = .fromCoords(1, 1, 1); + if (block.hasBackFace()) { + model.appendInternalQuadsToList(&faceData, main.stackAllocator, block, pos, true); + for (main.chunk.Neighbor.iterable) |neighbor| { + model.appendNeighborFacingQuadsToList(&faceData, main.stackAllocator, block, neighbor, pos, true); + } + } + model.appendInternalQuadsToList(&faceData, main.stackAllocator, block, pos, false); + for (main.chunk.Neighbor.iterable) |neighbor| { + model.appendNeighborFacingQuadsToList(&faceData, main.stackAllocator, block, neighbor, pos.neighbor(neighbor)[0], false); + } + + for (faceData.items) |*face| { + face.position.lightIndex = 0; + } + var allocation: SubAllocation = .{.start = 0, .len = 0}; + main.renderer.chunk_meshing.faceBuffers[0].uploadData(faceData.items, &allocation); + defer main.renderer.chunk_meshing.faceBuffers[0].free(allocation); + var lightAllocation: SubAllocation = .{.start = 0, .len = 0}; + main.renderer.chunk_meshing.lightBuffers[0].uploadData(&.{0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff}, &lightAllocation); + defer main.renderer.chunk_meshing.lightBuffers[0].free(lightAllocation); + + { + const i = 4; // Easily switch between the 8 diagonal coordinates. + var x: f64 = -65.5 + 1.5; + var y: f64 = -65.5 + 1.5; + var z: f64 = -92.631 + 1.5; + if (i & 1 != 0) x = -x + 3; + if (i & 2 != 0) y = -y + 3; + if (i & 4 != 0) z = -z + 3; + var chunkAllocation: SubAllocation = .{.start = 0, .len = 0}; + main.renderer.chunk_meshing.chunkBuffer.uploadData(&.{.{ + .position = .{0, 0, 0}, + .min = undefined, + .max = undefined, + .voxelSize = 1, + .lightStart = lightAllocation.start, + .vertexStartOpaque = undefined, + .faceCountsByNormalOpaque = undefined, + .vertexStartTransparent = undefined, + .vertexCountTransparent = undefined, + .visibilityState = 0, + .oldVisibilityState = 0, + }}, &chunkAllocation); + defer main.renderer.chunk_meshing.chunkBuffer.free(chunkAllocation); + if (block.transparent()) { + c.glBlendEquation(c.GL_FUNC_ADD); + c.glBlendFunc(c.GL_ONE, c.GL_SRC1_COLOR); + main.renderer.chunk_meshing.bindTransparentShaderAndUniforms(projMatrix, .{1, 1, 1}, .{x, y, z}); + } else { + main.renderer.chunk_meshing.bindShaderAndUniforms(projMatrix, .{1, 1, 1}, .{x, y, z}); + } + c.glUniform1f(uniforms.contrast, 0.25); + c.glActiveTexture(c.GL_TEXTURE0); + main.blocks.meshes.blockTextureArray.bind(); + c.glActiveTexture(c.GL_TEXTURE1); + main.blocks.meshes.emissionTextureArray.bind(); + c.glActiveTexture(c.GL_TEXTURE2); + main.blocks.meshes.reflectivityAndAbsorptionTextureArray.bind(); + block_texture.depthTexture.bindTo(5); + c.glDrawElementsInstancedBaseVertexBaseInstance(c.GL_TRIANGLES, @intCast(6*faceData.items.len), c.GL_UNSIGNED_INT, null, 1, allocation.start*4, chunkAllocation.start); + } + + c.glDisable(c.GL_CULL_FACE); + var finalFrameBuffer: FrameBuffer = undefined; + finalFrameBuffer.init(false, c.GL_NEAREST, c.GL_REPEAT); + finalFrameBuffer.updateSize(textureSize, textureSize, c.GL_RGBA8); + finalFrameBuffer.bind(); + const texture = Texture{.textureID = finalFrameBuffer.texture}; + defer c.glDeleteFramebuffers(1, &finalFrameBuffer.frameBuffer); + block_texture.pipeline.bind(null); + c.glUniform1i(block_texture.uniforms.transparent, if (block.transparent()) c.GL_TRUE else c.GL_FALSE); + frameBuffer.bindTexture(c.GL_TEXTURE3); + + draw.rectVao.bind(); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + + c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0); + + c.glViewport(0, 0, main.Window.width, main.Window.height); + c.glBlendFunc(c.GL_SRC_ALPHA, c.GL_ONE_MINUS_SRC_ALPHA); + return texture; } diff --git a/src/gui/windows/controls.zig b/src/gui/windows/controls.zig index ef32ce1a96..4355d888e7 100644 --- a/src/gui/windows/controls.zig +++ b/src/gui/windows/controls.zig @@ -17,8 +17,8 @@ const VerticalList = @import("../components/VerticalList.zig"); const ContinuousSlider = @import("../components/ContinuousSlider.zig"); pub var window = GuiWindow{ - .contentSize = Vec2f{ 128, 192 }, - .closeIfMouseIsGrabbed = true, + .contentSize = Vec2f{128, 192}, + .closeIfMouseIsGrabbed = true, }; const padding: f32 = 8; @@ -26,59 +26,59 @@ var selectedKey: ?*main.Window.Key = null; var editingKeyboard: bool = true; var needsUpdate: bool = false; fn keyFunction(key: *main.Window.Key) void { - main.Window.setNextKeypressListener(&keypressListener) catch return; - selectedKey = key; - needsUpdate = true; + main.Window.setNextKeypressListener(&keypressListener) catch return; + selectedKey = key; + needsUpdate = true; } fn keypressListener(key: c_int, mouseButton: c_int, scancode: c_int) void { - selectedKey.?.key = key; - selectedKey.?.mouseButton = mouseButton; - selectedKey.?.scancode = scancode; - selectedKey = null; - needsUpdate = true; - main.settings.save(); + selectedKey.?.key = key; + selectedKey.?.mouseButton = mouseButton; + selectedKey.?.scancode = scancode; + selectedKey = null; + needsUpdate = true; + main.settings.save(); } fn gamepadFunction(key: *main.Window.Key) void { - main.Window.setNextGamepadListener(&gamepadListener) catch return; - selectedKey = key; - needsUpdate = true; + main.Window.setNextGamepadListener(&gamepadListener) catch return; + selectedKey = key; + needsUpdate = true; } fn gamepadListener(axis: ?main.Window.GamepadAxis, btn: c_int) void { - selectedKey.?.gamepadAxis = axis; - selectedKey.?.gamepadButton = btn; - selectedKey = null; - needsUpdate = true; - main.settings.save(); + selectedKey.?.gamepadAxis = axis; + selectedKey.?.gamepadButton = btn; + selectedKey = null; + needsUpdate = true; + main.settings.save(); } fn updateSensitivity(sensitivity: f32) void { - if (editingKeyboard) { - main.settings.mouseSensitivity = sensitivity; - } else { - main.settings.controllerSensitivity = sensitivity; - } - main.settings.save(); + if (editingKeyboard) { + main.settings.mouseSensitivity = sensitivity; + } else { + main.settings.controllerSensitivity = sensitivity; + } + main.settings.save(); } fn invertMouseYCallback(newValue: bool) void { - main.settings.invertMouseY = newValue; - main.settings.save(); + main.settings.invertMouseY = newValue; + main.settings.save(); } fn sprintIsToggleCallback(newValue: bool) void { - main.KeyBoard.setIsToggling("sprint", newValue); - main.settings.save(); + main.KeyBoard.setIsToggling("sprint", newValue); + main.settings.save(); } fn updateDeadzone(deadzone: f32) void { - main.settings.controllerAxisDeadzone = deadzone; + main.settings.controllerAxisDeadzone = deadzone; } fn deadzoneFormatter(allocator: main.heap.NeverFailingAllocator, value: f32) []const u8 { - return std.fmt.allocPrint(allocator.allocator, "Deadzone: {d:.0}%", .{value * 100}) catch unreachable; + return std.fmt.allocPrint(allocator.allocator, "Deadzone: {d:.0}%", .{value*100}) catch unreachable; } fn sensitivityFormatter(allocator: main.heap.NeverFailingAllocator, value: f32) []const u8 { - return std.fmt.allocPrint(allocator.allocator, "{s} Sensitivity: {d:.0}%", .{ if (editingKeyboard) "Mouse" else "Controller", value * 100 }) catch unreachable; + return std.fmt.allocPrint(allocator.allocator, "{s} Sensitivity: {d:.0}%", .{if (editingKeyboard) "Mouse" else "Controller", value*100}) catch unreachable; } fn abortBindingProcess() void { @@ -93,16 +93,16 @@ fn toggleKeyboard() void { } fn unbindKey(keyPtr: usize) void { - var key: ?*main.Window.Key = @ptrFromInt(keyPtr); - if (editingKeyboard) { - key.?.key = c.GLFW_KEY_UNKNOWN; - key.?.mouseButton = -1; - key.?.scancode = 0; - } else { - key.?.gamepadAxis = null; - key.?.gamepadButton = -1; - } - needsUpdate = true; + var key: ?*main.Window.Key = @ptrFromInt(keyPtr); + if (editingKeyboard) { + key.?.key = c.GLFW_KEY_UNKNOWN; + key.?.mouseButton = -1; + key.?.scancode = 0; + } else { + key.?.gamepadAxis = null; + key.?.gamepadButton = -1; + } + needsUpdate = true; } fn initWindow() void { diff --git a/src/gui/windows/crosshair.zig b/src/gui/windows/crosshair.zig index 9360b9af83..18406f8832 100644 --- a/src/gui/windows/crosshair.zig +++ b/src/gui/windows/crosshair.zig @@ -13,55 +13,55 @@ const GuiComponent = gui.GuiComponent; const size: f32 = 64; pub var window = GuiWindow{ - .contentSize = Vec2f{ size, size }, - .showTitleBar = false, - .hasBackground = false, - .isHud = true, - .hideIfMouseIsGrabbed = false, - .closeable = false, + .contentSize = Vec2f{size, size}, + .showTitleBar = false, + .hasBackground = false, + .isHud = true, + .hideIfMouseIsGrabbed = false, + .closeable = false, }; var texture: Texture = undefined; var pipeline: graphics.Pipeline = undefined; var uniforms: struct { - screen: c_int, - start: c_int, - size: c_int, - color: c_int, - uvOffset: c_int, - uvDim: c_int, + screen: c_int, + start: c_int, + size: c_int, + color: c_int, + uvOffset: c_int, + uvDim: c_int, } = undefined; pub fn init() void { - pipeline = graphics.Pipeline.init( - "assets/cubyz/shaders/graphics/Image.vert", - "assets/cubyz/shaders/graphics/Image.frag", - "", - &uniforms, - graphics.draw.SimpleVertex2D, - &.{}, - .{ .cullMode = .none }, - .{ .depthTest = false, .depthWrite = false }, - .{ .attachments = &.{.{ - .srcColorBlendFactor = .one, - .dstColorBlendFactor = .one, - .colorBlendOp = .subtract, - .srcAlphaBlendFactor = .one, - .dstAlphaBlendFactor = .one, - .alphaBlendOp = .subtract, - }} }, - ); - texture = Texture.initFromFile("assets/cubyz/ui/hud/crosshair.png"); + pipeline = graphics.Pipeline.init( + "assets/cubyz/shaders/graphics/Image.vert", + "assets/cubyz/shaders/graphics/Image.frag", + "", + &uniforms, + graphics.draw.SimpleVertex2D, + &.{}, + .{.cullMode = .none}, + .{.depthTest = false, .depthWrite = false}, + .{.attachments = &.{.{ + .srcColorBlendFactor = .one, + .dstColorBlendFactor = .one, + .colorBlendOp = .subtract, + .srcAlphaBlendFactor = .one, + .dstAlphaBlendFactor = .one, + .alphaBlendOp = .subtract, + }}}, + ); + texture = Texture.initFromFile("assets/cubyz/ui/hud/crosshair.png"); } pub fn deinit() void { - pipeline.deinit(); - texture.deinit(); + pipeline.deinit(); + texture.deinit(); } pub fn render() void { - texture.bindTo(0); - graphics.draw.setColor(0xffffffff); - pipeline.bind(graphics.draw.getScissor()); - graphics.draw.customShadedImage(&uniforms, .{ 0, 0 }, .{ size, size }); + texture.bindTo(0); + graphics.draw.setColor(0xffffffff); + pipeline.bind(graphics.draw.getScissor()); + graphics.draw.customShadedImage(&uniforms, .{0, 0}, .{size, size}); } diff --git a/src/gui/windows/main.zig b/src/gui/windows/main.zig index ee4d745b6f..3056283fc4 100644 --- a/src/gui/windows/main.zig +++ b/src/gui/windows/main.zig @@ -12,29 +12,29 @@ const Button = @import("../components/Button.zig"); const VerticalList = @import("../components/VerticalList.zig"); pub var window = GuiWindow{ - .contentSize = Vec2f{ 128, 256 }, - .closeable = false, + .contentSize = Vec2f{128, 256}, + .closeable = false, }; const padding: f32 = 8; fn exitGame() void { - c.glfwSetWindowShouldClose(main.Window.window, c.GLFW_TRUE); + c.glfwSetWindowShouldClose(main.Window.window, c.GLFW_TRUE); } pub fn onOpen() void { - const list = VerticalList.init(.{ padding, 16 + padding }, 300, 16); - list.add(Button.initText(.{ 0, 0 }, 128, "Singleplayer", gui.openWindowCallback("save_selection"))); - list.add(Button.initText(.{ 0, 0 }, 128, "Multiplayer", gui.openWindowCallback("multiplayer"))); - list.add(Button.initText(.{ 0, 0 }, 128, "Settings", gui.openWindowCallback("settings"))); - list.add(Button.initText(.{ 0, 0 }, 128, "Touch Grass", .init(exitGame))); - list.finish(.center); - window.rootComponent = list.toComponent(); - window.contentSize = window.rootComponent.?.pos() + window.rootComponent.?.size() + @as(Vec2f, @splat(padding)); - gui.updateWindowPositions(); + const list = VerticalList.init(.{padding, 16 + padding}, 300, 16); + list.add(Button.initText(.{0, 0}, 128, "Singleplayer", gui.openWindowCallback("save_selection"))); + list.add(Button.initText(.{0, 0}, 128, "Multiplayer", gui.openWindowCallback("multiplayer"))); + list.add(Button.initText(.{0, 0}, 128, "Settings", gui.openWindowCallback("settings"))); + list.add(Button.initText(.{0, 0}, 128, "Touch Grass", .init(exitGame))); + list.finish(.center); + window.rootComponent = list.toComponent(); + window.contentSize = window.rootComponent.?.pos() + window.rootComponent.?.size() + @as(Vec2f, @splat(padding)); + gui.updateWindowPositions(); } pub fn onClose() void { - if (window.rootComponent) |*comp| { - comp.deinit(); - } + if (window.rootComponent) |*comp| { + comp.deinit(); + } } diff --git a/src/itemdrop.zig b/src/itemdrop.zig index de9595c900..292ebeb477 100644 --- a/src/itemdrop.zig +++ b/src/itemdrop.zig @@ -39,9 +39,9 @@ const ItemDrop = struct { // MARK: ItemDrop }; pub const ItemDropNetworkData = struct { - index: u16, - pos: Vec3d, - vel: Vec3d, + index: u16, + pos: Vec3d, + vel: Vec3d, }; pub const ItemDropManager = struct { // MARK: ItemDropManager @@ -395,424 +395,424 @@ pub const ItemDropManager = struct { // MARK: ItemDropManager }; pub const ClientItemDropManager = struct { // MARK: ClientItemDropManager - const maxf64Capacity = ItemDropManager.maxCapacity * @sizeOf(Vec3d) / @sizeOf(f64); - - super: ItemDropManager, - - lastTime: i16, - - timeDifference: utils.TimeDifference = .{}, - - interpolation: utils.GenericInterpolation(maxf64Capacity) align(64) = undefined, - - var instance: ?*ClientItemDropManager = null; - - var mutex: main.utils.Mutex = .{}; - - pub fn init(self: *ClientItemDropManager, allocator: NeverFailingAllocator) void { - std.debug.assert(instance == null); // Only one instance allowed. - instance = self; - self.* = .{ - .super = undefined, - .lastTime = @as(i16, @truncate(main.timestamp().toMilliseconds())) -% settings.entityLookback, - }; - self.super.init(allocator, null); - self.interpolation.init( - @ptrCast(self.super.list.items(.pos).ptr), - @ptrCast(self.super.list.items(.vel).ptr), - ); - } - - pub fn deinit(self: *ClientItemDropManager) void { - std.debug.assert(instance != null); // Double deinit. - self.super.deinit(); - instance = null; - } - - pub fn readPosition(self: *ClientItemDropManager, time: i16, itemData: []ItemDropNetworkData) void { - self.timeDifference.addDataPoint(time); - var pos: [ItemDropManager.maxCapacity]Vec3d = undefined; - var vel: [ItemDropManager.maxCapacity]Vec3d = undefined; - for (itemData) |data| { - pos[data.index] = data.pos; - vel[data.index] = data.vel; - } - mutex.lock(); - defer mutex.unlock(); - self.interpolation.updatePosition(@ptrCast(&pos), @ptrCast(&vel), time); // TODO: Only update the ones we actually changed. - } - - pub fn updateInterpolationData(self: *ClientItemDropManager) void { - self.super.processChanges(); - var time = @as(i16, @truncate(main.timestamp().toMilliseconds())) -% settings.entityLookback; - time -%= self.timeDifference.difference.load(.monotonic); - { - mutex.lock(); - defer mutex.unlock(); - self.interpolation.updateIndexed(time, self.lastTime, self.super.indices[0..self.super.size], 4); - } - self.lastTime = time; - } - - fn clientSideInternalAdd(_: *ItemDropManager, i: u16, drop: ItemDrop) void { - mutex.lock(); - defer mutex.unlock(); - for (&instance.?.interpolation.lastVel) |*lastVel| { - @as(*align(8) [ItemDropManager.maxCapacity]Vec3d, @ptrCast(lastVel))[i] = Vec3d{ 0, 0, 0 }; - } - for (&instance.?.interpolation.lastPos) |*lastPos| { - @as(*align(8) [ItemDropManager.maxCapacity]Vec3d, @ptrCast(lastPos))[i] = drop.pos; - } - } - - pub fn remove(self: *ClientItemDropManager, i: u16) void { - self.super.emptyMutex.lock(); - self.super.isEmpty.set(i); - self.super.emptyMutex.unlock(); - self.super.changeQueue.pushBack(.{ .remove = i }); - } - - pub fn loadFrom(self: *ClientItemDropManager, zon: ZonElement) void { - self.super.loadFrom(zon); - } - - pub fn addFromZon(self: *ClientItemDropManager, zon: ZonElement) void { - self.super.addFromZon(zon); - } + const maxf64Capacity = ItemDropManager.maxCapacity*@sizeOf(Vec3d)/@sizeOf(f64); + + super: ItemDropManager, + + lastTime: i16, + + timeDifference: utils.TimeDifference = .{}, + + interpolation: utils.GenericInterpolation(maxf64Capacity) align(64) = undefined, + + var instance: ?*ClientItemDropManager = null; + + var mutex: main.utils.Mutex = .{}; + + pub fn init(self: *ClientItemDropManager, allocator: NeverFailingAllocator) void { + std.debug.assert(instance == null); // Only one instance allowed. + instance = self; + self.* = .{ + .super = undefined, + .lastTime = @as(i16, @truncate(main.timestamp().toMilliseconds())) -% settings.entityLookback, + }; + self.super.init(allocator, null); + self.interpolation.init( + @ptrCast(self.super.list.items(.pos).ptr), + @ptrCast(self.super.list.items(.vel).ptr), + ); + } + + pub fn deinit(self: *ClientItemDropManager) void { + std.debug.assert(instance != null); // Double deinit. + self.super.deinit(); + instance = null; + } + + pub fn readPosition(self: *ClientItemDropManager, time: i16, itemData: []ItemDropNetworkData) void { + self.timeDifference.addDataPoint(time); + var pos: [ItemDropManager.maxCapacity]Vec3d = undefined; + var vel: [ItemDropManager.maxCapacity]Vec3d = undefined; + for (itemData) |data| { + pos[data.index] = data.pos; + vel[data.index] = data.vel; + } + mutex.lock(); + defer mutex.unlock(); + self.interpolation.updatePosition(@ptrCast(&pos), @ptrCast(&vel), time); // TODO: Only update the ones we actually changed. + } + + pub fn updateInterpolationData(self: *ClientItemDropManager) void { + self.super.processChanges(); + var time = @as(i16, @truncate(main.timestamp().toMilliseconds())) -% settings.entityLookback; + time -%= self.timeDifference.difference.load(.monotonic); + { + mutex.lock(); + defer mutex.unlock(); + self.interpolation.updateIndexed(time, self.lastTime, self.super.indices[0..self.super.size], 4); + } + self.lastTime = time; + } + + fn clientSideInternalAdd(_: *ItemDropManager, i: u16, drop: ItemDrop) void { + mutex.lock(); + defer mutex.unlock(); + for (&instance.?.interpolation.lastVel) |*lastVel| { + @as(*align(8) [ItemDropManager.maxCapacity]Vec3d, @ptrCast(lastVel))[i] = Vec3d{0, 0, 0}; + } + for (&instance.?.interpolation.lastPos) |*lastPos| { + @as(*align(8) [ItemDropManager.maxCapacity]Vec3d, @ptrCast(lastPos))[i] = drop.pos; + } + } + + pub fn remove(self: *ClientItemDropManager, i: u16) void { + self.super.emptyMutex.lock(); + self.super.isEmpty.set(i); + self.super.emptyMutex.unlock(); + self.super.changeQueue.pushBack(.{.remove = i}); + } + + pub fn loadFrom(self: *ClientItemDropManager, zon: ZonElement) void { + self.super.loadFrom(zon); + } + + pub fn addFromZon(self: *ClientItemDropManager, zon: ZonElement) void { + self.super.addFromZon(zon); + } }; // Going to handle item animations and other things like - bobbing, interpolation, movement reactions pub const ItemDisplayManager = struct { // MARK: ItemDisplayManager - pub var showItem: bool = true; - var cameraFollow: Vec3f = @splat(0); - var cameraFollowVel: Vec3f = @splat(0); - const damping: Vec3f = @splat(130); + pub var showItem: bool = true; + var cameraFollow: Vec3f = @splat(0); + var cameraFollowVel: Vec3f = @splat(0); + const damping: Vec3f = @splat(130); - pub fn update(deltaTime: f64) void { - if (deltaTime == 0) return; - const dt: f32 = @floatCast(deltaTime); + pub fn update(deltaTime: f64) void { + if (deltaTime == 0) return; + const dt: f32 = @floatCast(deltaTime); - var playerVel: Vec3f = .{ @floatCast((game.Player.super.vel[2] * 0.009 + game.Player.eye.vel[2] * 0.0075)), 0, 0 }; - playerVel = vec.clampMag(playerVel, 0.32); + var playerVel: Vec3f = .{@floatCast((game.Player.super.vel[2]*0.009 + game.Player.eye.vel[2]*0.0075)), 0, 0}; + playerVel = vec.clampMag(playerVel, 0.32); - // TODO: add *smooth* item sway - const n1: Vec3f = cameraFollowVel - (cameraFollow - playerVel) * damping * damping * @as(Vec3f, @splat(dt)); - const n2: Vec3f = @as(Vec3f, @splat(1)) + damping * @as(Vec3f, @splat(dt)); - cameraFollowVel = n1 / (n2 * n2); + // TODO: add *smooth* item sway + const n1: Vec3f = cameraFollowVel - (cameraFollow - playerVel)*damping*damping*@as(Vec3f, @splat(dt)); + const n2: Vec3f = @as(Vec3f, @splat(1)) + damping*@as(Vec3f, @splat(dt)); + cameraFollowVel = n1/(n2*n2); - cameraFollow += cameraFollowVel * @as(Vec3f, @splat(dt)); - } + cameraFollow += cameraFollowVel*@as(Vec3f, @splat(dt)); + } }; pub const ItemDropRenderer = struct { // MARK: ItemDropRenderer - var itemPipeline: graphics.Pipeline = undefined; - var itemUniforms: struct { - projectionMatrix: c_int, - modelMatrix: c_int, - viewMatrix: c_int, - ambientLight: c_int, - modelIndex: c_int, - block: c_int, - reflectionMapSize: c_int, - contrast: c_int, - glDepthRange: c_int, - } = undefined; - - var itemModelSSBO: graphics.SSBO = undefined; - var modelData: main.List(u32) = undefined; - var freeSlots: main.List(*ItemVoxelModel) = undefined; - - const ItemVoxelModel = struct { - index: u31 = undefined, - len: u31 = undefined, - item: items.Item, - - fn getSlot(len: u31) u31 { - for (freeSlots.items, 0..) |potentialSlot, i| { - if (len == potentialSlot.len) { - _ = freeSlots.swapRemove(i); - const result = potentialSlot.index; - main.globalAllocator.destroy(potentialSlot); - return result; - } - } - const result: u31 = @intCast(modelData.items.len); - modelData.resize(result + len); - return result; - } - - fn init(template: ItemVoxelModel) *ItemVoxelModel { - const self = main.globalAllocator.create(ItemVoxelModel); - self.* = ItemVoxelModel{ - .item = template.item, - }; - if (self.item == .baseItem and self.item.baseItem.block() != null and self.item.baseItem.image().imageData.ptr == graphics.Image.defaultImage.imageData.ptr) { - // Find sizes and free index: - var block = blocks.Block{ .typ = self.item.baseItem.block().?, .data = 0 }; - block.data = block.mode().naturalStandard; - const model = blocks.meshes.model(block).model(); - var data = main.List(u32).init(main.stackAllocator); - defer data.deinit(); - for (model.internalQuads) |quad| { - const textureIndex = blocks.meshes.textureIndex(block, quad.quadInfo().textureSlot); - data.append(@as(u32, @intFromEnum(quad)) << 16 | textureIndex); // modelAndTexture - data.append(0); // offsetByNormal - } - for (model.neighborFacingQuads) |list| { - for (list) |quad| { - const textureIndex = blocks.meshes.textureIndex(block, quad.quadInfo().textureSlot); - data.append(@as(u32, @intFromEnum(quad)) << 16 | textureIndex); // modelAndTexture - data.append(1); // offsetByNormal - } - } - self.len = @intCast(data.items.len); - self.index = getSlot(self.len); - @memcpy(modelData.items[self.index..][0..self.len], data.items); - } else { - // Find sizes and free index: - const img = self.item.getImage(); - const size = Vec3i{ img.width, 1, img.height }; - self.len = @intCast(3 + @reduce(.Mul, size)); - self.index = getSlot(self.len); - var dataSection: []u32 = undefined; - dataSection = modelData.items[self.index..][0..self.len]; - dataSection[0] = @intCast(size[0]); - dataSection[1] = @intCast(size[1]); - dataSection[2] = @intCast(size[2]); - var i: u32 = 3; - var z: u32 = 0; - while (z < 1) : (z += 1) { - var x: u32 = 0; - while (x < img.width) : (x += 1) { - var y: u32 = 0; - while (y < img.height) : (y += 1) { - dataSection[i] = img.getRGB(x, y).toARBG(); - i += 1; - } - } - } - } - itemModelSSBO.bufferData(u32, modelData.items); - return self; - } - - fn deinit(self: *ItemVoxelModel) void { - freeSlots.append(self); - } - - pub fn equals(self: ItemVoxelModel, other: ?*ItemVoxelModel) bool { - if (other == null) return false; - return std.meta.eql(self.item, other.?.item); - } - - pub fn hashCode(self: ItemVoxelModel) u32 { - return self.item.hashCode(); - } - }; - - pub fn init() void { - itemPipeline = graphics.Pipeline.init( - "assets/cubyz/shaders/item_drop.vert", - "assets/cubyz/shaders/item_drop.frag", - "", - &itemUniforms, - graphics.VertexArray.EmptyVertex, - &.{}, - .{}, - .{ .depthTest = true }, - .{ .attachments = &.{.alphaBlending} }, - ); - itemModelSSBO = .init(); - itemModelSSBO.bufferData(i32, &[3]i32{ 1, 1, 1 }); - itemModelSSBO.bind(2); - - modelData = .init(main.globalAllocator); - freeSlots = .init(main.globalAllocator); - } - - pub fn deinit() void { - itemPipeline.deinit(); - itemModelSSBO.deinit(); - modelData.deinit(); - voxelModels.clear(); - for (freeSlots.items) |freeSlot| { - main.globalAllocator.destroy(freeSlot); - } - freeSlots.deinit(); - } - - var voxelModels: utils.Cache(ItemVoxelModel, 32, 32, ItemVoxelModel.deinit) = .{}; - - fn getModel(item: items.Item) *ItemVoxelModel { - const compareObject = ItemVoxelModel{ .item = item }; - return voxelModels.findOrCreate(compareObject, ItemVoxelModel.init, null); - } - - fn bindCommonUniforms(projMatrix: Mat4f, viewMatrix: Mat4f, ambientLight: Vec3f) void { - itemPipeline.bind(null); - c.glUniform1f(itemUniforms.reflectionMapSize, main.renderer.reflectionCubeMapSize); - c.glUniformMatrix4fv(itemUniforms.projectionMatrix, 1, c.GL_TRUE, @ptrCast(&projMatrix)); - c.glUniform3fv(itemUniforms.ambientLight, 1, @ptrCast(&ambientLight)); - c.glUniformMatrix4fv(itemUniforms.viewMatrix, 1, c.GL_TRUE, @ptrCast(&viewMatrix)); - c.glUniform1f(itemUniforms.contrast, 0.12); - var depthRange: [2]f32 = undefined; - c.glGetFloatv(c.GL_DEPTH_RANGE, &depthRange); - c.glUniform2fv(itemUniforms.glDepthRange, 1, &depthRange); - } - - fn bindLightUniform(light: [6]u8, ambientLight: Vec3f) void { - const sunLight: Vec3f = ambientLight * @as(Vec3f, @floatFromInt(Vec3i{ light[0], light[1], light[2] })) / @as(Vec3f, @splat(255)); - const blockLight: Vec3f = @as(Vec3f, @floatFromInt(Vec3i{ light[3], light[4], light[5] })) / @as(Vec3f, @splat(255)); - c.glUniform3fv(itemUniforms.ambientLight, 1, @ptrCast(&@min(@sqrt(sunLight * sunLight + blockLight * blockLight), @as(Vec3f, @splat(1))))); - } - - fn bindModelUniforms(modelIndex: u31, blockType: u16) void { - c.glUniform1i(itemUniforms.modelIndex, modelIndex); - c.glUniform1i(itemUniforms.block, blockType); - } - - fn drawItem(vertices: u31, modelMatrix: Mat4f) void { - c.glUniformMatrix4fv(itemUniforms.modelMatrix, 1, c.GL_TRUE, @ptrCast(&modelMatrix)); - main.renderer.chunk_meshing.vao.bind(); - c.glDrawElements(c.GL_TRIANGLES, vertices, c.GL_UNSIGNED_INT, null); - } - - pub fn renderItemDrops(projMatrix: Mat4f, ambientLight: Vec3f, playerPos: Vec3d) void { - game.world.?.itemDrops.updateInterpolationData(); - - bindCommonUniforms(projMatrix, game.camera.viewMatrix, ambientLight); - const itemDrops = &game.world.?.itemDrops.super; - for (itemDrops.indices[0..itemDrops.size]) |i| { - const item = itemDrops.list.items(.itemStack)[i].item; - if (item != .null) { - var pos = itemDrops.list.items(.pos)[i]; - const rot = itemDrops.list.items(.rot)[i]; - const blockPos: Vec3i = @floor(pos); - const light: [6]u8 = main.renderer.mesh_storage.getLight(blockPos[0], blockPos[1], blockPos[2]) orelse @splat(0); - bindLightUniform(light, ambientLight); - pos -= playerPos; - - const model = getModel(item); - var vertices: u31 = 36; - - var scale: f32 = 0.3; - var blockType: u16 = 0; - if (item == .baseItem and item.baseItem.block() != null and item.baseItem.image().imageData.ptr == graphics.Image.defaultImage.imageData.ptr) { - blockType = item.baseItem.block().?; - vertices = model.len / 2 * 6; - } else { - scale = 0.5; - } - bindModelUniforms(model.index, blockType); - - var modelMatrix = Mat4f.translation(@floatCast(pos)); - modelMatrix = modelMatrix.mul(Mat4f.rotationX(-rot[0])); - modelMatrix = modelMatrix.mul(Mat4f.rotationY(-rot[1])); - modelMatrix = modelMatrix.mul(Mat4f.rotationZ(-rot[2])); - modelMatrix = modelMatrix.mul(Mat4f.scale(@splat(scale))); - modelMatrix = modelMatrix.mul(Mat4f.translation(@splat(-0.5))); - drawItem(vertices, modelMatrix); - } - } - } - - inline fn getIndex(x: u8, y: u8, z: u8) u32 { - return (z * 4) + (y * 2) + (x); - } - - inline fn blendColors(a: [6]f32, b: [6]f32, t: f32) [6]f32 { - var result: [6]f32 = .{ 0, 0, 0, 0, 0, 0 }; - inline for (0..6) |i| { - result[i] = std.math.lerp(a[i], b[i], t); - } - return result; - } - - pub fn renderDisplayItems(ambientLight: Vec3f, playerPos: Vec3d) void { - if (!ItemDisplayManager.showItem) return; - - const projMatrix: Mat4f = Mat4f.perspective(std.math.degreesToRadians(65), @as(f32, @floatFromInt(main.renderer.lastWidth)) / @as(f32, @floatFromInt(main.renderer.lastHeight)), 0.01, 3); - const viewMatrix = Mat4f.identity(); - bindCommonUniforms(projMatrix, viewMatrix, ambientLight); - - const item = game.Player.inventory.getItem(game.Player.selectedSlot); - if (item != .null) { - var pos: Vec3d = Vec3d{ 0, 0, 0 }; - const rot: Vec3f = ItemDisplayManager.cameraFollow; - - const lightPos = @as(Vec3d, @floatCast(playerPos)) - @as(Vec3f, @splat(0.5)); - const blockPos: Vec3i = @floor(lightPos); - const localBlockPos: Vec3f = @floatCast(lightPos - @as(Vec3d, @floatFromInt(blockPos))); - - var samples: [8][6]f32 = @splat(@splat(0)); - inline for (0..2) |z| { - inline for (0..2) |y| { - inline for (0..2) |x| { - const light: [6]u8 = main.renderer.mesh_storage.getLight( - blockPos[0] +% @as(i32, @intCast(x)), - blockPos[1] +% @as(i32, @intCast(y)), - blockPos[2] +% @as(i32, @intCast(z)), - ) orelse @splat(0); - - inline for (0..6) |i| { - samples[getIndex(x, y, z)][i] = @as(f32, @floatFromInt(light[i])); - } - } - } - } - - inline for (0..2) |y| { - inline for (0..2) |x| { - samples[getIndex(x, y, 0)] = blendColors(samples[getIndex(x, y, 0)], samples[getIndex(x, y, 1)], localBlockPos[2]); - } - } - - inline for (0..2) |x| { - samples[getIndex(x, 0, 0)] = blendColors(samples[getIndex(x, 0, 0)], samples[getIndex(x, 1, 0)], localBlockPos[1]); - } - - var result: [6]u8 = .{ 0, 0, 0, 0, 0, 0 }; - inline for (0..6) |i| { - const val = std.math.lerp(samples[getIndex(0, 0, 0)][i], samples[getIndex(1, 0, 0)][i], localBlockPos[0]); - result[i] = @floor(val); - } - - bindLightUniform(result, ambientLight); - - const model = getModel(item); - var vertices: u31 = 36; - - const isBlock: bool = item == .baseItem and item.baseItem.block() != null and item.baseItem.image().imageData.ptr == graphics.Image.defaultImage.imageData.ptr; - var scale: f32 = 0; - var blockType: u16 = 0; - if (isBlock) { - blockType = item.baseItem.block().?; - vertices = model.len / 2 * 6; - scale = 0.3; - pos = Vec3d{ 0.4, 0.55, -0.32 }; - } else { - scale = 0.57; - pos = Vec3d{ 0.4, 0.65, -0.3 }; - } - bindModelUniforms(model.index, blockType); - - var modelMatrix = Mat4f.rotationZ(-rot[2]); - modelMatrix = modelMatrix.mul(Mat4f.rotationY(-rot[1])); - modelMatrix = modelMatrix.mul(Mat4f.rotationX(-rot[0])); - modelMatrix = modelMatrix.mul(Mat4f.translation(@floatCast(pos))); - if (!isBlock) { - if (item == .proceduralItem) { - modelMatrix = modelMatrix.mul(Mat4f.rotationZ(-std.math.pi * 0.47)); - modelMatrix = modelMatrix.mul(Mat4f.rotationY(std.math.pi * 0.25)); - } else { - modelMatrix = modelMatrix.mul(Mat4f.rotationZ(-std.math.pi * 0.45)); - } - } else { - modelMatrix = modelMatrix.mul(Mat4f.rotationZ(-std.math.pi * 0.2)); - } - modelMatrix = modelMatrix.mul(Mat4f.scale(@splat(scale))); - modelMatrix = modelMatrix.mul(Mat4f.translation(@splat(-0.5))); - drawItem(vertices, modelMatrix); - } - } + var itemPipeline: graphics.Pipeline = undefined; + var itemUniforms: struct { + projectionMatrix: c_int, + modelMatrix: c_int, + viewMatrix: c_int, + ambientLight: c_int, + modelIndex: c_int, + block: c_int, + reflectionMapSize: c_int, + contrast: c_int, + glDepthRange: c_int, + } = undefined; + + var itemModelSSBO: graphics.SSBO = undefined; + var modelData: main.List(u32) = undefined; + var freeSlots: main.List(*ItemVoxelModel) = undefined; + + const ItemVoxelModel = struct { + index: u31 = undefined, + len: u31 = undefined, + item: items.Item, + + fn getSlot(len: u31) u31 { + for (freeSlots.items, 0..) |potentialSlot, i| { + if (len == potentialSlot.len) { + _ = freeSlots.swapRemove(i); + const result = potentialSlot.index; + main.globalAllocator.destroy(potentialSlot); + return result; + } + } + const result: u31 = @intCast(modelData.items.len); + modelData.resize(result + len); + return result; + } + + fn init(template: ItemVoxelModel) *ItemVoxelModel { + const self = main.globalAllocator.create(ItemVoxelModel); + self.* = ItemVoxelModel{ + .item = template.item, + }; + if (self.item == .baseItem and self.item.baseItem.block() != null and self.item.baseItem.image().imageData.ptr == graphics.Image.defaultImage.imageData.ptr) { + // Find sizes and free index: + var block = blocks.Block{.typ = self.item.baseItem.block().?, .data = 0}; + block.data = block.mode().naturalStandard; + const model = blocks.meshes.model(block).model(); + var data = main.List(u32).init(main.stackAllocator); + defer data.deinit(); + for (model.internalQuads) |quad| { + const textureIndex = blocks.meshes.textureIndex(block, quad.quadInfo().textureSlot); + data.append(@as(u32, @intFromEnum(quad)) << 16 | textureIndex); // modelAndTexture + data.append(0); // offsetByNormal + } + for (model.neighborFacingQuads) |list| { + for (list) |quad| { + const textureIndex = blocks.meshes.textureIndex(block, quad.quadInfo().textureSlot); + data.append(@as(u32, @intFromEnum(quad)) << 16 | textureIndex); // modelAndTexture + data.append(1); // offsetByNormal + } + } + self.len = @intCast(data.items.len); + self.index = getSlot(self.len); + @memcpy(modelData.items[self.index..][0..self.len], data.items); + } else { + // Find sizes and free index: + const img = self.item.getImage(); + const size = Vec3i{img.width, 1, img.height}; + self.len = @intCast(3 + @reduce(.Mul, size)); + self.index = getSlot(self.len); + var dataSection: []u32 = undefined; + dataSection = modelData.items[self.index..][0..self.len]; + dataSection[0] = @intCast(size[0]); + dataSection[1] = @intCast(size[1]); + dataSection[2] = @intCast(size[2]); + var i: u32 = 3; + var z: u32 = 0; + while (z < 1) : (z += 1) { + var x: u32 = 0; + while (x < img.width) : (x += 1) { + var y: u32 = 0; + while (y < img.height) : (y += 1) { + dataSection[i] = img.getRGB(x, y).toARBG(); + i += 1; + } + } + } + } + itemModelSSBO.bufferData(u32, modelData.items); + return self; + } + + fn deinit(self: *ItemVoxelModel) void { + freeSlots.append(self); + } + + pub fn equals(self: ItemVoxelModel, other: ?*ItemVoxelModel) bool { + if (other == null) return false; + return std.meta.eql(self.item, other.?.item); + } + + pub fn hashCode(self: ItemVoxelModel) u32 { + return self.item.hashCode(); + } + }; + + pub fn init() void { + itemPipeline = graphics.Pipeline.init( + "assets/cubyz/shaders/item_drop.vert", + "assets/cubyz/shaders/item_drop.frag", + "", + &itemUniforms, + graphics.VertexArray.EmptyVertex, + &.{}, + .{}, + .{.depthTest = true}, + .{.attachments = &.{.alphaBlending}}, + ); + itemModelSSBO = .init(); + itemModelSSBO.bufferData(i32, &[3]i32{1, 1, 1}); + itemModelSSBO.bind(2); + + modelData = .init(main.globalAllocator); + freeSlots = .init(main.globalAllocator); + } + + pub fn deinit() void { + itemPipeline.deinit(); + itemModelSSBO.deinit(); + modelData.deinit(); + voxelModels.clear(); + for (freeSlots.items) |freeSlot| { + main.globalAllocator.destroy(freeSlot); + } + freeSlots.deinit(); + } + + var voxelModels: utils.Cache(ItemVoxelModel, 32, 32, ItemVoxelModel.deinit) = .{}; + + fn getModel(item: items.Item) *ItemVoxelModel { + const compareObject = ItemVoxelModel{.item = item}; + return voxelModels.findOrCreate(compareObject, ItemVoxelModel.init, null); + } + + fn bindCommonUniforms(projMatrix: Mat4f, viewMatrix: Mat4f, ambientLight: Vec3f) void { + itemPipeline.bind(null); + c.glUniform1f(itemUniforms.reflectionMapSize, main.renderer.reflectionCubeMapSize); + c.glUniformMatrix4fv(itemUniforms.projectionMatrix, 1, c.GL_TRUE, @ptrCast(&projMatrix)); + c.glUniform3fv(itemUniforms.ambientLight, 1, @ptrCast(&ambientLight)); + c.glUniformMatrix4fv(itemUniforms.viewMatrix, 1, c.GL_TRUE, @ptrCast(&viewMatrix)); + c.glUniform1f(itemUniforms.contrast, 0.12); + var depthRange: [2]f32 = undefined; + c.glGetFloatv(c.GL_DEPTH_RANGE, &depthRange); + c.glUniform2fv(itemUniforms.glDepthRange, 1, &depthRange); + } + + fn bindLightUniform(light: [6]u8, ambientLight: Vec3f) void { + const sunLight: Vec3f = ambientLight*@as(Vec3f, @floatFromInt(Vec3i{light[0], light[1], light[2]}))/@as(Vec3f, @splat(255)); + const blockLight: Vec3f = @as(Vec3f, @floatFromInt(Vec3i{light[3], light[4], light[5]}))/@as(Vec3f, @splat(255)); + c.glUniform3fv(itemUniforms.ambientLight, 1, @ptrCast(&@min(@sqrt(sunLight*sunLight + blockLight*blockLight), @as(Vec3f, @splat(1))))); + } + + fn bindModelUniforms(modelIndex: u31, blockType: u16) void { + c.glUniform1i(itemUniforms.modelIndex, modelIndex); + c.glUniform1i(itemUniforms.block, blockType); + } + + fn drawItem(vertices: u31, modelMatrix: Mat4f) void { + c.glUniformMatrix4fv(itemUniforms.modelMatrix, 1, c.GL_TRUE, @ptrCast(&modelMatrix)); + main.renderer.chunk_meshing.vao.bind(); + c.glDrawElements(c.GL_TRIANGLES, vertices, c.GL_UNSIGNED_INT, null); + } + + pub fn renderItemDrops(projMatrix: Mat4f, ambientLight: Vec3f, playerPos: Vec3d) void { + game.world.?.itemDrops.updateInterpolationData(); + + bindCommonUniforms(projMatrix, game.camera.viewMatrix, ambientLight); + const itemDrops = &game.world.?.itemDrops.super; + for (itemDrops.indices[0..itemDrops.size]) |i| { + const item = itemDrops.list.items(.itemStack)[i].item; + if (item != .null) { + var pos = itemDrops.list.items(.pos)[i]; + const rot = itemDrops.list.items(.rot)[i]; + const blockPos: Vec3i = @floor(pos); + const light: [6]u8 = main.renderer.mesh_storage.getLight(blockPos[0], blockPos[1], blockPos[2]) orelse @splat(0); + bindLightUniform(light, ambientLight); + pos -= playerPos; + + const model = getModel(item); + var vertices: u31 = 36; + + var scale: f32 = 0.3; + var blockType: u16 = 0; + if (item == .baseItem and item.baseItem.block() != null and item.baseItem.image().imageData.ptr == graphics.Image.defaultImage.imageData.ptr) { + blockType = item.baseItem.block().?; + vertices = model.len/2*6; + } else { + scale = 0.5; + } + bindModelUniforms(model.index, blockType); + + var modelMatrix = Mat4f.translation(@floatCast(pos)); + modelMatrix = modelMatrix.mul(Mat4f.rotationX(-rot[0])); + modelMatrix = modelMatrix.mul(Mat4f.rotationY(-rot[1])); + modelMatrix = modelMatrix.mul(Mat4f.rotationZ(-rot[2])); + modelMatrix = modelMatrix.mul(Mat4f.scale(@splat(scale))); + modelMatrix = modelMatrix.mul(Mat4f.translation(@splat(-0.5))); + drawItem(vertices, modelMatrix); + } + } + } + + inline fn getIndex(x: u8, y: u8, z: u8) u32 { + return (z*4) + (y*2) + (x); + } + + inline fn blendColors(a: [6]f32, b: [6]f32, t: f32) [6]f32 { + var result: [6]f32 = .{0, 0, 0, 0, 0, 0}; + inline for (0..6) |i| { + result[i] = std.math.lerp(a[i], b[i], t); + } + return result; + } + + pub fn renderDisplayItems(ambientLight: Vec3f, playerPos: Vec3d) void { + if (!ItemDisplayManager.showItem) return; + + const projMatrix: Mat4f = Mat4f.perspective(std.math.degreesToRadians(65), @as(f32, @floatFromInt(main.renderer.lastWidth))/@as(f32, @floatFromInt(main.renderer.lastHeight)), 0.01, 3); + const viewMatrix = Mat4f.identity(); + bindCommonUniforms(projMatrix, viewMatrix, ambientLight); + + const item = game.Player.inventory.getItem(game.Player.selectedSlot); + if (item != .null) { + var pos: Vec3d = Vec3d{0, 0, 0}; + const rot: Vec3f = ItemDisplayManager.cameraFollow; + + const lightPos = @as(Vec3d, @floatCast(playerPos)) - @as(Vec3f, @splat(0.5)); + const blockPos: Vec3i = @floor(lightPos); + const localBlockPos: Vec3f = @floatCast(lightPos - @as(Vec3d, @floatFromInt(blockPos))); + + var samples: [8][6]f32 = @splat(@splat(0)); + inline for (0..2) |z| { + inline for (0..2) |y| { + inline for (0..2) |x| { + const light: [6]u8 = main.renderer.mesh_storage.getLight( + blockPos[0] +% @as(i32, @intCast(x)), + blockPos[1] +% @as(i32, @intCast(y)), + blockPos[2] +% @as(i32, @intCast(z)), + ) orelse @splat(0); + + inline for (0..6) |i| { + samples[getIndex(x, y, z)][i] = @as(f32, @floatFromInt(light[i])); + } + } + } + } + + inline for (0..2) |y| { + inline for (0..2) |x| { + samples[getIndex(x, y, 0)] = blendColors(samples[getIndex(x, y, 0)], samples[getIndex(x, y, 1)], localBlockPos[2]); + } + } + + inline for (0..2) |x| { + samples[getIndex(x, 0, 0)] = blendColors(samples[getIndex(x, 0, 0)], samples[getIndex(x, 1, 0)], localBlockPos[1]); + } + + var result: [6]u8 = .{0, 0, 0, 0, 0, 0}; + inline for (0..6) |i| { + const val = std.math.lerp(samples[getIndex(0, 0, 0)][i], samples[getIndex(1, 0, 0)][i], localBlockPos[0]); + result[i] = @floor(val); + } + + bindLightUniform(result, ambientLight); + + const model = getModel(item); + var vertices: u31 = 36; + + const isBlock: bool = item == .baseItem and item.baseItem.block() != null and item.baseItem.image().imageData.ptr == graphics.Image.defaultImage.imageData.ptr; + var scale: f32 = 0; + var blockType: u16 = 0; + if (isBlock) { + blockType = item.baseItem.block().?; + vertices = model.len/2*6; + scale = 0.3; + pos = Vec3d{0.4, 0.55, -0.32}; + } else { + scale = 0.57; + pos = Vec3d{0.4, 0.65, -0.3}; + } + bindModelUniforms(model.index, blockType); + + var modelMatrix = Mat4f.rotationZ(-rot[2]); + modelMatrix = modelMatrix.mul(Mat4f.rotationY(-rot[1])); + modelMatrix = modelMatrix.mul(Mat4f.rotationX(-rot[0])); + modelMatrix = modelMatrix.mul(Mat4f.translation(@floatCast(pos))); + if (!isBlock) { + if (item == .proceduralItem) { + modelMatrix = modelMatrix.mul(Mat4f.rotationZ(-std.math.pi*0.47)); + modelMatrix = modelMatrix.mul(Mat4f.rotationY(std.math.pi*0.25)); + } else { + modelMatrix = modelMatrix.mul(Mat4f.rotationZ(-std.math.pi*0.45)); + } + } else { + modelMatrix = modelMatrix.mul(Mat4f.rotationZ(-std.math.pi*0.2)); + } + modelMatrix = modelMatrix.mul(Mat4f.scale(@splat(scale))); + modelMatrix = modelMatrix.mul(Mat4f.translation(@splat(-0.5))); + drawItem(vertices, modelMatrix); + } + } }; diff --git a/src/particles.zig b/src/particles.zig index 6487779cc8..2c759d1c25 100644 --- a/src/particles.zig +++ b/src/particles.zig @@ -24,140 +24,140 @@ const Vec2f = vec.Vec2f; const c = @import("c"); pub const ParticleManager = struct { - var particleTypesSSBO: SSBO = undefined; - var types: main.ListUnmanaged(ParticleType) = .{}; - var typesLocal: main.ListUnmanaged(ParticleTypeLocal) = .{}; - var textures: main.ListUnmanaged(Image) = .{}; - var emissionTextures: main.ListUnmanaged(Image) = .{}; - - var textureArray: TextureArray = undefined; - var emissionTextureArray: TextureArray = undefined; - - const ParticleIndex = u16; - var particleTypeHashmap: std.StringHashMapUnmanaged(ParticleIndex) = .{}; - - pub fn init() void { - textureArray = .init(); - emissionTextureArray = .init(); - particleTypesSSBO = SSBO.init(); - ParticleSystem.init(); - } - - pub fn deinit() void { - textureArray.deinit(); - emissionTextureArray.deinit(); - ParticleSystem.deinit(); - particleTypesSSBO.deinit(); - } - - pub fn reset() void { - types = .{}; - typesLocal = .{}; - textures = .{}; - emissionTextures = .{}; - particleTypeHashmap = .{}; - ParticleSystem.reset(); - } - - pub fn register(assetsFolder: []const u8, id: []const u8, zon: ZonElement) void { - const textureId = zon.get(?[]const u8, "texture", null) orelse { - std.log.err("Particle texture id was not specified for {s} ({s})", .{ id, assetsFolder }); - return; - }; - - const particleType = readTextureDataAndParticleType(assetsFolder, textureId); - var rotVel: RandomRange(f32) = RandomRange(f32).fromZon(zon.getChild("rotationVelocity")) orelse .init(20, 60); - rotVel.min = std.math.degreesToRadians(rotVel.min); - rotVel.max = std.math.degreesToRadians(rotVel.max); - const particleTypeLocal = ParticleTypeLocal{ - .density = RandomRange(f32).fromZon(zon.getChild("density")) orelse .init(2, 3), - .rotVel = rotVel, - .dragCoefficient = RandomRange(f32).fromZon(zon.getChild("dragCoefficient")) orelse .init(0.5, 0.6), - }; - - particleTypeHashmap.put(main.worldArena.allocator, id, @intCast(types.items.len)) catch unreachable; - types.append(main.worldArena, particleType); - typesLocal.append(main.worldArena, particleTypeLocal); - - std.log.debug("Registered particle type: {s}", .{id}); - } - - fn readTextureDataAndParticleType(assetsFolder: []const u8, textureId: []const u8) ParticleType { - var typ: ParticleType = undefined; - - const base = readTexture(assetsFolder, textureId, ".png", Image.defaultImage, .isMandatory); - const emission = readTexture(assetsFolder, textureId, "_emission.png", Image.emptyImage, .isOptional); - const hasEmission = (emission.imageData.ptr != Image.emptyImage.imageData.ptr); - const baseAnimationFrameCount = base.height / base.width; - const emissionAnimationFrameCount = emission.height / emission.width; - - typ.frameCount = @floatFromInt(baseAnimationFrameCount); - typ.startFrame = @floatFromInt(textures.items.len); - typ.size = @as(f32, @floatFromInt(base.width)) / 16; - - var isBaseBroken = false; - var isEmissionBroken = false; - - if (base.height % base.width != 0) { - std.log.err("Particle base texture has incorrect dimensions ({}x{}) expected height to be multiple of width for {s} ({s})", .{ base.width, base.height, textureId, assetsFolder }); - isBaseBroken = true; - } - if (hasEmission and emission.height % emission.width != 0) { - std.log.err("Particle emission texture has incorrect dimensions ({}x{}) expected height to be multiple of width for {s} ({s})", .{ base.width, base.height, textureId, assetsFolder }); - isEmissionBroken = true; - } - if (hasEmission and baseAnimationFrameCount != emissionAnimationFrameCount) { - std.log.err("Particle base texture and emission texture frame count mismatch ({} vs {}) for {s} ({s})", .{ baseAnimationFrameCount, emissionAnimationFrameCount, textureId, assetsFolder }); - isEmissionBroken = true; - } - - createAnimationFrames(&textures, baseAnimationFrameCount, base, isBaseBroken); - createAnimationFrames(&emissionTextures, baseAnimationFrameCount, emission, isBaseBroken or isEmissionBroken or !hasEmission); - - return typ; - } - - fn readTexture(assetsFolder: []const u8, textureId: []const u8, suffix: []const u8, default: graphics.Image, status: enum { isOptional, isMandatory }) graphics.Image { - var splitter = std.mem.splitScalar(u8, textureId, ':'); - const mod = splitter.first(); - const id = splitter.rest(); - - const gameAssetsPath = std.fmt.allocPrint(main.stackAllocator.allocator, "assets/{s}/particles/textures/{s}{s}", .{ mod, id, suffix }) catch unreachable; - defer main.stackAllocator.free(gameAssetsPath); - - const worldAssetsPath = std.fmt.allocPrint(main.stackAllocator.allocator, "{s}/{s}/particles/textures/{s}{s}", .{ assetsFolder, mod, id, suffix }) catch unreachable; - defer main.stackAllocator.free(worldAssetsPath); - - return graphics.Image.readFromFile(main.worldArena, worldAssetsPath) catch graphics.Image.readFromFile(main.worldArena, gameAssetsPath) catch { - if (status == .isMandatory) std.log.err("Particle texture not found in {s} and {s}.", .{ worldAssetsPath, gameAssetsPath }); - return default; - }; - } - - fn createAnimationFrames(container: *main.ListUnmanaged(Image), frameCount: usize, image: Image, isBroken: bool) void { - for (0..frameCount) |i| { - container.append(main.worldArena, if (isBroken) image else extractAnimationSlice(image, i)); - } - } - - fn extractAnimationSlice(image: Image, frameIndex: usize) Image { - const frameCount = image.height / image.width; - const frameHeight = image.height / frameCount; - const startHeight = frameHeight * frameIndex; - const endHeight = frameHeight * (frameIndex + 1); - var result = image; - result.height = @intCast(frameHeight); - result.imageData = result.imageData[startHeight * image.width .. endHeight * image.width]; - return result; - } - - pub fn generateTextureArray() void { - textureArray.generate(textures.items, true, true); - emissionTextureArray.generate(emissionTextures.items, true, false); - - particleTypesSSBO.bufferData(ParticleType, ParticleManager.types.items); - particleTypesSSBO.bind(14); - } + var particleTypesSSBO: SSBO = undefined; + var types: main.ListUnmanaged(ParticleType) = .{}; + var typesLocal: main.ListUnmanaged(ParticleTypeLocal) = .{}; + var textures: main.ListUnmanaged(Image) = .{}; + var emissionTextures: main.ListUnmanaged(Image) = .{}; + + var textureArray: TextureArray = undefined; + var emissionTextureArray: TextureArray = undefined; + + const ParticleIndex = u16; + var particleTypeHashmap: std.StringHashMapUnmanaged(ParticleIndex) = .{}; + + pub fn init() void { + textureArray = .init(); + emissionTextureArray = .init(); + particleTypesSSBO = SSBO.init(); + ParticleSystem.init(); + } + + pub fn deinit() void { + textureArray.deinit(); + emissionTextureArray.deinit(); + ParticleSystem.deinit(); + particleTypesSSBO.deinit(); + } + + pub fn reset() void { + types = .{}; + typesLocal = .{}; + textures = .{}; + emissionTextures = .{}; + particleTypeHashmap = .{}; + ParticleSystem.reset(); + } + + pub fn register(assetsFolder: []const u8, id: []const u8, zon: ZonElement) void { + const textureId = zon.get(?[]const u8, "texture", null) orelse { + std.log.err("Particle texture id was not specified for {s} ({s})", .{id, assetsFolder}); + return; + }; + + const particleType = readTextureDataAndParticleType(assetsFolder, textureId); + var rotVel: RandomRange(f32) = RandomRange(f32).fromZon(zon.getChild("rotationVelocity")) orelse .init(20, 60); + rotVel.min = std.math.degreesToRadians(rotVel.min); + rotVel.max = std.math.degreesToRadians(rotVel.max); + const particleTypeLocal = ParticleTypeLocal{ + .density = RandomRange(f32).fromZon(zon.getChild("density")) orelse .init(2, 3), + .rotVel = rotVel, + .dragCoefficient = RandomRange(f32).fromZon(zon.getChild("dragCoefficient")) orelse .init(0.5, 0.6), + }; + + particleTypeHashmap.put(main.worldArena.allocator, id, @intCast(types.items.len)) catch unreachable; + types.append(main.worldArena, particleType); + typesLocal.append(main.worldArena, particleTypeLocal); + + std.log.debug("Registered particle type: {s}", .{id}); + } + + fn readTextureDataAndParticleType(assetsFolder: []const u8, textureId: []const u8) ParticleType { + var typ: ParticleType = undefined; + + const base = readTexture(assetsFolder, textureId, ".png", Image.defaultImage, .isMandatory); + const emission = readTexture(assetsFolder, textureId, "_emission.png", Image.emptyImage, .isOptional); + const hasEmission = (emission.imageData.ptr != Image.emptyImage.imageData.ptr); + const baseAnimationFrameCount = base.height/base.width; + const emissionAnimationFrameCount = emission.height/emission.width; + + typ.frameCount = @floatFromInt(baseAnimationFrameCount); + typ.startFrame = @floatFromInt(textures.items.len); + typ.size = @as(f32, @floatFromInt(base.width))/16; + + var isBaseBroken = false; + var isEmissionBroken = false; + + if (base.height%base.width != 0) { + std.log.err("Particle base texture has incorrect dimensions ({}x{}) expected height to be multiple of width for {s} ({s})", .{base.width, base.height, textureId, assetsFolder}); + isBaseBroken = true; + } + if (hasEmission and emission.height%emission.width != 0) { + std.log.err("Particle emission texture has incorrect dimensions ({}x{}) expected height to be multiple of width for {s} ({s})", .{base.width, base.height, textureId, assetsFolder}); + isEmissionBroken = true; + } + if (hasEmission and baseAnimationFrameCount != emissionAnimationFrameCount) { + std.log.err("Particle base texture and emission texture frame count mismatch ({} vs {}) for {s} ({s})", .{baseAnimationFrameCount, emissionAnimationFrameCount, textureId, assetsFolder}); + isEmissionBroken = true; + } + + createAnimationFrames(&textures, baseAnimationFrameCount, base, isBaseBroken); + createAnimationFrames(&emissionTextures, baseAnimationFrameCount, emission, isBaseBroken or isEmissionBroken or !hasEmission); + + return typ; + } + + fn readTexture(assetsFolder: []const u8, textureId: []const u8, suffix: []const u8, default: graphics.Image, status: enum { isOptional, isMandatory }) graphics.Image { + var splitter = std.mem.splitScalar(u8, textureId, ':'); + const mod = splitter.first(); + const id = splitter.rest(); + + const gameAssetsPath = std.fmt.allocPrint(main.stackAllocator.allocator, "assets/{s}/particles/textures/{s}{s}", .{mod, id, suffix}) catch unreachable; + defer main.stackAllocator.free(gameAssetsPath); + + const worldAssetsPath = std.fmt.allocPrint(main.stackAllocator.allocator, "{s}/{s}/particles/textures/{s}{s}", .{assetsFolder, mod, id, suffix}) catch unreachable; + defer main.stackAllocator.free(worldAssetsPath); + + return graphics.Image.readFromFile(main.worldArena, worldAssetsPath) catch graphics.Image.readFromFile(main.worldArena, gameAssetsPath) catch { + if (status == .isMandatory) std.log.err("Particle texture not found in {s} and {s}.", .{worldAssetsPath, gameAssetsPath}); + return default; + }; + } + + fn createAnimationFrames(container: *main.ListUnmanaged(Image), frameCount: usize, image: Image, isBroken: bool) void { + for (0..frameCount) |i| { + container.append(main.worldArena, if (isBroken) image else extractAnimationSlice(image, i)); + } + } + + fn extractAnimationSlice(image: Image, frameIndex: usize) Image { + const frameCount = image.height/image.width; + const frameHeight = image.height/frameCount; + const startHeight = frameHeight*frameIndex; + const endHeight = frameHeight*(frameIndex + 1); + var result = image; + result.height = @intCast(frameHeight); + result.imageData = result.imageData[startHeight*image.width .. endHeight*image.width]; + return result; + } + + pub fn generateTextureArray() void { + textureArray.generate(textures.items, true, true); + emissionTextureArray.generate(emissionTextures.items, true, false); + + particleTypesSSBO.bufferData(ParticleType, ParticleManager.types.items); + particleTypesSSBO.bind(14); + } }; pub const ParticleSystem = struct { @@ -364,206 +364,206 @@ pub const ParticleSystem = struct { }; pub const EmitterProperties = struct { - speed: RandomRange(f32), - lifeTime: RandomRange(f32), - randomizeRotation: bool, - - pub fn parse(zon: ZonElement) EmitterProperties { - return EmitterProperties{ - .speed = RandomRange(f32).fromZon(zon.getChild("speed")) orelse .init(1, 1.5), - .lifeTime = RandomRange(f32).fromZon(zon.getChild("lifeTime")) orelse .init(0.75, 1), - .randomizeRotation = zon.get(bool, "randomRotate", true), - }; - } + speed: RandomRange(f32), + lifeTime: RandomRange(f32), + randomizeRotation: bool, + + pub fn parse(zon: ZonElement) EmitterProperties { + return EmitterProperties{ + .speed = RandomRange(f32).fromZon(zon.getChild("speed")) orelse .init(1, 1.5), + .lifeTime = RandomRange(f32).fromZon(zon.getChild("lifeTime")) orelse .init(0.75, 1), + .randomizeRotation = zon.get(bool, "randomRotate", true), + }; + } }; pub const DirectionMode = union(enum) { - // The particle goes in the direction away from the center - spread: void, - // The particle goes in a random direction - scatter: void, - // The particle goes in the specified direction - direction: Vec3f, - - pub fn parse(zon: ZonElement) !DirectionMode { - const dirModeName = zon.get([]const u8, "mode", @tagName(DirectionMode.spread)); - const dirMode = std.meta.stringToEnum(std.meta.Tag(DirectionMode), dirModeName) orelse return error.InvalidDirectionMode; - return switch (dirMode) { - .direction => .{ .direction = zon.get(Vec3f, "direction", .{ 0, 0, 1 }) }, - inline else => |mode| @unionInit(DirectionMode, @tagName(mode), {}), - }; - } + // The particle goes in the direction away from the center + spread: void, + // The particle goes in a random direction + scatter: void, + // The particle goes in the specified direction + direction: Vec3f, + + pub fn parse(zon: ZonElement) !DirectionMode { + const dirModeName = zon.get([]const u8, "mode", @tagName(DirectionMode.spread)); + const dirMode = std.meta.stringToEnum(std.meta.Tag(DirectionMode), dirModeName) orelse return error.InvalidDirectionMode; + return switch (dirMode) { + .direction => .{.direction = zon.get(Vec3f, "direction", .{0, 0, 1})}, + inline else => |mode| @unionInit(DirectionMode, @tagName(mode), {}), + }; + } }; pub const Emitter = struct { - typ: u16 = 0, - particleType: ParticleTypeLocal, - collides: bool, - spawnShape: SpawnShape, - properties: EmitterProperties, - mode: DirectionMode, - - pub const SpawnShape = union(enum) { - point: SpawnPoint, - sphere: SpawnSphere, - cube: SpawnCube, - - pub fn spawn(self: SpawnShape, pos: Vec3d, properties: EmitterProperties, mode: DirectionMode) struct { Vec3d, Vec3f } { - return switch (self) { - inline else => |shape| shape.spawn(pos, properties, mode), - }; - } - - pub fn parse(zon: ZonElement) !SpawnShape { - const typeZon = zon.get([]const u8, "shape", @tagName(SpawnShape.point)); - const spawnType = std.meta.stringToEnum(std.meta.Tag(SpawnShape), typeZon) orelse return error.InvalidType; - return switch (spawnType) { - inline else => |shape| @unionInit(SpawnShape, @tagName(shape), try @FieldType(SpawnShape, @tagName(shape)).parse(zon)), - }; - } - }; - - pub const SpawnPoint = struct { - pub fn spawn(_: SpawnPoint, pos: Vec3d, properties: EmitterProperties, mode: DirectionMode) struct { Vec3d, Vec3f } { - const particlePos = pos; - const speed: Vec3f = @splat(properties.speed.get(&main.seed)); - const dir: Vec3f = switch (mode) { - .direction => |dir| vec.normalize(dir), - .scatter, .spread => vec.normalize(random.nextFloatVectorSigned(3, &main.seed)), - }; - const particleVel = dir * speed; - - return .{ particlePos, particleVel }; - } - - pub fn parse(_: ZonElement) !SpawnPoint { - return SpawnPoint{}; - } - }; - - pub const SpawnSphere = struct { - radius: f32, - - pub fn spawn(self: SpawnSphere, pos: Vec3d, properties: EmitterProperties, mode: DirectionMode) struct { Vec3d, Vec3f } { - const spawnPos: Vec3f = @splat(self.radius); - var offsetPos: Vec3f = undefined; - while (true) { - offsetPos = random.nextFloatVectorSigned(3, &main.seed); - if (vec.lengthSquare(offsetPos) <= 1) break; - } - const particlePos = pos + @as(Vec3d, @floatCast(offsetPos * spawnPos)); - const speed: Vec3f = @splat(properties.speed.get(&main.seed)); - const dir: Vec3f = switch (mode) { - .direction => |dir| vec.normalize(dir), - .scatter => vec.normalize(random.nextFloatVectorSigned(3, &main.seed)), - .spread => @floatCast(offsetPos), - }; - const particleVel = dir * speed; - - return .{ particlePos, particleVel }; - } - - pub fn parse(zon: ZonElement) !SpawnSphere { - return SpawnSphere{ - .radius = zon.get(f32, "radius", 1), - }; - } - }; - - pub const SpawnCube = struct { - size: Vec3f, - - pub fn spawn(self: SpawnCube, pos: Vec3d, properties: EmitterProperties, mode: DirectionMode) struct { Vec3d, Vec3f } { - const spawnPos: Vec3f = self.size; - const offsetPos: Vec3f = random.nextFloatVectorSigned(3, &main.seed); - const particlePos = pos + @as(Vec3d, @floatCast(offsetPos * spawnPos)); - const speed: Vec3f = @splat(properties.speed.get(&main.seed)); - const dir: Vec3f = switch (mode) { - .direction => |dir| vec.normalize(dir), - .scatter => vec.normalize(random.nextFloatVectorSigned(3, &main.seed)), - .spread => vec.normalize(@as(Vec3f, @floatCast(offsetPos))), - }; - const particleVel = dir * speed; - - return .{ particlePos, particleVel }; - } - - pub fn parse(zon: ZonElement) !SpawnCube { - return SpawnCube{ - .size = zon.get(?Vec3f, "size", null) orelse @splat(zon.get(f32, "size", 1)), - }; - } - }; - - pub fn init(id: []const u8, collides: bool, spawnShape: SpawnShape, properties: EmitterProperties, mode: DirectionMode) Emitter { - const typ = ParticleManager.particleTypeHashmap.get(id) orelse 0; - - return Emitter{ - .typ = typ, - .particleType = ParticleManager.typesLocal.items[typ], - .collides = collides, - .spawnShape = spawnShape, - .properties = properties, - .mode = mode, - }; - } - - pub fn initFromZon(id: []const u8, collides: bool, zon: ZonElement) Emitter { - const typ = ParticleManager.particleTypeHashmap.get(id) orelse 0; - const mode = DirectionMode.parse(zon) catch |err| blk: { - std.log.err("Error while parsing direction mode: \"{s}\"", .{@errorName(err)}); - break :blk .spread; - }; - const spawnShape = Emitter.SpawnShape.parse(zon) catch |err| blk: { - std.log.err("Error while parsing particle spawn data: \"{s}\"", .{@errorName(err)}); - break :blk Emitter.SpawnShape{ .point = .{} }; - }; - - return Emitter{ - .typ = typ, - .particleType = ParticleManager.typesLocal.items[typ], - .collides = collides, - .spawnShape = spawnShape, - .properties = EmitterProperties.parse(zon), - .mode = mode, - }; - } - - pub fn spawnParticles(self: Emitter, pos: Vec3d, spawnCount: u32) void { - const count = @min(spawnCount, ParticleSystem.maxCapacity - ParticleSystem.particleCount); - for (0..count) |_| { - const particlePos, const particleVel = self.spawnShape.spawn(pos, self.properties, self.mode); - - ParticleSystem.addParticle(self.typ, self.particleType, particlePos, particleVel, self.collides, self.properties); - } - } + typ: u16 = 0, + particleType: ParticleTypeLocal, + collides: bool, + spawnShape: SpawnShape, + properties: EmitterProperties, + mode: DirectionMode, + + pub const SpawnShape = union(enum) { + point: SpawnPoint, + sphere: SpawnSphere, + cube: SpawnCube, + + pub fn spawn(self: SpawnShape, pos: Vec3d, properties: EmitterProperties, mode: DirectionMode) struct { Vec3d, Vec3f } { + return switch (self) { + inline else => |shape| shape.spawn(pos, properties, mode), + }; + } + + pub fn parse(zon: ZonElement) !SpawnShape { + const typeZon = zon.get([]const u8, "shape", @tagName(SpawnShape.point)); + const spawnType = std.meta.stringToEnum(std.meta.Tag(SpawnShape), typeZon) orelse return error.InvalidType; + return switch (spawnType) { + inline else => |shape| @unionInit(SpawnShape, @tagName(shape), try @FieldType(SpawnShape, @tagName(shape)).parse(zon)), + }; + } + }; + + pub const SpawnPoint = struct { + pub fn spawn(_: SpawnPoint, pos: Vec3d, properties: EmitterProperties, mode: DirectionMode) struct { Vec3d, Vec3f } { + const particlePos = pos; + const speed: Vec3f = @splat(properties.speed.get(&main.seed)); + const dir: Vec3f = switch (mode) { + .direction => |dir| vec.normalize(dir), + .scatter, .spread => vec.normalize(random.nextFloatVectorSigned(3, &main.seed)), + }; + const particleVel = dir*speed; + + return .{particlePos, particleVel}; + } + + pub fn parse(_: ZonElement) !SpawnPoint { + return SpawnPoint{}; + } + }; + + pub const SpawnSphere = struct { + radius: f32, + + pub fn spawn(self: SpawnSphere, pos: Vec3d, properties: EmitterProperties, mode: DirectionMode) struct { Vec3d, Vec3f } { + const spawnPos: Vec3f = @splat(self.radius); + var offsetPos: Vec3f = undefined; + while (true) { + offsetPos = random.nextFloatVectorSigned(3, &main.seed); + if (vec.lengthSquare(offsetPos) <= 1) break; + } + const particlePos = pos + @as(Vec3d, @floatCast(offsetPos*spawnPos)); + const speed: Vec3f = @splat(properties.speed.get(&main.seed)); + const dir: Vec3f = switch (mode) { + .direction => |dir| vec.normalize(dir), + .scatter => vec.normalize(random.nextFloatVectorSigned(3, &main.seed)), + .spread => @floatCast(offsetPos), + }; + const particleVel = dir*speed; + + return .{particlePos, particleVel}; + } + + pub fn parse(zon: ZonElement) !SpawnSphere { + return SpawnSphere{ + .radius = zon.get(f32, "radius", 1), + }; + } + }; + + pub const SpawnCube = struct { + size: Vec3f, + + pub fn spawn(self: SpawnCube, pos: Vec3d, properties: EmitterProperties, mode: DirectionMode) struct { Vec3d, Vec3f } { + const spawnPos: Vec3f = self.size; + const offsetPos: Vec3f = random.nextFloatVectorSigned(3, &main.seed); + const particlePos = pos + @as(Vec3d, @floatCast(offsetPos*spawnPos)); + const speed: Vec3f = @splat(properties.speed.get(&main.seed)); + const dir: Vec3f = switch (mode) { + .direction => |dir| vec.normalize(dir), + .scatter => vec.normalize(random.nextFloatVectorSigned(3, &main.seed)), + .spread => vec.normalize(@as(Vec3f, @floatCast(offsetPos))), + }; + const particleVel = dir*speed; + + return .{particlePos, particleVel}; + } + + pub fn parse(zon: ZonElement) !SpawnCube { + return SpawnCube{ + .size = zon.get(?Vec3f, "size", null) orelse @splat(zon.get(f32, "size", 1)), + }; + } + }; + + pub fn init(id: []const u8, collides: bool, spawnShape: SpawnShape, properties: EmitterProperties, mode: DirectionMode) Emitter { + const typ = ParticleManager.particleTypeHashmap.get(id) orelse 0; + + return Emitter{ + .typ = typ, + .particleType = ParticleManager.typesLocal.items[typ], + .collides = collides, + .spawnShape = spawnShape, + .properties = properties, + .mode = mode, + }; + } + + pub fn initFromZon(id: []const u8, collides: bool, zon: ZonElement) Emitter { + const typ = ParticleManager.particleTypeHashmap.get(id) orelse 0; + const mode = DirectionMode.parse(zon) catch |err| blk: { + std.log.err("Error while parsing direction mode: \"{s}\"", .{@errorName(err)}); + break :blk .spread; + }; + const spawnShape = Emitter.SpawnShape.parse(zon) catch |err| blk: { + std.log.err("Error while parsing particle spawn data: \"{s}\"", .{@errorName(err)}); + break :blk Emitter.SpawnShape{.point = .{}}; + }; + + return Emitter{ + .typ = typ, + .particleType = ParticleManager.typesLocal.items[typ], + .collides = collides, + .spawnShape = spawnShape, + .properties = EmitterProperties.parse(zon), + .mode = mode, + }; + } + + pub fn spawnParticles(self: Emitter, pos: Vec3d, spawnCount: u32) void { + const count = @min(spawnCount, ParticleSystem.maxCapacity - ParticleSystem.particleCount); + for (0..count) |_| { + const particlePos, const particleVel = self.spawnShape.spawn(pos, self.properties, self.mode); + + ParticleSystem.addParticle(self.typ, self.particleType, particlePos, particleVel, self.collides, self.properties); + } + } }; pub const ParticleType = struct { - frameCount: f32, - startFrame: f32, - size: f32, + frameCount: f32, + startFrame: f32, + size: f32, }; pub const ParticleTypeLocal = struct { - density: RandomRange(f32), - rotVel: RandomRange(f32), - dragCoefficient: RandomRange(f32), + density: RandomRange(f32), + rotVel: RandomRange(f32), + dragCoefficient: RandomRange(f32), }; pub const Particle = extern struct { - pos: [3]f32 align(16), - rot: f32 = 0, - lifeRatio: f32 = 1, - light: u32 = 0, - typ: u32, - // 4 bytes left for use + pos: [3]f32 align(16), + rot: f32 = 0, + lifeRatio: f32 = 1, + light: u32 = 0, + typ: u32, + // 4 bytes left for use }; pub const ParticleLocal = struct { - velAndRotationVel: Vec4f, - lifeVelocity: f32, - density: f32, - dragCoefficient: f32, - collides: bool, + velAndRotationVel: Vec4f, + lifeVelocity: f32, + density: f32, + dragCoefficient: f32, + collides: bool, }; diff --git a/src/renderer.zig b/src/renderer.zig index b469c8025e..750ef83fd2 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -37,24 +37,24 @@ pub const zFar = 65536.0; // TODO: Fix z-fighting problems. var deferredRenderPassPipeline: graphics.Pipeline = undefined; var deferredUniforms: struct { - @"fog.color": c_int, - @"fog.density": c_int, - @"fog.fogLower": c_int, - @"fog.fogHigher": c_int, - tanXY: c_int, - zNear: c_int, - zFar: c_int, - invViewMatrix: c_int, - playerPositionInteger: c_int, - playerPositionFraction: c_int, + @"fog.color": c_int, + @"fog.density": c_int, + @"fog.fogLower": c_int, + @"fog.fogHigher": c_int, + tanXY: c_int, + zNear: c_int, + zFar: c_int, + invViewMatrix: c_int, + playerPositionInteger: c_int, + playerPositionFraction: c_int, } = undefined; var fakeReflectionPipeline: graphics.Pipeline = undefined; var fakeReflectionUniforms: struct { - normalVector: c_int, - upVector: c_int, - rightVector: c_int, - frequency: c_int, - reflectionMapSize: c_int, + normalVector: c_int, + upVector: c_int, + rightVector: c_int, + frequency: c_int, + reflectionMapSize: c_int, } = undefined; pub var activeFrameBuffer: c_uint = 0; @@ -63,71 +63,71 @@ pub const reflectionCubeMapSize = 64; var reflectionCubeMap: graphics.CubeMapTexture = undefined; pub fn init() void { - deferredRenderPassPipeline = graphics.Pipeline.init( - "assets/cubyz/shaders/deferred_render_pass.vert", - "assets/cubyz/shaders/deferred_render_pass.frag", - "", - &deferredUniforms, - graphics.draw.SimpleVertex2D, - &.{}, - .{ .cullMode = .none }, - .{ .depthTest = false, .depthWrite = false }, - .{ .attachments = &.{.noBlending} }, - ); - fakeReflectionPipeline = graphics.Pipeline.init( - "assets/cubyz/shaders/fake_reflection.vert", - "assets/cubyz/shaders/fake_reflection.frag", - "", - &fakeReflectionUniforms, - graphics.draw.SimpleVertex2D, - &.{}, - .{ .cullMode = .none }, - .{ .depthTest = false, .depthWrite = false }, - .{ .attachments = &.{.noBlending} }, - ); - worldFrameBuffer.init(true, c.GL_NEAREST, c.GL_CLAMP_TO_EDGE); - worldFrameBuffer.updateSize(Window.width, Window.height, c.GL_RGB16F); - Bloom.init(); - MeshSelection.init(); - MenuBackGround.init(); - Skybox.init(); - chunk_meshing.init(); - mesh_storage.init(); - reflectionCubeMap = .init(); - reflectionCubeMap.generate(reflectionCubeMapSize, reflectionCubeMapSize); - initReflectionCubeMap(); + deferredRenderPassPipeline = graphics.Pipeline.init( + "assets/cubyz/shaders/deferred_render_pass.vert", + "assets/cubyz/shaders/deferred_render_pass.frag", + "", + &deferredUniforms, + graphics.draw.SimpleVertex2D, + &.{}, + .{.cullMode = .none}, + .{.depthTest = false, .depthWrite = false}, + .{.attachments = &.{.noBlending}}, + ); + fakeReflectionPipeline = graphics.Pipeline.init( + "assets/cubyz/shaders/fake_reflection.vert", + "assets/cubyz/shaders/fake_reflection.frag", + "", + &fakeReflectionUniforms, + graphics.draw.SimpleVertex2D, + &.{}, + .{.cullMode = .none}, + .{.depthTest = false, .depthWrite = false}, + .{.attachments = &.{.noBlending}}, + ); + worldFrameBuffer.init(true, c.GL_NEAREST, c.GL_CLAMP_TO_EDGE); + worldFrameBuffer.updateSize(Window.width, Window.height, c.GL_RGB16F); + Bloom.init(); + MeshSelection.init(); + MenuBackGround.init(); + Skybox.init(); + chunk_meshing.init(); + mesh_storage.init(); + reflectionCubeMap = .init(); + reflectionCubeMap.generate(reflectionCubeMapSize, reflectionCubeMapSize); + initReflectionCubeMap(); } pub fn deinit() void { - deferredRenderPassPipeline.deinit(); - fakeReflectionPipeline.deinit(); - worldFrameBuffer.deinit(); - Bloom.deinit(); - MeshSelection.deinit(); - MenuBackGround.deinit(); - Skybox.deinit(); - mesh_storage.deinit(); - chunk_meshing.deinit(); - reflectionCubeMap.deinit(); + deferredRenderPassPipeline.deinit(); + fakeReflectionPipeline.deinit(); + worldFrameBuffer.deinit(); + Bloom.deinit(); + MeshSelection.deinit(); + MenuBackGround.deinit(); + Skybox.deinit(); + mesh_storage.deinit(); + chunk_meshing.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(); - fakeReflectionPipeline.bind(null); - 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)); - graphics.draw.rectVao.bind(); - c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); - } + 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(); + fakeReflectionPipeline.bind(null); + 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)); + graphics.draw.rectVao.bind(); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + } } var worldFrameBuffer: graphics.FrameBuffer = undefined; @@ -136,1056 +136,1056 @@ pub var lastWidth: u31 = 0; pub var lastHeight: u31 = 0; var lastFov: f32 = 0; pub fn updateFov(fov: f32) void { - if (lastFov != fov) { - lastFov = fov; - game.projectionMatrix = Mat4f.perspective(std.math.degreesToRadians(fov), @as(f32, @floatFromInt(lastWidth)) / @as(f32, @floatFromInt(lastHeight)), zNear, zFar); - } + if (lastFov != fov) { + lastFov = fov; + game.projectionMatrix = Mat4f.perspective(std.math.degreesToRadians(fov), @as(f32, @floatFromInt(lastWidth))/@as(f32, @floatFromInt(lastHeight)), zNear, zFar); + } } pub fn updateViewport(width: u31, height: u31) void { - lastWidth = @trunc(@as(f32, @floatFromInt(width)) * main.settings.resolutionScale); - lastHeight = @trunc(@as(f32, @floatFromInt(height)) * main.settings.resolutionScale); - game.projectionMatrix = Mat4f.perspective(std.math.degreesToRadians(lastFov), @as(f32, @floatFromInt(lastWidth)) / @as(f32, @floatFromInt(lastHeight)), zNear, zFar); - worldFrameBuffer.updateSize(lastWidth, lastHeight, c.GL_RGB16F); - worldFrameBuffer.unbind(); + lastWidth = @trunc(@as(f32, @floatFromInt(width))*main.settings.resolutionScale); + lastHeight = @trunc(@as(f32, @floatFromInt(height))*main.settings.resolutionScale); + game.projectionMatrix = Mat4f.perspective(std.math.degreesToRadians(lastFov), @as(f32, @floatFromInt(lastWidth))/@as(f32, @floatFromInt(lastHeight)), zNear, zFar); + worldFrameBuffer.updateSize(lastWidth, lastHeight, c.GL_RGB16F); + worldFrameBuffer.unbind(); } pub fn render(playerPosition: Vec3d, deltaTime: f64) void { - // TODO: player bobbing - // TODO: Handle colors and sun position in the world. - std.debug.assert(game.world != null); + // TODO: player bobbing + // TODO: Handle colors and sun position in the world. + std.debug.assert(game.world != null); - const nightColor: Vec3f = .{ 0.3, 0.4, 0.5 }; - const ambient = @max(nightColor * @as(Vec3f, @splat(settings.nightBrightness)), @as(Vec3f, @splat(game.world.?.ambientLight))); + const nightColor: Vec3f = .{0.3, 0.4, 0.5}; + const ambient = @max(nightColor*@as(Vec3f, @splat(settings.nightBrightness)), @as(Vec3f, @splat(game.world.?.ambientLight))); - itemdrop.ItemDisplayManager.update(deltaTime); - renderWorld(game.world.?, ambient, game.fog.skyColor, playerPosition); - const startTime = main.timestamp(); - mesh_storage.updateMeshes(startTime.addDuration(maximumMeshTime)); + itemdrop.ItemDisplayManager.update(deltaTime); + renderWorld(game.world.?, ambient, game.fog.skyColor, playerPosition); + const startTime = main.timestamp(); + mesh_storage.updateMeshes(startTime.addDuration(maximumMeshTime)); } pub fn crosshairDirection(rotationMatrix: Mat4f, fovY: f32, width: u31, height: u31) Vec3f { - // stolen code from Frustum.init - const invRotationMatrix = rotationMatrix.transpose(); - const cameraDir = vec.xyz(invRotationMatrix.mulVec(Vec4f{ 0, 1, 0, 1 })); - const cameraUp = vec.xyz(invRotationMatrix.mulVec(Vec4f{ 0, 0, 1, 1 })); - const cameraRight = vec.xyz(invRotationMatrix.mulVec(Vec4f{ 1, 0, 0, 1 })); - - const screenSize = Vec2f{ @floatFromInt(width), @floatFromInt(height) }; - const screenCoord = (crosshair.window.pos + crosshair.window.contentSize * Vec2f{ 0.5, 0.5 } * @as(Vec2f, @splat(crosshair.window.scale))) * @as(Vec2f, @splat(main.gui.scale * main.settings.resolutionScale)); - - const halfVSide = std.math.tan(std.math.degreesToRadians(fovY) * 0.5); - const halfHSide = halfVSide * screenSize[0] / screenSize[1]; - const sides = Vec2f{ halfHSide, halfVSide }; - - const scale = (Vec2f{ -1, 1 } + Vec2f{ 2, -2 } * screenCoord / screenSize) * sides; - const forwards = cameraDir; - const horizontal = cameraRight * @as(Vec3f, @splat(scale[0])); - const vertical = cameraUp * @as(Vec3f, @splat(scale[1])); // adjust for y coordinate - - const adjusted = forwards + horizontal + vertical; - return adjusted; + // stolen code from Frustum.init + const invRotationMatrix = rotationMatrix.transpose(); + const cameraDir = vec.xyz(invRotationMatrix.mulVec(Vec4f{0, 1, 0, 1})); + const cameraUp = vec.xyz(invRotationMatrix.mulVec(Vec4f{0, 0, 1, 1})); + const cameraRight = vec.xyz(invRotationMatrix.mulVec(Vec4f{1, 0, 0, 1})); + + const screenSize = Vec2f{@floatFromInt(width), @floatFromInt(height)}; + const screenCoord = (crosshair.window.pos + crosshair.window.contentSize*Vec2f{0.5, 0.5}*@as(Vec2f, @splat(crosshair.window.scale)))*@as(Vec2f, @splat(main.gui.scale*main.settings.resolutionScale)); + + const halfVSide = std.math.tan(std.math.degreesToRadians(fovY)*0.5); + const halfHSide = halfVSide*screenSize[0]/screenSize[1]; + const sides = Vec2f{halfHSide, halfVSide}; + + const scale = (Vec2f{-1, 1} + Vec2f{2, -2}*screenCoord/screenSize)*sides; + const forwards = cameraDir; + const horizontal = cameraRight*@as(Vec3f, @splat(scale[0])); + const vertical = cameraUp*@as(Vec3f, @splat(scale[1])); // adjust for y coordinate + + const adjusted = forwards + horizontal + vertical; + return adjusted; } pub fn renderWorld(world: *World, ambientLight: Vec3f, skyColor: Vec3f, playerPos: Vec3d) void { // MARK: renderWorld() - worldFrameBuffer.bind(); - c.glViewport(0, 0, lastWidth, lastHeight); - 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); - - const time: u32 = @intCast(main.timestamp().toMilliseconds() & std.math.maxInt(u32)); - - gpu_performance_measuring.startQuery(.skybox); - Skybox.render(); - gpu_performance_measuring.stopQuery(); - - 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, playerPos); - - c.glActiveTexture(c.GL_TEXTURE0); - blocks.meshes.blockTextureArray.bind(); - c.glActiveTexture(c.GL_TEXTURE1); - blocks.meshes.emissionTextureArray.bind(); - c.glActiveTexture(c.GL_TEXTURE2); - blocks.meshes.reflectivityAndAbsorptionTextureArray.bind(); - c.glActiveTexture(c.GL_TEXTURE5); - blocks.meshes.ditherTexture.bind(); - reflectionCubeMap.bindTo(4); - - chunk_meshing.quadsDrawn = 0; - chunk_meshing.transparentQuadsDrawn = 0; - const meshes = mesh_storage.updateAndGetRenderChunks(world.conn, &frustum, playerPos, settings.renderDistance); - - gpu_performance_measuring.startQuery(.chunk_rendering_preparation); - const direction = crosshairDirection(game.camera.viewMatrix, lastFov, lastWidth, lastHeight); - MeshSelection.select(playerPos, direction, game.Player.inventory.getItem(game.Player.selectedSlot)); - - chunk_meshing.beginRender(); - - var chunkLists: [main.settings.highestSupportedLod + 1]main.List(u32) = @splat(main.List(u32).init(main.stackAllocator)); - defer for (chunkLists) |list| list.deinit(); - for (meshes) |mesh| { - mesh.prepareRendering(&chunkLists); - } - gpu_performance_measuring.stopQuery(); - gpu_performance_measuring.startQuery(.chunk_rendering); - chunk_meshing.drawChunksIndirect(&chunkLists, game.projectionMatrix, ambientLight, playerPos, false); - gpu_performance_measuring.stopQuery(); - - gpu_performance_measuring.startQuery(.entity_rendering); - main.entity.client.render(game.projectionMatrix, ambientLight, playerPos, main.lastDeltaTime.load(.monotonic)); - - itemdrop.ItemDropRenderer.renderItemDrops(game.projectionMatrix, ambientLight, playerPos); - gpu_performance_measuring.stopQuery(); - - gpu_performance_measuring.startQuery(.block_entity_rendering); - main.block_entity.renderAll(game.projectionMatrix, ambientLight, playerPos); - gpu_performance_measuring.stopQuery(); - - gpu_performance_measuring.startQuery(.particle_rendering); - particles.ParticleSystem.render(game.projectionMatrix, game.camera.viewMatrix, ambientLight); - gpu_performance_measuring.stopQuery(); - - // Rebind block textures back to their original slots - c.glActiveTexture(c.GL_TEXTURE0); - blocks.meshes.blockTextureArray.bind(); - c.glActiveTexture(c.GL_TEXTURE1); - blocks.meshes.emissionTextureArray.bind(); - - MeshSelection.render(game.projectionMatrix, game.camera.viewMatrix, playerPos); - - // Render transparent chunk meshes: - worldFrameBuffer.bindDepthTexture(c.GL_TEXTURE5); - - gpu_performance_measuring.startQuery(.transparent_rendering_preparation); - c.glTextureBarrier(); - - { - for (&chunkLists) |*list| list.clearRetainingCapacity(); - var i: usize = meshes.len; - while (true) { - if (i == 0) break; - i -= 1; - meshes[i].prepareTransparentRendering(playerPos, &chunkLists); - } - gpu_performance_measuring.stopQuery(); - gpu_performance_measuring.startQuery(.transparent_rendering); - chunk_meshing.drawChunksIndirect(&chunkLists, game.projectionMatrix, ambientLight, playerPos, true); - gpu_performance_measuring.stopQuery(); - } - - c.glDepthRange(0, 0.001); - itemdrop.ItemDropRenderer.renderDisplayItems(ambientLight, playerPos); - c.glDepthRange(0.001, 1); - - chunk_meshing.endRender(); - - worldFrameBuffer.bindTexture(c.GL_TEXTURE3); - - const playerBlock = mesh_storage.getBlockFromAnyLodFromRenderThread(@floor(playerPos[0]), @floor(playerPos[1]), @floor(playerPos[2])); - - if (settings.bloom) { - Bloom.render(lastWidth, lastHeight, playerBlock, playerPos, game.camera.viewMatrix); - } else { - Bloom.bindReplacementImage(); - } - gpu_performance_measuring.startQuery(.final_copy); - if (activeFrameBuffer == 0) c.glViewport(0, 0, main.Window.width, main.Window.height); - worldFrameBuffer.bindTexture(c.GL_TEXTURE3); - worldFrameBuffer.bindDepthTexture(c.GL_TEXTURE4); - worldFrameBuffer.unbind(); - deferredRenderPassPipeline.bind(null); - if (!blocks.meshes.hasFog(playerBlock)) { - c.glUniform3fv(deferredUniforms.@"fog.color", 1, @ptrCast(&game.fog.fogColor)); - c.glUniform1f(deferredUniforms.@"fog.density", game.fog.density); - c.glUniform1f(deferredUniforms.@"fog.fogLower", game.fog.fogLower); - c.glUniform1f(deferredUniforms.@"fog.fogHigher", game.fog.fogHigher); - } 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.@"fog.fogLower", 1e10); - c.glUniform1f(deferredUniforms.@"fog.fogHigher", 1e10); - } - c.glUniformMatrix4fv(deferredUniforms.invViewMatrix, 1, c.GL_TRUE, @ptrCast(&game.camera.viewMatrix.transpose())); - c.glUniform3i(deferredUniforms.playerPositionInteger, @floor(playerPos[0]), @floor(playerPos[1]), @floor(playerPos[2])); - c.glUniform3f(deferredUniforms.playerPositionFraction, @floatCast(@mod(playerPos[0], 1)), @floatCast(@mod(playerPos[1], 1)), @floatCast(@mod(playerPos[2], 1))); - 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][2]); - - c.glBindFramebuffer(c.GL_FRAMEBUFFER, activeFrameBuffer); - - graphics.draw.rectVao.bind(); - c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); - - c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0); - - if (!main.gui.hideGui) main.entity.client.renderHud(game.projectionMatrix, ambientLight, playerPos); - gpu_performance_measuring.stopQuery(); + worldFrameBuffer.bind(); + c.glViewport(0, 0, lastWidth, lastHeight); + 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); + + const time: u32 = @intCast(main.timestamp().toMilliseconds() & std.math.maxInt(u32)); + + gpu_performance_measuring.startQuery(.skybox); + Skybox.render(); + gpu_performance_measuring.stopQuery(); + + 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, playerPos); + + c.glActiveTexture(c.GL_TEXTURE0); + blocks.meshes.blockTextureArray.bind(); + c.glActiveTexture(c.GL_TEXTURE1); + blocks.meshes.emissionTextureArray.bind(); + c.glActiveTexture(c.GL_TEXTURE2); + blocks.meshes.reflectivityAndAbsorptionTextureArray.bind(); + c.glActiveTexture(c.GL_TEXTURE5); + blocks.meshes.ditherTexture.bind(); + reflectionCubeMap.bindTo(4); + + chunk_meshing.quadsDrawn = 0; + chunk_meshing.transparentQuadsDrawn = 0; + const meshes = mesh_storage.updateAndGetRenderChunks(world.conn, &frustum, playerPos, settings.renderDistance); + + gpu_performance_measuring.startQuery(.chunk_rendering_preparation); + const direction = crosshairDirection(game.camera.viewMatrix, lastFov, lastWidth, lastHeight); + MeshSelection.select(playerPos, direction, game.Player.inventory.getItem(game.Player.selectedSlot)); + + chunk_meshing.beginRender(); + + var chunkLists: [main.settings.highestSupportedLod + 1]main.List(u32) = @splat(main.List(u32).init(main.stackAllocator)); + defer for (chunkLists) |list| list.deinit(); + for (meshes) |mesh| { + mesh.prepareRendering(&chunkLists); + } + gpu_performance_measuring.stopQuery(); + gpu_performance_measuring.startQuery(.chunk_rendering); + chunk_meshing.drawChunksIndirect(&chunkLists, game.projectionMatrix, ambientLight, playerPos, false); + gpu_performance_measuring.stopQuery(); + + gpu_performance_measuring.startQuery(.entity_rendering); + main.entity.client.render(game.projectionMatrix, ambientLight, playerPos, main.lastDeltaTime.load(.monotonic)); + + itemdrop.ItemDropRenderer.renderItemDrops(game.projectionMatrix, ambientLight, playerPos); + gpu_performance_measuring.stopQuery(); + + gpu_performance_measuring.startQuery(.block_entity_rendering); + main.block_entity.renderAll(game.projectionMatrix, ambientLight, playerPos); + gpu_performance_measuring.stopQuery(); + + gpu_performance_measuring.startQuery(.particle_rendering); + particles.ParticleSystem.render(game.projectionMatrix, game.camera.viewMatrix, ambientLight); + gpu_performance_measuring.stopQuery(); + + // Rebind block textures back to their original slots + c.glActiveTexture(c.GL_TEXTURE0); + blocks.meshes.blockTextureArray.bind(); + c.glActiveTexture(c.GL_TEXTURE1); + blocks.meshes.emissionTextureArray.bind(); + + MeshSelection.render(game.projectionMatrix, game.camera.viewMatrix, playerPos); + + // Render transparent chunk meshes: + worldFrameBuffer.bindDepthTexture(c.GL_TEXTURE5); + + gpu_performance_measuring.startQuery(.transparent_rendering_preparation); + c.glTextureBarrier(); + + { + for (&chunkLists) |*list| list.clearRetainingCapacity(); + var i: usize = meshes.len; + while (true) { + if (i == 0) break; + i -= 1; + meshes[i].prepareTransparentRendering(playerPos, &chunkLists); + } + gpu_performance_measuring.stopQuery(); + gpu_performance_measuring.startQuery(.transparent_rendering); + chunk_meshing.drawChunksIndirect(&chunkLists, game.projectionMatrix, ambientLight, playerPos, true); + gpu_performance_measuring.stopQuery(); + } + + c.glDepthRange(0, 0.001); + itemdrop.ItemDropRenderer.renderDisplayItems(ambientLight, playerPos); + c.glDepthRange(0.001, 1); + + chunk_meshing.endRender(); + + worldFrameBuffer.bindTexture(c.GL_TEXTURE3); + + const playerBlock = mesh_storage.getBlockFromAnyLodFromRenderThread(@floor(playerPos[0]), @floor(playerPos[1]), @floor(playerPos[2])); + + if (settings.bloom) { + Bloom.render(lastWidth, lastHeight, playerBlock, playerPos, game.camera.viewMatrix); + } else { + Bloom.bindReplacementImage(); + } + gpu_performance_measuring.startQuery(.final_copy); + if (activeFrameBuffer == 0) c.glViewport(0, 0, main.Window.width, main.Window.height); + worldFrameBuffer.bindTexture(c.GL_TEXTURE3); + worldFrameBuffer.bindDepthTexture(c.GL_TEXTURE4); + worldFrameBuffer.unbind(); + deferredRenderPassPipeline.bind(null); + if (!blocks.meshes.hasFog(playerBlock)) { + c.glUniform3fv(deferredUniforms.@"fog.color", 1, @ptrCast(&game.fog.fogColor)); + c.glUniform1f(deferredUniforms.@"fog.density", game.fog.density); + c.glUniform1f(deferredUniforms.@"fog.fogLower", game.fog.fogLower); + c.glUniform1f(deferredUniforms.@"fog.fogHigher", game.fog.fogHigher); + } 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.@"fog.fogLower", 1e10); + c.glUniform1f(deferredUniforms.@"fog.fogHigher", 1e10); + } + c.glUniformMatrix4fv(deferredUniforms.invViewMatrix, 1, c.GL_TRUE, @ptrCast(&game.camera.viewMatrix.transpose())); + c.glUniform3i(deferredUniforms.playerPositionInteger, @floor(playerPos[0]), @floor(playerPos[1]), @floor(playerPos[2])); + c.glUniform3f(deferredUniforms.playerPositionFraction, @floatCast(@mod(playerPos[0], 1)), @floatCast(@mod(playerPos[1], 1)), @floatCast(@mod(playerPos[2], 1))); + 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][2]); + + c.glBindFramebuffer(c.GL_FRAMEBUFFER, activeFrameBuffer); + + graphics.draw.rectVao.bind(); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + + c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0); + + if (!main.gui.hideGui) main.entity.client.renderHud(game.projectionMatrix, ambientLight, playerPos); + gpu_performance_measuring.stopQuery(); } const Bloom = struct { // MARK: Bloom - 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 firstPassPipeline: graphics.Pipeline = undefined; - var secondPassPipeline: graphics.Pipeline = undefined; - var colorExtractAndDownsamplePipeline: graphics.Pipeline = undefined; - var colorExtractUniforms: struct { - zNear: c_int, - zFar: c_int, - tanXY: c_int, - @"fog.color": c_int, - @"fog.density": c_int, - @"fog.fogLower": c_int, - @"fog.fogHigher": c_int, - invViewMatrix: c_int, - playerPositionInteger: c_int, - playerPositionFraction: 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 = .init(); - emptyBuffer.generate(graphics.Image.emptyImage); - firstPassPipeline = graphics.Pipeline.init( - "assets/cubyz/shaders/bloom/first_pass.vert", - "assets/cubyz/shaders/bloom/first_pass.frag", - "", - null, - graphics.draw.SimpleVertex2D, - &.{.{ .binding = 3, .count = 1, .type = .combinedImageSampler, .stageFlags = .{ .fragment = true } }}, - .{ .cullMode = .none }, - .{ .depthTest = false, .depthWrite = false }, - .{ .attachments = &.{.noBlending} }, - ); - secondPassPipeline = graphics.Pipeline.init( - "assets/cubyz/shaders/bloom/second_pass.vert", - "assets/cubyz/shaders/bloom/second_pass.frag", - "", - null, - graphics.draw.SimpleVertex2D, - &.{.{ .binding = 3, .count = 1, .type = .combinedImageSampler, .stageFlags = .{ .fragment = true } }}, - .{ .cullMode = .none }, - .{ .depthTest = false, .depthWrite = false }, - .{ .attachments = &.{.noBlending} }, - ); - colorExtractAndDownsamplePipeline = graphics.Pipeline.init( - "assets/cubyz/shaders/bloom/color_extractor_downsample.vert", - "assets/cubyz/shaders/bloom/color_extractor_downsample.frag", - "", - &colorExtractUniforms, - graphics.draw.SimpleVertex2D, - &.{}, - .{ .cullMode = .none }, - .{ .depthTest = false, .depthWrite = false }, - .{ .attachments = &.{.noBlending} }, - ); - } - - pub fn deinit() void { - buffer1.deinit(); - buffer2.deinit(); - firstPassPipeline.deinit(); - secondPassPipeline.deinit(); - colorExtractAndDownsamplePipeline.deinit(); - } - - fn extractImageDataAndDownsample(playerBlock: blocks.Block, playerPos: Vec3d, viewMatrix: Mat4f) void { - colorExtractAndDownsamplePipeline.bind(null); - worldFrameBuffer.bindTexture(c.GL_TEXTURE3); - worldFrameBuffer.bindDepthTexture(c.GL_TEXTURE4); - buffer1.bind(); - if (!blocks.meshes.hasFog(playerBlock)) { - c.glUniform3fv(colorExtractUniforms.@"fog.color", 1, @ptrCast(&game.fog.fogColor)); - c.glUniform1f(colorExtractUniforms.@"fog.density", game.fog.density); - c.glUniform1f(colorExtractUniforms.@"fog.fogLower", game.fog.fogLower); - c.glUniform1f(colorExtractUniforms.@"fog.fogHigher", game.fog.fogHigher); - } 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.@"fog.fogLower", 1e10); - c.glUniform1f(colorExtractUniforms.@"fog.fogHigher", 1e10); - } - - c.glUniformMatrix4fv(colorExtractUniforms.invViewMatrix, 1, c.GL_TRUE, @ptrCast(&viewMatrix.transpose())); - c.glUniform3i(colorExtractUniforms.playerPositionInteger, @floor(playerPos[0]), @floor(playerPos[1]), @floor(playerPos[2])); - c.glUniform3f(colorExtractUniforms.playerPositionFraction, @floatCast(@mod(playerPos[0], 1)), @floatCast(@mod(playerPos[1], 1)), @floatCast(@mod(playerPos[2], 1))); - 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][2]); - graphics.draw.rectVao.bind(); - c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); - } - - fn firstPass() void { - firstPassPipeline.bind(null); - buffer1.bindTexture(c.GL_TEXTURE3); - buffer2.bind(); - graphics.draw.rectVao.bind(); - c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); - } - - fn secondPass() void { - secondPassPipeline.bind(null); - buffer2.bindTexture(c.GL_TEXTURE3); - buffer1.bind(); - graphics.draw.rectVao.bind(); - c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); - } - - fn render(currentWidth: u31, currentHeight: u31, playerBlock: blocks.Block, playerPos: Vec3d, viewMatrix: Mat4f) void { - if (width != currentWidth or height != currentHeight) { - width = currentWidth; - height = currentHeight; - buffer1.updateSize(width / 4, height / 4, c.GL_R11F_G11F_B10F); - std.debug.assert(buffer1.validate()); - buffer2.updateSize(width / 4, height / 4, c.GL_R11F_G11F_B10F); - std.debug.assert(buffer2.validate()); - } - gpu_performance_measuring.startQuery(.bloom_extract_downsample); - - c.glViewport(0, 0, width / 4, height / 4); - extractImageDataAndDownsample(playerBlock, playerPos, viewMatrix); - 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); - - gpu_performance_measuring.stopQuery(); - } - - fn bindReplacementImage() void { - emptyBuffer.bindTo(5); - } + 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 firstPassPipeline: graphics.Pipeline = undefined; + var secondPassPipeline: graphics.Pipeline = undefined; + var colorExtractAndDownsamplePipeline: graphics.Pipeline = undefined; + var colorExtractUniforms: struct { + zNear: c_int, + zFar: c_int, + tanXY: c_int, + @"fog.color": c_int, + @"fog.density": c_int, + @"fog.fogLower": c_int, + @"fog.fogHigher": c_int, + invViewMatrix: c_int, + playerPositionInteger: c_int, + playerPositionFraction: 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 = .init(); + emptyBuffer.generate(graphics.Image.emptyImage); + firstPassPipeline = graphics.Pipeline.init( + "assets/cubyz/shaders/bloom/first_pass.vert", + "assets/cubyz/shaders/bloom/first_pass.frag", + "", + null, + graphics.draw.SimpleVertex2D, + &.{.{.binding = 3, .count = 1, .type = .combinedImageSampler, .stageFlags = .{.fragment = true}}}, + .{.cullMode = .none}, + .{.depthTest = false, .depthWrite = false}, + .{.attachments = &.{.noBlending}}, + ); + secondPassPipeline = graphics.Pipeline.init( + "assets/cubyz/shaders/bloom/second_pass.vert", + "assets/cubyz/shaders/bloom/second_pass.frag", + "", + null, + graphics.draw.SimpleVertex2D, + &.{.{.binding = 3, .count = 1, .type = .combinedImageSampler, .stageFlags = .{.fragment = true}}}, + .{.cullMode = .none}, + .{.depthTest = false, .depthWrite = false}, + .{.attachments = &.{.noBlending}}, + ); + colorExtractAndDownsamplePipeline = graphics.Pipeline.init( + "assets/cubyz/shaders/bloom/color_extractor_downsample.vert", + "assets/cubyz/shaders/bloom/color_extractor_downsample.frag", + "", + &colorExtractUniforms, + graphics.draw.SimpleVertex2D, + &.{}, + .{.cullMode = .none}, + .{.depthTest = false, .depthWrite = false}, + .{.attachments = &.{.noBlending}}, + ); + } + + pub fn deinit() void { + buffer1.deinit(); + buffer2.deinit(); + firstPassPipeline.deinit(); + secondPassPipeline.deinit(); + colorExtractAndDownsamplePipeline.deinit(); + } + + fn extractImageDataAndDownsample(playerBlock: blocks.Block, playerPos: Vec3d, viewMatrix: Mat4f) void { + colorExtractAndDownsamplePipeline.bind(null); + worldFrameBuffer.bindTexture(c.GL_TEXTURE3); + worldFrameBuffer.bindDepthTexture(c.GL_TEXTURE4); + buffer1.bind(); + if (!blocks.meshes.hasFog(playerBlock)) { + c.glUniform3fv(colorExtractUniforms.@"fog.color", 1, @ptrCast(&game.fog.fogColor)); + c.glUniform1f(colorExtractUniforms.@"fog.density", game.fog.density); + c.glUniform1f(colorExtractUniforms.@"fog.fogLower", game.fog.fogLower); + c.glUniform1f(colorExtractUniforms.@"fog.fogHigher", game.fog.fogHigher); + } 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.@"fog.fogLower", 1e10); + c.glUniform1f(colorExtractUniforms.@"fog.fogHigher", 1e10); + } + + c.glUniformMatrix4fv(colorExtractUniforms.invViewMatrix, 1, c.GL_TRUE, @ptrCast(&viewMatrix.transpose())); + c.glUniform3i(colorExtractUniforms.playerPositionInteger, @floor(playerPos[0]), @floor(playerPos[1]), @floor(playerPos[2])); + c.glUniform3f(colorExtractUniforms.playerPositionFraction, @floatCast(@mod(playerPos[0], 1)), @floatCast(@mod(playerPos[1], 1)), @floatCast(@mod(playerPos[2], 1))); + 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][2]); + graphics.draw.rectVao.bind(); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + } + + fn firstPass() void { + firstPassPipeline.bind(null); + buffer1.bindTexture(c.GL_TEXTURE3); + buffer2.bind(); + graphics.draw.rectVao.bind(); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + } + + fn secondPass() void { + secondPassPipeline.bind(null); + buffer2.bindTexture(c.GL_TEXTURE3); + buffer1.bind(); + graphics.draw.rectVao.bind(); + c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4); + } + + fn render(currentWidth: u31, currentHeight: u31, playerBlock: blocks.Block, playerPos: Vec3d, viewMatrix: Mat4f) void { + if (width != currentWidth or height != currentHeight) { + width = currentWidth; + height = currentHeight; + buffer1.updateSize(width/4, height/4, c.GL_R11F_G11F_B10F); + std.debug.assert(buffer1.validate()); + buffer2.updateSize(width/4, height/4, c.GL_R11F_G11F_B10F); + std.debug.assert(buffer2.validate()); + } + gpu_performance_measuring.startQuery(.bloom_extract_downsample); + + c.glViewport(0, 0, width/4, height/4); + extractImageDataAndDownsample(playerBlock, playerPos, viewMatrix); + 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); + + gpu_performance_measuring.stopQuery(); + } + + fn bindReplacementImage() void { + emptyBuffer.bindTo(5); + } }; pub const MenuBackGround = struct { - var pipeline: graphics.Pipeline = undefined; - var uniforms: struct { - viewMatrix: c_int, - projectionMatrix: c_int, - } = undefined; - - var vao: graphics.VertexArray = undefined; - var texture: graphics.Texture = undefined; - - var angle: f32 = 0; - - fn init() void { - const MenuBackgroundVertex = struct { - pos: [3]f32, - uv: [2]f32, - - pub const attributeDescriptions: []const c.VkVertexInputAttributeDescription = &.{ - .{ - .location = 0, - .format = c.VK_FORMAT_R32G32B32_SFLOAT, - .offset = @offsetOf(@This(), "pos"), - }, - .{ - .location = 1, - .format = c.VK_FORMAT_R32G32_SFLOAT, - .offset = @offsetOf(@This(), "uv"), - }, - }; - }; - pipeline = graphics.Pipeline.init( - "assets/cubyz/shaders/background/vertex.vert", - "assets/cubyz/shaders/background/fragment.frag", - "", - &uniforms, - MenuBackgroundVertex, - &.{}, - .{ .cullMode = .none }, - .{ .depthTest = false, .depthWrite = false }, - .{ .attachments = &.{.noBlending} }, - ); - // 4 sides of a simple cube with some panorama texture on it. - const rawData = [_]MenuBackgroundVertex{ - .{ .pos = .{ -1, 1, -1 }, .uv = .{ 1, 1 } }, - .{ .pos = .{ -1, 1, 1 }, .uv = .{ 1, 0 } }, - .{ .pos = .{ -1, -1, -1 }, .uv = .{ 0.75, 1 } }, - .{ .pos = .{ -1, -1, 1 }, .uv = .{ 0.75, 0 } }, - .{ .pos = .{ 1, -1, -1 }, .uv = .{ 0.5, 1 } }, - .{ .pos = .{ 1, -1, 1 }, .uv = .{ 0.5, 0 } }, - .{ .pos = .{ 1, 1, -1 }, .uv = .{ 0.25, 1 } }, - .{ .pos = .{ 1, 1, 1 }, .uv = .{ 0.25, 0 } }, - .{ .pos = .{ -1, 1, -1 }, .uv = .{ 0, 1 } }, - .{ .pos = .{ -1, 1, 1 }, .uv = .{ 0, 0 } }, - }; - - const indices = [_]u32{ - 0, 1, 2, - 2, 3, 1, - 2, 3, 4, - 4, 5, 3, - 4, 5, 6, - 6, 7, 5, - 6, 7, 8, - 8, 9, 7, - }; - - vao = .init(MenuBackgroundVertex, &rawData, &indices); - - const backgroundPath = chooseBackgroundImagePath(main.stackAllocator) catch |err| { - std.log.err("Couldn't open background path: {s}", .{@errorName(err)}); - texture = .{ .textureID = 0 }; - return; - }; - defer main.stackAllocator.free(backgroundPath); - texture = graphics.Texture.initFromFile(backgroundPath); - } - - fn chooseBackgroundImagePath(allocator: main.heap.NeverFailingAllocator) ![]const u8 { - var dir = try main.files.cubyzDir().openIterableDir("backgrounds"); - defer dir.close(); - - // Whenever the version changes copy over the new background image and display it. - if (!std.mem.eql(u8, settings.lastVersionString, settings.version.version)) { - const defaultImageData = try main.files.cwd().read(main.stackAllocator, "assets/cubyz/default_background.png"); - defer main.stackAllocator.free(defaultImageData); - try dir.write("default_background.png", defaultImageData); - - return std.fmt.allocPrint(allocator.allocator, "{s}/backgrounds/default_background.png", .{main.files.cubyzDirStr()}) catch unreachable; - } - - // Otherwise load a random texture from the backgrounds folder. The player may make their own pictures which can be chosen as well. - var walker = dir.walk(main.stackAllocator); - defer walker.deinit(); - var fileList = main.List([]const u8).init(main.stackAllocator); - defer { - for (fileList.items) |fileName| { - main.stackAllocator.free(fileName); - } - fileList.deinit(); - } - - while (try walker.next(main.io)) |entry| { - if (entry.kind == .file and std.ascii.endsWithIgnoreCase(entry.basename, ".png")) { - fileList.append(main.stackAllocator.dupe(u8, entry.path)); - } - } - if (fileList.items.len == 0) { - return error.NoBackgroundImagesFound; - } - const theChosenOne = main.random.nextIntBounded(u32, &main.seed, @as(u32, @intCast(fileList.items.len))); - return std.fmt.allocPrint(allocator.allocator, "{s}/backgrounds/{s}", .{ main.files.cubyzDirStr(), fileList.items[theChosenOne] }) catch unreachable; - } - - pub fn deinit() void { - pipeline.deinit(); - vao.deinit(); - } - - pub fn hasImage() bool { - return texture.textureID != 0; - } - - pub fn render(deltaTime: f64) void { - c.glViewport(0, 0, main.Window.width, main.Window.height); - if (texture.textureID == 0) return; - - // Use a simple rotation around the z axis, with a steadily increasing angle. - angle += @as(f32, @floatCast(deltaTime)) / 20.0; - const viewMatrix = Mat4f.rotationZ(angle); - pipeline.bind(null); - c.glUniformMatrix4fv(uniforms.viewMatrix, 1, c.GL_TRUE, @ptrCast(&viewMatrix)); - c.glUniformMatrix4fv(uniforms.projectionMatrix, 1, c.GL_TRUE, @ptrCast(&game.projectionMatrix)); - - texture.bindTo(0); - - vao.bind(); - 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 = main.stackAllocator.alloc(u32, size * size); - defer main.stackAllocator.free(pixels); - - // Change the viewport and the matrices to render 4 cube faces: - - const oldResolutionScale = main.settings.resolutionScale; - main.settings.resolutionScale = 1; - updateViewport(size, size); - updateFov(90.0); - defer updateFov(main.settings.fov); - main.settings.resolutionScale = oldResolutionScale; - defer updateViewport(Window.width, Window.height); - - 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 = graphics.Image.init(main.stackAllocator, 4 * size, size); - defer image.deinit(main.stackAllocator); - - for (0..4) |i| { - c.glDepthFunc(c.GL_LESS); - c.glDepthMask(c.GL_TRUE); - c.glDisable(c.GL_SCISSOR_TEST); - game.camera.rotation = .{ 0, 0, angles[i] }; - // Draw to frame buffer. - buffer.bind(); - c.glClear(c.GL_DEPTH_BUFFER_BIT | c.GL_STENCIL_BUFFER_BIT | c.GL_COLOR_BUFFER_BIT); - main.renderer.render(game.Player.getEyePosBlocking(), 0); - // 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.glBindFramebuffer(c.GL_FRAMEBUFFER, 0); - - const fileName = std.fmt.allocPrint(main.stackAllocator.allocator, "{s}/backgrounds/{s}_{}.png", .{ main.files.cubyzDirStr(), game.world.?.name, game.world.?.gameTime.load(.monotonic) }) catch unreachable; - defer main.stackAllocator.free(fileName); - image.exportToFile(fileName) catch |err| { - std.log.err("Cannot write file {s} due to {s}", .{ fileName, @errorName(err) }); - }; - // TODO: Performance is terrible even with -O3. Consider using qoi instead. - } + var pipeline: graphics.Pipeline = undefined; + var uniforms: struct { + viewMatrix: c_int, + projectionMatrix: c_int, + } = undefined; + + var vao: graphics.VertexArray = undefined; + var texture: graphics.Texture = undefined; + + var angle: f32 = 0; + + fn init() void { + const MenuBackgroundVertex = struct { + pos: [3]f32, + uv: [2]f32, + + pub const attributeDescriptions: []const c.VkVertexInputAttributeDescription = &.{ + .{ + .location = 0, + .format = c.VK_FORMAT_R32G32B32_SFLOAT, + .offset = @offsetOf(@This(), "pos"), + }, + .{ + .location = 1, + .format = c.VK_FORMAT_R32G32_SFLOAT, + .offset = @offsetOf(@This(), "uv"), + }, + }; + }; + pipeline = graphics.Pipeline.init( + "assets/cubyz/shaders/background/vertex.vert", + "assets/cubyz/shaders/background/fragment.frag", + "", + &uniforms, + MenuBackgroundVertex, + &.{}, + .{.cullMode = .none}, + .{.depthTest = false, .depthWrite = false}, + .{.attachments = &.{.noBlending}}, + ); + // 4 sides of a simple cube with some panorama texture on it. + const rawData = [_]MenuBackgroundVertex{ + .{.pos = .{-1, 1, -1}, .uv = .{1, 1}}, + .{.pos = .{-1, 1, 1}, .uv = .{1, 0}}, + .{.pos = .{-1, -1, -1}, .uv = .{0.75, 1}}, + .{.pos = .{-1, -1, 1}, .uv = .{0.75, 0}}, + .{.pos = .{1, -1, -1}, .uv = .{0.5, 1}}, + .{.pos = .{1, -1, 1}, .uv = .{0.5, 0}}, + .{.pos = .{1, 1, -1}, .uv = .{0.25, 1}}, + .{.pos = .{1, 1, 1}, .uv = .{0.25, 0}}, + .{.pos = .{-1, 1, -1}, .uv = .{0, 1}}, + .{.pos = .{-1, 1, 1}, .uv = .{0, 0}}, + }; + + const indices = [_]u32{ + 0, 1, 2, + 2, 3, 1, + 2, 3, 4, + 4, 5, 3, + 4, 5, 6, + 6, 7, 5, + 6, 7, 8, + 8, 9, 7, + }; + + vao = .init(MenuBackgroundVertex, &rawData, &indices); + + const backgroundPath = chooseBackgroundImagePath(main.stackAllocator) catch |err| { + std.log.err("Couldn't open background path: {s}", .{@errorName(err)}); + texture = .{.textureID = 0}; + return; + }; + defer main.stackAllocator.free(backgroundPath); + texture = graphics.Texture.initFromFile(backgroundPath); + } + + fn chooseBackgroundImagePath(allocator: main.heap.NeverFailingAllocator) ![]const u8 { + var dir = try main.files.cubyzDir().openIterableDir("backgrounds"); + defer dir.close(); + + // Whenever the version changes copy over the new background image and display it. + if (!std.mem.eql(u8, settings.lastVersionString, settings.version.version)) { + const defaultImageData = try main.files.cwd().read(main.stackAllocator, "assets/cubyz/default_background.png"); + defer main.stackAllocator.free(defaultImageData); + try dir.write("default_background.png", defaultImageData); + + return std.fmt.allocPrint(allocator.allocator, "{s}/backgrounds/default_background.png", .{main.files.cubyzDirStr()}) catch unreachable; + } + + // Otherwise load a random texture from the backgrounds folder. The player may make their own pictures which can be chosen as well. + var walker = dir.walk(main.stackAllocator); + defer walker.deinit(); + var fileList = main.List([]const u8).init(main.stackAllocator); + defer { + for (fileList.items) |fileName| { + main.stackAllocator.free(fileName); + } + fileList.deinit(); + } + + while (try walker.next(main.io)) |entry| { + if (entry.kind == .file and std.ascii.endsWithIgnoreCase(entry.basename, ".png")) { + fileList.append(main.stackAllocator.dupe(u8, entry.path)); + } + } + if (fileList.items.len == 0) { + return error.NoBackgroundImagesFound; + } + const theChosenOne = main.random.nextIntBounded(u32, &main.seed, @as(u32, @intCast(fileList.items.len))); + return std.fmt.allocPrint(allocator.allocator, "{s}/backgrounds/{s}", .{main.files.cubyzDirStr(), fileList.items[theChosenOne]}) catch unreachable; + } + + pub fn deinit() void { + pipeline.deinit(); + vao.deinit(); + } + + pub fn hasImage() bool { + return texture.textureID != 0; + } + + pub fn render(deltaTime: f64) void { + c.glViewport(0, 0, main.Window.width, main.Window.height); + if (texture.textureID == 0) return; + + // Use a simple rotation around the z axis, with a steadily increasing angle. + angle += @as(f32, @floatCast(deltaTime))/20.0; + const viewMatrix = Mat4f.rotationZ(angle); + pipeline.bind(null); + c.glUniformMatrix4fv(uniforms.viewMatrix, 1, c.GL_TRUE, @ptrCast(&viewMatrix)); + c.glUniformMatrix4fv(uniforms.projectionMatrix, 1, c.GL_TRUE, @ptrCast(&game.projectionMatrix)); + + texture.bindTo(0); + + vao.bind(); + 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 = main.stackAllocator.alloc(u32, size*size); + defer main.stackAllocator.free(pixels); + + // Change the viewport and the matrices to render 4 cube faces: + + const oldResolutionScale = main.settings.resolutionScale; + main.settings.resolutionScale = 1; + updateViewport(size, size); + updateFov(90.0); + defer updateFov(main.settings.fov); + main.settings.resolutionScale = oldResolutionScale; + defer updateViewport(Window.width, Window.height); + + 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 = graphics.Image.init(main.stackAllocator, 4*size, size); + defer image.deinit(main.stackAllocator); + + for (0..4) |i| { + c.glDepthFunc(c.GL_LESS); + c.glDepthMask(c.GL_TRUE); + c.glDisable(c.GL_SCISSOR_TEST); + game.camera.rotation = .{0, 0, angles[i]}; + // Draw to frame buffer. + buffer.bind(); + c.glClear(c.GL_DEPTH_BUFFER_BIT | c.GL_STENCIL_BUFFER_BIT | c.GL_COLOR_BUFFER_BIT); + main.renderer.render(game.Player.getEyePosBlocking(), 0); + // 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.glBindFramebuffer(c.GL_FRAMEBUFFER, 0); + + const fileName = std.fmt.allocPrint(main.stackAllocator.allocator, "{s}/backgrounds/{s}_{}.png", .{main.files.cubyzDirStr(), game.world.?.name, game.world.?.gameTime.load(.monotonic)}) catch unreachable; + defer main.stackAllocator.free(fileName); + image.exportToFile(fileName) catch |err| { + std.log.err("Cannot write file {s} due to {s}", .{fileName, @errorName(err)}); + }; + // TODO: Performance is terrible even with -O3. Consider using qoi instead. + } }; pub const Skybox = struct { - var starPipeline: graphics.Pipeline = undefined; - var starUniforms: struct { - mvp: c_int, - starOpacity: c_int, - } = undefined; + var starPipeline: graphics.Pipeline = undefined; + var starUniforms: struct { + mvp: c_int, + starOpacity: c_int, + } = undefined; - var starVao: graphics.VertexArray = undefined; + var starVao: graphics.VertexArray = undefined; - var starSsbo: graphics.SSBO = undefined; + var starSsbo: graphics.SSBO = undefined; - const numStars = 10000; + const numStars = 10000; - fn getStarPos(seed: *u64) Vec3f { - const x: f32 = @floatCast(main.random.nextFloatGauss(seed)); - const y: f32 = @floatCast(main.random.nextFloatGauss(seed)); - const z: f32 = @floatCast(main.random.nextFloatGauss(seed)); + fn getStarPos(seed: *u64) Vec3f { + const x: f32 = @floatCast(main.random.nextFloatGauss(seed)); + const y: f32 = @floatCast(main.random.nextFloatGauss(seed)); + const z: f32 = @floatCast(main.random.nextFloatGauss(seed)); - const r = std.math.cbrt(main.random.nextFloat(seed)) * 5000.0; + const r = std.math.cbrt(main.random.nextFloat(seed))*5000.0; - return vec.normalize(Vec3f{ x, y, z }) * @as(Vec3f, @splat(r)); - } + return vec.normalize(Vec3f{x, y, z})*@as(Vec3f, @splat(r)); + } - fn getStarColor(temperature: f32, light: f32, image: graphics.Image) Vec3f { - const rgbCol = image.getRGB(@trunc(std.math.clamp(temperature / 15000.0 * @as(f32, @floatFromInt(image.width)), 0.0, @as(f32, @floatFromInt(image.width - 1)))), 0); - var rgb: Vec3f = @floatFromInt(Vec3i{ rgbCol.r, rgbCol.g, rgbCol.b }); - rgb /= @splat(255.0); + fn getStarColor(temperature: f32, light: f32, image: graphics.Image) Vec3f { + const rgbCol = image.getRGB(@trunc(std.math.clamp(temperature/15000.0*@as(f32, @floatFromInt(image.width)), 0.0, @as(f32, @floatFromInt(image.width - 1)))), 0); + var rgb: Vec3f = @floatFromInt(Vec3i{rgbCol.r, rgbCol.g, rgbCol.b}); + rgb /= @splat(255.0); - rgb *= @as(Vec3f, @splat(light)); + rgb *= @as(Vec3f, @splat(light)); - const m = @reduce(.Max, rgb); - if (m > 1.0) { - rgb /= @as(Vec3f, @splat(m)); - } + const m = @reduce(.Max, rgb); + if (m > 1.0) { + rgb /= @as(Vec3f, @splat(m)); + } - return rgb; - } + return rgb; + } - fn init() void { - const starColorImage = graphics.Image.readFromFile(main.stackAllocator, "assets/cubyz/star.png") catch |err| { - std.log.err("Failed to load star image: {s}", .{@errorName(err)}); - return; - }; - defer starColorImage.deinit(main.stackAllocator); + fn init() void { + const starColorImage = graphics.Image.readFromFile(main.stackAllocator, "assets/cubyz/star.png") catch |err| { + std.log.err("Failed to load star image: {s}", .{@errorName(err)}); + return; + }; + defer starColorImage.deinit(main.stackAllocator); - starPipeline = graphics.Pipeline.init( - "assets/cubyz/shaders/skybox/star.vert", - "assets/cubyz/shaders/skybox/star.frag", - "", - &starUniforms, - graphics.VertexArray.EmptyVertex, - &.{}, - .{ .cullMode = .none }, - .{ .depthTest = false, .depthWrite = false }, - .{ .attachments = &.{.{ - .srcColorBlendFactor = .one, - .dstColorBlendFactor = .one, - .colorBlendOp = .add, - .srcAlphaBlendFactor = .one, - .dstAlphaBlendFactor = .one, - .alphaBlendOp = .add, - }} }, - ); + starPipeline = graphics.Pipeline.init( + "assets/cubyz/shaders/skybox/star.vert", + "assets/cubyz/shaders/skybox/star.frag", + "", + &starUniforms, + graphics.VertexArray.EmptyVertex, + &.{}, + .{.cullMode = .none}, + .{.depthTest = false, .depthWrite = false}, + .{.attachments = &.{.{ + .srcColorBlendFactor = .one, + .dstColorBlendFactor = .one, + .colorBlendOp = .add, + .srcAlphaBlendFactor = .one, + .dstAlphaBlendFactor = .one, + .alphaBlendOp = .add, + }}}, + ); - var starData: [numStars * 20]f32 = undefined; + var starData: [numStars*20]f32 = undefined; - const starDist = 200.0; + const starDist = 200.0; - const off: f32 = @sqrt(3.0) / 6.0; + const off: f32 = @sqrt(3.0)/6.0; - const triVertA = Vec3f{ 0.5, starDist, -off }; - const triVertB = Vec3f{ -0.5, starDist, -off }; - const triVertC = Vec3f{ 0.0, starDist, @sqrt(3.0) / 2.0 - off }; + const triVertA = Vec3f{0.5, starDist, -off}; + const triVertB = Vec3f{-0.5, starDist, -off}; + const triVertC = Vec3f{0.0, starDist, @sqrt(3.0)/2.0 - off}; - var seed: u64 = 0; + var seed: u64 = 0; - for (0..numStars) |i| { - var pos: Vec3f = undefined; + for (0..numStars) |i| { + var pos: Vec3f = undefined; - var radius: f32 = undefined; + var radius: f32 = undefined; - var temperature: f32 = undefined; + var temperature: f32 = undefined; - var light: f32 = 0; + var light: f32 = 0; - while (light < 0.1) { - pos = getStarPos(&seed); + while (light < 0.1) { + pos = getStarPos(&seed); - radius = @floatCast(main.random.nextFloatExp(&seed) * 4 + 0.2); + radius = @floatCast(main.random.nextFloatExp(&seed)*4 + 0.2); - temperature = @floatCast(@abs(main.random.nextFloatGauss(&seed) * 3000.0 + 5000.0) + 1000.0); + temperature = @floatCast(@abs(main.random.nextFloatGauss(&seed)*3000.0 + 5000.0) + 1000.0); - // 3.6e-12 can be modified to change the brightness of the stars - light = (3.6e-12 * radius * radius * temperature * temperature * temperature * temperature) / (vec.dot(pos, pos)); - } + // 3.6e-12 can be modified to change the brightness of the stars + light = (3.6e-12*radius*radius*temperature*temperature*temperature*temperature)/(vec.dot(pos, pos)); + } - pos = vec.normalize(pos) * @as(Vec3f, @splat(starDist)); + pos = vec.normalize(pos)*@as(Vec3f, @splat(starDist)); - const normPos = vec.normalize(pos); + const normPos = vec.normalize(pos); - const color = getStarColor(temperature, light, starColorImage); + const color = getStarColor(temperature, light, starColorImage); - const latitude: f32 = @floatCast(std.math.asin(normPos[2])); - const longitude: f32 = @floatCast(std.math.atan2(-normPos[0], normPos[1])); + const latitude: f32 = @floatCast(std.math.asin(normPos[2])); + const longitude: f32 = @floatCast(std.math.atan2(-normPos[0], normPos[1])); - const mat = Mat4f.rotationZ(longitude).mul(Mat4f.rotationX(latitude)); + const mat = Mat4f.rotationZ(longitude).mul(Mat4f.rotationX(latitude)); - const posA = vec.xyz(mat.mulVec(.{ triVertA[0], triVertA[1], triVertA[2], 1.0 })); - const posB = vec.xyz(mat.mulVec(.{ triVertB[0], triVertB[1], triVertB[2], 1.0 })); - const posC = vec.xyz(mat.mulVec(.{ triVertC[0], triVertC[1], triVertC[2], 1.0 })); + const posA = vec.xyz(mat.mulVec(.{triVertA[0], triVertA[1], triVertA[2], 1.0})); + const posB = vec.xyz(mat.mulVec(.{triVertB[0], triVertB[1], triVertB[2], 1.0})); + const posC = vec.xyz(mat.mulVec(.{triVertC[0], triVertC[1], triVertC[2], 1.0})); - starData[i * 20 ..][0..3].* = posA; - starData[i * 20 + 4 ..][0..3].* = posB; - starData[i * 20 + 8 ..][0..3].* = posC; + starData[i*20 ..][0..3].* = posA; + starData[i*20 + 4 ..][0..3].* = posB; + starData[i*20 + 8 ..][0..3].* = posC; - starData[i * 20 + 12 ..][0..3].* = pos; - starData[i * 20 + 16 ..][0..3].* = color; - } + starData[i*20 + 12 ..][0..3].* = pos; + starData[i*20 + 16 ..][0..3].* = color; + } - starSsbo = graphics.SSBO.initStatic(f32, &starData); + starSsbo = graphics.SSBO.initStatic(f32, &starData); - starVao = .init(graphics.VertexArray.EmptyVertex, &.{}, null); - } + starVao = .init(graphics.VertexArray.EmptyVertex, &.{}, null); + } - pub fn deinit() void { - starPipeline.deinit(); - starSsbo.deinit(); - starVao.deinit(); - } + pub fn deinit() void { + starPipeline.deinit(); + starSsbo.deinit(); + starVao.deinit(); + } - pub fn render() void { - const viewMatrix = game.camera.viewMatrix; + pub fn render() void { + const viewMatrix = game.camera.viewMatrix; - const time = game.world.?.gameTime.load(.monotonic); + const time = game.world.?.gameTime.load(.monotonic); - var starOpacity: f32 = 0; - const dayTime = @abs(@mod(time, game.World.dayCycle) -% game.World.dayCycle / 2); - if (dayTime < game.World.dayCycle / 4 - game.World.dayCycle / 16) { - starOpacity = 1; - } else if (dayTime > game.World.dayCycle / 4 + game.World.dayCycle / 16) { - starOpacity = 0; - } else { - starOpacity = 1 - @as(f32, @floatFromInt(dayTime - (game.World.dayCycle / 4 - game.World.dayCycle / 16))) / @as(f32, @floatFromInt(game.World.dayCycle / 8)); - } + var starOpacity: f32 = 0; + const dayTime = @abs(@mod(time, game.World.dayCycle) -% game.World.dayCycle/2); + if (dayTime < game.World.dayCycle/4 - game.World.dayCycle/16) { + starOpacity = 1; + } else if (dayTime > game.World.dayCycle/4 + game.World.dayCycle/16) { + starOpacity = 0; + } else { + starOpacity = 1 - @as(f32, @floatFromInt(dayTime - (game.World.dayCycle/4 - game.World.dayCycle/16)))/@as(f32, @floatFromInt(game.World.dayCycle/8)); + } - if (starOpacity != 0) { - starPipeline.bind(null); + if (starOpacity != 0) { + starPipeline.bind(null); - const starMatrix = game.projectionMatrix.mul(viewMatrix.mul(Mat4f.rotationX(@as(f32, @floatFromInt(time)) / @as(f32, @floatFromInt(main.game.World.dayCycle))))); + const starMatrix = game.projectionMatrix.mul(viewMatrix.mul(Mat4f.rotationX(@as(f32, @floatFromInt(time))/@as(f32, @floatFromInt(main.game.World.dayCycle))))); - starSsbo.bind(12); + starSsbo.bind(12); - c.glUniform1f(starUniforms.starOpacity, starOpacity); - c.glUniformMatrix4fv(starUniforms.mvp, 1, c.GL_TRUE, @ptrCast(&starMatrix)); + c.glUniform1f(starUniforms.starOpacity, starOpacity); + c.glUniformMatrix4fv(starUniforms.mvp, 1, c.GL_TRUE, @ptrCast(&starMatrix)); - starVao.bind(); - c.glDrawArrays(c.GL_TRIANGLES, 0, numStars * 3); + starVao.bind(); + c.glDrawArrays(c.GL_TRIANGLES, 0, numStars*3); - c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, 0); - } - } + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, 0); + } + } }; pub const Frustum = struct { // MARK: Frustum - 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, 1, 0, 1 })); - const cameraUp = vec.xyz(invRotationMatrix.mulVec(Vec4f{ 0, 0, 1, 1 })); - const cameraRight = vec.xyz(invRotationMatrix.mulVec(Vec4f{ 1, 0, 0, 1 })); - - const halfVSide = std.math.tan(std.math.degreesToRadians(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; - } + 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, 1, 0, 1})); + const cameraUp = vec.xyz(invRotationMatrix.mulVec(Vec4f{0, 0, 1, 1})); + const cameraRight = vec.xyz(invRotationMatrix.mulVec(Vec4f{1, 0, 0, 1})); + + const halfVSide = std.math.tan(std.math.degreesToRadians(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 { // MARK: MeshSelection - var pipeline: graphics.Pipeline = undefined; - var uniforms: struct { - projectionMatrix: c_int, - viewMatrix: c_int, - modelPosition: c_int, - lowerBounds: c_int, - upperBounds: c_int, - lineSize: c_int, - } = undefined; - - pub fn init() void { - pipeline = graphics.Pipeline.init( - "assets/cubyz/shaders/block_selection_vertex.vert", - "assets/cubyz/shaders/block_selection_fragment.frag", - "", - &uniforms, - graphics.VertexArray.EmptyVertex, - &.{}, - .{ .cullMode = .none }, - .{ .depthTest = true, .depthWrite = true }, - .{ .attachments = &.{.alphaBlending} }, - ); - } - - pub fn deinit() void { - pipeline.deinit(); - } - - var posBeforeBlock: Vec3i = undefined; - var neighborOfSelection: chunk.Neighbor = undefined; - pub var selectedBlockPos: ?Vec3i = null; - var lastSelectedBlockPos: Vec3i = undefined; - var currentBlockProgress: f32 = 0; - var currentSwingProgress: f32 = 0; - var currentSwingTime: f32 = 0; - var selectionMin: Vec3f = undefined; - var selectionMax: Vec3f = undefined; - var selectionNormal: Vec3f = undefined; - var lastPos: Vec3d = undefined; - var lastDir: Vec3f = undefined; - pub fn select(pos: Vec3d, _dir: Vec3f, item: main.items.Item) void { - 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 = 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 = @floor(pos); - - var total_tMax: f64 = 0; - - selectedBlockPos = null; - - while (total_tMax < closestDistance) { - const block = mesh_storage.getBlockFromRenderThread(voxelPos[0], voxelPos[1], voxelPos[2]) orelse break; - if (block.typ != 0) blk: { - if (!block.isSelectableByItem(item)) break :blk; - - const relativePlayerPos: Vec3f = @floatCast(pos - @as(Vec3d, @floatFromInt(voxelPos))); - if (block.mode().rayIntersection(block, item, relativePlayerPos, _dir)) |intersection| { - if (intersection.distance <= closestDistance) { - selectedBlockPos = voxelPos; - selectionMin = intersection.min; - selectionMax = intersection.max; - selectionNormal = intersection.normal; - 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]; - neighborOfSelection = if (step[0] == 1) .dirPosX else .dirNegX; - } else { - voxelPos[2] +%= step[2]; - total_tMax = tMax[2]; - tMax[2] += tDelta[2]; - neighborOfSelection = if (step[2] == 1) .dirUp else .dirDown; - } - } else { - if (tMax[1] < tMax[2]) { - voxelPos[1] +%= step[1]; - total_tMax = tMax[1]; - tMax[1] += tDelta[1]; - neighborOfSelection = if (step[1] == 1) .dirPosY else .dirNegY; - } else { - voxelPos[2] +%= step[2]; - total_tMax = tMax[2]; - tMax[2] += tDelta[2]; - neighborOfSelection = if (step[2] == 1) .dirUp else .dirDown; - } - } - } - // TODO: Test entities - } - - fn canPlaceBlock(pos: Vec3i, block: main.blocks.Block) bool { - if (main.physics.collision.collideWithBlock(block, pos[0], pos[1], pos[2], main.game.Player.getPosBlocking() + main.game.Player.outerBoundingBox.center(), main.game.Player.outerBoundingBox.extent(), .{ 0, 0, 0 }) != null) { - return false; - } - return true; // TODO: Check other entities - } - - pub fn placeBlock(inventory: main.items.Inventory.ClientInventory, slot: u32) void { - if (selectedBlockPos) |selectedPos| { - var oldBlock = mesh_storage.getBlockFromRenderThread(selectedPos[0], selectedPos[1], selectedPos[2]) orelse return; - var block = oldBlock; - switch (inventory.getItem(slot)) { - .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: Vec3f = @floatCast(lastPos - @as(Vec3d, @floatFromInt(selectedPos))); - if (rotationMode.generateData(main.game.world.?, selectedPos, relPos, lastDir, neighborDir, null, &block, .{ .typ = 0, .data = 0 }, false)) { - if (!canPlaceBlock(selectedPos, block)) return; - updateBlockAndSendUpdate(inventory, slot, selectedPos, oldBlock, block); - return; - } - } else { - if (rotationMode.modifyBlock(&block, itemBlock)) { - if (!canPlaceBlock(selectedPos, block)) return; - updateBlockAndSendUpdate(inventory, slot, selectedPos, oldBlock, block); - return; - } - } - // Check the block in front of it: - const neighborPos = posBeforeBlock; - neighborDir = selectedPos - posBeforeBlock; - const relPos: Vec3f = @floatCast(lastPos - @as(Vec3d, @floatFromInt(neighborPos))); - const neighborBlock = block; - oldBlock = mesh_storage.getBlockFromRenderThread(neighborPos[0], neighborPos[1], neighborPos[2]) orelse return; - block = oldBlock; - if (block.typ == itemBlock) { - if (rotationMode.generateData(main.game.world.?, neighborPos, relPos, lastDir, neighborDir, neighborOfSelection, &block, neighborBlock, false)) { - if (!canPlaceBlock(neighborPos, block)) return; - updateBlockAndSendUpdate(inventory, slot, neighborPos, oldBlock, block); - return; - } - } else { - if (!block.replaceable()) return; - block.typ = itemBlock; - block.data = 0; - if (rotationMode.generateData(main.game.world.?, neighborPos, relPos, lastDir, neighborDir, neighborOfSelection, &block, neighborBlock, true)) { - if (!canPlaceBlock(neighborPos, block)) return; - updateBlockAndSendUpdate(inventory, slot, neighborPos, oldBlock, block); - return; - } - } - } - if (std.mem.eql(u8, baseItem.id(), "cubyz:selection_wand")) { - game.Player.selectionPosition2 = selectedPos; - main.network.protocols.genericUpdate.sendWorldEditPos(main.game.world.?.conn, .selectedPos2, selectedPos); - return; - } - }, - .proceduralItem => |proceduralItem| { - _ = proceduralItem; // TODO: Tools might change existing blocks. - }, - .null => {}, - } - } - } - - pub fn breakBlock(inventory: main.items.Inventory.ClientInventory, slot: u32, deltaTime: f64) void { - if (selectedBlockPos) |selectedPos| { - const stack = inventory.getStack(slot); - const isSelectionWand = stack.item == .baseItem and std.mem.eql(u8, stack.item.baseItem.id(), "cubyz:selection_wand"); - if (isSelectionWand) { - game.Player.selectionPosition1 = selectedPos; - main.network.protocols.genericUpdate.sendWorldEditPos(main.game.world.?.conn, .selectedPos1, selectedPos); - return; - } - - if (@reduce(.Or, lastSelectedBlockPos != selectedPos)) { - mesh_storage.removeBreakingAnimation(lastSelectedBlockPos); - currentSwingProgress = 0; - currentSwingTime = 0; - lastSelectedBlockPos = selectedPos; - currentBlockProgress = 0; - } - const block = mesh_storage.getBlockFromRenderThread(selectedPos[0], selectedPos[1], selectedPos[2]) orelse return; - const holdingTargetedBlock = stack.item == .baseItem and stack.item.baseItem.block() == block.typ; - if ((block.hasTag(.fluid) or block.hasTag(.air)) and !holdingTargetedBlock) return; - - const relPos: Vec3f = @floatCast(lastPos - @as(Vec3d, @floatFromInt(selectedPos))); - - main.sync.ClientSide.mutex.lock(); - if (!game.Player.isCreative()) { - var damage: f32 = main.game.Player.defaultBlockDamage; - const isProceduralItem = stack.item == .proceduralItem; - if (isProceduralItem) { - damage = stack.item.proceduralItem.getBlockDamage(block); - } - damage -= block.blockResistance(); - if (damage > 0) { - const swingTime = if (isProceduralItem and stack.item.proceduralItem.isEffectiveOn(block)) 1.0 / stack.item.proceduralItem.getProperty(.swingSpeed) else 0.5; - if (currentSwingTime > swingTime) { - currentSwingProgress = 0; - currentSwingTime = 0; - } - if (currentSwingTime == 0) { - const swings = @ceil(block.blockHealth() / damage); - const damagePerSwing = block.blockHealth() / swings; - currentSwingTime = damagePerSwing / damage * swingTime; - } - currentSwingProgress += @floatCast(deltaTime); - while (currentSwingProgress > currentSwingTime) { - currentSwingProgress -= currentSwingTime; - currentBlockProgress += damage * currentSwingTime / swingTime / block.blockHealth(); - if (currentBlockProgress > 0.9999) break; - const swings = @ceil(block.blockHealth() / damage); - const damagePerSwing = block.blockHealth() / swings; - currentSwingTime = damagePerSwing / damage * swingTime; - } - if (currentBlockProgress < 0.9999) { - mesh_storage.removeBreakingAnimation(lastSelectedBlockPos); - if (currentBlockProgress != 0) { - mesh_storage.addBreakingAnimation(lastSelectedBlockPos, currentBlockProgress); - } - main.sync.ClientSide.mutex.unlock(); - - return; - } else { - currentSwingProgress = 0; - mesh_storage.removeBreakingAnimation(lastSelectedBlockPos); - currentBlockProgress = 0; - currentSwingTime = 0; - } - } else { - main.sync.ClientSide.mutex.unlock(); - return; - } - } else { - mesh_storage.removeBreakingAnimation(lastSelectedBlockPos); - } - - var newBlock = block; - block.mode().onBlockBreaking(inventory.getStack(slot).item, relPos, lastDir, &newBlock); - main.sync.ClientSide.mutex.unlock(); - - if (newBlock != block) { - updateBlockAndSendUpdate(inventory, slot, selectedPos, block, newBlock); - } - } - } - - fn updateBlockAndSendUpdate(source: main.items.Inventory.ClientInventory, slot: u32, pos: Vec3i, oldBlock: blocks.Block, newBlock: blocks.Block) void { - main.sync.ClientSide.executeCommand(.{ - .updateBlock = .{ - .source = .{ .inv = source.super, .slot = slot }, - .pos = pos, - .dropLocation = .{ - .normalDir = selectionNormal, - .min = selectionMin, - .max = selectionMax, - }, - .oldBlock = oldBlock, - .newBlock = newBlock, - }, - }); - mesh_storage.updateBlock(.{ .pos = pos, .newBlock = newBlock, .blockEntityData = &.{} }); - } - - pub fn drawCube(projectionMatrix: Mat4f, viewMatrix: Mat4f, relativePositionToPlayer: Vec3d, min: Vec3f, max: Vec3f) void { - pipeline.bind(null); - - 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.glUniform1f(uniforms.lineSize, 1.0 / 128.0); - - main.renderer.chunk_meshing.vao.bind(); - c.glDrawElements(c.GL_TRIANGLES, 12 * 6 * 6, c.GL_UNSIGNED_INT, null); - } - - pub fn render(projectionMatrix: Mat4f, viewMatrix: Mat4f, playerPos: Vec3d) void { - if (main.gui.hideGui) return; - if (selectedBlockPos) |_selectedBlockPos| { - drawCube(projectionMatrix, viewMatrix, @as(Vec3d, @floatFromInt(_selectedBlockPos)) - playerPos, selectionMin, selectionMax); - } - if (game.Player.selectionPosition1) |pos1| { - if (game.Player.selectionPosition2) |pos2| { - const bottomLeft: Vec3i = @min(pos1, pos2); - const topRight: Vec3i = @max(pos1, pos2); - drawCube(projectionMatrix, viewMatrix, @as(Vec3d, @floatFromInt(bottomLeft)) - playerPos, .{ 0, 0, 0 }, @floatFromInt(topRight - bottomLeft + Vec3i{ 1, 1, 1 })); - } - } - } + var pipeline: graphics.Pipeline = undefined; + var uniforms: struct { + projectionMatrix: c_int, + viewMatrix: c_int, + modelPosition: c_int, + lowerBounds: c_int, + upperBounds: c_int, + lineSize: c_int, + } = undefined; + + pub fn init() void { + pipeline = graphics.Pipeline.init( + "assets/cubyz/shaders/block_selection_vertex.vert", + "assets/cubyz/shaders/block_selection_fragment.frag", + "", + &uniforms, + graphics.VertexArray.EmptyVertex, + &.{}, + .{.cullMode = .none}, + .{.depthTest = true, .depthWrite = true}, + .{.attachments = &.{.alphaBlending}}, + ); + } + + pub fn deinit() void { + pipeline.deinit(); + } + + var posBeforeBlock: Vec3i = undefined; + var neighborOfSelection: chunk.Neighbor = undefined; + pub var selectedBlockPos: ?Vec3i = null; + var lastSelectedBlockPos: Vec3i = undefined; + var currentBlockProgress: f32 = 0; + var currentSwingProgress: f32 = 0; + var currentSwingTime: f32 = 0; + var selectionMin: Vec3f = undefined; + var selectionMax: Vec3f = undefined; + var selectionNormal: Vec3f = undefined; + var lastPos: Vec3d = undefined; + var lastDir: Vec3f = undefined; + pub fn select(pos: Vec3d, _dir: Vec3f, item: main.items.Item) void { + 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 = 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 = @floor(pos); + + var total_tMax: f64 = 0; + + selectedBlockPos = null; + + while (total_tMax < closestDistance) { + const block = mesh_storage.getBlockFromRenderThread(voxelPos[0], voxelPos[1], voxelPos[2]) orelse break; + if (block.typ != 0) blk: { + if (!block.isSelectableByItem(item)) break :blk; + + const relativePlayerPos: Vec3f = @floatCast(pos - @as(Vec3d, @floatFromInt(voxelPos))); + if (block.mode().rayIntersection(block, item, relativePlayerPos, _dir)) |intersection| { + if (intersection.distance <= closestDistance) { + selectedBlockPos = voxelPos; + selectionMin = intersection.min; + selectionMax = intersection.max; + selectionNormal = intersection.normal; + 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]; + neighborOfSelection = if (step[0] == 1) .dirPosX else .dirNegX; + } else { + voxelPos[2] +%= step[2]; + total_tMax = tMax[2]; + tMax[2] += tDelta[2]; + neighborOfSelection = if (step[2] == 1) .dirUp else .dirDown; + } + } else { + if (tMax[1] < tMax[2]) { + voxelPos[1] +%= step[1]; + total_tMax = tMax[1]; + tMax[1] += tDelta[1]; + neighborOfSelection = if (step[1] == 1) .dirPosY else .dirNegY; + } else { + voxelPos[2] +%= step[2]; + total_tMax = tMax[2]; + tMax[2] += tDelta[2]; + neighborOfSelection = if (step[2] == 1) .dirUp else .dirDown; + } + } + } + // TODO: Test entities + } + + fn canPlaceBlock(pos: Vec3i, block: main.blocks.Block) bool { + if (main.physics.collision.collideWithBlock(block, pos[0], pos[1], pos[2], main.game.Player.getPosBlocking() + main.game.Player.outerBoundingBox.center(), main.game.Player.outerBoundingBox.extent(), .{0, 0, 0}) != null) { + return false; + } + return true; // TODO: Check other entities + } + + pub fn placeBlock(inventory: main.items.Inventory.ClientInventory, slot: u32) void { + if (selectedBlockPos) |selectedPos| { + var oldBlock = mesh_storage.getBlockFromRenderThread(selectedPos[0], selectedPos[1], selectedPos[2]) orelse return; + var block = oldBlock; + switch (inventory.getItem(slot)) { + .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: Vec3f = @floatCast(lastPos - @as(Vec3d, @floatFromInt(selectedPos))); + if (rotationMode.generateData(main.game.world.?, selectedPos, relPos, lastDir, neighborDir, null, &block, .{.typ = 0, .data = 0}, false)) { + if (!canPlaceBlock(selectedPos, block)) return; + updateBlockAndSendUpdate(inventory, slot, selectedPos, oldBlock, block); + return; + } + } else { + if (rotationMode.modifyBlock(&block, itemBlock)) { + if (!canPlaceBlock(selectedPos, block)) return; + updateBlockAndSendUpdate(inventory, slot, selectedPos, oldBlock, block); + return; + } + } + // Check the block in front of it: + const neighborPos = posBeforeBlock; + neighborDir = selectedPos - posBeforeBlock; + const relPos: Vec3f = @floatCast(lastPos - @as(Vec3d, @floatFromInt(neighborPos))); + const neighborBlock = block; + oldBlock = mesh_storage.getBlockFromRenderThread(neighborPos[0], neighborPos[1], neighborPos[2]) orelse return; + block = oldBlock; + if (block.typ == itemBlock) { + if (rotationMode.generateData(main.game.world.?, neighborPos, relPos, lastDir, neighborDir, neighborOfSelection, &block, neighborBlock, false)) { + if (!canPlaceBlock(neighborPos, block)) return; + updateBlockAndSendUpdate(inventory, slot, neighborPos, oldBlock, block); + return; + } + } else { + if (!block.replaceable()) return; + block.typ = itemBlock; + block.data = 0; + if (rotationMode.generateData(main.game.world.?, neighborPos, relPos, lastDir, neighborDir, neighborOfSelection, &block, neighborBlock, true)) { + if (!canPlaceBlock(neighborPos, block)) return; + updateBlockAndSendUpdate(inventory, slot, neighborPos, oldBlock, block); + return; + } + } + } + if (std.mem.eql(u8, baseItem.id(), "cubyz:selection_wand")) { + game.Player.selectionPosition2 = selectedPos; + main.network.protocols.genericUpdate.sendWorldEditPos(main.game.world.?.conn, .selectedPos2, selectedPos); + return; + } + }, + .proceduralItem => |proceduralItem| { + _ = proceduralItem; // TODO: Tools might change existing blocks. + }, + .null => {}, + } + } + } + + pub fn breakBlock(inventory: main.items.Inventory.ClientInventory, slot: u32, deltaTime: f64) void { + if (selectedBlockPos) |selectedPos| { + const stack = inventory.getStack(slot); + const isSelectionWand = stack.item == .baseItem and std.mem.eql(u8, stack.item.baseItem.id(), "cubyz:selection_wand"); + if (isSelectionWand) { + game.Player.selectionPosition1 = selectedPos; + main.network.protocols.genericUpdate.sendWorldEditPos(main.game.world.?.conn, .selectedPos1, selectedPos); + return; + } + + if (@reduce(.Or, lastSelectedBlockPos != selectedPos)) { + mesh_storage.removeBreakingAnimation(lastSelectedBlockPos); + currentSwingProgress = 0; + currentSwingTime = 0; + lastSelectedBlockPos = selectedPos; + currentBlockProgress = 0; + } + const block = mesh_storage.getBlockFromRenderThread(selectedPos[0], selectedPos[1], selectedPos[2]) orelse return; + const holdingTargetedBlock = stack.item == .baseItem and stack.item.baseItem.block() == block.typ; + if ((block.hasTag(.fluid) or block.hasTag(.air)) and !holdingTargetedBlock) return; + + const relPos: Vec3f = @floatCast(lastPos - @as(Vec3d, @floatFromInt(selectedPos))); + + main.sync.ClientSide.mutex.lock(); + if (!game.Player.isCreative()) { + var damage: f32 = main.game.Player.defaultBlockDamage; + const isProceduralItem = stack.item == .proceduralItem; + if (isProceduralItem) { + damage = stack.item.proceduralItem.getBlockDamage(block); + } + damage -= block.blockResistance(); + if (damage > 0) { + const swingTime = if (isProceduralItem and stack.item.proceduralItem.isEffectiveOn(block)) 1.0/stack.item.proceduralItem.getProperty(.swingSpeed) else 0.5; + if (currentSwingTime > swingTime) { + currentSwingProgress = 0; + currentSwingTime = 0; + } + if (currentSwingTime == 0) { + const swings = @ceil(block.blockHealth()/damage); + const damagePerSwing = block.blockHealth()/swings; + currentSwingTime = damagePerSwing/damage*swingTime; + } + currentSwingProgress += @floatCast(deltaTime); + while (currentSwingProgress > currentSwingTime) { + currentSwingProgress -= currentSwingTime; + currentBlockProgress += damage*currentSwingTime/swingTime/block.blockHealth(); + if (currentBlockProgress > 0.9999) break; + const swings = @ceil(block.blockHealth()/damage); + const damagePerSwing = block.blockHealth()/swings; + currentSwingTime = damagePerSwing/damage*swingTime; + } + if (currentBlockProgress < 0.9999) { + mesh_storage.removeBreakingAnimation(lastSelectedBlockPos); + if (currentBlockProgress != 0) { + mesh_storage.addBreakingAnimation(lastSelectedBlockPos, currentBlockProgress); + } + main.sync.ClientSide.mutex.unlock(); + + return; + } else { + currentSwingProgress = 0; + mesh_storage.removeBreakingAnimation(lastSelectedBlockPos); + currentBlockProgress = 0; + currentSwingTime = 0; + } + } else { + main.sync.ClientSide.mutex.unlock(); + return; + } + } else { + mesh_storage.removeBreakingAnimation(lastSelectedBlockPos); + } + + var newBlock = block; + block.mode().onBlockBreaking(inventory.getStack(slot).item, relPos, lastDir, &newBlock); + main.sync.ClientSide.mutex.unlock(); + + if (newBlock != block) { + updateBlockAndSendUpdate(inventory, slot, selectedPos, block, newBlock); + } + } + } + + fn updateBlockAndSendUpdate(source: main.items.Inventory.ClientInventory, slot: u32, pos: Vec3i, oldBlock: blocks.Block, newBlock: blocks.Block) void { + main.sync.ClientSide.executeCommand(.{ + .updateBlock = .{ + .source = .{.inv = source.super, .slot = slot}, + .pos = pos, + .dropLocation = .{ + .normalDir = selectionNormal, + .min = selectionMin, + .max = selectionMax, + }, + .oldBlock = oldBlock, + .newBlock = newBlock, + }, + }); + mesh_storage.updateBlock(.{.pos = pos, .newBlock = newBlock, .blockEntityData = &.{}}); + } + + pub fn drawCube(projectionMatrix: Mat4f, viewMatrix: Mat4f, relativePositionToPlayer: Vec3d, min: Vec3f, max: Vec3f) void { + pipeline.bind(null); + + 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.glUniform1f(uniforms.lineSize, 1.0/128.0); + + main.renderer.chunk_meshing.vao.bind(); + c.glDrawElements(c.GL_TRIANGLES, 12*6*6, c.GL_UNSIGNED_INT, null); + } + + pub fn render(projectionMatrix: Mat4f, viewMatrix: Mat4f, playerPos: Vec3d) void { + if (main.gui.hideGui) return; + if (selectedBlockPos) |_selectedBlockPos| { + drawCube(projectionMatrix, viewMatrix, @as(Vec3d, @floatFromInt(_selectedBlockPos)) - playerPos, selectionMin, selectionMax); + } + if (game.Player.selectionPosition1) |pos1| { + if (game.Player.selectionPosition2) |pos2| { + const bottomLeft: Vec3i = @min(pos1, pos2); + const topRight: Vec3i = @max(pos1, pos2); + drawCube(projectionMatrix, viewMatrix, @as(Vec3d, @floatFromInt(bottomLeft)) - playerPos, .{0, 0, 0}, @floatFromInt(topRight - bottomLeft + Vec3i{1, 1, 1})); + } + } + } }; From 72dd615a1a710d3af48e819842c0b86d16c7fe61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Fernandes?= Date: Fri, 8 May 2026 10:34:02 +0100 Subject: [PATCH 12/14] more reordering --- src/blocks.zig | 19 +++++++++---------- src/gui/windows/controls.zig | 1 - src/itemdrop.zig | 8 ++++---- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/blocks.zig b/src/blocks.zig index 128825fa2f..9dea78e3c0 100644 --- a/src/blocks.zig +++ b/src/blocks.zig @@ -3,16 +3,6 @@ const std = @import("std"); const main = @import("main"); const Tag = main.Tag; const utils = main.utils; -const ClientBlockCallback = main.callbacks.ClientBlockCallback; -const ServerBlockCallback = main.callbacks.ServerBlockCallback; -const BlockTouchCallback = main.callbacks.BlockTouchCallback; -const sbb = main.server.terrain.structure_building_blocks; -const blueprint = main.blueprint; -const Assets = main.assets.Assets; -const Entity = main.server.Entity; - -const c = @import("c"); - const ZonElement = @import("zon.zig").ZonElement; const chunk = @import("chunk.zig"); const Neighbor = chunk.Neighbor; @@ -29,8 +19,17 @@ const ModelIndex = models.ModelIndex; const rotation = @import("rotation.zig"); const RotationMode = rotation.RotationMode; const Degrees = rotation.Degrees; +const Entity = main.server.Entity; const block_entity = @import("block_entity.zig"); const BlockEntityType = block_entity.BlockEntityType; +const ClientBlockCallback = main.callbacks.ClientBlockCallback; +const ServerBlockCallback = main.callbacks.ServerBlockCallback; +const BlockTouchCallback = main.callbacks.BlockTouchCallback; +const sbb = main.server.terrain.structure_building_blocks; +const blueprint = main.blueprint; +const Assets = main.assets.Assets; + +const c = @import("c"); pub const maxBlockCount: usize = 65536; // 16 bit limit diff --git a/src/gui/windows/controls.zig b/src/gui/windows/controls.zig index 4355d888e7..34c19943b4 100644 --- a/src/gui/windows/controls.zig +++ b/src/gui/windows/controls.zig @@ -8,7 +8,6 @@ const c = @import("c"); const gui = @import("../gui.zig"); const GuiComponent = gui.GuiComponent; const GuiWindow = gui.GuiWindow; - const Button = @import("../components/Button.zig"); const CheckBox = @import("../components/CheckBox.zig"); const HorizontalList = @import("../components/HorizontalList.zig"); diff --git a/src/itemdrop.zig b/src/itemdrop.zig index 292ebeb477..13c8e939cc 100644 --- a/src/itemdrop.zig +++ b/src/itemdrop.zig @@ -5,16 +5,13 @@ const chunk_zig = @import("chunk.zig"); const ServerChunk = chunk_zig.ServerChunk; const game = @import("game.zig"); const World = game.World; +const ServerWorld = main.server.ServerWorld; const graphics = @import("graphics.zig"); const items = @import("items.zig"); const ItemStack = items.ItemStack; const ZonElement = @import("zon.zig").ZonElement; const main = @import("main"); const physics = main.physics; -const ServerWorld = main.server.ServerWorld; -const BinaryReader = main.utils.BinaryReader; -const BinaryWriter = main.utils.BinaryWriter; -const NeverFailingAllocator = main.heap.NeverFailingAllocator; const random = @import("random.zig"); const settings = @import("settings.zig"); const utils = @import("utils.zig"); @@ -23,6 +20,9 @@ const Mat4f = vec.Mat4f; const Vec3d = vec.Vec3d; const Vec3f = vec.Vec3f; const Vec3i = vec.Vec3i; +const BinaryReader = main.utils.BinaryReader; +const BinaryWriter = main.utils.BinaryWriter; +const NeverFailingAllocator = main.heap.NeverFailingAllocator; const c = @import("c"); From 1183ebf28fc834d910fd45aaa9b5579a6f918aa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Fernandes?= Date: Fri, 8 May 2026 10:38:15 +0100 Subject: [PATCH 13/14] put worlds together --- src/entityComponent/_template.zig | 2 +- src/entityComponent/bag.zig | 2 +- src/entitySystem/_template.zig | 2 +- src/entitySystem/modelRenderer.zig | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/entityComponent/_template.zig b/src/entityComponent/_template.zig index 9f524fb423..0f4774fe67 100644 --- a/src/entityComponent/_template.zig +++ b/src/entityComponent/_template.zig @@ -3,7 +3,6 @@ const std = @import("std"); const main = @import("main"); const chunk = main.chunk; const game = main.game; -const World = game.World; const graphics = main.graphics; const ZonElement = main.ZonElement; const renderer = main.renderer; @@ -21,6 +20,7 @@ const NeverFailingAllocator = main.heap.NeverFailingAllocator; const blocks = main.blocks; const chunk_zig = main.chunk; const ServerChunk = chunk_zig.ServerChunk; +const World = game.World; const ServerWorld = main.server.ServerWorld; const items = main.items; const ItemStack = items.ItemStack; diff --git a/src/entityComponent/bag.zig b/src/entityComponent/bag.zig index 99ecf58cb9..8ae84d6359 100644 --- a/src/entityComponent/bag.zig +++ b/src/entityComponent/bag.zig @@ -3,7 +3,6 @@ const std = @import("std"); const main = @import("main"); const chunk = main.chunk; const game = main.game; -const World = game.World; const graphics = main.graphics; const ZonElement = main.ZonElement; const renderer = main.renderer; @@ -21,6 +20,7 @@ const NeverFailingAllocator = main.heap.NeverFailingAllocator; const blocks = main.blocks; const chunk_zig = main.chunk; const ServerChunk = chunk_zig.ServerChunk; +const World = game.World; const ServerWorld = main.server.ServerWorld; const items = main.items; const ItemStack = items.ItemStack; diff --git a/src/entitySystem/_template.zig b/src/entitySystem/_template.zig index e84932959d..d3846a50d2 100644 --- a/src/entitySystem/_template.zig +++ b/src/entitySystem/_template.zig @@ -3,7 +3,6 @@ const std = @import("std"); const main = @import("main"); const chunk = main.chunk; const game = main.game; -const World = game.World; const graphics = main.graphics; const ZonElement = main.ZonElement; const renderer = main.renderer; @@ -21,6 +20,7 @@ const NeverFailingAllocator = main.heap.NeverFailingAllocator; const blocks = main.blocks; const chunk_zig = main.chunk; const ServerChunk = chunk_zig.ServerChunk; +const World = game.World; const ServerWorld = main.server.ServerWorld; const items = main.items; const ItemStack = items.ItemStack; diff --git a/src/entitySystem/modelRenderer.zig b/src/entitySystem/modelRenderer.zig index 0cc6633b56..616faf46bb 100644 --- a/src/entitySystem/modelRenderer.zig +++ b/src/entitySystem/modelRenderer.zig @@ -3,7 +3,6 @@ const std = @import("std"); const main = @import("main"); const chunk = main.chunk; const game = main.game; -const World = game.World; const graphics = main.graphics; const ZonElement = main.ZonElement; const renderer = main.renderer; @@ -21,6 +20,7 @@ const NeverFailingAllocator = main.heap.NeverFailingAllocator; const blocks = main.blocks; const chunk_zig = main.chunk; const ServerChunk = chunk_zig.ServerChunk; +const World = game.World; const ServerWorld = main.server.ServerWorld; const items = main.items; const ItemStack = items.ItemStack; From a98b90ec38a88d9e57cc1aa2e986e9acd14a7b8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Fernandes?= Date: Fri, 8 May 2026 10:40:21 +0100 Subject: [PATCH 14/14] formatting --- src/entityComponent/bag.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/entityComponent/bag.zig b/src/entityComponent/bag.zig index 8ae84d6359..47d6b94ade 100644 --- a/src/entityComponent/bag.zig +++ b/src/entityComponent/bag.zig @@ -20,7 +20,7 @@ const NeverFailingAllocator = main.heap.NeverFailingAllocator; const blocks = main.blocks; const chunk_zig = main.chunk; const ServerChunk = chunk_zig.ServerChunk; -const World = game.World; +const World = game.World; const ServerWorld = main.server.ServerWorld; const items = main.items; const ItemStack = items.ItemStack;