diff --git a/assets/blocks/Grass.json b/assets/blocks/Grass.json index 0eb5910..aef43c8 100644 --- a/assets/blocks/Grass.json +++ b/assets/blocks/Grass.json @@ -1,10 +1,22 @@ { - "materials": [ - "GrassSide.json", - "GrassSide.json", - "GrassSide.json", - "GrassSide.json", - "GrassTop.json", - "Dirt.json" - ] + "walls": { + "positive_x": { + "material": "GrassSide.json" + }, + "negative_x": { + "material": "GrassSide.json" + }, + "positive_y": { + "material": "GrassSide.json" + }, + "negative_y": { + "material": "GrassSide.json" + }, + "positive_z": { + "material": "GrassTop.json" + }, + "negative_z": { + "material": "Dirt.json" + } + } } diff --git a/assets/blocks/OakLogX.json b/assets/blocks/OakLogX.json index 91fac1e..704d45d 100644 --- a/assets/blocks/OakLogX.json +++ b/assets/blocks/OakLogX.json @@ -1,10 +1,22 @@ { - "materials": [ - "OakLogTop.json", - "OakLogTop.json", - "OakLog.json", - "OakLog.json", - "OakLog.json", - "OakLog.json" - ] + "walls": { + "positive_x": { + "material": "OakLogTop.json" + }, + "negative_x": { + "material": "OakLogTop.json" + }, + "positive_y": { + "material": "OakLog.json" + }, + "negative_y": { + "material": "OakLog.json" + }, + "positive_z": { + "material": "OakLog.json" + }, + "negative_z": { + "material": "OakLog.json" + } + } } diff --git a/assets/blocks/OakLogY.json b/assets/blocks/OakLogY.json index 015bcc7..2fd93a6 100644 --- a/assets/blocks/OakLogY.json +++ b/assets/blocks/OakLogY.json @@ -1,10 +1,22 @@ { - "materials": [ - "OakLog.json", - "OakLog.json", - "OakLogTop.json", - "OakLogTop.json", - "OakLog.json", - "OakLog.json" - ] + "walls": { + "positive_x": { + "material": "OakLog.json" + }, + "negative_x": { + "material": "OakLog.json" + }, + "positive_y": { + "material": "OakLogTop.json" + }, + "negative_y": { + "material": "OakLogTop.json" + }, + "positive_z": { + "material": "OakLog.json" + }, + "negative_z": { + "material": "OakLog.json" + } + } } diff --git a/assets/blocks/OakLogZ.json b/assets/blocks/OakLogZ.json index d86b3bf..d08e9d8 100644 --- a/assets/blocks/OakLogZ.json +++ b/assets/blocks/OakLogZ.json @@ -1,10 +1,22 @@ { - "materials": [ - "OakLog.json", - "OakLog.json", - "OakLog.json", - "OakLog.json", - "OakLogTop.json", - "OakLogTop.json" - ] + "walls": { + "positive_x": { + "material": "OakLog.json" + }, + "negative_x": { + "material": "OakLog.json" + }, + "positive_y": { + "material": "OakLog.json" + }, + "negative_y": { + "material": "OakLog.json" + }, + "positive_z": { + "material": "OakLogTop.json" + }, + "negative_z": { + "material": "OakLogTop.json" + } + } } diff --git a/src/Game.zig b/src/Game.zig index 321708e..f715f21 100644 --- a/src/Game.zig +++ b/src/Game.zig @@ -1,8 +1,9 @@ const Game = @This(); const std = @import("std"); -const math = @import("math.zig"); const glfw = @import("zglfw"); +const math = @import("math.zig"); +const shaders = @import("shaders.zig"); const vk = @import("vulkan"); const worldgen = @import("worldgen.zig"); @@ -10,7 +11,6 @@ const Blocks = @import("assets/Blocks.zig"); const Chunk = @import("assets/Chunk.zig"); const CommandBuffer = @import("engine/CommandBuffer.zig"); const Engine = @import("engine/Engine.zig"); -const GenericBuffer = @import("engine/GenericBuffer.zig").GenericBuffer; const Iterator2 = math.Iterator2; const Materials = @import("assets/Materials.zig"); const Matrix4x4 = math.Matrix4x4; @@ -22,87 +22,6 @@ const Textures = @import("assets/Textures.zig"); const Vector2 = math.Vector2; const Vector2x8 = math.Vector2x8; const Vector3 = math.Vector3; -const Vector4 = math.Vector4; - -const PointLight = extern struct { - positionWS: [3]f32, - color: [3]f32, - - pub fn init(position_ws: Vector3, color: Vector3) PointLight { - return .{ - .positionWS = position_ws.asArray(), - .color = color.asArray(), - }; - } -}; - -const DirectionalLight = extern struct { - directionWS: [3]f32, - color: [3]f32, - - pub fn init(direction_ws: Vector3, color: Vector3) DirectionalLight { - return .{ - .directionWS = direction_ws.asArray(), - .color = color.asArray(), - }; - } -}; - -const GlobalUniforms = extern struct { - matrixWStoVS: [16]f32, - matrixVStoCS: [16]f32, - ambientLight: [3]f32, - - pub fn init(matrix_ws_to_vs: Matrix4x4, matrix_vs_to_cs: Matrix4x4, ambient_light: Vector3) GlobalUniforms { - return .{ - .matrixWStoVS = matrix_ws_to_vs.asArray(), - .matrixVStoCS = matrix_vs_to_cs.asArray(), - .ambientLight = ambient_light.asArray(), - }; - } -}; - -pub const ObjectUniforms = extern struct { - matrixOStoWS: [16]f32, - matrixOStoWSNormal: [16]f32, - - material: Materials.Id, - - pub fn init(matrix_os_to_ws: Matrix4x4, matrix_ow_to_ws_normal: Matrix4x4, material: Materials.Id) ObjectUniforms { - return .{ - .matrixOStoWS = matrix_os_to_ws.asArray(), - .matrixOStoWSNormal = matrix_ow_to_ws_normal.asArray(), - .material = material, - }; - } -}; - -const Vertex = extern struct { - positionOS: [3]f32, - texCoord: [2]u16, - normalOS: [3]i8, - tangentOS: [4]i8, - - pub fn init(position_os: Vector3, tex_coord: Vector2, normal_os: Vector3, tangent_os: Vector4) Vertex { - return .{ - .positionOS = position_os.asArray(), - .texCoord = tex_coord.asArrayNorm(u16), - .normalOS = normal_os.asArrayNorm(i8), - .tangentOS = tangent_os.asArrayNorm(i8), - }; - } -}; - -const GlobalUniformsBuffer = GenericBuffer(GlobalUniforms, void); -const PointLightBuffer = GenericBuffer(u32, PointLight); -const DirectionalLightBuffer = GenericBuffer(u32, DirectionalLight); -const ObjectUniformsBuffer = GenericBuffer(void, ObjectUniforms); - -const VertexBuffer = GenericBuffer(void, Vertex); -const IndexBuffer = GenericBuffer(void, u16); - -const main_vert_spv align(4) = @embedFile("shaders/main_vert.spv").*; -const main_frag_spv align(4) = @embedFile("shaders/main_frag.spv").*; allocator: std.mem.Allocator, engine: *Engine, @@ -114,15 +33,15 @@ global_descriptor_set: vk.DescriptorSet, pipeline_layout: vk.PipelineLayout, pipeline: vk.Pipeline, -vertex_buffer: VertexBuffer, -index_buffer: IndexBuffer, +vertex_buffer: shaders.VertexBuffer, +index_buffer: shaders.IndexBuffer, -global_uniforms: GlobalUniformsBuffer, +global_uniforms: shaders.GlobalUniformsBuffer, global_uniforms_staging_buffer: StagingBuffer, global_uniforms_transfer_command_buffer: CommandBuffer, global_uniforms_transfer_semaphores: []vk.Semaphore, -point_lights: PointLightBuffer, -directional_lights: DirectionalLightBuffer, +point_lights: shaders.PointLightBuffer, +directional_lights: shaders.DirectionalLightBuffer, sampler: vk.Sampler, blocks: Blocks, @@ -235,15 +154,15 @@ pub fn init(allocator: std.mem.Allocator, engine: *Engine, swapchain: *Swapchain errdefer engine.destroyPipelineLayout(pipeline_layout); engine.setObjectName(pipeline_layout, "PL", .{}); - const vertex_shader = try engine.createShaderModule(.{ .code = &main_vert_spv }); + const vertex_shader = try engine.createShaderModule(.{ .code = &shaders.main_vert_spv }); defer engine.destroyShaderModule(vertex_shader); engine.setObjectName(vertex_shader, "SM main_vert", .{}); - const fragment_shader = try engine.createShaderModule(.{ .code = &main_frag_spv }); + const fragment_shader = try engine.createShaderModule(.{ .code = &shaders.main_frag_spv }); defer engine.destroyShaderModule(fragment_shader); engine.setObjectName(fragment_shader, "SM main_frag", .{}); - var vertex_buffer = try VertexBuffer.init(engine, .{ + var vertex_buffer = try shaders.VertexBuffer.init(engine, .{ .usage = .vertex, .target_queue = .graphics, .array_capacity = 4, @@ -279,7 +198,7 @@ pub fn init(allocator: std.mem.Allocator, engine: *Engine, swapchain: *Swapchain }, }); - var index_buffer = try IndexBuffer.init(engine, .{ + var index_buffer = try shaders.IndexBuffer.init(engine, .{ .usage = .index, .target_queue = .graphics, .array_capacity = 6, @@ -290,7 +209,7 @@ pub fn init(allocator: std.mem.Allocator, engine: *Engine, swapchain: *Swapchain .elements = &.{ 0, 1, 2, 2, 1, 3 }, }); - var global_uniforms = try GlobalUniformsBuffer.init(engine, .{ + var global_uniforms = try shaders.GlobalUniformsBuffer.init(engine, .{ .usage = .uniform, .target_queue = .graphics, .name = "GlobalUniforms", @@ -298,7 +217,7 @@ pub fn init(allocator: std.mem.Allocator, engine: *Engine, swapchain: *Swapchain errdefer global_uniforms.deinit(engine); var global_uniforms_staging_buffer = try StagingBuffer.init(engine, .{ - .capacity = @sizeOf(GlobalUniforms), + .capacity = @sizeOf(shaders.GlobalUniforms), .target_queue = .graphics, }); errdefer global_uniforms_staging_buffer.deinit(engine); @@ -314,7 +233,7 @@ pub fn init(allocator: std.mem.Allocator, engine: *Engine, swapchain: *Swapchain .{ .src_offset = 0, .dst_offset = 0, - .size = @sizeOf(GlobalUniforms), + .size = @sizeOf(shaders.GlobalUniforms), }, }, ); @@ -341,7 +260,7 @@ pub fn init(allocator: std.mem.Allocator, engine: *Engine, swapchain: *Swapchain allocator.free(global_uniforms_transfer_semaphores); } - var point_lights = try PointLightBuffer.init(engine, .{ + var point_lights = try shaders.PointLightBuffer.init(engine, .{ .usage = .storage, .target_queue = .graphics, .array_capacity = max_point_lights, @@ -349,7 +268,7 @@ pub fn init(allocator: std.mem.Allocator, engine: *Engine, swapchain: *Swapchain }); errdefer point_lights.deinit(engine); - var directional_lights = try DirectionalLightBuffer.init(engine, .{ + var directional_lights = try shaders.DirectionalLightBuffer.init(engine, .{ .usage = .storage, .target_queue = .graphics, .array_capacity = max_directional_lights, @@ -374,7 +293,7 @@ pub fn init(allocator: std.mem.Allocator, engine: *Engine, swapchain: *Swapchain .vertex_binding_descriptions = &.{ .{ .binding = 0, - .stride = @sizeOf(Vertex), + .stride = @sizeOf(shaders.Vertex), .input_rate = .vertex, }, }, @@ -383,25 +302,25 @@ pub fn init(allocator: std.mem.Allocator, engine: *Engine, swapchain: *Swapchain .location = 0, .binding = 0, .format = .r32g32b32_sfloat, - .offset = @offsetOf(Vertex, "positionOS"), + .offset = @offsetOf(shaders.Vertex, "positionOS"), }, .{ .location = 1, .binding = 0, .format = .r16g16_unorm, - .offset = @offsetOf(Vertex, "texCoord"), + .offset = @offsetOf(shaders.Vertex, "texCoord"), }, .{ .location = 2, .binding = 0, .format = .r8g8b8_snorm, - .offset = @offsetOf(Vertex, "normalOS"), + .offset = @offsetOf(shaders.Vertex, "normalOS"), }, .{ .location = 3, .binding = 0, .format = .r8g8b8a8_snorm, - .offset = @offsetOf(Vertex, "tangentOS"), + .offset = @offsetOf(shaders.Vertex, "tangentOS"), }, }, }, @@ -502,19 +421,19 @@ pub fn init(allocator: std.mem.Allocator, engine: *Engine, swapchain: *Swapchain const global_descriptor_set = try engine.allocateDescriptorSet(.{ .descriptor_pool = descriptor_pool, .set_layout = global_descriptor_set_layout, - .variable_descriptor_count = @intCast(textures.textures.items.len), + .variable_descriptor_count = @intCast(textures.array.items.len), }); engine.setObjectName(global_descriptor_set, "DS Global", .{}); - const block_grass = try blocks.getOrLoadFilename(engine, &materials, &textures, "Grass.json", allocator); - const block_dirt = try blocks.getOrLoadFilename(engine, &materials, &textures, "Dirt.json", allocator); - const block_stone = try blocks.getOrLoadFilename(engine, &materials, &textures, "Stone.json", allocator); - const block_bedrock = try blocks.getOrLoadFilename(engine, &materials, &textures, "Bedrock.json", allocator); + const block_grass = try blocks.getOrLoad(engine, &materials, &textures, "Grass.json", allocator); + const block_dirt = try blocks.getOrLoad(engine, &materials, &textures, "Dirt.json", allocator); + const block_stone = try blocks.getOrLoad(engine, &materials, &textures, "Stone.json", allocator); + const block_bedrock = try blocks.getOrLoad(engine, &materials, &textures, "Bedrock.json", allocator); // VOLATILE Load all assets before this point - const descriptor_images = try allocator.alloc(vk.DescriptorImageInfo, textures.textures.items.len); - for (textures.textures.items, descriptor_images) |texture, *info| { + const descriptor_images = try allocator.alloc(vk.DescriptorImageInfo, textures.array.items.len); + for (textures.array.items, descriptor_images) |texture, *info| { info.* = .{ .sampler = .null_handle, .image_view = texture.image_view, @@ -680,13 +599,13 @@ pub fn init(allocator: std.mem.Allocator, engine: *Engine, swapchain: *Swapchain }, allocator); } - const point_lights_data: []const PointLight = &.{}; + const point_lights_data: []const shaders.PointLight = &.{}; try point_lights.write(engine, .{ .header = @intCast(point_lights_data.len), .elements = point_lights_data, }); - const directional_lights_data: []const DirectionalLight = &.{ + const directional_lights_data: []const shaders.DirectionalLight = &.{ .{ .directionWS = .{ 0, 0, -1 }, .color = .{ 0.3, 0.3, 0.3 }, @@ -797,7 +716,7 @@ pub fn update(self: *Game, dt: f32) void { const ambient_light = Vector3.init(0.01, 0.01, 0.01); - const global_uniforms_data: GlobalUniforms = .{ + const global_uniforms_data: shaders.GlobalUniforms = .{ .matrixWStoVS = matrix_ws_to_vs.asArray(), .matrixVStoCS = matrix_vs_to_cs.asArray(), .ambientLight = ambient_light.asArray(), diff --git a/src/assets/Blocks.zig b/src/assets/Blocks.zig index d9b44aa..18f680e 100644 --- a/src/assets/Blocks.zig +++ b/src/assets/Blocks.zig @@ -1,143 +1,135 @@ +//! Module for loading block definitions. + const Blocks = @This(); const std = @import("std"); +const voxels = @import("../voxels.zig"); + const Atom = @import("../engine/Atom.zig").Atom; const Engine = @import("../engine/Engine.zig"); const Materials = @import("Materials.zig"); const Textures = @import("Textures.zig"); -pub const Block = struct { - positive_x: Materials.Id, - negative_x: Materials.Id, - positive_y: Materials.Id, - negative_y: Materials.Id, - positive_z: Materials.Id, - negative_z: Materials.Id, +pub const Id = enum(u16) { + // VOLATILE Synchronize explicit values with `init` implementation. - pub fn initUniform(material: Materials.Id) Block { - return .{ - .positive_x = material, - .negative_x = material, - .positive_y = material, - .negative_y = material, - .positive_z = material, - .negative_z = material, - }; - } - - pub fn initSeparate( - positive_x: Materials.Id, - negative_x: Materials.Id, - positive_y: Materials.Id, - negative_y: Materials.Id, - positive_z: Materials.Id, - negative_z: Materials.Id, - ) Block { - return .{ - .positive_x = positive_x, - .negative_x = negative_x, - .positive_y = positive_y, - .negative_y = negative_y, - .positive_z = positive_z, - .negative_z = negative_z, - }; - } - - pub fn initArray(materials: [6]Materials.Id) Block { - return .{ - .positive_x = materials[0], - .negative_x = materials[1], - .positive_y = materials[2], - .negative_y = materials[3], - .positive_z = materials[4], - .negative_z = materials[5], - }; - } -}; - -map: Map, -blocks: Array, - -pub const capacity = std.math.maxInt(std.meta.Tag(Id)); - -pub const Key = struct { atom: Atom }; -pub const Id = enum(u12) { + /// An empty block. air = 0, _, - pub fn next(self: Id) Id { - return @enumFromInt(@intFromEnum(self) + 1); + /// Cast an integer into an ID. This can produce an invalid ID. + pub fn fromInt(value: u16) Id { + return @enumFromInt(value); + } + + /// Cast an index into an ID. This can produce an invalid ID. The caller + /// asserts that the index is not greater than the max ID value. + pub fn fromIndex(index: usize) Id { + std.debug.assert(index < max_blocks); + return @enumFromInt(@as(u16, @intCast(index))); + } + + /// Cast an index into an ID. This can produce an invalid ID. Returns an + /// error if the index is greater than the max ID value. + pub fn fromIndexSafe(index: usize) error{Overflow}!Id { + if (index >= max_blocks) return error.Overflow; + return @enumFromInt(@as(u16, @intCast(index))); + } + + /// Cast an ID into an integer. + pub fn toInt(self: Id) u16 { + return @intFromEnum(self); } }; -pub const Map = std.AutoHashMapUnmanaged(Key, Id); -pub const Array = std.ArrayList(Block); +pub const Key = struct { + /// Atom representing the filename of the block definition. + filename: Atom, +}; + +pub const Definition = struct { + pub const Wall = struct { + material: Materials.Id = .empty, + transform: voxels.Transform = .identity, + }; + + pub const Walls = std.enums.EnumFieldStruct(voxels.Orientation, Wall, .{}); + + walls: Walls = .{}, + + pub fn initUniform(material: Materials.Id) Definition { + const wall: Wall = .{ + .material = material, + }; + + return .{ + .walls = .{ + .negative_x = wall, + .positive_x = wall, + .negative_y = wall, + .positive_y = wall, + .negative_z = wall, + .positive_z = wall, + }, + }; + } +}; + +/// Maps a key value to a block definition ID. +map: std.AutoHashMapUnmanaged(Key, Id), +/// Stores all `Definition` structs and maps a block definition ID to a +/// `Definition` struct. +array: std.ArrayList(Definition), + +/// With `@sizeOf(Definition) == 24` and `max_blocks = 4096`, the block +/// definitions should take ~95.4 kiB in RAM. +pub const max_blocks = 4096; pub fn init(allocator: std.mem.Allocator) !Blocks { - var map: Map = .empty; + var map: std.AutoHashMapUnmanaged(Key, Id) = .empty; errdefer map.deinit(allocator); - try map.ensureTotalCapacity(allocator, capacity); + try map.ensureTotalCapacity(allocator, max_blocks); - var blocks: Array = try .initCapacity(allocator, capacity); - errdefer blocks.deinit(allocator); + var array: std.ArrayList(Definition) = try .initCapacity(allocator, max_blocks); + errdefer array.deinit(allocator); - const air = Block.initUniform(.empty); - blocks.appendAssumeCapacity(air); + // VOLATILE Synchronize with explicit values on top of `Id` type. + + array.appendAssumeCapacity(.{}); return .{ .map = map, - .blocks = blocks, + .array = array, }; } pub fn deinit(self: *Blocks, allocator: std.mem.Allocator) void { std.log.scoped(.deinit).debug("Deinitializing {*} with Allocator{{{*},{*}}}", .{ self, allocator.ptr, allocator.vtable }); - self.blocks.deinit(allocator); + self.array.deinit(allocator); self.map.deinit(allocator); self.* = undefined; } -pub fn getAtom(self: *const Blocks, atom: Atom) ?Id { - const key: Key = .{ .atom = atom }; - return self.map.get(key); +/// Get the ID of a block definition given its filename (as a string). Returns +/// `null` if such block hasn't been loaded. +pub fn get(self: *const Blocks, filename: []const u8) ?Id { + return self.map.get(.{ + // If the atom doesn't exist, then the block definition cannot possibly + // exist. + .filename = .fromStringIfExists(filename) orelse return null, + }); } -pub fn getFilename(self: *const Blocks, filename: []const u8) ?Id { - const atom = Atom.fromStringIfExists(filename) orelse return null; - const key: Key = .{ .atom = atom }; - return self.map.get(key); +/// Get the ID of a block definition given its filename (as an atom). Returns +/// `null` if such block hasn't been loaded. +pub fn getAtom(self: *const Blocks, filename: Atom) ?Id { + return self.map.get(.{ + .filename = filename, + }); } -pub fn getBlock(self: *const Blocks, id: Id) ?*Block { - const index: usize = @intFromEnum(id); - return if (index < self.blocks.items.len) &self.blocks.items[index] else null; -} - -pub fn getOrLoadAtom( - self: *Blocks, - engine: *Engine, - materials: *Materials, - textures: *Textures, - atom: Atom, - temp_allocator: std.mem.Allocator, -) !Id { - const key: Key = .{ .atom = atom }; - const entry = self.map.getOrPutAssumeCapacity(key); - - if (entry.found_existing) { - return entry.value_ptr.*; - } else { - errdefer _ = self.map.remove(key); - const block = try loadBlock(engine, materials, textures, Atom.toString(), temp_allocator); - const id = self.nextId(); - entry.value_ptr.* = id; - self.blocks.appendAssumeCapacity(block); - return id; - } -} - -pub fn getOrLoadFilename( +pub fn getOrLoad( self: *Blocks, engine: *Engine, materials: *Materials, @@ -145,18 +137,56 @@ pub fn getOrLoadFilename( filename: []const u8, temp_allocator: std.mem.Allocator, ) !Id { - const atom = try Atom.fromString(filename); - const key: Key = .{ .atom = atom }; - const entry = self.map.getOrPutAssumeCapacity(key); + const key: Key = .{ + // If the material already exists, then the atom must exist and the + // following line will not return any error. + .filename = try .fromString(filename), + }; - if (entry.found_existing) { - return entry.value_ptr.*; + // We don't use `getOrPutAssumeCapacity` method, because we might already be + // at full capacity, in which case we should return `error.OutOfBlocks`. + + if (self.map.get(key)) |id| { + return id; } else { - errdefer _ = self.map.remove(key); - const texture = try loadBlock(engine, materials, textures, filename, temp_allocator); - const id = self.nextId(); - entry.value_ptr.* = id; - self.blocks.appendAssumeCapacity(texture); + const id = Id.fromIndexSafe(self.array.items.len) catch |err| switch (err) { + error.Overflow => return error.OutOfBlocks, + }; + const def = try loadBlock(engine, materials, textures, filename, temp_allocator); + + self.map.putAssumeCapacityNoClobber(key, id); + self.array.appendAssumeCapacity(def); + + return id; + } +} + +pub fn getOrLoadAtom( + self: *Blocks, + engine: *Engine, + materials: *Materials, + textures: *Textures, + filename: Atom, + temp_allocator: std.mem.Allocator, +) !Id { + const key: Key = .{ + .filename = filename, + }; + + // We don't use `getOrPutAssumeCapacity` method, because we might already be + // at full capacity, in which case we should return `error.OutOfBlocks`. + + if (self.map.get(key)) |id| { + return id; + } else { + const id = Id.fromIndexSafe(self.array.items.len) catch |err| switch (err) { + error.Overflow => return error.OutOfBlocks, + }; + const def = try loadBlock(engine, materials, textures, filename.toString(), temp_allocator); + + self.map.putAssumeCapacityNoClobber(key, id); + self.array.appendAssumeCapacity(def); + return id; } } @@ -171,23 +201,23 @@ pub fn loadAll( const cwd = std.fs.cwd(); var dir = cwd.openDir("assets/blocks", .{ .iterate = true }) catch |err| { - std.log.err("Error while opening blocks directory: {s}", .{@errorName(err)}); + std.log.err("Error while opening block definitions directory: {s}", .{@errorName(err)}); return; }; defer dir.close(); var it = dir.iterate(); while (it.next() catch |err| { - std.log.err("Error while iterating over blocks directory: {s}", .{@errorName(err)}); + std.log.err("Error while iterating over block definitions directory: {s}", .{@errorName(err)}); return; }) |entry| { if (entry.kind != .file) { - std.log.warn("Skipping block entry {s}, which is not a file", .{entry.name}); + std.log.warn("Skipping block definition entry {s}, which is not a file", .{entry.name}); continue; } - _ = self.getOrLoadFilename(engine, materials, textures, entry.name, temp_allocator) catch |err| { - std.log.err("Error while loading block entry {s}: {s}", .{ entry.name, @errorName(err) }); + _ = self.getOrLoad(engine, materials, textures, entry.name, temp_allocator) catch |err| { + std.log.err("Error while loading block definition entry {s}: {s}", .{ entry.name, @errorName(err) }); }; } } @@ -198,10 +228,17 @@ fn loadBlock( textures: *Textures, filename: []const u8, temp_allocator: std.mem.Allocator, -) !Block { - const BlockJson = struct { +) !Definition { + const DefinitionJson = struct { + pub const Wall = struct { + material: ?[]const u8 = null, + transform: voxels.Transform = .identity, + }; + + pub const Walls = std.enums.EnumFieldStruct(voxels.Orientation, Wall, .{}); + material: ?[]const u8 = null, - materials: ?[6][]const u8 = null, + walls: ?Walls = null, }; std.log.debug("Loading block \"{s}\"...", .{filename}); @@ -212,7 +249,7 @@ fn loadBlock( defer dir.close(); // NOTE Buffer size approximated based on expected JSON structure. - var buffer: [256]u8 = undefined; + var buffer: [1024]u8 = undefined; const file = try dir.openFile(filename, .{}); defer file.close(); @@ -221,42 +258,44 @@ fn loadBlock( var json_reader: std.json.Reader = .init(temp_allocator, &file_reader.interface); defer json_reader.deinit(); - const parsed: std.json.Parsed(BlockJson) = try std.json.parseFromTokenSource(BlockJson, temp_allocator, &json_reader, .{ + const parsed: std.json.Parsed(DefinitionJson) = try std.json.parseFromTokenSource(DefinitionJson, temp_allocator, &json_reader, .{ .duplicate_field_behavior = .@"error", .ignore_unknown_fields = false, .allocate = .alloc_if_needed, }); defer parsed.deinit(); - const block_json = parsed.value; + const def_json = parsed.value; - const block: Block = blk: { - if (block_json.material) |name| { - if (block_json.materials != null) { - std.log.err("Block entry {s} has both properties \"material\" and \"materials\" defined, but exactly one of them must be defined", .{filename}); + const block: Definition = blk: { + if (def_json.material) |name| { + if (def_json.walls != null) { + std.log.err("Block definition entry {s} has both properties \"material\" and \"walls\" defined, but exactly one of them must be defined", .{filename}); return error.ParseError; } - const material = try materials.getOrLoadFilename(engine, textures, name, temp_allocator); + const material = try materials.getOrLoad(engine, textures, name, temp_allocator); break :blk .initUniform(material); } - if (block_json.materials) |names| { - var ids: [6]Materials.Id = undefined; - for (names, &ids) |name, *id| { - id.* = try materials.getOrLoadFilename(engine, textures, name, temp_allocator); - } - break :blk .initArray(ids); + if (def_json.walls) |walls| { + break :blk .{ + .walls = walls: { + var ret: Definition.Walls = undefined; + inline for (@typeInfo(voxels.Orientation).@"enum".fields) |field| { + @field(ret, field.name) = .{ + .material = try materials.getOrLoad(engine, textures, @field(walls, field.name).material, temp_allocator), + .transform = @field(walls, field.name).transform, + }; + } + break :walls ret; + }, + }; } - std.log.err("Block entry {s} has neither \"material\" or \"materials\" properties defined, but exactly one of them must be defined", .{filename}); + std.log.err("Block definition entry {s} has neither \"material\" or \"walls\" properties defined, but exactly one of them must be defined", .{filename}); return error.ParseError; }; return block; } - -fn nextId(self: *const Blocks) Id { - const index = self.blocks.items.len; - return @enumFromInt(index); -} diff --git a/src/assets/Chunk.zig b/src/assets/Chunk.zig index e1bbe3f..ffd22d9 100644 --- a/src/assets/Chunk.zig +++ b/src/assets/Chunk.zig @@ -1,9 +1,10 @@ const Chunk = @This(); const std = @import("std"); -const vk = @import("vulkan"); - const math = @import("../math.zig"); +const shaders = @import("../shaders.zig"); +const vk = @import("vulkan"); +const voxels = @import("../voxels.zig"); const Blocks = @import("Blocks.zig"); const CommandBuffer = @import("../engine/CommandBuffer.zig"); @@ -11,10 +12,9 @@ const Engine = @import("../engine/Engine.zig"); const Game = @import("../Game.zig"); const GenericBuffer = @import("../engine/GenericBuffer.zig").GenericBuffer; const Materials = @import("Materials.zig"); -const Orientation = @import("../voxel.zig").Orientation; const Matrix4x4 = math.Matrix4x4; -const ObjectUniformsBuffer = GenericBuffer(void, Game.ObjectUniforms); +const ObjectUniformsBuffer = GenericBuffer(void, shaders.ObjectUniforms); const Vector3 = math.Vector3; pub const chunk_size = 16; @@ -107,7 +107,7 @@ pub fn deinit(self: *Chunk, engine: *Engine, descriptor_pool: vk.DescriptorPool) } pub fn refresh(self: *Chunk, engine: *Engine, blocks: *const Blocks, neighbors: Neighbors, temp_allocator: std.mem.Allocator) !void { - var uniforms: std.ArrayList(Game.ObjectUniforms) = try .initCapacity(temp_allocator, initial_capacity); + var uniforms: std.ArrayList(shaders.ObjectUniforms) = try .initCapacity(temp_allocator, initial_capacity); defer uniforms.deinit(temp_allocator); for (self.blocks, 0..) |plane, iz| { @@ -116,81 +116,16 @@ pub fn refresh(self: *Chunk, engine: *Engine, blocks: *const Blocks, neighbors: const fy: f32 = @floatFromInt(iy); for (row, 0..) |id, ix| { const fx: f32 = @floatFromInt(ix); - const block = blocks.getBlock(id).?; - + const block: *const Blocks.Definition = &blocks.array.items[id.toInt()]; const center = Vector3.add(self.origin, .init(fx, fy, fz)); - const cx, const cy, const cz = center.asArray(); - if (block.positive_x != .empty and self.getOpposite(.positive_x, ix, iy, iz, blocks, neighbors) == .empty) { - // zig fmt: off - const matrix: Matrix4x4 = .init( - 0, 1, 0, 0, - 0, 0, 1, 0, - 1, 0, 0, 0, - cx + 0.5, cy, cz, 1, - ); - // zig fmt: on - try uniforms.append(temp_allocator, .init(matrix, matrix, block.positive_x)); - } - - if (block.negative_x != .empty and self.getOpposite(.negative_x, ix, iy, iz, blocks, neighbors) == .empty) { - // zig fmt: off - const matrix: Matrix4x4 = .init( - 0, -1, 0, 0, - 0, 0, 1, 0, - -1, 0, 0, 0, - cx - 0.5, cy, cz, 1, - ); - // zig fmt: on - try uniforms.append(temp_allocator, .init(matrix, matrix, block.negative_x)); - } - - if (block.positive_y != .empty and self.getOpposite(.positive_y, ix, iy, iz, blocks, neighbors) == .empty) { - // zig fmt: off - const matrix: Matrix4x4 = .init( - -1, 0, 0, 0, - 0, 0, 1, 0, - 0, 1, 0, 0, - cx, cy + 0.5, cz, 1, - ); - // zig fmt: on - try uniforms.append(temp_allocator, .init(matrix, matrix, block.positive_y)); - } - - if (block.negative_y != .empty and self.getOpposite(.negative_y, ix, iy, iz, blocks, neighbors) == .empty) { - // zig fmt: off - const matrix: Matrix4x4 = .init( - 1, 0, 0, 0, - 0, 0, 1, 0, - 0, -1, 0, 0, - cx, cy - 0.5, cz, 1, - ); - // zig fmt: on - try uniforms.append(temp_allocator, .init(matrix, matrix, block.negative_y)); - } - - if (block.positive_z != .empty and self.getOpposite(.positive_z, ix, iy, iz, blocks, neighbors) == .empty) { - // zig fmt: off - const matrix: Matrix4x4 = .init( - 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - cx, cy, cz + 0.5, 1, - ); - // zig fmt: on - try uniforms.append(temp_allocator, .init(matrix, matrix, block.positive_z)); - } - - if (block.negative_z != .empty and self.getOpposite(.negative_z, ix, iy, iz, blocks, neighbors) == .empty) { - // zig fmt: off - const matrix: Matrix4x4 = .init( - 1, 0, 0, 0, - 0, -1, 0, 0, - 0, 0, -1, 0, - cx, cy, cz - 0.5, 1, - ); - // zig fmt: on - try uniforms.append(temp_allocator, .init(matrix, matrix, block.negative_z)); + inline for (@typeInfo(voxels.Orientation).@"enum".fields) |field| { + const side = @field(voxels.Orientation, field.name); + const material = @field(block.walls, field.name).material; + if (material != .empty and self.getOpposite(side, ix, iy, iz, blocks, neighbors) == .empty) { + const matrix = getMatrix(side, center); + try uniforms.append(temp_allocator, .init(matrix, matrix, material)); + } } } } @@ -240,43 +175,86 @@ pub fn draw(self: *const Chunk, layout: vk.PipelineLayout, command_buffer: Comma command_buffer.drawIndexed(.{ .index_count = 6, .instance_count = self.object_count }); } -fn getOpposite(self: *const Chunk, comptime side: Orientation, x: usize, y: usize, z: usize, blocks: *const Blocks, neighbors: Neighbors) Materials.Id { +fn getOpposite(self: *const Chunk, comptime side: voxels.Orientation, x: usize, y: usize, z: usize, blocks: *const Blocks, neighbors: Neighbors) Materials.Id { return switch (side) { .positive_x => if (x + 1 < chunk_size) - blocks.getBlock(self.blocks[z][y][x + 1]).?.negative_x + blocks.array.items[self.blocks[z][y][x + 1].toInt()].walls.negative_x.material else if (neighbors.positive_x) |neighbor| - blocks.getBlock(neighbor.blocks[z][y][0]).?.negative_x + blocks.array.items[neighbor.blocks[z][y][0].toInt()].walls.negative_x.material else .empty, .negative_x => if (x > 0) - blocks.getBlock(self.blocks[z][y][x - 1]).?.positive_x + blocks.array.items[self.blocks[z][y][x - 1].toInt()].walls.positive_x.material else if (neighbors.negative_x) |neighbor| - blocks.getBlock(neighbor.blocks[z][y][chunk_size - 1]).?.positive_x + blocks.array.items[neighbor.blocks[z][y][chunk_size - 1].toInt()].walls.positive_x.material else .empty, .positive_y => if (y + 1 < chunk_size) - blocks.getBlock(self.blocks[z][y + 1][x]).?.negative_y + blocks.array.items[self.blocks[z][y + 1][x].toInt()].walls.negative_y.material else if (neighbors.positive_y) |neighbor| - blocks.getBlock(neighbor.blocks[z][0][x]).?.negative_y + blocks.array.items[neighbor.blocks[z][0][x].toInt()].walls.negative_y.material else .empty, .negative_y => if (y > 0) - blocks.getBlock(self.blocks[z][y - 1][x]).?.positive_y + blocks.array.items[self.blocks[z][y - 1][x].toInt()].walls.positive_y.material else if (neighbors.negative_y) |neighbor| - blocks.getBlock(neighbor.blocks[z][chunk_size - 1][x]).?.positive_y + blocks.array.items[neighbor.blocks[z][chunk_size - 1][x].toInt()].walls.positive_y.material else .empty, .positive_z => if (z + 1 < chunk_size) - blocks.getBlock(self.blocks[z + 1][y][x]).?.negative_z + blocks.array.items[self.blocks[z + 1][y][x].toInt()].walls.negative_z.material else if (neighbors.positive_z) |neighbor| - blocks.getBlock(neighbor.blocks[0][y][x]).?.negative_z + blocks.array.items[neighbor.blocks[0][y][x].toInt()].walls.negative_z.material else .empty, .negative_z => if (z > 0) - blocks.getBlock(self.blocks[z - 1][y][x]).?.positive_z + blocks.array.items[self.blocks[z - 1][y][x].toInt()].walls.positive_z.material else if (neighbors.negative_z) |neighbor| - blocks.getBlock(neighbor.blocks[chunk_size - 1][y][x]).?.positive_z + blocks.array.items[neighbor.blocks[chunk_size - 1][y][x].toInt()].walls.positive_z.material else .empty, }; } + +fn getMatrix(comptime side: voxels.Orientation, center: Vector3) Matrix4x4 { + return switch (side) { + // zig fmt: off + .positive_x => .init( + 0, 1, 0, 0, + 0, 0, 1, 0, + 1, 0, 0, 0, + center.getX() + 0.5, center.getY(), center.getZ(), 1, + ), + .negative_x => .init( + 0, -1, 0, 0, + 0, 0, 1, 0, + -1, 0, 0, 0, + center.getX() - 0.5, center.getY(), center.getZ(), 1, + ), + .positive_y => .init( + -1, 0, 0, 0, + 0, 0, 1, 0, + 0, 1, 0, 0, + center.getX(), center.getY() + 0.5, center.getZ(), 1, + ), + .negative_y => .init( + 1, 0, 0, 0, + 0, 0, 1, 0, + 0, -1, 0, 0, + center.getX(), center.getY() - 0.5, center.getZ(), 1, + ), + .positive_z => .init( + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + center.getX(), center.getY(), center.getZ() + 0.5, 1, + ), + .negative_z => .init( + 1, 0, 0, 0, + 0, -1, 0, 0, + 0, 0, -1, 0, + center.getX(), center.getY(), center.getZ() - 0.5, 1, + ), + // zig fmt: on + }; +} diff --git a/src/assets/Materials.zig b/src/assets/Materials.zig index 7b7e01f..b1fb3d7 100644 --- a/src/assets/Materials.zig +++ b/src/assets/Materials.zig @@ -1,68 +1,104 @@ +//! Module for loading persistent materials. + const Materials = @This(); const std = @import("std"); +const shaders = @import("../shaders.zig"); const vk = @import("vulkan"); const Atom = @import("../engine/Atom.zig").Atom; const Engine = @import("../engine/Engine.zig"); -const GenericBuffer = @import("../engine/GenericBuffer.zig").GenericBuffer; const Textures = @import("Textures.zig"); -const MaterialBuffer = GenericBuffer(void, Material); - -map: Map, -material_buffer: MaterialBuffer, -next_id: Id, - -// capacity * @sizeOf(Material) = 832 kiB -pub const capacity = std.math.maxInt(std.meta.Tag(Id)); - -pub const Key = struct { atom: Atom }; pub const Id = enum(u16) { + // VOLATILE Synchronize explicit values with `init` implementation. + + /// A material ID that can be used as a "null" material. An object with this + /// material ID must not be rendered. If this ID is rendered, an "error" + /// replacement material will be used. empty, _, - pub fn next(self: Id) Id { - return @enumFromInt(@intFromEnum(self) + 1); + /// Cast an integer into an ID. This can produce an invalid ID. + pub fn fromInt(value: u16) Id { + return @enumFromInt(value); + } + + /// Cast an index into an ID. This can produce an invalid ID. The caller + /// asserts that the index is not greater than the max ID value. + pub fn fromIndex(index: usize) Id { + std.debug.assert(index < max_materials); + return @enumFromInt(@as(u16, @intCast(index))); + } + + /// Cast an index into an ID. This can produce an invalid ID. Returns an + /// error if the index is greater than the max ID value. + pub fn fromIndexSafe(index: usize) error{Overflow}!Id { + if (index >= max_materials) return error.Overflow; + return @enumFromInt(@as(u16, @intCast(index))); + } + + /// Cast an ID into an integer. + pub fn toInt(self: Id) u16 { + return @intFromEnum(self); } }; -pub const Map = std.AutoHashMapUnmanaged(Key, Id); - -pub const Material = extern struct { - base_color: [3]f32, - emissive: [3]f32, - ior: f32, - metallic: f32, - normal_scale: f32, - occlusion_texture_strength: f32, - roughness: f32, - - base_color_texture: Textures.Id, - emissive_texture: Textures.Id, - normal_texture: Textures.Id, - occlusion_roughness_metallic_texture: Textures.Id, +pub const Key = struct { + /// Atom representing the filename of the material. + filename: Atom, }; -pub fn init(engine: *Engine, allocator: std.mem.Allocator) !Materials { - var map: Map = .empty; - errdefer map.deinit(allocator); - try map.ensureTotalCapacity(allocator, capacity); +/// Maps a key value to a material ID. +map: std.AutoHashMapUnmanaged(Key, Id), +/// Stores all material data in a single contiguous storage buffer. Use the +/// material ID as an index into this buffer. +material_buffer: shaders.MaterialBuffer, +/// The amount of materials currently loaded. +material_count: usize, - var material_buffer = try MaterialBuffer.init(engine, .{ +/// With `@sizeOf(Material) == 52` and `max_materials == 4096`, the material +/// storage buffer should take 208 kiB in VRAM. +pub const max_materials = 4096; + +pub fn init(engine: *Engine, allocator: std.mem.Allocator) !Materials { + var map: std.AutoHashMapUnmanaged(Key, Id) = .empty; + errdefer map.deinit(allocator); + try map.ensureTotalCapacity(allocator, max_materials); + + var material_buffer = try shaders.MaterialBuffer.init(engine, .{ .usage = .storage, .target_queue = .graphics, - .array_capacity = capacity, + .array_capacity = max_materials, .name = "Materials", }); errdefer material_buffer.deinit(engine); - // TODO Add "error" material to represent the empty value (it shouldn't be rendered, so it's good to know when it is). + // VOLATILE Synchronize with explicit values on top of `Id` type. + + try material_buffer.write(engine, .{ + .element_offset = Id.empty.toInt(), + .elements = &.{ + .{ + .base_color = .{ 0, 0, 0 }, + .emissive = .{ 1, 0, 1 }, + .ior = 1.45, + .metallic = 0, + .normal_scale = 1, + .occlusion_texture_strength = 1, + .roughness = 1, + .base_color_texture = .empty_base_color, + .emissive_texture = .empty_emissive, + .normal_texture = .empty_normal, + .occlusion_roughness_metallic_texture = .empty_occlusion_roughness_metallic, + }, + }, + }); return .{ .map = map, .material_buffer = material_buffer, - .next_id = @enumFromInt(1), + .material_count = @typeInfo(Id).@"enum".fields.len, }; } @@ -74,46 +110,121 @@ pub fn deinit(self: *Materials, engine: *Engine, allocator: std.mem.Allocator) v self.* = undefined; } -pub fn getAtom(self: *const Materials, atom: Atom) ?Id { - const key: Key = .{ .atom = atom }; - return self.map.get(key); -} - -pub fn getFilename(self: *const Materials, filename: []const u8) ?Id { - const atom = Atom.fromStringIfExists(filename) orelse return null; - const key: Key = .{ .atom = atom }; - return self.map.get(key); -} - -pub fn getOrLoadAtom(self: *Materials, engine: *Engine, textures: *Textures, atom: Atom, temp_allocator: std.mem.Allocator) !Id { - const key: Key = .{ .atom = atom }; - const entry = self.map.getOrPutAssumeCapacity(key); - - if (entry.found_existing) { - return entry.value_ptr.*; +/// Get the ID of a material given its filename (as a string). Returns `null` if +/// such material hasn't been loaded. Returns `.empty` when the filename is +/// `null`. +pub fn get(self: *const Materials, maybe_filename: ?[]const u8) ?Id { + if (maybe_filename) |filename| { + return self.map.get(.{ + // If the atom doesn't exist, then the material cannot possibly exist. + .filename = .fromStringIfExists(filename) orelse return null, + }); } else { - errdefer _ = self.map.remove(key); - const id = try self.loadMaterial(engine, textures, atom.toString(), temp_allocator); - entry.value_ptr.* = id; - return id; + return .empty; } } -pub fn getOrLoadFilename(self: *Materials, engine: *Engine, textures: *Textures, filename: []const u8, temp_allocator: std.mem.Allocator) !Id { - const atom = try Atom.fromString(filename); - const key: Key = .{ .atom = atom }; - const entry = self.map.getOrPutAssumeCapacity(key); - - if (entry.found_existing) { - return entry.value_ptr.*; +/// Get the ID of a material given its filename (as an atom). Returns `null` if +/// such material hasn't been loaded. Returns `.empty` when the filename is +/// `.empty`. +pub fn getAtom(self: *const Materials, filename: Atom) ?Id { + if (filename != .empty) { + return self.map.get(.{ + .filename = filename, + }); } else { - errdefer _ = self.map.remove(key); - const id = try self.loadMaterial(engine, textures, filename, temp_allocator); - entry.value_ptr.* = id; - return id; + return .empty; } } +/// Get the ID of a material given its filename (as a string). Returns either an +/// existing material ID or loads a new material along with its textures and +/// assigns a new ID, if necessary. Will not return any error if the material +/// already exists. Returns `.empty` when the filename is `null`. +/// +/// When a material or its textures are being loaded, `temp_allocator` is used +/// for temporary allocations necessary to perform all operations. No memory +/// allocated with `temp_allocator` is retained, so the allocator can be +/// deinitialized or reset after this function returns. Note that during loading +/// the engine will make its own persistent allocations, so an out of memory +/// error is not necessarily related to `temp_allocator`. +pub fn getOrLoad(self: *Materials, engine: *Engine, textures: *Textures, maybe_filename: ?[]const u8, temp_allocator: std.mem.Allocator) !Id { + if (maybe_filename) |filename| { + const key: Key = .{ + // If the material already exists, then the atom must exist and the + // following line will not return any error. + .filename = try .fromString(filename), + }; + + // We don't use `getOrPutAssumeCapacity` method, because we might already be + // at full capacity, in which case we should return `error.OutOfMaterials`. + + if (self.map.get(key)) |id| { + return id; + } else { + const id = Id.fromIndexSafe(self.material_count) catch |err| switch (err) { + error.Overflow => return error.OutOfMaterials, + }; + try self.loadMaterial(engine, textures, filename, id.toInt(), temp_allocator); + + self.map.putAssumeCapacityNoClobber(key, id); + self.material_count += 1; + + return id; + } + } else { + return .empty; + } +} + +/// Get the ID of a material given its filename (as an atom). Returns either an +/// existing material ID or loads a new material along with its textures and +/// assigns a new ID, if necessary. Will not return any error if the material +/// already exists. Returns `.empty` when the filename is `.empty`. +/// +/// When a material or its textures are being loaded, `temp_allocator` is used +/// for temporary allocations necessary to perform all operations. No memory +/// allocated with `temp_allocator` is retained, so the allocator can be +/// deinitialized or reset after this function returns. Note that during loading +/// the engine will make its own persistent allocations, so an out of memory +/// error is not necessarily related to `temp_allocator`. +pub fn getOrLoadAtom(self: *Materials, engine: *Engine, textures: *Textures, filename: Atom, temp_allocator: std.mem.Allocator) !Id { + if (filename != .empty) { + const key: Key = .{ + .filename = filename, + }; + + // We don't use `getOrPutAssumeCapacity` method, because we might already be + // at full capacity, in which case we should return `error.OutOfMaterials`. + + if (self.map.get(key)) |id| { + return id; + } else { + const id = Id.fromIndexSafe(self.material_count) catch |err| switch (err) { + error.Overflow => return error.OutOfMaterials, + }; + try self.loadMaterial(engine, textures, filename.toString(), id.toInt(), temp_allocator); + + self.map.putAssumeCapacityNoClobber(key, id); + self.material_count += 1; + + return id; + } + } else { + return .empty; + } +} + +/// Scan the materials directory and load all materials that haven't been loaded +/// already. Will not traverse subdirectories. Any errors will be skipped and +/// logged. +/// +/// When a material or its textures are being loaded, `temp_allocator` is used +/// for temporary allocations necessary to perform all operations. No memory +/// allocated with `temp_allocator` is retained, so the allocator can be +/// deinitialized or reset after this function returns. Note that during loading +/// the engine will make its own persistent allocations, so an out of memory +/// error is not necessarily related to `temp_allocator`. pub fn loadAll(self: *Materials, engine: *Engine, textures: *Textures, temp_allocator: std.mem.Allocator) void { const cwd = std.fs.cwd(); @@ -133,13 +244,13 @@ pub fn loadAll(self: *Materials, engine: *Engine, textures: *Textures, temp_allo continue; } - _ = self.loadMaterial(engine, textures, entry.name, temp_allocator) catch |err| { + _ = self.getOrLoad(engine, textures, entry.name, temp_allocator) catch |err| { std.log.err("Error while loading material entry {s}: {s}", .{ entry.name, @errorName(err) }); }; } } -fn loadMaterial(self: *Materials, engine: *Engine, textures: *Textures, filename: []const u8, temp_allocator: std.mem.Allocator) !Id { +fn loadMaterial(self: *Materials, engine: *Engine, textures: *Textures, filename: []const u8, index: u32, temp_allocator: std.mem.Allocator) !void { const MaterialJson = struct { baseColor: [3]f32 = .{ 1, 1, 1 }, baseColorTexture: ?[]const u8 = null, @@ -180,40 +291,8 @@ fn loadMaterial(self: *Materials, engine: *Engine, textures: *Textures, filename const material_json = parsed.value; - const base_color_texture = blk: { - if (material_json.baseColorTexture) |name| { - break :blk try textures.getOrLoadFilename(engine, name, .base_color, temp_allocator); - } else { - break :blk .empty_base_color; - } - }; - - const emissive_texture = blk: { - if (material_json.emissiveTexture) |name| { - break :blk try textures.getOrLoadFilename(engine, name, .emissive, temp_allocator); - } else { - break :blk .empty_emissive; - } - }; - - const normal_texture = blk: { - if (material_json.normalTexture) |name| { - break :blk try textures.getOrLoadFilename(engine, name, .normal, temp_allocator); - } else { - break :blk .empty_normal; - } - }; - - const occlusion_roughness_metallic_texture = blk: { - if (material_json.occlusionRoughnessMetallicTexture) |name| { - break :blk try textures.getOrLoadFilename(engine, name, .occlusion_roughness_metallic, temp_allocator); - } else { - break :blk .empty_occlusion_roughness_metallic; - } - }; - try self.material_buffer.write(engine, .{ - .element_offset = @intFromEnum(self.next_id), + .element_offset = index, .elements = &.{ .{ .base_color = material_json.baseColor, @@ -223,16 +302,11 @@ fn loadMaterial(self: *Materials, engine: *Engine, textures: *Textures, filename .normal_scale = material_json.normalScale, .occlusion_texture_strength = material_json.occlusionTextureStrength, .roughness = material_json.roughness, - .base_color_texture = base_color_texture, - .emissive_texture = emissive_texture, - .normal_texture = normal_texture, - .occlusion_roughness_metallic_texture = occlusion_roughness_metallic_texture, + .base_color_texture = try textures.getOrLoad(engine, material_json.baseColorTexture, .base_color, temp_allocator), + .emissive_texture = try textures.getOrLoad(engine, material_json.emissiveTexture, .emissive, temp_allocator), + .normal_texture = try textures.getOrLoad(engine, material_json.normalTexture, .normal, temp_allocator), + .occlusion_roughness_metallic_texture = try textures.getOrLoad(engine, material_json.occlusionRoughnessMetallicTexture, .occlusion_roughness_metallic, temp_allocator), }, }, }); - - const id = self.next_id; - self.next_id = self.next_id.next(); - - return id; } diff --git a/src/assets/Textures.zig b/src/assets/Textures.zig index 93b182a..fd76300 100644 --- a/src/assets/Textures.zig +++ b/src/assets/Textures.zig @@ -1,3 +1,5 @@ +//! Module for loading persistent textures. + const Textures = @This(); const std = @import("std"); @@ -7,44 +9,77 @@ const Atom = @import("../engine/Atom.zig").Atom; const Engine = @import("../engine/Engine.zig"); const Texture = @import("../engine/Texture.zig"); -map: Map, -textures: Array, - -pub const capacity = std.math.maxInt(std.meta.Tag(Id)); - -pub const Key = struct { - atom: Atom, - usage: Texture.Usage, -}; - pub const Id = enum(u16) { + // VOLATILE Synchronize explicit values with `init` implementation. + + /// 1×1 texture with usage `.base_color`, whose texel is read as `vec4(1)`. empty_base_color = 0, + /// 1×1 texture with usage `.emissive`, whose texel is read as `vec4(1)`. empty_emissive = 1, + /// 1×1 texture with usage `.normal`, whose texel is read as + /// `vec4(0, 0, 1, 1)`. empty_normal = 2, + /// 1×1 texture with usage `,occlusion_roughness_metallic`, whose texel is + /// read as `vec4(1)`. empty_occlusion_roughness_metallic = 3, _, - pub fn next(self: Id) Id { - return @enumFromInt(@intFromEnum(self) + 1); + /// Cast an integer into an ID. This can produce an invalid ID. + pub fn fromInt(value: u16) Id { + return @enumFromInt(value); + } + + /// Cast an index into an ID. This can produce an invalid ID. The caller + /// asserts that the index is not greater than the max ID value. + pub fn fromIndex(index: usize) Id { + std.debug.assert(index < max_textures); + return @enumFromInt(@as(u16, @intCast(index))); + } + + /// Cast an index into an ID. This can produce an invalid ID. Returns an + /// error if the index is greater than the max ID value. + pub fn fromIndexSafe(index: usize) error{Overflow}!Id { + if (index >= max_textures) return error.Overflow; + return @enumFromInt(@as(u16, @intCast(index))); + } + + /// Cast an ID into an integer. + pub fn toInt(self: Id) u16 { + return @intFromEnum(self); } }; -pub const Map = std.AutoHashMapUnmanaged(Key, Id); -pub const Array = std.ArrayList(Texture); +pub const Key = struct { + /// Atom representing the filename of the texture. + filename: Atom, + /// Desired usage of the texture. + usage: Texture.Usage, +}; + +/// Maps a key value to a texture ID. +map: std.AutoHashMapUnmanaged(Key, Id), +/// Stores all `Texture` structs and maps a texture ID to a `Texture` struct. +array: std.ArrayList(Texture), + +/// 4096 textures of usage `.base_color` and 16×16 dimensions should take 4 MiB +/// in VRAM. +pub const max_textures = 4096; pub fn init(engine: *Engine, allocator: std.mem.Allocator) !Textures { - var map: Map = .empty; + var map: std.AutoHashMapUnmanaged(Key, Id) = .empty; errdefer map.deinit(allocator); - try map.ensureTotalCapacity(allocator, capacity); + try map.ensureTotalCapacity(allocator, max_textures); - var textures: Array = try .initCapacity(allocator, capacity); + var array: std.ArrayList(Texture) = try .initCapacity(allocator, max_textures); errdefer { - for (textures.items) |*texture| { + for (array.items) |*texture| { texture.deinit(engine); } - textures.deinit(allocator); + array.deinit(allocator); } + // VOLATILE Synchronize with explicit values on top of `Id` type. + const empty_base_color_texture = try Texture.init(engine, .{ .width = 1, .height = 1, @@ -52,7 +87,7 @@ pub fn init(engine: *Engine, allocator: std.mem.Allocator) !Textures { .target_queue = .graphics, .name = "@Empty", }); - textures.appendAssumeCapacity(empty_base_color_texture); + array.appendAssumeCapacity(empty_base_color_texture); const empty_emissive_texture = try Texture.init(engine, .{ .width = 1, @@ -61,7 +96,7 @@ pub fn init(engine: *Engine, allocator: std.mem.Allocator) !Textures { .target_queue = .graphics, .name = "@Empty", }); - textures.appendAssumeCapacity(empty_emissive_texture); + array.appendAssumeCapacity(empty_emissive_texture); const empty_normal_texture = try Texture.init(engine, .{ .width = 1, @@ -70,7 +105,7 @@ pub fn init(engine: *Engine, allocator: std.mem.Allocator) !Textures { .target_queue = .graphics, .name = "@Empty", }); - textures.appendAssumeCapacity(empty_normal_texture); + array.appendAssumeCapacity(empty_normal_texture); const empty_occlusuion_roughness_metallic_texture = try Texture.init(engine, .{ .width = 1, @@ -79,7 +114,7 @@ pub fn init(engine: *Engine, allocator: std.mem.Allocator) !Textures { .target_queue = .graphics, .name = "@Empty", }); - textures.appendAssumeCapacity(empty_occlusuion_roughness_metallic_texture); + array.appendAssumeCapacity(empty_occlusuion_roughness_metallic_texture); try empty_base_color_texture.writeSamples(u8, engine, &.{ 255, 255, 255, 255 }); try empty_emissive_texture.writeSamples(f16, engine, &.{ 1.0, 1.0, 1.0, 1.0 }); @@ -88,67 +123,129 @@ pub fn init(engine: *Engine, allocator: std.mem.Allocator) !Textures { return .{ .map = map, - .textures = textures, + .array = array, }; } pub fn deinit(self: *Textures, engine: *Engine, allocator: std.mem.Allocator) void { std.log.scoped(.deinit).debug("Deinitializing {*} with {*} and Allocator{{{*},{*}}}", .{ self, engine, allocator.ptr, allocator.vtable }); - for (self.textures.items) |*texture| { + for (self.array.items) |*texture| { texture.deinit(engine); } - self.textures.deinit(allocator); + self.array.deinit(allocator); self.map.deinit(allocator); self.* = undefined; } -pub fn getAtom(self: *const Textures, atom: Atom, usage: Texture.Usage) ?Id { - const key: Key = .{ .atom = atom, .usage = usage }; - return self.map.get(key); -} - -pub fn getFilename(self: *const Textures, filename: []const u8, usage: Texture.Usage) ?Id { - const atom = Atom.fromStringIfExists(filename) orelse return null; - const key: Key = .{ .atom = atom, .usage = usage }; - return self.map.get(key); -} - -pub fn getTexture(self: *const Textures, id: Id) ?*Texture { - const index: usize = @intFromEnum(id); - return if (index < self.textures.items.len) &self.textures.items[index] else null; -} - -pub fn getOrLoadAtom(self: *Textures, engine: *Engine, atom: Atom, usage: Texture.Usage, temp_allocator: std.mem.Allocator) !Id { - const key: Key = .{ .atom = atom, .usage = usage }; - const entry = self.map.getOrPutAssumeCapacity(key); - - if (entry.found_existing) { - return entry.value_ptr.*; +/// Get the ID of a texture given its filename (as a string) and usage. Returns +/// `null` if such texture hasn't been loaded. When the filename is `null`, +/// returns an empty texture ID appropriate for given usage. +pub fn get(self: *const Textures, maybe_filename: []const u8, usage: Texture.Usage) ?Id { + if (maybe_filename) |filename| { + return self.map.get(.{ + // If the atom doesn't exist, then the texture cannot possibly exist. + .filename = .fromStringIfExists(filename) orelse return null, + .usage = usage, + }); } else { - errdefer _ = self.map.remove(key); - const texture = try loadTexture(engine, atom.toString(), usage, temp_allocator); - const id = self.nextId(); - entry.value_ptr.* = id; - self.textures.appendAssumeCapacity(texture); - return id; + return emptyTextureForUsage(usage); } } -pub fn getOrLoadFilename(self: *Textures, engine: *Engine, filename: []const u8, usage: Texture.Usage, temp_allocator: std.mem.Allocator) !Id { - const atom = try Atom.fromString(filename); - const key: Key = .{ .atom = atom, .usage = usage }; - const entry = self.map.getOrPutAssumeCapacity(key); - - if (entry.found_existing) { - return entry.value_ptr.*; +/// Get the ID of a texture given its filename (as an atom) and usage. Returns +/// `null` if such texture hasn't been loaded. When the filename is `.empty`, +/// returns an empty texture ID appropriate for given usage. +pub fn getAtom(self: *const Textures, filename: Atom, usage: Texture.Usage) ?Id { + if (filename != .empty) { + return self.map.get(.{ + .filename = filename, + .usage = usage, + }); } else { - errdefer _ = self.map.remove(key); - const texture = try loadTexture(engine, filename, usage, temp_allocator); - const id = self.nextId(); - entry.value_ptr.* = id; - self.textures.appendAssumeCapacity(texture); - return id; + return emptyTextureForUsage(usage); + } +} + +/// Get the ID of a texture given its filename (as a string) and usage. Returns +/// either an existing texture ID or loads a new texture and assigns a new ID, +/// if necessary. Will not return any error if the texture already exists. When +/// the filename is `null`, returns an empty texture ID appropriate for given +/// usage. +/// +/// When a texture is being loaded, `temp_allocator` is used for temporary +/// allocations necessary to perform all operations. No memory allocated with +/// `temp_allocator` is retained, so the allocator can be deinitialized or reset +/// after this function returns. Note that during loading the engine will make +/// its own persistent allocations, so an out of memory error is not necessarily +/// related to `temp_allocator`. +pub fn getOrLoad(self: *Textures, engine: *Engine, maybe_filename: ?[]const u8, usage: Texture.Usage, temp_allocator: std.mem.Allocator) !Id { + if (maybe_filename) |filename| { + const key: Key = .{ + // If the texture already exists, then the atom must exist and the + // following line will not return any error. + .filename = try .fromString(filename), + .usage = usage, + }; + + // We don't use `getOrPutAssumeCapacity` method, because we might already be + // at full capacity, in which case we should return `error.OutOfTextures`. + + if (self.map.get(key)) |id| { + return id; + } else { + const id = Id.fromIndexSafe(self.array.items.len) catch |err| switch (err) { + error.Overflow => return error.OutOfTextures, + }; + const texture = try loadTexture(engine, filename, usage, temp_allocator); + + self.map.putAssumeCapacityNoClobber(key, id); + self.array.appendAssumeCapacity(texture); + + return id; + } + } else { + return emptyTextureForUsage(usage); + } +} + +/// Get the ID of a texture given its filename (as an atom) and usage. Returns +/// either an existing texture ID or loads a new texture and assigns a new ID, +/// if necessary. Will not return any error if the texture already exists. When +/// the filename is `.empty`, returns an empty texture ID appropriate for given +/// usage. +/// +/// When a texture is being loaded, `temp_allocator` is used for temporary +/// allocations necessary to perform all operations. No memory allocated with +/// `temp_allocator` is retained, so the allocator can be deinitialized or reset +/// after this function returns. Note that during loading the engine will make +/// its own persistent allocations, so an out of memory is not necessarily +/// related to `temp_allocator`. +pub fn getOrLoadAtom(self: *Textures, engine: *Engine, filename: Atom, usage: Texture.Usage, temp_allocator: std.mem.Allocator) !Id { + if (filename != .empty) { + const key: Key = .{ + .filename = filename, + .usage = usage, + }; + + // We don't use `getOrPutAssumeCapacity` method, because we might already be + // at full capacity, in which case we should return `error.OutOfTextures`. + + if (self.map.get(key)) |id| { + return id; + } else { + const id = Id.fromIndexSafe(self.array.items.len) catch |err| switch (err) { + error.Overflow => return error.OutOfTextures, + }; + const texture = try loadTexture(engine, filename.toString(), usage, temp_allocator); + + self.map.putAssumeCapacityNoClobber(key, id); + self.array.appendAssumeCapacity(texture); + + return id; + } + } else { + return emptyTextureForUsage(usage); } } @@ -160,10 +257,7 @@ fn loadTexture(engine: *Engine, filename: []const u8, usage: Texture.Usage, temp var dir = try cwd.openDir("assets/textures", .{}); defer dir.close(); - const file = try dir.openFile(filename, .{}); - defer file.close(); - - const file_buf = try file.readToEndAlloc(temp_allocator, std.math.maxInt(usize)); + const file_buf = try dir.readFileAlloc(temp_allocator, filename, std.math.maxInt(usize)); defer temp_allocator.free(file_buf); var img = try stbi.Image.loadFromMemory(file_buf, usage.samplesPerTexel()); @@ -188,7 +282,12 @@ fn loadTexture(engine: *Engine, filename: []const u8, usage: Texture.Usage, temp return texture; } -fn nextId(self: *const Textures) Id { - const index = self.textures.items.len; - return @enumFromInt(index); +fn emptyTextureForUsage(usage: Texture.Usage) !Id { + return switch (usage) { + .base_color => .empty_base_color, + .normal => .empty_normal, + .occlusion_roughness_metallic => .empty_occlusion_roughness_metallic, + .emissive => .empty_emissive, + .depth => error.InvalidUsage, + }; } diff --git a/src/engine/Atom.zig b/src/engine/Atom.zig index a4f6118..8785bda 100644 --- a/src/engine/Atom.zig +++ b/src/engine/Atom.zig @@ -1,9 +1,9 @@ //! Module for string interning. A string can be converted to a stable integer -//! constant, called an *atom*. The value of an for a given string is guaranteed -//! to be stable throughout a program's runtime, but not across different runs. -//! There can be no more than 2¹⁶ atoms. +//! constant, called an *atom*. The value of an atom for a given string is +//! guaranteed to be stable throughout a program's runtime, but not across +//! different runs. There can be no more than 2¹⁶ atoms. //! -//! Use this module for converting string IDs into numbers, so that they can be +//! Use this module to convert string IDs into numbers, so that they can be //! compared more easily. The users of this module should import it with //! `@import("Atom.zig").Atom` and use the methods available in the `Atom` type. @@ -76,7 +76,7 @@ pub const Atom = enum(u16) { } /// Cast an atom into an integer. - pub fn toInt(self: Atom) u32 { + pub fn toInt(self: Atom) u16 { return @intFromEnum(self); } diff --git a/src/shaders.zig b/src/shaders.zig new file mode 100644 index 0000000..91b866a --- /dev/null +++ b/src/shaders.zig @@ -0,0 +1,106 @@ +const math = @import("math.zig"); + +const Materials = @import("assets/Materials.zig"); +const Matrix4x4 = math.Matrix4x4; +const GenericBuffer = @import("engine/GenericBuffer.zig").GenericBuffer; +const Textures = @import("assets/Textures.zig"); +const Vector2 = math.Vector2; +const Vector3 = math.Vector3; +const Vector4 = math.Vector4; + +pub const VertexBuffer = GenericBuffer(void, Vertex); +pub const IndexBuffer = GenericBuffer(void, Index); +pub const GlobalUniformsBuffer = GenericBuffer(GlobalUniforms, void); +pub const PointLightBuffer = GenericBuffer(u32, PointLight); +pub const DirectionalLightBuffer = GenericBuffer(u32, DirectionalLight); +pub const MaterialBuffer = GenericBuffer(void, Material); +pub const ObjectUniformsBuffer = GenericBuffer(void, ObjectUniforms); + +pub const Vertex = extern struct { + positionOS: [3]f32, + texCoord: [2]u16, + normalOS: [3]i8, + tangentOS: [4]i8, + + pub fn init(position_os: Vector3, tex_coord: Vector2, normal_os: Vector3, tangent_os: Vector4) Vertex { + return .{ + .positionOS = position_os.asArray(), + .texCoord = tex_coord.asArrayNorm(u16), + .normalOS = normal_os.asArrayNorm(i8), + .tangentOS = tangent_os.asArrayNorm(i8), + }; + } +}; + +pub const Index = u16; + +pub const GlobalUniforms = extern struct { + matrixWStoVS: [16]f32, + matrixVStoCS: [16]f32, + ambientLight: [3]f32, + + pub fn init(matrix_ws_to_vs: Matrix4x4, matrix_vs_to_cs: Matrix4x4, ambient_light: Vector3) GlobalUniforms { + return .{ + .matrixWStoVS = matrix_ws_to_vs.asArray(), + .matrixVStoCS = matrix_vs_to_cs.asArray(), + .ambientLight = ambient_light.asArray(), + }; + } +}; + +pub const PointLight = extern struct { + positionWS: [3]f32, + color: [3]f32, + + pub fn init(position_ws: Vector3, color: Vector3) PointLight { + return .{ + .positionWS = position_ws.asArray(), + .color = color.asArray(), + }; + } +}; + +pub const DirectionalLight = extern struct { + directionWS: [3]f32, + color: [3]f32, + + pub fn init(direction_ws: Vector3, color: Vector3) DirectionalLight { + return .{ + .directionWS = direction_ws.asArray(), + .color = color.asArray(), + }; + } +}; + +pub const Material = extern struct { + base_color: [3]f32, + emissive: [3]f32, + ior: f32, + metallic: f32, + normal_scale: f32, + occlusion_texture_strength: f32, + roughness: f32, + + base_color_texture: Textures.Id, + emissive_texture: Textures.Id, + normal_texture: Textures.Id, + occlusion_roughness_metallic_texture: Textures.Id, +}; + +pub const ObjectUniforms = extern struct { + matrixOStoWS: [16]f32, + matrixOStoWSNormal: [16]f32, + + material: Materials.Id, + + pub fn init(matrix_os_to_ws: Matrix4x4, matrix_ow_to_ws_normal: Matrix4x4, material: Materials.Id) ObjectUniforms { + return .{ + .matrixOStoWS = matrix_os_to_ws.asArray(), + .matrixOStoWSNormal = matrix_ow_to_ws_normal.asArray(), + .material = material, + }; + } +}; + +pub const main_vert_spv align(4) = @embedFile("shaders/main_vert.spv").*; +pub const main_frag_spv align(4) = @embedFile("shaders/main_frag.spv").*; diff --git a/src/voxel.zig b/src/voxel.zig deleted file mode 100644 index a15c74d..0000000 --- a/src/voxel.zig +++ /dev/null @@ -1,25 +0,0 @@ -const Materials = @import("assets/Materials.zig"); - -pub const Orientation = enum(u4) { - negative_x, - positive_x, - negative_y, - positive_y, - negative_z, - positive_z, -}; - -// ┌────────────────── x -// │ ┌────────────── y -// │ │ ┌───────── z -// │ │ │ ┌───── orientation -// │ │ │ │ ┌ material -// ┌┴─┐┌┴─┐ ┌┴─┐┌┴─┐ ┌┴──────────────┐ -// 10987654 32109876 54321098 76543210 -pub const Wall = packed struct(u32) { - material: Materials.Id, - orientation: Orientation, - z: u4, - y: u4, - x: u4, -}; diff --git a/src/voxels.zig b/src/voxels.zig new file mode 100644 index 0000000..8779c92 --- /dev/null +++ b/src/voxels.zig @@ -0,0 +1,38 @@ +const Materials = @import("assets/Materials.zig"); + +pub const Orientation = enum(u3) { + negative_x, + positive_x, + negative_y, + positive_y, + negative_z, + positive_z, +}; + +pub const Transform = enum(u3) { + identity, + rotate_cw, + rotate_ccw, + rotate_180, + mirror, + mirror_rotate_cw, + mirror_rotate_ccw, + mirror_rotate_180, +}; + +// ┌──────────────────── x +// │ ┌──────────────── y +// │ │ ┌─────────── z +// │ │ │ ┌─────── orientation +// │ │ │ │ ┌──── transform +// │ │ │ │ │ ┌ material +// ┌┴─┐┌┴─┐ ┌┴─┐┌┴┐┌┴─┐┌┴────────────┐ +// 10987654 32109876 54321098 76543210 +pub const Wall = packed struct(u32) { + material: u14, + transform: Transform, + orientation: Orientation, + z: u4, + y: u4, + x: u4, +};