render-zig

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

commit ab98ceef3563a133a5a8366e6c2f63058789df53
parent bb734a86d55b2aa1e960d3d8e8b1af428e6f1fe1
Author: Christian Ermann <christianermann@gmail.com>
Date:   Wed,  8 May 2024 19:42:17 -0400

Add instance buffer for meshes

Diffstat:
Msrc/load_obj.zig | 15+++++++++++----
Msrc/main.zig | 138++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
2 files changed, 123 insertions(+), 30 deletions(-)

diff --git a/src/load_obj.zig b/src/load_obj.zig @@ -3,12 +3,13 @@ const gpu = @import("mach_gpu"); const Mesh = @import("main.zig").Mesh; const MeshBuffer = @import("main.zig").MeshBuffer; +const MeshVertexData = @import("main.zig").MeshVertexData; const f32x2 = @Vector(2, f32); const f32x3 = @Vector(3, f32); const f32x4 = @Vector(4, f32); -fn parsePosition(line: []u8, scale: f32) !f32x4 { +fn parsePosition(line: []u8) !f32x4 { var iter = std.mem.tokenize(u8, line[2..], " "); var position = f32x4{ 0, 0, 0, 1 }; var idx: u32 = 0; @@ -16,7 +17,7 @@ fn parsePosition(line: []u8, scale: f32) !f32x4 { if (idx >= 4) { return error.InvalidFormat; } - position[idx] = try std.fmt.parseFloat(f32, part) * scale; + position[idx] = try std.fmt.parseFloat(f32, part); } if (idx < 3) { return error.InvalidFormat; @@ -104,6 +105,7 @@ pub const LoadFileOptions = struct { mesh_buffer: *MeshBuffer, path: []const u8, scale: f32 = 1.0, + offset: f32x3 = .{ 0, 0, 0 }, }; pub fn loadFile(options: LoadFileOptions) !Mesh { @@ -129,7 +131,7 @@ pub fn loadFile(options: LoadFileOptions) !Mesh { var vtx_idx: u32 = 0; while (try stream.readUntilDelimiterOrEof(&buf, '\n')) |line| { if (std.mem.startsWith(u8, line, "v ")) { - try positions.append(try parsePosition(line, options.scale)); + try positions.append(try parsePosition(line)); } else if (std.mem.startsWith(u8, line, "vt ")) { try tex_coords.append(try parseTexCoord(line)); } else if (std.mem.startsWith(u8, line, "vn ")) { @@ -187,10 +189,15 @@ pub fn loadFile(options: LoadFileOptions) !Mesh { const tslice = try tex_coords_indexed.toOwnedSlice(); const islice = try indices.toOwnedSlice(); - return options.mesh_buffer.initMesh(&.{ + const mesh_data = MeshVertexData{ .positions = pslice, .normals = nslice, .tex_coords = tslice, .indices = islice, + }; + return options.mesh_buffer.initMesh(&.{ + .scale = options.scale, + .offset = options.offset, + .data = &mesh_data, }); } diff --git a/src/main.zig b/src/main.zig @@ -88,7 +88,16 @@ const App = struct { props.driver_description, }); - app.device = app.adapter.createDevice(null) orelse { + const features = [_]gpu.FeatureName{ + gpu.FeatureName.indirect_first_instance, + }; + const device_descriptor = gpu.Device.Descriptor{ + .required_features_count = 1, + .required_features = &features, + .device_lost_callback = undefined, + .device_lost_userdata = undefined, + }; + app.device = app.adapter.createDevice(&device_descriptor) orelse { std.log.err("failed to create GPU device", .{}); std.process.exit(1); }; @@ -331,6 +340,11 @@ const DefaultRenderPipeline = struct { const f32x3 = @Vector(3, f32); const f32x4 = @Vector(4, f32); +const mat4 = [4]@Vector(4, f32); + +const InstanceData = struct { + model_matrix: mat4, +}; pub const Buffer = struct { data: *gpu.Buffer, @@ -376,6 +390,24 @@ pub const Buffer = struct { }; } + pub fn init_instance(n_instances: u32, device: *gpu.Device) Buffer { + std.log.info("buffer init: {} instances", .{n_instances}); + const attrib_size = @sizeOf(InstanceData); + const size = n_instances * attrib_size; + const descriptor = gpu.Buffer.Descriptor{ + .size = size, + .usage = .{ .storage = true, .copy_dst = true }, + .mapped_at_creation = .false, + }; + const data = device.createBuffer(&descriptor); + return .{ + .data = data, + .size = size, + .offset = 0, + .attrib_size = attrib_size, + }; + } + pub fn alloc(self: *Buffer, n_elements: u32) !u32 { const size = n_elements * self.attrib_size; const offset_start = self.offset; @@ -413,8 +445,9 @@ pub const MeshVertexData = struct { }; pub const MeshOptions = struct { - queue: *gpu.Queue, - mesh_data: *MeshVertexData, + scale: f32 = 1.0, + offset: f32x3 = .{ 0, 0, 0 }, + data: *const MeshVertexData, }; pub const MeshBuffer = struct { @@ -425,6 +458,7 @@ pub const MeshBuffer = struct { normals: Buffer, tex_coords: Buffer, indices: Buffer, + instances: Buffer, buf_indirect: *gpu.Buffer, meshes: std.ArrayList(Mesh), @@ -448,12 +482,14 @@ pub const MeshBuffer = struct { .normals = Buffer.init_vtx(n_vertices, device), .tex_coords = Buffer.init_vtx(n_vertices, device), .indices = Buffer.init_idx(n_indices, device), + .instances = Buffer.init_instance(options.n_meshes, device), .buf_indirect = buf_indirect, .meshes = std.ArrayList(Mesh).init(options.allocator), }; } - pub fn initMesh(self: *MeshBuffer, mesh_data: *const MeshVertexData) !Mesh { + pub fn initMesh(self: *MeshBuffer, options: *const MeshOptions) !Mesh { + const mesh_data = options.data; const num_vertices = mesh_data.positions.len; const num_indices = mesh_data.indices.len; std.log.info("init mesh with {} vertices, {} indices", .{ num_vertices, num_indices }); @@ -466,26 +502,31 @@ pub const MeshBuffer = struct { const offset_i = try self.indices.allocWrite(mesh_data.indices, self.queue); std.log.info("idx offset: {} bytes", .{offset_i}); + const mesh = Mesh{ + .num_vertices = num_vertices, + .num_indices = num_indices, + .scale = options.scale, + .offset = options.offset, + }; + + const instances = [_]InstanceData{ + mesh.instanceData(), + }; + const offset_u = try self.instances.allocWrite(&instances, self.queue); + std.log.info("uniform offset: {} bytes", .{offset_u}); + const mesh_id: u32 = @intCast(self.meshes.items.len); const indirect_args = [_]u32{ @intCast(num_indices), 1, offset_i / self.indices.attrib_size, offset_p / self.positions.attrib_size, - 0, + mesh_id, }; std.log.info("indirect: {any}", .{indirect_args}); const offset = mesh_id * 5 * @sizeOf(u32); self.queue.writeBuffer(self.buf_indirect, offset, &indirect_args); - const mesh = .{ - .num_vertices = num_vertices, - .offset_positions = offset_p, - .offset_normals = offset_n, - .offset_tex_coords = offset_t, - .num_indices = num_indices, - .offset_indices = offset_i, - }; try self.meshes.append(mesh); return mesh; } @@ -493,41 +534,61 @@ pub const MeshBuffer = struct { pub const Mesh = struct { num_vertices: usize, - offset_positions: usize, - offset_normals: ?usize, - offset_tex_coords: ?usize, num_indices: usize, - offset_indices: usize, + scale: f32 = 1.0, + offset: f32x3 = .{ 0.0, 0.0, 0.0 }, + + pub fn instanceData(self: *const Mesh) InstanceData { + return InstanceData{ .model_matrix = .{ + .{ self.scale, 0, 0, 0 }, + .{ 0, self.scale, 0, 0 }, + .{ 0, 0, self.scale, 0 }, + .{ self.offset[0], self.offset[1], self.offset[2], 1 }, + } }; + } - pub fn layouts() [3]gpu.VertexBufferLayout { - const p_buffer_layout = gpu.VertexBufferLayout.init(.{ + pub fn bufferLayouts() [3]gpu.VertexBufferLayout { + const positions = gpu.VertexBufferLayout.init(.{ .array_stride = @sizeOf(f32x4), .step_mode = .vertex, .attributes = &.{ .{ .format = .float32x4, .shader_location = 0, .offset = 0 }, }, }); - const n_buffer_layout = gpu.VertexBufferLayout.init(.{ + const normals = gpu.VertexBufferLayout.init(.{ .array_stride = @sizeOf(f32x4), .step_mode = .vertex, .attributes = &.{ .{ .format = .float32x3, .shader_location = 1, .offset = 0 }, }, }); - const t_buffer_layout = gpu.VertexBufferLayout.init(.{ + const tex_coords = gpu.VertexBufferLayout.init(.{ .array_stride = @sizeOf(f32x4), .step_mode = .vertex, .attributes = &.{ .{ .format = .float32x3, .shader_location = 2, .offset = 0 }, }, }); - return .{ p_buffer_layout, n_buffer_layout, t_buffer_layout }; + return .{ positions, normals, tex_coords }; + } + + pub fn bindGroupLayout(device: *gpu.Device) *gpu.BindGroupLayout { + const descriptor = gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &.{ + gpu.BindGroupLayout.Entry.buffer(0, .{ + .vertex = true, + .fragment = false, + }, .read_only_storage, false, 0), + }, + }); + return device.createBindGroupLayout(&descriptor); } }; const UnlitRenderPipeline = struct { gpu_pipeline: *gpu.RenderPipeline, mesh_buffer: *const MeshBuffer, + bind_group: *gpu.BindGroup, pub fn init(app: *App, mesh_buffer: *const MeshBuffer, _: std.mem.Allocator) !UnlitRenderPipeline { const vs = @@ -535,14 +596,21 @@ const UnlitRenderPipeline = struct { \\ @location(0) position: vec4<f32>, \\ }; \\ - \\ @vertex fn main(in: VertexInput) -> @builtin(position) vec4<f32> { - \\ return in.position; + \\ @group(0) @binding(0) + \\ var<storage, read> instances: array<Instance>; + \\ struct Instance { + \\ model_matrix: mat4x4<f32>, + \\ }; + \\ + \\ @vertex fn main(in: VertexInput, @builtin(instance_index) idx: u32) -> @builtin(position) vec4<f32> { + \\ let model_matrix = instances[idx].model_matrix; + \\ return model_matrix * in.position; \\ } ; const vs_module = app.device.createShaderModuleWGSL("default vertex shader", vs); defer vs_module.release(); - const layouts = Mesh.layouts(); + const layouts = Mesh.bufferLayouts(); const vertex = gpu.VertexState.init(.{ .module = vs_module, .entry_point = "main", @@ -569,10 +637,25 @@ const UnlitRenderPipeline = struct { .targets = &.{color_target}, }); + const bind_group_layout = Mesh.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), + }, + }); + const bind_group = app.device.createBindGroup(&bind_group_descriptor); + + const pipeline_layout_descriptor = gpu.PipelineLayout.Descriptor.init(.{ + .bind_group_layouts = &.{bind_group_layout}, + }); + const pipeline_layout = app.device.createPipelineLayout(&pipeline_layout_descriptor); + defer pipeline_layout.release(); + const pipeline_descriptor = gpu.RenderPipeline.Descriptor{ .label = "default render pipeline", .fragment = &fragment, - .layout = null, + .layout = pipeline_layout, .depth_stencil = null, .vertex = vertex, .multisample = .{}, @@ -586,6 +669,7 @@ const UnlitRenderPipeline = struct { return .{ .gpu_pipeline = app.device.createRenderPipeline(&pipeline_descriptor), .mesh_buffer = mesh_buffer, + .bind_group = bind_group, }; } @@ -596,6 +680,7 @@ const UnlitRenderPipeline = struct { pass.setVertexBuffer(0, self.mesh_buffer.positions.data, 0, self.mesh_buffer.positions.size); pass.setVertexBuffer(1, self.mesh_buffer.normals.data, 0, self.mesh_buffer.normals.size); pass.setVertexBuffer(2, self.mesh_buffer.tex_coords.data, 0, self.mesh_buffer.tex_coords.size); + pass.setBindGroup(0, self.bind_group, null); pass.setIndexBuffer(self.mesh_buffer.indices.data, .uint32, 0, self.mesh_buffer.indices.size); for (0..self.mesh_buffer.meshes.items.len) |mesh_idx| { @@ -640,6 +725,7 @@ pub fn main() !void { .mesh_buffer = &mesh_buffer, .path = "teapot.obj", .scale = 0.05, + .offset = .{ 0, -0.5, 0 }, }); var drp = try UnlitRenderPipeline.init(&app, &mesh_buffer, allocator);