render-zig

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

commit 3f164f4e03c5760ada8c0169014b5aeec9b23e09
parent d0f22beff84f999692e3194da21e16634ea73a4a
Author: Christian Ermann <christianermann@gmail.com>
Date:   Fri, 17 May 2024 17:10:28 -0400

Add cube map, finish skybox render pipeline

Diffstat:
Mbuild.zig | 7+++++++
Mbuild.zig.zon | 5+++++
Acube/cube-neg-x.png | 0
Acube/cube-neg-y.png | 0
Acube/cube-neg-z.png | 0
Acube/cube-pos-x.png | 0
Acube/cube-pos-y.png | 0
Acube/cube-pos-z.png | 0
Asrc/cubemap.zig | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/main.zig | 13++++++++++++-
Msrc/render_pipeline.zig | 22+++++++++++++++++++---
11 files changed, 156 insertions(+), 4 deletions(-)

diff --git a/build.zig b/build.zig @@ -52,6 +52,13 @@ pub fn build(b: *std.Build) void { try gpu.link(gpu_dep.builder, exe, &exe.root_module, .{}); } + if (b.lazyDependency("zigimg", .{ + .target = target, + .optimize = optimize, + })) |img_dep| { + exe.root_module.addImport("zigimg", img_dep.module("zigimg")); + } + // This declares intent for the executable to be installed into the // standard location when the user invokes the "install" step (the default // step when running `zig build`). diff --git a/build.zig.zon b/build.zig.zon @@ -25,6 +25,11 @@ .hash = "1220fe2e555ca66741539bc0f97769b2513c5e609c968d27eb8997f577a1d195f048", .lazy = true, }, + .zigimg = .{ + .url = "https://github.com/zigimg/zigimg/archive/637974e2d31dcdbc33f1e9cc8ffb2e46abd2e215.tar.gz", + .hash = "122012026c3a65ff1d4acba3b3fe80785f7cee9c6b4cdaff7ed0fbf23b0a6c803989", + .lazy = true, + }, }, .paths = .{ // This makes *all* files, recursively, included in this package. It is generally diff --git a/cube/cube-neg-x.png b/cube/cube-neg-x.png Binary files differ. diff --git a/cube/cube-neg-y.png b/cube/cube-neg-y.png Binary files differ. diff --git a/cube/cube-neg-z.png b/cube/cube-neg-z.png Binary files differ. diff --git a/cube/cube-pos-x.png b/cube/cube-pos-x.png Binary files differ. diff --git a/cube/cube-pos-y.png b/cube/cube-pos-y.png Binary files differ. diff --git a/cube/cube-pos-z.png b/cube/cube-pos-z.png Binary files differ. diff --git a/src/cubemap.zig b/src/cubemap.zig @@ -0,0 +1,113 @@ +const std = @import("std"); +const gpu = @import("mach_gpu"); +const Image = @import("zigimg").Image; +const App = @import("main.zig").App; + +view: *gpu.TextureView, +sampler: *gpu.Sampler, + +const CubeMap = @This(); + +const CubeMapPaths = struct { + pos_x: []const u8, + neg_x: []const u8, + pos_y: []const u8, + neg_y: []const u8, + pos_z: []const u8, + neg_z: []const u8, +}; + +pub fn init(paths: CubeMapPaths, allocator: std.mem.Allocator, app: *App) !CubeMap { + var images: [6]Image = undefined; + images[0] = try Image.fromFilePath(allocator, paths.pos_x); + defer images[0].deinit(); + images[1] = try Image.fromFilePath(allocator, paths.neg_x); + defer images[1].deinit(); + images[2] = try Image.fromFilePath(allocator, paths.pos_y); + defer images[2].deinit(); + images[3] = try Image.fromFilePath(allocator, paths.neg_y); + defer images[3].deinit(); + images[4] = try Image.fromFilePath(allocator, paths.pos_z); + defer images[4].deinit(); + images[5] = try Image.fromFilePath(allocator, paths.neg_z); + defer images[5].deinit(); + + const img_size = gpu.Extent3D{ + .width = @as(u32, @intCast(images[0].width)), + .height = @as(u32, @intCast(images[1].height)), + }; + + const tex_size = gpu.Extent3D{ + .width = @as(u32, @intCast(images[0].width)), + .height = @as(u32, @intCast(images[1].height)), + .depth_or_array_layers = 6, + }; + + const texture = app.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 = app.device.createCommandEncoder(null); + + var staging_buf: [6]*gpu.Buffer = undefined; + for (0..6) |i| { + staging_buf[i] = app.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| { + const staging_map = staging_buf[i].getMappedRange(u32, 0, @as(u64, @intCast(images[0].width)) * @as(u64, @intCast(images[0].height))); + @memcpy(staging_map.?, @as([]u32, @ptrCast(@alignCast(pixels)))); + staging_buf[i].unmap(); + }, + else => @panic("unsupported image color format"), + } + + const copy_buf = gpu.ImageCopyBuffer{ + .layout = data_layout, + .buffer = staging_buf[i], + }; + const copy_tex = gpu.ImageCopyTexture{ + .texture = texture, + .origin = gpu.Origin3D{ .x = 0, .y = 0, .z = @as(u32, @intCast(i)) }, + }; + + encoder.copyBufferToTexture(&copy_buf, &copy_tex, &img_size); + staging_buf[i].release(); + } + + var command = encoder.finish(null); + encoder.release(); + + app.queue.submit(&[_]*gpu.CommandBuffer{command}); + command.release(); + + const view = texture.createView( + &gpu.TextureView.Descriptor{ .dimension = .dimension_cube }, + ); + texture.release(); + + const sampler = app.device.createSampler(&.{ + .mag_filter = .linear, + .min_filter = .linear, + }); + + return .{ + .view = view, + .sampler = sampler, + }; +} diff --git a/src/main.zig b/src/main.zig @@ -6,6 +6,7 @@ const builtin = @import("builtin"); const RenderPipeline = @import("render_pipeline.zig").RenderPipeline; const MeshRenderPipeline = @import("render_pipeline.zig").MeshRenderPipeline; const SkyBoxRenderPipeline = @import("render_pipeline.zig").SkyBoxRenderPipeline; +const CubeMap = @import("cubemap.zig"); const Camera = @import("camera.zig"); const input = @import("input.zig"); @@ -535,6 +536,16 @@ pub fn main() !void { try app.init(allocator); defer app.deinit(); + const images = .{ + .pos_x = "cube/cube-pos-x.png", + .neg_x = "cube/cube-neg-x.png", + .pos_y = "cube/cube-pos-y.png", + .neg_y = "cube/cube-neg-y.png", + .pos_z = "cube/cube-pos-z.png", + .neg_z = "cube/cube-neg-z.png", + }; + const cube_map = try CubeMap.init(images, allocator, &app); + var camera = Camera{}; camera.logParams(); @@ -562,7 +573,7 @@ pub fn main() !void { }); var rp = try MeshRenderPipeline.init(&app, &mesh_buffer, allocator); - var skybox = SkyBoxRenderPipeline.init(&app); + var skybox = SkyBoxRenderPipeline.init(&app, cube_map); const pipelines = [_]RenderPipeline{ rp.pipeline(), skybox.pipeline() }; diff --git a/src/render_pipeline.zig b/src/render_pipeline.zig @@ -6,6 +6,7 @@ const MeshBuffer = @import("main.zig").MeshBuffer; const UniformData = @import("main.zig").UniformData; const f32x4 = @import("main.zig").f32x4; const mat4 = @import("main.zig").mat4; +const CubeMap = @import("cubemap.zig"); pub const RenderPipeline = struct { ptr: *anyopaque, @@ -290,7 +291,7 @@ pub const SkyBoxRenderPipeline = struct { inv_proj: mat4, }; - pub fn init(app: *App) SkyBoxRenderPipeline { + pub fn init(app: *App, cube_map: CubeMap) SkyBoxRenderPipeline { const vs = \\ struct VertexOutput { \\ @builtin(position) clip_position: vec4<f32>, @@ -325,6 +326,9 @@ pub const SkyBoxRenderPipeline = struct { \\ inv_proj: mat4x4<f32>, \\ }; \\ + \\ @group(0) @binding(1) var cubemap_sampler: sampler; + \\ @group(0) @binding(2) var cubemap_texture: texture_cube<f32>; + \\ \\ struct VertexOutput { \\ @builtin(position) clip_position: vec4<f32>, \\ @location(0) position: vec4<f32>, @@ -333,8 +337,10 @@ pub const SkyBoxRenderPipeline = struct { \\ @fragment fn main(in: VertexOutput) -> @location(0) vec4<f32> { \\ let inv_cam = uniform_data.inv_view * uniform_data.inv_proj; \\ let pos_world = inv_cam * in.position; - \\ let dir = normalize(pos_world.xyz / pos_world.w); - \\ return vec4<f32>((dir + 1) * 0.5, 1.0); + \\ let dir = normalize(pos_world.xyz / pos_world.w) * vec3<f32>(-1, 1, 1); + \\ let sample = textureSample(cubemap_texture, cubemap_sampler, dir); + \\ //return vec4<f32>((dir + 1) * 0.5, 1.0); + \\ return sample; \\ } ; const fs_module = app.device.createShaderModuleWGSL("skybox fragment shader", fs); @@ -363,6 +369,8 @@ pub const SkyBoxRenderPipeline = struct { .layout = bind_group_layout, .entries = &.{ gpu.BindGroup.Entry.buffer(0, uniform_buffer, 0, @sizeOf(SkyBoxRenderPipeline.UniformData)), + gpu.BindGroup.Entry.sampler(1, cube_map.sampler), + gpu.BindGroup.Entry.textureView(2, cube_map.view), }, }); const bind_group = app.device.createBindGroup(&bind_group_descriptor); @@ -401,6 +409,14 @@ pub const SkyBoxRenderPipeline = struct { .vertex = false, .fragment = true, }, .uniform, false, 0), + gpu.BindGroupLayout.Entry.sampler(1, .{ + .vertex = false, + .fragment = true, + }, .filtering), + gpu.BindGroupLayout.Entry.texture(2, .{ + .vertex = false, + .fragment = true, + }, .float, .dimension_cube, false), }, }); return device.createBindGroupLayout(&descriptor);