2d物理引擎学习 - 解决接触面穿透问题

就是一个物体掉落到另一个物体表面时,相互穿透,部分区域发生了重叠:

  

 

原因

现实世界,时间是连续的;但在计算机中,时间是离散的,帧与帧之间的时间间隔,没法保证两物体发生碰撞时,正好是表面刚接触。

比如:物体1和物体2相距6米,大小均为1x1,物体1向右以10米/s速度移动,物体2静止,0.5s后物体1和物体2的边缘正好刚发生接触。

同样的时间间隔,如果物体1是以12米/s的速度向右移动的,那0.5s后物体1和物体2就会有1米的穿透距离(重叠区域)。

 

解决办法

碰撞时,检查是否发生穿透,发生穿透我们就加一个修正用的速度让它们分离开。

这个速度用多大呢?v = 穿透距离 / 帧间隔时间

同时,为了防止用力过大,我们会加一个百分比参数来控制修正的程度:v = 修正程度 * 穿透距离 / 帧间隔时间

物理引擎中常用的方法叫:Baumgarte Stabilization

 

变化代码

//碰撞点信息
public class ContactInfo
{
    public Vector2 m_Point; //碰撞点
    public Vector2 m_Normal; //碰撞法向量(分离方向), 这边用A指向B, 即: B反弹方向
    public float m_Penetration; //穿透深度(分离距离)
    public uint m_Fp; //用于识别两box的碰撞

    public float m_ImpulseNormal; //法线方向累加冲量
    public float m_ImpulseTangent; //切线方向累加冲量

    public float m_MassNormal; //碰撞后速度计算公式中会用到: 1/m1+1/m2, 这里存放的就是这个结果
    public float m_MassTangent;

    public float m_Bias; //穿透修正速度
}

 

MyPhysics的PreSeperation和PostSeperation变化了少量代码

private void PreSeperation(float dt, CollisionPair collisionPair)
{
    const float allowedPenetration = 0.01f;
    const float biasFactor = 0.2f;
    float invDt = 1 / dt;

    var rigidbodyA = collisionPair.m_RigidbodyA;
    var rigidbodyB = collisionPair.m_RigidbodyB;

    for (int i = 0; i < collisionPair.m_NumContacts; ++i)
    {
        var contact = collisionPair.m_Contacts[i];
        Vector2 ra = contact.m_Point - rigidbodyA.Position;
        Vector2 rb = contact.m_Point - rigidbodyB.Position;

        Vector2 normal = contact.m_Normal;
        Vector2 tangent = new Vector2(normal.y, -normal.x); //切线(顺时针)

        float kMassNormal = rigidbodyA.InvMass + rigidbodyB.InvMass;
        float raN = Vector2.Dot(ra, normal);
        float rbN = Vector2.Dot(rb, normal);
        kMassNormal += rigidbodyA.InvInertia * (Vector2.Dot(ra, ra) - raN * raN) + rigidbodyB.InvInertia * (Vector2.Dot(rb, rb) - rbN * rbN);            
        contact.m_MassNormal = 1 / kMassNormal;

        float kMassTangent = rigidbodyA.InvMass + rigidbodyB.InvMass;
        float raT = Vector2.Dot(ra, tangent);
        float rbT = Vector2.Dot(rb, tangent);
        kMassTangent += rigidbodyA.InvInertia * (Vector2.Dot(ra, ra) - raT * raT) + rigidbodyB.InvInertia * (Vector2.Dot(rb, rb) - rbT * rbT);
        contact.m_MassTangent = 1 / kMassTangent;

        contact.m_Bias = -biasFactor * invDt * Mathf.Min(0, contact.m_Penetration + allowedPenetration);

        if (m_AccumulateImpulses)
        {
            Vector2 impulse = contact.m_ImpulseNormal * normal; //冲量大小转成冲量向量
            impulse += contact.m_ImpulseTangent * tangent; //切线方向

            rigidbodyA.ApplyImpulse(-impulse);
            rigidbodyA.ApplyTorqueImpulse(ra, -impulse);

            rigidbodyB.ApplyImpulse(impulse);
            rigidbodyB.ApplyTorqueImpulse(rb, impulse);
        }
    }
}


private void PostSeperation(float dt, CollisionPair collisionPair)
{
    var rigidbodyA = collisionPair.m_RigidbodyA;
    var rigidbodyB = collisionPair.m_RigidbodyB;
    float mergeFriction = Mathf.Sqrt(rigidbodyA.Friction * rigidbodyB.Friction);

    for (int i = 0; i < collisionPair.m_NumContacts; ++i)
    {
        var contact = collisionPair.m_Contacts[i];
        Vector2 ra = contact.m_Point - rigidbodyA.Position;
        Vector2 rb = contact.m_Point - rigidbodyB.Position;
        var relativeV = rigidbodyB.GetPointVelocity(rb) - rigidbodyA.GetPointVelocity(ra);

        var normal = contact.m_Normal;
        float relativeVN = Vector2.Dot(relativeV, normal); //投影到法向量
        //if (relativeVN > 0) //相对速度>0时, 表明没有碰撞趋势了
        //    return;

        //Δp = (1 + e) * (v2 - v1) / kMass
        float deltaPN = (-relativeVN + contact.m_Bias) * contact.m_MassNormal;

        if (m_AccumulateImpulses)
        {float lastImpulseN = contact.m_ImpulseNormal;
            contact.m_ImpulseNormal += deltaPN; //叠加本次冲量(冲量=Δp)
            if (contact.m_ImpulseNormal <= 0) //防止弹开过程中, 变成拉回来的冲量
            {
                contact.m_ImpulseNormal = 0;
                deltaPN = -lastImpulseN;
            }
        }
        else
        {
            deltaPN = Mathf.Max(deltaPN, 0); //冲量为负, 碰撞后就加速了, 这样不对
        }

        Vector2 impulseN = deltaPN * normal; //转为矢量
        rigidbodyA.ApplyImpulse(-impulseN);
        rigidbodyA.ApplyTorqueImpulse(ra, -impulseN);

        rigidbodyB.ApplyImpulse(impulseN);
        rigidbodyB.ApplyTorqueImpulse(rb, impulseN);

        //摩擦力(切线方向)
        relativeV = rigidbodyB.GetPointVelocity(rb) - rigidbodyA.GetPointVelocity(ra); //上面的冲量生效后, 再计算相对速度

        var tangent = new Vector2(normal.y, -normal.x); //切线(顺时针)
        float relativeVT = Vector2.Dot(relativeV, tangent); //投影到切线方向
        relativeVT = -relativeVT; //取反让值为正(与运动方向相同)
        float deltaPT = relativeVT * contact.m_MassTangent;

        if (m_AccumulateImpulses)
        {float maxDeltaPt = mergeFriction * contact.m_ImpulseNormal; //不能超过法向量的冲量
            float lastImpulseT = contact.m_ImpulseTangent;
            contact.m_ImpulseTangent = Mathf.Clamp(contact.m_ImpulseTangent + deltaPT, -maxDeltaPt, maxDeltaPt);
            deltaPT = contact.m_ImpulseTangent - lastImpulseT;
        }
        else
        {
            float maxDeltaPt = mergeFriction * deltaPN;
            deltaPT = Mathf.Clamp(deltaPT, -maxDeltaPt, maxDeltaPt);
        }

        Vector2 impulseT = deltaPT * tangent;
        rigidbodyA.ApplyImpulse(-impulseT); //摩擦力与运动(趋势)方向相反
        rigidbodyA.ApplyTorqueImpulse(ra, -impulseT);

        rigidbodyB.ApplyImpulse(impulseT);
        rigidbodyB.ApplyTorqueImpulse(rb, impulseT);
    }
}

 

参考

2D 游戏物理引擎 - 碰撞约束 - 知乎 (zhihu.com)

从零手写游戏引擎24:物理引擎ResolvePhase - 知乎 (zhihu.com)

 

posted @ 2024-01-14 23:21  yanghui01  阅读(88)  评论(0编辑  收藏  举报