mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-12 14:59:48 -10:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
059a8715d8 | ||
|
|
7f3e5fae60 | ||
|
|
bb2b2f2869 | ||
|
|
72053c7ddb | ||
|
|
8d4bde4a74 | ||
|
|
5cb93b1bf8 | ||
|
|
a39bb9b773 | ||
|
|
96482c9ef6 |
No files matched your search
+25
-6
@@ -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));
|
||||
|
||||
@@ -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()
|
||||
*
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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];
|
||||
};
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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 ==========
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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( ¢er_point );
|
||||
|
||||
for (i=0;i<nv;i++) {
|
||||
vm_vec_add2( ¢er_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( ¢er_point, Interp_verts[verts[i*2]] );
|
||||
if ( dist > rad ) {
|
||||
rad = dist;
|
||||
}
|
||||
}
|
||||
fl(p+32) = rad;
|
||||
}
|
||||
}
|
||||
|
||||
void swap_bsp_sortnorm2(polymodel* pm, ubyte* p)
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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])) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
+9
@@ -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))
|
||||
+7
@@ -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))
|
||||
+17
@@ -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))
|
||||
+7
@@ -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))
|
||||
+21
@@ -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])
|
||||
+1
@@ -0,0 +1 @@
|
||||
ui.ShipWepSelect.Loadout_Ships[1].Weapons[3] = 3
|
||||
@@ -0,0 +1 @@
|
||||
ui.ShipWepSelect.Loadout_Ships[1].Weapons[2] = 5
|
||||
@@ -1,2 +0,0 @@
|
||||
set(FSO_VERSION_REVISION 20261112)
|
||||
set(FSO_VERSION_REVISION_STR 20261112_185b2fbae)
|
||||
Reference in new issue
Block a user