diff --git a/.gitignore b/.gitignore index 4674f89..d8b5c00 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,6 @@ /zig-out/ # Dependencies (cloned manually) -/libs/flecs-zig-ble/ -/libs/zig-gamedev/ +# Could be symlinks, so not including trailing slash. +/libs/flecs-zig-ble +/libs/zig-gamedev diff --git a/build.zig b/build.zig index 4495509..9784814 100644 --- a/build.zig +++ b/build.zig @@ -18,7 +18,7 @@ pub fn build(b: *std.Build) !void { const app = try mach.CoreApp.init(b, mach_dep.builder, .{ .name = "zig-bloxel-game", - .src = "src/main.zig", + .src = "src/App.zig", .target = target, .optimize = optimize, .deps = &.{ @@ -36,7 +36,7 @@ pub fn build(b: *std.Build) !void { run_step.dependOn(&app.run.step); const unit_tests = b.addTest(.{ - .root_source_file = .{ .path = "src/main.zig" }, + .root_source_file = .{ .path = "src/App.zig" }, .target = target, .optimize = optimize, }); diff --git a/src/main.zig b/src/App.zig similarity index 64% rename from src/main.zig rename to src/App.zig index 0616ee7..b42354f 100644 --- a/src/main.zig +++ b/src/App.zig @@ -2,18 +2,15 @@ const std = @import("std"); const GeneralPurposeAllocator = std.heap.GeneralPurposeAllocator(.{}); const core = @import("mach").core; -const Renderer = @import("./renderer.zig"); +const Renderer = @import("./Renderer.zig"); const flecszigble = @import("flecs-zig-ble"); +const flecs = flecszigble.flecs; + const Context = flecszigble.Context(void); const World = Context.World; const Iter = Context.Iter; -const flecs = flecszigble.flecs; -const OnLoad = flecs.pipeline.OnLoad; -const OnUpdate = flecs.pipeline.OnUpdate; -const OnStore = flecs.pipeline.OnStore; - pub const App = @This(); gpa: GeneralPurposeAllocator, @@ -57,14 +54,10 @@ pub fn init(app: *App) !void { app.world = world; // Create a singleton component for accessing the `App` from ECS. - _ = try world.singleton("App", *App, app); - - // TODO: The way we register systems using flecs-zig-ble is still very WIP. - _ = try world.system("PollEvents", pollEvents, OnLoad, "App"); + _ = try world.singleton(*App, app); - const s = try world.system("UpdateWindowTitle", updateWindowTitle, OnStore, ""); - // Set the update interval of the `UpdateWindowTitle` system to 1 second. - _ = flecszigble.c.ecs_set_interval(world.raw, s.raw, 1.0); + _ = try world.system(PollEvents); + _ = try world.system(UpdateWindowTitle); app.renderer = try Renderer.init(app); } @@ -84,30 +77,36 @@ pub fn update(app: *App) !bool { return !app.world.progress(0.0); } -/// Read events from the OS such as input. -pub fn pollEvents(it: Iter) void { - const app = it.field(*App, 1)[0]; - - var pollIter = core.pollEvents(); - while (pollIter.next()) |event| { - switch (event) { - // Allow the renderer to act on the window being resized. - // This is required so we can resize necessary buffers. - .framebuffer_resize => |_| app.renderer.resize(), - - // Close the window when requested, such as when - // pressing the X button in the window title bar. - .close => it.world.quit(), - - else => {}, +/// System that reads events from the OS such as input. +pub const PollEvents = struct { + pub const phase = flecs.pipeline.OnLoad; + pub const expr = "App($)"; + pub fn callback(world: *World, app: *const *App) void { + var pollIter = core.pollEvents(); + while (pollIter.next()) |event| { + switch (event) { + // Allow the renderer to act on the window being resized. + // This is required so we can resize necessary buffers. + .framebuffer_resize => |_| app.*.renderer.resize(), + + // Close the window when requested, such as when + // pressing the X button in the window title bar. + .close => world.quit(), + + else => {}, + } } } -} - -/// Update the window title to show FPS and input frequency. -pub fn updateWindowTitle(_: Iter) void { - core.printTitle( - "Triangle [ {d}fps ] [ Input {d}hz ]", - .{ core.frameRate(), core.inputRate() }, - ) catch @panic("Title too long!"); -} +}; + +/// System that updates the window title to show FPS and input frequency. +pub const UpdateWindowTitle = struct { + pub const phase = flecs.pipeline.OnStore; + pub const interval = 1.0; // Run only once a second. + pub fn callback(_: Iter) void { + core.printTitle( + "Triangle [ {d}fps ] [ Input {d}hz ]", + .{ core.frameRate(), core.inputRate() }, + ) catch @panic("Title too long!"); + } +}; diff --git a/src/renderer.zig b/src/Renderer.zig similarity index 74% rename from src/renderer.zig rename to src/Renderer.zig index cb4d310..55128ce 100644 --- a/src/renderer.zig +++ b/src/Renderer.zig @@ -11,20 +11,19 @@ const zm = @import("zmath"); const vec = zm.f32x4; const Mat = zm.Mat; -const App = @import("./main.zig"); +const App = @import("./App.zig"); const primitives = @import("./primitives.zig"); const VertexData = primitives.VertexData; const PrimitiveData = primitives.PrimitiveData; const flecszigble = @import("flecs-zig-ble"); +const flecs = flecszigble.flecs; + const Context = flecszigble.Context(void); const Entity = Context.Entity; const Iter = Context.Iter; -const flecs = flecszigble.flecs; -const OnStore = flecs.pipeline.OnStore; - const Transform = struct { value: Mat }; const CameraPerspective = struct { /// Vertical field of view (in degrees). @@ -231,8 +230,8 @@ pub fn init(app: *App) !*Renderer { } // Register components necessary for the camera. - _ = try app.world.component("Transform", Transform); - _ = try app.world.component("CameraPerspective", CameraPerspective); + _ = try app.world.component(Transform); + _ = try app.world.component(CameraPerspective); const camera_entity = try app.world.entity( .{ .name = "Camera", .symbol = "Camera" }, @@ -244,8 +243,7 @@ pub fn init(app: *App) !*Renderer { .far_plane = 80.0, }); - const render_expr = "App, [in] CameraPerspective(Camera), [out] Transform(Camera)"; - _ = try app.world.system("Render", render, OnStore, render_expr); + _ = try app.world.system(Render); const result = try app.allocator.create(Renderer); result.* = .{ @@ -293,105 +291,110 @@ pub fn resize(self: *Renderer) void { self.recreateDepthTexture(); } -pub fn render(it: Iter) void { - const app = it.field(*App, 1)[0]; - const camera_perspective = it.field(CameraPerspective, 2)[0]; - const camera_transform = &it.field(Transform, 3)[0]; - - const self = app.renderer; - self.time += it.deltaTime(); - - // Set up a view matrix from the camera transform. - // This moves everything to be relative to the camera. - // TODO: Actually implement camera transform instead of hardcoding a look-at matrix. - // const view_matrix = zm.inverse(app.camera_transform); - const camera_distance = 8.0; - const x = @cos(self.time * std.math.tau / 20) * camera_distance; - const z = @sin(self.time * std.math.tau / 20) * camera_distance; - const camera_pos = vec(x, 2.0, z, 1.0); - const view_matrix = zm.lookAtLh(camera_pos, vec(0, 0, 0, 1), vec(0, 1, 0, 1)); - - // Setting the transform here doesn't do anything because it's not used - // anywhere. In the future we would want to set the camera transform - // outside of the rendering step, and then get and use it here, instead. - camera_transform.* = .{ .value = view_matrix }; - // TODO: Not sure if this is the proper transform, or actually inverted. - - // Set up a projection matrix using the size of the window. - // The perspective projection will make things further away appear smaller. - const width: f32 = @floatFromInt(core.descriptor.width); - const height: f32 = @floatFromInt(core.descriptor.height); - const proj_matrix = zm.perspectiveFovLh( - std.math.degreesToRadians(f32, camera_perspective.field_of_view), - width / height, - camera_perspective.near_plane, - camera_perspective.far_plane, - ); - - const view_proj_matrix = zm.mul(view_matrix, proj_matrix); - - // Get back buffer texture to render to. - const back_buffer_view = core.swap_chain.getCurrentTextureView().?; - defer back_buffer_view.release(); - // Once rendering is done (hence `defer`), swap back buffer to the front to display. - defer core.swap_chain.present(); - - const render_pass_info = gpu.RenderPassDescriptor.init(.{ - .color_attachments = &.{.{ - .view = back_buffer_view, - .clear_value = std.mem.zeroes(gpu.Color), - .load_op = .clear, - .store_op = .store, - }}, - .depth_stencil_attachment = &.{ - .view = self.depth_texture_view.?, - .depth_load_op = .clear, - .depth_store_op = .store, - .depth_clear_value = 1.0, - }, - }); - - // Create a `WGPUCommandEncoder` which provides an interface for recording GPU commands. - const encoder = core.device.createCommandEncoder(null); - defer encoder.release(); - - // Write to the scene uniform buffer for this set of commands. - encoder.writeBuffer(self.view_proj_buffer, 0, &[_]zm.Mat{ - // All matrices the GPU has to work with need to be transposed, - // because WebGPU uses column-major matrices while zmath is row-major. - zm.transpose(view_proj_matrix), - }); - - { - const pass = encoder.beginRenderPass(&render_pass_info); - defer pass.release(); - defer pass.end(); - - pass.setPipeline(self.pipeline); - pass.setBindGroup(0, self.camera_bind_group, &.{}); - - for (self.object_data) |object| { - // Set the vertex and index buffer used to render this - // object to the ones from the primitive it wants to use. - const prim = object.primitive; - pass.setVertexBuffer(0, prim.vertex_buffer, 0, prim.vertex_count * @sizeOf(VertexData)); - pass.setIndexBuffer(prim.index_buffer, .uint32, 0, prim.index_count * @sizeOf(u32)); - - // Set the bind group for the object we want to render. - pass.setBindGroup(1, object.model_bind_group, &.{}); - - // Draw a number of triangles as specified in the index buffer. - pass.drawIndexed(prim.index_count, 1, 0, 0, 0); +/// System which renders the game world from the camera entity's perspective. +pub const Render = struct { + pub const phase = flecs.pipeline.OnStore; + pub const expr = "App($), [in] CameraPerspective(Camera), [out] Transform(Camera)"; + pub fn callback(it: Iter) void { + const app = it.field(*App, 1)[0]; + const camera_perspective = it.field(CameraPerspective, 2)[0]; + const camera_transform = &it.field(Transform, 3)[0]; + + const self = app.renderer; + self.time += it.deltaTime(); + + // Set up a view matrix from the camera transform. + // This moves everything to be relative to the camera. + // TODO: Actually implement camera transform instead of hardcoding a look-at matrix. + // const view_matrix = zm.inverse(app.camera_transform); + const camera_distance = 8.0; + const x = @cos(self.time * std.math.tau / 20) * camera_distance; + const z = @sin(self.time * std.math.tau / 20) * camera_distance; + const camera_pos = vec(x, 2.0, z, 1.0); + const view_matrix = zm.lookAtLh(camera_pos, vec(0, 0, 0, 1), vec(0, 1, 0, 1)); + + // Setting the transform here doesn't do anything because it's not used + // anywhere. In the future we would want to set the camera transform + // outside of the rendering step, and then get and use it here, instead. + camera_transform.* = .{ .value = view_matrix }; + // TODO: Not sure if this is the proper transform, or actually inverted. + + // Set up a projection matrix using the size of the window. + // The perspective projection will make things further away appear smaller. + const width: f32 = @floatFromInt(core.descriptor.width); + const height: f32 = @floatFromInt(core.descriptor.height); + const proj_matrix = zm.perspectiveFovLh( + std.math.degreesToRadians(f32, camera_perspective.field_of_view), + width / height, + camera_perspective.near_plane, + camera_perspective.far_plane, + ); + + const view_proj_matrix = zm.mul(view_matrix, proj_matrix); + + // Get back buffer texture to render to. + const back_buffer_view = core.swap_chain.getCurrentTextureView().?; + defer back_buffer_view.release(); + // Once rendering is done (hence `defer`), swap back buffer to the front to display. + defer core.swap_chain.present(); + + const render_pass_info = gpu.RenderPassDescriptor.init(.{ + .color_attachments = &.{.{ + .view = back_buffer_view, + .clear_value = std.mem.zeroes(gpu.Color), + .load_op = .clear, + .store_op = .store, + }}, + .depth_stencil_attachment = &.{ + .view = self.depth_texture_view.?, + .depth_load_op = .clear, + .depth_store_op = .store, + .depth_clear_value = 1.0, + }, + }); + + // Create a `WGPUCommandEncoder` which provides an interface for recording GPU commands. + const encoder = core.device.createCommandEncoder(null); + defer encoder.release(); + + // Write to the scene uniform buffer for this set of commands. + encoder.writeBuffer(self.view_proj_buffer, 0, &[_]zm.Mat{ + // All matrices the GPU has to work with need to be transposed, + // because WebGPU uses column-major matrices while zmath is row-major. + zm.transpose(view_proj_matrix), + }); + + { + const pass = encoder.beginRenderPass(&render_pass_info); + defer pass.release(); + defer pass.end(); + + pass.setPipeline(self.pipeline); + pass.setBindGroup(0, self.camera_bind_group, &.{}); + + for (self.object_data) |object| { + // Set the vertex and index buffer used to render this + // object to the ones from the primitive it wants to use. + const prim = object.primitive; + pass.setVertexBuffer(0, prim.vertex_buffer, 0, prim.vertex_count * @sizeOf(VertexData)); + pass.setIndexBuffer(prim.index_buffer, .uint32, 0, prim.index_count * @sizeOf(u32)); + + // Set the bind group for the object we want to render. + pass.setBindGroup(1, object.model_bind_group, &.{}); + + // Draw a number of triangles as specified in the index buffer. + pass.drawIndexed(prim.index_count, 1, 0, 0, 0); + } } - } - // Finish recording commands, creating a `WGPUCommandBuffer`. - var command = encoder.finish(null); - defer command.release(); + // Finish recording commands, creating a `WGPUCommandBuffer`. + var command = encoder.finish(null); + defer command.release(); - // Submit the command(s) to the GPU. - core.queue.submit(&.{command}); -} + // Submit the command(s) to the GPU. + core.queue.submit(&.{command}); + } +}; /// Loads a texture from the provided buffer and uploads it to the GPU. pub fn loadTexture(allocator: std.mem.Allocator, buffer: []const u8) !*gpu.TextureView { diff --git a/src/primitives.zig b/src/primitives.zig index 4e1a890..15765cb 100644 --- a/src/primitives.zig +++ b/src/primitives.zig @@ -3,7 +3,7 @@ const tau = std.math.tau; const gpu = @import("mach").core.gpu; -const Renderer = @import("./renderer.zig"); +const Renderer = @import("./Renderer.zig"); const createAndWriteBuffer = Renderer.createAndWriteBuffer; /// Describes the layout of each vertex that a primitive is made of.