跪拜 Guibai
← Back to the summary

A Pure D3D11 Frosted Glass Effect for Win32, with Zero Dependencies

Foreword

I've always found macOS's Frosted Glass effect very appealing — a blurred background behind a semi-transparent window creates a strong sense of depth. On the Windows side, achieving something similar in the Win32 ecosystem usually means binding to UWP/WinUI or pulling in Qt/Electron — all too heavy.

So I spent two weeks hand-rolling a pure D3D11 + HLSL implementation. It depends on no frameworks or third-party libraries, and can be embedded into any Win32 window with just a few lines of code. Below are the results and the implementation process.

Effect Preview

demo.gif

Supports Windows 10/11, requires a GPU compatible with D3D11 Feature Level 11.0 or higher.

How to Use

Drop three files into your project and write a few lines of code:

#include "LiquidGlass.h"
using namespace LiquidGlass;

Renderer glass;

// Initialization
glass.Init(hwnd, width, height)
     .Blur(12)             // Gaussian blur
     .Radius(40)           // Corner radius
     .Saturation(1.5f)     // Saturation
     .RefractionAmount(50) // Refraction intensity
     .RefractionHeight(30) // Refraction range
     .Dispersion(1.0f);    // Chromatic dispersion

// Per-frame rendering
glass.BeginFrame();
glass.RenderGlass(x, y, w, h);
glass.EndFrame();

All parameters use a chainable API — IDE autocomplete works all the way through with .. It also includes an ImGui control panel with six sliders for real-time parameter tuning, giving you WYSIWYG feedback.

Implementation Principles

Each frame is composited from five render passes. Readers not interested in graphics rendering can skip this section and jump straight to the end to drop a Star (just kidding).

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

1. Background Capture

The window background (solid color or image; images are scaled in cover mode) is drawn to the swap chain's backbuffer and simultaneously copied to bgRT (background render target) for sampling in subsequent blur passes.

2. Gaussian Blur

This step gives the background its frosted "fuzziness."

A 31-tap separable Gaussian blur is used: for each pixel, 15 neighboring pixels on each side are sampled for a weighted average. For efficiency, this is split into two steps — first blur horizontally, then vertically. A 31×31 2D convolution kernel (961 samples) is compressed into two 1D convolutions (62 samples), producing the same result with an order-of-magnitude speed difference.

Another optimization: the Gaussian kernel weights (exp(-x²/2σ²)) are precomputed on the CPU and written directly into a constant buffer. With the default σ=12, each blur pass requires 31 samples; two blur passes per frame at 1080p means 62 million samples. Throwing those 62 million exp() calls at the GPU would waste compute — by precomputing on the CPU, the GPU only handles sampling and weighting, saving over a hundred million transcendental function evaluations per frame.

3. Drop Shadow

On glassRT (the glass render target), an SDF (Signed Distance Field) draws a rounded rectangle matching the glass shape, offset downward by 6 pixels to simulate a top light source. A smoothstep(±20px) creates a soft transition, and it is written with 30% opacity black via alpha blending.

The benefit of SDF is built-in edge anti-aliasing — smoothstep(-1, 1, sd) interpolates across a 2-pixel-wide transition band, producing jagged-free edges without MSAA.

4. Glass Body

This is the core step. Also on glassRT, the SDF determines whether each pixel falls inside the rounded rectangle. Interior pixels sample blurVRT (the vertically blurred background) and apply refractive distortion:

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

Here, 1 - sqrt(1 - t²) is the circleMap — a convex function mapping from zero to one. It maps the linear distance t to the curvature of a convex lens: the glass center (t ≈ 0) has almost no refraction, while the edges (t ≈ 1) refract the most. This mimics the real-world visual characteristic of frosted glass being thin in the middle and thick at the edges.

The gradient is a superposition of two directions:

After superposition, the gradient is normalized, producing a refraction direction that gradually shifts from the edge inward.

Next, saturation enhancement is applied: lerp(grayscale, original color, saturation), defaulting to 1.5×. Glass tends to wash out colors, so moderately boosting saturation restores color vibrancy. Finally, a ×0.92 slight darkening is applied — purely to make the glass visible against light backgrounds, not for physical simulation.

Chromatic dispersion is implemented by splitting a single sample into seven, sampling at different offsets along the refraction direction. The red channel weight is biased toward larger offsets, blue toward the opposite side, and green stays centered. This simulates the physical phenomenon where different wavelengths of light refract at different angles when passing through glass (light dispersion). The RGB weights for the seven samples are:

┌───────┬───────┬───────┬───────┐ │ Offset│ 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 │ └───────┴───────┴───────┴───────┘

The total weight per channel sums to exactly 1.0, preserving energy. When dispersion is disabled, the shader switches to a single-sample path, saving six extra texture samples.

5. Compositing

The final step is the simplest: a PassthroughPS blends glassRT (containing the drop shadow + glass body) back onto the backbuffer via alpha blending. The backbuffer already holds the original background; after compositing, the glass region shows the blurred refractive effect while the rest of the window is unaffected.

Some Pitfalls Encountered

▎ HLSL arrays are not arrays

float weights[16] in HLSL allocates 16 bytes per element — because constant buffers align to float4. Sixteen floats that should occupy 64 bytes actually take up 256 bytes, misaligning all subsequent data. Switching to float4 weightPack[4] and manually unpacking solved it. No wonder Microsoft recommends always adding packoffset.

▎ The sign(0) = 0 disaster

The sign() function in the SDF gradient returns 0 when the argument is 0, causing the gradient on the glass's central axis to become a zero vector. normalize(zero vector) = NaN, and the GPU renders NaN as white. Moreover, this bug only triggers when the glass dimensions are an odd number of pixels — because only then does the pixel center fall exactly on the central axis. The default 220 was fine; scrolling one notch to 235 produced a white line; scrolling further to 250 made it disappear again. Took ages to track down.

▎ The mutual exclusion of SRV and RT

D3D11 stipulates: a resource cannot be simultaneously bound as a render target (RT) and a shader resource view (SRV). If a resource is still bound as an RT when you try to bind its SRV, D3D11 does not report an error — it silently sets the SRV to NULL, and the render output goes completely black. The correct approach is to first call OMSetRenderTargets to switch to a new RT, unbinding the old RT, and then call PSSetShaderResources to bind the old RT's SRV.

Final Words

The project is fully open source under the Apache 2.0 license. Currently at v0.0.2, experimental stage; the API may change. Issues and PRs are welcome.

▎ This article was coded and written with assistance from Claude Code (DeepSeek-v4), taking about two weeks of spare time.

GitHub: JetComX/WindowsLiquidGlass

If you think it's good, dropping a Star is the best support.

Comments

Top 1 from juejin.cn, machine-translated. The original thread is authoritative.

用户531204235244 1 likes

The code is very well-structured.