render-zig

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

commit 9bbfcb6ed996d596cd3e321fb01441585115dbdf
parent 54f8213cf6fe2f6650294d54ab5697dcc3bf31b1
Author: Christian Ermann <christianermann@gmail.com>
Date:   Sun, 21 Jul 2024 14:57:18 -0400

Add bounding box rendering

Diffstat:
Asrc/line_render_pipeline.zig | 145+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/load_obj.zig | 4++--
Msrc/main.zig | 80++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/mesh.zig | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/shaders/lines.wgsl | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 348 insertions(+), 3 deletions(-)

diff --git a/src/line_render_pipeline.zig b/src/line_render_pipeline.zig @@ -0,0 +1,145 @@ +const std = @import("std"); +const gpu = @import("mach_gpu"); + +const App = @import("main.zig").App; +const MeshBuffer = @import("main.zig").MeshBuffer; +const UniformData = @import("main.zig").UniformData; +const f32x4 = @import("main.zig").f32x4; +const mat4 = @import("main.zig").mat4; +const CubeMap = @import("cubemap.zig"); +const Textures = @import("textures.zig"); +const RenderPipeline = @import("render_pipeline.zig").RenderPipeline; + +const Self = @This(); + +gpu_pipeline: *gpu.RenderPipeline, +mesh_buffer: *const MeshBuffer, +bind_group: *gpu.BindGroup, +uniform_buffer: *gpu.Buffer, + +pub fn init( + app: *App, + mesh_buffer: *const MeshBuffer, + comptime shader_path: []const u8, +) !Self { + const src = @embedFile(shader_path); + + const module = app.device.createShaderModuleWGSL("line shader", src); + defer module.release(); + + const layouts = Self.bufferLayouts(); + const vertex = gpu.VertexState.init(.{ + .module = module, + .entry_point = "vertex", + .buffers = &layouts, + }); + + const blend: gpu.BlendState = .{}; + const color_target = gpu.ColorTargetState{ + .format = app.swap_chain.format, + .blend = &blend, + .write_mask = gpu.ColorWriteMaskFlags.all, + }; + const fragment = gpu.FragmentState.init(.{ + .module = module, + .entry_point = "fragment", + .targets = &.{color_target}, + }); + + const buffer_descriptor = gpu.Buffer.Descriptor{ + .size = @sizeOf(UniformData), + .usage = .{ .uniform = true, .copy_dst = true }, + .mapped_at_creation = .false, + }; + const uniform_buffer = app.device.createBuffer(&buffer_descriptor); + + const bind_group_layout = Self.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), + gpu.BindGroup.Entry.buffer(1, uniform_buffer, 0, @sizeOf(UniformData)), + }, + }); + 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 = "line render pipeline", + .fragment = &fragment, + .layout = pipeline_layout, + .depth_stencil = &.{ + .format = .depth24_plus, + .depth_write_enabled = .true, + .depth_compare = .less, + }, + .vertex = vertex, + .multisample = .{}, + .primitive = .{ + .topology = .line_list, + .front_face = .ccw, + .cull_mode = .back, + }, + }; + + return .{ + .gpu_pipeline = app.device.createRenderPipeline(&pipeline_descriptor), + .mesh_buffer = mesh_buffer, + .bind_group = bind_group, + .uniform_buffer = uniform_buffer, + }; +} + +fn bufferLayouts() [1]gpu.VertexBufferLayout { + const positions = gpu.VertexBufferLayout.init(.{ + .array_stride = @sizeOf(f32x4), + .step_mode = .vertex, + .attributes = &.{ + .{ .format = .float32x4, .shader_location = 0, .offset = 0 }, + }, + }); + return .{positions}; +} + +fn bindGroupLayout(device: *gpu.Device) *gpu.BindGroupLayout { + const descriptor = gpu.BindGroupLayout.Descriptor.init(.{ + .entries = &.{ + gpu.BindGroupLayout.Entry.buffer(0, .{ + .vertex = true, + .fragment = true, + }, .read_only_storage, false, 0), + gpu.BindGroupLayout.Entry.buffer(1, .{ + .vertex = true, + .fragment = false, + }, .uniform, false, 0), + }, + }); + return device.createBindGroupLayout(&descriptor); +} + +pub fn frame(ptr: *anyopaque, pass: *gpu.RenderPassEncoder) void { + const self: *Self = @ptrCast(@alignCast(ptr)); + pass.setPipeline(self.gpu_pipeline); + + pass.setVertexBuffer(0, self.mesh_buffer.positions.data, 0, self.mesh_buffer.positions.size); + pass.setIndexBuffer(self.mesh_buffer.indices.data, .uint32, 0, self.mesh_buffer.indices.size); + pass.setBindGroup(0, self.bind_group, null); + + const num_indirect = self.mesh_buffer.lines_indirect.count(); + for (0..num_indirect) |idx| { + const offset = idx * self.mesh_buffer.lines_indirect.attrib_size; + pass.drawIndexedIndirect(self.mesh_buffer.lines_indirect.data, offset); + } +} + +pub fn pipeline(self: *Self) RenderPipeline { + return .{ + .ptr = self, + .frameFn = frame, + }; +} diff --git a/src/load_obj.zig b/src/load_obj.zig @@ -183,7 +183,7 @@ pub fn loadFile(options: LoadFileOptions) !Mesh { const nslice = try normals_indexed.toOwnedSlice(); const tslice = try tex_coords_indexed.toOwnedSlice(); const islice = try indices.toOwnedSlice(); - const mesh = Mesh{ + var mesh = Mesh{ .positions = pslice, .normals = nslice, .tex_coords = tslice, @@ -191,7 +191,7 @@ pub fn loadFile(options: LoadFileOptions) !Mesh { .tangents = try allocator.alloc(f32x3, pslice.len), .bitangents = try allocator.alloc(f32x3, pslice.len), }; - + mesh.bbox = mesh.axisAlignedBoundingBox(); try tangentsAndBitangents(&mesh, allocator); return mesh; diff --git a/src/main.zig b/src/main.zig @@ -7,6 +7,7 @@ const RenderPass = @import("render_pass.zig").RenderPass; const ForwardScreenPass = @import("render_pass.zig").ForwardScreenPass; const RenderPipeline = @import("render_pipeline.zig").RenderPipeline; const MeshRenderPipeline = @import("mesh_render_pipeline.zig"); +const LineRenderPipeline = @import("line_render_pipeline.zig"); const SkyBoxRenderPipeline = @import("render_pipeline.zig").SkyBoxRenderPipeline; const CubeMap = @import("cubemap.zig"); const Textures = @import("textures.zig"); @@ -399,6 +400,9 @@ pub const MeshBuffer = struct { meshes: std.ArrayList(RenderMesh), + lines: std.ArrayList(RenderLines), + lines_indirect: Buffer([5]u32), + pub fn init(options: MeshBufferOptions) MeshBuffer { const device = options.device; const n_vertices = options.n_vertices; @@ -455,10 +459,47 @@ pub const MeshBuffer = struct { .{ .indirect = true, .copy_dst = true }, false, ), + .lines_indirect = Buffer([5]u32).init( + options.n_meshes, + device, + .{ .indirect = true, .copy_dst = true }, + false, + ), .meshes = std.ArrayList(RenderMesh).init(options.allocator), + .lines = std.ArrayList(RenderLines).init(options.allocator), }; } + pub fn initLines( + self: *MeshBuffer, + positions: []const f32x4, + indices: []const u32, + ) !*RenderLines { + const num_vertices = positions.len; + const num_indices = indices.len; + + const offset_p = try self.positions.allocWrite(positions, self.queue); + const offset_i = try self.indices.allocWrite(indices, self.queue); + + const instance_id = self.indirect.count(); + const indirect_args = [_][5]u32{.{ + @intCast(num_indices), + 1, + offset_i, + offset_p, + instance_id, + }}; + _ = try self.lines_indirect.allocWrite(&indirect_args, self.queue); + + const lines = RenderLines{ + .num_vertices = num_vertices, + .num_indices = num_indices, + }; + + try self.lines.append(lines); + return &self.lines.items[0]; + } + pub fn initMesh( self: *MeshBuffer, options: *const MeshOptions, @@ -538,6 +579,36 @@ pub const MeshBuffer = struct { } }; +pub const RenderLines = struct { + num_vertices: usize, + num_indices: usize, + scale: f32 = 1.0, + offset: f32x3 = .{ 0.0, 0.0, 0.0 }, + dirty: bool = false, + + pub fn instanceData(self: *const RenderLines) 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 }, + }, + .material = undefined, + }; + } + + pub fn setScale(self: *RenderMesh, scale: f32) void { + self.scale = scale; + self.dirty = true; + } + + pub fn setOffset(self: *RenderMesh, offset: f32x3) void { + self.offset = offset; + self.dirty = true; + } +}; + pub const RenderMesh = struct { num_vertices: usize, num_indices: usize, @@ -645,7 +716,13 @@ pub fn main() !void { .offset = .{ 0, 0, 0 }, }, allocator); + const corners = mesh.bboxVertices(); + const indices = Mesh.bboxIndices(); + + _ = try mesh_buffer.initLines(&corners, &indices); + var rp = try MeshRenderPipeline.init(&app, &mesh_buffer, &textures, "shaders/mesh_clusters.wgsl"); + var lrp = try LineRenderPipeline.init(&app, &mesh_buffer, "shaders/lines.wgsl"); const material = try Material.init( &textures, @@ -663,7 +740,7 @@ pub fn main() !void { var fp = ForwardScreenPass.init(.{ .depth = app.depth_texture_view, - .pipelines = &.{ rp.pipeline(), skybox.pipeline() }, + .pipelines = &.{ rp.pipeline(), lrp.pipeline(), skybox.pipeline() }, }); const passes = [_]RenderPass{fp.renderPass()}; @@ -689,6 +766,7 @@ pub fn main() !void { camera.proj(&uniform_data.proj_matrix); uniform_data.pw_camera = camera.position; app.queue.writeBuffer(rp.uniform_buffer, 0, &[1]UniformData{uniform_data}); + app.queue.writeBuffer(lrp.uniform_buffer, 0, &[1]UniformData{uniform_data}); camera.invView(&skybox_uniform.inv_view); camera.invProj(&skybox_uniform.inv_proj); diff --git a/src/mesh.zig b/src/mesh.zig @@ -3,6 +3,8 @@ const std = @import("std"); const f32x3 = @Vector(3, f32); const f32x4 = @Vector(4, f32); +const Self = @This(); + positions: []f32x4, normals: []f32x3, tex_coords: []f32x3, @@ -10,9 +12,72 @@ tangents: []f32x3, bitangents: []f32x3, indices: []u32, descriptors: ?[]MeshletDescriptor = null, +bbox: [6]f32 = undefined, pub const MeshletDescriptor = struct { offset: u32, num_vertices: u32, num_triangles: u32, }; + +pub fn axisAlignedBoundingBox(self: *const Self) [6]f32 { + var min_x: f32 = self.positions[0][0]; + var max_x: f32 = self.positions[0][0]; + var min_y: f32 = self.positions[0][1]; + var max_y: f32 = self.positions[0][1]; + var min_z: f32 = self.positions[0][2]; + var max_z: f32 = self.positions[0][2]; + for (self.positions) |p| { + const x = p[0]; + const y = p[1]; + const z = p[2]; + if (x < min_x) { + min_x = x; + } + if (y < min_y) { + min_y = y; + } + if (z < min_z) { + min_z = z; + } + if (x > max_x) { + max_x = x; + } + if (y > max_y) { + max_y = y; + } + if (z > max_z) { + max_z = z; + } + } + std.log.info("bbox min: {} {} {}", .{ min_x, min_y, min_z }); + std.log.info("bbox max: {} {} {}", .{ max_x, max_y, max_z }); + return .{ min_x, min_y, min_z, max_x, max_y, max_z }; +} + +pub fn bboxVertices(self: *const Self) [8]f32x4 { + const min_x = self.bbox[0]; + const min_y = self.bbox[1]; + const min_z = self.bbox[2]; + const max_x = self.bbox[3]; + const max_y = self.bbox[4]; + const max_z = self.bbox[5]; + return .{ + .{ min_x, min_y, min_z, 1 }, + .{ max_x, min_y, min_z, 1 }, + .{ max_x, max_y, min_z, 1 }, + .{ min_x, max_y, min_z, 1 }, + .{ min_x, min_y, max_z, 1 }, + .{ max_x, min_y, max_z, 1 }, + .{ max_x, max_y, max_z, 1 }, + .{ min_x, max_y, max_z, 1 }, + }; +} + +pub fn bboxIndices() [24]u32 { + return .{ + 0, 1, 1, 2, 2, 3, 3, 0, + 4, 5, 5, 6, 6, 7, 7, 4, + 0, 4, 1, 5, 2, 6, 3, 7, + }; +} diff --git a/src/shaders/lines.wgsl b/src/shaders/lines.wgsl @@ -0,0 +1,57 @@ + +struct VertexInput { + @location(0) position: vec4<f32>, +}; + +struct VertexOutput { + @builtin(position) clip_position: vec4<f32>, + @location(0) @interpolate(flat) instance_id: u32, +}; + +@group(0) @binding(0) +var<storage, read> instances: array<Instance>; +struct Instance { + model_matrix: mat4x4<f32>, + material: Material, +}; +struct Material { + albedo: u32, + normal: u32, + roughness: u32, + metalness: u32, +}; + +@group(0) @binding(1) +var<uniform> uniform_data: UniformData; +struct UniformData { + pw_camera: vec3<f32>, + view_matrix: mat4x4<f32>, + proj_matrix: mat4x4<f32>, +}; + +@vertex fn vertex( + in: VertexInput, + @builtin(instance_index) idx: u32, +) -> VertexOutput { + let model_matrix = instances[0].model_matrix; + let camera_matrix = uniform_data.proj_matrix * uniform_data.view_matrix; + let pc_vertex = camera_matrix * model_matrix * in.position; + return VertexOutput(pc_vertex, idx); +} + +@group(0) @binding(2) var material_sampler: sampler; +@group(0) @binding(3) var material_texture: texture_2d_array<f32>; + +@fragment fn fragment(in: VertexOutput) -> @location(0) vec4<f32> { + let colors = array<vec3<f32>, 6>( + vec3<f32>(1, 1, 0), + vec3<f32>(0, 1, 1), + vec3<f32>(1, 0, 1), + vec3<f32>(1, 0, 0), + vec3<f32>(0, 1, 0), + vec3<f32>(0, 0, 1), + ); + let color = colors[in.instance_id % 6]; + + return vec4<f32>(0.0, 0.0, 0.0, 1.0); +}