在实现很多数学相关的功能的时候, 数学模型的选择尤为重要, 一个是在准确性上, 一个是在扩展性上.
比如最近我要计算一个射线跟一个线段的交点, 初中的时候就学过, 直线和直线外一点的交点怎样计算, 这里
直接上已经写完的两段代码.
简图
因为是计算机, 肯定是通过两点来确定直线方程了, 首先是用点斜式的计算方法( 原始方程 y = kx + b, 推导过程略 ):
/// <summary> /// 点斜式推理出的交点方程 /// </summary> /// <param name="ray"></param> /// <param name="p1"></param> /// <param name="p2"></param> /// <param name="point"></param> /// <returns></returns> public static bool IntersectRayAndLineSegment_XOZ_PointSlope(Ray ray, Vector3 p1, Vector3 p2, out Vector3 point) { float k = (p2.z - p1.z) / (p2.x - p1.x); float x = (p2.z - p1.z + k * p1.x + p2.x / k) / (k + 1.0f / k); float z = p1.z + k * (x - p1.x); point = new Vector3(x, 0, z); bool intersect = Vector3.Dot(new Vector3(x, 0, z) - ray.origin, ray.direction) > 0; if(intersect) { intersect = (x >= Mathf.Min(p1.x, p2.x) && x <= Mathf.Max(p1.x, p2.x)) && (z >= Mathf.Min(p1.z, p2.z) && z <= Mathf.Max(p1.z, p2.z)); } return intersect; }
然后是两点式的计算方法( 原始方程 (y-y1)/(x-x1) = (y-y2)/(x-x2), 推导过程略 ):
/// <summary> /// 两点式推出的交点方程 /// </summary> /// <param name="ray"></param> /// <param name="p1"></param> /// <param name="p2"></param> /// <param name="point"></param> /// <returns></returns> public static bool IntersectRayAndLineSegment_XOZ_TwoPoint(Ray ray, Vector3 p1, Vector3 p2, out Vector3 point) { point = Vector3.zero; Vector3 p3 = ray.origin; Vector3 p4 = ray.GetPoint(1.0f); float a1, b1, c1; PointToLineEquation_2D_XOZ(p1, p2, out a1, out b1, out c1); float a2, b2, c2; PointToLineEquation_2D_XOZ(p3, p4, out a2, out b2, out c2); float D = a1 * b2 - a2 * b1; if(D == 0) { return false; } float D1 = -c1 * b2 + c2 * b1; float D2 = -c1 * a1 + a2 * c1; point = new Vector3(D1 / D, 0, D2 / D); var intersect = (point.x >= Mathf.Min(p1.x, p2.x) && point.x <= Mathf.Max(p1.x, p2.x)) && (point.z >= Mathf.Min(p1.z, p2.z) && point.z <= Mathf.Max(p1.z, p2.z)); return intersect; } public static void PointToLineEquation_2D_XOZ(Vector3 p1, Vector3 p2, out float a, out float b, out float c) { float x1 = p1.x; float y1 = p1.z; float x2 = p2.x; float y2 = p2.z; a = y2 - y1; b = x1 - x2; c = (y1 * x2) - (y2 * x1); }
在大部分情况下它们的结果是正确的, 可是在线段是平行于坐标轴的情况下, 点斜式的结果就不对了, 往回看计算过程:
float k = (p2.z - p1.z) / (p2.x - p1.x); float x = (p2.y - p1.y + k * p1.x + p2.x / k) / (k + 1.0f / k);
点斜式在刚开始就计算了斜率k, 然后k被作为了分母参与计算, 这样在平行于x轴的时候斜率就是0, 被除之后得到无穷大(或无穷小), 在平行y轴的时候( 两点的x相等 ), k直接就是无穷大(或无穷小).
所以点斜式在计算平行于坐标轴的情况就不行了. 点斜式的问题就在于过早进行了除法计算, 而且分母还可能为0, 这在使用计算机的系统里面天生就存在重大隐患.
然后是两点式, 它的过程是由两点式推算出一般方程的各个变量( 一般方程 ax + by + c = 0 ), 然后联立两个一般方程来进行求解, 它的稳定性和可扩展性就体现在这里:
a = y2 - y1; b = x1 - x2; c = (y1 * x2) - (y2 * x1);
它的初始计算完全不涉及除法, 第一步天生就是计算上稳定的. 然后是计算联立方程的方法, 这里直接用了二元一次线性方程组的求解
float D = a1 * b2 - a2 * b1; if(D == 0) { return false; } float D1 = -c1 * b2 + c2 * b1; float D2 = -c1 * a1 + a2 * c1; point = new Vector3(D1 / D, 0, D2 / D);
使用行列式计算的好处是很容易判断出是否有解, D==0 时就是无解, 这也是计算稳定性的保证, 然后这里是只计算x, z平面的, 也就是2D的,
如果要计算3D的只需要把行列式和一般方程封装成任意元的多元方法即可, 例如一般方程为( ax + by + cz + d = 0 )甚至更高阶的( ax + by + cz + dw......+n = 0 ),
然后行列式求多元线性方程组的解就更简单了, 这就提供了很强大的扩展性, 不仅2维, 3维, 甚至N维都能提供解.
所以在做功能前仔细想好怎样做, 慎重选择数学模型是非常重要的. 虽然这些活看似初中生也能做, 大学生也能做, 差距还是一目了然的吧.
PS: 两点式没有考虑射线的方向, 直接抄来用结果肯定是错的 have fun