Compare commits

..

No commits in common. 'wip/new-system-init' and 'main' have entirely different histories.

  1. 5
      .gitignore
  2. 4
      build.zig
  3. 75
      src/main.zig
  4. 2
      src/primitives.zig
  5. 211
      src/renderer.zig

5
.gitignore vendored

@ -3,6 +3,5 @@
/zig-out/ /zig-out/
# Dependencies (cloned manually) # Dependencies (cloned manually)
# Could be symlinks, so not including trailing slash. /libs/flecs-zig-ble/
/libs/flecs-zig-ble /libs/zig-gamedev/
/libs/zig-gamedev

@ -18,7 +18,7 @@ pub fn build(b: *std.Build) !void {
const app = try mach.CoreApp.init(b, mach_dep.builder, .{ const app = try mach.CoreApp.init(b, mach_dep.builder, .{
.name = "zig-bloxel-game", .name = "zig-bloxel-game",
.src = "src/App.zig", .src = "src/main.zig",
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
.deps = &.{ .deps = &.{
@ -36,7 +36,7 @@ pub fn build(b: *std.Build) !void {
run_step.dependOn(&app.run.step); run_step.dependOn(&app.run.step);
const unit_tests = b.addTest(.{ const unit_tests = b.addTest(.{
.root_source_file = .{ .path = "src/App.zig" }, .root_source_file = .{ .path = "src/main.zig" },
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
}); });

@ -2,15 +2,18 @@ const std = @import("std");
const GeneralPurposeAllocator = std.heap.GeneralPurposeAllocator(.{}); const GeneralPurposeAllocator = std.heap.GeneralPurposeAllocator(.{});
const core = @import("mach").core; const core = @import("mach").core;
const Renderer = @import("./Renderer.zig"); const Renderer = @import("./renderer.zig");
const flecszigble = @import("flecs-zig-ble"); const flecszigble = @import("flecs-zig-ble");
const flecs = flecszigble.flecs;
const Context = flecszigble.Context(void); const Context = flecszigble.Context(void);
const World = Context.World; const World = Context.World;
const Iter = Context.Iter; 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(); pub const App = @This();
gpa: GeneralPurposeAllocator, gpa: GeneralPurposeAllocator,
@ -54,10 +57,14 @@ pub fn init(app: *App) !void {
app.world = world; app.world = world;
// Create a singleton component for accessing the `App` from ECS. // Create a singleton component for accessing the `App` from ECS.
_ = try world.singleton(*App, app); _ = 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.system(PollEvents); const s = try world.system("UpdateWindowTitle", updateWindowTitle, OnStore, "");
_ = try world.system(UpdateWindowTitle); // Set the update interval of the `UpdateWindowTitle` system to 1 second.
_ = flecszigble.c.ecs_set_interval(world.raw, s.raw, 1.0);
app.renderer = try Renderer.init(app); app.renderer = try Renderer.init(app);
} }
@ -77,36 +84,30 @@ pub fn update(app: *App) !bool {
return !app.world.progress(0.0); return !app.world.progress(0.0);
} }
/// System that reads events from the OS such as input. /// Read events from the OS such as input.
pub const PollEvents = struct { pub fn pollEvents(it: Iter) void {
pub const phase = flecs.pipeline.OnLoad; const app = it.field(*App, 1)[0];
pub const expr = "App($)";
pub fn callback(world: *World, app: *const *App) void { var pollIter = core.pollEvents();
var pollIter = core.pollEvents(); while (pollIter.next()) |event| {
while (pollIter.next()) |event| { switch (event) {
switch (event) { // Allow the renderer to act on the window being resized.
// Allow the renderer to act on the window being resized. // This is required so we can resize necessary buffers.
// This is required so we can resize necessary buffers. .framebuffer_resize => |_| app.renderer.resize(),
.framebuffer_resize => |_| app.*.renderer.resize(),
// Close the window when requested, such as when
// Close the window when requested, such as when // pressing the X button in the window title bar.
// pressing the X button in the window title bar. .close => it.world.quit(),
.close => world.quit(),
else => {},
else => {},
}
} }
} }
}; }
/// System that updates the window title to show FPS and input frequency. /// Update the window title to show FPS and input frequency.
pub const UpdateWindowTitle = struct { pub fn updateWindowTitle(_: Iter) void {
pub const phase = flecs.pipeline.OnStore; core.printTitle(
pub const interval = 1.0; // Run only once a second. "Triangle [ {d}fps ] [ Input {d}hz ]",
pub fn callback(_: Iter) void { .{ core.frameRate(), core.inputRate() },
core.printTitle( ) catch @panic("Title too long!");
"Triangle [ {d}fps ] [ Input {d}hz ]", }
.{ core.frameRate(), core.inputRate() },
) catch @panic("Title too long!");
}
};

@ -3,7 +3,7 @@ const tau = std.math.tau;
const gpu = @import("mach").core.gpu; const gpu = @import("mach").core.gpu;
const Renderer = @import("./Renderer.zig"); const Renderer = @import("./renderer.zig");
const createAndWriteBuffer = Renderer.createAndWriteBuffer; const createAndWriteBuffer = Renderer.createAndWriteBuffer;
/// Describes the layout of each vertex that a primitive is made of. /// Describes the layout of each vertex that a primitive is made of.

@ -11,19 +11,20 @@ const zm = @import("zmath");
const vec = zm.f32x4; const vec = zm.f32x4;
const Mat = zm.Mat; const Mat = zm.Mat;
const App = @import("./App.zig"); const App = @import("./main.zig");
const primitives = @import("./primitives.zig"); const primitives = @import("./primitives.zig");
const VertexData = primitives.VertexData; const VertexData = primitives.VertexData;
const PrimitiveData = primitives.PrimitiveData; const PrimitiveData = primitives.PrimitiveData;
const flecszigble = @import("flecs-zig-ble"); const flecszigble = @import("flecs-zig-ble");
const flecs = flecszigble.flecs;
const Context = flecszigble.Context(void); const Context = flecszigble.Context(void);
const Entity = Context.Entity; const Entity = Context.Entity;
const Iter = Context.Iter; const Iter = Context.Iter;
const flecs = flecszigble.flecs;
const OnStore = flecs.pipeline.OnStore;
const Transform = struct { value: Mat }; const Transform = struct { value: Mat };
const CameraPerspective = struct { const CameraPerspective = struct {
/// Vertical field of view (in degrees). /// Vertical field of view (in degrees).
@ -230,8 +231,8 @@ pub fn init(app: *App) !*Renderer {
} }
// Register components necessary for the camera. // Register components necessary for the camera.
_ = try app.world.component(Transform); _ = try app.world.component("Transform", Transform);
_ = try app.world.component(CameraPerspective); _ = try app.world.component("CameraPerspective", CameraPerspective);
const camera_entity = try app.world.entity( const camera_entity = try app.world.entity(
.{ .name = "Camera", .symbol = "Camera" }, .{ .name = "Camera", .symbol = "Camera" },
@ -243,7 +244,8 @@ pub fn init(app: *App) !*Renderer {
.far_plane = 80.0, .far_plane = 80.0,
}); });
_ = try app.world.system(Render); const render_expr = "App, [in] CameraPerspective(Camera), [out] Transform(Camera)";
_ = try app.world.system("Render", render, OnStore, render_expr);
const result = try app.allocator.create(Renderer); const result = try app.allocator.create(Renderer);
result.* = .{ result.* = .{
@ -291,110 +293,105 @@ pub fn resize(self: *Renderer) void {
self.recreateDepthTexture(); self.recreateDepthTexture();
} }
/// System which renders the game world from the camera entity's perspective. pub fn render(it: Iter) void {
pub const Render = struct { const app = it.field(*App, 1)[0];
pub const phase = flecs.pipeline.OnStore; const camera_perspective = it.field(CameraPerspective, 2)[0];
pub const expr = "App($), [in] CameraPerspective(Camera), [out] Transform(Camera)"; const camera_transform = &it.field(Transform, 3)[0];
pub fn callback(it: Iter) void {
const app = it.field(*App, 1)[0]; const self = app.renderer;
const camera_perspective = it.field(CameraPerspective, 2)[0]; self.time += it.deltaTime();
const camera_transform = &it.field(Transform, 3)[0];
// Set up a view matrix from the camera transform.
const self = app.renderer; // This moves everything to be relative to the camera.
self.time += it.deltaTime(); // TODO: Actually implement camera transform instead of hardcoding a look-at matrix.
// const view_matrix = zm.inverse(app.camera_transform);
// Set up a view matrix from the camera transform. const camera_distance = 8.0;
// This moves everything to be relative to the camera. const x = @cos(self.time * std.math.tau / 20) * camera_distance;
// TODO: Actually implement camera transform instead of hardcoding a look-at matrix. const z = @sin(self.time * std.math.tau / 20) * camera_distance;
// const view_matrix = zm.inverse(app.camera_transform); const camera_pos = vec(x, 2.0, z, 1.0);
const camera_distance = 8.0; const view_matrix = zm.lookAtLh(camera_pos, vec(0, 0, 0, 1), vec(0, 1, 0, 1));
const x = @cos(self.time * std.math.tau / 20) * camera_distance;
const z = @sin(self.time * std.math.tau / 20) * camera_distance; // Setting the transform here doesn't do anything because it's not used
const camera_pos = vec(x, 2.0, z, 1.0); // anywhere. In the future we would want to set the camera transform
const view_matrix = zm.lookAtLh(camera_pos, vec(0, 0, 0, 1), vec(0, 1, 0, 1)); // outside of the rendering step, and then get and use it here, instead.
camera_transform.* = .{ .value = view_matrix };
// Setting the transform here doesn't do anything because it's not used // TODO: Not sure if this is the proper transform, or actually inverted.
// 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. // Set up a projection matrix using the size of the window.
camera_transform.* = .{ .value = view_matrix }; // The perspective projection will make things further away appear smaller.
// TODO: Not sure if this is the proper transform, or actually inverted. const width: f32 = @floatFromInt(core.descriptor.width);
const height: f32 = @floatFromInt(core.descriptor.height);
// Set up a projection matrix using the size of the window. const proj_matrix = zm.perspectiveFovLh(
// The perspective projection will make things further away appear smaller. std.math.degreesToRadians(f32, camera_perspective.field_of_view),
const width: f32 = @floatFromInt(core.descriptor.width); width / height,
const height: f32 = @floatFromInt(core.descriptor.height); camera_perspective.near_plane,
const proj_matrix = zm.perspectiveFovLh( camera_perspective.far_plane,
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`. const view_proj_matrix = zm.mul(view_matrix, proj_matrix);
var command = encoder.finish(null);
defer command.release(); // 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,
},
});
// Submit the command(s) to the GPU. // Create a `WGPUCommandEncoder` which provides an interface for recording GPU commands.
core.queue.submit(&.{command}); 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();
// Submit the command(s) to the GPU.
core.queue.submit(&.{command});
}
/// Loads a texture from the provided buffer and uploads it to the GPU. /// 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 { pub fn loadTexture(allocator: std.mem.Allocator, buffer: []const u8) !*gpu.TextureView {
Loading…
Cancel
Save