From 4c381f5e776b0dff93fa826c735e25ad339740cc Mon Sep 17 00:00:00 2001 From: Szymon Nowakowski Date: Sat, 8 Feb 2025 23:13:31 +0100 Subject: [PATCH 1/5] Introduce textures --- .gitattributes | 1 + textures/BaseColor.png | 3 +++ textures/Normal.png | 3 +++ textures/OcclusionRoughnessMetallic.png | 3 +++ 4 files changed, 10 insertions(+) create mode 100644 .gitattributes create mode 100644 textures/BaseColor.png create mode 100644 textures/Normal.png create mode 100644 textures/OcclusionRoughnessMetallic.png 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/textures/BaseColor.png b/textures/BaseColor.png new file mode 100644 index 0000000..d3b8742 --- /dev/null +++ b/textures/BaseColor.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a93379cd6b8ca483163bd3d059e6270b9f863b63f72dab7baabf2d1521412e64 +size 2779 diff --git a/textures/Normal.png b/textures/Normal.png new file mode 100644 index 0000000..ba9d01b --- /dev/null +++ b/textures/Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ddf3fab756c50827096cbced25cbde34ca6b50b8c3da4d904b3ce9eabb782c76 +size 46935 diff --git a/textures/OcclusionRoughnessMetallic.png b/textures/OcclusionRoughnessMetallic.png new file mode 100644 index 0000000..ef516bf --- /dev/null +++ b/textures/OcclusionRoughnessMetallic.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:21fd3dc352ca68f30f476fda7aa4e59422d0165c2ae0f431da6c560ae5658aa8 +size 367 From 88e2e58228608109f8bba4455836698d4b5f1c81 Mon Sep 17 00:00:00 2001 From: Szymon Nowakowski Date: Tue, 11 Feb 2025 21:11:25 +0100 Subject: [PATCH 2/5] Introduce pipeline, shader sketch --- .gitignore | 1 + {textures => assets/textures}/BaseColor.png | 0 {textures => assets/textures}/Normal.png | 0 .../textures/OcclusionRoughnessMetallic.png | 3 + build.zig | 22 ++ pipeline/main.zig | 124 +++++++++++ shaders/block.wgsl | 201 ++++++++++++++++++ src/game.zig | 25 +++ textures/OcclusionRoughnessMetallic.png | 3 - 9 files changed, 376 insertions(+), 3 deletions(-) rename {textures => assets/textures}/BaseColor.png (100%) rename {textures => assets/textures}/Normal.png (100%) create mode 100644 assets/textures/OcclusionRoughnessMetallic.png create mode 100644 pipeline/main.zig create mode 100644 shaders/block.wgsl delete mode 100644 textures/OcclusionRoughnessMetallic.png diff --git a/.gitignore b/.gitignore index d8c8979..eec15b8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .zig-cache zig-out +/library diff --git a/textures/BaseColor.png b/assets/textures/BaseColor.png similarity index 100% rename from textures/BaseColor.png rename to assets/textures/BaseColor.png diff --git a/textures/Normal.png b/assets/textures/Normal.png similarity index 100% rename from textures/Normal.png rename to assets/textures/Normal.png 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 bc1c73c..da90c83 100644 --- a/build.zig +++ b/build.zig @@ -21,6 +21,28 @@ pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); + const pipeline_debug = b.option(bool, "pipeline_debug", "Build asset pipeline in debug mode") orelse false; + + const pipeline_mod = b.createModule(.{ + .root_source_file = b.path("pipeline/main.zig"), + .target = b.graph.host, + .optimize = if (pipeline_debug) .Debug else .ReleaseSafe, + }); + + pipeline_mod.addImport("zstbi", zstbi.module("root")); + + const pipeline = b.addExecutable(.{ + .name = "pipeline", + .root_module = pipeline_mod, + }); + + pipeline.linkLibrary(zstbi.artifact("zstbi")); + + const pipeline_cmd = b.addRunArtifact(pipeline); + pipeline_cmd.setCwd(b.path(".")); + + const pipeline_step = b.step("pipeline", "Run the asset pipeline"); + pipeline_step.dependOn(&pipeline_cmd.step); const exe_mod = b.createModule(.{ .root_source_file = b.path("src/main.zig"), diff --git a/pipeline/main.zig b/pipeline/main.zig new file mode 100644 index 0000000..0f10293 --- /dev/null +++ b/pipeline/main.zig @@ -0,0 +1,124 @@ +const std = @import("std"); + +const zstbi = @import("zstbi"); + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + + const allocator = gpa.allocator(); + + zstbi.init(allocator); + defer zstbi.deinit(); + + const cwd = std.fs.cwd(); + + var assets_dir = openDirOrExit(cwd, "assets", .{ .iterate = true }); + defer assets_dir.close(); + + var library_dir = openDirOrExit(cwd, "library", .{}); + defer library_dir.close(); + + visit(assets_dir, library_dir, allocator); +} + +fn visit(assets_dir: std.fs.Dir, library_dir: std.fs.Dir, allocator: std.mem.Allocator) void { + var it = assets_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 == .directory) { + var assets_subdir = assets_dir.openDir(entry.name, .{ .iterate = true }) catch |err| { + std.log.warn("Skipping directory \"{s}\" due to an error: {s}", .{ entry.name, @errorName(err) }); + continue; + }; + defer assets_subdir.close(); + + library_dir.makeDir(entry.name) catch |err| switch (err) { + error.PathAlreadyExists => { + // This is fine + }, + else => { + std.log.warn("Skipping directory \"{s}\" due to an error while creating corresponding library directory: {s}", .{ entry.name, @errorName(err) }); + continue; + }, + }; + + var library_subdir = library_dir.openDir(entry.name, .{}) catch |err| { + std.log.warn("Skipping directory \"{s}\" due to an error while opening corresponding library directory: {s}", .{ entry.name, @errorName(err) }); + continue; + }; + defer library_subdir.close(); + + visit(assets_subdir, library_subdir, allocator); + + continue; + } + if (entry.kind != .file) { + std.log.warn("Skipping \"{s}\", which is not a file nor a directory.", .{entry.name}); + continue; + } + + std.log.info("Processing \"{s}\"...", .{entry.name}); + + const infile = assets_dir.openFile(entry.name, .{}) catch |err| { + std.log.err("Could not open \"{s}\" file: {s}", .{ entry.name, @errorName(err) }); + continue; + }; + defer infile.close(); + + const inbuf = infile.readToEndAlloc(allocator, std.math.maxInt(usize)) catch |err| { + std.log.err("Could not read \"{s}\" file contents due to an error: {s}", .{ entry.name, @errorName(err) }); + continue; + }; + defer allocator.free(inbuf); + + var img = zstbi.Image.loadFromMemory(inbuf, 4) catch |err| { + std.log.err("Error reading \"{s}\" as an image file: {s}", .{ entry.name, @errorName(err) }); + continue; + }; + 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) }); + continue; + }; + 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) }); + continue; + }; + + std.log.debug("tile size: {}×{}", .{ tile_w, tile_h }); + + const outbuf = allocator.alloc(u8, 4 * tile_w * tile_h * tile_count) catch { + std.log.err("Ran out of memory while trying to allocate output buffer", .{}); + continue; + }; + defer allocator.free(outbuf); + + @memcpy(outbuf, img.data); + + const out_name = std.fs.path.stem(entry.name); + + library_dir.writeFile(.{ + .data = outbuf, + .sub_path = out_name, + }) catch |err| { + std.log.err("Couldn't write \"{s}\" file corresponding to \"{s}\" asset file: {s}", .{ out_name, entry.name, @errorName(err) }); + }; + } +} + +fn openDirOrExit(dir: std.fs.Dir, sub_path: []const u8, args: std.fs.Dir.OpenOptions) std.fs.Dir { + return dir.openDir(sub_path, args) catch |err| { + std.log.err("Could not open \"{s}\" directory: {s}", .{ sub_path, @errorName(err) }); + std.posix.exit(1); + }; +} diff --git a/shaders/block.wgsl b/shaders/block.wgsl new file mode 100644 index 0000000..da212ad --- /dev/null +++ b/shaders/block.wgsl @@ -0,0 +1,201 @@ +struct Vertex { + @location(0) positionOS: vec3, + @location(1) texCoord: vec2, + @location(2) normalOS: vec3, + @location(3) tangentOS: vec4, +} + +struct Varyings { + @builtin(position) positionCS: vec4, + @location(0) positionVS: vec3, + @location(1) texCoord: vec2, + @location(2) normalVS: vec3, + @location(3) tangentVS: vec3, + @location(4) bitangentVS: vec3, +} + +struct PointLight { + positionWS: vec3, + color: vec3, +} + +struct DirectionalLight { + directionWS: vec3, + color: vec3, +} + +struct GlobalUniforms { + matrixWStoVS: mat4x4, + matrixVStoCS: mat4x4, + ambientLight: vec3, + pointLightCount: u32, + directionalLightCount: u32, +} + +struct ObjectUniforms { + matrixOStoWS: mat4x4, + matrixOStoWSNormal: mat4x4, +} + +// --- GROUP 0 --- GLOBAL ------------------------------------------------------ + +@group(0) @binding(0) var _Global: GlobalUniforms; + +@group(0) @binding(1) var _PointLights: array; +@group(0) @binding(2) var _DirectionalLights: array; + +@group(0) @binding(3) var _Sampler: sampler; +@group(0) @binding(4) var _BaseColorTexture: texture_2d_array; +@group(0) @binding(5) var _OcclusionRoughnessMetallicTexture: texture_2d_array; +@group(0) @binding(6) var _NormalTexture: texture_2d_array; + +// --- GROUP 1 --- PER MATERIAL ------------------------------------------------ + +@group(1) @binding(0) var _TextureIndex: u32; + +// --- GROUP 2 --- PER OBJECT -------------------------------------------------- + +@group(2) @binding(0) var _Object: ObjectUniforms; + +// ----------------------------------------------------------------------------- + +const INV_PI: f32 = 0.31830987; +const IOR: f32 = 1.45; +const DIELECTRIC_F0: vec3 = vec3(pow((IOR - 1.0) / (IOR + 1.0), 2.0)); +const F90 = vec3(1.0); + +fn fresnelSchlick(dotVH: f32, f0: vec3) -> vec3 { + return mix(f0, F90, pow(1.0 - dotVH, 5.0)); +} + +fn visibilityGGX(dotNL: f32, dotNV: f32, alpha: f32) -> f32 { + let alphaSquared = alpha * alpha; + + let vGGX = dotNL * sqrt(dotNV * dotNV * (1.0 - alphaSquared) + alphaSquared); + let lGGX = dotNV * sqrt(dotNL * dotNL * (1.0 - alphaSquared) + alphaSquared); + let GGX = vGGX + lGGX; + return select(0.0, 0.5 / GGX, GGX > 0.0); +} + +fn distributionGGX(dotNH: f32, alpha: f32) -> f32 { + let alphaSquared = alpha * alpha; + let tmp = dotNH * dotNH * (alphaSquared - 1.0) + 1.0; + return alphaSquared * INV_PI / (tmp * tmp); +} + +fn toneMapAcesNarkowicz(color: vec3) -> vec3 { + const A: f32 = 2.51; + const B: f32 = 0.03; + const C: f32 = 2.43; + const D: f32 = 0.59; + const E: f32 = 0.14; + return saturate((color * (A * color + B)) / (color * (C * color + D) + E)); +} + +fn lightOutgoingRadiance( + viewDirectionVS: vec3, normalVS: vec3, dotNV: f32, + baseColor: vec3, alpha: f32, metallic: f32, f0: vec3, + incomingRadiance: vec3, lightDirectionVS: vec3, +) -> vec3 { + let halfVectorVS = normalize(lightDirectionVS + viewDirectionVS); + let dotVH = saturate(dot(viewDirectionVS, halfVectorVS)); + let dotNH = saturate(dot(normalVS, halfVectorVS)); + let dotNL = saturate(dot(normalVS, lightDirectionVS)); + + let fresnel = fresnelSchlick(dotVH, f0); + let visibility = visibilityGGX(dotNL, dotNV, alpha); + let distribution = distributionGGX(dotNH, alpha); + + let scatteredFactor = (1.0 - fresnel) * (1.0 - metallic) * baseColor * INV_PI; + let reflectedFactor = fresnel * visibility * distribution; + + return (scatteredFactor + reflectedFactor) * incomingRadiance * dotNL; +} + +@vertex +fn vert(vertex: Vertex) -> Varyings { + let positionWS = (_Object.matrixOStoWS * vec4(vertex.positionOS, 1.0)).xyz; + let positionVS = (_Global.matrixWStoVS * vec4(positionWS, 1.0)).xyz; + let positionCS = _Global.matrixVStoCS * vec4(positionVS, 1.0); + + let normalWS = normalize((_Object.matrixOStoWSNormal * vec4(vertex.normalOS, 0.0)).xyz); + let normalVS = normalize((_Global.matrixWStoVS * vec4(normalWS, 0.0)).xyz); + + let tangentWS = normalize((_Object.matrixOStoWS * vec4(vertex.tangentOS.xyz, 0.0)).xyz); + let tangentVS = normalize((_Global.matrixWStoVS * vec4(tangentWS, 0.0)).xyz); + + let bitangentVS = vertex.tangentOS.w * normalize(cross(normalVS, tangentVS)); + + var output: Varyings; + output.positionCS = positionCS; + output.positionVS = positionVS; + output.texCoord = vertex.texCoord; + output.normalVS = normalVS; + output.tangentVS = tangentVS; + output.bitangentVS = bitangentVS; + + return output; +} + +@fragment +fn frag(fragment: Varyings) -> @location(0) vec4 { + let baseColorTexel = textureSample(_BaseColorTexture, _Sampler, fragment.texCoord, _TextureIndex); + let occlusionRoughnessMetallicTexel = textureSample(_OcclusionRoughnessMetallicTexture, _Sampler, fragment.texCoord, _TextureIndex); + let normalTextureTexel = textureSample(_NormalTexture, _Sampler, fragment.texCoord, _TextureIndex); + + let baseColor = baseColorTexel.rgb; + let occlusion = occlusionRoughnessMetallicTexel.r; + let roughness = occlusionRoughnessMetallicTexel.g; + let metallic = occlusionRoughnessMetallicTexel.b; + + let tangentVS = normalize(fragment.tangentVS); + let bitangentVS = normalize(fragment.bitangentVS); + let matrixTStoVS = mat3x3(tangentVS, bitangentVS, fragment.normalVS); + let normalTS = normalTextureTexel.xyz * 2.0 - 1.0; + let normalVS = normalize(matrixTStoVS * normalTS); + + let positionVS = fragment.positionVS; + let viewDirectionVS = normalize(-positionVS); + let dotNV = saturate(dot(normalVS, viewDirectionVS)); + let alpha = roughness * roughness; + + let f0 = mix(DIELECTRIC_F0, baseColor, metallic); + + var outgoingRadiance = vec3(0.0); + + for (var i: u32 = 0; i < _Global.pointLightCount; i++) { + let light = _PointLights[i]; + + let lightPositionVS = (_Global.matrixWStoVS * vec4(light.positionWS, 1.0)).xyz; + let lightDirectionVS = normalize(lightPositionVS - positionVS); + let lightDistance = distance(positionVS, lightPositionVS); + let lightAttenuation = 1.0 / (lightDistance * lightDistance); + let incomingRadiance = light.color * lightAttenuation; + + outgoingRadiance += lightOutgoingRadiance( + viewDirectionVS, normalVS, dotNV, + baseColor, alpha, metallic, f0, + incomingRadiance, lightDirectionVS, + ); + } + + for (var i: u32 = 0; i < _Global.directionalLightCount; i++) { + let light = _DirectionalLights[i]; + + let lightDirectionVS = normalize((_Global.matrixWStoVS * vec4(light.directionWS, 0.0)).xyz); + let incomingRadiance = light.color; + + outgoingRadiance += lightOutgoingRadiance( + viewDirectionVS, normalVS, dotNV, + baseColor, alpha, metallic, f0, + incomingRadiance, lightDirectionVS, + ); + } + + outgoingRadiance += _Global.ambientLight * baseColor * occlusion; + + let toneMappedLinearColor = toneMapAcesNarkowicz(outgoingRadiance); + let toneMappedSrgbColor = pow(toneMappedLinearColor, vec3(1.0 / 2.2)); + + return vec4(toneMappedSrgbColor, 1.0); +} diff --git a/src/game.zig b/src/game.zig index b7da1ea..f21b128 100644 --- a/src/game.zig +++ b/src/game.zig @@ -8,6 +8,31 @@ const main = @import("main.zig"); var show_console: bool = false; var show_demo_window: bool = false; +const Vertex = extern struct { + position: [3]f32, + tex_coord: [2]f32, + normal: [3]f32, + tangent: [4]f32, + + pub fn init(x: f32, y: f32, z: f32, u: f32, v: f32, nx: f32, ny: f32, nz: f32, tx: f32, ty: f32, tz: f32, tw: f32) Vertex { + return .{ + .position = .{ x, y, z }, + .tex_coord = .{ u, v }, + .normal = .{ nx, ny, nz }, + .tangent = .{ tx, ty, tw, tz }, + }; + } +}; + +const vertex_buffer = [_]Vertex{ + Vertex.init(-0.5, -0.5, 0, 0, 1, 0, 0, 1, 1, 0, 0, -1), + Vertex.init(0.5, -0.5, 0, 1, 1, 0, 0, 1, 1, 0, 0, -1), + Vertex.init(-0.5, 0.5, 0, 0, 0, 0, 0, 1, 1, 0, 0, -1), + Vertex.init(0.5, 0.5, 0, 1, 0, 0, 0, 1, 1, 0, 0, -1), +}; + +const index_buffer = [_]u16{ 0, 1, 2, 2, 1, 3 }; + pub fn init() void {} pub fn update(dt: f32) void { diff --git a/textures/OcclusionRoughnessMetallic.png b/textures/OcclusionRoughnessMetallic.png deleted file mode 100644 index ef516bf..0000000 --- a/textures/OcclusionRoughnessMetallic.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:21fd3dc352ca68f30f476fda7aa4e59422d0165c2ae0f431da6c560ae5658aa8 -size 367 From d7794972d4739a0064c43bd1fce2aa8ef8817d62 Mon Sep 17 00:00:00 2001 From: Szymon Nowakowski Date: Sun, 16 Feb 2025 16:54:15 +0100 Subject: [PATCH 3/5] Add tilemap to texture array conversion to pipeline --- pipeline/main.zig | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/pipeline/main.zig b/pipeline/main.zig index 0f10293..460e37d 100644 --- a/pipeline/main.zig +++ b/pipeline/main.zig @@ -103,7 +103,7 @@ fn visit(assets_dir: std.fs.Dir, library_dir: std.fs.Dir, allocator: std.mem.All }; defer allocator.free(outbuf); - @memcpy(outbuf, img.data); + rearrange(grid_w, grid_h, tile_w, tile_h, img.data, outbuf); const out_name = std.fs.path.stem(entry.name); @@ -116,6 +116,42 @@ fn visit(assets_dir: std.fs.Dir, library_dir: std.fs.Dir, allocator: std.mem.All } } +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; + } + } + } +} + fn openDirOrExit(dir: std.fs.Dir, sub_path: []const u8, args: std.fs.Dir.OpenOptions) std.fs.Dir { return dir.openDir(sub_path, args) catch |err| { std.log.err("Could not open \"{s}\" directory: {s}", .{ sub_path, @errorName(err) }); From e101346d4f5a6d5f85ade2f5fa77d65ea453b5b0 Mon Sep 17 00:00:00 2001 From: Szymon Nowakowski Date: Sun, 16 Feb 2025 19:44:18 +0100 Subject: [PATCH 4/5] Steal some renderer helpers from my previous project --- src/IndexBuffer.zig | 49 ++++++++++++++++++++++++++ src/Samplers.zig | 47 +++++++++++++++++++++++++ src/Texture.zig | 82 ++++++++++++++++++++++++++++++++++++++++++++ src/VertexBuffer.zig | 53 ++++++++++++++++++++++++++++ 4 files changed, 231 insertions(+) create mode 100644 src/IndexBuffer.zig create mode 100644 src/Samplers.zig create mode 100644 src/Texture.zig create mode 100644 src/VertexBuffer.zig diff --git a/src/IndexBuffer.zig b/src/IndexBuffer.zig new file mode 100644 index 0000000..216ca16 --- /dev/null +++ b/src/IndexBuffer.zig @@ -0,0 +1,49 @@ +const zgpu = @import("zgpu"); + +const main = @import("main.zig"); + +index_buffer_handle: zgpu.BufferHandle, + +pub fn init(index_count: usize) @This() { + // NOTE For some reason, the method function call syntax doesn't compile + const index_buffer_handle = zgpu.GraphicsContext.createBuffer(main.gctx, .{ + .usage = .{ .copy_dst = true, .index = true }, + .size = index_count * @sizeOf(u16), + }); + + return .{ + .index_buffer_handle = index_buffer_handle, + }; +} + +pub fn deinit(self: *@This()) void { + main.gctx.releaseResource(self.index_buffer_handle); + self.* = undefined; +} + +pub fn indexCount(self: @This()) usize { + return @divExact(self.info().size, @sizeOf(u16)); +} + +pub fn write(self: @This(), offset: usize, data: []const u16) void { + main.gctx.queue.writeBuffer(self.obj(), offset, u16, data); +} + +pub fn ensureCapacityDiscard(self: *@This(), desired_index_count: usize) void { + if (self.indexCount() < desired_index_count) { + main.gctx.releaseResource(self.index_buffer_handle); + // NOTE For some reason, the method function call syntax doesn't compile + self.index_buffer_handle = zgpu.GraphicsContext.createBuffer(main.gctx, .{ + .usage = .{ .copy_dst = true, .index = true }, + .size = desired_index_count * @sizeOf(u16), + }); + } +} + +pub fn info(self: @This()) zgpu.BufferInfo { + return main.gctx.lookupResourceInfo(self.index_buffer_handle).?; +} + +pub fn obj(self: @This()) zgpu.wgpu.Buffer { + return main.gctx.lookupResource(self.index_buffer_handle).?; +} diff --git a/src/Samplers.zig b/src/Samplers.zig new file mode 100644 index 0000000..18ee15a --- /dev/null +++ b/src/Samplers.zig @@ -0,0 +1,47 @@ +const std = @import("std"); + +const zgpu = @import("zgpu"); + +const main = @import("main.zig"); + +pub var sampler_cache: std.AutoHashMapUnmanaged(Descriptor, zgpu.SamplerHandle) = .{}; + +pub const Descriptor = struct { + address_mode_u: zgpu.wgpu.AddressMode = .repeat, + address_mode_v: zgpu.wgpu.AddressMode = .repeat, + address_mode_w: zgpu.wgpu.AddressMode = .repeat, + mag_filter: zgpu.wgpu.FilterMode = .linear, + min_filter: zgpu.wgpu.FilterMode = .linear, + mipmap_filter: zgpu.wgpu.MipmapFilterMode = .linear, +}; + +pub fn getOrCreateSampler(descriptor: Descriptor) !zgpu.SamplerHandle { + const entry = try sampler_cache.getOrPut(main.allocator, descriptor); + + if (entry.found_existing) { + return entry.value_ptr.*; + } else { + const sampler_handle = main.gctx.createSampler(.{ + .address_mode_u = descriptor.address_mode_u, + .address_mode_v = descriptor.address_mode_v, + .address_mode_w = descriptor.address_mode_w, + .mag_filter = descriptor.mag_filter, + .min_filter = descriptor.min_filter, + .mipmap_filter = descriptor.mipmap_filter, + }); + + entry.key_ptr.* = descriptor; + entry.value_ptr.* = sampler_handle; + + return sampler_handle; + } +} + +pub fn deinit() void { + var sampler_cache_it = sampler_cache.valueIterator(); + while (sampler_cache_it.next()) |sampler_handle| { + main.gctx.releaseResource(sampler_handle.*); + } + + sampler_cache.deinit(main.allocator); +} diff --git a/src/Texture.zig b/src/Texture.zig new file mode 100644 index 0000000..cc4df3f --- /dev/null +++ b/src/Texture.zig @@ -0,0 +1,82 @@ +const zgpu = @import("zgpu"); + +const main = @import("main.zig"); + +texture_handle: zgpu.TextureHandle, +texture_view_handle: zgpu.TextureViewHandle, + +pub fn init2DSingleChannel(width: u32, height: u32) @This() { + const texture_handle = main.gctx.createTexture(.{ + .usage = .{ .texture_binding = true, .copy_dst = true }, + .size = .{ .width = width, .height = height }, + .format = .r8_unorm, + }); + + const texture_view_handle = main.gctx.createTextureView(texture_handle, .{ + .format = .r8_unorm, + .dimension = .tvdim_2d, + }); + + return .{ + .texture_handle = texture_handle, + .texture_view_handle = texture_view_handle, + }; +} + +pub fn init2D(width: u32, height: u32) @This() { + const texture_handle = main.gctx.createTexture(.{ + .usage = .{ .texture_binding = true, .copy_dst = true }, + .size = .{ .width = width, .height = height }, + .format = .rgba8_unorm, + }); + + const texture_view_handle = main.gctx.createTextureView(texture_handle, .{ + .format = .rgba8_unorm, + .dimension = .tvdim_2d, + }); + + return .{ + .texture_handle = texture_handle, + .texture_view_handle = texture_view_handle, + }; +} + +pub fn init2DArray(width: u32, height: u32, layers: u32) @This() { + const texture_handle = main.gctx.createTexture(.{ + .usage = .{ .texture_binding = true, .copy_dst = true }, + .size = .{ .width = width, .height = height, .depth_or_array_layers = layers }, + .format = .rgba8_unorm, + }); + + const texture_view_handle = main.gctx.createTextureView(texture_handle, .{ + .format = .rgba8_unorm, + .dimension = .tvdim_2d_array, + }); + + return .{ + .texture_handle = texture_handle, + .texture_view_handle = texture_view_handle, + }; +} + +pub fn deinit(self: *@This()) void { + main.gctx.releaseResource(self.texture_view_handle); + main.gctx.releaseResource(self.texture_handle); + self.* = undefined; +} + +pub fn texInfo(self: @This()) zgpu.TextureInfo { + return main.gctx.lookupResourceInfo(self.texture_handle).?; +} + +pub fn texObj(self: @This()) zgpu.wgpu.Texture { + return main.gctx.lookupResource(self.texture_handle).?; +} + +pub fn tvInfo(self: @This()) zgpu.TextureViewInfo { + return main.gctx.lookupResourceInfo(self.texture_view_handle).?; +} + +pub fn tvObj(self: @This()) zgpu.wgpu.TextureView { + return main.gctx.lookupResource(self.texture_view_handle).?; +} diff --git a/src/VertexBuffer.zig b/src/VertexBuffer.zig new file mode 100644 index 0000000..b16accf --- /dev/null +++ b/src/VertexBuffer.zig @@ -0,0 +1,53 @@ +const zgpu = @import("zgpu"); + +const main = @import("main.zig"); + +pub fn VertexBuffer(comptime T: type) type { + return struct { + vertex_buffer_handle: zgpu.BufferHandle, + + pub fn init(vertex_count: usize) @This() { + // NOTE For some reason, the method function call syntax doesn't compile + const vertex_buffer_handle = zgpu.GraphicsContext.createBuffer(main.gctx, .{ + .usage = .{ .copy_dst = true, .vertex = true }, + .size = vertex_count * @sizeOf(T), + }); + + return .{ + .vertex_buffer_handle = vertex_buffer_handle, + }; + } + + pub fn deinit(self: *@This()) void { + main.gctx.releaseResource(self.vertex_buffer_handle); + self.* = undefined; + } + + pub fn vertexCount(self: @This()) usize { + return @divExact(self.info().size, @sizeOf(T)); + } + + pub fn write(self: @This(), offset: usize, data: []const T) void { + main.gctx.queue.writeBuffer(self.obj(), offset, T, data); + } + + pub fn ensureCapacityDiscard(self: *@This(), desired_vertex_count: usize) void { + if (self.vertexCount() < desired_vertex_count) { + main.gctx.releaseResource(self.vertex_buffer_handle); + // NOTE For some reason, the method function call syntax doesn't compile + self.vertex_buffer_handle = zgpu.GraphicsContext.createBuffer(main.gctx, .{ + .usage = .{ .copy_dst = true, .vertex = true }, + .size = desired_vertex_count * @sizeOf(T), + }); + } + } + + pub fn info(self: @This()) zgpu.BufferInfo { + return main.gctx.lookupResourceInfo(self.vertex_buffer_handle).?; + } + + pub fn obj(self: @This()) zgpu.wgpu.Buffer { + return main.gctx.lookupResource(self.vertex_buffer_handle).?; + } + }; +} From 439d8d3b251c1880723144fb06acafb730944c2d Mon Sep 17 00:00:00 2001 From: Szymon Nowakowski Date: Sun, 16 Feb 2025 22:01:42 +0100 Subject: [PATCH 5/5] Renderer code --- src/StorageBuffer.zig | 53 ++++++ src/game.zig | 372 ++++++++++++++++++++++++++++++++++++++++-- src/main.zig | 2 +- 3 files changed, 409 insertions(+), 18 deletions(-) create mode 100644 src/StorageBuffer.zig diff --git a/src/StorageBuffer.zig b/src/StorageBuffer.zig new file mode 100644 index 0000000..8421e5f --- /dev/null +++ b/src/StorageBuffer.zig @@ -0,0 +1,53 @@ +const zgpu = @import("zgpu"); + +const main = @import("main.zig"); + +pub fn StorageBuffer(comptime T: type) type { + return struct { + storage_buffer_handle: zgpu.BufferHandle, + + pub fn init(desired_capacity: usize) @This() { + // NOTE For some reason, the method function call syntax doesn't compile + const storage_buffer_handle = zgpu.GraphicsContext.createBuffer(main.gctx, .{ + .usage = .{ .copy_dst = true, .storage = true }, + .size = desired_capacity * @sizeOf(T), + }); + + return .{ + .storage_buffer_handle = storage_buffer_handle, + }; + } + + pub fn deinit(self: *@This()) void { + main.gctx.releaseResource(self.storage_buffer_handle); + self.* = undefined; + } + + pub fn capacity(self: @This()) usize { + return @divExact(self.info().size, @sizeOf(T)); + } + + pub fn write(self: @This(), offset: usize, data: []const T) void { + main.gctx.queue.writeBuffer(self.obj(), offset, T, data); + } + + pub fn ensureCapacityDiscard(self: *@This(), desired_capacity: usize) void { + if (self.capacity() < desired_capacity) { + main.gctx.releaseResource(self.storage_buffer_handle); + // NOTE For some reason, the method function call syntax doesn't compile + self.storage_buffer_handle = zgpu.GraphicsContext.createBuffer(main.gctx, .{ + .usage = .{ .copy_dst = true, .storage = true }, + .size = desired_capacity * @sizeOf(T), + }); + } + } + + pub fn info(self: @This()) zgpu.BufferInfo { + return main.gctx.lookupResourceInfo(self.vertex_buffer_handle).?; + } + + pub fn obj(self: @This()) zgpu.wgpu.Buffer { + return main.gctx.lookupResource(self.vertex_buffer_handle).?; + } + }; +} diff --git a/src/game.zig b/src/game.zig index f21b128..065b7ea 100644 --- a/src/game.zig +++ b/src/game.zig @@ -2,38 +2,239 @@ const std = @import("std"); const zglfw = @import("zglfw"); const zgui = @import("zgui"); +const zgpu = @import("zgpu"); const main = @import("main.zig"); - -var show_console: bool = false; -var show_demo_window: bool = false; +const IndexBuffer = @import("IndexBuffer.zig"); +const Samplers = @import("Samplers.zig"); +const StorageBuffer = @import("StorageBuffer.zig").StorageBuffer; +const Texture = @import("Texture.zig"); +const VertexBuffer = @import("VertexBuffer.zig").VertexBuffer; const Vertex = extern struct { - position: [3]f32, + position_os: [3]f32, tex_coord: [2]f32, - normal: [3]f32, - tangent: [4]f32, + normal_os: [3]f32, + tangent_os: [4]f32, pub fn init(x: f32, y: f32, z: f32, u: f32, v: f32, nx: f32, ny: f32, nz: f32, tx: f32, ty: f32, tz: f32, tw: f32) Vertex { return .{ - .position = .{ x, y, z }, + .position_os = .{ x, y, z }, .tex_coord = .{ u, v }, - .normal = .{ nx, ny, nz }, - .tangent = .{ tx, ty, tw, tz }, + .normal_os = .{ nx, ny, nz }, + .tangent_os = .{ tx, ty, tw, tz }, }; } }; -const vertex_buffer = [_]Vertex{ - Vertex.init(-0.5, -0.5, 0, 0, 1, 0, 0, 1, 1, 0, 0, -1), - Vertex.init(0.5, -0.5, 0, 1, 1, 0, 0, 1, 1, 0, 0, -1), - Vertex.init(-0.5, 0.5, 0, 0, 0, 0, 0, 1, 1, 0, 0, -1), - Vertex.init(0.5, 0.5, 0, 1, 0, 0, 0, 1, 1, 0, 0, -1), +const PointLight = extern struct { + position_ws: [3]f32, + color: [3]f32, }; -const index_buffer = [_]u16{ 0, 1, 2, 2, 1, 3 }; +const DirectionalLight = extern struct { + direction_ws: [3]f32, + color: [3]f32, +}; -pub fn init() void {} +const GlobalUniforms = extern struct { + matrix_ws_to_vs: [16]f32, + matrix_vs_to_cs: [16]f32, + ambient_light: [3]f32, + point_light_count: u32, + directional_light_count: u32, +}; + +const ObjectUniforms = extern struct { + matrix_os_to_ws: [16]f32, + matrix_os_to_ws_normal: [16]f32, +}; + +var show_console: bool = false; +var show_demo_window: bool = false; + +var global_bind_group_layout: zgpu.BindGroupLayoutHandle = undefined; +var per_material_bind_group_layout: zgpu.BindGroupLayoutHandle = undefined; +var per_object_bind_group_layout: zgpu.BindGroupLayoutHandle = undefined; + +var vertex_buffer: VertexBuffer(Vertex) = undefined; +var index_buffer: IndexBuffer = undefined; + +var block_pipeline: zgpu.RenderPipelineHandle = undefined; + +var point_light_buffer: StorageBuffer(PointLight) = undefined; +var directional_light_buffer: StorageBuffer(DirectionalLight) = undefined; +var sampler: zgpu.SamplerHandle = undefined; +var base_color_texture: Texture = undefined; +var occlusion_roughness_metallic_texture: Texture = undefined; +var normal_texture: Texture = undefined; + +var global_bind_group: zgpu.BindGroupHandle = undefined; +var per_material_bind_group: zgpu.BindGroupHandle = undefined; +var per_object_bind_group: zgpu.BindGroupHandle = undefined; + +pub fn init() !void { + global_bind_group_layout = main.gctx.createBindGroupLayout(&.{ + zgpu.bufferEntry(0, .{ .vertex = true, .fragment = true }, .uniform, true, 0), + zgpu.bufferEntry(1, .{ .fragment = true }, .read_only_storage, false, 0), + zgpu.bufferEntry(2, .{ .fragment = true }, .read_only_storage, false, 0), + zgpu.samplerEntry(3, .{ .fragment = true }, .filtering), + zgpu.textureEntry(4, .{ .fragment = true }, .float, .tvdim_2d_array, false), + zgpu.textureEntry(5, .{ .fragment = true }, .float, .tvdim_2d_array, false), + zgpu.textureEntry(6, .{ .fragment = true }, .float, .tvdim_2d_array, false), + }); + errdefer main.gctx.releaseResource(global_bind_group_layout); + + per_material_bind_group_layout = main.gctx.createBindGroupLayout(&.{ + zgpu.bufferEntry(0, .{ .fragment = true }, .uniform, true, 0), + }); + errdefer main.gctx.releaseResource(per_material_bind_group_layout); + + per_object_bind_group_layout = main.gctx.createBindGroupLayout(&.{ + zgpu.bufferEntry(0, .{ .fragment = true }, .uniform, true, 0), + }); + errdefer main.gctx.releaseResource(per_object_bind_group_layout); + + vertex_buffer = VertexBuffer(Vertex).init(4); + vertex_buffer.write(0, &[_]Vertex{ + Vertex.init(-0.5, -0.5, 0, 0, 1, 0, 0, 1, 1, 0, 0, -1), + Vertex.init(0.5, -0.5, 0, 1, 1, 0, 0, 1, 1, 0, 0, -1), + Vertex.init(-0.5, 0.5, 0, 0, 0, 0, 0, 1, 1, 0, 0, -1), + Vertex.init(0.5, 0.5, 0, 1, 0, 0, 0, 1, 1, 0, 0, -1), + }); + errdefer vertex_buffer.deinit(); + + index_buffer = IndexBuffer.init(6); + index_buffer.write(0, &[_]u16{ 0, 1, 2, 2, 1, 3 }); + errdefer index_buffer.deinit(); + + block_pipeline = block_pipeline: { + const pipeline_layout = main.gctx.createPipelineLayout(&.{ + global_bind_group_layout, + per_material_bind_group_layout, + per_object_bind_group_layout, + }); + defer main.gctx.releaseResource(pipeline_layout); + + const module = zgpu.createWgslShaderModule(main.gctx.device, @embedFile("../shaders/block.wgsl"), null); + defer module.release(); + + const vertex_buffers = [_]zgpu.wgpu.VertexBufferLayout{ + vertexBufferLayoutFromStruct(Vertex), + }; + + const targets = [_]zgpu.wgpu.ColorTargetState{.{ + .format = zgpu.GraphicsContext.swapchain_format, + .blend = &.{ + .color = .{ + .operation = .add, + .src_factor = .one, + .dst_factor = .one_minus_src_alpha, + }, + .alpha = .{ + .operation = .add, + .src_factor = .one, + .dst_factor = .one_minus_src_alpha, + }, + }, + }}; + + const pipeline_descriptor = zgpu.wgpu.RenderPipelineDescriptor{ + .vertex = .{ + .module = module, + .entry_point = "vert", + .buffer_count = vertex_buffers.len, + .buffers = &vertex_buffers, + }, + .fragment = .{ + .module = module, + .entry_point = "frag", + .target_count = targets.len, + .targets = &targets, + }, + .primitive = .{ + .cull_mode = .back, + }, + }; + break :block_pipeline main.gctx.createRenderPipeline(pipeline_layout, pipeline_descriptor); + }; + errdefer main.gctx.releaseResource(block_pipeline); + + point_light_buffer = StorageBuffer(PointLight).init(4); + errdefer point_light_buffer.deinit(); + + directional_light_buffer = StorageBuffer(DirectionalLight).init(4); + errdefer directional_light_buffer.deinit(); + + sampler = main.gctx.createSampler(.{ + .address_mode_u = .repeat, + .address_mode_v = .repeat, + .address_mode_w = .repeat, + .mag_filter = .linear, + .min_filter = .linear, + .mipmap_filter = .linear, + }); + errdefer main.gctx.releaseResource(sampler); + + const tile_count = 16; + + const base_color_texture_data = @embedFile("../library/textures/BaseColor"); + const base_color_tile_size = std.math.sqrt(@divExact(base_color_texture_data.len, 4 * tile_count)); + base_color_texture = Texture.init2DArray(base_color_tile_size, base_color_tile_size, tile_count); + errdefer base_color_texture.deinit(); + main.gctx.queue.writeTexture( + .{ .texture = base_color_texture.texObj() }, + .{ .bytes_per_row = 4 * base_color_tile_size, .rows_per_image = base_color_tile_size }, + .{ .width = base_color_tile_size, .height = base_color_tile_size, .depth_or_array_layers = tile_count }, + u8, + base_color_texture_data, + ); + + const occlusion_roughness_metallic_texture_data = @embedFile("../library/textures/OcclusionRoughnessMetallic"); + const occlusion_roughness_metallic_tile_size = std.math.sqrt(@divExact(occlusion_roughness_metallic_texture_data.len, 4 * tile_count)); + occlusion_roughness_metallic_texture = Texture.init2DArray(occlusion_roughness_metallic_tile_size, occlusion_roughness_metallic_tile_size, tile_count); + errdefer occlusion_roughness_metallic_texture.deinit(); + main.gctx.queue.writeTexture( + .{ .texture = occlusion_roughness_metallic_texture.texObj() }, + .{ .bytes_per_row = 4 * occlusion_roughness_metallic_tile_size, .rows_per_image = occlusion_roughness_metallic_tile_size }, + .{ .width = occlusion_roughness_metallic_tile_size, .height = occlusion_roughness_metallic_tile_size, .depth_or_array_layers = tile_count }, + u8, + occlusion_roughness_metallic_texture_data, + ); + + const normal_texture_data = @embedFile("../library/textures/Normal"); + const normal_tile_size = std.math.sqrt(@divExact(normal_texture_data.len, 4 * tile_count)); + normal_texture = Texture.init2DArray(normal_tile_size, normal_tile_size, tile_count); + errdefer normal_texture.deinit(); + main.gctx.queue.writeTexture( + .{ .texture = normal_texture.texObj() }, + .{ .bytes_per_row = 4 * normal_tile_size, .rows_per_image = normal_tile_size }, + .{ .width = normal_tile_size, .height = normal_tile_size, .depth_or_array_layers = tile_count }, + u8, + normal_texture_data, + ); + + global_bind_group = main.gctx.createBindGroup(global_bind_group_layout, &.{ + .{ .binding = 0, .buffer_handle = main.gctx.uniforms.buffer, .size = @sizeOf(GlobalUniforms) }, + .{ .binding = 1, .buffer_handle = point_light_buffer.storage_buffer_handle, .size = point_light_buffer.info().size }, + .{ .binding = 2, .buffer_handle = directional_light_buffer.storage_buffer_handle, .size = directional_light_buffer.info().size }, + .{ .binding = 3, .sampler_handle = sampler }, + .{ .binding = 4, .texture_view_handle = base_color_texture.texture_view_handle }, + .{ .binding = 5, .texture_view_handle = occlusion_roughness_metallic_texture.texture_view_handle }, + .{ .binding = 6, .texture_view_handle = normal_texture.texture_view_handle }, + }); + errdefer main.gctx.releaseResource(global_bind_group); + + per_material_bind_group = main.gctx.createBindGroup(per_material_bind_group, &.{ + .{ .binding = 0, .buffer_handle = main.gctx.uniforms.buffer, .size = @sizeOf(u32) }, + }); + errdefer main.gctx.releaseResource(per_material_bind_group); + + per_object_bind_group = main.gctx.createBindGroup(per_object_bind_group, &.{ + .{ .binding = 0, .buffer_handle = main.gctx.uniforms.buffer, .size = @sizeOf(ObjectUniforms) }, + }); + errdefer main.gctx.releaseResource(per_object_bind_group); +} pub fn update(dt: f32) void { _ = dt; @@ -45,6 +246,68 @@ pub fn update(dt: f32) void { if (show_demo_window) { zgui.showDemoWindow(&show_demo_window); } + + const back_buffer_view = main.gctx.swapchain.getCurrentTextureView(); + defer back_buffer_view.release(); + + const commands = commands: { + const encoder = main.gctx.device.createCommandEncoder(null); + defer encoder.release(); + + { + const color_attachments = [_]zgpu.wgpu.RenderPassColorAttachment{.{ + .view = back_buffer_view, + .load_op = .clear, + .store_op = .store, + .clear_value = .{ .r = 0.0, .g = 0.0, .b = 0.0, .a = 1.0 }, + }}; + const pass = encoder.beginRenderPass(.{ + .color_attachment_count = color_attachments.len, + .color_attachments = &color_attachments, + }); + defer { + pass.end(); + pass.release(); + } + + const block_pipeline_obj = main.gctx.lookupResource(block_pipeline).?; + const global_bind_group_obj = main.gctx.lookupResource(global_bind_group).?; + const per_material_bind_group_obj = main.gctx.lookupResource(per_material_bind_group).?; + const per_object_bind_group_obj = main.gctx.lookupResource(per_object_bind_group).?; + + pass.setPipeline(block_pipeline_obj); + + const vb = vertex_buffer.info(); + const ib = index_buffer.info(); + + pass.setVertexBuffer(0, vb.gpuobj.?, 0, vb.size); + pass.setIndexBuffer(ib.gpuobj.?, .uint16, 0, ib.size); + + const global_uniforms_offsets = allocateUniform(GlobalUniforms{ + .matrix_ws_to_vs = .{ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 }, + .matrix_vs_to_cs = .{ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 }, + .ambient_light = .{ 0, 0, 0 }, + .point_light_count = 0, + .directional_light_count = 0, + }); + pass.setBindGroup(0, global_bind_group_obj, &global_uniforms_offsets); + + const per_material_offsets = allocateUniform(@as(u32, 0)); + pass.setBindGroup(1, per_material_bind_group_obj, &per_material_offsets); + + const per_object_offsets = allocateUniform(ObjectUniforms{ + .matrix_os_to_ws = .{ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 }, + .matrix_os_to_ws_normal = .{ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 }, + }); + pass.setBindGroup(2, per_object_bind_group_obj, &per_object_offsets); + + pass.drawIndexed(@intCast(@divExact(ib.size, @sizeOf(u16))), 1, 0, 0, 0); + } + break :commands encoder.finish(null); + }; + defer commands.release(); + + main.gctx.submit(&.{commands}); } pub fn charCallback( @@ -103,7 +366,9 @@ pub fn scrollCallback( _ = yoffset; } -pub fn deinit() void {} +pub fn deinit() void { + Samplers.deinit(); +} fn showConsole() void { const display_size = zgui.io.getDisplaySize(); @@ -133,3 +398,76 @@ fn showConsole() void { } zgui.end(); } + +fn allocateUniform(value: anytype) [1]u32 { + const mem = main.gctx.uniformsAllocate(@TypeOf(value), 1); + mem.slice[0] = value; + return [1]u32{mem.offset}; +} + +fn vertexAttributesFromStruct(comptime T: type) [structFieldCount(T)]zgpu.wgpu.VertexAttribute { + const struct_info = @typeInfo(T).Struct; + if (struct_info.layout != .Extern) { + @compileError("Vertex struct " ++ @typeName(T) ++ " does not have extern layout"); + } + + comptime var ret: [structFieldCount(T)]zgpu.wgpu.VertexAttribute = undefined; + inline for (struct_info.fields, 0..) |struct_field, i| { + const vertex_format: zgpu.wgpu.VertexFormat = switch (struct_field.type) { + [2]u8 => .uint8x2, + [4]u8 => .uint8x4, + + [2]i8 => .sint8x2, + [4]i8 => .sint8x4, + + [2]u16 => .uint16x2, + [4]u16 => .uint16x4, + + [2]i16 => .sint16x2, + [4]i16 => .sint16x4, + + [2]f16 => .float16x2, + [4]f16 => .float16x4, + + f32 => .float32, + [1]f32 => .float32, + [2]f32 => .float32x2, + [3]f32 => .float32x3, + [4]f32 => .float32x4, + + u32 => .uint32, + [1]u32 => .uint32, + [2]u32 => .uint32x2, + [3]u32 => .uint32x3, + [4]u32 => .uint32x4, + + i32 => .sint32, + [1]i32 => .sint32, + [2]i32 => .sint32x2, + [3]i32 => .sint32x3, + [4]i32 => .sint32x4, + else => @compileError("Vertex attribute of type " ++ @typeName(struct_field.type) ++ " not supported"), + }; + + ret[i] = .{ + .format = vertex_format, + .offset = @offsetOf(T, struct_field.name), + .shader_location = i, + }; + } + + return ret; +} + +fn vertexBufferLayoutFromStruct(comptime T: type) zgpu.wgpu.VertexBufferLayout { + return .{ + .array_stride = @sizeOf(T), + .step_mode = .vertex, + .attribute_count = structFieldCount(T), + .attributes = comptime &vertexAttributesFromStruct(T), + }; +} + +fn structFieldCount(comptime T: type) comptime_int { + return @typeInfo(T).Struct.fields.len; +} diff --git a/src/main.zig b/src/main.zig index 92438cc..9b436db 100644 --- a/src/main.zig +++ b/src/main.zig @@ -129,7 +129,7 @@ pub fn main() !void { const zone_init_game = ztracy.ZoneN(@src(), "Init game"); - game.init(); + try game.init(); defer game.deinit(); zone_init_game.End();