상세 컨텐츠

본문 제목

FT_Newton: Physics Loop의 마지막 단계 Solve - (8)

Computer Graphics/FT_Newton

by Banjosh 2025. 1. 16. 11:41

본문

Physics Loop의 마지막 단계인 Solve에 대해 알아보자

이전 단계인 Narrow Phase를 통해 우리는 충돌마다 Manifold들을 생성할 수 있었다.

struct Manifold
{
    float seperation;      // 충돌 깊이
    glm::vec3 normal;      // 충돌 방향
    glm::vec3 pointA;      // 오브젝트 A의 충돌 지점
    glm::vec3 pointB;      // 오브젝트 B의 충돌 지점
};

 

Solve 단계에서는 위 정보를 토대로 VelocityConstraintsPositionConstraints를 처리한다.


VelocityConstraints와 PositionConstraints란?

1. VelocityConstraints

 VelocityConstraints는 두 물체가 충돌한 경우 발생한 충격량에 따라 속도를 변화시켜주는 것을 말한다. 이 Solve VelocityConstraint 단계에서는 충돌 정보인 Manifold뿐 아니라, 두 물체의 질량, 관성 텐서, 상대 속도 등 물리적 속성들에 의해서도 결과 값이 달라진다.

2. PositionConstraints

 PositionConstraints는 충돌 시 두 물체의 겹침을 해소시켜주는 것을 말한다. 실제 세계의 물리와는 다르게, 물리 엔진에서는 프레임 별로 물체를 이동하다 보면 두 물체가 겹침이 발생하고 이를 충돌했다고 본다. 따라서 Narrow Phase에서 생성한 Manifold의 정보와 물체의 물리적 속성을 이용하여 두 물체를 분리시켜 겹침을 해소시켜 자연스러운 처리를 해야 한다.


Solve 처리 과정

void Island::solve(float duration)
{
    int32_t bodyLength = m_bodies.size();
    m_positions.resize(bodyLength);
    m_velocities.resize(bodyLength);

    for (int32_t i = 0; i < bodyLength; i++)
    {
        Rigidbody *body = m_bodies[i];

        m_positions[i].position = body->getPosition();
        m_positions[i].orientation = body->getOrientation();
        m_velocities[i].linearVelocity = body->getLinearVelocity();
        m_velocities[i].angularVelocity = body->getAngularVelocity();
    }

    ContactSolver contactSolver(duration, m_contacts, m_positions, m_velocities);

    for (int32_t i = 0; i < VELOCITY_ITERATION; ++i)
    {
        contactSolver.solveVelocityConstraints(VELOCITY_ITERATION);
    }

    for (int32_t i = 0; i < POSITION_ITERATION; ++i)
    {
        bool isConstraintSolved = contactSolver.solvePositionConstraints(POSITION_ITERATION);

        if (isConstraintSolved)
        {
            break;
        }
    }

    for (int32_t i = 0; i < bodyLength; ++i)
    {
        Rigidbody *body = m_bodies[i];
        body->updateSweep();
        body->setPosition(m_positions[i].position);
        body->setOrientation(m_positions[i].orientation);
        body->setLinearVelocity(m_velocities[i].linearVelocity);
        body->setAngularVelocity(m_velocities[i].angularVelocity);
        body->synchronizeFixtures();
    }
}

 

위 코드는 Island 별로 Solve를 진행하는 코드로 정리하면 다음과 같다:

  1. 현재 Island에 포함되어 있는 오브젝트들의 위치, 각도, 속도, 각속도 값을 복사
  2. VelocityIteration 만큼 VelocityConstraint 처리 진행
  3. PositionIteration 만큼 PositionConstraint 처리 진행 (도중에 모든 겹침이 해소되는 경우 break)
  4. 변경된 위치, 각도, 속도, 각속도를 업데이트

Solve Constraint의 세부 처리

solveVelocityConstraints()

  1. Normal Impulse 구하기
  2. Normal Impulse로 발생할 수 있는 maxFriction 구하기
  3. Tangent Impulse 구하기
  4. Tangent Impulse를 [−maxFriction ~ maxFriction] 범위로 clamping
  5. Normal Impulse에 의한 속도, 각속도 변화 구하기
  6. Tangent Impulse에 의한 속도, 각속도 변화 구하기

solvePositionConstraints()

  1. 물체 A와 B의 질량 비 구하기
  2. 충돌 깊이를 질량 비에 반비례하게 나누어 A와 B의 이동 거리 구하기
  3. A와 B는 각자의 충돌 방향 반대로 이동
  4. 겹침이 해소되었는지 확인

Solve Constraint를 Iteration 만큼 반복하는 이유

만약 Island의 물체가 A, B, C 3개이고 이를 반복 없이 한 번에 처리한다고 가정해보자:

  • 충돌 1: A - B
  • 충돌 2: B - C

 위와 같이 충돌이 발생한 경우 먼저 충돌 1을 처리하면 AB의 속도, 각속도 혹은 위치, 각도가 변경될 것이다. 그 후 충돌 2를 처리하려고 보면, B는 이미 충돌 1의 처리에 의해 충돌 당시에 비해 값이 너무 크게 바뀌어 버린 것을 볼 수 있고 충돌 2의 처리는 처음 충돌이 발생했을 때와는 다른 유형의 충돌이 되어버린다. 즉 그대로 충돌 2를 처리하면 부자연스러운 결과가 나오게 된다.

 

 실제로 처음 구현 당시 충돌 처리를 한 번에 처리를 하였더니 부자연스러운 결과가 나왔었고, 그때 왜 Box2D에서 충돌 처리를 Iteration 만큼 반복했는지 알 수 있었다. 그래서 현재는 Island의 모든 충돌을 반복적으로 순회처리함으로써 자연스러운 충돌 처리를 구현할 수 있게 되었다. 


추가할 만한 부분

실제 Box2D에서는 다음과 같은 추가 과정을 통해 더욱 자연스러운 결과를 만든다

  1. Warm Start: 이전 프레임의 Impulse 값을 저장해 초기 충돌 계산에 활용.
  2. Impulse 누적: NormalImpulse와 TangentImpulse를 누적해서 계산하여 반복 계산의 정확성을 높임.

현재는 기본적인 기능만을 구현하기 위해 이러한 부분들을 제외했지만, 최적화가 완료된 후 기능 업데이트를 위한 시간이 생긴다면 추가로 고려해볼 만한 부분인 것 같다.

관련글 더보기