src/core: move mach-core@9a4d09707d9f1cb6ea5602bdf58caeefc46146be package to here

Helps hexops/mach#1165

Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
Stephen Gutekanst 2024-03-04 23:25:11 -07:00 committed by Stephen Gutekanst
parent fa3f6161ad
commit 38f296ecce
157 changed files with 28383 additions and 0 deletions

66
src/core/Frequency.zig Normal file
View file

@ -0,0 +1,66 @@
const std = @import("std");
const core = @import("main.zig");
const Timer = @import("Timer.zig");
pub const Frequency = @This();
/// The target frequency (e.g. 60hz) or zero for unlimited
target: u32 = 0,
/// The estimated delay that is needed to achieve the target frequency. Updated during tick()
delay_ns: u64 = 0,
/// The actual measured frequency. This is updated every second.
rate: u32 = 0,
delta_time: ?*f32 = null,
delta_time_ns: *u64 = undefined,
/// Internal fields, this must be initialized via a call to start().
internal: struct {
// The frame number in this second's cycle. e.g. zero to 59
count: u32,
timer: Timer,
last_time: u64,
} = undefined,
/// Starts the timer used for frequency calculation. Must be called once before anything else.
pub fn start(f: *Frequency) !void {
f.internal = .{
.count = 0,
.timer = try Timer.start(),
.last_time = 0,
};
}
/// Tick should be called at each occurrence (e.g. frame)
pub inline fn tick(f: *Frequency) void {
var current_time = f.internal.timer.readPrecise();
if (f.delta_time) |delta_time| {
f.delta_time_ns.* = current_time -| f.internal.last_time;
delta_time.* = @as(f32, @floatFromInt(f.delta_time_ns.*)) / @as(f32, @floatFromInt(std.time.ns_per_s));
}
if (current_time >= std.time.ns_per_s) {
f.rate = f.internal.count;
f.internal.count = 0;
f.internal.timer.reset();
current_time -= std.time.ns_per_s;
}
f.internal.last_time = current_time;
f.internal.count += 1;
if (f.target != 0) {
const limited_count = @min(f.target, f.internal.count);
const target_time_per_tick: u64 = (std.time.ns_per_s / f.target);
const target_time = target_time_per_tick * limited_count;
if (current_time > target_time) {
f.delay_ns = 0;
} else {
f.delay_ns = target_time - current_time;
}
} else {
f.delay_ns = 0;
}
}

25
src/core/InputState.zig Normal file
View file

@ -0,0 +1,25 @@
const std = @import("std");
const core = @import("main.zig");
const KeyBitSet = std.StaticBitSet(@intFromEnum(core.Key.max) + 1);
const MouseButtonSet = std.StaticBitSet(@as(u4, @intFromEnum(core.MouseButton.max)) + 1);
const InputState = @This();
keys: KeyBitSet = KeyBitSet.initEmpty(),
mouse_buttons: MouseButtonSet = MouseButtonSet.initEmpty(),
mouse_position: core.Position = .{ .x = 0, .y = 0 },
pub inline fn isKeyPressed(self: InputState, key: core.Key) bool {
return self.keys.isSet(@intFromEnum(key));
}
pub inline fn isKeyReleased(self: InputState, key: core.Key) bool {
return !self.isKeyPressed(key);
}
pub inline fn isMouseButtonPressed(self: InputState, button: core.MouseButton) bool {
return self.mouse_buttons.isSet(@intFromEnum(button));
}
pub inline fn isMouseButtonReleased(self: InputState, button: core.MouseButton) bool {
return !self.isMouseButtonPressed(button);
}

38
src/core/Timer.zig Normal file
View file

@ -0,0 +1,38 @@
const std = @import("std");
const platform = @import("platform.zig");
pub const Timer = @This();
internal: platform.Timer,
/// Initialize the timer.
pub fn start() !Timer {
return Timer{
.internal = try platform.Timer.start(),
};
}
/// Reads the timer value since start or the last reset in nanoseconds.
pub inline fn readPrecise(timer: *Timer) u64 {
return timer.internal.read();
}
/// Reads the timer value since start or the last reset in seconds.
pub inline fn read(timer: *Timer) f32 {
return @as(f32, @floatFromInt(timer.readPrecise())) / @as(f32, @floatFromInt(std.time.ns_per_s));
}
/// Resets the timer value to 0/now.
pub inline fn reset(timer: *Timer) void {
timer.internal.reset();
}
/// Returns the current value of the timer in nanoseconds, then resets it.
pub inline fn lapPrecise(timer: *Timer) u64 {
return timer.internal.lap();
}
/// Returns the current value of the timer in seconds, then resets it.
pub inline fn lap(timer: *Timer) f32 {
return @as(f32, @floatFromInt(timer.lapPrecise())) / @as(f32, @floatFromInt(std.time.ns_per_s));
}

View file

@ -0,0 +1,26 @@
Copyright 2019 WebGPU Samples Contributors
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from this
software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,263 @@
/// A port of Austin Eng's "computeBoids" webgpu sample.
/// https://github.com/austinEng/webgpu-samples/blob/main/src/sample/computeBoids/main.ts
const std = @import("std");
const core = @import("mach").core;
const gpu = core.gpu;
title_timer: core.Timer,
timer: core.Timer,
compute_pipeline: *gpu.ComputePipeline,
render_pipeline: *gpu.RenderPipeline,
sprite_vertex_buffer: *gpu.Buffer,
particle_buffers: [2]*gpu.Buffer,
particle_bind_groups: [2]*gpu.BindGroup,
sim_param_buffer: *gpu.Buffer,
frame_counter: usize,
pub const App = @This();
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const num_particle = 1500;
var sim_params = [_]f32{
0.04, // .delta_T
0.1, // .rule_1_distance
0.025, // .rule_2_distance
0.025, // .rule_3_distance
0.02, // .rule_1_scale
0.05, // .rule_2_scale
0.005, // .rule_3_scale
};
pub fn init(app: *App) !void {
try core.init(.{});
const sprite_shader_module = core.device.createShaderModuleWGSL(
"sprite.wgsl",
@embedFile("sprite.wgsl"),
);
defer sprite_shader_module.release();
const update_sprite_shader_module = core.device.createShaderModuleWGSL(
"updateSprites.wgsl",
@embedFile("updateSprites.wgsl"),
);
defer update_sprite_shader_module.release();
const instanced_particles_attributes = [_]gpu.VertexAttribute{
.{
// instance position
.shader_location = 0,
.offset = 0,
.format = .float32x2,
},
.{
// instance velocity
.shader_location = 1,
.offset = 2 * 4,
.format = .float32x2,
},
};
const vertex_buffer_attributes = [_]gpu.VertexAttribute{
.{
// vertex positions
.shader_location = 2,
.offset = 0,
.format = .float32x2,
},
};
const render_pipeline = core.device.createRenderPipeline(&gpu.RenderPipeline.Descriptor{
.vertex = gpu.VertexState.init(.{
.module = sprite_shader_module,
.entry_point = "vert_main",
.buffers = &.{
gpu.VertexBufferLayout.init(.{
// instanced particles buffer
.array_stride = 4 * 4,
.step_mode = .instance,
.attributes = &instanced_particles_attributes,
}),
gpu.VertexBufferLayout.init(.{
// vertex buffer
.array_stride = 2 * 4,
.step_mode = .vertex,
.attributes = &vertex_buffer_attributes,
}),
},
}),
.fragment = &gpu.FragmentState.init(.{
.module = sprite_shader_module,
.entry_point = "frag_main",
.targets = &[_]gpu.ColorTargetState{.{
.format = core.descriptor.format,
}},
}),
});
const compute_pipeline = core.device.createComputePipeline(&gpu.ComputePipeline.Descriptor{ .compute = gpu.ProgrammableStageDescriptor{
.module = update_sprite_shader_module,
.entry_point = "main",
} });
const vert_buffer_data = [_]f32{
-0.01, -0.02, 0.01,
-0.02, 0.0, 0.02,
};
const sprite_vertex_buffer = core.device.createBuffer(&gpu.Buffer.Descriptor{
.label = "sprite_vertex_buffer",
.usage = .{ .vertex = true },
.mapped_at_creation = .true,
.size = vert_buffer_data.len * @sizeOf(f32),
});
const vertex_mapped = sprite_vertex_buffer.getMappedRange(f32, 0, vert_buffer_data.len);
@memcpy(vertex_mapped.?, vert_buffer_data[0..]);
sprite_vertex_buffer.unmap();
const sim_param_buffer = core.device.createBuffer(&gpu.Buffer.Descriptor{
.label = "sim_param_buffer",
.usage = .{ .uniform = true, .copy_dst = true },
.size = sim_params.len * @sizeOf(f32),
});
core.queue.writeBuffer(sim_param_buffer, 0, sim_params[0..]);
var initial_particle_data: [num_particle * 4]f32 = undefined;
var rng = std.rand.DefaultPrng.init(0);
const random = rng.random();
var i: usize = 0;
while (i < num_particle) : (i += 1) {
initial_particle_data[4 * i + 0] = 2 * (random.float(f32) - 0.5);
initial_particle_data[4 * i + 1] = 2 * (random.float(f32) - 0.5);
initial_particle_data[4 * i + 2] = 2 * (random.float(f32) - 0.5) * 0.1;
initial_particle_data[4 * i + 3] = 2 * (random.float(f32) - 0.5) * 0.1;
}
var particle_buffers: [2]*gpu.Buffer = undefined;
var particle_bind_groups: [2]*gpu.BindGroup = undefined;
i = 0;
while (i < 2) : (i += 1) {
particle_buffers[i] = core.device.createBuffer(&gpu.Buffer.Descriptor{
.label = "particle_buffer",
.mapped_at_creation = .true,
.usage = .{
.vertex = true,
.storage = true,
},
.size = initial_particle_data.len * @sizeOf(f32),
});
const mapped = particle_buffers[i].getMappedRange(f32, 0, initial_particle_data.len);
@memcpy(mapped.?, initial_particle_data[0..]);
particle_buffers[i].unmap();
}
i = 0;
while (i < 2) : (i += 1) {
const layout = compute_pipeline.getBindGroupLayout(0);
defer layout.release();
particle_bind_groups[i] = core.device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{
.layout = layout,
.entries = &.{
gpu.BindGroup.Entry.buffer(0, sim_param_buffer, 0, sim_params.len * @sizeOf(f32)),
gpu.BindGroup.Entry.buffer(1, particle_buffers[i], 0, initial_particle_data.len * @sizeOf(f32)),
gpu.BindGroup.Entry.buffer(2, particle_buffers[(i + 1) % 2], 0, initial_particle_data.len * @sizeOf(f32)),
},
}));
}
app.* = .{
.timer = try core.Timer.start(),
.title_timer = try core.Timer.start(),
.compute_pipeline = compute_pipeline,
.render_pipeline = render_pipeline,
.sprite_vertex_buffer = sprite_vertex_buffer,
.particle_buffers = particle_buffers,
.particle_bind_groups = particle_bind_groups,
.sim_param_buffer = sim_param_buffer,
.frame_counter = 0,
};
}
pub fn deinit(app: *App) void {
defer _ = gpa.deinit();
defer core.deinit();
app.compute_pipeline.release();
app.render_pipeline.release();
app.sprite_vertex_buffer.release();
for (app.particle_buffers) |particle_buffer| particle_buffer.release();
for (app.particle_bind_groups) |particle_bind_group| particle_bind_group.release();
app.sim_param_buffer.release();
}
pub fn update(app: *App) !bool {
const delta_time = app.timer.lap();
var iter = core.pollEvents();
while (iter.next()) |event| {
if (event == .close) return true;
}
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
const color_attachment = gpu.RenderPassColorAttachment{
.view = back_buffer_view,
.clear_value = std.mem.zeroes(gpu.Color),
.load_op = .clear,
.store_op = .store,
};
const render_pass_descriptor = gpu.RenderPassDescriptor.init(.{
.color_attachments = &.{
color_attachment,
},
});
sim_params[0] = @as(f32, @floatCast(delta_time));
core.queue.writeBuffer(app.sim_param_buffer, 0, sim_params[0..]);
const command_encoder = core.device.createCommandEncoder(null);
{
const pass_encoder = command_encoder.beginComputePass(null);
pass_encoder.setPipeline(app.compute_pipeline);
pass_encoder.setBindGroup(0, app.particle_bind_groups[app.frame_counter % 2], null);
pass_encoder.dispatchWorkgroups(@as(u32, @intFromFloat(@ceil(@as(f32, num_particle) / 64))), 1, 1);
pass_encoder.end();
pass_encoder.release();
}
{
const pass_encoder = command_encoder.beginRenderPass(&render_pass_descriptor);
pass_encoder.setPipeline(app.render_pipeline);
pass_encoder.setVertexBuffer(0, app.particle_buffers[(app.frame_counter + 1) % 2], 0, num_particle * 4 * @sizeOf(f32));
pass_encoder.setVertexBuffer(1, app.sprite_vertex_buffer, 0, 6 * @sizeOf(f32));
pass_encoder.draw(3, num_particle, 0, 0);
pass_encoder.end();
pass_encoder.release();
}
app.frame_counter += 1;
if (app.frame_counter % 60 == 0) {
std.log.info("Frame {}", .{app.frame_counter});
}
var command = command_encoder.finish(null);
command_encoder.release();
core.queue.submit(&[_]*gpu.CommandBuffer{command});
command.release();
core.swap_chain.present();
back_buffer_view.release();
// update the window title every second
if (app.title_timer.read() >= 1.0) {
app.title_timer.reset();
try core.printTitle("Boids [ {d}fps ] [ Input {d}hz ]", .{
core.frameRate(),
core.inputRate(),
});
}
return false;
}

View file

@ -0,0 +1,15 @@
@vertex
fn vert_main(@location(0) a_particlePos : vec2<f32>,
@location(1) a_particleVel : vec2<f32>,
@location(2) a_pos : vec2<f32>) -> @builtin(position) vec4<f32> {
let angle = -atan2(a_particleVel.x, a_particleVel.y);
let pos = vec2<f32>(
(a_pos.x * cos(angle)) - (a_pos.y * sin(angle)),
(a_pos.x * sin(angle)) + (a_pos.y * cos(angle)));
return vec4<f32>(pos + a_particlePos, 0.0, 1.0);
}
@fragment
fn frag_main() -> @location(0) vec4<f32> {
return vec4<f32>(1.0, 1.0, 1.0, 1.0);
}

View file

@ -0,0 +1,90 @@
struct Particle {
pos : vec2<f32>,
vel : vec2<f32>,
};
struct SimParams {
deltaT : f32,
rule1Distance : f32,
rule2Distance : f32,
rule3Distance : f32,
rule1Scale : f32,
rule2Scale : f32,
rule3Scale : f32,
};
struct Particles {
particles : array<Particle>,
};
@binding(0) @group(0) var<uniform> params : SimParams;
@binding(1) @group(0) var<storage, read> particlesA : Particles;
@binding(2) @group(0) var<storage, read_write> particlesB : Particles;
// https://github.com/austinEng/Project6-Vulkan-Flocking/blob/master/data/shaders/computeparticles/particle.comp
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) GlobalInvocationID : vec3<u32>) {
var index : u32 = GlobalInvocationID.x;
if (index >= arrayLength(&particlesA.particles)) {
return;
}
var vPos = particlesA.particles[index].pos;
var vVel = particlesA.particles[index].vel;
var cMass = vec2<f32>(0.0, 0.0);
var cVel = vec2<f32>(0.0, 0.0);
var colVel = vec2<f32>(0.0, 0.0);
var cMassCount : u32 = 0u;
var cVelCount : u32 = 0u;
var pos : vec2<f32>;
var vel : vec2<f32>;
for (var i : u32 = 0u; i < arrayLength(&particlesA.particles); i = i + 1u) {
if (i == index) {
continue;
}
pos = particlesA.particles[i].pos.xy;
vel = particlesA.particles[i].vel.xy;
if (distance(pos, vPos) < params.rule1Distance) {
cMass = cMass + pos;
cMassCount = cMassCount + 1u;
}
if (distance(pos, vPos) < params.rule2Distance) {
colVel = colVel - (pos - vPos);
}
if (distance(pos, vPos) < params.rule3Distance) {
cVel = cVel + vel;
cVelCount = cVelCount + 1u;
}
}
if (cMassCount > 0u) {
var temp = f32(cMassCount);
cMass = (cMass / vec2<f32>(temp, temp)) - vPos;
}
if (cVelCount > 0u) {
var temp = f32(cVelCount);
cVel = cVel / vec2<f32>(temp, temp);
}
vVel = vVel + (cMass * params.rule1Scale) + (colVel * params.rule2Scale) +
(cVel * params.rule3Scale);
// clamp velocity for a more pleasing simulation
vVel = normalize(vVel) * clamp(length(vVel), 0.0, 0.1);
// kinematic update
vPos = vPos + (vVel * params.deltaT);
// Wrap around boundary
if (vPos.x < -1.0) {
vPos.x = 1.0;
}
if (vPos.x > 1.0) {
vPos.x = -1.0;
}
if (vPos.y < -1.0) {
vPos.y = 1.0;
}
if (vPos.y > 1.0) {
vPos.y = -1.0;
}
// Write back
particlesB.particles[index].pos = vPos;
particlesB.particles[index].vel = vVel;
}

View file

@ -0,0 +1,78 @@
const std = @import("std");
const core = @import("mach").core;
const gpu = core.gpu;
const renderer = @import("renderer.zig");
pub const App = @This();
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
title_timer: core.Timer,
pub fn init(app: *App) !void {
try core.init(.{});
app.* = .{
.title_timer = try core.Timer.start(),
};
}
pub fn deinit(app: *App) void {
_ = app;
defer _ = gpa.deinit();
defer core.deinit();
}
pub fn update(app: *App) !bool {
var iter = core.pollEvents();
while (iter.next()) |event| {
switch (event) {
.key_press => |ev| {
if (ev.key == .space) return true;
},
.close => return true,
else => {},
}
}
app.render();
// update the window title every second
if (app.title_timer.read() >= 1.0) {
app.title_timer.reset();
try core.printTitle("Clear Color [ {d}fps ] [ Input {d}hz ]", .{
core.frameRate(),
core.inputRate(),
});
}
return false;
}
fn render(app: *App) void {
_ = app;
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
const color_attachment = gpu.RenderPassColorAttachment{
.view = back_buffer_view,
.clear_value = gpu.Color{ .r = 0.0, .g = 0.0, .b = 1.0, .a = 1.0 },
.load_op = .clear,
.store_op = .store,
};
const encoder = core.device.createCommandEncoder(null);
const render_pass_info = gpu.RenderPassDescriptor.init(.{
.color_attachments = &.{color_attachment},
});
const pass = encoder.beginRenderPass(&render_pass_info);
pass.end();
pass.release();
var command = encoder.finish(null);
encoder.release();
var queue = core.queue;
queue.submit(&[_]*gpu.CommandBuffer{command});
command.release();
core.swap_chain.present();
back_buffer_view.release();
}

View file

@ -0,0 +1,32 @@
const core = @import("mach").core;
const gpu = core.gpu;
pub const Renderer = @This();
pub fn RenderUpdate() void {
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
const color_attachment = gpu.RenderPassColorAttachment{
.view = back_buffer_view,
.clear_value = gpu.Color{ .r = 0.0, .g = 0.0, .b = 1.0, .a = 1.0 },
.load_op = .clear,
.store_op = .store,
};
const encoder = core.device.createCommandEncoder(null);
const render_pass_info = gpu.RenderPassDescriptor.init(.{
.color_attachments = &.{color_attachment},
});
const pass = encoder.beginRenderPass(&render_pass_info);
pass.end();
pass.release();
var command = encoder.finish(null);
encoder.release();
const queue = core.queue;
queue.submit(&[_]*gpu.CommandBuffer{command});
command.release();
core.swap_chain.present();
back_buffer_view.release();
}

View file

@ -0,0 +1,49 @@
pub const Vertex = extern struct {
pos: @Vector(4, f32),
col: @Vector(4, f32),
uv: @Vector(2, f32),
};
pub const vertices = [_]Vertex{
.{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } },
};

View file

@ -0,0 +1,392 @@
const std = @import("std");
const core = @import("mach").core;
const gpu = core.gpu;
const zm = @import("zmath");
const zigimg = @import("zigimg");
const Vertex = @import("cube_mesh.zig").Vertex;
const vertices = @import("cube_mesh.zig").vertices;
const assets = @import("assets");
const UniformBufferObject = struct {
mat: zm.Mat,
};
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
title_timer: core.Timer,
timer: core.Timer,
pipeline: *gpu.RenderPipeline,
vertex_buffer: *gpu.Buffer,
uniform_buffer: *gpu.Buffer,
bind_group: *gpu.BindGroup,
depth_texture: *gpu.Texture,
depth_texture_view: *gpu.TextureView,
pub const App = @This();
pub fn init(app: *App) !void {
try core.init(.{});
const allocator = gpa.allocator();
const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl"));
defer shader_module.release();
const vertex_attributes = [_]gpu.VertexAttribute{
.{ .format = .float32x4, .offset = @offsetOf(Vertex, "pos"), .shader_location = 0 },
.{ .format = .float32x2, .offset = @offsetOf(Vertex, "uv"), .shader_location = 1 },
};
const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{
.array_stride = @sizeOf(Vertex),
.step_mode = .vertex,
.attributes = &vertex_attributes,
});
const blend = gpu.BlendState{
.color = .{
.operation = .add,
.src_factor = .src_alpha,
.dst_factor = .one_minus_src_alpha,
},
.alpha = .{
.operation = .add,
.src_factor = .one,
.dst_factor = .zero,
},
};
const color_target = gpu.ColorTargetState{
.format = core.descriptor.format,
.blend = &blend,
.write_mask = gpu.ColorWriteMaskFlags.all,
};
const fragment = gpu.FragmentState.init(.{
.module = shader_module,
.entry_point = "frag_main",
.targets = &.{color_target},
});
const pipeline_descriptor = gpu.RenderPipeline.Descriptor{
.fragment = &fragment,
// Enable depth testing so that the fragment closest to the camera
// is rendered in front.
.depth_stencil = &.{
.format = .depth24_plus,
.depth_write_enabled = .true,
.depth_compare = .less,
},
.vertex = gpu.VertexState.init(.{
.module = shader_module,
.entry_point = "vertex_main",
.buffers = &.{vertex_buffer_layout},
}),
.primitive = .{
// Since the cube has its face pointing outwards, cull_mode must be
// set to .front or .none here since we are inside the cube looking out.
// Ideally you would set this to .back and have a custom cube primitive
// with the faces pointing towards the inside of the cube.
.cull_mode = .none,
},
};
const pipeline = core.device.createRenderPipeline(&pipeline_descriptor);
const vertex_buffer = core.device.createBuffer(&.{
.usage = .{ .vertex = true },
.size = @sizeOf(Vertex) * vertices.len,
.mapped_at_creation = .true,
});
const vertex_mapped = vertex_buffer.getMappedRange(Vertex, 0, vertices.len);
@memcpy(vertex_mapped.?, vertices[0..]);
vertex_buffer.unmap();
const uniform_buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .uniform = true },
.size = @sizeOf(UniformBufferObject),
.mapped_at_creation = .false,
});
// Create a sampler with linear filtering for smooth interpolation.
const sampler = core.device.createSampler(&.{
.mag_filter = .linear,
.min_filter = .linear,
});
const queue = core.queue;
// WebGPU expects the cubemap textures in this order: (+X,-X,+Y,-Y,+Z,-Z)
var images: [6]zigimg.Image = undefined;
images[0] = try zigimg.Image.fromMemory(allocator, assets.skybox_posx_png);
defer images[0].deinit();
images[1] = try zigimg.Image.fromMemory(allocator, assets.skybox_negx_png);
defer images[1].deinit();
images[2] = try zigimg.Image.fromMemory(allocator, assets.skybox_posy_png);
defer images[2].deinit();
images[3] = try zigimg.Image.fromMemory(allocator, assets.skybox_negy_png);
defer images[3].deinit();
images[4] = try zigimg.Image.fromMemory(allocator, assets.skybox_posz_png);
defer images[4].deinit();
images[5] = try zigimg.Image.fromMemory(allocator, assets.skybox_negz_png);
defer images[5].deinit();
// Use the first image of the set for sizing
const img_size = gpu.Extent3D{
.width = @as(u32, @intCast(images[0].width)),
.height = @as(u32, @intCast(images[0].height)),
};
// We set depth_or_array_layers to 6 here to indicate there are 6 images in this texture
const tex_size = gpu.Extent3D{
.width = @as(u32, @intCast(images[0].width)),
.height = @as(u32, @intCast(images[0].height)),
.depth_or_array_layers = 6,
};
// Same as a regular texture, but with a Z of 6 (defined in tex_size)
const cube_texture = core.device.createTexture(&.{
.size = tex_size,
.format = .rgba8_unorm,
.dimension = .dimension_2d,
.usage = .{
.texture_binding = true,
.copy_dst = true,
.render_attachment = false,
},
});
const data_layout = gpu.Texture.DataLayout{
.bytes_per_row = @as(u32, @intCast(images[0].width * 4)),
.rows_per_image = @as(u32, @intCast(images[0].height)),
};
const encoder = core.device.createCommandEncoder(null);
// We have to create a staging buffer, copy all the image data into the
// staging buffer at the correct Z offset, encode a command to copy
// the buffer to the texture for each image, then push it to the command
// queue
var staging_buff: [6]*gpu.Buffer = undefined;
var i: u32 = 0;
while (i < 6) : (i += 1) {
staging_buff[i] = core.device.createBuffer(&.{
.usage = .{ .copy_src = true, .map_write = true },
.size = @as(u64, @intCast(images[0].width)) * @as(u64, @intCast(images[0].height)) * @sizeOf(u32),
.mapped_at_creation = .true,
});
switch (images[i].pixels) {
.rgba32 => |pixels| {
// Map a section of the staging buffer
const staging_map = staging_buff[i].getMappedRange(u32, 0, @as(u64, @intCast(images[i].width)) * @as(u64, @intCast(images[i].height)));
// Copy the image data into the mapped buffer
@memcpy(staging_map.?, @as([]u32, @ptrCast(@alignCast(pixels))));
// And release the mapping
staging_buff[i].unmap();
},
.rgb24 => |pixels| {
const staging_map = staging_buff[i].getMappedRange(u32, 0, @as(u64, @intCast(images[i].width)) * @as(u64, @intCast(images[i].height)));
// In this case, we have to convert the data to rgba32 first
const data = try rgb24ToRgba32(allocator, pixels);
defer data.deinit(allocator);
@memcpy(staging_map.?, @as([]u32, @ptrCast(@alignCast(data.rgba32))));
staging_buff[i].unmap();
},
else => @panic("unsupported image color format"),
}
// These define the source and target for the buffer to texture copy command
const copy_buff = gpu.ImageCopyBuffer{
.layout = data_layout,
.buffer = staging_buff[i],
};
const copy_tex = gpu.ImageCopyTexture{
.texture = cube_texture,
.origin = gpu.Origin3D{ .x = 0, .y = 0, .z = i },
};
// Encode the copy command, we do this for every image in the texture.
encoder.copyBufferToTexture(&copy_buff, &copy_tex, &img_size);
staging_buff[i].release();
}
// Now that the commands to copy our buffer data to the texture is filled,
// push the encoded commands over to the queue and execute to get the
// texture filled with the image data.
var command = encoder.finish(null);
encoder.release();
queue.submit(&[_]*gpu.CommandBuffer{command});
command.release();
// The textureView in the bind group needs dimension defined as "dimension_cube".
const cube_texture_view = cube_texture.createView(&gpu.TextureView.Descriptor{ .dimension = .dimension_cube });
cube_texture.release();
const bind_group_layout = pipeline.getBindGroupLayout(0);
const bind_group = core.device.createBindGroup(
&gpu.BindGroup.Descriptor.init(.{
.layout = bind_group_layout,
.entries = &.{
gpu.BindGroup.Entry.buffer(0, uniform_buffer, 0, @sizeOf(UniformBufferObject)),
gpu.BindGroup.Entry.sampler(1, sampler),
gpu.BindGroup.Entry.textureView(2, cube_texture_view),
},
}),
);
sampler.release();
cube_texture_view.release();
bind_group_layout.release();
const depth_texture = core.device.createTexture(&gpu.Texture.Descriptor{
.size = gpu.Extent3D{
.width = core.descriptor.width,
.height = core.descriptor.height,
},
.format = .depth24_plus,
.usage = .{
.render_attachment = true,
.texture_binding = true,
},
});
const depth_texture_view = depth_texture.createView(&gpu.TextureView.Descriptor{
.format = .depth24_plus,
.dimension = .dimension_2d,
.array_layer_count = 1,
.mip_level_count = 1,
});
app.timer = try core.Timer.start();
app.title_timer = try core.Timer.start();
app.pipeline = pipeline;
app.vertex_buffer = vertex_buffer;
app.uniform_buffer = uniform_buffer;
app.bind_group = bind_group;
app.depth_texture = depth_texture;
app.depth_texture_view = depth_texture_view;
}
pub fn deinit(app: *App) void {
defer _ = gpa.deinit();
defer core.deinit();
app.pipeline.release();
app.vertex_buffer.release();
app.uniform_buffer.release();
app.bind_group.release();
app.depth_texture.release();
app.depth_texture_view.release();
}
pub fn update(app: *App) !bool {
var iter = core.pollEvents();
while (iter.next()) |event| {
switch (event) {
.key_press => |ev| {
if (ev.key == .space) return true;
},
.close => return true,
.framebuffer_resize => |ev| {
// If window is resized, recreate depth buffer otherwise we cannot use it.
app.depth_texture.release();
app.depth_texture = core.device.createTexture(&gpu.Texture.Descriptor{
.size = gpu.Extent3D{
.width = ev.width,
.height = ev.height,
},
.format = .depth24_plus,
.usage = .{
.render_attachment = true,
.texture_binding = true,
},
});
app.depth_texture_view.release();
app.depth_texture_view = app.depth_texture.createView(&gpu.TextureView.Descriptor{
.format = .depth24_plus,
.dimension = .dimension_2d,
.array_layer_count = 1,
.mip_level_count = 1,
});
},
else => {},
}
}
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
const color_attachment = gpu.RenderPassColorAttachment{
.view = back_buffer_view,
.clear_value = .{ .r = 0.5, .g = 0.5, .b = 0.5, .a = 0.0 },
.load_op = .clear,
.store_op = .store,
};
const encoder = core.device.createCommandEncoder(null);
const render_pass_info = gpu.RenderPassDescriptor.init(.{
.color_attachments = &.{color_attachment},
.depth_stencil_attachment = &.{
.view = app.depth_texture_view,
.depth_clear_value = 1.0,
.depth_load_op = .clear,
.depth_store_op = .store,
},
});
{
const time = app.timer.read();
const aspect = @as(f32, @floatFromInt(core.descriptor.width)) / @as(f32, @floatFromInt(core.descriptor.height));
const proj = zm.perspectiveFovRh((2 * std.math.pi) / 5.0, aspect, 0.1, 3000);
const model = zm.mul(
zm.scaling(1000, 1000, 1000),
zm.rotationX(std.math.pi / 2.0 * 3.0),
);
const view = zm.mul(
zm.mul(
zm.lookAtRh(
zm.Vec{ 0, 0, 0, 1 },
zm.Vec{ 1, 0, 0, 1 },
zm.Vec{ 0, 0, 1, 0 },
),
zm.rotationY(time * 0.2),
),
zm.rotationX((std.math.pi / 10.0) * std.math.sin(time)),
);
const mvp = zm.mul(zm.mul(zm.transpose(model), view), proj);
const ubo = UniformBufferObject{ .mat = mvp };
encoder.writeBuffer(app.uniform_buffer, 0, &[_]UniformBufferObject{ubo});
}
const pass = encoder.beginRenderPass(&render_pass_info);
pass.setPipeline(app.pipeline);
pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len);
pass.setBindGroup(0, app.bind_group, &.{});
pass.draw(vertices.len, 1, 0, 0);
pass.end();
pass.release();
var command = encoder.finish(null);
encoder.release();
const queue = core.queue;
queue.submit(&[_]*gpu.CommandBuffer{command});
command.release();
core.swap_chain.present();
back_buffer_view.release();
// update the window title every second
if (app.title_timer.read() >= 1.0) {
app.title_timer.reset();
try core.printTitle("Cube Map [ {d}fps ] [ Input {d}hz ]", .{
core.frameRate(),
core.inputRate(),
});
}
return false;
}
fn rgb24ToRgba32(allocator: std.mem.Allocator, in: []zigimg.color.Rgb24) !zigimg.color.PixelStorage {
const out = try zigimg.color.PixelStorage.init(allocator, .rgba32, in.len);
var i: usize = 0;
while (i < in.len) : (i += 1) {
out.rgba32[i] = zigimg.color.Rgba32{ .r = in[i].r, .g = in[i].g, .b = in[i].b, .a = 255 };
}
return out;
}

View file

@ -0,0 +1,34 @@
struct Uniforms {
modelViewProjectionMatrix : mat4x4<f32>,
}
@binding(0) @group(0) var<uniform> uniforms : Uniforms;
struct VertexOutput {
@builtin(position) Position : vec4<f32>,
@location(0) fragUV : vec2<f32>,
@location(1) fragPosition: vec4<f32>,
}
@vertex
fn vertex_main(
@location(0) position : vec4<f32>,
@location(1) uv : vec2<f32>
) -> VertexOutput {
var output : VertexOutput;
output.Position = uniforms.modelViewProjectionMatrix * position;
output.fragUV = uv;
output.fragPosition = 0.5 * (position + vec4<f32>(1.0, 1.0, 1.0, 1.0));
return output;
}
@group(0) @binding(1) var mySampler: sampler;
@group(0) @binding(2) var myTexture: texture_cube<f32>;
@fragment
fn frag_main(
@location(0) fragUV: vec2<f32>,
@location(1) fragPosition: vec4<f32>
) -> @location(0) vec4<f32> {
var cubemapVec = fragPosition.xyz - vec3<f32>(0.5, 0.5, 0.5);
return textureSample(myTexture, mySampler, cubemapVec);
}

View file

@ -0,0 +1,83 @@
@group(0) @binding(0) var gBufferNormal: texture_2d<f32>;
@group(0) @binding(1) var gBufferAlbedo: texture_2d<f32>;
@group(0) @binding(2) var gBufferDepth: texture_depth_2d;
struct LightData {
position : vec4<f32>,
color : vec3<f32>,
radius : f32,
}
struct LightsBuffer {
lights: array<LightData>,
}
@group(1) @binding(0) var<storage, read> lightsBuffer: LightsBuffer;
struct Config {
numLights : u32,
}
struct Camera {
viewProjectionMatrix : mat4x4<f32>,
invViewProjectionMatrix : mat4x4<f32>,
}
@group(1) @binding(1) var<uniform> config: Config;
@group(1) @binding(2) var<uniform> camera: Camera;
fn world_from_screen_coord(coord : vec2<f32>, depth_sample: f32) -> vec3<f32> {
// reconstruct world-space position from the screen coordinate.
let posClip = vec4(coord.x * 2.0 - 1.0, (1.0 - coord.y) * 2.0 - 1.0, depth_sample, 1.0);
let posWorldW = camera.invViewProjectionMatrix * posClip;
let posWorld = posWorldW.xyz / posWorldW.www;
return posWorld;
}
@fragment
fn main(
@builtin(position) coord : vec4<f32>
) -> @location(0) vec4<f32> {
var result : vec3<f32>;
let depth = textureLoad(
gBufferDepth,
vec2<i32>(floor(coord.xy)),
0
);
// Don't light the sky.
if (depth >= 1.0) {
discard;
}
let bufferSize = textureDimensions(gBufferDepth);
let coordUV = coord.xy / vec2<f32>(bufferSize);
let position = world_from_screen_coord(coordUV, depth);
let normal = textureLoad(
gBufferNormal,
vec2<i32>(floor(coord.xy)),
0
).xyz;
let albedo = textureLoad(
gBufferAlbedo,
vec2<i32>(floor(coord.xy)),
0
).rgb;
for (var i = 0u; i < config.numLights; i++) {
let L = lightsBuffer.lights[i].position.xyz - position;
let distance = length(L);
if (distance > lightsBuffer.lights[i].radius) {
continue;
}
let lambert = max(dot(normal, normalize(L)), 0.0);
result += vec3<f32>(
lambert * pow(1.0 - distance / lightsBuffer.lights[i].radius, 2.0) * lightsBuffer.lights[i].color * albedo
);
}
// some manual ambient
result += vec3(0.2);
return vec4(result, 1.0);
}

View file

@ -0,0 +1,44 @@
@group(0) @binding(0) var gBufferNormal: texture_2d<f32>;
@group(0) @binding(1) var gBufferAlbedo: texture_2d<f32>;
@group(0) @binding(2) var gBufferDepth: texture_depth_2d;
@group(1) @binding(0) var<uniform> canvas : CanvasConstants;
struct CanvasConstants {
size: vec2<f32>,
}
@fragment
fn main(
@builtin(position) coord : vec4<f32>
) -> @location(0) vec4<f32> {
var result : vec4<f32>;
let c = coord.xy / vec2<f32>(canvas.size.x, canvas.size.y);
if (c.x < 0.33333) {
let rawDepth = textureLoad(
gBufferDepth,
vec2<i32>(floor(coord.xy)),
0
);
// remap depth into something a bit more visible
let depth = (1.0 - rawDepth) * 50.0;
result = vec4(depth);
} else if (c.x < 0.66667) {
result = textureLoad(
gBufferNormal,
vec2<i32>(floor(coord.xy)),
0
);
result.x = (result.x + 1.0) * 0.5;
result.y = (result.y + 1.0) * 0.5;
result.z = (result.z + 1.0) * 0.5;
} else {
result = textureLoad(
gBufferAlbedo,
vec2<i32>(floor(coord.xy)),
0
);
}
return result;
}

View file

@ -0,0 +1,22 @@
struct GBufferOutput {
@location(0) normal : vec4<f32>,
// Textures: diffuse color, specular color, smoothness, emissive etc. could go here
@location(1) albedo : vec4<f32>,
}
@fragment
fn main(
@location(0) fragNormal: vec3<f32>,
@location(1) fragUV : vec2<f32>
) -> GBufferOutput {
// faking some kind of checkerboard texture
let uv = floor(30.0 * fragUV);
let c = 0.2 + 0.5 * ((uv.x + uv.y) - 2.0 * floor((uv.x + uv.y) / 2.0));
var output : GBufferOutput;
output.normal = vec4(fragNormal, 1.0);
output.albedo = vec4(c, c, c, 1.0);
return output;
}

View file

@ -0,0 +1,34 @@
struct LightData {
position : vec4<f32>,
color : vec3<f32>,
radius : f32,
}
struct LightsBuffer {
lights: array<LightData>,
}
@group(0) @binding(0) var<storage, read_write> lightsBuffer: LightsBuffer;
struct Config {
numLights : u32,
}
@group(0) @binding(1) var<uniform> config: Config;
struct LightExtent {
min : vec4<f32>,
max : vec4<f32>,
}
@group(0) @binding(2) var<uniform> lightExtent: LightExtent;
@compute @workgroup_size(64, 1, 1)
fn main(@builtin(global_invocation_id) GlobalInvocationID : vec3<u32>) {
var index = GlobalInvocationID.x;
if (index >= config.numLights) {
return;
}
lightsBuffer.lights[index].position.y = lightsBuffer.lights[index].position.y - 0.5 - 0.003 * (f32(index) - 64.0 * floor(f32(index) / 64.0));
if (lightsBuffer.lights[index].position.y < lightExtent.min.y) {
lightsBuffer.lights[index].position.y = lightExtent.max.y;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,11 @@
@vertex
fn main(
@builtin(vertex_index) VertexIndex : u32
) -> @builtin(position) vec4<f32> {
const pos = array(
vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0),
vec2(-1.0, 1.0), vec2(1.0, -1.0), vec2(1.0, 1.0),
);
return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
}

View file

@ -0,0 +1,30 @@
struct Uniforms {
modelMatrix : mat4x4<f32>,
normalModelMatrix : mat4x4<f32>,
}
struct Camera {
viewProjectionMatrix : mat4x4<f32>,
invViewProjectionMatrix : mat4x4<f32>,
}
@group(0) @binding(0) var<uniform> uniforms : Uniforms;
@group(0) @binding(1) var<uniform> camera : Camera;
struct VertexOutput {
@builtin(position) Position : vec4<f32>,
@location(0) fragNormal: vec3<f32>, // normal in world space
@location(1) fragUV: vec2<f32>,
}
@vertex
fn main(
@location(0) position : vec3<f32>,
@location(1) normal : vec3<f32>,
@location(2) uv : vec2<f32>
) -> VertexOutput {
var output : VertexOutput;
let worldPosition = (uniforms.modelMatrix * vec4(position, 1.0)).xyz;
output.Position = camera.viewProjectionMatrix * vec4(worldPosition, 1.0);
output.fragNormal = normalize((uniforms.normalModelMatrix * vec4(normal, 1.0)).xyz);
output.fragUV = uv;
return output;
}

View file

@ -0,0 +1,188 @@
const std = @import("std");
/// Vertex writer manages the placement of vertices by tracking which are unique. If a duplicate vertex is added
/// with `put`, only it's index will be written to the index buffer.
/// `IndexType` should match the integer type used for the index buffer
pub fn VertexWriter(comptime VertexType: type, comptime IndexType: type) type {
return struct {
const MapEntry = struct {
packed_index: IndexType = null_index,
next_sparse: IndexType = null_index,
};
const null_index: IndexType = std.math.maxInt(IndexType);
vertices: []VertexType,
indices: []IndexType,
sparse_to_packed_map: []MapEntry,
/// Next index outside of the 1:1 mapping range for storing
/// position -> normal collisions
next_collision_index: IndexType,
/// Next packed index
next_packed_index: IndexType,
written_indices_count: IndexType,
/// Allocate storage and set default values
/// `sparse_vertices_count` is the number of vertices in the source before de-duplication / remapping
/// Put more succinctly, the largest index value in source index buffer
/// `max_vertex_count` is largest permutation of vertices assuming that {vertex, uv, normal} never map 1:1 and always
/// create a new mapping
pub fn init(
allocator: std.mem.Allocator,
indices_count: IndexType,
sparse_vertices_count: IndexType,
max_vertex_count: IndexType,
) !@This() {
var result: @This() = undefined;
result.vertices = try allocator.alloc(VertexType, max_vertex_count);
result.indices = try allocator.alloc(IndexType, indices_count);
result.sparse_to_packed_map = try allocator.alloc(MapEntry, max_vertex_count);
result.next_collision_index = sparse_vertices_count;
result.next_packed_index = 0;
result.written_indices_count = 0;
@memset(result.sparse_to_packed_map, .{});
return result;
}
pub fn put(self: *@This(), vertex: VertexType, sparse_index: IndexType) void {
if (self.sparse_to_packed_map[sparse_index].packed_index == null_index) {
// New start of chain, reserve a new packed index and add entry to `index_map`
const packed_index = self.next_packed_index;
self.sparse_to_packed_map[sparse_index].packed_index = packed_index;
self.vertices[packed_index] = vertex;
self.indices[self.written_indices_count] = packed_index;
self.written_indices_count += 1;
self.next_packed_index += 1;
return;
}
var previous_sparse_index: IndexType = undefined;
var current_sparse_index = sparse_index;
while (current_sparse_index != null_index) {
const packed_index = self.sparse_to_packed_map[current_sparse_index].packed_index;
if (std.mem.eql(u8, &std.mem.toBytes(self.vertices[packed_index]), &std.mem.toBytes(vertex))) {
// We already have a record for this vertex in our chain
self.indices[self.written_indices_count] = packed_index;
self.written_indices_count += 1;
return;
}
previous_sparse_index = current_sparse_index;
current_sparse_index = self.sparse_to_packed_map[current_sparse_index].next_sparse;
}
// This is a new mapping for the given sparse index
const packed_index = self.next_packed_index;
const remapped_sparse_index = self.next_collision_index;
self.indices[self.written_indices_count] = packed_index;
self.vertices[packed_index] = vertex;
self.sparse_to_packed_map[previous_sparse_index].next_sparse = remapped_sparse_index;
self.sparse_to_packed_map[remapped_sparse_index].packed_index = packed_index;
self.next_packed_index += 1;
self.next_collision_index += 1;
self.written_indices_count += 1;
}
pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void {
allocator.free(self.vertices);
allocator.free(self.indices);
allocator.free(self.sparse_to_packed_map);
}
pub fn indexBuffer(self: @This()) []IndexType {
return self.indices;
}
pub fn vertexBuffer(self: @This()) []VertexType {
return self.vertices[0..self.next_packed_index];
}
};
}
test "VertexWriter" {
const Vec3 = [3]f32;
const Vertex = extern struct {
position: Vec3,
normal: Vec3,
};
const expect = std.testing.expect;
const allocator = std.testing.allocator;
const Face = struct {
position: [3]u16,
normal: [3]u16,
};
const vertices = [_]Vec3{
Vec3{ 1.0, 0.0, 0.0 }, // 0: Position
Vec3{ 2.0, 0.0, 0.0 }, // 1: Position
Vec3{ 3.0, 0.0, 0.0 }, // 2: Position
Vec3{ 1.0, 0.0, 0.0 }, // 3: Normal
Vec3{ 4.0, 0.0, 0.0 }, // 4: Position
Vec3{ 0.0, 1.0, 0.0 }, // 5: Normal
Vec3{ 5.0, 0.0, 0.0 }, // 6: Position
Vec3{ 0.0, 0.0, 1.0 }, // 7: Normal
Vec3{ 1.0, 0.0, 1.0 }, // 8: Normal
Vec3{ 6.0, 0.0, 0.0 }, // 9: Position
};
const faces = [_]Face{
.{ .position = .{ 0, 4, 2 }, .normal = .{ 7, 5, 3 } },
.{ .position = .{ 2, 3, 9 }, .normal = .{ 3, 7, 8 } },
.{ .position = .{ 9, 2, 4 }, .normal = .{ 8, 7, 5 } },
.{ .position = .{ 2, 6, 1 }, .normal = .{ 3, 5, 7 } },
.{ .position = .{ 9, 6, 0 }, .normal = .{ 5, 7, 8 } },
};
var writer = try VertexWriter(Vertex, u32).init(
allocator,
faces.len * 3, // indices count
vertices.len, // original vertices count
faces.len * 3, // maximum vertices count
);
defer writer.deinit(allocator);
for (faces) |face| {
var x: usize = 0;
while (x < 3) : (x += 1) {
const position_index = face.position[x];
const position = vertices[position_index];
const normal = vertices[face.normal[x]];
const vertex = Vertex{
.position = position,
.normal = normal,
};
writer.put(vertex, position_index);
}
}
const indices = writer.indexBuffer();
try expect(indices.len == faces.len * 3);
// Face 0
try expect(indices[0] == 0); // (0, 7) New
try expect(indices[1] == 1); // (4, 5) New
try expect(indices[2] == 2); // (2, 3) New
// Face 1
try expect(indices[3 + 0] == 2); // (2, 3) Duplicate - Reuse index
try expect(indices[3 + 1] == 3); // (3, 7) New
try expect(indices[3 + 2] == 4); // (9, 8) New
// Face 2
try expect(indices[6 + 0] == 4); // (9, 8) Duplicate - Reuse index
try expect(indices[6 + 1] == 5); // (2, 7) New normal mapping (Don't clobber)
try expect(indices[6 + 2] == 1); // (4, 5) Duplicate - Reuse Index
// Face 3
try expect(indices[9 + 0] == 2); // (2, 3) Duplicate - Reuse index
try expect(indices[9 + 1] == 6); // (6, 5) New
try expect(indices[9 + 2] == 7); // (1, 7) New
// Face 4
try expect(indices[12 + 0] == 8); // (9, 5) New normal mapping (Don't clobber)
try expect(indices[12 + 1] == 9); // (6, 7) New normal mapping (Don't clobber)
try expect(indices[12 + 2] == 10); // (0, 8) New normal mapping (Don't clobber)
try expect(writer.vertexBuffer().len == 11);
}

View file

@ -0,0 +1,49 @@
pub const Vertex = extern struct {
pos: @Vector(4, f32),
col: @Vector(4, f32),
uv: @Vector(2, f32),
};
pub const vertices = [_]Vertex{
.{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } },
};

View file

@ -0,0 +1,371 @@
//! To get the effect we want, we need a texture on which to render;
//! we can't use the swapchain texture directly, but we can get the effect
//! by doing the same render pass twice, on the texture and the swapchain.
//! We also need a second texture to use on the cube (after the render pass
//! it needs to copy the other texture.) We can't use the same texture since
//! it would interfere with the synchronization on the gpu during the render pass.
//! This demo currently does not work on opengl, because core.descriptor.width/height,
//! are set to 0 after core.init() and because webgpu does not implement copyTextureToTexture,
//! for opengl
const std = @import("std");
const core = @import("mach").core;
const gpu = core.gpu;
const zm = @import("zmath");
const Vertex = @import("cube_mesh.zig").Vertex;
const vertices = @import("cube_mesh.zig").vertices;
pub const App = @This();
const UniformBufferObject = struct {
mat: zm.Mat,
};
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
title_timer: core.Timer,
timer: core.Timer,
pipeline: *gpu.RenderPipeline,
vertex_buffer: *gpu.Buffer,
uniform_buffer: *gpu.Buffer,
bind_group: *gpu.BindGroup,
depth_texture: ?*gpu.Texture,
depth_texture_view: *gpu.TextureView,
cube_texture: *gpu.Texture,
cube_texture_view: *gpu.TextureView,
cube_texture_render: *gpu.Texture,
cube_texture_view_render: *gpu.TextureView,
sampler: *gpu.Sampler,
bgl: *gpu.BindGroupLayout,
pub fn init(app: *App) !void {
try core.init(.{});
const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl"));
const vertex_attributes = [_]gpu.VertexAttribute{
.{ .format = .float32x4, .offset = @offsetOf(Vertex, "pos"), .shader_location = 0 },
.{ .format = .float32x2, .offset = @offsetOf(Vertex, "uv"), .shader_location = 1 },
};
const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{
.array_stride = @sizeOf(Vertex),
.attributes = &vertex_attributes,
});
const blend = gpu.BlendState{};
const color_target = gpu.ColorTargetState{
.format = core.descriptor.format,
.blend = &blend,
.write_mask = gpu.ColorWriteMaskFlags.all,
};
const fragment = gpu.FragmentState.init(.{
.module = shader_module,
.entry_point = "frag_main",
.targets = &.{color_target},
});
const bgle_buffer = gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, true, 0);
const bgle_sampler = gpu.BindGroupLayout.Entry.sampler(1, .{ .fragment = true }, .filtering);
const bgle_textureview = gpu.BindGroupLayout.Entry.texture(2, .{ .fragment = true }, .float, .dimension_2d, false);
const bgl = core.device.createBindGroupLayout(
&gpu.BindGroupLayout.Descriptor.init(.{
.entries = &.{ bgle_buffer, bgle_sampler, bgle_textureview },
}),
);
const bind_group_layouts = [_]*gpu.BindGroupLayout{bgl};
const pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{
.bind_group_layouts = &bind_group_layouts,
}));
const pipeline_descriptor = gpu.RenderPipeline.Descriptor{
.fragment = &fragment,
.layout = pipeline_layout,
.depth_stencil = &.{
.format = .depth24_plus,
.depth_write_enabled = .true,
.depth_compare = .less,
},
.vertex = gpu.VertexState.init(.{
.module = shader_module,
.entry_point = "vertex_main",
.buffers = &.{vertex_buffer_layout},
}),
.primitive = .{
.cull_mode = .back,
},
};
const vertex_buffer = core.device.createBuffer(&.{
.usage = .{ .vertex = true },
.size = @sizeOf(Vertex) * vertices.len,
.mapped_at_creation = .true,
});
const vertex_mapped = vertex_buffer.getMappedRange(Vertex, 0, vertices.len);
@memcpy(vertex_mapped.?, vertices[0..]);
vertex_buffer.unmap();
const uniform_buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .uniform = true },
.size = @sizeOf(UniformBufferObject),
.mapped_at_creation = .false,
});
// The texture to put on the cube
const cube_texture = core.device.createTexture(&gpu.Texture.Descriptor{
.usage = .{ .texture_binding = true, .copy_dst = true },
.size = .{ .width = core.descriptor.width, .height = core.descriptor.height },
.format = core.descriptor.format,
});
// The texture on which we render
const cube_texture_render = core.device.createTexture(&gpu.Texture.Descriptor{
.usage = .{ .render_attachment = true, .copy_src = true },
.size = .{ .width = core.descriptor.width, .height = core.descriptor.height },
.format = core.descriptor.format,
});
const sampler = core.device.createSampler(&gpu.Sampler.Descriptor{
.mag_filter = .linear,
.min_filter = .linear,
});
const cube_texture_view = cube_texture.createView(&gpu.TextureView.Descriptor{
.format = core.descriptor.format,
.dimension = .dimension_2d,
.mip_level_count = 1,
.array_layer_count = 1,
});
const cube_texture_view_render = cube_texture_render.createView(&gpu.TextureView.Descriptor{
.format = core.descriptor.format,
.dimension = .dimension_2d,
.mip_level_count = 1,
.array_layer_count = 1,
});
const bind_group = core.device.createBindGroup(
&gpu.BindGroup.Descriptor.init(.{
.layout = bgl,
.entries = &.{
gpu.BindGroup.Entry.buffer(0, uniform_buffer, 0, @sizeOf(UniformBufferObject)),
gpu.BindGroup.Entry.sampler(1, sampler),
gpu.BindGroup.Entry.textureView(2, cube_texture_view),
},
}),
);
const depth_texture = core.device.createTexture(&gpu.Texture.Descriptor{
.usage = .{ .render_attachment = true },
.size = .{ .width = core.descriptor.width, .height = core.descriptor.height },
.format = .depth24_plus,
});
const depth_texture_view = depth_texture.createView(&gpu.TextureView.Descriptor{
.format = .depth24_plus,
.dimension = .dimension_2d,
.array_layer_count = 1,
.mip_level_count = 1,
});
app.timer = try core.Timer.start();
app.title_timer = try core.Timer.start();
app.pipeline = core.device.createRenderPipeline(&pipeline_descriptor);
app.vertex_buffer = vertex_buffer;
app.uniform_buffer = uniform_buffer;
app.bind_group = bind_group;
app.depth_texture = depth_texture;
app.depth_texture_view = depth_texture_view;
app.cube_texture = cube_texture;
app.cube_texture_view = cube_texture_view;
app.cube_texture_render = cube_texture_render;
app.cube_texture_view_render = cube_texture_view_render;
app.sampler = sampler;
app.bgl = bgl;
shader_module.release();
pipeline_layout.release();
}
pub fn deinit(app: *App) void {
defer _ = gpa.deinit();
defer core.deinit();
app.pipeline.release();
app.bgl.release();
app.vertex_buffer.release();
app.uniform_buffer.release();
app.cube_texture.release();
app.cube_texture_render.release();
app.sampler.release();
app.cube_texture_view.release();
app.cube_texture_view_render.release();
app.bind_group.release();
app.depth_texture.?.release();
app.depth_texture_view.release();
}
pub fn update(app: *App) !bool {
var iter = core.pollEvents();
while (iter.next()) |event| {
switch (event) {
.key_press => |ev| {
if (ev.key == .space) return true;
},
.close => return true,
.framebuffer_resize => |ev| {
app.depth_texture.?.release();
app.depth_texture = core.device.createTexture(&gpu.Texture.Descriptor{
.usage = .{ .render_attachment = true },
.size = .{ .width = ev.width, .height = ev.height },
.format = .depth24_plus,
});
app.cube_texture.release();
app.cube_texture = core.device.createTexture(&gpu.Texture.Descriptor{
.usage = .{ .texture_binding = true, .copy_dst = true },
.size = .{ .width = ev.width, .height = ev.height },
.format = core.descriptor.format,
});
app.cube_texture_render.release();
app.cube_texture_render = core.device.createTexture(&gpu.Texture.Descriptor{
.usage = .{ .render_attachment = true, .copy_src = true },
.size = .{ .width = ev.width, .height = ev.height },
.format = core.descriptor.format,
});
app.depth_texture_view.release();
app.depth_texture_view = app.depth_texture.?.createView(&gpu.TextureView.Descriptor{
.format = .depth24_plus,
.dimension = .dimension_2d,
.array_layer_count = 1,
.mip_level_count = 1,
});
app.cube_texture_view.release();
app.cube_texture_view = app.cube_texture.createView(&gpu.TextureView.Descriptor{
.format = core.descriptor.format,
.dimension = .dimension_2d,
.mip_level_count = 1,
.array_layer_count = 1,
});
app.cube_texture_view_render.release();
app.cube_texture_view_render = app.cube_texture_render.createView(&gpu.TextureView.Descriptor{
.format = core.descriptor.format,
.dimension = .dimension_2d,
.mip_level_count = 1,
.array_layer_count = 1,
});
app.bind_group.release();
app.bind_group = core.device.createBindGroup(
&gpu.BindGroup.Descriptor.init(.{
.layout = app.bgl,
.entries = &.{
gpu.BindGroup.Entry.buffer(0, app.uniform_buffer, 0, @sizeOf(UniformBufferObject)),
gpu.BindGroup.Entry.sampler(1, app.sampler),
gpu.BindGroup.Entry.textureView(2, app.cube_texture_view),
},
}),
);
},
else => {},
}
}
const cube_view = app.cube_texture_view_render;
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
const cube_color_attachment = gpu.RenderPassColorAttachment{
.view = cube_view,
.clear_value = gpu.Color{ .r = 0.5, .g = 0.5, .b = 0.5, .a = 1 },
.load_op = .clear,
.store_op = .store,
};
const color_attachment = gpu.RenderPassColorAttachment{
.view = back_buffer_view,
.clear_value = gpu.Color{ .r = 0.5, .g = 0.5, .b = 0.5, .a = 1 },
.load_op = .clear,
.store_op = .store,
};
const depth_stencil_attachment = gpu.RenderPassDepthStencilAttachment{
.view = app.depth_texture_view,
.depth_load_op = .clear,
.depth_store_op = .store,
.depth_clear_value = 1.0,
};
const encoder = core.device.createCommandEncoder(null);
const cube_render_pass_info = gpu.RenderPassDescriptor.init(.{
.color_attachments = &.{cube_color_attachment},
.depth_stencil_attachment = &depth_stencil_attachment,
});
const render_pass_info = gpu.RenderPassDescriptor.init(.{
.color_attachments = &.{color_attachment},
.depth_stencil_attachment = &depth_stencil_attachment,
});
{
const time = app.timer.read();
const model = zm.mul(zm.rotationX(time * (std.math.pi / 2.0)), zm.rotationZ(time * (std.math.pi / 2.0)));
const view = zm.lookAtRh(
zm.Vec{ 0, -4, 0, 1 },
zm.Vec{ 0, 0, 0, 1 },
zm.Vec{ 0, 0, 1, 0 },
);
const proj = zm.perspectiveFovRh(
(std.math.pi * 2.0 / 5.0),
@as(f32, @floatFromInt(core.descriptor.width)) / @as(f32, @floatFromInt(core.descriptor.height)),
1,
100,
);
const ubo = UniformBufferObject{
.mat = zm.transpose(zm.mul(zm.mul(model, view), proj)),
};
encoder.writeBuffer(app.uniform_buffer, 0, &[_]UniformBufferObject{ubo});
}
const pass = encoder.beginRenderPass(&render_pass_info);
pass.setPipeline(app.pipeline);
pass.setBindGroup(0, app.bind_group, &.{0});
pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len);
pass.draw(vertices.len, 1, 0, 0);
pass.end();
pass.release();
encoder.copyTextureToTexture(
&gpu.ImageCopyTexture{
.texture = app.cube_texture_render,
},
&gpu.ImageCopyTexture{
.texture = app.cube_texture,
},
&.{ .width = core.descriptor.width, .height = core.descriptor.height },
);
const cube_pass = encoder.beginRenderPass(&cube_render_pass_info);
cube_pass.setPipeline(app.pipeline);
cube_pass.setBindGroup(0, app.bind_group, &.{0});
cube_pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len);
cube_pass.draw(vertices.len, 1, 0, 0);
cube_pass.end();
cube_pass.release();
var command = encoder.finish(null);
encoder.release();
const queue = core.queue;
queue.submit(&[_]*gpu.CommandBuffer{command});
command.release();
core.swap_chain.present();
back_buffer_view.release();
// update the window title every second
if (app.title_timer.read() >= 1.0) {
app.title_timer.reset();
try core.printTitle("Fractal Cube [ {d}fps ] [ Input {d}hz ]", .{
core.frameRate(),
core.inputRate(),
});
}
return false;
}

View file

@ -0,0 +1,36 @@
struct Uniforms {
matrix : mat4x4<f32>,
};
@binding(0) @group(0) var<uniform> ubo : Uniforms;
struct VertexOut {
@builtin(position) Position : vec4<f32>,
@location(0) fragUV : vec2<f32>,
@location(1) fragPosition: vec4<f32>,
}
@vertex fn vertex_main(
@location(0) position : vec4<f32>,
@location(1) uv: vec2<f32>
) -> VertexOut {
var output : VertexOut;
output.Position = position * ubo.matrix;
output.fragUV = uv;
output.fragPosition = 0.5 * (position + vec4<f32>(1.0, 1.0, 1.0, 1.0));
return output;
}
@binding(1) @group(0) var mySampler: sampler;
@binding(2) @group(0) var myTexture: texture_2d<f32>;
@fragment fn frag_main(
@location(0) fragUV: vec2<f32>,
@location(1) fragPosition: vec4<f32>
) -> @location(0) vec4<f32> {
let texColor = textureSample(myTexture, mySampler, fragUV * 0.8 + vec2<f32>(0.1, 0.1));
let f = f32(length(texColor.rgb - vec3<f32>(0.5, 0.5, 0.5)) < 0.01);
return (1.0 - f) * texColor + f * fragPosition;
// return vec4<f32>(texColor.rgb,1.0);
}

View file

@ -0,0 +1,75 @@
struct CameraUniform {
pos: vec4<f32>,
view_proj: mat4x4<f32>,
};
struct InstanceInput {
@location(3) model_matrix_0: vec4<f32>,
@location(4) model_matrix_1: vec4<f32>,
@location(5) model_matrix_2: vec4<f32>,
@location(6) model_matrix_3: vec4<f32>,
};
struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) normal: vec3<f32>,
@location(2) tex_coords: vec2<f32>,
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) tex_coords: vec2<f32>,
@location(1) normal: vec3<f32>,
@location(2) position: vec3<f32>,
};
struct Light {
position: vec4<f32>,
color: vec4<f32>,
};
@group(0) @binding(0) var<uniform> camera: CameraUniform;
@group(1) @binding(0) var t_diffuse: texture_2d<f32>;
@group(1) @binding(1) var s_diffuse: sampler;
@group(2) @binding(0) var<uniform> light: Light;
@vertex
fn vs_main(model: VertexInput, instance: InstanceInput) -> VertexOutput {
let model_matrix = mat4x4<f32>(
instance.model_matrix_0,
instance.model_matrix_1,
instance.model_matrix_2,
instance.model_matrix_3,
);
var out: VertexOutput;
let world_pos = model_matrix * vec4<f32>(model.position, 1.0);
out.position = world_pos.xyz;
out.normal = (model_matrix * vec4<f32>(model.normal, 0.0)).xyz;
out.clip_position = camera.view_proj * world_pos;
out.tex_coords = model.tex_coords;
return out;
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let object_color = textureSample(t_diffuse, s_diffuse, in.tex_coords);
let ambient = 0.1;
let ambient_color = light.color.rbg * ambient;
let light_dir = normalize(light.position.xyz - in.position);
let diffuse = max(dot(in.normal, light_dir), 0.0);
let diffuse_color = light.color.rgb * diffuse;
let view_dir = normalize(camera.pos.xyz - in.position);
let half_dir = normalize(view_dir + light_dir);
let specular = pow(max(dot(in.normal, half_dir), 0.0), 32.0);
let specular_color = light.color.rbg * specular;
let all = ambient_color + diffuse_color + specular_color;
let result = all * object_color.rgb;
return vec4<f32>(result, object_color.a);
}

View file

@ -0,0 +1,35 @@
struct CameraUniform {
view_pos: vec4<f32>,
view_proj: mat4x4<f32>,
};
struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) normal: vec3<f32>,
@location(2) tex_coords: vec2<f32>,
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
};
struct Light {
position: vec4<f32>,
color: vec4<f32>,
};
@group(0) @binding(0) var<uniform> camera: CameraUniform;
@group(1) @binding(0) var<uniform> light: Light;
@vertex
fn vs_main(model: VertexInput) -> VertexOutput {
var out: VertexOutput;
let world_pos = vec4<f32>(model.position + light.position.xyz, 1.0);
out.clip_position = camera.view_proj * world_pos;
return out;
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return vec4<f32>(1.0, 1.0, 1.0, 0.5);
}

View file

@ -0,0 +1,891 @@
// in this example:
// - comptime generated image data for texture
// - Blinn-Phong lighting
// - several pipelines
//
// quit with escape, q or space
// move camera with arrows or wasd
const std = @import("std");
const core = @import("mach").core;
const gpu = core.gpu;
const zm = @import("zmath");
const Vec = zm.Vec;
const Mat = zm.Mat;
const Quat = zm.Quat;
pub const App = @This();
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
title_timer: core.Timer,
timer: core.Timer,
cube: Cube,
camera: Camera,
light: Light,
depth: Texture,
keys: u8,
const Dir = struct {
const up: u8 = 0b0001;
const down: u8 = 0b0010;
const left: u8 = 0b0100;
const right: u8 = 0b1000;
};
pub fn init(app: *App) !void {
try core.init(.{});
app.title_timer = try core.Timer.start();
app.timer = try core.Timer.start();
const eye = Vec{ 5.0, 7.0, 5.0, 0.0 };
const target = Vec{ 0.0, 0.0, 0.0, 0.0 };
const framebuffer = core.descriptor;
const aspect_ratio = @as(f32, @floatFromInt(framebuffer.width)) / @as(f32, @floatFromInt(framebuffer.height));
app.cube = Cube.init();
app.light = Light.init();
app.depth = Texture.depth(core.device, framebuffer.width, framebuffer.height);
app.camera = Camera.init(core.device, eye, target, zm.Vec{ 0.0, 1.0, 0.0, 0.0 }, aspect_ratio, 45.0, 0.1, 100.0);
app.keys = 0;
}
pub fn deinit(app: *App) void {
defer _ = gpa.deinit();
defer core.deinit();
app.cube.deinit();
app.camera.deinit();
app.light.deinit();
app.depth.release();
}
pub fn update(app: *App) !bool {
const delta_time = app.timer.lap();
var iter = core.pollEvents();
while (iter.next()) |event| {
switch (event) {
.key_press => |ev| switch (ev.key) {
.q, .escape, .space => return true,
.w, .up => {
app.keys |= Dir.up;
},
.s, .down => {
app.keys |= Dir.down;
},
.a, .left => {
app.keys |= Dir.left;
},
.d, .right => {
app.keys |= Dir.right;
},
.one => core.setDisplayMode(.windowed),
.two => core.setDisplayMode(.fullscreen),
.three => core.setDisplayMode(.borderless),
else => {},
},
.key_release => |ev| switch (ev.key) {
.w, .up => {
app.keys &= ~Dir.up;
},
.s, .down => {
app.keys &= ~Dir.down;
},
.a, .left => {
app.keys &= ~Dir.left;
},
.d, .right => {
app.keys &= ~Dir.right;
},
else => {},
},
.framebuffer_resize => |ev| {
// recreates the sampler, which is a waste, but for an example it's ok
app.depth.release();
app.depth = Texture.depth(core.device, ev.width, ev.height);
},
.close => return true,
else => {},
}
}
// move camera
const speed = zm.Vec{ delta_time * 5, delta_time * 5, delta_time * 5, delta_time * 5 };
const fwd = zm.normalize3(app.camera.target - app.camera.eye);
const right = zm.normalize3(zm.cross3(fwd, app.camera.up));
if (app.keys & Dir.up != 0)
app.camera.eye += fwd * speed;
if (app.keys & Dir.down != 0)
app.camera.eye -= fwd * speed;
if (app.keys & Dir.right != 0)
app.camera.eye += right * speed
else if (app.keys & Dir.left != 0)
app.camera.eye -= right * speed
else
app.camera.eye += right * (speed * @Vector(4, f32){ 0.5, 0.5, 0.5, 0.5 });
const queue = core.queue;
app.camera.update(queue);
// move light
const light_speed = delta_time * 2.5;
app.light.update(queue, light_speed);
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
defer back_buffer_view.release();
const encoder = core.device.createCommandEncoder(null);
defer encoder.release();
const color_attachment = gpu.RenderPassColorAttachment{
.view = back_buffer_view,
.clear_value = gpu.Color{ .r = 0.0, .g = 0.0, .b = 0.4, .a = 1.0 },
.load_op = .clear,
.store_op = .store,
};
const render_pass_descriptor = gpu.RenderPassDescriptor.init(.{
.color_attachments = &.{color_attachment},
.depth_stencil_attachment = &.{
.view = app.depth.view,
.depth_load_op = .clear,
.depth_store_op = .store,
.depth_clear_value = 1.0,
},
});
const pass = encoder.beginRenderPass(&render_pass_descriptor);
defer pass.release();
// brick cubes
pass.setPipeline(app.cube.pipeline);
pass.setBindGroup(0, app.camera.bind_group, &.{});
pass.setBindGroup(1, app.cube.texture.bind_group.?, &.{});
pass.setBindGroup(2, app.light.bind_group, &.{});
pass.setVertexBuffer(0, app.cube.mesh.buffer, 0, app.cube.mesh.size);
pass.setVertexBuffer(1, app.cube.instance.buffer, 0, app.cube.instance.size);
pass.draw(4, app.cube.instance.len, 0, 0);
pass.draw(4, app.cube.instance.len, 4, 0);
pass.draw(4, app.cube.instance.len, 8, 0);
pass.draw(4, app.cube.instance.len, 12, 0);
pass.draw(4, app.cube.instance.len, 16, 0);
pass.draw(4, app.cube.instance.len, 20, 0);
// light source
pass.setPipeline(app.light.pipeline);
pass.setBindGroup(0, app.camera.bind_group, &.{});
pass.setBindGroup(1, app.light.bind_group, &.{});
pass.setVertexBuffer(0, app.cube.mesh.buffer, 0, app.cube.mesh.size);
pass.draw(4, 1, 0, 0);
pass.draw(4, 1, 4, 0);
pass.draw(4, 1, 8, 0);
pass.draw(4, 1, 12, 0);
pass.draw(4, 1, 16, 0);
pass.draw(4, 1, 20, 0);
pass.end();
var command = encoder.finish(null);
defer command.release();
queue.submit(&[_]*gpu.CommandBuffer{command});
core.swap_chain.present();
// update the window title every second
if (app.title_timer.read() >= 1.0) {
app.title_timer.reset();
try core.printTitle("Gen Texture Light [ {d}fps ] [ Input {d}hz ]", .{
core.frameRate(),
core.inputRate(),
});
}
return false;
}
const Camera = struct {
const Self = @This();
eye: Vec,
target: Vec,
up: Vec,
aspect: f32,
fovy: f32,
near: f32,
far: f32,
bind_group: *gpu.BindGroup,
buffer: Buffer,
const Uniform = extern struct {
pos: Vec,
mat: Mat,
};
fn init(device: *gpu.Device, eye: Vec, target: Vec, up: Vec, aspect: f32, fovy: f32, near: f32, far: f32) Self {
var self: Self = .{
.eye = eye,
.target = target,
.up = up,
.aspect = aspect,
.near = near,
.far = far,
.fovy = fovy,
.buffer = undefined,
.bind_group = undefined,
};
const view = self.buildViewProjMatrix();
const uniform = Uniform{
.pos = self.eye,
.mat = view,
};
const buffer = .{
.buffer = initBuffer(device, .{ .uniform = true }, &@as([20]f32, @bitCast(uniform))),
.size = @sizeOf(@TypeOf(uniform)),
};
const layout = Self.bindGroupLayout(device);
const bind_group = device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{
.layout = layout,
.entries = &.{
gpu.BindGroup.Entry.buffer(0, buffer.buffer, 0, buffer.size),
},
}));
layout.release();
self.buffer = buffer;
self.bind_group = bind_group;
return self;
}
fn deinit(self: *Self) void {
self.bind_group.release();
self.buffer.release();
}
fn update(self: *Self, queue: *gpu.Queue) void {
const mat = self.buildViewProjMatrix();
const uniform = .{
.pos = self.eye,
.mat = mat,
};
queue.writeBuffer(self.buffer.buffer, 0, &[_]Uniform{uniform});
}
inline fn buildViewProjMatrix(s: *const Camera) Mat {
const view = zm.lookAtRh(s.eye, s.target, s.up);
const proj = zm.perspectiveFovRh(s.fovy, s.aspect, s.near, s.far);
return zm.mul(view, proj);
}
inline fn bindGroupLayout(device: *gpu.Device) *gpu.BindGroupLayout {
const visibility = .{ .vertex = true, .fragment = true };
return device.createBindGroupLayout(&gpu.BindGroupLayout.Descriptor.init(.{
.entries = &.{
gpu.BindGroupLayout.Entry.buffer(0, visibility, .uniform, false, 0),
},
}));
}
};
const Buffer = struct {
const Self = @This();
buffer: *gpu.Buffer,
size: usize,
len: u32 = 0,
fn release(self: *Self) void {
self.buffer.release();
}
};
const Cube = struct {
const Self = @This();
pipeline: *gpu.RenderPipeline,
mesh: Buffer,
instance: Buffer,
texture: Texture,
const IPR = 20; // instances per row
const SPACING = 2; // spacing between cubes
const DISPLACEMENT = vec3u(IPR * SPACING / 2, 0, IPR * SPACING / 2);
fn init() Self {
const device = core.device;
const texture = Brick.texture(device);
// instance buffer
var ibuf: [IPR * IPR * 16]f32 = undefined;
var z: usize = 0;
while (z < IPR) : (z += 1) {
var x: usize = 0;
while (x < IPR) : (x += 1) {
const pos = vec3u(x * SPACING, 0, z * SPACING) - DISPLACEMENT;
const rot = blk: {
if (pos[0] == 0 and pos[2] == 0) {
break :blk zm.rotationZ(0.0);
} else {
break :blk zm.mul(zm.rotationX(zm.clamp(zm.abs(pos[0]), 0, 45.0)), zm.rotationZ(zm.clamp(zm.abs(pos[2]), 0, 45.0)));
}
};
const index = z * IPR + x;
const inst = Instance{
.position = pos,
.rotation = rot,
};
zm.storeMat(ibuf[index * 16 ..], inst.toMat());
}
}
const instance = Buffer{
.buffer = initBuffer(device, .{ .vertex = true }, &ibuf),
.len = IPR * IPR,
.size = @sizeOf(@TypeOf(ibuf)),
};
return Self{
.mesh = mesh(device),
.texture = texture,
.instance = instance,
.pipeline = pipeline(),
};
}
fn deinit(self: *Self) void {
self.pipeline.release();
self.mesh.release();
self.instance.release();
self.texture.release();
}
fn pipeline() *gpu.RenderPipeline {
const device = core.device;
const camera_layout = Camera.bindGroupLayout(device);
const texture_layout = Texture.bindGroupLayout(device);
const light_layout = Light.bindGroupLayout(device);
const layout_descriptor = gpu.PipelineLayout.Descriptor.init(.{
.bind_group_layouts = &.{
camera_layout,
texture_layout,
light_layout,
},
});
defer camera_layout.release();
defer texture_layout.release();
defer light_layout.release();
const layout = device.createPipelineLayout(&layout_descriptor);
defer layout.release();
const shader = device.createShaderModuleWGSL("cube.wgsl", @embedFile("cube.wgsl"));
defer shader.release();
const blend = gpu.BlendState{};
const color_target = gpu.ColorTargetState{
.format = core.descriptor.format,
.blend = &blend,
};
const fragment = gpu.FragmentState.init(.{
.module = shader,
.entry_point = "fs_main",
.targets = &.{color_target},
});
const descriptor = gpu.RenderPipeline.Descriptor{
.layout = layout,
.fragment = &fragment,
.vertex = gpu.VertexState.init(.{
.module = shader,
.entry_point = "vs_main",
.buffers = &.{
Self.vertexBufferLayout(),
Self.instanceLayout(),
},
}),
.depth_stencil = &.{
.format = Texture.DEPTH_FORMAT,
.depth_write_enabled = .true,
.depth_compare = .less,
},
.primitive = .{
.cull_mode = .back,
.topology = .triangle_strip,
},
};
return device.createRenderPipeline(&descriptor);
}
fn mesh(device: *gpu.Device) Buffer {
// generated texture has aspect ratio of 1:2
// `h` reflects that ratio
// `v` sets how many times texture repeats across surface
const v = 2;
const h = v * 2;
const buf = asFloats(.{
// z+ face
0, 0, 1, 0, 0, 1, 0, h,
1, 0, 1, 0, 0, 1, v, h,
0, 1, 1, 0, 0, 1, 0, 0,
1, 1, 1, 0, 0, 1, v, 0,
// z- face
1, 0, 0, 0, 0, -1, 0, h,
0, 0, 0, 0, 0, -1, v, h,
1, 1, 0, 0, 0, -1, 0, 0,
0, 1, 0, 0, 0, -1, v, 0,
// x+ face
1, 0, 1, 1, 0, 0, 0, h,
1, 0, 0, 1, 0, 0, v, h,
1, 1, 1, 1, 0, 0, 0, 0,
1, 1, 0, 1, 0, 0, v, 0,
// x- face
0, 0, 0, -1, 0, 0, 0, h,
0, 0, 1, -1, 0, 0, v, h,
0, 1, 0, -1, 0, 0, 0, 0,
0, 1, 1, -1, 0, 0, v, 0,
// y+ face
1, 1, 0, 0, 1, 0, 0, h,
0, 1, 0, 0, 1, 0, v, h,
1, 1, 1, 0, 1, 0, 0, 0,
0, 1, 1, 0, 1, 0, v, 0,
// y- face
0, 0, 0, 0, -1, 0, 0, h,
1, 0, 0, 0, -1, 0, v, h,
0, 0, 1, 0, -1, 0, 0, 0,
1, 0, 1, 0, -1, 0, v, 0,
});
return Buffer{
.buffer = initBuffer(device, .{ .vertex = true }, &buf),
.size = @sizeOf(@TypeOf(buf)),
};
}
fn vertexBufferLayout() gpu.VertexBufferLayout {
const attributes = [_]gpu.VertexAttribute{
.{
.format = .float32x3,
.offset = 0,
.shader_location = 0,
},
.{
.format = .float32x3,
.offset = @sizeOf([3]f32),
.shader_location = 1,
},
.{
.format = .float32x2,
.offset = @sizeOf([6]f32),
.shader_location = 2,
},
};
return gpu.VertexBufferLayout.init(.{
.array_stride = @sizeOf([8]f32),
.attributes = &attributes,
});
}
fn instanceLayout() gpu.VertexBufferLayout {
const attributes = [_]gpu.VertexAttribute{
.{
.format = .float32x4,
.offset = 0,
.shader_location = 3,
},
.{
.format = .float32x4,
.offset = @sizeOf([4]f32),
.shader_location = 4,
},
.{
.format = .float32x4,
.offset = @sizeOf([8]f32),
.shader_location = 5,
},
.{
.format = .float32x4,
.offset = @sizeOf([12]f32),
.shader_location = 6,
},
};
return gpu.VertexBufferLayout.init(.{
.array_stride = @sizeOf([16]f32),
.step_mode = .instance,
.attributes = &attributes,
});
}
};
fn asFloats(comptime arr: anytype) [arr.len]f32 {
const len = arr.len;
comptime var out: [len]f32 = undefined;
comptime var i = 0;
inline while (i < len) : (i += 1) {
out[i] = @as(f32, @floatFromInt(arr[i]));
}
return out;
}
const Brick = struct {
const W = 12;
const H = 6;
fn texture(device: *gpu.Device) Texture {
const slice: []const u8 = &data();
return Texture.fromData(device, W, H, u8, slice);
}
fn data() [W * H * 4]u8 {
comptime var out: [W * H * 4]u8 = undefined;
// fill all the texture with brick color
comptime var i = 0;
inline while (i < H) : (i += 1) {
comptime var j = 0;
inline while (j < W * 4) : (j += 4) {
out[i * W * 4 + j + 0] = 210;
out[i * W * 4 + j + 1] = 30;
out[i * W * 4 + j + 2] = 30;
out[i * W * 4 + j + 3] = 0;
}
}
const f = 10;
// fill the cement lines
inline for ([_]comptime_int{ 0, 1 }) |k| {
inline for ([_]comptime_int{ 5 * 4, 11 * 4 }) |m| {
out[k * W * 4 + m + 0] = f;
out[k * W * 4 + m + 1] = f;
out[k * W * 4 + m + 2] = f;
out[k * W * 4 + m + 3] = 0;
}
}
inline for ([_]comptime_int{ 3, 4 }) |k| {
inline for ([_]comptime_int{ 2 * 4, 8 * 4 }) |m| {
out[k * W * 4 + m + 0] = f;
out[k * W * 4 + m + 1] = f;
out[k * W * 4 + m + 2] = f;
out[k * W * 4 + m + 3] = 0;
}
}
inline for ([_]comptime_int{ 2, 5 }) |k| {
comptime var m = 0;
inline while (m < W * 4) : (m += 4) {
out[k * W * 4 + m + 0] = f;
out[k * W * 4 + m + 1] = f;
out[k * W * 4 + m + 2] = f;
out[k * W * 4 + m + 3] = 0;
}
}
return out;
}
};
// don't confuse with gpu.Texture
const Texture = struct {
const Self = @This();
texture: *gpu.Texture,
view: *gpu.TextureView,
sampler: *gpu.Sampler,
bind_group: ?*gpu.BindGroup,
const DEPTH_FORMAT = .depth32_float;
const FORMAT = .rgba8_unorm;
fn release(self: *Self) void {
self.texture.release();
self.view.release();
self.sampler.release();
if (self.bind_group) |bind_group| bind_group.release();
}
fn fromData(device: *gpu.Device, width: u32, height: u32, comptime T: type, data: []const T) Self {
const extent = gpu.Extent3D{
.width = width,
.height = height,
};
const texture = device.createTexture(&gpu.Texture.Descriptor{
.size = extent,
.format = FORMAT,
.usage = .{ .copy_dst = true, .texture_binding = true },
});
const view = texture.createView(&gpu.TextureView.Descriptor{
.format = FORMAT,
.dimension = .dimension_2d,
.array_layer_count = 1,
.mip_level_count = 1,
});
const sampler = device.createSampler(&gpu.Sampler.Descriptor{
.address_mode_u = .repeat,
.address_mode_v = .repeat,
.address_mode_w = .repeat,
.mag_filter = .linear,
.min_filter = .linear,
.mipmap_filter = .linear,
.max_anisotropy = 1, // 1,2,4,8,16
});
core.queue.writeTexture(
&gpu.ImageCopyTexture{
.texture = texture,
},
&gpu.Texture.DataLayout{
.bytes_per_row = 4 * width,
.rows_per_image = height,
},
&extent,
data,
);
const bind_group_layout = Self.bindGroupLayout(device);
const bind_group = device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{
.layout = bind_group_layout,
.entries = &.{
gpu.BindGroup.Entry.textureView(0, view),
gpu.BindGroup.Entry.sampler(1, sampler),
},
}));
bind_group_layout.release();
return Self{
.view = view,
.texture = texture,
.sampler = sampler,
.bind_group = bind_group,
};
}
fn depth(device: *gpu.Device, width: u32, height: u32) Self {
const extent = gpu.Extent3D{
.width = width,
.height = height,
};
const texture = device.createTexture(&gpu.Texture.Descriptor{
.size = extent,
.format = DEPTH_FORMAT,
.usage = .{
.render_attachment = true,
.texture_binding = true,
},
});
const view = texture.createView(&gpu.TextureView.Descriptor{
.dimension = .dimension_2d,
.array_layer_count = 1,
.mip_level_count = 1,
});
const sampler = device.createSampler(&gpu.Sampler.Descriptor{
.mag_filter = .linear,
.compare = .less_equal,
});
return Self{
.texture = texture,
.view = view,
.sampler = sampler,
.bind_group = null, // not used
};
}
inline fn bindGroupLayout(device: *gpu.Device) *gpu.BindGroupLayout {
const visibility = .{ .fragment = true };
const Entry = gpu.BindGroupLayout.Entry;
return device.createBindGroupLayout(&gpu.BindGroupLayout.Descriptor.init(.{
.entries = &.{
Entry.texture(0, visibility, .float, .dimension_2d, false),
Entry.sampler(1, visibility, .filtering),
},
}));
}
};
const Light = struct {
const Self = @This();
uniform: Uniform,
buffer: Buffer,
bind_group: *gpu.BindGroup,
pipeline: *gpu.RenderPipeline,
const Uniform = extern struct {
position: Vec,
color: Vec,
};
fn init() Self {
const device = core.device;
const uniform = Uniform{
.color = vec3u(1, 1, 1),
.position = vec3u(3, 7, 2),
};
const buffer = .{
.buffer = initBuffer(device, .{ .uniform = true }, &@as([8]f32, @bitCast(uniform))),
.size = @sizeOf(@TypeOf(uniform)),
};
const layout = Self.bindGroupLayout(device);
const bind_group = device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{
.layout = layout,
.entries = &.{
gpu.BindGroup.Entry.buffer(0, buffer.buffer, 0, buffer.size),
},
}));
layout.release();
return Self{
.buffer = buffer,
.uniform = uniform,
.bind_group = bind_group,
.pipeline = Self.pipeline(),
};
}
fn deinit(self: *Self) void {
self.buffer.release();
self.bind_group.release();
self.pipeline.release();
}
fn update(self: *Self, queue: *gpu.Queue, delta: f32) void {
const old = self.uniform;
const new = Light.Uniform{
.position = zm.qmul(zm.quatFromAxisAngle(vec3u(0, 1, 0), delta), old.position),
.color = old.color,
};
queue.writeBuffer(self.buffer.buffer, 0, &[_]Light.Uniform{new});
self.uniform = new;
}
inline fn bindGroupLayout(device: *gpu.Device) *gpu.BindGroupLayout {
const visibility = .{ .vertex = true, .fragment = true };
const Entry = gpu.BindGroupLayout.Entry;
return device.createBindGroupLayout(&gpu.BindGroupLayout.Descriptor.init(.{
.entries = &.{
Entry.buffer(0, visibility, .uniform, false, 0),
},
}));
}
fn pipeline() *gpu.RenderPipeline {
const device = core.device;
const camera_layout = Camera.bindGroupLayout(device);
const light_layout = Light.bindGroupLayout(device);
const layout_descriptor = gpu.PipelineLayout.Descriptor.init(.{
.bind_group_layouts = &.{
camera_layout,
light_layout,
},
});
defer camera_layout.release();
defer light_layout.release();
const layout = device.createPipelineLayout(&layout_descriptor);
defer layout.release();
const shader = core.device.createShaderModuleWGSL("light.wgsl", @embedFile("light.wgsl"));
defer shader.release();
const blend = gpu.BlendState{};
const color_target = gpu.ColorTargetState{
.format = core.descriptor.format,
.blend = &blend,
};
const fragment = gpu.FragmentState.init(.{
.module = shader,
.entry_point = "fs_main",
.targets = &.{color_target},
});
const descriptor = gpu.RenderPipeline.Descriptor{
.layout = layout,
.fragment = &fragment,
.vertex = gpu.VertexState.init(.{
.module = shader,
.entry_point = "vs_main",
.buffers = &.{
Cube.vertexBufferLayout(),
},
}),
.depth_stencil = &.{
.format = Texture.DEPTH_FORMAT,
.depth_write_enabled = .true,
.depth_compare = .less,
},
.primitive = .{
.cull_mode = .back,
.topology = .triangle_strip,
},
};
return device.createRenderPipeline(&descriptor);
}
};
inline fn initBuffer(device: *gpu.Device, usage: gpu.Buffer.UsageFlags, data: anytype) *gpu.Buffer {
std.debug.assert(@typeInfo(@TypeOf(data)) == .Pointer);
const T = std.meta.Elem(@TypeOf(data));
var u = usage;
u.copy_dst = true;
const buffer = device.createBuffer(&.{
.size = @sizeOf(T) * data.len,
.usage = u,
.mapped_at_creation = .true,
});
const mapped = buffer.getMappedRange(T, 0, data.len);
@memcpy(mapped.?, data);
buffer.unmap();
return buffer;
}
fn vec3i(x: isize, y: isize, z: isize) Vec {
return Vec{ @floatFromInt(x), @floatFromInt(y), @floatFromInt(z), 0.0 };
}
fn vec3u(x: usize, y: usize, z: usize) Vec {
return zm.Vec{ @floatFromInt(x), @floatFromInt(y), @floatFromInt(z), 0.0 };
}
// todo indside Cube
const Instance = struct {
const Self = @This();
position: Vec,
rotation: Mat,
fn toMat(self: *const Self) Mat {
return zm.mul(self.rotation, zm.translationV(self.position));
}
};

View file

@ -0,0 +1,81 @@
struct Params {
filterDim : i32,
blockDim : u32,
}
@group(0) @binding(0) var samp : sampler;
@group(0) @binding(1) var<uniform> params : Params;
@group(1) @binding(1) var inputTex : texture_2d<f32>;
@group(1) @binding(2) var outputTex : texture_storage_2d<rgba8unorm, write>;
struct Flip {
value : u32,
}
@group(1) @binding(3) var<uniform> flip : Flip;
// This shader blurs the input texture in one direction, depending on whether
// |flip.value| is 0 or 1.
// It does so by running (128 / 4) threads per workgroup to load 128
// texels into 4 rows of shared memory. Each thread loads a
// 4 x 4 block of texels to take advantage of the texture sampling
// hardware.
// Then, each thread computes the blur result by averaging the adjacent texel values
// in shared memory.
// Because we're operating on a subset of the texture, we cannot compute all of the
// results since not all of the neighbors are available in shared memory.
// Specifically, with 128 x 128 tiles, we can only compute and write out
// square blocks of size 128 - (filterSize - 1). We compute the number of blocks
// needed in Javascript and dispatch that amount.
var<workgroup> tile : array<array<vec3<f32>, 128>, 4>;
@compute @workgroup_size(32, 1, 1)
fn main(
@builtin(workgroup_id) WorkGroupID : vec3<u32>,
@builtin(local_invocation_id) LocalInvocationID : vec3<u32>
) {
let filterOffset = (params.filterDim - 1) / 2;
let dims = vec2<i32>(textureDimensions(inputTex, 0));
let baseIndex = vec2<i32>(WorkGroupID.xy * vec2(params.blockDim, 4) +
LocalInvocationID.xy * vec2(4, 1))
- vec2(filterOffset, 0);
for (var r = 0; r < 4; r++) {
for (var c = 0; c < 4; c++) {
var loadIndex = baseIndex + vec2(c, r);
if (flip.value != 0u) {
loadIndex = loadIndex.yx;
}
tile[r][4 * LocalInvocationID.x + u32(c)] = textureSampleLevel(
inputTex,
samp,
(vec2<f32>(loadIndex) + vec2<f32>(0.25, 0.25)) / vec2<f32>(dims),
0.0
).rgb;
}
}
workgroupBarrier();
for (var r = 0; r < 4; r++) {
for (var c = 0; c < 4; c++) {
var writeIndex = baseIndex + vec2(c, r);
if (flip.value != 0) {
writeIndex = writeIndex.yx;
}
let center = i32(4 * LocalInvocationID.x) + c;
if (center >= filterOffset &&
center < 128 - filterOffset &&
all(writeIndex < dims)) {
var acc = vec3(0.0, 0.0, 0.0);
for (var f = 0; f < params.filterDim; f++) {
var i = center + f - filterOffset;
acc = acc + (1.0 / f32(params.filterDim)) * tile[r][i];
}
textureStore(outputTex, writeIndex, vec4(acc, 1.0));
}
}
}
}

View file

@ -0,0 +1,38 @@
@group(0) @binding(0) var mySampler : sampler;
@group(0) @binding(1) var myTexture : texture_2d<f32>;
struct VertexOutput {
@builtin(position) Position : vec4<f32>,
@location(0) fragUV : vec2<f32>,
}
@vertex
fn vert_main(@builtin(vertex_index) VertexIndex : u32) -> VertexOutput {
var pos = array<vec2<f32>, 6>(
vec2<f32>( 1.0, 1.0),
vec2<f32>( 1.0, -1.0),
vec2<f32>(-1.0, -1.0),
vec2<f32>( 1.0, 1.0),
vec2<f32>(-1.0, -1.0),
vec2<f32>(-1.0, 1.0)
);
var uv = array<vec2<f32>, 6>(
vec2<f32>(1.0, 0.0),
vec2<f32>(1.0, 1.0),
vec2<f32>(0.0, 1.0),
vec2<f32>(1.0, 0.0),
vec2<f32>(0.0, 1.0),
vec2<f32>(0.0, 0.0)
);
var output : VertexOutput;
output.Position = vec4<f32>(pos[VertexIndex], 0.0, 1.0);
output.fragUV = uv[VertexIndex];
return output;
}
@fragment
fn frag_main(@location(0) fragUV : vec2<f32>) -> @location(0) vec4<f32> {
return textureSample(myTexture, mySampler, fragUV);
}

View file

@ -0,0 +1,326 @@
const std = @import("std");
const core = @import("mach").core;
const gpu = core.gpu;
const zigimg = @import("zigimg");
const assets = @import("assets");
title_timer: core.Timer,
blur_pipeline: *gpu.ComputePipeline,
fullscreen_quad_pipeline: *gpu.RenderPipeline,
cube_texture: *gpu.Texture,
textures: [2]*gpu.Texture,
blur_params_buffer: *gpu.Buffer,
compute_constants: *gpu.BindGroup,
compute_bind_group_0: *gpu.BindGroup,
compute_bind_group_1: *gpu.BindGroup,
compute_bind_group_2: *gpu.BindGroup,
show_result_bind_group: *gpu.BindGroup,
img_size: gpu.Extent3D,
pub const App = @This();
// Constants from the blur.wgsl shader
const tile_dimension: u32 = 128;
const batch: [2]u32 = .{ 4, 4 };
// Currently hardcoded
const filter_size: u32 = 15;
const iterations: u32 = 2;
var block_dimension: u32 = tile_dimension - (filter_size - 1);
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
pub fn init(app: *App) !void {
try core.init(.{});
const allocator = gpa.allocator();
const queue = core.queue;
const blur_shader_module = core.device.createShaderModuleWGSL("blur.wgsl", @embedFile("blur.wgsl"));
const blur_pipeline_descriptor = gpu.ComputePipeline.Descriptor{
.compute = gpu.ProgrammableStageDescriptor{
.module = blur_shader_module,
.entry_point = "main",
},
};
const blur_pipeline = core.device.createComputePipeline(&blur_pipeline_descriptor);
blur_shader_module.release();
const fullscreen_quad_vs_module = core.device.createShaderModuleWGSL(
"fullscreen_textured_quad.wgsl",
@embedFile("fullscreen_textured_quad.wgsl"),
);
const fullscreen_quad_fs_module = core.device.createShaderModuleWGSL(
"fullscreen_textured_quad.wgsl",
@embedFile("fullscreen_textured_quad.wgsl"),
);
const blend = gpu.BlendState{};
const color_target = gpu.ColorTargetState{
.format = core.descriptor.format,
.blend = &blend,
.write_mask = gpu.ColorWriteMaskFlags.all,
};
const fragment_state = gpu.FragmentState.init(.{
.module = fullscreen_quad_fs_module,
.entry_point = "frag_main",
.targets = &.{color_target},
});
const fullscreen_quad_pipeline_descriptor = gpu.RenderPipeline.Descriptor{
.fragment = &fragment_state,
.vertex = .{
.module = fullscreen_quad_vs_module,
.entry_point = "vert_main",
},
};
const fullscreen_quad_pipeline = core.device.createRenderPipeline(&fullscreen_quad_pipeline_descriptor);
fullscreen_quad_vs_module.release();
fullscreen_quad_fs_module.release();
const sampler = core.device.createSampler(&.{
.mag_filter = .linear,
.min_filter = .linear,
});
var img = try zigimg.Image.fromMemory(allocator, assets.gotta_go_fast_png);
defer img.deinit();
const img_size = gpu.Extent3D{ .width = @as(u32, @intCast(img.width)), .height = @as(u32, @intCast(img.height)) };
const cube_texture = core.device.createTexture(&.{
.size = img_size,
.format = .rgba8_unorm,
.usage = .{
.texture_binding = true,
.copy_dst = true,
.render_attachment = true,
},
});
const data_layout = gpu.Texture.DataLayout{
.bytes_per_row = @as(u32, @intCast(img.width * 4)),
.rows_per_image = @as(u32, @intCast(img.height)),
};
switch (img.pixels) {
.rgba32 => |pixels| queue.writeTexture(&.{ .texture = cube_texture }, &data_layout, &img_size, pixels),
.rgb24 => |pixels| {
const data = try rgb24ToRgba32(allocator, pixels);
defer data.deinit(allocator);
queue.writeTexture(&.{ .texture = cube_texture }, &data_layout, &img_size, data.rgba32);
},
else => @panic("unsupported image color format"),
}
var textures: [2]*gpu.Texture = undefined;
for (textures, 0..) |_, i| {
textures[i] = core.device.createTexture(&.{
.size = img_size,
.format = .rgba8_unorm,
.usage = .{
.storage_binding = true,
.texture_binding = true,
.copy_dst = true,
},
});
}
// the shader blurs the input texture in one direction,
// depending on whether flip value is 0 or 1
var flip: [2]*gpu.Buffer = undefined;
for (flip, 0..) |_, i| {
const buffer = core.device.createBuffer(&.{
.usage = .{ .uniform = true },
.size = @sizeOf(u32),
.mapped_at_creation = .true,
});
const buffer_mapped = buffer.getMappedRange(u32, 0, 1);
buffer_mapped.?[0] = @as(u32, @intCast(i));
buffer.unmap();
flip[i] = buffer;
}
const blur_params_buffer = core.device.createBuffer(&.{
.size = 8,
.usage = .{ .copy_dst = true, .uniform = true },
});
const blur_bind_group_layout0 = blur_pipeline.getBindGroupLayout(0);
const blur_bind_group_layout1 = blur_pipeline.getBindGroupLayout(1);
const fullscreen_bind_group_layout = fullscreen_quad_pipeline.getBindGroupLayout(0);
const cube_texture_view = cube_texture.createView(&gpu.TextureView.Descriptor{});
const texture0_view = textures[0].createView(&gpu.TextureView.Descriptor{});
const texture1_view = textures[1].createView(&gpu.TextureView.Descriptor{});
const compute_constants = core.device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{
.layout = blur_bind_group_layout0,
.entries = &.{
gpu.BindGroup.Entry.sampler(0, sampler),
gpu.BindGroup.Entry.buffer(1, blur_params_buffer, 0, 8),
},
}));
const compute_bind_group_0 = core.device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{
.layout = blur_bind_group_layout1,
.entries = &.{
gpu.BindGroup.Entry.textureView(1, cube_texture_view),
gpu.BindGroup.Entry.textureView(2, texture0_view),
gpu.BindGroup.Entry.buffer(3, flip[0], 0, 4),
},
}));
const compute_bind_group_1 = core.device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{
.layout = blur_bind_group_layout1,
.entries = &.{
gpu.BindGroup.Entry.textureView(1, texture0_view),
gpu.BindGroup.Entry.textureView(2, texture1_view),
gpu.BindGroup.Entry.buffer(3, flip[1], 0, 4),
},
}));
const compute_bind_group_2 = core.device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{
.layout = blur_bind_group_layout1,
.entries = &.{
gpu.BindGroup.Entry.textureView(1, texture1_view),
gpu.BindGroup.Entry.textureView(2, texture0_view),
gpu.BindGroup.Entry.buffer(3, flip[0], 0, 4),
},
}));
const show_result_bind_group = core.device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{
.layout = fullscreen_bind_group_layout,
.entries = &.{
gpu.BindGroup.Entry.sampler(0, sampler),
gpu.BindGroup.Entry.textureView(1, texture1_view),
},
}));
blur_bind_group_layout0.release();
blur_bind_group_layout1.release();
fullscreen_bind_group_layout.release();
sampler.release();
flip[0].release();
flip[1].release();
cube_texture_view.release();
texture0_view.release();
texture1_view.release();
const blur_params_buffer_data = [_]u32{ filter_size, block_dimension };
queue.writeBuffer(blur_params_buffer, 0, &blur_params_buffer_data);
app.title_timer = try core.Timer.start();
app.blur_pipeline = blur_pipeline;
app.fullscreen_quad_pipeline = fullscreen_quad_pipeline;
app.cube_texture = cube_texture;
app.textures = textures;
app.blur_params_buffer = blur_params_buffer;
app.compute_constants = compute_constants;
app.compute_bind_group_0 = compute_bind_group_0;
app.compute_bind_group_1 = compute_bind_group_1;
app.compute_bind_group_2 = compute_bind_group_2;
app.show_result_bind_group = show_result_bind_group;
app.img_size = img_size;
}
pub fn deinit(app: *App) void {
defer _ = gpa.deinit();
defer core.deinit();
app.blur_pipeline.release();
app.fullscreen_quad_pipeline.release();
app.cube_texture.release();
app.textures[0].release();
app.textures[1].release();
app.blur_params_buffer.release();
app.compute_constants.release();
app.compute_bind_group_0.release();
app.compute_bind_group_1.release();
app.compute_bind_group_2.release();
app.show_result_bind_group.release();
}
pub fn update(app: *App) !bool {
var iter = core.pollEvents();
while (iter.next()) |event| {
if (event == .close) return true;
}
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
const encoder = core.device.createCommandEncoder(null);
const compute_pass = encoder.beginComputePass(null);
compute_pass.setPipeline(app.blur_pipeline);
compute_pass.setBindGroup(0, app.compute_constants, &.{});
const width: u32 = @as(u32, @intCast(app.img_size.width));
const height: u32 = @as(u32, @intCast(app.img_size.height));
compute_pass.setBindGroup(1, app.compute_bind_group_0, &.{});
compute_pass.dispatchWorkgroups(try std.math.divCeil(u32, width, block_dimension), try std.math.divCeil(u32, height, batch[1]), 1);
compute_pass.setBindGroup(1, app.compute_bind_group_1, &.{});
compute_pass.dispatchWorkgroups(try std.math.divCeil(u32, height, block_dimension), try std.math.divCeil(u32, width, batch[1]), 1);
var i: u32 = 0;
while (i < iterations - 1) : (i += 1) {
compute_pass.setBindGroup(1, app.compute_bind_group_2, &.{});
compute_pass.dispatchWorkgroups(try std.math.divCeil(u32, width, block_dimension), try std.math.divCeil(u32, height, batch[1]), 1);
compute_pass.setBindGroup(1, app.compute_bind_group_1, &.{});
compute_pass.dispatchWorkgroups(try std.math.divCeil(u32, height, block_dimension), try std.math.divCeil(u32, width, batch[1]), 1);
}
compute_pass.end();
compute_pass.release();
const color_attachment = gpu.RenderPassColorAttachment{
.view = back_buffer_view,
.clear_value = std.mem.zeroes(gpu.Color),
.load_op = .clear,
.store_op = .store,
};
const render_pass_descriptor = gpu.RenderPassDescriptor.init(.{
.color_attachments = &.{color_attachment},
});
const render_pass = encoder.beginRenderPass(&render_pass_descriptor);
render_pass.setPipeline(app.fullscreen_quad_pipeline);
render_pass.setBindGroup(0, app.show_result_bind_group, &.{});
render_pass.draw(6, 1, 0, 0);
render_pass.end();
render_pass.release();
var command = encoder.finish(null);
encoder.release();
const queue = core.queue;
queue.submit(&[_]*gpu.CommandBuffer{command});
command.release();
core.swap_chain.present();
back_buffer_view.release();
// update the window title every second
if (app.title_timer.read() >= 1.0) {
app.title_timer.reset();
try core.printTitle("Image Blur [ {d}fps ] [ Input {d}hz ]", .{
core.frameRate(),
core.inputRate(),
});
}
return false;
}
fn rgb24ToRgba32(allocator: std.mem.Allocator, in: []zigimg.color.Rgb24) !zigimg.color.PixelStorage {
const out = try zigimg.color.PixelStorage.init(allocator, .rgba32, in.len);
var i: usize = 0;
while (i < in.len) : (i += 1) {
out.rgba32[i] = zigimg.color.Rgba32{ .r = in[i].r, .g = in[i].g, .b = in[i].b, .a = 255 };
}
return out;
}

View file

@ -0,0 +1,39 @@
@group(0) @binding(0) var mySampler : sampler;
@group(0) @binding(1) var myTexture : texture_2d<f32>;
struct VertexOutput {
@builtin(position) Position : vec4<f32>,
@location(0) fragUV : vec2<f32>,
}
@vertex
fn vert_main(@builtin(vertex_index) VertexIndex : u32) -> VertexOutput {
// Draw a fullscreen quad using two triangles, with UV coordinates (normalized pixel coordinates)
// that would have the full texture be displayed.
var pos = array<vec2<f32>, 6>(
vec2<f32>( 1.0, 1.0), // right, top
vec2<f32>( 1.0, -1.0), // right, bottom
vec2<f32>(-1.0, -1.0), // left, bottom
vec2<f32>( 1.0, 1.0), // right, top
vec2<f32>(-1.0, -1.0), // left, bottom
vec2<f32>(-1.0, 1.0) // left, top
);
var uv = array<vec2<f32>, 6>(
vec2<f32>(1.0, 0.0),
vec2<f32>(1.0, 1.0),
vec2<f32>(0.0, 1.0),
vec2<f32>(1.0, 0.0),
vec2<f32>(0.0, 1.0),
vec2<f32>(0.0, 0.0)
);
var output : VertexOutput;
output.Position = vec4<f32>(pos[VertexIndex], 0.0, 1.0);
output.fragUV = uv[VertexIndex];
return output;
}
@fragment
fn frag_main(@location(0) fragUV : vec2<f32>) -> @location(0) vec4<f32> {
return textureSample(myTexture, mySampler, fragUV);
}

View file

@ -0,0 +1,184 @@
const std = @import("std");
const core = @import("mach").core;
const gpu = core.gpu;
const zigimg = @import("zigimg");
const assets = @import("assets");
title_timer: core.Timer,
pipeline: *gpu.RenderPipeline,
texture: *gpu.Texture,
bind_group: *gpu.BindGroup,
img_size: gpu.Extent3D,
pub const App = @This();
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
pub fn init(app: *App) !void {
try core.init(.{});
const allocator = gpa.allocator();
// Load our shader that will render a fullscreen textured quad using two triangles, needed to
// get the image on screen.
const fullscreen_quad_vs_module = core.device.createShaderModuleWGSL(
"fullscreen_textured_quad.wgsl",
@embedFile("fullscreen_textured_quad.wgsl"),
);
defer fullscreen_quad_vs_module.release();
const fullscreen_quad_fs_module = core.device.createShaderModuleWGSL(
"fullscreen_textured_quad.wgsl",
@embedFile("fullscreen_textured_quad.wgsl"),
);
defer fullscreen_quad_fs_module.release();
// Create our render pipeline
const blend = gpu.BlendState{};
const color_target = gpu.ColorTargetState{
.format = core.descriptor.format,
.blend = &blend,
.write_mask = gpu.ColorWriteMaskFlags.all,
};
const fragment_state = gpu.FragmentState.init(.{
.module = fullscreen_quad_fs_module,
.entry_point = "frag_main",
.targets = &.{color_target},
});
const pipeline_descriptor = gpu.RenderPipeline.Descriptor{
.fragment = &fragment_state,
.vertex = .{
.module = fullscreen_quad_vs_module,
.entry_point = "vert_main",
},
};
const pipeline = core.device.createRenderPipeline(&pipeline_descriptor);
// Create a texture sampler. This determines what happens when the texture doesn't match the
// dimensions of the screen it's being displayed on. If the image needs to be magnified or
// minified to fit, it can be linearly interpolated (i.e. 'blurred', .linear) or the nearest
// pixel may be used (i.e. 'pixelated', .nearest)
const sampler = core.device.createSampler(&.{
.mag_filter = .linear,
.min_filter = .linear,
});
defer sampler.release();
// Load the pixels of the image
var img = try zigimg.Image.fromMemory(allocator, assets.gotta_go_fast_png);
defer img.deinit();
const img_size = gpu.Extent3D{ .width = @as(u32, @intCast(img.width)), .height = @as(u32, @intCast(img.height)) };
// Create a texture
const texture = core.device.createTexture(&.{
.size = img_size,
.format = .rgba8_unorm,
.usage = .{
.texture_binding = true,
.copy_dst = true,
.render_attachment = true,
},
});
// Upload the pixels (from the CPU) to the GPU. You could e.g. do this once per frame if you
// wanted the image to be updated dynamically.
const data_layout = gpu.Texture.DataLayout{
.bytes_per_row = @as(u32, @intCast(img.width * 4)),
.rows_per_image = @as(u32, @intCast(img.height)),
};
switch (img.pixels) {
.rgba32 => |pixels| core.queue.writeTexture(&.{ .texture = texture }, &data_layout, &img_size, pixels),
.rgb24 => |pixels| {
const data = try rgb24ToRgba32(allocator, pixels);
defer data.deinit(allocator);
core.queue.writeTexture(&.{ .texture = texture }, &data_layout, &img_size, data.rgba32);
},
else => @panic("unsupported image color format"),
}
// Describe which data we will pass to our shader (GPU program)
const bind_group_layout = pipeline.getBindGroupLayout(0);
defer bind_group_layout.release();
const texture_view = texture.createView(&gpu.TextureView.Descriptor{});
defer texture_view.release();
const bind_group = core.device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{
.layout = bind_group_layout,
.entries = &.{
gpu.BindGroup.Entry.sampler(0, sampler),
gpu.BindGroup.Entry.textureView(1, texture_view),
},
}));
app.* = .{
.title_timer = try core.Timer.start(),
.pipeline = pipeline,
.texture = texture,
.bind_group = bind_group,
.img_size = img_size,
};
}
pub fn deinit(app: *App) void {
defer _ = gpa.deinit();
defer core.deinit();
app.pipeline.release();
app.texture.release();
app.bind_group.release();
}
pub fn update(app: *App) !bool {
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
defer back_buffer_view.release();
// Poll for events (keyboard input, etc.)
var iter = core.pollEvents();
while (iter.next()) |event| {
if (event == .close) return true;
}
const encoder = core.device.createCommandEncoder(null);
defer encoder.release();
// Begin our render pass by clearing the pixels that were on the screen from the previous frame.
const color_attachment = gpu.RenderPassColorAttachment{
.view = back_buffer_view,
.clear_value = std.mem.zeroes(gpu.Color),
.load_op = .clear,
.store_op = .store,
};
const render_pass_descriptor = gpu.RenderPassDescriptor.init(.{
.color_attachments = &.{color_attachment},
});
const render_pass = encoder.beginRenderPass(&render_pass_descriptor);
defer render_pass.release();
// Render using our pipeline
render_pass.setPipeline(app.pipeline);
render_pass.setBindGroup(0, app.bind_group, &.{});
render_pass.draw(6, 1, 0, 0); // Tell the GPU to draw 6 vertices, one object
render_pass.end();
// Submit all the commands to the GPU and render the frame.
var command = encoder.finish(null);
defer command.release();
core.queue.submit(&[_]*gpu.CommandBuffer{command});
core.swap_chain.present();
// update the window title every second to have the FPS
if (app.title_timer.read() >= 1.0) {
app.title_timer.reset();
try core.printTitle("Image [ {d}fps ] [ Input {d}hz ]", .{
core.frameRate(),
core.inputRate(),
});
}
return false;
}
fn rgb24ToRgba32(allocator: std.mem.Allocator, in: []zigimg.color.Rgb24) !zigimg.color.PixelStorage {
const out = try zigimg.color.PixelStorage.init(allocator, .rgba32, in.len);
var i: usize = 0;
while (i < in.len) : (i += 1) {
out.rgba32[i] = zigimg.color.Rgba32{ .r = in[i].r, .g = in[i].g, .b = in[i].b, .a = 255 };
}
return out;
}

View file

@ -0,0 +1,49 @@
pub const Vertex = extern struct {
pos: @Vector(4, f32),
col: @Vector(4, f32),
uv: @Vector(2, f32),
};
pub const vertices = [_]Vertex{
.{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } },
};

View file

@ -0,0 +1,205 @@
const std = @import("std");
const core = @import("mach").core;
const gpu = core.gpu;
const zm = @import("zmath");
const Vertex = @import("cube_mesh.zig").Vertex;
const vertices = @import("cube_mesh.zig").vertices;
const UniformBufferObject = struct {
mat: zm.Mat,
};
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
title_timer: core.Timer,
timer: core.Timer,
pipeline: *gpu.RenderPipeline,
vertex_buffer: *gpu.Buffer,
uniform_buffer: *gpu.Buffer,
bind_group: *gpu.BindGroup,
pub const App = @This();
pub fn init(app: *App) !void {
try core.init(.{});
app.timer = try core.Timer.start();
const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl"));
const vertex_attributes = [_]gpu.VertexAttribute{
.{ .format = .float32x4, .offset = @offsetOf(Vertex, "pos"), .shader_location = 0 },
.{ .format = .float32x2, .offset = @offsetOf(Vertex, "uv"), .shader_location = 1 },
};
const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{
.array_stride = @sizeOf(Vertex),
.step_mode = .vertex,
.attributes = &vertex_attributes,
});
const color_target = gpu.ColorTargetState{
.format = core.descriptor.format,
.write_mask = gpu.ColorWriteMaskFlags.all,
};
const fragment = gpu.FragmentState.init(.{
.module = shader_module,
.entry_point = "frag_main",
.targets = &.{color_target},
});
const bgle = gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, true, 0);
const bgl = core.device.createBindGroupLayout(
&gpu.BindGroupLayout.Descriptor.init(.{
.entries = &.{bgle},
}),
);
const bind_group_layouts = [_]*gpu.BindGroupLayout{bgl};
const pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{
.bind_group_layouts = &bind_group_layouts,
}));
const pipeline_descriptor = gpu.RenderPipeline.Descriptor{
.fragment = &fragment,
.layout = pipeline_layout,
.vertex = gpu.VertexState.init(.{
.module = shader_module,
.entry_point = "vertex_main",
.buffers = &.{vertex_buffer_layout},
}),
.primitive = .{
.cull_mode = .back,
},
};
const vertex_buffer = core.device.createBuffer(&.{
.usage = .{ .vertex = true },
.size = @sizeOf(Vertex) * vertices.len,
.mapped_at_creation = .true,
});
const vertex_mapped = vertex_buffer.getMappedRange(Vertex, 0, vertices.len);
@memcpy(vertex_mapped.?, vertices[0..]);
vertex_buffer.unmap();
const x_count = 4;
const y_count = 4;
const num_instances = x_count * y_count;
const uniform_buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .uniform = true },
.size = @sizeOf(UniformBufferObject) * num_instances,
.mapped_at_creation = .false,
});
const bind_group = core.device.createBindGroup(
&gpu.BindGroup.Descriptor.init(.{
.layout = bgl,
.entries = &.{
gpu.BindGroup.Entry.buffer(0, uniform_buffer, 0, @sizeOf(UniformBufferObject) * num_instances),
},
}),
);
app.title_timer = try core.Timer.start();
app.pipeline = core.device.createRenderPipeline(&pipeline_descriptor);
app.vertex_buffer = vertex_buffer;
app.uniform_buffer = uniform_buffer;
app.bind_group = bind_group;
shader_module.release();
pipeline_layout.release();
bgl.release();
}
pub fn deinit(app: *App) void {
defer _ = gpa.deinit();
defer core.deinit();
app.pipeline.release();
app.vertex_buffer.release();
app.bind_group.release();
app.uniform_buffer.release();
}
pub fn update(app: *App) !bool {
var iter = core.pollEvents();
while (iter.next()) |event| {
switch (event) {
.key_press => |ev| {
if (ev.key == .space) return true;
},
.close => return true,
else => {},
}
}
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
const color_attachment = gpu.RenderPassColorAttachment{
.view = back_buffer_view,
.clear_value = std.mem.zeroes(gpu.Color),
.load_op = .clear,
.store_op = .store,
};
const encoder = core.device.createCommandEncoder(null);
const render_pass_info = gpu.RenderPassDescriptor.init(.{
.color_attachments = &.{color_attachment},
});
{
const proj = zm.perspectiveFovRh(
(std.math.pi / 3.0),
@as(f32, @floatFromInt(core.descriptor.width)) / @as(f32, @floatFromInt(core.descriptor.height)),
10,
30,
);
var ubos: [16]UniformBufferObject = undefined;
const time = app.timer.read();
const step: f32 = 4.0;
var m: u8 = 0;
var x: u8 = 0;
while (x < 4) : (x += 1) {
var y: u8 = 0;
while (y < 4) : (y += 1) {
const trans = zm.translation(step * (@as(f32, @floatFromInt(x)) - 2.0 + 0.5), step * (@as(f32, @floatFromInt(y)) - 2.0 + 0.5), -20);
const localTime = time + @as(f32, @floatFromInt(m)) * 0.5;
const model = zm.mul(zm.mul(zm.mul(zm.rotationX(localTime * (std.math.pi / 2.1)), zm.rotationY(localTime * (std.math.pi / 0.9))), zm.rotationZ(localTime * (std.math.pi / 1.3))), trans);
const mvp = zm.mul(model, proj);
const ubo = UniformBufferObject{
.mat = mvp,
};
ubos[m] = ubo;
m += 1;
}
}
encoder.writeBuffer(app.uniform_buffer, 0, &ubos);
}
const pass = encoder.beginRenderPass(&render_pass_info);
pass.setPipeline(app.pipeline);
pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len);
pass.setBindGroup(0, app.bind_group, &.{0});
pass.draw(vertices.len, 16, 0, 0);
pass.end();
pass.release();
var command = encoder.finish(null);
encoder.release();
const queue = core.queue;
queue.submit(&[_]*gpu.CommandBuffer{command});
command.release();
core.swap_chain.present();
back_buffer_view.release();
// update the window title every second
if (app.title_timer.read() >= 1.0) {
app.title_timer.reset();
try core.printTitle("Instanced Cube [ {d}fps ] [ Input {d}hz ]", .{
core.frameRate(),
core.inputRate(),
});
}
return false;
}

View file

@ -0,0 +1,25 @@
@binding(0) @group(0) var<uniform> ubos : array<mat4x4<f32>, 16>;
struct VertexOutput {
@builtin(position) position_clip : vec4<f32>,
@location(0) fragUV : vec2<f32>,
@location(1) fragPosition: vec4<f32>,
};
@vertex
fn vertex_main(@builtin(instance_index) instanceIdx : u32,
@location(0) position : vec4<f32>,
@location(1) uv : vec2<f32>) -> VertexOutput {
var output : VertexOutput;
output.position_clip = ubos[instanceIdx] * position;
output.fragUV = uv;
output.fragPosition = 0.5 * (position + vec4<f32>(1.0, 1.0, 1.0, 1.0));
return output;
}
@fragment fn frag_main(
@location(0) fragUV: vec2<f32>,
@location(1) fragPosition: vec4<f32>
) -> @location(0) vec4<f32> {
return fragPosition;
}

View file

@ -0,0 +1,16 @@
@group(0) @binding(0) var<storage, read_write> output: array<f32>;
@compute @workgroup_size(64, 1, 1)
fn main(
@builtin(global_invocation_id)
global_id : vec3<u32>,
@builtin(local_invocation_id)
local_id : vec3<u32>,
) {
if (global_id.x >= arrayLength(&output)) {
return;
}
output[global_id.x] =
f32(global_id.x) * 1000. + f32(local_id.x);
}

View file

@ -0,0 +1,101 @@
const std = @import("std");
const core = @import("mach").core;
const gpu = core.gpu;
pub const App = @This();
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const workgroup_size = 64;
const buffer_size = 1000;
pub fn init(app: *App) !void {
try core.init(.{});
app.* = .{};
const output = core.device.createBuffer(&.{
.usage = .{ .storage = true, .copy_src = true },
.size = buffer_size * @sizeOf(f32),
.mapped_at_creation = .false,
});
defer output.release();
const staging = core.device.createBuffer(&.{
.usage = .{ .map_read = true, .copy_dst = true },
.size = buffer_size * @sizeOf(f32),
.mapped_at_creation = .false,
});
defer staging.release();
const compute_module = core.device.createShaderModuleWGSL("main.wgsl", @embedFile("main.wgsl"));
const compute_pipeline = core.device.createComputePipeline(&gpu.ComputePipeline.Descriptor{ .compute = gpu.ProgrammableStageDescriptor{
.module = compute_module,
.entry_point = "main",
} });
defer compute_pipeline.release();
const layout = compute_pipeline.getBindGroupLayout(0);
defer layout.release();
const compute_bind_group = core.device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{
.layout = layout,
.entries = &.{
gpu.BindGroup.Entry.buffer(0, output, 0, buffer_size * @sizeOf(f32)),
},
}));
defer compute_bind_group.release();
compute_module.release();
const encoder = core.device.createCommandEncoder(null);
const compute_pass = encoder.beginComputePass(null);
compute_pass.setPipeline(compute_pipeline);
compute_pass.setBindGroup(0, compute_bind_group, &.{});
compute_pass.dispatchWorkgroups(try std.math.divCeil(u32, buffer_size, workgroup_size), 1, 1);
compute_pass.end();
compute_pass.release();
encoder.copyBufferToBuffer(output, 0, staging, 0, buffer_size * @sizeOf(f32));
var command = encoder.finish(null);
encoder.release();
var response: gpu.Buffer.MapAsyncStatus = undefined;
const callback = (struct {
pub inline fn callback(ctx: *gpu.Buffer.MapAsyncStatus, status: gpu.Buffer.MapAsyncStatus) void {
ctx.* = status;
}
}).callback;
var queue = core.queue;
queue.submit(&[_]*gpu.CommandBuffer{command});
command.release();
staging.mapAsync(.{ .read = true }, 0, buffer_size * @sizeOf(f32), &response, callback);
while (true) {
if (response == gpu.Buffer.MapAsyncStatus.success) {
break;
} else {
core.device.tick();
}
}
const staging_mapped = staging.getConstMappedRange(f32, 0, buffer_size);
for (staging_mapped.?) |v| {
std.debug.print("{d} ", .{v});
}
std.debug.print("\n", .{});
staging.unmap();
}
pub fn deinit(app: *App) void {
_ = app;
defer _ = gpa.deinit();
core.deinit();
}
pub fn update(_: *App) !bool {
return true;
}

View file

@ -0,0 +1,920 @@
const std = @import("std");
const core = @import("mach").core;
const gpu = core.gpu;
const m3d = @import("model3d");
const zm = @import("zmath");
const assets = @import("assets");
const VertexWriter = @import("vertex_writer.zig").VertexWriter;
pub const App = @This();
const Vec4 = [4]f32;
const Vec3 = [3]f32;
const Vec2 = [2]f32;
const Mat4 = [4]Vec4;
fn Dimensions2D(comptime T: type) type {
return struct {
width: T,
height: T,
};
}
const Vertex = extern struct {
position: Vec3,
normal: Vec3,
};
const Model = struct {
vertex_count: u32,
index_count: u32,
vertex_buffer: *gpu.Buffer,
index_buffer: *gpu.Buffer,
};
const Material = struct {
const Params = extern struct {
roughness: f32,
metallic: f32,
color: Vec3,
};
name: []const u8,
params: Params,
};
const PressedKeys = packed struct(u16) {
right: bool = false,
left: bool = false,
up: bool = false,
down: bool = false,
padding: u12 = undefined,
pub inline fn areKeysPressed(self: @This()) bool {
return (self.up or self.down or self.left or self.right);
}
pub inline fn clear(self: *@This()) void {
self.right = false;
self.left = false;
self.up = false;
self.down = false;
}
};
const Camera = struct {
const Matrices = struct {
perspective: Mat4 = [1]Vec4{[1]f32{0.0} ** 4} ** 4,
view: Mat4 = [1]Vec4{[1]f32{0.0} ** 4} ** 4,
};
rotation: Vec3 = .{ 0.0, 0.0, 0.0 },
position: Vec3 = .{ 0.0, 0.0, 0.0 },
view_position: Vec4 = .{ 0.0, 0.0, 0.0, 0.0 },
fov: f32 = 0.0,
znear: f32 = 0.0,
zfar: f32 = 0.0,
rotation_speed: f32 = 0.0,
movement_speed: f32 = 0.0,
updated: bool = false,
matrices: Matrices = .{},
pub fn calculateMovement(self: *@This(), pressed_keys: PressedKeys) void {
std.debug.assert(pressed_keys.areKeysPressed());
const rotation_radians = Vec3{
toRadians(self.rotation[0]),
toRadians(self.rotation[1]),
toRadians(self.rotation[2]),
};
var camera_front = zm.Vec{ -zm.cos(rotation_radians[0]) * zm.sin(rotation_radians[1]), zm.sin(rotation_radians[0]), zm.cos(rotation_radians[0]) * zm.cos(rotation_radians[1]), 0 };
camera_front = zm.normalize3(camera_front);
if (pressed_keys.up) {
camera_front[0] *= self.movement_speed;
camera_front[1] *= self.movement_speed;
camera_front[2] *= self.movement_speed;
self.position = Vec3{
self.position[0] + camera_front[0],
self.position[1] + camera_front[1],
self.position[2] + camera_front[2],
};
}
if (pressed_keys.down) {
camera_front[0] *= self.movement_speed;
camera_front[1] *= self.movement_speed;
camera_front[2] *= self.movement_speed;
self.position = Vec3{
self.position[0] - camera_front[0],
self.position[1] - camera_front[1],
self.position[2] - camera_front[2],
};
}
if (pressed_keys.right) {
camera_front = zm.cross3(.{ 0.0, 1.0, 0.0, 0.0 }, camera_front);
camera_front = zm.normalize3(camera_front);
camera_front[0] *= self.movement_speed;
camera_front[1] *= self.movement_speed;
camera_front[2] *= self.movement_speed;
self.position = Vec3{
self.position[0] - camera_front[0],
self.position[1] - camera_front[1],
self.position[2] - camera_front[2],
};
}
if (pressed_keys.left) {
camera_front = zm.cross3(.{ 0.0, 1.0, 0.0, 0.0 }, camera_front);
camera_front = zm.normalize3(camera_front);
camera_front[0] *= self.movement_speed;
camera_front[1] *= self.movement_speed;
camera_front[2] *= self.movement_speed;
self.position = Vec3{
self.position[0] + camera_front[0],
self.position[1] + camera_front[1],
self.position[2] + camera_front[2],
};
}
self.updateViewMatrix();
}
fn updateViewMatrix(self: *@This()) void {
const rotation_x = zm.rotationX(toRadians(self.rotation[2]));
const rotation_y = zm.rotationY(toRadians(self.rotation[1]));
const rotation_z = zm.rotationZ(toRadians(self.rotation[0]));
const rotation_matrix = zm.mul(rotation_z, zm.mul(rotation_x, rotation_y));
const translation_matrix: zm.Mat = zm.translationV(.{
self.position[0],
self.position[1],
self.position[2],
0,
});
const view = zm.mul(translation_matrix, rotation_matrix);
self.matrices.view[0] = view[0];
self.matrices.view[1] = view[1];
self.matrices.view[2] = view[2];
self.matrices.view[3] = view[3];
self.view_position = .{
-self.position[0],
self.position[1],
-self.position[2],
0.0,
};
self.updated = true;
}
pub fn setMovementSpeed(self: *@This(), speed: f32) void {
self.movement_speed = speed;
}
pub fn setPerspective(self: *@This(), fov: f32, aspect: f32, znear: f32, zfar: f32) void {
self.fov = fov;
self.znear = znear;
self.zfar = zfar;
const perspective = zm.perspectiveFovRhGl(toRadians(fov), aspect, znear, zfar);
self.matrices.perspective[0] = perspective[0];
self.matrices.perspective[1] = perspective[1];
self.matrices.perspective[2] = perspective[2];
self.matrices.perspective[3] = perspective[3];
}
pub fn setRotationSpeed(self: *@This(), speed: f32) void {
self.rotation_speed = speed;
}
pub fn setRotation(self: *@This(), rotation: Vec3) void {
self.rotation = rotation;
self.updateViewMatrix();
}
pub fn rotate(self: *@This(), delta: Vec2) void {
self.rotation[0] -= delta[1];
self.rotation[1] -= delta[0];
self.updateViewMatrix();
}
pub fn setPosition(self: *@This(), position: Vec3) void {
self.position = .{
position[0],
-position[1],
position[2],
};
self.updateViewMatrix();
}
};
const UniformBuffers = struct {
const Params = struct {
buffer: *gpu.Buffer,
buffer_size: u64,
model_size: u64,
};
const Buffer = struct {
buffer: *gpu.Buffer,
size: u32,
};
ubo_matrices: Buffer,
ubo_params: Buffer,
material_params: Params,
object_params: Params,
};
const UboParams = struct {
lights: [4]Vec4,
};
const UboMatrices = extern struct {
projection: Mat4,
model: Mat4,
view: Mat4,
camera_position: Vec3,
};
const grid_element_count = grid_dimensions * grid_dimensions;
const MaterialParamsDynamic = extern struct {
roughness: f32 = 0,
metallic: f32 = 0,
color: Vec3 = .{ 0, 0, 0 },
padding: [236]u8 = [1]u8{0} ** 236,
};
const MaterialParamsDynamicGrid = [grid_element_count]MaterialParamsDynamic;
const ObjectParamsDynamic = extern struct {
position: Vec3 = .{ 0, 0, 0 },
padding: [244]u8 = [1]u8{0} ** 244,
};
const ObjectParamsDynamicGrid = [grid_element_count]ObjectParamsDynamic;
//
// Globals
//
const material_names = [11][:0]const u8{
"Gold", "Copper", "Chromium", "Nickel", "Titanium", "Cobalt", "Platinum",
// Testing materials
"White", "Red", "Blue", "Black",
};
const object_names = [5][:0]const u8{ "Sphere", "Teapot", "Torusknot", "Venus", "Stanford Dragon" };
const materials = [_]Material{
.{ .name = "Gold", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 1.0, 0.765557, 0.336057 } } },
.{ .name = "Copper", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.955008, 0.637427, 0.538163 } } },
.{ .name = "Chromium", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.549585, 0.556114, 0.554256 } } },
.{ .name = "Nickel", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 1.0, 0.608679, 0.525649 } } },
.{ .name = "Titanium", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.541931, 0.496791, 0.449419 } } },
.{ .name = "Cobalt", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.662124, 0.654864, 0.633732 } } },
.{ .name = "Platinum", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.672411, 0.637331, 0.585456 } } },
// Testing colors
.{ .name = "White", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 1.0, 1.0, 1.0 } } },
.{ .name = "Red", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 1.0, 0.0, 0.0 } } },
.{ .name = "Blue", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.0, 0.0, 1.0 } } },
.{ .name = "Black", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.0, 0.0, 0.0 } } },
};
const grid_dimensions = 7;
const model_embeds = [_][:0]const u8{
assets.sphere_m3d,
assets.teapot_m3d,
assets.torusknot_m3d,
assets.venus_m3d,
assets.stanford_dragon_m3d,
};
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
//
// Member variables
//
title_timer: core.Timer,
timer: core.Timer,
camera: Camera,
render_pipeline: *gpu.RenderPipeline,
render_pass_descriptor: gpu.RenderPassDescriptor,
bind_group: *gpu.BindGroup,
color_attachment: gpu.RenderPassColorAttachment,
depth_stencil_attachment_description: gpu.RenderPassDepthStencilAttachment,
depth_texture: *gpu.Texture,
depth_texture_view: *gpu.TextureView,
pressed_keys: PressedKeys,
models: [5]Model,
ubo_params: UboParams,
ubo_matrices: UboMatrices,
uniform_buffers: UniformBuffers,
material_params_dynamic: MaterialParamsDynamicGrid = [1]MaterialParamsDynamic{.{}} ** grid_element_count,
object_params_dynamic: ObjectParamsDynamicGrid = [1]ObjectParamsDynamic{.{}} ** grid_element_count,
uniform_buffers_dirty: bool,
buffers_bound: bool,
is_paused: bool,
current_material_index: usize,
current_object_index: usize,
mouse_position: core.Position,
is_rotating: bool,
//
// Functions
//
pub fn init(app: *App) !void {
try core.init(.{});
app.timer = try core.Timer.start();
app.title_timer = try core.Timer.start();
app.pressed_keys = .{};
app.buffers_bound = false;
app.is_paused = false;
app.uniform_buffers_dirty = false;
app.current_material_index = 0;
app.current_object_index = 0;
app.mouse_position = .{ .x = 0, .y = 0 };
app.is_rotating = false;
setupCamera(app);
try loadModels(std.heap.c_allocator, app);
prepareUniformBuffers(app);
setupPipeline(app);
setupRenderPass(app);
app.printControls();
}
pub fn deinit(app: *App) void {
defer _ = gpa.deinit();
defer core.deinit();
app.bind_group.release();
app.render_pipeline.release();
app.depth_texture_view.release();
app.depth_texture.release();
app.uniform_buffers.ubo_matrices.buffer.release();
app.uniform_buffers.ubo_params.buffer.release();
app.uniform_buffers.material_params.buffer.release();
app.uniform_buffers.object_params.buffer.release();
}
pub fn update(app: *App) !bool {
var iter = core.pollEvents();
while (iter.next()) |event| {
app.updateUI(event);
switch (event) {
.mouse_motion => |ev| {
if (app.is_rotating) {
const delta = Vec2{
@as(f32, @floatCast((app.mouse_position.x - ev.pos.x) * app.camera.rotation_speed)),
@as(f32, @floatCast((app.mouse_position.y - ev.pos.y) * app.camera.rotation_speed)),
};
app.mouse_position = ev.pos;
app.camera.rotate(delta);
app.uniform_buffers_dirty = true;
}
},
.mouse_press => |ev| {
if (ev.button == .left) {
app.is_rotating = true;
app.mouse_position = ev.pos;
}
},
.mouse_release => |ev| {
if (ev.button == .left) {
app.is_rotating = false;
}
},
.key_press, .key_repeat => |ev| {
const key = ev.key;
if (key == .up or key == .w) app.pressed_keys.up = true;
if (key == .down or key == .s) app.pressed_keys.down = true;
if (key == .left or key == .a) app.pressed_keys.left = true;
if (key == .right or key == .d) app.pressed_keys.right = true;
},
.framebuffer_resize => |ev| {
app.depth_texture_view.release();
app.depth_texture.release();
app.depth_texture = core.device.createTexture(&gpu.Texture.Descriptor{
.usage = .{ .render_attachment = true },
.format = .depth24_plus_stencil8,
.sample_count = 1,
.size = .{
.width = ev.width,
.height = ev.height,
.depth_or_array_layers = 1,
},
});
app.depth_texture_view = app.depth_texture.createView(&gpu.TextureView.Descriptor{
.format = .depth24_plus_stencil8,
.dimension = .dimension_2d,
.array_layer_count = 1,
.aspect = .all,
});
app.depth_stencil_attachment_description = gpu.RenderPassDepthStencilAttachment{
.view = app.depth_texture_view,
.depth_load_op = .clear,
.depth_store_op = .store,
.depth_clear_value = 1.0,
.stencil_clear_value = 0,
.stencil_load_op = .clear,
.stencil_store_op = .store,
};
const aspect_ratio = @as(f32, @floatFromInt(ev.width)) / @as(f32, @floatFromInt(ev.height));
app.camera.setPerspective(60.0, aspect_ratio, 0.1, 256.0);
app.uniform_buffers_dirty = true;
},
.close => return true,
else => {},
}
}
if (app.pressed_keys.areKeysPressed()) {
app.camera.calculateMovement(app.pressed_keys);
app.pressed_keys.clear();
app.uniform_buffers_dirty = true;
}
if (app.uniform_buffers_dirty) {
updateUniformBuffers(app);
app.uniform_buffers_dirty = false;
}
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
app.color_attachment.view = back_buffer_view;
app.render_pass_descriptor = gpu.RenderPassDescriptor{
.color_attachment_count = 1,
.color_attachments = &[_]gpu.RenderPassColorAttachment{app.color_attachment},
.depth_stencil_attachment = &app.depth_stencil_attachment_description,
};
const encoder = core.device.createCommandEncoder(null);
const current_model = app.models[app.current_object_index];
const pass = encoder.beginRenderPass(&app.render_pass_descriptor);
const dimensions = Dimensions2D(f32){
.width = @as(f32, @floatFromInt(core.descriptor.width)),
.height = @as(f32, @floatFromInt(core.descriptor.height)),
};
pass.setViewport(
0,
0,
dimensions.width,
dimensions.height,
0.0,
1.0,
);
pass.setScissorRect(0, 0, core.descriptor.width, core.descriptor.height);
pass.setPipeline(app.render_pipeline);
if (!app.is_paused) {
app.updateLights();
}
var i: usize = 0;
while (i < (grid_dimensions * grid_dimensions)) : (i += 1) {
const alignment = 256;
const dynamic_offset: u32 = @as(u32, @intCast(i)) * alignment;
const dynamic_offsets = [2]u32{ dynamic_offset, dynamic_offset };
pass.setBindGroup(0, app.bind_group, &dynamic_offsets);
if (!app.buffers_bound) {
pass.setVertexBuffer(0, current_model.vertex_buffer, 0, @sizeOf(Vertex) * current_model.vertex_count);
pass.setIndexBuffer(current_model.index_buffer, .uint32, 0, gpu.whole_size);
app.buffers_bound = true;
}
pass.drawIndexed(
current_model.index_count, // index_count
1, // instance_count
0, // first_index
0, // base_vertex
0, // first_instance
);
}
pass.end();
pass.release();
var command = encoder.finish(null);
encoder.release();
const queue = core.queue;
queue.submit(&[_]*gpu.CommandBuffer{command});
command.release();
core.swap_chain.present();
back_buffer_view.release();
app.buffers_bound = false;
// update the window title every second
if (app.title_timer.read() >= 1.0) {
app.title_timer.reset();
try core.printTitle("PBR Basic [ {d}fps ] [ Input {d}hz ]", .{
core.frameRate(),
core.inputRate(),
});
}
return false;
}
fn prepareUniformBuffers(app: *App) void {
comptime {
std.debug.assert(@sizeOf(ObjectParamsDynamic) == 256);
std.debug.assert(@sizeOf(MaterialParamsDynamic) == 256);
}
app.uniform_buffers.ubo_matrices.size = roundToMultipleOf4(u32, @as(u32, @intCast(@sizeOf(UboMatrices)))) + 4;
app.uniform_buffers.ubo_matrices.buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .uniform = true },
.size = app.uniform_buffers.ubo_matrices.size,
.mapped_at_creation = .false,
});
app.uniform_buffers.ubo_params.size = roundToMultipleOf4(u32, @as(u32, @intCast(@sizeOf(UboParams)))) + 4;
app.uniform_buffers.ubo_params.buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .uniform = true },
.size = app.uniform_buffers.ubo_params.size,
.mapped_at_creation = .false,
});
//
// Material parameter uniform buffer
//
app.uniform_buffers.material_params.model_size = @sizeOf(Vec2) + @sizeOf(Vec3);
app.uniform_buffers.material_params.buffer_size = calculateConstantBufferByteSize(@sizeOf(MaterialParamsDynamicGrid));
std.debug.assert(app.uniform_buffers.material_params.buffer_size >= app.uniform_buffers.material_params.model_size);
app.uniform_buffers.material_params.buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .uniform = true },
.size = app.uniform_buffers.material_params.buffer_size,
.mapped_at_creation = .false,
});
//
// Object parameter uniform buffer
//
app.uniform_buffers.object_params.model_size = @sizeOf(Vec3) + 4;
app.uniform_buffers.object_params.buffer_size = calculateConstantBufferByteSize(@sizeOf(MaterialParamsDynamicGrid)) + 4;
std.debug.assert(app.uniform_buffers.object_params.buffer_size >= app.uniform_buffers.object_params.model_size);
app.uniform_buffers.object_params.buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .uniform = true },
.size = app.uniform_buffers.object_params.buffer_size,
.mapped_at_creation = .false,
});
app.updateUniformBuffers();
app.updateDynamicUniformBuffer();
app.updateLights();
}
fn updateDynamicUniformBuffer(app: *App) void {
var index: u32 = 0;
var y: usize = 0;
while (y < grid_dimensions) : (y += 1) {
var x: usize = 0;
while (x < grid_dimensions) : (x += 1) {
const grid_dimensions_float = @as(f32, @floatFromInt(grid_dimensions));
app.object_params_dynamic[index].position[0] = (@as(f32, @floatFromInt(x)) - (grid_dimensions_float / 2) * 2.5);
app.object_params_dynamic[index].position[1] = 0;
app.object_params_dynamic[index].position[2] = (@as(f32, @floatFromInt(y)) - (grid_dimensions_float / 2) * 2.5);
app.material_params_dynamic[index].metallic = zm.clamp(@as(f32, @floatFromInt(x)) / (grid_dimensions_float - 1), 0.1, 1.0);
app.material_params_dynamic[index].roughness = zm.clamp(@as(f32, @floatFromInt(y)) / (grid_dimensions_float - 1), 0.05, 1.0);
app.material_params_dynamic[index].color = materials[app.current_material_index].params.color;
index += 1;
}
}
const queue = core.queue;
queue.writeBuffer(
app.uniform_buffers.object_params.buffer,
0,
&app.object_params_dynamic,
);
queue.writeBuffer(
app.uniform_buffers.material_params.buffer,
0,
&app.material_params_dynamic,
);
}
fn updateUniformBuffers(app: *App) void {
app.ubo_matrices.projection = app.camera.matrices.perspective;
app.ubo_matrices.view = app.camera.matrices.view;
const rotation_degrees = if (app.current_object_index == 1) @as(f32, -45.0) else @as(f32, -90.0);
const model = zm.rotationY(rotation_degrees);
app.ubo_matrices.model[0] = model[0];
app.ubo_matrices.model[1] = model[1];
app.ubo_matrices.model[2] = model[2];
app.ubo_matrices.model[3] = model[3];
app.ubo_matrices.camera_position = .{
-app.camera.position[0],
-app.camera.position[1],
-app.camera.position[2],
};
const queue = core.queue;
queue.writeBuffer(app.uniform_buffers.ubo_matrices.buffer, 0, &[_]UboMatrices{app.ubo_matrices});
}
fn updateLights(app: *App) void {
const p: f32 = 15.0;
app.ubo_params.lights[0] = Vec4{ -p, -p * 0.5, -p, 1.0 };
app.ubo_params.lights[1] = Vec4{ -p, -p * 0.5, p, 1.0 };
app.ubo_params.lights[2] = Vec4{ p, -p * 0.5, p, 1.0 };
app.ubo_params.lights[3] = Vec4{ p, -p * 0.5, -p, 1.0 };
const base_value = toRadians(@mod(app.timer.read() * 0.1, 1.0) * 360.0);
app.ubo_params.lights[0][0] = @sin(base_value) * 20.0;
app.ubo_params.lights[0][2] = @cos(base_value) * 20.0;
app.ubo_params.lights[1][0] = @cos(base_value) * 20.0;
app.ubo_params.lights[1][1] = @sin(base_value) * 20.0;
const queue = core.queue;
queue.writeBuffer(
app.uniform_buffers.ubo_params.buffer,
0,
&[_]UboParams{app.ubo_params},
);
}
fn setupPipeline(app: *App) void {
comptime {
std.debug.assert(@sizeOf(Vertex) == @sizeOf(f32) * 6);
}
const bind_group_layout_entries = [_]gpu.BindGroupLayout.Entry{
.{
.binding = 0,
.visibility = .{ .vertex = true, .fragment = true },
.buffer = .{
.type = .uniform,
.has_dynamic_offset = .false,
.min_binding_size = app.uniform_buffers.ubo_matrices.size,
},
},
.{
.binding = 1,
.visibility = .{ .fragment = true },
.buffer = .{
.type = .uniform,
.has_dynamic_offset = .false,
.min_binding_size = app.uniform_buffers.ubo_params.size,
},
},
.{
.binding = 2,
.visibility = .{ .fragment = true },
.buffer = .{
.type = .uniform,
.has_dynamic_offset = .true,
.min_binding_size = app.uniform_buffers.material_params.model_size,
},
},
.{
.binding = 3,
.visibility = .{ .vertex = true },
.buffer = .{
.type = .uniform,
.has_dynamic_offset = .true,
.min_binding_size = app.uniform_buffers.object_params.model_size,
},
},
};
const bind_group_layout = core.device.createBindGroupLayout(
&gpu.BindGroupLayout.Descriptor.init(.{
.entries = bind_group_layout_entries[0..],
}),
);
const bind_group_layouts = [_]*gpu.BindGroupLayout{bind_group_layout};
const pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{
.bind_group_layouts = &bind_group_layouts,
}));
const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{
.array_stride = @sizeOf(Vertex),
.step_mode = .vertex,
.attributes = &.{
.{ .format = .float32x3, .offset = @offsetOf(Vertex, "position"), .shader_location = 0 },
.{ .format = .float32x3, .offset = @offsetOf(Vertex, "normal"), .shader_location = 1 },
},
});
const blend_component_descriptor = gpu.BlendComponent{
.operation = .add,
.src_factor = .one,
.dst_factor = .zero,
};
const color_target_state = gpu.ColorTargetState{
.format = core.descriptor.format,
.blend = &.{
.color = blend_component_descriptor,
.alpha = blend_component_descriptor,
},
};
const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl"));
const pipeline_descriptor = gpu.RenderPipeline.Descriptor{
.layout = pipeline_layout,
.primitive = .{
.cull_mode = .back,
},
.depth_stencil = &.{
.format = .depth24_plus_stencil8,
.depth_write_enabled = .true,
.depth_compare = .less,
},
.fragment = &gpu.FragmentState.init(.{
.module = shader_module,
.entry_point = "frag_main",
.targets = &.{color_target_state},
}),
.vertex = gpu.VertexState.init(.{
.module = shader_module,
.entry_point = "vertex_main",
.buffers = &.{vertex_buffer_layout},
}),
};
app.render_pipeline = core.device.createRenderPipeline(&pipeline_descriptor);
shader_module.release();
{
const bind_group_entries = [_]gpu.BindGroup.Entry{
.{
.binding = 0,
.buffer = app.uniform_buffers.ubo_matrices.buffer,
.size = app.uniform_buffers.ubo_matrices.size,
},
.{
.binding = 1,
.buffer = app.uniform_buffers.ubo_params.buffer,
.size = app.uniform_buffers.ubo_params.size,
},
.{
.binding = 2,
.buffer = app.uniform_buffers.material_params.buffer,
.size = app.uniform_buffers.material_params.model_size,
},
.{
.binding = 3,
.buffer = app.uniform_buffers.object_params.buffer,
.size = app.uniform_buffers.object_params.model_size,
},
};
app.bind_group = core.device.createBindGroup(
&gpu.BindGroup.Descriptor.init(.{
.layout = bind_group_layout,
.entries = &bind_group_entries,
}),
);
}
}
fn setupRenderPass(app: *App) void {
app.color_attachment = gpu.RenderPassColorAttachment{
.clear_value = .{
.r = 0.0,
.g = 0.0,
.b = 0.0,
.a = 0.0,
},
.load_op = .clear,
.store_op = .store,
};
app.depth_texture = core.device.createTexture(&.{
.usage = .{ .render_attachment = true, .copy_src = true },
.format = .depth24_plus_stencil8,
.sample_count = 1,
.size = .{
.width = core.descriptor.width,
.height = core.descriptor.height,
.depth_or_array_layers = 1,
},
});
app.depth_texture_view = app.depth_texture.createView(&.{
.format = .depth24_plus_stencil8,
.dimension = .dimension_2d,
.array_layer_count = 1,
.aspect = .all,
});
app.depth_stencil_attachment_description = gpu.RenderPassDepthStencilAttachment{
.view = app.depth_texture_view,
.depth_load_op = .clear,
.depth_store_op = .store,
.depth_clear_value = 1.0,
.stencil_clear_value = 0,
.stencil_load_op = .clear,
.stencil_store_op = .store,
};
}
fn loadModels(allocator: std.mem.Allocator, app: *App) !void {
for (model_embeds, 0..) |model_data, model_data_i| {
const m3d_model = m3d.load(model_data, null, null, null) orelse return error.LoadModelFailed;
const vertex_count = m3d_model.handle.numvertex;
const face_count = m3d_model.handle.numface;
var model: *Model = &app.models[model_data_i];
model.index_count = face_count * 3;
var vertex_writer = try VertexWriter(Vertex, u32).init(allocator, face_count * 3, vertex_count, face_count * 3);
defer vertex_writer.deinit(allocator);
const scale: f32 = 0.45;
const vertices = m3d_model.handle.vertex[0..vertex_count];
var i: usize = 0;
while (i < face_count) : (i += 1) {
const face = m3d_model.handle.face[i];
var x: usize = 0;
while (x < 3) : (x += 1) {
const vertex_index = face.vertex[x];
const normal_index = face.normal[x];
const vertex = Vertex{
.position = .{
vertices[vertex_index].x * scale,
vertices[vertex_index].y * scale,
vertices[vertex_index].z * scale,
},
.normal = .{
vertices[normal_index].x,
vertices[normal_index].y,
vertices[normal_index].z,
},
};
vertex_writer.put(vertex, vertex_index);
}
}
const vertex_buffer = vertex_writer.vertexBuffer();
const index_buffer = vertex_writer.indexBuffer();
model.vertex_count = @as(u32, @intCast(vertex_buffer.len));
model.vertex_buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .vertex = true },
.size = @sizeOf(Vertex) * model.vertex_count,
.mapped_at_creation = .false,
});
const queue = core.queue;
queue.writeBuffer(model.vertex_buffer, 0, vertex_buffer);
model.index_buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .index = true },
.size = @sizeOf(u32) * model.index_count,
.mapped_at_creation = .false,
});
queue.writeBuffer(model.index_buffer, 0, index_buffer);
}
}
fn printControls(app: *App) void {
std.debug.print("[controls]\n", .{});
std.debug.print("[p] paused: {}\n", .{app.is_paused});
std.debug.print("[m] material: {s}\n", .{material_names[app.current_material_index]});
std.debug.print("[o] object: {s}\n", .{object_names[app.current_object_index]});
}
fn updateUI(app: *App, event: core.Event) void {
switch (event) {
.key_press => |ev| {
var update_uniform_buffers: bool = false;
switch (ev.key) {
.p => app.is_paused = !app.is_paused,
.m => {
app.current_material_index = (app.current_material_index + 1) % material_names.len;
update_uniform_buffers = true;
},
.o => {
app.current_object_index = (app.current_object_index + 1) % object_names.len;
update_uniform_buffers = true;
},
else => return,
}
app.printControls();
if (update_uniform_buffers) {
updateDynamicUniformBuffer(app);
}
},
else => {},
}
}
fn setupCamera(app: *App) void {
app.camera = Camera{
.rotation_speed = 1.0,
.movement_speed = 1.0,
};
const aspect_ratio: f32 = @as(f32, @floatFromInt(core.descriptor.width)) / @as(f32, @floatFromInt(core.descriptor.height));
app.camera.setPosition(.{ 10.0, 6.0, 6.0 });
app.camera.setRotation(.{ 62.5, 90.0, 0.0 });
app.camera.setMovementSpeed(0.5);
app.camera.setPerspective(60.0, aspect_ratio, 0.1, 256.0);
app.camera.setRotationSpeed(0.25);
}
inline fn roundToMultipleOf4(comptime T: type, value: T) T {
return (value + 3) & ~@as(T, 3);
}
inline fn calculateConstantBufferByteSize(byte_size: usize) usize {
return (byte_size + 255) & ~@as(usize, 255);
}
inline fn toRadians(degrees: f32) f32 {
return degrees * (std.math.pi / 180.0);
}

View file

@ -0,0 +1,118 @@
@group(0) @binding(0) var<uniform> ubo : UBO;
@group(0) @binding(1) var<uniform> uboParams : UBOShared;
@group(0) @binding(2) var<uniform> material : MaterialParams;
@group(0) @binding(3) var<uniform> object : ObjectParams;
struct VertexOut {
@builtin(position) position_clip : vec4<f32>,
@location(0) fragPosition : vec3<f32>,
@location(1) fragNormal : vec3<f32>,
}
struct MaterialParams {
roughness : f32,
metallic : f32,
r : f32,
g : f32,
b : f32
}
struct UBOShared {
lights : array<vec4<f32>, 4>,
}
struct UBO {
projection : mat4x4<f32>,
model : mat4x4<f32>,
view : mat4x4<f32>,
camPos : vec3<f32>,
}
struct ObjectParams {
position : vec3<f32>
}
@vertex fn vertex_main(
@location(0) position : vec3<f32>,
@location(1) normal : vec3<f32>
) -> VertexOut {
var output : VertexOut;
var locPos = vec4<f32>(ubo.model * vec4<f32>(position, 1.0));
output.fragPosition = locPos.xyz + object.position;
output.fragNormal = mat3x3<f32>(ubo.model[0].xyz, ubo.model[1].xyz, ubo.model[2].xyz) * normal;
output.position_clip = ubo.projection * ubo.view * vec4<f32>(output.fragPosition, 1.0);
return output;
}
@fragment fn frag_main(
@location(0) position : vec3<f32>,
@location(1) normal: vec3<f32>
) -> @location(0) vec4<f32> {
var N : vec3<f32> = normalize(normal);
var V : vec3<f32> = normalize(ubo.camPos - position);
var Lo = vec3<f32>(0.0);
// Specular contribution
for(var i: i32 = 0; i < 4; i++) {
var L : vec3<f32> = normalize(uboParams.lights[i].xyz - position);
Lo += BRDF(L, V, N, material.metallic, material.roughness);
}
// Combine with ambient
var color : vec3<f32> = material_color() * 0.02;
color += Lo;
// Gamma correct
color = pow(color, vec3<f32>(0.4545));
return vec4<f32>(color, 1.0);
}
const PI : f32 = 3.14159265359;
fn material_color() -> vec3<f32> {
return vec3<f32>(material.r, material.g, material.b);
}
// Normal Distribution function --------------------------------------
fn D_GGX(dotNH : f32, roughness : f32) -> f32 {
var alpha : f32 = roughness * roughness;
var alpha2 : f32 = alpha * alpha;
var denom : f32 = dotNH * dotNH * (alpha2 - 1.0) + 1.0;
return alpha2 / (PI * denom * denom);
}
// Geometric Shadowing function --------------------------------------
fn G_SchlicksmithGGX(dotNL : f32, dotNV : f32, roughness : f32) -> f32 {
var r : f32 = roughness + 1.0;
var k : f32 = (r * r) / 8.0;
var GL : f32 = dotNL / (dotNL * (1.0 - k) + k);
var GV : f32 = dotNV / (dotNV * (1.0 - k) + k);
return GL * GV;
}
// Fresnel function ----------------------------------------------------
fn F_Schlick(cosTheta : f32, metallic : f32) -> vec3<f32> {
var F0 : vec3<f32> = mix(vec3<f32>(0.04), material_color(), metallic);
var F : vec3<f32> = F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
return F;
}
// Specular BRDF composition --------------------------------------------
fn BRDF(L : vec3<f32>, V : vec3<f32>, N : vec3<f32>, metallic : f32, roughness : f32) -> vec3<f32> {
var H : vec3<f32> = normalize(V + L);
var dotNV : f32 = clamp(dot(N, V), 0.0, 1.0);
var dotNL : f32 = clamp(dot(N, L), 0.0, 1.0);
var dotLH : f32 = clamp(dot(L, H), 0.0, 1.0);
var dotNH : f32 = clamp(dot(N, H), 0.0, 1.0);
var lightColor = vec3<f32>(1.0);
var color = vec3<f32>(0.0);
if(dotNL > 0.0) {
var rroughness : f32 = max(0.05, roughness);
// D = Normal distribution (Distribution of the microfacets)
var D : f32 = D_GGX(dotNH, roughness);
// G = Geometric shadowing term (Microfacets shadowing)
var G : f32 = G_SchlicksmithGGX(dotNL, dotNV, roughness);
// F = Fresnel factor (Reflectance depending on angle of incidence)
var F : vec3<f32> = F_Schlick(dotNV, metallic);
var spec : vec3<f32> = (D * F * G) / (4.0 * dotNL * dotNV);
color += spec * dotNL * lightColor;
}
return color;
}

View file

@ -0,0 +1,188 @@
const std = @import("std");
/// Vertex writer manages the placement of vertices by tracking which are unique. If a duplicate vertex is added
/// with `put`, only it's index will be written to the index buffer.
/// `IndexType` should match the integer type used for the index buffer
pub fn VertexWriter(comptime VertexType: type, comptime IndexType: type) type {
return struct {
const MapEntry = struct {
packed_index: IndexType = null_index,
next_sparse: IndexType = null_index,
};
const null_index: IndexType = std.math.maxInt(IndexType);
vertices: []VertexType,
indices: []IndexType,
sparse_to_packed_map: []MapEntry,
/// Next index outside of the 1:1 mapping range for storing
/// position -> normal collisions
next_collision_index: IndexType,
/// Next packed index
next_packed_index: IndexType,
written_indices_count: IndexType,
/// Allocate storage and set default values
/// `sparse_vertices_count` is the number of vertices in the source before de-duplication / remapping
/// Put more succinctly, the largest index value in source index buffer
/// `max_vertex_count` is largest permutation of vertices assuming that {vertex, uv, normal} never map 1:1 and always
/// create a new mapping
pub fn init(
allocator: std.mem.Allocator,
indices_count: IndexType,
sparse_vertices_count: IndexType,
max_vertex_count: IndexType,
) !@This() {
var result: @This() = undefined;
result.vertices = try allocator.alloc(VertexType, max_vertex_count);
result.indices = try allocator.alloc(IndexType, indices_count);
result.sparse_to_packed_map = try allocator.alloc(MapEntry, max_vertex_count);
result.next_collision_index = sparse_vertices_count;
result.next_packed_index = 0;
result.written_indices_count = 0;
@memset(result.sparse_to_packed_map, .{});
return result;
}
pub fn put(self: *@This(), vertex: VertexType, sparse_index: IndexType) void {
if (self.sparse_to_packed_map[sparse_index].packed_index == null_index) {
// New start of chain, reserve a new packed index and add entry to `index_map`
const packed_index = self.next_packed_index;
self.sparse_to_packed_map[sparse_index].packed_index = packed_index;
self.vertices[packed_index] = vertex;
self.indices[self.written_indices_count] = packed_index;
self.written_indices_count += 1;
self.next_packed_index += 1;
return;
}
var previous_sparse_index: IndexType = undefined;
var current_sparse_index = sparse_index;
while (current_sparse_index != null_index) {
const packed_index = self.sparse_to_packed_map[current_sparse_index].packed_index;
if (std.mem.eql(u8, &std.mem.toBytes(self.vertices[packed_index]), &std.mem.toBytes(vertex))) {
// We already have a record for this vertex in our chain
self.indices[self.written_indices_count] = packed_index;
self.written_indices_count += 1;
return;
}
previous_sparse_index = current_sparse_index;
current_sparse_index = self.sparse_to_packed_map[current_sparse_index].next_sparse;
}
// This is a new mapping for the given sparse index
const packed_index = self.next_packed_index;
const remapped_sparse_index = self.next_collision_index;
self.indices[self.written_indices_count] = packed_index;
self.vertices[packed_index] = vertex;
self.sparse_to_packed_map[previous_sparse_index].next_sparse = remapped_sparse_index;
self.sparse_to_packed_map[remapped_sparse_index].packed_index = packed_index;
self.next_packed_index += 1;
self.next_collision_index += 1;
self.written_indices_count += 1;
}
pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void {
allocator.free(self.vertices);
allocator.free(self.indices);
allocator.free(self.sparse_to_packed_map);
}
pub fn indexBuffer(self: @This()) []IndexType {
return self.indices;
}
pub fn vertexBuffer(self: @This()) []VertexType {
return self.vertices[0..self.next_packed_index];
}
};
}
test "VertexWriter" {
const Vec3 = [3]f32;
const Vertex = extern struct {
position: Vec3,
normal: Vec3,
};
const expect = std.testing.expect;
const allocator = std.testing.allocator;
const Face = struct {
position: [3]u16,
normal: [3]u16,
};
const vertices = [_]Vec3{
Vec3{ 1.0, 0.0, 0.0 }, // 0: Position
Vec3{ 2.0, 0.0, 0.0 }, // 1: Position
Vec3{ 3.0, 0.0, 0.0 }, // 2: Position
Vec3{ 1.0, 0.0, 0.0 }, // 3: Normal
Vec3{ 4.0, 0.0, 0.0 }, // 4: Position
Vec3{ 0.0, 1.0, 0.0 }, // 5: Normal
Vec3{ 5.0, 0.0, 0.0 }, // 6: Position
Vec3{ 0.0, 0.0, 1.0 }, // 7: Normal
Vec3{ 1.0, 0.0, 1.0 }, // 8: Normal
Vec3{ 6.0, 0.0, 0.0 }, // 9: Position
};
const faces = [_]Face{
.{ .position = .{ 0, 4, 2 }, .normal = .{ 7, 5, 3 } },
.{ .position = .{ 2, 3, 9 }, .normal = .{ 3, 7, 8 } },
.{ .position = .{ 9, 2, 4 }, .normal = .{ 8, 7, 5 } },
.{ .position = .{ 2, 6, 1 }, .normal = .{ 3, 5, 7 } },
.{ .position = .{ 9, 6, 0 }, .normal = .{ 5, 7, 8 } },
};
var writer = try VertexWriter(Vertex, u32).init(
allocator,
faces.len * 3, // indices count
vertices.len, // original vertices count
faces.len * 3, // maximum vertices count
);
defer writer.deinit(allocator);
for (faces) |face| {
var x: usize = 0;
while (x < 3) : (x += 1) {
const position_index = face.position[x];
const position = vertices[position_index];
const normal = vertices[face.normal[x]];
const vertex = Vertex{
.position = position,
.normal = normal,
};
writer.put(vertex, position_index);
}
}
const indices = writer.indexBuffer();
try expect(indices.len == faces.len * 3);
// Face 0
try expect(indices[0] == 0); // (0, 7) New
try expect(indices[1] == 1); // (4, 5) New
try expect(indices[2] == 2); // (2, 3) New
// Face 1
try expect(indices[3 + 0] == 2); // (2, 3) Duplicate - Reuse index
try expect(indices[3 + 1] == 3); // (3, 7) New
try expect(indices[3 + 2] == 4); // (9, 8) New
// Face 2
try expect(indices[6 + 0] == 4); // (9, 8) Duplicate - Reuse index
try expect(indices[6 + 1] == 5); // (2, 7) New normal mapping (Don't clobber)
try expect(indices[6 + 2] == 1); // (4, 5) Duplicate - Reuse Index
// Face 3
try expect(indices[9 + 0] == 2); // (2, 3) Duplicate - Reuse index
try expect(indices[9 + 1] == 6); // (6, 5) New
try expect(indices[9 + 2] == 7); // (1, 7) New
// Face 4
try expect(indices[12 + 0] == 8); // (9, 5) New normal mapping (Don't clobber)
try expect(indices[12 + 1] == 9); // (6, 7) New normal mapping (Don't clobber)
try expect(indices[12 + 2] == 10); // (0, 8) New normal mapping (Don't clobber)
try expect(writer.vertexBuffer().len == 11);
}

View file

@ -0,0 +1,49 @@
pub const Vertex = extern struct {
pos: @Vector(3, f32),
normal: @Vector(3, f32),
uv: @Vector(2, f32),
};
pub const vertices = [_]Vertex{
.{ .pos = .{ 1, -1, 1 }, .normal = .{ 0, -1, 0 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, -1, 1 }, .normal = .{ 0, -1, 0 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, -1, -1 }, .normal = .{ 0, -1, 0 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, -1, -1 }, .normal = .{ 0, -1, 0 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, -1, 1 }, .normal = .{ 0, -1, 0 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, -1, -1 }, .normal = .{ 0, -1, 0 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, 1, 1 }, .normal = .{ 1, 0, 0 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, -1, 1 }, .normal = .{ 1, 0, 0 }, .uv = .{ 0, 1 } },
.{ .pos = .{ 1, -1, -1 }, .normal = .{ 1, 0, 0 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, 1, -1 }, .normal = .{ 1, 0, 0 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, 1, 1 }, .normal = .{ 1, 0, 0 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, -1, -1 }, .normal = .{ 1, 0, 0 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, 1, 1 }, .normal = .{ 0, 1, 0 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, 1, 1 }, .normal = .{ 0, 1, 0 }, .uv = .{ 0, 1 } },
.{ .pos = .{ 1, 1, -1 }, .normal = .{ 0, 1, 0 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, 1, -1 }, .normal = .{ 0, 1, 0 }, .uv = .{ 1, 0 } },
.{ .pos = .{ -1, 1, 1 }, .normal = .{ 0, 1, 0 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, 1, -1 }, .normal = .{ 0, 1, 0 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, -1, 1 }, .normal = .{ -1, 0, 0 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, 1, 1 }, .normal = .{ -1, 0, 0 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, 1, -1 }, .normal = .{ -1, 0, 0 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, -1, -1 }, .normal = .{ -1, 0, 0 }, .uv = .{ 1, 0 } },
.{ .pos = .{ -1, -1, 1 }, .normal = .{ -1, 0, 0 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, 1, -1 }, .normal = .{ -1, 0, 0 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, 1, 1 }, .normal = .{ 0, 0, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, 1, 1 }, .normal = .{ 0, 0, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, -1, 1 }, .normal = .{ 0, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, -1, 1 }, .normal = .{ 0, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, -1, 1 }, .normal = .{ 0, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, 1, 1 }, .normal = .{ 0, 0, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, -1, -1 }, .normal = .{ 0, 0, -1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, -1, -1 }, .normal = .{ 0, 0, -1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, 1, -1 }, .normal = .{ 0, 0, -1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, 1, -1 }, .normal = .{ 0, 0, -1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, -1, -1 }, .normal = .{ 0, 0, -1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, 1, -1 }, .normal = .{ 0, 0, -1 }, .uv = .{ 0, 0 } },
};

View file

@ -0,0 +1,461 @@
const std = @import("std");
const core = @import("mach").core;
const gpu = core.gpu;
const zm = @import("zmath");
const Vertex = @import("cube_mesh.zig").Vertex;
const vertices = @import("cube_mesh.zig").vertices;
const Quad = @import("quad_mesh.zig").Quad;
const quad = @import("quad_mesh.zig").quad;
pub const App = @This();
const pixel_size = 8;
const UniformBufferObject = struct {
mat: zm.Mat,
};
const PostUniformBufferObject = extern struct {
width: u32,
height: u32,
pixel_size: u32 = pixel_size,
};
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
title_timer: core.Timer,
timer: core.Timer,
pipeline: *gpu.RenderPipeline,
normal_pipeline: *gpu.RenderPipeline,
vertex_buffer: *gpu.Buffer,
uniform_buffer: *gpu.Buffer,
bind_group: *gpu.BindGroup,
post_pipeline: *gpu.RenderPipeline,
post_vertex_buffer: *gpu.Buffer,
post_uniform_buffer: *gpu.Buffer,
post_bind_group: *gpu.BindGroup,
draw_texture_view: *gpu.TextureView,
depth_texture_view: *gpu.TextureView,
normal_texture_view: *gpu.TextureView,
pub fn init(app: *App) !void {
try core.init(.{});
app.title_timer = try core.Timer.start();
app.timer = try core.Timer.start();
try app.createRenderTextures();
app.createDrawPipeline();
app.createPostPipeline();
}
pub fn deinit(app: *App) void {
defer _ = gpa.deinit();
defer core.deinit();
app.cleanup();
}
pub fn update(app: *App) !bool {
var iter = core.pollEvents();
while (iter.next()) |event| {
switch (event) {
.key_press => |ev| {
if (ev.key == .space) return true;
},
.close => return true,
.framebuffer_resize => {
app.cleanup();
try app.createRenderTextures();
app.createDrawPipeline();
app.createPostPipeline();
},
else => {},
}
}
const size = core.size();
const encoder = core.device.createCommandEncoder(null);
encoder.writeBuffer(app.post_uniform_buffer, 0, &[_]PostUniformBufferObject{
PostUniformBufferObject{
.width = size.width,
.height = size.height,
},
});
{
const time = app.timer.read() * 0.5;
const model = zm.mul(zm.rotationX(time * (std.math.pi / 2.0)), zm.rotationZ(time * (std.math.pi / 2.0)));
const view = zm.lookAtRh(
zm.Vec{ 0, 5, 2, 1 },
zm.Vec{ 0, 0, 0, 1 },
zm.Vec{ 0, 0, 1, 0 },
);
const proj = zm.perspectiveFovRh(
(std.math.pi / 4.0),
@as(f32, @floatFromInt(core.descriptor.width)) / @as(f32, @floatFromInt(core.descriptor.height)),
0.1,
10,
);
const mvp = zm.mul(zm.mul(model, view), proj);
const ubo = UniformBufferObject{
.mat = zm.transpose(mvp),
};
encoder.writeBuffer(app.uniform_buffer, 0, &[_]UniformBufferObject{ubo});
}
{
// render scene to downscaled texture
const color_attachment = gpu.RenderPassColorAttachment{
.view = app.draw_texture_view,
.clear_value = std.mem.zeroes(gpu.Color),
.load_op = .clear,
.store_op = .store,
};
const render_pass_info = gpu.RenderPassDescriptor.init(.{
.color_attachments = &.{color_attachment},
.depth_stencil_attachment = &gpu.RenderPassDepthStencilAttachment{
.view = app.depth_texture_view,
.depth_load_op = .clear,
.depth_store_op = .store,
.depth_clear_value = 1.0,
},
});
const pass = encoder.beginRenderPass(&render_pass_info);
pass.setPipeline(app.pipeline);
pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len);
pass.setBindGroup(0, app.bind_group, &.{0});
pass.draw(vertices.len, 1, 0, 0);
pass.end();
pass.release();
}
{
// render scene normals to texture
const normal_color_attachment = gpu.RenderPassColorAttachment{
.view = app.normal_texture_view,
.clear_value = .{ .r = 0.5, .b = 0.5, .g = 0.5, .a = 1.0 },
.load_op = .clear,
.store_op = .store,
};
const normal_render_pass_info = gpu.RenderPassDescriptor.init(.{
.color_attachments = &.{normal_color_attachment},
});
const normal_pass = encoder.beginRenderPass(&normal_render_pass_info);
normal_pass.setPipeline(app.normal_pipeline);
normal_pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len);
normal_pass.setBindGroup(0, app.bind_group, &.{0});
normal_pass.draw(vertices.len, 1, 0, 0);
normal_pass.end();
normal_pass.release();
}
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
{
// render to swap chain using previous passes
const post_color_attachment = gpu.RenderPassColorAttachment{
.view = back_buffer_view,
.clear_value = std.mem.zeroes(gpu.Color),
.load_op = .clear,
.store_op = .store,
};
const post_render_pass_info = gpu.RenderPassDescriptor.init(.{
.color_attachments = &.{post_color_attachment},
});
const draw_pass = encoder.beginRenderPass(&post_render_pass_info);
draw_pass.setPipeline(app.post_pipeline);
draw_pass.setVertexBuffer(0, app.post_vertex_buffer, 0, @sizeOf(Quad) * quad.len);
draw_pass.setBindGroup(0, app.post_bind_group, &.{0});
draw_pass.draw(quad.len, 1, 0, 0);
draw_pass.end();
draw_pass.release();
}
var command = encoder.finish(null);
encoder.release();
const queue = core.queue;
queue.submit(&[_]*gpu.CommandBuffer{command});
command.release();
core.swap_chain.present();
back_buffer_view.release();
// update the window title every second
if (app.title_timer.read() >= 1.0) {
app.title_timer.reset();
try core.printTitle("Pixel Post Process [ {d}fps ] [ Input {d}hz ]", .{
core.frameRate(),
core.inputRate(),
});
}
return false;
}
fn cleanup(app: *App) void {
app.pipeline.release();
app.normal_pipeline.release();
app.vertex_buffer.release();
app.uniform_buffer.release();
app.bind_group.release();
app.post_pipeline.release();
app.post_vertex_buffer.release();
app.post_uniform_buffer.release();
app.post_bind_group.release();
app.draw_texture_view.release();
app.depth_texture_view.release();
app.normal_texture_view.release();
}
fn createRenderTextures(app: *App) !void {
const size = core.size();
const draw_texture_desc = gpu.Texture.Descriptor.init(.{
.size = .{ .width = size.width / pixel_size, .height = size.height / pixel_size },
.format = .bgra8_unorm,
.usage = .{ .texture_binding = true, .copy_dst = true, .render_attachment = true },
});
const draw_texture = core.device.createTexture(&draw_texture_desc);
app.draw_texture_view = draw_texture.createView(null);
draw_texture.release();
const depth_texture_desc = gpu.Texture.Descriptor.init(.{
.size = .{ .width = size.width / pixel_size, .height = size.height / pixel_size },
.format = .depth32_float,
.usage = .{ .texture_binding = true, .copy_dst = true, .render_attachment = true },
});
const depth_texture = core.device.createTexture(&depth_texture_desc);
app.depth_texture_view = depth_texture.createView(null);
depth_texture.release();
const normal_texture_desc = gpu.Texture.Descriptor.init(.{
.size = .{ .width = size.width / pixel_size, .height = size.height / pixel_size },
.format = .bgra8_unorm,
.usage = .{ .texture_binding = true, .copy_dst = true, .render_attachment = true },
});
const normal_texture = core.device.createTexture(&normal_texture_desc);
app.normal_texture_view = normal_texture.createView(null);
normal_texture.release();
}
fn createDrawPipeline(app: *App) void {
const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl"));
const vertex_attributes = [_]gpu.VertexAttribute{
.{ .format = .float32x3, .offset = @offsetOf(Vertex, "pos"), .shader_location = 0 },
.{ .format = .float32x3, .offset = @offsetOf(Vertex, "normal"), .shader_location = 1 },
.{ .format = .float32x2, .offset = @offsetOf(Vertex, "uv"), .shader_location = 2 },
};
const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{
.array_stride = @sizeOf(Vertex),
.step_mode = .vertex,
.attributes = &vertex_attributes,
});
const vertex = gpu.VertexState.init(.{
.module = shader_module,
.entry_point = "vertex_main",
.buffers = &.{vertex_buffer_layout},
});
const blend = gpu.BlendState{};
const color_target = gpu.ColorTargetState{
.format = core.descriptor.format,
.blend = &blend,
.write_mask = gpu.ColorWriteMaskFlags.all,
};
const fragment = gpu.FragmentState.init(.{
.module = shader_module,
.entry_point = "frag_main",
.targets = &.{color_target},
});
const bgle = gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, true, 0);
const bgl = core.device.createBindGroupLayout(
&gpu.BindGroupLayout.Descriptor.init(.{
.entries = &.{bgle},
}),
);
const bind_group_layouts = [_]*gpu.BindGroupLayout{bgl};
const pipeline_layout = core.device.createPipelineLayout(
&gpu.PipelineLayout.Descriptor.init(.{
.bind_group_layouts = &bind_group_layouts,
}),
);
const vertex_buffer = core.device.createBuffer(&.{
.usage = .{ .vertex = true },
.size = @sizeOf(Vertex) * vertices.len,
.mapped_at_creation = .true,
});
const vertex_mapped = vertex_buffer.getMappedRange(Vertex, 0, vertices.len);
@memcpy(vertex_mapped.?, vertices[0..]);
vertex_buffer.unmap();
const uniform_buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .uniform = true },
.size = @sizeOf(UniformBufferObject),
.mapped_at_creation = .false,
});
const bind_group = core.device.createBindGroup(
&gpu.BindGroup.Descriptor.init(.{
.layout = bgl,
.entries = &.{
gpu.BindGroup.Entry.buffer(0, uniform_buffer, 0, @sizeOf(UniformBufferObject)),
},
}),
);
const pipeline_descriptor = gpu.RenderPipeline.Descriptor{
.fragment = &fragment,
.layout = pipeline_layout,
.vertex = vertex,
.primitive = .{
.cull_mode = .back,
},
.depth_stencil = &gpu.DepthStencilState{
.format = .depth32_float,
.depth_write_enabled = .true,
.depth_compare = .less,
},
};
{
// "same" pipeline, different fragment shader to create a texture with normal information
const normal_fs_module = core.device.createShaderModuleWGSL("normal_frag.wgsl", @embedFile("normal_frag.wgsl"));
const normal_fragment = gpu.FragmentState.init(.{
.module = normal_fs_module,
.entry_point = "main",
.targets = &.{color_target},
});
const normal_pipeline_descriptor = gpu.RenderPipeline.Descriptor{
.fragment = &normal_fragment,
.layout = pipeline_layout,
.vertex = vertex,
.primitive = .{
.cull_mode = .back,
},
};
app.normal_pipeline = core.device.createRenderPipeline(&normal_pipeline_descriptor);
normal_fs_module.release();
}
app.pipeline = core.device.createRenderPipeline(&pipeline_descriptor);
app.vertex_buffer = vertex_buffer;
app.uniform_buffer = uniform_buffer;
app.bind_group = bind_group;
shader_module.release();
pipeline_layout.release();
bgl.release();
}
fn createPostPipeline(app: *App) void {
const vs_module = core.device.createShaderModuleWGSL("pixel_vert.wgsl", @embedFile("pixel_vert.wgsl"));
const vertex_attributes = [_]gpu.VertexAttribute{
.{ .format = .float32x3, .offset = @offsetOf(Quad, "pos"), .shader_location = 0 },
.{ .format = .float32x2, .offset = @offsetOf(Quad, "uv"), .shader_location = 1 },
};
const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{
.array_stride = @sizeOf(Quad),
.step_mode = .vertex,
.attributes = &vertex_attributes,
});
const vertex = gpu.VertexState.init(.{
.module = vs_module,
.entry_point = "main",
.buffers = &.{vertex_buffer_layout},
});
const fs_module = core.device.createShaderModuleWGSL("pixel_frag.wgsl", @embedFile("pixel_frag.wgsl"));
const blend = gpu.BlendState{};
const color_target = gpu.ColorTargetState{
.format = core.descriptor.format,
.blend = &blend,
.write_mask = gpu.ColorWriteMaskFlags.all,
};
const fragment = gpu.FragmentState.init(.{
.module = fs_module,
.entry_point = "main",
.targets = &.{color_target},
});
const bgl = core.device.createBindGroupLayout(
&gpu.BindGroupLayout.Descriptor.init(.{
.entries = &.{
gpu.BindGroupLayout.Entry.texture(0, .{ .fragment = true }, .float, .dimension_2d, false),
gpu.BindGroupLayout.Entry.sampler(1, .{ .fragment = true }, .filtering),
gpu.BindGroupLayout.Entry.texture(2, .{ .fragment = true }, .depth, .dimension_2d, false),
gpu.BindGroupLayout.Entry.sampler(3, .{ .fragment = true }, .filtering),
gpu.BindGroupLayout.Entry.texture(4, .{ .fragment = true }, .float, .dimension_2d, false),
gpu.BindGroupLayout.Entry.sampler(5, .{ .fragment = true }, .filtering),
gpu.BindGroupLayout.Entry.buffer(6, .{ .fragment = true }, .uniform, true, 0),
},
}),
);
const bind_group_layouts = [_]*gpu.BindGroupLayout{bgl};
const pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{
.bind_group_layouts = &bind_group_layouts,
}));
const vertex_buffer = core.device.createBuffer(&.{
.usage = .{ .vertex = true },
.size = @sizeOf(Quad) * quad.len,
.mapped_at_creation = .true,
});
const vertex_mapped = vertex_buffer.getMappedRange(Quad, 0, quad.len);
@memcpy(vertex_mapped.?, quad[0..]);
vertex_buffer.unmap();
const draw_sampler = core.device.createSampler(null);
const depth_sampler = core.device.createSampler(null);
const normal_sampler = core.device.createSampler(null);
const uniform_buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .uniform = true },
.size = @sizeOf(PostUniformBufferObject),
.mapped_at_creation = .false,
});
const bind_group = core.device.createBindGroup(
&gpu.BindGroup.Descriptor.init(.{
.layout = bgl,
.entries = &[_]gpu.BindGroup.Entry{
gpu.BindGroup.Entry.textureView(0, app.draw_texture_view),
gpu.BindGroup.Entry.sampler(1, draw_sampler),
gpu.BindGroup.Entry.textureView(2, app.depth_texture_view),
gpu.BindGroup.Entry.sampler(3, depth_sampler),
gpu.BindGroup.Entry.textureView(4, app.normal_texture_view),
gpu.BindGroup.Entry.sampler(5, normal_sampler),
gpu.BindGroup.Entry.buffer(6, uniform_buffer, 0, @sizeOf(PostUniformBufferObject)),
},
}),
);
draw_sampler.release();
depth_sampler.release();
normal_sampler.release();
const pipeline_descriptor = gpu.RenderPipeline.Descriptor{
.fragment = &fragment,
.layout = pipeline_layout,
.vertex = vertex,
.primitive = .{
.cull_mode = .back,
},
};
app.post_pipeline = core.device.createRenderPipeline(&pipeline_descriptor);
app.post_vertex_buffer = vertex_buffer;
app.post_uniform_buffer = uniform_buffer;
app.post_bind_group = bind_group;
vs_module.release();
fs_module.release();
pipeline_layout.release();
bgl.release();
}

View file

@ -0,0 +1,6 @@
@fragment fn main(
@location(0) normal: vec3<f32>,
@location(1) uv: vec2<f32>,
) -> @location(0) vec4<f32> {
return vec4<f32>(normal / 2 + 0.5, 1.0);
}

View file

@ -0,0 +1,74 @@
@group(0) @binding(0)
var draw_texture: texture_2d<f32>;
@group(0) @binding(1)
var draw_texture_sampler: sampler;
@group(0) @binding(2)
var depth_texture: texture_depth_2d;
@group(0) @binding(3)
var depth_texture_sampler: sampler;
@group(0) @binding(4)
var normal_texture: texture_2d<f32>;
@group(0) @binding(5)
var normal_texture_sampler: sampler;
struct View {
@location(0) width: u32,
@location(1) height: u32,
@location(2) pixel_size: u32,
}
@group(0) @binding(6)
var<uniform> view: View;
fn sample_depth(uv: vec2<f32>, x: f32, y: f32) -> f32 {
return textureSample(
depth_texture,
depth_texture_sampler,
uv + vec2<f32>(x * f32(view.pixel_size) / f32(view.width), y * f32(view.pixel_size) / f32(view.height))
);
}
fn sample_normal(uv: vec2<f32>, x: f32, y: f32) -> vec3<f32> {
return textureSample(
normal_texture,
normal_texture_sampler,
uv + vec2<f32>(x * f32(view.pixel_size) / f32(view.width), y * f32(view.pixel_size) / f32(view.height))
).xyz;
}
fn normal_indicator(uv: vec2<f32>, x: f32, y: f32) -> f32 {
var depth_diff = sample_depth(uv, 0, 0) - sample_depth(uv, x, y);
var dx = sample_normal(uv, 0, 0);
var dy = sample_normal(uv, x, y);
if (depth_diff > 0) {
// only sample normals from closest pixel
return 0;
}
return distance(dx, dy);
}
@fragment fn main(
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>
) -> @location(0) vec4<f32> {
var depth = sample_depth(uv, 0, 0);
var depth_diff: f32 = 0;
depth_diff += abs(depth - sample_depth(uv, -1, 0));
depth_diff += abs(depth - sample_depth(uv, 1, 0));
depth_diff += abs(depth - sample_depth(uv, 0, -1));
depth_diff += abs(depth - sample_depth(uv, 0, 1));
var normal_diff: f32 = 0;
normal_diff += normal_indicator(uv, -1, 0);
normal_diff += normal_indicator(uv, 1, 0);
normal_diff += normal_indicator(uv, 0, -1);
normal_diff += normal_indicator(uv, 0, 1);
var color = textureSample(draw_texture, draw_texture_sampler, uv);
if (depth_diff > 0.007) { // magic number from testing
return color * 0.7;
}
// add instead of multiply so really dark pixels get brighter
return color + (vec4<f32>(1) * step(0.1, normal_diff) * 0.7);
}

View file

@ -0,0 +1,14 @@
struct VertexOut {
@builtin(position) position_clip: vec4<f32>,
@location(0) uv: vec2<f32>
}
@vertex fn main(
@location(0) position: vec3<f32>,
@location(1) uv: vec2<f32>
) -> VertexOut {
var output : VertexOut;
output.position_clip = vec4<f32>(position.xy, 0.0, 1.0);
output.uv = uv;
return output;
}

View file

@ -0,0 +1,13 @@
pub const Quad = extern struct {
pos: @Vector(3, f32),
uv: @Vector(2, f32),
};
pub const quad = [_]Quad{
.{ .pos = .{ -1.0, 1.0, 0.0 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1.0, -1.0, 0.0 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1.0, 1.0, 0.0 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1.0, 1.0, 0.0 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1.0, -1.0, 0.0 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1.0, -1.0, 0.0 }, .uv = .{ 1, 0 } },
};

View file

@ -0,0 +1,27 @@
@group(0) @binding(0) var<uniform> ubo: mat4x4<f32>;
struct VertexOut {
@builtin(position) position_clip: vec4<f32>,
@location(0) normal: vec3<f32>,
@location(1) uv: vec2<f32>,
}
@vertex fn vertex_main(
@location(0) position: vec3<f32>,
@location(1) normal: vec3<f32>,
@location(2) uv: vec2<f32>
) -> VertexOut {
var output: VertexOut;
output.position_clip = vec4<f32>(position, 1) * ubo;
output.normal = (vec4<f32>(normal, 0) * ubo).xyz;
output.uv = uv;
return output;
}
@fragment fn frag_main(
@location(0) normal: vec3<f32>,
@location(1) uv: vec2<f32>,
) -> @location(0) vec4<f32> {
var color = floor((uv * 0.5 + 0.25) * 32) / 32;
return vec4<f32>(color, 1, 1);
}

View file

@ -0,0 +1,62 @@
const std = @import("std");
const core = @import("mach").core;
const gpu = core.gpu;
const renderer = @import("renderer.zig");
pub const App = @This();
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
title_timer: core.Timer,
pub fn init(app: *App) !void {
try core.init(.{ .required_limits = gpu.Limits{
.max_vertex_buffers = 1,
.max_vertex_attributes = 2,
.max_bind_groups = 1,
.max_uniform_buffers_per_shader_stage = 1,
.max_uniform_buffer_binding_size = 16 * 1 * @sizeOf(f32),
} });
const allocator = gpa.allocator();
const timer = try core.Timer.start();
try renderer.init(allocator, timer);
app.* = .{ .title_timer = try core.Timer.start() };
}
pub fn deinit(app: *App) void {
_ = app;
defer _ = gpa.deinit();
defer core.deinit();
defer renderer.deinit();
}
pub fn update(app: *App) !bool {
var iter = core.pollEvents();
while (iter.next()) |event| {
switch (event) {
.key_press => |ev| {
if (ev.key == .space) return true;
if (ev.key == .right) {
renderer.curr_primitive_index += 1;
renderer.curr_primitive_index %= 7;
}
},
.close => return true,
else => {},
}
}
renderer.update();
// update the window title every second
if (app.title_timer.read() >= 1.0) {
app.title_timer.reset();
try core.printTitle("Procedural Primitives [ {d}fps ] [ Input {d}hz ]", .{
core.frameRate(),
core.inputRate(),
});
}
return false;
}

View file

@ -0,0 +1,336 @@
const std = @import("std");
const zmath = @import("zmath");
const PI = 3.1415927410125732421875;
pub const F32x3 = @Vector(3, f32);
pub const F32x4 = @Vector(4, f32);
pub const VertexData = struct {
position: F32x3,
normal: F32x3,
};
pub const PrimitiveType = enum(u4) { none, triangle, quad, plane, circle, uv_sphere, ico_sphere, cylinder, cone, torus };
pub const Primitive = struct {
vertex_data: std.ArrayList(VertexData),
vertex_count: u32,
index_data: std.ArrayList(u32),
index_count: u32,
type: PrimitiveType = .none,
};
// 2D Primitives
pub fn createTrianglePrimitive(allocator: std.mem.Allocator, size: f32) !Primitive {
const vertex_count = 3;
const index_count = 3;
var vertex_data = try std.ArrayList(VertexData).initCapacity(allocator, vertex_count);
const edge = size / 2.0;
vertex_data.appendSliceAssumeCapacity(&[vertex_count]VertexData{
VertexData{ .position = F32x3{ -edge, -edge, 0.0 }, .normal = F32x3{ -edge, -edge, 0.0 } },
VertexData{ .position = F32x3{ edge, -edge, 0.0 }, .normal = F32x3{ edge, -edge, 0.0 } },
VertexData{ .position = F32x3{ 0.0, edge, 0.0 }, .normal = F32x3{ 0.0, edge, 0.0 } },
});
var index_data = try std.ArrayList(u32).initCapacity(allocator, index_count);
index_data.appendSliceAssumeCapacity(&[index_count]u32{ 0, 1, 2 });
return Primitive{ .vertex_data = vertex_data, .vertex_count = 3, .index_data = index_data, .index_count = 3, .type = .triangle };
}
pub fn createQuadPrimitive(allocator: std.mem.Allocator, size: f32) !Primitive {
const vertex_count = 4;
const index_count = 6;
var vertex_data = try std.ArrayList(VertexData).initCapacity(allocator, vertex_count);
const edge = size / 2.0;
vertex_data.appendSliceAssumeCapacity(&[vertex_count]VertexData{
VertexData{ .position = F32x3{ -edge, -edge, 0.0 }, .normal = F32x3{ -edge, -edge, 0.0 } },
VertexData{ .position = F32x3{ edge, -edge, 0.0 }, .normal = F32x3{ edge, -edge, 0.0 } },
VertexData{ .position = F32x3{ -edge, edge, 0.0 }, .normal = F32x3{ -edge, edge, 0.0 } },
VertexData{ .position = F32x3{ edge, edge, 0.0 }, .normal = F32x3{ edge, edge, 0.0 } },
});
var index_data = try std.ArrayList(u32).initCapacity(allocator, index_count);
index_data.appendSliceAssumeCapacity(&[index_count]u32{
0, 1, 2,
1, 3, 2,
});
return Primitive{ .vertex_data = vertex_data, .vertex_count = 4, .index_data = index_data, .index_count = 6, .type = .quad };
}
pub fn createPlanePrimitive(allocator: std.mem.Allocator, x_subdivision: u32, y_subdivision: u32, size: f32) !Primitive {
const x_num_vertices = x_subdivision + 1;
const y_num_vertices = y_subdivision + 1;
const vertex_count = x_num_vertices * y_num_vertices;
var vertex_data = try std.ArrayList(VertexData).initCapacity(allocator, vertex_count);
const vertices_distance_y = (size / @as(f32, @floatFromInt(y_subdivision)));
const vertices_distance_x = (size / @as(f32, @floatFromInt(x_subdivision)));
var y: u32 = 0;
while (y < y_num_vertices) : (y += 1) {
var x: u32 = 0;
const pos_y = (-size / 2.0) + @as(f32, @floatFromInt(y)) * vertices_distance_y;
while (x < x_num_vertices) : (x += 1) {
const pos_x = (-size / 2.0) + @as(f32, @floatFromInt(x)) * vertices_distance_x;
vertex_data.appendAssumeCapacity(VertexData{ .position = F32x3{ pos_x, pos_y, 0.0 }, .normal = F32x3{ pos_x, pos_y, 0.0 } });
}
}
const index_count = x_subdivision * y_subdivision * 2 * 3;
var index_data = try std.ArrayList(u32).initCapacity(allocator, index_count);
y = 0;
while (y < y_subdivision) : (y += 1) {
var x: u32 = 0;
while (x < x_subdivision) : (x += 1) {
// First Triangle of Quad
index_data.appendAssumeCapacity(x + y * y_num_vertices);
index_data.appendAssumeCapacity(x + 1 + y * y_num_vertices);
index_data.appendAssumeCapacity(x + (y + 1) * y_num_vertices);
// Second Triangle of Quad
index_data.appendAssumeCapacity(x + 1 + y * y_num_vertices);
index_data.appendAssumeCapacity(x + (y + 1) * y_num_vertices + 1);
index_data.appendAssumeCapacity(x + (y + 1) * y_num_vertices);
}
}
return Primitive{ .vertex_data = vertex_data, .vertex_count = vertex_count, .index_data = index_data, .index_count = index_count, .type = .plane };
}
pub fn createCirclePrimitive(allocator: std.mem.Allocator, vertices: u32, radius: f32) !Primitive {
const vertex_count = vertices + 1;
var vertex_data = try std.ArrayList(VertexData).initCapacity(allocator, vertex_count);
// Mid point of circle
vertex_data.appendAssumeCapacity(VertexData{ .position = F32x3{ 0, 0, 0.0 }, .normal = F32x3{ 0, 0, 0.0 } });
var x: u32 = 0;
const angle = 2 * PI / @as(f32, @floatFromInt(vertices));
while (x < vertices) : (x += 1) {
const x_f = @as(f32, @floatFromInt(x));
const pos_x = radius * zmath.cos(angle * x_f);
const pos_y = radius * zmath.sin(angle * x_f);
vertex_data.appendAssumeCapacity(VertexData{ .position = F32x3{ pos_x, pos_y, 0.0 }, .normal = F32x3{ pos_x, pos_y, 0.0 } });
}
const index_count = (vertices + 1) * 3;
var index_data = try std.ArrayList(u32).initCapacity(allocator, index_count);
x = 1;
while (x <= vertices) : (x += 1) {
index_data.appendAssumeCapacity(0);
index_data.appendAssumeCapacity(x);
index_data.appendAssumeCapacity(x + 1);
}
index_data.appendAssumeCapacity(0);
index_data.appendAssumeCapacity(vertices);
index_data.appendAssumeCapacity(1);
return Primitive{ .vertex_data = vertex_data, .vertex_count = vertex_count, .index_data = index_data, .index_count = index_count, .type = .plane };
}
// 3D Primitives
pub fn createCubePrimitive(allocator: std.mem.Allocator, size: f32) !Primitive {
const vertex_count = 8;
const index_count = 36;
var vertex_data = try std.ArrayList(VertexData).initCapacity(allocator, vertex_count);
const edge = size / 2.0;
vertex_data.appendSliceAssumeCapacity(&[vertex_count]VertexData{
// Front positions
VertexData{ .position = F32x3{ -edge, -edge, edge }, .normal = F32x3{ -edge, -edge, edge } },
VertexData{ .position = F32x3{ edge, -edge, edge }, .normal = F32x3{ edge, -edge, edge } },
VertexData{ .position = F32x3{ edge, edge, edge }, .normal = F32x3{ edge, edge, edge } },
VertexData{ .position = F32x3{ -edge, edge, edge }, .normal = F32x3{ -edge, edge, edge } },
// Back positions
VertexData{ .position = F32x3{ -edge, -edge, -edge }, .normal = F32x3{ -edge, -edge, -edge } },
VertexData{ .position = F32x3{ edge, -edge, -edge }, .normal = F32x3{ edge, -edge, -edge } },
VertexData{ .position = F32x3{ edge, edge, -edge }, .normal = F32x3{ edge, edge, -edge } },
VertexData{ .position = F32x3{ -edge, edge, -edge }, .normal = F32x3{ -edge, edge, -edge } },
});
var index_data = try std.ArrayList(u32).initCapacity(allocator, index_count);
index_data.appendSliceAssumeCapacity(&[index_count]u32{
// front quad
0, 1, 2,
2, 3, 0,
// right quad
1, 5, 6,
6, 2, 1,
// back quad
7, 6, 5,
5, 4, 7,
// left quad
4, 0, 3,
3, 7, 4,
// bottom quad
4, 5, 1,
1, 0, 4,
// top quad
3, 2, 6,
6, 7, 3,
});
return Primitive{ .vertex_data = vertex_data, .vertex_count = vertex_count, .index_data = index_data, .index_count = index_count, .type = .quad };
}
const VertexDataMAL = std.MultiArrayList(VertexData);
pub fn createCylinderPrimitive(allocator: std.mem.Allocator, radius: f32, height: f32, num_sides: u32) !Primitive {
const alloc_amt_vert: u32 = num_sides * 2 + 2;
const alloc_amt_idx: u32 = num_sides * 12;
var vertex_data = VertexDataMAL{};
try vertex_data.ensureTotalCapacity(allocator, alloc_amt_vert);
defer vertex_data.deinit(allocator);
var out_vertex_data = try std.ArrayList(VertexData).initCapacity(allocator, alloc_amt_vert);
var index_data = try std.ArrayList(u32).initCapacity(allocator, alloc_amt_idx);
vertex_data.appendAssumeCapacity(VertexData{ .position = F32x3{ 0.0, (height / 2.0), 0.0 }, .normal = undefined });
vertex_data.appendAssumeCapacity(VertexData{ .position = F32x3{ 0.0, -(height / 2.0), 0.0 }, .normal = undefined });
const angle = 2.0 * PI / @as(f32, @floatFromInt(num_sides));
for (1..num_sides + 1) |i| {
const float_i = @as(f32, @floatFromInt(i));
const x: f32 = radius * zmath.sin(angle * float_i);
const y: f32 = radius * zmath.cos(angle * float_i);
vertex_data.appendAssumeCapacity(VertexData{ .position = F32x3{ x, (height / 2.0), y }, .normal = undefined });
vertex_data.appendAssumeCapacity(VertexData{ .position = F32x3{ x, -(height / 2.0), y }, .normal = undefined });
}
var group1: u32 = 1;
var group2: u32 = 3;
for (0..num_sides) |_| {
if (group2 >= num_sides * 2) group2 = 1;
index_data.appendSliceAssumeCapacity(&[_]u32{
0, group1 + 1, group2 + 1,
group1 + 1, group1 + 2, group2 + 1,
group1 + 2, group2 + 2, group2 + 1,
group2 + 2, group1 + 2, 1,
});
group1 += 2;
group2 += 2;
}
{
var i: u32 = 0;
while (i < alloc_amt_idx) : (i += 3) {
const indexA: u32 = index_data.items[i];
const indexB: u32 = index_data.items[i + 1];
const indexC: u32 = index_data.items[i + 2];
const vert1: F32x4 = F32x4{ vertex_data.get(indexA).position[0], vertex_data.get(indexA).position[1], vertex_data.get(indexA).position[2], 1.0 };
const vert2: F32x4 = F32x4{ vertex_data.get(indexB).position[0], vertex_data.get(indexB).position[1], vertex_data.get(indexB).position[2], 1.0 };
const vert3: F32x4 = F32x4{ vertex_data.get(indexC).position[0], vertex_data.get(indexC).position[1], vertex_data.get(indexC).position[2], 1.0 };
const edgeAB: F32x4 = vert2 - vert1;
const edgeAC: F32x4 = vert3 - vert1;
const cross = zmath.cross3(edgeAB, edgeAC);
vertex_data.items(.normal)[indexA][0] += cross[0];
vertex_data.items(.normal)[indexA][1] += cross[1];
vertex_data.items(.normal)[indexA][2] += cross[2];
vertex_data.items(.normal)[indexB][0] += cross[0];
vertex_data.items(.normal)[indexB][1] += cross[1];
vertex_data.items(.normal)[indexB][2] += cross[2];
vertex_data.items(.normal)[indexC][0] += cross[0];
vertex_data.items(.normal)[indexC][1] += cross[1];
vertex_data.items(.normal)[indexC][2] += cross[2];
}
}
for (vertex_data.items(.position), vertex_data.items(.normal)) |pos, nor| {
out_vertex_data.appendAssumeCapacity(VertexData{ .position = pos, .normal = nor });
}
return Primitive{ .vertex_data = out_vertex_data, .vertex_count = alloc_amt_vert, .index_data = index_data, .index_count = alloc_amt_idx, .type = .cylinder };
}
pub fn createConePrimitive(allocator: std.mem.Allocator, radius: f32, height: f32, num_sides: u32) !Primitive {
const alloc_amt_vert: u32 = num_sides + 2;
const alloc_amt_idx: u32 = num_sides * 6;
var vertex_data = VertexDataMAL{};
try vertex_data.ensureTotalCapacity(allocator, alloc_amt_vert);
defer vertex_data.deinit(allocator);
var out_vertex_data = try std.ArrayList(VertexData).initCapacity(allocator, alloc_amt_vert);
var index_data = try std.ArrayList(u32).initCapacity(allocator, alloc_amt_idx);
vertex_data.appendAssumeCapacity(VertexData{ .position = F32x3{ 0.0, (height / 2.0), 0.0 }, .normal = undefined });
vertex_data.appendAssumeCapacity(VertexData{ .position = F32x3{ 0.0, -(height / 2.0), 0.0 }, .normal = undefined });
const angle = 2.0 * PI / @as(f32, @floatFromInt(num_sides));
for (1..num_sides + 1) |i| {
const float_i = @as(f32, @floatFromInt(i));
const x: f32 = radius * zmath.sin(angle * float_i);
const y: f32 = radius * zmath.cos(angle * float_i);
vertex_data.appendAssumeCapacity(VertexData{ .position = F32x3{ x, -(height / 2.0), y }, .normal = undefined });
}
var group1: u32 = 1;
var group2: u32 = 2;
for (0..num_sides) |_| {
if (group2 >= num_sides + 1) group2 = 1;
index_data.appendSliceAssumeCapacity(&[_]u32{
0, group1 + 1, group2 + 1,
group2 + 1, group1 + 1, 1,
});
group1 += 1;
group2 += 1;
}
{
var i: u32 = 0;
while (i < alloc_amt_idx) : (i += 3) {
const indexA: u32 = index_data.items[i];
const indexB: u32 = index_data.items[i + 1];
const indexC: u32 = index_data.items[i + 2];
const vert1: F32x4 = F32x4{ vertex_data.get(indexA).position[0], vertex_data.get(indexA).position[1], vertex_data.get(indexA).position[2], 1.0 };
const vert2: F32x4 = F32x4{ vertex_data.get(indexB).position[0], vertex_data.get(indexB).position[1], vertex_data.get(indexB).position[2], 1.0 };
const vert3: F32x4 = F32x4{ vertex_data.get(indexC).position[0], vertex_data.get(indexC).position[1], vertex_data.get(indexC).position[2], 1.0 };
const edgeAB: F32x4 = vert2 - vert1;
const edgeAC: F32x4 = vert3 - vert1;
const cross = zmath.cross3(edgeAB, edgeAC);
vertex_data.items(.normal)[indexA][0] += cross[0];
vertex_data.items(.normal)[indexA][1] += cross[1];
vertex_data.items(.normal)[indexA][2] += cross[2];
vertex_data.items(.normal)[indexB][0] += cross[0];
vertex_data.items(.normal)[indexB][1] += cross[1];
vertex_data.items(.normal)[indexB][2] += cross[2];
vertex_data.items(.normal)[indexC][0] += cross[0];
vertex_data.items(.normal)[indexC][1] += cross[1];
vertex_data.items(.normal)[indexC][2] += cross[2];
}
}
for (vertex_data.items(.position), vertex_data.items(.normal)) |pos, nor| {
out_vertex_data.appendAssumeCapacity(VertexData{ .position = pos, .normal = nor });
}
return Primitive{ .vertex_data = out_vertex_data, .vertex_count = alloc_amt_vert, .index_data = index_data, .index_count = alloc_amt_idx, .type = .cone };
}

View file

@ -0,0 +1,325 @@
const std = @import("std");
const core = @import("mach").core;
const gpu = core.gpu;
const zm = @import("zmath");
const primitives = @import("procedural-primitives.zig");
const Primitive = primitives.Primitive;
const VertexData = primitives.VertexData;
pub const Renderer = @This();
var queue: *gpu.Queue = undefined;
var pipeline: *gpu.RenderPipeline = undefined;
var app_timer: core.Timer = undefined;
var depth_texture: *gpu.Texture = undefined;
var depth_texture_view: *gpu.TextureView = undefined;
const PrimitiveRenderData = struct {
vertex_buffer: *gpu.Buffer,
index_buffer: *gpu.Buffer,
vertex_count: u32,
index_count: u32,
};
const UniformBufferObject = struct {
mvp_matrix: zm.Mat,
};
var uniform_buffer: *gpu.Buffer = undefined;
var bind_group: *gpu.BindGroup = undefined;
var primitives_data: [7]PrimitiveRenderData = undefined;
pub var curr_primitive_index: u4 = 0;
pub fn init(allocator: std.mem.Allocator, timer: core.Timer) !void {
queue = core.queue;
app_timer = timer;
{
const triangle_primitive = try primitives.createTrianglePrimitive(allocator, 1);
primitives_data[0] = PrimitiveRenderData{ .vertex_buffer = createVertexBuffer(triangle_primitive), .index_buffer = createIndexBuffer(triangle_primitive), .vertex_count = triangle_primitive.vertex_count, .index_count = triangle_primitive.index_count };
defer triangle_primitive.vertex_data.deinit();
defer triangle_primitive.index_data.deinit();
}
{
const quad_primitive = try primitives.createQuadPrimitive(allocator, 1.4);
primitives_data[1] = PrimitiveRenderData{ .vertex_buffer = createVertexBuffer(quad_primitive), .index_buffer = createIndexBuffer(quad_primitive), .vertex_count = quad_primitive.vertex_count, .index_count = quad_primitive.index_count };
defer quad_primitive.vertex_data.deinit();
defer quad_primitive.index_data.deinit();
}
{
const plane_primitive = try primitives.createPlanePrimitive(allocator, 1000, 1000, 1.5);
primitives_data[2] = PrimitiveRenderData{ .vertex_buffer = createVertexBuffer(plane_primitive), .index_buffer = createIndexBuffer(plane_primitive), .vertex_count = plane_primitive.vertex_count, .index_count = plane_primitive.index_count };
defer plane_primitive.vertex_data.deinit();
defer plane_primitive.index_data.deinit();
}
{
const circle_primitive = try primitives.createCirclePrimitive(allocator, 64, 1);
primitives_data[3] = PrimitiveRenderData{ .vertex_buffer = createVertexBuffer(circle_primitive), .index_buffer = createIndexBuffer(circle_primitive), .vertex_count = circle_primitive.vertex_count, .index_count = circle_primitive.index_count };
defer circle_primitive.vertex_data.deinit();
defer circle_primitive.index_data.deinit();
}
{
const cube_primitive = try primitives.createCubePrimitive(allocator, 0.5);
primitives_data[4] = PrimitiveRenderData{ .vertex_buffer = createVertexBuffer(cube_primitive), .index_buffer = createIndexBuffer(cube_primitive), .vertex_count = cube_primitive.vertex_count, .index_count = cube_primitive.index_count };
defer cube_primitive.vertex_data.deinit();
defer cube_primitive.index_data.deinit();
}
{
const cylinder_primitive = try primitives.createCylinderPrimitive(allocator, 1.0, 1.0, 6);
primitives_data[5] = PrimitiveRenderData{ .vertex_buffer = createVertexBuffer(cylinder_primitive), .index_buffer = createIndexBuffer(cylinder_primitive), .vertex_count = cylinder_primitive.vertex_count, .index_count = cylinder_primitive.index_count };
defer cylinder_primitive.vertex_data.deinit();
defer cylinder_primitive.index_data.deinit();
}
{
const cone_primitive = try primitives.createConePrimitive(allocator, 0.7, 1.0, 15);
primitives_data[6] = PrimitiveRenderData{ .vertex_buffer = createVertexBuffer(cone_primitive), .index_buffer = createIndexBuffer(cone_primitive), .vertex_count = cone_primitive.vertex_count, .index_count = cone_primitive.index_count };
defer cone_primitive.vertex_data.deinit();
defer cone_primitive.index_data.deinit();
}
var bind_group_layout = createBindGroupLayout();
defer bind_group_layout.release();
createBindBuffer(bind_group_layout);
createDepthTexture();
var shader = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl"));
defer shader.release();
pipeline = createPipeline(shader, bind_group_layout);
}
fn createVertexBuffer(primitive: Primitive) *gpu.Buffer {
const vertex_buffer_descriptor = gpu.Buffer.Descriptor{
.size = primitive.vertex_count * @sizeOf(VertexData),
.usage = .{ .vertex = true, .copy_dst = true },
.mapped_at_creation = .false,
};
const vertex_buffer = core.device.createBuffer(&vertex_buffer_descriptor);
queue.writeBuffer(vertex_buffer, 0, primitive.vertex_data.items[0..]);
return vertex_buffer;
}
fn createIndexBuffer(primitive: Primitive) *gpu.Buffer {
const index_buffer_descriptor = gpu.Buffer.Descriptor{
.size = primitive.index_count * @sizeOf(u32),
.usage = .{ .index = true, .copy_dst = true },
.mapped_at_creation = .false,
};
const index_buffer = core.device.createBuffer(&index_buffer_descriptor);
queue.writeBuffer(index_buffer, 0, primitive.index_data.items[0..]);
return index_buffer;
}
fn createBindGroupLayout() *gpu.BindGroupLayout {
const bgle = gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true, .fragment = false }, .uniform, true, 0);
return core.device.createBindGroupLayout(
&gpu.BindGroupLayout.Descriptor.init(.{
.entries = &.{bgle},
}),
);
}
fn createBindBuffer(bind_group_layout: *gpu.BindGroupLayout) void {
uniform_buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .uniform = true },
.size = @sizeOf(UniformBufferObject),
.mapped_at_creation = .false,
});
bind_group = core.device.createBindGroup(
&gpu.BindGroup.Descriptor.init(.{
.layout = bind_group_layout,
.entries = &.{
gpu.BindGroup.Entry.buffer(0, uniform_buffer, 0, @sizeOf(UniformBufferObject)),
},
}),
);
}
fn createDepthTexture() void {
depth_texture = core.device.createTexture(&gpu.Texture.Descriptor{
.usage = .{ .render_attachment = true },
.size = .{ .width = core.descriptor.width, .height = core.descriptor.height },
.format = .depth24_plus,
});
depth_texture_view = depth_texture.createView(&gpu.TextureView.Descriptor{
.format = .depth24_plus,
.dimension = .dimension_2d,
.array_layer_count = 1,
.mip_level_count = 1,
});
}
fn createPipeline(shader_module: *gpu.ShaderModule, bind_group_layout: *gpu.BindGroupLayout) *gpu.RenderPipeline {
const vertex_attributes = [_]gpu.VertexAttribute{
.{ .format = .float32x3, .shader_location = 0, .offset = 0 },
.{ .format = .float32x3, .shader_location = 1, .offset = @sizeOf(primitives.F32x3) },
};
const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{
.array_stride = @sizeOf(VertexData),
.step_mode = .vertex,
.attributes = &vertex_attributes,
});
const vertex_pipeline_state = gpu.VertexState.init(.{ .module = shader_module, .entry_point = "vertex_main", .buffers = &.{vertex_buffer_layout} });
const primitive_pipeline_state = gpu.PrimitiveState{
.topology = .triangle_list,
.front_face = .ccw,
.cull_mode = .back,
};
// Fragment Pipeline State
const blend = gpu.BlendState{
.color = gpu.BlendComponent{ .operation = .add, .src_factor = .src_alpha, .dst_factor = .one_minus_src_alpha },
.alpha = gpu.BlendComponent{ .operation = .add, .src_factor = .zero, .dst_factor = .one },
};
const color_target = gpu.ColorTargetState{
.format = core.descriptor.format,
.blend = &blend,
.write_mask = gpu.ColorWriteMaskFlags.all,
};
const fragment_pipeline_state = gpu.FragmentState.init(.{
.module = shader_module,
.entry_point = "frag_main",
.targets = &.{color_target},
});
const depth_stencil_state = gpu.DepthStencilState{
.format = .depth24_plus,
.depth_write_enabled = .true,
.depth_compare = .less,
};
const multi_sample_state = gpu.MultisampleState{
.count = 1,
.mask = 0xFFFFFFFF,
.alpha_to_coverage_enabled = .false,
};
const bind_group_layouts = [_]*gpu.BindGroupLayout{bind_group_layout};
// Pipeline Layout
const pipeline_layout_descriptor = gpu.PipelineLayout.Descriptor.init(.{
.bind_group_layouts = &bind_group_layouts,
});
const pipeline_layout = core.device.createPipelineLayout(&pipeline_layout_descriptor);
defer pipeline_layout.release();
const pipeline_descriptor = gpu.RenderPipeline.Descriptor{
.label = "Main Pipeline",
.layout = pipeline_layout,
.vertex = vertex_pipeline_state,
.primitive = primitive_pipeline_state,
.depth_stencil = &depth_stencil_state,
.multisample = multi_sample_state,
.fragment = &fragment_pipeline_state,
};
return core.device.createRenderPipeline(&pipeline_descriptor);
}
pub const F32x1 = @Vector(1, f32);
pub fn update() void {
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
const color_attachment = gpu.RenderPassColorAttachment{
.view = back_buffer_view,
.clear_value = gpu.Color{ .r = 0.2, .g = 0.2, .b = 0.2, .a = 1.0 },
.load_op = .clear,
.store_op = .store,
};
const depth_stencil_attachment = gpu.RenderPassDepthStencilAttachment{
.view = depth_texture_view,
.depth_load_op = .clear,
.depth_store_op = .store,
.depth_clear_value = 1.0,
};
const encoder = core.device.createCommandEncoder(null);
const render_pass_info = gpu.RenderPassDescriptor.init(.{
.color_attachments = &.{color_attachment},
.depth_stencil_attachment = &depth_stencil_attachment,
});
if (curr_primitive_index >= 4) {
const time = app_timer.read() / 5;
const model = zm.mul(zm.rotationX(time * (std.math.pi / 2.0)), zm.rotationZ(time * (std.math.pi / 2.0)));
const view = zm.lookAtRh(
zm.Vec{ 0, 4, 2, 1 },
zm.Vec{ 0, 0, 0, 1 },
zm.Vec{ 0, 0, 1, 0 },
);
const proj = zm.perspectiveFovRh(
(std.math.pi / 4.0),
@as(f32, @floatFromInt(core.descriptor.width)) / @as(f32, @floatFromInt(core.descriptor.height)),
0.1,
10,
);
const mvp = zm.mul(zm.mul(model, view), proj);
const ubo = UniformBufferObject{
.mvp_matrix = zm.transpose(mvp),
};
encoder.writeBuffer(uniform_buffer, 0, &[_]UniformBufferObject{ubo});
} else {
const ubo = UniformBufferObject{
.mvp_matrix = zm.identity(),
};
encoder.writeBuffer(uniform_buffer, 0, &[_]UniformBufferObject{ubo});
}
const pass = encoder.beginRenderPass(&render_pass_info);
pass.setPipeline(pipeline);
const vertex_buffer = primitives_data[curr_primitive_index].vertex_buffer;
const vertex_count = primitives_data[curr_primitive_index].vertex_count;
pass.setVertexBuffer(0, vertex_buffer, 0, @sizeOf(VertexData) * vertex_count);
pass.setBindGroup(0, bind_group, &.{0});
const index_buffer = primitives_data[curr_primitive_index].index_buffer;
const index_count = primitives_data[curr_primitive_index].index_count;
pass.setIndexBuffer(index_buffer, .uint32, 0, @sizeOf(u32) * index_count);
pass.drawIndexed(index_count, 1, 0, 0, 0);
pass.end();
pass.release();
var command = encoder.finish(null);
encoder.release();
queue.submit(&[_]*gpu.CommandBuffer{command});
command.release();
core.swap_chain.present();
back_buffer_view.release();
}
pub fn deinit() void {
var i: u4 = 0;
while (i < 7) : (i += 1) {
primitives_data[i].vertex_buffer.release();
primitives_data[i].index_buffer.release();
}
bind_group.release();
uniform_buffer.release();
depth_texture.release();
depth_texture_view.release();
pipeline.release();
}

View file

@ -0,0 +1,33 @@
struct Uniforms {
mvp_matrix : mat4x4<f32>,
};
@binding(0) @group(0) var<uniform> ubo : Uniforms;
struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) normal: vec3<f32>,
};
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) normal: vec3<f32>,
};
@vertex fn vertex_main(in : VertexInput) -> VertexOutput {
var out: VertexOutput;
out.position = vec4<f32>(in.position, 1.0) * ubo.mvp_matrix;
out.normal = in.normal;
return out;
}
struct FragmentOutput {
@location(0) pixel_color: vec4<f32>
};
@fragment fn frag_main(in: VertexOutput) -> FragmentOutput {
var out : FragmentOutput;
out.pixel_color = vec4<f32>((in.normal + 1) / 2, 1.0);
return out;
}

View file

@ -0,0 +1,138 @@
const std = @import("std");
const core = @import("mach").core;
const gpu = core.gpu;
const Vertex = extern struct {
pos: @Vector(2, f32),
col: @Vector(3, f32),
};
const vertices = [_]Vertex{
.{ .pos = .{ -0.5, -0.5 }, .col = .{ 1, 0, 0 } },
.{ .pos = .{ 0.5, -0.5 }, .col = .{ 0, 1, 0 } },
.{ .pos = .{ 0.5, 0.5 }, .col = .{ 0, 0, 1 } },
.{ .pos = .{ -0.5, 0.5 }, .col = .{ 1, 1, 1 } },
};
const index_data = [_]u32{ 0, 1, 2, 2, 3, 0 };
pub const App = @This();
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
title_timer: core.Timer,
pipeline: *gpu.RenderPipeline,
vertex_buffer: *gpu.Buffer,
index_buffer: *gpu.Buffer,
pub fn init(app: *App) !void {
try core.init(.{});
const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl"));
defer shader_module.release();
const vertex_attributes = [_]gpu.VertexAttribute{
.{ .format = .float32x2, .offset = @offsetOf(Vertex, "pos"), .shader_location = 0 },
.{ .format = .float32x3, .offset = @offsetOf(Vertex, "col"), .shader_location = 1 },
};
const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{
.array_stride = @sizeOf(Vertex),
.step_mode = .vertex,
.attributes = &vertex_attributes,
});
const color_target = gpu.ColorTargetState{
.format = core.descriptor.format,
.blend = &.{},
.write_mask = gpu.ColorWriteMaskFlags.all,
};
const fragment = gpu.FragmentState.init(.{
.module = shader_module,
.entry_point = "frag_main",
.targets = &.{color_target},
});
const pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{}));
defer pipeline_layout.release();
const pipeline_descriptor = gpu.RenderPipeline.Descriptor{
.fragment = &fragment,
.layout = pipeline_layout,
.vertex = gpu.VertexState.init(.{
.module = shader_module,
.entry_point = "vertex_main",
.buffers = &.{vertex_buffer_layout},
}),
.primitive = .{ .cull_mode = .back },
};
const vertex_buffer = core.device.createBuffer(&.{
.usage = .{ .vertex = true },
.size = @sizeOf(Vertex) * vertices.len,
.mapped_at_creation = .true,
});
const vertex_mapped = vertex_buffer.getMappedRange(Vertex, 0, vertices.len);
@memcpy(vertex_mapped.?, vertices[0..]);
vertex_buffer.unmap();
const index_buffer = core.device.createBuffer(&.{
.usage = .{ .index = true },
.size = @sizeOf(u32) * index_data.len,
.mapped_at_creation = .true,
});
const index_mapped = index_buffer.getMappedRange(u32, 0, index_data.len);
@memcpy(index_mapped.?, index_data[0..]);
index_buffer.unmap();
app.title_timer = try core.Timer.start();
app.pipeline = core.device.createRenderPipeline(&pipeline_descriptor);
app.vertex_buffer = vertex_buffer;
app.index_buffer = index_buffer;
}
pub fn deinit(app: *App) void {
app.vertex_buffer.release();
app.index_buffer.release();
app.pipeline.release();
core.deinit();
_ = gpa.deinit();
}
pub fn update(app: *App) !bool {
var iter = core.pollEvents();
while (iter.next()) |event| if (event == .close) return true;
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
const encoder = core.device.createCommandEncoder(null);
const color_attachment = gpu.RenderPassColorAttachment{
.view = back_buffer_view,
.clear_value = .{ .r = 0, .g = 0, .b = 0, .a = 1 },
.load_op = .clear,
.store_op = .store,
};
const render_pass_info = gpu.RenderPassDescriptor.init(.{ .color_attachments = &.{color_attachment} });
const pass = encoder.beginRenderPass(&render_pass_info);
pass.setPipeline(app.pipeline);
pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len);
pass.setIndexBuffer(app.index_buffer, .uint32, 0, @sizeOf(u32) * index_data.len);
pass.drawIndexed(index_data.len, 1, 0, 0, 0);
pass.end();
pass.release();
var command = encoder.finish(null);
encoder.release();
core.queue.submit(&.{command});
command.release();
core.swap_chain.present();
back_buffer_view.release();
// update the window title every second
if (app.title_timer.read() >= 1.0) {
app.title_timer.reset();
try core.printTitle("RGB Quad [ {d}fps ] [ Input {d}hz ]", .{
core.frameRate(),
core.inputRate(),
});
}
return false;
}

View file

@ -0,0 +1,15 @@
struct Output {
@builtin(position) pos: vec4<f32>,
@location(0) color: vec3<f32>,
};
@vertex fn vertex_main(@location(0) pos: vec2<f32>, @location(1) color: vec3<f32>) -> Output {
var output: Output;
output.pos = vec4(pos, 0, 1);
output.color = color;
return output;
}
@fragment fn frag_main(@location(0) color: vec3<f32>) -> @location(0) vec4<f32> {
return vec4(color, 1);
}

View file

@ -0,0 +1,49 @@
pub const Vertex = extern struct {
pos: @Vector(4, f32),
col: @Vector(4, f32),
uv: @Vector(2, f32),
};
pub const vertices = [_]Vertex{
.{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } },
};

View file

@ -0,0 +1,192 @@
const std = @import("std");
const core = @import("mach").core;
const gpu = core.gpu;
const zm = @import("zmath");
const Vertex = @import("cube_mesh.zig").Vertex;
const vertices = @import("cube_mesh.zig").vertices;
pub const App = @This();
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const UniformBufferObject = struct {
mat: zm.Mat,
};
title_timer: core.Timer,
timer: core.Timer,
pipeline: *gpu.RenderPipeline,
vertex_buffer: *gpu.Buffer,
uniform_buffer: *gpu.Buffer,
bind_group: *gpu.BindGroup,
pub fn init(app: *App) !void {
try core.init(.{});
const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl"));
const vertex_attributes = [_]gpu.VertexAttribute{
.{ .format = .float32x4, .offset = @offsetOf(Vertex, "pos"), .shader_location = 0 },
.{ .format = .float32x2, .offset = @offsetOf(Vertex, "uv"), .shader_location = 1 },
};
const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{
.array_stride = @sizeOf(Vertex),
.step_mode = .vertex,
.attributes = &vertex_attributes,
});
const blend = gpu.BlendState{};
const color_target = gpu.ColorTargetState{
.format = core.descriptor.format,
.blend = &blend,
.write_mask = gpu.ColorWriteMaskFlags.all,
};
const fragment = gpu.FragmentState.init(.{
.module = shader_module,
.entry_point = "frag_main",
.targets = &.{color_target},
});
const bgle = gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, true, 0);
const bgl = core.device.createBindGroupLayout(
&gpu.BindGroupLayout.Descriptor.init(.{
.entries = &.{bgle},
}),
);
const bind_group_layouts = [_]*gpu.BindGroupLayout{bgl};
const pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{
.bind_group_layouts = &bind_group_layouts,
}));
const pipeline_descriptor = gpu.RenderPipeline.Descriptor{
.fragment = &fragment,
.layout = pipeline_layout,
.vertex = gpu.VertexState.init(.{
.module = shader_module,
.entry_point = "vertex_main",
.buffers = &.{vertex_buffer_layout},
}),
.primitive = .{
.cull_mode = .back,
},
};
const vertex_buffer = core.device.createBuffer(&.{
.usage = .{ .vertex = true },
.size = @sizeOf(Vertex) * vertices.len,
.mapped_at_creation = .true,
});
const vertex_mapped = vertex_buffer.getMappedRange(Vertex, 0, vertices.len);
@memcpy(vertex_mapped.?, vertices[0..]);
vertex_buffer.unmap();
const uniform_buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .uniform = true },
.size = @sizeOf(UniformBufferObject),
.mapped_at_creation = .false,
});
const bind_group = core.device.createBindGroup(
&gpu.BindGroup.Descriptor.init(.{
.layout = bgl,
.entries = &.{
gpu.BindGroup.Entry.buffer(0, uniform_buffer, 0, @sizeOf(UniformBufferObject)),
},
}),
);
app.title_timer = try core.Timer.start();
app.timer = try core.Timer.start();
app.pipeline = core.device.createRenderPipeline(&pipeline_descriptor);
app.vertex_buffer = vertex_buffer;
app.uniform_buffer = uniform_buffer;
app.bind_group = bind_group;
shader_module.release();
pipeline_layout.release();
bgl.release();
}
pub fn deinit(app: *App) void {
defer _ = gpa.deinit();
defer core.deinit();
app.vertex_buffer.release();
app.uniform_buffer.release();
app.bind_group.release();
app.pipeline.release();
}
pub fn update(app: *App) !bool {
var iter = core.pollEvents();
while (iter.next()) |event| {
switch (event) {
.key_press => |ev| {
if (ev.key == .space) return true;
},
.close => return true,
else => {},
}
}
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
const color_attachment = gpu.RenderPassColorAttachment{
.view = back_buffer_view,
.clear_value = std.mem.zeroes(gpu.Color),
.load_op = .clear,
.store_op = .store,
};
const queue = core.queue;
const encoder = core.device.createCommandEncoder(null);
const render_pass_info = gpu.RenderPassDescriptor.init(.{
.color_attachments = &.{color_attachment},
});
{
const time = app.timer.read();
const model = zm.mul(zm.rotationX(time * (std.math.pi / 2.0)), zm.rotationZ(time * (std.math.pi / 2.0)));
const view = zm.lookAtRh(
zm.Vec{ 0, 4, 2, 1 },
zm.Vec{ 0, 0, 0, 1 },
zm.Vec{ 0, 0, 1, 0 },
);
const proj = zm.perspectiveFovRh(
(std.math.pi / 4.0),
@as(f32, @floatFromInt(core.descriptor.width)) / @as(f32, @floatFromInt(core.descriptor.height)),
0.1,
10,
);
const mvp = zm.mul(zm.mul(model, view), proj);
const ubo = UniformBufferObject{
.mat = zm.transpose(mvp),
};
queue.writeBuffer(app.uniform_buffer, 0, &[_]UniformBufferObject{ubo});
}
const pass = encoder.beginRenderPass(&render_pass_info);
pass.setPipeline(app.pipeline);
pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len);
pass.setBindGroup(0, app.bind_group, &.{0});
pass.draw(vertices.len, 1, 0, 0);
pass.end();
pass.release();
var command = encoder.finish(null);
encoder.release();
queue.submit(&[_]*gpu.CommandBuffer{command});
command.release();
core.swap_chain.present();
back_buffer_view.release();
// update the window title every second
if (app.title_timer.read() >= 1.0) {
app.title_timer.reset();
try core.printTitle("Rotating Cube [ {d}fps ] [ Input {d}hz ]", .{
core.frameRate(),
core.inputRate(),
});
}
return false;
}

View file

@ -0,0 +1,24 @@
@group(0) @binding(0) var<uniform> ubo : mat4x4<f32>;
struct VertexOut {
@builtin(position) position_clip : vec4<f32>,
@location(0) fragUV : vec2<f32>,
@location(1) fragPosition: vec4<f32>,
}
@vertex fn vertex_main(
@location(0) position : vec4<f32>,
@location(1) uv: vec2<f32>
) -> VertexOut {
var output : VertexOut;
output.position_clip = position * ubo;
output.fragUV = uv;
output.fragPosition = 0.5 * (position + vec4<f32>(1.0, 1.0, 1.0, 1.0));
return output;
}
@fragment fn frag_main(
@location(0) fragUV: vec2<f32>,
@location(1) fragPosition: vec4<f32>
) -> @location(0) vec4<f32> {
return fragPosition;
}

View file

@ -0,0 +1,354 @@
const std = @import("std");
const core = @import("mach").core;
const gpu = core.gpu;
const zm = @import("zmath");
const zigimg = @import("zigimg");
const assets = @import("assets");
const json = std.json;
pub const App = @This();
const speed = 2.0 * 100.0; // pixels per second
const Vec2 = @Vector(2, f32);
const UniformBufferObject = struct {
mat: zm.Mat,
};
const Sprite = extern struct {
pos: Vec2,
size: Vec2,
world_pos: Vec2,
sheet_size: Vec2,
};
const SpriteFrames = extern struct {
up: Vec2,
down: Vec2,
left: Vec2,
right: Vec2,
};
const JSONFrames = struct {
up: []f32,
down: []f32,
left: []f32,
right: []f32,
};
const JSONSprite = struct {
pos: []f32,
size: []f32,
world_pos: []f32,
is_player: bool = false,
frames: JSONFrames,
};
const SpriteSheet = struct {
width: f32,
height: f32,
};
const JSONData = struct {
sheet: SpriteSheet,
sprites: []JSONSprite,
};
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
title_timer: core.Timer,
timer: core.Timer,
fps_timer: core.Timer,
pipeline: *gpu.RenderPipeline,
uniform_buffer: *gpu.Buffer,
bind_group: *gpu.BindGroup,
sheet: SpriteSheet,
sprites_buffer: *gpu.Buffer,
sprites: std.ArrayList(Sprite),
sprites_frames: std.ArrayList(SpriteFrames),
player_pos: Vec2,
direction: Vec2,
player_sprite_index: usize,
pub fn init(app: *App) !void {
try core.init(.{});
const allocator = gpa.allocator();
const sprites_file = try std.fs.cwd().openFile("../../examples/sprite2d/sprites.json", .{ .mode = .read_only });
defer sprites_file.close();
const file_size = (try sprites_file.stat()).size;
const buffer = try allocator.alloc(u8, file_size);
defer allocator.free(buffer);
try sprites_file.reader().readNoEof(buffer);
const root = try std.json.parseFromSlice(JSONData, allocator, buffer, .{});
defer root.deinit();
app.player_pos = Vec2{ 0, 0 };
app.direction = Vec2{ 0, 0 };
app.sheet = root.value.sheet;
std.log.info("Sheet Dimensions: {} {}", .{ app.sheet.width, app.sheet.height });
app.sprites = std.ArrayList(Sprite).init(allocator);
app.sprites_frames = std.ArrayList(SpriteFrames).init(allocator);
for (root.value.sprites) |sprite| {
std.log.info("Sprite World Position: {} {}", .{ sprite.world_pos[0], sprite.world_pos[1] });
std.log.info("Sprite Texture Position: {} {}", .{ sprite.pos[0], sprite.pos[1] });
std.log.info("Sprite Dimensions: {} {}", .{ sprite.size[0], sprite.size[1] });
if (sprite.is_player) {
app.player_sprite_index = app.sprites.items.len;
}
try app.sprites.append(.{
.pos = Vec2{ sprite.pos[0], sprite.pos[1] },
.size = Vec2{ sprite.size[0], sprite.size[1] },
.world_pos = Vec2{ sprite.world_pos[0], sprite.world_pos[1] },
.sheet_size = Vec2{ app.sheet.width, app.sheet.height },
});
try app.sprites_frames.append(.{ .up = Vec2{ sprite.frames.up[0], sprite.frames.up[1] }, .down = Vec2{ sprite.frames.down[0], sprite.frames.down[1] }, .left = Vec2{ sprite.frames.left[0], sprite.frames.left[1] }, .right = Vec2{ sprite.frames.right[0], sprite.frames.right[1] } });
}
std.log.info("Number of sprites: {}", .{app.sprites.items.len});
const shader_module = core.device.createShaderModuleWGSL("sprite-shader.wgsl", @embedFile("sprite-shader.wgsl"));
const blend = gpu.BlendState{
.color = .{
.operation = .add,
.src_factor = .src_alpha,
.dst_factor = .one_minus_src_alpha,
},
.alpha = .{
.operation = .add,
.src_factor = .one,
.dst_factor = .zero,
},
};
const color_target = gpu.ColorTargetState{
.format = core.descriptor.format,
.blend = &blend,
.write_mask = gpu.ColorWriteMaskFlags.all,
};
const fragment = gpu.FragmentState.init(.{
.module = shader_module,
.entry_point = "frag_main",
.targets = &.{color_target},
});
const pipeline_descriptor = gpu.RenderPipeline.Descriptor{
.fragment = &fragment,
.vertex = gpu.VertexState.init(.{
.module = shader_module,
.entry_point = "vertex_main",
}),
};
const pipeline = core.device.createRenderPipeline(&pipeline_descriptor);
const sprites_buffer = core.device.createBuffer(&.{
.usage = .{ .storage = true, .copy_dst = true },
.size = @sizeOf(Sprite) * app.sprites.items.len,
.mapped_at_creation = .true,
});
const sprites_mapped = sprites_buffer.getMappedRange(Sprite, 0, app.sprites.items.len);
@memcpy(sprites_mapped.?, app.sprites.items[0..]);
sprites_buffer.unmap();
// Create a sampler with linear filtering for smooth interpolation.
const sampler = core.device.createSampler(&.{
.mag_filter = .linear,
.min_filter = .linear,
});
const queue = core.queue;
var img = try zigimg.Image.fromMemory(allocator, assets.sprites_sheet_png);
defer img.deinit();
const img_size = gpu.Extent3D{ .width = @as(u32, @intCast(img.width)), .height = @as(u32, @intCast(img.height)) };
std.log.info("Image Dimensions: {} {}", .{ img.width, img.height });
const texture = core.device.createTexture(&.{
.size = img_size,
.format = .rgba8_unorm,
.usage = .{
.texture_binding = true,
.copy_dst = true,
.render_attachment = true,
},
});
const data_layout = gpu.Texture.DataLayout{
.bytes_per_row = @as(u32, @intCast(img.width * 4)),
.rows_per_image = @as(u32, @intCast(img.height)),
};
switch (img.pixels) {
.rgba32 => |pixels| queue.writeTexture(&.{ .texture = texture }, &data_layout, &img_size, pixels),
.rgb24 => |pixels| {
const data = try rgb24ToRgba32(allocator, pixels);
defer data.deinit(allocator);
queue.writeTexture(&.{ .texture = texture }, &data_layout, &img_size, data.rgba32);
},
else => @panic("unsupported image color format"),
}
const uniform_buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .uniform = true },
.size = @sizeOf(UniformBufferObject),
.mapped_at_creation = .false,
});
const texture_view = texture.createView(&gpu.TextureView.Descriptor{});
texture.release();
const bind_group_layout = pipeline.getBindGroupLayout(0);
const bind_group = core.device.createBindGroup(
&gpu.BindGroup.Descriptor.init(.{
.layout = bind_group_layout,
.entries = &.{
gpu.BindGroup.Entry.buffer(0, uniform_buffer, 0, @sizeOf(UniformBufferObject)),
gpu.BindGroup.Entry.sampler(1, sampler),
gpu.BindGroup.Entry.textureView(2, texture_view),
gpu.BindGroup.Entry.buffer(3, sprites_buffer, 0, @sizeOf(Sprite) * app.sprites.items.len),
},
}),
);
texture_view.release();
sampler.release();
bind_group_layout.release();
app.title_timer = try core.Timer.start();
app.timer = try core.Timer.start();
app.fps_timer = try core.Timer.start();
app.pipeline = pipeline;
app.uniform_buffer = uniform_buffer;
app.bind_group = bind_group;
app.sprites_buffer = sprites_buffer;
shader_module.release();
}
pub fn deinit(app: *App) void {
defer _ = gpa.deinit();
defer core.deinit();
app.pipeline.release();
app.sprites.deinit();
app.sprites_frames.deinit();
app.uniform_buffer.release();
app.bind_group.release();
app.sprites_buffer.release();
}
pub fn update(app: *App) !bool {
// Handle input by determining the direction the player wants to go.
var iter = core.pollEvents();
while (iter.next()) |event| {
switch (event) {
.key_press => |ev| {
switch (ev.key) {
.space => return true,
.left => app.direction[0] += 1,
.right => app.direction[0] -= 1,
.up => app.direction[1] += 1,
.down => app.direction[1] -= 1,
else => {},
}
},
.key_release => |ev| {
switch (ev.key) {
.left => app.direction[0] -= 1,
.right => app.direction[0] += 1,
.up => app.direction[1] -= 1,
.down => app.direction[1] += 1,
else => {},
}
},
.close => return true,
else => {},
}
}
// Calculate the player position, by moving in the direction the player wants to go
// by the speed amount. Multiply by delta_time to ensure that movement is the same speed
// regardless of the frame rate.
const delta_time = app.fps_timer.lap();
app.player_pos += app.direction * Vec2{ speed, speed } * Vec2{ delta_time, delta_time };
// Render the frame
try app.render();
// update the window title every second
if (app.title_timer.read() >= 1.0) {
app.title_timer.reset();
try core.printTitle("Sprite2D [ {d}fps ] [ Input {d}hz ]", .{
core.frameRate(),
core.inputRate(),
});
}
return false;
}
fn render(app: *App) !void {
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
const color_attachment = gpu.RenderPassColorAttachment{
.view = back_buffer_view,
// sky blue background color:
.clear_value = .{ .r = 0.52, .g = 0.8, .b = 0.92, .a = 1.0 },
.load_op = .clear,
.store_op = .store,
};
const encoder = core.device.createCommandEncoder(null);
const render_pass_info = gpu.RenderPassDescriptor.init(.{
.color_attachments = &.{color_attachment},
});
const player_sprite = &app.sprites.items[app.player_sprite_index];
const player_sprite_frame = &app.sprites_frames.items[app.player_sprite_index];
if (app.direction[0] == -1.0) {
player_sprite.pos = player_sprite_frame.left;
} else if (app.direction[0] == 1.0) {
player_sprite.pos = player_sprite_frame.right;
} else if (app.direction[1] == -1.0) {
player_sprite.pos = player_sprite_frame.down;
} else if (app.direction[1] == 1.0) {
player_sprite.pos = player_sprite_frame.up;
}
player_sprite.world_pos = app.player_pos;
// One pixel in our scene will equal one window pixel (i.e. be roughly the same size
// irrespective of whether the user has a Retina/HDPI display.)
const proj = zm.orthographicRh(
@as(f32, @floatFromInt(core.size().width)),
@as(f32, @floatFromInt(core.size().height)),
0.1,
1000,
);
const view = zm.lookAtRh(
zm.Vec{ 0, 1000, 0, 1 },
zm.Vec{ 0, 0, 0, 1 },
zm.Vec{ 0, 0, 1, 0 },
);
const mvp = zm.mul(view, proj);
const ubo = UniformBufferObject{
.mat = zm.transpose(mvp),
};
// Pass the latest uniform values & sprite values to the shader program.
encoder.writeBuffer(app.uniform_buffer, 0, &[_]UniformBufferObject{ubo});
encoder.writeBuffer(app.sprites_buffer, 0, app.sprites.items);
// Draw the sprite batch
const total_vertices = @as(u32, @intCast(app.sprites.items.len * 6));
const pass = encoder.beginRenderPass(&render_pass_info);
pass.setPipeline(app.pipeline);
pass.setBindGroup(0, app.bind_group, &.{});
pass.draw(total_vertices, 1, 0, 0);
pass.end();
pass.release();
// Submit the frame.
var command = encoder.finish(null);
encoder.release();
const queue = core.queue;
queue.submit(&[_]*gpu.CommandBuffer{command});
command.release();
core.swap_chain.present();
back_buffer_view.release();
}
fn rgb24ToRgba32(allocator: std.mem.Allocator, in: []zigimg.color.Rgb24) !zigimg.color.PixelStorage {
const out = try zigimg.color.PixelStorage.init(allocator, .rgba32, in.len);
var i: usize = 0;
while (i < in.len) : (i += 1) {
out.rgba32[i] = zigimg.color.Rgba32{ .r = in[i].r, .g = in[i].g, .b = in[i].b, .a = 255 };
}
return out;
}

View file

@ -0,0 +1,82 @@
struct Uniforms {
modelViewProjectionMatrix : mat4x4<f32>,
};
@binding(0) @group(0) var<uniform> uniforms : Uniforms;
struct VertexOutput {
@builtin(position) Position : vec4<f32>,
@location(0) fragUV : vec2<f32>,
@location(1) spriteIndex : f32,
};
struct Sprite {
pos: vec2<f32>,
size: vec2<f32>,
world_pos: vec2<f32>,
sheet_size: vec2<f32>,
};
@binding(3) @group(0) var<storage, read> sprites: array<Sprite>;
@vertex
fn vertex_main(
@builtin(vertex_index) VertexIndex : u32
) -> VertexOutput {
var sprite = sprites[VertexIndex / 6];
// Calculate the vertex position
var positions = array<vec2<f32>, 6>(
vec2<f32>(0.0, 0.0), // bottom-left
vec2<f32>(0.0, 1.0), // top-left
vec2<f32>(1.0, 0.0), // bottom-right
vec2<f32>(1.0, 0.0), // bottom-right
vec2<f32>(0.0, 1.0), // top-left
vec2<f32>(1.0, 1.0), // top-right
);
var pos = positions[VertexIndex % 6];
pos.x *= sprite.size.x;
pos.y *= sprite.size.y;
pos.x += sprite.world_pos.x;
pos.y += sprite.world_pos.y;
// Calculate the UV coordinate
var uvs = array<vec2<f32>, 6>(
vec2<f32>(0.0, 1.0), // bottom-left
vec2<f32>(0.0, 0.0), // top-left
vec2<f32>(1.0, 1.0), // bottom-right
vec2<f32>(1.0, 1.0), // bottom-right
vec2<f32>(0.0, 0.0), // top-left
vec2<f32>(1.0, 0.0), // top-right
);
var uv = uvs[VertexIndex % 6];
uv.x *= sprite.size.x / sprite.sheet_size.x;
uv.y *= sprite.size.y / sprite.sheet_size.y;
uv.x += sprite.pos.x / sprite.sheet_size.x;
uv.y += sprite.pos.y / sprite.sheet_size.y;
var output : VertexOutput;
output.Position = vec4<f32>(pos.x, 0.0, pos.y, 1.0) * uniforms.modelViewProjectionMatrix;
output.fragUV = uv;
output.spriteIndex = f32(VertexIndex / 6);
return output;
}
@group(0) @binding(1) var spriteSampler: sampler;
@group(0) @binding(2) var spriteTexture: texture_2d<f32>;
@fragment
fn frag_main(
@location(0) fragUV: vec2<f32>,
@location(1) spriteIndex: f32
) -> @location(0) vec4<f32> {
var color = textureSample(spriteTexture, spriteSampler, fragUV);
if (spriteIndex == 0.0) {
if (color[3] > 0.0) {
color[0] = 0.3;
color[1] = 0.2;
color[2] = 0.5;
color[3] = 1.0;
}
}
return color;
}

View file

@ -0,0 +1,53 @@
{
"sheet": {
"width": 352.0,
"height": 32.0
},
"sprites": [
{
"pos": [ 0.0, 0.0 ],
"size": [ 32.0, 32.0 ],
"world_pos": [ 0.0, 0.0 ],
"is_player": true,
"frames": {
"down": [ 0.0, 0.0 ],
"left": [ 32.0, 0.0 ],
"right": [ 64.0, 0.0 ],
"up": [ 96.0, 0.0 ]
}
},
{
"pos": [ 128.0, 0.0 ],
"size": [ 32.0, 32.0 ],
"world_pos": [ 32.0, 32.0 ],
"frames": {
"down": [ 128.0, 0.0 ],
"left": [ 160.0, 0.0 ],
"right": [ 192.0, 0.0 ],
"up": [ 224.0, 0.0 ]
}
},
{
"pos": [ 128.0, 0.0 ],
"size": [ 32.0, 32.0 ],
"world_pos": [ 64.0, 64.0 ],
"frames": {
"down": [ 0.0, 0.0 ],
"left": [ 0.0, 0.0 ],
"right": [ 0.0, 0.0 ],
"up": [ 0.0, 0.0 ]
}
},
{
"pos": [ 256.0, 0.0 ],
"size": [ 32.0, 32.0 ],
"world_pos": [ 96.0, 96.0 ],
"frames": {
"down": [ 0.0, 0.0 ],
"left": [ 0.0, 0.0 ],
"right": [ 0.0, 0.0 ],
"up": [ 0.0, 0.0 ]
}
}
]
}

View file

@ -0,0 +1,268 @@
/// A port of Austin Eng's "computeBoids" webgpu sample.
/// https://github.com/austinEng/webgpu-samples/blob/main/src/sample/computeBoids/main.ts
const std = @import("std");
const core = @import("mach").core;
const gpu = core.gpu;
title_timer: core.Timer,
timer: core.Timer,
compute_pipeline: *gpu.ComputePipeline,
render_pipeline: *gpu.RenderPipeline,
sprite_vertex_buffer: *gpu.Buffer,
particle_buffers: [2]*gpu.Buffer,
particle_bind_groups: [2]*gpu.BindGroup,
sim_param_buffer: *gpu.Buffer,
frame_counter: usize,
pub const App = @This();
pub const mach_core_options = core.ComptimeOptions{
.use_wgpu = false,
.use_sysgpu = true,
};
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const num_particle = 1500;
var sim_params = [_]f32{
0.04, // .delta_T
0.1, // .rule_1_distance
0.025, // .rule_2_distance
0.025, // .rule_3_distance
0.02, // .rule_1_scale
0.05, // .rule_2_scale
0.005, // .rule_3_scale
};
pub fn init(app: *App) !void {
try core.init(.{});
const sprite_shader_module = core.device.createShaderModuleWGSL(
"sprite.wgsl",
@embedFile("sprite.wgsl"),
);
defer sprite_shader_module.release();
const update_sprite_shader_module = core.device.createShaderModuleWGSL(
"updateSprites.wgsl",
@embedFile("updateSprites.wgsl"),
);
defer update_sprite_shader_module.release();
const instanced_particles_attributes = [_]gpu.VertexAttribute{
.{
// instance position
.shader_location = 0,
.offset = 0,
.format = .float32x2,
},
.{
// instance velocity
.shader_location = 1,
.offset = 2 * 4,
.format = .float32x2,
},
};
const vertex_buffer_attributes = [_]gpu.VertexAttribute{
.{
// vertex positions
.shader_location = 2,
.offset = 0,
.format = .float32x2,
},
};
const render_pipeline = core.device.createRenderPipeline(&gpu.RenderPipeline.Descriptor{
.vertex = gpu.VertexState.init(.{
.module = sprite_shader_module,
.entry_point = "vert_main",
.buffers = &.{
gpu.VertexBufferLayout.init(.{
// instanced particles buffer
.array_stride = 4 * 4,
.step_mode = .instance,
.attributes = &instanced_particles_attributes,
}),
gpu.VertexBufferLayout.init(.{
// vertex buffer
.array_stride = 2 * 4,
.step_mode = .vertex,
.attributes = &vertex_buffer_attributes,
}),
},
}),
.fragment = &gpu.FragmentState.init(.{
.module = sprite_shader_module,
.entry_point = "frag_main",
.targets = &[_]gpu.ColorTargetState{.{
.format = core.descriptor.format,
}},
}),
});
const compute_pipeline = core.device.createComputePipeline(&gpu.ComputePipeline.Descriptor{ .compute = gpu.ProgrammableStageDescriptor{
.module = update_sprite_shader_module,
.entry_point = "main",
} });
const vert_buffer_data = [_]f32{
-0.01, -0.02, 0.01,
-0.02, 0.0, 0.02,
};
const sprite_vertex_buffer = core.device.createBuffer(&gpu.Buffer.Descriptor{
.label = "sprite_vertex_buffer",
.usage = .{ .vertex = true },
.mapped_at_creation = .true,
.size = vert_buffer_data.len * @sizeOf(f32),
});
const vertex_mapped = sprite_vertex_buffer.getMappedRange(f32, 0, vert_buffer_data.len);
@memcpy(vertex_mapped.?, vert_buffer_data[0..]);
sprite_vertex_buffer.unmap();
const sim_param_buffer = core.device.createBuffer(&gpu.Buffer.Descriptor{
.label = "sim_param_buffer",
.usage = .{ .uniform = true, .copy_dst = true },
.size = sim_params.len * @sizeOf(f32),
});
core.queue.writeBuffer(sim_param_buffer, 0, sim_params[0..]);
var initial_particle_data: [num_particle * 4]f32 = undefined;
var rng = std.rand.DefaultPrng.init(0);
const random = rng.random();
var i: usize = 0;
while (i < num_particle) : (i += 1) {
initial_particle_data[4 * i + 0] = 2 * (random.float(f32) - 0.5);
initial_particle_data[4 * i + 1] = 2 * (random.float(f32) - 0.5);
initial_particle_data[4 * i + 2] = 2 * (random.float(f32) - 0.5) * 0.1;
initial_particle_data[4 * i + 3] = 2 * (random.float(f32) - 0.5) * 0.1;
}
var particle_buffers: [2]*gpu.Buffer = undefined;
var particle_bind_groups: [2]*gpu.BindGroup = undefined;
i = 0;
while (i < 2) : (i += 1) {
particle_buffers[i] = core.device.createBuffer(&gpu.Buffer.Descriptor{
.label = "particle_buffer",
.mapped_at_creation = .true,
.usage = .{
.vertex = true,
.storage = true,
},
.size = initial_particle_data.len * @sizeOf(f32),
});
const mapped = particle_buffers[i].getMappedRange(f32, 0, initial_particle_data.len);
@memcpy(mapped.?, initial_particle_data[0..]);
particle_buffers[i].unmap();
}
i = 0;
while (i < 2) : (i += 1) {
const layout = compute_pipeline.getBindGroupLayout(0);
defer layout.release();
particle_bind_groups[i] = core.device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{
.layout = layout,
.entries = &.{
gpu.BindGroup.Entry.buffer(0, sim_param_buffer, 0, sim_params.len * @sizeOf(f32), sim_params.len * @sizeOf(f32)),
gpu.BindGroup.Entry.buffer(1, particle_buffers[i], 0, initial_particle_data.len * @sizeOf(f32), 4 * @sizeOf(f32)),
gpu.BindGroup.Entry.buffer(2, particle_buffers[(i + 1) % 2], 0, initial_particle_data.len * @sizeOf(f32), 4 * @sizeOf(f32)),
},
}));
}
app.* = .{
.timer = try core.Timer.start(),
.title_timer = try core.Timer.start(),
.compute_pipeline = compute_pipeline,
.render_pipeline = render_pipeline,
.sprite_vertex_buffer = sprite_vertex_buffer,
.particle_buffers = particle_buffers,
.particle_bind_groups = particle_bind_groups,
.sim_param_buffer = sim_param_buffer,
.frame_counter = 0,
};
}
pub fn deinit(app: *App) void {
defer _ = gpa.deinit();
defer core.deinit();
app.compute_pipeline.release();
app.render_pipeline.release();
app.sprite_vertex_buffer.release();
for (app.particle_buffers) |particle_buffer| particle_buffer.release();
for (app.particle_bind_groups) |particle_bind_group| particle_bind_group.release();
app.sim_param_buffer.release();
}
pub fn update(app: *App) !bool {
const delta_time = app.timer.lap();
var iter = core.pollEvents();
while (iter.next()) |event| {
if (event == .close) return true;
}
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
const color_attachment = gpu.RenderPassColorAttachment{
.view = back_buffer_view,
.clear_value = std.mem.zeroes(gpu.Color),
.load_op = .clear,
.store_op = .store,
};
const render_pass_descriptor = gpu.RenderPassDescriptor.init(.{
.color_attachments = &.{
color_attachment,
},
});
sim_params[0] = @as(f32, @floatCast(delta_time));
core.queue.writeBuffer(app.sim_param_buffer, 0, sim_params[0..]);
const command_encoder = core.device.createCommandEncoder(null);
{
const pass_encoder = command_encoder.beginComputePass(null);
pass_encoder.setPipeline(app.compute_pipeline);
pass_encoder.setBindGroup(0, app.particle_bind_groups[app.frame_counter % 2], null);
pass_encoder.dispatchWorkgroups(@as(u32, @intFromFloat(@ceil(@as(f32, num_particle) / 64))), 1, 1);
pass_encoder.end();
pass_encoder.release();
}
{
const pass_encoder = command_encoder.beginRenderPass(&render_pass_descriptor);
pass_encoder.setPipeline(app.render_pipeline);
pass_encoder.setVertexBuffer(0, app.particle_buffers[(app.frame_counter + 1) % 2], 0, num_particle * 4 * @sizeOf(f32));
pass_encoder.setVertexBuffer(1, app.sprite_vertex_buffer, 0, 6 * @sizeOf(f32));
pass_encoder.draw(3, num_particle, 0, 0);
pass_encoder.end();
pass_encoder.release();
}
app.frame_counter += 1;
if (app.frame_counter % 60 == 0) {
std.log.info("Frame {}", .{app.frame_counter});
}
var command = command_encoder.finish(null);
command_encoder.release();
core.queue.submit(&[_]*gpu.CommandBuffer{command});
command.release();
core.swap_chain.present();
back_buffer_view.release();
// update the window title every second
if (app.title_timer.read() >= 1.0) {
app.title_timer.reset();
try core.printTitle("Boids [ {d}fps ] [ Input {d}hz ]", .{
core.frameRate(),
core.inputRate(),
});
}
return false;
}

View file

@ -0,0 +1,15 @@
@vertex
fn vert_main(@location(0) a_particlePos : vec2<f32>,
@location(1) a_particleVel : vec2<f32>,
@location(2) a_pos : vec2<f32>) -> @builtin(position) vec4<f32> {
let angle = -atan2(a_particleVel.x, a_particleVel.y);
let pos = vec2<f32>(
(a_pos.x * cos(angle)) - (a_pos.y * sin(angle)),
(a_pos.x * sin(angle)) + (a_pos.y * cos(angle)));
return vec4<f32>(pos + a_particlePos, 0.0, 1.0);
}
@fragment
fn frag_main() -> @location(0) vec4<f32> {
return vec4<f32>(1.0, 1.0, 1.0, 1.0);
}

View file

@ -0,0 +1,90 @@
struct Particle {
pos : vec2<f32>,
vel : vec2<f32>,
};
struct SimParams {
deltaT : f32,
rule1Distance : f32,
rule2Distance : f32,
rule3Distance : f32,
rule1Scale : f32,
rule2Scale : f32,
rule3Scale : f32,
};
struct Particles {
particles : array<Particle>,
};
@binding(0) @group(0) var<uniform> params : SimParams;
@binding(1) @group(0) var<storage, read> particlesA : Particles;
@binding(2) @group(0) var<storage, read_write> particlesB : Particles;
// https://github.com/austinEng/Project6-Vulkan-Flocking/blob/master/data/shaders/computeparticles/particle.comp
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) GlobalInvocationID : vec3<u32>) {
var index : u32 = GlobalInvocationID.x;
if (index >= arrayLength(&particlesA.particles)) {
return;
}
var vPos = particlesA.particles[index].pos;
var vVel = particlesA.particles[index].vel;
var cMass = vec2<f32>(0.0, 0.0);
var cVel = vec2<f32>(0.0, 0.0);
var colVel = vec2<f32>(0.0, 0.0);
var cMassCount : u32 = 0u;
var cVelCount : u32 = 0u;
var pos : vec2<f32>;
var vel : vec2<f32>;
for (var i : u32 = 0u; i < arrayLength(&particlesA.particles); i = i + 1u) {
if (i == index) {
continue;
}
pos = particlesA.particles[i].pos.xy;
vel = particlesA.particles[i].vel.xy;
if (distance(pos, vPos) < params.rule1Distance) {
cMass = cMass + pos;
cMassCount = cMassCount + 1u;
}
if (distance(pos, vPos) < params.rule2Distance) {
colVel = colVel - (pos - vPos);
}
if (distance(pos, vPos) < params.rule3Distance) {
cVel = cVel + vel;
cVelCount = cVelCount + 1u;
}
}
if (cMassCount > 0u) {
var temp = f32(cMassCount);
cMass = (cMass / vec2<f32>(temp, temp)) - vPos;
}
if (cVelCount > 0u) {
var temp = f32(cVelCount);
cVel = cVel / vec2<f32>(temp, temp);
}
vVel = vVel + (cMass * params.rule1Scale) + (colVel * params.rule2Scale) +
(cVel * params.rule3Scale);
// clamp velocity for a more pleasing simulation
vVel = normalize(vVel) * clamp(length(vVel), 0.0, 0.1);
// kinematic update
vPos = vPos + (vVel * params.deltaT);
// Wrap around boundary
if (vPos.x < -1.0) {
vPos.x = 1.0;
}
if (vPos.x > 1.0) {
vPos.x = -1.0;
}
if (vPos.y < -1.0) {
vPos.y = 1.0;
}
if (vPos.y > 1.0) {
vPos.y = -1.0;
}
// Write back
particlesB.particles[index].pos = vPos;
particlesB.particles[index].vel = vVel;
}

View file

@ -0,0 +1,83 @@
const std = @import("std");
const core = @import("mach").core;
const gpu = core.gpu;
const renderer = @import("renderer.zig");
pub const App = @This();
pub const mach_core_options = core.ComptimeOptions{
.use_wgpu = false,
.use_sysgpu = true,
};
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
title_timer: core.Timer,
pub fn init(app: *App) !void {
try core.init(.{});
app.* = .{
.title_timer = try core.Timer.start(),
};
}
pub fn deinit(app: *App) void {
_ = app;
defer _ = gpa.deinit();
defer core.deinit();
}
pub fn update(app: *App) !bool {
var iter = core.pollEvents();
while (iter.next()) |event| {
switch (event) {
.key_press => |ev| {
if (ev.key == .space) return true;
},
.close => return true,
else => {},
}
}
app.render();
// update the window title every second
if (app.title_timer.read() >= 1.0) {
app.title_timer.reset();
try core.printTitle("Clear Color [ {d}fps ] [ Input {d}hz ]", .{
core.frameRate(),
core.inputRate(),
});
}
return false;
}
fn render(app: *App) void {
_ = app;
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
const color_attachment = gpu.RenderPassColorAttachment{
.view = back_buffer_view,
.clear_value = gpu.Color{ .r = 0.0, .g = 0.0, .b = 1.0, .a = 1.0 },
.load_op = .clear,
.store_op = .store,
};
const encoder = core.device.createCommandEncoder(null);
const render_pass_info = gpu.RenderPassDescriptor.init(.{
.color_attachments = &.{color_attachment},
});
const pass = encoder.beginRenderPass(&render_pass_info);
pass.end();
pass.release();
var command = encoder.finish(null);
encoder.release();
var queue = core.queue;
queue.submit(&[_]*gpu.CommandBuffer{command});
command.release();
core.swap_chain.present();
back_buffer_view.release();
}

View file

@ -0,0 +1,32 @@
const core = @import("mach").core;
const gpu = core.gpu;
pub const Renderer = @This();
pub fn RenderUpdate() void {
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
const color_attachment = gpu.RenderPassColorAttachment{
.view = back_buffer_view,
.clear_value = gpu.Color{ .r = 0.0, .g = 0.0, .b = 1.0, .a = 1.0 },
.load_op = .clear,
.store_op = .store,
};
const encoder = core.device.createCommandEncoder(null);
const render_pass_info = gpu.RenderPassDescriptor.init(.{
.color_attachments = &.{color_attachment},
});
const pass = encoder.beginRenderPass(&render_pass_info);
pass.end();
pass.release();
var command = encoder.finish(null);
encoder.release();
const queue = core.queue;
queue.submit(&[_]*gpu.CommandBuffer{command});
command.release();
core.swap_chain.present();
back_buffer_view.release();
}

View file

@ -0,0 +1,49 @@
pub const Vertex = extern struct {
pos: @Vector(4, f32),
col: @Vector(4, f32),
uv: @Vector(2, f32),
};
pub const vertices = [_]Vertex{
.{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } },
};

View file

@ -0,0 +1,397 @@
const std = @import("std");
const core = @import("mach").core;
const gpu = core.gpu;
const zm = @import("zmath");
const zigimg = @import("zigimg");
const Vertex = @import("cube_mesh.zig").Vertex;
const vertices = @import("cube_mesh.zig").vertices;
const assets = @import("assets");
const UniformBufferObject = struct {
mat: zm.Mat,
};
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
title_timer: core.Timer,
timer: core.Timer,
pipeline: *gpu.RenderPipeline,
vertex_buffer: *gpu.Buffer,
uniform_buffer: *gpu.Buffer,
bind_group: *gpu.BindGroup,
depth_texture: *gpu.Texture,
depth_texture_view: *gpu.TextureView,
pub const App = @This();
pub const mach_core_options = core.ComptimeOptions{
.use_wgpu = false,
.use_sysgpu = true,
};
pub fn init(app: *App) !void {
try core.init(.{});
const allocator = gpa.allocator();
const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl"));
defer shader_module.release();
const vertex_attributes = [_]gpu.VertexAttribute{
.{ .format = .float32x4, .offset = @offsetOf(Vertex, "pos"), .shader_location = 0 },
.{ .format = .float32x2, .offset = @offsetOf(Vertex, "uv"), .shader_location = 1 },
};
const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{
.array_stride = @sizeOf(Vertex),
.step_mode = .vertex,
.attributes = &vertex_attributes,
});
const blend = gpu.BlendState{
.color = .{
.operation = .add,
.src_factor = .src_alpha,
.dst_factor = .one_minus_src_alpha,
},
.alpha = .{
.operation = .add,
.src_factor = .one,
.dst_factor = .zero,
},
};
const color_target = gpu.ColorTargetState{
.format = core.descriptor.format,
.blend = &blend,
.write_mask = gpu.ColorWriteMaskFlags.all,
};
const fragment = gpu.FragmentState.init(.{
.module = shader_module,
.entry_point = "frag_main",
.targets = &.{color_target},
});
const pipeline_descriptor = gpu.RenderPipeline.Descriptor{
.fragment = &fragment,
// Enable depth testing so that the fragment closest to the camera
// is rendered in front.
.depth_stencil = &.{
.format = .depth24_plus,
.depth_write_enabled = .true,
.depth_compare = .less,
},
.vertex = gpu.VertexState.init(.{
.module = shader_module,
.entry_point = "vertex_main",
.buffers = &.{vertex_buffer_layout},
}),
.primitive = .{
// Since the cube has its face pointing outwards, cull_mode must be
// set to .front or .none here since we are inside the cube looking out.
// Ideally you would set this to .back and have a custom cube primitive
// with the faces pointing towards the inside of the cube.
.cull_mode = .none,
},
};
const pipeline = core.device.createRenderPipeline(&pipeline_descriptor);
const vertex_buffer = core.device.createBuffer(&.{
.usage = .{ .vertex = true },
.size = @sizeOf(Vertex) * vertices.len,
.mapped_at_creation = .true,
});
const vertex_mapped = vertex_buffer.getMappedRange(Vertex, 0, vertices.len);
@memcpy(vertex_mapped.?, vertices[0..]);
vertex_buffer.unmap();
const uniform_buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .uniform = true },
.size = @sizeOf(UniformBufferObject),
.mapped_at_creation = .false,
});
// Create a sampler with linear filtering for smooth interpolation.
const sampler = core.device.createSampler(&.{
.mag_filter = .linear,
.min_filter = .linear,
});
const queue = core.queue;
// WebGPU expects the cubemap textures in this order: (+X,-X,+Y,-Y,+Z,-Z)
var images: [6]zigimg.Image = undefined;
images[0] = try zigimg.Image.fromMemory(allocator, assets.skybox_posx_png);
defer images[0].deinit();
images[1] = try zigimg.Image.fromMemory(allocator, assets.skybox_negx_png);
defer images[1].deinit();
images[2] = try zigimg.Image.fromMemory(allocator, assets.skybox_posy_png);
defer images[2].deinit();
images[3] = try zigimg.Image.fromMemory(allocator, assets.skybox_negy_png);
defer images[3].deinit();
images[4] = try zigimg.Image.fromMemory(allocator, assets.skybox_posz_png);
defer images[4].deinit();
images[5] = try zigimg.Image.fromMemory(allocator, assets.skybox_negz_png);
defer images[5].deinit();
// Use the first image of the set for sizing
const img_size = gpu.Extent3D{
.width = @as(u32, @intCast(images[0].width)),
.height = @as(u32, @intCast(images[0].height)),
};
// We set depth_or_array_layers to 6 here to indicate there are 6 images in this texture
const tex_size = gpu.Extent3D{
.width = @as(u32, @intCast(images[0].width)),
.height = @as(u32, @intCast(images[0].height)),
.depth_or_array_layers = 6,
};
// Same as a regular texture, but with a Z of 6 (defined in tex_size)
const cube_texture = core.device.createTexture(&.{
.size = tex_size,
.format = .rgba8_unorm,
.dimension = .dimension_2d,
.usage = .{
.texture_binding = true,
.copy_dst = true,
.render_attachment = false,
},
});
const data_layout = gpu.Texture.DataLayout{
.bytes_per_row = @as(u32, @intCast(images[0].width * 4)),
.rows_per_image = @as(u32, @intCast(images[0].height)),
};
const encoder = core.device.createCommandEncoder(null);
// We have to create a staging buffer, copy all the image data into the
// staging buffer at the correct Z offset, encode a command to copy
// the buffer to the texture for each image, then push it to the command
// queue
var staging_buff: [6]*gpu.Buffer = undefined;
var i: u32 = 0;
while (i < 6) : (i += 1) {
staging_buff[i] = core.device.createBuffer(&.{
.usage = .{ .copy_src = true, .map_write = true },
.size = @as(u64, @intCast(images[0].width)) * @as(u64, @intCast(images[0].height)) * @sizeOf(u32),
.mapped_at_creation = .true,
});
switch (images[i].pixels) {
.rgba32 => |pixels| {
// Map a section of the staging buffer
const staging_map = staging_buff[i].getMappedRange(u32, 0, @as(u64, @intCast(images[i].width)) * @as(u64, @intCast(images[i].height)));
// Copy the image data into the mapped buffer
@memcpy(staging_map.?, @as([]u32, @ptrCast(@alignCast(pixels))));
// And release the mapping
staging_buff[i].unmap();
},
.rgb24 => |pixels| {
const staging_map = staging_buff[i].getMappedRange(u32, 0, @as(u64, @intCast(images[i].width)) * @as(u64, @intCast(images[i].height)));
// In this case, we have to convert the data to rgba32 first
const data = try rgb24ToRgba32(allocator, pixels);
defer data.deinit(allocator);
@memcpy(staging_map.?, @as([]u32, @ptrCast(@alignCast(data.rgba32))));
staging_buff[i].unmap();
},
else => @panic("unsupported image color format"),
}
// These define the source and target for the buffer to texture copy command
const copy_buff = gpu.ImageCopyBuffer{
.layout = data_layout,
.buffer = staging_buff[i],
};
const copy_tex = gpu.ImageCopyTexture{
.texture = cube_texture,
.origin = gpu.Origin3D{ .x = 0, .y = 0, .z = i },
};
// Encode the copy command, we do this for every image in the texture.
encoder.copyBufferToTexture(&copy_buff, &copy_tex, &img_size);
staging_buff[i].release();
}
// Now that the commands to copy our buffer data to the texture is filled,
// push the encoded commands over to the queue and execute to get the
// texture filled with the image data.
var command = encoder.finish(null);
encoder.release();
queue.submit(&[_]*gpu.CommandBuffer{command});
command.release();
// The textureView in the bind group needs dimension defined as "dimension_cube".
const cube_texture_view = cube_texture.createView(&gpu.TextureView.Descriptor{ .dimension = .dimension_cube });
cube_texture.release();
const bind_group_layout = pipeline.getBindGroupLayout(0);
const bind_group = core.device.createBindGroup(
&gpu.BindGroup.Descriptor.init(.{
.layout = bind_group_layout,
.entries = &.{
gpu.BindGroup.Entry.buffer(0, uniform_buffer, 0, @sizeOf(UniformBufferObject), @sizeOf(UniformBufferObject)),
gpu.BindGroup.Entry.sampler(1, sampler),
gpu.BindGroup.Entry.textureView(2, cube_texture_view),
},
}),
);
sampler.release();
cube_texture_view.release();
bind_group_layout.release();
const depth_texture = core.device.createTexture(&gpu.Texture.Descriptor{
.size = gpu.Extent3D{
.width = core.descriptor.width,
.height = core.descriptor.height,
},
.format = .depth24_plus,
.usage = .{
.render_attachment = true,
.texture_binding = true,
},
});
const depth_texture_view = depth_texture.createView(&gpu.TextureView.Descriptor{
.format = .depth24_plus,
.dimension = .dimension_2d,
.array_layer_count = 1,
.mip_level_count = 1,
});
app.timer = try core.Timer.start();
app.title_timer = try core.Timer.start();
app.pipeline = pipeline;
app.vertex_buffer = vertex_buffer;
app.uniform_buffer = uniform_buffer;
app.bind_group = bind_group;
app.depth_texture = depth_texture;
app.depth_texture_view = depth_texture_view;
}
pub fn deinit(app: *App) void {
defer _ = gpa.deinit();
defer core.deinit();
app.pipeline.release();
app.vertex_buffer.release();
app.uniform_buffer.release();
app.bind_group.release();
app.depth_texture.release();
app.depth_texture_view.release();
}
pub fn update(app: *App) !bool {
var iter = core.pollEvents();
while (iter.next()) |event| {
switch (event) {
.key_press => |ev| {
if (ev.key == .space) return true;
},
.close => return true,
.framebuffer_resize => |ev| {
// If window is resized, recreate depth buffer otherwise we cannot use it.
app.depth_texture.release();
app.depth_texture = core.device.createTexture(&gpu.Texture.Descriptor{
.size = gpu.Extent3D{
.width = ev.width,
.height = ev.height,
},
.format = .depth24_plus,
.usage = .{
.render_attachment = true,
.texture_binding = true,
},
});
app.depth_texture_view.release();
app.depth_texture_view = app.depth_texture.createView(&gpu.TextureView.Descriptor{
.format = .depth24_plus,
.dimension = .dimension_2d,
.array_layer_count = 1,
.mip_level_count = 1,
});
},
else => {},
}
}
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
const color_attachment = gpu.RenderPassColorAttachment{
.view = back_buffer_view,
.clear_value = .{ .r = 0.5, .g = 0.5, .b = 0.5, .a = 0.0 },
.load_op = .clear,
.store_op = .store,
};
const encoder = core.device.createCommandEncoder(null);
const render_pass_info = gpu.RenderPassDescriptor.init(.{
.color_attachments = &.{color_attachment},
.depth_stencil_attachment = &.{
.view = app.depth_texture_view,
.depth_clear_value = 1.0,
.depth_load_op = .clear,
.depth_store_op = .store,
},
});
{
const time = app.timer.read();
const aspect = @as(f32, @floatFromInt(core.descriptor.width)) / @as(f32, @floatFromInt(core.descriptor.height));
const proj = zm.perspectiveFovRh((2 * std.math.pi) / 5.0, aspect, 0.1, 3000);
const model = zm.mul(
zm.scaling(1000, 1000, 1000),
zm.rotationX(std.math.pi / 2.0 * 3.0),
);
const view = zm.mul(
zm.mul(
zm.lookAtRh(
zm.Vec{ 0, 0, 0, 1 },
zm.Vec{ 1, 0, 0, 1 },
zm.Vec{ 0, 0, 1, 0 },
),
zm.rotationY(time * 0.2),
),
zm.rotationX((std.math.pi / 10.0) * std.math.sin(time)),
);
const mvp = zm.mul(zm.mul(zm.transpose(model), view), proj);
const ubo = UniformBufferObject{ .mat = mvp };
encoder.writeBuffer(app.uniform_buffer, 0, &[_]UniformBufferObject{ubo});
}
const pass = encoder.beginRenderPass(&render_pass_info);
pass.setPipeline(app.pipeline);
pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len);
pass.setBindGroup(0, app.bind_group, &.{});
pass.draw(vertices.len, 1, 0, 0);
pass.end();
pass.release();
var command = encoder.finish(null);
encoder.release();
const queue = core.queue;
queue.submit(&[_]*gpu.CommandBuffer{command});
command.release();
core.swap_chain.present();
back_buffer_view.release();
// update the window title every second
if (app.title_timer.read() >= 1.0) {
app.title_timer.reset();
try core.printTitle("Cube Map [ {d}fps ] [ Input {d}hz ]", .{
core.frameRate(),
core.inputRate(),
});
}
return false;
}
fn rgb24ToRgba32(allocator: std.mem.Allocator, in: []zigimg.color.Rgb24) !zigimg.color.PixelStorage {
const out = try zigimg.color.PixelStorage.init(allocator, .rgba32, in.len);
var i: usize = 0;
while (i < in.len) : (i += 1) {
out.rgba32[i] = zigimg.color.Rgba32{ .r = in[i].r, .g = in[i].g, .b = in[i].b, .a = 255 };
}
return out;
}

View file

@ -0,0 +1,34 @@
struct Uniforms {
modelViewProjectionMatrix : mat4x4<f32>,
}
@binding(0) @group(0) var<uniform> uniforms : Uniforms;
struct VertexOutput {
@builtin(position) Position : vec4<f32>,
@location(0) fragUV : vec2<f32>,
@location(1) fragPosition: vec4<f32>,
}
@vertex
fn vertex_main(
@location(0) position : vec4<f32>,
@location(1) uv : vec2<f32>
) -> VertexOutput {
var output : VertexOutput;
output.Position = uniforms.modelViewProjectionMatrix * position;
output.fragUV = uv;
output.fragPosition = 0.5 * (position + vec4<f32>(1.0, 1.0, 1.0, 1.0));
return output;
}
@group(0) @binding(1) var mySampler: sampler;
@group(0) @binding(2) var myTexture: texture_cube<f32>;
@fragment
fn frag_main(
@location(0) fragUV: vec2<f32>,
@location(1) fragPosition: vec4<f32>
) -> @location(0) vec4<f32> {
var cubemapVec = fragPosition.xyz - vec3<f32>(0.5, 0.5, 0.5);
return textureSample(myTexture, mySampler, cubemapVec);
}

View file

@ -0,0 +1,89 @@
@group(0) @binding(0) var gBufferNormal: texture_2d<f32>;
@group(0) @binding(1) var gBufferAlbedo: texture_2d<f32>;
@group(0) @binding(2) var gBufferDepth: texture_depth_2d;
struct LightData {
position : vec4<f32>,
// TODO - vec3 alignment
color_x : f32,
color_y : f32,
color_z : f32,
radius : f32,
}
struct LightsBuffer {
lights: array<LightData>,
}
@group(1) @binding(0) var<storage, read> lightsBuffer: LightsBuffer;
struct Config {
numLights : u32,
}
struct Camera {
viewProjectionMatrix : mat4x4<f32>,
invViewProjectionMatrix : mat4x4<f32>,
}
@group(1) @binding(1) var<uniform> config: Config;
@group(1) @binding(2) var<uniform> camera: Camera;
fn world_from_screen_coord(coord : vec2<f32>, depth_sample: f32) -> vec3<f32> {
// reconstruct world-space position from the screen coordinate.
let posClip = vec4(coord.x * 2.0 - 1.0, (1.0 - coord.y) * 2.0 - 1.0, depth_sample, 1.0);
let posWorldW = camera.invViewProjectionMatrix * posClip;
let posWorld = posWorldW.xyz / posWorldW.www;
return posWorld;
}
@fragment
fn main(
@builtin(position) coord : vec4<f32>
) -> @location(0) vec4<f32> {
// TODO - variable initialization
var result = vec3<f32>(0, 0, 0);
let depth = textureLoad(
gBufferDepth,
vec2<i32>(floor(coord.xy)),
0
);
// Don't light the sky.
if (depth >= 1.0) {
discard;
}
let bufferSize = textureDimensions(gBufferDepth);
let coordUV = coord.xy / vec2<f32>(bufferSize);
let position = world_from_screen_coord(coordUV, depth);
let normal = textureLoad(
gBufferNormal,
vec2<i32>(floor(coord.xy)),
0
).xyz;
let albedo = textureLoad(
gBufferAlbedo,
vec2<i32>(floor(coord.xy)),
0
).rgb;
for (var i = 0u; i < config.numLights; i++) {
// TODO - vec3 alignment
let lightColor = vec3<f32>(lightsBuffer.lights[i].color_x, lightsBuffer.lights[i].color_y, lightsBuffer.lights[i].color_z);
let L = lightsBuffer.lights[i].position.xyz - position;
let distance = length(L);
if (distance > lightsBuffer.lights[i].radius) {
continue;
}
let lambert = max(dot(normal, normalize(L)), 0.0);
result += vec3<f32>(
lambert * pow(1.0 - distance / lightsBuffer.lights[i].radius, 2.0) * lightColor * albedo
);
}
// some manual ambient
result += vec3(0.2);
return vec4(result, 1.0);
}

View file

@ -0,0 +1,44 @@
@group(0) @binding(0) var gBufferNormal: texture_2d<f32>;
@group(0) @binding(1) var gBufferAlbedo: texture_2d<f32>;
@group(0) @binding(2) var gBufferDepth: texture_depth_2d;
@group(1) @binding(0) var<uniform> canvas : CanvasConstants;
struct CanvasConstants {
size: vec2<f32>,
}
@fragment
fn main(
@builtin(position) coord : vec4<f32>
) -> @location(0) vec4<f32> {
var result : vec4<f32>;
let c = coord.xy / vec2<f32>(canvas.size.x, canvas.size.y);
if (c.x < 0.33333) {
let rawDepth = textureLoad(
gBufferDepth,
vec2<i32>(floor(coord.xy)),
0
);
// remap depth into something a bit more visible
let depth = (1.0 - rawDepth) * 50.0;
result = vec4(depth);
} else if (c.x < 0.66667) {
result = textureLoad(
gBufferNormal,
vec2<i32>(floor(coord.xy)),
0
);
result.x = (result.x + 1.0) * 0.5;
result.y = (result.y + 1.0) * 0.5;
result.z = (result.z + 1.0) * 0.5;
} else {
result = textureLoad(
gBufferAlbedo,
vec2<i32>(floor(coord.xy)),
0
);
}
return result;
}

View file

@ -0,0 +1,22 @@
struct GBufferOutput {
@location(0) normal : vec4<f32>,
// Textures: diffuse color, specular color, smoothness, emissive etc. could go here
@location(1) albedo : vec4<f32>,
}
@fragment
fn main(
@location(0) fragNormal: vec3<f32>,
@location(1) fragUV : vec2<f32>
) -> GBufferOutput {
// faking some kind of checkerboard texture
let uv = floor(30.0 * fragUV);
let c = 0.2 + 0.5 * ((uv.x + uv.y) - 2.0 * floor((uv.x + uv.y) / 2.0));
var output : GBufferOutput;
output.normal = vec4(fragNormal, 1.0);
output.albedo = vec4(c, c, c, 1.0);
return output;
}

View file

@ -0,0 +1,37 @@
struct LightData {
position : vec4<f32>,
// TODO - vec3 alignment
color_x : f32,
color_y : f32,
color_z : f32,
radius : f32,
}
struct LightsBuffer {
lights: array<LightData>,
}
@group(0) @binding(0) var<storage, read_write> lightsBuffer: LightsBuffer;
struct Config {
numLights : u32,
}
@group(0) @binding(1) var<uniform> config: Config;
struct LightExtent {
min : vec4<f32>,
max : vec4<f32>,
}
@group(0) @binding(2) var<uniform> lightExtent: LightExtent;
@compute @workgroup_size(64, 1, 1)
fn main(@builtin(global_invocation_id) GlobalInvocationID : vec3<u32>) {
var index = GlobalInvocationID.x;
if (index >= config.numLights) {
return;
}
lightsBuffer.lights[index].position.y = lightsBuffer.lights[index].position.y - 0.5 - 0.003 * (f32(index) - 64.0 * floor(f32(index) / 64.0));
if (lightsBuffer.lights[index].position.y < lightExtent.min.y) {
lightsBuffer.lights[index].position.y = lightExtent.max.y;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,12 @@
@vertex
fn main(
@builtin(vertex_index) VertexIndex : u32
) -> @builtin(position) vec4<f32> {
// TODO - array initialization
var pos = array<vec2<f32>, 6>(
vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0),
vec2(-1.0, 1.0), vec2(1.0, -1.0), vec2(1.0, 1.0),
);
return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
}

View file

@ -0,0 +1,30 @@
struct Uniforms {
modelMatrix : mat4x4<f32>,
normalModelMatrix : mat4x4<f32>,
}
struct Camera {
viewProjectionMatrix : mat4x4<f32>,
invViewProjectionMatrix : mat4x4<f32>,
}
@group(0) @binding(0) var<uniform> uniforms : Uniforms;
@group(0) @binding(1) var<uniform> camera : Camera;
struct VertexOutput {
@builtin(position) Position : vec4<f32>,
@location(0) fragNormal: vec3<f32>, // normal in world space
@location(1) fragUV: vec2<f32>,
}
@vertex
fn main(
@location(0) position : vec3<f32>,
@location(1) normal : vec3<f32>,
@location(2) uv : vec2<f32>
) -> VertexOutput {
var output : VertexOutput;
let worldPosition = (uniforms.modelMatrix * vec4(position, 1.0)).xyz;
output.Position = camera.viewProjectionMatrix * vec4(worldPosition, 1.0);
output.fragNormal = normalize((uniforms.normalModelMatrix * vec4(normal, 1.0)).xyz);
output.fragUV = uv;
return output;
}

View file

@ -0,0 +1,188 @@
const std = @import("std");
/// Vertex writer manages the placement of vertices by tracking which are unique. If a duplicate vertex is added
/// with `put`, only it's index will be written to the index buffer.
/// `IndexType` should match the integer type used for the index buffer
pub fn VertexWriter(comptime VertexType: type, comptime IndexType: type) type {
return struct {
const MapEntry = struct {
packed_index: IndexType = null_index,
next_sparse: IndexType = null_index,
};
const null_index: IndexType = std.math.maxInt(IndexType);
vertices: []VertexType,
indices: []IndexType,
sparse_to_packed_map: []MapEntry,
/// Next index outside of the 1:1 mapping range for storing
/// position -> normal collisions
next_collision_index: IndexType,
/// Next packed index
next_packed_index: IndexType,
written_indices_count: IndexType,
/// Allocate storage and set default values
/// `sparse_vertices_count` is the number of vertices in the source before de-duplication / remapping
/// Put more succinctly, the largest index value in source index buffer
/// `max_vertex_count` is largest permutation of vertices assuming that {vertex, uv, normal} never map 1:1 and always
/// create a new mapping
pub fn init(
allocator: std.mem.Allocator,
indices_count: IndexType,
sparse_vertices_count: IndexType,
max_vertex_count: IndexType,
) !@This() {
var result: @This() = undefined;
result.vertices = try allocator.alloc(VertexType, max_vertex_count);
result.indices = try allocator.alloc(IndexType, indices_count);
result.sparse_to_packed_map = try allocator.alloc(MapEntry, max_vertex_count);
result.next_collision_index = sparse_vertices_count;
result.next_packed_index = 0;
result.written_indices_count = 0;
@memset(result.sparse_to_packed_map, .{});
return result;
}
pub fn put(self: *@This(), vertex: VertexType, sparse_index: IndexType) void {
if (self.sparse_to_packed_map[sparse_index].packed_index == null_index) {
// New start of chain, reserve a new packed index and add entry to `index_map`
const packed_index = self.next_packed_index;
self.sparse_to_packed_map[sparse_index].packed_index = packed_index;
self.vertices[packed_index] = vertex;
self.indices[self.written_indices_count] = packed_index;
self.written_indices_count += 1;
self.next_packed_index += 1;
return;
}
var previous_sparse_index: IndexType = undefined;
var current_sparse_index = sparse_index;
while (current_sparse_index != null_index) {
const packed_index = self.sparse_to_packed_map[current_sparse_index].packed_index;
if (std.mem.eql(u8, &std.mem.toBytes(self.vertices[packed_index]), &std.mem.toBytes(vertex))) {
// We already have a record for this vertex in our chain
self.indices[self.written_indices_count] = packed_index;
self.written_indices_count += 1;
return;
}
previous_sparse_index = current_sparse_index;
current_sparse_index = self.sparse_to_packed_map[current_sparse_index].next_sparse;
}
// This is a new mapping for the given sparse index
const packed_index = self.next_packed_index;
const remapped_sparse_index = self.next_collision_index;
self.indices[self.written_indices_count] = packed_index;
self.vertices[packed_index] = vertex;
self.sparse_to_packed_map[previous_sparse_index].next_sparse = remapped_sparse_index;
self.sparse_to_packed_map[remapped_sparse_index].packed_index = packed_index;
self.next_packed_index += 1;
self.next_collision_index += 1;
self.written_indices_count += 1;
}
pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void {
allocator.free(self.vertices);
allocator.free(self.indices);
allocator.free(self.sparse_to_packed_map);
}
pub fn indexBuffer(self: @This()) []IndexType {
return self.indices;
}
pub fn vertexBuffer(self: @This()) []VertexType {
return self.vertices[0..self.next_packed_index];
}
};
}
test "VertexWriter" {
const Vec3 = [3]f32;
const Vertex = extern struct {
position: Vec3,
normal: Vec3,
};
const expect = std.testing.expect;
const allocator = std.testing.allocator;
const Face = struct {
position: [3]u16,
normal: [3]u16,
};
const vertices = [_]Vec3{
Vec3{ 1.0, 0.0, 0.0 }, // 0: Position
Vec3{ 2.0, 0.0, 0.0 }, // 1: Position
Vec3{ 3.0, 0.0, 0.0 }, // 2: Position
Vec3{ 1.0, 0.0, 0.0 }, // 3: Normal
Vec3{ 4.0, 0.0, 0.0 }, // 4: Position
Vec3{ 0.0, 1.0, 0.0 }, // 5: Normal
Vec3{ 5.0, 0.0, 0.0 }, // 6: Position
Vec3{ 0.0, 0.0, 1.0 }, // 7: Normal
Vec3{ 1.0, 0.0, 1.0 }, // 8: Normal
Vec3{ 6.0, 0.0, 0.0 }, // 9: Position
};
const faces = [_]Face{
.{ .position = .{ 0, 4, 2 }, .normal = .{ 7, 5, 3 } },
.{ .position = .{ 2, 3, 9 }, .normal = .{ 3, 7, 8 } },
.{ .position = .{ 9, 2, 4 }, .normal = .{ 8, 7, 5 } },
.{ .position = .{ 2, 6, 1 }, .normal = .{ 3, 5, 7 } },
.{ .position = .{ 9, 6, 0 }, .normal = .{ 5, 7, 8 } },
};
var writer = try VertexWriter(Vertex, u32).init(
allocator,
faces.len * 3, // indices count
vertices.len, // original vertices count
faces.len * 3, // maximum vertices count
);
defer writer.deinit(allocator);
for (faces) |face| {
var x: usize = 0;
while (x < 3) : (x += 1) {
const position_index = face.position[x];
const position = vertices[position_index];
const normal = vertices[face.normal[x]];
const vertex = Vertex{
.position = position,
.normal = normal,
};
writer.put(vertex, position_index);
}
}
const indices = writer.indexBuffer();
try expect(indices.len == faces.len * 3);
// Face 0
try expect(indices[0] == 0); // (0, 7) New
try expect(indices[1] == 1); // (4, 5) New
try expect(indices[2] == 2); // (2, 3) New
// Face 1
try expect(indices[3 + 0] == 2); // (2, 3) Duplicate - Reuse index
try expect(indices[3 + 1] == 3); // (3, 7) New
try expect(indices[3 + 2] == 4); // (9, 8) New
// Face 2
try expect(indices[6 + 0] == 4); // (9, 8) Duplicate - Reuse index
try expect(indices[6 + 1] == 5); // (2, 7) New normal mapping (Don't clobber)
try expect(indices[6 + 2] == 1); // (4, 5) Duplicate - Reuse Index
// Face 3
try expect(indices[9 + 0] == 2); // (2, 3) Duplicate - Reuse index
try expect(indices[9 + 1] == 6); // (6, 5) New
try expect(indices[9 + 2] == 7); // (1, 7) New
// Face 4
try expect(indices[12 + 0] == 8); // (9, 5) New normal mapping (Don't clobber)
try expect(indices[12 + 1] == 9); // (6, 7) New normal mapping (Don't clobber)
try expect(indices[12 + 2] == 10); // (0, 8) New normal mapping (Don't clobber)
try expect(writer.vertexBuffer().len == 11);
}

View file

@ -0,0 +1,49 @@
pub const Vertex = extern struct {
pos: @Vector(4, f32),
col: @Vector(4, f32),
uv: @Vector(2, f32),
};
pub const vertices = [_]Vertex{
.{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } },
};

View file

@ -0,0 +1,376 @@
//! To get the effect we want, we need a texture on which to render;
//! we can't use the swapchain texture directly, but we can get the effect
//! by doing the same render pass twice, on the texture and the swapchain.
//! We also need a second texture to use on the cube (after the render pass
//! it needs to copy the other texture.) We can't use the same texture since
//! it would interfere with the synchronization on the gpu during the render pass.
//! This demo currently does not work on opengl, because core.descriptor.width/height,
//! are set to 0 after core.init() and because webgpu does not implement copyTextureToTexture,
//! for opengl
const std = @import("std");
const core = @import("mach").core;
const gpu = core.gpu;
const zm = @import("zmath");
const Vertex = @import("cube_mesh.zig").Vertex;
const vertices = @import("cube_mesh.zig").vertices;
pub const App = @This();
pub const mach_core_options = core.ComptimeOptions{
.use_wgpu = false,
.use_sysgpu = true,
};
const UniformBufferObject = struct {
mat: zm.Mat,
};
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
title_timer: core.Timer,
timer: core.Timer,
pipeline: *gpu.RenderPipeline,
vertex_buffer: *gpu.Buffer,
uniform_buffer: *gpu.Buffer,
bind_group: *gpu.BindGroup,
depth_texture: ?*gpu.Texture,
depth_texture_view: *gpu.TextureView,
cube_texture: *gpu.Texture,
cube_texture_view: *gpu.TextureView,
cube_texture_render: *gpu.Texture,
cube_texture_view_render: *gpu.TextureView,
sampler: *gpu.Sampler,
bgl: *gpu.BindGroupLayout,
pub fn init(app: *App) !void {
try core.init(.{});
const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl"));
const vertex_attributes = [_]gpu.VertexAttribute{
.{ .format = .float32x4, .offset = @offsetOf(Vertex, "pos"), .shader_location = 0 },
.{ .format = .float32x2, .offset = @offsetOf(Vertex, "uv"), .shader_location = 1 },
};
const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{
.array_stride = @sizeOf(Vertex),
.attributes = &vertex_attributes,
});
const blend = gpu.BlendState{};
const color_target = gpu.ColorTargetState{
.format = core.descriptor.format,
.blend = &blend,
.write_mask = gpu.ColorWriteMaskFlags.all,
};
const fragment = gpu.FragmentState.init(.{
.module = shader_module,
.entry_point = "frag_main",
.targets = &.{color_target},
});
const bgle_buffer = gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, true, 0);
const bgle_sampler = gpu.BindGroupLayout.Entry.sampler(1, .{ .fragment = true }, .filtering);
const bgle_textureview = gpu.BindGroupLayout.Entry.texture(2, .{ .fragment = true }, .float, .dimension_2d, false);
const bgl = core.device.createBindGroupLayout(
&gpu.BindGroupLayout.Descriptor.init(.{
.entries = &.{ bgle_buffer, bgle_sampler, bgle_textureview },
}),
);
const bind_group_layouts = [_]*gpu.BindGroupLayout{bgl};
const pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{
.bind_group_layouts = &bind_group_layouts,
}));
const pipeline_descriptor = gpu.RenderPipeline.Descriptor{
.fragment = &fragment,
.layout = pipeline_layout,
.depth_stencil = &.{
.format = .depth24_plus,
.depth_write_enabled = .true,
.depth_compare = .less,
},
.vertex = gpu.VertexState.init(.{
.module = shader_module,
.entry_point = "vertex_main",
.buffers = &.{vertex_buffer_layout},
}),
.primitive = .{
.cull_mode = .back,
},
};
const vertex_buffer = core.device.createBuffer(&.{
.usage = .{ .vertex = true },
.size = @sizeOf(Vertex) * vertices.len,
.mapped_at_creation = .true,
});
const vertex_mapped = vertex_buffer.getMappedRange(Vertex, 0, vertices.len);
@memcpy(vertex_mapped.?, vertices[0..]);
vertex_buffer.unmap();
const uniform_buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .uniform = true },
.size = @sizeOf(UniformBufferObject),
.mapped_at_creation = .false,
});
// The texture to put on the cube
const cube_texture = core.device.createTexture(&gpu.Texture.Descriptor{
.usage = .{ .texture_binding = true, .copy_dst = true },
.size = .{ .width = core.descriptor.width, .height = core.descriptor.height },
.format = core.descriptor.format,
});
// The texture on which we render
const cube_texture_render = core.device.createTexture(&gpu.Texture.Descriptor{
.usage = .{ .render_attachment = true, .copy_src = true },
.size = .{ .width = core.descriptor.width, .height = core.descriptor.height },
.format = core.descriptor.format,
});
const sampler = core.device.createSampler(&gpu.Sampler.Descriptor{
.mag_filter = .linear,
.min_filter = .linear,
});
const cube_texture_view = cube_texture.createView(&gpu.TextureView.Descriptor{
.format = core.descriptor.format,
.dimension = .dimension_2d,
.mip_level_count = 1,
.array_layer_count = 1,
});
const cube_texture_view_render = cube_texture_render.createView(&gpu.TextureView.Descriptor{
.format = core.descriptor.format,
.dimension = .dimension_2d,
.mip_level_count = 1,
.array_layer_count = 1,
});
const bind_group = core.device.createBindGroup(
&gpu.BindGroup.Descriptor.init(.{
.layout = bgl,
.entries = &.{
gpu.BindGroup.Entry.buffer(0, uniform_buffer, 0, @sizeOf(UniformBufferObject), @sizeOf(UniformBufferObject)),
gpu.BindGroup.Entry.sampler(1, sampler),
gpu.BindGroup.Entry.textureView(2, cube_texture_view),
},
}),
);
const depth_texture = core.device.createTexture(&gpu.Texture.Descriptor{
.usage = .{ .render_attachment = true },
.size = .{ .width = core.descriptor.width, .height = core.descriptor.height },
.format = .depth24_plus,
});
const depth_texture_view = depth_texture.createView(&gpu.TextureView.Descriptor{
.format = .depth24_plus,
.dimension = .dimension_2d,
.array_layer_count = 1,
.mip_level_count = 1,
});
app.timer = try core.Timer.start();
app.title_timer = try core.Timer.start();
app.pipeline = core.device.createRenderPipeline(&pipeline_descriptor);
app.vertex_buffer = vertex_buffer;
app.uniform_buffer = uniform_buffer;
app.bind_group = bind_group;
app.depth_texture = depth_texture;
app.depth_texture_view = depth_texture_view;
app.cube_texture = cube_texture;
app.cube_texture_view = cube_texture_view;
app.cube_texture_render = cube_texture_render;
app.cube_texture_view_render = cube_texture_view_render;
app.sampler = sampler;
app.bgl = bgl;
shader_module.release();
pipeline_layout.release();
}
pub fn deinit(app: *App) void {
defer _ = gpa.deinit();
defer core.deinit();
app.pipeline.release();
app.bgl.release();
app.vertex_buffer.release();
app.uniform_buffer.release();
app.cube_texture.release();
app.cube_texture_render.release();
app.sampler.release();
app.cube_texture_view.release();
app.cube_texture_view_render.release();
app.bind_group.release();
app.depth_texture.?.release();
app.depth_texture_view.release();
}
pub fn update(app: *App) !bool {
var iter = core.pollEvents();
while (iter.next()) |event| {
switch (event) {
.key_press => |ev| {
if (ev.key == .space) return true;
},
.close => return true,
.framebuffer_resize => |ev| {
app.depth_texture.?.release();
app.depth_texture = core.device.createTexture(&gpu.Texture.Descriptor{
.usage = .{ .render_attachment = true },
.size = .{ .width = ev.width, .height = ev.height },
.format = .depth24_plus,
});
app.cube_texture.release();
app.cube_texture = core.device.createTexture(&gpu.Texture.Descriptor{
.usage = .{ .texture_binding = true, .copy_dst = true },
.size = .{ .width = ev.width, .height = ev.height },
.format = core.descriptor.format,
});
app.cube_texture_render.release();
app.cube_texture_render = core.device.createTexture(&gpu.Texture.Descriptor{
.usage = .{ .render_attachment = true, .copy_src = true },
.size = .{ .width = ev.width, .height = ev.height },
.format = core.descriptor.format,
});
app.depth_texture_view.release();
app.depth_texture_view = app.depth_texture.?.createView(&gpu.TextureView.Descriptor{
.format = .depth24_plus,
.dimension = .dimension_2d,
.array_layer_count = 1,
.mip_level_count = 1,
});
app.cube_texture_view.release();
app.cube_texture_view = app.cube_texture.createView(&gpu.TextureView.Descriptor{
.format = core.descriptor.format,
.dimension = .dimension_2d,
.mip_level_count = 1,
.array_layer_count = 1,
});
app.cube_texture_view_render.release();
app.cube_texture_view_render = app.cube_texture_render.createView(&gpu.TextureView.Descriptor{
.format = core.descriptor.format,
.dimension = .dimension_2d,
.mip_level_count = 1,
.array_layer_count = 1,
});
app.bind_group.release();
app.bind_group = core.device.createBindGroup(
&gpu.BindGroup.Descriptor.init(.{
.layout = app.bgl,
.entries = &.{
gpu.BindGroup.Entry.buffer(0, app.uniform_buffer, 0, @sizeOf(UniformBufferObject), @sizeOf(UniformBufferObject)),
gpu.BindGroup.Entry.sampler(1, app.sampler),
gpu.BindGroup.Entry.textureView(2, app.cube_texture_view),
},
}),
);
},
else => {},
}
}
const cube_view = app.cube_texture_view_render;
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
const cube_color_attachment = gpu.RenderPassColorAttachment{
.view = cube_view,
.clear_value = gpu.Color{ .r = 0.5, .g = 0.5, .b = 0.5, .a = 1 },
.load_op = .clear,
.store_op = .store,
};
const color_attachment = gpu.RenderPassColorAttachment{
.view = back_buffer_view,
.clear_value = gpu.Color{ .r = 0.5, .g = 0.5, .b = 0.5, .a = 1 },
.load_op = .clear,
.store_op = .store,
};
const depth_stencil_attachment = gpu.RenderPassDepthStencilAttachment{
.view = app.depth_texture_view,
.depth_load_op = .clear,
.depth_store_op = .store,
.depth_clear_value = 1.0,
};
const encoder = core.device.createCommandEncoder(null);
const cube_render_pass_info = gpu.RenderPassDescriptor.init(.{
.color_attachments = &.{cube_color_attachment},
.depth_stencil_attachment = &depth_stencil_attachment,
});
const render_pass_info = gpu.RenderPassDescriptor.init(.{
.color_attachments = &.{color_attachment},
.depth_stencil_attachment = &depth_stencil_attachment,
});
{
const time = app.timer.read();
const model = zm.mul(zm.rotationX(time * (std.math.pi / 2.0)), zm.rotationZ(time * (std.math.pi / 2.0)));
const view = zm.lookAtRh(
zm.Vec{ 0, -4, 0, 1 },
zm.Vec{ 0, 0, 0, 1 },
zm.Vec{ 0, 0, 1, 0 },
);
const proj = zm.perspectiveFovRh(
(std.math.pi * 2.0 / 5.0),
@as(f32, @floatFromInt(core.descriptor.width)) / @as(f32, @floatFromInt(core.descriptor.height)),
1,
100,
);
const ubo = UniformBufferObject{
.mat = zm.transpose(zm.mul(zm.mul(model, view), proj)),
};
encoder.writeBuffer(app.uniform_buffer, 0, &[_]UniformBufferObject{ubo});
}
const pass = encoder.beginRenderPass(&render_pass_info);
pass.setPipeline(app.pipeline);
pass.setBindGroup(0, app.bind_group, &.{0});
pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len);
pass.draw(vertices.len, 1, 0, 0);
pass.end();
pass.release();
encoder.copyTextureToTexture(
&gpu.ImageCopyTexture{
.texture = app.cube_texture_render,
},
&gpu.ImageCopyTexture{
.texture = app.cube_texture,
},
&.{ .width = core.descriptor.width, .height = core.descriptor.height },
);
const cube_pass = encoder.beginRenderPass(&cube_render_pass_info);
cube_pass.setPipeline(app.pipeline);
cube_pass.setBindGroup(0, app.bind_group, &.{0});
cube_pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len);
cube_pass.draw(vertices.len, 1, 0, 0);
cube_pass.end();
cube_pass.release();
var command = encoder.finish(null);
encoder.release();
const queue = core.queue;
queue.submit(&[_]*gpu.CommandBuffer{command});
command.release();
core.swap_chain.present();
back_buffer_view.release();
// update the window title every second
if (app.title_timer.read() >= 1.0) {
app.title_timer.reset();
try core.printTitle("Fractal Cube [ {d}fps ] [ Input {d}hz ]", .{
core.frameRate(),
core.inputRate(),
});
}
return false;
}

View file

@ -0,0 +1,36 @@
struct Uniforms {
matrix : mat4x4<f32>,
};
@binding(0) @group(0) var<uniform> ubo : Uniforms;
struct VertexOut {
@builtin(position) Position : vec4<f32>,
@location(0) fragUV : vec2<f32>,
@location(1) fragPosition: vec4<f32>,
}
@vertex fn vertex_main(
@location(0) position : vec4<f32>,
@location(1) uv: vec2<f32>
) -> VertexOut {
var output : VertexOut;
output.Position = position * ubo.matrix;
output.fragUV = uv;
output.fragPosition = 0.5 * (position + vec4<f32>(1.0, 1.0, 1.0, 1.0));
return output;
}
@binding(1) @group(0) var mySampler: sampler;
@binding(2) @group(0) var myTexture: texture_2d<f32>;
@fragment fn frag_main(
@location(0) fragUV: vec2<f32>,
@location(1) fragPosition: vec4<f32>
) -> @location(0) vec4<f32> {
let texColor = textureSample(myTexture, mySampler, fragUV * 0.8 + vec2<f32>(0.1, 0.1));
let f = f32(length(texColor.rgb - vec3<f32>(0.5, 0.5, 0.5)) < 0.01);
return (1.0 - f) * texColor + f * fragPosition;
// return vec4<f32>(texColor.rgb,1.0);
}

View file

@ -0,0 +1,71 @@
struct CameraUniform {
pos: vec4<f32>,
view_proj: mat4x4<f32>,
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) tex_coords: vec2<f32>,
@location(1) normal: vec3<f32>,
@location(2) position: vec3<f32>,
};
struct Light {
position: vec4<f32>,
color: vec4<f32>,
};
@group(0) @binding(0) var<uniform> camera: CameraUniform;
@group(1) @binding(0) var t_diffuse: texture_2d<f32>;
@group(1) @binding(1) var s_diffuse: sampler;
@group(2) @binding(0) var<uniform> light: Light;
@vertex
fn vs_main(
// TODO - struct input
@location(0) model_position: vec3<f32>,
@location(1) model_normal: vec3<f32>,
@location(2) model_tex_coords: vec2<f32>,
@location(3) instance_model_matrix_0: vec4<f32>,
@location(4) instance_model_matrix_1: vec4<f32>,
@location(5) instance_model_matrix_2: vec4<f32>,
@location(6) instance_model_matrix_3: vec4<f32>)
-> VertexOutput {
let model_matrix = mat4x4<f32>(
instance_model_matrix_0,
instance_model_matrix_1,
instance_model_matrix_2,
instance_model_matrix_3,
);
var out: VertexOutput;
let world_pos = model_matrix * vec4<f32>(model_position, 1.0);
out.position = world_pos.xyz;
out.normal = (model_matrix * vec4<f32>(model_normal, 0.0)).xyz;
out.clip_position = camera.view_proj * world_pos;
out.tex_coords = model_tex_coords;
return out;
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let object_color = textureSample(t_diffuse, s_diffuse, in.tex_coords);
let ambient = 0.1;
let ambient_color = light.color.rbg * ambient;
let light_dir = normalize(light.position.xyz - in.position);
let diffuse = max(dot(in.normal, light_dir), 0.0);
let diffuse_color = light.color.rgb * diffuse;
let view_dir = normalize(camera.pos.xyz - in.position);
let half_dir = normalize(view_dir + light_dir);
let specular = pow(max(dot(in.normal, half_dir), 0.0), 32.0);
let specular_color = light.color.rbg * specular;
let all = ambient_color + diffuse_color + specular_color;
let result = all * object_color.rgb;
return vec4<f32>(result, object_color.a);
}

View file

@ -0,0 +1,34 @@
struct CameraUniform {
view_pos: vec4<f32>,
view_proj: mat4x4<f32>,
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
};
struct Light {
position: vec4<f32>,
color: vec4<f32>,
};
@group(0) @binding(0) var<uniform> camera: CameraUniform;
@group(1) @binding(0) var<uniform> light: Light;
@vertex
fn vs_main(
// TODO - struct input
@location(0) model_position: vec3<f32>,
@location(1) model_normal: vec3<f32>,
@location(2) model_tex_coords: vec2<f32>,
) -> VertexOutput {
var out: VertexOutput;
let world_pos = vec4<f32>(model_position + light.position.xyz, 1.0);
out.clip_position = camera.view_proj * world_pos;
return out;
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return vec4<f32>(1.0, 1.0, 1.0, 0.5);
}

View file

@ -0,0 +1,896 @@
// in this example:
// - comptime generated image data for texture
// - Blinn-Phong lighting
// - several pipelines
//
// quit with escape, q or space
// move camera with arrows or wasd
const std = @import("std");
const core = @import("mach").core;
const gpu = core.gpu;
const zm = @import("zmath");
const Vec = zm.Vec;
const Mat = zm.Mat;
const Quat = zm.Quat;
pub const App = @This();
pub const mach_core_options = core.ComptimeOptions{
.use_wgpu = false,
.use_sysgpu = true,
};
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
title_timer: core.Timer,
timer: core.Timer,
cube: Cube,
camera: Camera,
light: Light,
depth: Texture,
keys: u8,
const Dir = struct {
const up: u8 = 0b0001;
const down: u8 = 0b0010;
const left: u8 = 0b0100;
const right: u8 = 0b1000;
};
pub fn init(app: *App) !void {
try core.init(.{});
app.title_timer = try core.Timer.start();
app.timer = try core.Timer.start();
const eye = Vec{ 5.0, 7.0, 5.0, 0.0 };
const target = Vec{ 0.0, 0.0, 0.0, 0.0 };
const framebuffer = core.descriptor;
const aspect_ratio = @as(f32, @floatFromInt(framebuffer.width)) / @as(f32, @floatFromInt(framebuffer.height));
app.cube = Cube.init();
app.light = Light.init();
app.depth = Texture.depth(core.device, framebuffer.width, framebuffer.height);
app.camera = Camera.init(core.device, eye, target, zm.Vec{ 0.0, 1.0, 0.0, 0.0 }, aspect_ratio, 45.0, 0.1, 100.0);
app.keys = 0;
}
pub fn deinit(app: *App) void {
defer _ = gpa.deinit();
defer core.deinit();
app.cube.deinit();
app.camera.deinit();
app.light.deinit();
app.depth.release();
}
pub fn update(app: *App) !bool {
const delta_time = app.timer.lap();
var iter = core.pollEvents();
while (iter.next()) |event| {
switch (event) {
.key_press => |ev| switch (ev.key) {
.q, .escape, .space => return true,
.w, .up => {
app.keys |= Dir.up;
},
.s, .down => {
app.keys |= Dir.down;
},
.a, .left => {
app.keys |= Dir.left;
},
.d, .right => {
app.keys |= Dir.right;
},
.one => core.setDisplayMode(.windowed),
.two => core.setDisplayMode(.fullscreen),
.three => core.setDisplayMode(.borderless),
else => {},
},
.key_release => |ev| switch (ev.key) {
.w, .up => {
app.keys &= ~Dir.up;
},
.s, .down => {
app.keys &= ~Dir.down;
},
.a, .left => {
app.keys &= ~Dir.left;
},
.d, .right => {
app.keys &= ~Dir.right;
},
else => {},
},
.framebuffer_resize => |ev| {
// recreates the sampler, which is a waste, but for an example it's ok
app.depth.release();
app.depth = Texture.depth(core.device, ev.width, ev.height);
},
.close => return true,
else => {},
}
}
// move camera
const speed = zm.Vec{ delta_time * 5, delta_time * 5, delta_time * 5, delta_time * 5 };
const fwd = zm.normalize3(app.camera.target - app.camera.eye);
const right = zm.normalize3(zm.cross3(fwd, app.camera.up));
if (app.keys & Dir.up != 0)
app.camera.eye += fwd * speed;
if (app.keys & Dir.down != 0)
app.camera.eye -= fwd * speed;
if (app.keys & Dir.right != 0)
app.camera.eye += right * speed
else if (app.keys & Dir.left != 0)
app.camera.eye -= right * speed
else
app.camera.eye += right * (speed * @Vector(4, f32){ 0.5, 0.5, 0.5, 0.5 });
const queue = core.queue;
app.camera.update(queue);
// move light
const light_speed = delta_time * 2.5;
app.light.update(queue, light_speed);
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
defer back_buffer_view.release();
const encoder = core.device.createCommandEncoder(null);
defer encoder.release();
const color_attachment = gpu.RenderPassColorAttachment{
.view = back_buffer_view,
.clear_value = gpu.Color{ .r = 0.0, .g = 0.0, .b = 0.4, .a = 1.0 },
.load_op = .clear,
.store_op = .store,
};
const render_pass_descriptor = gpu.RenderPassDescriptor.init(.{
.color_attachments = &.{color_attachment},
.depth_stencil_attachment = &.{
.view = app.depth.view,
.depth_load_op = .clear,
.depth_store_op = .store,
.depth_clear_value = 1.0,
},
});
const pass = encoder.beginRenderPass(&render_pass_descriptor);
defer pass.release();
// brick cubes
pass.setPipeline(app.cube.pipeline);
pass.setBindGroup(0, app.camera.bind_group, &.{});
pass.setBindGroup(1, app.cube.texture.bind_group.?, &.{});
pass.setBindGroup(2, app.light.bind_group, &.{});
pass.setVertexBuffer(0, app.cube.mesh.buffer, 0, app.cube.mesh.size);
pass.setVertexBuffer(1, app.cube.instance.buffer, 0, app.cube.instance.size);
pass.draw(4, app.cube.instance.len, 0, 0);
pass.draw(4, app.cube.instance.len, 4, 0);
pass.draw(4, app.cube.instance.len, 8, 0);
pass.draw(4, app.cube.instance.len, 12, 0);
pass.draw(4, app.cube.instance.len, 16, 0);
pass.draw(4, app.cube.instance.len, 20, 0);
// light source
pass.setPipeline(app.light.pipeline);
pass.setBindGroup(0, app.camera.bind_group, &.{});
pass.setBindGroup(1, app.light.bind_group, &.{});
pass.setVertexBuffer(0, app.cube.mesh.buffer, 0, app.cube.mesh.size);
pass.draw(4, 1, 0, 0);
pass.draw(4, 1, 4, 0);
pass.draw(4, 1, 8, 0);
pass.draw(4, 1, 12, 0);
pass.draw(4, 1, 16, 0);
pass.draw(4, 1, 20, 0);
pass.end();
var command = encoder.finish(null);
defer command.release();
queue.submit(&[_]*gpu.CommandBuffer{command});
core.swap_chain.present();
// update the window title every second
if (app.title_timer.read() >= 1.0) {
app.title_timer.reset();
try core.printTitle("Gen Texture Light [ {d}fps ] [ Input {d}hz ]", .{
core.frameRate(),
core.inputRate(),
});
}
return false;
}
const Camera = struct {
const Self = @This();
eye: Vec,
target: Vec,
up: Vec,
aspect: f32,
fovy: f32,
near: f32,
far: f32,
bind_group: *gpu.BindGroup,
buffer: Buffer,
const Uniform = extern struct {
pos: Vec,
mat: Mat,
};
fn init(device: *gpu.Device, eye: Vec, target: Vec, up: Vec, aspect: f32, fovy: f32, near: f32, far: f32) Self {
var self: Self = .{
.eye = eye,
.target = target,
.up = up,
.aspect = aspect,
.near = near,
.far = far,
.fovy = fovy,
.buffer = undefined,
.bind_group = undefined,
};
const view = self.buildViewProjMatrix();
const uniform = Uniform{
.pos = self.eye,
.mat = view,
};
const buffer = .{
.buffer = initBuffer(device, .{ .uniform = true }, &@as([20]f32, @bitCast(uniform))),
.size = @sizeOf(@TypeOf(uniform)),
};
const layout = Self.bindGroupLayout(device);
const bind_group = device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{
.layout = layout,
.entries = &.{
gpu.BindGroup.Entry.buffer(0, buffer.buffer, 0, buffer.size, buffer.size),
},
}));
layout.release();
self.buffer = buffer;
self.bind_group = bind_group;
return self;
}
fn deinit(self: *Self) void {
self.bind_group.release();
self.buffer.release();
}
fn update(self: *Self, queue: *gpu.Queue) void {
const mat = self.buildViewProjMatrix();
const uniform = .{
.pos = self.eye,
.mat = mat,
};
queue.writeBuffer(self.buffer.buffer, 0, &[_]Uniform{uniform});
}
inline fn buildViewProjMatrix(s: *const Camera) Mat {
const view = zm.lookAtRh(s.eye, s.target, s.up);
const proj = zm.perspectiveFovRh(s.fovy, s.aspect, s.near, s.far);
return zm.mul(view, proj);
}
inline fn bindGroupLayout(device: *gpu.Device) *gpu.BindGroupLayout {
const visibility = .{ .vertex = true, .fragment = true };
return device.createBindGroupLayout(&gpu.BindGroupLayout.Descriptor.init(.{
.entries = &.{
gpu.BindGroupLayout.Entry.buffer(0, visibility, .uniform, false, 0),
},
}));
}
};
const Buffer = struct {
const Self = @This();
buffer: *gpu.Buffer,
size: usize,
len: u32 = 0,
fn release(self: *Self) void {
self.buffer.release();
}
};
const Cube = struct {
const Self = @This();
pipeline: *gpu.RenderPipeline,
mesh: Buffer,
instance: Buffer,
texture: Texture,
const IPR = 20; // instances per row
const SPACING = 2; // spacing between cubes
const DISPLACEMENT = vec3u(IPR * SPACING / 2, 0, IPR * SPACING / 2);
fn init() Self {
const device = core.device;
const texture = Brick.texture(device);
// instance buffer
var ibuf: [IPR * IPR * 16]f32 = undefined;
var z: usize = 0;
while (z < IPR) : (z += 1) {
var x: usize = 0;
while (x < IPR) : (x += 1) {
const pos = vec3u(x * SPACING, 0, z * SPACING) - DISPLACEMENT;
const rot = blk: {
if (pos[0] == 0 and pos[2] == 0) {
break :blk zm.rotationZ(0.0);
} else {
break :blk zm.mul(zm.rotationX(zm.clamp(zm.abs(pos[0]), 0, 45.0)), zm.rotationZ(zm.clamp(zm.abs(pos[2]), 0, 45.0)));
}
};
const index = z * IPR + x;
const inst = Instance{
.position = pos,
.rotation = rot,
};
zm.storeMat(ibuf[index * 16 ..], inst.toMat());
}
}
const instance = Buffer{
.buffer = initBuffer(device, .{ .vertex = true }, &ibuf),
.len = IPR * IPR,
.size = @sizeOf(@TypeOf(ibuf)),
};
return Self{
.mesh = mesh(device),
.texture = texture,
.instance = instance,
.pipeline = pipeline(),
};
}
fn deinit(self: *Self) void {
self.pipeline.release();
self.mesh.release();
self.instance.release();
self.texture.release();
}
fn pipeline() *gpu.RenderPipeline {
const device = core.device;
const camera_layout = Camera.bindGroupLayout(device);
const texture_layout = Texture.bindGroupLayout(device);
const light_layout = Light.bindGroupLayout(device);
const layout_descriptor = gpu.PipelineLayout.Descriptor.init(.{
.bind_group_layouts = &.{
camera_layout,
texture_layout,
light_layout,
},
});
defer camera_layout.release();
defer texture_layout.release();
defer light_layout.release();
const layout = device.createPipelineLayout(&layout_descriptor);
defer layout.release();
const shader = device.createShaderModuleWGSL("cube.wgsl", @embedFile("cube.wgsl"));
defer shader.release();
const blend = gpu.BlendState{};
const color_target = gpu.ColorTargetState{
.format = core.descriptor.format,
.blend = &blend,
};
const fragment = gpu.FragmentState.init(.{
.module = shader,
.entry_point = "fs_main",
.targets = &.{color_target},
});
const descriptor = gpu.RenderPipeline.Descriptor{
.layout = layout,
.fragment = &fragment,
.vertex = gpu.VertexState.init(.{
.module = shader,
.entry_point = "vs_main",
.buffers = &.{
Self.vertexBufferLayout(),
Self.instanceLayout(),
},
}),
.depth_stencil = &.{
.format = Texture.DEPTH_FORMAT,
.depth_write_enabled = .true,
.depth_compare = .less,
},
.primitive = .{
.cull_mode = .back,
.topology = .triangle_strip,
},
};
return device.createRenderPipeline(&descriptor);
}
fn mesh(device: *gpu.Device) Buffer {
// generated texture has aspect ratio of 1:2
// `h` reflects that ratio
// `v` sets how many times texture repeats across surface
const v = 2;
const h = v * 2;
const buf = asFloats(.{
// z+ face
0, 0, 1, 0, 0, 1, 0, h,
1, 0, 1, 0, 0, 1, v, h,
0, 1, 1, 0, 0, 1, 0, 0,
1, 1, 1, 0, 0, 1, v, 0,
// z- face
1, 0, 0, 0, 0, -1, 0, h,
0, 0, 0, 0, 0, -1, v, h,
1, 1, 0, 0, 0, -1, 0, 0,
0, 1, 0, 0, 0, -1, v, 0,
// x+ face
1, 0, 1, 1, 0, 0, 0, h,
1, 0, 0, 1, 0, 0, v, h,
1, 1, 1, 1, 0, 0, 0, 0,
1, 1, 0, 1, 0, 0, v, 0,
// x- face
0, 0, 0, -1, 0, 0, 0, h,
0, 0, 1, -1, 0, 0, v, h,
0, 1, 0, -1, 0, 0, 0, 0,
0, 1, 1, -1, 0, 0, v, 0,
// y+ face
1, 1, 0, 0, 1, 0, 0, h,
0, 1, 0, 0, 1, 0, v, h,
1, 1, 1, 0, 1, 0, 0, 0,
0, 1, 1, 0, 1, 0, v, 0,
// y- face
0, 0, 0, 0, -1, 0, 0, h,
1, 0, 0, 0, -1, 0, v, h,
0, 0, 1, 0, -1, 0, 0, 0,
1, 0, 1, 0, -1, 0, v, 0,
});
return Buffer{
.buffer = initBuffer(device, .{ .vertex = true }, &buf),
.size = @sizeOf(@TypeOf(buf)),
};
}
fn vertexBufferLayout() gpu.VertexBufferLayout {
const attributes = [_]gpu.VertexAttribute{
.{
.format = .float32x3,
.offset = 0,
.shader_location = 0,
},
.{
.format = .float32x3,
.offset = @sizeOf([3]f32),
.shader_location = 1,
},
.{
.format = .float32x2,
.offset = @sizeOf([6]f32),
.shader_location = 2,
},
};
return gpu.VertexBufferLayout.init(.{
.array_stride = @sizeOf([8]f32),
.attributes = &attributes,
});
}
fn instanceLayout() gpu.VertexBufferLayout {
const attributes = [_]gpu.VertexAttribute{
.{
.format = .float32x4,
.offset = 0,
.shader_location = 3,
},
.{
.format = .float32x4,
.offset = @sizeOf([4]f32),
.shader_location = 4,
},
.{
.format = .float32x4,
.offset = @sizeOf([8]f32),
.shader_location = 5,
},
.{
.format = .float32x4,
.offset = @sizeOf([12]f32),
.shader_location = 6,
},
};
return gpu.VertexBufferLayout.init(.{
.array_stride = @sizeOf([16]f32),
.step_mode = .instance,
.attributes = &attributes,
});
}
};
fn asFloats(comptime arr: anytype) [arr.len]f32 {
const len = arr.len;
comptime var out: [len]f32 = undefined;
comptime var i = 0;
inline while (i < len) : (i += 1) {
out[i] = @as(f32, @floatFromInt(arr[i]));
}
return out;
}
const Brick = struct {
const W = 12;
const H = 6;
fn texture(device: *gpu.Device) Texture {
const slice: []const u8 = &data();
return Texture.fromData(device, W, H, u8, slice);
}
fn data() [W * H * 4]u8 {
comptime var out: [W * H * 4]u8 = undefined;
// fill all the texture with brick color
comptime var i = 0;
inline while (i < H) : (i += 1) {
comptime var j = 0;
inline while (j < W * 4) : (j += 4) {
out[i * W * 4 + j + 0] = 210;
out[i * W * 4 + j + 1] = 30;
out[i * W * 4 + j + 2] = 30;
out[i * W * 4 + j + 3] = 0;
}
}
const f = 10;
// fill the cement lines
inline for ([_]comptime_int{ 0, 1 }) |k| {
inline for ([_]comptime_int{ 5 * 4, 11 * 4 }) |m| {
out[k * W * 4 + m + 0] = f;
out[k * W * 4 + m + 1] = f;
out[k * W * 4 + m + 2] = f;
out[k * W * 4 + m + 3] = 0;
}
}
inline for ([_]comptime_int{ 3, 4 }) |k| {
inline for ([_]comptime_int{ 2 * 4, 8 * 4 }) |m| {
out[k * W * 4 + m + 0] = f;
out[k * W * 4 + m + 1] = f;
out[k * W * 4 + m + 2] = f;
out[k * W * 4 + m + 3] = 0;
}
}
inline for ([_]comptime_int{ 2, 5 }) |k| {
comptime var m = 0;
inline while (m < W * 4) : (m += 4) {
out[k * W * 4 + m + 0] = f;
out[k * W * 4 + m + 1] = f;
out[k * W * 4 + m + 2] = f;
out[k * W * 4 + m + 3] = 0;
}
}
return out;
}
};
// don't confuse with gpu.Texture
const Texture = struct {
const Self = @This();
texture: *gpu.Texture,
view: *gpu.TextureView,
sampler: *gpu.Sampler,
bind_group: ?*gpu.BindGroup,
const DEPTH_FORMAT = .depth32_float;
const FORMAT = .rgba8_unorm;
fn release(self: *Self) void {
self.texture.release();
self.view.release();
self.sampler.release();
if (self.bind_group) |bind_group| bind_group.release();
}
fn fromData(device: *gpu.Device, width: u32, height: u32, comptime T: type, data: []const T) Self {
const extent = gpu.Extent3D{
.width = width,
.height = height,
};
const texture = device.createTexture(&gpu.Texture.Descriptor{
.size = extent,
.format = FORMAT,
.usage = .{ .copy_dst = true, .texture_binding = true },
});
const view = texture.createView(&gpu.TextureView.Descriptor{
.format = FORMAT,
.dimension = .dimension_2d,
.array_layer_count = 1,
.mip_level_count = 1,
});
const sampler = device.createSampler(&gpu.Sampler.Descriptor{
.address_mode_u = .repeat,
.address_mode_v = .repeat,
.address_mode_w = .repeat,
.mag_filter = .linear,
.min_filter = .linear,
.mipmap_filter = .linear,
.max_anisotropy = 1, // 1,2,4,8,16
});
core.queue.writeTexture(
&gpu.ImageCopyTexture{
.texture = texture,
},
&gpu.Texture.DataLayout{
.bytes_per_row = 4 * width,
.rows_per_image = height,
},
&extent,
data,
);
const bind_group_layout = Self.bindGroupLayout(device);
const bind_group = device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{
.layout = bind_group_layout,
.entries = &.{
gpu.BindGroup.Entry.textureView(0, view),
gpu.BindGroup.Entry.sampler(1, sampler),
},
}));
bind_group_layout.release();
return Self{
.view = view,
.texture = texture,
.sampler = sampler,
.bind_group = bind_group,
};
}
fn depth(device: *gpu.Device, width: u32, height: u32) Self {
const extent = gpu.Extent3D{
.width = width,
.height = height,
};
const texture = device.createTexture(&gpu.Texture.Descriptor{
.size = extent,
.format = DEPTH_FORMAT,
.usage = .{
.render_attachment = true,
.texture_binding = true,
},
});
const view = texture.createView(&gpu.TextureView.Descriptor{
.dimension = .dimension_2d,
.array_layer_count = 1,
.mip_level_count = 1,
});
const sampler = device.createSampler(&gpu.Sampler.Descriptor{
.mag_filter = .linear,
.compare = .less_equal,
});
return Self{
.texture = texture,
.view = view,
.sampler = sampler,
.bind_group = null, // not used
};
}
inline fn bindGroupLayout(device: *gpu.Device) *gpu.BindGroupLayout {
const visibility = .{ .fragment = true };
const Entry = gpu.BindGroupLayout.Entry;
return device.createBindGroupLayout(&gpu.BindGroupLayout.Descriptor.init(.{
.entries = &.{
Entry.texture(0, visibility, .float, .dimension_2d, false),
Entry.sampler(1, visibility, .filtering),
},
}));
}
};
const Light = struct {
const Self = @This();
uniform: Uniform,
buffer: Buffer,
bind_group: *gpu.BindGroup,
pipeline: *gpu.RenderPipeline,
const Uniform = extern struct {
position: Vec,
color: Vec,
};
fn init() Self {
const device = core.device;
const uniform = Uniform{
.color = vec3u(1, 1, 1),
.position = vec3u(3, 7, 2),
};
const buffer = .{
.buffer = initBuffer(device, .{ .uniform = true }, &@as([8]f32, @bitCast(uniform))),
.size = @sizeOf(@TypeOf(uniform)),
};
const layout = Self.bindGroupLayout(device);
const bind_group = device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{
.layout = layout,
.entries = &.{
gpu.BindGroup.Entry.buffer(0, buffer.buffer, 0, buffer.size, buffer.size),
},
}));
layout.release();
return Self{
.buffer = buffer,
.uniform = uniform,
.bind_group = bind_group,
.pipeline = Self.pipeline(),
};
}
fn deinit(self: *Self) void {
self.buffer.release();
self.bind_group.release();
self.pipeline.release();
}
fn update(self: *Self, queue: *gpu.Queue, delta: f32) void {
const old = self.uniform;
const new = Light.Uniform{
.position = zm.qmul(zm.quatFromAxisAngle(vec3u(0, 1, 0), delta), old.position),
.color = old.color,
};
queue.writeBuffer(self.buffer.buffer, 0, &[_]Light.Uniform{new});
self.uniform = new;
}
inline fn bindGroupLayout(device: *gpu.Device) *gpu.BindGroupLayout {
const visibility = .{ .vertex = true, .fragment = true };
const Entry = gpu.BindGroupLayout.Entry;
return device.createBindGroupLayout(&gpu.BindGroupLayout.Descriptor.init(.{
.entries = &.{
Entry.buffer(0, visibility, .uniform, false, 0),
},
}));
}
fn pipeline() *gpu.RenderPipeline {
const device = core.device;
const camera_layout = Camera.bindGroupLayout(device);
const light_layout = Light.bindGroupLayout(device);
const layout_descriptor = gpu.PipelineLayout.Descriptor.init(.{
.bind_group_layouts = &.{
camera_layout,
light_layout,
},
});
defer camera_layout.release();
defer light_layout.release();
const layout = device.createPipelineLayout(&layout_descriptor);
defer layout.release();
const shader = core.device.createShaderModuleWGSL("light.wgsl", @embedFile("light.wgsl"));
defer shader.release();
const blend = gpu.BlendState{};
const color_target = gpu.ColorTargetState{
.format = core.descriptor.format,
.blend = &blend,
};
const fragment = gpu.FragmentState.init(.{
.module = shader,
.entry_point = "fs_main",
.targets = &.{color_target},
});
const descriptor = gpu.RenderPipeline.Descriptor{
.layout = layout,
.fragment = &fragment,
.vertex = gpu.VertexState.init(.{
.module = shader,
.entry_point = "vs_main",
.buffers = &.{
Cube.vertexBufferLayout(),
},
}),
.depth_stencil = &.{
.format = Texture.DEPTH_FORMAT,
.depth_write_enabled = .true,
.depth_compare = .less,
},
.primitive = .{
.cull_mode = .back,
.topology = .triangle_strip,
},
};
return device.createRenderPipeline(&descriptor);
}
};
inline fn initBuffer(device: *gpu.Device, usage: gpu.Buffer.UsageFlags, data: anytype) *gpu.Buffer {
std.debug.assert(@typeInfo(@TypeOf(data)) == .Pointer);
const T = std.meta.Elem(@TypeOf(data));
var u = usage;
u.copy_dst = true;
const buffer = device.createBuffer(&.{
.size = @sizeOf(T) * data.len,
.usage = u,
.mapped_at_creation = .true,
});
const mapped = buffer.getMappedRange(T, 0, data.len);
@memcpy(mapped.?, data);
buffer.unmap();
return buffer;
}
fn vec3i(x: isize, y: isize, z: isize) Vec {
return Vec{ @floatFromInt(x), @floatFromInt(y), @floatFromInt(z), 0.0 };
}
fn vec3u(x: usize, y: usize, z: usize) Vec {
return zm.Vec{ @floatFromInt(x), @floatFromInt(y), @floatFromInt(z), 0.0 };
}
// todo indside Cube
const Instance = struct {
const Self = @This();
position: Vec,
rotation: Mat,
fn toMat(self: *const Self) Mat {
return zm.mul(self.rotation, zm.translationV(self.position));
}
};

View file

@ -0,0 +1,82 @@
struct Params {
filterDim : i32,
blockDim : u32,
}
@group(0) @binding(0) var samp : sampler;
@group(0) @binding(1) var<uniform> params : Params;
@group(1) @binding(1) var inputTex : texture_2d<f32>;
@group(1) @binding(2) var outputTex : texture_storage_2d<rgba8unorm, write>;
struct Flip {
value : u32,
}
@group(1) @binding(3) var<uniform> flip : Flip;
// This shader blurs the input texture in one direction, depending on whether
// |flip.value| is 0 or 1.
// It does so by running (128 / 4) threads per workgroup to load 128
// texels into 4 rows of shared memory. Each thread loads a
// 4 x 4 block of texels to take advantage of the texture sampling
// hardware.
// Then, each thread computes the blur result by averaging the adjacent texel values
// in shared memory.
// Because we're operating on a subset of the texture, we cannot compute all of the
// results since not all of the neighbors are available in shared memory.
// Specifically, with 128 x 128 tiles, we can only compute and write out
// square blocks of size 128 - (filterSize - 1). We compute the number of blocks
// needed in Javascript and dispatch that amount.
var<workgroup> tile : array<array<vec3<f32>, 128>, 4>;
@compute @workgroup_size(32, 1, 1)
fn main(
@builtin(workgroup_id) WorkGroupID : vec3<u32>,
@builtin(local_invocation_id) LocalInvocationID : vec3<u32>
) {
// TODO - mixed vector arithmetic (vec2<u32> and vec2<i32>)
let filterOffset = (params.filterDim - 1) / 2;
let dims = vec2<u32>(textureDimensions(inputTex, 0));
let baseIndex = vec2<u32>(WorkGroupID.xy * vec2(params.blockDim, 4) +
LocalInvocationID.xy * vec2<u32>(4, 1))
- vec2<u32>(filterOffset, 0);
for (var r = 0; r < 4; r++) {
for (var c = 0; c < 4; c++) {
var loadIndex = baseIndex + vec2<u32>(c, r);
if (flip.value != 0u) {
loadIndex = loadIndex.yx;
}
tile[r][4 * LocalInvocationID.x + u32(c)] = textureSampleLevel(
inputTex,
samp,
(vec2<f32>(loadIndex) + vec2<f32>(0.25, 0.25)) / vec2<f32>(dims),
0.0
).rgb;
}
}
workgroupBarrier();
for (var r = 0; r < 4; r++) {
for (var c = 0; c < 4; c++) {
var writeIndex = baseIndex + vec2<u32>(c, r);
if (flip.value != 0) {
writeIndex = writeIndex.yx;
}
let center = u32(4 * LocalInvocationID.x) + c;
if (center >= filterOffset &&
center < 128 - filterOffset &&
all(writeIndex < dims)) {
var acc = vec3(0.0, 0.0, 0.0);
for (var f = 0; f < params.filterDim; f++) {
var i = center + f - filterOffset;
acc = acc + (1.0 / f32(params.filterDim)) * tile[r][i];
}
textureStore(outputTex, writeIndex, vec4(acc, 1.0));
}
}
}
}

View file

@ -0,0 +1,38 @@
@group(0) @binding(0) var mySampler : sampler;
@group(0) @binding(1) var myTexture : texture_2d<f32>;
struct VertexOutput {
@builtin(position) Position : vec4<f32>,
@location(0) fragUV : vec2<f32>,
}
@vertex
fn vert_main(@builtin(vertex_index) VertexIndex : u32) -> VertexOutput {
var pos = array<vec2<f32>, 6>(
vec2<f32>( 1.0, 1.0),
vec2<f32>( 1.0, -1.0),
vec2<f32>(-1.0, -1.0),
vec2<f32>( 1.0, 1.0),
vec2<f32>(-1.0, -1.0),
vec2<f32>(-1.0, 1.0)
);
var uv = array<vec2<f32>, 6>(
vec2<f32>(1.0, 0.0),
vec2<f32>(1.0, 1.0),
vec2<f32>(0.0, 1.0),
vec2<f32>(1.0, 0.0),
vec2<f32>(0.0, 1.0),
vec2<f32>(0.0, 0.0)
);
var output : VertexOutput;
output.Position = vec4<f32>(pos[VertexIndex], 0.0, 1.0);
output.fragUV = uv[VertexIndex];
return output;
}
@fragment
fn frag_main(@location(0) fragUV : vec2<f32>) -> @location(0) vec4<f32> {
return textureSample(myTexture, mySampler, fragUV);
}

View file

@ -0,0 +1,331 @@
const std = @import("std");
const core = @import("mach").core;
const gpu = core.gpu;
const zigimg = @import("zigimg");
const assets = @import("assets");
title_timer: core.Timer,
blur_pipeline: *gpu.ComputePipeline,
fullscreen_quad_pipeline: *gpu.RenderPipeline,
cube_texture: *gpu.Texture,
textures: [2]*gpu.Texture,
blur_params_buffer: *gpu.Buffer,
compute_constants: *gpu.BindGroup,
compute_bind_group_0: *gpu.BindGroup,
compute_bind_group_1: *gpu.BindGroup,
compute_bind_group_2: *gpu.BindGroup,
show_result_bind_group: *gpu.BindGroup,
img_size: gpu.Extent3D,
pub const App = @This();
pub const mach_core_options = core.ComptimeOptions{
.use_wgpu = false,
.use_sysgpu = true,
};
// Constants from the blur.wgsl shader
const tile_dimension: u32 = 128;
const batch: [2]u32 = .{ 4, 4 };
// Currently hardcoded
const filter_size: u32 = 15;
const iterations: u32 = 2;
var block_dimension: u32 = tile_dimension - (filter_size - 1);
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
pub fn init(app: *App) !void {
try core.init(.{});
const allocator = gpa.allocator();
const queue = core.queue;
const blur_shader_module = core.device.createShaderModuleWGSL("blur.wgsl", @embedFile("blur.wgsl"));
const blur_pipeline_descriptor = gpu.ComputePipeline.Descriptor{
.compute = gpu.ProgrammableStageDescriptor{
.module = blur_shader_module,
.entry_point = "main",
},
};
const blur_pipeline = core.device.createComputePipeline(&blur_pipeline_descriptor);
blur_shader_module.release();
const fullscreen_quad_vs_module = core.device.createShaderModuleWGSL(
"fullscreen_textured_quad.wgsl",
@embedFile("fullscreen_textured_quad.wgsl"),
);
const fullscreen_quad_fs_module = core.device.createShaderModuleWGSL(
"fullscreen_textured_quad.wgsl",
@embedFile("fullscreen_textured_quad.wgsl"),
);
const blend = gpu.BlendState{};
const color_target = gpu.ColorTargetState{
.format = core.descriptor.format,
.blend = &blend,
.write_mask = gpu.ColorWriteMaskFlags.all,
};
const fragment_state = gpu.FragmentState.init(.{
.module = fullscreen_quad_fs_module,
.entry_point = "frag_main",
.targets = &.{color_target},
});
const fullscreen_quad_pipeline_descriptor = gpu.RenderPipeline.Descriptor{
.fragment = &fragment_state,
.vertex = .{
.module = fullscreen_quad_vs_module,
.entry_point = "vert_main",
},
};
const fullscreen_quad_pipeline = core.device.createRenderPipeline(&fullscreen_quad_pipeline_descriptor);
fullscreen_quad_vs_module.release();
fullscreen_quad_fs_module.release();
const sampler = core.device.createSampler(&.{
.mag_filter = .linear,
.min_filter = .linear,
});
var img = try zigimg.Image.fromMemory(allocator, assets.gotta_go_fast_png);
defer img.deinit();
const img_size = gpu.Extent3D{ .width = @as(u32, @intCast(img.width)), .height = @as(u32, @intCast(img.height)) };
const cube_texture = core.device.createTexture(&.{
.size = img_size,
.format = .rgba8_unorm,
.usage = .{
.texture_binding = true,
.copy_dst = true,
.render_attachment = true,
},
});
const data_layout = gpu.Texture.DataLayout{
.bytes_per_row = @as(u32, @intCast(img.width * 4)),
.rows_per_image = @as(u32, @intCast(img.height)),
};
switch (img.pixels) {
.rgba32 => |pixels| queue.writeTexture(&.{ .texture = cube_texture }, &data_layout, &img_size, pixels),
.rgb24 => |pixels| {
const data = try rgb24ToRgba32(allocator, pixels);
defer data.deinit(allocator);
queue.writeTexture(&.{ .texture = cube_texture }, &data_layout, &img_size, data.rgba32);
},
else => @panic("unsupported image color format"),
}
var textures: [2]*gpu.Texture = undefined;
for (textures, 0..) |_, i| {
textures[i] = core.device.createTexture(&.{
.size = img_size,
.format = .rgba8_unorm,
.usage = .{
.storage_binding = true,
.texture_binding = true,
.copy_dst = true,
},
});
}
// the shader blurs the input texture in one direction,
// depending on whether flip value is 0 or 1
var flip: [2]*gpu.Buffer = undefined;
for (flip, 0..) |_, i| {
const buffer = core.device.createBuffer(&.{
.usage = .{ .uniform = true },
.size = @sizeOf(u32),
.mapped_at_creation = .true,
});
const buffer_mapped = buffer.getMappedRange(u32, 0, 1);
buffer_mapped.?[0] = @as(u32, @intCast(i));
buffer.unmap();
flip[i] = buffer;
}
const blur_params_buffer = core.device.createBuffer(&.{
.size = 8,
.usage = .{ .copy_dst = true, .uniform = true },
});
const blur_bind_group_layout0 = blur_pipeline.getBindGroupLayout(0);
const blur_bind_group_layout1 = blur_pipeline.getBindGroupLayout(1);
const fullscreen_bind_group_layout = fullscreen_quad_pipeline.getBindGroupLayout(0);
const cube_texture_view = cube_texture.createView(&gpu.TextureView.Descriptor{});
const texture0_view = textures[0].createView(&gpu.TextureView.Descriptor{});
const texture1_view = textures[1].createView(&gpu.TextureView.Descriptor{});
const compute_constants = core.device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{
.layout = blur_bind_group_layout0,
.entries = &.{
gpu.BindGroup.Entry.sampler(0, sampler),
gpu.BindGroup.Entry.buffer(1, blur_params_buffer, 0, 8, 8),
},
}));
const compute_bind_group_0 = core.device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{
.layout = blur_bind_group_layout1,
.entries = &.{
gpu.BindGroup.Entry.textureView(1, cube_texture_view),
gpu.BindGroup.Entry.textureView(2, texture0_view),
gpu.BindGroup.Entry.buffer(3, flip[0], 0, 4, 4),
},
}));
const compute_bind_group_1 = core.device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{
.layout = blur_bind_group_layout1,
.entries = &.{
gpu.BindGroup.Entry.textureView(1, texture0_view),
gpu.BindGroup.Entry.textureView(2, texture1_view),
gpu.BindGroup.Entry.buffer(3, flip[1], 0, 4, 4),
},
}));
const compute_bind_group_2 = core.device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{
.layout = blur_bind_group_layout1,
.entries = &.{
gpu.BindGroup.Entry.textureView(1, texture1_view),
gpu.BindGroup.Entry.textureView(2, texture0_view),
gpu.BindGroup.Entry.buffer(3, flip[0], 0, 4, 4),
},
}));
const show_result_bind_group = core.device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{
.layout = fullscreen_bind_group_layout,
.entries = &.{
gpu.BindGroup.Entry.sampler(0, sampler),
gpu.BindGroup.Entry.textureView(1, texture1_view),
},
}));
blur_bind_group_layout0.release();
blur_bind_group_layout1.release();
fullscreen_bind_group_layout.release();
sampler.release();
flip[0].release();
flip[1].release();
cube_texture_view.release();
texture0_view.release();
texture1_view.release();
const blur_params_buffer_data = [_]u32{ filter_size, block_dimension };
queue.writeBuffer(blur_params_buffer, 0, &blur_params_buffer_data);
app.title_timer = try core.Timer.start();
app.blur_pipeline = blur_pipeline;
app.fullscreen_quad_pipeline = fullscreen_quad_pipeline;
app.cube_texture = cube_texture;
app.textures = textures;
app.blur_params_buffer = blur_params_buffer;
app.compute_constants = compute_constants;
app.compute_bind_group_0 = compute_bind_group_0;
app.compute_bind_group_1 = compute_bind_group_1;
app.compute_bind_group_2 = compute_bind_group_2;
app.show_result_bind_group = show_result_bind_group;
app.img_size = img_size;
}
pub fn deinit(app: *App) void {
defer _ = gpa.deinit();
defer core.deinit();
app.blur_pipeline.release();
app.fullscreen_quad_pipeline.release();
app.cube_texture.release();
app.textures[0].release();
app.textures[1].release();
app.blur_params_buffer.release();
app.compute_constants.release();
app.compute_bind_group_0.release();
app.compute_bind_group_1.release();
app.compute_bind_group_2.release();
app.show_result_bind_group.release();
}
pub fn update(app: *App) !bool {
var iter = core.pollEvents();
while (iter.next()) |event| {
if (event == .close) return true;
}
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
const encoder = core.device.createCommandEncoder(null);
const compute_pass = encoder.beginComputePass(null);
compute_pass.setPipeline(app.blur_pipeline);
compute_pass.setBindGroup(0, app.compute_constants, &.{});
const width: u32 = @as(u32, @intCast(app.img_size.width));
const height: u32 = @as(u32, @intCast(app.img_size.height));
compute_pass.setBindGroup(1, app.compute_bind_group_0, &.{});
compute_pass.dispatchWorkgroups(try std.math.divCeil(u32, width, block_dimension), try std.math.divCeil(u32, height, batch[1]), 1);
compute_pass.setBindGroup(1, app.compute_bind_group_1, &.{});
compute_pass.dispatchWorkgroups(try std.math.divCeil(u32, height, block_dimension), try std.math.divCeil(u32, width, batch[1]), 1);
var i: u32 = 0;
while (i < iterations - 1) : (i += 1) {
compute_pass.setBindGroup(1, app.compute_bind_group_2, &.{});
compute_pass.dispatchWorkgroups(try std.math.divCeil(u32, width, block_dimension), try std.math.divCeil(u32, height, batch[1]), 1);
compute_pass.setBindGroup(1, app.compute_bind_group_1, &.{});
compute_pass.dispatchWorkgroups(try std.math.divCeil(u32, height, block_dimension), try std.math.divCeil(u32, width, batch[1]), 1);
}
compute_pass.end();
compute_pass.release();
const color_attachment = gpu.RenderPassColorAttachment{
.view = back_buffer_view,
.clear_value = std.mem.zeroes(gpu.Color),
.load_op = .clear,
.store_op = .store,
};
const render_pass_descriptor = gpu.RenderPassDescriptor.init(.{
.color_attachments = &.{color_attachment},
});
const render_pass = encoder.beginRenderPass(&render_pass_descriptor);
render_pass.setPipeline(app.fullscreen_quad_pipeline);
render_pass.setBindGroup(0, app.show_result_bind_group, &.{});
render_pass.draw(6, 1, 0, 0);
render_pass.end();
render_pass.release();
var command = encoder.finish(null);
encoder.release();
const queue = core.queue;
queue.submit(&[_]*gpu.CommandBuffer{command});
command.release();
core.swap_chain.present();
back_buffer_view.release();
// update the window title every second
if (app.title_timer.read() >= 1.0) {
app.title_timer.reset();
try core.printTitle("Image Blur [ {d}fps ] [ Input {d}hz ]", .{
core.frameRate(),
core.inputRate(),
});
}
return false;
}
fn rgb24ToRgba32(allocator: std.mem.Allocator, in: []zigimg.color.Rgb24) !zigimg.color.PixelStorage {
const out = try zigimg.color.PixelStorage.init(allocator, .rgba32, in.len);
var i: usize = 0;
while (i < in.len) : (i += 1) {
out.rgba32[i] = zigimg.color.Rgba32{ .r = in[i].r, .g = in[i].g, .b = in[i].b, .a = 255 };
}
return out;
}

View file

@ -0,0 +1,39 @@
@group(0) @binding(0) var mySampler : sampler;
@group(0) @binding(1) var myTexture : texture_2d<f32>;
struct VertexOutput {
@builtin(position) Position : vec4<f32>,
@location(0) fragUV : vec2<f32>,
}
@vertex
fn vert_main(@builtin(vertex_index) VertexIndex : u32) -> VertexOutput {
// Draw a fullscreen quad using two triangles, with UV coordinates (normalized pixel coordinates)
// that would have the full texture be displayed.
var pos = array<vec2<f32>, 6>(
vec2<f32>( 1.0, 1.0), // right, top
vec2<f32>( 1.0, -1.0), // right, bottom
vec2<f32>(-1.0, -1.0), // left, bottom
vec2<f32>( 1.0, 1.0), // right, top
vec2<f32>(-1.0, -1.0), // left, bottom
vec2<f32>(-1.0, 1.0) // left, top
);
var uv = array<vec2<f32>, 6>(
vec2<f32>(1.0, 0.0),
vec2<f32>(1.0, 1.0),
vec2<f32>(0.0, 1.0),
vec2<f32>(1.0, 0.0),
vec2<f32>(0.0, 1.0),
vec2<f32>(0.0, 0.0)
);
var output : VertexOutput;
output.Position = vec4<f32>(pos[VertexIndex], 0.0, 1.0);
output.fragUV = uv[VertexIndex];
return output;
}
@fragment
fn frag_main(@location(0) fragUV : vec2<f32>) -> @location(0) vec4<f32> {
return textureSample(myTexture, mySampler, fragUV);
}

View file

@ -0,0 +1,189 @@
const std = @import("std");
const core = @import("mach").core;
const gpu = core.gpu;
const zigimg = @import("zigimg");
const assets = @import("assets");
pub const mach_core_options = core.ComptimeOptions{
.use_wgpu = false,
.use_sysgpu = true,
};
title_timer: core.Timer,
pipeline: *gpu.RenderPipeline,
texture: *gpu.Texture,
bind_group: *gpu.BindGroup,
img_size: gpu.Extent3D,
pub const App = @This();
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
pub fn init(app: *App) !void {
try core.init(.{});
const allocator = gpa.allocator();
// Load our shader that will render a fullscreen textured quad using two triangles, needed to
// get the image on screen.
const fullscreen_quad_vs_module = core.device.createShaderModuleWGSL(
"fullscreen_textured_quad.wgsl",
@embedFile("fullscreen_textured_quad.wgsl"),
);
defer fullscreen_quad_vs_module.release();
const fullscreen_quad_fs_module = core.device.createShaderModuleWGSL(
"fullscreen_textured_quad.wgsl",
@embedFile("fullscreen_textured_quad.wgsl"),
);
defer fullscreen_quad_fs_module.release();
// Create our render pipeline
const blend = gpu.BlendState{};
const color_target = gpu.ColorTargetState{
.format = core.descriptor.format,
.blend = &blend,
.write_mask = gpu.ColorWriteMaskFlags.all,
};
const fragment_state = gpu.FragmentState.init(.{
.module = fullscreen_quad_fs_module,
.entry_point = "frag_main",
.targets = &.{color_target},
});
const pipeline_descriptor = gpu.RenderPipeline.Descriptor{
.fragment = &fragment_state,
.vertex = .{
.module = fullscreen_quad_vs_module,
.entry_point = "vert_main",
},
};
const pipeline = core.device.createRenderPipeline(&pipeline_descriptor);
// Create a texture sampler. This determines what happens when the texture doesn't match the
// dimensions of the screen it's being displayed on. If the image needs to be magnified or
// minified to fit, it can be linearly interpolated (i.e. 'blurred', .linear) or the nearest
// pixel may be used (i.e. 'pixelated', .nearest)
const sampler = core.device.createSampler(&.{
.mag_filter = .linear,
.min_filter = .linear,
});
defer sampler.release();
// Load the pixels of the image
var img = try zigimg.Image.fromMemory(allocator, assets.gotta_go_fast_png);
defer img.deinit();
const img_size = gpu.Extent3D{ .width = @as(u32, @intCast(img.width)), .height = @as(u32, @intCast(img.height)) };
// Create a texture
const texture = core.device.createTexture(&.{
.size = img_size,
.format = .rgba8_unorm,
.usage = .{
.texture_binding = true,
.copy_dst = true,
.render_attachment = true,
},
});
// Upload the pixels (from the CPU) to the GPU. You could e.g. do this once per frame if you
// wanted the image to be updated dynamically.
const data_layout = gpu.Texture.DataLayout{
.bytes_per_row = @as(u32, @intCast(img.width * 4)),
.rows_per_image = @as(u32, @intCast(img.height)),
};
switch (img.pixels) {
.rgba32 => |pixels| core.queue.writeTexture(&.{ .texture = texture }, &data_layout, &img_size, pixels),
.rgb24 => |pixels| {
const data = try rgb24ToRgba32(allocator, pixels);
defer data.deinit(allocator);
core.queue.writeTexture(&.{ .texture = texture }, &data_layout, &img_size, data.rgba32);
},
else => @panic("unsupported image color format"),
}
// Describe which data we will pass to our shader (GPU program)
const bind_group_layout = pipeline.getBindGroupLayout(0);
defer bind_group_layout.release();
const texture_view = texture.createView(&gpu.TextureView.Descriptor{});
defer texture_view.release();
const bind_group = core.device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{
.layout = bind_group_layout,
.entries = &.{
gpu.BindGroup.Entry.sampler(0, sampler),
gpu.BindGroup.Entry.textureView(1, texture_view),
},
}));
app.* = .{
.title_timer = try core.Timer.start(),
.pipeline = pipeline,
.texture = texture,
.bind_group = bind_group,
.img_size = img_size,
};
}
pub fn deinit(app: *App) void {
defer _ = gpa.deinit();
defer core.deinit();
app.pipeline.release();
app.texture.release();
app.bind_group.release();
}
pub fn update(app: *App) !bool {
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
defer back_buffer_view.release();
// Poll for events (keyboard input, etc.)
var iter = core.pollEvents();
while (iter.next()) |event| {
if (event == .close) return true;
}
const encoder = core.device.createCommandEncoder(null);
defer encoder.release();
// Begin our render pass by clearing the pixels that were on the screen from the previous frame.
const color_attachment = gpu.RenderPassColorAttachment{
.view = back_buffer_view,
.clear_value = std.mem.zeroes(gpu.Color),
.load_op = .clear,
.store_op = .store,
};
const render_pass_descriptor = gpu.RenderPassDescriptor.init(.{
.color_attachments = &.{color_attachment},
});
const render_pass = encoder.beginRenderPass(&render_pass_descriptor);
defer render_pass.release();
// Render using our pipeline
render_pass.setPipeline(app.pipeline);
render_pass.setBindGroup(0, app.bind_group, &.{});
render_pass.draw(6, 1, 0, 0); // Tell the GPU to draw 6 vertices, one object
render_pass.end();
// Submit all the commands to the GPU and render the frame.
var command = encoder.finish(null);
defer command.release();
core.queue.submit(&[_]*gpu.CommandBuffer{command});
core.swap_chain.present();
// update the window title every second to have the FPS
if (app.title_timer.read() >= 1.0) {
app.title_timer.reset();
try core.printTitle("Image [ {d}fps ] [ Input {d}hz ]", .{
core.frameRate(),
core.inputRate(),
});
}
return false;
}
fn rgb24ToRgba32(allocator: std.mem.Allocator, in: []zigimg.color.Rgb24) !zigimg.color.PixelStorage {
const out = try zigimg.color.PixelStorage.init(allocator, .rgba32, in.len);
var i: usize = 0;
while (i < in.len) : (i += 1) {
out.rgba32[i] = zigimg.color.Rgba32{ .r = in[i].r, .g = in[i].g, .b = in[i].b, .a = 255 };
}
return out;
}

View file

@ -0,0 +1,49 @@
pub const Vertex = extern struct {
pos: @Vector(4, f32),
col: @Vector(4, f32),
uv: @Vector(2, f32),
};
pub const vertices = [_]Vertex{
.{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, 1, 1, 1 }, .col = .{ 0, 1, 1, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, -1, 1, 1 }, .col = .{ 0, 0, 1, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, -1, 1, 1 }, .col = .{ 1, 0, 1, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, 1, 1, 1 }, .col = .{ 1, 1, 1, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, -1, -1, 1 }, .col = .{ 0, 0, 0, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, 1, -1, 1 }, .col = .{ 1, 1, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, -1, -1, 1 }, .col = .{ 1, 0, 0, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, 1, -1, 1 }, .col = .{ 0, 1, 0, 1 }, .uv = .{ 0, 0 } },
};

View file

@ -0,0 +1,210 @@
const std = @import("std");
const core = @import("mach").core;
const gpu = core.gpu;
const zm = @import("zmath");
const Vertex = @import("cube_mesh.zig").Vertex;
const vertices = @import("cube_mesh.zig").vertices;
const UniformBufferObject = struct {
mat: zm.Mat,
};
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
title_timer: core.Timer,
timer: core.Timer,
pipeline: *gpu.RenderPipeline,
vertex_buffer: *gpu.Buffer,
uniform_buffer: *gpu.Buffer,
bind_group: *gpu.BindGroup,
pub const App = @This();
pub const mach_core_options = core.ComptimeOptions{
.use_wgpu = false,
.use_sysgpu = true,
};
pub fn init(app: *App) !void {
try core.init(.{});
app.timer = try core.Timer.start();
const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl"));
const vertex_attributes = [_]gpu.VertexAttribute{
.{ .format = .float32x4, .offset = @offsetOf(Vertex, "pos"), .shader_location = 0 },
.{ .format = .float32x2, .offset = @offsetOf(Vertex, "uv"), .shader_location = 1 },
};
const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{
.array_stride = @sizeOf(Vertex),
.step_mode = .vertex,
.attributes = &vertex_attributes,
});
const color_target = gpu.ColorTargetState{
.format = core.descriptor.format,
.write_mask = gpu.ColorWriteMaskFlags.all,
};
const fragment = gpu.FragmentState.init(.{
.module = shader_module,
.entry_point = "frag_main",
.targets = &.{color_target},
});
const bgle = gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, true, 0);
const bgl = core.device.createBindGroupLayout(
&gpu.BindGroupLayout.Descriptor.init(.{
.entries = &.{bgle},
}),
);
const bind_group_layouts = [_]*gpu.BindGroupLayout{bgl};
const pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{
.bind_group_layouts = &bind_group_layouts,
}));
const pipeline_descriptor = gpu.RenderPipeline.Descriptor{
.fragment = &fragment,
.layout = pipeline_layout,
.vertex = gpu.VertexState.init(.{
.module = shader_module,
.entry_point = "vertex_main",
.buffers = &.{vertex_buffer_layout},
}),
.primitive = .{
.cull_mode = .back,
},
};
const vertex_buffer = core.device.createBuffer(&.{
.usage = .{ .vertex = true },
.size = @sizeOf(Vertex) * vertices.len,
.mapped_at_creation = .true,
});
const vertex_mapped = vertex_buffer.getMappedRange(Vertex, 0, vertices.len);
@memcpy(vertex_mapped.?, vertices[0..]);
vertex_buffer.unmap();
const x_count = 4;
const y_count = 4;
const num_instances = x_count * y_count;
const uniform_buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .uniform = true },
.size = @sizeOf(UniformBufferObject) * num_instances,
.mapped_at_creation = .false,
});
const bind_group = core.device.createBindGroup(
&gpu.BindGroup.Descriptor.init(.{
.layout = bgl,
.entries = &.{
gpu.BindGroup.Entry.buffer(0, uniform_buffer, 0, @sizeOf(UniformBufferObject) * num_instances, @sizeOf(UniformBufferObject)),
},
}),
);
app.title_timer = try core.Timer.start();
app.pipeline = core.device.createRenderPipeline(&pipeline_descriptor);
app.vertex_buffer = vertex_buffer;
app.uniform_buffer = uniform_buffer;
app.bind_group = bind_group;
shader_module.release();
pipeline_layout.release();
bgl.release();
}
pub fn deinit(app: *App) void {
defer _ = gpa.deinit();
defer core.deinit();
app.pipeline.release();
app.vertex_buffer.release();
app.bind_group.release();
app.uniform_buffer.release();
}
pub fn update(app: *App) !bool {
var iter = core.pollEvents();
while (iter.next()) |event| {
switch (event) {
.key_press => |ev| {
if (ev.key == .space) return true;
},
.close => return true,
else => {},
}
}
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
const color_attachment = gpu.RenderPassColorAttachment{
.view = back_buffer_view,
.clear_value = std.mem.zeroes(gpu.Color),
.load_op = .clear,
.store_op = .store,
};
const encoder = core.device.createCommandEncoder(null);
const render_pass_info = gpu.RenderPassDescriptor.init(.{
.color_attachments = &.{color_attachment},
});
{
const proj = zm.perspectiveFovRh(
(std.math.pi / 3.0),
@as(f32, @floatFromInt(core.descriptor.width)) / @as(f32, @floatFromInt(core.descriptor.height)),
10,
30,
);
var ubos: [16]UniformBufferObject = undefined;
const time = app.timer.read();
const step: f32 = 4.0;
var m: u8 = 0;
var x: u8 = 0;
while (x < 4) : (x += 1) {
var y: u8 = 0;
while (y < 4) : (y += 1) {
const trans = zm.translation(step * (@as(f32, @floatFromInt(x)) - 2.0 + 0.5), step * (@as(f32, @floatFromInt(y)) - 2.0 + 0.5), -20);
const localTime = time + @as(f32, @floatFromInt(m)) * 0.5;
const model = zm.mul(zm.mul(zm.mul(zm.rotationX(localTime * (std.math.pi / 2.1)), zm.rotationY(localTime * (std.math.pi / 0.9))), zm.rotationZ(localTime * (std.math.pi / 1.3))), trans);
const mvp = zm.mul(model, proj);
const ubo = UniformBufferObject{
.mat = mvp,
};
ubos[m] = ubo;
m += 1;
}
}
encoder.writeBuffer(app.uniform_buffer, 0, &ubos);
}
const pass = encoder.beginRenderPass(&render_pass_info);
pass.setPipeline(app.pipeline);
pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len);
pass.setBindGroup(0, app.bind_group, &.{0});
pass.draw(vertices.len, 16, 0, 0);
pass.end();
pass.release();
var command = encoder.finish(null);
encoder.release();
const queue = core.queue;
queue.submit(&[_]*gpu.CommandBuffer{command});
command.release();
core.swap_chain.present();
back_buffer_view.release();
// update the window title every second
if (app.title_timer.read() >= 1.0) {
app.title_timer.reset();
try core.printTitle("Instanced Cube [ {d}fps ] [ Input {d}hz ]", .{
core.frameRate(),
core.inputRate(),
});
}
return false;
}

View file

@ -0,0 +1,25 @@
@binding(0) @group(0) var<uniform> ubos : array<mat4x4<f32>, 16>;
struct VertexOutput {
@builtin(position) position_clip : vec4<f32>,
@location(0) fragUV : vec2<f32>,
@location(1) fragPosition: vec4<f32>,
};
@vertex
fn vertex_main(@builtin(instance_index) instanceIdx : u32,
@location(0) position : vec4<f32>,
@location(1) uv : vec2<f32>) -> VertexOutput {
var output : VertexOutput;
output.position_clip = ubos[instanceIdx] * position;
output.fragUV = uv;
output.fragPosition = 0.5 * (position + vec4<f32>(1.0, 1.0, 1.0, 1.0));
return output;
}
@fragment fn frag_main(
@location(0) fragUV: vec2<f32>,
@location(1) fragPosition: vec4<f32>
) -> @location(0) vec4<f32> {
return fragPosition;
}

View file

@ -0,0 +1,16 @@
@group(0) @binding(0) var<storage, read_write> output: array<f32>;
@compute @workgroup_size(64, 1, 1)
fn main(
@builtin(global_invocation_id)
global_id : vec3<u32>,
@builtin(local_invocation_id)
local_id : vec3<u32>,
) {
if (global_id.x >= arrayLength(&output)) {
return;
}
output[global_id.x] =
f32(global_id.x) * 1000. + f32(local_id.x);
}

View file

@ -0,0 +1,106 @@
const std = @import("std");
const core = @import("mach").core;
const gpu = core.gpu;
pub const App = @This();
pub const mach_core_options = core.ComptimeOptions{
.use_wgpu = false,
.use_sysgpu = true,
};
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const workgroup_size = 64;
const buffer_size = 1000;
pub fn init(app: *App) !void {
try core.init(.{});
app.* = .{};
const output = core.device.createBuffer(&.{
.usage = .{ .storage = true, .copy_src = true },
.size = buffer_size * @sizeOf(f32),
.mapped_at_creation = .false,
});
defer output.release();
const staging = core.device.createBuffer(&.{
.usage = .{ .map_read = true, .copy_dst = true },
.size = buffer_size * @sizeOf(f32),
.mapped_at_creation = .false,
});
defer staging.release();
const compute_module = core.device.createShaderModuleWGSL("main.wgsl", @embedFile("main.wgsl"));
const compute_pipeline = core.device.createComputePipeline(&gpu.ComputePipeline.Descriptor{ .compute = gpu.ProgrammableStageDescriptor{
.module = compute_module,
.entry_point = "main",
} });
defer compute_pipeline.release();
const layout = compute_pipeline.getBindGroupLayout(0);
defer layout.release();
const compute_bind_group = core.device.createBindGroup(&gpu.BindGroup.Descriptor.init(.{
.layout = layout,
.entries = &.{
gpu.BindGroup.Entry.buffer(0, output, 0, buffer_size * @sizeOf(f32), @sizeOf(f32)),
},
}));
defer compute_bind_group.release();
compute_module.release();
const encoder = core.device.createCommandEncoder(null);
const compute_pass = encoder.beginComputePass(null);
compute_pass.setPipeline(compute_pipeline);
compute_pass.setBindGroup(0, compute_bind_group, &.{});
compute_pass.dispatchWorkgroups(try std.math.divCeil(u32, buffer_size, workgroup_size), 1, 1);
compute_pass.end();
compute_pass.release();
encoder.copyBufferToBuffer(output, 0, staging, 0, buffer_size * @sizeOf(f32));
var command = encoder.finish(null);
encoder.release();
var response: gpu.Buffer.MapAsyncStatus = undefined;
const callback = (struct {
pub inline fn callback(ctx: *gpu.Buffer.MapAsyncStatus, status: gpu.Buffer.MapAsyncStatus) void {
ctx.* = status;
}
}).callback;
var queue = core.queue;
queue.submit(&[_]*gpu.CommandBuffer{command});
command.release();
staging.mapAsync(.{ .read = true }, 0, buffer_size * @sizeOf(f32), &response, callback);
while (true) {
if (response == gpu.Buffer.MapAsyncStatus.success) {
break;
} else {
core.device.tick();
}
}
const staging_mapped = staging.getConstMappedRange(f32, 0, buffer_size);
for (staging_mapped.?) |v| {
std.debug.print("{d} ", .{v});
}
std.debug.print("\n", .{});
staging.unmap();
}
pub fn deinit(app: *App) void {
_ = app;
defer _ = gpa.deinit();
core.deinit();
}
pub fn update(_: *App) !bool {
return true;
}

View file

@ -0,0 +1,931 @@
const std = @import("std");
const core = @import("mach").core;
const gpu = core.gpu;
const m3d = @import("model3d");
const zm = @import("zmath");
const assets = @import("assets");
const VertexWriter = @import("vertex_writer.zig").VertexWriter;
pub const App = @This();
pub const mach_core_options = core.ComptimeOptions{
.use_wgpu = false,
.use_sysgpu = true,
};
const Vec4 = [4]f32;
const Vec3 = [3]f32;
const Vec2 = [2]f32;
const Mat4 = [4]Vec4;
fn Dimensions2D(comptime T: type) type {
return struct {
width: T,
height: T,
};
}
const Vertex = extern struct {
position: Vec3,
normal: Vec3,
};
const Model = struct {
vertex_count: u32,
index_count: u32,
vertex_buffer: *gpu.Buffer,
index_buffer: *gpu.Buffer,
};
const Material = struct {
const Params = extern struct {
roughness: f32,
metallic: f32,
color: Vec3,
};
name: []const u8,
params: Params,
};
const PressedKeys = packed struct(u16) {
right: bool = false,
left: bool = false,
up: bool = false,
down: bool = false,
padding: u12 = undefined,
pub inline fn areKeysPressed(self: @This()) bool {
return (self.up or self.down or self.left or self.right);
}
pub inline fn clear(self: *@This()) void {
self.right = false;
self.left = false;
self.up = false;
self.down = false;
}
};
const Camera = struct {
const Matrices = struct {
perspective: Mat4 = [1]Vec4{[1]f32{0.0} ** 4} ** 4,
view: Mat4 = [1]Vec4{[1]f32{0.0} ** 4} ** 4,
};
rotation: Vec3 = .{ 0.0, 0.0, 0.0 },
position: Vec3 = .{ 0.0, 0.0, 0.0 },
view_position: Vec4 = .{ 0.0, 0.0, 0.0, 0.0 },
fov: f32 = 0.0,
znear: f32 = 0.0,
zfar: f32 = 0.0,
rotation_speed: f32 = 0.0,
movement_speed: f32 = 0.0,
updated: bool = false,
matrices: Matrices = .{},
pub fn calculateMovement(self: *@This(), pressed_keys: PressedKeys) void {
std.debug.assert(pressed_keys.areKeysPressed());
const rotation_radians = Vec3{
toRadians(self.rotation[0]),
toRadians(self.rotation[1]),
toRadians(self.rotation[2]),
};
var camera_front = zm.Vec{ -zm.cos(rotation_radians[0]) * zm.sin(rotation_radians[1]), zm.sin(rotation_radians[0]), zm.cos(rotation_radians[0]) * zm.cos(rotation_radians[1]), 0 };
camera_front = zm.normalize3(camera_front);
if (pressed_keys.up) {
camera_front[0] *= self.movement_speed;
camera_front[1] *= self.movement_speed;
camera_front[2] *= self.movement_speed;
self.position = Vec3{
self.position[0] + camera_front[0],
self.position[1] + camera_front[1],
self.position[2] + camera_front[2],
};
}
if (pressed_keys.down) {
camera_front[0] *= self.movement_speed;
camera_front[1] *= self.movement_speed;
camera_front[2] *= self.movement_speed;
self.position = Vec3{
self.position[0] - camera_front[0],
self.position[1] - camera_front[1],
self.position[2] - camera_front[2],
};
}
if (pressed_keys.right) {
camera_front = zm.cross3(.{ 0.0, 1.0, 0.0, 0.0 }, camera_front);
camera_front = zm.normalize3(camera_front);
camera_front[0] *= self.movement_speed;
camera_front[1] *= self.movement_speed;
camera_front[2] *= self.movement_speed;
self.position = Vec3{
self.position[0] - camera_front[0],
self.position[1] - camera_front[1],
self.position[2] - camera_front[2],
};
}
if (pressed_keys.left) {
camera_front = zm.cross3(.{ 0.0, 1.0, 0.0, 0.0 }, camera_front);
camera_front = zm.normalize3(camera_front);
camera_front[0] *= self.movement_speed;
camera_front[1] *= self.movement_speed;
camera_front[2] *= self.movement_speed;
self.position = Vec3{
self.position[0] + camera_front[0],
self.position[1] + camera_front[1],
self.position[2] + camera_front[2],
};
}
self.updateViewMatrix();
}
fn updateViewMatrix(self: *@This()) void {
const rotation_x = zm.rotationX(toRadians(self.rotation[2]));
const rotation_y = zm.rotationY(toRadians(self.rotation[1]));
const rotation_z = zm.rotationZ(toRadians(self.rotation[0]));
const rotation_matrix = zm.mul(rotation_z, zm.mul(rotation_x, rotation_y));
const translation_matrix: zm.Mat = zm.translationV(.{
self.position[0],
self.position[1],
self.position[2],
0,
});
const view = zm.mul(translation_matrix, rotation_matrix);
self.matrices.view[0] = view[0];
self.matrices.view[1] = view[1];
self.matrices.view[2] = view[2];
self.matrices.view[3] = view[3];
self.view_position = .{
-self.position[0],
self.position[1],
-self.position[2],
0.0,
};
self.updated = true;
}
pub fn setMovementSpeed(self: *@This(), speed: f32) void {
self.movement_speed = speed;
}
pub fn setPerspective(self: *@This(), fov: f32, aspect: f32, znear: f32, zfar: f32) void {
self.fov = fov;
self.znear = znear;
self.zfar = zfar;
const perspective = zm.perspectiveFovRhGl(toRadians(fov), aspect, znear, zfar);
self.matrices.perspective[0] = perspective[0];
self.matrices.perspective[1] = perspective[1];
self.matrices.perspective[2] = perspective[2];
self.matrices.perspective[3] = perspective[3];
}
pub fn setRotationSpeed(self: *@This(), speed: f32) void {
self.rotation_speed = speed;
}
pub fn setRotation(self: *@This(), rotation: Vec3) void {
self.rotation = rotation;
self.updateViewMatrix();
}
pub fn rotate(self: *@This(), delta: Vec2) void {
self.rotation[0] -= delta[1];
self.rotation[1] -= delta[0];
self.updateViewMatrix();
}
pub fn setPosition(self: *@This(), position: Vec3) void {
self.position = .{
position[0],
-position[1],
position[2],
};
self.updateViewMatrix();
}
};
const UniformBuffers = struct {
const Params = struct {
buffer: *gpu.Buffer,
buffer_size: u64,
model_size: u64,
};
const Buffer = struct {
buffer: *gpu.Buffer,
size: u32,
};
ubo_matrices: Buffer,
ubo_params: Buffer,
material_params: Params,
object_params: Params,
};
const UboParams = struct {
lights: [4]Vec4,
};
const UboMatrices = extern struct {
projection: Mat4,
model: Mat4,
view: Mat4,
camera_position: Vec3,
};
const grid_element_count = grid_dimensions * grid_dimensions;
const MaterialParamsDynamic = extern struct {
roughness: f32 = 0,
metallic: f32 = 0,
color: Vec3 = .{ 0, 0, 0 },
padding: [236]u8 = [1]u8{0} ** 236,
};
const MaterialParamsDynamicGrid = [grid_element_count]MaterialParamsDynamic;
const ObjectParamsDynamic = extern struct {
position: Vec3 = .{ 0, 0, 0 },
padding: [244]u8 = [1]u8{0} ** 244,
};
const ObjectParamsDynamicGrid = [grid_element_count]ObjectParamsDynamic;
//
// Globals
//
const material_names = [11][:0]const u8{
"Gold", "Copper", "Chromium", "Nickel", "Titanium", "Cobalt", "Platinum",
// Testing materials
"White", "Red", "Blue", "Black",
};
const object_names = [5][:0]const u8{ "Sphere", "Teapot", "Torusknot", "Venus", "Stanford Dragon" };
const materials = [_]Material{
.{ .name = "Gold", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 1.0, 0.765557, 0.336057 } } },
.{ .name = "Copper", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.955008, 0.637427, 0.538163 } } },
.{ .name = "Chromium", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.549585, 0.556114, 0.554256 } } },
.{ .name = "Nickel", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 1.0, 0.608679, 0.525649 } } },
.{ .name = "Titanium", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.541931, 0.496791, 0.449419 } } },
.{ .name = "Cobalt", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.662124, 0.654864, 0.633732 } } },
.{ .name = "Platinum", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.672411, 0.637331, 0.585456 } } },
// Testing colors
.{ .name = "White", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 1.0, 1.0, 1.0 } } },
.{ .name = "Red", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 1.0, 0.0, 0.0 } } },
.{ .name = "Blue", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.0, 0.0, 1.0 } } },
.{ .name = "Black", .params = .{ .roughness = 0.1, .metallic = 1.0, .color = .{ 0.0, 0.0, 0.0 } } },
};
const grid_dimensions = 7;
const model_embeds = [_][:0]const u8{
assets.sphere_m3d,
assets.teapot_m3d,
assets.torusknot_m3d,
assets.venus_m3d,
assets.stanford_dragon_m3d,
};
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
//
// Member variables
//
title_timer: core.Timer,
timer: core.Timer,
camera: Camera,
render_pipeline: *gpu.RenderPipeline,
render_pass_descriptor: gpu.RenderPassDescriptor,
bind_group: *gpu.BindGroup,
color_attachment: gpu.RenderPassColorAttachment,
depth_stencil_attachment_description: gpu.RenderPassDepthStencilAttachment,
depth_texture: *gpu.Texture,
depth_texture_view: *gpu.TextureView,
pressed_keys: PressedKeys,
models: [5]Model,
ubo_params: UboParams,
ubo_matrices: UboMatrices,
uniform_buffers: UniformBuffers,
material_params_dynamic: MaterialParamsDynamicGrid = [1]MaterialParamsDynamic{.{}} ** grid_element_count,
object_params_dynamic: ObjectParamsDynamicGrid = [1]ObjectParamsDynamic{.{}} ** grid_element_count,
uniform_buffers_dirty: bool,
buffers_bound: bool,
is_paused: bool,
current_material_index: usize,
current_object_index: usize,
mouse_position: core.Position,
is_rotating: bool,
//
// Functions
//
pub fn init(app: *App) !void {
try core.init(.{});
app.timer = try core.Timer.start();
app.title_timer = try core.Timer.start();
app.pressed_keys = .{};
app.buffers_bound = false;
app.is_paused = false;
app.uniform_buffers_dirty = false;
app.current_material_index = 0;
app.current_object_index = 0;
app.mouse_position = .{ .x = 0, .y = 0 };
app.is_rotating = false;
setupCamera(app);
try loadModels(std.heap.c_allocator, app);
prepareUniformBuffers(app);
setupPipeline(app);
setupRenderPass(app);
app.printControls();
}
pub fn deinit(app: *App) void {
defer _ = gpa.deinit();
defer core.deinit();
app.bind_group.release();
app.render_pipeline.release();
app.depth_texture_view.release();
app.depth_texture.release();
for (app.models) |model| {
model.index_buffer.release();
model.vertex_buffer.release();
}
app.uniform_buffers.ubo_matrices.buffer.release();
app.uniform_buffers.ubo_params.buffer.release();
app.uniform_buffers.material_params.buffer.release();
app.uniform_buffers.object_params.buffer.release();
}
pub fn update(app: *App) !bool {
var iter = core.pollEvents();
while (iter.next()) |event| {
app.updateUI(event);
switch (event) {
.mouse_motion => |ev| {
if (app.is_rotating) {
const delta = Vec2{
@as(f32, @floatCast((app.mouse_position.x - ev.pos.x) * app.camera.rotation_speed)),
@as(f32, @floatCast((app.mouse_position.y - ev.pos.y) * app.camera.rotation_speed)),
};
app.mouse_position = ev.pos;
app.camera.rotate(delta);
app.uniform_buffers_dirty = true;
}
},
.mouse_press => |ev| {
if (ev.button == .left) {
app.is_rotating = true;
app.mouse_position = ev.pos;
}
},
.mouse_release => |ev| {
if (ev.button == .left) {
app.is_rotating = false;
}
},
.key_press, .key_repeat => |ev| {
const key = ev.key;
if (key == .up or key == .w) app.pressed_keys.up = true;
if (key == .down or key == .s) app.pressed_keys.down = true;
if (key == .left or key == .a) app.pressed_keys.left = true;
if (key == .right or key == .d) app.pressed_keys.right = true;
},
.framebuffer_resize => |ev| {
app.depth_texture_view.release();
app.depth_texture.release();
app.depth_texture = core.device.createTexture(&gpu.Texture.Descriptor{
.usage = .{ .render_attachment = true },
.format = .depth24_plus_stencil8,
.sample_count = 1,
.size = .{
.width = ev.width,
.height = ev.height,
.depth_or_array_layers = 1,
},
});
app.depth_texture_view = app.depth_texture.createView(&gpu.TextureView.Descriptor{
.format = .depth24_plus_stencil8,
.dimension = .dimension_2d,
.array_layer_count = 1,
.aspect = .all,
});
app.depth_stencil_attachment_description = gpu.RenderPassDepthStencilAttachment{
.view = app.depth_texture_view,
.depth_load_op = .clear,
.depth_store_op = .store,
.depth_clear_value = 1.0,
.stencil_clear_value = 0,
.stencil_load_op = .clear,
.stencil_store_op = .store,
};
const aspect_ratio = @as(f32, @floatFromInt(ev.width)) / @as(f32, @floatFromInt(ev.height));
app.camera.setPerspective(60.0, aspect_ratio, 0.1, 256.0);
app.uniform_buffers_dirty = true;
},
.close => return true,
else => {},
}
}
if (app.pressed_keys.areKeysPressed()) {
app.camera.calculateMovement(app.pressed_keys);
app.pressed_keys.clear();
app.uniform_buffers_dirty = true;
}
if (app.uniform_buffers_dirty) {
updateUniformBuffers(app);
app.uniform_buffers_dirty = false;
}
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
app.color_attachment.view = back_buffer_view;
app.render_pass_descriptor = gpu.RenderPassDescriptor{
.color_attachment_count = 1,
.color_attachments = &[_]gpu.RenderPassColorAttachment{app.color_attachment},
.depth_stencil_attachment = &app.depth_stencil_attachment_description,
};
const encoder = core.device.createCommandEncoder(null);
const current_model = app.models[app.current_object_index];
const pass = encoder.beginRenderPass(&app.render_pass_descriptor);
const dimensions = Dimensions2D(f32){
.width = @as(f32, @floatFromInt(core.descriptor.width)),
.height = @as(f32, @floatFromInt(core.descriptor.height)),
};
pass.setViewport(
0,
0,
dimensions.width,
dimensions.height,
0.0,
1.0,
);
pass.setScissorRect(0, 0, core.descriptor.width, core.descriptor.height);
pass.setPipeline(app.render_pipeline);
if (!app.is_paused) {
app.updateLights();
}
var i: usize = 0;
while (i < (grid_dimensions * grid_dimensions)) : (i += 1) {
const alignment = 256;
const dynamic_offset: u32 = @as(u32, @intCast(i)) * alignment;
const dynamic_offsets = [2]u32{ dynamic_offset, dynamic_offset };
pass.setBindGroup(0, app.bind_group, &dynamic_offsets);
if (!app.buffers_bound) {
pass.setVertexBuffer(0, current_model.vertex_buffer, 0, @sizeOf(Vertex) * current_model.vertex_count);
pass.setIndexBuffer(current_model.index_buffer, .uint32, 0, gpu.whole_size);
app.buffers_bound = true;
}
pass.drawIndexed(
current_model.index_count, // index_count
1, // instance_count
0, // first_index
0, // base_vertex
0, // first_instance
);
}
pass.end();
pass.release();
var command = encoder.finish(null);
encoder.release();
const queue = core.queue;
queue.submit(&[_]*gpu.CommandBuffer{command});
command.release();
core.swap_chain.present();
back_buffer_view.release();
app.buffers_bound = false;
// update the window title every second
if (app.title_timer.read() >= 1.0) {
app.title_timer.reset();
try core.printTitle("PBR Basic [ {d}fps ] [ Input {d}hz ]", .{
core.frameRate(),
core.inputRate(),
});
}
return false;
}
fn prepareUniformBuffers(app: *App) void {
comptime {
std.debug.assert(@sizeOf(ObjectParamsDynamic) == 256);
std.debug.assert(@sizeOf(MaterialParamsDynamic) == 256);
}
app.uniform_buffers.ubo_matrices.size = roundToMultipleOf4(u32, @as(u32, @intCast(@sizeOf(UboMatrices)))) + 4;
app.uniform_buffers.ubo_matrices.buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .uniform = true },
.size = app.uniform_buffers.ubo_matrices.size,
.mapped_at_creation = .false,
});
app.uniform_buffers.ubo_params.size = roundToMultipleOf4(u32, @as(u32, @intCast(@sizeOf(UboParams)))) + 4;
app.uniform_buffers.ubo_params.buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .uniform = true },
.size = app.uniform_buffers.ubo_params.size,
.mapped_at_creation = .false,
});
//
// Material parameter uniform buffer
//
app.uniform_buffers.material_params.model_size = @sizeOf(Vec2) + @sizeOf(Vec3);
app.uniform_buffers.material_params.buffer_size = calculateConstantBufferByteSize(@sizeOf(MaterialParamsDynamicGrid));
std.debug.assert(app.uniform_buffers.material_params.buffer_size >= app.uniform_buffers.material_params.model_size);
app.uniform_buffers.material_params.buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .uniform = true },
.size = app.uniform_buffers.material_params.buffer_size,
.mapped_at_creation = .false,
});
//
// Object parameter uniform buffer
//
app.uniform_buffers.object_params.model_size = @sizeOf(Vec3) + 4;
app.uniform_buffers.object_params.buffer_size = calculateConstantBufferByteSize(@sizeOf(MaterialParamsDynamicGrid)) + 4;
std.debug.assert(app.uniform_buffers.object_params.buffer_size >= app.uniform_buffers.object_params.model_size);
app.uniform_buffers.object_params.buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .uniform = true },
.size = app.uniform_buffers.object_params.buffer_size,
.mapped_at_creation = .false,
});
app.updateUniformBuffers();
app.updateDynamicUniformBuffer();
app.updateLights();
}
fn updateDynamicUniformBuffer(app: *App) void {
var index: u32 = 0;
var y: usize = 0;
while (y < grid_dimensions) : (y += 1) {
var x: usize = 0;
while (x < grid_dimensions) : (x += 1) {
const grid_dimensions_float = @as(f32, @floatFromInt(grid_dimensions));
app.object_params_dynamic[index].position[0] = (@as(f32, @floatFromInt(x)) - (grid_dimensions_float / 2) * 2.5);
app.object_params_dynamic[index].position[1] = 0;
app.object_params_dynamic[index].position[2] = (@as(f32, @floatFromInt(y)) - (grid_dimensions_float / 2) * 2.5);
app.material_params_dynamic[index].metallic = zm.clamp(@as(f32, @floatFromInt(x)) / (grid_dimensions_float - 1), 0.1, 1.0);
app.material_params_dynamic[index].roughness = zm.clamp(@as(f32, @floatFromInt(y)) / (grid_dimensions_float - 1), 0.05, 1.0);
app.material_params_dynamic[index].color = materials[app.current_material_index].params.color;
index += 1;
}
}
const queue = core.queue;
queue.writeBuffer(
app.uniform_buffers.object_params.buffer,
0,
&app.object_params_dynamic,
);
queue.writeBuffer(
app.uniform_buffers.material_params.buffer,
0,
&app.material_params_dynamic,
);
}
fn updateUniformBuffers(app: *App) void {
app.ubo_matrices.projection = app.camera.matrices.perspective;
app.ubo_matrices.view = app.camera.matrices.view;
const rotation_degrees = if (app.current_object_index == 1) @as(f32, -45.0) else @as(f32, -90.0);
const model = zm.rotationY(rotation_degrees);
app.ubo_matrices.model[0] = model[0];
app.ubo_matrices.model[1] = model[1];
app.ubo_matrices.model[2] = model[2];
app.ubo_matrices.model[3] = model[3];
app.ubo_matrices.camera_position = .{
-app.camera.position[0],
-app.camera.position[1],
-app.camera.position[2],
};
const queue = core.queue;
queue.writeBuffer(app.uniform_buffers.ubo_matrices.buffer, 0, &[_]UboMatrices{app.ubo_matrices});
}
fn updateLights(app: *App) void {
const p: f32 = 15.0;
app.ubo_params.lights[0] = Vec4{ -p, -p * 0.5, -p, 1.0 };
app.ubo_params.lights[1] = Vec4{ -p, -p * 0.5, p, 1.0 };
app.ubo_params.lights[2] = Vec4{ p, -p * 0.5, p, 1.0 };
app.ubo_params.lights[3] = Vec4{ p, -p * 0.5, -p, 1.0 };
const base_value = toRadians(@mod(app.timer.read() * 0.1, 1.0) * 360.0);
app.ubo_params.lights[0][0] = @sin(base_value) * 20.0;
app.ubo_params.lights[0][2] = @cos(base_value) * 20.0;
app.ubo_params.lights[1][0] = @cos(base_value) * 20.0;
app.ubo_params.lights[1][1] = @sin(base_value) * 20.0;
const queue = core.queue;
queue.writeBuffer(
app.uniform_buffers.ubo_params.buffer,
0,
&[_]UboParams{app.ubo_params},
);
}
fn setupPipeline(app: *App) void {
comptime {
std.debug.assert(@sizeOf(Vertex) == @sizeOf(f32) * 6);
}
const bind_group_layout_entries = [_]gpu.BindGroupLayout.Entry{
.{
.binding = 0,
.visibility = .{ .vertex = true, .fragment = true },
.buffer = .{
.type = .uniform,
.has_dynamic_offset = .false,
.min_binding_size = app.uniform_buffers.ubo_matrices.size,
},
},
.{
.binding = 1,
.visibility = .{ .fragment = true },
.buffer = .{
.type = .uniform,
.has_dynamic_offset = .false,
.min_binding_size = app.uniform_buffers.ubo_params.size,
},
},
.{
.binding = 2,
.visibility = .{ .fragment = true },
.buffer = .{
.type = .uniform,
.has_dynamic_offset = .true,
.min_binding_size = app.uniform_buffers.material_params.model_size,
},
},
.{
.binding = 3,
.visibility = .{ .vertex = true },
.buffer = .{
.type = .uniform,
.has_dynamic_offset = .true,
.min_binding_size = app.uniform_buffers.object_params.model_size,
},
},
};
const bind_group_layout = core.device.createBindGroupLayout(
&gpu.BindGroupLayout.Descriptor.init(.{
.entries = bind_group_layout_entries[0..],
}),
);
defer bind_group_layout.release();
const bind_group_layouts = [_]*gpu.BindGroupLayout{bind_group_layout};
const pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{
.bind_group_layouts = &bind_group_layouts,
}));
defer pipeline_layout.release();
const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{
.array_stride = @sizeOf(Vertex),
.step_mode = .vertex,
.attributes = &.{
.{ .format = .float32x3, .offset = @offsetOf(Vertex, "position"), .shader_location = 0 },
.{ .format = .float32x3, .offset = @offsetOf(Vertex, "normal"), .shader_location = 1 },
},
});
const blend_component_descriptor = gpu.BlendComponent{
.operation = .add,
.src_factor = .one,
.dst_factor = .zero,
};
const color_target_state = gpu.ColorTargetState{
.format = core.descriptor.format,
.blend = &.{
.color = blend_component_descriptor,
.alpha = blend_component_descriptor,
},
};
const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl"));
const pipeline_descriptor = gpu.RenderPipeline.Descriptor{
.layout = pipeline_layout,
.primitive = .{
.cull_mode = .back,
},
.depth_stencil = &.{
.format = .depth24_plus_stencil8,
.depth_write_enabled = .true,
.depth_compare = .less,
},
.fragment = &gpu.FragmentState.init(.{
.module = shader_module,
.entry_point = "frag_main",
.targets = &.{color_target_state},
}),
.vertex = gpu.VertexState.init(.{
.module = shader_module,
.entry_point = "vertex_main",
.buffers = &.{vertex_buffer_layout},
}),
};
app.render_pipeline = core.device.createRenderPipeline(&pipeline_descriptor);
shader_module.release();
{
const bind_group_entries = [_]gpu.BindGroup.Entry{
.{
.binding = 0,
.buffer = app.uniform_buffers.ubo_matrices.buffer,
.size = app.uniform_buffers.ubo_matrices.size,
},
.{
.binding = 1,
.buffer = app.uniform_buffers.ubo_params.buffer,
.size = app.uniform_buffers.ubo_params.size,
},
.{
.binding = 2,
.buffer = app.uniform_buffers.material_params.buffer,
.size = app.uniform_buffers.material_params.model_size,
},
.{
.binding = 3,
.buffer = app.uniform_buffers.object_params.buffer,
.size = app.uniform_buffers.object_params.model_size,
},
};
app.bind_group = core.device.createBindGroup(
&gpu.BindGroup.Descriptor.init(.{
.layout = bind_group_layout,
.entries = &bind_group_entries,
}),
);
}
}
fn setupRenderPass(app: *App) void {
app.color_attachment = gpu.RenderPassColorAttachment{
.clear_value = .{
.r = 0.0,
.g = 0.0,
.b = 0.0,
.a = 0.0,
},
.load_op = .clear,
.store_op = .store,
};
app.depth_texture = core.device.createTexture(&.{
.usage = .{ .render_attachment = true, .copy_src = true },
.format = .depth24_plus_stencil8,
.sample_count = 1,
.size = .{
.width = core.descriptor.width,
.height = core.descriptor.height,
.depth_or_array_layers = 1,
},
});
app.depth_texture_view = app.depth_texture.createView(&.{
.format = .depth24_plus_stencil8,
.dimension = .dimension_2d,
.array_layer_count = 1,
.aspect = .all,
});
app.depth_stencil_attachment_description = gpu.RenderPassDepthStencilAttachment{
.view = app.depth_texture_view,
.depth_load_op = .clear,
.depth_store_op = .store,
.depth_clear_value = 1.0,
.stencil_clear_value = 0,
.stencil_load_op = .clear,
.stencil_store_op = .store,
};
}
fn loadModels(allocator: std.mem.Allocator, app: *App) !void {
for (model_embeds, 0..) |model_data, model_data_i| {
const m3d_model = m3d.load(model_data, null, null, null) orelse return error.LoadModelFailed;
const vertex_count = m3d_model.handle.numvertex;
const face_count = m3d_model.handle.numface;
var model: *Model = &app.models[model_data_i];
model.index_count = face_count * 3;
var vertex_writer = try VertexWriter(Vertex, u32).init(allocator, face_count * 3, vertex_count, face_count * 3);
defer vertex_writer.deinit(allocator);
const scale: f32 = 0.45;
const vertices = m3d_model.handle.vertex[0..vertex_count];
var i: usize = 0;
while (i < face_count) : (i += 1) {
const face = m3d_model.handle.face[i];
var x: usize = 0;
while (x < 3) : (x += 1) {
const vertex_index = face.vertex[x];
const normal_index = face.normal[x];
const vertex = Vertex{
.position = .{
vertices[vertex_index].x * scale,
vertices[vertex_index].y * scale,
vertices[vertex_index].z * scale,
},
.normal = .{
vertices[normal_index].x,
vertices[normal_index].y,
vertices[normal_index].z,
},
};
vertex_writer.put(vertex, vertex_index);
}
}
const vertex_buffer = vertex_writer.vertexBuffer();
const index_buffer = vertex_writer.indexBuffer();
model.vertex_count = @as(u32, @intCast(vertex_buffer.len));
model.vertex_buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .vertex = true },
.size = @sizeOf(Vertex) * model.vertex_count,
.mapped_at_creation = .false,
});
const queue = core.queue;
queue.writeBuffer(model.vertex_buffer, 0, vertex_buffer);
model.index_buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .index = true },
.size = @sizeOf(u32) * model.index_count,
.mapped_at_creation = .false,
});
queue.writeBuffer(model.index_buffer, 0, index_buffer);
}
}
fn printControls(app: *App) void {
std.debug.print("[controls]\n", .{});
std.debug.print("[p] paused: {}\n", .{app.is_paused});
std.debug.print("[m] material: {s}\n", .{material_names[app.current_material_index]});
std.debug.print("[o] object: {s}\n", .{object_names[app.current_object_index]});
}
fn updateUI(app: *App, event: core.Event) void {
switch (event) {
.key_press => |ev| {
var update_uniform_buffers: bool = false;
switch (ev.key) {
.p => app.is_paused = !app.is_paused,
.m => {
app.current_material_index = (app.current_material_index + 1) % material_names.len;
update_uniform_buffers = true;
},
.o => {
app.current_object_index = (app.current_object_index + 1) % object_names.len;
update_uniform_buffers = true;
},
else => return,
}
app.printControls();
if (update_uniform_buffers) {
updateDynamicUniformBuffer(app);
}
},
else => {},
}
}
fn setupCamera(app: *App) void {
app.camera = Camera{
.rotation_speed = 1.0,
.movement_speed = 1.0,
};
const aspect_ratio: f32 = @as(f32, @floatFromInt(core.descriptor.width)) / @as(f32, @floatFromInt(core.descriptor.height));
app.camera.setPosition(.{ 10.0, 6.0, 6.0 });
app.camera.setRotation(.{ 62.5, 90.0, 0.0 });
app.camera.setMovementSpeed(0.5);
app.camera.setPerspective(60.0, aspect_ratio, 0.1, 256.0);
app.camera.setRotationSpeed(0.25);
}
inline fn roundToMultipleOf4(comptime T: type, value: T) T {
return (value + 3) & ~@as(T, 3);
}
inline fn calculateConstantBufferByteSize(byte_size: usize) usize {
return (byte_size + 255) & ~@as(usize, 255);
}
inline fn toRadians(degrees: f32) f32 {
return degrees * (std.math.pi / 180.0);
}

View file

@ -0,0 +1,119 @@
@group(0) @binding(0) var<uniform> ubo : UBO;
@group(0) @binding(1) var<uniform> uboParams : UBOShared;
@group(0) @binding(2) var<uniform> material : MaterialParams;
@group(0) @binding(3) var<uniform> object : ObjectParams;
struct VertexOut {
@builtin(position) position_clip : vec4<f32>,
@location(0) fragPosition : vec3<f32>,
@location(1) fragNormal : vec3<f32>,
}
struct MaterialParams {
roughness : f32,
metallic : f32,
r : f32,
g : f32,
b : f32
}
struct UBOShared {
lights : array<vec4<f32>, 4>,
}
struct UBO {
projection : mat4x4<f32>,
model : mat4x4<f32>,
view : mat4x4<f32>,
camPos : vec3<f32>,
}
struct ObjectParams {
position : vec3<f32>
}
@vertex fn vertex_main(
@location(0) position : vec3<f32>,
@location(1) normal : vec3<f32>
) -> VertexOut {
var output : VertexOut;
var locPos = vec4<f32>(ubo.model * vec4<f32>(position, 1.0));
output.fragPosition = locPos.xyz + object.position;
output.fragNormal = mat3x3<f32>(ubo.model[0].xyz, ubo.model[1].xyz, ubo.model[2].xyz) * normal;
output.position_clip = ubo.projection * ubo.view * vec4<f32>(output.fragPosition, 1.0);
return output;
}
const PI : f32 = 3.14159265359;
fn material_color() -> vec3<f32> {
return vec3<f32>(material.r, material.g, material.b);
}
// Normal Distribution function --------------------------------------
fn D_GGX(dotNH : f32, roughness : f32) -> f32 {
var alpha : f32 = roughness * roughness;
var alpha2 : f32 = alpha * alpha;
var denom : f32 = dotNH * dotNH * (alpha2 - 1.0) + 1.0;
return alpha2 / (PI * denom * denom);
}
// Geometric Shadowing function --------------------------------------
fn G_SchlicksmithGGX(dotNL : f32, dotNV : f32, roughness : f32) -> f32 {
var r : f32 = roughness + 1.0;
var k : f32 = (r * r) / 8.0;
var GL : f32 = dotNL / (dotNL * (1.0 - k) + k);
var GV : f32 = dotNV / (dotNV * (1.0 - k) + k);
return GL * GV;
}
// Fresnel function ----------------------------------------------------
fn F_Schlick(cosTheta : f32, metallic : f32) -> vec3<f32> {
var F0 : vec3<f32> = mix(vec3<f32>(0.04), material_color(), metallic);
var F : vec3<f32> = F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
return F;
}
// Specular BRDF composition --------------------------------------------
fn BRDF(L : vec3<f32>, V : vec3<f32>, N : vec3<f32>, metallic : f32, roughness : f32) -> vec3<f32> {
var H : vec3<f32> = normalize(V + L);
var dotNV : f32 = clamp(dot(N, V), 0.0, 1.0);
var dotNL : f32 = clamp(dot(N, L), 0.0, 1.0);
var dotLH : f32 = clamp(dot(L, H), 0.0, 1.0);
var dotNH : f32 = clamp(dot(N, H), 0.0, 1.0);
var lightColor = vec3<f32>(1.0);
var color = vec3<f32>(0.0);
if(dotNL > 0.0) {
var rroughness : f32 = max(0.05, roughness);
// D = Normal distribution (Distribution of the microfacets)
var D : f32 = D_GGX(dotNH, roughness);
// G = Geometric shadowing term (Microfacets shadowing)
var G : f32 = G_SchlicksmithGGX(dotNL, dotNV, roughness);
// F = Fresnel factor (Reflectance depending on angle of incidence)
var F : vec3<f32> = F_Schlick(dotNV, metallic);
var spec : vec3<f32> = (D * F * G) / (4.0 * dotNL * dotNV);
color += spec * dotNL * lightColor;
}
return color;
}
// TODO - global variable declaration order
@fragment fn frag_main(
@location(0) position : vec3<f32>,
@location(1) normal: vec3<f32>
) -> @location(0) vec4<f32> {
var N : vec3<f32> = normalize(normal);
var V : vec3<f32> = normalize(ubo.camPos - position);
var Lo = vec3<f32>(0.0);
// Specular contribution
for(var i: i32 = 0; i < 4; i++) {
var L : vec3<f32> = normalize(uboParams.lights[i].xyz - position);
Lo += BRDF(L, V, N, material.metallic, material.roughness);
}
// Combine with ambient
var color : vec3<f32> = material_color() * 0.02;
color += Lo;
// Gamma correct
color = pow(color, vec3<f32>(0.4545));
return vec4<f32>(color, 1.0);
}

View file

@ -0,0 +1,188 @@
const std = @import("std");
/// Vertex writer manages the placement of vertices by tracking which are unique. If a duplicate vertex is added
/// with `put`, only it's index will be written to the index buffer.
/// `IndexType` should match the integer type used for the index buffer
pub fn VertexWriter(comptime VertexType: type, comptime IndexType: type) type {
return struct {
const MapEntry = struct {
packed_index: IndexType = null_index,
next_sparse: IndexType = null_index,
};
const null_index: IndexType = std.math.maxInt(IndexType);
vertices: []VertexType,
indices: []IndexType,
sparse_to_packed_map: []MapEntry,
/// Next index outside of the 1:1 mapping range for storing
/// position -> normal collisions
next_collision_index: IndexType,
/// Next packed index
next_packed_index: IndexType,
written_indices_count: IndexType,
/// Allocate storage and set default values
/// `sparse_vertices_count` is the number of vertices in the source before de-duplication / remapping
/// Put more succinctly, the largest index value in source index buffer
/// `max_vertex_count` is largest permutation of vertices assuming that {vertex, uv, normal} never map 1:1 and always
/// create a new mapping
pub fn init(
allocator: std.mem.Allocator,
indices_count: IndexType,
sparse_vertices_count: IndexType,
max_vertex_count: IndexType,
) !@This() {
var result: @This() = undefined;
result.vertices = try allocator.alloc(VertexType, max_vertex_count);
result.indices = try allocator.alloc(IndexType, indices_count);
result.sparse_to_packed_map = try allocator.alloc(MapEntry, max_vertex_count);
result.next_collision_index = sparse_vertices_count;
result.next_packed_index = 0;
result.written_indices_count = 0;
@memset(result.sparse_to_packed_map, .{});
return result;
}
pub fn put(self: *@This(), vertex: VertexType, sparse_index: IndexType) void {
if (self.sparse_to_packed_map[sparse_index].packed_index == null_index) {
// New start of chain, reserve a new packed index and add entry to `index_map`
const packed_index = self.next_packed_index;
self.sparse_to_packed_map[sparse_index].packed_index = packed_index;
self.vertices[packed_index] = vertex;
self.indices[self.written_indices_count] = packed_index;
self.written_indices_count += 1;
self.next_packed_index += 1;
return;
}
var previous_sparse_index: IndexType = undefined;
var current_sparse_index = sparse_index;
while (current_sparse_index != null_index) {
const packed_index = self.sparse_to_packed_map[current_sparse_index].packed_index;
if (std.mem.eql(u8, &std.mem.toBytes(self.vertices[packed_index]), &std.mem.toBytes(vertex))) {
// We already have a record for this vertex in our chain
self.indices[self.written_indices_count] = packed_index;
self.written_indices_count += 1;
return;
}
previous_sparse_index = current_sparse_index;
current_sparse_index = self.sparse_to_packed_map[current_sparse_index].next_sparse;
}
// This is a new mapping for the given sparse index
const packed_index = self.next_packed_index;
const remapped_sparse_index = self.next_collision_index;
self.indices[self.written_indices_count] = packed_index;
self.vertices[packed_index] = vertex;
self.sparse_to_packed_map[previous_sparse_index].next_sparse = remapped_sparse_index;
self.sparse_to_packed_map[remapped_sparse_index].packed_index = packed_index;
self.next_packed_index += 1;
self.next_collision_index += 1;
self.written_indices_count += 1;
}
pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void {
allocator.free(self.vertices);
allocator.free(self.indices);
allocator.free(self.sparse_to_packed_map);
}
pub fn indexBuffer(self: @This()) []IndexType {
return self.indices;
}
pub fn vertexBuffer(self: @This()) []VertexType {
return self.vertices[0..self.next_packed_index];
}
};
}
test "VertexWriter" {
const Vec3 = [3]f32;
const Vertex = extern struct {
position: Vec3,
normal: Vec3,
};
const expect = std.testing.expect;
const allocator = std.testing.allocator;
const Face = struct {
position: [3]u16,
normal: [3]u16,
};
const vertices = [_]Vec3{
Vec3{ 1.0, 0.0, 0.0 }, // 0: Position
Vec3{ 2.0, 0.0, 0.0 }, // 1: Position
Vec3{ 3.0, 0.0, 0.0 }, // 2: Position
Vec3{ 1.0, 0.0, 0.0 }, // 3: Normal
Vec3{ 4.0, 0.0, 0.0 }, // 4: Position
Vec3{ 0.0, 1.0, 0.0 }, // 5: Normal
Vec3{ 5.0, 0.0, 0.0 }, // 6: Position
Vec3{ 0.0, 0.0, 1.0 }, // 7: Normal
Vec3{ 1.0, 0.0, 1.0 }, // 8: Normal
Vec3{ 6.0, 0.0, 0.0 }, // 9: Position
};
const faces = [_]Face{
.{ .position = .{ 0, 4, 2 }, .normal = .{ 7, 5, 3 } },
.{ .position = .{ 2, 3, 9 }, .normal = .{ 3, 7, 8 } },
.{ .position = .{ 9, 2, 4 }, .normal = .{ 8, 7, 5 } },
.{ .position = .{ 2, 6, 1 }, .normal = .{ 3, 5, 7 } },
.{ .position = .{ 9, 6, 0 }, .normal = .{ 5, 7, 8 } },
};
var writer = try VertexWriter(Vertex, u32).init(
allocator,
faces.len * 3, // indices count
vertices.len, // original vertices count
faces.len * 3, // maximum vertices count
);
defer writer.deinit(allocator);
for (faces) |face| {
var x: usize = 0;
while (x < 3) : (x += 1) {
const position_index = face.position[x];
const position = vertices[position_index];
const normal = vertices[face.normal[x]];
const vertex = Vertex{
.position = position,
.normal = normal,
};
writer.put(vertex, position_index);
}
}
const indices = writer.indexBuffer();
try expect(indices.len == faces.len * 3);
// Face 0
try expect(indices[0] == 0); // (0, 7) New
try expect(indices[1] == 1); // (4, 5) New
try expect(indices[2] == 2); // (2, 3) New
// Face 1
try expect(indices[3 + 0] == 2); // (2, 3) Duplicate - Reuse index
try expect(indices[3 + 1] == 3); // (3, 7) New
try expect(indices[3 + 2] == 4); // (9, 8) New
// Face 2
try expect(indices[6 + 0] == 4); // (9, 8) Duplicate - Reuse index
try expect(indices[6 + 1] == 5); // (2, 7) New normal mapping (Don't clobber)
try expect(indices[6 + 2] == 1); // (4, 5) Duplicate - Reuse Index
// Face 3
try expect(indices[9 + 0] == 2); // (2, 3) Duplicate - Reuse index
try expect(indices[9 + 1] == 6); // (6, 5) New
try expect(indices[9 + 2] == 7); // (1, 7) New
// Face 4
try expect(indices[12 + 0] == 8); // (9, 5) New normal mapping (Don't clobber)
try expect(indices[12 + 1] == 9); // (6, 7) New normal mapping (Don't clobber)
try expect(indices[12 + 2] == 10); // (0, 8) New normal mapping (Don't clobber)
try expect(writer.vertexBuffer().len == 11);
}

View file

@ -0,0 +1,49 @@
pub const Vertex = extern struct {
pos: @Vector(3, f32),
normal: @Vector(3, f32),
uv: @Vector(2, f32),
};
pub const vertices = [_]Vertex{
.{ .pos = .{ 1, -1, 1 }, .normal = .{ 0, -1, 0 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, -1, 1 }, .normal = .{ 0, -1, 0 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, -1, -1 }, .normal = .{ 0, -1, 0 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, -1, -1 }, .normal = .{ 0, -1, 0 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, -1, 1 }, .normal = .{ 0, -1, 0 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, -1, -1 }, .normal = .{ 0, -1, 0 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, 1, 1 }, .normal = .{ 1, 0, 0 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, -1, 1 }, .normal = .{ 1, 0, 0 }, .uv = .{ 0, 1 } },
.{ .pos = .{ 1, -1, -1 }, .normal = .{ 1, 0, 0 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, 1, -1 }, .normal = .{ 1, 0, 0 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, 1, 1 }, .normal = .{ 1, 0, 0 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, -1, -1 }, .normal = .{ 1, 0, 0 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, 1, 1 }, .normal = .{ 0, 1, 0 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, 1, 1 }, .normal = .{ 0, 1, 0 }, .uv = .{ 0, 1 } },
.{ .pos = .{ 1, 1, -1 }, .normal = .{ 0, 1, 0 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, 1, -1 }, .normal = .{ 0, 1, 0 }, .uv = .{ 1, 0 } },
.{ .pos = .{ -1, 1, 1 }, .normal = .{ 0, 1, 0 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, 1, -1 }, .normal = .{ 0, 1, 0 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, -1, 1 }, .normal = .{ -1, 0, 0 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, 1, 1 }, .normal = .{ -1, 0, 0 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, 1, -1 }, .normal = .{ -1, 0, 0 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, -1, -1 }, .normal = .{ -1, 0, 0 }, .uv = .{ 1, 0 } },
.{ .pos = .{ -1, -1, 1 }, .normal = .{ -1, 0, 0 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, 1, -1 }, .normal = .{ -1, 0, 0 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, 1, 1 }, .normal = .{ 0, 0, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, 1, 1 }, .normal = .{ 0, 0, 1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, -1, 1 }, .normal = .{ 0, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ -1, -1, 1 }, .normal = .{ 0, 0, 1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, -1, 1 }, .normal = .{ 0, 0, 1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, 1, 1 }, .normal = .{ 0, 0, 1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1, -1, -1 }, .normal = .{ 0, 0, -1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, -1, -1 }, .normal = .{ 0, 0, -1 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1, 1, -1 }, .normal = .{ 0, 0, -1 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1, 1, -1 }, .normal = .{ 0, 0, -1 }, .uv = .{ 1, 0 } },
.{ .pos = .{ 1, -1, -1 }, .normal = .{ 0, 0, -1 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1, 1, -1 }, .normal = .{ 0, 0, -1 }, .uv = .{ 0, 0 } },
};

View file

@ -0,0 +1,466 @@
const std = @import("std");
const core = @import("mach").core;
const gpu = core.gpu;
const zm = @import("zmath");
const Vertex = @import("cube_mesh.zig").Vertex;
const vertices = @import("cube_mesh.zig").vertices;
const Quad = @import("quad_mesh.zig").Quad;
const quad = @import("quad_mesh.zig").quad;
pub const App = @This();
pub const mach_core_options = core.ComptimeOptions{
.use_wgpu = false,
.use_sysgpu = true,
};
const pixel_size = 8;
const UniformBufferObject = struct {
mat: zm.Mat,
};
const PostUniformBufferObject = extern struct {
width: u32,
height: u32,
pixel_size: u32 = pixel_size,
};
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
title_timer: core.Timer,
timer: core.Timer,
pipeline: *gpu.RenderPipeline,
normal_pipeline: *gpu.RenderPipeline,
vertex_buffer: *gpu.Buffer,
uniform_buffer: *gpu.Buffer,
bind_group: *gpu.BindGroup,
post_pipeline: *gpu.RenderPipeline,
post_vertex_buffer: *gpu.Buffer,
post_uniform_buffer: *gpu.Buffer,
post_bind_group: *gpu.BindGroup,
draw_texture_view: *gpu.TextureView,
depth_texture_view: *gpu.TextureView,
normal_texture_view: *gpu.TextureView,
pub fn init(app: *App) !void {
try core.init(.{});
app.title_timer = try core.Timer.start();
app.timer = try core.Timer.start();
try app.createRenderTextures();
app.createDrawPipeline();
app.createPostPipeline();
}
pub fn deinit(app: *App) void {
defer _ = gpa.deinit();
defer core.deinit();
app.cleanup();
}
pub fn update(app: *App) !bool {
var iter = core.pollEvents();
while (iter.next()) |event| {
switch (event) {
.key_press => |ev| {
if (ev.key == .space) return true;
},
.close => return true,
.framebuffer_resize => {
app.cleanup();
try app.createRenderTextures();
app.createDrawPipeline();
app.createPostPipeline();
},
else => {},
}
}
const size = core.size();
const encoder = core.device.createCommandEncoder(null);
encoder.writeBuffer(app.post_uniform_buffer, 0, &[_]PostUniformBufferObject{
PostUniformBufferObject{
.width = size.width,
.height = size.height,
},
});
{
const time = app.timer.read() * 0.5;
const model = zm.mul(zm.rotationX(time * (std.math.pi / 2.0)), zm.rotationZ(time * (std.math.pi / 2.0)));
const view = zm.lookAtRh(
zm.Vec{ 0, 5, 2, 1 },
zm.Vec{ 0, 0, 0, 1 },
zm.Vec{ 0, 0, 1, 0 },
);
const proj = zm.perspectiveFovRh(
(std.math.pi / 4.0),
@as(f32, @floatFromInt(core.descriptor.width)) / @as(f32, @floatFromInt(core.descriptor.height)),
0.1,
10,
);
const mvp = zm.mul(zm.mul(model, view), proj);
const ubo = UniformBufferObject{
.mat = zm.transpose(mvp),
};
encoder.writeBuffer(app.uniform_buffer, 0, &[_]UniformBufferObject{ubo});
}
{
// render scene to downscaled texture
const color_attachment = gpu.RenderPassColorAttachment{
.view = app.draw_texture_view,
.clear_value = std.mem.zeroes(gpu.Color),
.load_op = .clear,
.store_op = .store,
};
const render_pass_info = gpu.RenderPassDescriptor.init(.{
.color_attachments = &.{color_attachment},
.depth_stencil_attachment = &gpu.RenderPassDepthStencilAttachment{
.view = app.depth_texture_view,
.depth_load_op = .clear,
.depth_store_op = .store,
.depth_clear_value = 1.0,
},
});
const pass = encoder.beginRenderPass(&render_pass_info);
pass.setPipeline(app.pipeline);
pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len);
pass.setBindGroup(0, app.bind_group, &.{0});
pass.draw(vertices.len, 1, 0, 0);
pass.end();
pass.release();
}
{
// render scene normals to texture
const normal_color_attachment = gpu.RenderPassColorAttachment{
.view = app.normal_texture_view,
.clear_value = .{ .r = 0.5, .b = 0.5, .g = 0.5, .a = 1.0 },
.load_op = .clear,
.store_op = .store,
};
const normal_render_pass_info = gpu.RenderPassDescriptor.init(.{
.color_attachments = &.{normal_color_attachment},
});
const normal_pass = encoder.beginRenderPass(&normal_render_pass_info);
normal_pass.setPipeline(app.normal_pipeline);
normal_pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(Vertex) * vertices.len);
normal_pass.setBindGroup(0, app.bind_group, &.{0});
normal_pass.draw(vertices.len, 1, 0, 0);
normal_pass.end();
normal_pass.release();
}
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
{
// render to swap chain using previous passes
const post_color_attachment = gpu.RenderPassColorAttachment{
.view = back_buffer_view,
.clear_value = std.mem.zeroes(gpu.Color),
.load_op = .clear,
.store_op = .store,
};
const post_render_pass_info = gpu.RenderPassDescriptor.init(.{
.color_attachments = &.{post_color_attachment},
});
const draw_pass = encoder.beginRenderPass(&post_render_pass_info);
draw_pass.setPipeline(app.post_pipeline);
draw_pass.setVertexBuffer(0, app.post_vertex_buffer, 0, @sizeOf(Quad) * quad.len);
draw_pass.setBindGroup(0, app.post_bind_group, &.{0});
draw_pass.draw(quad.len, 1, 0, 0);
draw_pass.end();
draw_pass.release();
}
var command = encoder.finish(null);
encoder.release();
const queue = core.queue;
queue.submit(&[_]*gpu.CommandBuffer{command});
command.release();
core.swap_chain.present();
back_buffer_view.release();
// update the window title every second
if (app.title_timer.read() >= 1.0) {
app.title_timer.reset();
try core.printTitle("Pixel Post Process [ {d}fps ] [ Input {d}hz ]", .{
core.frameRate(),
core.inputRate(),
});
}
return false;
}
fn cleanup(app: *App) void {
app.pipeline.release();
app.normal_pipeline.release();
app.vertex_buffer.release();
app.uniform_buffer.release();
app.bind_group.release();
app.post_pipeline.release();
app.post_vertex_buffer.release();
app.post_uniform_buffer.release();
app.post_bind_group.release();
app.draw_texture_view.release();
app.depth_texture_view.release();
app.normal_texture_view.release();
}
fn createRenderTextures(app: *App) !void {
const size = core.size();
const draw_texture_desc = gpu.Texture.Descriptor.init(.{
.size = .{ .width = size.width / pixel_size, .height = size.height / pixel_size },
.format = .bgra8_unorm,
.usage = .{ .texture_binding = true, .copy_dst = true, .render_attachment = true },
});
const draw_texture = core.device.createTexture(&draw_texture_desc);
app.draw_texture_view = draw_texture.createView(null);
draw_texture.release();
const depth_texture_desc = gpu.Texture.Descriptor.init(.{
.size = .{ .width = size.width / pixel_size, .height = size.height / pixel_size },
.format = .depth32_float,
.usage = .{ .texture_binding = true, .copy_dst = true, .render_attachment = true },
});
const depth_texture = core.device.createTexture(&depth_texture_desc);
app.depth_texture_view = depth_texture.createView(null);
depth_texture.release();
const normal_texture_desc = gpu.Texture.Descriptor.init(.{
.size = .{ .width = size.width / pixel_size, .height = size.height / pixel_size },
.format = .bgra8_unorm,
.usage = .{ .texture_binding = true, .copy_dst = true, .render_attachment = true },
});
const normal_texture = core.device.createTexture(&normal_texture_desc);
app.normal_texture_view = normal_texture.createView(null);
normal_texture.release();
}
fn createDrawPipeline(app: *App) void {
const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl"));
const vertex_attributes = [_]gpu.VertexAttribute{
.{ .format = .float32x3, .offset = @offsetOf(Vertex, "pos"), .shader_location = 0 },
.{ .format = .float32x3, .offset = @offsetOf(Vertex, "normal"), .shader_location = 1 },
.{ .format = .float32x2, .offset = @offsetOf(Vertex, "uv"), .shader_location = 2 },
};
const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{
.array_stride = @sizeOf(Vertex),
.step_mode = .vertex,
.attributes = &vertex_attributes,
});
const vertex = gpu.VertexState.init(.{
.module = shader_module,
.entry_point = "vertex_main",
.buffers = &.{vertex_buffer_layout},
});
const blend = gpu.BlendState{};
const color_target = gpu.ColorTargetState{
.format = core.descriptor.format,
.blend = &blend,
.write_mask = gpu.ColorWriteMaskFlags.all,
};
const fragment = gpu.FragmentState.init(.{
.module = shader_module,
.entry_point = "frag_main",
.targets = &.{color_target},
});
const bgle = gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, true, 0);
const bgl = core.device.createBindGroupLayout(
&gpu.BindGroupLayout.Descriptor.init(.{
.entries = &.{bgle},
}),
);
const bind_group_layouts = [_]*gpu.BindGroupLayout{bgl};
const pipeline_layout = core.device.createPipelineLayout(
&gpu.PipelineLayout.Descriptor.init(.{
.bind_group_layouts = &bind_group_layouts,
}),
);
const vertex_buffer = core.device.createBuffer(&.{
.usage = .{ .vertex = true },
.size = @sizeOf(Vertex) * vertices.len,
.mapped_at_creation = .true,
});
const vertex_mapped = vertex_buffer.getMappedRange(Vertex, 0, vertices.len);
@memcpy(vertex_mapped.?, vertices[0..]);
vertex_buffer.unmap();
const uniform_buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .uniform = true },
.size = @sizeOf(UniformBufferObject),
.mapped_at_creation = .false,
});
const bind_group = core.device.createBindGroup(
&gpu.BindGroup.Descriptor.init(.{
.layout = bgl,
.entries = &.{
gpu.BindGroup.Entry.buffer(0, uniform_buffer, 0, @sizeOf(UniformBufferObject), @sizeOf(UniformBufferObject)),
},
}),
);
const pipeline_descriptor = gpu.RenderPipeline.Descriptor{
.fragment = &fragment,
.layout = pipeline_layout,
.vertex = vertex,
.primitive = .{
.cull_mode = .back,
},
.depth_stencil = &gpu.DepthStencilState{
.format = .depth32_float,
.depth_write_enabled = .true,
.depth_compare = .less,
},
};
{
// "same" pipeline, different fragment shader to create a texture with normal information
const normal_fs_module = core.device.createShaderModuleWGSL("normal_frag.wgsl", @embedFile("normal_frag.wgsl"));
const normal_fragment = gpu.FragmentState.init(.{
.module = normal_fs_module,
.entry_point = "main",
.targets = &.{color_target},
});
const normal_pipeline_descriptor = gpu.RenderPipeline.Descriptor{
.fragment = &normal_fragment,
.layout = pipeline_layout,
.vertex = vertex,
.primitive = .{
.cull_mode = .back,
},
};
app.normal_pipeline = core.device.createRenderPipeline(&normal_pipeline_descriptor);
normal_fs_module.release();
}
app.pipeline = core.device.createRenderPipeline(&pipeline_descriptor);
app.vertex_buffer = vertex_buffer;
app.uniform_buffer = uniform_buffer;
app.bind_group = bind_group;
shader_module.release();
pipeline_layout.release();
bgl.release();
}
fn createPostPipeline(app: *App) void {
const vs_module = core.device.createShaderModuleWGSL("pixel_vert.wgsl", @embedFile("pixel_vert.wgsl"));
const vertex_attributes = [_]gpu.VertexAttribute{
.{ .format = .float32x3, .offset = @offsetOf(Quad, "pos"), .shader_location = 0 },
.{ .format = .float32x2, .offset = @offsetOf(Quad, "uv"), .shader_location = 1 },
};
const vertex_buffer_layout = gpu.VertexBufferLayout.init(.{
.array_stride = @sizeOf(Quad),
.step_mode = .vertex,
.attributes = &vertex_attributes,
});
const vertex = gpu.VertexState.init(.{
.module = vs_module,
.entry_point = "main",
.buffers = &.{vertex_buffer_layout},
});
const fs_module = core.device.createShaderModuleWGSL("pixel_frag.wgsl", @embedFile("pixel_frag.wgsl"));
const blend = gpu.BlendState{};
const color_target = gpu.ColorTargetState{
.format = core.descriptor.format,
.blend = &blend,
.write_mask = gpu.ColorWriteMaskFlags.all,
};
const fragment = gpu.FragmentState.init(.{
.module = fs_module,
.entry_point = "main",
.targets = &.{color_target},
});
const bgl = core.device.createBindGroupLayout(
&gpu.BindGroupLayout.Descriptor.init(.{
.entries = &.{
gpu.BindGroupLayout.Entry.texture(0, .{ .fragment = true }, .float, .dimension_2d, false),
gpu.BindGroupLayout.Entry.sampler(1, .{ .fragment = true }, .filtering),
gpu.BindGroupLayout.Entry.texture(2, .{ .fragment = true }, .depth, .dimension_2d, false),
gpu.BindGroupLayout.Entry.sampler(3, .{ .fragment = true }, .filtering),
gpu.BindGroupLayout.Entry.texture(4, .{ .fragment = true }, .float, .dimension_2d, false),
gpu.BindGroupLayout.Entry.sampler(5, .{ .fragment = true }, .filtering),
gpu.BindGroupLayout.Entry.buffer(6, .{ .fragment = true }, .uniform, true, 0),
},
}),
);
const bind_group_layouts = [_]*gpu.BindGroupLayout{bgl};
const pipeline_layout = core.device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{
.bind_group_layouts = &bind_group_layouts,
}));
const vertex_buffer = core.device.createBuffer(&.{
.usage = .{ .vertex = true },
.size = @sizeOf(Quad) * quad.len,
.mapped_at_creation = .true,
});
const vertex_mapped = vertex_buffer.getMappedRange(Quad, 0, quad.len);
@memcpy(vertex_mapped.?, quad[0..]);
vertex_buffer.unmap();
const draw_sampler = core.device.createSampler(null);
const depth_sampler = core.device.createSampler(null);
const normal_sampler = core.device.createSampler(null);
const uniform_buffer = core.device.createBuffer(&.{
.usage = .{ .copy_dst = true, .uniform = true },
.size = @sizeOf(PostUniformBufferObject),
.mapped_at_creation = .false,
});
const bind_group = core.device.createBindGroup(
&gpu.BindGroup.Descriptor.init(.{
.layout = bgl,
.entries = &[_]gpu.BindGroup.Entry{
gpu.BindGroup.Entry.textureView(0, app.draw_texture_view),
gpu.BindGroup.Entry.sampler(1, draw_sampler),
gpu.BindGroup.Entry.textureView(2, app.depth_texture_view),
gpu.BindGroup.Entry.sampler(3, depth_sampler),
gpu.BindGroup.Entry.textureView(4, app.normal_texture_view),
gpu.BindGroup.Entry.sampler(5, normal_sampler),
gpu.BindGroup.Entry.buffer(6, uniform_buffer, 0, @sizeOf(PostUniformBufferObject), @sizeOf(PostUniformBufferObject)),
},
}),
);
draw_sampler.release();
depth_sampler.release();
normal_sampler.release();
const pipeline_descriptor = gpu.RenderPipeline.Descriptor{
.fragment = &fragment,
.layout = pipeline_layout,
.vertex = vertex,
.primitive = .{
.cull_mode = .back,
},
};
app.post_pipeline = core.device.createRenderPipeline(&pipeline_descriptor);
app.post_vertex_buffer = vertex_buffer;
app.post_uniform_buffer = uniform_buffer;
app.post_bind_group = bind_group;
vs_module.release();
fs_module.release();
pipeline_layout.release();
bgl.release();
}

View file

@ -0,0 +1,6 @@
@fragment fn main(
@location(0) normal: vec3<f32>,
@location(1) uv: vec2<f32>,
) -> @location(0) vec4<f32> {
return vec4<f32>(normal / 2 + 0.5, 1.0);
}

View file

@ -0,0 +1,77 @@
@group(0) @binding(0)
var draw_texture: texture_2d<f32>;
@group(0) @binding(1)
var draw_texture_sampler: sampler;
@group(0) @binding(2)
var depth_texture: texture_depth_2d;
@group(0) @binding(3)
var depth_texture_sampler: sampler;
@group(0) @binding(4)
var normal_texture: texture_2d<f32>;
@group(0) @binding(5)
var normal_texture_sampler: sampler;
struct View {
@location(0) width: u32,
@location(1) height: u32,
@location(2) pixel_size: u32,
}
@group(0) @binding(6)
var<uniform> view: View;
fn sample_depth(uv: vec2<f32>, x: f32, y: f32) -> f32 {
return textureSample(
depth_texture,
depth_texture_sampler,
uv + vec2<f32>(x * f32(view.pixel_size) / f32(view.width), y * f32(view.pixel_size) / f32(view.height))
);
}
fn sample_normal(uv: vec2<f32>, x: f32, y: f32) -> vec3<f32> {
return textureSample(
normal_texture,
normal_texture_sampler,
uv + vec2<f32>(x * f32(view.pixel_size) / f32(view.width), y * f32(view.pixel_size) / f32(view.height))
).xyz;
}
fn normal_indicator(uv: vec2<f32>, x: f32, y: f32) -> f32 {
// TODO - integer promotion to float argument
var depth_diff = sample_depth(uv, 0.0, 0.0) - sample_depth(uv, x, y);
var dx = sample_normal(uv, 0.0, 0.0);
var dy = sample_normal(uv, x, y);
if (depth_diff > 0) {
// only sample normals from closest pixel
return 0;
}
return distance(dx, dy);
}
@fragment fn main(
// TODO - vertex/fragment linkage
@location(0) uv: vec2<f32>,
@builtin(position) position: vec4<f32>
) -> @location(0) vec4<f32> {
// TODO - integer promotion to float argument
var depth = sample_depth(uv, 0.0, 0.0);
var depth_diff: f32 = 0;
depth_diff += abs(depth - sample_depth(uv, -1.0, 0.0));
depth_diff += abs(depth - sample_depth(uv, 1.0, 0.0));
depth_diff += abs(depth - sample_depth(uv, 0.0, -1.0));
depth_diff += abs(depth - sample_depth(uv, 0.0, 1.0));
var normal_diff: f32 = 0;
normal_diff += normal_indicator(uv, -1.0, 0.0);
normal_diff += normal_indicator(uv, 1.0, 0.0);
normal_diff += normal_indicator(uv, 0.0, -1.0);
normal_diff += normal_indicator(uv, 0.0, 1.0);
var color = textureSample(draw_texture, draw_texture_sampler, uv);
if (depth_diff > 0.007) { // magic number from testing
return color * 0.7;
}
// add instead of multiply so really dark pixels get brighter
return color + (vec4<f32>(1) * step(0.1, normal_diff) * 0.7);
}

View file

@ -0,0 +1,14 @@
struct VertexOut {
@builtin(position) position_clip: vec4<f32>,
@location(0) uv: vec2<f32>
}
@vertex fn main(
@location(0) position: vec3<f32>,
@location(1) uv: vec2<f32>
) -> VertexOut {
var output : VertexOut;
output.position_clip = vec4<f32>(position.xy, 0.0, 1.0);
output.uv = uv;
return output;
}

View file

@ -0,0 +1,13 @@
pub const Quad = extern struct {
pos: @Vector(3, f32),
uv: @Vector(2, f32),
};
pub const quad = [_]Quad{
.{ .pos = .{ -1.0, 1.0, 0.0 }, .uv = .{ 0, 1 } },
.{ .pos = .{ -1.0, -1.0, 0.0 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1.0, 1.0, 0.0 }, .uv = .{ 1, 1 } },
.{ .pos = .{ 1.0, 1.0, 0.0 }, .uv = .{ 1, 1 } },
.{ .pos = .{ -1.0, -1.0, 0.0 }, .uv = .{ 0, 0 } },
.{ .pos = .{ 1.0, -1.0, 0.0 }, .uv = .{ 1, 0 } },
};

View file

@ -0,0 +1,27 @@
@group(0) @binding(0) var<uniform> ubo: mat4x4<f32>;
struct VertexOut {
@builtin(position) position_clip: vec4<f32>,
@location(0) normal: vec3<f32>,
@location(1) uv: vec2<f32>,
}
@vertex fn vertex_main(
@location(0) position: vec3<f32>,
@location(1) normal: vec3<f32>,
@location(2) uv: vec2<f32>
) -> VertexOut {
var output: VertexOut;
output.position_clip = vec4<f32>(position, 1) * ubo;
output.normal = (vec4<f32>(normal, 0) * ubo).xyz;
output.uv = uv;
return output;
}
@fragment fn frag_main(
@location(0) normal: vec3<f32>,
@location(1) uv: vec2<f32>,
) -> @location(0) vec4<f32> {
var color = floor((uv * 0.5 + 0.25) * 32) / 32;
return vec4<f32>(color, 1, 1);
}

Some files were not shown because too many files have changed in this diff Show more