px3World.cpp 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
  1. //-----------------------------------------------------------------------------
  2. // Copyright (c) 2012 GarageGames, LLC
  3. //
  4. // Permission is hereby granted, free of charge, to any person obtaining a copy
  5. // of this software and associated documentation files (the "Software"), to
  6. // deal in the Software without restriction, including without limitation the
  7. // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
  8. // sell copies of the Software, and to permit persons to whom the Software is
  9. // furnished to do so, subject to the following conditions:
  10. //
  11. // The above copyright notice and this permission notice shall be included in
  12. // all copies or substantial portions of the Software.
  13. //
  14. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  19. // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
  20. // IN THE SOFTWARE.
  21. //-----------------------------------------------------------------------------
  22. #include "platform/platform.h"
  23. #include "T3D/physics/physx3/px3World.h"
  24. #include "T3D/physics/physx3/px3.h"
  25. #include "T3D/physics/physx3/px3Plugin.h"
  26. #include "T3D/physics/physx3/px3Casts.h"
  27. #include "T3D/physics/physx3/px3Stream.h"
  28. #include "T3D/physics/physicsUserData.h"
  29. #include "console/engineAPI.h"
  30. #include "core/stream/bitStream.h"
  31. #include "platform/profiler.h"
  32. #include "sim/netConnection.h"
  33. #include "console/console.h"
  34. #include "console/consoleTypes.h"
  35. #include "core/util/safeDelete.h"
  36. #include "collision/collision.h"
  37. #include "T3D/gameBase/gameProcess.h"
  38. #include "gfx/sim/debugDraw.h"
  39. #include "gfx/primBuilder.h"
  40. physx::PxPhysics* gPhysics3SDK = NULL;
  41. physx::PxCooking* Px3World::smCooking = NULL;
  42. physx::PxFoundation* Px3World::smFoundation = NULL;
  43. physx::PxProfileZoneManager* Px3World::smProfileZoneManager = NULL;
  44. physx::PxDefaultCpuDispatcher* Px3World::smCpuDispatcher=NULL;
  45. Px3ConsoleStream* Px3World::smErrorCallback = NULL;
  46. physx::PxVisualDebuggerConnection* Px3World::smPvdConnection=NULL;
  47. physx::PxDefaultAllocator Px3World::smMemoryAlloc;
  48. Px3World::Px3World(): mScene( NULL ),
  49. mProcessList( NULL ),
  50. mIsSimulating( false ),
  51. mErrorReport( false ),
  52. mTickCount( 0 ),
  53. mIsEnabled( false ),
  54. mEditorTimeScale( 1.0f ),
  55. mAccumulator( 0 ),
  56. mControllerManager(NULL),
  57. mIsSceneLocked(false),
  58. mRenderBuffer(NULL)
  59. {
  60. }
  61. Px3World::~Px3World()
  62. {
  63. }
  64. physx::PxCooking *Px3World::getCooking()
  65. {
  66. return smCooking;
  67. }
  68. bool Px3World::restartSDK( bool destroyOnly, Px3World *clientWorld, Px3World *serverWorld)
  69. {
  70. // If either the client or the server still exist
  71. // then we cannot reset the SDK.
  72. if ( clientWorld || serverWorld )
  73. return false;
  74. if(smPvdConnection)
  75. smPvdConnection->release();
  76. if(smCooking)
  77. smCooking->release();
  78. if(smCpuDispatcher)
  79. smCpuDispatcher->release();
  80. // Destroy the existing SDK.
  81. if ( gPhysics3SDK )
  82. {
  83. PxCloseExtensions();
  84. gPhysics3SDK->release();
  85. }
  86. if(smErrorCallback)
  87. {
  88. SAFE_DELETE(smErrorCallback);
  89. }
  90. if(smFoundation)
  91. {
  92. smFoundation->release();
  93. SAFE_DELETE(smErrorCallback);
  94. }
  95. // If we're not supposed to restart... return.
  96. if ( destroyOnly )
  97. return true;
  98. bool memTrack = false;
  99. #ifdef TORQUE_DEBUG
  100. memTrack = true;
  101. #endif
  102. smErrorCallback = new Px3ConsoleStream;
  103. smFoundation = PxCreateFoundation(PX_PHYSICS_VERSION, smMemoryAlloc, *smErrorCallback);
  104. smProfileZoneManager = &physx::PxProfileZoneManager::createProfileZoneManager(smFoundation);
  105. gPhysics3SDK = PxCreatePhysics(PX_PHYSICS_VERSION, *smFoundation, physx::PxTolerancesScale(),memTrack,smProfileZoneManager);
  106. if ( !gPhysics3SDK )
  107. {
  108. Con::errorf( "PhysX3 failed to initialize!" );
  109. Platform::messageBox( Con::getVariable( "$appName" ),
  110. avar("PhysX3 could not be started!\r\n"),
  111. MBOk, MIStop );
  112. Platform::forceShutdown( -1 );
  113. // We shouldn't get here, but this shuts up
  114. // source diagnostic tools.
  115. return false;
  116. }
  117. if(!PxInitExtensions(*gPhysics3SDK))
  118. {
  119. Con::errorf( "PhysX3 failed to initialize extensions!" );
  120. Platform::messageBox( Con::getVariable( "$appName" ),
  121. avar("PhysX3 could not be started!\r\n"),
  122. MBOk, MIStop );
  123. Platform::forceShutdown( -1 );
  124. return false;
  125. }
  126. smCooking = PxCreateCooking(PX_PHYSICS_VERSION, *smFoundation, physx::PxCookingParams(physx::PxTolerancesScale()));
  127. if(!smCooking)
  128. {
  129. Con::errorf( "PhysX3 failed to initialize cooking!" );
  130. Platform::messageBox( Con::getVariable( "$appName" ),
  131. avar("PhysX3 could not be started!\r\n"),
  132. MBOk, MIStop );
  133. Platform::forceShutdown( -1 );
  134. return false;
  135. }
  136. #ifdef TORQUE_DEBUG
  137. physx::PxVisualDebuggerConnectionFlags connectionFlags(physx::PxVisualDebuggerExt::getAllConnectionFlags());
  138. smPvdConnection = physx::PxVisualDebuggerExt::createConnection(gPhysics3SDK->getPvdConnectionManager(),
  139. "localhost", 5425, 100, connectionFlags);
  140. #endif
  141. return true;
  142. }
  143. void Px3World::destroyWorld()
  144. {
  145. getPhysicsResults();
  146. mRenderBuffer = NULL;
  147. // Release the tick processing signals.
  148. if ( mProcessList )
  149. {
  150. mProcessList->preTickSignal().remove( this, &Px3World::getPhysicsResults );
  151. mProcessList->postTickSignal().remove( this, &Px3World::tickPhysics );
  152. mProcessList = NULL;
  153. }
  154. if(mControllerManager)
  155. {
  156. mControllerManager->release();
  157. mControllerManager = NULL;
  158. }
  159. // Destroy the scene.
  160. if ( mScene )
  161. {
  162. // Release the scene.
  163. mScene->release();
  164. mScene = NULL;
  165. }
  166. }
  167. bool Px3World::initWorld( bool isServer, ProcessList *processList )
  168. {
  169. if ( !gPhysics3SDK )
  170. {
  171. Con::errorf( "Physx3World::init - PhysXSDK not initialized!" );
  172. return false;
  173. }
  174. mIsServer = isServer;
  175. physx::PxSceneDesc sceneDesc(gPhysics3SDK->getTolerancesScale());
  176. sceneDesc.gravity = px3Cast<physx::PxVec3>(mGravity);
  177. sceneDesc.userData = this;
  178. if(!sceneDesc.cpuDispatcher)
  179. {
  180. //Create shared cpu dispatcher
  181. if(!smCpuDispatcher)
  182. smCpuDispatcher = physx::PxDefaultCpuDispatcherCreate(PHYSICSMGR->getThreadCount());
  183. sceneDesc.cpuDispatcher = smCpuDispatcher;
  184. Con::printf("PhysX3 using Cpu: %d workers", smCpuDispatcher->getWorkerCount());
  185. }
  186. sceneDesc.flags |= physx::PxSceneFlag::eENABLE_CCD;
  187. sceneDesc.flags |= physx::PxSceneFlag::eENABLE_ACTIVETRANSFORMS;
  188. sceneDesc.filterShader = physx::PxDefaultSimulationFilterShader;
  189. mScene = gPhysics3SDK->createScene(sceneDesc);
  190. //cache renderbuffer for use with debug drawing
  191. mRenderBuffer = const_cast<physx::PxRenderBuffer*>(&mScene->getRenderBuffer());
  192. physx::PxDominanceGroupPair debrisDominance( 0.0f, 1.0f );
  193. mScene->setDominanceGroupPair(0,31,debrisDominance);
  194. mControllerManager = PxCreateControllerManager(*mScene);
  195. AssertFatal( processList, "Px3World::init() - We need a process list to create the world!" );
  196. mProcessList = processList;
  197. mProcessList->preTickSignal().notify( this, &Px3World::getPhysicsResults );
  198. mProcessList->postTickSignal().notify( this, &Px3World::tickPhysics, 1000.0f );
  199. return true;
  200. }
  201. // Most of this borrowed from bullet physics library, see btDiscreteDynamicsWorld.cpp
  202. bool Px3World::_simulate(const F32 dt)
  203. {
  204. S32 numSimulationSubSteps = 0;
  205. //fixed timestep with interpolation
  206. mAccumulator += dt;
  207. if (mAccumulator >= smPhysicsStepTime)
  208. {
  209. numSimulationSubSteps = S32(mAccumulator / smPhysicsStepTime);
  210. mAccumulator -= numSimulationSubSteps * smPhysicsStepTime;
  211. }
  212. if (numSimulationSubSteps)
  213. {
  214. //clamp the number of substeps, to prevent simulation grinding spiralling down to a halt
  215. S32 clampedSimulationSteps = (numSimulationSubSteps > smPhysicsMaxSubSteps)? smPhysicsMaxSubSteps : numSimulationSubSteps;
  216. for (S32 i=0;i<clampedSimulationSteps;i++)
  217. {
  218. mScene->fetchResults(true);
  219. mScene->simulate(smPhysicsStepTime);
  220. }
  221. }
  222. mIsSimulating = true;
  223. return true;
  224. }
  225. void Px3World::tickPhysics( U32 elapsedMs )
  226. {
  227. if ( !mScene || !mIsEnabled )
  228. return;
  229. // Did we forget to call getPhysicsResults somewhere?
  230. AssertFatal( !mIsSimulating, "PhysX3World::tickPhysics() - Already simulating!" );
  231. // The elapsed time should be non-zero and
  232. // a multiple of TickMs!
  233. AssertFatal( elapsedMs != 0 &&
  234. ( elapsedMs % TickMs ) == 0 , "PhysX3World::tickPhysics() - Got bad elapsed time!" );
  235. PROFILE_SCOPE(Px3World_TickPhysics);
  236. // Convert it to seconds.
  237. const F32 elapsedSec = (F32)elapsedMs * 0.001f;
  238. mIsSimulating = _simulate(elapsedSec * mEditorTimeScale);
  239. //Con::printf( "%s PhysX3World::tickPhysics!", mIsServer ? "Client" : "Server" );
  240. }
  241. void Px3World::getPhysicsResults()
  242. {
  243. if ( !mScene || !mIsSimulating )
  244. return;
  245. PROFILE_SCOPE(Px3World_GetPhysicsResults);
  246. // Get results from scene.
  247. mScene->fetchResults(true);
  248. mIsSimulating = false;
  249. mTickCount++;
  250. // Con::printf( "%s PhysXWorld::getPhysicsResults!", this == smClientWorld ? "Client" : "Server" );
  251. }
  252. void Px3World::releaseWriteLocks()
  253. {
  254. Px3World *world = dynamic_cast<Px3World*>( PHYSICSMGR->getWorld( "server" ) );
  255. if ( world )
  256. world->releaseWriteLock();
  257. world = dynamic_cast<Px3World*>( PHYSICSMGR->getWorld( "client" ) );
  258. if ( world )
  259. world->releaseWriteLock();
  260. }
  261. void Px3World::releaseWriteLock()
  262. {
  263. if ( !mScene || !mIsSimulating )
  264. return;
  265. PROFILE_SCOPE(PxWorld_ReleaseWriteLock);
  266. // We use checkResults here to release the write lock
  267. // but we do not change the simulation flag or increment
  268. // the tick count... we may have gotten results, but the
  269. // simulation hasn't really ticked!
  270. mScene->checkResults( true );
  271. //AssertFatal( mScene->isWritable(), "PhysX3World::releaseWriteLock() - We should have been writable now!" );
  272. }
  273. void Px3World::lockScenes()
  274. {
  275. Px3World *world = dynamic_cast<Px3World*>(PHYSICSMGR->getWorld("server"));
  276. if (world)
  277. world->lockScene();
  278. world = dynamic_cast<Px3World*>(PHYSICSMGR->getWorld("client"));
  279. if (world)
  280. world->lockScene();
  281. }
  282. void Px3World::unlockScenes()
  283. {
  284. Px3World *world = dynamic_cast<Px3World*>(PHYSICSMGR->getWorld("server"));
  285. if (world)
  286. world->unlockScene();
  287. world = dynamic_cast<Px3World*>(PHYSICSMGR->getWorld("client"));
  288. if (world)
  289. world->unlockScene();
  290. }
  291. void Px3World::lockScene()
  292. {
  293. if (!mScene)
  294. return;
  295. if (mIsSceneLocked)
  296. {
  297. Con::printf("Px3World: Attempting to lock a scene that is already locked.");
  298. return;
  299. }
  300. mScene->lockWrite();
  301. mIsSceneLocked = true;
  302. }
  303. void Px3World::unlockScene()
  304. {
  305. if (!mScene)
  306. return;
  307. if (!mIsSceneLocked)
  308. {
  309. Con::printf("Px3World: Attempting to unlock a scene that is not locked.");
  310. return;
  311. }
  312. mScene->unlockWrite();
  313. mIsSceneLocked = false;
  314. }
  315. bool Px3World::castRay( const Point3F &startPnt, const Point3F &endPnt, RayInfo *ri, const Point3F &impulse )
  316. {
  317. physx::PxVec3 orig = px3Cast<physx::PxVec3>( startPnt );
  318. physx::PxVec3 dir = px3Cast<physx::PxVec3>( endPnt - startPnt );
  319. physx::PxF32 maxDist = dir.magnitude();
  320. dir.normalize();
  321. U32 groups = 0xffffffff;
  322. groups &= ~( PX3_TRIGGER ); // No trigger shapes!
  323. physx::PxHitFlags outFlags(physx::PxHitFlag::eDISTANCE | physx::PxHitFlag::eIMPACT | physx::PxHitFlag::eNORMAL);
  324. physx::PxQueryFilterData filterData(physx::PxQueryFlag::eSTATIC|physx::PxQueryFlag::eDYNAMIC);
  325. filterData.data.word0 = groups;
  326. physx::PxRaycastBuffer buf;
  327. if(!mScene->raycast(orig,dir,maxDist,buf,outFlags,filterData))
  328. return false;
  329. if(!buf.hasBlock)
  330. return false;
  331. const physx::PxRaycastHit hit = buf.block;
  332. physx::PxRigidActor *actor = hit.actor;
  333. PhysicsUserData *userData = PhysicsUserData::cast( actor->userData );
  334. if ( ri )
  335. {
  336. ri->object = ( userData != NULL ) ? userData->getObject() : NULL;
  337. if ( ri->object == NULL )
  338. ri->distance = hit.distance;
  339. ri->normal = px3Cast<Point3F>( hit.normal );
  340. ri->point = px3Cast<Point3F>( hit.position );
  341. ri->t = maxDist / hit.distance;
  342. }
  343. if ( impulse.isZero() ||
  344. !actor->isRigidDynamic() ||
  345. actor->is<physx::PxRigidDynamic>()->getRigidDynamicFlags() & physx::PxRigidDynamicFlag::eKINEMATIC )
  346. return true;
  347. physx::PxRigidBody *body = actor->is<physx::PxRigidBody>();
  348. physx::PxVec3 force = px3Cast<physx::PxVec3>( impulse );
  349. physx::PxRigidBodyExt::addForceAtPos(*body,force,hit.position,physx::PxForceMode::eIMPULSE);
  350. return true;
  351. }
  352. PhysicsBody* Px3World::castRay( const Point3F &start, const Point3F &end, U32 bodyTypes )
  353. {
  354. physx::PxVec3 orig = px3Cast<physx::PxVec3>( start );
  355. physx::PxVec3 dir = px3Cast<physx::PxVec3>( end - start );
  356. physx::PxF32 maxDist = dir.magnitude();
  357. dir.normalize();
  358. U32 groups = 0xFFFFFFFF;
  359. if ( !( bodyTypes & BT_Player ) )
  360. groups &= ~( PX3_PLAYER );
  361. // TODO: For now always skip triggers and debris,
  362. // but we should consider how game specifc this API
  363. // should be in the future.
  364. groups &= ~( PX3_TRIGGER ); // triggers
  365. groups &= ~( PX3_DEBRIS ); // debris
  366. physx::PxHitFlags outFlags(physx::PxHitFlag::eDISTANCE | physx::PxHitFlag::eIMPACT | physx::PxHitFlag::eNORMAL);
  367. physx::PxQueryFilterData filterData;
  368. if(bodyTypes & BT_Static)
  369. filterData.flags |= physx::PxQueryFlag::eSTATIC;
  370. if(bodyTypes & BT_Dynamic)
  371. filterData.flags |= physx::PxQueryFlag::eDYNAMIC;
  372. filterData.data.word0 = groups;
  373. physx::PxRaycastBuffer buf;
  374. if( !mScene->raycast(orig,dir,maxDist,buf,outFlags,filterData) )
  375. return NULL;
  376. if(!buf.hasBlock)
  377. return NULL;
  378. physx::PxRigidActor *actor = buf.block.actor;
  379. PhysicsUserData *userData = PhysicsUserData::cast( actor->userData );
  380. if( !userData )
  381. return NULL;
  382. return userData->getBody();
  383. }
  384. void Px3World::explosion( const Point3F &pos, F32 radius, F32 forceMagnitude )
  385. {
  386. physx::PxVec3 nxPos = px3Cast<physx::PxVec3>( pos );
  387. const physx::PxU32 bufferSize = 10;
  388. physx::PxSphereGeometry worldSphere(radius);
  389. physx::PxTransform pose(nxPos);
  390. physx::PxOverlapBufferN<bufferSize> buffer;
  391. if(!mScene->overlap(worldSphere,pose,buffer))
  392. return;
  393. for ( physx::PxU32 i = 0; i < buffer.nbTouches; i++ )
  394. {
  395. physx::PxRigidActor *actor = buffer.touches[i].actor;
  396. bool dynamic = actor->isRigidDynamic();
  397. if ( !dynamic )
  398. continue;
  399. bool kinematic = actor->is<physx::PxRigidDynamic>()->getRigidDynamicFlags() & physx::PxRigidDynamicFlag::eKINEMATIC;
  400. if ( kinematic )
  401. continue;
  402. physx::PxVec3 force = actor->getGlobalPose().p - nxPos;
  403. force.normalize();
  404. force *= forceMagnitude;
  405. physx::PxRigidBody *body = actor->is<physx::PxRigidBody>();
  406. physx::PxRigidBodyExt::addForceAtPos(*body,force,nxPos,physx::PxForceMode::eIMPULSE);
  407. }
  408. }
  409. void Px3World::setEnabled( bool enabled )
  410. {
  411. mIsEnabled = enabled;
  412. if ( !mIsEnabled )
  413. getPhysicsResults();
  414. }
  415. physx::PxController* Px3World::createController( physx::PxControllerDesc &desc )
  416. {
  417. if ( !mScene )
  418. return NULL;
  419. // We need the writelock!
  420. releaseWriteLock();
  421. physx::PxController* pController = mControllerManager->createController(desc);
  422. AssertFatal( pController, "Px3World::createController - Got a null!" );
  423. return pController;
  424. }
  425. static ColorI getDebugColor( physx::PxU32 packed )
  426. {
  427. ColorI col;
  428. col.blue = (packed)&0xff;
  429. col.green = (packed>>8)&0xff;
  430. col.red = (packed>>16)&0xff;
  431. col.alpha = 255;
  432. return col;
  433. }
  434. void Px3World::onDebugDraw( const SceneRenderState *state )
  435. {
  436. if ( !mScene || !mRenderBuffer )
  437. return;
  438. mScene->setVisualizationParameter(physx::PxVisualizationParameter::eSCALE,1.0f);
  439. mScene->setVisualizationParameter(physx::PxVisualizationParameter::eBODY_AXES,1.0f);
  440. mScene->setVisualizationParameter(physx::PxVisualizationParameter::eCOLLISION_SHAPES,1.0f);
  441. // Render points
  442. {
  443. physx::PxU32 numPoints = mRenderBuffer->getNbPoints();
  444. const physx::PxDebugPoint *points = mRenderBuffer->getPoints();
  445. PrimBuild::begin( GFXPointList, numPoints );
  446. while ( numPoints-- )
  447. {
  448. PrimBuild::color( getDebugColor(points->color) );
  449. PrimBuild::vertex3fv(px3Cast<Point3F>(points->pos));
  450. points++;
  451. }
  452. PrimBuild::end();
  453. }
  454. // Render lines
  455. {
  456. physx::PxU32 numLines = mRenderBuffer->getNbLines();
  457. const physx::PxDebugLine *lines = mRenderBuffer->getLines();
  458. PrimBuild::begin( GFXLineList, numLines * 2 );
  459. while ( numLines-- )
  460. {
  461. PrimBuild::color( getDebugColor( lines->color0 ) );
  462. PrimBuild::vertex3fv( px3Cast<Point3F>(lines->pos0));
  463. PrimBuild::color( getDebugColor( lines->color1 ) );
  464. PrimBuild::vertex3fv( px3Cast<Point3F>(lines->pos1));
  465. lines++;
  466. }
  467. PrimBuild::end();
  468. }
  469. // Render triangles
  470. {
  471. physx::PxU32 numTris = mRenderBuffer->getNbTriangles();
  472. const physx::PxDebugTriangle *triangles = mRenderBuffer->getTriangles();
  473. PrimBuild::begin( GFXTriangleList, numTris * 3 );
  474. while ( numTris-- )
  475. {
  476. PrimBuild::color( getDebugColor( triangles->color0 ) );
  477. PrimBuild::vertex3fv( px3Cast<Point3F>(triangles->pos0) );
  478. PrimBuild::color( getDebugColor( triangles->color1 ) );
  479. PrimBuild::vertex3fv( px3Cast<Point3F>(triangles->pos1));
  480. PrimBuild::color( getDebugColor( triangles->color2 ) );
  481. PrimBuild::vertex3fv( px3Cast<Point3F>(triangles->pos2) );
  482. triangles++;
  483. }
  484. PrimBuild::end();
  485. }
  486. }