关于射线检测与碰撞检测

关于射线检测与碰撞检测_基础

    • 基础知识 射线与平面的交点
    • 1. 射线与球体的交点检测
    • 2. AABB框重叠检测,可以快速判断两个AABB是否产生碰撞, 以用来排除一些更复杂的检测
    • 3. 判断AABB包含点算法,AABB最近点算法,可以计算AABB和球体的碰撞关系
    • 4. AABB框的射线检测算法,同理也可实现长方体射线检测。
    • 5. 长方体的射线检测算法,调用了AABB射线检测
    • 6. 长方体的碰撞检测,思路简单高效。

基础知识 射线与平面的交点

我们假设射线的参数如下:
起点为点 R 0 R_0 R0
方向为单位向量 R d R_d Rd

假设我们已经找到了两者的交点 P n P_n Pn
到射线上离起点距离为 t t t的点 P n P_n Pn可表示为
P n = R 0 + R d × t P_n=R_0+R_d\times t Pn=R0+Rd×t

我们设平面上有一点为点 P 0 P_0 P0
平面的法线为单位向量 D D D
则平面的一点 P n P_n Pn满足
P n ⋅ D = P 0 ⋅ D P_n \cdot D = P_0 \cdot D PnD=P0D
上式代入这个式子得式子

( R 0 + R d × t ) ⋅ D = P 0 ⋅ D (R_0+R_d\times t) \cdot D = P_0 \cdot D (R0+Rd×t)D=P0D

我们目标是要先求出 t t t
因为 t × R d ⋅ D = P 0 ⋅ D − R 0 ⋅ D t\times R_d\cdot D = P_0 \cdot D - R_0\cdot D t×RdD=P0DR0D

解出 t = P 0 ⋅ D − R 0 ⋅ D R d ⋅ D t = \frac { P_0 \cdot D - R_0\cdot D}{R_d\cdot D} t=RdDP0DR0D

t = ( P 0 − R 0 ) ⋅ D R d ⋅ D t = \frac { (P_0 - R_0)\cdot D}{R_d\cdot D} t=RdD(P0R0)D

这时我们利用公式 P n = R 0 + R d × t P_n=R_0+R_d\times t Pn=R0+Rd×t 就可求出相交点了。

这个方法很基础,是一切平面射线检测的前提。

bool RayPlane(Vector3 p, Vector3 a, Ray ray, float dis, out Vector3 intP, out float f)//平面与射线
{
    intP = Vector3.zero; f = 0;//输出值

    float dn = Vector3.Dot(ray.direction, a);//先计算dn, 公式 t=(p-pD)·n/pD·n
    if (dn > -1e-6f) return false;//大于0表示同向,等于0表示射线与平面平等,两种情况都返回假

    float t = Vector3.Dot(p - ray.origin, a) / dn;//根据公式求 t=(p-pD)·n/pD·n
    if (t > dis || t < 0) return false;//距离太大,或在射线的反向延长线上,都返回假

    f = t;//保存到原点的距离
    intP = ray.origin + t * ray.direction; //求出最终交点
    return Contains(intP);//返回这点是否出界,这个方法写在后面
}

1. 射线与球体的交点检测

我们假设射线 起点为点 R 0 R_0 R0
方向为单位向量 R d R_d Rd
假设我们已经找到了两者的交点 P n P_n Pn
这个点在射线上,也在球面上,
设这个点到射线起点距离为 t t t,就可表示为
P n = R 0 + R d × t P_n=R_0+R_d\times t Pn=R0+Rd×t

我们设圆或球
球心为点 C C C
球的半径是 r r r
D = R 0 − C D = R_0-C D=R0C 从射线起点到球心
a = D ⋅ R d a = D\cdot R_d a=DRd 表示 D D D投影到射线的长度
e 2 = D ⋅ D e^2 = D\cdot D e2=DD 表示从射线起点到球心模长平方
f = a − t f = a-t f=at 表示投影长度减去t的长度
设球心到射线的垂线长为 g g g 形成直角三角形,则有
g 2 + a 2 = e 2 g^2+a^2 = e^2 g2+a2=e2

另有 g 2 + f 2 = r 2 g^2+f^2 = r^2 g2+f2=r2

合并两式得 r 2 − f 2 + a 2 = e 2 r^2- f^2 + a^2 = e^2 r2f2+a2=e2 我们先求出 f f f
f = a 2 + r 2 − e 2 f=\sqrt{a^2+r^2-e^2} f=a2+r2e2 此处有正负号,舍弃负号是为了求近点碰撞点
∵ f = a − t \because f = a-t f=at
∴ t = a − a 2 + r 2 − e 2 \therefore t = a-\sqrt{a^2+r^2-e^2} t=aa2+r2e2
如果 a 2 + r 2 − e 2 < 0 a^2+r^2-e^2<0 a2+r2e2<0 则表示射线与球体没有交点。
好了,既然 t t t 求出来了,就好办了。
我们利用射线上的点的公式 P n = R 0 + R d × t P_n=R_0+R_d\times t Pn=R0+Rd×t 就可求出相交点了

public bool RayCast(Ray ray, float d2, out Hitinfo hitinfo)//求射线与球体相交:射线,距离平方,输出
{
    hitinfo = new Hitinfo();d
    Vector3 e = Pos - ray.origin, d = ray.direction;
    float
        e2 = e.sqrMagnitude,//模平方
        r2 = rad * rad,//圆半径平方
        a = Vector3.Dot(e, d),//e投影到d的模长
        a2 = a * a,//a平方
        f2 = r2 + a2 - e2;//f平方
    if (e2 < r2 || e2 > d2 + r2 || a < 0 || f2 < 0) return false;//内部,距离太短,相反,无交点
    hitinfo.dis = a - Mathf.Sqrt(f2);//到射线原点的距离
    hitinfo.point = ray.GetPoint(hitinfo.dis);//射线与球体的交点
    hitinfo.obj = this;//球体
    return true;
}

2. AABB框重叠检测,可以快速判断两个AABB是否产生碰撞, 以用来排除一些更复杂的检测

如果两个AABB框重叠,则在三个轴上均会有交集,

使用它们的最小角点和最大角点即可判断出来

Corners[0]代表最小角点,Corners[1]代表最大角点,

public bool Overlap(Aabb b)//检查两个AABB是否重叠
{
   Vector3[] cor = Corners, bcor = b.Corners;
   if (cor[0].x > bcor[1].x || bcor[0].x > cor[1].x) return false;//区间无相交检测
   if (cor[0].y > bcor[1].y || bcor[0].y > cor[1].y) return false;//区间无相交检测
   if (cor[0].z > bcor[1].z || bcor[0].z > cor[1].z) return false;//区间无相交检测
   return true;//三个方向都有交集,才相交
}

3. 判断AABB包含点算法,AABB最近点算法,可以计算AABB和球体的碰撞关系

判断AABB框是否包含一个点,若包含则这个点比一定最小角点大,比最大角点小

另外,如果我们求出球心到AABB框最近点, 即可求出球体和AABB是否有碰撞呢。
为什么这么说呢?
因为我们只需要判断最近点到球心的距离是否小于球半径,就能判断两者是否碰撞了。

public bool Contains(Vector3 p)//判断是否包含一点
{
    var cor = Corners;//获取两个极值端点
    return Vector3.Min(cor[0], p) == cor[0] && Vector3.Max(cor[1], p) == cor[1];
}
public Vector3 NearestPoint(Vector3 p)//AABB上离点P最近的点
{
    var cor = Corners;
    if (p.x < cor[0].x) p.x = cor[0].x; else if (p.x > cor[1].x) p.x = cor[1].x;
    if (p.y < cor[0].y) p.y = cor[0].y; else if (p.y > cor[1].y) p.y = cor[1].y;
    if (p.z < cor[0].z) p.z = cor[0].z; else if (p.z > cor[1].z) p.z = cor[1].z;
    return p;
}
public bool OverlapSphere(TranSp sp)//AABB和球体是否相交
{
    if (Contains(sp.Pos)) return true;
    Vector3 n = NearestPoint(sp.Pos);
    return (n - sp.Pos).sqrMagnitude < sp.rad * sp.rad;
}

同理可以实现长方体与球体的碰撞检测,只需要转换一次圆心坐标。

4. AABB框的射线检测算法,同理也可实现长方体射线检测。

如果射线起点在框内,可以直接返回假。

要射线与六个面的交点会有点多,但如果面与射线同向或平行,我们就排除。

剩下最多三个面,再来用射线与平面相交法来检测交点。

如果交点在射线的反向延长线上,我们就丢弃。

如果交点在AABB框内,表示这个点是击中点, 表示相交成功!

public bool RayCast(Ray ray, float dis, out Hitinfo hit)//AABB射线相交检测,射线分别与六个面检测
{
    hit = new Hitinfo();
    Vector3[] corns = Corners;//最小最大角点数组
    if (Contains(ray.origin)) return false;//如果包含原点,返回假
    for (int i = 0; i < 6; i++)//遍历6个面, min点管左下后,max点管右上前
    {
        if (RayPlane(corns[i / 3], sixP[i], ray, dis, out Vector3 intP, out float f))//如果有交点
        {
            hit.dis = f;//保存到射线原点的距离
            hit.point = intP;//射线与球体的交点
            hit.obj = this;//球体
            return true;
        }
    }
    return false;
}

5. 长方体的射线检测算法,调用了AABB射线检测

1。获取这个长方体的原始AABB框,并将射线转换到局部空间。问题转换成对AABB求射线交点,和上一个问题是一样的。

2。获取射线与AABB框的交点,调用上面的方法!

3。转换这个交点到世界空间,即是碰撞点,完成!

public bool RayCast(Ray ray, float dis, out Hitinfo hit)//Box的射线相交检测, 转换到局部,再应用AABB检测
{
    hit = new Hitinfo();
    ray.origin = TranformPointInv(ray.origin);//转换射线起点到局部坐标
    ray.direction = TranformDirInv(ray.direction);//转换射线方向到局部坐标
    bool hited = Ab0.RayCast(ray, dis, out hit);
    hit.point = TranformPoint(hit.point);//交点 转换回到 世界坐标
    return hited;
}

6. 长方体的碰撞检测,思路简单高效。

思路如下(有两个长方体一个叫a, 一个叫b):

AABB包围框测试, a,b的包围框没有交集,则不可能发生碰撞,直接返回假

顶点包含测试:
b有任何一个顶点包含于a中,返回真
a有任何一个顶点包含于b中,返回真

这样可以筛选出大量的有相交和无相交的情况

如果以上两种情况都不满足,那做边包含检测,过程如下:

将b的每一条边当作一条定长的射线, 射向a求交点, 调用上面的求长方体与射线的交点即可得到答案, 长方体的边共有12条,那需要12次运算,
但只要有其中任何一条边和a相交,不需要再继续遍历了,到这可立即返回真

另需要说明一点,因为之前做了顶点包含测试,射线起点不会在盒内部,所以只需要单向测试,并不需要反转射线再测试一遍。其中原委您仔细想想就应该明白了。

public bool Overlap(Box b)//检测盒子和盒子是否相交, 把b的每条边当成射线进行检测。
{
    Vector3[]
        averts = Array.ConvertAll(Ab0.Verts, g => TranformPoint(g)),//顶点转换到世界坐标
        bverts = Array.ConvertAll(b.Ab0.Verts, g => b.TranformPoint(g));//顶点转换到世界坐标 可用于构建射线
    if (ContainsOne(bverts)) return true;//顶点包含测试, 以返回已相交的几何体
    if (b.ContainsOne(averts)) return true;//顶点包含测试, 以返回已相交的几何体
    if (!AbVerts.Overlap(b.AbVerts)) return false;//最新AABB没相交,返回假, 以排除不可能相交的几何体
    var size = b.Ab0.Size;//size是x,y,z三个方向的长度尺寸
    foreach (var item in Aabb.edges)//12条边做成12根射线,分别做一次射线检测
    {
        Hitinfo hit;
        Ray ray = new Ray(bverts[item[0]], bverts[item[1]] - bverts[item[0]]);//生成射线
        if (RayCast(ray, size[item[2]], out hit)) return true;//定长射线的相交测试
    }
    return false;
}

以上内容介绍并不是很详细,比如怎么从局部坐标转到世界坐标,怎么从世界坐标转换到局部坐标,这个问题的讨论又需要花费一个较大的篇幅,好吧,本文主要是为了记录一下编程的思想,也希望能为您提供启发。先这样吧!

你可能感兴趣的:(算法,unity3d,c#,图形学)