Files
fs2open.github.com/code/graphics/shadows.cpp
T
Fabian WoltermannandClaude Opus 5.5 7f3e5fae60 Softer shadows in raytracing and RTAO (#7695)
* Add RTAO and unify raytraced/shadow-mapped shadow softness on sun angular size

Raytraced shadow penumbras needed a hand-authored $SunAngularSize: in
stars.tbl, which no shipped content sets -- so in practice nothing ever
got a penumbra, and the shadow-mapped path's $Shadow Smoothness Factor:
had no relation to it at all. Derive a size from the sun bitmap instead
and drive both methods from the same number.

g3_render_rect_screen_aligned_2d() sizes the sun quad so its rad is the
tangent of the half-angle it subtends, making tan(angular radius) equal
to 0.05 * the mission's +Scale: * the emitting disc's fraction of the
bitmap. That fraction is the area of the pixels at or above 90% of the
brightest one, converted to an equivalent radius. Across 90 sun bitmaps
from retail, the MediaVPs, Blue Planet and BtA it lands at a median of
0.261 (retail's chunkier art at 0.487), and the threshold acts as a
constant factor rather than a per-bitmap judgement call -- Spearman rho
0.96 between a 90% and a 50% cutoff.

Taken literally that yields suns 3.9-8.5x Sol's apparent diameter once
+Scale: is folded in (median 1.55, up to 5.0 in retail's own missions),
so the measurement is scaled by 0.25 and clamped to 1.5 degrees, putting
typical content near Sol. Derivation is the default: $SunAngularSize: 0
asks for hard shadows, an explicit value still wins, and missions can
also set +AngularSize: on an individual sun to override both, with FRED
and QtFRED UI (validation and fallback included) to author it. Bitmaps
that can't be read -- and the deliberately blank sun bitmaps mods ship
to get a light source with no visible disc -- fall back to hard shadows
rather than to the widest possible penumbra.

That angular size now sizes the raytraced penumbra cone directly
(traceShadowRayCone() in shadows.sdr), with sample count following the
same Shadow_quality tier that picks the shadow map's resolution (1 ray
at Low, up to 16 at Ultra) rather than being a separate setting. The
shadow-mapped path's tabled smoothness values were tuned by eye against
Sol-sized retail/MediaVPs art, so shadow_smoothness_scale() in
shadows.cpp scales them by the active sun's size relative to Sol,
clamped to what the shadow map can represent (shadow_clamp_smoothness()).
The two methods still disagree about occluder distance -- the raytracer
uses the real one, the shadow map's tabled values stand in for a blocker
distance pinned to the cascade's own extent -- so RT_SHADOW_SUN_SIZE_CALIBRATION
in shadows.sdr is a constant factor that makes a given $SunAngularSize:
read as roughly the same softness either way; it's the number to revisit
if the shadow map ever grows a real blocker search.

Also adds RTAO (raytraced ambient occlusion) sharing the same TLAS as
the shadow rays, with lighting-profile-driven radius/strength, and lab
controls for RT shadow quality (Low/High), the local light cap, RT
shadow samples, RTAO, and an angular-size override for comparing both
shadow methods against the same sun. All are safe to change live: they
only gate which lights are picked as shadow casters or scale existing
uniforms while filling the per-frame light data.

Co-Authored-By: Claude Opus 5 <[email protected]>

* Add shadow contact hardening via PCSS blocker search

The shadow-mapped path's penumbra width was a fixed per-cascade value
(shadow_smoothness_scale()'s ceiling), so every occluder blurred its
shadow by the same amount regardless of how close it actually was to
the receiver -- unlike the raytraced path, which gets that for free
from traceShadowRayCone()'s real occluder distance. Close that gap
with PCSS: search nearby texels for occluders, then scale the filter
radius by how far away they were.

The blocker search (pcssBlockerSearch() in shadows.sdr) needs raw,
uncompared depth reads, which the existing PCF sampler can't do -- it's
bound with a compare mode for the hardware shadow-compare instructions
the filtered sampling itself relies on. So shadow maps now get a second
sampler, shadow_map_raw, bound to the same texture with compare mode
off; on OpenGL that needs a second sampler object and GL 3.3
(shadow_contact_hardening_supported()), Vulkan can always bind it.
Where the raw sampler isn't available, or the new Graphics.ShadowContactHardening
option is off, shadows_start_render() sets Shadow_penumbra_scale[cascade]
to -1 as a sentinel and pcssPenumbraRadius() (shadows.sdr) falls straight
back to the fixed radius, bit-identical to the pre-contact-hardening
behavior and skipping the extra texture reads.

When it is on, shadows_start_render() computes each cascade's penumbra
scale from its own frustum extents and the sun's tanθ; the shader turns
that plus the blocker search's average depth into a per-pixel radius,
clamped to [1 texel, the old fixed ceiling]. A blocker search that finds
nothing doesn't fall back to "no shadow" -- a sparse 8-tap search can
miss a thin occluder, so it instead runs the full filter at the hardest
(1-texel) radius, trading a touch of extra blur for not popping a real
shadow to nothing.

Drops RT_SHADOW_SUN_SIZE_CALIBRATION accordingly: that constant existed
only to fake a blocker-distance-driven penumbra on the shadow-mapped path
by pre-scaling the raytraced cone to match it, and the shadow-mapped path
now has a real blocker search of its own, so the raytraced cone goes back
to its physically correct size.

* Apply review feedback on softer shadows

- Bind the GL raw-depth sampler on unit 11 in the deferred pass and
  unbind it after the pass.
- Enable RTAO only when shadows are on, and remove the RTAO option when
  ray tracing is not supported.
- Use every second Poisson tap in the PCSS blocker search.
- Use TLAS-relative ray origins for RTAO.
- Gate +AngularSize: at version 27.0.
- Measure an .eff sun from its first frame, and skip the measurement
  when shadows are off.
- Guard the GL 3.3 sampler check for GLES builds.
- Restore the lab session overrides when the lab closes.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

* apply review suggestions

* Fix review findings and build the ray-tracing structure once per frame

Build the ray-tracing TLAS one time in each frame, in
vulkan_scene_texture_begin(), where no render pass is open. Remove the
TLAS build from the shadow pass and the second build in
vulkan_deferred_lighting_finish(). Remove gr_build_shadow_tlas.

Move the RTAO inputs and the deferred ray inputs (ray origin, cull mask,
bias) into globalDeferredData. Write them in each frame with
shadows_fill_deferred_global_data(). RTAO now works with shadows off and
in frames with no sun.

Other fixes from the review:
- Add the sampler2DArray precision to the OpenGL ES shader blocks.
- Declare shadowCascadeParams one time, in shadow_cascade_params.sdr.
- Use one ray function for traceShadowRay() and traceShadowRayCone().
- Restore the shadow method and the raytraced light limits when the lab
  closes.
- Limit the sun angular size to 90 degrees in stars.tbl and in missions.
- Make the OpenGL sampler part of the texture unit binding, and name the
  raw shadow map unit SHADOW_MAP_RAW_TEXTURE_UNIT.
- Measure the sun disc when the sun bitmap loads, not in the draw path.
- Use starfield_copy_entry_to_instance() in stars_set_data().
- Share the sun quad radius as SUN_QUAD_RADIUS.
- Correct the sun size tooltips and comments: the size affects both
  shadow methods.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5 <[email protected]>
2026-11-12 21:05:22 +01:00

1585 lines
58 KiB
C++

/*
* Copyright (C) Freespace Open 2013. All rights reserved.
*
* All source code herein is the property of Freespace Open. You may not sell
* or otherwise commercially exploit the source or things you created based on the
* source.
*
*/
#include "graphics/shadows.h"
#include "graphics/2d.h"
#include "graphics/uniforms.h"
#include "asteroid/asteroid.h"
#include "camera/camera.h"
#include "cmdline/cmdline.h"
#include "debris/debris.h"
#include "graphics/matrix.h"
#include "graphics/rtao.h"
#include "lighting/lighting.h"
#include "lighting/lighting_profiles.h"
#include "math/vecmat.h"
#include "mod_table/mod_table.h"
#include "model/model.h"
#include "model/modelrender.h"
#include "object/objectdock.h"
#include "options/Option.h"
#include "prop/prop.h"
#include "ship/ship.h"
#include "ship/shipfx.h"
#include "render/3d.h"
#include "starfield/starfield.h"
#include "starfield/sun_disc.h"
#include "tracing/tracing.h"
#include "util/uniform_structs.h"
#include <algorithm>
extern vec3d check_offsets[8];
matrix4 Shadow_view_matrix_light;
matrix4 Shadow_view_matrix_render;
vec3d Shadow_rt_tlas_origin = vmd_zero_vector;
vec3d shadow_rt_relative(const vec3d& world_pos)
{
vec3d out;
vm_vec_sub(&out, &world_pos, &Shadow_rt_tlas_origin);
return out;
}
SCP_vector<matrix4> Shadow_proj_matrix;
SCP_vector<float> Shadow_cascade_distances;
SCP_vector<float> Shadow_penumbra_scale;
static SCP_vector<light_frustum_info> Shadow_frustums;
ShadowQuality Shadow_quality = ShadowQuality::Disabled;
bool Shadow_quality_uses_mod_option = false;
static void parse_shadow_quality_func()
{
SCP_string mode;
stuff_string(mode, F_NAME);
// Convert to lowercase once
SCP_tolower(mode);
// Use a map to associate strings with their respective actions
static const std::unordered_map<std::string, std::function<void()>> effectActions = {
{"disabled", []() { Shadow_quality = ShadowQuality::Disabled; }},
{"low", []() { Shadow_quality = ShadowQuality::Low; }},
{"medium", []() { Shadow_quality = ShadowQuality::Medium; }},
{"high", []() { Shadow_quality = ShadowQuality::High; }},
{"ultra", []() { Shadow_quality = ShadowQuality::Ultra; }}
};
auto it = effectActions.find(mode);
if (it != effectActions.end()) {
it->second(); // Execute the corresponding action
} else {
error_display(0, "%s is not a valid shadow quality setting", mode.c_str());
}
}
// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton
auto ShadowQualityOption = options::OptionBuilder<ShadowQuality>("Graphics.Shadows",
std::pair<const char*, int>{"Shadow Quality", 1750},
std::pair<const char*, int>{"The quality of the shadows", 1751})
.values({{ShadowQuality::Disabled, {"Disabled", 413}},
{ShadowQuality::Low, {"Low", 1160}},
{ShadowQuality::Medium, {"Medium", 1161}},
{ShadowQuality::High, {"High", 1162}},
{ShadowQuality::Ultra, {"Ultra", 1721}}})
.change_listener([](ShadowQuality val, bool initial) {if (initial) {Shadow_quality = val;}return initial;})
.level(options::ExpertLevel::Advanced)
.category(std::make_pair("Graphics", 1825))
// Return the global, like Graphics.VSync and Graphics.AAMode do, rather than a constant.
// By the time the options manager reads this, Shadow_quality already holds whatever
// -enable_shadows and game_settings.tbl's "$Shadow Quality Default:" put there, so a
// constant here would discard both. A value the player saved still wins over this
// default, and -shadow_quality still wins over that as a registered override.
.default_func([]() { return Shadow_quality; } )
.importance(80)
.parser(parse_shadow_quality_func)
.finish();
ShadowRenderMethod Shadow_render_method = ShadowRenderMethod::ShadowMap;
static void parse_shadow_render_method_func()
{
SCP_string mode;
stuff_string(mode, F_NAME);
SCP_tolower(mode);
if (mode == "shadow map" || mode == "shadow maps") {
Shadow_render_method = ShadowRenderMethod::ShadowMap;
} else if (mode == "raytraced") {
Shadow_render_method = ShadowRenderMethod::Raytraced;
} else {
error_display(0, "%s is not a valid shadow render method setting", mode.c_str());
}
}
// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton
auto ShadowRenderMethodOption = options::OptionBuilder<ShadowRenderMethod>("Graphics.ShadowRenderMethod",
std::pair<const char*, int>{"Shadow Method", -1},
std::pair<const char*, int>{"Whether shadows are rendered via cascaded shadow maps or hardware raytracing", -1})
// Raytraced is only ever offered on hardware that actually supports it.
.enumerator([]() -> SCP_vector<ShadowRenderMethod> {
if (shadows_raytracing_supported()) {
return {ShadowRenderMethod::ShadowMap, ShadowRenderMethod::Raytraced};
}
return {ShadowRenderMethod::ShadowMap};
})
.display(options::MapValueDisplay<ShadowRenderMethod>({
{ShadowRenderMethod::ShadowMap, {"Shadow Maps", -1}},
{ShadowRenderMethod::Raytraced, {"Raytraced", -1}}
}))
// Live-apply: switching this only changes which shader variant/flags get
// requested on the next draw (lazily compiled/cached), no restart needed.
.bind_to(&Shadow_render_method)
.flags({options::OptionFlags::ForceMultiValueSelection})
.level(options::ExpertLevel::Advanced)
.category(std::make_pair("Graphics", 1825))
// Return the global, for the same reason Graphics.Shadows above does: a constant here
// would discard the value parse_shadow_render_method_func() reads from the mod's
// "$Shadow Render Method:" table entry, because bind_to() writes this back to the
// global on the initial load.
.default_func([]() { return Shadow_render_method; })
.importance(79)
.parser(parse_shadow_render_method_func)
.finish();
int Max_rt_shadow_lights = 4;
// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton
auto MaxRtShadowLightsOption = options::OptionBuilder<int>("Graphics.MaxRtShadowLights",
std::pair<const char*, int>{"Max Raytraced Shadow Lights", -1},
std::pair<const char*, int>{"Maximum number of directional lights that cast raytraced shadows", -1})
.enumerator([]() -> SCP_vector<int> {
if (shadows_raytracing_supported()) {
return {1, 2, 3, 4, 5, 6, 7, 8};
}
return {4}; // inert default when RT isn't supported
})
.bind_to(&Max_rt_shadow_lights)
.flags({options::OptionFlags::ForceMultiValueSelection})
.level(options::ExpertLevel::Advanced)
.category(std::make_pair("Graphics", 1825))
.default_func([]() { return 4; })
.importance(78)
.finish();
RTShadowQuality Rt_shadow_quality = RTShadowQuality::Low;
// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton
auto RTShadowQualityOption = options::OptionBuilder<RTShadowQuality>("Graphics.RTShadowQuality",
std::pair<const char*, int>{"Raytraced Shadow Quality", -1},
std::pair<const char*, int>{"Low shadows directional lights only (up to Max Raytraced Shadow Lights); High also shadows point, tube, and cone lights", -1})
.enumerator([]() -> SCP_vector<RTShadowQuality> {
if (shadows_raytracing_supported()) {
return {RTShadowQuality::Low, RTShadowQuality::High};
}
return {RTShadowQuality::Low};
})
.display(options::MapValueDisplay<RTShadowQuality>({
{RTShadowQuality::Low, {"Low", -1}},
{RTShadowQuality::High, {"High", -1}}
}))
.bind_to(&Rt_shadow_quality)
.flags({options::OptionFlags::ForceMultiValueSelection})
.level(options::ExpertLevel::Advanced)
.category(std::make_pair("Graphics", 1825))
.default_func([]() { return RTShadowQuality::Low; })
.importance(77)
.finish();
// 4 is both the option's default and the smallest value its enumerator offers, so start there. The
// in-game-options path already set 4 through the option's default_func; this makes the legacy path,
// which never loads the option, agree instead of leaving an out-of-range 3 in place.
int Max_rt_shadow_local_lights = 4;
// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton
auto MaxRtShadowLocalLightsOption = options::OptionBuilder<int>("Graphics.MaxRtShadowLocalLights",
std::pair<const char*, int>{"Max Raytraced Local Shadow Lights", -1},
std::pair<const char*, int>{"Maximum number of point, tube, and cone lights that cast raytraced shadows (only used at High Raytraced Shadow Quality)", -1})
.enumerator([]() -> SCP_vector<int> {
if (shadows_raytracing_supported()) {
return {4, 8, 16, 32, 64};
}
return {4}; // inert default when RT isn't supported
})
.bind_to(&Max_rt_shadow_local_lights)
.flags({options::OptionFlags::ForceMultiValueSelection})
.level(options::ExpertLevel::Advanced)
.category(std::make_pair("Graphics", 1825))
.default_func([]() { return 4; })
.importance(76)
.finish();
// Defaults confirmed by empirical testing via the LabUi slider: 0.5 clears acne on
// even very close geometry, and distant geometry never needed more than 4.0.
float Rt_shadow_bias_min = 0.5f;
float Rt_shadow_bias_max = 4.0f;
// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton
auto RtShadowBiasMinOption = options::OptionBuilder<float>("Graphics.RtShadowBiasMin",
std::pair<const char*, int>{"Min Raytraced Shadow Bias", -1},
std::pair<const char*, int>{"Raytraced shadow ray origin offset (world units) used for geometry close to the camera, to avoid self-intersection shadow acne", -1})
.category(std::make_pair("Graphics", 1825))
.level(options::ExpertLevel::Advanced)
.default_func([]() { return 0.5f; })
.range(0.0f, 4.0f)
.bind_to(&Rt_shadow_bias_min)
.importance(75)
.finish();
// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton
auto RtShadowBiasMaxOption = options::OptionBuilder<float>("Graphics.RtShadowBiasMax",
std::pair<const char*, int>{"Max Raytraced Shadow Bias", -1},
std::pair<const char*, int>{"Raytraced shadow ray origin offset (world units) used for geometry far from the camera, to avoid self-intersection shadow acne", -1})
.category(std::make_pair("Graphics", 1825))
.level(options::ExpertLevel::Advanced)
.default_func([]() { return 4.0f; })
.range(0.5f, 16.0f)
.bind_to(&Rt_shadow_bias_max)
.importance(74)
.finish();
int Rt_shadow_samples_override = -1;
bool Shadow_contact_hardening_enabled = true;
// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton
auto ShadowContactHardeningOption = options::OptionBuilder<bool>("Graphics.ShadowContactHardening",
std::pair<const char*, int>{"Shadow Contact Hardening", -1},
std::pair<const char*, int>{"Sizes each shadow's penumbra by how close its occluder is, instead of a fixed blur width. Costs an extra shadow-map search per shadowed pixel", -1})
// Only ever offered on hardware that can do the raw-depth read the
// blocker search needs -- see shadow_contact_hardening_supported().
.enumerator([]() -> SCP_vector<bool> {
if (shadow_contact_hardening_supported()) {
return {true, false};
}
return {false}; // inert default when unsupported
})
// Live-apply: shadows_start_render() reads Shadow_contact_hardening_enabled()
// fresh every frame, no restart needed.
.bind_to(&Shadow_contact_hardening_enabled)
.flags({options::OptionFlags::ForceMultiValueSelection})
.level(options::ExpertLevel::Advanced)
.category(std::make_pair("Graphics", 1825))
.default_func([]() { return true; })
.importance(73)
.finish();
bool shadows_raytracing_supported()
{
return gr_is_capable(gr_capability::CAPABILITY_RAYTRACED_SHADOWS);
}
bool shadow_contact_hardening_supported()
{
return gr_is_capable(gr_capability::CAPABILITY_SHADOW_CONTACT_HARDENING);
}
bool shadow_contact_hardening_enabled()
{
return shadow_contact_hardening_supported() && Shadow_contact_hardening_enabled;
}
int shadows_map_resolution()
{
switch (Shadow_quality) {
case ShadowQuality::Low: return 512;
case ShadowQuality::Medium: return 1024;
case ShadowQuality::High: return 2048;
case ShadowQuality::Ultra: return 4096;
default: return 512;
}
}
int shadows_rt_sample_count()
{
if (Rt_shadow_samples_override > 0) {
return Rt_shadow_samples_override;
}
// The same tiers that pick the shadow map's resolution pick the ray count, so one
// setting covers "how much am I willing to spend on shadows" for either method. Low
// stays at a single ray -- that is the hard-shadow, one-ray-per-light floor, matching
// how a 512 map is the cheap floor for the other method rather than a soft-shadow
// setting the user has to find separately.
switch (Shadow_quality) {
case ShadowQuality::Low: return 1;
case ShadowQuality::Medium: return 4;
case ShadowQuality::High: return 8;
case ShadowQuality::Ultra: return 16;
default: return 1;
}
}
// The apparent sun size the $Shadow Smoothness Factor: values are taken to be tuned against.
// Sol is the natural choice: measured across the sun art of retail, the MediaVPs and several
// mods, the calibration in sun_disc.cpp lands median content near Sol, and the shipped
// smoothness defaults (1/300 .. 1/200) work out to a Sol-sized sun's penumbra at a blocker
// distance of roughly 0.7-1.1 cascade widths. So a Sol-sized sun reproduces the look every
// existing mod already tuned for, and everything else scales from there.
static float shadow_smoothness_reference_tangent()
{
return sun_disc_tangent_from_diameter(SUN_ANGULAR_SIZE_SOL);
}
// Returns the softness of the shadow-mapped sun, relative to the reference above. The tabled
// smoothness values are multiplied by this factor.
//
// The shadow map and the raytracer both use the same value: the tangent of the angular radius
// of the sun. This value comes from $SunAngularSize: or from a measurement of the sun bitmap
// (see sun_angular_radius_tangent() in starfield.cpp). When the sun becomes two times larger,
// the raytraced penumbra cone and this filter width also become two times larger. Thus a
// mission author sets one value, and both methods follow it.
//
// When shadow_contact_hardening_supported() is true, this value is the maximum penumbra radius.
// pcssPenumbraRadius() (shadows.sdr) calculates a radius for each pixel from the blocker search,
// and clamps it to this maximum (see Shadow_penumbra_scale). When contact hardening is not
// supported (OpenGL before 3.3), this value is the fixed penumbra radius. In both cases the
// tabled values keep their old meaning, so mods that tuned them see no change.
//
// This function uses Static_light.front(), which is also the light of the cascades in
// shadows_start_render(). Each directional light at this point has a source radius: suns get
// it in stars_draw_sun(), and common_setup_room_lights() gives its lights the radius of a sun.
// Thus a radius of 0 is a source with no size, for example a blank sun bitmap of a mod. Such a
// source must have a hard edge, so the shadow map uses its narrowest filter
// (see shadow_clamp_smoothness()).
static float shadow_smoothness_scale()
{
if (Static_light.empty()) {
return 1.0f;
}
const float reference = shadow_smoothness_reference_tangent();
if (reference <= 0.0f) {
return 1.0f;
}
// For directional lights source_radius is the tangent of the angular radius, not a
// world-space size -- see light_add_directional() in starfield.cpp's stars_draw_sun().
return Static_light.front().source_radius / reference;
}
// Keeps a derived filter width inside what the shadow map can actually represent.
//
// The floor is one texel: a sun small enough to want a sub-texel penumbra can't get one out of
// a shadow map, and dropping below a texel only collapses all 16 Poisson taps into the same
// texel -- paying for the taps and getting an aliased edge for it. (A raytraced shadow *does*
// go properly hard there; this is a limit of the method, not of the parameter.)
//
// The ceiling bounds how far the taps spread. Missions set +AngularSize: freely, and a large
// enough sun would scatter the 16 taps far enough apart to read as separate blobs, while the
// comparison depth stays at the receiver and turns the widened filter into acne.
//
// Note the floor also applies to the tabled values themselves, so a mod that asked for a very
// small $Shadow Smoothness Factor: gets widened to a texel at low shadow-map resolutions. That
// value was already sub-texel there -- i.e. already doing nothing except cost taps -- so this
// trades a hair of extra blur for an antialiased edge.
static float shadow_clamp_smoothness(float smoothness)
{
// shadows_map_resolution() is 512 at its smallest, so the floor always stays below the
// ceiling and std::clamp()'s lo <= hi precondition holds.
const float min_smoothness = 1.0f / static_cast<float>(shadows_map_resolution());
constexpr float max_smoothness = 0.02f;
return std::clamp(smoothness, min_smoothness, max_smoothness);
}
void shadows_remove_unsupported_options()
{
rtao_remove_unsupported_options();
if (!shadow_contact_hardening_supported()) {
// No raw-depth sampler support (pre-3.3 OpenGL): the option is inert, so drop it.
// The bound global keeps its default; shadow_contact_hardening_enabled() folding
// in shadow_contact_hardening_supported() keeps rendering on the fixed-width
// fallback either way.
options::OptionsManager::instance()->removeOption(ShadowContactHardeningOption);
}
if (shadows_raytracing_supported()) {
return;
}
// No raytracing support (e.g. the OpenGL renderer): the render-method selector
// degenerates to a single value and every raytraced-shadow tuning option is inert, so
// drop them all. The bound globals keep their defaults, so shadow-map rendering is
// unaffected. Graphics.Shadows (base shadow quality) is deliberately kept.
auto* mgr = options::OptionsManager::instance();
mgr->removeOption(ShadowRenderMethodOption);
mgr->removeOption(MaxRtShadowLightsOption);
mgr->removeOption(RTShadowQualityOption);
mgr->removeOption(MaxRtShadowLocalLightsOption);
mgr->removeOption(RtShadowBiasMinOption);
mgr->removeOption(RtShadowBiasMaxOption);
}
bool shadows_use_raytracing()
{
return Shadow_render_method == ShadowRenderMethod::Raytraced && shadows_raytracing_supported();
}
bool shadows_rt_scene_needed()
{
return rtao_enabled() || (shadows_use_raytracing() && Shadow_quality != ShadowQuality::Disabled);
}
bool shadows_use_raytraced_local_lights()
{
return shadows_use_raytracing() && Rt_shadow_quality == RTShadowQuality::High;
}
bool shadows_obj_in_frustum(const object *objp, const matrix *light_orient, const vec3d *min, const vec3d *max)
{
vec3d pos, pos_rot;
vm_vec_sub(&pos, &objp->pos, &Eye_position);
vm_vec_rotate(&pos_rot, &pos, light_orient);
if ( (pos_rot.xyz.x - objp->radius) > max->xyz.x
|| (pos_rot.xyz.x + objp->radius) < min->xyz.x
|| (pos_rot.xyz.y - objp->radius) > max->xyz.y
|| (pos_rot.xyz.y + objp->radius) < min->xyz.y
|| (pos_rot.xyz.z - objp->radius) > max->xyz.z ) {
return false;
}
return true;
}
void shadows_construct_light_proj(light_frustum_info *shadow_data)
{
memset(&shadow_data->proj_matrix, 0, sizeof(matrix4));
shadow_data->proj_matrix.a1d[0] = 2.0f / ( shadow_data->max.xyz.x - shadow_data->min.xyz.x );
shadow_data->proj_matrix.a1d[5] = 2.0f / ( shadow_data->max.xyz.y - shadow_data->min.xyz.y );
shadow_data->proj_matrix.a1d[12] = -(shadow_data->max.xyz.x + shadow_data->min.xyz.x) / ( shadow_data->max.xyz.x - shadow_data->min.xyz.x );
shadow_data->proj_matrix.a1d[13] = -(shadow_data->max.xyz.y + shadow_data->min.xyz.y) / ( shadow_data->max.xyz.y - shadow_data->min.xyz.y );
shadow_data->proj_matrix.a1d[15] = 1.0f;
if (gr_screen.mode == GraphicsAPI::Vulkan) {
// Vulkan uses [0, 1] depth range
shadow_data->proj_matrix.a1d[10] = -1.0f / ( shadow_data->max.xyz.z - shadow_data->min.xyz.z );
shadow_data->proj_matrix.a1d[14] = -shadow_data->min.xyz.z / ( shadow_data->max.xyz.z - shadow_data->min.xyz.z );
} else {
// OpenGL uses [-1, 1] depth range
shadow_data->proj_matrix.a1d[10] = -2.0f / ( shadow_data->max.xyz.z - shadow_data->min.xyz.z );
shadow_data->proj_matrix.a1d[14] = -(shadow_data->max.xyz.z + shadow_data->min.xyz.z) / ( shadow_data->max.xyz.z - shadow_data->min.xyz.z );
}
}
void shadows_debug_show_frustum(matrix* orient, vec3d *pos, float fov, float aspect, float z_near, float z_far)
{
// find the widths and heights of the near plane and far plane to determine the points of this frustum
float near_height = tanf(fov * 0.5f) * z_near;
float near_width = near_height * aspect;
float far_height = tanf(fov * 0.5f) * z_far;
float far_width = far_height * aspect;
vec3d up_scale = ZERO_VECTOR;
vec3d right_scale = ZERO_VECTOR;
vec3d forward_scale_near = orient->vec.fvec;
vec3d forward_scale_far = orient->vec.fvec;
vm_vec_scale(&forward_scale_near, z_near);
vm_vec_scale(&forward_scale_far, z_far);
// find the eight points using eye orientation and position
vec3d near_top_left = ZERO_VECTOR;
vec3d near_top_right = ZERO_VECTOR;
vec3d near_bottom_left = ZERO_VECTOR;
vec3d near_bottom_right = ZERO_VECTOR;
// near top left
up_scale = orient->vec.uvec;
vm_vec_scale(&up_scale, -near_height);
right_scale = orient->vec.rvec;
vm_vec_scale(&right_scale, -near_width);
vm_vec_add(&near_top_left, &up_scale, &right_scale);
vm_vec_add2(&near_top_left, &forward_scale_near);
// near top right
up_scale = orient->vec.uvec;
vm_vec_scale(&up_scale, -near_height);
right_scale = orient->vec.rvec;
vm_vec_scale(&right_scale, near_width);
vm_vec_add(&near_top_right, &up_scale, &right_scale);
vm_vec_add2(&near_top_right, &forward_scale_near);
// near bottom left
up_scale = orient->vec.uvec;
vm_vec_scale(&up_scale, near_height);
right_scale = orient->vec.rvec;
vm_vec_scale(&right_scale, -near_width);
vm_vec_add(&near_bottom_left, &up_scale, &right_scale);
vm_vec_add2(&near_bottom_left, &forward_scale_near);
// near bottom right
up_scale = orient->vec.uvec;
vm_vec_scale(&up_scale, near_height);
right_scale = orient->vec.rvec;
vm_vec_scale(&right_scale, near_width);
vm_vec_add(&near_bottom_right, &up_scale, &right_scale);
vm_vec_add2(&near_bottom_right, &forward_scale_near);
vec3d far_top_left = ZERO_VECTOR;
vec3d far_top_right = ZERO_VECTOR;
vec3d far_bottom_left = ZERO_VECTOR;
vec3d far_bottom_right = ZERO_VECTOR;
// far top left
up_scale = orient->vec.uvec;
vm_vec_scale(&up_scale, -far_height);
right_scale = orient->vec.rvec;
vm_vec_scale(&right_scale, -far_width);
vm_vec_add(&far_top_left, &up_scale, &right_scale);
vm_vec_add2(&far_top_left, &forward_scale_far);
// far top right
up_scale = orient->vec.uvec;
vm_vec_scale(&up_scale, -far_height);
right_scale = orient->vec.rvec;
vm_vec_scale(&right_scale, far_width);
vm_vec_add(&far_top_right, &up_scale, &right_scale);
vm_vec_add2(&far_top_right, &forward_scale_far);
// far bottom left
up_scale = orient->vec.uvec;
vm_vec_scale(&up_scale, far_height);
right_scale = orient->vec.rvec;
vm_vec_scale(&right_scale, -far_width);
vm_vec_add(&far_bottom_left, &up_scale, &right_scale);
vm_vec_add2(&far_bottom_left, &forward_scale_far);
// far bottom right
up_scale = orient->vec.uvec;
vm_vec_scale(&up_scale, far_height);
right_scale = orient->vec.rvec;
vm_vec_scale(&right_scale, far_width);
vm_vec_add(&far_bottom_right, &up_scale, &right_scale);
vm_vec_add2(&far_bottom_right, &forward_scale_far);
// translate frustum
vm_vec_add2(&near_bottom_left, pos);
vm_vec_add2(&near_bottom_right, pos);
vm_vec_add2(&near_top_right, pos);
vm_vec_add2(&near_top_left, pos);
vm_vec_add2(&far_top_left, pos);
vm_vec_add2(&far_top_right, pos);
vm_vec_add2(&far_bottom_right, pos);
vm_vec_add2(&far_bottom_left, pos);
gr_set_color(0, 255, 255);
// g3_draw_htl_line(&near_bottom_left, &near_bottom_right);
// g3_draw_htl_line(&near_bottom_right, &near_top_right);
// g3_draw_htl_line(&near_bottom_right, &near_top_left);
// g3_draw_htl_line(&near_top_right, &near_top_left);
// g3_draw_htl_line(&far_top_left, &far_top_right);
// g3_draw_htl_line(&far_top_right, &far_bottom_right);
// g3_draw_htl_line(&far_bottom_right, &far_bottom_left);
// g3_draw_htl_line(&far_bottom_left, &far_top_left);
g3_render_line_3d(true, &near_bottom_left, &near_bottom_right);
g3_render_line_3d(true, &near_bottom_right, &near_top_right);
g3_render_line_3d(true, &near_bottom_right, &near_top_left);
g3_render_line_3d(true, &near_top_right, &near_top_left);
g3_render_line_3d(true, &far_top_left, &far_top_right);
g3_render_line_3d(true, &far_top_right, &far_bottom_right);
g3_render_line_3d(true, &far_bottom_right, &far_bottom_left);
g3_render_line_3d(true, &far_bottom_left, &far_top_left);
}
void shadows_construct_light_frustum(light_frustum_info *shadow_data, matrix *light_matrix, matrix *orient, vec3d * /*pos*/, fov_t fov, float aspect, float z_near, float z_far)
{
// find the widths and heights of the near plane and far plane to determine the points of this frustum
float near_l, near_r, near_u, near_d;
float far_l, far_r, far_u, far_d;
if (std::holds_alternative<float>(fov)) {
near_d = tanf(std::get<float>(fov) * 0.5f) * z_near;
near_u = -near_d;
near_r = near_d * aspect;
near_l = -near_r;
far_d = tanf(std::get<float>(fov) * 0.5f) * z_far;
far_u = -far_d;
far_r = far_d * aspect;
far_l = -far_r;
}
else {
const auto& afov = std::get<asymmetric_fov>(fov);
near_d = tanf(-afov.down) * z_near;
near_u = -tanf(afov.up) * z_near;
near_r = tanf(-afov.left) * z_near;
near_l = -tanf(afov.right) * z_near;
far_d = tanf(-afov.down) * z_far;
far_u = -tanf(afov.up) * z_far;
far_r = tanf(-afov.left) * z_far;
far_l = -tanf(afov.right) * z_far;
}
vec3d up_scale = ZERO_VECTOR;
vec3d right_scale = ZERO_VECTOR;
vec3d forward_scale_near = orient->vec.fvec;
vec3d forward_scale_far = orient->vec.fvec;
vm_vec_scale(&forward_scale_near, z_near);
vm_vec_scale(&forward_scale_far, z_far);
// find the eight points using eye orientation and position
vec3d near_top_left = ZERO_VECTOR;
vec3d near_top_right = ZERO_VECTOR;
vec3d near_bottom_left = ZERO_VECTOR;
vec3d near_bottom_right = ZERO_VECTOR;
// near top left
up_scale = orient->vec.uvec;
vm_vec_scale(&up_scale, near_u);
right_scale = orient->vec.rvec;
vm_vec_scale(&right_scale, near_l);
vm_vec_add(&near_top_left, &up_scale, &right_scale);
vm_vec_add2(&near_top_left, &forward_scale_near);
// near top right
up_scale = orient->vec.uvec;
vm_vec_scale(&up_scale, near_u);
right_scale = orient->vec.rvec;
vm_vec_scale(&right_scale, near_r);
vm_vec_add(&near_top_right, &up_scale, &right_scale);
vm_vec_add2(&near_top_right, &forward_scale_near);
// near bottom left
up_scale = orient->vec.uvec;
vm_vec_scale(&up_scale, near_d);
right_scale = orient->vec.rvec;
vm_vec_scale(&right_scale, near_l);
vm_vec_add(&near_bottom_left, &up_scale, &right_scale);
vm_vec_add2(&near_bottom_left, &forward_scale_near);
// near bottom right
up_scale = orient->vec.uvec;
vm_vec_scale(&up_scale, near_d);
right_scale = orient->vec.rvec;
vm_vec_scale(&right_scale, near_r);
vm_vec_add(&near_bottom_right, &up_scale, &right_scale);
vm_vec_add2(&near_bottom_right, &forward_scale_near);
vec3d far_top_left = ZERO_VECTOR;
vec3d far_top_right = ZERO_VECTOR;
vec3d far_bottom_left = ZERO_VECTOR;
vec3d far_bottom_right = ZERO_VECTOR;
// far top left
up_scale = orient->vec.uvec;
vm_vec_scale(&up_scale, far_u);
right_scale = orient->vec.rvec;
vm_vec_scale(&right_scale, far_l);
vm_vec_add(&far_top_left, &up_scale, &right_scale);
vm_vec_add2(&far_top_left, &forward_scale_far);
// far top right
up_scale = orient->vec.uvec;
vm_vec_scale(&up_scale, far_u);
right_scale = orient->vec.rvec;
vm_vec_scale(&right_scale, far_r);
vm_vec_add(&far_top_right, &up_scale, &right_scale);
vm_vec_add2(&far_top_right, &forward_scale_far);
// far bottom left
up_scale = orient->vec.uvec;
vm_vec_scale(&up_scale, far_d);
right_scale = orient->vec.rvec;
vm_vec_scale(&right_scale, far_l);
vm_vec_add(&far_bottom_left, &up_scale, &right_scale);
vm_vec_add2(&far_bottom_left, &forward_scale_far);
// far bottom right
up_scale = orient->vec.uvec;
vm_vec_scale(&up_scale, far_d);
right_scale = orient->vec.rvec;
vm_vec_scale(&right_scale, far_r);
vm_vec_add(&far_bottom_right, &up_scale, &right_scale);
vm_vec_add2(&far_bottom_right, &forward_scale_far);
vec3d frustum_pts[8];
// bring frustum points into light space
vm_vec_rotate(&frustum_pts[0], &near_bottom_left, light_matrix);
vm_vec_rotate(&frustum_pts[1], &near_bottom_right, light_matrix);
vm_vec_rotate(&frustum_pts[2], &near_top_right, light_matrix);
vm_vec_rotate(&frustum_pts[3], &near_top_left, light_matrix);
vm_vec_rotate(&frustum_pts[4], &far_top_left, light_matrix);
vm_vec_rotate(&frustum_pts[5], &far_top_right, light_matrix);
vm_vec_rotate(&frustum_pts[6], &far_bottom_right, light_matrix);
vm_vec_rotate(&frustum_pts[7], &far_bottom_left, light_matrix);
vec3d min = ZERO_VECTOR;
vec3d max = ZERO_VECTOR;
min = frustum_pts[0];
max = frustum_pts[0];
// find min and max of frustum points
for (int i = 0; i < 8; ++i) {
if ( frustum_pts[i].xyz.x < min.xyz.x ) {
min.xyz.x = frustum_pts[i].xyz.x;
}
if ( frustum_pts[i].xyz.x > max.xyz.x ) {
max.xyz.x = frustum_pts[i].xyz.x;
}
if ( frustum_pts[i].xyz.y < min.xyz.y ) {
min.xyz.y = frustum_pts[i].xyz.y;
}
if ( frustum_pts[i].xyz.y > max.xyz.y ) {
max.xyz.y = frustum_pts[i].xyz.y;
}
if ( frustum_pts[i].xyz.z < min.xyz.z ) {
min.xyz.z = frustum_pts[i].xyz.z;
}
if ( frustum_pts[i].xyz.z > max.xyz.z ) {
max.xyz.z = frustum_pts[i].xyz.z;
}
}
shadow_data->min = min;
shadow_data->max = max;
shadows_construct_light_proj(shadow_data);
}
matrix shadows_start_render(matrix *eye_orient, vec3d *eye_pos, fov_t fov, fov_t cockpit_fov, float aspect, const std::optional<SCP_vector<float>>& cascade_distances_override)
{
if(Static_light.empty())
return vmd_identity_matrix;
auto& lp = Static_light.front();
vec3d light_dir;
matrix light_matrix;
vm_vec_copy_normalize(&light_dir, &lp.vec);
vm_vector_2_matrix_norm(&light_matrix, &light_dir, &eye_orient->vec.uvec, nullptr);
const int num_cascades = Num_shadow_cascades + Num_cockpit_shadow_cascades;
SCP_vector<float> cascade_distances_rebuild;
int max_skip_override = 0;
if (cascade_distances_override.has_value()) {
cascade_distances_rebuild.reserve(num_cascades);
// If we have override cascades, this is the techroom or similar.
// In this case, we must fit the override cascades into the given cascade count allocated for the main game.
// Cockpit cascades are skipped. If there is too few cascades in the override, prepend with zeroes.
// If there's too many cascades in the override, skip the excess.
max_skip_override = std::max(Num_cockpit_shadow_cascades, num_cascades - static_cast<int>(cascade_distances_override->size()));
for (int i = 0; i < max_skip_override; i++) {
cascade_distances_rebuild.emplace_back(0.0f);
}
for (int i = 0; i < num_cascades - max_skip_override; i++) {
cascade_distances_rebuild.emplace_back(cascade_distances_override->at(i));
}
}
const auto& cascade_distances_actual = cascade_distances_override.has_value() ? cascade_distances_rebuild : Shadow_distances;
// Only ever do cockpit cascades if there's no override
bool render_cockpit_cascades = !cascade_distances_override.has_value() && ship_render_player_has_closeup_visuals();
// Computed once, not per-cascade -- see shadow_contact_hardening_enabled().
const bool contact_hardening_active = shadow_contact_hardening_enabled();
for (int i = 0; i < num_cascades; i++) {
float z_near;
if (i == 0 || (!render_cockpit_cascades && i == Num_cockpit_shadow_cascades)) {
z_near = 0.0f;
} else {
z_near = cascade_distances_actual[i - 1] - (cascade_distances_actual[i - 1] - (i >= 2 ? cascade_distances_actual[i - 2] : 0.0f)) * 0.2f;
}
float z_far = cascade_distances_actual[i];
shadows_construct_light_frustum(&Shadow_frustums[i], &light_matrix, eye_orient, nullptr, i < Num_cockpit_shadow_cascades ? cockpit_fov : fov, aspect, z_near, z_far);
Shadow_cascade_distances[i] = cascade_distances_actual[i];
Shadow_proj_matrix[i] = Shadow_frustums[i].proj_matrix;
// PCSS penumbra scale: world-space blocker/receiver depth separation (read back from
// the shadow map by the blocker search) times this equals the UV-space penumbra
// radius directly -- see the derivation next to shadow_smoothness_scale() below.
// Sentinel -1 when unsupported OR the user has the option off tells the shader to
// fall back to the fixed smoothness_factors[cascade] (today's behavior) instead of
// reading a raw depth sampler -- which either doesn't exist on this hardware, or
// does but isn't worth the extra shadow-map search right now.
Shadow_penumbra_scale[i] = contact_hardening_active
? std::max(0.0f, (Shadow_frustums[i].max.xyz.z - Shadow_frustums[i].min.xyz.z) * lp.source_radius /
(Shadow_frustums[i].max.xyz.x - Shadow_frustums[i].min.xyz.x))
: -1.0f;
}
gr_shadow_map_start(&Shadow_view_matrix_light, &light_matrix, eye_pos, true);
if (cascade_distances_override)
shadow_cascade_params_bind(max_skip_override, num_cascades - max_skip_override);
else if (render_cockpit_cascades)
shadow_cascade_params_bind(0, num_cascades);
else
shadow_cascade_params_bind(Num_cockpit_shadow_cascades, Num_shadow_cascades);
return light_matrix;
}
void shadows_end_render()
{
gr_shadow_map_end();
}
static bool shadow_obj_clip_plane(const object* objp, shadow_render_list::clip_plane_info* clip)
{
if (objp->type != OBJ_SHIP)
return false;
ship* shipp = &Ships[objp->instance];
if (shipp->is_arriving(ship::warpstage::BOTH, true)) {
model_render_params dummy;
if (shipp->warpin_effect->warpShipClip(&dummy)) {
clip->normal = dummy.get_clip_plane_normal();
clip->position = dummy.get_clip_plane_pos();
return true;
}
}
if (shipp->flags[Ship::Ship_Flags::Depart_warp]) {
model_render_params dummy;
if (shipp->warpout_effect->warpShipClip(&dummy)) {
clip->normal = dummy.get_clip_plane_normal();
clip->position = dummy.get_clip_plane_pos();
return true;
}
}
dock_function_info dfi;
dock_evaluate_all_docked_objects(const_cast<object*>(objp), &dfi,
[](object* docked, dock_function_info* info) {
if (docked->type != OBJ_SHIP) return;
auto* dship = &Ships[docked->instance];
if (dship->is_arriving(ship::warpstage::BOTH, true) || dship->flags[Ship::Ship_Flags::Depart_warp]) {
info->maintained_variables.bool_value = true;
info->maintained_variables.objp_value = docked;
}
});
if (dfi.maintained_variables.bool_value) {
auto* dship = &Ships[dfi.maintained_variables.objp_value->instance];
auto* warp_effect = (dship->is_arriving(ship::warpstage::BOTH, true)
? dship->warpin_effect : dship->warpout_effect).get();
model_render_params dummy;
if (warp_effect->warpShipClip(&dummy)) {
clip->normal = dummy.get_clip_plane_normal();
clip->position = dummy.get_clip_plane_pos();
return true;
}
}
return false;
}
static void render_viewer_shadow(object* objp, const matrix* light_matrix,
const vec3d* cam_offset, const matrix* rot_offset)
{
if (objp == nullptr || objp->type != OBJ_SHIP || objp->instance < 0)
return;
ship* shipp = &Ships[objp->instance];
ship_info* sip = &Ship_info[shipp->ship_info_index];
vec3d eye_pos_local;
matrix eye_orient;
object_get_eye(&eye_pos_local, &eye_orient, objp, true, true, false);
if (cam_offset != nullptr) {
vec3d offset_local;
vm_vec_unrotate(&offset_local, cam_offset, &eye_orient);
(void)offset_local;
}
if (rot_offset != nullptr) {
eye_orient = *rot_offset * eye_orient;
}
vec3d eye_offset;
vm_vec_copy_scale(&eye_offset, &eye_pos_local, -1.0f);
if (!Disable_cockpit_sway)
eye_offset += sip->cockpit_sway_val * objp->phys_info.acceleration;
vec3d view_pos_local;
vm_vec_rotate(&view_pos_local, &eye_pos_local, &objp->orient);
if (Show_ship_casts_shadow) {
matrix4 dummy_view;
bool casts_shadow_on_cockpit = ship_render_player_ship_casts_shadow_on_cockpit();
gr_shadow_map_start(&dummy_view, light_matrix, &vmd_zero_vector, false);
if (casts_shadow_on_cockpit)
shadow_cascade_params_bind(0, Num_cockpit_shadow_cascades + Num_shadow_cascades);
else
shadow_cascade_params_bind(Num_cockpit_shadow_cascades, Num_shadow_cascades);
model_clear_instance(sip->model_num);
polymodel_instance* pmi = nullptr;
if (shipp->model_instance_num >= 0) {
pmi = model_get_instance(shipp->model_instance_num);
}
auto pm = model_get(sip->model_num);
shadow_render_list viewer_list;
shadow_render_list::add_model_draws(&viewer_list, pm, pmi, OBJ_INDEX(objp),
&eye_offset, &objp->orient, nullptr, 0, &view_pos_local);
viewer_list.init_render(false);
viewer_list.render_all();
}
if (shadows_cockpit_casts_shadow(sip)) {
matrix4 dummy_view;
gr_shadow_map_start(&dummy_view, light_matrix, &vmd_zero_vector, false);
shadow_cascade_params_bind(0, Num_cockpit_shadow_cascades);
vec3d cockpit_offset = ship_cockpit_render_offset(sip, objp);
model_clear_instance(sip->cockpit_model_num);
polymodel_instance* cockpit_pmi = nullptr;
if (shipp->cockpit_model_instance >= 0)
cockpit_pmi = model_get_instance(shipp->cockpit_model_instance);
auto cockpit_pm = model_get(sip->cockpit_model_num);
vec3d cockpit_view_local = view_pos_local;
vm_vec_sub2(&cockpit_view_local, &sip->cockpit_offset);
shadow_render_list cockpit_list;
shadow_render_list::add_model_draws(&cockpit_list, cockpit_pm, cockpit_pmi, OBJ_INDEX(objp),
&cockpit_offset, &objp->orient, nullptr, 0, &cockpit_view_local);
cockpit_list.init_render(false);
cockpit_list.render_all();
}
}
void shadows_render_all(fov_t fov, matrix *eye_orient, vec3d *eye_pos,
const vec3d* cam_offset, const matrix* rot_offset, const fov_t* fov_override)
{
if (gr_screen.mode == GraphicsAPI::Stub) {
return;
}
GR_DEBUG_SCOPE("Render shadows");
TRACE_SCOPE(tracing::BuildShadowMap);
// The deferred pass reads this matrix in each frame, also when no shadow map renders (for
// example, RTAO in a mission with no sun). Thus write it before the early returns below.
Shadow_view_matrix_render = gr_view_matrix;
if ( Static_light.empty() ) {
return;
}
if (Shadow_quality == ShadowQuality::Disabled) {
return;
}
//shadows_debug_show_frustum(&Player_obj->orient, &Player_obj->pos, fov, gr_screen.clip_aspect, Min_draw_distance, 3000.0f);
gr_end_proj_matrix();
gr_end_view_matrix();
fov_t cockpit_fov;
if (fov_override)
cockpit_fov = *fov_override;
else if (Sexp_fov > 0.0f)
cockpit_fov = Sexp_fov;
else
cockpit_fov = COCKPIT_ZOOM_DEFAULT;
cockpit_fov = cockpit_fov * PROJ_FOV_FACTOR;
matrix light_matrix = shadows_start_render(eye_orient, eye_pos, fov, cockpit_fov, gr_screen.clip_aspect);
shadow_render_list shadow_list;
object *objp = Objects;
for ( int i = 0; i <= Highest_object_index; i++, objp++ ) {
if ( objp->flags[Object::Object_Flags::Should_be_dead] )
continue;
bool cull = true;
for (const auto& Shadow_frustum : Shadow_frustums) {
if ( shadows_obj_in_frustum(objp, &light_matrix, &Shadow_frustum.min, &Shadow_frustum.max) ) {
cull = false;
break;
}
}
if ( cull ) continue;
switch (objp->type) {
case OBJ_SHIP: {
if (objp == Viewer_obj) {
continue;
}
ship* shipp = &Ships[objp->instance];
if (shipp->large_ship_blowup_index >= 0) {
shipfx_shadow_render_blowup(&shadow_list, shipp);
continue;
}
model_clear_instance(Ship_info[shipp->ship_info_index].model_num);
shadow_render_list::clip_plane_info clip;
bool has_clip = shadow_obj_clip_plane(objp, &clip);
auto pm = model_get(Ship_info[shipp->ship_info_index].model_num);
polymodel_instance* pmi = nullptr;
if (shipp->model_instance_num >= 0) {
pmi = model_get_instance(shipp->model_instance_num);
}
vec3d eye_rel, view_pos_local;
vm_vec_sub(&eye_rel, eye_pos, &objp->pos);
vm_vec_rotate(&view_pos_local, &eye_rel, &objp->orient);
shadow_render_list::add_model_draws(&shadow_list, pm, pmi, OBJ_INDEX(objp), &objp->pos, &objp->orient, has_clip ? &clip : nullptr, -1, &view_pos_local);
break;
}
case OBJ_RAW_POF:
case OBJ_PROP: {
int model_num = object_get_model_num(objp);
if (model_num < 0)
break;
auto pm = model_get(model_num);
model_clear_instance(model_num);
polymodel_instance* pmi = nullptr;
int instance_num = object_get_model_instance_num(objp);
if (instance_num >= 0) {
pmi = model_get_instance(instance_num);
}
vec3d eye_rel, view_pos_local;
vm_vec_sub(&eye_rel, eye_pos, &objp->pos);
vm_vec_rotate(&view_pos_local, &eye_rel, &objp->orient);
shadow_render_list::add_model_draws(&shadow_list, pm, pmi, OBJ_INDEX(objp), &objp->pos, &objp->orient, nullptr, -1, &view_pos_local);
break;
}
case OBJ_ASTEROID: {
int num = objp->instance;
auto* ast = &Asteroids[num];
int model_num = Asteroid_info[ast->asteroid_type].subtypes[ast->asteroid_subtype].model_number;
model_clear_instance(model_num);
auto pm = model_get(model_num);
vec3d eye_rel, view_pos_local;
vm_vec_sub(&eye_rel, eye_pos, &objp->pos);
vm_vec_rotate(&view_pos_local, &eye_rel, &objp->orient);
shadow_render_list::add_model_draws(&shadow_list, pm, nullptr, OBJ_INDEX(objp), &objp->pos, &objp->orient, nullptr, -1, &view_pos_local);
break;
}
case OBJ_DEBRIS: {
debris* db = &Debris[objp->instance];
if ( !(db->flags[Debris_Flags::Used]) ) continue;
auto pm = model_get(db->model_num);
object* debris_obj = &Objects[db->objnum];
polymodel_instance* pmi = nullptr;
if (db->model_instance_num >= 0) {
pmi = model_get_instance(db->model_instance_num);
}
vec3d eye_rel, view_pos_local;
vm_vec_sub(&eye_rel, eye_pos, &debris_obj->pos);
vm_vec_rotate(&view_pos_local, &eye_rel, &debris_obj->orient);
shadow_render_list::add_model_draws(&shadow_list, pm, pmi, db->objnum, &debris_obj->pos, &debris_obj->orient, nullptr, -1, &view_pos_local);
break;
}
default:
break;
}
}
shadow_list.init_render(false);
shadow_list.render_all();
render_viewer_shadow(Viewer_obj, &light_matrix, cam_offset, rot_offset);
shadows_end_render();
gr_zbias(0);
gr_zbuffer_set(ZBUFFER_TYPE_READ);
gr_set_cull(0);
gr_clear_states();
gr_set_proj_matrix(Proj_fov, gr_screen.clip_aspect, Min_draw_distance, Max_draw_distance);
gr_set_view_matrix(&Eye_position, &Eye_matrix);
}
static bool shadow_override_backup = false;
static bool last_override = false;
bool shadow_maybe_start_frame(const bool& override) {
last_override = override;
if (last_override) {
shadow_override_backup = Shadow_override;
Shadow_override = true;
return false;
}
return Shadow_quality != ShadowQuality::Disabled;
}
void shadow_end_frame() {
if (last_override) {
Shadow_override = shadow_override_backup;
last_override = false;
}
}
static gr_buffer_handle Shadow_cascade_params_buffer;
static size_t Shadow_cascade_params_buffer_size = 0;
// Number of padded per-cascade float arrays shadow_cascade_params_bind() writes after the
// proj matrices: cascade distances, smoothness factors, PCSS penumbra scale.
static constexpr size_t Num_cascade_float_arrays = 3;
static std::pair<size_t, size_t> compute_cascade_params_size(int num_cascades) {
size_t padding_required = num_cascades % 4;
if (padding_required != 0)
padding_required = 4 - padding_required;
return {sizeof(graphics::shadow_cascade_static_data)
+ sizeof(matrix4) * num_cascades
+ Num_cascade_float_arrays * sizeof(float) * (num_cascades + padding_required),
padding_required};
}
// Writes num_cascades floats (value_at(0)..value_at(num_cascades - 1)) into buffer at offset,
// advancing offset past both the values and the std140 padding that keeps the next field
// vec4-aligned. Shared by shadow_cascade_params_bind()'s three per-cascade float arrays so the
// offset/padding bookkeeping can't drift between them.
template <typename F>
static void write_padded_floats(SCP_vector<uint8_t>& buffer, size_t& offset, int num_cascades, size_t padding, F&& value_at) {
for (int i = 0; i < num_cascades; i++) {
*reinterpret_cast<float*>(buffer.data() + offset) = value_at(i);
offset += sizeof(float);
}
offset += sizeof(float) * padding;
}
void shadow_cascade_params_init() {
const int num_cascades = Num_shadow_cascades + Num_cockpit_shadow_cascades;
Shadow_cascade_params_buffer_size = compute_cascade_params_size(num_cascades).first;
Shadow_cascade_params_buffer = gr_create_buffer(BufferType::Uniform, BufferUsageHint::Dynamic);
SCP_vector<uint8_t> zero_data(Shadow_cascade_params_buffer_size, 0);
gr_update_buffer_data(Shadow_cascade_params_buffer, Shadow_cascade_params_buffer_size, zero_data.data());
Shadow_frustums.resize(num_cascades);
Shadow_cascade_distances.resize(num_cascades);
Shadow_proj_matrix.resize(num_cascades);
Shadow_penumbra_scale.resize(num_cascades);
}
void shadow_cascade_params_shutdown() {
if (Shadow_cascade_params_buffer.isValid()) {
gr_delete_buffer(Shadow_cascade_params_buffer);
Shadow_cascade_params_buffer = gr_buffer_handle();
}
}
int Shadow_cascade_count = 0;
void shadow_cascade_params_bind(int cascade_offset, int cascade_count, const shadow_ray_params& ray) {
if (!Shadow_cascade_params_buffer.isValid()) {
return;
}
const int num_cascades = Num_shadow_cascades + Num_cockpit_shadow_cascades;
const auto [required_size, padding] = compute_cascade_params_size(num_cascades);
Assertion(required_size <= Shadow_cascade_params_buffer_size, "The shadow cascade parameter buffer grew in size!");
Assertion(Shadow_proj_matrix.size() == static_cast<size_t>(num_cascades) && Shadow_cascade_distances.size() == static_cast<size_t>(num_cascades) && Shadow_smoothness_factor.size() == static_cast<size_t>(num_cascades) && Shadow_penumbra_scale.size() == static_cast<size_t>(num_cascades), "Shadow cascade data buffers are of incorrect size! (Expected %d, got %d, %d, %d, %d)", num_cascades, static_cast<int>(Shadow_proj_matrix.size()), static_cast<int>(Shadow_cascade_distances.size()), static_cast<int>(Shadow_smoothness_factor.size()), static_cast<int>(Shadow_penumbra_scale.size()));
Assertion(cascade_count + cascade_offset <= num_cascades, "Requested drawing out-of-range cascades!");
SCP_vector<uint8_t> buffer(required_size, 0);
size_t offset = 0;
auto& static_data = *reinterpret_cast<graphics::shadow_cascade_static_data*>(buffer.data());
static_data.cascade_offset = cascade_offset;
static_data.cascade_count = cascade_count;
static_data.rtShadowBiasMin = Rt_shadow_bias_min;
static_data.rtShadowBiasMax = Rt_shadow_bias_max;
static_data.rtShadowSampleCount = shadows_rt_sample_count();
static_data.shadow_mv_matrix = Shadow_view_matrix_light;
static_data.shadow_ray_cull_mask = ray.cull_mask;
static_data.shadow_ray_view_origin = ray.view_origin;
Shadow_cascade_count = cascade_count;
offset += sizeof(graphics::shadow_cascade_static_data);
for (int i = 0; i < num_cascades; i++) {
auto& proj_matrix = *reinterpret_cast<matrix4*>(buffer.data() + offset);
proj_matrix = Shadow_proj_matrix[i];
offset += sizeof(matrix4);
}
write_padded_floats(buffer, offset, num_cascades, padding,
[](int i) { return Shadow_cascade_distances[i]; });
// Scaled by the sun's apparent size so shadow-mapped and raytraced shadows soften
// together -- see shadow_smoothness_scale().
const float smoothness_scale = shadow_smoothness_scale();
write_padded_floats(buffer, offset, num_cascades, padding,
[smoothness_scale](int i) { return shadow_clamp_smoothness(Shadow_smoothness_factor[i] * smoothness_scale); });
// PCSS penumbra scale, appended last so every field above keeps its existing byte
// offset -- see Shadow_penumbra_scale's derivation comment in shadows.h.
write_padded_floats(buffer, offset, num_cascades, padding,
[](int i) { return Shadow_penumbra_scale[i]; });
// Full replacement, not _offset: this runs several times per frame with different content,
// and Vulkan's streaming buffer only bump-allocates fresh memory for a full update.
gr_update_buffer_data(Shadow_cascade_params_buffer, required_size, buffer.data());
gr_bind_uniform_buffer(uniform_block_type::ShadowCascadeParams, 0, required_size, Shadow_cascade_params_buffer);
}
void shadow_cascade_params_bind(int cascade_offset, int cascade_count) {
shadow_cascade_params_bind(cascade_offset, cascade_count, shadow_ray_params{shadow_rt_relative(Eye_position)});
}
shadow_ray_params shadow_ray_params_cockpit(const object* viewer) {
shadow_ray_params ray{shadow_rt_relative(viewer->pos)};
vm_vec_add2(&ray.view_origin, &leaning_position);
return ray;
}
// The ray origin and cull mask of the current deferred lighting pass: the cockpit pass or the
// main scene pass.
static shadow_ray_params shadow_ray_params_deferred() {
if (Lighting_mode == lighting_mode::COCKPIT && Viewer_obj != nullptr) {
shadow_ray_params ray = shadow_ray_params_cockpit(Viewer_obj);
if (ship_render_player_ship_casts_shadow_on_cockpit()) {
ray.cull_mask = TLAS_MASK_ALL;
}
return ray;
}
return shadow_ray_params{shadow_rt_relative(Eye_position)};
}
void shadow_cascade_params_bind_deferred() {
const bool cockpit = Lighting_mode == lighting_mode::COCKPIT;
const int offset = cockpit ? 0 : Num_cockpit_shadow_cascades;
const int count = cockpit ? Num_cockpit_shadow_cascades : Num_shadow_cascades;
shadow_cascade_params_bind(offset, count, shadow_ray_params_deferred());
}
void shadows_fill_deferred_global_data(graphics::deferred_global_data& data) {
vm_inverse_matrix4(&data.inv_view_matrix, &Shadow_view_matrix_render);
const shadow_ray_params ray = shadow_ray_params_deferred();
data.rayViewOrigin = ray.view_origin;
data.rayCullMask = ray.cull_mask;
data.rayBiasMin = Rt_shadow_bias_min;
data.rayBiasMax = Rt_shadow_bias_max;
data.rtaoSampleCount = Rtao_samples;
data.rtaoRadius = lighting_profiles::current_rtao_radius();
data.rtaoStrength = lighting_profiles::current_rtao_strength();
}
bool shadows_cockpit_casts_shadow(const ship_info* sip) {
return ship_render_player_cockpit(sip) && !Shadow_disable_overrides.disable_cockpit;
}
shadow_render_list::shadow_render_list() {
reset();
}
void shadow_render_list::add_draw(const indexed_vertex_source* vert_src,
vertex_buffer* buffer,
size_t texi,
const matrix4& model_matrix,
const vec3d& scale,
const clip_plane_info* clip)
{
shadow_batch_entry entry;
entry.model_matrix = model_matrix;
entry.scale = scale;
entry.transform_buffer_offset = _batchBuffer.get_buffer_offset();
entry.flags = 0;
entry.vert_src = vert_src;
entry.buffer = buffer;
entry.texi = texi;
entry.has_clip_plane = (clip != nullptr);
if (clip != nullptr) {
entry.clip_equation.xyzw.x = clip->normal.xyz.x;
entry.clip_equation.xyzw.y = clip->normal.xyz.y;
entry.clip_equation.xyzw.z = clip->normal.xyz.z;
entry.clip_equation.xyzw.w = -vm_vec_dot(&clip->normal, &clip->position);
} else {
entry.clip_equation.xyzw.x = 0.0f;
entry.clip_equation.xyzw.y = 0.0f;
entry.clip_equation.xyzw.z = 0.0f;
entry.clip_equation.xyzw.w = 0.0f;
}
push_element(std::move(entry));
}
bool shadow_render_list::sort_draw_pair(int a, int b) const
{
auto* draw_call_a = &_elements[a];
auto* draw_call_b = &_elements[b];
if (draw_call_a->flags != draw_call_b->flags) {
return draw_call_a->flags < draw_call_b->flags;
}
return a < b;
}
void shadow_render_list::build_uniform_buffer()
{
GR_DEBUG_SCOPE("Build shadow uniform buffer");
_dataBuffer = gr_get_uniform_buffer(uniform_block_type::ShadowMapData, _keys.size());
for (auto render_index : _keys) {
auto& queued_draw = _elements[render_index];
_lights.resetLightState();
auto element = _dataBuffer.aligner().addTypedElement<graphics::shadow_uniform_data>();
graphics::uniforms::convert_shadow_material(element,
queued_draw.model_matrix,
queued_draw.scale,
queued_draw.transform_buffer_offset);
element->clip_equation = queued_draw.clip_equation;
element->use_clip_plane = queued_draw.has_clip_plane ? 1 : 0;
queued_draw.uniform_buffer_offset = _dataBuffer.getCurrentAlignerOffset();
}
_dataBuffer.submitData();
}
void shadow_render_list::render_buffer(const shadow_batch_entry& entry)
{
GR_DEBUG_SCOPE("Render shadow buffer");
auto* datap = &entry.buffer->tex_buf[entry.texi];
if (datap->n_verts == 0) {
return;
}
gr_bind_uniform_buffer(uniform_block_type::ShadowMapData, entry.uniform_buffer_offset,
sizeof(graphics::shadow_uniform_data), _dataBuffer.bufferHandle());
gr_render_shadow_draw(_dataBuffer.bufferHandle(),
entry.uniform_buffer_offset,
sizeof(graphics::shadow_uniform_data),
entry.buffer,
const_cast<indexed_vertex_source*>(entry.vert_src),
entry.texi);
}
void shadow_render_list::add_model_draws(shadow_render_list* list,
polymodel* pm,
polymodel_instance* pmi,
int obj_num,
const vec3d* pos, const matrix* orient,
const clip_plane_info* clip,
int detail_level_lock,
const vec3d* view_pos)
{
int detail_level;
if (detail_level_lock >= 0) {
detail_level = detail_level_lock;
} else {
float depth = model_render_determine_depth(obj_num, pm->id, orient, pos, -1);
detail_level = model_render_determine_detail(depth, pm->id, -1);
}
int detail_root = pm->detail[detail_level];
bool render_root_geometry = true;
if (view_pos != nullptr) {
if (!model_render_check_detail_box(view_pos, pm, detail_root, 0)) {
render_root_geometry = false;
}
}
list->clear_transforms();
list->push_transform(pos, orient);
//TODO This might be incorrect for some models
const vec3d scale_identity = SCALE_IDENTITY_VECTOR;
matrix4 identity_4;
vm_matrix4_set_identity(&identity_4);
{
list->start_model_batch(pm->n_models);
auto& detail_buffer = pm->detail_buffers[detail_level];
for (size_t j = 0; j < detail_buffer.tex_buf.size(); j++) {
if (detail_buffer.tex_buf[j].n_verts == 0) {
continue;
}
int tmap_num = detail_buffer.tex_buf[j].texture;
if (pm->maps[tmap_num].is_transparent) {
continue;
}
int base_tex = pm->maps[tmap_num].textures[TM_BASE_TYPE].GetTexture();
bool skip = false;
if (pmi != nullptr && pmi->texture_replace != nullptr) {
int replace = (*pmi->texture_replace)[tmap_num * TM_NUM_TYPES + TM_BASE_TYPE];
if (replace == REPLACE_WITH_INVISIBLE) {
skip = true;
} else if (replace < 0 && base_tex < 0) {
skip = true;
} else if (replace >= 0) {
base_tex = 0;
}
} else if (base_tex < 0) {
skip = true;
}
if (skip) {
continue;
}
list->add_draw(&pm->vert_source, &detail_buffer, j, identity_4, scale_identity, clip);
}
}
int i = pm->submodel[detail_root].first_child;
while (i >= 0) {
if (!pm->submodel[i].flags[Model::Submodel_flags::Is_thruster]) {
render_submodel_children(list, pm, pmi, i, clip, view_pos);
}
i = pm->submodel[i].next_sibling;
}
if (render_root_geometry)
list->add_submodel_to_batch(detail_root);
list->pop_transform();
}
void shadow_render_list::render_submodel_children(shadow_render_list* list,
polymodel* pm,
polymodel_instance* pmi,
int mn,
const clip_plane_info* clip,
const vec3d* view_pos)
{
bsp_info* sm = &pm->submodel[mn];
submodel_instance* smi = nullptr;
if (pmi != nullptr) {
smi = &pmi->submodel[mn];
if (smi->blown_off) {
return;
}
}
if (view_pos != nullptr) {
if (!model_render_check_detail_box(view_pos, pm, mn, 0)) {
return;
}
}
matrix submodel_orient = vmd_identity_matrix;
vec3d submodel_offset = sm->offset;
if (smi != nullptr) {
submodel_orient = smi->canonical_orient;
vm_vec_add2(&submodel_offset, &smi->canonical_offset);
}
list->push_transform(&submodel_offset, &submodel_orient);
list->add_submodel_to_batch(mn);
// Recurse into children
int i = sm->first_child;
while (i >= 0) {
if (!pm->submodel[i].flags[Model::Submodel_flags::Is_thruster]) {
render_submodel_children(list, pm, pmi, i, clip, view_pos);
}
i = pm->submodel[i].next_sibling;
}
list->pop_transform();
}