第6章:曲线与曲面
6.1 参数曲线
6.1.1 曲线的表示方法
在计算机图形学中,曲线有三种主要的表示方法:显式、隐式和参数形式。
曲线的三种表示方法
1. 显式表示(Explicit)
y = f(x)
例如:y = x² + 2x + 1
优点:简单直观
缺点:无法表示垂直线,多值函数困难
2. 隐式表示(Implicit)
F(x, y) = 0
例如:x² + y² - r² = 0(圆)
优点:容易判断点在曲线内/外
缺点:难以沿曲线遍历
3. 参数表示(Parametric)★
x = x(t), y = y(t),t ∈ [0, 1]
例如:x = r·cos(t), y = r·sin(t)
优点:
- 可以表示任意复杂曲线
- 容易沿曲线遍历
- 便于交互设计
缺点:不唯一(同一曲线可有不同参数化)6.1.2 参数曲线的性质
参数曲线 P(t) = (x(t), y(t))
当 t 从 0 变化到 1 时,点 P(t) 沿曲线移动。
y
│
│ ●───────────● t=1
│ ╱
│ ╱
│ ● t=0.5
│ ╱
│ ╱
│ ● t=0
└─────────────────────► x
曲线的导数:
P'(t) = (x'(t), y'(t)) ← 切向量
切向量的方向是曲线在该点的切线方向
切向量的大小表示"速度"(参数变化时点移动的快慢)
示例:直线段
从点 A 到点 B 的直线段:
P(t) = (1-t)·A + t·B,t ∈ [0, 1]
当 t=0 时,P(0) = A
当 t=1 时,P(1) = B
当 t=0.5 时,P(0.5) = (A+B)/2(中点)6.1.3 代码实现
javascript
/**
* 参数曲线基类
*/
class ParametricCurve {
/**
* 计算曲线上 t 处的点
* @param t 参数,范围 [0, 1]
* @returns {x, y}
*/
evaluate(t) {
throw new Error('Must be implemented by subclass');
}
/**
* 计算 t 处的切向量(一阶导数)
*/
tangent(t) {
// 数值微分(默认实现)
const h = 0.0001;
const p1 = this.evaluate(t - h);
const p2 = this.evaluate(t + h);
return {
x: (p2.x - p1.x) / (2 * h),
y: (p2.y - p1.y) / (2 * h)
};
}
/**
* 计算 t 处的单位切向量
*/
unitTangent(t) {
const tan = this.tangent(t);
const len = Math.sqrt(tan.x * tan.x + tan.y * tan.y);
return { x: tan.x / len, y: tan.y / len };
}
/**
* 计算 t 处的法向量
*/
normal(t) {
const tan = this.unitTangent(t);
return { x: -tan.y, y: tan.x };
}
/**
* 将曲线离散化为点数组
* @param segments 分段数
*/
toPoints(segments = 100) {
const points = [];
for (let i = 0; i <= segments; i++) {
const t = i / segments;
points.push(this.evaluate(t));
}
return points;
}
/**
* 绘制曲线
*/
draw(ctx, segments = 100) {
const points = this.toPoints(segments);
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) {
ctx.lineTo(points[i].x, points[i].y);
}
ctx.stroke();
}
}
/**
* 直线段
*/
class LineSegment extends ParametricCurve {
constructor(start, end) {
super();
this.start = start;
this.end = end;
}
evaluate(t) {
return {
x: (1 - t) * this.start.x + t * this.end.x,
y: (1 - t) * this.start.y + t * this.end.y
};
}
tangent(t) {
return {
x: this.end.x - this.start.x,
y: this.end.y - this.start.y
};
}
}
/**
* 圆弧
*/
class CircleArc extends ParametricCurve {
constructor(center, radius, startAngle, endAngle) {
super();
this.center = center;
this.radius = radius;
this.startAngle = startAngle;
this.endAngle = endAngle;
}
evaluate(t) {
const angle = this.startAngle + t * (this.endAngle - this.startAngle);
return {
x: this.center.x + this.radius * Math.cos(angle),
y: this.center.y + this.radius * Math.sin(angle)
};
}
tangent(t) {
const angle = this.startAngle + t * (this.endAngle - this.startAngle);
const da = this.endAngle - this.startAngle;
return {
x: -this.radius * Math.sin(angle) * da,
y: this.radius * Math.cos(angle) * da
};
}
}6.2 贝塞尔曲线
6.2.1 贝塞尔曲线简介
贝塞尔曲线(Bézier Curve)是计算机图形学中最重要的曲线类型之一,由控制点定义,广泛用于字体设计、路径绘制和动画。
贝塞尔曲线的特点
1. 由控制点定义
2. 曲线经过首末控制点
3. 曲线在首末点的切线方向由相邻控制点决定
4. 曲线完全在控制点的凸包内
控制点示意(三次贝塞尔):
P1 ●─────────────● P2
╲ ╱
╲ ╱
╲ 曲线 ╱
╲ ╱
╲ ╱
P0 ●───────────────● P36.2.2 伯恩斯坦基函数
伯恩斯坦基函数(Bernstein Basis)
n 次贝塞尔曲线的基函数:
B_i,n(t) = C(n,i) · t^i · (1-t)^(n-i)
其中 C(n,i) = n! / (i! · (n-i)!) 是二项式系数
常用基函数:
一次(n=1):
B_0,1(t) = 1-t
B_1,1(t) = t
二次(n=2):
B_0,2(t) = (1-t)²
B_1,2(t) = 2t(1-t)
B_2,2(t) = t²
三次(n=3):
B_0,3(t) = (1-t)³
B_1,3(t) = 3t(1-t)²
B_2,3(t) = 3t²(1-t)
B_3,3(t) = t³
基函数的性质:
1. 非负:B_i,n(t) ≥ 0
2. 归一化:∑B_i,n(t) = 1
3. 端点:B_0,n(0) = 1, B_n,n(1) = 16.2.3 贝塞尔曲线公式
贝塞尔曲线方程
P(t) = ∑(i=0 to n) P_i · B_i,n(t)
一次贝塞尔曲线(直线):
P(t) = (1-t)P₀ + tP₁
二次贝塞尔曲线:
P(t) = (1-t)²P₀ + 2t(1-t)P₁ + t²P₂
三次贝塞尔曲线:
P(t) = (1-t)³P₀ + 3t(1-t)²P₁ + 3t²(1-t)P₂ + t³P₃
德卡斯特里奥算法(De Casteljau):
一种递归计算贝塞尔曲线点的方法:
P_i^(0) = P_i
P_i^(r) = (1-t)P_i^(r-1) + tP_(i+1)^(r-1)
最终 P_0^(n) 就是曲线上的点
图示(三次曲线,t=0.5):
P1 ●────M1────● P2
╲ ╲ ╱
╲ M4 M2
╲ ╲ ╱
M0╲╱M3
╲╱
P0 ●───●──────────● P3
P(0.5)
M0 = (P0+P1)/2
M1 = (P1+P2)/2
M2 = (P2+P3)/2
M3 = (M0+M1)/2
M4 = (M1+M2)/2
P(0.5) = (M3+M4)/26.2.4 贝塞尔曲线实现
javascript
/**
* 计算二项式系数 C(n, k)
*/
function binomial(n, k) {
if (k < 0 || k > n) return 0;
if (k === 0 || k === n) return 1;
let result = 1;
for (let i = 0; i < k; i++) {
result = result * (n - i) / (i + 1);
}
return result;
}
/**
* 伯恩斯坦基函数
*/
function bernstein(i, n, t) {
return binomial(n, i) * Math.pow(t, i) * Math.pow(1 - t, n - i);
}
/**
* 通用贝塞尔曲线
*/
class BezierCurve extends ParametricCurve {
constructor(controlPoints) {
super();
this.controlPoints = controlPoints;
this.degree = controlPoints.length - 1;
}
evaluate(t) {
const n = this.degree;
let x = 0, y = 0;
for (let i = 0; i <= n; i++) {
const basis = bernstein(i, n, t);
x += this.controlPoints[i].x * basis;
y += this.controlPoints[i].y * basis;
}
return { x, y };
}
/**
* 德卡斯特里奥算法
*/
evaluateDeCasteljau(t) {
let points = [...this.controlPoints];
while (points.length > 1) {
const newPoints = [];
for (let i = 0; i < points.length - 1; i++) {
newPoints.push({
x: (1 - t) * points[i].x + t * points[i + 1].x,
y: (1 - t) * points[i].y + t * points[i + 1].y
});
}
points = newPoints;
}
return points[0];
}
/**
* 计算导数曲线的控制点
*/
derivativeControlPoints() {
const n = this.degree;
const dPoints = [];
for (let i = 0; i < n; i++) {
dPoints.push({
x: n * (this.controlPoints[i + 1].x - this.controlPoints[i].x),
y: n * (this.controlPoints[i + 1].y - this.controlPoints[i].y)
});
}
return dPoints;
}
/**
* 计算切向量
*/
tangent(t) {
const dPoints = this.derivativeControlPoints();
const dCurve = new BezierCurve(dPoints);
return dCurve.evaluate(t);
}
/**
* 在 t 处分割曲线为两段
*/
split(t) {
const left = [];
const right = [];
let points = [...this.controlPoints];
left.push(points[0]);
right.unshift(points[points.length - 1]);
while (points.length > 1) {
const newPoints = [];
for (let i = 0; i < points.length - 1; i++) {
newPoints.push({
x: (1 - t) * points[i].x + t * points[i + 1].x,
y: (1 - t) * points[i].y + t * points[i + 1].y
});
}
left.push(newPoints[0]);
right.unshift(newPoints[newPoints.length - 1]);
points = newPoints;
}
return {
left: new BezierCurve(left),
right: new BezierCurve(right)
};
}
/**
* 计算曲线的包围盒
*/
boundingBox() {
// 简单方法:使用控制点的包围盒(实际包围盒更小)
let minX = Infinity, minY = Infinity;
let maxX = -Infinity, maxY = -Infinity;
for (const p of this.controlPoints) {
minX = Math.min(minX, p.x);
minY = Math.min(minY, p.y);
maxX = Math.max(maxX, p.x);
maxY = Math.max(maxY, p.y);
}
return { minX, minY, maxX, maxY };
}
}
/**
* 三次贝塞尔曲线(优化版本)
*/
class CubicBezier extends ParametricCurve {
constructor(p0, p1, p2, p3) {
super();
this.p0 = p0;
this.p1 = p1;
this.p2 = p2;
this.p3 = p3;
}
evaluate(t) {
const t2 = t * t;
const t3 = t2 * t;
const mt = 1 - t;
const mt2 = mt * mt;
const mt3 = mt2 * mt;
return {
x: mt3 * this.p0.x + 3 * mt2 * t * this.p1.x +
3 * mt * t2 * this.p2.x + t3 * this.p3.x,
y: mt3 * this.p0.y + 3 * mt2 * t * this.p1.y +
3 * mt * t2 * this.p2.y + t3 * this.p3.y
};
}
tangent(t) {
const t2 = t * t;
const mt = 1 - t;
const mt2 = mt * mt;
return {
x: 3 * mt2 * (this.p1.x - this.p0.x) +
6 * mt * t * (this.p2.x - this.p1.x) +
3 * t2 * (this.p3.x - this.p2.x),
y: 3 * mt2 * (this.p1.y - this.p0.y) +
6 * mt * t * (this.p2.y - this.p1.y) +
3 * t2 * (this.p3.y - this.p2.y)
};
}
}6.3 B-样条曲线
6.3.1 贝塞尔曲线的局限性
贝塞尔曲线的问题
1. 全局性:移动一个控制点会影响整条曲线
2. 阶数限制:控制点多时,曲线阶数高,计算复杂
3. 不便于局部编辑
B-样条的解决方案:
1. 局部性:每个控制点只影响曲线的一部分
2. 固定阶数:无论多少控制点,都可以使用低阶曲线
3. 便于局部编辑
对比:
贝塞尔曲线: B-样条曲线:
● ●
╱ ╲ ╱ ╲
●───● ●───●
↓ ↓
移动此点 移动此点
整条曲线都变 只有局部变化6.3.2 B-样条基函数
B-样条基函数(Cox-de Boor 递归公式)
N_i,0(t) = { 1, 如果 t_i ≤ t < t_(i+1)
{ 0, 否则
N_i,k(t) = ((t - t_i) / (t_(i+k) - t_i)) · N_i,k-1(t) +
((t_(i+k+1) - t) / (t_(i+k+1) - t_(i+1))) · N_(i+1),k-1(t)
其中 t_0, t_1, ..., t_m 是节点向量(knot vector)
k 是曲线的阶数(degree)
k=1:线性
k=2:二次
k=3:三次(最常用)
节点向量的例子:
均匀 B-样条(uniform):
t = [0, 1, 2, 3, 4, 5, 6, 7]
开放均匀(open uniform):
t = [0, 0, 0, 0, 1, 2, 3, 4, 4, 4, 4]
↑首末节点重复 k+1 次,使曲线经过首末控制点6.3.3 B-样条曲线公式
B-样条曲线
P(t) = ∑(i=0 to n) P_i · N_i,k(t)
其中:
- P_i 是控制点
- N_i,k(t) 是 k 阶 B-样条基函数
- n+1 是控制点数量
节点数量:
m = n + k + 1
例如:6 个控制点(n=5),三次曲线(k=3)
需要 m = 5 + 3 + 1 = 9 个节点6.3.4 B-样条实现
javascript
/**
* B-样条曲线
*/
class BSplineCurve extends ParametricCurve {
/**
* @param controlPoints 控制点数组
* @param degree 曲线阶数
* @param knots 节点向量(可选,默认使用开放均匀节点)
*/
constructor(controlPoints, degree = 3, knots = null) {
super();
this.controlPoints = controlPoints;
this.degree = degree;
this.knots = knots || this.generateOpenUniformKnots();
}
/**
* 生成开放均匀节点向量
*/
generateOpenUniformKnots() {
const n = this.controlPoints.length - 1;
const k = this.degree;
const m = n + k + 1;
const knots = [];
for (let i = 0; i <= m; i++) {
if (i <= k) {
knots.push(0);
} else if (i >= m - k) {
knots.push(1);
} else {
knots.push((i - k) / (m - 2 * k));
}
}
return knots;
}
/**
* 计算 B-样条基函数 N_i,p(t)
*/
basisFunction(i, p, t) {
const knots = this.knots;
if (p === 0) {
return (knots[i] <= t && t < knots[i + 1]) ? 1 : 0;
}
let left = 0, right = 0;
const denom1 = knots[i + p] - knots[i];
if (denom1 !== 0) {
left = ((t - knots[i]) / denom1) * this.basisFunction(i, p - 1, t);
}
const denom2 = knots[i + p + 1] - knots[i + 1];
if (denom2 !== 0) {
right = ((knots[i + p + 1] - t) / denom2) * this.basisFunction(i + 1, p - 1, t);
}
return left + right;
}
/**
* 计算曲线上的点
*/
evaluate(t) {
// 处理边界情况
if (t >= 1) t = 0.9999;
const n = this.controlPoints.length - 1;
let x = 0, y = 0;
for (let i = 0; i <= n; i++) {
const basis = this.basisFunction(i, this.degree, t);
x += this.controlPoints[i].x * basis;
y += this.controlPoints[i].y * basis;
}
return { x, y };
}
/**
* de Boor 算法(更高效的计算方法)
*/
evaluateDeBoor(t) {
const k = this.degree;
const knots = this.knots;
// 处理边界
if (t >= 1) t = 0.9999;
// 找到包含 t 的节点区间
let span = k;
while (span < knots.length - 1 - k && knots[span + 1] <= t) {
span++;
}
// 复制受影响的控制点
const d = [];
for (let i = 0; i <= k; i++) {
const idx = span - k + i;
d.push({ ...this.controlPoints[idx] });
}
// de Boor 递归
for (let r = 1; r <= k; r++) {
for (let j = k; j >= r; j--) {
const i = span - k + j;
const alpha = (t - knots[i]) / (knots[i + k + 1 - r] - knots[i]);
d[j].x = (1 - alpha) * d[j - 1].x + alpha * d[j].x;
d[j].y = (1 - alpha) * d[j - 1].y + alpha * d[j].y;
}
}
return d[k];
}
/**
* 插入节点
*/
insertKnot(t) {
const k = this.degree;
const knots = this.knots;
const points = this.controlPoints;
// 找到插入位置
let span = 0;
while (span < knots.length - 1 && knots[span + 1] <= t) {
span++;
}
// 计算新控制点
const newPoints = [];
for (let i = 0; i <= span - k; i++) {
newPoints.push({ ...points[i] });
}
for (let i = span - k + 1; i <= span; i++) {
const alpha = (t - knots[i]) / (knots[i + k] - knots[i]);
newPoints.push({
x: (1 - alpha) * points[i - 1].x + alpha * points[i].x,
y: (1 - alpha) * points[i - 1].y + alpha * points[i].y
});
}
for (let i = span; i < points.length; i++) {
newPoints.push({ ...points[i] });
}
// 更新节点向量
const newKnots = [...knots];
newKnots.splice(span + 1, 0, t);
this.controlPoints = newPoints;
this.knots = newKnots;
}
}6.4 NURBS
6.4.1 NURBS 简介
NURBS(Non-Uniform Rational B-Splines,非均匀有理B样条)是B-样条的扩展,增加了权重因子,可以精确表示圆、椭圆等二次曲线。
NURBS vs B-样条
B-样条:
P(t) = ∑ P_i · N_i,k(t)
NURBS:
∑ w_i · P_i · N_i,k(t)
P(t) = ─────────────────────────
∑ w_i · N_i,k(t)
其中 w_i 是每个控制点的权重
权重的作用:
- w_i > 1:曲线被"拉向"该控制点
- w_i = 1:标准 B-样条行为
- w_i < 1:曲线被"推离"该控制点
NURBS 的优势:
1. 可以精确表示圆锥曲线(圆、椭圆、抛物线、双曲线)
2. 在投影变换下保持有理性
3. 是工业标准(CAD/CAM)6.4.2 用 NURBS 表示圆
NURBS 圆弧
一个四分之一圆可以用二次 NURBS 表示:
控制点:P0 = (1, 0), P1 = (1, 1), P2 = (0, 1)
权重: w0 = 1, w1 = √2/2 ≈ 0.707, w2 = 1
P1 ●
╱ ╲
╱ ╲
╱ ╲
╱ 圆弧 ╲
P0 ●───────────● P2
完整圆需要至少 7 个控制点(或分成 4 段)
控制点(9点版本,方便计算):
(1,0), (1,1), (0,1), (-1,1), (-1,0), (-1,-1), (0,-1), (1,-1), (1,0)
权重:
1, w, 1, w, 1, w, 1, w, 1 (w = √2/2)6.4.3 NURBS 实现
javascript
/**
* NURBS 曲线
*/
class NURBSCurve extends ParametricCurve {
/**
* @param controlPoints 控制点数组
* @param weights 权重数组
* @param degree 曲线阶数
* @param knots 节点向量
*/
constructor(controlPoints, weights, degree = 3, knots = null) {
super();
this.controlPoints = controlPoints;
this.weights = weights || controlPoints.map(() => 1);
this.degree = degree;
this.knots = knots || this.generateOpenUniformKnots();
}
generateOpenUniformKnots() {
const n = this.controlPoints.length - 1;
const k = this.degree;
const m = n + k + 1;
const knots = [];
for (let i = 0; i <= m; i++) {
if (i <= k) knots.push(0);
else if (i >= m - k) knots.push(1);
else knots.push((i - k) / (m - 2 * k));
}
return knots;
}
/**
* B-样条基函数
*/
basisFunction(i, p, t) {
const knots = this.knots;
if (p === 0) {
return (knots[i] <= t && t < knots[i + 1]) ? 1 : 0;
}
let left = 0, right = 0;
const d1 = knots[i + p] - knots[i];
if (d1 !== 0) {
left = ((t - knots[i]) / d1) * this.basisFunction(i, p - 1, t);
}
const d2 = knots[i + p + 1] - knots[i + 1];
if (d2 !== 0) {
right = ((knots[i + p + 1] - t) / d2) * this.basisFunction(i + 1, p - 1, t);
}
return left + right;
}
/**
* 计算 NURBS 曲线上的点
*/
evaluate(t) {
if (t >= 1) t = 0.9999;
const n = this.controlPoints.length - 1;
let sumX = 0, sumY = 0, sumW = 0;
for (let i = 0; i <= n; i++) {
const basis = this.basisFunction(i, this.degree, t);
const wBasis = this.weights[i] * basis;
sumX += this.controlPoints[i].x * wBasis;
sumY += this.controlPoints[i].y * wBasis;
sumW += wBasis;
}
return {
x: sumX / sumW,
y: sumY / sumW
};
}
/**
* 创建圆弧(四分之一圆)
*/
static quarterCircle(center, radius, startAngle = 0) {
const w = Math.SQRT2 / 2; // √2/2
const c = Math.cos(startAngle);
const s = Math.sin(startAngle);
const p0 = {
x: center.x + radius * c,
y: center.y + radius * s
};
const p2 = {
x: center.x + radius * Math.cos(startAngle + Math.PI / 2),
y: center.y + radius * Math.sin(startAngle + Math.PI / 2)
};
const p1 = {
x: center.x + radius * (c + Math.cos(startAngle + Math.PI / 2)),
y: center.y + radius * (s + Math.sin(startAngle + Math.PI / 2))
};
return new NURBSCurve(
[p0, p1, p2],
[1, w, 1],
2,
[0, 0, 0, 1, 1, 1]
);
}
/**
* 创建完整圆
*/
static circle(center, radius) {
const w = Math.SQRT2 / 2;
const r = radius;
const cx = center.x;
const cy = center.y;
const controlPoints = [
{ x: cx + r, y: cy },
{ x: cx + r, y: cy + r },
{ x: cx, y: cy + r },
{ x: cx - r, y: cy + r },
{ x: cx - r, y: cy },
{ x: cx - r, y: cy - r },
{ x: cx, y: cy - r },
{ x: cx + r, y: cy - r },
{ x: cx + r, y: cy }
];
const weights = [1, w, 1, w, 1, w, 1, w, 1];
const knots = [0, 0, 0, 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 1, 1, 1];
return new NURBSCurve(controlPoints, weights, 2, knots);
}
}6.5 曲线拟合
6.5.1 插值与拟合
插值 vs 拟合
插值(Interpolation):
- 曲线必须经过所有给定点
- 点越多,曲线可能越不平滑
拟合(Approximation):
- 曲线尽量接近给定点,但不必经过
- 可以过滤噪声,保持平滑
常见方法:
1. 分段线性插值
2. 拉格朗日插值
3. 三次样条插值 ★
4. 最小二乘拟合6.5.2 三次样条插值
javascript
/**
* 三次样条插值
*/
class CubicSplineInterpolation {
/**
* @param points 数据点数组 [{x, y}, ...],按 x 排序
*/
constructor(points) {
this.points = points;
this.n = points.length - 1;
this.coefficients = this.computeCoefficients();
}
/**
* 计算样条系数
* 自然边界条件:两端点的二阶导数为 0
*/
computeCoefficients() {
const n = this.n;
const points = this.points;
// 计算 h[i] = x[i+1] - x[i]
const h = [];
for (let i = 0; i < n; i++) {
h.push(points[i + 1].x - points[i].x);
}
// 构建三对角矩阵并求解
// 自然样条的方程组
const alpha = [0];
for (let i = 1; i < n; i++) {
alpha.push(
(3 / h[i]) * (points[i + 1].y - points[i].y) -
(3 / h[i - 1]) * (points[i].y - points[i - 1].y)
);
}
// 求解三对角方程组
const l = [1];
const mu = [0];
const z = [0];
for (let i = 1; i < n; i++) {
l.push(2 * (points[i + 1].x - points[i - 1].x) - h[i - 1] * mu[i - 1]);
mu.push(h[i] / l[i]);
z.push((alpha[i] - h[i - 1] * z[i - 1]) / l[i]);
}
l.push(1);
z.push(0);
// 回代求解
const c = new Array(n + 1).fill(0);
const b = new Array(n).fill(0);
const d = new Array(n).fill(0);
for (let j = n - 1; j >= 0; j--) {
c[j] = z[j] - mu[j] * c[j + 1];
b[j] = (points[j + 1].y - points[j].y) / h[j] -
h[j] * (c[j + 1] + 2 * c[j]) / 3;
d[j] = (c[j + 1] - c[j]) / (3 * h[j]);
}
// 存储系数
const coeffs = [];
for (let i = 0; i < n; i++) {
coeffs.push({
a: points[i].y,
b: b[i],
c: c[i],
d: d[i],
x: points[i].x
});
}
return coeffs;
}
/**
* 计算插值
*/
evaluate(x) {
const coeffs = this.coefficients;
const points = this.points;
// 找到包含 x 的区间
let i = 0;
while (i < this.n - 1 && x > points[i + 1].x) {
i++;
}
const { a, b, c, d, x: xi } = coeffs[i];
const dx = x - xi;
return a + b * dx + c * dx * dx + d * dx * dx * dx;
}
/**
* 生成平滑曲线点
*/
toPoints(resolution = 100) {
const points = [];
const xMin = this.points[0].x;
const xMax = this.points[this.n].x;
for (let i = 0; i <= resolution; i++) {
const x = xMin + (xMax - xMin) * i / resolution;
points.push({ x, y: this.evaluate(x) });
}
return points;
}
}6.6 参数曲面
6.6.1 曲面的参数表示
参数曲面
曲面由两个参数 (u, v) 定义:
P(u, v) = (x(u,v), y(u,v), z(u,v))
其中 u, v ∈ [0, 1]
常见参数曲面:
平面:
P(u, v) = P0 + u·V1 + v·V2
球面:
x = r·sin(θ)·cos(φ)
y = r·sin(θ)·sin(φ)
z = r·cos(θ)
其中 θ = π·u, φ = 2π·v
圆柱面:
x = r·cos(2πu)
y = r·sin(2πu)
z = v·h6.6.2 贝塞尔曲面
贝塞尔曲面
将贝塞尔曲线扩展到二维参数空间:
P(u, v) = ∑∑ P_ij · B_i,m(u) · B_j,n(v)
双三次贝塞尔曲面:
4×4 = 16 个控制点
P03 ●────────●────────●────────● P33
╱ ╱ ╱ ╱
P02 ●────────●────────●────────● P32
╱ ╱ ╱ ╱
P01 ●────────●────────●────────● P31
╱ ╱ ╱ ╱
P00 ●────────●────────●────────● P30
曲面经过四个角点:P00, P30, P03, P336.6.3 贝塞尔曲面实现
javascript
/**
* 双三次贝塞尔曲面
*/
class BicubicBezierSurface {
/**
* @param controlPoints 4x4 控制点网格
*/
constructor(controlPoints) {
this.controlPoints = controlPoints;
}
/**
* 计算曲面上的点
*/
evaluate(u, v) {
const points = this.controlPoints;
// 先沿 u 方向插值得到 4 个点
const uPoints = [];
for (let j = 0; j < 4; j++) {
const row = [points[0][j], points[1][j], points[2][j], points[3][j]];
const curve = new CubicBezier(row[0], row[1], row[2], row[3]);
uPoints.push(curve.evaluate(u));
}
// 再沿 v 方向插值
const curve = new CubicBezier(uPoints[0], uPoints[1], uPoints[2], uPoints[3]);
return curve.evaluate(v);
}
/**
* 计算法向量
*/
normal(u, v) {
const h = 0.001;
const p = this.evaluate(u, v);
const pu = this.evaluate(u + h, v);
const pv = this.evaluate(u, v + h);
// 偏导数
const du = {
x: (pu.x - p.x) / h,
y: (pu.y - p.y) / h,
z: (pu.z - p.z) / h
};
const dv = {
x: (pv.x - p.x) / h,
y: (pv.y - p.y) / h,
z: (pv.z - p.z) / h
};
// 叉积得到法向量
const normal = {
x: du.y * dv.z - du.z * dv.y,
y: du.z * dv.x - du.x * dv.z,
z: du.x * dv.y - du.y * dv.x
};
// 归一化
const len = Math.sqrt(
normal.x * normal.x +
normal.y * normal.y +
normal.z * normal.z
);
return {
x: normal.x / len,
y: normal.y / len,
z: normal.z / len
};
}
/**
* 生成三角网格
*/
toMesh(uSegments = 10, vSegments = 10) {
const vertices = [];
const normals = [];
const indices = [];
// 生成顶点
for (let j = 0; j <= vSegments; j++) {
const v = j / vSegments;
for (let i = 0; i <= uSegments; i++) {
const u = i / uSegments;
vertices.push(this.evaluate(u, v));
normals.push(this.normal(u, v));
}
}
// 生成三角形索引
for (let j = 0; j < vSegments; j++) {
for (let i = 0; i < uSegments; i++) {
const idx = j * (uSegments + 1) + i;
// 两个三角形组成一个四边形
indices.push(idx, idx + 1, idx + uSegments + 1);
indices.push(idx + 1, idx + uSegments + 2, idx + uSegments + 1);
}
}
return { vertices, normals, indices };
}
}6.7 本章小结
曲线类型对比
| 类型 | 控制点数 | 特点 | 应用场景 |
|---|---|---|---|
| 贝塞尔 | n+1 | 简单、全局影响 | 字体、简单路径 |
| B-样条 | 任意 | 局部控制 | 复杂曲线 |
| NURBS | 任意 | 可表示圆锥曲线 | CAD/CAM |
关键公式
贝塞尔曲线:
P(t) = ∑ P_i · B_i,n(t)
B-样条基函数(Cox-de Boor):
N_i,k(t) = ((t-t_i)/(t_{i+k}-t_i)) · N_{i,k-1}(t) +
((t_{i+k+1}-t)/(t_{i+k+1}-t_{i+1})) · N_{i+1,k-1}(t)
NURBS:
P(t) = (∑ w_i · P_i · N_i,k(t)) / (∑ w_i · N_i,k(t))关键要点
- 参数表示是图形学中曲线曲面的标准方法
- 贝塞尔曲线简单但受全局影响
- B-样条提供局部控制能力
- NURBS 是工业标准,可精确表示圆锥曲线
- 曲面是曲线向二维参数空间的扩展
下一章预告:在第7章中,我们将学习几何建模基础,包括多边形网格、细分曲面和布尔运算。
文档版本:v1.0
字数统计:约 12,000 字
