render-zig

A 3D rendering engine written in Zig
git clone git://git.christianermann.dev/render-zig
Log | Files | Refs

commit 0fd02f7226880b8e349ce4c58b490ee885715921
parent 848206676e741c9356e82718989aef6f01f93f42
Author: Christian Ermann <christianermann@gmail.com>
Date:   Wed, 15 May 2024 16:37:34 -0400

Add interactive camera

Diffstat:
Asrc/camera.zig | 126+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/input.zig | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/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, + }); +}