Files
dusklight/mods/shadow_mod/src/mod.cpp
T
2026-07-15 12:04:26 -06:00

1110 lines
40 KiB
C++

// Dynamic shadows example mod.
//
// Replays the game's populated opaque scene draw lists into an offscreen pass with a light-space
// projection to produce a shadow map of the live scene, then composites deferred shadows over the
// world (scene depth + CameraService unproject + PCF against the map).
//
// The optional contact-shadow raymarch in the composite is reimplemented from Panos Karabelas'
// screen-space shadows (MIT, via Spartan Engine); see res/shadow.wgsl.
#include "global.h"
#include "JSystem/J3DGraphBase/J3DShape.h"
#include "JSystem/J3DU/J3DUClipper.h"
#include "JSystem/JMath/JMath.h"
#include "d/d_com_inf_game.h"
#include "d/d_kankyo.h"
#include "d/d_kankyo_rain.h"
#include "dolphin/gx/GXAurora.h"
#include "dolphin/gx/GXGet.h"
#include "dolphin/gx/GXPixel.h"
#include "dolphin/gx/GXTransform.h"
#include "m_Do/m_Do_mtx.h"
#include "mods/hook.hpp"
#include "mods/service.hpp"
#include "mods/svc/camera.h"
#include "mods/svc/config.h"
#include "mods/svc/gfx.h"
#include "mods/svc/hook.h"
#include "mods/svc/log.h"
#include "mods/svc/resource.h"
#include "mods/svc/ui.h"
#include <algorithm>
#include <cmath>
#include <cstring>
#include <type_traits>
#include <utility>
#include <webgpu/webgpu.h>
DEFINE_MOD();
IMPORT_SERVICE(ConfigService, svc_config);
IMPORT_SERVICE(ResourceService, svc_resource);
IMPORT_SERVICE(UiService, svc_ui);
IMPORT_SERVICE(GfxService, svc_gfx);
IMPORT_SERVICE(CameraService, svc_camera);
IMPORT_SERVICE(HookService, svc_hook);
IMPORT_SERVICE(LogService, svc_log);
namespace {
ConfigVarHandle g_cvarEnabled = 0;
ConfigVarHandle g_cvarMapSize = 0;
ConfigVarHandle g_cvarNoFrustumClipping = 0;
ConfigVarHandle g_cvarStrength = 0;
ConfigVarHandle g_cvarPcf = 0;
ConfigVarHandle g_cvarBias = 0;
ConfigVarHandle g_cvarBoxRadius = 0;
ConfigVarHandle g_cvarEdgeFadeWidth = 0;
ConfigVarHandle g_cvarContactShadows = 0;
ConfigVarHandle g_cvarDebugView = 0;
GfxDrawTypeHandle g_drawType = 0;
GfxStageHookHandle g_sceneBeginHook = 0;
GfxStageHookHandle g_sceneAfterTerrainHook = 0;
GfxStageHookHandle g_frameBeforeHudHook = 0;
UiWindowHandle g_controlsWindow = 0;
ResourceBuffer g_shaderSource = RESOURCE_BUFFER_INIT;
GfxDeviceInfo g_deviceInfo = GFX_DEVICE_INFO_INIT;
WGPURenderPipeline g_compositePipeline = nullptr; // multiply blend
WGPURenderPipeline g_compositeDebugPipeline = nullptr; // no blend (debug views)
WGPUBindGroupLayout g_compositeLayout = nullptr;
WGPUBindGroupLayout g_compositeDebugLayout = nullptr;
struct MapPassOutput {
bool ready = false;
WGPUTextureView shadowMap = nullptr; // frame-pooled
WGPUTextureView lightColor = nullptr; // frame-pooled
uint32_t mapSize = 0;
Mtx44 lightVp; // world -> light receiver projection, row-major game convention
float dirToLightWorld[3]; // toward the light, normalized
float fade = 0.0f;
};
MapPassOutput g_mapPass;
bool g_replayingSceneLists = false;
constexpr float kLightDistance = 30000.0f;
constexpr float kLightNear = 100.0f;
constexpr float kLightFar = 60000.0f;
constexpr float kMaxLightLookahead = 10000.0f;
constexpr float kSunMoonDistance = 80000.0f;
constexpr float kSunMoonZDistance = -48000.0f;
DEFINE_HOOK(&dDlst_shadowControl_c::imageDraw, GameShadowImageDraw);
DEFINE_HOOK(&dDlst_shadowControl_c::draw, GameShadowDraw);
DEFINE_HOOK(&drawCloudShadow, CloudShadowDraw);
DEFINE_HOOK(static_cast<int (J3DUClipper::*)(f32 const (*)[4], Vec, f32) const>(&J3DUClipper::clip),
ClipperSphereClip);
DEFINE_HOOK(
static_cast<int (J3DUClipper::*)(f32 const (*)[4], Vec*, Vec*) const>(&J3DUClipper::clip),
ClipperBoxClip);
DEFINE_HOOK(GXCopyTex, CopyTex);
// Mirror of the WGSL Uniforms struct (keep in sync with res/shadow.wgsl).
struct ShadowUniforms {
float world_from_proj[16];
float view_from_proj[16];
float proj_from_view[16];
float light_vp[16];
float light_dir_view[3];
float bias;
float size[2];
float inv_size[2];
float edge_fade_width;
float strength;
float pcf_taps;
float contact_enabled;
float contact_thickness;
float contact_length;
uint32_t debug_mode;
float _pad0;
};
static_assert(sizeof(ShadowUniforms) % 16 == 0);
struct DrawPayload {
WGPUTextureView sceneDepth; // frame-pooled
WGPUTextureView shadowMap; // frame-pooled
WGPUTextureView lightColor; // frame-pooled
uint32_t uniform_offset;
uint32_t uniform_size;
uint32_t debug_mode;
};
static_assert(sizeof(DrawPayload) <= GFX_INLINE_DRAW_PAYLOAD_SIZE);
static_assert(std::is_trivially_copyable_v<DrawPayload>);
struct LightCamera {
Mtx view;
Mtx44 ortho;
Mtx44 vp;
float dirToLight[3];
float fade = 0.0f;
};
struct SceneCamera {
bool valid = false;
bool raw_valid = false;
CameraInfo info = CAMERA_INFO_INIT;
Mtx raw_view;
f32 raw_projection[7]{};
Mtx44 raw_projection_mtx;
};
SceneCamera g_sceneCamera;
struct ActualLightDebugState {
bool active = false;
Mtx savedView;
f32 savedProjection[7];
f32 savedViewport[6];
u32 savedScissor[4];
};
ActualLightDebugState g_actualLightDebug;
struct replay_scope {
replay_scope() { g_replayingSceneLists = true; }
~replay_scope() { g_replayingSceneLists = false; }
};
int64_t get_int_option(ConfigVarHandle handle, int64_t fallback) {
int64_t value = fallback;
if (handle == 0 || svc_config->get_int(mod_ctx, handle, &value) != MOD_OK) {
return fallback;
}
return value;
}
bool get_bool_option(ConfigVarHandle handle, bool fallback) {
bool value = fallback;
if (handle == 0 || svc_config->get_bool(mod_ctx, handle, &value) != MOD_OK) {
return fallback;
}
return value;
}
int64_t get_debug_mode() {
return std::clamp<int64_t>(get_int_option(g_cvarDebugView, 0), 0, 10);
}
bool matrix_ready(const Mtx m) {
float basis = 0.0f;
for (int r = 0; r < 3; ++r) {
for (int c = 0; c < 4; ++c) {
if (!std::isfinite(m[r][c])) {
return false;
}
if (c < 3) {
basis += std::fabs(m[r][c]);
}
}
}
return basis > 0.001f;
}
bool projection_vector_ready(const f32 projection[7]) {
if (projection[0] != 0.0f) {
return false;
}
for (int i = 1; i < 7; ++i) {
if (!std::isfinite(projection[i])) {
return false;
}
}
return std::fabs(projection[1]) > 0.001f && std::fabs(projection[3]) > 0.001f &&
std::fabs(projection[6]) > 0.001f;
}
// Row-major game matrix -> column-major WGSL layout (matching CameraService).
void store_column_major(const Mtx44 in, float out[16]) {
for (int c = 0; c < 4; ++c) {
for (int r = 0; r < 4; ++r) {
out[c * 4 + r] = in[r][c];
}
}
}
void copy_projection(const Mtx44 in, Mtx44 out) {
std::memcpy(out, in, sizeof(Mtx44));
// TODO: check GfxDeviceInfo.uses_reversed_z
for (int c = 0; c < 4; ++c) {
out[2][c] = -out[2][c];
}
}
void projection_vector_from_perspective(const Mtx44 projection, f32 out[7]) {
out[0] = 0.0f;
out[1] = projection[0][0];
out[2] = projection[0][2];
out[3] = projection[1][1];
out[4] = projection[1][2];
out[5] = projection[2][2];
out[6] = projection[2][3];
}
const view_class* stage_game_view(const GfxStageContext* stageCtx) {
if (stageCtx == nullptr || stageCtx->struct_size < sizeof(GfxStageContext) ||
stageCtx->game_view == nullptr)
{
return nullptr;
}
return static_cast<const view_class*>(stageCtx->game_view);
}
bool capture_raw_camera(
const view_class* gameView, Mtx outView, Mtx44 outProjectionMtx, f32 outProjection[7]) {
if (gameView == nullptr || !matrix_ready(gameView->viewMtx)) {
return false;
}
std::memcpy(outProjectionMtx, gameView->projMtx, sizeof(Mtx44));
projection_vector_from_perspective(outProjectionMtx, outProjection);
if (!projection_vector_ready(outProjection)) {
return false;
}
cMtx_copy(gameView->viewMtx, outView);
return true;
}
bool capture_scene_camera(const GfxStageContext* stageCtx) {
g_sceneCamera.valid = false;
g_sceneCamera.raw_valid = false;
const view_class* gameView = stage_game_view(stageCtx);
if (gameView == nullptr) {
return false;
}
CameraInfo info = CAMERA_INFO_INIT;
if (svc_camera->get_camera(mod_ctx, stageCtx->game_view, &info) != MOD_OK) {
return false;
}
g_sceneCamera.info = info;
g_sceneCamera.valid = true;
g_sceneCamera.raw_valid = capture_raw_camera(gameView, g_sceneCamera.raw_view,
g_sceneCamera.raw_projection_mtx, g_sceneCamera.raw_projection);
return true;
}
bool get_replay_camera(Mtx outView, Mtx44 outProjectionMtx, f32 outProjection[7]) {
if (g_sceneCamera.raw_valid && matrix_ready(g_sceneCamera.raw_view)) {
cMtx_copy(g_sceneCamera.raw_view, outView);
std::memcpy(outProjectionMtx, g_sceneCamera.raw_projection_mtx,
sizeof(g_sceneCamera.raw_projection_mtx));
std::memcpy(
outProjection, g_sceneCamera.raw_projection, sizeof(g_sceneCamera.raw_projection));
return projection_vector_ready(outProjection);
}
return false;
}
float wrap_daytime(float daytime) {
if (!std::isfinite(daytime)) {
return 180.0f;
}
float wrapped = std::fmod(daytime, 360.0f);
if (wrapped < 0.0f) {
wrapped += 360.0f;
}
return wrapped;
}
float daytime_percent(float max, float min, float value) {
const float range = max - min;
if (range == 0.0f) {
return 1.0f;
}
const float percent = 1.0f - ((max - value) / range);
return percent < 1.0f ? percent : 1.0f;
}
float sun_moon_angle(float daytime) {
daytime = wrap_daytime(daytime);
if (daytime >= 90.0f && daytime <= 270.0f) {
return daytime_percent(270.0f, 90.0f, daytime) * 150.0f + 105.0f;
}
float angle = daytime;
if (angle < 90.0f) {
angle += 360.0f;
}
angle = daytime_percent(450.0f, 270.0f, angle) * 210.0f + 255.0f;
if (angle > 360.0f) {
angle -= 360.0f;
}
return angle;
}
cXyz sun_moon_offset(float daytime) {
const float angle = DEG_TO_RAD(sun_moon_angle(daytime));
const float angleSin = sinf(angle);
const float angleCos = cosf(angle);
return cXyz{
angleSin * kSunMoonDistance, -angleCos * kSunMoonDistance, angleCos * kSunMoonZDistance};
}
bool build_composite_pipeline(
bool blend, WGPURenderPipeline& outPipeline, WGPUBindGroupLayout& outLayout) {
WGPUShaderSourceWGSL wgsl = WGPU_SHADER_SOURCE_WGSL_INIT;
wgsl.code = {static_cast<const char*>(g_shaderSource.data), g_shaderSource.size};
WGPUShaderModuleDescriptor moduleDesc = WGPU_SHADER_MODULE_DESCRIPTOR_INIT;
moduleDesc.nextInChain = &wgsl.chain;
moduleDesc.label = {"shadow composite", WGPU_STRLEN};
WGPUShaderModule module = wgpuDeviceCreateShaderModule(g_deviceInfo.device, &moduleDesc);
if (module == nullptr) {
return false;
}
// Multiply blend: fragment output is the darkening multiplier (result = dst * src).
WGPUBlendState blendState{
.color = {.operation = WGPUBlendOperation_Add,
.srcFactor = WGPUBlendFactor_Dst,
.dstFactor = WGPUBlendFactor_Zero},
.alpha = {.operation = WGPUBlendOperation_Add,
.srcFactor = WGPUBlendFactor_Zero,
.dstFactor = WGPUBlendFactor_One},
};
WGPUColorTargetState colorTarget = WGPU_COLOR_TARGET_STATE_INIT;
colorTarget.format = g_deviceInfo.color_format;
if (blend) {
colorTarget.blend = &blendState;
}
WGPUFragmentState fragment = WGPU_FRAGMENT_STATE_INIT;
fragment.module = module;
fragment.entryPoint = {"fs_main", WGPU_STRLEN};
fragment.targetCount = 1;
fragment.targets = &colorTarget;
WGPUDepthStencilState depthStencil = WGPU_DEPTH_STENCIL_STATE_INIT;
depthStencil.format = g_deviceInfo.depth_format;
depthStencil.depthWriteEnabled = WGPUOptionalBool_False;
depthStencil.depthCompare = WGPUCompareFunction_Always;
WGPURenderPipelineDescriptor pipelineDesc = WGPU_RENDER_PIPELINE_DESCRIPTOR_INIT;
pipelineDesc.label = {blend ? "shadow composite" : "shadow composite (debug)", WGPU_STRLEN};
pipelineDesc.vertex.module = module;
pipelineDesc.vertex.entryPoint = {"vs_main", WGPU_STRLEN};
pipelineDesc.primitive.topology = WGPUPrimitiveTopology_TriangleList;
pipelineDesc.depthStencil = &depthStencil;
pipelineDesc.multisample.count = g_deviceInfo.sample_count;
pipelineDesc.fragment = &fragment;
outPipeline = wgpuDeviceCreateRenderPipeline(g_deviceInfo.device, &pipelineDesc);
wgpuShaderModuleRelease(module);
if (outPipeline == nullptr) {
return false;
}
outLayout = wgpuRenderPipelineGetBindGroupLayout(outPipeline, 0);
return outLayout != nullptr;
}
// Render worker thread: fullscreen deferred-shadow composite.
void on_draw(
ModContext*, const GfxDrawContext* ctx, const void* payload, size_t payloadSize, void*) {
if (payloadSize != sizeof(DrawPayload)) {
return;
}
DrawPayload data;
std::memcpy(&data, payload, sizeof(data));
WGPURenderPipeline pipeline =
data.debug_mode != 0 ? g_compositeDebugPipeline : g_compositePipeline;
WGPUBindGroupLayout layout = data.debug_mode != 0 ? g_compositeDebugLayout : g_compositeLayout;
if (data.sceneDepth == nullptr || data.shadowMap == nullptr || data.lightColor == nullptr ||
pipeline == nullptr)
{
return;
}
WGPUBindGroupEntry entries[4] = {WGPU_BIND_GROUP_ENTRY_INIT, WGPU_BIND_GROUP_ENTRY_INIT,
WGPU_BIND_GROUP_ENTRY_INIT, WGPU_BIND_GROUP_ENTRY_INIT};
entries[0].binding = 0;
entries[0].textureView = data.sceneDepth;
entries[1].binding = 1;
entries[1].textureView = data.shadowMap;
entries[2].binding = 2;
entries[2].buffer = ctx->uniform_buffer;
entries[2].offset = data.uniform_offset;
entries[2].size = data.uniform_size;
entries[3].binding = 3;
entries[3].textureView = data.lightColor;
WGPUBindGroupDescriptor bindGroupDesc = WGPU_BIND_GROUP_DESCRIPTOR_INIT;
bindGroupDesc.layout = layout;
bindGroupDesc.entryCount = 4;
bindGroupDesc.entries = entries;
WGPUBindGroup bindGroup = wgpuDeviceCreateBindGroup(ctx->device, &bindGroupDesc);
if (bindGroup == nullptr) {
return;
}
wgpuRenderPassEncoderSetPipeline(ctx->pass, pipeline);
wgpuRenderPassEncoderSetBindGroup(ctx->pass, 0, bindGroup, 0, nullptr);
wgpuRenderPassEncoderDraw(ctx->pass, 3, 1, 0, 0);
wgpuBindGroupRelease(bindGroup);
}
// Picks the sun or moon (whichever is above the horizon) and returns the normalized
// world-space direction *toward* the light plus a horizon fade factor. False = no light.
bool compute_light(float outDirToLight[3], float& outFade) {
dScnKy_env_light_c* envLight = dKy_getEnvlight();
if (envLight == nullptr) {
return false;
}
// The packet positions can be stale when this runs before the world lists are consumed.
// Mirror dScnKy_env_light_c::setSunpos() so --time-of-day directly moves the debug light.
const float daytime = wrap_daytime(dComIfGs_getTime());
cXyz offset = sun_moon_offset(daytime);
if (offset.y <= 0.0f) {
offset = sun_moon_offset(daytime + 180.0f);
}
const float length = std::sqrt(offset.x * offset.x + offset.y * offset.y + offset.z * offset.z);
if (length < 1.0f) {
return false;
}
outDirToLight[0] = offset.x / length;
outDirToLight[1] = offset.y / length;
outDirToLight[2] = offset.z / length;
// Fade shadows out as the light approaches the horizon (elevation below ~11 degrees).
outFade = std::clamp((outDirToLight[1] - 0.05f) / 0.15f, 0.0f, 1.0f);
return outFade > 0.0f;
}
bool build_light_camera(const Mtx cameraView, uint32_t mapSize, float radius, LightCamera& out) {
Mtx cameraInvView;
cMtx_inverse(cameraView, cameraInvView);
if (!matrix_ready(cameraInvView)) {
return false;
}
if (!compute_light(out.dirToLight, out.fade)) {
return false;
}
// Fit a fixed-radius ortho box around the visible play space. The camera target alone can sit
// behind the receiver field, while a far-horizon center drops foreground receivers.
const cXyz eye{cameraInvView[0][3], cameraInvView[1][3], cameraInvView[2][3]};
cXyz forward{-cameraInvView[0][2], -cameraInvView[1][2], -cameraInvView[2][2]};
const float forwardLength =
std::sqrt(forward.x * forward.x + forward.y * forward.y + forward.z * forward.z);
if (forwardLength > 0.001f) {
forward = forward / forwardLength;
} else {
forward = cXyz{0.0f, 0.0f, -1.0f};
}
const float lookahead = std::min(radius * 0.75f, kMaxLightLookahead);
const cXyz center = eye + forward * lookahead;
const cXyz lightEye{center.x + out.dirToLight[0] * kLightDistance,
center.y + out.dirToLight[1] * kLightDistance,
center.z + out.dirToLight[2] * kLightDistance};
const bool nearlyVertical = std::fabs(out.dirToLight[1]) > 0.99f;
cXyz up = nearlyVertical ? cXyz{0.0f, 0.0f, 1.0f} : cXyz{0.0f, 1.0f, 0.0f};
cMtx_lookAt(out.view, &lightEye, &center, &up, 0);
const float unitsPerTexel = (2.0f * radius) / static_cast<float>(mapSize);
out.view[0][3] = std::round(out.view[0][3] / unitsPerTexel) * unitsPerTexel;
out.view[1][3] = std::round(out.view[1][3] / unitsPerTexel) * unitsPerTexel;
C_MTXOrtho(out.ortho, radius, -radius, -radius, radius, kLightNear, kLightFar);
cMtx_concatProjView(out.ortho, out.view, out.vp);
return true;
}
bool build_light_replay_projection(
const LightCamera& lightCamera, const Mtx cameraView, Mtx44 out) {
Mtx cameraInvView;
cMtx_inverse(cameraView, cameraInvView);
if (!matrix_ready(cameraInvView)) {
return false;
}
Mtx lightFromCamera;
cMtx_concat(lightCamera.view, cameraInvView, lightFromCamera);
cMtx_concatProjView(lightCamera.ortho, lightFromCamera, out);
return true;
}
// True when the dynamic shadow pass will run this frame: enabled, a camera exists, and the
// sun or moon is above the horizon. Also gates the game-shadow skip hooks, which fire earlier
// in the painter than our SCENE_AFTER_TERRAIN hook.
bool dynamic_shadows_wanted() {
if (!get_bool_option(g_cvarEnabled, true)) {
return false;
}
if (!g_sceneCamera.raw_valid) {
return false;
}
float dirToLight[3];
float fade = 0.0f;
return compute_light(dirToLight, fade);
}
HookAction on_game_shadow_pre(ModContext*, void*, void*, void*) {
if (!dynamic_shadows_wanted()) {
return HOOK_CONTINUE;
}
return HOOK_SKIP_ORIGINAL;
}
HookAction on_frustum_clip_pre(ModContext*, void*, void* retval, void*) {
if (!get_bool_option(g_cvarNoFrustumClipping, false) || !dynamic_shadows_wanted()) {
return HOOK_CONTINUE;
}
if (retval != nullptr) {
*static_cast<int*>(retval) = 0;
}
return HOOK_SKIP_ORIGINAL;
}
HookAction on_copy_tex_pre(ModContext*, void*, void*, void*) {
return g_replayingSceneLists ? HOOK_SKIP_ORIGINAL : HOOK_CONTINUE;
}
void draw_opaque_scene_lists() {
dComIfGd_drawOpaListBG();
dComIfGd_drawOpaListDarkBG();
dComIfGd_drawOpaListMiddle();
dComIfGd_drawOpaList();
dComIfGd_drawOpaListDark();
dComIfGd_drawOpaListPacket();
}
bool draw_lists_ready() {
return dComIfGd_getOpaListBG() != nullptr && dComIfGd_getOpaList() != nullptr &&
dComIfGd_getOpaListDark() != nullptr && dComIfGd_getXluListBG() != nullptr &&
dComIfGd_getListPacket() != nullptr;
}
void render_shadow_map(
const Mtx replayView, const Mtx44 replayProjectionMtx, const f32 replayProjection[7]);
void restore_actual_light_debug() {
if (!g_actualLightDebug.active) {
return;
}
j3dSys.setViewMtx(g_actualLightDebug.savedView);
GXSetProjectionv(g_actualLightDebug.savedProjection);
GXSetViewport(g_actualLightDebug.savedViewport[0], g_actualLightDebug.savedViewport[1],
g_actualLightDebug.savedViewport[2], g_actualLightDebug.savedViewport[3],
g_actualLightDebug.savedViewport[4], g_actualLightDebug.savedViewport[5]);
GXSetScissor(g_actualLightDebug.savedScissor[0], g_actualLightDebug.savedScissor[1],
g_actualLightDebug.savedScissor[2], g_actualLightDebug.savedScissor[3]);
dKy_setLight();
J3DShape::resetVcdVatCache();
g_actualLightDebug.active = false;
}
void on_scene_begin(ModContext*, const GfxStageContext* stageCtx, void*) {
restore_actual_light_debug();
capture_scene_camera(stageCtx);
if (!get_bool_option(g_cvarEnabled, true) || get_debug_mode() != 9) {
return;
}
Mtx cameraView;
if (!g_sceneCamera.raw_valid || !matrix_ready(g_sceneCamera.raw_view)) {
return;
}
cMtx_copy(g_sceneCamera.raw_view, cameraView);
const uint32_t mapSize = 1024u << std::clamp<int64_t>(get_int_option(g_cvarMapSize, 1), 0, 2);
const float radius =
static_cast<float>(std::clamp<int64_t>(get_int_option(g_cvarBoxRadius, 6000), 1000, 20000));
LightCamera lightCamera{};
if (!build_light_camera(cameraView, mapSize, radius, lightCamera)) {
return;
}
Mtx44 lightProjection;
if (!build_light_replay_projection(lightCamera, cameraView, lightProjection)) {
return;
}
cMtx_copy(cameraView, g_actualLightDebug.savedView);
GXGetProjectionv(g_actualLightDebug.savedProjection);
GXGetViewportv(g_actualLightDebug.savedViewport);
GXGetScissor(&g_actualLightDebug.savedScissor[0], &g_actualLightDebug.savedScissor[1],
&g_actualLightDebug.savedScissor[2], &g_actualLightDebug.savedScissor[3]);
g_actualLightDebug.active = true;
j3dSys.setViewMtx(g_actualLightDebug.savedView);
GXSetProjectionFull(lightProjection);
dKy_setLight();
J3DShape::resetVcdVatCache();
}
void on_scene_after_terrain(ModContext*, const GfxStageContext* stageCtx, void*) {
if (g_mapPass.ready) {
return;
}
const view_class* gameView = stage_game_view(stageCtx);
Mtx replayView;
Mtx44 replayProjectionMtx;
f32 replayProjection[7];
if (!capture_raw_camera(gameView, replayView, replayProjectionMtx, replayProjection)) {
return;
}
render_shadow_map(replayView, replayProjectionMtx, replayProjection);
}
// Game thread, after the draw handlers have populated next frame's scene lists: replay opaque scene
// geometry from the light's point of view.
void render_shadow_map(
const Mtx replayView, const Mtx44 replayProjectionMtx, const f32 replayProjection[7]) {
if (g_mapPass.ready || !get_bool_option(g_cvarEnabled, true)) {
return;
}
const int64_t debugMode = get_debug_mode();
if (debugMode == 9) {
return;
}
if (!matrix_ready(replayView)) {
return;
}
Mtx replayViewMtx;
cMtx_copy(replayView, replayViewMtx);
const uint32_t mapSize = 1024u << std::clamp<int64_t>(get_int_option(g_cvarMapSize, 1), 0, 2);
const bool cameraReplayDebug = debugMode == 10;
const float radius =
static_cast<float>(std::clamp<int64_t>(get_int_option(g_cvarBoxRadius, 6000), 1000, 20000));
LightCamera lightCamera{};
if (!build_light_camera(replayViewMtx, mapSize, radius, lightCamera)) {
return;
}
Mtx44 lightReplayProjection;
if (!build_light_replay_projection(lightCamera, replayViewMtx, lightReplayProjection)) {
return;
}
f32 savedProjection[7];
GXGetProjectionv(savedProjection);
f32 savedViewport[6];
GXGetViewportv(savedViewport);
u32 savedScissor[4];
GXGetScissor(&savedScissor[0], &savedScissor[1], &savedScissor[2], &savedScissor[3]);
Mtx savedView;
cMtx_copy(j3dSys.getViewMtx(), savedView);
auto restore_game_camera = [&]() {
j3dSys.setViewMtx(savedView);
GXSetProjectionv(savedProjection);
GXSetViewport(savedViewport[0], savedViewport[1], savedViewport[2], savedViewport[3],
savedViewport[4], savedViewport[5]);
GXSetScissor(savedScissor[0], savedScissor[1], savedScissor[2], savedScissor[3]);
dKy_setLight();
};
auto set_replay_camera = [&]() {
j3dSys.setViewMtx(replayViewMtx);
if (cameraReplayDebug) {
GXSetProjectionv(replayProjection);
} else {
GXSetProjectionFull(lightReplayProjection);
}
dKy_setLight();
};
if (!draw_lists_ready()) {
return;
}
if (svc_gfx->create_pass(mod_ctx, mapSize, mapSize) != MOD_OK) {
return;
}
J3DShape::resetVcdVatCache();
set_replay_camera();
GXSetViewport(0.0f, 0.0f, static_cast<float>(mapSize), static_cast<float>(mapSize), 0.0f, 1.0f);
GXSetViewportRender(
0.0f, 0.0f, static_cast<float>(mapSize), static_cast<float>(mapSize), 0.0f, 1.0f);
GXSetScissorRender(0, 0, mapSize, mapSize);
dKy_setLight();
GXSetColorUpdate(GX_TRUE);
GXSetAlphaUpdate(GX_TRUE);
GXSetZMode(GX_TRUE, GX_LEQUAL, GX_TRUE);
{
replay_scope replay;
draw_opaque_scene_lists();
}
j3dSys.reinitGX();
J3DShape::resetVcdVatCache();
restore_game_camera();
GfxResolveDesc resolveDesc = GFX_RESOLVE_DESC_INIT;
resolveDesc.color = true;
resolveDesc.depth = true;
GfxResolvedTargets resolved = GFX_RESOLVED_TARGETS_INIT;
if (svc_gfx->resolve_pass(mod_ctx, &resolveDesc, &resolved) != MOD_OK ||
resolved.color == nullptr || resolved.depth == nullptr)
{
return;
}
j3dSys.reinitGX();
J3DShape::resetVcdVatCache();
restore_game_camera();
g_mapPass.lightColor = resolved.color;
g_mapPass.shadowMap = resolved.depth;
g_mapPass.mapSize = mapSize;
copy_projection(lightCamera.vp, g_mapPass.lightVp);
std::memcpy(
g_mapPass.dirToLightWorld, lightCamera.dirToLight, sizeof(g_mapPass.dirToLightWorld));
g_mapPass.fade = lightCamera.fade;
g_mapPass.ready = true;
}
// Game thread, after the full 3D scene: deferred composite.
void on_frame_before_hud(ModContext*, const GfxStageContext*, void*) {
const int64_t debugMode = get_debug_mode();
restore_actual_light_debug();
const MapPassOutput mapPass = std::exchange(g_mapPass, {});
if (debugMode == 9) {
return;
}
if (!mapPass.ready || mapPass.shadowMap == nullptr || mapPass.lightColor == nullptr) {
return;
}
if (!g_sceneCamera.valid) {
return;
}
const CameraInfo& camera = g_sceneCamera.info;
GfxResolveDesc resolveDesc = GFX_RESOLVE_DESC_INIT;
resolveDesc.color = false;
resolveDesc.depth = true;
GfxResolvedTargets resolved = GFX_RESOLVED_TARGETS_INIT;
if (svc_gfx->resolve_pass(mod_ctx, &resolveDesc, &resolved) != MOD_OK ||
resolved.depth == nullptr)
{
return;
}
ShadowUniforms uniforms{};
std::memcpy(uniforms.world_from_proj, camera.world_from_proj, sizeof(uniforms.world_from_proj));
std::memcpy(uniforms.view_from_proj, camera.view_from_proj, sizeof(uniforms.view_from_proj));
std::memcpy(uniforms.proj_from_view, camera.proj_from_view, sizeof(uniforms.proj_from_view));
store_column_major(mapPass.lightVp, uniforms.light_vp);
// Rotate the world-space light direction into view space (w = 0).
for (int r = 0; r < 3; ++r) {
uniforms.light_dir_view[r] =
camera.view_from_world[0 * 4 + r] * mapPass.dirToLightWorld[0] +
camera.view_from_world[1 * 4 + r] * mapPass.dirToLightWorld[1] +
camera.view_from_world[2 * 4 + r] * mapPass.dirToLightWorld[2];
}
// Bias is configured in world units along the light direction.
uniforms.bias =
static_cast<float>(std::clamp<int64_t>(get_int_option(g_cvarBias, 15), 0, 200)) /
(kLightFar - kLightNear);
uniforms.size[0] = static_cast<float>(mapPass.mapSize);
uniforms.size[1] = static_cast<float>(mapPass.mapSize);
uniforms.inv_size[0] = 1.0f / uniforms.size[0];
uniforms.inv_size[1] = 1.0f / uniforms.size[1];
uniforms.edge_fade_width =
static_cast<float>(std::clamp<int64_t>(get_int_option(g_cvarEdgeFadeWidth, 32), 0, 128));
uniforms.strength =
mapPass.fade *
static_cast<float>(std::clamp<int64_t>(get_int_option(g_cvarStrength, 45), 0, 100)) /
100.0f;
uniforms.pcf_taps = static_cast<float>(std::clamp<int64_t>(get_int_option(g_cvarPcf, 1), 0, 2));
uniforms.contact_enabled = get_bool_option(g_cvarContactShadows, false) ? 1.0f : 0.0f;
uniforms.contact_thickness = 25.0f;
uniforms.contact_length = 60.0f;
uniforms.debug_mode = static_cast<uint32_t>(debugMode);
GfxRange uniformRange{0, 0};
if (svc_gfx->push_uniform(mod_ctx, &uniforms, sizeof(uniforms), &uniformRange) != MOD_OK) {
return;
}
const DrawPayload payload{resolved.depth, mapPass.shadowMap, mapPass.lightColor,
uniformRange.offset, uniformRange.size, static_cast<uint32_t>(debugMode)};
svc_gfx->push_draw(mod_ctx, g_drawType, &payload, sizeof(payload));
}
void add_control(UiElementHandle pane, const UiControlDesc& desc) {
svc_ui->pane_add_control(mod_ctx, pane, &desc, nullptr);
}
void add_toggle(UiElementHandle pane, const char* label, ConfigVarHandle cvar, const char* help) {
UiControlDesc control = UI_CONTROL_DESC_INIT;
control.kind = UI_CONTROL_TOGGLE;
control.label = label;
control.help_rml = help;
control.binding = UI_BINDING_CONFIG_VAR;
control.config_var = cvar;
add_control(pane, control);
}
void add_select(UiElementHandle pane, const char* label, ConfigVarHandle cvar, const char** options,
uint32_t optionCount, const char* help) {
UiControlDesc control = UI_CONTROL_DESC_INIT;
control.kind = UI_CONTROL_SELECT;
control.label = label;
control.help_rml = help;
control.binding = UI_BINDING_CONFIG_VAR;
control.config_var = cvar;
control.options = options;
control.option_count = optionCount;
add_control(pane, control);
}
void add_number(UiElementHandle pane, const char* label, ConfigVarHandle cvar, int64_t min,
int64_t max, int64_t step, const char* suffix, const char* help) {
UiControlDesc control = UI_CONTROL_DESC_INIT;
control.kind = UI_CONTROL_NUMBER;
control.label = label;
control.help_rml = help;
control.binding = UI_BINDING_CONFIG_VAR;
control.config_var = cvar;
control.min = min;
control.max = max;
control.step = step;
control.suffix = suffix;
add_control(pane, control);
}
ModResult build_controls_tab(
ModContext*, UiWindowHandle, UiElementHandle left, UiElementHandle right, void*, ModError*) {
(void)right;
svc_ui->pane_add_section(mod_ctx, left, "Shadow Map");
add_toggle(left, "Enabled", g_cvarEnabled, "Enables dynamic shadows.");
static const char* kMapSizes[] = {"1024", "2048", "4096"};
add_select(left, "Map Size", g_cvarMapSize, kMapSizes, 3,
"Shadow map resolution. Larger is sharper and slower.");
add_toggle(left, "No Frustum Clipping", g_cvarNoFrustumClipping,
"Keeps camera-frustum-culled objects in draw lists so off-screen objects can cast "
"dynamic shadows. This can be expensive.");
add_number(left, "Coverage", g_cvarBoxRadius, 1000, 20000, 500, nullptr,
"Radius of the shadowed area around the camera, in world units. Smaller is sharper.");
add_number(left, "Fade Out", g_cvarEdgeFadeWidth, 0, 128, 32, " texels",
"Fade out shadows gradually near the edge of the coverage area.");
svc_ui->pane_add_section(mod_ctx, left, "Appearance");
add_number(left, "Strength", g_cvarStrength, 0, 100, 5, "%", "How dark shadowed areas become.");
static const char* kPcfOptions[] = {"Off", "3x3", "5x5"};
add_select(left, "Soft Shadows", g_cvarPcf, kPcfOptions, 3,
"Percentage-closer filtering tap pattern; softens shadow edges.");
add_number(left, "Bias", g_cvarBias, 0, 200, 5, nullptr,
"Depth bias in world units. Raise to remove shadow acne; lower to reduce peter-panning.");
add_toggle(left, "Contact Shadows", g_cvarContactShadows,
"Adds a screen-space raymarch for small-scale contact darkening the map misses.");
svc_ui->pane_add_section(mod_ctx, left, "Debug");
static const char* kDebugOptions[] = {"Off", "Shadow Map", "Shadow Factor", "Occlusion",
"Light UV", "Compare Sign", "Depth Values", "Receiver Range", "Bounds", "Light View",
"Camera Replay"};
add_select(left, "Debug View", g_cvarDebugView, kDebugOptions, 11,
"Shadow Map: light-space depth buffer<br/>Shadow Factor: final "
"darkening term<br/>Occlusion: map comparison result<br/>Light UV: receiver "
"projection coverage<br/>Compare Sign: current comparison in red and opposite "
"comparison in blue<br/>Depth Values: receiver depth in red and map depth in green<br/>"
"Receiver Range: beyond-far in red, valid depth in green, and before-near in blue<br/>"
"Bounds: valid X in red, valid Y in green, and valid depth in blue<br/>Light View: "
"renders the game world directly from the light camera<br/>Camera Replay: "
"captures the same draw-list replay from the gameplay camera");
return MOD_OK;
}
void on_controls_window_closed(ModContext*, UiWindowHandle, void*) {
g_controlsWindow = 0;
}
void on_open_controls(ModContext*, void*) {
if (g_controlsWindow != 0) {
return;
}
UiTabDesc tabs[1] = {UI_TAB_DESC_INIT};
tabs[0].title = "Controls";
tabs[0].build = build_controls_tab;
UiWindowDesc desc = UI_WINDOW_DESC_INIT;
desc.tabs = tabs;
desc.tab_count = 1;
desc.on_closed = on_controls_window_closed;
if (svc_ui->window_push(mod_ctx, &desc, &g_controlsWindow) != MOD_OK) {
svc_log->error(mod_ctx, "failed to open shadow controls window");
}
}
ModResult build_panel(ModContext*, UiElementHandle panel, void*, ModError*) {
UiControlDesc control = UI_CONTROL_DESC_INIT;
control.kind = UI_CONTROL_TOGGLE;
control.label = "Enabled";
control.binding = UI_BINDING_CONFIG_VAR;
control.config_var = g_cvarEnabled;
add_control(panel, control);
control = UI_CONTROL_DESC_INIT;
control.kind = UI_CONTROL_BUTTON;
control.label = "Open Controls";
control.on_pressed = on_open_controls;
add_control(panel, control);
return MOD_OK;
}
ModResult register_bool_option(
const char* name, bool defaultValue, ConfigVarHandle& outHandle, ModError* error) {
ConfigVarDesc cvarDesc = CONFIG_VAR_DESC_INIT;
cvarDesc.name = name;
cvarDesc.type = CONFIG_VAR_BOOL;
cvarDesc.default_bool = defaultValue;
if (svc_config->register_var(mod_ctx, &cvarDesc, &outHandle) != MOD_OK) {
return mods::set_error(error, MOD_ERROR, "failed to register shadow option");
}
return MOD_OK;
}
ModResult register_int_option(
const char* name, int64_t defaultValue, ConfigVarHandle& outHandle, ModError* error) {
ConfigVarDesc cvarDesc = CONFIG_VAR_DESC_INIT;
cvarDesc.name = name;
cvarDesc.type = CONFIG_VAR_INT;
cvarDesc.default_int = defaultValue;
if (svc_config->register_var(mod_ctx, &cvarDesc, &outHandle) != MOD_OK) {
return mods::set_error(error, MOD_ERROR, "failed to register shadow option");
}
return MOD_OK;
}
} // namespace
extern "C" {
MOD_EXPORT ModResult mod_initialize(ModError* error) {
ModResult result = svc_resource->load(mod_ctx, "shadow.wgsl", &g_shaderSource);
if (result != MOD_OK || g_shaderSource.data == nullptr) {
return mods::set_error(error, result, "failed to load shadow.wgsl");
}
result = register_bool_option("effectEnabled", false, g_cvarEnabled, error);
if (result != MOD_OK) {
return result;
}
result = register_int_option("mapSize", 2, g_cvarMapSize, error);
if (result != MOD_OK) {
return result;
}
result = register_bool_option("noFrustumClipping", true, g_cvarNoFrustumClipping, error);
if (result != MOD_OK) {
return result;
}
result = register_int_option("strength", 45, g_cvarStrength, error);
if (result != MOD_OK) {
return result;
}
result = register_int_option("pcf", 2, g_cvarPcf, error);
if (result != MOD_OK) {
return result;
}
result = register_int_option("bias", 55, g_cvarBias, error);
if (result != MOD_OK) {
return result;
}
result = register_int_option("boxRadius", 6000, g_cvarBoxRadius, error);
if (result != MOD_OK) {
return result;
}
result = register_int_option("edgeFadeWidth", 32, g_cvarEdgeFadeWidth, error);
if (result != MOD_OK) {
return result;
}
result = register_bool_option("contactShadows", true, g_cvarContactShadows, error);
if (result != MOD_OK) {
return result;
}
result = register_int_option("debugView", 0, g_cvarDebugView, error);
if (result != MOD_OK) {
return result;
}
if (svc_gfx->get_device_info(mod_ctx, &g_deviceInfo) != MOD_OK) {
return mods::set_error(error, MOD_ERROR, "failed to query device info");
}
if (!build_composite_pipeline(true, g_compositePipeline, g_compositeLayout) ||
!build_composite_pipeline(false, g_compositeDebugPipeline, g_compositeDebugLayout))
{
return mods::set_error(error, MOD_ERROR, "failed to create composite pipeline");
}
GfxDrawTypeDesc drawDesc = GFX_DRAW_TYPE_DESC_INIT;
drawDesc.label = "shadow composite";
drawDesc.draw = on_draw;
if (svc_gfx->register_draw_type(mod_ctx, &drawDesc, &g_drawType) != MOD_OK) {
return mods::set_error(error, MOD_ERROR, "failed to register draw type");
}
GfxStageHookDesc stageDesc = GFX_STAGE_HOOK_DESC_INIT;
stageDesc.callback = on_scene_begin;
if (svc_gfx->register_stage_hook(
mod_ctx, GFX_STAGE_SCENE_BEGIN, &stageDesc, &g_sceneBeginHook) != MOD_OK)
{
return mods::set_error(error, MOD_ERROR, "failed to register stage hook");
}
stageDesc.callback = on_scene_after_terrain;
if (svc_gfx->register_stage_hook(
mod_ctx, GFX_STAGE_SCENE_AFTER_TERRAIN, &stageDesc, &g_sceneAfterTerrainHook) != MOD_OK)
{
return mods::set_error(error, MOD_ERROR, "failed to register stage hook");
}
stageDesc.callback = on_frame_before_hud;
if (svc_gfx->register_stage_hook(
mod_ctx, GFX_STAGE_FRAME_BEFORE_HUD, &stageDesc, &g_frameBeforeHudHook) != MOD_OK)
{
return mods::set_error(error, MOD_ERROR, "failed to register stage hook");
}
// Skip the game's own shadow rendering while the dynamic pass is active: the
// shadowControl pair covers the actor real/blob shadows, drawCloudShadow the weather
// cloud shadows.
if (mods::hook_add_pre<GameShadowImageDraw>(svc_hook, on_game_shadow_pre) != MOD_OK ||
mods::hook_add_pre<GameShadowDraw>(svc_hook, on_game_shadow_pre) != MOD_OK ||
mods::hook_add_pre<CloudShadowDraw>(svc_hook, on_game_shadow_pre) != MOD_OK)
{
return mods::set_error(error, MOD_ERROR, "failed to hook game shadow rendering");
}
if (mods::hook_add_pre<ClipperSphereClip>(svc_hook, on_frustum_clip_pre) != MOD_OK ||
mods::hook_add_pre<ClipperBoxClip>(svc_hook, on_frustum_clip_pre) != MOD_OK)
{
return mods::set_error(error, MOD_ERROR, "failed to hook frustum clipping");
}
if (mods::hook_add_pre<CopyTex>(svc_hook, on_copy_tex_pre) != MOD_OK) {
return mods::set_error(error, MOD_ERROR, "failed to hook GXCopyTex");
}
UiModsPanelDesc panelDesc = UI_MODS_PANEL_DESC_INIT;
panelDesc.build = build_panel;
svc_ui->register_mods_panel(mod_ctx, &panelDesc);
return MOD_OK;
}
MOD_EXPORT ModResult mod_update(ModError*) {
return MOD_OK;
}
MOD_EXPORT ModResult mod_shutdown(ModError*) {
restore_actual_light_debug();
svc_resource->free(mod_ctx, &g_shaderSource);
if (g_compositePipeline != nullptr) {
wgpuRenderPipelineRelease(g_compositePipeline);
g_compositePipeline = nullptr;
}
if (g_compositeDebugPipeline != nullptr) {
wgpuRenderPipelineRelease(g_compositeDebugPipeline);
g_compositeDebugPipeline = nullptr;
}
if (g_compositeLayout != nullptr) {
wgpuBindGroupLayoutRelease(g_compositeLayout);
g_compositeLayout = nullptr;
}
if (g_compositeDebugLayout != nullptr) {
wgpuBindGroupLayoutRelease(g_compositeDebugLayout);
g_compositeDebugLayout = nullptr;
}
g_cvarEnabled = g_cvarMapSize = g_cvarNoFrustumClipping = 0;
g_cvarStrength = 0;
g_cvarPcf = g_cvarBias = g_cvarBoxRadius = g_cvarEdgeFadeWidth = g_cvarContactShadows =
g_cvarDebugView = 0;
g_drawType = g_sceneBeginHook = g_sceneAfterTerrainHook = g_frameBeforeHudHook = 0;
g_controlsWindow = 0;
g_mapPass = {};
g_sceneCamera.valid = false;
g_sceneCamera.raw_valid = false;
return MOD_OK;
}
}