贝塞尔曲线(面)二三维可视化(Three+d3)
在学完 games101 几何后开始实践,可视化贝塞尔曲线
我想实现三维的贝塞尔曲线,用 threejs,但是 threejs 控制太麻烦了,因此,我使用了 d3js 实现二维贝塞尔曲线的控制,threejs 实现三维贝塞尔曲线的可视化
展示一下二三维贝塞尔曲线的样子
功能一:重现二维和三维的贝塞尔曲线;功能二:可对二维贝塞尔曲线进行控制
理论基础
首先我们看三个控制点的贝塞尔曲线是如何画出来的
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,就能够得到一个贝塞尔曲线。
控制点为四个时,取点方式原理一样
也是设置一个变量 t,第一次选择 t 位置的点就能将四个控制点的情况转换为三个控制点的情况,如此,我们就能计算任意控制点的贝塞尔曲线了,因为了解了过程原理,最终的计算也就能够理解了,公式如下所示
可以看到,bi 前面的系数为 (1-t+t)^2 的展开式,总结规律得
有了这个公式后就可以开始做实践了,在做贝塞尔曲面时需要两个自变量,u,v 替换 t,实质是将 u 取代了 t,然后再让 v 成为新的 t,再次遍历。这里以 16 个点为例,每四个点先做一次贝塞尔曲线转换
这里可以看到,每四个点都做了一次贝塞尔曲线的转换,再这个基础上,把得到的四个贝塞尔曲线上同 u 的点再次进行贝塞尔曲线的转换。
- u 从 0 到 1 ,得到了四条曲线
- 当 u 为某个值时,得到四条曲线上四个顶点作为控制点
- 以控制点为基础,v 从 0 到 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);
给四个点添加拖动事件,每次拖动时重现计算线条
在三维情况下复杂一点,但是核心不变
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)
}
注意点
- getFinalPosition 方法和二维类似,重现方程
- 将 u 和 v 进行循环,u 为外层,v 为里层,v 遍历后相当于产生了四条控制线;u 遍历后产生最终的点位
- 里面用到了一些 threejs 中的方法,如果感兴趣可以相互交流
总结
实现贝塞尔曲线最重要的是理解公式,实现公式的语言和可视化方法有很多种,我采用了 three 展示三维和 d3js 展示二维。代码太凌乱,有兴趣可以交流。
贝塞尔曲线目前采用了以线取代控制点的方式,但是由于时间关系,没有使用 d3js 实现这个功能,如果点赞够多,给个鼓励,我就有精力实现一下,