понедельник, 13 февраля 2012 г.

Интеграция физики PhysX в DirectX приложение.

Урок о том, как подружить физический движок PhysX c DirectX и какие подводные камни могут встретиться на этом пути.


Так же я рассказываю как получить из DirectX mesh file (.x) NxConvexMesh и NxTriangleMesh.


read english version
Введение. О PhysX.


Для начала читаем в вики.

Что есть такой физический движок PhysX сейчас знают все подряд, особенно после шумихи с тем что он стал вначале бесплатным, а потом фирма Ageia перешла под крыло Nvidia, это было прецедентом и дало толчок интеграции физики в видеокарты нового поколения.
Насколько мне известно, сейчас ведутся переговоры по поводу официальной аппаратной поддержки PhysX на картах ATI. (не официально это уже сделали)

Часто слышу разговоры о том, что наверняка скоро Nvidia сделает движок платным, потому поясняю – даже если это случится, версии 2.8.1 SDK это никак не касается, потому что, скачав и установив ее вы заключаете лицензионное соглашение которое действует в двустороннем порядке и не имеет временных интервалов. Все это можно узнать из лицензионного соглашения, а так же то что вы можете бесплатно использовать движок в своих программах (даже коммерческих) с тем лишь условием, что в начале программы будет «splash screen», которые все видели в играх, о том что вы используете данный физический движок, еще нужно об этом писать в документах по программе.

Итак, заходим сюда, переходим в раздел «Get NVIDIA PhysX Now!» и качаем PhysX_2.8.1_SDK_Core.msi и PhysX_Game_installer_281.msi. Первый файл это сдк, второй это драйвер к нему.

Введение. Об уроке.


Как ни странно в SDK практически нет примеров использования PhysX с DirectX (практически, потому что один пример таки есть, но чересчур "накручен".DXUT, если вы понимаете о чем это я)) 
Ну так вот, google тут не помогает, примеров в инете или нет или найти их очень сложно. Единственный пример есть на CodeSampler.com, но он базируется на NovodeX (дедуля физикса)и для полных новичков не подходит (в конце урока читайте про то как заставить этот пример работать). 

Если спросить "DX+PhysX = How?"  на форумах GameDev.com\Nvidia\Gamedev.ru то обычно в ответ получаешь что то типа: 
"Ну так физикс не основан ни на ОГЛ ни на ДХ, потому и туда и туда включается абсолютно одинаково и легко!"

Нихрена подобного скажу я вам! (извините, наболело) И если вам так сказали, то это были те, кто сам не пробовал или думает, что пробовал. Обидно, да. В итоге все проходят одни и те же проблемы изобретают велосипеды, которые потом едут в другую сторону и с колесом вместо руля, как то так. Так что мой урок призван помочь всем на пути объединения DirectX и PhysX, чтобы ни у кого не появлялось, по мере получения очередных глюков, мысли бросить PhysX в топку ^_^. После трех недель разрушения мозга я таки разобрался с основами и хочу об этом написать. 
В уроке не будет всех основ PhysX, я хочу лишь акцентировать внимание на деталях его использования вместе с DirectX.

Что нам нужно помимо PhysX? Нужны -  установленный DX SDK и, как среда разработки, MSVS C++ 2005.


С чего начать?


Начать как ни странно лучше с примеров из сдк, открыть проект "AllSamples - VC8" и в нем позапускать примеры, посмотреть код в SampleBoxes (NxBoxes.cpp) , а именно InitNx() , CreateCube() - эти функции будут у вас идентичными, так же обратите внимание как идет обновление физики в функции RenderCallback() :

 RenderCallback() {  
 ...  
 gScene->simulate(1.0f/60.0f);  
 ...  
 }  


У нас естественно будет анологично) Общий порядок такой - создали объект сдк, создали объект сцены (их может быть несколько), создаем физические объекты и передаем их в объект сцены, где то запускаем постоянно обновление мира.




Проблемы. Матрицы.

Во первых пару слов о том, что такое VRD - Visual Remote Debugger, это такая программа, входящая в состав сдк, она позволяет получить из вашего приложения информацию о вашей физической геометрии и ее отобразить, это невероятно удобно и позволяет "на лету" отлавливать ошибки в физике, а так же манипулировать (двигать\таскать) объектами. 
Теперь о плохом, о проблеме первой, товарищи из Ageia хитрят, говоря, что их координатные системы в PhysX не опираются на конкретный графический апи, хоть потому что их программа   (а она считай часть PhysX) VRD использует RHCS - Right Hand Coordinate System, но в программах на DirectX всегда используют LHCS! Из-за этого то, что мы увидим в VRD будет зеркальным отражением того что будет в нашей программе. Если этого не знать заранее, то можно получить вывих мозга:) И кстати в VRD (дебагере) нельзя сменить ориентацию пространства. Чем это чревато? Я, в попытках получить идентичную картинку и там и там, делал поворот геометрии при получении матриц, затем отражение координат и в итоге получалось, что объекты стоят там где надо, но они развернуты симметрично-в-другую-сторону... Весело было.


Проблема вторая, как преобразовать матрицу NxMat34 в D3DXMATRIX? Кому то это покажется легким вопросом, а кому то нет, мне предлагали 3 варианта!) Из всех работал лишь один и он очень прост.

 //NxMat34 a;  
 a.getColumnMajor44( d3dMat );  


И как же зная матрицы нарисовать графический объект там где находится физический? Ну в общем случае отрисовка объекта будет выглядеть так:

 //NxActor* object; LPD3DXMESH* g_pObjectMesh;  
 D3DXMATRIX d3dMat;  
 taburet_object->getGlobalPose().getColumnMajor44( d3dMat );  
 g_pd3dDevice->SetTransform( D3DTS_WORLD, &matWorld );  
 g_pObjectMesh->DrawSubset(0);  


Проблемы. Меши.

Я конечно понимаю, да, то как получить из модели массив вершин и массив индексов знает ну каждый кодер... шутка) Я не знал, потому что это было мне не нужно, я не писал парсер к моделям и меня все это не интересовало.. Но вот он момент, мне нужна инфа и опять ее нигде нету! Просто ужас какой то, опять приходится парить мозг друзьям и людям на форумах, тем  кто вроде как что то знает об этом. Вообщем кто ищет тот всегда найдет ( или он найдет того кого можно достать так что тот найдет)) На форуме Nvidia мне написал код некий Chillypacman, что меня сильно выручило.. но и нехило вставило мозг о____о потому что в его функциях было два скрытых прикола (это я потом ели ели допер что не так, ели ели потому что в них не искал). 1 - в функции для создания статических мешей координаты интвртировались (а я потом ломал голову почему у меня одно в VRD инвертированно, другое нет), 2 - в функции для конвексов координаты ужимались вдвое!) Мдя, было весело.  Так же отмечу, что для получения массива вершин будем использовать тип vector (из namespace std).

Итак собственно функции построенные на его примере:

1. для получения массива вершин из LPD3DXMESH


 vector<float> GetVertices(LPD3DXMESH  *g_pMesh )  
 {  
      vector<float> vertices;  
      DWORD stride = D3DXGetFVFVertexSize((*g_pMesh)->GetFVF());  
      BYTE* vbptr = NULL;  
      (*g_pMesh)->LockVertexBuffer(0, (LPVOID*)&vbptr);  
      int ii = -1;  
      for(int i = 0; i < (*g_pMesh)->GetNumVertices(); i++)  
      {  
           ii++;  
           D3DXVECTOR3* pos = (D3DXVECTOR3*)vbptr;  
           vertices.push_back(pos->x);  
           vertices.push_back(pos->y);  
           vertices.push_back(pos->z);  
           vbptr += stride;  
      }  
      (*g_pMesh)->UnlockVertexBuffer();  
      return vertices;  
 }  


2. для получения массива индексов:

 vector<short> GetIndices(LPD3DXMESH   *g_pMesh)  
 {  
      LPVOID * ppData;  
      DWORD stride = sizeof(short);  
      BYTE* ibptr = NULL;  
      short* indices = new short[(*g_pMesh)->GetNumFaces() * 3];  
      vector<short> copy;  
      (*g_pMesh)->LockIndexBuffer(0, (LPVOID*)&indices);  
      for(int i = 0; i < (*g_pMesh)->GetNumFaces() * 3; i++)  
      {  
           copy.push_back(indices[i]);  
      }  
      (*g_pMesh)->UnlockIndexBuffer();  
      return copy;  
 }  


Итак перед тем как покажу функции для создания объектов для физикса давайте поясню про то, что же такое Triangle Mesh и Convex Mesh(конвекс).
Первое это вид чисто статических объектов, его размеры вроде бы никак не ограниченны, это могут быть например стены. Второй тип геометрии может быть и статическим и динамическим, причем чаще именно вторым, для того чтобы представить что такое конвекс предстваьте стул, теперь как бы обтяните его тканью, это и будет нечто вроде конвекса, причем это всегда выпуклый объект, что делает рассчеты более простыми. Число полигонов сетки конвекса ограниченно 256ю, но так как он генерируется самим PhysX, это нам не страшно. Подробнее про типы геометрии можно прочитать в сдк. Скажу лишь еще, что NxTriangleMesh в скором времени (в новом PhysX) уже будет динамическим.


Итак функции:

1. Для объекта на основе NxTriangleMesh:


 NxActor* GenTriangleMesh(NxVec3 pos, vector<short> indices, vector<float> vertices )  
 {  
      int NumVerticies = vertices.size() / 3;  
      int NumTriangles = indices.size() / 3;  
      //Create pointer for vertices  
      NxVec3* verts = new NxVec3[NumVerticies];  
      int ii = -1;  
      for(int i = 0; i < NumVerticies; i++)  
      {  
           ++ii;  
           verts[i].x = vertices[ii];  
           verts[i].y = vertices[++ii];  
           verts[i].z = vertices[++ii];  
      }  
      //Create pointer for indices  
      NxU16 *tris = new NxU16[indices.size()];  
      for(int i = indices.size() - 1; i >= 0; i--)  
      tris[i] = i;  
      // Build physical model  
      NxTriangleMeshDesc TriMeshDesc;  
      TriMeshDesc.numVertices = NumVerticies;  
      TriMeshDesc.numTriangles = NumTriangles;  
      TriMeshDesc.pointStrideBytes = sizeof(NxVec3);  
      TriMeshDesc.triangleStrideBytes = 3*sizeof(NxU16);  
      TriMeshDesc.points = verts;  
      TriMeshDesc.triangles = tris;  
      TriMeshDesc.flags = NX_MF_16_BIT_INDICES ;//| NX_MF_FLIPNORMALS ;  
      NxTriangleMeshShapeDesc ShapeDesc;  
      NxInitCooking();  
      // Cooking from memory  
      MemoryWriteBuffer buf;  
      bool status = NxCookTriangleMesh(TriMeshDesc, buf);  
      ShapeDesc.meshData = g_pPhysicsSDK->createTriangleMesh(MemoryReadBuffer(buf.data));  
      NxActorDesc actorDesc;  
      actorDesc.shapes.pushBack(&ShapeDesc);  
      actorDesc.globalPose.t = pos;  
      NxActor* act = g_pScene->createActor(actorDesc);  
      delete[] verts;  
      delete[] tris;  
      return act;  
 }  


2. Для объекта на основе NxConvexMesh:

 NxActor* GenConvexMesh(const NxVec3& pos, vector<float> vertices, const NxReal density)  
 {  
      int i = 0;  
      NxActorDesc actorDesc;  
      NxBodyDesc bodyDesc;  
      int numVertices = vertices.size() / 3;  
      NxVec3* verts = new NxVec3[numVertices];  
      int ii = -1;  
      for(int i = 0; i < numVertices; i++)  
      {  
           ++ii;  
           verts[i].x = vertices[ii];  
           verts[i].y = vertices[++ii];  
           verts[i].z = vertices[++ii];  
      }  
      // Create descriptor for convex mesh  
      NxConvexMeshDesc convexDesc;  
      convexDesc.numVertices     = numVertices;  
      convexDesc.pointStrideBytes  = sizeof(NxVec3);  
      convexDesc.points          = verts;  
      convexDesc.flags           = NX_CF_COMPUTE_CONVEX;  
      NxActorDesc actorDesc;  
      NxInitCooking();  
      MemoryWriteBuffer buf;  
      bool status = NxCookConvexMesh(convexDesc, buf);  
      NxConvexShapeDesc* convexShapeDesc  =  new NxConvexShapeDesc();  
      convexShapeDesc->localPose.t     =  NxVec3(0,0,0);  
      convexShapeDesc->materialIndex    =  0;  
      convexShapeDesc->meshData = g_pPhysicsSDK->createConvexMesh(MemoryReadBuffer(buf.data));  
      actorDesc.shapes.pushBack(convexShapeDesc);  
      NxBodyDesc bodyDesc;  
      if(!stat)  
      {  
           actorDesc.body = &bodyDesc;  
           actorDesc.density = 1;  
      }  
      else  
           actorDesc.body = NULL;  
      actorDesc.globalPose.t = NxVec3(0, 0, 0);  
      actorDesc.name = "boo";  
      NxActor* actor = g_pScene->createActor(actorDesc);  
      return actor;  
 }  


Думаю это наиболее часто разыскиваемые функции у тех, кто пишет под DirectX)...



Проблемы. Cooking.h и Stream.h


Эти хедеры используются в примерах PhysX и не входят в стандартный набор. Использование их, к сожалению, бывает весьма проблематичным и может вызвать массу ошибок при компиляции, поэтому я предлагаю вам вариант «cooking_stream.h», этот хедер написан на основе кода, написанного моим другом - ASD, этот файл объединяет функционал двух библиотек и избавляет от ошибок, связанных с логикой классов у Ageia.
Файл вы найдете в архиве прилагающемся к уроку.


Дополнения.

Как подключить к программе VRD?


Для того чтобы подключить к своей программе VRD нужно скопировать в начало файла где у вас инициализируется PhysX следующий код:

 #define SAMPLES_VRD_HOST "localhost"  
 #define SAMPLES_VRD_PORT NX_DBG_DEFAULT_PORT  
 #define SAMPLES_VRD_EVENTMASK NX_DBG_EVENTMASK_EVERYTHING  
 int  gAppData;  
 void vrd_init()  
 {  
  NxRemoteDebugger* pRemDeb = g_pPhysicsSDK->getFoundationSDK().getRemoteDebugger();  
  pRemDeb->connect(SAMPLES_VRD_HOST,  SAMPLES_VRD_PORT,  SAMPLES_VRD_EVENTMASK);  
  if (pRemDeb->isConnected())  
  {  
    pRemDeb->createObject(&gAppData, NX_DBG_OBJECTTYPE_GENERIC,"AppData",  NX_DBG_EVENTMASK_EVERYTHING);  
    pRemDeb->writeParameter("Info text",  &gAppData,  true,"Info",   NX_DBG_EVENTMASK_EVERYTHING);  
    pRemDeb->createObject(&gAppData+1,  NX_DBG_OBJECTTYPE_VECTOR,"AVector",  NX_DBG_EVENTMASK_EVERYTHING);  
    pRemDeb->writeParameter(NxVec3(0, 0, 0), &gAppData+1,  true,"Origin", NX_DBG_EVENTMASK_EVERYTHING);  
    pRemDeb->writeParameter(NxVec3(1, 1, 1), &gAppData+1, true,"Extent", NX_DBG_EVENTMASK_EVERYTHING);  
    pRemDeb->addChild(&gAppData, &gAppData+1, NX_DBG_EVENTMASK_EVERYTHING);  
    // Tells our application that we have created the VRD object  
    // and that it is ok to set information in it  
    gAppData = 1;   
  }  
 }  


Ну и конечно вызвать после инициализации PhysX  функцию vrd_init();. Теперь запустите VRD (NVIDIA PhysX SDK\v2.8.1\Bin\win32\ RemoteDebugger.exe), после запускайте ваше приложение и вуаля, все физические модели показываются в VRD, но учтите, что они симметричны относительно вашей программы!


Содержимое архива.

Теперь о том, что находится в архиве. Конечно это пример, причем пример на основе того что можно найти на CS.
Как ни странно этот пример, написанный еще для NovodeХ, все еще работоспособен! (а многие ошибочно не верят в это) Однако конечно нужно было в нем сделать ряд изменений.


1. В настройках проекта нужно убрать Physics.lib (что то типа того, сами увидите на что ругается компилятор), и вместо нее скинуть в папку с проектом PhysXLoader.lib,nxcooking.lib,NxCharacter.lib. (они есть в NVIDIA PhysX SDK\v2.8.1\SDKs\lib\Win32) Для того чтобы их подконнектить к проекту пишем где нибудь:



 #pragma comment(lib, "physxloader.lib")  
 #pragma comment(lib, "nxcooking.lib")  
 #pragma comment(lib, "NxCharacter.lib")  


2. При компиляции появятся ошибки в следующих строках функции initNovodeX():


 sceneDesc.broadPhase = NX_BROADPHASE_COHERENT;
 sceneDesc.collisionDetection = true;



просто уберите их, из PhysX эти параметры были убраны.
Теперь все должно компилироваться! 

Архив к уроку : dx9_novodex_simple_derivedbycrol

Послесловие.

На этом мой урок заканчивается, я уверен многим он будет полезен и не даст наступить на все те грабли, которые так незаметно расставлены на пути интеграции PhysX в DirectX приложение. Советую вам чаще смотреть в хелп к сдк, там написано много полезного. Еще было бы неплохо раздобыть 2.7.3 SDK, потому что в нем есть раздел “tutorials”, еде есть примеры и детальное описание к ним, а в 2.8.3 нету. (Nvidia обещает вскоре исправить этот досадный недостаток)

Большое спасибо ASD и Chillypacman за помощь в написании кода.
Так же огромное спасибо Nvidia и Ageia за их физический движок.

До новых встреч и желаю вам кода без Access Violations!) 



1 комментарий: