談談"求線段交點"的幾種算法(js實現,完整版)


"求線段交點"是一種非常基礎的幾何計算, 在很多游戲中都會被使用到. 
下面我就現學現賣的把最近才學會的一些"求線段交點"的算法說一說, 希望對大家有所幫助. 
本文講的內容都很初級, 主要是面向和我一樣的初學者, 所以請各位算法帝們輕拍啊 嘎嘎 

引用
已知線段1(a,b) 和線段2(c,d) ,其中a b c d為端點, 求線段交點p .(平行或共線視作不相交)



=============================== 
算法一: 求兩條線段所在直線的交點, 再判斷交點是否在兩條線段上. 

求直線交點時 我們可通過直線的一般方程 ax+by+c=0 求得(方程中的abc為系數,不是前面提到的端點,另外也可用點斜式方程和斜截式方程,此處暫且不論). 
然后根據交點的與線段端點的位置關系來判斷交點是否在線段上. 公式如下圖: 



實現代碼如下 : 

Javascript代碼   
  1. function segmentsIntr(a, b, c, d){  
  2.   
  3. /** 1 解線性方程組, 求線段交點. **/  
  4. // 如果分母為0 則平行或共線, 不相交  
  5.     var denominator = (b.y - a.y)*(d.x - c.x) - (a.x - b.x)*(c.y - d.y);  
  6.     if (denominator==0) {  
  7.         return false;  
  8.     }  
  9.    
  10. // 線段所在直線的交點坐標 (x , y)      
  11.     var x = ( (b.x - a.x) * (d.x - c.x) * (c.y - a.y)   
  12.                 + (b.y - a.y) * (d.x - c.x) * a.x   
  13.                 - (d.y - c.y) * (b.x - a.x) * c.x ) / denominator ;  
  14.     var y = -( (b.y - a.y) * (d.y - c.y) * (c.x - a.x)   
  15.                 + (b.x - a.x) * (d.y - c.y) * a.y   
  16.                 - (d.x - c.x) * (b.y - a.y) * c.y ) / denominator;  
  17.   
  18. /** 2 判斷交點是否在兩條線段上 **/  
  19.     if (  
  20.         // 交點在線段1上  
  21.         (x - a.x) * (x - b.x) <= 0 && (y - a.y) * (y - b.y) <= 0  
  22.         // 且交點也在線段2上  
  23.          && (x - c.x) * (x - d.x) <= 0 && (y - c.y) * (y - d.y) <= 0  
  24.         ){  
  25.   
  26.         // 返回交點p  
  27.         return {  
  28.                 x :  x,  
  29.                 y :  y  
  30.             }  
  31.     }  
  32.     //否則不相交  
  33.     return false  
  34.   
  35. }  

                  

算法一思路比較清晰易懂, 但是性能並不高. 因為它在不確定交點是否有效(在線段上)之前, 就先去計算了交點, 耗費了較多的時間. 
如果最后發現交點無效, 那么之前的計算就白折騰了. 而且整個計算的過程也很復雜. 
那么有沒有一種思路,可以讓我們先判斷是否存在有效交點,然后再去計算它呢? 
顯然答案是肯定的. 於是就有了后面的一些算法. 


=============================== 
算法二: 判斷每一條線段的兩個端點是否都在另一條線段的兩側, 是則求出兩條線段所在直線的交點, 否則不相交. 

第一步判斷兩個點是否在某條線段的兩側, 通常可采用投影法: 

求出線段的法線向量, 然后把點投影到法線上, 最后根據投影的位置來判斷點和線段的關系. 見下圖 

 

點a和點b在線段cd法線上的投影如圖所示, 這時候我們還要做一次線段cd在自己法線上的投影(選擇點c或點d中的一個即可). 
主要用來做參考. 
圖中點a投影和點b投影在點c投影的兩側, 說明線段ab的端點在線段cd的兩側. 

同理, 再判斷一次cd是否在線段ab兩側即可. 

求法線 , 求投影 什么的聽起來很復雜的樣子, 實際上對於我來說也確實挺復雜,在幾個月前我也不會(念書那會兒的幾何知識都忘光了 :'( )' 
不過好在學習和實現起來還不算復雜, 皆有公式可循: 


求線段ab的法線: 

Javascript代碼   
  1. var nx=b.y - a.y,   
  2.     ny=a.x - b.x;  
  3. var normalLine = {  x: nx, y: ny };  



注意: 其中 normalLine.x和normalLine.y的幾何意義表示法線的方向, 而不是坐標. 


求點c在法線上的投影位置: 

Javascript代碼   
  1. var dist= normalLine.x*c.x + normalLine.y*c.y;  



注意: 這里的"投影位置"是一個標量, 表示的是到法線原點的距離, 而不是投影點的坐標. 
通常知道這個距離就足夠了. 

當我們把圖中 點a投影(distA),點b投影(distB),點c投影(distC) 都求出來之后, 就可以很容易的根據各自的大小判斷出相對位置. 

distA==distB==distC 時, 兩條線段共線 
distA==distB!=distC 時, 兩條線段平行 
distA 和 distB 在distC 同側時, 兩條線段不相交. 
distA 和 distB 在distC 異側時, 兩條線段是否相交需要再判斷點c點d與線段ab的關系. 

前面的那些步驟, 只是實現了"判斷線段是否相交", 當結果為true時, 我們還需要進一步求交點. 
求交點的過程后面再說, 先看一下該算法的完整實現 : 

Javascript代碼   
  1. function segmentsIntr(a, b, c, d){  
  2.   
  3.     //線段ab的法線N1  
  4.     var nx1 = (b.y - a.y), ny1 = (a.x - b.x);  
  5.   
  6.     //線段cd的法線N2  
  7.     var nx2 = (d.y - c.y), ny2 = (c.x - d.x);  
  8.       
  9.     //兩條法線做叉乘, 如果結果為0, 說明線段ab和線段cd平行或共線,不相交  
  10.     var denominator = nx1*ny2 - ny1*nx2;  
  11.     if (denominator==0) {  
  12.         return false;  
  13.     }  
  14.       
  15.     //在法線N2上的投影  
  16.     var distC_N2=nx2 * c.x + ny2 * c.y;  
  17.     var distA_N2=nx2 * a.x + ny2 * a.y-distC_N2;  
  18.     var distB_N2=nx2 * b.x + ny2 * b.y-distC_N2;  
  19.   
  20.     // 點a投影和點b投影在點c投影同側 (對點在線段上的情況,本例當作不相交處理);  
  21.     if ( distA_N2*distB_N2>=0  ) {  
  22.         return false;  
  23.     }  
  24.       
  25.     //  
  26.     //判斷點c點d 和線段ab的關系, 原理同上  
  27.     //  
  28.     //在法線N1上的投影  
  29.     var distA_N1=nx1 * a.x + ny1 * a.y;  
  30.     var distC_N1=nx1 * c.x + ny1 * c.y-distA_N1;  
  31.     var distD_N1=nx1 * d.x + ny1 * d.y-distA_N1;  
  32.     if ( distC_N1*distD_N1>=0  ) {  
  33.         return false;  
  34.     }  
  35.   
  36.     //計算交點坐標  
  37.     var fraction= distA_N2 / denominator;  
  38.     var dx= fraction * ny1,  
  39.         dy= -fraction * nx1;  
  40.     return { x: a.x + dx , y: a.y + dy };  
  41. }  



最后 求交點坐標的部分 所用的方法看起來有點奇怪, 有種摸不着頭腦的感覺. 
其實它和算法一 里面的算法是類似的,只是里面的很多計算項已經被提前計算好了. 
換句話說, 算法二里求交點坐標的部分 其實也是用的直線的線性方程組來做的. 

現在來簡單粗略 很不科學的對比一下算法一和算法二: 
1 最好情況下, 兩種算法的復雜度相同 
2 最壞情況, 算法一和算法二的計算量差不多 
3 但是算法二提供了 更多的"提前結束條件",所以平均情況下,應該算法二更優. 

實際測試下來, 實際情況也確實如此. 

前面的兩種算法基本上是比較常見的可以應付絕大多數情況. 但是事實上還有一種更好的算法. 
這也是我最近才新學會的(我現學現賣了,大家不要介意啊...) 

=============================== 
算法三: 判斷每一條線段的兩個端點是否都在另一條線段的兩側, 是則求出兩條線段所在直線的交點, 否則不相交. 

(咦? 怎么感覺和算法二一樣啊? 不要懷疑 確實一樣 ... 囧) 
所謂算法三, 其實只是對算法二的一個改良, 改良的地方主要就是 : 
不通過法線投影來判斷點和線段的位置關系, 而是通過點和線段構成的三角形面積來判斷. 

先來復習下三角形面積公式: 已知三角形三點a(x,y) b(x,y) c(x,y), 三角形面積為: 

Javascript代碼   
  1. var triArea=( (a.x - c.x) * (b.y - c.y) - (a.y - c.y) * (b.x - c.x) ) /2 ;  



因為 兩向量叉乘==兩向量構成的平行四邊形(以兩向量為鄰邊)的面積 , 所以上面的公式也不難理解. 
而且由於向量是有方向的, 所以面積也是有方向的, 通常我們以逆時針為正, 順時針為負數. 

改良算法關鍵點就是: 
如果"線段ab和點c構成的三角形面積"與"線段ab和點d構成的三角形面積" 構成的三角形面積的正負符號相異, 
那么點c和點d位於線段ab兩側. 如下圖所示: 

 

圖中虛線所示的三角形, 纏繞方向(三邊的定義順序)不同, 所以面積的正負符號不同. 


下面還是先看代碼: 
由於我們只要判斷符號即可, 所以前面的三角形面積公式我們就不需要后面的 除以2 了. 

Javascript代碼   
  1. function segmentsIntr(a, b, c, d){  
  2.   
  3.     // 三角形abc 面積的2倍  
  4.     var area_abc = (a.x - c.x) * (b.y - c.y) - (a.y - c.y) * (b.x - c.x);  
  5.   
  6.     // 三角形abd 面積的2倍  
  7.     var area_abd = (a.x - d.x) * (b.y - d.y) - (a.y - d.y) * (b.x - d.x);   
  8.   
  9.     // 面積符號相同則兩點在線段同側,不相交 (對點在線段上的情況,本例當作不相交處理);  
  10.     if ( area_abc*area_abd>=0 ) {  
  11.         return false;  
  12.     }  
  13.   
  14.     // 三角形cda 面積的2倍  
  15.     var area_cda = (c.x - a.x) * (d.y - a.y) - (c.y - a.y) * (d.x - a.x);  
  16.     // 三角形cdb 面積的2倍  
  17.     // 注意: 這里有一個小優化.不需要再用公式計算面積,而是通過已知的三個面積加減得出.  
  18.     var area_cdb = area_cda + area_abc - area_abd ;  
  19.     if (  area_cda * area_cdb >= 0 ) {  
  20.         return false;  
  21.     }  
  22.   
  23.     //計算交點坐標  
  24.     var t = area_cda / ( area_abd- area_abc );  
  25.     var dx= t*(b.x - a.x),  
  26.         dy= t*(b.y - a.y);  
  27.     return { x: a.x + dx , y: a.y + dy };  
  28.   
  29. }  




最后 計算交點坐標的部分 和算法二同理. 


算法三在算法二的基礎上, 大大簡化了計算步驟, 代碼也更精簡. 可以說,是三種算法里, 最好的.實際測試結果也是如此. 

當然必須坦誠的來說, 在Javascript里, 對於普通的計算, 三種算法的時間復雜度其實是差不多的(尤其是V8引擎下). 
我的測試用例里也是進行變態的百萬次級別的線段相交測試 才能拉開三種算法之間的差距. 

不過本着精益求精 以及學習的態度而言, 追求一個更好的算法, 總是有其積極意義的. 


好了 不啰嗦了, 就到這里吧. 
現學現賣的東西, 難免有錯誤, 還請大家不吝斧正. 先謝謝啦 

文章來源:http://fins.iteye.com/blog/1522259


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM