几何的显示表示

点阵云

用点表示模型的所有的几何点,只要当点云的密度足够大,那么我们就看不出来点与点之间的距离,但是需要表示非常精细的模型通常需要使用非常多非常密的点,通常将现实世界中的物体扫描到电脑中都是使用的点云。由于点云需要非常多的点才能精确的表示一个模型,所以通常人们会想办法使用点云中的点构建三角形面

Polygon Mesh

在图形学中使用的最广泛的显示表示方法就是多边形面了,特别是三角形面和四边形面,我们将物体使用各种多边形面去表示,我们之前的也都是使用三角形描述不同的物体的,它们涉及到不同的三角形面的链接关系

The Wavefront Object File Format

The Wavefront Object File Format 是一种用于描述三维模型的文件格式,后缀是.obj,它是使用的Polygon Mesh表示方式,它是一种文本文件格式,其中使用文本描述了一个模型的三角形顶点,法线,纹理坐标等信息
例如

# Quad OBJ file

# 顶点坐标 (v x y z)
v -1.0 0.0 -1.0
v 1.0 0.0 -1.0
v 1.0 0.0 1.0
v -1.0 0.0 1.0

# 纹理坐标 (vt u v)
vt 0.0 0.0
vt 1.0 0.0
vt 1.0 1.0
vt 0.0 1.0

# 法线坐标 (vn x y z)
vn 0.0 1.0 0.0

# 面索引 (f v/vt/vn)
# 这里的 1/1/1 表示:第1个顶点/第1个纹理坐标/第1个法线
f 1/1/1 2/2/1 3/3/1 4/4/1

Curves

Bezier Curves

贝塞尔曲线 Bezier Curves 是一种利用一系列控制点去定义某一个曲线,这些控制点会控制曲线的一些属性,比如曲线的形状,曲线的曲率,曲线的起始点,曲线的结束点等等。在起始点的曲线的切线必然与$P_1-P_0$方向一致,在结束点的曲线的切线必然与$P_3-P_2$方向一致, 最重要的问题是我们怎么去画一条 Bezier Curve.
我们先看看一个最最简单的问题,如何使用两个点表示两个点之间的任何一个点,当然这就是线性插值,我们可以使用一个参数$t$决定这个点在两个点之间的位置

$$b_0 = lerp(p_0, p_1, t) = (1-t)p_0 + tp_1$$

现在增加一个点$p_2$,那么我们能表示两个线段,我们再使用一个线性插值

$$b_1 = lerp(p_1, p_2, t) = (1-t)p_1 + tp_2$$

现在我们通过3个点,插值得到了两个点$b_0$和$b_1$,然后我们对这两个点再进行一次插值

$$b_2 = lerp(b_0, b_1, t) = (1-t)b_0 + tb_1$$

这样我们就得到了$b_2$,只要我们让t从0到1,遍历一次,我们就能得到一条曲线,这个曲线是$b_2$的运动轨迹,恭喜你发明了2次贝塞尔曲线。 将上述推导中的 $b_0$ 和 $b_1$ 代入到 $b_2$ 的等式中:

$$\begin{aligned} \mathbf{B}(t) &= (1-t)b_0 + t b_1 \\ &= (1-t)[(1-t)p_0 + t p_1] + t[(1-t)p_1 + t p_2] \\ &= (1-t)^2 p_0 + 2t(1-t)p_1 + t^2 p_2 \end{aligned}$$

这就是二次贝塞尔曲线 (Quadratic Bézier Curve) 的最终形式,其中 $t \in [0, 1]$。 类似的我们可以增加更多控制点,比如在3个控制点上再加一个,就能表示三个线段,我们对三个线段进行线性插值,得到3个点,而3个点的情形我们已经知道了。依次类推我们能得到各种阶的贝塞尔曲线.
这种嵌套插值的过程本质上是一个递归定义,被称为 De Casteljau 算法,它是计算机图形学处理贝塞尔曲线的工业标准。对于任意一阶Bezier曲线,我们可以通过下面的公式来求解:

$$b^n(t) = b_0^n(t)=\sum_{j=0}^{n}b_jB^{n}_j(t)$$

其中

$$B^{n}_i(t) = \begin{pmatrix} n \\ i \end{pmatrix}t^i(1-t)^{n-i}$$

例如:

Bezier Curves的性质

  • $b(0)=b_0$,$b(1)=b_1$
  • 对于三阶的Bezier Curve $b^{\prime}(0)=3(b_1-b_0)$,$b^{\prime}(1)=3(b_3-b_2)$
  • 对控制点做仿射变换后得到的曲线和对贝塞尔曲线通过参数t得到的轨迹点做仿射变换得到的曲线是一样的,对投影变换不行
  • 贝塞尔曲线一定在控制点的凸包内部

Piecewise Bezier Curves

我们看上面这副图,我们使用了11个控制点绘制一个贝塞尔曲线但是中间的点并没有让曲线弯曲的很明显,所以这种多个控制点控制一条曲线的效果可能并不是很好,所以人们就想我们能不能每次都使用很少的控制点(一般是4个控制点)画一个小的曲线,然后我们将这些曲线链接起来变成一个大的曲线,这正是PS中的钢笔工具

Bezier Surfaces

我们已经知道如何获得一条贝塞尔曲线,那么如何获得一个贝塞尔曲面呢?

其实思路是一脉相承的:曲面就是“曲线的曲线”

在 $u$ 方向上插值 假设我们空间中有 $4 \times 4$ 个控制点(即 16 个点组成的阵列)。我们先看每一行的 4 个点,这 4 个点可以定义一条关于参数 $u$ 的贝塞尔曲线。因为有 4 行,所以我们得到了 4 条平行的曲线:$C_1, C_2, C_3, C_4$。 当我们选定一个具体的 $u$ 值时,这 4 条曲线上分别会产生 4 个动点,我们称之为 $p_0, p_1, p_2, p_3$。

在 $v$ 方向上插值 现在,这 4 个动点 $p_0$ 到 $p_3$ 随着 $u$ 的移动而在空间中变换位置。关键点来了:我们将这 4 个动点看作是“新的控制点”,再定义一条关于第二个参数 $v$ 的贝塞尔曲线。 随着 $u$ 在 $[0, 1]$ 范围移动,我们插值出的“新曲线”也会在空间中扫掠。这个由两个参数 $(u, v)$ 共同决定的点 $\mathbf{B}(u, v)$ 的运动轨迹,就是 Bezier Surface(贝塞尔曲面)

数学表达

如果用公式来表达这种“双重插值”,它看起来是这样的:

$$\mathbf{P}(u, v) = \sum_{i=0}^{n} \sum_{j=0}^{m} B_i^n(u) B_j^m(v) \mathbf{P}_{i,j}$$

这里的 $B(u)$ 和 $B(v)$ 就是我们之前提到的基函数。你可以直观地理解为:先在 $u$ 方向做一次加权平均,把结果作为系数,再在 $v$ 方向做一次加权平均。

作业4解析

这次作业非常简单,画一条贝塞尔曲线就行了,作业框架还是像往常一样配置,没有新添加的内容

 1cv::Point2f recursive_bezier(const std::vector<cv::Point2f> &control_points,
 2                             float t) {
 3  // 如果只有一个控制点,直接返回这个点
 4  if (control_points.size() == 1) {
 5    return control_points[0];
 6  }
 7
 8  std::vector<cv::Point2f> next_points;
 9  for (size_t i = 0; i < control_points.size() - 1; ++i) {
10    // 通过线性插值计算下一层的控制点
11    cv::Point2f p = (1 - t) * control_points[i] + t * control_points[i + 1];
12    // 将计算得到的点加入下一层的控制点列表
13    next_points.push_back(p);
14  }
15  return recursive_bezier(next_points, t);
16}
17
18void bezier(const std::vector<cv::Point2f> &control_points, cv::Mat &window) {
19  for (double t = 0.0; t <= 1.0; t += 0.001) {
20    // 调用递归的贝塞尔算法,得到 t 对应的点
21    cv::Point2f point = recursive_bezier(control_points, t);
22    // 绘制这个点,颜色是绿色
23    window.at<cv::Vec3b>(point.y, point.x)[1] = 255;
24  }
25}

运行结果如下: