mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-07-11 05:04:40 -04:00
Dynamic shadows demo mod
This commit is contained in:
@@ -561,6 +561,7 @@ if (DUSK_ENABLE_CODE_MODS AND CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR
|
||||
add_custom_target(dusklight_mods) # Aggregate target for all in-tree mods
|
||||
add_subdirectory(mods/template_mod)
|
||||
add_subdirectory(mods/ao_mod)
|
||||
add_subdirectory(mods/shadow_mod)
|
||||
endif ()
|
||||
|
||||
if (APPLE)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "JSystem/J3DGraphBase/J3DShapeDraw.h"
|
||||
#include "JSystem/J3DAssert.h"
|
||||
#include "JSystem/J3DGraphBase/J3DFifo.h"
|
||||
#include "JSystem/JMath/JMath.h"
|
||||
#include <mtx.h>
|
||||
#include "dusk/endian_gx.hpp"
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
cmake_minimum_required(VERSION 3.25)
|
||||
project(shadow_mod CXX)
|
||||
|
||||
if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||
set(DUSK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../.." CACHE PATH "Path to dusk source root")
|
||||
option(DUSK_MOD_USE_FULL_TREE "Use full build instead of the minimal mod SDK" OFF)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
if (DUSK_MOD_USE_FULL_TREE)
|
||||
add_subdirectory("${DUSK_DIR}" dusk EXCLUDE_FROM_ALL)
|
||||
else ()
|
||||
add_subdirectory("${DUSK_DIR}/sdk" dusk-sdk EXCLUDE_FROM_ALL)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
add_mod(shadow_mod
|
||||
SOURCES src/mod.cpp
|
||||
MOD_JSON mod.json
|
||||
RES_DIR res
|
||||
BUNDLE
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"id": "dev.twilitrealm.shadow_mod",
|
||||
"name": "[Demo] Dynamic Shadows",
|
||||
"version": "1.0.0",
|
||||
"author": "encounter",
|
||||
"description": "Demo showcasing dynamic shadow maps: re-renders geometry from the sun (or moon) point of view and composites real-time shadows over the world, with screen-space contact shadows for fine detail."
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
// Deferred shadow composite: reconstructs the world position of every scene pixel from the
|
||||
// depth snapshot (CameraService matrices), transforms it into the light's clip space, and
|
||||
// PCF-compares against the shadow map rendered earlier this frame. Drawn as a fullscreen
|
||||
// triangle with multiply blending (srcFactor = Dst, dstFactor = Zero) before the HUD.
|
||||
//
|
||||
// Depth conventions (both reversed-Z): the scene snapshot has 1.0 at the camera near plane;
|
||||
// the shadow map, rendered through the game's GX pipeline with a GC-convention light matrix,
|
||||
// stores clip.z, i.e. 1.0 nearest to the light and 0.0 at the light frustum far plane.
|
||||
//
|
||||
// The optional contact-shadow raymarch follows Panos Karabelas' screen-space shadows
|
||||
// (https://panoskarabelas.com/blog/posts/screen_space_shadows/, MIT via Spartan Engine):
|
||||
// march from the pixel toward the light in view space and mark occlusion when the ray dips
|
||||
// behind the depth buffer within a thickness threshold.
|
||||
|
||||
struct Uniforms {
|
||||
world_from_proj: mat4x4f, // scene depth unproject (camera)
|
||||
view_from_proj: mat4x4f, // scene depth -> view space (contact shadows)
|
||||
proj_from_view: mat4x4f, // view -> clip (contact shadows re-projection)
|
||||
light_vp: mat4x4f, // world -> light receiver projection (UV/depth basis)
|
||||
light_dir_view: vec3f, // direction *toward* the light, view space, normalized
|
||||
bias: f32, // shadow-map depth bias (reversed-depth units)
|
||||
size: vec2f, // shadow map size in texels
|
||||
inv_size: vec2f,
|
||||
strength: f32, // final darkening amount, horizon fade baked in
|
||||
pcf_taps: f32, // 0 = single tap, 1 = 3x3, 2 = 5x5
|
||||
contact_enabled: f32,
|
||||
contact_thickness: f32, // view-space thickness threshold
|
||||
contact_length: f32, // view-space march distance
|
||||
debug_mode: u32, // 0 = composite; nonzero modes are diagnostic views
|
||||
_pad0: f32,
|
||||
_pad1: f32,
|
||||
}
|
||||
|
||||
@group(0) @binding(0) var scene_depth: texture_2d<f32>;
|
||||
@group(0) @binding(1) var shadow_map: texture_2d<f32>;
|
||||
@group(0) @binding(2) var<uniform> uniforms: Uniforms;
|
||||
@group(0) @binding(3) var light_color: texture_2d<f32>;
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) position: vec4f,
|
||||
@location(0) uv: vec2f,
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_main(@builtin(vertex_index) index: u32) -> VertexOutput {
|
||||
var out: VertexOutput;
|
||||
let uv = vec2f(f32((index << 1u) & 2u), f32(index & 2u));
|
||||
out.position = vec4f(uv * vec2f(2.0, -2.0) + vec2f(-1.0, 1.0), 0.0, 1.0);
|
||||
out.uv = uv;
|
||||
return out;
|
||||
}
|
||||
|
||||
fn load_shadow(texel: vec2<i32>) -> f32 {
|
||||
let clamped = clamp(texel, vec2<i32>(0i), vec2<i32>(uniforms.size) - 1i);
|
||||
return textureLoad(shadow_map, clamped, 0i).r;
|
||||
}
|
||||
|
||||
// Returns 1.0 when the pixel at light-space depth `receiver` is shadowed by the map texel.
|
||||
fn shadow_test(texel: vec2<i32>, receiver: f32) -> f32 {
|
||||
// Reversed depth: a larger stored value is closer to the light, i.e. an occluder.
|
||||
return select(0.0, 1.0, load_shadow(texel) > receiver + uniforms.bias);
|
||||
}
|
||||
|
||||
// Bilinearly weighted comparison (what a hardware comparison sampler would do): filter the
|
||||
// four *comparison results*, never the depths themselves. This is what turns per-texel
|
||||
// staircases into smooth penumbra edges.
|
||||
fn shadow_compare_bilinear(light_uv: vec2f, receiver: f32) -> f32 {
|
||||
let coordinates = light_uv * uniforms.size - 0.5;
|
||||
let base = floor(coordinates);
|
||||
let fraction = coordinates - base;
|
||||
let texel = vec2<i32>(base);
|
||||
let s00 = shadow_test(texel, receiver);
|
||||
let s10 = shadow_test(texel + vec2<i32>(1i, 0i), receiver);
|
||||
let s01 = shadow_test(texel + vec2<i32>(0i, 1i), receiver);
|
||||
let s11 = shadow_test(texel + vec2<i32>(1i, 1i), receiver);
|
||||
let top = mix(s00, s10, fraction.x);
|
||||
let bottom = mix(s01, s11, fraction.x);
|
||||
return mix(top, bottom, fraction.y);
|
||||
}
|
||||
|
||||
fn sample_shadow_pcf(light_uv: vec2f, receiver: f32) -> f32 {
|
||||
let radius = i32(uniforms.pcf_taps);
|
||||
var sum = 0.0;
|
||||
var count = 0.0;
|
||||
for (var y = -radius; y <= radius; y += 1i) {
|
||||
for (var x = -radius; x <= radius; x += 1i) {
|
||||
let offset = vec2f(f32(x), f32(y)) * uniforms.inv_size;
|
||||
sum += shadow_compare_bilinear(light_uv + offset, receiver);
|
||||
count += 1.0;
|
||||
}
|
||||
}
|
||||
return sum / count;
|
||||
}
|
||||
|
||||
fn scene_depth_at(uv: vec2f) -> f32 {
|
||||
let size = vec2<i32>(textureDimensions(scene_depth));
|
||||
let texel = clamp(vec2<i32>(uv * vec2f(size)), vec2<i32>(0i), size - 1i);
|
||||
return textureLoad(scene_depth, texel, 0i).r;
|
||||
}
|
||||
|
||||
fn light_color_at(uv: vec2f) -> vec4f {
|
||||
let size = vec2<i32>(textureDimensions(light_color));
|
||||
let texel = clamp(vec2<i32>(uv * vec2f(size)), vec2<i32>(0i), size - 1i);
|
||||
return textureLoad(light_color, texel, 0i);
|
||||
}
|
||||
|
||||
fn light_depth_debug_at(uv: vec2f) -> vec3f {
|
||||
let texel = vec2<i32>(uv * uniforms.size);
|
||||
let depth = load_shadow(texel);
|
||||
if depth <= 0.0 {
|
||||
return vec3f(0.0);
|
||||
}
|
||||
|
||||
let dx = abs(depth - load_shadow(texel + vec2<i32>(1i, 0i)));
|
||||
let dy = abs(depth - load_shadow(texel + vec2<i32>(0i, 1i)));
|
||||
let edge = saturate((dx + dy) * 500.0);
|
||||
let shade = saturate(depth * 1.5);
|
||||
let bands = 0.08 * (0.5 + 0.5 * cos(depth * 96.0));
|
||||
return vec3f(saturate(shade + bands + edge));
|
||||
}
|
||||
|
||||
fn view_position(uv: vec2f, depth: f32) -> vec3f {
|
||||
let ndc = vec4f(uv.x * 2.0 - 1.0, 1.0 - 2.0 * uv.y, depth, 1.0);
|
||||
let position = uniforms.view_from_proj * ndc;
|
||||
return position.xyz / position.w;
|
||||
}
|
||||
|
||||
// Interleaved gradient noise (Jimenez); fixed per-pixel dither, no temporal rotation.
|
||||
fn ign(pixel: vec2f) -> f32 {
|
||||
return fract(52.9829189 * fract(dot(pixel, vec2f(0.06711056, 0.00583715))));
|
||||
}
|
||||
|
||||
// Screen-space contact shadows: march toward the light in view space; occluded when the ray
|
||||
// passes behind the depth buffer by less than the thickness threshold. Faded out with view
|
||||
// distance: position reconstruction error grows with distance while the thickness threshold
|
||||
// is fixed, so far surfaces (and anything translucent composited over them - clouds, fog)
|
||||
// would otherwise pick up dithered false occlusion. Contact shadows are a near-field effect.
|
||||
fn contact_shadow_fade(view_distance: f32) -> f32 {
|
||||
return saturate(1.0 - (view_distance - 3000.0) / 5000.0);
|
||||
}
|
||||
|
||||
fn contact_shadow(origin: vec3f, pixel: vec2f) -> f32 {
|
||||
let steps = 24;
|
||||
let step_vec = uniforms.light_dir_view * (uniforms.contact_length / f32(steps));
|
||||
var ray = origin + step_vec * ign(pixel);
|
||||
for (var i = 0; i < steps; i += 1) {
|
||||
ray += step_vec;
|
||||
// Project the ray position back to screen space.
|
||||
let clip = uniforms.proj_from_view * vec4f(ray, 1.0);
|
||||
if clip.w <= 0.0 {
|
||||
break;
|
||||
}
|
||||
let ray_ndc = clip.xyz / clip.w;
|
||||
let ray_uv = vec2f(0.5 + 0.5 * ray_ndc.x, 0.5 - 0.5 * ray_ndc.y);
|
||||
if any(ray_uv < vec2f(0.0)) || any(ray_uv > vec2f(1.0)) {
|
||||
break;
|
||||
}
|
||||
let scene = scene_depth_at(ray_uv);
|
||||
if scene <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
// Compare in view space: positive delta = the ray is behind the scene surface.
|
||||
let scene_z = view_position(ray_uv, scene).z;
|
||||
let delta = scene_z - ray.z; // view space looks down -z; larger z = closer
|
||||
if delta > 0.0 && delta < uniforms.contact_thickness {
|
||||
return 1.0;
|
||||
}
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4f {
|
||||
let depth = scene_depth_at(in.uv);
|
||||
if uniforms.debug_mode == 1u {
|
||||
let value = load_shadow(vec2<i32>(in.uv * uniforms.size));
|
||||
return vec4f(value, value, value, 1.0);
|
||||
}
|
||||
if uniforms.debug_mode == 9u || uniforms.debug_mode == 10u {
|
||||
let color = light_color_at(in.uv);
|
||||
let color_luma = max(color.r, max(color.g, color.b));
|
||||
let depth_color = light_depth_debug_at(in.uv);
|
||||
let rgb = select(depth_color, color.rgb, color_luma > (1.0 / 255.0));
|
||||
return vec4f(rgb, 1.0);
|
||||
}
|
||||
|
||||
if depth <= 0.0 {
|
||||
// Sky / cleared pixels receive no shadow.
|
||||
if uniforms.debug_mode >= 3u {
|
||||
return vec4f(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
return vec4f(1.0);
|
||||
}
|
||||
|
||||
let ndc = vec4f(in.uv.x * 2.0 - 1.0, 1.0 - 2.0 * in.uv.y, depth, 1.0);
|
||||
let world4 = uniforms.world_from_proj * ndc;
|
||||
let world = world4.xyz / world4.w;
|
||||
|
||||
let light_clip = uniforms.light_vp * vec4f(world, 1.0);
|
||||
let light_ndc = light_clip.xyz / light_clip.w;
|
||||
let receiver = light_ndc.z; // reversed light depth, 1 = nearest to the light
|
||||
let light_uv = vec2f(0.5 + 0.5 * light_ndc.x, 0.5 - 0.5 * light_ndc.y);
|
||||
let in_shadow_bounds = all(light_uv >= vec2f(0.0)) && all(light_uv <= vec2f(1.0)) &&
|
||||
receiver > 0.0 && receiver <= 1.0;
|
||||
let shadow_depth = load_shadow(vec2<i32>(light_uv * uniforms.size));
|
||||
|
||||
if uniforms.debug_mode == 4u {
|
||||
let valid = select(0.0, 1.0, in_shadow_bounds);
|
||||
return vec4f(saturate(light_uv.x), saturate(light_uv.y), valid, 1.0);
|
||||
}
|
||||
|
||||
if uniforms.debug_mode == 5u {
|
||||
if !in_shadow_bounds {
|
||||
return vec4f(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
let current_compare = select(0.0, 1.0, shadow_depth > receiver + uniforms.bias);
|
||||
let opposite_compare = select(0.0, 1.0, shadow_depth < receiver - uniforms.bias);
|
||||
return vec4f(current_compare, 0.0, opposite_compare, 1.0);
|
||||
}
|
||||
|
||||
if uniforms.debug_mode == 6u {
|
||||
let valid = select(0.0, 1.0, in_shadow_bounds);
|
||||
return vec4f(saturate(receiver), shadow_depth, valid, 1.0);
|
||||
}
|
||||
|
||||
if uniforms.debug_mode == 7u {
|
||||
let beyond_far = select(0.0, 1.0, receiver <= 0.0);
|
||||
let valid_depth = select(0.0, 1.0, receiver > 0.0 && receiver <= 1.0);
|
||||
let before_near = select(0.0, 1.0, receiver > 1.0);
|
||||
return vec4f(beyond_far, valid_depth, before_near, 1.0);
|
||||
}
|
||||
|
||||
if uniforms.debug_mode == 8u {
|
||||
let valid_x = select(0.0, 1.0, light_uv.x >= 0.0 && light_uv.x <= 1.0);
|
||||
let valid_y = select(0.0, 1.0, light_uv.y >= 0.0 && light_uv.y <= 1.0);
|
||||
let valid_depth = select(0.0, 1.0, receiver > 0.0 && receiver <= 1.0);
|
||||
return vec4f(valid_x, valid_y, valid_depth, 1.0);
|
||||
}
|
||||
|
||||
var occlusion = 0.0;
|
||||
if in_shadow_bounds {
|
||||
occlusion = sample_shadow_pcf(light_uv, receiver);
|
||||
}
|
||||
|
||||
if uniforms.debug_mode == 3u {
|
||||
return vec4f(occlusion, occlusion, occlusion, 1.0);
|
||||
}
|
||||
|
||||
if uniforms.contact_enabled != 0.0 && occlusion < 1.0 {
|
||||
let origin = view_position(in.uv, depth);
|
||||
let fade = contact_shadow_fade(-origin.z);
|
||||
if fade > 0.0 {
|
||||
occlusion = max(occlusion, fade * contact_shadow(origin, in.position.xy));
|
||||
}
|
||||
}
|
||||
|
||||
let value = 1.0 - uniforms.strength * occlusion;
|
||||
if uniforms.debug_mode == 2u {
|
||||
return vec4f(value, value, value, 1.0);
|
||||
}
|
||||
return vec4f(value, value, value, 1.0);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user