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