From a41e4b64d384243ebbdb1154d512a05f6293157d Mon Sep 17 00:00:00 2001 From: sergeypdev Date: Fri, 15 Mar 2024 04:44:23 +0400 Subject: [PATCH] Fix direct shadow shimmering when view is changed - Use bounding sphere of frustum instead of bbox - Snap directional light projection offset to texels --- assets/shaders/mesh.glsl | 3 +- src/Render.zig | 69 +++++++++++++++++++++++++--------------- src/game.zig | 3 +- src/globals.zig | 3 +- src/math.zig | 7 ++-- 5 files changed, 54 insertions(+), 31 deletions(-) diff --git a/assets/shaders/mesh.glsl b/assets/shaders/mesh.glsl index a352133..b322bc5 100644 --- a/assets/shaders/mesh.glsl +++ b/assets/shaders/mesh.glsl @@ -193,7 +193,8 @@ float map(float value, float min1, float max1, float min2, float max2) { vec3 microfacetModel(Material mat, int light_idx, Light light, vec3 P, vec3 N) { int csm_split_idx = getCSMSplit(light_idx, P.z); - mat.albedo = vec4(mix(mat.albedo.rgb, csm_split_colors[csm_split_idx], 0.8), mat.albedo.a); + // Visualize CSM splits + //mat.albedo = vec4(mix(mat.albedo.rgb, csm_split_colors[csm_split_idx], 0.8), mat.albedo.a); vec3 diffuseBrdf = vec3(0); // metallic if (!mat.metallic) { diff --git a/src/Render.zig b/src/Render.zig index 5338d11..4a7efe9 100644 --- a/src/Render.zig +++ b/src/Render.zig @@ -20,6 +20,7 @@ pub const MAX_LIGHTS = 8; pub const MAX_DRAW_COMMANDS = 4096; pub const MAX_LIGHT_COMMANDS = 2048; pub const CSM_SPLITS = 4; +pub const DIRECTIONAL_SHADOW_MAP_SIZE = 2048; // affects how cascades are split // 0 - uniform // 1 - exponential @@ -176,7 +177,7 @@ pub fn init(allocator: std.mem.Allocator, frame_arena: std.mem.Allocator, assetm checkGLError(); std.debug.assert(render.shadow_texture_array != 0); - gl.textureStorage3D(render.shadow_texture_array, 1, gl.DEPTH_COMPONENT16, 2048, 2048, CSM_SPLITS); + gl.textureStorage3D(render.shadow_texture_array, 1, gl.DEPTH_COMPONENT16, DIRECTIONAL_SHADOW_MAP_SIZE, DIRECTIONAL_SHADOW_MAP_SIZE, CSM_SPLITS); checkGLError(); gl.textureParameteri(render.shadow_texture_array, gl.TEXTURE_COMPARE_MODE, gl.COMPARE_REF_TO_TEXTURE); @@ -521,7 +522,7 @@ pub fn finish(self: *Render) void { .directional => |dir_light| { light.pos = dir_light.dir.toVec4(0); light.color_radius = dir_light.color.toVec4(0); - gl.viewport(0, 0, 2048, 2048); + gl.viewport(0, 0, DIRECTIONAL_SHADOW_MAP_SIZE, DIRECTIONAL_SHADOW_MAP_SIZE); const camera_matrix = &self.shadow_matrices; @@ -562,8 +563,8 @@ pub fn finish(self: *Render) void { var projection: Mat4 = undefined; { + var camera = self.camera.*; if (self.update_view_frustum) { - var camera = self.camera.*; camera.near = split_near; camera.far = split_far; const inv_csm_proj = camera.projection().mul(camera.view_mat).inv(); @@ -574,39 +575,55 @@ pub fn finish(self: *Render) void { } } - var dir_aabb_min = Vec3.zero(); - var dir_aabb_max = Vec3.zero(); - for (self.world_view_frustum_corners[split_idx]) |corner| { - const pos4 = view.mulByVec4(corner.toVec4(1)); - const pos = pos4.toVec3(); - dir_aabb_min = pos.min(dir_aabb_min); - dir_aabb_max = pos.max(dir_aabb_max); + // Find minimal bounding sphere for a frustum + // Taken from: + // https://lxjk.github.io/2017/04/15/Calculate-Minimal-Bounding-Sphere-of-Frustum.html + const inv_aspect_sqr = (camera.aspect) * (camera.aspect); + const k = @sqrt(1 + inv_aspect_sqr) * @tan(za.toRadians(camera.fovy) / 2); + + var center = Vec3.zero(); + var radius: f32 = 0; + if (k * k >= (camera.far - camera.near) / (camera.far + camera.near)) { + center = Vec3.new(0, 0, -camera.far); + radius = camera.far * k; + } else { + center = Vec3.new(0, 0, -0.5 * (camera.far + camera.near) * (1 + k * k)); + radius = 0.5 * @sqrt((camera.far - camera.near) * (camera.far - camera.near) + 2 * (camera.far * camera.far + camera.near * camera.near) * k * k + (camera.far + camera.near) * (camera.far + camera.near) * k * k * k * k); } - // Flip z because it's negative in view space, but near, far is positive - { - const min_z = dir_aabb_min.z(); - dir_aabb_min.zMut().* = -dir_aabb_max.z(); - dir_aabb_max.zMut().* = -min_z; - } - const b_sphere = math.AABB.fromMinMax(dir_aabb_min, dir_aabb_max).toSphere(); + + center = camera.view_mat.inv().mulByVec4(center.toVec4(1)).toVec3(); + center = view.mulByVec4(center.toVec4(1)).toVec3(); // NOTE: Use bounding sphere instead of AABB to prevent split size changing with rotation projection = math.orthographic( - b_sphere.origin.x() - b_sphere.radius, - b_sphere.origin.x() + b_sphere.radius, - b_sphere.origin.y() - b_sphere.radius, - b_sphere.origin.y() + b_sphere.radius, - b_sphere.origin.z() - b_sphere.radius, - b_sphere.origin.z() + b_sphere.radius, + center.x() - radius - 0.0001, + center.x() + radius, + center.y() - radius, + center.y() + radius, + -center.z() - radius, + -center.z() + radius, ); } + var shadow_view_proj = projection.mul(view); + + // Snap to texels + { + var shadow_origin = shadow_view_proj.mulByVec4(Vec4.new(0, 0, 0, 1)); + shadow_origin = shadow_origin.scale(1.0 / shadow_origin.w()); + shadow_origin = shadow_origin.scale(DIRECTIONAL_SHADOW_MAP_SIZE / 2); + var rounded_origin: Vec4 = undefined; + rounded_origin.data = @round(shadow_origin.data); + var offset = rounded_origin.sub(shadow_origin).toVec2().toVec3(0); + offset = offset.scale(2.0 / @as(f32, DIRECTIONAL_SHADOW_MAP_SIZE)); + projection = projection.translate(offset); + shadow_view_proj = projection.mul(view); + } + camera_matrix.* = .{ .view = view, .projection = projection, }; - - const shadow_view_proj = projection.mul(view); dir_view_proj_mat[split_idx] = shadow_view_proj; const light_frustum = math.Frustum.new(shadow_view_proj); @@ -1116,6 +1133,8 @@ pub const Uniform = enum(gl.GLint) { // TODO: support ortho pub const Camera = struct { + pos: Vec3 = Vec3.zero(), + fovy: f32 = 60, aspect: f32 = 1, near: f32 = 0.1, diff --git a/src/game.zig b/src/game.zig index c160bb5..20c4dc4 100644 --- a/src/game.zig +++ b/src/game.zig @@ -230,8 +230,7 @@ export fn game_init(global_allocator: *std.mem.Allocator) void { .mesh = .{ .handle = a.Meshes.plane.Plane, .material = .{ - .blend_mode = .AlphaBlend, - .albedo = Vec4.new(1, 1, 1, 0.5), + .albedo = Vec4.one(), .normal_map = a.Textures.@"tile.norm", }, .override_material = true, diff --git a/src/globals.zig b/src/globals.zig index 333bc81..9039e60 100644 --- a/src/globals.zig +++ b/src/globals.zig @@ -74,7 +74,8 @@ pub const FreeLookCamera = struct { const movement = right.scale(move.x()).add(forward.scale(move.y())).add(up.scale(move.z())); self.pos = self.pos.add(movement.scale(self.move_speed * dt)); - + // TODO: refactor + self.camera.pos = self.pos; self.camera.view_mat = Mat4.lookAt(self.pos, self.pos.add(forward), Vec3.up()); } }; diff --git a/src/math.zig b/src/math.zig index d7c9c9a..4d93c02 100644 --- a/src/math.zig +++ b/src/math.zig @@ -100,10 +100,9 @@ pub const AABB = struct { } pub fn toSphere(self: *const AABB) BoundingSphere { - const max_extent = @max(@max(self.extents.x(), self.extents.y()), self.extents.z()); return BoundingSphere{ .origin = self.origin, - .radius = max_extent, + .radius = self.extents.length(), }; } }; @@ -111,6 +110,10 @@ pub const AABB = struct { pub const BoundingSphere = struct { origin: Vec3 = Vec3.zero(), radius: f32 = 0, + + pub fn new(origin: Vec3, radius: f32) BoundingSphere { + return BoundingSphere{ .origin = origin, .radius = radius }; + } }; pub const Frustum = struct {