commit 0fd02f7226880b8e349ce4c58b490ee885715921
parent 848206676e741c9356e82718989aef6f01f93f42
Author: Christian Ermann <christianermann@gmail.com>
Date: Wed, 15 May 2024 16:37:34 -0400
Add interactive camera
Diffstat:
A | src/camera.zig | | | 126 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | src/input.zig | | | 52 | ++++++++++++++++++++++++++++++++++++++++++++++++++++ |
M | src/main.zig | | | 117 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----- |
3 files changed, 289 insertions(+), 6 deletions(-)
diff --git a/src/camera.zig b/src/camera.zig
@@ -0,0 +1,126 @@
+const std = @import("std");
+const WASDInput = @import("input.zig").WASDInput;
+
+const Camera = @This();
+
+const f32x3 = @Vector(3, f32);
+const f32x4 = @Vector(4, f32);
+const mat4 = [4]f32x4;
+
+fovy: f32 = 60,
+aspect: f32 = 1,
+near: f32 = 0.1,
+far: f32 = 100,
+
+yaw: f32 = 90.0,
+pitch: f32 = 0.0,
+
+position: f32x3 = .{ 0, 0, -1 },
+
+input: WASDInput = WASDInput{},
+
+pub fn logParams(self: *const Camera) void {
+ std.log.info("camera fovy: {}", .{self.fovy});
+ std.log.info("camera aspect: {}", .{self.aspect});
+ std.log.info("camera near: {}", .{self.near});
+ std.log.info("camera far: {}", .{self.far});
+}
+
+fn f32x3_dot(u: f32x3, v: f32x3) f32 {
+ return @reduce(.Add, u * v);
+}
+
+fn f32x3_normalize(v: f32x3) f32x3 {
+ return v / @as(f32x3, @splat(@sqrt(f32x3_dot(v, v))));
+}
+
+/// Modified from https://geometrian.com/programming/tutorials/cross-product/index.php
+fn f32x3_mul(u: f32x3, v: f32x3) f32x3 {
+ const mask = @Vector(3, i32){ 1, 2, 0 };
+ const t1 = u * @shuffle(f32, v, undefined, mask);
+ const t2 = v * @shuffle(f32, u, undefined, mask);
+ return @shuffle(f32, t1 - t2, undefined, mask);
+}
+
+test "f32x3_mul works" {
+ const u = f32x3{ 1, 2, 3 };
+ const v = f32x3{ 4, 5, 6 };
+ const result = f32x3_mul(u, v);
+ try std.testing.expectApproxEqAbs(-3, result[0], 0.00001);
+}
+
+fn f32x4_scale(v: f32x4, s: f32) f32x4 {
+ return v * @as(f32x4, @splat(s));
+}
+
+fn mat4_mul(l: mat4, r: mat4, result: *mat4) void {
+ result.* = .{
+ f32x4_scale(l[0], r[0][0]) + f32x4_scale(l[1], r[0][1]) + f32x4_scale(l[2], r[0][2]) + f32x4_scale(l[3], r[0][3]),
+ f32x4_scale(l[0], r[1][0]) + f32x4_scale(l[1], r[1][1]) + f32x4_scale(l[2], r[1][2]) + f32x4_scale(l[3], r[1][3]),
+ f32x4_scale(l[0], r[2][0]) + f32x4_scale(l[1], r[2][1]) + f32x4_scale(l[2], r[2][2]) + f32x4_scale(l[3], r[2][3]),
+ f32x4_scale(l[0], r[3][0]) + f32x4_scale(l[1], r[3][1]) + f32x4_scale(l[2], r[3][2]) + f32x4_scale(l[3], r[3][3]),
+ };
+}
+
+fn lookAt(eye: f32x3, target: f32x3, up: f32x3, mat: *mat4) void {
+ const f = f32x3_normalize(target - eye);
+ const s = f32x3_normalize(f32x3_mul(f, up));
+ const u = f32x3_normalize(f32x3_mul(s, f));
+ mat.* = .{
+ .{ s[0], u[0], -f[0], 0 },
+ .{ s[1], u[1], -f[1], 0 },
+ .{ s[2], u[2], -f[2], 0 },
+ .{ -f32x3_dot(s, eye), -f32x3_dot(u, eye), f32x3_dot(f, eye), 1.0 },
+ };
+}
+
+fn perspective(fovy: f32, aspect: f32, near: f32, far: f32, mat: *mat4) void {
+ const tan_half_fov = @tan(std.math.degreesToRadians(f32, fovy * 0.5));
+ const a = 1.0 / (aspect * tan_half_fov);
+ const b = 1.0 / tan_half_fov;
+ const c = -(far + near) / (far - near);
+ const d = -(2 * far * near) / (far - near);
+ mat.* = .{
+ .{ a, 0, 0, 0 },
+ .{ 0, b, 0, 0 },
+ .{ 0, 0, c, -1 },
+ .{ 0, 0, d, 0 },
+ };
+}
+
+pub fn view(self: *const Camera, mat: *mat4) void {
+ const cos_yaw = @cos(std.math.degreesToRadians(f32, self.yaw));
+ const sin_yaw = @sin(std.math.degreesToRadians(f32, self.yaw));
+ const cos_pitch = @cos(std.math.degreesToRadians(f32, self.pitch));
+ const sin_pitch = @sin(std.math.degreesToRadians(f32, self.pitch));
+ const forward = f32x3_normalize(
+ f32x3{ cos_yaw * cos_pitch, sin_pitch, sin_yaw * cos_pitch },
+ );
+
+ const target = self.position + forward;
+ lookAt(self.position, target, f32x3{ 0, 1, 0 }, mat);
+}
+
+pub fn proj(self: *const Camera, mat: *mat4) void {
+ perspective(self.fovy, self.aspect, self.near, self.far, mat);
+}
+
+pub fn update(self: *Camera, _: f32) void {
+ var forward: f32 = 0;
+ var right: f32 = 0;
+ if (self.input.w) {
+ forward += 1;
+ }
+ if (self.input.s) {
+ forward -= 1;
+ }
+ if (self.input.d) {
+ right += 1;
+ }
+ if (self.input.a) {
+ right -= 1;
+ }
+
+ self.position[0] -= right * 0.01;
+ self.position[2] += forward * 0.01;
+}
diff --git a/src/input.zig b/src/input.zig
@@ -0,0 +1,52 @@
+const std = @import("std");
+const glfw = @import("mach_glfw");
+
+pub const UserInput = struct {
+ ptr: *anyopaque,
+ keyCallbackFn: KeyCallbackFn,
+
+ pub fn keyCallback(self: UserInput, args: KeyCallbackArgs) void {
+ return self.keyCallbackFn(self.ptr, args);
+ }
+};
+
+pub const KeyCallbackArgs = struct {
+ window: glfw.Window,
+ key: glfw.Key,
+ scancode: i32,
+ action: glfw.Action,
+ mods: glfw.Mods,
+};
+
+const KeyCallbackFn = *const fn (
+ ptr: *anyopaque,
+ args: KeyCallbackArgs,
+) void;
+
+pub const WASDInput = struct {
+ w: bool = false,
+ a: bool = false,
+ s: bool = false,
+ d: bool = false,
+
+ fn keyCallback(ptr: *anyopaque, args: KeyCallbackArgs) void {
+ const self: *WASDInput = @ptrCast(@alignCast(ptr));
+ if ((args.action != glfw.Action.press) and (args.action != glfw.Action.release)) {
+ return;
+ }
+ switch (args.key) {
+ .w => self.w = !self.w,
+ .a => self.a = !self.a,
+ .s => self.s = !self.s,
+ .d => self.d = !self.d,
+ else => {},
+ }
+ }
+
+ pub fn userInput(self: *WASDInput) UserInput {
+ return .{
+ .ptr = self,
+ .keyCallbackFn = WASDInput.keyCallback,
+ };
+ }
+};
diff --git a/src/main.zig b/src/main.zig
@@ -3,6 +3,9 @@ const glfw = @import("mach_glfw");
const gpu = @import("mach_gpu");
const builtin = @import("builtin");
+const Camera = @import("camera.zig");
+const input = @import("input.zig");
+
const load_obj = @import("load_obj.zig");
const objc = @import("objc_message.zig");
@@ -51,6 +54,9 @@ const App = struct {
device: *gpu.Device,
queue: *gpu.Queue,
swap_chain: *AppSwapChain,
+ depth_texture: *gpu.Texture,
+ depth_texture_view: *gpu.TextureView,
+ inputs: std.ArrayList(input.UserInput),
pub fn init(app: *App, allocator: std.mem.Allocator) !void {
try gpu.Impl.init(allocator, .{});
@@ -106,12 +112,35 @@ const App = struct {
app.queue = app.device.getQueue();
app.swap_chain = try app.createSwapChain(allocator);
- app.window.setUserPointer(app.swap_chain);
+
+ app.inputs = std.ArrayList(input.UserInput).init(allocator);
+ app.window.setKeyCallback(glfwKeyCallback);
+
+ app.window.setUserPointer(app);
+
+ const framebuffer_size = app.window.getFramebufferSize();
+ app.depth_texture = app.device.createTexture(&gpu.Texture.Descriptor{
+ .size = gpu.Extent3D{
+ .width = framebuffer_size.width,
+ .height = framebuffer_size.height,
+ },
+ .format = .depth24_plus,
+ .usage = .{
+ .render_attachment = true,
+ },
+ });
+ app.depth_texture_view = app.depth_texture.createView(&gpu.TextureView.Descriptor{
+ .format = .depth24_plus,
+ .dimension = .dimension_2d,
+ .array_layer_count = 1,
+ .mip_level_count = 1,
+ });
}
pub fn deinit(app: *App) void {
app.window.destroy();
glfw.terminate();
+ app.inputs.deinit();
}
fn createSurfaceForWindow(app: *App) !*gpu.Surface {
@@ -196,7 +225,7 @@ const App = struct {
}
fn getCurrentSwapChain(app: *App) *gpu.SwapChain {
- const sc = app.window.getUserPointer(AppSwapChain).?;
+ const sc = app.swap_chain;
const not_exists = sc.gpu_swap_chain == null;
const changed = !std.meta.eql(sc.current_descriptor, sc.target_descriptor);
if (not_exists or changed) {
@@ -227,6 +256,12 @@ const App = struct {
};
const render_pass_info = gpu.RenderPassDescriptor.init(.{
.color_attachments = &.{color_attachment},
+ .depth_stencil_attachment = &.{
+ .view = app.depth_texture_view,
+ .depth_clear_value = 1.0,
+ .depth_load_op = .clear,
+ .depth_store_op = .store,
+ },
});
{
@@ -247,6 +282,12 @@ const App = struct {
}
}
}
+
+ pub fn keyCallback(app: *App, args: input.KeyCallbackArgs) void {
+ for (app.inputs.items) |user_input| {
+ user_input.keyCallback(args);
+ }
+ }
};
const AppSwapChain = struct {
@@ -346,6 +387,11 @@ const InstanceData = struct {
model_matrix: mat4,
};
+const UniformData = struct {
+ view_matrix: mat4,
+ proj_matrix: mat4,
+};
+
pub const Buffer = struct {
data: *gpu.Buffer,
size: u32,
@@ -563,6 +609,7 @@ const UnlitRenderPipeline = struct {
gpu_pipeline: *gpu.RenderPipeline,
mesh_buffer: *const MeshBuffer,
bind_group: *gpu.BindGroup,
+ uniform_buffer: *gpu.Buffer,
pub fn init(app: *App, mesh_buffer: *const MeshBuffer, _: std.mem.Allocator) !UnlitRenderPipeline {
const vs =
@@ -582,12 +629,21 @@ const UnlitRenderPipeline = struct {
\\ model_matrix: mat4x4<f32>,
\\ };
\\
+ \\ @group(0) @binding(1)
+ \\ var<uniform> uniform_data: UniformData;
+ \\ struct UniformData {
+ \\ view_matrix: mat4x4<f32>,
+ \\ proj_matrix: mat4x4<f32>,
+ \\ };
+ \\
\\ @vertex fn main(
\\ in: VertexInput,
\\ @builtin(instance_index) idx: u32,
\\ ) -> VertexOutput {
\\ let model_matrix = instances[idx].model_matrix;
- \\ return VertexOutput(model_matrix * in.position, in.normal);
+ \\ let camera_matrix = uniform_data.proj_matrix * uniform_data.view_matrix;
+ \\ let matrix = camera_matrix * model_matrix;
+ \\ return VertexOutput(matrix * in.position, in.normal);
\\ }
;
const vs_module = app.device.createShaderModuleWGSL("default vertex shader", vs);
@@ -626,11 +682,19 @@ const UnlitRenderPipeline = struct {
.targets = &.{color_target},
});
+ const buffer_descriptor = gpu.Buffer.Descriptor{
+ .size = @sizeOf(UniformData),
+ .usage = .{ .uniform = true, .copy_dst = true },
+ .mapped_at_creation = .false,
+ };
+ const uniform_buffer = app.device.createBuffer(&buffer_descriptor);
+
const bind_group_layout = UnlitRenderPipeline.bindGroupLayout(app.device);
const bind_group_descriptor = gpu.BindGroup.Descriptor.init(.{
.layout = bind_group_layout,
.entries = &.{
gpu.BindGroup.Entry.buffer(0, mesh_buffer.instances.data, 0, mesh_buffer.instances.size),
+ gpu.BindGroup.Entry.buffer(1, uniform_buffer, 0, @sizeOf(UniformData)),
},
});
const bind_group = app.device.createBindGroup(&bind_group_descriptor);
@@ -645,13 +709,17 @@ const UnlitRenderPipeline = struct {
.label = "default render pipeline",
.fragment = &fragment,
.layout = pipeline_layout,
- .depth_stencil = null,
+ .depth_stencil = &.{
+ .format = .depth24_plus,
+ .depth_write_enabled = .true,
+ .depth_compare = .less,
+ },
.vertex = vertex,
.multisample = .{},
.primitive = .{
.topology = .triangle_list,
.front_face = .ccw,
- .cull_mode = .none,
+ .cull_mode = .back,
},
};
@@ -659,6 +727,7 @@ const UnlitRenderPipeline = struct {
.gpu_pipeline = app.device.createRenderPipeline(&pipeline_descriptor),
.mesh_buffer = mesh_buffer,
.bind_group = bind_group,
+ .uniform_buffer = uniform_buffer,
};
}
@@ -694,6 +763,10 @@ const UnlitRenderPipeline = struct {
.vertex = true,
.fragment = false,
}, .read_only_storage, false, 0),
+ gpu.BindGroupLayout.Entry.buffer(1, .{
+ .vertex = true,
+ .fragment = false,
+ }, .uniform, false, 0),
},
});
return device.createBindGroupLayout(&descriptor);
@@ -731,6 +804,9 @@ pub fn main() !void {
try app.init(allocator);
defer app.deinit();
+ var camera = Camera{};
+ camera.logParams();
+
var mesh_buffer = MeshBuffer.init(.{
.device = app.device,
.queue = app.queue,
@@ -744,7 +820,7 @@ pub fn main() !void {
.allocator = allocator,
.mesh_buffer = &mesh_buffer,
.path = "bunny-fixed.obj",
- .scale = -1.5,
+ .scale = 1.5,
});
_ = try load_obj.loadFile(.{
.allocator = allocator,
@@ -758,8 +834,20 @@ pub fn main() !void {
const pipelines = [_]RenderPipeline{drp.pipeline()};
+ try app.inputs.append(camera.input.userInput());
+
+ var uniform_data = UniformData{
+ .view_matrix = undefined,
+ .proj_matrix = undefined,
+ };
while (!app.window.shouldClose()) {
glfw.pollEvents();
+
+ camera.update(0.01);
+ camera.view(&uniform_data.view_matrix);
+ camera.proj(&uniform_data.proj_matrix);
+ app.queue.writeBuffer(drp.uniform_buffer, 0, &[1]UniformData{uniform_data});
+
try app.frame(&pipelines);
}
}
@@ -783,3 +871,20 @@ pub fn msgSend(obj: anytype, sel_name: [:0]const u8, args: anytype, comptime Ret
return @call(.auto, func, .{ obj, sel } ++ args);
}
+
+fn glfwKeyCallback(
+ window: glfw.Window,
+ key: glfw.Key,
+ scancode: i32,
+ action: glfw.Action,
+ mods: glfw.Mods,
+) void {
+ const app = window.getUserPointer(App).?;
+ app.keyCallback(.{
+ .window = window,
+ .key = key,
+ .scancode = scancode,
+ .action = action,
+ .mods = mods,
+ });
+}