From 83992920798a6b066bfbb782a9056563116aaa43 Mon Sep 17 00:00:00 2001 From: guoyao Date: Thu, 16 Jul 2026 20:54:07 +0800 Subject: [PATCH] feat: rewrite glass effect with three-stage pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Rewrite liquidglass.frag with SDF-based refraction algorithm (rounded-rect SDF, surface height profile, Snell displacement) 2. Add three-stage rendering pipeline in GlassEffect.qml: blur MultiEffect → glass ShaderEffect → shadow MultiEffect 3. Move outer shadow from shader to MultiEffect (Stage 3) with autoPaddingEnabled for shadow overflow beyond item bounds 4. Simplify glass parameters to 7 knobs: bezelWidth, thickness, ior, specularOpacity, tintOpacity, shadow, radius 5. Update DConfig: remove obsolete keys, add glassSpecularOpacity, glassTintOpacity, glassShadow with new default values 6. Update RoundBlur.qml to remove obsolete property references 7. Update test_glass helper and Main.qml for new parameter set 8. Add shadow/ior/thickness render difference tests, remove obsolete highlight/dispersion/light-angle tests Log: Rewrote glass effect shader and added three-stage rendering pipeline with MultiEffect-based outer shadow Influence: 1. Test glass effect rendering with various parameter combinations 2. Verify glass effect toggle between liquid glass and traditional blur 3. Test backdrop blur with rounded corners in windows and panels 4. Verify lockscreen RoundBlur still works with glass disabled 5. Test DConfig parameter changes take effect at runtime feat: 重写玻璃效果并添加三阶段渲染管线 1. 重写 liquidglass.frag,采用基于 SDF 的折射算法 (圆角矩形 SDF、表面高度剖面、Snell 位移) 2. 在 GlassEffect.qml 中添加三阶段渲染管线: 模糊 MultiEffect → 玻璃 ShaderEffect → 阴影 MultiEffect 3. 将外阴影从 shader 移至 MultiEffect(第三阶段), 通过 autoPaddingEnabled 实现阴影溢出 item 边界 4. 精简玻璃参数为 7 个:bezelWidth、thickness、 ior、specularOpacity、tintOpacity、shadow、radius 5. 更新 DConfig:移除过时 key,新增 glassSpecularOpacity、 glassTintOpacity、glassShadow 并使用新默认值 6. 更新 RoundBlur.qml 移除过时属性引用 7. 更新 test_glass helper 和 Main.qml 适配新参数集 8. 新增 shadow/ior/thickness 渲染差异测试,移除过时的 highlight/dispersion/light-angle 测试 Log: 重写玻璃效果着色器并添加基于 MultiEffect 外阴影的 三阶段渲染管线 Influence: 1. 测试玻璃效果在不同参数组合下的渲染 2. 验证玻璃效果与传统模糊之间的切换 3. 测试窗口和面板中带圆角的背景模糊 4. 验证锁屏 RoundBlur 在 glass 禁用时仍正常工作 5. 测试 DConfig 参数变更在运行时生效 --- examples/test_glass/Main.qml | 314 ++++-------- examples/test_glass/helper.h | 54 +-- .../dconfig/org.deepin.dde.treeland.user.json | 98 ++-- misc/shaders/liquidglass.frag | 454 ++++-------------- src/core/qml/Effects/Blur.qml | 25 +- src/core/qml/Effects/GlassEffect.qml | 107 ++--- src/plugins/lockscreen/qml/RoundBlur.qml | 2 - tests/test_effect_glass/main.cpp | 441 +++-------------- 8 files changed, 329 insertions(+), 1166 deletions(-) diff --git a/examples/test_glass/Main.qml b/examples/test_glass/Main.qml index 40666541cf..3cb3e86959 100644 --- a/examples/test_glass/Main.qml +++ b/examples/test_glass/Main.qml @@ -17,26 +17,13 @@ Item { property bool glassMode: true property bool glassBlurEnabled: true property real effectRadius: 34 - property real glassBezelWidth: 30 + property real glassBezelWidth: 60 property real glassThickness: 50 - property real glassDisplacementFactor: 1.0 - property real glassIor: 1.2 - property real glassDispersion: 0.0 - property real glassBrightness: 0.05 - property real glassContrast: -0.12 - property real glassSaturation: 0.4 - property real glassColorization: 0.12 + property real glassIor: 3.0 + property real glassSpecularOpacity: 0.55 + property real glassTintOpacity: 0.08 + property real glassShadow: 0.5 property int glassBlurMax: 36 - property real glassStrokeWidth: 1.4 - property real glassStrokeStrength: 1.5 - property real glassSpecularOpacity: 0.82 - property bool glassHighlightEnabled: true - property bool glassRimReflectionEnabled: true - property real glassLightAngle: -126.87 - property real glassLightPower: 3.0 - property real glassEdgeSaturation: 0.0 - property real glassReflectionOffset: 12 - property bool advancedExpanded: false Shortcut { sequences: [StandardKey.Quit] @@ -138,195 +125,19 @@ Item { Image { id: background source: root.wallpaperSource - fillMode: Image.PreserveAspectFit - asynchronous: true + fillMode: Image.PreserveAspectCrop anchors.fill: parent - smooth: true } - Column { - id: controlColumn - anchors { - bottom: parent.bottom - left: parent.left - margins: 10 - } - - spacing: 10 - - // ── Action buttons ────────────────────────── - Row { - spacing: 10 - Button { - text: root.glassMode ? "Switch to Blur" : "Switch to Glass" - onClicked: root.glassMode = !root.glassMode - } - Button { - text: "Grab Glass" - onClicked: { - effectPanel.grabToImage(function(result) { - const path = "/tmp/treeland-liquid-glass-grab.png" - if (result.saveToFile(path)) { - console.log("Liquid Glass grab saved", path, result.image.size) - } else { - console.warn("Liquid Glass grab failed", path) - } - }) - } - } - Button { - text: root.glassHighlightEnabled ? "Highlight: on" : "Highlight: off" - onClicked: root.glassHighlightEnabled = !root.glassHighlightEnabled - } - Button { - text: root.glassRimReflectionEnabled ? "Rim refl: on" : "Rim refl: off" - onClicked: root.glassRimReflectionEnabled = !root.glassRimReflectionEnabled - } - Button { - text: root.glassBlurEnabled ? "Blur: on" : "Blur: off" - onClicked: root.glassBlurEnabled = !root.glassBlurEnabled - } - } - - // ── Common sliders (always visible, single row) ── - Row { - spacing: 20 - - Column { - spacing: 2 - Label { text: "radius " + Math.round(root.effectRadius); color: "white" } - Slider { from: 0; to: 80; value: root.effectRadius; onMoved: root.effectRadius = value } - } - Column { - spacing: 2 - Label { text: "bezel " + Math.round(root.glassBezelWidth); color: "white" } - Slider { from: 1; to: 80; value: root.glassBezelWidth; onMoved: root.glassBezelWidth = value } - } - Column { - spacing: 2 - Label { text: "blur max " + root.glassBlurMax; color: "white" } - Slider { from: 0; to: 96; stepSize: 1; value: root.glassBlurMax; onMoved: root.glassBlurMax = value } - } - } - - // ── Advanced toggle ───────────────────────── - Button { - text: root.advancedExpanded ? "▼ Advanced" : "▶ Advanced" - onClicked: root.advancedExpanded = !root.advancedExpanded - } - - // ── Advanced sliders (collapsible, multi-column) ── - Row { - spacing: 20 - visible: root.advancedExpanded - - // Column A: glass material & optics - Column { - spacing: 2 - Label { text: "thickness " + Math.round(root.glassThickness); color: "white" } - Slider { from: 1; to: 200; value: root.glassThickness; onMoved: root.glassThickness = value } - Label { text: "displace " + root.glassDisplacementFactor.toFixed(2); color: "white" } - Slider { from: 0; to: 4; value: root.glassDisplacementFactor; onMoved: root.glassDisplacementFactor = value } - Label { text: "ior " + root.glassIor.toFixed(2); color: "white" } - Slider { from: 1.0; to: 2.5; value: root.glassIor; onMoved: root.glassIor = value } - Label { text: "dispersion " + root.glassDispersion.toFixed(3); color: "white" } - Slider { from: 0; to: 0.1; value: root.glassDispersion; onMoved: root.glassDispersion = value } - Label { text: "edge sat " + root.glassEdgeSaturation.toFixed(2); color: "white" } - Slider { from: 0; to: 1; value: root.glassEdgeSaturation; onMoved: root.glassEdgeSaturation = value } - } - - // Column B: specular & lighting - Column { - spacing: 2 - Label { text: "stroke " + root.glassStrokeWidth.toFixed(1); color: "white" } - Slider { from: 0; to: 8; value: root.glassStrokeWidth; onMoved: root.glassStrokeWidth = value } - Label { text: "stroke str " + root.glassStrokeStrength.toFixed(2); color: "white" } - Slider { from: 0; to: 2; value: root.glassStrokeStrength; onMoved: root.glassStrokeStrength = value } - Label { text: "spec opacity " + root.glassSpecularOpacity.toFixed(2); color: "white" } - Slider { from: 0; to: 1; value: root.glassSpecularOpacity; onMoved: root.glassSpecularOpacity = value } - Label { text: "light angle " + Math.round(root.glassLightAngle) + "°"; color: "white" } - Slider { from: -180; to: 180; value: root.glassLightAngle; onMoved: root.glassLightAngle = value } - Label { text: "light pow " + root.glassLightPower.toFixed(1); color: "white" } - Slider { from: 1; to: 16; value: root.glassLightPower; onMoved: root.glassLightPower = value } - Label { text: "refl offset " + Math.round(root.glassReflectionOffset); color: "white" } - Slider { from: 0; to: 60; value: root.glassReflectionOffset; onMoved: root.glassReflectionOffset = value } - } - - // Column C: colour grading (MultiEffect) - Column { - spacing: 2 - Label { text: "brightness " + root.glassBrightness.toFixed(2); color: "white" } - Slider { from: -1; to: 1; value: root.glassBrightness; onMoved: root.glassBrightness = value } - Label { text: "contrast " + root.glassContrast.toFixed(2); color: "white" } - Slider { from: -1; to: 1; value: root.glassContrast; onMoved: root.glassContrast = value } - Label { text: "saturation " + root.glassSaturation.toFixed(2); color: "white" } - Slider { from: -1; to: 1; value: root.glassSaturation; onMoved: root.glassSaturation = value } - Label { text: "colorization " + root.glassColorization.toFixed(2); color: "white" } - Slider { from: 0; to: 1; value: root.glassColorization; onMoved: root.glassColorization = value } - } - } - } - - Column { - anchors { - bottom: parent.bottom - right: parent.right - margins: 10 - } - - spacing: 10 - - Button { - text: "1X" - onClicked: { - onscreenViewport.setOutputScale(1) - } - } - - Button { - text: "1.5X" - onClicked: { - onscreenViewport.setOutputScale(1.5) - } - } - - Button { - text: "Normal" - onClicked: { - outputViewport.rotationOutput(WaylandOutput.Normal) - } - } - - Button { - text: "R90" - onClicked: { - outputViewport.rotationOutput(WaylandOutput.R90) - } - } - - Button { - text: "R270" - onClicked: { - outputViewport.rotationOutput(WaylandOutput.R270) - } - } - - Button { - text: "Quit" - onClicked: { - Qt.quit() - } - } - } - - Text { + Image { + id: iconImage + source: "qrc:/shaders/test-glass-icon.svg" anchors.centerIn: parent - text: "'Ctrl+Q' quit" - font.pointSize: 40 - color: "white" + width: 100 + height: 100 + fillMode: Image.PreserveAspectFit - SequentialAnimation on rotation { - id: ani + RotationAnimation on rotation { running: true PauseAnimation { duration: 1500 } NumberAnimation { from: 0; to: 360; duration: 5000; easing.type: Easing.InOutCubic } @@ -386,22 +197,10 @@ Item { blurMax: root.glassBlurMax bezelWidth: root.glassBezelWidth thickness: root.glassThickness - displacementFactor: root.glassDisplacementFactor ior: root.glassIor - dispersion: root.glassDispersion - brightness: root.glassBrightness - contrast: root.glassContrast - saturation: root.glassSaturation - colorization: root.glassColorization - strokeWidth: root.glassStrokeWidth - strokeStrength: root.glassStrokeStrength specularOpacity: root.glassSpecularOpacity - highlightEnabled: root.glassHighlightEnabled - rimReflectionEnabled: root.glassRimReflectionEnabled - lightAngle: root.glassLightAngle - lightPower: root.glassLightPower - edgeSaturation: root.glassEdgeSaturation - reflectionOffset: root.glassReflectionOffset + tintOpacity: root.glassTintOpacity + shadow: root.glassShadow smooth: true } } @@ -426,9 +225,6 @@ Item { } // ── Draggable RoundBlur demo ──────────────────────────────────── - // Uses the real RoundBlur component (Blur subclass) from the - // GlassExample QML module, which reads glass parameters from - // Helper.config. Drag to reposition. Item { id: roundBlurPanel width: 30 @@ -460,4 +256,84 @@ Item { } } } + + // ── Control panel ────────────────────────────────────────────────── + Frame { + anchors.right: parent.right + anchors.top: parent.top + width: 320 + height: 420 + opacity: 0.85 + + Column { + anchors.fill: parent + spacing: 6 + + Row { + spacing: 8 + Button { + text: root.glassMode ? "Glass" : "Blur" + onClicked: root.glassMode = !root.glassMode + } + Button { + text: root.glassBlurEnabled ? "Blur: on" : "Blur: off" + onClicked: root.glassBlurEnabled = !root.glassBlurEnabled + } + Button { + text: "Grab Glass" + onClicked: { + effectPanel.grabToImage(function(result) { + const path = "/tmp/treeland-glass-grab.png" + if (result.saveToFile(path)) { + console.log("Liquid Glass grab saved", path, result.image.size) + } else { + console.warn("Liquid Glass grab failed", path) + } + }) + } + } + } + + Column { + spacing: 2 + Label { text: "radius " + Math.round(root.effectRadius); color: "white" } + Slider { from: 0; to: 80; value: root.effectRadius; onMoved: root.effectRadius = value } + } + Column { + spacing: 2 + Label { text: "bezel " + Math.round(root.glassBezelWidth); color: "white" } + Slider { from: 2; to: 60; value: root.glassBezelWidth; onMoved: root.glassBezelWidth = value } + } + Column { + spacing: 2 + Label { text: "thickness " + Math.round(root.glassThickness); color: "white" } + Slider { from: 10; to: 200; value: root.glassThickness; onMoved: root.glassThickness = value } + } + Column { + spacing: 2 + Label { text: "ior " + root.glassIor.toFixed(2); color: "white" } + Slider { from: 1.0; to: 3.0; value: root.glassIor; onMoved: root.glassIor = value } + } + Column { + spacing: 2 + Label { text: "specular " + root.glassSpecularOpacity.toFixed(2); color: "white" } + Slider { from: 0; to: 1; value: root.glassSpecularOpacity; onMoved: root.glassSpecularOpacity = value } + } + Column { + spacing: 2 + Label { text: "tint " + root.glassTintOpacity.toFixed(2); color: "white" } + Slider { from: 0; to: 1; value: root.glassTintOpacity; onMoved: root.glassTintOpacity = value } + } + Column { + spacing: 2 + Label { text: "shadow " + root.glassShadow.toFixed(2); color: "white" } + Slider { from: 0; to: 1; value: root.glassShadow; onMoved: root.glassShadow = value } + } + Column { + spacing: 2 + Label { text: "blur max " + root.glassBlurMax; color: "white" } + Slider { from: 0; to: 96; stepSize: 1; value: root.glassBlurMax; onMoved: root.glassBlurMax = value } + } + } + } } diff --git a/examples/test_glass/helper.h b/examples/test_glass/helper.h index 137cd33f94..04e497af74 100644 --- a/examples/test_glass/helper.h +++ b/examples/test_glass/helper.h @@ -27,14 +27,10 @@ class GlassConfig : public QObject { // Glass material parameters Q_PROPERTY(qreal glassBezel READ glassBezel WRITE setGlassBezel NOTIFY glassBezelChanged) Q_PROPERTY(qreal glassThickness READ glassThickness WRITE setGlassThickness NOTIFY glassThicknessChanged) - Q_PROPERTY(qreal glassDisplacementFactor READ glassDisplacementFactor WRITE setGlassDisplacementFactor NOTIFY glassDisplacementFactorChanged) Q_PROPERTY(qreal glassIor READ glassIor WRITE setGlassIor NOTIFY glassIorChanged) - Q_PROPERTY(qreal glassDispersion READ glassDispersion WRITE setGlassDispersion NOTIFY glassDispersionChanged) - Q_PROPERTY(qreal glassBrightness READ glassBrightness WRITE setGlassBrightness NOTIFY glassBrightnessChanged) - Q_PROPERTY(qreal glassEdgeSaturation READ glassEdgeSaturation WRITE setGlassEdgeSaturation NOTIFY glassEdgeSaturationChanged) - Q_PROPERTY(qreal glassLightAngle READ glassLightAngle WRITE setGlassLightAngle NOTIFY glassLightAngleChanged) - Q_PROPERTY(qreal glassReflectionOffset READ glassReflectionOffset WRITE setGlassReflectionOffset NOTIFY glassReflectionOffsetChanged) - Q_PROPERTY(bool glassHighlightEnabled READ glassHighlightEnabled WRITE setGlassHighlightEnabled NOTIFY glassHighlightEnabledChanged) + Q_PROPERTY(qreal glassSpecularOpacity READ glassSpecularOpacity WRITE setGlassSpecularOpacity NOTIFY glassSpecularOpacityChanged) + Q_PROPERTY(qreal glassTintOpacity READ glassTintOpacity WRITE setGlassTintOpacity NOTIFY glassTintOpacityChanged) + Q_PROPERTY(qreal glassShadow READ glassShadow WRITE setGlassShadow NOTIFY glassShadowChanged) public: explicit GlassConfig(QObject *parent = nullptr); @@ -53,22 +49,14 @@ class GlassConfig : public QObject { void setGlassBezel(qreal v) { if (m_glassBezel != v) { m_glassBezel = v; emit glassBezelChanged(); } } qreal glassThickness() const { return m_glassThickness; } void setGlassThickness(qreal v) { if (m_glassThickness != v) { m_glassThickness = v; emit glassThicknessChanged(); } } - qreal glassDisplacementFactor() const { return m_glassDisplacementFactor; } - void setGlassDisplacementFactor(qreal v) { if (m_glassDisplacementFactor != v) { m_glassDisplacementFactor = v; emit glassDisplacementFactorChanged(); } } qreal glassIor() const { return m_glassIor; } void setGlassIor(qreal v) { if (m_glassIor != v) { m_glassIor = v; emit glassIorChanged(); } } - qreal glassDispersion() const { return m_glassDispersion; } - void setGlassDispersion(qreal v) { if (m_glassDispersion != v) { m_glassDispersion = v; emit glassDispersionChanged(); } } - qreal glassBrightness() const { return m_glassBrightness; } - void setGlassBrightness(qreal v) { if (m_glassBrightness != v) { m_glassBrightness = v; emit glassBrightnessChanged(); } } - qreal glassEdgeSaturation() const { return m_glassEdgeSaturation; } - void setGlassEdgeSaturation(qreal v) { if (m_glassEdgeSaturation != v) { m_glassEdgeSaturation = v; emit glassEdgeSaturationChanged(); } } - qreal glassLightAngle() const { return m_glassLightAngle; } - void setGlassLightAngle(qreal v) { if (m_glassLightAngle != v) { m_glassLightAngle = v; emit glassLightAngleChanged(); } } - qreal glassReflectionOffset() const { return m_glassReflectionOffset; } - void setGlassReflectionOffset(qreal v) { if (m_glassReflectionOffset != v) { m_glassReflectionOffset = v; emit glassReflectionOffsetChanged(); } } - bool glassHighlightEnabled() const { return m_glassHighlightEnabled; } - void setGlassHighlightEnabled(bool v) { if (m_glassHighlightEnabled != v) { m_glassHighlightEnabled = v; emit glassHighlightEnabledChanged(); } } + qreal glassSpecularOpacity() const { return m_glassSpecularOpacity; } + void setGlassSpecularOpacity(qreal v) { if (m_glassSpecularOpacity != v) { m_glassSpecularOpacity = v; emit glassSpecularOpacityChanged(); } } + qreal glassTintOpacity() const { return m_glassTintOpacity; } + void setGlassTintOpacity(qreal v) { if (m_glassTintOpacity != v) { m_glassTintOpacity = v; emit glassTintOpacityChanged(); } } + qreal glassShadow() const { return m_glassShadow; } + void setGlassShadow(qreal v) { if (m_glassShadow != v) { m_glassShadow = v; emit glassShadowChanged(); } } Q_SIGNALS: void glassEnabledChanged(); @@ -77,30 +65,22 @@ class GlassConfig : public QObject { void blurMultiplierChanged(); void glassBezelChanged(); void glassThicknessChanged(); - void glassDisplacementFactorChanged(); void glassIorChanged(); - void glassDispersionChanged(); - void glassBrightnessChanged(); - void glassEdgeSaturationChanged(); - void glassLightAngleChanged(); - void glassReflectionOffsetChanged(); - void glassHighlightEnabledChanged(); + void glassSpecularOpacityChanged(); + void glassTintOpacityChanged(); + void glassShadowChanged(); private: bool m_glassEnabled = true; int m_blurStrength = 20; qreal m_blurAmount = 1.0; qreal m_blurMultiplier = 0.0; - qreal m_glassBezel = 30; + qreal m_glassBezel = 60; qreal m_glassThickness = 50; - qreal m_glassDisplacementFactor = 1.0; - qreal m_glassIor = 1.2; - qreal m_glassDispersion = 0.0; - qreal m_glassBrightness = 0.0; - qreal m_glassEdgeSaturation = 0.4; - qreal m_glassLightAngle = -127; - qreal m_glassReflectionOffset = 12.0; - bool m_glassHighlightEnabled = false; + qreal m_glassIor = 3.0; + qreal m_glassSpecularOpacity = 0.55; + qreal m_glassTintOpacity = 0.08; + qreal m_glassShadow = 0.5; }; WAYLIB_SERVER_BEGIN_NAMESPACE diff --git a/misc/dconfig/org.deepin.dde.treeland.user.json b/misc/dconfig/org.deepin.dde.treeland.user.json index cacdfbe13a..5f55b9ddda 100644 --- a/misc/dconfig/org.deepin.dde.treeland.user.json +++ b/misc/dconfig/org.deepin.dde.treeland.user.json @@ -476,24 +476,24 @@ "visibility": "public" }, "glassBezel": { - "value": 30, + "value": 60, "serial": 0, "flags": [], "name": "Glass Bezel Width", "name[zh_CN]": "玻璃边缘厚度", - "description": "The width of the rounded edge bevel for the Liquid Glass effect. Controls how much light bends around the border.", - "description[zh_CN]": "液态玻璃效果的边缘倒角厚度,控制光线在边界处的折射强度。", + "description": "The width of the rounded edge bevel for the Liquid Glass effect. Controls how much light bends around the border. Range: 2-60.", + "description[zh_CN]": "液态玻璃效果的边缘倒角厚度,控制光线在边界处的折射强度。范围:2-60。", "permissions": "readwrite", "visibility": "public" }, "glassThickness": { - "value": 20, + "value": 50, "serial": 0, "flags": [], "name": "Glass Base Thickness", "name[zh_CN]": "玻璃基础厚度", - "description": "The base thickness of the Liquid Glass effect. Controls the overall strength of background image distortion/refraction.", - "description[zh_CN]": "液态玻璃效果的基础厚度,控制整体对背景图像的畸变/折射强度。", + "description": "The base thickness of the Liquid Glass effect. Controls the overall strength of background image distortion/refraction. Range: 10-200.", + "description[zh_CN]": "液态玻璃效果的基础厚度,控制整体对背景图像的畸变/折射强度。范围:10-200。", "permissions": "readwrite", "visibility": "public" }, @@ -519,91 +519,47 @@ "permissions": "readwrite", "visibility": "public" }, - "glassDisplacementFactor": { - "value": 1.0, - "serial": 0, - "flags": [], - "name": "Glass Displacement Factor", - "name[zh_CN]": "玻璃位移强度", - "description": "Scalar applied to Liquid Glass refraction displacement. Higher values bend the sampled backdrop more strongly. Range: 0.0-4.0.", - "description[zh_CN]": "液态玻璃折射位移的缩放系数。值越大,背景采样弯曲越明显。范围:0.0-4.0。", - "permissions": "readwrite", - "visibility": "public" - }, "glassIor": { - "value": 1.5, + "value": 3.0, "serial": 0, "flags": [], "name": "Glass Refraction Index", "name[zh_CN]": "玻璃折射率", - "description": "Refraction index used by the Liquid Glass material. Values near 1 reduce refraction; larger values bend light more. Range: 1.0-2.5.", - "description[zh_CN]": "液态玻璃材质使用的折射率。接近 1 时折射较弱,值越大光线弯折越明显。范围:1.0-2.5。", - "permissions": "readwrite", - "visibility": "public" - }, - "glassDispersion": { - "value": 0.0, - "serial": 0, - "flags": [], - "name": "Glass Dispersion", - "name[zh_CN]": "玻璃色散", - "description": "Chromatic dispersion amount for the Liquid Glass refraction. 0 disables RGB channel separation. Range: 0.0-0.1.", - "description[zh_CN]": "液态玻璃折射的色散强度。0 表示关闭 RGB 通道分离。范围:0.0-0.1。", - "permissions": "readwrite", - "visibility": "public" - }, - "glassBrightness": { - "value": 0.0, - "serial": 0, - "flags": [], - "name": "Glass Brightness", - "name[zh_CN]": "玻璃亮度", - "description": "Brightness adjustment applied to the Liquid Glass backdrop before refraction. 0 keeps the sampled backdrop unchanged. Range: -1.0-1.0.", - "description[zh_CN]": "液态玻璃折射前应用于背景采样的亮度调节。0 表示保持采样背景不变。范围:-1.0-1.0。", + "description": "Refraction index used by the Liquid Glass material. Values near 1 reduce refraction; larger values bend light more. Range: 1.0-3.0.", + "description[zh_CN]": "液态玻璃材质使用的折射率。接近 1 时折射较弱,值越大光线弯折越明显。范围:1.0-3.0。", "permissions": "readwrite", "visibility": "public" }, - "glassEdgeSaturation": { - "value": 0.4, + "glassSpecularOpacity": { + "value": 0.55, "serial": 0, "flags": [], - "name": "Glass Edge Saturation", - "name[zh_CN]": "玻璃边缘饱和度", - "description": "Additional saturation applied only inside the Liquid Glass bevel/edge zone. 0 keeps the sampled backdrop unchanged. Range: 0.0-1.0.", - "description[zh_CN]": "仅应用在液态玻璃倒角/边缘区域的额外饱和度。0 表示保持采样背景不变。范围:0.0-1.0。", + "name": "Glass Specular Opacity", + "name[zh_CN]": "玻璃高光强度", + "description": "Strength of the specular edge highlight on the Liquid Glass surface. Range: 0.0-1.0.", + "description[zh_CN]": "液态玻璃表面边缘高光的强度。范围:0.0-1.0。", "permissions": "readwrite", "visibility": "public" }, - "glassLightAngle": { - "value": -127, + "glassTintOpacity": { + "value": 0.08, "serial": 0, "flags": [], - "name": "Glass Light Angle", - "name[zh_CN]": "玻璃光线角度", - "description": "Light direction angle in degrees for Liquid Glass optical modulation. 0 points right, -90 points up, 90 points down. Range: -180.0-180.0.", - "description[zh_CN]": "液态玻璃光学调制使用的光线方向角度(度)。0 向右,-90 向上,90 向下。范围:-180.0-180.0。", + "name": "Glass Tint Opacity", + "name[zh_CN]": "玻璃着色强度", + "description": "Amount of white tint mixed into the Liquid Glass surface. The tint blends the glass color toward white. Range: 0.0-1.0.", + "description[zh_CN]": "混合到液态玻璃表面的白色着色量。着色将玻璃颜色向白色混合。范围:0.0-1.0。", "permissions": "readwrite", "visibility": "public" }, - "glassReflectionOffset": { - "value": 12.0, - "serial": 0, - "flags": [], - "name": "Glass Reflection Offset", - "name[zh_CN]": "玻璃反射偏移", - "description": "Backdrop sampling offset in pixels for Liquid Glass reflection/rim tint. 0 samples from the current pixel. Range: 0.0-60.0.", - "description[zh_CN]": "液态玻璃反射/边缘染色使用的背景采样偏移(像素)。0 表示从当前像素采样。范围:0.0-60.0。", - "permissions": "readwrite", - "visibility": "public" - }, - "glassHighlightEnabled": { - "value": false, + "glassShadow": { + "value": 0.5, "serial": 0, "flags": [], - "name": "Glass Highlight Enabled", - "name[zh_CN]": "玻璃高光开关", - "description": "Enable directional specular rim highlights on the Liquid Glass edge. When disabled, the glass shows only refraction and reflection without bright rim light.", - "description[zh_CN]": "在液态玻璃边缘启用方向性镜面高光。关闭时玻璃仅显示折射和反射,无明亮边缘高光。", + "name": "Glass Shadow", + "name[zh_CN]": "玻璃阴影", + "description": "Outer shadow strength around the Liquid Glass item, rendered via MultiEffect. Range: 0.0-1.0.", + "description[zh_CN]": "液态玻璃项目周围的外阴影强度,通过 MultiEffect 渲染。范围:0.0-1.0。", "permissions": "readwrite", "visibility": "public" } diff --git a/misc/shaders/liquidglass.frag b/misc/shaders/liquidglass.frag index ca5a9a9383..24cb053bd8 100644 --- a/misc/shaders/liquidglass.frag +++ b/misc/shaders/liquidglass.frag @@ -3,26 +3,21 @@ #version 440 -// Liquid Glass fragment shader — closely follows the liquid-dom reference -// implementation (https://github.com/AndrewPrifer/liquid-dom). +// Liquid Glass fragment shader for treeland. // -// Key concepts ported from liquid-dom's GLASS_SHADER (WGSL → GLSL 440): -// • Convex-squircle height profile for the beveled edge -// • Physical refraction via refract() with per-channel IOR dispersion -// • Luminance-gated environment reflection (rim band only) -// • Coloured edge specular (refracted ↔ reflected mix) -// • Additive white rim specular with configurable opacity -// • Tint, saturation, and brightness handled by MultiEffect (not shader) +// Algorithm pipeline: +// 1. SDF of rounded rectangle +// 2. Convex-squircle surface height profile +// 3. Snell refraction displacement +// 4. Specular edge highlight (fixed light direction) +// 5. Inner shadow +// 6. Inner rim glow +// 7. White tint mix +// 8. Alpha anti-aliasing // -// Performance structure (hot path, sample-bound): -// 1. Outside AA band → 0 samples -// 2. Flat interior → 1 sample -// 3. Bezel interior → 1–3 sharp refraction samples -// 4. Silhouette AA band only → 2×2 mono geometric SS -// 5. Lit rim → + reflection samples -// Dispersion RGB split is gated on the uniform (coherent across the draw). -// Do NOT multi-tap filter warped content: it smears bright icon edges into -// pale ghosts. Keep a single bilinear sample so ellipses stay sharp. +// Blur is handled externally by MultiEffect; this shader samples the +// (already blurred) source texture with a single bilinear tap. +// Outer shadow is handled by MultiEffect in the QML layer. layout(location = 0) in vec2 texCoord; layout(location = 0) out vec4 fragColor; @@ -31,370 +26,97 @@ layout(std140, binding = 0) uniform buf { mat4 qt_Matrix; float qt_Opacity; vec2 itemSize; - float radius; // corner radius (px) - float bezelWidth; // edge bevel width (px) — liquid-dom bezelWidth - float thickness; // base glass thickness (px) — liquid-dom thickness - float displacementFactor; // scalar on displacement — liquid-dom displacementFactor - float ior; // refractive index — liquid-dom ior - float dispersion; // RGB channel separation — liquid-dom dispersion - vec4 highlightColor; // coloured specular tint (white in liquid-dom) - float strokeWidth; // specular rim band width (px) — liquid-dom specular.width - float strokeStrength; // specular strength — liquid-dom specular.strength - vec2 lightDirection; // 2D light dir (renormalized in shader) - float lightPower; // specular sharpness exponent — liquid-dom specular.sharpness - float edgeSaturation; // extra edge saturation boost - float reflectionOffset; // reflection sampling offset (px) — liquid-dom reflectionOffset - float specularOpacity; // white specular opacity — liquid-dom specular.opacity - float rimReflectionStrength; // 0 = pure-white rim, >0 tints rim from backdrop + float radius; // corner radius (px) + float bezelWidth; // edge bevel width (px) + float thickness; // base glass thickness (px) + float ior; // refractive index + float specularOpacity; // specular highlight strength + float tintOpacity; // white tint mix amount } ubuf; layout(binding = 1) uniform sampler2D source; -const vec3 kLumaWeights = vec3(0.2126, 0.7152, 0.0722); -const vec3 kViewDir = vec3(0.0, 0.0, -1.0); - -// --------------------------------------------------------------------------- -// SDF helpers (rounded rectangle) -// --------------------------------------------------------------------------- - -// Returns (signed distance, outward surface normal). -// Flat-edge midpoints use an axis-aligned fallback when delta ~ 0. -vec3 roundedRectSDF(vec2 p, vec2 halfSize, float radius) -{ - float r = min(radius, min(halfSize.x, halfSize.y)); - vec2 inner = halfSize - vec2(r); - vec2 delta = p - clamp(p, -inner, inner); - float len2 = dot(delta, delta); - // inversesqrt avoids a separate sqrt+divide for the normal. - float invLen = inversesqrt(max(len2, 1e-16)); - float len = len2 * invLen; // == sqrt(len2) for len2 > 0 - - // SDF: distance to the rounded rectangle boundary (negative inside). - vec2 q = abs(p) - inner; - float dist = len + min(max(q.x, q.y), 0.0) - r; - - vec2 edge = halfSize - abs(p); - float useX = step(edge.x, edge.y); - vec2 axisNormal = vec2(sign(p.x) * useX, sign(p.y) * (1.0 - useX)); - vec2 normal = (len2 > 1e-16) ? (delta * invLen) : axisNormal; - - return vec3(dist, normal); -} - -// Normal-only path for the expanded-radius refraction field (dist unused). -vec2 roundedRectNormal(vec2 p, vec2 halfSize, float radius) -{ - float r = min(radius, min(halfSize.x, halfSize.y)); - vec2 inner = halfSize - vec2(r); - vec2 delta = p - clamp(p, -inner, inner); - float len2 = dot(delta, delta); - if (len2 > 1e-16) - return delta * inversesqrt(len2); - - vec2 edge = halfSize - abs(p); - float useX = step(edge.x, edge.y); - return vec2(sign(p.x) * useX, sign(p.y) * (1.0 - useX)); -} - -// --------------------------------------------------------------------------- -// Colour / math helpers -// --------------------------------------------------------------------------- - -vec3 applySaturation(vec3 color, float saturationValue) -{ - float luminance = dot(color, kLumaWeights); - return mix(vec3(luminance), color, saturationValue); -} - -vec4 sampleBackdrop(vec2 coord) -{ - // Soft clamp: ShaderEffect source wrap is not guaranteed clamp-to-edge. - return texture(source, clamp(coord, vec2(0.001), vec2(0.999))); -} - -// Product defaults are lightPower 2 or 3 — multiply is cheaper than pow. -float specularPower(float x, float p) -{ - float v = max(x, 0.0); - // Uniform-driven: whole draw takes one arm, no pixel divergence. - if (abs(p - 2.0) < 0.001) - return v * v; - if (abs(p - 3.0) < 0.001) - return v * v * v; - if (abs(p - 4.0) < 0.001) { - float v2 = v * v; - return v2 * v2; - } - return pow(v, p); -} - -// Height profile: convex squircle (liquid-dom). -// Returns vec2(height, derivative); x is bezelProgress [0..1] -// (0 = shape boundary, 1 = bezelWidth inside). -// height = (1 - u^4)^0.25 where u = 1 - clamp(x, 0, 1) -// derivative = d(height)/dx = u^3 * height / inside [= u^3 / inside^0.75] -vec2 convexSquircle(float x) +// Signed distance to a rounded rectangle. +float sdRoundedRect(vec2 p, vec2 halfSize, float r) { - float u = 1.0 - clamp(x, 0.0, 1.0); - float u2 = u * u; - float u4 = u2 * u2; - float inside = max(1.0 - u4, 0.0001); - float height = sqrt(sqrt(inside)); // inside^0.25 - float derivative = (u2 * u) * height / inside; // u^3 / inside^0.75 - return vec2(height, derivative); -} - -vec2 refractDisplacement(vec3 ray, float scale) -{ - return ray.xy / max(-ray.z, 0.0001) * scale; -} - -// Sample backdrop with optional RGB dispersion along `offsetDir` (in UV). -// Uniform `useDispersion` keeps the branch coherent across the whole draw. -vec3 sampleDispersed(vec2 baseUv, vec2 offsetDir, float offsetPx, vec2 pixelSize, bool useDispersion) -{ - vec2 baseOffset = offsetDir * (offsetPx * pixelSize); - if (useDispersion) { - float refDisp = max(ubuf.dispersion, 0.0) * 2.0; - vec2 uvR = baseUv + baseOffset * (1.0 - refDisp); - vec2 uvG = baseUv + baseOffset; - vec2 uvB = baseUv + baseOffset * (1.0 + refDisp); - return vec3( - sampleBackdrop(uvR).r, - sampleBackdrop(uvG).g, - sampleBackdrop(uvB).b - ); - } - return sampleBackdrop(baseUv + baseOffset).rgb; + vec2 q = abs(p) - halfSize + r; + return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r; } -// Refraction at pixel-space point `p` (origin = item center). Used for -// silhouette 2×2 supersampling where the normal rotates within one pixel. -// `aaWidth` is the parent fragment's screen-space SDF width (fwidth at offsets -// is unreliable). Edge SS always uses mono IOR; dispersion stays on the -// single-center bezel path. -vec3 sampleRefractedAt( - vec2 p, - vec2 halfSize, - vec2 size, - vec2 pixelSize, - float bw, - float aaWidth, - float baseIor, - vec2 lightDir) +// Convex-squircle height profile for the beveled edge. +float surfaceHeight(float t) { - vec3 localSdf = roundedRectSDF(p, halfSize, ubuf.radius); - float sdf = localSdf.x; - vec2 n = localSdf.yz; - float inward = max(-sdf, 0.0); - float cover = 1.0 - smoothstep(0.0, aaWidth, sdf); - vec2 uv = (p + halfSize) / size; - - if (cover <= 1e-4) - return sampleBackdrop(uv).rgb; - - float bezelProgress = inward / bw; - vec2 profile = convexSquircle(bezelProgress); - float surfaceHeight = ubuf.thickness + profile.x * bw; - vec2 refrN = roundedRectNormal(p, halfSize, ubuf.radius + bw); - float bevelFade = 1.0 - smoothstep(bw * 0.85, bw, inward); - float outerSoft = smoothstep(0.0, max(2.5 * aaWidth, 2.5), inward); - float slope = min(profile.y * bevelFade * outerSoft, 8.0); - vec3 surfN = normalize(vec3(refrN * slope, 1.0)); - - float lightFacing = clamp(dot(n, lightDir) * 0.5 + 0.5, 0.0, 1.0); - float dirOpt = mix(0.85, 1.15, lightFacing); - float scale = surfaceHeight * ubuf.displacementFactor * dirOpt * cover; - - if (slope < 1e-4 || scale < 1e-4) - return sampleBackdrop(uv).rgb; - - vec3 ray = refract(kViewDir, surfN, 1.0 / baseIor); - // Geometric 2×2 already SS the silhouette; single bilinear is enough here. - return sampleBackdrop(uv + refractDisplacement(ray, scale) * pixelSize).rgb; + float s = 1.0 - t; + return pow(1.0 - s * s * s * s, 0.25); } -// --------------------------------------------------------------------------- -// Fragment main -// --------------------------------------------------------------------------- - void main() { - vec2 size = max(ubuf.itemSize, vec2(1.0)); - vec2 pixelSize = 1.0 / size; - vec2 centered = texCoord * size - size * 0.5; - vec2 halfSize = size * 0.5; + // Convert UV to pixel space relative to glass center. + vec2 screenPx = texCoord * ubuf.itemSize; + vec2 p = screenPx - ubuf.itemSize * 0.5; + vec2 halfSize = ubuf.itemSize * 0.5; - // SDF + normal in one pass (shared corner-clamp computation). - vec3 sdfResult = roundedRectSDF(centered, halfSize, ubuf.radius); - float sdf = sdfResult.x; - vec2 normal = sdfResult.yz; + float sd = sdRoundedRect(p, halfSize, ubuf.radius); - // Screen-space AA width from SDF derivatives. Floor 0.5px keeps thin - // coverage without the heavier 0.75 blur on sharp 1× edges. - float shapeAntialiasWidth = max(fwidth(sdf), 0.5); - float fillMask = 1.0 - smoothstep(0.0, shapeAntialiasWidth, sdf); - - // ── 0 samples: fully outside the AA band ────────────────────────── - if (fillMask <= 0.0) { + // Outside the glass: fully transparent (outer shadow is handled by + // MultiEffect in the QML layer). + if (sd > 0.0) { fragColor = vec4(0.0); return; } - float outAlpha = fillMask * ubuf.qt_Opacity; - float inwardDistance = max(-sdf, 0.0); - - float rimWidthPx = max(ubuf.strokeWidth, 0.0001); - // Rim outer/inner feathers track shape AA so highlights stay aligned - // with the coverage edge under scale / HiDPI. - float rimBandMask = - (1.0 - smoothstep(0.0, shapeAntialiasWidth, sdf)) - * (1.0 - smoothstep(rimWidthPx, rimWidthPx + shapeAntialiasWidth, inwardDistance)); - - float configuredBezel = max(ubuf.bezelWidth, 1.0); - float bw = max( - min(configuredBezel, max(min(halfSize.x, halfSize.y) - shapeAntialiasWidth, 1.0)), - 1.0); - - // ── 1 sample: flat interior past bezel and rim ──────────────────── - // Slope and rim contribution are both zero; refracted == background. - if (inwardDistance > bw && rimBandMask <= 0.0) { - vec3 color = sampleBackdrop(texCoord).rgb; - fragColor = vec4(clamp(color, 0.0, 1.0) * outAlpha, outAlpha); - return; - } - - // ── Bezel / rim band ────────────────────────────────────────────── - // Prefer the QML unit lightDirection; renorm defensively if unbound. - vec2 rawLightDir = ubuf.lightDirection; - float lightLen2 = dot(rawLightDir, rawLightDir); - vec2 lightDir = lightLen2 > 1e-8 - ? rawLightDir * inversesqrt(lightLen2) - : vec2(-0.70710678, -0.70710678); - // Specular is pure ALU and only non-zero in the lit rim. The uniform - // gate skips that work for the entire draw when highlights are disabled. - float combinedSpecular = 0.0; - if (ubuf.strokeStrength > 1e-4) { - float inwardProgress = clamp(inwardDistance / max(rimWidthPx, 1.0), 0.0, 1.0); - float strengthFalloff = 1.0 - inwardProgress * inwardProgress * 0.5; - float normalAlignment = abs(dot(normal, lightDir)); - combinedSpecular = clamp( - specularPower(normalAlignment, ubuf.lightPower) - * ubuf.strokeStrength * strengthFalloff * rimBandMask, - 0.0, 1.0); - } - - // Height profile + radiating refraction field (radius + bw). - float bezelProgress = inwardDistance / bw; - vec2 profileResult = convexSquircle(bezelProgress); - // convexSquircle clamps progress; its height is exactly 1 past the bezel. - float surfaceHeight = ubuf.thickness + profileResult.x * bw; - - // Expanded-radius normal only — distance is unused here. - vec2 refractionNormal = roundedRectNormal(centered, halfSize, ubuf.radius + bw); - float bevelFade = 1.0 - smoothstep(bw * 0.85, bw, inwardDistance); - // Outer ease-in: squircle derivative peaks at the silhouette (→∞ then clamp - // to ~85°). Without this, UV warp slams to max just inside the coverage - // edge and high-contrast backdrop edges grow triangular spikes / hooks. - float outerSoft = smoothstep(0.0, max(2.5 * shapeAntialiasWidth, 2.5), inwardDistance); - float clampedSlope = min(profileResult.y * bevelFade * outerSoft, 8.0); // tan≈83° - vec3 surfaceNormal = normalize(vec3(refractionNormal * clampedSlope, 1.0)); - - float lightFacing = clamp(dot(normal, lightDir) * 0.5 + 0.5, 0.0, 1.0); - float directionalOpticalStrength = mix(0.85, 1.15, lightFacing); - - float baseIor = max(ubuf.ior, 1.0001); - float nz = 1.0 - surfaceNormal.z; - float nz2 = nz * nz; - float prismEffect = 1.0 + nz2 * 0.3; - // Uniform gate: whole draw shares one arm (no per-pixel dispersion flicker). - bool useDispersion = ubuf.dispersion > 1e-4; - float disp = useDispersion - ? (ubuf.dispersion * directionalOpticalStrength * prismEffect) - : 0.0; - - // fillMask is already the 0..1 AA coverage; do not re-smoothstep it - // against fwidth (which can be > 1 under scale and would crush interior - // refraction). Soft edge fade is exactly the coverage itself. - float displaceScale = fillMask; - float commonScale = surfaceHeight * ubuf.displacementFactor - * directionalOpticalStrength * displaceScale; - - // Refraction samples. - // Only the shape AA band needs geometric 2×2 SS (coverage + normal rotate - // inside a pixel). The bezel interior keeps a single sharp warp sample so - // high-contrast icon edges become clean ellipses, not pale side-lobes. - bool needEdgeSS = fillMask < 0.999; - - vec3 refractedColor; - if (clampedSlope < 1e-4 || commonScale < 1e-4) { - refractedColor = sampleBackdrop(texCoord).rgb; - } else if (needEdgeSS) { - vec2 o = vec2(0.25); - vec3 acc = - sampleRefractedAt(centered + vec2(-o.x, -o.y), halfSize, size, pixelSize, - bw, shapeAntialiasWidth, baseIor, lightDir) - + sampleRefractedAt(centered + vec2( o.x, -o.y), halfSize, size, pixelSize, - bw, shapeAntialiasWidth, baseIor, lightDir) - + sampleRefractedAt(centered + vec2(-o.x, o.y), halfSize, size, pixelSize, - bw, shapeAntialiasWidth, baseIor, lightDir) - + sampleRefractedAt(centered + vec2( o.x, o.y), halfSize, size, pixelSize, - bw, shapeAntialiasWidth, baseIor, lightDir); - refractedColor = acc * 0.25; - } else if (useDispersion) { - vec2 scaledPixel = commonScale * pixelSize; - vec3 rayR = refract(kViewDir, surfaceNormal, 1.0 / max(baseIor - disp, 1.0001)); - vec3 rayG = refract(kViewDir, surfaceNormal, 1.0 / baseIor); - vec3 rayB = refract(kViewDir, surfaceNormal, 1.0 / max(baseIor + disp, 1.0001)); - refractedColor = vec3( - sampleBackdrop(texCoord + refractDisplacement(rayR, 1.0) * scaledPixel).r, - sampleBackdrop(texCoord + refractDisplacement(rayG, 1.0) * scaledPixel).g, - sampleBackdrop(texCoord + refractDisplacement(rayB, 1.0) * scaledPixel).b - ); - } else { - vec3 ray = refract(kViewDir, surfaceNormal, 1.0 / baseIor); - refractedColor = sampleBackdrop( - texCoord + refractDisplacement(ray, commonScale) * pixelSize).rgb; - } - - float edgeInfluence = 1.0 - smoothstep(0.0, bw, inwardDistance); - vec3 glassInterior = refractedColor; - // Skip the luma mix when edge saturation is off or we are past the bezel. - if (ubuf.edgeSaturation != 0.0 && edgeInfluence > 0.0) - glassInterior = applySaturation( - refractedColor, 1.0 + edgeInfluence * ubuf.edgeSaturation); - - // Base glass: fillMask is 1 over almost the whole interior of the shape, - // so avoid a redundant background fetch on the common path. - vec3 color; - if (fillMask >= 0.999) - color = glassInterior; - else - color = mix(sampleBackdrop(texCoord).rgb, glassInterior, fillMask); - - // Reflection + rim specular only where combinedSpecular contributes. - // (highlightEnabled=false zeroes strokeStrength → this whole block is skipped.) - if (combinedSpecular > 1e-4) { - vec3 reflectedColor = sampleDispersed( - texCoord, normal, ubuf.reflectionOffset, pixelSize, useDispersion); - - float fresnel = nz2 * nz; // (1 - n.z)^3 - float reflectionBlend = clamp( - smoothstep(0.2, 0.85, dot(reflectedColor, kLumaWeights)) - * (1.0 - smoothstep(0.35, 0.85, dot(refractedColor, kLumaWeights))) - + fresnel * 0.3, - 0.0, 1.0); - - vec3 edgeSpecularColor = mix(refractedColor, reflectedColor, reflectionBlend); - vec3 rimReflectionTint = mix(vec3(1.0), reflectedColor, ubuf.rimReflectionStrength); - - vec3 specularAdd = rimReflectionTint * ubuf.specularOpacity - + ubuf.highlightColor.rgb * (ubuf.highlightColor.a * 0.5); - - float specularMask = combinedSpecular * fillMask; - color = mix(color, edgeSpecularColor, specularMask); - color += specularAdd * specularMask; - } - - fragColor = vec4(clamp(color, 0.0, 1.0) * outAlpha, outAlpha); + // Distance from the edge, clamped to the bezel zone. + float distFromEdge = -sd; + float bezel = min(ubuf.bezelWidth, + min(ubuf.radius, min(halfSize.x, halfSize.y)) - 1.0); + float t = clamp(distFromEdge / bezel, 0.0, 1.0); + + // Surface height and its derivative. + float h = surfaceHeight(t); + float dt = 0.001; + float h2 = surfaceHeight(min(t + dt, 1.0)); + float dh = (h2 - h) / dt; + + // Snell refraction displacement. + float slopeAngle = atan(dh * (ubuf.thickness / bezel)); + float sinR = sin(slopeAngle) / ubuf.ior; + sinR = clamp(sinR, -1.0, 1.0); + float thetaR = asin(sinR); + float displacement = h * ubuf.thickness * (tan(slopeAngle) - tan(thetaR)); + + // Gradient of the SDF for displacement direction. + vec2 grad; + float eps = 0.5; + grad.x = sdRoundedRect(p + vec2(eps, 0.0), halfSize, ubuf.radius) - sd; + grad.y = sdRoundedRect(p + vec2(0.0, eps), halfSize, ubuf.radius) - sd; + grad = normalize(grad); + + // Sample the (pre-blurred) backdrop at the refracted position. + vec2 offset = -grad * displacement / ubuf.itemSize; + vec2 refractedUV = texCoord + offset; + vec3 color = texture(source, refractedUV).rgb; + + // Specular edge highlight (fixed light direction). + vec2 lightDir = normalize(vec2(0.5, -0.7)); + float rimDot = abs(dot(grad, lightDir)); + float rimFalloff = 1.0 - smoothstep(0.0, bezel * 0.4, distFromEdge); + float specHighlight = pow(rimDot * rimFalloff, 1.5); + color += vec3(specHighlight * ubuf.specularOpacity); + + // Inner shadow. + float innerShadow = 1.0 - smoothstep(0.0, bezel * 0.6, distFromEdge); + color *= mix(1.0, 0.7, innerShadow * 0.3); + + // Inner rim glow. + float innerRim = smoothstep(0.0, 2.0, distFromEdge) + * (1.0 - smoothstep(2.0, 5.0, distFromEdge)); + color += vec3(innerRim * 0.15 * ubuf.specularOpacity); + + // White tint. + color = mix(color, vec3(1.0), ubuf.tintOpacity); + + // Alpha anti-aliasing. + float alpha = smoothstep(0.0, 1.5, distFromEdge); + fragColor = vec4(color * alpha, alpha) * ubuf.qt_Opacity; } diff --git a/src/core/qml/Effects/Blur.qml b/src/core/qml/Effects/Blur.qml index f41de75a7a..4c5f99efa4 100644 --- a/src/core/qml/Effects/Blur.qml +++ b/src/core/qml/Effects/Blur.qml @@ -17,17 +17,11 @@ RenderBufferBlitter { property bool blurEnabled: blurMax > 0 && blurAmount > 0 property real blurAmount: Helper.config.blurAmount property real multiplier: Helper.config.blurMultiplier - property real brightness: Helper.config.glassBrightness - property real lightAngle: Helper.config.glassLightAngle - property bool highlightEnabled: Helper.config.glassHighlightEnabled property bool glassEnabled: Helper.config.glassEnabled z: parent.z ? parent.z - 1 : -1 anchors.fill: parent - // Dispatch between Liquid Glass and traditional blur via a Loader so only - // the active branch is instantiated. Toggling the DConfig key unloads one - // Component and loads the other. Loader { anchors.fill: parent sourceComponent: blitter.glassEnabled ? glassComponent : blurComponent @@ -43,26 +37,13 @@ RenderBufferBlitter { blurMax: blitter.blurMax blurAmount: blitter.blurAmount blurMultiplier: blitter.multiplier - brightness: blitter.brightness - highlightEnabled: blitter.highlightEnabled - lightAngle: blitter.lightAngle bezelWidth: Helper.config.glassBezel thickness: Helper.config.glassThickness - displacementFactor: Helper.config.glassDisplacementFactor ior: Helper.config.glassIor - dispersion: Helper.config.glassDispersion - contrast: -0.12 - saturation: 0.4 - colorization: 0.12 - edgeSaturation: Helper.config.glassEdgeSaturation - highlightColor: Qt.rgba(1, 1, 1, 0.3) - strokeWidth: 0.5 - strokeStrength: 1.5 - specularOpacity: 0.82 - rimReflectionEnabled: true - lightPower: 3.0 - reflectionOffset: Helper.config.glassReflectionOffset + specularOpacity: Helper.config.glassSpecularOpacity + tintOpacity: Helper.config.glassTintOpacity + shadow: Helper.config.glassShadow } } diff --git a/src/core/qml/Effects/GlassEffect.qml b/src/core/qml/Effects/GlassEffect.qml index b3db9e155c..461ea38448 100644 --- a/src/core/qml/Effects/GlassEffect.qml +++ b/src/core/qml/Effects/GlassEffect.qml @@ -15,108 +15,71 @@ Item { property real blurAmount: 1.0 property real blurMultiplier: 0.0 - // Glass material parameters (aligned with liquid-dom naming) - property real bezelWidth: 30 // edge bevel width (px) - property real thickness: 20 // base glass thickness (px) - property real displacementFactor: 1 // scalar on displacement - property real ior: 1.5 // refractive index - property real dispersion: 0.0 // RGB channel separation; 0 disables + // Glass material parameters + property real bezelWidth: 60 // edge bevel width (px) + property real thickness: 50 // base glass thickness (px) + property real ior: 3.0 // refractive index + property real specularOpacity: 0.55 // specular highlight strength + property real tintOpacity: 0.08 // white tint mix amount + property real shadow: 0.5 // outer shadow strength (MultiEffect) - // Colour controls — applied to the backdrop BEFORE refraction, so the - // glass specular / rim highlights stay sharp and uncoloured. - // Delegated to MultiEffect (avoid duplicate implementation) - property real brightness: 0.0 // [-1, 1], 0 = no change - property real contrast: 0.0 // [-1, 1], 0 = no change - property real saturation: 0.4 // [-1, 1], 0 = no change - property real colorization: 0.0 // [0, 1], 0 = no tint - property color colorizationColor: Qt.rgba(1, 1, 1, 1) - - // Edge-local saturation boost — MultiEffect can't do spatially-varying, so - // this stays in the shader. Only affects the bezel zone. - property real edgeSaturation: 0.0 - - // Specular / highlight controls - property color highlightColor: Qt.rgba(1, 1, 1, 0.35) - property real strokeWidth: 1.5 // specular rim band width (px) - property real strokeStrength: 1.0 // specular strength - property real specularOpacity: 0.6 // white specular opacity (liquid-dom demo value) - property bool highlightEnabled: false // master toggle for edge specular highlights - - // Rim reflection tint: when enabled, the white specular rim picks up a - // small amount of nearby backdrop colour. The shader receives only a - // float gate to avoid bool/int uniform binding issues on GLES drivers. - property bool rimReflectionEnabled: true - - // Light direction. Degrees; 0 points right, -90 points up. - // Always a unit vector (cos/sin); the fragment shader consumes it as-is. - property real lightAngle: -135.0 - readonly property real lightAngleRadians: lightAngle * Math.PI / 180 - property real lightPower: 2.0 // specular sharpness exponent - readonly property vector2d lightDirection: Qt.vector2d( - Math.cos(effect.lightAngleRadians), - Math.sin(effect.lightAngleRadians)) - - // Reflection - property real reflectionOffset: 18 // reflection sampling offset (px) - - - // True when MultiEffect is needed for blur or colour grading of the backdrop readonly property bool multiEffectEnabled: - (blurEnabled && blurAmount > 0 && blurMax > 0) - || brightness != 0 || contrast != 0 || saturation != 0 || colorization > 0 + blurEnabled && blurAmount > 0 && blurMax > 0 + readonly property bool shadowEnabled: shadow > 0 anchors.fill: parent - // ── Stage 1: blur + colour-grade the raw backdrop ────────────────── - // NOT directly visible — its layer texture is sampled by the glass - // shader. Blur happens here so the glass specular / rim highlights - // added in Stage 2 stay sharp. + // ── Stage 1: blur the raw backdrop ─────────────────────────────── + // Not drawn to screen; its layer texture is sampled by Stage 2. MultiEffect { id: blurredSource anchors.fill: parent visible: effect.multiEffectEnabled layer.enabled: effect.multiEffectEnabled smooth: true - opacity: 0 // not drawn to screen; sampled as a texture + opacity: 0 source: effect.source autoPaddingEnabled: false blurEnabled: effect.blurEnabled && effect.blurAmount > 0 blur: blurEnabled ? effect.blurAmount : 0.0 blurMax: effect.blurMax blurMultiplier: effect.blurMultiplier - brightness: effect.brightness - contrast: effect.contrast - saturation: effect.saturation - colorization: effect.colorization - colorizationColor: effect.colorizationColor } - // ── Stage 2: glass shader refracts the (blurred) backdrop and adds - // specular highlights / rim light on top. This item intentionally - // owns no rounded Shape clip; wrappers such as Glass.qml provide that. + // ── Stage 2: glass shader refracts the (blurred) backdrop ─────── + // Not drawn to screen; its layer texture is sampled by Stage 3. ShaderEffect { id: glassShader objectName: "glassShader" anchors.fill: parent - smooth: true // Enables linear filtering for the sampler to prevent jagged steps + smooth: true + opacity: 0 + layer.enabled: true property variant source: effect.multiEffectEnabled ? blurredSource : effect.source readonly property vector2d itemSize: Qt.vector2d(Math.max(width, 1), Math.max(height, 1)) readonly property real radius: effect.radius readonly property real bezelWidth: effect.bezelWidth readonly property real thickness: effect.thickness - readonly property real displacementFactor: effect.displacementFactor readonly property real ior: effect.ior - readonly property real dispersion: effect.dispersion - readonly property color highlightColor: effect.highlightColor - readonly property real strokeWidth: effect.strokeWidth - readonly property real strokeStrength: effect.highlightEnabled ? effect.strokeStrength : 0.0 - readonly property vector2d lightDirection: effect.lightDirection - readonly property real lightPower: effect.lightPower - readonly property real edgeSaturation: effect.edgeSaturation - readonly property real reflectionOffset: effect.reflectionOffset - readonly property real specularOpacity: effect.highlightEnabled ? effect.specularOpacity : 0.0 - readonly property real rimReflectionStrength: effect.rimReflectionEnabled ? 0.22 : 0.0 + readonly property real specularOpacity: effect.specularOpacity + readonly property real tintOpacity: effect.tintOpacity vertexShader: "qrc:/shaders/liquidglass.vert.qsb" fragmentShader: "qrc:/shaders/liquidglass.frag.qsb" } + + // ── Stage 3: final output with outer shadow ───────────────────── + // autoPaddingEnabled expands the item beyond the source size so the + // shadow can overflow the glass bounds. + MultiEffect { + id: shadowOutput + width: effect.width + height: effect.height + source: glassShader + autoPaddingEnabled: effect.shadowEnabled + shadowEnabled: effect.shadowEnabled + shadowOpacity: effect.shadow + shadowColor: "black" + shadowBlur: 0.5 + shadowScale: 1.0 + } } diff --git a/src/plugins/lockscreen/qml/RoundBlur.qml b/src/plugins/lockscreen/qml/RoundBlur.qml index 581a24eab7..c42397b51e 100644 --- a/src/plugins/lockscreen/qml/RoundBlur.qml +++ b/src/plugins/lockscreen/qml/RoundBlur.qml @@ -9,8 +9,6 @@ Blur { id: root glassEnabled: false blurMax: 64 - brightness: 0 - highlightEnabled: false property color color: Qt.rgba(1, 1, 1, 0.1) diff --git a/tests/test_effect_glass/main.cpp b/tests/test_effect_glass/main.cpp index 6a7ef11230..30ba1e8293 100644 --- a/tests/test_effect_glass/main.cpp +++ b/tests/test_effect_glass/main.cpp @@ -116,10 +116,6 @@ class GlassEffectTest : public QObject const int w = img.width(); int maxStep = 0; - // Top-right has only the smooth horizontal gradient behind it in this - // fixture, so any large cross-diagonal jump is from the shader rather - // than a backdrop feature. The paired samples straddle the possible - // 45° seam where top-edge and right-edge corner math meet. for (int d = 12; d <= 46; ++d) { const QRgb a = img.pixel(w - 1 - d, d + 1); const QRgb b = img.pixel(w - 2 - d, d); @@ -131,31 +127,26 @@ class GlassEffectTest : public QObject void setSmallRadiusLargeBezel() { - m_glass->setProperty("highlightEnabled", false); - m_glass->setProperty("rimReflectionEnabled", false); m_glass->setProperty("blurEnabled", false); m_glass->setProperty("radius", 8.0); m_glass->setProperty("bezelWidth", 52.0); m_glass->setProperty("thickness", 120.0); - m_glass->setProperty("displacementFactor", 0.85); m_glass->setProperty("ior", 1.45); - m_glass->setProperty("dispersion", 0.018); - m_glass->setProperty("strokeStrength", 0.0); m_glass->setProperty("specularOpacity", 0.0); - m_glass->setProperty("edgeSaturation", 0.0); + m_glass->setProperty("tintOpacity", 0.0); + m_glass->setProperty("shadow", 0.0); QTest::qWait(50); } static bool requiresShaderRendering(const char *testFunction) { static constexpr const char *renderingTests[] = { - "highlightToggleProducesDifferentRender", - "rimReflectionToggleProducesDifferentRender", - "lightAngleShiftMovesHighlight", + "shadowProducesDifferentRender", + "iorProducesDifferentRender", + "thicknessProducesDifferentRender", "radiusProducesTransparentCorners", "largeBezelWithSmallRadiusDoesNotIntroduceCornerDiagonalSeam", "largeBezelRemainsVisibleAlongStraightEdgesWithSmallRadius", - "highDispersionRefractsEdgesWithSpecularDisabled", "zeroBlurMultiplierStillAppliesGaussianBlur", "blurAmountAndMultiplierChangeRenderedBlurStrength", "blurToggleProducesDifferentRender", @@ -172,32 +163,21 @@ class GlassEffectTest : public QObject { return m_helper && !m_helper->usesSoftwareRenderer(); } + /// Reset glass to default property values (called before each test). void resetGlass() { - m_glass->setProperty("lightAngle", -135.0); - m_glass->setProperty("highlightEnabled", true); - m_glass->setProperty("rimReflectionEnabled", true); m_glass->setProperty("blurEnabled", false); m_glass->setProperty("blurMax", 36); m_glass->setProperty("blurAmount", 1.0); m_glass->setProperty("blurMultiplier", 0.0); m_glass->setProperty("radius", 0.0); - m_glass->setProperty("bezelWidth", 18.0); - m_glass->setProperty("thickness", 90.0); - m_glass->setProperty("displacementFactor", 0.45); - m_glass->setProperty("ior", 1.42); - m_glass->setProperty("dispersion", 0.012); - m_glass->setProperty("brightness", 0.0); - m_glass->setProperty("contrast", 0.0); - m_glass->setProperty("saturation", 0.0); - m_glass->setProperty("colorization", 0.0); - m_glass->setProperty("specularOpacity", 0.6); - m_glass->setProperty("strokeWidth", 1.0); - m_glass->setProperty("strokeStrength", 1.0); - m_glass->setProperty("lightPower", 2.0); - m_glass->setProperty("edgeSaturation", 0.0); - m_glass->setProperty("reflectionOffset", 12.0); + m_glass->setProperty("bezelWidth", 60.0); + m_glass->setProperty("thickness", 50.0); + m_glass->setProperty("ior", 3.0); + m_glass->setProperty("specularOpacity", 0.55); + m_glass->setProperty("tintOpacity", 0.08); + m_glass->setProperty("shadow", 0.5); QTest::qWait(50); } @@ -223,96 +203,12 @@ private Q_SLOTS: resetGlass(); } - // ── Property tests: read derived properties at runtime ───────────── - - void lightDirectionDerivesFromLightAngle() - { - // lightAngle default in GlassEffect.qml is -135° - // lightDirection = (cos(angle), sin(angle)) - const qreal angle = m_glass->property("lightAngle").toReal(); - QCOMPARE(angle, -135.0); - - const auto dir = m_glass->property("lightDirection").value(); - const qreal rad = angle * M_PI / 180.0; - QVERIFY(qAbs(dir.x() - std::cos(rad)) < 0.001); - QVERIFY(qAbs(dir.y() - std::sin(rad)) < 0.001); - - // lightAngleRadians should match - const qreal radians = m_glass->property("lightAngleRadians").toReal(); - QVERIFY(qAbs(radians - rad) < 0.001); - } - - void changingLightAngleUpdatesDerivedProperties() - { - // Set to 0° → direction should be (1, 0) - m_glass->setProperty("lightAngle", 0.0); - QTest::qWait(10); // let bindings update - const auto dir0 = m_glass->property("lightDirection").value(); - QVERIFY(qAbs(dir0.x() - 1.0) < 0.001); - QVERIFY(qAbs(dir0.y() - 0.0) < 0.001); - - // Set to 90° → direction should be (0, 1) - m_glass->setProperty("lightAngle", 90.0); - QTest::qWait(10); - const auto dir90 = m_glass->property("lightDirection").value(); - QVERIFY(qAbs(dir90.x() - 0.0) < 0.001); - QVERIFY(qAbs(dir90.y() - 1.0) < 0.001); - - // Set to -90° → direction should be (0, -1) - m_glass->setProperty("lightAngle", -90.0); - QTest::qWait(10); - const auto dirN90 = m_glass->property("lightDirection").value(); - QVERIFY(qAbs(dirN90.x() - 0.0) < 0.001); - QVERIFY(qAbs(dirN90.y() - (-1.0)) < 0.001); - } - - void highlightEnabledTogglesZeroShaderSpecular() - { - // Find the internal ShaderEffect (has objectName "glassShader") - auto *shader = m_glass->findChild("glassShader"); - QVERIFY(shader); - - // When highlightEnabled is true, shader gets non-zero specular values - m_glass->setProperty("highlightEnabled", true); - QTest::qWait(10); - - const qreal specOn = shader->property("specularOpacity").toReal(); - const qreal strokeOn = shader->property("strokeStrength").toReal(); - QVERIFY(specOn > 0.0); - QVERIFY(strokeOn > 0.0); - - // When highlightEnabled is false, both must be zero - m_glass->setProperty("highlightEnabled", false); - QTest::qWait(10); - - QCOMPARE(shader->property("specularOpacity").toReal(), 0.0); - QCOMPARE(shader->property("strokeStrength").toReal(), 0.0); - } - - void rimReflectionEnabledTogglesFloatGate() - { - auto *shader = m_glass->findChild("glassShader"); - QVERIFY(shader); - - // Enabled → 0.22 tint mix - m_glass->setProperty("rimReflectionEnabled", true); - QTest::qWait(10); - QCOMPARE(shader->property("rimReflectionStrength").toReal(), 0.22); + // ── Property tests ──────────────────────────────────────────────── - // Disabled → 0.0 (pure white specular, no tint) - m_glass->setProperty("rimReflectionEnabled", false); - QTest::qWait(10); - QCOMPARE(shader->property("rimReflectionStrength").toReal(), 0.0); - } - - void multiEffectEnabledReflectsBlurAndColorParams() + void multiEffectEnabledReflectsBlurParams() { - // Default scene: blurEnabled=false, all color params=0 → false + // Default scene: blurEnabled=false → false m_glass->setProperty("blurEnabled", false); - m_glass->setProperty("brightness", 0.0); - m_glass->setProperty("contrast", 0.0); - m_glass->setProperty("saturation", 0.0); - m_glass->setProperty("colorization", 0.0); QTest::qWait(10); QVERIFY(!m_glass->property("multiEffectEnabled").toBool()); @@ -320,33 +216,9 @@ private Q_SLOTS: m_glass->setProperty("blurEnabled", true); QTest::qWait(10); QVERIFY(m_glass->property("multiEffectEnabled").toBool()); - - // Non-zero brightness alone → true - m_glass->setProperty("blurEnabled", false); - m_glass->setProperty("brightness", 0.05); - QTest::qWait(10); - QVERIFY(m_glass->property("multiEffectEnabled").toBool()); - - // Non-zero contrast alone → true - m_glass->setProperty("brightness", 0.0); - m_glass->setProperty("contrast", -0.12); - QTest::qWait(10); - QVERIFY(m_glass->property("multiEffectEnabled").toBool()); - - // Non-zero saturation alone → true - m_glass->setProperty("contrast", 0.0); - m_glass->setProperty("saturation", -0.15); - QTest::qWait(10); - QVERIFY(m_glass->property("multiEffectEnabled").toBool()); - - // Non-zero colorization alone → true - m_glass->setProperty("saturation", 0.0); - m_glass->setProperty("colorization", 0.12); - QTest::qWait(10); - QVERIFY(m_glass->property("multiEffectEnabled").toBool()); } - void dconfigFacingGlassKnobsAreRuntimeQmlProperties() + void glassKnobsAreRuntimeQmlProperties() { auto *shader = m_glass->findChild("glassShader"); QVERIFY(shader); @@ -357,15 +229,10 @@ private Q_SLOTS: "blurMax", "bezelWidth", "thickness", - "displacementFactor", "ior", - "dispersion", - "brightness", - "contrast", - "saturation", - "colorization", - "edgeSaturation", - "reflectionOffset", + "specularOpacity", + "tintOpacity", + "shadow", }; for (const QByteArray &name : propertyNames) { @@ -376,9 +243,10 @@ private Q_SLOTS: QVERIFY(m_glass->setProperty("bezelWidth", 47.0)); QVERIFY(m_glass->setProperty("thickness", 133.0)); - QVERIFY(m_glass->setProperty("displacementFactor", 0.72)); - QVERIFY(m_glass->setProperty("ior", 1.37)); - QVERIFY(m_glass->setProperty("dispersion", 0.021)); + QVERIFY(m_glass->setProperty("ior", 2.0)); + QVERIFY(m_glass->setProperty("specularOpacity", 0.9)); + QVERIFY(m_glass->setProperty("tintOpacity", 0.2)); + QVERIFY(m_glass->setProperty("shadow", 0.8)); QVERIFY(m_glass->setProperty("blurEnabled", true)); QVERIFY(m_glass->setProperty("blurMax", 18)); QVERIFY(m_glass->setProperty("blurAmount", 0.75)); @@ -387,33 +255,16 @@ private Q_SLOTS: QCOMPARE(shader->property("bezelWidth").toReal(), 47.0); QCOMPARE(shader->property("thickness").toReal(), 133.0); - QCOMPARE(shader->property("displacementFactor").toReal(), 0.72); - QCOMPARE(shader->property("ior").toReal(), 1.37); - QCOMPARE(shader->property("dispersion").toReal(), 0.021); + QCOMPARE(shader->property("ior").toReal(), 2.0); + QCOMPARE(shader->property("specularOpacity").toReal(), 0.9); + QCOMPARE(shader->property("tintOpacity").toReal(), 0.2); + // shadow is a GlassEffect-level property consumed by the Stage 3 + // MultiEffect; it is not forwarded to the ShaderEffect. + QVERIFY(!shader->property("shadow").isValid()); + QCOMPARE(m_glass->property("shadow").toReal(), 0.8); QCOMPARE(m_glass->property("multiEffectEnabled").toBool(), true); } - void shaderReceivesEdgeMaterialPropertiesUsedByFragmentShader() - { - auto *shader = m_glass->findChild("glassShader"); - QVERIFY(shader); - - QVERIFY2(m_glass->setProperty("edgeSaturation", 1.35), - "GlassEffect must expose edgeSaturation as a runtime QML property"); - QVERIFY2(m_glass->setProperty("reflectionOffset", 27.0), - "GlassEffect must expose reflectionOffset as a runtime QML property"); - QTest::qWait(10); - - const bool edgeSaturationForwarded = shader->property("edgeSaturation").isValid(); - const bool reflectionOffsetForwarded = shader->property("reflectionOffset").isValid(); - QVERIFY2(edgeSaturationForwarded && reflectionOffsetForwarded, - qPrintable(QStringLiteral("glassShader must receive edgeSaturation and reflectionOffset for the fragment shader: edgeSaturation=%1 reflectionOffset=%2") - .arg(edgeSaturationForwarded ? QStringLiteral("valid") : QStringLiteral("missing")) - .arg(reflectionOffsetForwarded ? QStringLiteral("valid") : QStringLiteral("missing")))); - QCOMPARE(shader->property("edgeSaturation").toReal(), 1.35); - QCOMPARE(shader->property("reflectionOffset").toReal(), 27.0); - } - // ── Rendering tests: grabToImage + image comparison ─────────────── void grabIsDeterministic() @@ -424,179 +275,83 @@ private Q_SLOTS: const QImage img2 = grabImage(m_scene); QVERIFY(!img2.isNull()); - // Same scene, same settings → identical output QCOMPARE(img1, img2); } - void highlightToggleProducesDifferentRender() + void shadowProducesDifferentRender() { - // Set prominent glass params for visible highlights - m_glass->setProperty("highlightEnabled", true); m_glass->setProperty("radius", 34.0); - m_glass->setProperty("bezelWidth", 16.0); - m_glass->setProperty("specularOpacity", 0.82); - m_glass->setProperty("strokeStrength", 1.5); - m_glass->setProperty("strokeWidth", 1.4); + m_glass->setProperty("shadow", 0.0); QTest::qWait(50); - const QImage withHighlight = grabImage(m_scene); - QVERIFY(!withHighlight.isNull()); + const QImage noShadow = grabImage(m_scene); + QVERIFY(!noShadow.isNull()); - m_glass->setProperty("highlightEnabled", false); + m_glass->setProperty("shadow", 1.0); QTest::qWait(50); - const QImage withoutHighlight = grabImage(m_scene); - QVERIFY(!withoutHighlight.isNull()); - - // Images must differ — highlights are visible - QVERIFY(withHighlight != withoutHighlight); - - // The difference should be concentrated near edges (rim), not center - const int w = withHighlight.width(); - const int h = withHighlight.height(); - int edgeDiff = 0, centerDiff = 0; - const int edgeBand = 24; // px from border - const int cx = w / 2, cy = h / 2; - const int centerRadius = 32; + const QImage withShadow = grabImage(m_scene); + QVERIFY(!withShadow.isNull()); - for (int y = 0; y < h; ++y) { - const auto *p1 = reinterpret_cast(withHighlight.scanLine(y)); - const auto *p2 = reinterpret_cast(withoutHighlight.scanLine(y)); - for (int x = 0; x < w; ++x) { - if (p1[x] != p2[x]) { - if (x < edgeBand || x >= w - edgeBand || y < edgeBand || y >= h - edgeBand) - ++edgeDiff; - else if (std::hypot(x - cx, y - cy) < centerRadius) - ++centerDiff; - } - } - } - // Highlights are an edge/rim phenomenon - QVERIFY2(edgeDiff > centerDiff, - qPrintable(QStringLiteral("highlight diff should be edge-dominated: edge=%1 center=%2") - .arg(edgeDiff).arg(centerDiff))); + QVERIFY(noShadow != withShadow); } - void rimReflectionToggleProducesDifferentRender() + void iorProducesDifferentRender() { - // Ensure highlights are on for rim reflection to be visible - m_glass->setProperty("highlightEnabled", true); m_glass->setProperty("radius", 34.0); - m_glass->setProperty("rimReflectionEnabled", true); + m_glass->setProperty("bezelWidth", 40.0); + m_glass->setProperty("thickness", 80.0); + m_glass->setProperty("ior", 1.0); QTest::qWait(50); - const QImage withTint = grabImage(m_scene); - QVERIFY(!withTint.isNull()); + const QImage lowIor = grabImage(m_scene); + QVERIFY(!lowIor.isNull()); - m_glass->setProperty("rimReflectionEnabled", false); + m_glass->setProperty("ior", 3.0); QTest::qWait(50); - const QImage withoutTint = grabImage(m_scene); - QVERIFY(!withoutTint.isNull()); + const QImage highIor = grabImage(m_scene); + QVERIFY(!highIor.isNull()); - // Images must differ — rim tint affects specular color - const int diff = pixelDiffCount(withTint, withoutTint); - QVERIFY2(diff > 0, - qPrintable(QStringLiteral("rim reflection toggle must produce different output, got %1 differing pixels").arg(diff))); + QVERIFY(lowIor != highIor); } - void lightAngleShiftMovesHighlight() + void thicknessProducesDifferentRender() { - m_glass->setProperty("highlightEnabled", true); m_glass->setProperty("radius", 34.0); - m_glass->setProperty("specularOpacity", 0.82); - m_glass->setProperty("strokeStrength", 1.5); - - // Use 0° and 90° — the shader's specular is symmetric (both rim - // and opposite-rim contribute equally), so 0° vs 180° would be - // identical. 0° puts highlights on left/right edges; 90° puts - // them on top/bottom edges. - m_glass->setProperty("lightAngle", 0.0); + m_glass->setProperty("bezelWidth", 40.0); + m_glass->setProperty("thickness", 10.0); QTest::qWait(50); - const QImage imgHorizontal = grabImage(m_scene); - QVERIFY(!imgHorizontal.isNull()); - m_glass->setProperty("lightAngle", 90.0); + const QImage thinGlass = grabImage(m_scene); + QVERIFY(!thinGlass.isNull()); + + m_glass->setProperty("thickness", 150.0); QTest::qWait(50); - const QImage imgVertical = grabImage(m_scene); - QVERIFY(!imgVertical.isNull()); - - QVERIFY(imgHorizontal != imgVertical); - - // When light is at 0°, highlights are on left/right edges. - // When light is at 90°, highlights are on top/bottom edges. - // So horizontal-edge brightness should be higher at 90°, and - // vertical-edge brightness should be higher at 0°. - const int edgeBand = 16; - - auto edgeBrightness = [](const QImage &img, int startX, int endX) { - int sum = 0; - for (int y = 0; y < img.height(); ++y) { - const auto *p = reinterpret_cast(img.scanLine(y)); - for (int x = startX; x < endX; ++x) { - const QRgb px = p[x]; - sum += qRed(px) + qGreen(px) + qBlue(px); - } - } - return sum; - }; - auto rowBrightness = [](const QImage &img, int startY, int endY) { - int sum = 0; - for (int y = startY; y < endY; ++y) { - const auto *p = reinterpret_cast(img.scanLine(y)); - for (int x = 0; x < img.width(); ++x) { - const QRgb px = p[x]; - sum += qRed(px) + qGreen(px) + qBlue(px); - } - } - return sum; - }; + const QImage thickGlass = grabImage(m_scene); + QVERIFY(!thickGlass.isNull()); - const int w = imgHorizontal.width(); - const int h = imgHorizontal.height(); - - // Light at 0° → vertical edges (left/right) brighter - const int vertBright_0deg = edgeBrightness(imgHorizontal, 0, edgeBand) - + edgeBrightness(imgHorizontal, w - edgeBand, w); - const int vertBright_90deg = edgeBrightness(imgVertical, 0, edgeBand) - + edgeBrightness(imgVertical, w - edgeBand, w); - QVERIFY2(vertBright_0deg > vertBright_90deg, - qPrintable(QStringLiteral("vertical edges should be brighter at 0°: 0deg=%1 90deg=%2") - .arg(vertBright_0deg).arg(vertBright_90deg))); - - // Light at 90° → horizontal edges (top/bottom) brighter - const int horizBright_90deg = rowBrightness(imgVertical, 0, edgeBand) - + rowBrightness(imgVertical, h - edgeBand, h); - const int horizBright_0deg = rowBrightness(imgHorizontal, 0, edgeBand) - + rowBrightness(imgHorizontal, h - edgeBand, h); - QVERIFY2(horizBright_90deg > horizBright_0deg, - qPrintable(QStringLiteral("horizontal edges should be brighter at 90°: 90deg=%1 0deg=%2") - .arg(horizBright_90deg).arg(horizBright_0deg))); + QVERIFY(thinGlass != thickGlass); } void radiusProducesTransparentCorners() { - // Large radius for clearly transparent corners m_glass->setProperty("radius", 60.0); - m_glass->setProperty("highlightEnabled", false); QTest::qWait(50); - // Grab the glass item directly (not root) so the visible backdrop - // doesn't fill the transparent corners. const QImage img = grabImage(m_glass); QVERIFY(!img.isNull()); - // Corner pixels should be fully transparent (alpha = 0) const int w = img.width(); const int h = img.height(); - const int offset = 2; // a few pixels in from the absolute corner + const int offset = 2; const QList cornerPixels = { - {offset, offset}, // top-left - {w - 1 - offset, offset}, // top-right - {offset, h - 1 - offset}, // bottom-left - {w - 1 - offset, h - 1 - offset}, // bottom-right + {offset, offset}, + {w - 1 - offset, offset}, + {offset, h - 1 - offset}, + {w - 1 - offset, h - 1 - offset}, }; for (const auto &pt : cornerPixels) { @@ -606,7 +361,6 @@ private Q_SLOTS: .arg(pt.x()).arg(pt.y()).arg(qAlpha(px)))); } - // Center pixel should be opaque const QRgb centerPx = img.pixel(w / 2, h / 2); QVERIFY2(qAlpha(centerPx) == 255, qPrintable(QStringLiteral("center pixel should be opaque, got alpha=%1") @@ -629,7 +383,6 @@ private Q_SLOTS: void largeBezelRemainsVisibleAlongStraightEdgesWithSmallRadius() { setSmallRadiusLargeBezel(); - m_glass->setProperty("edgeSaturation", 1.5); QTest::qWait(50); const QImage largeBezel = grabImage(m_scene); @@ -641,11 +394,6 @@ private Q_SLOTS: const QImage tinyBezel = grabImage(m_scene); QVERIFY(!tinyBezel.isNull()); - // Far enough from the corners that radius must not suppress the - // straight-edge bezel, and far enough from the boundary that a shader - // clamping bezel width to radius would leave the region unchanged. - // Use the right edge: its horizontal refraction crosses the fixture's - // horizontal gradient, so a live bezel changes observable pixels. const QRect rightStraightEdge(212, 72, 24, 112); const int activePixels = regionDiffCount(largeBezel, tinyBezel, rightStraightEdge, 8); QVERIFY2(activePixels > rightStraightEdge.width() * rightStraightEdge.height() / 3, @@ -654,69 +402,8 @@ private Q_SLOTS: .arg(rightStraightEdge.width() * rightStraightEdge.height()))); } - void highDispersionRefractsEdgesWithSpecularDisabled() - { - setSmallRadiusLargeBezel(); - QVERIFY(m_glass->setProperty("highlightEnabled", false)); - QVERIFY(m_glass->setProperty("rimReflectionEnabled", false)); - QVERIFY(m_glass->setProperty("blurEnabled", false)); - QVERIFY(m_glass->setProperty("radius", 8.0)); - QVERIFY(m_glass->setProperty("bezelWidth", 64.0)); - QVERIFY(m_glass->setProperty("thickness", 140.0)); - QVERIFY(m_glass->setProperty("displacementFactor", 1.0)); - QVERIFY(m_glass->setProperty("ior", 1.5)); - QVERIFY(m_glass->setProperty("dispersion", 0.0)); - QVERIFY(m_glass->setProperty("strokeStrength", 0.0)); - QVERIFY(m_glass->setProperty("specularOpacity", 0.0)); - QVERIFY(m_glass->setProperty("edgeSaturation", 0.0)); - QTest::qWait(50); - - const QImage zeroDispersion = grabImage(m_scene); - QVERIFY(!zeroDispersion.isNull()); - - QVERIFY(m_glass->setProperty("dispersion", 0.2)); - QTest::qWait(50); - - const QImage highDispersion = grabImage(m_scene); - QVERIFY(!highDispersion.isNull()); - - // Sample all straight-edge bevels. The fixture has black vertical - // contrast bars at x=25% and x=75%, so the left/right bevels cross - // high-contrast backdrop content without relying on exact colors. - const QList edgeRefractionBands = { - QRect(0, 56, 72, 144), - QRect(184, 56, 72, 144), - QRect(56, 0, 144, 72), - QRect(56, 184, 144, 72), - }; - const QRect centerInterior(104, 104, 48, 48); - - int edgeChanged = 0; - int edgePixels = 0; - for (const QRect &band : edgeRefractionBands) { - edgeChanged += regionDiffCount(zeroDispersion, highDispersion, band, 6); - edgePixels += band.width() * band.height(); - } - const int centerChanged = regionDiffCount(zeroDispersion, highDispersion, centerInterior, 6); - - QVERIFY2(edgeChanged > edgePixels / 10, - qPrintable(QStringLiteral("high dispersion should visibly change edge refraction without specular: edgeChanged=%1 regionPixels=%2") - .arg(edgeChanged) - .arg(edgePixels))); - QVERIFY2(centerChanged < centerInterior.width() * centerInterior.height() / 20, - qPrintable(QStringLiteral("high dispersion should leave the flat center comparatively stable: centerChanged=%1 regionPixels=%2") - .arg(centerChanged) - .arg(centerInterior.width() * centerInterior.height()))); - QVERIFY2(edgeChanged > centerChanged * 4 + 20, - qPrintable(QStringLiteral("dispersion response should be edge-dominated: edgeChanged=%1 centerChanged=%2") - .arg(edgeChanged) - .arg(centerChanged))); - } - void zeroBlurMultiplierStillAppliesGaussianBlur() { - m_glass->setProperty("highlightEnabled", false); - m_glass->setProperty("rimReflectionEnabled", false); m_glass->setProperty("blurEnabled", true); m_glass->setProperty("blurMax", 48); m_glass->setProperty("blurAmount", 1.0); @@ -742,7 +429,6 @@ private Q_SLOTS: void blurAmountAndMultiplierChangeRenderedBlurStrength() { - m_glass->setProperty("highlightEnabled", false); m_glass->setProperty("blurEnabled", true); m_glass->setProperty("blurMax", 48); m_glass->setProperty("radius", 34.0); @@ -769,6 +455,7 @@ private Q_SLOTS: .arg(changedPixels) .arg(centerContrastFeature.width() * centerContrastFeature.height()))); } + void blurToggleProducesDifferentRender() { m_glass->setProperty("blurEnabled", true);