Files
voxel-game/src/engine/Skybox.zig
2026-10-12 00:45:24 +02:00

812 lines
27 KiB
Zig

const Skybox = @This();
const std = @import("std");
const ctx = @import("../AppContext.zig");
const media = @import("media");
const shaders = @import("../shaders.zig");
const vk = @import("vulkan");
const vm = @import("vecmath");
const CommandBuffer = @import("CommandBuffer.zig");
const Engine = @import("Engine.zig");
const GenericBuffer = @import("GenericBuffer.zig").GenericBuffer;
const Swapchain = @import("Swapchain.zig");
const Texture = @import("Texture.zig");
image: vk.Image,
image_view: vk.ImageView,
device_memory: vk.DeviceMemory,
vertex_buffer: GenericBuffer(vm.Vector3),
index_buffer: shaders.IndexBuffer,
sampler: vk.Sampler,
descriptor_set_layout: vk.DescriptorSetLayout,
descriptor_pool: vk.DescriptorPool,
descriptor_set: vk.DescriptorSet,
pipeline_layout: vk.PipelineLayout,
pipeline: vk.Pipeline,
pub fn load(
filename: []const u8,
cube_size: u32,
global_uniforms_buffer: vk.Buffer,
) !Skybox {
const allocator_frame = ctx.allocator_frame;
const io = ctx.io;
const stbi = ctx.stbi;
const engine = ctx.engine;
const swapchain = ctx.swapchain;
std.log.debug("Loading skybox \"{s}\"...", .{filename});
// --- LOAD IMAGE FROM MEMORY ----------------------------------------------
const cwd = std.Io.Dir.cwd();
var dir = try cwd.openDir(io, "assets", .{});
defer dir.close(io);
const file_buf = try dir.readFileAlloc(io, filename, allocator_frame, .unlimited);
const img = try stbi.loadHdrBuf(file_buf);
defer stbi.freeHdr(img);
// --- SYNCHRONIZATION PRIMITIVES ------------------------------------------
const semaphore_transfer_transition = try engine.createSemaphore();
defer engine.destroySemaphore(semaphore_transfer_transition);
const semaphore_transition_compute = try engine.createSemaphore();
defer engine.destroySemaphore(semaphore_transition_compute);
const semaphore_compute_transition = try engine.createSemaphore();
defer engine.destroySemaphore(semaphore_compute_transition);
const fence = try engine.createFence(.{});
defer engine.destroyFence(fence);
// --- LOAD IMAGE INTO STAGING BUFFER --------------------------------------
var staging_buffer = try GenericBuffer(vm.ColorHdr).init(.{
.usage = .staging,
.target_queue = .compute,
.mappable = true,
.capacity = img.width * img.height,
});
defer staging_buffer.deinit();
{
const staging_memory = try staging_buffer.mapSlice();
defer staging_buffer.unmap();
@memcpy(staging_memory, img.data);
try staging_buffer.flush(0, img.width * img.height);
}
// --- CREATE EQUIRECTANGULAR IMAGE ----------------------------------------
const equirect_image = try engine.createImage(.{
.image_type = .@"2d",
.format = .r16g16b16a16_sfloat,
.extent = .{
.width = img.width,
.height = img.height,
.depth = 1,
},
.mip_levels = 1,
.array_layers = 1,
.samples = .{ .@"1" = true },
.tiling = .optimal,
.usage = .{
.transfer_dst = true,
.sampled = true,
},
.queue_family_indices = &.{
engine.compute_queue.allocation.family,
engine.transfer_queue.allocation.family,
},
.initial_layout = .undefined,
});
defer engine.destroyImage(equirect_image);
const equirect_memory_requirements = engine.getImageMemoryRequirements(equirect_image);
const equirect_device_memory = try engine.allocate(equirect_memory_requirements, .{ .device_local = true });
defer engine.freeMemory(equirect_device_memory);
try engine.bindImageMemory(equirect_image, equirect_device_memory, 0);
const equirect_image_view = try engine.createImageView(.{
.image = equirect_image,
.view_type = .@"2d",
.format = .r16g16b16a16_sfloat,
.subresource_range = .{
.aspect_mask = .{ .color = true },
.base_mip_level = 0,
.level_count = 1,
.base_array_layer = 0,
.layer_count = 1,
},
});
defer engine.destroyImageView(equirect_image_view);
// --- TRANSITION TO TRANSFER_DST_OPTIMAL AND COPY -------------------------
var transfer_command_buffer = try CommandBuffer.init(.transfer);
defer transfer_command_buffer.deinit();
try transfer_command_buffer.beginCommandBuffer();
transfer_command_buffer.pipelineBarrier(.{
.src_stage_mask = .{ .top_of_pipe = true },
.dst_stage_mask = .{ .transfer = true },
.image_memory_barriers = &.{
.{
.src_access_mask = .{},
.dst_access_mask = .{ .transfer_write = true },
.old_layout = .undefined,
.new_layout = .transfer_dst_optimal,
.src_queue_family_index = vk.QUEUE_FAMILY_IGNORED,
.dst_queue_family_index = vk.QUEUE_FAMILY_IGNORED,
.image = equirect_image,
.subresource_range = .{
.aspect_mask = .{ .color = true },
.base_mip_level = 0,
.level_count = 1,
.base_array_layer = 0,
.layer_count = 1,
},
},
},
});
transfer_command_buffer.copyBufferToImage(
staging_buffer.buffer,
equirect_image,
.transfer_dst_optimal,
&.{
.{
.buffer_offset = 0,
.buffer_row_length = img.width,
.buffer_image_height = img.height,
.image_subresource = .{
.aspect_mask = .{ .color = true },
.mip_level = 0,
.base_array_layer = 0,
.layer_count = 1,
},
.image_offset = .{ .x = 0, .y = 0, .z = 0 },
.image_extent = .{ .width = img.width, .height = img.height, .depth = 1 },
},
},
);
try transfer_command_buffer.endCommandBuffer();
try transfer_command_buffer.submit(.{
.signal_semaphores = &.{semaphore_transfer_transition},
});
// --- TRANSITION TO SHADER_READ_ONLY_OPTIMAL ------------------------------
var transition1_command_buffer = try CommandBuffer.init(.compute);
defer transition1_command_buffer.deinit();
try transition1_command_buffer.beginCommandBuffer();
transition1_command_buffer.pipelineBarrier(.{
.src_stage_mask = .{ .transfer = true },
.dst_stage_mask = .{ .compute_shader = true },
.image_memory_barriers = &.{
.{
.src_access_mask = .{ .transfer_write = true },
.dst_access_mask = .{ .shader_read = true },
.old_layout = .transfer_dst_optimal,
.new_layout = .shader_read_only_optimal,
.src_queue_family_index = vk.QUEUE_FAMILY_IGNORED,
.dst_queue_family_index = vk.QUEUE_FAMILY_IGNORED,
.image = equirect_image,
.subresource_range = .{
.aspect_mask = .{ .color = true },
.base_mip_level = 0,
.level_count = 1,
.base_array_layer = 0,
.layer_count = 1,
},
},
},
});
try transition1_command_buffer.endCommandBuffer();
try transition1_command_buffer.submit(.{
.wait_semaphores = &.{.{ .semaphore = semaphore_transfer_transition }},
.signal_semaphores = &.{semaphore_transition_compute},
});
// --- CREATE CUBEMAP IMAGE ------------------------------------------------
const cubemap_image = try engine.createImage(.{
.flags = .{ .cube_compatible = true },
.image_type = .@"2d",
.format = .r16g16b16a16_sfloat,
.extent = .{
.width = cube_size,
.height = cube_size,
.depth = 1,
},
.mip_levels = 1,
.array_layers = 6,
.samples = .{ .@"1" = true },
.tiling = .optimal,
.usage = .{
.sampled = true,
.storage = true,
},
.queue_family_indices = &.{
engine.graphics_queue.allocation.family,
engine.compute_queue.allocation.family,
},
.initial_layout = .undefined,
});
errdefer engine.destroyImage(cubemap_image);
const cubemap_memory_requirements = engine.getImageMemoryRequirements(cubemap_image);
const cubemap_device_memory = try engine.allocate(cubemap_memory_requirements, .{ .device_local = true });
errdefer engine.freeMemory(cubemap_device_memory);
try engine.bindImageMemory(cubemap_image, cubemap_device_memory, 0);
const cubemap_image_view = try engine.createImageView(.{
.image = cubemap_image,
.view_type = .cube,
.format = .r16g16b16a16_sfloat,
.subresource_range = .{
.aspect_mask = .{ .color = true },
.base_mip_level = 0,
.level_count = 1,
.base_array_layer = 0,
.layer_count = 6,
},
});
errdefer engine.destroyImageView(cubemap_image_view);
// NOTE Because slang cannot conceive of such a thing as RWTextureCube
// (equivalent to imageCube in GLSL, which docs claim exists, but it
// doesn't), we have to resort to RWTexture2DArray (WTexture2DArray to be
// specific) in the compute shader. We have to make another image view with
// appropriate view_type to pass validation.
const cubemap_image_view_2d_array = try engine.createImageView(.{
.image = cubemap_image,
.view_type = .@"2d_array",
.format = .r16g16b16a16_sfloat,
.subresource_range = .{
.aspect_mask = .{ .color = true },
.base_mip_level = 0,
.level_count = 1,
.base_array_layer = 0,
.layer_count = 6,
},
});
defer engine.destroyImageView(cubemap_image_view_2d_array);
// --- PIPELINE AND DESCRIPTORS --------------------------------------------
const sampler = try engine.createSampler(.{
.mag_filter = .linear,
.min_filter = .linear,
.mipmap_mode = .linear,
.address_mode_u = .repeat,
.address_mode_v = .repeat,
.address_mode_w = .repeat,
.mip_lod_bias = 0,
.anisotropy_enable = .false,
.max_anisotropy = 0,
.compare_enable = .false,
.compare_op = .always,
.min_lod = 0,
.max_lod = vk.LOD_CLAMP_NONE,
.border_color = .float_transparent_black,
.unnormalized_coordinates = .false,
});
errdefer engine.destroySampler(sampler);
const compute_descriptor_set_layout = try engine.createDescriptorSetLayout(.{
.bindings = &.{
.{
.binding = 0,
.descriptor_type = .sampler,
.descriptor_count = 1,
.stage_flags = .{ .compute = true },
.immutable_samplers = &.{sampler},
},
.{
.binding = 1,
.descriptor_type = .sampled_image,
.descriptor_count = 1,
.stage_flags = .{ .compute = true },
},
.{
.binding = 2,
.descriptor_type = .storage_image,
.descriptor_count = 1,
.stage_flags = .{ .compute = true },
},
},
});
defer engine.destroyDescriptorSetLayout(compute_descriptor_set_layout);
const compute_pipeline_layout = try engine.createPipelineLayout(.{
.set_layouts = &.{compute_descriptor_set_layout},
});
defer engine.destroyPipelineLayout(compute_pipeline_layout);
const compute_shader = try engine.createShaderModule(.{ .code = &shaders.equirect_to_cube_spv });
defer engine.destroyShaderModule(compute_shader);
var compute_pipeline: vk.Pipeline = undefined;
_ = try engine.device.createComputePipelines(.null_handle, &.{
.{
.stage = .{
.stage = .{ .compute = true },
.module = compute_shader,
.p_name = "main",
},
.layout = compute_pipeline_layout,
.base_pipeline_handle = .null_handle,
.base_pipeline_index = -1,
},
}, ctx.vk_allocator.capture(), @ptrCast(&compute_pipeline));
defer engine.destroyPipeline(compute_pipeline);
const compute_descriptor_pool = try engine.createDescriptorPool(.{
.max_sets = 1,
.pool_sizes = &.{
.{
.type = .sampler,
.descriptor_count = 1,
},
.{
.type = .sampled_image,
.descriptor_count = 1,
},
.{
.type = .storage_image,
.descriptor_count = 1,
},
},
});
defer engine.destroyDescriptorPool(compute_descriptor_pool);
const compute_descriptor_set = try engine.allocateDescriptorSet(.{
.descriptor_pool = compute_descriptor_pool,
.set_layout = compute_descriptor_set_layout,
});
try engine.updateDescriptorSets(.{
.writes = &.{
.{
.dst_set = compute_descriptor_set,
.dst_binding = 1,
.dst_array_element = 0,
.descriptor_type = .sampled_image,
.descriptor_infos = .{
.image = &.{
.{
.sampler = .null_handle,
.image_view = equirect_image_view,
.image_layout = .shader_read_only_optimal,
},
},
},
},
.{
.dst_set = compute_descriptor_set,
.dst_binding = 2,
.dst_array_element = 0,
.descriptor_type = .storage_image,
.descriptor_infos = .{
.image = &.{
.{
.sampler = .null_handle,
.image_view = cubemap_image_view_2d_array,
.image_layout = .general,
},
},
},
},
},
});
// --- COMMAND BUFFERS -----------------------------------------------------
// 1. Run compute shader
var compute_command_buffer = try CommandBuffer.init(.compute);
defer compute_command_buffer.deinit();
try compute_command_buffer.beginCommandBuffer();
compute_command_buffer.pipelineBarrier(.{
.src_stage_mask = .{ .top_of_pipe = true },
.dst_stage_mask = .{ .compute_shader = true },
.image_memory_barriers = &.{
.{
.src_access_mask = .{},
.dst_access_mask = .{ .shader_write = true },
.old_layout = .undefined,
.new_layout = .general,
.src_queue_family_index = vk.QUEUE_FAMILY_IGNORED,
.dst_queue_family_index = vk.QUEUE_FAMILY_IGNORED,
.image = cubemap_image,
.subresource_range = .{
.aspect_mask = .{ .color = true },
.base_mip_level = 0,
.level_count = 1,
.base_array_layer = 0,
.layer_count = 6,
},
},
},
});
compute_command_buffer.bindPipeline(.compute, compute_pipeline);
compute_command_buffer.bindDescriptorSet(.compute, compute_pipeline_layout, 0, compute_descriptor_set, null);
compute_command_buffer.proxy.dispatch(@divExact(cube_size, 8), @divExact(cube_size, 8), 6);
try compute_command_buffer.endCommandBuffer();
try compute_command_buffer.submit(.{
.wait_semaphores = &.{
.{
.semaphore = semaphore_transition_compute,
.stage_flags = .{ .compute_shader = true },
},
},
.signal_semaphores = &.{semaphore_compute_transition},
});
// 2. Transition cubemap
var transition2_command_buffer = try CommandBuffer.init(.graphics);
defer transition2_command_buffer.deinit();
try transition2_command_buffer.beginCommandBuffer();
transition2_command_buffer.pipelineBarrier(.{
.src_stage_mask = .{ .compute_shader = true },
.dst_stage_mask = .{ .fragment_shader = true },
.image_memory_barriers = &.{
.{
.src_access_mask = .{ .shader_write = true },
.dst_access_mask = .{ .shader_read = true },
.old_layout = .general,
.new_layout = .shader_read_only_optimal,
.src_queue_family_index = vk.QUEUE_FAMILY_IGNORED,
.dst_queue_family_index = vk.QUEUE_FAMILY_IGNORED,
.image = cubemap_image,
.subresource_range = .{
.aspect_mask = .{ .color = true },
.base_mip_level = 0,
.level_count = 1,
.base_array_layer = 0,
.layer_count = 6,
},
},
},
});
try transition2_command_buffer.endCommandBuffer();
try transition2_command_buffer.submit(.{
.wait_semaphores = &.{.{ .semaphore = semaphore_compute_transition }},
.fence = fence,
});
// 3. Synchronize
try engine.waitForFence(fence);
// --- SKYBOX PIPELINE -----------------------------------------------------
var vertex_buffer = try GenericBuffer(vm.Vector3).init(.{
.usage = .vertex,
.target_queue = .graphics,
.capacity = 8,
});
errdefer vertex_buffer.deinit();
// 6━━━━7
// ╱│ ╱┃
// 4━┿━━5 ┃ Z
// ┃ │ ┃ ┃ │
// ┃ 2──╂─3 │ Y
// ┃╱ ┃╱ │╱
// 0━━━━1 O────X
try vertex_buffer.writeSlice(&.{
.init(-1, -1, -1),
.init(1, -1, -1),
.init(-1, 1, -1),
.init(1, 1, -1),
.init(-1, -1, 1),
.init(1, -1, 1),
.init(-1, 1, 1),
.init(1, 1, 1),
});
var index_buffer: shaders.IndexBuffer = try .init(.{
.usage = .index,
.target_queue = .graphics,
.capacity = 36,
});
errdefer index_buffer.deinit();
try index_buffer.writeSlice(&.{
// Positive X
3, 1, 7,
7, 1, 5,
// Negative X
0, 2, 4,
4, 2, 6,
// Positive Y
2, 3, 6,
6, 3, 7,
// Negative Y
1, 0, 5,
5, 0, 4,
// Positive Z
6, 7, 4,
4, 7, 5,
// Negative Z
0, 1, 2,
2, 1, 3,
});
const descriptor_set_layout = try engine.createDescriptorSetLayout(.{
.bindings = &.{
.{
.binding = 0,
.descriptor_type = .uniform_buffer,
.descriptor_count = 1,
.stage_flags = .{ .vertex = true },
},
.{
.binding = 1,
.descriptor_type = .sampler,
.descriptor_count = 1,
.stage_flags = .{ .fragment = true },
.immutable_samplers = &.{sampler},
},
.{
.binding = 2,
.descriptor_type = .sampled_image,
.descriptor_count = 1,
.stage_flags = .{ .fragment = true },
},
},
});
errdefer engine.destroyDescriptorSetLayout(descriptor_set_layout);
const pipeline_layout = try engine.createPipelineLayout(.{
.set_layouts = &.{descriptor_set_layout},
});
errdefer engine.destroyPipelineLayout(pipeline_layout);
const descriptor_pool = try engine.createDescriptorPool(.{
.max_sets = 1,
.pool_sizes = &.{
.{
.type = .uniform_buffer,
.descriptor_count = 1,
},
.{
.type = .sampler,
.descriptor_count = 1,
},
.{
.type = .sampled_image,
.descriptor_count = 1,
},
},
});
errdefer engine.destroyDescriptorPool(compute_descriptor_pool);
const descriptor_set = try engine.allocateDescriptorSet(.{
.descriptor_pool = descriptor_pool,
.set_layout = descriptor_set_layout,
});
try engine.updateDescriptorSets(.{
.writes = &.{
.{
.dst_set = descriptor_set,
.dst_binding = 0,
.dst_array_element = 0,
.descriptor_type = .uniform_buffer,
.descriptor_infos = .{
.buffer = &.{
.{
.buffer = global_uniforms_buffer,
.offset = 0,
.range = vk.WHOLE_SIZE,
},
},
},
},
.{
.dst_set = descriptor_set,
.dst_binding = 2,
.dst_array_element = 0,
.descriptor_type = .sampled_image,
.descriptor_infos = .{
.image = &.{
.{
.sampler = .null_handle,
.image_view = cubemap_image_view,
.image_layout = .shader_read_only_optimal,
},
},
},
},
},
});
const shader = try engine.createShaderModule(.{ .code = &shaders.skybox_spv });
defer engine.destroyShaderModule(shader);
const pipeline = try engine.createGraphicsPipeline(.{
.stages = &.{
.{
.stage = .{ .vertex = true },
.module = shader,
.name = "vertexMain",
},
.{
.stage = .{ .fragment = true },
.module = shader,
.name = "fragmentMain",
},
},
.vertex_input_state = .{
.vertex_binding_descriptions = &.{
.{
.binding = 0,
.stride = @sizeOf([3]f32),
.input_rate = .vertex,
},
},
.vertex_attribute_descriptions = &.{
.{
.location = 0,
.binding = 0,
.format = .r32g32b32_sfloat,
.offset = 0,
},
},
},
.input_assembly_state = .{
.topology = .triangle_list,
.primitive_restart_enable = .false,
},
.viewport_state = .{
.viewports = &.{undefined}, // dynamic
.scissors = &.{undefined}, // dynamic
},
.rasterization_state = .{
.depth_clamp_enable = .false,
.rasterizer_discard_enable = .false,
.polygon_mode = .fill,
.cull_mode = .{ .back = true },
.front_face = .counter_clockwise,
.depth_bias_enable = .false,
.depth_bias_constant_factor = 0,
.depth_bias_clamp = 0,
.depth_bias_slope_factor = 0,
.line_width = 1,
},
.multisample_state = .{
.rasterization_samples = .{ .@"1" = true },
.sample_shading_enable = .false,
.min_sample_shading = 1,
.alpha_to_coverage_enable = .false,
.alpha_to_one_enable = .false,
},
.depth_stencil_state = .{
.depth_test_enable = .true,
.depth_write_enable = .false,
.depth_compare_op = .equal,
.depth_bounds_test_enable = .false,
.stencil_test_enable = .false,
.front = .{
.fail_op = .keep,
.pass_op = .keep,
.depth_fail_op = .keep,
.compare_op = .never,
.compare_mask = 0,
.write_mask = 0,
.reference = 0,
},
.back = .{
.fail_op = .keep,
.pass_op = .keep,
.depth_fail_op = .keep,
.compare_op = .never,
.compare_mask = 0,
.write_mask = 0,
.reference = 0,
},
.min_depth_bounds = 0,
.max_depth_bounds = 1,
},
.color_blend_state = .{
.logic_op_enable = .false,
.logic_op = .copy,
.attachments = &.{
.{
.blend_enable = .false,
.src_color_blend_factor = .one,
.dst_color_blend_factor = .zero,
.color_blend_op = .add,
.src_alpha_blend_factor = .one,
.dst_alpha_blend_factor = .zero,
.alpha_blend_op = .add,
.color_write_mask = .{
.r = true,
.g = true,
.b = true,
.a = true,
},
},
},
.blend_constants = .{ 0, 0, 0, 0 },
},
.dynamic_state = .{
.dynamic_states = &.{ .viewport, .scissor },
},
.layout = pipeline_layout,
.view_mask = 0,
.color_attachment_formats = &.{swapchain.params.surface_format.format},
.depth_attachment_format = Texture.Usage.depth.vkFormat(),
.stencil_attachment_format = .undefined,
});
return .{
.image = cubemap_image,
.image_view = cubemap_image_view,
.device_memory = cubemap_device_memory,
.vertex_buffer = vertex_buffer,
.index_buffer = index_buffer,
.sampler = sampler,
.descriptor_set_layout = descriptor_set_layout,
.descriptor_pool = descriptor_pool,
.descriptor_set = descriptor_set,
.pipeline_layout = pipeline_layout,
.pipeline = pipeline,
};
}
pub fn deinit(self: *Skybox) void {
const engine = ctx.engine;
engine.destroyPipeline(self.pipeline);
engine.destroyPipelineLayout(self.pipeline_layout);
engine.destroyDescriptorPool(self.descriptor_pool);
engine.destroyDescriptorSetLayout(self.descriptor_set_layout);
engine.destroySampler(self.sampler);
self.index_buffer.deinit();
self.vertex_buffer.deinit();
engine.destroyImageView(self.image_view);
engine.freeMemory(self.device_memory);
engine.destroyImage(self.image);
}
pub fn draw(self: *const Skybox, command_buffer: CommandBuffer) !void {
try command_buffer.bindVertexBuffers(0, &.{
.{
.buffer = self.vertex_buffer.buffer,
.offset = 0,
},
});
command_buffer.bindIndexBuffer(self.index_buffer.buffer, 0, .uint16);
command_buffer.bindPipeline(.graphics, self.pipeline);
command_buffer.bindDescriptorSet(.graphics, self.pipeline_layout, 0, self.descriptor_set, null);
command_buffer.drawIndexed(.{ .index_count = 36 });
}