diff --git a/docs/liquid-glass-dispersion.md b/docs/liquid-glass-dispersion.md new file mode 100644 index 0000000000..dffdb4241f --- /dev/null +++ b/docs/liquid-glass-dispersion.md @@ -0,0 +1,147 @@ +# Liquid Glass 色散(Chromatic Dispersion)实现说明 + +> 文档记录删除前的实现方式,供后续重做时查阅。 +> **当前代码库已移除色散功能**;玻璃仅保留单通道折射 + Fresnel 高光。 + +## 1. 目标观感 + +希望边缘出现由**折射后的背景**被「拉开」形成的蓝/青/紫细边,而不是: + +- 在玻璃轮廓上描固定色边 +- 对未折射底图做大位移 RGB 错位(露底、叠红) +- 轴对齐矩形色带 + +理想结构: + +```text +玻璃外缘 (SDF) ──弧形环带── 折射后的内容轮廓 + ↑ + 色散主要出现在这里 +``` + +## 2. 整体管线(删除前) + +```text +1) 圆角矩形 SDF → 外形裁切 + AA +2) bezel 剖面 surfaceHeight(t) → 边缘斜率 slopeMag +3) 平面法线 n2(圆角连续旋转) +4) 单通道主折射: + inward = -n2 + magG = Snell 楔形幅度 * contentRamp + uvG = texCoord + inward * magG / size +5) 色散(可选): + 在 distFromEdge < dispersionWidth 的环带内 + 对 R/G/B 使用略不同的 mag(同一 inward 方向) + color = mix(sG, vec3(sR.r, sG.g, sB.b), fringeMask) +6) Fresnel 式边缘高光 + tint +``` + +## 3. 主折射(与色散共用的场) + +### 3.1 几何 + +| 量 | 含义 | +|----|------| +| `distFromEdge` | SDF 内侧距离(像素) | +| `bezel` | 厚度从 0→满的过渡带宽度 | +| `t = clamp(distFromEdge/bezel,0,1)` | 在 bezel 内的归一化位置 | +| `h = surfaceHeight(t)` | squircle 剖面相对高度 | +| `H = h * thickness` | 有效光学高度 | +| `n2` | 圆角矩形连续平面法线(指向外侧) | +| `inward = -n2` | 内容采样方向(向内拉) | + +### 3.2 幅度(Snell 薄楔近似) + +```text +slopeMag = min(|dh| * thickness/bezel, refractionMaxTan) +slopeAngle = atan(slopeMag) +eta = 1/ior +sinI = slopeMag / sqrt(slopeMag²+1) +mag = H * contentRamp * max(tan(slopeAngle) - tan(asin(eta*sinI)), 0) +mag = min(mag, maxDisp) +off = inward * mag +uv = texCoord + off/size +``` + +### 3.3 contentRamp(贴边 vs 折射强度) + +```text +contentRamp = mix(contentEdgePull, 1.0, smoothstep(0, contentRampEnd, t)) +``` + +- **contentEdgePull**:贴边保留多少折射位移(默认 0.42) +- **contentRampEnd**:bezel 内 t 到多少拉满(默认 0.50) +- 用于减轻「图标被拽进内侧、外圈大黑缝」,与色散正交,删除色散后仍可保留。 + +## 4. 色散算法(已删除部分) + +### 4.1 参数 + +| Uniform / API | 默认 | 含义 | +|---------------|------|------| +| `dispersion` / `dispersionPx` | 5 | 最大通道分离(像素量级) | +| `dispersionWidth` | 6 | 环带宽度(从玻璃外缘向内,px);0=关 | +| `dispersionBlend` | 0.75 | 混入强度 | + +DConfig:`glassDispersionPx` / `glassDispersionWidth` / `glassDispersionBlend`。 + +### 4.2 空间 mask + +```text +fringeMask = smoothstep 从外缘向内 width 像素 +fringeMask = fringeMask² * blend +``` + +仅在此外圈环带做 CA;带外只用 G 路径单次采样。 + +### 4.3 分通道折射幅度 + +```text +di ∝ dispersion * fringeMask +iorR = ior - di, iorG = ior, iorB = ior + di // 蓝光 n 更大 +magR, magG, magB = 各自 Snell 幅度 +// 保证最小分离,避免 di 太小时看不出 CA: +if (magB - magR < dispersion*fringeMask) + 以 magG 为中心强制拉开 minSplit +uvR/G/B = texCoord + inward * magR/G/B / size +color = mix(sG, vec3(sR.r, sG.g, sB.b), fringeMask) +``` + +要点: + +- **方向相同**(都是 `inward`),只变 \|pull\| +- **不**用未折射的 `texCoord` 单独作为 R 通道(避免露底叠红) +- 结果色来自背景采样差,不是固定调色板 + +### 4.4 与错误实现的对比 + +| 错误 | 后果 | +|------|------| +| R 采 `texCoord`(位移 0),B 大位移 | 露底图 + 叠红 | +| 固定蓝/青 `mix` 染色 | 像描边不是色散 | +| CA 强度绑整块 displacement | 整个折射区都在色散,width 失效 | +| 轴对齐大 offset | 矩形色带,不贴圆角弧 | + +## 5. 已知局限(删除原因) + +1. 环带锚在 **玻璃 SDF 外缘**,不是严格的「折射后图标轮廓」;易与目标效果的「贴小圆角的弧形膜」不一致。 +2. 最小 px 分离与 Snell 微差混用,大 thickness 时仍可能脏边、黑点(远处深色被拉开)。 +3. 多次调参仍难稳定达到「细、柔、青蓝、弧形填缝」观感。 +4. 产品决策:**去掉色散**,只保留单通道折射玻璃。 + +## 6. 删除后的代码状态 + +- Shader:仅 `magG` 单次折射采样 + Fresnel 高光 +- 移除 uniforms / QML / dconfig / test_glass 滑条 / 色散单测 +- **保留** `contentEdgePull` / `contentRampEnd` / `refractionMaxTan`(折射塑形,与色散无关) + +## 7. 若将来重做,建议顺序 + +1. 先把单通道 distortion 做到:直线进圆角连续弯、内容贴边、无大黑缝 +2. 再在**折射 UV**上做 1–3px 级 CA,mask 跟折射梯度或内容轮廓走 +3. 最后才加 Fresnel/Bloom +4. 验收以 QQ 顶弧是否「细青蓝膜」为准,而不是 RGB 分离是否「够大」 + +--- + +*文档对应删除前工作树中的 `liquidglass.frag` 实现;删除提交见后续 git history。* diff --git a/examples/test_glass/CMakeLists.txt b/examples/test_glass/CMakeLists.txt index e150955870..36da6e435d 100644 --- a/examples/test_glass/CMakeLists.txt +++ b/examples/test_glass/CMakeLists.txt @@ -54,6 +54,7 @@ qt_add_qml_module(treeland_stub ) qt_add_shaders(treeland_stub "treeland_stub_liquidglass_shaders" PRECOMPILE + OPTIMIZED PREFIX "/shaders" BASE diff --git a/examples/test_glass/Main.qml b/examples/test_glass/Main.qml index 40666541cf..fe20e82acd 100644 --- a/examples/test_glass/Main.qml +++ b/examples/test_glass/Main.qml @@ -15,29 +15,30 @@ Item { readonly property url wallpaperSource: Helper.wallpaperSource property bool glassMode: true + property real effectWidth: 300 + property real effectHeight: 200 + property real effectRadius: 12 + property real glassThickness: 40 + property real glassBezelWidth: 36 + property real glassIor: 1.33 property bool glassBlurEnabled: true - property real effectRadius: 34 - property real glassBezelWidth: 30 - 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 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 int glassBlurMax: 64 + property real glassBlurAmount: 0.6 + property real glassBlurMultiplier: 0.0 + property real glassSpecular: 0.0 + property real glassTint: 0.0 + property real glassRefractionMaxTan: 2.0 + property real glassContentEdgePull: 0.0 + property real glassContentRampEnd: 0.0 + property real glassProfilePower: 2.0 + property real glassInnerShadow: 0.0 + property real glassBrightness: 0.0 + property real glassContrast: 0.0 + property real glassSaturation: 0.0 property bool advancedExpanded: false + + Shortcut { sequences: [StandardKey.Quit] context: Qt.ApplicationShortcut @@ -174,95 +175,130 @@ Item { }) } } - 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 + GridLayout { + columns: 2 + columnSpacing: 14 + rowSpacing: 4 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 } + Slider { from: 0; to: 120; value: root.effectRadius; onMoved: root.effectRadius = 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 } + Slider { from: 0; to: 128; stepSize: 1; value: root.glassBlurMax; onMoved: root.glassBlurMax = value } } } - // ── Advanced toggle ───────────────────────── Button { - text: root.advancedExpanded ? "▼ Advanced" : "▶ Advanced" + text: (root.advancedExpanded ? "▼" : "▶") + " Advanced" onClicked: root.advancedExpanded = !root.advancedExpanded } - // ── Advanced sliders (collapsible, multi-column) ── - Row { - spacing: 20 + GridLayout { visible: root.advancedExpanded + columns: 4 + columnSpacing: 14 + rowSpacing: 4 - // Column A: glass material & optics + Column { + spacing: 2 + Label { text: "width " + Math.round(root.effectWidth); color: "white" } + Slider { from: 30; to: 700; value: root.effectWidth; onMoved: root.effectWidth = value } + } + Column { + spacing: 2 + Label { text: "height " + Math.round(root.effectHeight); color: "white" } + Slider { from: 30; to: 800; value: root.effectHeight; onMoved: root.effectHeight = value } + } 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 } + Slider { from: 10; to: 200; value: root.glassThickness; onMoved: root.glassThickness = 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: "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 } + Slider { from: 1.0; to: 3.0; stepSize: 0.05; value: root.glassIor; onMoved: root.glassIor = 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 } + Label { text: "blurAmount " + root.glassBlurAmount.toFixed(2); color: "white" } + Slider { from: 0; to: 1; stepSize: 0.05; value: root.glassBlurAmount; onMoved: root.glassBlurAmount = value } + } + Column { + spacing: 2 + Label { text: "blurMultiplier " + root.glassBlurMultiplier.toFixed(2); color: "white" } + Slider { from: 0; to: 4; stepSize: 0.05; value: root.glassBlurMultiplier; onMoved: root.glassBlurMultiplier = value } + } + Label { + text: "Optics / Profile" + color: "white" + font.bold: true + Layout.columnSpan: 4 + } + Column { + spacing: 2 + Label { text: "specular " + root.glassSpecular.toFixed(2); color: "white" } + Slider { from: 0; to: 1; stepSize: 0.05; value: root.glassSpecular; onMoved: root.glassSpecular = value } + } + Column { + spacing: 2 + Label { text: "tint " + Math.round(root.glassTint * 100) + "%"; color: "white" } + Slider { from: 0; to: 0.4; stepSize: 0.01; value: root.glassTint; onMoved: root.glassTint = value } + } + Column { + spacing: 2 + Label { text: "max tan " + root.glassRefractionMaxTan.toFixed(2); color: "white" } + Slider { from: 0.5; to: 20; stepSize: 0.05; value: root.glassRefractionMaxTan; onMoved: root.glassRefractionMaxTan = value } + } + Column { + spacing: 2 + Label { text: "edge pull " + root.glassContentEdgePull.toFixed(2); color: "white" } + Slider { from: 0; to: 1; stepSize: 0.05; value: root.glassContentEdgePull; onMoved: root.glassContentEdgePull = value } + } + Column { + spacing: 2 + Label { text: "ramp end " + root.glassContentRampEnd.toFixed(2); color: "white" } + Slider { from: 0.05; to: 1; stepSize: 0.05; value: root.glassContentRampEnd; onMoved: root.glassContentRampEnd = value } + } + Column { + spacing: 2 + Label { text: "profilePower " + root.glassProfilePower.toFixed(2); color: "white" } + Slider { from: 1; to: 10; stepSize: 0.1; value: root.glassProfilePower; onMoved: root.glassProfilePower = value } + } + Column { + spacing: 2 + Label { text: "innerShadow " + root.glassInnerShadow.toFixed(2); color: "white" } + Slider { from: 0; to: 1; stepSize: 0.05; value: root.glassInnerShadow; onMoved: root.glassInnerShadow = 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 } + Slider { from: -1; to: 1; stepSize: 0.05; value: root.glassBrightness; onMoved: root.glassBrightness = value } + } + Column { + spacing: 2 Label { text: "contrast " + root.glassContrast.toFixed(2); color: "white" } - Slider { from: -1; to: 1; value: root.glassContrast; onMoved: root.glassContrast = value } + Slider { from: -1; to: 1; stepSize: 0.05; value: root.glassContrast; onMoved: root.glassContrast = value } + } + Column { + spacing: 2 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 } + Slider { from: -1; to: 1; stepSize: 0.05; value: root.glassSaturation; onMoved: root.glassSaturation = value } } } } @@ -352,13 +388,14 @@ Item { Item { id: effectPanel - width: 500 - height: 300 + width: root.effectWidth + height: root.effectHeight x: (parent.width - width) / 2 y: (parent.height - height) / 2 MouseArea { anchors.fill: parent + enabled: true drag.target: effectPanel drag.axis: Drag.XAndYAxis drag.minimumX: 0 @@ -384,29 +421,27 @@ Item { radius: root.effectRadius blurEnabled: root.glassBlurEnabled blurMax: root.glassBlurMax + blurAmount: root.glassBlurAmount + blurMultiplier: root.glassBlurMultiplier bezelWidth: root.glassBezelWidth thickness: root.glassThickness - displacementFactor: root.glassDisplacementFactor ior: root.glassIor - dispersion: root.glassDispersion + specular: root.glassSpecular + tint: root.glassTint + refractionMaxTan: root.glassRefractionMaxTan + contentEdgePull: root.glassContentEdgePull + contentRampEnd: root.glassContentRampEnd + profilePower: root.glassProfilePower + innerShadow: root.glassInnerShadow + smooth: true 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 - smooth: true } } } + Component { id: globalBlurComponent RenderBufferBlitter { diff --git a/examples/test_glass/helper.h b/examples/test_glass/helper.h index 137cd33f94..e367674e09 100644 --- a/examples/test_glass/helper.h +++ b/examples/test_glass/helper.h @@ -27,14 +27,17 @@ 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 glassSpecular READ glassSpecular WRITE setGlassSpecular NOTIFY glassSpecularChanged) + Q_PROPERTY(qreal glassTint READ glassTint WRITE setGlassTint NOTIFY glassTintChanged) + Q_PROPERTY(qreal glassContentEdgePull READ glassContentEdgePull WRITE setGlassContentEdgePull NOTIFY glassContentEdgePullChanged) + Q_PROPERTY(qreal glassContentRampEnd READ glassContentRampEnd WRITE setGlassContentRampEnd NOTIFY glassContentRampEndChanged) + Q_PROPERTY(qreal glassRefractionMaxTan READ glassRefractionMaxTan WRITE setGlassRefractionMaxTan NOTIFY glassRefractionMaxTanChanged) + Q_PROPERTY(qreal glassProfilePower READ glassProfilePower WRITE setGlassProfilePower NOTIFY glassProfilePowerChanged) + Q_PROPERTY(qreal glassInnerShadow READ glassInnerShadow WRITE setGlassInnerShadow NOTIFY glassInnerShadowChanged) 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 glassContrast READ glassContrast WRITE setGlassContrast NOTIFY glassContrastChanged) + Q_PROPERTY(qreal glassSaturation READ glassSaturation WRITE setGlassSaturation NOTIFY glassSaturationChanged) public: explicit GlassConfig(QObject *parent = nullptr); @@ -53,22 +56,28 @@ 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 glassSpecular() const { return m_glassSpecular; } + void setGlassSpecular(qreal v) { if (m_glassSpecular != v) { m_glassSpecular = v; emit glassSpecularChanged(); } } + qreal glassTint() const { return m_glassTint; } + void setGlassTint(qreal v) { if (m_glassTint != v) { m_glassTint = v; emit glassTintChanged(); } } + qreal glassContentEdgePull() const { return m_glassContentEdgePull; } + void setGlassContentEdgePull(qreal v) { if (m_glassContentEdgePull != v) { m_glassContentEdgePull = v; emit glassContentEdgePullChanged(); } } + qreal glassContentRampEnd() const { return m_glassContentRampEnd; } + void setGlassContentRampEnd(qreal v) { if (m_glassContentRampEnd != v) { m_glassContentRampEnd = v; emit glassContentRampEndChanged(); } } + qreal glassRefractionMaxTan() const { return m_glassRefractionMaxTan; } + void setGlassRefractionMaxTan(qreal v) { if (m_glassRefractionMaxTan != v) { m_glassRefractionMaxTan = v; emit glassRefractionMaxTanChanged(); } } + qreal glassProfilePower() const { return m_glassProfilePower; } + void setGlassProfilePower(qreal v) { if (m_glassProfilePower != v) { m_glassProfilePower = v; emit glassProfilePowerChanged(); } } + qreal glassInnerShadow() const { return m_glassInnerShadow; } + void setGlassInnerShadow(qreal v) { if (m_glassInnerShadow != v) { m_glassInnerShadow = v; emit glassInnerShadowChanged(); } } 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 glassContrast() const { return m_glassContrast; } + void setGlassContrast(qreal v) { if (m_glassContrast != v) { m_glassContrast = v; emit glassContrastChanged(); } } + qreal glassSaturation() const { return m_glassSaturation; } + void setGlassSaturation(qreal v) { if (m_glassSaturation != v) { m_glassSaturation = v; emit glassSaturationChanged(); } } Q_SIGNALS: void glassEnabledChanged(); @@ -77,30 +86,36 @@ class GlassConfig : public QObject { void blurMultiplierChanged(); void glassBezelChanged(); void glassThicknessChanged(); - void glassDisplacementFactorChanged(); void glassIorChanged(); - void glassDispersionChanged(); + void glassSpecularChanged(); + void glassTintChanged(); + void glassContentEdgePullChanged(); + void glassContentRampEndChanged(); + void glassRefractionMaxTanChanged(); + void glassProfilePowerChanged(); + void glassInnerShadowChanged(); void glassBrightnessChanged(); - void glassEdgeSaturationChanged(); - void glassLightAngleChanged(); - void glassReflectionOffsetChanged(); - void glassHighlightEnabledChanged(); + void glassContrastChanged(); + void glassSaturationChanged(); private: bool m_glassEnabled = true; - int m_blurStrength = 20; - qreal m_blurAmount = 1.0; + int m_blurStrength = 60; + qreal m_blurAmount = 0.6; qreal m_blurMultiplier = 0.0; - qreal m_glassBezel = 30; - qreal m_glassThickness = 50; - qreal m_glassDisplacementFactor = 1.0; - qreal m_glassIor = 1.2; - qreal m_glassDispersion = 0.0; + qreal m_glassBezel = 36; + qreal m_glassThickness = 40; + qreal m_glassIor = 1.33; + qreal m_glassSpecular = 0.0; + qreal m_glassTint = 0.0; + qreal m_glassContentEdgePull = 0.0; + qreal m_glassContentRampEnd = 0.0; + qreal m_glassRefractionMaxTan = 2.0; + qreal m_glassProfilePower = 2.0; + qreal m_glassInnerShadow = 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_glassContrast = 0.0; + qreal m_glassSaturation = 0.0; }; WAYLIB_SERVER_BEGIN_NAMESPACE diff --git a/examples/test_glass/main.cpp b/examples/test_glass/main.cpp index fec3a8c3d9..2c54085c17 100644 --- a/examples/test_glass/main.cpp +++ b/examples/test_glass/main.cpp @@ -60,12 +60,5 @@ int main(int argc, char *argv[]) { helper->initProtocols(window, &waylandEngine); - // multi output - qobject_cast(helper->backend()->handle())->for_each_backend([] (wlr_backend *backend, void *) { - if (auto x11 = qw_x11_backend::from(backend)) { - x11->output_create(); - } - }, nullptr); - return app.exec(); } diff --git a/misc/dconfig/org.deepin.dde.treeland.user.json b/misc/dconfig/org.deepin.dde.treeland.user.json index cacdfbe13a..f6e5ce71e6 100644 --- a/misc/dconfig/org.deepin.dde.treeland.user.json +++ b/misc/dconfig/org.deepin.dde.treeland.user.json @@ -476,35 +476,35 @@ "visibility": "public" }, "glassBezel": { - "value": 30, + "value": 36, "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 rounded edge bevel width in pixels for the Liquid Glass effect. Range: 2-60.", + "description[zh_CN]": "液态玻璃效果的圆角边缘倒角宽度(像素)。范围:2-60。", "permissions": "readwrite", "visibility": "public" }, "glassThickness": { - "value": 20, + "value": 40, "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 glass thickness in pixels for Liquid Glass refraction. Range: 10.0-200.0.", + "description[zh_CN]": "液态玻璃折射使用的玻璃厚度(像素)。范围:10.0-200.0。", "permissions": "readwrite", "visibility": "public" }, "blurAmount": { - "value": 1.0, + "value": 0.6, "serial": 0, "flags": [], "name": "Blur Amount", "name[zh_CN]": "模糊量", - "description": "Normalized blur amount applied by Qt MultiEffect. 0 disables the blur contribution while keeping the effect item alive; 1 uses the configured blur strength fully. Range: 0.0-1.0.", - "description[zh_CN]": "Qt MultiEffect 使用的归一化模糊量。0 表示关闭模糊贡献但保留效果项,1 表示完整使用配置的模糊强度。范围:0.0-1.0。", + "description": "Normalized blur amount applied by Qt MultiEffect. 0 disables the blur contribution while keeping the effect item alive; default 0.6 uses 60% of the configured blur strength. Range: 0.0-1.0.", + "description[zh_CN]": "Qt MultiEffect 使用的归一化模糊量。0 表示关闭模糊贡献但保留效果项;默认 0.6 表示使用配置模糊强度的 60%。范围:0.0-1.0。", "permissions": "readwrite", "visibility": "public" }, @@ -519,91 +519,36 @@ "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, - "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": { + "glassSaturation": { "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。", + "name": "Glass Saturation", + "name[zh_CN]": "玻璃饱和度", + "description": "Saturation adjustment applied by Qt MultiEffect before Liquid Glass refraction. Range: -1.0-1.0.", + "description[zh_CN]": "液态玻璃折射前由 Qt MultiEffect 应用的饱和度调节。范围:-1.0-1.0。", "permissions": "readwrite", "visibility": "public" }, - "glassBrightness": { + "glassSpecular": { "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。", + "name": "Glass Specular", + "name[zh_CN]": "玻璃高光强度", + "description": "Specular highlight intensity for the Liquid Glass rim and Fresnel response. 0 disables specular highlights. Range: 0.0-1.0.", + "description[zh_CN]": "液态玻璃边缘高光与菲涅尔响应强度。0 表示关闭高光。范围:0.0-1.0。", "permissions": "readwrite", "visibility": "public" }, - "glassEdgeSaturation": { - "value": 0.4, - "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。", - "permissions": "readwrite", - "visibility": "public" - }, - "glassLightAngle": { - "value": -127, - "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。", - "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, + "glassProfilePower": { + "value": 2.0, "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 Profile Power", + "name[zh_CN]": "玻璃轮廓幂次", + "description": "Surface profile exponent controlling the super-ellipse shape. 2 produces a rounder profile; higher values sharpen the bezel transition. Range: 1.0-10.0.", + "description[zh_CN]": "控制超椭圆形状的表面轮廓指数。2 产生更圆的轮廓;值越高边缘过渡越尖锐。范围:1.0-10.0。", "permissions": "readwrite", "visibility": "public" } diff --git a/misc/shaders/liquidglass.frag b/misc/shaders/liquidglass.frag index ca5a9a9383..2bf8d71bea 100644 --- a/misc/shaders/liquidglass.frag +++ b/misc/shaders/liquidglass.frag @@ -3,27 +3,6 @@ #version 440 -// Liquid Glass fragment shader — closely follows the liquid-dom reference -// implementation (https://github.com/AndrewPrifer/liquid-dom). -// -// 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) -// -// 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. - layout(location = 0) in vec2 texCoord; layout(location = 0) out vec4 fragColor; @@ -31,370 +10,466 @@ 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 + vec2 lightDirection; + float radius; + float bezelWidth; + float thickness; + float ior; + float specular; + float tint; + float contentEdgePull; // fraction of optical pull kept at the glass lip (0–1) + float contentRampEnd; // t in bezel where content pull reaches full (0–1) + float refractionMaxTan; // cap on geometric slope tan(theta) + float profilePower; // surface profile exponent (default 4.0 = squircle) + float innerShadow; // inner shadow intensity (0–1, default 0.25) } 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); +const float kSqrt2 = 1.4142135623730951; +const float kInvSqrt2 = 0.7071067811865476; + +vec2 halfSize = max(ubuf.itemSize, vec2(1.0)) * 0.5; + +float sdRoundedRect(vec2 p, vec2 halfSize, float r) +{ + float rr = min(max(r, 0.0), min(halfSize.x, halfSize.y)); + vec2 q = abs(p) - halfSize + rr; + return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - rr; +} + +// Analytic gradient of sdRoundedRect w.r.t. p (outward-pointing, unit length on the surface). +vec2 sdRoundedRectGrad(vec2 p, vec2 halfSize, float r) +{ + float rr = min(max(r, 0.0), min(halfSize.x, halfSize.y)); + vec2 ap = abs(p); + vec2 q = ap - halfSize + rr; + vec2 s = vec2(p.x < 0.0 ? -1.0 : 1.0, p.y < 0.0 ? -1.0 : 1.0); + + if (min(q.x, q.y) > 0.0) { + float ql = length(q); + return s * (q / max(ql, 1e-6)); + } + + // Flat / interior: sd = max(q.x, q.y) - rr when inside the expanded rect. + if (q.x > q.y) + return vec2(s.x, 0.0); + if (q.y > q.x) + return vec2(0.0, s.y); + // Tie on the diagonal — any convex combination is a subgradient; pick the bisector. + return s * kInvSqrt2; +} + +// ── Linear bezel field ──────────────────────────────────────────────── +float fieldBezel() +{ + float limit = min(halfSize.x, halfSize.y); + return clamp(ubuf.bezelWidth, 1.0, max(limit - 1.0, 1.0)); +} + +float fieldT(vec2 p, float rr) +{ + float limit = min(halfSize.x, halfSize.y); + float r = clamp(rr, 0.0, limit); + float b = fieldBezel(); + return clamp(-sdRoundedRect(p, halfSize, r) / b, 0.0, 1.0); +} + +// Outward unit normal of the bezel field (matches previous -normalize(∇fieldT)). +vec2 fieldNormal(vec2 p, float rr) +{ + vec2 g = sdRoundedRectGrad(p, halfSize, rr); + float len = length(g); + return len > 1e-5 ? (g / len) : vec2(0.0); +} + +// ── Rounded-square Coons map (actual → virtual) ─────────────────────── +struct BoundaryResult { + vec2 point; + vec2 derivative; +}; -// --------------------------------------------------------------------------- -// SDF helpers (rounded rectangle) -// --------------------------------------------------------------------------- +struct RoundedMapResult { + vec2 point; + vec4 jacobian; +}; -// 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 cornerQuintic(float value) { - 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); + float s = clamp(value, 0.0, 1.0); + return s * s * s * (s * (s * 6.0 - 15.0) + 10.0); } -// Normal-only path for the expanded-radius refraction field (dist unused). -vec2 roundedRectNormal(vec2 p, vec2 halfSize, float radius) +float cornerQuinticDerivative(float value) { - 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)); + float s = clamp(value, 0.0, 1.0); + return 30.0 * s * s * (s - 1.0) * (s - 1.0); } -// --------------------------------------------------------------------------- -// Colour / math helpers -// --------------------------------------------------------------------------- +float cornerArcParameter(float value) +{ + float s = clamp(value, 0.0, 1.0); + float tangent = kSqrt2; + float s2 = s * s; + float s3 = s2 * s; + return (s3 - 2.0 * s2 + s) * tangent + + (-2.0 * s3 + 3.0 * s2) + + (s3 - s2) * tangent; +} -vec3 applySaturation(vec3 color, float saturationValue) +float cornerArcParameterDerivative(float value) { - float luminance = dot(color, kLumaWeights); - return mix(vec3(luminance), color, saturationValue); + float s = clamp(value, 0.0, 1.0); + float tangent = kSqrt2; + return (3.0 * s * s - 4.0 * s + 1.0) * tangent + + (-6.0 * s * s + 6.0 * s) + + (3.0 * s * s - 2.0 * s) * tangent; } -vec4 sampleBackdrop(vec2 coord) +BoundaryResult cornerUnitArc(vec2 start, vec2 end, float parameter, float parameterDerivative) { - // Soft clamp: ShaderEffect source wrap is not guaranteed clamp-to-edge. - return texture(source, clamp(coord, vec2(0.001), vec2(0.999))); + vec2 delta = end - start; + vec2 raw = start + delta * parameter; + float rawLength = length(raw); + vec2 direction = raw / rawLength; + vec2 rawDerivative = delta * parameterDerivative; + float parallel = dot(direction, rawDerivative); + BoundaryResult result; + result.point = direction; + result.derivative = (rawDerivative - direction * parallel) / rawLength; + return result; } -// Product defaults are lightPower 2 or 3 — multiply is cheaper than pow. -float specularPower(float x, float p) +BoundaryResult cornerTopBoundary(float value, float alpha) { - 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; + BoundaryResult result; + if (alpha <= 1e-5 || value <= 1.0 - alpha) { + result.point = vec2(value, 1.0); + result.derivative = vec2(1.0, 0.0); + return result; } - return pow(v, p); + float local = (value - (1.0 - alpha)) / alpha; + BoundaryResult arc = cornerUnitArc(vec2(0.0, 1.0), vec2(kInvSqrt2), + cornerArcParameter(local), cornerArcParameterDerivative(local)); + result.point = vec2(1.0 - alpha) + alpha * arc.point; + result.derivative = arc.derivative; + return result; } -// 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) +BoundaryResult cornerRightBoundary(float value, float alpha) { - 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); + BoundaryResult result; + if (alpha <= 1e-5 || value <= 1.0 - alpha) { + result.point = vec2(1.0, value); + result.derivative = vec2(0.0, 1.0); + return result; + } + float local = (value - (1.0 - alpha)) / alpha; + BoundaryResult arc = cornerUnitArc(vec2(1.0, 0.0), vec2(kInvSqrt2), + cornerArcParameter(local), cornerArcParameterDerivative(local)); + result.point = vec2(1.0 - alpha) + alpha * arc.point; + result.derivative = arc.derivative; + return result; +} + +RoundedMapResult roundedSquareMap(vec2 canonical, float alpha) +{ + float u = canonical.x; + float v = canonical.y; + BoundaryResult top = cornerTopBoundary(u, alpha); + BoundaryResult right = cornerRightBoundary(v, alpha); + vec2 topDelta = top.point - vec2(u, 1.0); + vec2 rightDelta = right.point - vec2(1.0, v); + float cornerDelta = alpha * (kInvSqrt2 - 1.0); + float hu = cornerQuintic(u); + float hv = cornerQuintic(v); + float dhu = cornerQuinticDerivative(u); + float dhv = cornerQuinticDerivative(v); + vec2 topDerivative = top.derivative - vec2(1.0, 0.0); + vec2 rightDerivative = right.derivative - vec2(0.0, 1.0); + RoundedMapResult result; + result.point = canonical + + hv * topDelta + + hu * rightDelta + - hu * hv * vec2(cornerDelta); + result.jacobian = vec4( + 1.0 + hv * topDerivative.x + dhu * rightDelta.x - dhu * hv * cornerDelta, + dhv * topDelta.x + hu * rightDerivative.x - hu * dhv * cornerDelta, + hv * topDerivative.y + dhu * rightDelta.y - dhu * hv * cornerDelta, + 1.0 + dhv * topDelta.y + hu * rightDerivative.y - hu * dhv * cornerDelta + ); + return result; +} + +vec4 inverseCornerJacobian(vec4 jacobian) +{ + float determinant = jacobian.x * jacobian.w - jacobian.y * jacobian.z; + return vec4(jacobian.w, -jacobian.y, -jacobian.z, jacobian.x) / determinant; } -vec2 refractDisplacement(vec3 ray, float scale) +vec2 cornerDiscToSquare(vec2 point) { - return ray.xy / max(-ray.z, 0.0001) * scale; + float root2 = kSqrt2; + float a = 2.0 + point.x * point.x - point.y * point.y; + float b = 2.0 - point.x * point.x + point.y * point.y; + return 0.5 * vec2( + sqrt(max(a + 2.0 * root2 * point.x, 0.0)) + - sqrt(max(a - 2.0 * root2 * point.x, 0.0)), + sqrt(max(b + 2.0 * root2 * point.y, 0.0)) + - sqrt(max(b - 2.0 * root2 * point.y, 0.0)) + ); } -// 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 invertRoundedSquare(vec2 point, float alpha) { - 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 + float corner = 1.0 - alpha + alpha * kInvSqrt2; + if (distance(point, vec2(corner)) <= 1e-5) + return vec2(1.0); + vec2 discInitial = cornerDiscToSquare(point); + vec2 canonical = clamp(mix(point, discInitial, alpha), vec2(0.0), vec2(1.0)); + // Good disc initial guess typically converges in 3–4 steps; keep 5 with error exit. + for (int iteration = 0; iteration < 5; ++iteration) { + RoundedMapResult mapped = roundedSquareMap(canonical, alpha); + vec2 error = mapped.point - point; + if (dot(error, error) <= 1e-10) + break; + vec4 inverse = inverseCornerJacobian(mapped.jacobian); + canonical = clamp( + canonical - vec2(inverse.x * error.x + inverse.y * error.y, + inverse.z * error.x + inverse.w * error.y), + vec2(0.0), vec2(1.0) ); } - return sampleBackdrop(baseUv + baseOffset).rgb; + return canonical; } -// 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) +vec2 mapActualToVirtualPoint(vec2 p, float fromR, float toR, float bezel) { - 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 limit = min(halfSize.x, halfSize.y); + float from = clamp(fromR, 0.0, limit); + float to = clamp(toR, 0.0, limit); + if (abs(from - to) <= 1e-4) + return p; + + float extent = min(max(max(max(bezel, from), to), 1.0), limit); + vec2 pointSign = vec2(p.x < 0.0 ? -1.0 : 1.0, p.y < 0.0 ? -1.0 : 1.0); + vec2 distanceFromEdge = halfSize - abs(p); + if (distanceFromEdge.x >= extent || distanceFromEdge.y >= extent) + return p; + + vec2 normalized = vec2(1.0) - distanceFromEdge / extent; + vec2 canonical = invertRoundedSquare(normalized, from / extent); + vec2 target = roundedSquareMap(canonical, to / extent).point; + return pointSign * (halfSize - vec2(extent) + target * extent); } -// --------------------------------------------------------------------------- -// Fragment main -// --------------------------------------------------------------------------- +// ── Corner-depth preservation ───────────────────────────────────────── +float postprocessCornerDepth(float xProgress, float yProgress) +{ + float x = clamp(xProgress, 0.0, 1.0); + float y = clamp(yProgress, 0.0, 1.0); + float lower = min(x, y); + float difference = abs(x - y); + float blendWidth = 6.0 * x * y * (1.0 - x) * (1.0 - y); + if (blendWidth <= 1e-5 || difference >= blendWidth) + return lower; + float blendProgress = difference / blendWidth; + float remaining = 1.0 - blendProgress; + return clamp(lower + 0.5 * difference * remaining * remaining, 0.0, 1.0); +} + +float arcOuter(float d, float r) +{ + return d < r ? r - sqrt(max(0.0, d * (2.0 * r - d))) : 0.0; +} + +float sourcePostprocessDepth(vec2 p, float rr, float bezel) +{ + if (rr >= bezel) + return fieldT(p, rr); + vec2 d = halfSize - abs(p); + float topProgress = clamp((d.y - arcOuter(d.x, rr)) / bezel, 0.0, 1.0); + float sideProgress = clamp((d.x - arcOuter(d.y, rr)) / bezel, 0.0, 1.0); + return postprocessCornerDepth(topProgress, sideProgress); +} + +vec2 preservePostprocessDepth(vec2 actualPoint, vec2 mappedPoint, float actualRadius, float opticalRadius, float bezel) +{ + float strength = clamp((opticalRadius - actualRadius) / max(bezel, 1.0), 0.0, 1.0); + if (strength <= 1e-5) + return mappedPoint; + + float extent = min(max(max(max(bezel, actualRadius), opticalRadius), 1.0), min(halfSize.x, halfSize.y)); + vec2 edgeDistance = halfSize - abs(actualPoint); + if (edgeDistance.x >= extent || edgeDistance.y >= extent) + return mappedPoint; + + float baseT = fieldT(mappedPoint, opticalRadius); + float sourceT = sourcePostprocessDepth(actualPoint, actualRadius, bezel); + float targetT = mix(baseT, sourceT, strength); + vec2 result = mappedPoint; + float b = fieldBezel(); + for (int iteration = 0; iteration < 3; ++iteration) { + float sdVal = sdRoundedRect(result, halfSize, opticalRadius); + float currentT = clamp(-sdVal / b, 0.0, 1.0); + float error = currentT - targetT; + if (abs(error) <= 1e-5) + break; + + // ∇fieldT ≈ -∇sd / bezel inside the unclamped band; sd is 1-Lipschitz. + vec2 gsd = sdRoundedRectGrad(result, halfSize, opticalRadius); + vec2 g = -gsd / b; + float gradientSquared = dot(g, g); + if (gradientSquared <= 1e-10) + break; + float stepLength = clamp(error / gradientSquared, -bezel * bezel, bezel * bezel); + result -= g * stepLength; + } + return result; +} + +// ── Surface profile ─────────────────────────────────────────────────── +vec2 surfaceProfile(float t) +{ + float pp = max(ubuf.profilePower, 1.0); + float s = 1.0 - clamp(t, 0.0, 1.0); + float sp = pow(s, pp); + float inside = max(1.0 - sp, 1e-4); + float height = pow(inside, 1.0 / pp); + float derivative = pow(s, pp - 1.0) * height / inside; + return vec2(height, derivative); +} + +vec3 sampleBg(vec2 uv) +{ + return texture(source, clamp(uv, vec2(0.002), vec2(0.998))).rgb; +} +vec4 finishColor(vec3 color, float shapeAlpha) +{ + if (ubuf.tint > 1e-4) + color = mix(color, vec3(1.0), clamp(ubuf.tint, 0.0, 1.0)); + float alpha = shapeAlpha * ubuf.qt_Opacity; + return vec4(clamp(color, 0.0, 1.0) * alpha, alpha); +} + +// ── 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; - - // SDF + normal in one pass (shared corner-clamp computation). - vec3 sdfResult = roundedRectSDF(centered, halfSize, ubuf.radius); - float sdf = sdfResult.x; - vec2 normal = sdfResult.yz; - - // 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) { + vec2 p = texCoord * size - size * 0.5; + + float outerRadius = min(max(ubuf.radius, 0.0), min(halfSize.x, halfSize.y)); + float sd = sdRoundedRect(p, halfSize, outerRadius); + + // Anti-aliased shape edge. + float scale = length(vec2(dFdx(sd), dFdy(sd))); + float edgeAA = max(scale * 2.0, 2.0); + float shapeAlpha = smoothstep(edgeAA, -edgeAA, sd); + + if (shapeAlpha <= 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); + float maxSizeBezel = max(min(halfSize.x, halfSize.y) - 1.0, 1.0); + float bezel = min(configuredBezel, maxSizeBezel); + // virtualRadius is private: radius < bezel → follow bezel, else follow radius. + float opticalRadius = clamp(max(outerRadius, bezel), 0.0, min(halfSize.x, halfSize.y)); + + float effectExtent = bezel; + if (ubuf.specular > 1e-4) + effectExtent = max(effectExtent, 5.0); + + // Parameterized ring margin so the cheap center path never skips pixels that + // mapping/preserve could still pull into the bezel effect band. + // + // radiusLift = opticalRadius - outerRadius (0 when radius >= bezel). + // Bounds used (all scale with lift; zero when mapping is identity): + // • fixed-p SDF shrink when r rises: (√2 - 1) * lift + // • Coons corner boundary displacement: (1 - 1/√2) * lift + // • preserve depth correction travel: lift + // Sum = lift * (1 + 1/√2). + float radiusLift = max(opticalRadius - outerRadius, 0.0); + float safetyMargin = radiusLift * (1.0 + kInvSqrt2); + + float actualDistFromEdge = -sd; + if (actualDistFromEdge > effectExtent + safetyMargin) { + fragColor = finishColor(sampleBg(texCoord), shapeAlpha); 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); + // Deform actual coordinates to virtual optical coordinates. + vec2 mappedPoint = mapActualToVirtualPoint(p, outerRadius, opticalRadius, bezel); + vec2 shadePoint = preservePostprocessDepth(p, mappedPoint, outerRadius, opticalRadius, bezel); + float opticalDistFromEdge = -sdRoundedRect(shadePoint, halfSize, opticalRadius); + + if (opticalDistFromEdge > effectExtent) { + fragColor = finishColor(sampleBg(texCoord), shapeAlpha); + return; } - // 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 t = fieldT(shadePoint, opticalRadius); + vec2 n2 = fieldNormal(shadePoint, opticalRadius); + + float maxTan = max(ubuf.refractionMaxTan, 0.1); + + vec2 profile = surfaceProfile(t); + float h = profile.x; + + float thick = max(ubuf.thickness, 0.0); + float edgePull = clamp(ubuf.contentEdgePull, 0.0, 1.0); + float outerSoft = smoothstep(0.0, max(2.5 * edgeAA, 2.5), opticalDistFromEdge); + float slopeSoft = mix(outerSoft, 1.0, edgePull); + float rawTan = profile.y * (thick / bezel); + float slopeMag = min(rawTan, maxTan) * slopeSoft; + + float H = h * thick; + + float rampEnd = clamp(ubuf.contentRampEnd, 0.001, 1.0); + // edgePull=0 → contentRamp = smoothstep(0, rampEnd, t); keep exact mix for pull>0. + float contentRamp = (edgePull <= 1e-5) + ? smoothstep(0.0, rampEnd, t) + : mix(edgePull, 1.0, smoothstep(0.0, rampEnd, t)); + float maxDisp = min(min(bezel * 0.85, thick * 0.75), 48.0) + * max(maxTan / 2.75, 1.0); + + float iorG = max(ubuf.ior, 1.0001); + + float incidentInvLen = inversesqrt(slopeMag * slopeMag + 1.0); + float sinI = slopeMag * incidentInvLen; + float sinT = clamp(sinI / iorG, 0.0, 0.999); + float tanT = sinT * inversesqrt(max(1.0 - sinT * sinT, 1e-4)); + float magG = H * contentRamp * max(slopeMag - tanT, 0.0); + magG = min(magG, maxDisp); + vec2 inward = -n2; + vec2 offG = inward * magG; + + vec2 baseUv = shadePoint / size + 0.5; + // sampleBg clamps; avoid a second clamp here. + vec3 color = sampleBg(baseUv + offG / size); + + if (ubuf.specular > 1e-4) { + vec3 N = normalize(vec3(n2 * slopeMag, 1.0)); + float oneMinusCos = 1.0 - clamp(N.z, 0.0, 1.0); + float oneMinusCos2 = oneMinusCos * oneMinusCos; + float fresnel = 0.04 + 0.96 * oneMinusCos2 * oneMinusCos2 * oneMinusCos; + vec2 lightDir = normalize(ubuf.lightDirection); + float rimDot = abs(dot(n2, lightDir)); + float rimFalloff = 1.0 - smoothstep(0.0, bezel * 0.45, opticalDistFromEdge); + float specHighlight = fresnel * pow(rimDot * rimFalloff, 1.35); + color += vec3(specHighlight * clamp(ubuf.specular, 0.0, 1.0)); + + float innerRim = smoothstep(0.0, 2.0, opticalDistFromEdge) + * (1.0 - smoothstep(2.0, 5.0, opticalDistFromEdge)); + color += vec3(innerRim * 0.12 * clamp(ubuf.specular, 0.0, 1.0)); } - 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; + if (ubuf.innerShadow > 1e-4) { + float innerShadowMask = 1.0 - smoothstep(0.0, bezel * 0.6, opticalDistFromEdge); + color *= mix(1.0, 0.75, innerShadowMask * clamp(ubuf.innerShadow, 0.0, 1.0)); } - fragColor = vec4(clamp(color, 0.0, 1.0) * outAlpha, outAlpha); + fragColor = finishColor(color, shapeAlpha); } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 77d1fd45e1..be64ce9f62 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -294,6 +294,7 @@ qt_add_shaders(libtreeland "treeland_shaders_ng" # so BATCHABLE is unnecessary (it only adds an unused batchable variant). qt_add_shaders(libtreeland "treeland_liquidglass_shaders" PRECOMPILE + OPTIMIZED PREFIX "/shaders" BASE diff --git a/src/core/qml/Effects/Blur.qml b/src/core/qml/Effects/Blur.qml index f41de75a7a..788c1002d4 100644 --- a/src/core/qml/Effects/Blur.qml +++ b/src/core/qml/Effects/Blur.qml @@ -17,9 +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 real brightness: 0.0 + property real contrast: 0.0 + property real saturation: Helper.config.glassSaturation + property real glassSpecular: Helper.config.glassSpecular + property real glassTint: 0.0 property bool glassEnabled: Helper.config.glassEnabled z: parent.z ? parent.z - 1 : -1 @@ -43,26 +45,19 @@ 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 + ior: 1.33 + specular: blitter.glassSpecular + tint: blitter.glassTint + brightness: blitter.brightness + contrast: blitter.contrast + saturation: blitter.saturation + contentEdgePull: 0.0 + contentRampEnd: 0.0 + refractionMaxTan: 2.0 + profilePower: Helper.config.glassProfilePower + innerShadow: 0.0 } } diff --git a/src/core/qml/Effects/GlassEffect.qml b/src/core/qml/Effects/GlassEffect.qml index b3db9e155c..444124f5ae 100644 --- a/src/core/qml/Effects/GlassEffect.qml +++ b/src/core/qml/Effects/GlassEffect.qml @@ -9,113 +9,80 @@ Item { required property variant source - property real radius: 0 - property bool blurEnabled: false - property int blurMax: 32 - property real blurAmount: 1.0 + // Liquid Glass material controls. + // Blur and shadow stay delegated to Qt Quick MultiEffect / callers. + property real radius: 12 + property real thickness: 40 + property real bezelWidth: 36 + property real ior: 1.33 + property real specular: 0.0 + property real tint: 0.0 + // Limit the geometric surface slope near the silhouette. + property real refractionMaxTan: 2.0 + property real contentEdgePull: 0.0 + property real contentRampEnd: 0.0 + property real profilePower: 2.0 + property real innerShadow: 0.0 + + // Compatibility inputs kept for existing Blur.qml users. MultiEffect owns + // backdrop blur and colour adjustment before the refraction shader. + property bool blurEnabled: true + property int blurMax: 64 + property real blurAmount: 0.6 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 - - // 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 + property real brightness: 0.0 + property real contrast: 0.0 + property real saturation: 0.0 + readonly property vector2d lightDirection: Qt.vector2d(0.5, -0.7) readonly property bool multiEffectEnabled: (blurEnabled && blurAmount > 0 && blurMax > 0) - || brightness != 0 || contrast != 0 || saturation != 0 || colorization > 0 + || brightness != 0 || contrast != 0 || saturation != 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. MultiEffect { - id: blurredSource + id: processedSource anchors.fill: parent visible: effect.multiEffectEnabled - layer.enabled: effect.multiEffectEnabled - smooth: true - opacity: 0 // not drawn to screen; sampled as a texture source: effect.source autoPaddingEnabled: false - blurEnabled: effect.blurEnabled && effect.blurAmount > 0 - blur: blurEnabled ? effect.blurAmount : 0.0 + blurEnabled: effect.blurEnabled + blur: effect.blurAmount blurMax: effect.blurMax blurMultiplier: effect.blurMultiplier brightness: effect.brightness contrast: effect.contrast saturation: effect.saturation - colorization: effect.colorization - colorizationColor: effect.colorizationColor + } + ShaderEffectSource { + id: processedTexture + anchors.fill: parent + sourceItem: processedSource + hideSource: true + live: true + visible: false } - // ── 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. ShaderEffect { id: glassShader objectName: "glassShader" anchors.fill: parent - smooth: true // Enables linear filtering for the sampler to prevent jagged steps - property variant source: effect.multiEffectEnabled ? blurredSource : effect.source + smooth: true + property variant source: effect.multiEffectEnabled ? processedTexture : effect.source + // Property order MUST match the UBO layout in liquidglass.frag (after qt_Matrix, qt_Opacity) readonly property vector2d itemSize: Qt.vector2d(Math.max(width, 1), Math.max(height, 1)) + readonly property vector2d lightDirection: effect.lightDirection 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 specular: Math.max(0, Math.min(1, effect.specular)) + readonly property real tint: Math.max(0, Math.min(1, effect.tint)) + readonly property real contentEdgePull: Math.max(0, Math.min(1, effect.contentEdgePull)) + readonly property real contentRampEnd: Math.max(0.001, Math.min(1, effect.contentRampEnd)) + readonly property real refractionMaxTan: Math.max(0.1, effect.refractionMaxTan) + readonly property real profilePower: Math.max(1, effect.profilePower) + readonly property real innerShadow: Math.max(0, Math.min(1, effect.innerShadow)) + vertexShader: "qrc:/shaders/liquidglass.vert.qsb" fragmentShader: "qrc:/shaders/liquidglass.frag.qsb" } diff --git a/src/plugins/lockscreen/qml/RoundBlur.qml b/src/plugins/lockscreen/qml/RoundBlur.qml index 581a24eab7..1fb358a980 100644 --- a/src/plugins/lockscreen/qml/RoundBlur.qml +++ b/src/plugins/lockscreen/qml/RoundBlur.qml @@ -10,7 +10,6 @@ Blur { 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/CMakeLists.txt b/tests/test_effect_glass/CMakeLists.txt index 69f72c9f4d..c8090798ff 100644 --- a/tests/test_effect_glass/CMakeLists.txt +++ b/tests/test_effect_glass/CMakeLists.txt @@ -47,6 +47,7 @@ qt_add_qml_module(test_effect_glass qt_add_shaders(test_effect_glass "test_glass_shaders" PRECOMPILE + OPTIMIZED PREFIX "/shaders" BASE diff --git a/tests/test_effect_glass/GlassEffectScene.qml b/tests/test_effect_glass/GlassEffectScene.qml index 3f9043d18f..0f31654d89 100644 --- a/tests/test_effect_glass/GlassEffectScene.qml +++ b/tests/test_effect_glass/GlassEffectScene.qml @@ -12,6 +12,10 @@ Item { /// Expose the GlassEffect instance for property manipulation from C++. property alias glass: glassEffect + property alias backdropVisible: backdrop.visible + property alias glassXScale: glassScale.xScale + property alias glassYScale: glassScale.yScale + property bool cornerProbeVisible: false /// A colourful, high-contrast backdrop so refraction / highlights / rim /// tint are clearly visible in grabbed images. @@ -73,6 +77,25 @@ Item { width: 8; height: 112 color: "#000000" } + Grid { + anchors.top: parent.top + anchors.right: parent.right + columns: 8 + visible: root.cornerProbeVisible + + Repeater { + model: 64 + + Rectangle { + required property int index + width: 8 + height: 8 + color: ((index % 8) + Math.floor(index / 8)) % 2 === 0 + ? "#ffffff" + : "#000000" + } + } + } } GlassEffect { @@ -80,5 +103,11 @@ Item { objectName: "glassEffect" anchors.fill: parent source: backdrop + + transform: Scale { + id: glassScale + origin.x: glassEffect.width * 0.5 + origin.y: glassEffect.height * 0.5 + } } } diff --git a/tests/test_effect_glass/main.cpp b/tests/test_effect_glass/main.cpp index 6a7ef11230..3587e5a053 100644 --- a/tests/test_effect_glass/main.cpp +++ b/tests/test_effect_glass/main.cpp @@ -1,15 +1,21 @@ // Copyright (C) 2026 UnionTech Software Technology Co., Ltd. // SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#include #include #include +#include #include #include +#include +#include +#include #include #include #include #include +#include #include #include @@ -131,34 +137,34 @@ 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("profilePower", 4.0); 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); QTest::qWait(50); } static bool requiresShaderRendering(const char *testFunction) { static constexpr const char *renderingTests[] = { - "highlightToggleProducesDifferentRender", - "rimReflectionToggleProducesDifferentRender", - "lightAngleShiftMovesHighlight", + "specularProducesDifferentRender", + "tintControlProducesDifferentRender", + "specularControlChangesRim", + "narrowBezelPreservesInnerRim", "radiusProducesTransparentCorners", + "nonUniformScaleKeepsEdgeAntialiasingSymmetric", "largeBezelWithSmallRadiusDoesNotIntroduceCornerDiagonalSeam", - "largeBezelRemainsVisibleAlongStraightEdgesWithSmallRadius", - "highDispersionRefractsEdgesWithSpecularDisabled", + "largeBezelWithSmallRadiusKeepsStraightEdgeRefraction", + "contentEdgePullChangesSilhouetteRefraction", + "refractionMaxTanAboveOneChangesEdgeRefraction", + "radiusLargerThanBezelDoesNotIntroduceCornerDiagonalSeam", + "tintControlChangesRenderedColor", "zeroBlurMultiplierStillAppliesGaussianBlur", "blurAmountAndMultiplierChangeRenderedBlurStrength", "blurToggleProducesDifferentRender", + "benchmarkGlassShader", }; for (const auto *name : renderingTests) { @@ -175,29 +181,23 @@ class GlassEffectTest : public QObject /// 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("blurMax", 12); + m_glass->setProperty("blurAmount", 0.6); 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("radius", 60.0); + m_glass->setProperty("bezelWidth", 60.0); + m_glass->setProperty("thickness", 50.0); + m_glass->setProperty("ior", 1.5); 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("saturation", 0.04); + m_glass->setProperty("specular", 0.0); + m_glass->setProperty("tint", 0.0); + m_scene->setProperty("backdropVisible", true); + m_scene->setProperty("glassXScale", 1.0); + m_scene->setProperty("glassYScale", 1.0); + m_scene->setProperty("cornerProbeVisible", false); QTest::qWait(50); } @@ -225,84 +225,62 @@ private Q_SLOTS: // ── Property tests: read derived properties at runtime ───────────── - void lightDirectionDerivesFromLightAngle() + void liquidGlassDefaultsMatch() { - // lightAngle default in GlassEffect.qml is -135° - // lightDirection = (cos(angle), sin(angle)) - const qreal angle = m_glass->property("lightAngle").toReal(); - QCOMPARE(angle, -135.0); + QCOMPARE(m_glass->property("radius").toReal(), 12.0); + QCOMPARE(m_glass->property("thickness").toReal(), 40.0); + QCOMPARE(m_glass->property("bezelWidth").toReal(), 36.0); + QCOMPARE(m_glass->property("ior").toReal(), 1.33); + QCOMPARE(m_glass->property("specular").toReal(), 0.0); + QCOMPARE(m_glass->property("tint").toReal(), 0.0); + QCOMPARE(m_glass->property("saturation").toReal(), 0.0); + QCOMPARE(m_glass->property("blurAmount").toReal(), 0.6); - 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() + void dconfigDefaultsMatch() { - // 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); + QFile descriptor(QStringLiteral(SOURCE_DIR + "/misc/dconfig/org.deepin.dde.treeland.user.json")); + QVERIFY2(descriptor.open(QIODevice::ReadOnly), qPrintable(descriptor.errorString())); + + const QJsonObject contents = + QJsonDocument::fromJson(descriptor.readAll()).object().value("contents").toObject(); + QCOMPARE(contents.value("glassBezel").toObject().value("value").toDouble(), 36.0); + QCOMPARE(contents.value("glassThickness").toObject().value("value").toDouble(), 40.0); + QCOMPARE(contents.value("glassProfilePower").toObject().value("value").toDouble(), 2.0); + QCOMPARE(contents.value("glassSaturation").toObject().value("value").toDouble(), 0.0); + QCOMPARE(contents.value("glassSpecular").toObject().value("value").toDouble(), 0.0); + QCOMPARE(contents.value("blurStrength").toObject().value("value").toInt(), 60); + QCOMPARE(contents.value("blurAmount").toObject().value("value").toDouble(), 0.6); + QCOMPARE(contents.value("blurMultiplier").toObject().value("value").toDouble(), 0.0); } - - void highlightEnabledTogglesZeroShaderSpecular() + void innerShadowDefaultIsDisabled() { - // 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); + QCOMPARE(m_glass->property("innerShadow").toReal(), 0.0); + } - 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() + void specularAndTintAreOnlyMaterialControls() { auto *shader = m_glass->findChild("glassShader"); QVERIFY(shader); - // Enabled → 0.22 tint mix - m_glass->setProperty("rimReflectionEnabled", true); + QVERIFY2(m_glass->metaObject()->indexOfProperty("highlightEnabled") < 0, + "GlassEffect must use specular=0 instead of a duplicate highlight switch"); + QVERIFY2(m_glass->metaObject()->indexOfProperty("colorization") < 0, + "GlassEffect must use tint instead of duplicate white colorization"); + QVERIFY2(m_glass->metaObject()->indexOfProperty("colorizationColor") < 0, + "GlassEffect must not expose colorization state after removing colorization"); + + QVERIFY(m_glass->setProperty("specular", 0.55)); QTest::qWait(10); - QCOMPARE(shader->property("rimReflectionStrength").toReal(), 0.22); + QCOMPARE(shader->property("specular").toReal(), 0.55); - // Disabled → 0.0 (pure white specular, no tint) - m_glass->setProperty("rimReflectionEnabled", false); + QVERIFY(m_glass->setProperty("specular", 0.0)); QTest::qWait(10); - QCOMPARE(shader->property("rimReflectionStrength").toReal(), 0.0); + QCOMPARE(shader->property("specular").toReal(), 0.0); } void multiEffectEnabledReflectsBlurAndColorParams() @@ -312,7 +290,6 @@ private Q_SLOTS: 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()); @@ -339,11 +316,6 @@ private Q_SLOTS: 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() @@ -357,15 +329,10 @@ private Q_SLOTS: "blurMax", "bezelWidth", "thickness", - "displacementFactor", "ior", - "dispersion", "brightness", "contrast", "saturation", - "colorization", - "edgeSaturation", - "reflectionOffset", }; for (const QByteArray &name : propertyNames) { @@ -376,9 +343,7 @@ 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("blurEnabled", true)); QVERIFY(m_glass->setProperty("blurMax", 18)); QVERIFY(m_glass->setProperty("blurAmount", 0.75)); @@ -387,31 +352,82 @@ 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(m_glass->property("multiEffectEnabled").toBool(), true); } + + void materialKnobsAreRuntimeQmlProperties() + { + auto *shader = m_glass->findChild("glassShader"); + QVERIFY(shader); + + const QList propertyNames = { + "specular", + "tint", + }; + for (const QByteArray &name : propertyNames) { + QVERIFY2(m_glass->metaObject()->indexOfProperty(name.constData()) >= 0, + qPrintable(QStringLiteral("GlassEffect must expose material knob %1") + .arg(QString::fromLatin1(name)))); + } + + QVERIFY(m_glass->setProperty("specular", 0.35)); + QVERIFY(m_glass->setProperty("tint", 0.22)); + QTest::qWait(10); + + QCOMPARE(shader->property("specular").toReal(), 0.35); + QCOMPARE(shader->property("tint").toReal(), 0.22); + QVERIFY2(m_glass->metaObject()->indexOfProperty("blurRadius") < 0, + "GlassEffect must use MultiEffect blur controls instead of exposing shader blurRadius"); + QVERIFY2(m_glass->metaObject()->indexOfProperty("shadow") < 0, + "GlassEffect must not expose a dedicated shadow control"); + } + + void multiEffectBlurControlsRemainRuntimeQmlProperties() + { + QVERIFY2(m_glass->metaObject()->indexOfProperty("blurEnabled") >= 0, + "GlassEffect must preserve blurEnabled for MultiEffect blur"); + QVERIFY2(m_glass->metaObject()->indexOfProperty("blurMax") >= 0, + "GlassEffect must preserve blurMax for MultiEffect blur"); + QVERIFY2(m_glass->metaObject()->indexOfProperty("blurAmount") >= 0, + "GlassEffect must preserve blurAmount for MultiEffect blur"); + QVERIFY2(m_glass->metaObject()->indexOfProperty("blurMultiplier") >= 0, + "GlassEffect must preserve blurMultiplier for MultiEffect blur"); + + QVERIFY(m_glass->setProperty("blurEnabled", false)); + QVERIFY(m_glass->setProperty("blurAmount", 1.0)); + QVERIFY(m_glass->setProperty("blurMax", 48)); + QVERIFY(m_glass->setProperty("blurMultiplier", 2.0)); + QVERIFY(m_glass->setProperty("saturation", 0.0)); + QTest::qWait(10); + QVERIFY(!m_glass->property("multiEffectEnabled").toBool()); + + QVERIFY(m_glass->setProperty("blurEnabled", true)); + QTest::qWait(10); + QVERIFY(m_glass->property("multiEffectEnabled").toBool()); + } + + void shaderKeepsGlassBoundsWithoutDedicatedShadow() + { + auto *shader = qobject_cast(m_glass->findChild("glassShader")); + QVERIFY(shader); + + QTest::qWait(10); + + QCOMPARE(shader->x(), 0.0); + QCOMPARE(shader->y(), 0.0); + QCOMPARE(shader->width(), m_glass->width()); + QCOMPARE(shader->height(), m_glass->height()); + } + 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 ─────────────── @@ -428,21 +444,18 @@ private Q_SLOTS: QCOMPARE(img1, img2); } - void highlightToggleProducesDifferentRender() + void specularProducesDifferentRender() { - // Set prominent glass params for visible highlights - m_glass->setProperty("highlightEnabled", true); + // Set prominent glass params for visible highlights. 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("specular", 0.82); QTest::qWait(50); const QImage withHighlight = grabImage(m_scene); QVERIFY(!withHighlight.isNull()); - m_glass->setProperty("highlightEnabled", false); + m_glass->setProperty("specular", 0.0); QTest::qWait(50); const QImage withoutHighlight = grabImage(m_scene); @@ -477,109 +490,83 @@ private Q_SLOTS: .arg(edgeDiff).arg(centerDiff))); } - void rimReflectionToggleProducesDifferentRender() + void tintControlProducesDifferentRender() { - // 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("radius", 60.0); + m_glass->setProperty("blurEnabled", false); + m_glass->setProperty("specular", 0.0); + m_glass->setProperty("tint", 0.0); QTest::qWait(50); - const QImage withTint = grabImage(m_scene); - QVERIFY(!withTint.isNull()); + const QImage noTint = grabImage(m_scene); + QVERIFY(!noTint.isNull()); - m_glass->setProperty("rimReflectionEnabled", false); + m_glass->setProperty("tint", 0.4); QTest::qWait(50); - const QImage withoutTint = grabImage(m_scene); - QVERIFY(!withoutTint.isNull()); + const QImage strongTint = grabImage(m_scene); + QVERIFY(!strongTint.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))); + const int diff = regionDiffCount(noTint, strongTint, QRect(64, 64, 128, 128), 4); + QVERIFY2(diff > 128 * 128 / 3, + qPrintable(QStringLiteral("tint control must visibly change the glass interior, got %1 changed pixels").arg(diff))); } - void lightAngleShiftMovesHighlight() + void specularControlChangesRim() { - 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("radius", 60.0); + m_glass->setProperty("blurEnabled", false); + m_glass->setProperty("specular", 0.0); QTest::qWait(50); - const QImage imgHorizontal = grabImage(m_scene); - QVERIFY(!imgHorizontal.isNull()); - m_glass->setProperty("lightAngle", 90.0); + const QImage withoutSpecular = grabImage(m_scene); + QVERIFY(!withoutSpecular.isNull()); + + m_glass->setProperty("specular", 1.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 withSpecular = grabImage(m_scene); + QVERIFY(!withSpecular.isNull()); - const int w = imgHorizontal.width(); - const int h = imgHorizontal.height(); + const int edgeDiff = regionDiffCount(withoutSpecular, withSpecular, QRect(0, 0, withSpecular.width(), 32), 4) + + regionDiffCount(withoutSpecular, withSpecular, QRect(0, withSpecular.height() - 32, withSpecular.width(), 32), 4) + + regionDiffCount(withoutSpecular, withSpecular, QRect(0, 0, 32, withSpecular.height()), 4) + + regionDiffCount(withoutSpecular, withSpecular, QRect(withSpecular.width() - 32, 0, 32, withSpecular.height()), 4); + QVERIFY2(edgeDiff > 500, + qPrintable(QStringLiteral("specular control must visibly change rim highlights, edgeDiff=%1").arg(edgeDiff))); + } - // 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))); + void narrowBezelPreservesInnerRim() + { + m_glass->setProperty("radius", 60.0); + m_glass->setProperty("bezelWidth", 3.0); + m_glass->setProperty("blurEnabled", false); + m_glass->setProperty("specular", 0.0); + QTest::qWait(50); + + const QImage withoutSpecular = grabImage(m_scene); + QVERIFY(!withoutSpecular.isNull()); + + m_glass->setProperty("specular", 1.0); + QTest::qWait(50); + + const QImage withSpecular = grabImage(m_scene); + QVERIFY(!withSpecular.isNull()); - // 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))); + const QRect innerTopRim(64, 4, 128, 1); + const int changed = regionDiffCount(withoutSpecular, withSpecular, innerTopRim, 2); + QVERIFY2(changed > innerTopRim.width() / 2, + qPrintable(QStringLiteral("narrow bezel must preserve the fixed-width inner rim: changed=%1 width=%2") + .arg(changed) + .arg(innerTopRim.width()))); } + + 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 @@ -613,6 +600,32 @@ private Q_SLOTS: .arg(qAlpha(centerPx)))); } + void nonUniformScaleKeepsEdgeAntialiasingSymmetric() + { + m_scene->setProperty("backdropVisible", false); + m_scene->setProperty("glassXScale", 0.75); + m_scene->setProperty("glassYScale", 0.25); + m_glass->setProperty("radius", 0.0); + QTest::qWait(50); + + const QImage img = grabImage(m_scene); + QVERIFY(!img.isNull()); + + int horizontalPartialAlpha = 0; + int verticalPartialAlpha = 0; + for (int coordinate = 0; coordinate < img.width(); ++coordinate) { + const int horizontalAlpha = qAlpha(img.pixel(coordinate, img.height() / 2)); + const int verticalAlpha = qAlpha(img.pixel(img.width() / 2, coordinate)); + horizontalPartialAlpha += horizontalAlpha > 0 && horizontalAlpha < 255; + verticalPartialAlpha += verticalAlpha > 0 && verticalAlpha < 255; + } + + QVERIFY2(std::abs(horizontalPartialAlpha - verticalPartialAlpha) <= 1, + qPrintable(QStringLiteral("non-uniform scaling must preserve symmetric screen-space AA: horizontal=%1 vertical=%2") + .arg(horizontalPartialAlpha) + .arg(verticalPartialAlpha))); + } + void largeBezelWithSmallRadiusDoesNotIntroduceCornerDiagonalSeam() { setSmallRadiusLargeBezel(); @@ -626,97 +639,134 @@ private Q_SLOTS: .arg(seamStep))); } - void largeBezelRemainsVisibleAlongStraightEdgesWithSmallRadius() + + void largeBezelWithSmallRadiusKeepsStraightEdgeRefraction() + { + setSmallRadiusLargeBezel(); + + m_glass->setProperty("ior", 1.0001); + QTest::qWait(50); + const QImage neutral = grabImage(m_scene); + QVERIFY(!neutral.isNull()); + + m_glass->setProperty("ior", 1.45); + QTest::qWait(50); + const QImage refracted = grabImage(m_scene); + QVERIFY(!refracted.isNull()); + + const QRect straightEdgeBand(214, 80, 30, 96); + const int changed = regionDiffCount(neutral, refracted, straightEdgeBand, 4); + const int samples = straightEdgeBand.width() * straightEdgeBand.height(); + QVERIFY2(changed > samples / 2, + qPrintable(QStringLiteral("large bezel must keep right-edge refraction active when radius is small: changed=%1 samples=%2") + .arg(changed) + .arg(samples))); + } + + + void contentEdgePullChangesSilhouetteRefraction() { setSmallRadiusLargeBezel(); - m_glass->setProperty("edgeSaturation", 1.5); + m_glass->setProperty("contentRampEnd", 0.15); + m_glass->setProperty("contentEdgePull", 0.0); QTest::qWait(50); - const QImage largeBezel = grabImage(m_scene); - QVERIFY(!largeBezel.isNull()); + const QImage noEdgePull = grabImage(m_scene); + QVERIFY(!noEdgePull.isNull()); - m_glass->setProperty("bezelWidth", 1.0); + m_glass->setProperty("contentEdgePull", 1.0); QTest::qWait(50); - const QImage tinyBezel = grabImage(m_scene); - QVERIFY(!tinyBezel.isNull()); + const QImage fullEdgePull = grabImage(m_scene); + QVERIFY(!fullEdgePull.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, - qPrintable(QStringLiteral("large straight-edge bezel should remain active beyond the small radius: changedPixels=%1 regionPixels=%2") - .arg(activePixels) - .arg(rightStraightEdge.width() * rightStraightEdge.height()))); + const QRect rightEdgeBand(fullEdgePull.width() - 4, 48, 3, 160); + const int changed = regionDiffCount(noEdgePull, fullEdgePull, rightEdgeBand, 4); + QVERIFY2(changed > rightEdgeBand.width() * rightEdgeBand.height() / 2, + qPrintable(QStringLiteral("contentEdgePull must visibly change silhouette refraction: changed=%1 regionPixels=%2") + .arg(changed) + .arg(rightEdgeBand.width() * rightEdgeBand.height()))); } - void highDispersionRefractsEdgesWithSpecularDisabled() + void refractionMaxTanAboveOneChangesEdgeRefraction() { - 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)); + m_glass->setProperty("blurEnabled", false); + m_glass->setProperty("radius", 60.0); + m_glass->setProperty("bezelWidth", 40.0); + m_glass->setProperty("thickness", 80.0); + m_glass->setProperty("ior", 1.45); + m_glass->setProperty("contentRampEnd", 0.15); + m_glass->setProperty("contentEdgePull", 1.0); + m_glass->setProperty("refractionMaxTan", 1.0); QTest::qWait(50); - const QImage zeroDispersion = grabImage(m_scene); - QVERIFY(!zeroDispersion.isNull()); + const QImage cappedAtOne = grabImage(m_scene); + QVERIFY(!cappedAtOne.isNull()); - QVERIFY(m_glass->setProperty("dispersion", 0.2)); + m_glass->setProperty("refractionMaxTan", 20.0); 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); + const QImage cappedAtTwenty = grabImage(m_scene); + QVERIFY(!cappedAtTwenty.isNull()); - 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) + const QRect rightEdgeBand(cappedAtTwenty.width() - 12, 32, 10, 192); + const int changed = regionDiffCount(cappedAtOne, cappedAtTwenty, rightEdgeBand, 4); + m_glass->setProperty("contentEdgePull", 0.0); + m_glass->setProperty("refractionMaxTan", 2.75); + QTest::qWait(50); + QVERIFY2(changed > rightEdgeBand.width() * rightEdgeBand.height() / 8, + qPrintable(QStringLiteral("refractionMaxTan above 1 must visibly change edge refraction: changed=%1 regionPixels=%2") + .arg(changed) + .arg(rightEdgeBand.width() * rightEdgeBand.height()))); + } + + + void radiusLargerThanBezelDoesNotIntroduceCornerDiagonalSeam() + { + m_glass->setProperty("blurEnabled", false); + // Isolate corner-geometry: geometry-only isolation; + // colour effect that reads as cross-diagonal colour steps here. + m_glass->setProperty("radius", 90.0); + m_glass->setProperty("bezelWidth", 60.0); + m_glass->setProperty("thickness", 50.0); + m_glass->setProperty("ior", 3.0); + QTest::qWait(50); + + const QImage img = grabImage(m_scene); + QVERIFY(!img.isNull()); + + const int seamStep = maxDiagonalCornerDiscontinuity(img); + QVERIFY2(seamStep < 24, + qPrintable(QStringLiteral("radius larger than bezel introduced a sharp diagonal seam: max cross-diagonal step=%1") + .arg(seamStep))); + } + + void tintControlChangesRenderedColor() + { + m_glass->setProperty("blurEnabled", false); + m_glass->setProperty("radius", 60.0); + m_glass->setProperty("tint", 0.0); + QTest::qWait(50); + + const QImage noTint = grabImage(m_scene); + QVERIFY(!noTint.isNull()); + + m_glass->setProperty("tint", 0.4); + QTest::qWait(50); + + const QImage strongTint = grabImage(m_scene); + QVERIFY(!strongTint.isNull()); + + const QRect centerInterior(64, 64, 128, 128); + const int changed = regionDiffCount(noTint, strongTint, centerInterior, 4); + QVERIFY2(changed > centerInterior.width() * centerInterior.height() / 3, + qPrintable(QStringLiteral("Liquid Glass tint control should change rendered color: changed=%1 regionPixels=%2") + .arg(changed) .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); @@ -734,15 +784,14 @@ private Q_SLOTS: const QRect centerContrastFeature(92, 92, 72, 72); const int changedPixels = regionDiffCount(defaultQualityBlur, withoutBlur, centerContrastFeature, 4); - QVERIFY2(changedPixels > centerContrastFeature.width() * centerContrastFeature.height() / 5, - qPrintable(QStringLiteral("blurMultiplier=0 must keep blur active instead of disabling it: changedPixels=%1 regionPixels=%2") + QVERIFY2(changedPixels > centerContrastFeature.width() * centerContrastFeature.height() / 10, + qPrintable(QStringLiteral("MultiEffect blur must stay active with blurMultiplier=0: changedPixels=%1 regionPixels=%2") .arg(changedPixels) .arg(centerContrastFeature.width() * centerContrastFeature.height()))); } void blurAmountAndMultiplierChangeRenderedBlurStrength() { - m_glass->setProperty("highlightEnabled", false); m_glass->setProperty("blurEnabled", true); m_glass->setProperty("blurMax", 48); m_glass->setProperty("radius", 34.0); @@ -764,7 +813,7 @@ private Q_SLOTS: const QRect centerContrastFeature(92, 92, 72, 72); const int changedPixels = regionDiffCount(weakBlur, strongBlur, centerContrastFeature, 4); - QVERIFY2(changedPixels > centerContrastFeature.width() * centerContrastFeature.height() / 5, + QVERIFY2(changedPixels > centerContrastFeature.width() * centerContrastFeature.height() / 10, qPrintable(QStringLiteral("blurAmount/blurMultiplier must visibly change the sampled backdrop blur: changedPixels=%1 regionPixels=%2") .arg(changedPixels) .arg(centerContrastFeature.width() * centerContrastFeature.height()))); @@ -773,6 +822,7 @@ private Q_SLOTS: { m_glass->setProperty("blurEnabled", true); m_glass->setProperty("blurMax", 36); + m_glass->setProperty("blurAmount", 1.0); m_glass->setProperty("radius", 34.0); QTest::qWait(50); @@ -787,6 +837,69 @@ private Q_SLOTS: QVERIFY(withBlur != withoutBlur); } + + void benchmarkGlassShader() + { + // Offscreen grab round-trip benchmark for the Liquid Glass shader. + // Skipped in normal ctest runs; run explicitly: + // GLASS_BENCH=1 test_effect_glass benchmarkGlassShader + // GLASS_BENCH_SIZE=2048 to change grab size (default 1024) + // GLASS_BENCH_DRAWS=120 to change draws per round (default 120) + // GLASS_BENCH_ROUNDS=7 to change rounds (default 7) + // GLASS_BENCH_RADIUS=36 GLASS_BENCH_BEZEL=36 to change geometry + if (!qEnvironmentVariableIsSet("GLASS_BENCH")) { + QSKIP("Set GLASS_BENCH=1 to run the GPU benchmark"); + } + const int size = qgetenv("GLASS_BENCH_SIZE").toInt() > 0 ? qgetenv("GLASS_BENCH_SIZE").toInt() : 1024; + const int draws = qgetenv("GLASS_BENCH_DRAWS").toInt() > 0 ? qgetenv("GLASS_BENCH_DRAWS").toInt() : 120; + const int rounds = qgetenv("GLASS_BENCH_ROUNDS").toInt() > 0 ? qgetenv("GLASS_BENCH_ROUNDS").toInt() : 7; + + const qreal radius = qgetenv("GLASS_BENCH_RADIUS").toDouble(); + const qreal bezel = qgetenv("GLASS_BENCH_BEZEL").toDouble(); + m_glass->setProperty("blurEnabled", false); + m_glass->setProperty("specular", 0.0); + m_glass->setProperty("tint", 0.0); + if (radius > 0) + m_glass->setProperty("radius", radius); + if (bezel > 0) + m_glass->setProperty("bezelWidth", bezel); + QTest::qWait(50); + + // Warmup + auto warmup = m_scene->grabToImage(QSize(size, size)); + QSignalSpy warmupSpy(warmup.data(), &QQuickItemGrabResult::ready); + warmupSpy.wait(5000); + + std::vector medians; + for (int round = 0; round < rounds; ++round) { + std::vector times; + times.reserve(draws); + for (int i = 0; i < draws; ++i) { + auto result = m_scene->grabToImage(QSize(size, size)); + if (!result) + continue; + QElapsedTimer timer; + timer.start(); + QSignalSpy spy(result.data(), &QQuickItemGrabResult::ready); + spy.wait(5000); + times.push_back(timer.elapsed()); + } + std::sort(times.begin(), times.end()); + const qint64 median = times[times.size() / 2]; + medians.push_back(median); + qDebug("Round %d: median %lld ms (%d draws, %dx%d)", + round + 1, (long long)median, draws, size, size); + } + std::sort(medians.begin(), medians.end()); + const double overallMedian = static_cast(medians[medians.size() / 2]); + qDebug("Overall median: %.2f ms (%dx%d, radius %.1f, bezel %.1f, " + "%d rounds x %d draws)", + overallMedian, size, size, + m_glass->property("radius").toReal(), + m_glass->property("bezelWidth").toReal(), + rounds, draws); + QTest::qCompare(overallMedian, overallMedian, "median", "median", __FILE__, __LINE__); + } }; int main(int argc, char *argv[])