mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-08 17:48:46 -10:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5b761ca5d | ||
|
|
9443c4abe0 | ||
|
|
5e745f7535 |
No files matched your search
@@ -883,6 +883,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, "");
|
||||
|
||||
@@ -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()
|
||||
*
|
||||
|
||||
@@ -26,13 +26,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 +42,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;
|
||||
@@ -71,6 +73,10 @@ uniform shadowCascadeParams {
|
||||
int cascade_count;
|
||||
float rtShadowBiasMin;
|
||||
float rtShadowBiasMax;
|
||||
int rtShadowSampleCount;
|
||||
int rtaoSampleCount;
|
||||
float rtaoRadius;
|
||||
float rtaoStrength;
|
||||
|
||||
mat4 shadow_mv_matrix;
|
||||
|
||||
@@ -80,6 +86,7 @@ uniform shadowCascadeParams {
|
||||
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];
|
||||
};
|
||||
|
||||
#ifdef VULKAN
|
||||
@@ -303,6 +310,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 + shadow_ray_view_origin;
|
||||
vec3 worldNormal = normalize((inv_view_matrix * vec4(normal, 0.0)).xyz);
|
||||
float rtaoBias = computeRtShadowBias(length(position), rtShadowBiasMin, rtShadowBiasMax);
|
||||
ao *= traceAmbientOcclusion(shadow_tlas, worldPos, worldNormal,
|
||||
rtaoRadius, rtaoStrength, rtaoSampleCount, rtaoBias, gl_FragCoord.xy);
|
||||
}
|
||||
#endif
|
||||
fragmentColor.rgb = diffuseLightColor * diffColor * ao;
|
||||
|
||||
#ifdef ENV_MAP
|
||||
@@ -330,7 +349,14 @@ void main()
|
||||
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));
|
||||
// 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(shadow_ray_cull_mask), 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 +364,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
|
||||
}
|
||||
|
||||
|
||||
@@ -110,6 +110,10 @@ uniform shadowCascadeParams {
|
||||
int cascade_count;
|
||||
float rtShadowBiasMin;
|
||||
float rtShadowBiasMax;
|
||||
int rtShadowSampleCount;
|
||||
int rtaoSampleCount;
|
||||
float rtaoRadius;
|
||||
float rtaoStrength;
|
||||
|
||||
mat4 shadow_mv_matrix;
|
||||
|
||||
@@ -119,6 +123,7 @@ uniform shadowCascadeParams {
|
||||
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];
|
||||
};
|
||||
|
||||
|
||||
@@ -138,6 +143,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 +189,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 +274,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 +450,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);
|
||||
|
||||
@@ -125,6 +125,10 @@ uniform shadowCascadeParams {
|
||||
int cascade_count;
|
||||
float rtShadowBiasMin;
|
||||
float rtShadowBiasMax;
|
||||
int rtShadowSampleCount;
|
||||
int rtaoSampleCount;
|
||||
float rtaoRadius;
|
||||
float rtaoStrength;
|
||||
|
||||
mat4 shadow_mv_matrix;
|
||||
|
||||
@@ -134,6 +138,7 @@ uniform shadowCascadeParams {
|
||||
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];
|
||||
};
|
||||
|
||||
#ifdef VULKAN
|
||||
|
||||
@@ -14,11 +14,18 @@ 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;
|
||||
// OpenGL-only: the Vulkan backend has no geometry-shader stage, and disables the
|
||||
// shadow pass outright when it can't write gl_Layer from the vertex shader, so it
|
||||
// never requests GEOMETRY_FALLBACK. Hence no Vulkan set/binding qualifier here.
|
||||
layout(std140) uniform shadowCascadeParams {
|
||||
int cascade_offset;
|
||||
int cascade_count;
|
||||
float rtShadowBiasMin;
|
||||
float rtShadowBiasMax;
|
||||
int rtShadowSampleCount;
|
||||
int rtaoSampleCount;
|
||||
float rtaoRadius;
|
||||
float rtaoStrength;
|
||||
|
||||
mat4 shadow_mv_matrix;
|
||||
|
||||
@@ -28,6 +35,7 @@ layout (std140) uniform shadowCascadeParams {
|
||||
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];
|
||||
};
|
||||
|
||||
in VertexOutput {
|
||||
|
||||
@@ -45,10 +45,14 @@ layout(set = 0, binding = 6, std140)
|
||||
layout(std140)
|
||||
#endif
|
||||
uniform shadowCascadeParams {
|
||||
int cascade_offset;
|
||||
int cascade_count;
|
||||
float rtShadowBiasMin;
|
||||
float rtShadowBiasMax;
|
||||
int cascade_offset;
|
||||
int cascade_count;
|
||||
float rtShadowBiasMin;
|
||||
float rtShadowBiasMax;
|
||||
int rtShadowSampleCount;
|
||||
int rtaoSampleCount;
|
||||
float rtaoRadius;
|
||||
float rtaoStrength;
|
||||
|
||||
mat4 shadow_mv_matrix;
|
||||
|
||||
@@ -58,6 +62,7 @@ uniform shadowCascadeParams {
|
||||
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];
|
||||
};
|
||||
|
||||
#ifdef VULKAN
|
||||
|
||||
@@ -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,7 +167,7 @@ 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/
|
||||
@@ -132,6 +220,141 @@ float traceShadowRay(accelerationStructureEXT tlas, vec3 worldPos, vec3 worldNor
|
||||
|
||||
return (rayQueryGetIntersectionTypeEXT(rq, true) == gl_RayQueryCommittedIntersectionNoneEXT) ? 1.0 : 0.0;
|
||||
}
|
||||
|
||||
// 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). rtShadowSampleCount/
|
||||
// rtaoSampleCount (shadowCascadeParams) 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);
|
||||
|
||||
vec3 tangent, bitangent;
|
||||
rtBuildOnb(direction, 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 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);
|
||||
|
||||
rayQueryEXT rq;
|
||||
rayQueryInitializeEXT(rq, tlas, gl_RayFlagsOpaqueEXT | gl_RayFlagsTerminateOnFirstHitEXT,
|
||||
cullMask, origin, 0.001, sampleDir, tMax);
|
||||
while (rayQueryProceedEXT(rq)) {}
|
||||
|
||||
if (rayQueryGetIntersectionTypeEXT(rq, true) == gl_RayQueryCommittedIntersectionNoneEXT) {
|
||||
visibility += 1.0;
|
||||
}
|
||||
}
|
||||
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, 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,
|
||||
0xFF, 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
|
||||
|
||||
vec4 transformToShadowMap(mat4 shadow_proj_matrix, int i, vec4 pos)
|
||||
|
||||
@@ -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
-1
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -174,6 +174,11 @@ void gr_opengl_deferred_lighting_end()
|
||||
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
||||
|
||||
GL_state.ColorMask(true, true, true, false);
|
||||
|
||||
// Do not leave the raw-depth sampler bound after the pass.
|
||||
if (Shadow_map_raw_sampler) {
|
||||
GL_state.Texture.BindSampler(11, 0);
|
||||
}
|
||||
}
|
||||
|
||||
static GLuint deferred_light_cylinder_vbo = 0;
|
||||
@@ -253,6 +258,10 @@ 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(11, GL_TEXTURE_2D_ARRAY, Shadow_map_depth_texture);
|
||||
GL_state.Texture.BindSampler(11, Shadow_map_raw_sampler);
|
||||
}
|
||||
}
|
||||
|
||||
if (ENVMAP > 0) {
|
||||
|
||||
@@ -433,6 +433,9 @@ 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);
|
||||
// Unit 11 matches the model pass. Unit 7 is the model misc map, so do not use it
|
||||
// -- see the raw-sampler bind in gropengldeferred.cpp.
|
||||
Current_shader->program->Uniforms.setTextureUniform("shadow_map_raw", 11);
|
||||
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);
|
||||
|
||||
@@ -113,6 +114,18 @@ void opengl_texture_state::Enable(GLuint unit, GLenum tex_target, GLuint tex_id)
|
||||
Enable(tex_id);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (tex_id == 0) {
|
||||
|
||||
@@ -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
|
||||
@@ -65,7 +71,20 @@ class opengl_texture_state
|
||||
*/
|
||||
void Enable(GLuint unit, GLenum tex_target, GLuint tex_id);
|
||||
void Delete(GLuint tex_id);
|
||||
|
||||
|
||||
/**
|
||||
* @brief Binds a sampler object to the specified unit, or unbinds one if sampler is 0
|
||||
*
|
||||
* 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,16 @@ 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(7, 0);
|
||||
GL_state.Texture.BindSampler(11, 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 +972,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);
|
||||
// Unit 11 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", 11);
|
||||
}
|
||||
Current_shader->program->Uniforms.setTextureUniform("sFramebuffer", 9);
|
||||
if (setAllUniforms || (flags & MODEL_SDR_FLAG_TRANSFORM))
|
||||
Current_shader->program->Uniforms.setTextureUniform("transform_tex", 10);
|
||||
@@ -1044,6 +1062,10 @@ 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(11, GL_TEXTURE_2D_ARRAY, Shadow_map_depth_texture);
|
||||
GL_state.Texture.BindSampler(11, Shadow_map_raw_sampler);
|
||||
}
|
||||
}
|
||||
|
||||
if (material_info->get_animated_effect() >= 0) {
|
||||
|
||||
@@ -35,6 +35,11 @@ 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;
|
||||
|
||||
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 "graphics/shadows.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()
|
||||
{
|
||||
// The shadow cascade parameters that RTAO reads are only created when shadows are on.
|
||||
return Rtao_samples > 0 && rtao_supported() && Shadow_quality != ShadowQuality::Disabled;
|
||||
}
|
||||
|
||||
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,28 @@
|
||||
#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, the hardware supports it, AND shadows are on. 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;
|
||||
}
|
||||
|
||||
+211
-15
@@ -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,165 @@ 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);
|
||||
}
|
||||
|
||||
// Softness of the shadow-mapped sun relative to that reference, i.e. the factor the tabled
|
||||
// smoothness values get scaled by.
|
||||
//
|
||||
// This is what makes shadow softness method-agnostic: the shadow map and the raytracer are
|
||||
// driven by the same number -- the tangent of the sun's angular radius, from $SunAngularSize:
|
||||
// or measured off the sun bitmap (see sun_angular_radius_tangent() in starfield.cpp). Doubling
|
||||
// a sun's apparent size doubles the raytraced penumbra cone and this filter width alike, so a
|
||||
// mission author tunes one parameter and both methods follow.
|
||||
//
|
||||
// Where shadow_contact_hardening_supported() is true, the value returned here no longer *is*
|
||||
// the penumbra radius -- it's the ceiling pcssPenumbraRadius() (shadows.sdr) clamps a real,
|
||||
// per-pixel, blocker-search-driven radius to (see Shadow_penumbra_scale). Where it's false
|
||||
// (GL below 3.3), it's still used directly as the fixed radius, exactly
|
||||
// as before contact hardening existed -- this function's output means "the widest the penumbra
|
||||
// is allowed to get" either way, which is why the tabled values keep their old meaning for mods
|
||||
// that already tuned them.
|
||||
//
|
||||
// Uses Static_light.front(), the same light shadows_start_render() builds the cascades from.
|
||||
// Every directional light that reaches this point carries a source radius: suns get theirs in
|
||||
// stars_draw_sun(), and common_setup_room_lights() -- the only other producer -- gives its
|
||||
// stand-in lights a sun's. A zero radius therefore means a genuinely sizeless source (a mod's
|
||||
// deliberately blank sun bitmap, say), which asks for a hard edge; the shadow map answers with
|
||||
// the narrowest filter it can, 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;
|
||||
}
|
||||
@@ -670,6 +829,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 +844,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);
|
||||
@@ -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,10 @@ 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.rtaoSampleCount = Rtao_samples;
|
||||
static_data.rtaoRadius = lighting_profiles::current_rtao_radius();
|
||||
static_data.rtaoStrength = lighting_profiles::current_rtao_strength();
|
||||
static_data.shadow_mv_matrix = Shadow_view_matrix_light;
|
||||
|
||||
static_data.shadow_ray_cull_mask = ray.cull_mask;
|
||||
@@ -1089,19 +1284,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.
|
||||
|
||||
@@ -67,6 +67,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.
|
||||
@@ -105,6 +131,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,
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include "globalincs/pstypes.h"
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
using SPIRV_FLOAT_MAT_4x4 = matrix4;
|
||||
using SPIRV_FLOAT_VEC4 = vec4;
|
||||
|
||||
@@ -146,6 +148,14 @@ struct shadow_cascade_static_data {
|
||||
int cascade_count;
|
||||
float rtShadowBiasMin;
|
||||
float rtShadowBiasMax;
|
||||
int rtShadowSampleCount;
|
||||
int rtaoSampleCount;
|
||||
float rtaoRadius;
|
||||
float rtaoStrength;
|
||||
// The scalars above come to exactly 32 bytes, so std140 puts the following mat4
|
||||
// straight after them with no padding. Adding or removing a scalar here changes
|
||||
// that -- keep the C++ layout in lockstep with the shadowCascadeParams block
|
||||
// declarations in deferred-f/main-f/main-v/shadow_map-g/shadow_map-v.sdr.
|
||||
matrix4 shadow_mv_matrix;
|
||||
|
||||
// Position of the view-space origin (the camera) in TLAS-relative space. The RT shadow
|
||||
@@ -173,13 +183,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 every .sdr file that declares it "
|
||||
"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 [8 leading scalars] + 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
|
||||
*/
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "graphics/matrix.h"
|
||||
#include "graphics/material.h"
|
||||
#include "graphics/grinternal.h"
|
||||
#include "graphics/rtao.h"
|
||||
#include "graphics/shadows.h"
|
||||
#include "lighting/lighting.h"
|
||||
#include "mission/missionparse.h"
|
||||
@@ -538,10 +539,23 @@ void vulkan_deferred_lighting_finish()
|
||||
auto* stateTracker = getStateTracker();
|
||||
vk::CommandBuffer cmd = stateTracker->getCommandBuffer();
|
||||
|
||||
// 1. End G-buffer render pass
|
||||
// TLAS for RTAO: when shadow rendering is off (Shadow_quality Disabled, or a
|
||||
// frame that skips shadows_render_all entirely), nothing has built this
|
||||
// frame's TLAS yet -- request it here. No-op when the shadow path already
|
||||
// built it (buildTlas()'s per-frame guard). Acceleration-structure builds
|
||||
// must be recorded outside a render pass, so this ends the G-buffer pass and
|
||||
// clears it from the state tracker -- which is why step 1 below only ends
|
||||
// the pass when it is still active.
|
||||
if (rtao_enabled()) {
|
||||
vulkan_build_shadow_tlas();
|
||||
}
|
||||
|
||||
// 1. End G-buffer render pass (unless the RTAO TLAS build above already did)
|
||||
// All 6 color attachments → eShaderReadOnlyOptimal
|
||||
// Depth → eDepthStencilAttachmentOptimal
|
||||
cmd.endRenderPass();
|
||||
if (stateTracker->getCurrentRenderPass()) {
|
||||
cmd.endRenderPass();
|
||||
}
|
||||
|
||||
// 2. Copy emissive → composite (the emissive data becomes the base for light accumulation)
|
||||
// Emissive → eShaderReadOnlyOptimal (done), composite → eColorAttachmentOptimal (for light accum)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1433,6 +1433,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;
|
||||
|
||||
@@ -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"
|
||||
@@ -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;
|
||||
|
||||
@@ -275,6 +275,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 actually ran, for the
|
||||
// per-frame idempotence guard (multiple callers may request a TLAS per frame).
|
||||
uint64_t m_lastTlasBuildFrame = UINT64_MAX;
|
||||
|
||||
vk::Device m_device;
|
||||
vk::PhysicalDevice m_physicalDevice;
|
||||
VulkanMemoryManager* m_memoryManager = nullptr;
|
||||
|
||||
@@ -425,6 +425,16 @@ void VulkanRaytracingManager::buildTlas()
|
||||
return;
|
||||
}
|
||||
|
||||
// Idempotence guard: both the shadow path (shadows_render_all ->
|
||||
// gr_build_shadow_tlas) and the RTAO fallback trigger
|
||||
// (vulkan_deferred_lighting_finish, for when shadow rendering is disabled)
|
||||
// may request a TLAS in the same frame -- only the first request builds.
|
||||
const uint64_t frameNumber = m_bufferManager->getCurrentFrameNumber();
|
||||
if (m_lastTlasBuildFrame == frameNumber) {
|
||||
return;
|
||||
}
|
||||
m_lastTlasBuildFrame = frameNumber;
|
||||
|
||||
// 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).
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -601,6 +579,35 @@ void init_function_pointers()
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
// Outside the anonymous namespace: declared in gr_vulkan.h so the RTAO fallback
|
||||
// trigger in vulkan_deferred_lighting_finish() (VulkanDeferred.cpp) can call it
|
||||
// when shadow rendering didn't build this frame's TLAS.
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
void initialize_function_pointers() {
|
||||
init_function_pointers();
|
||||
}
|
||||
|
||||
@@ -11,6 +11,13 @@ bool initialize(std::unique_ptr<os::GraphicsOperations>&& graphicsOps);
|
||||
|
||||
VulkanRenderer* getRendererInstance();
|
||||
|
||||
// Build (or refresh) this frame's raytraced-shadow/RTAO TLAS. Safe to call more
|
||||
// than once per frame (VulkanRaytracingManager::buildTlas() is frame-guarded),
|
||||
// but acceleration-structure builds must be recorded outside a render pass, so
|
||||
// this ends the state tracker's current render pass (and clears it in the
|
||||
// tracker) if one is active.
|
||||
void vulkan_build_shadow_tlas();
|
||||
|
||||
void cleanup();
|
||||
|
||||
} // namespace graphics::vulkan
|
||||
@@ -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("RT 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("RT 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 (deg)", &sun_size, 0.0f, 10.0f)) {
|
||||
LabRenderer::setSunAngularSizeOverride(sun_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LabUi::build_rtao_sliders()
|
||||
{
|
||||
// Unlike the RT shadow sliders, RTAO is independent of the shadow method --
|
||||
// it only needs ray-query support.
|
||||
if (!rtao_supported()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int samples = Rtao_samples;
|
||||
if (SliderInt("RTAO Samples", &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("RTAO Radius", &radius, 0.0f, 200.0f)) {
|
||||
lighting_profiles::lab_set_rtao_radius(radius);
|
||||
}
|
||||
|
||||
float strength = lighting_profiles::current_rtao_strength();
|
||||
if (SliderFloat("RTAO 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,47 @@ 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.
|
||||
struct LabSavedSettings {
|
||||
RTShadowQuality rt_shadow_quality;
|
||||
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 = {Rt_shadow_quality,
|
||||
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) {
|
||||
Rt_shadow_quality = Lab_saved_settings.rt_shadow_quality;
|
||||
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();
|
||||
|
||||
@@ -6278,6 +6278,17 @@ 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 27.0 simply don't have it
|
||||
if (optional_string("+AngularSize:")) {
|
||||
stuff_float(&sle.angular_size);
|
||||
if (sle.angular_size < 0.0f) {
|
||||
error_display(0, "+AngularSize: for sun '%s' must be >= 0 (got %f); ignoring it.", sle.filename, sle.angular_size);
|
||||
sle.angular_size = SUN_ANGULAR_SIZE_UNSPECIFIED;
|
||||
}
|
||||
}
|
||||
|
||||
// add it
|
||||
background->suns.push_back(sle);
|
||||
}
|
||||
@@ -9791,3 +9802,19 @@ bool check_for_25_1_data()
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool check_for_27_0_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_27_0_data();
|
||||
|
||||
#define WING_PLAYER_BASE 0x80000 // used by Fred to tell ship_index in a wing points to a player
|
||||
|
||||
|
||||
@@ -1035,6 +1035,15 @@ 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_27_0_data()
|
||||
FRED_ENSURE_PROPERTY_VERSION_WITH_DEFAULT_F("+AngularSize:",
|
||||
1,
|
||||
";;FSO 27.0.0;;",
|
||||
SUN_ANGULAR_SIZE_UNSPECIFIED,
|
||||
" %f",
|
||||
sle->angular_size);
|
||||
}
|
||||
|
||||
// save background bitmaps by filename
|
||||
@@ -3171,7 +3180,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_27_0 = gameversion::version(27, 0);
|
||||
if (MISSION_VERSION >= version_27_0) {
|
||||
Warning(LOCATION,
|
||||
"Notify an SCP coder: now that the required mission version is at least 27.0, the check_for_27_0_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_27_0_data()) {
|
||||
The_mission.required_fso_version = version_27_0;
|
||||
} 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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -481,6 +481,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 +1762,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
|
||||
|
||||
+147
-25
@@ -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,11 @@ 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 -- cached sun_disc_measure_bitmap() result for
|
||||
// disc_measured_for; negative means "not measured yet"
|
||||
int disc_measured_for; // only for suns -- bitmap_id disc_fraction was measured from
|
||||
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 +114,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 +134,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 +412,9 @@ 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;
|
||||
sbm->disc_measured_for = -1;
|
||||
|
||||
for (i = 0; i < MAX_FLARE_BMP; i++) {
|
||||
sbm->flare_bitmaps[i].bitmap_id = -1;
|
||||
@@ -497,6 +512,20 @@ 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). Only affects
|
||||
// raytraced shadows, which get a penumbra sized to match. 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);
|
||||
if (sbm.angular_size < 0.0f) {
|
||||
error_display(0, "$SunAngularSize: for sun '%s' must be >= 0 (got %f); measuring the bitmap instead.", sbm.filename, sbm.angular_size);
|
||||
sbm.angular_size = SUN_ANGULAR_SIZE_UNSPECIFIED;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lens flare stuff
|
||||
if (optional_string("$Flare:")) {
|
||||
sbm.flare = 1;
|
||||
@@ -855,6 +884,14 @@ void stars_pre_level_init(bool clear_backgrounds)
|
||||
sb.bitmap_id = -1;
|
||||
}
|
||||
|
||||
// drop any derived angular size along with the bitmap it was measured from. The
|
||||
// handle check in sun_angular_radius_tangent() can't be relied on for this: bm_load()
|
||||
// hands back a recycled slot index, so a reloaded bitmap can come back on the handle
|
||||
// it had before. This is what makes a size change take effect when the background
|
||||
// changes -- which, outside of a mission, is every time the lab switches backgrounds.
|
||||
sb.disc_fraction = -1.0f;
|
||||
sb.disc_measured_for = -1;
|
||||
|
||||
if (sb.glow_bitmap > 0) {
|
||||
bm_release(sb.glow_bitmap);
|
||||
sb.glow_bitmap = -1;
|
||||
@@ -1270,6 +1307,89 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief How much of this sun's bitmap is its emitting disc, measuring it on first use
|
||||
*
|
||||
* @return the disc fraction, or 0 for a sun whose bitmap has no measurable disc
|
||||
*/
|
||||
static float sun_measured_disc_fraction(starfield_bitmap *bm)
|
||||
{
|
||||
// measuring reads the bitmap's pixels, so it happens once and is cached against the
|
||||
// handle it was measured from
|
||||
if ( (bm->disc_fraction >= 0.0f) && (bm->disc_measured_for == bm->bitmap_id) ) {
|
||||
return bm->disc_fraction;
|
||||
}
|
||||
|
||||
const float measured = sun_disc_measure_bitmap(bm->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", bm->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", bm->filename));
|
||||
} else {
|
||||
// logged once per sun bitmap, not per frame -- 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", bm->filename, measured,
|
||||
fl_degrees(2.0f * atanf(sun_disc_tangent_from_fraction(measured, 1.0f)))));
|
||||
}
|
||||
|
||||
bm->disc_fraction = (measured > 0.0f) ? measured : 0.0f;
|
||||
bm->disc_measured_for = bm->bitmap_id;
|
||||
|
||||
return bm->disc_fraction;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
|
||||
// the size is only used to soften shadows, so do not read the bitmap when shadows are off
|
||||
if (Shadow_quality == ShadowQuality::Disabled) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
return sun_disc_tangent_from_fraction(sun_measured_disc_fraction(bm), scale_x);
|
||||
}
|
||||
|
||||
// draw sun
|
||||
void stars_draw_sun(int show_sun)
|
||||
{
|
||||
@@ -1318,9 +1438,13 @@ 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);
|
||||
// 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.
|
||||
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, Suns[idx].angular_size));
|
||||
}
|
||||
|
||||
// if supernova
|
||||
if ( supernova_active() && (idx == 0) )
|
||||
@@ -2449,6 +2573,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 three 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 +2619,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)
|
||||
@@ -2497,6 +2636,7 @@ void stars_set_data(bool is_sun, int idx, starfield_list_entry& sle)
|
||||
item.div_y = sle.div_y;
|
||||
item.scale_x = sle.scale_x;
|
||||
item.scale_y = sle.scale_y;
|
||||
item.angular_size = sle.angular_size;
|
||||
|
||||
// this is necessary when modifying bitmaps, but not when modifying suns
|
||||
if (!is_sun)
|
||||
@@ -2512,13 +2652,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);
|
||||
|
||||
@@ -2608,13 +2742,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 +3004,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,35 @@
|
||||
|
||||
|
||||
// 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. Only affects raytraced shadows.
|
||||
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 only exists to keep the value finite and non-negative.
|
||||
constexpr float SUN_ANGULAR_SIZE_MAX = 90.0f;
|
||||
|
||||
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 +88,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,222 @@
|
||||
#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>
|
||||
|
||||
// The sun quad is drawn with rad = 0.05f * scale_x (see stars_draw_sun()), and
|
||||
// g3_render_rect_screen_aligned_2d() sizes it so that rad is exactly the tangent of the
|
||||
// half-angle it subtends, independent of FOV, resolution and aspect ratio.
|
||||
static const float Sun_quad_tan_half_angle = 0.05f;
|
||||
|
||||
// How much of that drawn size 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_tan_half_angle * 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);
|
||||
return -1.0f;
|
||||
}
|
||||
|
||||
bm_unlock(bitmap_handle);
|
||||
}
|
||||
|
||||
if (weights.empty()) {
|
||||
return -1.0f;
|
||||
}
|
||||
|
||||
return sun_disc_fraction_from_weights(weights.data(), width, height);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
#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.
|
||||
|
||||
// Pixels at or above this fraction of the brightest pixel count as part of the disc.
|
||||
// The measurement is only weakly sensitive to it -- across the shipped sun art of retail,
|
||||
// the MediaVPs and several mods, moving this to 0.5 rescales every bitmap by ~1.37x while
|
||||
// preserving their order almost exactly (Spearman rho 0.96) -- so it behaves as a constant
|
||||
// factor that the calibration in starfield.cpp absorbs, not as a per-bitmap judgement call.
|
||||
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. 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
|
||||
|
||||
@@ -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 this sun's apparent diameter for this mission. Unchecked, the sun uses its stars.tbl size, or a size measured from its bitmap.</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Ang. size</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QDoubleSpinBox" name="sunAngularSizeDoubleSpinBox">
|
||||
<property name="toolTip">
|
||||
<string>Apparent diameter in degrees; the Sun as seen from Earth is about 0.53. Only affects raytraced shadows, whose penumbras are sized to match.</string>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>75</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="suffix">
|
||||
<string> deg</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>
|
||||
|
||||
@@ -97,6 +97,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]);
|
||||
}
|
||||
Reference in new issue
Block a user