diff --git a/code/def_files/data/effects/fxaa-v.sdr b/code/def_files/data/effects/fxaa-v.sdr
index 22a9053f2a1..1b4e4d255c5 100644
--- a/code/def_files/data/effects/fxaa-v.sdr
+++ b/code/def_files/data/effects/fxaa-v.sdr
@@ -8,6 +8,7 @@ void main() {
}
#else
in vec4 vertPosition;
+in vec4 vertTexCoord;
out vec2 v_rcpFrame;
noperspective out vec2 v_pos;
@@ -20,6 +21,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
diff --git a/code/graphics/2d.cpp b/code/graphics/2d.cpp
index 4dba605070f..30edc07c8d9 100644
--- a/code/graphics/2d.cpp
+++ b/code/graphics/2d.cpp
@@ -859,6 +859,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 gr_get_supported_anisotropy_levels()
+{
+ float max;
+ if (!gr_get_property(gr_property::MAX_ANISOTROPY, &max)) {
+ return {};
+ }
+
+ if (max <= 2.0f) {
+ return {};
+ }
+
+ SCP_vector 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;
@@ -1647,6 +1670,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)
diff --git a/code/graphics/2d.h b/code/graphics/2d.h
index 71f1f3663ea..c98ff258bd2 100644
--- a/code/graphics/2d.h
+++ b/code/graphics/2d.h
@@ -917,6 +917,12 @@ typedef struct screen {
std::function gf_scene_texture_end;
std::function 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 gf_resize_render_targets;
+
std::function gf_zbias;
std::function gf_set_fill_mode;
@@ -1494,6 +1500,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 gr_get_supported_anisotropy_levels();
+
inline void gr_push_debug_group(const char* name)
{
gr_screen.gf_push_debug_group(name);
diff --git a/code/graphics/opengl/gropengl.cpp b/code/graphics/opengl/gropengl.cpp
index 0bd86bdd539..413c6638829 100644
--- a/code/graphics/opengl/gropengl.cpp
+++ b/code/graphics/opengl/gropengl.cpp
@@ -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;
@@ -1514,7 +1515,7 @@ bool gr_opengl_init(std::unique_ptr&& 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
diff --git a/code/graphics/opengl/gropengldeferred.cpp b/code/graphics/opengl/gropengldeferred.cpp
index 5f97cb8cd99..f306a9451e5 100644
--- a/code/graphics/opengl/gropengldeferred.cpp
+++ b/code/graphics/opengl/gropengldeferred.cpp
@@ -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()
@@ -325,8 +327,12 @@ void gr_opengl_deferred_lighting_finish()
shadow_cascade_params_bind(offset, count);
}
- 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;
{
@@ -561,7 +567,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);
@@ -657,6 +664,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);
diff --git a/code/graphics/opengl/gropengldraw.cpp b/code/graphics/opengl/gropengldraw.cpp
index 63ac62ffe6b..ea32c2950b7 100644
--- a/code/graphics/opengl/gropengldraw.cpp
+++ b/code/graphics/opengl/gropengldraw.cpp
@@ -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,77 +704,103 @@ void opengl_scene_texture_shutdown()
return;
}
- if ( Scene_color_texture ) {
- glDeleteTextures(1, &Scene_color_texture);
- Scene_color_texture = 0;
- }
-
- if ( Scene_position_texture ) {
- glDeleteTextures(1, &Scene_position_texture);
- Scene_position_texture = 0;
- }
-
- if ( Scene_normal_texture ) {
- glDeleteTextures(1, &Scene_normal_texture);
- Scene_normal_texture = 0;
- }
-
- if ( Scene_specular_texture ) {
- glDeleteTextures(1, &Scene_specular_texture);
- Scene_specular_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);
+
+ 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);
+
+ opengl_delete_render_texture(Back_texture);
+ opengl_delete_render_texture(Back_depth_texture);
+ opengl_delete_render_framebuffer(Back_framebuffer);
+
+ opengl_delete_render_texture(GammaBlit_texture);
+ opengl_delete_render_framebuffer(GammaBlit_framebuffer);
+
+ opengl_delete_render_texture(Distortion_texture[0]);
+ opengl_delete_render_texture(Distortion_texture[1]);
+ opengl_delete_render_framebuffer(Distortion_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;
- }
+ Scene_texture_initialized = 0;
+ Scene_framebuffer_in_frame = false;
+}
- if (Back_framebuffer) {
- glDeleteFramebuffers(1, &Back_framebuffer);
- Back_framebuffer = 0;
+void gr_opengl_resize_render_targets()
+{
+ // Nothing allocated yet (still inside gr_init()), or FBOs are unavailable entirely.
+ if ( !Scene_texture_initialized ) {
+ return;
}
- if (GammaBlit_texture) {
- glDeleteTextures(1, &GammaBlit_texture);
- GammaBlit_texture = 0;
+ // 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;
}
- if (GammaBlit_framebuffer) {
- glDeleteFramebuffers(1, &GammaBlit_framebuffer);
- GammaBlit_framebuffer = 0;
+ // 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;
}
- glDeleteTextures(2, Distortion_texture);
- Distortion_texture[0] = 0;
- Distortion_texture[1] = 0;
-
- if ( Distortion_framebuffer ) {
- glDeleteFramebuffers(1, &Distortion_framebuffer);
- Distortion_framebuffer = 0;
+ 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;
}
- Scene_texture_initialized = 0;
- Scene_framebuffer_in_frame = false;
+ opengl_post_resize_render_targets();
}
void gr_opengl_scene_texture_begin()
@@ -776,20 +819,35 @@ void gr_opengl_scene_texture_begin()
GL_state.PushFramebufferState();
GL_state.BindFrameBuffer(Scene_framebuffer);
- if (GL_rendering_to_texture)
- {
- Scene_texture_u_scale = i2fl(gr_screen.max_w) / i2fl(Scene_texture_width);
- Scene_texture_v_scale = i2fl(gr_screen.max_h) / i2fl(Scene_texture_height);
-
- CLAMP(Scene_texture_u_scale, 0.0f, 1.0f);
- CLAMP(Scene_texture_v_scale, 0.0f, 1.0f);
- }
- else
- {
- Scene_texture_u_scale = 1.0f;
- Scene_texture_v_scale = 1.0f;
+ // The fraction of the scene textures this frame actually renders into. Normally 1.0 -- the
+ // targets are grown to cover gr_screen (gr_opengl_resize_render_targets()) -- but they are
+ // never shrunk back, so a viewport that got smaller leaves the rest of the allocation stale.
+ // Every pass that samples these textures has to stay inside this sub-rectangle; use
+ // opengl_draw_full_screen_scene_texture() rather than open-coding the extents.
+ Scene_texture_u_scale = i2fl(gr_screen.max_w) / i2fl(Scene_texture_width);
+ Scene_texture_v_scale = i2fl(gr_screen.max_h) / i2fl(Scene_texture_height);
+
+ // Above 1.0 means the viewport outgrew the allocation and the resize could not keep up -- only
+ // reachable when GL_max_renderbuffer_size capped the targets. Render what fits and let the
+ // blit stretch it; 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);
@@ -1236,6 +1294,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,
diff --git a/code/graphics/opengl/gropengldraw.h b/code/graphics/opengl/gropengldraw.h
index 3cfd3183c80..5bfa170b341 100644
--- a/code/graphics/opengl/gropengldraw.h
+++ b/code/graphics/opengl/gropengldraw.h
@@ -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);
void gr_opengl_scene_texture_begin();
void gr_opengl_scene_texture_end();
void gr_opengl_copy_effect_texture();
@@ -147,6 +153,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();
diff --git a/code/graphics/opengl/gropenglpostprocessing.cpp b/code/graphics/opengl/gropenglpostprocessing.cpp
index ed9e14a995c..76dc1324709 100644
--- a/code/graphics/opengl/gropenglpostprocessing.cpp
+++ b/code/graphics/opengl/gropenglpostprocessing.cpp
@@ -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.BindFrameBuffer(0);
+ GL_state.PopFramebufferState();
- rval = true;
+ GL_state.BindFrameBuffer(0);
+}
- if ( opengl_check_for_errors("post_init_framebuffer()") ) {
- rval = false;
- }
+void opengl_post_process_shutdown_bloom()
+{
+ opengl_delete_render_texture(Bloom_textures[0]);
+ opengl_delete_render_texture(Bloom_textures[1]);
+ opengl_delete_render_framebuffer(Bloom_framebuffer);
+}
- return rval;
+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;
+ }
+ opengl_post_shutdown_render_targets();
+ opengl_post_setup_render_targets();
+}
-void opengl_post_process_shutdown_bloom()
+// generate and test the framebuffer and textures that we are going to use
+static bool opengl_post_init_framebuffer()
{
- if ( Bloom_textures[0] ) {
- glDeleteTextures(1, &Bloom_textures[0]);
- Bloom_textures[0] = 0;
- }
+ setup_smaa_lookup_textures();
- if ( Bloom_textures[1] ) {
- glDeleteTextures(1, &Bloom_textures[1]);
- Bloom_textures[1] = 0;
- }
+ opengl_post_setup_render_targets();
- if ( Bloom_framebuffer > 0 ) {
- glDeleteFramebuffers(1, &Bloom_framebuffer);
- Bloom_framebuffer = 0;
- }
+ return !opengl_check_for_errors("post_init_framebuffer()");
}
void opengl_post_process_init()
@@ -1141,20 +1163,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;
diff --git a/code/graphics/opengl/gropenglpostprocessing.h b/code/graphics/opengl/gropenglpostprocessing.h
index d820222bb89..cdba2579d88 100644
--- a/code/graphics/opengl/gropenglpostprocessing.h
+++ b/code/graphics/opengl/gropenglpostprocessing.h
@@ -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();
+
void gr_opengl_post_process_set_effect(const char *name, int x, const vec3d *rgb);
void gr_opengl_post_process_set_defaults();
void gr_opengl_post_process_save_zbuffer();
diff --git a/code/graphics/opengl/gropengltexture.cpp b/code/graphics/opengl/gropengltexture.cpp
index d0711ecc192..63bdbe5090b 100644
--- a/code/graphics/opengl/gropengltexture.cpp
+++ b/code/graphics/opengl/gropengltexture.cpp
@@ -90,28 +90,6 @@ static auto TextureFilteringOption __UNUSED = options::OptionBuilder("Graph
.parser(parse_texture_filtering_func)
.finish();
-static SCP_vector anisotropic_value_enumerator()
-{
- float max;
- if (!gr_get_property(gr_property::MAX_ANISOTROPY, &max)) {
- return SCP_vector();
- }
-
- if (max <= 2.0f) {
- return SCP_vector();
- }
-
- SCP_vector 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) {
@@ -135,7 +113,7 @@ static float anisotropic_default()
static auto AnisotropyOption = options::OptionBuilder("Graphics.Anisotropy",
std::pair{"Anistropic filtering", 1736},
std::pair{"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)
@@ -190,8 +168,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;
}
diff --git a/code/graphics/vulkan/VulkanPostProcessingLighting.cpp b/code/graphics/vulkan/VulkanPostProcessingLighting.cpp
index 1e1fb3fc4fc..90517eb30d9 100644
--- a/code/graphics/vulkan/VulkanPostProcessingLighting.cpp
+++ b/code/graphics/vulkan/VulkanPostProcessingLighting.cpp
@@ -494,8 +494,12 @@ void VulkanDeferredLighting::render(vk::CommandBuffer cmd)
{
auto* header = reinterpret_cast(uboMapped);
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(m_ctx->sceneExtent.width);
+ header->invScreenHeight = 1.0f / static_cast(m_ctx->sceneExtent.height);
header->nearPlane = gr_near_plane;
if (m_shadow->isInitialized() && Shadow_quality != ShadowQuality::Disabled) {
diff --git a/qtfred/AGENTS.md b/qtfred/AGENTS.md
new file mode 100644
index 00000000000..c54ef11c87a
--- /dev/null
+++ b/qtfred/AGENTS.md
@@ -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`.
diff --git a/qtfred/help-src/doc/general/PreferencesDialog.html b/qtfred/help-src/doc/general/PreferencesDialog.html
index 8a8ca2e3e84..cb4bfca04ba 100644
--- a/qtfred/help-src/doc/general/PreferencesDialog.html
+++ b/qtfred/help-src/doc/general/PreferencesDialog.html
@@ -10,7 +10,9 @@
Preferences
Opens via File › Preferences.
Controls QtFRED editor settings. Changes take effect immediately; there is no
-Apply button.
+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.
General
Miscellaneous editor options. Hover over each setting for a tooltip
@@ -30,6 +32,50 @@
Autosave
being asked.
+Graphics
+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.
+
+Post-processing
+Enable post-processing in the viewport 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
+View › Enable Post Processing.
+Every other setting in this section only changes what you see while
+post-processing is on, because they are all consumed by that pipeline.
+
+Shadows & anti-aliasing
+
+ - Shadow quality - detail level of the shadows cast by the
+ mission's sun. Higher settings cost more performance. Shadows are off
+ until you raise this above Disabled, so enabling post-processing
+ alone will not produce any. Requires a restart - including the
+ first time you turn it on.
+ - Anti-aliasing - post-process anti-aliasing mode (FXAA or
+ SMAA, in increasing quality). Applies immediately.
+ - MSAA - multisample anti-aliasing for the 3D scene, off
+ or 4x/8x. More expensive than the post-process modes above but higher
+ quality. Requires a restart.
+
+
+Textures
+
+ - Texture filtering - bilinear or trilinear mipmap
+ filtering. Left alone, QtFRED uses the same default the game does.
+ Requires a restart.
+ - Anisotropic filtering - 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. Requires a restart.
+
+
+Gamma
+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.
+
Grid
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
diff --git a/qtfred/source_groups.cmake b/qtfred/source_groups.cmake
index 6718b68a21a..2bd6c19312c 100644
--- a/qtfred/source_groups.cmake
+++ b/qtfred/source_groups.cmake
@@ -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
diff --git a/qtfred/src/mission/EditorViewport.cpp b/qtfred/src/mission/EditorViewport.cpp
index e91fcb7d67f..bc289f83833 100644
--- a/qtfred/src/mission/EditorViewport.cpp
+++ b/qtfred/src/mission/EditorViewport.cpp
@@ -122,6 +122,7 @@ EditorViewport::EditorViewport(Editor* in_editor, std::unique_ptr&
syncMissionLayerNames();
loadSettings();
+ view.Graphics.applyLive();
fredApp->runAfterInit([this]() { initialSetup(); });
}
@@ -180,6 +181,8 @@ void EditorViewport::loadSettings() {
camera.setInvertOrbitX(settings.value("camera_invert_orbit_x", camera.getInvertOrbitX()).toBool());
camera.setInvertOrbitY(settings.value("camera_invert_orbit_y", camera.getInvertOrbitY()).toBool());
settings.endGroup();
+
+ view.Graphics = GraphicsSettings::load();
}
void EditorViewport::saveSettings() const {
@@ -228,7 +231,10 @@ void EditorViewport::saveSettings() const {
settings.setValue("camera_invert_orbit_x", camera.getInvertOrbitX());
settings.setValue("camera_invert_orbit_y", camera.getInvertOrbitY());
settings.endGroup();
+
+ view.Graphics.save();
}
+
void EditorViewport::needsUpdate() {
_renderer->scheduleUpdate();
}
diff --git a/qtfred/src/mission/EditorViewport.h b/qtfred/src/mission/EditorViewport.h
index 26962561525..4004f97f6b7 100644
--- a/qtfred/src/mission/EditorViewport.h
+++ b/qtfred/src/mission/EditorViewport.h
@@ -4,6 +4,7 @@
#include "CameraController.h"
#include "FredRenderer.h"
#include "Editor.h"
+#include "GraphicsSettings.h"
#include "IDialogProvider.h"
#include "ui/ThemeMode.h"
@@ -63,6 +64,9 @@ struct ViewSettings {
bool Highlight_selectable_subsys = false;
int Outline_lod = 1;
+ //! Preferences > Graphics. Owns its own persistence and apply rules; see GraphicsSettings.
+ GraphicsSettings Graphics;
+
ViewSettings();
};
diff --git a/qtfred/src/mission/FredRenderer.cpp b/qtfred/src/mission/FredRenderer.cpp
index 5bd82493288..9ee0517f3a2 100644
--- a/qtfred/src/mission/FredRenderer.cpp
+++ b/qtfred/src/mission/FredRenderer.cpp
@@ -15,6 +15,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -28,6 +29,8 @@
#include
#include
+#include
+
#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;
@@ -994,6 +1029,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 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;
@@ -1003,6 +1047,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();
@@ -1019,6 +1067,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();
}
diff --git a/qtfred/src/mission/GraphicsSettings.cpp b/qtfred/src/mission/GraphicsSettings.cpp
new file mode 100644
index 00000000000..560c8f3b6dc
--- /dev/null
+++ b/qtfred/src/mission/GraphicsSettings.cpp
@@ -0,0 +1,132 @@
+#include "mission/GraphicsSettings.h"
+
+#include
+#include
+
+#include
+
+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
+T readEnum(const QSettings& settings, const char* key, T fallback, T highest)
+{
+ const int raw = settings.value(key, static_cast(fallback)).toInt();
+
+ if (raw < 0 || raw > static_cast(highest)) {
+ return fallback;
+ }
+
+ return static_cast(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 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(shadowQuality));
+ settings.setValue(KEY_AA_MODE, static_cast(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));
+ }
+
+ if (settings.anisotropy != NO_ANISOTROPY_CHOICE) {
+ options->setOverride("Graphics.Anisotropy", std::to_string(settings.anisotropy));
+ }
+
+ return settings;
+}
+
+} // namespace fso::fred
diff --git a/qtfred/src/mission/GraphicsSettings.h b/qtfred/src/mission/GraphicsSettings.h
new file mode 100644
index 00000000000..8e85a368360
--- /dev/null
+++ b/qtfred/src/mission/GraphicsSettings.h
@@ -0,0 +1,81 @@
+#pragma once
+
+#include
+#include
+
+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 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
diff --git a/qtfred/src/mission/dialogs/PreferencesDialogModel.cpp b/qtfred/src/mission/dialogs/PreferencesDialogModel.cpp
index c674f26744e..57681573958 100644
--- a/qtfred/src/mission/dialogs/PreferencesDialogModel.cpp
+++ b/qtfred/src/mission/dialogs/PreferencesDialogModel.cpp
@@ -26,6 +26,7 @@ PreferencesDialogModel::PreferencesDialogModel(QObject* parent, EditorViewport*
, _dataMenuStyle(viewport->Data_menu_style)
, _toolbarIconSize(viewport->toolbar_icon_size)
, _outlineLod(viewport->view.Outline_lod)
+ , _graphics(viewport->view.Graphics)
, _invertOrbitX(viewport->camera.getInvertOrbitX())
, _invertOrbitY(viewport->camera.getInvertOrbitY())
, _gridCenterX(static_cast(viewport->The_grid->center.xyz.x))
@@ -68,6 +69,9 @@ bool PreferencesDialogModel::apply() {
_viewport->Data_menu_style = _dataMenuStyle;
_viewport->toolbar_icon_size = _toolbarIconSize;
_viewport->view.Outline_lod = _outlineLod;
+ _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);
@@ -180,6 +184,9 @@ void PreferencesDialogModel::setToolbarIconSize(int size) { modify(_toolbarIconS
int PreferencesDialogModel::getOutlineLod() const { return _outlineLod; }
void PreferencesDialogModel::setOutlineLod(int value) { modify(_outlineLod, 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!");
diff --git a/qtfred/src/mission/dialogs/PreferencesDialogModel.h b/qtfred/src/mission/dialogs/PreferencesDialogModel.h
index 9fcc5b50884..0d12bb0dcfe 100644
--- a/qtfred/src/mission/dialogs/PreferencesDialogModel.h
+++ b/qtfred/src/mission/dialogs/PreferencesDialogModel.h
@@ -1,5 +1,6 @@
#pragma once
+#include "mission/GraphicsSettings.h"
#include "mission/dialogs/AbstractDialogModel.h"
#include "ui/ControlBindings.h"
#include "ui/ThemeMode.h"
@@ -67,6 +68,11 @@ class PreferencesDialogModel : public AbstractDialogModel {
int getOutlineLod() const;
void setOutlineLod(int 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);
@@ -109,6 +115,8 @@ class PreferencesDialogModel : public AbstractDialogModel {
int _toolbarIconSize;
int _outlineLod;
+ GraphicsSettings _graphics;
+
// Controls
std::map _controlKeys;
bool _invertOrbitX;
diff --git a/qtfred/src/mission/management.cpp b/qtfred/src/mission/management.cpp
index cc8cf8efb8d..db32e78246d 100644
--- a/qtfred/src/mission/management.cpp
+++ b/qtfred/src/mission/management.cpp
@@ -3,6 +3,8 @@
#include "object.h"
+#include "mission/GraphicsSettings.h"
+
#include "cmdline/cmdline.h"
#include
@@ -125,9 +127,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 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);
diff --git a/qtfred/src/ui/FredView.cpp b/qtfred/src/ui/FredView.cpp
index 7c6aae8f482..4bbe97d6e41 100644
--- a/qtfred/src/ui/FredView.cpp
+++ b/qtfred/src/ui/FredView.cpp
@@ -942,6 +942,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);
diff --git a/qtfred/src/ui/dialogs/PreferencesDialog.cpp b/qtfred/src/ui/dialogs/PreferencesDialog.cpp
index 07db9e06d92..0067bb2b90c 100644
--- a/qtfred/src/ui/dialogs/PreferencesDialog.cpp
+++ b/qtfred/src/ui/dialogs/PreferencesDialog.cpp
@@ -8,9 +8,54 @@
#include "ui/util/SignalBlockers.h"
#include "ui/widgets/sexp_tree_view.h"
+#include
+#include
+
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 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
+void editGraphics(PreferencesDialogModel* model, F&& mutate)
+{
+ GraphicsSettings graphics = model->getGraphics();
+ mutate(graphics);
+ model->setGraphics(graphics);
+}
+
int themeModeToIndex(ThemeMode mode)
{
switch (mode) {
@@ -94,6 +139,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();
@@ -125,6 +191,35 @@ void PreferencesDialog::updateUi() {
const int iconSize = _model->getToolbarIconSize();
ui->toolbarIconSizeCombo->setCurrentIndex(iconSize <= 16 ? 0 : iconSize >= 32 ? 2 : 1);
ui->outlineLodCombo->setCurrentIndex(_model->getOutlineLod());
+
+ // Graphics
+ const GraphicsSettings& graphics = _model->getGraphics();
+
+ ui->enablePostProcessing->setChecked(graphics.enablePostProcessing);
+ ui->shadowQualityCombo->setCurrentIndex(static_cast(graphics.shadowQuality));
+ ui->aaModeCombo->setCurrentIndex(static_cast(graphics.aaMode));
+ ui->gammaSpin->setValue(graphics.gamma);
+
+ const auto msaaCounts = GraphicsSettings::validMsaaSampleCounts();
+ ui->msaaCombo->setCurrentIndex(
+ static_cast(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(_anisotropyLevels.size()) - 1;
+ for (size_t i = 0; i < _anisotropyLevels.size(); ++i) {
+ if (_anisotropyLevels[i] == graphics.anisotropy) {
+ index = static_cast(i);
+ break;
+ }
+ }
+ ui->anisotropyCombo->setCurrentIndex(index);
+ }
+
ui->showSexpHelpMissionEvents->setChecked(_model->getShowSexpHelpMissionEvents());
ui->showSexpHelpMissionGoals->setChecked(_model->getShowSexpHelpMissionGoals());
ui->showSexpHelpMissionCutscenes->setChecked(_model->getShowSexpHelpMissionCutscenes());
@@ -202,6 +297,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(index); });
+}
+
+void PreferencesDialog::on_aaModeCombo_currentIndexChanged(int index) {
+ editGraphics(_model.get(), [=](GraphicsSettings& g) { g.aaMode = static_cast(index); });
+}
+
+void PreferencesDialog::on_msaaCombo_currentIndexChanged(int index) {
+ const auto counts = GraphicsSettings::validMsaaSampleCounts();
+ if (index < 0 || static_cast(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(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(value); });
+}
+
void PreferencesDialog::on_dataMenuStyleCombo_currentIndexChanged(int index) {
_model->setDataMenuStyle(static_cast(index));
}
diff --git a/qtfred/src/ui/dialogs/PreferencesDialog.h b/qtfred/src/ui/dialogs/PreferencesDialog.h
index e4e8266866e..a4ed07e2614 100644
--- a/qtfred/src/ui/dialogs/PreferencesDialog.h
+++ b/qtfred/src/ui/dialogs/PreferencesDialog.h
@@ -34,6 +34,14 @@ private slots:
void on_toolbarIconSizeCombo_currentIndexChanged(int index);
void on_outlineLodCombo_currentIndexChanged(int index);
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_showSexpHelpMissionEvents_toggled(bool checked);
void on_showSexpHelpMissionGoals_toggled(bool checked);
@@ -63,6 +71,8 @@ private slots:
std::unique_ptr ui;
std::unique_ptr _model;
+ //! Anisotropy levels backing the combo, in combo order. Queried once; hardware-dependent.
+ SCP_vector _anisotropyLevels;
std::map _controlEditors;
FredView* _fredView = nullptr;
EditorViewport* _viewport = nullptr;
diff --git a/qtfred/ui/FredView.ui b/qtfred/ui/FredView.ui
index 7d8cdc3e035..dd9d82c97b9 100644
--- a/qtfred/ui/FredView.ui
+++ b/qtfred/ui/FredView.ui
@@ -120,6 +120,8 @@
+
+
+
+
+ Graphics
+
+
+ -
+
+
+ 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.
+
+
+ Enable post-processing in the viewport
+
+
+
+ -
+
+
+ Shadows && Anti-Aliasing
+
+
+
-
+
+
+ Shadow quality:
+
+
+
+ -
+
+
+ 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.
+
+
+
+ -
+
+
+ Anti-aliasing:
+
+
+
+ -
+
+
+ Post-process anti-aliasing mode used in the viewport. Requires post-processing to be enabled.
+
+
+
+ -
+
+
+ MSAA:
+
+
+
+ -
+
+
+ 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.
+
+
+
+
+
+
+ -
+
+
+ Textures
+
+
+
-
+
+
+ Texture filtering:
+
+
+
+ -
+
+
+ Mipmap filtering used for textures. Restarting qtFRED is required for a change to take effect.
+
+
+
+ -
+
+
+ Anisotropic filtering:
+
+
+
+ -
+
+
+ Anisotropic texture filtering level. Restarting qtFRED is required for a change to take effect.
+
+
+
+
+
+
+ -
+
+
+ Brightness
+
+
+
-
+
+
+ Gamma:
+
+
+
+ -
+
+
+ Brightness of the viewport. qtFRED defaults this higher than the game since the viewport isn't normally tonemapped.
+
+
+ 2
+
+
+ 0.10
+
+
+ 5.00
+
+
+ 0.05
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
Grid