remove examples that require model3d

See hexops/mach#969

Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
Stephen Gutekanst 2024-06-02 09:01:07 -07:00
parent 13ff5097db
commit ffd03c3b0b
24 changed files with 0 additions and 5771 deletions

View file

@ -560,7 +560,6 @@ fn buildExamples(
) !void { ) !void {
const Dependency = enum { const Dependency = enum {
assets, assets,
model3d,
freetype, freetype,
zigimg, zigimg,
}; };
@ -600,12 +599,6 @@ fn buildExamples(
.optimize = optimize, .optimize = optimize,
})) |dep| exe.root_module.addImport("assets", dep.module("mach-example-assets")); })) |dep| exe.root_module.addImport("assets", dep.module("mach-example-assets"));
}, },
.model3d => {
if (b.lazyDependency("mach_model3d", .{
.target = target,
.optimize = optimize,
})) |dep| exe.root_module.addImport("model3d", dep.module("mach-model3d"));
},
.freetype => { .freetype => {
if (b.lazyDependency("mach_freetype", .{ if (b.lazyDependency("mach_freetype", .{
.target = target, .target = target,
@ -642,7 +635,6 @@ fn buildCoreExamples(
) !void { ) !void {
const Dependency = enum { const Dependency = enum {
zigimg, zigimg,
model3d,
assets, assets,
zmath, zmath,
}; };
@ -667,16 +659,6 @@ fn buildCoreExamples(
.{ .name = "fractal-cube", .deps = &.{.zmath} }, .{ .name = "fractal-cube", .deps = &.{.zmath} },
.{ .name = "map-async", .deps = &.{.zmath} }, .{ .name = "map-async", .deps = &.{.zmath} },
.{ .name = "rgb-quad", .deps = &.{.zmath} }, .{ .name = "rgb-quad", .deps = &.{.zmath} },
.{
.name = "pbr-basic",
.deps = &.{ .zmath, .model3d, .assets },
.std_platform_only = true,
},
.{
.name = "deferred-rendering",
.deps = &.{ .zmath, .model3d, .assets },
.std_platform_only = true,
},
.{ .name = "textured-cube", .deps = &.{ .zmath, .zigimg, .assets } }, .{ .name = "textured-cube", .deps = &.{ .zmath, .zigimg, .assets } },
.{ .name = "textured-quad", .deps = &.{ .zmath, .zigimg, .assets } }, .{ .name = "textured-quad", .deps = &.{ .zmath, .zigimg, .assets } },
.{ .name = "sprite2d", .deps = &.{ .zmath, .zigimg, .assets } }, .{ .name = "sprite2d", .deps = &.{ .zmath, .zigimg, .assets } },
@ -688,13 +670,11 @@ fn buildCoreExamples(
.{ .name = "boids", .deps = &.{.zmath}, .sysgpu = true }, .{ .name = "boids", .deps = &.{.zmath}, .sysgpu = true },
.{ .name = "clear-color", .deps = &.{.zmath}, .sysgpu = true }, .{ .name = "clear-color", .deps = &.{.zmath}, .sysgpu = true },
.{ .name = "cubemap", .deps = &.{ .zmath, .zigimg, .assets }, .sysgpu = true }, .{ .name = "cubemap", .deps = &.{ .zmath, .zigimg, .assets }, .sysgpu = true },
.{ .name = "deferred-rendering", .deps = &.{ .zmath, .model3d, .assets }, .std_platform_only = true, .sysgpu = true },
.{ .name = "fractal-cube", .deps = &.{.zmath}, .sysgpu = true }, .{ .name = "fractal-cube", .deps = &.{.zmath}, .sysgpu = true },
.{ .name = "gen-texture-light", .deps = &.{.zmath}, .sysgpu = true }, .{ .name = "gen-texture-light", .deps = &.{.zmath}, .sysgpu = true },
.{ .name = "image-blur", .deps = &.{ .zmath, .zigimg, .assets }, .sysgpu = true }, .{ .name = "image-blur", .deps = &.{ .zmath, .zigimg, .assets }, .sysgpu = true },
.{ .name = "instanced-cube", .deps = &.{.zmath}, .sysgpu = true }, .{ .name = "instanced-cube", .deps = &.{.zmath}, .sysgpu = true },
.{ .name = "map-async", .deps = &.{.zmath}, .sysgpu = true }, .{ .name = "map-async", .deps = &.{.zmath}, .sysgpu = true },
.{ .name = "pbr-basic", .deps = &.{ .zmath, .model3d, .assets }, .std_platform_only = true, .sysgpu = true },
.{ .name = "pixel-post-process", .deps = &.{.zmath}, .sysgpu = true }, .{ .name = "pixel-post-process", .deps = &.{.zmath}, .sysgpu = true },
.{ .name = "procedural-primitives", .deps = &.{.zmath}, .sysgpu = true }, .{ .name = "procedural-primitives", .deps = &.{.zmath}, .sysgpu = true },
.{ .name = "rotating-cube", .deps = &.{.zmath}, .sysgpu = true }, .{ .name = "rotating-cube", .deps = &.{.zmath}, .sysgpu = true },
@ -746,12 +726,6 @@ fn buildCoreExamples(
.optimize = optimize, .optimize = optimize,
})) |dep| app.module.addImport("zigimg", dep.module("zigimg")); })) |dep| app.module.addImport("zigimg", dep.module("zigimg"));
}, },
.model3d => {
if (b.lazyDependency("mach_model3d", .{
.target = target,
.optimize = optimize,
})) |dep| app.module.addImport("model3d", dep.module("mach-model3d"));
},
.assets => { .assets => {
if (b.lazyDependency("mach_example_assets", .{ if (b.lazyDependency("mach_example_assets", .{
.target = target, .target = target,

View file

@ -87,11 +87,6 @@
.hash = "1220ebfa8587cfd644995fc08e218dbb3ebd7344fb8e129ff02bc5a6d52a2325370d", .hash = "1220ebfa8587cfd644995fc08e218dbb3ebd7344fb8e129ff02bc5a6d52a2325370d",
.lazy = true, .lazy = true,
}, },
.mach_model3d = .{
.url = "https://pkg.machengine.org/mach-model3d/5d4dd54db2da123b38656d4574afaa4c04fd3838.tar.gz",
.hash = "1220c1c16fa17783379f773c291af3693b84744200099f1cf5c1ddb66c4fe88bcf3a",
.lazy = true,
},
.mach_opus = .{ .mach_opus = .{
.url = "https://pkg.machengine.org/mach-opus/58a22201881d7639339f3c029d13a00bbfe005e2.tar.gz", .url = "https://pkg.machengine.org/mach-opus/58a22201881d7639339f3c029d13a00bbfe005e2.tar.gz",
.hash = "1220821426e087874779f8d76a6bb74844aa3934648aad5b7331701e5f386791c51e", .hash = "1220821426e087874779f8d76a6bb74844aa3934648aad5b7331701e5f386791c51e",

View file

@ -1,83 +0,0 @@
@group(0) @binding(0) var gBufferNormal: texture_2d<f32>;
@group(0) @binding(1) var gBufferAlbedo: texture_2d<f32>;
@group(0) @binding(2) var gBufferDepth: texture_depth_2d;
struct LightData {
position : vec4<f32>,
color : vec3<f32>,
radius : f32,
}
struct LightsBuffer {
lights: array<LightData>,
}
@group(1) @binding(0) var<storage, read> lightsBuffer: LightsBuffer;
struct Config {
numLights : u32,
}
struct Camera {
viewProjectionMatrix : mat4x4<f32>,
invViewProjectionMatrix : mat4x4<f32>,
}
@group(1) @binding(1) var<uniform> config: Config;
@group(1) @binding(2) var<uniform> camera: Camera;
fn world_from_screen_coord(coord : vec2<f32>, depth_sample: f32) -> vec3<f32> {
// reconstruct world-space position from the screen coordinate.
let posClip = vec4(coord.x * 2.0 - 1.0, (1.0 - coord.y) * 2.0 - 1.0, depth_sample, 1.0);
let posWorldW = camera.invViewProjectionMatrix * posClip;
let posWorld = posWorldW.xyz / posWorldW.www;
return posWorld;
}
@fragment
fn main(
@builtin(position) coord : vec4<f32>
) -> @location(0) vec4<f32> {
var result : vec3<f32>;
let depth = textureLoad(
gBufferDepth,
vec2<i32>(floor(coord.xy)),
0
);
// Don't light the sky.
if (depth >= 1.0) {
discard;
}
let bufferSize = textureDimensions(gBufferDepth);
let coordUV = coord.xy / vec2<f32>(bufferSize);
let position = world_from_screen_coord(coordUV, depth);
let normal = textureLoad(
gBufferNormal,
vec2<i32>(floor(coord.xy)),
0
).xyz;
let albedo = textureLoad(
gBufferAlbedo,
vec2<i32>(floor(coord.xy)),
0
).rgb;
for (var i = 0u; i < config.numLights; i++) {
let L = lightsBuffer.lights[i].position.xyz - position;
let distance = length(L);
if (distance > lightsBuffer.lights[i].radius) {
continue;
}
let lambert = max(dot(normal, normalize(L)), 0.0);
result += vec3<f32>(
lambert * pow(1.0 - distance / lightsBuffer.lights[i].radius, 2.0) * lightsBuffer.lights[i].color * albedo
);
}
// some manual ambient
result += vec3(0.2);
return vec4(result, 1.0);
}

View file

@ -1,44 +0,0 @@
@group(0) @binding(0) var gBufferNormal: texture_2d<f32>;
@group(0) @binding(1) var gBufferAlbedo: texture_2d<f32>;
@group(0) @binding(2) var gBufferDepth: texture_depth_2d;
@group(1) @binding(0) var<uniform> canvas : CanvasConstants;
struct CanvasConstants {
size: vec2<f32>,
}
@fragment
fn main(
@builtin(position) coord : vec4<f32>
) -> @location(0) vec4<f32> {
var result : vec4<f32>;
let c = coord.xy / vec2<f32>(canvas.size.x, canvas.size.y);
if (c.x < 0.33333) {
let rawDepth = textureLoad(
gBufferDepth,
vec2<i32>(floor(coord.xy)),
0
);
// remap depth into something a bit more visible
let depth = (1.0 - rawDepth) * 50.0;
result = vec4(depth);
} else if (c.x < 0.66667) {
result = textureLoad(
gBufferNormal,
vec2<i32>(floor(coord.xy)),
0
);
result.x = (result.x + 1.0) * 0.5;
result.y = (result.y + 1.0) * 0.5;
result.z = (result.z + 1.0) * 0.5;
} else {
result = textureLoad(
gBufferAlbedo,
vec2<i32>(floor(coord.xy)),
0
);
}
return result;
}

View file

@ -1,22 +0,0 @@
struct GBufferOutput {
@location(0) normal : vec4<f32>,
// Textures: diffuse color, specular color, smoothness, emissive etc. could go here
@location(1) albedo : vec4<f32>,
}
@fragment
fn main(
@location(0) fragNormal: vec3<f32>,
@location(1) fragUV : vec2<f32>
) -> GBufferOutput {
// faking some kind of checkerboard texture
let uv = floor(30.0 * fragUV);
let c = 0.2 + 0.5 * ((uv.x + uv.y) - 2.0 * floor((uv.x + uv.y) / 2.0));
var output : GBufferOutput;
output.normal = vec4(fragNormal, 1.0);
output.albedo = vec4(c, c, c, 1.0);
return output;
}

View file

@ -1,34 +0,0 @@
struct LightData {
position : vec4<f32>,
color : vec3<f32>,
radius : f32,
}
struct LightsBuffer {
lights: array<LightData>,
}
@group(0) @binding(0) var<storage, read_write> lightsBuffer: LightsBuffer;
struct Config {
numLights : u32,
}
@group(0) @binding(1) var<uniform> config: Config;
struct LightExtent {
min : vec4<f32>,
max : vec4<f32>,
}
@group(0) @binding(2) var<uniform> lightExtent: LightExtent;
@compute @workgroup_size(64, 1, 1)
fn main(@builtin(global_invocation_id) GlobalInvocationID : vec3<u32>) {
var index = GlobalInvocationID.x;
if (index >= config.numLights) {
return;
}
lightsBuffer.lights[index].position.y = lightsBuffer.lights[index].position.y - 0.5 - 0.003 * (f32(index) - 64.0 * floor(f32(index) / 64.0));
if (lightsBuffer.lights[index].position.y < lightExtent.min.y) {
lightsBuffer.lights[index].position.y = lightExtent.max.y;
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,11 +0,0 @@
@vertex
fn main(
@builtin(vertex_index) VertexIndex : u32
) -> @builtin(position) vec4<f32> {
const pos = array(
vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0),
vec2(-1.0, 1.0), vec2(1.0, -1.0), vec2(1.0, 1.0),
);
return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
}

View file

@ -1,30 +0,0 @@
struct Uniforms {
modelMatrix : mat4x4<f32>,
normalModelMatrix : mat4x4<f32>,
}
struct Camera {
viewProjectionMatrix : mat4x4<f32>,
invViewProjectionMatrix : mat4x4<f32>,
}
@group(0) @binding(0) var<uniform> uniforms : Uniforms;
@group(0) @binding(1) var<uniform> camera : Camera;
struct VertexOutput {
@builtin(position) Position : vec4<f32>,
@location(0) fragNormal: vec3<f32>, // normal in world space
@location(1) fragUV: vec2<f32>,
}
@vertex
fn main(
@location(0) position : vec3<f32>,
@location(1) normal : vec3<f32>,
@location(2) uv : vec2<f32>
) -> VertexOutput {
var output : VertexOutput;
let worldPosition = (uniforms.modelMatrix * vec4(position, 1.0)).xyz;
output.Position = camera.viewProjectionMatrix * vec4(worldPosition, 1.0);
output.fragNormal = normalize((uniforms.normalModelMatrix * vec4(normal, 1.0)).xyz);
output.fragUV = uv;
return output;
}

View file

@ -1,188 +0,0 @@
const std = @import("std");
/// Vertex writer manages the placement of vertices by tracking which are unique. If a duplicate vertex is added
/// with `put`, only it's index will be written to the index buffer.
/// `IndexType` should match the integer type used for the index buffer
pub fn VertexWriter(comptime VertexType: type, comptime IndexType: type) type {
return struct {
const MapEntry = struct {
packed_index: IndexType = null_index,
next_sparse: IndexType = null_index,
};
const null_index: IndexType = std.math.maxInt(IndexType);
vertices: []VertexType,
indices: []IndexType,
sparse_to_packed_map: []MapEntry,
/// Next index outside of the 1:1 mapping range for storing
/// position -> normal collisions
next_collision_index: IndexType,
/// Next packed index
next_packed_index: IndexType,
written_indices_count: IndexType,
/// Allocate storage and set default values
/// `sparse_vertices_count` is the number of vertices in the source before de-duplication / remapping
/// Put more succinctly, the largest index value in source index buffer
/// `max_vertex_count` is largest permutation of vertices assuming that {vertex, uv, normal} never map 1:1 and always
/// create a new mapping
pub fn init(
allocator: std.mem.Allocator,
indices_count: IndexType,
sparse_vertices_count: IndexType,
max_vertex_count: IndexType,
) !@This() {
var result: @This() = undefined;
result.vertices = try allocator.alloc(VertexType, max_vertex_count);
result.indices = try allocator.alloc(IndexType, indices_count);
result.sparse_to_packed_map = try allocator.alloc(MapEntry, max_vertex_count);
result.next_collision_index = sparse_vertices_count;
result.next_packed_index = 0;
result.written_indices_count = 0;
@memset(result.sparse_to_packed_map, .{});
return result;
}
pub fn put(self: *@This(), vertex: VertexType, sparse_index: IndexType) void {
if (self.sparse_to_packed_map[sparse_index].packed_index == null_index) {
// New start of chain, reserve a new packed index and add entry to `index_map`
const packed_index = self.next_packed_index;
self.sparse_to_packed_map[sparse_index].packed_index = packed_index;
self.vertices[packed_index] = vertex;
self.indices[self.written_indices_count] = packed_index;
self.written_indices_count += 1;
self.next_packed_index += 1;
return;
}
var previous_sparse_index: IndexType = undefined;
var current_sparse_index = sparse_index;
while (current_sparse_index != null_index) {
const packed_index = self.sparse_to_packed_map[current_sparse_index].packed_index;
if (std.mem.eql(u8, &std.mem.toBytes(self.vertices[packed_index]), &std.mem.toBytes(vertex))) {
// We already have a record for this vertex in our chain
self.indices[self.written_indices_count] = packed_index;
self.written_indices_count += 1;
return;
}
previous_sparse_index = current_sparse_index;
current_sparse_index = self.sparse_to_packed_map[current_sparse_index].next_sparse;
}
// This is a new mapping for the given sparse index
const packed_index = self.next_packed_index;
const remapped_sparse_index = self.next_collision_index;
self.indices[self.written_indices_count] = packed_index;
self.vertices[packed_index] = vertex;
self.sparse_to_packed_map[previous_sparse_index].next_sparse = remapped_sparse_index;
self.sparse_to_packed_map[remapped_sparse_index].packed_index = packed_index;
self.next_packed_index += 1;
self.next_collision_index += 1;
self.written_indices_count += 1;
}
pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void {
allocator.free(self.vertices);
allocator.free(self.indices);
allocator.free(self.sparse_to_packed_map);
}
pub fn indexBuffer(self: @This()) []IndexType {
return self.indices;
}
pub fn vertexBuffer(self: @This()) []VertexType {
return self.vertices[0..self.next_packed_index];
}
};
}
test "VertexWriter" {
const Vec3 = [3]f32;
const Vertex = extern struct {
position: Vec3,
normal: Vec3,
};
const expect = std.testing.expect;
const allocator = std.testing.allocator;
const Face = struct {
position: [3]u16,
normal: [3]u16,
};
const vertices = [_]Vec3{
Vec3{ 1.0, 0.0, 0.0 }, // 0: Position
Vec3{ 2.0, 0.0, 0.0 }, // 1: Position
Vec3{ 3.0, 0.0, 0.0 }, // 2: Position
Vec3{ 1.0, 0.0, 0.0 }, // 3: Normal
Vec3{ 4.0, 0.0, 0.0 }, // 4: Position
Vec3{ 0.0, 1.0, 0.0 }, // 5: Normal
Vec3{ 5.0, 0.0, 0.0 }, // 6: Position
Vec3{ 0.0, 0.0, 1.0 }, // 7: Normal
Vec3{ 1.0, 0.0, 1.0 }, // 8: Normal
Vec3{ 6.0, 0.0, 0.0 }, // 9: Position
};
const faces = [_]Face{
.{ .position = .{ 0, 4, 2 }, .normal = .{ 7, 5, 3 } },
.{ .position = .{ 2, 3, 9 }, .normal = .{ 3, 7, 8 } },
.{ .position = .{ 9, 2, 4 }, .normal = .{ 8, 7, 5 } },
.{ .position = .{ 2, 6, 1 }, .normal = .{ 3, 5, 7 } },
.{ .position = .{ 9, 6, 0 }, .normal = .{ 5, 7, 8 } },
};
var writer = try VertexWriter(Vertex, u32).init(
allocator,
faces.len * 3, // indices count
vertices.len, // original vertices count
faces.len * 3, // maximum vertices count
);
defer writer.deinit(allocator);
for (faces) |face| {
var x: usize = 0;
while (x < 3) : (x += 1) {
const position_index = face.position[x];
const position = vertices[position_index];
const normal = vertices[face.normal[x]];
const vertex = Vertex{
.position = position,
.normal = normal,
};
writer.put(vertex, position_index);
}
}
const indices = writer.indexBuffer();
try expect(indices.len == faces.len * 3);
// Face 0
try expect(indices[0] == 0); // (0, 7) New
try expect(indices[1] == 1); // (4, 5) New
try expect(indices[2] == 2); // (2, 3) New
// Face 1
try expect(indices[3 + 0] == 2); // (2, 3) Duplicate - Reuse index
try expect(indices[3 + 1] == 3); // (3, 7) New
try expect(indices[3 + 2] == 4); // (9, 8) New
// Face 2
try expect(indices[6 + 0] == 4); // (9, 8) Duplicate - Reuse index
try expect(indices[6 + 1] == 5); // (2, 7) New normal mapping (Don't clobber)
try expect(indices[6 + 2] == 1); // (4, 5) Duplicate - Reuse Index
// Face 3
try expect(indices[9 + 0] == 2); // (2, 3) Duplicate - Reuse index
try expect(indices[9 + 1] == 6); // (6, 5) New
try expect(indices[9 + 2] == 7); // (1, 7) New
// Face 4
try expect(indices[12 + 0] == 8); // (9, 5) New normal mapping (Don't clobber)
try expect(indices[12 + 1] == 9); // (6, 7) New normal mapping (Don't clobber)
try expect(indices[12 + 2] == 10); // (0, 8) New normal mapping (Don't clobber)
try expect(writer.vertexBuffer().len == 11);
}

View file

@ -1,923 +0,0 @@
const std = @import("std");
const mach = @import("mach");
const core = mach.core;
const gpu = mach.gpu;
const m3d = @import("model3d");
const zm = @import("zmath");
const assets = @import("assets");
const VertexWriter = @import("vertex_writer.zig").VertexWriter;
pub const App = @This();
const Vec4 = [4]f32;
const Vec3 = [3]f32;
const Vec2 = [2]f32;
const Mat4 = [4]Vec4;
fn Dimensions2D(comptime T: type) type {
return struct {
width: T,
height: T,
};
}
const Vertex = extern struct {
position: Vec3,
normal: Vec3,
};
const Model = struct {
vertex_count: u32,
index_count: u32,
vertex_buffer: *gpu.Buffer,
index_buffer: *gpu.Buffer,
};
const Material = struct {
const Params = extern struct {
roughness: f32,
metallic: f32,
color: Vec3,
};
name: []const u8,
params: Params,
};
const PressedKeys = packed struct(u16) {
right: bool = false,
left: bool = false,
up: bool = false,
down: bool = false,
padding: u12 = undefined,
pub inline fn areKeysPressed(self: @This()) bool {
return (self.up or self.down or self.left or self.right);
}
pub inline fn clear(self: *@This()) void {
self.right = false;
self.left = false;
self.up = false;
self.down = false;
}
};
const Camera = struct {
const Matrices = struct {
perspective: Mat4 = [1]Vec4{[1]f32{0.0} ** 4} ** 4,
view: Mat4 = [1]Vec4{[1]f32{0.0} ** 4} ** 4,
};
rotation: Vec3 = .{ 0.0, 0.0, 0.0 },
position: Vec3 = .{ 0.0, 0.0, 0.0 },
view_position: Vec4 = .{ 0.0, 0.0, 0.0, 0.0 },
fov: f32 = 0.0,
znear: f32 = 0.0,
zfar: f32 = 0.0,
rotation_speed: f32 = 0.0,
movement_speed: f32 = 0.0,
updated: bool = false,
matrices: Matrices = .{},
pub fn calculateMovement(self: *@This(), pressed_keys: PressedKeys) void {
std.debug.assert(pressed_keys.areKeysPressed());
const rotation_radians = Vec3{
toRadians(self.rotation[0]),
toRadians(self.rotation[1]),
toRadians(self.rotation[2]),
};
var camera_front = zm.Vec{ -zm.cos(rotation_radians[0]) * zm.sin(rotation_radians[1]), zm.sin(rotation_radians[0]), zm.cos(rotation_radians[0]) * zm.cos(rotation_radians[1]), 0 };
camera_front = zm.normalize3(camera_front);
if (pressed_keys.up) {
camera_front[0] *= self.movement_speed;
camera_front[1] *= self.movement_speed;
camera_front[2] *= self.movement_speed;
self.position = Vec3{
self.position[0] + camera_front[0],
self.position[1] + camera_front[1],
self.position[2] + camera_front[2],
};
}
if (pressed_keys.down) {
camera_front[0] *= self.movement_speed;
camera_front[1] *= self.movement_speed;
camera_front[2] *= self.movement_speed;
self.position = Vec3{
self.position[0] - camera_front[0],
self.position[1] - camera_front[1],
self.position[2] - camera_front[2],
};
}
if (pressed_keys.right) {
camera_front = zm.cross3(.{ 0.0, 1.0, 0.0, 0.0 }, camera_front);
camera_front = zm.normalize3(camera_front);
camera_front[0] *= self.movement_speed;
camera_front[1] *= self.movement_speed;
camera_front[2] *= self.movement_speed;
self.position = Vec3{
self.position[0] - camera_front[0],
self.position[1] - camera_front[1],
self.position[2] - camera_front[2],
};
}
if (pressed_keys.left) {
camera_front = zm.cross3(.{ 0.0, 1.0, 0.0, 0.0 }, camera_front);
camera_front = zm.normalize3(camera_front);
camera_front[0] *= self.movement_speed;
camera_front[1] *= self.movement_speed;
camera_front[2] *= self.movement_speed;
self.position = Vec3{
self.position[0] + camera_front[0],
self.position[1] + camera_front[1],
self.position[2] + camera_front[2],
};
}
self.updateViewMatrix();
}
fn updateViewMatrix(self: *@This()) void {
const rotation_x = zm.rotationX(toRadians(self.rotation[2]));
const rotation_y = zm.rotationY(toRadians(self.rotation[1]));
const rotation_z = zm.rotationZ(toRadians(self.rotation[0]));
const rotation_matrix = zm.mul(rotation_z, zm.mul(rotation_x, rotation_y));
const translation_matrix: zm.Mat = zm.translationV(.{
self.position[0],
self.position[1],
self.position[2],
0,
});
const view = zm.mul(translation_matrix, rotation_matrix);
self.matrices.view[0] = view[0];
self.matrices.view[1] = view[1];
self.matrices.view[2] = view[2];
self.matrices.view[3] = view[3];
self.view_position = .{
-self.position[0],
self.position[1],
-self.position[2],
0.0,
};
self.updated = true;
}
pub fn setMovementSpeed(self: *@This(), speed: f32) void {
self.movement_speed = speed;
}
pub fn setPerspective(self: *@This(), fov: f32, aspect: f32, znear: f32, zfar: f32) void {
self.fov = fov;
self.znear = znear;
self.zfar = zfar;
const perspective = zm.perspectiveFovRhGl(toRadians(fov), aspect, znear, zfar);
self.matrices.perspective[0] = perspective[0];
self.matrices.perspective[1] = perspective[1];
self.matrices.perspective[2] = perspective[2];
self.matrices.perspective[3] = perspective[3];
}
pub fn setRotationSpeed(self: *@This(), speed: f32) void {
self.rotation_speed = speed;
}
pub fn setRotation(self: *@This(), rotation: Vec3) void {
self.rotation = rotation;
self.updateViewMatrix();
}
pub fn rotate(self: *@This(), delta: Vec2) void {
self.rotation[0] -= delta[1];
self.rotation[1] -= delta[0];
self.updateViewMatrix();
}
pub fn setPosition(self: *@This(), position: Vec3) void {
self.position = .{
position[0],
-position[1],
position[2],
};
self.updateViewMatrix();
}
};
const UniformBuffers = struct {
const Params = struct {
buffer: *gpu.Buffer,
buffer_size: u64,
model_size: u64,
};
const Buffer = struct {
buffer: *gpu.Buffer,
size: u32,
};
ubo_matrices: Buffer,
ubo_params: Buffer,
material_params: Params,
object_params: Params,
};
const UboParams = struct {
lights: [4]Vec4,
};
const UboMatrices = extern struct {
projection: Mat4,
model: Mat4,
view: Mat4,
camera_position: Vec3,
};
const grid_element_count = grid_dimensions * grid_dimensions;
const MaterialParamsDynamic = extern struct {
roughness: f32 = 0,
metallic: f32 = 0,
color: Vec3 = .{ 0, 0, 0 },
padding: [236]u8 = [1]u8{0} ** 236,
};
const MaterialParamsDynamicGrid = [grid_element_count]MaterialParamsDynamic;
const ObjectParamsDynamic = extern struct {
position: Vec3 = .{ 0, 0, 0 },
padding: [244]u8 = [1]u8{0} ** 244,
};
const ObjectParamsDynamicGrid = [grid_element_count]ObjectParamsDynamic;
//
// Globals
//
const material_names = [11][:0]const u8{
"Gold", "Copper", "Chromium", "Nickel", "Titanium", "Cobalt", "Platinum",
// Testing materials
"White", "Red", "Blue", "Black",
};
const object_names = [5][:0]const u8{ "Sphere", "Teapot", "Torusknot", "Venus", "Stanford Dragon" };
const materials = [_]Material{
.{ .name = "Gold", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 1.0, 0.765557, 0.336057 } } },
.{ .name = "Copper", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.955008, 0.637427, 0.538163 } } },
.{ .name = "Chromium", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.549585, 0.556114, 0.554256 } } },
.{ .name = "Nickel", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 1.0, 0.608679, 0.525649 } } },
.{ .name = "Titanium", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.541931, 0.496791, 0.449419 } } },
.{ .name = "Cobalt", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.662124, 0.654864, 0.633732 } } },
.{ .name = "Platinum", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.672411, 0.637331, 0.585456 } } },
// Testing colors
.{ .name = "White", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 1.0, 1.0, 1.0 } } },
.{ .name = "Red", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 1.0, 0.0, 0.0 } } },
.{ .name = "Blue", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.0, 0.0, 1.0 } } },
.{ .name = "Black", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.0, 0.0, 0.0 } } },
};
const grid_dimensions = 7;
const model_embeds = [_][:0]const u8{
assets.sphere_m3d,
assets.teapot_m3d,
assets.torusknot_m3d,
assets.venus_m3d,
assets.stanford_dragon_m3d,
};
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
//
// Member variables
//
title_timer: core.Timer,
timer: core.Timer,
camera: Camera,
render_pipeline: *gpu.RenderPipeline,
render_pass_descriptor: gpu.RenderPassDescriptor,
bind_group: *gpu.BindGroup,
color_attachment: gpu.RenderPassColorAttachment,
depth_stencil_attachment_description: gpu.RenderPassDepthStencilAttachment,
depth_texture: *gpu.Texture,
depth_texture_view: *gpu.TextureView,
pressed_keys: PressedKeys,
models: [5]Model,
ubo_params: UboParams,
ubo_matrices: UboMatrices,
uniform_buffers: UniformBuffers,
material_params_dynamic: MaterialParamsDynamicGrid = [1]MaterialParamsDynamic{.{}} ** grid_element_count,
object_params_dynamic: ObjectParamsDynamicGrid = [1]ObjectParamsDynamic{.{}} ** grid_element_count,
uniform_buffers_dirty: bool,
buffers_bound: bool,
is_paused: bool,
current_material_index: usize,
current_object_index: usize,
mouse_position: core.Position,
is_rotating: bool,
//
// Functions
//
pub fn init(app: *App) !void {
try core.init(.{});
app.timer = try core.Timer.start();
app.title_timer = try core.Timer.start();
app.pressed_keys = .{};
app.buffers_bound = false;
app.is_paused = false;
app.uniform_buffers_dirty = false;
app.current_material_index = 0;
app.current_object_index = 0;
app.mouse_position = .{ .x = 0, .y = 0 };
app.is_rotating = false;
setupCamera(app);
try loadModels(std.heap.c_allocator, app);
prepareUniformBuffers(app);
setupPipeline(app);
setupRenderPass(app);
app.printControls();
}
pub fn deinit(app: *App) void {
defer _ = gpa.deinit();
defer core.deinit();
app.bind_group.release();
app.render_pipeline.release();
app.depth_texture_view.release();
app.depth_texture.release();
app.uniform_buffers.ubo_matrices.buffer.release();
app.uniform_buffers.ubo_params.buffer.release();
app.uniform_buffers.material_params.buffer.release();
app.uniform_buffers.object_params.buffer.release();
}
pub fn update(app: *App) !bool {
var iter = core.pollEvents();
while (iter.next()) |event| {
app.updateUI(event);
switch (event) {
.mouse_motion => |ev| {
if (app.is_rotating) {
const delta = Vec2{
@as(f32, @floatCast((app.mouse_position.x - ev.pos.x) * app.camera.rotation_speed)),
@as(f32, @floatCast((app.mouse_position.y - ev.pos.y) * app.camera.rotation_speed)),
};
app.mouse_position = ev.pos;
app.camera.rotate(delta);
app.uniform_buffers_dirty = true;
}
},
.mouse_press => |ev| {
if (ev.button == .left) {
app.is_rotating = true;
app.mouse_position = ev.pos;
}
},
.mouse_release => |ev| {
if (ev.button == .left) {
app.is_rotating = false;
}
},
.key_press, .key_repeat => |ev| {
const key = ev.key;
if (key == .up or key == .w) app.pressed_keys.up = true;
if (key == .down or key == .s) app.pressed_keys.down = true;
if (key == .left or key == .a) app.pressed_keys.left = true;
if (key == .right or key == .d) app.pressed_keys.right = true;
},
.framebuffer_resize => |ev| {
app.depth_texture_view.release();
app.depth_texture.release();
app.depth_texture = core.device.createTexture(&gpu.Texture.Descriptor{
.usage = .{ .render_attachment = true },
.format = .depth24_plus_stencil8,
.sample_count = 1,
.size = .{
.width = ev.width,
.height = ev.height,
.depth_or_array_layers = 1,
},
});
app.depth_texture_view = app.depth_texture.createView(&gpu.TextureView.Descriptor{
.format = .depth24_plus_stencil8,
.dimension = .dimension_2d,
.array_layer_count = 1,
.aspect = .all,
});
app.depth_stencil_attachment_description = gpu.RenderPassDepthStencilAttachment{
.view = app.depth_texture_view,
.depth_load_op = .clear,
.depth_store_op = .store,
.depth_clear_value = 1.0,
.stencil_clear_value = 0,
.stencil_load_op = .clear,
.stencil_store_op = .store,
};
const aspect_ratio = @as(f32, @floatFromInt(ev.width)) / @as(f32, @floatFromInt(ev.height));
app.camera.setPerspective(60.0, aspect_ratio, 0.1, 256.0);
app.uniform_buffers_dirty = true;
},
.close => return true,
else => {},
}
}
if (app.pressed_keys.areKeysPressed()) {
app.camera.calculateMovement(app.pressed_keys);
app.pressed_keys.clear();
app.uniform_buffers_dirty = true;
}
if (app.uniform_buffers_dirty) {
updateUniformBuffers(app);
app.uniform_buffers_dirty = false;
}
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
app.color_attachment.view = back_buffer_view;
app.render_pass_descriptor = gpu.RenderPassDescriptor{
.color_attachment_count = 1,
.color_attachments = &[_]gpu.RenderPassColorAttachment{app.color_attachment},
.depth_stencil_attachment = &app.depth_stencil_attachment_description,
};
const encoder = core.device.createCommandEncoder(null);
const current_model = app.models[app.current_object_index];
const pass = encoder.beginRenderPass(&app.render_pass_descriptor);
const dimensions = Dimensions2D(f32){
.width = @as(f32, @floatFromInt(core.descriptor.width)),
.height = @as(f32, @floatFromInt(core.descriptor.height)),
};
pass.setViewport(
0,
0,
dimensions.width,
dimensions.height,
0.0,
1.0,
);
pass.setScissorRect(0, 0, core.descriptor.width, core.descriptor.height);
pass.setPipeline(app.render_pipeline);
if (!app.is_paused) {
app.updateLights();
}
var i: usize = 0;
while (i < (grid_dimensions * grid_dimensions)) : (i += 1) {
const alignment = 256;
const dynamic_offset: u32 = @as(u32, @intCast(i)) * alignment;
const dynamic_offsets = [2]u32{ dynamic_offset, dynamic_offset };
pass.setBindGroup(0, app.bind_group, &dynamic_offsets);
if (!app.buffers_bound) {
pass.setVertexBuffer(0, current_model.vertex_buffer, 0, @sizeOf(Vertex) * current_model.vertex_count);
pass.setIndexBuffer(current_model.index_buffer, .uint32, 0, gpu.whole_size);
app.buffers_bound = true;
}
pass.drawIndexed(
current_model.index_count, // index_count
1, // instance_count
0, // first_index
0, // base_vertex
0, // first_instance
);
}
pass.end();
pass.release();
var command = encoder.finish(null);
encoder.release();
const queue = core.queue;
queue.submit(&[_]*gpu.CommandBuffer{command});
command.release();
core.swap_chain.present();
back_buffer_view.release();
app.buffers_bound = false;
// update the window title every second
if (app.title_timer.read() >= 1.0) {
app.title_timer.reset();
try core.printTitle("PBR Basic [ {d}fps ] [ Input {d}hz ]", .{
core.frameRate(),
core.inputRate(),
});
}
return false;
}
fn prepareUniformBuffers(app: *App) void {
comptime {
std.debug.assert(@sizeOf(ObjectParamsDynamic) == 256);
std.debug.assert(@sizeOf(MaterialParamsDynamic) == 256);
}
app.uniform_buffers.ubo_matrices.size = roundToMultipleOf4(u32, @as(u32, @intCast(@sizeOf(UboMatrices)))) + 4;
app.uniform_buffers.ubo_matrices.buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .uniform = true },
.size = app.uniform_buffers.ubo_matrices.size,
.mapped_at_creation = .false,
});
app.uniform_buffers.ubo_params.size = roundToMultipleOf4(u32, @as(u32, @intCast(@sizeOf(UboParams)))) + 4;
app.uniform_buffers.ubo_params.buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .uniform = true },
.size = app.uniform_buffers.ubo_params.size,
.mapped_at_creation = .false,
});
//
// Material parameter uniform buffer
//
app.uniform_buffers.material_params.model_size = @sizeOf(Vec2) + @sizeOf(Vec3);
app.uniform_buffers.material_params.buffer_size = calculateConstantBufferByteSize(@sizeOf(MaterialParamsDynamicGrid));
std.debug.assert(app.uniform_buffers.material_params.buffer_size >= app.uniform_buffers.material_params.model_size);
app.uniform_buffers.material_params.buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .uniform = true },
.size = app.uniform_buffers.material_params.buffer_size,
.mapped_at_creation = .false,
});
//
// Object parameter uniform buffer
//
app.uniform_buffers.object_params.model_size = @sizeOf(Vec3) + 4;
app.uniform_buffers.object_params.buffer_size = calculateConstantBufferByteSize(@sizeOf(MaterialParamsDynamicGrid)) + 4;
std.debug.assert(app.uniform_buffers.object_params.buffer_size >= app.uniform_buffers.object_params.model_size);
app.uniform_buffers.object_params.buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .uniform = true },
.size = app.uniform_buffers.object_params.buffer_size,
.mapped_at_creation = .false,
});
app.updateUniformBuffers();
app.updateDynamicUniformBuffer();
app.updateLights();
}
fn updateDynamicUniformBuffer(app: *App) void {
var index: u32 = 0;
var y: usize = 0;
while (y < grid_dimensions) : (y += 1) {
var x: usize = 0;
while (x < grid_dimensions) : (x += 1) {
const grid_dimensions_float = @as(f32, @floatFromInt(grid_dimensions));
app.object_params_dynamic[index].position[0] = (@as(f32, @floatFromInt(x)) - (grid_dimensions_float / 2) * 2.5);
app.object_params_dynamic[index].position[1] = 0;
app.object_params_dynamic[index].position[2] = (@as(f32, @floatFromInt(y)) - (grid_dimensions_float / 2) * 2.5);
app.material_params_dynamic[index].metallic = zm.clamp(@as(f32, @floatFromInt(x)) / (grid_dimensions_float - 1), 0.1, 1.0);
app.material_params_dynamic[index].roughness = zm.clamp(@as(f32, @floatFromInt(y)) / (grid_dimensions_float - 1), 0.05, 1.0);
app.material_params_dynamic[index].color = materials[app.current_material_index].params.color;
index += 1;
}
}
const queue = core.queue;
queue.writeBuffer(
app.uniform_buffers.object_params.buffer,
0,
&app.object_params_dynamic,
);
queue.writeBuffer(
app.uniform_buffers.material_params.buffer,
0,
&app.material_params_dynamic,
);
}
fn updateUniformBuffers(app: *App) void {
app.ubo_matrices.projection = app.camera.matrices.perspective;
app.ubo_matrices.view = app.camera.matrices.view;
const rotation_degrees = if (app.current_object_index == 1) @as(f32, -45.0) else @as(f32, -90.0);
const model = zm.rotationY(rotation_degrees);
app.ubo_matrices.model[0] = model[0];
app.ubo_matrices.model[1] = model[1];
app.ubo_matrices.model[2] = model[2];
app.ubo_matrices.model[3] = model[3];
app.ubo_matrices.camera_position = .{
-app.camera.position[0],
-app.camera.position[1],
-app.camera.position[2],
};
const queue = core.queue;
queue.writeBuffer(app.uniform_buffers.ubo_matrices.buffer, 0, &[_]UboMatrices{app.ubo_matrices});
}
fn updateLights(app: *App) void {
const p: f32 = 15.0;
app.ubo_params.lights[0] = Vec4{ -p, -p * 0.5, -p, 1.0 };
app.ubo_params.lights[1] = Vec4{ -p, -p * 0.5, p, 1.0 };
app.ubo_params.lights[2] = Vec4{ p, -p * 0.5, p, 1.0 };
app.ubo_params.lights[3] = Vec4{ p, -p * 0.5, -p, 1.0 };
const base_value = toRadians(@mod(app.timer.read() * 0.1, 1.0) * 360.0);
app.ubo_params.lights[0][0] = @sin(base_value) * 20.0;
app.ubo_params.lights[0][2] = @cos(base_value) * 20.0;
app.ubo_params.lights[1][0] = @cos(base_value) * 20.0;
app.ubo_params.lights[1][1] = @sin(base_value) * 20.0;
const queue = core.queue;
queue.writeBuffer(
app.uniform_buffers.ubo_params.buffer,
0,
&[_]UboParams{app.ubo_params},
);
}
fn setupPipeline(app: *App) void {
comptime {
std.debug.assert(@sizeOf(Vertex) == @sizeOf(f32) * 6);
}
const bind_group_layout_entries = [_]gpu.BindGroupLayout.Entry{
.{
.binding = 0,
.visibility = .{ .vertex = true, .fragment = true },
.buffer = .{
.type = .uniform,
.has_dynamic_offset = .false,
.min_binding_size = app.uniform_buffers.ubo_matrices.size,
},
},
.{
.binding = 1,
.visibility = .{ .fragment = true },
.buffer = .{
.type = .uniform,
.has_dynamic_offset = .false,
.min_binding_size = app.uniform_buffers.ubo_params.size,
},
},
.{
.binding = 2,
.visibility = .{ .fragment = true },
.buffer = .{
.type = .uniform,
.has_dynamic_offset = .true,
.min_binding_size = app.uniform_buffers.material_params.model_size,
},
},
.{
.binding = 3,
.visibility = .{ .vertex = true },
.buffer = .{
.type = .uniform,
.has_dynamic_offset = .true,
.min_binding_size = app.uniform_buffers.object_params.model_size,
},
},
};
const bind_group_layout = core.device.createBindGroupLayout(
&gpu.BindGroupLayout.Descriptor.init(.{
.entries = bind_group_layout_entries[0..],
}),
);
const bind_group_layouts = [_]*gpu.BindGroupLayout{bind_group_layout};
const pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{
.bind_group_layouts = &bind_group_layouts,
}));
const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{
.array_stride = @sizeOf(Vertex),
.step_mode = .vertex,
.attributes = &.{
.{ .format = .float32x3, .offset = @offsetOf(Vertex, "position"), .shader_location = 0 },
.{ .format = .float32x3, .offset = @offsetOf(Vertex, "normal"), .shader_location = 1 },
},
});
const blend_component_descriptor = gpu.BlendComponent{
.operation = .add,
.src_factor = .one,
.dst_factor = .zero,
};
const color_target_state = gpu.ColorTargetState{
.format = core.descriptor.format,
.blend = &.{
.color = blend_component_descriptor,
.alpha = blend_component_descriptor,
},
};
const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl"));
const pipeline_descriptor = gpu.RenderPipeline.Descriptor{
.layout = pipeline_layout,
.primitive = .{
.cull_mode = .back,
},
.depth_stencil = &.{
.format = .depth24_plus_stencil8,
.depth_write_enabled = .true,
.depth_compare = .less,
},
.fragment = &gpu.FragmentState.init(.{
.module = shader_module,
.entry_point = "frag_main",
.targets = &.{color_target_state},
}),
.vertex = gpu.VertexState.init(.{
.module = shader_module,
.entry_point = "vertex_main",
.buffers = &.{vertex_buffer_layout},
}),
};
app.render_pipeline = core.device.createRenderPipeline(&pipeline_descriptor);
shader_module.release();
{
const bind_group_entries = [_]gpu.BindGroup.Entry{
.{
.binding = 0,
.buffer = app.uniform_buffers.ubo_matrices.buffer,
.size = app.uniform_buffers.ubo_matrices.size,
},
.{
.binding = 1,
.buffer = app.uniform_buffers.ubo_params.buffer,
.size = app.uniform_buffers.ubo_params.size,
},
.{
.binding = 2,
.buffer = app.uniform_buffers.material_params.buffer,
.size = app.uniform_buffers.material_params.model_size,
},
.{
.binding = 3,
.buffer = app.uniform_buffers.object_params.buffer,
.size = app.uniform_buffers.object_params.model_size,
},
};
app.bind_group = core.device.createBindGroup(
&gpu.BindGroup.Descriptor.init(.{
.layout = bind_group_layout,
.entries = &bind_group_entries,
}),
);
}
}
fn setupRenderPass(app: *App) void {
app.color_attachment = gpu.RenderPassColorAttachment{
.clear_value = .{
.r = 0.0,
.g = 0.0,
.b = 0.0,
.a = 0.0,
},
.load_op = .clear,
.store_op = .store,
};
app.depth_texture = core.device.createTexture(&.{
.usage = .{ .render_attachment = true, .copy_src = true },
.format = .depth24_plus_stencil8,
.sample_count = 1,
.size = .{
.width = core.descriptor.width,
.height = core.descriptor.height,
.depth_or_array_layers = 1,
},
});
app.depth_texture_view = app.depth_texture.createView(&.{
.format = .depth24_plus_stencil8,
.dimension = .dimension_2d,
.array_layer_count = 1,
.aspect = .all,
});
app.depth_stencil_attachment_description = gpu.RenderPassDepthStencilAttachment{
.view = app.depth_texture_view,
.depth_load_op = .clear,
.depth_store_op = .store,
.depth_clear_value = 1.0,
.stencil_clear_value = 0,
.stencil_load_op = .clear,
.stencil_store_op = .store,
};
}
fn loadModels(allocator: std.mem.Allocator, app: *App) !void {
for (model_embeds, 0..) |model_data, model_data_i| {
const m3d_model = m3d.load(model_data, null, null, null) orelse return error.LoadModelFailed;
const vertex_count = m3d_model.handle.numvertex;
const face_count = m3d_model.handle.numface;
var model: *Model = &app.models[model_data_i];
model.index_count = face_count * 3;
var vertex_writer = try VertexWriter(Vertex, u32).init(allocator, face_count * 3, vertex_count, face_count * 3);
defer vertex_writer.deinit(allocator);
const scale: f32 = 0.45;
const vertices = m3d_model.handle.vertex[0..vertex_count];
var i: usize = 0;
while (i < face_count) : (i += 1) {
const face = m3d_model.handle.face[i];
var x: usize = 0;
while (x < 3) : (x += 1) {
const vertex_index = face.vertex[x];
const normal_index = face.normal[x];
const vertex = Vertex{
.position = .{
vertices[vertex_index].x * scale,
vertices[vertex_index].y * scale,
vertices[vertex_index].z * scale,
},
.normal = .{
vertices[normal_index].x,
vertices[normal_index].y,
vertices[normal_index].z,
},
};
vertex_writer.put(vertex, vertex_index);
}
}
const vertex_buffer = vertex_writer.vertexBuffer();
const index_buffer = vertex_writer.indexBuffer();
model.vertex_count = @as(u32, @intCast(vertex_buffer.len));
model.vertex_buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .vertex = true },
.size = @sizeOf(Vertex) * model.vertex_count,
.mapped_at_creation = .false,
});
const queue = core.queue;
queue.writeBuffer(model.vertex_buffer, 0, vertex_buffer);
model.index_buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .index = true },
.size = @sizeOf(u32) * model.index_count,
.mapped_at_creation = .false,
});
queue.writeBuffer(model.index_buffer, 0, index_buffer);
}
}
fn printControls(app: *App) void {
std.debug.print("[controls]\n", .{});
std.debug.print("[p] paused: {}\n", .{app.is_paused});
std.debug.print("[m] material: {s}\n", .{material_names[app.current_material_index]});
std.debug.print("[o] object: {s}\n", .{object_names[app.current_object_index]});
}
fn updateUI(app: *App, event: core.Event) void {
switch (event) {
.key_press => |ev| {
var update_uniform_buffers: bool = false;
switch (ev.key) {
.p => app.is_paused = !app.is_paused,
.m => {
app.current_material_index = (app.current_material_index + 1) % material_names.len;
update_uniform_buffers = true;
},
.o => {
app.current_object_index = (app.current_object_index + 1) % object_names.len;
update_uniform_buffers = true;
},
else => return,
}
app.printControls();
if (update_uniform_buffers) {
updateDynamicUniformBuffer(app);
}
},
else => {},
}
}
fn setupCamera(app: *App) void {
app.camera = Camera{
.rotation_speed = 1.0,
.movement_speed = 1.0,
};
const aspect_ratio: f32 = @as(f32, @floatFromInt(core.descriptor.width)) / @as(f32, @floatFromInt(core.descriptor.height));
app.camera.setPosition(.{ 10.0, 6.0, 6.0 });
app.camera.setRotation(.{ 62.5, 90.0, 0.0 });
app.camera.setMovementSpeed(0.5);
app.camera.setPerspective(60.0, aspect_ratio, 0.1, 256.0);
app.camera.setRotationSpeed(0.25);
}
inline fn roundToMultipleOf4(comptime T: type, value: T) T {
return (value + 3) & ~@as(T, 3);
}
inline fn calculateConstantBufferByteSize(byte_size: usize) usize {
return (byte_size + 255) & ~@as(usize, 255);
}
inline fn toRadians(degrees: f32) f32 {
return degrees * (std.math.pi / 180.0);
}

View file

@ -1,118 +0,0 @@
@group(0) @binding(0) var<uniform> ubo : UBO;
@group(0) @binding(1) var<uniform> uboParams : UBOShared;
@group(0) @binding(2) var<uniform> material : MaterialParams;
@group(0) @binding(3) var<uniform> object : ObjectParams;
struct VertexOut {
@builtin(position) position_clip : vec4<f32>,
@location(0) fragPosition : vec3<f32>,
@location(1) fragNormal : vec3<f32>,
}
struct MaterialParams {
roughness : f32,
metallic : f32,
r : f32,
g : f32,
b : f32
}
struct UBOShared {
lights : array<vec4<f32>, 4>,
}
struct UBO {
projection : mat4x4<f32>,
model : mat4x4<f32>,
view : mat4x4<f32>,
camPos : vec3<f32>,
}
struct ObjectParams {
position : vec3<f32>
}
@vertex fn vertex_main(
@location(0) position : vec3<f32>,
@location(1) normal : vec3<f32>
) -> VertexOut {
var output : VertexOut;
var locPos = vec4<f32>(ubo.model * vec4<f32>(position, 1.0));
output.fragPosition = locPos.xyz + object.position;
output.fragNormal = mat3x3<f32>(ubo.model[0].xyz, ubo.model[1].xyz, ubo.model[2].xyz) * normal;
output.position_clip = ubo.projection * ubo.view * vec4<f32>(output.fragPosition, 1.0);
return output;
}
@fragment fn frag_main(
@location(0) position : vec3<f32>,
@location(1) normal: vec3<f32>
) -> @location(0) vec4<f32> {
var N : vec3<f32> = normalize(normal);
var V : vec3<f32> = normalize(ubo.camPos - position);
var Lo = vec3<f32>(0.0);
// Specular contribution
for(var i: i32 = 0; i < 4; i++) {
var L : vec3<f32> = normalize(uboParams.lights[i].xyz - position);
Lo += BRDF(L, V, N, material.metallic, material.roughness);
}
// Combine with ambient
var color : vec3<f32> = material_color() * 0.02;
color += Lo;
// Gamma correct
color = pow(color, vec3<f32>(0.4545));
return vec4<f32>(color, 1.0);
}
const PI : f32 = 3.14159265359;
fn material_color() -> vec3<f32> {
return vec3<f32>(material.r, material.g, material.b);
}
// Normal Distribution function --------------------------------------
fn D_GGX(dotNH : f32, roughness : f32) -> f32 {
var alpha : f32 = roughness * roughness;
var alpha2 : f32 = alpha * alpha;
var denom : f32 = dotNH * dotNH * (alpha2 - 1.0) + 1.0;
return alpha2 / (PI * denom * denom);
}
// Geometric Shadowing function --------------------------------------
fn G_SchlicksmithGGX(dotNL : f32, dotNV : f32, roughness : f32) -> f32 {
var r : f32 = roughness + 1.0;
var k : f32 = (r * r) / 8.0;
var GL : f32 = dotNL / (dotNL * (1.0 - k) + k);
var GV : f32 = dotNV / (dotNV * (1.0 - k) + k);
return GL * GV;
}
// Fresnel function ----------------------------------------------------
fn F_Schlick(cosTheta : f32, metallic : f32) -> vec3<f32> {
var F0 : vec3<f32> = mix(vec3<f32>(0.04), material_color(), metallic);
var F : vec3<f32> = F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
return F;
}
// Specular BRDF composition --------------------------------------------
fn BRDF(L : vec3<f32>, V : vec3<f32>, N : vec3<f32>, metallic : f32, roughness : f32) -> vec3<f32> {
var H : vec3<f32> = normalize(V + L);
var dotNV : f32 = clamp(dot(N, V), 0.0, 1.0);
var dotNL : f32 = clamp(dot(N, L), 0.0, 1.0);
var dotLH : f32 = clamp(dot(L, H), 0.0, 1.0);
var dotNH : f32 = clamp(dot(N, H), 0.0, 1.0);
var lightColor = vec3<f32>(1.0);
var color = vec3<f32>(0.0);
if(dotNL > 0.0) {
var rroughness : f32 = max(0.05, roughness);
// D = Normal distribution (Distribution of the microfacets)
var D : f32 = D_GGX(dotNH, roughness);
// G = Geometric shadowing term (Microfacets shadowing)
var G : f32 = G_SchlicksmithGGX(dotNL, dotNV, roughness);
// F = Fresnel factor (Reflectance depending on angle of incidence)
var F : vec3<f32> = F_Schlick(dotNV, metallic);
var spec : vec3<f32> = (D * F * G) / (4.0 * dotNL * dotNV);
color += spec * dotNL * lightColor;
}
return color;
}

View file

@ -1,188 +0,0 @@
const std = @import("std");
/// Vertex writer manages the placement of vertices by tracking which are unique. If a duplicate vertex is added
/// with `put`, only it's index will be written to the index buffer.
/// `IndexType` should match the integer type used for the index buffer
pub fn VertexWriter(comptime VertexType: type, comptime IndexType: type) type {
return struct {
const MapEntry = struct {
packed_index: IndexType = null_index,
next_sparse: IndexType = null_index,
};
const null_index: IndexType = std.math.maxInt(IndexType);
vertices: []VertexType,
indices: []IndexType,
sparse_to_packed_map: []MapEntry,
/// Next index outside of the 1:1 mapping range for storing
/// position -> normal collisions
next_collision_index: IndexType,
/// Next packed index
next_packed_index: IndexType,
written_indices_count: IndexType,
/// Allocate storage and set default values
/// `sparse_vertices_count` is the number of vertices in the source before de-duplication / remapping
/// Put more succinctly, the largest index value in source index buffer
/// `max_vertex_count` is largest permutation of vertices assuming that {vertex, uv, normal} never map 1:1 and always
/// create a new mapping
pub fn init(
allocator: std.mem.Allocator,
indices_count: IndexType,
sparse_vertices_count: IndexType,
max_vertex_count: IndexType,
) !@This() {
var result: @This() = undefined;
result.vertices = try allocator.alloc(VertexType, max_vertex_count);
result.indices = try allocator.alloc(IndexType, indices_count);
result.sparse_to_packed_map = try allocator.alloc(MapEntry, max_vertex_count);
result.next_collision_index = sparse_vertices_count;
result.next_packed_index = 0;
result.written_indices_count = 0;
@memset(result.sparse_to_packed_map, .{});
return result;
}
pub fn put(self: *@This(), vertex: VertexType, sparse_index: IndexType) void {
if (self.sparse_to_packed_map[sparse_index].packed_index == null_index) {
// New start of chain, reserve a new packed index and add entry to `index_map`
const packed_index = self.next_packed_index;
self.sparse_to_packed_map[sparse_index].packed_index = packed_index;
self.vertices[packed_index] = vertex;
self.indices[self.written_indices_count] = packed_index;
self.written_indices_count += 1;
self.next_packed_index += 1;
return;
}
var previous_sparse_index: IndexType = undefined;
var current_sparse_index = sparse_index;
while (current_sparse_index != null_index) {
const packed_index = self.sparse_to_packed_map[current_sparse_index].packed_index;
if (std.mem.eql(u8, &std.mem.toBytes(self.vertices[packed_index]), &std.mem.toBytes(vertex))) {
// We already have a record for this vertex in our chain
self.indices[self.written_indices_count] = packed_index;
self.written_indices_count += 1;
return;
}
previous_sparse_index = current_sparse_index;
current_sparse_index = self.sparse_to_packed_map[current_sparse_index].next_sparse;
}
// This is a new mapping for the given sparse index
const packed_index = self.next_packed_index;
const remapped_sparse_index = self.next_collision_index;
self.indices[self.written_indices_count] = packed_index;
self.vertices[packed_index] = vertex;
self.sparse_to_packed_map[previous_sparse_index].next_sparse = remapped_sparse_index;
self.sparse_to_packed_map[remapped_sparse_index].packed_index = packed_index;
self.next_packed_index += 1;
self.next_collision_index += 1;
self.written_indices_count += 1;
}
pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void {
allocator.free(self.vertices);
allocator.free(self.indices);
allocator.free(self.sparse_to_packed_map);
}
pub fn indexBuffer(self: @This()) []IndexType {
return self.indices;
}
pub fn vertexBuffer(self: @This()) []VertexType {
return self.vertices[0..self.next_packed_index];
}
};
}
test "VertexWriter" {
const Vec3 = [3]f32;
const Vertex = extern struct {
position: Vec3,
normal: Vec3,
};
const expect = std.testing.expect;
const allocator = std.testing.allocator;
const Face = struct {
position: [3]u16,
normal: [3]u16,
};
const vertices = [_]Vec3{
Vec3{ 1.0, 0.0, 0.0 }, // 0: Position
Vec3{ 2.0, 0.0, 0.0 }, // 1: Position
Vec3{ 3.0, 0.0, 0.0 }, // 2: Position
Vec3{ 1.0, 0.0, 0.0 }, // 3: Normal
Vec3{ 4.0, 0.0, 0.0 }, // 4: Position
Vec3{ 0.0, 1.0, 0.0 }, // 5: Normal
Vec3{ 5.0, 0.0, 0.0 }, // 6: Position
Vec3{ 0.0, 0.0, 1.0 }, // 7: Normal
Vec3{ 1.0, 0.0, 1.0 }, // 8: Normal
Vec3{ 6.0, 0.0, 0.0 }, // 9: Position
};
const faces = [_]Face{
.{ .position = .{ 0, 4, 2 }, .normal = .{ 7, 5, 3 } },
.{ .position = .{ 2, 3, 9 }, .normal = .{ 3, 7, 8 } },
.{ .position = .{ 9, 2, 4 }, .normal = .{ 8, 7, 5 } },
.{ .position = .{ 2, 6, 1 }, .normal = .{ 3, 5, 7 } },
.{ .position = .{ 9, 6, 0 }, .normal = .{ 5, 7, 8 } },
};
var writer = try VertexWriter(Vertex, u32).init(
allocator,
faces.len * 3, // indices count
vertices.len, // original vertices count
faces.len * 3, // maximum vertices count
);
defer writer.deinit(allocator);
for (faces) |face| {
var x: usize = 0;
while (x < 3) : (x += 1) {
const position_index = face.position[x];
const position = vertices[position_index];
const normal = vertices[face.normal[x]];
const vertex = Vertex{
.position = position,
.normal = normal,
};
writer.put(vertex, position_index);
}
}
const indices = writer.indexBuffer();
try expect(indices.len == faces.len * 3);
// Face 0
try expect(indices[0] == 0); // (0, 7) New
try expect(indices[1] == 1); // (4, 5) New
try expect(indices[2] == 2); // (2, 3) New
// Face 1
try expect(indices[3 + 0] == 2); // (2, 3) Duplicate - Reuse index
try expect(indices[3 + 1] == 3); // (3, 7) New
try expect(indices[3 + 2] == 4); // (9, 8) New
// Face 2
try expect(indices[6 + 0] == 4); // (9, 8) Duplicate - Reuse index
try expect(indices[6 + 1] == 5); // (2, 7) New normal mapping (Don't clobber)
try expect(indices[6 + 2] == 1); // (4, 5) Duplicate - Reuse Index
// Face 3
try expect(indices[9 + 0] == 2); // (2, 3) Duplicate - Reuse index
try expect(indices[9 + 1] == 6); // (6, 5) New
try expect(indices[9 + 2] == 7); // (1, 7) New
// Face 4
try expect(indices[12 + 0] == 8); // (9, 5) New normal mapping (Don't clobber)
try expect(indices[12 + 1] == 9); // (6, 7) New normal mapping (Don't clobber)
try expect(indices[12 + 2] == 10); // (0, 8) New normal mapping (Don't clobber)
try expect(writer.vertexBuffer().len == 11);
}

View file

@ -1,89 +0,0 @@
@group(0) @binding(0) var gBufferNormal: texture_2d<f32>;
@group(0) @binding(1) var gBufferAlbedo: texture_2d<f32>;
@group(0) @binding(2) var gBufferDepth: texture_depth_2d;
struct LightData {
position : vec4<f32>,
// TODO - vec3 alignment
color_x : f32,
color_y : f32,
color_z : f32,
radius : f32,
}
struct LightsBuffer {
lights: array<LightData>,
}
@group(1) @binding(0) var<storage, read> lightsBuffer: LightsBuffer;
struct Config {
numLights : u32,
}
struct Camera {
viewProjectionMatrix : mat4x4<f32>,
invViewProjectionMatrix : mat4x4<f32>,
}
@group(1) @binding(1) var<uniform> config: Config;
@group(1) @binding(2) var<uniform> camera: Camera;
fn world_from_screen_coord(coord : vec2<f32>, depth_sample: f32) -> vec3<f32> {
// reconstruct world-space position from the screen coordinate.
let posClip = vec4(coord.x * 2.0 - 1.0, (1.0 - coord.y) * 2.0 - 1.0, depth_sample, 1.0);
let posWorldW = camera.invViewProjectionMatrix * posClip;
let posWorld = posWorldW.xyz / posWorldW.www;
return posWorld;
}
@fragment
fn main(
@builtin(position) coord : vec4<f32>
) -> @location(0) vec4<f32> {
// TODO - variable initialization
var result = vec3<f32>(0, 0, 0);
let depth = textureLoad(
gBufferDepth,
vec2<i32>(floor(coord.xy)),
0
);
// Don't light the sky.
if (depth >= 1.0) {
discard;
}
let bufferSize = textureDimensions(gBufferDepth);
let coordUV = coord.xy / vec2<f32>(bufferSize);
let position = world_from_screen_coord(coordUV, depth);
let normal = textureLoad(
gBufferNormal,
vec2<i32>(floor(coord.xy)),
0
).xyz;
let albedo = textureLoad(
gBufferAlbedo,
vec2<i32>(floor(coord.xy)),
0
).rgb;
for (var i = 0u; i < config.numLights; i++) {
// TODO - vec3 alignment
let lightColor = vec3<f32>(lightsBuffer.lights[i].color_x, lightsBuffer.lights[i].color_y, lightsBuffer.lights[i].color_z);
let L = lightsBuffer.lights[i].position.xyz - position;
let distance = length(L);
if (distance > lightsBuffer.lights[i].radius) {
continue;
}
let lambert = max(dot(normal, normalize(L)), 0.0);
result += vec3<f32>(
lambert * pow(1.0 - distance / lightsBuffer.lights[i].radius, 2.0) * lightColor * albedo
);
}
// some manual ambient
result += vec3(0.2);
return vec4(result, 1.0);
}

View file

@ -1,44 +0,0 @@
@group(0) @binding(0) var gBufferNormal: texture_2d<f32>;
@group(0) @binding(1) var gBufferAlbedo: texture_2d<f32>;
@group(0) @binding(2) var gBufferDepth: texture_depth_2d;
@group(1) @binding(0) var<uniform> canvas : CanvasConstants;
struct CanvasConstants {
size: vec2<f32>,
}
@fragment
fn main(
@builtin(position) coord : vec4<f32>
) -> @location(0) vec4<f32> {
var result : vec4<f32>;
let c = coord.xy / vec2<f32>(canvas.size.x, canvas.size.y);
if (c.x < 0.33333) {
let rawDepth = textureLoad(
gBufferDepth,
vec2<i32>(floor(coord.xy)),
0
);
// remap depth into something a bit more visible
let depth = (1.0 - rawDepth) * 50.0;
result = vec4(depth);
} else if (c.x < 0.66667) {
result = textureLoad(
gBufferNormal,
vec2<i32>(floor(coord.xy)),
0
);
result.x = (result.x + 1.0) * 0.5;
result.y = (result.y + 1.0) * 0.5;
result.z = (result.z + 1.0) * 0.5;
} else {
result = textureLoad(
gBufferAlbedo,
vec2<i32>(floor(coord.xy)),
0
);
}
return result;
}

View file

@ -1,22 +0,0 @@
struct GBufferOutput {
@location(0) normal : vec4<f32>,
// Textures: diffuse color, specular color, smoothness, emissive etc. could go here
@location(1) albedo : vec4<f32>,
}
@fragment
fn main(
@location(0) fragNormal: vec3<f32>,
@location(1) fragUV : vec2<f32>
) -> GBufferOutput {
// faking some kind of checkerboard texture
let uv = floor(30.0 * fragUV);
let c = 0.2 + 0.5 * ((uv.x + uv.y) - 2.0 * floor((uv.x + uv.y) / 2.0));
var output : GBufferOutput;
output.normal = vec4(fragNormal, 1.0);
output.albedo = vec4(c, c, c, 1.0);
return output;
}

View file

@ -1,37 +0,0 @@
struct LightData {
position : vec4<f32>,
// TODO - vec3 alignment
color_x : f32,
color_y : f32,
color_z : f32,
radius : f32,
}
struct LightsBuffer {
lights: array<LightData>,
}
@group(0) @binding(0) var<storage, read_write> lightsBuffer: LightsBuffer;
struct Config {
numLights : u32,
}
@group(0) @binding(1) var<uniform> config: Config;
struct LightExtent {
min : vec4<f32>,
max : vec4<f32>,
}
@group(0) @binding(2) var<uniform> lightExtent: LightExtent;
@compute @workgroup_size(64, 1, 1)
fn main(@builtin(global_invocation_id) GlobalInvocationID : vec3<u32>) {
var index = GlobalInvocationID.x;
if (index >= config.numLights) {
return;
}
lightsBuffer.lights[index].position.y = lightsBuffer.lights[index].position.y - 0.5 - 0.003 * (f32(index) - 64.0 * floor(f32(index) / 64.0));
if (lightsBuffer.lights[index].position.y < lightExtent.min.y) {
lightsBuffer.lights[index].position.y = lightExtent.max.y;
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,12 +0,0 @@
@vertex
fn main(
@builtin(vertex_index) VertexIndex : u32
) -> @builtin(position) vec4<f32> {
// TODO - array initialization
var pos = array<vec2<f32>, 6>(
vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0),
vec2(-1.0, 1.0), vec2(1.0, -1.0), vec2(1.0, 1.0),
);
return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
}

View file

@ -1,30 +0,0 @@
struct Uniforms {
modelMatrix : mat4x4<f32>,
normalModelMatrix : mat4x4<f32>,
}
struct Camera {
viewProjectionMatrix : mat4x4<f32>,
invViewProjectionMatrix : mat4x4<f32>,
}
@group(0) @binding(0) var<uniform> uniforms : Uniforms;
@group(0) @binding(1) var<uniform> camera : Camera;
struct VertexOutput {
@builtin(position) Position : vec4<f32>,
@location(0) fragNormal: vec3<f32>, // normal in world space
@location(1) fragUV: vec2<f32>,
}
@vertex
fn main(
@location(0) position : vec3<f32>,
@location(1) normal : vec3<f32>,
@location(2) uv : vec2<f32>
) -> VertexOutput {
var output : VertexOutput;
let worldPosition = (uniforms.modelMatrix * vec4(position, 1.0)).xyz;
output.Position = camera.viewProjectionMatrix * vec4(worldPosition, 1.0);
output.fragNormal = normalize((uniforms.normalModelMatrix * vec4(normal, 1.0)).xyz);
output.fragUV = uv;
return output;
}

View file

@ -1,188 +0,0 @@
const std = @import("std");
/// Vertex writer manages the placement of vertices by tracking which are unique. If a duplicate vertex is added
/// with `put`, only it's index will be written to the index buffer.
/// `IndexType` should match the integer type used for the index buffer
pub fn VertexWriter(comptime VertexType: type, comptime IndexType: type) type {
return struct {
const MapEntry = struct {
packed_index: IndexType = null_index,
next_sparse: IndexType = null_index,
};
const null_index: IndexType = std.math.maxInt(IndexType);
vertices: []VertexType,
indices: []IndexType,
sparse_to_packed_map: []MapEntry,
/// Next index outside of the 1:1 mapping range for storing
/// position -> normal collisions
next_collision_index: IndexType,
/// Next packed index
next_packed_index: IndexType,
written_indices_count: IndexType,
/// Allocate storage and set default values
/// `sparse_vertices_count` is the number of vertices in the source before de-duplication / remapping
/// Put more succinctly, the largest index value in source index buffer
/// `max_vertex_count` is largest permutation of vertices assuming that {vertex, uv, normal} never map 1:1 and always
/// create a new mapping
pub fn init(
allocator: std.mem.Allocator,
indices_count: IndexType,
sparse_vertices_count: IndexType,
max_vertex_count: IndexType,
) !@This() {
var result: @This() = undefined;
result.vertices = try allocator.alloc(VertexType, max_vertex_count);
result.indices = try allocator.alloc(IndexType, indices_count);
result.sparse_to_packed_map = try allocator.alloc(MapEntry, max_vertex_count);
result.next_collision_index = sparse_vertices_count;
result.next_packed_index = 0;
result.written_indices_count = 0;
@memset(result.sparse_to_packed_map, .{});
return result;
}
pub fn put(self: *@This(), vertex: VertexType, sparse_index: IndexType) void {
if (self.sparse_to_packed_map[sparse_index].packed_index == null_index) {
// New start of chain, reserve a new packed index and add entry to `index_map`
const packed_index = self.next_packed_index;
self.sparse_to_packed_map[sparse_index].packed_index = packed_index;
self.vertices[packed_index] = vertex;
self.indices[self.written_indices_count] = packed_index;
self.written_indices_count += 1;
self.next_packed_index += 1;
return;
}
var previous_sparse_index: IndexType = undefined;
var current_sparse_index = sparse_index;
while (current_sparse_index != null_index) {
const packed_index = self.sparse_to_packed_map[current_sparse_index].packed_index;
if (std.mem.eql(u8, &std.mem.toBytes(self.vertices[packed_index]), &std.mem.toBytes(vertex))) {
// We already have a record for this vertex in our chain
self.indices[self.written_indices_count] = packed_index;
self.written_indices_count += 1;
return;
}
previous_sparse_index = current_sparse_index;
current_sparse_index = self.sparse_to_packed_map[current_sparse_index].next_sparse;
}
// This is a new mapping for the given sparse index
const packed_index = self.next_packed_index;
const remapped_sparse_index = self.next_collision_index;
self.indices[self.written_indices_count] = packed_index;
self.vertices[packed_index] = vertex;
self.sparse_to_packed_map[previous_sparse_index].next_sparse = remapped_sparse_index;
self.sparse_to_packed_map[remapped_sparse_index].packed_index = packed_index;
self.next_packed_index += 1;
self.next_collision_index += 1;
self.written_indices_count += 1;
}
pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void {
allocator.free(self.vertices);
allocator.free(self.indices);
allocator.free(self.sparse_to_packed_map);
}
pub fn indexBuffer(self: @This()) []IndexType {
return self.indices;
}
pub fn vertexBuffer(self: @This()) []VertexType {
return self.vertices[0..self.next_packed_index];
}
};
}
test "VertexWriter" {
const Vec3 = [3]f32;
const Vertex = extern struct {
position: Vec3,
normal: Vec3,
};
const expect = std.testing.expect;
const allocator = std.testing.allocator;
const Face = struct {
position: [3]u16,
normal: [3]u16,
};
const vertices = [_]Vec3{
Vec3{ 1.0, 0.0, 0.0 }, // 0: Position
Vec3{ 2.0, 0.0, 0.0 }, // 1: Position
Vec3{ 3.0, 0.0, 0.0 }, // 2: Position
Vec3{ 1.0, 0.0, 0.0 }, // 3: Normal
Vec3{ 4.0, 0.0, 0.0 }, // 4: Position
Vec3{ 0.0, 1.0, 0.0 }, // 5: Normal
Vec3{ 5.0, 0.0, 0.0 }, // 6: Position
Vec3{ 0.0, 0.0, 1.0 }, // 7: Normal
Vec3{ 1.0, 0.0, 1.0 }, // 8: Normal
Vec3{ 6.0, 0.0, 0.0 }, // 9: Position
};
const faces = [_]Face{
.{ .position = .{ 0, 4, 2 }, .normal = .{ 7, 5, 3 } },
.{ .position = .{ 2, 3, 9 }, .normal = .{ 3, 7, 8 } },
.{ .position = .{ 9, 2, 4 }, .normal = .{ 8, 7, 5 } },
.{ .position = .{ 2, 6, 1 }, .normal = .{ 3, 5, 7 } },
.{ .position = .{ 9, 6, 0 }, .normal = .{ 5, 7, 8 } },
};
var writer = try VertexWriter(Vertex, u32).init(
allocator,
faces.len * 3, // indices count
vertices.len, // original vertices count
faces.len * 3, // maximum vertices count
);
defer writer.deinit(allocator);
for (faces) |face| {
var x: usize = 0;
while (x < 3) : (x += 1) {
const position_index = face.position[x];
const position = vertices[position_index];
const normal = vertices[face.normal[x]];
const vertex = Vertex{
.position = position,
.normal = normal,
};
writer.put(vertex, position_index);
}
}
const indices = writer.indexBuffer();
try expect(indices.len == faces.len * 3);
// Face 0
try expect(indices[0] == 0); // (0, 7) New
try expect(indices[1] == 1); // (4, 5) New
try expect(indices[2] == 2); // (2, 3) New
// Face 1
try expect(indices[3 + 0] == 2); // (2, 3) Duplicate - Reuse index
try expect(indices[3 + 1] == 3); // (3, 7) New
try expect(indices[3 + 2] == 4); // (9, 8) New
// Face 2
try expect(indices[6 + 0] == 4); // (9, 8) Duplicate - Reuse index
try expect(indices[6 + 1] == 5); // (2, 7) New normal mapping (Don't clobber)
try expect(indices[6 + 2] == 1); // (4, 5) Duplicate - Reuse Index
// Face 3
try expect(indices[9 + 0] == 2); // (2, 3) Duplicate - Reuse index
try expect(indices[9 + 1] == 6); // (6, 5) New
try expect(indices[9 + 2] == 7); // (1, 7) New
// Face 4
try expect(indices[12 + 0] == 8); // (9, 5) New normal mapping (Don't clobber)
try expect(indices[12 + 1] == 9); // (6, 7) New normal mapping (Don't clobber)
try expect(indices[12 + 2] == 10); // (0, 8) New normal mapping (Don't clobber)
try expect(writer.vertexBuffer().len == 11);
}

View file

@ -1,932 +0,0 @@
const std = @import("std");
const mach = @import("mach");
const core = mach.core;
const gpu = mach.gpu;
const m3d = @import("model3d");
const zm = @import("zmath");
const assets = @import("assets");
const VertexWriter = @import("vertex_writer.zig").VertexWriter;
pub const App = @This();
// Use experimental sysgpu graphics API
pub const use_sysgpu = true;
const Vec4 = [4]f32;
const Vec3 = [3]f32;
const Vec2 = [2]f32;
const Mat4 = [4]Vec4;
fn Dimensions2D(comptime T: type) type {
return struct {
width: T,
height: T,
};
}
const Vertex = extern struct {
position: Vec3,
normal: Vec3,
};
const Model = struct {
vertex_count: u32,
index_count: u32,
vertex_buffer: *gpu.Buffer,
index_buffer: *gpu.Buffer,
};
const Material = struct {
const Params = extern struct {
roughness: f32,
metallic: f32,
color: Vec3,
};
name: []const u8,
params: Params,
};
const PressedKeys = packed struct(u16) {
right: bool = false,
left: bool = false,
up: bool = false,
down: bool = false,
padding: u12 = undefined,
pub inline fn areKeysPressed(self: @This()) bool {
return (self.up or self.down or self.left or self.right);
}
pub inline fn clear(self: *@This()) void {
self.right = false;
self.left = false;
self.up = false;
self.down = false;
}
};
const Camera = struct {
const Matrices = struct {
perspective: Mat4 = [1]Vec4{[1]f32{0.0} ** 4} ** 4,
view: Mat4 = [1]Vec4{[1]f32{0.0} ** 4} ** 4,
};
rotation: Vec3 = .{ 0.0, 0.0, 0.0 },
position: Vec3 = .{ 0.0, 0.0, 0.0 },
view_position: Vec4 = .{ 0.0, 0.0, 0.0, 0.0 },
fov: f32 = 0.0,
znear: f32 = 0.0,
zfar: f32 = 0.0,
rotation_speed: f32 = 0.0,
movement_speed: f32 = 0.0,
updated: bool = false,
matrices: Matrices = .{},
pub fn calculateMovement(self: *@This(), pressed_keys: PressedKeys) void {
std.debug.assert(pressed_keys.areKeysPressed());
const rotation_radians = Vec3{
toRadians(self.rotation[0]),
toRadians(self.rotation[1]),
toRadians(self.rotation[2]),
};
var camera_front = zm.Vec{ -zm.cos(rotation_radians[0]) * zm.sin(rotation_radians[1]), zm.sin(rotation_radians[0]), zm.cos(rotation_radians[0]) * zm.cos(rotation_radians[1]), 0 };
camera_front = zm.normalize3(camera_front);
if (pressed_keys.up) {
camera_front[0] *= self.movement_speed;
camera_front[1] *= self.movement_speed;
camera_front[2] *= self.movement_speed;
self.position = Vec3{
self.position[0] + camera_front[0],
self.position[1] + camera_front[1],
self.position[2] + camera_front[2],
};
}
if (pressed_keys.down) {
camera_front[0] *= self.movement_speed;
camera_front[1] *= self.movement_speed;
camera_front[2] *= self.movement_speed;
self.position = Vec3{
self.position[0] - camera_front[0],
self.position[1] - camera_front[1],
self.position[2] - camera_front[2],
};
}
if (pressed_keys.right) {
camera_front = zm.cross3(.{ 0.0, 1.0, 0.0, 0.0 }, camera_front);
camera_front = zm.normalize3(camera_front);
camera_front[0] *= self.movement_speed;
camera_front[1] *= self.movement_speed;
camera_front[2] *= self.movement_speed;
self.position = Vec3{
self.position[0] - camera_front[0],
self.position[1] - camera_front[1],
self.position[2] - camera_front[2],
};
}
if (pressed_keys.left) {
camera_front = zm.cross3(.{ 0.0, 1.0, 0.0, 0.0 }, camera_front);
camera_front = zm.normalize3(camera_front);
camera_front[0] *= self.movement_speed;
camera_front[1] *= self.movement_speed;
camera_front[2] *= self.movement_speed;
self.position = Vec3{
self.position[0] + camera_front[0],
self.position[1] + camera_front[1],
self.position[2] + camera_front[2],
};
}
self.updateViewMatrix();
}
fn updateViewMatrix(self: *@This()) void {
const rotation_x = zm.rotationX(toRadians(self.rotation[2]));
const rotation_y = zm.rotationY(toRadians(self.rotation[1]));
const rotation_z = zm.rotationZ(toRadians(self.rotation[0]));
const rotation_matrix = zm.mul(rotation_z, zm.mul(rotation_x, rotation_y));
const translation_matrix: zm.Mat = zm.translationV(.{
self.position[0],
self.position[1],
self.position[2],
0,
});
const view = zm.mul(translation_matrix, rotation_matrix);
self.matrices.view[0] = view[0];
self.matrices.view[1] = view[1];
self.matrices.view[2] = view[2];
self.matrices.view[3] = view[3];
self.view_position = .{
-self.position[0],
self.position[1],
-self.position[2],
0.0,
};
self.updated = true;
}
pub fn setMovementSpeed(self: *@This(), speed: f32) void {
self.movement_speed = speed;
}
pub fn setPerspective(self: *@This(), fov: f32, aspect: f32, znear: f32, zfar: f32) void {
self.fov = fov;
self.znear = znear;
self.zfar = zfar;
const perspective = zm.perspectiveFovRhGl(toRadians(fov), aspect, znear, zfar);
self.matrices.perspective[0] = perspective[0];
self.matrices.perspective[1] = perspective[1];
self.matrices.perspective[2] = perspective[2];
self.matrices.perspective[3] = perspective[3];
}
pub fn setRotationSpeed(self: *@This(), speed: f32) void {
self.rotation_speed = speed;
}
pub fn setRotation(self: *@This(), rotation: Vec3) void {
self.rotation = rotation;
self.updateViewMatrix();
}
pub fn rotate(self: *@This(), delta: Vec2) void {
self.rotation[0] -= delta[1];
self.rotation[1] -= delta[0];
self.updateViewMatrix();
}
pub fn setPosition(self: *@This(), position: Vec3) void {
self.position = .{
position[0],
-position[1],
position[2],
};
self.updateViewMatrix();
}
};
const UniformBuffers = struct {
const Params = struct {
buffer: *gpu.Buffer,
buffer_size: u64,
model_size: u64,
};
const Buffer = struct {
buffer: *gpu.Buffer,
size: u32,
};
ubo_matrices: Buffer,
ubo_params: Buffer,
material_params: Params,
object_params: Params,
};
const UboParams = struct {
lights: [4]Vec4,
};
const UboMatrices = extern struct {
projection: Mat4,
model: Mat4,
view: Mat4,
camera_position: Vec3,
};
const grid_element_count = grid_dimensions * grid_dimensions;
const MaterialParamsDynamic = extern struct {
roughness: f32 = 0,
metallic: f32 = 0,
color: Vec3 = .{ 0, 0, 0 },
padding: [236]u8 = [1]u8{0} ** 236,
};
const MaterialParamsDynamicGrid = [grid_element_count]MaterialParamsDynamic;
const ObjectParamsDynamic = extern struct {
position: Vec3 = .{ 0, 0, 0 },
padding: [244]u8 = [1]u8{0} ** 244,
};
const ObjectParamsDynamicGrid = [grid_element_count]ObjectParamsDynamic;
//
// Globals
//
const material_names = [11][:0]const u8{
"Gold", "Copper", "Chromium", "Nickel", "Titanium", "Cobalt", "Platinum",
// Testing materials
"White", "Red", "Blue", "Black",
};
const object_names = [5][:0]const u8{ "Sphere", "Teapot", "Torusknot", "Venus", "Stanford Dragon" };
const materials = [_]Material{
.{ .name = "Gold", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 1.0, 0.765557, 0.336057 } } },
.{ .name = "Copper", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.955008, 0.637427, 0.538163 } } },
.{ .name = "Chromium", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.549585, 0.556114, 0.554256 } } },
.{ .name = "Nickel", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 1.0, 0.608679, 0.525649 } } },
.{ .name = "Titanium", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.541931, 0.496791, 0.449419 } } },
.{ .name = "Cobalt", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.662124, 0.654864, 0.633732 } } },
.{ .name = "Platinum", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.672411, 0.637331, 0.585456 } } },
// Testing colors
.{ .name = "White", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 1.0, 1.0, 1.0 } } },
.{ .name = "Red", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 1.0, 0.0, 0.0 } } },
.{ .name = "Blue", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.0, 0.0, 1.0 } } },
.{ .name = "Black", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.0, 0.0, 0.0 } } },
};
const grid_dimensions = 7;
const model_embeds = [_][:0]const u8{
assets.sphere_m3d,
assets.teapot_m3d,
assets.torusknot_m3d,
assets.venus_m3d,
assets.stanford_dragon_m3d,
};
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
//
// Member variables
//
title_timer: core.Timer,
timer: core.Timer,
camera: Camera,
render_pipeline: *gpu.RenderPipeline,
render_pass_descriptor: gpu.RenderPassDescriptor,
bind_group: *gpu.BindGroup,
color_attachment: gpu.RenderPassColorAttachment,
depth_stencil_attachment_description: gpu.RenderPassDepthStencilAttachment,
depth_texture: *gpu.Texture,
depth_texture_view: *gpu.TextureView,
pressed_keys: PressedKeys,
models: [5]Model,
ubo_params: UboParams,
ubo_matrices: UboMatrices,
uniform_buffers: UniformBuffers,
material_params_dynamic: MaterialParamsDynamicGrid = [1]MaterialParamsDynamic{.{}} ** grid_element_count,
object_params_dynamic: ObjectParamsDynamicGrid = [1]ObjectParamsDynamic{.{}} ** grid_element_count,
uniform_buffers_dirty: bool,
buffers_bound: bool,
is_paused: bool,
current_material_index: usize,
current_object_index: usize,
mouse_position: core.Position,
is_rotating: bool,
//
// Functions
//
pub fn init(app: *App) !void {
try core.init(.{});
app.timer = try core.Timer.start();
app.title_timer = try core.Timer.start();
app.pressed_keys = .{};
app.buffers_bound = false;
app.is_paused = false;
app.uniform_buffers_dirty = false;
app.current_material_index = 0;
app.current_object_index = 0;
app.mouse_position = .{ .x = 0, .y = 0 };
app.is_rotating = false;
setupCamera(app);
try loadModels(std.heap.c_allocator, app);
prepareUniformBuffers(app);
setupPipeline(app);
setupRenderPass(app);
app.printControls();
}
pub fn deinit(app: *App) void {
defer _ = gpa.deinit();
defer core.deinit();
app.bind_group.release();
app.render_pipeline.release();
app.depth_texture_view.release();
app.depth_texture.release();
for (app.models) |model| {
model.index_buffer.release();
model.vertex_buffer.release();
}
app.uniform_buffers.ubo_matrices.buffer.release();
app.uniform_buffers.ubo_params.buffer.release();
app.uniform_buffers.material_params.buffer.release();
app.uniform_buffers.object_params.buffer.release();
}
pub fn update(app: *App) !bool {
var iter = core.pollEvents();
while (iter.next()) |event| {
app.updateUI(event);
switch (event) {
.mouse_motion => |ev| {
if (app.is_rotating) {
const delta = Vec2{
@as(f32, @floatCast((app.mouse_position.x - ev.pos.x) * app.camera.rotation_speed)),
@as(f32, @floatCast((app.mouse_position.y - ev.pos.y) * app.camera.rotation_speed)),
};
app.mouse_position = ev.pos;
app.camera.rotate(delta);
app.uniform_buffers_dirty = true;
}
},
.mouse_press => |ev| {
if (ev.button == .left) {
app.is_rotating = true;
app.mouse_position = ev.pos;
}
},
.mouse_release => |ev| {
if (ev.button == .left) {
app.is_rotating = false;
}
},
.key_press, .key_repeat => |ev| {
const key = ev.key;
if (key == .up or key == .w) app.pressed_keys.up = true;
if (key == .down or key == .s) app.pressed_keys.down = true;
if (key == .left or key == .a) app.pressed_keys.left = true;
if (key == .right or key == .d) app.pressed_keys.right = true;
},
.framebuffer_resize => |ev| {
app.depth_texture_view.release();
app.depth_texture.release();
app.depth_texture = core.device.createTexture(&gpu.Texture.Descriptor{
.usage = .{ .render_attachment = true },
.format = .depth24_plus_stencil8,
.sample_count = 1,
.size = .{
.width = ev.width,
.height = ev.height,
.depth_or_array_layers = 1,
},
});
app.depth_texture_view = app.depth_texture.createView(&gpu.TextureView.Descriptor{
.format = .depth24_plus_stencil8,
.dimension = .dimension_2d,
.array_layer_count = 1,
.aspect = .all,
});
app.depth_stencil_attachment_description = gpu.RenderPassDepthStencilAttachment{
.view = app.depth_texture_view,
.depth_load_op = .clear,
.depth_store_op = .store,
.depth_clear_value = 1.0,
.stencil_clear_value = 0,
.stencil_load_op = .clear,
.stencil_store_op = .store,
};
const aspect_ratio = @as(f32, @floatFromInt(ev.width)) / @as(f32, @floatFromInt(ev.height));
app.camera.setPerspective(60.0, aspect_ratio, 0.1, 256.0);
app.uniform_buffers_dirty = true;
},
.close => return true,
else => {},
}
}
if (app.pressed_keys.areKeysPressed()) {
app.camera.calculateMovement(app.pressed_keys);
app.pressed_keys.clear();
app.uniform_buffers_dirty = true;
}
if (app.uniform_buffers_dirty) {
updateUniformBuffers(app);
app.uniform_buffers_dirty = false;
}
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
app.color_attachment.view = back_buffer_view;
app.render_pass_descriptor = gpu.RenderPassDescriptor{
.color_attachment_count = 1,
.color_attachments = &[_]gpu.RenderPassColorAttachment{app.color_attachment},
.depth_stencil_attachment = &app.depth_stencil_attachment_description,
};
const encoder = core.device.createCommandEncoder(null);
const current_model = app.models[app.current_object_index];
const pass = encoder.beginRenderPass(&app.render_pass_descriptor);
const dimensions = Dimensions2D(f32){
.width = @as(f32, @floatFromInt(core.descriptor.width)),
.height = @as(f32, @floatFromInt(core.descriptor.height)),
};
pass.setViewport(
0,
0,
dimensions.width,
dimensions.height,
0.0,
1.0,
);
pass.setScissorRect(0, 0, core.descriptor.width, core.descriptor.height);
pass.setPipeline(app.render_pipeline);
if (!app.is_paused) {
app.updateLights();
}
var i: usize = 0;
while (i < (grid_dimensions * grid_dimensions)) : (i += 1) {
const alignment = 256;
const dynamic_offset: u32 = @as(u32, @intCast(i)) * alignment;
const dynamic_offsets = [2]u32{ dynamic_offset, dynamic_offset };
pass.setBindGroup(0, app.bind_group, &dynamic_offsets);
if (!app.buffers_bound) {
pass.setVertexBuffer(0, current_model.vertex_buffer, 0, @sizeOf(Vertex) * current_model.vertex_count);
pass.setIndexBuffer(current_model.index_buffer, .uint32, 0, gpu.whole_size);
app.buffers_bound = true;
}
pass.drawIndexed(
current_model.index_count, // index_count
1, // instance_count
0, // first_index
0, // base_vertex
0, // first_instance
);
}
pass.end();
pass.release();
var command = encoder.finish(null);
encoder.release();
const queue = core.queue;
queue.submit(&[_]*gpu.CommandBuffer{command});
command.release();
core.swap_chain.present();
back_buffer_view.release();
app.buffers_bound = false;
// update the window title every second
if (app.title_timer.read() >= 1.0) {
app.title_timer.reset();
try core.printTitle("PBR Basic [ {d}fps ] [ Input {d}hz ]", .{
core.frameRate(),
core.inputRate(),
});
}
return false;
}
fn prepareUniformBuffers(app: *App) void {
comptime {
std.debug.assert(@sizeOf(ObjectParamsDynamic) == 256);
std.debug.assert(@sizeOf(MaterialParamsDynamic) == 256);
}
app.uniform_buffers.ubo_matrices.size = roundToMultipleOf4(u32, @as(u32, @intCast(@sizeOf(UboMatrices)))) + 4;
app.uniform_buffers.ubo_matrices.buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .uniform = true },
.size = app.uniform_buffers.ubo_matrices.size,
.mapped_at_creation = .false,
});
app.uniform_buffers.ubo_params.size = roundToMultipleOf4(u32, @as(u32, @intCast(@sizeOf(UboParams)))) + 4;
app.uniform_buffers.ubo_params.buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .uniform = true },
.size = app.uniform_buffers.ubo_params.size,
.mapped_at_creation = .false,
});
//
// Material parameter uniform buffer
//
app.uniform_buffers.material_params.model_size = @sizeOf(Vec2) + @sizeOf(Vec3);
app.uniform_buffers.material_params.buffer_size = calculateConstantBufferByteSize(@sizeOf(MaterialParamsDynamicGrid));
std.debug.assert(app.uniform_buffers.material_params.buffer_size >= app.uniform_buffers.material_params.model_size);
app.uniform_buffers.material_params.buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .uniform = true },
.size = app.uniform_buffers.material_params.buffer_size,
.mapped_at_creation = .false,
});
//
// Object parameter uniform buffer
//
app.uniform_buffers.object_params.model_size = @sizeOf(Vec3) + 4;
app.uniform_buffers.object_params.buffer_size = calculateConstantBufferByteSize(@sizeOf(MaterialParamsDynamicGrid)) + 4;
std.debug.assert(app.uniform_buffers.object_params.buffer_size >= app.uniform_buffers.object_params.model_size);
app.uniform_buffers.object_params.buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .uniform = true },
.size = app.uniform_buffers.object_params.buffer_size,
.mapped_at_creation = .false,
});
app.updateUniformBuffers();
app.updateDynamicUniformBuffer();
app.updateLights();
}
fn updateDynamicUniformBuffer(app: *App) void {
var index: u32 = 0;
var y: usize = 0;
while (y < grid_dimensions) : (y += 1) {
var x: usize = 0;
while (x < grid_dimensions) : (x += 1) {
const grid_dimensions_float = @as(f32, @floatFromInt(grid_dimensions));
app.object_params_dynamic[index].position[0] = (@as(f32, @floatFromInt(x)) - (grid_dimensions_float / 2) * 2.5);
app.object_params_dynamic[index].position[1] = 0;
app.object_params_dynamic[index].position[2] = (@as(f32, @floatFromInt(y)) - (grid_dimensions_float / 2) * 2.5);
app.material_params_dynamic[index].metallic = zm.clamp(@as(f32, @floatFromInt(x)) / (grid_dimensions_float - 1), 0.1, 1.0);
app.material_params_dynamic[index].roughness = zm.clamp(@as(f32, @floatFromInt(y)) / (grid_dimensions_float - 1), 0.05, 1.0);
app.material_params_dynamic[index].color = materials[app.current_material_index].params.color;
index += 1;
}
}
const queue = core.queue;
queue.writeBuffer(
app.uniform_buffers.object_params.buffer,
0,
&app.object_params_dynamic,
);
queue.writeBuffer(
app.uniform_buffers.material_params.buffer,
0,
&app.material_params_dynamic,
);
}
fn updateUniformBuffers(app: *App) void {
app.ubo_matrices.projection = app.camera.matrices.perspective;
app.ubo_matrices.view = app.camera.matrices.view;
const rotation_degrees = if (app.current_object_index == 1) @as(f32, -45.0) else @as(f32, -90.0);
const model = zm.rotationY(rotation_degrees);
app.ubo_matrices.model[0] = model[0];
app.ubo_matrices.model[1] = model[1];
app.ubo_matrices.model[2] = model[2];
app.ubo_matrices.model[3] = model[3];
app.ubo_matrices.camera_position = .{
-app.camera.position[0],
-app.camera.position[1],
-app.camera.position[2],
};
const queue = core.queue;
queue.writeBuffer(app.uniform_buffers.ubo_matrices.buffer, 0, &[_]UboMatrices{app.ubo_matrices});
}
fn updateLights(app: *App) void {
const p: f32 = 15.0;
app.ubo_params.lights[0] = Vec4{ -p, -p * 0.5, -p, 1.0 };
app.ubo_params.lights[1] = Vec4{ -p, -p * 0.5, p, 1.0 };
app.ubo_params.lights[2] = Vec4{ p, -p * 0.5, p, 1.0 };
app.ubo_params.lights[3] = Vec4{ p, -p * 0.5, -p, 1.0 };
const base_value = toRadians(@mod(app.timer.read() * 0.1, 1.0) * 360.0);
app.ubo_params.lights[0][0] = @sin(base_value) * 20.0;
app.ubo_params.lights[0][2] = @cos(base_value) * 20.0;
app.ubo_params.lights[1][0] = @cos(base_value) * 20.0;
app.ubo_params.lights[1][1] = @sin(base_value) * 20.0;
const queue = core.queue;
queue.writeBuffer(
app.uniform_buffers.ubo_params.buffer,
0,
&[_]UboParams{app.ubo_params},
);
}
fn setupPipeline(app: *App) void {
comptime {
std.debug.assert(@sizeOf(Vertex) == @sizeOf(f32) * 6);
}
const bind_group_layout_entries = [_]gpu.BindGroupLayout.Entry{
.{
.binding = 0,
.visibility = .{ .vertex = true, .fragment = true },
.buffer = .{
.type = .uniform,
.has_dynamic_offset = .false,
.min_binding_size = app.uniform_buffers.ubo_matrices.size,
},
},
.{
.binding = 1,
.visibility = .{ .fragment = true },
.buffer = .{
.type = .uniform,
.has_dynamic_offset = .false,
.min_binding_size = app.uniform_buffers.ubo_params.size,
},
},
.{
.binding = 2,
.visibility = .{ .fragment = true },
.buffer = .{
.type = .uniform,
.has_dynamic_offset = .true,
.min_binding_size = app.uniform_buffers.material_params.model_size,
},
},
.{
.binding = 3,
.visibility = .{ .vertex = true },
.buffer = .{
.type = .uniform,
.has_dynamic_offset = .true,
.min_binding_size = app.uniform_buffers.object_params.model_size,
},
},
};
const bind_group_layout = core.device.createBindGroupLayout(
&gpu.BindGroupLayout.Descriptor.init(.{
.entries = bind_group_layout_entries[0..],
}),
);
defer bind_group_layout.release();
const bind_group_layouts = [_]*gpu.BindGroupLayout{bind_group_layout};
const pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{
.bind_group_layouts = &bind_group_layouts,
}));
defer pipeline_layout.release();
const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{
.array_stride = @sizeOf(Vertex),
.step_mode = .vertex,
.attributes = &.{
.{ .format = .float32x3, .offset = @offsetOf(Vertex, "position"), .shader_location = 0 },
.{ .format = .float32x3, .offset = @offsetOf(Vertex, "normal"), .shader_location = 1 },
},
});
const blend_component_descriptor = gpu.BlendComponent{
.operation = .add,
.src_factor = .one,
.dst_factor = .zero,
};
const color_target_state = gpu.ColorTargetState{
.format = core.descriptor.format,
.blend = &.{
.color = blend_component_descriptor,
.alpha = blend_component_descriptor,
},
};
const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl"));
const pipeline_descriptor = gpu.RenderPipeline.Descriptor{
.layout = pipeline_layout,
.primitive = .{
.cull_mode = .back,
},
.depth_stencil = &.{
.format = .depth24_plus_stencil8,
.depth_write_enabled = .true,
.depth_compare = .less,
},
.fragment = &gpu.FragmentState.init(.{
.module = shader_module,
.entry_point = "frag_main",
.targets = &.{color_target_state},
}),
.vertex = gpu.VertexState.init(.{
.module = shader_module,
.entry_point = "vertex_main",
.buffers = &.{vertex_buffer_layout},
}),
};
app.render_pipeline = core.device.createRenderPipeline(&pipeline_descriptor);
shader_module.release();
{
const bind_group_entries = [_]gpu.BindGroup.Entry{
.{
.binding = 0,
.buffer = app.uniform_buffers.ubo_matrices.buffer,
.size = app.uniform_buffers.ubo_matrices.size,
},
.{
.binding = 1,
.buffer = app.uniform_buffers.ubo_params.buffer,
.size = app.uniform_buffers.ubo_params.size,
},
.{
.binding = 2,
.buffer = app.uniform_buffers.material_params.buffer,
.size = app.uniform_buffers.material_params.model_size,
},
.{
.binding = 3,
.buffer = app.uniform_buffers.object_params.buffer,
.size = app.uniform_buffers.object_params.model_size,
},
};
app.bind_group = core.device.createBindGroup(
&gpu.BindGroup.Descriptor.init(.{
.layout = bind_group_layout,
.entries = &bind_group_entries,
}),
);
}
}
fn setupRenderPass(app: *App) void {
app.color_attachment = gpu.RenderPassColorAttachment{
.clear_value = .{
.r = 0.0,
.g = 0.0,
.b = 0.0,
.a = 0.0,
},
.load_op = .clear,
.store_op = .store,
};
app.depth_texture = core.device.createTexture(&.{
.usage = .{ .render_attachment = true, .copy_src = true },
.format = .depth24_plus_stencil8,
.sample_count = 1,
.size = .{
.width = core.descriptor.width,
.height = core.descriptor.height,
.depth_or_array_layers = 1,
},
});
app.depth_texture_view = app.depth_texture.createView(&.{
.format = .depth24_plus_stencil8,
.dimension = .dimension_2d,
.array_layer_count = 1,
.aspect = .all,
});
app.depth_stencil_attachment_description = gpu.RenderPassDepthStencilAttachment{
.view = app.depth_texture_view,
.depth_load_op = .clear,
.depth_store_op = .store,
.depth_clear_value = 1.0,
.stencil_clear_value = 0,
.stencil_load_op = .clear,
.stencil_store_op = .store,
};
}
fn loadModels(allocator: std.mem.Allocator, app: *App) !void {
for (model_embeds, 0..) |model_data, model_data_i| {
const m3d_model = m3d.load(model_data, null, null, null) orelse return error.LoadModelFailed;
const vertex_count = m3d_model.handle.numvertex;
const face_count = m3d_model.handle.numface;
var model: *Model = &app.models[model_data_i];
model.index_count = face_count * 3;
var vertex_writer = try VertexWriter(Vertex, u32).init(allocator, face_count * 3, vertex_count, face_count * 3);
defer vertex_writer.deinit(allocator);
const scale: f32 = 0.45;
const vertices = m3d_model.handle.vertex[0..vertex_count];
var i: usize = 0;
while (i < face_count) : (i += 1) {
const face = m3d_model.handle.face[i];
var x: usize = 0;
while (x < 3) : (x += 1) {
const vertex_index = face.vertex[x];
const normal_index = face.normal[x];
const vertex = Vertex{
.position = .{
vertices[vertex_index].x * scale,
vertices[vertex_index].y * scale,
vertices[vertex_index].z * scale,
},
.normal = .{
vertices[normal_index].x,
vertices[normal_index].y,
vertices[normal_index].z,
},
};
vertex_writer.put(vertex, vertex_index);
}
}
const vertex_buffer = vertex_writer.vertexBuffer();
const index_buffer = vertex_writer.indexBuffer();
model.vertex_count = @as(u32, @intCast(vertex_buffer.len));
model.vertex_buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .vertex = true },
.size = @sizeOf(Vertex) * model.vertex_count,
.mapped_at_creation = .false,
});
const queue = core.queue;
queue.writeBuffer(model.vertex_buffer, 0, vertex_buffer);
model.index_buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .index = true },
.size = @sizeOf(u32) * model.index_count,
.mapped_at_creation = .false,
});
queue.writeBuffer(model.index_buffer, 0, index_buffer);
}
}
fn printControls(app: *App) void {
std.debug.print("[controls]\n", .{});
std.debug.print("[p] paused: {}\n", .{app.is_paused});
std.debug.print("[m] material: {s}\n", .{material_names[app.current_material_index]});
std.debug.print("[o] object: {s}\n", .{object_names[app.current_object_index]});
}
fn updateUI(app: *App, event: core.Event) void {
switch (event) {
.key_press => |ev| {
var update_uniform_buffers: bool = false;
switch (ev.key) {
.p => app.is_paused = !app.is_paused,
.m => {
app.current_material_index = (app.current_material_index + 1) % material_names.len;
update_uniform_buffers = true;
},
.o => {
app.current_object_index = (app.current_object_index + 1) % object_names.len;
update_uniform_buffers = true;
},
else => return,
}
app.printControls();
if (update_uniform_buffers) {
updateDynamicUniformBuffer(app);
}
},
else => {},
}
}
fn setupCamera(app: *App) void {
app.camera = Camera{
.rotation_speed = 1.0,
.movement_speed = 1.0,
};
const aspect_ratio: f32 = @as(f32, @floatFromInt(core.descriptor.width)) / @as(f32, @floatFromInt(core.descriptor.height));
app.camera.setPosition(.{ 10.0, 6.0, 6.0 });
app.camera.setRotation(.{ 62.5, 90.0, 0.0 });
app.camera.setMovementSpeed(0.5);
app.camera.setPerspective(60.0, aspect_ratio, 0.1, 256.0);
app.camera.setRotationSpeed(0.25);
}
inline fn roundToMultipleOf4(comptime T: type, value: T) T {
return (value + 3) & ~@as(T, 3);
}
inline fn calculateConstantBufferByteSize(byte_size: usize) usize {
return (byte_size + 255) & ~@as(usize, 255);
}
inline fn toRadians(degrees: f32) f32 {
return degrees * (std.math.pi / 180.0);
}

View file

@ -1,119 +0,0 @@
@group(0) @binding(0) var<uniform> ubo : UBO;
@group(0) @binding(1) var<uniform> uboParams : UBOShared;
@group(0) @binding(2) var<uniform> material : MaterialParams;
@group(0) @binding(3) var<uniform> object : ObjectParams;
struct VertexOut {
@builtin(position) position_clip : vec4<f32>,
@location(0) fragPosition : vec3<f32>,
@location(1) fragNormal : vec3<f32>,
}
struct MaterialParams {
roughness : f32,
metallic : f32,
r : f32,
g : f32,
b : f32
}
struct UBOShared {
lights : array<vec4<f32>, 4>,
}
struct UBO {
projection : mat4x4<f32>,
model : mat4x4<f32>,
view : mat4x4<f32>,
camPos : vec3<f32>,
}
struct ObjectParams {
position : vec3<f32>
}
@vertex fn vertex_main(
@location(0) position : vec3<f32>,
@location(1) normal : vec3<f32>
) -> VertexOut {
var output : VertexOut;
var locPos = vec4<f32>(ubo.model * vec4<f32>(position, 1.0));
output.fragPosition = locPos.xyz + object.position;
output.fragNormal = mat3x3<f32>(ubo.model[0].xyz, ubo.model[1].xyz, ubo.model[2].xyz) * normal;
output.position_clip = ubo.projection * ubo.view * vec4<f32>(output.fragPosition, 1.0);
return output;
}
const PI : f32 = 3.14159265359;
fn material_color() -> vec3<f32> {
return vec3<f32>(material.r, material.g, material.b);
}
// Normal Distribution function --------------------------------------
fn D_GGX(dotNH : f32, roughness : f32) -> f32 {
var alpha : f32 = roughness * roughness;
var alpha2 : f32 = alpha * alpha;
var denom : f32 = dotNH * dotNH * (alpha2 - 1.0) + 1.0;
return alpha2 / (PI * denom * denom);
}
// Geometric Shadowing function --------------------------------------
fn G_SchlicksmithGGX(dotNL : f32, dotNV : f32, roughness : f32) -> f32 {
var r : f32 = roughness + 1.0;
var k : f32 = (r * r) / 8.0;
var GL : f32 = dotNL / (dotNL * (1.0 - k) + k);
var GV : f32 = dotNV / (dotNV * (1.0 - k) + k);
return GL * GV;
}
// Fresnel function ----------------------------------------------------
fn F_Schlick(cosTheta : f32, metallic : f32) -> vec3<f32> {
var F0 : vec3<f32> = mix(vec3<f32>(0.04), material_color(), metallic);
var F : vec3<f32> = F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
return F;
}
// Specular BRDF composition --------------------------------------------
fn BRDF(L : vec3<f32>, V : vec3<f32>, N : vec3<f32>, metallic : f32, roughness : f32) -> vec3<f32> {
var H : vec3<f32> = normalize(V + L);
var dotNV : f32 = clamp(dot(N, V), 0.0, 1.0);
var dotNL : f32 = clamp(dot(N, L), 0.0, 1.0);
var dotLH : f32 = clamp(dot(L, H), 0.0, 1.0);
var dotNH : f32 = clamp(dot(N, H), 0.0, 1.0);
var lightColor = vec3<f32>(1.0);
var color = vec3<f32>(0.0);
if(dotNL > 0.0) {
var rroughness : f32 = max(0.05, roughness);
// D = Normal distribution (Distribution of the microfacets)
var D : f32 = D_GGX(dotNH, roughness);
// G = Geometric shadowing term (Microfacets shadowing)
var G : f32 = G_SchlicksmithGGX(dotNL, dotNV, roughness);
// F = Fresnel factor (Reflectance depending on angle of incidence)
var F : vec3<f32> = F_Schlick(dotNV, metallic);
var spec : vec3<f32> = (D * F * G) / (4.0 * dotNL * dotNV);
color += spec * dotNL * lightColor;
}
return color;
}
// TODO - global variable declaration order
@fragment fn frag_main(
@location(0) position : vec3<f32>,
@location(1) normal: vec3<f32>
) -> @location(0) vec4<f32> {
var N : vec3<f32> = normalize(normal);
var V : vec3<f32> = normalize(ubo.camPos - position);
var Lo = vec3<f32>(0.0);
// Specular contribution
for(var i: i32 = 0; i < 4; i++) {
var L : vec3<f32> = normalize(uboParams.lights[i].xyz - position);
Lo += BRDF(L, V, N, material.metallic, material.roughness);
}
// Combine with ambient
var color : vec3<f32> = material_color() * 0.02;
color += Lo;
// Gamma correct
color = pow(color, vec3<f32>(0.4545));
return vec4<f32>(color, 1.0);
}

View file

@ -1,188 +0,0 @@
const std = @import("std");
/// Vertex writer manages the placement of vertices by tracking which are unique. If a duplicate vertex is added
/// with `put`, only it's index will be written to the index buffer.
/// `IndexType` should match the integer type used for the index buffer
pub fn VertexWriter(comptime VertexType: type, comptime IndexType: type) type {
return struct {
const MapEntry = struct {
packed_index: IndexType = null_index,
next_sparse: IndexType = null_index,
};
const null_index: IndexType = std.math.maxInt(IndexType);
vertices: []VertexType,
indices: []IndexType,
sparse_to_packed_map: []MapEntry,
/// Next index outside of the 1:1 mapping range for storing
/// position -> normal collisions
next_collision_index: IndexType,
/// Next packed index
next_packed_index: IndexType,
written_indices_count: IndexType,
/// Allocate storage and set default values
/// `sparse_vertices_count` is the number of vertices in the source before de-duplication / remapping
/// Put more succinctly, the largest index value in source index buffer
/// `max_vertex_count` is largest permutation of vertices assuming that {vertex, uv, normal} never map 1:1 and always
/// create a new mapping
pub fn init(
allocator: std.mem.Allocator,
indices_count: IndexType,
sparse_vertices_count: IndexType,
max_vertex_count: IndexType,
) !@This() {
var result: @This() = undefined;
result.vertices = try allocator.alloc(VertexType, max_vertex_count);
result.indices = try allocator.alloc(IndexType, indices_count);
result.sparse_to_packed_map = try allocator.alloc(MapEntry, max_vertex_count);
result.next_collision_index = sparse_vertices_count;
result.next_packed_index = 0;
result.written_indices_count = 0;
@memset(result.sparse_to_packed_map, .{});
return result;
}
pub fn put(self: *@This(), vertex: VertexType, sparse_index: IndexType) void {
if (self.sparse_to_packed_map[sparse_index].packed_index == null_index) {
// New start of chain, reserve a new packed index and add entry to `index_map`
const packed_index = self.next_packed_index;
self.sparse_to_packed_map[sparse_index].packed_index = packed_index;
self.vertices[packed_index] = vertex;
self.indices[self.written_indices_count] = packed_index;
self.written_indices_count += 1;
self.next_packed_index += 1;
return;
}
var previous_sparse_index: IndexType = undefined;
var current_sparse_index = sparse_index;
while (current_sparse_index != null_index) {
const packed_index = self.sparse_to_packed_map[current_sparse_index].packed_index;
if (std.mem.eql(u8, &std.mem.toBytes(self.vertices[packed_index]), &std.mem.toBytes(vertex))) {
// We already have a record for this vertex in our chain
self.indices[self.written_indices_count] = packed_index;
self.written_indices_count += 1;
return;
}
previous_sparse_index = current_sparse_index;
current_sparse_index = self.sparse_to_packed_map[current_sparse_index].next_sparse;
}
// This is a new mapping for the given sparse index
const packed_index = self.next_packed_index;
const remapped_sparse_index = self.next_collision_index;
self.indices[self.written_indices_count] = packed_index;
self.vertices[packed_index] = vertex;
self.sparse_to_packed_map[previous_sparse_index].next_sparse = remapped_sparse_index;
self.sparse_to_packed_map[remapped_sparse_index].packed_index = packed_index;
self.next_packed_index += 1;
self.next_collision_index += 1;
self.written_indices_count += 1;
}
pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void {
allocator.free(self.vertices);
allocator.free(self.indices);
allocator.free(self.sparse_to_packed_map);
}
pub fn indexBuffer(self: @This()) []IndexType {
return self.indices;
}
pub fn vertexBuffer(self: @This()) []VertexType {
return self.vertices[0..self.next_packed_index];
}
};
}
test "VertexWriter" {
const Vec3 = [3]f32;
const Vertex = extern struct {
position: Vec3,
normal: Vec3,
};
const expect = std.testing.expect;
const allocator = std.testing.allocator;
const Face = struct {
position: [3]u16,
normal: [3]u16,
};
const vertices = [_]Vec3{
Vec3{ 1.0, 0.0, 0.0 }, // 0: Position
Vec3{ 2.0, 0.0, 0.0 }, // 1: Position
Vec3{ 3.0, 0.0, 0.0 }, // 2: Position
Vec3{ 1.0, 0.0, 0.0 }, // 3: Normal
Vec3{ 4.0, 0.0, 0.0 }, // 4: Position
Vec3{ 0.0, 1.0, 0.0 }, // 5: Normal
Vec3{ 5.0, 0.0, 0.0 }, // 6: Position
Vec3{ 0.0, 0.0, 1.0 }, // 7: Normal
Vec3{ 1.0, 0.0, 1.0 }, // 8: Normal
Vec3{ 6.0, 0.0, 0.0 }, // 9: Position
};
const faces = [_]Face{
.{ .position = .{ 0, 4, 2 }, .normal = .{ 7, 5, 3 } },
.{ .position = .{ 2, 3, 9 }, .normal = .{ 3, 7, 8 } },
.{ .position = .{ 9, 2, 4 }, .normal = .{ 8, 7, 5 } },
.{ .position = .{ 2, 6, 1 }, .normal = .{ 3, 5, 7 } },
.{ .position = .{ 9, 6, 0 }, .normal = .{ 5, 7, 8 } },
};
var writer = try VertexWriter(Vertex, u32).init(
allocator,
faces.len * 3, // indices count
vertices.len, // original vertices count
faces.len * 3, // maximum vertices count
);
defer writer.deinit(allocator);
for (faces) |face| {
var x: usize = 0;
while (x < 3) : (x += 1) {
const position_index = face.position[x];
const position = vertices[position_index];
const normal = vertices[face.normal[x]];
const vertex = Vertex{
.position = position,
.normal = normal,
};
writer.put(vertex, position_index);
}
}
const indices = writer.indexBuffer();
try expect(indices.len == faces.len * 3);
// Face 0
try expect(indices[0] == 0); // (0, 7) New
try expect(indices[1] == 1); // (4, 5) New
try expect(indices[2] == 2); // (2, 3) New
// Face 1
try expect(indices[3 + 0] == 2); // (2, 3) Duplicate - Reuse index
try expect(indices[3 + 1] == 3); // (3, 7) New
try expect(indices[3 + 2] == 4); // (9, 8) New
// Face 2
try expect(indices[6 + 0] == 4); // (9, 8) Duplicate - Reuse index
try expect(indices[6 + 1] == 5); // (2, 7) New normal mapping (Don't clobber)
try expect(indices[6 + 2] == 1); // (4, 5) Duplicate - Reuse Index
// Face 3
try expect(indices[9 + 0] == 2); // (2, 3) Duplicate - Reuse index
try expect(indices[9 + 1] == 6); // (6, 5) New
try expect(indices[9 + 2] == 7); // (1, 7) New
// Face 4
try expect(indices[12 + 0] == 8); // (9, 5) New normal mapping (Don't clobber)
try expect(indices[12 + 1] == 9); // (6, 7) New normal mapping (Don't clobber)
try expect(indices[12 + 2] == 10); // (0, 8) New normal mapping (Don't clobber)
try expect(writer.vertexBuffer().len == 11);
}