diff --git a/src/main.zig b/src/main.zig index a432593..8c91fc7 100644 --- a/src/main.zig +++ b/src/main.zig @@ -17,6 +17,16 @@ const ObjectUniformBuffer = struct { color: [3]f32, }; +/// Holds data on what is needed to render an object in a rendering pass. +const ObjectData = struct { + /// Reference to data stored on the GPU of type `ObjectUniformBuffer`. + uniform_buffer: *gpu.Buffer, + /// Bind group used to associate the buffer to the `object` shader parameter. + uniform_bind_group: *gpu.BindGroup, + /// Index into `primitives` to specify which primitive to render. + primitive_index: usize, +}; + /// Describes the layout of each vertex that a primitive is made of. const VertexData = struct { position: [3]f32, @@ -45,14 +55,9 @@ app_timer: core.Timer, title_timer: core.Timer, pipeline: *gpu.RenderPipeline, - scene_uniform_buffer: *gpu.Buffer, -scene_bind_group: *gpu.BindGroup, - -object_uniform_buffers: [3]*gpu.Buffer, -object_bind_groups: [3]*gpu.BindGroup, -object_primitive_indices: [3]usize, - +scene_uniform_bind_group: *gpu.BindGroup, +object_data: [3]ObjectData, primitives: [2]PrimitiveData, pub fn init(app: *App) !void { @@ -91,69 +96,63 @@ pub fn init(app: *App) !void { }, }); - // Set up uniform buffers and bind groups. + // Set up scene related uniform buffers and bind groups. + { + const result = createAndWriteUniformBuffer( + app.pipeline.getBindGroupLayout(0), + SceneUniformBuffer{ .view_proj_matrix = zm.identity() }, + ); - // The "scene" uniform contains information for each rendered scene. - app.scene_uniform_buffer = core.device.createBuffer(&.{ - .usage = .{ .copy_dst = true, .uniform = true }, - .size = @sizeOf(SceneUniformBuffer), - .mapped_at_creation = .false, - }); - // "Bind groups" are used to associate data from buffers with shader parameters. - // So for example the `scene_bind_group` is accessible via `scene` in our shader. - // Essentially, buffer = data, and bind group = binding parameter to that data. - app.scene_bind_group = core.device.createBindGroup( - &gpu.BindGroup.Descriptor.init(.{ - .layout = app.pipeline.getBindGroupLayout(0), - .entries = &.{ - gpu.BindGroup.Entry.buffer(0, app.scene_uniform_buffer, 0, @sizeOf(SceneUniformBuffer)), - }, - }), - ); + app.scene_uniform_buffer = result.buffer; + app.scene_uniform_bind_group = result.bind_group; + } - // The "object" uniforms contain information about how to render each object in a scene. - for (0..3) |i| { - app.object_uniform_buffers[i] = core.device.createBuffer(&.{ - .usage = .{ .copy_dst = true, .uniform = true }, - .size = @sizeOf(ObjectUniformBuffer), - .mapped_at_creation = .false, - }); - app.object_bind_groups[i] = core.device.createBindGroup( - &gpu.BindGroup.Descriptor.init(.{ - .layout = app.pipeline.getBindGroupLayout(1), - .entries = &.{ - gpu.BindGroup.Entry.buffer(0, app.object_uniform_buffers[i], 0, @sizeOf(ObjectUniformBuffer)), + // Set up object related uniform buffers and bind groups. + // This uploads data to the GPU about all the object we + // want to render, such as their location and color. + { + const ObjectDescription = struct { pos: [3]f32, color: [3]f32, prim: usize }; + const object_desc = [_]ObjectDescription{ + // zig fmt: off + .{ .pos = .{ -1.25, 0.25, 0.0 }, .color = .{ 1.0, 0.0, 0.0 }, .prim = 0 }, + .{ .pos = .{ 0.0 , -0.25, 0.0 }, .color = .{ 0.0, 1.0, 0.0 }, .prim = 1 }, + .{ .pos = .{ 1.25, 0.0 , 0.0 }, .color = .{ 0.0, 0.0, 1.0 }, .prim = 0 }, + // zig fmt: on + }; + + // The objects are rotated 180° to face the camera, or else we + // would see the back side of the triangles, which are culled. + const rotation = zm.rotationY(std.math.tau / 2.0); + + for (object_desc, &app.object_data) |desc, *object| { + const translation = zm.translation(desc.pos[0], desc.pos[1], desc.pos[2]); + const model_matrix = zm.mul(rotation, translation); + + const result = createAndWriteUniformBuffer( + app.pipeline.getBindGroupLayout(1), + ObjectUniformBuffer{ + .model_matrix = zm.transpose(model_matrix), + .color = desc.color, }, - }), - ); + ); + + object.* = .{ + .uniform_buffer = result.buffer, + .uniform_bind_group = result.bind_group, + .primitive_index = desc.prim, + }; + } } - // Upload object information (model matrix + color) to the GPU. - const rotation = zm.rotationY(std.math.tau / 2.0); - // The objects are rotated 180° to face the camera, or else we - // would see the back side of the triangles, which are culled. - core.queue.writeBuffer(app.object_uniform_buffers[0], 0, &[_]ObjectUniformBuffer{.{ - .model_matrix = zm.transpose(zm.mul(rotation, zm.translation(-1.0, 0.25, 0.0))), - .color = .{ 1.0, 0.0, 0.0 }, - }}); - app.object_primitive_indices[0] = 0; - core.queue.writeBuffer(app.object_uniform_buffers[1], 0, &[_]ObjectUniformBuffer{.{ - .model_matrix = zm.transpose(zm.mul(rotation, zm.translation(0.0, -0.25, 0.0))), - .color = .{ 0.0, 1.0, 0.0 }, - }}); - app.object_primitive_indices[1] = 1; - core.queue.writeBuffer(app.object_uniform_buffers[2], 0, &[_]ObjectUniformBuffer{.{ - .model_matrix = zm.transpose(zm.mul(rotation, zm.translation(1.0, 0.0, 0.0))), - .color = .{ 0.0, 0.0, 1.0 }, - }}); - app.object_primitive_indices[2] = 0; // Set up the primitives we want to render. // Triangle app.primitives[0] = createPrimitive( &.{ - .{ .position = .{ 0.0, 0.5, 0.0 } }, - .{ .position = .{ 0.5, -0.5, 0.0 } }, + // zig fmt: off + .{ .position = .{ 0.0, 0.5, 0.0 } }, + .{ .position = .{ 0.5, -0.5, 0.0 } }, .{ .position = .{ -0.5, -0.5, 0.0 } }, + // zig fmt: on }, // Note that the back faces of triangles are "culled", and thus not visible. // We need to take care to specify the vertices in counter-clock orientation. @@ -163,20 +162,25 @@ pub fn init(app: *App) !void { ); // Square app.primitives[1] = createPrimitive( - // 0--2 - // | | - // | | - // 1--3 + // A square is made up of 4 vertices. + // 0--2 + // | | + // | | + // 1--3 &.{ + // zig fmt: off .{ .position = .{ -0.5, -0.5, 0.0 } }, - .{ .position = .{ -0.5, 0.5, 0.0 } }, - .{ .position = .{ 0.5, -0.5, 0.0 } }, - .{ .position = .{ 0.5, 0.5, 0.0 } }, + .{ .position = .{ -0.5, 0.5, 0.0 } }, + .{ .position = .{ 0.5, -0.5, 0.0 } }, + .{ .position = .{ 0.5, 0.5, 0.0 } }, + // zig fmt: on }, - // 0--2 4 - // | / /| - // |/ / | - // 1 5--3 + // But it has to be split up into 2 triangles, + // specified in a counter-clockwise orientation. + // 0--2 4 + // | / /| + // |/ / | + // 1 5--3 &.{ 0, 1, 2, 3, 2, 1, @@ -184,6 +188,30 @@ pub fn init(app: *App) !void { ); } +/// Creates a buffer on the GPU to store uniform parameter information as +/// well as a bind group with the specified layout pointing to that buffer. +/// Additionally, immediately fills the buffer with the provided data. +fn createAndWriteUniformBuffer( + layout: *gpu.BindGroupLayout, + data: anytype, +) struct { + buffer: *gpu.Buffer, + bind_group: *gpu.BindGroup, +} { + const T = @TypeOf(data); + const usage = gpu.Buffer.UsageFlags{ .copy_dst = true, .uniform = true }; + const buffer = createAndWriteBuffer(T, &.{data}, usage); + + // "Bind groups" are used to associate data from buffers with shader parameters. + // So for example the `scene_uniform_bind_group` is accessible via `scene` in our shader. + // Essentially, buffer = data, and bind group = binding parameter to that data. + const bind_group_entry = gpu.BindGroup.Entry.buffer(0, buffer, 0, @sizeOf(T)); + const bind_group_desc = gpu.BindGroup.Descriptor.init(.{ .layout = layout, .entries = &.{bind_group_entry} }); + const bind_group = core.device.createBindGroup(&bind_group_desc); + + return .{ .buffer = buffer, .bind_group = bind_group }; +} + /// Creates a buffer on the GPU with the specified usage /// flags and immediately fills it with the provided data. fn createAndWriteBuffer( @@ -200,8 +228,8 @@ fn createAndWriteBuffer( return buffer; } -// Creates a primitive from the provided vertices and indices, -// and uploads the buffers necessary to render it to the GPU. +/// Creates a primitive from the provided vertices and indices, +/// and uploads the buffers necessary to render it to the GPU. fn createPrimitive( vertices: []const VertexData, indices: []const u32, @@ -220,9 +248,11 @@ pub fn deinit(app: *App) void { defer core.deinit(); defer app.pipeline.release(); defer app.scene_uniform_buffer.release(); - defer app.scene_bind_group.release(); - defer for (app.object_uniform_buffers) |b| b.release(); - defer for (app.object_bind_groups) |g| g.release(); + defer app.scene_uniform_bind_group.release(); + defer for (app.object_data) |o| { + o.uniform_buffer.release(); + o.uniform_bind_group.release(); + }; defer for (app.primitives) |p| { p.vertex_buffer.release(); p.index_buffer.release(); @@ -288,18 +318,18 @@ pub fn update(app: *App) !bool { defer pass.end(); pass.setPipeline(app.pipeline); - pass.setBindGroup(0, app.scene_bind_group, &.{}); + pass.setBindGroup(0, app.scene_uniform_bind_group, &.{}); - for (app.object_bind_groups, 0..) |object_bind_group, i| { + for (app.object_data) |object| { // Set the vertex and index buffer used to render this object // to the primitive it wants to use (either triangle or square). - const primitive_index = app.object_primitive_indices[i]; + const primitive_index = object.primitive_index; const primitive = app.primitives[primitive_index]; pass.setVertexBuffer(0, primitive.vertex_buffer, 0, primitive.vertex_count * @sizeOf(VertexData)); pass.setIndexBuffer(primitive.index_buffer, .uint32, 0, primitive.index_count * @sizeOf(u32)); // Set the bind group for an object we want to render. - pass.setBindGroup(1, object_bind_group, &.{}); + pass.setBindGroup(1, object.uniform_bind_group, &.{}); // Draw a number of triangles as specified in the index buffer. pass.drawIndexed(primitive.index_count, 1, 0, 0, 0);