diff --git a/src/mods/Graphics.cpp b/src/mods/Graphics.cpp index b3649aa24..d1e579d9c 100644 --- a/src/mods/Graphics.cpp +++ b/src/mods/Graphics.cpp @@ -69,13 +69,12 @@ void Graphics::on_draw_ui() { ImGui::SetNextItemOpen(true, ImGuiCond_::ImGuiCond_Once); if (ImGui::TreeNode("Ultrawide/FOV Options")) { if (m_ultrawide_fix->draw("Ultrawide/FOV/Aspect Ratio Fix") && m_ultrawide_fix->value() == false) { - set_vertical_fov(false); do_ultrawide_fov_restore(true); } if (m_ultrawide_fix->value()) { m_ultrawide_vertical_fov->draw("Ultrawide: Enable Vertical FOV"); - m_ultrawide_fov->draw("Ultrawide: Override FOV"); + m_ultrawide_custom_fov->draw("Ultrawide: Override FOV"); m_ultrawide_fov_multiplier->draw("Ultrawide: FOV Multiplier"); } @@ -360,20 +359,19 @@ void Graphics::do_ultrawide_fix() { return; } + set_ultrawide_fov(m_ultrawide_vertical_fov->value()); + #if defined(RE4) { std::shared_lock _{m_re4.time_mtx}; const auto now = std::chrono::steady_clock::now(); if (now - m_re4.last_inventory_open < std::chrono::milliseconds(100)) { - set_vertical_fov(false); return; } } #endif - set_vertical_fov(m_ultrawide_vertical_fov->value()); - static auto via_scene_view = sdk::find_type_definition("via.SceneView"); static auto set_display_type_method = via_scene_view->get_method("set_DisplayType"); @@ -383,6 +381,8 @@ void Graphics::do_ultrawide_fix() { return; } + // This disables any kind of pillarboxing and letterboxing. + // This cannot be directly restored once applied. if (set_display_type_method != nullptr) { set_display_type_method->call(sdk::get_thread_context(), main_view, via::DisplayType::Fit); } @@ -398,7 +398,7 @@ void Graphics::do_ultrawide_fov_restore(bool force) { return; } -#if defined(RE4) +#if defined(RE4) // Don't restore the FOV if we've just opened the inventory const auto now = std::chrono::steady_clock::now(); if (now - m_re4.last_inventory_open < std::chrono::milliseconds(100)) { return; @@ -407,27 +407,57 @@ void Graphics::do_ultrawide_fov_restore(bool force) { static auto via_camera = sdk::find_type_definition("via.Camera"); static auto set_fov_method = via_camera->get_method("set_FOV"); + static auto set_vertical_enable_method = via_camera->get_method("set_VerticalEnable"); - if (set_fov_method != nullptr && m_ultrawide_fov->value()) { - std::scoped_lock _{m_fov_mutex}; + std::scoped_lock _{m_fov_mutex}; + if (set_fov_method != nullptr) { for (auto it : m_fov_map) { auto camera = it.first; set_fov_method->call(sdk::get_thread_context(), camera, m_fov_map[camera]); utility::re_managed_object::release(camera); } - m_fov_map.clear(); } + + if (set_vertical_enable_method != nullptr) { + for (auto it : m_vertical_fov_map) { + auto camera = it.first; + set_vertical_enable_method->call(sdk::get_thread_context(), camera, m_vertical_fov_map[camera]); + utility::re_managed_object::release(camera); + } + m_vertical_fov_map.clear(); + } +} + +float hor_to_ver_fov(float fov, float aspect_ratio) { + return glm::degrees(2.f * glm::atan(glm::tan(glm::radians(fov) / 2.f) / aspect_ratio)); +} +float ver_to_hor_fov(float fov, float aspect_ratio) { + return glm::degrees(2.f * glm::atan(glm::tan(glm::radians(fov) / 2.f) * aspect_ratio)); } -void Graphics::set_vertical_fov(bool enable) { +void Graphics::set_ultrawide_fov(bool use_vertical_fov) { auto camera = sdk::get_primary_camera(); if (camera == nullptr) { return; } + bool allow_changing_fov = true; +#if defined(RE4) + // Never scale the FOV if the inventory just opened, otherwise it could make the inventory appear much smaller than it should. + // Unfortunately it doesn't scale right at 21:9 even in the unpatched game. + const auto now = std::chrono::steady_clock::now(); + if (now - m_re4.last_inventory_open < std::chrono::milliseconds(100)) { + allow_changing_fov = false; + use_vertical_fov = false; + // Clear the cached FOV values as they wouldn't be up to date anymore + std::scoped_lock _{m_fov_mutex}; + m_fov_map.clear(); + } +#endif + static auto via_camera = sdk::find_type_definition("via.Camera"); static auto get_vertical_enable_method = via_camera->get_method("get_VerticalEnable"); static auto set_vertical_enable_method = via_camera->get_method("set_VerticalEnable"); @@ -436,51 +466,97 @@ void Graphics::set_vertical_fov(bool enable) { static auto get_aspect_method = via_camera->get_method("get_AspectRatio"); bool was_vertical_fov_enabled = false; + bool is_vertical_fov_enabled = false; if (get_vertical_enable_method != nullptr) { was_vertical_fov_enabled = get_vertical_enable_method->call(sdk::get_thread_context(), camera); + + { + std::scoped_lock _{m_fov_mutex}; + + if (!m_vertical_fov_map.contains(camera)) { + m_vertical_fov_map[camera] = was_vertical_fov_enabled; + utility::re_managed_object::add_ref(camera); + } else { + m_vertical_fov_map[camera] = was_vertical_fov_enabled; + } + } } if (set_vertical_enable_method != nullptr) { - set_vertical_enable_method->call(sdk::get_thread_context(), camera, enable); + set_vertical_enable_method->call(sdk::get_thread_context(), camera, use_vertical_fov); + is_vertical_fov_enabled = use_vertical_fov; } - -#if defined(RE4) - const auto now = std::chrono::steady_clock::now(); - if (now - m_re4.last_inventory_open < std::chrono::milliseconds(100)) { + if (!allow_changing_fov || get_fov_method == nullptr || set_fov_method == nullptr) { return; } -#endif - if (m_ultrawide_fov->value() && get_fov_method != nullptr && set_fov_method != nullptr && get_aspect_method != nullptr) { - const auto fov = get_fov_method->call(sdk::get_thread_context(), camera); + // This is usually the horizontal FOV but it could also be vertical. + const auto fov = get_fov_method->call(sdk::get_thread_context(), camera); - { - std::scoped_lock _{m_fov_mutex}; + { + std::scoped_lock _{m_fov_mutex}; - if (!m_fov_map.contains(camera)) { - m_fov_map[camera] = fov; - utility::re_managed_object::add_ref(camera); - } else { - m_fov_map[camera] = fov; - } + if (!m_fov_map.contains(camera)) { + m_fov_map[camera] = fov; + utility::re_managed_object::add_ref(camera); + } else { + m_fov_map[camera] = fov; } + } - if (!was_vertical_fov_enabled) { - constexpr auto old_aspect_ratio = 16.0f / 9.0f; - // convert hfov to vfov in radians - const auto vfov_og = 2.0f * glm::atan(glm::tan(glm::radians(fov / 2.0f)) / old_aspect_ratio); - - const auto new_aspect_ratio = get_aspect_method->call(sdk::get_thread_context(), camera); - // convert vfov to hfov for new aspect ratio and convert to degrees - const auto new_hfov = glm::degrees(2.0f * glm::atan(glm::tan(vfov_og / 2.0f) * new_aspect_ratio)) * m_ultrawide_fov_multiplier->value(); + // Customize UW FOV with multiplier + if (m_ultrawide_custom_fov->value()) { + // Note: values a certain beyond a certain range might be rejected. + const auto scaled_fov = std::clamp(fov * m_ultrawide_fov_multiplier->value(), 1.f, 179.f); + set_fov_method->call(sdk::get_thread_context(), camera, scaled_fov); + } + // Automatically patch the FOV to make it look as it does at 16:9 (Hor+ scaling, thus fixed Ver FOV) + else { + constexpr float default_aspect_ratio = 16.f / 9.f; + // The threshold for letter boxing + constexpr float min_supported_aspect_ratio = default_aspect_ratio; + // The threshold for pillar boxing (or shifting to Ver- FOV) +#if defined(RE8) + constexpr float max_supported_aspect_ratio = 32.f / 9.f; +#elif defined(RE2) || defined(RE3) || defined(RE4) + // Even if most 21:9 resolutions actually have a higher aspect ratio than 2.333, that's actually what some games wrongfully use + constexpr float max_supported_aspect_ratio = 21.f / 9.f; +#else + constexpr float max_supported_aspect_ratio = default_aspect_ratio; +#endif - set_fov_method->call(sdk::get_thread_context(), camera, new_hfov); - } else { // No need to calculate new hfov if vertical fov is enabled by the engine itself. - const auto new_vfov = fov * m_ultrawide_fov_multiplier->value(); + float current_aspect_ratio = default_aspect_ratio; + float target_aspect_ratio = default_aspect_ratio; + // The backbuffer doesn't always represent the game internal aspect ratio, as it also accounts for black bars. + // For example, when set to borderless and using a game resolution different form the current monitor one, the black + // bars on the side will be accounted in it, which is why we use it to calculate the target aspect ratio. + if (m_backbuffer_size.has_value()) { + const float resolution_x = (float)(*m_backbuffer_size)[0]; + const float resolution_y = (float)(*m_backbuffer_size)[1]; + target_aspect_ratio = resolution_x / resolution_y; + } + // The camera aspect ratio represents the aspect ratio the game uses within the black bars + if (get_aspect_method) { + current_aspect_ratio = get_aspect_method->call(sdk::get_thread_context(), camera); + } + // Depending on the game, the FOV might have already automatically scaled up to "max_supported_aspect_ratio" by the time it reaches here + const float fov_aspect_ratio = std::clamp(current_aspect_ratio, min_supported_aspect_ratio, max_supported_aspect_ratio); - set_fov_method->call(sdk::get_thread_context(), camera, new_vfov); + if (was_vertical_fov_enabled) { + // Nothing to do, the FOV should already be correct under any aspect ratio in this case + } + else if (target_aspect_ratio >= min_supported_aspect_ratio) { + const auto vfov = hor_to_ver_fov(fov, fov_aspect_ratio); + const auto hfov_corrected = ver_to_hor_fov(vfov, target_aspect_ratio); + set_fov_method->call(sdk::get_thread_context(), camera, is_vertical_fov_enabled ? vfov : hfov_corrected); + } + // Account for the letter boxing and expand the vertical aspect ratio + else { + const auto vfov = hor_to_ver_fov(fov, target_aspect_ratio); + const auto hfov_corrected = ver_to_hor_fov(vfov, min_supported_aspect_ratio); + set_fov_method->call(sdk::get_thread_context(), camera, is_vertical_fov_enabled ? vfov : hfov_corrected); } } } diff --git a/src/mods/Graphics.hpp b/src/mods/Graphics.hpp index b4f2114cf..f0d07778c 100644 --- a/src/mods/Graphics.hpp +++ b/src/mods/Graphics.hpp @@ -32,19 +32,20 @@ class Graphics : public Mod { void do_scope_tweaks(sdk::renderer::layer::Scene* layer); void do_ultrawide_fix(); void do_ultrawide_fov_restore(bool force = false); - void set_vertical_fov(bool enable); + void set_ultrawide_fov(bool enable); std::recursive_mutex m_fov_mutex{}; std::unordered_map<::REManagedObject*, float> m_fov_map{}; + std::unordered_map<::REManagedObject*, bool> m_vertical_fov_map{}; struct { std::shared_mutex time_mtx{}; std::chrono::steady_clock::time_point last_inventory_open{}; } m_re4; - + const ModToggle::Ptr m_ultrawide_fix{ ModToggle::create(generate_name("UltrawideFix"), false) }; const ModToggle::Ptr m_ultrawide_vertical_fov{ ModToggle::create(generate_name("UltrawideFixVerticalFOV_V2"), false) }; - const ModToggle::Ptr m_ultrawide_fov{ ModToggle::create(generate_name("UltrawideFixFOV"), true) }; + const ModToggle::Ptr m_ultrawide_custom_fov{ModToggle::create(generate_name("UltrawideCustomFOV"), false)}; const ModSlider::Ptr m_ultrawide_fov_multiplier{ ModSlider::create(generate_name("UltrawideFOVMultiplier_V2"), 0.01f, 3.0f, 1.0f) }; const ModToggle::Ptr m_disable_gui{ ModToggle::create(generate_name("DisableGUI"), false) }; const ModToggle::Ptr m_force_render_res_to_window{ ModToggle::create(generate_name("ForceRenderResToWindow"), false) }; @@ -61,7 +62,7 @@ class Graphics : public Mod { ValueList m_options{ *m_ultrawide_fix, *m_ultrawide_vertical_fov, - *m_ultrawide_fov, + *m_ultrawide_custom_fov, *m_ultrawide_fov_multiplier, *m_disable_gui, *m_force_render_res_to_window,