谈谈"求线段交点"的几种算法(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. /** 1 解线性方程组, 求线段交点. **/
  3. // 如果分母为0 则平行或共线, 不相交
  4. var denominator = (b.y - a.y)*(d.x - c.x) - (a.x - b.x)*(c.y - d.y);
  5. if (denominator==0) {
  6. return false;
  7. }
  8. // 线段所在直线的交点坐标 (x , y)
  9. var x = ( (b.x - a.x) * (d.x - c.x) * (c.y - a.y)
  10. + (b.y - a.y) * (d.x - c.x) * a.x
  11. - (d.y - c.y) * (b.x - a.x) * c.x ) / denominator ;
  12. var y = -( (b.y - a.y) * (d.y - c.y) * (c.x - a.x)
  13. + (b.x - a.x) * (d.y - c.y) * a.y
  14. - (d.x - c.x) * (b.y - a.y) * c.y ) / denominator;
  15. /** 2 判断交点是否在两条线段上 **/
  16. if (
  17. // 交点在线段1上
  18. (x - a.x) * (x - b.x) <= 0 && (y - a.y) * (y - b.y) <= 0
  19. // 且交点也在线段2上
  20. && (x - c.x) * (x - d.x) <= 0 && (y - c.y) * (y - d.y) <= 0
  21. ){
  22. // 返回交点p
  23. return {
  24. x :  x,
  25. y :  y
  26. }
  27. }
  28. //否则不相交
  29. return false
  30. }

算法一思路比较清晰易懂, 但是性能并不高. 因为它在不确定交点是否有效(在线段上)之前, 就先去计算了交点, 耗费了较多的时间. 
如果最后发现交点无效, 那么之前的计算就白折腾了. 而且整个计算的过程也很复杂. 
那么有没有一种思路,可以让我们先判断是否存在有效交点,然后再去计算它呢? 
显然答案是肯定的. 于是就有了后面的一些算法.

=============================== 
算法二: 判断每一条线段的两个端点是否都在另一条线段的两侧, 是则求出两条线段所在直线的交点, 否则不相交.

第一步判断两个点是否在某条线段的两侧, 通常可采用投影法:

求出线段的法线向量, 然后把点投影到法线上, 最后根据投影的位置来判断点和线段的关系. 见下图

点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. //线段ab的法线N1
  3. var nx1 = (b.y - a.y), ny1 = (a.x - b.x);
  4. //线段cd的法线N2
  5. var nx2 = (d.y - c.y), ny2 = (c.x - d.x);
  6. //两条法线做叉乘, 如果结果为0, 说明线段ab和线段cd平行或共线,不相交
  7. var denominator = nx1*ny2 - ny1*nx2;
  8. if (denominator==0) {
  9. return false;
  10. }
  11. //在法线N2上的投影
  12. var distC_N2=nx2 * c.x + ny2 * c.y;
  13. var distA_N2=nx2 * a.x + ny2 * a.y-distC_N2;
  14. var distB_N2=nx2 * b.x + ny2 * b.y-distC_N2;
  15. // 点a投影和点b投影在点c投影同侧 (对点在线段上的情况,本例当作不相交处理);
  16. if ( distA_N2*distB_N2>=0  ) {
  17. return false;
  18. }
  19. //
  20. //判断点c点d 和线段ab的关系, 原理同上
  21. //
  22. //在法线N1上的投影
  23. var distA_N1=nx1 * a.x + ny1 * a.y;
  24. var distC_N1=nx1 * c.x + ny1 * c.y-distA_N1;
  25. var distD_N1=nx1 * d.x + ny1 * d.y-distA_N1;
  26. if ( distC_N1*distD_N1>=0  ) {
  27. return false;
  28. }
  29. //计算交点坐标
  30. var fraction= distA_N2 / denominator;
  31. var dx= fraction * ny1,
  32. dy= -fraction * nx1;
  33. return { x: a.x + dx , y: a.y + dy };
  34. }

最后 求交点坐标的部分 所用的方法看起来有点奇怪, 有种摸不着头脑的感觉. 
其实它和算法一 里面的算法是类似的,只是里面的很多计算项已经被提前计算好了. 
换句话说, 算法二里求交点坐标的部分 其实也是用的直线的线性方程组来做的.

现在来简单粗略 很不科学的对比一下算法一和算法二: 
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. // 三角形abc 面积的2倍
  3. var area_abc = (a.x - c.x) * (b.y - c.y) - (a.y - c.y) * (b.x - c.x);
  4. // 三角形abd 面积的2倍
  5. var area_abd = (a.x - d.x) * (b.y - d.y) - (a.y - d.y) * (b.x - d.x);
  6. // 面积符号相同则两点在线段同侧,不相交 (对点在线段上的情况,本例当作不相交处理);
  7. if ( area_abc*area_abd>=0 ) {
  8. return false;
  9. }
  10. // 三角形cda 面积的2倍
  11. var area_cda = (c.x - a.x) * (d.y - a.y) - (c.y - a.y) * (d.x - a.x);
  12. // 三角形cdb 面积的2倍
  13. // 注意: 这里有一个小优化.不需要再用公式计算面积,而是通过已知的三个面积加减得出.
  14. var area_cdb = area_cda + area_abc - area_abd ;
  15. if (  area_cda * area_cdb >= 0 ) {
  16. return false;
  17. }
  18. //计算交点坐标
  19. var t = area_cda / ( area_abd- area_abc );
  20. var dx= t*(b.x - a.x),
  21. dy= t*(b.y - a.y);
  22. return { x: a.x + dx , y: a.y + dy };
  23. }

最后 计算交点坐标的部分 和算法二同理.

算法三在算法二的基础上, 大大简化了计算步骤, 代码也更精简. 可以说,是三种算法里, 最好的.实际测试结果也是如此.

当然必须坦诚的来说, 在Javascript里, 对于普通的计算, 三种算法的时间复杂度其实是差不多的(尤其是V8引擎下). 
我的测试用例里也是进行变态的百万次级别的线段相交测试 才能拉开三种算法之间的差距.

不过本着精益求精 以及学习的态度而言, 追求一个更好的算法, 总是有其积极意义的.

好了 不啰嗦了, 就到这里吧. 
现学现卖的东西, 难免有错误, 还请大家不吝斧正. 先谢谢啦

补充: 
后来微博上@miloyip (这个是真正的大牛, 是会自己写3D引擎的人哦 )还推荐了另外一种更好的算法, 不过我还没有理解透彻. 
等我学会了 再来和大家分享

时间: 2024-08-09 09:42:55

谈谈"求线段交点"的几种算法(js实现,完整版)的相关文章

求线段交点&quot;的几种算法(js实现,完整版)

"求线段交点"是一种非常基础的几何计算, 在很多游戏中都会被使用到. 下面我就现学现卖的把最近才学会的一些"求线段交点"的算法说一说, 希望对大家有所帮助. 本文讲的内容都很初级, 主要是面向和我一样的初学者, 所以请各位算法帝们轻拍啊 嘎嘎 引用 已知线段1(a,b) 和线段2(c,d) ,其中a b c d为端点, 求线段交点p .(平行或共线视作不相交) 算法一: 求两条线段所在直线的交点, 再判断交点是否在两条线段上. 求直线交点时 我们可通过直线的一般方程

POJ 1039 (求线段交点 + 思维)

题目:传送门 题意:有一个 n 个拐点的曲折的管道,你有一束光射进去(直射),问你最远能射到点的 x 坐标是多大.     1 <= n <= 20 思路:首先需要想到,这条线肯定是经过管道的一个上拐点和一个下拐点. 然后就枚举所有情况就行了. #include <iostream> #include <stdio.h> #include <string.h> #include <algorithm> #include <queue>

FIFO调用算法(不完整版)

#include<stdio.h>#include<iostream.h>#include <stdlib.h>#include<time.h>#define Stack_size 4 struct aa{ //代表一个物理页的节点int page; //物理页存放的虚存页号aa* next; //指向下一物理页};typedef struct{int elem[Stack_size];int top;int top2;}Seq; void Soseq(Se

fzu 1015 土地划分(判断线段相交+求出交点+找规律)

链接:http://acm.fzu.edu.cn/problem.php?pid=1015  Problem 1015 土地划分 Accept: 714    Submit: 1675Time Limit: 1000 mSec    Memory Limit : 32768 KB  Problem Description 在Dukeswood这块土地上生活着一个富有的农庄主和他的几个孩子.在他临终时,他想把他的土地分给他的孩子.他有许多农场,每个农场都是一块矩形土地.他在农场地图上划上一些直线将

poj1039——计算几何 求直线与线段交点,判断两条直线是否相交

poj1039——计算几何  求直线与线段交点,判断两条直线是否相交 Pipe Time Limit: 1000MS   Memory Limit: 10000K Total Submissions: 9439   Accepted: 2854 Description The GX Light Pipeline Company started to prepare bent pipes for the new transgalactic light pipeline. During the de

求逆元的四种算法(拓欧费马小线性推欧拉)

求逆元的四种算法 拓展欧几里得算法求逆元 上一篇博客中已经讲过拓展欧几里得算法,并且讲解了求逆元的原理.这里只列出代码 在要求逆元的数与p互质时使用 代码 //扩展欧几里得定理 int ex_gcd(int a,int b,int& x,int& y) { if(b==0) { x=1; y=0; return a; } int ans = ex_gcd(b,a%b,x,y); int tmp = x; x = y; y = tmp-a/b*y; return ans; } int cal

POJ 1127 Jack Straws ( 求直线交点, 判断线段是否相交(含端点) )

题目:传送门 题意: 给你 n 条线段的两个端点, 然后有多次询问, 每次询问, 问你线段 x 和 线段 y 是否相交. 若线段 A 和线段 B 相交且线段 A 和线段 C 相交,那么线段 B 和线段 C 相交.     1 < n < 13 题解: 暴力求线段是否相交, 然后再跑个 Floyd 或者并查集都可以的. #include <iostream> #include <stdio.h> #include <string.h> #include <

最小生成树的两种算法:Prim和Kruskal算法

越来越明白了一个道理:你写不出代码的原因只有一个,那就是你没有彻底理解这个算法的思想!! 以前写过最小生成树,但是,水了几道题后,过了一段时间,就会忘却,一点也写不出来了.也许原因只有一个,那就是我没有彻底理解这两种算法. 主题: 其实,求最小生成树有两个要点,一个是权值最小,还有一个就是这个图必须是树.而Prim和Kruskal的不同之处在于两者选择的变量不同,Prim选择的是始终保持权值最小,然后逐个加点构建一棵树.而Kruskal则是始终保证是一棵树(虽然构建过程中不一定是真正的树,但并查

线段与多边形关系的算法

目录 介绍 解决思路 问题一:点与线段的关系 问题二:线段与线段的关系 问题三:点与多边形的关系 问题四:线段与多边形的关系 总结 源码 介绍 最近项目中要用到有关几何(Geometry)方面的知识,程序需要判断给定的一条线段(Segment)与指定多边形(Polygon)的位置关系.这种关系分为三种:多边形包含线段.多边形与线段相交以及多边形与线段无关联. 起初我以为.NET类库中已经包含此种判定功能的API,比如类似System.Drawing.Region这些类型,后来等到实际要用的时候才