mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-07 22:56:30 -10:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76286c5332 | ||
|
|
e4546de24e | ||
|
|
336f198594 | ||
|
|
5c7059b451 | ||
|
|
4c0ff8414d | ||
|
|
019c4bacea | ||
|
|
e7d58d85cb | ||
|
|
84fa0a622f | ||
|
|
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
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ void main() {
|
||||
}
|
||||
#else
|
||||
in vec4 vertPosition;
|
||||
in vec4 vertTexCoord;
|
||||
|
||||
out vec2 v_rcpFrame;
|
||||
#ifdef GL_ES
|
||||
@@ -27,6 +28,8 @@ layout (std140) uniform genericData {
|
||||
void main() {
|
||||
gl_Position = vertPosition;
|
||||
v_rcpFrame = vec2(1.0/rt_w, 1.0/rt_h);
|
||||
v_pos = vertPosition.xy*0.5 + 0.5;
|
||||
// Use the real texcoord rather than deriving it from vertPosition: the draw call may ask for a
|
||||
// sub-rectangle of the source texture, which the clip-space formula ignored. Matches post-v.sdr.
|
||||
v_pos = vertTexCoord.xy;
|
||||
}
|
||||
#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]);
|
||||
@@ -926,6 +927,29 @@ bool gr_is_smaa_mode(AntiAliasMode mode) {
|
||||
return mode == AntiAliasMode::SMAA_Low || mode == AntiAliasMode::SMAA_Medium || mode == AntiAliasMode::SMAA_High || mode == AntiAliasMode::SMAA_Ultra;
|
||||
}
|
||||
|
||||
SCP_vector<float> gr_get_supported_anisotropy_levels()
|
||||
{
|
||||
float max;
|
||||
if (!gr_get_property(gr_property::MAX_ANISOTROPY, &max)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (max <= 2.0f) {
|
||||
return {};
|
||||
}
|
||||
|
||||
SCP_vector<float> out;
|
||||
|
||||
// We assume here that the anisotropy levels are powers of two...
|
||||
float current = 1.0f;
|
||||
while (current <= max) {
|
||||
out.push_back(current);
|
||||
current *= 2.0f;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
static void parse_post_processing_func()
|
||||
{
|
||||
bool value;
|
||||
@@ -1709,6 +1733,13 @@ void gr_screen_resize(int width, int height)
|
||||
gr_screen.save_max_h_unscaled_zoomed = gr_screen.max_h_unscaled_zoomed;
|
||||
|
||||
gr_setup_viewport();
|
||||
|
||||
// The offscreen targets that back the scene/post-processing pipeline were sized for the old
|
||||
// gr_screen; give the backend a chance to grow them before anything renders at the new size.
|
||||
// Backends that already handle this elsewhere (Vulkan, via recreateSwapChain()) leave it unset.
|
||||
if (gr_screen.gf_resize_render_targets) {
|
||||
gr_screen.gf_resize_render_targets();
|
||||
}
|
||||
}
|
||||
|
||||
void gr_window_to_render_pos(float& x, float& y)
|
||||
|
||||
+19
-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 {
|
||||
@@ -911,6 +917,12 @@ typedef struct screen {
|
||||
std::function<void()> gf_scene_texture_end;
|
||||
std::function<void()> gf_copy_effect_texture;
|
||||
|
||||
// Grow the offscreen render targets that back the scene/post-processing pipeline to cover the
|
||||
// current gr_screen, if they don't already. Called from gr_screen_resize(). Optional: the
|
||||
// Vulkan backend leaves this unset because VulkanRenderer::recreateSwapChain() already owns
|
||||
// resizing its extent-sized targets.
|
||||
std::function<void()> gf_resize_render_targets;
|
||||
|
||||
std::function<void(int zbias)> gf_zbias;
|
||||
|
||||
std::function<void(int)> gf_set_fill_mode;
|
||||
@@ -1496,6 +1508,12 @@ inline bool gr_get_property(gr_property property, void* destination)
|
||||
return gr_screen.gf_get_property(property, destination);
|
||||
}
|
||||
|
||||
// Anisotropic filtering levels the current hardware supports: 1.0 (off), then powers of two up to
|
||||
// the reported maximum. Empty if anisotropy is unavailable or the hardware caps out below 4x, in
|
||||
// which case there is nothing meaningful to offer. Backs both the in-game option's enumerator and
|
||||
// qtFRED's Preferences combo, so the two can't drift.
|
||||
SCP_vector<float> gr_get_supported_anisotropy_levels();
|
||||
|
||||
inline void gr_push_debug_group(const char* name)
|
||||
{
|
||||
gr_screen.gf_push_debug_group(name);
|
||||
|
||||
@@ -1126,6 +1126,7 @@ void gr_opengl_init_function_pointers()
|
||||
gr_screen.gf_scene_texture_begin = gr_opengl_scene_texture_begin;
|
||||
gr_screen.gf_scene_texture_end = gr_opengl_scene_texture_end;
|
||||
gr_screen.gf_copy_effect_texture = gr_opengl_copy_effect_texture;
|
||||
gr_screen.gf_resize_render_targets = gr_opengl_resize_render_targets;
|
||||
|
||||
gr_screen.gf_deferred_lighting_begin = gr_opengl_deferred_lighting_begin;
|
||||
gr_screen.gf_deferred_lighting_msaa = gr_opengl_deferred_lighting_msaa;
|
||||
@@ -1516,7 +1517,7 @@ bool gr_opengl_init(std::unique_ptr<os::GraphicsOperations>&& graphicsOps)
|
||||
opengl_shader_init();
|
||||
|
||||
// post processing effects, after shaders are initialized
|
||||
opengl_setup_scene_textures();
|
||||
opengl_setup_scene_textures(gr_screen.max_w, gr_screen.max_h);
|
||||
opengl_post_process_init();
|
||||
|
||||
// must be called after extensions are setup
|
||||
@@ -1631,6 +1632,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
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ void gr_opengl_deferred_lighting_begin(bool clearNonColorBufs)
|
||||
Current_shader->program->Uniforms.setTextureUniform("tex", 0);
|
||||
GL_state.SetAlphaBlendMode(gr_alpha_blend::ALPHA_BLEND_NONE);
|
||||
GL_state.SetZbufferType(ZBUFFER_TYPE_NONE);
|
||||
opengl_draw_full_screen_textured(0, 0, 1, 1);
|
||||
opengl_draw_full_screen_scene_texture();
|
||||
} else {
|
||||
// Copy the existing color data into the emissive part of the G-buffer since everything that already existed is
|
||||
// treated as emissive
|
||||
@@ -159,7 +159,9 @@ void gr_opengl_deferred_lighting_msaa()
|
||||
});
|
||||
GL_state.SetAlphaBlendMode(gr_alpha_blend::ALPHA_BLEND_NONE);
|
||||
GL_state.SetZbufferType(ZBUFFER_TYPE_WRITE);
|
||||
opengl_draw_full_screen_textured(0, 0, 1, 1);
|
||||
// msaa-f.sdr resolves via ivec2(textureSize(texColor) * fragTexCoord), so the texcoords have to
|
||||
// stay inside the rendered sub-rectangle of the multisampled G-buffer.
|
||||
opengl_draw_full_screen_scene_texture();
|
||||
}
|
||||
|
||||
void gr_opengl_deferred_lighting_end()
|
||||
@@ -174,6 +176,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 +260,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) {
|
||||
@@ -319,8 +330,12 @@ void gr_opengl_deferred_lighting_finish()
|
||||
shadow_cascade_params_bind_deferred();
|
||||
}
|
||||
|
||||
header->invScreenWidth = 1.0f / gr_screen.max_w;
|
||||
header->invScreenHeight = 1.0f / gr_screen.max_h;
|
||||
// deferred-f.sdr turns gl_FragCoord into a G-buffer texture coordinate with these, so they
|
||||
// have to normalize against the G-buffer's own dimensions. Those only equal gr_screen while
|
||||
// the viewport exactly fills the scene textures -- not after a shrink, and not when the
|
||||
// allocation was clamped by GL_max_renderbuffer_size.
|
||||
header->invScreenWidth = 1.0f / Scene_texture_width;
|
||||
header->invScreenHeight = 1.0f / Scene_texture_height;
|
||||
header->nearPlane = gr_near_plane;
|
||||
|
||||
{
|
||||
@@ -555,7 +570,8 @@ void gr_opengl_deferred_lighting_finish()
|
||||
data->clip_dist = Neb2_fog_clip_distance;
|
||||
});
|
||||
|
||||
opengl_draw_full_screen_textured(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
// fog-f.sdr samples the composite and depth targets straight off fragTexCoord.
|
||||
opengl_draw_full_screen_scene_texture();
|
||||
|
||||
if (bDrawNebVolumetrics) {
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
@@ -651,6 +667,12 @@ void gr_opengl_deferred_lighting_finish()
|
||||
|
||||
{
|
||||
GR_DEBUG_SCOPE("Volumetric Nebulae Draw");
|
||||
// Deliberately unscaled. volumetric-f.sdr uses fragTexCoord for two incompatible
|
||||
// things: reconstructing an eye-space ray direction, which needs the full 0..1 range
|
||||
// across the viewport, and sampling composite/depth/emissive, which needs the
|
||||
// rendered sub-rectangle. Scaling here would fix the sampling and skew every ray.
|
||||
// Separating the two needs a second varying (or a scale uniform) in the shader; until
|
||||
// then volumetrics are only correct while the targets exactly match the viewport.
|
||||
opengl_draw_full_screen_textured(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
}
|
||||
GL_state.Texture.Enable(Scene_emissive_texture);
|
||||
|
||||
@@ -70,6 +70,33 @@ int Scene_texture_height;
|
||||
GLfloat Scene_texture_u_scale = 1.0f;
|
||||
GLfloat Scene_texture_v_scale = 1.0f;
|
||||
|
||||
// Render targets are torn down and rebuilt mid-session by gr_opengl_resize_render_targets(), not
|
||||
// just at shutdown, so deletion has to go through the state cache: the driver is free to hand a
|
||||
// freed name straight back out, and a cache entry still holding that name would make a later
|
||||
// Enable() of the recycled texture a no-op.
|
||||
void opengl_delete_render_texture(GLuint& tex)
|
||||
{
|
||||
if ( !tex ) {
|
||||
return;
|
||||
}
|
||||
|
||||
GL_state.Texture.Delete(tex);
|
||||
glDeleteTextures(1, &tex);
|
||||
tex = 0;
|
||||
}
|
||||
|
||||
// Callers must have bound something else first (the resize path binds 0); the framebuffer cache
|
||||
// has no equivalent of Texture.Delete() to unbind through.
|
||||
void opengl_delete_render_framebuffer(GLuint& fbo)
|
||||
{
|
||||
if ( !fbo ) {
|
||||
return;
|
||||
}
|
||||
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
fbo = 0;
|
||||
}
|
||||
|
||||
inline GLenum opengl_primitive_type(primitive_type prim_type)
|
||||
{
|
||||
switch ( prim_type ) {
|
||||
@@ -98,7 +125,7 @@ void gr_opengl_sphere(material* material_def, float /*rad*/)
|
||||
}
|
||||
|
||||
extern int opengl_check_framebuffer();
|
||||
void opengl_setup_scene_textures()
|
||||
void opengl_setup_scene_textures(int width, int height)
|
||||
{
|
||||
Scene_texture_initialized = 0;
|
||||
|
||||
@@ -113,10 +140,10 @@ void opengl_setup_scene_textures()
|
||||
return;
|
||||
}
|
||||
|
||||
// clamp size, if needed
|
||||
Scene_texture_width = gr_screen.max_w;
|
||||
Scene_texture_height = gr_screen.max_h;
|
||||
Scene_texture_width = width;
|
||||
Scene_texture_height = height;
|
||||
|
||||
// clamp size, if needed
|
||||
if ( Scene_texture_width > GL_max_renderbuffer_size ) {
|
||||
Scene_texture_width = GL_max_renderbuffer_size;
|
||||
}
|
||||
@@ -125,6 +152,13 @@ void opengl_setup_scene_textures()
|
||||
Scene_texture_height = GL_max_renderbuffer_size;
|
||||
}
|
||||
|
||||
mprintf((" Scene textures: %dx%d (screen %dx%d, max renderbuffer %d)\n",
|
||||
Scene_texture_width,
|
||||
Scene_texture_height,
|
||||
gr_screen.max_w,
|
||||
gr_screen.max_h,
|
||||
GL_max_renderbuffer_size));
|
||||
|
||||
// create framebuffer
|
||||
glGenFramebuffers(1, &Scene_framebuffer);
|
||||
GL_state.BindFrameBuffer(Scene_framebuffer);
|
||||
@@ -333,32 +367,15 @@ void opengl_setup_scene_textures()
|
||||
|
||||
if ( opengl_check_framebuffer() ) {
|
||||
GL_state.BindFrameBuffer(0);
|
||||
glDeleteFramebuffers(1, &Scene_framebuffer);
|
||||
Scene_framebuffer = 0;
|
||||
opengl_delete_render_framebuffer(Scene_framebuffer);
|
||||
|
||||
glDeleteTextures(1, &Scene_color_texture);
|
||||
Scene_color_texture = 0;
|
||||
|
||||
glDeleteTextures(1, &Scene_position_texture);
|
||||
Scene_position_texture = 0;
|
||||
|
||||
glDeleteTextures(1, &Scene_normal_texture);
|
||||
Scene_normal_texture = 0;
|
||||
|
||||
glDeleteTextures(1, &Scene_specular_texture);
|
||||
Scene_specular_texture = 0;
|
||||
|
||||
glDeleteTextures(1, &Scene_emissive_texture);
|
||||
Scene_emissive_texture = 0;
|
||||
|
||||
glDeleteTextures(1, &Scene_depth_texture);
|
||||
Scene_depth_texture = 0;
|
||||
|
||||
glDeleteTextures(1, &Scene_luminance_texture);
|
||||
Scene_luminance_texture = 0;
|
||||
|
||||
//glDeleteTextures(1, &Scene_fxaa_output_texture);
|
||||
//Scene_fxaa_output_texture = 0;
|
||||
opengl_delete_render_texture(Scene_color_texture);
|
||||
opengl_delete_render_texture(Scene_position_texture);
|
||||
opengl_delete_render_texture(Scene_normal_texture);
|
||||
opengl_delete_render_texture(Scene_specular_texture);
|
||||
opengl_delete_render_texture(Scene_emissive_texture);
|
||||
opengl_delete_render_texture(Scene_depth_texture);
|
||||
opengl_delete_render_texture(Scene_luminance_texture);
|
||||
|
||||
Gr_post_processing_enabled = false;
|
||||
Gr_enable_soft_particles = false;
|
||||
@@ -687,79 +704,105 @@ void opengl_scene_texture_shutdown()
|
||||
return;
|
||||
}
|
||||
|
||||
if ( Scene_color_texture ) {
|
||||
glDeleteTextures(1, &Scene_color_texture);
|
||||
Scene_color_texture = 0;
|
||||
}
|
||||
// Everything opengl_setup_scene_textures() generated, in the same order. Note that
|
||||
// GammaBlit_texture is 0 when the gamma pass is aliasing Scene_ldr_texture, so the shared
|
||||
// texture is only released once.
|
||||
opengl_delete_render_texture(Scene_color_texture);
|
||||
opengl_delete_render_texture(Scene_ldr_texture);
|
||||
opengl_delete_render_texture(Scene_position_texture);
|
||||
opengl_delete_render_texture(Scene_normal_texture);
|
||||
opengl_delete_render_texture(Scene_specular_texture);
|
||||
opengl_delete_render_texture(Scene_emissive_texture);
|
||||
opengl_delete_render_texture(Scene_composite_texture);
|
||||
opengl_delete_render_texture(Scene_luminance_texture);
|
||||
opengl_delete_render_texture(Cockpit_depth_texture);
|
||||
opengl_delete_render_texture(Scene_depth_texture);
|
||||
opengl_delete_render_framebuffer(Scene_framebuffer);
|
||||
|
||||
if ( Scene_position_texture ) {
|
||||
glDeleteTextures(1, &Scene_position_texture);
|
||||
Scene_position_texture = 0;
|
||||
}
|
||||
opengl_delete_render_texture(Scene_color_texture_ms);
|
||||
opengl_delete_render_texture(Scene_position_texture_ms);
|
||||
opengl_delete_render_texture(Scene_normal_texture_ms);
|
||||
opengl_delete_render_texture(Scene_specular_texture_ms);
|
||||
opengl_delete_render_texture(Scene_emissive_texture_ms);
|
||||
opengl_delete_render_texture(Scene_depth_texture_ms);
|
||||
opengl_delete_render_framebuffer(Scene_framebuffer_ms);
|
||||
|
||||
if ( Scene_normal_texture ) {
|
||||
glDeleteTextures(1, &Scene_normal_texture);
|
||||
Scene_normal_texture = 0;
|
||||
}
|
||||
opengl_delete_render_texture(Back_texture);
|
||||
opengl_delete_render_texture(Back_depth_texture);
|
||||
opengl_delete_render_framebuffer(Back_framebuffer);
|
||||
|
||||
if ( Scene_specular_texture ) {
|
||||
glDeleteTextures(1, &Scene_specular_texture);
|
||||
Scene_specular_texture = 0;
|
||||
}
|
||||
opengl_delete_render_texture(GammaBlit_texture);
|
||||
opengl_delete_render_framebuffer(GammaBlit_framebuffer);
|
||||
|
||||
if (Scene_emissive_texture) {
|
||||
glDeleteTextures(1, &Scene_emissive_texture);
|
||||
Scene_emissive_texture = 0;
|
||||
}
|
||||
|
||||
if ( Scene_depth_texture ) {
|
||||
glDeleteTextures(1, &Scene_depth_texture);
|
||||
Scene_depth_texture = 0;
|
||||
}
|
||||
|
||||
if ( Scene_framebuffer ) {
|
||||
glDeleteFramebuffers(1, &Scene_framebuffer);
|
||||
Scene_framebuffer = 0;
|
||||
}
|
||||
|
||||
if (Back_texture) {
|
||||
glDeleteTextures(1, &Back_texture);
|
||||
Back_texture = 0;
|
||||
}
|
||||
|
||||
if (Back_depth_texture) {
|
||||
glDeleteTextures(1, &Back_depth_texture);
|
||||
Back_depth_texture = 0;
|
||||
}
|
||||
|
||||
if (Back_framebuffer) {
|
||||
glDeleteFramebuffers(1, &Back_framebuffer);
|
||||
Back_framebuffer = 0;
|
||||
}
|
||||
|
||||
if (GammaBlit_texture) {
|
||||
glDeleteTextures(1, &GammaBlit_texture);
|
||||
GammaBlit_texture = 0;
|
||||
}
|
||||
|
||||
if (GammaBlit_framebuffer) {
|
||||
glDeleteFramebuffers(1, &GammaBlit_framebuffer);
|
||||
GammaBlit_framebuffer = 0;
|
||||
}
|
||||
|
||||
glDeleteTextures(2, Distortion_texture);
|
||||
Distortion_texture[0] = 0;
|
||||
Distortion_texture[1] = 0;
|
||||
|
||||
if ( Distortion_framebuffer ) {
|
||||
glDeleteFramebuffers(1, &Distortion_framebuffer);
|
||||
Distortion_framebuffer = 0;
|
||||
}
|
||||
opengl_delete_render_texture(Distortion_texture[0]);
|
||||
opengl_delete_render_texture(Distortion_texture[1]);
|
||||
opengl_delete_render_framebuffer(Distortion_framebuffer);
|
||||
|
||||
Scene_texture_initialized = 0;
|
||||
Scene_framebuffer_in_frame = false;
|
||||
}
|
||||
|
||||
void gr_opengl_resize_render_targets()
|
||||
{
|
||||
// Nothing allocated yet (still inside gr_init()), or FBOs are unavailable entirely.
|
||||
if ( !Scene_texture_initialized ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Grow only. Shrinking back would mean reallocating every G-buffer again the moment the window
|
||||
// grew back, and the shrunk state is already handled correctly: Scene_texture_u_scale and
|
||||
// _v_scale confine rendering to the sub-rectangle actually in use. The hardware limit is
|
||||
// applied here rather than left to opengl_setup_scene_textures(), so that a viewport larger
|
||||
// than anything the GPU can allocate compares equal below and stops asking.
|
||||
const int new_width = MIN(MAX(gr_screen.max_w, Scene_texture_width), GL_max_renderbuffer_size);
|
||||
const int new_height = MIN(MAX(gr_screen.max_h, Scene_texture_height), GL_max_renderbuffer_size);
|
||||
|
||||
// The overwhelmingly common case: qtFred calls gr_screen_resize() every frame and the game
|
||||
// calls it on every SDL resize event, almost always at a size the current targets already
|
||||
// cover -- or, past the hardware limit, at one they never will.
|
||||
if ( new_width == Scene_texture_width && new_height == Scene_texture_height ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Tearing down the framebuffer we are currently rendering into would corrupt the frame rather
|
||||
// than fail cleanly, so refuse rather than trying to recover. Callers resize between frames.
|
||||
// Scene_framebuffer_in_frame covers the post-processing passes too: they only ever run inside
|
||||
// gr_scene_texture_begin()/end(), so it is set for the whole of Post_in_frame as well.
|
||||
if ( Scene_framebuffer_in_frame ) {
|
||||
Assertion(false, "Tried to resize the render targets to %dx%d while a scene was being "
|
||||
"rendered into them! The resize has been skipped; the frame will be stretched.",
|
||||
new_width, new_height);
|
||||
return;
|
||||
}
|
||||
|
||||
mprintf(("Growing render targets from %dx%d to %dx%d to cover the new %dx%d viewport.\n",
|
||||
Scene_texture_width, Scene_texture_height, new_width, new_height,
|
||||
gr_screen.max_w, gr_screen.max_h));
|
||||
|
||||
// Leave the framebuffer cache pointing at a name that cannot be deleted out from under it.
|
||||
GL_state.BindFrameBufferBoth(0, 0);
|
||||
|
||||
// Only the size-dependent resources are touched. The post-processing table, the compiled
|
||||
// shaders and the SMAA lookup textures are all resolution-independent and stay alive, which is
|
||||
// what keeps this cheap enough to run off a window drag. The post-processing targets are
|
||||
// rebuilt after the scene textures because they are sized to match them.
|
||||
opengl_scene_texture_shutdown();
|
||||
opengl_setup_scene_textures(new_width, new_height);
|
||||
|
||||
// Reallocating larger is exactly when running out of video memory is most likely, and
|
||||
// opengl_setup_scene_textures() reports that by leaving the scene uninitialized (having
|
||||
// already turned post-processing and soft particles off). Rebuilding the post-processing
|
||||
// targets on top of scene textures that don't exist would only make it worse, so stop here;
|
||||
// the renderer keeps drawing without the offscreen pipeline.
|
||||
if ( !Scene_texture_initialized ) {
|
||||
mprintf(("Failed to allocate %dx%d render targets! The offscreen rendering pipeline has "
|
||||
"been disabled for the rest of this session.\n", new_width, new_height));
|
||||
return;
|
||||
}
|
||||
|
||||
opengl_post_resize_render_targets();
|
||||
}
|
||||
|
||||
size_t opengl_get_scene_render_target_bytes()
|
||||
{
|
||||
// The bytes for each pixel come from the internal formats that opengl_setup_scene_textures()
|
||||
@@ -860,6 +903,30 @@ void gr_opengl_scene_texture_begin()
|
||||
Scene_texture_v_scale = 1.0f;
|
||||
}
|
||||
|
||||
// A ratio above 1.0 means the viewport outgrew the allocation after the fact -- the scene
|
||||
// texture can't be resized, so all we can do is render the part that fits and let the blit
|
||||
// stretch it back out, which misaligns everything by a different amount on each axis. The
|
||||
// floor above is meant to make this unreachable; if it does happen (a display hotplug or
|
||||
// resolution change after gr_init(), or an allocation clamped by GL_max_renderbuffer_size on
|
||||
// low-end hardware) say so once rather than every frame.
|
||||
if (Scene_texture_u_scale > 1.0f || Scene_texture_v_scale > 1.0f) {
|
||||
static bool reported_undersized_scene_texture = false;
|
||||
|
||||
if (!reported_undersized_scene_texture) {
|
||||
reported_undersized_scene_texture = true;
|
||||
nprintf(("OpenGL",
|
||||
"Viewport (%dx%d) is larger than the scene texture backing it (%dx%d); "
|
||||
"the post-processed image will be stretched to fit.\n",
|
||||
gr_screen.max_w,
|
||||
gr_screen.max_h,
|
||||
Scene_texture_width,
|
||||
Scene_texture_height));
|
||||
}
|
||||
}
|
||||
|
||||
CLAMP(Scene_texture_u_scale, 0.0f, 1.0f);
|
||||
CLAMP(Scene_texture_v_scale, 0.0f, 1.0f);
|
||||
|
||||
if (!light_deferred_enabled()) {
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
@@ -1306,6 +1373,11 @@ void opengl_draw_full_screen_textured(GLfloat u1, GLfloat v1, GLfloat u2, GLfloa
|
||||
opengl_render_primitives_immediate(PRIM_TYPE_TRIS, &vert_def, 3, glVertices, sizeof(glVertices));
|
||||
}
|
||||
|
||||
void opengl_draw_full_screen_scene_texture()
|
||||
{
|
||||
opengl_draw_full_screen_textured(0.0f, 0.0f, Scene_texture_u_scale, Scene_texture_v_scale);
|
||||
}
|
||||
|
||||
void gr_opengl_render_decals(decal_material* material_info,
|
||||
primitive_type prim_type,
|
||||
vertex_layout* layout,
|
||||
|
||||
@@ -55,8 +55,14 @@ void gr_opengl_render_shield_impact(shield_material* material_info,
|
||||
gr_buffer_handle buffer_handle,
|
||||
int n_verts);
|
||||
|
||||
void opengl_setup_scene_textures();
|
||||
void opengl_setup_scene_textures(int width, int height);
|
||||
void opengl_scene_texture_shutdown();
|
||||
void gr_opengl_resize_render_targets();
|
||||
|
||||
// Release a render target, keeping the GL state cache in sync. See the definitions for why the
|
||||
// cache matters now that these are rebuilt mid-session.
|
||||
void opengl_delete_render_texture(GLuint& tex);
|
||||
void opengl_delete_render_framebuffer(GLuint& fbo);
|
||||
|
||||
/**
|
||||
* @brief Gives the bytes that the scene, backbuffer, and distortion textures hold
|
||||
@@ -156,6 +162,12 @@ void opengl_draw_textured_quad(GLfloat x1,
|
||||
*/
|
||||
void opengl_draw_full_screen_textured(GLfloat u1, GLfloat v1, GLfloat u2, GLfloat v2);
|
||||
|
||||
// Fullscreen pass over a source that is one of the scene/post-processing textures. Those are only
|
||||
// filled out to Scene_texture_u_scale/v_scale of their allocation, so sampling them over the full
|
||||
// [0,1] range would pull in whatever is beyond the rendered region. Prefer this over passing
|
||||
// literal 1.0f extents whenever the bound texture came from that pipeline.
|
||||
void opengl_draw_full_screen_scene_texture();
|
||||
|
||||
inline GLenum opengl_primitive_type(primitive_type prim_type);
|
||||
|
||||
void gr_opengl_start_decal_pass();
|
||||
|
||||
@@ -28,6 +28,9 @@
|
||||
#include "es_compatibility.h"
|
||||
#endif
|
||||
|
||||
static void opengl_post_setup_render_targets();
|
||||
static void opengl_post_shutdown_render_targets();
|
||||
|
||||
extern bool PostProcessing_override;
|
||||
extern int opengl_check_framebuffer();
|
||||
// Needed to track where the FXAA shaders are
|
||||
@@ -99,7 +102,7 @@ void opengl_post_pass_tonemap()
|
||||
|
||||
GL_state.Texture.Enable(0, GL_TEXTURE_2D, Scene_color_texture);
|
||||
|
||||
opengl_draw_full_screen_textured(0.0f, 0.0f, Scene_texture_u_scale, Scene_texture_u_scale);
|
||||
opengl_draw_full_screen_scene_texture();
|
||||
}
|
||||
|
||||
void opengl_post_pass_bloom()
|
||||
@@ -134,7 +137,10 @@ void opengl_post_pass_bloom()
|
||||
|
||||
GL_state.Texture.Enable(0, GL_TEXTURE_2D, Scene_color_texture);
|
||||
|
||||
opengl_draw_full_screen_textured(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
// Reads the scene texture directly rather than an already-cropped intermediate, so it is
|
||||
// the scaled variant. The blur/composite passes below read Bloom_textures, which this pass
|
||||
// fills edge to edge, so those stay unscaled.
|
||||
opengl_draw_full_screen_scene_texture();
|
||||
}
|
||||
// ------ end bright pass ------
|
||||
|
||||
@@ -293,7 +299,7 @@ void opengl_post_pass_fxaa()
|
||||
|
||||
GL_state.Texture.Enable(0, GL_TEXTURE_2D, Scene_ldr_texture);
|
||||
|
||||
opengl_draw_full_screen_textured(0.0f, 0.0f, Scene_texture_u_scale, Scene_texture_u_scale);
|
||||
opengl_draw_full_screen_scene_texture();
|
||||
|
||||
// set and configure post shader ..
|
||||
opengl_shader_set_current(gr_opengl_maybe_create_shader(SDR_TYPE_POST_PROCESS_FXAA, 0));
|
||||
@@ -310,7 +316,7 @@ void opengl_post_pass_fxaa()
|
||||
|
||||
GL_state.Texture.Enable(0, GL_TEXTURE_2D, Scene_luminance_texture);
|
||||
|
||||
opengl_draw_full_screen_textured(0.0f, 0.0f, Scene_texture_u_scale, Scene_texture_u_scale);
|
||||
opengl_draw_full_screen_scene_texture();
|
||||
|
||||
opengl_shader_set_current();
|
||||
}
|
||||
@@ -333,7 +339,7 @@ static void smaa_detect_edges()
|
||||
|
||||
GL_state.Texture.Enable(0, GL_TEXTURE_2D, Scene_ldr_texture);
|
||||
|
||||
opengl_draw_full_screen_textured(0.0f, 0.0f, Scene_texture_u_scale, Scene_texture_u_scale);
|
||||
opengl_draw_full_screen_scene_texture();
|
||||
}
|
||||
|
||||
static void smaa_calculate_blending_weights()
|
||||
@@ -358,7 +364,7 @@ static void smaa_calculate_blending_weights()
|
||||
GL_state.Texture.Enable(1, GL_TEXTURE_2D, Smaa_area_tex);
|
||||
GL_state.Texture.Enable(2, GL_TEXTURE_2D, Smaa_search_tex);
|
||||
|
||||
opengl_draw_full_screen_textured(0.0f, 0.0f, Scene_texture_u_scale, Scene_texture_u_scale);
|
||||
opengl_draw_full_screen_scene_texture();
|
||||
}
|
||||
|
||||
static void smaa_neighborhood_blending()
|
||||
@@ -381,7 +387,7 @@ static void smaa_neighborhood_blending()
|
||||
GL_state.Texture.Enable(0, GL_TEXTURE_2D, Scene_ldr_texture);
|
||||
GL_state.Texture.Enable(1, GL_TEXTURE_2D, Smaa_blend_tex);
|
||||
|
||||
opengl_draw_full_screen_textured(0.0f, 0.0f, Scene_texture_u_scale, Scene_texture_u_scale);
|
||||
opengl_draw_full_screen_scene_texture();
|
||||
}
|
||||
|
||||
void smaa_resolve()
|
||||
@@ -491,7 +497,7 @@ void opengl_post_lightshafts()
|
||||
GL_state.Blend(GL_TRUE);
|
||||
GL_state.SetAlphaBlendMode(ALPHA_BLEND_ADDITIVE);
|
||||
|
||||
opengl_draw_full_screen_textured(0.0f, 0.0f, Scene_texture_u_scale, Scene_texture_u_scale);
|
||||
opengl_draw_full_screen_scene_texture();
|
||||
|
||||
GL_state.Blend(GL_FALSE);
|
||||
break;
|
||||
@@ -625,7 +631,7 @@ void gr_opengl_post_process_end()
|
||||
// now render it to the screen ...
|
||||
GL_state.PopFramebufferState();
|
||||
|
||||
opengl_draw_full_screen_textured(0.0f, 0.0f, Scene_texture_u_scale, Scene_texture_u_scale);
|
||||
opengl_draw_full_screen_scene_texture();
|
||||
|
||||
//Shadow Map debug window
|
||||
//#define SHADOW_DEBUG
|
||||
@@ -1020,77 +1026,93 @@ static GLuint load_smaa_texture(GLsizei width, GLsizei height, GLenum format, co
|
||||
return tex;
|
||||
}
|
||||
|
||||
static void setup_smaa_resources()
|
||||
// The SMAA area and search textures are fixed-size lookup tables baked into the binary, so unlike
|
||||
// everything else here they survive a resolution change untouched.
|
||||
static void setup_smaa_lookup_textures()
|
||||
{
|
||||
GL_state.PushFramebufferState();
|
||||
|
||||
Smaa_area_tex = load_smaa_texture(AREATEX_WIDTH, AREATEX_HEIGHT, GL_RG8, areaTexBytes, "SMAA Area Texture");
|
||||
Smaa_search_tex =
|
||||
load_smaa_texture(SEARCHTEX_WIDTH, SEARCHTEX_HEIGHT, GL_R8, searchTexBytes, "SMAA Search Texture");
|
||||
}
|
||||
|
||||
static void setup_smaa_render_targets()
|
||||
{
|
||||
setup_smaa_edges_resources();
|
||||
|
||||
setup_smaa_blending_weight_resources();
|
||||
|
||||
setup_smaa_neighborhood_blending_resources();
|
||||
|
||||
GL_state.PopFramebufferState();
|
||||
}
|
||||
|
||||
// generate and test the framebuffer and textures that we are going to use
|
||||
static bool opengl_post_init_framebuffer()
|
||||
static void shutdown_smaa_render_targets()
|
||||
{
|
||||
bool rval = false;
|
||||
opengl_delete_render_texture(Smaa_edges_tex);
|
||||
opengl_delete_render_framebuffer(Smaa_edge_detection_fb);
|
||||
|
||||
// clamp size, if needed
|
||||
Post_texture_width = gr_screen.max_w;
|
||||
Post_texture_height = gr_screen.max_h;
|
||||
opengl_delete_render_texture(Smaa_blend_tex);
|
||||
opengl_delete_render_framebuffer(Smaa_blending_weight_fb);
|
||||
|
||||
if (Post_texture_width > GL_max_renderbuffer_size) {
|
||||
Post_texture_width = GL_max_renderbuffer_size;
|
||||
}
|
||||
opengl_delete_render_texture(Smaa_output_tex);
|
||||
opengl_delete_render_framebuffer(Smaa_neighborhood_blending_fb);
|
||||
}
|
||||
|
||||
if (Post_texture_height > GL_max_renderbuffer_size) {
|
||||
Post_texture_height = GL_max_renderbuffer_size;
|
||||
}
|
||||
// Allocate every post-processing resource whose size follows the scene textures. Split out from
|
||||
// opengl_post_process_init() so gr_opengl_resize_render_targets() can rebuild just these without
|
||||
// re-parsing post_processing.tbl or recompiling shaders.
|
||||
static void opengl_post_setup_render_targets()
|
||||
{
|
||||
// These consume the scene textures pass by pass, so they have to match them exactly rather
|
||||
// than being sized from gr_screen independently -- see gr_opengl_scene_texture_begin() for
|
||||
// what the two sizes diverging would mean.
|
||||
Post_texture_width = Scene_texture_width;
|
||||
Post_texture_height = Scene_texture_height;
|
||||
|
||||
GL_state.PushFramebufferState();
|
||||
|
||||
opengl_setup_bloom_textures();
|
||||
|
||||
// Always set up SMAA resources so the user can switch to an SMAA preset
|
||||
// at runtime even when starting with a non-SMAA AA mode, such as None.
|
||||
//if (Gr_aa_mode != AntiAliasMode::None) {
|
||||
setup_smaa_resources();
|
||||
//}
|
||||
setup_smaa_render_targets();
|
||||
|
||||
GL_state.PopFramebufferState();
|
||||
|
||||
GL_state.BindFrameBuffer(0);
|
||||
|
||||
rval = true;
|
||||
|
||||
if ( opengl_check_for_errors("post_init_framebuffer()") ) {
|
||||
rval = false;
|
||||
}
|
||||
|
||||
return rval;
|
||||
}
|
||||
|
||||
|
||||
|
||||
void opengl_post_process_shutdown_bloom()
|
||||
{
|
||||
if ( Bloom_textures[0] ) {
|
||||
glDeleteTextures(1, &Bloom_textures[0]);
|
||||
Bloom_textures[0] = 0;
|
||||
opengl_delete_render_texture(Bloom_textures[0]);
|
||||
opengl_delete_render_texture(Bloom_textures[1]);
|
||||
opengl_delete_render_framebuffer(Bloom_framebuffer);
|
||||
}
|
||||
|
||||
static void opengl_post_shutdown_render_targets()
|
||||
{
|
||||
opengl_post_process_shutdown_bloom();
|
||||
shutdown_smaa_render_targets();
|
||||
}
|
||||
|
||||
void opengl_post_resize_render_targets()
|
||||
{
|
||||
// Post-processing may have been disabled outright (no FBOs, missing shaders, or turned off in
|
||||
// the table), in which case none of these resources exist and none should start existing now.
|
||||
if ( !Post_initialized ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( Bloom_textures[1] ) {
|
||||
glDeleteTextures(1, &Bloom_textures[1]);
|
||||
Bloom_textures[1] = 0;
|
||||
}
|
||||
opengl_post_shutdown_render_targets();
|
||||
opengl_post_setup_render_targets();
|
||||
}
|
||||
|
||||
if ( Bloom_framebuffer > 0 ) {
|
||||
glDeleteFramebuffers(1, &Bloom_framebuffer);
|
||||
Bloom_framebuffer = 0;
|
||||
}
|
||||
// generate and test the framebuffer and textures that we are going to use
|
||||
static bool opengl_post_init_framebuffer()
|
||||
{
|
||||
setup_smaa_lookup_textures();
|
||||
|
||||
opengl_post_setup_render_targets();
|
||||
|
||||
return !opengl_check_for_errors("post_init_framebuffer()");
|
||||
}
|
||||
|
||||
size_t opengl_get_postprocessing_render_target_bytes()
|
||||
@@ -1188,20 +1210,16 @@ void opengl_post_process_shutdown()
|
||||
return;
|
||||
}
|
||||
|
||||
if (Post_framebuffer_id[0]) {
|
||||
glDeleteFramebuffers(1, &Post_framebuffer_id[0]);
|
||||
Post_framebuffer_id[0] = 0;
|
||||
|
||||
if (Post_framebuffer_id[1]) {
|
||||
glDeleteFramebuffers(1, &Post_framebuffer_id[1]);
|
||||
Post_framebuffer_id[1] = 0;
|
||||
}
|
||||
}
|
||||
opengl_delete_render_framebuffer(Post_framebuffer_id[0]);
|
||||
opengl_delete_render_framebuffer(Post_framebuffer_id[1]);
|
||||
|
||||
graphics::Post_processing_manager->clear();
|
||||
graphics::Post_processing_manager = nullptr;
|
||||
|
||||
opengl_post_process_shutdown_bloom();
|
||||
opengl_post_shutdown_render_targets();
|
||||
|
||||
opengl_delete_render_texture(Smaa_area_tex);
|
||||
opengl_delete_render_texture(Smaa_search_tex);
|
||||
|
||||
Post_in_frame = false;
|
||||
Post_active_shader_index = 0;
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
void opengl_post_process_init();
|
||||
void opengl_post_process_shutdown();
|
||||
|
||||
// Rebuild the resolution-dependent subset of the above for the current scene texture size, without
|
||||
// re-parsing post_processing.tbl or recompiling shaders. No-op if post-processing isn't active.
|
||||
void opengl_post_resize_render_targets();
|
||||
|
||||
/**
|
||||
* @brief Gives the bytes that the bloom and SMAA render textures hold
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -92,28 +92,6 @@ static auto TextureFilteringOption __UNUSED = options::OptionBuilder<int>("Graph
|
||||
.parser(parse_texture_filtering_func)
|
||||
.finish();
|
||||
|
||||
static SCP_vector<float> anisotropic_value_enumerator()
|
||||
{
|
||||
float max;
|
||||
if (!gr_get_property(gr_property::MAX_ANISOTROPY, &max)) {
|
||||
return SCP_vector<float>();
|
||||
}
|
||||
|
||||
if (max <= 2.0f) {
|
||||
return SCP_vector<float>();
|
||||
}
|
||||
|
||||
SCP_vector<float> out;
|
||||
|
||||
// We assume here that the anisotropy levels are powers of two...
|
||||
float current = 1.0f;
|
||||
while (current <= max) {
|
||||
out.push_back(current);
|
||||
current *= 2.0f;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
static SCP_string anisotropic_display(float val)
|
||||
{
|
||||
if (val < 2.0f) {
|
||||
@@ -137,7 +115,7 @@ static float anisotropic_default()
|
||||
static auto AnisotropyOption = options::OptionBuilder<float>("Graphics.Anisotropy",
|
||||
std::pair<const char*, int>{"Anistropic filtering", 1736},
|
||||
std::pair<const char*, int>{"Controls the amount of anistropic filtering of the textures", 1737})
|
||||
.enumerator(anisotropic_value_enumerator)
|
||||
.enumerator(gr_get_supported_anisotropy_levels)
|
||||
.category(std::make_pair("Graphics", 1825))
|
||||
.display(anisotropic_display)
|
||||
.default_func(anisotropic_default)
|
||||
@@ -213,8 +191,15 @@ void opengl_tcache_init()
|
||||
// check what mipmap filter we should be using
|
||||
// 0 == Bilinear
|
||||
// 1 == Trilinear
|
||||
// Seed from the legacy config key first: TextureFilteringOption's default_func returns
|
||||
// GL_mipmap_filter, so this read is what supplies that default. Only then let the option
|
||||
// override it, the same order the anisotropy setting below uses.
|
||||
GL_mipmap_filter = os_config_read_uint(NULL, "TextureFilter", 1);
|
||||
|
||||
if (Using_in_game_options) {
|
||||
GL_mipmap_filter = TextureFilteringOption->getValue();
|
||||
}
|
||||
|
||||
if (GL_mipmap_filter > 1) {
|
||||
GL_mipmap_filter = 1;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -506,8 +507,12 @@ void VulkanDeferredLighting::render(vk::CommandBuffer cmd)
|
||||
{
|
||||
auto* header = reinterpret_cast<graphics::deferred_global_data*>(uboMapped + regionBase);
|
||||
memset(header, 0, sizeof(graphics::deferred_global_data));
|
||||
header->invScreenWidth = 1.0f / gr_screen.max_w;
|
||||
header->invScreenHeight = 1.0f / gr_screen.max_h;
|
||||
// Same as the OpenGL backend: deferred-f.sdr normalizes gl_FragCoord against these to
|
||||
// sample the G-buffer, so they must describe the G-buffer, not gr_screen. resize() keeps
|
||||
// sceneExtent equal to gr_screen today, which is why deriving it from the extent is a
|
||||
// no-op here -- but it states the actual requirement instead of relying on that.
|
||||
header->invScreenWidth = 1.0f / static_cast<float>(m_ctx->sceneExtent.width);
|
||||
header->invScreenHeight = 1.0f / static_cast<float>(m_ctx->sceneExtent.height);
|
||||
header->nearPlane = gr_near_plane;
|
||||
|
||||
if (m_shadow->isInitialized() && Shadow_quality != ShadowQuality::Disabled) {
|
||||
@@ -687,6 +692,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 +762,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 +811,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
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# Module: qtfred
|
||||
|
||||
Entry-point guide for this module lives at:
|
||||
**`documentation/modules/qtfred.md`** (from repo root).
|
||||
|
||||
It covers the editor's purpose, build requirements, layout, and how it relates to
|
||||
the shared engine code in `code/`.
|
||||
|
||||
For the engine-wide architecture overview see `documentation/ARCHITECTURE.md`.
|
||||
For build/test/style conventions see the root `AGENTS.md`.
|
||||
@@ -10,7 +10,9 @@
|
||||
<h1>Preferences</h1>
|
||||
<p>Opens via <strong>File › Preferences</strong>.</p>
|
||||
<p>Controls QtFRED editor settings. Changes take effect immediately; there is no
|
||||
Apply button.</p>
|
||||
Apply button. The exception is a handful of graphics settings that the renderer
|
||||
can only read while it is starting up - those are marked below, and QtFRED
|
||||
applies them the next time it is launched.</p>
|
||||
|
||||
<h2>General</h2>
|
||||
<p>Miscellaneous editor options. Hover over each setting for a tooltip
|
||||
@@ -37,6 +39,50 @@ and <strong>Misc</strong>, which holds the object and subsystem commands and the
|
||||
remaining tools. The menu items and their shortcuts are the same in both layouts,
|
||||
and the change takes effect immediately.</p>
|
||||
|
||||
<h2>Graphics</h2>
|
||||
<p>Controls how the 3D viewport is rendered. These settings affect the editor
|
||||
only - they are stored separately from the game's own graphics options and
|
||||
never change them.</p>
|
||||
|
||||
<h3>Post-processing</h3>
|
||||
<p><strong>Enable post-processing in the viewport</strong> routes the viewport
|
||||
through the same HDR rendering pipeline the game uses, adding bloom,
|
||||
tonemapping, and lightshafts. It is off by default, so missions look the way
|
||||
they always have until you opt in. The same switch is available as
|
||||
<strong>View › Enable Post Processing</strong>.</p>
|
||||
<p>Every other setting in this section only changes what you see while
|
||||
post-processing is on, because they are all consumed by that pipeline.</p>
|
||||
|
||||
<h3>Shadows & anti-aliasing</h3>
|
||||
<ul>
|
||||
<li><strong>Shadow quality</strong> - detail level of the shadows cast by the
|
||||
mission's sun. Higher settings cost more performance. Shadows are off
|
||||
until you raise this above <em>Disabled</em>, so enabling post-processing
|
||||
alone will not produce any. <em>Requires a restart</em> - including the
|
||||
first time you turn it on.</li>
|
||||
<li><strong>Anti-aliasing</strong> - post-process anti-aliasing mode (FXAA or
|
||||
SMAA, in increasing quality). Applies immediately.</li>
|
||||
<li><strong>MSAA</strong> - multisample anti-aliasing for the 3D scene, off
|
||||
or 4x/8x. More expensive than the post-process modes above but higher
|
||||
quality. <em>Requires a restart.</em></li>
|
||||
</ul>
|
||||
|
||||
<h3>Textures</h3>
|
||||
<ul>
|
||||
<li><strong>Texture filtering</strong> - bilinear or trilinear mipmap
|
||||
filtering. Left alone, QtFRED uses the same default the game does.
|
||||
<em>Requires a restart.</em></li>
|
||||
<li><strong>Anisotropic filtering</strong> - sharpens textures viewed at a
|
||||
steep angle. The list is limited to the levels your GPU reports, and the
|
||||
control is greyed out if it reports nothing worth offering. Left alone,
|
||||
QtFRED uses your hardware's maximum. <em>Requires a restart.</em></li>
|
||||
</ul>
|
||||
|
||||
<h3>Gamma</h3>
|
||||
<p>Brightness of the viewport, from 0.10 to 5.00. QtFRED defaults this to 3.00,
|
||||
higher than the game, because the viewport is not tonemapped unless
|
||||
post-processing is enabled. Applies immediately.</p>
|
||||
|
||||
<h2>Grid</h2>
|
||||
<p>Configures the reference grid shown in the main viewport. You can set which
|
||||
world-space plane the grid lies on (XZ, XY, or YZ) and adjust the grid's center
|
||||
|
||||
@@ -28,6 +28,8 @@ add_file_folder("Source/Mission"
|
||||
src/mission/EditorViewport.h
|
||||
src/mission/FredRenderer.cpp
|
||||
src/mission/FredRenderer.h
|
||||
src/mission/GraphicsSettings.cpp
|
||||
src/mission/GraphicsSettings.h
|
||||
src/mission/IDialogProvider.h
|
||||
src/mission/management.cpp
|
||||
src/mission/management.h
|
||||
|
||||
@@ -123,6 +123,7 @@ EditorViewport::EditorViewport(Editor* in_editor, std::unique_ptr<FredRenderer>&
|
||||
syncMissionLayerNames();
|
||||
|
||||
loadSettings();
|
||||
view.Graphics.applyLive();
|
||||
|
||||
fredApp->runAfterInit([this]() { initialSetup(); });
|
||||
}
|
||||
@@ -185,6 +186,8 @@ void EditorViewport::loadSettings() {
|
||||
camera.setInvertOrbitY(settings.value("camera_invert_orbit_y", camera.getInvertOrbitY()).toBool());
|
||||
camera.setOrbitSelectionMidpoint(settings.value("camera_orbit_selection_midpoint", camera.getOrbitSelectionMidpoint()).toBool());
|
||||
settings.endGroup();
|
||||
|
||||
view.Graphics = GraphicsSettings::load();
|
||||
}
|
||||
|
||||
void EditorViewport::saveSettings() const {
|
||||
@@ -236,7 +239,10 @@ void EditorViewport::saveSettings() const {
|
||||
settings.setValue("camera_invert_orbit_y", camera.getInvertOrbitY());
|
||||
settings.setValue("camera_orbit_selection_midpoint", camera.getOrbitSelectionMidpoint());
|
||||
settings.endGroup();
|
||||
|
||||
view.Graphics.save();
|
||||
}
|
||||
|
||||
void EditorViewport::needsUpdate() {
|
||||
_renderer->scheduleUpdate();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "CameraController.h"
|
||||
#include "FredRenderer.h"
|
||||
#include "Editor.h"
|
||||
#include "GraphicsSettings.h"
|
||||
#include "IDialogProvider.h"
|
||||
#include "ui/ThemeMode.h"
|
||||
|
||||
@@ -64,6 +65,9 @@ struct ViewSettings {
|
||||
int Outline_lod = 1;
|
||||
float Label_font_scale = 1.0f; // multiplier applied to viewport text labels
|
||||
|
||||
//! Preferences > Graphics. Owns its own persistence and apply rules; see GraphicsSettings.
|
||||
GraphicsSettings Graphics;
|
||||
|
||||
ViewSettings();
|
||||
};
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <graphics/font.h>
|
||||
#include <graphics/matrix.h>
|
||||
#include <graphics/light.h>
|
||||
#include <graphics/shadows.h>
|
||||
#include <lighting/lighting.h>
|
||||
#include <starfield/starfield.h>
|
||||
#include <ship/ship.h>
|
||||
@@ -28,6 +29,8 @@
|
||||
#include <mod_table/mod_table.h>
|
||||
#include <cfile/cfile.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "mission/object.h"
|
||||
#include "prop/prop.h"
|
||||
#include "weapon/weapon.h"
|
||||
@@ -56,6 +59,38 @@ void disable_htl() {
|
||||
gr_end_view_matrix();
|
||||
}
|
||||
|
||||
//! Scoped gr_scene_texture_begin()/end(), so the two can't drift apart as render_frame() grows.
|
||||
struct ScenePostProcessing {
|
||||
ScenePostProcessing() { gr_scene_texture_begin(); }
|
||||
~ScenePostProcessing() { gr_scene_texture_end(); }
|
||||
|
||||
ScenePostProcessing(const ScenePostProcessing&) = delete;
|
||||
ScenePostProcessing& operator=(const ScenePostProcessing&) = delete;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Render the shadow pass for the current camera.
|
||||
*
|
||||
* Only meaningful inside a ScenePostProcessing scope: the shadow pass writes into deferred
|
||||
* G-buffer surfaces that only exist while the scene texture is bound. Mirrors game_render_frame(),
|
||||
* which calls this right after its own gr_scene_texture_begin() + stars_draw().
|
||||
*
|
||||
* shadows_render_all() works on the HTL proj/view matrix stack -- the same one enable_htl() and
|
||||
* disable_htl() push and pop for the starfield -- and expects it to be open: it ends whatever is
|
||||
* active (gr_end_view_matrix() asserts on modelview_matrix_depth) and restores it when done. The
|
||||
* starfield's disable_htl() just popped that stack, hence the push here, and the matching pop
|
||||
* afterwards so the next frame's enable_htl() doesn't assert on a stack left open.
|
||||
*/
|
||||
void render_shadows() {
|
||||
gr_set_proj_matrix(Proj_fov, gr_screen.clip_aspect, Min_draw_distance, Max_draw_distance);
|
||||
gr_set_view_matrix(&Eye_position, &Eye_matrix);
|
||||
|
||||
shadows_render_all(Proj_fov, &Eye_matrix, &Eye_position, nullptr, nullptr, nullptr);
|
||||
|
||||
gr_end_proj_matrix();
|
||||
gr_end_view_matrix();
|
||||
}
|
||||
|
||||
bool fred_colors_inited = false;
|
||||
color colour_white;
|
||||
color colour_green;
|
||||
@@ -995,6 +1030,15 @@ void FredRenderer::render_frame(int cur_object_index,
|
||||
|
||||
g3_set_view_matrix(&_viewport->camera.eye_pos, &_viewport->camera.eye_orient, 0.5f);
|
||||
|
||||
// Optionally run the 3D world through the game's HDR post-processing pipeline (bloom,
|
||||
// tonemapping, lightshafts, shadows) instead of drawing straight to the default framebuffer.
|
||||
// Brackets only the 3D content, the same way game_render_frame() does; the 2D overlays further
|
||||
// down (distances, ship info, tooltips) stay outside it.
|
||||
std::optional<ScenePostProcessing> postProcessing;
|
||||
if (view().Graphics.enablePostProcessing) {
|
||||
postProcessing.emplace();
|
||||
}
|
||||
|
||||
// Force max star detail so the editor always shows the full Num_stars count
|
||||
// regardless of the player's graphics quality setting (Detail.num_stars can be 0).
|
||||
int saved_detail_stars = Detail.num_stars;
|
||||
@@ -1004,6 +1048,10 @@ void FredRenderer::render_frame(int cur_object_index,
|
||||
disable_htl();
|
||||
Detail.num_stars = saved_detail_stars;
|
||||
|
||||
if (postProcessing) {
|
||||
render_shadows();
|
||||
}
|
||||
|
||||
if (view().Show_horizon) {
|
||||
gr_set_color(128, 128, 64);
|
||||
g3_draw_horizon_line();
|
||||
@@ -1020,6 +1068,8 @@ void FredRenderer::render_frame(int cur_object_index,
|
||||
render_models(cur_object_index);
|
||||
render_volumetric_overlay();
|
||||
|
||||
postProcessing.reset();
|
||||
|
||||
if (view().Show_distances) {
|
||||
display_distances();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
#include "mission/GraphicsSettings.h"
|
||||
|
||||
#include <cmdline/cmdline.h>
|
||||
#include <options/OptionsManager.h>
|
||||
|
||||
#include <QSettings>
|
||||
|
||||
namespace fso::fred {
|
||||
|
||||
namespace {
|
||||
|
||||
const char* SETTINGS_GROUP = "Preferences";
|
||||
|
||||
const char* KEY_POST_PROCESSING = "view_enable_post_processing";
|
||||
const char* KEY_SHADOW_QUALITY = "view_graphics_shadow_quality";
|
||||
const char* KEY_AA_MODE = "view_graphics_aa_mode";
|
||||
const char* KEY_MSAA_SAMPLES = "view_graphics_msaa_samples";
|
||||
const char* KEY_TEXTURE_FILTER = "view_graphics_texture_filter";
|
||||
const char* KEY_ANISOTROPY = "view_graphics_anisotropy";
|
||||
const char* KEY_GAMMA = "view_graphics_gamma";
|
||||
|
||||
// A settings file can be hand-edited or left over from a build with a different set of values, so
|
||||
// anything that ends up in an enum or a fixed value list gets checked rather than cast blindly.
|
||||
template <typename T>
|
||||
T readEnum(const QSettings& settings, const char* key, T fallback, T highest)
|
||||
{
|
||||
const int raw = settings.value(key, static_cast<int>(fallback)).toInt();
|
||||
|
||||
if (raw < 0 || raw > static_cast<int>(highest)) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return static_cast<T>(raw);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool GraphicsSettings::operator==(const GraphicsSettings& rhs) const
|
||||
{
|
||||
return enablePostProcessing == rhs.enablePostProcessing && shadowQuality == rhs.shadowQuality &&
|
||||
aaMode == rhs.aaMode && msaaSamples == rhs.msaaSamples && gamma == rhs.gamma &&
|
||||
textureFilter == rhs.textureFilter && anisotropy == rhs.anisotropy;
|
||||
}
|
||||
|
||||
SCP_vector<int> GraphicsSettings::validMsaaSampleCounts()
|
||||
{
|
||||
return {0, 4, 8};
|
||||
}
|
||||
|
||||
GraphicsSettings GraphicsSettings::load()
|
||||
{
|
||||
GraphicsSettings out;
|
||||
|
||||
QSettings settings;
|
||||
settings.beginGroup(SETTINGS_GROUP);
|
||||
|
||||
out.enablePostProcessing = settings.value(KEY_POST_PROCESSING, out.enablePostProcessing).toBool();
|
||||
out.shadowQuality = readEnum(settings, KEY_SHADOW_QUALITY, out.shadowQuality, ShadowQuality::Ultra);
|
||||
out.aaMode = readEnum(settings, KEY_AA_MODE, out.aaMode, AntiAliasMode::SMAA_Ultra);
|
||||
out.gamma = settings.value(KEY_GAMMA, out.gamma).toFloat();
|
||||
|
||||
if (settings.contains(KEY_TEXTURE_FILTER)) {
|
||||
out.textureFilter = settings.value(KEY_TEXTURE_FILTER).toInt() != 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
if (settings.contains(KEY_ANISOTROPY)) {
|
||||
out.anisotropy = settings.value(KEY_ANISOTROPY).toFloat();
|
||||
}
|
||||
|
||||
const int samples = settings.value(KEY_MSAA_SAMPLES, out.msaaSamples).toInt();
|
||||
const auto validSamples = validMsaaSampleCounts();
|
||||
if (std::find(validSamples.begin(), validSamples.end(), samples) != validSamples.end()) {
|
||||
out.msaaSamples = samples;
|
||||
}
|
||||
|
||||
settings.endGroup();
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
void GraphicsSettings::save() const
|
||||
{
|
||||
QSettings settings;
|
||||
settings.beginGroup(SETTINGS_GROUP);
|
||||
|
||||
settings.setValue(KEY_POST_PROCESSING, enablePostProcessing);
|
||||
settings.setValue(KEY_SHADOW_QUALITY, static_cast<int>(shadowQuality));
|
||||
settings.setValue(KEY_AA_MODE, static_cast<int>(aaMode));
|
||||
settings.setValue(KEY_MSAA_SAMPLES, msaaSamples);
|
||||
settings.setValue(KEY_GAMMA, gamma);
|
||||
|
||||
// Don't write the sentinels back out -- an absent key is what keeps the engine default in play.
|
||||
if (textureFilter != NO_TEXTURE_FILTER_CHOICE) {
|
||||
settings.setValue(KEY_TEXTURE_FILTER, textureFilter);
|
||||
}
|
||||
|
||||
if (anisotropy != NO_ANISOTROPY_CHOICE) {
|
||||
settings.setValue(KEY_ANISOTROPY, anisotropy);
|
||||
}
|
||||
|
||||
settings.endGroup();
|
||||
}
|
||||
|
||||
void GraphicsSettings::applyLive() const
|
||||
{
|
||||
Gr_aa_mode = aaMode;
|
||||
gr_set_gamma(gamma);
|
||||
}
|
||||
|
||||
GraphicsSettings GraphicsSettings::applyBeforeGrInit()
|
||||
{
|
||||
const GraphicsSettings settings = load();
|
||||
|
||||
Cmdline_msaa_enabled = settings.msaaSamples;
|
||||
Shadow_quality = settings.shadowQuality;
|
||||
|
||||
auto* options = options::OptionsManager::instance();
|
||||
|
||||
// Overrides are in-memory only, so they steer this session's gr_init() without touching the
|
||||
// config file the game reads its own graphics settings from.
|
||||
if (settings.textureFilter != NO_TEXTURE_FILTER_CHOICE) {
|
||||
options->setOverride("Graphics.TextureFilter", std::to_string(settings.textureFilter), "qtFRED Preferences");
|
||||
}
|
||||
|
||||
if (settings.anisotropy != NO_ANISOTROPY_CHOICE) {
|
||||
options->setOverride("Graphics.Anisotropy", std::to_string(settings.anisotropy), "qtFRED Preferences");
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
} // namespace fso::fred
|
||||
@@ -0,0 +1,81 @@
|
||||
#pragma once
|
||||
|
||||
#include <graphics/2d.h>
|
||||
#include <graphics/shadows.h>
|
||||
|
||||
namespace fso::fred {
|
||||
|
||||
/**
|
||||
* @brief qtFRED's Preferences > Graphics settings.
|
||||
*
|
||||
* Single owner of these values: the QSettings keys, their defaults, and the rules for when each
|
||||
* one can be applied all live here. Two very different callers need them -- management.cpp before
|
||||
* gr_init(), and EditorViewport once the editor is up -- and previously each read QSettings with
|
||||
* its own copy of the key strings and default values.
|
||||
*
|
||||
* The settings split by *when* they can take effect, which is what the two apply functions below
|
||||
* encode:
|
||||
*
|
||||
* - applyLive() settings can be changed at any time and are re-applied whenever the user hits
|
||||
* Apply in Preferences.
|
||||
* - applyBeforeGrInit() settings are baked into GPU resources during gr_init() and cannot be
|
||||
* changed afterwards, so they are read straight out of QSettings before the editor exists.
|
||||
* Shadow quality is the sharpest example: shadow_cascade_params_init() only runs during
|
||||
* gr_init(), and only if Shadow_quality is already non-Disabled, so setting it later leaves the
|
||||
* cascade buffers unsized and crashes the next frame that renders shadows.
|
||||
*/
|
||||
struct GraphicsSettings {
|
||||
// Texture filtering and anisotropy reach the engine through the options system, whose defaults
|
||||
// are better informed than anything qtFRED could hardcode -- the user's existing config for
|
||||
// one, the hardware maximum for the other. Both therefore carry a sentinel meaning "the user
|
||||
// has not chosen one", and applyBeforeGrInit() overrides only once there is a real choice.
|
||||
static constexpr int NO_TEXTURE_FILTER_CHOICE = -1;
|
||||
static constexpr float NO_ANISOTROPY_CHOICE = 0.0f;
|
||||
|
||||
/**
|
||||
* @brief Run the viewport through the game's HDR scene-texture + post-processing pipeline.
|
||||
*
|
||||
* Off by default so missions keep looking exactly as they do today unless a user opts in.
|
||||
* Everything else on this struct is only visible in the viewport while this is on.
|
||||
*/
|
||||
bool enablePostProcessing = false;
|
||||
|
||||
ShadowQuality shadowQuality = ShadowQuality::Disabled;
|
||||
AntiAliasMode aaMode = AntiAliasMode::None;
|
||||
|
||||
int msaaSamples = 0; //!< 0 (off), 4, or 8; see validMsaaSampleCounts()
|
||||
float gamma = 3.0f; //!< matches the brightness qtFRED has always launched with
|
||||
|
||||
int textureFilter = NO_TEXTURE_FILTER_CHOICE; //!< 0 = bilinear, 1 = trilinear
|
||||
float anisotropy = NO_ANISOTROPY_CHOICE; //!< 1.0 = off, otherwise a power of two
|
||||
|
||||
bool operator==(const GraphicsSettings& rhs) const;
|
||||
bool operator!=(const GraphicsSettings& rhs) const { return !(*this == rhs); }
|
||||
|
||||
/**
|
||||
* @brief The MSAA sample counts the Preferences combo offers, in combo order.
|
||||
*/
|
||||
static SCP_vector<int> validMsaaSampleCounts();
|
||||
|
||||
static GraphicsSettings load();
|
||||
void save() const;
|
||||
|
||||
/**
|
||||
* @brief Push the settings that can be changed without a restart into the engine.
|
||||
*/
|
||||
void applyLive() const;
|
||||
|
||||
/**
|
||||
* @brief Load and apply the settings that must be in place before gr_init() runs.
|
||||
*
|
||||
* The engine reads texture filtering and anisotropy through the options system, so those are
|
||||
* pushed as in-memory-only overrides rather than written out -- qtFRED must never rewrite the
|
||||
* config file the game reads its own graphics settings from.
|
||||
*
|
||||
* @return the settings that were loaded, so the caller can applyLive() them once gr_init() has
|
||||
* returned and there is a renderer to apply them to.
|
||||
*/
|
||||
static GraphicsSettings applyBeforeGrInit();
|
||||
};
|
||||
|
||||
} // namespace fso::fred
|
||||
@@ -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();
|
||||
|
||||
@@ -28,6 +28,7 @@ PreferencesDialogModel::PreferencesDialogModel(QObject* parent, EditorViewport*
|
||||
, _toolbarIconSize(viewport->toolbar_icon_size)
|
||||
, _outlineLod(viewport->view.Outline_lod)
|
||||
, _labelFontScale(viewport->view.Label_font_scale)
|
||||
, _graphics(viewport->view.Graphics)
|
||||
, _invertOrbitX(viewport->camera.getInvertOrbitX())
|
||||
, _invertOrbitY(viewport->camera.getInvertOrbitY())
|
||||
, _orbitSelectionMidpoint(viewport->camera.getOrbitSelectionMidpoint())
|
||||
@@ -73,6 +74,9 @@ bool PreferencesDialogModel::apply() {
|
||||
_viewport->toolbar_icon_size = _toolbarIconSize;
|
||||
_viewport->view.Outline_lod = _outlineLod;
|
||||
_viewport->view.Label_font_scale = _labelFontScale;
|
||||
_viewport->view.Graphics = _graphics;
|
||||
// Only some of these can take effect now; the rest need a restart (see GraphicsSettings).
|
||||
_viewport->view.Graphics.applyLive();
|
||||
_viewport->camera.setInvertOrbitX(_invertOrbitX);
|
||||
_viewport->camera.setInvertOrbitY(_invertOrbitY);
|
||||
_viewport->camera.setOrbitSelectionMidpoint(_orbitSelectionMidpoint);
|
||||
@@ -192,6 +196,9 @@ void PreferencesDialogModel::setOutlineLod(int value) { modify(_outlineLod, valu
|
||||
double PreferencesDialogModel::getLabelFontScale() const { return _labelFontScale; }
|
||||
void PreferencesDialogModel::setLabelFontScale(double value) { modify(_labelFontScale, static_cast<float>(value)); }
|
||||
|
||||
const GraphicsSettings& PreferencesDialogModel::getGraphics() const { return _graphics; }
|
||||
void PreferencesDialogModel::setGraphics(const GraphicsSettings& value) { modify(_graphics, value); }
|
||||
|
||||
QKeySequence PreferencesDialogModel::getControlKey(ControlAction action) const {
|
||||
auto it = _controlKeys.find(action);
|
||||
Assertion(it != _controlKeys.end(), "Unknown control action!");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "mission/GraphicsSettings.h"
|
||||
#include "mission/dialogs/AbstractDialogModel.h"
|
||||
#include "ui/ControlBindings.h"
|
||||
#include "ui/ThemeMode.h"
|
||||
@@ -73,6 +74,11 @@ public:
|
||||
double getLabelFontScale() const;
|
||||
void setLabelFontScale(double value);
|
||||
|
||||
// Graphics. The dialog edits a whole GraphicsSettings rather than a field at a time; the
|
||||
// setter exists so edits still route through modify() and mark the model dirty.
|
||||
const GraphicsSettings& getGraphics() const;
|
||||
void setGraphics(const GraphicsSettings& value);
|
||||
|
||||
// Controls
|
||||
QKeySequence getControlKey(ControlAction action) const;
|
||||
void setControlKey(ControlAction action, const QKeySequence& sequence);
|
||||
@@ -119,6 +125,8 @@ private:
|
||||
int _outlineLod;
|
||||
float _labelFontScale;
|
||||
|
||||
GraphicsSettings _graphics;
|
||||
|
||||
// Controls
|
||||
std::map<ControlAction, QKeySequence> _controlKeys;
|
||||
bool _invertOrbitX;
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
#include "object.h"
|
||||
|
||||
#include "mission/GraphicsSettings.h"
|
||||
|
||||
#include "cmdline/cmdline.h"
|
||||
|
||||
#include <asteroid/asteroid.h>
|
||||
@@ -126,9 +128,16 @@ initialize(const std::string& cfilepath, int argc, char* argv[], Editor* editor,
|
||||
// Cmdline_noglow = 1;
|
||||
Cmdline_window = 1;
|
||||
|
||||
// These have to be in place before gr_init() bakes them into GPU resources, which is well
|
||||
// before EditorViewport (and its copy of the settings) exists.
|
||||
const GraphicsSettings graphicsSettings = GraphicsSettings::applyBeforeGrInit();
|
||||
|
||||
std::unique_ptr<QtGraphicsOperations> graphicsOps(new QtGraphicsOperations(editor));
|
||||
gr_init(std::move(graphicsOps));
|
||||
gr_set_gamma(3.0f);
|
||||
|
||||
// The rest needs a live renderer. EditorViewport re-applies these once it exists, but the
|
||||
// startup screens render before that.
|
||||
graphicsSettings.applyLive();
|
||||
|
||||
io::mouse::CursorManager::get()->showCursor(false);
|
||||
|
||||
|
||||
@@ -959,6 +959,7 @@ void FredView::syncViewOptions() {
|
||||
|
||||
connectActionToViewSetting(ui->actionLighting_from_Suns, &_viewport->view.Lighting_on);
|
||||
connectActionToViewSetting(ui->actionRender_Full_Detail, &_viewport->view.FullDetail);
|
||||
connectActionToViewSetting(ui->actionEnable_Post_Processing, &_viewport->view.Graphics.enablePostProcessing);
|
||||
|
||||
connectActionToViewSetting(ui->actionShowDistances, &_viewport->view.Show_distances);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -8,9 +8,54 @@
|
||||
#include "ui/util/SignalBlockers.h"
|
||||
#include "ui/widgets/sexp_tree_view.h"
|
||||
|
||||
#include <options/Option.h>
|
||||
#include <options/OptionsManager.h>
|
||||
|
||||
namespace fso::fred::dialogs {
|
||||
namespace {
|
||||
|
||||
// The engine already describes each of these settings once, with its own value list and display
|
||||
// names (Graphics.Shadows in shadows.cpp, Graphics.AAMode and Graphics.TextureFilter in 2d.cpp and
|
||||
// gropengltexture.cpp). Reading the labels back out of those definitions keeps this dialog from
|
||||
// carrying a second, silently-diverging copy of them.
|
||||
//
|
||||
// The values are enumerated in declaration order, which for these three is enum order, so a combo
|
||||
// index is the enum value. Returns empty if the option isn't registered, in which case the caller
|
||||
// leaves the combo alone rather than showing a half-populated list.
|
||||
SCP_vector<options::ValueDescription> engineOptionValues(const char* configKey)
|
||||
{
|
||||
for (const auto& option : options::OptionsManager::instance()->getOptions()) {
|
||||
if (option->getConfigKey() == configKey && option->getType() == options::OptionType::Selection) {
|
||||
return option->getValidValues();
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void populateFromEngineOption(QComboBox* combo, const char* configKey)
|
||||
{
|
||||
const auto values = engineOptionValues(configKey);
|
||||
|
||||
for (const auto& value : values) {
|
||||
combo->addItem(QString::fromStdString(value.display));
|
||||
}
|
||||
|
||||
// Nothing to choose from means the option isn't registered in this build (a renderer compiled
|
||||
// out, say). Grey the control out rather than leaving an empty combo that looks broken.
|
||||
combo->setEnabled(!values.empty());
|
||||
}
|
||||
|
||||
// The model stores graphics settings as one struct, so an edit is read-modify-write. Doing it this
|
||||
// way keeps every control's slot to the one line that actually differs.
|
||||
template <typename F>
|
||||
void editGraphics(PreferencesDialogModel* model, F&& mutate)
|
||||
{
|
||||
GraphicsSettings graphics = model->getGraphics();
|
||||
mutate(graphics);
|
||||
model->setGraphics(graphics);
|
||||
}
|
||||
|
||||
int themeModeToIndex(ThemeMode mode)
|
||||
{
|
||||
switch (mode) {
|
||||
@@ -95,6 +140,27 @@ void PreferencesDialog::applyChanges() {
|
||||
}
|
||||
|
||||
void PreferencesDialog::initializeUi() {
|
||||
// setupUi() has already run connectSlotsByName(), so filling a combo here would fire its
|
||||
// currentIndexChanged slot and mark the model modified before the user has touched anything.
|
||||
// (Items declared in the .ui file were added before the connections existed and so were safe.)
|
||||
util::SignalBlockers blockers(this);
|
||||
|
||||
populateFromEngineOption(ui->shadowQualityCombo, "Graphics.Shadows");
|
||||
populateFromEngineOption(ui->aaModeCombo, "Graphics.AAMode");
|
||||
populateFromEngineOption(ui->textureFilterCombo, "Graphics.TextureFilter");
|
||||
|
||||
// Anisotropy levels are hardware-dependent, so ask the engine for the same list its own
|
||||
// options screen offers. Empty means the hardware can't do it; leave the combo disabled.
|
||||
_anisotropyLevels = gr_get_supported_anisotropy_levels();
|
||||
for (float level : _anisotropyLevels) {
|
||||
ui->anisotropyCombo->addItem(level <= 1.0f ? tr("Off") : tr("%1x").arg(level, 0, 'g', 0));
|
||||
}
|
||||
ui->anisotropyCombo->setEnabled(!_anisotropyLevels.empty());
|
||||
|
||||
for (int samples : GraphicsSettings::validMsaaSampleCounts()) {
|
||||
ui->msaaCombo->addItem(samples == 0 ? tr("Off") : tr("%1x").arg(samples));
|
||||
}
|
||||
|
||||
// Build the controls key-binding form dynamically from the registered bindings
|
||||
auto* form = new QFormLayout(ui->controlsFormWidget);
|
||||
auto& bindings = ControlBindings::instance();
|
||||
@@ -128,6 +194,35 @@ void PreferencesDialog::updateUi() {
|
||||
ui->toolbarIconSizeCombo->setCurrentIndex(iconSize <= 16 ? 0 : iconSize >= 32 ? 2 : 1);
|
||||
ui->outlineLodCombo->setCurrentIndex(_model->getOutlineLod());
|
||||
ui->labelFontScaleSpin->setValue(_model->getLabelFontScale());
|
||||
|
||||
// Graphics
|
||||
const GraphicsSettings& graphics = _model->getGraphics();
|
||||
|
||||
ui->enablePostProcessing->setChecked(graphics.enablePostProcessing);
|
||||
ui->shadowQualityCombo->setCurrentIndex(static_cast<int>(graphics.shadowQuality));
|
||||
ui->aaModeCombo->setCurrentIndex(static_cast<int>(graphics.aaMode));
|
||||
ui->gammaSpin->setValue(graphics.gamma);
|
||||
|
||||
const auto msaaCounts = GraphicsSettings::validMsaaSampleCounts();
|
||||
ui->msaaCombo->setCurrentIndex(
|
||||
static_cast<int>(std::find(msaaCounts.begin(), msaaCounts.end(), graphics.msaaSamples) - msaaCounts.begin()));
|
||||
|
||||
// Both of these can be "not chosen", in which case the engine picks: trilinear, and the
|
||||
// hardware's maximum anisotropy. Show what will actually be used rather than a blank.
|
||||
ui->textureFilterCombo->setCurrentIndex(
|
||||
graphics.textureFilter == GraphicsSettings::NO_TEXTURE_FILTER_CHOICE ? 1 : graphics.textureFilter);
|
||||
|
||||
if (!_anisotropyLevels.empty()) {
|
||||
int index = static_cast<int>(_anisotropyLevels.size()) - 1;
|
||||
for (size_t i = 0; i < _anisotropyLevels.size(); ++i) {
|
||||
if (_anisotropyLevels[i] == graphics.anisotropy) {
|
||||
index = static_cast<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
ui->anisotropyCombo->setCurrentIndex(index);
|
||||
}
|
||||
|
||||
ui->showSexpHelpMissionEvents->setChecked(_model->getShowSexpHelpMissionEvents());
|
||||
ui->showSexpHelpMissionGoals->setChecked(_model->getShowSexpHelpMissionGoals());
|
||||
ui->showSexpHelpMissionCutscenes->setChecked(_model->getShowSexpHelpMissionCutscenes());
|
||||
@@ -210,6 +305,43 @@ void PreferencesDialog::on_themeCombo_currentIndexChanged(int index) {
|
||||
_model->setThemeMode(themeModeFromIndex(index));
|
||||
}
|
||||
|
||||
void PreferencesDialog::on_enablePostProcessing_toggled(bool checked) {
|
||||
editGraphics(_model.get(), [=](GraphicsSettings& g) { g.enablePostProcessing = checked; });
|
||||
}
|
||||
|
||||
void PreferencesDialog::on_shadowQualityCombo_currentIndexChanged(int index) {
|
||||
editGraphics(_model.get(), [=](GraphicsSettings& g) { g.shadowQuality = static_cast<ShadowQuality>(index); });
|
||||
}
|
||||
|
||||
void PreferencesDialog::on_aaModeCombo_currentIndexChanged(int index) {
|
||||
editGraphics(_model.get(), [=](GraphicsSettings& g) { g.aaMode = static_cast<AntiAliasMode>(index); });
|
||||
}
|
||||
|
||||
void PreferencesDialog::on_msaaCombo_currentIndexChanged(int index) {
|
||||
const auto counts = GraphicsSettings::validMsaaSampleCounts();
|
||||
if (index < 0 || static_cast<size_t>(index) >= counts.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
editGraphics(_model.get(), [=](GraphicsSettings& g) { g.msaaSamples = counts[index]; });
|
||||
}
|
||||
|
||||
void PreferencesDialog::on_textureFilterCombo_currentIndexChanged(int index) {
|
||||
editGraphics(_model.get(), [=](GraphicsSettings& g) { g.textureFilter = index; });
|
||||
}
|
||||
|
||||
void PreferencesDialog::on_anisotropyCombo_currentIndexChanged(int index) {
|
||||
if (index < 0 || static_cast<size_t>(index) >= _anisotropyLevels.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
editGraphics(_model.get(), [=](GraphicsSettings& g) { g.anisotropy = _anisotropyLevels[index]; });
|
||||
}
|
||||
|
||||
void PreferencesDialog::on_gammaSpin_valueChanged(double value) {
|
||||
editGraphics(_model.get(), [=](GraphicsSettings& g) { g.gamma = static_cast<float>(value); });
|
||||
}
|
||||
|
||||
void PreferencesDialog::on_dataMenuStyleCombo_currentIndexChanged(int index) {
|
||||
_model->setDataMenuStyle(static_cast<DataMenuStyle>(index));
|
||||
}
|
||||
|
||||
@@ -35,6 +35,14 @@ private slots:
|
||||
void on_outlineLodCombo_currentIndexChanged(int index);
|
||||
void on_labelFontScaleSpin_valueChanged(double value);
|
||||
void on_themeCombo_currentIndexChanged(int index);
|
||||
// Graphics
|
||||
void on_enablePostProcessing_toggled(bool checked);
|
||||
void on_shadowQualityCombo_currentIndexChanged(int index);
|
||||
void on_aaModeCombo_currentIndexChanged(int index);
|
||||
void on_msaaCombo_currentIndexChanged(int index);
|
||||
void on_textureFilterCombo_currentIndexChanged(int index);
|
||||
void on_anisotropyCombo_currentIndexChanged(int index);
|
||||
void on_gammaSpin_valueChanged(double value);
|
||||
void on_dataMenuStyleCombo_currentIndexChanged(int index);
|
||||
void on_classicMenuLayout_toggled(bool checked);
|
||||
void on_showSexpHelpMissionEvents_toggled(bool checked);
|
||||
@@ -66,6 +74,8 @@ private: // NOLINT(readability-redundant-access-specifiers)
|
||||
|
||||
std::unique_ptr<Ui::PreferencesDialog> ui;
|
||||
std::unique_ptr<PreferencesDialogModel> _model;
|
||||
//! Anisotropy levels backing the combo, in combo order. Queried once; hardware-dependent.
|
||||
SCP_vector<float> _anisotropyLevels;
|
||||
std::map<ControlAction, QKeySequenceEdit*> _controlEditors;
|
||||
FredView* _fredView = nullptr;
|
||||
EditorViewport* _viewport = nullptr;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -120,6 +120,8 @@
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionLighting_from_Suns"/>
|
||||
<addaction name="actionRender_Full_Detail"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionEnable_Post_Processing"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuSpeed">
|
||||
<property name="title">
|
||||
@@ -917,6 +919,17 @@
|
||||
<string>Render Full Detail</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionEnable_Post_Processing">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Enable Post Processing</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Render the 3D viewport through the game's HDR post-processing pipeline (bloom, tonemapping, lightshafts)</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionCamera">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
|
||||
@@ -399,6 +399,159 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="graphicsTab">
|
||||
<attribute name="title">
|
||||
<string>Graphics</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="graphicsLayout">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="enablePostProcessing">
|
||||
<property name="toolTip">
|
||||
<string>Route the 3D viewport through the game's HDR post-processing pipeline (bloom, tonemapping, lightshafts) instead of drawing straight to the screen. The other settings on this tab only have a visible effect while this is on.</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Enable post-processing in the viewport</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="shadowsAAGroup">
|
||||
<property name="title">
|
||||
<string>Shadows && Anti-Aliasing</string>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="shadowsAALayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="shadowQualityLabel">
|
||||
<property name="text">
|
||||
<string>Shadow quality:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="shadowQualityCombo">
|
||||
<property name="toolTip">
|
||||
<string>Quality of the shadows cast by the mission's sun in the viewport. Requires post-processing to be enabled. Restarting qtFRED is required for a change to take effect.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="aaModeLabel">
|
||||
<property name="text">
|
||||
<string>Anti-aliasing:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="aaModeCombo">
|
||||
<property name="toolTip">
|
||||
<string>Post-process anti-aliasing mode used in the viewport. Requires post-processing to be enabled.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="msaaLabel">
|
||||
<property name="text">
|
||||
<string>MSAA:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QComboBox" name="msaaCombo">
|
||||
<property name="toolTip">
|
||||
<string>Multisample anti-aliasing used for the viewport's 3D scene. Requires post-processing to be enabled. Restarting qtFRED is required for a change to take effect.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="texturesGroup">
|
||||
<property name="title">
|
||||
<string>Textures</string>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="texturesLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="textureFilterLabel">
|
||||
<property name="text">
|
||||
<string>Texture filtering:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="textureFilterCombo">
|
||||
<property name="toolTip">
|
||||
<string>Mipmap filtering used for textures. Restarting qtFRED is required for a change to take effect.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="anisotropyLabel">
|
||||
<property name="text">
|
||||
<string>Anisotropic filtering:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="anisotropyCombo">
|
||||
<property name="toolTip">
|
||||
<string>Anisotropic texture filtering level. Restarting qtFRED is required for a change to take effect.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="brightnessGroup">
|
||||
<property name="title">
|
||||
<string>Brightness</string>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="brightnessLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="gammaLabel">
|
||||
<property name="text">
|
||||
<string>Gamma:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QDoubleSpinBox" name="gammaSpin">
|
||||
<property name="toolTip">
|
||||
<string>Brightness of the viewport. qtFRED defaults this higher than the game since the viewport isn't normally tonemapped.</string>
|
||||
</property>
|
||||
<property name="decimals">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>0.10</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>5.00</double>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<double>0.05</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="graphicsSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="gridTab">
|
||||
<attribute name="title">
|
||||
<string>Grid</string>
|
||||
|
||||
@@ -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