피직스에서 물리적인 충돌이 일어나거나 트리거 볼륨과 충돌체가 겹치는 일이 일어나면 이벤트를 발생시킬 필요가 있다. PhysX에서 물리 이벤트에 대한 콜백 함수를 어떻게 등록할 수 있는지 PhysX 예제 Snippet ContactReport 프로젝트를 분석해 알아보자.

ContactReport 프로젝트에서는 접촉이 일어날때마다 빨간색 선으로 충돌지점과 충격의 방향을 표시해주고 디버그 로그를 찍는다.

	PxSceneDesc sceneDesc(gPhysics->getTolerancesScale());
	sceneDesc.cpuDispatcher = gDispatcher;
	sceneDesc.gravity = PxVec3(0, -9.81f, 0);
	sceneDesc.filterShader	= contactReportFilterShader;			
	sceneDesc.simulationEventCallback = &gContactReportCallback;	
	gScene = gPhysics->createScene(sceneDesc);

 

 먼저 PhysX 씬을 생성하는 부분이다. sceneDesc에 filterShader와 simulationEventCallback를 제대로 설정해야 씬에서 생기는 충돌 이벤트를 잘 처리할 수 있다. simulationEventCallback은 당연히 물리 시뮬레이션 진행 도중 이벤트가 생길 때마다 호출되는 콜백객체고, filterShader는 타입이 PxSimulationFilterShader로 명명된 함수 포인터다. 이 함수 포인터 타입에 대한 설명은 다음과 같다.

/**
\brief Filter method to specify how a pair of potentially colliding objects should be processed.

Collision filtering is a mechanism to specify how a pair of potentially colliding objects should be processed by the
simulation. A pair of objects is potentially colliding if the bounding volumes of the two objects overlap.
In short, a collision filter decides whether a collision pair should get processed, temporarily ignored or discarded.
If a collision pair should get processed, the filter can additionally specify how it should get processed, for instance,
whether contacts should get resolved, which callbacks should get invoked or which reports should be sent etc.
The function returns the PxFilterFlag flags and sets the PxPairFlag flags to define what the simulation should do with the given collision pair.

\note A default implementation of a filter shader is provided in the PhysX extensions library, see #PxDefaultSimulationFilterShader.

This methods gets called when:
\li The bounding volumes of two objects start to overlap.
\li The bounding volumes of two objects overlap and the filter data or filter attributes of one of the objects changed
\li A re-filtering was forced through resetFiltering() (see #PxScene::resetFiltering())
\li Filtering is requested in scene queries

\note Certain pairs of objects are always ignored and this method does not get called. This is the case for the
following pairs:

\li Pair of static rigid actors
\li A static rigid actor and a kinematic actor (unless one is a trigger or if explicitly enabled through PxPairFilteringMode::eKEEP)
\li Two kinematic actors (unless one is a trigger or if explicitly enabled through PxPairFilteringMode::eKEEP)
\li Two jointed rigid bodies and the joint was defined to disable collision
\li Two articulation links if connected through an articulation joint

\note This is a performance critical method and should be stateless. You should neither access external objects 
from within this method nor should you call external methods that are not inlined. If you need a more complex
logic to filter a collision pair then use the filter callback mechanism for this pair (see #PxSimulationFilterCallback,
#PxFilterFlag::eCALLBACK, #PxFilterFlag::eNOTIFY).

\param[in] attributes0 The filter attribute of the first object
\param[in] filterData0 The custom filter data of the first object
\param[in] attributes1 The filter attribute of the second object
\param[in] filterData1 The custom filter data of the second object
\param[out] pairFlags Flags giving additional information on how an accepted pair should get processed
\param[in] constantBlock The constant global filter data (see #PxSceneDesc.filterShaderData)
\param[in] constantBlockSize Size of the global filter data (see #PxSceneDesc.filterShaderDataSize)
\return Filter flags defining whether the pair should be discarded, temporarily ignored, processed and whether the
filter callback should get invoked for this pair.

@see PxSimulationFilterCallback PxFilterData PxFilterObjectAttributes PxFilterFlag PxFilterFlags PxPairFlag PxPairFlags PxSceneDesc.filterShader
*/
typedef PxFilterFlags (*PxSimulationFilterShader)
	(PxFilterObjectAttributes attributes0, PxFilterData filterData0, 
	 PxFilterObjectAttributes attributes1, PxFilterData filterData1,
	 PxPairFlags& pairFlags, const void* constantBlock, PxU32 constantBlockSize);

 

 

 PhysX의 충돌 객체들은 서로의 바운딩 볼륨의 영역이 서로 겹치고 난 다음에야 더 정밀한 충돌체크를 하든, 트리거 이벤트를 발생시키든 추가적인 작업을 한다. PxSimulationFilterShader는 두 충돌체의 바운딩 볼륨이 겹쳤을 때 어떤 작업을 해야 하는지, 콜백 함수를 불러야 한다면 어떤 함수를 불러야 하는지를 정의한다. 충돌체들이 서로 어떤 관계를 가져야 하는지 정의하는 함수가 바로 FilterShader라고 할 수 있겠다.

 이 함수는 PxFilterFlag를 반환하며, PxPairFlags의 값들을 수정하는 것으로 물리 시뮬레이션이 충돌체 쌍을 어떻게 처리해야하는지에 대한 정보를 저장한다.

 이 함수는 두 충돌체의 바운딩 볼륨이 겹치기 시작할때, 혹은 바운딩 볼륨이 겹쳐진 상태에서 한 오브젝트의 속성값이 바뀌었을때 호출된다.

 이 함수는 여러 스레드에 의해 수없이 많이 불리기 때문에 퍼포먼스에 심각한 영향을 줄 수 있다. 정말 제대로 물리 이벤트가 터졌을 때 호출되는 함수가 아니라 충돌체들이 어느정도 가까워지기만 하면 호출되기 때문이다. 함수가 어떤 상태에 따라 동작이 달라져서는 아니될 것이며, 심지어는 함수 호출조차 인라인 함수만 사용해야 한다.

 

 함수의 각 파라미터들에 대한 정보는 다음과 같다.

attribute0 [in] : 첫번째 오브젝트의 필터 속성

filterData0 [in] : 첫번째 오브젝트의 사용자 정의 필터 데이터

attribute1 [in] : 두번째 오브젝트의 필터 속성

filterData1 [in] : 두번째 오브젝트의 사용자 정의 필터 데이터

pairFlags [out] : 충돌체 쌍이 어떻게 처리되어야 할지 추가 정보를 저장할 플래그 정보

constantBlock [in] : 전역 필터 데이터, 물리 시뮬레이션을 시작하기 전에 전역으로 임의의 필터링 플래그를 세워두고 이 전역 플래그에 따라 필터링 동작을 다르게 하고 싶을 때 쓸 수 있다.

constantBlockSize [in] : 전역 필터 데이터의 사이즈

 

이중 attribute와 filterData는 PxShape로부터 데이터를 추출한다.

 

 FilterShader 함수를 부를 가치조차 없는 충돌체 쌍들도 존재한다. 다음과 같은 경우들은 바운딩 볼륨이 겹치든 말든 어차피 뭔가를 처리할 필요가 없기 때문에 무시된다.

- 위치와 회전이 항상 고정된 정적 충돌체들로만 이루어진 쌍(Pair of static rigid actors)

- 트랜스폼 상태를 바꿀 수는 있지만 물리법칙에 상태가 영향받지는 않는 키네마틱 충돌체로 이루어진 쌍 (Two Kinematic actors)

- 키네마틱 충돌체와 정적 충돌체로 이루어진 쌍 (A static rigid actor and a kinematic actor)

- etc...

 

static PxFilterFlags contactReportFilterShader(	PxFilterObjectAttributes attributes0, PxFilterData filterData0, 
												PxFilterObjectAttributes attributes1, PxFilterData filterData1,
												PxPairFlags& pairFlags, const void* constantBlock, PxU32 constantBlockSize)
{
	PX_UNUSED(attributes0);
	PX_UNUSED(attributes1);
	PX_UNUSED(filterData0);
	PX_UNUSED(filterData1);
	PX_UNUSED(constantBlockSize);
	PX_UNUSED(constantBlock);

	// all initial and persisting reports for everything, with per-point data
	pairFlags = PxPairFlag::eSOLVE_CONTACT | PxPairFlag::eDETECT_DISCRETE_CONTACT
			  |	PxPairFlag::eNOTIFY_TOUCH_FOUND 
			  | PxPairFlag::eNOTIFY_TOUCH_PERSISTS
			  | PxPairFlag::eNOTIFY_CONTACT_POINTS;
	return PxFilterFlag::eDEFAULT;
}

 

 SnippetContactReport 프로젝트의 filterShader는 충돌체 객체들의 속성이나 필터 데이터에 상관없이 무조건 고정된 플래그들을 반환하게 만들어놨다. 이 플래그들은 각각 다음과 같은 의미를 가진다.

 

 PxPairFlag::eSOLVE_CONTACT : 충돌이 일어났을때 충돌체들이 서로 파고들지 않게 dynamics solver로 충돌체들의 트랜스폼 값들을 수정하게 한다.

 PxPairFlag::eDETECT_DISCRETE_CONTACT : 충돌체들의 충돌여부를 체크하는 방식으로 discrete_contact를 사용한다. discrete는 연속적으로 충돌여부를 체크하는 CCD(Continuous Collision Detection) 방식과 대비되는 고효율 저정밀 충돌체크 방식이다.
PxPairFlag::eNOTIFY_TOUCH_FOUND : 충돌체간에 접촉을 시작할때 접촉 콜백 함수를 호출한다.
PxPairFlag::eNOTIFY_TOUCH_PERSISTS : 충돌체가 다른 충돌체와 접촉을 유지하고 있을 때 콜백 함수를 호출한다.
PxPairFlag::eNOTIFY_CONTACT_POINTS : 충돌체가 다른 충돌체와 접촉을 그만두었을 때 콜백 함수를 호출한다.

 

/**
\brief An interface class that the user can implement in order to receive simulation events.

With the exception of onAdvance(), the events get sent during the call to either #PxScene::fetchResults() or 
#PxScene::flushSimulation() with sendPendingReports=true. onAdvance() gets called while the simulation
is running (that is between PxScene::simulate() or PxScene::advance() and PxScene::fetchResults()).

\note SDK state should not be modified from within the callbacks. In particular objects should not
be created or destroyed. If state modification is needed then the changes should be stored to a buffer
and performed after the simulation step.

<b>Threading:</b> With the exception of onAdvance(), it is not necessary to make these callbacks thread safe as 
they will only be called in the context of the user thread.

@see PxScene.setSimulationEventCallback() PxScene.getSimulationEventCallback()
*/
class PxSimulationEventCallback
{
	...

 

 sceneDesc의 simulationEventCallback은 라이브러리의 사용자가 상속받아 구현할 수 있는 인터페이스다. PhysX의 시뮬레이션 연산은 Cpu에서 멀티스레딩으로 동작하거나 GPU 상에서 동작하는데, PxSimulationEventCallback의 함수들은 한 틱의 물리 시뮬레이션을 다 끝내고 사용자가 직접 메인 스레드에서 PxScene::fetchResults() 함수를 호출할 때 실행되기 때문에 스레드 안전성을 고민할 필요까지는 없다.

 

'자체엔진 Yunuty > PhysX' 카테고리의 다른 글

PhysX 코드 분석 : Hello World  (0) 2023.11.03

 

 

실행하면 세모 모양으로 쌓인 큐브 블록들을 보여주는 PhysX의 HelloWorld 예제

 

 HelloWorld 프로젝트에서는 간단하게 박스로 피라미드를 쌓은 예제를 보여준다. 어떻게 PhysX 환경을 조성하고 강체 시뮬레이션을 돌릴 수 있는지 코드를 분석해 알아보자.

void initPhysics(bool interactive)
{
	gFoundation = PxCreateFoundation(PX_PHYSICS_VERSION, gAllocator, gErrorCallback);

	gPvd = PxCreatePvd(*gFoundation);
	PxPvdTransport* transport = PxDefaultPvdSocketTransportCreate(PVD_HOST, 5425, 10);
	gPvd->connect(*transport,PxPvdInstrumentationFlag::eALL);

	gPhysics = PxCreatePhysics(PX_PHYSICS_VERSION, *gFoundation, PxTolerancesScale(),true,gPvd);

	PxSceneDesc sceneDesc(gPhysics->getTolerancesScale());
	sceneDesc.gravity = PxVec3(0.0f, -9.81f, 0.0f);
	gDispatcher = PxDefaultCpuDispatcherCreate(2);
	sceneDesc.cpuDispatcher	= gDispatcher;
	sceneDesc.filterShader	= PxDefaultSimulationFilterShader;
	gScene = gPhysics->createScene(sceneDesc);

	PxPvdSceneClient* pvdClient = gScene->getScenePvdClient();
	if(pvdClient)
	{
		pvdClient->setScenePvdFlag(PxPvdSceneFlag::eTRANSMIT_CONSTRAINTS, true);
		pvdClient->setScenePvdFlag(PxPvdSceneFlag::eTRANSMIT_CONTACTS, true);
		pvdClient->setScenePvdFlag(PxPvdSceneFlag::eTRANSMIT_SCENEQUERIES, true);
	}
	gMaterial = gPhysics->createMaterial(0.5f, 0.5f, 0.6f);

	PxRigidStatic* groundPlane = PxCreatePlane(*gPhysics, PxPlane(0,1,0,0), *gMaterial);
	gScene->addActor(*groundPlane);

	for(PxU32 i=0;i<5;i++)
		createStack(PxTransform(PxVec3(0,0,stackZ-=10.0f)), 10, 2.0f);

	if(!interactive)
		createDynamic(PxTransform(PxVec3(0,40,100)), PxSphereGeometry(10), PxVec3(0,-50,-100));
}

 샘플 코드에서 호출되는 함수들을 타고 타고 들어가면 initPhysics라는 이름의 함수가 호출되는데, 이 함수에서 피직스에 필요한 모든 초기화 작업이 진행된다.

void initPhysics(bool interactive)
{
	gFoundation = PxCreateFoundation(PX_PHYSICS_VERSION, gAllocator, gErrorCallback);

 

 PxFoundation은 PhysX와 관련된 모든 객체들의 기반이 되는 싱글톤 객체라고 한다. 이 파운데이션 객체에 매달린 객체들이 먼저 모두 릴리즈되어야 파운데이션 객체도 릴리즈할 수 있다.

	gPvd = PxCreatePvd(*gFoundation);
	PxPvdTransport* transport = PxDefaultPvdSocketTransportCreate(PVD_HOST, 5425, 10);
	gPvd->connect(*transport,PxPvdInstrumentationFlag::eALL);

 

 PxCreatePvd함수는 파운데이션 객체에 의존하는 PxPvd 클래스를 생성한다. PVD는 PhysX Visual Debugger라는 의미로, 이 클래스로부터 connect 함수를 호출하면 PhysX 기반으로 물리연산을 하는 어플리케이션으로부터 물리정보를 받아 PhysX Visual Debugger 프로그램에서 물리엔진의 동작 상태를 시각적으로 확인할 수 있다.

좌측의 프로그램은 PhysX를 기반으로 물리시뮬레이션을 돌리는 프로그램, 오른쪽은 PhysX Visual Debugger. PVD는 알짜 물리연산 정보들만 사용자에게 표시해준다,

	gPhysics = PxCreatePhysics(PX_PHYSICS_VERSION, *gFoundation, PxTolerancesScale(),true,gPvd);

 

 CreatePhysics 함수는 피직스 객체들을 생성할 수 있는 싱글톤 팩토리 클래스이다. 씬이나 매터리얼을 생성하거나 물리 객체의 형체 정보(Shape), 강체 인스턴스 등 거의 대부분의 PhysX 객체들을 생성할 때 사용된다.

	PxSceneDesc sceneDesc(gPhysics->getTolerancesScale());
	sceneDesc.gravity = PxVec3(0.0f, -9.81f, 0.0f);
	gDispatcher = PxDefaultCpuDispatcherCreate(2);
	sceneDesc.cpuDispatcher	= gDispatcher;
	sceneDesc.filterShader	= PxDefaultSimulationFilterShader;
	gScene = gPhysics->createScene(sceneDesc);

 

 PhysXScene은 PhysX 액터들이 배치될 수 있는 씬이다.

	PxPvdSceneClient* pvdClient = gScene->getScenePvdClient();
	if(pvdClient)
	{
		pvdClient->setScenePvdFlag(PxPvdSceneFlag::eTRANSMIT_CONSTRAINTS, true);
		pvdClient->setScenePvdFlag(PxPvdSceneFlag::eTRANSMIT_CONTACTS, true);
		pvdClient->setScenePvdFlag(PxPvdSceneFlag::eTRANSMIT_SCENEQUERIES, true);
	}

 

 PvdSceneClient는 Physx Visual Debugger 프로그램에 한 씬의 정보를 전달할 때 어떤 정보를 전달해 줄지 플래그를 설정해주는 객체다.

	gMaterial = gPhysics->createMaterial(0.5f, 0.5f, 0.6f);

	PxRigidStatic* groundPlane = PxCreatePlane(*gPhysics, PxPlane(0,1,0,0), *gMaterial);
	gScene->addActor(*groundPlane);

 

 Material은 물리적인 표면 재질의 상태를 나타내는 객체다. 표면의 정지마찰계수, 운동마찰계수, 반발계수 등 표면 재질의 다양한 성질을 지정할 수 있다. groundPlane을 만들고 이 재질을 입히면 표면 재질의 물리적 특성이 반영되어 지표면이 만들어진다.

	for(PxU32 i=0;i<5;i++)
		createStack(PxTransform(PxVec3(0,0,stackZ-=10.0f)), 10, 2.0f);

	if(!interactive)
		createDynamic(PxTransform(PxVec3(0,40,100)), PxSphereGeometry(10), PxVec3(0,-50,-100));
}
static void createStack(const PxTransform& t, PxU32 size, PxReal halfExtent)
{
	PxShape* shape = gPhysics->createShape(PxBoxGeometry(halfExtent, halfExtent, halfExtent), *gMaterial);
	for(PxU32 i=0; i<size;i++)
	{
		for(PxU32 j=0;j<size-i;j++)
		{
			PxTransform localTm(PxVec3(PxReal(j*2) - PxReal(size-i), PxReal(i*2+1), 0) * halfExtent);
			PxRigidDynamic* body = gPhysics->createRigidDynamic(t.transform(localTm));
			body->attachShape(*shape);
			PxRigidBodyExt::updateMassAndInertia(*body, 10.0f);
			gScene->addActor(*body);
		}
	}
	shape->release();
}

 

 createStack, createDynamic은 샘플 프로젝트에서 정의된 코드다. 하나의 박스 객체를 만들기 위해서는 Physics 객체로부터 강체를 생성해낸 다음 박스모양의 Shape를 만들어 붙여 씬에 액터로 등록한다.

PX_FORCE_INLINE	PxShape* createShape(	const PxGeometry& geometry,
											const PxMaterial& material,
											bool isExclusive = false,
											PxShapeFlags shapeFlags = PxShapeFlag::eVISUALIZATION | PxShapeFlag::eSCENE_QUERY_SHAPE | PxShapeFlag::eSIMULATION_SHAPE)
	{
		PxMaterial* materialPtr = const_cast<PxMaterial*>(&material);
		return createShape(geometry, &materialPtr, 1, isExclusive, shapeFlags);
	}

 

 CreateShape 함수는 Shape의 속성을 나타내는 PxShapeFlags를 매개변수로 받는데 여기에 어떤 플래그를 활성화시키느냐에 따라 같은 Shape를 물리 시뮬레이션이 되는 덩어리 물질로 쓸수도 있고, 다른 물질이 닿았는지만 체크하는 볼륨 트리거로 쓸 수도 있다.

void renderCallback()
{
	stepPhysics(true);

	Snippets::startRender(sCamera);

	PxScene* scene;
	PxGetPhysics().getScenes(&scene,1);
	PxU32 nbActors = scene->getNbActors(PxActorTypeFlag::eRIGID_DYNAMIC | PxActorTypeFlag::eRIGID_STATIC);
	if(nbActors)
	{
		std::vector<PxRigidActor*> actors(nbActors);
		scene->getActors(PxActorTypeFlag::eRIGID_DYNAMIC | PxActorTypeFlag::eRIGID_STATIC, reinterpret_cast<PxActor**>(&actors[0]), nbActors);
		Snippets::renderActors(&actors[0], static_cast<PxU32>(actors.size()), true);
	}

	Snippets::finishRender();
}
void stepPhysics(bool /*interactive*/)
{
	gScene->simulate(1.0f/60.0f);
	gScene->fetchResults(true);
}

 

 renderCallback은 매 렌더 업데이트마다 호출되는 함수다. 먼저 씬의 simulate 함수에 델타 타임을 매개변수로 넣어 시뮬레이션을 돌리고 그 결과를 받는다. 이 예제 코드에서는 업데이트된 액터들의 상태를 토대로 렌더링만 시키는데 내 게임엔진에서는 액터들의 상태를 이용해 컴포넌트들의 콜백 함수를 호출하거나 상태를 바꾸면 될 것 같다.

'자체엔진 Yunuty > PhysX' 카테고리의 다른 글

PhysX Snippet ContactReport 프로젝트 분석  (0) 2024.01.05