欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

关于射线检测与碰撞检测

程序员文章站 2024-03-16 16:58:16
...

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

我们假设射线
起点为点 R 0 R_0 R0
方向为单位向量 R d R_d Rd
到射线起点距离为 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
到射线起点距离为 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

我们设圆
圆心为点 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 bool ContainsExpand(Vector3 p, float e)//尺寸扩展后 是否包含一点
{
    extents += Vector3.one * e;//扩展
    bool res = Contains(p);//判断包含
    extents -= Vector3.one * e;//恢复
    return res;
}

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):

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

AABB包围框测试, 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;
}

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