코승호딩의 메모장

[PhysX 개요와 초기화 ] 본문

PhysX

[PhysX 개요와 초기화 ]

코승호딩 2023. 9. 18. 00:02

이번 포스팅에서는 NVIDIA에서 제공하는 물리 엔진 PhysX(피직스)를 다뤄 볼까 합니다. PhysX에 대한 설명이 인터넷에 많이 있지 않아 기술하는 대부분의 내용은 NVIDIA에서 제공하는 PhysX documentation을 기반으로 기술하려 합니다. 또한 참고 자료가 많지 않아 PhysX에 대한 내용을 깊이 파고 들기 보다는 게임에 추가할 만한 요소들만 간단히 사용하는 방식으로 진행하도록 하겠습니다. 필자가 학부생이라는 점 감안하여 추가할 내용 및 수정할 내용 피드백 주시면 정말 감사드리겠습니다. 또한 PhysX의 가장 최신 버전 5.2.1을 사용하겠습니다. 대부분의 API가 이전 버전과 다르지 않기 때문에 다른 버전을 사용하더라도 문제는 없을 것입니다. 결과적으로 DirectX12에 PhysX를 추가하여 간단한 게임을 만드는 것이 목표입니다.

 

PhysX API Basics — physx 5.2.1 documentation

Math Classes The common math classes used in PhysX are PxVec2, PxVec3, PxVec4, PxMat33, PxMat44, PxTransform, PxQuat and PxPlane, which are are defined in their respective header files, e.g. (SDKRoot)/include/foundation/PxVec3.h. Most of these classes exis

nvidia-omniverse.github.io


PhysX의 개요

출처 : https://www.youtube.com/watch?v=5ZFRLpz5mYk

PhysX는 NVIDIA에서 제공하는 실시간 물리 엔진 SDK(소프트웨어 개발 키트)이다. 위 플레이 영상에서 볼 수 있듯이 연기나 안개 효과, 폭발이나 충돌 시 입자들이 현실처럼 튀는 것을 볼 수 있으며 옷감이나 깃발 등이 공기의 흐름에 따라 자연스럽게 펄럭이는 것이 바로 PhysX의 효과이다. 

 

PhysX는 에이지아의 소유였으나 2008년 2월 NVIDIA에서 에이지아를 인수하여 현재는 NVIDIA의 소유이다. PhysX를 통해 하드웨어 가속을 지원하는 비디오 게임들은 Geforce GPU를 통해 가속을 받을 수 있다. 이 가속을 통해 CPU의 물리 계산의 짐을 덜어 주어 CPU가 다른 작업을 대신 수행할 수 있도록 한다. PhysX의 가장 강한 장점은 현대 게임에서 사용하는 복잡한 물리 상호 작용을 다루기 위해 게임 개발자들이 자신들의 코드를 작성하지 않고 PhysX의 API만 사용하면 된다는 것이다.  

 

PhysX는 여러 모양으로 구성될 수 있는 액터로 구성된 3차원 세계를 표현하기 위한 라이브러리이다. 개발자는 이 액터들을 생성할 수 있고 파괴할 수 있다. 액터들은 정적이거나 사용자에 움직이거나 고전 역학의 법칙에 따라 PhysX에 의해 움직일 수도 있다. 그리고 PhysX의 역학 시뮬레이션 기능에는 충돌, 관절 작동, 광선 투사 등 다양한 도구를 사용할 수 있다. PhysX는 기본적으로 GPU에서 어떠한 코드도 실행하지 않는다. 다만 성능 이점을 제공하는 CUDA를 이용하여 GPU를 활용하도록 구성할 수 있다. 

 

PhysX 시뮬레이션 내 세계의 기본 개념은 다음과 같이 정의된다.

  1. PhysX World는 각각 Actors라고 불리는 객체를 포함하는 Scene 모음으로 구성된다.
  2. 각 Scene은 모든 공간과 시간을 포괄하는 자체적인 프레임을 정의한다.
  3. 서로 다른 Scene의 Actors는 서로 상호 작용하지 않는다.
  4. 캐릭터와 차량은 Actors로 만들어진 복잡한 특수 객체이다.
  5. Actors는 물리적 상태(위치, 방향, 속도, 운동량, 에너지 등)을 갖는다.
  6. Actors의 물리적 상태는 적용된 힘, 관절이나 접촉, Actors간의 상호 작용으로 시간이 지남에 따라 변화할 수 있다.

PhysX를 사용할 때 함께 사용되는 것이 있는데 바로 PVD(PhysX Visual Debugger)이다. PVD는 응용 프로그램에서 시뮬레이션된 세계를 시각화하는 데 사용된다. 또한 PhysX 개체의 변수를 검사할 수 있으며 메모리 및 데이터를 기록하고 시각화할 수도 있다. PVD에 대한 자세한 설명은 다음 사이트에서 찾아볼 수 있다.

 

PhysX Visual Debugger (PVD) — NVIDIA PhysX SDK 4.1 Documentation

Custom PvdClient Implement the PvdClient interface if your application needs to react upon connection or disconnection from PVD, or if you plan to send custom PVD events from your application. It is recommended to toggle the contact and constraint visualiz

gameworksdocs.nvidia.com


Vcpkg를 이용한 PhysX 라이브러리 설치

다음은 PhysX API를 사용하기 위한 설치 방법을 설명한다. 우선 Vcpkg를 Github에서 다운 받아야 하는데 Vcpkg란 MS가 제공하는 C++용 패키지 관리자이다. 즉, 소프트웨어 패키지를 관리하는 프로그램으로 설치된 프로그램을 업데이트 하거나 제거하는 용도로 사용할 수 있다. 또한 만약 이미 설치되었다면 설치되지 않도록 관리한다.

 

GitHub - microsoft/vcpkg: C++ Library Manager for Windows, Linux, and MacOS

C++ Library Manager for Windows, Linux, and MacOS. Contribute to microsoft/vcpkg development by creating an account on GitHub.

github.com

위 그림처럼 git clone을 이용하여 vckpg를 다운받았다면 cmdbootstrap-vcpkg을 열어 vcpkg 실행 파일을 다운 받는다.

 

이후 vcpkg의 install 명령어를 통해 physX를 설치할 수 있다.

 

다음으로 시스템 변수의 Path의 편집에 들어가 vckpg의 위치를 새로 만들기를 통해 넣어주면 된다.

 

마지막으로 integrate 명령어를 사용하게 되면 Visual studio와 vcpkg를 연결할 수 있다.

 

결과적으로 Visual studio에서 다른 동작 없이 include로 편리하게 vcpkg에 있는 include 파일의 physx 파일에 접근할 수 있다.


PhysX 초기화 및 예제

PhysX에 필요한 변수들을 선언하는 코드이다. PhysX API는 주로 추상 인터페이스 클래스로 구성되며 클래스, 함수에는 접두사 Px가 있다. 또한 한 가지 중요한 점은 PhysX에서는 반환되는 메모리가 16바이트로 정렬되어야 한다. Direct3D 또한 최적화를 위해 16바이트 정렬을 사용하는 것과 같다. 그렇다면 각 변수들이 어떤 일을 하는지 자세히 살펴보자.

#include <physx/PxPhysics.h>
#include <physx/PxPhysicsAPI.h>
using namespace physx;

PxDefaultAllocator	mDefaultAllocatorCallback;
PxDefaultErrorCallback	mDefaultErrorCallback;
PxFoundation*		mFoundation = NULL;
PxPhysics*		mPhysics = NULL;
PxTolerancesScale	mToleranceScale;
PxDefaultCpuDispatcher*	mDispatcher = NULL;
PxScene*		mScene = NULL;
PxMaterial*		mMaterial = NULL;
PxPvd*			mPvd = NULL;
  • PxDefaultAllocator : PxAllocatorCallback을 상속받으며 모든 할당을 수행한다. PxAllocatorCallback 클래스의 간단한 구현을 위해 사용된다. 뒤에 나오는 mFoundation을 생성하기 위해 필요한 변수이다.
  • PxDefaultErrorCallback : PxErrorCallback을 상속받으며 모든 오류 메시지를 기록한다. PxErrorCallback 클래스의 간단한 구현을 위해 사용된다. 뒤에 나오는 mFoundation을 생성하기 위해 필요한 변수이다.
  • PxFoundation : 모든 PhysX 모듈을 사용하려면 PxFoundation 인스턴스가 필요하다. 뒤에 나오는 PxPhysics을 생성하기 위해 필요한 변수이다.
  • PxPhysics : 모든 Scene에 영향을 미치는 개체로 가장 핵심적인 클래스이다.
  • PxTolerancesScale : 시뮬레이션이 실행되는 규모를 정의하는 클래스이다. 개체의 대략적인 크기와 속도를 정할 수 있다.
  • PxDefaultCpuDispatcher : CPU 리소스를 멀티 쓰레드와 같은 환경에서 효율적으로 공유할 수 있게 해주는 클래스이다.
  • PxScene : 액터와 연결된 Shape들에 대해 충돌 쿼리를 수행하는 메서드를 제공한다.
  • PxMaterial : 일련의 표면 특성을 나타내는 클래스이다. 마찰력, Dynamic 마찰력, 탄성력을 지정하여 사용할 수 있다.
  • PxPvd : PhysX Visual Debugger을 실행시키기 위해 필요하다.

 

void initPhysics(bool interactive)
{
	mFoundation = PxCreateFoundation(PX_PHYSICS_VERSION, mDefaultAllocatorCallback, mDefaultErrorCallback);
	if (!mFoundation) throw("PxCreateFoundation failed!");
	mPvd = PxCreatePvd(*mFoundation);
	physx::PxPvdTransport* transport = physx::PxDefaultPvdSocketTransportCreate("127.0.0.1", 5425, 10);
	mPvd->connect(*transport, physx::PxPvdInstrumentationFlag::eALL);
	mToleranceScale.length = 100;        // typical length of an object
	mToleranceScale.speed = 981;         // typical speed of an object, gravity*1s is a reasonable choice
	mPhysics = PxCreatePhysics(PX_PHYSICS_VERSION, *mFoundation, mToleranceScale, true, mPvd);
	physx::PxSceneDesc sceneDesc(mPhysics->getTolerancesScale());
	sceneDesc.gravity = physx::PxVec3(0.f, -9.81f, 0.f);
	mDispatcher = physx::PxDefaultCpuDispatcherCreate(2);
	sceneDesc.cpuDispatcher = mDispatcher;
	sceneDesc.filterShader = physx::PxDefaultSimulationFilterShader;
	mScene = mPhysics->createScene(sceneDesc);


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

위 코드는 앞에서 선언한 변수들을 각각 초기화 해주는 함수이다. PxPvd와 PxPhysics를 생성하기 위해 PxFoundation 변수를 Create함수를 통해 초기화하는데 역시 할당자와 에러 클래스가 매개 변수로 들어간다. 그리고 PxPvd를 생성하고 PxPvdTransport를 이용하여 TCP/IP 주소를 활용한 자신의 컴퓨터에 열려 있는 PVD로 전송한다. 그 다음 PxTolerancesScale을 통해 객체의 대략적인 크기와 속도를 지정하는데 100을 지정하면 1cm를 기준으로 하겠다는 것이다. 이후로 PxPhysics를 생성하며 이를 이용하여 Scene을 만들어 준다. 

 

static PxReal stackZ = 10.0f;

void initPhysics(bool interactive)
{
	mMaterial = mPhysics->createMaterial(1.0f, 1.0f, 1.0f);
	physx::PxRigidStatic* groundPlane = PxCreatePlane(*mPhysics, physx::PxPlane(0, 1, 0, 0), *mMaterial);
	mScene->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(20.f), PxVec3(0, 0, -90));
}

다음은 액터들에게 넣을 머티리얼을 생성하고 바닥을 생성한다. 이후 addActor함수를 통해 Scene과 바닥 액터를 연결해 주고 강체인 액터 박스 여러 개와 동적인 액터 공 한개를 생성한다.

 

static void createStack(const PxTransform& t, PxU32 size, PxReal halfExtent)
{
	PxShape* shape = mPhysics->createShape(PxBoxGeometry(halfExtent, halfExtent, halfExtent), *mMaterial);
	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 = mPhysics->createRigidDynamic(t.transform(localTm));
			body->attachShape(*shape);
			PxRigidBodyExt::updateMassAndInertia(*body, 10.0f);
			mScene->addActor(*body);
		}
	}
	shape->release();
}

위 코드는 박스 여러 개를 생성하는 함수이다. 박스가 공에 부딪히면 튕겨 나가야 하므로 박스와 공은 모두 동적 강체 시뮬레이션 객체이다. 또한 Shape는 액터에 부착되며 기본적으로 큐브, 구, 캡슐 등이 있다. Shape의 모양에 따라 물리 적용이 되며, 하나의 액터에 여러 개의 Shape를 부착할 수 있다. 따라서 먼저 shape를 PxBoxGeomtry로 만들어 주고 머티리얼을 부착한 다음, 고정 강체 액터를 만들어서 이 액터에 shape를 부착한다. 그리고 고정 강체 액터를 시뮬레이션하기 위해서는 질량 및 관성 텐서가 필요하다. updateMassAndInertia 함수는 매개 변수를 기반으로 필요한 질량과 관성을 계산하는 기능을 제공한다. 마지막으로 Scene에 액터를 추가하고 shape는 모두 사용하였으므로 release를 시켜 준다.

 

static PxRigidDynamic* createDynamic(const PxTransform& t, const PxGeometry& geometry, const PxVec3& velocity = PxVec3(0))
{
	PxRigidDynamic* dynamic = PxCreateDynamic(*mPhysics, t, geometry, *mMaterial, 10.0f);
	dynamic->setAngularDamping(0.5f);
	dynamic->setLinearVelocity(velocity);
	mScene->addActor(*dynamic);
	return dynamic;
}

위 코드는 동적 강체 액터를 생성하는 함수이다. 공을 생성하는 함수이며 PxCreateDynamic을 통해 쉽게 액터를 만든다. 만들 때 매개 변수로 고정 강체 액터와는 다르게 PxPhysics와 물체의 밀도가 들어간다. 또한 굴러가는 액터이기 때문에 setAngularDamping 함수를 통해 각도 감쇠 계수를 설정한다. 그리고 액터의 선형 속도를 설정한다. 마지막으로 Scene에 액터를 추가한다.

 

int main()
{
	initPhysics(false);

	// run simulation
	while (1) {
		mScene->simulate(1.0f / 60.0f);
		mScene->fetchResults(true);
	}
}

위 작업이 모두 끝났다면 메인 함수에서 Scene의 simulate 함수를 이용하여 시뮬레이션한다. PVD를 실행한 상태에서 디버깅 실행을 누른다면 다음과 같은 시뮬레이션 화면이 PVD에 나올 것이다. 코드에서 각 변수 값을 조절하면서 시뮬레이션 해보면 어떤 느낌인지 잘 알 것이다.


 

추가적으로 아직 PhysX에 대해 자세히 알지는 못하지만 지인과 구글링을 통해 검색한 결과 PhysX의 구동 방식은 PhysX의 물리 엔진에 애플리케이션에서 오브젝트를 넘겨 주고 해당 오브젝트를 PhysX에서 시뮬레이션 한 후의 결과 값을 다시 애플리케이션으로 가져와서 적용하는 것 같다. 예를 들어 큐브의 정보를 PhysX에 넘겨줘서 튕겨 나가는 시뮬레이션을 적용하며 프레임 단위로 적용된 월드 행렬 및 시뮬레이션 된 정보를 애플리케이션으로 가져와 적용하면 되는 것이 아닐까 싶다.