如图所示,本文介绍如何判断一个物体是否被一个凸边体区域所囊括,本文将该功能的实现拆分成了如下步骤:
1.如何判断两条线段是否相交
2.如何判断一个点是否在一个凸边形范围内(2D、xz轴构成的平面)
3.如何判断一个点是否在一个凸边体范围内(3D)
4.如何判断一个物体是否在一个凸边体范围内
依次实现:
1.如何判断两条线段是否相交:
通过矢量叉积的符号可以判断两矢量相互之间的顺逆时针关系,如下图所示,点A和点B分别在线段CD两侧,点C和点D分别在线段AB两侧,这时可以判断它们相交。判断点A和点B是否在线段CD两侧,也就是判断向量A-D和向量B-D在向量C-D的两侧,也就是叉积的结果是异号的,即:(A-D)X(C-D)*(B-D)X(C-D)< 0。同样的,判断点C和点B是否在线段AB的两侧:(D-A)X(B-A)*(C-A)X(B-A)< 0,以上这两个条件成立时,可判断两线段相交。
当然,出现以下这种情况,即(A-D)X(C-D)*(B-D)X(C-D)= 0时,两条线段也是相交的:
在Unity中封装该判断函数:
//判断AB与CD是否相交privateboolIsIntersection(Vector3A, Vector3B, Vector3C, Vector3D) { //A-D与C-D叉积结果大等于0时返回1 小于0时返回-1floatsign1=Mathf.Sign(Vector3.Cross(A-D, C-D).y); //B-D与C-D叉积结果大等于0时返回1 小于0时返回-1floatsign2=Mathf.Sign(Vector3.Cross(B-D, C-D).y); //C-A与B-A叉积结果大等于0时返回1 小于0时返回-1floatsign3=Mathf.Sign(Vector3.Cross(C-A, B-A).y); //D-A与B-A叉积结果大等于0时返回1 小于0时返回-1floatsign4=Mathf.Sign(Vector3.Cross(D-A, B-A).y); //AB与CD相交返回true 否则返回falsereturn!Mathf.Approximately(sign1, sign2) &&!Mathf.Approximately(sign3, sign4); }
测试脚本如下:
usingUnityEngine; usingUnityEditor; publicclassExample : MonoBehaviour{ [SerializeField] privateTransforma; [SerializeField] privateTransformb; [SerializeField] privateTransformc; [SerializeField] privateTransformd; //判断AB与CD是否相交privateboolIsIntersection(Vector3A, Vector3B, Vector3C, Vector3D) { //A-D与C-D叉积结果大等于0时返回1 小于0时返回-1floatsign1=Mathf.Sign(Vector3.Cross(A-D, C-D).y); //B-D与C-D叉积结果大等于0时返回1 小于0时返回-1floatsign2=Mathf.Sign(Vector3.Cross(B-D, C-D).y); //C-A与B-A叉积结果大等于0时返回1 小于0时返回-1floatsign3=Mathf.Sign(Vector3.Cross(C-A, B-A).y); //D-A与B-A叉积结果大等于0时返回1 小于0时返回-1floatsign4=Mathf.Sign(Vector3.Cross(D-A, B-A).y); //AB与CD相交返回true 否则返回falsereturn!Mathf.Approximately(sign1, sign2) &&!Mathf.Approximately(sign3, sign4); } privatevoidOnDrawGizmos() { if (a==null||b==null||c==null||d==null) return; Handles.Label(a.position, "A"); Handles.Label(b.position, "B"); Handles.Label(c.position, "C"); Handles.Label(d.position, "D"); boolflag=IsIntersection(a.position, b.position, c.position, d.position); Handles.color=flag?Color.cyan : Color.red; Handles.DrawLine(a.position, b.position); Handles.DrawLine(c.position, d.position); } }
2.如何判断一个点是否在一个凸边形范围内(2D、xz轴构成的平面):
若从该点发出的射线与平面内凸边形的交点的个数为偶数,则点在凸边形外,若为奇数,则点在凸边形内。因此取一条从该点向凸边形发出的射线,遍历凸边形的每一条边,判断射线与边的交点个数,若个数为奇数,则可以判断该点在凸边形范围内。
//判断点A是否在凸边型范围内privateboolIsInRange(Transform[] points, Vector3A) { //取第一条边中点Vector3half01= (points[0].position+points[1].position) * .5f; //中点延伸(射线)half01+= (half01-A).normalized*100000; //用于记录交点的个数intcount=0; //遍历for (inti=0; i<points.Length; i++) { vara=points[i%points.Length]; varb=points[(i+1) %points.Length]; //判断是否相交if (IsIntersection(a.position, b.position, A, half01)) count++; } //交点个数为奇数则表示点A在凸边型范围内returncount%2==1; }
测试代码如下:
usingUnityEngine; usingUnityEditor; publicclassExample : MonoBehaviour{ [SerializeField] privateTransformpoint; [SerializeField] privateTransform[] points; //凸边型顶点集合//判断点A是否在凸边型范围内privateboolIsInRange(Transform[] points, Vector3A) { //取第一条边中点Vector3half01= (points[0].position+points[1].position) * .5f; //中点延伸(射线)half01+= (half01-A).normalized*100000; //用于记录交点的个数intcount=0; //遍历for (inti=0; i<points.Length; i++) { vara=points[i%points.Length]; varb=points[(i+1) %points.Length]; //判断是否相交if (IsIntersection(a.position, b.position, A, half01)) count++; } //交点个数为奇数则表示点A在凸边型范围内returncount%2==1; } //判断AB与CD是否相交privateboolIsIntersection(Vector3A, Vector3B, Vector3C, Vector3D) { //A-D与C-D叉积结果大等于0时返回1 小于0时返回-1floatsign1=Mathf.Sign(Vector3.Cross(A-D, C-D).y); //B-D与C-D叉积结果大等于0时返回1 小于0时返回-1floatsign2=Mathf.Sign(Vector3.Cross(B-D, C-D).y); //C-A与B-A叉积结果大等于0时返回1 小于0时返回-1floatsign3=Mathf.Sign(Vector3.Cross(C-A, B-A).y); //D-A与B-A叉积结果大等于0时返回1 小于0时返回-1floatsign4=Mathf.Sign(Vector3.Cross(D-A, B-A).y); //AB与CD相交返回true 否则返回falsereturn!Mathf.Approximately(sign1, sign2) &&!Mathf.Approximately(sign3, sign4); } privatevoidOnDrawGizmos() { if (points.Length<3||point==null) return; boolflag=IsInRange(points, point.position); Handles.Label(point.position, "A"); Vector3half01= (points[0].position+points[1].position) * .5f; half01+= (half01-point.position).normalized*100000; for (inti=0; i<points.Length; i++) { vara=points[i%points.Length]; varb=points[(i+1) %points.Length]; Handles.color=flag?Color.cyan : Color.red; Handles.DrawLine(a.position, b.position); Handles.Label(points[i].position, $"顶点{i + 1}"); Handles.color=Color.yellow; if (IsIntersection(a.position, b.position, point.position, half01)) { Handles.DrawLine(point.position, half01); } } } }
3.如何判断一个点是否在一个凸边体范围内(3D):
上述部分我们在xz轴所在的平面构建了一个凸边形,现在我们给其一个高度,即可构成一个凸边体空间区域:
要判断一个点是否在该凸边体范围内,只需要在满足处于xz轴所在的凸边形范围内的同时,其坐标点的y值既小等于凸边体height高度值的一半,又大等于负的高度值的一半:
封装判断函数:
//判断点A是否在凸边体范围内privateboolIsInRange(Transform[] points, floatheight, Vector3A) { boolflag=true; flag&=A.y<=height* .5f; flag&=A.y>=-height* .5f; flag&=IsInRange(points, A); returnflag; }
测试代码如下:
usingUnityEngine; usingUnityEditor; publicclassExample : MonoBehaviour{ [SerializeField] privateTransformpoint; [SerializeField] privateTransform[] points; //凸边型顶点集合 [SerializeField] privatefloatheight=1f; //判断点A是否在凸边体范围内privateboolIsInRange(Transform[] points, floatheight, Vector3A) { boolflag=true; flag&=A.y<=height* .5f; flag&=A.y>=-height* .5f; flag&=IsInRange(points, A); returnflag; } //判断点A是否在凸边型范围内privateboolIsInRange(Transform[] points, Vector3A) { //取第一条边中点Vector3half01= (points[0].position+points[1].position) * .5f; //中点延伸(射线)half01+= (half01-A).normalized*100000; //用于记录交点的个数intcount=0; //遍历for (inti=0; i<points.Length; i++) { vara=points[i%points.Length]; varb=points[(i+1) %points.Length]; //判断是否相交if (IsIntersection(a.position, b.position, A, half01)) count++; } //交点个数为奇数则表示点A在凸边型范围内returncount%2==1; } //判断AB与CD是否相交privateboolIsIntersection(Vector3A, Vector3B, Vector3C, Vector3D) { //A-D与C-D叉积结果大等于0时返回1 小于0时返回-1floatsign1=Mathf.Sign(Vector3.Cross(A-D, C-D).y); //B-D与C-D叉积结果大等于0时返回1 小于0时返回-1floatsign2=Mathf.Sign(Vector3.Cross(B-D, C-D).y); //C-A与B-A叉积结果大等于0时返回1 小于0时返回-1floatsign3=Mathf.Sign(Vector3.Cross(C-A, B-A).y); //D-A与B-A叉积结果大等于0时返回1 小于0时返回-1floatsign4=Mathf.Sign(Vector3.Cross(D-A, B-A).y); //AB与CD相交返回true 否则返回falsereturn!Mathf.Approximately(sign1, sign2) &&!Mathf.Approximately(sign3, sign4); } privatevoidOnDrawGizmos() { if (points.Length<3||point==null) return; boolflag=IsInRange(points, height, point.position); Handles.color=flag?Color.cyan : Color.red; Handles.Label(point.position, "A"); for (inti=0; i<points.Length; i++) { Handles.Label(points[i].position-newVector3(0, height* .5f, 0), $"顶点{i + 1}"); Handles.Label(points[i].position+newVector3(0, height* .5f, 0), $"顶点{i + 1 + points.Length}"); vara=points[i%points.Length]; varb=points[(i+1) %points.Length]; varminA=a.position-newVector3(0, height* .5f, 0); varmaxA=a.position+newVector3(0, height* .5f, 0); varminB=b.position-newVector3(0, height* .5f, 0); varmaxB=b.position+newVector3(0, height* .5f, 0); Handles.DrawAAPolyLine(minA, minB); Handles.DrawAAPolyLine(maxA, maxB); Handles.DrawAAPolyLine(minA, maxA); Handles.DrawAAPolyLine(minB, maxB); } } }
4.如何判断一个物体是否在一个凸边体范围内:
上述部分判断的是一个坐标点是否在一个凸边体范围内,要判断一个物体是否被该凸边体区域所囊括,需要获取该物体及其子物体构成的Bounds边界盒,如果Bounds边界盒的每一个顶点都在该凸边体范围内,则可以大致推断该物体被这个凸边体所囊括,当然要想更精确还是要获取该物体的更为精确的边界点,在这里不做延申。
首先来看Unity圣典中关于Bounds边界盒及其核心变量的介绍:
其中max、min分别是最大、最小点,可以通过这两点获取到其它各顶点的坐标,测试代码如下:
usingUnityEngine; usingUnityEditor; publicclassExample : MonoBehaviour{ [SerializeField] privateGameObjectobj; privatevoidOnDrawGizmos() { if (obj==null) return; Boundsbounds=obj.GetComponent<MeshRenderer>().bounds; Vector3point1=bounds.min; Vector3point2=bounds.max; Vector3point3=newVector3(point1.x, point1.y, point2.z); Vector3point4=newVector3(point1.x, point2.y, point1.z); Vector3point5=newVector3(point2.x, point1.y, point1.z); Vector3point6=newVector3(point1.x, point2.y, point2.z); Vector3point7=newVector3(point2.x, point1.y, point2.z); Vector3point8=newVector3(point2.x, point2.y, point1.z); Handles.DrawLine(point6, point2); Handles.DrawLine(point2, point8); Handles.DrawLine(point8, point4); Handles.DrawLine(point4, point6); Handles.DrawLine(point3, point7); Handles.DrawLine(point7, point5); Handles.DrawLine(point5, point1); Handles.DrawLine(point1, point3); Handles.DrawLine(point6, point3); Handles.DrawLine(point2, point7); Handles.DrawLine(point8, point5); Handles.DrawLine(point4, point1); Handles.Label(point1, "顶点1"); Handles.Label(point2, "顶点2"); Handles.Label(point3, "顶点3"); Handles.Label(point4, "顶点4"); Handles.Label(point5, "顶点5"); Handles.Label(point6, "顶点6"); Handles.Label(point7, "顶点7"); Handles.Label(point8, "顶点8"); } }
一个物体可能包含若干个带有MeshRenderer组件的子物体,因此我们要获取一个囊括所有的Bounds边界盒,要使用到Bounds类中的Encapsulate函数:
usingUnityEngine; usingUnityEditor; publicclassExample : MonoBehaviour{ [SerializeField] privateGameObjectobj; privatevoidOnDrawGizmos() { if (obj==null) return; Boundsbounds=newBounds(Vector3.zero, Vector3.zero); //获取所有MeshRenderer 包括子物体varmrs=obj.GetComponentsInChildren<MeshRenderer>(true); Vector3center=Vector3.zero; for (inti=0; i<mrs.Length; i++) { center+=mrs[i].bounds.center; //Encapsulate函数重新计算boundsbounds.Encapsulate(mrs[i].bounds); } Vector3point1=bounds.min; Vector3point2=bounds.max; Vector3point3=newVector3(point1.x, point1.y, point2.z); Vector3point4=newVector3(point1.x, point2.y, point1.z); Vector3point5=newVector3(point2.x, point1.y, point1.z); Vector3point6=newVector3(point1.x, point2.y, point2.z); Vector3point7=newVector3(point2.x, point1.y, point2.z); Vector3point8=newVector3(point2.x, point2.y, point1.z); Handles.DrawLine(point6, point2); Handles.DrawLine(point2, point8); Handles.DrawLine(point8, point4); Handles.DrawLine(point4, point6); Handles.DrawLine(point3, point7); Handles.DrawLine(point7, point5); Handles.DrawLine(point5, point1); Handles.DrawLine(point1, point3); Handles.DrawLine(point6, point3); Handles.DrawLine(point2, point7); Handles.DrawLine(point8, point5); Handles.DrawLine(point4, point1); Handles.Label(point1, "顶点1"); Handles.Label(point2, "顶点2"); Handles.Label(point3, "顶点3"); Handles.Label(point4, "顶点4"); Handles.Label(point5, "顶点5"); Handles.Label(point6, "顶点6"); Handles.Label(point7, "顶点7"); Handles.Label(point8, "顶点8"); } }
封装判断函数:
//判断一个物体是否在凸边体范围内privateboolIsInRange(Transform[] points, floatheight, GameObjectobj) { Boundsbounds=newBounds(Vector3.zero, Vector3.zero); //获取所有MeshRenderer 包括子物体varmrs=obj.GetComponentsInChildren<MeshRenderer>(true); Vector3center=Vector3.zero; for (inti=0; i<mrs.Length; i++) { center+=mrs[i].bounds.center; //Encapsulate函数重新计算boundsbounds.Encapsulate(mrs[i].bounds); } Vector3min=bounds.min; Vector3max=bounds.max; returnIsInRange(points, height, min) &&IsInRange(points, height, max) &&IsInRange(points, height, newVector3(min.x, min.y, max.z)) &&IsInRange(points, height, newVector3(min.x, max.y, min.z)) &&IsInRange(points, height, newVector3(max.x, min.y, min.z)) &&IsInRange(points, height, newVector3(min.x, max.y, max.z)) &&IsInRange(points, height, newVector3(max.x, min.y, max.z)) &&IsInRange(points, height, newVector3(max.x, max.y, min.z)); }
测试代码如下:
usingUnityEngine; usingUnityEditor; publicclassExample : MonoBehaviour{ [SerializeField] privateGameObjectobj; [SerializeField] privateTransform[] points; //凸边型顶点集合 [SerializeField] privatefloatheight=1f; //判断一个物体是否在凸边体范围内privateboolIsInRange(Transform[] points, floatheight, GameObjectobj) { Boundsbounds=newBounds(Vector3.zero, Vector3.zero); //获取所有MeshRenderer 包括子物体varmrs=obj.GetComponentsInChildren<MeshRenderer>(true); Vector3center=Vector3.zero; for (inti=0; i<mrs.Length; i++) { center+=mrs[i].bounds.center; //Encapsulate函数重新计算boundsbounds.Encapsulate(mrs[i].bounds); } Vector3min=bounds.min; Vector3max=bounds.max; returnIsInRange(points, height, min) &&IsInRange(points, height, max) &&IsInRange(points, height, newVector3(min.x, min.y, max.z)) &&IsInRange(points, height, newVector3(min.x, max.y, min.z)) &&IsInRange(points, height, newVector3(max.x, min.y, min.z)) &&IsInRange(points, height, newVector3(min.x, max.y, max.z)) &&IsInRange(points, height, newVector3(max.x, min.y, max.z)) &&IsInRange(points, height, newVector3(max.x, max.y, min.z)); } //判断点A是否在凸边体范围内privateboolIsInRange(Transform[] points, floatheight, Vector3A) { boolflag=true; flag&=A.y<=height* .5f; flag&=A.y>=-height* .5f; flag&=IsInRange(points, A); returnflag; } //判断点A是否在凸边型范围内privateboolIsInRange(Transform[] points, Vector3A) { //取第一条边中点Vector3half01= (points[0].position+points[1].position) * .5f; //中点延伸(射线)half01+= (half01-A).normalized*100000; //用于记录交点的个数intcount=0; //遍历for (inti=0; i<points.Length; i++) { vara=points[i%points.Length]; varb=points[(i+1) %points.Length]; //判断是否相交if (IsIntersection(a.position, b.position, A, half01)) count++; } //交点个数为奇数则表示点A在凸边型范围内returncount%2==1; } //判断AB与CD是否相交privateboolIsIntersection(Vector3A, Vector3B, Vector3C, Vector3D) { //A-D与C-D叉积结果大等于0时返回1 小于0时返回-1floatsign1=Mathf.Sign(Vector3.Cross(A-D, C-D).y); //B-D与C-D叉积结果大等于0时返回1 小于0时返回-1floatsign2=Mathf.Sign(Vector3.Cross(B-D, C-D).y); //C-A与B-A叉积结果大等于0时返回1 小于0时返回-1floatsign3=Mathf.Sign(Vector3.Cross(C-A, B-A).y); //D-A与B-A叉积结果大等于0时返回1 小于0时返回-1floatsign4=Mathf.Sign(Vector3.Cross(D-A, B-A).y); //AB与CD相交返回true 否则返回falsereturn!Mathf.Approximately(sign1, sign2) &&!Mathf.Approximately(sign3, sign4); } privatevoidOnDrawGizmos() { if (points.Length<3||obj==null) return; boolflag=IsInRange(points, height, obj); Handles.color=flag?Color.cyan : Color.red; for (inti=0; i<points.Length; i++) { Handles.Label(points[i].position-newVector3(0, height* .5f, 0), $"顶点{i + 1}"); Handles.Label(points[i].position+newVector3(0, height* .5f, 0), $"顶点{i + 1 + points.Length}"); vara=points[i%points.Length]; varb=points[(i+1) %points.Length]; varminA=a.position-newVector3(0, height* .5f, 0); varmaxA=a.position+newVector3(0, height* .5f, 0); varminB=b.position-newVector3(0, height* .5f, 0); varmaxB=b.position+newVector3(0, height* .5f, 0); Handles.DrawAAPolyLine(minA, minB); Handles.DrawAAPolyLine(maxA, maxB); Handles.DrawAAPolyLine(minA, maxA); Handles.DrawAAPolyLine(minB, maxB); } Boundsbounds=newBounds(Vector3.zero, Vector3.zero); //获取所有MeshRenderer 包括子物体varmrs=obj.GetComponentsInChildren<MeshRenderer>(true); Vector3center=Vector3.zero; for (inti=0; i<mrs.Length; i++) { center+=mrs[i].bounds.center; //Encapsulate函数重新计算boundsbounds.Encapsulate(mrs[i].bounds); } Vector3point1=bounds.min; Vector3point2=bounds.max; Vector3point3=newVector3(point1.x, point1.y, point2.z); Vector3point4=newVector3(point1.x, point2.y, point1.z); Vector3point5=newVector3(point2.x, point1.y, point1.z); Vector3point6=newVector3(point1.x, point2.y, point2.z); Vector3point7=newVector3(point2.x, point1.y, point2.z); Vector3point8=newVector3(point2.x, point2.y, point1.z); Handles.color=Color.yellow; Handles.DrawLine(point6, point2); Handles.DrawLine(point2, point8); Handles.DrawLine(point8, point4); Handles.DrawLine(point4, point6); Handles.DrawLine(point3, point7); Handles.DrawLine(point7, point5); Handles.DrawLine(point5, point1); Handles.DrawLine(point1, point3); Handles.DrawLine(point6, point3); Handles.DrawLine(point2, point7); Handles.DrawLine(point8, point5); Handles.DrawLine(point4, point1); } }