物理引擎Havok教程(四)第一个Havok程序

时间:2022-01-30 14:33:25

 这一期详细分析我在前面发布的那个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