Compare commits

..
Author SHA1 Message Date
dustinspace217andDustinspace217 059a8715d8 P2: Fix isWeaponAllowedOnShip secondary bank lookup (#8033)
* Fix isWeaponAllowedOnShip bank lookup for secondary banks

isWeaponAllowedOnShip numbers banks per ship class, with the secondaries
right after the primaries, but the bank restriction lists keep secondary
bank n at MAX_SHIP_PRIMARY_BANKS + n. On a ship with fewer than three
primary banks, the secondary banks were checked against the wrong list.
Map the bank before the lookup.

The restriction lists are only allocated for ship classes that have
$Allowed PBanks/SBanks, so both this function and the loadout weapon
setter read past the end of an empty vector for any other class. Skip
the bank check when there are no lists.

The loadout weapon setter also treated a primary slot the ship does not
have (bank 3 on a two-primary ship) as a secondary bank.

Fixes #7985

* Cover more bank layouts and the loadout setter in the shipclass tests

Add classes with one and three primary banks, a restricted primary bank,
the bank-omitted case, and tests for the loadout weapon setter's bank
range and restriction checks.

* Fall back to the class weapon list in loadout bank checks

The weapon loadout's slot and pool moves read a ship class's per-bank
restriction lists without checking they exist. Those lists are only
allocated for classes with $Allowed PBanks/SBanks, so a move request for
any other class (from a script through sendWeaponRequestPacket, or a
multiplayer client) read past the end of an empty vector and crashed.

When a class has no per-bank lists, check the weapon against the
class-wide allowed weapons instead. Classes with lists behave as before.

* Simplify the loadout bank checks and tidy the loadout setter

wl_bank_allows_weapon already allows a bank with no restriction, so the
four loadout checks call it directly. The comment now says what the
empty case means: a class without bank lists allows no weapons.

The Loadout_Weapon setter rejects a weapon index one past the end, and
only resets the amount when it actually stored the weapon.

* Check the class weapon list in the loadout bank check

A bank without its own list accepted any weapon of the right type, so a
move request from a script or a multiplayer client could load a weapon
the ship class can't carry. Check the class-wide list first, as the
rearm code does. An empty slot coming back from a swap has nothing to
check, which also stops it adding a -1 entry to the weapon pool.

---------

Co-authored-by: Dustinspace217 <[email protected]>
2026-11-12 17:03:34 -05:00
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
Goober5000andClaude Opus 5.5 bb2b2f2869 Remove dead modelinterp code (#7782)
* Remove dead BSP-parsing and software-interp code

model_interp_defpoints has had no callers since model_interp_sub was
removed (1ef7688fa), and the parse_bsp and find_tri_counts chains have
had no entry point since bsp_polygon_data replaced them (c62037235).
Remove them, along with the globals that only they read: tri_count,
model_radius, GEOMETRY_NOISE, Interp_flags, Interp_lights,
Interp_thrust_scale_subobj and Interp_warp_scale_*.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>

* Remove the interp-data allocator; give the BE swap path its own vertex list

On big-endian builds, swap_bsp_defpoints was the only remaining caller
of model_allocate_interp_data, and swap_bsp_flatpoly the only reader of
the buffers it allocated (Interp_verts).  Pass a local vertex list
through the BSP swap functions instead, and remove the allocator, the
deallocator, and the Interp_* buffers and lighting globals it managed.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>

* Remove write-only interp state and the functions that only set it

The thruster, outline, detail-lock, insignia, forced-bitmap and box-scale
globals in modelinterp.cpp are written but never read; rendering takes
these from model_render_params instead.  Remove them, along with
model_set_thrust and model_set_outline_color (no callers),
model_set_detail_level (its calls sit beside the real
set_detail_level_lock), interp_clear_instance, and the unused
modelstats_num_sortnorms and modelstats_num_boxes counters.

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

* Drop the unused big-endian flat-poly recompute

swap_bsp_flatpoly recomputed the center and radius of pre-2003 flat
polygons, but nothing reads those fields on any platform: collision
reads the normal and vertices, bsp_polygon_data reads the count, color
and vertices, and flash_ball skips flat polygons.  Remove it, along with
the vertex list that only it needed, so swap_bsp_data is back to its
original shape with only the dead code deleted.

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

* remove unused Interp_detail_level

---------

Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
2026-11-12 14:58:49 -05:00
Shivansps 72053c7ddb Merge pull request #8022 from Shivansps/fix-android-scpui
Fix SCPUI not accepting text input on Android.
2026-11-12 13:42:01 -05:00
Goober5000 8d4bde4a74 Merge pull request #8026 from Goober5000/fix/bigendian_bsp_bounds
set Macro_ubyte_bounds around swap_bsp_data in model loading
2026-11-12 13:04:10 -05:00
Goober5000andClaude Opus 5.5 5cb93b1bf8 Audio robustness: fix stale stream handles (#7995)
* reset stale audio stream handles and guard stream play/stop

The training voice stream handle was never initialized to -1 or reset
after closing, so the none.wav path could play a freed (or reassigned)
stream slot.  In release builds the status Assert is a no-op, and
AudioStream::Play() on a freed slot registered an SDL timer whose ID was
later discarded by m_timer.constructor(), leaving it running forever.

Reset the handle after every close, make audiostream_play() and
audiostream_stop() return early on a freed slot, and have Play() remove
any lingering timer instead of forgetting it.  Also reset the event
music pattern and command briefing stage handles after closing them.

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

* fix uninitialized size and past-the-end read in ds_init_channels

Initialize the attribute count before querying it, skip the parse if
none are reported, and walk the zero-terminated list as key/value pairs
so attrs[i+1] can't run past the end and a value can't be mistaken for
the ALC_MONO_SOURCES key.

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

* remove unreachable training voice branch and fix small OpenAL nits

The Training_voice == index check in message_play_training_voice can
never be true for a streamed voice, since the enclosing condition
requires Training_voice != index in that case.  Also correct the
element count passed to alcGetIntegerv for ALC_FREQUENCY and update the
stale MIN_SOURCES comment, which still described the old 32-channel
MAX_CHANNELS.

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

* finish fading out audio streams that are at zero volume

A stream closed with fade while playing at volume 0 computed a cutoff
of 0 and then waited for its volume to drop below it, which never
happens.  The stream was never destroyed, so its slot and service timer
stayed held for the rest of the session.  With only 30 stream slots and
briefings opening many at once, leaked slots eventually make
audiostream_open() fail, silencing briefing voices and music until the
game is restarted.

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-12 12:41:00 -05:00
dustinspace217 a39bb9b773 Merge pull request #7977 from dustinspace217/bmpman/eff-buffer-size
P3: Size the .eff parse buffers from the file
2026-11-12 12:40:19 -05:00
Goober5000andClaude Opus 5.5 96482c9ef6 set Macro_ubyte_bounds around swap_bsp_data in model loading
The checked byte-access macros assert against Macro_ubyte_bounds in Debug builds, but it was still nullptr when model loading byte-swapped each submodel's BSP data, so big-endian Debug builds asserted on the first access in swap_bsp_data.  Little-endian builds were unaffected because swap_bsp_data is a no-op there.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-11 02:54:52 -05:00
96 changed files with 2821 additions and 1326 deletions

No files matched your search

+25 -6
View File
@@ -25,6 +25,7 @@
#include "io/key.h"
#include "io/timer.h"
#include "jpgutils/jpgutils.h"
#include "mod_table/mod_table.h"
#include "network/multiutil.h"
#include "parse/parselo.h"
#include "pcxutils/pcxutils.h"
@@ -883,6 +884,16 @@ const char *bm_get_filename(int handle) {
return entry->filename;
}
const char *bm_get_image_filename(int handle) {
auto entry = bm_get_entry(handle);
// an EFF entry stores the file of its individual image separately
if (entry->type == BM_TYPE_EFF)
return entry->info.ani.eff.filename;
return entry->filename;
}
void bm_get_filename(int bitmapnum, char *filename) {
if (!bm_is_valid(bitmapnum)) {
strcpy(filename, "");
@@ -1361,12 +1372,20 @@ bool bm_load_and_parse_eff(const char *filename, int dir_type, int *nframes, int
int frames = 0, fps = 30, keyframe = 0;
char ext[8];
BM_TYPE c_type = BM_TYPE_NONE;
char file_text[1024];
char file_text_raw[1024];
memset(ext, 0, sizeof(ext));
memset(file_text, 0, sizeof(file_text));
memset(file_text_raw, 0, sizeof(file_text_raw));
// cf_find_file_location reports the stored size, which is smaller than the text for a compressed file
CFILE* eff_cfp = cfopen(filename, "rb", dir_type);
const size_t file_len = (eff_cfp != nullptr) ? static_cast<size_t>(std::max(cfilelength(eff_cfp), 0)) : 0;
if (eff_cfp != nullptr) {
cfclose(eff_cfp);
}
// encrypted text unpacks 7 bytes into 8, and outside Unicode mode processing can turn one character into two
const size_t raw_len = file_len + file_len / 7 + 16;
SCP_vector<char> file_text_raw(raw_len, '\0');
SCP_vector<char> file_text(Unicode_text_mode ? raw_len : raw_len * 2, '\0');
// pause anything that may happen to be parsing right now
pause_parse();
@@ -1374,8 +1393,8 @@ bool bm_load_and_parse_eff(const char *filename, int dir_type, int *nframes, int
try
{
// now start parsing the EFF
read_file_text(filename, dir_type, file_text, file_text_raw);
reset_parse(file_text);
read_file_text(filename, dir_type, file_text.data(), file_text_raw.data());
reset_parse(file_text.data());
required_string("$Type:");
stuff_string(ext, F_NAME, sizeof(ext));
+7
View File
@@ -436,6 +436,13 @@ void bm_get_filename(int bitmapnum, char *filename);
*/
const char *bm_get_filename(int handle);
/**
* @brief Gets the name of the file that holds the pixels of this bitmap
*
* This is the same as bm_get_filename(), except for an EFF. For an EFF it is the file of the individual image.
*/
const char *bm_get_image_filename(int handle);
/**
* @brief Unloads all used bitmaps, should only ever be called by game_shutdown()
*
+41 -27
View File
@@ -4,6 +4,7 @@ precision highp float;
precision highp int;
precision highp sampler2D;
precision highp samplerCube;
precision highp sampler2DArray;
precision highp sampler2DArrayShadow;
#endif
@@ -26,13 +27,14 @@ layout(set = 1, binding = 1) uniform sampler2D sTextures[16];
#define SpecBuffer sTextures[3]
layout(set = 0, binding = 2) uniform sampler2DArrayShadow shadow_map;
layout(set = 0, binding = 7) uniform sampler2DArray shadow_map_raw;
#ifdef ENV_MAP
layout(set = 0, binding = 3) uniform samplerCube sEnvmap;
layout(set = 0, binding = 4) uniform samplerCube sIrrmap;
#endif
#ifdef RT_SHADOWS
#if defined(RT_SHADOWS) || defined(RTAO)
layout(set = 0, binding = 5) uniform accelerationStructureEXT shadow_tlas;
#endif
#else
@@ -41,6 +43,7 @@ uniform sampler2D NormalBuffer;
uniform sampler2D PositionBuffer;
uniform sampler2D SpecBuffer;
uniform sampler2DArrayShadow shadow_map;
uniform sampler2DArray shadow_map_raw;
#ifdef ENV_MAP
uniform samplerCube sEnvmap;
@@ -59,28 +62,20 @@ uniform globalDeferredData {
float invScreenWidth;
float invScreenHeight;
float nearPlane;
int rtaoSampleCount;
// The ray-tracing inputs of this pass, for raytraced shadows and RTAO. See
// deferred_global_data in uniform_structs.h.
vec3 rayViewOrigin;
int rayCullMask;
float rayBiasMin;
float rayBiasMax;
float rtaoRadius;
float rtaoStrength;
};
#ifdef VULKAN
layout(set = 0, binding = 6, std140)
#else
layout(std140)
#endif
uniform shadowCascadeParams {
int cascade_offset;
int cascade_count;
float rtShadowBiasMin;
float rtShadowBiasMax;
mat4 shadow_mv_matrix;
// See shadow_cascade_static_data (uniform_structs.h).
vec3 shadow_ray_view_origin;
int shadow_ray_cull_mask;
mat4 shadow_proj_matrix[NUM_SHADOW_CASCADES];
vec4 cascade_distances[(NUM_SHADOW_CASCADES + 4 - 1) / 4];
vec4 smoothness_factors[(NUM_SHADOW_CASCADES + 4 - 1) / 4];
};
#include "shadow_cascade_params.sdr" //! #include "shadow_cascade_params.sdr"
#ifdef VULKAN
layout(set = 2, binding = 1, std140)
@@ -303,6 +298,18 @@ void main()
if (lightType == LT_AMBIENT) {
float ao = position_buffer.w;
#ifdef RTAO
{
// Raytraced AO multiplies into the same ao term as the baked AO maps,
// so the env-map/IBL ambient below darkens consistently for free.
// Ray origin in TLAS-relative space, as for the shadow rays below.
vec3 worldPos = mat3(inv_view_matrix) * position + rayViewOrigin;
vec3 worldNormal = normalize((inv_view_matrix * vec4(normal, 0.0)).xyz);
float rtaoBias = computeRtShadowBias(length(position), rayBiasMin, rayBiasMax);
ao *= traceAmbientOcclusion(shadow_tlas, worldPos, worldNormal,
rtaoRadius, rtaoStrength, rtaoSampleCount, rtaoBias, uint(rayCullMask), gl_FragCoord.xy);
}
#endif
fragmentColor.rgb = diffuseLightColor * diffColor * ao;
#ifdef ENV_MAP
@@ -322,15 +329,22 @@ void main()
if (enable_shadows != 0) {
#ifdef RT_SHADOWS
// Ray origin in TLAS-relative space; see shadow_cascade_static_data (uniform_structs.h).
// Ray origin in TLAS-relative space; see deferred_global_data (uniform_structs.h).
// Only the rotation of inv_view_matrix is used: its translation is the (large) camera
// world position, which shadow_ray_view_origin supplies already rebased onto the TLAS.
vec3 worldPos = mat3(inv_view_matrix) * position + shadow_ray_view_origin;
// world position, which rayViewOrigin supplies already rebased onto the TLAS.
vec3 worldPos = mat3(inv_view_matrix) * position + rayViewOrigin;
vec3 worldNormal = normalize((inv_view_matrix * vec4(normal, 0.0)).xyz);
vec3 worldLightDir = normalize((inv_view_matrix * vec4(lightDir, 0.0)).xyz);
float rtShadowBias = computeRtShadowBias(length(position), rtShadowBiasMin, rtShadowBiasMax);
attenuation *= traceShadowRay(shadow_tlas, worldPos, worldNormal, worldLightDir, lightDist, rtShadowBias, uint(shadow_ray_cull_mask));
float rtShadowBias = computeRtShadowBias(length(position), rayBiasMin, rayBiasMax);
// Penumbra cone: for directional lights sourceRadius IS the tangent of the
// sun's angular radius ($SunAngularSize); for local lights the subtended
// half-angle follows from the physical source radius and the light distance.
float coneTan = (lightType == LT_DIRECTIONAL)
? sourceRadius
: sourceRadius / max(lightDist, 1e-3);
attenuation *= traceShadowRayCone(shadow_tlas, worldPos, worldNormal, worldLightDir, lightDist,
rtShadowBias, uint(rayCullMask), coneTan, rtShadowSampleCount, gl_FragCoord.xy);
#else
vec4 fragShadowPos = shadow_mv_matrix * inv_view_matrix * vec4(position, 1.0);
vec4 fragShadowUV[NUM_SHADOW_CASCADES];
@@ -338,7 +352,7 @@ void main()
fragShadowUV[i] = transformToShadowMap(shadow_proj_matrix[i], i, fragShadowPos);
}
attenuation *= getShadowValue(shadow_map, -position.z, fragShadowUV, cascade_distances, smoothness_factors, cascade_offset, cascade_count);
attenuation *= getShadowValue(shadow_map, shadow_map_raw, -position.z, fragShadowUV, cascade_distances, smoothness_factors, penumbra_scale, cascade_offset, cascade_count);
#endif
}
+9 -22
View File
@@ -100,26 +100,7 @@ uniform modelData {
int flags;
};
#ifdef VULKAN
layout(set = 0, binding = 6, std140)
#else
layout(std140)
#endif
uniform shadowCascadeParams {
int cascade_offset;
int cascade_count;
float rtShadowBiasMin;
float rtShadowBiasMax;
mat4 shadow_mv_matrix;
// See shadow_cascade_static_data (uniform_structs.h).
vec3 shadow_ray_view_origin;
int shadow_ray_cull_mask;
mat4 shadow_proj_matrix[NUM_SHADOW_CASCADES];
vec4 cascade_distances[(NUM_SHADOW_CASCADES + 4 - 1) / 4];
vec4 smoothness_factors[(NUM_SHADOW_CASCADES + 4 - 1) / 4];
};
#include "shadow_cascade_params.sdr" //! #include "shadow_cascade_params.sdr"
layout(location = 0) out vec4 fragOut0;
@@ -138,6 +119,7 @@ layout(set = 1, binding = 1) uniform sampler2DArray materialTextures[16];
#define sMiscmap materialTextures[6]
layout(set = 0, binding = 2) uniform sampler2DArrayShadow shadow_map;
layout(set = 0, binding = 7) uniform sampler2DArray shadow_map_raw;
layout(set = 1, binding = 5) uniform sampler2D sFramebuffer;
#ifdef MODEL_SDR_FLAG_RT_SHADOWS
@@ -183,6 +165,7 @@ uniform sampler2DArray sMiscmap;
#prereplace ENDIF_FLAG_COMPILED MODEL_SDR_FLAG_MISC
#prereplace IF_FLAG_COMPILED MODEL_SDR_FLAG_SHADOWS
uniform sampler2DArrayShadow shadow_map;
uniform sampler2DArray shadow_map_raw;
#prereplace ENDIF_FLAG_COMPILED MODEL_SDR_FLAG_SHADOWS
in VertexOutput {
@@ -267,7 +250,11 @@ vec3 CalculateLighting(vec3 normal, vec3 diffuseMaterial, vec3 specularMaterial,
if (rtShadowsActive && lights[i].light_type == LT_DIRECTIONAL && shadowedDirectionalCount < MAX_RT_SHADOW_LIGHTS) {
vec3 worldSunDir = normalize((invView * vec4(lights[i].position.xyz, 0.0)).xyz);
float rtShadowBias = computeRtShadowBias(length(vertIn.position.xyz), rtShadowBiasMin, rtShadowBiasMax);
shadow = traceShadowRay(shadow_tlas, worldPos, worldNormal, worldSunDir, RT_SHADOW_MAX_DISTANCE, rtShadowBias, uint(shadow_ray_cull_mask));
// For directional lights ml_sourceRadius is the tangent of the sun's
// angular radius ($SunAngularSize) -- 0 keeps hard shadows.
shadow = traceShadowRayCone(shadow_tlas, worldPos, worldNormal, worldSunDir, RT_SHADOW_MAX_DISTANCE,
rtShadowBias, uint(shadow_ray_cull_mask), lights[i].ml_sourceRadius,
rtShadowSampleCount, gl_FragCoord.xy);
++shadowedDirectionalCount;
} else {
shadow = 1.0;
@@ -439,7 +426,7 @@ void main()
// up to MAX_RT_SHADOW_LIGHTS) inside CalculateLighting()'s loop instead --
// this single shared `shadow` value only matters for the CSM fallback below.
#ifndef MODEL_SDR_FLAG_RT_SHADOWS
shadow = getShadowValue(shadow_map, -vertIn.position.z, vertIn.shadowUV, cascade_distances, smoothness_factors, cascade_offset, cascade_count);
shadow = getShadowValue(shadow_map, shadow_map_raw, -vertIn.position.z, vertIn.shadowUV, cascade_distances, smoothness_factors, penumbra_scale, cascade_offset, cascade_count);
#endif
#prereplace ENDIF_FLAG //MODEL_SDR_FLAG_SHADOWS
baseColor.rgb = CalculateLighting(normal, baseColor.rgb, specColor.rgb, glossData, fresnelFactor, shadow, aoFactors.x);
+2 -20
View File
@@ -7,6 +7,7 @@
precision highp float;
precision highp int;
precision highp samplerBuffer;
precision highp sampler2DArray;
precision highp sampler2DArrayShadow;
#else
#extension GL_ARB_gpu_shader5: enable
@@ -115,26 +116,7 @@ uniform modelData {
int flags;
};
#ifdef VULKAN
layout(set = 0, binding = 6, std140)
#else
layout(std140)
#endif
uniform shadowCascadeParams {
int cascade_offset;
int cascade_count;
float rtShadowBiasMin;
float rtShadowBiasMax;
mat4 shadow_mv_matrix;
// See shadow_cascade_static_data (uniform_structs.h). Unused here; declared for block layout.
vec3 shadow_ray_view_origin;
int shadow_ray_cull_mask;
mat4 shadow_proj_matrix[NUM_SHADOW_CASCADES];
vec4 cascade_distances[(NUM_SHADOW_CASCADES + 4 - 1) / 4];
vec4 smoothness_factors[(NUM_SHADOW_CASCADES + 4 - 1) / 4];
};
#include "shadow_cascade_params.sdr" //! #include "shadow_cascade_params.sdr"
#ifdef VULKAN
// Transform buffer for batched submodel rendering
@@ -0,0 +1,31 @@
// The shadowCascadeParams uniform block. All shaders that read the shadow cascades include this
// file, so the block has one declaration. The C++ layout is shadow_cascade_static_data in
// uniform_structs.h. Change both declarations together.
#ifdef VULKAN
layout(set = 0, binding = 6, std140)
#else
layout(std140)
#endif
uniform shadowCascadeParams {
int cascade_offset;
int cascade_count;
float rtShadowBiasMin;
float rtShadowBiasMax;
int rtShadowSampleCount;
// Padding. The scalars must fill 32 bytes, so that shadow_mv_matrix starts on a 16-byte boundary.
int shadowParamsPad0;
int shadowParamsPad1;
int shadowParamsPad2;
mat4 shadow_mv_matrix;
// The camera position in the coordinate space of the ray-tracing structure.
// See shadow_cascade_static_data in uniform_structs.h.
vec3 shadow_ray_view_origin;
int shadow_ray_cull_mask;
mat4 shadow_proj_matrix[NUM_SHADOW_CASCADES];
vec4 cascade_distances[(NUM_SHADOW_CASCADES + 4 - 1) / 4];
vec4 smoothness_factors[(NUM_SHADOW_CASCADES + 4 - 1) / 4];
vec4 penumbra_scale[(NUM_SHADOW_CASCADES + 4 - 1) / 4];
};
+3 -15
View File
@@ -14,21 +14,9 @@ layout (triangle_strip, max_vertices = 3) out;
layout(invocations = NUM_SHADOW_CASCADES) in;
#endif
layout (std140) uniform shadowCascadeParams {
int cascade_offset;
int cascade_count;
float rtShadowBiasMin;
float rtShadowBiasMax;
mat4 shadow_mv_matrix;
// See shadow_cascade_static_data (uniform_structs.h). Unused here; declared for block layout.
vec3 shadow_ray_view_origin;
int shadow_ray_cull_mask;
mat4 shadow_proj_matrix[NUM_SHADOW_CASCADES];
vec4 cascade_distances[(NUM_SHADOW_CASCADES + 4 - 1) / 4];
vec4 smoothness_factors[(NUM_SHADOW_CASCADES + 4 - 1) / 4];
};
// OpenGL-only: the Vulkan backend has no geometry-shader stage, so the Vulkan binding in the
// include below does not apply here.
#include "shadow_cascade_params.sdr" //! #include "shadow_cascade_params.sdr"
in VertexOutput {
#if !defined(GL_ARB_gpu_shader5) && !defined(GL_EXT_gpu_shader5)
+1 -20
View File
@@ -39,26 +39,7 @@ uniform shadowMapData {
int buffer_matrix_offset;
};
#ifdef VULKAN
layout(set = 0, binding = 6, std140)
#else
layout(std140)
#endif
uniform shadowCascadeParams {
int cascade_offset;
int cascade_count;
float rtShadowBiasMin;
float rtShadowBiasMax;
mat4 shadow_mv_matrix;
// See shadow_cascade_static_data (uniform_structs.h). Unused here; declared for block layout.
vec3 shadow_ray_view_origin;
int shadow_ray_cull_mask;
mat4 shadow_proj_matrix[NUM_SHADOW_CASCADES];
vec4 cascade_distances[(NUM_SHADOW_CASCADES + 4 - 1) / 4];
vec4 smoothness_factors[(NUM_SHADOW_CASCADES + 4 - 1) / 4];
};
#include "shadow_cascade_params.sdr" //! #include "shadow_cascade_params.sdr"
#ifdef VULKAN
// Transform buffer for batched submodel rendering (matches main-v.sdr)
+260 -39
View File
@@ -6,33 +6,47 @@
#define SAMPLES_PCSS 1
#endif
float samplePoissonPCSS(sampler2DArrayShadow shadow_map, vec4 shadowUV, float maxUVOffset)
{
// Full 16-tap Poisson disc. Unconditional (independent of SMOOTH_PCSS) because it has three
// consumers with different needs: samplePoissonPCSS() takes the first SAMPLES_PCSS of these,
// pcssBlockerSearch() always wants 8 (every second tap) regardless of SAMPLES_PCSS (a
// blocker search only needs a coarse estimate), and the raytraced cone/AO sampling further
// below always
// wants the full spread. Keeping one table means none of those three can end up indexing
// past a table sized for a different consumer.
const vec2 poissonDisc16[16] = vec2[](
vec2(-0.76275, -0.3432573),
vec2(-0.5226235, -0.8277544),
vec2(-0.3780261, 0.01528688),
vec2(-0.7742821, 0.4245702),
vec2(0.04196143, -0.02622231),
vec2(-0.2974772, -0.4722782),
vec2(-0.516093, 0.71495),
vec2(-0.3257416, 0.3910343),
vec2(0.2705966, 0.6670476),
vec2(0.4918377, 0.1853267),
vec2(0.4428544, -0.6251478),
vec2(-0.09204347, 0.9267113),
vec2(0.391505, -0.2558275),
vec2(0.05605913, -0.7570801),
vec2(0.81772, -0.02475523),
vec2(0.6890262, 0.5191521)
);
// Low-quality raster PCSS fallback: a single centered sample (i.e. no offset at all) rather
// than poissonDisc16[0], which is just one arbitrary tap among sixteen and no better centered
// than any other.
const vec2 poissonDiscCenter[1] = vec2[](vec2(0.0, 0.0));
// samplePoissonPCSS()'s tap table: the full disc when taking all SAMPLES_PCSS == 16 of them,
// or the single centered sample when SAMPLES_PCSS == 1.
#if SMOOTH_PCSS
vec2 poissonDisc[16] = vec2[](
vec2(-0.76275, -0.3432573),
vec2(-0.5226235, -0.8277544),
vec2(-0.3780261, 0.01528688),
vec2(-0.7742821, 0.4245702),
vec2(0.04196143, -0.02622231),
vec2(-0.2974772, -0.4722782),
vec2(-0.516093, 0.71495),
vec2(-0.3257416, 0.3910343),
vec2(0.2705966, 0.6670476),
vec2(0.4918377, 0.1853267),
vec2(0.4428544, -0.6251478),
vec2(-0.09204347, 0.9267113),
vec2(0.391505, -0.2558275),
vec2(0.05605913, -0.7570801),
vec2(0.81772, -0.02475523),
vec2(0.6890262, 0.5191521)
);
#define poissonDisc poissonDisc16
#else
vec2 poissonDisc[1] = vec2[](
vec2(0.0,0.0)
);
#define poissonDisc poissonDiscCenter
#endif
float samplePoissonPCSS(sampler2DArrayShadow shadow_map, vec4 shadowUV, float maxUVOffset)
{
float visibility = 0.0f;
for (int i=0; i<SAMPLES_PCSS; i++) {
vec4 uv = shadowUV;
@@ -42,8 +56,82 @@ float samplePoissonPCSS(sampler2DArrayShadow shadow_map, vec4 shadowUV, float ma
return visibility * (1.0f/float(SAMPLES_PCSS));
}
float getShadowValue(sampler2DArrayShadow shadow_map, float depth, vec4 shadowUV[NUM_SHADOW_CASCADES],
// PCSS blocker search: raw (non-compare) depth taps within searchRadiusUV, using every
// second tap of poissonDisc16. The first 8 taps are mostly on one side (x < 0), so they
// would search the penumbra on one side only. This is always the full-spread table, not
// samplePoissonPCSS()'s poissonDisc --
// a blocker search only needs a coarse estimate, not the final filtered result, but it needs
// that estimate regardless of how many taps the raster PCSS path itself is set to take).
// "Blocker" = stored < receiverDepth, matching the GL_LEQUAL/eLessOrEqual compare convention
// the hardware PCF sampler already uses (visible iff Dref <= stored) -- see the compare-sampler
// setup in gropengltnl.cpp / VulkanPostProcessingShadow.cpp.
// A negative return means "no blockers found", which the average blocker depth (always >= 0)
// can't otherwise be distinguished from "found some at depth 0".
float pcssBlockerSearch(sampler2DArray shadow_map_raw, vec4 shadowUV, float searchRadiusUV)
{
float sum = 0.0;
int blockerCount = 0;
for (int i = 0; i < 8; i++) {
vec2 uv = shadowUV.xy + poissonDisc16[i * 2] * searchRadiusUV;
float sampled = texture(shadow_map_raw, vec3(uv, shadowUV.z)).r;
if (sampled < shadowUV.w) {
sum += sampled;
blockerCount++;
}
}
return blockerCount > 0 ? (sum / float(blockerCount)) : -1.0;
}
// Computes the PCSS penumbra radius for one cascade sample: a blocker search followed by
// the orthographic-light penumbra formula (see shadow_smoothness_scale()'s doc comment in
// shadows.cpp for the derivation), clamped to [1 texel, smoothnessCeiling]. penumbraScale is
// Shadow_penumbra_scale[cascade] -- a negative value is the "unsupported or user-disabled"
// sentinel (see shadow_contact_hardening_enabled() in shadows.cpp), in which case this
// returns smoothnessCeiling immediately: bit-identical to the fixed-width behavior from
// before contact hardening existed, and skips the blocker search's extra texture reads.
float pcssPenumbraRadius(sampler2DArrayShadow shadow_map, sampler2DArray shadow_map_raw,
vec4 shadowUV, float penumbraScale, float smoothnessCeiling)
{
float texelFloor = 1.0 / float(textureSize(shadow_map, 0).x);
if (penumbraScale < 0.0) {
return smoothnessCeiling;
}
float avgBlockerDepth = pcssBlockerSearch(shadow_map_raw, shadowUV, smoothnessCeiling);
if (avgBlockerDepth < 0.0) {
// The sparse 8-tap search found nothing, but that doesn't prove there's no thin
// occluder nearby -- run the full PCF at the hardest radius rather than assuming
// fully lit, so a missed thin blocker still reads as an edge instead of popping to
// no shadow at all.
return texelFloor;
}
float deltaDepth = max(shadowUV.w - avgBlockerDepth, 0.0);
return clamp(deltaDepth * penumbraScale, texelFloor, smoothnessCeiling);
}
// PCSS-filtered shadow visibility for a single cascade: looks up its penumbra radius (see
// pcssPenumbraRadius()) and samples the shadow map at that radius. Factored out of
// getShadowValue() below so the cascade-index unpacking (v/i from cascade/4, cascade%4) and
// the penumbra-radius lookup each appear once, whichever of getShadowValue()'s two return
// paths runs.
float shadowVisibilityForCascade(sampler2DArrayShadow shadow_map, sampler2DArray shadow_map_raw,
vec4 shadowUV, int cascade, vec4 smoothness_factors[(NUM_SHADOW_CASCADES + 4 - 1) / 4],
vec4 penumbra_scale[(NUM_SHADOW_CASCADES + 4 - 1) / 4])
{
int v = cascade / 4;
int i = cascade % 4;
float radius = pcssPenumbraRadius(shadow_map, shadow_map_raw, shadowUV,
penumbra_scale[v][i], smoothness_factors[v][i]);
return samplePoissonPCSS(shadow_map, shadowUV, radius);
}
float getShadowValue(sampler2DArrayShadow shadow_map, sampler2DArray shadow_map_raw, float depth, vec4 shadowUV[NUM_SHADOW_CASCADES],
vec4 cascade_distances[(NUM_SHADOW_CASCADES + 4 - 1) / 4], vec4 smoothness_factors[(NUM_SHADOW_CASCADES + 4 - 1) / 4],
vec4 penumbra_scale[(NUM_SHADOW_CASCADES + 4 - 1) / 4],
int cascade_offset, int cascade_count)
{
int cascade = cascade_offset + cascade_count;
@@ -52,19 +140,19 @@ float getShadowValue(sampler2DArrayShadow shadow_map, float depth, vec4 shadowUV
}
if (cascade >= cascade_offset + cascade_count || cascade < cascade_offset) return 1.0;
int cascade_v = cascade / 4;
int cascade_i = cascade % 4;
float cascade_start = (cascade > cascade_offset) ? cascade_distances[(cascade - 1) / 4][(cascade - 1) % 4] : 0.0;
float cascade_end = cascade_distances[cascade_v][cascade_i];
float cascade_end = cascade_distances[cascade / 4][cascade % 4];
float dist_threshold = (cascade_end - cascade_start) * 0.2;
if (cascade_end - dist_threshold > depth || cascade >= cascade_offset + cascade_count - 1)
return samplePoissonPCSS(shadow_map, shadowUV[cascade], smoothness_factors[cascade_v][cascade_i]);
return shadowVisibilityForCascade(shadow_map, shadow_map_raw, shadowUV[cascade], cascade,
smoothness_factors, penumbra_scale);
return mix(
samplePoissonPCSS(shadow_map, shadowUV[cascade], smoothness_factors[cascade_v][cascade_i]),
samplePoissonPCSS(shadow_map, shadowUV[cascade + 1], smoothness_factors[(cascade + 1) / 4][(cascade + 1) % 4]),
shadowVisibilityForCascade(shadow_map, shadow_map_raw, shadowUV[cascade], cascade,
smoothness_factors, penumbra_scale),
shadowVisibilityForCascade(shadow_map, shadow_map_raw, shadowUV[cascade + 1], cascade + 1,
smoothness_factors, penumbra_scale),
smoothstep(cascade_end - dist_threshold, cascade_end, depth));
}
// Raytraced shadow test via inline ray query against the shadow TLAS. Only
@@ -79,11 +167,11 @@ float getShadowValue(sampler2DArrayShadow shadow_map, float depth, vec4 shadowUV
// in deferred-f.sdr/main-f.sdr references it regardless of which shadow method is active.
const float RT_SHADOW_MAX_DISTANCE = 10000.0;
#if defined(MODEL_SDR_FLAG_RT_SHADOWS) || defined(RT_SHADOWS)
#if defined(MODEL_SDR_FLAG_RT_SHADOWS) || defined(RT_SHADOWS) || defined(RTAO)
#extension GL_EXT_ray_query : require
// Ray origin bias grows with distance from the camera, between the rtShadowBiasMin/
// rtShadowBiasMax bounds (see shadowCascadeParams uniform block, and the Min/Max
// rtShadowBiasMax bounds (see the shadowCascadeParams and globalDeferredData blocks, and the Min/Max
// Raytraced Shadow Bias options in shadows.cpp). Empirically, geometry close to the
// camera only needs biasMin worth of offset to clear self-intersection acne, while
// distant geometry needs more -- consistent with the error source being G-buffer/
@@ -100,6 +188,20 @@ float computeRtShadowBias(float cameraDist, float biasMin, float biasMax)
return mix(biasMin, biasMax, clamp(cameraDist / RT_SHADOW_BIAS_RAMP_DISTANCE, 0.0, 1.0));
}
// Traces one occlusion ray. traceShadowRay() and traceShadowRayCone() both use this function.
// The origin must already include the bias. The direction must be a unit vector. cullMask has
// the same meaning as in traceShadowRay(). Returns 1.0 when the ray reaches tMax with no hit, and
// 0.0 when it hits an instance.
float traceVisibilityRay(accelerationStructureEXT tlas, vec3 origin, vec3 direction, float tMax, uint cullMask)
{
rayQueryEXT rq;
rayQueryInitializeEXT(rq, tlas, gl_RayFlagsOpaqueEXT | gl_RayFlagsTerminateOnFirstHitEXT,
cullMask, origin, 0.001, direction, tMax);
while (rayQueryProceedEXT(rq)) {}
return (rayQueryGetIntersectionTypeEXT(rq, true) == gl_RayQueryCommittedIntersectionNoneEXT) ? 1.0 : 0.0;
}
// worldPos/worldNormal/worldLightDir must all be in the same world space the
// shadow TLAS was built in (see VulkanRaytracingManager::buildTlas()) -- i.e.
// FS2Open object-space positions/orientations, not view/eye-space. worldLightDir
@@ -117,20 +219,139 @@ float computeRtShadowBias(float cameraDist, float biasMin, float biasMax)
//
// cullMask selects which TLAS instances this ray can hit (Vulkan spec: an instance
// is a candidate iff (cullMask & instance.mask) != 0). Callers should pass
// shadow_ray_cull_mask (shadowCascadeParams uniform block) rather than a literal --
// the shadow ray cull mask from a uniform block rather than a literal --
// see SHADOW_RAY_CULL_MASK_EXCLUDE_VIEWER_HULL/TLAS_MASK_VIEWER_HULL (shadows.h)
// for how it excludes the viewer ship's own hull outside the cockpit pass.
float traceShadowRay(accelerationStructureEXT tlas, vec3 worldPos, vec3 worldNormal, vec3 worldLightDir, float tMax, float bias, uint cullMask)
{
return traceVisibilityRay(tlas, worldPos + worldNormal * bias, normalize(worldLightDir), tMax, cullMask);
}
// Penumbra/AO sampling: at most this many rays per fragment per light, matching the size of
// poissonDisc16 above (the RT cone/AO sampling below always wants the full 16-tap spread,
// independent of SAMPLES_PCSS/SMOOTH_PCSS -- see the comment on poissonDisc16). The
// rtShadowSampleCount and rtaoSampleCount uniforms are clamped against it, so a bad uniform value can't
// index out of the pattern.
#define RT_SHADOW_MAX_SAMPLES 16
// Orthonormal basis spanning the plane perpendicular to axis (a unit vector).
void rtBuildOnb(vec3 axis, out vec3 tangent, out vec3 bitangent)
{
vec3 up = (abs(axis.z) < 0.999) ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
tangent = normalize(cross(up, axis));
bitangent = cross(axis, tangent);
}
// Interleaved gradient noise (Jimenez 2014) -> per-fragment [0,1) value used to
// rotate the shared disc pattern. Callers pass gl_FragCoord.xy as the seed; it is
// a parameter rather than read here because this file is also compiled in vertex
// stages, where gl_FragCoord doesn't exist.
float rtInterleavedGradientNoise(vec2 seed)
{
return fract(52.9829189 * fract(dot(seed, vec2(0.06711056, 0.00583715))));
}
// Soft (penumbra) variant of traceShadowRay: averages up to sampleCount occlusion
// rays whose directions are jittered inside a cone of half-angle
// atan(coneHalfAngleTan) around worldLightDir. Callers derive coneHalfAngleTan
// from the light's source radius -- for directional lights the light's
// source_radius IS the tangent of its angular radius (see $SunAngularSize in
// stars.tbl / starfield.cpp), for local lights it's sourceRadius / lightDist --
// so penumbras widen with occluder distance like a real area light's would.
//
// cullMask has the same meaning as in traceShadowRay().
//
// noiseSeed decorrelates the sample pattern between fragments (see
// rtInterleavedGradientNoise() for why it's a parameter). There is no temporal
// accumulation in the engine, so the penumbra must fully resolve in this one
// pass: the disc pattern is fixed and only its per-fragment rotation is
// randomized, trading a little visible grain for stable, flicker-free output.
//
// sampleCount <= 1 or coneHalfAngleTan <= 0.0 falls back to the single hard ray,
// bit-identical to traceShadowRay() -- that keeps the pre-penumbra look and cost
// as the default (mods opt in via source radii, users via the sample option).
float traceShadowRayCone(accelerationStructureEXT tlas, vec3 worldPos, vec3 worldNormal, vec3 worldLightDir,
float tMax, float bias, uint cullMask, float coneHalfAngleTan, int sampleCount, vec2 noiseSeed)
{
if (sampleCount <= 1 || coneHalfAngleTan <= 0.0) {
return traceShadowRay(tlas, worldPos, worldNormal, worldLightDir, tMax, bias, cullMask);
}
vec3 origin = worldPos + worldNormal * bias;
vec3 direction = normalize(worldLightDir);
rayQueryEXT rq;
rayQueryInitializeEXT(rq, tlas, gl_RayFlagsOpaqueEXT | gl_RayFlagsTerminateOnFirstHitEXT,
cullMask, origin, 0.001, direction, tMax);
while (rayQueryProceedEXT(rq)) {}
vec3 tangent, bitangent;
rtBuildOnb(direction, tangent, bitangent);
return (rayQueryGetIntersectionTypeEXT(rq, true) == gl_RayQueryCommittedIntersectionNoneEXT) ? 1.0 : 0.0;
float rotAngle = rtInterleavedGradientNoise(noiseSeed) * 6.2831853;
float cosR = cos(rotAngle);
float sinR = sin(rotAngle);
int samples = min(sampleCount, RT_SHADOW_MAX_SAMPLES);
float visibility = 0.0;
for (int i = 0; i < samples; ++i) {
vec2 p = poissonDisc16[i];
p = vec2(p.x * cosR - p.y * sinR, p.x * sinR + p.y * cosR);
vec3 sampleDir = normalize(direction + (tangent * p.x + bitangent * p.y) * coneHalfAngleTan);
visibility += traceVisibilityRay(tlas, origin, sampleDir, tMax, cullMask);
}
return visibility / float(samples);
}
// Raytraced ambient occlusion: averages up to sampleCount occlusion rays of
// length `radius` over the hemisphere around worldNormal and returns ambient
// visibility in [0,1] (1 = fully open), shaped by pow(visibility, strength).
// Mapping the shared disc pattern up onto the hemisphere (z = sqrt(1-|p|^2))
// makes the samples cosine-weighted, matching the diffuse ambient term being
// occluded. radius/strength come from the mod's lighting profile ($RTAO Radius /
// $RTAO Strength); scene scale varies too much (fighters vs. capships) for a
// built-in constant.
//
// Hits accumulate with a (1 - t/radius) falloff so geometry entering the radius
// occludes gradually instead of popping. That needs the *closest* hit's t, so
// these rays deliberately omit gl_RayFlagsTerminateOnFirstHitEXT (any-hit t
// would make the falloff arbitrary); the extra traversal cost is small because
// tMax = radius keeps the rays short.
//
// sampleCount < 1, radius <= 0, or strength <= 0 returns 1.0 (no occlusion) --
// callers gate on the RTAO shader variant, this just keeps bad uniforms benign.
float traceAmbientOcclusion(accelerationStructureEXT tlas, vec3 worldPos, vec3 worldNormal,
float radius, float strength, int sampleCount, float bias, uint cullMask, vec2 noiseSeed)
{
if (sampleCount < 1 || radius <= 0.0 || strength <= 0.0) {
return 1.0;
}
vec3 origin = worldPos + worldNormal * bias;
vec3 tangent, bitangent;
rtBuildOnb(worldNormal, tangent, bitangent);
float rotAngle = rtInterleavedGradientNoise(noiseSeed) * 6.2831853;
float cosR = cos(rotAngle);
float sinR = sin(rotAngle);
int samples = min(sampleCount, RT_SHADOW_MAX_SAMPLES);
float occlusion = 0.0;
for (int i = 0; i < samples; ++i) {
vec2 p = poissonDisc16[i];
p = vec2(p.x * cosR - p.y * sinR, p.x * sinR + p.y * cosR);
vec3 sampleDir = normalize(tangent * p.x + bitangent * p.y
+ worldNormal * sqrt(max(1.0 - dot(p, p), 0.0)));
rayQueryEXT rq;
rayQueryInitializeEXT(rq, tlas, gl_RayFlagsOpaqueEXT,
cullMask, origin, 0.001, sampleDir, radius);
while (rayQueryProceedEXT(rq)) {}
if (rayQueryGetIntersectionTypeEXT(rq, true) != gl_RayQueryCommittedIntersectionNoneEXT) {
float hitT = rayQueryGetIntersectionTEXT(rq, true);
occlusion += 1.0 - clamp(hitT / radius, 0.0, 1.0);
}
}
float visibility = clamp(1.0 - occlusion / float(samples), 0.0, 1.0);
return pow(visibility, strength);
}
#endif
+2
View File
@@ -788,6 +788,8 @@ void event_music_level_close()
}
else
audiostream_close_file( Patterns[i].handle, 0 );
Patterns[i].handle = -1;
}
} else {
// close em all down then
+1
View File
@@ -85,6 +85,7 @@ gr_capability_def gr_capabilities[] = {
GR_CAPABILITY_ENTRY(INSTANCED_RENDERING),
GR_CAPABILITY_ENTRY(FAST_SHADOWS),
GR_CAPABILITY_ENTRY(RAYTRACED_SHADOWS),
GR_CAPABILITY_ENTRY(SHADOW_CONTACT_HARDENING),
};
const size_t gr_capabilities_num = sizeof(gr_capabilities) / sizeof(gr_capabilities[0]);
+7 -10
View File
@@ -262,6 +262,7 @@ enum shader_type {
#define SDR_FLAG_ENV_MAP (1 << 0)
#define SDR_FLAG_DEFERRED_RT_SHADOWS (1 << 1)
#define SDR_FLAG_DEFERRED_RTAO (1 << 2)
#define SDR_FLAG_SHADOW_FALLBACK (1 << 0)
@@ -370,7 +371,12 @@ enum class gr_capability {
CAPABILITY_INSTANCED_RENDERING,
CAPABILITY_FAST_SHADOWS,
CAPABILITY_QUERIES_REUSABLE,
CAPABILITY_RAYTRACED_SHADOWS
CAPABILITY_RAYTRACED_SHADOWS,
// A second, non-compare sampler bound to the shadow map for raw depth reads, needed by
// the shadow-map PCSS blocker search. Vulkan always has this (samplers are independent
// of images there); OpenGL needs GL 3.3 (glGenSamplers/glBindSampler), since the compare mode is
// otherwise texture-object state shared by every sampler bound to that texture.
CAPABILITY_SHADOW_CONTACT_HARDENING
};
struct gr_capability_def {
@@ -879,13 +885,6 @@ typedef struct screen {
std::function<void(int pm_id)> gf_model_loaded = [](int) {};
std::function<void(int pm_id)> gf_model_unloaded = [](int) {};
// Rebuilds the raytraced-shadow top-level acceleration structure for the
// current frame from the live shadow-casting object set. Called once per
// frame from shadows_render_all(), alongside the cascaded shadow map pass --
// the shading pass picks between the two per shadows_use_raytracing().
// No-op default.
std::function<void()> gf_build_shadow_tlas = []() {};
std::function<void(gr_buffer_handle handle, size_t size, const void* data)> gf_update_buffer_data;
std::function<void(gr_buffer_handle handle, size_t offset, size_t size, const void* data)>
gf_update_buffer_data_offset;
@@ -1327,8 +1326,6 @@ inline void gr_model_unloaded(int pm_id)
gr_screen.gf_model_unloaded(pm_id);
}
#define gr_build_shadow_tlas GR_CALL(gr_screen.gf_build_shadow_tlas)
#define gr_update_buffer_data GR_CALL(gr_screen.gf_update_buffer_data)
#define gr_update_buffer_data_offset GR_CALL(gr_screen.gf_update_buffer_data_offset)
inline void* gr_map_buffer(gr_buffer_handle handle)
+12
View File
@@ -1631,6 +1631,18 @@ bool gr_opengl_is_capable(gr_capability capability)
case gr_capability::CAPABILITY_RAYTRACED_SHADOWS:
// Raytraced shadows are only implemented for the Vulkan backend.
return false;
case gr_capability::CAPABILITY_SHADOW_CONTACT_HARDENING:
// Needs a second sampler object (compare mode off) on the shadow map. Sampler
// objects are technically also available pre-3.3 via ARB_sampler_objects, but this
// project's glad build only wires glGenSamplers/glBindSampler/glSamplerParameteri
// up behind the core GL_VERSION_3_3 flag, not a separate ARB entry point -- so that's
// the check that actually reflects whether those functions are non-null here.
#ifdef USE_OPENGL_ES
// GLES has no GLAD_GL_VERSION_3_3 symbol, and the sampler setup here is desktop GL only.
return false;
#else
return GLAD_GL_VERSION_3_3 != 0;
#endif
}
+4 -1
View File
@@ -253,6 +253,9 @@ void gr_opengl_deferred_lighting_finish()
GL_state.Texture.Enable(3, GL_TEXTURE_2D, Scene_specular_texture);
if (Shadow_quality != ShadowQuality::Disabled) {
GL_state.Texture.Enable(4, GL_TEXTURE_2D_ARRAY, Shadow_map_depth_texture);
if (Shadow_map_raw_sampler) {
GL_state.Texture.Enable(SHADOW_MAP_RAW_TEXTURE_UNIT, GL_TEXTURE_2D_ARRAY, Shadow_map_depth_texture, Shadow_map_raw_sampler);
}
}
if (ENVMAP > 0) {
@@ -314,8 +317,8 @@ void gr_opengl_deferred_lighting_finish()
auto lp = ltp::current();
auto header = light_uniform_aligner.getHeader<deferred_global_data>();
shadows_fill_deferred_global_data(*header);
if (Shadow_quality != ShadowQuality::Disabled) {
vm_inverse_matrix4(&header->inv_view_matrix, &Shadow_view_matrix_render);
shadow_cascade_params_bind_deferred();
}
+1
View File
@@ -433,6 +433,7 @@ static void opengl_set_default_uniforms(const opengl_shader_t& sdr) {
Current_shader->program->Uniforms.setTextureUniform("PositionBuffer", 2);
Current_shader->program->Uniforms.setTextureUniform("SpecBuffer", 3);
Current_shader->program->Uniforms.setTextureUniform("shadow_map", 4);
Current_shader->program->Uniforms.setTextureUniform("shadow_map_raw", SHADOW_MAP_RAW_TEXTURE_UNIT);
break;
case SDR_TYPE_PASSTHROUGH_RENDER:
+25 -7
View File
@@ -37,6 +37,7 @@ void opengl_texture_state::init(GLuint n_units)
for (unsigned int unit = 0; unit < num_texture_units; unit++) {
units[unit].enabled = GL_FALSE;
units[unit].bound_sampler = 0;
default_values(unit);
@@ -93,17 +94,20 @@ void opengl_texture_state::Enable(GLuint tex_id)
return;
}
if (units[active_texture_unit].texture_id != tex_id) {
glBindTexture(units[active_texture_unit].texture_target, tex_id);
units[active_texture_unit].texture_id = tex_id;
}
glBindTexture(units[active_texture_unit].texture_target, tex_id);
units[active_texture_unit].texture_id = tex_id;
// A sampler that an earlier bind set belongs to the earlier texture. No GL call when
// the unit has no sampler.
BindSampler(active_texture_unit, 0);
}
void opengl_texture_state::Enable(GLuint unit, GLenum tex_target, GLuint tex_id) {
void opengl_texture_state::Enable(GLuint unit, GLenum tex_target, GLuint tex_id, GLuint sampler) {
Assertion(unit < num_texture_units, "Invalid texture unit value!");
if (units[unit].texture_target == tex_target && units[unit].texture_id == tex_id) {
// The texture unit already uses this texture. There is no need to change it
if (units[unit].texture_target == tex_target && units[unit].texture_id == tex_id
&& units[unit].bound_sampler == sampler) {
// The texture unit already uses this texture and sampler. There is no need to change it
return;
}
@@ -111,6 +115,19 @@ void opengl_texture_state::Enable(GLuint unit, GLenum tex_target, GLuint tex_id)
SetActiveUnit(unit);
SetTarget(tex_target);
Enable(tex_id);
BindSampler(unit, sampler);
}
void opengl_texture_state::BindSampler(GLuint unit, GLuint sampler)
{
Assertion(unit < num_texture_units, "Invalid texture unit value!");
if (units[unit].bound_sampler == sampler) {
return;
}
glBindSampler(unit, sampler);
units[unit].bound_sampler = sampler;
}
void opengl_texture_state::Delete(GLuint tex_id)
@@ -128,6 +145,7 @@ void opengl_texture_state::Delete(GLuint tex_id)
glBindTexture(units[i].texture_target, 0);
units[i].texture_id = 0;
BindSampler(i, 0);
default_values(i, units[i].texture_target);
+28 -2
View File
@@ -27,6 +27,12 @@ struct opengl_texture_unit {
GLenum texture_target;
GLuint texture_id;
// Sampler object bound to this unit, overriding the texture object's own filter/compare
// state for as long as it stays bound (0 = none, texture-level state applies as usual).
// Tracked here for the same reason texture_id is: so BindSampler() can skip a redundant
// glBindSampler call, exactly like Enable() already does for glBindTexture.
GLuint bound_sampler;
};
class opengl_texture_state
@@ -59,13 +65,33 @@ class opengl_texture_state
* @warning This may not actually set the active texture unit to the specified value! If you require that then
* you need to call SetActiveUnit!
*
* The sampler object is part of the unit binding: each call also sets it. So a later bind of
* a different texture on this unit removes a sampler that an earlier bind set.
*
* @param unit The texture unit to use
* @param tex_target The texture target of the texture id
* @param tex_id The ID of the texture to enable
* @param sampler The sampler object for this unit, or 0 for the texture's own sampling state
*/
void Enable(GLuint unit, GLenum tex_target, GLuint tex_id);
void Enable(GLuint unit, GLenum tex_target, GLuint tex_id, GLuint sampler = 0);
void Delete(GLuint tex_id);
/**
* @brief Binds a sampler object to the specified unit, or unbinds one if sampler is 0
*
* Prefer the sampler argument of Enable(). Use this function directly only to clear a
* sampler before it is deleted.
*
* Sampler objects override the bound texture's own filter/compare-mode state for
* that unit only, which is otherwise impossible to vary per-unit in this codebase
* (texture parameters like GL_TEXTURE_COMPARE_MODE live on the texture object, not
* a sampler). No-ops if the unit already has this sampler bound, mirroring Enable().
*
* @param unit The texture unit to bind the sampler to
* @param sampler The sampler object to bind, or 0 to unbind
*/
void BindSampler(GLuint unit, GLuint sampler);
inline GLenum GetTarget();
inline void SetShaderMode(GLboolean mode);
};
+39 -19
View File
@@ -79,6 +79,7 @@ GLint GL_max_elements_vertices = 4096;
GLint GL_max_elements_indices = 4096;
GLuint Shadow_map_depth_texture = 0;
GLuint Shadow_map_raw_sampler = 0;
GLuint shadow_fbo = 0;
int Shadow_texture_size = 0;
@@ -514,6 +515,24 @@ static bool opengl_init_shadow_framebuffer(int size)
mprintf(("Shadow framebuffer created successfully.\n"));
Shadow_texture_size = size;
Shadow_texture_layers = Num_shadow_cascades + Num_cockpit_shadow_cascades;
// Second sampler for Shadow_map_depth_texture, compare mode off, so PCSS blocker
// search (shadows.sdr) can read raw depth off the same texture that the compare
// sampler reads filtered visibility off -- see shadow_contact_hardening_supported()
// for why this needs a sampler object rather than a second glTexParameteri call.
if (shadow_contact_hardening_supported()) {
glGenSamplers(1, &Shadow_map_raw_sampler);
// GL_NEAREST: the blocker search wants the actual stored per-texel depth, not a
// hardware-filtered blend across the compare boundary -- deliberately different
// from the compare sampler's GL_LINEAR.
glSamplerParameteri(Shadow_map_raw_sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glSamplerParameteri(Shadow_map_raw_sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glSamplerParameteri(Shadow_map_raw_sampler, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glSamplerParameteri(Shadow_map_raw_sampler, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glSamplerParameteri(Shadow_map_raw_sampler, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
glSamplerParameteri(Shadow_map_raw_sampler, GL_TEXTURE_COMPARE_MODE, GL_NONE);
}
return true;
}
@@ -565,24 +584,7 @@ void opengl_tnl_init()
Transform_buffer_handle = opengl_create_texture_buffer_object();
if (Shadow_quality != ShadowQuality::Disabled) {
int size;
switch (Shadow_quality) {
case ShadowQuality::Low:
size = 512;
break;
case ShadowQuality::Medium:
size = 1024;
break;
case ShadowQuality::High:
size = 2048;
break;
case ShadowQuality::Ultra:
size = 4096;
break;
default:
size = 256;
break;
}
const int size = shadows_map_resolution();
if (!opengl_init_shadow_framebuffer(size)) {
mprintf(("Failed to create either shadow framebuffer. Disabling shadow support.\n"));
@@ -608,6 +610,15 @@ void opengl_tnl_shutdown()
shadow_cascade_params_shutdown();
if ( Shadow_map_raw_sampler ) {
// Clear the tracked bindings before deleting -- glDeleteSamplers silently unbinds
// the sampler from every unit on the GL side, which would leave GL_state's tracking
// stale (BindSampler() would then skip a rebind of a since-deleted sampler name).
GL_state.Texture.BindSampler(SHADOW_MAP_RAW_TEXTURE_UNIT, 0);
glDeleteSamplers(1, &Shadow_map_raw_sampler);
Shadow_map_raw_sampler = 0;
}
if ( Shadow_map_depth_texture ) {
glDeleteTextures(1, &Shadow_map_depth_texture);
Shadow_map_depth_texture = 0;
@@ -960,8 +971,14 @@ void opengl_tnl_set_model_material(model_material *material_info)
Current_shader->program->Uniforms.setTextureUniform("sAmbientmap", 6);
if (setAllUniforms || (flags & MODEL_SDR_FLAG_MISC))
Current_shader->program->Uniforms.setTextureUniform("sMiscmap", 7);
if (setAllUniforms || (flags & MODEL_SDR_FLAG_SHADOWS))
if (setAllUniforms || (flags & MODEL_SDR_FLAG_SHADOWS)) {
Current_shader->program->Uniforms.setTextureUniform("shadow_map", 8);
// SHADOW_MAP_RAW_TEXTURE_UNIT is reserved for this -- see the raw-sampler bind alongside
// "shadow_map" below. Set unconditionally (even when contact hardening is unsupported) for the
// same AMD unbound-uniform reason as the rest of this block; the shader's own
// penumbra_scale sentinel check is what prevents the read at runtime.
Current_shader->program->Uniforms.setTextureUniform("shadow_map_raw", SHADOW_MAP_RAW_TEXTURE_UNIT);
}
Current_shader->program->Uniforms.setTextureUniform("sFramebuffer", 9);
if (setAllUniforms || (flags & MODEL_SDR_FLAG_TRANSFORM))
Current_shader->program->Uniforms.setTextureUniform("transform_tex", 10);
@@ -1044,6 +1061,9 @@ void opengl_tnl_set_model_material(model_material *material_info)
if (material_info->is_shadow_receiving()) {
GL_state.Texture.Enable(8, GL_TEXTURE_2D_ARRAY, Shadow_map_depth_texture);
if (Shadow_map_raw_sampler) {
GL_state.Texture.Enable(SHADOW_MAP_RAW_TEXTURE_UNIT, GL_TEXTURE_2D_ARRAY, Shadow_map_depth_texture, Shadow_map_raw_sampler);
}
}
if (material_info->get_animated_effect() >= 0) {
+9
View File
@@ -35,6 +35,15 @@ extern float shadow_fardist;
extern GLuint Shadow_map_depth_texture;
// Second sampler object bound to Shadow_map_depth_texture, compare mode off, for the raw
// (uncompared) depth reads PCSS blocker search needs. Only created when
// shadow_contact_hardening_supported() is true; 0 otherwise. See gropengltnl.cpp.
extern GLuint Shadow_map_raw_sampler;
// The texture unit of the "shadow_map_raw" sampler, in the model shaders and in the deferred
// lighting shader. Unit 7 is the model misc map, so the raw sampler must not use it.
constexpr GLuint SHADOW_MAP_RAW_TEXTURE_UNIT = 11;
struct opengl_vertex_bind {
vertex_format_data::vertex_format format;
GLint size;
+50
View File
@@ -0,0 +1,50 @@
#include "graphics/rtao.h"
#include "graphics/2d.h"
#include "options/Option.h"
int Rtao_samples = 0;
// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton
auto RtaoQualityOption = options::OptionBuilder<int>("Graphics.RtaoQuality",
std::pair<const char*, int>{"Raytraced Ambient Occlusion", -1},
std::pair<const char*, int>{"Rays traced per pixel for ambient occlusion in the deferred ambient pass; Off keeps baked AO maps only", -1})
.enumerator([]() -> SCP_vector<int> {
if (rtao_supported()) {
return {0, 4, 8, 16};
}
return {0}; // inert default when RT isn't supported
})
.display(options::MapValueDisplay<int>({
{0, {"Off", -1}},
{4, {"Low", -1}},
{8, {"Medium", -1}},
{16, {"High", -1}}
}))
.bind_to(&Rtao_samples)
.flags({options::OptionFlags::ForceMultiValueSelection})
.level(options::ExpertLevel::Advanced)
.category(std::make_pair("Graphics", 1825))
.default_func([]() { return 0; })
.importance(71)
.finish();
bool rtao_supported()
{
return gr_is_capable(gr_capability::CAPABILITY_RAYTRACED_SHADOWS);
}
bool rtao_enabled()
{
// RTAO reads its inputs from the deferred global data, and the frame TLAS build also
// runs for RTAO alone. So RTAO does not depend on the shadow settings.
return Rtao_samples > 0 && rtao_supported();
}
void rtao_remove_unsupported_options()
{
if (!rtao_supported()) {
// The option has no effect without raytracing support, so remove it.
options::OptionsManager::instance()->removeOption(RtaoQualityOption);
}
}
+29
View File
@@ -0,0 +1,29 @@
#pragma once
// Raytraced ambient occlusion (RTAO): short hemisphere occlusion rays traced via
// inline ray query against the raytraced-shadow TLAS, inside the deferred ambient
// light pass (LT_AMBIENT branch of deferred-f.sdr, RTAO shader variant). Composes
// multiplicatively with baked AO maps through the shared G-buffer `ao` term, so
// env-map/IBL ambient darkens consistently for free. The AO radius and strength
// are content-scale-dependent and therefore mod-owned -- see $RTAO Radius: /
// $RTAO Strength: in lighting_profiles.h.
// Rays traced per pixel for ambient occlusion. 0 (the default) disables RTAO
// entirely; nonzero values only take effect when rtao_supported(). Clamped
// in-shader to RT_SHADOW_MAX_SAMPLES (16). See RtaoQualityOption in rtao.cpp.
extern int Rtao_samples;
// Whether the hardware/renderer can trace AO rays at all -- the same requirement
// set as raytraced shadows (Vulkan + VK_KHR_acceleration_structure +
// VK_KHR_ray_query), so this simply forwards to that capability.
bool rtao_supported();
// Whether the deferred ambient pass should trace AO this frame, i.e. the user has
// enabled it and the hardware supports it. The shadow settings have no effect on RTAO.
// This is the single source of truth --
// gate any RTAO shader-flag, TLAS-build, or uniform code on this, not on
// Rtao_samples/rtao_supported() separately.
bool rtao_enabled();
// Remove the RTAO option from the options list when the renderer cannot trace rays.
void rtao_remove_unsupported_options();
+3 -1
View File
@@ -133,6 +133,8 @@ static ShaderVariantInfo SHADER_VARIANTS[] = {
{SDR_TYPE_DEFERRED_LIGHTING, false, SDR_FLAG_DEFERRED_RT_SHADOWS, "RT_SHADOWS", {}, "Use raytraced (TLAS ray query) shadows instead of cascaded shadow maps"},
{SDR_TYPE_DEFERRED_LIGHTING, false, SDR_FLAG_DEFERRED_RTAO, "RTAO", {}, "Trace raytraced ambient occlusion in the ambient light pass"},
{SDR_TYPE_POST_PROCESS_BLUR, false, SDR_FLAG_BLUR_HORIZONTAL, "PASS_0", {}, "Horizontal blur pass"},
{SDR_TYPE_POST_PROCESS_BLUR, false, SDR_FLAG_BLUR_VERTICAL, "PASS_1", {}, "Vertical blur pass"},
@@ -221,7 +223,7 @@ bool shader_variant_requires_raytracing(shader_type type, unsigned int flags)
case SDR_TYPE_MODEL:
return (flags & MODEL_SDR_FLAG_RT_SHADOWS) != 0;
case SDR_TYPE_DEFERRED_LIGHTING:
return (flags & SDR_FLAG_DEFERRED_RT_SHADOWS) != 0;
return (flags & (SDR_FLAG_DEFERRED_RT_SHADOWS | SDR_FLAG_DEFERRED_RTAO)) != 0;
default:
return false;
}
+243 -31
View File
@@ -16,7 +16,9 @@
#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"
@@ -27,9 +29,13 @@
#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;
@@ -45,6 +51,7 @@ vec3d shadow_rt_relative(const vec3d& world_pos)
}
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;
@@ -243,13 +250,163 @@ auto RtShadowBiasMaxOption = options::OptionBuilder<float>("Graphics.RtShadowBia
.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;
}
@@ -272,6 +429,11 @@ 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;
@@ -670,6 +832,9 @@ matrix shadows_start_render(matrix *eye_orient, vec3d *eye_pos, fov_t fov, fov_t
// 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)) {
@@ -682,6 +847,18 @@ matrix shadows_start_render(matrix *eye_orient, vec3d *eye_pos, fov_t fov, fov_t
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);
@@ -837,6 +1014,10 @@ void shadows_render_all(fov_t fov, matrix *eye_orient, vec3d *eye_pos,
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;
}
@@ -845,15 +1026,8 @@ void shadows_render_all(fov_t fov, matrix *eye_orient, vec3d *eye_pos,
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();
@@ -1020,6 +1194,10 @@ void shadow_end_frame() {
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)
@@ -1027,11 +1205,23 @@ static std::pair<size_t, size_t> compute_cascade_params_size(int num_cascades) {
return {sizeof(graphics::shadow_cascade_static_data)
+ sizeof(matrix4) * num_cascades
+ sizeof(float) * (num_cascades + padding_required)
+ sizeof(float) * (num_cascades + padding_required),
+ 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;
@@ -1043,6 +1233,7 @@ void shadow_cascade_params_init() {
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() {
@@ -1062,7 +1253,7 @@ void shadow_cascade_params_bind(int cascade_offset, int cascade_count, const sha
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(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);
@@ -1074,6 +1265,7 @@ void shadow_cascade_params_bind(int cascade_offset, int cascade_count, const sha
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;
@@ -1089,19 +1281,20 @@ void shadow_cascade_params_bind(int cascade_offset, int cascade_count, const sha
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;
write_padded_floats(buffer, offset, num_cascades, padding,
[](int i) { return Shadow_cascade_distances[i]; });
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;
// 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.
@@ -1119,20 +1312,39 @@ shadow_ray_params shadow_ray_params_cockpit(const object* viewer) {
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;
if (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;
}
shadow_cascade_params_bind(offset, count, ray);
} else {
shadow_cascade_params_bind(offset, count);
}
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) {
+73
View File
@@ -17,6 +17,10 @@
class ship_info;
namespace graphics {
struct deferred_global_data;
}
struct light_frustum_info
{
matrix4 proj_matrix;
@@ -67,6 +71,32 @@ extern int Max_rt_shadow_local_lights;
extern float Rt_shadow_bias_min;
extern float Rt_shadow_bias_max;
// Rays traced per pixel per shadowed light, for lights that have a source size --
// suns from their drawn size, or from $SunAngularSize in stars.tbl when that
// overrides it; local lights via their source_radius. 1 means the single hard ray
// in traceShadowRayCone() (shadows.sdr); above that it samples a penumbra.
//
// This is a cost knob, not an appearance knob: how *wide* a penumbra is comes
// solely from the light's source size, which is what also drives the shadow-map
// path and keeps the two methods consistent for content authors. The sample count
// only decides how well that penumbra resolves.
//
// Which is why it is not a setting of its own: it follows Shadow_quality
// (Low/Medium/High/Ultra -> 1/4/8/16), the same tier that picks the shadow map's
// resolution, so one control covers shadow cost whichever method is active.
// Capped in-shader at RT_SHADOW_MAX_SAMPLES (16).
int shadows_rt_sample_count();
// Session-only override for shadows_rt_sample_count(), for iterating in the lab
// without touching a persisted setting. <= 0 means "follow Shadow_quality".
extern int Rt_shadow_samples_override;
// Edge length, in texels, of one square cascade of the shadow map, as chosen by
// Shadow_quality. Meaningless when Shadow_quality is Disabled (callers guard on that
// first); the value returned in that case is only there to keep the result usable as a
// divisor.
int shadows_map_resolution();
// Whether the current hardware/renderer can do anything with ShadowRenderMethod::Raytraced
// at all (Vulkan + VK_KHR_acceleration_structure + VK_KHR_ray_query support). Independent
// of which method is currently selected -- use this to decide whether to offer the choice.
@@ -88,6 +118,11 @@ bool shadows_use_raytracing();
// i.e. shadows_use_raytracing() is true AND the user has selected RTShadowQuality::High.
bool shadows_use_raytraced_local_lights();
// Whether this frame needs the ray-tracing TLAS: raytraced shadows with shadows on, or RTAO.
// The Vulkan backend builds the TLAS one time in each frame when this is true. See
// vulkan_scene_texture_begin().
bool shadows_rt_scene_needed();
extern matrix4 Shadow_view_matrix_light;
extern matrix4 Shadow_view_matrix_render;
@@ -105,6 +140,38 @@ constexpr uint8_t TLAS_MASK_ALL = 0xFF;
constexpr uint8_t TLAS_MASK_VIEWER_HULL = 0x80;
constexpr uint8_t SHADOW_RAY_CULL_MASK_EXCLUDE_VIEWER_HULL = static_cast<uint8_t>(~TLAS_MASK_VIEWER_HULL);
// Per-cascade PCSS penumbra scale: world-space blocker/receiver depth separation, scaled
// by the sun's tanθ and converted to UV space, gives the contact-hardened penumbra radius
// directly (see the derivation comment above shadow_smoothness_scale() in shadows.cpp).
// A negative value is a sentinel meaning shadow_contact_hardening_enabled() was false
// when it was computed -- the shader falls back to the fixed smoothness_factors[cascade].
extern SCP_vector<float> Shadow_penumbra_scale;
// Whether the current renderer can do a raw (uncompared) depth read off the shadow map,
// which PCSS blocker search needs. True on Vulkan always; on OpenGL requires
// GL 3.3 (a second sampler object with compare mode off, bound to the
// same shadow texture -- see shadow_map_raw in shadows.sdr). Independent of whether the
// user currently has contact hardening turned on -- use this to decide whether to offer
// the option at all.
bool shadow_contact_hardening_supported();
// User-facing toggle for shadow contact hardening (PCSS blocker search): sizes each
// cascade's penumbra from actual occluder distance instead of the fixed
// $Shadow Smoothness Factor: width. Costs an extra raw-depth search per shadowed pixel
// (see pcssBlockerSearch() in shadows.sdr), so this is exposed as a setting rather than
// being unconditional -- persisted via Graphics.ShadowContactHardening. See
// shadow_contact_hardening_enabled() for the flag that actually gates the per-frame cost.
extern bool Shadow_contact_hardening_enabled;
// Whether shadow rendering should actually do the contact-hardening blocker search this
// frame, i.e. shadow_contact_hardening_supported() is true AND the user has the option on.
// This is the single source of truth -- gate any per-cascade penumbra-scale computation on
// this, not on shadow_contact_hardening_supported()/Shadow_contact_hardening_enabled
// separately. When false, Shadow_penumbra_scale gets the same negative sentinel as
// "unsupported", so the shader takes the pre-contact-hardening fixed-width path at zero
// extra cost (see pcssPenumbraRadius() in shadows.sdr).
bool shadow_contact_hardening_enabled();
void shadows_construct_light_frustum(vec3d *min_out, vec3d *max_out, vec3d light_vec, matrix *orient, vec3d *pos, fov_t fov, float aspect, float z_near, float z_far);
bool shadows_obj_in_frustum(object *objp, vec3d *min, vec3d *max, matrix *light_orient);
void shadows_render_all(fov_t fov, matrix *eye_orient, vec3d *eye_pos,
@@ -154,6 +221,12 @@ void shadow_cascade_params_bind(int cascade_offset, int cascade_count, const sha
// full-screen passes, which shade one frame per pass.
void shadow_cascade_params_bind_deferred();
// Writes the view and ray-tracing fields of the global data of the deferred lighting pass:
// inv_view_matrix, the ray origin and cull mask of the current Lighting_mode, the ray bias,
// and the RTAO settings. Both backends call this one time for each deferred lighting pass.
// These fields do not depend on the shadow cascades, so RTAO also works with shadows off.
void shadows_fill_deferred_global_data(graphics::deferred_global_data& data);
// True when the viewer's cockpit model also gets a shadow pass (raster and RT).
bool shadows_cockpit_casts_shadow(const ship_info* sip);
+30 -4
View File
@@ -2,6 +2,8 @@
#include "globalincs/pstypes.h"
#include <cstddef>
using SPIRV_FLOAT_MAT_4x4 = matrix4;
using SPIRV_FLOAT_VEC4 = vec4;
@@ -23,9 +25,24 @@ struct deferred_global_data {
float invScreenWidth;
float invScreenHeight;
float nearPlane;
// RTAO rays for each pixel. 0 when RTAO is off.
int rtaoSampleCount;
float pad;
// The ray-tracing inputs of the deferred pass, for raytraced shadows and RTAO. They are
// here and not in shadowCascadeParams, because the deferred pass always binds this block,
// also when shadows are off. shadows_fill_deferred_global_data() (shadows.cpp) sets them.
// rayViewOrigin has the same meaning as shadow_cascade_static_data::shadow_ray_view_origin.
// The int after the vec3 fills the std140 padding of the vec3.
vec3d rayViewOrigin;
int rayCullMask;
float rayBiasMin;
float rayBiasMax;
float rtaoRadius;
float rtaoStrength;
};
static_assert(offsetof(deferred_global_data, rayViewOrigin) == 80 && sizeof(deferred_global_data) == 112,
"deferred_global_data must match the std140 layout of the globalDeferredData block in deferred-f.sdr");
/**
* @brief Data for one deferred light rendering call
@@ -146,6 +163,11 @@ struct shadow_cascade_static_data {
int cascade_count;
float rtShadowBiasMin;
float rtShadowBiasMax;
int rtShadowSampleCount;
// Padding. The scalars must fill 32 bytes, so that std140 puts the following mat4
// directly after them. Keep this struct and the shadowCascadeParams block in
// shadow_cascade_params.sdr the same.
int pad[3];
matrix4 shadow_mv_matrix;
// Position of the view-space origin (the camera) in TLAS-relative space. The RT shadow
@@ -173,13 +195,17 @@ struct shadow_cascade_static_data {
// main shadow cascades. Set in shadow_cascade_params_bind() (shadows.cpp).
int shadow_ray_cull_mask;
};
static_assert(offsetof(shadow_cascade_static_data, shadow_mv_matrix) == 32,
"shadow_cascade_static_data's scalar fields must total exactly 32 bytes to match std140's "
"mat4 alignment -- update the shadowCascadeParams block in shadow_cascade_params.sdr "
"if this changes.");
// Must match the GLSL shadowCascadeParams block's implicit std140 padding
// exactly (16 [4 leading scalars] + 64 [matrix4] + 12 [shadow_ray_view_origin]
// + 4 [shadow_ray_cull_mask, packed into the vec3's alignment padding] = 96) --
// exactly (32 [5 leading scalars + 3 padding] + 64 [matrix4] + 12 [shadow_ray_view_origin]
// + 4 [shadow_ray_cull_mask, packed into the vec3's alignment padding] = 112) --
// shadow_cascade_params_bind() packs shadow_proj_matrix[] immediately after
// this struct via sizeof(), so a mismatch here silently shifts every cascade
// matrix in the buffer, in both backends.
static_assert(sizeof(shadow_cascade_static_data) == 96, "shadow_cascade_static_data must match the GLSL shadowCascadeParams layout (see comment above)");
static_assert(sizeof(shadow_cascade_static_data) == 112, "shadow_cascade_static_data must match the GLSL shadowCascadeParams layout (see comment above)");
enum class NanoVGShaderType: int32_t {
FillGradient = 0, FillImage = 1, Simple = 2, Image = 3
+6
View File
@@ -118,6 +118,12 @@ public:
*/
uint32_t getCurrentFrame() const { return m_currentFrame; }
/**
* @brief Get the monotonic frame number set by setCurrentFrame (total frames
* rendered, never wraps back -- unlike getCurrentFrame()'s in-flight slot index)
*/
uint64_t getCurrentFrameNumber() const { return m_currentFrameNumber; }
/**
* @brief Get the Vulkan logical device
*/
+20 -19
View File
@@ -683,10 +683,7 @@ void vulkan_shadow_map_start(matrix4* shadow_view_matrix, const matrix* light_ma
}
}
// vulkan_build_shadow_tlas() (called just before this, every frame shadows
// are enabled) already ends whatever render pass was active -- TLAS builds
// are illegal inside a render pass instance -- and nulls out the tracked
// render pass. Only end it here if that didn't already happen.
// End the active render pass, if the state tracker has one.
if (stateTracker->getCurrentRenderPass()) {
cmd.endRenderPass();
}
@@ -831,22 +828,26 @@ void vulkan_shadow_map_end()
renderer->resumeSceneRendering();
}
// Restore viewport and scissor to scene size
{
vk::Viewport viewport;
viewport.x = static_cast<float>(gr_screen.offset_x);
viewport.y = static_cast<float>(gr_screen.offset_y);
viewport.width = static_cast<float>(gr_screen.clip_width);
viewport.height = static_cast<float>(gr_screen.clip_height);
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;
cmd.setViewport(0, viewport);
vulkan_restore_scene_viewport();
}
vk::Rect2D scissor;
scissor.offset = vk::Offset2D(gr_screen.offset_x, gr_screen.offset_y);
scissor.extent = vk::Extent2D(static_cast<uint32_t>(gr_screen.clip_width), static_cast<uint32_t>(gr_screen.clip_height));
cmd.setScissor(0, scissor);
}
void vulkan_restore_scene_viewport()
{
vk::CommandBuffer cmd = getStateTracker()->getCommandBuffer();
vk::Viewport viewport;
viewport.x = static_cast<float>(gr_screen.offset_x);
viewport.y = static_cast<float>(gr_screen.offset_y);
viewport.width = static_cast<float>(gr_screen.clip_width);
viewport.height = static_cast<float>(gr_screen.clip_height);
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;
cmd.setViewport(0, viewport);
vk::Rect2D scissor;
scissor.offset = vk::Offset2D(gr_screen.offset_x, gr_screen.offset_y);
scissor.extent = vk::Extent2D(static_cast<uint32_t>(gr_screen.clip_width), static_cast<uint32_t>(gr_screen.clip_height));
cmd.setScissor(0, scissor);
}
// ========== Decal Pass ==========
+4
View File
@@ -24,6 +24,10 @@ void vulkan_override_fog(bool set_override);
void vulkan_shadow_map_start(matrix4* shadow_view_matrix, const matrix* light_matrix, vec3d* eye_pos, bool first_pass);
void vulkan_shadow_map_end();
// Sets the viewport and scissor to the current gr_screen clip area. Call this after a render
// pass resumes in the middle of a scene, because the resume sets a full-extent viewport.
void vulkan_restore_scene_viewport();
// Decal pass
void vulkan_start_decal_pass();
void vulkan_stop_decal_pass();
@@ -15,6 +15,7 @@ static constexpr DescriptorBindingTemplate s_globalBindings[] = {
{GlobalBinding::EnvMap, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment, vk::ImageViewType::eCube},
{GlobalBinding::IrradianceMap, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment, vk::ImageViewType::eCube},
{GlobalBinding::ShadowCascadeParams, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
{GlobalBinding::ShadowMapRaw, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment, vk::ImageViewType::e2DArray},
};
static constexpr DescriptorBindingTemplate s_globalTlasBinding{
GlobalBinding::Tlas, vk::DescriptorType::eAccelerationStructureKHR, 1, vk::ShaderStageFlagBits::eFragment};
@@ -78,6 +78,7 @@ namespace GlobalBinding {
static constexpr uint32_t IrradianceMap = 4; // samplerCube: irradiance map
static constexpr uint32_t Tlas = 5; // accelerationStructureEXT: raytraced shadow TLAS (only present when raytraced shadows are supported)
static constexpr uint32_t ShadowCascadeParams = 6; // UBO: shadow cascade projection matrices/distances (per-frame, shared by all consumers)
static constexpr uint32_t ShadowMapRaw = 7; // sampler2DArray: raw (non-compare) shadow map read, for PCSS blocker search
}
// Material Set (Set 1) bindings — per-material data
+1
View File
@@ -1437,6 +1437,7 @@ bool VulkanDrawManager::applyMaterial(material* mat, primitive_type prim_type, v
bindPendingUBOs(DescriptorSetIndex::Global);
if (shadowReady) {
writer.setImage(GlobalBinding::ShadowMap, pp->getShadowTextureInfo());
writer.setImage(GlobalBinding::ShadowMapRaw, pp->getShadowRawTextureInfo());
}
m_cachedGlobalHadShadow = shadowReady;
m_globalSetDirty = false;
+25 -1
View File
@@ -11,6 +11,7 @@
#include "VulkanTexture.h"
#include "VulkanRenderer.h"
#include "VulkanPostProcessing.h"
#include "VulkanDeferred.h"
#include "VulkanDescriptorManager.h"
#include "VulkanDeletionQueue.h"
#include "VulkanMemory.h"
@@ -216,7 +217,30 @@ void vulkan_scene_texture_begin()
// Switch to HDR scene render pass when post-processing is enabled
auto* pp = getPostProcessor();
if (pp && pp->isInitialized() && Gr_post_processing_enabled && !PostProcessing_override) {
const bool useSceneTarget = pp && pp->isInitialized() && Gr_post_processing_enabled && !PostProcessing_override;
// Build the ray-tracing TLAS for this frame here, one time, before the scene draws start.
// The camera and the objects are final at this point. A TLAS build must be outside a render
// pass. The scene pass starts below, so the build adds no pass break to that path.
if (vulkan_frame_tlas_pending() && !renderer->isSceneRendering()) {
renderer->endCurrentRenderPass();
vulkan_build_frame_tlas();
if (!useSceneTarget) {
// No scene pass follows, so resume the pass that the build interrupted.
auto* texMgr = getTextureManager();
const int rtHandle = (renderer->isRenderTargetActive() && texMgr) ? texMgr->getCurrentRenderTarget() : -1;
auto* rtSlot = rtHandle >= 0 ? texMgr->getTextureSlot(rtHandle) : nullptr;
if (rtSlot != nullptr) {
renderer->resumeRenderTargetPass(rtSlot);
} else {
renderer->resumeSwapChainPass();
}
vulkan_restore_scene_viewport();
}
}
if (useSceneTarget) {
renderer->beginSceneRendering();
High_dynamic_range = true;
} else {
@@ -204,6 +204,7 @@ public:
vk::ImageView depthView() const { return m_depth.view; }
vk::Image depthImage() const { return m_depth.image; }
vk::Sampler compareSampler() const { return m_compareSampler; }
vk::Sampler rawSampler() const { return m_rawSampler; }
vk::RenderPass renderPass() const { return m_renderPass; }
vk::Framebuffer framebuffer() const { return m_framebuffer; }
@@ -211,6 +212,7 @@ private:
PostProcessContext* m_ctx = nullptr;
RenderTarget m_depth; // D32F, 2D array (Num_shadow_cascades + Num_cockpit_shadow_cascades layers)
vk::Sampler m_compareSampler; // Depth-compare sampler for hardware PCF (sampler2DArrayShadow)
vk::Sampler m_rawSampler; // Non-compare sampler on the same view, for PCSS blocker search
vk::RenderPass m_renderPass;
vk::Framebuffer m_framebuffer;
int m_textureSize = 0;
@@ -1111,6 +1113,16 @@ public:
return {m_shadow.compareSampler(), m_shadow.depthView(), vk::ImageLayout::eShaderReadOnlyOptimal};
}
/**
* @brief Get a ready-to-use DescriptorImageInfo for raw (uncompared) shadow map reads
*
* Same image view as getShadowTextureInfo(), but with compare mode off, for the PCSS
* blocker search (which needs actual depth values, not a compare result).
*/
vk::DescriptorImageInfo getShadowRawTextureInfo() const {
return {m_shadow.rawSampler(), m_shadow.depthView(), vk::ImageLayout::eShaderReadOnlyOptimal};
}
// ========== Fog / Volumetric Nebula ==========
/**
@@ -13,6 +13,7 @@
#include "graphics/grinternal.h"
#include "graphics/light.h"
#include "graphics/matrix.h"
#include "graphics/rtao.h"
#include "graphics/shadows.h"
#include "graphics/2d.h"
#include "bmpman/bmpman.h"
@@ -509,9 +510,9 @@ void VulkanDeferredLighting::render(vk::CommandBuffer cmd)
header->invScreenWidth = 1.0f / gr_screen.max_w;
header->invScreenHeight = 1.0f / gr_screen.max_h;
header->nearPlane = gr_near_plane;
shadows_fill_deferred_global_data(*header);
if (m_shadow->isInitialized() && Shadow_quality != ShadowQuality::Disabled) {
vm_inverse_matrix4(&header->inv_view_matrix, &Shadow_view_matrix_render);
shadow_cascade_params_bind_deferred();
}
}
@@ -687,6 +688,9 @@ void VulkanDeferredLighting::render(vk::CommandBuffer cmd)
if (rtShadowsActive) {
lightShaderFlags |= SDR_FLAG_DEFERRED_RT_SHADOWS;
}
if (rtao_enabled()) {
lightShaderFlags |= SDR_FLAG_DEFERRED_RTAO;
}
if (envMapAvailable) {
lightShaderFlags |= SDR_FLAG_ENV_MAP;
}
@@ -754,8 +758,10 @@ void VulkanDeferredLighting::render(vk::CommandBuffer cmd)
// Shadow map is depth-only, sampled with a depth-compare sampler
// (sampler2DArrayShadow) for hardware PCF.
vk::DescriptorImageInfo shadowTexInfo;
vk::DescriptorImageInfo shadowRawTexInfo;
if (m_shadow->isInitialized() && m_shadow->depthView()) {
shadowTexInfo = {m_shadow->compareSampler(), m_shadow->depthView(), vk::ImageLayout::eShaderReadOnlyOptimal};
shadowRawTexInfo = {m_shadow->rawSampler(), m_shadow->depthView(), vk::ImageLayout::eShaderReadOnlyOptimal};
}
// Shadow cascade params (projection matrices/distances) are bound per-frame via
@@ -801,6 +807,7 @@ void VulkanDeferredLighting::render(vk::CommandBuffer cmd)
writer.setImage(GlobalBinding::EnvMap, envTexInfo);
writer.setImage(GlobalBinding::IrradianceMap, irrTexInfo);
writer.setBuffer(GlobalBinding::ShadowCascadeParams, shadowCascadeParamsInfo);
writer.setImage(GlobalBinding::ShadowMapRaw, shadowRawTexInfo);
// Set 1: Material
vk::DescriptorSet materialSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::Material);
@@ -34,14 +34,7 @@ bool VulkanShadowMap::init(PostProcessContext& ctx)
return false;
}
int size;
switch (Shadow_quality) {
case ShadowQuality::Low: size = 512; break;
case ShadowQuality::Medium: size = 1024; break;
case ShadowQuality::High: size = 2048; break;
case ShadowQuality::Ultra: size = 4096; break;
default: size = 512; break;
}
const int size = shadows_map_resolution();
const auto layers = static_cast<uint32_t>(Num_shadow_cascades + Num_cockpit_shadow_cascades);
nprintf(("vulkan", "VulkanPostProcessor: Creating %dx%d shadow map (%d cascades)\n", size, size, layers));
@@ -188,6 +181,32 @@ bool VulkanShadowMap::init(PostProcessContext& ctx)
}
}
// Raw (non-compare) sampler on the same depth view, for the PCSS blocker search --
// it needs actual stored depth values, not a compare result. GL_NEAREST-equivalent
// filtering: the blocker search wants the exact per-texel depth, not a hardware-
// filtered blend across the compare boundary (deliberately different from the compare
// sampler's linear filtering above). Vulkan samplers are independent of images, so this
// is always available -- no capability gate needed here (contrast the OpenGL backend,
// which needs GL 3.3 because compare mode there is texture-object state).
{
vk::SamplerCreateInfo samplerInfo;
samplerInfo.magFilter = vk::Filter::eNearest;
samplerInfo.minFilter = vk::Filter::eNearest;
samplerInfo.mipmapMode = vk::SamplerMipmapMode::eNearest;
samplerInfo.addressModeU = vk::SamplerAddressMode::eClampToEdge;
samplerInfo.addressModeV = vk::SamplerAddressMode::eClampToEdge;
samplerInfo.addressModeW = vk::SamplerAddressMode::eClampToEdge;
samplerInfo.compareEnable = VK_FALSE;
samplerInfo.borderColor = vk::BorderColor::eFloatOpaqueWhite;
try {
m_rawSampler = m_ctx->device.createSampler(samplerInfo);
} catch (const vk::SystemError& e) {
nprintf(("vulkan", "VulkanPostProcessor: Failed to create shadow raw sampler: %s\n", e.what()));
return false;
}
}
m_textureSize = size;
m_initialized = true;
nprintf(("vulkan", "VulkanPostProcessor: Shadow map initialized (%dx%d, %d cascades)\n", size, size, layers));
@@ -204,6 +223,10 @@ void VulkanShadowMap::shutdown()
m_ctx->device.destroySampler(m_compareSampler);
m_compareSampler = nullptr;
}
if (m_rawSampler) {
m_ctx->device.destroySampler(m_rawSampler);
m_rawSampler = nullptr;
}
if (m_framebuffer) {
m_ctx->device.destroyFramebuffer(m_framebuffer);
m_framebuffer = nullptr;
+13
View File
@@ -119,6 +119,15 @@ public:
*/
bool isEnabled() const { return m_enabled; }
/**
* @brief Whether buildTlas() already ran in the current frame. game_render_frame() can run
* more than once in a frame (for example, one time for each OpenXR eye), but one build in
* each frame is sufficient.
*/
bool isTlasBuiltThisFrame() const {
return m_bufferManager != nullptr && m_lastTlasBuildFrame == m_bufferManager->getCurrentFrameNumber();
}
/**
* @brief A permanent, 0-instance TLAS built once at init(). Used as a
* valid (non-null) descriptor value in place of getTlasForShaderBinding()
@@ -275,6 +284,10 @@ private:
// gated behind m_enabled/m_initialized so this only matters defensively.
uint32_t currentFrameIndex() const { return m_bufferManager != nullptr ? m_bufferManager->getCurrentFrame() : 0; }
// Monotonic frame number of the last buildTlas() that ran. buildTlas() uses it to build
// only one time in each frame. See isTlasBuiltThisFrame().
uint64_t m_lastTlasBuildFrame = UINT64_MAX;
vk::Device m_device;
vk::PhysicalDevice m_physicalDevice;
VulkanMemoryManager* m_memoryManager = nullptr;
@@ -425,6 +425,12 @@ void VulkanRaytracingManager::buildTlas()
return;
}
// Build only one time in each frame. See isTlasBuiltThisFrame().
if (isTlasBuiltThisFrame()) {
return;
}
m_lastTlasBuildFrame = m_bufferManager->getCurrentFrameNumber();
// Each frame-in-flight slot owns its own instance/TLAS/scratch buffers (see
// FrameTlasResources' declaration for why a single shared set would race
// across overlapping in-flight frames).
+3 -2
View File
@@ -273,8 +273,9 @@ void VulkanRenderer::beginSceneRendering()
return;
}
// End the current swap chain render pass
m_currentCommandBuffer.endRenderPass();
// End the current swap chain render pass. vulkan_scene_texture_begin() can end it before
// this call, for the TLAS build, so end it only when it is still open.
endCurrentRenderPass();
// Use G-buffer render pass when deferred lighting is enabled and G-buffer is ready
m_useGbufRenderPass = m_postProcessor->deferred().isInitialized() && light_deferred_enabled();
+28 -27
View File
@@ -75,32 +75,6 @@ void vulkan_model_unloaded(int pm_id)
}
}
void vulkan_build_shadow_tlas()
{
if (auto* rt = getRaytracingManager()) {
// vkCmdBuildAccelerationStructuresKHR (and the memory barrier that follows
// it in buildTlas()) must be recorded outside any render pass instance.
// This is called once per frame, before the shadow map render pass begins,
// while the previous render pass (G-buffer/scene) is typically still
// active -- end it here. vulkan_shadow_map_start()'s first_pass branch
// skips its own endRenderPass() call when it finds none active.
auto* stateTracker = getStateTracker();
if (stateTracker->getCurrentRenderPass()) {
stateTracker->getCommandBuffer().endRenderPass();
stateTracker->setRenderPass(vk::RenderPass());
}
rt->buildTlas();
// Refresh the Global set's live TLAS fallback so every writeSet(Global)
// this frame picks up the freshly built TLAS automatically -- see
// DescriptorFallbacks::shadowTlas.
getDescriptorManager()->setCurrentShadowTlas(rt->getTlasForShaderBinding());
// The memoized per-frame Global set (VulkanDrawManager) must rebuild so the
// new TLAS reaches subsequent draws' Set 0 instead of a cached stale one.
getDrawManager()->invalidateGlobalSet();
}
}
bool vulkan_is_capable(gr_capability capability)
{
switch (capability) {
@@ -162,6 +136,10 @@ bool vulkan_is_capable(gr_capability capability)
return false;
case gr_capability::CAPABILITY_RAYTRACED_SHADOWS:
return getRendererInstance()->supportsRaytracedShadows();
case gr_capability::CAPABILITY_SHADOW_CONTACT_HARDENING:
// Vulkan samplers are independent of images, so a second (non-compare) sampler on
// the same shadow map view is always available -- no capability gap here.
return true;
}
return false;
}
@@ -503,7 +481,6 @@ void init_function_pointers()
gr_screen.gf_model_loaded = vulkan_model_loaded;
gr_screen.gf_model_unloaded = vulkan_model_unloaded;
gr_screen.gf_build_shadow_tlas = vulkan_build_shadow_tlas;
gr_screen.gf_update_transform_buffer = vulkan_update_transform_buffer;
gr_screen.gf_update_buffer_data = vulkan_update_buffer_data;
@@ -601,6 +578,30 @@ void init_function_pointers()
} // anonymous namespace
bool vulkan_frame_tlas_pending()
{
auto* rt = getRaytracingManager();
return rt != nullptr && rt->isEnabled() && shadows_rt_scene_needed() && !rt->isTlasBuiltThisFrame();
}
void vulkan_build_frame_tlas()
{
auto* rt = getRaytracingManager();
if (rt == nullptr) {
return;
}
// vkCmdBuildAccelerationStructuresKHR and the barrier after it must be recorded outside a
// render pass instance. The caller makes sure that no render pass is open.
rt->buildTlas();
// Refresh the live TLAS value of the Global set, so that each writeSet(Global) in this
// frame gets the new TLAS. See DescriptorFallbacks::shadowTlas.
getDescriptorManager()->setCurrentShadowTlas(rt->getTlasForShaderBinding());
// The memoized Global set of VulkanDrawManager must rebuild, so that the next draws get
// the new TLAS and not a stale cached set.
getDrawManager()->invalidateGlobalSet();
}
void initialize_function_pointers() {
init_function_pointers();
}
+9
View File
@@ -11,6 +11,15 @@ bool initialize(std::unique_ptr<os::GraphicsOperations>&& graphicsOps);
VulkanRenderer* getRendererInstance();
// True when this frame needs a ray-tracing TLAS (see shadows_rt_scene_needed()) and no build
// for this frame occurred yet.
bool vulkan_frame_tlas_pending();
// Builds the ray-tracing TLAS for this frame. vulkan_scene_texture_begin() calls this once per
// frame, before the scene render pass starts. Call it only when no render pass is open, because
// acceleration-structure builds are not permitted inside a render pass.
void vulkan_build_frame_tlas();
void cleanup();
} // namespace graphics::vulkan
-1
View File
@@ -599,7 +599,6 @@ void HudGaugeTargetBox::renderTargetSetup(vec3d *camera_eye, matrix *camera_orie
g3_start_frame(1); // Turn on zbuffering
hud_save_restore_camera_data(1);
g3_set_view_matrix( camera_eye, camera_orient, zoom);
model_set_detail_level(1); // use medium detail level
setClip(position[0] + Viewport_offsets[0], position[1] + Viewport_offsets[1], Viewport_w, Viewport_h);
+123
View File
@@ -528,6 +528,32 @@ void LabUi::build_shadow_method_combobox()
}
}
static const char* rt_shadow_quality_settings[] = {
"Low (directional lights only)",
"High (also point/tube/cone lights)",
};
void LabUi::build_rt_shadow_quality_combobox()
{
// Only meaningful when raytraced shadows are actually the active method.
if (!shadows_raytracing_supported() || Shadow_render_method != ShadowRenderMethod::Raytraced) {
return;
}
with_Combo("Raytraced Shadow Quality", rt_shadow_quality_settings[static_cast<int>(Rt_shadow_quality)])
{
for (int n = 0; n < IM_ARRAYSIZE(rt_shadow_quality_settings); n++) {
bool is_selected = static_cast<int>(Rt_shadow_quality) == n;
if (Selectable(rt_shadow_quality_settings[n], is_selected))
LabRenderer::setRtShadowQuality(static_cast<RTShadowQuality>(n));
if (is_selected)
SetItemDefaultFocus();
}
}
}
void LabUi::build_max_rt_shadow_lights_slider()
{
// Only meaningful when raytraced shadows are actually the active method.
@@ -539,6 +565,19 @@ void LabUi::build_max_rt_shadow_lights_slider()
if (SliderInt("Max Raytraced Shadow Lights", &count, 1, 8)) {
LabRenderer::setMaxRtShadowLights(count);
}
// The local-light cap is only consulted at High quality, so don't offer it at Low --
// same as how the sun size slider only appears once its override is on.
if (Rt_shadow_quality != RTShadowQuality::High) {
return;
}
// 0 is a useful setting rather than a degenerate one: it turns local-light shadows off
// without leaving High, which isolates what the directional lights are contributing.
int local_count = Max_rt_shadow_local_lights;
if (SliderInt("Max Raytraced Local Shadow Lights", &local_count, 0, 64)) {
LabRenderer::setMaxRtShadowLocalLights(local_count);
}
}
void LabUi::build_rt_shadow_bias_sliders()
@@ -559,6 +598,84 @@ void LabUi::build_rt_shadow_bias_sliders()
}
}
void LabUi::build_shadow_penumbra_sliders()
{
// The sample count is a raytracing cost knob, so it only appears when raytraced shadows
// are actually the active method. The sun size below is deliberately not gated that way.
if (shadows_raytracing_supported() && Shadow_render_method == ShadowRenderMethod::Raytraced) {
// Starts at whatever Shadow_quality implies; moving it pins a session-only
// override, so the slider stops tracking the quality tier from then on.
int samples = shadows_rt_sample_count();
if (SliderInt("Raytraced Shadow Samples", &samples, 1, 16)) {
LabRenderer::setRtShadowSamples(samples);
}
}
// Contact hardening is a shadow-map-only technique (see pcssPenumbraRadius() in
// shadows.sdr) -- raytraced shadows size their penumbra from traceShadowRayCone()'s
// cone sampling instead, so this has no effect there and isn't worth showing.
if (shadow_contact_hardening_supported() && Shadow_render_method == ShadowRenderMethod::ShadowMap) {
bool contact_hardening = Shadow_contact_hardening_enabled;
if (Checkbox("Shadow Contact Hardening", &contact_hardening)) {
LabRenderer::setShadowContactHardening(contact_hardening);
}
}
// Overrides whatever size the suns would otherwise use -- their $SunAngularSize, or
// the size measured from their bitmaps -- so penumbras can be tried out against any
// background; the checkbox's default slider value is Sol's apparent diameter.
//
// Shown for both shadow methods, because this is the one parameter that drives softness
// in both: it sizes the raytraced penumbra cone and scales the shadow map's filter width
// (see shadow_smoothness_scale() in shadows.cpp). Sitting on the slider and flipping the
// render method is the intended way to check the two against each other.
//
// It needs a mission background loaded to do anything: it overrides the size of drawn
// suns, and with no background the lab lights the scene from common_setup_room_lights()
// instead, which draws no suns for this to override.
if (Shadow_quality == ShadowQuality::Disabled) {
return;
}
bool override_sun = Sun_angular_size_override >= 0.0f;
if (Checkbox("Override Sun Angular Size", &override_sun)) {
LabRenderer::setSunAngularSizeOverride(override_sun ? SUN_ANGULAR_SIZE_SOL : SUN_ANGULAR_SIZE_UNSPECIFIED);
}
if (override_sun) {
float sun_size = Sun_angular_size_override;
if (SliderFloat("Sun Angular Size (degrees)", &sun_size, 0.0f, 10.0f)) {
LabRenderer::setSunAngularSizeOverride(sun_size);
}
}
}
void LabUi::build_rtao_sliders()
{
// Unlike the RT shadow sliders, RTAO (raytraced ambient occlusion) does not depend on
// the shadow method or the shadow quality. It only needs ray-query support.
if (!rtao_supported()) {
return;
}
int samples = Rtao_samples;
if (SliderInt("Ambient Occlusion Rays", &samples, 0, 16)) {
LabRenderer::setRtaoSamples(samples);
}
// Radius/strength are the mod-owned lighting-profile values ($RTAO Radius /
// $RTAO Strength); these override the active profile for this session only,
// same as the exposure/tonemapper controls below.
float radius = lighting_profiles::current_rtao_radius();
if (SliderFloat("Ambient Occlusion Radius", &radius, 0.0f, 200.0f)) {
lighting_profiles::lab_set_rtao_radius(radius);
}
float strength = lighting_profiles::current_rtao_strength();
if (SliderFloat("Ambient Occlusion Strength", &strength, 0.0f, 2.0f)) {
lighting_profiles::lab_set_rtao_strength(strength);
}
}
namespace ltp = lighting_profiles;
using namespace ltp;
@@ -658,10 +775,16 @@ void LabUi::show_render_options()
build_shadow_method_combobox();
build_rt_shadow_quality_combobox();
build_max_rt_shadow_lights_slider();
build_rt_shadow_bias_sliders();
build_shadow_penumbra_sliders();
build_rtao_sliders();
build_tone_mapper_combobox();
if (ltp::current_tonemapper() == TonemapperAlgorithm::PPC ||
+3
View File
@@ -44,8 +44,11 @@ class LabUi {
void build_texture_quality_combobox();
void build_antialiasing_combobox();
static void build_shadow_method_combobox();
static void build_rt_shadow_quality_combobox();
static void build_max_rt_shadow_lights_slider();
static void build_rt_shadow_bias_sliders();
static void build_shadow_penumbra_sliders();
static void build_rtao_sliders();
void build_tone_mapper_combobox();
void build_model_info_box(ship_info* sip, polymodel* pm) const;
void build_subsystem_list(object* objp, ship* shipp) const;
+49
View File
@@ -2,6 +2,9 @@
#include "lab/labv2_internal.h"
#include "lab/manager/lab_manager.h"
#include "gamesequence/gamesequence.h"
#include "graphics/rtao.h"
#include "graphics/shadows.h"
#include "starfield/starfield.h"
static std::unique_ptr<LabManager> LMGR;
@@ -13,11 +16,57 @@ const std::unique_ptr<LabManager> &getLabManager() {
return LMGR;
}
namespace {
// The lab changes these settings directly, so we save them when the lab opens and restore them when it closes.
// Each LabRenderer setter that writes a game global must have a field here.
struct LabSavedSettings {
ShadowRenderMethod shadow_render_method;
RTShadowQuality rt_shadow_quality;
int max_rt_shadow_lights;
int max_rt_shadow_local_lights;
float rt_shadow_bias_min;
float rt_shadow_bias_max;
int rt_shadow_samples_override;
float sun_angular_size_override;
int rtao_samples;
bool shadow_contact_hardening_enabled;
};
bool Lab_settings_saved = false;
LabSavedSettings Lab_saved_settings;
}
void lab_init() {
gr_set_clear_color(0, 0, 0);
Lab_saved_settings = {Shadow_render_method,
Rt_shadow_quality,
Max_rt_shadow_lights,
Max_rt_shadow_local_lights,
Rt_shadow_bias_min,
Rt_shadow_bias_max,
Rt_shadow_samples_override,
Sun_angular_size_override,
Rtao_samples,
Shadow_contact_hardening_enabled};
Lab_settings_saved = true;
}
void lab_close(bool reset) {
if (Lab_settings_saved) {
Shadow_render_method = Lab_saved_settings.shadow_render_method;
Rt_shadow_quality = Lab_saved_settings.rt_shadow_quality;
Max_rt_shadow_lights = Lab_saved_settings.max_rt_shadow_lights;
Max_rt_shadow_local_lights = Lab_saved_settings.max_rt_shadow_local_lights;
Rt_shadow_bias_min = Lab_saved_settings.rt_shadow_bias_min;
Rt_shadow_bias_max = Lab_saved_settings.rt_shadow_bias_max;
Rt_shadow_samples_override = Lab_saved_settings.rt_shadow_samples_override;
Sun_angular_size_override = Lab_saved_settings.sun_angular_size_override;
Rtao_samples = Lab_saved_settings.rtao_samples;
Shadow_contact_hardening_enabled = Lab_saved_settings.shadow_contact_hardening_enabled;
Lab_settings_saved = false;
}
if (reset) {
LMGR.reset();
}
+43
View File
@@ -3,6 +3,7 @@
#include "globalincs/pstypes.h"
#include "globalincs/flagset.h"
#include "graphics/2d.h"
#include "graphics/rtao.h"
#include "graphics/shadows.h"
#include "lighting/lighting_profiles.h"
#include "camera/camera.h"
@@ -130,12 +131,27 @@ public:
Shadow_render_method = method;
}
// Session-only override, same as setShadowRenderMethod -- does not touch the
// persisted Raytraced Shadow Quality option. Safe to change at any time: the
// only thing it gates is whether local lights are picked as shadow casters
// while filling the light uniforms, which happens fresh every frame, so
// nothing has to be rebuilt for the switch to take effect.
static void setRtShadowQuality(RTShadowQuality quality) {
Rt_shadow_quality = quality;
}
// Session-only override, same as setShadowRenderMethod -- does not touch the
// persisted Max Raytraced Shadow Lights option.
static void setMaxRtShadowLights(int count) {
Max_rt_shadow_lights = count;
}
// Session-only override, same as setMaxRtShadowLights -- does not touch the persisted
// Max Raytraced Local Shadow Lights option. Only consulted at RTShadowQuality::High.
static void setMaxRtShadowLocalLights(int count) {
Max_rt_shadow_local_lights = count;
}
// Session-only overrides, same as setMaxRtShadowLights -- do not touch the
// persisted Min/Max Raytraced Shadow Bias options. See Rt_shadow_bias_min/max
// in shadows.h.
@@ -147,6 +163,33 @@ public:
Rt_shadow_bias_max = bias;
}
// Ray count normally follows Shadow_quality (see shadows_rt_sample_count()), which is
// fixed at startup because the shadow map is sized then. The ray count isn't, so the
// lab can sweep it live through this session-only override. See
// Rt_shadow_samples_override in shadows.h.
static void setRtShadowSamples(int samples) {
Rt_shadow_samples_override = samples;
}
// Session-only override of every sun's $SunAngularSize (degrees of apparent
// diameter); pass a negative value to return to the stars.tbl values. See
// Sun_angular_size_override in starfield.h.
static void setSunAngularSizeOverride(float degrees) {
Sun_angular_size_override = degrees;
}
// Session-only override, same as setRtShadowSamples -- does not touch the
// persisted Raytraced Ambient Occlusion option. See Rtao_samples in rtao.h.
static void setRtaoSamples(int samples) {
Rtao_samples = samples;
}
// Session-only override, same as setRtaoSamples -- does not touch the persisted
// Shadow Contact Hardening option. See Shadow_contact_hardening_enabled in shadows.h.
static void setShadowContactHardening(bool enabled) {
Shadow_contact_hardening_enabled = enabled;
}
static void setTonemapper(ltp::TonemapperAlgorithm mode) {
ltp::lab_set_tonemapper(mode);
}
+38
View File
@@ -97,6 +97,16 @@ float current_exposure()
return _current.exposure;
}
float current_rtao_radius()
{
return _current.rtao_radius;
}
float current_rtao_strength()
{
return _current.rtao_strength;
}
piecewise_power_curve_intermediates current_piecewise_intermediates()
{
return calc_intermediates(_current.ppc_values);
@@ -211,6 +221,16 @@ void lab_set_exposure(float exIn)
_current.exposure = exIn;
}
void lab_set_rtao_radius(float radius)
{
_current.rtao_radius = radius;
}
void lab_set_rtao_strength(float strength)
{
_current.rtao_strength = strength;
}
void lab_set_tonemapper(TonemapperAlgorithm tnin)
{
_current.tonemapper = tnin;
@@ -381,6 +401,21 @@ void profile::parse(const char* filename, const SCP_string& profile_name, const
parsed |= parse_optional_float_into("$PPC Shoulder Angle:", &ppc_values.shoulder_angle);
parsed |= parse_optional_float_into("$Exposure:", &exposure);
if (parse_optional_float_into("$RTAO Radius:", &rtao_radius)) {
parsed = true;
if (rtao_radius < 0.0f) {
error_display(0, "$RTAO Radius: must be >= 0 (got %f); using 0.", rtao_radius);
rtao_radius = 0.0f;
}
}
if (parse_optional_float_into("$RTAO Strength:", &rtao_strength)) {
parsed = true;
if (rtao_strength < 0.0f) {
error_display(0, "$RTAO Strength: must be >= 0 (got %f); using 0.", rtao_strength);
rtao_strength = 0.0f;
}
}
parsed |= adjustment::parse(filename, "$Missile light brightness:", profile_name, &missile_light_brightness);
parsed |= adjustment::parse(filename, "$Missile light radius:", profile_name, &missile_light_radius);
@@ -444,6 +479,9 @@ void profile::reset()
exposure = 4.0f;
rtao_radius = 20.0f;
rtao_strength = 1.0f;
missile_light_brightness.reset();
missile_light_radius.reset();
missile_light_radius.base = 20.0f;
+11
View File
@@ -59,6 +59,13 @@ class profile {
TonemapperAlgorithm tonemapper;
piecewise_power_curve_values ppc_values;
float exposure;
// Raytraced ambient occlusion tuning (only sampled when rtao_enabled(), see
// graphics/rtao.h). Radius is the AO ray length in world units -- it is
// content-scale-dependent (fighters vs. capships), which is why it lives in
// the mod-owned lighting profile rather than a user option. Strength is an
// exponent on the occlusion term (1 = physical, >1 darkens, <1 lightens).
float rtao_radius;
float rtao_strength;
adjustment missile_light_brightness;
adjustment missile_light_radius;
adjustment laser_light_brightness;
@@ -97,7 +104,11 @@ const piecewise_power_curve_values& current_piecewise_values();
piecewise_power_curve_intermediates current_piecewise_intermediates();
piecewise_power_curve_intermediates calc_intermediates(piecewise_power_curve_values input);
float current_exposure();
float current_rtao_radius();
float current_rtao_strength();
void lab_set_exposure(float exIn);
void lab_set_rtao_radius(float radius);
void lab_set_rtao_strength(float strength);
void lab_set_tonemapper(TonemapperAlgorithm tnin);
void lab_set_ppc(const piecewise_power_curve_values &ppcin);
const piecewise_power_curve_values &lab_get_ppc();
+24
View File
@@ -6181,6 +6181,14 @@ void parse_one_background(background_t *background)
sle.div_x = 1;
sle.div_y = 1;
// apparent diameter in degrees, overriding both the sun's stars.tbl entry and the
// size that would otherwise be measured from its bitmap. Optional, so missions
// written before 26.3 simply don't have it
if (optional_string("+AngularSize:")) {
stuff_float(&sle.angular_size);
sle.angular_size = stars_check_angular_size(sle.angular_size, "+AngularSize:", sle.filename);
}
// add it
background->suns.push_back(sle);
}
@@ -9694,3 +9702,19 @@ bool check_for_25_1_data()
return false;
}
bool check_for_26_3_data()
{
// a sun that carries its own apparent diameter (+AngularSize:) can't be represented
// in an older mission file
for (const auto &background : Backgrounds)
{
for (const auto &sun : background.suns)
{
if (sun.angular_size >= 0.0f)
return true;
}
}
return false;
}
+1
View File
@@ -61,6 +61,7 @@ extern bool check_for_23_3_data();
extern bool check_for_24_1_data();
extern bool check_for_24_3_data();
extern bool check_for_25_1_data();
extern bool check_for_26_3_data();
#define WING_PLAYER_BASE 0x80000 // used by Fred to tell ship_index in a wing points to a player
+6 -5
View File
@@ -79,7 +79,7 @@ int Training_message_method = 1;
int Training_num_lines = 0;
int Training_voice = -1;
int Training_voice_type;
int Training_voice_soundstream;
int Training_voice_soundstream = -1;
sound_handle Training_voice_snd_handle;
int Training_flag = 0;
int Training_failure = 0;
@@ -699,6 +699,7 @@ void training_mission_shutdown()
if (Training_voice >= 0) {
if (Training_voice_type) {
audiostream_close_file(Training_voice_soundstream, false);
Training_voice_soundstream = -1;
} else {
snd_stop(Training_voice_snd_handle);
@@ -832,6 +833,7 @@ int message_play_training_voice(int index)
if (Training_voice >= 0) {
if (Training_voice_type) {
audiostream_close_file(Training_voice_soundstream, false);
Training_voice_soundstream = -1;
} else {
snd_stop(Training_voice_snd_handle);
@@ -853,10 +855,8 @@ int message_play_training_voice(int index)
if ((Training_voice < 0) || !Training_voice_type || (Training_voice != index)) {
if (Training_voice >= 0) {
if (Training_voice_type) {
if (Training_voice == index)
audiostream_stop(Training_voice_soundstream, 1, 0);
else
audiostream_close_file(Training_voice_soundstream, false);
audiostream_close_file(Training_voice_soundstream, false);
Training_voice_soundstream = -1;
} else {
snd_stop(Training_voice_snd_handle);
@@ -892,6 +892,7 @@ int message_play_training_voice(int index)
if (Training_voice >= 0) {
if (Training_voice_type) {
audiostream_close_file(Training_voice_soundstream, false);
Training_voice_soundstream = -1;
} else {
snd_stop(Training_voice_snd_handle);
+20 -1
View File
@@ -1035,6 +1035,17 @@ int Fred_mission_save::save_bitmaps()
required_string_fred("+Scale:");
parse_comments();
fout(" %f", sle->scale_x);
// apparent diameter, only written when this mission actually sets one; see
// check_for_26_3_data()
fso_comment_push(";;FSO 26.3.0;;");
FRED_ENSURE_PROPERTY_VERSION_WITH_DEFAULT_F("+AngularSize:",
1,
";;FSO 26.3.0;;",
SUN_ANGULAR_SIZE_UNSPECIFIED,
" %f",
sle->angular_size);
fso_comment_pop();
}
// save background bitmaps by filename
@@ -3171,7 +3182,15 @@ void Fred_mission_save::save_mission_internal(const char* pathname)
auto version_24_1 = gameversion::version(24, 1);
auto version_24_3 = gameversion::version(24, 3);
auto version_25_1 = gameversion::version(25, 1);
if (MISSION_VERSION >= version_25_1) {
auto version_26_3 = gameversion::version(26, 3);
if (MISSION_VERSION >= version_26_3) {
Warning(LOCATION,
"Notify an SCP coder: now that the required mission version is at least 26.3, the check_for_26_3_data(), "
"check_for_25_1_data(), check_for_24_3_data(), check_for_24_1_data(), and check_for_23_3_data() code can "
"be removed");
} else if (check_for_26_3_data()) {
The_mission.required_fso_version = version_26_3;
} else if (MISSION_VERSION >= version_25_1) {
Warning(LOCATION,
"Notify an SCP coder: now that the required mission version is at least 25.1, the check_for_25_1_data(), "
"check_for_24_3_data(), check_for_24_1_data(), and check_for_23_3_data() code can be removed");
+1 -1
View File
@@ -651,7 +651,7 @@ void cmd_brief_close()
for (i=0; i<Cur_cmd_brief->num_stages; i++) {
if (Cur_cmd_brief->stage[i].wave >= 0)
audiostream_close_file(Cur_cmd_brief->stage[i].wave, 0);
Cur_cmd_brief->stage[i].wave = -1;
}
// so that the same ani will reload properly upon return
+12 -3
View File
@@ -33,6 +33,8 @@
#include "io/timer.h"
#include "lighting/lighting.h"
#include "lighting/lighting_profiles.h"
#include "starfield/starfield.h"
#include "starfield/sun_disc.h"
#include "missionui/chatbox.h"
#include "missionui/missionbrief.h"
#include "missionui/missionscreencommon.h"
@@ -1989,16 +1991,23 @@ void draw_model_rotating(model_render_params *render_info, int ship_class, int m
*/
void common_setup_room_lights()
{
// These stand in for a sun rather than for a point source, so they get a sun's apparent
// size: for a directional light source_radius is the tangent of the angular radius, and
// it is what sizes both the raytraced penumbra cone and the shadow map's filter width
// (see shadow_smoothness_scale() in shadows.cpp). Leaving it at 0 would read as a light
// with no extent at all and give these rooms hard, aliased shadow edges.
const float room_light_source_radius = sun_disc_tangent_from_diameter(SUN_ANGULAR_SIZE_SOL);
light_reset();
auto tempv = vm_vec_new(-1.0f,0.3f,-1.0f);
auto tempc = hdr_color(1.0f,0.95f,0.9f, 0.0f, 1.5f);
light_add_directional(&tempv,-1,false,&tempc);
light_add_directional(&tempv,-1,false,&tempc,room_light_source_radius);
tempv.xyz={-0.4f,0.4f,1.1f};
tempc = hdr_color(0.788f,0.886f,1.0f,0.0f,1.5f);
light_add_directional(&tempv,-1,false,&tempc);
light_add_directional(&tempv,-1,false,&tempc,room_light_source_radius);
tempv.xyz={0.4f,0.1f,0.4f};
tempc = hdr_color(1.0f,1.0f,1.0f,0.0f,0.4f);
light_add_directional(&tempv,-1,false,&tempc);
light_add_directional(&tempv,-1,false,&tempc,room_light_source_radius);
gr_set_ambient_light(53, 53, 53);
}
+52 -44
View File
@@ -1248,6 +1248,22 @@ int eval_weapon_flag_for_game_type(int weapon_flags)
return rval;
}
static bool wl_bank_allows_weapon(const ship_info* sip, int bank, int weapon_class)
{
// an empty bank has nothing to place; this check runs on the weapon coming back from a swap
if (weapon_class < 0)
return true;
if (!eval_weapon_flag_for_game_type(sip->allowed_weapons[weapon_class]))
return false;
// allowed_weapons is built from the bank lists, so a class without them never gets here
if (!eval_weapon_flag_for_game_type(sip->restricted_loadout_flag[bank]))
return true;
return eval_weapon_flag_for_game_type(sip->allowed_bank_restricted_weapons[bank][weapon_class]) != 0;
}
/**
* Return the weapon class shown at the given on-screen row of a scrolling icon list,
* or -1 if that row is past the end of the list.
@@ -3684,34 +3700,30 @@ int wl_swap_slot_slot(int from_bank, int to_bank, int ship_slot, interface_snd_i
ship_info *sip = &Ship_info[slot->ship_class];
// check the to-bank first
if (eval_weapon_flag_for_game_type(sip->restricted_loadout_flag[to_bank])) {
if (!eval_weapon_flag_for_game_type(sip->allowed_bank_restricted_weapons[to_bank][slot->wep[from_bank]])) {
SCP_string txt;
sprintf(txt, XSTR("This bank is unable to carry %s weaponry", 1628), Weapon_info[slot->wep[from_bank]].get_display_name());
if (!wl_bank_allows_weapon(sip, to_bank, slot->wep[from_bank])) {
SCP_string txt;
sprintf(txt, XSTR("This bank is unable to carry %s weaponry", 1628), Weapon_info[slot->wep[from_bank]].get_display_name());
if ( !(Game_mode & GM_MULTIPLAYER) || (Netgame.host == pl) ) {
popup(PF_USE_AFFIRMATIVE_ICON, 1, POPUP_OK, txt.c_str());
} else if (pl != NULL) {
send_game_chat_packet(Netgame.host, txt.c_str(), MULTI_MSG_TARGET, pl, nullptr, 1);
}
return forced_update;
if ( !(Game_mode & GM_MULTIPLAYER) || (Netgame.host == pl) ) {
popup(PF_USE_AFFIRMATIVE_ICON, 1, POPUP_OK, txt.c_str());
} else if (pl != nullptr) {
send_game_chat_packet(Netgame.host, txt.c_str(), MULTI_MSG_TARGET, pl, nullptr, 1);
}
return forced_update;
}
// check the from-bank
if (eval_weapon_flag_for_game_type(sip->restricted_loadout_flag[from_bank])) {
if (!eval_weapon_flag_for_game_type(sip->allowed_bank_restricted_weapons[from_bank][slot->wep[to_bank]])) {
// going from "from" to "to" is valid (from previous if), but going the other way isn't...
// so return the "to" to the list and just move the "from"
if (!wl_bank_allows_weapon(sip, from_bank, slot->wep[to_bank])) {
// going from "from" to "to" is valid (from previous if), but going the other way isn't...
// so return the "to" to the list and just move the "from"
// put to_bank back into list
(*Wl_pool)[slot->wep[to_bank]] += slot->wep_count[to_bank]; // return to list
slot->wep[to_bank] = -1; // remove from slot
slot->wep_count[to_bank] = 0;
*sound=InterfaceSounds::ICON_DROP; // unless it changes later
forced_update = 1; // because we can't return right away
}
// put to_bank back into list
(*Wl_pool)[slot->wep[to_bank]] += slot->wep_count[to_bank]; // return to list
slot->wep[to_bank] = -1; // remove from slot
slot->wep_count[to_bank] = 0;
*sound=InterfaceSounds::ICON_DROP; // unless it changes later
forced_update = 1; // because we can't return right away
}
}
@@ -3839,19 +3851,17 @@ int wl_grab_from_list(int from_list, int to_bank, int ship_slot, interface_snd_i
ship_info *sip = &Ship_info[slot->ship_class];
// ensure that this bank will accept the weapon...
if (eval_weapon_flag_for_game_type(sip->restricted_loadout_flag[to_bank])) {
if (!eval_weapon_flag_for_game_type(sip->allowed_bank_restricted_weapons[to_bank][from_list])) {
SCP_string txt;
sprintf(txt, XSTR("This bank is unable to carry %s weaponry", 1628), Weapon_info[from_list].get_display_name());
if (!wl_bank_allows_weapon(sip, to_bank, from_list)) {
SCP_string txt;
sprintf(txt, XSTR("This bank is unable to carry %s weaponry", 1628), Weapon_info[from_list].get_display_name());
if ( !(Game_mode & GM_MULTIPLAYER) || (Netgame.host == pl) ) {
popup(PF_USE_AFFIRMATIVE_ICON, 1, POPUP_OK, txt.c_str());
} else if (pl != NULL) {
send_game_chat_packet(Netgame.host, txt.c_str(), MULTI_MSG_TARGET, pl, nullptr, 1);
}
return 0;
if ( !(Game_mode & GM_MULTIPLAYER) || (Netgame.host == pl) ) {
popup(PF_USE_AFFIRMATIVE_ICON, 1, POPUP_OK, txt.c_str());
} else if (pl != nullptr) {
send_game_chat_packet(Netgame.host, txt.c_str(), MULTI_MSG_TARGET, pl, nullptr, 1);
}
return 0;
}
// we will have some sort of success from this point
@@ -3917,19 +3927,17 @@ int wl_swap_list_slot(int from_list, int to_bank, int ship_slot, interface_snd_i
ship_info *sip = &Ship_info[slot->ship_class];
// ensure that this bank will accept the weapon
if (eval_weapon_flag_for_game_type(sip->restricted_loadout_flag[to_bank])) {
if (!eval_weapon_flag_for_game_type(sip->allowed_bank_restricted_weapons[to_bank][from_list])) {
SCP_string txt;
sprintf(txt, XSTR("This bank is unable to carry %s weaponry", 1628), Weapon_info[from_list].get_display_name());
if (!wl_bank_allows_weapon(sip, to_bank, from_list)) {
SCP_string txt;
sprintf(txt, XSTR("This bank is unable to carry %s weaponry", 1628), Weapon_info[from_list].get_display_name());
if ( !(Game_mode & GM_MULTIPLAYER) || (Netgame.host == pl) ) {
popup(PF_USE_AFFIRMATIVE_ICON, 1, POPUP_OK, txt.c_str());
} else if (pl != NULL) {
send_game_chat_packet(Netgame.host, txt.c_str(), MULTI_MSG_TARGET, pl, nullptr, 1);
}
return 0;
if ( !(Game_mode & GM_MULTIPLAYER) || (Netgame.host == pl) ) {
popup(PF_USE_AFFIRMATIVE_ICON, 1, POPUP_OK, txt.c_str());
} else if (pl != nullptr) {
send_game_chat_packet(Netgame.host, txt.c_str(), MULTI_MSG_TARGET, pl, nullptr, 1);
}
return 0;
}
// dump slot weapon back into list
+6
View File
@@ -853,6 +853,12 @@ void parse_mod_table(const char *filename)
}
}
// Per-cascade shadow map filter radius, in shadow map UV units, for a
// Sol-sized sun. It is no longer the final width: shadow_cascade_params_bind()
// scales these by the sun's apparent size relative to Sol, so that the same
// $SunAngularSize: drives both shadow-mapped and raytraced softness. A mod that
// tuned these against retail/MediaVPs sun art keeps the look it tuned for --
// see shadow_smoothness_scale() in shadows.cpp for why Sol is the reference.
if (optional_string("$Shadow Smoothness Factor:")) {
SCP_vector<float> smoothness;
stuff_float_list(smoothness);
+2 -16
View File
@@ -1075,22 +1075,14 @@ polymodel_instance* model_get_instance(int model_instance_num);
// routine to copy subsystems. Must be called when subsystems sets are the same -- see ship.cpp
void model_copy_subsystems(int n_subsystems, model_subsystem *d_sp, model_subsystem *s_sp);
// If MR_FLAG_OUTLINE bit set this color will be used for outlines.
// This defaults to black.
void model_set_outline_color(int r, int g, int b);
// IF MR_LOCK_DETAIL is set, then it will always draw detail level 'n'
// This defaults to 0. (0=highest, larger=lower)
void model_set_detail_level(int n);
// Flags you can pass to model_render
#define MR_NORMAL (0) // Draw a normal object
#define MR_SHOW_OUTLINE (1<<0) // Draw the object in outline mode. Color specified by model_set_outline_color
#define MR_SHOW_OUTLINE (1<<0) // Draw the object in outline mode. Color specified by model_render_params::set_color
#define MR_SKYBOX (1<<1) // Draw as a skybox
#define MR_DESATURATED (1<<2) // Draw model in monochrome using outline color
#define MR_STENCIL_WRITE (1<<3) // Write stencil buffere where the model was rendered
#define MR_STENCIL_READ (1<<4) // Only draw pixels of the model where the stencil buffer has the right value (see MR_STENCIL_WRITE)
#define MR_SHOW_THRUSTERS (1<<5) // Show the engine thrusters. See model_set_thrust for how long it draws.
#define MR_SHOW_THRUSTERS (1<<5) // Show the engine thrusters. See model_render_params::set_thruster_info for how long it draws.
#define MR_NO_COLOR_WRITES (1<<6) // Don't write anything to the color buffers (used when setting the stencil buffer)
#define MR_NO_POLYS (1<<7) // Don't draw the polygons.
#define MR_NO_LIGHTING (1<<8) // Don't perform any lighting on the model.
@@ -1181,7 +1173,6 @@ bool submodel_is_destroyed_form(const char *name);
extern int modelstats_num_polys;
extern int modelstats_num_polys_drawn;
extern int modelstats_num_verts;
extern int modelstats_num_sortnorms;
#endif
// Tries to move joints so that the turret points to the point dst.
@@ -1471,10 +1462,6 @@ typedef struct mst_info {
{}
} mst_info;
// scale the engines thrusters by this much
// Only enabled if MR_SHOW_THRUSTERS is on
void model_set_thrust(int model_num = -1, mst_info *mst = NULL);
//=======================================================================================
// Finds the closest point on a model to a point in space. Actually only finds a point
@@ -1535,7 +1522,6 @@ void model_draw_dock_points_htl(int model_num);
bool model_interp_config_buffer(indexed_vertex_source *vert_src, vertex_buffer *vb, bool update_ibuffer_only);
bool model_interp_pack_buffer(indexed_vertex_source *vert_src, vertex_buffer *vb);
void model_interp_submit_buffers(indexed_vertex_source *vert_src, size_t vertex_stride);
void model_allocate_interp_data(uint n_verts = 0, uint n_norms = 0);
void glowpoint_init();
SCP_vector<glow_point_bank_override>::iterator get_glowpoint_bank_override_by_name(const char* name);
+3 -3
View File
@@ -81,13 +81,13 @@ namespace Model {
FLAG_LIST(Render_Flags) {
Normal, // Draw a normal object
Show_outline, // Draw the object in outline mode. Color specified by model_set_outline_color
Show_outline, // Draw the object in outline mode. Color specified by model_render_params::set_color
Show_pivots, // Show the pivot points
Show_paths, // Show the paths associated with a model
Show_radius, // Show the radius around the object
Show_shields, // Show the shield mesh
Show_thrusters, // Show the engine thrusters. See model_set_thrust for how long it draws.
Lock_detail, // Only draw the detail level defined in model_set_detail_level
Show_thrusters, // Show the engine thrusters. See model_render_params::set_thruster_info for how long it draws.
Lock_detail, // Only draw the detail level defined in model_render_params::set_detail_level_lock
No_polys, // Don't draw the polygons.
No_lighting, // Don't perform any lighting on the model.
No_texturing, // Draw textures as flat-shaded polygons.
+8 -790
View File
@@ -42,30 +42,13 @@
#include <climits>
float model_radius = 0;
// Some debug variables used externally for displaying stats
#ifndef NDEBUG
int modelstats_num_polys = 0;
int modelstats_num_polys_drawn = 0;
int modelstats_num_verts = 0;
int modelstats_num_sortnorms = 0;
int modelstats_num_boxes = 0;
#endif
typedef struct model_light {
ubyte r, g, b;
} model_light;
// a lighting object
typedef struct model_light_object {
model_light *lights;
int objnum;
int skip;
int skip_max;
} model_light_object;
struct bsp_vertex
{
vec3d position;
@@ -135,383 +118,9 @@ struct interp_vertex {
// Local variables
//
static uint Num_interp_verts_allocated = 0;
vec3d **Interp_verts = NULL;
static vertex *Interp_points = NULL;
static uint Interp_num_verts = 0;
static float Interp_box_scale = 1.0f; // this is used to scale both detail boxes and spheres
// -------------------------------------------------------------------
// lighting save stuff
//
model_light_object Interp_lighting_temp;
model_light_object *Interp_lighting = &Interp_lighting_temp;
int Interp_use_saved_lighting = 0;
int Interp_saved_lighting_full = 0;
//
// lighting save stuff
// -------------------------------------------------------------------
static uint Num_interp_norms_allocated = 0;
static vec3d **Interp_norms = NULL;
static ubyte *Interp_light_applied = NULL;
static uint Interp_num_norms = 0;
static ubyte *Interp_lights;
// Stuff to control rendering parameters
static color Interp_outline_color;
static int Interp_detail_level_locked = -1;
static uint Interp_flags = 0;
// If non-zero, then the subobject gets scaled by Interp_thrust_scale.
int Interp_thrust_scale_subobj = 0;
float Interp_thrust_scale = 0.1f;
static float Interp_thrust_scale_x = 0.0f;//added -bobboau
static float Interp_thrust_scale_y = 0.0f;//added -bobboau
static int Interp_thrust_bitmap = -1;
static int Interp_thrust_glow_bitmap = -1;
static float Interp_thrust_glow_noise = 1.0f;
static bool Interp_afterburner = false;
// Bobboau's thruster stuff
static int Interp_secondary_thrust_glow_bitmap = -1;
static int Interp_tertiary_thrust_glow_bitmap = -1;
static int Interp_distortion_thrust_bitmap = -1;
static float Interp_thrust_glow_rad_factor = 1.0f;
static float Interp_secondary_thrust_glow_rad_factor = 1.0f;
static float Interp_tertiary_thrust_glow_rad_factor = 1.0f;
static float Interp_distortion_thrust_rad_factor = 1.0f;
static float Interp_distortion_thrust_length_factor = 1.0f;
static float Interp_thrust_glow_len_factor = 1.0f;
static vec3d Interp_thrust_rotvel = ZERO_VECTOR;
static bool Interp_draw_distortion = true;
// Bobboau's warp stuff
static float Interp_warp_scale_x = 1.0f;
static float Interp_warp_scale_y = 1.0f;
static float Interp_warp_scale_z = 1.0f;
// if != -1, use this bitmap when rendering ship insignias
static int Interp_insignia_bitmap = -1;
// if != -1, use this bitmap when rendering with a forced texture
static int Interp_forced_bitmap = -1;
// our current level of detail (LOD)
int Interp_detail_level = 0;
// forward references
int model_should_render_engine_glow(int objnum, int bank_obj);
void model_deallocate_interp_data()
{
if (Interp_verts != nullptr) {
vm_free(Interp_verts);
Interp_verts = nullptr;
}
if (Interp_points != nullptr) {
vm_free(Interp_points);
Interp_points = nullptr;
}
if (Interp_norms != nullptr) {
vm_free(Interp_norms);
Interp_norms = nullptr;
}
if (Interp_light_applied != nullptr) {
vm_free(Interp_light_applied);
Interp_light_applied = nullptr;
}
if (Interp_lighting_temp.lights != nullptr) {
vm_free(Interp_lighting_temp.lights);
Interp_lighting_temp.lights = nullptr;
}
Num_interp_verts_allocated = 0;
Num_interp_norms_allocated = 0;
}
void model_allocate_interp_data(uint n_verts, uint n_norms)
{
static ubyte dealloc = 0;
if (!dealloc) {
atexit(model_deallocate_interp_data);
dealloc = 1;
}
Assert( (n_verts || Num_interp_verts_allocated) && (n_norms || Num_interp_norms_allocated) );
if (n_verts > Num_interp_verts_allocated) {
if (Interp_verts != NULL) {
vm_free(Interp_verts);
Interp_verts = NULL;
}
// Interp_verts can't be reliably realloc'd so free and malloc it on each resize (no data needs to be carried over)
Interp_verts = (vec3d**) vm_malloc( n_verts * sizeof(vec3d *) );
Interp_points = (vertex*) vm_realloc( Interp_points, n_verts * sizeof(vertex) );
Num_interp_verts_allocated = n_verts;
}
if (n_norms > Num_interp_norms_allocated) {
if (Interp_norms != NULL) {
vm_free(Interp_norms);
Interp_norms = NULL;
}
// Interp_norms can't be reliably realloc'd so free and malloc it on each resize (no data needs to be carried over)
Interp_norms = (vec3d**) vm_malloc( n_norms * sizeof(vec3d *) );
// these next two lighting things aren't values that need to be carried over, but we need to make sure they are 0 by default
if (Interp_light_applied != NULL) {
vm_free(Interp_light_applied);
Interp_light_applied = NULL;
}
if (Interp_lighting_temp.lights != NULL) {
vm_free(Interp_lighting_temp.lights);
Interp_lighting_temp.lights = NULL;
}
Interp_light_applied = (ubyte*) vm_malloc( n_norms * sizeof(ubyte) );
Interp_lighting_temp.lights = (model_light*) vm_malloc( n_norms * sizeof(model_light) );
memset( Interp_light_applied, 0, n_norms * sizeof(ubyte) );
memset( Interp_lighting_temp.lights, 0, n_norms * sizeof(model_light) );
Num_interp_norms_allocated = n_norms;
}
Interp_num_verts = n_verts;
Interp_num_norms = n_norms;
// check that everything is still usable (works in release and debug builds)
Verify( Interp_points != NULL );
Verify( Interp_verts != NULL );
Verify( Interp_norms != NULL );
Verify( Interp_light_applied != NULL );
}
void interp_clear_instance()
{
Interp_thrust_scale = 0.1f;
Interp_thrust_scale_x = 0.0f;//added-Bobboau
Interp_thrust_scale_y = 0.0f;//added-Bobboau
Interp_thrust_bitmap = -1;
Interp_thrust_glow_bitmap = -1;
Interp_thrust_glow_noise = 1.0f;
Interp_insignia_bitmap = -1;
Interp_afterburner = false;
// Bobboau's thruster stuff
{
Interp_thrust_glow_rad_factor = 1.0f;
Interp_secondary_thrust_glow_bitmap = -1;
Interp_secondary_thrust_glow_rad_factor = 1.0f;
Interp_tertiary_thrust_glow_bitmap = -1;
Interp_tertiary_thrust_glow_rad_factor = 1.0f;
Interp_thrust_glow_len_factor = 1.0f;
vm_vec_zero(&Interp_thrust_rotvel);
}
Interp_box_scale = 1.0f;
Interp_detail_level_locked = -1;
Interp_forced_bitmap = -1;
}
/**
* Scales the engines thrusters by this much
*/
void model_set_thrust(int /*model_num*/, mst_info *mst)
{
if (mst == NULL) {
Int3();
return;
}
Interp_thrust_scale = mst->length.xyz.z;
Interp_thrust_scale_x = mst->length.xyz.x;
Interp_thrust_scale_y = mst->length.xyz.y;
CLAMP(Interp_thrust_scale, 0.1f, 1.0f);
Interp_thrust_bitmap = mst->primary_bitmap;
Interp_thrust_glow_bitmap = mst->primary_glow_bitmap;
Interp_secondary_thrust_glow_bitmap = mst->secondary_glow_bitmap;
Interp_tertiary_thrust_glow_bitmap = mst->tertiary_glow_bitmap;
Interp_distortion_thrust_bitmap = mst->distortion_bitmap;
Interp_thrust_glow_noise = mst->glow_noise;
Interp_afterburner = mst->use_ab;
Interp_thrust_rotvel = mst->rotvel;
Interp_thrust_glow_rad_factor = mst->glow_rad_factor;
Interp_secondary_thrust_glow_rad_factor = mst->secondary_glow_rad_factor;
Interp_tertiary_thrust_glow_rad_factor = mst->tertiary_glow_rad_factor;
Interp_thrust_glow_len_factor = mst->glow_length_factor;
Interp_distortion_thrust_rad_factor = mst->distortion_rad_factor;
Interp_distortion_thrust_length_factor = mst->distortion_length_factor;
Interp_draw_distortion = mst->draw_distortion;
}
float GEOMETRY_NOISE = 0.0f;
// Point list
// +0 int id
// +4 int size
// +8 int n_verts
// +12 int n_norms
// +16 int offset from start of chunk to vertex data
// +20 n_verts*char norm_counts
// +offset vertex data. Each vertex n is a point followed by norm_counts[n] normals.
void model_interp_defpoints(ubyte * p, polymodel *pm, bsp_info *sm)
{
SCP_UNUSED(pm);
SCP_UNUSED(sm);
uint i, n;
uint nverts = uw(p+8);
uint offset = uw(p+16);
uint next_norm = 0;
uint nnorms = 0;
ubyte * normcount = p+20;
vertex *dest = NULL;
vec3d *src = vp(p+offset);
// Get pointer to lights
Interp_lights = p+20+nverts;
for (i = 0; i < nverts; i++) {
nnorms += normcount[i];
}
// allocate new Interp data if size is greater than we already have ready to use
model_allocate_interp_data(nverts, nnorms);
dest = Interp_points;
Assert( dest != NULL );
#ifndef NDEBUG
modelstats_num_verts += nverts;
#endif
if (Interp_thrust_scale_subobj) {
// Only scale vertices that aren't on the "base" of
// the effect. Base is something Adam decided to be
// anything under 1.5 meters, hence the 1.5f.
float min_thruster_dist = -1.5f;
if ( Interp_flags & MR_IS_MISSILE ) {
min_thruster_dist = 0.5f;
}
for (n=0; n<nverts; n++ ) {
vec3d tmp;
Interp_verts[n] = src;
// Only scale vertices that aren't on the "base" of
// the effect. Base is something Adam decided to be
// anything under 1.5 meters, hence the 1.5f.
if ( src->xyz.z < min_thruster_dist ) {
tmp.xyz.x = src->xyz.x * 1.0f;
tmp.xyz.y = src->xyz.y * 1.0f;
tmp.xyz.z = src->xyz.z * Interp_thrust_scale;
} else {
tmp = *src;
}
g3_rotate_vertex(dest,&tmp);
src++; // move to normal
for (i=0; i<normcount[n]; i++ ) {
Interp_light_applied[next_norm] = 0;
Interp_norms[next_norm] = src;
next_norm++;
src++;
}
dest++;
}
} else if ( (Interp_warp_scale_x != 1.0f) || (Interp_warp_scale_y != 1.0f) || (Interp_warp_scale_z != 1.0f)) {
for (n=0; n<nverts; n++ ) {
vec3d tmp;
Interp_verts[n] = src;
tmp.xyz.x = (src->xyz.x) * Interp_warp_scale_x;
tmp.xyz.y = (src->xyz.y) * Interp_warp_scale_y;
tmp.xyz.z = (src->xyz.z) * Interp_warp_scale_z;
g3_rotate_vertex(dest,&tmp);
src++; // move to normal
for (i=0; i<normcount[n]; i++ ) {
Interp_light_applied[next_norm] = 0;
Interp_norms[next_norm] = src;
next_norm++;
src++;
}
dest++;
}
} else {
vec3d point;
for (n=0; n<nverts; n++ ) {
if(GEOMETRY_NOISE!=0.0f){
GEOMETRY_NOISE = model_radius / 50;
Interp_verts[n] = src;
point.xyz.x = src->xyz.x + frand_range(GEOMETRY_NOISE,-GEOMETRY_NOISE);
point.xyz.y = src->xyz.y + frand_range(GEOMETRY_NOISE,-GEOMETRY_NOISE);
point.xyz.z = src->xyz.z + frand_range(GEOMETRY_NOISE,-GEOMETRY_NOISE);
g3_rotate_vertex(dest, &point);
}else{
Interp_verts[n] = src;
g3_rotate_vertex(dest, src);
}
src++; // move to normal
for (i=0; i<normcount[n]; i++ ) {
Interp_light_applied[next_norm] = 0;
Interp_norms[next_norm] = src;
next_norm++;
src++;
}
dest++;
}
}
Interp_num_norms = next_norm;
}
void model_interp_edge_alpha( ubyte *param_r, ubyte *param_g, ubyte *param_b, vec3d *pnt, vec3d *norm, float alpha, bool invert = false)
{
vec3d r;
@@ -1203,21 +812,6 @@ void submodel_get_cross_sectional_random_pos(int model_num, int submodel_num, fl
}
}
// If MR_FLAG_OUTLINE bit set this color will be used for outlines.
// This defaults to black.
void model_set_outline_color(int r, int g, int b )
{
gr_init_color( &Interp_outline_color, r, g, b );
}
// IF MR_LOCK_DETAIL is set, then it will always draw detail level 'n'
// This defaults to 0. (0=highest, larger=lower)
void model_set_detail_level(int n)
{
Interp_detail_level_locked = n;
}
/**
* Returns number of tmaps & flat polys in a submodel;
*/
@@ -1475,39 +1069,8 @@ void model_page_out_textures(polymodel* pm, bool release, const SCP_set<int>& sk
//**********vertex buffer stuff**********//
int tri_count[MAX_MODEL_TEXTURES];
poly_list polygon_list[MAX_MODEL_TEXTURES];
void parse_defpoint(int off, ubyte *bsp_data)
{
uint i, n;
uint nverts = uw(off+bsp_data+8);
uint offset = uw(off+bsp_data+16);
uint next_norm = 0;
ubyte *normcount = off+bsp_data+20;
vec3d *src = vp(off+bsp_data+offset);
// Get pointer to lights
Interp_lights = off+bsp_data+20+nverts;
#ifndef NDEBUG
modelstats_num_verts += nverts;
#endif
for (n = 0; n < nverts; n++) {
Interp_verts[n] = src;
src++; // move to normal
for (i = 0; i < normcount[n]; i++) {
Interp_norms[next_norm] = src;
next_norm++;
src++;
}
}
}
int check_values(vec3d *N)
{
// Values equal to -1.#IND0
@@ -1524,357 +1087,6 @@ int check_values(vec3d *N)
int Parse_normal_problem_count = 0;
void parse_tmap(int offset, ubyte *bsp_data)
{
int pof_tex = w(bsp_data+offset+TMAP_TEXNUM);
uint n_vert = uw(bsp_data+offset+TMAP_NVERTS);
ubyte *p = &bsp_data[offset+TMAP_NORMAL];
auto tverts = reinterpret_cast<model_tmap_vert_old*>(&bsp_data[offset + TMAP_VERTS]);
vertex *V;
vec3d *v;
vec3d *N;
int problem_count = 0;
for (uint i = 1; i < (n_vert-1); i++) {
V = &polygon_list[pof_tex].vert[(polygon_list[pof_tex].n_verts)];
N = &polygon_list[pof_tex].norm[(polygon_list[pof_tex].n_verts)];
v = Interp_verts[tverts[0].vertnum];
V->world.xyz.x = v->xyz.x;
V->world.xyz.y = v->xyz.y;
V->world.xyz.z = v->xyz.z;
V->texture_position.u = tverts[0].u;
V->texture_position.v = tverts[0].v;
*N = *Interp_norms[tverts[0].normnum];
if ( IS_VEC_NULL(N) )
*N = *vp(p);
problem_count += check_values(N);
vm_vec_normalize_safe(N);
V = &polygon_list[pof_tex].vert[(polygon_list[pof_tex].n_verts)+1];
N = &polygon_list[pof_tex].norm[(polygon_list[pof_tex].n_verts)+1];
v = Interp_verts[tverts[i].vertnum];
V->world.xyz.x = v->xyz.x;
V->world.xyz.y = v->xyz.y;
V->world.xyz.z = v->xyz.z;
V->texture_position.u = tverts[i].u;
V->texture_position.v = tverts[i].v;
*N = *Interp_norms[tverts[i].normnum];
if ( IS_VEC_NULL(N) )
*N = *vp(p);
problem_count += check_values(N);
vm_vec_normalize_safe(N);
V = &polygon_list[pof_tex].vert[(polygon_list[pof_tex].n_verts)+2];
N = &polygon_list[pof_tex].norm[(polygon_list[pof_tex].n_verts)+2];
v = Interp_verts[tverts[i+1].vertnum];
V->world.xyz.x = v->xyz.x;
V->world.xyz.y = v->xyz.y;
V->world.xyz.z = v->xyz.z;
V->texture_position.u = tverts[i+1].u;
V->texture_position.v = tverts[i+1].v;
*N = *Interp_norms[tverts[i+1].normnum];
if ( IS_VEC_NULL(N) )
*N = *vp(p);
problem_count += check_values(N);
vm_vec_normalize_safe(N);
polygon_list[pof_tex].n_verts += 3;
}
Parse_normal_problem_count += problem_count;
}
/**
* @brief Parses a TMAP2POLY chunk into a list of polygons.
*
* @param offset The byte offset to the current TMAP2POLY chunk within bsp_data.
* @param[in] bsp_data The byte buffer containing the BSP information for the current model.
*/
void parse_tmap2(int offset, ubyte* bsp_data)
{
int pof_tex = w(bsp_data + offset + TMAP2_TEXNUM);
uint n_vert = uw(bsp_data + offset + TMAP2_NVERTS);
ubyte* p = &bsp_data[offset + TMAP2_NORMAL];
model_tmap_vert* tverts;
vertex* V;
vec3d* v;
vec3d* N;
int problem_count = 0;
tverts = reinterpret_cast<model_tmap_vert*>(&bsp_data[offset + TMAP2_VERTS]);
for (uint i = 1; i < (n_vert - 1); i++) {
V = &polygon_list[pof_tex].vert[(polygon_list[pof_tex].n_verts)];
N = &polygon_list[pof_tex].norm[(polygon_list[pof_tex].n_verts)];
v = Interp_verts[tverts[0].vertnum];
V->world.xyz.x = v->xyz.x;
V->world.xyz.y = v->xyz.y;
V->world.xyz.z = v->xyz.z;
V->texture_position.u = tverts[0].u;
V->texture_position.v = tverts[0].v;
*N = *Interp_norms[tverts[0].normnum];
if (IS_VEC_NULL(N))
*N = *vp(p);
problem_count += check_values(N);
vm_vec_normalize_safe(N);
V = &polygon_list[pof_tex].vert[(polygon_list[pof_tex].n_verts) + 1];
N = &polygon_list[pof_tex].norm[(polygon_list[pof_tex].n_verts) + 1];
v = Interp_verts[tverts[i].vertnum];
V->world.xyz.x = v->xyz.x;
V->world.xyz.y = v->xyz.y;
V->world.xyz.z = v->xyz.z;
V->texture_position.u = tverts[i].u;
V->texture_position.v = tverts[i].v;
*N = *Interp_norms[tverts[i].normnum];
if (IS_VEC_NULL(N))
*N = *vp(p);
problem_count += check_values(N);
vm_vec_normalize_safe(N);
V = &polygon_list[pof_tex].vert[(polygon_list[pof_tex].n_verts) + 2];
N = &polygon_list[pof_tex].norm[(polygon_list[pof_tex].n_verts) + 2];
v = Interp_verts[tverts[i + 1].vertnum];
V->world.xyz.x = v->xyz.x;
V->world.xyz.y = v->xyz.y;
V->world.xyz.z = v->xyz.z;
V->texture_position.u = tverts[i + 1].u;
V->texture_position.v = tverts[i + 1].v;
*N = *Interp_norms[tverts[i + 1].normnum];
if (IS_VEC_NULL(N))
*N = *vp(p);
problem_count += check_values(N);
vm_vec_normalize_safe(N);
polygon_list[pof_tex].n_verts += 3;
}
Parse_normal_problem_count += problem_count;
}
void parse_bsp(int offset, ubyte* bsp_data);
void parse_sortnorm(int offset, ubyte* bsp_data)
{
int frontlist, backlist, prelist, postlist, onlist;
frontlist = w(bsp_data + offset + 36);
backlist = w(bsp_data + offset + 40);
prelist = w(bsp_data + offset + 44);
postlist = w(bsp_data + offset + 48);
onlist = w(bsp_data + offset + 52);
if (prelist) parse_bsp(offset + prelist, bsp_data);
if (backlist) parse_bsp(offset + backlist, bsp_data);
if (onlist) parse_bsp(offset + onlist, bsp_data);
if (frontlist) parse_bsp(offset + frontlist, bsp_data);
if (postlist) parse_bsp(offset + postlist, bsp_data);
}
/**
* @brief Parses a SORTNORM2 by recursively parsing into the two pointers it contains.
*
* @param offset The byte offset to the current SORT2NORM chunk within bsp_data.
* @param bsp_data The byte buffer containing the BSP information for the current model.
*/
void parse_sortnorm2(int offset, ubyte* bsp_data)
{
int frontlist, backlist;
frontlist = w(bsp_data + offset + 8);
backlist = w(bsp_data + offset + 12);
if (backlist) parse_bsp(offset + backlist, bsp_data);
if (frontlist) parse_bsp(offset + frontlist, bsp_data);
}
void parse_bsp(int offset, ubyte *bsp_data)
{
int id = w(bsp_data+offset);
int size = w(bsp_data+offset+4);
bool end = id == OP_EOF;
while (!end) {
switch (id)
{
case OP_DEFPOINTS:
parse_defpoint(offset, bsp_data);
break;
case OP_SORTNORM:
parse_sortnorm(offset, bsp_data);
break;
case OP_SORTNORM2:
parse_sortnorm2(offset, bsp_data);
end = true; // should not continue after this chunk
break;
case OP_FLATPOLY:
break;
case OP_TMAPPOLY:
parse_tmap(offset, bsp_data);
break;
case OP_BOUNDBOX:
break;
case OP_TMAP2POLY:
parse_tmap2(offset, bsp_data);
end = true; // should not continue after this chunk
break;
default:
return;
}
if (end) break;
offset += size;
id = w(bsp_data+offset);
size = w(bsp_data+offset+4);
if (size < 1 || id == OP_EOF)
end = true;
}
}
void find_tmap(int offset, const ubyte *bsp_data, int id)
{
int pof_tex = cw(bsp_data+offset+(id == OP_TMAP2POLY ? TMAP2_TEXNUM : TMAP_TEXNUM));
uint n_vert = cuw(bsp_data+offset+ (id == OP_TMAP2POLY ? TMAP2_NVERTS : TMAP_NVERTS));
tri_count[pof_tex] += n_vert-2;
}
void find_defpoint(int off, ubyte *bsp_data)
{
uint n;
uint nverts = uw(off+bsp_data+8);
ubyte * normcount = off+bsp_data+20;
// Get pointer to lights
Interp_lights = off+bsp_data+20+nverts;
#ifndef NDEBUG
modelstats_num_verts += nverts;
#endif
int norm_num = 0;
for (n = 0; n < nverts; n++) {
norm_num += normcount[n];
}
Interp_num_verts = nverts;
Interp_num_norms = norm_num;
}
void find_tri_counts(int offset, ubyte* bsp_data);
void find_sortnorm(int offset, ubyte* bsp_data)
{
int frontlist, backlist, prelist, postlist, onlist;
frontlist = w(bsp_data + offset + 36);
backlist = w(bsp_data + offset + 40);
prelist = w(bsp_data + offset + 44);
postlist = w(bsp_data + offset + 48);
onlist = w(bsp_data + offset + 52);
if (prelist) find_tri_counts(offset + prelist, bsp_data);
if (backlist) find_tri_counts(offset + backlist, bsp_data);
if (onlist) find_tri_counts(offset + onlist, bsp_data);
if (frontlist) find_tri_counts(offset + frontlist, bsp_data);
if (postlist) find_tri_counts(offset + postlist, bsp_data);
}
void find_sortnorm2(int offset, ubyte* bsp_data)
{
int frontlist, backlist;
frontlist = w(bsp_data + offset + 8);
backlist = w(bsp_data + offset + 12);
if (backlist) find_tri_counts(offset + backlist, bsp_data);
if (frontlist) find_tri_counts(offset + frontlist, bsp_data);
}
// tri_count
void find_tri_counts(int offset, ubyte *bsp_data)
{
int id = w(bsp_data+offset);
int size = w(bsp_data+offset+4);
bool end = id == OP_EOF;
while (!end) {
switch (id)
{
case OP_DEFPOINTS:
find_defpoint(offset, bsp_data);
break;
case OP_SORTNORM:
find_sortnorm(offset, bsp_data);
break;
case OP_SORTNORM2:
find_sortnorm2(offset, bsp_data);
end = true; // should not continue after this chunk
break;
case OP_FLATPOLY:
break;
case OP_TMAPPOLY:
find_tmap(offset, bsp_data, id);
break;
case OP_TMAP2POLY:
find_tmap(offset, bsp_data, id);
end = true; // should not continue after this chunk
break;
case OP_BOUNDBOX:
break;
default:
return;
}
if (end) break;
offset += size;
id = w(bsp_data+offset);
size = w(bsp_data+offset+4);
if (size < 1 || id == OP_EOF)
end = true;
}
}
void model_interp_submit_buffers(indexed_vertex_source *vert_src, size_t vertex_stride)
{
Assert(vert_src != NULL);
@@ -2105,7 +1317,6 @@ void interp_configure_vertex_buffers(polymodel *pm, int mn, const model_read_def
for (i = 0; i < MAX_MODEL_TEXTURES; i++) {
polygon_list[i].n_verts = 0;
tri_count[i] = 0;
}
int milliseconds = timer_get_milliseconds();
@@ -2118,7 +1329,6 @@ void interp_configure_vertex_buffers(polymodel *pm, int mn, const model_read_def
for (i = 0; i < MAX_MODEL_TEXTURES; i++) {
int vert_count = bsp_polies->get_num_triangles(i) * 3;
tri_count[i] = vert_count / 3;
total_verts += vert_count;
polygon_list[i].allocate(vert_count);
@@ -2887,6 +2097,14 @@ void bsp_polygon_data::process_bsp(int offset, ubyte* bsp_data)
}
}
// Point list
// +0 int id
// +4 int size
// +8 int n_verts
// +12 int n_norms
// +16 int offset from start of chunk to vertex data
// +20 n_verts*char norm_counts
// +offset vertex data. Each vertex n is a point followed by norm_counts[n] normals.
void bsp_polygon_data::process_defpoints(int off, ubyte* bsp_data)
{
uint i, n;
+4 -31
View File
@@ -2128,7 +2128,9 @@ modelread_status read_model_file_no_subsys(polymodel * pm, const char* filename,
if (sm->bsp_data_size > 0) {
sm->bsp_data = make_shared<ubyte[]>(sm->bsp_data_size);
cfread(sm->bsp_data.get(), 1, sm->bsp_data_size, fp);
Macro_ubyte_bounds = sm->bsp_data.get() + sm->bsp_data_size;
swap_bsp_data(pm, sm->bsp_data.get());
Macro_ubyte_bounds = nullptr;
}
else {
sm->bsp_data = nullptr;
@@ -2144,7 +2146,9 @@ modelread_status read_model_file_no_subsys(polymodel * pm, const char* filename,
cfread(bsp_data.get(), 1, sm->bsp_data_size, fp);
// byte swap first thing
Macro_ubyte_bounds = bsp_data.get() + sm->bsp_data_size;
swap_bsp_data(pm, bsp_data.get());
Macro_ubyte_bounds = nullptr;
extern bool Cmdline_no_bsp_align;
if (Cmdline_no_bsp_align) {
@@ -5138,8 +5142,6 @@ void model_clear_instance(int model_num)
pm->maps[i].ResetToOriginal();
}
// ---- end of stuff that should be moved into model instances at some point
interp_clear_instance();
}
void model_set_submodel_instance_motion_info(bsp_info *sm, submodel_instance *smi)
@@ -5599,14 +5601,11 @@ void swap_bsp_defpoints(ubyte * p)
ubyte * normcount = p+20;
vec3d *src = vp(p+offset);
model_allocate_interp_data(nverts, n_norms);
for (n=0; n<nverts; n++ ) {
src->xyz.x = INTEL_FLOAT( &src->xyz.x ); //tigital
src->xyz.y = INTEL_FLOAT( &src->xyz.y );
src->xyz.z = INTEL_FLOAT( &src->xyz.z );
Interp_verts[n] = src;
src++; //tigital
for (i=0; i<normcount[n]; i++){
@@ -5709,32 +5708,6 @@ void swap_bsp_flatpoly( polymodel * pm, ubyte * p )
for (i=0; i<nv*2; i++){
verts[i] = INTEL_SHORT( verts[i] );
}
if ( pm->version < 2003 ) {
// Set the "normal_point" part of field to be the center of the polygon
vec3d center_point;
vm_vec_zero( &center_point );
for (i=0;i<nv;i++) {
vm_vec_add2( &center_point, Interp_verts[verts[i*2]] );
}
center_point.xyz.x /= nv;
center_point.xyz.y /= nv;
center_point.xyz.z /= nv;
*vp(p+20) = center_point;
float rad = 0.0f;
for (i=0;i<nv;i++) {
float dist = vm_vec_dist( &center_point, Interp_verts[verts[i*2]] );
if ( dist > rad ) {
rad = dist;
}
}
fl(p+32) = rad;
}
}
void swap_bsp_sortnorm2(polymodel* pm, ubyte* p)
-13
View File
@@ -44,7 +44,6 @@
extern int Model_texturing;
extern int Model_polys;
extern int tiling;
extern float model_radius;
extern bool Scene_framebuffer_in_frame;
color Wireframe_color;
@@ -2783,16 +2782,6 @@ void model_render_queue(const model_render_params* interp, model_draw_list* scen
Lab_object_detail_level = detail_level;
}
// #ifndef NDEBUG
// if ( Interp_detail_level == 0 ) {
// MONITOR_INC( NumHiModelsRend, 1 );
// } else if ( Interp_detail_level == pm->n_detail_levels-1 ) {
// MONITOR_INC( NumLowModelsRend, 1 );
// } else {
// MONITOR_INC( NumMedModelsRend, 1 );
// }
// #endif
vec3d auto_back = ZERO_VECTOR;
bool set_autocen = model_render_determine_autocenter(&auto_back, pm, detail_level, model_flags);
@@ -2900,8 +2889,6 @@ void model_render_queue(const model_render_params* interp, model_draw_list* scen
i = pm->submodel[i].next_sibling;
}
model_radius = pm->submodel[pm->detail[detail_level]].rad;
//*************************** draw the hull of the ship *********************************************
vec3d view_pos = scene->get_view_position();
-4
View File
@@ -88,14 +88,10 @@ extern const ubyte* Macro_ubyte_bounds;
void model_calc_bound_box(vec3d *box, const vec3d *big_mn, const vec3d *big_mx);
void interp_clear_instance();
// endian swapping stuff - tigital
void swap_bsp_data( polymodel *pm, void *model_ptr );
// endian swapping stuff - kaz
void swap_sldc_data(ubyte *buffer);
extern vec3d **Interp_verts;
#endif
+10 -23
View File
@@ -542,31 +542,18 @@ bool text_input_handler(const SDL_Event& evt)
return false;
}
// libRocket expects UCS-2 so we first need to convert to that
// NOTE: not using SDL_iconv_utf8_ucs2() macro here due to Linux compatiblity issue
#if SDL_BYTEORDER == SDL_BIG_ENDIAN
const char *tocode = "UCS-2BE";
#else
const char *tocode = "UCS-2LE";
#endif
auto text = reinterpret_cast<char16_t *>(SDL_iconv_string(tocode, "UTF-8", evt.text.text, SDL_strlen(evt.text.text)+1));
if ( !text ) {
// encoding failed
return false;
// libRocket accepts one UCS-2 character at a time, so codepoints outside the BMP are replaced
const char* text = evt.text.text;
bool not_consumed = true;
while (*text != '\0')
{
auto codepoint = SDL_StepUTF8(&text, nullptr);
if (codepoint > 0xFFFF)
codepoint = SDL_INVALID_UNICODE_CODEPOINT;
not_consumed = input_context->ProcessTextInput(static_cast<word>(codepoint)) && not_consumed;
}
auto ucs2String = std::u16string(text);
SDL_free(text);
bool consumed = true;
for (auto& c : ucs2String) {
consumed = consumed && input_context->ProcessTextInput(c);
}
return consumed;
return not_consumed;
}
} // namespace
-1
View File
@@ -1024,7 +1024,6 @@ ADE_FUNC(drawModel, l_Graphics, "model model, vector position, orientation orien
//Draw the ship!!
model_clear_instance(model_num);
model_set_detail_level(0);
model_render_params render_info;
render_info.set_detail_level_lock(0);
+5 -1
View File
@@ -482,9 +482,13 @@ ADE_FUNC(isWeaponAllowedOnShip,
return ade_set_error(L, "b", false);
};
bank--; // Convert from Lua
// The restriction lists keep secondaries at MAX_SHIP_PRIMARY_BANKS onward, not right after this class's primaries
if (bank >= Ship_info[idx].num_primary_banks)
bank += MAX_SHIP_PRIMARY_BANKS - Ship_info[idx].num_primary_banks;
bool retv = false;
if ((bank >= 0) && (eval_weapon_flag_for_game_type(Ship_info[idx].restricted_loadout_flag[bank]))) {
if ((bank >= 0) && !Ship_info[idx].restricted_loadout_flag.empty() &&
(eval_weapon_flag_for_game_type(Ship_info[idx].restricted_loadout_flag[bank]))) {
if (eval_weapon_flag_for_game_type(Ship_info[idx].allowed_bank_restricted_weapons[bank][wepidx]))
retv = true;
} else if (eval_weapon_flag_for_game_type(Ship_info[idx].allowed_weapons[wepidx])) {
+5 -4
View File
@@ -323,7 +323,7 @@ ADE_INDEXER(l_Loadout_Weapon,
// Decide if we're setting a valid primary or secondary bank on the current ship class
if (bank <= sip->num_primary_banks) {
bankType = WP_LASER;
} else if (bank <= (sip->num_secondary_banks + MAX_SHIP_PRIMARY_BANKS)) {
} else if (bank > MAX_SHIP_PRIMARY_BANKS && bank <= (sip->num_secondary_banks + MAX_SHIP_PRIMARY_BANKS)) {
bankType = WP_MISSILE;
}
@@ -334,14 +334,15 @@ ADE_INDEXER(l_Loadout_Weapon,
LuaError(L, "This property may not be modified in multiplayer.");
return ADE_RETURN_NIL;
}
if (bankType == -1 || wepIndex > weapon_info_size()) {
if (bankType == -1 || wepIndex >= weapon_info_size()) {
LuaError(L, "The provided bank or weapon index is invalid!");
} else {
bool valid = false;
//First we check that this is a valid bank for the ship and that the script is not trying to empty the bank
if (wepIndex > -1) {
// Next we check that this is a weapon the ship can carry
if (eval_weapon_flag_for_game_type(sip->restricted_loadout_flag[bank])) {
if (!sip->restricted_loadout_flag.empty() &&
eval_weapon_flag_for_game_type(sip->restricted_loadout_flag[bank])) {
if (eval_weapon_flag_for_game_type(sip->allowed_bank_restricted_weapons[bank][wepIndex])) {
valid = true;
}
@@ -356,10 +357,10 @@ ADE_INDEXER(l_Loadout_Weapon,
weapon_info* wip = &Weapon_info[wepIndex];
if (wip->subtype == bankType) {
current.getBank()->wep[bank] = wepIndex;
current.getBank()->wep_count[bank] = 0;
} else {
LuaError(L, "Cannot put a primary in a secondary bank or a secondary in a primary bank!");
}
current.getBank()->wep_count[bank] = 0;
}
else {
//If the ship cannot carry this weapon in this bank then we set it to -1 and the amount to -1
+7 -2
View File
@@ -550,7 +550,7 @@ bool AudioStream::ServiceBuffer (void)
// nprintf(("Sound","SOUND => Volume for stream sound is %d\n",vol));
// nprintf(("Alan","Cuttoff Volume is: %d\n",m_lCutoffVolume));
if ( vol < m_lCutoffVolume ) {
if ( vol <= m_lCutoffVolume ) {
m_bFade = false;
m_lCutoffVolume = 0.0f;
@@ -711,7 +711,7 @@ void AudioStream::Play (float volume, int looping)
Set_Volume(volume);
// Kick off timer to service buffer
m_timer.constructor();
m_timer.destructor();
m_timer.Create (m_nBufService, m_nBufService, ptr_u (this), TimerCallback);
@@ -1111,6 +1111,9 @@ void audiostream_play(int i, float volume, int looping)
CAP(volume, 0.0f, 1.0f);
Assert( Audio_streams[i].status == ASF_USED );
if ( Audio_streams[i].status != ASF_USED )
return;
Audio_streams[i].Set_Default_Volume(volume);
Audio_streams[i].Play(volume, looping);
}
@@ -1139,6 +1142,8 @@ void audiostream_stop(int i, int rewind, int paused)
Assert( i >= 0 && i < MAX_AUDIO_STREAMS );
Assert( Audio_streams[i].status == ASF_USED );
if ( Audio_streams[i].status != ASF_USED )
return;
if ( rewind )
Audio_streams[i].Stop_and_Rewind();
+13 -10
View File
@@ -330,16 +330,19 @@ void ds_init_channels()
if (Cmdline_no_enhanced_sound)
MAX_CHANNELS = 32; // Old sound code limts
else {
ALCint size;
ALCint size = 0;
alcGetIntegerv(ds_sound_device, ALC_ATTRIBUTES_SIZE, 1, &size);
std::vector<ALCint> attrs(size);
alcGetIntegerv(ds_sound_device, ALC_ALL_ATTRIBUTES, size, &attrs[0]);
for (size_t i = 0; i < attrs.size(); ++i) {
//We reserve a portion of the reported channels to avoid colliding with other types of sound
//Using the 32 channels of the no_enhanced_sound as a floor.
if (attrs[i] == ALC_MONO_SOURCES && attrs[i+1] - DS_RESERVED_CHANNELS > 32) {
MAX_CHANNELS = attrs[i + 1] - DS_RESERVED_CHANNELS;
mprintf((" ALC reported %i available sources, setting max to use at %i ", attrs[i + 1],MAX_CHANNELS));
if (size > 0) {
std::vector<ALCint> attrs(size);
alcGetIntegerv(ds_sound_device, ALC_ALL_ATTRIBUTES, size, attrs.data());
// the list is zero-terminated key/value pairs
for (size_t i = 0; i + 1 < attrs.size() && attrs[i] != 0; i += 2) {
//We reserve a portion of the reported channels to avoid colliding with other types of sound
//Using the 32 channels of the no_enhanced_sound as a floor.
if (attrs[i] == ALC_MONO_SOURCES && attrs[i+1] - DS_RESERVED_CHANNELS > 32) {
MAX_CHANNELS = attrs[i + 1] - DS_RESERVED_CHANNELS;
mprintf((" ALC reported %i available sources, setting max to use at %i ", attrs[i + 1],MAX_CHANNELS));
}
}
}
}
@@ -528,7 +531,7 @@ int ds_init()
{
ALCint freq = 0;
OpenAL_ErrorPrint( alcGetIntegerv(ds_sound_device, ALC_FREQUENCY, sizeof(ALCint), &freq) );
OpenAL_ErrorPrint( alcGetIntegerv(ds_sound_device, ALC_FREQUENCY, 1, &freq) );
mprintf((" Sample rate: %d (%d)\n", freq, sample_rate));
}
+1 -1
View File
@@ -335,7 +335,7 @@ static void find_playback_device(OpenALInformation* info)
alcGetError(device);
// check how many sources we can create
static const int MIN_SOURCES = 48; // MAX_CHANNELS + 16 spare
static const int MIN_SOURCES = 48; // the 32 channels of no_enhanced_sound + 16 spare
int si = 0;
for (si = 0; si < MIN_SOURCES; si++) {
+5
View File
@@ -269,6 +269,7 @@ add_file_folder("Default files\\\\data\\\\effects"
def_files/data/effects/post-v.sdr
def_files/data/effects/rocketui-f.sdr
def_files/data/effects/rocketui-v.sdr
def_files/data/effects/shadow_cascade_params.sdr
def_files/data/effects/shadow_map-f.sdr
def_files/data/effects/shadow_map-g.sdr
def_files/data/effects/shadow_map-v.sdr
@@ -481,6 +482,8 @@ add_file_folder("Graphics"
graphics/shader_types.cpp
graphics/shader_types.h
graphics/render_queue.h
graphics/rtao.cpp
graphics/rtao.h
graphics/shadows.cpp
graphics/shadows.h
graphics/tmapper.h
@@ -1760,6 +1763,8 @@ add_file_folder("Starfield"
starfield/nebula.h
starfield/starfield.cpp
starfield/starfield.h
starfield/sun_disc.cpp
starfield/sun_disc.h
starfield/supernova.cpp
starfield/supernova.h
starfield/starfield_flags.h
+189 -80
View File
@@ -34,6 +34,8 @@
#include "render/batching.h"
#include "starfield/nebula.h"
#include "starfield/starfield.h"
#include "graphics/shadows.h"
#include "starfield/sun_disc.h"
#include "starfield/supernova.h"
#include "tracing/tracing.h"
#include "utils/Random.h"
@@ -92,6 +94,10 @@ typedef struct starfield_bitmap {
int glow_fps;
int xparent;
float r, g, b, i; // only for suns
float angular_size; // only for suns -- this sun's apparent diameter from stars.tbl;
// see SUN_ANGULAR_SIZE_UNSPECIFIED
float disc_fraction; // only for suns -- sun_disc_measure_bitmap() result for bitmap_id,
// set by sun_bitmap_load(); negative means "not measured"
int glare; // only for suns
int flare; // Is there a lens-flare for this sun?
flare_info flare_infos[MAX_FLARE_COUNT]; // each flare can use a texture in flare_bmp, with different scale
@@ -107,11 +113,14 @@ typedef struct starfield_bitmap_instance {
float scale_x, scale_y; // x and y scale
int div_x, div_y; // # of x and y divisions
angles ang; // angles from FRED
float angular_size; // only for suns -- this mission's apparent diameter for the sun,
// in degrees; see SUN_ANGULAR_SIZE_UNSPECIFIED
int star_bitmap_index; // index into starfield_bitmap array
int n_verts;
vertex *verts;
starfield_bitmap_instance() : scale_x(1.0f), scale_y(1.0f), div_x(1), div_y(1), star_bitmap_index(0), n_verts(0), verts(NULL) {
starfield_bitmap_instance() : scale_x(1.0f), scale_y(1.0f), div_x(1), div_y(1),
angular_size(SUN_ANGULAR_SIZE_UNSPECIFIED), star_bitmap_index(0), n_verts(0), verts(nullptr) {
ang.p = 0.0f;
ang.b = 0.0f;
ang.h = 0.0f;
@@ -124,6 +133,8 @@ static SCP_vector<starfield_bitmap_instance> Starfield_bitmap_instances;
// sun bitmaps and sun glow bitmaps
static SCP_vector<starfield_bitmap> Sun_bitmaps;
float Sun_angular_size_override = -1.0f;
static SCP_vector<starfield_bitmap_instance> Suns;
// Goober5000
@@ -400,6 +411,8 @@ static void starfield_bitmap_entry_init(starfield_bitmap *sbm)
sbm->bitmap_id = -1;
sbm->glow_bitmap = -1;
sbm->glow_n_frames = 1;
sbm->angular_size = SUN_ANGULAR_SIZE_UNSPECIFIED;
sbm->disc_fraction = -1.0f;
for (i = 0; i < MAX_FLARE_BMP; i++) {
sbm->flare_bitmaps[i].bitmap_id = -1;
@@ -497,6 +510,17 @@ void parse_startbl(const char *filename)
Warning(LOCATION, "%s", warning.c_str()); // customized case is significant
}
// apparent diameter of the sun's disc in degrees (Sol is ~0.53). It sets the
// shadow softness of both shadow methods. Left out, the size is
// measured from the sun bitmap instead -- see sun_angular_radius_tangent() -- so
// use 0 to ask for hard shadows and "derived" to say "measure it" outright.
if (optional_string("$SunAngularSize:")) {
if ( !optional_string("derived") ) {
stuff_float(&sbm.angular_size);
sbm.angular_size = stars_check_angular_size(sbm.angular_size, "$SunAngularSize:", sbm.filename);
}
}
// lens flare stuff
if (optional_string("$Flare:")) {
sbm.flare = 1;
@@ -701,6 +725,77 @@ void parse_startbl(const char *filename)
}
}
/**
* @brief Measures how much of a sun's bitmap is its emitting disc, and stores it in disc_fraction
*
* This reads the bitmap's pixels on the CPU, so it runs once, when the bitmap loads, and never
* in the draw path. The size only softens shadows, so with shadows off it does not read the
* bitmap. A sun whose stars.tbl entry gives a size also does not need the measurement.
*/
static void sun_measure_disc(starfield_bitmap& sb)
{
sb.disc_fraction = 0.0f;
if ((sb.bitmap_id < 0) || (Shadow_quality == ShadowQuality::Disabled) || (sb.angular_size >= 0.0f)) {
return;
}
const float measured = sun_disc_measure_bitmap(sb.bitmap_id);
// both a bitmap we couldn't read and one with no drawn disc at all fall back to hard
// shadows, but they are very different things -- keep them apart in the log, or a broken
// read reads as "working as intended". Neither is worth a Warning(): this runs for every
// sun in every mission, not just for content that asked for it, and hard shadows are a
// perfectly serviceable outcome
if (measured < 0.0f) {
mprintf(("Sun '%s': could not read the bitmap to measure it, using hard shadows\n", sb.filename));
} else if (measured == 0.0f) {
// mods ship blank sun bitmaps to get a light source without a visible sun
mprintf(("Sun '%s': no drawn disc, using hard shadows\n", sb.filename));
} else {
// logged once per sun bitmap load -- this is what a mod tunes its suns against. The
// size is quoted at +Scale: 1.0 since the measurement is a property of the bitmap; a
// mission scaling its sun scales this with it.
mprintf(("Sun '%s': measured disc fraction %.3f (%.2f degrees at +Scale: 1.0)\n", sb.filename, measured,
fl_degrees(2.0f * atanf(sun_disc_tangent_from_fraction(measured, 1.0f)))));
}
sb.disc_fraction = (measured > 0.0f) ? measured : 0.0f;
}
/**
* @brief Loads the main bitmap of a sun if it is not loaded, then measures its disc
*
* Tries a static image first, then an animation. All sun bitmap loads go through this
* function, so disc_fraction always belongs to the loaded bitmap_id.
*
* @param warn whether to show a Warning() when the bitmap does not load
* @return true if the sun has a loaded bitmap
*/
static bool sun_bitmap_load(starfield_bitmap& sb, bool warn)
{
if (sb.bitmap_id >= 0) {
return true;
}
sb.bitmap_id = bm_load(sb.filename);
// maybe didn't load a static image so try for an animated one
if (sb.bitmap_id < 0) {
sb.bitmap_id = bm_load_animation(sb.filename, &sb.n_frames, &sb.fps, nullptr, nullptr, true);
if (sb.bitmap_id < 0) {
if (warn) {
Warning(LOCATION, "Unable to load sun bitmap: '%s'!\n", sb.filename);
}
return false;
}
}
sun_measure_disc(sb);
return true;
}
void stars_load_all_bitmaps()
{
static int Star_bitmaps_loaded = 0;
@@ -733,18 +828,7 @@ void stars_load_all_bitmaps()
for (auto &sb : Sun_bitmaps) {
// normal bitmap
if (sb.bitmap_id < 0) {
sb.bitmap_id = bm_load(sb.filename);
// maybe didn't load a static image so try for an animated one
if (sb.bitmap_id < 0) {
sb.bitmap_id = bm_load_animation(sb.filename, &sb.n_frames, &sb.fps, nullptr, nullptr, true);
if (sb.bitmap_id < 0) {
Warning(LOCATION, "Unable to load sun bitmap: '%s'!\n", sb.filename);
}
}
}
sun_bitmap_load(sb, true);
// glow bitmap
if (sb.glow_bitmap < 0) {
@@ -855,6 +939,9 @@ void stars_pre_level_init(bool clear_backgrounds)
sb.bitmap_id = -1;
}
// the measurement belongs to the released bitmap; sun_bitmap_load() measures again
sb.disc_fraction = -1.0f;
if (sb.glow_bitmap > 0) {
bm_release(sb.glow_bitmap);
sb.glow_bitmap = -1;
@@ -1270,6 +1357,61 @@ void stars_get_sun_pos(int sun_n, vec3d *pos)
vm_vec_unrotate(pos, &temp, &rot);
}
/**
* @brief The apparent diameter this sun should use, in degrees, or negative if nothing sets one
*
* @param mission_angular_size this sun instance's +AngularSize: from the mission file
*/
static float sun_explicit_angular_size(const starfield_bitmap *bm, float mission_angular_size)
{
// the first layer that specifies a size wins: the LabUi session override, then the
// mission's value for this sun instance, then the sun's stars.tbl entry
for (float degrees : {Sun_angular_size_override, mission_angular_size, bm->angular_size}) {
if (degrees >= 0.0f) {
return degrees;
}
}
return SUN_ANGULAR_SIZE_UNSPECIFIED;
}
float stars_check_angular_size(float degrees, const char* field, const char* sun_filename)
{
if (degrees < 0.0f) {
error_display(0, "%s for sun '%s' must be >= 0 (got %f); ignoring it.", field, sun_filename, degrees);
return SUN_ANGULAR_SIZE_UNSPECIFIED;
}
if (degrees > SUN_ANGULAR_SIZE_MAX) {
error_display(0, "%s for sun '%s' must be <= %.0f (got %f); using %.0f.", field, sun_filename,
SUN_ANGULAR_SIZE_MAX, degrees, SUN_ANGULAR_SIZE_MAX);
return SUN_ANGULAR_SIZE_MAX;
}
return degrees;
}
/**
* @brief Tangent of a sun's angular radius, which is what a directional light's source_radius is
*
* See traceShadowRayCone() in shadows.sdr: for directional lights the source radius *is* the
* tangent of the angular radius, so the penumbra widens with occluder distance the way a real
* area light's would. 0 means a point source, i.e. hard shadows.
*
* @param scale_x the mission's +Scale: for this sun instance
* @param mission_angular_size this sun instance's +AngularSize: from the mission file
*/
static float sun_angular_radius_tangent(starfield_bitmap *bm, float scale_x, float mission_angular_size)
{
const float specified = sun_explicit_angular_size(bm, mission_angular_size);
if (specified >= 0.0f) {
return sun_disc_tangent_from_diameter(specified);
}
// sun_bitmap_load() measured the disc when the bitmap loaded. A sun that was not
// measured (for example, with shadows off) gets hard shadows.
return sun_disc_tangent_from_fraction(std::max(bm->disc_fraction, 0.0f), scale_x);
}
// draw sun
void stars_draw_sun(int show_sun)
{
@@ -1318,16 +1460,21 @@ void stars_draw_sun(int show_sun)
sun_dir = sun_pos;
vm_vec_normalize(&sun_dir);
// add the light source corresponding to the sun, except when rendering to an envmap
if ( !Rendering_to_env )
light_add_directional(&sun_dir, idx, !bm->glare, bm->i, bm->r, bm->g, bm->b);
// if supernova
if ( supernova_active() && (idx == 0) )
local_scale = 1.0f + (SUPERNOVA_SUN_SCALE * supernova_pct_complete());
else
local_scale = 1.0f;
// add the light source corresponding to the sun, except when rendering to an envmap.
// For directional lights, source_radius carries the tangent of the sun's angular
// radius (see traceShadowRayCone() in shadows.sdr), sizing its shadow penumbra.
// A measured size uses the drawn scale, so a growing supernova also softens its shadows.
if ( !Rendering_to_env ) {
light_add_directional(&sun_dir, idx, !bm->glare, bm->i, bm->r, bm->g, bm->b,
sun_angular_radius_tangent(bm, Suns[idx].scale_x * local_scale, Suns[idx].angular_size));
}
// draw the sun itself, keep track of how many we drew
int bitmap_id = -1;
if (bm->fps) {
@@ -1352,7 +1499,7 @@ void stars_draw_sun(int show_sun)
material mat_params;
material_set_unlit(&mat_params, bitmap_id, 0.999f, true, false);
g3_render_rect_screen_aligned_2d(&mat_params, &sun_vex, 0, 0.05f * Suns[idx].scale_x * local_scale, true);
g3_render_rect_screen_aligned_2d(&mat_params, &sun_vex, 0, SUN_QUAD_RADIUS * Suns[idx].scale_x * local_scale, true);
Sun_drew++;
// if ( !g3_draw_bitmap(&sun_vex, 0, 0.05f * Suns[idx].scale_x * local_scale, TMAP_FLAG_TEXTURED) )
@@ -2149,18 +2296,7 @@ void stars_page_in()
if (sb.preload) {
// normal bitmap
if (sb.bitmap_id < 0) {
sb.bitmap_id = bm_load(sb.filename);
// maybe didn't load a static image so try for an animated one
if (sb.bitmap_id < 0) {
sb.bitmap_id = bm_load_animation(sb.filename, &sb.n_frames, &sb.fps, nullptr, nullptr, true);
if (sb.bitmap_id < 0) {
Warning(LOCATION, "Unable to load sun bitmap: '%s'!\n", sb.filename);
}
}
}
sun_bitmap_load(sb, true);
// glow bitmap
if (sb.glow_bitmap < 0) {
@@ -2245,18 +2381,7 @@ void stars_page_in()
continue;
// normal bitmap
if (sb.bitmap_id < 0) {
sb.bitmap_id = bm_load(sb.filename);
// maybe didn't load a static image so try for an animated one
if (sb.bitmap_id < 0) {
sb.bitmap_id = bm_load_animation(sb.filename, &sb.n_frames, &sb.fps, nullptr, nullptr, true);
if (sb.bitmap_id < 0) {
Warning(LOCATION, "Unable to load sun bitmap: '%s'!\n", sb.filename);
}
}
}
sun_bitmap_load(sb, true);
// glow bitmap
if (sb.glow_bitmap < 0) {
@@ -2449,6 +2574,20 @@ int stars_find_bitmap(const char *name)
return -1;
}
// The mission-side list entry and the in-world instance carry the same fields, so keeping the
// copy in one place is what stops the call sites drifting apart. angular_size is only
// meaningful for suns, but copying it for bitmaps too is harmless and keeps every caller
// identical -- which beats being selectively correct in a way no reader can verify.
static void starfield_copy_entry_to_instance(const starfield_list_entry *sle, starfield_bitmap_instance &sbi)
{
sbi.ang = sle->ang;
sbi.scale_x = sle->scale_x;
sbi.scale_y = sle->scale_y;
sbi.div_x = sle->div_x;
sbi.div_y = sle->div_y;
sbi.angular_size = sle->angular_size;
}
// lookup a sun by bitmap filename, return index or -1 on fail
int stars_find_sun(const char *name)
{
@@ -2481,6 +2620,7 @@ void stars_get_data(bool is_sun, int idx, starfield_list_entry& sle)
sle.div_y = item.div_y;
sle.scale_x = item.scale_x;
sle.scale_y = item.scale_y;
sle.angular_size = item.angular_size;
}
void stars_set_data(bool is_sun, int idx, starfield_list_entry& sle)
@@ -2490,13 +2630,7 @@ void stars_set_data(bool is_sun, int idx, starfield_list_entry& sle)
if (!SCP_vector_inbounds(collection, idx))
return;
auto& item = collection[idx];
item.ang = sle.ang;
item.div_x = sle.div_x;
item.div_y = sle.div_y;
item.scale_x = sle.scale_x;
item.scale_y = sle.scale_y;
starfield_copy_entry_to_instance(&sle, collection[idx]);
// this is necessary when modifying bitmaps, but not when modifying suns
if (!is_sun)
@@ -2512,13 +2646,7 @@ int stars_add_sun_entry(starfield_list_entry *sun_ptr)
Assert(sun_ptr != NULL);
// copy information
sbi.ang.p = sun_ptr->ang.p;
sbi.ang.b = sun_ptr->ang.b;
sbi.ang.h = sun_ptr->ang.h;
sbi.scale_x = sun_ptr->scale_x;
sbi.scale_y = sun_ptr->scale_y;
sbi.div_x = sun_ptr->div_x;
sbi.div_y = sun_ptr->div_y;
starfield_copy_entry_to_instance(sun_ptr, sbi);
int idx = stars_find_sun(sun_ptr->filename);
@@ -2534,16 +2662,9 @@ int stars_add_sun_entry(starfield_list_entry *sun_ptr)
// make sure any needed bitmaps are loaded
if (Sun_bitmaps[idx].bitmap_id < 0) {
// normal bitmap
Sun_bitmaps[idx].bitmap_id = bm_load(Sun_bitmaps[idx].filename);
// maybe didn't load a static image so try for an animated one
if (Sun_bitmaps[idx].bitmap_id < 0) {
Sun_bitmaps[idx].bitmap_id = bm_load_animation(Sun_bitmaps[idx].filename, &Sun_bitmaps[idx].n_frames, &Sun_bitmaps[idx].fps, nullptr, nullptr, true);
if (Sun_bitmaps[idx].bitmap_id < 0) {
// failed
return -1;
}
if (!sun_bitmap_load(Sun_bitmaps[idx], false)) {
// failed
return -1;
}
// glow bitmap
@@ -2608,13 +2729,7 @@ int stars_add_bitmap_entry(starfield_list_entry *sle)
Assert(sle != NULL);
// copy information
sbi.ang.p = sle->ang.p;
sbi.ang.b = sle->ang.b;
sbi.ang.h = sle->ang.h;
sbi.scale_x = sle->scale_x;
sbi.scale_y = sle->scale_y;
sbi.div_x = sle->div_x;
sbi.div_y = sle->div_y;
starfield_copy_entry_to_instance(sle, sbi);
idx = stars_find_bitmap(sle->filename);
@@ -2876,13 +2991,7 @@ void stars_modify_entry_FRED(int index, const char *name, starfield_list_entry *
Assert( sbi_new != NULL );
// copy information
sbi.ang.p = sbi_new->ang.p;
sbi.ang.b = sbi_new->ang.b;
sbi.ang.h = sbi_new->ang.h;
sbi.scale_x = sbi_new->scale_x;
sbi.scale_y = sbi_new->scale_y;
sbi.div_x = sbi_new->div_x;
sbi.div_y = sbi_new->div_y;
starfield_copy_entry_to_instance(sbi_new, sbi);
if (is_a_sun) {
idx = stars_find_sun((char*)name);
+36
View File
@@ -29,11 +29,42 @@
// starfield list
// A sun's apparent diameter, in degrees. Every layer that can supply one -- the mission file's
// +AngularSize:, stars.tbl's $SunAngularSize:, and the LabUi session override -- uses this same
// encoding, so "the first one that specifies a size wins" is all the precedence logic there is
// (see sun_angular_radius_tangent() in starfield.cpp). Negative means that layer doesn't specify
// a size; 0 asks for hard shadows outright. The size sets the shadow softness of both shadow
// methods: the raytraced penumbra cone and the shadow-map filter width.
constexpr float SUN_ANGULAR_SIZE_UNSPECIFIED = -1.0f;
// Sol's apparent diameter from Earth -- the one figure anyone has an intuition for, so it's what
// the editors and the lab start from when switching a sun's size on.
constexpr float SUN_ANGULAR_SIZE_SOL = 0.53f;
// Well past anything plausible. The cap keeps the penumbra cone at a half angle of 45 degrees or
// less. Above 180 degrees the tangent becomes negative, and both shadow methods become hard.
constexpr float SUN_ANGULAR_SIZE_MAX = 90.0f;
// Checks a sun angular size from a table or mission file. A negative value gets an error and
// becomes SUN_ANGULAR_SIZE_UNSPECIFIED. A value above SUN_ANGULAR_SIZE_MAX gets an error and
// becomes SUN_ANGULAR_SIZE_MAX. field is the name of the parsed field, for the error message.
float stars_check_angular_size(float degrees, const char* field, const char* sun_filename);
typedef struct starfield_list_entry {
char filename[MAX_FILENAME_LEN]; // bitmap filename
float scale_x, scale_y; // x and y scale
int div_x, div_y; // # of x and y divisions
angles ang; // angles from FRED
float angular_size; // only for suns; see SUN_ANGULAR_SIZE_UNSPECIFIED
starfield_list_entry() : scale_x(1.0f), scale_y(1.0f), div_x(1), div_y(1),
angular_size(SUN_ANGULAR_SIZE_UNSPECIFIED)
{
filename[0] = '\0';
ang.p = 0.0f;
ang.b = 0.0f;
ang.h = 0.0f;
}
} starfield_list_entry;
// backgrounds
@@ -64,6 +95,11 @@ extern float Nmodel_alpha;
extern bool Motion_debris_override;
extern bool Motion_debris_enabled;
// Session-only override (LabUi) of every sun's $SunAngularSize, in degrees of
// apparent diameter -- sizes the penumbra of raytraced sun shadows. Negative
// (the default) means no override: each sun uses its stars.tbl value.
extern float Sun_angular_size_override;
struct motion_debris_bitmaps {
int bm;
int nframes;
+223
View File
@@ -0,0 +1,223 @@
#include "starfield/sun_disc.h"
#include "bmpman/bmpman.h"
#include "cfile/cfile.h"
#include "ddsutils/ddsutils.h"
#include "math/floating.h"
#include "starfield/starfield.h"
#include <cmath>
// How much of the drawn size (SUN_QUAD_RADIUS, sun_disc.h) to believe. The drawn sun is an art decision rather than a
// physical one: measured across the sun art of retail, the MediaVPs and several mods, the
// emitting disc subtends a median of ~3.9x Sol's 0.53 degrees once the mission's +Scale: is
// applied, and up to 26x in the worst retail mission. Taken literally that gives shadow edges
// tens of metres wide. This scales the measurement so that median MediaVPs content -- what
// nearly all modern mods ship -- lands near Sol; installs running retail's chunkier sun art
// land about twice as soft.
static const float Sun_derived_size_calibration = 0.25f;
// Ceiling on a derived sun's apparent diameter, in degrees. Missions scale their suns freely
// (retail's own missions go up to +Scale: 5.0), and penumbra width also decides how many rays
// are needed to resolve it without grain -- shadows_rt_sample_count() caps at
// RT_SHADOW_MAX_SAMPLES.
static const float Sun_derived_max_diameter = 1.5f;
float sun_disc_tangent_from_diameter(float degrees)
{
return tanf(fl_radians(degrees) * 0.5f);
}
float sun_disc_tangent_from_fraction(float disc_fraction, float scale_x)
{
if (disc_fraction <= 0.0f) {
return 0.0f;
}
return MIN(SUN_QUAD_RADIUS * scale_x * disc_fraction * Sun_derived_size_calibration,
sun_disc_tangent_from_diameter(Sun_derived_max_diameter));
}
void sun_disc_build_weights(const ubyte* pixels, int width, int height, int bytes_per_pixel, bool has_alpha,
SCP_vector<ubyte>& out_weights)
{
Assertion(pixels != nullptr, "sun_disc_build_weights() called with no pixel data!");
Assertion(bytes_per_pixel == 3 || bytes_per_pixel == 4, "sun_disc_build_weights() only handles BGR and BGRA, got %d bytes per pixel!", bytes_per_pixel);
if (width <= 0 || height <= 0) {
out_weights.clear();
return;
}
const size_t num_pixels = static_cast<size_t>(width) * static_cast<size_t>(height);
out_weights.resize(num_pixels);
const bool use_alpha = has_alpha && (bytes_per_pixel == 4);
for (size_t i = 0; i < num_pixels; i++) {
const ubyte* px = pixels + (i * static_cast<size_t>(bytes_per_pixel));
// channel order doesn't matter to the maximum, so this works for both BGR and RGB
ubyte weight = MAX(px[0], MAX(px[1], px[2]));
if (use_alpha) {
weight = static_cast<ubyte>((static_cast<int>(weight) * static_cast<int>(px[3])) / 255);
}
out_weights[i] = weight;
}
}
void sun_disc_build_weights_16(const ubyte* pixels, int width, int height, bool has_alpha,
SCP_vector<ubyte>& out_weights)
{
Assertion(pixels != nullptr, "sun_disc_build_weights_16() called with no pixel data!");
if (width <= 0 || height <= 0) {
out_weights.clear();
return;
}
const size_t num_pixels = static_cast<size_t>(width) * static_cast<size_t>(height);
out_weights.resize(num_pixels);
// bm_get_components() reads through whichever format bmpman currently has selected, and
// bm_load_image_data() leaves that on the screen format, so point it at the texture format
// for the duration -- the same select/restore bmpman does around its own loaders
BM_SELECT_TEX_FORMAT();
for (size_t i = 0; i < num_pixels; i++) {
ubyte r = 0;
ubyte g = 0;
ubyte b = 0;
ubyte a = 1;
bm_get_components(const_cast<ubyte*>(pixels + (i * 2)), &r, &g, &b, has_alpha ? &a : nullptr);
// the texture format only carries one bit of alpha (Gr_t_alpha), and bm_get_components()
// hands it back as 0 or 1 rather than 0-255, so it masks rather than scales. The colour
// channels are 5 bits each, so weights off this path are quantized to 1/31 -- coarser
// than the 8-bit paths, but far finer than the measurement needs.
out_weights[i] = (has_alpha && (a == 0)) ? static_cast<ubyte>(0) : MAX(r, MAX(g, b));
}
BM_SELECT_SCREEN_FORMAT();
}
float sun_disc_fraction_from_weights(const ubyte* weights, int width, int height)
{
if ((weights == nullptr) || (width <= 0) || (height <= 0)) {
return 0.0f;
}
const size_t num_pixels = static_cast<size_t>(width) * static_cast<size_t>(height);
ubyte peak = 0;
for (size_t i = 0; i < num_pixels; i++) {
if (weights[i] > peak) {
peak = weights[i];
}
}
// blank or near-blank art has no disc to measure
if (peak < SUN_DISC_MIN_PEAK_WEIGHT) {
return 0.0f;
}
const auto cutoff = static_cast<ubyte>(std::ceil(SUN_DISC_THRESHOLD * static_cast<float>(peak)));
size_t disc_pixels = 0;
for (size_t i = 0; i < num_pixels; i++) {
if (weights[i] >= cutoff) {
disc_pixels++;
}
}
if (disc_pixels == 0) {
return 0.0f;
}
const float radius = fl_sqrt(static_cast<float>(disc_pixels) / PI);
const float half_edge = 0.5f * static_cast<float>(MIN(width, height));
// a bitmap lit all the way into its corners measures slightly over 1; the disc still
// can't be bigger than the quad it is drawn on
return MIN(radius / half_edge, 1.0f);
}
float sun_disc_measure_bitmap(int bitmap_handle)
{
if (bitmap_handle < 0) {
return -1.0f;
}
const bool has_alpha = bm_has_alpha_channel(bitmap_handle);
int width = 0;
int height = 0;
SCP_vector<ubyte> weights;
if (bm_is_compressed(bitmap_handle)) {
// bm_lock() hands back block-compressed data as-is whenever the renderer supports
// S3TC/BPTC (see bm_lock_dds()), so this path has to go around bmpman and decompress
// the file itself. Sun art really does ship compressed, so it isn't an edge case.
const char* filename = bm_get_image_filename(bitmap_handle);
if ((filename == nullptr) || (*filename == '\0')) {
return -1.0f;
}
SCP_vector<ubyte> pixels;
if (dds_decompress_top_mip_bgra(filename, CF_TYPE_ANY, &width, &height, pixels) != DDS_ERROR_NONE) {
return -1.0f;
}
sun_disc_build_weights(pixels.data(), width, height, 4, has_alpha, weights);
} else {
bitmap* bmp = bm_lock(bitmap_handle, 32, BMP_TEX_XPARENT);
if (bmp == nullptr) {
return -1.0f;
}
if (bmp->data == 0) {
// bm_lock() took a reference before it failed to populate the data
bm_unlock(bitmap_handle);
return -1.0f;
}
// The requested bpp is a request, not a guarantee: bm_lock() ignores it for JPGs (always
// 24-bit BGR) and uncompressed DDS (whatever the file uses), and for anything already
// paged in it returns the data in the format page-in chose -- 16-bit for textures, see
// bm_page_in_stop(). Handle each layout rather than assuming the 32 we asked for.
width = bmp->w;
height = bmp->h;
const auto* pixels = reinterpret_cast<const ubyte*>(bmp->data);
if (bmp->bpp == 32) {
sun_disc_build_weights(pixels, width, height, 4, has_alpha, weights);
} else if (bmp->bpp == 24) {
sun_disc_build_weights(pixels, width, height, 3, false, weights);
} else if (bmp->bpp == 16) {
sun_disc_build_weights_16(pixels, width, height, has_alpha, weights);
} else {
bm_unlock(bitmap_handle);
bm_unload_fast(bitmap_handle);
return -1.0f;
}
bm_unlock(bitmap_handle);
// The measurement runs when the bitmap loads, before page-in. Free the CPU copy that
// bm_lock() made, so that page-in loads the texture in its usual format. This frees
// only CPU memory. A texture that the GPU already has stays as it is.
bm_unload_fast(bitmap_handle);
}
if (weights.empty()) {
return -1.0f;
}
return sun_disc_fraction_from_weights(weights.data(), width, height);
}
+100
View File
@@ -0,0 +1,100 @@
#pragma once
#include "globalincs/pstypes.h"
// Measures how much of a sun bitmap is the emitting disc, as opposed to the glow/corona
// painted around it. This is how a sun's apparent angular size is worked out when its table
// entry doesn't give one outright, which is the usual case -- see sun_angular_radius_tangent()
// in starfield.cpp for how the measurement becomes a shadow penumbra, and $SunAngularSize: in
// stars.tbl for how a table opts out of it.
// Radius of the drawn sun quad at +Scale: 1.0. stars_draw_sun() draws the quad with this radius
// times the scale, and g3_render_rect_screen_aligned_2d() makes that radius the tangent of the
// half angle that the quad subtends, for all FOVs, resolutions and aspect ratios.
constexpr float SUN_QUAD_RADIUS = 0.05f;
// Pixels at or above this fraction of the brightest pixel are part of the disc. The
// measurement changes only a small quantity with this value. We tested the sun art of retail,
// the MediaVPs and several mods. A value of 0.5 makes each bitmap approximately 1.37 times
// larger, and keeps their order almost the same (Spearman rank correlation 0.96). Thus the
// value acts as a constant factor, and the calibration in sun_disc.cpp compensates for it.
constexpr float SUN_DISC_THRESHOLD = 0.9f;
// Sun bitmaps whose brightest pixel is dimmer than this are treated as having no disc at
// all. Mods ship deliberately blank sun bitmaps (BtA's SunAntares*-BLANK, Blue Planet's
// SunSolDummy) to get a light source without a visible sun, and without this floor the
// threshold above would collapse to zero, match every pixel, and hand a sun that isn't
// drawn at all the widest penumbra in the game.
constexpr ubyte SUN_DISC_MIN_PEAK_WEIGHT = 8;
/**
* @brief Reduces BGR(A) pixels to the per-pixel weight the disc measurement works on
*
* The weight is how much the texel actually contributes where the sun quad is drawn, which
* depends on how it is blended: sun bitmaps without an alpha channel are drawn additively
* and bitmaps with one are alpha blended (see material_determine_blend_mode()).
*
* @param pixels BGR or BGRA pixel data, @a width * @a height * @a bytes_per_pixel bytes
* @param bytes_per_pixel 3 (BGR) or 4 (BGRA)
* @param has_alpha whether the alpha channel is meaningful; ignored unless BGRA
* @param[out] out_weights resized to @a width * @a height
*/
void sun_disc_build_weights(const ubyte* pixels, int width, int height, int bytes_per_pixel, bool has_alpha,
SCP_vector<ubyte>& out_weights);
/**
* @brief sun_disc_build_weights() for 16-bit texture-format pixels
*
* Bitmaps that went through bm_page_in_texture() are locked at 16 bpp (see bm_page_in_stop()),
* so anything but a compressed DDS is likely to be in the renderer's packed 16-bit texture
* format by the time a sun is drawn -- retail's PCX sun art included.
*/
void sun_disc_build_weights_16(const ubyte* pixels, int width, int height, bool has_alpha,
SCP_vector<ubyte>& out_weights);
/**
* @brief Radius of the emitting disc, as a fraction of the drawn quad's half-width
*
* Coverage based: the disc's area is taken as the number of pixels at or above
* SUN_DISC_THRESHOLD of the brightest one, converted to the radius of a circle of that
* area. That survives off-centre discs, baked-in flare spikes and the halo tail, all of
* which a luminance-weighted radius would be dominated by.
*
* Normalized against the shorter edge, because that is the edge
* g3_render_rect_screen_aligned_2d() fits the quad to.
*
* @return the disc radius in [0, 1], or 0 if the bitmap has no measurable disc
*/
float sun_disc_fraction_from_weights(const ubyte* weights, int width, int height);
/**
* @brief Tangent of the angular radius for an apparent diameter in degrees
*
* This is the form a directional light's source_radius takes -- see traceShadowRayCone() in
* shadows.sdr, where it sizes the shadow penumbra. 0 in, 0 out, i.e. hard shadows.
*/
float sun_disc_tangent_from_diameter(float degrees);
/**
* @brief Same tangent, for a sun whose size is measured from its bitmap rather than given
*
* Turns a disc fraction into an angular size using the geometry of the drawn sun quad, then
* applies the calibration and ceiling that keep the result usable (see the constants in
* sun_disc.cpp for why a measured sun can't be believed literally).
*
* @param scale_x the mission's +Scale: for this sun instance
* @return the tangent, or 0 for a sun with no measurable disc
*/
float sun_disc_tangent_from_fraction(float disc_fraction, float scale_x);
/**
* @brief Measures the disc fraction of a loaded sun bitmap
*
* Reads the bitmap's pixels on the CPU, so this is a one-shot operation -- callers must
* cache the result rather than calling it per frame. It frees the CPU copy of the pixels
* after the measurement, so call it when the bitmap loads, before page-in. For animated sun bitmaps this measures
* the first frame, which is the handle bm_load_animation() returns.
*
* @return the disc fraction, or a negative value if the bitmap could not be read
*/
float sun_disc_measure_bitmap(int bitmap_handle);
+45
View File
@@ -63,6 +63,8 @@ bg_bitmap_dlg::bg_bitmap_dlg(CWnd* pParent) : CDialog(bg_bitmap_dlg::IDD, pParen
s_bank = 0.f;
s_heading = 0.f;
s_scale = 1.0f;
s_angular_size_override = FALSE;
s_angular_size = SUN_ANGULAR_SIZE_SOL;
s_index = -1;
b_pitch = 0.f;
b_bank = 0.f;
@@ -116,6 +118,9 @@ void bg_bitmap_dlg::DoDataExchange(CDataExchange* pDX)
DDV_MinMaxFloat(pDX, s_heading, 0.f, DEGREE_UB);
DDX_Text(pDX, IDC_SUN1_SCALE, s_scale);
DDV_MinMaxFloat(pDX, s_scale, 0.1f, 50.0f);
DDX_Check(pDX, IDC_SUN1_ANGULAR_SIZE_OVERRIDE, s_angular_size_override);
DDX_Text(pDX, IDC_SUN1_ANGULAR_SIZE, s_angular_size);
DDV_MinMaxFloat(pDX, s_angular_size, 0.0f, SUN_ANGULAR_SIZE_MAX);
DDX_Text(pDX, IDC_SBITMAP, b_name);
DDX_Text(pDX, IDC_SBITMAP_P, b_pitch);
DDV_MinMaxFloat(pDX, b_pitch, 0.f, DEGREE_UB);
@@ -169,6 +174,7 @@ BEGIN_MESSAGE_MAP(bg_bitmap_dlg, CDialog)
ON_CBN_SELCHANGE(IDC_NEB2_TEXTURE, OnSelchangeNeb2Texture)
ON_WM_HSCROLL()
ON_LBN_SELCHANGE(IDC_SUN1_LIST, OnSunChange)
ON_BN_CLICKED(IDC_SUN1_ANGULAR_SIZE_OVERRIDE, OnSunAngularSizeOverride)
ON_BN_CLICKED(IDC_ADD_SUN, OnAddSun)
ON_BN_CLICKED(IDC_DEL_SUN, OnDelSun)
ON_CBN_SELCHANGE(IDC_SUN1, OnSunDropdownChange)
@@ -193,6 +199,7 @@ BEGIN_MESSAGE_MAP(bg_bitmap_dlg, CDialog)
ON_EN_KILLFOCUS(IDC_SUN1_H, OnKillfocusSun1H)
ON_EN_KILLFOCUS(IDC_SUN1_B, OnKillfocusSun1B)
ON_EN_KILLFOCUS(IDC_SUN1_SCALE, OnKillfocusSun1Scale)
ON_EN_KILLFOCUS(IDC_SUN1_ANGULAR_SIZE, OnKillfocusSun1AngularSize)
ON_BN_CLICKED(IDC_ADD_BACKGROUND, OnAddBackground)
ON_BN_CLICKED(IDC_REMOVE_BACKGROUND, OnRemoveBackground)
ON_BN_CLICKED(IDC_IMPORT_BACKGROUND, OnImportBackground)
@@ -794,6 +801,10 @@ void bg_bitmap_dlg::sun_data_init()
clb->SetCurSel(0);
OnSunChange();
}
else
{
update_sun_angular_size_enabled();
}
}
void bg_bitmap_dlg::sun_data_close()
@@ -822,6 +833,7 @@ void bg_bitmap_dlg::sun_data_save_current()
sle->scale_y = 1.0f;
sle->div_x = 1;
sle->div_y = 1;
sle->angular_size = s_angular_size_override ? s_angular_size : SUN_ANGULAR_SIZE_UNSPECIFIED;
}
}
@@ -846,6 +858,11 @@ void bg_bitmap_dlg::OnSunChange()
s_heading = fl_degrees_100ths(sle->ang.h);
s_scale = sle->scale_x;
// an unset angular size leaves the edit box showing Sol's, so that ticking the
// checkbox starts somewhere sensible rather than at whatever was last selected
s_angular_size_override = (sle->angular_size >= 0.0f) ? TRUE : FALSE;
s_angular_size = (sle->angular_size >= 0.0f) ? sle->angular_size : SUN_ANGULAR_SIZE_SOL;
// make sure angles are in the 0-359 degree range;
// an angle of 6.28318310, which is less than 6.28318548,
// is converted to 359.999847, which (if converted to int) is rounded to 360
@@ -862,6 +879,8 @@ void bg_bitmap_dlg::OnSunChange()
((CComboBox*) GetDlgItem(IDC_SUN1))->SetCurSel(drop_index);
}
update_sun_angular_size_enabled();
// refresh the background
stars_load_background(get_active_background());
}
@@ -1342,6 +1361,32 @@ void bg_bitmap_dlg::OnKillfocusSun1Scale()
OnSunChange();
}
void bg_bitmap_dlg::OnKillfocusSun1AngularSize()
{
if (s_index < 0) return;
get_data_float(IDC_SUN1_ANGULAR_SIZE, &s_angular_size, 0.0f, SUN_ANGULAR_SIZE_MAX, 3);
OnSunChange();
}
void bg_bitmap_dlg::OnSunAngularSizeOverride()
{
UpdateData(TRUE);
update_sun_angular_size_enabled();
sun_data_save_current();
}
// the size only means anything when the mission is actually setting one
void bg_bitmap_dlg::update_sun_angular_size_enabled()
{
CWnd *size_edit = GetDlgItem(IDC_SUN1_ANGULAR_SIZE);
if (size_edit != nullptr)
size_edit->EnableWindow((s_index >= 0) && s_angular_size_override);
CWnd *size_check = GetDlgItem(IDC_SUN1_ANGULAR_SIZE_OVERRIDE);
if (size_check != nullptr)
size_check->EnableWindow(s_index >= 0);
}
void bg_bitmap_dlg::OnDeltaposSkyboxPSpin(NMHDR* pNMHDR, LRESULT* pResult)
{
NM_UPDOWN* pNMUpDown = (NM_UPDOWN*)pNMHDR;
+8
View File
@@ -70,6 +70,11 @@ public:
float s_bank;
float s_heading;
float s_scale;
// Apparent diameter of this sun in degrees, and whether the mission sets one at all.
// Unchecked leaves the sun on its stars.tbl size, or on the size measured from its
// bitmap; see SUN_ANGULAR_SIZE_UNSPECIFIED in starfield.h.
BOOL s_angular_size_override;
float s_angular_size;
int s_index;
CString b_name;
float b_pitch;
@@ -116,6 +121,7 @@ protected:
int get_active_background();
int get_swap_background();
void reinitialize_lists();
void update_sun_angular_size_enabled();
void background_flags_init();
void background_flags_close();
@@ -141,6 +147,7 @@ protected:
afx_msg void OnKillfocusNeb2FogB();
afx_msg void OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar);
afx_msg void OnSunChange();
afx_msg void OnSunAngularSizeOverride();
afx_msg void OnAddSun();
afx_msg void OnDelSun();
afx_msg void OnSunDropdownChange();
@@ -165,6 +172,7 @@ protected:
afx_msg void OnKillfocusSun1H();
afx_msg void OnKillfocusSun1B();
afx_msg void OnKillfocusSun1Scale();
afx_msg void OnKillfocusSun1AngularSize();
afx_msg void OnAddBackground();
afx_msg void OnRemoveBackground();
afx_msg void OnImportBackground();
+2
View File
@@ -1808,6 +1808,8 @@ BEGIN
EDITTEXT IDC_SUN1_H,349,87,31,14,ES_AUTOHSCROLL
CONTROL "Spin2",IDC_SUN1_H_SPIN,"msctls_updown32",UDS_ARROWKEYS,381,87,11,14
EDITTEXT IDC_SUN1_SCALE,362,105,31,14,ES_AUTOHSCROLL
CONTROL "Size",IDC_SUN1_ANGULAR_SIZE_OVERRIDE,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,338,123,32,10
EDITTEXT IDC_SUN1_ANGULAR_SIZE,374,121,31,14,ES_AUTOHSCROLL
CONTROL "Full Nebula",IDC_FULLNEB,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,14,186,62,10
EDITTEXT IDC_NEB2_INTENSITY,68,201,63,12,ES_AUTOHSCROLL
COMBOBOX IDC_NEB2_TEXTURE,68,217,63,88,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP
+3 -1
View File
@@ -1311,6 +1311,8 @@
#define IDC_MESSAGE_MOVE_DOWN 1744
#define IDC_MESSAGE_MOVE_TO_BOTTOM 1745
#define IDC_INSERT_MSG 1746
#define IDC_SUN1_ANGULAR_SIZE_OVERRIDE 1747
#define IDC_SUN1_ANGULAR_SIZE 1748
#define IDC_SEXP_POPUP_LIST 32770
#define ID_FILE_MISSIONNOTES 32771
#define ID_DUPLICATE 32774
@@ -1639,7 +1641,7 @@
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_3D_CONTROLS 1
#define _APS_NEXT_RESOURCE_VALUE 340
#define _APS_NEXT_CONTROL_VALUE 1747
#define _APS_NEXT_CONTROL_VALUE 1749
#define _APS_NEXT_COMMAND_VALUE 33125
#define _APS_NEXT_SYMED_VALUE 105
#endif
-7
View File
@@ -1018,11 +1018,6 @@ void game_level_close()
if (Game_mode & GM_STANDALONE_SERVER) {
model_free_all(); // Free all existing models if standalone server
// clean out interp data as it's better to allocate as needed here instead
// of letting it use a bunch of memory
extern void model_deallocate_interp_data();
model_deallocate_interp_data();
}
}
else
@@ -2473,7 +2468,6 @@ void game_show_framerate()
modelstats_num_polys = 0;
modelstats_num_polys_drawn = 0;
modelstats_num_verts = 0;
modelstats_num_sortnorms = 0;
#endif
}
@@ -7527,7 +7521,6 @@ void Time_model( int modelnum )
model_render_params render_info;
render_info.set_detail_level_lock(0);
model_set_detail_level(0); // use highest detail level
model_render_immediate( &render_info, modelnum, &model_orient, &model_pos ); //|MR_NO_POLYS );
g3_end_frame();
@@ -730,6 +730,50 @@ void BackgroundEditorDialogModel::setSunScale(float v)
refreshBackgroundPreview();
}
bool BackgroundEditorDialogModel::getSunAngularSizeEnabled() const
{
auto* s = getActiveSun();
if (!s)
return false;
return s->angular_size >= 0.0f;
}
void BackgroundEditorDialogModel::setSunAngularSizeEnabled(bool enabled)
{
auto* s = getActiveSun();
if (!s)
return;
modify(s->angular_size, enabled ? (s->angular_size >= 0.0f ? s->angular_size : SUN_ANGULAR_SIZE_SOL)
: SUN_ANGULAR_SIZE_UNSPECIFIED);
refreshBackgroundPreview();
}
float BackgroundEditorDialogModel::getSunAngularSize() const
{
auto* s = getActiveSun();
if (!s || s->angular_size < 0.0f)
return SUN_ANGULAR_SIZE_SOL;
return s->angular_size;
}
void BackgroundEditorDialogModel::setSunAngularSize(float v)
{
auto* s = getActiveSun();
if (!s)
return;
// don't let a value edit turn the override on by itself -- that's the checkbox's job
if (s->angular_size < 0.0f)
return;
CLAMP(v, getSunAngularSizeLimit().first, getSunAngularSizeLimit().second);
modify(s->angular_size, v);
refreshBackgroundPreview();
}
SCP_vector<SCP_string> BackgroundEditorDialogModel::getLightningNames()
{
SCP_vector<SCP_string> out;
@@ -26,6 +26,7 @@ class BackgroundEditorDialogModel : public AbstractDialogModel {
static std::pair<float, float> getFloatOrientLimit() { return {0.f, DEGREE_UB}; }
static std::pair<float,float> getBitmapScaleLimit() { return {0.001f, 18.0f}; }
static std::pair<float,float> getSunScaleLimit() { return {0.1f, 50.0f}; }
static std::pair<float,float> getSunAngularSizeLimit() { return {0.0f, SUN_ANGULAR_SIZE_MAX}; }
static std::pair<int, int> getDivisionLimit() { return {1, 5}; }
static std::pair<int, int> getStarsLimit() { return {0, MAX_STARS}; }
@@ -82,6 +83,12 @@ class BackgroundEditorDialogModel : public AbstractDialogModel {
void setSunHeading(float deg);
float getSunScale() const; // uses scale_x for both x and y
void setSunScale(float v);
// Whether this mission sets the sun's apparent diameter at all. Off means the sun keeps
// its stars.tbl $SunAngularSize:, or the size measured from its bitmap.
bool getSunAngularSizeEnabled() const;
void setSunAngularSizeEnabled(bool enabled);
float getSunAngularSize() const; // apparent diameter in degrees
void setSunAngularSize(float v);
// nebula group
static SCP_vector<SCP_string> getLightningNames();
@@ -67,6 +67,8 @@ void BackgroundEditorDialog::initializeUi()
ui->sunPitchSpin->setRange(_model->getFloatOrientLimit().first, _model->getFloatOrientLimit().second);
ui->sunHeadingSpin->setRange(_model->getFloatOrientLimit().first, _model->getFloatOrientLimit().second);
ui->sunScaleDoubleSpinBox->setRange(_model->getSunScaleLimit().first, _model->getSunScaleLimit().second);
ui->sunAngularSizeDoubleSpinBox->setRange(_model->getSunAngularSizeLimit().first,
_model->getSunAngularSizeLimit().second);
const auto& sun_names = _model->getAvailableSunNames();
for (const auto& s : sun_names) {
@@ -253,6 +255,9 @@ void BackgroundEditorDialog::updateSunControls()
ui->sunPitchSpin->setEnabled(enabled);
ui->sunHeadingSpin->setEnabled(enabled);
ui->sunScaleDoubleSpinBox->setEnabled(enabled);
ui->sunAngularSizeCheckBox->setEnabled(enabled);
// the value only means anything when this mission is actually setting one
ui->sunAngularSizeDoubleSpinBox->setEnabled(enabled && _model->getSunAngularSizeEnabled());
const int index = ui->sunSelectionCombo->findText(QString::fromStdString(_model->getSunName()));
ui->sunSelectionCombo->setCurrentIndex(index);
@@ -260,6 +265,8 @@ void BackgroundEditorDialog::updateSunControls()
ui->sunPitchSpin->setValue(_model->getSunPitch());
ui->sunHeadingSpin->setValue(_model->getSunHeading());
ui->sunScaleDoubleSpinBox->setValue(_model->getSunScale());
ui->sunAngularSizeCheckBox->setChecked(_model->getSunAngularSizeEnabled());
ui->sunAngularSizeDoubleSpinBox->setValue(_model->getSunAngularSize());
}
void BackgroundEditorDialog::updateNebulaControls()
@@ -633,6 +640,24 @@ void BackgroundEditorDialog::on_sunHeadingSpin_valueChanged(double arg1)
_model->setSunHeading(static_cast<float>(arg1));
}
void BackgroundEditorDialog::on_sunAngularSizeCheckBox_toggled(bool checked)
{
_model->setSunAngularSizeEnabled(checked);
// take whatever the box is already showing, so ticking this can't quietly set a
// different size than the one on screen
if (checked)
_model->setSunAngularSize(static_cast<float>(ui->sunAngularSizeDoubleSpinBox->value()));
// toggling this enables or greys the value box next to it
updateSunControls();
}
void BackgroundEditorDialog::on_sunAngularSizeDoubleSpinBox_valueChanged(double arg1)
{
_model->setSunAngularSize(static_cast<float>(arg1));
}
void BackgroundEditorDialog::on_sunScaleDoubleSpinBox_valueChanged(double arg1)
{
_model->setSunScale(static_cast<float>(arg1));
@@ -49,6 +49,8 @@ private slots:
void on_sunPitchSpin_valueChanged(double arg1);
void on_sunHeadingSpin_valueChanged(double arg1);
void on_sunScaleDoubleSpinBox_valueChanged(double arg1);
void on_sunAngularSizeCheckBox_toggled(bool checked);
void on_sunAngularSizeDoubleSpinBox_valueChanged(double arg1);
void on_addSunButton_clicked();
void on_changeSunButton_clicked();
void on_deleteSunButton_clicked();
+35
View File
@@ -376,6 +376,41 @@
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="sunAngularSizeCheckBox">
<property name="toolTip">
<string>Set the apparent diameter of this sun for this mission. When this box is clear, the sun uses the size from stars.tbl, or a size measured from its bitmap.</string>
</property>
<property name="text">
<string>Angular size</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QDoubleSpinBox" name="sunAngularSizeDoubleSpinBox">
<property name="toolTip">
<string>The apparent diameter in degrees. The Sun, seen from Earth, is approximately 0.53 degrees. A larger size gives softer shadows, for raytraced shadows and for shadow maps.</string>
</property>
<property name="minimumSize">
<size>
<width>75</width>
<height>0</height>
</size>
</property>
<property name="suffix">
<string>°</string>
</property>
<property name="decimals">
<number>3</number>
</property>
<property name="maximum">
<double>90.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
</widget>
</item>
</layout>
</item>
</layout>
+23
View File
@@ -0,0 +1,23 @@
#include <gtest/gtest.h>
#include <bmpman/bmpman.h>
#include <cfile/cfile.h>
#include "util/FSTestFixture.h"
class EffTest : public test::FSTestFixture {
public:
EffTest() : test::FSTestFixture(INIT_CFILE) {
pushModDir("bmpman");
pushModDir("eff");
}
};
TEST_F(EffTest, reads_an_eff_longer_than_1024_bytes) {
int frames = 0, fps = 0, key = 0;
BM_TYPE type = BM_TYPE_NONE;
ASSERT_TRUE(bm_load_and_parse_eff("long_eff.eff", CF_TYPE_MAPS, &frames, &fps, &key, &type));
EXPECT_EQ(frames, 2);
EXPECT_EQ(fps, 15);
EXPECT_EQ(key, 1);
EXPECT_EQ(type, BM_TYPE_PNG);
}
+158
View File
@@ -0,0 +1,158 @@
#include "missionui/missionscreencommon.h"
#include "ship/ship.h"
#include "weapon/weapon.h"
#include "scripting/ScriptingTestFixture.h"
extern bool Ships_inited;
static ship_info make_ship_class(int num_primary_banks, int num_secondary_banks)
{
ship_info sip;
sip.num_primary_banks = num_primary_banks;
sip.num_secondary_banks = num_secondary_banks;
return sip;
}
static void allow_weapon(ship_info& sip, int weapon_class)
{
sip.allowed_weapons.set_flag(weapon_class, REGULAR_WEAPON);
}
static void restrict_bank(ship_info& sip, int bank, int weapon_class)
{
if (sip.restricted_loadout_flag.empty()) {
sip.restricted_loadout_flag.resize(MAX_SHIP_WEAPONS);
sip.allowed_bank_restricted_weapons.resize(MAX_SHIP_WEAPONS);
}
sip.restricted_loadout_flag[bank] = REGULAR_WEAPON;
sip.allowed_bank_restricted_weapons[bank].set_flag(weapon_class, REGULAR_WEAPON);
}
class ScriptingShipclassTest : public test::scripting::ScriptingTestFixture {
public:
ScriptingShipclassTest() : test::scripting::ScriptingTestFixture(INIT_CFILE)
{
pushModDir("shipclass");
}
protected:
wss_unit* _saved_wss_slots = nullptr;
void SetUp() override
{
test::scripting::ScriptingTestFixture::SetUp();
Ship_info.clear();
Weapon_info.clear();
Weapon_info.resize(4);
Weapon_info[0].subtype = WP_LASER;
Weapon_info[1].subtype = WP_LASER;
Weapon_info[2].subtype = WP_MISSILE;
Weapon_info[3].subtype = WP_MISSILE;
auto two_primaries = make_ship_class(2, 2);
for (int weapon_class = 0; weapon_class < 4; weapon_class++) {
allow_weapon(two_primaries, weapon_class);
}
restrict_bank(two_primaries, 0, 0);
restrict_bank(two_primaries, MAX_SHIP_PRIMARY_BANKS, 2);
restrict_bank(two_primaries, MAX_SHIP_PRIMARY_BANKS + 1, 3);
Ship_info.push_back(std::move(two_primaries));
auto one_primary = make_ship_class(1, 3);
allow_weapon(one_primary, 0);
allow_weapon(one_primary, 2);
allow_weapon(one_primary, 3);
restrict_bank(one_primary, MAX_SHIP_PRIMARY_BANKS, 2);
restrict_bank(one_primary, MAX_SHIP_PRIMARY_BANKS + 2, 3);
Ship_info.push_back(std::move(one_primary));
auto three_primaries = make_ship_class(3, 1);
allow_weapon(three_primaries, 0);
allow_weapon(three_primaries, 2);
allow_weapon(three_primaries, 3);
restrict_bank(three_primaries, MAX_SHIP_PRIMARY_BANKS, 2);
Ship_info.push_back(std::move(three_primaries));
Ship_info.push_back(make_ship_class(2, 1));
Ships_inited = true;
_saved_wss_slots = Wss_slots;
Wss_slots = Wss_slots_teams[0];
for (int slot = 0; slot < 2; slot++) {
Wss_slots[slot].ship_class = slot == 0 ? 0 : 3;
for (int bank = 0; bank < MAX_SHIP_WEAPONS; bank++) {
Wss_slots[slot].wep[bank] = -1;
Wss_slots[slot].wep_count[bank] = -1;
}
}
}
void TearDown() override
{
for (int slot = 0; slot < 2; slot++) {
Wss_slots_teams[0][slot] = wss_unit();
}
Wss_slots = _saved_wss_slots;
Ships_inited = false;
Ship_info.clear();
Weapon_info.clear();
test::scripting::ScriptingTestFixture::TearDown();
}
};
TEST_F(ScriptingShipclassTest, isWeaponAllowedOnShip_twoPrimaries)
{
this->EvalTestScript();
}
TEST_F(ScriptingShipclassTest, isWeaponAllowedOnShip_onePrimary)
{
this->EvalTestScript();
}
TEST_F(ScriptingShipclassTest, isWeaponAllowedOnShip_threePrimaries)
{
this->EvalTestScript();
}
TEST_F(ScriptingShipclassTest, isWeaponAllowedOnShip_unrestrictedClass)
{
this->EvalTestScript();
}
TEST_F(ScriptingShipclassTest, loadoutWeapon_bankRestrictions)
{
this->EvalTestScript();
}
TEST_F(ScriptingShipclassTest, loadoutWeapon_missingPrimaryBank)
{
try {
this->EvalTestScript();
FAIL() << "Expected a Lua error for a primary bank the class does not have";
} catch (const os::dialogs::LuaErrorException& e) {
EXPECT_NE(SCP_string(e.what()).find("The provided bank or weapon index is invalid!"), SCP_string::npos)
<< e.what();
}
EXPECT_EQ(-1, Wss_slots[0].wep[2]);
}
TEST_F(ScriptingShipclassTest, loadoutWeapon_weaponPastEnd)
{
try {
this->EvalTestScript();
FAIL() << "Expected a Lua error for a weapon index one past the end";
} catch (const os::dialogs::LuaErrorException& e) {
EXPECT_NE(SCP_string(e.what()).find("The provided bank or weapon index is invalid!"), SCP_string::npos)
<< e.what();
}
EXPECT_EQ(-1, Wss_slots[0].wep[1]);
EXPECT_EQ(-1, Wss_slots[0].wep_count[1]);
}
+9
View File
@@ -13,6 +13,10 @@ add_file_folder("Actions\\\\Expression"
actions/expression/test_ExpressionParser.cpp
)
add_file_folder("Bmpman"
bmpman/test_eff.cpp
)
add_file_folder("CFile"
cfile/cfile.cpp
)
@@ -83,6 +87,7 @@ add_file_folder("Scripting\\\\API"
scripting/api/bitops.cpp
scripting/api/enums.cpp
scripting/api/hookvars.cpp
scripting/api/shipclass.cpp
)
add_file_folder("Scripting\\\\Lua"
@@ -97,6 +102,10 @@ add_file_folder("Scripting\\\\Lua"
scripting/lua/Value.cpp
)
add_file_folder("Starfield"
starfield/test_sun_disc.cpp
)
add_file_folder("Test Util"
util/FSTestFixture.cpp
util/FSTestFixture.h
+210
View File
@@ -0,0 +1,210 @@
#include <gtest/gtest.h>
#include <starfield/sun_disc.h>
#include <cmath>
#include <vector>
namespace {
// Builds a BGRA sun-like bitmap: a saturated disc of radius disc_radius, wrapped in a
// gaussian halo of the given peak brightness (0 for no halo at all). This is the shape the
// real art has -- a saturated plateau with a falloff -- and the halo is exactly what the
// measurement is supposed to ignore.
std::vector<ubyte> make_sun(int width, int height, float disc_radius, float halo_peak, float halo_sigma,
ubyte alpha = 255)
{
std::vector<ubyte> pixels(static_cast<size_t>(width) * height * 4, 0);
const float cx = (width - 1) * 0.5f;
const float cy = (height - 1) * 0.5f;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
const float dx = x - cx;
const float dy = y - cy;
const float r = std::sqrt(dx * dx + dy * dy);
float value;
if (r <= disc_radius) {
value = 255.0f;
} else {
const float t = (r - disc_radius) / halo_sigma;
value = halo_peak * std::exp(-t * t);
}
auto* px = &pixels[(static_cast<size_t>(y) * width + x) * 4];
px[0] = px[1] = px[2] = static_cast<ubyte>(value);
px[3] = alpha;
}
}
return pixels;
}
float measure(const std::vector<ubyte>& pixels, int width, int height, bool has_alpha = false)
{
SCP_vector<ubyte> weights;
sun_disc_build_weights(pixels.data(), width, height, 4, has_alpha, weights);
return sun_disc_fraction_from_weights(weights.data(), width, height);
}
} // namespace
// A saturated disc of known radius comes back as that radius, in units of the quad's
// half-width, regardless of the halo painted around it.
TEST(SunDiscTest, recovers_disc_radius)
{
constexpr int size = 256;
for (float radius : {16.0f, 32.0f, 64.0f}) {
const auto pixels = make_sun(size, size, radius, 200.0f, 24.0f);
const float expected = radius / (0.5f * size);
EXPECT_NEAR(expected, measure(pixels, size, size), 0.02f) << "disc radius " << radius;
}
}
// The halo must not move the answer -- a second-moment style estimator would be dominated
// by it, which is why the measurement is coverage based.
TEST(SunDiscTest, ignores_halo_size_and_brightness)
{
constexpr int size = 256;
constexpr float radius = 32.0f;
const float bare = measure(make_sun(size, size, radius, 0.0f, 1.0f), size, size);
const float small_halo = measure(make_sun(size, size, radius, 180.0f, 16.0f), size, size);
const float big_halo = measure(make_sun(size, size, radius, 220.0f, 64.0f), size, size);
EXPECT_NEAR(bare, small_halo, 0.01f);
EXPECT_NEAR(bare, big_halo, 0.01f);
}
// Mods ship deliberately blank sun bitmaps (BtA's SunAntares*-BLANK, Blue Planet's
// SunSolDummy) to get a light source without a visible sun. Without the peak floor the
// threshold collapses to zero, every pixel matches, and a sun that isn't drawn at all would
// get the widest penumbra in the game.
TEST(SunDiscTest, blank_bitmap_has_no_disc)
{
constexpr int size = 64;
const std::vector<ubyte> black(static_cast<size_t>(size) * size * 4, 0);
EXPECT_EQ(0.0f, measure(black, size, size));
}
TEST(SunDiscTest, near_black_bitmap_has_no_disc)
{
constexpr int size = 64;
std::vector<ubyte> dim(static_cast<size_t>(size) * size * 4, 0);
for (size_t i = 0; i < dim.size(); i += 4) {
dim[i] = dim[i + 1] = dim[i + 2] = SUN_DISC_MIN_PEAK_WEIGHT - 1;
dim[i + 3] = 255;
}
EXPECT_EQ(0.0f, measure(dim, size, size));
}
// A bitmap lit into its corners measures slightly over 1 before clamping; the disc still
// can't be wider than the quad it's drawn on.
TEST(SunDiscTest, fully_lit_bitmap_clamps_to_one)
{
constexpr int size = 64;
std::vector<ubyte> white(static_cast<size_t>(size) * size * 4, 255);
EXPECT_FLOAT_EQ(1.0f, measure(white, size, size));
}
// g3_render_rect_screen_aligned_2d() fits the quad to the bitmap's shorter edge, so that's
// what the fraction is relative to.
TEST(SunDiscTest, normalizes_against_the_shorter_edge)
{
constexpr int width = 256;
constexpr int height = 128;
constexpr float radius = 32.0f;
const auto pixels = make_sun(width, height, radius, 0.0f, 1.0f);
EXPECT_NEAR(radius / (0.5f * height), measure(pixels, width, height), 0.02f);
}
// Sun bitmaps with an alpha channel are alpha blended rather than drawn additively (see
// material_determine_blend_mode()), so alpha has to scale the weight.
TEST(SunDiscTest, alpha_scales_the_weight)
{
constexpr int size = 128;
constexpr float radius = 24.0f;
// fully transparent art contributes nothing, however bright its colour channels are
const auto transparent = make_sun(size, size, radius, 200.0f, 16.0f, 0);
EXPECT_EQ(0.0f, measure(transparent, size, size, true));
// and ignoring a meaningless alpha channel must not change an opaque bitmap
const auto opaque = make_sun(size, size, radius, 200.0f, 16.0f, 255);
EXPECT_FLOAT_EQ(measure(opaque, size, size, false), measure(opaque, size, size, true));
}
// The disc edge is where the plateau ends, so how steeply the halo falls off around it
// doesn't move the answer -- which is what lets the threshold act as a constant factor
// rather than a per-bitmap judgement call.
TEST(SunDiscTest, insensitive_to_falloff_steepness)
{
constexpr int size = 256;
constexpr float radius = 40.0f;
const float steep = measure(make_sun(size, size, radius, 200.0f, 4.0f), size, size);
const float shallow = measure(make_sun(size, size, radius, 200.0f, 48.0f), size, size);
EXPECT_NEAR(steep, shallow, 0.03f);
}
// The boundary that makes the above hold: a halo is only excluded because it is dimmer than
// SUN_DISC_THRESHOLD of the peak. Art whose surround stays within that of full brightness has
// no plateau edge to find, and counting it as part of the emitting disc is the right answer --
// it is just as bright as the disc. Real sun art doesn't do this, but pin the behaviour so a
// future threshold change is a deliberate one.
TEST(SunDiscTest, halo_brighter_than_the_threshold_counts_as_disc)
{
constexpr int size = 256;
constexpr float radius = 40.0f;
// 250/255 is above the 0.9 threshold, so the "halo" reads as more disc
const float bright_halo = measure(make_sun(size, size, radius, 250.0f, 48.0f), size, size);
const float dim_halo = measure(make_sun(size, size, radius, 200.0f, 48.0f), size, size);
EXPECT_GT(bright_halo, dim_halo);
EXPECT_NEAR(radius / (0.5f * size), dim_halo, 0.02f);
}
TEST(SunDiscTest, build_weights_takes_the_brightest_channel)
{
// one pixel each: blue-dominant, green-dominant, red-dominant (BGRA byte order)
const std::vector<ubyte> pixels = {
200, 10, 20, 255,
10, 180, 20, 255,
10, 20, 160, 255,
};
SCP_vector<ubyte> weights;
sun_disc_build_weights(pixels.data(), 3, 1, 4, false, weights);
ASSERT_EQ(3u, weights.size());
EXPECT_EQ(200, weights[0]);
EXPECT_EQ(180, weights[1]);
EXPECT_EQ(160, weights[2]);
}
TEST(SunDiscTest, build_weights_handles_bgr)
{
const std::vector<ubyte> pixels = {
200, 10, 20,
10, 180, 20,
};
SCP_vector<ubyte> weights;
sun_disc_build_weights(pixels.data(), 2, 1, 3, false, weights);
ASSERT_EQ(2u, weights.size());
EXPECT_EQ(200, weights[0]);
EXPECT_EQ(180, weights[1]);
}
@@ -0,0 +1,84 @@
$Type: png
$Frames: 2
$FPS: 15
; padding comment line 01 to push this file past the old 1024-byte buffer
; padding comment line 02 to push this file past the old 1024-byte buffer
; padding comment line 03 to push this file past the old 1024-byte buffer
; padding comment line 04 to push this file past the old 1024-byte buffer
; padding comment line 05 to push this file past the old 1024-byte buffer
; padding comment line 06 to push this file past the old 1024-byte buffer
; padding comment line 07 to push this file past the old 1024-byte buffer
; padding comment line 08 to push this file past the old 1024-byte buffer
; padding comment line 09 to push this file past the old 1024-byte buffer
; padding comment line 10 to push this file past the old 1024-byte buffer
; padding comment line 11 to push this file past the old 1024-byte buffer
; padding comment line 12 to push this file past the old 1024-byte buffer
; padding comment line 13 to push this file past the old 1024-byte buffer
; padding comment line 14 to push this file past the old 1024-byte buffer
; padding comment line 15 to push this file past the old 1024-byte buffer
; padding comment line 16 to push this file past the old 1024-byte buffer
; padding comment line 17 to push this file past the old 1024-byte buffer
; padding comment line 18 to push this file past the old 1024-byte buffer
; padding comment line 19 to push this file past the old 1024-byte buffer
; padding comment line 20 to push this file past the old 1024-byte buffer
; padding comment line 21 to push this file past the old 1024-byte buffer
; padding comment line 22 to push this file past the old 1024-byte buffer
; padding comment line 23 to push this file past the old 1024-byte buffer
; padding comment line 24 to push this file past the old 1024-byte buffer
; padding comment line 25 to push this file past the old 1024-byte buffer
; padding comment line 26 to push this file past the old 1024-byte buffer
; padding comment line 27 to push this file past the old 1024-byte buffer
; padding comment line 28 to push this file past the old 1024-byte buffer
; padding comment line 29 to push this file past the old 1024-byte buffer
; padding comment line 30 to push this file past the old 1024-byte buffer
; padding comment line 31 to push this file past the old 1024-byte buffer
; padding comment line 32 to push this file past the old 1024-byte buffer
; padding comment line 33 to push this file past the old 1024-byte buffer
; padding comment line 34 to push this file past the old 1024-byte buffer
; padding comment line 35 to push this file past the old 1024-byte buffer
; padding comment line 36 to push this file past the old 1024-byte buffer
; padding comment line 37 to push this file past the old 1024-byte buffer
; padding comment line 38 to push this file past the old 1024-byte buffer
; padding comment line 39 to push this file past the old 1024-byte buffer
; padding comment line 40 to push this file past the old 1024-byte buffer
; padding comment line 41 to push this file past the old 1024-byte buffer
; padding comment line 42 to push this file past the old 1024-byte buffer
; padding comment line 43 to push this file past the old 1024-byte buffer
; padding comment line 44 to push this file past the old 1024-byte buffer
; padding comment line 45 to push this file past the old 1024-byte buffer
; padding comment line 46 to push this file past the old 1024-byte buffer
; padding comment line 47 to push this file past the old 1024-byte buffer
; padding comment line 48 to push this file past the old 1024-byte buffer
; padding comment line 49 to push this file past the old 1024-byte buffer
; padding comment line 50 to push this file past the old 1024-byte buffer
; padding comment line 51 to push this file past the old 1024-byte buffer
; padding comment line 52 to push this file past the old 1024-byte buffer
; padding comment line 53 to push this file past the old 1024-byte buffer
; padding comment line 54 to push this file past the old 1024-byte buffer
; padding comment line 55 to push this file past the old 1024-byte buffer
; padding comment line 56 to push this file past the old 1024-byte buffer
; padding comment line 57 to push this file past the old 1024-byte buffer
; padding comment line 58 to push this file past the old 1024-byte buffer
; padding comment line 59 to push this file past the old 1024-byte buffer
; padding comment line 60 to push this file past the old 1024-byte buffer
; padding comment line 61 to push this file past the old 1024-byte buffer
; padding comment line 62 to push this file past the old 1024-byte buffer
; padding comment line 63 to push this file past the old 1024-byte buffer
; padding comment line 64 to push this file past the old 1024-byte buffer
; padding comment line 65 to push this file past the old 1024-byte buffer
; padding comment line 66 to push this file past the old 1024-byte buffer
; padding comment line 67 to push this file past the old 1024-byte buffer
; padding comment line 68 to push this file past the old 1024-byte buffer
; padding comment line 69 to push this file past the old 1024-byte buffer
; padding comment line 70 to push this file past the old 1024-byte buffer
; padding comment line 71 to push this file past the old 1024-byte buffer
; padding comment line 72 to push this file past the old 1024-byte buffer
; padding comment line 73 to push this file past the old 1024-byte buffer
; padding comment line 74 to push this file past the old 1024-byte buffer
; padding comment line 75 to push this file past the old 1024-byte buffer
; padding comment line 76 to push this file past the old 1024-byte buffer
; padding comment line 77 to push this file past the old 1024-byte buffer
; padding comment line 78 to push this file past the old 1024-byte buffer
; padding comment line 79 to push this file past the old 1024-byte buffer
; padding comment line 80 to push this file past the old 1024-byte buffer
$Keyframe: 1
@@ -0,0 +1,9 @@
local assert = require("assert")
local ship = tb.ShipClasses[2]
assert.equals(true, ship:isWeaponAllowedOnShip(3, 2))
assert.equals(false, ship:isWeaponAllowedOnShip(4, 2))
assert.equals(true, ship:isWeaponAllowedOnShip(4, 4))
assert.equals(true, ship:isWeaponAllowedOnShip(3, 3))
assert.equals(false, ship:isWeaponAllowedOnShip(3, 4))
@@ -0,0 +1,7 @@
local assert = require("assert")
local ship = tb.ShipClasses[3]
assert.equals(true, ship:isWeaponAllowedOnShip(1, 1))
assert.equals(true, ship:isWeaponAllowedOnShip(3, 4))
assert.equals(false, ship:isWeaponAllowedOnShip(4, 4))
@@ -0,0 +1,17 @@
local assert = require("assert")
local ship = tb.ShipClasses[1]
assert.equals(true, ship:isWeaponAllowedOnShip(2))
assert.equals(true, ship:isWeaponAllowedOnShip(3))
assert.equals(true, ship:isWeaponAllowedOnShip(1, 1))
assert.equals(false, ship:isWeaponAllowedOnShip(2, 1))
assert.equals(true, ship:isWeaponAllowedOnShip(2, 2))
assert.equals(true, ship:isWeaponAllowedOnShip(3, 3))
assert.equals(false, ship:isWeaponAllowedOnShip(4, 3))
assert.equals(false, ship:isWeaponAllowedOnShip(3, 4))
assert.equals(true, ship:isWeaponAllowedOnShip(4, 4))
assert.equals(false, ship:isWeaponAllowedOnShip(3, 5))
@@ -0,0 +1,7 @@
local assert = require("assert")
local ship = tb.ShipClasses[4]
assert.equals(false, ship:isWeaponAllowedOnShip(1))
assert.equals(false, ship:isWeaponAllowedOnShip(1, 1))
assert.equals(false, ship:isWeaponAllowedOnShip(3, 3))
@@ -0,0 +1,21 @@
local assert = require("assert")
local slot = ui.ShipWepSelect.Loadout_Ships[1]
slot.Weapons[4] = 3
assert.equals(3, slot.Weapons[4])
assert.equals(0, slot.Amounts[4])
slot.Weapons[5] = 3
assert.equals(0, slot.Weapons[5])
assert.equals(-1, slot.Amounts[5])
slot.Weapons[1] = 2
assert.equals(0, slot.Weapons[1])
slot.Weapons[1] = 1
assert.equals(1, slot.Weapons[1])
local unrestricted = ui.ShipWepSelect.Loadout_Ships[2]
unrestricted.Weapons[1] = 1
assert.equals(0, unrestricted.Weapons[1])
assert.equals(-1, unrestricted.Amounts[1])
@@ -0,0 +1 @@
ui.ShipWepSelect.Loadout_Ships[1].Weapons[3] = 3
@@ -0,0 +1 @@
ui.ShipWepSelect.Loadout_Ships[1].Weapons[2] = 5
-2
View File
@@ -1,2 +0,0 @@
set(FSO_VERSION_REVISION 20261112)
set(FSO_VERSION_REVISION_STR 20261112_185b2fbae)