计算机图形学笔记(三):几何建模与处理
这部分内容相对来说比较抽象,但是在实际应用中非常重要。特别是贝塞尔曲线,在UI设计、动画制作中到处都能看到它的身影。学完之后才明白为什么Photoshop的钢笔工具那么好用。
目录
参数曲线理论
12.1 参数曲线的数学基础
12.1.1 参数表示的数学优势
参数曲线的一般形式
三维参数曲线:
其中 、、 是关于参数 的连续可微函数。
参数表示相比隐式表示的优势
1. 计算便利性:
- 参数形式:直接代入参数值即可得到曲线上的点
- 隐式形式: 需要求解方程
2. 动画支持: 参数 可以直接作为时间参数,自然支持动画:
3. 多值函数处理: 避免了 形式无法表示垂直线和闭合曲线的问题。
4. 微分几何计算: 切向量、法向量、曲率等几何量的计算更加直接。
12.1.2 曲线的微分几何性质
切向量与速度
切向量(速度向量):
单位切向量:
曲率的数学定义
曲率的几何定义: 曲率 描述曲线在某点处偏离直线的程度:
平面曲线的曲率公式: 对于平面曲线 :
弧长参数化
弧长函数:
弧长参数化的优势: 当曲线以弧长为参数时,,简化了几何计算:
代码实现:
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 直线的参数表示
线性插值的数学形式
两点间直线的参数方程:
几何意义:
- :位于点
- :位于点
- :位于线段 内部
微分性质:
- 切向量:(常向量)
- 曲率:(直线曲率为零)
工程实现
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 圆与椭圆的参数表示
圆的参数方程
标准圆的参数形式:
微分性质:
- 切向量:
- 速度大小:
- 曲率:(常曲率)
椭圆的参数方程
标准椭圆:
其中 和 分别是长轴和短轴的半长度。
椭圆的曲率:
特殊点的曲率:
- 长轴端点:
- 短轴端点:
三维空间中的圆
任意平面上的圆:
其中 和 是圆所在平面的两个正交单位向量。
12.2.3 螺旋线
平面螺旋(阿基米德螺线):
空间螺旋:
贝塞尔曲线深度解析
13.1 贝塞尔曲线的数学原理
13.1.1 伯恩斯坦多项式的深度理论
伯恩斯坦多项式的数学定义
严格定义: 次伯恩斯坦基函数定义为:
其中二项式系数:
基本性质的数学证明
1. 非负性: 对所有
- 证明:,,
2. 归一性(权重和为1):
- 证明:利用二项式定理
3. 对称性:
- 证明:
4. 端点性质:
- (Kronecker delta)
5. 递推关系:
这个递推关系是de Casteljau算法的数学基础。
伯恩斯坦多项式的几何意义
概率解释: 可以解释为在 次独立伯努利试验中,成功概率为 时,恰好成功 次的概率。
权重函数性质:
- 在 时,只有 ,其他为0
- 在 时,只有 ,其他为0
- 在 时, 达到最大值
13.1.2 贝塞尔曲线定义
n次贝塞尔曲线:
常见次数的显式公式:
线性(1次):
二次:
三次:
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算法通过递归的线性插值来计算贝塞尔曲线上的点,这种方法在数值上比直接计算伯恩斯坦多项式更稳定。
数学表述: 设控制点为 ,定义递归序列:
最终结果:
算法的数学证明
定理:de Casteljau算法计算的结果等于贝塞尔曲线的定义式。
证明思路: 利用数学归纳法证明:
基础步骤: 时显然成立。
归纳步骤:假设对 成立,则:
递推关系:
代入归纳假设:
化简得到:
算法的几何解释
三次贝塞尔曲线示例: 对于控制点 :
第一层插值:
第二层插值:
第三层插值:
最终
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 重要几何性质
端点插值性:
端点切向量:
凸包性质: 贝塞尔曲线完全位于控制点的凸包内
仿射不变性: 先变换控制点再生成曲线 = 先生成曲线再变换
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 样条的数学定义
样条函数的严格定义
样条函数: 设节点序列 , 次样条函数 是满足以下条件的分段多项式:
- 在每个区间 上, 是次数不超过 的多项式
- 在整个定义域上具有 连续性
B样条基函数的递推定义
0次B样条基函数:
基函数定义:
当 时:
其他情况:
高次B样条基函数(Cox-de Boor递推公式):
约定:当分母为零时,对应项为零,即 。
B样条基函数的重要性质
1. 非负性: 对所有
2. 局部支撑性: 当
3. 权重和为1: 对所有
4. 连续性: 在节点处具有 连续性
14.1.2 B样条曲线的数学理论
B样条曲线的定义
数学表达式:
其中:
- 是控制点
- 是 次B样条基函数
- 是控制点的数量
节点向量的作用
节点向量:,其中
节点向量的分类:
- 均匀节点向量:节点等间距分布
- 非均匀节点向量:节点间距不等
- 开放节点向量:首末节点重复度为
B样条曲线的性质
1. 凸包性质:曲线位于控制点的凸包内
2. 局部控制性:移动控制点 只影响参数区间 内的曲线
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):
其中 是控制点 的权重。
有理基函数
有理基函数定义:
NURBS曲线的简化表示:
NURBS的几何意义
齐次坐标表示: NURBS可以看作是四维空间中B样条曲线的透视投影:
其中 是齐次控制点。
透视投影:
NURBS的优势
1. 精确表示圆锥曲线:通过适当选择权重,可以精确表示圆、椭圆、抛物线、双曲线
2. 局部权重控制:改变权重 可以局部调整曲线形状
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 贝塞尔曲面的数学理论
贝塞尔曲面的定义
双参数贝塞尔曲面:
其中:
- 是 控制点网格
- 和 是伯恩斯坦基函数
- 是参数域
曲面的几何性质
边界曲线: 贝塞尔曲面的四条边界都是贝塞尔曲线:
四条边界曲线:
左边界():
右边界():
下边界():
上边界():
角点插值:
曲面法向量计算
偏导数:
法向量:
单位法向量:
代码实现:
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):
其中:
- 是三角形面积
- 是三角形周长
数学性质:
- 对于等边三角形(最优)
- 对于退化三角形
- 对于所有有效三角形
面积计算: 对于顶点 :
周长计算:
其他质量度量
长宽比(Aspect Ratio):
其中 是半周长。
最小角度:
工程实现
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)
基本思想: 每个顶点关联一个二次误差矩阵,用于度量顶点到其邻接平面的距离平方和。
平面方程: 对于三角形面 ,其平面方程为:
其中 是单位法向量, 是到原点的距离。
二次误差矩阵: 对于平面 ,其对应的二次误差矩阵为:
二次误差矩阵 的结构:
- 第1行:
- 第2行:
- 第3行:
- 第4行:
顶点的二次误差: 顶点 的二次误差矩阵是其所有邻接面的二次误差矩阵之和:
点到平面距离的二次形式: 点 的二次误差为:
边折叠的最优位置
问题描述: 当边 折叠为新顶点 时,新顶点的二次误差矩阵为:
最优位置求解: 最小化二次误差 ,其中 。
对 求偏导并令其为零:
得到线性方程组:
系数矩阵为左上角 子矩阵,右端项为负的第4列前三个元素:
工程实现
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}$
#### 工程实现
```cppclass 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); }}