直观方法:判断射线是否与三角形所在的平面相交,如果相交,计算出射线与平面交点,如果交点在三角形内,则射线与三角形相交
接上一篇 射线与平面相交检测,在这里直接使用射线与平面相交计算出来的交点坐标,判断交点坐标是否在三角形内。
判断一个点是否在三角形内,需要使用到 Barycentric Coordinates(质心坐标系)
质心坐标系的基本概念:
几何结构中,图形中的点相对各顶点的位置,三角形中各顶点由(1, 0, 0),(0, 1, 0),(0, 0, 1)分别表示,则三角形内所有点的坐标 (u, v, w) 可以通过和三个顶点的关系求得,
且 w = 1 - u - v
u, v, w >= 0
三角形内各点坐标实际位置,可由公式 V(x,y,z) = u * p0, v * p1 + w * p2求得,其中 p0, p1, p2 分别为三角形各顶点的实际坐标。
看下图
三角形内任一点 V 的坐标可以由一个顶点和它相邻的两条边来表示
V = p0p1 * v + p0p2 * u
其中 向量p0p1 = p1 - p0,向量 p0p2 = p2 - p0
在3D 下计算 u、v、w 比较麻烦,效率比较低,所以一般情况下是将三角形投影到一个2D平面上来计算,根据三角形法线在 x/y/z 三条轴上的分量值中最大者所在轴作为投影平面,如果三角形的法线为 (x,y,z) = (0.267, 0.535, -0.802),因为z值最大,则选取投影面为 X轴Y轴组成的 XY平面,有同学可能会疑问 x、y 值 大于0,z 值小于零,为什么是 z值最大??因为这里取的是绝对值。
根据投影后计算的 u、v 值判断交点是否在三角形内
代码逻辑如下
public class RayTriangleCollision
{
/// <summary>
/// 射线与三角形相交检测
/// </summary>
/// <param name="source">射线起点坐标</param>
/// <param name="rayDirection">射线方向以及长度</param>
/// <param name="p0">三角形一个顶点</param>
/// <param name="p1">三角形一个顶点</param>
/// <param name="p2">三角形一个顶点,三角形三个顶点是有顺序要求的,因为输入的顶点次序(顺时针、逆时针)决定三角形法线的朝向</param>
/// <param name="hitPos">相交情况下交点坐标</param>
/// <returns>返回值为 true 为相交、false 为不相交</returns>
public bool IsRayTriangleCollision(Vector3 source, Vector3 rayDirection, Vector3 p0, Vector3 p1, Vector3 p2, ref Vector3 hitPos)
{
Vector3 E1 = p1 - p0;
Vector3 E2 = p2 - p1;
// 三角形所在平面法向量
Vector3 triangleNormal = Vector3.Cross(E1, E2);
// 射线起点到三角形一个顶点的向量
Vector3 PC = p0 - source;
float dot_rayDir_planeNormal = Dot(rayDirection, triangleNormal);
float dot_pa_planeNormal = Dot(PC, triangleNormal);
if ( dot_rayDir_planeNormal == 0
|| (dot_rayDir_planeNormal > 0 && dot_pa_planeNormal < 0)
|| (dot_rayDir_planeNormal < 0 && dot_pa_planeNormal > 0))
{
return false;
}
// 向量长度不足以到达三角形所在的平面
if (Mathf.Abs(dot_rayDir_planeNormal) < Mathf.Abs(dot_pa_planeNormal))
{
return false;
}
// 计算射线到平面的距离
float length = dot_pa_planeNormal / dot_rayDir_planeNormal;
// 计算射线与平面交点坐标
hitPos = source + rayDirection * length;
float u0, u1, u2;
float v0, v1, v2;
// 找到主要的轴,选择投影平面
if (Mathf.Abs(triangleNormal.x) > Mathf.Abs(triangleNormal.y))
{
if (Mathf.Abs(triangleNormal.x) > Mathf.Abs(triangleNormal.z))
{
u0 = hitPos.y - p0.y;
u1 = p1.y - p0.y;
u2 = p2.y - p0.y;
v0 = hitPos.z - p0.z;
v1 = p1.z - p0.z;
v2 = p2.z - p0.z;
}
else
{
u0 = hitPos.x - p0.x;
u1 = p1.x - p0.x;
u2 = p2.x - p0.x;
v0 = hitPos.y - p0.y;
v1 = p1.y - p0.y;
v2 = p2.y - p0.y;
}
}
else if (Mathf.Abs(triangleNormal.y) > Mathf.Abs(triangleNormal.z))
{
u0 = hitPos.x - p0.x;
u1 = p1.x - p0.x;
u2 = p2.x - p0.x;
v0 = hitPos.z - p0.z;
v1 = p1.z - p0.z;
v2 = p2.z - p0.z;
}
else
{
u0 = hitPos.x - p0.x;
u1 = p1.x - p0.x;
u2 = p2.x - p0.x;
v0 = hitPos.y - p0.y;
v1 = p1.y - p0.y;
v2 = p2.y - p0.y;
}
// 计算分母,检查其有效性
float temp = u1 * v2 - v1 * u2;
if (temp == 0)
{
return false;
}
temp = 1.0f / temp;
//计算重心坐标,每一步都检测边界条件
float alpha = (u0 * v2 - v0 * u2) * temp;
if (alpha < 0)
{
return false;
}
float beta = (u1 * v0 - v1 * u0) * temp;
if (beta < 0)
{
return false;
}
float gamma = 1.0f - alpha - beta;
if (gamma < 0)
{
return false;
}
return true;
}
public float Dot(Vector3 vector1, Vector3 vector2)
{
return vector1.x * vector2.x + vector1.y * vector2.y + vector1.z * vector2.z;
}
}