物理引擎Havok教程
在例子中,我用了開源的Ogre作為渲染引擎。如果你想撇開圖形渲染,只看Havok模擬的程式碼,可以參看Havok中自帶的一個例項StandAloneDemos。
一、類HavokSystem
例子中類HavokSystem封裝了Havok初始化和執行程式碼。它初始化基本庫和多執行緒模擬,並建立物理世界。宣告如下:
class HavokSystem
{
public:
HavokSystem(void);
~HavokSystem(void);
//建立hkpWorld
virtual bool createHavokWorld(hkReal worldsize);
//初始化VDB
virtual bool InitVDB();
//建立物理場景
virtual void createPhysicsScene();
void setGroundSize(hkReal x,hkReal y,hkReal z);
void setGroundPos(hkReal x,hkReal y,hkReal z);
//step simulation
virtual void simulate();
void setup();
//Physics
hkpWorld* m_World;
protected:
//成員變數
hkPoolMemory* m_MemoryManager;
hkThreadMemory* m_ThreadMemory;
char* m_StackBuffer;
int m_StackSize;
//多執行緒相關
hkJobThreadPool* m_ThreadPool;
int m_TotalNumThreadUsed;
hkJobQueue* m_JobQueue;
//VDB相關
hkArray<hkProcessContext*> m_Contexts;
hkpPhysicsContext* m_Context;
hkVisualDebugger* m_Vdb;
hkpRigidBody* m_Ground; //地面,即靜態剛體
hkVector4 m_GroundSize; //地面尺寸
hkVector4 m_GroundPos; //地面位置
// 省去無關細節
...
};
1.初始化多執行緒
HavokSystem類的建構函式實現了多執行緒模擬的初始化,來看具體程式碼:
首先,要初始化Havok的基本庫。初始化記憶體管理器,然後呼叫hkBaseSystem的init方法。注意,init呼叫後,m_MemoryManager被hkBaseSystem擁有,所以要記住將它的引用計數減一。
m_MemoryManager = new hkPoolMemory();
m_ThreadMemory = new hkThreadMemory(m_MemoryManager);
hkBaseSystem::init(m_MemoryManager,m_ThreadMemory,errorReport);
m_MemoryManager->removeReference();
接著初始化堆疊。
m_StackSize = 0x100000;
m_StackBuffer = hkAllocate<char>(m_StackSize,HK_MEMORY_CLASS_BASE);
hkThreadMemory::getInstance().setStackArea(m_StackBuffer,m_StackSize);
最後,真正的初始化多執行緒。通過hkHardwareInfo,可以獲取與系統執行和硬體相關的資訊,比如執行緒數,核心數等。這裡只是用它獲取在多執行緒模擬中使用的執行緒的數目。Havok在建立物件時,一般都使用*Cinfo的形式指定建立的引數,例如建立hkpWorld,就先填充引數到hkpWorldCinfo,然後用new hkpWorld(hkpWorldCinfo&)建立hkpWorld物件,其它與此類似。
int m_TotalNumThreadsUsed;
hkHardwareInfo hwInfo;
hkGetHardwareInfo(hwInfo);
m_TotalNumThreadsUsed = hwInfo.m_numThreads;
hkCpuJobThreadPoolCinfo,CPU執行緒池的資訊,如執行緒的數目,每個執行緒的堆疊的大小等。它的成員m_numThreads是可以使用的執行緒的數目,要減一,因為主執行緒不在計算內。m_timerBufferPerThreadAllocation,它是為了儲存timer的資訊,而在每個執行緒中分配的緩衝區的尺寸。如果使用VDB(視覺化偵錯程式),建議設為2000000。
hkCpuJobThreadPoolCinfo gThreadPoolCinfo;
gThreadPoolCinfo.m_numThreads = m_TotalNumThreadsUsed-1;
gThreadPoolCinfo.m_timerBufferPerThreadAllocation = 200000;
m_ThreadPool = new hkCpuJobThreadPool(gThreadPoolCinfo);
建立工作佇列。這在前面介紹基本庫時有介紹,可以複習一下。
hkJobQueueCinfo info;
info.m_jobQueueHwSetup.m_numCpuThreads = m_TotalNumThreadsUsed;
m_JobQueue = new hkJobQueue(info);
...//省去無關細節
以上就是Havok初始化多執行緒模擬的過程,完整的程式碼請檢視原始碼。
2.建立物理世界
類的createHavokWorld方法負責建立物理世界。一個Havok的模擬可以有一個或多個Havok世界,表現為hkpWorld的例項。它是一個容器,用來承載所有要模擬的物理物件。它有一些基本屬性,比如重力,Slover等,具體的可以檢視文件。下面演示如何建立hkpWorld例項。
hkpWorldCinfo成員m_simulationType,這裡是多執行緒模擬,所以用SIMULATION_TYPE_MULTITHREADED,另外常用的還有SIMULATION_TYPE_CONTINUOUS,表示連續模擬。m_broadPhaseBorderBehaviour,它指定hkpWorld BroadPhase(粗略檢測階段)的行為,這裡設定為BROADPHASE_BORDER_REMOVE_ENTITY,表示當物件超出hkpWorld的尺寸時,就將這個實體物件刪除。方法setBroadPhaseWorldSize()用於設定hkpWorld的尺寸,引數是一個hkVector4型別的向量。
hkpWorldCinfo worldInfo;
worldInfo.m_simulationType = hkpWorldCinfo::SIMULATION_TYPE_MULTITHREADED;
worldInfo.m_broadPhaseBorderBehaviour = hkpWorldCinfo::BROADPHASE_BORDER_REMOVE_ENTITY;
//設定world尺寸
worldInfo.setBroadPhaseWorldSize(worldsize);
//worldInfo.m_gravity = hkVector4(0.0f,-16.0f,0.0f);
建立hkpWorld,然後註冊碰撞代理(Collision Agent),需要注意的是,在多執行緒模擬時,hkpWorld提供了markForWrite和unMarkForWrite方法,這樣一種類似於臨界區的機制來防止競爭的發生。每當要修改hkpWolrd時,都要記得使用這兩個方法,或者與之功能類似的lock()和unlock()。
m_World = new hkpWorld(worldInfo);
m_World->m_wantDeactivation = false;
m_World->markForWrite();
//註冊碰撞代理
hkpAgentRegisterUtil::registerAllAgents(m_World->getCollisionDispatcher());
m_World->registerWithJobQueue(m_JobQueue);
m_World->unmarkForWrite();
3.建立物理場景
類的createPhysicsScene負責建立物理場景。這裡建立了地面。
剛體的建立,通過一個叫hkpRigidBodyCinfo的類,它指定了剛體的各種引數,如形狀(shape)、位置、運動型別等。以下演示瞭如何建立一個靜態的剛體。這裡是作為地面。注意,在修改hkpWorld之前,要先markForWrite。
m_World->markForWrite();
//建立Ground
hkpConvexShape* shape = new hkpBoxShape(m_GroundSize,0.05f);
hkpRigidBodyCinfo ci;
ci.m_shape = shape;
ci.m_motionType = hkpMotion::MOTION_FIXED;
ci.m_position = m_GroundPos;
ci.m_qualityType = HK_COLLIDABLE_QUALITY_FIXED;
建立剛體,然後新增的物理世界。hkpWorld呼叫addEntity之後,這個剛體就屬於hkpWorld了,不要忘了刪除一次引用。shape同理。
m_Ground = new hkpRigidBody(ci);
m_World->addEntity(m_Ground);
shape->removeReference();
m_World->unmarkForWrite();
4.開啟模擬
simulate方法負責整個模擬狀態的更新,每一幀或者每幾幀會呼叫它一次。為了簡單,我把模擬的頻率固定在了60幀,即1.0/60,程式碼如下:
兩次模擬的時間間隔,我固定死了,為1.0/60,這個值也是SDK推薦的值。你還可以用更小的1.0f/30,這樣可以獲得更高的效率。
hkReal timestep = 1.0f/60.0f;
hkStopwatch stopWatch;
stopWatch.start();
hkReal lastTime = stopWatch.getElapsedSeconds();
最重要的一次呼叫,進行了一次多執行緒模擬。
m_World->stepMultithreaded(m_JobQueue,m_ThreadPool,timestep);
(...省略無關細節)
hkMonitorStream::getInstance().reset();
m_ThreadPool->clearTimerData();
在這裡等待,以固定幀率。
while(stopWatch.getElapsedSeconds()<lastTime+timestep);
lastTime += timestep;
二、繫結Havok和Ogre
為了封裝Havok和Ogre,建立了一個OgreHavokBody類,它將Havok的剛體物件與Ogre的場景節點封裝在一起,簡化了操作。這個類內部會根據剛體的狀態改變而自動同步它的渲染物件。做這項工作的是它的update方法。每一幀,這個方法都會被呼叫,它讀取Havok剛體的位置和旋轉,然後用這些資訊更新Ogre的場景節點。
具體請看程式碼,註釋寫的很清楚。
好了就是這樣,文章寫的比較糟糕,見諒。有問題,可以和我聯絡,[email protected]