diff --git a/build.zig b/build.zig index 1a3091a..7f6fbdf 100644 --- a/build.zig +++ b/build.zig @@ -6,20 +6,17 @@ pub fn build(b: *std.Build) !void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); - const zmath_pkg = zmath.package(b, target, optimize, .{ - .options = .{ .enable_cross_platform_determinism = true }, - }); + const mach_core_dep = b.dependency("mach_core", .{ .target = target, .optimize = optimize }); + const zigimg_dep = b.dependency("zigimg", .{ .target = target, .optimize = optimize }); + const zmath_pkg = zmath.package(b, target, optimize, .{}); - const mach_core_dep = b.dependency("mach_core", .{ - .target = target, - .optimize = optimize, - }); const app = try mach_core.App.init(b, mach_core_dep.builder, .{ .name = "zig-bloxel-game", .src = "src/main.zig", .target = target, .optimize = optimize, .deps = &.{ + .{ .name = "zigimg", .module = zigimg_dep.module("zigimg") }, .{ .name = "zmath", .module = zmath_pkg.zmath }, }, }); diff --git a/build.zig.zon b/build.zig.zon index 43c92be..539df80 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -14,6 +14,10 @@ .url = "https://pkg.machengine.org/mach-core/6a62bcc90e0d072d632788a6575d77942bd09a19.tar.gz", .hash = "12209d39954fcda0be158461c10f64d14d5c7d097bd6d26785b332d75ffefa7dd7a0", }, + .zigimg = .{ + .url = "https://github.com/zigimg/zigimg/archive/ad6ad042662856f55a4d67499f1c4606c9951031.tar.gz", + .hash = "1220bf6b616ca219f95be1205b12aa8cdb7e09838fcebeae90b48b5ab0a030c5ab45", + }, .zmath = .{ .path = "libs/zig-gamedev/libs/zmath" }, }, } diff --git a/src/gfx/default.png b/src/gfx/default.png new file mode 100644 index 0000000..da85e02 Binary files /dev/null and b/src/gfx/default.png differ diff --git a/src/primitives.zig b/src/primitives.zig index 2f7e065..4f3ba54 100644 --- a/src/primitives.zig +++ b/src/primitives.zig @@ -10,6 +10,7 @@ const createAndWriteBuffer = Renderer.createAndWriteBuffer; /// Describes the layout of each vertex that a primitive is made of. pub const VertexData = struct { position: [3]f32, + uv: [2]f32, }; /// Contains the data to render a primitive (3D shape or model). @@ -43,8 +44,8 @@ pub fn createPrimitive( }; } -fn vert(x: f32, y: f32, z: f32) VertexData { - return .{ .position = .{ x, y, z } }; +fn vert(x: f32, y: f32, z: f32, tx: f32, ty: f32) VertexData { + return .{ .position = .{ x, y, z }, .uv = .{ tx, ty } }; } pub fn createTrianglePrimitive(length: f32) PrimitiveData { @@ -60,9 +61,9 @@ pub fn createTrianglePrimitive(length: f32) PrimitiveData { // / \ // 1-----2 &.{ - vert(@sin(a0) * radius, @cos(a0) * radius, 0.0), - vert(@sin(a1) * radius, @cos(a1) * radius, 0.0), - vert(@sin(a2) * radius, @cos(a2) * radius, 0.0), + vert(@sin(a0) * radius, @cos(a0) * radius, 0.0, 0.5, 0.0), + vert(@sin(a1) * radius, @cos(a1) * radius, 0.0, 0.0, 1.0), + vert(@sin(a2) * radius, @cos(a2) * radius, 0.0, 1.0, 1.0), }, // Vertices have to be specified in counter-clockwise, // so the "front" of the triangle is facing the right way. @@ -83,10 +84,10 @@ pub fn createSquarePrimitive(width: f32) PrimitiveData { // 1---3 &.{ // zig fmt: off - vert(-half_width, -half_width, 0.0), - vert(-half_width, half_width, 0.0), - vert( half_width, -half_width, 0.0), - vert( half_width, half_width, 0.0), + vert(-half_width, -half_width, 0.0, 0.0, 0.0), + vert(-half_width, half_width, 0.0, 0.0, 1.0), + vert( half_width, -half_width, 0.0, 1.0, 0.0), + vert( half_width, half_width, 0.0, 1.0, 1.0), // zig fmt: on }, // ... but it has to be split up into 2 triangles. @@ -127,35 +128,35 @@ pub fn createCubePrimitive(width: f32) PrimitiveData { // zig fmt: off &.{ // Right (+X) - vert( half_width, half_width, -half_width), - vert( half_width, -half_width, -half_width), - vert( half_width, half_width, half_width), - vert( half_width, -half_width, half_width), + vert( half_width, half_width, -half_width, 0.0, 0.0), + vert( half_width, -half_width, -half_width, 0.0, 1.0), + vert( half_width, half_width, half_width, 1.0, 0.0), + vert( half_width, -half_width, half_width, 1.0, 1.0), // Left (-X) - vert(-half_width, half_width, half_width), - vert(-half_width, -half_width, half_width), - vert(-half_width, half_width, -half_width), - vert(-half_width, -half_width, -half_width), + vert(-half_width, half_width, half_width, 0.0, 0.0), + vert(-half_width, -half_width, half_width, 0.0, 1.0), + vert(-half_width, half_width, -half_width, 1.0, 0.0), + vert(-half_width, -half_width, -half_width, 1.0, 1.0), // Top (+Y) - vert( half_width, half_width, -half_width), - vert( half_width, half_width, half_width), - vert(-half_width, half_width, -half_width), - vert(-half_width, half_width, half_width), + vert( half_width, half_width, -half_width, 0.0, 0.0), + vert( half_width, half_width, half_width, 0.0, 1.0), + vert(-half_width, half_width, -half_width, 1.0, 0.0), + vert(-half_width, half_width, half_width, 1.0, 1.0), // Bottom (-Y) - vert(-half_width, -half_width, -half_width), - vert(-half_width, -half_width, half_width), - vert( half_width, -half_width, -half_width), - vert( half_width, -half_width, half_width), + vert(-half_width, -half_width, -half_width, 1.0, 0.0), + vert(-half_width, -half_width, half_width, 0.0, 0.0), + vert( half_width, -half_width, -half_width, 1.0, 1.0), + vert( half_width, -half_width, half_width, 0.0, 1.0), // Front (+Z) - vert( half_width, half_width, half_width), - vert( half_width, -half_width, half_width), - vert(-half_width, half_width, half_width), - vert(-half_width, -half_width, half_width), + vert( half_width, half_width, half_width, 0.0, 0.0), + vert( half_width, -half_width, half_width, 0.0, 1.0), + vert(-half_width, half_width, half_width, 1.0, 0.0), + vert(-half_width, -half_width, half_width, 1.0, 1.0), // Back (-Z) - vert(-half_width, half_width, -half_width), - vert(-half_width, -half_width, -half_width), - vert( half_width, half_width, -half_width), - vert( half_width, -half_width, -half_width), + vert(-half_width, half_width, -half_width, 0.0, 0.0), + vert(-half_width, -half_width, -half_width, 0.0, 1.0), + vert( half_width, half_width, -half_width, 1.0, 0.0), + vert( half_width, -half_width, -half_width, 1.0, 1.0), }, &.{ 0, 1, 2, 3, 2, 1, // Right @@ -175,26 +176,26 @@ pub fn createPyramidPrimitive(width: f32) PrimitiveData { // zig fmt: off &.{ // Right - vert( 0.0, half_width, 0.0), - vert( half_width, -half_width, -half_width), - vert( half_width, -half_width, half_width), + vert( 0.0, half_width, 0.0, 0.5, 0.0), + vert( half_width, -half_width, -half_width, 0.0, 1.0), + vert( half_width, -half_width, half_width, 1.0, 1.0), // Left - vert( 0.0, half_width, 0.0), - vert(-half_width, -half_width, half_width), - vert(-half_width, -half_width, -half_width), + vert( 0.0, half_width, 0.0, 0.5, 0.0), + vert(-half_width, -half_width, half_width, 0.0, 1.0), + vert(-half_width, -half_width, -half_width, 1.0, 1.0), // Front - vert( 0.0, half_width, 0.0), - vert( half_width, -half_width, half_width), - vert(-half_width, -half_width, half_width), + vert( 0.0, half_width, 0.0, 0.5, 0.0), + vert( half_width, -half_width, half_width, 0.0, 1.0), + vert(-half_width, -half_width, half_width, 1.0, 1.0), // Back - vert( 0.0, half_width, 0.0), - vert(-half_width, -half_width, -half_width), - vert( half_width, -half_width, -half_width), + vert( 0.0, half_width, 0.0, 0.5, 0.0), + vert(-half_width, -half_width, -half_width, 0.0, 1.0), + vert( half_width, -half_width, -half_width, 1.0, 1.0), // Bottom - vert(-half_width, -half_width, -half_width), - vert(-half_width, -half_width, half_width), - vert( half_width, -half_width, -half_width), - vert( half_width, -half_width, half_width), + vert(-half_width, -half_width, -half_width, 0.0, 0.0), + vert(-half_width, -half_width, half_width, 0.0, 1.0), + vert( half_width, -half_width, -half_width, 1.0, 0.0), + vert( half_width, -half_width, half_width, 1.0, 1.0), }, &.{ 0, 1, 2, // Right diff --git a/src/renderer.zig b/src/renderer.zig index adaa092..48b8304 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -1,5 +1,9 @@ const std = @import("std"); +const zigimg = @import("zigimg"); +const Rgb24 = zigimg.color.Rgb24; +const Rgba32 = zigimg.color.Rgba32; + const core = @import("mach-core"); const gpu = core.gpu; @@ -40,6 +44,11 @@ pub fn init(app: *App) !*Renderer { // A string buffer used to format objects' labels. var label_buffer: [256]u8 = undefined; + // Embed `default.png` texture in the executable and upload it to the GPU. + const texture_bytes = @embedFile("./gfx/default.png"); + const texture_view = try loadTexture(app.allocator, texture_bytes); + defer texture_view.release(); + const shader_module = core.device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl")); defer shader_module.release(); @@ -56,8 +65,10 @@ pub fn init(app: *App) !*Renderer { var model_bind_group_layout = core.device.createBindGroupLayout(&gpu.BindGroupLayout.Descriptor.init(.{ .label = "Model Bind Group Layout", .entries = &.{ - gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, false, 0), - gpu.BindGroupLayout.Entry.buffer(1, .{ .vertex = true }, .uniform, false, 0), + gpu.BindGroupLayout.Entry.sampler(0, .{ .fragment = true }, .filtering), + gpu.BindGroupLayout.Entry.texture(1, .{ .fragment = true }, .float, .dimension_2d, false), + gpu.BindGroupLayout.Entry.buffer(2, .{ .vertex = true }, .uniform, false, 0), + gpu.BindGroupLayout.Entry.buffer(3, .{ .vertex = true }, .uniform, false, 0), }, })); defer model_bind_group_layout.release(); @@ -82,6 +93,7 @@ pub fn init(app: *App) !*Renderer { .step_mode = .vertex, .attributes = &.{ .{ .format = .float32x3, .shader_location = 0, .offset = @offsetOf(VertexData, "position") }, + .{ .format = .float32x2, .shader_location = 1, .offset = @offsetOf(VertexData, "uv") }, }, }), }, @@ -116,6 +128,11 @@ pub fn init(app: *App) !*Renderer { }, })); + // Create a sampler that tells the GPU how to sample pixels from a texture. + // Includes filtering (nearest-neighbor, linear, ..) and wrapping behavior. + const texture_sampler = core.device.createSampler(&.{}); + defer texture_sampler.release(); + // Set up the primitives we want to render. // // Using `dupe` to allocate a slice here allows easily adjusting the @@ -171,8 +188,10 @@ pub fn init(app: *App) !*Renderer { .label = model_bind_group_label, .layout = model_bind_group_layout, .entries = &.{ - gpu.BindGroup.Entry.buffer(0, model_matrix_buffer, 0, @sizeOf(zm.Mat)), - gpu.BindGroup.Entry.buffer(1, model_color_buffer, 0, @sizeOf([3]f32)), + gpu.BindGroup.Entry.sampler(0, texture_sampler), + gpu.BindGroup.Entry.textureView(1, texture_view), + gpu.BindGroup.Entry.buffer(2, model_matrix_buffer, 0, @sizeOf(zm.Mat)), + gpu.BindGroup.Entry.buffer(3, model_color_buffer, 0, @sizeOf([3]f32)), }, })); @@ -318,6 +337,53 @@ pub fn update(self: *Renderer) void { core.queue.submit(&.{command}); } +/// Loads a texture from the provided buffer and uploads it to the GPU. +pub fn loadTexture(allocator: std.mem.Allocator, buffer: []const u8) !*gpu.TextureView { + var img = try zigimg.Image.fromMemory(allocator, buffer); + defer img.deinit(); + const img_size = gpu.Extent3D{ + .width = @intCast(img.width), + .height = @intCast(img.height), + }; + + const texture = core.device.createTexture(&.{ + .size = img_size, + .format = .rgba8_unorm, + .usage = .{ + .texture_binding = true, + .copy_dst = true, + .render_attachment = true, + }, + }); + defer texture.release(); + + const data_layout = gpu.Texture.DataLayout{ + .bytes_per_row = @intCast(img.width * 4), + .rows_per_image = @intCast(img.height), + }; + switch (img.pixels) { + .rgba32 => |pixels| { + core.queue.writeTexture(&.{ .texture = texture }, &data_layout, &img_size, pixels); + }, + .rgb24 => |pixels_rgb24| { + const pixels = try rgb24ToRgba32(allocator, pixels_rgb24); + defer allocator.free(pixels); + core.queue.writeTexture(&.{ .texture = texture }, &data_layout, &img_size, pixels); + }, + else => std.debug.panic("Unsupported image color format {s}", .{@tagName(img.pixels)}), + } + + return texture.createView(&.{}); +} + +/// Converts a raw 24-bit RGB pixel buffer to 32-bit RGBA. +fn rgb24ToRgba32(allocator: std.mem.Allocator, in: []Rgb24) ![]Rgba32 { + const out = try allocator.alloc(Rgba32, in.len); + for (in, out) |src, *dest| + dest.* = .{ .r = src.r, .g = src.g, .b = src.b, .a = 255 }; + return out; +} + /// Creates a depth texture. This is used to ensure that when things are /// rendered, an object behind another won't draw over one in front, simply /// because it was rendered at a later point in time. diff --git a/src/shader.wgsl b/src/shader.wgsl index 30463f5..1e06d6a 100644 --- a/src/shader.wgsl +++ b/src/shader.wgsl @@ -1,15 +1,20 @@ @group(0) @binding(0) var view_proj_matrix: mat4x4; -@group(1) @binding(0) var model_matrix: mat4x4; -@group(1) @binding(1) var model_color: vec3; + +@group(1) @binding(0) var texture_sampler: sampler; +@group(1) @binding(1) var model_texture: texture_2d; +@group(1) @binding(2) var model_matrix: mat4x4; +@group(1) @binding(3) var model_color: vec3; struct VertexInput { @location(0) position: vec3, + @location(1) uv: vec2, }; struct VertexOutput { @builtin(position) position: vec4, @location(0) color: vec4, + @location(1) frag_uv: vec2, }; struct FragmentOutput { @@ -21,11 +26,13 @@ struct FragmentOutput { let mvp = model_matrix * view_proj_matrix; out.position = vec4(in.position, 1.0) * mvp; out.color = vec4(model_color, 1.0); + out.frag_uv = in.uv; return out; } @fragment fn frag_main(in: VertexOutput) -> FragmentOutput { var out: FragmentOutput; - out.pixel_color = in.color; + let texture_color = textureSample(model_texture, texture_sampler, in.frag_uv); + out.pixel_color = texture_color * in.color; return out; }