shadow_sample.slang

cross platform rendering playground

src/shaders/lib/shadow_sample.slang

5.35 KB
#pragma once

#include "shared.h"

static const f32 NORMAL_BIAS = 1.0f; // world normal offset, texels (sun-facing)
static const f32 NORMAL_BIAS_SLOPE = 0.25f;
static const f32 PCF_RADIUS = 3.0f;
static const int PCF_TAP_COUNT = 16;

// poisson disk samples in [-1, 1] range, gpu gems
static const vec2 POISSON_64[64] = {
    vec2(0.1303, -0.4774),  vec2(-0.0790, 0.1597),  vec2(-0.2146, -0.7675), vec2(0.5641, 0.2770),
    vec2(-0.8458, 0.0653),  vec2(-0.2290, 0.5203),  vec2(0.7014, -0.2814),  vec2(-0.5949, -0.2615),
    vec2(0.0567, 0.8423),   vec2(-0.4671, -0.5259), vec2(0.9188, 0.1313),   vec2(0.3152, -0.1929),
    vec2(-0.6835, 0.3073),  vec2(0.3916, -0.6913),  vec2(-0.2006, -0.1625), vec2(0.8501, -0.4715),
    vec2(-0.3950, 0.7455),  vec2(0.1216, -0.0897),  vec2(0.5703, 0.5782),   vec2(-0.9340, -0.2388),
    vec2(-0.0417, -0.4026), vec2(0.4583, -0.4611),  vec2(0.1635, 0.4569),   vec2(-0.5940, 0.5467),
    vec2(-0.5231, -0.7638), vec2(-0.1682, 0.9132),  vec2(0.7403, 0.4275),   vec2(0.2742, -0.8772),
    vec2(0.3046, 0.7118),   vec2(-0.3307, -0.3606), vec2(0.0315, -0.6573),  vec2(0.6764, -0.6494),
    vec2(-0.0946, 0.3509),  vec2(-0.7726, -0.4933), vec2(-0.7603, 0.6081),  vec2(-0.4377, 0.0655),
    vec2(0.0247, -0.2155),  vec2(0.8682, -0.2073),  vec2(0.4264, 0.1474),   vec2(-0.2869, -0.0960),
    vec2(-0.6156, -0.0669), vec2(-0.1416, -0.8762), vec2(0.4938, 0.8424),   vec2(-0.8577, -0.3580),
    vec2(0.9839, 0.0778),   vec2(0.0538, 0.6073),   vec2(0.5629, -0.1489),  vec2(0.1398, -0.5184),
    vec2(-0.3444, 0.2341),  vec2(-0.4997, 0.4327),  vec2(-0.0025, 0.9896),  vec2(0.7817, -0.0236),
    vec2(-0.2096, 0.6826),  vec2(0.2251, -0.3476),  vec2(0.6264, 0.7023),   vec2(0.0367, -0.0060),
    vec2(-0.9582, 0.1479),  vec2(-0.6141, 0.7459),  vec2(0.3188, 0.4669),   vec2(0.4180, -0.3110),
    vec2(-0.4167, -0.6154), vec2(-0.0996, -0.0930), vec2(0.8276, 0.2998),   vec2(-0.7435, -0.6373),
};

f32 hash2(vec2 p) {
    return frac(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}

u32 select_cascade_aabb(vec3 worldPos, vec3 camera_pos, ShadowCascadesGPU all_cascades) {
    vec3 relPos = worldPos - camera_pos;
    u32 cascade_idx = all_cascades.cascade_count - 1;
    for (u32 i = 0; i < all_cascades.cascade_count; ++i) {
        vec3 ls_pos = (vec4(relPos, 1.0) * all_cascades.cascade[i].view).xyz;
        if (all(ls_pos >= all_cascades.cascade[i].caster_min_ls) &&
            all(ls_pos <= all_cascades.cascade[i].caster_max_ls)) {
            cascade_idx = i;
            break;
        }
    }
    return cascade_idx;
}

f32 sample_cascade_at(
    vec3 worldPos,
    vec3 normal,
    f32 NdotL,
    vec3 camera_pos,
    vec2 screen_pos,
    ShadowCascadeGPU cascade,
    SamplerComparisonState cmp_samp
) {
    vec3 relPos = worldPos - camera_pos;

    f32 NdotL_clamped = saturate(NdotL);
    f32 grazing = sqrt(max(0.0f, 1.0f - NdotL_clamped * NdotL_clamped));
    f32 normal_bias = NORMAL_BIAS + grazing * NORMAL_BIAS_SLOPE;

    vec3 shadow_pos = relPos + normalize(normal) * (cascade.texel_size * normal_bias);
    vec4 shadow_clip = vec4(shadow_pos, 1.0f) * cascade.view_proj;
    vec2 shadow_uv = vec2(shadow_clip.x, -shadow_clip.y) * 0.5f + 0.5f;
    f32 offset_depth = shadow_clip.z;

    if (shadow_uv.x < 0.0f || shadow_uv.x > 1.0f || shadow_uv.y < 0.0f || shadow_uv.y > 1.0f || offset_depth < 0.0f ||
        offset_depth > 1.0f) {
        return 1.0f;
    }

    u32 width;
    u32 height;
    Texture2D shadow_map = cascade.img;
    shadow_map.GetDimensions(width, height);
    vec2 texel_uv = 1.0f / vec2(width, height);

    f32 angle = hash2(screen_pos) * 6.2832f;
    f32 cos_a = cos(angle);
    f32 sin_a = sin(angle);

    f32 shadow = 0.0f;
    [unroll]
    for (int i = 0; i < PCF_TAP_COUNT; ++i) {
        vec2 rotated;
        rotated.x = POISSON_64[i].x * cos_a - POISSON_64[i].y * sin_a;
        rotated.y = POISSON_64[i].x * sin_a + POISSON_64[i].y * cos_a;
        vec2 offset = rotated * texel_uv * PCF_RADIUS;
        shadow += shadow_map.SampleCmpLevelZero(cmp_samp, shadow_uv + offset, offset_depth);
    }
    return shadow / (f32)PCF_TAP_COUNT;
}

f32 sample_shadow(
    vec3 world_pos,
    vec3 normal,
    vec2 screen_pos,
    f32 NdotL,
    vec3 camera_pos,
    ShadowCascadesGPU cascades,
    SamplerComparisonState cmp_samp
) {
    u32 cascade_idx = select_cascade_aabb(world_pos, camera_pos, cascades);
    ShadowCascadeGPU selected_cascade = cascades.cascade[cascade_idx];
    f32 shadow = sample_cascade_at(world_pos, normal, NdotL, camera_pos, screen_pos, selected_cascade, cmp_samp);

    // TODO: should do a cheaper path for this rather than just the same sampling
    // blend with the next cascade near the current cascade bounds
    // u32 next_idx = min(cascade_idx + 1, cascades.cascade_count - 1);
    // if (next_idx != cascade_idx) {
    //     vec3 relPos = worldPos - camera_pos;
    //     vec3 ls_pos = (vec4(relPos, 1.0) * selected_cascade.view).xyz;
    //     vec3 edge_dist = min(ls_pos - selected_cascade.caster_min_ls, selected_cascade.caster_max_ls - ls_pos);
    //     f32 min_edge = min(edge_dist.x, min(edge_dist.y, edge_dist.z));
    //     f32 blend = 1.0f - saturate(min_edge / (selected_cascade.texel_size * 4.0f));
    //
    //     if (blend > 0.0f) {
    //         f32 shadow_next = sample_cascade_at(worldPos, normal, NdotL, worldPos, screen_pos, selected_cascade);
    //         shadow = lerp(shadow, shadow_next, blend);
    //     }
    // }

    return shadow;
}