diff --git a/src/config/supplementary/propRefresher/PropRefresher.cpp b/src/config/supplementary/propRefresher/PropRefresher.cpp index b4179022d..555c7bb16 100644 --- a/src/config/supplementary/propRefresher/PropRefresher.cpp +++ b/src/config/supplementary/propRefresher/PropRefresher.cpp @@ -76,6 +76,8 @@ void CPropRefresher::refreshProp(const bool execdAsScheduled) { } if (m_propsTripped & REFRESH_BLUR_FB) { + g_pHyprRenderer->refreshBlurProvider(); + for (auto const& m : State::monitorState()->monitors()) { if (!m) continue; diff --git a/src/config/values/ConfigValues.cpp b/src/config/values/ConfigValues.cpp index 333768cd6..27545af41 100644 --- a/src/config/values/ConfigValues.cpp +++ b/src/config/values/ConfigValues.cpp @@ -235,7 +235,26 @@ std::vector> Values::getConfigValues() { * blur: */ - MS("decoration:blur:enabled", "enable kawase window background blur", true, {.refresh = Supplementary::REFRESH_BLUR_FB}), + MS("decoration:blur:enabled", "enable window background blur", true, {.refresh = Supplementary::REFRESH_BLUR_FB}), + MS("decoration:blur:variant", "set the blur variant. Blur variants enhance regular blur, but may increase GPU and CPU usage, significantly so if they are animated.", + 0, + {.min = 0, + .max = 10, + .map = + OptionMap{ + {"kawase", 0}, + {"frost", 1}, + {"ripple", 2}, + {"drops", 3}, + {"water", 4}, + {"fluid_jar", 5}, + {"prism", 6}, + {"heat_shimmer", 7}, + {"acrylic", 8}, + {"aurora", 9}, + {"haze", 10}, + }, + .refresh = Supplementary::REFRESH_BLUR_FB}), MS("decoration:blur:size", "blur size (distance)", 8, {.min = 0, .max = 100, .refresh = Supplementary::REFRESH_BLUR_FB}), MS("decoration:blur:passes", "the amount of passes to perform", 1, {.min = 0, .max = 10, .refresh = Supplementary::REFRESH_BLUR_FB}), MS("decoration:blur:ignore_opacity", "make the blur layer ignore the opacity of the window", true, {.refresh = Supplementary::REFRESH_BLUR_FB}), @@ -254,6 +273,61 @@ std::vector> Values::getConfigValues() { MS("decoration:blur:input_methods_ignorealpha", "works like ignorealpha in layer rules. If pixel opacity is below set value, will not blur.", 0.2, {.min = 0, .max = 1, .refresh = Supplementary::REFRESH_BLUR_FB}), + // specific blur stuff + MS("decoration:blur:glass:refraction", "maximum refraction displacement for glass blur types in pixels", 20.F, + {.min = 0, .max = 20, .refresh = Supplementary::REFRESH_BLUR_FB}), + MS("decoration:blur:glass:size", "pattern size for glass blur types in pixels", 40.F, {.min = 4, .max = 512, .refresh = Supplementary::REFRESH_BLUR_FB}), + MS("decoration:blur:glass:roughness", "strength of the glass relief shading", 1.F, {.min = 0, .max = 1, .refresh = Supplementary::REFRESH_BLUR_FB}), + + MS("decoration:blur:acrylic:refraction", "maximum acrylic lens displacement in pixels", 24.F, {.min = 0, .max = 48, .refresh = Supplementary::REFRESH_BLUR_FB}), + MS("decoration:blur:acrylic:bulb", "width of the curved acrylic edge in pixels", 48.F, {.min = 4, .max = 256, .refresh = Supplementary::REFRESH_BLUR_FB}), + MS("decoration:blur:acrylic:clarity", "amount of sharp backdrop transmitted through the acrylic surface", 0.82F, + {.min = 0, .max = 1, .refresh = Supplementary::REFRESH_BLUR_FB}), + MS("decoration:blur:acrylic:aberration", "relative chromatic separation in the acrylic lens", 0.025F, + {.min = 0, .max = 0.25, .refresh = Supplementary::REFRESH_BLUR_FB}), + MS("decoration:blur:acrylic:tint", "acrylic tint color. Alpha controls optical absorption.", 0x14EEF5FF, {.refresh = Supplementary::REFRESH_BLUR_FB}), + + MS("decoration:blur:drops:speed", "animation speed for drops blur. 0 disables the animation. Enabling will significantly increase GPU usage.", 3.F, + {.min = 0, .max = 10, .refresh = Supplementary::REFRESH_BLUR_FB}), + + MS("decoration:blur:heat_shimmer:speed", "animation speed for heat shimmer blur. 0 disables the animation. Enabling will increase GPU usage.", 1.F, + {.min = 0, .max = 10, .refresh = Supplementary::REFRESH_BLUR_FB}), + + MS("decoration:blur:aurora:speed", "animation speed for aurora blur. 0 freezes the animation. Enabling will increase GPU usage.", 1.F, + {.min = 0, .max = 10, .refresh = Supplementary::REFRESH_BLUR_FB}), + MS("decoration:blur:aurora:intensity", "strength of the aurora color contribution", 0.35F, {.min = 0, .max = 1, .refresh = Supplementary::REFRESH_BLUR_FB}), + MS("decoration:blur:aurora:color1", "first aurora curtain color. Alpha controls its contribution.", 0x29F0A0FF, {.refresh = Supplementary::REFRESH_BLUR_FB}), + MS("decoration:blur:aurora:color2", "second aurora curtain color. Alpha controls its contribution.", 0x7A4DFFFF, {.refresh = Supplementary::REFRESH_BLUR_FB}), + + MS("decoration:blur:haze:intensity", "strength of the haze pearlescent sheen", 0.35F, {.min = 0, .max = 1, .refresh = Supplementary::REFRESH_BLUR_FB}), + MS("decoration:blur:haze:iridescence", "strength of the haze pearlescent color shift", 0.7F, {.min = 0, .max = 1, .refresh = Supplementary::REFRESH_BLUR_FB}), + + MS("decoration:blur:ripple:strength", "maximum refraction displacement of click ripples in pixels", 30.F, + {.min = 0, .max = 32, .refresh = Supplementary::REFRESH_BLUR_FB}), + MS("decoration:blur:ripple:radius", "maximum radius of click ripples in pixels", 400.F, {.min = 1, .max = 1000, .refresh = Supplementary::REFRESH_BLUR_FB}), + MS("decoration:blur:ripple:width", "width of click ripple waves in pixels", 32.F, {.min = 1, .max = 200, .refresh = Supplementary::REFRESH_BLUR_FB}), + MS("decoration:blur:ripple:duration", "duration of click ripples in seconds", 0.45F, {.min = 0.05, .max = 5, .refresh = Supplementary::REFRESH_BLUR_FB}), + + MS("decoration:blur:water:strength", "maximum refraction displacement and injection strength for water blur in pixels", 32.F, + {.min = 0, .max = 32, .refresh = Supplementary::REFRESH_BLUR_FB}), + MS("decoration:blur:water:radius", "pointer radius for water blur in pixels", 20.F, {.min = 1, .max = 1000, .refresh = Supplementary::REFRESH_BLUR_FB}), + MS("decoration:blur:water:speed", "propagation speed for water blur", 0.76F, {.min = 0, .max = 10, .refresh = Supplementary::REFRESH_BLUR_FB}), + MS("decoration:blur:water:damping", "decay damping for water blur", 0.95F, {.min = 0, .max = 1, .refresh = Supplementary::REFRESH_BLUR_FB}), + MS("decoration:blur:water:duration", "maximum water blur animation duration in seconds", 12.F, {.min = 0.5, .max = 60, .refresh = Supplementary::REFRESH_BLUR_FB}), + + MS("decoration:blur:fluid_jar:color", "fluid color for fluid jar blur", 0xCC3399FF, {.refresh = Supplementary::REFRESH_BLUR_FB}), + MS("decoration:blur:fluid_jar:speed", "animation speed for fluid jar blur", 3.7F, {.min = 0, .max = 10, .refresh = Supplementary::REFRESH_BLUR_FB}), + MS("decoration:blur:fluid_jar:fill_amount", "fill amount for fluid jar blur", 0.5F, {.min = 0, .max = 1, .refresh = Supplementary::REFRESH_BLUR_FB}), + MS("decoration:blur:fluid_jar:mass", "inertial mass for fluid jar blur", 1.4F, {.min = 0.1, .max = 10, .refresh = Supplementary::REFRESH_BLUR_FB}), + MS("decoration:blur:fluid_jar:precision", "fluid simulation precision multiplier. 2x is a good compromise. 4x is expensive. 8x is extreme and unnecessary.", 2.F, + {.min = 0.5, .max = 8, .refresh = Supplementary::REFRESH_BLUR_FB}), + MS("decoration:blur:fluid_jar:turbulence", "interior fluid turbulence multiplier", 1.2F, {.min = 0, .max = 5, .refresh = Supplementary::REFRESH_BLUR_FB}), + MS("decoration:blur:fluid_jar:distortion", "fluid refraction distortion multiplier", 8.F, {.min = 0, .max = 10, .refresh = Supplementary::REFRESH_BLUR_FB}), + + /* + * motion_blur: + */ + MS("decoration:motion_blur:enabled", "enable motion blur for moving and resizing windows", false, {.refresh = Supplementary::REFRESH_WINDOW_STATES}), MS("decoration:motion_blur:samples", "amount of samples used for motion blur", 7, {.min = 1, .max = 64, .refresh = Supplementary::REFRESH_WINDOW_STATES}), MS("decoration:wobble:enabled", "enable wobble deformation for moving and resizing windows", false, {.refresh = Supplementary::REFRESH_WINDOW_STATES}), diff --git a/src/desktop/view/LayerSurface.cpp b/src/desktop/view/LayerSurface.cpp index 69c1db830..4a8b007fd 100644 --- a/src/desktop/view/LayerSurface.cpp +++ b/src/desktop/view/LayerSurface.cpp @@ -10,6 +10,7 @@ #include "../../animation/AnimationManager.hpp" #include "../../render/Renderer.hpp" #include "../../config/shared/animation/AnimationTree.hpp" +#include "../../config/ConfigValue.hpp" #include "../../output/Monitor.hpp" #include "../../managers/input/InputManager.hpp" #include "../../ipc/s2/S2.hpp" @@ -484,3 +485,15 @@ std::optional CLayerSurface::alphaGenericToKey(eAlphaModifiableProp p) static_assert(ALPHA_MODIFIABLE_LAST == 1); UNREACHABLE(); } + +bool CLayerSurface::shouldBlur() const { + static auto PBLUR = CConfigValue("decoration:blur:enabled"); + if (!*PBLUR) + return false; + + auto surface = wlSurface(); + if (surface && surface->m_hasBackgroundEffect) + return !surface->m_blurRegion.empty(); + + return m_ruleApplicator->blur().valueOrDefault(); +} diff --git a/src/desktop/view/LayerSurface.hpp b/src/desktop/view/LayerSurface.hpp index ccd4e874b..97e1ec48b 100644 --- a/src/desktop/view/LayerSurface.hpp +++ b/src/desktop/view/LayerSurface.hpp @@ -54,6 +54,7 @@ namespace Desktop::View { virtual std::optional alphaGenericToKey(eAlphaModifiableProp p) override; WP m_layerSurface; + bool shouldBlur() const; LayerFlags m_flags = LAYER_FLAG_ABOVE_FULLSCREEN; diff --git a/src/desktop/view/Popup.cpp b/src/desktop/view/Popup.cpp index 249716aeb..c077b7273 100644 --- a/src/desktop/view/Popup.cpp +++ b/src/desktop/view/Popup.cpp @@ -726,3 +726,10 @@ std::optional CPopup::alphaGenericToKey(eAlphaModifiableProp p) { static_assert(ALPHA_MODIFIABLE_LAST == 1); UNREACHABLE(); } + +bool CPopup::shouldBlur() const { + static CConfigValue PBLURPOPUPS = CConfigValue("decoration:blur:popups"); + static CConfigValue PBLUR = CConfigValue("decoration:blur:enabled"); + + return *PBLURPOPUPS && *PBLUR; +} diff --git a/src/desktop/view/Popup.hpp b/src/desktop/view/Popup.hpp index 8d38ce4f4..e21229d89 100644 --- a/src/desktop/view/Popup.hpp +++ b/src/desktop/view/Popup.hpp @@ -68,6 +68,7 @@ namespace Desktop::View { void onReposition(); void recheckTree(); + bool shouldBlur() const; bool inert() const; diff --git a/src/desktop/view/window/Window.cpp b/src/desktop/view/window/Window.cpp index 2c377287e..bb01ce341 100644 --- a/src/desktop/view/window/Window.cpp +++ b/src/desktop/view/window/Window.cpp @@ -606,6 +606,22 @@ bool CWindow::isHidden() const { return m_hidden; } +bool CWindow::shouldBlur() const { + static auto PBLUR = CConfigValue("decoration:blur:enabled"); + if (!*PBLUR) + return false; + + const bool DONT_BLUR = m_ruleApplicator->noBlur().valueOrDefault() || m_ruleApplicator->RGBX().valueOrDefault() || presentation().opaque(); + if (DONT_BLUR) + return false; + + auto surface = wlSurface(); + if (surface && surface->m_hasBackgroundEffect) + return !surface->m_blurRegion.empty(); + + return true; +} + void CWindow::onInputBlockStateUpdated(bool blocked) { if (blocked && Desktop::focusState()->window() == m_self) Desktop::focusState()->fullWindowFocus(nullptr, eFocusReason::FOCUS_REASON_SWITCH_TO_WINDOW_SOFT); @@ -1049,7 +1065,7 @@ bool CWindow::priorityFocus() { return !m_backend->isX11() && CAsyncDialogBox::isPriorityDialogBox(m_backend->pid()); } -SP CWindow::getSolitaryResource() { +SP CWindow::getSolitaryResource() const { if (!m_wlSurface || !m_wlSurface->resource()) return nullptr; diff --git a/src/desktop/view/window/Window.hpp b/src/desktop/view/window/Window.hpp index be0bf00be..34b434b66 100644 --- a/src/desktop/view/window/Window.hpp +++ b/src/desktop/view/window/Window.hpp @@ -199,6 +199,7 @@ namespace Desktop::View { void onMap(); void setHidden(bool hidden); bool isHidden() const; + bool shouldBlur() const; bool isAllowedOverFullscreen() const; bool isBlockedByFullscreen() const; bool isFadingOutUnderFullscreen() const; @@ -229,7 +230,7 @@ namespace Desktop::View { void deactivateGroupMembers(); bool isNotResponding(); bool priorityFocus(); - SP getSolitaryResource(); + SP getSolitaryResource() const; std::optional calculateExpression(const Math::SExpressionVec2& expr); std::optional minSize(); std::optional maxSize(); diff --git a/src/desktop/view/window/WindowPresentation.cpp b/src/desktop/view/window/WindowPresentation.cpp index 6850ed8b6..16ef077d3 100644 --- a/src/desktop/view/window/WindowPresentation.cpp +++ b/src/desktop/view/window/WindowPresentation.cpp @@ -215,7 +215,7 @@ void CWindowPresentation::invalidateBorderSize() { m_borderDecoration->invalidateBorderSize(); } -bool CWindowPresentation::opaque() { +bool CWindowPresentation::opaque() const { if (alphaValue(WINDOW_ALPHA_FADE) != 1.f || alphaValue(WINDOW_ALPHA_FULLSCREEN) != 1.f || alphaValue(WINDOW_ALPHA_ACTIVE) != 1.f) return false; diff --git a/src/desktop/view/window/WindowPresentation.hpp b/src/desktop/view/window/WindowPresentation.hpp index 5451a5d9f..7c55a6025 100644 --- a/src/desktop/view/window/WindowPresentation.hpp +++ b/src/desktop/view/window/WindowPresentation.hpp @@ -49,7 +49,7 @@ namespace Desktop::View { int borderSize() const; void invalidateBorderSize(); - bool opaque(); + bool opaque() const; float rounding(); float roundingPower(); bool isInCurvedCorner(double x, double y); diff --git a/src/render/ElementRenderer.cpp b/src/render/ElementRenderer.cpp index 330f90108..c847a9ffa 100644 --- a/src/render/ElementRenderer.cpp +++ b/src/render/ElementRenderer.cpp @@ -32,6 +32,7 @@ void IElementRenderer::drawElement(WP element, const CRegion& dama case EK_TEXTURE: drawTex(dynamicPointerCast(element), damage); break; case EK_TEXTURE_MATTE: drawTexMatte(dynamicPointerCast(element), damage); break; case EK_TRANSFORMED_WINDOW: drawTransformedWindow(dynamicPointerCast(element), damage); break; + case EK_BACKDROP_SCOPE: drawCustom(element, damage); break; case EK_CUSTOM: drawCustom(element, damage); break; default: Log::logger->log(Log::WARN, "Unimplimented draw for {}", element->passName()); } @@ -328,7 +329,10 @@ void IElementRenderer::drawSurface(WP element, const CRegio // is a subsurface that does NOT cover the entire frame. In such cases, we probably should fall back // to what we do for misaligned surfaces (blur the entire thing and then render shit without blur) if (m_data.surfaceCounter == 0 && !m_data.popup) { - if (BLUR) + if (BLUR) { + CBox blurPatternBox = {m_data.pos.x - m_data.pMonitor->m_position.x, m_data.pos.y - m_data.pMonitor->m_position.y, m_data.w, m_data.h}; + blurPatternBox.scale(m_data.pMonitor->m_scale).round(); + drawElement(makeShared(CTexPassElement::SRenderData{ .tex = TEXTURE, .box = windowBox, @@ -338,6 +342,7 @@ void IElementRenderer::drawSurface(WP element, const CRegio .round = rounding, .roundingPower = roundingPower, .blur = true, + .blurPatternBox = blurPatternBox, .blockBlurOptimization = m_data.blockBlurOptimization, .allowCustomUV = true, .surface = m_data.surface, @@ -347,9 +352,11 @@ void IElementRenderer::drawSurface(WP element, const CRegio .discardOpacity = m_data.discardOpacity, .clipRegion = clipRegion, .currentLS = m_data.pLS, + .blurOwner = m_data.pWindow, }), + surfaceDamage()); - else + } else drawElement(makeShared(CTexPassElement::SRenderData{ .tex = TEXTURE, .box = windowBox, @@ -492,15 +499,29 @@ void IElementRenderer::drawTex(WP element, const CRegion& damag inverseOpaque = {0, 0, element->m_data.box.width, element->m_data.box.height}; inverseOpaque.scale(m_renderData.pMonitor->m_scale); - element->m_data.blockBlurOptimization = element->m_data.blockBlurOptimization.value_or(false) || - !g_pHyprRenderer->shouldUseNewBlurOptimizations(element->m_data.currentLS.lock(), m_renderData.currentWindow.lock()); + element->m_data.blockBlurOptimization = element->usesLiveBlur(); // vvv TODO: layered blur fbs? + SP blurredFB; if (element->m_data.blockBlurOptimization.value_or(false)) { inverseOpaque.translate(box.pos()); m_renderData.renderModif.applyToRegion(inverseOpaque); inverseOpaque.intersect(element->m_data.damage); - element->m_data.blurredBG = g_pHyprRenderer->blurMainFramebuffer(element->m_data.a, &inverseOpaque); + auto patternBox = element->m_data.blurPatternBox.value_or(box); + m_renderData.renderModif.applyToBox(patternBox); + std::optional shape; + if (!element->m_data.blurShapeInvalid) { + auto shapeBox = box; + m_renderData.renderModif.applyToBox(shapeBox); + if (std::abs(shapeBox.rot) < 0.0001F) + shape = SBlurShape{ + .box = shapeBox, + .radius = std::max(sc(element->m_data.round), 0.F), + .roundingPower = element->m_data.roundingPower, + }; + } + blurredFB = g_pHyprRenderer->blurMainFramebuffer(element->m_data.a, inverseOpaque, {.patternBox = patternBox, .owner = element->m_data.blurOwner, .shape = shape}); + element->m_data.blurredBG = blurredFB->getTexture(); } else element->m_data.blurredBG = m_renderData.pMonitor->resources()->m_blurFB->getTexture(); @@ -800,12 +821,15 @@ void IElementRenderer::drawTransformedWindow(WP e data.useProvidedDamage = true; if (element->m_data.blur && blurAlphaMatte) { - data.blur = true; - data.forceBlurBlend = true; - data.blockBlurOptimization = true; - data.blurA = element->m_data.blurA; - data.blurAlphaMatte = blurAlphaMatte; - data.discardMode = 0; + data.blur = true; + data.forceBlurBlend = true; + data.blurPatternBox = element->m_data.blurBox; + data.blurShapeInvalid = true; + data.liveBlurOverride = element->m_data.blurUsesLive; + data.blurOwner = element->m_data.window; + data.blurA = element->m_data.blurA; + data.blurAlphaMatte = blurAlphaMatte; + data.discardMode = 0; } (void)blurAlphaMatteFB; diff --git a/src/render/GLRenderer.cpp b/src/render/GLRenderer.cpp index aabd90a57..62b7814c7 100644 --- a/src/render/GLRenderer.cpp +++ b/src/render/GLRenderer.cpp @@ -10,18 +10,26 @@ #include "../protocols/core/DataDevice.hpp" #include "../protocols/core/Compositor.hpp" #include "../debug/Overlay.hpp" +#include "../desktop/state/WindowState.hpp" +#include "../desktop/view/window/Window.hpp" +#include "../desktop/view/window/WindowPresentation.hpp" +#include "../event/EventBus.hpp" #include "../output/Monitor.hpp" #include "pass/TexPassElement.hpp" #include "pass/SurfacePassElement.hpp" #include "../debug/log/Logger.hpp" #include "../protocols/types/ContentType.hpp" +#include "../state/MonitorState.hpp" #include "OpenGL.hpp" #include "Renderer.hpp" #include "./gl/GLElementRenderer.hpp" #include "./gl/GLFramebuffer.hpp" #include "./gl/GLTexture.hpp" +#include "./gl/blur/Factory.hpp" +#include "./gl/blur/Provider.hpp" #include +#include #include #include #include @@ -36,7 +44,12 @@ extern "C" { #include } -CHyprGLRenderer::CHyprGLRenderer() : IHyprRenderer(), m_elementRenderer(makeUnique()) {} +CHyprGLRenderer::CHyprGLRenderer() : IHyprRenderer(), m_elementRenderer(makeUnique()) { + refreshBlurProvider(); + m_preRenderListener = Event::bus()->m_events.render.pre.listen([this](PHLMONITOR monitor) { preRender(monitor); }); +} + +CHyprGLRenderer::~CHyprGLRenderer() = default; IHyprRenderer::eType CHyprGLRenderer::type() { return RT_GL; @@ -308,9 +321,100 @@ void CHyprGLRenderer::drawGlow(const CBox& box, int round, float roundingPower, g_pHyprOpenGL->renderInnerGlow(box, round, roundingPower, range, grad1, grad2, lerp, 0, a); } -SP CHyprGLRenderer::blurFramebuffer(SP source, float a, CRegion* originalDamage) { - auto src = GLFB(source); - return g_pHyprOpenGL->blurFramebufferWithDamage(a, originalDamage, *src)->getTexture(); +SP CHyprGLRenderer::blurFramebuffer(SP source, float strength, const CRegion& originalDamage, const SBlurContext& context) { + RASSERT(m_blur, "Cannot blur without a blur provider"); + return m_blur->blur(source, strength, originalDamage, context); +} + +void CHyprGLRenderer::refreshBlurProvider() { + static auto PBLURTYPE = CConfigValue("decoration:blur:variant"); + + const auto type = sc(*PBLURTYPE); + if (m_blur && m_blur->type() == type) + return; + + m_blur = createBlurProvider(type, *g_pHyprOpenGL); +} + +void CHyprGLRenderer::expandBlurDamage(CRegion& damage, float multiplier) const { + RASSERT(m_blur, "Cannot expand blur damage without a blur provider"); + m_blur->expandDamage(damage, multiplier); +} + +bool CHyprGLRenderer::blurProviderIsAnimated() const { + return m_blur && m_blur->isAnimated(); +} + +bool CHyprGLRenderer::blurProviderRequiresLiveBlur() const { + return m_blur && m_blur->requiresLiveBlur(); +} + +void CHyprGLRenderer::preRender(PHLMONITOR pMonitor) { + static auto PBLURNEWOPTIMIZE = CConfigValue("decoration:blur:new_optimizations"); + static auto PBLURXRAY = CConfigValue("decoration:blur:xray"); + static auto PBLUR = CConfigValue("decoration:blur:enabled"); + + if (!*PBLURNEWOPTIMIZE || !pMonitor->m_blurFBDirty || !*PBLUR) + return; + + if (!pMonitor->m_solitaryClient.expired()) + return; + + auto windowShouldBeBlurred = [](PHLWINDOW pWindow) -> bool { + if (!pWindow || pWindow->m_ruleApplicator->noBlur().valueOrDefault()) + return false; + + if (pWindow->wlSurface()->small() && !pWindow->wlSurface()->m_fillIgnoreSmall) + return true; + + const auto PSURFACE = pWindow->wlSurface()->resource(); + const auto PWORKSPACE = pWindow->m_workspace; + const float A = pWindow->presentation().alphaValue(Desktop::View::WINDOW_ALPHA_FADE) * pWindow->presentation().alphaValue(Desktop::View::WINDOW_ALPHA_FULLSCREEN) * + pWindow->presentation().alphaValue(Desktop::View::WINDOW_ALPHA_LAYOUT) * pWindow->presentation().alphaValue(Desktop::View::WINDOW_ALPHA_ACTIVE) * + PWORKSPACE->m_alpha->value(); + + if (A < 1.F) + return true; + + pixman_box32_t surfbox = {0, 0, PSURFACE->m_current.size.x, PSURFACE->m_current.size.y}; + CRegion inverseOpaque; + CRegion opaqueRegion{PSURFACE->m_current.opaque}; + inverseOpaque.set(opaqueRegion).invert(&surfbox).intersect(0, 0, PSURFACE->m_current.size.x, PSURFACE->m_current.size.y); + return !inverseOpaque.empty(); + }; + + bool hasWindows = false; + for (const auto& w : Desktop::windowState()->windows()) { + const auto& XRAY_RULE = w->m_ruleApplicator->xray(); + const bool XRAY = XRAY_RULE.hasValue() ? XRAY_RULE.valueOrDefault() : *PBLURXRAY; + const bool ON_ACTIVE_WORKSPACE = w->m_workspace && (w->m_workspace == pMonitor->m_activeWorkspace || w->m_workspace == pMonitor->m_activeSpecialWorkspace); + if (!ON_ACTIVE_WORKSPACE || !w->mapped() || !w->acceptsInput() || !w->alphaNonZero() || ((w->isFloating() || w->onSpecialWorkspace()) && !XRAY) || + !windowShouldBeBlurred(w)) + continue; + + hasWindows = true; + break; + } + + if (!hasWindows) { + for (const auto& m : State::monitorState()->monitors()) { + for (const auto& layer : m->m_layerSurfaceLayers) { + if (std::ranges::any_of(layer, [](const auto& ls) { return ls->m_layerSurface && ls->m_ruleApplicator->xray().valueOrDefault() == 1; })) { + hasWindows = true; + break; + } + } + + if (hasWindows) + break; + } + } + + if (!hasWindows) + return; + + g_pHyprRenderer->damageMonitor(pMonitor); + pMonitor->m_blurFBShouldRender = true; } void CHyprGLRenderer::setViewport(int x, int y, int width, int height) { diff --git a/src/render/GLRenderer.hpp b/src/render/GLRenderer.hpp index a5343e555..28eda645f 100644 --- a/src/render/GLRenderer.hpp +++ b/src/render/GLRenderer.hpp @@ -4,10 +4,12 @@ #include "render/ElementRenderer.hpp" namespace Render::GL { + class IGLBlurProvider; + class CHyprGLRenderer : public Render::IHyprRenderer { public: CHyprGLRenderer(); - ~CHyprGLRenderer() = default; + ~CHyprGLRenderer(); eType type() override; void endRender(const std::function& renderingDoneCallback = {}) override; @@ -33,7 +35,11 @@ namespace Render::GL { void drawGlow(const CBox& box, int round, float roundingPower, int range, const Config::CGradientValueData& color, float a) override; void drawGlow(const CBox& box, int round, float roundingPower, int range, const Config::CGradientValueData& grad1, const Config::CGradientValueData& grad2, float lerp, float a) override; - SP blurFramebuffer(SP source, float a, CRegion* originalDamage) override; + SP blurFramebuffer(SP source, float strength, const CRegion& originalDamage, const Render::SBlurContext& context = {}) override; + void refreshBlurProvider() override; + void expandBlurDamage(CRegion& damage, float multiplier = 1.F) const override; + bool blurProviderIsAnimated() const override; + bool blurProviderRequiresLiveBlur() const override; void setViewport(int x, int y, int width, int height) override; bool reloadShaders(const std::string& path = "") override; @@ -41,6 +47,7 @@ namespace Render::GL { WP elementRenderer() override; private: + void preRender(PHLMONITOR pMonitor); void renderOffToMain(SP off) override; SP getOrCreateRenderbufferInternal(SP buffer, uint32_t fmt) override; bool beginRenderInternal(PHLMONITOR pMonitor, CRegion& damage, bool simple = false) override; @@ -52,6 +59,8 @@ namespace Render::GL { SP m_currentRenderbuffer; UP m_elementRenderer; + UP m_blur; + CHyprSignalListener m_preRenderListener; friend class CHyprOpenGLImpl; }; diff --git a/src/render/OpenGL.cpp b/src/render/OpenGL.cpp index db3b65fdb..4b9c201b6 100644 --- a/src/render/OpenGL.cpp +++ b/src/render/OpenGL.cpp @@ -412,8 +412,6 @@ CHyprOpenGLImpl::CHyprOpenGLImpl() : m_drmFD(g_pCompositor->m_drmRenderNode.fd > initDRMFormats(); - static auto P = Event::bus()->m_events.render.pre.listen([&](PHLMONITOR mon) { preRender(mon); }); - RASSERT(eglMakeCurrent(m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT), "Couldn't unset current EGL!"); static auto addLastPressToHistory = [this](const Vector2D& pos, bool killing, bool touch) { @@ -940,14 +938,44 @@ void CHyprOpenGLImpl::end() { } static const std::vector SHADER_INCLUDES = { - "defines.h", "constants.h", "cm_helpers.glsl", "rounding.glsl", "CM.glsl", "tonemap.glsl", "gain.glsl", "border.glsl", - "shadow.glsl", "inner_glow.glsl", "blurprepare.glsl", "blur1.glsl", "blur2.glsl", "blurFinish.glsl", "motion_blur.glsl", "gradient.glsl", + "defines.h", "constants.h", "cm_helpers.glsl", "rounding.glsl", "CM.glsl", "tonemap.glsl", "gain.glsl", "border.glsl", "shadow.glsl", + "inner_glow.glsl", "blurprepare.glsl", "blur1.glsl", "blur2.glsl", "blurFinish.glsl", "motion_blur.glsl", "gradient.glsl", "glassFinish.glsl", "fluidJar.glsl", }; // order matters, see ePreparedFragmentShader const std::array FRAG_SHADERS = { - "quad.frag", "passthru.frag", "rgbamatte.frag", "ext.frag", "blur1.frag", "blur2.frag", "blurprepare.frag", - "blurfinish.frag", "shadow.frag", "inner_glow.frag", "surface.frag", "border.frag", "glitch.frag", + "quad.frag", + "passthru.frag", + "rgbamatte.frag", + "ext.frag", + "blur1.frag", + "blur2.frag", + "blurprepare.frag", + "blurfinish.frag", + "shadow.frag", + "inner_glow.frag", + "surface.frag", + "border.frag", + "glitch.frag", + "frostfinish.frag", + "ripplefinish.frag", + "dropsfinish.frag", + "waterstep.frag", + "waterfinish.frag", + "fluidjarinit.frag", + "fluidjarstep.frag", + "fluidjargraph.frag", + "fluidjartrack.frag", + "fluidjarvisual.frag", + "fluidjarresample.frag", + "fluidjarhistoryresample.frag", + "fluidjartrackingresample.frag", + "fluidjarfinish.frag", + "prismfinish.frag", + "heatshimmerfinish.frag", + "acrylicfinish.frag", + "aurorafinish.frag", + "hazefinish.frag", }; bool CHyprOpenGLImpl::initShaders(const std::string& path) { @@ -1060,6 +1088,10 @@ void CHyprOpenGLImpl::blend(bool enabled) { m_blend = enabled; } +bool CHyprOpenGLImpl::blendEnabled() const { + return m_blend; +} + void CHyprOpenGLImpl::scissor(const CBox& originalBox, bool transform) { auto& m_renderData = g_pHyprRenderer->m_renderData; RASSERT(m_renderData.pMonitor, "Tried to scissor without begin()!"); @@ -1124,7 +1156,21 @@ void CHyprOpenGLImpl::renderRectWithBlurInternal(const CBox& box, const CHyprCol CRegion damage{g_pHyprRenderer->m_renderData.damage}; damage.intersect(box); - auto blurredBG = data.xray ? g_pHyprRenderer->m_renderData.pMonitor->resources()->m_blurFB->getTexture() : g_pHyprRenderer->blurMainFramebuffer(data.blurA, &damage); + auto patternBox = data.blurPatternBox.value_or(box); + g_pHyprRenderer->m_renderData.renderModif.applyToBox(patternBox); + auto shapeBox = box; + g_pHyprRenderer->m_renderData.renderModif.applyToBox(shapeBox); + std::optional shape; + if (std::abs(shapeBox.rot) < 0.0001F) + shape = SBlurShape{ + .box = shapeBox, + .radius = std::max(sc(data.round), 0.F), + .roundingPower = data.roundingPower, + }; + const bool usePrecomputedBlur = data.xray && !g_pHyprRenderer->blurProviderRequiresLiveBlur(); + const auto blurredFB = usePrecomputedBlur ? g_pHyprRenderer->m_renderData.pMonitor->resources()->m_blurFB : + g_pHyprRenderer->blurMainFramebuffer(data.blurA, damage, {.patternBox = patternBox, .owner = data.blurOwner, .shape = shape}); + const auto blurredBG = blurredFB->getTexture(); const auto SAVEDRENDERMODIF = g_pHyprRenderer->m_renderData.renderModif; g_pHyprRenderer->m_renderData.renderModif = {}; // fix shit @@ -1651,6 +1697,8 @@ void CHyprOpenGLImpl::renderTextureInternal(SP tex, const CBox& box, c damageClip.intersect(data.clipRegion); } + damageClip.intersect(*data.damage); + if (!damageClip.empty()) { damageClip.forEachRect([this](const auto& RECT) { scissor(&RECT, g_pHyprRenderer->m_renderData.transformDamage); @@ -1815,324 +1863,6 @@ void CHyprOpenGLImpl::renderTextureMatte(SP tex, const CBox& box, SPunbind(); } -static SCMSettings blurIntermediateCMSettings(bool toIntermediate) { - const auto WORKBUFFER = g_pHyprRenderer->workBufferImageDescription(); - const auto INTERMEDIATE = getDefaultImageDescription(); - - auto settings = toIntermediate ? g_pHyprRenderer->getCMSettings(WORKBUFFER, INTERMEDIATE) : g_pHyprRenderer->getCMSettings(INTERMEDIATE, WORKBUFFER); - auto& range = toIntermediate ? settings.dstTFRange : settings.srcTFRange; - range.max = std::max(range.max, sc(WORKBUFFER->value().luminances.max)); - return settings; -} - -// This probably isn't the fastest -// but it works... well, I guess? -// -// Dual (or more) kawase blur -SP CHyprOpenGLImpl::blurFramebufferWithDamage(float a, CRegion* originalDamage, CGLFramebuffer& source) { - TRACY_GPU_ZONE("RenderBlurFramebufferWithDamage"); - auto& m_renderData = g_pHyprRenderer->m_renderData; - - const auto BLENDBEFORE = m_blend; - blend(false); - setCapStatus(GL_STENCIL_TEST, false); - - CBox MONITORBOX = {0, 0, m_renderData.pMonitor->m_transformedSize.x, m_renderData.pMonitor->m_transformedSize.y}; - - const auto& glMatrix = g_pHyprRenderer->projectBoxToTarget(MONITORBOX); - - // get the config settings - static auto PBLURSIZE = CConfigValue("decoration:blur:size"); - static auto PBLURPASSES = CConfigValue("decoration:blur:passes"); - static auto PBLURVIBRANCY = CConfigValue("decoration:blur:vibrancy"); - static auto PBLURVIBRANCYDARKNESS = CConfigValue("decoration:blur:vibrancy_darkness"); - - const auto BLUR_PASSES = std::clamp(*PBLURPASSES, sc(1), sc(8)); - - // prep damage - CRegion damage{*originalDamage}; - damage.expand(std::clamp(*PBLURSIZE, sc(1), sc(40)) * pow(2, BLUR_PASSES)); - - // helper - const auto PMIRRORFB = g_pHyprRenderer->m_renderData.pMonitor->resources()->getUnusedWorkBuffer(); - const auto PMIRRORSWAPFB = g_pHyprRenderer->m_renderData.pMonitor->resources()->getUnusedWorkBuffer(); - - auto currentRenderToFB = PMIRRORFB; - - // Begin with base color adjustments - global brightness and contrast - // TODO: make this a part of the first pass maybe to save on a drawcall? - { - static auto PBLURCONTRAST = CConfigValue("decoration:blur:contrast"); - static auto PBLURBRIGHTNESS = CConfigValue("decoration:blur:brightness"); - PMIRRORSWAPFB->bind(); - GLFB(PMIRRORSWAPFB)->clearAfterInvalidation(); - - setActiveTexture(GL_TEXTURE0); - - auto currentTex = source.getTexture(); - - currentTex->bind(); - currentTex->setTexParameter(GL_TEXTURE_MIN_FILTER, GL_LINEAR); - - WP shader; - - // From FB to sRGB - const bool skipCM = !m_cmSupported || !g_pHyprRenderer->workBufferImageDescription()->needsCM(getDefaultImageDescription()); - if (!skipCM) { - const auto settings = blurIntermediateCMSettings(/* toIntermediate */ true); - shader = useShader(getShaderVariant(SH_FRAG_BLURPREPARE, SH_FEAT_CM, settings.sourceTF, settings.targetTF)); - - passCMUniforms(shader, g_pHyprRenderer->workBufferImageDescription(), getDefaultImageDescription(), false, -1.F, -1, settings); - shader->setUniformFloat(SHADER_SDR_SATURATION, - m_renderData.pMonitor->m_sdrSaturation > 0 && - g_pHyprRenderer->workBufferImageDescription()->value().transferFunction == NColorManagement::CM_TRANSFER_FUNCTION_ST2084_PQ ? - m_renderData.pMonitor->m_sdrSaturation : - 1.0f); - shader->setUniformFloat(SHADER_SDR_BRIGHTNESS, - m_renderData.pMonitor->m_sdrBrightness > 0 && - g_pHyprRenderer->workBufferImageDescription()->value().transferFunction == NColorManagement::CM_TRANSFER_FUNCTION_ST2084_PQ ? - m_renderData.pMonitor->m_sdrBrightness : - 1.0f); - } else - shader = useShader(getShaderVariant(SH_FRAG_BLURPREPARE)); - - shader->setUniformMatrix3fv(SHADER_PROJ, 1, GL_TRUE, glMatrix.getMatrix()); - shader->setUniformFloat(SHADER_CONTRAST, *PBLURCONTRAST); - shader->setUniformFloat(SHADER_BRIGHTNESS, *PBLURBRIGHTNESS); - shader->setUniformInt(SHADER_TEX, 0); - - glBindVertexArray(shader->getUniformLocation(SHADER_SHADER_VAO)); - - if (!damage.empty()) { - damage.forEachRect([this](const auto& RECT) { - scissor(&RECT, false); - glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - }); - } - - glBindVertexArray(0); - currentRenderToFB = PMIRRORSWAPFB; - } - - // declare the draw func - auto drawPass = [&](WP shader, ePreparedFragmentShader frag, CRegion* pDamage) { - if (currentRenderToFB == PMIRRORFB) - PMIRRORSWAPFB->bind(); - else - PMIRRORFB->bind(); - - setActiveTexture(GL_TEXTURE0); - - auto currentTex = currentRenderToFB->getTexture(); - - currentTex->bind(); - - currentTex->setTexParameter(GL_TEXTURE_MIN_FILTER, GL_LINEAR); - - // prep two shaders - shader->setUniformMatrix3fv(SHADER_PROJ, 1, GL_TRUE, glMatrix.getMatrix()); - shader->setUniformFloat(SHADER_RADIUS, *PBLURSIZE * a); // this makes the blursize change with a - if (frag == SH_FRAG_BLUR1) { - shader->setUniformFloat2(SHADER_HALFPIXEL, 0.5f / (m_renderData.pMonitor->m_transformedSize.x / 2.f), 0.5f / (m_renderData.pMonitor->m_transformedSize.y / 2.f)); - shader->setUniformInt(SHADER_PASSES, BLUR_PASSES); - shader->setUniformFloat(SHADER_VIBRANCY, *PBLURVIBRANCY); - shader->setUniformFloat(SHADER_VIBRANCY_DARKNESS, *PBLURVIBRANCYDARKNESS); - } else - shader->setUniformFloat2(SHADER_HALFPIXEL, 0.5f / (m_renderData.pMonitor->m_transformedSize.x * 2.f), 0.5f / (m_renderData.pMonitor->m_transformedSize.y * 2.f)); - shader->setUniformInt(SHADER_TEX, 0); - - glBindVertexArray(shader->getUniformLocation(SHADER_SHADER_VAO)); - - if (!pDamage->empty()) { - pDamage->forEachRect([this](const auto& RECT) { - scissor(&RECT, false); - glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - }); - } - - glBindVertexArray(0); - - if (currentRenderToFB != PMIRRORFB) - currentRenderToFB = PMIRRORFB; - else - currentRenderToFB = PMIRRORSWAPFB; - }; - - // draw the things. - // first draw is swap -> mirr - PMIRRORFB->bind(); - GLFB(PMIRRORFB)->clearAfterInvalidation(); - PMIRRORSWAPFB->getTexture()->bind(); - - // damage region will be scaled, make a temp - CRegion tempDamage{damage}; - - // and draw - auto shader = useShader(getShaderVariant(SH_FRAG_BLUR1)); - for (auto i = 1; i <= BLUR_PASSES; ++i) { - tempDamage = damage.copy().scale(1.f / (1 << i)); - drawPass(shader, SH_FRAG_BLUR1, &tempDamage); // down - } - - shader = useShader(getShaderVariant(SH_FRAG_BLUR2)); - for (auto i = BLUR_PASSES - 1; i >= 0; --i) { - tempDamage = damage.copy().scale(1.f / (1 << i)); // when upsampling we make the region twice as big - drawPass(shader, SH_FRAG_BLUR2, &tempDamage); // up - } - - // finalize the image - { - static auto PBLURNOISE = CConfigValue("decoration:blur:noise"); - static auto PBLURBRIGHTNESS = CConfigValue("decoration:blur:brightness"); - - if (currentRenderToFB == PMIRRORFB) - PMIRRORSWAPFB->bind(); - else - PMIRRORFB->bind(); - - setActiveTexture(GL_TEXTURE0); - - auto currentTex = currentRenderToFB->getTexture(); - - currentTex->bind(); - - currentTex->setTexParameter(GL_TEXTURE_MIN_FILTER, GL_LINEAR); - - // From FB to sRGB - const bool skipCM = !m_cmSupported || !g_pHyprRenderer->workBufferImageDescription()->needsCM(getDefaultImageDescription()); - if (!skipCM) { - const auto settings = blurIntermediateCMSettings(/* toIntermediate */ false); - shader = useShader(getShaderVariant(SH_FRAG_BLURFINISH, SH_FEAT_CM, settings.sourceTF, settings.targetTF)); - - passCMUniforms(shader, getDefaultImageDescription(), g_pHyprRenderer->workBufferImageDescription(), false, -1.F, -1, settings); - shader->setUniformFloat(SHADER_SDR_SATURATION, - m_renderData.pMonitor->m_sdrSaturation > 0 && - g_pHyprRenderer->workBufferImageDescription()->value().transferFunction == NColorManagement::CM_TRANSFER_FUNCTION_ST2084_PQ ? - m_renderData.pMonitor->m_sdrSaturation : - 1.0f); - shader->setUniformFloat(SHADER_SDR_BRIGHTNESS, - m_renderData.pMonitor->m_sdrBrightness > 0 && - g_pHyprRenderer->workBufferImageDescription()->value().transferFunction == NColorManagement::CM_TRANSFER_FUNCTION_ST2084_PQ ? - m_renderData.pMonitor->m_sdrBrightness : - 1.0f); - } else - shader = useShader(getShaderVariant(SH_FRAG_BLURFINISH)); - - shader->setUniformMatrix3fv(SHADER_PROJ, 1, GL_TRUE, glMatrix.getMatrix()); - shader->setUniformFloat(SHADER_NOISE, *PBLURNOISE); - shader->setUniformFloat(SHADER_BRIGHTNESS, *PBLURBRIGHTNESS); - - shader->setUniformInt(SHADER_TEX, 0); - - glBindVertexArray(shader->getUniformLocation(SHADER_SHADER_VAO)); - - if (!damage.empty()) { - damage.forEachRect([this](const auto& RECT) { - scissor(&RECT, false /* this region is already transformed */); - glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - }); - } - - glBindVertexArray(0); - - if (currentRenderToFB != PMIRRORFB) - currentRenderToFB = PMIRRORFB; - else - currentRenderToFB = PMIRRORSWAPFB; - } - - // finish - PMIRRORFB->getTexture()->unbind(); - - blend(BLENDBEFORE); - - return currentRenderToFB; -} - -void CHyprOpenGLImpl::preRender(PHLMONITOR pMonitor) { - static auto PBLURNEWOPTIMIZE = CConfigValue("decoration:blur:new_optimizations"); - static auto PBLURXRAY = CConfigValue("decoration:blur:xray"); - static auto PBLUR = CConfigValue("decoration:blur:enabled"); - - if (!*PBLURNEWOPTIMIZE || !pMonitor->m_blurFBDirty || !*PBLUR) - return; - - // ignore if solitary present, nothing to blur - if (!pMonitor->m_solitaryClient.expired()) - return; - - // check if we need to update the blur fb - // if there are no windows that would benefit from it, - // we will ignore that the blur FB is dirty. - - auto windowShouldBeBlurred = [&](PHLWINDOW pWindow) -> bool { - if (!pWindow) - return false; - - if (pWindow->m_ruleApplicator->noBlur().valueOrDefault()) - return false; - - if (pWindow->wlSurface()->small() && !pWindow->wlSurface()->m_fillIgnoreSmall) - return true; - - const auto PSURFACE = pWindow->wlSurface()->resource(); - - const auto PWORKSPACE = pWindow->m_workspace; - const float A = pWindow->presentation().alphaValue(WINDOW_ALPHA_FADE) * pWindow->presentation().alphaValue(WINDOW_ALPHA_FULLSCREEN) * - pWindow->presentation().alphaValue(WINDOW_ALPHA_LAYOUT) * pWindow->presentation().alphaValue(WINDOW_ALPHA_ACTIVE) * PWORKSPACE->m_alpha->value(); - - if (A >= 1.f) { - // if (PSURFACE->opaque) - // return false; - - CRegion inverseOpaque; - - pixman_box32_t surfbox = {0, 0, PSURFACE->m_current.size.x, PSURFACE->m_current.size.y}; - CRegion opaqueRegion{PSURFACE->m_current.opaque}; - inverseOpaque.set(opaqueRegion).invert(&surfbox).intersect(0, 0, PSURFACE->m_current.size.x, PSURFACE->m_current.size.y); - - if (inverseOpaque.empty()) - return false; - } - - return true; - }; - - bool hasWindows = false; - for (auto const& w : Desktop::windowState()->windows()) { - if (w->m_workspace == pMonitor->m_activeWorkspace && w->mapped() && w->acceptsInput() && w->alphaNonZero() && (!w->isFloating() || *PBLURXRAY)) { - - // check if window is valid - if (!windowShouldBeBlurred(w)) - continue; - - hasWindows = true; - break; - } - } - - for (auto const& m : State::monitorState()->monitors()) { - for (auto const& lsl : m->m_layerSurfaceLayers) { - for (auto const& ls : lsl) { - if (!ls->m_layerSurface || ls->m_ruleApplicator->xray().valueOrDefault() != 1) - continue; - - // if (ls->layerSurface->surface->opaque && ls->alpha->value() >= 1.f) - // continue; - - hasWindows = true; - break; - } - } - } - - if (!hasWindows) - return; - - g_pHyprRenderer->damageMonitor(pMonitor); - pMonitor->m_blurFBShouldRender = true; -} - void CHyprOpenGLImpl::renderTextureWithBlurInternal(SP tex, const CBox& box, const STextureRenderData& data) { auto& m_renderData = g_pHyprRenderer->m_renderData; RASSERT(m_renderData.pMonitor, "Tried to render texture with blur without begin()!"); diff --git a/src/render/OpenGL.hpp b/src/render/OpenGL.hpp index 1b82d2b1d..9f2b42794 100644 --- a/src/render/OpenGL.hpp +++ b/src/render/OpenGL.hpp @@ -51,6 +51,7 @@ namespace Config { } namespace Render::GL { + class CDualKawaseBlurProvider; CBox resolveBlurUV(const CBox& destinationBox, const Vector2D& textureSize); @@ -152,12 +153,14 @@ namespace Render::GL { ~CHyprOpenGLImpl(); struct SRectRenderData { - const CRegion* damage = nullptr; - int round = 0; - float roundingPower = 2.F; - bool blur = false; - float blurA = 1.F; - bool xray = false; + const CRegion* damage = nullptr; + int round = 0; + float roundingPower = 2.F; + bool blur = false; + float blurA = 1.F; + bool xray = false; + std::optional blurPatternBox; + PHLWINDOWREF blurOwner; }; struct STextureRenderData { @@ -229,6 +232,7 @@ namespace Render::GL { void onFramebufferDeleted(GLuint fb); void blend(bool enabled); + bool blendEnabled() const; void scissor(const CBox&, bool transform = true); void scissor(const pixman_box32*, bool transform = true); @@ -236,8 +240,6 @@ namespace Render::GL { void destroyMonitorResources(PHLMONITORREF); - void preRender(PHLMONITOR); - bool saveBufferForMirror(const CBox&); void applyScreenShader(const std::string& path); @@ -362,22 +364,19 @@ namespace Render::GL { // std::optional> getModsForFormat(EGLint format); - // returns the out FB, can be either Mirror or MirrorSwap - SP blurFramebufferWithDamage(float a, CRegion* damage, CGLFramebuffer& source); - - void passCMUniforms(WP, const NColorManagement::PImageDescription imageDescription, const NColorManagement::PImageDescription targetImageDescription, - bool modifySDR, float sdrMinLuminance, int sdrMaxLuminance, const SCMSettings& settings); - void passCMUniforms(WP, const NColorManagement::PImageDescription imageDescription, const NColorManagement::PImageDescription targetImageDescription, - bool modifySDR = false, float sdrMinLuminance = -1.0f, int sdrMaxLuminance = -1); - void passCMUniforms(WP, const NColorManagement::PImageDescription imageDescription); - void passCMUniforms(WP, const NColorManagement::PImageDescription imageDescription, const SCMSettings& settings); - void renderRectInternal(const CBox&, const CHyprColor&, const SRectRenderData& data); - void renderRectWithBlurInternal(const CBox&, const CHyprColor&, const SRectRenderData& data); - void renderRectWithDamageInternal(const CBox&, const CHyprColor&, const SRectRenderData& data); - WP renderScreenShaderInternal(); - WP renderToFBInternal(SP tex, const STextureRenderData& data, eTextureType texType, const CBox& newBox); - void renderTextureInternal(SP, const CBox&, const STextureRenderData& data); - void renderTextureWithBlurInternal(SP, const CBox&, const STextureRenderData& data); + void passCMUniforms(WP, const NColorManagement::PImageDescription imageDescription, const NColorManagement::PImageDescription targetImageDescription, + bool modifySDR, float sdrMinLuminance, int sdrMaxLuminance, const SCMSettings& settings); + void passCMUniforms(WP, const NColorManagement::PImageDescription imageDescription, const NColorManagement::PImageDescription targetImageDescription, + bool modifySDR = false, float sdrMinLuminance = -1.0f, int sdrMaxLuminance = -1); + void passCMUniforms(WP, const NColorManagement::PImageDescription imageDescription); + void passCMUniforms(WP, const NColorManagement::PImageDescription imageDescription, const SCMSettings& settings); + void renderRectInternal(const CBox&, const CHyprColor&, const SRectRenderData& data); + void renderRectWithBlurInternal(const CBox&, const CHyprColor&, const SRectRenderData& data); + void renderRectWithDamageInternal(const CBox&, const CHyprColor&, const SRectRenderData& data); + WP renderScreenShaderInternal(); + WP renderToFBInternal(SP tex, const STextureRenderData& data, eTextureType texType, const CBox& newBox); + void renderTextureInternal(SP, const CBox&, const STextureRenderData& data); + void renderTextureWithBlurInternal(SP, const CBox&, const STextureRenderData& data); friend class IHyprRenderer; friend class CHyprGLRenderer; @@ -385,6 +384,7 @@ namespace Render::GL { friend class CTexPassElement; friend class CPreBlurElement; friend class CSurfacePassElement; + friend class CDualKawaseBlurProvider; }; inline UP g_pHyprOpenGL; diff --git a/src/render/Renderer.cpp b/src/render/Renderer.cpp index 42a5a977d..fe12c002c 100644 --- a/src/render/Renderer.cpp +++ b/src/render/Renderer.cpp @@ -48,6 +48,7 @@ #include "pass/RectPassElement.hpp" #include "pass/RendererHintsPassElement.hpp" #include "pass/SurfacePassElement.hpp" +#include "pass/BackdropScopePassElement.hpp" #include "../debug/log/Logger.hpp" #include "../protocols/ColorManagement.hpp" #include "../protocols/types/ContentType.hpp" @@ -598,7 +599,7 @@ void IHyprRenderer::renderWindow(PHLWINDOW pWindow, PHLMONITOR pMonitor, const T decorate && !pWindow->backend().traits().suggestsNoBorder && Fullscreen::controller()->getFullscreenModes(pWindow).internal != Fullscreen::FSMODE_FULLSCREEN; renderdata.rounding = standalone || renderdata.dontRound ? 0 : pWindow->presentation().rounding() * pMonitor->m_scale; renderdata.roundingPower = standalone || renderdata.dontRound ? 2.0f : pWindow->presentation().roundingPower(); - renderdata.blur = !standalone && shouldBlur(pWindow); + renderdata.blur = !standalone && !m_bRenderingSnapshot && pWindow->shouldBlur(); renderdata.pWindow = pWindow; if (standalone) { @@ -645,7 +646,11 @@ void IHyprRenderer::renderWindow(PHLWINDOW pWindow, PHLMONITOR pMonitor, const T const bool TRANSFORMEDWINDOW = pWindow->effects().hasActiveTransformers(); UP transformedPass; UP passRedirect; - const bool windowBlur = renderdata.blur; + const bool windowBlur = renderdata.blur; + const bool windowBlurUsesLive = windowBlur && !shouldUseNewBlurOptimizations(nullptr, pWindow); + const auto backdropScope = makeShared(); + + addPassElement(makeUnique(CBackdropScopePassElement::eAction::BEGIN, backdropScope)); if (TRANSFORMEDWINDOW) { transformedPass = makeUnique(); @@ -679,12 +684,14 @@ void IHyprRenderer::renderWindow(PHLWINDOW pWindow, PHLMONITOR pMonitor, const T CBox wb = {renderdata.pos.x - pMonitor->m_position.x, renderdata.pos.y - pMonitor->m_position.y, renderdata.w, renderdata.h}; wb.scale(pMonitor->m_scale).round(); CRectPassElement::SRectData data; - data.color = CHyprColor(0, 0, 0, 0); - data.box = wb; - data.round = renderdata.dontRound ? 0 : renderdata.rounding - 1; - data.blur = true; - data.blurA = renderdata.fadeAlpha; - data.xray = shouldUseNewBlurOptimizations(nullptr, pWindow); + data.color = CHyprColor(0, 0, 0, 0); + data.box = wb; + data.round = renderdata.dontRound ? 0 : renderdata.rounding - 1; + data.blur = true; + data.blurA = renderdata.fadeAlpha; + data.xray = shouldUseNewBlurOptimizations(nullptr, pWindow); + data.blurPatternBox = wb; + data.blurOwner = pWindow; addPassElement(makeUnique(data)); renderdata.blur = false; } @@ -740,6 +747,7 @@ void IHyprRenderer::renderWindow(PHLWINDOW pWindow, PHLMONITOR pMonitor, const T .currentBox = currentBox, .blurBox = blurBox, .blur = windowBlur, + .blurUsesLive = windowBlurUsesLive, .blurA = renderdata.fadeAlpha, .blurRound = renderdata.dontRound ? 0 : std::max(renderdata.rounding - 1, 0), .blurRoundingPower = renderdata.roundingPower, @@ -751,6 +759,8 @@ void IHyprRenderer::renderWindow(PHLWINDOW pWindow, PHLMONITOR pMonitor, const T renderdata.blur = windowBlur; } + + addPassElement(makeUnique(CBackdropScopePassElement::eAction::END, backdropScope)); } m_renderData.clipBox = CBox(); @@ -767,7 +777,7 @@ void IHyprRenderer::renderWindow(PHLWINDOW pWindow, PHLMONITOR pMonitor, const T static CConfigValue PBLURIGNOREA = CConfigValue("decoration:blur:popups_ignorealpha"); - renderdata.blur = shouldBlur(pWindow->popupHead()); + renderdata.blur = !m_bRenderingSnapshot && pWindow->popupHead()->shouldBlur(); if (renderdata.blur) { renderdata.discardMode |= DISCARD_ALPHA; @@ -889,7 +899,7 @@ bool IHyprRenderer::preBlurQueued(PHLMONITORREF pMonitor) { if (!pMonitor) return false; - return m_renderData.pMonitor->m_blurFBDirty && *PBLURNEWOPTIMIZE && *PBLUR && m_renderData.pMonitor->m_blurFBShouldRender; + return pMonitor->m_blurFBDirty && *PBLURNEWOPTIMIZE && *PBLUR && pMonitor->m_blurFBShouldRender; } SP IHyprRenderer::createTexture(const SP buffer, bool keepDataCopy) { @@ -950,7 +960,7 @@ void IHyprRenderer::renderLayer(PHLLS pLayer, PHLMONITOR pMonitor, const Time::s CSurfacePassElement::SRenderData renderdata = {pMonitor, time, REALPOS}; renderdata.fadeAlpha = pLayer->alpha()[LS_ALPHA_FADE]->value(); - renderdata.blur = shouldBlur(pLayer); + renderdata.blur = !m_bRenderingSnapshot && pLayer->shouldBlur(); renderdata.surface = pLayer->wlSurface()->resource(); renderdata.decorate = false; renderdata.w = REALSIZ.x; @@ -1518,6 +1528,9 @@ bool IHyprRenderer::shouldUseNewBlurOptimizations(PHLLS pLayer, PHLWINDOW pWindo if (!getBlurTexture(m_renderData.pMonitor)) return false; + if (blurProviderRequiresLiveBlur()) + return false; + if (pWindow && pWindow->m_ruleApplicator->xray().hasValue() && !pWindow->m_ruleApplicator->xray().valueOrDefault()) return false; @@ -1732,6 +1745,7 @@ void IHyprRenderer::renderSessionLockMissing(PHLMONITOR pMonitor) { bool IHyprRenderer::beginRender(PHLMONITOR pMonitor, CRegion& damage, eRenderMode mode, SP buffer, SP fb, bool simple) { m_renderPass.clear(); + m_backdropCaptures.clear(); clearCMSettingsCache(); m_renderMode = mode; m_renderData.pMonitor = pMonitor; @@ -1828,19 +1842,78 @@ Mat3x3 IHyprRenderer::projectBoxToTarget(const CBox& box, std::optional IHyprRenderer::blurMainFramebuffer(float a, CRegion* originalDamage) { - if (!m_renderData.currentFB->getTexture()) { +SP IHyprRenderer::blurMainFramebuffer(float strength, const CRegion& originalDamage, const SBlurContext& context) { + const auto renderTarget = m_renderData.currentFB; + const auto blurSource = !m_backdropCaptures.empty() && m_backdropCaptures.back().framebuffer ? m_backdropCaptures.back().framebuffer : renderTarget; + + if (!blurSource || !blurSource->getTexture()) { Log::logger->log(Log::ERR, "BUG THIS: null fb texture while attempting to blur main fb?! (introspection off?!)"); - return m_renderData.pMonitor->resources()->m_blurFB->getTexture(); // return something to sample from at least + return m_renderData.pMonitor->resources()->m_blurFB; // return something to sample from at least } - auto guard = bindTempFB(m_renderData.currentFB); // blurFramebuffer messes with FB bindings - return blurFramebuffer(m_renderData.currentFB, a, originalDamage); + auto guard = bindTempFB(renderTarget); // blurFramebuffer messes with FB bindings + return blurFramebuffer(blurSource, strength, originalDamage, context); } -void IHyprRenderer::preBlurForCurrentMonitor(CRegion* fakeDamage) { +void IHyprRenderer::beginBackdropScope(SP scope) { + RASSERT(scope, "Cannot begin a null backdrop scope"); - const auto blurredTex = blurMainFramebuffer(1, fakeDamage); + SP backdrop; + if (scope->required && !scope->damage.empty() && m_renderData.currentFB && m_renderData.currentFB->getTexture()) { + backdrop = m_renderData.pMonitor->resources()->getUnusedWorkBuffer(); + if (backdrop) { + const auto renderTarget = m_renderData.currentFB; + const auto savedDamage = m_renderData.damage.copy(); + const auto savedRenderModif = m_renderData.renderModif; + const auto savedNearest = m_renderData.useNearestNeighbor; + const auto backend = glBackend(); + const auto savedBlend = backend && backend->blendEnabled(); + + { + auto guard = bindTempFB(backdrop); + m_renderData.damage = scope->damage; + m_renderData.renderModif = {}; + m_renderData.useNearestNeighbor = true; + blend(false); + renderOffToMain(renderTarget); + blend(savedBlend); + } + + m_renderData.damage = savedDamage; + m_renderData.renderModif = savedRenderModif; + m_renderData.useNearestNeighbor = savedNearest; + } else { + static bool warned = false; + if (!warned) { + warned = true; + Log::logger->log(Log::WARN, "Failed to allocate a clean backdrop buffer; live blur will include the current window's rendered content"); + } + } + } + + m_backdropCaptures.emplace_back(SBackdropCapture{.scope = std::move(scope), .framebuffer = std::move(backdrop)}); +} + +void IHyprRenderer::endBackdropScope(SP scope) { + RASSERT(!m_backdropCaptures.empty() && m_backdropCaptures.back().scope == scope, "Unbalanced runtime backdrop scope"); + m_backdropCaptures.pop_back(); +} + +void IHyprRenderer::scheduleFrameForAnimatedBlur(const CRegion& damage, bool usesPrecomputedBlur) { + const auto monitor = m_renderData.pMonitor; + if (m_renderMode != RENDER_MODE_NORMAL || !monitor || monitor->isMirror() || damage.empty()) + return; + + if (usesPrecomputedBlur) + monitor->m_blurFBDirty = true; + + monitor->addDamage(damage); +} + +void IHyprRenderer::preBlurForCurrentMonitor(const CRegion& fakeDamage) { + + const auto blurredFB = blurMainFramebuffer(1, fakeDamage); + const auto blurredTex = blurredFB->getTexture(); // render onto blurFB auto guard = bindTempFB(m_renderData.pMonitor->resources()->m_blurFB); @@ -1851,9 +1924,9 @@ void IHyprRenderer::preBlurForCurrentMonitor(CRegion* fakeDamage) { CTexPassElement::SRenderData{ .tex = blurredTex, .box = CBox{0, 0, m_renderData.pMonitor->m_transformedSize.x, m_renderData.pMonitor->m_transformedSize.y}, - .damage = *fakeDamage, + .damage = fakeDamage, }, - *fakeDamage); // .noAA = true + fakeDamage); // .noAA = true } static bool isSDR2HDR(const NColorManagement::SImageDescription& imageDescription, const NColorManagement::SImageDescription& targetImageDescription) { @@ -3267,6 +3340,7 @@ void IHyprRenderer::renderFadeouts(PHLMONITOR monitor, Desktop::eFadeoutPlane pl data.blur = EFFECTS.textureBlur.enabled; data.blurA = EFFECTS.textureBlur.alpha; data.forceBlurBlend = EFFECTS.textureBlur.forceBlend; + data.blurShapeInvalid = true; data.ignoreAlpha = EFFECTS.textureBlur.ignoreAlpha; data.blockBlurOptimization = EFFECTS.textureBlur.blockBlurOptimization; @@ -3281,47 +3355,6 @@ NColorManagement::PImageDescription IHyprRenderer::workBufferImageDescription() return m_renderData.pMonitor->workBufferImageDescription(); } -bool IHyprRenderer::shouldBlur(PHLLS ls) { - if (m_bRenderingSnapshot) - return false; - - static auto PBLUR = CConfigValue("decoration:blur:enabled"); - if (!*PBLUR) - return false; - - auto surface = ls->wlSurface(); - if (surface && surface->m_hasBackgroundEffect) - return !surface->m_blurRegion.empty(); - - return ls->m_ruleApplicator->blur().valueOrDefault(); -} - -bool IHyprRenderer::shouldBlur(PHLWINDOW w) { - if (m_bRenderingSnapshot) - return false; - - static auto PBLUR = CConfigValue("decoration:blur:enabled"); - if (!*PBLUR) - return false; - - const bool DONT_BLUR = w->m_ruleApplicator->noBlur().valueOrDefault() || w->m_ruleApplicator->RGBX().valueOrDefault() || w->presentation().opaque(); - if (DONT_BLUR) - return false; - - auto surface = w->wlSurface(); - if (surface && surface->m_hasBackgroundEffect) - return !surface->m_blurRegion.empty(); - - return true; -} - -bool IHyprRenderer::shouldBlur(WP p) { - static CConfigValue PBLURPOPUPS = CConfigValue("decoration:blur:popups"); - static CConfigValue PBLUR = CConfigValue("decoration:blur:enabled"); - - return *PBLURPOPUPS && *PBLUR; -} - SP IHyprRenderer::renderSplash(const std::function(const int, const int, unsigned char* const)>& handleData, const int fontSize, const int maxWidth, const int maxHeight) { static auto PSPLASHCOLOR = CConfigValue("misc:col.splash"); diff --git a/src/render/Renderer.hpp b/src/render/Renderer.hpp index d74f3c530..bc2827706 100644 --- a/src/render/Renderer.hpp +++ b/src/render/Renderer.hpp @@ -10,6 +10,7 @@ #include #include #include "OpenGL.hpp" +#include "blur/Provider.hpp" #include "./SyncFDManager.hpp" #include "./pass/Pass.hpp" #include "./pass/BorderPassElement.hpp" @@ -45,6 +46,7 @@ class CEventLoopTimer; class CToplevelExportProtocolManager; class CInputManager; struct SSessionLockSurface; +struct SBackdropScope; namespace Screenshare { class CScreenshareFrame; }; @@ -187,30 +189,37 @@ namespace Render { virtual void disableScissor() = 0; virtual void blend(bool enabled) = 0; virtual void drawShadow(const CBox& box, int round, float roundingPower, int range, const Config::CGradientValueData& color, float a) = 0; - virtual void drawShadow(const CBox& box, int round, float roundingPower, int range, const Config::CGradientValueData& grad1, const Config::CGradientValueData& grad2, - float lerp, float a) = 0; - virtual void drawGlow(const CBox& box, int round, float roundingPower, int range, const Config::CGradientValueData& color, float a) = 0; - virtual void drawGlow(const CBox& box, int round, float roundingPower, int range, const Config::CGradientValueData& grad1, const Config::CGradientValueData& grad2, - float lerp, float a) = 0; - virtual void setViewport(int x, int y, int width, int height) = 0; + virtual void drawShadow(const CBox& box, int round, float roundingPower, int range, const Config::CGradientValueData& grad1, const Config::CGradientValueData& grad2, + float lerp, float a) = 0; + virtual void drawGlow(const CBox& box, int round, float roundingPower, int range, const Config::CGradientValueData& color, float a) = 0; + virtual void drawGlow(const CBox& box, int round, float roundingPower, int range, const Config::CGradientValueData& grad1, const Config::CGradientValueData& grad2, + float lerp, float a) = 0; + virtual void setViewport(int x, int y, int width, int height) = 0; - bool preBlurQueued(PHLMONITORREF pMonitor); - void sendFrameEventsToWorkspace(PHLMONITOR pMonitor, PHLWORKSPACE pWorkspace, const Time::steady_tp& now); + bool preBlurQueued(PHLMONITORREF pMonitor); + void sendFrameEventsToWorkspace(PHLMONITOR pMonitor, PHLWORKSPACE pWorkspace, const Time::steady_tp& now); - void setProjectionType(const Vector2D& fbSize); - void setProjectionType(eRenderProjectionType projectionType); - Mat3x3 getBoxProjection(const CBox& box, std::optional transform = std::nullopt); - Mat3x3 projectBoxToTarget(const CBox& box, std::optional transform = std::nullopt); + void setProjectionType(const Vector2D& fbSize); + void setProjectionType(eRenderProjectionType projectionType); + Mat3x3 getBoxProjection(const CBox& box, std::optional transform = std::nullopt); + Mat3x3 projectBoxToTarget(const CBox& box, std::optional transform = std::nullopt); - SP blurMainFramebuffer(float a, CRegion* originalDamage); - virtual SP blurFramebuffer(SP source, float a, CRegion* originalDamage) = 0; - void preBlurForCurrentMonitor(CRegion* fakeDamage); + SP blurMainFramebuffer(float strength, const CRegion& originalDamage, const Render::SBlurContext& context = {}); + void beginBackdropScope(SP scope); + void endBackdropScope(SP scope); + virtual SP blurFramebuffer(SP source, float strength, const CRegion& originalDamage, const Render::SBlurContext& context = {}) = 0; + virtual void refreshBlurProvider() = 0; + virtual void expandBlurDamage(CRegion& damage, float multiplier = 1.F) const = 0; + virtual bool blurProviderIsAnimated() const = 0; + virtual bool blurProviderRequiresLiveBlur() const = 0; + void scheduleFrameForAnimatedBlur(const CRegion& damage, bool usesPrecomputedBlur); + void preBlurForCurrentMonitor(const CRegion& fakeDamage); - SCMSettings getCMSettings(const NColorManagement::PImageDescription imageDescription, const NColorManagement::PImageDescription targetImageDescription, - SP surface = nullptr, bool modifySDR = false, float sdrMinLuminance = -1.0f, int sdrMaxLuminance = -1, - bool shouldUseSurface = false); - void clearCMSettingsCache(); - virtual bool reloadShaders(const std::string& path = "") = 0; + SCMSettings getCMSettings(const NColorManagement::PImageDescription imageDescription, const NColorManagement::PImageDescription targetImageDescription, + SP surface = nullptr, bool modifySDR = false, float sdrMinLuminance = -1.0f, int sdrMaxLuminance = -1, + bool shouldUseSurface = false); + void clearCMSettingsCache(); + virtual bool reloadShaders(const std::string& path = "") = 0; protected: virtual void renderOffToMain(SP off) = 0; @@ -277,10 +286,6 @@ namespace Render { ASP m_backgroundResource; bool m_backgroundResourceFailed = false; - bool shouldBlur(PHLLS ls); - bool shouldBlur(PHLWINDOW w); - bool shouldBlur(WP p); - bool m_cursorHidden = false; bool m_cursorHiddenByCondition = false; bool m_cursorHasSurface = false; @@ -302,6 +307,13 @@ namespace Render { std::vector m_renderUnfocused; SP m_renderUnfocusedTimer; + struct SBackdropCapture { + SP scope; + SP framebuffer; + }; + + std::vector m_backdropCaptures; + friend class CRenderPass; friend class Render::GL::CHyprOpenGLImpl; friend class CToplevelExportFrame; diff --git a/src/render/Shader.cpp b/src/render/Shader.cpp index 5aaafb7da..6e18d14f5 100644 --- a/src/render/Shader.cpp +++ b/src/render/Shader.cpp @@ -183,46 +183,113 @@ void CShader::getUniformLocations() { fullSize = getUniform("screenSize"); m_uniformLocations[SHADER_FULL_SIZE] = fullSize; - m_uniformLocations[SHADER_FULL_SIZE_UNTRANSFORMED] = getUniform("fullSizeUntransformed"); - m_uniformLocations[SHADER_RADIUS] = getUniform("radius"); - m_uniformLocations[SHADER_RADIUS_OUTER] = getUniform("radiusOuter"); - m_uniformLocations[SHADER_ROUNDING_POWER] = getUniform("roundingPower"); - m_uniformLocations[SHADER_THICK] = getUniform("thick"); - m_uniformLocations[SHADER_HALFPIXEL] = getUniform("halfpixel"); - m_uniformLocations[SHADER_RANGE] = getUniform("range"); - m_uniformLocations[SHADER_SHADOW_POWER] = getUniform("shadowPower"); - m_uniformLocations[SHADER_USE_ALPHA_MATTE] = getUniform("useAlphaMatte"); - m_uniformLocations[SHADER_APPLY_TINT] = getUniform("applyTint"); - m_uniformLocations[SHADER_TINT] = getUniform("tint"); - m_uniformLocations[SHADER_GRADIENT] = getUniform("gradient"); - m_uniformLocations[SHADER_GRADIENT_LENGTH] = getUniform("gradientLength"); - m_uniformLocations[SHADER_GRADIENT2] = getUniform("gradient2"); - m_uniformLocations[SHADER_GRADIENT2_LENGTH] = getUniform("gradient2Length"); - m_uniformLocations[SHADER_ANGLE] = getUniform("angle"); - m_uniformLocations[SHADER_ANGLE2] = getUniform("angle2"); - m_uniformLocations[SHADER_GRADIENT_LERP] = getUniform("gradientLerp"); - m_uniformLocations[SHADER_TIME] = getUniform("time"); - m_uniformLocations[SHADER_DISTORT] = getUniform("distort"); - m_uniformLocations[SHADER_WL_OUTPUT] = getUniform("wl_output"); - m_uniformLocations[SHADER_CONTRAST] = getUniform("contrast"); - m_uniformLocations[SHADER_PASSES] = getUniform("passes"); - m_uniformLocations[SHADER_VIBRANCY] = getUniform("vibrancy"); - m_uniformLocations[SHADER_VIBRANCY_DARKNESS] = getUniform("vibrancy_darkness"); - m_uniformLocations[SHADER_BRIGHTNESS] = getUniform("brightness"); - m_uniformLocations[SHADER_NOISE] = getUniform("noise"); - m_uniformLocations[SHADER_POINTER] = getUniform("pointer_position"); - m_uniformLocations[SHADER_POINTER_SHAPE] = getUniform("pointer_shape"); - m_uniformLocations[SHADER_POINTER_SWITCH_TIME] = getUniform("pointer_switch_time"); - m_uniformLocations[SHADER_POINTER_SHAPE_PREVIOUS] = getUniform("pointer_shape_previous"); - m_uniformLocations[SHADER_POINTER_PRESSED_POSITIONS] = getUniform("pointer_pressed_positions"); - m_uniformLocations[SHADER_POINTER_HIDDEN] = getUniform("pointer_hidden"); - m_uniformLocations[SHADER_POINTER_KILLING] = getUniform("pointer_killing"); - m_uniformLocations[SHADER_POINTER_PRESSED_TIMES] = getUniform("pointer_pressed_times"); - m_uniformLocations[SHADER_POINTER_PRESSED_KILLED] = getUniform("pointer_pressed_killed"); - m_uniformLocations[SHADER_POINTER_PRESSED_TOUCHED] = getUniform("pointer_pressed_touched"); - m_uniformLocations[SHADER_POINTER_INACTIVE_TIMEOUT] = getUniform("pointer_inactive_timeout"); - m_uniformLocations[SHADER_POINTER_LAST_ACTIVE] = getUniform("pointer_last_active"); - m_uniformLocations[SHADER_POINTER_SIZE] = getUniform("pointer_size"); + m_uniformLocations[SHADER_FULL_SIZE_UNTRANSFORMED] = getUniform("fullSizeUntransformed"); + m_uniformLocations[SHADER_RADIUS] = getUniform("radius"); + m_uniformLocations[SHADER_RADIUS_OUTER] = getUniform("radiusOuter"); + m_uniformLocations[SHADER_ROUNDING_POWER] = getUniform("roundingPower"); + m_uniformLocations[SHADER_THICK] = getUniform("thick"); + m_uniformLocations[SHADER_HALFPIXEL] = getUniform("halfpixel"); + m_uniformLocations[SHADER_RANGE] = getUniform("range"); + m_uniformLocations[SHADER_SHADOW_POWER] = getUniform("shadowPower"); + m_uniformLocations[SHADER_USE_ALPHA_MATTE] = getUniform("useAlphaMatte"); + m_uniformLocations[SHADER_APPLY_TINT] = getUniform("applyTint"); + m_uniformLocations[SHADER_TINT] = getUniform("tint"); + m_uniformLocations[SHADER_GRADIENT] = getUniform("gradient"); + m_uniformLocations[SHADER_GRADIENT_LENGTH] = getUniform("gradientLength"); + m_uniformLocations[SHADER_GRADIENT2] = getUniform("gradient2"); + m_uniformLocations[SHADER_GRADIENT2_LENGTH] = getUniform("gradient2Length"); + m_uniformLocations[SHADER_ANGLE] = getUniform("angle"); + m_uniformLocations[SHADER_ANGLE2] = getUniform("angle2"); + m_uniformLocations[SHADER_GRADIENT_LERP] = getUniform("gradientLerp"); + m_uniformLocations[SHADER_TIME] = getUniform("time"); + m_uniformLocations[SHADER_DISTORT] = getUniform("distort"); + m_uniformLocations[SHADER_WL_OUTPUT] = getUniform("wl_output"); + m_uniformLocations[SHADER_CONTRAST] = getUniform("contrast"); + m_uniformLocations[SHADER_PASSES] = getUniform("passes"); + m_uniformLocations[SHADER_VIBRANCY] = getUniform("vibrancy"); + m_uniformLocations[SHADER_VIBRANCY_DARKNESS] = getUniform("vibrancy_darkness"); + m_uniformLocations[SHADER_BRIGHTNESS] = getUniform("brightness"); + m_uniformLocations[SHADER_NOISE] = getUniform("noise"); + m_uniformLocations[SHADER_GLASS_REFRACTION] = getUniform("glassRefraction"); + m_uniformLocations[SHADER_GLASS_SIZE] = getUniform("glassSize"); + m_uniformLocations[SHADER_GLASS_ROUGHNESS] = getUniform("glassRoughness"); + m_uniformLocations[SHADER_GLASS_POSITION] = getUniform("glassPosition"); + m_uniformLocations[SHADER_DROPS_POSITION] = getUniform("dropsPosition"); + m_uniformLocations[SHADER_SHARP_TEX] = getUniform("sharpTex"); + m_uniformLocations[SHADER_RIPPLE_COUNT] = getUniform("rippleCount"); + m_uniformLocations[SHADER_RIPPLE_IMPULSES] = getUniform("rippleImpulses[0]"); + m_uniformLocations[SHADER_RIPPLE_PARAMS] = getUniform("rippleParams"); + m_uniformLocations[SHADER_WATER_ENABLED] = getUniform("waterEnabled"); + m_uniformLocations[SHADER_WATER_STATE_TEX] = getUniform("waterStateTex"); + m_uniformLocations[SHADER_WATER_TEXEL_SIZE] = getUniform("waterTexelSize"); + m_uniformLocations[SHADER_WATER_EXTENT] = getUniform("waterExtent"); + m_uniformLocations[SHADER_WATER_REFRACTION] = getUniform("waterRefraction"); + m_uniformLocations[SHADER_WATER_PARAMS] = getUniform("waterParams"); + m_uniformLocations[SHADER_WATER_IMPULSE_COUNT] = getUniform("waterImpulseCount"); + m_uniformLocations[SHADER_WATER_IMPULSES] = getUniform("waterImpulses[0]"); + m_uniformLocations[SHADER_FLUIDJAR_PARTICLE_TEX] = getUniform("fluidJarParticleTex"); + m_uniformLocations[SHADER_FLUIDJAR_GRAPH_TEX] = getUniform("fluidJarGraphTex"); + m_uniformLocations[SHADER_FLUIDJAR_TRACKING_TEX] = getUniform("fluidJarTrackingTex"); + m_uniformLocations[SHADER_FLUIDJAR_VISUAL_TEX] = getUniform("fluidJarVisualTex"); + m_uniformLocations[SHADER_FLUIDJAR_RESOLUTION] = getUniform("fluidJarResolution"); + m_uniformLocations[SHADER_FLUIDJAR_GRID_SIZE] = getUniform("fluidJarGridSize"); + m_uniformLocations[SHADER_FLUIDJAR_PARTICLE_COUNT] = getUniform("fluidJarParticleCount"); + m_uniformLocations[SHADER_FLUIDJAR_FRAME] = getUniform("fluidJarFrame"); + m_uniformLocations[SHADER_FLUIDJAR_DT] = getUniform("fluidJarDt"); + m_uniformLocations[SHADER_FLUIDJAR_MASS] = getUniform("fluidJarMass"); + m_uniformLocations[SHADER_FLUIDJAR_OLD_RESOLUTION] = getUniform("fluidJarOldResolution"); + m_uniformLocations[SHADER_FLUIDJAR_OLD_GRID_SIZE] = getUniform("fluidJarOldGridSize"); + m_uniformLocations[SHADER_FLUIDJAR_OLD_PARTICLE_COUNT] = getUniform("fluidJarOldParticleCount"); + m_uniformLocations[SHADER_FLUIDJAR_TRANSFORM] = getUniform("fluidJarTransform"); + m_uniformLocations[SHADER_FLUIDJAR_VELOCITY_SCALE] = getUniform("fluidJarVelocityScale"); + m_uniformLocations[SHADER_FLUIDJAR_WALL_VELOCITIES] = getUniform("fluidJarWallVelocities"); + m_uniformLocations[SHADER_FLUIDJAR_HISTORY_TEX] = getUniform("fluidJarHistoryTex"); + m_uniformLocations[SHADER_FLUIDJAR_HISTORY_TRANSFORM] = getUniform("fluidJarHistoryTransform"); + m_uniformLocations[SHADER_FLUIDJAR_HISTORY_FALLBACK] = getUniform("fluidJarHistoryFallback"); + m_uniformLocations[SHADER_FLUIDJAR_EXTENT] = getUniform("fluidJarExtent"); + m_uniformLocations[SHADER_FLUIDJAR_OUTPUT_TRANSFORM] = getUniform("fluidJarOutputTransform"); + m_uniformLocations[SHADER_FLUIDJAR_OUTPUT_OFFSET] = getUniform("fluidJarOutputOffset"); + m_uniformLocations[SHADER_FLUIDJAR_LOGICAL_SIZE] = getUniform("fluidJarLogicalSize"); + m_uniformLocations[SHADER_FLUIDJAR_COLOR] = getUniform("fluidJarColor"); + m_uniformLocations[SHADER_FLUIDJAR_REFRACTION] = getUniform("fluidJarRefraction"); + m_uniformLocations[SHADER_FLUIDJAR_TRANSFER_FUNCTION] = getUniform("fluidJarTransferFunction"); + m_uniformLocations[SHADER_FLUIDJAR_VISUAL_RESPONSE] = getUniform("fluidJarVisualResponse"); + m_uniformLocations[SHADER_FLUIDJAR_STRENGTH] = getUniform("fluidJarStrength"); + m_uniformLocations[SHADER_FLUIDJAR_TURBULENCE] = getUniform("fluidJarTurbulence"); + m_uniformLocations[SHADER_FLUIDJAR_DISTORTION] = getUniform("fluidJarDistortion"); + m_uniformLocations[SHADER_FLUIDJAR_ENABLED] = getUniform("fluidJarEnabled"); + m_uniformLocations[SHADER_ACRYLIC_ENABLED] = getUniform("acrylicEnabled"); + m_uniformLocations[SHADER_ACRYLIC_EXTENT] = getUniform("acrylicExtent"); + m_uniformLocations[SHADER_ACRYLIC_RADIUS] = getUniform("acrylicRadius"); + m_uniformLocations[SHADER_ACRYLIC_ROUNDING_POWER] = getUniform("acrylicRoundingPower"); + m_uniformLocations[SHADER_ACRYLIC_REFRACTION] = getUniform("acrylicRefraction"); + m_uniformLocations[SHADER_ACRYLIC_BULB] = getUniform("acrylicBulb"); + m_uniformLocations[SHADER_ACRYLIC_CLARITY] = getUniform("acrylicClarity"); + m_uniformLocations[SHADER_ACRYLIC_ABERRATION] = getUniform("acrylicAberration"); + m_uniformLocations[SHADER_ACRYLIC_TINT] = getUniform("acrylicTint"); + m_uniformLocations[SHADER_ACRYLIC_STRENGTH] = getUniform("acrylicStrength"); + m_uniformLocations[SHADER_ACRYLIC_TRANSFER_FUNCTION] = getUniform("acrylicTransferFunction"); + m_uniformLocations[SHADER_ACRYLIC_LUMINANCE_SCALE] = getUniform("acrylicLuminanceScale"); + m_uniformLocations[SHADER_AURORA_INTENSITY] = getUniform("auroraIntensity"); + m_uniformLocations[SHADER_AURORA_COLOR1] = getUniform("auroraColor1"); + m_uniformLocations[SHADER_AURORA_COLOR2] = getUniform("auroraColor2"); + m_uniformLocations[SHADER_AURORA_TRANSFER_FUNCTION] = getUniform("auroraTransferFunction"); + m_uniformLocations[SHADER_HAZE_INTENSITY] = getUniform("hazeIntensity"); + m_uniformLocations[SHADER_HAZE_IRIDESCENCE] = getUniform("hazeIridescence"); + m_uniformLocations[SHADER_HAZE_TRANSFER_FUNCTION] = getUniform("hazeTransferFunction"); + m_uniformLocations[SHADER_POINTER] = getUniform("pointer_position"); + m_uniformLocations[SHADER_POINTER_SHAPE] = getUniform("pointer_shape"); + m_uniformLocations[SHADER_POINTER_SWITCH_TIME] = getUniform("pointer_switch_time"); + m_uniformLocations[SHADER_POINTER_SHAPE_PREVIOUS] = getUniform("pointer_shape_previous"); + m_uniformLocations[SHADER_POINTER_PRESSED_POSITIONS] = getUniform("pointer_pressed_positions"); + m_uniformLocations[SHADER_POINTER_HIDDEN] = getUniform("pointer_hidden"); + m_uniformLocations[SHADER_POINTER_KILLING] = getUniform("pointer_killing"); + m_uniformLocations[SHADER_POINTER_PRESSED_TIMES] = getUniform("pointer_pressed_times"); + m_uniformLocations[SHADER_POINTER_PRESSED_KILLED] = getUniform("pointer_pressed_killed"); + m_uniformLocations[SHADER_POINTER_PRESSED_TOUCHED] = getUniform("pointer_pressed_touched"); + m_uniformLocations[SHADER_POINTER_INACTIVE_TIMEOUT] = getUniform("pointer_inactive_timeout"); + m_uniformLocations[SHADER_POINTER_LAST_ACTIVE] = getUniform("pointer_last_active"); + m_uniformLocations[SHADER_POINTER_SIZE] = getUniform("pointer_size"); } void CShader::createVao() { diff --git a/src/render/Shader.hpp b/src/render/Shader.hpp index 3df2618bd..1d8feb969 100644 --- a/src/render/Shader.hpp +++ b/src/render/Shader.hpp @@ -91,6 +91,73 @@ enum eShaderUniform : uint8_t { SHADER_BLUR_ALPHA_MATTE, SHADER_BLUR_ALPHA, SHADER_TONEMAP_MODE, + SHADER_GLASS_REFRACTION, + SHADER_GLASS_SIZE, + SHADER_GLASS_ROUGHNESS, + SHADER_GLASS_POSITION, + SHADER_DROPS_POSITION, + SHADER_SHARP_TEX, + SHADER_RIPPLE_COUNT, + SHADER_RIPPLE_IMPULSES, + SHADER_RIPPLE_PARAMS, + SHADER_WATER_ENABLED, + SHADER_WATER_STATE_TEX, + SHADER_WATER_TEXEL_SIZE, + SHADER_WATER_EXTENT, + SHADER_WATER_REFRACTION, + SHADER_WATER_PARAMS, + SHADER_WATER_IMPULSE_COUNT, + SHADER_WATER_IMPULSES, + SHADER_FLUIDJAR_PARTICLE_TEX, + SHADER_FLUIDJAR_GRAPH_TEX, + SHADER_FLUIDJAR_TRACKING_TEX, + SHADER_FLUIDJAR_VISUAL_TEX, + SHADER_FLUIDJAR_RESOLUTION, + SHADER_FLUIDJAR_GRID_SIZE, + SHADER_FLUIDJAR_PARTICLE_COUNT, + SHADER_FLUIDJAR_FRAME, + SHADER_FLUIDJAR_DT, + SHADER_FLUIDJAR_MASS, + SHADER_FLUIDJAR_OLD_RESOLUTION, + SHADER_FLUIDJAR_OLD_GRID_SIZE, + SHADER_FLUIDJAR_OLD_PARTICLE_COUNT, + SHADER_FLUIDJAR_TRANSFORM, + SHADER_FLUIDJAR_VELOCITY_SCALE, + SHADER_FLUIDJAR_WALL_VELOCITIES, + SHADER_FLUIDJAR_HISTORY_TEX, + SHADER_FLUIDJAR_HISTORY_TRANSFORM, + SHADER_FLUIDJAR_HISTORY_FALLBACK, + SHADER_FLUIDJAR_EXTENT, + SHADER_FLUIDJAR_OUTPUT_TRANSFORM, + SHADER_FLUIDJAR_OUTPUT_OFFSET, + SHADER_FLUIDJAR_LOGICAL_SIZE, + SHADER_FLUIDJAR_COLOR, + SHADER_FLUIDJAR_REFRACTION, + SHADER_FLUIDJAR_TRANSFER_FUNCTION, + SHADER_FLUIDJAR_VISUAL_RESPONSE, + SHADER_FLUIDJAR_STRENGTH, + SHADER_FLUIDJAR_TURBULENCE, + SHADER_FLUIDJAR_DISTORTION, + SHADER_FLUIDJAR_ENABLED, + SHADER_ACRYLIC_ENABLED, + SHADER_ACRYLIC_EXTENT, + SHADER_ACRYLIC_RADIUS, + SHADER_ACRYLIC_ROUNDING_POWER, + SHADER_ACRYLIC_REFRACTION, + SHADER_ACRYLIC_BULB, + SHADER_ACRYLIC_CLARITY, + SHADER_ACRYLIC_ABERRATION, + SHADER_ACRYLIC_TINT, + SHADER_ACRYLIC_STRENGTH, + SHADER_ACRYLIC_TRANSFER_FUNCTION, + SHADER_ACRYLIC_LUMINANCE_SCALE, + SHADER_AURORA_INTENSITY, + SHADER_AURORA_COLOR1, + SHADER_AURORA_COLOR2, + SHADER_AURORA_TRANSFER_FUNCTION, + SHADER_HAZE_INTENSITY, + SHADER_HAZE_IRIDESCENCE, + SHADER_HAZE_TRANSFER_FUNCTION, SHADER_LAST, }; diff --git a/src/render/ShaderLoader.hpp b/src/render/ShaderLoader.hpp index 032da697b..3247b3b4f 100644 --- a/src/render/ShaderLoader.hpp +++ b/src/render/ShaderLoader.hpp @@ -56,6 +56,25 @@ namespace Render { SH_FRAG_SURFACE, SH_FRAG_BORDER1, SH_FRAG_GLITCH, + SH_FRAG_FROSTFINISH, + SH_FRAG_RIPPLEFINISH, + SH_FRAG_DROPSFINISH, + SH_FRAG_WATERSTEP, + SH_FRAG_WATERFINISH, + SH_FRAG_FLUIDJARINIT, + SH_FRAG_FLUIDJARSTEP, + SH_FRAG_FLUIDJARGRAPH, + SH_FRAG_FLUIDJARTRACK, + SH_FRAG_FLUIDJARVISUAL, + SH_FRAG_FLUIDJARRESAMPLE, + SH_FRAG_FLUIDJARHISTORYRESAMPLE, + SH_FRAG_FLUIDJARTRACKINGRESAMPLE, + SH_FRAG_FLUIDJARFINISH, + SH_FRAG_PRISMFINISH, + SH_FRAG_HEATSHIMMERFINISH, + SH_FRAG_ACRYLICFINISH, + SH_FRAG_AURORAFINISH, + SH_FRAG_HAZEFINISH, SH_FRAG_LAST, }; diff --git a/src/render/blur/Provider.hpp b/src/render/blur/Provider.hpp new file mode 100644 index 000000000..7e7b01531 --- /dev/null +++ b/src/render/blur/Provider.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include "../../desktop/DesktopTypes.hpp" +#include "../../helpers/math/Math.hpp" +#include "../Framebuffer.hpp" + +#include + +namespace Render { + struct SBlurShape { + CBox box; + float radius = 0.F; + float roundingPower = 2.F; + }; + + struct SBlurContext { + std::optional patternBox; + PHLWINDOWREF owner; + std::optional shape; + }; + + enum class eBlurType : uint8_t { + BLUR_DUAL_KAWASE = 0, + BLUR_FROST = 1, + BLUR_RIPPLE = 2, + BLUR_DROPS = 3, + BLUR_WATER = 4, + BLUR_FLUID_JAR = 5, + BLUR_PRISM = 6, + BLUR_HEAT_SHIMMER = 7, + BLUR_ACRYLIC = 8, + BLUR_AURORA = 9, + BLUR_HAZE = 10, + }; + + // "Jaki kurwa provident????" + class IBlurProvider { + public: + virtual ~IBlurProvider() = default; + + virtual eBlurType type() const noexcept = 0; + virtual bool isAnimated() const noexcept = 0; + virtual bool requiresLiveBlur() const noexcept = 0; + + virtual void expandDamage(CRegion& damage, float multiplier = 1.F) const = 0; + virtual SP blur(SP source, float strength, const CRegion& originalDamage, const SBlurContext& context = {}) = 0; + + protected: + IBlurProvider() = default; + }; +}; diff --git a/src/render/gl/GLElementRenderer.cpp b/src/render/gl/GLElementRenderer.cpp index 446774e72..1290b6af9 100644 --- a/src/render/gl/GLElementRenderer.cpp +++ b/src/render/gl/GLElementRenderer.cpp @@ -77,7 +77,7 @@ void CGLElementRenderer::draw(WP element, const CRegion& da void CGLElementRenderer::draw(WP element, const CRegion& damage) { auto dmg = damage; - g_pHyprRenderer->preBlurForCurrentMonitor(&dmg); + g_pHyprRenderer->preBlurForCurrentMonitor(dmg); }; void CGLElementRenderer::draw(WP element, const CRegion& damage) { @@ -87,7 +87,13 @@ void CGLElementRenderer::draw(WP element, const CRegion& damag g_pHyprOpenGL->renderRect(m_data.box, m_data.color, {.damage = &damage, .round = m_data.round, .roundingPower = m_data.roundingPower}); else g_pHyprOpenGL->renderRect(m_data.box, m_data.color, - {.round = m_data.round, .roundingPower = m_data.roundingPower, .blur = true, .blurA = m_data.blurA, .xray = m_data.xray}); + {.round = m_data.round, + .roundingPower = m_data.roundingPower, + .blur = true, + .blurA = m_data.blurA, + .xray = m_data.xray, + .blurPatternBox = m_data.blurPatternBox, + .blurOwner = m_data.blurOwner}); }; void CGLElementRenderer::draw(WP element, const CRegion& damage) { diff --git a/src/render/gl/blur/Acrylic.cpp b/src/render/gl/blur/Acrylic.cpp new file mode 100644 index 000000000..a3b717aab --- /dev/null +++ b/src/render/gl/blur/Acrylic.cpp @@ -0,0 +1,101 @@ +#include "Acrylic.hpp" + +#include "../../Renderer.hpp" +#include "../../Shader.hpp" +#include "../../ShaderLoader.hpp" +#include "../../../config/ConfigValue.hpp" +#include "../../../helpers/Color.hpp" +#include "../../../helpers/cm/ColorManagement.hpp" + +#include +#include + +using namespace Render; +using namespace Render::GL; +using namespace NColorManagement; + +static constexpr float MAX_ACRYLIC_REFRACTION = 48.F; +static constexpr float MIN_ACRYLIC_BULB = 4.F; +static constexpr float MAX_ACRYLIC_BULB = 256.F; + +static float acrylicSampleRadius(float refraction) { + const auto CLAMPED = std::clamp(refraction, 0.F, MAX_ACRYLIC_REFRACTION); + return CLAMPED > 0.F ? std::ceil(CLAMPED + 1.F) : 0.F; +} + +static float srgbToLinear(float value) { + return value <= 0.04045F ? value / 12.92F : std::pow((value + 0.055F) / 1.055F, 2.4F); +} + +static float acrylicLuminanceScale() { + const auto INTERMEDIATE = getDefaultImageDescription(); + const auto WORKBUFFER = g_pHyprRenderer->workBufferImageDescription(); + if (!WORKBUFFER) + return 1.F; + + const auto MINIMUM = INTERMEDIATE->value().getTFMinLuminance(); + const auto MAXIMUM = INTERMEDIATE->value().getTFMaxLuminance(); + const auto RANGE = std::max(MAXIMUM, sc(WORKBUFFER->value().luminances.max)) - MINIMUM; + return (MAXIMUM - MINIMUM) / std::max(RANGE, 0.001F); +} + +CAcrylicBlurProvider::CAcrylicBlurProvider(CHyprOpenGLImpl& impl) : CDualKawaseBlurProvider(impl, makeUnique()) { + ; +} + +eBlurType CAcrylicBlurMaterial::type() const noexcept { + return eBlurType::BLUR_ACRYLIC; +} + +SBlurMaterialRequirements CAcrylicBlurMaterial::requirements() const noexcept { + return { + .finishFragment = SH_FRAG_ACRYLICFINISH, + .preparedInput = true, + .liveBlur = true, + }; +} + +float CAcrylicBlurMaterial::sampleRadius() const { + static auto PACRYLICREFRACTION = CConfigValue("decoration:blur:acrylic:refraction"); + return acrylicSampleRadius(*PACRYLICREFRACTION); +} + +void CAcrylicBlurMaterial::bindFinish(WP shader, const SBlurMaterialContext& context) const { + shader->setUniformInt(SHADER_ACRYLIC_ENABLED, 0); + + if (!context.blurContext.shape) + return; + + const auto extent = context.blurContext.shape->box; + if (extent.width <= 0 || extent.height <= 0) + return; + + static auto PACRYLICREFRACTION = CConfigValue("decoration:blur:acrylic:refraction"); + static auto PACRYLICBULB = CConfigValue("decoration:blur:acrylic:bulb"); + static auto PACRYLICCLARITY = CConfigValue("decoration:blur:acrylic:clarity"); + static auto PACRYLICABERRATION = CConfigValue("decoration:blur:acrylic:aberration"); + static auto PACRYLICTINT = CConfigValue("decoration:blur:acrylic:tint"); + + const auto TINT = CHyprColor(*PACRYLICTINT); + const auto TINT_ALPHA = std::clamp(sc(TINT.a), 0.F, 1.F); + const auto TINT_DEPTH = -std::log(std::max(1.F - TINT_ALPHA, 0.0001F)); + const auto LUMINANCE_SCALE = acrylicLuminanceScale(); + + shader->setUniformInt(SHADER_ACRYLIC_ENABLED, 1); + shader->setUniformFloat4(SHADER_ACRYLIC_EXTENT, sc(extent.x), sc(extent.y), sc(extent.width), sc(extent.height)); + shader->setUniformFloat(SHADER_ACRYLIC_RADIUS, std::max(context.blurContext.shape->radius, 0.F)); + shader->setUniformFloat(SHADER_ACRYLIC_ROUNDING_POWER, std::max(context.blurContext.shape->roundingPower, 1.F)); + shader->setUniformFloat(SHADER_ACRYLIC_REFRACTION, std::clamp(*PACRYLICREFRACTION, 0.F, MAX_ACRYLIC_REFRACTION)); + shader->setUniformFloat(SHADER_ACRYLIC_BULB, std::clamp(*PACRYLICBULB, MIN_ACRYLIC_BULB, MAX_ACRYLIC_BULB)); + shader->setUniformFloat(SHADER_ACRYLIC_CLARITY, std::clamp(*PACRYLICCLARITY, 0.F, 1.F)); + shader->setUniformFloat(SHADER_ACRYLIC_ABERRATION, std::clamp(*PACRYLICABERRATION, 0.F, 0.25F)); + shader->setUniformFloat4(SHADER_ACRYLIC_TINT, srgbToLinear(sc(TINT.r)) * LUMINANCE_SCALE, srgbToLinear(sc(TINT.g)) * LUMINANCE_SCALE, + srgbToLinear(sc(TINT.b)) * LUMINANCE_SCALE, TINT_DEPTH); + shader->setUniformFloat(SHADER_ACRYLIC_STRENGTH, std::clamp(context.strength, 0.F, 1.F)); + shader->setUniformInt(SHADER_ACRYLIC_TRANSFER_FUNCTION, sc(getDefaultImageDescription()->value().transferFunction)); + shader->setUniformFloat(SHADER_ACRYLIC_LUMINANCE_SCALE, LUMINANCE_SCALE); +} + +float Render::GL::acrylicDamageRadius(int64_t size, int64_t passes, float refraction) { + return dualKawaseDamageRadius(size, passes) + acrylicSampleRadius(refraction); +} diff --git a/src/render/gl/blur/Acrylic.hpp b/src/render/gl/blur/Acrylic.hpp new file mode 100644 index 000000000..6c472449a --- /dev/null +++ b/src/render/gl/blur/Acrylic.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "Kawase.hpp" + +namespace Render::GL { + class CAcrylicBlurMaterial final : public IGLBlurMaterial { + public: + eBlurType type() const noexcept override; + SBlurMaterialRequirements requirements() const noexcept override; + float sampleRadius() const override; + void bindFinish(WP shader, const SBlurMaterialContext& context) const override; + }; + + class CAcrylicBlurProvider final : public CDualKawaseBlurProvider { + public: + explicit CAcrylicBlurProvider(CHyprOpenGLImpl& impl); + }; + + float acrylicDamageRadius(int64_t size, int64_t passes, float refraction); +} diff --git a/src/render/gl/blur/Aurora.cpp b/src/render/gl/blur/Aurora.cpp new file mode 100644 index 000000000..199dcc1dc --- /dev/null +++ b/src/render/gl/blur/Aurora.cpp @@ -0,0 +1,102 @@ +#include "Aurora.hpp" + +#include "../../Renderer.hpp" +#include "../../Shader.hpp" +#include "../../ShaderLoader.hpp" +#include "../../../config/ConfigValue.hpp" +#include "../../../event/EventBus.hpp" +#include "../../../helpers/Color.hpp" +#include "../../../helpers/cm/ColorManagement.hpp" + +#include +#include +#include + +using namespace Render; +using namespace Render::GL; +using namespace NColorManagement; + +static constexpr float MAX_AURORA_SPEED = 10.F; +static constexpr double AURORA_BASE_SPEED = 0.22; +static constexpr double AURORA_PERIOD = 6.283185307179586; + +static float auroraSpeed() { + static auto PAURORASPEED = CConfigValue("decoration:blur:aurora:speed"); + return std::clamp(*PAURORASPEED, 0.F, MAX_AURORA_SPEED); +} + +static float srgbToLinear(float value) { + return value <= 0.04045F ? value / 12.92F : std::pow((value + 0.055F) / 1.055F, 2.4F); +} + +static float auroraLuminanceScale() { + const auto INTERMEDIATE = getDefaultImageDescription(); + const auto WORKBUFFER = g_pHyprRenderer->workBufferImageDescription(); + if (!WORKBUFFER) + return 1.F; + + const auto MINIMUM = INTERMEDIATE->value().getTFMinLuminance(); + const auto MAXIMUM = INTERMEDIATE->value().getTFMaxLuminance(); + const auto RANGE = std::max(MAXIMUM, sc(WORKBUFFER->value().luminances.max)) - MINIMUM; + return (MAXIMUM - MINIMUM) / std::max(RANGE, 0.001F); +} + +CAuroraBlurMaterial::CAuroraBlurMaterial() : CGlassBlurMaterial(eBlurType::BLUR_AURORA, SH_FRAG_AURORAFINISH), m_lastAnimationUpdate(Time::steadyNow()) { + m_configListener = Event::bus()->m_events.config.props_refreshed.listen([this](const bool) { updateAnimation(auroraSpeed()); }); +} + +CAuroraBlurProvider::CAuroraBlurProvider(CHyprOpenGLImpl& impl) : CGlassBlurProvider(impl, makeUnique()) { + ; +} + +bool CAuroraBlurMaterial::isAnimated() const noexcept { + static auto PBLURENABLED = CConfigValue("decoration:blur:enabled"); + static auto PGLASSREFRACTION = CConfigValue("decoration:blur:glass:refraction"); + static auto PGLASSROUGHNESS = CConfigValue("decoration:blur:glass:roughness"); + static auto PAURORAINTENSITY = CConfigValue("decoration:blur:aurora:intensity"); + static auto PAURORACOLOR1 = CConfigValue("decoration:blur:aurora:color1"); + static auto PAURORACOLOR2 = CConfigValue("decoration:blur:aurora:color2"); + + const auto SPEED = auroraSpeed(); + const auto COLOR1 = CHyprColor(*PAURORACOLOR1); + const auto COLOR2 = CHyprColor(*PAURORACOLOR2); + const bool HAS_COLOR = *PAURORAINTENSITY > 0.F && (COLOR1.a > 0.F || COLOR2.a > 0.F); + updateAnimation(SPEED); + return *PBLURENABLED && SPEED > 0.F && (HAS_COLOR || *PGLASSREFRACTION > 0.F || *PGLASSROUGHNESS > 0.F); +} + +void CAuroraBlurMaterial::bindFinish(WP shader, const SBlurMaterialContext& context) const { + static auto PAURORAINTENSITY = CConfigValue("decoration:blur:aurora:intensity"); + static auto PAURORACOLOR1 = CConfigValue("decoration:blur:aurora:color1"); + static auto PAURORACOLOR2 = CConfigValue("decoration:blur:aurora:color2"); + + CGlassBlurMaterial::bindFinish(shader, context); + updateAnimation(auroraSpeed()); + + const auto COLOR1 = CHyprColor(*PAURORACOLOR1); + const auto COLOR2 = CHyprColor(*PAURORACOLOR2); + const auto SCALE = auroraLuminanceScale(); + + const auto bindColor = [&](eShaderUniform uniform, const CHyprColor& color) { + const auto ALPHA = sc(color.a); + shader->setUniformFloat4(uniform, srgbToLinear(sc(color.r)) * SCALE * ALPHA, srgbToLinear(sc(color.g)) * SCALE * ALPHA, + srgbToLinear(sc(color.b)) * SCALE * ALPHA, ALPHA); + }; + + shader->setUniformFloat(SHADER_TIME, animationPhase()); + shader->setUniformFloat(SHADER_AURORA_INTENSITY, std::clamp(*PAURORAINTENSITY, 0.F, 1.F) * std::clamp(context.strength, 0.F, 1.F)); + bindColor(SHADER_AURORA_COLOR1, COLOR1); + bindColor(SHADER_AURORA_COLOR2, COLOR2); + shader->setUniformInt(SHADER_AURORA_TRANSFER_FUNCTION, sc(getDefaultImageDescription()->value().transferFunction)); +} + +void CAuroraBlurMaterial::updateAnimation(float speed) const { + const auto NOW = Time::steadyNow(); + m_animationTime += std::chrono::duration(NOW - m_lastAnimationUpdate).count() * m_previousSpeed; + m_lastAnimationUpdate = NOW; + m_previousSpeed = speed; +} + +float CAuroraBlurMaterial::animationPhase() const { + return sc(std::fmod(m_animationTime * AURORA_BASE_SPEED, AURORA_PERIOD)); +} diff --git a/src/render/gl/blur/Aurora.hpp b/src/render/gl/blur/Aurora.hpp new file mode 100644 index 000000000..511b09a5a --- /dev/null +++ b/src/render/gl/blur/Aurora.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include "Glass.hpp" + +#include "../../../helpers/signal/Signal.hpp" +#include "../../../helpers/time/Time.hpp" + +namespace Render::GL { + class CAuroraBlurMaterial final : public CGlassBlurMaterial { + public: + CAuroraBlurMaterial(); + + bool isAnimated() const noexcept override; + void bindFinish(WP shader, const SBlurMaterialContext& context) const override; + + private: + void updateAnimation(float speed) const; + float animationPhase() const; + + mutable Time::steady_tp m_lastAnimationUpdate; + mutable double m_animationTime = 0.0; + mutable float m_previousSpeed = 0.F; + CHyprSignalListener m_configListener; + }; + + class CAuroraBlurProvider final : public CGlassBlurProvider { + public: + explicit CAuroraBlurProvider(CHyprOpenGLImpl& impl); + }; +} diff --git a/src/render/gl/blur/Drops.cpp b/src/render/gl/blur/Drops.cpp new file mode 100644 index 000000000..d01d790a6 --- /dev/null +++ b/src/render/gl/blur/Drops.cpp @@ -0,0 +1,66 @@ +#include "Drops.hpp" + +#include "../../Renderer.hpp" +#include "../../Shader.hpp" +#include "../../ShaderLoader.hpp" +#include "../../../config/ConfigValue.hpp" +#include "../../../event/EventBus.hpp" + +#include +#include +#include + +using namespace Render; +using namespace Render::GL; + +static constexpr float MAX_DROPS_SPEED = 10.F; +static constexpr double DROPS_BASE_SPEED = 0.055; +static constexpr double DROPS_PATTERN_PERIOD = 256.0; + +static float dropsSpeed() { + static auto PDROPSSPEED = CConfigValue("decoration:blur:drops:speed"); + return std::clamp(*PDROPSSPEED, 0.F, MAX_DROPS_SPEED); +} + +CDropsBlurMaterial::CDropsBlurMaterial() : CGlassBlurMaterial(eBlurType::BLUR_DROPS, SH_FRAG_DROPSFINISH, true), m_lastAnimationUpdate(Time::steadyNow()) { + m_configListener = Event::bus()->m_events.config.props_refreshed.listen([this](const bool) { updateAnimation(dropsSpeed()); }); +} + +CDropsBlurProvider::CDropsBlurProvider(CHyprOpenGLImpl& impl) : CGlassBlurProvider(impl, makeUnique()) { + ; +} + +bool CDropsBlurMaterial::isAnimated() const noexcept { + static auto PBLURENABLED = CConfigValue("decoration:blur:enabled"); + static auto PGLASSREFRACTION = CConfigValue("decoration:blur:glass:refraction"); + static auto PGLASSROUGHNESS = CConfigValue("decoration:blur:glass:roughness"); + + const auto SPEED = dropsSpeed(); + updateAnimation(SPEED); + return *PBLURENABLED && SPEED > 0.F && (*PGLASSREFRACTION > 0.F || *PGLASSROUGHNESS > 0.F); +} + +void CDropsBlurMaterial::bindFinish(WP shader, const SBlurMaterialContext& context) const { + CGlassBlurMaterial::bindFinish(shader, context); + updateAnimation(dropsSpeed()); + shader->setUniformFloat(SHADER_TIME, animationPhase()); + + const CBox patternBox = context.blurContext.patternBox.value_or(CBox{}); + + shader->setUniformFloat2(SHADER_DROPS_POSITION, sc(patternBox.x), sc(patternBox.y)); +} + +void CDropsBlurMaterial::updateAnimation(float speed) const { + const auto NOW = Time::steadyNow(); + m_animationTime += std::chrono::duration(NOW - m_lastAnimationUpdate).count() * m_previousSpeed; + m_lastAnimationUpdate = NOW; + m_previousSpeed = speed; +} + +float CDropsBlurMaterial::animationPhase() const { + if (m_previousSpeed <= 0.F) + return 0.F; + + const auto PHASE = sc(std::fmod(m_animationTime * DROPS_BASE_SPEED, DROPS_PATTERN_PERIOD)); + return std::max(PHASE, 0.00002F); +} diff --git a/src/render/gl/blur/Drops.hpp b/src/render/gl/blur/Drops.hpp new file mode 100644 index 000000000..26f88d88d --- /dev/null +++ b/src/render/gl/blur/Drops.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include "Glass.hpp" + +#include "../../../helpers/signal/Signal.hpp" +#include "../../../helpers/time/Time.hpp" + +namespace Render::GL { + class CDropsBlurMaterial final : public CGlassBlurMaterial { + public: + CDropsBlurMaterial(); + + bool isAnimated() const noexcept override; + + void bindFinish(WP shader, const SBlurMaterialContext& context) const override; + + private: + void updateAnimation(float speed) const; + float animationPhase() const; + + mutable Time::steady_tp m_lastAnimationUpdate; + mutable double m_animationTime = 0.0; + mutable float m_previousSpeed = 0.F; + CHyprSignalListener m_configListener; + }; + + class CDropsBlurProvider final : public CGlassBlurProvider { + public: + explicit CDropsBlurProvider(CHyprOpenGLImpl& impl); + }; +} diff --git a/src/render/gl/blur/Factory.cpp b/src/render/gl/blur/Factory.cpp new file mode 100644 index 000000000..5eaba9244 --- /dev/null +++ b/src/render/gl/blur/Factory.cpp @@ -0,0 +1,37 @@ +#include "Factory.hpp" + +#include "Acrylic.hpp" +#include "Aurora.hpp" +#include "Drops.hpp" +#include "FluidJar.hpp" +#include "Glass.hpp" +#include "Haze.hpp" +#include "HeatShimmer.hpp" +#include "Kawase.hpp" +#include "Prism.hpp" +#include "Ripple.hpp" +#include "Water.hpp" +#include "../../ShaderLoader.hpp" +#include "../../../debug/log/Logger.hpp" + +using namespace Render; +using namespace Render::GL; + +UP Render::GL::createBlurProvider(eBlurType type, CHyprOpenGLImpl& impl) { + switch (type) { + case eBlurType::BLUR_DUAL_KAWASE: return makeUnique(impl); + case eBlurType::BLUR_FROST: return makeUnique(impl, type, SH_FRAG_FROSTFINISH); + case eBlurType::BLUR_RIPPLE: return makeUnique(impl); + case eBlurType::BLUR_DROPS: return makeUnique(impl); + case eBlurType::BLUR_WATER: return makeUnique(impl); + case eBlurType::BLUR_FLUID_JAR: return makeUnique(impl); + case eBlurType::BLUR_PRISM: return makeUnique(impl); + case eBlurType::BLUR_HEAT_SHIMMER: return makeUnique(impl); + case eBlurType::BLUR_ACRYLIC: return makeUnique(impl); + case eBlurType::BLUR_AURORA: return makeUnique(impl); + case eBlurType::BLUR_HAZE: return makeUnique(impl); + } + + Log::logger->log(Log::ERR, "Unknown blur provider {}, falling back to dual Kawase", sc(type)); + return makeUnique(impl); +} diff --git a/src/render/gl/blur/Factory.hpp b/src/render/gl/blur/Factory.hpp new file mode 100644 index 000000000..7a98f418f --- /dev/null +++ b/src/render/gl/blur/Factory.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "../../../helpers/memory/Memory.hpp" +#include "../../blur/Provider.hpp" + +namespace Render::GL { + class CHyprOpenGLImpl; + class IGLBlurProvider; + + UP createBlurProvider(eBlurType type, CHyprOpenGLImpl& impl); +} diff --git a/src/render/gl/blur/FluidJar.cpp b/src/render/gl/blur/FluidJar.cpp new file mode 100644 index 000000000..47f29f4d8 --- /dev/null +++ b/src/render/gl/blur/FluidJar.cpp @@ -0,0 +1,673 @@ +#include "FluidJar.hpp" + +#include "Kawase.hpp" + +#include "../GLFramebuffer.hpp" +#include "../../OpenGL.hpp" +#include "../../Renderer.hpp" +#include "../../Shader.hpp" +#include "../../ShaderLoader.hpp" +#include "../../../config/ConfigValue.hpp" +#include "../../../desktop/view/window/Window.hpp" +#include "../../../desktop/view/window/WindowPresentation.hpp" +#include "../../../desktop/Workspace.hpp" +#include "../../../event/EventBus.hpp" +#include "../../../helpers/Color.hpp" +#include "../../../helpers/cm/ColorManagement.hpp" + +#include +#include +#include +#include +#include + +using namespace Render; +using namespace Render::GL; +using namespace NColorManagement; + +static constexpr float BASE_SIMULATION_SCALE = 0.25F; +static constexpr int BASE_SIMULATION_SIDE = 256; +static constexpr float MIN_PRECISION = 0.5F; +static constexpr float MAX_PRECISION = 8.F; +static constexpr int MAX_PARTICLES = 131072; +static constexpr float FIXED_TIMESTEP = 1.F / 60.F; +static constexpr float SOLVER_TIMESTEP = 3.F; +static constexpr float MAX_WALL_SPEED = 1.25F; +static constexpr float MAX_MOTION_INTERVAL = 0.1F; +static constexpr int MAX_SUBSTEPS = 8; +static constexpr int INITIAL_GRAPH_STEPS = 8; +static constexpr int INITIAL_TRACK_STEPS = 4; +static constexpr int INITIAL_VISUAL_STEPS = 4; +static constexpr float MAX_REFRACTION = 8.F; +static constexpr float MAX_TURBULENCE = 5.F; +static constexpr float MAX_DISTORTION = 10.F; +static constexpr double ANIMATION_PERIOD = 200.0 * std::numbers::pi; + +static void bindNearestTexture(SP buffer, GLenum unit) { + glActiveTexture(unit); + const auto texture = buffer->getTexture(); + texture->bind(); + texture->setTexParameter(GL_TEXTURE_MIN_FILTER, GL_NEAREST); + texture->setTexParameter(GL_TEXTURE_MAG_FILTER, GL_NEAREST); +} + +static bool geometryChanged(const CBox& lhs, const CBox& rhs) { + return lhs.x != rhs.x || lhs.y != rhs.y || lhs.width != rhs.width || lhs.height != rhs.height; +} + +static bool geometryDiscontinuous(const CBox& oldExtent, const CBox& newExtent, float elapsed) { + if (elapsed <= 0.F || elapsed > MAX_MOTION_INTERVAL) + return true; + + const auto oldRight = oldExtent.x + oldExtent.width; + const auto newRight = newExtent.x + newExtent.width; + const auto oldBottom = oldExtent.y + oldExtent.height; + const auto newBottom = newExtent.y + newExtent.height; + const auto horizontalDelta = std::max(std::abs(newExtent.x - oldExtent.x), std::abs(newRight - oldRight)); + const auto verticalDelta = std::max(std::abs(newExtent.y - oldExtent.y), std::abs(newBottom - oldBottom)); + const auto width = std::max({oldExtent.width, newExtent.width, 1.0}); + const auto height = std::max({oldExtent.height, newExtent.height, 1.0}); + return horizontalDelta > width * 0.75 || verticalDelta > height * 0.75; +} + +static CBox renderedWindowBox(PHLWINDOW window) { + if (!window) + return {}; + + auto position = window->position(Desktop::View::IGeometric::GEOMETRIC_CURRENT) + window->presentation().floatingOffset(); + if (!(window->m_state & Desktop::View::WINDOW_STATE_PINNED) && window->m_workspace) + position += window->m_workspace->m_renderOffset->value(); + + const auto size = window->size(Desktop::View::IGeometric::GEOMETRIC_CURRENT); + return {position.x, position.y, size.x, size.y}; +} + +CFluidJarBlurMaterial::CFluidJarBlurMaterial(CHyprOpenGLImpl& impl) : m_impl(impl), m_supported(impl.m_exts.EXT_color_buffer_half_float) { + if (!m_supported) + Log::logger->log(Log::WARN, "fluid_jar blur requires GL_EXT_color_buffer_half_float; falling back to Kawase blur"); + + m_listeners.renderPre = Event::bus()->m_events.render.pre.listen([this](PHLMONITOR) { ++m_frame; }); + m_listeners.windowDestroy = + Event::bus()->m_events.window.destroy.listen([this](PHLWINDOWREF window) { std::erase_if(m_states, [&](const auto& state) { return state.window == window; }); }); +} + +CFluidJarBlurProvider::CFluidJarBlurProvider(CHyprOpenGLImpl& impl) : CDualKawaseBlurProvider(impl, makeUnique(impl)) { + ; +} + +eBlurType CFluidJarBlurMaterial::type() const noexcept { + return eBlurType::BLUR_FLUID_JAR; +} + +SBlurMaterialRequirements CFluidJarBlurMaterial::requirements() const noexcept { + return { + .finishFragment = SH_FRAG_FLUIDJARFINISH, + .preparedInput = m_supported, + .liveBlur = m_supported, + }; +} + +int64_t CFluidJarBlurMaterial::blurSizeForDamage(int64_t size) const { + return m_supported ? size : std::clamp(size, 1, 40); +} + +void CFluidJarBlurMaterial::prepare(const SBlurMaterialContext& context) { + if (!m_supported || context.blurContext.owner.expired()) + return; + + pruneStates(); + + const auto state = stateForContext(context.blurContext, true); + const auto renderExtent = transformedPatternBox(context.blurContext); + const auto window = context.blurContext.owner.lock(); + const auto physicsExtent = renderedWindowBox(window); + if (!state || physicsExtent.width <= 0 || physicsExtent.height <= 0 || renderExtent.width <= 0 || renderExtent.height <= 0 || !g_pHyprRenderer->m_renderData.pMonitor) + return; + + updateState(*state, physicsExtent); +} + +void CFluidJarBlurMaterial::bindFinish(WP shader, const SBlurMaterialContext& context) const { + const auto state = stateForContext(context.blurContext); + if (!m_supported || !state || !state->visual[state->currentVisual]) { + shader->setUniformInt(SHADER_FLUIDJAR_ENABLED, 0); + return; + } + + const auto extent = transformedPatternBox(context.blurContext); + if (extent.width <= 0 || extent.height <= 0) { + shader->setUniformInt(SHADER_FLUIDJAR_ENABLED, 0); + return; + } + + const auto monitor = g_pHyprRenderer->m_renderData.pMonitor; + if (!monitor) { + shader->setUniformInt(SHADER_FLUIDJAR_ENABLED, 0); + return; + } + + const auto logicalExtent = context.blurContext.patternBox.value_or(CBox{0, 0, monitor->m_transformedSize.x, monitor->m_transformedSize.y}); + const auto outputTransform = fluidJarOutputTransform(HYPRUTILS_TRANSFORM_NORMAL); + + glActiveTexture(GL_TEXTURE2); + const auto texture = state->visual[state->currentVisual]->getTexture(); + texture->bind(); + texture->setTexParameter(GL_TEXTURE_MIN_FILTER, GL_LINEAR); + texture->setTexParameter(GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glActiveTexture(GL_TEXTURE0); + + static auto PFLUIDCOLOR = CConfigValue("decoration:blur:fluid_jar:color"); + static auto PFLUIDTURBULENCE = CConfigValue("decoration:blur:fluid_jar:turbulence"); + static auto PFLUIDDISTORTION = CConfigValue("decoration:blur:fluid_jar:distortion"); + const auto color = CHyprColor(*PFLUIDCOLOR); + const auto animationPhase = sc(std::fmod(state->animationTime, ANIMATION_PERIOD)); + + shader->setUniformInt(SHADER_FLUIDJAR_ENABLED, 1); + shader->setUniformInt(SHADER_FLUIDJAR_VISUAL_TEX, 2); + shader->setUniformFloat4(SHADER_FLUIDJAR_EXTENT, sc(extent.x), sc(extent.y), sc(extent.width), sc(extent.height)); + shader->setUniformFloat4(SHADER_FLUIDJAR_OUTPUT_TRANSFORM, sc(outputTransform.xAxis.x), sc(outputTransform.yAxis.x), sc(outputTransform.xAxis.y), + sc(outputTransform.yAxis.y)); + shader->setUniformFloat2(SHADER_FLUIDJAR_OUTPUT_OFFSET, sc(outputTransform.offset.x), sc(outputTransform.offset.y)); + shader->setUniformFloat2(SHADER_FLUIDJAR_LOGICAL_SIZE, sc(logicalExtent.width), sc(logicalExtent.height)); + shader->setUniformFloat4(SHADER_FLUIDJAR_COLOR, color.r, color.g, color.b, color.a); + shader->setUniformFloat(SHADER_FLUIDJAR_REFRACTION, MAX_REFRACTION); + shader->setUniformInt(SHADER_FLUIDJAR_TRANSFER_FUNCTION, sc(getDefaultImageDescription()->value().transferFunction)); + shader->setUniformFloat(SHADER_FLUIDJAR_STRENGTH, std::clamp(context.strength, 0.F, 1.F)); + shader->setUniformFloat(SHADER_FLUIDJAR_TURBULENCE, std::clamp(*PFLUIDTURBULENCE, 0.F, MAX_TURBULENCE)); + shader->setUniformFloat(SHADER_FLUIDJAR_DISTORTION, std::clamp(*PFLUIDDISTORTION, 0.F, MAX_DISTORTION)); + shader->setUniformFloat(SHADER_TIME, animationPhase); +} + +float CFluidJarBlurMaterial::sampleRadius() const { + if (!m_supported) + return 0.F; + + static auto PDISTORTION = CConfigValue("decoration:blur:fluid_jar:distortion"); + return std::ceil(MAX_REFRACTION * std::clamp(*PDISTORTION, 0.F, MAX_DISTORTION)); +} + +CFluidJarBlurMaterial::SState* CFluidJarBlurMaterial::stateForContext(const SBlurContext& context, bool create) { + if (context.owner.expired()) + return nullptr; + + const auto state = std::ranges::find_if(m_states, [&](const auto& candidate) { return candidate.window == context.owner; }); + if (state != m_states.end()) + return &*state; + + if (!create) + return nullptr; + + return &m_states.emplace_back(SState{.window = context.owner}); +} + +const CFluidJarBlurMaterial::SState* CFluidJarBlurMaterial::stateForContext(const SBlurContext& context) const { + if (context.owner.expired()) + return nullptr; + + const auto state = std::ranges::find_if(m_states, [&](const auto& candidate) { return candidate.window == context.owner; }); + return state != m_states.end() ? &*state : nullptr; +} + +void CFluidJarBlurMaterial::updateState(SState& state, const CBox& extent) { + if (state.lastFrame == m_frame) + return; + + static auto PFLUIDSPEED = CConfigValue("decoration:blur:fluid_jar:speed"); + static auto PFLUIDFILL = CConfigValue("decoration:blur:fluid_jar:fill_amount"); + static auto PPRECISION = CConfigValue("decoration:blur:fluid_jar:precision"); + + const auto precision = std::clamp(*PPRECISION, MIN_PRECISION, MAX_PRECISION); + const auto simulationSize = fluidJarSimulationSize(extent.size(), precision); + const auto fillAmount = std::clamp(*PFLUIDFILL, 0.F, 1.F); + if (simulationSize.x <= 0 || simulationSize.y <= 0) + return; + + const auto now = Time::steadyNow(); + const auto elapsed = state.lastUpdate == Time::steady_tp{} ? FIXED_TIMESTEP : std::clamp(std::chrono::duration(now - state.lastUpdate).count(), 0.F, 0.5F); + const auto speed = std::clamp(*PFLUIDSPEED, 0.F, 10.F); + + if (!state.particles[0] || state.fillAmount != fillAmount || state.precision != precision) { + initializeState(state, simulationSize, fillAmount, precision); + state.extent = extent; + state.hasExtent = true; + } else if (!state.hasExtent) { + state.extent = extent; + state.hasExtent = true; + } else if (state.simulationSize != simulationSize || geometryChanged(state.extent, extent)) { + const auto discontinuous = geometryDiscontinuous(state.extent, extent, elapsed); + const auto transform = fluidJarGeometryTransform(state.extent, extent, state.simulationSize, simulationSize, !discontinuous); + state.wallVelocities = discontinuous ? std::array{} : fluidJarWallVelocities(state.extent, extent, simulationSize, elapsed, speed); + transformState(state, simulationSize, transform, state.wallVelocities); + state.extent = extent; + } else + state.wallVelocities = {}; + + if (speed > 0.F && state.particleCount > 0) { + state.animationTime += std::min(elapsed, 0.05F) * speed; + state.accumulator += std::min(elapsed, 0.05F) * speed; + + int substeps = 0; + while (state.accumulator >= FIXED_TIMESTEP && substeps < MAX_SUBSTEPS) { + drawParticleStep(state, SOLVER_TIMESTEP); + drawGraphStep(state); + drawTrackingStep(state); + state.accumulator -= FIXED_TIMESTEP; + ++state.simulationFrame; + ++substeps; + } + + if (substeps > 0) + drawVisualStep(state, substeps); + + if (substeps == MAX_SUBSTEPS) + state.accumulator = std::min(state.accumulator, sc(FIXED_TIMESTEP)); + + scheduleNextFrame(state); + } + + state.lastUpdate = now; + state.lastFrame = m_frame; +} + +void CFluidJarBlurMaterial::initializeState(SState& state, const Vector2D& simulationSize, float fillAmount, float precision) { + state.simulationSize = simulationSize; + state.gridSize = fluidJarGridSize(simulationSize); + state.particleTextureSize = {state.gridSize.x * 4.0, state.gridSize.y}; + state.graphTextureSize = {state.gridSize.x * 8.0, state.gridSize.y}; + + allocateBuffers(state.particles, state.particleTextureSize, "Fluid jar particles"); + allocateBuffers(state.graph, state.graphTextureSize, "Fluid jar graph", DRM_FORMAT_ABGR16161616); + allocateBuffers(state.tracking, simulationSize, "Fluid jar tracking", DRM_FORMAT_ABGR16161616); + allocateBuffers(state.visual, simulationSize, "Fluid jar visual"); + + state.fillAmount = fillAmount; + state.precision = precision; + state.particleCount = fluidJarInitialParticleCount(simulationSize, fillAmount); + state.currentParticles = 0; + state.currentGraph = 0; + state.currentTracking = 0; + state.currentVisual = 0; + state.simulationFrame = 0; + state.accumulator = 0.0; + state.animationTime = 0.0; + state.lastUpdate = {}; + state.wallVelocities = {}; + + for (const auto& buffer : state.particles) + drawInitialize(state, buffer); + + clearIntegerBuffers(state.graph); + for (int i = 0; i < INITIAL_GRAPH_STEPS; ++i) { + drawGraphStep(state); + ++state.simulationFrame; + } + + clearIntegerBuffers(state.tracking); + clearBuffers(state.visual, {0.F, 0.F, 0.F, 0.F}); + if (state.particleCount > 0) { + for (int i = 0; i < INITIAL_TRACK_STEPS; ++i) { + drawTrackingStep(state); + ++state.simulationFrame; + } + for (int i = 0; i < INITIAL_VISUAL_STEPS; ++i) + drawVisualStep(state); + } +} + +void CFluidJarBlurMaterial::transformState(SState& state, const Vector2D& simulationSize, const SFluidJarGeometryTransform& transform, const std::array& wallVelocities) { + const auto oldSize = state.simulationSize; + const auto oldGridSize = state.gridSize; + const auto oldParticleCount = state.particleCount; + const auto oldParticles = state.particles[state.currentParticles]; + const auto oldTracking = state.tracking[state.currentTracking]; + const auto oldVisual = state.visual[state.currentVisual]; + const bool resized = oldSize != simulationSize; + + state.simulationSize = simulationSize; + state.particleCount = fluidJarResizedParticleCount(oldParticleCount, simulationSize); + + const auto particleTarget = state.particles[1 - state.currentParticles]; + drawResample(state, oldParticles, particleTarget, oldGridSize, oldParticleCount, transform, wallVelocities); + state.currentParticles = 1 - state.currentParticles; + drawGraphStep(state); + + if (resized) { + std::array, 2> tracking; + std::array, 2> visual; + allocateBuffers(tracking, simulationSize, "Fluid jar resized tracking", DRM_FORMAT_ABGR16161616); + allocateBuffers(visual, simulationSize, "Fluid jar resized visual"); + clearIntegerBuffers(tracking); + clearBuffers(visual, {0.F, 0.F, 0.F, 0.F}); + drawTrackingResample(oldTracking, tracking[0], oldSize, transform); + drawHistoryResample(oldVisual, visual[0], oldSize, transform, {0.F, 0.F, 0.F, 0.F}, true); + state.tracking = std::move(tracking); + state.visual = std::move(visual); + state.currentTracking = 0; + state.currentVisual = 0; + } else { + drawTrackingResample(oldTracking, state.tracking[1 - state.currentTracking], oldSize, transform); + drawHistoryResample(oldVisual, state.visual[1 - state.currentVisual], oldSize, transform, {0.F, 0.F, 0.F, 0.F}, true); + state.currentTracking = 1 - state.currentTracking; + state.currentVisual = 1 - state.currentVisual; + } + + if (state.particleCount > 0) { + drawTrackingStep(state); + const bool velocityScaleChanged = std::abs(transform.velocityScale.x - 1.0) > 0.001 || std::abs(transform.velocityScale.y - 1.0) > 0.001; + drawVisualStep(state, resized || velocityScaleChanged ? INITIAL_VISUAL_STEPS : 1); + } +} + +void CFluidJarBlurMaterial::allocateBuffers(std::array, 2>& buffers, const Vector2D& size, const std::string& name, DRMFormat format) const { + for (auto& buffer : buffers) { + if (!buffer) + buffer = dynamicPointerCast(g_pHyprRenderer->createFB(name)); + buffer->alloc(sc(size.x), sc(size.y), format); + } +} + +void CFluidJarBlurMaterial::clearIntegerBuffers(const std::array, 2>& buffers) const { + constexpr std::array CLEAR_VALUE = {}; + for (const auto& buffer : buffers) { + buffer->bind(); + g_pHyprRenderer->disableScissor(); + g_pHyprRenderer->blend(false); + glClearBufferuiv(GL_COLOR, 0, CLEAR_VALUE.data()); + } +} + +void CFluidJarBlurMaterial::clearBuffers(const std::array, 2>& buffers, const std::array& color) const { + for (const auto& buffer : buffers) { + buffer->bind(); + g_pHyprRenderer->setViewport(0, 0, sc(buffer->m_size.x), sc(buffer->m_size.y)); + g_pHyprRenderer->disableScissor(); + glClearColor(color[0], color[1], color[2], color[3]); + glClear(GL_COLOR_BUFFER_BIT); + } +} + +void CFluidJarBlurMaterial::drawInitialize(const SState& state, SP target) const { + const auto shader = m_impl.useShader(m_impl.getShaderVariant(SH_FRAG_FLUIDJARINIT)); + preparePass(target, state.particleTextureSize, shader); + shader->setUniformFloat2(SHADER_FLUIDJAR_RESOLUTION, state.simulationSize.x, state.simulationSize.y); + shader->setUniformFloat2(SHADER_FLUIDJAR_GRID_SIZE, state.gridSize.x, state.gridSize.y); + shader->setUniformInt(SHADER_FLUIDJAR_PARTICLE_COUNT, state.particleCount); + glBindVertexArray(shader->getUniformLocation(SHADER_SHADER_VAO)); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + glBindVertexArray(0); +} + +void CFluidJarBlurMaterial::drawResample(const SState& state, SP source, SP target, const Vector2D& oldGridSize, int oldParticleCount, + const SFluidJarGeometryTransform& transform, const std::array& wallVelocities) const { + static auto PFLUIDMASS = CConfigValue("decoration:blur:fluid_jar:mass"); + + bindNearestTexture(source, GL_TEXTURE0); + const auto shader = m_impl.useShader(m_impl.getShaderVariant(SH_FRAG_FLUIDJARRESAMPLE)); + preparePass(target, state.particleTextureSize, shader); + shader->setUniformInt(SHADER_FLUIDJAR_PARTICLE_TEX, 0); + shader->setUniformFloat2(SHADER_FLUIDJAR_RESOLUTION, state.simulationSize.x, state.simulationSize.y); + shader->setUniformFloat2(SHADER_FLUIDJAR_GRID_SIZE, state.gridSize.x, state.gridSize.y); + shader->setUniformInt(SHADER_FLUIDJAR_PARTICLE_COUNT, state.particleCount); + shader->setUniformFloat2(SHADER_FLUIDJAR_OLD_GRID_SIZE, oldGridSize.x, oldGridSize.y); + shader->setUniformInt(SHADER_FLUIDJAR_OLD_PARTICLE_COUNT, oldParticleCount); + shader->setUniformFloat4(SHADER_FLUIDJAR_TRANSFORM, transform.positionScale.x, transform.positionScale.y, transform.positionOffset.x, transform.positionOffset.y); + shader->setUniformFloat2(SHADER_FLUIDJAR_VELOCITY_SCALE, transform.velocityScale.x, transform.velocityScale.y); + shader->setUniformFloat4(SHADER_FLUIDJAR_WALL_VELOCITIES, wallVelocities[0], wallVelocities[1], wallVelocities[2], wallVelocities[3]); + shader->setUniformFloat(SHADER_FLUIDJAR_MASS, std::clamp(*PFLUIDMASS, 0.1F, 10.F)); + glBindVertexArray(shader->getUniformLocation(SHADER_SHADER_VAO)); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + glBindVertexArray(0); +} + +void CFluidJarBlurMaterial::drawHistoryResample(SP source, SP target, const Vector2D& oldSize, const SFluidJarGeometryTransform& transform, + const std::array& fallback, bool linear) const { + glActiveTexture(GL_TEXTURE0); + const auto texture = source->getTexture(); + texture->bind(); + texture->setTexParameter(GL_TEXTURE_MIN_FILTER, linear ? GL_LINEAR : GL_NEAREST); + texture->setTexParameter(GL_TEXTURE_MAG_FILTER, linear ? GL_LINEAR : GL_NEAREST); + + const Vector2D inverseScale = {1.0 / transform.positionScale.x, 1.0 / transform.positionScale.y}; + const Vector2D inverseOffset = {-transform.positionOffset.x * inverseScale.x, -transform.positionOffset.y * inverseScale.y}; + const auto shader = m_impl.useShader(m_impl.getShaderVariant(SH_FRAG_FLUIDJARHISTORYRESAMPLE)); + preparePass(target, target->m_size, shader); + shader->setUniformInt(SHADER_FLUIDJAR_HISTORY_TEX, 0); + shader->setUniformFloat2(SHADER_FLUIDJAR_OLD_RESOLUTION, oldSize.x, oldSize.y); + shader->setUniformFloat4(SHADER_FLUIDJAR_HISTORY_TRANSFORM, inverseScale.x, inverseScale.y, inverseOffset.x, inverseOffset.y); + shader->setUniformFloat4(SHADER_FLUIDJAR_HISTORY_FALLBACK, fallback[0], fallback[1], fallback[2], fallback[3]); + glBindVertexArray(shader->getUniformLocation(SHADER_SHADER_VAO)); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + glBindVertexArray(0); +} + +void CFluidJarBlurMaterial::drawParticleStep(SState& state, float dt) const { + static auto PFLUIDMASS = CConfigValue("decoration:blur:fluid_jar:mass"); + + const auto source = state.particles[state.currentParticles]; + const auto target = state.particles[1 - state.currentParticles]; + bindNearestTexture(source, GL_TEXTURE0); + bindNearestTexture(state.graph[state.currentGraph], GL_TEXTURE1); + const auto shader = m_impl.useShader(m_impl.getShaderVariant(SH_FRAG_FLUIDJARSTEP)); + preparePass(target, state.particleTextureSize, shader); + shader->setUniformInt(SHADER_FLUIDJAR_PARTICLE_TEX, 0); + shader->setUniformInt(SHADER_FLUIDJAR_GRAPH_TEX, 1); + shader->setUniformFloat2(SHADER_FLUIDJAR_RESOLUTION, state.simulationSize.x, state.simulationSize.y); + shader->setUniformFloat2(SHADER_FLUIDJAR_GRID_SIZE, state.gridSize.x, state.gridSize.y); + shader->setUniformInt(SHADER_FLUIDJAR_PARTICLE_COUNT, state.particleCount); + shader->setUniformFloat(SHADER_FLUIDJAR_DT, dt); + shader->setUniformFloat(SHADER_FLUIDJAR_MASS, std::clamp(*PFLUIDMASS, 0.1F, 10.F)); + shader->setUniformFloat4(SHADER_FLUIDJAR_WALL_VELOCITIES, state.wallVelocities[0], state.wallVelocities[1], state.wallVelocities[2], state.wallVelocities[3]); + glBindVertexArray(shader->getUniformLocation(SHADER_SHADER_VAO)); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + glBindVertexArray(0); + state.currentParticles = 1 - state.currentParticles; +} + +void CFluidJarBlurMaterial::drawGraphStep(SState& state) const { + const auto target = state.graph[1 - state.currentGraph]; + bindNearestTexture(state.particles[state.currentParticles], GL_TEXTURE0); + bindNearestTexture(state.graph[state.currentGraph], GL_TEXTURE1); + const auto shader = m_impl.useShader(m_impl.getShaderVariant(SH_FRAG_FLUIDJARGRAPH)); + preparePass(target, state.graphTextureSize, shader); + shader->setUniformInt(SHADER_FLUIDJAR_PARTICLE_TEX, 0); + shader->setUniformInt(SHADER_FLUIDJAR_GRAPH_TEX, 1); + shader->setUniformFloat2(SHADER_FLUIDJAR_RESOLUTION, state.simulationSize.x, state.simulationSize.y); + shader->setUniformFloat2(SHADER_FLUIDJAR_GRID_SIZE, state.gridSize.x, state.gridSize.y); + shader->setUniformInt(SHADER_FLUIDJAR_PARTICLE_COUNT, state.particleCount); + shader->setUniformInt(SHADER_FLUIDJAR_FRAME, sc(state.simulationFrame)); + glBindVertexArray(shader->getUniformLocation(SHADER_SHADER_VAO)); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + glBindVertexArray(0); + state.currentGraph = 1 - state.currentGraph; +} + +void CFluidJarBlurMaterial::drawTrackingStep(SState& state) const { + const auto target = state.tracking[1 - state.currentTracking]; + bindNearestTexture(state.particles[state.currentParticles], GL_TEXTURE0); + bindNearestTexture(state.graph[state.currentGraph], GL_TEXTURE1); + bindNearestTexture(state.tracking[state.currentTracking], GL_TEXTURE2); + const auto shader = m_impl.useShader(m_impl.getShaderVariant(SH_FRAG_FLUIDJARTRACK)); + preparePass(target, state.simulationSize, shader); + shader->setUniformInt(SHADER_FLUIDJAR_PARTICLE_TEX, 0); + shader->setUniformInt(SHADER_FLUIDJAR_GRAPH_TEX, 1); + shader->setUniformInt(SHADER_FLUIDJAR_TRACKING_TEX, 2); + shader->setUniformFloat2(SHADER_FLUIDJAR_RESOLUTION, state.simulationSize.x, state.simulationSize.y); + shader->setUniformFloat2(SHADER_FLUIDJAR_GRID_SIZE, state.gridSize.x, state.gridSize.y); + shader->setUniformInt(SHADER_FLUIDJAR_PARTICLE_COUNT, state.particleCount); + shader->setUniformInt(SHADER_FLUIDJAR_FRAME, sc(state.simulationFrame)); + glBindVertexArray(shader->getUniformLocation(SHADER_SHADER_VAO)); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + glBindVertexArray(0); + state.currentTracking = 1 - state.currentTracking; +} + +void CFluidJarBlurMaterial::drawTrackingResample(SP source, SP target, const Vector2D& oldSize, const SFluidJarGeometryTransform& transform) const { + bindNearestTexture(source, GL_TEXTURE0); + + const Vector2D inverseScale = {1.0 / transform.positionScale.x, 1.0 / transform.positionScale.y}; + const Vector2D inverseOffset = {-transform.positionOffset.x * inverseScale.x, -transform.positionOffset.y * inverseScale.y}; + const auto shader = m_impl.useShader(m_impl.getShaderVariant(SH_FRAG_FLUIDJARTRACKINGRESAMPLE)); + preparePass(target, target->m_size, shader); + shader->setUniformInt(SHADER_FLUIDJAR_HISTORY_TEX, 0); + shader->setUniformFloat2(SHADER_FLUIDJAR_OLD_RESOLUTION, oldSize.x, oldSize.y); + shader->setUniformFloat4(SHADER_FLUIDJAR_HISTORY_TRANSFORM, inverseScale.x, inverseScale.y, inverseOffset.x, inverseOffset.y); + glBindVertexArray(shader->getUniformLocation(SHADER_SHADER_VAO)); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + glBindVertexArray(0); +} + +void CFluidJarBlurMaterial::drawVisualStep(SState& state, int steps) const { + const auto target = state.visual[1 - state.currentVisual]; + bindNearestTexture(state.particles[state.currentParticles], GL_TEXTURE0); + bindNearestTexture(state.graph[state.currentGraph], GL_TEXTURE1); + bindNearestTexture(state.tracking[state.currentTracking], GL_TEXTURE2); + bindNearestTexture(state.visual[state.currentVisual], GL_TEXTURE3); + const auto shader = m_impl.useShader(m_impl.getShaderVariant(SH_FRAG_FLUIDJARVISUAL)); + preparePass(target, state.simulationSize, shader); + shader->setUniformInt(SHADER_FLUIDJAR_PARTICLE_TEX, 0); + shader->setUniformInt(SHADER_FLUIDJAR_GRAPH_TEX, 1); + shader->setUniformInt(SHADER_FLUIDJAR_TRACKING_TEX, 2); + shader->setUniformInt(SHADER_FLUIDJAR_VISUAL_TEX, 3); + shader->setUniformFloat2(SHADER_FLUIDJAR_RESOLUTION, state.simulationSize.x, state.simulationSize.y); + shader->setUniformFloat2(SHADER_FLUIDJAR_GRID_SIZE, state.gridSize.x, state.gridSize.y); + shader->setUniformInt(SHADER_FLUIDJAR_PARTICLE_COUNT, state.particleCount); + shader->setUniformFloat(SHADER_FLUIDJAR_VISUAL_RESPONSE, sc(std::max(steps, 1))); + glBindVertexArray(shader->getUniformLocation(SHADER_SHADER_VAO)); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + glBindVertexArray(0); + state.currentVisual = 1 - state.currentVisual; + glActiveTexture(GL_TEXTURE0); +} + +void CFluidJarBlurMaterial::preparePass(SP target, const Vector2D& size, WP shader) const { + target->bind(); + g_pHyprRenderer->setViewport(0, 0, sc(size.x), sc(size.y)); + g_pHyprRenderer->disableScissor(); + g_pHyprRenderer->blend(false); + const auto monitor = g_pHyprRenderer->m_renderData.pMonitor; + const auto matrix = g_pHyprRenderer->projectBoxToTarget({0, 0, monitor->m_transformedSize.x, monitor->m_transformedSize.y}); + shader->setUniformMatrix3fv(SHADER_PROJ, 1, GL_TRUE, matrix.getMatrix()); +} + +CBox CFluidJarBlurMaterial::transformedPatternBox(const SBlurContext& context) const { + const auto monitor = g_pHyprRenderer->m_renderData.pMonitor; + if (!monitor) + return {}; + + return context.patternBox.value_or(CBox{0, 0, monitor->m_transformedSize.x, monitor->m_transformedSize.y}); +} + +void CFluidJarBlurMaterial::scheduleNextFrame(const SState& state) const { + const auto window = state.window.lock(); + if (!window) + return; + g_pHyprRenderer->damageWindow(window); +} + +void CFluidJarBlurMaterial::pruneStates() { + std::erase_if(m_states, [](const auto& state) { return state.window.expired() || !state.window->shouldBlur(); }); +} + +Vector2D Render::GL::fluidJarSimulationSize(const Vector2D& extent, float precision) { + if (extent.x <= 0 || extent.y <= 0) + return {}; + + const auto clampedPrecision = std::clamp(precision, MIN_PRECISION, MAX_PRECISION); + const auto simulationScale = BASE_SIMULATION_SCALE * clampedPrecision; + const auto maxSide = BASE_SIMULATION_SIDE * clampedPrecision; + const auto scale = std::min({sc(simulationScale), sc(maxSide) / extent.x, sc(maxSide) / extent.y}); + Vector2D size = {std::max(8.0, std::floor(extent.x * scale)), std::max(4.0, std::floor(extent.y * scale))}; + + const auto capacity = fluidJarParticleCapacity(size); + if (capacity > MAX_PARTICLES) { + const auto particleScale = std::sqrt(sc(MAX_PARTICLES) / capacity); + size.x = std::max(8.0, std::floor(size.x * particleScale)); + size.y = std::max(4.0, std::floor(size.y * particleScale)); + } + + while (fluidJarParticleCapacity(size) > MAX_PARTICLES) { + if (size.x >= size.y) + size.x -= 1.0; + else + size.y -= 1.0; + } + + return size; +} + +Vector2D Render::GL::fluidJarGridSize(const Vector2D& simulationSize) { + return {std::max(1.0, std::floor(simulationSize.x * 0.5 / 4.0)), std::max(1.0, std::floor(simulationSize.y * 0.5 / 2.0))}; +} + +int Render::GL::fluidJarParticleCapacity(const Vector2D& simulationSize) { + const auto grid = fluidJarGridSize(simulationSize); + return sc(grid.x * grid.y); +} + +int Render::GL::fluidJarInitialParticleCount(const Vector2D& simulationSize, float fillAmount) { + return sc(std::floor(fluidJarParticleCapacity(simulationSize) * std::clamp(fillAmount, 0.F, 1.F))); +} + +int Render::GL::fluidJarResizedParticleCount(int oldParticleCount, const Vector2D&) { + return std::clamp(oldParticleCount, 0, MAX_PARTICLES); +} + +float Render::GL::fluidJarDamageRadius(int64_t size, int64_t passes, float distortion) { + return dualKawaseDamageRadius(size, passes) + std::ceil(MAX_REFRACTION * std::clamp(distortion, 0.F, MAX_DISTORTION)); +} + +SFluidJarOutputTransform Render::GL::fluidJarOutputTransform(eTransform transform) { + constexpr Vector2D UNIT_EXTENT = {1, 1}; + + const auto offset = Vector2D{}.transform(transform, UNIT_EXTENT); + return { + .xAxis = Vector2D{1, 0}.transform(transform, UNIT_EXTENT) - offset, + .yAxis = Vector2D{0, 1}.transform(transform, UNIT_EXTENT) - offset, + .offset = offset, + }; +} + +SFluidJarGeometryTransform Render::GL::fluidJarGeometryTransform(const CBox& oldExtent, const CBox& newExtent, const Vector2D& oldSimulationSize, const Vector2D& newSimulationSize, + bool preserveWorldPosition) { + if (oldExtent.width <= 0 || oldExtent.height <= 0 || newExtent.width <= 0 || newExtent.height <= 0 || oldSimulationSize.x <= 0 || oldSimulationSize.y <= 0 || + newSimulationSize.x <= 0 || newSimulationSize.y <= 0) + return {}; + + const Vector2D oldScale = {oldSimulationSize.x / oldExtent.width, oldSimulationSize.y / oldExtent.height}; + const Vector2D newScale = {newSimulationSize.x / newExtent.width, newSimulationSize.y / newExtent.height}; + const Vector2D scale = {newScale.x / oldScale.x, newScale.y / oldScale.y}; + if (!preserveWorldPosition) + return {.positionScale = {newSimulationSize.x / oldSimulationSize.x, newSimulationSize.y / oldSimulationSize.y}, .velocityScale = {0, 0}}; + + const auto oldBottom = oldExtent.y + oldExtent.height; + const auto newBottom = newExtent.y + newExtent.height; + return { + .positionScale = scale, + .positionOffset = {(oldExtent.x - newExtent.x) * newScale.x, (newBottom - oldBottom) * newScale.y}, + .velocityScale = scale, + }; +} + +std::array Render::GL::fluidJarWallVelocities(const CBox& oldExtent, const CBox& newExtent, const Vector2D& simulationSize, float elapsed, float speed) { + if (elapsed <= 0.F || oldExtent.width <= 0 || oldExtent.height <= 0 || newExtent.width <= 0 || newExtent.height <= 0 || simulationSize.x <= 0 || simulationSize.y <= 0) + return {}; + + if (speed <= 0.F) + return {}; + + const auto scaleX = simulationSize.x / newExtent.width; + const auto scaleY = simulationSize.y / newExtent.height; + const auto oldRight = oldExtent.x + oldExtent.width; + const auto newRight = newExtent.x + newExtent.width; + const auto oldBottom = oldExtent.y + oldExtent.height; + const auto newBottom = newExtent.y + newExtent.height; + const auto solverScale = FIXED_TIMESTEP / (SOLVER_TIMESTEP * speed); + const auto velocity = [&](double displacement, double scale) { return std::clamp(sc(displacement / elapsed * scale * solverScale), -MAX_WALL_SPEED, MAX_WALL_SPEED); }; + + return { + velocity(newExtent.x - oldExtent.x, scaleX), + velocity(newRight - oldRight, scaleX), + velocity(oldBottom - newBottom, scaleY), + velocity(newExtent.y - oldExtent.y, scaleY), + }; +} diff --git a/src/render/gl/blur/FluidJar.hpp b/src/render/gl/blur/FluidJar.hpp new file mode 100644 index 000000000..7a6d3b51c --- /dev/null +++ b/src/render/gl/blur/FluidJar.hpp @@ -0,0 +1,115 @@ +#pragma once + +#include "Kawase.hpp" + +#include "../../../helpers/signal/Signal.hpp" +#include "../../../helpers/time/Time.hpp" + +#include +#include +#include + +namespace Render::GL { + class CGLFramebuffer; + + struct SFluidJarGeometryTransform { + Vector2D positionScale = {1, 1}; + Vector2D positionOffset = {}; + Vector2D velocityScale = {1, 1}; + }; + + struct SFluidJarOutputTransform { + Vector2D xAxis = {1, 0}; + Vector2D yAxis = {0, 1}; + Vector2D offset = {}; + }; + + class CFluidJarBlurMaterial final : public IGLBlurMaterial { + public: + explicit CFluidJarBlurMaterial(CHyprOpenGLImpl& impl); + + eBlurType type() const noexcept override; + SBlurMaterialRequirements requirements() const noexcept override; + int64_t blurSizeForDamage(int64_t size) const override; + float sampleRadius() const override; + void prepare(const SBlurMaterialContext& context) override; + void bindFinish(WP shader, const SBlurMaterialContext& context) const override; + + private: + struct SState { + PHLWINDOWREF window; + std::array, 2> particles; + std::array, 2> graph; + std::array, 2> tracking; + std::array, 2> visual; + Vector2D simulationSize = {}; + Vector2D particleTextureSize = {}; + Vector2D graphTextureSize = {}; + Vector2D gridSize = {}; + CBox extent = {}; + Time::steady_tp lastUpdate = {}; + std::array wallVelocities = {}; + double accumulator = 0.0; + double animationTime = 0.0; + float fillAmount = 0.F; + float precision = 1.F; + int particleCount = 0; + uint64_t simulationFrame = 0; + uint64_t lastFrame = std::numeric_limits::max(); + uint8_t currentParticles = 0; + uint8_t currentGraph = 0; + uint8_t currentTracking = 0; + uint8_t currentVisual = 0; + bool hasExtent = false; + }; + + SState* stateForContext(const SBlurContext& context, bool create); + const SState* stateForContext(const SBlurContext& context) const; + void updateState(SState& state, const CBox& extent); + void initializeState(SState& state, const Vector2D& simulationSize, float fillAmount, float precision); + void transformState(SState& state, const Vector2D& simulationSize, const SFluidJarGeometryTransform& transform, const std::array& wallVelocities); + void allocateBuffers(std::array, 2>& buffers, const Vector2D& size, const std::string& name, DRMFormat format = DRM_FORMAT_ABGR16161616F) const; + void clearBuffers(const std::array, 2>& buffers, const std::array& color) const; + void clearIntegerBuffers(const std::array, 2>& buffers) const; + void drawInitialize(const SState& state, SP target) const; + void drawResample(const SState& state, SP source, SP target, const Vector2D& oldGridSize, int oldParticleCount, + const SFluidJarGeometryTransform& transform, const std::array& wallVelocities) const; + void drawHistoryResample(SP source, SP target, const Vector2D& oldSize, const SFluidJarGeometryTransform& transform, + const std::array& fallback, bool linear) const; + void drawParticleStep(SState& state, float dt) const; + void drawGraphStep(SState& state) const; + void drawTrackingStep(SState& state) const; + void drawTrackingResample(SP source, SP target, const Vector2D& oldSize, const SFluidJarGeometryTransform& transform) const; + void drawVisualStep(SState& state, int steps = 1) const; + void preparePass(SP target, const Vector2D& size, WP shader) const; + CBox transformedPatternBox(const SBlurContext& context) const; + void scheduleNextFrame(const SState& state) const; + void pruneStates(); + + CHyprOpenGLImpl& m_impl; + std::vector m_states; + uint64_t m_frame = 0; + bool m_supported = false; + + struct { + CHyprSignalListener renderPre; + CHyprSignalListener windowDestroy; + } m_listeners; + }; + + class CFluidJarBlurProvider final : public CDualKawaseBlurProvider { + public: + explicit CFluidJarBlurProvider(CHyprOpenGLImpl& impl); + }; + + Vector2D fluidJarSimulationSize(const Vector2D& extent, float precision = 1.F); + Vector2D fluidJarGridSize(const Vector2D& simulationSize); + int fluidJarParticleCapacity(const Vector2D& simulationSize); + int fluidJarInitialParticleCount(const Vector2D& simulationSize, float fillAmount); + int fluidJarResizedParticleCount(int oldParticleCount, const Vector2D& simulationSize); + float fluidJarDamageRadius(int64_t size, int64_t passes, float distortion = 1.F); + SFluidJarOutputTransform fluidJarOutputTransform(eTransform transform); + SFluidJarGeometryTransform fluidJarGeometryTransform(const CBox& oldExtent, const CBox& newExtent, const Vector2D& oldSimulationSize, const Vector2D& newSimulationSize, + bool preserveWorldPosition = true); + std::array fluidJarWallVelocities(const CBox& oldExtent, const CBox& newExtent, const Vector2D& simulationSize, float elapsed, float speed = 1.F); +} diff --git a/src/render/gl/blur/Glass.cpp b/src/render/gl/blur/Glass.cpp new file mode 100644 index 000000000..6d4252f83 --- /dev/null +++ b/src/render/gl/blur/Glass.cpp @@ -0,0 +1,68 @@ +#include "Glass.hpp" + +#include "Kawase.hpp" + +#include "../../Renderer.hpp" +#include "../../Shader.hpp" +#include "../../ShaderLoader.hpp" +#include "../../../config/ConfigValue.hpp" + +#include +#include +#include + +using namespace Render; +using namespace Render::GL; + +static constexpr float MAX_GLASS_REFRACTION = 20.F; + +CGlassBlurMaterial::CGlassBlurMaterial(eBlurType type, ePreparedFragmentShader finishFragment, bool preparedInput) : + m_type(type), m_finishFragment(finishFragment), m_preparedInput(preparedInput) { + ; +} + +CGlassBlurProvider::CGlassBlurProvider(CHyprOpenGLImpl& impl, eBlurType type, ePreparedFragmentShader finishFragment) : + CDualKawaseBlurProvider(impl, makeUnique(type, finishFragment)) { + ; +} + +CGlassBlurProvider::CGlassBlurProvider(CHyprOpenGLImpl& impl, UP material) : CDualKawaseBlurProvider(impl, std::move(material)) { + ; +} + +eBlurType CGlassBlurMaterial::type() const noexcept { + return m_type; +} + +SBlurMaterialRequirements CGlassBlurMaterial::requirements() const noexcept { + return { + .finishFragment = m_finishFragment, + .preparedInput = m_preparedInput, + }; +} + +void CGlassBlurMaterial::bindFinish(WP shader, const SBlurMaterialContext& context) const { + static auto PGLASSREFRACTION = CConfigValue("decoration:blur:glass:refraction"); + static auto PGLASSSIZE = CConfigValue("decoration:blur:glass:size"); + static auto PGLASSROUGHNESS = CConfigValue("decoration:blur:glass:roughness"); + + const auto clampedStrength = std::clamp(context.strength, 0.F, 1.F); + + shader->setUniformFloat(SHADER_GLASS_REFRACTION, std::clamp(*PGLASSREFRACTION, 0.F, MAX_GLASS_REFRACTION) * clampedStrength); + shader->setUniformFloat(SHADER_GLASS_SIZE, std::clamp(*PGLASSSIZE, 4.F, 512.F)); + shader->setUniformFloat(SHADER_GLASS_ROUGHNESS, std::clamp(*PGLASSROUGHNESS, 0.F, 1.F) * clampedStrength); + + const CBox patternBox = context.blurContext.patternBox.value_or(CBox{}); + + shader->setUniformFloat2(SHADER_GLASS_POSITION, sc(patternBox.x), sc(patternBox.y)); +} + +float CGlassBlurMaterial::sampleRadius() const { + static auto PGLASSREFRACTION = CConfigValue("decoration:blur:glass:refraction"); + + return std::ceil(std::clamp(*PGLASSREFRACTION, 0.F, MAX_GLASS_REFRACTION)); +} + +float Render::GL::glassDamageRadius(int64_t size, int64_t passes, float refraction) { + return dualKawaseDamageRadius(size, passes) + std::ceil(std::clamp(refraction, 0.F, MAX_GLASS_REFRACTION)); +} diff --git a/src/render/gl/blur/Glass.hpp b/src/render/gl/blur/Glass.hpp new file mode 100644 index 000000000..76671add0 --- /dev/null +++ b/src/render/gl/blur/Glass.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include "Kawase.hpp" + +namespace Render::GL { + class CGlassBlurMaterial : public IGLBlurMaterial { + public: + CGlassBlurMaterial(eBlurType type, ePreparedFragmentShader finishFragment, bool preparedInput = false); + + eBlurType type() const noexcept override; + SBlurMaterialRequirements requirements() const noexcept override; + float sampleRadius() const override; + void bindFinish(WP shader, const SBlurMaterialContext& context) const override; + + private: + const eBlurType m_type; + const ePreparedFragmentShader m_finishFragment; + const bool m_preparedInput; + }; + + class CGlassBlurProvider : public CDualKawaseBlurProvider { + public: + CGlassBlurProvider(CHyprOpenGLImpl& impl, eBlurType type, ePreparedFragmentShader finishFragment); + + protected: + CGlassBlurProvider(CHyprOpenGLImpl& impl, UP material); + }; + + float glassDamageRadius(int64_t size, int64_t passes, float refraction); +} diff --git a/src/render/gl/blur/Haze.cpp b/src/render/gl/blur/Haze.cpp new file mode 100644 index 000000000..b9ef3817c --- /dev/null +++ b/src/render/gl/blur/Haze.cpp @@ -0,0 +1,40 @@ +#include "Haze.hpp" + +#include "../../Renderer.hpp" +#include "../../Shader.hpp" +#include "../../ShaderLoader.hpp" +#include "../../../config/ConfigValue.hpp" +#include "../../../helpers/cm/ColorManagement.hpp" + +#include + +using namespace Render; +using namespace Render::GL; +using namespace NColorManagement; + +eBlurType CHazeBlurMaterial::type() const noexcept { + return eBlurType::BLUR_HAZE; +} + +SBlurMaterialRequirements CHazeBlurMaterial::requirements() const noexcept { + return { + .finishFragment = SH_FRAG_HAZEFINISH, + }; +} + +int64_t CHazeBlurMaterial::blurSizeForDamage(int64_t size) const { + return std::clamp(size, 1, 40); +} + +void CHazeBlurMaterial::bindFinish(WP shader, const SBlurMaterialContext& context) const { + static auto PHAZEINTENSITY = CConfigValue("decoration:blur:haze:intensity"); + static auto PHAZEIRIDESCENCE = CConfigValue("decoration:blur:haze:iridescence"); + + shader->setUniformFloat(SHADER_HAZE_INTENSITY, std::clamp(*PHAZEINTENSITY, 0.F, 1.F) * std::clamp(context.strength, 0.F, 1.F)); + shader->setUniformFloat(SHADER_HAZE_IRIDESCENCE, std::clamp(*PHAZEIRIDESCENCE, 0.F, 1.F)); + shader->setUniformInt(SHADER_HAZE_TRANSFER_FUNCTION, sc(getDefaultImageDescription()->value().transferFunction)); +} + +CHazeBlurProvider::CHazeBlurProvider(CHyprOpenGLImpl& impl) : CDualKawaseBlurProvider(impl, makeUnique()) { + ; +} diff --git a/src/render/gl/blur/Haze.hpp b/src/render/gl/blur/Haze.hpp new file mode 100644 index 000000000..426654e94 --- /dev/null +++ b/src/render/gl/blur/Haze.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "Kawase.hpp" + +namespace Render::GL { + class CHazeBlurMaterial final : public IGLBlurMaterial { + public: + eBlurType type() const noexcept override; + SBlurMaterialRequirements requirements() const noexcept override; + int64_t blurSizeForDamage(int64_t size) const override; + void bindFinish(WP shader, const SBlurMaterialContext& context) const override; + }; + + class CHazeBlurProvider final : public CDualKawaseBlurProvider { + public: + explicit CHazeBlurProvider(CHyprOpenGLImpl& impl); + }; +} diff --git a/src/render/gl/blur/HeatShimmer.cpp b/src/render/gl/blur/HeatShimmer.cpp new file mode 100644 index 000000000..3971df4b1 --- /dev/null +++ b/src/render/gl/blur/HeatShimmer.cpp @@ -0,0 +1,60 @@ +#include "HeatShimmer.hpp" + +#include "../../Shader.hpp" +#include "../../ShaderLoader.hpp" +#include "../../../config/ConfigValue.hpp" +#include "../../../event/EventBus.hpp" + +#include +#include +#include + +using namespace Render; +using namespace Render::GL; + +static constexpr float MAX_HEAT_SHIMMER_SPEED = 10.F; +static constexpr double HEAT_SHIMMER_BASE_SPEED = 0.8; +static constexpr double HEAT_SHIMMER_PERIOD = 6.283185307179586; + +static float heatShimmerSpeed() { + static auto PHEATSHIMMERSPEED = CConfigValue("decoration:blur:heat_shimmer:speed"); + return std::clamp(*PHEATSHIMMERSPEED, 0.F, MAX_HEAT_SHIMMER_SPEED); +} + +CHeatShimmerBlurMaterial::CHeatShimmerBlurMaterial() : CGlassBlurMaterial(eBlurType::BLUR_HEAT_SHIMMER, SH_FRAG_HEATSHIMMERFINISH), m_lastAnimationUpdate(Time::steadyNow()) { + m_configListener = Event::bus()->m_events.config.props_refreshed.listen([this](const bool) { updateAnimation(heatShimmerSpeed()); }); +} + +CHeatShimmerBlurProvider::CHeatShimmerBlurProvider(CHyprOpenGLImpl& impl) : CGlassBlurProvider(impl, makeUnique()) { + ; +} + +bool CHeatShimmerBlurMaterial::isAnimated() const noexcept { + static auto PBLURENABLED = CConfigValue("decoration:blur:enabled"); + static auto PGLASSREFRACTION = CConfigValue("decoration:blur:glass:refraction"); + static auto PGLASSROUGHNESS = CConfigValue("decoration:blur:glass:roughness"); + + const auto SPEED = heatShimmerSpeed(); + updateAnimation(SPEED); + return *PBLURENABLED && SPEED > 0.F && (*PGLASSREFRACTION > 0.F || *PGLASSROUGHNESS > 0.F); +} + +void CHeatShimmerBlurMaterial::bindFinish(WP shader, const SBlurMaterialContext& context) const { + CGlassBlurMaterial::bindFinish(shader, context); + updateAnimation(heatShimmerSpeed()); + shader->setUniformFloat(SHADER_TIME, animationPhase()); +} + +void CHeatShimmerBlurMaterial::updateAnimation(float speed) const { + const auto NOW = Time::steadyNow(); + m_animationTime += std::chrono::duration(NOW - m_lastAnimationUpdate).count() * m_previousSpeed; + m_lastAnimationUpdate = NOW; + m_previousSpeed = speed; +} + +float CHeatShimmerBlurMaterial::animationPhase() const { + if (m_previousSpeed <= 0.F) + return 0.F; + + return sc(std::fmod(m_animationTime * HEAT_SHIMMER_BASE_SPEED, HEAT_SHIMMER_PERIOD)); +} diff --git a/src/render/gl/blur/HeatShimmer.hpp b/src/render/gl/blur/HeatShimmer.hpp new file mode 100644 index 000000000..9c73650aa --- /dev/null +++ b/src/render/gl/blur/HeatShimmer.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include "Glass.hpp" + +#include "../../../helpers/signal/Signal.hpp" +#include "../../../helpers/time/Time.hpp" + +namespace Render::GL { + class CHeatShimmerBlurMaterial final : public CGlassBlurMaterial { + public: + CHeatShimmerBlurMaterial(); + + bool isAnimated() const noexcept override; + void bindFinish(WP shader, const SBlurMaterialContext& context) const override; + + private: + void updateAnimation(float speed) const; + float animationPhase() const; + + mutable Time::steady_tp m_lastAnimationUpdate; + mutable double m_animationTime = 0.0; + mutable float m_previousSpeed = 0.F; + CHyprSignalListener m_configListener; + }; + + class CHeatShimmerBlurProvider final : public CGlassBlurProvider { + public: + explicit CHeatShimmerBlurProvider(CHyprOpenGLImpl& impl); + }; +} diff --git a/src/render/gl/blur/Kawase.cpp b/src/render/gl/blur/Kawase.cpp new file mode 100644 index 000000000..54612392f --- /dev/null +++ b/src/render/gl/blur/Kawase.cpp @@ -0,0 +1,292 @@ +#include "Kawase.hpp" + +#include "../../OpenGL.hpp" +#include "../../Renderer.hpp" +#include "../../../config/ConfigValue.hpp" + +#include +#include + +using namespace Render; +using namespace Render::GL; +using namespace NColorManagement; + +static SCMSettings blurIntermediateCMSettings(bool toIntermediate) { + const auto WORKBUFFER = g_pHyprRenderer->workBufferImageDescription(); + const auto INTERMEDIATE = getDefaultImageDescription(); + + auto settings = toIntermediate ? g_pHyprRenderer->getCMSettings(WORKBUFFER, INTERMEDIATE) : g_pHyprRenderer->getCMSettings(INTERMEDIATE, WORKBUFFER); + auto& range = toIntermediate ? settings.dstTFRange : settings.srcTFRange; + range.max = std::max(range.max, sc(WORKBUFFER->value().luminances.max)); + return settings; +} + +CDualKawaseBlurProvider::CDualKawaseBlurProvider(CHyprOpenGLImpl& impl) : CDualKawaseBlurProvider(impl, makeUnique()) { + ; +} + +CDualKawaseBlurProvider::CDualKawaseBlurProvider(CHyprOpenGLImpl& impl, UP material) : m_impl(impl), m_material(std::move(material)) { + RASSERT(m_material, "Cannot create a dual Kawase blur provider without a material"); + ; +} + +eBlurType CDualKawaseBlurProvider::type() const noexcept { + return m_material->type(); +} + +bool CDualKawaseBlurProvider::isAnimated() const noexcept { + return m_material->isAnimated(); +} + +bool CDualKawaseBlurProvider::requiresLiveBlur() const noexcept { + return m_material->requirements().liveBlur; +} + +float Render::GL::dualKawaseDamageRadius(int64_t size, int64_t passes) { + const auto blurPasses = std::clamp(passes, sc(1), sc(8)); + const auto accumulatedScale = (1 << blurPasses) - 1; + return 2.F * std::max(size, sc(1)) * accumulatedScale; +} + +void CDualKawaseBlurProvider::expandDamage(CRegion& damage, float multiplier) const { + damage.expand(damageRadius() * multiplier); +} + +float CDualKawaseBlurProvider::damageRadius() const { + static auto PBLURSIZE = CConfigValue("decoration:blur:size"); + static auto PBLURPASSES = CConfigValue("decoration:blur:passes"); + + return dualKawaseDamageRadius(m_material->blurSizeForDamage(*PBLURSIZE), *PBLURPASSES) + m_material->sampleRadius(); +} + +SP CDualKawaseBlurProvider::blurGL(SP source, float strength, const CRegion& originalDamage, const SBlurContext& context) { + TRACY_GPU_ZONE("RenderBlurFramebufferWithDamage"); + auto& m_renderData = g_pHyprRenderer->m_renderData; + + const auto BLENDBEFORE = m_impl.m_blend; + m_impl.blend(false); + m_impl.setCapStatus(GL_STENCIL_TEST, false); + + CBox MONITORBOX = {0, 0, m_renderData.pMonitor->m_transformedSize.x, m_renderData.pMonitor->m_transformedSize.y}; + + const auto& glMatrix = g_pHyprRenderer->projectBoxToTarget(MONITORBOX); + + static auto PBLURSIZE = CConfigValue("decoration:blur:size"); + static auto PBLURPASSES = CConfigValue("decoration:blur:passes"); + static auto PBLURVIBRANCY = CConfigValue("decoration:blur:vibrancy"); + static auto PBLURVIBRANCYDARKNESS = CConfigValue("decoration:blur:vibrancy_darkness"); + + const auto BLUR_PASSES = std::clamp(*PBLURPASSES, sc(1), sc(8)); + + CRegion outputDamage{originalDamage}; + + const SBlurMaterialContext materialContext{ + .blurContext = context, + .outputDamage = outputDamage, + .strength = strength, + }; + m_material->prepare(materialContext); + + CRegion workingDamage{outputDamage}; + expandDamage(workingDamage); + + const auto MATERIAL_REQUIREMENTS = m_material->requirements(); + const bool REQUIRES_PREPARED_INPUT = MATERIAL_REQUIREMENTS.preparedInput; + + const auto PMIRRORFB = dynamicPointerCast(m_renderData.pMonitor->resources()->getUnusedWorkBuffer()); + const auto PMIRRORSWAPFB = dynamicPointerCast(m_renderData.pMonitor->resources()->getUnusedWorkBuffer()); + RASSERT(PMIRRORFB && PMIRRORSWAPFB, "Failed to obtain GL work buffers for dual Kawase blur"); + + const auto PPREPAREDFB = REQUIRES_PREPARED_INPUT ? dynamicPointerCast(m_renderData.pMonitor->resources()->getUnusedWorkBuffer()) : PMIRRORSWAPFB; + RASSERT(PPREPAREDFB, "Failed to obtain GL prepared work buffer for dual Kawase blur"); + + auto currentRenderToFB = PMIRRORFB; + + // Begin with base color adjustments - global brightness and contrast + // TODO: make this a part of the first pass maybe to save on a drawcall? + { + static auto PBLURCONTRAST = CConfigValue("decoration:blur:contrast"); + static auto PBLURBRIGHTNESS = CConfigValue("decoration:blur:brightness"); + static auto PBLEND = CConfigValue("render:use_shader_blur_blend"); + + PPREPAREDFB->bind(); + PPREPAREDFB->clearAfterInvalidation(); + + m_impl.setActiveTexture(GL_TEXTURE0); + + auto currentTex = source->getTexture(); + + currentTex->bind(); + currentTex->setTexParameter(GL_TEXTURE_MIN_FILTER, GL_LINEAR); + + WP shader; + + const bool skipCM = !m_impl.m_cmSupported || !g_pHyprRenderer->workBufferImageDescription()->needsCM(getDefaultImageDescription()); + if (!skipCM) { + const auto settings = blurIntermediateCMSettings(/* toIntermediate */ true); + shader = m_impl.useShader(m_impl.getShaderVariant(SH_FRAG_BLURPREPARE, SH_FEAT_CM, settings.sourceTF, settings.targetTF)); + + m_impl.passCMUniforms(shader, g_pHyprRenderer->workBufferImageDescription(), getDefaultImageDescription(), false, -1.F, -1, settings); + shader->setUniformFloat(SHADER_SDR_SATURATION, + m_renderData.pMonitor->m_sdrSaturation > 0 && + g_pHyprRenderer->workBufferImageDescription()->value().transferFunction == CM_TRANSFER_FUNCTION_ST2084_PQ ? + m_renderData.pMonitor->m_sdrSaturation : + 1.0f); + shader->setUniformFloat(SHADER_SDR_BRIGHTNESS, + m_renderData.pMonitor->m_sdrBrightness > 0 && + g_pHyprRenderer->workBufferImageDescription()->value().transferFunction == CM_TRANSFER_FUNCTION_ST2084_PQ ? + m_renderData.pMonitor->m_sdrBrightness : + 1.0f); + } else + shader = m_impl.useShader(m_impl.getShaderVariant(SH_FRAG_BLURPREPARE)); + + shader->setUniformMatrix3fv(SHADER_PROJ, 1, GL_TRUE, glMatrix.getMatrix()); + shader->setUniformFloat(SHADER_CONTRAST, *PBLURCONTRAST); + shader->setUniformFloat(SHADER_BRIGHTNESS, *PBLURBRIGHTNESS); + shader->setUniformInt(SHADER_TEX, 0); + + glBindVertexArray(shader->getUniformLocation(SHADER_SHADER_VAO)); + + if (!workingDamage.empty()) { + workingDamage.forEachRect([this](const auto& RECT) { + m_impl.scissor(&RECT, false); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + }); + } + + glBindVertexArray(0); + currentRenderToFB = PPREPAREDFB; + } + + auto drawPass = [&](WP shader, ePreparedFragmentShader frag, CRegion* passDamage) { + if (currentRenderToFB == PMIRRORFB) + PMIRRORSWAPFB->bind(); + else + PMIRRORFB->bind(); + + m_impl.setActiveTexture(GL_TEXTURE0); + + auto currentTex = currentRenderToFB->getTexture(); + + currentTex->bind(); + currentTex->setTexParameter(GL_TEXTURE_MIN_FILTER, GL_LINEAR); + + shader->setUniformMatrix3fv(SHADER_PROJ, 1, GL_TRUE, glMatrix.getMatrix()); + shader->setUniformFloat(SHADER_RADIUS, *PBLURSIZE * strength); + if (frag == SH_FRAG_BLUR1) { + shader->setUniformFloat2(SHADER_HALFPIXEL, 0.5f / (m_renderData.pMonitor->m_transformedSize.x / 2.f), 0.5f / (m_renderData.pMonitor->m_transformedSize.y / 2.f)); + shader->setUniformInt(SHADER_PASSES, BLUR_PASSES); + shader->setUniformFloat(SHADER_VIBRANCY, *PBLURVIBRANCY); + shader->setUniformFloat(SHADER_VIBRANCY_DARKNESS, *PBLURVIBRANCYDARKNESS); + } else + shader->setUniformFloat2(SHADER_HALFPIXEL, 0.5f / (m_renderData.pMonitor->m_transformedSize.x * 2.f), 0.5f / (m_renderData.pMonitor->m_transformedSize.y * 2.f)); + shader->setUniformInt(SHADER_TEX, 0); + + glBindVertexArray(shader->getUniformLocation(SHADER_SHADER_VAO)); + + if (!passDamage->empty()) { + passDamage->forEachRect([this](const auto& RECT) { + m_impl.scissor(&RECT, false); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + }); + } + + glBindVertexArray(0); + + if (currentRenderToFB != PMIRRORFB) + currentRenderToFB = PMIRRORFB; + else + currentRenderToFB = PMIRRORSWAPFB; + }; + + PMIRRORFB->bind(); + PMIRRORFB->clearAfterInvalidation(); + PMIRRORSWAPFB->getTexture()->bind(); + + CRegion tempDamage{workingDamage}; + + auto shader = m_impl.useShader(m_impl.getShaderVariant(SH_FRAG_BLUR1)); + for (auto i = 1; i <= BLUR_PASSES; ++i) { + tempDamage = workingDamage.copy().scale(1.f / (1 << i)); + drawPass(shader, SH_FRAG_BLUR1, &tempDamage); + } + + shader = m_impl.useShader(m_impl.getShaderVariant(SH_FRAG_BLUR2)); + for (auto i = BLUR_PASSES - 1; i >= 0; --i) { + tempDamage = workingDamage.copy().scale(1.f / (1 << i)); + drawPass(shader, SH_FRAG_BLUR2, &tempDamage); + } + + { + static auto PBLURNOISE = CConfigValue("decoration:blur:noise"); + static auto PBLURBRIGHTNESS = CConfigValue("decoration:blur:brightness"); + + if (currentRenderToFB == PMIRRORFB) + PMIRRORSWAPFB->bind(); + else + PMIRRORFB->bind(); + + m_impl.setActiveTexture(GL_TEXTURE0); + + auto currentTex = currentRenderToFB->getTexture(); + + currentTex->bind(); + currentTex->setTexParameter(GL_TEXTURE_MIN_FILTER, GL_LINEAR); + + if (REQUIRES_PREPARED_INPUT) { + m_impl.setActiveTexture(GL_TEXTURE1); + auto preparedTex = PPREPAREDFB->getTexture(); + preparedTex->bind(); + preparedTex->setTexParameter(GL_TEXTURE_MIN_FILTER, GL_LINEAR); + m_impl.setActiveTexture(GL_TEXTURE0); + } + + const bool skipCM = !m_impl.m_cmSupported || !g_pHyprRenderer->workBufferImageDescription()->needsCM(getDefaultImageDescription()); + if (!skipCM) { + const auto settings = blurIntermediateCMSettings(/* toIntermediate */ false); + shader = m_impl.useShader(m_impl.getShaderVariant(MATERIAL_REQUIREMENTS.finishFragment, SH_FEAT_CM, settings.sourceTF, settings.targetTF)); + + m_impl.passCMUniforms(shader, getDefaultImageDescription(), g_pHyprRenderer->workBufferImageDescription(), false, -1.F, -1, settings); + shader->setUniformFloat(SHADER_SDR_SATURATION, + m_renderData.pMonitor->m_sdrSaturation > 0 && + g_pHyprRenderer->workBufferImageDescription()->value().transferFunction == CM_TRANSFER_FUNCTION_ST2084_PQ ? + m_renderData.pMonitor->m_sdrSaturation : + 1.0f); + shader->setUniformFloat(SHADER_SDR_BRIGHTNESS, + m_renderData.pMonitor->m_sdrBrightness > 0 && + g_pHyprRenderer->workBufferImageDescription()->value().transferFunction == CM_TRANSFER_FUNCTION_ST2084_PQ ? + m_renderData.pMonitor->m_sdrBrightness : + 1.0f); + } else + shader = m_impl.useShader(m_impl.getShaderVariant(MATERIAL_REQUIREMENTS.finishFragment)); + + shader->setUniformMatrix3fv(SHADER_PROJ, 1, GL_TRUE, glMatrix.getMatrix()); + shader->setUniformFloat(SHADER_NOISE, *PBLURNOISE); + shader->setUniformFloat(SHADER_BRIGHTNESS, *PBLURBRIGHTNESS); + shader->setUniformInt(SHADER_TEX, 0); + if (REQUIRES_PREPARED_INPUT) + shader->setUniformInt(SHADER_SHARP_TEX, 1); + m_material->bindFinish(shader, materialContext); + + glBindVertexArray(shader->getUniformLocation(SHADER_SHADER_VAO)); + + if (!outputDamage.empty()) { + outputDamage.forEachRect([this](const auto& RECT) { + m_impl.scissor(&RECT, false /* this region is already transformed */); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + }); + } + + glBindVertexArray(0); + + if (currentRenderToFB != PMIRRORFB) + currentRenderToFB = PMIRRORFB; + else + currentRenderToFB = PMIRRORSWAPFB; + } + + PMIRRORFB->getTexture()->unbind(); + m_impl.blend(BLENDBEFORE); + + return currentRenderToFB; +} diff --git a/src/render/gl/blur/Kawase.hpp b/src/render/gl/blur/Kawase.hpp new file mode 100644 index 000000000..8f82eb78a --- /dev/null +++ b/src/render/gl/blur/Kawase.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include "Material.hpp" +#include "Provider.hpp" + +namespace Render::GL { + class CHyprOpenGLImpl; + + class CDualKawaseBlurProvider : public IGLBlurProvider { + public: + explicit CDualKawaseBlurProvider(CHyprOpenGLImpl& impl); + CDualKawaseBlurProvider(CHyprOpenGLImpl& impl, UP material); + + eBlurType type() const noexcept override; + bool isAnimated() const noexcept override; + bool requiresLiveBlur() const noexcept override; + void expandDamage(CRegion& damage, float multiplier = 1.F) const override; + + protected: + SP blurGL(SP source, float strength, const CRegion& originalDamage, const SBlurContext& context) override; + + private: + float damageRadius() const; + + CHyprOpenGLImpl& m_impl; + UP m_material; + }; + + float dualKawaseDamageRadius(int64_t size, int64_t passes); +} diff --git a/src/render/gl/blur/Material.cpp b/src/render/gl/blur/Material.cpp new file mode 100644 index 000000000..5c21554c8 --- /dev/null +++ b/src/render/gl/blur/Material.cpp @@ -0,0 +1,42 @@ +#include "Material.hpp" + +#include "../../ShaderLoader.hpp" + +#include + +using namespace Render; +using namespace Render::GL; + +bool IGLBlurMaterial::isAnimated() const noexcept { + return false; +} + +int64_t IGLBlurMaterial::blurSizeForDamage(int64_t size) const { + return size; +} + +float IGLBlurMaterial::sampleRadius() const { + return 0.F; +} + +void IGLBlurMaterial::prepare(const SBlurMaterialContext&) { + ; +} + +void IGLBlurMaterial::bindFinish(WP, const SBlurMaterialContext&) const { + ; +} + +eBlurType CDefaultBlurMaterial::type() const noexcept { + return eBlurType::BLUR_DUAL_KAWASE; +} + +SBlurMaterialRequirements CDefaultBlurMaterial::requirements() const noexcept { + return { + .finishFragment = SH_FRAG_BLURFINISH, + }; +} + +int64_t CDefaultBlurMaterial::blurSizeForDamage(int64_t size) const { + return std::clamp(size, 1, 40); +} diff --git a/src/render/gl/blur/Material.hpp b/src/render/gl/blur/Material.hpp new file mode 100644 index 000000000..b3b35e0df --- /dev/null +++ b/src/render/gl/blur/Material.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include "../../ShaderLoader.hpp" +#include "../../blur/Provider.hpp" + +class CShader; + +namespace Render::GL { + class CHyprOpenGLImpl; + + struct SBlurMaterialRequirements { + ePreparedFragmentShader finishFragment = SH_FRAG_BLURFINISH; + bool preparedInput = false; + bool liveBlur = false; + }; + + struct SBlurMaterialContext { + const SBlurContext& blurContext; + const CRegion& outputDamage; + float strength = 1.F; + }; + + class IGLBlurMaterial { + public: + virtual ~IGLBlurMaterial() = default; + + virtual eBlurType type() const noexcept = 0; + virtual SBlurMaterialRequirements requirements() const noexcept = 0; + virtual bool isAnimated() const noexcept; + virtual int64_t blurSizeForDamage(int64_t size) const; + virtual float sampleRadius() const; + virtual void prepare(const SBlurMaterialContext& context); + virtual void bindFinish(WP shader, const SBlurMaterialContext& context) const; + + protected: + IGLBlurMaterial() = default; + }; + + class CDefaultBlurMaterial final : public IGLBlurMaterial { + public: + eBlurType type() const noexcept override; + SBlurMaterialRequirements requirements() const noexcept override; + int64_t blurSizeForDamage(int64_t size) const override; + }; +} diff --git a/src/render/gl/blur/Prism.cpp b/src/render/gl/blur/Prism.cpp new file mode 100644 index 000000000..0a073ab4d --- /dev/null +++ b/src/render/gl/blur/Prism.cpp @@ -0,0 +1,14 @@ +#include "Prism.hpp" + +#include "../../ShaderLoader.hpp" + +using namespace Render; +using namespace Render::GL; + +CPrismBlurMaterial::CPrismBlurMaterial() : CGlassBlurMaterial(eBlurType::BLUR_PRISM, SH_FRAG_PRISMFINISH, true) { + ; +} + +CPrismBlurProvider::CPrismBlurProvider(CHyprOpenGLImpl& impl) : CGlassBlurProvider(impl, makeUnique()) { + ; +} diff --git a/src/render/gl/blur/Prism.hpp b/src/render/gl/blur/Prism.hpp new file mode 100644 index 000000000..22346db73 --- /dev/null +++ b/src/render/gl/blur/Prism.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include "Glass.hpp" + +namespace Render::GL { + class CPrismBlurMaterial final : public CGlassBlurMaterial { + public: + CPrismBlurMaterial(); + }; + + class CPrismBlurProvider final : public CGlassBlurProvider { + public: + explicit CPrismBlurProvider(CHyprOpenGLImpl& impl); + }; +} diff --git a/src/render/gl/blur/Provider.cpp b/src/render/gl/blur/Provider.cpp new file mode 100644 index 000000000..10781c616 --- /dev/null +++ b/src/render/gl/blur/Provider.cpp @@ -0,0 +1,13 @@ +#include "Provider.hpp" + +#include "../GLFramebuffer.hpp" +#include "../../../debug/log/Logger.hpp" + +using namespace Render; +using namespace Render::GL; + +SP IGLBlurProvider::blur(SP source, float strength, const CRegion& originalDamage, const SBlurContext& context) { + const auto glSource = dynamicPointerCast(source); + RASSERT(glSource, "Tried to use a GL blur provider with a non-GL framebuffer"); + return blurGL(glSource, strength, originalDamage, context); +} diff --git a/src/render/gl/blur/Provider.hpp b/src/render/gl/blur/Provider.hpp new file mode 100644 index 000000000..dc3f80b68 --- /dev/null +++ b/src/render/gl/blur/Provider.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include "../../blur/Provider.hpp" + +namespace Render::GL { + class CGLFramebuffer; + + class IGLBlurProvider : public Render::IBlurProvider { + public: + SP blur(SP source, float strength, const CRegion& originalDamage, const SBlurContext& context = {}) final; + + protected: + IGLBlurProvider() = default; + + virtual SP blurGL(SP source, float strength, const CRegion& originalDamage, const SBlurContext& context) = 0; + }; +} diff --git a/src/render/gl/blur/Ripple.cpp b/src/render/gl/blur/Ripple.cpp new file mode 100644 index 000000000..e55ee0b15 --- /dev/null +++ b/src/render/gl/blur/Ripple.cpp @@ -0,0 +1,194 @@ +#include "Ripple.hpp" + +#include "Kawase.hpp" + +#include "../../Renderer.hpp" +#include "../../Shader.hpp" +#include "../../ShaderLoader.hpp" +#include "../../../config/ConfigValue.hpp" +#include "../../../event/EventBus.hpp" +#include "../../../pointer/PointerManager.hpp" +#include "../../../state/MonitorState.hpp" +#include "../../../managers/input/InputManager.hpp" + +#include +#include +#include +#include + +using namespace Render; +using namespace Render::GL; + +static constexpr float MAX_RIPPLE_DISPLACEMENT = 32.F; + +CRippleBlurMaterial::CRippleBlurMaterial() { + m_listeners.mouseButton = Event::bus()->m_events.input.mouse.button.listen([this](IPointer::SButtonEvent event, Event::SCallbackInfo&) { + m_lastMouseHeldCoord.reset(); + + if (event.state == WL_POINTER_BUTTON_STATE_PRESSED) { + m_mouseIsHeld = true; + return; + } + + if (event.state != WL_POINTER_BUTTON_STATE_RELEASED) + return; + + m_mouseIsHeld = false; + + static auto PRIPPLESTRENGTH = CConfigValue("decoration:blur:ripple:strength"); + static auto PRIPPLEDURATION = CConfigValue("decoration:blur:ripple:duration"); + static auto PBLURENABLED = CConfigValue("decoration:blur:enabled"); + + if (!*PBLURENABLED || *PRIPPLESTRENGTH <= 0.F || *PRIPPLEDURATION <= 0.F) + return; + + addImpulse(); + }); + + m_listeners.mouseMotion = Event::bus()->m_events.input.mouse.move.listen([this](Vector2D pos, Event::SCallbackInfo&) { + if (!m_mouseIsHeld) + return; + + static auto PRIPPLESTRENGTH = CConfigValue("decoration:blur:ripple:strength"); + static auto PRIPPLEDURATION = CConfigValue("decoration:blur:ripple:duration"); + static auto PBLURENABLED = CConfigValue("decoration:blur:enabled"); + + if (!*PBLURENABLED || *PRIPPLESTRENGTH <= 0.F || *PRIPPLEDURATION <= 0.F) + return; + + if (m_lastMouseHeldCoord) { + const auto Δ = (*m_lastMouseHeldCoord - g_pInputManager->getMouseCoordsInternal()).size(); + if (Δ < 6.9F) // arbitrarily chosen by me, fuck you + return; + } + + addImpulse(); + + m_lastMouseHeldCoord = g_pInputManager->getMouseCoordsInternal(); + }); +} + +CRippleBlurProvider::CRippleBlurProvider(CHyprOpenGLImpl& impl) : CDualKawaseBlurProvider(impl, makeUnique()) { + ; +} + +eBlurType CRippleBlurMaterial::type() const noexcept { + return eBlurType::BLUR_RIPPLE; +} + +void CRippleBlurMaterial::addImpulse() { + static auto PRIPPLESTRENGTH = CConfigValue("decoration:blur:ripple:strength"); + static auto PRIPPLERADIUS = CConfigValue("decoration:blur:ripple:radius"); + static auto PRIPPLEWIDTH = CConfigValue("decoration:blur:ripple:width"); + static auto PRIPPLEDURATION = CConfigValue("decoration:blur:ripple:duration"); + static auto PBLURENABLED = CConfigValue("decoration:blur:enabled"); + + const auto POS = g_pInputManager->getMouseCoordsInternal(); + const auto PMONITOR = State::monitorState()->query().vec(POS).run(); + if (!PMONITOR) + return; + + const auto NOW = Time::steadyNow(); + auto& impulse = m_impulses[m_nextImpulse]; + const auto AGE = std::chrono::duration(NOW - impulse.started).count(); + if (impulse.occupied && AGE >= 0.F && AGE < *PRIPPLEDURATION) + damageImpulse(impulse); + + impulse = SImpulse{ + .globalPosition = POS, + .started = NOW, + .monitor = PMONITOR, + .damageReach = rippleOutputReach(*PRIPPLERADIUS, *PRIPPLEWIDTH), + .occupied = true, + }; + + m_nextImpulse = (m_nextImpulse + 1) % MAX_IMPULSES; + damageImpulse(impulse); +} + +bool CRippleBlurMaterial::isAnimated() const noexcept { + static auto PRIPPLESTRENGTH = CConfigValue("decoration:blur:ripple:strength"); + static auto PRIPPLEDURATION = CConfigValue("decoration:blur:ripple:duration"); + static auto PBLURENABLED = CConfigValue("decoration:blur:enabled"); + + if (!*PBLURENABLED || *PRIPPLESTRENGTH <= 0.F || *PRIPPLEDURATION <= 0.F || !g_pHyprRenderer->m_renderData.pMonitor) + return false; + + const auto now = Time::steadyNow(); + return std::ranges::any_of(m_impulses, [&](const auto& impulse) { return impulseIsActive(impulse, g_pHyprRenderer->m_renderData.pMonitor, now, *PRIPPLEDURATION); }); +} + +SBlurMaterialRequirements CRippleBlurMaterial::requirements() const noexcept { + return { + .finishFragment = SH_FRAG_RIPPLEFINISH, + }; +} + +void CRippleBlurMaterial::bindFinish(WP shader, const SBlurMaterialContext& context) const { + static auto PRIPPLESTRENGTH = CConfigValue("decoration:blur:ripple:strength"); + static auto PRIPPLERADIUS = CConfigValue("decoration:blur:ripple:radius"); + static auto PRIPPLEWIDTH = CConfigValue("decoration:blur:ripple:width"); + static auto PRIPPLEDURATION = CConfigValue("decoration:blur:ripple:duration"); + + const auto monitor = g_pHyprRenderer->m_renderData.pMonitor; + const auto now = Time::steadyNow(); + const auto duration = std::max(*PRIPPLEDURATION, 0.001F); + + std::vector impulses; + impulses.reserve(MAX_IMPULSES * 4); + + for (const auto& impulse : m_impulses) { + if (!impulseIsActive(impulse, monitor, now, duration)) + continue; + + const auto position = (impulse.globalPosition - monitor->m_position) * monitor->m_scale; + + const auto age = std::chrono::duration(now - impulse.started).count(); + impulses.insert(impulses.end(), {sc(position.x), sc(position.y), age, 0.F}); + } + + const auto count = sc(impulses.size() / 4); + shader->setUniformInt(SHADER_RIPPLE_COUNT, count); + if (count > 0) + shader->setUniform4fv(SHADER_RIPPLE_IMPULSES, count, impulses); + + shader->setUniformFloat4(SHADER_RIPPLE_PARAMS, duration, std::max(*PRIPPLERADIUS, 1.F), std::max(*PRIPPLEWIDTH, 1.F), + std::clamp(*PRIPPLESTRENGTH, 0.F, MAX_RIPPLE_DISPLACEMENT) * std::clamp(context.strength, 0.F, 1.F)); +} + +float CRippleBlurMaterial::sampleRadius() const { + static auto PRIPPLESTRENGTH = CConfigValue("decoration:blur:ripple:strength"); + + return std::ceil(std::clamp(*PRIPPLESTRENGTH, 0.F, MAX_RIPPLE_DISPLACEMENT)); +} + +void CRippleBlurMaterial::damageImpulse(const SImpulse& impulse) const { + const auto monitor = impulse.monitor.lock(); + if (!monitor) + return; + + const auto local = (impulse.globalPosition - monitor->m_position) * monitor->m_scale; + const auto left = std::floor(local.x - impulse.damageReach); + const auto top = std::floor(local.y - impulse.damageReach); + const auto right = std::ceil(local.x + impulse.damageReach); + const auto bottom = std::ceil(local.y + impulse.damageReach); + + monitor->m_blurFBDirty = true; + monitor->addDamage(CBox{left, top, right - left, bottom - top}); +} + +bool CRippleBlurMaterial::impulseIsActive(const SImpulse& impulse, PHLMONITORREF monitor, const Time::steady_tp& now, float duration) const { + if (!impulse.occupied || !monitor || impulse.monitor != monitor) + return false; + + const auto age = std::chrono::duration(now - impulse.started).count(); + return age >= 0.F && age < duration; +} + +float Render::GL::rippleDamageRadius(int64_t size, int64_t passes, float displacement) { + return dualKawaseDamageRadius(size, passes) + std::ceil(std::clamp(displacement, 0.F, MAX_RIPPLE_DISPLACEMENT)); +} + +float Render::GL::rippleOutputReach(float radius, float width) { + return std::ceil(std::max(radius, 0.F) + std::max(width, 0.F)); +} diff --git a/src/render/gl/blur/Ripple.hpp b/src/render/gl/blur/Ripple.hpp new file mode 100644 index 000000000..7bd319a51 --- /dev/null +++ b/src/render/gl/blur/Ripple.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include "Kawase.hpp" + +#include "../../../helpers/signal/Signal.hpp" +#include "../../../helpers/time/Time.hpp" + +#include +#include + +namespace Render::GL { + class CRippleBlurMaterial final : public IGLBlurMaterial { + public: + CRippleBlurMaterial(); + + eBlurType type() const noexcept override; + SBlurMaterialRequirements requirements() const noexcept override; + bool isAnimated() const noexcept override; + float sampleRadius() const override; + void bindFinish(WP shader, const SBlurMaterialContext& context) const override; + + private: + static constexpr size_t MAX_IMPULSES = 256; + + struct SImpulse { + Vector2D globalPosition = {}; + Time::steady_tp started = {}; + PHLMONITORREF monitor; + float damageReach = 0.F; + bool occupied = false; + }; + + void damageImpulse(const SImpulse& impulse) const; + bool impulseIsActive(const SImpulse& impulse, PHLMONITORREF monitor, const Time::steady_tp& now, float duration) const; + void addImpulse(); + + std::array m_impulses; + size_t m_nextImpulse = 0; + bool m_mouseIsHeld = false; + std::optional m_lastMouseHeldCoord; + + struct { + CHyprSignalListener mouseButton; + CHyprSignalListener mouseMotion; + } m_listeners; + }; + + class CRippleBlurProvider final : public CDualKawaseBlurProvider { + public: + explicit CRippleBlurProvider(CHyprOpenGLImpl& impl); + }; + + float rippleDamageRadius(int64_t size, int64_t passes, float displacement); + float rippleOutputReach(float radius, float width); +} diff --git a/src/render/gl/blur/Water.cpp b/src/render/gl/blur/Water.cpp new file mode 100644 index 000000000..798e6afb8 --- /dev/null +++ b/src/render/gl/blur/Water.cpp @@ -0,0 +1,348 @@ +#include "Water.hpp" + +#include "Kawase.hpp" + +#include "../GLFramebuffer.hpp" +#include "../../OpenGL.hpp" +#include "../../Renderer.hpp" +#include "../../Shader.hpp" +#include "../../ShaderLoader.hpp" +#include "../../../config/ConfigValue.hpp" +#include "../../../desktop/state/ViewState.hpp" +#include "../../../desktop/view/window/Window.hpp" +#include "../../../event/EventBus.hpp" +#include "../../../managers/input/InputManager.hpp" +#include "../../../pointer/PointerManager.hpp" +#include "../../../state/MonitorState.hpp" + +#include +#include +#include +#include + +using namespace Render; +using namespace Render::GL; + +static constexpr float MAX_WATER_DISPLACEMENT = 32.F; +static constexpr float SIMULATION_SCALE = 0.25F; +static constexpr float MIN_SIMULATION_SIZE = 32.F; +static constexpr float MAX_SIMULATION_SIZE = 512.F; +static constexpr float WATER_FADE_DURATION = 2.F; +static constexpr size_t MAX_STORED_IMPULSES = 64; + +CWaterBlurMaterial::CWaterBlurMaterial(CHyprOpenGLImpl& impl) : m_impl(impl) { + m_listeners.mouseButton = Event::bus()->m_events.input.mouse.button.listen([this](IPointer::SButtonEvent event, Event::SCallbackInfo&) { + m_lastMouseHeldCoord.reset(); + + if (event.state == WL_POINTER_BUTTON_STATE_PRESSED) { + m_mouseHeld = true; + addImpulse(); + return; + } + + if (event.state == WL_POINTER_BUTTON_STATE_RELEASED) + m_mouseHeld = false; + }); + + m_listeners.mouseMotion = Event::bus()->m_events.input.mouse.move.listen([this](Vector2D position, Event::SCallbackInfo&) { + if (!m_mouseHeld) + return; + + if (m_lastMouseHeldCoord && (*m_lastMouseHeldCoord - position).size() < 6.9F) + return; + + addImpulse(); + m_lastMouseHeldCoord = position; + }); + + m_listeners.renderPre = Event::bus()->m_events.render.pre.listen([this](PHLMONITOR) { ++m_frame; }); + m_listeners.windowDestroy = + Event::bus()->m_events.window.destroy.listen([this](PHLWINDOWREF window) { std::erase_if(m_windowStates, [&](const auto& state) { return state.window == window; }); }); + m_listeners.config = Event::bus()->m_events.config.props_refreshed.listen([this](const bool) { + for (auto& state : m_windowStates) + state.reset = true; + for (auto& state : m_monitorStates) + state.reset = true; + }); +} + +CWaterBlurProvider::CWaterBlurProvider(CHyprOpenGLImpl& impl) : CDualKawaseBlurProvider(impl, makeUnique(impl)) { + ; +} + +eBlurType CWaterBlurMaterial::type() const noexcept { + return eBlurType::BLUR_WATER; +} + +bool CWaterBlurMaterial::isAnimated() const noexcept { + static auto PBLURENABLED = CConfigValue("decoration:blur:enabled"); + static auto PWATERSTRENGTH = CConfigValue("decoration:blur:water:strength"); + + if (!*PBLURENABLED || *PWATERSTRENGTH <= 0.F) + return false; + + pruneStates(); + + const auto monitor = g_pHyprRenderer->m_renderData.pMonitor; + if (!monitor) + return false; + + const auto now = Time::steadyNow(); + return std::ranges::any_of(m_windowStates, [&](const auto& state) { return state.monitor == monitor && stateIsActive(state, now); }) || + std::ranges::any_of(m_monitorStates, [&](const auto& state) { return state.monitor == monitor && stateIsActive(state, now); }); +} + +SBlurMaterialRequirements CWaterBlurMaterial::requirements() const noexcept { + return { + .finishFragment = SH_FRAG_WATERFINISH, + }; +} + +void CWaterBlurMaterial::prepare(const SBlurMaterialContext& context) { + pruneStates(); + + const auto state = stateForContext(context.blurContext, false); + if (!state || !g_pHyprRenderer->m_renderData.pMonitor) + return; + + const auto extent = transformedPatternBox(context.blurContext); + state->monitor = g_pHyprRenderer->m_renderData.pMonitor; + updateState(*state, extent); +} + +void CWaterBlurMaterial::bindFinish(WP shader, const SBlurMaterialContext& context) const { + static auto PWATERSTRENGTH = CConfigValue("decoration:blur:water:strength"); + + const auto state = stateForContext(context.blurContext); + if (!state || !state->buffers[state->currentBuffer]) { + shader->setUniformInt(SHADER_WATER_ENABLED, 0); + return; + } + + const auto extent = transformedPatternBox(context.blurContext); + if (extent.width <= 0 || extent.height <= 0) { + shader->setUniformInt(SHADER_WATER_ENABLED, 0); + return; + } + + glActiveTexture(GL_TEXTURE2); + const auto texture = state->buffers[state->currentBuffer]->getTexture(); + texture->bind(); + texture->setTexParameter(GL_TEXTURE_MIN_FILTER, GL_LINEAR); + texture->setTexParameter(GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glActiveTexture(GL_TEXTURE0); + + shader->setUniformInt(SHADER_WATER_ENABLED, 1); + shader->setUniformInt(SHADER_WATER_STATE_TEX, 2); + shader->setUniformFloat2(SHADER_WATER_TEXEL_SIZE, 1.F / state->simulationSize.x, 1.F / state->simulationSize.y); + shader->setUniformFloat4(SHADER_WATER_EXTENT, sc(extent.x), sc(extent.y), sc(extent.width), sc(extent.height)); + const auto secondsRemaining = std::chrono::duration(state->activeUntil - Time::steadyNow()).count(); + const auto fade = std::clamp(secondsRemaining / WATER_FADE_DURATION, 0.F, 1.F); + shader->setUniformFloat(SHADER_WATER_REFRACTION, std::clamp(*PWATERSTRENGTH, 0.F, MAX_WATER_DISPLACEMENT) * std::clamp(context.strength, 0.F, 1.F) * fade); +} + +float CWaterBlurMaterial::sampleRadius() const { + static auto PWATERSTRENGTH = CConfigValue("decoration:blur:water:strength"); + + return std::ceil(std::clamp(*PWATERSTRENGTH, 0.F, MAX_WATER_DISPLACEMENT)); +} + +CWaterBlurMaterial::SState* CWaterBlurMaterial::stateForContext(const SBlurContext& context, bool create) { + if (!context.owner.expired()) + return windowState(context.owner, create); + + return monitorState(g_pHyprRenderer->m_renderData.pMonitor, create); +} + +const CWaterBlurMaterial::SState* CWaterBlurMaterial::stateForContext(const SBlurContext& context) const { + if (!context.owner.expired()) { + const auto state = std::ranges::find_if(m_windowStates, [&](const auto& candidate) { return candidate.window == context.owner; }); + return state != m_windowStates.end() ? &*state : nullptr; + } + + const auto monitor = g_pHyprRenderer->m_renderData.pMonitor; + const auto state = std::ranges::find_if(m_monitorStates, [&](const auto& candidate) { return candidate.monitor == monitor; }); + return state != m_monitorStates.end() ? &*state : nullptr; +} + +CWaterBlurMaterial::SState* CWaterBlurMaterial::windowState(PHLWINDOWREF window, bool create) { + const auto state = std::ranges::find_if(m_windowStates, [&](const auto& candidate) { return candidate.window == window; }); + if (state != m_windowStates.end()) + return &*state; + + if (!create) + return nullptr; + + return &m_windowStates.emplace_back(SState{.window = window}); +} + +CWaterBlurMaterial::SState* CWaterBlurMaterial::monitorState(PHLMONITORREF monitor, bool create) { + const auto state = std::ranges::find_if(m_monitorStates, [&](const auto& candidate) { return candidate.monitor == monitor; }); + if (state != m_monitorStates.end()) + return &*state; + + if (!create) + return nullptr; + + return &m_monitorStates.emplace_back(SState{.monitor = monitor}); +} + +void CWaterBlurMaterial::addImpulse() { + static auto PBLURENABLED = CConfigValue("decoration:blur:enabled"); + static auto PWATERSTRENGTH = CConfigValue("decoration:blur:water:strength"); + static auto PWATERRADIUS = CConfigValue("decoration:blur:water:radius"); + + if (!*PBLURENABLED || *PWATERSTRENGTH <= 0.F) + return; + + const auto position = g_pInputManager->getMouseCoordsInternal(); + const auto monitor = State::monitorState()->query().vec(position).run(); + if (!monitor) + return; + + const auto localPosition = (position - monitor->m_position) * monitor->m_scale; + const auto monitorPosition = Vector2D{localPosition.x / monitor->m_transformedSize.x, localPosition.y / monitor->m_transformedSize.y}; + const auto amplitude = std::clamp(*PWATERSTRENGTH / MAX_WATER_DISPLACEMENT, 0.F, 1.F) * 0.5F; + + queueImpulse(*monitorState(monitor, true), monitorPosition, *PWATERRADIUS, amplitude); + + static auto PBLURSIZE = CConfigValue("decoration:blur:size"); + static auto PBLURPASSES = CConfigValue("decoration:blur:passes"); + + const auto reach = *PWATERRADIUS + waterDamageRadius(*PBLURSIZE, *PBLURPASSES, *PWATERSTRENGTH); + monitor->m_blurFBDirty = true; + monitor->addDamage(CBox{ + std::floor(localPosition.x - reach), + std::floor(localPosition.y - reach), + std::ceil(reach * 2.F), + std::ceil(reach * 2.F), + }); + + const auto window = Desktop::viewState()->hitTest().windowAt(position, Desktop::View::RESERVED_EXTENTS | Desktop::View::INPUT_EXTENTS | Desktop::View::ALLOW_FLOATING); + if (!window) + return; + + const auto box = window->logicalBox(); + if (!box || box->width <= 0 || box->height <= 0) + return; + + const auto windowPosition = Vector2D{(position.x - box->x) / box->width, (position.y - box->y) / box->height}; + const auto state = windowState(window, true); + state->monitor = monitor; + queueImpulse(*state, windowPosition, *PWATERRADIUS, amplitude); +} + +void CWaterBlurMaterial::queueImpulse(SState& state, Vector2D position, float radius, float amplitude) { + static auto PWATERDURATION = CConfigValue("decoration:blur:water:duration"); + + position.x = std::clamp(position.x, 0.0, 1.0); + position.y = std::clamp(position.y, 0.0, 1.0); + + state.impulses.push_back({.position = position, .radius = std::max(radius, 1.F), .amplitude = amplitude}); + if (state.impulses.size() > MAX_STORED_IMPULSES) + state.impulses.erase(state.impulses.begin()); + + const auto duration = std::clamp(*PWATERDURATION, 0.5F, 60.F); + state.activeUntil = Time::steadyNow() + std::chrono::duration_cast(std::chrono::duration(duration)); +} + +void CWaterBlurMaterial::updateState(SState& state, const CBox& extent) { + const auto now = Time::steadyNow(); + if (!stateIsActive(state, now) || state.lastFrame == m_frame) + return; + + const auto simulationSize = Vector2D{ + std::clamp(std::ceil(extent.width * SIMULATION_SCALE), sc(MIN_SIMULATION_SIZE), sc(MAX_SIMULATION_SIZE)), + std::clamp(std::ceil(extent.height * SIMULATION_SCALE), sc(MIN_SIMULATION_SIZE), sc(MAX_SIMULATION_SIZE)), + }; + + if (state.reset || state.simulationSize != simulationSize) + resetState(state, simulationSize); + + const auto dt = state.lastUpdate == Time::steady_tp{} ? 1.F / 60.F : std::clamp(sc(std::chrono::duration(now - state.lastUpdate).count()), 0.F, 1.F / 20.F); + drawStateStep(state, dt, extent); + state.lastUpdate = now; + state.lastFrame = m_frame; +} + +void CWaterBlurMaterial::resetState(SState& state, const Vector2D& simulationSize) { + for (auto& buffer : state.buffers) { + if (!buffer) + buffer = dynamicPointerCast(g_pHyprRenderer->createFB("Water simulation")); + + buffer->alloc(sc(simulationSize.x), sc(simulationSize.y), DRM_FORMAT_ARGB8888); + buffer->bind(); + g_pHyprRenderer->disableScissor(); + glClearColor(0.5F, 0.5F, 0.F, 1.F); + glClear(GL_COLOR_BUFFER_BIT); + } + + state.simulationSize = simulationSize; + state.currentBuffer = 0; + state.lastUpdate = {}; + state.reset = false; +} + +void CWaterBlurMaterial::drawStateStep(SState& state, float dt, const CBox& extent) { + static auto PWATERSPEED = CConfigValue("decoration:blur:water:speed"); + static auto PWATERDAMPING = CConfigValue("decoration:blur:water:damping"); + + const auto source = state.buffers[state.currentBuffer]; + const auto target = state.buffers[1 - state.currentBuffer]; + target->bind(); + g_pHyprRenderer->setViewport(0, 0, sc(state.simulationSize.x), sc(state.simulationSize.y)); + g_pHyprRenderer->disableScissor(); + + glActiveTexture(GL_TEXTURE0); + const auto texture = source->getTexture(); + texture->bind(); + texture->setTexParameter(GL_TEXTURE_MIN_FILTER, GL_NEAREST); + texture->setTexParameter(GL_TEXTURE_MAG_FILTER, GL_NEAREST); + + const auto monitor = g_pHyprRenderer->m_renderData.pMonitor; + const auto matrix = g_pHyprRenderer->projectBoxToTarget({0, 0, monitor->m_transformedSize.x, monitor->m_transformedSize.y}); + const auto shader = m_impl.useShader(m_impl.getShaderVariant(SH_FRAG_WATERSTEP)); + shader->setUniformMatrix3fv(SHADER_PROJ, 1, GL_TRUE, matrix.getMatrix()); + shader->setUniformInt(SHADER_WATER_STATE_TEX, 0); + shader->setUniformFloat2(SHADER_WATER_TEXEL_SIZE, 1.F / state.simulationSize.x, 1.F / state.simulationSize.y); + shader->setUniformFloat4(SHADER_WATER_PARAMS, dt, std::clamp(*PWATERSPEED, 0.F, 10.F), std::clamp(*PWATERDAMPING, 0.F, 1.F), 0.F); + + std::vector impulses; + impulses.reserve(std::min(state.impulses.size(), MAX_IMPULSES) * 4); + const auto scale = std::max(std::min(sc(extent.width), sc(extent.height)), 1.F); + for (const auto& impulse : state.impulses | std::views::take(MAX_IMPULSES)) + impulses.insert(impulses.end(), {sc(impulse.position.x), sc(impulse.position.y), impulse.radius / scale, impulse.amplitude}); + + shader->setUniformInt(SHADER_WATER_IMPULSE_COUNT, sc(impulses.size() / 4)); + if (!impulses.empty()) + shader->setUniform4fv(SHADER_WATER_IMPULSES, sc(impulses.size() / 4), impulses); + + glBindVertexArray(shader->getUniformLocation(SHADER_SHADER_VAO)); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + glBindVertexArray(0); + + state.impulses.clear(); + state.currentBuffer = 1 - state.currentBuffer; +} + +CBox CWaterBlurMaterial::transformedPatternBox(const SBlurContext& context) const { + const auto monitor = g_pHyprRenderer->m_renderData.pMonitor; + if (!monitor) + return {}; + + return context.patternBox.value_or(CBox{0, 0, monitor->m_transformedSize.x, monitor->m_transformedSize.y}); +} + +bool CWaterBlurMaterial::stateIsActive(const SState& state, const Time::steady_tp& now) const { + return !state.impulses.empty() || (state.activeUntil != Time::steady_tp{} && now < state.activeUntil); +} + +void CWaterBlurMaterial::pruneStates() const { + const auto now = Time::steadyNow(); + std::erase_if(m_windowStates, [&](const auto& state) { return state.window.expired() || !state.window->shouldBlur() || !stateIsActive(state, now); }); + std::erase_if(m_monitorStates, [&](const auto& state) { return state.monitor.expired() || !stateIsActive(state, now); }); +} + +float Render::GL::waterDamageRadius(int64_t size, int64_t passes, float displacement) { + return dualKawaseDamageRadius(size, passes) + std::ceil(std::clamp(displacement, 0.F, MAX_WATER_DISPLACEMENT)); +} diff --git a/src/render/gl/blur/Water.hpp b/src/render/gl/blur/Water.hpp new file mode 100644 index 000000000..dcb17d10b --- /dev/null +++ b/src/render/gl/blur/Water.hpp @@ -0,0 +1,81 @@ +#pragma once + +#include "Kawase.hpp" + +#include "../../../helpers/signal/Signal.hpp" +#include "../../../helpers/time/Time.hpp" + +#include + +namespace Render::GL { + class CGLFramebuffer; + + class CWaterBlurMaterial final : public IGLBlurMaterial { + public: + explicit CWaterBlurMaterial(CHyprOpenGLImpl& impl); + + eBlurType type() const noexcept override; + SBlurMaterialRequirements requirements() const noexcept override; + bool isAnimated() const noexcept override; + float sampleRadius() const override; + void prepare(const SBlurMaterialContext& context) override; + void bindFinish(WP shader, const SBlurMaterialContext& context) const override; + + private: + static constexpr size_t MAX_IMPULSES = 16; + + struct SImpulse { + Vector2D position = {}; + float radius = 0.F; + float amplitude = 0.F; + }; + + struct SState { + PHLWINDOWREF window; + PHLMONITORREF monitor; + SP buffers[2]; + Vector2D simulationSize = {}; + std::vector impulses; + Time::steady_tp lastUpdate = {}; + Time::steady_tp activeUntil = {}; + uint64_t lastFrame = 0; + uint8_t currentBuffer = 0; + bool reset = true; + }; + + SState* stateForContext(const SBlurContext& context, bool create); + const SState* stateForContext(const SBlurContext& context) const; + SState* windowState(PHLWINDOWREF window, bool create); + SState* monitorState(PHLMONITORREF monitor, bool create); + void addImpulse(); + void queueImpulse(SState& state, Vector2D position, float radius, float amplitude); + void updateState(SState& state, const CBox& extent); + void resetState(SState& state, const Vector2D& simulationSize); + void drawStateStep(SState& state, float dt, const CBox& extent); + CBox transformedPatternBox(const SBlurContext& context) const; + bool stateIsActive(const SState& state, const Time::steady_tp& now) const; + void pruneStates() const; + + CHyprOpenGLImpl& m_impl; + mutable std::vector m_windowStates; + mutable std::vector m_monitorStates; + uint64_t m_frame = 0; + bool m_mouseHeld = false; + std::optional m_lastMouseHeldCoord; + + struct { + CHyprSignalListener mouseButton; + CHyprSignalListener mouseMotion; + CHyprSignalListener renderPre; + CHyprSignalListener windowDestroy; + CHyprSignalListener config; + } m_listeners; + }; + + class CWaterBlurProvider final : public CDualKawaseBlurProvider { + public: + explicit CWaterBlurProvider(CHyprOpenGLImpl& impl); + }; + + float waterDamageRadius(int64_t size, int64_t passes, float displacement); +} diff --git a/src/render/pass/BackdropScopePassElement.cpp b/src/render/pass/BackdropScopePassElement.cpp new file mode 100644 index 000000000..3af05d2f4 --- /dev/null +++ b/src/render/pass/BackdropScopePassElement.cpp @@ -0,0 +1,72 @@ +#include "BackdropScopePassElement.hpp" +#include "../Renderer.hpp" + +void CBackdropScopePlanner::begin(SP scope) { + RASSERT(scope, "Cannot plan a null backdrop scope"); + scope->required = false; + scope->damage.clear(); + m_scopes.emplace_back(std::move(scope)); +} + +void CBackdropScopePlanner::addLiveBlur(const CRegion& damage) { + if (m_scopes.empty()) + return; + + m_scopes.back()->required = true; + m_scopes.back()->damage.add(damage); +} + +void CBackdropScopePlanner::end(SP scope, const CBox& bounds) { + RASSERT(!m_scopes.empty() && m_scopes.back() == scope, "Unbalanced backdrop scope markers"); + if (scope->required) + scope->damage.intersect(bounds); + m_scopes.pop_back(); +} + +bool CBackdropScopePlanner::empty() const { + return m_scopes.empty(); +} + +CBackdropScopePassElement::CBackdropScopePassElement(eAction action, SP scope) : m_action(action), m_scope(std::move(scope)) { + ; +} + +std::vector> CBackdropScopePassElement::draw() { + if (!m_scope->required) + return {}; + + if (m_action == eAction::BEGIN) + g_pHyprRenderer->beginBackdropScope(m_scope); + else + g_pHyprRenderer->endBackdropScope(m_scope); + + return {}; +} + +bool CBackdropScopePassElement::needsLiveBlur() { + return false; +} + +bool CBackdropScopePassElement::needsPrecomputeBlur() { + return false; +} + +bool CBackdropScopePassElement::undiscardable() { + return true; +} + +const char* CBackdropScopePassElement::passName() { + return "CBackdropScopePassElement"; +} + +ePassElementType CBackdropScopePassElement::type() { + return EK_BACKDROP_SCOPE; +} + +CBackdropScopePassElement::eAction CBackdropScopePassElement::action() const { + return m_action; +} + +SP CBackdropScopePassElement::scope() const { + return m_scope; +} diff --git a/src/render/pass/BackdropScopePassElement.hpp b/src/render/pass/BackdropScopePassElement.hpp new file mode 100644 index 000000000..86cb76082 --- /dev/null +++ b/src/render/pass/BackdropScopePassElement.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include "PassElement.hpp" + +struct SBackdropScope { + bool required = false; + CRegion damage; +}; + +class CBackdropScopePlanner { + public: + void begin(SP scope); + void addLiveBlur(const CRegion& damage); + void end(SP scope, const CBox& bounds); + bool empty() const; + + private: + std::vector> m_scopes; +}; + +class CBackdropScopePassElement : public IPassElement { + public: + enum class eAction : uint8_t { + BEGIN = 0, + END, + }; + + CBackdropScopePassElement(eAction action, SP scope); + virtual ~CBackdropScopePassElement() = default; + + virtual std::vector> draw(); + virtual bool needsLiveBlur(); + virtual bool needsPrecomputeBlur(); + virtual bool undiscardable(); + + virtual const char* passName(); + virtual ePassElementType type(); + + eAction action() const; + SP scope() const; + + private: + eAction m_action = eAction::BEGIN; + SP m_scope; +}; diff --git a/src/render/pass/Pass.cpp b/src/render/pass/Pass.cpp index a14c1ff16..fc0760e00 100644 --- a/src/render/pass/Pass.cpp +++ b/src/render/pass/Pass.cpp @@ -12,6 +12,7 @@ #include "../../protocols/core/Compositor.hpp" #include "../../state/MonitorState.hpp" #include "RectPassElement.hpp" +#include "BackdropScopePassElement.hpp" #include "macros.hpp" using namespace Render; @@ -24,6 +25,14 @@ bool CRenderPass::single() const { return m_passElements.size() == 1; } +bool CRenderPass::needsLiveBlur() { + return std::ranges::any_of(m_passElements, [](const auto& el) { return el.element->needsLiveBlur(); }); +} + +bool CRenderPass::needsPrecomputeBlur() { + return std::ranges::any_of(m_passElements, [](const auto& el) { return el.element->needsPrecomputeBlur(); }); +} + void CRenderPass::add(UP&& el) { m_passElements.emplace_back(SPassElementData{.element = std::move(el)}); } @@ -104,6 +113,30 @@ void CRenderPass::clear() { m_passElements.clear(); } +void CRenderPass::planBackdropScopes() { + CBackdropScopePlanner planner; + const CBox bounds = {{}, g_pHyprRenderer->m_renderData.pMonitor->m_transformedSize}; + + for (auto& el : m_passElements) { + if (el.element->type() != EK_BACKDROP_SCOPE) { + if (!el.discard && el.element->needsLiveBlurCached) + planner.addLiveBlur(el.elementDamage); + continue; + } + + const auto marker = sc(el.element.get()); + const auto scope = marker->scope(); + RASSERT(scope, "Backdrop scope marker has no scope"); + + if (marker->action() == CBackdropScopePassElement::eAction::BEGIN) + planner.begin(scope); + else + planner.end(scope, bounds); + } + + RASSERT(planner.empty(), "Unclosed backdrop scope marker"); +} + CRegion CRenderPass::render(const CRegion& damage_) { const auto pMonitor = g_pHyprRenderer->m_renderData.pMonitor; static auto PDEBUGPASS = CConfigValue("debug:pass"); @@ -156,16 +189,18 @@ CRegion CRenderPass::render(const CRegion& damage_) { blurRegion.scale(pMonitor->m_scale); // save a copy for simplify's occlusion test before we mutate for damage expansion - liveBlurRegion = blurRegion.copy().expand(oneBlurRadius() * 2.F); + liveBlurRegion = blurRegion.copy(); + g_pHyprRenderer->expandBlurDamage(liveBlurRegion, 2.F); - blurRegion.intersect(m_damage).expand(oneBlurRadius()); + blurRegion.intersect(m_damage); + g_pHyprRenderer->expandBlurDamage(blurRegion); g_pHyprRenderer->m_renderData.finalDamage = blurRegion.copy().add(m_damage); // FIXME: why does this break on * 1.F ? // used to work when we expand all the damage... I think? Well, before pass. // moving a window over blur shows the edges being wonk. - blurRegion.expand(oneBlurRadius() * 1.5F); + g_pHyprRenderer->expandBlurDamage(blurRegion, 1.5F); m_damage = blurRegion.copy().add(m_damage); } else @@ -178,12 +213,18 @@ CRegion CRenderPass::render(const CRegion& damage_) { } else simplify(willBlur, liveBlurRegion); + planBackdropScopes(); + if (g_pHyprRenderer->m_renderData.pMonitor) g_pHyprRenderer->m_renderData.pMonitor->m_blurFBShouldRender = willPrecomputeBlur; if (m_passElements.empty()) return {}; + const bool providerIsAnimated = g_pHyprRenderer->blurProviderIsAnimated(); + CRegion animatedBlurDamage; + bool usesPrecomputedBlur = false; + for (auto& el : m_passElements) { if (el.discard) { el.element->discard(); @@ -192,8 +233,25 @@ CRegion CRenderPass::render(const CRegion& damage_) { g_pHyprRenderer->m_renderData.damage = el.elementDamage; g_pHyprRenderer->draw(el.element, el.elementDamage); + + if (!providerIsAnimated || (!el.element->needsLiveBlurCached && !el.element->needsPrecomputeBlurCached)) + continue; + + const auto BB = el.element->boundingBox(); + if (!BB) + animatedBlurDamage.add(CBox{{}, pMonitor->m_transformedSize}); + else { + auto box = BB->copy().scale(pMonitor->m_scale); + g_pHyprRenderer->m_renderData.renderModif.applyToBox(box); + animatedBlurDamage.add(box); + } + + usesPrecomputedBlur = usesPrecomputedBlur || el.element->needsPrecomputeBlurCached; } + animatedBlurDamage.intersect(CBox{{}, pMonitor->m_transformedSize}); + g_pHyprRenderer->scheduleFrameForAnimatedBlur(animatedBlurDamage, usesPrecomputedBlur); + if (*PDEBUGPASS) { renderDebugData(); g_pEventLoopManager->doLater([] { @@ -310,16 +368,6 @@ void CRenderPass::renderDebugData() { m_damage); } -float CRenderPass::oneBlurRadius() { - // TODO: is this exact range correct? - static auto PBLURSIZE = CConfigValue("decoration:blur:size"); - static auto PBLURPASSES = CConfigValue("decoration:blur:passes"); - - const auto BLUR_PASSES = std::clamp(*PBLURPASSES, sc(1), sc(8)); - - return std::clamp(*PBLURSIZE, sc(1), sc(40)) * pow(2, BLUR_PASSES); // is this 2^pass? I don't know but it works... I think. -} - void CRenderPass::removeAllOfType(const std::string& type) { std::erase_if(m_passElements, [&type](const auto& e) { return e.element->passName() == type; }); } diff --git a/src/render/pass/Pass.hpp b/src/render/pass/Pass.hpp index 20d9d7570..34c75bbaf 100644 --- a/src/render/pass/Pass.hpp +++ b/src/render/pass/Pass.hpp @@ -12,6 +12,8 @@ namespace Render { public: bool empty() const; bool single() const; + bool needsLiveBlur(); + bool needsPrecomputeBlur(); void add(UP&& elem); void clear(); @@ -33,14 +35,12 @@ namespace Render { std::vector m_passElements; void simplify(bool willBlur, const CRegion& liveBlurRegion); - float oneBlurRadius(); + void planBackdropScopes(); void renderDebugData(); struct { bool present = false; SP keyboardFocusText, pointerFocusText, lastWindowText; } m_debugData; - - friend class CHyprOpenGLImpl; }; } diff --git a/src/render/pass/PassElement.hpp b/src/render/pass/PassElement.hpp index df6641ff7..f9a859cb9 100644 --- a/src/render/pass/PassElement.hpp +++ b/src/render/pass/PassElement.hpp @@ -18,6 +18,7 @@ enum ePassElementType : uint8_t { EK_INNER_GLOW, EK_TRANSFORMED_WINDOW, EK_CUSTOM, + EK_BACKDROP_SCOPE, }; class IPassElement { diff --git a/src/render/pass/RectPassElement.cpp b/src/render/pass/RectPassElement.cpp index 042dfd2a4..1f3a83b06 100644 --- a/src/render/pass/RectPassElement.cpp +++ b/src/render/pass/RectPassElement.cpp @@ -6,11 +6,11 @@ CRectPassElement::CRectPassElement(const CRectPassElement::SRectData& data_) : m } bool CRectPassElement::needsLiveBlur() { - return m_data.color.a < 1.F && !m_data.xray && m_data.blur; + return m_data.color.a < 1.F && m_data.blur && (!m_data.xray || g_pHyprRenderer->blurProviderRequiresLiveBlur()); } bool CRectPassElement::needsPrecomputeBlur() { - return m_data.color.a < 1.F && m_data.xray && m_data.blur; + return m_data.color.a < 1.F && m_data.xray && m_data.blur && !g_pHyprRenderer->blurProviderRequiresLiveBlur(); } std::optional CRectPassElement::boundingBox() { diff --git a/src/render/pass/RectPassElement.hpp b/src/render/pass/RectPassElement.hpp index a61ab1558..5bf8f53c7 100644 --- a/src/render/pass/RectPassElement.hpp +++ b/src/render/pass/RectPassElement.hpp @@ -1,17 +1,20 @@ #pragma once #include "PassElement.hpp" #include +#include class CRectPassElement : public IPassElement { public: struct SRectData { - CBox box; - CHyprColor color; - int round = 0; - float roundingPower = 2.0f; - bool blur = false, xray = false; - float blurA = 1.F; - CBox clipBox; + CBox box; + CHyprColor color; + int round = 0; + float roundingPower = 2.0f; + bool blur = false, xray = false; + float blurA = 1.F; + std::optional blurPatternBox; + PHLWINDOWREF blurOwner; + CBox clipBox; // internal CBox modifiedBox; diff --git a/src/render/pass/TexPassElement.cpp b/src/render/pass/TexPassElement.cpp index ede4df818..398fddb3e 100644 --- a/src/render/pass/TexPassElement.cpp +++ b/src/render/pass/TexPassElement.cpp @@ -18,11 +18,25 @@ CTexPassElement::CTexPassElement(CTexPassElement::SRenderData&& data) : m_data(s } bool CTexPassElement::needsLiveBlur() { - return false; // TODO? + return usesLiveBlur(); } bool CTexPassElement::needsPrecomputeBlur() { - return false; // TODO? + return m_data.blur && !usesLiveBlur(); +} + +bool CTexPassElement::usesLiveBlur() { + if (m_usesLiveBlur.has_value()) + return *m_usesLiveBlur; + + if (m_data.liveBlurOverride.has_value()) { + m_usesLiveBlur = m_data.blur && *m_data.liveBlurOverride; + return *m_usesLiveBlur; + } + + m_usesLiveBlur = + m_data.blur && (m_data.blockBlurOptimization.value_or(false) || !g_pHyprRenderer->shouldUseNewBlurOptimizations(m_data.currentLS.lock(), m_data.blurOwner.lock())); + return *m_usesLiveBlur; } std::optional CTexPassElement::boundingBox() { diff --git a/src/render/pass/TexPassElement.hpp b/src/render/pass/TexPassElement.hpp index 7c2fb5288..49d7c9a6d 100644 --- a/src/render/pass/TexPassElement.hpp +++ b/src/render/pass/TexPassElement.hpp @@ -45,8 +45,10 @@ class CTexPassElement : public IPassElement { CBox clipBox; bool blur = false; bool forceBlurBlend = false; + std::optional blurPatternBox; std::optional ignoreAlpha; std::optional blockBlurOptimization; + std::optional liveBlurOverride; bool cmBackToSRGB = false; bool discardActive = false; @@ -61,10 +63,12 @@ class CTexPassElement : public IPassElement { CRegion clipRegion; PHLLSREF currentLS; + PHLWINDOWREF blurOwner; SP blurredBG; SP blurAlphaMatte; SMotionBlurData motionBlur; + bool blurShapeInvalid = false; }; CTexPassElement(const SRenderData& data); @@ -77,6 +81,8 @@ class CTexPassElement : public IPassElement { virtual CRegion opaqueRegion(); virtual void discard(); + bool usesLiveBlur(); + virtual const char* passName() { return "CTexPassElement"; } @@ -86,4 +92,7 @@ class CTexPassElement : public IPassElement { }; SRenderData m_data; + + private: + std::optional m_usesLiveBlur; }; diff --git a/src/render/pass/TransformedWindowPassElement.cpp b/src/render/pass/TransformedWindowPassElement.cpp index 75e40def2..d246bd773 100644 --- a/src/render/pass/TransformedWindowPassElement.cpp +++ b/src/render/pass/TransformedWindowPassElement.cpp @@ -5,11 +5,11 @@ CTransformedWindowPassElement::CTransformedWindowPassElement(CTransformedWindowP } bool CTransformedWindowPassElement::needsLiveBlur() { - return m_data.blur; + return (m_data.blur && m_data.blurUsesLive) || (m_data.pass && m_data.pass->needsLiveBlur()); } bool CTransformedWindowPassElement::needsPrecomputeBlur() { - return false; + return (m_data.blur && !m_data.blurUsesLive) || (m_data.pass && m_data.pass->needsPrecomputeBlur()); } std::optional CTransformedWindowPassElement::boundingBox() { diff --git a/src/render/pass/TransformedWindowPassElement.hpp b/src/render/pass/TransformedWindowPassElement.hpp index 476291d35..497855696 100644 --- a/src/render/pass/TransformedWindowPassElement.hpp +++ b/src/render/pass/TransformedWindowPassElement.hpp @@ -11,6 +11,7 @@ class CTransformedWindowPassElement : public IPassElement { CBox currentBox; CBox blurBox; bool blur = false; + bool blurUsesLive = false; float blurA = 1.F; int blurRound = 0; float blurRoundingPower = 2.F; diff --git a/src/render/shaders/glsl/acrylicfinish.frag b/src/render/shaders/glsl/acrylicfinish.frag new file mode 100644 index 000000000..f0f8ffc47 --- /dev/null +++ b/src/render/shaders/glsl/acrylicfinish.frag @@ -0,0 +1,232 @@ +#version 300 es +#define ALLOW_INCLUDES +#extension GL_ARB_shading_language_include : enable + +precision highp float; + +in vec2 v_texcoord; +uniform sampler2D tex; +uniform sampler2D sharpTex; + +uniform int acrylicEnabled; +uniform vec4 acrylicExtent; +uniform float acrylicRadius; +uniform float acrylicRoundingPower; +uniform float acrylicRefraction; +uniform float acrylicBulb; +uniform float acrylicClarity; +uniform float acrylicAberration; +uniform vec4 acrylicTint; +uniform float acrylicStrength; +uniform int acrylicTransferFunction; +uniform float acrylicLuminanceScale; +uniform float noise; +uniform float brightness; + +#include "defines.h" +#if USE_CM +uniform int sourceTF; +uniform int targetTF; +#include "CM.glsl" +#endif + +#include "cm_helpers.glsl" +#include "blurFinish.glsl" + +layout(location = 0) out vec4 fragColor; + +float acrylicLength(vec2 value, float power) { + value = abs(value); + return pow(pow(value.x, power) + pow(value.y, power), 1.0 / power); +} + +float roundedBoxSDF(vec2 position, vec2 halfSize, float radius, float power) { + radius = clamp(radius, 0.0, min(halfSize.x, halfSize.y)); + vec2 offset = abs(position) - (halfSize - vec2(radius)); + vec2 outside = max(offset, vec2(0.0)); + float cornerDistance = (outside.x > 0.0 || outside.y > 0.0) ? acrylicLength(outside, power) : 0.0; + return cornerDistance + min(max(offset.x, offset.y), 0.0) - radius; +} + +float smootherstep(float edge0, float edge1, float value) { + float progress = clamp((value - edge0) / max(edge1 - edge0, 0.0001), 0.0, 1.0); + return progress * progress * progress * (progress * (progress * 6.0 - 15.0) + 10.0); +} + +float nestedRoundedBoxSDF(vec2 position, vec2 halfSize, float radius, float power, float inset) { + vec2 nestedHalfSize = max(halfSize - vec2(inset), vec2(0.001)); + float nestedRadius = clamp(radius, 0.0, min(nestedHalfSize.x, nestedHalfSize.y)); + return roundedBoxSDF(position, nestedHalfSize, nestedRadius, power); +} + +float roundedProfileDepth(vec2 position, vec2 halfSize, float radius, float power, float width, float boundarySdf) { + if (boundarySdf >= 0.0) + return 0.0; + + if (nestedRoundedBoxSDF(position, halfSize, radius, power, width) <= 0.0) + return width; + + float lower = 0.0; + float upper = width; + for (int i = 0; i < 8; ++i) { + float middle = (lower + upper) * 0.5; + if (nestedRoundedBoxSDF(position, halfSize, radius, power, middle) <= 0.0) + lower = middle; + else + upper = middle; + } + + return (lower + upper) * 0.5; +} + +vec2 roundedProfileNormal(vec2 position, vec2 halfSize, float radius, float power, float inset) { + vec2 nestedHalfSize = max(halfSize - vec2(inset), vec2(0.001)); + float nestedRadius = clamp(radius, 0.0, min(nestedHalfSize.x, nestedHalfSize.y)); + vec2 corner = max(abs(position) - (nestedHalfSize - vec2(nestedRadius)), vec2(0.0)); + vec2 gradient; + + if (corner.x > 0.0001 || corner.y > 0.0001) { + if (power <= 1.0001) + gradient = vec2(corner.x > 0.0001 ? 1.0 : 0.0, corner.y > 0.0001 ? 1.0 : 0.0); + else + gradient = pow(corner, vec2(power - 1.0)); + } else { + vec2 edgeDistance = nestedHalfSize - abs(position); + gradient = edgeDistance.x < edgeDistance.y ? vec2(1.0, 0.0) : vec2(0.0, 1.0); + } + + gradient *= sign(position); + return gradient / max(length(gradient), 0.0001); +} + +float availableTextureTravel(vec2 origin, vec2 direction, vec2 minimumUV, vec2 maximumUV) { + float travel = 1e10; + + if (direction.x > 0.000001) + travel = min(travel, (maximumUV.x - origin.x) / direction.x); + else if (direction.x < -0.000001) + travel = min(travel, (minimumUV.x - origin.x) / direction.x); + + if (direction.y > 0.000001) + travel = min(travel, (maximumUV.y - origin.y) / direction.y); + else if (direction.y < -0.000001) + travel = min(travel, (minimumUV.y - origin.y) / direction.y); + + return max(travel, 0.0); +} + +vec4 applyAcrylic(vec4 blurred) { + float effect = clamp(acrylicStrength, 0.0, 1.0); + if (effect <= 0.0) + return blurred; + + vec2 halfSize = acrylicExtent.zw * 0.5; + vec2 center = acrylicExtent.xy + halfSize; + vec2 position = gl_FragCoord.xy - center; + float power = clamp(acrylicRoundingPower, 1.0, 10.0); + float minimumHalfSize = min(halfSize.x, halfSize.y); + float boundaryRadius = clamp(acrylicRadius, 0.0, minimumHalfSize); + float maximumBulb = max(minimumHalfSize * 0.8, 1.0); + float bulbWidth = clamp(acrylicBulb, 1.0, maximumBulb); + + float boundarySdf = roundedBoxSDF(position, halfSize, boundaryRadius, power); + float antialias = max(fwidth(boundarySdf), 0.75); + float shape = 1.0 - smoothstep(-antialias, antialias, boundarySdf); + vec2 texcoordDx = dFdx(v_texcoord); + vec2 texcoordDy = dFdy(v_texcoord); + if (shape <= 0.0) + return blurred; + + float opticalDepth = roundedProfileDepth(position, halfSize, boundaryRadius, power, bulbWidth, boundarySdf); + float progress = clamp(opticalDepth / bulbWidth, 0.0, 1.0); + float clarityCore = smootherstep(0.25, 0.6, progress); + float curvature = 1.0 - smootherstep(0.0, 1.0, progress); + float lensEntry = smootherstep(0.0, 0.08, progress); + float lensExit = 1.0 - smootherstep(0.16, 1.0, progress); + float lens = lensEntry * lensExit; + float outerRim = 1.0 - smootherstep(0.0, max(bulbWidth * 0.07, 2.0), opticalDepth); + float caustic = smootherstep(0.04, 0.13, progress) * (1.0 - smootherstep(0.22, 0.48, progress)); + float counterRim = smootherstep(0.18, 0.34, progress) * (1.0 - smootherstep(0.42, 0.72, progress)); + + vec2 outward = roundedProfileNormal(position, halfSize, boundaryRadius, power, opticalDepth); + vec2 sourceSize = vec2(textureSize(tex, 0)); + vec2 halfTexel = 0.5 / sourceSize; + vec2 maximumUV = vec2(1.0) - halfTexel; + vec2 outwardUV = outward.x * texcoordDx + outward.y * texcoordDy; + float maximumRefraction = max(acrylicRefraction, 0.0); + float availableTravel = availableTextureTravel(v_texcoord, outwardUV, halfTexel, maximumUV); + float edgeValidity = smoothstep(0.0, maximumRefraction + 1.0, availableTravel); + float safeRefraction = min(maximumRefraction * edgeValidity, max(availableTravel - 0.5, 0.0)); + vec2 displacementPixels = outward * safeRefraction * lens * effect; + vec2 displacementUV = displacementPixels.x * texcoordDx + displacementPixels.y * texcoordDy; + float aberration = clamp(acrylicAberration, 0.0, 0.25); + vec2 redUV = clamp(v_texcoord + displacementUV, halfTexel, maximumUV); + vec2 greenUV = clamp(v_texcoord + displacementUV * (1.0 - aberration * 0.5), halfTexel, maximumUV); + vec2 blueUV = clamp(v_texcoord + displacementUV * (1.0 - aberration), halfTexel, maximumUV); + + float outputAlpha = blurred.a; + vec3 blurredLinear = toLinearRGB(blurred.rgb / max(blurred.a, 0.001), acrylicTransferFunction); + vec3 acrylicLinear = blurredLinear; + if (lens > 0.0001 && safeRefraction > 0.0001) { + vec4 displacedBlurred = texture(tex, greenUV); + vec3 displacedBlurredLinear = toLinearRGB(displacedBlurred.rgb / max(displacedBlurred.a, 0.001), acrylicTransferFunction); + acrylicLinear = mix(acrylicLinear, displacedBlurredLinear, effect * curvature * edgeValidity); + } + + float fresnelTransmission = mix(1.0, 0.84, curvature); + float clarity = clamp(acrylicClarity, 0.0, 1.0) * effect * fresnelTransmission * clarityCore; + if (clarity > 0.0001) { + vec4 refractedGreen = texture(sharpTex, greenUV); + vec3 refractedLinear = toLinearRGB(refractedGreen.rgb / max(refractedGreen.a, 0.001), acrylicTransferFunction); + if (aberration > 0.0001 && lens > 0.0001 && safeRefraction > 0.0001) { + vec4 refractedRed = texture(sharpTex, redUV); + vec4 refractedBlue = texture(sharpTex, blueUV); + vec3 refractedRedLinear = toLinearRGB(refractedRed.rgb / max(refractedRed.a, 0.001), acrylicTransferFunction); + vec3 refractedBlueLinear = toLinearRGB(refractedBlue.rgb / max(refractedBlue.a, 0.001), acrylicTransferFunction); + refractedLinear.r = refractedRedLinear.r; + refractedLinear.b = refractedBlueLinear.b; + } + acrylicLinear = mix(acrylicLinear, refractedLinear, clarity); + } + + vec3 tintLinear = acrylicTint.rgb; + float thickness = mix(0.34, 1.0, curvature); + float transmission = exp(-acrylicTint.a * effect * thickness); + acrylicLinear = acrylicLinear * transmission + tintLinear * (1.0 - transmission); + + vec3 surfaceNormal = normalize(vec3(outward * curvature * 2.15, 1.0)); + const vec2 LIGHT_DIRECTION = vec2(-0.451219, 0.892413); + vec3 light = normalize(vec3(LIGHT_DIRECTION, 0.72)); + vec3 halfway = normalize(light + vec3(0.0, 0.0, 1.0)); + float oneMinusNV = 1.0 - max(surfaceNormal.z, 0.0); + float fresnel = 0.0204 + 0.9796 * pow(oneMinusNV, 5.0); + float specular = pow(max(dot(surfaceNormal, halfway), 0.0), 40.0); + float directional = dot(outward, LIGHT_DIRECTION); + float backdropLuma = dot(acrylicLinear, vec3(0.2126, 0.7152, 0.0722)); + float normalizedLuma = clamp(backdropLuma / max(acrylicLuminanceScale, 0.001), 0.0, 1.0); + float highlight = outerRim * (0.045 + 0.12 * max(directional, 0.0)); + highlight += caustic * (0.035 + 0.08 * max(directional, 0.0)); + highlight += curvature * (0.08 * fresnel + 0.13 * specular); + highlight *= effect * mix(1.0, 0.4, normalizedLuma); + + float shadow = outerRim * 0.09 * max(-directional, 0.0) + counterRim * (0.025 + 0.05 * max(-directional, 0.0)); + shadow *= effect * mix(0.45, 1.0, normalizedLuma); + acrylicLinear *= 1.0 - shadow; + acrylicLinear += mix(vec3(acrylicLuminanceScale), tintLinear, 0.08) * highlight; + + vec4 acrylic = fromLinear(vec4(max(acrylicLinear, vec3(0.0)) * outputAlpha, outputAlpha), acrylicTransferFunction); + return mix(blurred, acrylic, shape); +} + +void main() { + vec4 color = texture(tex, v_texcoord); + if (acrylicEnabled != 0) + color = applyAcrylic(color); + + fragColor = blurFinish(color, v_texcoord, noise, brightness +#if USE_CM + , + sourceTF, targetTF, convertMatrix, srcTFRange, dstTFRange +#endif + ); +} diff --git a/src/render/shaders/glsl/aurorafinish.frag b/src/render/shaders/glsl/aurorafinish.frag new file mode 100644 index 000000000..85d6b7e2e --- /dev/null +++ b/src/render/shaders/glsl/aurorafinish.frag @@ -0,0 +1,79 @@ +#version 300 es +#define ALLOW_INCLUDES +#extension GL_ARB_shading_language_include : enable + +precision highp float; +in vec2 v_texcoord; +uniform sampler2D tex; + +uniform float noise; +uniform float brightness; +uniform float glassRefraction; +uniform float glassSize; +uniform float glassRoughness; +uniform vec2 glassPosition; +uniform float time; +uniform float auroraIntensity; +uniform vec4 auroraColor1; +uniform vec4 auroraColor2; +uniform int auroraTransferFunction; + +#include "defines.h" +#if USE_CM +uniform int sourceTF; +uniform int targetTF; +#include "CM.glsl" +#endif + +#include "cm_helpers.glsl" +#include "blurFinish.glsl" + +layout(location = 0) out vec4 fragColor; + +vec3 auroraSurface(vec2 position) { + float verticalWarp = sin(position.y * 0.19 - time) * 0.78 + sin(position.y * 0.43 + time * 2.0) * 0.24; + float warpedX = position.x + verticalWarp; + float broadPhase = warpedX * 0.58 + time; + float narrowPhase = warpedX * 1.07 - time * 2.0 + sin(position.y * 0.13 + time) * 0.42; + float broadRibbon = 0.5 + 0.5 * sin(broadPhase); + float narrowRibbon = 0.5 + 0.5 * sin(narrowPhase); + + broadRibbon *= broadRibbon; + narrowRibbon *= narrowRibbon; + + float verticalLight = 0.72 + 0.28 * sin(position.y * 0.27 - time * 2.0 + sin(warpedX * 0.21)); + float curtain = clamp((broadRibbon * 0.72 + narrowRibbon * 0.38) * verticalLight, 0.0, 1.0); + float colorMix = 0.5 + 0.5 * sin(warpedX * 0.31 - position.y * 0.09 + time * 3.0); + float height = broadRibbon * 0.68 + narrowRibbon * 0.31 + verticalLight * 0.1; + return vec3(height, curtain, colorMix); +} + +void main() { + vec2 position = (gl_FragCoord.xy - glassPosition) / glassSize; + vec3 surface = auroraSurface(position); + vec2 normal = vec2(dFdx(surface.x), dFdy(surface.x)) * glassSize * 0.75; + normal /= max(1.0, length(normal)); + + vec2 uvStep = normal.x * dFdx(v_texcoord) + normal.y * dFdy(v_texcoord); + vec2 displacedUV = clamp(v_texcoord + glassRefraction * uvStep, vec2(0.0), vec2(1.0)); + vec4 color = texture(tex, displacedUV); + + vec4 palette = mix(auroraColor1, auroraColor2, surface.z); + float amount = clamp(auroraIntensity * surface.y, 0.0, 1.0); + vec3 linearColor = toLinearRGB(color.rgb / max(color.a, 0.001), auroraTransferFunction); + + linearColor = mix(linearColor, palette.rgb / max(palette.a, 0.001), amount * palette.a * 0.3); + linearColor += palette.rgb * amount * 0.035; + + const vec2 LIGHT_DIRECTION = vec2(-0.451219, 0.892413); + float emboss = dot(normal, LIGHT_DIRECTION); + linearColor *= 1.0 + emboss * glassRoughness * 0.08; + + color = fromLinear(vec4(max(linearColor, vec3(0.0)) * color.a, color.a), auroraTransferFunction); + fragColor = blurFinish(color, v_texcoord, noise, brightness +#if USE_CM + , + sourceTF, targetTF, convertMatrix, srcTFRange, dstTFRange +#endif + ); +} diff --git a/src/render/shaders/glsl/dropsfinish.frag b/src/render/shaders/glsl/dropsfinish.frag new file mode 100644 index 000000000..9e7a55f13 --- /dev/null +++ b/src/render/shaders/glsl/dropsfinish.frag @@ -0,0 +1,307 @@ +#version 300 es +#define ALLOW_INCLUDES +#extension GL_ARB_shading_language_include : enable + +precision highp float; +in vec2 v_texcoord; +uniform sampler2D tex; + +uniform float noise; +uniform float brightness; +uniform float glassRefraction; +uniform float glassSize; +uniform float glassRoughness; +uniform float time; +uniform vec2 dropsPosition; +uniform sampler2D sharpTex; + +#include "defines.h" +#if USE_CM +uniform int sourceTF; +uniform int targetTF; +#include "CM.glsl" +#endif + +#include "glassFinish.glsl" + +layout(location = 0) out vec4 fragColor; + +const float TAU = 6.28318530718; + +struct SDropSurface { + float height; + float clarity; +}; + +SDropSurface combineDropSurfaces(SDropSurface first, SDropSurface second) { + return SDropSurface(max(first.height, second.height), max(first.clarity, second.clarity)); +} + +vec4 dropRandom(vec2 cell, float seed) { + vec2 key = cell + vec2(seed, seed * 1.61803); + return vec4(hash(key + vec2(17.17, 91.73)), hash(key + vec2(63.31, 11.89)), hash(key + vec2(37.61, 53.47)), + hash(key + vec2(79.13, 41.27))); +} + +float sphericalCap(vec2 offset) { + float distanceSquared = dot(offset, offset); + float edge = 1.0 - smoothstep(0.82, 1.0, distanceSquared); + return sqrt(max(0.0, 1.0 - distanceSquared)) * edge; +} + +float pulse(float beginValue, float peakValue, float fadeValue, float endValue, float value) { + return smoothstep(beginValue, peakValue, value) * (1.0 - smoothstep(fadeValue, endValue, value)); +} + +float smootherStep(float beginValue, float endValue, float value) { + float progress = clamp((value - beginValue) / max(endValue - beginValue, 0.001), 0.0, 1.0); + return progress * progress * progress * (progress * (progress * 6.0 - 15.0) + 10.0); +} + +float smootherStepVelocity(float beginValue, float endValue, float value) { + float progress = clamp((value - beginValue) / max(endValue - beginValue, 0.001), 0.0, 1.0); + float inverseProgress = 1.0 - progress; + return 30.0 * progress * progress * inverseProgress * inverseProgress / max(endValue - beginValue, 0.001); +} + +float trailCenter(float offset, float center, vec3 randomValue) { + float result = center + sin(offset * mix(7.0, 12.0, randomValue.z)) * mix(0.018, 0.055, randomValue.x); + return result + sin(offset * 23.0) * 0.009 * randomValue.y; +} + +SDropSurface staticRainLayer(vec2 position, float seed, float density) { + const vec2 CELL_SIZE = vec2(1.45, 3.2); + + float column = floor(position.x / CELL_SIZE.x); + float columnShift = hash(vec2(column, seed + 9.71)); + vec2 gridPosition = position / CELL_SIZE + vec2(0.0, columnShift); + vec2 cell = floor(gridPosition); + vec2 local = fract(gridPosition); + vec4 randomValue = dropRandom(cell, seed); + + float presence = step(1.0 - density, randomValue.w); + if (presence <= 0.0) + return SDropSurface(0.0, 0.0); + vec2 center = vec2(mix(0.22, 0.78, randomValue.x), mix(0.18, 0.38, randomValue.y)); + vec2 radius = vec2(mix(0.11, 0.17, randomValue.z), mix(0.075, 0.12, randomValue.x)); + + vec2 bodyOffset = local - center; + float verticalPosition = bodyOffset.y / radius.y; + float taper = mix(1.05, 0.58, smoothstep(-0.75, 0.95, verticalPosition)); + float body = sphericalCap(vec2(bodyOffset.x / (radius.x * taper), verticalPosition)); + + float trailStart = center.y + radius.y * 0.55; + float trailLength = min(mix(0.3, 0.52, randomValue.y), 0.96 - trailStart); + float trailProgress = (local.y - trailStart) / max(trailLength, 0.001); + float trailWindow = smoothstep(0.0, 0.08, trailProgress) * (1.0 - smoothstep(0.82, 1.0, trailProgress)); + + float trailOffset = max(0.0, local.y - trailStart); + float trailX = trailCenter(trailOffset, center.x, randomValue.xyz); + + float trailWidth = mix(radius.x * 0.3, radius.x * 0.12, clamp(trailProgress, 0.0, 1.0)); + float trailDistance = abs(local.x - trailX) / max(trailWidth, 0.001); + float trailProfile = sqrt(max(0.0, 1.0 - trailDistance * trailDistance)); + float breakup = mix(0.45, 1.0, smoothstep(-0.3, 0.25, sin((local.y + randomValue.z) * 43.0))); + float trail = trailProfile * trailWindow * breakup * 0.24; + + float satelliteProgress = mix(0.28, 0.7, randomValue.z); + float satelliteY = trailStart + trailLength * satelliteProgress; + float satelliteOffset = satelliteY - trailStart; + float satelliteX = trailCenter(satelliteOffset, center.x, randomValue.xyz); + vec2 satelliteRadius = radius * mix(0.18, 0.3, randomValue.y); + float satellite = sphericalCap((local - vec2(satelliteX, satelliteY)) / satelliteRadius) * 0.55; + + float height = max(body, max(trail, satellite)); + float clarity = max(smoothstep(0.04, 0.34, body) * 0.72, max(smoothstep(0.015, 0.2, trail) * 0.36, smoothstep(0.04, 0.35, satellite) * 0.5)); + return SDropSurface(height, clarity); +} + +float stickSlipProgress(float phase, vec4 randomValue) { + float shift = (randomValue.z - 0.5) * 0.05; + float progress = 0.0; + progress += smootherStep(0.13 + shift, 0.28 + shift, phase) * 0.16; + progress += smootherStep(0.41 - shift, 0.58 - shift, phase) * 0.29; + progress += smootherStep(0.68 + shift, 0.91 + shift, phase) * 0.55; + return progress; +} + +float slideAmount(float phase, vec4 randomValue) { + float shift = (randomValue.z - 0.5) * 0.05; + float velocity = smootherStepVelocity(0.13 + shift, 0.28 + shift, phase) * 0.16; + velocity += smootherStepVelocity(0.41 - shift, 0.58 - shift, phase) * 0.29; + velocity += smootherStepVelocity(0.68 + shift, 0.91 + shift, phase) * 0.55; + return clamp(velocity / 4.6, 0.0, 1.0); +} + +float dropPath(float progress, float start, vec4 randomValue) { + float phase = randomValue.w * TAU; + float wave = sin(progress * mix(4.0, 7.0, randomValue.z) + phase) - sin(phase); + float drift = (randomValue.y - 0.5) * 0.12 * progress; + return clamp(start + drift + wave * mix(0.012, 0.035, randomValue.x), 0.2, 0.8); +} + +SDropSurface movingDrop(vec2 local, vec2 cell, float seed, float density, float animationTime, float cycleRate) { + vec4 randomValue = dropRandom(cell, seed); + + float presence = step(1.0 - density, randomValue.w); + if (presence <= 0.0) + return SDropSurface(0.0, 0.0); + float phaseOffset = hash(cell + vec2(seed + 43.17, 71.53)); + float rate = (0.75 + floor(hash(cell + vec2(seed + 81.31, 19.47)) * 3.0) * 0.25) * cycleRate; + float phase = fract(animationTime * rate + phaseOffset); + float progress = stickSlipProgress(phase, randomValue); + float sliding = slideAmount(phase, randomValue); + + float startX = mix(0.23, 0.77, randomValue.x); + float startY = mix(0.45, 0.82, randomValue.y); + float travelDistance = mix(0.42, 0.82, randomValue.z); + vec2 center = vec2(dropPath(progress, startX, randomValue), startY - travelDistance * progress); + + float landing = smootherStep(0.0, 0.035, phase); + float impact = pulse(0.0, 0.014, 0.035, 0.075, phase); + float drain = smootherStep(0.9, 0.975, phase); + float bodyLife = landing * (1.0 - smootherStep(0.955, 0.985, phase)); + float scale = mix(0.58, 1.0, landing) * (1.0 + impact * 0.12) * mix(1.0, 0.28, drain); + + vec2 baseRadius = vec2(mix(0.11, 0.17, randomValue.z), mix(0.075, 0.12, randomValue.x)); + vec2 radius = baseRadius * vec2(mix(1.0, 0.82, sliding), mix(1.0, 1.24, sliding)) * scale; + vec2 bodyOffset = local - center; + float verticalPosition = bodyOffset.y / radius.y; + float taper = mix(1.05, 0.58, smoothstep(-0.75, 0.95, verticalPosition)); + float body = sphericalCap(vec2(bodyOffset.x / (radius.x * taper), verticalPosition)) * bodyLife * mix(1.0, 0.42, drain); + + float historicalProgress = (startY - local.y) / travelDistance; + float trailWindow = smoothstep(-0.015, 0.015, historicalProgress) * + (1.0 - smoothstep(max(0.0, progress - 0.015), progress + 0.015, historicalProgress)); + float trailProgress = clamp(historicalProgress, 0.0, progress); + float trailX = dropPath(trailProgress, startX, randomValue); + float trailWidth = mix(baseRadius.x * 0.13, baseRadius.x * 0.27, clamp(trailProgress / max(progress, 0.001), 0.0, 1.0)); + float trailDistance = abs(local.x - trailX) / max(trailWidth, 0.001); + float trailProfile = sqrt(max(0.0, 1.0 - trailDistance * trailDistance)); + float trailAge = mix(0.35, 1.0, clamp(trailProgress / max(progress, 0.001), 0.0, 1.0)); + float breakup = mix(0.42, 1.0, smoothstep(-0.35, 0.2, sin((local.y + randomValue.z) * 47.0))); + float trailLife = landing * (1.0 - smootherStep(0.965, 1.0, phase)) * smootherStep(0.025, 0.12, progress); + float trail = trailProfile * trailWindow * trailAge * breakup * trailLife * 0.24; + + float satelliteProgress = progress * mix(0.28, 0.62, randomValue.z); + vec2 satelliteCenter = vec2(dropPath(satelliteProgress, startX, randomValue), startY - travelDistance * satelliteProgress); + vec2 satelliteRadius = baseRadius * mix(0.18, 0.29, randomValue.y); + float satellite = sphericalCap((local - satelliteCenter) / satelliteRadius) * trailLife * 0.5; + + float height = max(body, max(trail, satellite)); + float clarity = max(smoothstep(0.04, 0.34, body) * 0.76, max(smoothstep(0.015, 0.2, trail) * 0.42, smoothstep(0.04, 0.35, satellite) * 0.54)); + return SDropSurface(height, clarity); +} + +SDropSurface movingRainLayer(vec2 position, float seed, float density, float animationTime, float cycleRate) { + const vec2 CELL_SIZE = vec2(1.45, 3.2); + + float column = floor(position.x / CELL_SIZE.x); + float columnShift = hash(vec2(column, seed + 9.71)); + vec2 gridPosition = position / CELL_SIZE + vec2(0.0, columnShift); + vec2 cell = floor(gridPosition); + vec2 local = fract(gridPosition); + + SDropSurface drop = movingDrop(local, cell, seed, density, animationTime, cycleRate); + SDropSurface dropFromAbove = SDropSurface(0.0, 0.0); + if (local.y > 0.4) + dropFromAbove = movingDrop(local - vec2(0.0, 1.0), cell + vec2(0.0, 1.0), seed, density, animationTime, cycleRate); + return combineDropSurfaces(drop, dropFromAbove); +} + +SDropSurface rainLayer(vec2 position, float seed, float density, float animationTime, float cycleRate) { + if (animationTime <= 0.0) + return staticRainLayer(position, seed, density); + return movingRainLayer(position, seed, density, animationTime, cycleRate); +} + +SDropSurface beadLayer(vec2 position, float seed, float density, float animationTime) { + vec2 cell = floor(position); + vec2 local = fract(position); + vec4 randomValue = dropRandom(cell, seed); + + float presence = step(1.0 - density, randomValue.w); + if (presence <= 0.0) + return SDropSurface(0.0, 0.0); + vec2 center = mix(vec2(0.2), vec2(0.8), randomValue.xy); + float radius = mix(0.08, 0.19, randomValue.z * randomValue.z); + vec2 radii = vec2(radius * mix(0.82, 1.08, randomValue.x), radius * mix(0.9, 1.22, randomValue.y)); + float amplitude = mix(0.42, 0.78, randomValue.z); + + if (animationTime <= 0.0) { + float height = sphericalCap((local - center) / radii) * amplitude; + return SDropSurface(height, smoothstep(0.04, 0.36, height) * 0.46); + } + + float behavior = hash(cell + vec2(seed + 57.91, 13.37)); + float persistent = step(0.48, behavior); + float phaseOffset = hash(cell + vec2(seed + 23.73, 89.11)); + float rate = 0.75 + floor(hash(cell + vec2(seed + 67.19, 31.43)) * 3.0) * 0.25; + float phase = fract(animationTime * rate + phaseOffset); + + float landing = smootherStep(0.0, 0.03, phase); + float impact = pulse(0.0, 0.012, 0.03, 0.07, phase) * (1.0 - persistent); + float transientLife = landing * (1.0 - smootherStep(0.84, 1.0, phase)); + float life = mix(transientLife, 1.0, persistent); + float transientRadiusScale = mix(0.48, 1.0, landing) * (1.0 + impact * 0.16); + float edgeDistance = min(min(center.x, 1.0 - center.x), min(center.y, 1.0 - center.y)); + float maximumRadius = max(radii.x, radii.y); + float persistentRadiusScale = min(1.0, edgeDistance / max(maximumRadius, 0.001)); + transientRadiusScale = min(transientRadiusScale, edgeDistance / max(maximumRadius * 1.7, 0.001)); + float radiusScale = mix(transientRadiusScale, persistentRadiusScale, persistent); + + vec2 normalizedOffset = (local - center) / (radii * max(radiusScale, 0.16)); + float body = sphericalCap(normalizedOffset) * life; + float ringDistance = abs(length(normalizedOffset) - 1.4); + float ring = (1.0 - smoothstep(0.1, 0.26, ringDistance)) * impact * 0.08; + + float height = max(body, ring) * amplitude; + float clarity = max(smoothstep(0.04, 0.36, body * amplitude) * 0.48, smoothstep(0.01, 0.06, ring * amplitude) * 0.16); + return SDropSurface(height, clarity); +} + +SDropSurface dropsSurface(vec2 position) { + SDropSurface largeDrops = rainLayer(position, 3.17, 0.55, time, 0.45); + SDropSurface smallDrops = rainLayer(position * 1.43 + vec2(0.37, 1.91), 17.83, 0.35, time, 1.0); + SDropSurface beads = beadLayer(position * 1.75 + vec2(4.13, 2.71), 31.41, 0.36, time); + + smallDrops.height *= 0.78; + smallDrops.clarity *= 0.74; + return combineDropSurfaces(largeDrops, combineDropSurfaces(smallDrops, beads)); +} + +vec4 dropsFinish(vec2 normal, float clarity) { + normal /= max(1.0, length(normal)); + + vec2 uvStep = normal.x * dFdx(v_texcoord) + normal.y * dFdy(v_texcoord); + vec2 displacedUV = clamp(v_texcoord + glassRefraction * uvStep, vec2(0.0), vec2(1.0)); + vec4 pixColor = mix(texture(tex, displacedUV), texture(sharpTex, displacedUV), clarity); + + const vec2 LIGHT_DIRECTION = vec2(-0.451219, 0.892413); + float emboss = dot(normal, LIGHT_DIRECTION); + pixColor.rgb *= 1.0 + emboss * glassRoughness * 0.12; + + return blurFinish(pixColor, v_texcoord, noise, brightness +#if USE_CM + , + sourceTF, targetTF, convertMatrix, srcTFRange, dstTFRange +#endif + ); +} + +void main() { + vec2 fragmentPosition = gl_FragCoord.xy - dropsPosition; + vec2 position = vec2(fragmentPosition.x, -fragmentPosition.y) / glassSize; + SDropSurface surface = dropsSurface(position); + vec2 gradient; + if (time <= 0.0) + gradient = vec2(dFdx(surface.height), dFdy(surface.height)); + else { + float pixelStep = 1.0 / glassSize; + float horizontalHeight = dropsSurface(position + vec2(pixelStep, 0.0)).height; + float verticalHeight = dropsSurface(position - vec2(0.0, pixelStep)).height; + gradient = vec2(horizontalHeight - surface.height, verticalHeight - surface.height); + } + gradient *= glassSize * 0.16; + fragColor = dropsFinish(gradient, surface.clarity); +} diff --git a/src/render/shaders/glsl/fluidJar.glsl b/src/render/shaders/glsl/fluidJar.glsl new file mode 100644 index 000000000..1bcb1a1e5 --- /dev/null +++ b/src/render/shaders/glsl/fluidJar.glsl @@ -0,0 +1,136 @@ +#ifndef ALLOW_INCLUDES +#define ALLOW_INCLUDES +#extension GL_ARB_shading_language_include : enable +#endif + +precision highp usampler2D; + +const float FLUID_JAR_PI = 3.14159265; +const ivec2 FLUID_JAR_STATE_SLOT_SIZE = ivec2(4, 1); +const ivec2 FLUID_JAR_GRAPH_SLOT_SIZE = ivec2(8, 1); + +struct FluidJarParticle { + int id; + vec2 position; + vec2 velocity; + float pressure; + float density; + float smoothScale; + float divergence; + vec4 data; +}; + +ivec2 fluidJarAddress(int id, int field, ivec2 gridSize) { + return FLUID_JAR_STATE_SLOT_SIZE * ivec2(id % gridSize.x, id / gridSize.x) + ivec2(field, 0); +} + +ivec2 fluidJarGraphAddress(int id, int direction, int pairIndex, ivec2 gridSize) { + return FLUID_JAR_GRAPH_SLOT_SIZE * ivec2(id % gridSize.x, id / gridSize.x) + ivec2(direction * 2 + pairIndex, 0); +} + +uvec2 fluidJarEncodeId(int id) { + if (id < 0) + return uvec2(0u); + + uint value = uint(id) + 1u; + return uvec2(value & 65535u, value >> 16u); +} + +int fluidJarDecodeId(uvec2 encoded) { + uint value = encoded.x | (encoded.y << 16u); + return value == 0u ? -1 : int(value - 1u); +} + +uvec4 fluidJarEncodeIdPair(ivec2 ids) { + return uvec4(fluidJarEncodeId(ids.x), fluidJarEncodeId(ids.y)); +} + +ivec2 fluidJarDecodeIdPair(uvec4 encoded) { + return ivec2(fluidJarDecodeId(encoded.rg), fluidJarDecodeId(encoded.ba)); +} + +ivec4 fluidJarLoadNeighbors(usampler2D graphTex, int id, int direction, ivec2 gridSize) { + ivec2 first = fluidJarDecodeIdPair(texelFetch(graphTex, fluidJarGraphAddress(id, direction, 0, gridSize), 0)); + ivec2 second = fluidJarDecodeIdPair(texelFetch(graphTex, fluidJarGraphAddress(id, direction, 1, gridSize), 0)); + return ivec4(first, second); +} + +FluidJarParticle fluidJarLoadParticle(sampler2D particleTex, int id, ivec2 gridSize) { + FluidJarParticle particle; + vec4 value = texelFetch(particleTex, fluidJarAddress(id, 0, gridSize), 0); + particle.position = value.xy; + particle.velocity = value.zw; + + value = texelFetch(particleTex, fluidJarAddress(id, 1, gridSize), 0); + particle.pressure = value.x; + particle.density = value.y; + particle.smoothScale = value.z; + particle.divergence = value.w; + particle.data = texelFetch(particleTex, fluidJarAddress(id, 2, gridSize), 0); + particle.id = id; + return particle; +} + +vec4 fluidJarSaveParticle(FluidJarParticle particle, int field) { + if (field == 0) + return vec4(particle.position, particle.velocity); + if (field == 1) + return vec4(particle.pressure, particle.density, particle.smoothScale, particle.divergence); + if (field == 2) + return particle.data; + return vec4(0.0); +} + +void fluidJarResolveBoundaries(inout FluidJarParticle particle, vec2 resolution, vec4 wallVelocities, float mass) { + float restitution = clamp(0.12 / sqrt(max(mass, 0.1)), 0.02, 0.4); + float left = min(2.0, resolution.x * 0.5); + float right = max(resolution.x - 2.0, left); + float bottom = min(2.0, resolution.y); + float top = max(bottom, resolution.y); + + if (particle.position.x < left) { + particle.position.x = left; + if (particle.velocity.x < wallVelocities.x) + particle.velocity.x = wallVelocities.x - restitution * (particle.velocity.x - wallVelocities.x); + } + + if (particle.position.x > right) { + particle.position.x = right; + if (particle.velocity.x > wallVelocities.y) + particle.velocity.x = wallVelocities.y - restitution * (particle.velocity.x - wallVelocities.y); + } + + if (particle.position.y < bottom) { + particle.position.y = bottom; + if (particle.velocity.y < wallVelocities.z) + particle.velocity.y = wallVelocities.z - restitution * (particle.velocity.y - wallVelocities.z); + } + + if (particle.position.y > top) { + particle.position.y = top; + if (particle.velocity.y > wallVelocities.w) + particle.velocity.y = wallVelocities.w - restitution * (particle.velocity.y - wallVelocities.w); + } +} + +float fluidJarSquared(float value) { + return value * value + 1e-2; +} + +float fluidJarKernel(float distanceValue, float scale) { + return exp(-fluidJarSquared(distanceValue / scale)) / (FLUID_JAR_PI * fluidJarSquared(scale)); +} + +float fluidJarKernelGradient(float distanceValue, float scale) { + return 2.0 * distanceValue * fluidJarKernel(distanceValue, scale) / fluidJarSquared(scale); +} + +float fluidJarHash13(vec3 value) { + value = fract(value * 0.1031); + value += dot(value, value.yzx + 33.33); + return fract((value.x + value.y) * value.z); +} + +ivec2 fluidJarCrossDistribution(int index) { + return (1 << (index / 4)) * ivec2(((index & 2) / 2) ^ 1, (index & 2) / 2) * (2 * (index % 2) - 1); +} diff --git a/src/render/shaders/glsl/fluidjarfinish.frag b/src/render/shaders/glsl/fluidjarfinish.frag new file mode 100644 index 000000000..0509014f1 --- /dev/null +++ b/src/render/shaders/glsl/fluidjarfinish.frag @@ -0,0 +1,170 @@ +#version 300 es +#define ALLOW_INCLUDES +#extension GL_ARB_shading_language_include : enable + +precision highp float; + +in vec2 v_texcoord; +uniform sampler2D tex; +uniform sampler2D sharpTex; +uniform sampler2D fluidJarVisualTex; +uniform int fluidJarEnabled; +uniform vec4 fluidJarExtent; +uniform vec4 fluidJarOutputTransform; +uniform vec2 fluidJarOutputOffset; +uniform vec2 fluidJarLogicalSize; +uniform vec4 fluidJarColor; +uniform float fluidJarRefraction; +uniform int fluidJarTransferFunction; +uniform float fluidJarStrength; +uniform float fluidJarTurbulence; +uniform float fluidJarDistortion; +uniform float time; +uniform float noise; +uniform float brightness; + +#include "defines.h" +#if USE_CM +uniform int sourceTF; +uniform int targetTF; +#include "CM.glsl" +#endif + +#include "cm_helpers.glsl" +#include "blurFinish.glsl" + +layout(location = 0) out vec4 fragColor; + +vec4 sampleFluid(vec2 uv) { + return texture(fluidJarVisualTex, clamp(uv, vec2(0.0), vec2(1.0))); +} + +vec2 outputToLogical(vec2 position) { + return vec2(dot(fluidJarOutputTransform.xy, position), dot(fluidJarOutputTransform.zw, position)) + fluidJarOutputOffset; +} + +vec2 logicalToOutputVector(vec2 vector) { + return vec2(dot(fluidJarOutputTransform.xz, vector), dot(fluidJarOutputTransform.yw, vector)); +} + +vec2 shimmerWaves(vec2 position, float material) { + const vec2 DIRECTION_A = vec2(0.894427, 0.447214); + const vec2 DIRECTION_B = vec2(-0.351123, 0.936329); + const vec2 DIRECTION_C = vec2(0.196116, -0.980581); + float materialPhase = 1.4 * material; + float waveA = cos(0.115 * dot(position, DIRECTION_A) + 0.73 * time + materialPhase); + float waveB = cos(0.168 * dot(position, DIRECTION_B) - 0.51 * time - 0.8 * materialPhase); + float waveC = cos(0.237 * dot(position, DIRECTION_C) + 0.37 * time + 1.3 * materialPhase); + return 0.46 * DIRECTION_A * waveA + 0.34 * DIRECTION_B * waveB + 0.20 * DIRECTION_C * waveC; +} + +vec4 applyLiquid(vec4 blurred, vec2 fluidUV, vec2 sourceSize, vec2 sourcePosition, vec2 logicalPosition) { + vec2 visualSize = vec2(textureSize(fluidJarVisualTex, 0)); + vec2 texel = 1.0 / visualSize; + vec2 cellPixels = fluidJarLogicalSize / visualSize; + + vec4 center = sampleFluid(fluidUV); + vec4 left = sampleFluid(fluidUV - vec2(texel.x, 0.0)); + vec4 right = sampleFluid(fluidUV + vec2(texel.x, 0.0)); + vec4 down = sampleFluid(fluidUV - vec2(0.0, texel.y)); + vec4 up = sampleFluid(fluidUV + vec2(0.0, texel.y)); + + float field = (4.0 * center.a + left.a + right.a + down.a + up.a) * 0.125; + float mask = smoothstep(0.18, 0.42, field); + float opacity = clamp(fluidJarColor.a, 0.0, 1.0); + float effect = opacity * clamp(fluidJarStrength, 0.0, 1.0); + if (mask <= 0.0 || effect <= 0.0) + return blurred; + + vec2 gradient = vec2((right.a - left.a) / max(2.0 * cellPixels.x, 0.001), -(up.a - down.a) / max(2.0 * cellPixels.y, 0.001)); + float gradientLength = length(gradient); + vec2 outwardNormal = gradientLength > 1e-5 ? -gradient / gradientLength : vec2(0.0); + float edge = pow(clamp(4.0 * mask * (1.0 - mask), 0.0, 1.0), 0.75) * smoothstep(0.0001, 0.01, gradientLength); + + vec2 centerVelocity = vec2(center.r * cellPixels.x, -center.g * cellPixels.y) * 1.5; + vec2 leftVelocity = vec2(left.r * cellPixels.x, -left.g * cellPixels.y) * 1.5; + vec2 rightVelocity = vec2(right.r * cellPixels.x, -right.g * cellPixels.y) * 1.5; + vec2 downVelocity = vec2(down.r * cellPixels.x, -down.g * cellPixels.y) * 1.5; + vec2 upVelocity = vec2(up.r * cellPixels.x, -up.g * cellPixels.y) * 1.5; + vec2 outputVelocity = (2.0 * centerVelocity + leftVelocity + rightVelocity + downVelocity + upVelocity) / 6.0; + float speed = length(outputVelocity); + vec2 flowPixels = 2.0 * outputVelocity / (0.8 + speed); + + float motion = smoothstep(0.05, 0.8, speed); + float turbulence = clamp(fluidJarTurbulence, 0.0, 5.0); + vec2 turbulentPixels = vec2(0.0); + if (turbulence > 0.0) { + float curl = (rightVelocity.y - leftVelocity.y) / max(2.0 * cellPixels.x, 0.001) - (downVelocity.x - upVelocity.x) / max(2.0 * cellPixels.y, 0.001); + float normalizedCurl = tanh(curl / 0.12); + vec2 swirlPixels = normalizedCurl * vec2(-outputVelocity.y, outputVelocity.x) / (0.5 + speed); + vec2 materialGradient = vec2((right.b - left.b) / max(2.0 * cellPixels.x, 0.001), -(up.b - down.b) / max(2.0 * cellPixels.y, 0.001)); + vec2 materialPixels = 4.0 * min(cellPixels.x, cellPixels.y) * materialGradient * mix(0.65, 1.35, motion) * (1.0 + 0.5 * abs(normalizedCurl)); + float shimmerActivity = mix(0.24, 1.25, motion) * (1.0 + 0.35 * abs(normalizedCurl)); + vec2 shimmerPixels = 1.6 * shimmerActivity * shimmerWaves(logicalPosition, center.b); + turbulentPixels = turbulence * (materialPixels + swirlPixels + shimmerPixels); + } + float interior = smoothstep(0.45, 0.8, mask) * (1.0 - edge); + vec2 interiorPixels = interior * (flowPixels + turbulentPixels); + interiorPixels /= max(1.0, length(interiorPixels) / max(fluidJarRefraction, 0.001)); + + float edgePixels = mix(5.0, fluidJarRefraction, motion); + float distortion = clamp(fluidJarDistortion, 0.0, 10.0); + float maximumDisplacement = fluidJarRefraction * distortion; + vec2 displacementPixels = distortion * effect * (mask * interiorPixels + edge * outwardNormal * edgePixels); + float sourceEdgeDistance = min(min(sourcePosition.x, sourceSize.x - sourcePosition.x), min(sourcePosition.y, sourceSize.y - sourcePosition.y)); + displacementPixels *= smoothstep(0.0, maximumDisplacement + 1.0, sourceEdgeDistance); + displacementPixels /= max(1.0, length(displacementPixels) / max(maximumDisplacement, 0.001)); + displacementPixels = logicalToOutputVector(displacementPixels); + + vec3 surfaceNormal = normalize(vec3(-gradient * 20.0, 1.0)); + float oneMinusNV = 1.0 - max(surfaceNormal.z, 0.0); + float fresnel = 0.0204 + 0.9796 * pow(oneMinusNV, 5.0); + + vec2 halfTexel = 0.5 / sourceSize; + vec2 refractedUV = clamp(v_texcoord + displacementPixels / sourceSize, halfTexel, vec2(1.0) - halfTexel); + vec4 refracted = texture(sharpTex, refractedUV); + float sharpAmount = effect * mask * mix(0.98, 0.78, fresnel); + + float outputAlpha = blurred.a; + vec3 blurredLinear = toLinearRGB(blurred.rgb / max(blurred.a, 0.001), fluidJarTransferFunction); + vec3 refractedLinear = toLinearRGB(refracted.rgb / max(refracted.a, 0.001), fluidJarTransferFunction); + vec3 liquidLinear = mix(blurredLinear, refractedLinear, sharpAmount); + + vec3 tintLinear = toLinearRGB(fluidJarColor.rgb, CM_TRANSFER_FUNCTION_SRGB); + float opticalDepth = -log(max(1.0 - opacity, 0.0001)); + float thickness = clamp(fluidJarStrength, 0.0, 1.0) * mask * mix(0.25, 1.0, field); + float transmission = exp(-opticalDepth * thickness); + liquidLinear = liquidLinear * transmission + tintLinear * (1.0 - transmission); + + const vec2 LIGHT_DIRECTION = vec2(-0.451219, 0.892413); + vec3 light = normalize(vec3(LIGHT_DIRECTION, 0.75)); + vec3 halfway = normalize(light + vec3(0.0, 0.0, 1.0)); + float specular = pow(max(dot(surfaceNormal, halfway), 0.0), 32.0); + float directional = max(dot(outwardNormal, LIGHT_DIRECTION), 0.0); + float highlight = effect * edge * (0.04 + 0.35 * fresnel + 0.18 * specular + 0.04 * directional); + liquidLinear += mix(vec3(1.0), tintLinear, 0.12) * highlight; + + return fromLinear(vec4(liquidLinear * outputAlpha, outputAlpha), fluidJarTransferFunction); +} + +void main() { + vec4 color = texture(tex, v_texcoord); + if (fluidJarEnabled != 0) { + vec2 sourceSize = vec2(textureSize(tex, 0)); + vec2 position = v_texcoord * sourceSize; + vec2 outputUV = (position - fluidJarExtent.xy) / fluidJarExtent.zw; + if (all(greaterThanEqual(outputUV, vec2(0.0))) && all(lessThanEqual(outputUV, vec2(1.0)))) { + vec2 logicalUV = outputToLogical(outputUV); + vec2 fluidUV = logicalUV; + fluidUV.y = 1.0 - fluidUV.y; + color = applyLiquid(color, fluidUV, sourceSize, position, logicalUV * fluidJarLogicalSize); + } + } + + fragColor = blurFinish(color, v_texcoord, noise, brightness +#if USE_CM + , + sourceTF, targetTF, convertMatrix, srcTFRange, dstTFRange +#endif + ); +} diff --git a/src/render/shaders/glsl/fluidjargraph.frag b/src/render/shaders/glsl/fluidjargraph.frag new file mode 100644 index 000000000..852ab78b4 --- /dev/null +++ b/src/render/shaders/glsl/fluidjargraph.frag @@ -0,0 +1,126 @@ +#version 300 es +#define ALLOW_INCLUDES +#extension GL_ARB_shading_language_include : enable + +precision highp float; +precision highp int; +precision highp usampler2D; + +in vec2 v_texcoord; +uniform sampler2D fluidJarParticleTex; +uniform usampler2D fluidJarGraphTex; +uniform vec2 fluidJarResolution; +uniform vec2 fluidJarGridSize; +uniform int fluidJarParticleCount; +uniform int fluidJarFrame; + +#include "fluidJar.glsl" + +layout(location = 0) out uvec4 fragColor; + +ivec4 nearestIds = ivec4(-1); +vec4 nearestDistances = vec4(1e6); + +void insertNearest(float distanceValue, int id) { + if (nearestDistances.x > distanceValue) { + nearestDistances = vec4(distanceValue, nearestDistances.xyz); + nearestIds = ivec4(id, nearestIds.xyz); + } else if (nearestDistances.y > distanceValue) { + nearestDistances.yzw = vec3(distanceValue, nearestDistances.yz); + nearestIds.yzw = ivec3(id, nearestIds.yz); + } else if (nearestDistances.z > distanceValue) { + nearestDistances.zw = vec2(distanceValue, nearestDistances.z); + nearestIds.zw = ivec2(id, nearestIds.z); + } else if (nearestDistances.w > distanceValue) { + nearestDistances.w = distanceValue; + nearestIds.w = id; + } +} + +bool alreadySorted(int id, int currentId) { + return id < 0 || id >= fluidJarParticleCount || id == currentId || any(equal(nearestIds, ivec4(id))); +} + +int neighborDirection(vec2 delta, int firstId, int secondId) { + if (dot(delta, delta) < 1e-6) { + float pairHash = fluidJarHash13(vec3(float(min(firstId, secondId)), float(max(firstId, secondId)), 0.731)); + int pairDirection = min(int(pairHash * 4.0), 3); + return firstId < secondId ? pairDirection : pairDirection ^ 1; + } + + if (abs(delta.x) >= abs(delta.y)) + return delta.x >= 0.0 ? 0 : 1; + return delta.y >= 0.0 ? 2 : 3; +} + +void sortCandidate(int candidate, int direction, FluidJarParticle particle, ivec2 gridSize) { + if (alreadySorted(candidate, particle.id)) + return; + + vec2 neighborPosition = texelFetch(fluidJarParticleTex, fluidJarAddress(candidate, 0, gridSize), 0).xy; + vec2 delta = neighborPosition - particle.position; + if (neighborDirection(delta, particle.id, candidate) != direction) + return; + insertNearest(length(delta), candidate); +} + +void sortGridCandidate(int row, int columnOffset, int direction, FluidJarParticle particle, ivec2 gridSize) { + if (row < 0 || row >= gridSize.y) + return; + + int rowStart = row * gridSize.x; + int rowCount = min(gridSize.x, fluidJarParticleCount - rowStart); + if (rowCount <= 0) + return; + + vec2 spacing = fluidJarResolution / vec2(gridSize); + float centering = 0.5 * float(gridSize.x - rowCount); + int column = clamp(int(particle.position.x / spacing.x - centering) + columnOffset, 0, rowCount - 1); + sortCandidate(rowStart + column, direction, particle, gridSize); +} + +void main() { + ivec2 pixel = ivec2(gl_FragCoord.xy); + ivec2 gridSize = ivec2(fluidJarGridSize); + ivec2 storageSize = FLUID_JAR_GRAPH_SLOT_SIZE * gridSize; + if (any(greaterThanEqual(pixel, storageSize))) { + fragColor = uvec4(0u); + return; + } + + ivec2 cell = pixel / FLUID_JAR_GRAPH_SLOT_SIZE; + int id = cell.x + cell.y * gridSize.x; + int field = pixel.x % FLUID_JAR_GRAPH_SLOT_SIZE.x; + int direction = field / 2; + int pairIndex = field % 2; + if (id >= fluidJarParticleCount) { + fragColor = uvec4(0u); + return; + } + + FluidJarParticle particle = fluidJarLoadParticle(fluidJarParticleTex, id, gridSize); + int estimatedRow = min(int(particle.position.y / (fluidJarResolution.y / float(gridSize.y))), (fluidJarParticleCount - 1) / gridSize.x); + for (int offset = -4; offset <= 4; ++offset) + sortGridCandidate(estimatedRow + offset, 0, direction, particle, gridSize); + for (int offset = 1; offset <= 2; ++offset) { + sortGridCandidate(estimatedRow, -offset, direction, particle, gridSize); + sortGridCandidate(estimatedRow, offset, direction, particle, gridSize); + } + + for (int index = 0; index < 8; ++index) + sortCandidate(int(float(fluidJarParticleCount) * fluidJarHash13(vec3(float(fluidJarFrame), float(id), float(index)))), direction, particle, gridSize); + + ivec4 directNeighbors = fluidJarLoadNeighbors(fluidJarGraphTex, id, direction, gridSize); + for (int index = 0; index < 4; ++index) { + int neighborId = directNeighbors[index]; + sortCandidate(neighborId, direction, particle, gridSize); + if (neighborId < 0 || neighborId >= fluidJarParticleCount) + continue; + + ivec4 indirectNeighbors = fluidJarLoadNeighbors(fluidJarGraphTex, neighborId, (fluidJarFrame + id) % 4, gridSize); + for (int indirect = 0; indirect < 2; ++indirect) + sortCandidate(indirectNeighbors[indirect], direction, particle, gridSize); + } + + fragColor = fluidJarEncodeIdPair(pairIndex == 0 ? nearestIds.xy : nearestIds.zw); +} diff --git a/src/render/shaders/glsl/fluidjarhistoryresample.frag b/src/render/shaders/glsl/fluidjarhistoryresample.frag new file mode 100644 index 000000000..c8bed3362 --- /dev/null +++ b/src/render/shaders/glsl/fluidjarhistoryresample.frag @@ -0,0 +1,21 @@ +#version 300 es + +precision highp float; + +in vec2 v_texcoord; +uniform sampler2D fluidJarHistoryTex; +uniform vec2 fluidJarOldResolution; +uniform vec4 fluidJarHistoryTransform; +uniform vec4 fluidJarHistoryFallback; + +layout(location = 0) out vec4 fragColor; + +void main() { + vec2 oldPosition = gl_FragCoord.xy * fluidJarHistoryTransform.xy + fluidJarHistoryTransform.zw; + if (any(lessThan(oldPosition, vec2(0.0))) || any(greaterThanEqual(oldPosition, fluidJarOldResolution))) { + fragColor = fluidJarHistoryFallback; + return; + } + + fragColor = texture(fluidJarHistoryTex, oldPosition / fluidJarOldResolution); +} diff --git a/src/render/shaders/glsl/fluidjarinit.frag b/src/render/shaders/glsl/fluidjarinit.frag new file mode 100644 index 000000000..5b98dae09 --- /dev/null +++ b/src/render/shaders/glsl/fluidjarinit.frag @@ -0,0 +1,52 @@ +#version 300 es +#define ALLOW_INCLUDES +#extension GL_ARB_shading_language_include : enable + +precision highp float; +precision highp int; + +in vec2 v_texcoord; +uniform vec2 fluidJarResolution; +uniform vec2 fluidJarGridSize; +uniform int fluidJarParticleCount; + +#include "fluidJar.glsl" + +layout(location = 0) out vec4 fragColor; + +void main() { + ivec2 pixel = ivec2(gl_FragCoord.xy); + ivec2 gridSize = ivec2(fluidJarGridSize); + ivec2 storageSize = FLUID_JAR_STATE_SLOT_SIZE * gridSize; + if (any(greaterThanEqual(pixel, storageSize))) { + fragColor = vec4(-1.0); + return; + } + + ivec2 cell = pixel / FLUID_JAR_STATE_SLOT_SIZE; + int id = cell.x + cell.y * gridSize.x; + int field = pixel.x % FLUID_JAR_STATE_SLOT_SIZE.x; + if (id >= fluidJarParticleCount) { + fragColor = vec4(-1.0); + return; + } + + FluidJarParticle particle; + particle.id = id; + vec2 spacing = fluidJarResolution / vec2(gridSize); + particle.position = (vec2(cell) + vec2(0.5)) * spacing; + int partialRowCount = fluidJarParticleCount % gridSize.x; + if (partialRowCount > 0 && cell.y == fluidJarParticleCount / gridSize.x) + particle.position.x += 0.5 * float(gridSize.x - partialRowCount) * spacing.x; + particle.velocity = vec2(0.0); + particle.pressure = 0.0; + particle.density = 5.0; + particle.smoothScale = 1.0; + particle.divergence = 0.0; + float materialA = 0.5 + 0.25 * sin(dot(particle.position, vec2(0.17, 0.11))) + 0.25 * sin(dot(particle.position, vec2(-0.09, 0.23)) + 1.7); + float materialB = 0.5 + 0.25 * sin(dot(particle.position, vec2(-0.13, 0.07)) + 0.8) + 0.25 * sin(dot(particle.position, vec2(0.05, 0.19)) + 2.4); + float shapeA = fluidJarHash13(vec3(float(id) + 0.37, 1.73, 4.91)); + float shapeB = fluidJarHash13(vec3(float(id) + 2.11, 5.29, 0.83)); + particle.data = vec4(clamp(materialA, 0.0, 1.0), clamp(materialB, 0.0, 1.0), shapeA, shapeB); + fragColor = fluidJarSaveParticle(particle, field); +} diff --git a/src/render/shaders/glsl/fluidjarresample.frag b/src/render/shaders/glsl/fluidjarresample.frag new file mode 100644 index 000000000..acd660449 --- /dev/null +++ b/src/render/shaders/glsl/fluidjarresample.frag @@ -0,0 +1,50 @@ +#version 300 es +#define ALLOW_INCLUDES +#extension GL_ARB_shading_language_include : enable + +precision highp float; +precision highp int; + +in vec2 v_texcoord; +uniform sampler2D fluidJarParticleTex; +uniform vec2 fluidJarResolution; +uniform vec2 fluidJarGridSize; +uniform int fluidJarParticleCount; +uniform vec2 fluidJarOldGridSize; +uniform int fluidJarOldParticleCount; +uniform vec4 fluidJarTransform; +uniform vec2 fluidJarVelocityScale; +uniform vec4 fluidJarWallVelocities; +uniform float fluidJarMass; + +#include "fluidJar.glsl" + +layout(location = 0) out vec4 fragColor; + +void main() { + ivec2 pixel = ivec2(gl_FragCoord.xy); + ivec2 gridSize = ivec2(fluidJarGridSize); + ivec2 storageSize = FLUID_JAR_STATE_SLOT_SIZE * gridSize; + if (any(greaterThanEqual(pixel, storageSize))) { + fragColor = vec4(-1.0); + return; + } + + ivec2 cell = pixel / FLUID_JAR_STATE_SLOT_SIZE; + int id = cell.x + cell.y * gridSize.x; + int field = pixel.x % FLUID_JAR_STATE_SLOT_SIZE.x; + if (id >= fluidJarParticleCount || id >= fluidJarOldParticleCount) { + fragColor = vec4(-1.0); + return; + } + + FluidJarParticle particle = fluidJarLoadParticle(fluidJarParticleTex, id, ivec2(fluidJarOldGridSize)); + particle.position = particle.position * fluidJarTransform.xy + fluidJarTransform.zw; + particle.velocity *= fluidJarVelocityScale; + if (any(greaterThan(abs(fluidJarTransform.xy - vec2(1.0)), vec2(0.001)))) { + particle.pressure = 0.0; + particle.divergence = 0.0; + } + fluidJarResolveBoundaries(particle, fluidJarResolution, fluidJarWallVelocities, fluidJarMass); + fragColor = fluidJarSaveParticle(particle, field); +} diff --git a/src/render/shaders/glsl/fluidjarstep.frag b/src/render/shaders/glsl/fluidjarstep.frag new file mode 100644 index 000000000..ebb6ca382 --- /dev/null +++ b/src/render/shaders/glsl/fluidjarstep.frag @@ -0,0 +1,89 @@ +#version 300 es +#define ALLOW_INCLUDES +#extension GL_ARB_shading_language_include : enable + +precision highp float; +precision highp int; +precision highp usampler2D; + +in vec2 v_texcoord; +uniform sampler2D fluidJarParticleTex; +uniform usampler2D fluidJarGraphTex; +uniform vec2 fluidJarResolution; +uniform vec2 fluidJarGridSize; +uniform int fluidJarParticleCount; +uniform float fluidJarDt; +uniform vec4 fluidJarWallVelocities; +uniform float fluidJarMass; + +#include "fluidJar.glsl" + +layout(location = 0) out vec4 fragColor; + +bool isReciprocalNeighbor(int particleId, int neighborId, int direction, ivec2 gridSize) { + return any(equal(fluidJarLoadNeighbors(fluidJarGraphTex, neighborId, direction ^ 1, gridSize), ivec4(particleId))); +} + +void main() { + ivec2 pixel = ivec2(gl_FragCoord.xy); + ivec2 gridSize = ivec2(fluidJarGridSize); + ivec2 storageSize = FLUID_JAR_STATE_SLOT_SIZE * gridSize; + if (any(greaterThanEqual(pixel, storageSize))) { + fragColor = vec4(0.0); + return; + } + + ivec2 cell = pixel / FLUID_JAR_STATE_SLOT_SIZE; + int id = cell.x + cell.y * gridSize.x; + int field = pixel.x % FLUID_JAR_STATE_SLOT_SIZE.x; + if (id >= fluidJarParticleCount) { + fragColor = vec4(0.0); + return; + } + + FluidJarParticle particle = fluidJarLoadParticle(fluidJarParticleTex, id, gridSize); + vec2 force = vec2(0.0, -0.001); + float scale = 0.21 / 0.036; + float divergence = 0.0; + float density = fluidJarKernel(0.0, scale); + vec2 averageMaterial = particle.data.xy; + float neighborCount = 1.0; + + for (int direction = 0; direction < 4; ++direction) { + ivec4 neighbors = fluidJarLoadNeighbors(fluidJarGraphTex, id, direction, gridSize); + for (int index = 0; index < 4; ++index) { + int neighborId = neighbors[index]; + if (neighborId < 0 || neighborId >= fluidJarParticleCount) + continue; + if (!isReciprocalNeighbor(id, neighborId, direction, gridSize)) + continue; + + FluidJarParticle neighbor = fluidJarLoadParticle(fluidJarParticleTex, neighborId, gridSize); + float distanceValue = distance(particle.position, neighbor.position); + vec2 velocityDelta = neighbor.velocity - particle.velocity; + vec2 positionDelta = neighbor.position - particle.position; + vec2 directionVector = positionDelta / (distanceValue + 0.001); + float kernel = fluidJarKernel(distanceValue, scale); + float velocityProjection = dot(directionVector, velocityDelta); + vec2 pressureForce = -(neighbor.pressure / fluidJarSquared(neighbor.density) + particle.pressure / fluidJarSquared(particle.density)) * directionVector * kernel; + divergence += velocityProjection * kernel; + density += kernel; + averageMaterial += neighbor.data.xy; + vec2 viscosity = 1.4 * (3.0 + 3.0 * length(velocityDelta)) * directionVector * velocityProjection * kernel; + force += pressureForce / fluidJarMass + viscosity; + neighborCount += 1.0; + } + } + + particle.density = density; + particle.divergence = divergence; + particle.smoothScale = 0.0; + float waterPressure = 0.035 * 0.036 * (pow(abs(particle.density / 0.036), 7.0) - 1.0); + particle.pressure = clamp(waterPressure, 0.0, 0.04); + particle.velocity += force * fluidJarDt; + particle.velocity -= particle.velocity * (0.5 * tanh(8.0 * (length(particle.velocity) - 1.5)) + 0.5); + particle.position += particle.velocity * fluidJarDt; + fluidJarResolveBoundaries(particle, fluidJarResolution, fluidJarWallVelocities, fluidJarMass); + particle.data.xy = mix(particle.data.xy, averageMaterial / neighborCount, 0.003); + fragColor = fluidJarSaveParticle(particle, field); +} diff --git a/src/render/shaders/glsl/fluidjartrack.frag b/src/render/shaders/glsl/fluidjartrack.frag new file mode 100644 index 000000000..3f6f9db78 --- /dev/null +++ b/src/render/shaders/glsl/fluidjartrack.frag @@ -0,0 +1,84 @@ +#version 300 es +#define ALLOW_INCLUDES +#extension GL_ARB_shading_language_include : enable + +precision highp float; +precision highp int; +precision highp usampler2D; + +in vec2 v_texcoord; +uniform sampler2D fluidJarParticleTex; +uniform usampler2D fluidJarGraphTex; +uniform usampler2D fluidJarTrackingTex; +uniform vec2 fluidJarResolution; +uniform vec2 fluidJarGridSize; +uniform int fluidJarParticleCount; +uniform int fluidJarFrame; + +#include "fluidJar.glsl" + +layout(location = 0) out uvec4 fragColor; + +float nearestDistance = 1e10; +int nearestId = -1; +vec2 position; + +void sortCandidate(int candidate, ivec2 gridSize) { + if (candidate < 0 || candidate >= fluidJarParticleCount) + return; + vec2 particlePosition = texelFetch(fluidJarParticleTex, fluidJarAddress(candidate, 0, gridSize), 0).xy; + float candidateDistance = distance(position, particlePosition); + if (candidateDistance >= nearestDistance) + return; + nearestDistance = candidateDistance; + nearestId = candidate; +} + +void sortGridCandidate(int row, ivec2 gridSize) { + if (row < 0 || row >= gridSize.y) + return; + + int rowStart = row * gridSize.x; + int rowCount = min(gridSize.x, fluidJarParticleCount - rowStart); + if (rowCount <= 0) + return; + + vec2 spacing = fluidJarResolution / vec2(gridSize); + float centering = 0.5 * float(gridSize.x - rowCount); + int column = clamp(int(position.x / spacing.x - centering), 0, rowCount - 1); + sortCandidate(rowStart + column, gridSize); +} + +void main() { + ivec2 pixel = ivec2(gl_FragCoord.xy); + ivec2 gridSize = ivec2(fluidJarGridSize); + position = vec2(pixel) + vec2(0.5); + + int previousId = fluidJarDecodeId(texelFetch(fluidJarTrackingTex, clamp(pixel, ivec2(0), ivec2(fluidJarResolution) - 1), 0).rg); + sortCandidate(previousId, gridSize); + + int estimatedRow = min(int(position.y / (fluidJarResolution.y / float(gridSize.y))), (fluidJarParticleCount - 1) / gridSize.x); + sortGridCandidate(estimatedRow - 1, gridSize); + sortGridCandidate(estimatedRow, gridSize); + sortGridCandidate(estimatedRow + 1, gridSize); + + for (int index = 0; index < 8; ++index) { + ivec2 samplePixel = clamp(pixel + fluidJarCrossDistribution(index), ivec2(0), ivec2(fluidJarResolution) - 1); + sortCandidate(fluidJarDecodeId(texelFetch(fluidJarTrackingTex, samplePixel, 0).rg), gridSize); + } + + for (int index = 0; index < 5; ++index) + sortCandidate(int(float(fluidJarParticleCount) * + fluidJarHash13(vec3(float(fluidJarFrame + index) + 0.5, float(pixel.x) + 0.37, float(pixel.y) + 0.61))), + gridSize); + + if (nearestId >= 0) { + for (int direction = 0; direction < 4; ++direction) { + ivec4 neighbors = fluidJarLoadNeighbors(fluidJarGraphTex, nearestId, direction, gridSize); + for (int index = 0; index < 4; ++index) + sortCandidate(neighbors[index], gridSize); + } + } + + fragColor = uvec4(fluidJarEncodeId(nearestId), 0u, 0u); +} diff --git a/src/render/shaders/glsl/fluidjartrackingresample.frag b/src/render/shaders/glsl/fluidjartrackingresample.frag new file mode 100644 index 000000000..1e3344d3f --- /dev/null +++ b/src/render/shaders/glsl/fluidjartrackingresample.frag @@ -0,0 +1,21 @@ +#version 300 es + +precision highp float; +precision highp usampler2D; + +in vec2 v_texcoord; +uniform usampler2D fluidJarHistoryTex; +uniform vec2 fluidJarOldResolution; +uniform vec4 fluidJarHistoryTransform; + +layout(location = 0) out uvec4 fragColor; + +void main() { + vec2 oldPosition = gl_FragCoord.xy * fluidJarHistoryTransform.xy + fluidJarHistoryTransform.zw; + if (any(lessThan(oldPosition, vec2(0.0))) || any(greaterThanEqual(oldPosition, fluidJarOldResolution))) { + fragColor = uvec4(0u); + return; + } + + fragColor = texelFetch(fluidJarHistoryTex, ivec2(oldPosition), 0); +} diff --git a/src/render/shaders/glsl/fluidjarvisual.frag b/src/render/shaders/glsl/fluidjarvisual.frag new file mode 100644 index 000000000..e2deaab54 --- /dev/null +++ b/src/render/shaders/glsl/fluidjarvisual.frag @@ -0,0 +1,89 @@ +#version 300 es +#define ALLOW_INCLUDES +#extension GL_ARB_shading_language_include : enable + +precision highp float; +precision highp int; +precision highp usampler2D; + +in vec2 v_texcoord; +uniform sampler2D fluidJarParticleTex; +uniform usampler2D fluidJarGraphTex; +uniform usampler2D fluidJarTrackingTex; +uniform sampler2D fluidJarVisualTex; +uniform vec2 fluidJarResolution; +uniform vec2 fluidJarGridSize; +uniform int fluidJarParticleCount; +uniform float fluidJarVisualResponse; + +#include "fluidJar.glsl" + +layout(location = 0) out vec4 fragColor; + +const float FIELD_FALLOFF = 0.055; + +vec2 samplePosition; +float fieldSum = 0.0; +float weightSum = 0.0; +vec2 velocitySum = vec2(0.0); +float materialSum = 0.0; + +float shapedDistanceSquared(vec2 delta, vec2 shapeSeed) { + float distanceSquared = dot(delta, delta); + if (distanceSquared < 1e-6) + return 0.0; + + vec2 axis = 2.0 * shapeSeed - 1.0; + axis *= inversesqrt(max(dot(axis, axis), 0.01)); + vec2 direction = delta * inversesqrt(distanceSquared); + float along = dot(direction, axis); + float across = dot(direction, vec2(-axis.y, axis.x)); + float thirdOrder = 4.0 * along * along * along - 3.0 * along; + float radialScale = clamp(1.0 + 0.10 * along + 0.06 * (2.0 * across * across - 1.0) + 0.035 * thirdOrder, 0.80, 1.20); + return distanceSquared / (radialScale * radialScale); +} + +void addParticle(int id, ivec2 gridSize) { + if (id < 0 || id >= fluidJarParticleCount) + return; + + vec4 state = texelFetch(fluidJarParticleTex, fluidJarAddress(id, 0, gridSize), 0); + vec4 material = texelFetch(fluidJarParticleTex, fluidJarAddress(id, 2, gridSize), 0); + vec2 delta = samplePosition - state.xy; + float contribution = exp(-FIELD_FALLOFF * shapedDistanceSquared(delta, material.zw)); + fieldSum += contribution; + weightSum += contribution; + velocitySum += state.zw * contribution; + float materialValue = dot(2.0 * material - 1.0, vec4(0.40, 0.25, 0.22, 0.13)); + materialSum += materialValue * contribution; +} + +void main() { + ivec2 pixel = ivec2(gl_FragCoord.xy); + ivec2 gridSize = ivec2(fluidJarGridSize); + int id = fluidJarDecodeId(texelFetch(fluidJarTrackingTex, pixel, 0).rg); + vec4 previous = texelFetch(fluidJarVisualTex, pixel, 0); + if (id < 0 || id >= fluidJarParticleCount) { + fragColor = mix(previous, vec4(0.0), 0.65); + return; + } + + samplePosition = vec2(pixel) + vec2(0.5); + addParticle(id, gridSize); + for (int direction = 0; direction < 4; ++direction) { + ivec4 neighbors = fluidJarLoadNeighbors(fluidJarGraphTex, id, direction, gridSize); + for (int index = 0; index < 4; ++index) + addParticle(neighbors[index], gridSize); + } + + float field = 1.0 - exp(-fieldSum); + vec2 flow = weightSum > 1e-5 ? velocitySum / (1.5 * weightSum) : vec2(0.0); + float heterogeneity = weightSum > 1e-5 ? clamp(materialSum / weightSum, -1.0, 1.0) : 0.0; + float presence = smoothstep(0.03, 0.2, field); + vec4 current = vec4(clamp(flow, vec2(-1.0), vec2(1.0)) * presence, heterogeneity * presence, clamp(field, 0.0, 1.0)); + + vec3 response = vec3(1.0) - pow(vec3(0.55, 0.45, 0.15), vec3(max(fluidJarVisualResponse, 1.0))); + fragColor.rg = mix(previous.rg, current.rg, response.x); + fragColor.b = mix(previous.b, current.b, response.y); + fragColor.a = mix(previous.a, current.a, response.z); +} diff --git a/src/render/shaders/glsl/frostfinish.frag b/src/render/shaders/glsl/frostfinish.frag new file mode 100644 index 000000000..579bd7564 --- /dev/null +++ b/src/render/shaders/glsl/frostfinish.frag @@ -0,0 +1,91 @@ +#version 300 es +#define ALLOW_INCLUDES +#extension GL_ARB_shading_language_include : enable + +precision highp float; +in vec2 v_texcoord; +uniform sampler2D tex; + +uniform float noise; +uniform float brightness; +uniform float glassRefraction; +uniform float glassSize; +uniform float glassRoughness; +uniform vec2 glassPosition; + +#include "defines.h" +#if USE_CM +uniform int sourceTF; +uniform int targetTF; +#include "CM.glsl" +#endif + +#include "glassFinish.glsl" + +layout(location = 0) out vec4 fragColor; + +vec2 frostRandom(vec2 cell) { + return vec2(hash(cell + vec2(13.37, 71.91)), hash(cell + vec2(83.17, 29.53))); +} + +vec2 frostWarp(vec2 position) { + const vec2 DIRECTION_1 = vec2(1.73, -1.21); + const vec2 DIRECTION_2 = vec2(1.11, 1.87); + const vec2 DIRECTION_3 = vec2(-2.19, 0.83); + + vec2 warp = vec2(sin(dot(position, DIRECTION_1) + 0.7), cos(dot(position, DIRECTION_2) + 1.9)); + warp += vec2(cos(dot(position, DIRECTION_3) + 2.8), sin(dot(position, DIRECTION_1 - DIRECTION_2) + 4.1)) * 0.45; + return warp * 0.16; +} + +void frostCellular(vec2 position, out vec2 nearestOffset, out vec2 secondOffset) { + vec2 baseCell = floor(position); + float nearestDistance = 1e10; + float secondDistance = 1e10; + + nearestOffset = vec2(0.0); + secondOffset = vec2(0.0); + + for (int y = -1; y <= 1; ++y) { + for (int x = -1; x <= 1; ++x) { + vec2 cell = baseCell + vec2(float(x), float(y)); + vec2 center = cell + mix(vec2(0.16), vec2(0.84), frostRandom(cell)); + vec2 offset = position - center; + float distance = dot(offset, offset); + + if (distance < nearestDistance) { + secondDistance = nearestDistance; + secondOffset = nearestOffset; + nearestDistance = distance; + nearestOffset = offset; + } else if (distance < secondDistance) { + secondDistance = distance; + secondOffset = offset; + } + } + } +} + +vec2 frostGradient(vec2 position) { + position += frostWarp(position); + + vec2 nearestOffset; + vec2 secondOffset; + frostCellular(position, nearestOffset, secondOffset); + + float boundary = length(secondOffset) - length(nearestOffset); + float seamWidth = 0.028 + fwidth(boundary) * 1.5; + float seam = 1.0 - smoothstep(0.0, seamWidth, boundary); + + vec2 seamDirection = secondOffset - nearestOffset; + seamDirection /= max(length(seamDirection), 0.0001); + + vec2 grain = vec2(sin(dot(position, vec2(2.61, -1.43))), cos(dot(position, vec2(1.19, 2.37)))) * 0.09; + return nearestOffset * 0.34 + grain + seamDirection * seam * 0.55; +} + +void main() { + vec2 position = (gl_FragCoord.xy - glassPosition) / glassSize; + vec2 gradient = frostGradient(position); + fragColor = glassFinish(gradient); +} diff --git a/src/render/shaders/glsl/glassFinish.glsl b/src/render/shaders/glsl/glassFinish.glsl new file mode 100644 index 000000000..8cd970cd2 --- /dev/null +++ b/src/render/shaders/glsl/glassFinish.glsl @@ -0,0 +1,25 @@ +#ifndef ALLOW_INCLUDES +#define ALLOW_INCLUDES +#extension GL_ARB_shading_language_include : enable +#endif + +#include "blurFinish.glsl" + +vec4 glassFinish(vec2 normal) { + normal /= max(1.0, length(normal)); + + vec2 uvStep = normal.x * dFdx(v_texcoord) + normal.y * dFdy(v_texcoord); + vec2 displacedUV = clamp(v_texcoord + glassRefraction * uvStep, vec2(0.0), vec2(1.0)); + vec4 pixColor = texture(tex, displacedUV); + + const vec2 LIGHT_DIRECTION = vec2(-0.451219, 0.892413); + float emboss = dot(normal, LIGHT_DIRECTION); + pixColor.rgb *= 1.0 + emboss * glassRoughness * 0.12; + + return blurFinish(pixColor, v_texcoord, noise, brightness +#if USE_CM + , + sourceTF, targetTF, convertMatrix, srcTFRange, dstTFRange +#endif + ); +} diff --git a/src/render/shaders/glsl/hazefinish.frag b/src/render/shaders/glsl/hazefinish.frag new file mode 100644 index 000000000..6bbd2c08d --- /dev/null +++ b/src/render/shaders/glsl/hazefinish.frag @@ -0,0 +1,69 @@ +#version 300 es +#define ALLOW_INCLUDES +#extension GL_ARB_shading_language_include : enable + +precision highp float; +in vec2 v_texcoord; +uniform sampler2D tex; + +uniform float noise; +uniform float brightness; +uniform float hazeIntensity; +uniform float hazeIridescence; +uniform int hazeTransferFunction; + +#include "defines.h" +#if USE_CM +uniform int sourceTF; +uniform int targetTF; +#include "CM.glsl" +#endif + +#include "cm_helpers.glsl" +#include "blurFinish.glsl" + +layout(location = 0) out vec4 fragColor; + +const vec3 BT709_LUMA = vec3(0.2126, 0.7152, 0.0722); +const vec3 PEARL_COOL = vec3(0.55, 1.08, 1.35); +const vec3 PEARL_MID = vec3(1.28, 0.86, 1.30); +const vec3 PEARL_WARM = vec3(1.40, 0.92, 0.58); + +vec3 pearlColor(float phase) { + float position = phase * 0.5 + 0.5; + vec3 color; + if (position < 0.5) + color = mix(PEARL_COOL, PEARL_MID, position * 2.0); + else + color = mix(PEARL_MID, PEARL_WARM, (position - 0.5) * 2.0); + return color / max(dot(color, BT709_LUMA), 0.001); +} + +vec4 applyHaze(vec4 color) { + float intensity = clamp(hazeIntensity, 0.0, 1.0); + if (intensity <= 0.0 || color.a <= 0.001) + return color; + + float alpha = color.a; + vec3 linearColor = toLinearRGB(max(color.rgb / alpha, vec3(0.0)), hazeTransferFunction); + float luminance = max(dot(linearColor, BT709_LUMA), 0.0); + float phase = clamp(dot(v_texcoord - vec2(0.5), vec2(1.28, -0.72)), -1.0, 1.0); + float iridescence = clamp(hazeIridescence, 0.0, 1.0); + vec3 spectralShift = luminance * (pearlColor(phase) - vec3(1.0)) * iridescence * 0.65; + float sheen = 1.0 + (1.0 - abs(phase)) * 0.08; + vec3 filmColor = max((linearColor + spectralShift) * sheen, vec3(0.0)); + + linearColor = mix(linearColor, filmColor, intensity); + return fromLinear(vec4(max(linearColor, vec3(0.0)) * alpha, alpha), hazeTransferFunction); +} + +void main() { + vec4 color = applyHaze(texture(tex, v_texcoord)); + + fragColor = blurFinish(color, v_texcoord, noise, brightness +#if USE_CM + , + sourceTF, targetTF, convertMatrix, srcTFRange, dstTFRange +#endif + ); +} diff --git a/src/render/shaders/glsl/heatshimmerfinish.frag b/src/render/shaders/glsl/heatshimmerfinish.frag new file mode 100644 index 000000000..58430125f --- /dev/null +++ b/src/render/shaders/glsl/heatshimmerfinish.frag @@ -0,0 +1,44 @@ +#version 300 es +#define ALLOW_INCLUDES +#extension GL_ARB_shading_language_include : enable + +precision highp float; +in vec2 v_texcoord; +uniform sampler2D tex; + +uniform float noise; +uniform float brightness; +uniform float glassRefraction; +uniform float glassSize; +uniform float glassRoughness; +uniform vec2 glassPosition; +uniform float time; + +#include "defines.h" +#if USE_CM +uniform int sourceTF; +uniform int targetTF; +#include "CM.glsl" +#endif + +#include "glassFinish.glsl" + +layout(location = 0) out vec4 fragColor; + +vec2 heatShimmerNormal(vec2 position) { + vec2 warpedPosition = position; + warpedPosition.x += sin(position.y * 0.73 - time) * 0.32; + warpedPosition.y += sin(position.x * 0.41 + time) * 0.12; + + float broadPhase = warpedPosition.y * 1.15 + warpedPosition.x * 0.22 + time; + float detailPhase = warpedPosition.y * 2.37 - warpedPosition.x * 0.31 + time * 2.0; + float crossPhase = warpedPosition.x * 0.74 + warpedPosition.y * 0.41 - time; + + return vec2(cos(broadPhase) * 0.46 + cos(detailPhase) * 0.22 + sin(crossPhase) * 0.09, + cos(crossPhase) * 0.09 + sin(detailPhase) * 0.05); +} + +void main() { + vec2 position = (gl_FragCoord.xy - glassPosition) / glassSize; + fragColor = glassFinish(heatShimmerNormal(position)); +} diff --git a/src/render/shaders/glsl/prismfinish.frag b/src/render/shaders/glsl/prismfinish.frag new file mode 100644 index 000000000..cac2537da --- /dev/null +++ b/src/render/shaders/glsl/prismfinish.frag @@ -0,0 +1,103 @@ +#version 300 es +#define ALLOW_INCLUDES +#extension GL_ARB_shading_language_include : enable + +precision highp float; +in vec2 v_texcoord; +uniform sampler2D tex; +uniform sampler2D sharpTex; + +uniform float noise; +uniform float brightness; +uniform float glassRefraction; +uniform float glassSize; +uniform float glassRoughness; +uniform vec2 glassPosition; + +#include "defines.h" +#if USE_CM +uniform int sourceTF; +uniform int targetTF; +#include "CM.glsl" +#endif + +#include "blurFinish.glsl" + +layout(location = 0) out vec4 fragColor; + +const float TAU = 6.28318530718; + +float triangleWave(float phase) { + return 1.0 - abs(fract(phase) * 2.0 - 1.0); +} + +float distanceToFold(float phase) { + phase = fract(phase); + return min(min(phase, 1.0 - phase), abs(phase - 0.5)); +} + +vec2 prismSurface(vec2 position, out float edge, out float spectrumPhase) { + const vec2 AXIS_A = vec2(1.0, 0.0); + const vec2 AXIS_B = vec2(-0.5, 0.8660254); + const vec2 AXIS_C = vec2(-0.5, -0.8660254); + + vec3 phase = vec3(dot(position, AXIS_A), dot(position, AXIS_B), dot(position, AXIS_C)); + vec3 slope = 1.0 - 2.0 * step(vec3(0.5), fract(phase)); + vec2 normal = (slope.x * AXIS_A + slope.y * AXIS_B + slope.z * AXIS_C) * 0.46; + + vec2 cell = floor(vec2(phase.x, phase.y)); + vec2 variation = vec2(hash(cell + vec2(19.17, 73.41)), hash(cell + vec2(61.83, 11.29))) - 0.5; + normal += variation * 0.18; + + float foldDistance = min(distanceToFold(phase.x), min(distanceToFold(phase.y), distanceToFold(phase.z))); + float antialias = max(fwidth(foldDistance), 0.001); + edge = 1.0 - smoothstep(antialias * 0.65, antialias * 2.8, foldDistance); + + float height = triangleWave(phase.x) + triangleWave(phase.y) + triangleWave(phase.z); + spectrumPhase = fract(height * 0.19 + hash(cell) * 0.24); + return normal; +} + +vec4 prismFinish(vec2 normal, float edge, float spectrumPhase) { + normal /= max(1.0, length(normal)); + + vec2 uvStep = normal.x * dFdx(v_texcoord) + normal.y * dFdy(v_texcoord); + vec2 refraction = glassRefraction * uvStep; + vec2 texSize = vec2(textureSize(tex, 0)); + vec2 halfTexel = 0.5 / texSize; + vec2 minimumUV = halfTexel; + vec2 maximumUV = vec2(1.0) - halfTexel; + + vec2 centerUV = clamp(v_texcoord + refraction * 0.72, minimumUV, maximumUV); + float dispersion = mix(0.12, 0.28, glassRoughness); + vec2 redUV = clamp(centerUV + refraction * dispersion, minimumUV, maximumUV); + vec2 blueUV = clamp(centerUV - refraction * dispersion, minimumUV, maximumUV); + + vec4 blurred = texture(tex, centerUV); + vec4 sharpCenter = texture(sharpTex, centerUV); + vec3 dispersed = vec3(texture(sharpTex, redUV).r, sharpCenter.g, texture(sharpTex, blueUV).b); + float clarity = smoothstep(0.0, 6.0, glassRefraction) * mix(0.24, 0.42, glassRoughness); + vec4 pixColor = vec4(mix(blurred.rgb, dispersed, clarity), blurred.a); + + const vec2 LIGHT_DIRECTION = vec2(-0.451219, 0.892413); + float emboss = dot(normal, LIGHT_DIRECTION); + float highlight = edge * glassRoughness * (0.018 + 0.055 * max(emboss, 0.0)); + vec3 spectrum = 0.58 + 0.42 * cos(TAU * (spectrumPhase + vec3(0.0, 0.333333, 0.666667))); + pixColor.rgb *= 1.0 + emboss * glassRoughness * 0.1; + pixColor.rgb += spectrum * highlight * pixColor.a; + + return blurFinish(pixColor, v_texcoord, noise, brightness +#if USE_CM + , + sourceTF, targetTF, convertMatrix, srcTFRange, dstTFRange +#endif + ); +} + +void main() { + vec2 position = (gl_FragCoord.xy - glassPosition) / glassSize; + float edge; + float spectrumPhase; + vec2 normal = prismSurface(position, edge, spectrumPhase); + fragColor = prismFinish(normal, edge, spectrumPhase); +} diff --git a/src/render/shaders/glsl/ripplefinish.frag b/src/render/shaders/glsl/ripplefinish.frag new file mode 100644 index 000000000..a1628e8ec --- /dev/null +++ b/src/render/shaders/glsl/ripplefinish.frag @@ -0,0 +1,72 @@ +#version 300 es +#define ALLOW_INCLUDES +#extension GL_ARB_shading_language_include : enable + +precision highp float; +in vec2 v_texcoord; +uniform sampler2D tex; + +uniform float noise; +uniform float brightness; + +const int MAX_RIPPLE_IMPULSES = 256; +uniform int rippleCount; +uniform vec4 rippleImpulses[MAX_RIPPLE_IMPULSES]; +uniform vec4 rippleParams; + +#include "defines.h" +#if USE_CM +uniform int sourceTF; +uniform int targetTF; +#include "CM.glsl" +#endif + +#include "blurFinish.glsl" + +layout(location = 0) out vec4 fragColor; + +void main() { + vec2 texSize = vec2(textureSize(tex, 0)); + vec2 position = v_texcoord * texSize; + vec2 displacement = vec2(0.0); + + float duration = max(rippleParams.x, 0.001); + float maximumRadius = max(rippleParams.y, 1.0); + float waveWidth = max(rippleParams.z, 1.0); + float amplitude = max(rippleParams.w, 0.0); + + for (int i = 0; i < MAX_RIPPLE_IMPULSES; ++i) { + if (i >= rippleCount) + break; + + float progress = clamp(rippleImpulses[i].z / duration, 0.0, 1.0); + vec2 delta = position - rippleImpulses[i].xy; + float distance = length(delta); + vec2 direction = delta / max(distance, 1.0); + + float waveRadius = maximumRadius * progress; + float signedDistance = distance - waveRadius; + float distanceFromWave = abs(signedDistance); + if (distanceFromWave >= waveWidth) + continue; + + float envelope = 0.5 + 0.5 * cos(3.14159265359 * distanceFromWave / waveWidth); + float wave = cos(6.28318530718 * signedDistance / waveWidth); + displacement += direction * wave * envelope * (1.0 - progress); + } + + float displacementLength = length(displacement); + if (displacementLength > 1.0) + displacement /= displacementLength; + displacement *= amplitude; + + vec2 displacedUV = clamp(v_texcoord + displacement / texSize, vec2(0.0), vec2(1.0)); + vec4 pixColor = texture(tex, displacedUV); + + fragColor = blurFinish(pixColor, v_texcoord, noise, brightness +#if USE_CM + , + sourceTF, targetTF, convertMatrix, srcTFRange, dstTFRange +#endif + ); +} diff --git a/src/render/shaders/glsl/waterfinish.frag b/src/render/shaders/glsl/waterfinish.frag new file mode 100644 index 000000000..642d3adff --- /dev/null +++ b/src/render/shaders/glsl/waterfinish.frag @@ -0,0 +1,57 @@ +#version 300 es +#define ALLOW_INCLUDES +#extension GL_ARB_shading_language_include : enable + +precision highp float; +in vec2 v_texcoord; + +uniform sampler2D tex; +uniform sampler2D waterStateTex; +uniform int waterEnabled; +uniform vec2 waterTexelSize; +uniform vec4 waterExtent; +uniform float waterRefraction; +uniform float noise; +uniform float brightness; + +#include "defines.h" +#if USE_CM +uniform int sourceTF; +uniform int targetTF; +#include "CM.glsl" +#endif + +#include "blurFinish.glsl" + +layout(location = 0) out vec4 fragColor; + +float waterHeight(vec2 uv) { + return texture(waterStateTex, clamp(uv, vec2(0.0), vec2(1.0))).r * 2.0 - 1.0; +} + +void main() { + vec2 displacedUV = v_texcoord; + if (waterEnabled != 0) { + vec2 position = v_texcoord * vec2(textureSize(tex, 0)); + vec2 waterUV = (position - waterExtent.xy) / waterExtent.zw; + if (all(greaterThanEqual(waterUV, vec2(0.0))) && all(lessThanEqual(waterUV, vec2(1.0)))) { + float left = waterHeight(waterUV - vec2(waterTexelSize.x, 0.0)); + float right = waterHeight(waterUV + vec2(waterTexelSize.x, 0.0)); + float up = waterHeight(waterUV - vec2(0.0, waterTexelSize.y)); + float down = waterHeight(waterUV + vec2(0.0, waterTexelSize.y)); + vec2 gradient = vec2(left - right, up - down); + float gradientLength = length(gradient); + if (gradientLength > 1.0) + gradient /= gradientLength; + vec2 texSize = vec2(textureSize(tex, 0)); + displacedUV = clamp(v_texcoord + gradient * waterRefraction / texSize, vec2(0.0), vec2(1.0)); + } + } + + fragColor = blurFinish(texture(tex, displacedUV), v_texcoord, noise, brightness +#if USE_CM + , + sourceTF, targetTF, convertMatrix, srcTFRange, dstTFRange +#endif + ); +} diff --git a/src/render/shaders/glsl/waterstep.frag b/src/render/shaders/glsl/waterstep.frag new file mode 100644 index 000000000..3bdfb9f5b --- /dev/null +++ b/src/render/shaders/glsl/waterstep.frag @@ -0,0 +1,60 @@ +#version 300 es +#define ALLOW_INCLUDES +#extension GL_ARB_shading_language_include : enable + +precision highp float; +in vec2 v_texcoord; + +uniform sampler2D waterStateTex; +uniform vec2 waterTexelSize; +uniform vec4 waterParams; + +const int MAX_WATER_IMPULSES = 16; +uniform int waterImpulseCount; +uniform vec4 waterImpulses[MAX_WATER_IMPULSES]; + +layout(location = 0) out vec4 fragColor; + +float decodeState(float value) { + return value * 2.0 - 1.0; +} + +float encodeState(float value) { + return clamp(value * 0.5 + 0.5, 0.0, 1.0); +} + +void main() { + vec2 state = texture(waterStateTex, v_texcoord).rg; + float height = decodeState(state.r); + float velocity = decodeState(state.g); + + float left = decodeState(texture(waterStateTex, v_texcoord - vec2(waterTexelSize.x, 0.0)).r); + float right = decodeState(texture(waterStateTex, v_texcoord + vec2(waterTexelSize.x, 0.0)).r); + float up = decodeState(texture(waterStateTex, v_texcoord - vec2(0.0, waterTexelSize.y)).r); + float down = decodeState(texture(waterStateTex, v_texcoord + vec2(0.0, waterTexelSize.y)).r); + + float frameStep = clamp(waterParams.x * 60.0, 0.0, 3.0); + float propagation = waterParams.y * 0.24; + float damping = pow(waterParams.z, frameStep); + float laplacian = left + right + up + down - 4.0 * height; + + velocity = (velocity + laplacian * propagation * frameStep) * damping; + height += velocity * frameStep; + + float edgeDistance = min(min(v_texcoord.x, 1.0 - v_texcoord.x), min(v_texcoord.y, 1.0 - v_texcoord.y)); + float edgeFade = smoothstep(0.0, 0.04, edgeDistance); + velocity *= edgeFade; + height *= mix(0.9, 1.0, edgeFade); + + for (int i = 0; i < MAX_WATER_IMPULSES; ++i) { + if (i >= waterImpulseCount) + break; + + vec2 delta = v_texcoord - waterImpulses[i].xy; + float radius = max(waterImpulses[i].z, 0.0001); + float influence = exp(-dot(delta, delta) / (radius * radius)); + height += influence * waterImpulses[i].w; + } + + fragColor = vec4(encodeState(height), encodeState(velocity), 0.0, 1.0); +} diff --git a/tests/config/BlurVariants.cpp b/tests/config/BlurVariants.cpp new file mode 100644 index 000000000..667404238 --- /dev/null +++ b/tests/config/BlurVariants.cpp @@ -0,0 +1,255 @@ +#include +#include + +#include + +#include +#include +#include + +using namespace Config::Values; +using namespace Render; + +TEST(Config, blurVariantsMatchRendererTypes) { + const auto VALUE = std::ranges::find_if(CONFIG_VALUES, [](const auto& value) { return std::string_view{value->name()} == "decoration:blur:variant"; }); + ASSERT_NE(VALUE, CONFIG_VALUES.end()); + + const auto INT_VALUE = dynamicPointerCast(*VALUE); + ASSERT_TRUE(INT_VALUE); + ASSERT_TRUE(INT_VALUE->m_min.has_value()); + ASSERT_TRUE(INT_VALUE->m_max.has_value()); + ASSERT_TRUE(INT_VALUE->m_map.has_value()); + + EXPECT_EQ(INT_VALUE->defaultVal(), sc(eBlurType::BLUR_DUAL_KAWASE)); + EXPECT_EQ(*INT_VALUE->m_min, sc(eBlurType::BLUR_DUAL_KAWASE)); + EXPECT_EQ(*INT_VALUE->m_max, sc(eBlurType::BLUR_HAZE)); + + const auto& MAP = *INT_VALUE->m_map; + EXPECT_EQ(MAP.size(), 11); + EXPECT_EQ(MAP.at("kawase"), sc(eBlurType::BLUR_DUAL_KAWASE)); + EXPECT_EQ(MAP.at("frost"), sc(eBlurType::BLUR_FROST)); + EXPECT_FALSE(MAP.contains("fluted")); + EXPECT_FALSE(MAP.contains("hammered")); + EXPECT_EQ(MAP.at("ripple"), sc(eBlurType::BLUR_RIPPLE)); + EXPECT_EQ(MAP.at("drops"), sc(eBlurType::BLUR_DROPS)); + EXPECT_EQ(MAP.at("water"), sc(eBlurType::BLUR_WATER)); + EXPECT_EQ(MAP.at("fluid_jar"), sc(eBlurType::BLUR_FLUID_JAR)); + EXPECT_EQ(MAP.at("prism"), sc(eBlurType::BLUR_PRISM)); + EXPECT_EQ(MAP.at("heat_shimmer"), sc(eBlurType::BLUR_HEAT_SHIMMER)); + EXPECT_EQ(MAP.at("acrylic"), sc(eBlurType::BLUR_ACRYLIC)); + EXPECT_EQ(MAP.at("aurora"), sc(eBlurType::BLUR_AURORA)); + EXPECT_EQ(MAP.at("haze"), sc(eBlurType::BLUR_HAZE)); + EXPECT_EQ(INT_VALUE->refreshBits(), Config::Supplementary::REFRESH_BLUR_FB); +} + +TEST(Config, dropsAnimationSpeedIsBounded) { + const auto VALUE = std::ranges::find_if(CONFIG_VALUES, [](const auto& value) { return std::string_view{value->name()} == "decoration:blur:drops:speed"; }); + ASSERT_NE(VALUE, CONFIG_VALUES.end()); + + const auto FLOAT_VALUE = dynamicPointerCast(*VALUE); + ASSERT_TRUE(FLOAT_VALUE); + ASSERT_TRUE(FLOAT_VALUE->m_min.has_value()); + ASSERT_TRUE(FLOAT_VALUE->m_max.has_value()); + + EXPECT_FLOAT_EQ(FLOAT_VALUE->defaultVal(), 3.F); + EXPECT_FLOAT_EQ(*FLOAT_VALUE->m_min, 0.F); + EXPECT_FLOAT_EQ(*FLOAT_VALUE->m_max, 10.F); +} + +TEST(Config, heatShimmerAnimationSpeedIsBounded) { + const auto VALUE = std::ranges::find_if(CONFIG_VALUES, [](const auto& value) { return std::string_view{value->name()} == "decoration:blur:heat_shimmer:speed"; }); + ASSERT_NE(VALUE, CONFIG_VALUES.end()); + + const auto FLOAT_VALUE = dynamicPointerCast(*VALUE); + ASSERT_TRUE(FLOAT_VALUE); + ASSERT_TRUE(FLOAT_VALUE->m_min.has_value()); + ASSERT_TRUE(FLOAT_VALUE->m_max.has_value()); + + EXPECT_FLOAT_EQ(FLOAT_VALUE->defaultVal(), 1.F); + EXPECT_FLOAT_EQ(*FLOAT_VALUE->m_min, 0.F); + EXPECT_FLOAT_EQ(*FLOAT_VALUE->m_max, 10.F); + EXPECT_EQ(FLOAT_VALUE->refreshBits(), Config::Supplementary::REFRESH_BLUR_FB); +} + +TEST(Config, auroraSettingsAreBounded) { + struct SAuroraSetting { + std::string_view name; + float defaultValue; + float min; + float max; + }; + + constexpr std::array SETTINGS = { + SAuroraSetting{"decoration:blur:aurora:speed", 1.F, 0.F, 10.F}, + SAuroraSetting{"decoration:blur:aurora:intensity", 0.35F, 0.F, 1.F}, + }; + + for (const auto& setting : SETTINGS) { + const auto VALUE = std::ranges::find_if(CONFIG_VALUES, [&setting](const auto& value) { return std::string_view{value->name()} == setting.name; }); + ASSERT_NE(VALUE, CONFIG_VALUES.end()); + + const auto FLOAT_VALUE = dynamicPointerCast(*VALUE); + ASSERT_TRUE(FLOAT_VALUE); + ASSERT_TRUE(FLOAT_VALUE->m_min.has_value()); + ASSERT_TRUE(FLOAT_VALUE->m_max.has_value()); + + EXPECT_FLOAT_EQ(FLOAT_VALUE->defaultVal(), setting.defaultValue); + EXPECT_FLOAT_EQ(*FLOAT_VALUE->m_min, setting.min); + EXPECT_FLOAT_EQ(*FLOAT_VALUE->m_max, setting.max); + EXPECT_EQ(FLOAT_VALUE->refreshBits(), Config::Supplementary::REFRESH_BLUR_FB); + } + + struct SAuroraColor { + std::string_view name; + int64_t defaultValue; + }; + + constexpr std::array COLORS = { + SAuroraColor{"decoration:blur:aurora:color1", 0x29F0A0FF}, + SAuroraColor{"decoration:blur:aurora:color2", 0x7A4DFFFF}, + }; + + for (const auto& color : COLORS) { + const auto VALUE = std::ranges::find_if(CONFIG_VALUES, [&color](const auto& value) { return std::string_view{value->name()} == color.name; }); + ASSERT_NE(VALUE, CONFIG_VALUES.end()); + + const auto COLOR_VALUE = dynamicPointerCast(*VALUE); + ASSERT_TRUE(COLOR_VALUE); + EXPECT_EQ(COLOR_VALUE->defaultVal(), color.defaultValue); + EXPECT_EQ(COLOR_VALUE->refreshBits(), Config::Supplementary::REFRESH_BLUR_FB); + } +} + +TEST(Config, hazeSettingsAreBounded) { + struct SHazeSetting { + std::string_view name; + float defaultValue; + }; + + constexpr std::array SETTINGS = { + SHazeSetting{"decoration:blur:haze:intensity", 0.35F}, + SHazeSetting{"decoration:blur:haze:iridescence", 0.7F}, + }; + + for (const auto& setting : SETTINGS) { + const auto VALUE = std::ranges::find_if(CONFIG_VALUES, [&setting](const auto& value) { return std::string_view{value->name()} == setting.name; }); + ASSERT_NE(VALUE, CONFIG_VALUES.end()); + + const auto FLOAT_VALUE = dynamicPointerCast(*VALUE); + ASSERT_TRUE(FLOAT_VALUE); + ASSERT_TRUE(FLOAT_VALUE->m_min.has_value()); + ASSERT_TRUE(FLOAT_VALUE->m_max.has_value()); + + EXPECT_FLOAT_EQ(FLOAT_VALUE->defaultVal(), setting.defaultValue); + EXPECT_FLOAT_EQ(*FLOAT_VALUE->m_min, 0.F); + EXPECT_FLOAT_EQ(*FLOAT_VALUE->m_max, 1.F); + EXPECT_EQ(FLOAT_VALUE->refreshBits(), Config::Supplementary::REFRESH_BLUR_FB); + } +} + +TEST(Config, acrylicSettingsAreBounded) { + struct SAcrylicSetting { + std::string_view name; + float defaultValue; + float min; + float max; + }; + + constexpr std::array SETTINGS = { + SAcrylicSetting{"decoration:blur:acrylic:refraction", 24.F, 0.F, 48.F}, + SAcrylicSetting{"decoration:blur:acrylic:bulb", 48.F, 4.F, 256.F}, + SAcrylicSetting{"decoration:blur:acrylic:clarity", 0.82F, 0.F, 1.F}, + SAcrylicSetting{"decoration:blur:acrylic:aberration", 0.025F, 0.F, 0.25F}, + }; + + for (const auto& setting : SETTINGS) { + const auto VALUE = std::ranges::find_if(CONFIG_VALUES, [&setting](const auto& value) { return std::string_view{value->name()} == setting.name; }); + ASSERT_NE(VALUE, CONFIG_VALUES.end()); + + const auto FLOAT_VALUE = dynamicPointerCast(*VALUE); + ASSERT_TRUE(FLOAT_VALUE); + ASSERT_TRUE(FLOAT_VALUE->m_min.has_value()); + ASSERT_TRUE(FLOAT_VALUE->m_max.has_value()); + + EXPECT_FLOAT_EQ(FLOAT_VALUE->defaultVal(), setting.defaultValue); + EXPECT_FLOAT_EQ(*FLOAT_VALUE->m_min, setting.min); + EXPECT_FLOAT_EQ(*FLOAT_VALUE->m_max, setting.max); + EXPECT_EQ(FLOAT_VALUE->refreshBits(), Config::Supplementary::REFRESH_BLUR_FB); + } + + const auto TINT = std::ranges::find_if(CONFIG_VALUES, [](const auto& value) { return std::string_view{value->name()} == "decoration:blur:acrylic:tint"; }); + ASSERT_NE(TINT, CONFIG_VALUES.end()); + + const auto COLOR_VALUE = dynamicPointerCast(*TINT); + ASSERT_TRUE(COLOR_VALUE); + EXPECT_EQ(COLOR_VALUE->defaultVal(), 0x14EEF5FF); + EXPECT_EQ(COLOR_VALUE->refreshBits(), Config::Supplementary::REFRESH_BLUR_FB); +} + +TEST(Config, waterSettingsAreBounded) { + struct SWaterSetting { + std::string_view name; + float defaultValue; + float min; + float max; + }; + + constexpr std::array SETTINGS = { + SWaterSetting{"decoration:blur:water:strength", 32.F, 0.F, 32.F}, SWaterSetting{"decoration:blur:water:radius", 20.F, 1.F, 1000.F}, + SWaterSetting{"decoration:blur:water:speed", 0.76F, 0.F, 10.F}, SWaterSetting{"decoration:blur:water:damping", 0.95F, 0.F, 1.F}, + SWaterSetting{"decoration:blur:water:duration", 12.F, 0.5F, 60.F}, + }; + + for (const auto& setting : SETTINGS) { + const auto VALUE = std::ranges::find_if(CONFIG_VALUES, [&setting](const auto& value) { return std::string_view{value->name()} == setting.name; }); + ASSERT_NE(VALUE, CONFIG_VALUES.end()); + + const auto FLOAT_VALUE = dynamicPointerCast(*VALUE); + ASSERT_TRUE(FLOAT_VALUE); + ASSERT_TRUE(FLOAT_VALUE->m_min.has_value()); + ASSERT_TRUE(FLOAT_VALUE->m_max.has_value()); + + EXPECT_FLOAT_EQ(FLOAT_VALUE->defaultVal(), setting.defaultValue); + EXPECT_FLOAT_EQ(*FLOAT_VALUE->m_min, setting.min); + EXPECT_FLOAT_EQ(*FLOAT_VALUE->m_max, setting.max); + EXPECT_EQ(FLOAT_VALUE->refreshBits(), Config::Supplementary::REFRESH_BLUR_FB); + } +} + +TEST(Config, fluidJarSettingsAreBounded) { + const auto COLOR = std::ranges::find_if(CONFIG_VALUES, [](const auto& value) { return std::string_view{value->name()} == "decoration:blur:fluid_jar:color"; }); + ASSERT_NE(COLOR, CONFIG_VALUES.end()); + + const auto COLOR_VALUE = dynamicPointerCast(*COLOR); + ASSERT_TRUE(COLOR_VALUE); + EXPECT_EQ(COLOR_VALUE->defaultVal(), 0xCC3399FF); + EXPECT_EQ(COLOR_VALUE->refreshBits(), Config::Supplementary::REFRESH_BLUR_FB); + + struct SFluidJarSetting { + std::string_view name; + float defaultValue; + float min; + float max; + }; + + constexpr std::array SETTINGS = { + SFluidJarSetting{"decoration:blur:fluid_jar:speed", 3.7F, 0.F, 10.F}, SFluidJarSetting{"decoration:blur:fluid_jar:fill_amount", 0.5F, 0.F, 1.F}, + SFluidJarSetting{"decoration:blur:fluid_jar:mass", 1.4F, 0.1F, 10.F}, SFluidJarSetting{"decoration:blur:fluid_jar:precision", 2.F, 0.5F, 8.F}, + SFluidJarSetting{"decoration:blur:fluid_jar:turbulence", 1.2F, 0.F, 5.F}, SFluidJarSetting{"decoration:blur:fluid_jar:distortion", 8.F, 0.F, 10.F}, + }; + + for (const auto& setting : SETTINGS) { + const auto VALUE = std::ranges::find_if(CONFIG_VALUES, [&setting](const auto& value) { return std::string_view{value->name()} == setting.name; }); + ASSERT_NE(VALUE, CONFIG_VALUES.end()); + + const auto FLOAT_VALUE = dynamicPointerCast(*VALUE); + ASSERT_TRUE(FLOAT_VALUE); + ASSERT_TRUE(FLOAT_VALUE->m_min.has_value()); + ASSERT_TRUE(FLOAT_VALUE->m_max.has_value()); + + EXPECT_FLOAT_EQ(FLOAT_VALUE->defaultVal(), setting.defaultValue); + EXPECT_FLOAT_EQ(*FLOAT_VALUE->m_min, setting.min); + EXPECT_FLOAT_EQ(*FLOAT_VALUE->m_max, setting.max); + EXPECT_EQ(FLOAT_VALUE->refreshBits(), Config::Supplementary::REFRESH_BLUR_FB); + } +} diff --git a/tests/render/BackdropScopePassElement.cpp b/tests/render/BackdropScopePassElement.cpp new file mode 100644 index 000000000..6f7351d87 --- /dev/null +++ b/tests/render/BackdropScopePassElement.cpp @@ -0,0 +1,103 @@ +#include +#include +#include + +#include + +TEST(BackdropScopePassElement, MarkersAreBalancedPassMetadata) { + const auto scope = makeShared(); + CBackdropScopePassElement begin{CBackdropScopePassElement::eAction::BEGIN, scope}; + CBackdropScopePassElement end{CBackdropScopePassElement::eAction::END, scope}; + + EXPECT_EQ(begin.action(), CBackdropScopePassElement::eAction::BEGIN); + EXPECT_EQ(end.action(), CBackdropScopePassElement::eAction::END); + EXPECT_EQ(begin.scope(), scope); + EXPECT_EQ(end.scope(), scope); + EXPECT_EQ(begin.type(), EK_BACKDROP_SCOPE); + EXPECT_EQ(end.type(), EK_BACKDROP_SCOPE); +} + +TEST(BackdropScopePassElement, MarkersSurviveSimplificationWithoutRequestingBlur) { + const auto scope = makeShared(); + CBackdropScopePassElement marker{CBackdropScopePassElement::eAction::BEGIN, scope}; + + EXPECT_TRUE(marker.undiscardable()); + EXPECT_FALSE(marker.needsLiveBlur()); + EXPECT_FALSE(marker.needsPrecomputeBlur()); + EXPECT_FALSE(marker.disableSimplification()); + EXPECT_FALSE(marker.boundingBox().has_value()); + EXPECT_TRUE(marker.opaqueRegion().empty()); +} + +TEST(BackdropScopePlanner, ActivatesOnlyInnermostScopeAndClipsDamage) { + CBackdropScopePlanner planner; + const auto outer = makeShared(); + const auto inner = makeShared(); + + planner.begin(outer); + planner.begin(inner); + planner.addLiveBlur(CRegion{80, 80, 40, 40}); + planner.end(inner, CBox{0, 0, 100, 100}); + planner.end(outer, CBox{0, 0, 100, 100}); + + EXPECT_FALSE(outer->required); + EXPECT_TRUE(outer->damage.empty()); + EXPECT_TRUE(inner->required); + EXPECT_EQ(inner->damage.getExtents(), CBox(80, 80, 20, 20)); + EXPECT_TRUE(planner.empty()); +} + +TEST(BackdropScopePlanner, UnionsLiveBlurDamageWithinScope) { + CBackdropScopePlanner planner; + const auto scope = makeShared(); + + planner.begin(scope); + planner.addLiveBlur(CRegion{10, 20, 30, 40}); + planner.addLiveBlur(CRegion{50, 60, 20, 10}); + planner.end(scope, CBox{0, 0, 100, 100}); + + EXPECT_TRUE(scope->required); + EXPECT_EQ(scope->damage.getExtents(), CBox(10, 20, 60, 50)); +} + +TEST(BackdropScopePlanner, TransformedWindowReportsNestedLiveBlur) { + auto nestedPass = makeUnique(); + nestedPass->add(makeUnique(CTexPassElement::SRenderData{ + .blur = true, + .blockBlurOptimization = true, + })); + + CTransformedWindowPassElement transformed{CTransformedWindowPassElement::SData{.pass = std::move(nestedPass)}}; + EXPECT_TRUE(transformed.needsLiveBlur()); +} + +TEST(BackdropScopePlanner, TransformedWindowReportsNestedPrecomputedBlur) { + auto nestedPass = makeUnique(); + nestedPass->add(makeUnique(CTexPassElement::SRenderData{ + .blur = true, + .liveBlurOverride = false, + })); + + CTransformedWindowPassElement transformed{CTransformedWindowPassElement::SData{.pass = std::move(nestedPass)}}; + EXPECT_TRUE(transformed.needsPrecomputeBlur()); +} + +TEST(BackdropScopePlanner, TransformedWindowPreservesLiveBlurMode) { + CTransformedWindowPassElement transformed{CTransformedWindowPassElement::SData{ + .blur = true, + .blurUsesLive = true, + }}; + + EXPECT_TRUE(transformed.needsLiveBlur()); + EXPECT_FALSE(transformed.needsPrecomputeBlur()); +} + +TEST(BackdropScopePlanner, TransformedWindowPreservesPrecomputedBlurMode) { + CTransformedWindowPassElement transformed{CTransformedWindowPassElement::SData{ + .blur = true, + .blurUsesLive = false, + }}; + + EXPECT_FALSE(transformed.needsLiveBlur()); + EXPECT_TRUE(transformed.needsPrecomputeBlur()); +} diff --git a/tests/render/BlurDamage.cpp b/tests/render/BlurDamage.cpp new file mode 100644 index 000000000..aafb27946 --- /dev/null +++ b/tests/render/BlurDamage.cpp @@ -0,0 +1,291 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace Render::GL; + +TEST(BlurMaterial, DefaultUsesPlainFinish) { + const CDefaultBlurMaterial material; + const auto requirements = material.requirements(); + + EXPECT_EQ(material.type(), Render::eBlurType::BLUR_DUAL_KAWASE); + EXPECT_EQ(requirements.finishFragment, Render::SH_FRAG_BLURFINISH); + EXPECT_FALSE(requirements.preparedInput); + EXPECT_FALSE(requirements.liveBlur); + EXPECT_FALSE(material.isAnimated()); + EXPECT_EQ(material.blurSizeForDamage(100), 40); + EXPECT_FLOAT_EQ(material.sampleRadius(), 0.F); +} + +TEST(BlurMaterial, GlassCapabilitiesAreConfiguredByMaterial) { + const CGlassBlurMaterial frost(Render::eBlurType::BLUR_FROST, Render::SH_FRAG_FROSTFINISH); + const auto frostRequirements = frost.requirements(); + EXPECT_EQ(frost.type(), Render::eBlurType::BLUR_FROST); + EXPECT_EQ(frostRequirements.finishFragment, Render::SH_FRAG_FROSTFINISH); + EXPECT_FALSE(frostRequirements.preparedInput); + + const CPrismBlurMaterial prism; + const auto prismRequirements = prism.requirements(); + EXPECT_EQ(prism.type(), Render::eBlurType::BLUR_PRISM); + EXPECT_EQ(prismRequirements.finishFragment, Render::SH_FRAG_PRISMFINISH); + EXPECT_TRUE(prismRequirements.preparedInput); +} + +TEST(BlurMaterial, HeatShimmerUsesAnimatedGlassFinish) { + const CHeatShimmerBlurMaterial heatShimmer; + const auto requirements = heatShimmer.requirements(); + + EXPECT_EQ(heatShimmer.type(), Render::eBlurType::BLUR_HEAT_SHIMMER); + EXPECT_EQ(requirements.finishFragment, Render::SH_FRAG_HEATSHIMMERFINISH); + EXPECT_FALSE(requirements.preparedInput); + EXPECT_FALSE(requirements.liveBlur); +} + +TEST(BlurMaterial, AuroraUsesAnimatedGlassFinish) { + const CAuroraBlurMaterial aurora; + const auto requirements = aurora.requirements(); + + EXPECT_EQ(aurora.type(), Render::eBlurType::BLUR_AURORA); + EXPECT_EQ(requirements.finishFragment, Render::SH_FRAG_AURORAFINISH); + EXPECT_FALSE(requirements.preparedInput); + EXPECT_FALSE(requirements.liveBlur); +} + +TEST(BlurMaterial, HazeUsesStaticPearlescentFinish) { + const CHazeBlurMaterial haze; + const auto requirements = haze.requirements(); + + EXPECT_EQ(haze.type(), Render::eBlurType::BLUR_HAZE); + EXPECT_EQ(requirements.finishFragment, Render::SH_FRAG_HAZEFINISH); + EXPECT_FALSE(requirements.preparedInput); + EXPECT_FALSE(requirements.liveBlur); + EXPECT_FALSE(haze.isAnimated()); + EXPECT_EQ(haze.blurSizeForDamage(100), 40); + EXPECT_FLOAT_EQ(haze.sampleRadius(), 0.F); +} + +TEST(BlurMaterial, AcrylicUsesPreparedLiveFinish) { + const CAcrylicBlurMaterial acrylic; + const auto requirements = acrylic.requirements(); + + EXPECT_EQ(acrylic.type(), Render::eBlurType::BLUR_ACRYLIC); + EXPECT_EQ(requirements.finishFragment, Render::SH_FRAG_ACRYLICFINISH); + EXPECT_TRUE(requirements.preparedInput); + EXPECT_TRUE(requirements.liveBlur); + EXPECT_FALSE(acrylic.isAnimated()); +} + +TEST(BlurDamage, DualKawaseUsesOperationalMinimums) { + EXPECT_FLOAT_EQ(dualKawaseDamageRadius(0, 0), 2.F); + EXPECT_FLOAT_EQ(dualKawaseDamageRadius(-10, -10), 2.F); +} + +TEST(BlurDamage, DualKawaseCalculatesConfiguredRadius) { + EXPECT_FLOAT_EQ(dualKawaseDamageRadius(8, 1), 16.F); + EXPECT_FLOAT_EQ(dualKawaseDamageRadius(8, 2), 48.F); + EXPECT_FLOAT_EQ(dualKawaseDamageRadius(12, 3), 168.F); +} + +TEST(BlurDamage, DualKawaseUsesOperationalMaximums) { + EXPECT_FLOAT_EQ(dualKawaseDamageRadius(40, 8), 20400.F); + EXPECT_FLOAT_EQ(dualKawaseDamageRadius(100, 10), 51000.F); +} + +TEST(BlurDamage, GlassIncludesRefractionReach) { + EXPECT_FLOAT_EQ(glassDamageRadius(8, 1, 3.F), 19.F); + EXPECT_FLOAT_EQ(glassDamageRadius(12, 3, 4.25F), 173.F); +} + +TEST(BlurDamage, GlassClampsRefractionReach) { + EXPECT_FLOAT_EQ(glassDamageRadius(8, 1, -1.F), 16.F); + EXPECT_FLOAT_EQ(glassDamageRadius(8, 1, 100.F), 36.F); +} + +TEST(BlurDamage, AcrylicIncludesFilteredRefractionReach) { + EXPECT_FLOAT_EQ(acrylicDamageRadius(8, 1, 3.F), 20.F); + EXPECT_FLOAT_EQ(acrylicDamageRadius(12, 3, 4.25F), 174.F); +} + +TEST(BlurDamage, AcrylicClampsRefractionReach) { + EXPECT_FLOAT_EQ(acrylicDamageRadius(8, 1, -1.F), 16.F); + EXPECT_FLOAT_EQ(acrylicDamageRadius(8, 1, 100.F), 65.F); +} + +TEST(BlurDamage, RippleIncludesBoundedDisplacement) { + EXPECT_FLOAT_EQ(rippleDamageRadius(8, 1, 6.F), 22.F); + EXPECT_FLOAT_EQ(rippleDamageRadius(8, 1, -1.F), 16.F); + EXPECT_FLOAT_EQ(rippleDamageRadius(8, 1, 100.F), 48.F); +} + +TEST(BlurDamage, RippleOutputReachIncludesWaveWidth) { + EXPECT_FLOAT_EQ(rippleOutputReach(180.F, 24.F), 204.F); + EXPECT_FLOAT_EQ(rippleOutputReach(10.25F, 2.25F), 13.F); + EXPECT_FLOAT_EQ(rippleOutputReach(-1.F, -1.F), 0.F); +} + +TEST(BlurDamage, WaterIncludesBoundedDisplacement) { + EXPECT_FLOAT_EQ(waterDamageRadius(8, 1, 6.F), 22.F); + EXPECT_FLOAT_EQ(waterDamageRadius(8, 1, -1.F), 16.F); + EXPECT_FLOAT_EQ(waterDamageRadius(8, 1, 100.F), 48.F); +} + +TEST(FluidJar, SimulationSizePreservesAspectAndBoundsParticles) { + const auto wide = fluidJarSimulationSize({1920, 1080}); + EXPECT_EQ(wide, Vector2D(256, 144)); + EXPECT_LE(fluidJarParticleCapacity(wide), 2048); + + const auto square = fluidJarSimulationSize({1000, 1000}); + EXPECT_EQ(square, Vector2D(250, 250)); + EXPECT_LE(fluidJarParticleCapacity(square), 2048); +} + +TEST(FluidJar, DamageIncludesBoundedRefraction) { + EXPECT_FLOAT_EQ(fluidJarDamageRadius(8, 1), 24.F); + EXPECT_FLOAT_EQ(fluidJarDamageRadius(8, 2), 56.F); + EXPECT_FLOAT_EQ(fluidJarDamageRadius(8, 1, 0.F), 16.F); + EXPECT_FLOAT_EQ(fluidJarDamageRadius(8, 1, 2.F), 32.F); + EXPECT_FLOAT_EQ(fluidJarDamageRadius(8, 1, 4.F), 48.F); + EXPECT_FLOAT_EQ(fluidJarDamageRadius(8, 1, 10.F), 96.F); +} + +TEST(FluidJar, OutputTransformsMapFramebufferToLogicalCoordinates) { + const std::array expected = { + SFluidJarOutputTransform{.xAxis = {1, 0}, .yAxis = {0, 1}, .offset = {0, 0}}, SFluidJarOutputTransform{.xAxis = {0, -1}, .yAxis = {1, 0}, .offset = {0, 1}}, + SFluidJarOutputTransform{.xAxis = {-1, 0}, .yAxis = {0, -1}, .offset = {1, 1}}, SFluidJarOutputTransform{.xAxis = {0, 1}, .yAxis = {-1, 0}, .offset = {1, 0}}, + SFluidJarOutputTransform{.xAxis = {-1, 0}, .yAxis = {0, 1}, .offset = {1, 0}}, SFluidJarOutputTransform{.xAxis = {0, 1}, .yAxis = {1, 0}, .offset = {0, 0}}, + SFluidJarOutputTransform{.xAxis = {1, 0}, .yAxis = {0, -1}, .offset = {0, 1}}, SFluidJarOutputTransform{.xAxis = {0, -1}, .yAxis = {-1, 0}, .offset = {1, 1}}, + }; + + for (size_t i = 0; i < expected.size(); ++i) { + const auto transform = fluidJarOutputTransform(sc(i)); + EXPECT_EQ(transform.xAxis, expected[i].xAxis); + EXPECT_EQ(transform.yAxis, expected[i].yAxis); + EXPECT_EQ(transform.offset, expected[i].offset); + } +} + +TEST(FluidJar, OutputTransformsKeepTheFloorAtLogicalBottom) { + const std::array outputBottomCenters = { + Vector2D{0.5, 1.0}, Vector2D{0.0, 0.5}, Vector2D{0.5, 0.0}, Vector2D{1.0, 0.5}, Vector2D{0.5, 1.0}, Vector2D{1.0, 0.5}, Vector2D{0.5, 0.0}, Vector2D{0.0, 0.5}, + }; + + for (size_t i = 0; i < outputBottomCenters.size(); ++i) { + const auto transform = fluidJarOutputTransform(sc(i)); + const auto point = outputBottomCenters[i]; + const auto logical = transform.xAxis * point.x + transform.yAxis * point.y + transform.offset; + EXPECT_EQ(logical, Vector2D(0.5, 1.0)); + } +} + +TEST(FluidJar, OutputTransformVectorsRoundTrip) { + constexpr Vector2D LOGICAL_VECTOR = {0.25, -0.75}; + + for (int i = 0; i < 8; ++i) { + const auto transform = fluidJarOutputTransform(sc(i)); + const Vector2D outputVector = { + transform.xAxis.x * LOGICAL_VECTOR.x + transform.xAxis.y * LOGICAL_VECTOR.y, + transform.yAxis.x * LOGICAL_VECTOR.x + transform.yAxis.y * LOGICAL_VECTOR.y, + }; + const auto logicalVector = transform.xAxis * outputVector.x + transform.yAxis * outputVector.y; + EXPECT_EQ(logicalVector, LOGICAL_VECTOR); + } +} + +TEST(FluidJar, SimulationSizeRejectsEmptyExtents) { + EXPECT_EQ(fluidJarSimulationSize({0, 100}), Vector2D()); + EXPECT_EQ(fluidJarSimulationSize({100, 0}), Vector2D()); +} + +TEST(FluidJar, PrecisionScalesSimulationAndClamps) { + EXPECT_EQ(fluidJarSimulationSize({1920, 1080}, 0.5F), Vector2D(128, 72)); + EXPECT_EQ(fluidJarSimulationSize({1920, 1080}, 2.F), Vector2D(512, 288)); + EXPECT_EQ(fluidJarSimulationSize({1920, 1080}, 4.F), Vector2D(1024, 576)); + EXPECT_EQ(fluidJarSimulationSize({1920, 1080}, 8.F), Vector2D(2048, 1152)); + EXPECT_EQ(fluidJarSimulationSize({1920, 1080}, 0.1F), Vector2D(128, 72)); + EXPECT_EQ(fluidJarSimulationSize({1920, 1080}, 20.F), Vector2D(2048, 1152)); +} + +TEST(FluidJar, MaximumPrecisionBoundsParticleCapacity) { + const auto size = fluidJarSimulationSize({4096, 4096}, 8.F); + EXPECT_EQ(size, Vector2D(2048, 2048)); + EXPECT_EQ(fluidJarParticleCapacity(size), 131072); +} + +TEST(FluidJar, FillAmountControlsInitialParticles) { + const Vector2D size = {256, 144}; + EXPECT_EQ(fluidJarParticleCapacity(size), 1152); + EXPECT_EQ(fluidJarInitialParticleCount(size, 0.4F), 460); + EXPECT_EQ(fluidJarInitialParticleCount(size, -1.F), 0); + EXPECT_EQ(fluidJarInitialParticleCount(size, 2.F), 1152); +} + +TEST(FluidJar, ResizePreservesParticles) { + EXPECT_EQ(fluidJarResizedParticleCount(460, {256, 144}), 460); + EXPECT_EQ(fluidJarResizedParticleCount(460, {64, 64}), 460); + EXPECT_EQ(fluidJarResizedParticleCount(64, {256, 144}), 64); +} + +TEST(FluidJar, GeometryTransformPreservesWorldPositionDuringMove) { + const auto transform = fluidJarGeometryTransform({100, 200, 800, 600}, {140, 220, 800, 600}, {200, 150}, {200, 150}); + EXPECT_EQ(transform.positionScale, Vector2D(1, 1)); + EXPECT_EQ(transform.positionOffset, Vector2D(-10, 5)); + EXPECT_EQ(transform.velocityScale, Vector2D(1, 1)); +} + +TEST(FluidJar, GeometryTransformPreservesStationaryResizeEdges) { + const auto right = fluidJarGeometryTransform({0, 0, 800, 600}, {0, 0, 1000, 600}, {200, 150}, {250, 150}); + EXPECT_EQ(right.positionScale, Vector2D(1, 1)); + EXPECT_EQ(right.positionOffset, Vector2D(0, 0)); + + const auto left = fluidJarGeometryTransform({0, 0, 800, 600}, {-200, 0, 1000, 600}, {200, 150}, {250, 150}); + EXPECT_EQ(left.positionScale, Vector2D(1, 1)); + EXPECT_EQ(left.positionOffset, Vector2D(50, 0)); + + const auto bottom = fluidJarGeometryTransform({0, 0, 800, 600}, {0, 0, 800, 800}, {200, 150}, {200, 200}); + EXPECT_EQ(bottom.positionScale, Vector2D(1, 1)); + EXPECT_EQ(bottom.positionOffset, Vector2D(0, 50)); +} + +TEST(FluidJar, DiscontinuousGeometryFollowsContainer) { + const auto transform = fluidJarGeometryTransform({0, 0, 800, 600}, {1200, 400, 1000, 800}, {200, 150}, {250, 200}, false); + EXPECT_EQ(transform.positionScale, Vector2D(1.25, 4.0 / 3.0)); + EXPECT_EQ(transform.positionOffset, Vector2D(0, 0)); + EXPECT_EQ(transform.velocityScale, Vector2D(0, 0)); +} + +TEST(FluidJar, WallVelocityUsesSimulationCoordinates) { + const auto moved = fluidJarWallVelocities({0, 0, 800, 600}, {6, 6, 800, 600}, {200, 150}, 1.F / 60.F); + EXPECT_FLOAT_EQ(moved[0], 0.5F); + EXPECT_FLOAT_EQ(moved[1], 0.5F); + EXPECT_FLOAT_EQ(moved[2], -0.5F); + + const auto fasterSimulation = fluidJarWallVelocities({0, 0, 800, 600}, {6, 6, 800, 600}, {200, 150}, 1.F / 60.F, 2.F); + EXPECT_FLOAT_EQ(fasterSimulation[0], 0.25F); + EXPECT_FLOAT_EQ(fasterSimulation[1], 0.25F); + EXPECT_FLOAT_EQ(fasterSimulation[2], -0.25F); + + const auto resized = fluidJarWallVelocities({0, 0, 800, 600}, {0, 0, 806, 600}, {200, 150}, 1.F / 60.F); + EXPECT_FLOAT_EQ(resized[0], 0.F); + EXPECT_NEAR(resized[1], 6.F * (200.F / 806.F) / 3.F, 0.0001F); + EXPECT_FLOAT_EQ(resized[2], 0.F); +} + +TEST(FluidJar, WallVelocityRejectsInvalidIntervalsAndClampsSpikes) { + EXPECT_EQ(fluidJarWallVelocities({0, 0, 800, 600}, {10, 0, 800, 600}, {200, 150}, 0.F), (std::array{})); + + const auto velocity = fluidJarWallVelocities({0, 0, 800, 600}, {1000, 0, 800, 600}, {200, 150}, 1.F / 60.F); + EXPECT_FLOAT_EQ(velocity[0], 1.25F); + EXPECT_FLOAT_EQ(velocity[1], 1.25F); + + EXPECT_EQ(fluidJarWallVelocities({0, 0, 800, 600}, {10, 0, 800, 600}, {200, 150}, 1.F / 60.F, 0.F), (std::array{})); +} diff --git a/tests/render/TexPassElement.cpp b/tests/render/TexPassElement.cpp new file mode 100644 index 000000000..c2cca5e47 --- /dev/null +++ b/tests/render/TexPassElement.cpp @@ -0,0 +1,40 @@ +#include + +#include + +TEST(TexPassElement, ReportsNoBlur) { + CTexPassElement element{CTexPassElement::SRenderData{}}; + + EXPECT_FALSE(element.needsLiveBlur()); + EXPECT_FALSE(element.needsPrecomputeBlur()); +} + +TEST(TexPassElement, ReportsExplicitLiveBlur) { + CTexPassElement element{CTexPassElement::SRenderData{ + .blur = true, + .blockBlurOptimization = true, + }}; + + EXPECT_TRUE(element.needsLiveBlur()); + EXPECT_FALSE(element.needsPrecomputeBlur()); +} + +TEST(TexPassElement, LiveBlurOverrideForcesLiveBlur) { + CTexPassElement element{CTexPassElement::SRenderData{ + .blur = true, + .liveBlurOverride = true, + }}; + + EXPECT_TRUE(element.needsLiveBlur()); + EXPECT_FALSE(element.needsPrecomputeBlur()); +} + +TEST(TexPassElement, LiveBlurOverrideForcesPrecomputedBlur) { + CTexPassElement element{CTexPassElement::SRenderData{ + .blur = true, + .liveBlurOverride = false, + }}; + + EXPECT_FALSE(element.needsLiveBlur()); + EXPECT_TRUE(element.needsPrecomputeBlur()); +}