空間或平面判斷兩線段相交(求交點)

charlee44發表於2021-06-15

1. 概述

研究了一些空間計算幾何的相關演算法,現在對《計算幾何》這門科學有了更多的認識。以前,解決空間幾何問題都是通過解析幾何的角度來解決問題的(高中數學知識),雖然解決思路比較直觀,但是很多時候都要付出昂貴的代價,比如精度、效率,以及繁複的判斷。而計算幾何是通過向量來解決空間幾何問題的,可以規避這些問題,使得精度和效率更高。

2. 詳論

2.1. 解析幾何演算法

比如說,在平面中判斷兩線段相交,我們可以很容易通過解析幾何來求解,聯立兩直線的代數方程:

\[(y-y2)/(y1-y2) = (x-x2)/(x1-x2) \]

然後對這個二元二次方程進行求解。很容易得到相應演算法的程式碼:

//判斷兩線段相交
bool IsIntersect(double px1, double py1, double px2, double py2, double px3, double py3, double px4, double py4)
{
	bool flag = false;
	double d = (px2 - px1) * (py4 - py3) - (py2 - py1) * (px4 - px3);
	if (d != 0)
	{
		double r = ((py1 - py3) * (px4 - px3) - (px1 - px3) * (py4 - py3)) / d;
		double s = ((py1 - py3) * (px2 - px1) - (px1 - px3) * (py2 - py1)) / d;
		if ((r >= 0) && (r <= 1) && (s >= 0) && (s <= 1))
		{
			flag = true;
		}
	}
	return flag;
}

可以看出這個演算法其實並不嚴密,其實缺少了對一些極端條件的判斷,比如與座標軸平行的情況,兩直線平行的情況,還需要做額外的判斷。同時用了很多乘法和除法,演算法效率並不高。

2.2. 同側法

這種演算法的思想是:如果兩條線段相交,那麼一條線段的兩端點必然位於另一條線段的兩端點的異側。那麼問題就可以轉換成點是否在一條線段的同側。同側判斷可以通過向量叉乘的方法來實現,即判斷最後叉乘的方向是否相同。

這個演算法與平面中判斷點在三角形內演算法這篇文章介紹的同側/異側判斷是一樣的,我認為算是比較優秀快速的演算法了。不過這個演算法可以判斷定性判斷,無法定量判斷準確的交點。而且實際使用過程中,似乎精度不太準確(個人實驗結論,尤其是位於三角形邊上的點)。

2.3. 向量方程法

2.3.1. 原理

已知空間中線段的起點O和終點E,那麼顯然方向向量D為:

\[D = E - O \]

這時,可以確定線段上某一點P為:

\[P = O + tD \]

其中,t為範圍滿足\(0<=t<=1\)的標量。

這個方程就是線段上某一點的向量方程。如果要求兩線段的交點,很顯然可以將兩個線段進行聯立:

\[\begin{cases} P = O_1 + t_1 D_1 \\ P = O_2 + t_2 D_2 \\ \end{cases} \]

上式-下式,有:

\[t_1 D_1 - t_2 D_2 = O_2 - O_1 = O_{12} \tag{1} \]

在平面上展開,也就是使用X和Y分量:

\[\begin{bmatrix} {D_1.x}&{-D_2.x}\\ {D_1.y}&{-D_2.y}\\ \end{bmatrix} \begin{bmatrix} {t1}\\ {t2}\\ \end{bmatrix}= \begin{bmatrix} {O_{12}.x}\\ {O_{12}.y}\\ \end{bmatrix} \]

那麼這個問題就轉換成了求解2行2列的線性方程組,如果有解,說明存在交點並直接求出。2行2列線性方程組直接使用克萊姆法則求解即可。

2.3.2. 實現

具體的C++實現程式碼如下:

//空間直線
template <class T>
class LineSegment
{
public:
    Vec3<T> startPoint;
    Vec3<T> endPoint;
    Vec3<T> direction;

    Vec3<T> min;
    Vec3<T> max;

    LineSegment()
    {
    }

    LineSegment(Vec3<T> start, Vec3<T> end)
    {
        startPoint = start;
        endPoint = end;
        direction = end - start;
        CalMinMax();
    }

    inline void Set(Vec3<T> start, Vec3<T> end)
    {
        startPoint = start;
        endPoint = end;
        direction = end - start;
        CalMinMax();
    }

    inline void CalMinMax()
    {
        min.x() = std::min<T>(startPoint.x(), endPoint.x());
        min.y() = std::min<T>(startPoint.y(), endPoint.y());
        min.z() = std::min<T>(startPoint.z(), endPoint.z());

        max.x() = std::max<T>(startPoint.x(), endPoint.x());
        max.y() = std::max<T>(startPoint.y(), endPoint.y());
        max.z() = std::max<T>(startPoint.z(), endPoint.z());
    }

    //兩條線段相交
    inline static bool Intersection2D(LineSegment & line1, LineSegment & line2, Vec3<T>& insPoint)
    {
        double D = -line1.direction.x() * line2.direction.y() + line1.direction.y() * line2.direction.x();
        if(D == 0.0)
        {
            return false;
        }

        auto O12 = line2.startPoint - line1.startPoint;
        T D1 = -O12.x() * line2.direction.y() + O12.y() * line2.direction.x();
        T D2 = line1.direction.x() * O12.y() - line1.direction.y() * O12.x();

        T t1 = D1 / D;
        if(t1<0 || t1 > 1)
        {
            return false;
        }

        T t2 = D2 / D;
        if(t2<0 || t2 > 1)
        {
            return false;
        }

        insPoint = line1.startPoint + line1.direction * t1;     //這樣計算得到的Z值是不準確的

        return true;
    }  
};

2.3.3. 三維展開

這個演算法還有一個好處是也很適合三維空間展開,因為線段上點的向量方程是二三維通用的。當使用X,Y,Z三個分量帶入式(1),這時得到的就是一個3行2列的超定方程組。可以繼續求解原來的2行2列的線性方程組,只有當得到的t1,t2也能滿足Z方向上的式子成立,才能說明存在交點。

3. 參考

  1. 計算幾何-判斷線段是否相交

詳細程式碼

相關文章