Files
voxel-game/src/engine/Skybox.zig
2026-05-13 05:40:31 +02:00

787 lines
26 KiB
Zig

const Skybox = @This();
const std = @import("std");
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 StagingBuffer = @import("StagingBuffer.zig");
image: vk.Image,
image_view: vk.ImageView,
device_memory: vk.DeviceMemory,
vertex_buffer: GenericBuffer(void, 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,
engine: *Engine,
stbi: *media.stbi,
cube_size: u32,
global_uniforms_buffer: vk.Buffer,
render_pass: vk.RenderPass,
temp_allocator: std.mem.Allocator,
io: std.Io,
) !Skybox {
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, temp_allocator, .unlimited);
defer temp_allocator.free(file_buf);
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 StagingBuffer.init(engine, .{
.capacity = @intCast(img.width * img.height * @sizeOf(vm.ColorHdr)),
.target_queue = .compute,
});
defer staging_buffer.deinit(engine);
const staging_memory = try staging_buffer.map(engine);
@memcpy(staging_memory, @as([*]const u8, @ptrCast(img.data)));
staging_buffer.unmap(engine);
// --- 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_bit" = true },
.tiling = .optimal,
.usage = .{
.transfer_dst_bit = true,
.sampled_bit = 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_bit = 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_bit = 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(engine, .transfer);
defer transfer_command_buffer.deinit(engine);
try transfer_command_buffer.beginCommandBuffer();
transfer_command_buffer.pipelineBarrier(.{
.src_stage_mask = .{ .top_of_pipe_bit = true },
.dst_stage_mask = .{ .transfer_bit = true },
.image_memory_barriers = &.{
.{
.src_access_mask = .{},
.dst_access_mask = .{ .transfer_write_bit = 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_bit = 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_bit = 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(engine, .{
.signal_semaphores = &.{semaphore_transfer_transition},
});
// --- TRANSITION TO SHADER_READ_ONLY_OPTIMAL ------------------------------
var transition1_command_buffer = try CommandBuffer.init(engine, .compute);
defer transition1_command_buffer.deinit(engine);
try transition1_command_buffer.beginCommandBuffer();
transition1_command_buffer.pipelineBarrier(.{
.src_stage_mask = .{ .transfer_bit = true },
.dst_stage_mask = .{ .compute_shader_bit = true },
.image_memory_barriers = &.{
.{
.src_access_mask = .{ .transfer_write_bit = true },
.dst_access_mask = .{ .shader_read_bit = 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_bit = 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(engine, .{
.wait_semaphores = &.{.{ .semaphore = semaphore_transfer_transition }},
.signal_semaphores = &.{semaphore_transition_compute},
});
// --- CREATE CUBEMAP IMAGE ------------------------------------------------
const cubemap_image = try engine.createImage(.{
.flags = .{ .cube_compatible_bit = true },
.image_type = .@"2d",
.format = .r16g16b16a16_sfloat,
.extent = .{
.width = cube_size,
.height = cube_size,
.depth = 1,
},
.mip_levels = 1,
.array_layers = 6,
.samples = .{ .@"1_bit" = true },
.tiling = .optimal,
.usage = .{
.sampled_bit = true,
.storage_bit = 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_bit = 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_bit = true },
.base_mip_level = 0,
.level_count = 1,
.base_array_layer = 0,
.layer_count = 6,
},
});
errdefer engine.destroyImageView(cubemap_image_view);
// --- 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_bit = true },
.immutable_samplers = &.{sampler},
},
.{
.binding = 1,
.descriptor_type = .sampled_image,
.descriptor_count = 1,
.stage_flags = .{ .compute_bit = true },
},
.{
.binding = 2,
.descriptor_type = .storage_image,
.descriptor_count = 1,
.stage_flags = .{ .compute_bit = 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_comp_spv });
defer engine.destroyShaderModule(compute_shader);
var compute_pipeline: vk.Pipeline = undefined;
_ = try engine.device.createComputePipelines(.null_handle, &.{
.{
.stage = .{
.stage = .{ .compute_bit = true },
.module = compute_shader,
.p_name = "main",
},
.layout = compute_pipeline_layout,
.base_pipeline_handle = .null_handle,
.base_pipeline_index = -1,
},
}, &engine.vk_allocator.interface, @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,
.image_layout = .general,
},
},
},
},
},
});
// --- COMMAND BUFFERS -----------------------------------------------------
// 1. Run compute shader
var compute_command_buffer = try CommandBuffer.init(engine, .compute);
defer compute_command_buffer.deinit(engine);
try compute_command_buffer.beginCommandBuffer();
compute_command_buffer.pipelineBarrier(.{
.src_stage_mask = .{ .top_of_pipe_bit = true },
.dst_stage_mask = .{ .compute_shader_bit = true },
.image_memory_barriers = &.{
.{
.src_access_mask = .{},
.dst_access_mask = .{ .shader_write_bit = 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_bit = 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(engine, .{
.wait_semaphores = &.{
.{
.semaphore = semaphore_transition_compute,
.stage_flags = .{ .compute_shader_bit = true },
},
},
.signal_semaphores = &.{semaphore_compute_transition},
});
// 2. Transition cubemap
var transition2_command_buffer = try CommandBuffer.init(engine, .graphics);
defer transition2_command_buffer.deinit(engine);
try transition2_command_buffer.beginCommandBuffer();
transition2_command_buffer.pipelineBarrier(.{
.src_stage_mask = .{ .compute_shader_bit = true },
.dst_stage_mask = .{ .fragment_shader_bit = true },
.image_memory_barriers = &.{
.{
.src_access_mask = .{ .shader_write_bit = true },
.dst_access_mask = .{ .shader_read_bit = 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_bit = 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(engine, .{
.wait_semaphores = &.{.{ .semaphore = semaphore_compute_transition }},
.fence = fence,
});
// 3. Synchronize
try engine.waitForFence(fence);
// --- SKYBOX PIPELINE -----------------------------------------------------
var vertex_buffer = try GenericBuffer(void, vm.Vector3).init(engine, .{
.usage = .vertex,
.target_queue = .graphics,
.array_capacity = 8,
});
errdefer vertex_buffer.deinit(engine);
// 6━━━━7
// ╱│ ╱┃
// 4━┿━━5 ┃ Z
// ┃ │ ┃ ┃ │
// ┃ 2──╂─3 │ Y
// ┃╱ ┃╱ │╱
// 0━━━━1 O────X
try vertex_buffer.write(engine, .{
.elements = &.{
.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 = try GenericBuffer(void, u16).init(engine, .{
.usage = .index,
.target_queue = .graphics,
.array_capacity = 36,
});
errdefer index_buffer.deinit(engine);
try index_buffer.write(engine, .{
.elements = &.{
// 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_bit = true },
},
.{
.binding = 1,
.descriptor_type = .sampler,
.descriptor_count = 1,
.stage_flags = .{ .fragment_bit = true },
.immutable_samplers = &.{sampler},
},
.{
.binding = 2,
.descriptor_type = .sampled_image,
.descriptor_count = 1,
.stage_flags = .{ .fragment_bit = 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 vertex_shader = try engine.createShaderModule(.{ .code = &shaders.skybox_vert_spv });
defer engine.destroyShaderModule(vertex_shader);
const fragment_shader = try engine.createShaderModule(.{ .code = &shaders.skybox_frag_spv });
defer engine.destroyShaderModule(fragment_shader);
const pipeline = try engine.createGraphicsPipeline(.{
.stages = &.{
.{
.stage = .{ .vertex_bit = true },
.module = vertex_shader,
.name = "main",
},
.{
.stage = .{ .fragment_bit = true },
.module = fragment_shader,
.name = "main",
},
},
.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_bit = 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_bit" = 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_bit = true,
.g_bit = true,
.b_bit = true,
.a_bit = true,
},
},
},
.blend_constants = .{ 0, 0, 0, 0 },
},
.dynamic_state = .{
.dynamic_states = &.{ .viewport, .scissor },
},
.layout = pipeline_layout,
.render_pass = render_pass,
.subpass = 0,
});
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, engine: *Engine) void {
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(engine);
self.vertex_buffer.deinit(engine);
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 });
}