Compare commits

..
Author SHA1 Message Date
the-e 76286c5332 Remove unused graphics settings from EditorViewport
Eliminate redundant graphics preference fields (post-processing, shadow quality, MSAA, texture filtering, anisotropy, gamma) that were initialized but unused in `EditorViewport`. Clean up associated setting writes for consistency.
2026-11-07 19:46:00 +01:00
the-eandClaude Opus 5 e4546de24e Update the design notes and help for the resize rework
documentation/qtfred-post-processing-viewport-resize.md still described the
Gr_min_render_target_w/h floor, which no longer exists, and listed
reallocate-on-resize as rejected -- which is now the implemented approach.

Rewrite it around gf_resize_render_targets, and keep the two failed attempts as
history, because the reason they failed is the reason the current one works.
That document had already identified the prerequisite correctly: the scene
teardown left FBOs attached to stale texture handles, and draws to an incomplete
FBO go nowhere, hence the black viewport. Completing the teardown was what made
reallocation viable. Also records the two properties that are easy to get wrong
(clamp to the hardware limit before deciding whether to resize, or a viewport
past that limit rebuilds every frame; never resize mid-frame), the sites fixed
in the deferred and Vulkan paths, and why the volumetric nebula pass is
deliberately left unscaled.

Corrections to the Preferences help page from the same review:

 - Enabling post-processing does not by itself produce shadows. Shadow quality
   defaults to Disabled and needs a restart to change, so the page said the
   opposite of what a user will experience.
 - The anisotropy control also greys out when the GPU reports a maximum of 2x,
   not only when it lacks the feature outright.

The help keyword index is left alone: keywords there are page-level, with
multiple entries only ever used as synonyms for one page, so per-setting
keywords would be inventing a convention rather than following one.

Finally, note in the qtfred module guide that its viewport calls
gr_screen_resize() every frame -- the assumption whose violation caused all of
the above -- and where the graphics preferences now live.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-11-07 19:46:00 +01:00
the-eandClaude Opus 5 336f198594 Document the Graphics preferences in the help that ships
The Graphics tab was documented in help-src/doc/dialogs/PreferencesDialog.html,
which is not listed in doc/qtfred.qhp and so is never compiled into
qtfred_help.qch. It is a stale duplicate of general/PreferencesDialog.html --
the page the table of contents, the Preferences keyword, and Viewport.html all
point at -- and one of only two orphaned files under help-src/doc. None of the
new documentation reached the Help viewer.

Document the tab on the page that actually ships, and revert the edit to the
orphan so nothing is left stranded there. The orphan itself predates this
branch and is left alone; it should probably be deleted, but that is a separate
question from this branch.

The content is also corrected against what the settings now do:

 - Shadow quality requires a restart. The orphan did not say so, and the
   dropdown entries no longer carry a "(restart required)" suffix now that
   their labels come from the engine's own option definition.
 - Texture filtering and anisotropy default to the engine's own choice
   (the config default, and the hardware maximum) until the user picks one.
 - Anisotropy is disabled outright on hardware that does not support it.
 - Gamma documents its actual range and default.
 - The page's "changes take effect immediately" claim is qualified, since it
   is not true of the four restart-only settings.

Also notes the View > Enable Post Processing menu equivalent, which had no
documentation anywhere -- no help page covers the View menu's display toggles.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-11-07 19:46:00 +01:00
the-eandClaude Opus 5 5c7059b451 Give qtFRED's graphics preferences a single owner
The Preferences > Graphics settings were read from QSettings in two places --
management.cpp before gr_init(), and EditorViewport once the editor exists --
each with its own copy of the key strings and default values, so renaming a key
in one would silently stop the other from working. The split between settings
that apply live and settings that need a restart was recorded only in comments
that had to stay in sync across five files.

Move both into GraphicsSettings, which owns the keys, the defaults, and the two
apply paths (applyLive() and applyBeforeGrInit()). ViewSettings holds one of
these instead of seven loose fields.

Behaviour fixes that fall out of having one reader:

 - Values are range-checked before being cast to ShadowQuality/AntiAliasMode
   and validated against the MSAA list, matching what the neighbouring
   DataMenuStyle load already did. A hand-edited settings file no longer
   produces an out-of-range enum.
 - The two OptionsManager overrides are now consistent. Texture filtering was
   overridden unconditionally with a hardcoded default while anisotropy was
   guarded, so a fresh install masked the engine's own texture-filter default
   for no reason. Both now use a sentinel for "the user has not chosen" and
   leave the engine option alone until there is a real choice.
 - gr_set_gamma(3.0f) is no longer duplicated as a literal next to the struct
   default.

The dialog's value lists now come from the engine option definitions
(Graphics.Shadows, Graphics.AAMode, Graphics.TextureFilter) instead of the
hardcoded .ui items that duplicated them, and anisotropy uses the shared
gr_get_supported_anisotropy_levels() rather than a near-verbatim copy of the
engine's enumerator. Adding an AA mode upstream no longer silently desyncs
qtFRED's dropdown. Note that the shadow-quality entries therefore lose the
"(restart required)" suffix they carried in the .ui; the control's tooltip and
the help page still say it.

Populating combos now blocks signals: setupUi() has already run
connectSlotsByName(), so filling them would otherwise fire the change slots and
mark the model modified before the user touched anything.

Finally, render_frame() gets its three correlated EnablePostProcessing branches
replaced by a scoped ScenePostProcessing guard, and the shadow pass -- with the
HTL matrix-stack dance it needs -- moves into its own named function.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-11-07 19:45:48 +01:00
the-eandClaude Opus 5 4c0ff8414d Grow the scene/post render targets when the viewport does
The offscreen targets backing the scene texture and post-processing were
allocated once from gr_screen at gr_init() and never revisited. Anything drawn
while gr_screen was larger got clipped to their edge and stretched back over
the viewport. The game hits this on the SDL window-resize path
(osapi.cpp calls gr_screen_resize()); qtFRED hits it constantly, since its 3D
viewport is a resizable dock widget.

Add gf_resize_render_targets, called from gr_screen_resize(), and implement it
for OpenGL: grow the targets to cover the new gr_screen, rebuilding only the
resolution-dependent resources. The post-processing table, the compiled
shaders and the SMAA lookup textures are resolution-independent and stay
alive, which is what makes this cheap enough to run off a window drag. This
mirrors what the Vulkan backend already does in VulkanPostProcessor::resize();
Vulkan leaves the hook unset because recreateSwapChain() owns it there.

The allocation only ever grows: gr_screen_resize() is called every frame by
qtFRED and repeatedly by the briefing map widget, so tracking the high-water
mark avoids thrashing, and the shrunk state is already handled by
Scene_texture_u_scale/v_scale. The size is also clamped to the hardware limit
up front, so a viewport past that limit stops asking to be resized instead of
rebuilding every frame. If the larger allocation fails outright -- most likely
precisely when growing -- the resize stops before rebuilding the
post-processing targets on top of scene textures that no longer exist.

This replaces Gr_min_render_target_w/h, which sized the targets for the
largest attached display up front -- on a 4K display at 2x scaling that was
over a gigabyte of VRAM, allocated at launch, for a feature that is off by
default and may never be switched on.

Deletions now go through GL_state.Texture.Delete(): a freed texture name the
state cache still holds would make a later Enable() of the recycled name a
silent no-op. This mattered little when teardown only ran at shutdown. While
there, the scene teardown now releases everything setup allocates -- it was
leaking Scene_ldr/composite/luminance/Cockpit_depth and all six MSAA targets --
and post-processing shutdown releases the SMAA lookup textures.

Separately, fix the sampling extents that the above makes reachable:

 - deferred-f.sdr turns gl_FragCoord into a G-buffer coordinate using
   invScreenWidth/Height, which described gr_screen rather than the G-buffer.
   Fixed in both backends; it is currently a no-op under Vulkan, where
   resize() keeps the extent equal to gr_screen, but states the requirement
   instead of relying on that.
 - the MSAA scene-colour copy, the MSAA resolve and the fog pass sampled the
   full [0,1] range of targets that are only filled to the u/v scale.
 - fxaa-v.sdr derived its texcoord from vertPosition, ignoring the
   sub-rectangle the draw call asked for.

Those extents now come from opengl_draw_full_screen_scene_texture() rather
than being open-coded, so a new pass cannot quietly reintroduce the bug. The
volumetric nebula pass is deliberately left unscaled and commented: it uses
fragTexCoord both to reconstruct a ray direction and to sample, which needs a
shader change to separate.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-11-07 19:45:48 +01:00
the-eandClaude Opus 5 019c4bacea Fix texture filtering silently defaulting to bilinear
opengl_tcache_init() read GL_mipmap_filter out of TextureFilteringOption
before seeding it from the legacy config key. That option's default_func
returns GL_mipmap_filter itself, so with no persisted "Graphics.TextureFilter"
value getValue() fell through to a still-zero-initialized global -- bilinear,
where the config default is trilinear. Since in-game options are on by default,
that affected every player who had never explicitly set the option, and it
ignored the legacy TextureFilter key existing installs were configured through.

Seed from config first, then let the option override, matching the order the
anisotropy setting below already uses (its default_func queries the hardware
directly, which is why it was never affected).

Also route the option's value enumerator through the new shared
gr_get_supported_anisotropy_levels() rather than a private copy.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-11-07 19:45:48 +01:00
the-e e7d58d85cb Add configurable graphics settings to Preferences dialog
Introduce post-processing, shadow quality, anti-aliasing, anisotropic filtering, texture filtering, MSAA, and gamma controls to qtFRED's Preferences. Ensures real-time application of certain settings and requires restart for others. Adjust graphics initialization to support baked settings before `gr_init()`. Update documentation to reflect these additions.
2026-11-07 19:45:48 +01:00
the-eandClaude Opus 5 84fa0a622f Size qtFRED's post-processing targets for its resizable viewport
The offscreen render targets that back post-processing -- the scene
textures and the post-processing surfaces -- are allocated once from
gr_screen.max_w/max_h at renderer init and never resized. The game never
notices, because its window size is fixed after gr_init(). qtFRED does:
FredRenderer::render_frame() calls gr_screen_resize() every frame to match
a dockable, resizable widget, so growing the viewport past the startup size
clipped the render to the old, smaller texture and then stretched it back
over the new, larger viewport.

Add Gr_min_render_target_w/h as a floor those allocations respect, and have
qtFRED set it to the largest size its viewport can reach (the biggest screen
the window could be maximized onto, in device pixels) before calling
gr_init(). Left at 0 for the game, which keeps sizing them purely from
gr_screen.

That makes Scene_texture_u_scale/v_scale meaningful outside the game for the
first time, which turns up two latent bugs:

  - Seven post-processing passes passed Scene_texture_u_scale for both axes.
    Harmless while the scene texture matches the screen exactly (u == v),
    wrong the moment it doesn't.
  - The bloom bright pass hardcoded 1.0/1.0. Unlike the passes after it, it
    reads the scene texture directly rather than an already-cropped
    intermediate, so it has to confine itself to the sub-rectangle that was
    actually rendered into.

Finally, expose the pipeline in qtFRED at all: a View menu toggle brackets
the 3D scene in gr_scene_texture_begin/end, the same way game_render_frame()
brackets its own. Off by default, and it covers only the 3D world content --
the 2D overlays stay outside it.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-11-07 19:45:48 +01:00
27 changed files with 1006 additions and 188 deletions

No files matched your search

+4 -1
View File
@@ -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
+30
View File
@@ -927,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;
@@ -1710,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)
+12
View File
@@ -917,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;
@@ -1502,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);
+2 -1
View File
@@ -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
+18 -5
View File
@@ -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()
@@ -328,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;
{
@@ -564,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);
@@ -660,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);
+165 -93
View File
@@ -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,
+13 -1
View File
@@ -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();
+78 -60
View File
@@ -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
*
+8 -23
View File
@@ -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;
}
@@ -507,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) {
+10
View File
@@ -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 &rsaquo; 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 &rsaquo; 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 &amp; 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
+2
View File
@@ -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
+6
View File
@@ -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
View File
@@ -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();
};
+50
View File
@@ -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();
}
+132
View File
@@ -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
+81
View File
@@ -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
@@ -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;
+10 -1
View File
@@ -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);
+1
View File
@@ -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);
+132
View File
@@ -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));
}
+10
View File
@@ -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;
+13
View File
@@ -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>
+153
View File
@@ -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 &amp;&amp; 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>