贝塞尔曲线(面)二三维可视化(Three+d3)

贝塞尔曲线(面)二三维可视化(Three+d3)

在学完 games101 几何后开始实践,可视化贝塞尔曲线

我想实现三维的贝塞尔曲线,用 threejs,但是 threejs 控制太麻烦了,因此,我使用了 d3js 实现二维贝塞尔曲线的控制,threejs 实现三维贝塞尔曲线的可视化

展示一下二三维贝塞尔曲线的样子

展示3

展示1

功能一:重现二维和三维的贝塞尔曲线;功能二:可对二维贝塞尔曲线进行控制

理论基础

首先我们看三个控制点的贝塞尔曲线是如何画出来的

曲线1

b0, b1, b2 分别为三个控制点,设置一个自变量 t,t 的取值范围 [0, 1],0 对应一段线条的起始点,1 对应线段的终点。对于第一条线段 b0 b1,t=0 代表 b0 处,t=1 代表 b1 处。设 t 此时为定义域内的某个值,如 0.3,在 b0 b1,b1 b2,上分别取这个比例位置的点 b10,b11。连接 b10,b11,再取 0.3 的点 b20。最终得到的 b20 就是当 t=0.3 时点的位置。

令 t 从 0 变化到 1,就能够得到一个贝塞尔曲线。

控制点为四个时,取点方式原理一样

曲线2

也是设置一个变量 t,第一次选择 t 位置的点就能将四个控制点的情况转换为三个控制点的情况,如此,我们就能计算任意控制点的贝塞尔曲线了,因为了解了过程原理,最终的计算也就能够理解了,公式如下所示

公式1

可以看到,bi 前面的系数为 (1-t+t)^2 的展开式,总结规律得

\[b^n(t)=b^n_0(t)=\sum^n_{i=0} C^i_n \times t^i \times (1-t)^{n-i} \times b_i
\]

有了这个公式后就可以开始做实践了,在做贝塞尔曲面时需要两个自变量,u,v 替换 t,实质是将 u 取代了 t,然后再让 v 成为新的 t,再次遍历。这里以 16 个点为例,每四个点先做一次贝塞尔曲线转换

三维1

这里可以看到,每四个点都做了一次贝塞尔曲线的转换,再这个基础上,把得到的四个贝塞尔曲线上同 u 的点再次进行贝塞尔曲线的转换。

  1. u 从 0 到 1 ,得到了四条曲线
  2. 当 u 为某个值时,得到四条曲线上四个顶点作为控制点
  3. 以控制点为基础,v 从 0 到 1,得到一条曲线
  4. 遍历下去,由一条条曲线也就得到了类似曲面的图形

曲面1

代码实践

在代码中,最重要得是算法的重现,首先看三个控制点如何实现

// 求得 t 时刻点的位置
function getFinalPosition(t, controls, length){
    const n = length-1;
    const sum = {x:0, y:0};
    for (let i = 0; i <= n; i++) {
        const Bjn = getCombination(n, i)*Math.pow(t, i)*Math.pow(1-t, n-i);
        sum.x += controls[i].x * Bjn;
        sum.y += controls[i].y * Bjn;
    }
    return sum;
}

// 求阶乘
function getFactorial(n){
    let result = 1;
    for (let i = 2; i <= n; i++) {
        result *= i;
    }
    return result;
}

// 求组合 C
function getCombination(n, m){
    if(m === 0 || m === n){
        return 1;
    }
    return getFactorial(n)/(getFactorial(m)*getFactorial(n-m))
}

function fillPoints(){
    const resultPoints = [];
    for (let i = 0; i < NUMBER; i++) {
        const v1 = getFinalPosition(i/NUMBER, data, 4);
        resultPoints.push(v1);
    }
    path.attr('d', line(resultPoints))
}
fillPoints()

注意点

  • getFinalPosition 方法就是计算公式的转换
  • 求组合 C 的公式是 n!/(m!*(n-m)!),这里有加速的方法,比如C0n=Cnn
  • fillPoints 方法是将所有点收集起来,最后连成线

这块是使用了 d3js 进行展示,球和线均手动创建,由于是自定义的图形,因此使用 d3js 自由绘画,十分方便,画点和线的代码如下

// 点
const points = canvas.selectAll('.point')
    .data(data)
    .enter()
    .append('circle')
    .attr('cx', d=>d.x)
    .attr('cy', d=>d.y)
    .attr('r', d=>10)
    .attr('fill', '#f9d2bb')
    .call(
        d3.drag()
            .on("drag", (event, d)=>{
                d.x = event.x;
                d.y = event.y;
            })
            .on("end", ()=>{
                console.log(3)
            })
            .on('drag.update', ()=>{
                points.attr('cx', d=>d.x)
                    .attr('cy', d=>d.y);
                fillPoints();
            })
    )

const line = d3.line()
    .x(d => {
        return d.x})
    .y(d => d.y);

// 线
const path = canvas
    .append('path')
    .attr("stroke", "red")
    .attr("stroke-width", 3)
    .attr("fill", "none")
    .attr("stroke-opacity", 0.4);

给四个点添加拖动事件,每次拖动时重现计算线条

展示2

在三维情况下复杂一点,但是核心不变

const NUMBER = 1000;
for (let u = 0; u < NUMBER; u++) {
    const resultPoints = [];
    const resultColor = [];
    for (let v = 0; v < NUMBER; v++) {
        const acrossPoints = [];
        for (let i = 0; i < 4; i++) {
            const v1 = getFinalPosition(v/NUMBER, points[i], 4);
            acrossPoints.push(v1.clone());
        }
        const v2 = getFinalPosition(u/NUMBER, acrossPoints, 4);
        resultPoints.push(v2.clone());
        const color = new THREE.Color(
            u/NUMBER, 1-u/NUMBER, u/NUMBER)
        resultColor.push(color.r, color.g, color.b);
    }
    const lineGeometry = new THREE.BufferGeometry().setFromPoints(resultPoints);
    lineGeometry.setAttribute('color', new THREE.Float32BufferAttribute(resultColor, 3))
    const lineMaterial = new THREE.LineBasicMaterial({
        vertexColors: true
    })
    const line = new THREE.Line(lineGeometry, lineMaterial);
    scene.add(line)
}

注意点

  1. getFinalPosition 方法和二维类似,重现方程
  2. 将 u 和 v 进行循环,u 为外层,v 为里层,v 遍历后相当于产生了四条控制线;u 遍历后产生最终的点位
  3. 里面用到了一些 threejs 中的方法,如果感兴趣可以相互交流

总结

实现贝塞尔曲线最重要的是理解公式,实现公式的语言和可视化方法有很多种,我采用了 three 展示三维和 d3js 展示二维。代码太凌乱,有兴趣可以交流。

贝塞尔曲线目前采用了以线取代控制点的方式,但是由于时间关系,没有使用 d3js 实现这个功能,如果点赞够多,给个鼓励,我就有精力实现一下,😀

控制点1