Note 4 Geometry

课上只是简单提了下几何专题,我也认为这个专题相较于别的几个没那么重要,所以只是记一下隐式表示和参数表示的相关内容,再简单提一句网格处理。

隐式表示和参数表示

对一个几何图形,我们一般有两种表示方法,一种是用普通方程 $f(x,y,z)=0$ 来表示,另一种是用参数方程 $g(u,v)=(x,y,z)$ 来表示。前者是隐式表示,因为不能方便地求出这个方程表示的所有点;后者是参数表示,通过参数 $(u,v)$ 的变化可以轻易求出这个方程表示的所有点。

不过隐式表示也有优点,它能很方便地判断一个点 $(x,y,z)$ 是否在曲线上 / 曲线内 / 曲线外。接下来我们看看隐式表示和参数表示的代表应用。

隐式表示:符号距离场

符号距离场(Signed Distance Filed, SDF)是隐式表示的代表应用之一,自 2007 年 Valve 的论文以来,它一直被用于游戏内的文本渲染。虽然现在用的更多是多通道符号距离场(MSDF),但我们这里只简单介绍下符号距离场,因为后者更加复杂。

符号距离场就是带符号的距离场,对给定的点 $(u,v)$,我们记录它到图形的最近距离——如果它在图形内部(图形的线条是有宽度的,这个内部指在线条内而非几何闭环内),距离为正值,否则为负值。

在 V 社的论文中,他们先对矢量文字做了光栅化得到了 $4096\times 4096$ 的图像,然后基于这张图得到了 $64\times 64$ 的符号距离场。

下图展示了原图和符号距离场。图像不透明度是在 $[0,1]$ 间的非负数,为了可视化符号距离场,我们把距离为正的内部映射到 $(0.5, 1]$,距离为 $0$ 的边缘被映射到 $0.5$,距离为负的外部映射到 $[0, 0.5)$.

得到了符号距离场之后,我们就能对任何一个点插值出它与图形的距离,然后根据这个距离决定它是否显示出来。我们之前已经提过了距离到不透明度的映射,我们可以简单设置一个阈值来决定是否显示某个像素:

baseColor.a = distAlphaMask >= 0.5;

我个人的思考是,不要因为我们可视化了符号距离场就把它当作图像。它本质上是一个距离函数 $d(u,v)$,我们的 $64\times 64$ 的 SDF 图像的每个像素表示的不是“平均颜色”,而是“像素中心点到图形的距离”。要把它看作点,而非方块。

参数表示:贝塞尔曲线和曲面

贝塞尔曲线和曲面是参数表示的很好例子,我们先来看看计算贝塞尔曲线的常用算法和一般公式。

贝塞尔曲线

常用的生成贝塞尔曲线的 Casteljau Algorithm 如下,我们以四个点求三阶贝塞尔曲线为例:

def bezier(t: float):
    # 第一层线性插值
    p0_1 = (1 - t) * p0 + t * p1
    p1_1 = (1 - t) * p1 + t * p2
    p2_1 = (1 - t) * p2 + t * p3

    # 第二层线性插值
    p0_2 = (1 - t) * p0_1 + t * p1_1
    p1_2 = (1 - t) * p1_1 + t * p2_1

    # 第三层(最后一层)线性插值
    p_final = (1 - t) * p0_2 + t * p1_2

    return p_final

看看这个算法也就知道贝塞尔曲线是怎么来的了。

三阶公式是

$$
\mathbf{B}(t) = (1-t)^3 \mathbf{P}_0 + 3(1-t)^2 t \mathbf{P}_1 + 3(1-t) t^2 \mathbf{P}_2 + t^3 \mathbf{P}_3
$$

一般的 $n$ 阶公式是

$$
\mathbf{B}(t) = \sum_{i=0}^{m} \binom{m}{i} (1-t)^{m-i} t^i \mathbf{P}_i
$$

下图中,左图是 Godot 里的 Curve,它就用到了贝塞尔曲线;右图贝塞尔曲线的生成方法,和上面提到的 Casteljau Algorithm 一致。

再看看左图,我们会发现它是由三个贝塞尔曲线拼接而成的,而且看起来很平滑,这就是所谓的“分段贝塞尔曲线”。高阶贝塞尔曲线不容易控制,所以我们更倾向于把几个低阶贝塞尔曲线拼接起来形成复杂曲线。

贝塞尔曲面

我们同样用 Casteljau Algorithm 生成贝塞尔曲面:

def _de_casteljau_1d(points, t):
    """对一维点序列执行 Casteljau 算法。"""
    while len(points) > 1:
        points = [(1 - t) * p1 + t * p2 for p1, p2 in zip(points, points[1:])]
    return points[0]

def bezier_surface(control_points, u, v):
    """计算贝塞尔曲面上的一点。"""
    intermediate_points = [_de_casteljau_1d(row, u) for row in control_points]
    final_point = _de_casteljau_1d(intermediate_points, v)
    return final_point

如下图中的右图所示,我们本质上是先沿着一个轴向生成一组贝塞尔曲线,再在曲线上取值生成一组新的控制点,然后用这组新控制点定义一条新的曲线,最后在新的曲线上取值得到曲面上的最终点。

网格处理

生成三角形网格后,我们经常还希望做一些处理,比如细分来让模型更光滑;减少三角形数量来简化网格。下面简单提一下网格细分和网格简化的常用算法。

网格细分

网格细分算法的绝对主流是 Catmull-Clark 细分,Blender 的表面细分修改器的默认算法就是这个。它可以细分任意形状的多边形,但在细分四边形时效果最好。

课上还提到了用于细分三角形的 Loop 细分,不过两个细分都只是简单提了提概念,我们这里不多记笔记。

网格简化

网格简化的基本思路是每次把一条边坍缩成一个点。我们用二次误差衡量每条边坍缩后引入的误差,然后选择最小的边来坍缩,之后重新计算二次误差(因为坍缩后形状会改变),然后再次选择最小的边,如此重复。这是个贪婪算法,但能拿到不错的结果。

具体公式我们也不写,因为课上没详细讲,我也不感兴趣,用到的时候去查查就好了。