根據(jù)貝塞爾曲線上的點(diǎn)反算t值

這是一個(gè)項(xiàng)目中遇到的實(shí)際需求复旬。
場(chǎng)景是一個(gè)智能倉(cāng)庫(kù)管理系統(tǒng)诸蚕,場(chǎng)景里面有直線和曲線構(gòu)成的環(huán)穿軌道。環(huán)穿軌道上面會(huì)有小車運(yùn)動(dòng)自赔,后臺(tái)推動(dòng)小車的兩個(gè)點(diǎn)位A和B妈嘹,其中A和B都會(huì)在軌道上面,前端需要根據(jù)這兩個(gè)推送點(diǎn)绍妨,自動(dòng)播放小車從A點(diǎn)沿軌道到B點(diǎn)的動(dòng)畫(huà)润脸。下面是項(xiàng)目截圖:

項(xiàng)目截圖

項(xiàng)目中使用的是二次貝塞爾曲線,所以本文也主要以二次貝塞爾曲線為講解重點(diǎn)他去。
要實(shí)現(xiàn)上述動(dòng)畫(huà)津函,需要首先確定A點(diǎn)和B點(diǎn)在曲線上面的比例值ta和tb

最終的需求變成:“根據(jù)貝塞爾曲線上的點(diǎn)反算t值”。 大概有以下幾種方法」乱常現(xiàn)假設(shè)貝塞爾曲線上的點(diǎn)為點(diǎn)P(后續(xù)會(huì)用到該點(diǎn))尔苦。

分片迭代

分片迭代是一種近似的方法。我們知道行施,二次貝塞爾曲線的公式如下:
B(t) = (1-t)2 * P0 + 2t(1-t) * P1 + t2 * P2
其中: t \in[0,1]允坚,P0為二次貝塞爾曲線的起始點(diǎn),P1為控制點(diǎn)蛾号,P2為終止點(diǎn)稠项。

如果你對(duì)于上面的知識(shí)點(diǎn)不是很熟悉,建議學(xué)習(xí)貝塞爾曲線相關(guān)知識(shí)鲜结。推薦學(xué)習(xí)本人的專欄Canvas高級(jí)進(jìn)階, 里面有專門的章節(jié)對(duì)貝塞爾曲線進(jìn)行了全面詳細(xì)的講解展运。本文也是從該專欄的文章中摘錄并適當(dāng)改編而成的活逆。

從以上公式,我們可以得到拗胜,對(duì)于任意給定的比例值t蔗候,可以求出對(duì)應(yīng)該比例值的點(diǎn)B(t)。分片迭代思路是:現(xiàn)在加設(shè)把范圍[0,1]平均分成N(比如100)等份埂软,形成一系列的比例值t锈遥,對(duì)于每一個(gè)t值,求取對(duì)應(yīng)的點(diǎn)B(t) 勘畔,然后讓點(diǎn)B(t)和已知在貝塞爾曲線上的點(diǎn)P進(jìn)行比較所灸,如果點(diǎn)B(t)和點(diǎn)P之間的直線距離在一定的誤差范圍之內(nèi),則認(rèn)為B(t)等于P炫七,而此時(shí)的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);//根據(jù)二次貝塞爾曲線公式求B(t)万哪,其中point = B(t)
      if(distance(point,p) < 0.01){ // 判斷point和p點(diǎn)的距離是否在特定誤差之內(nèi)
        return t;
      }
      t+= 0.001;
  }
  return null;
}

上述分片迭代的方法懦尝,思路最簡(jiǎn)單,最直觀壤圃。在精度要求不高的情況下是可以滿足的陵霉。而在精度要求高的時(shí)候,即代碼中的“特定誤差”值要很小伍绳,可能會(huì)出現(xiàn)函數(shù)返回值為null的情況踊挠,在精度要求高的時(shí)候要能夠計(jì)算出值,就要增加迭代次數(shù)冲杀,此時(shí)會(huì)極大增加性能消耗效床。比如上面代碼的迭代次數(shù)可能會(huì)變成10000甚至10000。

迭代方法同樣適用于三次貝塞爾曲線和更加高階的貝塞爾曲線权谁。

分片迭代優(yōu)化版本

上面提到在精度要求高的情況下剩檀,要得到正確結(jié)果,要極大的增加迭代次數(shù)旺芽,造成性能的極大消耗沪猴。 有沒(méi)有辦法既提高精度,又不大量增加迭代次數(shù)呢采章? 經(jīng)過(guò)筆者的思考运嗜,發(fā)現(xiàn)是可以的。想想假設(shè)要求的t值在0.5附近悯舟,那么我們只需要在0.5附近加大分片的數(shù)量担租,而不需要在其他地方(0.10.4,0.61.0)增加分片的數(shù)量抵怎。 應(yīng)此升級(jí)版本的思路就是奋救,先用比較粗的分片初步確定t值的一個(gè)大致范圍岭参,再在該范圍之類,比較細(xì)的分片確定t值尝艘。注意這是個(gè)遞歸的過(guò)程演侯,如果在第二次比較細(xì)的分片情況下,仍然不能確定t值利耍,那么就確定一個(gè)t值的更小分范圍;重復(fù)上面過(guò)程盔粹,直到找到t值為止隘梨。
大致步驟如下:

  • 首先,通過(guò)一個(gè)小的迭代次數(shù)進(jìn)行分片迭代舷嗡;
  • 在迭代的過(guò)程中如果找到了符合的比例值t轴猎,直接返回;
  • 在迭代的過(guò)程中同時(shí)記錄離目標(biāo)點(diǎn)P最近的t值进萄,如果上一步未找到符合的t值捻脖,則進(jìn)行下一步操作。
  • 上一步找到了離目標(biāo)點(diǎn)P最近的t值中鼠,在t值的附近(t - step,t + step)(其中step為上一次分片的步進(jìn)值)進(jìn)行分片迭代查找可婶,在迭代的過(guò)程中如果找到了符合的比例值t,直接返回援雇。
  • 如果沒(méi)找到矛渴,重復(fù)上面的不斷縮小范圍并加大分片精度的過(guò)程。 直到找到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);
}

以上過(guò)程雖然增加了一定的迭代次數(shù)具温,但是是常量級(jí)別的增加,而非數(shù)量級(jí)別的增加筐赔,所以會(huì)極大提高性能。 比如目標(biāo)t值在0.5附近,第一次通過(guò)100次迭代可以確定t值的范圍在0.4 ~ 0.6之間膳音;然后進(jìn)行第二次迭代姐刁,第二次迭代此次數(shù)仍然為100次,假設(shè)確定t值的范圍在0.51 ~ 0.53之間贿肩;然后進(jìn)行第三次迭代鳞绕,第三次迭代此次數(shù)仍然為100次,此時(shí)可以獲取t值為0.516尸曼,可以看出最多值迭代了300次们何。 假設(shè)總共經(jīng)過(guò)第N次迭代,每次迭代次數(shù)為M控轿,才找到t值冤竹,那么總共的迭代次數(shù)是N * M拂封。

該迭代方法同樣適用于三次貝塞爾曲線和更加高階的貝塞爾曲線。而且相對(duì)于未優(yōu)化的版本鹦蠕,該方法的性能好了很多冒签。是適合所有貝塞爾曲線的比較好的反算t值的方法。

二分法

二分法的思路是:

  • 首先確定一個(gè)起始t值和結(jié)束t值t0和t1钟病,初始值t0 = 0萧恕,t1 = 1。
  • 取t0和t1的中間值tm = (t0+t1)/2
  • 通過(guò)tm計(jì)算出點(diǎn)Pm肠阱,如果Pm和目標(biāo)點(diǎn)P之間的距離在誤差值范圍之內(nèi)票唆,則tm為需要計(jì)算的目標(biāo)t值。
  • 如果上一步Pm和目標(biāo)點(diǎn)P之間的距離不在誤差值范圍之內(nèi)屹徘,則判斷Pm和目標(biāo)點(diǎn)P的前后順序走趋,如果Pm在目標(biāo)點(diǎn)P的前面,則把tm賦值給t1噪伊;否則把tm賦值給t0簿煌。
  • 重復(fù)以上步驟直到找到合適的tm值。

上述步驟有一個(gè)難點(diǎn): 如何判斷Pm和目標(biāo)點(diǎn)P的前后順序鉴吹?
對(duì)于二次貝塞爾曲線姨伟,如下圖所示:

二次貝塞爾曲線

其中,P0為起始點(diǎn)豆励,P2為終止點(diǎn)授滓,P1為控制點(diǎn)。 二次貝塞爾曲線有如下特點(diǎn):
線段(P1肆糕,P0)般堆、(P1,P2)和曲線相切诚啃,這也就意味著曲線一定在三角形(P0淮摔,P1,P2)之內(nèi)始赎,而且二次貝塞爾曲線本身不會(huì)自身相交和橙,所有我們可以有如下結(jié)論,

對(duì)于曲線上面的點(diǎn)A造垛,直線(P1,A)和線段(P0,P1)相交于點(diǎn)a魔招;對(duì)于曲線上面的點(diǎn)B,直線(P1,B)和線段(P0,P1)相交于點(diǎn)b五辽。點(diǎn)A和點(diǎn)B的先后順序與點(diǎn)a和點(diǎn)b的先后順序是一致的办斑,而直線上面的點(diǎn)(a和b)的前后順序是容易判斷的。 也就是說(shuō)如果點(diǎn)a在點(diǎn)b的前面,則點(diǎn)A也在點(diǎn)B的前面乡翅,反之亦然鳞疲。如下圖所示:

判斷先后順序

有了以上的結(jié)論,我們就找到了判斷Pm和目標(biāo)點(diǎn)P的前后順序的方法蠕蚜。

如果你對(duì)上述結(jié)論不熟悉尚洽,建議學(xué)習(xí)貝塞爾曲線的相關(guān)知識(shí),推薦學(xué)習(xí)本人的專欄Canvas高級(jí)進(jìn)階, 里面有專門的章節(jié)對(duì)貝塞爾曲線進(jìn)行了全面詳細(xì)的講解靶累。本文也是從該專欄的文章中摘錄并適當(dāng)改編而成的腺毫。

有了這個(gè)方法,加上前面描述的二分查找的步驟挣柬,可以得出示例代碼如下:

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;
   }
   //求交點(diǎn):
   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);
}

解方程

前面說(shuō)過(guò)潮酒,貝塞爾曲線的公式如下:
B(t) = (1-t)2 * P0 + 2t(1-t) * P1 + t2 * P2
其中: t \in[0,1],P0為二次貝塞爾曲線的起始點(diǎn)凛忿,P1為控制點(diǎn)澈灼,P2為終止點(diǎn)竞川。
分別表示成x和y的方程店溢,則可以表示如下:

  • xP = (1-t)2 * xP0 + 2t(1-t) * xP1 + t2 * xP2
  • yP = (1-t)2 * yP0 + 2t(1-t) * yP1 + t2 * yP2

實(shí)際上就是兩個(gè)變量t的二次元方程,取上面任意一個(gè)方程委乌,帶入相關(guān)的值解方程床牧,方程的解即為我們要求的目標(biāo)t值。

整理方程: xP = (1-t)2 * xP0 + 2t(1-t) * xP1 + t2 * xP2遭贸,可以得出二次方程如下:
(xP2 + xP0 - 2 * xP1 ) * t2 + 2(xP1 - xP0) * t + (xP0 - xP) = 0戈咳。
我們已知二次方程的: a
t2 + b * t + c = 0的解為:

  • 如果a = 0,則解為 -c/b
  • 如果a != 0壕吹,解如下圖所示:


    方程的解

應(yīng)此令:

  • a = (xP2 + xP0 - 2 * xP1 )
  • b = 2*(xP1 - xP0)
  • c = (xP0 - xP)
    可以方便求出方程的解著蛙。

需要注意的是,二次方程的解可能會(huì)有兩個(gè)耳贬。如果求出的解有兩個(gè)怎么辦呢踏堡。 首先我們知道貝塞爾曲線的t值的范圍是t \in[0,1],所以如果有兩個(gè)解:

  • 其中一個(gè)不再[0,1]的范圍之內(nèi)咒劲,則另外一個(gè)解就是目標(biāo)t值顷蟆。(注意不可能兩個(gè)都不在[0,1]范圍之內(nèi),因?yàn)槲覀冎栏辏繕?biāo)點(diǎn)P在貝塞爾曲線上面)帐偎。
  • 如果兩個(gè)解都在[0,1]范圍之內(nèi),那就把兩個(gè)解再帶入貝塞爾曲線的公式蛔屹,分別求出兩個(gè)B(t)點(diǎn)削樊,那個(gè)離目標(biāo)點(diǎn)P近,就取那個(gè)解兔毒。

下面是示例代碼,其中函數(shù)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坐標(biāo)值或者y坐標(biāo)值摇肌,zp代表目標(biāo)點(diǎn)P的x坐標(biāo)值或者y坐標(biāo)值
    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;
}

幾種方法的比較

從性能方面來(lái)說(shuō):

  • 解方程的方式是最快的
  • 二分法和分片迭代的優(yōu)化版次之
  • 原始的分片迭代方法最差

從通用性來(lái)說(shuō),分片迭代的方式是適合任意階的貝塞爾曲線仪际。但是考慮到性能問(wèn)題所以分片迭代的優(yōu)化版是通用性最好的求解方法围小。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市树碱,隨后出現(xiàn)的幾起案子肯适,更是在濱河造成了極大的恐慌,老刑警劉巖成榜,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件框舔,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡赎婚,警方通過(guò)查閱死者的電腦和手機(jī)刘绣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)挣输,“玉大人纬凤,你說(shuō)我怎么就攤上這事×媒溃” “怎么了停士?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)完丽。 經(jīng)常有香客問(wèn)我恋技,道長(zhǎng),這世上最難降的妖魔是什么逻族? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任蜻底,我火速辦了婚禮,結(jié)果婚禮上瓷耙,老公的妹妹穿的比我還像新娘朱躺。我一直安慰自己,他們只是感情好搁痛,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開(kāi)白布长搀。 她就那樣靜靜地躺著,像睡著了一般鸡典。 火紅的嫁衣襯著肌膚如雪源请。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,763評(píng)論 1 307
  • 那天,我揣著相機(jī)與錄音谁尸,去河邊找鬼舅踪。 笑死,一個(gè)胖子當(dāng)著我的面吹牛良蛮,可吹牛的內(nèi)容都是我干的抽碌。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼决瞳,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼货徙!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起皮胡,我...
    開(kāi)封第一講書(shū)人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤痴颊,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后屡贺,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體蠢棱,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年甩栈,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了泻仙。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡谤职,死狀恐怖饰豺,靈堂內(nèi)的尸體忽然破棺而出亿鲜,到底是詐尸還是另有隱情允蜈,我是刑警寧澤,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布蒿柳,位于F島的核電站饶套,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏垒探。R本人自食惡果不足惜妓蛮,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望圾叼。 院中可真熱鬧蛤克,春花似錦、人聲如沸夷蚊。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)惕鼓。三九已至筋现,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背矾飞。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工一膨, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人洒沦。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓豹绪,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親申眼。 傳聞我的和親對(duì)象是個(gè)殘疾皇子森篷,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容