Premiers pas avec PhysX 3.0

Thibaut Cuvelier

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

- Titre : Premiers pas avec PhysX 3.0

- Auteur : Thibaut Cuvelier

- Parution : 9 septembre 2011

- Mise à jour : 18 octobre 2011

- Licence : 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.

Sommaire

- I. PhysX

- I-A. Petit historique

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

- I-C. Succès de PhysX

- II. Obtenir le PhysX SDK

- II-A. Compiler les exemples

- II-B. Préparer l'environnement

- III. Utiliser le PhysX SDK

- III-A. Initialisation du SDK

- III-B. Rappel d'allocation

- III-C. Rappel d'erreur

- III-D. Extensions et cooking

- IV. Une scène

- V. Premiers acteurs

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

- VII. Conclusions

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 :

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.

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 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.

Le SDK pour Windows est prévu pour Microsoft Visual Studio 2008 SP1, il est donc préférable pour la suite d'utiliser cet EDI. L'édition express convient parfaitement à l'usage et est disponible gratuitement. Les versions postérieures sont également supportées. 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 : $physx$\Docs\PhysXSDK.chm pour la version 3.0.2 ; pour la 3.0.1, $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\vc9win[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.

Si vous utilisez une version récente du Windows SDK (par exemple, livrée avec Visual Studio 2010), faites attention à la version antique du DirectX SDK livrée dans le PhysX SDK (novembre 2008) ! Cela peut poser quelques erreurs un peu spéciales :

2>  D3D9RendererVertexBuffer.cpp
2>C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\include\objidl.h(11280): error C2061: syntax error : identifier '__RPC__out_xcount_part'
2>C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\include\objidl.h(11281): error C2059: syntax error : ')'
2>C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\include\objidl.h(11281): fatal error C1903: unable to recover from previous error(s); stopping compilation

Il faut donc installer la dernière version du Windows SDK et du DirectX SDK. Il faut s'assurer que les en-têtes du Windows SDK sont bien avant ceux du DirectX SDK dans la liste des dossiers d'en-têtes de Visual Studio (le plus simple est d'installer le DirectX SDK puis le Windows SDK). Si l'erreur est toujours présente, il faut empêcher Visual Studio d'aller voir les en-têtes de la version du DirectX SDK livrée avec le PhysX SDK (renommer ou supprimer le dossier $physx$\externals\microsoft\directx).

Pour configurer les dossiers d'en-têtes dans Visual Studio 2008 : Tools > Options > Project and Solutions > VC++ Directories. Y ajouter d'abord le Windows SDK puis le DirectX SDK dans la section des fichiers d'en-têtes.

Pour configurer les dossiers d'en-têtes dans Visual Studio 2010 : View > Other Windows > Property Manager. Dans un projet (n'importe lequel), choisir un profil de compilation correspondant à l'environnement de compilation (32 ou 64 bits). Double-cliquer sur Microsoft.Cpp.[Win32|x64].user, Common Properties > VC++ Directories > Include Directories. Y ajouter d'abord le Windows SDK puis le DirectX SDK dans la section des fichiers d'en-tête. Ceci permet de les configurer globalement, non par projet.

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 :

$physx$\SDKs\PhysXAPI\pxtask\include
$physx$\SDKs\PhysXAPI\extensions
$physx$\SDKs\PhysXCommon\src
$physx$\SDKs\PhysXAPI\foundation
$physx$\SDKs\PhysXAPI\foundation\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

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 :

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 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 :

#include <iostream>
#include <PxPhysicsAPI.h>
#include <PxDefaultErrorCallback.h>
#include <PxDefaultAllocator.h>

// 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();
}

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 :

#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);
}
};

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 :

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): %s: %s\n", file, line, errorCode, 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 :

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) :

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 :

#include <iostream>
#include <PxPhysicsAPI.h>
#include <PxDefaultErrorCallback.h>
#include <PxDefaultAllocator.h>
#include <PxExtensionsAPI.h>

#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) :

#include <iostream>
#include <PxPhysicsAPI.h>
#include <PxDefaultErrorCallback.h>
#include <PxDefaultAllocator.h>
#include <PxExtensionsAPI.h>

#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.

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 :

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) :

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.

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 :

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) :

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 :

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.

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 :

// 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.

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 :

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 :

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) :

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.

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).

PxTransform np = PxShapeExt::getGlobalPose(* shape);

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

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.

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

#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 :

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 :

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 - 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 pour sa relecture orthographique !