From 0776a95a1ac8077303359d2be71bcf40d4544a01 Mon Sep 17 00:00:00 2001 From: copygirl Date: Fri, 23 Feb 2024 21:52:25 +0100 Subject: [PATCH] Allow rendering of different primitives --- src/main.zig | 149 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 122 insertions(+), 27 deletions(-) diff --git a/src/main.zig b/src/main.zig index 2e3cace..a432593 100644 --- a/src/main.zig +++ b/src/main.zig @@ -6,11 +6,6 @@ const zm = @import("zmath"); const vec = zm.f32x4; const Mat = zm.Mat; -/// Describes the layout of each vertex that a primitive is made of. -const VertexData = struct { - position: [3]f32, -}; - /// Holds information about how a perticular scene should be rendered. const SceneUniformBuffer = struct { view_proj_matrix: zm.Mat, @@ -22,18 +17,43 @@ const ObjectUniformBuffer = struct { color: [3]f32, }; +/// Describes the layout of each vertex that a primitive is made of. +const VertexData = struct { + position: [3]f32, +}; + +/// Contains the data to render a primitive (3D shape or model). +const PrimitiveData = struct { + /// Vertices describe the "points" that a primitive is made out of. + /// This buffer is of type `[]VertexData`. + vertex_buffer: *gpu.Buffer, + vertex_count: u32, + + /// Indices describe what vertices make up the triangles in a primitive. + /// This buffer is of type `[]u32`. + index_buffer: *gpu.Buffer, + index_count: u32, + + // For example, `vertex_buffer` may have 4 points defining a square, but + // since it needs to be rendered using 2 triangles, `index_buffer` will + // contain 6 entries, `0, 1, 2` and `3, 2, 1` making up one triangle each. +}; + pub const App = @This(); 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, -vertex_count: u32, -vertex_buffer: *gpu.Buffer, +object_primitive_indices: [3]usize, + +primitives: [2]PrimitiveData, pub fn init(app: *App) !void { try core.init(.{}); @@ -64,6 +84,11 @@ pub fn init(app: *App) !void { .entry_point = "frag_main", .targets = &.{.{ .format = core.descriptor.format }}, }), + .primitive = .{ + .topology = .triangle_list, + .front_face = .ccw, + .cull_mode = .back, + }, }); // Set up uniform buffers and bind groups. @@ -74,6 +99,9 @@ pub fn init(app: *App) !void { .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), @@ -100,33 +128,90 @@ pub fn init(app: *App) !void { ); } // 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.translation(-1.0, 0.25, 0.0)), + .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.translation(0.0, -0.25, 0.0)), + .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.translation(1.0, 0.0, 0.0)), + .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 vertex buffer, containing the vertex data we want to draw. - const vertices = [_]VertexData{ - .{ .position = .{ 0.0, 0.5, 0.0 } }, - .{ .position = .{ -0.5, -0.5, 0.0 } }, - .{ .position = .{ 0.5, -0.5, 0.0 } }, - }; - app.vertex_count = vertices.len; - app.vertex_buffer = core.device.createBuffer(&.{ - .size = app.vertex_count * @sizeOf(VertexData), - .usage = .{ .vertex = true, .copy_dst = true }, + // 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 } }, + .{ .position = .{ -0.5, -0.5, 0.0 } }, + }, + // 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. + &.{ + 0, 1, 2, + }, + ); + // Square + app.primitives[1] = createPrimitive( + // 0--2 + // | | + // | | + // 1--3 + &.{ + .{ .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 } }, + }, + // 0--2 4 + // | / /| + // |/ / | + // 1 5--3 + &.{ + 0, 1, 2, + 3, 2, 1, + }, + ); +} + +/// Creates a buffer on the GPU with the specified usage +/// flags and immediately fills it with the provided data. +fn createAndWriteBuffer( + comptime T: type, + data: []const T, + usage: gpu.Buffer.UsageFlags, +) *gpu.Buffer { + const buffer = core.device.createBuffer(&.{ + .size = data.len * @sizeOf(T), + .usage = usage, .mapped_at_creation = .false, }); - // Upload vertex buffer to the GPU. - core.queue.writeBuffer(app.vertex_buffer, 0, &vertices); + core.queue.writeBuffer(buffer, 0, data); + return buffer; +} + +// 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, +) PrimitiveData { + return .{ + .vertex_buffer = createAndWriteBuffer(VertexData, vertices, .{ .vertex = true, .copy_dst = true }), + .vertex_count = @intCast(vertices.len), + .index_buffer = createAndWriteBuffer(u32, indices, .{ .index = true, .copy_dst = true }), + .index_count = @intCast(indices.len), + }; } pub fn deinit(app: *App) void { @@ -138,7 +223,10 @@ pub fn deinit(app: *App) void { 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.vertex_buffer.release(); + defer for (app.primitives) |p| { + p.vertex_buffer.release(); + p.index_buffer.release(); + }; } pub fn update(app: *App) !bool { @@ -201,13 +289,20 @@ pub fn update(app: *App) !bool { pass.setPipeline(app.pipeline); pass.setBindGroup(0, app.scene_bind_group, &.{}); - pass.setVertexBuffer(0, app.vertex_buffer, 0, app.vertex_count * @sizeOf(VertexData)); - for (app.object_bind_groups) |object_bind_group| { + for (app.object_bind_groups, 0..) |object_bind_group, i| { + // 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 = 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, &.{}); - // Draw the vertices in `vertex_buffer`. - pass.draw(app.vertex_count, 1, 0, 0); + + // Draw a number of triangles as specified in the index buffer. + pass.drawIndexed(primitive.index_count, 1, 0, 0, 0); } }