Compare commits

..

4 Commits

  1. 5
      src/main.zig
  2. 145
      src/renderer.zig
  3. 17
      src/shader.wgsl

@ -16,7 +16,10 @@ app_timer: core.Timer,
title_timer: core.Timer, title_timer: core.Timer,
pub fn init(app: *App) !void { pub fn init(app: *App) !void {
try core.init(.{}); try core.init(.{
// Request high performance = prefer dedicated GPU when available.
.power_preference = .high_performance,
});
// Set up a "general purpose allocator" that will handle allocations for // Set up a "general purpose allocator" that will handle allocations for
// the lifetime of the application, for which a more specific allocation // the lifetime of the application, for which a more specific allocation

@ -13,23 +13,11 @@ const primitives = @import("./primitives.zig");
const VertexData = primitives.VertexData; const VertexData = primitives.VertexData;
const PrimitiveData = primitives.PrimitiveData; const PrimitiveData = primitives.PrimitiveData;
/// Holds information about how a perticular scene should be rendered.
const SceneUniformBuffer = struct {
view_proj_matrix: Mat,
};
/// Holds information about where and how an object should be rendered.
const ObjectUniformBuffer = struct {
model_matrix: Mat,
color: [3]f32,
};
/// Holds data needed to render an object in a rendering pass. /// Holds data needed to render an object in a rendering pass.
const ObjectData = struct { const ObjectData = struct {
/// Reference to data stored on the GPU of type `ObjectUniformBuffer`. /// Bind group which associates model-related buffers with parameters
uniform_buffer: *gpu.Buffer, /// in our shader. This one is accessible via `@group(1)` in our shader.
/// Bind group used to associate the buffer to the `object` shader parameter. model_bind_group: *gpu.BindGroup,
uniform_bind_group: *gpu.BindGroup,
/// Reference to the primitive (shape or model) to render for this object. /// Reference to the primitive (shape or model) to render for this object.
primitive: *PrimitiveData, primitive: *PrimitiveData,
}; };
@ -39,8 +27,8 @@ const Renderer = @This();
app: *App, app: *App,
pipeline: *gpu.RenderPipeline, pipeline: *gpu.RenderPipeline,
scene_uniform_buffer: *gpu.Buffer, view_proj_buffer: *gpu.Buffer,
scene_uniform_bind_group: *gpu.BindGroup, camera_bind_group: *gpu.BindGroup,
depth_texture: ?*gpu.Texture = null, depth_texture: ?*gpu.Texture = null,
depth_texture_view: ?*gpu.TextureView = null, depth_texture_view: ?*gpu.TextureView = null,
@ -49,11 +37,42 @@ primitive_data: []PrimitiveData,
object_data: []ObjectData, object_data: []ObjectData,
pub fn init(app: *App) !*Renderer { pub fn init(app: *App) !*Renderer {
// A string buffer used to format objects' labels.
var label_buffer: [256]u8 = undefined;
const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl")); const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl"));
defer shader_module.release(); defer shader_module.release();
// Define layouts for our bind groups and pipeline.
// This helps find errors with missing or mismatching shader properties.
var camera_bind_group_layout = core.device.createBindGroupLayout(&gpu.BindGroupLayout.Descriptor.init(.{
.label = "Camera Bind Group Layout",
.entries = &.{
gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, false, 0),
},
}));
defer camera_bind_group_layout.release();
var model_bind_group_layout = core.device.createBindGroupLayout(&gpu.BindGroupLayout.Descriptor.init(.{
.label = "Model Bind Group Layout",
.entries = &.{
gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, false, 0),
gpu.BindGroupLayout.Entry.buffer(1, .{ .vertex = true }, .uniform, false, 0),
},
}));
defer model_bind_group_layout.release();
const pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{
.bind_group_layouts = &.{
camera_bind_group_layout,
model_bind_group_layout,
},
}));
defer pipeline_layout.release();
// Set up rendering pipeline. // Set up rendering pipeline.
const pipeline = core.device.createRenderPipeline(&.{ const pipeline = core.device.createRenderPipeline(&.{
.layout = pipeline_layout,
.vertex = gpu.VertexState.init(.{ .vertex = gpu.VertexState.init(.{
.module = shader_module, .module = shader_module,
.entry_point = "vertex_main", .entry_point = "vertex_main",
@ -85,10 +104,17 @@ pub fn init(app: *App) !*Renderer {
}); });
// Set up scene related uniform buffers and bind groups. // Set up scene related uniform buffers and bind groups.
const scene_uniform = createAndWriteUniformBuffer( const view_proj_buffer = createAndWriteBuffer(zm.Mat, &.{zm.identity()}, .{ .copy_dst = true, .uniform = true });
pipeline.getBindGroupLayout(0),
SceneUniformBuffer{ .view_proj_matrix = zm.identity() }, // "Bind groups" are used to associate data from buffers with shader parameters.
); // So for example the `camera_bind_group` is accessible via `@group(0)` in our shader.
const camera_bind_group = core.device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{
.label = "Camera Bind Group",
.layout = camera_bind_group_layout,
.entries = &.{
gpu.BindGroup.Entry.buffer(0, view_proj_buffer, 0, @sizeOf(zm.Mat)),
},
}));
// Set up the primitives we want to render. // Set up the primitives we want to render.
// //
@ -128,19 +154,27 @@ pub fn init(app: *App) !*Renderer {
// Make the object have a color depending on its location in the grid. // Make the object have a color depending on its location in the grid.
// These values are layed out so each corner is red, green, blue and black. // These values are layed out so each corner is red, green, blue and black.
const color = .{ const model_color = .{
std.math.clamp(1.0 - x - z, 0.0, 1.0), std.math.clamp(1.0 - x - z, 0.0, 1.0),
std.math.clamp(x - z, 0.0, 1.0), std.math.clamp(x - z, 0.0, 1.0),
std.math.clamp(z - x, 0.0, 1.0), std.math.clamp(z - x, 0.0, 1.0),
}; };
const object_uniform = createAndWriteUniformBuffer( const model_matrix_buffer = createAndWriteBuffer(zm.Mat, &.{zm.transpose(model_matrix)}, .{ .copy_dst = true, .uniform = true });
pipeline.getBindGroupLayout(1), defer model_matrix_buffer.release();
ObjectUniformBuffer{
.model_matrix = zm.transpose(model_matrix), const model_color_buffer = createAndWriteBuffer([3]f32, &.{model_color}, .{ .copy_dst = true, .uniform = true });
.color = color, defer model_color_buffer.release();
const model_bind_group_label = try std.fmt.bufPrintZ(&label_buffer, "Model Bind Group {d}", .{i});
const model_bind_group = core.device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{
.label = model_bind_group_label,
.layout = model_bind_group_layout,
.entries = &.{
gpu.BindGroup.Entry.buffer(0, model_matrix_buffer, 0, @sizeOf(zm.Mat)),
gpu.BindGroup.Entry.buffer(1, model_color_buffer, 0, @sizeOf([3]f32)),
}, },
); }));
// Pick a "random" primitive to use for this object. // Pick a "random" primitive to use for this object.
const primitive_index = app.random.int(usize) % primitive_data.len; const primitive_index = app.random.int(usize) % primitive_data.len;
@ -149,8 +183,7 @@ pub fn init(app: *App) !*Renderer {
// The `*object` syntax gets us a pointer to each element in the // The `*object` syntax gets us a pointer to each element in the
// `object_data` slice, allowing us to override it within the loop. // `object_data` slice, allowing us to override it within the loop.
object.* = .{ object.* = .{
.uniform_buffer = object_uniform.buffer, .model_bind_group = model_bind_group,
.uniform_bind_group = object_uniform.bind_group,
.primitive = primitive, .primitive = primitive,
}; };
} }
@ -159,8 +192,8 @@ pub fn init(app: *App) !*Renderer {
result.* = .{ result.* = .{
.app = app, .app = app,
.pipeline = pipeline, .pipeline = pipeline,
.scene_uniform_buffer = scene_uniform.buffer, .view_proj_buffer = view_proj_buffer,
.scene_uniform_bind_group = scene_uniform.bind_group, .camera_bind_group = camera_bind_group,
.primitive_data = primitive_data, .primitive_data = primitive_data,
.object_data = object_data, .object_data = object_data,
}; };
@ -178,8 +211,8 @@ pub fn deinit(self: *Renderer) void {
defer self.app.allocator.destroy(self); defer self.app.allocator.destroy(self);
defer self.pipeline.release(); defer self.pipeline.release();
defer self.scene_uniform_buffer.release(); defer self.view_proj_buffer.release();
defer self.scene_uniform_bind_group.release(); defer self.camera_bind_group.release();
defer self.app.allocator.free(self.primitive_data); defer self.app.allocator.free(self.primitive_data);
defer for (self.primitive_data) |p| { defer for (self.primitive_data) |p| {
@ -188,8 +221,7 @@ pub fn deinit(self: *Renderer) void {
}; };
defer self.app.allocator.free(self.object_data); defer self.app.allocator.free(self.object_data);
defer for (self.object_data) |o| { defer for (self.object_data) |o| {
o.uniform_buffer.release(); o.model_bind_group.release();
o.uniform_bind_group.release();
}; };
defer if (self.depth_texture) |t| t.release(); defer if (self.depth_texture) |t| t.release();
@ -249,11 +281,11 @@ pub fn update(self: *Renderer) void {
defer encoder.release(); defer encoder.release();
// Write to the scene uniform buffer for this set of commands. // Write to the scene uniform buffer for this set of commands.
encoder.writeBuffer(self.scene_uniform_buffer, 0, &[_]SceneUniformBuffer{.{ encoder.writeBuffer(self.view_proj_buffer, 0, &[_]zm.Mat{
// All matrices the GPU has to work with need to be transposed, // All matrices the GPU has to work with need to be transposed,
// because WebGPU uses column-major matrices while zmath is row-major. // because WebGPU uses column-major matrices while zmath is row-major.
.view_proj_matrix = zm.transpose(view_proj_matrix), zm.transpose(view_proj_matrix),
}}); });
{ {
const pass = encoder.beginRenderPass(&render_pass_info); const pass = encoder.beginRenderPass(&render_pass_info);
@ -261,17 +293,17 @@ pub fn update(self: *Renderer) void {
defer pass.end(); defer pass.end();
pass.setPipeline(self.pipeline); pass.setPipeline(self.pipeline);
pass.setBindGroup(0, self.scene_uniform_bind_group, &.{}); pass.setBindGroup(0, self.camera_bind_group, &.{});
for (self.object_data) |object| { for (self.object_data) |object| {
// Set the vertex and index buffer used to render this object // Set the vertex and index buffer used to render this
// to the primitive it wants to use (either triangle or square). // object to the ones from the primitive it wants to use.
const prim = object.primitive; const prim = object.primitive;
pass.setVertexBuffer(0, prim.vertex_buffer, 0, prim.vertex_count * @sizeOf(VertexData)); pass.setVertexBuffer(0, prim.vertex_buffer, 0, prim.vertex_count * @sizeOf(VertexData));
pass.setIndexBuffer(prim.index_buffer, .uint32, 0, prim.index_count * @sizeOf(u32)); pass.setIndexBuffer(prim.index_buffer, .uint32, 0, prim.index_count * @sizeOf(u32));
// Set the bind group for an object we want to render. // Set the bind group for the object we want to render.
pass.setBindGroup(1, object.uniform_bind_group, &.{}); pass.setBindGroup(1, object.model_bind_group, &.{});
// Draw a number of triangles as specified in the index buffer. // Draw a number of triangles as specified in the index buffer.
pass.drawIndexed(prim.index_count, 1, 0, 0, 0); pass.drawIndexed(prim.index_count, 1, 0, 0, 0);
@ -295,6 +327,7 @@ pub fn recreateDepthTexture(self: *Renderer) void {
if (self.depth_texture_view) |v| v.release(); if (self.depth_texture_view) |v| v.release();
self.depth_texture = core.device.createTexture(&.{ self.depth_texture = core.device.createTexture(&.{
.label = "Depth Texture",
.usage = .{ .render_attachment = true }, .usage = .{ .render_attachment = true },
.size = .{ .width = core.descriptor.width, .height = core.descriptor.height }, .size = .{ .width = core.descriptor.width, .height = core.descriptor.height },
.format = .depth24_plus, .format = .depth24_plus,
@ -302,30 +335,6 @@ pub fn recreateDepthTexture(self: *Renderer) void {
self.depth_texture_view = self.depth_texture.?.createView(null); self.depth_texture_view = self.depth_texture.?.createView(null);
} }
/// 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.
pub 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 /// Creates a buffer on the GPU with the specified usage
/// flags and immediately fills it with the provided data. /// flags and immediately fills it with the provided data.
pub fn createAndWriteBuffer( pub fn createAndWriteBuffer(

@ -1,14 +1,7 @@
struct SceneUniformBuffer { @group(0) @binding(0) var<uniform> view_proj_matrix: mat4x4<f32>;
view_proj_matrix: mat4x4<f32>,
};
struct ObjectUniformBuffer {
model_matrix: mat4x4<f32>,
color: vec3<f32>,
};
@group(0) @binding(0) var<uniform> scene: SceneUniformBuffer; @group(1) @binding(0) var<uniform> model_matrix: mat4x4<f32>;
@group(1) @binding(0) var<uniform> object: ObjectUniformBuffer; @group(1) @binding(1) var<uniform> model_color: vec3<f32>;
struct VertexInput { struct VertexInput {
@location(0) position: vec3<f32>, @location(0) position: vec3<f32>,
@ -25,9 +18,9 @@ struct FragmentOutput {
@vertex fn vertex_main(in: VertexInput) -> VertexOutput { @vertex fn vertex_main(in: VertexInput) -> VertexOutput {
var out: VertexOutput; var out: VertexOutput;
let mvp = object.model_matrix * scene.view_proj_matrix; let mvp = model_matrix * view_proj_matrix;
out.position = vec4<f32>(in.position, 1.0) * mvp; out.position = vec4<f32>(in.position, 1.0) * mvp;
out.color = vec4<f32>(object.color, 1.0); out.color = vec4<f32>(model_color, 1.0);
return out; return out;
} }

Loading…
Cancel
Save