一共有 种概念,分别是:
所有的物理模拟都是在场景中进行的。一个场景包含actors, joints, effectors , 可以同时存在多个场景,每个场景各自进行模拟演算,各个场景之间的对象没有联系。不过你可以通过一些方法使场景之间可以进行通信。
场景没有空间上的范围,属于一个场景中的各种对象实际上构成了逻辑上的“组”。场景的任何特性都将作用于组内的各种对象和动作。
大多数的模拟都只需要在一个场景中进行。如果你在开发网络游戏,那么可能在Server 和Client各自存在一个场景,这些场景共同使用一个CPU(GPU?PPU?)。这个情况下,一个处理器就会创建多个场景:Client场景模拟用户周围的环境,而Server场景则模拟整个世界。
直接使用默认值,而不为场景描述符指定任何属性,就可以创建场景了。不过一般来说,场景总是需要一个重力加速度的,可以通过 NxScene::setGravity()来更改这一属性。
场景主要的特性是对物理模拟的实时演算。它将根据时间来计算各种变量的当前值。
模拟演算一针只会进行一次。通常我们会根据FPS来计算时间的流逝,以期望得到更加真实的模拟结果。
注意,时间片不能太长,否则模拟的稳定性就会下降,演算得出的结果也不再可靠。
一般使用以下方法来推进模拟:
Void simulate( NxReal elapsedTime);
通过调用该方法,可以通知引擎根据流逝的时间来进行从上次情况到当前情况的模拟演算。不过该方法的演算精确度还要取决于 setTiming()
Void setTinging ( NxReal maxTimestep = 1.0f / 60.0f ,
NxU32 maxIter = 8,
NxTimeStepMethod method = NX_TIMESTEP_FIXED)
时间片变量当然也可以通过场景描述符来指定。默认值可以适用于大多数程序的需求。
“使用修正的时间”这个步骤非常重要,它保证了模拟的稳定性和可重复性,因此在很多程序里,都推荐使用它—通过设置NX_TIMESTEP_FIXED。
在引擎内部,引擎自身可以将流逝的时间划分为更小的时间片。
当调用simulate() 的时候传递了一个 比 maxTimestep还要的大的值,引擎就使用maxTimestep对时间进行划片。剩余部分将累计到下一次的模拟中。
maxIter 变量规定了划分时间片的最大次数。如果超过了划分次数时间仍然有剩余,剩余部分将累计到下一次的模拟中。
不过使用可变长度的时间片依然有效。通过指定 NX_TIMESTEP_VARIABLE,引擎将不会对时间进行分片,而是直接使用用户给出的时间长度作为一个片。
推荐使用如下方法来划分时间片:
使用定长时间片划分策略,而流逝时间是一个常量,并且是maxTimestep的倍数。通过这种方法,用户可以清楚的知道自己把时间划分成了多少个片,
这样做的好处在于,任何物理上的行为,都是确定的,可以预测的。如果用户使用的是变长时间片模式来进行模拟,一切都将变得不可预测。不过使用变长时间片,用户可以得到多次调用 simulate的能力。比如在 moveGlobalPose() 和 addForce() 这2个函数中分别调用。
PhysX 是多线程的。物理演算在独立的线程中进行。演算的状态更新需要按以下顺序进行函数调用:
1、 开始模拟
2、 确保所有必须的数据都已经发送到模拟线程
3、 检查模拟是否完成,如果完成了就把模拟结果写入缓冲区。
4、 交换状态数据指针(双缓存?),将新的结果应用于程序。
函数队列可能长的跟下面差不多:
NxScene* gScene;
NxReal myTimestep = 1.0f / 60.0f
…
Void mySimulationStepFuncion()
{
gScene->simulate( myTimestep);
gScene->flushStream();
// …在这里利用上一贞计算的结果做些事情
gScene->fetchResults( NX_RIGID_BODY_FINISHED, true);
}
这里有一些关于 fetchResults()函数的使用方法的其他版本。通过函数的变量来进行选择,或者通过调用checkResults()来检查是否完成都可以。上面那段函数将发生死锁,直到所有的刚体计算完成(NX_RIGID_BODY_FISHED == true)。使用不会发生死锁的执行流程,可以更加充分的利用资源。
void mySimulationStepFunction()
{
gScene->simulate(myTimestep);
gScene->flushStream();
//...perform useful work here using previous frame's state data
while(!gScene->checkResults(NX_RIGID_BODY_FINISHED, false)
{
// do something useful
}
gScene-> fetchResults(NX_RIGID_BODY_FINISHED, true);
}
或者
void mySimulationStepFunction()
{
gScene->simulate(myTimestep);
gScene->flushStream();
//...perform useful work here using previous frame's state data
while(!gScene->fetchResults(NX_RIGID_BODY_FINISHED, false)
{
// do something useful
}
}
变量 NX_RIGID_BODY_FINISHED 是NxSimulationStatus的一个标志量
注意:在 2.1.2 之前,对应的函数是 startRun() / finishRun().
注意:在调用 simulate() / fetchResults() 这对函数之前,动态切换双缓存这个事情对一些函数是不可见的,比如 overlap? 和 raycasting?.
次世代主机正在朝着多处理器体系发展,因此也具有了更加强大的计算能力,这种提升将应用于渲染、动画、AI、物理、声音以及游戏的方方面面。这也是为物理引擎设计多线程模拟能力的基本动机。在多处理器的环境下,运行simulate()/fetchResults()函数块的线程可以从主线程中独立出去。所以就更快。
多线程物理API是为了“完美级”的物理表现效果而设计的,但是这也意味着次世代的开发者们面临着同步、负载平衡、系统瓶颈等等一系列新的问题。AGEIA针对次世代系统的前沿而设计,因此将发挥它们的最大潜力。
1、 PhysX默认计算4级连接以内的效果。这个值最大为30。
2、 旋转太快会出问题,所以应该设置最大值。
actor->setMaxAngularVelocity(maxAV);
3、 应该保证参与物理计算的各项值(如质量、速度、力)都是比较合理的,这样得出的结果的精确度才能够保证。可以参考真实世界的大致比例。
4、 通过gPhysicsSDK->setParameter(NX_ADAPTIVE_FORCE, 0); 关闭各种作用力的影响。
技巧:
1、尽量使用 弹性连接杆 来代替 弹簧 和 泵阀受动器(?)。
2、使用弹簧的时候,保持弹性常量 ,并且将衰减设置的低一些。
3、不要乱设质点,尽量把质点设置到对象的碰撞体内。特别是对那些附载了弹簧的对象。
4、通过调整皮肤厚度,可以让对象更方便的堆砌在一起。
对象分为两种:静态对象、动态对象。
静态对象主要用来做碰撞检测。有形状属性,没刚体属性。
动态对象,形状属性不是必须的,但是刚体属性是必须的。
Shapes in Actors
静态对象必须要指定形状。
没有固定形状的对象 必须指定刚体属性,包括质点和惯性张力。
一个有固定形状的动态对象应该满足以下的某一条:
1、 有质量,0密度,没有惯性张力(后两者可以计算得到)
2、 0质量,非0密度,没有惯性张力
3、 有质量,0密度,有惯性张力。
不满足以上条件的对象将不会被创建。
混合形状是自动构造的。
1、 为一个非混合形状的对象添加一个新形状,将产生一个混合形状。
2、 一个个的添加形状,比在构造对象的时候一次指定所有的形状要慢。
3、 应该避免静态对象有一个混合形状。混合形状有3角面限制,超过上限后,再添加的形状将直接忽略。
对象的一个重要属性是它的pose(位置和朝向)。PhysX在这个问题上提供了一种灵活的解决方法,因此首先应该理解一些空间关系。
举个例子,现在我们要模拟一张桌子。这个桌子有一个Box桌面和4个Box腿。
动态对象有一系列关于刚体模拟的属性。
线性相关变量:
Mass
Position
Velocity
Force
旋转相关变量:
Inertia
Orientation
Angular velocity
Torque
开发人员一般都对运动学知之甚少,因此惯性张量可能是刚体属性中最特殊的地方。之前在“刚体属性”这一章节,我们也提到过,惯性张量只用来描述刚体质量分布,即使这个Actor没有Shape,也可以设置Inertia。不过就算Actor有Shape和质量,我们为了实现一些特别的效果,可能会直接修改惯性张量。(也就是说惯性张量的优先级高于质量和形状。不过暂时未确定该说法…)
(译者: 假设物体绕着某个点旋转,物体和点之间的距离为 r, 角速度为 w.
惯性张量 I = m * r*r;
向心力 F = m * r * w*w
旋转纽矩 t = F*r = m * r*r * w*w = I * r;
因此对于旋转来说,惯性张量描述了改变物体旋转状态的难易程度。当物体绕着自己的质心旋转时,就反映了自转的难易程度。)
因此惯性张量 也代表了对象 转向的难易程度。从生活中就可以感觉到:旋转一根棍子很简单,但是旋转一个链条则不那么顺畅,摆动它远比摆动一根棍子要难。
高校的物理课本里曾经告诉你如何计算一个特定形状的瞬间惯性张量。不过这些入门级的知识只讲了计算沿着特定轴的惯性张量。在SDK里,我们使用矩阵(NxMat34),它可以描述对象在任意方向上的惯性张量。可以使用向量或矩阵在任意坐标系(本地-世界)中描述向量。
在做内部计算的时候,矩阵会被分解为 一个旋转矩阵 和一个平移向量。这样可以提高计算速度。
当你创建一个刚体,你也许会想为这个刚体单独指定惯性张量。默认情况下的惯性张量都是一样的,这可能跟你设置的形状不相符。最简单的方法是将惯性张量的大小和Actor的形状进行关联,SDK可以通过密度或总质量来计算出这个形状的惯性张量。当然,更有效的办法是你自己计算惯性张量。SDK提供了大量的相关函数,可以参阅
NxExportedUtils.h
NxInertiaTensor.h
跟mesh相关的,以下两个函数也很有用:
NxConvexMesh::getMassInformation
NxTriangleMesh::getMassInformation
NxActor 的方法:
void setMassSpaceInertiaTensor(const NxVec3 &m); NxVec3 getMassSpaceInertiaTensor();
以上两个函数提供了对惯性张量的访问函数。
旋转的惯性张量可以通过以下函数来访问
void setCMassLocalOrientation (const NxMat33 &); NxMat33 getCMassLocalOrientation ();
以下方法:
void getGlobalInertiaTensor (NxMat33 &dest); NxMat33 getGlobalInertiaTensorInverse();
rotate this tensor using the current actor orientation into world space; as a result, the tensor changes every time the actor moves.不懂..囧
有时候,手动给Actor对象指定一个质心很有用,可以描述不均匀的质量分布。(比如不倒翁)。指定一个比普通质心低的质心将增强不倒翁效果。
默认情况下,创建一个Actor时SDK自动计算质心和惯性张量。不过用户也可以通过Actor的成员变量 massLocalPose, massSpaceInertia 来重新设置这2个值:
NxReal NxComputeSphereMass (NxReal radius, NxReal density);
NxReal NxComputeSphereDensity (NxReal radius, NxReal mass);
NxReal NxComputeBoxMass (const NxVec3& extents, NxReal density);
NxReal NxComputeEllipsoidMass (const NxVec3& extents, NxReal density);
...
void NxComputeBoxInertiaTensor (NxVec3& diagInertia, NxReal mass, NxReal xlength, NxReal ylength, NxReal zlength);
void NxComputeSphereInertiaTensor(NxVec3& diagInertia, NxReal mass, NxReal radius, bool hollow);
massLocalPose 是一个将惯性张量对角线化的变换矩阵,用于改变对象原有的Pose(Pose,对象在本地坐标系的原点,并且没有旋转的状态)。
译者:如果将惯性张量(有九个分量,其中六个是独立的)对角线化,那么会得到一组主轴,以及一个转动惯量(只有三个分量)
另外一个常用的函数:
bool NxDiagonalizeInertiaTensor(const NxMat33 & denseInertia, NxVec3 & diagonalInertia, NxMat33 & rotation);
该函数根据一个旋转矩阵和一个密度惯性张量(惯性张量的密度?不懂囧)求得一个对角线化的惯性张量。
静态对象跟动态对象不同的地方在于没有任何关于刚体属性的描述。要创建一个静态对象,只需要保持Actor描述符中的刚体字段为空即可。
一旦静态对象被创建,就不要对它进行任何操作。虽然移动位置、添加形状、或者删除静态对象等操作并没有明确被禁止,这里有2个理由说明为什么:
1、 SDK认为静态对象一直都是静态的(不会改变初始状态),因此很多优化策略都是基于这点来进行的。改变静态对象将导致SDK重新计算这些预先优化的数据结构。
2、 编写一些关于Actor或者joints 的控制的代码时,已经假设了静态对象在模拟时间片内是不会发生改变的,因为这会带来极大的性能优化空间。
举个例子:如果移动一个在一堆盒子下面的静态对象,虽然这些盒子都飞到了半空中,但是他们不会被激活,即使他们被激活了,在“移动的静态对象”和动态对象之间进行碰撞检测的结果质量也不会很高。
为了实现可移动的静态对象,应该使用 “运动学对象”。