From 1d81357e773e088d545cbd4a4cf6efc74b3e2566 Mon Sep 17 00:00:00 2001 From: Nils Schimmelmann Date: Thu, 26 Mar 2026 17:01:13 -0500 Subject: [PATCH 1/2] refactor UboManager with lazy UBO management and subdata updates Adds UboBlocks.h with NamedColorsBlock definition and UboManager with lazy UBO rebuild pattern. Uses glBufferSubData on a buffer to update the block. --- src/display/mapcanvas_gl.cpp | 5 +- src/global/NamedColors.cpp | 15 +- src/global/NamedColors.h | 6 +- src/opengl/UboBlocks.h | 73 ++++++++++ src/opengl/UboManager.h | 145 ++++++++++++++++++-- src/opengl/legacy/AbstractShaderProgram.cpp | 2 + src/opengl/legacy/AbstractShaderProgram.h | 3 + src/opengl/legacy/Legacy.h | 50 ++----- 8 files changed, 231 insertions(+), 68 deletions(-) create mode 100644 src/opengl/UboBlocks.h diff --git a/src/display/mapcanvas_gl.cpp b/src/display/mapcanvas_gl.cpp index c8158e3bf..104a6b777 100644 --- a/src/display/mapcanvas_gl.cpp +++ b/src/display/mapcanvas_gl.cpp @@ -241,9 +241,8 @@ void MapCanvas::initializeGL() .registerRebuildFunction(Legacy::SharedVboEnum::NamedColorsBlock, [](Legacy::Functions &funcs) { auto &uboManager = funcs.getUboManager(); - uboManager.update(funcs, - Legacy::SharedVboEnum::NamedColorsBlock, - XNamedColor::getAllColorsAsVec4()); + uboManager.update( + funcs, XNamedColor::getAllColorsAsBlock()); }); updateMultisampling(); diff --git a/src/global/NamedColors.cpp b/src/global/NamedColors.cpp index 8cd093e0f..8bee0a8f3 100644 --- a/src/global/NamedColors.cpp +++ b/src/global/NamedColors.cpp @@ -3,6 +3,7 @@ #include "NamedColors.h" +#include "../opengl/UboBlocks.h" #include "EnumIndexedArray.h" #include @@ -17,13 +18,12 @@ struct NODISCARD GlobalData final using InitArray = EnumIndexedArray; using Map = std::map; using NamesVector = std::vector; - using Vec4Vector = std::vector; private: Map m_map; NamesVector m_names; ColorVector m_colors; - Vec4Vector m_vec4s; + Legacy::NamedColorsBlock m_colorsBlock; InitArray m_initialized; private: @@ -33,7 +33,6 @@ struct NODISCARD GlobalData final GlobalData() { m_colors.resize(NUM_NAMED_COLORS); - m_vec4s.resize(MAX_NAMED_COLORS); m_names.resize(NUM_NAMED_COLORS); static const auto white = Colors::white; @@ -46,7 +45,7 @@ struct NODISCARD GlobalData final const auto color = (id == NamedColorEnum::TRANSPARENT) ? transparent_black : white; m_colors[idx] = color; - m_vec4s[idx] = color.getVec4(); + m_colorsBlock.colors[idx] = color.getVec4(); m_names[idx] = name; m_map.emplace(name, id); }; @@ -73,7 +72,7 @@ struct NODISCARD GlobalData final const auto idx = getIndex(id); m_colors.at(idx) = c; - m_vec4s.at(idx) = c.getVec4(); + m_colorsBlock.colors.at(idx) = c.getVec4(); m_initialized.at(id) = true; return true; } @@ -86,7 +85,7 @@ struct NODISCARD GlobalData final NODISCARD const std::vector &getAllNames() const { return m_names; } NODISCARD const std::vector &getAllColors() const { return m_colors; } - NODISCARD const std::vector &getAllVec4s() const { return m_vec4s; } + NODISCARD const Legacy::NamedColorsBlock &getAllColorsAsBlock() const { return m_colorsBlock; } public: NODISCARD std::optional lookup(std::string_view name) const @@ -146,7 +145,7 @@ const std::vector &XNamedColor::getAllColors() return getGlobalData().getAllColors(); } -const std::vector &XNamedColor::getAllColorsAsVec4() +const Legacy::NamedColorsBlock &XNamedColor::getAllColorsAsBlock() { - return getGlobalData().getAllVec4s(); + return getGlobalData().getAllColorsAsBlock(); } diff --git a/src/global/NamedColors.h b/src/global/NamedColors.h index 7a7c5733e..860860771 100644 --- a/src/global/NamedColors.h +++ b/src/global/NamedColors.h @@ -9,6 +9,10 @@ #include #include +namespace Legacy { +struct NamedColorsBlock; +} + #undef TRANSPARENT // Bad dog, Microsoft; bad dog!!! // X(_id, _name) @@ -101,6 +105,6 @@ class NODISCARD XNamedColor final public: NODISCARD static const std::vector &getAllColors(); - NODISCARD static const std::vector &getAllColorsAsVec4(); + NODISCARD static const Legacy::NamedColorsBlock &getAllColorsAsBlock(); NODISCARD static const std::vector &getAllNames(); }; diff --git a/src/opengl/UboBlocks.h b/src/opengl/UboBlocks.h new file mode 100644 index 000000000..894066612 --- /dev/null +++ b/src/opengl/UboBlocks.h @@ -0,0 +1,73 @@ +#pragma once +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2026 The MMapper Authors + +#include "../global/NamedColors.h" +#include "../global/utils.h" + +#include +#include +#include +#include +#include + +#include + +namespace Legacy { + +// X(EnumName, GL_String_Name) +/** + * Note: SharedVboEnum values are implicitly used as UBO binding indices. + * They must be 0-based and contiguous. + */ +#define XFOREACH_SHARED_VBO(X) X(NamedColorsBlock, "NamedColorsBlock") + +#define X_ENUM(element, name) element, +enum class NODISCARD SharedVboEnum : uint8_t { XFOREACH_SHARED_VBO(X_ENUM) NUM_BLOCKS }; +#undef X_ENUM + +static constexpr size_t NUM_SHARED_VBOS = static_cast(SharedVboEnum::NUM_BLOCKS); + +inline constexpr const char *const SharedVboNames[] = { +#define X_NAME(EnumName, StringName) StringName, + XFOREACH_SHARED_VBO(X_NAME) +#undef X_NAME +}; + +/** + * @brief Memory layout for the NamedColors uniform block. + * Must match NamedColorsBlock in shaders (std140 layout). + */ +struct NODISCARD NamedColorsBlock final +{ + std::array colors{}; +}; + +template +struct BlockType; + +#define X_TYPE(EnumName, StringName) \ + template<> \ + struct BlockType \ + { \ + using type = EnumName; \ + }; +XFOREACH_SHARED_VBO(X_TYPE) +#undef X_TYPE + +#define X_ASSERT(EnumName, StringName) \ + static_assert(std::is_standard_layout_v, \ + "UBO block " #EnumName " must have standard layout for offset calculations"); \ + static_assert(std::is_trivially_copyable_v, \ + "UBO block " #EnumName " must be trivially copyable for buffer uploads"); +XFOREACH_SHARED_VBO(X_ASSERT) +#undef X_ASSERT + +template +auto MakeSharedVboBlocksHelper(std::index_sequence) + -> std::tuple(Is)>::type...>; + +using SharedVboBlocks = decltype(MakeSharedVboBlocksHelper( + std::make_index_sequence{})); + +} // namespace Legacy diff --git a/src/opengl/UboManager.h b/src/opengl/UboManager.h index f67e20be0..3cdcabaaf 100644 --- a/src/opengl/UboManager.h +++ b/src/opengl/UboManager.h @@ -6,6 +6,7 @@ #include "../global/RuleOf5.h" #include "../global/logging.h" #include "../global/utils.h" +#include "UboBlocks.h" #include "legacy/Legacy.h" #include "legacy/VBO.h" @@ -13,6 +14,7 @@ #include #include #include +#include #include #include @@ -36,6 +38,24 @@ class UboManager final DELETE_CTORS_AND_ASSIGN_OPS(UboManager); public: + /** + * @brief Accesses the CPU-side shadow copy of a UBO block by its enum. + */ + template + typename Legacy::BlockType::type &get() + { + return std::get::type>(m_shadowBlocks); + } + + /** + * @brief Accesses the CPU-side shadow copy of a UBO block by its enum (const). + */ + template + const typename Legacy::BlockType::type &get() const + { + return std::get::type>(m_shadowBlocks); + } + /** * @brief Marks a UBO block as dirty by resetting its bound state. */ @@ -51,17 +71,33 @@ class UboManager final /** * @brief Registers a function that can rebuild the UBO data. + * + * @param block UBO block identifier. + * @param func Function used to rebuild the UBO data. + * @param allowOverwrite If false (default), overwriting an existing rebuild function + * will trigger a debug assertion. Set to true only when an + * overwrite is intentional. */ - void registerRebuildFunction(Legacy::SharedVboEnum block, RebuildFunction func) + void registerRebuildFunction(Legacy::SharedVboEnum block, + RebuildFunction func, + bool allowOverwrite = false) { if (m_rebuildFunctions[block]) { - MMLOG_WARNING() << "UboManager::registerRebuildFunction: overwriting existing " - "rebuild function for UBO block " - << static_cast(block); + assert(allowOverwrite + && "UboManager::registerRebuildFunction: overwriting existing " + "rebuild function for UBO block"); } m_rebuildFunctions[block] = std::move(func); } + /** + * @brief Unregisters a rebuild function for a UBO block. + */ + void unregisterRebuildFunction(Legacy::SharedVboEnum block) + { + m_rebuildFunctions[block] = nullptr; + } + /** * @brief Checks if a UBO block is currently dirty/invalid. */ @@ -78,20 +114,26 @@ class UboManager final } const auto &func = m_rebuildFunctions[block]; - assert(func && "UBO block is invalid and no rebuild function is registered"); - if (func) { - func(gl); + if (!func) { + const char *name = Legacy::Functions::getUniformBlockName(block); + MMLOG_ERROR() << "UboManager::updateIfInvalid: UBO block '" << name + << "' is invalid and no rebuild function is registered"; + throw std::runtime_error("UBO block '" + std::string(name) + + "' is invalid and no rebuild function is registered"); + } - if (const auto bound = m_boundBuffers[block]) { - return *bound; - } + func(gl); - MMLOG_ERROR() << "UboManager::updateIfInvalid: rebuild function failed to call " - "update() for block " - << static_cast(block); - assert(false && "Rebuild function must call update()"); + if (const auto bound = m_boundBuffers[block]) { + return *bound; } - return 0; + + const char *name = Legacy::Functions::getUniformBlockName(block); + MMLOG_ERROR() << "UboManager::updateIfInvalid: rebuild function failed to call " + "update() for block '" + << name << "'"; + throw std::runtime_error("Rebuild function for block '" + std::string(name) + + "' failed to call update()"); } /** @@ -125,6 +167,76 @@ class UboManager final return bind_internal(gl, block, vbo.get()); } + /** + * @brief Type-safe upload to a UBO. + * Enforces the correct data structure for the given block identifier. + * Also updates the shadow copy. + */ + template + GLuint update(Legacy::Functions &gl, const typename Legacy::BlockType::type &data) + { + get() = data; + return update(gl, Block, data); + } + + /** + * @brief Syncs the entire shadow copy of a block to the GPU. + */ + template + GLuint sync(Legacy::Functions &gl) + { + return update(gl, Block, get()); + } + + /** + * @brief Syncs multiple specific fields of a block to the GPU in a single bind. + * @param gl Legacy functions. + * @param members Pointers to the members in the block struct. + */ + template + void syncFields(Legacy::Functions &gl, Us T::*...members) + { + using BlockType = typename Legacy::BlockType::type; + static_assert(std::is_same_v, "Members must belong to the correct block type"); + static_assert(std::is_standard_layout_v, + "Block type must have standard layout for offset calculation"); + + // If the block has never been fully uploaded, fall back to a full sync. + // glBufferSubData requires pre-allocated GPU storage from a prior glBufferData call. + if (isInvalid(Block)) { + sync(gl); + return; + } + + const auto &blockData = get(); + Legacy::VBO &vbo = getOrCreateVbo(gl, Block); + gl.glBindBuffer(GL_UNIFORM_BUFFER, vbo.get()); + + (gl.glBufferSubData(GL_UNIFORM_BUFFER, + static_cast( + reinterpret_cast(&(blockData.*members)) + - reinterpret_cast(&blockData)), + static_cast(sizeof(Us)), + &(blockData.*members)), + ...); + + gl.glBindBuffer(GL_UNIFORM_BUFFER, 0); + + // Ensure it's bound to the correct point. + bind_internal(gl, Block, vbo.get()); + } + + /** + * @brief Syncs a specific field of a block to the GPU. + * @param gl Legacy functions. + * @param member Pointer to the member in the block struct. + */ + template + void syncField(Legacy::Functions &gl, U T::*member) + { + syncFields(gl, member); + } + /** * @brief Binds the UBO to its assigned point. * If invalid and a rebuild function is registered, it will be updated first. @@ -183,6 +295,9 @@ class UboManager final private: EnumIndexedArray m_rebuildFunctions; EnumIndexedArray, Legacy::SharedVboEnum> m_boundBuffers; + + // Tuple of all block types for shadow storage. + Legacy::SharedVboBlocks m_shadowBlocks; }; } // namespace Legacy diff --git a/src/opengl/legacy/AbstractShaderProgram.cpp b/src/opengl/legacy/AbstractShaderProgram.cpp index 8e25dad98..1adecd1c5 100644 --- a/src/opengl/legacy/AbstractShaderProgram.cpp +++ b/src/opengl/legacy/AbstractShaderProgram.cpp @@ -5,6 +5,8 @@ #include "../../global/ConfigConsts.h" +#include + namespace Legacy { AbstractShaderProgram::AbstractShaderProgram(std::string dirName, diff --git a/src/opengl/legacy/AbstractShaderProgram.h b/src/opengl/legacy/AbstractShaderProgram.h index c5c8b880e..7765d047e 100644 --- a/src/opengl/legacy/AbstractShaderProgram.h +++ b/src/opengl/legacy/AbstractShaderProgram.h @@ -5,6 +5,9 @@ #include "Legacy.h" #include "VBO.h" +#include +#include + namespace Legacy { static constexpr GLuint INVALID_ATTRIB_LOCATION = ~0u; diff --git a/src/opengl/legacy/Legacy.h b/src/opengl/legacy/Legacy.h index eb15014d6..9cd19bf63 100644 --- a/src/opengl/legacy/Legacy.h +++ b/src/opengl/legacy/Legacy.h @@ -7,6 +7,7 @@ #include "../../global/utils.h" #include "../OpenGLConfig.h" #include "../OpenGLTypes.h" +#include "../UboBlocks.h" #include "FBO.h" #include @@ -25,35 +26,18 @@ class OpenGL; namespace Legacy { -class UboManager; class StaticVbos; class SharedVbos; class SharedVaos; +class UboManager; +class VBO; class VAO; struct AbstractShaderProgram; struct ShaderPrograms; struct PointSizeBinder; -// X(EnumName, GL_String_Name) -/** - * Note: SharedVboEnum values are implicitly used as UBO binding indices. - * They must be 0-based and contiguous. - */ -#define XFOREACH_SHARED_VBO(X) X(NamedColorsBlock, "NamedColorsBlock") - -#define X_COUNT_VBO(element, name) +1 -static constexpr size_t NUM_SHARED_VBOS = 0 XFOREACH_SHARED_VBO(X_COUNT_VBO); -#undef X_COUNT_VBO - -enum class SharedVboEnum : uint8_t { -#define X_ENUM(element, name) element, - XFOREACH_SHARED_VBO(X_ENUM) -#undef X_ENUM - NUM_BLOCKS -}; - -static_assert(NUM_SHARED_VBOS > 0, "At least one shared VBO must be defined"); -static_assert(static_cast(SharedVboEnum::NUM_BLOCKS) == NUM_SHARED_VBOS, +static_assert(Legacy::NUM_SHARED_VBOS > 0, "At least one shared VBO must be defined"); +static_assert(static_cast(Legacy::SharedVboEnum::NUM_BLOCKS) == Legacy::NUM_SHARED_VBOS, "SharedVboEnum must be 0-based and contiguous"); /** @@ -189,25 +173,12 @@ class NODISCARD Functions : protected QOpenGLExtraFunctions, using Base::glAttachShader; using Base::glBindBuffer; using Base::glBindBufferBase; - - /** - * @brief Binds a buffer to a uniform block binding point. - * @param target Must be GL_UNIFORM_BUFFER. - * @param block The uniform block to bind to. - * @param buffer The buffer ID. - * - * Note: This uses the enum value as the fixed binding point. - */ - void glBindBufferBase(const GLenum target, const SharedVboEnum block, const GLuint buffer) - { - assert(target == GL_UNIFORM_BUFFER); - Base::glBindBufferBase(target, getUboBindingIndex(block), buffer); - } using Base::glBindTexture; using Base::glBindVertexArray; using Base::glBlendFunc; using Base::glBlendFuncSeparate; using Base::glBufferData; + using Base::glBufferSubData; using Base::glClear; using Base::glClearColor; using Base::glCompileShader; @@ -263,10 +234,7 @@ class NODISCARD Functions : protected QOpenGLExtraFunctions, * * Note: This uses the enum value as the fixed binding point. */ - void glUniformBlockBinding(const GLuint program, const SharedVboEnum block) - { - virt_glUniformBlockBinding(program, block); - } + void glUniformBlockBinding(const GLuint program, const SharedVboEnum block); /** * @brief Automatically assigns fixed binding points to all known uniform blocks. @@ -356,16 +324,16 @@ class NODISCARD Functions : protected QOpenGLExtraFunctions, NODISCARD virtual const char *virt_getShaderVersion() const = 0; virtual void virt_glUniformBlockBinding(GLuint program, SharedVboEnum block); -protected: +public: NODISCARD static const char *getUniformBlockName(SharedVboEnum block); -public: /// platform-specific (ES vs GL) NODISCARD bool canRenderQuads() { return virt_canRenderQuads(); } /// platform-specific (ES vs GL) NODISCARD std::optional toGLenum(DrawModeEnum mode) { return virt_toGLenum(mode); } +protected: private: template static void enforceTriviallyCopyable() From 659fb4ae2d3d893a5a1aa03adf9db4f7e9fc5bfd Mon Sep 17 00:00:00 2001 From: Nils Schimmelmann Date: Thu, 26 Mar 2026 17:01:34 -0500 Subject: [PATCH 2/2] add weather system with particle simulation, atmosphere, and time-of-day Adds GLWeather with CameraBlock, TimeBlock, and WeatherBlock UBO support. Includes particle simulation via transform feedback, atmospheric overlay, and time-of-day color transitions driven by MUME weather state. Exposes weather intensity controls in the graphics preferences UI. --- src/CMakeLists.txt | 6 + src/configuration/configuration.cpp | 15 + src/configuration/configuration.h | 6 +- src/display/FrameManager.cpp | 38 +- src/display/FrameManager.h | 14 +- src/display/MapCanvasData.cpp | 36 +- src/display/MapCanvasData.h | 26 +- src/display/ProjectionUtils.cpp | 1 - src/display/ProjectionUtils.h | 1 + src/display/Textures.cpp | 78 ++- src/display/Textures.h | 10 +- src/display/mapcanvas.cpp | 9 +- src/display/mapcanvas.h | 15 +- src/display/mapcanvas_gl.cpp | 36 +- src/display/mapwindow.cpp | 8 +- src/display/mapwindow.h | 7 +- src/global/NamedColors.cpp | 10 + src/global/NamedColors.h | 6 +- src/mainwindow/mainwindow.cpp | 9 +- src/opengl/OpenGL.h | 2 + src/opengl/OpenGLTypes.h | 26 +- src/opengl/UboBlocks.h | 37 +- src/opengl/Weather.cpp | 460 ++++++++++++++++++ src/opengl/Weather.h | 123 +++++ src/opengl/legacy/Binders.cpp | 9 + src/opengl/legacy/Binders.h | 3 + src/opengl/legacy/Legacy.cpp | 2 + src/opengl/legacy/Legacy.h | 13 + src/opengl/legacy/ShaderUtils.cpp | 56 ++- src/opengl/legacy/ShaderUtils.h | 5 + src/opengl/legacy/Shaders.cpp | 65 ++- src/opengl/legacy/Shaders.h | 62 +++ src/opengl/legacy/TFO.cpp | 32 ++ src/opengl/legacy/TFO.h | 36 ++ src/opengl/legacy/WeatherMeshes.cpp | 218 +++++++++ src/opengl/legacy/WeatherMeshes.h | 93 ++++ src/preferences/AdvancedGraphics.cpp | 17 +- src/preferences/graphicspage.cpp | 19 + src/preferences/graphicspage.ui | 60 +++ src/resources/mmapper2.qrc | 8 + .../legacy/weather/atmosphere/frag.glsl | 119 +++++ .../legacy/weather/atmosphere/vert.glsl | 25 + .../shaders/legacy/weather/particle/frag.glsl | 75 +++ .../shaders/legacy/weather/particle/vert.glsl | 73 +++ .../legacy/weather/simulation/frag.glsl | 9 + .../legacy/weather/simulation/vert.glsl | 104 ++++ .../legacy/weather/timeofday/frag.glsl | 44 ++ .../legacy/weather/timeofday/vert.glsl | 8 + 48 files changed, 2070 insertions(+), 64 deletions(-) create mode 100644 src/opengl/Weather.cpp create mode 100644 src/opengl/Weather.h create mode 100644 src/opengl/legacy/TFO.cpp create mode 100644 src/opengl/legacy/TFO.h create mode 100644 src/opengl/legacy/WeatherMeshes.cpp create mode 100644 src/opengl/legacy/WeatherMeshes.h create mode 100644 src/resources/shaders/legacy/weather/atmosphere/frag.glsl create mode 100644 src/resources/shaders/legacy/weather/atmosphere/vert.glsl create mode 100644 src/resources/shaders/legacy/weather/particle/frag.glsl create mode 100644 src/resources/shaders/legacy/weather/particle/vert.glsl create mode 100644 src/resources/shaders/legacy/weather/simulation/frag.glsl create mode 100644 src/resources/shaders/legacy/weather/simulation/vert.glsl create mode 100644 src/resources/shaders/legacy/weather/timeofday/frag.glsl create mode 100644 src/resources/shaders/legacy/weather/timeofday/vert.glsl diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0107cef2c..453ca0a4e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -416,6 +416,8 @@ set(mmapper_SRCS opengl/OpenGL.cpp opengl/OpenGL.h opengl/UboManager.h + opengl/Weather.cpp + opengl/Weather.h opengl/OpenGLConfig.cpp opengl/OpenGLConfig.h opengl/OpenGLTypes.cpp @@ -444,10 +446,14 @@ set(mmapper_SRCS opengl/legacy/Shaders.h opengl/legacy/SimpleMesh.cpp opengl/legacy/SimpleMesh.h + opengl/legacy/TFO.cpp + opengl/legacy/TFO.h opengl/legacy/VAO.cpp opengl/legacy/VAO.h opengl/legacy/VBO.cpp opengl/legacy/VBO.h + opengl/legacy/WeatherMeshes.cpp + opengl/legacy/WeatherMeshes.h parser/Abbrev.cpp parser/Abbrev.h parser/AbstractParser-Actions.cpp diff --git a/src/configuration/configuration.cpp b/src/configuration/configuration.cpp index 7e3599dfb..5e4dfc28c 100644 --- a/src/configuration/configuration.cpp +++ b/src/configuration/configuration.cpp @@ -298,6 +298,9 @@ ConstString KEY_THEME = "Theme"; ConstString KEY_TLS_ENCRYPTION = "TLS encryption"; ConstString KEY_USE_INTERNAL_EDITOR = "Use internal editor"; ConstString KEY_USE_TRILINEAR_FILTERING = "Use trilinear filtering"; +ConstString KEY_WEATHER_ATMOSPHERE_INTENSITY = "weather.atmosphereIntensity"; +ConstString KEY_WEATHER_PRECIPITATION_INTENSITY = "weather.precipitationIntensity"; +ConstString KEY_WEATHER_TIME_OF_DAY_INTENSITY = "weather.todIntensity"; ConstString KEY_WINDOW_GEOMETRY = "Window Geometry"; ConstString KEY_WINDOW_STATE = "Window State"; ConstString KEY_BELL_AUDIBLE = "Bell audible"; @@ -662,6 +665,14 @@ void Configuration::CanvasSettings::read(const QSettings &conf) advanced.verticalAngle.set(conf.value(KEY_3D_VERTICAL_ANGLE, 450).toInt()); advanced.horizontalAngle.set(conf.value(KEY_3D_HORIZONTAL_ANGLE, 0).toInt()); advanced.layerHeight.set(conf.value(KEY_3D_LAYER_HEIGHT, 15).toInt()); + + weatherAtmosphereIntensity.set(conf.value(KEY_WEATHER_ATMOSPHERE_INTENSITY, 50).toInt()); + weatherPrecipitationIntensity.set(conf.value(KEY_WEATHER_PRECIPITATION_INTENSITY, 50).toInt()); + weatherTimeOfDayIntensity.set(conf.value(KEY_WEATHER_TIME_OF_DAY_INTENSITY, 50).toInt()); + + weatherAtmosphereIntensity.clamp(0, 100); + weatherPrecipitationIntensity.clamp(0, 100); + weatherTimeOfDayIntensity.clamp(0, 100); } void Configuration::AccountSettings::read(const QSettings &conf) @@ -859,6 +870,10 @@ void Configuration::CanvasSettings::write(QSettings &conf) const conf.setValue(KEY_3D_VERTICAL_ANGLE, advanced.verticalAngle.get()); conf.setValue(KEY_3D_HORIZONTAL_ANGLE, advanced.horizontalAngle.get()); conf.setValue(KEY_3D_LAYER_HEIGHT, advanced.layerHeight.get()); + + conf.setValue(KEY_WEATHER_ATMOSPHERE_INTENSITY, weatherAtmosphereIntensity.get()); + conf.setValue(KEY_WEATHER_PRECIPITATION_INTENSITY, weatherPrecipitationIntensity.get()); + conf.setValue(KEY_WEATHER_TIME_OF_DAY_INTENSITY, weatherTimeOfDayIntensity.get()); } void Configuration::AccountSettings::write(QSettings &conf) const diff --git a/src/configuration/configuration.h b/src/configuration/configuration.h index bd535c640..6f174fd0a 100644 --- a/src/configuration/configuration.h +++ b/src/configuration/configuration.h @@ -184,12 +184,16 @@ class NODISCARD Configuration final MMapper::Array mapRadius{100, 100, 100}; + NamedConfig weatherAtmosphereIntensity{"WEATHER_ATMOSPHERE_INTENSITY", 50}; + NamedConfig weatherPrecipitationIntensity{"WEATHER_PRECIPITATION_INTENSITY", 50}; + NamedConfig weatherTimeOfDayIntensity{"WEATHER_TIME_OF_DAY_INTENSITY", 50}; + struct NODISCARD Advanced final { NamedConfig use3D{"MMAPPER_3D", true}; NamedConfig autoTilt{"MMAPPER_AUTO_TILT", true}; NamedConfig printPerfStats{"MMAPPER_GL_PERFSTATS", IS_DEBUG_BUILD}; - FixedPoint<0> maximumFps{4, 300, 60}; + FixedPoint<0> maximumFps{4, 240, 60}; // 5..90 degrees FixedPoint<1> fov{50, 900, 765}; diff --git a/src/display/FrameManager.cpp b/src/display/FrameManager.cpp index fb7874fe5..7c1733b3c 100644 --- a/src/display/FrameManager.cpp +++ b/src/display/FrameManager.cpp @@ -9,10 +9,16 @@ #include -FrameManager::FrameManager(QOpenGLWindow &window, QObject *parent) +FrameManager::FrameManager(QOpenGLWindow &window, Legacy::UboManager &uboManager, QObject *parent) : QObject(parent) , m_window(window) + , m_uboManager(uboManager) { + m_uboManager.registerRebuildFunction(Legacy::SharedVboEnum::TimeBlock, + [this](Legacy::Functions &gl) { + m_uboManager.sync(gl); + }); + updateMinFrameTime(); setConfig().canvas.advanced.maximumFps.registerChangeCallback(m_configLifetime, [this]() { updateMinFrameTime(); @@ -25,6 +31,11 @@ FrameManager::FrameManager(QOpenGLWindow &window, QObject *parent) requestFrame(); } +FrameManager::~FrameManager() +{ + m_uboManager.unregisterRebuildFunction(Legacy::SharedVboEnum::TimeBlock); +} + void FrameManager::registerCallback(const Signal2Lifetime &lifetime, AnimationCallback callback) { m_callbacks.push_back({lifetime.getObj(), std::move(callback)}); @@ -105,10 +116,35 @@ std::optional FrameManager::beginFrame() } m_dirty = false; + // Calculate delta time + float deltaTime = 0.0f; + if (hasLastUpdate) { + const auto elapsed = now - m_lastUpdateTime; + deltaTime = std::chrono::duration(elapsed).count(); + } m_lastUpdateTime = now; + + // Cap deltaTime for simulation to match map movement during dragging and avoid quantization jitter. + // Cap at 1.0s to avoid huge jumps after window focus loss or lag, while supporting low FPS. + auto lastFrameDeltaTime = std::min(deltaTime, 1.0f); + + // Refresh internal struct for UBO + m_elapsedTime += lastFrameDeltaTime; + auto &timeBlock = m_uboManager.get(); + timeBlock.time = glm::vec4(m_elapsedTime, lastFrameDeltaTime, 0.0f, 0.0f); + + if (lastFrameDeltaTime > 0.0f) { + m_uboManager.invalidate(Legacy::SharedVboEnum::TimeBlock); + } + return Frame(*this); } +float FrameManager::getElapsedTime() const +{ + return m_elapsedTime; +} + void FrameManager::onHeartbeat() { if (!needsHeartbeat()) { diff --git a/src/display/FrameManager.h b/src/display/FrameManager.h index 1dbb49149..b318db3f8 100644 --- a/src/display/FrameManager.h +++ b/src/display/FrameManager.h @@ -5,6 +5,7 @@ #include "../global/RAII.h" #include "../global/RuleOf5.h" #include "../global/Signal2.h" +#include "../opengl/UboManager.h" #include #include @@ -60,8 +61,8 @@ class FrameManager final : public QObject QTimer m_heartbeatTimer; QOpenGLWindow &m_window; bool m_dirty = true; - - friend class TestFrameManager; + float m_elapsedTime = 0.0f; + Legacy::UboManager &m_uboManager; public: /** @@ -86,9 +87,15 @@ class FrameManager final : public QObject }; public: - explicit FrameManager(QOpenGLWindow &window, QObject *parent = nullptr); + explicit FrameManager(QOpenGLWindow &window, + Legacy::UboManager &uboManager, + QObject *parent = nullptr); + ~FrameManager() override; DELETE_CTORS_AND_ASSIGN_OPS(FrameManager); +public: + NODISCARD float getElapsedTime() const; + public: /** * @brief Register a callback that will be executed once per heartbeat. @@ -122,7 +129,6 @@ class FrameManager final : public QObject void recordFramePainted(); void updateMinFrameTime(); void cleanupExpiredCallbacks(); - NODISCARD std::chrono::nanoseconds getJitterTolerance() const; NODISCARD std::chrono::nanoseconds getTimeUntilNextFrame() const; private slots: diff --git a/src/display/MapCanvasData.cpp b/src/display/MapCanvasData.cpp index 40ee076c2..c68400d85 100644 --- a/src/display/MapCanvasData.cpp +++ b/src/display/MapCanvasData.cpp @@ -30,26 +30,26 @@ MapCanvasInputState::MapCanvasInputState(PrespammedPath &prespammedPath) MapCanvasInputState::~MapCanvasInputState() = default; +MapCanvasViewport::~MapCanvasViewport() = default; + const glm::mat4 &MapCanvasViewport::getViewProj() const { - if (m_viewProjDirty) { - const int w = width(); - const int h = height(); - if (w > 0 && h > 0) { - const float zoomScale = getTotalScaleFactor(); - const auto size = glm::ivec2(w, h); - m_viewProj = (!m_viewportConfig.use3D) - ? ProjectionUtils::calculateViewProjOld(getScroll(), - size, - zoomScale, - getCurrentLayer()) - : ProjectionUtils::calculateViewProj(m_viewportConfig, - getScroll(), - size, - zoomScale, - getCurrentLayer()); - m_viewProjDirty = false; - } + const int w = width(); + const int h = height(); + if (m_viewProjDirty && w > 0 && h > 0) { + const float zoomScale = getTotalScaleFactor(); + const auto size = glm::ivec2(w, h); + m_viewProj = (!m_viewportConfig.use3D) + ? ProjectionUtils::calculateViewProjOld(getScroll(), + size, + zoomScale, + getCurrentLayer()) + : ProjectionUtils::calculateViewProj(m_viewportConfig, + getScroll(), + size, + zoomScale, + getCurrentLayer()); + m_viewProjDirty = false; } return m_viewProj; } diff --git a/src/display/MapCanvasData.h b/src/display/MapCanvasData.h index b01a241b2..62748be52 100644 --- a/src/display/MapCanvasData.h +++ b/src/display/MapCanvasData.h @@ -94,19 +94,28 @@ struct NODISCARD MapCanvasViewport private: QWindow &m_window; +protected: + mutable int m_lastWidth = 0; + mutable int m_lastHeight = 0; + private: mutable glm::mat4 m_viewProj{1.f}; + mutable bool m_viewProjDirty = true; glm::vec2 m_scroll{0.f}; ScaleFactor m_scaleFactor; int m_currentLayer = 0; ViewportConfig m_viewportConfig; - mutable bool m_viewProjDirty = true; public: explicit MapCanvasViewport(QWindow &window) : m_window{window} {} + virtual ~MapCanvasViewport(); + +protected: + virtual void onViewProjDirty() const {} + public: NODISCARD auto width() const { return m_window.width(); } NODISCARD auto height() const { return m_window.height(); } @@ -117,7 +126,7 @@ struct NODISCARD MapCanvasViewport { if (m_scroll != scroll) { m_scroll = scroll; - m_viewProjDirty = true; + markViewProjDirty(); } } @@ -125,7 +134,7 @@ struct NODISCARD MapCanvasViewport void setScaleFactor(const ScaleFactor &scaleFactor) { m_scaleFactor = scaleFactor; - m_viewProjDirty = true; + markViewProjDirty(); } NODISCARD int getCurrentLayer() const { return m_currentLayer; } @@ -133,20 +142,25 @@ struct NODISCARD MapCanvasViewport { if (m_currentLayer != layer) { m_currentLayer = layer; - m_viewProjDirty = true; + markViewProjDirty(); } } - void markViewProjDirty() { m_viewProjDirty = true; } + void markViewProjDirty() + { + m_viewProjDirty = true; + onViewProjDirty(); + } void setViewportConfig(const ViewportConfig &config) { m_viewportConfig = config; - m_viewProjDirty = true; + markViewProjDirty(); } void setMvpExtern(const glm::mat4 &mvp) const { m_viewProj = mvp; m_viewProjDirty = false; + onViewProjDirty(); } NODISCARD const glm::mat4 &getViewProj() const; diff --git a/src/display/ProjectionUtils.cpp b/src/display/ProjectionUtils.cpp index f0e682ca9..2c694022d 100644 --- a/src/display/ProjectionUtils.cpp +++ b/src/display/ProjectionUtils.cpp @@ -110,7 +110,6 @@ glm::mat4 calculateViewProjOld(const glm::vec2 &scrollPos, const int /*currentLayer*/) { constexpr float FIXED_VIEW_DISTANCE = 60.f; - constexpr float ROOM_Z_SCALE = 7.f; constexpr auto baseSize = static_cast(ProjectionUtils::BASESIZE); const float swp = zoomScale * baseSize / static_cast(size.x); diff --git a/src/display/ProjectionUtils.h b/src/display/ProjectionUtils.h index a3c771f1b..6dc07e8d7 100644 --- a/src/display/ProjectionUtils.h +++ b/src/display/ProjectionUtils.h @@ -21,6 +21,7 @@ struct ViewportConfig namespace ProjectionUtils { static constexpr const int BASESIZE = 528; // REVISIT: Why this size? 16*33 isn't special. +constexpr float ROOM_Z_SCALE = 7.f; /** * @brief Calculate the pitch (vertical angle) in degrees, accounting for auto-tilt if enabled. diff --git a/src/display/Textures.cpp b/src/display/Textures.cpp index 8c4bf111b..890f65b71 100644 --- a/src/display/Textures.cpp +++ b/src/display/Textures.cpp @@ -8,12 +8,14 @@ #include "../global/thread_utils.h" #include "../global/utils.h" #include "../opengl/OpenGLTypes.h" +#include "../opengl/Weather.h" #include "Filenames.h" #include "RoadIndex.h" #include "mapcanvas.h" #include #include +#include #include #include #include @@ -151,8 +153,10 @@ MMTexture::MMTexture(Badge, const QString &name) tex.setMinMagFilters(QOpenGLTexture::Filter::LinearMipMapLinear, QOpenGLTexture::Filter::Linear); } -MMTexture::MMTexture(Badge, std::vector images) +MMTexture::MMTexture(Badge, std::vector images, bool forbidUpdates) : m_qt_texture{QOpenGLTexture::Target2D} + , m_id{INVALID_MM_TEXTURE_ID} + , m_forbidUpdates{forbidUpdates} , m_sourceData{std::make_unique(std::move(images))} { if (m_sourceData->m_images.empty()) { @@ -364,6 +368,71 @@ NODISCARD static std::vector createDottedWallImages(const ExitDirEnum di return images; } +NODISCARD static QImage createTileableValueNoiseImage(int size) +{ + QImage img(size, size, QImage::Format_RGBA8888); + + // Constants 127.1/311.7 provide high-frequency distribution to prevent Moire patterns + auto hash = [](float x, float y) -> float { + float dot = x * 127.1f + y * 311.7f; + float fract = std::sin(dot) * 43758.5453123f; + return fract - std::floor(fract); + }; + + auto lerp = [](float a, float b, float t) -> float { return a + t * (b - a); }; + + // Perlin's quintic curve ($6t^5-15t^4+10t^3$) ensures smooth C2 continuity at grid boundaries + auto smooth = [](float t) -> float { return t * t * t * (t * (t * 6.0f - 15.0f) + 10.0f); }; + + for (int y = 0; y < size; ++y) { + // scanLine avoids QImage's internal per-pixel coordinate-to-pointer overhead + uchar *line = img.scanLine(y); + + for (int x = 0; x < size; ++x) { + float xx = static_cast(x); + float yy = static_cast(y); + + float ix = std::floor(xx); + float iy = std::floor(yy); + float fx = xx - ix; + float fy = yy - iy; + + float sx = smooth(fx); + float sy = smooth(fy); + + // Double modulo ensures positive wrapping for seamless tiling + auto get_wrapped_hash = [&](int i, int j) { + float wi = static_cast((i % size + size) % size); + float wj = static_cast((j % size + size) % size); + return hash(wi, wj); + }; + + int iix = static_cast(ix); + int iiy = static_cast(iy); + + // Fetch corners for bilinear interpolation + float a = get_wrapped_hash(iix, iiy); + float b = get_wrapped_hash(iix + 1, iiy); + float c = get_wrapped_hash(iix, iiy + 1); + float d = get_wrapped_hash(iix + 1, iiy + 1); + + float v = lerp(lerp(a, b, sx), lerp(c, d, sx), sy); + + // Casting to uchar provides implicit floor and branchless clamping + uchar val = static_cast(std::clamp(v * 255.0f, 0.0f, 255.0f)); + + // Direct pointer offset for RGBA8888 interleaved memory + int offset = x * 4; + line[offset] = val; // R + line[offset + 1] = val; // G + line[offset + 2] = val; // B + line[offset + 3] = 255; // A + } + } + + return img; +} + template static void appendAll(std::vector &v, Type &&thing) { @@ -414,7 +483,12 @@ void MapCanvas::initTextures() // 1x1 QImage whitePixel(1, 1, QImage::Format_RGBA8888); whitePixel.fill(Qt::white); - textures.white_pixel = MMTexture::alloc(std::vector{whitePixel}); + textures.white_pixel = MMTexture::alloc(std::vector{whitePixel}, true); + } + { + // weather noise is 256 + QImage noiseImage = createTileableValueNoiseImage(256); + textures.noise = MMTexture::alloc(std::vector{noiseImage}, false); } // char images are 256 diff --git a/src/display/Textures.h b/src/display/Textures.h index 2672a0268..b0759d2b7 100644 --- a/src/display/Textures.h +++ b/src/display/Textures.h @@ -50,9 +50,10 @@ class NODISCARD MMTexture final : public std::enable_shared_from_this { return std::make_shared(Badge{}, name); } - NODISCARD static std::shared_ptr alloc(std::vector images) + NODISCARD static std::shared_ptr alloc(std::vector images, + bool forbidUpdates = false) { - return std::make_shared(Badge{}, std::move(images)); + return std::make_shared(Badge{}, std::move(images), forbidUpdates); } NODISCARD static std::shared_ptr alloc( const QOpenGLTexture::Target target, @@ -65,7 +66,7 @@ class NODISCARD MMTexture final : public std::enable_shared_from_this public: MMTexture() = delete; MMTexture(Badge, const QString &name); - MMTexture(Badge, std::vector images); + MMTexture(Badge, std::vector images, bool forbidUpdates = false); MMTexture(Badge, const QOpenGLTexture::Target target, const std::function &init, @@ -180,7 +181,8 @@ using TextureArrayNESWUD = EnumIndexedArray(*this)} , MapCanvasInputState{prespammedPath} , m_mapScreen{static_cast(*this)} + , m_observer{observer} , m_opengl{} , m_glFont{m_opengl} , m_data{mapData} , m_groupManager{groupManager} - , m_frameManager{static_cast(*this)} + , m_frameManager{static_cast(*this), m_opengl.getUboManager()} + , m_weather{m_opengl, m_data, m_textures, observer, m_frameManager} { syncViewportConfig(); + m_frameManager.registerCallback(m_lifetime, [this]() { return m_batches.remeshCookie.isPending() ? FrameManager::AnimationStatusEnum::Continue : FrameManager::AnimationStatusEnum::Stop; }); - NonOwningPointer &pmc = primaryMapCanvas(); if (pmc == nullptr) { pmc = this; @@ -1145,6 +1149,7 @@ void MapCanvas::onMovement() { const Coordinate &pos = m_data.tryGetPosition().value_or(Coordinate{}); setCurrentLayer(pos.z); + getOpenGL().getUboManager().invalidate(Legacy::SharedVboEnum::CameraBlock); const glm::vec2 newScroll = pos.to_vec2() + glm::vec2{0.5f, 0.5f}; if (!utils::isSameFloat(getScroll().x, newScroll.x) || !utils::isSameFloat(getScroll().y, newScroll.y)) { diff --git a/src/display/mapcanvas.h b/src/display/mapcanvas.h index 292cae815..996932b00 100644 --- a/src/display/mapcanvas.h +++ b/src/display/mapcanvas.h @@ -5,12 +5,15 @@ // Author: Marek Krejza (Caligor) // Author: Nils Schimmelmann (Jahara) +#include "../clock/mumemoment.h" #include "../global/ChangeMonitor.h" #include "../global/Signal2.h" +#include "../map/PromptFlags.h" #include "../mapdata/roomselection.h" #include "../opengl/Font.h" #include "../opengl/FontFormatFlags.h" #include "../opengl/OpenGL.h" +#include "../opengl/Weather.h" #include "FrameManager.h" #include "Infomarks.h" #include "MapCanvasData.h" @@ -18,12 +21,15 @@ #include "Textures.h" #include +#include #include #include +#include #include #include #include #include +#include #include #include @@ -37,6 +43,7 @@ class CharacterBatch; class ConnectionSelection; class Coordinate; +class GameObserver; class InfomarkSelection; class MapData; class Mmapper2Group; @@ -128,7 +135,8 @@ class NODISCARD_QOBJECT MapCanvas final : public QOpenGLWindow, private: MapScreen m_mapScreen; - OpenGL m_opengl; + GameObserver &m_observer; + mutable OpenGL m_opengl; GLFont m_glFont; Batches m_batches; MapCanvasTextures m_textures; @@ -138,9 +146,11 @@ class NODISCARD_QOBJECT MapCanvas final : public QOpenGLWindow, FrameManager m_frameManager; std::unique_ptr m_logger; Signal2Lifetime m_lifetime; + GLWeather m_weather; public: explicit MapCanvas(MapData &mapData, + GameObserver &observer, PrespammedPath &prespammedPath, Mmapper2Group &groupManager, QWindow *parent = nullptr); @@ -179,6 +189,9 @@ class NODISCARD_QOBJECT MapCanvas final : public QOpenGLWindow, void reportGLVersion(); NODISCARD bool isBlacklistedDriver(); +protected: + void onViewProjDirty() const override; + protected: void initializeGL() override; void paintGL() override; diff --git a/src/display/mapcanvas_gl.cpp b/src/display/mapcanvas_gl.cpp index 104a6b777..3a47433bd 100644 --- a/src/display/mapcanvas_gl.cpp +++ b/src/display/mapcanvas_gl.cpp @@ -18,6 +18,9 @@ #include "../opengl/OpenGLConfig.h" #include "../opengl/OpenGLTypes.h" #include "../opengl/legacy/Meshes.h" +#include "../opengl/legacy/TFO.h" +#include "../opengl/legacy/VAO.h" +#include "../opengl/legacy/VBO.h" #include "../src/global/SendToUser.h" #include "Connections.h" #include "MapCanvasConfig.h" @@ -35,8 +38,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -245,6 +250,18 @@ void MapCanvas::initializeGL() funcs, XNamedColor::getAllColorsAsBlock()); }); + gl.getUboManager().registerRebuildFunction( + Legacy::SharedVboEnum::CameraBlock, [this](Legacy::Functions &funcs) { + auto &camera = funcs.getUboManager().get(); + const auto playerPosCoord = m_data.tryGetPosition().value_or(Coordinate{0, 0, 0}); + camera.viewProj = getViewProj(); + camera.playerPos = glm::vec4(static_cast(playerPosCoord.x), + static_cast(playerPosCoord.y), + static_cast(playerPosCoord.z), + ProjectionUtils::ROOM_Z_SCALE); + funcs.getUboManager().sync(funcs); + }); + updateMultisampling(); // REVISIT: should the font texture have the lowest ID? @@ -356,7 +373,11 @@ void MapCanvas::setMvp(const glm::mat4 &viewProj) void MapCanvas::setViewportAndMvp(int width, int height) { - markViewProjDirty(); + if (width != m_lastWidth || height != m_lastHeight) { + m_lastWidth = width; + m_lastHeight = height; + markViewProjDirty(); + } auto &gl = getOpenGL(); gl.glViewport(0, 0, width, height); @@ -368,6 +389,11 @@ void MapCanvas::setViewportAndMvp(int width, int height) gl.setProjectionMatrix(MapCanvasViewport::getViewProj()); } +void MapCanvas::onViewProjDirty() const +{ + m_opengl.getUboManager().invalidate(Legacy::SharedVboEnum::CameraBlock); +} + void MapCanvas::resizeGL(int width, int height) { if (m_textures.room_highlight == nullptr) { @@ -500,11 +526,19 @@ void MapCanvas::actuallyPaintGL() if (m_data.isEmpty()) { getGLFont().renderTextCentered("No map loaded"); } else { + // Update animation state + m_weather.update(); + paintMap(); paintBatchedInfomarks(); paintSelections(); paintCharacters(); paintDifferences(); + + m_weather.prepare(); + gl.getUboManager().bind(funcs, Legacy::SharedVboEnum::TimeBlock); + + m_weather.render(m_opengl.getDefaultRenderState()); } gl.releaseFbo(); diff --git a/src/display/mapwindow.cpp b/src/display/mapwindow.cpp index d15934d47..2d2251e2b 100644 --- a/src/display/mapwindow.cpp +++ b/src/display/mapwindow.cpp @@ -23,7 +23,11 @@ class QResizeEvent; -MapWindow::MapWindow(MapData &mapData, PrespammedPath &pp, Mmapper2Group &gm, QWidget *const parent) +MapWindow::MapWindow(MapData &mapData, + GameObserver &observer, + PrespammedPath &pp, + Mmapper2Group &gm, + QWidget *const parent) : QWidget(parent) { m_gridLayout = mmqt::makeQPointer(this); @@ -46,7 +50,7 @@ MapWindow::MapWindow(MapData &mapData, PrespammedPath &pp, Mmapper2Group &gm, QW m_gridLayout->addWidget(m_horizontalScrollBar, 1, 0, 1, 1); - m_canvas = new MapCanvas(mapData, pp, gm); + m_canvas = new MapCanvas(mapData, observer, pp, gm); m_canvas->setMinimumSize(QSize(1280 / 4, 720 / 4)); m_canvas->resize(QSize(1280, 720)); diff --git a/src/display/mapwindow.h b/src/display/mapwindow.h index 49cc6d9bc..61a191e9b 100644 --- a/src/display/mapwindow.h +++ b/src/display/mapwindow.h @@ -18,6 +18,7 @@ #include #include +class GameObserver; class MapCanvas; class MapData; class Mmapper2Group; @@ -59,7 +60,11 @@ class NODISCARD_QOBJECT MapWindow final : public QWidget } m_knownMapSize; public: - explicit MapWindow(MapData &mapData, PrespammedPath &pp, Mmapper2Group &gm, QWidget *parent); + explicit MapWindow(MapData &mapData, + GameObserver &observer, + PrespammedPath &pp, + Mmapper2Group &gm, + QWidget *parent); ~MapWindow() final; public: diff --git a/src/global/NamedColors.cpp b/src/global/NamedColors.cpp index 8bee0a8f3..66f898905 100644 --- a/src/global/NamedColors.cpp +++ b/src/global/NamedColors.cpp @@ -57,6 +57,16 @@ struct NODISCARD GlobalData final init(NamedColorEnum::DEFAULT, ".default"); m_initialized[NamedColorEnum::DEFAULT] = true; m_initialized[NamedColorEnum::TRANSPARENT] = true; + + // Sane defaults for weather colors + std::ignore = setColor(NamedColorEnum::WEATHER_DAWN, + Color(102, 76, 51, 25)); // 0.4, 0.3, 0.2, 0.1 + std::ignore = setColor(NamedColorEnum::WEATHER_DUSK, + Color(76, 51, 102, 51)); // 0.3, 0.2, 0.4, 0.2 + std::ignore = setColor(NamedColorEnum::WEATHER_NIGHT, + Color(13, 13, 51, 89)); // 0.05, 0.05, 0.2, 0.35 + std::ignore = setColor(NamedColorEnum::WEATHER_NIGHT_MOON, + Color(13, 13, 64, 77)); // 0.05, 0.05, 0.25, 0.3 } public: diff --git a/src/global/NamedColors.h b/src/global/NamedColors.h index 860860771..1205ba1f4 100644 --- a/src/global/NamedColors.h +++ b/src/global/NamedColors.h @@ -43,7 +43,11 @@ struct NamedColorsBlock; X(WALL_COLOR_NOT_MAPPED, "wall-not-mapped") \ X(WALL_COLOR_RANDOM, "wall-random") \ X(WALL_COLOR_REGULAR_EXIT, "wall-regular-exit") \ - X(WALL_COLOR_SPECIAL, "wall-special") + X(WALL_COLOR_SPECIAL, "wall-special") \ + X(WEATHER_DAWN, "weather-dawn") \ + X(WEATHER_DUSK, "weather-dusk") \ + X(WEATHER_NIGHT, "weather-night") \ + X(WEATHER_NIGHT_MOON, "weather-night-moon") #define X_DECL(_id, _name) _id, enum class NODISCARD NamedColorEnum : uint8_t { DEFAULT = 0, XFOREACH_NAMED_COLOR_OPTIONS(X_DECL) }; diff --git a/src/mainwindow/mainwindow.cpp b/src/mainwindow/mainwindow.cpp index 7c6ea7f7e..19caa4b19 100644 --- a/src/mainwindow/mainwindow.cpp +++ b/src/mainwindow/mainwindow.cpp @@ -131,13 +131,18 @@ MainWindow::MainWindow() m_groupManager = new Mmapper2Group(this); m_groupManager->setObjectName("GroupManager"); - m_mapWindow = new MapWindow(mapData, deref(m_prespammedPath), deref(m_groupManager), this); + m_gameObserver = std::make_unique(); + + m_mapWindow = new MapWindow(mapData, + deref(m_gameObserver), + deref(m_prespammedPath), + deref(m_groupManager), + this); setCentralWidget(m_mapWindow); m_pathMachine = new Mmapper2PathMachine(mapData, this); m_pathMachine->setObjectName("Mmapper2PathMachine"); - m_gameObserver = std::make_unique(); m_mediaLibrary = new MediaLibrary(this); m_adventureTracker = new AdventureTracker(deref(m_gameObserver), this); m_audioManager = new AudioManager(deref(m_mediaLibrary), deref(m_gameObserver), this); diff --git a/src/opengl/OpenGL.h b/src/opengl/OpenGL.h index da06f75d0..6d8b1f9c3 100644 --- a/src/opengl/OpenGL.h +++ b/src/opengl/OpenGL.h @@ -20,6 +20,7 @@ class FBO; class MapCanvas; +class GLWeather; namespace Legacy { class Functions; } // namespace Legacy @@ -44,6 +45,7 @@ class NODISCARD OpenGL final public: NODISCARD const auto &getSharedFunctions(Badge) { return getSharedFunctions(); } + NODISCARD const auto &getSharedFunctions(Badge) { return getSharedFunctions(); } public: /* must be called before any other functions */ diff --git a/src/opengl/OpenGLTypes.h b/src/opengl/OpenGLTypes.h index efdf48019..f7a101ac5 100644 --- a/src/opengl/OpenGLTypes.h +++ b/src/opengl/OpenGLTypes.h @@ -25,6 +25,12 @@ #include #include +struct NODISCARD Viewport final +{ + glm::ivec2 offset{}; + glm::ivec2 size{}; +}; + struct NODISCARD TexVert final { glm::vec3 tex{}; @@ -87,6 +93,17 @@ struct NODISCARD ColorVert final {} }; +struct NODISCARD WeatherParticleVert final +{ + glm::vec2 pos{}; + float life = 0.0f; + + explicit WeatherParticleVert(const glm::vec2 &pos_, const float life_) + : pos{pos_} + , life{life_} + {} +}; + // Similar to ColoredTexVert, except it has a base position in world coordinates. // the font's vertex shader transforms the world position to screen space, // rounds to integer pixel offset, and then adds the vertex position in screen space. @@ -153,6 +170,9 @@ enum class NODISCARD BlendModeEnum { /* This mode allows you to multiply by the painted color, in the range [0,1]. * glEnable(GL_BLEND); glBlendFuncSeparate(GL_ZERO, GL_SRC_COLOR, GL_ZERO, GL_ONE); */ MODULATE, + /* This mode uses MAX for alpha blending, useful for weather effects. + * glBlendEquationSeparate(GL_FUNC_ADD, GL_MAX); */ + MAX_ALPHA, }; enum class NODISCARD CullingEnum { @@ -404,12 +424,6 @@ struct NODISCARD UniqueMeshVector final } }; -struct NODISCARD Viewport final -{ - glm::ivec2 offset{}; - glm::ivec2 size{}; -}; - static constexpr const size_t VERTS_PER_LINE = 2; static constexpr const size_t VERTS_PER_TRI = 3; static constexpr const size_t VERTS_PER_QUAD = 4; diff --git a/src/opengl/UboBlocks.h b/src/opengl/UboBlocks.h index 894066612..9bf4b072d 100644 --- a/src/opengl/UboBlocks.h +++ b/src/opengl/UboBlocks.h @@ -20,7 +20,11 @@ namespace Legacy { * Note: SharedVboEnum values are implicitly used as UBO binding indices. * They must be 0-based and contiguous. */ -#define XFOREACH_SHARED_VBO(X) X(NamedColorsBlock, "NamedColorsBlock") +#define XFOREACH_SHARED_VBO(X) \ + X(NamedColorsBlock, "NamedColorsBlock") \ + X(CameraBlock, "CameraBlock") \ + X(TimeBlock, "TimeBlock") \ + X(WeatherBlock, "WeatherBlock") #define X_ENUM(element, name) element, enum class NODISCARD SharedVboEnum : uint8_t { XFOREACH_SHARED_VBO(X_ENUM) NUM_BLOCKS }; @@ -34,6 +38,37 @@ inline constexpr const char *const SharedVboNames[] = { #undef X_NAME }; +/** + * @brief Memory layout for the Camera uniform block. + * Must match CameraBlock in shaders (std140 layout). + */ +struct NODISCARD CameraBlock final +{ + glm::mat4 viewProj{1.0f}; // 0-63 + glm::vec4 playerPos{0.0f}; // 64-79 (xyz, w=zScale) +}; + +/** + * @brief Memory layout for the Time uniform block. + * Must match TimeBlock in shaders (std140 layout). + */ +struct NODISCARD TimeBlock final +{ + glm::vec4 time{0.0f}; // 0-15 (x=time, y=delta, zw=unused) +}; + +/** + * @brief Memory layout for the Weather uniform block. + * Must match WeatherBlock in shaders (std140 layout). + */ +struct NODISCARD WeatherBlock final +{ + glm::vec4 intensities{0.0f}; // 0-15: rain, snow, clouds, fog (starts) + glm::vec4 targets{0.0f}; // 16-31: rain, snow, clouds, fog (targets) + glm::vec4 timeOfDay{0.0f}; // 32-47: startIdx, targetIdx, todStart, todTarget + glm::vec4 config{0.0f}; // 48-63: weatherStartTime, todStartTime, duration, unused +}; + /** * @brief Memory layout for the NamedColors uniform block. * Must match NamedColorsBlock in shaders (std140 layout). diff --git a/src/opengl/Weather.cpp b/src/opengl/Weather.cpp new file mode 100644 index 000000000..d1ed8de17 --- /dev/null +++ b/src/opengl/Weather.cpp @@ -0,0 +1,460 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2026 The MMapper Authors + +#include "Weather.h" + +#include "../configuration/configuration.h" +#include "../display/FrameManager.h" +#include "../display/Textures.h" +#include "../global/Badge.h" +#include "../map/coordinate.h" +#include "../mapdata/mapdata.h" +#include "OpenGL.h" +#include "OpenGLTypes.h" +#include "legacy/Legacy.h" + +#include +#include + +#include + +namespace { +template +T lerp(T a, T b, float t) +{ + return a + t * (b - a); +} + +} // namespace + +/** + * Weather transition authority documentation: + * + * Transitions (weather intensities, fog, time-of-day) are driven by a shared + * duration constant (WeatherConstants::TRANSITION_DURATION). + * + * The CPU (GLWeather::update/applyTransition) calculates interpolated values + * each frame. These CPU-side values are primarily used for high-level logic, + * such as: + * - Pruning: Skipping rendering of atmosphere/particles when intensities are zero. + * - Thinning: Scaling the number of particle instances based on intensity for performance. + * - Pacing: Determining if FrameManager heartbeat should continue. + * + * The GPU (shaders) is authoritative for the visual transition curves. + * By passing start/target pairs and start times to the shaders, the GPU + * performs per-pixel (or per-vertex) interpolation using its own clocks. + * This ensures perfectly smooth visuals even if the CPU frame rate is low + * or inconsistent, while avoiding constant UBO re-uploads for animation state. + */ + +GLWeather::GLWeather(OpenGL &gl, + MapData &mapData, + const MapCanvasTextures &textures, + GameObserver &observer, + FrameManager &animationManager) + : m_gl(gl) + , m_data(mapData) + , m_textures(textures) + , m_animationManager(animationManager) + , m_observer(observer) +{ + updateFromGame(); + + m_moonVisibility = m_observer.getMoonVisibility(); + m_currentTimeOfDay = m_observer.getTimeOfDay(); + m_gameTimeOfDayIntensity = (m_currentTimeOfDay == MumeTimeEnum::DAY) ? 0.0f : 1.0f; + + { + auto &wbInit = m_gl.getUboManager().get(); + const NamedColorEnum targetColorIdx = getCurrentColorIdx(); + + wbInit.timeOfDay.x = static_cast(targetColorIdx); // startIdx + wbInit.timeOfDay.y = static_cast(targetColorIdx); // targetIdx + wbInit.timeOfDay.z = m_gameTimeOfDayIntensity; // todStart + wbInit.timeOfDay.w = m_gameTimeOfDayIntensity; // todTarget + + wbInit.intensities = glm::vec4(m_gameRainIntensity, + m_gameSnowIntensity, + m_gameCloudsIntensity, + m_gameFogIntensity); + + updateTargets(); + wbInit.intensities = wbInit.targets; + + wbInit.config.x = -2.0f; // weatherStartTime + wbInit.config.y = -2.0f; // todStartTime + wbInit.config.z = WeatherConstants::TRANSITION_DURATION; + } + + auto startWeatherTransitions = [this]() { + auto &wb = m_gl.getUboManager().get(); + + const bool startingFromNice = (m_currentRainIntensity <= 0.0f + && m_currentSnowIntensity <= 0.0f); + + startTransitions(wb.config.x, + TransitionPair{wb.intensities.x, wb.targets.x}, + TransitionPair{wb.intensities.y, wb.targets.y}, + TransitionPair{wb.intensities.z, wb.targets.z}, + TransitionPair{wb.intensities.w, wb.targets.w}); + + if (startingFromNice) { + // If we are starting from clear skies, snap the start intensity for rain/snow + // immediately so the ratio (pType) is correct during fade-in. + wb.intensities.x = wb.targets.x > 0.0f ? 0.001f : 0.0f; + wb.intensities.y = wb.targets.y > 0.0f ? 0.001f : 0.0f; + } + }; + + m_observer.sig2_weatherChanged.connect(m_lifetime, + [this, startWeatherTransitions](PromptWeatherEnum) { + startWeatherTransitions(); + updateFromGame(); + updateTargets(); + syncWeatherAtmosphere(); + m_animationManager.requestUpdate(); + }); + + m_observer.sig2_fogChanged.connect(m_lifetime, [this, startWeatherTransitions](PromptFogEnum) { + startWeatherTransitions(); + updateFromGame(); + updateTargets(); + syncWeatherAtmosphere(); + m_animationManager.requestUpdate(); + }); + + auto startTimeOfDayTransitions = [this]() { + auto &wb = m_gl.getUboManager().get(); + auto startColor = static_cast(static_cast(wb.timeOfDay.x)); + auto targetColor = static_cast(static_cast(wb.timeOfDay.y)); + + startTransitions(wb.config.y, + TransitionPair{wb.timeOfDay.z, wb.timeOfDay.w}, + TransitionPair{startColor, targetColor}); + + // Capture the snapped current color as the new transition start. + // Callers that change the target color set wb.timeOfDay.y themselves. + wb.timeOfDay.x = static_cast(startColor); + }; + + m_observer.sig2_timeOfDayChanged + .connect(m_lifetime, [this, startTimeOfDayTransitions](MumeTimeEnum timeOfDay) { + if (timeOfDay == m_currentTimeOfDay) { + return; + } + + startTimeOfDayTransitions(); + + m_currentTimeOfDay = timeOfDay; + m_gameTimeOfDayIntensity = (timeOfDay == MumeTimeEnum::DAY) ? 0.0f : 1.0f; + updateTargets(); + + auto &wb_tod = m_gl.getUboManager().get(); + wb_tod.timeOfDay.y = static_cast(getCurrentColorIdx()); + + syncWeatherTimeOfDay(); + m_animationManager.requestUpdate(); + }); + + m_observer.sig2_moonVisibilityChanged + .connect(m_lifetime, [this, startTimeOfDayTransitions](MumeMoonVisibilityEnum visibility) { + if (visibility == m_moonVisibility) { + return; + } + startTimeOfDayTransitions(); + + m_moonVisibility = visibility; + auto &wb_moon = m_gl.getUboManager().get(); + wb_moon.timeOfDay.y = static_cast(getCurrentColorIdx()); + + syncWeatherTimeOfDay(); + m_animationManager.requestUpdate(); + }); + + auto onSettingChanged = [this, startWeatherTransitions]() { + startWeatherTransitions(); + updateTargets(); + syncWeatherAtmosphere(); + m_animationManager.requestUpdate(); + }; + + auto onTimeOfDaySettingChanged = [this, startTimeOfDayTransitions]() { + startTimeOfDayTransitions(); + updateTargets(); + syncWeatherTimeOfDay(); + m_animationManager.requestUpdate(); + }; + + setConfig().canvas.weatherPrecipitationIntensity.registerChangeCallback(m_lifetime, + onSettingChanged); + setConfig().canvas.weatherAtmosphereIntensity.registerChangeCallback(m_lifetime, + onSettingChanged); + setConfig().canvas.weatherTimeOfDayIntensity.registerChangeCallback(m_lifetime, + onTimeOfDaySettingChanged); + + m_animationManager.registerCallback(m_lifetime, [this]() { + return isAnimating() ? FrameManager::AnimationStatusEnum::Continue + : FrameManager::AnimationStatusEnum::Stop; + }); + + m_gl.getUboManager().registerRebuildFunction(Legacy::SharedVboEnum::WeatherBlock, + [this](Legacy::Functions &funcs) { + m_gl.getUboManager() + .sync( + funcs); + }); +} + +GLWeather::~GLWeather() +{ + m_gl.getUboManager().unregisterRebuildFunction(Legacy::SharedVboEnum::WeatherBlock); +} + +void GLWeather::updateFromGame() +{ + switch (m_observer.getWeather()) { + case PromptWeatherEnum::NICE: + m_gameCloudsIntensity = 0.0f; + m_gameRainIntensity = 0.0f; + m_gameSnowIntensity = 0.0f; + break; + case PromptWeatherEnum::CLOUDS: + m_gameCloudsIntensity = 0.5f; + m_gameRainIntensity = 0.0f; + m_gameSnowIntensity = 0.0f; + break; + case PromptWeatherEnum::RAIN: + m_gameCloudsIntensity = 0.8f; + m_gameRainIntensity = 0.5f; + m_gameSnowIntensity = 0.0f; + break; + case PromptWeatherEnum::HEAVY_RAIN: + m_gameCloudsIntensity = 1.0f; + m_gameRainIntensity = 1.0f; + m_gameSnowIntensity = 0.0f; + break; + case PromptWeatherEnum::SNOW: + m_gameCloudsIntensity = 0.8f; + m_gameRainIntensity = 0.0f; + m_gameSnowIntensity = 0.8f; + break; + } + + switch (m_observer.getFog()) { + case PromptFogEnum::NO_FOG: + m_gameFogIntensity = 0.0f; + break; + case PromptFogEnum::LIGHT_FOG: + m_gameFogIntensity = 0.5f; + break; + case PromptFogEnum::HEAVY_FOG: + m_gameFogIntensity = 1.0f; + break; + } +} + +void GLWeather::updateTargets() +{ + const auto &canvasSettings = getConfig().canvas; + auto &weather = m_gl.getUboManager().get(); + + weather.targets.x = m_gameRainIntensity + * (static_cast(canvasSettings.weatherPrecipitationIntensity.get()) + / 50.0f); + weather.targets.y = m_gameSnowIntensity + * (static_cast(canvasSettings.weatherPrecipitationIntensity.get()) + / 50.0f); + weather.targets.z = m_gameCloudsIntensity + * (static_cast(canvasSettings.weatherAtmosphereIntensity.get()) + / 50.0f); + weather.targets.w = m_gameFogIntensity + * (static_cast(canvasSettings.weatherAtmosphereIntensity.get()) + / 50.0f); + weather.timeOfDay.w = m_gameTimeOfDayIntensity + * (static_cast(canvasSettings.weatherTimeOfDayIntensity.get()) + / 50.0f); +} + +void GLWeather::update() +{ + const auto &weather = m_gl.getUboManager().get(); + + m_currentRainIntensity = applyTransition(weather.config.x, + weather.intensities.x, + weather.targets.x); + m_currentSnowIntensity = applyTransition(weather.config.x, + weather.intensities.y, + weather.targets.y); + m_currentCloudsIntensity = applyTransition(weather.config.x, + weather.intensities.z, + weather.targets.z); + m_currentFogIntensity = applyTransition(weather.config.x, + weather.intensities.w, + weather.targets.w); + + m_currentTimeOfDayIntensity = applyTransition(weather.config.y, + weather.timeOfDay.z, + weather.timeOfDay.w); +} + +bool GLWeather::isAnimating() const +{ + const bool activePrecipitation = m_currentRainIntensity > 0.0f || m_currentSnowIntensity > 0.0f; + const bool activeAtmosphere = m_currentCloudsIntensity > 0.0f || m_currentFogIntensity > 0.0f; + return isTransitioning() || activePrecipitation || activeAtmosphere; +} + +bool GLWeather::isTransitioning() const +{ + const float animTime = m_animationManager.getElapsedTime(); + const auto &wb = m_gl.getUboManager().get(); + const float duration = wb.config.z; + + const bool weatherTransitioning = (animTime - wb.config.x < duration); + const bool timeOfDayTransitioning = (animTime - wb.config.y < duration); + return weatherTransitioning || timeOfDayTransitioning; +} + +void GLWeather::syncWeatherAtmosphere() +{ + auto &funcs = deref(m_gl.getSharedFunctions(Badge{})); + auto &ubo = m_gl.getUboManager(); + ubo.syncFields(funcs, + &Legacy::WeatherBlock::config, + &Legacy::WeatherBlock::intensities, + &Legacy::WeatherBlock::targets); +} + +void GLWeather::syncWeatherTimeOfDay() +{ + auto &funcs = deref(m_gl.getSharedFunctions(Badge{})); + auto &ubo = m_gl.getUboManager(); + ubo.syncFields(funcs, + &Legacy::WeatherBlock::config, + &Legacy::WeatherBlock::timeOfDay); +} + +NamedColorEnum GLWeather::getCurrentColorIdx() const +{ + switch (m_currentTimeOfDay) { + case MumeTimeEnum::DAY: + return NamedColorEnum::TRANSPARENT; + case MumeTimeEnum::NIGHT: + return (m_moonVisibility == MumeMoonVisibilityEnum::BRIGHT) + ? NamedColorEnum::WEATHER_NIGHT_MOON + : NamedColorEnum::WEATHER_NIGHT; + case MumeTimeEnum::DAWN: + return NamedColorEnum::WEATHER_DAWN; + case MumeTimeEnum::DUSK: + return NamedColorEnum::WEATHER_DUSK; + case MumeTimeEnum::UNKNOWN: + return NamedColorEnum::TRANSPARENT; + } + return NamedColorEnum::TRANSPARENT; +} + +float GLWeather::applyTransition(const float startTime, + const float startVal, + const float targetVal) const +{ + const float t = (m_animationManager.getElapsedTime() - startTime) + / WeatherConstants::TRANSITION_DURATION; + const float factor = std::clamp(t, 0.0f, 1.0f); + return lerp(startVal, targetVal, factor); +} + +template +T GLWeather::applyTransition(float startTime, T startVal, T targetVal) const +{ + if (startVal == targetVal) { + return startVal; + } + const float t = (m_animationManager.getElapsedTime() - startTime) + / WeatherConstants::TRANSITION_DURATION; + return (t >= 1.0f) ? targetVal : startVal; +} + +template +void GLWeather::startTransitions(float &startTime, Pairs... pairs) +{ + float oldStartTime = startTime; + startTime = m_animationManager.getElapsedTime(); + (..., (pairs.start = applyTransition(oldStartTime, pairs.start, pairs.target))); +} + +template void GLWeather::startTransitions(float &startTime, + TransitionPair p1, + TransitionPair p2, + TransitionPair p3, + TransitionPair p4); + +template void GLWeather::startTransitions(float &startTime, + TransitionPair p1, + TransitionPair p2); + +void GLWeather::initMeshes() +{ + if (!m_simulation) { + auto funcs = m_gl.getSharedFunctions(Badge{}); + auto &shaderPrograms = funcs->getShaderPrograms(); + + m_simulation = std::make_unique( + funcs, shaderPrograms.getParticleSimulationShader()); + m_particles + = std::make_unique(funcs, + shaderPrograms.getParticleRenderShader(), + *m_simulation); + m_atmosphere = UniqueMesh( + std::make_unique(funcs, shaderPrograms.getAtmosphereShader())); + m_timeOfDay = UniqueMesh( + std::make_unique(funcs, shaderPrograms.getTimeOfDayShader())); + } +} + +void GLWeather::prepare() +{ + initMeshes(); + + auto &funcs = deref(m_gl.getSharedFunctions(Badge{})); + m_gl.getUboManager().bind(funcs, Legacy::SharedVboEnum::CameraBlock); + m_gl.getUboManager().bind(funcs, Legacy::SharedVboEnum::WeatherBlock); +} + +void GLWeather::render(const GLRenderState &rs) +{ + // 1. Render Particles (Simulation + Rendering) + const float rainMax = m_currentRainIntensity; + const float snowMax = m_currentSnowIntensity; + if (rainMax > 0.0f || snowMax > 0.0f) { + auto particleRs = rs.withBlend(BlendModeEnum::MAX_ALPHA); + + if (m_simulation) { + m_simulation->render(particleRs); + } + if (m_particles) { + m_particles->setIntensity(std::max(rainMax, snowMax)); + m_particles->render(particleRs); + } + } + + // 2. Render Atmosphere (TimeOfDay + Atmosphere) + const auto atmosphereRs = rs.withBlend(BlendModeEnum::TRANSPARENCY) + .withDepthFunction(std::nullopt); + + // TimeOfDay + const auto &wb = m_gl.getUboManager().get(); + const auto currentStartColor = static_cast(static_cast(wb.timeOfDay.x)); + if (m_currentTimeOfDay != MumeTimeEnum::DAY || currentStartColor != NamedColorEnum::TRANSPARENT + || m_currentTimeOfDayIntensity > 0.0f) { + if (m_timeOfDay) { + m_timeOfDay.render(atmosphereRs); + } + } + + // Atmosphere + const float cloudMax = m_currentCloudsIntensity; + const float fogMax = m_currentFogIntensity; + if ((cloudMax > 0.0f || fogMax > 0.0f) && m_atmosphere) { + m_atmosphere.render(atmosphereRs.withTexture0(m_textures.noise->getId())); + } +} diff --git a/src/opengl/Weather.h b/src/opengl/Weather.h new file mode 100644 index 000000000..f1d86ae8f --- /dev/null +++ b/src/opengl/Weather.h @@ -0,0 +1,123 @@ +#pragma once +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2026 The MMapper Authors + +#include "../clock/mumemoment.h" +#include "../global/ChangeMonitor.h" +#include "../global/ConfigEnums.h" +#include "../global/RuleOf5.h" +#include "../map/PromptFlags.h" +#include "../map/coordinate.h" +#include "../observer/gameobserver.h" +#include "OpenGL.h" + +class FrameManager; +#include "OpenGLTypes.h" +#include "legacy/Legacy.h" +#include "legacy/WeatherMeshes.h" + +#include + +#undef TRANSPARENT // Bad dog, Microsoft; bad dog!!! +#include + +class MapData; +struct MapCanvasTextures; + +namespace WeatherConstants { +/** + * @brief Spatial parameters for weather particles and atmosphere. + * + * WEATHER_RADIUS (14.0) defines the world-space distance from the player + * where particles wrap and atmosphere extents are calculated. + * + * WEATHER_EXTENT (28.0) is the total diameter of the toroidal simulation area. + * + * Atmosphere and particle masks typically fade out at a 12.0 radius to avoid + * sharp edges at the simulation boundaries. + */ +constexpr float WEATHER_RADIUS = 14.0f; +constexpr float WEATHER_EXTENT = 28.0f; +constexpr float WEATHER_MASK_RADIUS_OUTER = 12.0f; +constexpr float WEATHER_MASK_RADIUS_INNER = 8.0f; +constexpr float TRANSITION_DURATION = 2.0f; +} // namespace WeatherConstants + +/** + * @brief Manages the logic and rendering of the weather system. + * Follows the design and structure of GLFont. + */ +class NODISCARD GLWeather final +{ +private: + struct FrameManagerProxy; + OpenGL &m_gl; + MapData &m_data; + const MapCanvasTextures &m_textures; + FrameManager &m_animationManager; + GameObserver &m_observer; + ChangeMonitor::Lifetime m_lifetime; + + // Weather State + float m_currentRainIntensity = 0.0f; + float m_currentSnowIntensity = 0.0f; + float m_currentCloudsIntensity = 0.0f; + float m_currentFogIntensity = 0.0f; + float m_currentTimeOfDayIntensity = 0.0f; + + float m_gameRainIntensity = 0.0f; + float m_gameSnowIntensity = 0.0f; + float m_gameCloudsIntensity = 0.0f; + float m_gameFogIntensity = 0.0f; + float m_gameTimeOfDayIntensity = 0.0f; + MumeTimeEnum m_currentTimeOfDay = MumeTimeEnum::DAY; + MumeMoonVisibilityEnum m_moonVisibility = MumeMoonVisibilityEnum::UNKNOWN; + + // Meshes + std::unique_ptr m_simulation; + std::unique_ptr m_particles; + UniqueMesh m_atmosphere; + UniqueMesh m_timeOfDay; + +public: + explicit GLWeather(OpenGL &gl, + MapData &mapData, + const MapCanvasTextures &textures, + GameObserver &observer, + FrameManager &frameManager); + ~GLWeather(); + + DELETE_CTORS_AND_ASSIGN_OPS(GLWeather); + +public: + void update(); + void prepare(); + void render(const GLRenderState &rs); + + NODISCARD bool isAnimating() const; + NODISCARD bool isTransitioning() const; + +private: + void updateTargets(); + void updateFromGame(); + void initMeshes(); + void syncWeatherAtmosphere(); + void syncWeatherTimeOfDay(); + + NODISCARD float applyTransition(float startTime, float startVal, float targetVal) const; + + template + NODISCARD T applyTransition(float startTime, T startVal, T targetVal) const; + + template + struct TransitionPair + { + T &start; + T target; + }; + + template + void startTransitions(float &startTime, Pairs... pairs); + + NODISCARD NamedColorEnum getCurrentColorIdx() const; +}; diff --git a/src/opengl/legacy/Binders.cpp b/src/opengl/legacy/Binders.cpp index 41c071126..398c60a4d 100644 --- a/src/opengl/legacy/Binders.cpp +++ b/src/opengl/legacy/Binders.cpp @@ -25,6 +25,11 @@ BlendBinder::BlendBinder(Functions &functions, const BlendModeEnum blend) m_functions.glEnable(GL_BLEND); m_functions.glBlendFuncSeparate(GL_ZERO, GL_SRC_COLOR, GL_ZERO, GL_ONE); break; + case BlendModeEnum::MAX_ALPHA: + m_functions.glEnable(GL_BLEND); + m_functions.glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE); + m_functions.glBlendEquationSeparate(GL_FUNC_ADD, GL_MAX); + break; } } @@ -37,6 +42,10 @@ BlendBinder::~BlendBinder() case BlendModeEnum::MODULATE: m_functions.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); break; + case BlendModeEnum::MAX_ALPHA: + m_functions.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + m_functions.glBlendEquationSeparate(GL_FUNC_ADD, GL_FUNC_ADD); + break; } m_functions.glDisable(GL_BLEND); } diff --git a/src/opengl/legacy/Binders.h b/src/opengl/legacy/Binders.h index 38564b7ff..3da993741 100644 --- a/src/opengl/legacy/Binders.h +++ b/src/opengl/legacy/Binders.h @@ -4,6 +4,9 @@ #include "../../global/utils.h" #include "Legacy.h" +#include "TFO.h" +#include "VAO.h" +#include "VBO.h" #include diff --git a/src/opengl/legacy/Legacy.cpp b/src/opengl/legacy/Legacy.cpp index 0e4976d54..0aa784ccb 100644 --- a/src/opengl/legacy/Legacy.cpp +++ b/src/opengl/legacy/Legacy.cpp @@ -14,6 +14,8 @@ #include "ShaderUtils.h" #include "Shaders.h" #include "SimpleMesh.h" +#include "TFO.h" +#include "VAO.h" #include "VBO.h" #include diff --git a/src/opengl/legacy/Legacy.h b/src/opengl/legacy/Legacy.h index 9cd19bf63..d6f67065f 100644 --- a/src/opengl/legacy/Legacy.h +++ b/src/opengl/legacy/Legacy.h @@ -29,9 +29,11 @@ namespace Legacy { class StaticVbos; class SharedVbos; class SharedVaos; +class SharedTfos; class UboManager; class VBO; class VAO; +class Program; struct AbstractShaderProgram; struct ShaderPrograms; struct PointSizeBinder; @@ -173,8 +175,10 @@ class NODISCARD Functions : protected QOpenGLExtraFunctions, using Base::glAttachShader; using Base::glBindBuffer; using Base::glBindBufferBase; + using Base::glBindBufferRange; using Base::glBindTexture; using Base::glBindVertexArray; + using Base::glBlendEquationSeparate; using Base::glBlendFunc; using Base::glBlendFuncSeparate; using Base::glBufferData; @@ -188,6 +192,7 @@ class NODISCARD Functions : protected QOpenGLExtraFunctions, using Base::glDeleteBuffers; using Base::glDeleteProgram; using Base::glDeleteShader; + using Base::glDeleteTransformFeedbacks; using Base::glDeleteVertexArrays; using Base::glDepthFunc; using Base::glDetachShader; @@ -200,6 +205,7 @@ class NODISCARD Functions : protected QOpenGLExtraFunctions, using Base::glEnableVertexAttribArray; using Base::glGenBuffers; using Base::glGenerateMipmap; + using Base::glGenTransformFeedbacks; using Base::glGenVertexArrays; using Base::glGetAttribLocation; using Base::glGetIntegerv; @@ -220,6 +226,12 @@ class NODISCARD Functions : protected QOpenGLExtraFunctions, using Base::glLinkProgram; using Base::glPixelStorei; using Base::glShaderSource; + + using Base::glBeginTransformFeedback; + using Base::glBindTransformFeedback; + using Base::glEndTransformFeedback; + using Base::glTransformFeedbackVaryings; + using Base::glTexSubImage3D; using Base::glUniform1fv; using Base::glUniform1iv; @@ -235,6 +247,7 @@ class NODISCARD Functions : protected QOpenGLExtraFunctions, * Note: This uses the enum value as the fixed binding point. */ void glUniformBlockBinding(const GLuint program, const SharedVboEnum block); + void glUniformBlockBinding(const Program &program, const SharedVboEnum block); /** * @brief Automatically assigns fixed binding points to all known uniform blocks. diff --git a/src/opengl/legacy/ShaderUtils.cpp b/src/opengl/legacy/ShaderUtils.cpp index 8e11dc15e..b628b259e 100644 --- a/src/opengl/legacy/ShaderUtils.cpp +++ b/src/opengl/legacy/ShaderUtils.cpp @@ -8,6 +8,7 @@ #include "../../global/NamedColors.h" #include "../../global/PrintUtils.h" #include "../../global/TextUtils.h" +#include "../Weather.h" #include #include @@ -214,8 +215,25 @@ NODISCARD static GLuint compileShader(Functions &gl, const GLenum type, const So // that's the reason the `ptrs` array below is not `const`. std::string defineNamedColors = "#define MAX_NAMED_COLORS " + std::to_string(MAX_NAMED_COLORS) + "\n"; - std::array ptrs = {gl.getShaderVersion(), + std::string defineWeatherRadius = "#define WEATHER_RADIUS " + + std::to_string(WeatherConstants::WEATHER_RADIUS) + "\n"; + std::string defineWeatherExtent = "#define WEATHER_EXTENT " + + std::to_string(WeatherConstants::WEATHER_EXTENT) + "\n"; + std::string defineWeatherMaskOuter = "#define WEATHER_MASK_RADIUS_OUTER " + + std::to_string( + WeatherConstants::WEATHER_MASK_RADIUS_OUTER) + + "\n"; + std::string defineWeatherMaskInner = "#define WEATHER_MASK_RADIUS_INNER " + + std::to_string( + WeatherConstants::WEATHER_MASK_RADIUS_INNER) + + "\n"; + + std::array ptrs = {gl.getShaderVersion(), defineNamedColors.c_str(), + defineWeatherRadius.c_str(), + defineWeatherExtent.c_str(), + defineWeatherMaskOuter.c_str(), + defineWeatherMaskInner.c_str(), "#line 1\n", source.source.c_str()}; gl.glShaderSource(shaderId, static_cast(ptrs.size()), ptrs.data(), nullptr); @@ -266,4 +284,40 @@ Program loadShaders(Functions &gl, const Source &vert, const Source &frag) return result_prog; } +Program loadTransformFeedbackShaders(Functions &gl, + const Source &vert, + const Source &frag, + const std::vector &varyings) +{ + GLuint vshader = compileShader(gl, GL_VERTEX_SHADER, vert); + GLuint fshader = compileShader(gl, GL_FRAGMENT_SHADER, frag); + + Program result_prog; + result_prog.emplace(gl.shared_from_this()); + const GLuint prog = result_prog.get(); + + gl.glAttachShader(prog, vshader); + if (fshader != 0) { + gl.glAttachShader(prog, fshader); + } + + gl.glTransformFeedbackVaryings(prog, + static_cast(varyings.size()), + varyings.data(), + GL_INTERLEAVED_ATTRIBS); + + gl.glLinkProgram(prog); + checkProgramInfo(gl, prog); + gl.applyDefaultUniformBlockBindings(prog); + + gl.glDetachShader(prog, vshader); + gl.glDeleteShader(vshader); + if (fshader != 0) { + gl.glDetachShader(prog, fshader); + gl.glDeleteShader(fshader); + } + + return result_prog; +} + } // namespace ShaderUtils diff --git a/src/opengl/legacy/ShaderUtils.h b/src/opengl/legacy/ShaderUtils.h index 98ee2fffd..fd8664c7d 100644 --- a/src/opengl/legacy/ShaderUtils.h +++ b/src/opengl/legacy/ShaderUtils.h @@ -29,4 +29,9 @@ struct NODISCARD Source final NODISCARD Program loadShaders(Functions &gl, const Source &vert, const Source &frag); +NODISCARD Program loadTransformFeedbackShaders(Functions &gl, + const Source &vert, + const Source &frag, + const std::vector &varyings); + } // namespace ShaderUtils diff --git a/src/opengl/legacy/Shaders.cpp b/src/opengl/legacy/Shaders.cpp index 0ac694a3b..8b2e8eca1 100644 --- a/src/opengl/legacy/Shaders.cpp +++ b/src/opengl/legacy/Shaders.cpp @@ -3,6 +3,7 @@ #include "Shaders.h" +#include "../../global/TextUtils.h" #include "ShaderUtils.h" #include @@ -16,7 +17,7 @@ NODISCARD static std::string readWholeResourceFile(const std::string &fullPath) throw std::runtime_error("error opening file"); } QTextStream in(&f); - return in.readAll().toUtf8().toStdString(); + return mmqt::toStdStringUtf8(in.readAll()); } NODISCARD static ShaderUtils::Source readWholeShader(const std::string &dir, const std::string &name) @@ -38,6 +39,10 @@ FontShader::~FontShader() = default; PointShader::~PointShader() = default; BlitShader::~BlitShader() = default; FullScreenShader::~FullScreenShader() = default; +AtmosphereShader::~AtmosphereShader() = default; +TimeOfDayShader::~TimeOfDayShader() = default; +ParticleSimulationShader::~ParticleSimulationShader() = default; +ParticleRenderShader::~ParticleRenderShader() = default; void ShaderPrograms::early_init() { @@ -52,6 +57,10 @@ void ShaderPrograms::early_init() std::ignore = getPointShader(); std::ignore = getBlitShader(); std::ignore = getFullScreenShader(); + std::ignore = getAtmosphereShader(); + std::ignore = getTimeOfDayShader(); + std::ignore = getParticleSimulationShader(); + std::ignore = getParticleRenderShader(); } void ShaderPrograms::resetAll() @@ -67,6 +76,10 @@ void ShaderPrograms::resetAll() m_point.reset(); m_blit.reset(); m_fullscreen.reset(); + m_atmosphere.reset(); + m_timeOfDay.reset(); + m_particleSimulation.reset(); + m_particleRender.reset(); } // essentially a private member of ShaderPrograms @@ -143,4 +156,54 @@ const std::shared_ptr &ShaderPrograms::getFullScreenShader() return getInitialized(m_fullscreen, getFunctions(), "fullscreen"); } +const std::shared_ptr &ShaderPrograms::getAtmosphereShader() +{ + return getInitialized(m_atmosphere, getFunctions(), "weather/atmosphere"); +} + +const std::shared_ptr &ShaderPrograms::getTimeOfDayShader() +{ + return getInitialized(m_timeOfDay, getFunctions(), "weather/timeofday"); +} + +const std::shared_ptr &ShaderPrograms::getParticleSimulationShader() +{ + if (!m_particleSimulation) { + Functions &funcs = getFunctions(); + const auto getSource = [&funcs](const std::string &path) -> ShaderUtils::Source { + const auto fullPathName = ":/shaders/legacy/weather/simulation/" + path; + std::string src; + try { + src = ::readWholeResourceFile(fullPathName); + } catch (...) { + // Fallback for frag.glsl if file missing, as some drivers require it + if (path == "frag.glsl") { + src = std::string(funcs.getShaderVersion()) + + "\nprecision mediump float;\nvoid main() {}\n"; + } else { + throw; + } + } + return ShaderUtils::Source{fullPathName, src}; + }; + + std::vector varyings = {"vPos", "vLife"}; + auto program = ShaderUtils::loadTransformFeedbackShaders(funcs, + getSource("vert.glsl"), + getSource("frag.glsl"), + varyings); + m_particleSimulation = std::make_shared("weather/simulation", + funcs.shared_from_this(), + std::move(program)); + } + return m_particleSimulation; +} + +const std::shared_ptr &ShaderPrograms::getParticleRenderShader() +{ + return getInitialized(m_particleRender, + getFunctions(), + "weather/particle"); +} + } // namespace Legacy diff --git a/src/opengl/legacy/Shaders.h b/src/opengl/legacy/Shaders.h index db5b2d2cc..f83ddfe46 100644 --- a/src/opengl/legacy/Shaders.h +++ b/src/opengl/legacy/Shaders.h @@ -156,6 +156,60 @@ struct NODISCARD FullScreenShader final : public AbstractShaderProgram } }; +struct NODISCARD AtmosphereShader final : public AbstractShaderProgram +{ +public: + using AbstractShaderProgram::AbstractShaderProgram; + + ~AtmosphereShader() final; + +private: + void virt_setUniforms(const glm::mat4 & /*mvp*/, + const GLRenderState::Uniforms & /*uniforms*/) final + { + setTexture("uTexture", 0); + } +}; + +struct NODISCARD TimeOfDayShader final : public AbstractShaderProgram +{ +public: + using AbstractShaderProgram::AbstractShaderProgram; + + ~TimeOfDayShader() final; + +private: + void virt_setUniforms(const glm::mat4 & /*mvp*/, + const GLRenderState::Uniforms & /*uniforms*/) final + {} +}; + +struct NODISCARD ParticleSimulationShader final : public AbstractShaderProgram +{ +public: + using AbstractShaderProgram::AbstractShaderProgram; + + ~ParticleSimulationShader() final; + +private: + void virt_setUniforms(const glm::mat4 & /*mvp*/, + const GLRenderState::Uniforms & /*uniforms*/) final + {} +}; + +struct NODISCARD ParticleRenderShader final : public AbstractShaderProgram +{ +public: + using AbstractShaderProgram::AbstractShaderProgram; + + ~ParticleRenderShader() final; + +private: + void virt_setUniforms(const glm::mat4 & /*mvp*/, + const GLRenderState::Uniforms & /*uniforms*/) final + {} +}; + /* owned by Functions */ struct NODISCARD ShaderPrograms final { @@ -176,6 +230,10 @@ struct NODISCARD ShaderPrograms final std::shared_ptr m_point; std::shared_ptr m_blit; std::shared_ptr m_fullscreen; + std::shared_ptr m_atmosphere; + std::shared_ptr m_timeOfDay; + std::shared_ptr m_particleSimulation; + std::shared_ptr m_particleRender; public: explicit ShaderPrograms(Functions &functions) @@ -208,6 +266,10 @@ struct NODISCARD ShaderPrograms final NODISCARD const std::shared_ptr &getPointShader(); NODISCARD const std::shared_ptr &getBlitShader(); NODISCARD const std::shared_ptr &getFullScreenShader(); + NODISCARD const std::shared_ptr &getAtmosphereShader(); + NODISCARD const std::shared_ptr &getTimeOfDayShader(); + NODISCARD const std::shared_ptr &getParticleSimulationShader(); + NODISCARD const std::shared_ptr &getParticleRenderShader(); public: void early_init(); diff --git a/src/opengl/legacy/TFO.cpp b/src/opengl/legacy/TFO.cpp new file mode 100644 index 000000000..53ed37974 --- /dev/null +++ b/src/opengl/legacy/TFO.cpp @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2026 The MMapper Authors + +#include "TFO.h" + +#include "Legacy.h" + +namespace Legacy { + +void TFO::emplace(const SharedFunctions &sharedFunctions) +{ + reset(); + m_weakFunctions = sharedFunctions; + sharedFunctions->glGenTransformFeedbacks(1, &m_tfo); +} + +void TFO::reset() +{ + if (m_tfo != INVALID_TFO) { + if (auto shared = m_weakFunctions.lock()) { + shared->glDeleteTransformFeedbacks(1, &m_tfo); + } + m_tfo = INVALID_TFO; + } +} + +GLuint TFO::get() const +{ + return m_tfo; +} + +} // namespace Legacy diff --git a/src/opengl/legacy/TFO.h b/src/opengl/legacy/TFO.h new file mode 100644 index 000000000..c34088676 --- /dev/null +++ b/src/opengl/legacy/TFO.h @@ -0,0 +1,36 @@ +#pragma once +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2026 The MMapper Authors + +#include "Legacy.h" + +#include + +namespace Legacy { + +class NODISCARD TFO final +{ +private: + static inline constexpr GLuint INVALID_TFO = 0; + WeakFunctions m_weakFunctions; + GLuint m_tfo = INVALID_TFO; + +public: + TFO() = default; + ~TFO() { reset(); } + + DELETE_CTORS_AND_ASSIGN_OPS(TFO); + +public: + void emplace(const SharedFunctions &sharedFunctions); + void reset(); + NODISCARD GLuint get() const; + +public: + NODISCARD explicit operator bool() const { return m_tfo != INVALID_TFO; } +}; + +using SharedTfo = std::shared_ptr; +using WeakTfo = std::weak_ptr; + +} // namespace Legacy diff --git a/src/opengl/legacy/WeatherMeshes.cpp b/src/opengl/legacy/WeatherMeshes.cpp new file mode 100644 index 000000000..267f0dd55 --- /dev/null +++ b/src/opengl/legacy/WeatherMeshes.cpp @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2026 The MMapper Authors + +#include "WeatherMeshes.h" + +#include "../../global/random.h" +#include "../Weather.h" +#include "AttributeLessMeshes.h" +#include "Binders.h" + +namespace Legacy { + +AtmosphereMesh::AtmosphereMesh(SharedFunctions sharedFunctions, + std::shared_ptr program) + : FullScreenMesh(std::move(sharedFunctions), std::move(program), GL_TRIANGLE_STRIP, 4) +{} + +AtmosphereMesh::~AtmosphereMesh() = default; + +TimeOfDayMesh::~TimeOfDayMesh() = default; + +ParticleSimulationMesh::ParticleSimulationMesh(SharedFunctions shared_functions, + std::shared_ptr program) + : m_shared_functions(std::move(shared_functions)) + , m_functions(deref(m_shared_functions)) + , m_program(std::move(program)) +{ + m_tfo.emplace(m_shared_functions); + for (int i = 0; i < 2; ++i) { + m_vbos[i].emplace(m_shared_functions); + m_vaos[i].emplace(m_shared_functions); + } +} + +ParticleSimulationMesh::~ParticleSimulationMesh() = default; + +void ParticleSimulationMesh::init() +{ + if (m_initialized) { + return; + } + + auto get_random_float = []() { return static_cast(getRandom(1000000)) / 1000000.0f; }; + + std::vector initialData; + initialData.reserve(m_numParticles); + for (size_t i = 0; i < m_numParticles; ++i) { + initialData.emplace_back(glm::vec2(get_random_float() * WeatherConstants::WEATHER_EXTENT + - WeatherConstants::WEATHER_RADIUS, + get_random_float() * WeatherConstants::WEATHER_EXTENT + - WeatherConstants::WEATHER_RADIUS), + get_random_float()); + } + + for (int i = 0; i < 2; ++i) { + if (!m_vbos[i]) { + m_vbos[i].emplace(m_shared_functions); + } + m_functions.glBindBuffer(GL_ARRAY_BUFFER, m_vbos[i].get()); + m_functions.glBufferData(GL_ARRAY_BUFFER, + static_cast(initialData.size() + * sizeof(WeatherParticleVert)), + initialData.data(), + GL_STREAM_DRAW); + + if (!m_vaos[i]) { + m_vaos[i].emplace(m_shared_functions); + } + m_functions.glBindVertexArray(m_vaos[i].get()); + m_functions.enableAttrib(0, + 2, + GL_FLOAT, + GL_FALSE, + sizeof(WeatherParticleVert), + reinterpret_cast(offsetof(WeatherParticleVert, pos))); + m_functions.enableAttrib(1, + 1, + GL_FLOAT, + GL_FALSE, + sizeof(WeatherParticleVert), + reinterpret_cast(offsetof(WeatherParticleVert, life))); + m_functions.glBindVertexArray(0); + m_functions.glBindBuffer(GL_ARRAY_BUFFER, 0); + } + + m_initialized = true; +} + +void ParticleSimulationMesh::virt_reset() +{ + // Note: TFO is not reset here as it's only created in the constructor + // and can be reused. + for (int i = 0; i < 2; ++i) { + m_vbos[i].reset(); + m_vaos[i].reset(); + } + m_initialized = false; +} + +void ParticleSimulationMesh::virt_render(const GLRenderState &renderState) +{ + init(); + + auto binder = m_program->bind(); + const glm::mat4 mvp = renderState.mvp.value_or(m_functions.getProjectionMatrix()); + m_program->setUniforms(mvp, renderState.uniforms); + + const uint32_t bufferOut = 1 - m_currentBuffer; + + m_functions.glBindVertexArray(m_vaos[m_currentBuffer].get()); + m_functions.glEnable(GL_RASTERIZER_DISCARD); + m_functions.glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, m_tfo.get()); + m_functions.glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, m_vbos[bufferOut].get()); + + { + m_functions.glBeginTransformFeedback(GL_POINTS); + m_functions.glDrawArrays(GL_POINTS, 0, static_cast(m_numParticles)); + m_functions.glEndTransformFeedback(); + } + m_functions.glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0); + m_functions.glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, 0); + m_functions.glDisable(GL_RASTERIZER_DISCARD); + m_functions.glBindVertexArray(0); + + m_currentBuffer = bufferOut; +} + +ParticleRenderMesh::ParticleRenderMesh(SharedFunctions shared_functions, + std::shared_ptr program, + const ParticleSimulationMesh &simulation) + : m_shared_functions(std::move(shared_functions)) + , m_functions(deref(m_shared_functions)) + , m_program(std::move(program)) + , m_simulation(simulation) +{ + for (int i = 0; i < 2; ++i) { + m_vaos[i].emplace(m_shared_functions); + } +} + +ParticleRenderMesh::~ParticleRenderMesh() = default; + +void ParticleRenderMesh::init() +{ + if (m_initialized || m_simulation.virt_isEmpty()) { + return; + } + + for (uint32_t i = 0; i < 2; ++i) { + if (!m_vaos[i]) { + m_vaos[i].emplace(m_shared_functions); + } + m_functions.glBindVertexArray(m_vaos[i].get()); + + m_functions.glBindBuffer(GL_ARRAY_BUFFER, m_simulation.getParticleVbo(i).get()); + m_functions.enableAttrib(0, + 2, + GL_FLOAT, + GL_FALSE, + sizeof(WeatherParticleVert), + reinterpret_cast(offsetof(WeatherParticleVert, pos))); + m_functions.glVertexAttribDivisor(0, 1); + m_functions.enableAttrib(1, + 1, + GL_FLOAT, + GL_FALSE, + sizeof(WeatherParticleVert), + reinterpret_cast(offsetof(WeatherParticleVert, life))); + m_functions.glVertexAttribDivisor(1, 1); + } + + m_functions.glBindVertexArray(0); + m_functions.glBindBuffer(GL_ARRAY_BUFFER, 0); + m_initialized = true; +} + +void ParticleRenderMesh::virt_reset() +{ + for (int i = 0; i < 2; ++i) { + m_vaos[i].reset(); + } + m_initialized = false; +} + +bool ParticleRenderMesh::virt_isEmpty() const +{ + return m_simulation.virt_isEmpty(); +} + +void ParticleRenderMesh::virt_render(const GLRenderState &renderState) +{ + init(); + if (!m_initialized) { + return; + } + + // Thinning: use precipitation intensity to drive instance count + if (m_intensity <= 0.0f) { + return; + } + const GLsizei maxCount = static_cast(m_simulation.getNumParticles()); + const GLsizei count = std::max(1, + static_cast(m_intensity + * static_cast(maxCount))); + + auto binder = m_program->bind(); + const glm::mat4 mvp = renderState.mvp.value_or(m_functions.getProjectionMatrix()); + m_program->setUniforms(mvp, renderState.uniforms); + + RenderStateBinder rsBinder(m_functions, m_functions.getTexLookup(), renderState); + + const uint32_t bufferIdx = m_simulation.getCurrentBuffer(); + m_functions.glBindVertexArray(m_vaos[bufferIdx].get()); + m_functions.glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, count); + m_functions.glBindVertexArray(0); +} + +} // namespace Legacy diff --git a/src/opengl/legacy/WeatherMeshes.h b/src/opengl/legacy/WeatherMeshes.h new file mode 100644 index 000000000..fba15b9a4 --- /dev/null +++ b/src/opengl/legacy/WeatherMeshes.h @@ -0,0 +1,93 @@ +#pragma once +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2026 The MMapper Authors + +#include "../OpenGL.h" +#include "../OpenGLTypes.h" +#include "AttributeLessMeshes.h" +#include "Binders.h" +#include "Legacy.h" +#include "Shaders.h" +#include "TFO.h" +#include "VAO.h" +#include "VBO.h" + +namespace Legacy { + +class NODISCARD AtmosphereMesh final : public FullScreenMesh +{ +public: + explicit AtmosphereMesh(SharedFunctions sharedFunctions, + std::shared_ptr program); + ~AtmosphereMesh() override; +}; + +class NODISCARD TimeOfDayMesh final : public FullScreenMesh +{ +public: + using FullScreenMesh::FullScreenMesh; + ~TimeOfDayMesh() override; +}; + +class NODISCARD ParticleSimulationMesh final : public IRenderable +{ +private: + const SharedFunctions m_shared_functions; + Functions &m_functions; + const std::shared_ptr m_program; + + TFO m_tfo; + VBO m_vbos[2]; + VAO m_vaos[2]; + + uint32_t m_currentBuffer = 0; + uint32_t m_numParticles = 512; + bool m_initialized = false; + +public: + explicit ParticleSimulationMesh(SharedFunctions shared_functions, + std::shared_ptr program); + ~ParticleSimulationMesh() override; + +private: + void init(); + void virt_clear() override {} + void virt_reset() override; + void virt_render(const GLRenderState &renderState) override; + +public: + NODISCARD bool virt_isEmpty() const override { return !m_initialized; } + NODISCARD uint32_t getCurrentBuffer() const { return m_currentBuffer; } + NODISCARD uint32_t getNumParticles() const { return m_numParticles; } + NODISCARD const VBO &getParticleVbo(const uint32_t index) const { return m_vbos[index]; } +}; + +class NODISCARD ParticleRenderMesh final : public IRenderable +{ +private: + const SharedFunctions m_shared_functions; + Functions &m_functions; + const std::shared_ptr m_program; + const ParticleSimulationMesh &m_simulation; + + VAO m_vaos[2]; + float m_intensity = 0.0f; + bool m_initialized = false; + +public: + explicit ParticleRenderMesh(SharedFunctions shared_functions, + std::shared_ptr program, + const ParticleSimulationMesh &simulation); + ~ParticleRenderMesh() override; + + void setIntensity(const float intensity) { m_intensity = intensity; } + +private: + void init(); + void virt_clear() override {} + void virt_reset() override; + NODISCARD bool virt_isEmpty() const override; + void virt_render(const GLRenderState &renderState) override; +}; + +} // namespace Legacy diff --git a/src/preferences/AdvancedGraphics.cpp b/src/preferences/AdvancedGraphics.cpp index c4c10e776..de850c0cd 100644 --- a/src/preferences/AdvancedGraphics.cpp +++ b/src/preferences/AdvancedGraphics.cpp @@ -49,31 +49,32 @@ class NODISCARD FpSpinBox final : public QDoubleSpinBox private: using FP = FixedPoint; FP &m_fp; + const double m_multiplier; + const double m_fraction; public: explicit FpSpinBox(FixedPoint &fp) : QDoubleSpinBox() , m_fp{fp} + , m_multiplier{std::pow(10.0, static_cast(FP::digits))} + , m_fraction{1.0 / m_multiplier} { - const double fraction = std::pow(10.0, -FP::digits); - setRange(static_cast(m_fp.min) * fraction, static_cast(m_fp.max) * fraction); + setRange(static_cast(m_fp.min) * m_fraction, + static_cast(m_fp.max) * m_fraction); setValue(m_fp.getDouble()); setDecimals(FP::digits); - setSingleStep(fraction); + setSingleStep(m_fraction); } ~FpSpinBox() final = default; public: NODISCARD int getIntValue() const { - return static_cast(std::lround(std::clamp(value() * std::pow(10.0, FP::digits), + return static_cast(std::lround(std::clamp(value() * m_multiplier, static_cast(m_fp.min), static_cast(m_fp.max)))); } - void setIntValue(int value) - { - setValue(static_cast(value) * std::pow(10.0, -FP::digits)); - } + void setIntValue(int value) { setValue(static_cast(value) * m_fraction); } }; template diff --git a/src/preferences/graphicspage.cpp b/src/preferences/graphicspage.cpp index 1a2e09bb7..4305087c5 100644 --- a/src/preferences/graphicspage.cpp +++ b/src/preferences/graphicspage.cpp @@ -85,6 +85,21 @@ GraphicsPage::GraphicsPage(QWidget *parent) this, &GraphicsPage::slot_drawUpperLayersTexturedStateChanged); + connect(ui->weatherAtmosphereSlider, &QSlider::valueChanged, this, [this](int value) { + setConfig().canvas.weatherAtmosphereIntensity.set(value); + graphicsSettingsChanged(); + }); + + connect(ui->weatherPrecipitationSlider, &QSlider::valueChanged, this, [this](int value) { + setConfig().canvas.weatherPrecipitationIntensity.set(value); + graphicsSettingsChanged(); + }); + + connect(ui->weatherTimeOfDaySlider, &QSlider::valueChanged, this, [this](int value) { + setConfig().canvas.weatherTimeOfDayIntensity.set(value); + graphicsSettingsChanged(); + }); + connect(m_advanced.get(), &AdvancedGraphicsGroupBox::sig_graphicsSettingsChanged, this, @@ -126,6 +141,10 @@ void GraphicsPage::slot_loadConfig() ui->drawNeedsUpdate->setChecked(settings.showMissingMapId.get()); ui->drawNotMappedExits->setChecked(settings.showUnmappedExits.get()); ui->drawDoorNames->setChecked(settings.drawDoorNames); + + ui->weatherAtmosphereSlider->setValue(settings.weatherAtmosphereIntensity.get()); + ui->weatherPrecipitationSlider->setValue(settings.weatherPrecipitationIntensity.get()); + ui->weatherTimeOfDaySlider->setValue(settings.weatherTimeOfDayIntensity.get()); } void GraphicsPage::changeColorClicked(XNamedColor &namedColor, QPushButton *const pushButton) diff --git a/src/preferences/graphicspage.ui b/src/preferences/graphicspage.ui index 157c9ad89..281354fea 100644 --- a/src/preferences/graphicspage.ui +++ b/src/preferences/graphicspage.ui @@ -297,6 +297,66 @@ + + + + Weather and Atmosphere + + + + + + Atmosphere Intensity: + + + + + + + Qt::Horizontal + + + 100 + + + + + + + Precipitation Intensity: + + + + + + + Qt::Horizontal + + + 100 + + + + + + + Time of Day Intensity: + + + + + + + Qt::Horizontal + + + 100 + + + + + + diff --git a/src/resources/mmapper2.qrc b/src/resources/mmapper2.qrc index 6a703d7ef..c9d4c0e84 100644 --- a/src/resources/mmapper2.qrc +++ b/src/resources/mmapper2.qrc @@ -229,5 +229,13 @@ shaders/legacy/blit/vert.glsl shaders/legacy/fullscreen/frag.glsl shaders/legacy/fullscreen/vert.glsl + shaders/legacy/weather/atmosphere/frag.glsl + shaders/legacy/weather/atmosphere/vert.glsl + shaders/legacy/weather/particle/frag.glsl + shaders/legacy/weather/particle/vert.glsl + shaders/legacy/weather/simulation/frag.glsl + shaders/legacy/weather/simulation/vert.glsl + shaders/legacy/weather/timeofday/frag.glsl + shaders/legacy/weather/timeofday/vert.glsl diff --git a/src/resources/shaders/legacy/weather/atmosphere/frag.glsl b/src/resources/shaders/legacy/weather/atmosphere/frag.glsl new file mode 100644 index 000000000..39f53ee29 --- /dev/null +++ b/src/resources/shaders/legacy/weather/atmosphere/frag.glsl @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2026 The MMapper Authors + +precision highp float; + +layout(std140) uniform NamedColorsBlock +{ + vec4 uNamedColors[MAX_NAMED_COLORS]; +}; + +layout(std140) uniform CameraBlock +{ + mat4 uViewProj; + vec4 uPlayerPos; // xyz, w=zScale +}; + +layout(std140) uniform TimeBlock +{ + vec4 uTime; // x=time, y=delta, zw=unused +}; + +layout(std140) uniform WeatherBlock +{ + vec4 uIntensities; // rain_start, snow_start, clouds_start, fog_start + vec4 uTargets; // rain_target, snow_target, clouds_target, fog_target + vec4 uTimeOfDay; // x=startIdx, y=targetIdx, z=todStart, w=todTarget + vec4 uConfig; // x=weatherStartTime, y=todStartTime, z=duration, w=unused +}; + +uniform sampler2D uTexture; + +in vec3 vWorldPos; +out vec4 vFragmentColor; + +float get_noise(vec2 p) +{ + vec2 size = vec2(textureSize(uTexture, 0)); + return texture(uTexture, p / size).r; +} + +float fbm(vec2 p) +{ + float v = 0.0; + float a = 0.5; + vec2 shift = vec2(100.0); + for (int i = 0; i < 4; ++i) { + v += a * get_noise(p); + p = p * 2.0 + shift; + a *= 0.5; + } + return v; +} + +void main() +{ + vec3 worldPos = vWorldPos; + + float distToPlayer = distance(worldPos.xy, uPlayerPos.xy); + float localMask = 1.0 - smoothstep(WEATHER_MASK_RADIUS_INNER, WEATHER_MASK_RADIUS_OUTER, distToPlayer); + + float uCurrentTime = uTime.x; + float uWeatherStartTime = uConfig.x; + float uTimeOfDayStartTime = uConfig.y; + float uTransitionDuration = uConfig.z; + + float weatherLerp = clamp((uCurrentTime - uWeatherStartTime) / uTransitionDuration, 0.0, 1.0); + float uCloudsIntensity = mix(uIntensities[2], uTargets[2], weatherLerp); + float uFogIntensity = mix(uIntensities[3], uTargets[3], weatherLerp); + + float timeOfDayLerp = clamp((uCurrentTime - uTimeOfDayStartTime) / uTransitionDuration, + 0.0, + 1.0); + float currentTimeOfDayIntensity = mix(uTimeOfDay.z, uTimeOfDay.w, timeOfDayLerp); + vec4 timeOfDayStart = uNamedColors[int(uTimeOfDay.x)]; + vec4 timeOfDayTarget = uNamedColors[int(uTimeOfDay.y)]; + vec4 uTimeOfDayColor = mix(timeOfDayStart, timeOfDayTarget, timeOfDayLerp); + uTimeOfDayColor.a *= currentTimeOfDayIntensity; + + // Atmosphere overlay is now transparent by default (TimeOfDay is drawn separately) + vec4 result = vec4(0.0); + + // Fog: soft drifting noise + if (uFogIntensity > 0.0) { + float n = fbm(worldPos.xy * 0.15 + uCurrentTime * 0.1); + // Density increases non-linearly with intensity + float density = 0.4 + uFogIntensity * 0.4; + vec4 fog = vec4(0.8, 0.8, 0.85, uFogIntensity * n * localMask * density); + // Emissive boost at night + fog.rgb += uTimeOfDayColor.a * 0.15; + + // Blend fog over result + float combinedAlpha = 1.0 - (1.0 - result.a) * (1.0 - fog.a); + result.rgb = (result.rgb * result.a * (1.0 - fog.a) + fog.rgb * fog.a) + / max(combinedAlpha, 0.001); + result.a = combinedAlpha; + } + + // Clouds: puffy high-contrast noise + if (uCloudsIntensity > 0.0) { + float n = fbm(worldPos.xy * 0.06 - uCurrentTime * 0.03); + float puffy = smoothstep(0.45, 0.55, n); + // Clouds get darker and more "stormy" as intensity increases + float storminess = 1.0 - uCloudsIntensity * 0.4; + vec4 clouds = vec4(0.9 * storminess, + 0.9 * storminess, + 1.0 * storminess, + uCloudsIntensity * puffy * localMask * 0.5); + // Emissive boost at night + clouds.rgb += uTimeOfDayColor.a * 0.1; + + // Blend clouds over result + float combinedAlpha = 1.0 - (1.0 - result.a) * (1.0 - clouds.a); + result.rgb = (result.rgb * result.a * (1.0 - clouds.a) + clouds.rgb * clouds.a) + / max(combinedAlpha, 0.001); + result.a = combinedAlpha; + } + + vFragmentColor = result; +} diff --git a/src/resources/shaders/legacy/weather/atmosphere/vert.glsl b/src/resources/shaders/legacy/weather/atmosphere/vert.glsl new file mode 100644 index 000000000..1671aede1 --- /dev/null +++ b/src/resources/shaders/legacy/weather/atmosphere/vert.glsl @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2026 The MMapper Authors + +layout(std140) uniform CameraBlock +{ + mat4 uViewProj; + vec4 uPlayerPos; // xyz, w=zScale +}; + +out vec3 vWorldPos; + +void main() +{ + // Quad vertices 0..3 for TRIANGLE_STRIP + // 0: (-1, -1), 1: (1, -1), 2: (-1, 1), 3: (1, 1) + vec2 offset = vec2(float(gl_VertexID & 1) * 2.0 - 1.0, + float((gl_VertexID >> 1) & 1) * 2.0 - 1.0); + + // Large enough to cover the mask radius (WEATHER_RADIUS is currently 14.0) + vec2 worldXY = uPlayerPos.xy + offset * WEATHER_RADIUS; + vWorldPos = vec3(worldXY, uPlayerPos.z); + + // Project to screen space + gl_Position = uViewProj * vec4(vWorldPos.x, vWorldPos.y, vWorldPos.z * uPlayerPos.w, 1.0); +} diff --git a/src/resources/shaders/legacy/weather/particle/frag.glsl b/src/resources/shaders/legacy/weather/particle/frag.glsl new file mode 100644 index 000000000..212592f00 --- /dev/null +++ b/src/resources/shaders/legacy/weather/particle/frag.glsl @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2026 The MMapper Authors + +precision highp float; + +layout(std140) uniform NamedColorsBlock +{ + vec4 uNamedColors[MAX_NAMED_COLORS]; +}; + +layout(std140) uniform TimeBlock +{ + vec4 uTime; // x=time, y=delta, zw=unused +}; + +layout(std140) uniform WeatherBlock +{ + vec4 uIntensities; // rain_start, snow_start, clouds_start, fog_start + vec4 uTargets; // rain_target, snow_target, clouds_target, fog_target + vec4 uTimeOfDay; // x=startIdx, y=targetIdx, z=todStart, w=todTarget + vec4 uConfig; // x=weatherStartTime, y=todStartTime, z=duration, w=unused +}; + +in float vLife; +in vec2 vLocalCoord; +in float vLocalMask; + +out vec4 vFragmentColor; + +void main() +{ + float uCurrentTime = uTime.x; + float uWeatherStartTime = uConfig.x; + float uTimeOfDayStartTime = uConfig.y; + float uTransitionDuration = uConfig.z; + + float weatherLerp = clamp((uCurrentTime - uWeatherStartTime) / uTransitionDuration, 0.0, 1.0); + float pRain = mix(uIntensities.x, uTargets.x, weatherLerp); + float pSnow = mix(uIntensities.y, uTargets.y, weatherLerp); + float pIntensity = max(pRain, pSnow); + float pType = pSnow / max(pIntensity, 0.001); + + float timeOfDayLerp = clamp((uCurrentTime - uTimeOfDayStartTime) / uTransitionDuration, + 0.0, + 1.0); + float currentTimeOfDayIntensity = mix(uTimeOfDay.z, uTimeOfDay.w, timeOfDayLerp); + vec4 timeOfDayStart = uNamedColors[int(uTimeOfDay.x)]; + vec4 timeOfDayTarget = uNamedColors[int(uTimeOfDay.y)]; + vec4 uTimeOfDayColor = mix(timeOfDayStart, timeOfDayTarget, timeOfDayLerp); + uTimeOfDayColor.a *= currentTimeOfDayIntensity; + + float lifeFade = smoothstep(0.0, 0.15, vLife) * smoothstep(1.0, 0.85, vLife); + + // Rain visuals + float streak = 1.0 - smoothstep(0.0, 0.15, abs(vLocalCoord.x - 0.5)); + float rainAlpha = mix(0.4, 0.7, clamp(pIntensity, 0.0, 1.0)); + vec4 rainColor = vec4(0.6, 0.6, 1.0, pIntensity * streak * vLocalMask * rainAlpha * lifeFade); + rainColor.rgb += uTimeOfDayColor.a * 0.2; + + // Snow visuals + float dist = distance(vLocalCoord, vec2(0.5)); + float flake = 1.0 - smoothstep(0.1, 0.2, dist); + float snowAlpha = mix(0.6, 0.9, clamp(pIntensity, 0.0, 1.0)); + vec4 snowColor = vec4(1.0, 1.0, 1.1, pIntensity * flake * vLocalMask * snowAlpha * lifeFade); + snowColor.rgb += uTimeOfDayColor.a * 0.3; + + // Interpolate visuals based on pType + vec4 pColor = mix(rainColor, snowColor, pType); + + if (pColor.a <= 0.0) { + discard; + } + + vFragmentColor = pColor; +} diff --git a/src/resources/shaders/legacy/weather/particle/vert.glsl b/src/resources/shaders/legacy/weather/particle/vert.glsl new file mode 100644 index 000000000..7e2bafd31 --- /dev/null +++ b/src/resources/shaders/legacy/weather/particle/vert.glsl @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2026 The MMapper Authors + +layout(location = 0) in vec2 aParticlePos; +layout(location = 1) in float aLife; + +layout(std140) uniform CameraBlock +{ + mat4 uViewProj; + vec4 uPlayerPos; // xyz, w=zScale +}; + +layout(std140) uniform TimeBlock +{ + vec4 uTime; // x=time, y=delta, zw=unused +}; + +layout(std140) uniform WeatherBlock +{ + vec4 uIntensities; // rain_start, snow_start, clouds_start, fog_start + vec4 uTargets; // rain_target, snow_target, clouds_target, fog_target + vec4 uTimeOfDay; // x=startIdx, y=targetIdx, z=todStart, w=todTarget + vec4 uConfig; // x=weatherStartTime, y=todStartTime, z=duration, w=unused +}; + +out float vLife; +out vec2 vLocalCoord; +out float vLocalMask; + +float rand(float n) +{ + return fract(sin(n) * 43758.5453123); +} + +void main() +{ + float uCurrentTime = uTime.x; + float uZScale = uPlayerPos.w; + float uWeatherStartTime = uConfig.x; + float uTransitionDuration = uConfig.z; + + float weatherLerp = clamp((uCurrentTime - uWeatherStartTime) / uTransitionDuration, 0.0, 1.0); + float pRain = mix(uIntensities.x, uTargets.x, weatherLerp); + float pSnow = mix(uIntensities.y, uTargets.y, weatherLerp); + float pIntensity = max(pRain, pSnow); + float pType = pSnow / max(pIntensity, 0.001); + + float hash = rand(float(gl_InstanceID)); + + // Generate quad position from gl_VertexID (0..3) + vec2 quadPos = vec2(float(gl_VertexID & 1) - 0.5, float((gl_VertexID >> 1) & 1) - 0.5); + + vLife = aLife; + vLocalCoord = quadPos + 0.5; + + // Rain size + vec2 rainSize = vec2(1.0 / 12.0, 1.0 / (0.25 - clamp(pIntensity, 0.0, 2.0) * 0.1)); + // Snow size + vec2 snowSize = vec2(1.0 / 4.0, 1.0 / 4.0); + + vec2 size = mix(rainSize, snowSize, pType); + vec2 pos = aParticlePos; + + // Extra swaying for snow visuals + pos.x += sin(uCurrentTime * 1.2 + hash * 6.28) * 0.4 * pType; + + vec3 worldPos = vec3(pos + quadPos * size, uPlayerPos.z); + + float distToPlayer = distance(pos, uPlayerPos.xy); + vLocalMask = 1.0 - smoothstep(WEATHER_MASK_RADIUS_INNER, WEATHER_MASK_RADIUS_OUTER, distToPlayer); + + gl_Position = uViewProj * vec4(worldPos.x, worldPos.y, worldPos.z * uZScale, 1.0); +} diff --git a/src/resources/shaders/legacy/weather/simulation/frag.glsl b/src/resources/shaders/legacy/weather/simulation/frag.glsl new file mode 100644 index 000000000..728041d70 --- /dev/null +++ b/src/resources/shaders/legacy/weather/simulation/frag.glsl @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2026 The MMapper Authors + +precision highp float; + +void main() +{ + // Dummy fragment shader for WebGL2 compatibility +} diff --git a/src/resources/shaders/legacy/weather/simulation/vert.glsl b/src/resources/shaders/legacy/weather/simulation/vert.glsl new file mode 100644 index 000000000..e5b1c7525 --- /dev/null +++ b/src/resources/shaders/legacy/weather/simulation/vert.glsl @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2026 The MMapper Authors + +layout(location = 0) in vec2 aPos; +layout(location = 1) in float aLife; + +layout(std140) uniform CameraBlock +{ + mat4 uViewProj; + vec4 uPlayerPos; // xyz, w=zScale +}; + +layout(std140) uniform TimeBlock +{ + vec4 uTime; // x=time, y=delta, zw=unused +}; + +layout(std140) uniform WeatherBlock +{ + vec4 uIntensities; // rain_start, snow_start, clouds_start, fog_start + vec4 uTargets; // rain_target, snow_target, clouds_target, fog_target + vec4 uTimeOfDay; // x=startIdx, y=targetIdx, z=todStart, w=todTarget + vec4 uConfig; // x=weatherStartTime, y=todStartTime, z=duration, w=unused +}; + +out vec2 vPos; +out float vLife; + +float hash21(vec2 p) +{ + return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); +} + +float rand(float n) +{ + return fract(sin(n) * 43758.5453123); +} + +void main() +{ + float hash = rand(float(gl_VertexID)); + float uCurrentTime = uTime.x; + float uDeltaTime = uTime.y; + float uWeatherStartTime = uConfig.x; + float uTransitionDuration = uConfig.z; + + float weatherLerp = clamp((uCurrentTime - uWeatherStartTime) / uTransitionDuration, 0.0, 1.0); + float pRain = mix(uIntensities.x, uTargets.x, weatherLerp); + float pSnow = mix(uIntensities.y, uTargets.y, weatherLerp); + float pIntensity = max(pRain, pSnow); + float pType = pSnow / max(pIntensity, 0.001); // 0=rain, 1=snow + + vec2 pos = aPos; + float life = aLife; + + // Rain physics + float rainSpeed = (15.0 + pIntensity * 10.0) + hash * 5.0; + float rainDecay = 0.35 + hash * 0.15; + + // Snow physics + float snowSpeed = (0.75 + pIntensity * 0.75) + hash * 0.5; + float snowDecay = 0.015 + hash * 0.01; + + // Interpolate physics based on pType + float speed = mix(rainSpeed, snowSpeed, pType); + float decay = mix(rainDecay, snowDecay, pType); + + pos.y -= uDeltaTime * speed; + + // Horizontal swaying (only for snow) + pos.x += sin(uCurrentTime * 1.2 + hash * 6.28) * (0.1 + pIntensity * 0.2) * pType * uDeltaTime; + + life -= uDeltaTime * decay; + + // Spatial fading + float hole = hash21(floor(pos.xy * 4.0)); + if (hole < 0.02) { + life -= uDeltaTime * 2.0; // Expire quickly but not instantly + } + + if (life <= 0.0) { + life = 1.0; + // Respawn at the top + float r1 = rand(hash + uCurrentTime); + pos.x = uPlayerPos.x + (r1 * WEATHER_EXTENT - WEATHER_RADIUS); + pos.y = uPlayerPos.y + WEATHER_RADIUS; + } else { + // Toroidal wrap around player (WEATHER_EXTENT x WEATHER_EXTENT area) + vec2 rel = pos - uPlayerPos.xy; + if (rel.x < -WEATHER_RADIUS) + pos.x += WEATHER_EXTENT; + else if (rel.x > WEATHER_RADIUS) + pos.x -= WEATHER_EXTENT; + + if (rel.y < -WEATHER_RADIUS) + pos.y += WEATHER_EXTENT; + else if (rel.y > WEATHER_RADIUS) + pos.y -= WEATHER_EXTENT; + } + + vPos = pos; + vLife = life; + gl_Position = vec4(vPos, 0.0, 1.0); +} diff --git a/src/resources/shaders/legacy/weather/timeofday/frag.glsl b/src/resources/shaders/legacy/weather/timeofday/frag.glsl new file mode 100644 index 000000000..b6bfb136b --- /dev/null +++ b/src/resources/shaders/legacy/weather/timeofday/frag.glsl @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2026 The MMapper Authors + +precision highp float; + +layout(std140) uniform NamedColorsBlock +{ + vec4 uNamedColors[MAX_NAMED_COLORS]; +}; + +layout(std140) uniform TimeBlock +{ + vec4 uTime; // x=time, y=delta, zw=unused +}; + +layout(std140) uniform WeatherBlock +{ + vec4 uIntensities; // rain_start, snow_start, clouds_start, fog_start + vec4 uTargets; // rain_target, snow_target, clouds_target, fog_target + vec4 uTimeOfDay; // x=startIdx, y=targetIdx, z=todStart, w=todTarget + vec4 uConfig; // x=weatherStartTime, y=todStartTime, z=duration, w=unused +}; + +out vec4 vFragmentColor; + +void main() +{ + float uCurrentTime = uTime.x; + float uTimeOfDayStartTime = uConfig.y; + float uTransitionDuration = uConfig.z; + + vec4 timeOfDayStart = uNamedColors[int(uTimeOfDay.x)]; + vec4 timeOfDayTarget = uNamedColors[int(uTimeOfDay.y)]; + + float timeOfDayLerp = clamp((uCurrentTime - uTimeOfDayStartTime) / uTransitionDuration, + 0.0, + 1.0); + float currentTimeOfDayIntensity = mix(uTimeOfDay.z, uTimeOfDay.w, timeOfDayLerp); + + vec4 uTimeOfDayColor = mix(timeOfDayStart, timeOfDayTarget, timeOfDayLerp); + uTimeOfDayColor.a *= currentTimeOfDayIntensity; + + vFragmentColor = uTimeOfDayColor; +} diff --git a/src/resources/shaders/legacy/weather/timeofday/vert.glsl b/src/resources/shaders/legacy/weather/timeofday/vert.glsl new file mode 100644 index 000000000..b12c193cf --- /dev/null +++ b/src/resources/shaders/legacy/weather/timeofday/vert.glsl @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2026 The MMapper Authors + +void main() +{ + // Full screen triangle + gl_Position = vec4(vec2((gl_VertexID << 1) & 2, gl_VertexID & 2) * 2.0 - 1.0, 0.0, 1.0); +}