G1 vs. G2 圆角:理论与 wgpu 实现剖析

G1 vs. G2 圆角:理论与 wgpu 实现剖析

June 28, 2025

在现代 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;
}

工作原理:

  1. sd_rounded_rectangle 函数接收一个点 coord、矩形的半尺寸 half_size 和圆角半径 corner_radius
  2. 它首先计算一个内部矩形的半尺寸 inner_half_size,这个内部矩形比外部矩形在每个方向上都小 corner_radius
  3. 然后,它调用 sd_rectangle 计算该点到这个内部矩形的距离。
  4. 最后,将这个距离减去 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 质感。