5254 字
26 分钟
计算机图形学笔记(三):几何建模与处理

计算机图形学笔记(三):几何建模与处理#

这部分内容相对来说比较抽象,但是在实际应用中非常重要。特别是贝塞尔曲线,在UI设计、动画制作中到处都能看到它的身影。学完之后才明白为什么Photoshop的钢笔工具那么好用。

目录#

  1. 参数曲线理论 - 参数化表示的数学基础
  2. 贝塞尔曲线深度解析 - de Casteljau算法与工程实现
  3. 样条曲线与曲面 - B样条与NURBS理论
  4. 网格几何处理 - 半边结构与网格算法

参数曲线理论#

12.1 参数曲线的数学基础#

12.1.1 参数表示的数学优势#

参数曲线的一般形式#

三维参数曲线C(t)=(x(t)y(t)z(t)),t[a,b]\mathbf{C}(t) = \begin{pmatrix} x(t) \\ y(t) \\ z(t) \end{pmatrix}, \quad t \in [a, b]

其中 x(t)x(t)y(t)y(t)z(t)z(t) 是关于参数 tt 的连续可微函数。

参数表示相比隐式表示的优势#

1. 计算便利性

  • 参数形式:直接代入参数值即可得到曲线上的点
  • 隐式形式F(x,y,z)=0F(x,y,z) = 0 需要求解方程

2. 动画支持: 参数 tt 可以直接作为时间参数,自然支持动画: position(t)=C(t),velocity(t)=C(t)\mathbf{position}(t) = \mathbf{C}(t), \quad \mathbf{velocity}(t) = \mathbf{C}'(t)

3. 多值函数处理: 避免了 y=f(x)y = f(x) 形式无法表示垂直线和闭合曲线的问题。

4. 微分几何计算: 切向量、法向量、曲率等几何量的计算更加直接。

12.1.2 曲线的微分几何性质#

切向量与速度#

切向量(速度向量)T(t)=C(t)=(x(t)y(t)z(t))\mathbf{T}(t) = \mathbf{C}'(t) = \begin{pmatrix} x'(t) \\ y'(t) \\ z'(t) \end{pmatrix}

单位切向量T^(t)=T(t)T(t)=C(t)C(t)\hat{\mathbf{T}}(t) = \frac{\mathbf{T}(t)}{\|\mathbf{T}(t)\|} = \frac{\mathbf{C}'(t)}{\|\mathbf{C}'(t)\|}

曲率的数学定义#

曲率的几何定义: 曲率 κ(t)\kappa(t) 描述曲线在某点处偏离直线的程度: κ(t)=T(t)C(t)=C(t)×C(t)C(t)3\kappa(t) = \frac{\|\mathbf{T}'(t)\|}{\|\mathbf{C}'(t)\|} = \frac{\|\mathbf{C}'(t) \times \mathbf{C}''(t)\|}{\|\mathbf{C}'(t)\|^3}

平面曲线的曲率公式: 对于平面曲线 C(t)=(x(t),y(t))\mathbf{C}(t) = (x(t), y(t))κ(t)=x(t)y(t)y(t)x(t)(x(t)2+y(t)2)3/2\kappa(t) = \frac{|x'(t)y''(t) - y'(t)x''(t)|}{(x'(t)^2 + y'(t)^2)^{3/2}}

弧长参数化#

弧长函数s(t)=atC(τ)dτs(t) = \int_a^t \|\mathbf{C}'(\tau)\| d\tau

弧长参数化的优势: 当曲线以弧长为参数时,C(s)=1\|\mathbf{C}'(s)\| = 1,简化了几何计算: κ(s)=C(s)\kappa(s) = \|\mathbf{C}''(s)\|

代码实现

float calculate_curvature_2d(float t,
std::function<Vector2f(float)> curve,
std::function<Vector2f(float)> first_derivative,
std::function<Vector2f(float)> second_derivative) {
Vector2f first = first_derivative(t);
Vector2f second = second_derivative(t);
float numerator = std::abs(first.x() * second.y() - first.y() * second.x());
float denominator = std::pow(first.squaredNorm(), 1.5f);
return numerator / denominator;
}

12.2 常见参数曲线的数学分析#

12.2.1 直线的参数表示#

线性插值的数学形式#

两点间直线的参数方程L(t)=P0+t(P1P0)=(1t)P0+tP1,t[0,1]\mathbf{L}(t) = \mathbf{P}_0 + t(\mathbf{P}_1 - \mathbf{P}_0) = (1-t)\mathbf{P}_0 + t\mathbf{P}_1, \quad t \in [0, 1]

几何意义

  • t=0t = 0:位于点 P0\mathbf{P}_0
  • t=1t = 1:位于点 P1\mathbf{P}_1
  • 0<t<10 < t < 1:位于线段 P0P1\mathbf{P}_0\mathbf{P}_1 内部

微分性质

  • 切向量L(t)=P1P0\mathbf{L}'(t) = \mathbf{P}_1 - \mathbf{P}_0(常向量)
  • 曲率κ(t)=0\kappa(t) = 0(直线曲率为零)

工程实现#

class ParametricLine {
private:
Vector3f p0, p1;
public:
ParametricLine(const Vector3f& start, const Vector3f& end) : p0(start), p1(end) {}
Vector3f evaluate(float t) const {
return (1.0f - t) * p0 + t * p1;
}
Vector3f tangent() const {
return (p1 - p0).normalized();
}
float length() const {
return (p1 - p0).norm();
}
};

12.2.2 圆与椭圆的参数表示#

圆的参数方程#

标准圆的参数形式C(t)=center+r(cos(t)sin(t)),t[0,2π]\mathbf{C}(t) = \mathbf{center} + r\begin{pmatrix} \cos(t) \\ \sin(t) \end{pmatrix}, \quad t \in [0, 2\pi]

微分性质

  • 切向量C(t)=r(sin(t)cos(t))\mathbf{C}'(t) = r\begin{pmatrix} -\sin(t) \\ \cos(t) \end{pmatrix}
  • 速度大小C(t)=r\|\mathbf{C}'(t)\| = r
  • 曲率κ=1r\kappa = \frac{1}{r}(常曲率)

椭圆的参数方程#

标准椭圆E(t)=center+(acos(t)bsin(t)),t[0,2π]\mathbf{E}(t) = \mathbf{center} + \begin{pmatrix} a\cos(t) \\ b\sin(t) \end{pmatrix}, \quad t \in [0, 2\pi]

其中 aabb 分别是长轴和短轴的半长度。

椭圆的曲率κ(t)=ab(a2sin2(t)+b2cos2(t))3/2\kappa(t) = \frac{ab}{(a^2\sin^2(t) + b^2\cos^2(t))^{3/2}}

特殊点的曲率

  • 长轴端点:κ=b2a3\kappa = \frac{b^2}{a^3}
  • 短轴端点:κ=a2b3\kappa = \frac{a^2}{b^3}

三维空间中的圆#

任意平面上的圆C(t)=center+r(cos(t)u+sin(t)v)\mathbf{C}(t) = \mathbf{center} + r(\cos(t)\mathbf{u} + \sin(t)\mathbf{v})

其中 u\mathbf{u}v\mathbf{v} 是圆所在平面的两个正交单位向量。

12.2.3 螺旋线#

平面螺旋(阿基米德螺线)S(t)=(atcos(t),atsin(t)),t[0,n2π]\mathbf{S}(t) = (at\cos(t), at\sin(t)), \quad t \in [0, n \cdot 2\pi]

空间螺旋S(t)=(acos(t),asin(t),bt),t[0,n2π]\mathbf{S}(t) = (a\cos(t), a\sin(t), bt), \quad t \in [0, n \cdot 2\pi]


贝塞尔曲线深度解析#

13.1 贝塞尔曲线的数学原理#

13.1.1 伯恩斯坦多项式的深度理论#

伯恩斯坦多项式的数学定义#

严格定义nn 次伯恩斯坦基函数定义为: Bin(t)=(ni)ti(1t)ni,i=0,1,,nB_i^n(t) = \binom{n}{i} t^i (1-t)^{n-i}, \quad i = 0, 1, \ldots, n

其中二项式系数: (ni)=n!i!(ni)!\binom{n}{i} = \frac{n!}{i!(n-i)!}

基本性质的数学证明#

1. 非负性Bin(t)0B_i^n(t) \geq 0 对所有 t[0,1]t \in [0,1]

  • 证明(ni)0\binom{n}{i} \geq 0ti0t^i \geq 0(1t)ni0(1-t)^{n-i} \geq 0

2. 归一性(权重和为1)i=0nBin(t)=1\sum_{i=0}^{n} B_i^n(t) = 1

  • 证明:利用二项式定理 i=0nBin(t)=i=0n(ni)ti(1t)ni=(t+(1t))n=1\sum_{i=0}^{n} B_i^n(t) = \sum_{i=0}^{n} \binom{n}{i} t^i (1-t)^{n-i} = (t + (1-t))^n = 1

3. 对称性Bin(t)=Bnin(1t)B_i^n(t) = B_{n-i}^n(1-t)

  • 证明Bnin(1t)=(nni)(1t)niti=(ni)ti(1t)ni=Bin(t)B_{n-i}^n(1-t) = \binom{n}{n-i} (1-t)^{n-i} t^i = \binom{n}{i} t^i (1-t)^{n-i} = B_i^n(t)

4. 端点性质

  • Bin(0)=δi0B_i^n(0) = \delta_{i0}(Kronecker delta)
  • Bin(1)=δinB_i^n(1) = \delta_{in}

5. 递推关系Bin(t)=(1t)Bin1(t)+tBi1n1(t)B_i^n(t) = (1-t)B_i^{n-1}(t) + tB_{i-1}^{n-1}(t)

这个递推关系是de Casteljau算法的数学基础。

伯恩斯坦多项式的几何意义#

概率解释Bin(t)B_i^n(t) 可以解释为在 nn 次独立伯努利试验中,成功概率为 tt 时,恰好成功 ii 次的概率。

权重函数性质

  • t=0t = 0 时,只有 B0n(0)=1B_0^n(0) = 1,其他为0
  • t=1t = 1 时,只有 Bnn(1)=1B_n^n(1) = 1,其他为0
  • t=i/nt = i/n 时,Bin(t)B_i^n(t) 达到最大值

13.1.2 贝塞尔曲线定义#

n次贝塞尔曲线B(t)=i=0nPiBin(t),t[0,1]B(t) = \sum_{i=0}^{n} P_i \cdot B_i^n(t), \quad t \in [0,1]

常见次数的显式公式

线性(1次)B(t)=(1t)P0+tP1B(t) = (1-t)P_0 + tP_1

二次B(t)=(1t)2P0+2t(1t)P1+t2P2B(t) = (1-t)^2P_0 + 2t(1-t)P_1 + t^2P_2

三次B(t)=(1t)3P0+3t(1t)2P1+3t2(1t)P2+t3P3B(t) = (1-t)^3P_0 + 3t(1-t)^2P_1 + 3t^2(1-t)P_2 + t^3P_3

13.1.3 项目中的朴素实现#

基于GAMES101 Assignment4的实现

cv::Point2f naive_bezier(const std::vector<cv::Point2f>& points, float t) {
// 三次贝塞尔曲线的直接计算
auto& p0 = points[0], &p1 = points[1], &p2 = points[2], &p3 = points[3];
float u = 1.0f - t;
float tt = t * t;
float uu = u * u;
float uuu = uu * u;
float ttt = tt * t;
cv::Point2f point = uuu * p0; // $(1-t)^3 \cdot P_0$
point += 3 * uu * t * p1; // $3(1-t)^2t \cdot P_1$
point += 3 * u * tt * p2; // $3(1-t)t^2 \cdot P_2$
point += ttt * p3; // $t^3 \cdot P_3$
return point;
}
void draw_bezier_curve(const std::vector<cv::Point2f>& control_points, cv::Mat& window) {
for (double t = 0.0; t <= 1.0; t += 0.001) {
auto point = naive_bezier(control_points, t);
window.at<cv::Vec3b>[point.y, point.x](2) = 255; // 红色通道
}
}

13.2 de Casteljau算法的数学理论#

13.2.1 算法的数学基础#

de Casteljau算法的几何原理#

核心思想: de Casteljau算法通过递归的线性插值来计算贝塞尔曲线上的点,这种方法在数值上比直接计算伯恩斯坦多项式更稳定。

数学表述: 设控制点为 P0,P1,,Pn\mathbf{P}_0, \mathbf{P}_1, \ldots, \mathbf{P}_n,定义递归序列: Pi(0)=Pi,i=0,1,,n\mathbf{P}_i^{(0)} = \mathbf{P}_i, \quad i = 0, 1, \ldots, n Pi(k)=(1t)Pi(k1)+tPi+1(k1),k=1,2,,n;i=0,1,,nk\mathbf{P}_i^{(k)} = (1-t)\mathbf{P}_i^{(k-1)} + t\mathbf{P}_{i+1}^{(k-1)}, \quad k = 1, 2, \ldots, n; \quad i = 0, 1, \ldots, n-k

最终结果:B(t)=P0(n)\mathbf{B}(t) = \mathbf{P}_0^{(n)}

算法的数学证明#

定理:de Casteljau算法计算的结果等于贝塞尔曲线的定义式。

证明思路: 利用数学归纳法证明: Pi(k)=j=0k(kj)tj(1t)kjPi+j\mathbf{P}_i^{(k)} = \sum_{j=0}^{k} \binom{k}{j} t^j (1-t)^{k-j} \mathbf{P}_{i+j}

基础步骤k=0k=0 时显然成立。

归纳步骤:假设对 k1k-1 成立,则:

递推关系: Pi(k)=(1t)Pi(k1)+tPi+1(k1)\mathbf{P}_i^{(k)} = (1-t)\mathbf{P}_i^{(k-1)} + t\mathbf{P}_{i+1}^{(k-1)}

代入归纳假设: Pi(k)=(1t)j=0k1(k1j)tj(1t)k1jPi+j+tj=0k1(k1j)tj(1t)k1jPi+1+j\mathbf{P}_i^{(k)} = (1-t)\sum_{j=0}^{k-1} \binom{k-1}{j} t^j (1-t)^{k-1-j} \mathbf{P}_{i+j} + t\sum_{j=0}^{k-1} \binom{k-1}{j} t^j (1-t)^{k-1-j} \mathbf{P}_{i+1+j}

化简得到: Pi(k)=j=0k(kj)tj(1t)kjPi+j\mathbf{P}_i^{(k)} = \sum_{j=0}^{k} \binom{k}{j} t^j (1-t)^{k-j} \mathbf{P}_{i+j}

算法的几何解释#

三次贝塞尔曲线示例: 对于控制点 P0,P1,P2,P3\mathbf{P}_0, \mathbf{P}_1, \mathbf{P}_2, \mathbf{P}_3

第一层插值

  • P0(1)=(1t)P0+tP1\mathbf{P}_0^{(1)} = (1-t)\mathbf{P}_0 + t\mathbf{P}_1
  • P1(1)=(1t)P1+tP2\mathbf{P}_1^{(1)} = (1-t)\mathbf{P}_1 + t\mathbf{P}_2
  • P2(1)=(1t)P2+tP3\mathbf{P}_2^{(1)} = (1-t)\mathbf{P}_2 + t\mathbf{P}_3

第二层插值

  • P0(2)=(1t)P0(1)+tP1(1)\mathbf{P}_0^{(2)} = (1-t)\mathbf{P}_0^{(1)} + t\mathbf{P}_1^{(1)}
  • P1(2)=(1t)P1(1)+tP2(1)\mathbf{P}_1^{(2)} = (1-t)\mathbf{P}_1^{(1)} + t\mathbf{P}_2^{(1)}

第三层插值

  • P0(3)=(1t)P0(2)+tP1(2)\mathbf{P}_0^{(3)} = (1-t)\mathbf{P}_0^{(2)} + t\mathbf{P}_1^{(2)}

最终 B(t)=P0(3)\mathbf{B}(t) = \mathbf{P}_0^{(3)}

13.2.2 递归实现#

cv::Point2f recursive_bezier(const std::vector<cv::Point2f>& control_points, float t) {
if (control_points.size() == 1) {
return control_points[0];
}
std::vector<cv::Point2f> new_points;
for (size_t i = 0; i < control_points.size() - 1; ++i) {
cv::Point2f interpolated = (1.0f - t) * control_points[i] + t * control_points[i + 1];
new_points.push_back(interpolated);
}
return recursive_bezier(new_points, t);
}

13.2.3 迭代实现(更高效)#

cv::Point2f iterative_bezier(std::vector<cv::Point2f> points, float t) {
int n = points.size();
for (int level = 1; level < n; ++level) {
for (int i = 0; i < n - level; ++i) {
points[i] = (1.0f - t) * points[i] + t * points[i + 1];
}
}
return points[0];
}

13.3 贝塞尔曲线的性质与应用#

13.3.1 重要几何性质#

端点插值性

  • B(0)=P0B(0) = P_0
  • B(1)=PnB(1) = P_n

端点切向量

  • B(0)=n(P1P0)B'(0) = n(P_1 - P_0)
  • B(1)=n(PnPn1)B'(1) = n(P_n - P_{n-1})

凸包性质: 贝塞尔曲线完全位于控制点的凸包内

仿射不变性: 先变换控制点再生成曲线 = 先生成曲线再变换

13.3.2 导数计算#

一阶导数

cv::Point2f bezier_derivative(const std::vector<cv::Point2f>& points, float t) {
int n = points.size() - 1;
std::vector<cv::Point2f> derivative_points;
for (int i = 0; i < n; ++i) {
derivative_points.push_back(n * (points[i + 1] - points[i]));
}
return recursive_bezier(derivative_points, t);
}

曲率计算

float bezier_curvature(const std::vector<cv::Point2f>& points, float t) {
cv::Point2f first = bezier_derivative(points, t);
cv::Point2f second = bezier_second_derivative(points, t);
float numerator = std::abs(first.x * second.y - first.y * second.x);
float denominator = std::pow(first.x * first.x + first.y * first.y, 1.5f);
return numerator / denominator;
}

13.3.3 自适应细分#

基于曲率的细分

void adaptive_bezier_subdivision(const std::vector<cv::Point2f>& points,
cv::Mat& window, float tolerance = 0.1f) {
std::function<void(float, float, int)> subdivide =
[&](float t_start, float t_end, int depth) {
if (depth > 10) return; // 防止无限递归
float t_mid = (t_start + t_end) * 0.5f;
cv::Point2f p_start = recursive_bezier(points, t_start);
cv::Point2f p_mid = recursive_bezier(points, t_mid);
cv::Point2f p_end = recursive_bezier(points, t_end);
// 检查中点是否偏离直线太远
cv::Point2f line_mid = (p_start + p_end) * 0.5f;
float distance = cv::norm(p_mid - line_mid);
if (distance > tolerance) {
subdivide(t_start, t_mid, depth + 1);
subdivide(t_mid, t_end, depth + 1);
} else {
cv::line(window, p_start, p_end, cv::Scalar(0, 255, 0), 2);
}
};
subdivide(0.0f, 1.0f, 0);
}

样条曲线与曲面#

14.1 样条曲线理论#

14.1.1 样条的数学定义#

样条函数的严格定义#

样条函数: 设节点序列 t0<t1<<tmt_0 < t_1 < \cdots < t_mpp 次样条函数 S(t)S(t) 是满足以下条件的分段多项式:

  1. 在每个区间 [ti,ti+1][t_i, t_{i+1}] 上,S(t)S(t) 是次数不超过 pp 的多项式
  2. S(t)S(t) 在整个定义域上具有 Cp1C^{p-1} 连续性

B样条基函数的递推定义#

0次B样条基函数

基函数定义:

tit<ti+1t_i \leq t < t_{i+1} 时:Ni,0(t)=1N_{i,0}(t) = 1

其他情况:Ni,0(t)=0N_{i,0}(t) = 0

高次B样条基函数(Cox-de Boor递推公式)Ni,p(t)=ttiti+ptiNi,p1(t)+ti+p+1tti+p+1ti+1Ni+1,p1(t)N_{i,p}(t) = \frac{t - t_i}{t_{i+p} - t_i} N_{i,p-1}(t) + \frac{t_{i+p+1} - t}{t_{i+p+1} - t_{i+1}} N_{i+1,p-1}(t)

约定:当分母为零时,对应项为零,即 00=0\frac{0}{0} = 0

B样条基函数的重要性质#

1. 非负性Ni,p(t)0N_{i,p}(t) \geq 0 对所有 tt

2. 局部支撑性Ni,p(t)=0N_{i,p}(t) = 0t[ti,ti+p+1]t \notin [t_i, t_{i+p+1}]

3. 权重和为1iNi,p(t)=1\sum_{i} N_{i,p}(t) = 1 对所有 tt

4. 连续性Ni,p(t)N_{i,p}(t) 在节点处具有 Cp1C^{p-1} 连续性

14.1.2 B样条曲线的数学理论#

B样条曲线的定义#

数学表达式C(t)=i=0nPiNi,p(t)\mathbf{C}(t) = \sum_{i=0}^{n} \mathbf{P}_i N_{i,p}(t)

其中:

  • Pi\mathbf{P}_i 是控制点
  • Ni,p(t)N_{i,p}(t)pp 次B样条基函数
  • n+1n+1 是控制点的数量

节点向量的作用#

节点向量T={t0,t1,,tm}\mathbf{T} = \{t_0, t_1, \ldots, t_{m}\},其中 m=n+p+1m = n + p + 1

节点向量的分类

  1. 均匀节点向量:节点等间距分布
  2. 非均匀节点向量:节点间距不等
  3. 开放节点向量:首末节点重复度为 p+1p+1

B样条曲线的性质#

1. 凸包性质:曲线位于控制点的凸包内

2. 局部控制性:移动控制点 Pi\mathbf{P}_i 只影响参数区间 [ti,ti+p+1][t_i, t_{i+p+1}] 内的曲线

3. 变分递减性:曲线与任意直线的交点数不超过控制多边形与该直线的交点数

4. 仿射不变性:仿射变换可直接作用于控制点

代码实现

class BSpline {
private:
std::vector<Vector3f> control_points;
std::vector<float> knot_vector;
int degree;
public:
float basis_function(int i, int p, float t) {
if (p == 0) {
return (t >= knot_vector[i] && t < knot_vector[i + 1]) ? 1.0f : 0.0f;
}
float left_coeff = 0.0f, right_coeff = 0.0f;
if (knot_vector[i + p] != knot_vector[i]) {
left_coeff = (t - knot_vector[i]) / (knot_vector[i + p] - knot_vector[i]);
}
if (knot_vector[i + p + 1] != knot_vector[i + 1]) {
right_coeff = (knot_vector[i + p + 1] - t) / (knot_vector[i + p + 1] - knot_vector[i + 1]);
}
return left_coeff * basis_function(i, p - 1, t) +
right_coeff * basis_function(i + 1, p - 1, t);
}
Vector3f evaluate(float t) {
Vector3f point = Vector3f::Zero();
for (int i = 0; i < control_points.size(); ++i) {
point += control_points[i] * basis_function(i, degree, t);
}
return point;
}
};

14.1.3 NURBS曲线的数学理论#

NURBS的定义#

非均匀有理B样条(NURBS)C(t)=i=0nwiPiNi,p(t)i=0nwiNi,p(t)\mathbf{C}(t) = \frac{\sum_{i=0}^{n} w_i \mathbf{P}_i N_{i,p}(t)}{\sum_{i=0}^{n} w_i N_{i,p}(t)}

其中 wiw_i 是控制点 Pi\mathbf{P}_i 的权重。

有理基函数#

有理基函数定义Ri,p(t)=wiNi,p(t)j=0nwjNj,p(t)R_{i,p}(t) = \frac{w_i N_{i,p}(t)}{\sum_{j=0}^{n} w_j N_{j,p}(t)}

NURBS曲线的简化表示C(t)=i=0nPiRi,p(t)\mathbf{C}(t) = \sum_{i=0}^{n} \mathbf{P}_i R_{i,p}(t)

NURBS的几何意义#

齐次坐标表示: NURBS可以看作是四维空间中B样条曲线的透视投影: Cw(t)=i=0nPiwNi,p(t)\mathbf{C}^w(t) = \sum_{i=0}^{n} \mathbf{P}_i^w N_{i,p}(t)

其中 Piw=(wixi,wiyi,wizi,wi)\mathbf{P}_i^w = (w_i x_i, w_i y_i, w_i z_i, w_i) 是齐次控制点。

透视投影C(t)=(Cxw(t),Cyw(t),Czw(t))Cww(t)\mathbf{C}(t) = \frac{(\mathbf{C}^w_x(t), \mathbf{C}^w_y(t), \mathbf{C}^w_z(t))}{\mathbf{C}^w_w(t)}

NURBS的优势#

1. 精确表示圆锥曲线:通过适当选择权重,可以精确表示圆、椭圆、抛物线、双曲线

2. 局部权重控制:改变权重 wiw_i 可以局部调整曲线形状

3. 投影不变性:透视投影下NURBS仍为NURBS

代码实现

class NURBS : public BSpline {
private:
std::vector<float> weights;
public:
Vector3f evaluate(float t) override {
Vector3f numerator = Vector3f::Zero();
float denominator = 0.0f;
for (int i = 0; i < control_points.size(); ++i) {
float basis = basis_function(i, degree, t);
numerator += weights[i] * control_points[i] * basis;
denominator += weights[i] * basis;
}
return numerator / denominator;
}
};

14.2 曲面理论#

14.2.1 贝塞尔曲面的数学理论#

贝塞尔曲面的定义#

双参数贝塞尔曲面S(u,v)=i=0mj=0nPijBim(u)Bjn(v)\mathbf{S}(u,v) = \sum_{i=0}^{m} \sum_{j=0}^{n} \mathbf{P}_{ij} B_i^m(u) B_j^n(v)

其中:

  • Pij\mathbf{P}_{ij}(m+1)×(n+1)(m+1) \times (n+1) 控制点网格
  • Bim(u)B_i^m(u)Bjn(v)B_j^n(v) 是伯恩斯坦基函数
  • (u,v)[0,1]×[0,1](u,v) \in [0,1] \times [0,1] 是参数域

曲面的几何性质#

边界曲线: 贝塞尔曲面的四条边界都是贝塞尔曲线:

四条边界曲线

左边界(u=0u=0): S(0,v)=j=0nP0jBjn(v)\mathbf{S}(0,v) = \sum_{j=0}^{n} \mathbf{P}_{0j} B_j^n(v)

右边界(u=1u=1): S(1,v)=j=0nPmjBjn(v)\mathbf{S}(1,v) = \sum_{j=0}^{n} \mathbf{P}_{mj} B_j^n(v)

下边界(v=0v=0): S(u,0)=i=0mPi0Bim(u)\mathbf{S}(u,0) = \sum_{i=0}^{m} \mathbf{P}_{i0} B_i^m(u)

上边界(v=1v=1): S(u,1)=i=0mPinBim(u)\mathbf{S}(u,1) = \sum_{i=0}^{m} \mathbf{P}_{in} B_i^m(u)

角点插值S(0,0)=P00,S(1,0)=Pm0,S(0,1)=P0n,S(1,1)=Pmn\mathbf{S}(0,0) = \mathbf{P}_{00}, \quad \mathbf{S}(1,0) = \mathbf{P}_{m0}, \quad \mathbf{S}(0,1) = \mathbf{P}_{0n}, \quad \mathbf{S}(1,1) = \mathbf{P}_{mn}

曲面法向量计算#

偏导数Su=i=0mj=0nPijdBim(u)duBjn(v)\frac{\partial \mathbf{S}}{\partial u} = \sum_{i=0}^{m} \sum_{j=0}^{n} \mathbf{P}_{ij} \frac{dB_i^m(u)}{du} B_j^n(v)

Sv=i=0mj=0nPijBim(u)dBjn(v)dv\frac{\partial \mathbf{S}}{\partial v} = \sum_{i=0}^{m} \sum_{j=0}^{n} \mathbf{P}_{ij} B_i^m(u) \frac{dB_j^n(v)}{dv}

法向量N(u,v)=Su×Sv\mathbf{N}(u,v) = \frac{\partial \mathbf{S}}{\partial u} \times \frac{\partial \mathbf{S}}{\partial v}

单位法向量N^(u,v)=N(u,v)N(u,v)\hat{\mathbf{N}}(u,v) = \frac{\mathbf{N}(u,v)}{\|\mathbf{N}(u,v)\|}

代码实现

class BezierSurface {
private:
std::vector<std::vector<Vector3f>> control_points; // m+1 x n+1 网格
int m, n; // 度数
public:
Vector3f evaluate(float u, float v) {
Vector3f point = Vector3f::Zero();
for (int i = 0; i <= m; ++i) {
for (int j = 0; j <= n; ++j) {
float basis_u = bernstein_polynomial(i, m, u);
float basis_v = bernstein_polynomial(j, n, v);
point += control_points[i][j] * basis_u * basis_v;
}
}
return point;
}
Vector3f normal(float u, float v) {
Vector3f du = partial_derivative_u(u, v);
Vector3f dv = partial_derivative_v(u, v);
return du.cross(dv).normalized();
}
private:
float bernstein_polynomial(int i, int n, float t) {
return binomial_coefficient(n, i) *
std::pow(t, i) * std::pow(1 - t, n - i);
}
};

14.2.2 曲面细分#

均匀细分

void subdivide_surface(BezierSurface& surface, int subdivisions) {
float step = 1.0f / subdivisions;
for (int i = 0; i <= subdivisions; ++i) {
for (int j = 0; j <= subdivisions; ++j) {
float u = i * step;
float v = j * step;
Vector3f point = surface.evaluate(u, v);
Vector3f normal = surface.normal(u, v);
// 添加到网格
add_vertex(point, normal);
}
}
// 生成三角形索引
for (int i = 0; i < subdivisions; ++i) {
for (int j = 0; j < subdivisions; ++j) {
int idx = i * (subdivisions + 1) + j;
// 第一个三角形
add_triangle(idx, idx + 1, idx + subdivisions + 1);
// 第二个三角形
add_triangle(idx + 1, idx + subdivisions + 2, idx + subdivisions + 1);
}
}
}

网格几何处理#

15.1 网格数据结构#

15.1.1 半边数据结构#

核心概念

  • 每条边分为两个半边
  • 每个半边指向一个顶点
  • 每个半边属于一个面

数据结构定义

struct HalfEdge {
int vertex; // 指向的顶点
int face; // 所属面
int next; // 同一面的下一条半边
int prev; // 同一面的前一条半边
int twin; // 对偶半边
};
struct Vertex {
Vector3f position;
Vector3f normal;
int halfedge; // 任意一条出边
};
struct Face {
int halfedge; // 任意一条边界半边
Vector3f normal;
};
class HalfEdgeMesh {
private:
std::vector<Vertex> vertices;
std::vector<HalfEdge> halfedges;
std::vector<Face> faces;
public:
// 遍历顶点的所有邻接顶点
std::vector<int> get_vertex_neighbors(int vertex_id) {
std::vector<int> neighbors;
int start_he = vertices[vertex_id].halfedge;
int current_he = start_he;
do {
int twin_he = halfedges[current_he].twin;
neighbors.push_back(halfedges[twin_he].vertex);
current_he = halfedges[twin_he].next;
} while (current_he != start_he);
return neighbors;
}
};

15.1.2 网格质量评估的数学理论#

三角形质量度量#

等周比(Isoperimetric Ratio)Q=43AP2Q = \frac{4\sqrt{3} \cdot A}{P^2}

其中:

  • AA 是三角形面积
  • PP 是三角形周长

数学性质

  • Q=1Q = 1 对于等边三角形(最优)
  • Q0Q \to 0 对于退化三角形
  • 0<Q10 < Q \leq 1 对于所有有效三角形

面积计算: 对于顶点 v1,v2,v3\mathbf{v}_1, \mathbf{v}_2, \mathbf{v}_3A=12(v2v1)×(v3v1)A = \frac{1}{2}\|(\mathbf{v}_2 - \mathbf{v}_1) \times (\mathbf{v}_3 - \mathbf{v}_1)\|

周长计算P=v2v1+v3v2+v1v3P = \|\mathbf{v}_2 - \mathbf{v}_1\| + \|\mathbf{v}_3 - \mathbf{v}_2\| + \|\mathbf{v}_1 - \mathbf{v}_3\|

其他质量度量#

长宽比(Aspect Ratio)AR=abc8(sa)(sb)(sc)AR = \frac{a \cdot b \cdot c}{8(s-a)(s-b)(s-c)}

其中 s=a+b+c2s = \frac{a+b+c}{2} 是半周长。

最小角度θmin=min{arccosb2+c2a22bc,arccosa2+c2b22ac,arccosa2+b2c22ab}\theta_{min} = \min\{\arccos\frac{b^2+c^2-a^2}{2bc}, \arccos\frac{a^2+c^2-b^2}{2ac}, \arccos\frac{a^2+b^2-c^2}{2ab}\}

工程实现#

class TriangleQuality {
public:
static float isoperimetric_ratio(const Vector3f& v1, const Vector3f& v2, const Vector3f& v3) {
Vector3f e1 = v2 - v1;
Vector3f e2 = v3 - v2;
Vector3f e3 = v1 - v3;
float a = e1.norm();
float b = e2.norm();
float c = e3.norm();
float area = 0.5f * e1.cross(-e3).norm(); // 使用叉积计算面积
float perimeter = a + b + c;
if (perimeter < 1e-8f) return 0.0f; // 避免除零
return 4.0f * std::sqrt(3.0f) * area / (perimeter * perimeter);
}
static float min_angle(const Vector3f& v1, const Vector3f& v2, const Vector3f& v3) {
Vector3f e1 = (v2 - v1).normalized();
Vector3f e2 = (v3 - v2).normalized();
Vector3f e3 = (v1 - v3).normalized();
float angle1 = std::acos(std::clamp(e1.dot(-e3), -1.0f, 1.0f));
float angle2 = std::acos(std::clamp((-e1).dot(e2), -1.0f, 1.0f));
float angle3 = std::acos(std::clamp((-e2).dot(e3), -1.0f, 1.0f));
return std::min({angle1, angle2, angle3});
}
};

网格统计信息

struct MeshStatistics {
float min_edge_length;
float max_edge_length;
float avg_edge_length;
float min_triangle_quality;
float avg_triangle_quality;
int degenerate_triangles;
};
MeshStatistics analyze_mesh(const HalfEdgeMesh& mesh) {
MeshStatistics stats;
// 实现统计计算...
return stats;
}

15.2 网格处理算法#

15.2.1 网格简化的数学理论#

二次误差度量(QEM)#

基本思想: 每个顶点关联一个二次误差矩阵,用于度量顶点到其邻接平面的距离平方和。

平面方程: 对于三角形面 ff,其平面方程为: πf:ax+by+cz+d=0\pi_f: ax + by + cz + d = 0

其中 n=(a,b,c)T\mathbf{n} = (a, b, c)^T 是单位法向量,dd 是到原点的距离。

二次误差矩阵: 对于平面 π=(a,b,c,d)T\pi = (a, b, c, d)^T,其对应的二次误差矩阵为:

二次误差矩阵 Qπ=ππT\mathbf{Q}_\pi = \pi \pi^T 的结构:

  • 第1行:(a2,ab,ac,ad)(a^2, ab, ac, ad)
  • 第2行:(ab,b2,bc,bd)(ab, b^2, bc, bd)
  • 第3行:(ac,bc,c2,cd)(ac, bc, c^2, cd)
  • 第4行:(ad,bd,cd,d2)(ad, bd, cd, d^2)

Qπ=quadric error matrix\mathbf{Q}_\pi = \text{quadric error matrix}

顶点的二次误差: 顶点 vv 的二次误差矩阵是其所有邻接面的二次误差矩阵之和: Qv=ffaces(v)Qπf\mathbf{Q}_v = \sum_{f \in \text{faces}(v)} \mathbf{Q}_{\pi_f}

点到平面距离的二次形式: 点 p=(x,y,z,1)T\mathbf{p} = (x, y, z, 1)^T 的二次误差为: error(p)=pTQvp\text{error}(\mathbf{p}) = \mathbf{p}^T \mathbf{Q}_v \mathbf{p}

边折叠的最优位置#

问题描述: 当边 (v1,v2)(v_1, v_2) 折叠为新顶点 vˉ\bar{v} 时,新顶点的二次误差矩阵为: Qvˉ=Qv1+Qv2\mathbf{Q}_{\bar{v}} = \mathbf{Q}_{v_1} + \mathbf{Q}_{v_2}

最优位置求解: 最小化二次误差 pTQvˉp\mathbf{p}^T \mathbf{Q}_{\bar{v}} \mathbf{p},其中 p=(x,y,z,1)T\mathbf{p} = (x, y, z, 1)^T

x,y,zx, y, z 求偏导并令其为零: x(pTQvˉp)=0\frac{\partial}{\partial x}(\mathbf{p}^T \mathbf{Q}_{\bar{v}} \mathbf{p}) = 0

得到线性方程组:

系数矩阵为左上角 3×33 \times 3 子矩阵,右端项为负的第4列前三个元素:

Q3×3(x,y,z)T=(q14,q24,q34)T\mathbf{Q}_{3 \times 3} \cdot (x, y, z)^T = -(q_{14}, q_{24}, q_{34})^T

工程实现#

class QEMSimplification {
private:
struct QuadricMatrix {
float q[10]; // 对称矩阵的上三角部分
QuadricMatrix() { std::fill(q, q + 10, 0.0f); }
// 从平面构造二次误差矩阵
QuadricMatrix(const Vector4f& plane) {
float a = plane.x(), b = plane.y(), c = plane.z(), d = plane.w();
q[0] = a*a; q[1] = a*b; q[2] = a*c; q[3] = a*d;
q[4] = b*b; q[5] = b*c; q[6] = b*d;
q[7] = c*c; q[8] = c*d;
q[9] = d*d;
}
QuadricMatrix operator+(const QuadricMatrix& other) const {
QuadricMatrix result;
for (int i = 0; i < 10; ++i) {
result.q[i] = q[i] + other.q[i];
}
return result;
}
float evaluate(const Vector3f& point) const {
Vector4f p(point.x(), point.y(), point.z(), 1.0f);
return p.x()*p.x()*q[0] + 2*p.x()*p.y()*q[1] + 2*p.x()*p.z()*q[2] + 2*p.x()*p.w()*q[3] +
p.y()*p.y()*q[4] + 2*p.y()*p.z()*q[5] + 2*p.y()*p.w()*q[6] +
p.z()*p.z()*q[7] + 2*p.z()*p.w()*q[8] +
p.w()*p.w()*q[9];
}
};
std::vector<QuadricMatrix> vertex_quadrics;
public:
float calculate_collapse_cost(const HalfEdgeMesh& mesh, int edge_id, Vector3f& optimal_pos) {
auto [v1, v2] = mesh.get_edge_vertices(edge_id);
QuadricMatrix combined = vertex_quadrics[v1] + vertex_quadrics[v2];
// 尝试三个候选位置:v1, v2, 最优解
Vector3f pos1 = mesh.get_vertex_position(v1);
Vector3f pos2 = mesh.get_vertex_position(v2);
Vector3f pos_optimal = solve_optimal_position(combined);
float cost1 = combined.evaluate(pos1);
float cost2 = combined.evaluate(pos2);
float cost_optimal = combined.evaluate(pos_optimal);
if (cost_optimal <= cost1 && cost_optimal <= cost2) {
optimal_pos = pos_optimal;
return cost_optimal;
} else if (cost1 <= cost2) {
optimal_pos = pos1;
return cost1;
} else {
optimal_pos = pos2;
return cost2;
}
}
};
### 15.2.2 网格细分的数学理论
#### Loop细分算法
**基本思想**
Loop细分是一种逼近细分方案,通过在每个三角形中插入新顶点并重新连接来细化网格。
#### 顶点位置更新规则
**原有顶点的新位置**
对于度数为 $n$ 的顶点 $v$,其新位置为:
$$\mathbf{v}_{new} = (1 - n\beta)\mathbf{v} + \beta\sum_{i=1}^{n}\mathbf{v}_i$$
其中 $\mathbf{v}_i$ 是 $v$ 的邻接顶点,权重 $\beta$ 定义为:
权重计算:
当 $n = 3$ 时:$\beta = \frac{3}{16}$
当 $n > 3$ 时:$\beta = \frac{3}{8n}$
**新顶点(边中点)的位置**
对于边 $e = (v_1, v_2)$,设其两个邻接三角形的第三个顶点分别为 $v_3$ 和 $v_4$,则新顶点位置为:
$$\mathbf{v}_{edge} = \frac{3}{8}(\mathbf{v}_1 + \mathbf{v}_2) + \frac{1}{8}(\mathbf{v}_3 + \mathbf{v}_4)$$
#### 数学性质
**收敛性**
Loop细分在 $C^2$ 连续性下收敛到光滑极限曲面,除了特殊点(度数不为6的顶点)。
**特征值分析**
细分矩阵的特征值决定了收敛性质:
- 主特征值 $\lambda_0 = 1$
- 次特征值 $\lambda_1 = \lambda_2 = \frac{1}{2} + \frac{1}{4}\cos\frac{2\pi}{n}$
#### 工程实现
```cpp
class LoopSubdivision {
public:
static void subdivide(HalfEdgeMesh& mesh) {
std::vector<Vector3f> new_vertex_positions;
std::vector<Vector3f> edge_midpoints;
// 1. 计算原有顶点的新位置
for (int i = 0; i < mesh.vertex_count(); ++i) {
new_vertex_positions.push_back(compute_vertex_position(mesh, i));
}
// 2. 计算边中点位置
for (int i = 0; i < mesh.edge_count(); ++i) {
edge_midpoints.push_back(compute_edge_midpoint(mesh, i));
}
// 3. 重建网格拓扑
rebuild_subdivided_mesh(mesh, new_vertex_positions, edge_midpoints);
}
private:
static Vector3f compute_vertex_position(const HalfEdgeMesh& mesh, int vertex_id) {
std::vector<int> neighbors = mesh.get_vertex_neighbors(vertex_id);
int n = neighbors.size();
float beta = (n == 3) ? 3.0f/16.0f : 3.0f/(8.0f*n);
Vector3f original_pos = mesh.get_vertex_position(vertex_id);
Vector3f neighbor_sum = Vector3f::Zero();
for (int neighbor : neighbors) {
neighbor_sum += mesh.get_vertex_position(neighbor);
}
return (1.0f - n*beta) * original_pos + beta * neighbor_sum;
}
static Vector3f compute_edge_midpoint(const HalfEdgeMesh& mesh, int edge_id) {
auto [v1, v2] = mesh.get_edge_vertices(edge_id);
auto [v3, v4] = mesh.get_edge_opposite_vertices(edge_id);
Vector3f pos1 = mesh.get_vertex_position(v1);
Vector3f pos2 = mesh.get_vertex_position(v2);
Vector3f pos3 = mesh.get_vertex_position(v3);
Vector3f pos4 = mesh.get_vertex_position(v4);
return 0.375f * (pos1 + pos2) + 0.125f * (pos3 + pos4);
}
};

15.2.3 网格平滑#

拉普拉斯平滑

void laplacian_smoothing(HalfEdgeMesh& mesh, float lambda = 0.5f, int iterations = 10) {
for (int iter = 0; iter < iterations; ++iter) {
std::vector<Vector3f> new_positions(mesh.vertex_count());
for (int i = 0; i < mesh.vertex_count(); ++i) {
if (mesh.is_boundary_vertex(i)) {
new_positions[i] = mesh.get_vertex_position(i);
continue;
}
std::vector<int> neighbors = mesh.get_vertex_neighbors(i);
Vector3f laplacian = Vector3f::Zero();
for (int neighbor : neighbors) {
laplacian += mesh.get_vertex_position(neighbor);
}
laplacian /= neighbors.size();
laplacian -= mesh.get_vertex_position(i);
new_positions[i] = mesh.get_vertex_position(i) + lambda * laplacian;
}
mesh.update_vertex_positions(new_positions);
}
}
计算机图形学笔记(三):几何建模与处理
https://kyc001.github.io/posts/计算机图形学笔记三/
作者
kyc001
发布于
2025-07-23
许可协议
CC BY-NC-SA 4.0