第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ⁱ// 三次多项式曲线
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 参数化的选择
同一条几何曲线可以用不同的参数化方式表示,参数化的选择影响动画和插值的质量:
- 均匀参数化:参数等间距分布,简单但可能导致速度不均匀
- 弦长参数化:参数间距与弦长成比例,通常效果最好
- 向心参数化:参数间距与弦长的平方根成比例,避免尖角和自交
// 弦长参数化
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)!) 是二项式系数// 伯恩斯坦基函数
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)// 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 贝塞尔曲线的性质
贝塞尔曲线具有以下重要性质:
端点插值:曲线经过第一个和最后一个控制点
端点切线:曲线在端点处的切线方向指向相邻的控制点
凸包性:曲线完全包含在控制点的凸包内
仿射不变性:对控制点做仿射变换等价于对曲线做仿射变换
变差缩减性:曲线比控制多边形更"平滑"
// 常用贝塞尔曲线类型
// 一次(线性):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)// 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:完全断开// 生成 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 样条为什么需要"有理"表示?因为多项式曲线无法精确表示圆锥曲线(圆、椭圆、抛物线、双曲线)。例如,用多项式曲线只能近似表示圆,而用有理曲线可以精确表示。
// 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ᵢⱼ// 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)是另一种重要的曲面表示方法,与参数曲面有本质区别。它通过递归地细分网格并调整顶点位置来生成光滑曲面。
细分曲面的优势:
- 拓扑灵活性:可以表示任意拓扑的曲面,不需要参数曲面的矩形网格结构
- 简单实现:细分规则简单直观
- 任意细节:可以递归细分到任意精度
// 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 支持硬件曲面细分,可以在渲染管线中实时求值参数曲面:
// 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 交互式贝塞尔曲线编辑器
以下代码实现一个交互式的贝塞尔曲线编辑器,支持拖拽控制点:
#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 样条曲线与贝塞尔曲线对比
// 对比贝塞尔曲线和 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 细分算法,将一条贝塞尔曲线递归细分为线段,直到满足平坦度条件。