mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-10 04:31:34 -10:00
* Add command-line option overrides to options system - Extend `OptionsManager` to support storing and displaying reasons for overrides. - Update related code paths to prioritize command-line overrides, ensure correct option resolution, and display override details in the UI. - Add unit tests for override behavior and round-trip validation. - Remove unused legacy functions (`removeVSyncOption`, `removeWindowModeOption`) in favor of the new system. * Add unit tests for `Graphics.Anisotropy` option validation with and without OpenGL * Add support for associating command-line option names to `OptionsManager` overrides - Update HDR, shadow rendering, and graphics API option overrides to include their respective command-line flag names. - Refactor `Graphics.RenderAPI` to use the `GraphicsAPI` enum instead of raw integers for better type safety and clarity. - Ensure Vulkan support logic and fallback behavior function correctly with the updated options system. - Update unit tests to validate override functionality and enum-based API configuration. * Remove unused `removeVSyncOption` function * Apply the PR review feedback for the command-line option overrides Delete the unit-test run artifacts that this branch committed by mistake, and add .gitignore rules for the files that the unittests binary writes. Give an override priority over an in-session edit again. The options UI now shows a disabled control for an overridden option, and setConfigValue() refuses an edit for an overridden key. Before this change, a save wrote the edit to the config file and applied it to the bound global, but getValue() kept returning the override. The game and the options menu then showed different values for the rest of the session. Add a setOverride() overload that takes a JSON value. Every command-line flag now builds its override with json_integer, json_real, json_true, json_false or json_pack, in place of a hand-formatted string. Make Graphics.CockpitFOV and Graphics.CockpitFOVToggle independent of the order in which the options manager loads them, and remove the forced override on the player's toggle preference. The toggle listener also replaces an identical bind_to() call that made the listener dead code. Add 16 to the values of Graphics.MSAASamples. The -msaa flag accepts 16, so an override with that value made printValues() report an invalid value. Keep the render API that the caller gives to gr_init(). Only a Default value now comes from the Graphics.RenderAPI option, so FRED and the unit tests keep the backend that they ask for. Make the default_func of Graphics.Shadows and Graphics.ShadowRenderMethod return their global. A constant discarded the value that -enable_shadows and the mod table set. Set Max_rt_shadow_local_lights to 4, because 3 is not one of the values that the option offers. Also: resolve -lang one time, share the render API name with version.cpp, correct the stale comments, and add unit tests with a fixture that clears the overrides after each test. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UGh4ZM2u4oUGSbrP122Uyr * Fix the order-dependent crash in the MSAA option test The test `MsaaOverrideAcceptsEverySampleCountTheFlagAllows` calls `getValidValues()`. `Option<T>` makes a display string for each value, and the display function of `Graphics.MSAASamples` calls `XSTR()`. `XSTR()` runs `Int3()` if `lcl_xstr_init()` did not run before. Only `FSTestFixture` runs it, and `Xstr_inited` stays set for the remainder of the process. Thus the shuffled test order decided if the test passed or if it stopped the test binary with SIGILL. Move that one test to a new fixture that derives from `FSTestFixture`. The fixture initializes localization, and it also clears the overrides after each test. Refresh the comment that told that the test binary never runs `lcl_init()`. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UGh4ZM2u4oUGSbrP122Uyr --------- Co-authored-by: Claude Opus 5 <[email protected]>
1334 lines
47 KiB
C++
1334 lines
47 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 "lighting/lighting.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 "tracing/tracing.h"
|
|
#include "util/uniform_structs.h"
|
|
|
|
extern vec3d check_offsets[8];
|
|
|
|
matrix4 Shadow_view_matrix_light;
|
|
matrix4 Shadow_view_matrix_render;
|
|
SCP_vector<matrix4> Shadow_proj_matrix;
|
|
SCP_vector<float> Shadow_cascade_distances;
|
|
|
|
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();
|
|
|
|
bool shadows_raytracing_supported()
|
|
{
|
|
return gr_is_capable(gr_capability::CAPABILITY_RAYTRACED_SHADOWS);
|
|
}
|
|
|
|
void shadows_remove_unsupported_options()
|
|
{
|
|
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_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();
|
|
|
|
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;
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
const bool renderCockpitModel = (Viewer_mode != VM_TOPDOWN) && sip->cockpit_model_num >= 0 && !Disable_cockpits;
|
|
|
|
if (renderCockpitModel && !Shadow_disable_overrides.disable_cockpit) {
|
|
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 = sip->cockpit_offset;
|
|
vm_vec_unrotate(&cockpit_offset, &cockpit_offset, &objp->orient);
|
|
if (!Disable_cockpit_sway)
|
|
cockpit_offset += sip->cockpit_sway_val * objp->phys_info.acceleration;
|
|
|
|
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);
|
|
|
|
if ( Static_light.empty() ) {
|
|
return;
|
|
}
|
|
|
|
if (Shadow_quality == ShadowQuality::Disabled) {
|
|
return;
|
|
}
|
|
|
|
// Rebuilds the raytraced-shadow TLAS alongside the cascaded shadow map below
|
|
// -- Vulkan-only, no-op everywhere else. Which one shading actually samples
|
|
// is decided per shadows_use_raytracing(). See VulkanRaytracingManager::buildTlas().
|
|
gr_build_shadow_tlas();
|
|
|
|
//shadows_debug_show_frustum(&Player_obj->orient, &Player_obj->pos, fov, gr_screen.clip_aspect, Min_draw_distance, 3000.0f);
|
|
|
|
Shadow_view_matrix_render = gr_view_matrix;
|
|
|
|
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;
|
|
|
|
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
|
|
+ sizeof(float) * (num_cascades + padding_required)
|
|
+ sizeof(float) * (num_cascades + padding_required),
|
|
padding_required};
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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) {
|
|
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 cascade data buffers are of incorrect size! (Expected %d, got %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()));
|
|
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.shadow_mv_matrix = Shadow_view_matrix_light;
|
|
|
|
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);
|
|
}
|
|
|
|
for (int i = 0; i < num_cascades; i++) {
|
|
auto& cascade_distance = *reinterpret_cast<float*>(buffer.data() + offset);
|
|
cascade_distance = Shadow_cascade_distances[i];
|
|
offset += sizeof(float);
|
|
}
|
|
offset += sizeof(float) * padding;
|
|
|
|
for (int i = 0; i < num_cascades; i++) {
|
|
auto& smoothness_factor = *reinterpret_cast<float*>(buffer.data() + offset);
|
|
smoothness_factor = Shadow_smoothness_factor[i];
|
|
offset += sizeof(float);
|
|
}
|
|
offset += sizeof(float) * padding;
|
|
|
|
gr_update_buffer_data_offset(Shadow_cascade_params_buffer, 0, required_size, buffer.data());
|
|
gr_bind_uniform_buffer(uniform_block_type::ShadowCascadeParams, 0, required_size, Shadow_cascade_params_buffer);
|
|
}
|
|
|
|
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();
|
|
} |