OBB碰撞及四叉树优化

记录 专栏收录该内容
1 篇文章 0 订阅

项目最近在移植lua,考虑到后续会用到帧同步,就自告奋勇,尝试写一下碰撞系统,原来实现过AABB的碰撞,OBB碰撞就是在AABB的碰撞上加了个旋转,可理解为有向的AABB(AABB为轴对其,可理解为在同一坐标系中,各碰撞的正方向轴都一样),下图为AABB与OBB区别

AABB与OBB区别
刚开始做的时候把这个碰撞想的太简单,没有经过深思熟虑,直接就开始写,结果导致越写越乱,越写东西越多,写代码还是得先设计!

AABB碰撞检测

1.AABB构造:
矩形包围盒可采用:
①中心点加宽高
②者对角线两个点。(这是小学还是初中数学知识记不清了,确定一个矩形的方法)。
方便更新的话,我采用了中心点加宽高的构造,代码中物体变了位置,就改变一个position就行(AABB的center默认为position)

球形包围盒:圆心center,半径R

2.AABB相交;
①矩形与矩形相交:
在这里插入图片描述
其中min与max 都是根据AABB自身计算出的数值

②矩形与球形相交:
找到球与矩形最近的一点,最近的一点都没有相交,则不相交。这个转换需要自己想一会,刚开始我也没想明白,想明白了就好了。
 if (other.Shape == ShapeType.Sphere)
            {
                ColliderSphere sphere = other as ColliderSphere;

                FixPoint x = sphere.Position.x;
                FixPoint y = sphere.Position.y;
                FixPoint z = sphere.Position.z;

                if (x > MaxX) { x = MaxX; }
                if (x < MinX) { x = MinX; }

                if (y > MaxY) { y = MaxY; }
                if (y < MinY) { y = MinY; }

                if (z > MaxZ) { z = MaxZ; }
                if (z < MinZ) { z = MinZ; }

                FixPoint distance = (x - sphere.Position.x) * (x - sphere.Position.x) + 
                    (y - sphere.Position.y) * (y - sphere.Position.y) + 
                    (z - sphere.Position.z) * (z - sphere.Position.z);

                if (sphere.Radius * sphere.Radius >= distance)
                {
                    return true;
                }
                return false;
            }

③圆与圆相交:无需多言

if(other.Shape == ShapeType.Sphere)
            {
                Vector3FP other_pos = other.Position;
                return Position.Distance(ref other_pos) <= Radius + (other as ColliderSphere).Radius;
            }

AABB相交到此结束

OBB构造:

球旋转后还是自身,当做没旋转

长方体构造:
①8个顶点
②6个面
③3组平行面
④一个顶点和三个彼此正交的变相量
⑤中心点,旋转矩阵,3个1/2边长

一般都采用第五种构造方式,因为计算比较方便,但是内存也要考虑,一般都是储存欧拉角或者四元数代替旋转矩阵,在做相交测试时,又得转换回矩阵,得不偿失

一种较好的妥协方式是:只存储旋转矩阵中的两个轴,只是在测试时利用叉积计算第三个轴,相对来讲,能够降低CPU的操作开销,还能节省三个浮点数分量,降低了百分之20的内存消耗
–《实时检测碰撞算法技术》第4.4章

OBB相交:

抽象为分离轴测试,分离轴测试:对于某一轴L,如果两盒体投影半径之和小于中心点之间的投影距离,则OBB处于分离状态
在这里插入图片描述下面列出c#代码(刚开始做的时候没有想太多,在网上扒了一份,自己得代码就不贡献了,我没有共享精神,写这个博客也记录一下这个工作).

public class ObbNew : MonoBehaviour
{
    [HideInInspector]
    public Transform m_Transform;
    [HideInInspector]
    public BoxCollider m_BoxCollider;
    [HideInInspector]
    private float m_Rotation;
    [HideInInspector]
    public Vector2 m_Extents;
    [HideInInspector]
    public Vector2[] m_Axiss;

  

    void Start()
    {
        m_Transform = this.transform;
        m_BoxCollider = m_Transform.GetComponent<BoxCollider>();
        m_Axiss = new Vector2[2];
        SetExtents();
    }

    // Update is called once per frame
    void Update()
    {
        m_Rotation = m_Transform.eulerAngles.y * Mathf.PI / 180;

        //两个轴
        m_Axiss[0] = new Vector2((float)Mathf.Cos(m_Rotation), -(float)Mathf.Sin(m_Rotation));  
        m_Axiss[1] = new Vector2(Mathf.Sin(m_Rotation), Mathf.Cos(m_Rotation));

    }

    private void SetExtents()
    {
        Quaternion rotation = m_Transform.rotation;
        m_Transform.rotation = new Quaternion(0, 0, 0, 1);

        Vector3 center = m_BoxCollider.center;
        Vector3 size = m_BoxCollider.size / 2;

        Vector3 Point1 = new Vector3(center.x + size.x, center.y, center.z - size.z);
        Vector3 Point2 = new Vector3(center.x - size.x, center.y, center.z + size.z);


        Point1 = m_Transform.TransformPoint(Point1);
        Point2 = m_Transform.TransformPoint(Point2);

        m_Extents = new Vector2(Mathf.Abs(Point1.x - Point2.x) / 2, Mathf.Abs(Point2.z - Point1.z) / 2);
        m_Transform.rotation = rotation;


    }

    public float dot(Vector2 a, Vector2 b)
    {
        return Mathf.Abs(a.x * b.x + a.y * b.y);
    }
    public float getProjectionRadius(Vector2 axis)
    {
        return (m_Extents.x * dot(m_Axiss[0], axis) + m_Extents.y * dot(m_Axiss[1], axis));
    }

    public bool intersects(ObbNew other)
    {
        Update();
        other.Update();
        Vector2 distanceVector = new Vector2(m_Transform.position.x - other.m_Transform.position.x, m_Transform.position.z - other.m_Transform.position.z);

        Vector2[] checkObbVector2s =
        {
            m_Axiss[0],
            m_Axiss[1],
            other.m_Axiss[0],
            other.m_Axiss[1],

           
        };
  
        for (int index = 0; index < checkObbVector2s.Length; index++)
        {
            Vector2 curVector2 = checkObbVector2s[index];
            if ((getProjectionRadius(curVector2) + other.getProjectionRadius(curVector2)) <= dot(distanceVector, curVector2))
            {
                return false;
            }
        }
        return true;
    }



}

这是能跑的代码,直接拉过来一个cube加个BoxCollider就能跑了,不像别的博客,代码都是烂的(吐槽一下大多数博客,代码都不能运行你在那分享啥呢,误人子弟。我被坑过不少次

球与OBB的碰撞 与上述的圆与矩形碰撞相似,不同的是将获取圆上离矩形最近一点改为球上与OBB
距离最近的一点
下面是伪代码

//给定点p,返回点q在(或在)OBB b上,最接近p
Vector2 ClosedPtPointToOBB(Point p,OBB b)
{
Vector q 
Vector2 d = p -b.center

q = b.center
//2d只考虑x,y  3d则改为3个轴
//遍历 OBB的 轴
for(i = 0; i< 2;i++)
{
//把d投影到轴上,得到距离
//从盒子中心沿d轴
float dis = Vector2.Dot(d,b.u[i])
If(dis >b.e[i] )
  Dis = b.e[i]
If(dis < -b.e[i])
dis = -b.e[i]
沿轴移动该距离以获得世界坐标
q +=dis * b.u[i]

}
return q
}

Test SphereOBB(Sphere s, OBB b )
{
Vector2 p = ClosedPtPointToOBB(s.center,b)
Vector2 v = p - s.center

Return Mathf.Dot(v,v) <=s.radius * s.radius
}

碰撞到此结束,主要是数学知识,从毕业一直写业务逻辑,数学知识都还给老师们了,写的我相当难受,用进废退,以后没事还是得看看数学,不能做一个沉迷于历史的程序员
好好学数学,说不定去做引擎了呢(微博狗头表情)

下面是四叉树,为何要用四叉树,上学的时候学这种枯燥的数据结构,一直提不起兴趣,当然,也没好好学,什么狗屁二叉树,叉你二大爷啊叉,有啥叉的,就分裂呗,为啥分裂,不知道,为啥要有二叉树,不知道,反正是感觉用不上,就没太在意过,然而为了应付不挂科,也稍微留了点意。

不是代码写完了能用了功能就完成了,跑起来就蒙蔽了,场景里500个墙,一发子弹打出去,每帧遍历循环这么多次,这还只是一发子弹,要是都动起来,这帧率,用不了,用不了。等于没写代码。

想了想,果然***前人总结的数据结构、算法、设计模式都不是白给的,都是趟过的坑***。

四叉树优化空间探测。不需要考虑物体的数量,只需考虑树的深度。时间复杂度这个概念也一下在我心里高大了起来

四叉树的教程有很多,定义原理方面我就不做赘述。
直说我的实现:
1.在obj插入到四叉树中时,会有象限判断,计算符合该obj插入象限的位置,总共四个象限

在这里插入图片描述可以插入任何形状:矩形、圆形、三角形,四叉树不关心obj本身的类型,只关心obj的GetAABB接口

其中GetIndex方法为判断obj的aabb具体属于哪个象限,每个象限都有固定的center和长宽确定位置
只需通过数学计算obj.GetAABB是否属于该象限:
1.三角形是否属于这个象限,三角形三个顶点都在这个象限,则该三角形属于这个象限
2.矩形是够属于这个象限,矩形的最大顶点和最小顶点是否在同一象限(对角线顶点)
3.圆是够在这个象限,圆心与矩形中心的距离与半径判断

通过递归,将该obj插入到最符合的位置,如果属于某个象限,就插入到某象限,如果属于跨象限,则记录在父节点上,不插入到子节点中

有插入就要有对应的删除,删除某obj就必须先找到对应的obj
递归获取到该obj,再删除该obj
在这里插入图片描述在编写博客的时候顺便优化了下,原来是if self.objects[i].collider.entityid == obj.collider.entityid then
加了个GetID的接口,更加清晰整洁统一管理

目前是静态的四叉树,要让其动起来,就得需要update,但是考虑到很多静态场景碰撞盒不需要update变化,仅仅需要动起来的物体进行更新在四叉树中的位置
在这里插入图片描述
我项目当前运动碰撞体不是特别多,所以直接采用移除再插入方式,另一种方式是检查一下这个obj的位置变化后,如果还属于这个象限,那就不做操作,如果不属于,再修改,还是走一遍插入删除的逻辑,我这里直接为了方便就直接删除再插入了,各位看官要是有更好的优化意见可以告诉我

与碰撞相结合的地方就是优化遍历速度,不用跟场景中所有的碰撞盒一一检测,当前时间,与此obj可能发生碰撞的objs包括:与此obj同一象限内,此obj父节点的跨区域obj,父节点的父节点的跨区域obj(可能包围盒去也特别大)。
查找时,仅需定位该碰撞盒的所属区间,然后查找到其所有父节点,递归实现。

我的方案:
1.游戏初始化构造四叉树,深度定为8-10(综合项目地图和物体,这个深度需要根据不同情况变化)
2.实例化物体碰撞时,同步添加一份碰撞信息到树中。
3随着物体增多,树不断分裂
4.物体移动时,更新所在区域
5.检测碰撞时,找到这个物体可能碰撞的其余物体,四叉树递归
6.根据物体标签再进一步筛选可碰撞的list
7.碰撞检测.

总结:

1.写代码之前不设计,整个实现过程很慢,很浪费时间,如果在前***期设计规划一下,会很快的实现***。
2.数学呀!数学!
3.代码写完要优化,或者写的时候就要考虑到优化。我一直不信邪,一直被告诫c#和lua相互传递会很浪费效率,我就不以为意。直到500个碰撞体一起检测那一刹那,我的帧率快爆了在这里插入图片描述
在这里插入图片描述
就一个get_position就这么酸爽!

Entity通过Proto数据创建碰撞体,不要访问GameObject,碰撞体有自身相对的长宽高数据,再根据LogicTransform的Position,Rotation算出对应的顶点

相机也有与GameObject位置交互的地方,后续考虑还是得挪回到c#那里写,方案待定

  • 0
    点赞
  • 0
    评论
  • 3
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值