light.h

cross platform rendering playground

src/scene/light.h

7.77 KB
#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 &center, 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;
    }
}