我们假设射线的参数如下:
起点为点 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 Pn⋅D=P0⋅D
上式代入这个式子得式子
( 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=P0⋅D
我们目标是要先求出 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×Rd⋅D=P0⋅D−R0⋅D
解出 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=Rd⋅DP0⋅D−R0⋅D
即 t = ( P 0 − R 0 ) ⋅ D R d ⋅ D t = \frac { (P_0 - R_0)\cdot D}{R_d\cdot D} t=Rd⋅D(P0−R0)⋅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);//返回这点是否出界,这个方法写在后面
}
我们假设射线 起点为点 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=R0−C 从射线起点到球心
设 a = D ⋅ R d a = D\cdot R_d a=D⋅Rd 表示 D D D投影到射线的长度
设 e 2 = D ⋅ D e^2 = D\cdot D e2=D⋅D 表示从射线起点到球心模长平方
设 f = a − t f = a-t f=a−t 表示投影长度减去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 r2−f2+a2=e2 我们先求出 f f f
f = a 2 + r 2 − e 2 f=\sqrt{a^2+r^2-e^2} f=a2+r2−e2 此处有正负号,舍弃负号是为了求近点碰撞点
∵ f = a − t \because f = a-t ∵f=a−t
∴ t = a − a 2 + r 2 − e 2 \therefore t = a-\sqrt{a^2+r^2-e^2} ∴t=a−a2+r2−e2
如果 a 2 + r 2 − e 2 < 0 a^2+r^2-e^2<0 a2+r2−e2<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;
}
如果两个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;//三个方向都有交集,才相交
}
判断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;
}
同理可以实现长方体与球体的碰撞检测,只需要转换一次圆心坐标。
如果射线起点在框内,可以直接返回假。
要射线与六个面的交点会有点多,但如果面与射线同向或平行,我们就排除。
剩下最多三个面,再来用射线与平面相交法来检测交点。
如果交点在射线的反向延长线上,我们就丢弃。
如果交点在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;
}
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;
}
思路如下(有两个长方体一个叫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;
}
以上内容介绍并不是很详细,比如怎么从局部坐标转到世界坐标,怎么从世界坐标转换到局部坐标,这个问题的讨论又需要花费一个较大的篇幅,好吧,本文主要是为了记录一下编程的思想,也希望能为您提供启发。先这样吧!