这是一个项目中遇到的实际需求。

场景是一个智能仓库管理系统,场景里面有直线和曲线构成的环穿轨道。环穿轨道上面会有小车运动,后台推动小车的两个点位A和B,其中A和B都会在轨道上面,前端需要根据这两个推送点,自动播放小车从A点沿轨道到B点的动画。下面是项目截图:

项目截图

项目中使用的是二次贝塞尔曲线,所以本文也主要以二次贝塞尔曲线为讲解重点。

要实现上述动画,需要首先确定A点和B点在曲线上面的比例值ta和tb

最终的需求变成:“根据贝塞尔曲线上的点反算t值”。 大概有以下几种方法。现假设贝塞尔曲线上的点为点P(后续会用到该点)。

分片迭代

分片迭代是一种近似的方法。我们知道,二次贝塞尔曲线的公式如下:

B(t) = (1-t)2 * P0 + 2t(1-t) * P1 + t2 * P2

其中:

[0,1],P0为二次贝塞尔曲线的起始点,P1为控制点,P2为终止点。

如果你对于上面的知识点不是很熟悉,建议学习贝塞尔曲线相关知识。推荐学习本人的专栏Canvas高级进阶, 里面有专门的章节对贝塞尔曲线进行了全面详细的讲解。本文也是从该专栏的文章中摘录并适当改编而成的。

从以上公式,我们可以得到,对于任意给定的比例值t,可以求出对应该比例值的点B(t)。分片迭代思路是:现在加设把范围[0,1]平均分成N(比如100)等份,形成一系列的比例值t,对于每一个t值,求取对应的点B(t) ,然后让点B(t)和已知在贝塞尔曲线上的点P进行比较,如果点B(t)和点P之间的直线距离在一定的误差范围之内,则认为B(t)等于P,而此时的t值,就是我们要求的t值。

以下是主要代码:

function computeT(p0,p1,p2,p) {

var t = 0;

for(var i = 0;i < 1000;i ++){

var point = getPointOnQuadraticCurve(p0,p1,p2,t);//根据二次贝塞尔曲线公式求B(t),其中point = B(t)

if(distance(point,p) < 0.01){ // 判断point和p点的距离是否在特定误差之内

return t;

}

t+= 0.001;

}

return null;

}

上述分片迭代的方法,思路最简单,最直观。在精度要求不高的情况下是可以满足的。而在精度要求高的时候,即代码中的“特定误差”值要很小,可能会出现函数返回值为null的情况,在精度要求高的时候要能够计算出值,就要增加迭代次数,此时会极大增加性能消耗。比如上面代码的迭代次数可能会变成10000甚至10000。

迭代方法同样适用于三次贝塞尔曲线和更加高阶的贝塞尔曲线。

分片迭代优化版本

上面提到在精度要求高的情况下,要得到正确结果,要极大的增加迭代次数,造成性能的极大消耗。 有没有办法既提高精度,又不大量增加迭代次数呢? 经过笔者的思考,发现是可以的。想想假设要求的t值在0.5附近,那么我们只需要在0.5附近加大分片的数量,而不需要在其他地方(0.10.4,0.61.0)增加分片的数量。 应此升级版本的思路就是,先用比较粗的分片初步确定t值的一个大致范围,再在该范围之类,比较细的分片确定t值。注意这是个递归的过程,如果在第二次比较细的分片情况下,仍然不能确定t值,那么就确定一个t值的更小分范围;重复上面过程,直到找到t值为止。

大致步骤如下:

首先,通过一个小的迭代次数进行分片迭代;

在迭代的过程中如果找到了符合的比例值t,直接返回;

在迭代的过程中同时记录离目标点P最近的t值,如果上一步未找到符合的t值,则进行下一步操作。

上一步找到了离目标点P最近的t值,在t值的附近(t - step,t + step)(其中step为上一次分片的步进值)进行分片迭代查找,在迭代的过程中如果找到了符合的比例值t,直接返回。

如果没找到,重复上面的不断缩小范围并加大分片精度的过程。 直到找到t值为止。

下面是示例代码:

function computeT(p0, p1, p2, p,startT = 0,endT = 1) {

var t = startT;

var minDistance = Infinity,

minDistanceT = null;

var step = (endT - startT) / 100;

for (var i = 0; i < 100; i++) {

var point = getPointOnQuadraticCurve(p0, p1, p2, t);

var dst = distance(point,p);

if (dst < minDistance) {

minDistance = dst;

minDistanceT = t;

}

if (dst < 0.0001) {

return t;

}

t += step;

}

return computeT(p0, p1, p2, p, minDistanceT - step,minDistanceT + step);

}

以上过程虽然增加了一定的迭代次数,但是是常量级别的增加,而非数量级别的增加,所以会极大提高性能。 比如目标t值在0.5附近,第一次通过100次迭代可以确定t值的范围在0.4 ~ 0.6之间;然后进行第二次迭代,第二次迭代此次数仍然为100次,假设确定t值的范围在0.51 ~ 0.53之间;然后进行第三次迭代,第三次迭代此次数仍然为100次,此时可以获取t值为0.516,可以看出最多值迭代了300次。 假设总共经过第N次迭代,每次迭代次数为M,才找到t值,那么总共的迭代次数是N * M。

该迭代方法同样适用于三次贝塞尔曲线和更加高阶的贝塞尔曲线。而且相对于未优化的版本,该方法的性能好了很多。是适合所有贝塞尔曲线的比较好的反算t值的方法。

二分法

二分法的思路是:

首先确定一个起始t值和结束t值t0和t1,初始值t0 = 0,t1 = 1。

取t0和t1的中间值tm = (t0+t1)/2

通过tm计算出点Pm,如果Pm和目标点P之间的距离在误差值范围之内,则tm为需要计算的目标t值。

如果上一步Pm和目标点P之间的距离不在误差值范围之内,则判断Pm和目标点P的前后顺序,如果Pm在目标点P的前面,则把tm赋值给t1;否则把tm赋值给t0。

重复以上步骤直到找到合适的tm值。

上述步骤有一个难点: 如何判断Pm和目标点P的前后顺序?

对于二次贝塞尔曲线,如下图所示:

二次贝塞尔曲线

其中,P0为起始点,P2为终止点,P1为控制点。 二次贝塞尔曲线有如下特点:

线段(P1,P0)、(P1,P2)和曲线相切,这也就意味着曲线一定在三角形(P0,P1,P2)之内,而且二次贝塞尔曲线本身不会自身相交,所有我们可以有如下结论,

对于曲线上面的点A,直线(P1,A)和线段(P0,P1)相交于点a;对于曲线上面的点B,直线(P1,B)和线段(P0,P1)相交于点b。点A和点B的先后顺序与点a和点b的先后顺序是一致的,而直线上面的点(a和b)的前后顺序是容易判断的。 也就是说如果点a在点b的前面,则点A也在点B的前面,反之亦然。如下图所示:

判断先后顺序

有了以上的结论,我们就找到了判断Pm和目标点P的前后顺序的方法。

如果你对上述结论不熟悉,建议学习贝塞尔曲线的相关知识,推荐学习本人的专栏Canvas高级进阶, 里面有专门的章节对贝塞尔曲线进行了全面详细的讲解。本文也是从该专栏的文章中摘录并适当改编而成的。

有了这个方法,加上前面描述的二分查找的步骤,可以得出示例代码如下:

function computeT2(p0,p1,p2,p,startT = 0,endT = 1) {

var halfT = (startT + endT) / 2;

var halfPoint = getPointOnQuadraticCurve(p0,p1,p2,halfT);

if(distance(halfPoint,p) < 0.0001){

return halfT;

}

//求交点:

var inter1 = segmentsIntr(p0,p2,p1,p);

var inter2 = segmentsIntr(p0,p2,p1,halfPoint);

var r1 = interpolationRate(p0,inter1,p2),

r2 = interpolationRate(p0,inter2,p2);

if(r1 > r2){

startT = halfT;

}else {

endT = halfT;

}

return computeT2(p0,p1,p2,p,startT,endT);

}

解方程

前面说过,贝塞尔曲线的公式如下:

B(t) = (1-t)2 * P0 + 2t(1-t) * P1 + t2 * P2

其中:

[0,1],P0为二次贝塞尔曲线的起始点,P1为控制点,P2为终止点。

分别表示成x和y的方程,则可以表示如下:

xP = (1-t)2 * xP0 + 2t(1-t) * xP1 + t2 * xP2

yP = (1-t)2 * yP0 + 2t(1-t) * yP1 + t2 * yP2

实际上就是两个变量t的二次元方程,取上面任意一个方程,带入相关的值解方程,方程的解即为我们要求的目标t值。

整理方程: xP = (1-t)2 * xP0 + 2t(1-t) * xP1 + t2 * xP2,可以得出二次方程如下:

(xP2 + xP0 - 2 * xP1 ) * t2 + 2(xP1 - xP0) * t + (xP0 - xP) = 0。

我们已知二次方程的: at2 + b * t + c = 0的解为:

如果a = 0,则解为 -c/b

如果a != 0,解如下图所示:

方程的解

应此令:

a = (xP2 + xP0 - 2 * xP1 )

b = 2*(xP1 - xP0)

c = (xP0 - xP)

可以方便求出方程的解。

需要注意的是,二次方程的解可能会有两个。如果求出的解有两个怎么办呢。 首先我们知道贝塞尔曲线的t值的范围是

[0,1],所以如果有两个解:

其中一个不再[0,1]的范围之内,则另外一个解就是目标t值。(注意不可能两个都不在[0,1]范围之内,因为我们知道,目标点P在贝塞尔曲线上面)。

如果两个解都在[0,1]范围之内,那就把两个解再带入贝塞尔曲线的公式,分别求出两个B(t)点,那个离目标点P近,就取那个解。

下面是示例代码,其中函数equation2用于解曲线的方程:

function computeT(p0,p1,p2,p) {

let interpolationx = (p1.x - p0.x) / (p2.x - p0.x);

let tt;

if(interpolationx >= 0 && interpolationx <= 1){

let ty = equation2(p0.y,p1.y,p2.y,p.y);

return ty;

}else{

tt = equation2(p0.x,p1.x,p2.x,p.x);

if(tt.tt1){

var pointTest = getPointOnQuadraticCurve(p0,p1,p2,tt.tt1);

if(distance(pointTest,p) < 0.01){

return tt.tt1;

}else{

return tt.tt2;

}

}else{

return tt;

}

}

}

function equation2(z0,z1,z2,zp){ // z0、z1,z2代表P0、P1、P2的x坐标值或者y坐标值,zp代表目标点P的x坐标值或者y坐标值

var a = z0 - z1 * 2 + z2,

b = 2*(z1 - z0),

c = z0 - zp;

var tt = null;

if(a == 0 && b != 0){

tt = - c / b;

} else {

var sq = Math.sqrt( b * b - 4 * a * c );

var tt1 = (sq - b)/ (2 * a),

tt2 = (-sq - b) / (2 * a);

// console.log("tt1,tt2:",tt1,tt2);

if((tt1 <= 1 && tt1>= 0) && (tt2 <= 1 && tt2>= 0)){

return {tt1,tt2};

}else if(tt1 <= 1 && tt1>= 0){

tt = tt1;

}else {

tt = tt2;

}

}

return tt;

}

几种方法的比较

从性能方面来说:

解方程的方式是最快的

二分法和分片迭代的优化版次之

原始的分片迭代方法最差

从通用性来说,分片迭代的方式是适合任意阶的贝塞尔曲线。但是考虑到性能问题所以分片迭代的优化版是通用性最好的求解方法。

python 贝塞尔曲线 反算控制点_根据贝塞尔曲线上的点反算t值相关推荐

  1. python输入圆的半径公式_[图文]铁路曲线正矢的计算公式

    一.圆曲线正矢的计算 1.1 圆曲线正矢的计算公式 取圆曲线上两点拉一直线,叫做弦.弦上任意点至曲线上的垂直距离叫矢或叫矢距.在弦中央点的矢距叫正矢(下图). AB一弦;AC.CB一半弦;CD一正矢; ...

  2. css贝塞尔曲线 多个点_了解贝塞尔曲线的数学和Python实现示例

    贝塞尔曲线在计算机图形学中被大量使用,通常可以产生平滑的曲线.如果您曾经使用过Photoshop,则可能会发现名为"锚点"的工具,您可以在其中放置锚点并用它们绘制一些曲线,这些也是 ...

  3. python爬虫反爬机制_浅谈爬虫及绕过网站反爬取机制之Python深度应用

    我们中公优就业的老师希望能给那些面临困境的朋友们带来一点帮助!(相关阅读推荐:Python学习就看这里!) 爬虫是什么呢,简单而片面的说,爬虫就是由计算机自动与服务器交互获取数据的工具.爬虫的最基本就 ...

  4. python的使用涉及侵权吗_只要不用于商业用途就不算侵权吗

    展开全部 很有可能是侵权,但是仅仅依据是e69da5e887aa3231313335323631343130323136353331333365663434否用于商业用途难以判断是否侵权,只有满足著作 ...

  5. python真是最烂的语言_在大型项目上,Python 是个烂语言吗?

    展开全部 是存在的东西就不能用烂来形容,也许只是不对某些人的爱.e68a84e8a2ad3231313335323631343130323136353331333363396464 用 Boost 去 ...

  6. 硬件断点反跳似乎_高性能应用程序:多路复用,反跳,系统字体和其他技巧

    硬件断点反跳似乎 by Atila Fassina 通过阿蒂拉·法西纳(Atila Fassina) 高性能应用程序:多路复用,反跳,系统字体和其他技巧 (High Performance Apps: ...

  7. telnet怎么算成功_有机肥发酵剂有的作用,怎么才算发酵成功?

    有机肥发酵剂即有机物料腐熟剂,能够分解蛋白质.纤维素.半纤维素.木质素等,并将细菌.真菌等复合而成,有机肥发酵剂有效活菌数含量高,降解能力强,同时能够达到升温.除臭.消除病虫害.杂草种子和提高养分的效 ...

  8. 对坐标的曲线积分求做功_对坐标的曲线积分对弧长的曲线积分 二重积分

    高数:对坐标的曲线积分这题怎么写? 再问:可是答案是4a^2啊再问:奇怪再答:我感觉应该是你的答案错了吧,我找不出我哪里不对.再问:恩恩,那请问逆时针和顺时针区别在哪呢再答:如果是顺时针,那么用格林公 ...

  9. 如何编制试算平衡表_在实际工作中,余额试算平衡通过编制试算平衡表进行。()...

    参考答案如下 实作中制试衡量可兴奋组织兴奋性常用的指标是: 际工进行"税金及附加"账户用来反映企业应交税费的增加数. 额试肿瘤性增生与炎性增生的不同在于瘤细胞: 算平算平已知摄氏温 ...

  10. java 反查域名_爬虫实现:根据IP地址反查域名

    域名解析与IP地址 域名解析是把域名指向网站空间IP,让人们通过注册的域名可以方便地访问到网站的一种服务:IP地址是网络上标识站点的数字地址,为了方便记忆,采用域名来代替IP地址标识站点地址.域名解析 ...

最新文章

  1. HTML锚点控制,跳转页面后定位到相应位置
  2. 有逼格的产品经理都用什么样的杯子?
  3. linux—用nc命令监控检测服务器端口
  4. 2.3、Android Studio使用Layout Editor设计UI
  5. 数据结构——二叉树的最小深度算法
  6. codeforces B. The Fibonacci Segment 解题报告
  7. idea编辑器中使用@Data注解无效解决办法
  8. linux系统运行pbs出现ntf,Linux系统启动故障修复
  9. 文件上传控件 css,CSS3 自定义文件上传输入控件界面
  10. Snabbdom(虚拟dom)
  11. 格鲁吉亚理工学院计算机全美排名,乔治亚理工大学环境工程排名2019年
  12. linux驱动编写(设备树)
  13. 第二阶段小组冲刺第三天总结
  14. Python并行实现XML文件转换为XLSX文件
  15. 测试点击屏幕次数的软件_显示器响应时间测试软件
  16. python爬虫小说实例源码_Python下载网络小说实例代码
  17. 或有事项会计处理研究 ——以广西上市公司为例
  18. 只有程序员能看懂的西游记
  19. 问题事件名称: APPCRASH(解决方法)
  20. staircase nim 经典组合游戏

热门文章

  1. http请求与响应(content-type)
  2. attachEvent中this指向(转)
  3. 消息队列中间件的技术选型分析
  4. 【LeetCode】64. Minimum Path Sum
  5. 测试驱动开发(一)-我们要的不仅仅是“质量”
  6. 【OpenCV学习笔记】【函数学习】十四(cvSeq的用法说明(功能很多,按照需求使用))
  7. 智能优化算法:鼠群算法
  8. matlab-读取文件
  9. 鸿蒙比苹果流畅,华为鸿蒙应用恢复率优于苹果iOS,无惧老化36个月持续流畅
  10. 银行数据仓库体系实践_案例:农发行数据交换共享平台建设实践分享