package renderer // Reference directional-shadow adapter for examples/default app pipelines. // Do not grow this into a point-light shadow engine; generic Render_IR // render-target/pass substrate should enable external lighting engines. import "core:log" import "core:math" import "core:mem" import glsl "core:math/linalg/glsl" import bk "../backend" import "../resource" MAX_SHADOW_CASTERS :: 4 DEFAULT_SHADOW_ATLAS_SIZE :: 4096 // Push constants for shadow depth pass: light_proj_view + model = 128 bytes Push_Constants_Shadow :: struct { light_proj_view: glsl.mat4x4, // 64 bytes model: glsl.mat4x4, // 64 bytes } // std140-compatible UBO for shadow data passed to lit fragment shader Shadow_UBO_Data :: struct { light_matrices: [MAX_SHADOW_CASTERS]glsl.mat4x4, // 256 bytes shadow_count: [4]u32, // 16 bytes (x = count) shadow_params: [4]f32, // 16 bytes (x = bias, y = normal_bias, z = atlas_size, w = contact_steps) shadow_light_indices: [4]u32, // 16 bytes (maps shadow map idx -> light idx) atlas_regions: [MAX_SHADOW_CASTERS][4]f32, // 64 bytes (offset_u, offset_v, scale_u, scale_v) } Shadow_State :: struct { enabled: bool, bias_constant: f32, bias_slope: f32, shadow_half_extent: f32, shadow_depth: f32, // Depth-only render pass render_pass: bk.Render_Pass_Handle, // Shadow atlas atlas_texture: bk.Texture_Handle, atlas_framebuffer: bk.Framebuffer_Handle, atlas_size: u32, sampler: bk.Sampler_Handle, // Depth prepass depth_prepass_texture: bk.Texture_Handle, depth_prepass_framebuffer: bk.Framebuffer_Handle, depth_prepass_sampler: bk.Sampler_Handle, depth_prepass_width: u32, depth_prepass_height: u32, depth_format: bk.Format, // Light-space matrices light_matrices: [MAX_SHADOW_CASTERS]glsl.mat4x4, shadow_light_indices: [MAX_SHADOW_CASTERS]u32, shadow_count: u32, // Shadow UBO ubo_buffers: [bk.MAX_FRAMES_IN_FLIGHT]bk.Buffer_Handle, // Descriptor set for shadow data (set=2 in lit pipeline) descriptor_pool: bk.Descriptor_Handle, descriptor_set_layout: bk.Descriptor_Handle, descriptor_sets: [bk.MAX_FRAMES_IN_FLIGHT]bk.Descriptor_Handle, // Shadow depth pipelines (one per layout variant) depth_pipelines: [bk.MAX_LAYOUT_VARIANTS]bk.Pipeline_Handle, initialized: bool, } init_shadow_state :: proc(state: ^Shadow_State, b: ^bk.Backend, screen_width, screen_height: u32) -> bool { state.atlas_size = DEFAULT_SHADOW_ATLAS_SIZE state.bias_constant = 1.25 state.bias_slope = 1.75 state.shadow_half_extent = 15.0 state.shadow_depth = 40.0 state.depth_format = b.get_depth_format() // Create depth-only render pass (shared by atlas + depth prepass) rp, rp_ok := b.create_render_pass(bk.Render_Pass_Desc{ has_depth = true, depth_format = state.depth_format, depth_only = true, depth_load_op = .Clear, depth_store_op = .Store, depth_final_layout = .Depth_Stencil_Read_Only, }) if !rp_ok { log.error("gpu/renderer: failed to create shadow render pass") return false } state.render_pass = rp // Create shadow atlas depth image atlas_tex, atlas_ok := b.create_image(bk.Texture_Desc{ width = state.atlas_size, height = state.atlas_size, format = state.depth_format, usage = {.Depth_Stencil_Attachment, .Sampled}, }) if !atlas_ok { log.error("gpu/renderer: failed to create shadow atlas image") shutdown_shadow_state(state, b) return false } if !b.create_image_view(atlas_tex, state.depth_format, {.Depth}) { b.destroy_image(atlas_tex) log.error("gpu/renderer: failed to create shadow atlas image view") shutdown_shadow_state(state, b) return false } state.atlas_texture = atlas_tex // Create framebuffer for the atlas fb, fb_ok := b.create_framebuffer({ pass = state.render_pass, depth_view = atlas_tex, width = state.atlas_size, height = state.atlas_size, layers = 1, }) if !fb_ok { log.error("gpu/renderer: failed to create shadow atlas framebuffer") shutdown_shadow_state(state, b) return false } state.atlas_framebuffer = fb // Create comparison sampler for hardware shadow-map depth tests. sampler, sampler_ok := b.create_sampler(bk.Sampler_Desc{ mag_filter = .Linear, min_filter = .Linear, address_mode_u = .Clamp_To_Border, address_mode_v = .Clamp_To_Border, enable_compare = true, compare_op = .Less_Or_Equal, }) if !sampler_ok { log.error("gpu/renderer: failed to create shadow sampler") shutdown_shadow_state(state, b) return false } state.sampler = sampler // Create depth prepass resources if !create_depth_prepass(state, b, screen_width, screen_height) { log.error("gpu/renderer: failed to create depth prepass") shutdown_shadow_state(state, b) return false } // Create regular sampler for depth prepass (reads depth values, not comparison) dp_sampler, dp_sampler_ok := b.create_sampler(bk.Sampler_Desc{ mag_filter = .Nearest, min_filter = .Nearest, address_mode_u = .Clamp_To_Edge, address_mode_v = .Clamp_To_Edge, }) if !dp_sampler_ok { log.error("gpu/renderer: failed to create depth prepass sampler") shutdown_shadow_state(state, b) return false } state.depth_prepass_sampler = dp_sampler // Create descriptor set layout: binding 0 = UBO, binding 1 = shadow atlas, binding 3 = depth prepass // Note: binding 2 is skipped because Luma splits sampler2DShadow at binding 1 // into texture(1) + sampler(2), consuming binding 2 internally. layout_bindings := [3]bk.Descriptor_Set_Layout_Binding{ {binding = 0, type = .Uniform_Buffer, count = 1, stages = {.Fragment}}, {binding = 1, type = .Combined_Image_Sampler, count = 1, stages = {.Fragment}}, {binding = 3, type = .Combined_Image_Sampler, count = 1, stages = {.Fragment}}, } layout, layout_ok := b.create_descriptor_set_layout(layout_bindings[:]) if !layout_ok { log.error("gpu/renderer: failed to create shadow descriptor set layout") shutdown_shadow_state(state, b) return false } state.descriptor_set_layout = layout // Create descriptor pool types := [2]bk.Descriptor_Type{.Uniform_Buffer, .Combined_Image_Sampler} counts := [2]u32{bk.MAX_FRAMES_IN_FLIGHT, bk.MAX_FRAMES_IN_FLIGHT * 2} pool, pool_ok := b.create_descriptor_pool(bk.MAX_FRAMES_IN_FLIGHT, types[:], counts[:]) if !pool_ok { log.error("gpu/renderer: failed to create shadow descriptor pool") shutdown_shadow_state(state, b) return false } state.descriptor_pool = pool // Create per-frame UBO buffers and descriptor sets for i in 0..= MAX_SHADOW_CASTERS { break } light := &light_state.lights[i] if !light.enabled || !light.casts_shadow { continue } // Only directional lights get shadow maps if light.type != 0 { continue } light_dir := glsl.vec3{light.position.x, light.position.y, light.position.z} light_mat := compute_light_matrix(light_dir, camera_target, state.shadow_half_extent, state.shadow_depth) state.light_matrices[shadow_idx] = light_mat state.shadow_light_indices[shadow_idx] = u32(i) shadow_idx += 1 } state.shadow_count = shadow_idx if shadow_idx > 0 { // Render directional shadow atlas b.begin_render_pass(ctx, { pass = state.render_pass, framebuffer = state.atlas_framebuffer, width = state.atlas_size, height = state.atlas_size, clear_depth = 1, }) b.set_depth_bias(ctx, state.bias_constant, state.bias_slope) for i in 0.. bool { tex, tex_ok := b.create_image(bk.Texture_Desc{ width = width, height = height, format = state.depth_format, usage = {.Depth_Stencil_Attachment, .Sampled}, }) if !tex_ok { return false } if !b.create_image_view(tex, state.depth_format, {.Depth}) { b.destroy_image(tex) return false } state.depth_prepass_texture = tex fb, fb_ok := b.create_framebuffer({ pass = state.render_pass, depth_view = tex, width = width, height = height, layers = 1, }) if !fb_ok { b.destroy_image(tex) state.depth_prepass_texture = {} return false } state.depth_prepass_framebuffer = fb state.depth_prepass_width = width state.depth_prepass_height = height return true } @(private = "file") destroy_depth_prepass :: proc(state: ^Shadow_State, b: ^bk.Backend) { b.destroy_framebuffer(state.depth_prepass_framebuffer) b.destroy_image(state.depth_prepass_texture) state.depth_prepass_framebuffer = {} state.depth_prepass_texture = {} state.depth_prepass_width = 0 state.depth_prepass_height = 0 } @(private = "file") execute_depth_prepass :: proc(state: ^Shadow_State, b: ^bk.Backend, ctx: bk.Frame_Context, q: ^Draw_Queue_3D, proj_view: glsl.mat4x4) { if state.depth_prepass_width == 0 || state.depth_prepass_height == 0 { return } b.begin_render_pass(ctx, { pass = state.render_pass, framebuffer = state.depth_prepass_framebuffer, width = state.depth_prepass_width, height = state.depth_prepass_height, clear_depth = 1, }) b.set_viewport(ctx, 0, 0, f32(state.depth_prepass_width), f32(state.depth_prepass_height)) b.set_scissor(ctx, 0, 0, state.depth_prepass_width, state.depth_prepass_height) b.set_depth_bias(ctx, 0, 0) // no bias for depth prepass // Render all meshes with camera proj_view render_shadow_meshes(q, b, ctx, state.depth_pipelines, proj_view) b.end_render_pass(ctx) } // --- Atlas region computation --- Atlas_Region :: struct { x, y: i32, w, h: u32, } // Compute pixel-space atlas region for a shadow caster. // With 1 caster: full atlas. With 2-4: subdivide into quadrants. compute_atlas_region :: proc(index, count, atlas_size: u32) -> Atlas_Region { if count <= 1 { return {0, 0, atlas_size, atlas_size} } half := atlas_size / 2 switch index { case 0: return {0, 0, half, half} case 1: return {i32(half), 0, half, half} case 2: return {0, i32(half), half, half} case 3: return {i32(half), i32(half), half, half} } return {0, 0, half, half} } // Compute normalized UV region for shader sampling. compute_atlas_uv_region :: proc(index, count: u32) -> [4]f32 { if count <= 1 { return {0, 0, 1, 1} } switch index { case 0: return {0, 0, 0.5, 0.5} case 1: return {0.5, 0, 0.5, 0.5} case 2: return {0, 0.5, 0.5, 0.5} case 3: return {0.5, 0.5, 0.5, 0.5} } return {0, 0, 0.5, 0.5} } // --- Internal helpers --- @(private = "file") update_shadow_ubo :: proc(state: ^Shadow_State, b: ^bk.Backend, frame_index: u32) { ubo: Shadow_UBO_Data for i in 0.. glsl.mat4x4 { dir := normalize(light_dir) light_pos := camera_target - dir * (depth * 0.5) world_up: glsl.vec3 if math.abs(dir.y) > 0.99 { world_up = {0, 0, 1} } else { world_up = {0, 1, 0} } view := look_at(light_pos, camera_target, world_up) proj := ortho_shadow(-half_extent, half_extent, -half_extent, half_extent, 0, depth) return proj * view } @(private = "file") ortho_shadow :: proc(left, right, bottom, top, near, far: f32) -> glsl.mat4x4 { m: glsl.mat4x4 m[0, 0] = 2.0 / (right - left) m[1, 1] = -2.0 / (top - bottom) m[2, 2] = -1.0 / (far - near) m[0, 3] = -(right + left) / (right - left) m[1, 3] = -(top + bottom) / (top - bottom) m[2, 3] = -near / (far - near) m[3, 3] = 1.0 return m } @(private = "file") transition_atlas_empty :: proc(state: ^Shadow_State, b: ^bk.Backend, ctx: bk.Frame_Context) { b.begin_render_pass(ctx, { pass = state.render_pass, framebuffer = state.atlas_framebuffer, width = state.atlas_size, height = state.atlas_size, clear_depth = 1, }) b.end_render_pass(ctx) } @(private = "file") transition_depth_prepass_empty :: proc(state: ^Shadow_State, b: ^bk.Backend, ctx: bk.Frame_Context) { if state.depth_prepass_width == 0 || state.depth_prepass_height == 0 { return } b.begin_render_pass(ctx, { pass = state.render_pass, framebuffer = state.depth_prepass_framebuffer, width = state.depth_prepass_width, height = state.depth_prepass_height, clear_depth = 1, }) b.end_render_pass(ctx) }