2020년 7월 1일 수요일

Bullet Physics 정리

0. 세팅

http://www.cs.kent.edu/~ruttan/GameEngines/lectures/Bullet_User_Manual

위에서 대부분의 설명은 되어있음.

이 글은 위를 바탕으로 Hello World 예제코드를 중심으로 빠르게 학습하고 기억을 상기시킬 목적으로 씀.


https://github.com/Mona04/Bullet-Physics-Excercise

위에건 Hello World 예제코드를 조금 변형한 여기서 사용할 예제코드이다.


https://github.com/bulletphysics/bullet3

위에서 받아서 CMake 를 써서 프로젝트를 만들자.

그리고 64x/86x, MD/MT 를 잘 구분해서 빌드하자.


기본적인 lib 은 다음과 같다.

#pragma comment(lib, "BulletCollision.lib" )

#pragma comment(lib, "BulletDynamics.lib" )

#pragma comment(lib, "BulletSoftBody.lib" )

#pragma comment(lib, "LinearMath.lib" )

디버그 옵션이면 끝에 _Debug 인걸 들고 오면 된다.


그리고 얘네 오른손 좌표계를 쓴다.


1. World

물리 엔진은 시뮬레이션이다. 

그러므로 시뮬레이션이 일어나는 World 를 만드는 것이 당연히 처음할 일이다. 

BulletPhysics 는 Default 로 btDiscreteDynamicsWorld / btSoftRigidDynamicsWorld 두가지를 제공한다. 

soft rigid 는 고무공같은거나 천 휘날리는걸 생각하자. 

당연히 간단한걸 쓸거므로 전자를 만들자. 

Physics_World::Physics_World()
{
	collisionConfiguration = new btDefaultCollisionConfiguration();
	dispatcher = new btCollisionDispatcher(collisionConfiguration);
	broadphase = new btDbvtBroadphase();
	constraintSolver = new btSequentialImpulseConstraintSolver();
	world = new btDiscreteDynamicsWorld(dispatcher, broadphase, constraintSolver, collisionConfiguration);

	//SetUp Physics World
	world->setGravity(GRAVITY);
	world->getDispatchInfo().m_useContinuous = true;
	world->getSolverInfo().m_numIterations = 256;
}

btCollisionConfiguration 은 충돌 계산시 stack 을 얼마나 허용하고 memory pooling 을 얼마나 할 거고 등등 충돌 계산 때 사용할 리소스를 정한다. btDefaultCollisionConfiguration 은 거의 최대로 리소스를 허용하도록 설정되어 있다.

btCollisionDispacher 는 pair 로 된 데이터를 어떻게 계산할지에 관한 interface 이다. 이때 pair 는 오브젝트들이 겹쳐진 것들을 말하며 pair cache 라고도 한다. 다시말해 볼록오목 / 오목오목 등의 pair 의 충돌을 어떻게 계산할지 관여한다. 엔진 내부의 iterate 부분이 여기를 말하는 것이며 알고리즘은 유저가 설정도 가능하다.

이밖에 월드 생성에 여러가지 인수를 받는데, 초반에 세팅하고 후에 건들필요는 없다.


world 의 주요 설정은 위 세가지이다. 

Gravity 설정은 옆에 주석처럼 그냥 중력 크기만 지정해두면 된다. 

m_numiterations 는 충돌시 겹치는 부분을 떼어내는 과정인 dispatch 에서의 반복횟수다.

CCD(Continuous Collision Detection) 이라고 불리는 설정은 복잡한데, 한마디로 하면 얇은 물체끼리의 충돌처리를 용이하게 한다. 자세한 내용은 박사과정 수준이니까 즐겁게 사용하자.


2. Update

void Physics_World::Update()
{
	Timer* timer = Timer::Get();

	float delta_time = timer->GetDeltaTimeSec();
	float internal_time_step = 1.0f / 60.0f;
	int max_substeps = static_cast<int>(delta_time * 60.0f); 

	max_substeps = max_substeps > 1 ? max_substeps : 1;  // timeStep < maxSubSteps * fixedTimeStep

	world->stepSimulation(delta_time, max_substeps, internal_time_step);
}

world 는 stepSimulation 이라는 함수로 시뮬레이션을 돌린다.

이 함수는 현재 지나간 시간(delta time)과 엔진 내부의 시간단위(60 per second)를 바탕으로 step 횟수를 정한다. 

 이때 maxSubSteps 가 중요한데 0 인 경우는 가변적으로 정한다는건데 잘 쓰이지 않고, 1의 경우 가장 많이 사용되며 기존과 업데이트될 값 사이에서 바로 보간을 한다. 2 이상의 경우는 한 step 당 fixedTimeStep(위에선 internal_time_step) 에 해당하는 만큼 보간을 한다. 

 2 이상의 경우는 timeStep < maxSubSteps * fixedTimeStep 식을 만족해야지 엔진 내부에서 시간손실이 나지 않는다. (위 식의 각 변수는 코드에서 이름이 다르게 적혔는데 stepSimulation 에 들어가는 변수와 대응된다.) 위 식을 만족하지 않으면 엔진 내부에서 timeStep - maxSubSteps * fixedTimeStep 정도의 시간이 손실된다. 왜냐하면 한 step 당 fixedTimeStep 만큼 시간을 계산하기 때문이다.

 fixedTimeStep 은 엔진 내부에서 사용되는 시간단위로 1/60 tick per sec 이 기본값이다. 만약 delta time이 이 값 보다 작으면 MotionState(후에 설명) 의 위치를 delta time 에 따라 보간을 하되 물리시뮬레이션은 돌리지 않으며, 만약 넘으면 돌린다.

https://stackoverflow.com/questions/12778229/what-does-step-mean-in-stepsimulation-and-what-do-its-parameters-mean-in-bulle 자세한건 이거 참조.


3. CollisionShape

엔진 내부에서 계산될 단위이다. 

Bullet 에서 다양한 클래스를 제공하며 Convex Primitive 가 가장 많이 쓰인다.

계산 복잡도에 큰 영향을 주기 때문에 위의 공식 문서에서 제공하는 가이드를 숙지할 필요가 있다.



void RigidBody::SetCollisionShape()
{
	collisionShape = new btBoxShape(btVector3(0.5f, 0.5f, 0.5f));
	collisionShape->setLocalScaling(ToBtVector3(size));
	collisionShape->calculateLocalInertia(mass, localInertia);

	collisionShape->setUserPointer(this); // do not used by bullet
}

 RidgidBody 는 내가 임의로 만든 클래스로 보통 CollisionShape 를 관리하기 위해 클래스를 따로 만든다. 위 코드에서 초기 관성값을 계산해주는 것 외는 특별할 것 없다. 사실 초기값을 0벡터로 넣을 거라서 calculateLocalInertia 는 없어도 잘 동작은 한다. setUserPointer는 bullet 내부에선 사용되지 않고 indentify 용으로 사용된다. 

btBoxShape 는 #include <BulletCollision/CollisionShapes/btBoxShape.h> 여기에 있다.


4. MotionState


class MotionState : public btMotionState
{
public:
	MotionState(class RigidBody* rigidBody) : rigidBody(rigidBody) {}

	void getWorldTransform(btTransform& worldTrans) const override
	{
		worldTrans.setOrigin(ToBtVector3(rigidBody->position + rigidBody->rotation * rigidBody->massCentor));
		worldTrans.setRotation(ToBtQuaternion(rigidBody->rotation));

		//rigidBody->Print();
		//rigidBody->SetHasSimulated(true);
	}

	void setWorldTransform(const btTransform& worldTrans) override
	{

		rigidBody->rotation = ToD3DXQuaternion(worldTrans.getRotation());
		rigidBody->position = ToD3DXVector3(worldTrans.getOrigin()) - ToD3DXQuaternion(worldTrans.getRotation()) * rigidBody->massCentor;

		D3DXVECTOR3 position = rigidBody->position;
		D3DXQUATERNION quarternion = rigidBody->rotation;

		rigidBody->Print();
		//rigidBody->SetHasSimulated(true);
	}

private:
	RigidBody* rigidBody;
};

얘는 엔진 내부의 transform 정보를 외부로 보내는 Interface 역할이다. 

Hello World 예제에선 btDefaultMotionState 를 사용하지만 보통은 클래스를 따로 만든다.

주요 함수는 GetWorldTransform, SetWorldTransform 두개다. 

rigidBody 초기화 할 때나 시뮬레이션 초반에 Get 이 호출되고 시뮬레이션 후반에 Set 이 호출된다. 단 rigidBody 가 active 상태에서 움직였을 때만 호출이 된다.

즉 Get, Set 함수를 통해 엔진 내부의 객체와 우리가 원하는 객체를 연동시킬 수 있다.

렌더링이나 네트워크 업데이트 ect.


btTransform 의 경우 origin 은 물체의 무게중심이어야 한다. 

그래서 단순히 위치만을 origin 에 넣는게 아니라, 위치 + 회전 * 무게중심 을 넣어야한다. 


Get, Set 은 렌더링용인 보간된 좌표가 들어간다.

엔진의 tick 마다 업데이트 되는 보간되지 않은 값은 btRigidBody, btCollisionShape 의 GetWorldTransform() 으로 들고 올 수 있다.



5. AddToWorld

void RigidBody::AddToWorld()
{
	SetCollisionShape();

	// Make rigidBody
	MotionState* motionState = new MotionState(this);

	btRigidBody::btRigidBodyConstructionInfo constructionInfo(mass, motionState, collisionShape, localInertia);
	constructionInfo.m_motionState = motionState;
	constructionInfo.m_collisionShape = collisionShape;
	constructionInfo.m_mass = mass;
	constructionInfo.m_friction = friction;
	constructionInfo.m_rollingFriction = localFriction;
	constructionInfo.m_restitution = restitution;   // 탄성
	constructionInfo.m_localInertia = localInertia;

	rigidBody = new btRigidBody(constructionInfo);
	rigidBody->setUserPointer(this);  // guid 처럼 identify 용이나 가변적임

	// collisionFlag
	rigidBody->setCollisionFlags(rigidBody->getCollisionFlags() & ~btCollisionObject::CF_KINEMATIC_OBJECT);
	rigidBody->forceActivationState(DISABLE_DEACTIVATION);
	rigidBody->setDeactivationTime(2000.0f);
	
	rigidBody->setFlags(rigidBody->getFlags() & ~BT_DISABLE_WORLD_GRAVITY);
	rigidBody->setGravity(Physics_World::Get()->GetGravity());
	
	btTransform& worldTrans = rigidBody->getWorldTransform();
	worldTrans.setOrigin(ToBtVector3(position + rotation * massCentor));
	worldTrans.setRotation(ToBtQuaternion(rotation));
	rigidBody->updateInertiaTensor();
	
	if (mass > 0)
	{
		rigidBody->setLinearFactor(btVector3(1.0f, 1.0f, 1.0f));
		rigidBody->setAngularFactor(btVector3(1.0f, 1.0f, 1.0f));
		rigidBody->activate(true);  // only synchronizes 'active' objects
	
	}
	else
	{
		rigidBody->setLinearFactor(btVector3(0.0f, 0.0f, 0.0f));
		rigidBody->setAngularFactor(btVector3(0.0f, 0.0f, 0.0f));
		rigidBody->activate(false); // only synchronizes 'active' objects
	}

	rigidBody->setLinearVelocity(btVector3(0.0f, 0.0f, 0.0f));
	rigidBody->setAngularVelocity(btVector3(0.0f, 0.0f, 0.0f));
	
	Physics_World::Get()->GetWorld()->addRigidBody(rigidBody);

}

위에서 만든 CollisionShape, MotionState 가 사용된다. 

constructionInfo 에 다른 변수들은 영어 단어 그대로 진짜 물리 관련 변수이다.


중요한건 flag 와 mass 다. 

 mass 가 0 인 경우에 그 collider 는 움직이지 않는다. 마치 땅처럼 힘을 받아도 움직이지 않는 그런 상태가 된다. 그렇다고 크게하면 힘 약하게 할 경우 안움직이는 경우도 있으므로 일단 작게 해보자. 안움직이는줄 알았는데 알고보니 mass 가 너무 커서 그런 경우도 있다.

 CF_KINEMATIC_OBJECT 는 Unity 를 보면 쉽게 알 수 있는데, 적용되면 물리엔진에 의해 움직이는 상태가 아닌 직접 위치를 바꿔야하는 물체가 된다. 캐릭터 움직임 같은거랑 비슷한 것. 난 물체를 떨궈야 하므로 플래그를 제거했다. 

 Set ~~~Factor 의 경우 움직임을 허용하는 축을 결정한다. 뭔말이냐면 Vector3(1, 0, 0) 을 넣으면 x축 이동은 되는데 y, z 축 이동은 안되는 것이다. 일단 mass 크기에 맞춰서 다 움직이거나 다 안되거나로 해놨다. 저거 없어도 mass 0 이면 어차피 안움직이긴 하지만 말이다.

설정 다 하고 addRigidBody 를 하면 끝이다.


6. Collision Filtering

world 의 rigid body 중에 일부만 골라서 일부끼리만 충돌검사를 하고 싶을 수 있다.

이를 위해서 broadphase 단계에 대한 설정을 할 수 있도록 bullet 은 설계되었다.

방법은 두가지로 group/collision bit mask 와 Filter Callback 이 있다.


1. Masking

world->AddRigidBody 의 overloading 된 함수가 있다.

그 함수는 두번째 세번째 인자로 group, collide mask 를 받는다.

bool collides = (proxy0->m_collisionFilterGroup & proxy1->m_collisionFilterMask) != 0;
collides = collides && (proxy1->m_collisionFilterGroup & proxy0->m_collisionFilterMask);

계산은 위 식을 사용한다.

32bit 각각을 사용해서 32개의 group 을 만들고, 각 collide mask 는 영향을 줄 group 의 비트곱으로 만든다고 생각하면 편하다.


2. FilterCallback

struct PhysicsFilterCallback : public btOverlapFilterCallback
{
	bool needBroadphaseCollision(btBroadphaseProxy* proxy0, btBroadphaseProxy* proxy1) const override
	{
		auto findIt = std::find_if(
			m_nonFilterProxy.begin(),
			m_nonFilterProxy.end(),
			[proxy0, proxy1](const auto& x) {return x == proxy0 || x == proxy1; }
		);
		if (findIt != m_nonFilterProxy.end())
		{
			return true;
		}
		bool collides = (proxy0->m_collisionFilterGroup & proxy1->m_collisionFilterMask) != 0;
		collides = collides && (proxy1->m_collisionFilterGroup & proxy0->m_collisionFilterMask);
		return collides;
	}

	std::vector<btBroadphaseProxy*> m_nonFilterProxy;
};

Masking 으로 부족하면 커스터마이즈하라고 만든 것.

world->getPairCache()->setOverlapFilterCallback() 함수로 설정할 수 있음.

btBroadphaseProxy 는 각 RigidBody 마다 하나씩 가지고 있음. 

말 그대로 프록시패턴 쓴거.


위 코드는 내부의 벡터에 있는 것들은 서로 충돌검사를 하며, 없는 경우 마스크 검사를 함.

이건 그냥 예시고 요구사항에 맞춰서 커스터마이즈 하면 됨.


7. Constraint

3DF 의 Point2Point, 2DF 의 Hinge, 그리고 1DF 의 Slider 가 기본적으로 제공된다.

6DF 와 3DF 에 제한을 가한 Cone Twist Constraint 도 제공된다.

종류는 위 공식 문서를 참고하자.  

생성시 frameInA, frameInB 가 무엇인지 설명이 친절하지 않은데, 각 RigidBody 를 기준으로 한 상대적인 변환 행렬이다.

https://pybullet.org/Bullet/phpBB3/viewtopic.php?t=2027 이걸 참고.

world->AddConstraint() 로 추가한다.


주의사항으로 가끔 갑작스런 움직임은 Constraint 에 오작동을 불러일으킨다.

 예를들어 Constraint 를 집어 넣기 전에 Transform 을 업데이트 하지 않고 집어 넣고 후에 Transform을 업데이트를 했을 때, Transform 의 변화값이 매우 크면 constraint 가 따라오지 않는 경우가 있었다. 



8. DebugDrawer


class PhysicsDebugDraw : public btIDebugDraw
{
public:
	PhysicsDebugDraw() {}
	virtual ~PhysicsDebugDraw() {}

	virtual void drawLine(const btVector3& from, const btVector3& to, const btVector3& color) override {}
	virtual void drawContactPoint(const btVector3& PointOnB, const btVector3& normalOnB,
		btScalar distance, int lifeTime, const btVector3& color) override {};
	virtual void draw3dText(const btVector3& location, const char* textString) override {};
	virtual void setDebugMode(int debugMode) override {};
	virtual int getDebugMode() const override { return DBG_NoDebug; };
	virtual void reportErrorWarning(const char* warningString) override
	{
		OutputDebugStringA(warningString);
	};
};

디버그용 Interface 를 world->setDebugDrawer() 함수로 설정할 수 있다.

엔진 내의 상황을 볼 수 있게 해주는 가상함수이다.

이 함수를 검색하면 선같은게 그려진걸 볼 수 있다.


댓글 없음:

댓글 쓰기

List