游戏里的向量的计算(判断线段重叠、相交,合并线段,点与线的关系)

我正在参加掘金社区游戏创意投稿大赛个人赛,详情请看:游戏创意投稿大赛

主要内容:

  • 点乘与叉乘
  • 判断点与直线的位置关系
  • 判断两线段是否相交并计算交叉点
  • 判断两线段重合并计算其重合部分
  • todo 合并两条重合的线段

点乘(点积)

  • 计算公式:a·b = (ax,ay)·(bx,by) = axbx+ayby = |a|·|b|·cosθ;结果为标量。
  • 计算向量b在向量a(单位向量)上的投影:投影长度 = |b|·cosθ; 投影 = a·|b|·cosθ
  • 计算两个向量的夹角:cosθ = a·b / (|a|·|b|) => θ = arccos(a·b / (|a|·|b|))
  • 点乘结果越大,两向量夹角越小;可以用于判断夹角大小。
    • 点积结果 == 1: 夹角θ == 0;
    • 点积结果 > 0: 0 <= 夹角θ <90;
    • 点积结果 == 0: 夹角θ == 90;
    • 点积结果 < 0: 90 < 夹角θ < 180;
    • 点积结果 == -1: 夹角θ == 190。

叉乘(叉积)

  • 计算公式为:a×b=(ax,ay,az)×(bx,by,bz)=(aybz-azby,azbx-axbz,axby-aybx)。
  • 结果为垂直于a和b的向量,可以用于判断向量a和b的相对位置(顺时针方向or逆时针方向)。
  • |a×b| = |a|·|b|·sinθ = 以a和b为两边的平行四边形的面积

image.png

点与直线的位置关系

已知条件:点M,线段l(端点A、B),判断点与直线的位置关系

计算方式一:

直线公式:a * X + b * Y + c = 0

推导公式:

  1. 将线段端点代入直线公式:
    a * A.x + b * A.y + c = 0
    a * B.x + b * B.y + c = 0
  2. 两式分别相加、相减:
    (A.x + B.x) * a + (A.y + B.y) * b + 2c = 0
    (A.x - B.x) * a + (A.y - B.y) * b = 0
  3. 公式化简得:
    b = (B.x - A.x) / (A.y - B.y) * a
    c = -a * (A.y * B.x - A.x * B.y) / (A.y - B.y)
  4. 原直线公式用a表示:
    a * X + (B.x - A.x) / (A.y - B.y) * a * Y - a * (A.y * B.x - A.x * B.y) / (A.y - B.y) = 0
  5. 公式两边同时除a,直线公式用点A/B表示:
    X + (B.x - A.x) / (A.y - B.y) * Y - (A.y * B.x - A.x * B.y) / (A.y - B.y) = 0
  6. 继续化简:
    (A.y - B.y) * X + (B.x - A.x) * Y - (A.y * B.x - A.x * B.y) = 0
  7. 公式左侧==0,点在直线上,公式左侧>0,点在直线右侧,公式左侧<0,点在直线左侧。
  8. 将目标点M代入公式:
    (A.y - B.y) * M.x + (B.x - A.x) * M.y - (A.y * B.x - A.x * B.y)
    A.y * M.x - B.y * M.x + B.x * M.y - A.x * M.y - A.y * B.x + A.x * B.y
  9. 最终整理:
    (B.y - M.y) * (A.x - M.x) - (A.y - M.y) * (B.x - M.x)

代码实现:

    //点与直线的位置,目标点M,直线AB
    public float PointOfLine(Vector2 M,Vector2 A,Vector2 B)
    {
        // 返回值 > 0 在右侧, = 0 在线上, < 0 在左侧
        return (B.y - M.y) * (A.x - M.x) - (A.y - M.y) * (B.x - M.x);
    }
    
    //判断点是否在线段上,目标点M,线段AB
    public bool IsPointOnLine(Vector2 M,Vector2 A,Vector2 B)
    {
        return IsZero(PointOfLine(M,A,B));
    }
复制代码

计算方式二:

点直线段位置判断步骤:

  1. 点M与端点A或B重合;
  2. 线段没有斜率或斜率为0,判断M是否在直线AB上,在判断是否在线段内;
  3. 线段有斜率,判断线段MA与BM斜率是否一致

代码实现:

    // 判断点是否在线段上,目标点M,线段AB
    public bool IsPointOnLine(Vector2 M,Vector2 A,Vector2 B)
    {
        //与端点重合
        if(M == A || M == B)
            return true;

        //在同一竖直方向,线段竖直,点在该线段所在的直线上
        if(IsZero(A.x - B.x) && IsZero(A.x - M.x))
        {
            //已判定点在直线上,若点在两端点中间,即点在线段上
            //if ((M.y < B.y && M.y > A.y) || (M.y < A.y && M.y > B.y)),化简为:
            if((A.y - M.y) * (M.y - B.y) > 0.0f)
            {
                return true;
            }
            return false;
        }
        //在同一水平方向
        else if(IsZero(A.y - B.y) && IsZero(A.y - M.y))
        {
            if((A.x - M.x) * (M.x - B.x) > 0.0f)
            {
                return true;
            }
            return false;
        }
        //线段倾斜,此时线段所在直线存在斜率
        else
        {
            //点在直线上,MA与MB斜率相等,且有共同点M,此时MA与MB重合,即点M在直线AB上
            if(IsZero((A.y - M.y) / (A.x - M.x) - (M.y - B.y) / (M.x - B.x)))
            {
                //已判定点在直线上,若点在两端点中间,即点在线段上
                if(((A.y - M.y) * (M.y - B.y) > 0) && ((A.x - M.x) * (M.x - B.x) > 0))
                {
                    return true;
                }
            }
            return false;
        }
    }
复制代码

判断两线段是否相交

已知条件:线段l1(端点A、B)、线段l2(端点C、D)

两线段位相交关系(不考虑重叠的情况,如果线段重叠,也只返回一个点):

  • 完全相交:线段AB的端点分别在线段CD的两侧,且线段CD的端点也分别在线段AB的两侧;
  • 不完全相交:线段一个端点在另一条线段上(比如:端点A在线段CD上,如果端点B也在视为重叠,暂不区分重叠的情况)
  • 不相交:线段的两个端点都在另一条线段的同一侧

先计算每个端点与直线的位置关系,在判断段线段的位置关系

代码实现:

    //判断两线段交叉并计算交叉点,线段AB,线段CD
    public Vector2? LineCross(Vector2 A,Vector2 B,Vector2 C,Vector2 D)
    {
        //计算各端点与线段的关系
        float pointPosA = PointOfLine(A,C,D);
        float pointPosB = PointOfLine(B,C,D);
        float pointPosC = PointOfLine(C,A,B);
        float pointPosD = PointOfLine(D,A,B);

        //完全相交:线段AB的端点分别在线段CD的两侧,且线段CD的端点也分别在线段AB的两侧
        if(pointPosA * pointPosB < 0 && pointPosC * pointPosD < 0)
        {
            return GetCrosPoint(A,B,C,D);
        }

        //不完全相交:线段一个端点在另一条线段上
        if(IsZero(pointPosA))
            return A;
        if(IsZero(pointPosB))
            return B;
        if(IsZero(pointPosC))
            return C;
        if(IsZero(pointPosD))
            return D;
        //若不符合以上条件,则不相交
        //判断条件:pointPosA * pointPosB > 0 || pointPosC * pointPosD > 0
        return null;
    }
复制代码

计算两条相交线段的交点

直线方程:Y = aX + b

相交线段分情况处理(前提是已经确定线段相交):

  1. if 两线段都没有斜率,或者两斜率相等,此时两线段平行
    • 不存在交点
  2. else if 只有一条线段没有斜率(竖直方向的线段没有斜率)
  • 方法一:解方程式
    • 计算公式(假设线段AB不存在斜率,线段CD存在斜率):
    • 线段AB的直线方程:直线不存在斜率,X = Ax
    • 线段CD的直线方程:
      • 根据已有两点计算斜率:a2 = (D.y - C.y) / (D.x - C.x)
      • 截距:b = Y - aX = (C.y - C.x * a2)
      • 将斜率带入获得直线公式:Y = a2 * X + (C.y - C.x * a2)
    • 根据两个直线公式计算交点
      • x = A.x;
      • y = a2 * (A.x - C.x) + C.y;
  • 方式二:三角形三边关系
    • 假设线段AB不存在斜率,线段CD存在斜率,交点为M
    • 交点M在线段AB上,其x坐标与线段任意一点x坐标相同
    • 交点M在线段CD上,CM与CD在同一条直线上,两线段斜率相等
    • (M.y - C.y) / (M.x - C.x) = a2
    • M.y = a2 * (M.x - C.x) + C.y
    • M.x = A.x
    • M.y = a2 * (A.x - C.x) + C.y
  1. else 两条线断都有斜率
    • 计算两条线段所在直线的斜率、截距;解方程组。

代码实现:

    //计算两线段交点,线段AB,线段CD
    public Vector2? GetCrosPoint(Vector2 A,Vector2 B,Vector2 C,Vector2 D)
    {
        //直线方程: Y = aX + b

        if(IsZero((A.x - B.x) - (C.x - D.x)))
        {
            //两线段都没有斜率,或者两斜率相等,此时两线段平行
            return null;
        }
        else if(IsZero(A.x - B.x))
        {
            //线段AB竖直方向,交点为M

            //方法一:解方程式
            //线段AB的直线方程:直线不存在斜率,X = Ax
            //线段CD的直线方程:
            //根据已有两点计算斜率:a2 = (D.y - C.y) / (D.x - C.x)
            //截距:b = Y - aX = (C.y - C.x * a2)
            //将斜率带入获得直线公式:Y = a2 * X + (C.y - C.x * a2)
            //根据两个直线公式计算交点
            //x = A.x;    y = a2 * (A.x - C.x) + C.y;

            //方法二:三角形关系(不推荐)
            //计算过程如下:
            //交点M在线段AB上,其x坐标与线段任一点x坐标相同
            //交点M在线段CD上,CM与CD在同一条直线上,两线段斜率相等
            // (M.y - C.y) / (M.x - C.x) = a2
            // M.y = a2 * (M.x - C.x) + C.y
            // M.x = A.x
            // M.y = a2 * (A.x - C.x) + C.y

            //最终均可得
            float a2 = (D.y - C.y) / (D.x - C.x);
            float x = A.x;
            float y = a2 * (A.x - C.x) + C.y;
            return new Vector2(x,y);
        }
        else if(IsZero(C.x - D.x))
        {
            //线段CD竖直方向,计算过程同上
            float x = C.x;
            float a1 = (B.y - A.y) / (B.x - A.x);
            float y = a1 * (C.x - A.x) + A.y;
            return new Vector2(x,y);
        }
        else
        {
            //线段都有斜率,且不相等
            //解方程式,(方程式由公式Y = aX + b代入两点参数得)
            // Y = a1 * X + (A.y - A.x * a1)
            // Y = a2 * X + (C.y - C.x * a2)

            float a1 = (B.y - A.y) / (B.x - A.x);
            float a2 = (D.y - C.y) / (D.x - C.x);

            float x = (a1 * A.x - a2 * C.x - A.y + C.y) / (a1 - a2);
            float y = a1 * x - a1 * A.x + A.y;
            return new Vector2(x,y);
        }
    }
复制代码

判断两线段是否重合,并计算重合部分

线段重合情况:

  • 包含重合:一条线段包含在另一条线段内,比如:线段AB的端点A和端点B都在线段CD上;
  • 交叉重合:线段AB的端点A在线段CD上,线段CD的端点C在线段AB上,且AC不是同一个点;
  • 不重合。

判断两线段重合并计算其重合部分,线段AB,线段CD(重合部分为线段,返回重合线段两个端点,不考虑只有一个重合点的情况):

    public Vector2[] LineCoincide(Vector2 A,Vector2 B,Vector2 C,Vector2 D)
    {
        //可能情况说明:
        //01 包含关系,一条线段包含在另一条线段内,比如:线段AB的端点A和端点B都在线段CD上,或两条线段的两个端点完全重叠
        //  onLineA && onLineB
        //  onLineC && onLineD
        //02 交叉关系,各包含一个端点,比如:线段AB的端点A在线段CD上,线段CD的端点C在线段AB上,或两条线段的各一个端点重叠
        //  onLineA && onLineC && A != C
        //  onLineA && onLineD && A != D
        //  onLineB && onLineC && B != C
        //  onLineB && onLineD && B != D

        //计算各个端点与线段的关系(点是否在线段上)
        bool onLineA = IsPointOnLine(A,C,D);
        bool onLineB = IsPointOnLine(B,C,D);
        bool onLineC = IsPointOnLine(C,A,B);
        bool onLineD = IsPointOnLine(D,A,B);

        //coincide01
        if(onLineA && onLineB)
            return new Vector2[] { A,B };
        else if(onLineC && onLineD)
            return new Vector2[] { C,D };

        //coincide02
        if(onLineA && onLineC && (A != C))
            return new Vector2[] { A,C };
        else if(onLineA && onLineD && (A != D))
            return new Vector2[] { A,D };
        else if(onLineB && onLineC && (B != C))
            return new Vector2[] { B,C };
        else if(onLineB && onLineD && (B != D))
            return new Vector2[] { B,D };

        //若不符合以上条件,则不重合
        return null;
    }
复制代码

只判断两线段是否重合,线段AB,线段CD

    public bool IsLineCoincide(Vector2 A,Vector2 B,Vector2 C,Vector2 D)
    {
        bool onLineA = IsPointOnLine(A,C,D);
        bool onLineB = IsPointOnLine(B,C,D);
        bool onLineC = IsPointOnLine(C,A,B);
        bool onLineD = IsPointOnLine(D,A,B);

        if((onLineA && onLineB) || (onLineC && onLineD))
            return true;
        if((onLineA && onLineC && (A != C))
            || (onLineA && onLineD && (A != D))
            || (onLineB && onLineC && (B != C))
            || (onLineB && onLineD && (B != D)))
            return true;
        return false;
    }
复制代码
    //判断float == 0
    bool IsZero(float floatValue)
    {
        if(floatValue > -0.00001f & floatValue < 0.00001f)
            return true;
        else
            return false;
    }
复制代码

猜你喜欢

转载自juejin.im/post/7085274110015569927