G1 vs. G2 圆角:理论与 wgpu 实现剖析
在现代 UI/UX 设计中,圆角无处不在。从按钮到窗口,圆润的边角赋予了界面一种柔和、友好的感觉。然而,并非所有的圆角都是一样的。
你是否曾注意到,某些应用程序中的圆角看起来比其他地方的更“平滑”、更“自然”?这背后的秘密,往往在于几何形状的“连续性”——具体来说,就是 G1 和 G2 连续性的区别。
本文将深入探讨 G1 和 G2 圆角的理论差异,并通过两个具体的 wgpu 实现案例,分别展示如何利用符号距离函数(SDF)在 GPU 上创造出这两种不同的圆角。
G1 vs. G2 连续性:理论之辨
在几何学中,“连续性”描述了曲线或曲面片段之间连接的光滑程度。
G1 连续性(切线连续性)
G1 连续性,也称为切线连续性,是最基本的平滑连接。如果两条曲线在连接点处共享相同的切线方向,我们就说它们是 G1 连续的。我们日常在设计软件中用到的大多数“圆角”工具,生成的都是 G1 圆角。它简单、有效,但当曲率变化较大时,连接点会显得有些生硬。
G2 连续性(曲率连续性)
G2 连续性则更进了一步。它不仅要求在连接点处切线连续,还要求曲率也是连续的。曲率衡量的是曲线弯曲的剧烈程度。G2 连续性意味着在过渡点,曲线的弯曲程度不会发生突变。这种平滑的曲率过渡,使得 G2 圆角在视觉上更加和谐、有机。苹果公司在其设计语言中广泛使用的“连续曲线”圆角就是 G2 连续性的典范。
wgpu 实现方案 1:经典 SDF 实现 G1 圆角
实现 G1 圆角最经典的方法是使用基于几何偏移的符号距离函数(SDF)。这种方法通过将矩形向内收缩,然后减去一个半径来定义距离场,从而在角落处形成完美的圆形。
下面是一个来自玻璃效果着色器的 WGSL 代码片段,它完美地展示了这一点。
G1 SDF 核心函数
// 计算点到普通矩形的距离
fn sd_rectangle(coord: vec2<f32>, half_size: vec2<f32>) -> f32 {
let d = abs(coord) - half_size;
let outside = length(max(d, vec2<f32>(0.0)));
let inside = min(max(d.x, d.y), 0.0);
return outside + inside;
}
// 通过收缩矩形并减去半径来创建G1圆角矩形的SDF
fn sd_rounded_rectangle(coord: vec2<f32>, half_size: vec2<f32>, corner_radius: f32) -> f32 {
let inner_half_size = half_size - vec2<f32>(corner_radius);
return sd_rectangle(coord, inner_half_size) - corner_radius;
}
工作原理:
sd_rounded_rectangle
函数接收一个点coord
、矩形的半尺寸half_size
和圆角半径corner_radius
。- 它首先计算一个内部矩形的半尺寸
inner_half_size
,这个内部矩形比外部矩形在每个方向上都小corner_radius
。 - 然后,它调用
sd_rectangle
计算该点到这个内部矩形的距离。 - 最后,将这个距离减去
corner_radius
。
这种方法的巧妙之处在于,对于矩形直边部分上的点,这个操作相当于直接计算到原始矩形边的距离;而对于角落区域的点,这个操作等效于计算到以角落为圆心、半径为 corner_radius
的圆弧的距离。因为它产生的是一个精确的圆弧,所以这是一种纯粹的 G1 连续性 实现。
wgpu 实现方案 2:p-norm SDF 实现 G2 类圆角
要实现视觉上更平滑的 G2 类圆角,我们需要一种更灵活的 SDF。通过引入 p-norm(p-范数),我们可以控制角落曲线的形状,从而模拟出 G2 连续性的效果。
G2 SDF 核心函数
下面的 WGSL 代码展示了如何通过一个额外的参数 k
来实现 G1 和 G2 类的圆角。
// p: point to sample
// b: half-size of the box
// r: corner radius
// k: exponent for p-norm (k=2.0 for G1 circle, k>2.0 for G2-like superellipse)
fn sdf_g2_rounded_box(p: vec2f, b: vec2f, r: f32, k: f32) -> f32 {
let q = abs(p) - b + r;
let v_x = max(q.x, 0.0);
let v_y = max(q.y, 0.0);
var dist_corner_shape: f32;
// 使用一个小的 epsilon 来处理浮点数精度问题
if (abs(k - 2.0) < 0.001) { // G1 行为 (标准圆形)
dist_corner_shape = length(vec2f(v_x, v_y));
} else { // G2-like 行为 (超椭圆)
if (v_x == 0.0 && v_y == 0.0) {
dist_corner_shape = 0.0;
} else {
// p-norm
dist_corner_shape = pow(pow(v_x, k) + pow(v_y, k), 1.0/k);
}
}
return dist_corner_shape + min(max(q.x, q.y), 0.0) - r;
}
工作原理:
这个函数的精髓在于 k
这个参数,它控制着角落的形状:
- 当
k = 2.0
时:pow(pow(v_x, 2.0) + pow(v_y, 2.0), 1.0/2.0)
这正是计算欧几里得距离的公式,等价于length(vec2f(v_x, v_y))
。这会产生一个完美的圆形角落,其效果与方案 1 中的 G1 圆角完全相同。 - 当
k > 2.0
时: 我们得到的是一个超椭圆(Superellipse)。随着k
值的增大,角落的曲线会变得越来越“方”,但与直边的过渡却越来越平滑。在实现中通常会选择一个k
值(例如 2.5 或 3.0),以在视觉上获得一个令人愉悦的 G2 类圆角。
对比与总结
特性 | G1 实现 (经典 SDF) | G2 实现 (p-norm SDF) |
---|---|---|
核心方法 | 几何偏移 (收缩矩形) | p-范数 (p-norm) |
圆角形状 | 完美的圆弧 | 超椭圆 (Superellipse) |
连续性 | G1 (切线连续) | G2 类 (曲率近似连续) |
灵活性 | 只能生成一种圆角 | 可通过调整 k 值控制圆角形状 |
视觉效果 | 连接处可能显生硬 | 过渡更平滑、自然 |
计算成本 | 略低 | 略高 (涉及 pow 函数) |
结论
G1 和 G2 圆角之间的差异,根植于它们底层的数学连续性。虽然经典的 SDF 方法可以高效地实现 G1 圆角,足以满足许多场景的需求,但基于 p-norm 的 SDF 方法为我们打开了创造 G2 类圆角的大门,提供了更强的灵活性和更优的视觉效果。
通过 SDF 和 wgpu,我们看到了一种现代、高效的方式来实现这些高级的渲染效果。它将复杂的几何计算完全交给了 GPU 的并行处理能力,使得我们能够在实时应用中轻松地绘制出高质量、可定制的圆角,从而显著提升最终产品的 UI/UX 质感。