Introduce pipeline, shader sketch

This commit is contained in:
2025-02-11 21:11:25 +01:00
parent 4c381f5e77
commit 88e2e58228
9 changed files with 376 additions and 3 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
.zig-cache
zig-out
/library

BIN
assets/textures/OcclusionRoughnessMetallic.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -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"),

124
pipeline/main.zig Normal file
View File

@@ -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);
};
}

201
shaders/block.wgsl Normal file
View File

@@ -0,0 +1,201 @@
struct Vertex {
@location(0) positionOS: vec3<f32>,
@location(1) texCoord: vec2<f32>,
@location(2) normalOS: vec3<f32>,
@location(3) tangentOS: vec4<f32>,
}
struct Varyings {
@builtin(position) positionCS: vec4<f32>,
@location(0) positionVS: vec3<f32>,
@location(1) texCoord: vec2<f32>,
@location(2) normalVS: vec3<f32>,
@location(3) tangentVS: vec3<f32>,
@location(4) bitangentVS: vec3<f32>,
}
struct PointLight {
positionWS: vec3<f32>,
color: vec3<f32>,
}
struct DirectionalLight {
directionWS: vec3<f32>,
color: vec3<f32>,
}
struct GlobalUniforms {
matrixWStoVS: mat4x4<f32>,
matrixVStoCS: mat4x4<f32>,
ambientLight: vec3<f32>,
pointLightCount: u32,
directionalLightCount: u32,
}
struct ObjectUniforms {
matrixOStoWS: mat4x4<f32>,
matrixOStoWSNormal: mat4x4<f32>,
}
// --- GROUP 0 --- GLOBAL ------------------------------------------------------
@group(0) @binding(0) var<uniform> _Global: GlobalUniforms;
@group(0) @binding(1) var<storage> _PointLights: array<PointLight>;
@group(0) @binding(2) var<storage> _DirectionalLights: array<DirectionalLight>;
@group(0) @binding(3) var _Sampler: sampler;
@group(0) @binding(4) var _BaseColorTexture: texture_2d_array<f32>;
@group(0) @binding(5) var _OcclusionRoughnessMetallicTexture: texture_2d_array<f32>;
@group(0) @binding(6) var _NormalTexture: texture_2d_array<f32>;
// --- GROUP 1 --- PER MATERIAL ------------------------------------------------
@group(1) @binding(0) var<uniform> _TextureIndex: u32;
// --- GROUP 2 --- PER OBJECT --------------------------------------------------
@group(2) @binding(0) var<uniform> _Object: ObjectUniforms;
// -----------------------------------------------------------------------------
const INV_PI: f32 = 0.31830987;
const IOR: f32 = 1.45;
const DIELECTRIC_F0: vec3<f32> = vec3(pow((IOR - 1.0) / (IOR + 1.0), 2.0));
const F90 = vec3(1.0);
fn fresnelSchlick(dotVH: f32, f0: vec3<f32>) -> vec3<f32> {
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<f32>) -> vec3<f32> {
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<f32>, normalVS: vec3<f32>, dotNV: f32,
baseColor: vec3<f32>, alpha: f32, metallic: f32, f0: vec3<f32>,
incomingRadiance: vec3<f32>, lightDirectionVS: vec3<f32>,
) -> vec3<f32> {
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<f32> {
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);
}

View File

@@ -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 {

BIN
textures/OcclusionRoughnessMetallic.png (Stored with Git LFS)

Binary file not shown.