본문 바로가기

Programming/물리엔진/게임엔진

물리엔진 구현에 필요한 요소들 및 구현 팁(생각나는대로 추가 중)

<간단한 물리엔진의 구현에 있어서 고려해야 할 사항들>

 

#[간단한 물리엔진의 구성 요소]

1. 강체

2. 충돌체크

3. 충돌처리

 

[심화 된 물리엔진에 추가 될 요소]

4. 조인트

5. 파티클

6. 천 시뮬레이션

7. 소프트바디(젤리 같은 물체)

8. 액체 시뮬레이션

 

 

#[구현하기에 앞서 고려 해야 할 사항]

1. 움직임을 verlet 기반으로 구현 할 것인가(위치 기반), 일반적인 속도기반으로 구현 할 것인가

2. 선형동역학만 고려 할 것인가, 회전 동역학도 고려 할 것인가

(파티클, 3d에서의 구형 물체, 2d에서의 원형 물체인 경우 회전이 필요 없는 경우가 많음)

 

3. 충돌체크를 어떤 방식으로 할 것인가

(일반적인 물리엔진이라면 충돌체크의 결과로 충돌점(충돌 한 두 물체위의 각각의 충돌점을 구해 2개를 구할 수도 있음), 겹쳐진 길이(penetration depth), 충돌 법선벡터(contact normal) 이 필요)

-SAT

-바운딩구-바운딩구(혹은 각 모형 별 충돌체크 알고리즘 구현 - SAT도 여기에 포함 될 수 있음)

-GJK/EPA 혹은 GJK의 변형 알고리즘(각 모형별 알고리즘을 따로 구현 할 필요 없음)

-기타

 

4. 충돌체크에서 다중 충돌점을 어떻게 구할 것인가

(보통의 충돌체크 알고리즘은 하나의 충돌점만 구하게 됨)

(평면위의 육면체의 경우 충돌점이 4개(혹은 각 물체위의 충돌점을 각각 구할 경우 8개 필요))

-처음부터 모든 충돌점을 검출하는 충돌체크 알고리즘 사용

-여러프레임에 걸쳐 충돌점을 캐싱

-perturbation

 

5. 충돌처리를 어떤 방식으로 할 것인가

-constraint 기반(LCP solver 필요 - PGS가 안정적인듯?)

-impulse 기반(sequential impulse solver가 직관적임)

(인터넷을 뒤져보면 lcp 와 si는 수학적으로 동등하다는 글들이 있음)

(프로그래머의 입장에서 이해하기 쉬운것은 직관적인 si 임, lcp는 수학적 개념이 들어가서 복잡하지만 조금 더 안정적인듯?)

-spring 기반

 

6. tunneling 현상을 어떻게 해결 할 것인가?

-간단한 물리 시뮬레이션일 경우 강체가 일정 속도 이상을 넘지 못하게 제한을 걸수 있음(사실적이지 않음)

-ccd(continuous collision detection) 사용

-toi(time of impact)를 구해서 처리

 

7. 물리엔진의 결과물을 화면에 그릴 때 도구로써 어떤것을 사용 할 것인가

 - DirectX

 - OpenGL

 : 만약 DirectX 만을 사용 할 것이라면 Vector나 Matrix등의 클래스가 따로 필요 하지 않다. 하지만      DirectX 뿐 아니라 OpenGL이나 다른 것을 이용 하여 화면에 렌더링 할 생각 이면 DirectX에 무관해야 하므로 Vector 클래스나 Matrix(혹은 Quaternion 까지)클래스를 직접 구현 해야 한다.

보통의 물리엔진 이라면 lib나 dll 형태로 제공 혹은 사용 되므로 DirectX에 무관하게 어느 곳에서나 쓸수 있도록 구성 되어 있다. 그러므로 자체적으로 Vector, Matrix, Quaternion 클래스를 구현 한다.

 

 

 

#[강체의 구성 요소]

1. 질량 - F=ma에서 a = F/m으로 가속도를 구할 수 있음

-가속도 * 시간(a*t) = 속도(l)

-속도 * 시간(l*t) = 위치(p)

 

2. 회전관성(inertia tensor) - 선형동역학에서의 질량에 대비 될 수 있는 회전 동역학의 요소 - T = Iw 에서  w = T/i로 각가속도를 구할 수 있음(T는 토크, I는 inertia tensor)

-각가속도 * 시간(w*t) = 각속도(an)

-각속도 * 시간(an*t) = 현재의 회전 상태(전방/위/우 방향 벡터의 상태)

 

3. 선속도

4. 각속도

5. 강체의 무게 중심(위치 표현)

6. 강체의 지역 좌표계의 x,y,z축(한마디로 강체의 우/위/전방 벡터) - 선형 동역학에서 4번에 대응 되는 회전 동역학의 요소(회전을 위함)

7. 충돌체크를 위한 경계도형(OBB, AABB, 구 등등)

8. 강체의 회전 및 위치를 포함한 행렬 - 강체의 위치이동, 회전, 렌더링에 쓰이며 강체 자체의 고유 상태임

-보통 4 X 4 행렬로 표현

-엔진에 따라 쿼터니언으로 표현 하는 경우도 있음

-행렬 표현이 matrix(m00, m01, m02, m03,

                           m10, m11, m12, m13,

                           m20, m21, m22, m23,

                           m30, m31, m32, m33)

    이런식 이라면 

    -direct X의 경우 Row Vector on the left 형식 이므로

       우방향 벡터 = (m00, m01, m02)

       상향 벡터 = (m10, m11, m12)

       전방 벡터 = (m20, m21, m22)

       위치 = (m30, m31, m32)

 

     가 되며, 

    -openGL의 경우 Colume Vector on the right 형식 이므로

       우방향 벡터 = (m00, m10, m20)

       상향 벡터 = (m01, m11, m21)

       전방 벡터 = (m02, m12, m22)

       위치 = (m03, m13, m23)

     이 됨.

 

 

9. 추가 요소들 - 공기저항, 마찰력, 탄성계수, 운동에너지 와 sleep 상태, 움직일수 있는가, 충돌 할 수 있는가, 한 프레임 전의 행렬

 

 

 

#[강체의 구성요소 opcode]

float mass;

matrix inertiaTensor(혹은 vector inertiaTensor);

vector linVel;

vector angVel;

vector center;

vector right;  vector up;  vector forward;

Box boundingBox;

matrix state;

 

float airRes;

float friction;

float restitution;

float kineticMotion;

bool sleep;

bool movable;

bool collidable;

 

 

#[구현 팁]

1. main에서의 진행 순서가 중요하다.

- 선언 -> 각 수치 설정 -> 업데이트 -> 렌더링

- 보통 '선언' 및 '각 수치 설정' 은 최초 한번 수행 되며 '업데이트' 및 '렌더링'이 루프를 돌며 무한 히 수행 된다.

-업데이트 부분에서 충돌체크, 충돌처리, 제약조건 처리, 위치 이동 등의 각종 수치를 업데이트 한다.

-렌더링 부분에서 업데이트에서 변환 된 수치에 따라 화면에 그리게 된다. 그러므로 프레임 조절은 업데이트에서 하여 프레임이 너무 많이 나와서(속도가 빨라서) 속도 조절이 필요한 경우 업데이트에서 수치의 업데이트 속도를 늦추고 렌더링은 항상 하게 해야지 화면에 제대로 표시가 된다.

 

2. 업데이트에서의 순서가 중요하다. 

-충돌체크 -> 중력 적용으로 속도 변환(혹은 부력 등 지속적으로 작용하는 힘) -> (물체의 sleep 및 wake) -> 충돌 처리(각 충격력등의 적용으로 속도 변환) -> 각종 제약조건 처리(조인트 등) -> 변환 된 속도로 위치 업데이트

-위의 순서가 물리엔진의 특성에 따라 조금씩 달라질 수는 있지만 이 순서에 따라 다른결과가 나올 수 있다.(안정적인 stacking이 안된다던지 등등....)

 

3. SI 방식의 충돌처리에서 힘의 적용 방식이 중요하다.

-SI 방식을 이용 하여 지역적으로 각각의 충돌점에대해 충돌 처리를 하면 몇번의 iteration을 거쳐서 두 물체의 각 다중 충돌점에 올바른 충격력을 가하게 된다. 이 때 iteration이 보통 20~40번 정도 루프를 도는데 이 루프는 한프래임 내에서 도는 것이다. 그러므로 충돌처리 중에는 프래임이 변하지 않는다. 고로 엔진상에서의 시간도 변하지 않는다. 그렇기에 보통 중력을 적용할때 acceleration = gravity/mass 가 되어 velocity = velocity + acceleration * dt 가 될테지만 충돌해소를 위한 충격력을 적용 할 때는 dt만큼의 시간이 흐르지 않으므로 velocity = velocity + acceleration으로만 계산 해야 한다.

 

4. 충돌 처리 시 LCP 문제가 개입되는 이유

-충돌 처리시 적용되는 힘은 3가지 정도가 될 수 있다. 

   ㄱ. 물체가 더 이상 겹치지 않도록 상대속도가 0이 되게 밀어내는 힘(보통 물리책이나 인터넷을 찾아보면 나오는, 충돌에 관해 그 충돌의 충격력을 구하는 공식이 이 힘을 구하는 것이다.)

   ㄴ. 이미 겹쳐진 상태에서 penetration을 해결 하기 위해 normal 방향으로 penetration만큼 밀어 내  는 힘(현실에서는 일어나지 않는 현상이다.)

   ㄷ. 마찰력(충돌법선에 수직인 벡터 방향으로 힘을 줘야 한다.)

 

- 현실상에서 육면체가 평면 위에 있을 때, 육면체의 바닥 면 전체에 골고루, 바닦이 육면체를 미는 힘이 육면체에 적용되는 중력에 반하여 적용된다. 하지만 물리엔진에서는 이 면 자체가 무한의 선분들로 이루어져 있고, 이 선분들은 점들로 구성 되어있기에 이 모든 점을 저장 해 놓고 힘을 전부 적용 하는것이 불가능 하다. 그래서 보통 육면체 형태라면 바닦과 붙어있는 가장 끝점들인 각 모서리의 4개의 점들을 저장 해 두고 이 점들에 힘을 가하게 된다.

그런데 이 때 힘을 가할 때 문제가 된다. 현실이라면 이 4개 점에 동시에 같은 힘이 작용하고 있기 때문에 물체가 평면위에 올려져 있고 중력 외의 외부힘이 가해지지 않는 상태라면 Jittering 현상(물체의 떨림 현상이 일어나지 않는다.) 하지만 보통 물리엔진에서는 점1부터 점4까지 순차적으로 힘을 적용한다. 이 때 점1에 힘을 적용하면 점2,3,4의 상대속도는 변해 버리게 된다. 그러므로 점 2,3,4에 적용하는 힘이 힘을 적용할때 변하게 되어 버린다.

그렇기 때문에 각각의 점을 돌며 힘을 적용하는 지역적 방식 보다는 좀더 전역적으로 보고 한번에 알맞은 힘을 적용하는 global solver가 필요하다. 이를 구현 하기 위해 물리엔진 상의 모든 강체에 대해 물체끼리 더이상 파고 들지 않는 제약 조건, 물체가 충돌 했을때는 각 물체에 서로 밀어내는 힘인 척력만이 작용 해야 한다는 제약 조건등을 세워놓고 전체를 한번에 풀게 되며 일반인이 평소에 접하기 힘든 LCP 방정식이 세워지고 이를 풀어내어서 각 물체의 충돌 후 힘(혹은 속도)를 구해야 한다.

이 LCP 방정식을 푸는 방식에서 PGS나 SOR등의 방식이 사용 되며 과거의 물리엔진등은 대체로 이런식으로 구현을 하였다. 

 

하지만 LCP 방정식을 풀어야 하는 전역 적인 방식은 대충 훑어보면 '충돌에 관해 그 충돌의 충격력을 구하는 공식' 조차 사용 되지 않는것으로 보여(내부에 숨어 있기는 하다.) 직관적이지 않다.

그래서 이를 전역적으로 보지 말고, 각 물체의 다중 충돌점을 하나하나 돌며 '충돌에 관해 그 충돌의 충격력을 구하는 공식' 을 사용 하여 충돌을 지역적으로 해결하는 SI 알고리즘이 나오게 되었다.(Erin Catto가 GDC에서 발표 한것으로 알고 있다.)

이 방식은 각각의 충돌점에 알맞은 힘을 골고루 주기 위해 몇바퀴의 루프를 돌며(보통 20~30정도) 충격력을 주게 된다. 이 때 루프를 다 돌았을때 각각의 충돌점에 가해진 각각의 충격력의 합은 '물체가 충돌 했을때는 각 물체에 서로 밀어내는 힘인 척력만이 작용 해야 한다는 제약 조건' 을 적용 하여 척력이 되도록 양수가 되어야 하며, 이를 위해 루프를 한번 돌 때 마다 적용 시킨 힘을 clamp 시켜주고 그 때까지 적용 된힘은 cache해 두게 된다. 이 때 루프를 도는 중간중간에 힘의 방향이 반대가 될 수도 있다.(상대속도가 계속 변하므로 한번은 밀어내고 한번은 잡아 당기고 할 수가 있다.)하지만 전체 루프를 돌았을 때 각각의 점에 적용 된, 힘의 각각의 총합은 앞서 말했던 것처럼 물체가 더이상 겹치지 않도록 밀어내는 힘이 되도록 clamp해주므로 프래임 단위로 보면(적용된 힘의 합) 올바른 힘이 적용(척력만이 적용) 되어 물리 및 수학적으로 문제가 되지 않으며 iteration 루프가 많이 돌 수록 더 정확한 전체 힘이 적용 된다.

SI 방식은 이렇게 약간은 변형적이지만 명시적으로 물체에 가할 힘을 계산해서 조금씩 주는 방식이기에 이해 하기가 쉽다. 하지만 PGS 등의 방식은 이 강체의 상태를 방정식으로 두고 방정식을 풀어 진짜 적용 되어야 하는 힘을 구해 적용 하는 방식이기 때문에 무언가 복잡해 보이고 일반적이지 않아보인다.

 

인터넷을 찾아보면 LCP 방식중 하나인 PGS나 SI 방식은 수학적으로 동일하다고 증명 되었다고 한다.

실제로 이 둘을 분석해보다보면 굉장히 유사한 방식인데 하나는 전역적으로 처리하고 하나는 지역적으로 처리할 뿐이라는 것을 볼 수 있다. PGS 방식이 결과적으로 100이라는 힘이 작용 한다는 것을 구하기 위해 몇번의 iteration을 거쳐 100이라는 힘을 구하고 적용 시키는 방식 이라면 SI방식은 PGS방식에서 iteration을 거쳐 구해낸 작은 결과들을 계속적으로 적용 시켜서 결국에는 100만큼의 힘을 준 효과를 내는 것이다.(한번에 100을 주지 않고 10+30+90-10-20+10-10 의 힘을 차례로 준 느낌?)

 

5. SI방식에서는 warm starting, LCP방정식을 푸는 방식에서는 lambda를 캐싱 해 두는 이유

- SI 방식에서의 warm starting이나 lcp방정식을 푸는 방식에서 이전 프레임에서 구한 lambda를 캐싱 해 두는 이유는 같다. 이전 프레임에서와 같은 충돌이라면 이전 프레임에서 돌았던 루프들을 돌며  힘을 처음부터 다시 적용하거나, lcp방정식을 풀 필요가 없이 이전에 구했던 것을 토대로 더 빠르고 쉽게 이번에 적용 할 힘을 구하는 것이다. 이렇게 하면 이전 프레임과 같은 충돌에 대해(물체가 평면에 올려져 있는 경우 등) 중력에 반하는 힘을 미리 적용 시키게 되고 루프를 적게 돌면서도 최적의 힘을 찾게 되는 것이므로 jittering현상이 줄어들고 강체들간의 좀더 안정적인 stacking이 가능하게 된다.

 

 

6. GJK/EPA 알고리즘이 한번 구현 해 두면 여러모로 편하지만 기본도형에는 각 기본도형의 고유 충돌 체크 방식이 더 정확하다.

-구 와 구 충돌

-박스와 박스 충돌

-구와 박스 충돌

-구와 캡슐 충돌

-캡슐과 캡슐 충돌

그러므로 GJK만 이용 하지 말고 if 문 등을 이용 해 각각에 맞는 충돌 체크 방식을 선택 해 하는 것이 충돌 정보를 더 정확하게 찾는 방법이다. (찾아진 충돌 정보에 따라 충돌 처리로 인한 반응이 달라지므로 정확한 충돌 정보를 찾는 것이 장확한 처리를 하는 방법 이다.)