diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..24a8e87 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.png filter=lfs diff=lfs merge=lfs -text diff --git a/assets/shaders.glsl b/assets/shaders.glsl index 6dd8f32..3e778c8 100644 --- a/assets/shaders.glsl +++ b/assets/shaders.glsl @@ -141,10 +141,22 @@ vec3 lightOutgoingRadiance( return (scatteredFactor + reflectedFactor) * incomingRadiance * dotNL; } +vec4 texture2DArrayAA(texture2DArray tex, vec2 texCoord) { + vec2 size = vec2(textureSize(sampler2DArray(tex, _Sampler), 0).xy); + vec2 texCoordPX = texCoord * size; + vec2 seam = floor(texCoordPX + vec2(0.5)); + + texCoordPX = (texCoordPX - seam) / fwidth(texCoordPX) + seam; + texCoordPX = clamp(texCoordPX, seam - 0.5, seam + 0.5); + + vec3 texCoord3 = vec3(texCoordPX / size, float(_TextureIndex)); + return texture(sampler2DArray(tex, _Sampler), texCoord3); +} + void main() { - vec4 baseColorTexel = texture(sampler2DArray(_BaseColorTexture, _Sampler), vec3(var_texCoord, float(_TextureIndex))); - vec4 occlusionRoughnessMetallicTexel = texture(sampler2DArray(_OcclusionRoughnessMetallicTexture, _Sampler), vec3(var_texCoord, float(_TextureIndex))); - vec4 normalTextureTexel = texture(sampler2DArray(_NormalTexture, _Sampler), vec3(var_texCoord, float(_TextureIndex))); + vec4 baseColorTexel = texture2DArrayAA(_BaseColorTexture, var_texCoord); + vec4 occlusionRoughnessMetallicTexel = texture2DArrayAA(_OcclusionRoughnessMetallicTexture, var_texCoord); + vec4 normalTextureTexel = texture2DArrayAA(_NormalTexture, var_texCoord); vec3 baseColor = baseColorTexel.rgb; float occlusion = occlusionRoughnessMetallicTexel.r; diff --git a/assets/textures/BaseColor.png b/assets/textures/BaseColor.png new file mode 100644 index 0000000..d3b8742 --- /dev/null +++ b/assets/textures/BaseColor.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a93379cd6b8ca483163bd3d059e6270b9f863b63f72dab7baabf2d1521412e64 +size 2779 diff --git a/assets/textures/Normal.png b/assets/textures/Normal.png new file mode 100644 index 0000000..ba9d01b --- /dev/null +++ b/assets/textures/Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ddf3fab756c50827096cbced25cbde34ca6b50b8c3da4d904b3ce9eabb782c76 +size 46935 diff --git a/assets/textures/OcclusionRoughnessMetallic.png b/assets/textures/OcclusionRoughnessMetallic.png new file mode 100644 index 0000000..07cdc0e --- /dev/null +++ b/assets/textures/OcclusionRoughnessMetallic.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8005badc07c0e9cfb93071d6a5dd6291f8b00210a86fdbf8434490b0ed90cf82 +size 370 diff --git a/build.zig b/build.zig index 72ba10f..fa1acf5 100644 --- a/build.zig +++ b/build.zig @@ -27,6 +27,8 @@ pub fn build(b: *std.Build) !void { }); sokol_dep.artifact("sokol_clib").addIncludePath(cimgui_dep.path(cimgui_conf.include_dir)); + const zstbi_dep = b.dependency("zstbi", .{}); + const sokol_mod = sokol_dep.module("sokol"); const shdc_dep = sokol_dep.builder.dependency("shdc", .{}); @@ -41,9 +43,12 @@ pub fn build(b: *std.Build) !void { }, }); + const zstbi_mod = zstbi_dep.module("root"); + exe_mod.addImport("cimgui", cimgui_dep.module(cimgui_conf.module_name)); exe_mod.addImport("shaders", shaders_mod); exe_mod.addImport("sokol", sokol_mod); + exe_mod.addImport("zstbi", zstbi_mod); const exe = b.addExecutable(.{ .name = "voxel-game", diff --git a/build.zig.zon b/build.zig.zon index 291eb62..4a3d9fa 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -13,6 +13,10 @@ .url = "git+https://github.com/floooh/dcimgui.git#d5fb4e3d27b79062dc5981db3631dadc4f204654", .hash = "cimgui-0.1.0-44Clkd6YlAAYULKHDwsDX9EPmka-VAVEjUl-o6ve307E", }, + .zstbi = .{ + .url = "git+https://github.com/zig-gamedev/zstbi#2c4b3100ccb7aed90ecc9439030899764e2a8d47", + .hash = "zstbi-0.11.0-dev-L0Ea_yaWBwAHwFoCuyjkFyaiSsbjt4UOrkntR0c_nmzz", + }, }, .paths = .{ diff --git a/src/asset_pipeline.zig b/src/asset_pipeline.zig new file mode 100644 index 0000000..c1b2f4d --- /dev/null +++ b/src/asset_pipeline.zig @@ -0,0 +1,163 @@ +const std = @import("std"); + +const main = @import("main.zig"); +const zstbi = @import("zstbi"); + +pub const Texture = struct { + width: u32, + height: u32, + depth: u32, + data: []u8, + + pub fn deinit(self: *Texture, allocator: std.mem.Allocator) void { + allocator.free(self.data); + self.* = undefined; + } +}; + +pub const AssetMap = std.hash_map.StringHashMapUnmanaged; + +pub fn visitTextures(allocator: std.mem.Allocator) std.hash_map.StringHashMapUnmanaged(Texture) { + zstbi.init(allocator); + defer zstbi.deinit(); + + const cwd = std.fs.cwd(); + const sub_path = "assets/textures"; + + var ret: AssetMap(Texture) = .empty; + + var textures_dir = cwd.openDir(sub_path, .{ .iterate = true }) catch |err| { + std.log.err("Could not open \"{s}\" directory: {s}", .{ sub_path, @errorName(err) }); + return ret; + }; + defer textures_dir.close(); + + var it = textures_dir.iterate(); + while (it.next() catch |err| blk: { + std.log.err("Directory iteration interrupted due to an error: {s}", .{@errorName(err)}); + break :blk null; + }) |entry| { + if (entry.kind != .file) { + std.log.warn("Skipping \"{s}\", which is not a file.", .{entry.name}); + continue; + } + + var texture = visitTexture(allocator, textures_dir, entry.name) catch { + continue; + }; + + const name = allocator.dupe(u8, std.fs.path.stem(entry.name)) catch { + std.log.err("Ran out of memory while trying to allocate asset name", .{}); + texture.deinit(allocator); + continue; + }; + + ret.putNoClobber(allocator, name, texture) catch { + std.log.err("Ran out of memory while trying to save asset to map", .{}); + texture.deinit(allocator); + allocator.free(name); + continue; + }; + } + + return ret; +} + +pub fn visitTexture(allocator: std.mem.Allocator, dir: std.fs.Dir, filename: []const u8) !Texture { + std.log.info("Processing \"{s}\"...", .{filename}); + + const file = dir.openFile(filename, .{}) catch |err| { + std.log.err("Could not open \"{s}\" file: {s}", .{ filename, @errorName(err) }); + return err; + }; + defer file.close(); + + const file_buf = file.readToEndAlloc(allocator, std.math.maxInt(usize)) catch |err| { + std.log.err("Could not read \"{s}\" file contents due to an error: {s}", .{ filename, @errorName(err) }); + return err; + }; + defer allocator.free(file_buf); + + var img = zstbi.Image.loadFromMemory(file_buf, 4) catch |err| { + std.log.err("Error reading \"{s}\" as an image file: {s}", .{ filename, @errorName(err) }); + return err; + }; + defer img.deinit(); + + std.log.debug("size: {}×{} | components: {}", .{ img.width, img.height, img.num_components }); + + const grid_w = 4; + const grid_h = 4; + const tile_count = grid_w * grid_h; + + const tile_w = std.math.divExact(u32, img.width, grid_w) catch |err| { + std.log.err("Cannot divide image width ({}) by {}: {s}", .{ img.width, grid_w, @errorName(err) }); + return err; + }; + const tile_h = std.math.divExact(u32, img.height, grid_h) catch |err| { + std.log.err("Cannot divide image height ({}) by {}: {s}", .{ img.height, grid_h, @errorName(err) }); + return err; + }; + + std.log.debug("tile size: {}×{}", .{ tile_w, tile_h }); + + const data = allocator.alloc(u8, 4 * tile_w * tile_h * tile_count) catch |err| { + std.log.err("Ran out of memory while trying to allocate output buffer", .{}); + return err; + }; + errdefer allocator.free(data); + + rearrange(grid_w, grid_h, tile_w, tile_h, img.data, data); + + return .{ + .width = tile_w, + .height = tile_h, + .depth = tile_count, + .data = data, + }; +} + +fn rearrange(grid_w: u32, grid_h: u32, tile_w: u32, tile_h: u32, inbuf: []const u8, outbuf: []u8) void { + std.log.debug("rearrange: {}×{} grid of {}×{} tiles", .{ grid_w, grid_h, tile_w, tile_h }); + + const row_size = 4 * tile_w; + const row_stride = row_size * grid_w; + const tile_stride = row_stride * tile_h; + + std.debug.assert(inbuf.len == tile_stride * grid_h); + std.debug.assert(outbuf.len == tile_stride * grid_h); + + var outptr: usize = 0; + + var tile_y: u32 = 0; + while (tile_y < grid_h) : (tile_y += 1) { + const tile_byte_offset = tile_y * tile_stride; + + var tile_x: u32 = 0; + while (tile_x < grid_w) : (tile_x += 1) { + const column_byte_offset = tile_x * row_size; + + var row: u32 = 0; + while (row < tile_h) : (row += 1) { + const row_byte_offset = row * row_stride + tile_byte_offset; + const byte_offset = row_byte_offset + column_byte_offset; + + @memcpy( + outbuf[outptr .. outptr + row_size], + inbuf[byte_offset .. byte_offset + row_size], + ); + + outptr += row_size; + } + } + } +} + +pub fn deinitTextures(allocator: std.mem.Allocator, map: AssetMap(Texture)) void { + var it = map.iterator(); + while (it.next()) |entry| { + allocator.free(entry.key_ptr.*); + entry.value_ptr.deinit(allocator); + } + map.deinit(allocator); +} diff --git a/src/game.zig b/src/game.zig index 44093bd..3459bef 100644 --- a/src/game.zig +++ b/src/game.zig @@ -8,6 +8,7 @@ const sapp = sokol.app; const sg = sokol.gfx; const sglue = sokol.glue; +const ap = @import("asset_pipeline.zig"); const main = @import("main.zig"); const Samplers = @import("Samplers.zig"); @@ -28,7 +29,15 @@ var normal_texture: sg.Image = undefined; var bindings: sg.Bindings = undefined; +var textures: ap.AssetMap(ap.Texture) = undefined; + pub fn init() void { + textures = ap.visitTextures(main.allocator); + + const base_color = textures.getPtr("BaseColor").?; + const normal = textures.getPtr("Normal").?; + const occlusion_roughness_metallic = textures.getPtr("OcclusionRoughnessMetallic").?; + vertex_buffer = sg.makeBuffer(.{ .data = sg.asRange(&[_]f32{ // positionOS texCoord normalOS tangentOS @@ -95,13 +104,13 @@ pub fn init() void { base_color_texture = sg.makeImage(.{ .type = .ARRAY, .usage = .{ .immutable = true }, - .width = 16, - .height = 16, - .num_slices = 16, + .width = @intCast(base_color.width), + .height = @intCast(base_color.height), + .num_slices = @intCast(base_color.depth), .pixel_format = .RGBA8, .data = blk: { var ret: sg.ImageData = .{}; - ret.mip_levels[0] = sg.asRange(&[_]u8{ 255, 255, 255, 255 } ** (16 * 16 * 16)); + ret.mip_levels[0] = sg.asRange(base_color.data); break :blk ret; }, }); @@ -109,13 +118,13 @@ pub fn init() void { occlusion_roughness_metallic_texture = sg.makeImage(.{ .type = .ARRAY, .usage = .{ .immutable = true }, - .width = 1, - .height = 1, - .num_slices = 16, + .width = @intCast(occlusion_roughness_metallic.width), + .height = @intCast(occlusion_roughness_metallic.height), + .num_slices = @intCast(occlusion_roughness_metallic.depth), .pixel_format = .RGBA8, .data = blk: { var ret: sg.ImageData = .{}; - ret.mip_levels[0] = sg.asRange(&[_]u8{ 255, 255, 0, 255 } ** (1 * 1 * 16)); + ret.mip_levels[0] = sg.asRange(occlusion_roughness_metallic.data); break :blk ret; }, }); @@ -123,13 +132,13 @@ pub fn init() void { normal_texture = sg.makeImage(.{ .type = .ARRAY, .usage = .{ .immutable = true }, - .width = 128, - .height = 128, - .num_slices = 16, + .width = @intCast(normal.width), + .height = @intCast(normal.height), + .num_slices = @intCast(normal.depth), .pixel_format = .RGBA8, .data = blk: { var ret: sg.ImageData = .{}; - ret.mip_levels[0] = sg.asRange(&[_]u8{ 127, 127, 255, 255 } ** (128 * 128 * 16)); + ret.mip_levels[0] = sg.asRange(normal.data); break :blk ret; }, });