写在前面
一直觉得 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 就是最好的支持。