Premiers pas avec PhysX 3.1

PhysX est un moteur de physique développé par NVIDIA et qui, dans sa troisième version, permet d'utiliser la puissance des GPU pour accélérer les simulations physiques. Il est aussi fort populaire parmi les développeurs de jeux vidéo.

Commentez Donner une note à l'article (5)

Article lu   fois.

L'auteur

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. PhysX

PhysX est un moteur physique prévu pour des jeux vidéo. Il permet de gérer en temps réel des collisions et des processus physiques plus complexes. Sa réputation n'est plus à faire dans le monde du jeu vidéo : il est notamment intégré dans les moteurs de jeu UnrealEngine 3, Unity 3D, Gamebryo, Torque, etc. Notamment, c'est lui à l'œuvre dans l'UDK, une version grand public de l'Unreal Engine 3, prévue pour des utilisations non commerciales ou éducatives (il est possible, évidemment, de vendre des produits utilisant l'UDK, voir conditions sur le site).

La suite de l'article supposera que vous êtes sur une plateforme Windows 64 bits. Cependant, je tenterai d'expliciter autant que possible les différences par rapport à des plateformes 32 bits (généralement, un répertoire différent) ; par contre, pour les autres plateformes, je ne m'avancerai pas, ne pouvant tester confortablement (avec les exceptions des chemins et de la configuration de l'environnement de compilation, rien ne devrait changer).

I-A. Petit historique

À l'origine, ce middleware s'appelait NovodeX (d'où le préfixe Nx des classes dans la version 2, notamment) et était développé par une société suisse, NovodeX AG. Cette première version était déjà multithreadée, en 2001.

En 2004, Ageia Technologies, Inc. rachète NovodeX, avec une idée en tête : les simulations physiques nécessitent énormément de puissance de calcul pour être effectuées correctement, il faut décharger le processeur principal de ces calculs. D'où le PPU : Physics Processing Unit, l'équivalent du CPU pour les simulations physiques. Le moteur physique est alors renommé PhysX et est adapté pour tirer profit du PPU. Quelques titres sortiront. Cependant, les améliorations de performances ne sont pas vraiment au rendez-vous, à en croire Tom's Hardware :

Image non disponible

Les ventes ne seront pas suffisantes non plus pour permettre à Ageia de vivre de belles années ; cependant, la mode de l'accélération physique était lancée.

En 2008, Ageia est racheté par NVIDIA. L'utilité d'un PPU va mincir, vu l'activité de NVIDIA dans le GPGPU. Finalement, dès 2010, PhysX n'a plus supporté les PPU, seulement les GPU NVIDIA, grâce à CUDA. Il ne s'agit que de l'accélération, les simulations peuvent toujours se faire sur le CPU.

En 2011, le moteur a été presque complètement réécrit pour la troisième version, en repensant complètement l'architecture vieillissante de la série 2. On dispose alors d'un multithreading amélioré, d'une base de code unifiée pour toutes les plateformes (ce qui permettra un support de plateformes émergentes, comme Android) et d'autres.

I-B. La concurrence sur l'accélération matérielle

PhysX n'a pas été le seul projet de moteur de physique orienté jeux avec accélération matérielle. On peut aussi citer Havok FX, qui voulait tirer parti des systèmes à plusieurs GPU (avec les technologies SLI de NVIDIA ou CrossFire d'AMD). Il proposait de séparer les simulations en deux parties : celles qui influencent le gameplay et celles qui se limitent à du visuel. Les premières restent sur le CPU, car c'est là qu'est géré le gameplay ; les secondes, sur le GPU, car les calculs graphiques y sont effectués.

Cependant, Havok a été racheté par Intel en 2007, ce projet semble être tombé à l'eau depuis lors, sans qu'un seul jeu en ait profité (au contraire d'une quinzaine de titres ayant profité d'une accélération par PPU avec les technologies d'Ageia).

On peut citer la Open Physics Initiative, lancée par AMD en 2010, qui propulse Bullet et Pixelux DMM, avec OpenCL et DirectCompute (voir AMD and PhysX: History of the Problem).

I-C. Succès de PhysX

En regardant l'évolution du nombre de titres de 2006 à 2009, on se rend compte que PhysX dépasse largement Havok. Il faut encore noter que, en 2009, PhysX 3 était toujours en préparation, la version 2 étant à peu près en pause.

Image non disponible
Source : PhysXInfo (http://physxinfo.com/articles/?page_id=154)

II. Obtenir le PhysX SDK

C'est assez simple : il faut s'inscrire comme développeur sur le Developer Support Center.

Toutes les validations sont effectuées à la main, il faut donc que les informations entrées soient exactes. Notamment, on demande l'utilisation prévue du SDK : il est inutile d'inventer quelque chose, expérimenter est aussi une bonne réponse (quelques conseils sont disponibles en ligne).

Une fois le compte créé (un email devrait être envoyé, il semblerait que ce ne soit pas toujours le cas), il suffit de se connecter, puis, dans l'onglet Online Support, de choisir Downloads. On utilisera la version 3.1 du SDK dans ce tutoriel.

Il est possible pour des particuliers d'utiliser le moteur PhysX sans devoir de royalties ou autres à NVIDIA ; par contre, pour un usage commercial, d'autres conditions s'appliquent, il reste donc toujours important de lire le contrat de licence avant de télécharger le SDK.

Pour Windows, le SDK existe en deux variantes : VC9 et VC10. Bien que toute la documentation ne soit pas à jour (le guide utilisateur précise que Visual Studio 2010 n'est pas supporté, les notes de version que certaines choses devront être corrigées pour la version disponible au grand public), les tests prouvent que le SDK est au moins autant compatible avec ce compilateur que la version précédente. (1) Chaque variante supporte indifféremment les plateformes 32 et 64 bits, tant en développement qu'en déploiement.

L'édition express de Visual Studio convient parfaitement et est disponible gratuitement. L'autre environnement de développement officiellement supporté est Apple XCode 3.

Dans le SDK pour Windows, la documentation est exclusivement disponible sous la forme d'un fichier CHM, elle n'est pas disponible en ligne, dans le dossier $physx$\SDKs\Docs\PhysXSDK.chm, avec $physx$ le dossier d'installation de PhysX (cette convention sera gardée tout au long du tutoriel).

II-A. Compiler les exemples

Pour tester votre installation, tentez de recompiler tous les exemples du SDK (la solution VS se trouve dans $physx$\Samples\compiler\vc[9|10]win[32|64]public\). Pour information, cette solution, après conversion, fonctionne parfaitement avec Visual Studio 2010, avec l'exception de quelques avertissements à l'exécution concernant la compilation de shaders, si les dossiers de recherche des en-têtes sont correctement configurés.

Pour PhysX 3.0, le SDK DirectX de novembre 2008 était fourni dans le SDK PhysX, ce qui posait quelques problèmes de compilation avec un compilateur et un Windows SDK récents. Pour PhysX 3.1, c'est la version de juin 2010 qui est fournie, il n'est donc plus nécessaire d'utiliser une version téléchargée sur le site officiel pour développeurs de DirectX, puis de configurer l'EDI en fonction.

II-B. Préparer l'environnement

Après s'être assuré que tout fonctionnait bien dans les exemples, on peut préparer l'environnement pour utiliser le PhysX SDK sans devoir reconfigurer chaque projet. Pour ce faire, il faut ajouter une série de chemins de recherche d'en-têtes :

 
Sélectionnez
$physx$\SDKs\pxtask\include
$physx$\SDKs\PhysXAPI\extensions
$physx$\SDKs\PhysXCommon\src
$physx$\SDKs\PxFoundation
$physx$\SDKs\PxFoundation\internal\include
$physx$\SDKs\PhysXProfileSDK\sdk\include
$physx$\SDKs\PhysXVisualDebuggerSDK\PVDCommLayer\Public
$physx$\SDKs\PhysXAPI
$physx$\SDKs\GeomUtils\src
$physx$\SDKs\RepX\include
$physx$\SDKs\PhysXAPI\characterkinematic

PxTask et PxFoundation ont changé de répertoire depuis la version 3.0.

Il faudra aussi lier une série de bibliothèques, à définir par projet (les fichiers d'import sont disponibles dans $physx$\SDKs\lib\win[32|64]\, il faut aussi l'indiquer à l'EDI, dans la même fenêtre que les fichiers d'en-tête, sous Library Directories). Voici les bibliothèques disponibles dans PhysX :

 
Sélectionnez
Foundation
PhysX3Common
PhysX3CharacterDynamic
PhysX3CharacterKinematic
PhysX3Vehicle
PhysX3Cooking
PhysX3
PhysX3Extensions
LowLevel
GeomUtils
SceneQuery
SimulationController
PvdRuntime
RepX3

Il est aussi possible d'utiliser des #pragma pour définir les bibliothèques à lier, cette méthode sera préférée par la suite, étant donné qu'elle dispense de fournir un fichier de projet et qu'elle explicite, depuis les sources, les bibliothèques externes à utiliser. Cependant, il faudra adapter ces lignes pour correspondre à l'environnement de compilation le cas échéant (notamment pour d'autres plateformes).

Évidemment, tout ceci n'est nécessaire que pour une utilisation complète de PhysX.

III. Utiliser le PhysX SDK

Après l'avoir installé, vérifié qu'il était bien installé, il est grand temps de l'utiliser « pour de vrai », avec « de vrais morceaux de code ».

III-A. Initialisation du SDK

Première chose à faire : initialiser le SDK. Cette opération nécessite plusieurs paramètres :

  • la version des en-têtes de PhysX (pour comparaison à l'exécution avec la version des DLL) ;
  • un rappel pour les allocations de mémoire ;
  • un rappel pour les erreurs ;
  • les tolérances (pour que le contenu donne toujours une simulation correcte à différentes échelles) ;
  • un booléen indiquant s'il faut (true) ou non (false, par défaut) tenir une trace des allocations mémoire (cela détériore fortement les performances mais aide au débogage).

Des rappels par défaut sont fournis ; une macro donne la version des en-têtes de PhysX ; finalement, des paramètres par défaut sont également fournis pour les tolérances. On peut donc initialiser le SDK très rapidement en utilisant ses paramètres par défaut, sans oublier de libérer les objets créés :

 
Sélectionnez
#include <iostream>
#include <PxPhysicsAPI.h>
#include <PxDefaultErrorCallback.h>
#include <PxDefaultAllocator.h> 

using namespace physx; 

// Pour Windows 32 bits : #pragma comment(lib, "PhysX3_x86.lib")
#pragma comment(lib, "PhysX3_x64.lib")
#pragma comment(lib, "Foundation.lib")
#pragma comment(lib, "PhysX3Extensions.lib")
// Pour Windows 32 bits : #pragma comment(lib, "PhysX3Cooking_x86.lib")
#pragma comment(lib, "PhysX3Cooking_x64.lib")
#pragma comment(lib, "PxTask.lib")

bool recordMemoryAllocations = true;
PxPhysics * mSDK = NULL;
PxDefaultErrorCallback pDefaultErrorCallback;
PxDefaultAllocator pDefaultAllocatorCallback;

int main()
{
	mSDK = PxCreatePhysics(PX_PHYSICS_VERSION, pDefaultAllocatorCallback, pDefaultErrorCallback, PxTolerancesScale(), recordMemoryAllocations);
	
	if(mSDK == NULL)
	{
		std::cerr << "An error has happened." << std::endl; 
		exit(1);
	}
	
	mSDK->release();
}

La version 3.0 du SDK n'obligeait pas à utiliser l'espace de noms physx, du moins avec Visual Studio 2010 ; avec la version 3.1, ne pas inclure la ligne using namespace physx; donnera un bon nombre d'erreurs de symboles non définis.

III-B. Rappel d'allocation

Il s'agit d'une classe que le SDK va appeler à chaque fois qu'il a besoin de mémoire. Il y a toujours une implémentation par défaut pour chaque plateforme supportée. L'espace alloué doit être aligné sur seize octets (afin d'accélérer les chargements des variables par le processeur depuis la mémoire). Sur les consoles, cela se fait automatiquement ; par contre, pour les autres plateformes, il faut appeler une version de malloc() prévue à cet effet.

À titre d'exemple, voici l'implémentation du rappel par défaut sous Windows 32 bits :

 
Sélectionnez
#include <malloc.h>
class PxDefaultAllocator : public PxAllocatorCallback
{
	void* allocate(size_t size, const char*, const char*, int)
	{
		return _aligned_malloc(size, 16);	
	}

	void deallocate(void* ptr)
	{
		_aligned_free(ptr);
	}
};

Et en 64 bits, où l'on peut avoir de la mémoire alignée sur seize bits sans plus chipoter (de même que pour PlayStation 3 et XBox) :

 
Sélectionnez
class PxDefaultAllocator : public PxAllocatorCallback
{
	void* allocate(size_t size, const char*, const char*, int)
	{
		void *ptr = ::malloc(size);	
		PX_ASSERT((reinterpret_cast<size_t>(ptr) & 15)==0);
		return ptr;
	}

	void deallocate(void* ptr)
	{
		::free(ptr);			
	}
};

Ce bout de code est extrait des extensions PhysX, disponible dans le fichier $physx$\SDKs\PhysXAPI\extensions\PxDefaultAllocator.h, à côté des implémentations par défaut pour d'autres plateformes.

Pour vérifier l'utilisation de mémoire dynamique du SDK, on peut changer cette implémentation pour correspondre aux besoins.

Cette fonction ne devrait jamais retourner NULL. S'il n'y a plus de mémoire disponible ou que l'allocation n'est pas possible pour une autre raison, il faut agir en conséquence (arrêter proprement l'application, par exemple).

Les trois derniers paramètres déterminent le nom du type de données en cours d'allocation, ainsi que l'endroit où l'allocation a été demandée (les valeurs des macros __FILE__ et __LINE__), afin d'allouer, au besoin, la mémoire d'un tas spécifique.

L'état du SDK ne devrait pas être modifié par une (dés)allocation mémoire ; ces deux fonctions doivent être thread-safe (elles peuvent être appelées depuis le thread utilisateur ou un thread de simulation physique).

III-C. Rappel d'erreur

Cette classe occupe un rôle similaire, mais pour les erreurs : ce retour sera appelé à chaque fois que le SDK doit transmettre des messages d'erreur. Voici l'implémentation par défaut :

 
Sélectionnez
class PxDefaultErrorCallback : public PxErrorCallback
{
public:
        PxDefaultErrorCallback();
        ~PxDefaultErrorCallback();

        virtual void reportError(PxErrorCode::Enum code, const char* message, const char* file, int line);
};

void PxDefaultErrorCallback::reportError(PxErrorCode::Enum e, const char* message, const char* file, int line)
{
        const char* errorCode = NULL;

        switch (e)
        {
        case PxErrorCode::eINVALID_PARAMETER:
                errorCode = "invalid parameter";
                break;
        case PxErrorCode::eINVALID_OPERATION:
                errorCode = "invalid operation";
                break;
        case PxErrorCode::eOUT_OF_MEMORY:
                errorCode = "out of memory";
                break;
        case PxErrorCode::eDEBUG_INFO:
                errorCode = "info";
                break;
        case PxErrorCode::eDEBUG_WARNING:
                errorCode = "warning";
                break;
        default:
                errorCode = "unknown error";
                break;
        }

        printf("%s (%d) :", file, line);
        printf("%s", errorCode);
        printf(" : %s\n", message);
}

Ces deux rappels (d'allocation et d'erreur) par défaut sont définis dans les extensions de PhysX.

III-D. Extensions et cooking

Deux bibliothèques optionnelles sont généralement aussi utilisées avec PhysX : les extensions (dont le débogueur visuel et les jointures) et le cooking (certaines données sont trop lourdes à calculer en temps réel, on effectue donc ces calculs une fois pour toutes, par exemple en préparation de tests sur console).

On les initialise respectivement comme ceci :

 
Sélectionnez
if (! PxInitExtensions(*mSDK))
{
	std::cerr << "An error has happened." << std::endl; 
	exit(1);
}

PxCooking * mCooking = PxCreateCooking(PX_PHYSICS_VERSION, & mSDK->getFoundation(), PxCookingParams());
if (!mCooking)
{
	std::cerr << "An error has happened." << std::endl; 
	exit(1);
}

Pour initialiser les extensions, il est nécessaire d'inclure, en plus de PxPhysicsAPI.h, PxExtensionsAPI.h.

Initialiser les extensions n'est requis qu'en cas d'utilisation de fonctions nécessitant des allocations. Sinon, cela n'est pas nécessaire (si la seule partie utilisée des extensions est les rappels par défaut, cela n'est pas utile).

Il est préférable également de les fermer à la fin de l'application (ce n'est pas encore requis) :

 
Sélectionnez
PxCloseExtensions();
mCooking->release();

Pour le cooking, veillez à bien passer un objet de fondations venant de l'objet SDK, car ces fondations sont prévues comme un singleton (cela permet aussi de s'assurer que la gestion de la mémoire sera correcte pour les objets passant du cooking au SDK).

On a donc une initialisation et une fermeture propre des trois parties comme ceci :

 
Sélectionnez
#include <iostream>
#include <PxPhysicsAPI.h>
#include <PxDefaultErrorCallback.h>
#include <PxDefaultAllocator.h> 
#include <PxExtensionsAPI.h>

using namespace physx; 

#pragma comment(lib, "Foundation.lib")
#pragma comment(lib, "PhysX3_x64.lib") // Pour Windows 32 bits : #pragma comment(lib, "PhysX3_x86.lib")
#pragma comment(lib, "PhysX3Extensions.lib")
#pragma comment(lib, "PhysX3Cooking_x64.lib") // Pour Windows 32 bits : #pragma comment(lib, "PhysX3Cooking_x86.lib")
#pragma comment(lib, "GeomUtils.lib")
#pragma comment(lib, "PxTask.lib")

bool recordMemoryAllocations = true;
PxPhysics * mSDK = NULL;
PxCooking * mCooking = NULL;
PxDefaultErrorCallback pDefaultErrorCallback;
PxDefaultAllocator pDefaultAllocatorCallback;

int main()
{
	mSDK = PxCreatePhysics(PX_PHYSICS_VERSION, pDefaultAllocatorCallback, pDefaultErrorCallback, PxTolerancesScale(), recordMemoryAllocations);
	if(! mSDK)
	{
		std::cerr << "An error has happened." << std::endl; 
		exit(1);
	}
	
	if (! PxInitExtensions(*mSDK))
	{
		std::cerr << "An error has happened." << std::endl; 
		exit(1);
	}

	mCooking = PxCreateCooking(PX_PHYSICS_VERSION, & mSDK->getFoundation(), PxCookingParams());
	if (! mCooking)
	{
		std::cerr << "An error has happened." << std::endl; 
		exit(1);
	}
	
	mCooking->release();
	PxCloseExtensions();
	mSDK->release();
}

Tout ceci ne doit être fait qu'une fois par exécution du programme (même si, dans cet exemple minimaliste, c'est tout ce qui est fait).

On peut cependant commencer à améliorer cet exemple, en séparant main() des initialisations. On peut simplement déplacer le code dans des fonctions auxiliaires, étant donné que tous les objets sont déclarés dans l'espace global (pour faciliter le code, cela n'est pas le meilleur exemple à reproduire, il vaudrait mieux tout placer dans un objet, par exemple) :

 
Sélectionnez
#include <iostream>
#include <PxPhysicsAPI.h>
#include <PxDefaultErrorCallback.h>
#include <PxDefaultAllocator.h> 
#include <PxExtensionsAPI.h>

using namespace physx; 

#pragma comment(lib, "Foundation.lib")
#pragma comment(lib, "PhysX3_x64.lib") // Pour Windows 32 bits : #pragma comment(lib, "PhysX3_x86.lib")
#pragma comment(lib, "PhysX3Extensions.lib")
#pragma comment(lib, "PhysX3Cooking_x64.lib") // Pour Windows 32 bits : #pragma comment(lib, "PhysX3Cooking_x86.lib")
#pragma comment(lib, "GeomUtils.lib")
#pragma comment(lib, "PxTask.lib")

bool recordMemoryAllocations = true;
PxPhysics * mSDK = NULL;
PxCooking * mCooking = NULL;
PxDefaultErrorCallback pDefaultErrorCallback;
PxDefaultAllocator pDefaultAllocatorCallback;

void initPhysX();
void releasePhysX();

int main()
{
	initPhysX();
	releasePhysX();
}

void initPhysX()
{
	mSDK = PxCreatePhysics(PX_PHYSICS_VERSION, pDefaultAllocatorCallback, pDefaultErrorCallback, PxTolerancesScale(), recordMemoryAllocations);
	if(! mSDK)
	{
		std::cerr << "An error has happened." << std::endl; 
		exit(1);
	}
	
	if (! PxInitExtensions(*mSDK))
	{
		std::cerr << "An error has happened." << std::endl; 
		exit(1);
	}

	mCooking = PxCreateCooking(PX_PHYSICS_VERSION, & mSDK->getFoundation(), PxCookingParams());
	if (! mCooking)
	{
		std::cerr << "An error has happened." << std::endl; 
		exit(1);
	}
}

void releasePhysX()
{
	mCooking->release();
	PxCloseExtensions();
	mSDK->release();
}

Pour la suite, on ne présentera plus que le code de main(), étant donné qu'il est maintenant très concis.

IV. Une scène

Il faut commencer par créer un monde pour lancer la moindre simulation. En terminologie PhysX, c'est une scène PxScene qu'il faut instancier.

 
Sélectionnez
PxScene * mScene;

Chaque planète du système solaire est différente, au moins en termes de gravité. Il faut donc prévoir un set de paramètres pour gérer ces disparités. On peut commencer par définir la gravité, dans la direction souhaitée :

 
Sélectionnez
PxSceneDesc sceneDesc(mSDK->getTolerancesScale());
sceneDesc.gravity = PxVec3(0.0f, -9.81f, 0.0f);

On donne un vecteur pour la gravité, avec ses trois composantes : abscisse x, ordonnée y, cote z. Le plan XZ est celui du sol, les ordonnées sont donc perpendiculaires au sol, d'où la composante négative du vecteur. Ceci n'est jamais que pure convention, on pourrait décider de faire tout autrement sans que le moteur n'en soit affecté, il suffit de changer l'interprétation des données.

On peut également définir des retours lors d'événements lors de la simulation, des limites et des bordures, un seuil minimal de vitesse pour les rebondissements et bien d'autres, la référence est alors extrêmement utile. Cependant, ces valeurs sont données ou calculées à partir des tolérances passées en paramètre, il vaut donc mieux, autant que possible, les définir dans l'objet SDK plutôt que dans la scène (voir la documentation).

Il faut encore définir un mécanisme de filtrage des collisions. On peut utiliser celui par défaut de PhysX (toujours défini dans les extensions) :

 
Sélectionnez
PxSimulationFilterShader gDefaultFilterShader = PxDefaultSimulationFilterShader;

// ...

sceneDesc.filterShader = & gDefaultFilterShader;

On peut ici le définir directement, car on est sûr qu'il n'a pas pu être installé autre part. Par contre, dans les exemples de SDK, une vérification est d'abord exécutée, car une fonction (dépendante de l'exemple) est d'abord appelée sur ces descriptions de scène et cette fonction peut définir des paramètres à sa guise (c'est une des complexités ajoutées par le framework des exemples).

Finalement, il faut définir des objets qui vont gérer la répartition des tâches de simulation sur les threads (tant sur CPU que sur GPU, le GPU n'étant supporté que sous Windows). On définit ici le nombre de threads à utiliser à 1, d'autres valeurs permettront d'utiliser le multithreading par PhysX.

 
Sélectionnez
mCpuDispatcher = PxDefaultCpuDispatcherCreate(1);
if(!mCpuDispatcher)
    std::cerr << "PxDefaultCpuDispatcherCreate failed!";
sceneDesc.cpuDispatcher = mCpuDispatcher;

On peut enfin créer la scène avec tous ces paramètres :

 
Sélectionnez
mScene = mSDK->createScene(sceneDesc);
if (!mScene)
    std::cerr << "createScene failed!";

Avant de conclure sur les scènes, sachez qu'il est possible de faire varier la gravité de la scène après sa création. void PxScene::setGravity(const PxVec3 &) change cette gravité, tandis que PxVec3 getGravity() const la récupère. Il reste important de regarder les indications de la documentation sur l'utilisation de ces fonctions.

Il est difficile de ne pas remarquer que PhysX est prévu pour être personnalisé autant que possible pour une parfaite intégration avec l'existant, sans que les performances ne puissent en pâtir. Remarquons aussi que toutes les méthodes de PxScene sont virtuelles, permettant ainsi d'hériter et de redéfinir des méthodes au besoin.

On a donc ce code (en se basant sur le code complet disponible précédemment) :

 
Sélectionnez
PxSimulationFilterShader pDefaultFilterShader = PxDefaultSimulationFilterShader;
PxDefaultCpuDispatcher * mCpuDispatcher;
PxScene * mScene;

int main()
{
	initPhysX();

	PxSceneDesc sceneDesc(mSDK->getTolerancesScale());
	sceneDesc.gravity = PxVec3(0.0f, -9.81f, 0.0f);
	sceneDesc.filterShader = pDefaultFilterShader;

	mCpuDispatcher = PxDefaultCpuDispatcherCreate(1);
	if(!mCpuDispatcher)
		std::cerr << "PxDefaultCpuDispatcherCreate failed!";
	sceneDesc.cpuDispatcher = mCpuDispatcher;

	PxScene * mScene = mSDK->createScene(sceneDesc);
	if (!mScene)
		std::cerr << "createScene failed!";

	releasePhysX();
}

On peut également déplacer ce nouveau code dans une fonction auxiliaire, afin de toujours garder une fonction main() bien lisible, PhysX ayant besoin de beaucoup de déclarations.

En appelant la méthode release() de la scène, on supprime automatiquement tous les objets liés (acteurs, formes, etc.), qui seront vus plus tard.

V. Premiers acteurs

Au commencement, Dieu créa les cieux et la terre. La terre était informe et vide : il y avait des ténèbres à la surface de l'abîme et l'esprit de Dieu se mouvait au-dessus des eaux. (Ge, I, 1-2).

Il faut donc créer quelque chose dans ce monde vide. Première constatation : toute chose est faite d'une matière, au moins. On doit donc créer un matériau avant toute chose :

 
Sélectionnez
PxMaterial * mMaterial;

mMaterial = mSDK->createMaterial(0.5f, 0.5f, 0.1f); 
if(!mMaterial)
    std::cerr << "createMaterial failed!";

Les trois paramètres du matériau sont : la friction statique, la friction dynamique et la restitution. Les frictions déterminent les forces de frottement exercées par cet objet ; la restitution, l'énergie conservée après un choc. On peut pousser le vice : une friction anisotropique est un frottement dont le coefficient varie en fonction de la direction du corps en contact. La documentation donne les méthodes à appeler pour cela.

Il faut bien rappeler que PhysX est un moteur physique en temps réel, on n'a donc en pratique jamais le temps de résoudre exactement tous les systèmes en jeu dans une simulation pour chaque image. On doit donc effectuer des approximations, à chaque étape, ce qui fait qu'une boîte pourrait glisser sur une pente, alors qu'elle ne devrait pas, à cause de ces erreurs. Il faut toujours garder cela en tête : non, les simulations ne seront pas exactes, mais suffisamment précises pour les besoins d'un jeu (en règle générale).

Dans une utilisation réelle, créer autant de matériaux que nécessaire serait plus qu'une plaie. On voudrait donc externaliser toutes ces données en dehors du code source : c'est pour ça qu'il est possible de sérialiser ces objets (la scène ou le SDK entier). Cela sort cependant du cadre de cette introduction.

On veut ensuite poser un objet sur un plan. L'axe Y représentant la verticale (par convention, comme expliqué plus haut), le plan devra donc être parallèle à XZ. En effet, prenant la gravité telle que définie précédemment, un objet ne pourra être stable que sur un tel plan. On commence par en définir l'orientation.

 
Sélectionnez
PxReal d = 0.0f;
PxTransform pose = PxTransform(PxVec3(0.0f, d, 0.0f), PxQuat(PxHalfPi, PxVec3(0.0f, 0.0f, 1.0f)));

Si on avait voulu un plan orienté différemment, on aurait agi comme ceci :

 
Sélectionnez
// Perpendiculaire à l'axe X
pose = PxTransform(PxVec3(d, 0.0f, 0.0f));
// Perpendiculaire à l'axe Z
pose = PxTransform(PxVec3(0.0f, 0.0f, d), PxQuat(-PxHalfPi, PxVec3(0.0f, 1.0f, 0.0f)));

On crée ensuite le plan et on l'ajoute à la scène. Il s'agit d'un objet rigide et statique, d'où l'utilisation de la classe PxRigidStatic ; sa forme est, par définition, planaire ; on utilisera le matériau défini précédemment. Il faut cependant l'expliciter au moteur, il n'est pas capable de comprendre les intentions du développeur sans cela.

 
Sélectionnez
PxRigid* plane = mSDK->createRigidStatic(pose);
if (! plane)
    std::cerr << "create plane failed!";
PxShape * shape = plane->createShape(PxPlaneGeometry(), * mMaterial);
if (! shape)
    std::cerr << "create shape failed!";
mScene->addActor(* plane);

On peut également se simplifier un peu la vie en utilisant les fonctions auxiliaires disponibles dans les extensions de PhysX :

 
Sélectionnez
PxReal density = 1.0f;
PxTransform(PxVec3(0.0f, 5.0f, 0.0f), PxQuat::createIdentity());
PxVec3 dimensions(1.0f, 1.0f, 1.0f);
PxBoxGeometry geometry(dimensions);

PxRigidDynamic * actor = PxCreateDynamic(* mSDK, transform, geometry, * mMaterial, density);
if (!actor)
    std::cerr << "create actor failed!";
mScene->addActor(* actor);

Ceci crée un cube (tant acteur que forme), avec une certaine densité donnée, ce qui permet de calculer la masse et le tenseur d'inertie.

En résumé, voici ce que l'on vient de faire :

 
Sélectionnez
PxMaterial * mMaterial;
PxRigid* plane;
PxShape * shape;
PxRigidDynamic * actor;

int main()
{
	initPhysX();
	initScene();
	initActors();
	releasePhysX();
}

void initActors()
{
	// Material
	mMaterial = mSDK->createMaterial(0.5f, 0.5f, 0.1f); 
	if(! mMaterial)
		std::cerr << "createMaterial failed!";

	PxReal d = 0.0f;
	PxTransform pose = PxTransform(PxVec3(0.0f, d, 0.0f), PxQuat(PxHalfPi, PxVec3(0.0f, 0.0f, 1.0f)));

	// Plane
	plane = mSDK->createRigidStatic(pose);
	if (! plane)
		std::cerr << "create plane failed!";
	shape = plane->createShape(PxPlaneGeometry(), * mMaterial);
	if (! shape)
		std::cerr << "create shape failed!";
	mScene->addActor(* plane);

	// Cube

	PxReal density = 1.0f;
	PxTransform(PxVec3(0.0f, 5.0f, 0.0f), PxQuat::createIdentity());
	PxVec3 dimensions(1.0f, 1.0f, 1.0f);
	PxBoxGeometry geometry(dimensions);

	actor = PxCreateDynamic(* mSDK, transform, geometry, * mMaterial, density);
	if (! actor)
		std::cerr << "create actor failed!";
	mScene->addActor(* actor);
}

La scène est créée, les objets sont en place mais, pour le moment, ils ne font rien.

VI. Récupérer les données de la simulation

On commence par définir le pas de la simulation désiré : on doit afficher au moins une vingtaine d'images par seconde pour avoir une sensation de fluidité, il faut avoir soixante rendus par seconde pour atteindre la fluidité parfaitement.

Pour faire avancer la scène d'un pas (float) :

 
Sélectionnez
float mStepSize = 1.0f / 60.0f;
mScene->simulate(mStepSize);

Cette fonction simulate() est exécutée de manière asynchrone, en général (en fonction de l'implémentation du gestionnaire de tâches) : la simulation aura débuté dans un thread séparé. On a cependant besoin des résultats de la simulation : comment savoir quand ils sont disponibles ? On peut appeler PxScene::fetchResults(), qui renvoie un booléen indiquant si les résultats de la simulation sont disponibles ou non. On peut lui passer un booléen en paramètre : PxScene::fetchResults(true) ne va retourner que quand les résultats seront disponibles, pas avant.

 
Sélectionnez
mScene->fetchResults(true);

Dernière étape pour chaque itération : récupérer les données et les passer à qui de droit (généralement, le moteur 3D, pour qu'il affiche les changements calculés par le moteur physique). On doit pour cela les demander forme par forme à PhysX, grâce à la méthode statique inline PxTransform PxShapeExt::getGlobalPose(const PxShape & shape).

 
Sélectionnez
PxTransform np = PxShapeExt::getGlobalPose(* shape);

Se pose quand même un problème : on n'a pas de forme pour le cube ! Petite astuce :

 
Sélectionnez
PxU32 nShapes = actor->getNbShapes(); 
PxShape** shapes = new PxShape*[nShapes];
actor->getShapes(shapes, nShapes);

while (nShapes--) 
	foo(shapes[nShapes]); // on s'occupe de la forme courante

delete [] shapes;

Étant donné qu'il n'y a rien pour visualiser graphiquement les translations imposées par la physique, on va simplement afficher les vecteurs de transformation. Ceci étant le dernier exemple, on va le montrer en entier.

 
Sélectionnez
#include <iostream>
#include <PxPhysicsAPI.h>
#include <PxDefaultErrorCallback.h>
#include <PxDefaultAllocator.h> 
#include <PxExtensionsAPI.h>
#include <PxTask.h>

using namespace physx; 

#pragma comment(lib, "Foundation.lib")
#pragma comment(lib, "PhysX3_x64.lib") // Pour Windows 32 bits : #pragma comment(lib, "PhysX3_x86.lib")
#pragma comment(lib, "PhysX3Extensions.lib")
#pragma comment(lib, "GeomUtils.lib")
#pragma comment(lib, "PxTask.lib")

bool recordMemoryAllocations = true;
PxPhysics * mSDK = NULL;
PxCooking * mCooking = NULL;
PxDefaultErrorCallback pDefaultErrorCallback;
PxDefaultAllocator pDefaultAllocatorCallback;
PxSimulationFilterShader pDefaultFilterShader = PxDefaultSimulationFilterShader;
PxDefaultCpuDispatcher * mCpuDispatcher;
PxScene * mScene;
PxMaterial * mMaterial;
PxRigidStatic * plane;
PxShape * shape;
PxRigidDynamic * actor;
float mStepSize = 1.0f / 42.0f;

void initPhysX();
void initScene();
void initActors();
bool PhysXLoop();
void ShowTransformations();
void TransformPrint(PxShape*);
void releasePhysX();

int main()
{
	std::cout << "Starting..." << std::endl;

	initPhysX();
	initScene();
	initActors();

	std::cout << "Warmed up; looping" << std::endl;
	while(PhysXLoop()) ;
	std::cout << "\nLooping ended" << std::endl;

	releasePhysX();
	std::cout << "Closed" << std::endl;

	int foo;
	std::cin >> foo;
}

void initPhysX()
{
	mSDK = PxCreatePhysics(PX_PHYSICS_VERSION, pDefaultAllocatorCallback, pDefaultErrorCallback, PxTolerancesScale(), recordMemoryAllocations);
	if(! mSDK)
	{
		std::cerr << "An error has happened." << std::endl; 
		exit(1);
	}
	
	if (! PxInitExtensions(*mSDK))
	{
		std::cerr << "An error has happened." << std::endl; 
		exit(1);
	}
}
void initScene()
{
	PxSceneDesc sceneDesc(mSDK->getTolerancesScale());
	sceneDesc.gravity = PxVec3(0.0f, -9.81f, 0.0f);
	sceneDesc.filterShader = pDefaultFilterShader;

	mCpuDispatcher = PxDefaultCpuDispatcherCreate(1);
	if(!mCpuDispatcher)
		std::cerr << "PxDefaultCpuDispatcherCreate failed!";
	sceneDesc.cpuDispatcher = mCpuDispatcher;

	mScene = mSDK->createScene(sceneDesc);
	if (!mScene)
		std::cerr << "createScene failed!";
}
void initActors()
{
	// Material
	mMaterial = mSDK->createMaterial(0.5f, 0.5f, 0.1f); 
	if(! mMaterial)
		std::cerr << "createMaterial failed!";

	PxReal d = 0.0f;
	PxTransform pose = PxTransform(PxVec3(0.0f, d, 0.0f), PxQuat(PxHalfPi, PxVec3(0.0f, 0.0f, 1.0f)));

	// Plane
	plane = mSDK->createRigidStatic(pose);
	if (! plane)
		std::cerr << "create plane failed!";
	shape = plane->createShape(PxPlaneGeometry(), * mMaterial);
	if (! shape)
		std::cerr << "create shape failed!";
	mScene->addActor(* plane);

	// Cube

	PxReal density = 1.0f;
	PxTransform transform = PxTransform(PxVec3(0.0f, 5.0f, 0.0f), PxQuat::createIdentity());
	PxVec3 dimensions(1.0f, 1.0f, 1.0f);
	PxBoxGeometry geometry(dimensions);

	actor = PxCreateDynamic(* mSDK, transform, geometry, * mMaterial, density);
	if (! actor)
		std::cerr << "create actor failed!";
	mScene->addActor(* actor);
}
bool PhysXLoop()
{
	static int i = 0; 

	std::cout << "\n\t" << i << "\n" << std::endl;

	mScene->simulate(mStepSize);
	mScene->fetchResults(true);

	ShowTransformations();

	++i; 
	if(i == 42)
		return false;
	else
		return true;
}
void ShowTransformations()
{
	std::cout << "\tplane" << std::endl;
	TransformPrint(shape);

	std::cout << "\tcube" << std::endl;
	PxU32 nShapes = actor->getNbShapes(); 
	PxShape** shapes = new PxShape*[nShapes];
	actor->getShapes(shapes, nShapes);

	while (nShapes--) 
		TransformPrint(shapes[nShapes]);

	delete [] shapes;
}
void TransformPrint(PxShape * s)
{
	PxTransform np = PxShapeExt::getGlobalPose(* s);
	std::cout << "\t( " << np.p.x << " ; " << np.p.y << " ; " << np.p.z << " )" << std::endl;
}
void releasePhysX()
{
	PxCloseExtensions();
	mScene->release();
	mSDK->release();
}

On peut ensuite s'amuser à changer quelques paramètres, voir si tout s'adapte bien. Par exemple, si on multiplie à certains moments la gravité par deux, on obtient cette fonction de boucle :

 
Sélectionnez
bool PhysXLoop()
{
	static int i = 0; 

	std::cout << "\n\t" << i << "\n" << std::endl;

	mScene->simulate(mStepSize);
	mScene->fetchResults(true);

	ShowTransformations();

	if(i % 4)
		mScene->setGravity(2 * mScene->getGravity());

	++i; 
	if(i == 42)
		return false;
	else
		return true;
}

Les résultats obtenus vont bien varier fortement (les vecteurs de déplacement vont sans cesse augmenter en norme, puisque la gravité devient plus forte et que le cube est en chute libre). De même, si on oriente la gravité différemment en changeant son vecteur dans l'initialisation de la scène, les résultats vont varier, la composante Z de la chute du vecteur augmentant :

 
Sélectionnez
void initScene()
{
	PxSceneDesc sceneDesc(mSDK->getTolerancesScale());
	sceneDesc.gravity = PxVec3(0.0f, -9.81f, 2.0f);
	sceneDesc.filterShader = pDefaultFilterShader;

	mCpuDispatcher = PxDefaultCpuDispatcherCreate(1);
	if(!mCpuDispatcher)
		std::cerr << "PxDefaultCpuDispatcherCreate failed!";
	sceneDesc.cpuDispatcher = mCpuDispatcher;

	mScene = mSDK->createScene(sceneDesc);
	if (!mScene)
		std::cerr << "createScene failed!";
}

Tout ceci pour vous convaincre que la simulation a bien lieu, même si tout se passe très vite.

VII. Affichage des données

Pour vérifier que tout ceci a bien un sens, on va chercher à voir le mouvement simulé du corps. On va rester dans le domaine du très simple : modifier la fonction d'affichage des données pour les présenter sous une forme propice à l'exploitation sous MATLAB, lequel sera utilisé pour dessiner un graphique. Voici donc l'entièreté du code, qui a été quelque peu nettoyé pour qu'une simple commande donne les instructions MATLAB à exécuter pour remplir les vecteurs qui seront utilisés pour le dessin (2). Les deux principales modifications sont la longueur de la simulation (quarante-deux secondes au lieu d'une seule) et la sortie (qui est mise dans un format facilement interprétable par MATLAB).

 
Sélectionnez
#include <iostream>
#include <PxPhysicsAPI.h>
#include <PxDefaultErrorCallback.h>
#include <PxDefaultAllocator.h> 
#include <PxExtensionsAPI.h>
#include <PxTask.h>

using namespace physx; 

#pragma comment(lib, "Foundation.lib")
#pragma comment(lib, "PhysX3_x64.lib") // Pour Windows 32 bits : #pragma comment(lib, "PhysX3_x86.lib")
#pragma comment(lib, "PhysX3Extensions.lib")
#pragma comment(lib, "GeomUtils.lib")
#pragma comment(lib, "PxTask.lib")

bool recordMemoryAllocations = true;
PxPhysics * mSDK = NULL;
PxCooking * mCooking = NULL;
PxDefaultErrorCallback pDefaultErrorCallback;
PxDefaultAllocator pDefaultAllocatorCallback;
PxSimulationFilterShader pDefaultFilterShader = PxDefaultSimulationFilterShader;
PxDefaultCpuDispatcher * mCpuDispatcher;
PxScene * mScene;
PxMaterial * mMaterial;
PxRigidStatic * plane;
PxShape * shape;
PxRigidDynamic * actor;
float mStepSize = 1.0f / 42.0f;

void initPhysX();
void initScene();
void initActors();
bool PhysXLoop();
void ShowTransformations(int);
void TransformPrint(PxShape*, int);
void releasePhysX();

int main()
{
	initPhysX();
	initScene();
	initActors();

	while(PhysXLoop()) ;

	releasePhysX();

	int foo;
	std::cin >> foo;
}

void initPhysX()
{
	mSDK = PxCreatePhysics(PX_PHYSICS_VERSION, pDefaultAllocatorCallback, pDefaultErrorCallback, PxTolerancesScale(), recordMemoryAllocations);
	if(! mSDK)
	{
		std::cerr << "An error has happened." << std::endl; 
		exit(1);
	}
	
	if (! PxInitExtensions(*mSDK))
	{
		std::cerr << "An error has happened." << std::endl; 
		exit(1);
	}
}
void initScene()
{
	PxSceneDesc sceneDesc(mSDK->getTolerancesScale());
	sceneDesc.gravity = PxVec3(0.0f, -9.81f, 0.0f);
	sceneDesc.filterShader = pDefaultFilterShader;

	mCpuDispatcher = PxDefaultCpuDispatcherCreate(1);
	if(!mCpuDispatcher)
		std::cerr << "PxDefaultCpuDispatcherCreate failed!";
	sceneDesc.cpuDispatcher = mCpuDispatcher;

	mScene = mSDK->createScene(sceneDesc);
	if (!mScene)
		std::cerr << "createScene failed!";
}
void initActors()
{
	// Material
	mMaterial = mSDK->createMaterial(0.5f, 0.5f, 0.1f); 
	if(! mMaterial)
		std::cerr << "createMaterial failed!";

	PxReal d = 0.0f;
	PxTransform pose = PxTransform(PxVec3(0.0f, d, 0.0f), PxQuat(PxHalfPi, PxVec3(0.0f, 0.0f, 1.0f)));

	// Plane
	plane = mSDK->createRigidStatic(pose);
	if (! plane)
		std::cerr << "create plane failed!";
	shape = plane->createShape(PxPlaneGeometry(), * mMaterial);
	if (! shape)
		std::cerr << "create shape failed!";
	mScene->addActor(* plane);

	// Cube

	PxReal density = 1.0f;
	PxTransform transform = PxTransform(PxVec3(0.0f, 5.0f, 0.0f), PxQuat::createIdentity());
	PxVec3 dimensions(1.0f, 1.0f, 1.0f);
	PxBoxGeometry geometry(dimensions);

	actor = PxCreateDynamic(* mSDK, transform, geometry, * mMaterial, density);
	if (! actor)
		std::cerr << "create actor failed!";
	mScene->addActor(* actor);
}
bool PhysXLoop()
{
	static int i = 0; 

	mScene->simulate(mStepSize);
	mScene->fetchResults(true);

	ShowTransformations(i);

	++i; 
	if(i == 42 * 42)
		return false;
	else
		return true;
}
void ShowTransformations(int step)
{
	PxU32 nShapes = actor->getNbShapes(); 
	PxShape** shapes = new PxShape*[nShapes];
	actor->getShapes(shapes, nShapes);

	while (nShapes--) 
		TransformPrint(shapes[nShapes], step);

	delete [] shapes;
}
void TransformPrint(PxShape * s, int step)
{
	PxTransform np = PxShapeExt::getGlobalPose(* s);
	std::cout << "x(" << step + 1 << ") = " << np.p.x << "; " << std::endl;
	std::cout << "y(" << step + 1 << ") = " << np.p.y << "; " << std::endl;
	std::cout << "z(" << step + 1 << ") = " << np.p.z << "; " << std::endl;
}
void releasePhysX()
{
	PxCloseExtensions();
	mScene->release();
	mSDK->release();
}

Une fois compilé, il suffit d'exécuter ce code comme ceci pour en récupérer les données :

 
Sélectionnez
physx.exe > data

Dans MATLAB, une fois toutes les instructions de data exécutées, on peut lancer l'affichage du graphe :

 
Sélectionnez
plot3(x,y,z)
xlabel('X')
ylabel('Y')
zlabel('Z')
grid on

Ce qui donnera le résultat suivant :

Image non disponible

VIII. Redistribution

Depuis PhysX 2.8.4, il n'est plus nécessaire que le client installe un SystemSoftware pour le support de PhysX. Il est donc nécessaire de distribuer les bonnes DLL (sous Windows) avec l'application pour qu'elle puisse s'exécuter sans encombre. Dans la suite, * remplacera x86 ou x64, en fonction de la plateforme visée (32 ou 64 bits). Trois DLL au plus seront nécessaires :

  • PhysX3_*.dll, toujours requise, elle contient le cœur du moteur ;
  • PhysX3Cooking_*.dll, uniquement en cas d'utilisation du module de cooking (préparation des meshes avant utilisation) ;
  • PhysX3GPU_*.dll, uniquement si l'application est prévue pour lancer des simulations sur le GPU.

IX. Conclusions

L'utilisation de PhysX semble naturelle à tout qui connaît au moins un peu les principes physiques et géométriques sous-jacents.

Merci à Alexandre Laurent pour ses commentaires lors de la rédaction ! Merci à Claude Leloup et Mahefasoa pour leur relecture orthographique !

Cinématique
La cinématique, des mouvements monodimensionnels
La cinématique, des mouvements à plusieurs dimensions
Dynamique
La dynamique et les lois de Newton
Le principe fondamental de la statique
PhysX 3.0
Premiers pas
PhysX 3.1
Premiers pas

Certains font remarquer que, malgré la grande qualité du SDK, sa distribution est médiocre : notamment, une documentation pas entièrement mise à jour, les difficultés d'intégration à Visual Studio 2010, des exemples sans fichier de projet.
Pour rappel, les indices en MATLAB commencent à 1, non à 0, ce qui oblige à faire un step + 1 dans l'affichage.

  

Copyright © 2011 Thibaut Cuvelier. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.