这一期详细分析我在前面发布的那个Havok实例的代码。运行效果和代码下载请参看我之前的一篇文章《物理引擎Havok的一个简单实例(使用Ogre渲染)》。最新的代码可以在这里下载:http://code.google.com/p/ogrehavok/downloads/list
在例子中,我用了开源的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的场景节点。
具体请看代码,注释写的很清楚。
好了就是这样,文章写的比较糟糕,见谅。有问题,可以和我联系,songnianhu@163.com