#pragma once
#include "camera.h"
#include "core/math_rtm.h"
#include <float.h>
struct ShadowCascade {
mat4 view;
mat4 view_proj;
// area that can receive shadows.
vec3 receiver_min_ls;
vec3 receiver_max_ls;
// area allowed to contribute shadow casters.
vec3 caster_min_ls;
vec3 caster_max_ls;
f32 width; // right - left in light space
f32 height; // top - bottom in light space
f32 depth; // far - near in light space
f32 split; // far-plane of each cascade in view space
};
struct PointLight {
NodeHandle node = NODE_INVALID;
vec3 position = {0.0f, 0.0f, 0.0f};
vec3 color = {1.0f, 1.0f, 1.0f};
f32 intensity = 1.0f;
f32 range = 10.0f;
};
// TODO, rename to Directional Light
struct Light {
NodeHandle node = NODE_INVALID;
static constexpr u32 CASCADE_COUNT = 4;
ShadowCascade cascades[CASCADE_COUNT] = {};
vec3 direction = {0.2f, 0.2f, 1.0f};
f32 distance = 50.0f;
vec3 color = {1.0f, 0.9f, 0.7f};
f32 intensity = 15.0f;
f32 max_shadow_distance = 400.0f;
mat4 view_proj = identity();
};
// NOTE: deprecated, old non csm path
static void update_light(Light &light, Camera &camera) {
vec3 L = light.direction.normalized(); // world space direction from surface to light
vec3 center = camera.position;
vec3 eye = center + L * 50.0f;
vec3 up = fabs(L.dot(vec3(0, 0, 1))) > 0.99f ? vec3(0, 1, 0) : vec3(0, 0, 1);
mat4 view = look_at(eye, center, up);
mat4 proj = ortho(60.0f, 60.0f, 0.1f, 200.0f);
light.view_proj = view * proj;
}
static vec3 choose_light_up(vec3 light_dir) {
return fabsf(light_dir.dot(vec3(0, 0, 1))) > 0.99f ? vec3(0, 1, 0) : vec3(0, 0, 1);
}
// uses the common logarithmic/uniform blend (PSSM) so that more shadow
// resolution is allocated near the camera while still covering distant areas.
static void compute_cascade_splits(const Camera &camera, f32 max_shadow_distance, f32 *splits) {
splits[0] = camera.near_plane;
// lambda: blend factor between logarithmic and uniform partitioning.
constexpr f32 blend_factor = 0.9f;
for (u32 i = 1; i <= Light::CASCADE_COUNT; ++i) {
const f32 t = (f32)i / (f32)Light::CASCADE_COUNT;
const f32 log_split = camera.near_plane * powf(max_shadow_distance / camera.near_plane, t);
const f32 uniform_split = camera.near_plane + (max_shadow_distance - camera.near_plane) * t;
splits[i] = blend_factor * log_split + (1.0f - blend_factor) * uniform_split;
}
}
static mat4 make_light_grid_view(vec3 light_dir, vec3 up, f32 distance) {
return look_at(light_dir * distance, vec3(0.0f), up);
}
static void build_cascade_corners_vs(const Camera &camera, f32 near_d, f32 far_d, vec3 *corners) {
const f32 tan_half_fov = tanf(radians(camera.fov_y_deg) * 0.5f);
const f32 near_h = tan_half_fov * near_d;
const f32 near_w = near_h * camera.aspect;
const f32 far_h = tan_half_fov * far_d;
const f32 far_w = far_h * camera.aspect;
corners[0] = vec3(-near_w, -near_h, near_d);
corners[1] = vec3(near_w, -near_h, near_d);
corners[2] = vec3(-near_w, near_h, near_d);
corners[3] = vec3(near_w, near_h, near_d);
corners[4] = vec3(-far_w, -far_h, far_d);
corners[5] = vec3(far_w, -far_h, far_d);
corners[6] = vec3(-far_w, far_h, far_d);
corners[7] = vec3(far_w, far_h, far_d);
}
static void rotate_corners_to_render_relative(const Camera &camera, vec3 *corners, u32 count) {
for (u32 i = 0; i < count; ++i) {
corners[i] = transform_direction(corners[i], camera.inv_view);
}
}
static void compute_bounding_sphere(const vec3 *corners, u32 count, vec3 ¢er, f32 &radius) {
center = vec3(0.0f);
for (u32 i = 0; i < count; ++i) {
center += corners[i];
}
center /= (f32)count;
radius = 0.0f;
for (u32 i = 0; i < count; ++i) {
radius = fmaxf(radius, (corners[i] - center).length());
}
radius = ceilf(radius * 16.0f) / 16.0f;
}
static vec3 snap_center_to_light_texel_grid(
vec3 center_rel, vec3 render_origin_ws, mat4 light_grid_view, f32 radius, u32 shadow_width
) {
const f32 texel = (radius * 2.0f) / (f32)shadow_width;
// The cascade matrices consume render-relative positions, but the snap grid
// must remain world-anchored. Snap the absolute cascade center in light space,
// then subtract the render origin's light-space offset so the final projection
// still matches render-relative shader inputs.
const vec3 center_ws = render_origin_ws + center_rel;
vec4 center_ls_world = transform_point(center_ws, light_grid_view);
center_ls_world.x = floorf(center_ls_world.x / texel) * texel;
center_ls_world.y = floorf(center_ls_world.y / texel) * texel;
const vec3 origin_offset_ls = transform_direction(render_origin_ws, light_grid_view);
return vec3(
center_ls_world.x - origin_offset_ls.x,
center_ls_world.y - origin_offset_ls.y,
center_ls_world.z - origin_offset_ls.z
);
}
static void
compute_light_space_depth_bounds(const vec3 *corners_rel, u32 count, mat4 light_grid_view, f32 &min_z, f32 &max_z) {
min_z = FLT_MAX;
max_z = -FLT_MAX;
for (u32 i = 0; i < count; ++i) {
vec4 p = transform_point(corners_rel[i], light_grid_view);
min_z = fminf(min_z, p.z);
max_z = fmaxf(max_z, p.z);
}
const f32 z_padding = fmaxf((max_z - min_z) * 0.05f, 1.0f);
min_z -= z_padding;
max_z += z_padding;
}
static void store_shadow_cascade(
ShadowCascade &cascade,
mat4 light_grid_view,
mat4 light_proj,
f32 left,
f32 right,
f32 bottom,
f32 top,
f32 min_z,
f32 max_z,
f32 split
) {
cascade.view = light_grid_view;
cascade.view_proj = light_grid_view * light_proj;
cascade.width = right - left;
cascade.height = top - bottom;
cascade.depth = max_z - min_z;
cascade.split = split;
cascade.receiver_min_ls = vec3(left, bottom, min_z);
cascade.receiver_max_ls = vec3(right, top, max_z);
cascade.caster_min_ls = vec3(left, bottom, min_z);
cascade.caster_max_ls = vec3(right, top, max_z);
}
static void update_light_cascades(Light &light, Camera &camera, const u32 shadow_sizes[Light::CASCADE_COUNT]) {
const vec3 light_dir = light.direction.normalized();
const vec3 up = choose_light_up(light_dir);
const mat4 light_grid_view = make_light_grid_view(light_dir, up, light.distance);
f32 splits[Light::CASCADE_COUNT + 1];
compute_cascade_splits(camera, light.max_shadow_distance, splits);
for (u32 cascade_idx = 0; cascade_idx < Light::CASCADE_COUNT; ++cascade_idx) {
const f32 near_d = splits[cascade_idx];
const f32 far_d = splits[cascade_idx + 1];
vec3 corners_rel[8];
build_cascade_corners_vs(camera, near_d, far_d, corners_rel);
rotate_corners_to_render_relative(camera, corners_rel, 8);
vec3 center_rel;
f32 radius;
compute_bounding_sphere(corners_rel, 8, center_rel, radius);
const vec3 center_ls = snap_center_to_light_texel_grid(
center_rel, camera.position, light_grid_view, radius, shadow_sizes[cascade_idx]
);
const f32 left = center_ls.x - radius;
const f32 right = center_ls.x + radius;
const f32 bottom = center_ls.y - radius;
const f32 top = center_ls.y + radius;
f32 min_z;
f32 max_z;
compute_light_space_depth_bounds(corners_rel, 8, light_grid_view, min_z, max_z);
const mat4 light_proj = ortho_offcenter(left, right, bottom, top, min_z, max_z);
store_shadow_cascade(
light.cascades[cascade_idx], light_grid_view, light_proj, left, right, bottom, top, min_z, max_z, far_d
);
}
}
static void resolve_cascade_sizes(u32 largest, u32 out[Light::CASCADE_COUNT]) {
for (u32 i = 0; i < Light::CASCADE_COUNT; ++i) {
out[i] = largest;
}
}