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);