Liquid Glassで座布団を作ろう!〜角丸長方形の数学的仕組みと歪み問題の分析〜
Liquid Glass エフェクトが角丸長方形に対してどのように動作するかを解説し、深度(bezel)が角丸を超えたときに発生する視覚的アーティファクトの原因と修正方法を示します。
多分この記事をAIに読み込ませたら、あなたもLiquid Glassを実装できます。
1. 全体の処理パイプライン
Liquid Glass エフェクトは、以下の手順で「ガラスを通して覗いたような歪み」を生成します:
2. SDF(符号付き距離関数)とは
SDF (Signed Distance Function) は、空間上の任意の点から図形のエッジまでの符号付きの最短距離を返す関数です。
SDF(p) < 0→ 点 p は図形の内側SDF(p) = 0→ 点 p はエッジ上SDF(p) > 0→ 点 p は図形の外側
3. 角丸長方形の SDF
シェーダーで使われている角丸長方形の SDF(Inigo Quilez に基づく):
float sdRoundedRect(vec2 p, vec2 halfSize, float r) {
vec2 q = abs(p) - halfSize + r;
return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r;
}
4. Liquid Glass エフェクトの仕組み
4.1. 表面プロファイル: ガラスの断面形状
エッジからの距離を正規化(t = distFromEdge / bezel)し、Convex Squircle 関数で「ガラスの高さ」に変換します: h(t) = (1 - (1-t)4)1/4
4.2. ベゼル幅(深度)
ベゼル = 「屈折が起きる帯域の幅」。エッジから内側に何 px 分の範囲で歪みが発生するかを uDepth パラメータで制御します。
4.3. スネルの法則による屈折
ガラス表面の傾きからスネルの法則で視線の曲がり量を計算し、テクスチャのサンプリング位置をずらします:
offset = -grad × displacement × pxToUv
// grad: SDF の勾配(歪みの方向)
// displacement: スネルの法則から導出(歪みの量)
エッジ付近で視線が大きくずれ、中央はフラットなので真下を見る。
屈折率を上げることで、赤線はより内側のテクスチャを参照するようになり、エッジ付近のテクスチャは見えなくなります。そのためユーザーから見るとズーム効果のように見えます
屈折率を1にすると、赤線がガラス内部で交差し、参照するテクスチャが逆転する様子も確認できます。屈折率を上げて bazelを小さくすると、エッジ周辺で反対側のテクスチャが見えることがあるのは、赤線がガラス内部を通り越して反対側まで到達するためです。


4.4. SDF の勾配が歪みの方向を決める
SDF の勾配(gradient)= エッジに対する法線方向。シェーダーでは有限差分で近似します:
float eps = 0.5; // サンプル間の距離
vec2 grad = normalize(vec2(
SDF(p + vec2(eps, 0)) - SDF(p - vec2(eps, 0)),
SDF(p + vec2(0, eps)) - SDF(p - vec2(0, eps))
));
角丸が十分に大きい場合、角の近傍でも勾配は円弧に沿って滑らかに回転します。問題は角丸が小さい(またはゼロの)場合に発生します。
5. 深度が角丸を超えると起きる問題
5.1. 角の対角線上に現れる「尖り」の正体
cornerR = 0 の直角矩形では、角の頂点から内側に45°の対角線上で SDF の等高線が V 字に折れ曲がります。この折れ目を挟んで:
- 上辺側: 勾配は上向き (90°)
- 右辺側: 勾配は右向き (0°)
この90°のジャンプが1ピクセルの幅で起きるため、ガラスの表面に鋭い稜線(切り込み)が現れます。
5.2. ガラスの3D形状で見る「尖り」
SDF の等高線を高さマップとして3D描画すると、角丸が小さいときに角の対角線上にナイフのような尖った稜線が形成されることがわかります:
対角線上に鋭い稜線
角が滑らかなドーム状
5.3. offset ベクトルの比較: 角丸大 vs 角丸小
色 = 歪みの方向(色相)、明度 = 歪みの量。角丸が小さいと対角線上で色が鋭く切り替わります。
角丸が大きいことで、対角線上に滑らかな緩衝帯が形成されることがわかります。
5.4. この問題に対する各種ライブラリの対応
リキッドグラスを描画するライブラリ archisvaze/liquid-glass では、ベゼル幅の上限を角丸半径で明示的に制限しています:
// archisvaze/liquid-glass (WebGL版)
float bezel = min(uBezel, min(uRadius, min(halfSize.x, halfSize.y)) - 1.0);
// archisvaze/liquid-glass (SVG版)
const clampedBezel = Math.min(bezelW, radius - 1, Math.min(w, h) / 2 - 1);
この制限により bezel ≤ cornerR が保証され、ガラスの「ベゼル帯域」が角丸の円弧の内側に収まります。角丸の円弧がある範囲では SDF の等高線が滑らかな曲線を描くため、勾配も滑らかに変化し問題が起きません。即ち、cornerRadius = 0 のLiquid Glassは存在しないという解釈です。
GitHub 上の Liquid Glass 実装を調査したところ、cornerRadius=0 の直角矩形でアーティファクトなく動作する実装が2つ見つかりました。それぞれ異なるアプローチで角の問題を回避しています:
1. naughtyduk/liquidGL — SDF 勾配を使わないアプローチ
屈折オフセットの方向を SDF の勾配ではなく図形中心からの放射方向 normalize(p) で決定しているため、角の対角線上で勾配が急変する問題がそもそも発生しません:
// 屈折方向 = 中心からの放射方向(SDF 勾配を使わない)
vec2 offset = normalize(p) * offsetAmt;
ただし物理的正確性は低く、細長い矩形の短辺中央付近で屈折方向が辺の法線と大きくずれます。
2. rxing365/html-liquid-glass-effect-webgl — Smooth SDF アプローチ
SDF 関数内部の max/min を polynomial smooth max/min(Inigo Quilez の手法)で置き換えた sdRoundedBoxSmooth を実装。SDF 自体が C1 連続(微分可能)になるため、勾配も滑らか:
float smax_polynomial(float a, float b, float k) {
float h = clamp(0.5 + 0.5 * (a - b) / k, 0.0, 1.0);
return mix(b, a, h) + k * h * (1.0 - h);
}
float sdRoundedBoxSmooth(vec2 p, vec2 b, float r, float k_smooth) {
vec2 q = abs(p) - b + r;
float termA = smax_polynomial(q.x, q.y, k_smooth); // max → smooth max
float termB = smin_polynomial(termA, 0.0, k_smooth * 0.5); // min → smooth min
// ...
}
ただし結果は正確な距離場ではなくなるため、距離値に基づく他の処理(ベゼル幅の計算など)との整合性に注意が必要です。(自分でもこの手法を試行してみましたが、実装の仕方が悪かったのか、距離値のずれが発生したため諦めました。)
本実装は、SDF は正確なまま、勾配計算の eps を大きくすることで空間的に平滑化します。SDF の正確性を保ちつつ、角のアーティファクトを解消するバランスの取れた手法です。
5.5. インタラクティブ: 問題の可視化と解決策
繰り返しになりますが、bezel をcornerRadiusより大きくすると、対角線に沿って鋭い「切り込み」線が角丸の内側に現れることが確認できます。
cornerRadius を 0 に近づけた場合は、そもそも角が直角になることで緩やかな遷移がなくなります。ドラッグして確認してみてください↓
「切り込み」線を消すには、角丸を大きくするしかないのでしょうか?
いいえ、eps を調整することで滑らかにすることが可能です。epsを大きくしてみてください。↑
6. 解決策: eps による勾配の平滑化
6.1. 原理: なぜ eps を大きくすると滑らかになるか
勾配の計算 grad.x = SDF(p+eps) - SDF(p-eps) は、点 p の両側 eps 幅でサンプルした SDF 値の差を取っています。
- eps が小さい: ごく近い2点しか見ないため、対角線を跨いだ瞬間に勾配が急変する
- eps が大きい: 遠い2点を比較するため、対角線の両側の情報が混ざり段階的に遷移する
- 辺の直線部分: 勾配方向が一定なので、eps がいくら大きくても影響しない
下の図は対角線に沿った勾配角度のプロファイル。eps が大きいほど遷移が広く滑らかになります:
辺の直線部分(両端)は eps によらず同じ値。
6.2. 勾配ベクトル場での確認
6.3. 採用した修正
// 案 A: maxBezel から cornerR 依存を除去
float maxBezel = min(halfSize.x, halfSize.y) - 1.0;
// 案 B: eps を bezel に比例させ、中央差分に変更
float eps = max(0.5, bezel * 0.25);
float sdR = shapeSDF(p + vec2(eps, 0), ...);
float sdL = shapeSDF(p - vec2(eps, 0), ...);
float sdU = shapeSDF(p + vec2(0, eps), ...);
float sdD = shapeSDF(p - vec2(0, eps), ...);
vec2 grad = vec2(sdR - sdL, sdU - sdD);
前方差分 SDF(p+eps) - SDF(p) は eps の方向にバイアスがある。eps が大きくなると、4つの角のうちサンプル方向と整合する角でのみ平滑化が効き、反対側の角では切り込みが残る。中央差分 SDF(p+eps) - SDF(p-eps) は対称的にサンプルするので、全ての角で均等に平滑化される。
6.4. 試行して不採用とした案
| 案 | 方針 | 問題点 |
|---|---|---|
| Smooth Max | SDF の max を smoothMax に置換 | 距離値がずれて2層リング・踊り場が発生 |
| ポスト処理補間 | 角の近傍で距離・勾配をブレンド | ブレンド境界で新たな段差 |
| 仮想角丸 | effectR = max(cornerR, bezel) | 角の頂点付近でエフェクト消失 |
最終比較
| 案 | bezel制限 | 勾配不連続 | 性能 | 副作用 | 結果 |
|---|---|---|---|---|---|
| A: bezel上限撤廃 | 解決 | 未解決 | ◎ | なし | 単独では不足 |
| B: eps を bezel 比例 + 中央差分 | 未解決 | 解決 | ◎ | なし | 採用 |
| A + B | 解決 | 解決 | ◎ | なし | 採用 |
実際の Liquid Glass エフェクトのデモ
シェーダーと同じパイプライン(SDF → surfaceHeight → スネルの法則 → テクスチャサンプリング)を JavaScript で再現しています。チェッカーボード背景がガラスを通してどう歪むかを確認できます:
座布団ガラス完成!:eps 適用後のガラスの3D形状
>>> どう見ても盆に和尚さんが座っている座布団 <<<
eps=30 を適用すると、勾配の計算が平滑化され、角の対角線上の鋭い稜線が滑らかな曲面に変わります。ガラスの形状(surfaceHeight)自体は変わりませんが、屈折の方向を決める勾配が滑らかになるため、結果として「角が面取りされたガラス」のような歪みになります:
角の対角線上で色が滑らかに遷移している(鋭い稜線がない)。