Fix direct shadow shimmering when view is changed

- Use bounding sphere of frustum instead of bbox
- Snap directional light projection offset to texels
This commit is contained in:
sergeypdev 2024-03-15 04:44:23 +04:00
parent b4a2b1d728
commit a41e4b64d3
5 changed files with 54 additions and 31 deletions

View File

@ -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) { vec3 microfacetModel(Material mat, int light_idx, Light light, vec3 P, vec3 N) {
int csm_split_idx = getCSMSplit(light_idx, P.z); 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 vec3 diffuseBrdf = vec3(0); // metallic
if (!mat.metallic) { if (!mat.metallic) {

View File

@ -20,6 +20,7 @@ pub const MAX_LIGHTS = 8;
pub const MAX_DRAW_COMMANDS = 4096; pub const MAX_DRAW_COMMANDS = 4096;
pub const MAX_LIGHT_COMMANDS = 2048; pub const MAX_LIGHT_COMMANDS = 2048;
pub const CSM_SPLITS = 4; pub const CSM_SPLITS = 4;
pub const DIRECTIONAL_SHADOW_MAP_SIZE = 2048;
// affects how cascades are split // affects how cascades are split
// 0 - uniform // 0 - uniform
// 1 - exponential // 1 - exponential
@ -176,7 +177,7 @@ pub fn init(allocator: std.mem.Allocator, frame_arena: std.mem.Allocator, assetm
checkGLError(); checkGLError();
std.debug.assert(render.shadow_texture_array != 0); 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(); checkGLError();
gl.textureParameteri(render.shadow_texture_array, gl.TEXTURE_COMPARE_MODE, gl.COMPARE_REF_TO_TEXTURE); 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| { .directional => |dir_light| {
light.pos = dir_light.dir.toVec4(0); light.pos = dir_light.dir.toVec4(0);
light.color_radius = dir_light.color.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; const camera_matrix = &self.shadow_matrices;
@ -562,8 +563,8 @@ pub fn finish(self: *Render) void {
var projection: Mat4 = undefined; var projection: Mat4 = undefined;
{ {
var camera = self.camera.*;
if (self.update_view_frustum) { if (self.update_view_frustum) {
var camera = self.camera.*;
camera.near = split_near; camera.near = split_near;
camera.far = split_far; camera.far = split_far;
const inv_csm_proj = camera.projection().mul(camera.view_mat).inv(); 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(); // Find minimal bounding sphere for a frustum
var dir_aabb_max = Vec3.zero(); // Taken from:
for (self.world_view_frustum_corners[split_idx]) |corner| { // https://lxjk.github.io/2017/04/15/Calculate-Minimal-Bounding-Sphere-of-Frustum.html
const pos4 = view.mulByVec4(corner.toVec4(1)); const inv_aspect_sqr = (camera.aspect) * (camera.aspect);
const pos = pos4.toVec3(); const k = @sqrt(1 + inv_aspect_sqr) * @tan(za.toRadians(camera.fovy) / 2);
dir_aabb_min = pos.min(dir_aabb_min);
dir_aabb_max = pos.max(dir_aabb_max); 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
{ center = camera.view_mat.inv().mulByVec4(center.toVec4(1)).toVec3();
const min_z = dir_aabb_min.z(); center = view.mulByVec4(center.toVec4(1)).toVec3();
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();
// NOTE: Use bounding sphere instead of AABB to prevent split size changing with rotation // NOTE: Use bounding sphere instead of AABB to prevent split size changing with rotation
projection = math.orthographic( projection = math.orthographic(
b_sphere.origin.x() - b_sphere.radius, center.x() - radius - 0.0001,
b_sphere.origin.x() + b_sphere.radius, center.x() + radius,
b_sphere.origin.y() - b_sphere.radius, center.y() - radius,
b_sphere.origin.y() + b_sphere.radius, center.y() + radius,
b_sphere.origin.z() - b_sphere.radius, -center.z() - radius,
b_sphere.origin.z() + b_sphere.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.* = .{ camera_matrix.* = .{
.view = view, .view = view,
.projection = projection, .projection = projection,
}; };
const shadow_view_proj = projection.mul(view);
dir_view_proj_mat[split_idx] = shadow_view_proj; dir_view_proj_mat[split_idx] = shadow_view_proj;
const light_frustum = math.Frustum.new(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 // TODO: support ortho
pub const Camera = struct { pub const Camera = struct {
pos: Vec3 = Vec3.zero(),
fovy: f32 = 60, fovy: f32 = 60,
aspect: f32 = 1, aspect: f32 = 1,
near: f32 = 0.1, near: f32 = 0.1,

View File

@ -230,8 +230,7 @@ export fn game_init(global_allocator: *std.mem.Allocator) void {
.mesh = .{ .mesh = .{
.handle = a.Meshes.plane.Plane, .handle = a.Meshes.plane.Plane,
.material = .{ .material = .{
.blend_mode = .AlphaBlend, .albedo = Vec4.one(),
.albedo = Vec4.new(1, 1, 1, 0.5),
.normal_map = a.Textures.@"tile.norm", .normal_map = a.Textures.@"tile.norm",
}, },
.override_material = true, .override_material = true,

View File

@ -74,7 +74,8 @@ pub const FreeLookCamera = struct {
const movement = right.scale(move.x()).add(forward.scale(move.y())).add(up.scale(move.z())); 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)); 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()); self.camera.view_mat = Mat4.lookAt(self.pos, self.pos.add(forward), Vec3.up());
} }
}; };

View File

@ -100,10 +100,9 @@ pub const AABB = struct {
} }
pub fn toSphere(self: *const AABB) BoundingSphere { pub fn toSphere(self: *const AABB) BoundingSphere {
const max_extent = @max(@max(self.extents.x(), self.extents.y()), self.extents.z());
return BoundingSphere{ return BoundingSphere{
.origin = self.origin, .origin = self.origin,
.radius = max_extent, .radius = self.extents.length(),
}; };
} }
}; };
@ -111,6 +110,10 @@ pub const AABB = struct {
pub const BoundingSphere = struct { pub const BoundingSphere = struct {
origin: Vec3 = Vec3.zero(), origin: Vec3 = Vec3.zero(),
radius: f32 = 0, radius: f32 = 0,
pub fn new(origin: Vec3, radius: f32) BoundingSphere {
return BoundingSphere{ .origin = origin, .radius = radius };
}
}; };
pub const Frustum = struct { pub const Frustum = struct {