4计算机图形学原理及实践:基础方法(第3版)

曲线与曲面

几何建模

贝塞尔曲线B样条NURBS

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第4章 曲线与曲面(Curves and Surfaces)

一、导读

1.1 本章学习目标

在计算机图形学中,如何用数学方式精确描述复杂的形状是一个核心问题。无论是汽车车身的流线型曲面、角色动画中的有机形体,还是字体中的优美曲线,都需要一种灵活、精确且计算高效的几何表示方法。曲线与曲面理论正是解决这个问题的数学工具。

本章的核心学习目标包括:

  • 理解参数曲线的基本原理:掌握用参数方程表示曲线的方法,理解参数化方式对曲线质量的影响。
  • 掌握贝塞尔曲线的理论和算法:深入理解伯恩斯坦基函数的性质、de Casteljau 算法的几何直觉,以及贝塞尔曲线的凸包性、仿射不变性等重要性质。
  • 理解 B 样条的优势:为什么 B 样条比贝塞尔曲线更适合实际建模?局部控制性、节点向量和基函数的概念是什么?
  • 掌握 NURBS 的原理:NURBS 如何统一了贝塞尔曲线、B 样条和有理曲线的表示?为什么它成为 CAD 工业标准?
  • 了解曲面建模方法:从参数曲面到细分曲面,理解不同曲面表示方法的特点和适用场景。
  • 理解曲线曲面在实际应用中的角色:字体设计(TrueType 使用贝塞尔曲线)、CAD/CAM、动画路径设计等。

1.2 为什么本章如此重要

曲线与曲面是计算机辅助设计(CAD)、工业建模和动画系统的数学基础。当你使用 Photoshop 的钢笔工具绘制路径时,你在操作贝塞尔曲线;当工程师设计汽车或飞机时,他们使用 NURBS 曲面来定义精确的外形;当动画师为角色设计运动路径时,他们使用样条曲线来创建平滑的运动轨迹。

更重要的是,曲线曲面理论体现了计算机图形学中一个深刻的思想:用少量控制参数(控制点、权重等)来描述复杂的几何形状。这种"用少控多"的思想贯穿整个图形学——从变形技术到程序化生成,核心都是用紧凑的参数空间控制丰富的几何细节。

在现代渲染管线中,GPU 的曲面细分(Tessellation)阶段直接支持参数曲面的实时求值和渲染,使得曲线曲面理论从离线设计工具延伸到了实时渲染领域。

1.3 核心问题引导

在阅读本章时,请带着以下问题思考:

为什么用多项式表示曲线?高阶多项式有什么问题?为什么分段低阶多项式(样条)成为主流选择?

贝塞尔曲线的 de Casteljau 算法为什么比直接计算伯恩斯坦多项式更稳定?它的几何直觉是什么?

贝塞尔曲线的全局控制性是什么意思?为什么移动一个控制点会影响整条曲线?这在设计中有什么不便?

B 样条如何实现局部控制?节点向量的选择如何影响曲线的形状?均匀 B 样条和非均匀 B 样条有什么区别?

为什么需要"有理"曲线?圆锥曲线(圆、椭圆、抛物线)为什么不能用多项式曲线精确表示?

细分曲面(Subdivision Surface)与参数曲面有什么本质区别?Pixar 为什么选择细分曲面作为动画建模的基础?


二、核心概念详解

2.1 参数曲线基础

2.1.1 参数方程

参数曲线用参数 t 来表示曲线上的点。对于二维曲线:

P(t) = (x(t), y(t)),  t ∈ [a, b]

最常见的参数曲线是多项式参数曲线:

P(t) = a₀ + a₁t + a₂t² + ... + aₙtⁿ = Σ aᵢtⁱ
cpp
// 三次多项式曲线
struct CubicCurve {
  glm::vec3 a0, a1, a2, a3; // 系数

  glm::vec3 evaluate(float t) const {
    return a0 + a1 * t + a2 * t * t + a3 * t * t * t;
  }

  glm::vec3 derivative(float t) const {
    return a1 + 2.0f * a2 * t + 3.0f * a3 * t * t;
  }
};

2.1.2 参数化的选择

同一条几何曲线可以用不同的参数化方式表示,参数化的选择影响动画和插值的质量:

  • 均匀参数化:参数等间距分布,简单但可能导致速度不均匀
  • 弦长参数化:参数间距与弦长成比例,通常效果最好
  • 向心参数化:参数间距与弦长的平方根成比例,避免尖角和自交
cpp
// 弦长参数化
std::vector<float> chordLengthParam(const std::vector<glm::vec3>& points) {
  int n = points.size();
  std::vector<float> params(n);
  params[0] = 0.0f;
  float totalLength = 0.0f;
  for (int i = 1; i < n; i++) {
    totalLength += glm::length(points[i] - points[i-1]);
    params[i] = totalLength;
  }
  // 归一化到 [0, 1]
  for (int i = 0; i < n; i++) {
    params[i] /= totalLength;
  }
  return params;
}

2.2 贝塞尔曲线

2.2.1 定义与伯恩斯坦基函数

n 次贝塞尔曲线由 n+1 个控制点定义:

B(t) = Σᵢ₌₀ⁿ Pᵢ · Bᵢ,ₙ(t),  t ∈ [0, 1]

其中 Bᵢ,ₙ(t) 是 n 次伯恩斯坦基函数:
Bᵢ,ₙ(t) = C(n,i) · tⁱ · (1-t)ⁿ⁻ⁱ

C(n,i) = n! / (i! · (n-i)!) 是二项式系数
cpp
// 伯恩斯坦基函数
float bernstein(int i, int n, float t) {
  // 计算组合数 C(n, i)
  int comb = 1;
  for (int j = 0; j < i; j++) {
    comb = comb * (n - j) / (j + 1);
  }
  return comb * std::pow(t, i) * std::pow(1 - t, n - i);
}

// 贝塞尔曲线求值
glm::vec3 bezierCurve(const std::vector<glm::vec3>& controlPoints, float t) {
  int n = controlPoints.size() - 1;
  glm::vec3 result(0.0f);
  for (int i = 0; i <= n; i++) {
    result += bernstein(i, n, t) * controlPoints[i];
  }
  return result;
}

2.2.2 de Casteljau 算法

de Casteljau 算法是计算贝塞尔曲线上点的数值稳定方法。它的核心思想是递归的线性插值:

给定 n+1 个控制点 P₀, P₁, ..., Pₙ 和参数 t:

第 0 层: P₀⁰ = P₀, P₁⁰ = P₁, ..., Pₙ⁰ = Pₙ
第 r 层: Pᵢʳ = (1-t) · Pᵢʳ⁻¹ + t · Pᵢ₊₁ʳ⁻¹
...
第 n 层: P₀ⁿ = 曲线上的点 B(t)
cpp
// de Casteljau 算法
glm::vec3 deCasteljau(std::vector<glm::vec3> points, float t) {
  int n = points.size();
  for (int r = 1; r < n; r++) {
    for (int i = 0; i < n - r; i++) {
      points[i] = (1.0f - t) * points[i] + t * points[i + 1];
    }
  }
  return points[0];
}

// 带中间点保存的版本(用于曲线细分)
void deCasteljauWithPoints(const std::vector<glm::vec3>& controlPoints,
                           float t,
                           std::vector<glm::vec3>& leftPoints,
                           std::vec3<glm::vec3>& rightPoints) {
  int n = controlPoints.size();
  std::vector<std::vector<glm::vec3>> triangle(n);
  triangle[0] = controlPoints;

  for (int r = 1; r < n; r++) {
    triangle[r].resize(n - r);
    for (int i = 0; i < n - r; i++) {
      triangle[r][i] = (1.0f - t) * triangle[r-1][i] +
                        t * triangle[r-1][i+1];
    }
  }

  // 左子曲线的控制点
  for (int r = 0; r < n; r++) {
    leftPoints.push_back(triangle[r][0]);
  }
  // 右子曲线的控制点
  for (int r = 0; r < n; r++) {
    rightPoints.push_back(triangle[n-1-r][r]);
  }
}

de Casteljau 算法的优势:

  • 数值稳定:相比直接计算伯恩斯坦多项式,避免了高次幂的计算
  • 几何直觉清晰:每一步都是简单的线性插值
  • 支持曲线细分:可以将一条贝塞尔曲线精确地分成两条同阶的子曲线

2.2.3 贝塞尔曲线的性质

贝塞尔曲线具有以下重要性质:

端点插值:曲线经过第一个和最后一个控制点

端点切线:曲线在端点处的切线方向指向相邻的控制点

凸包性:曲线完全包含在控制点的凸包内

仿射不变性:对控制点做仿射变换等价于对曲线做仿射变换

变差缩减性:曲线比控制多边形更"平滑"

cpp
// 常用贝塞尔曲线类型
// 一次(线性):2个控制点,就是直线段
// 二次:3个控制点
glm::vec3 quadraticBezier(glm::vec3 p0, glm::vec3 p1, glm::vec3 p2, float t) {
  float u = 1.0f - t;
  return u*u*p0 + 2*u*t*p1 + t*t*p2;
}

// 三次:4个控制点(最常用,TrueType字体、SVG路径)
glm::vec3 cubicBezier(glm::vec3 p0, glm::vec3 p1,
                      glm::vec3 p2, glm::vec3 p3, float t) {
  float u = 1.0f - t;
  return u*u*u*p0 + 3*u*u*t*p1 + 3*u*t*t*p2 + t*t*t*p3;
}

2.3 B 样条曲线

2.3.1 贝塞尔曲线的局限性

贝塞尔曲线虽然优美,但存在一个严重的实际问题:全局控制性。移动任何一个控制点都会影响整条曲线的形状。当需要设计复杂的形状时,这变得非常不方便——调整曲线的一部分时,其他部分也会跟着变化。

B 样条(B-spline)通过引入节点向量和局部基函数解决了这个问题,实现了局部控制:移动一个控制点只影响曲线的局部区域。

2.3.2 B 样条的定义

p 次 B 样条曲线由 n+1 个控制点和节点向量定义:

C(t) = Σᵢ₌₀ⁿ Nᵢ,ₚ(t) · Pᵢ

其中 Nᵢ,ₚ(t) 是 p 次 B 样条基函数,由 Cox-de Boor 递推定义:

Nᵢ,₀(t) = 1  如果 tᵢ ≤ t < tᵢ₊₁
           0  其他

Nᵢ,ₚ(t) = (t - tᵢ)/(tᵢ₊ₚ - tᵢ) · Nᵢ,ₚ₋₁(t) +
           (tᵢ₊ₚ₊₁ - t)/(tᵢ₊ₚ₊₁ - tᵢ₊₁) · Nᵢ₊₁,ₚ₋₁(t)
cpp
// Cox-de Boor 递推计算 B 样条基函数
float basisFunction(int i, int p, float t,
                    const std::vector<float>& knots) {
  if (p == 0) {
    return (t >= knots[i] && t < knots[i + 1]) ? 1.0f : 0.0f;
  }

  float left = 0.0f, right = 0.0f;

  float denom1 = knots[i + p] - knots[i];
  if (denom1 > 1e-8f) {
    left = (t - knots[i]) / denom1 *
           basisFunction(i, p - 1, t, knots);
  }

  float denom2 = knots[i + p + 1] - knots[i + 1];
  if (denom2 > 1e-8f) {
    right = (knots[i + p + 1] - t) / denom2 *
            basisFunction(i + 1, p - 1, t, knots);
  }

  return left + right;
}

// B 样条曲线求值
glm::vec3 bsplineCurve(const std::vector<glm::vec3>& controlPoints,
                       int degree,
                       const std::vector<float>& knots,
                       float t) {
  int n = controlPoints.size();
  glm::vec3 result(0.0f);
  for (int i = 0; i < n; i++) {
    float basis = basisFunction(i, degree, t, knots);
    result += basis * controlPoints[i];
  }
  return result;
}

2.3.3 节点向量

节点向量是 B 样条的核心概念,它决定了基函数的形状和分布:

节点向量的选择影响曲线性质:

均匀节点向量:
T = {0, 1, 2, 3, 4, 5, 6, 7, 8}
→ 均匀 B 样条,基函数形状相同

非均匀节点向量:
T = {0, 0, 0, 0, 1, 2, 3, 3, 3, 3}
→  clamped B 样条,曲线经过首末控制点

节点重复度影响连续性:
- 重复度 = 1:C^(p-1) 连续(最高光滑度)
- 重复度 = p:C⁰ 连续(位置连续,可以有尖角)
- 重复度 = p+1:完全断开
cpp
// 生成 clamped 均匀节点向量
std::vector<float> makeClampedKnots(int n, int p) {
  int m = n + p + 1;
  std::vector<float> knots(m);

  // 前 p+1 个节点为 0
  for (int i = 0; i <= p; i++) knots[i] = 0.0f;

  // 中间节点均匀分布
  int internalKnots = m - 2 * (p + 1);
  for (int i = 1; i <= internalKnots; i++) {
    knots[p + i] = (float)i / (internalKnots + 1);
  }

  // 后 p+1 个节点为 1
  for (int i = m - p - 1; i < m; i++) knots[i] = 1.0f;

  return knots;
}

2.3.4 B 样条 vs 贝塞尔

特性贝塞尔曲线B 样条曲线
控制方式全局控制局部控制
端点插值自动经过首末控制点需要 clamped 节点才能插值端点
阶数与控制点数阶数 = 控制点数 - 1阶数独立于控制点数
连续性C^(n-1)(n为控制点数)C^(p-1)(p为阶数)
凸包性凸包较紧凸包较松(局部性)
适用场景简单形状设计复杂形状建模

2.4 NURBS

2.4.1 从 B 样条到 NURBS

NURBS(Non-Uniform Rational B-Splines,非均匀有理 B 样条)是 B 样条的有理推广。每个控制点附加一个权重 wᵢ:

C(t) = Σᵢ₌₀ⁿ Nᵢ,ₚ(t) · wᵢ · Pᵢ / Σᵢ₌₀ⁿ Nᵢ,ₚ(t) · wᵢ

当所有 wᵢ = 1 时,NURBS 退化为 B 样条

为什么需要"有理"表示?因为多项式曲线无法精确表示圆锥曲线(圆、椭圆、抛物线、双曲线)。例如,用多项式曲线只能近似表示圆,而用有理曲线可以精确表示。

cpp
// NURBS 曲线求值
glm::vec3 nurbsCurve(const std::vector<glm::vec3>& controlPoints,
                     const std::vector<float>& weights,
                     int degree,
                     const std::vector<float>& knots,
                     float t) {
  int n = controlPoints.size();
  glm::vec3 numerator(0.0f);
  float denominator = 0.0f;

  for (int i = 0; i < n; i++) {
    float basis = basisFunction(i, degree, t, knots);
    float w = weights[i];
    numerator += basis * w * controlPoints[i];
    denominator += basis * w;
  }

  if (std::abs(denominator) > 1e-8f) {
    return numerator / denominator;
  }
  return glm::vec3(0.0f);
}

// 用 NURBS 精确表示四分之一圆
// 需要 3 个控制点和权重 [1, √2/2, 1]
// 节点向量 [0, 0, 0, 1, 1, 1]
void makeQuarterCircle(std::vector<glm::vec3>& cps,
                       std::vector<float>& weights,
                       std::vector<float>& knots) {
  cps = {
    glm::vec3(1, 0, 0),
    glm::vec3(1, 1, 0),
    glm::vec3(0, 1, 0)
  };
  weights = {1.0f, std::sqrt(2.0f) / 2.0f, 1.0f};
  knots = {0, 0, 0, 1, 1, 1};
}

2.4.2 NURBS 的优势

NURBS 成为 CAD/CAM 工业标准的原因:

统一表示:同时精确表示自由形状和解析形状(圆锥曲线)

局部控制:移动一个控制点只影响局部区域

几何不变性:对控制点做刚体变换等价于对曲线做同样的变换

数值稳定性:de Boor 算法(B 样条的求值算法)数值稳定

国际标准:STEP、IGES 等 CAD 数据交换标准都基于 NURBS

2.5 曲面表示

2.5.1 参数曲面

参数曲面是参数曲线的自然推广,用两个参数 (u, v) 定义:

S(u, v) = Σᵢ Σⱼ Nᵢ,ₚ(u) · Nⱼ,ₑ(v) · wᵢⱼ · Pᵢⱼ /
          Σᵢ Σⱼ Nᵢ,ₚ(u) · Nⱼ,ₑ(v) · wᵢⱼ
cpp
// NURBS 曲面求值
glm::vec3 nurbsSurface(
    const std::vector<std::vector<glm::vec3>>& controlPoints,
    const std::vector<std::vector<float>>& weights,
    int degreeU, int degreeV,
    const std::vector<float>& knotsU,
    const std::vector<float>& knotsV,
    float u, float v) {

  int nU = controlPoints.size();
  int nV = controlPoints[0].size();
  glm::vec3 numerator(0.0f);
  float denominator = 0.0f;

  for (int i = 0; i < nU; i++) {
    float basisU = basisFunction(i, degreeU, u, knotsU);
    for (int j = 0; j < nV; j++) {
      float basisV = basisFunction(j, degreeV, v, knotsV);
      float w = weights[i][j];
      float basis = basisU * basisV * w;
      numerator += basis * controlPoints[i][j];
      denominator += basis;
    }
  }

  return (denominator > 1e-8f) ? numerator / denominator : glm::vec3(0);
}

2.5.2 细分曲面

细分曲面(Subdivision Surface)是另一种重要的曲面表示方法,与参数曲面有本质区别。它通过递归地细分网格并调整顶点位置来生成光滑曲面。

细分曲面的优势:

  • 拓扑灵活性:可以表示任意拓扑的曲面,不需要参数曲面的矩形网格结构
  • 简单实现:细分规则简单直观
  • 任意细节:可以递归细分到任意精度
cpp
// Catmull-Clark 细分(简化版)
// 每个步骤:
// 1. 对每个面,计算面点(面的中心)
// 2. 对每条边,计算边点(相邻面点和边端点的加权平均)
// 3. 对每个顶点,计算新位置(相邻面点、边点和原顶点的加权平均)
// 4. 用新点构建更细的网格

struct Mesh {
  std::vector<glm::vec3> vertices;
  std::vector<std::vector<int>> faces;
};

// 一次 Catmull-Clark 细分的简化实现
Mesh catmullClarkSubdivide(const Mesh& input) {
  Mesh output;

  // 1. 计算面点
  std::vector<glm::vec3> facePoints;
  for (const auto& face : input.faces) {
    glm::vec3 center(0.0f);
    for (int idx : face) center += input.vertices[idx];
    center /= (float)face.size();
    facePoints.push_back(center);
  }

  // 2. 计算边点(简化:只处理四边形)
  std::map<std::pair<int,int>, int> edgeMidpoints;
  for (const auto& face : input.faces) {
    int n = face.size();
    for (int i = 0; i < n; i++) {
      int v0 = face[i], v1 = face[(i+1) % n];
      auto edge = std::make_pair(std::min(v0,v1), std::max(v0,v1));
      if (edgeMidpoints.find(edge) == edgeMidpoints.end()) {
        glm::vec3 mid = (input.vertices[v0] + input.vertices[v1]) * 0.5f;
        edgeMidpoints[edge] = output.vertices.size();
        output.vertices.push_back(mid);
      }
    }
  }

  // 3. 更新原顶点位置(简化版)
  for (const auto& v : input.vertices) {
    output.vertices.push_back(v);
  }

  // 4. 添加面点
  for (const auto& fp : facePoints) {
    output.vertices.push_back(fp);
  }

  // 5. 构建新的面(省略具体连接逻辑)
  // ...

  return output;
}

2.6 GPU 曲面细分

现代 GPU 支持硬件曲面细分,可以在渲染管线中实时求值参数曲面:

glsl
// OpenGL 曲面细分着色器示例

// 控制着色器(Tessellation Control Shader)
#version 450 core
layout(vertices = 4) out;
uniform float uTessLevel;

void main() {
  gl_TessLevelOuter[0] = uTessLevel;
  gl_TessLevelOuter[1] = uTessLevel;
  gl_TessLevelOuter[2] = uTessLevel;
  gl_TessLevelOuter[3] = uTessLevel;
  gl_TessLevelInner[0] = uTessLevel;
  gl_TessLevelInner[1] = uTessLevel;
  gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
}

// 求值着色器(Tessellation Evaluation Shader)
#version 450 core
layout(quads, equal_spacing, ccw) in;

void main() {
  float u = gl_TessCoord.x;
  float v = gl_TessCoord.y;

  // 双三次插值
  vec3 p00 = gl_in[0].gl_Position.xyz;
  vec3 p10 = gl_in[1].gl_Position.xyz;
  vec3 p01 = gl_in[2].gl_Position.xyz;
  vec3 p11 = gl_in[3].gl_Position.xyz;

  vec3 pos = (1-u)*(1-v)*p00 + u*(1-v)*p10 +
             (1-u)*v*p01 + u*v*p11;

  gl_Position = vec4(pos, 1.0);
}

三、实战案例

3.1 交互式贝塞尔曲线编辑器

以下代码实现一个交互式的贝塞尔曲线编辑器,支持拖拽控制点:

cpp
#include <vector>
#include <glm/glm.hpp>

class BezierEditor {
public:
  BezierEditor(int degree = 3) : degree(degree) {
    // 初始化默认控制点
    for (int i = 0; i <= degree; i++) {
      controlPoints.push_back(
        glm::vec2(i * 200.0f / degree, 300.0f - 100.0f * std::sin(i))
      );
    }
  }

  // 绘制曲线(高分辨率采样)
  std::vector<glm::vec2> evaluateCurve(int samples = 200) const {
    std::vector<glm::vec2> curvePoints;
    for (int i = 0; i <= samples; i++) {
      float t = (float)i / samples;
      curvePoints.push_back(evaluateAt(t));
    }
    return curvePoints;
  }

  // 在参数 t 处求值
  glm::vec2 evaluateAt(float t) const {
    std::vector<glm::vec2> pts = controlPoints;
    int n = pts.size();
    for (int r = 1; r < n; r++) {
      for (int i = 0; i < n - r; i++) {
        pts[i] = (1.0f - t) * pts[i] + t * pts[i + 1];
      }
    }
    return pts[0];
  }

  // 计算切线
  glm::vec2 evaluateTangent(float t) const {
    float dt = 0.001f;
    glm::vec2 p1 = evaluateAt(t - dt);
    glm::vec2 p2 = evaluateAt(t + dt);
    return glm::normalize(p2 - p1);
  }

  // 绘制 de Casteljau 辅助线(用于可视化)
  std::vector<std::vector<glm::vec2>> getDeCasteljauLevels(float t) const {
    std::vector<std::vector<glm::vec2>> levels;
    std::vector<glm::vec2> pts = controlPoints;
    levels.push_back(pts);

    int n = pts.size();
    for (int r = 1; r < n; r++) {
      std::vector<glm::vec2> newPts;
      for (int i = 0; i < n - r; i++) {
        newPts.push_back((1.0f - t) * pts[i] + t * pts[i + 1]);
      }
      levels.push_back(newPts);
      pts = newPts;
    }
    return levels;
  }

  // 拖拽控制点
  void moveControlPoint(int index, glm::vec2 newPos) {
    if (index >= 0 && index < controlPoints.size()) {
      controlPoints[index] = newPos;
    }
  }

  // 计算曲线长度(数值积分)
  float computeLength(int samples = 1000) const {
    float length = 0.0f;
    glm::vec2 prev = evaluateAt(0.0f);
    for (int i = 1; i <= samples; i++) {
      float t = (float)i / samples;
      glm::vec2 curr = evaluateAt(t);
      length += glm::length(curr - prev);
      prev = curr;
    }
    return length;
  }

  const std::vector<glm::vec2>& getControlPoints() const {
    return controlPoints;
  }

private:
  int degree;
  std::vector<glm::vec2> controlPoints;
};

3.2 B 样条曲线与贝塞尔曲线对比

cpp
// 对比贝塞尔曲线和 B 样条曲线的局部控制性
void compareLocalControl() {
  // 贝塞尔曲线:移动一个控制点影响整条曲线
  std::vector<glm::vec2> bezierCPs = {
    {0, 0}, {1, 3}, {3, 3}, {4, 0}, {5, 2}, {7, 2}, {8, 0}
  };
  // 移动 P3 从 {4,0} 到 {4,2}
  // 整条曲线的形状都会改变

  // B 样条曲线:移动一个控制点只影响局部
  std::vector<glm::vec2> bsplineCPs = bezierCPs;
  int degree = 3;
  auto knots = makeClampedKnots(bsplineCPs.size(), degree);

  // 移动 P3
  bsplineCPs[3] = {4, 2};
  // 只有 t 在 P3 影响范围内的部分会改变
  // 曲线两端保持不变
}

四、本章小结

本章系统讲解了曲线与曲面的数学理论和实践应用:

参数曲线用参数方程表示几何形状,参数化的选择影响曲线的质量和应用效果。

贝塞尔曲线是最基础的参数曲线,具有凸包性、端点插值、仿射不变性等优美性质,但存在全局控制的局限。

de Casteljau 算法通过递归线性插值计算贝塞尔曲线,数值稳定且支持曲线细分。

B 样条通过节点向量和局部基函数实现了局部控制,是复杂形状建模的首选工具。

NURBS 统一了 B 样条和有理表示,能够精确表示圆锥曲线,成为 CAD 工业标准。

细分曲面通过递归细分生成光滑曲面,具有拓扑灵活性,是影视动画建模的主流方法。

GPU 曲面细分将参数曲面的实时渲染变为可能,连接了离线设计和实时渲染两个世界。


五、思考与练习

思考题

证明贝塞尔曲线的凸包性:曲线上的每个点都在控制点的凸包内。

为什么三次贝塞尔曲线(4个控制点)是最常用的阶数?二次和四次有什么缺点?

给定一条 NURBS 曲线,如果将所有权重加倍,曲线形状会改变吗?为什么?

细分曲面的极限曲面是什么?Catmull-Clark 细分的极限曲面与双三次 B 样条曲面有什么关系?

为什么 TrueType 字体使用二次贝塞尔曲线,而 PostScript 使用三次贝塞尔曲线?各有什么优劣?

编程练习

贝塞尔曲线可视化:实现一个交互式程序,显示贝塞尔曲线、控制多边形和 de Casteljau 辅助线。支持拖拽控制点,实时更新曲线。

B 样条实现:实现 B 样条曲线的 Cox-de Boor 递推算法。对比均匀节点和 clamped 节点的效果差异。

曲线细分:实现 de Casteljau 细分算法,将一条贝塞尔曲线递归细分为线段,直到满足平坦度条件。


4/8曲线与曲
0%