Skip to content

第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 ●───────────────● P3

6.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) = 1

6.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)/2

6.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·h

6.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, P33

6.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))

关键要点

  1. 参数表示是图形学中曲线曲面的标准方法
  2. 贝塞尔曲线简单但受全局影响
  3. B-样条提供局部控制能力
  4. NURBS 是工业标准,可精确表示圆锥曲线
  5. 曲面是曲线向二维参数空间的扩展

下一章预告:在第7章中,我们将学习几何建模基础,包括多边形网格、细分曲面和布尔运算。


文档版本:v1.0
字数统计:约 12,000 字

如有转载或 CV 的请标注本站原文地址