给 Win32 窗口加上毛玻璃效果——纯 D3D11 实现,零依赖

写在前面

一直觉得 macOS 的毛玻璃(Frosted Glass)很带感,模糊背景衬着半透明窗口,层次感拉满。回头看 Windows 这边,Win32 生态里想搞个类似的,要么绑 UWP/WinUI,要么上 Qt/Electron------都太重了。

于是我花了两周,用纯 D3D11 + HLSL 手搓了一个。不依赖任何框架和第三方库,几行代码就能嵌进任意 Win32 窗口。下面是成果和实现过程。

效果一览

支持 Windows 10/11,需要 GPU 兼容 D3D11 Feature Level 11.0 以上。

怎么用

把三个文件拖进项目,写几行代码就搞定:

cpp 复制代码
#include "LiquidGlass.h"
using namespace LiquidGlass;

Renderer glass;

// 初始化
glass.Init(hwnd, width, height)
     .Blur(12)             // 高斯模糊
     .Radius(40)           // 圆角
     .Saturation(1.5f)     // 饱和度
     .RefractionAmount(50) // 折射强度
     .RefractionHeight(30) // 折射范围
     .Dispersion(1.0f);    // 色散

// 每帧渲染
glass.BeginFrame();
glass.RenderGlass(x, y, w, h);
glass.EndFrame();

参数全部链式调用,IDE 自动补全一路 . 到底。还带一个 ImGui 控制面板,6 个滑块实时调参,所见即所得。

实现原理

每帧由 5 个渲染 Pass 叠加合成。对图形渲染不感兴趣的朋友可以跳过这一节,直接拉到文末给个 Star(不是

ApplyBg → BlurH → BlurV → Shadow → GlassBody → Composite → Present
         └──bgRT──┘  └─blurHRT→blurVRT─┘  └─────glassRT─────┘  └backbuffer┘

1. 背景捕获

窗口背景(纯色或图片,图片按 cover 模式缩放)绘制到交换链的 backbuffer,同时复制到 bgRT(背景渲染目标),供后续模糊
Pass 采样。

2. 高斯模糊

这一步给背景打上毛玻璃的"毛"。

用的是 31-tap 可分离高斯模糊:以每个像素为中心,左右各取 15
个邻近像素做加权平均。为了效率拆成两步------先沿水平方向模糊,再沿垂直方向模糊。一个 31×31 的二维卷积核(961
次采样)被压缩成两次一维卷积(62 次采样),效果一样,速度差一个数量级。

还有一个优化:高斯核的权重(exp(-x²/2σ²))是在 CPU 端预计算后直接写进常量缓冲区的。默认 σ=12 的条件下,每次模糊 Pass
要跑 31 次采样、每帧两次模糊、1080p 分辨率就是 6200 万次采样。如果把这 6200 万次 exp() 扔给 GPU
算,效果一样但浪费算力------换成 CPU 预计算,GPU 只管采样和加权,一帧省掉上亿次超越函数运算。

3. 投影

在 glassRT(玻璃渲染目标)上用 SDF(有符号距离场) 画一个和玻璃同形状的圆角矩形,向下偏移 6
像素模拟顶部光源,smoothstep(±20px) 做出柔化过渡,黑色 30% 透明度以 alpha blend 写入。

SDF 的好处是边缘抗锯齿天然支持------smoothstep(-1, 1, sd) 在 2 像素宽的过渡带上做插值,不用 MSAA 也能出无锯齿边缘。

4. 玻璃主体

这是最核心的一步。同样在 glassRT 上,用 SDF 判断每个像素是否落在圆角矩形内。内部像素采样
blurVRT(垂直模糊后的背景)做折射扭曲:

t     = clamp(1 + sd / refrHeight, 0, 1)
dist  = (1 - sqrt(1 - t²)) × refrAmount
uv    = (pixelPos + dist × gradient) / screenSize
color = blurVRT.Sample(uv)

这里 1 - sqrt(1 - t²) 就是 circleMap------一个从零到一的凸函数。把线性距离 t 映射成凸透镜的曲率:玻璃中心(t ≈
0)几乎不折射,边缘(t ≈ 1)折射量最大。这模仿了真实毛玻璃中间薄、边缘厚的视觉特征。

gradient 是两个方向的叠加:
- SDF 梯度,指向玻璃边缘(像素越靠近哪条边,折射越往那边偏)
- 深度效果,指向玻璃中心,模拟从边缘看向中心的纵深感

两者叠加后归一化,产生从边缘向内渐变的折射方向。

接着做饱和度增强:lerp(灰度, 原始颜色, saturation),默认 1.5
倍。玻璃会让画面变灰,适当拉饱和度能还原色彩的通透感。最后 ×0.92
微微压暗------纯粹为了让浅色背景上的玻璃能被看见,不是物理模拟。

色散的实现则是把单个采样拆成 7 次,沿折射方向在前后不同偏移处采样,红通道权重偏向偏移量大的一侧、蓝通道偏向另一侧、绿
通道居中。这模拟了不同波长光穿过玻璃时折射角不同的物理现象(光的色散)。7 次采样的 RGB 权重分别是:

┌───────┬───────┬───────┬───────┐
│ 偏移  │   R   │   G   │   B   │
├───────┼───────┼───────┼───────┤
│ +1.00 │ 3/3.5 │ 0     │ 0     │
├───────┼───────┼───────┼───────┤
│ +0.67 │ 3/3.5 │ 1/7   │ 0     │
├───────┼───────┼───────┼───────┤
│ +0.33 │ 3/3.5 │ 3/3.5 │ 0     │
├───────┼───────┼───────┼───────┤
│ 0     │ 0     │ 3/3.5 │ 0     │
├───────┼───────┼───────┼───────┤
│ -0.33 │ 0     │ 3/3.5 │ 3/3.0 │
├───────┼───────┼───────┼───────┤
│ -0.67 │ 0     │ 0     │ 3/3.0 │
├───────┼───────┼───────┼───────┤
│ -1.00 │ 1/7   │ 0     │ 3/3.0 │
└───────┴───────┴───────┴───────┘

每通道总权重加起来刚好 1.0,能量守恒。色散关闭时直接切到单采样着色器,省掉 6 次额外的纹理采样。

5. 合成

最后一步最简单:PassthroughPS 把 glassRT(包含投影 + 玻璃主体)以 alpha 混合叠加回 backbuffer。backbuffer
上已经画好了原始背景,叠加后玻璃区域呈现模糊折射效果,其余区域不受影响。

一些踩坑记录

▎ HLSL 的数组不是数组

float weights[16] 在 HLSL 里每个元素占 16 字节------因为常量缓冲区按 float4 对齐。16 个 float 本该占 64 字节,实际占了 256
字节,后面的数据全错位了。改成 float4 weightPack[4] 然后手动解包才搞定。难怪微软建议永远加 packoffset。

▎ sign(0) = 0 引发的惨案

SDF 梯度的 sign() 函数在参数为 0 时返回 0,导致玻璃中轴线上的梯度变成零向量。normalize(零向量) = NaN,GPU 把 NaN
渲染成白色。而且这个 bug 只在玻璃宽高为奇数像素时才触发------因为只有奇数值尺寸的像素中心恰好落在中轴线上。默认 220
没事,滚轮滚一格变 235 就出白线,再滚到 250 又消失。找了半天才定位。

▎ SRV 和 RT 的互斥关系

D3D11 规定:一个资源不能同时绑定为渲染目标(RT)和着色器资源(SRV)。如果在绑 SRV 的时候资源还挂在 RT 上,D3D11
不会报错------它只是默默把 SRV 置为 NULL,渲染结果全黑。正确做法是先 OMSetRenderTargets 切换到新 RT,解除旧 RT 的绑定,再
PSSetShaderResources 把旧 RT 的 SRV 绑上。

最后

项目完全开源,Apache 2.0 协议。目前是 v0.0.2,实验阶段,API 可能会有变动。欢迎提 Issue 和 PR。

▎ 本文由 Claude Code(DeepSeek-v4)辅助编码和撰写,耗时约两周的业余时间。

GitHub: JetComX/WindowsLiquidGlass

觉得不错的话,给个 Star 就是最好的支持。
相关推荐
ysa0510303 小时前
【板子】二分答案(最大最小?)
c++·笔记·算法·板子
stolentime4 小时前
SP8549 MAIN75 - BST again题解
c++·算法·二叉树·深度优先·图论·记忆化搜索·组合数学
ziguo11224 小时前
C/C++ 错误处理全解:从 errno 到 C++ 异常
linux·c语言·c++·windows·visual studio
stolentime4 小时前
AT_pakencamp_2020_day1_k Gcd of Sum题解
c++·算法
2601_956121974 小时前
map_计蒜客T1271 完美K倍子数组
c++·算法
王维同学6 小时前
IFEO Debugger、VerifierDlls 与 SilentProcessExit 配置
c++·windows·安全·注册表
小保CPP6 小时前
OpenCV C++计算多边形的最大内切圆
c++·人工智能·opencv·计算机视觉
lueluelue477 小时前
八股临时总结
c++
小保CPP8 小时前
OpenCV C++校正AI模型检测到的文本(OCR)
c++·人工智能·opencv·计算机视觉·ocr