物理引擎--Open Dynamics Engine(ODE)

物理引擎--Open Dynamics Engine--ODE

from http://ode.org/wiki/index.php/Manual

1 介绍

1.1 概述

ODE 是一个用于模拟刚体动力学的开源高性能库。它功能齐全、稳定、成熟且独立于平台,并具有易于使用的 C/C++ API。它具有先进的关节类型和集成的摩擦碰撞检测。ODE 对于模拟虚拟现实环境中的车辆、物体和虚拟生物非常有用。目前它被用于许多计算机游戏、3D 创作工具和模拟工具中。

1.2 代码

https://bitbucket.org/odedevs/ode/src/master/

1.3 wiki

http://ode.org/wiki/index.php/Main_Page
http://ode.org/wiki/index.php/Manual

1.4 特征

ODE 非常适合模拟铰接刚体结构。当各种形状的刚体通过各种关节连接在一起时,就会形成铰接结构。例如地面车辆(轮子连接到底盘)、有腿生物(腿连接到身体)或物体堆。

ODE 设计用于交互式或实时仿真。它特别适合在多变的虚拟现实环境中模拟移动物体。这是因为它快速、稳健且稳定,即使在模拟运行时,用户也可以完全自由地更改系统结构。

ODE使用高度稳定的积分器,因此模拟误差不会失控。其物理意义是模拟系统不应该无缘无故地“爆炸”(相信我,如果你不小心的话,其他模拟器会经常发生这种情况)。ODE 强调速度和稳定性而不是物理精度。

ODE 具有硬接触。这意味着只要两个物体发生碰撞,就会使用特殊的非穿透约束。许多其他模拟器中使用的替代方案是使用虚拟弹簧来表示接触。这很难正确完成,而且极易出错。

ODE 有一个内置的碰撞检测系统。但是,如果您愿意,您可以忽略它并进行自己的碰撞检测。当前的碰撞基元是球体、长方体、圆柱体、胶囊、平面、射线和三角形网格 - 稍后将出现更多碰撞对象。ODE 的碰撞系统通过“空间”的概念提供对潜在相交对象的快速识别。(请参阅碰撞矩阵以找出实现了哪些原语 — 原语碰撞)

以下是其特点:

  • 具有任意质量分布的刚体。
  • 关节类型:球窝、铰链、滑块(棱柱)、铰链 2、固定、角电机、线性电机、万向。
  • 碰撞基元:球体、长方体、圆柱体、胶囊体、平面、射线、三角形网格、凸面。
  • 碰撞空间:四叉树、哈希空间、简单。
  • 模拟方法:运动方程源自 Trinkle/Stewart 和 Anitescu/Potra 基于拉格朗日乘子速度的模型。
  • 使用一阶积分器。它速度很快,但对于定量工程来说还不够准确。高阶积分器稍后会出现。
  • 时间步进方法的选择:可以使用标准的“大矩阵”方法或更新的迭代 QuickStep 方法。
  • 接触和摩擦模型:这基于 Baraff 描述的 Dantzig LCP 求解器,尽管 ODE 实现了对哥伦布摩擦模型的更快近似。
  • 有一个本地 C 接口(尽管 ODE 主要是用 C++ 编写的)。
  • 有一个建立在 C 接口之上的 C++ 接口。
  • 许多单元测试,并且一直在编写更多单元测试。
  • 平台特定的优化。

1.5 许可

ODE 版权所有© 2001-2004 Russell L. Smith。版权所有。

该库是免费软件;您可以根据以下任一条款重新分发和/或修改它:

  • 自由软件基金会发布的GNU 宽通用公共许可证;许可证版本 2.1 或(由您选择)任何更高版本。GNU 宽通用公共许可证的文本包含在该库的文件中LICENSE.TXT。
  • 该库包含在文件中的BSD 样式许可证LICENSE-BSD.TXT。

分发此库是希望它有用,但不提供任何保证;甚至没有适销性或特定用途适用性的默示保证。请参阅文件LICENSE.TXT并LICENSE-BSD.TXT了解更多详细信息。

2 安装使用

http://ode.org/wiki/index.php/Manual#Install_and_Use

3 概念

3.1 背景

有关刚体动力学和模拟的背景信息,请参考 Baraff 优秀的SIGGRAPH 教程。
http://www.cs.cmu.edu/~baraff/sigcourse/index.html

3.2 刚体

从模拟的角度来看,刚体具有各种属性。一些属性会随着时间的推移而改变:

  • 身体参考点的位置向量 (x,y,z)。目前,参考点必须对应于身体的质心。
  • 参考点的线速度,矢量 (vx,vy,vz)。
  • 物体的方向,由四元数 (qs,qx,qy,qz) 或 3x3 旋转矩阵表示。
  • 角速度矢量 (wx,wy,wz) 描述方向如何随时间变化。

其他身体特性通常随着时间的推移保持不变:

  • 身体的质量。
  • 质心相对于参考点的位置。在当前的实现中,质心和参考点必须重合。
  • 惯性矩阵。这是一个 3x3 矩阵,描述了身体的质量如何围绕质心分布。从概念上讲,每个物体都嵌入了一个 xyz 坐标系,它随物体移动和旋转,如图 1 所示。
    在这里插入图片描述
    该坐标系的原点是身体的参考点。ODE 中的某些值(向量、矩阵等)是相对于身体坐标系的,而其他值是相对于全局坐标系的。
    请注意,刚体的形状不是动态属性(除非它影响各种质量属性)。只有碰撞检测才关心身体的细节形状。

3.2.1 岛屿和禁用实体

实体们通过关节相互连接。实体对应的“岛”是一个无法分开的群体——换句话说,每个实体都以某种方式与岛上的其他实体相连。

在进行模拟步骤时,世界上的每个岛屿都会被单独处理。知道这一点很有用:如果模拟中有N 个相似的岛屿,那么步骤计算时间将为O ( N )。

每个实体都可以启用或禁用。禁用的实体实际上被“关闭”,并且在模拟步骤期间不会更新。当已知物体静止或与模拟无关时,禁用物体是节省计算时间的有效方法。

如果岛上有任何启用的实体,则岛上的每个实体都将在下一个模拟步骤中启用。因此,为了有效地禁用一个实体岛,岛上的每个实体都必须被禁用。如果禁用的岛被另一个启用的实体接触,则整个岛将启用,因为接触关节会将启用的实体连接到岛。

3.3 一体化

随着时间的推移模拟刚体系统的过程称为积分。每个积分步骤都会将当前时间推进给定的步长,调整所有刚体的状态以获得新的时间值。与任何集成商合作时需要考虑两个主要问题:

  • 它有多准确?也就是说,模拟系统的行为与现实生活中发生的情况有多接近?
  • 它有多稳定?也就是说,计算错误是否会导致模拟系统完全非物理行为?(例如导致系统无缘无故“爆炸”)。

ODE 的电流积分器非常稳定,但不是特别准确,除非步长很小。对于 ODE 的大多数用途来说,这不是问题——在几乎所有情况下,ODE 的行为仍然看起来完全是物理的。但是,在未来版本中解决此准确性问题之前,ODE 不应用于定量工程。

3.4 积分器

在每个积分器步骤之间,用户可以调用函数来向刚体施加力。这些力被添加到刚体对象中的“力累加器”中。当积分器的下一个步骤发生时,所有施加的力的总和将用于推动身体。在每个积分器步骤之后,力累加器被设置为零。

3.5 关节和约束

在现实生活中,关节就像铰链一样,用于连接两个物体。在 ODE 中,关节非常相似:它是两个物体之间强制的关系,因此它们只能具有相对于彼此的特定位置和方向。这种关系称为约束——联合和约束这两个词经常互换使用。图 2 显示了三种不同的约束类型。
在这里插入图片描述
第一个是球窝关节,它限制一个物体的“球”与另一个物体的“球窝”位于同一位置。
第二个是铰链接头,它限制铰链的两个部分位于同一位置并沿铰链轴对齐。
第三个是滑块关节,它限制“活塞”和“插座”对齐,并另外限制两个实体具有相同的方向。

每当积分器迈出一步时,所有关节都可以向它们影响的物体施加约束力。这些力经过计算,使得身体以保持所有关节关系的方式移动。

每个关节都有许多控制其几何形状的参数。一个例子是球窝接头的球窝点的位置。设置关节参数的函数都采用全局坐标,而不是相对于身体的坐标。这样做的结果是,在连接关节之前,必须正确定位关节连接的刚体。

3.6 关节组

关节组是一个特殊的容器,用于容纳世界中的关节。可以将关节添加到一组中,然后当不再需要这些关节时,可以通过一个函数调用非常快速地销毁整组关节。然而,在整个组被清空之前,组中的单个关节不能被破坏。

这对于接触关节最有用,接触关节在每个时间步骤中都会成组添加和从世界中删除。

3.7 关节误差和误差减少参数 (ERP) Joint error and the Error Reduction Parameter

当关节连接两个物体时,这些物体需要具有一定的相对位置和方向。然而,身体可能处于不满足关节约束的位置。这种“联合错误”可能以两种方式发生:

  • 如果用户设置一个物体的位置/方向而没有正确设置另一物体的位置/方向。
  • 在模拟过程中,错误可能会逐渐出现,导致物体偏离所需的位置。

图 3 显示了球窝接头中的错误示例(球窝未对齐)。
在这里插入图片描述
有一种机制可以减少关节误差:在每个模拟步骤中,每个关节都会施加一种特殊的力,使其身体重新对齐。该力由误差减少参数(ERP)控制,该参数的值介于 0 和 1 之间。

ERP 指定在下一个模拟步骤中将修复关节误差的比例。如果 ERP=0,则不施加校正力,随着模拟的进行,物体最终会漂移开。如果 ERP=1,则模拟将尝试在下一个时间步修复所有关节错误。然而,不建议设置ERP=1,因为由于各种内部近似,关节误差不会完全固定。建议 ERP=0.1 至 0.8 的值(默认值 0.2)。

可以设置影响模拟中大多数关节的全局 ERP 值。然而,一些关节具有控制关节各个方面的局部 ERP 值。

3.8 软约束和约束力混合 (CFM) Soft constraint and Constraint Force Mixing

大多数约束本质上都是“困难的”。这意味着约束代表永远不会被违反的条件。例如,球必须始终位于插座中,并且铰链的两个部分必须始终对齐。实际上,无意中将错误引入系统可能会违反约束,但可以设置错误减少参数来纠正这些错误。

并非所有约束都是困难的。一些“软”约束是为了被违反而设计的。例如,防止碰撞对象穿透的接触约束在默认情况下是硬的,因此它的作用就好像碰撞表面是由钢制成的。但它可以制成软约束来模拟较软的材料,从而在两个物体被迫在一起时允许它们自然渗透。

有两个参数控制硬约束和软约束之间的区别。第一个是已经引入的误差减少参数(ERP)。第二个是约束力混合 (CFM) 值,如下所述。

3.8.1 约束力混合 (CFM) Constraint Force Mixing

以下是对 CFM 含义的技术性描述。如果您只想知道它在实践中如何使用,请跳到下一节。
传统上,每个关节的约束方程具有以下形式
J v = c
其中 v 是所涉及物体的速度矢量,J 是一个“雅可比”矩阵,关节从系统中移除的每个自由度都有一行,并且 c 是右侧向量。在下一个时间步长,计算 λ 向量(和 c 大小相同),以便施加到物体上以保持关节的力约束条件是:
F c = J T λ F_c = J^{T}λ Fc=JTλ
ODE 增添了新的变化。ODE 的约束方程的形式为
J v = c + C F M λ J v = c + CFMλ Jv=c+CFMλ
其中CFM是方对角矩阵。CFM将产生的约束力与产生它的约束混合在一起。CFM的非零(正)值允许违反原始约束方程的量与CFM乘以执行约束所需的恢复力λ成正比。求解λ得到
( J M − 1 J T + 1 h C F M ) λ = 1 h c (JM^{-1}J^{T}+\dfrac{1}{h}CFM)λ=\dfrac{1}{h}c (JM1JT+h1CFM)λ=h1c
因此, CFM只是简单地添加到原始系统矩阵的对角线上。使用正值CFM的另一个好处是可以使系统远离任何奇异点,从而提高因式分解器的精度。

3.8.2 如何使用 ERP 和 CFM

ERP和CFM可以在许多关节中独立设置。它们可以设置在接触关节、关节限制和各种其他地方,以控制关节(或关节限制)的海绵性和弹性。

如果 CFM 设置为零,则约束将很严格。如果 CFM 设置为正值,则可能会通过“推动”来违反约束(例如,对于通过将两个接触对象强制在一起的接触约束)。换句话说,约束将是软约束,并且软约束将随着 CFM 的增加而增加。这里实际发生的情况是,允许违反约束的量与 CFM 乘以强制执行约束所需的恢复力成正比。请注意,将 CFM 设置为负值可能会产生不良影响,例如不稳定。不要这样做。

通过调整ERP和CFM的值,可以达到多种效果。例如,您可以模拟弹性约束,其中两个实体就像通过弹簧连接一样振荡。或者您可以模拟更多的海绵约束,而无需振荡。事实上,可以选择 ERP 和 CFM 来达到与任何所需弹簧和阻尼器常数相同的效果。如果您有弹簧常数 k p k_p kp 和阻尼常数 k d k_d kd,则相应的 ODE 常数为:
E R P = h k p h k p + k d ERP=\dfrac{hk_p}{hk_p+k_d} ERP=hkp+kdhkp \qquad C F M = 1 h k p + k d CFM=\dfrac{1}{hk_p+k_d} CFM=hkp+kd1
h是步长。这些值将给出与隐式一阶积分模拟的弹簧-阻尼器系统相同的效果。
增大CFM,特别是全局CFM,可以减小模拟中的数值误差。如果系统接近奇异,那么这可以显著提高稳定性。事实上,如果系统行为不正常,首先要做的事情之一就是增加全局CFM。

3.9 碰撞处理

关于碰撞处理有很多东西需要写。
物体之间或物体与静态环境之间的碰撞处理如下:

  • 在每个模拟步骤之前,用户调用碰撞检测函数来确定什么正在接触什么。这些函数返回接触点列表。每个接触点指定空间中的位置、表面法线向量和穿透深度。
  • 为每个接触点创建一个特殊的接触接头。接触接头会获得有关接触的额外信息,例如接触表面上存在的摩擦力、弹性或柔软程度以及各种其他属性。
  • 接触接头被放置在一个接头“组”中,这使得它们可以非常快速地添加到系统中或从系统中删除。随着接触点数量的增加,模拟速度会下降,因此可以使用各种策略来限制接触点的数量。
  • 采取模拟步骤。
  • 所有接触接头均从系统中移除。

请注意,不必使用内置碰撞函数 - 可以使用其他碰撞检测库,只要它们提供正确类型的接触点信息即可。

3.10 典型仿真代码

典型的模拟将如下进行:

  • 创建一个动态的世界。
  • 在动态世界中创建物体。
  • 设置所有物体的状态(位置等)。
  • 在动态世界中创建关节。
  • 将关节​​连接到主体上。
  • 设置所有关节的参数。
  • 根据需要创建碰撞世界和碰撞几何对象。
  • 创建一个关节组来固定接触关节。
  • 环形:
    • 根据需要对主体施加力。
    • 根据需要调整关节参数。
    • 呼叫冲突检测。
    • 为每个碰撞点创建一个接触关节,并将其放入接触关节组中。
    • 采取模拟步骤。
    • 拆除接触接头组中的所有接头。
  • 摧毁动态和碰撞世界。

3.11 物理模型

这里讨论 ODE 中使用的各种方法和近似值。

3.11.1 摩擦近似

在这里插入图片描述
库仑摩擦模型是一种简单但有效的接触点摩擦建模方法。它是接触点处存在的法向力和切向力之间的简单关系(有关这些力的描述,请参阅接触接头部分)。规则是:

∣ F T ∣ ≦ μ ∣ F N ∣ |F_T|≦μ|F_N| FTμFN

其中 F T F_T FT F N F_N FN 分别为法向和切向力矢量,μ为摩擦系数(通常为1.0左右的数字)。这个方程定义了一个“摩擦锥体”——想象一个以 F N F_N FN 为轴,接触点为顶点的锥体。如果总摩擦力矢量在圆锥内,则接触处于“粘着模式”,并且摩擦力足以防止接触面相互移动。如果力矢量在锥体表面,则接触处于“滑动模式”,摩擦力通常不足以防止接触表面滑动。因此,参数μ表示切向力与法向力的最大比值。

由于效率的原因,ODE的摩擦模型近似于摩擦锥。目前有两种近似值可供选择:

  • μ 的含义被改变,以便它指定在任何一个切向摩擦方向上接触处可能存在的最大摩擦(切向)力。这是相当非物理的,因为它是独立于在法向力,但它可能是有用的,它是计算上最便宜的选择。请注意,在这种情况下 μ 是力极限,必须选择适合模拟的值。
  • 摩擦锥体近似于与第一和第二个摩擦方向对齐的摩擦金字塔(我真的需要一张图片)。进一步近似:首先ODE计算法向力,假设所有接触都是无摩擦的。然后计算摩擦(切向)力的最大极限 F M F_M FM
    F M = μ ∣ F N ∣ F_M=μ|F_N| FM=μFN

然后用这些固定的极限求解整个系统(类似于上面的近似1的方法)。这与真正的摩擦金字塔的不同之处在于,“有效” mu 并不完全固定。这种近似更容易使用,因为 μ 是一个无单位的比率,与普通的科隆摩擦系数相同,因此可以设置为1.0左右的恒定值,而无需考虑具体的模拟。

4 数据类型与约定

4.1 基本数据类型

ODE 库可以构建为使用单精度或双精度浮点数。单精度速度更快,使用的内存更少,但模拟会产生更多数值误差,从而导致明显的问题。单精度会降低准确性和稳定性。
(必须描述哪些因素影响准确性和稳定性)。
浮点数据类型是 dReal。其他常用的类型有 dVector3、dMatrix3、dQuaternion。
非标量浮点类型全部实现为简单的dReals 数组。它们的布局约定如下:

Name Implementation Format
dQuaternion dReal[4] [ w, x, y, z ], where w is the real part and (x, y, z) form the vector part.
[ w, x, y, z ],其中 w 是实部,(x, y, z) 形成向量部分。
dVector4 dReal[4] [ x, y, z, 1.0 ]
dVector3 dReal[4] Same as dVector4; the 4th element is there only for better alignment, and should be ignored.
与 dVector4 相同;第四个元素只是为了更好的对齐,应该被忽略。
dMatrix4 dReal[4*4] A 4x4 matrix, laid out in row-major order, usually used as a homogeneous transform matrix. This means that the upper-left 3x3 elements are a rotation matrix, the first three elements of the last column are a translation vector, and the last row is simply [ 0, 0, 0, 1 ].
4x4 矩阵,按行主序排列,通常用作齐次变换矩阵。这意味着左上角的 3x3 元素是旋转矩阵,最后一列的前三个元素是平移向量,最后一行只是 [ 0, 0, 0, 1 ]。
dMatrix3 dReal[3*4] A 3x3 matrix with the elements laid out in row-major order. The last column is ignored and, as dVector3, is used only for better alignment.
一个 3x3 矩阵,其元素按行优先顺序排列。最后一列被忽略,与 dVector3 一样,仅用于更好的对齐。

4.2 对象和 ID

可以创建多种类型的对象:

  • dWorld - 一个动态世界,包含所有模拟数据。
  • dbody - 刚体;它没有任何形状:它需要一个或多个 dGeom 来实现。
  • dJoint - 关节。
  • dJointGroup - 一组关节,可以轻松一次摧毁所有关节。
  • dSpace - 碰撞空间,用于组织和加速碰撞测试。
  • dGeom - 用于检测碰撞的形状。
    处理这些对象的函数获取并返回对象 ID。对象 ID 类型有 dWorldID、dbodyID 等。

4.3 参数约定

提供给“set”函数的所有 3 向量 (x,y,z) 均作为单独的 x,y,z 参数给出。
“get”函数的所有 3 向量结果参数都是指向 dReal 数组的指针。
较大的向量始终作为指向 dReal 数组的指针提供和返回。
除非另有说明,所有坐标均在全局坐标系中。

4.4 C 与 C++

ODE 库是用 C++ 编写的,但其公共接口由简单的 C 函数而不是类组成。为什么是这样?

  • 仅使用 C 接口更简单 - C++ 的功能对 ODE 没有太大帮助。
  • 它可以防止跨多个编译器的 C++ 修改和运行时支持问题。
  • 用户不必熟悉 C++ 怪癖即可使用 ODE。

在标头中,有一个随库一起分发的半官方 C++ 包装器odecpp*.h,但它有一些设计限制。

4.5 调试

ODE库可以在“调试”或“发布”模式下编译。调试模式速度较慢,但​​会检查函数参数并进行许多运行时测试以确保内部一致性。发布模式速度更快,但不进行任何检查。

5 世界

5.1 一般功能

世界对象是刚体和关节的容器。不同世界中的物体不能相互作用,例如来自两个不同世界的刚体不能碰撞。
世界中的所有对象都存在于同一时间点,因此使用单独世界的原因之一是以不同的速率模拟系统。
大多数应用程序只需要一个世界。

dWorldID dWorldCreate();

创建一个新的空世界并返回其 ID 号。

void dWorldDestroy (dWorldID);

毁灭一个世界以及其中的一切。这包括所有主体以及不属于关节组的所有关节。属于关节组一部分的关节将被停用,并且可以通过调用例如 dJointGroupEmpty 来销毁。

void dWorldSetGravity (dWorldID, dReal x, dReal y, dReal z);
void dWorldGetGravity (dWorldID, dVector3 重力);

设置并获取世界的全局重力矢量。在 SI 单位中,假设 +z 向上,地球的重力矢量将为 (0,0,-9.81)。默认为无重力,即(0,0,0)。

void dWorldSetERP (dWorldID, dReal erp);
dReal dWorldGetERP (dWorldID);

设置和获取全局 ERP 值,该值控制每个时间步执行的误差校正量。典型值范围为 0.1-0.8。默认值为 0.2。

void dWorldSetCFM (dWorldID, dReal cfm);
dReal dWorldGetCFM (dWorldID);

设置并获取全局 CFM(约束力混合)值。典型值的范围为 1 0 − 9 10^{-9} 109 – 1。如果使用单精度,则默认值为 1 0 − 5 10^{-5} 105 ;如果使用双精度,则默认值为 1 0 − 10 10^{-10} 1010

void dWorldSetAutoDisableFlag (dWorldID, int do_auto_disable);
int dWorldGetAutoDisableFlag (dWorldID);
void dWorldSetAutoDisableLinearThreshold (dWorldID, dReal Linear_threshold);
dReal dWorldGetAutoDisableLinearThreshold (dWorldID);
void dWorldSetAutoDisableAngularThreshold (dWorldID, dReal angular_threshold);
dReal dWorldGetAutoDisableAngularThreshold (dWorldID);
无效dWorldSetAutoDisableSteps(dWorldID,int步骤);
int dWorldGetAutoDisableSteps (dWorldID);
void dWorldSetAutoDisableTime (dWorldID, dReal time);
dReal dWorldGetAutoDisableTime (dWorldID);

设置并获取新创建的实体的默认自动禁用参数。有关此功能的说明,请参阅有关自动禁用的刚体文档。默认参数为:

AutoDisableFlag = disabled
AutoDisableLinearThreshold = 0.01
AutoDisableAngularThreshold = 0.01
AutoDisableSteps = 10
AutoDisableTime = 0
自动使能标志 = 禁用
自动禁用线性阈值 = 0.01
自动禁用角度阈值 = 0.01
自动禁用步骤 = 10
自动禁用时间 = 0
void dWorldImpulseToForce (dWorldID, dReal stepsize, dReal ix, dReal iy, dReal iz, dVector3 force);

如果你想对刚体施加线性或角冲量,而不是力或扭矩,那么你可以在调用 dBodyAdd 函数之前使用此函数将所需的冲量转换为力/扭矩矢量。
该函数给出所需的冲量为 (ix,iy,iz),并使力矢量生效。目前的算法只是将脉冲按1/stepsize进行缩放,其中stepsize是下一步要采取的步长。

这个函数被赋予了一个dWorldID,因为在将来,力的计算可能依赖于被设置为世界属性的积分器参数。

5.2 步进功能

void dWorldStep (dWorldID, dReal stepsize);

走遍世界。这使用了一种“大矩阵”方法,它需要 m 3 m^3 m3 量级的时间和 m 2 m^2 m2 量级的内存,其中 m 是约束行的总数。
对于大型系统,这将使用大量内存,并且可能非常慢,但这是目前最准确的方法。

void dWorldQuickStep (dWorldID, dReal stepsize);

逐步运行世界。它使用迭代方法,需要大约 m ∗ N m * N mN 的时间和大约 m 的内存,其中 m 是约束行的总数,N 是迭代次数。

对于大型系统,这比 dWorldStep 快很多,但准确性较差。

QuickStep 非常适合堆叠对象,尤其是在使用自动禁用功能时。然而,它对于近奇异系统的精度较差。当使用高摩擦接触、电机或某些铰接结构时,可能会出现近奇异系统。例如,坐在地上的多条腿机器人可能是近乎单一的。

有多种方法可以帮助克服 QuickStep 的不准确问题:

  • 增加 CFM。
  • 减少系统中的接触数量(例如,对机器人或生物的脚使用最少的接触数量)。
  • 请勿在接触处过度摩擦。
  • 如果适用,请使用接触滑条
  • 避免运动学循环(但是,运动学循环在有腿生物中是不可避免的)。
  • 不要使用过度的运动强度。
  • 使用基于力的电机而不是基于速度的电机。

增加 QuickStep 迭代次数可能会有所帮助,但如果您的系统确实接近奇异,则不会有太大帮助。

void dWorldSetQuickStepNumIterations (dWorldID, int num);
int dWorldGetQuickStepNumIterations (dWorldID);

设置和获取 QuickStep 方法每步执行的迭代次数。更多的迭代将给出更准确的解决方案,但计算时间会更长。默认值为 20 次迭代。

void dWorldSetQuickStepW (WorldID, dReal over_relaxation);
dReal dWorldGetQuickStepW (dWorldID);

设置和获取 QuickStep 的连续过松弛算法的过松弛参数。默认值为 1.3。

5.2.1 可变步长:不要!

您的应用程序中的步长应该保持不变。可变步长是一种令人头痛的单向旅行。例如,想象一个物体“静止”在地面上。它实际上会稳定在地下一小深度。当步长变化时,理想的稳定位置会在每一步发生变化,因此物体会抖动(并且很可能获得能量!)

ODE 的设计考虑了固定步长。可以在 ODE 中使用可变步长,但这并不容易(并且此编辑器无法帮助您)。

5.3 减震

dReal dWorldGetLinearDamping (dWorldID);
dReal dWorldGetAngularDamping (dWorldID);
void dWorldSetLinearDamping (dWorldID, dReal scale);
void dWorldSetAngularDamping (dWorldID, dReal scale);
void dWorldSetDamping (dWorldID, dReal linear_scale, dReal angular_scale); 
dReal dWorldGetLinearDampingThreshold (dWorldID);
dReal dWorldGetAngularDampingThreshold (dWorldID);
void dWorldSetLinearDampingThreshold (dWorldID, dReal threshold);
void dWorldSetAngularDampingThreshold (dWorldID, dReal threshold);

设置/获取世界的默认阻尼参数(物体默认使用世界的参数)。默认阈值为 0.01,默认阻尼为零(无阻尼)。

dReal dWorldGetMaxAngularSpeed (dWorldID);
void dWorldSetMaxAngularSpeed (dWorldID, dReal max_speed);

设置/获取新物体的默认最大角速度。

5.4 触点参数

void  dWorldSetContactMaxCorrectingVel (dWorldID, dReal vel); 
dReal dWorldGetContactMaxCorrectingVel (dWorldID);

设置并获取触点允许产生的最大校正速度。默认值为无穷大(即无限制)。减小该值有助于防止深度嵌入的对象“弹出”

void  dWorldSetContactSurfaceLayer (dWorldID, dReal depth); 
dReal dWorldGetContactSurfaceLayer (dWorldID);

设置和获取所有几何对象周围表面层的深度。在静止之前,允许触点沉入表面层达到给定的深度。默认值为零。将此值增加到某个较小的值(例如 0.001)可以帮助防止由于反复建立和断开接触而导致的抖动问题。

6 刚体函数

6.1 创建和销毁身体

dBodyID dBodyCreate (dWorldID);

在给定世界中创建一个物体,其默认质量参数位于位置 (0,0,0)。返回其 ID。

void dBodyDestroy (dBodyID);

毁掉一个物体。所有附加到该物体的关节都将被置于不定状态(即未附加且不影响模拟,但它们不会被删除)。

6.2 位置和方向

void dBodySetPosition (dBodyID, dReal x, dReal y, dReal z);
void dBodySetRotation (dBodyID, const dMatrix3 R);
void dBodySetQuaternion (dBodyID, const dQuaternion q);
void dBodySetLinearVel  (dBodyID, dReal x, dReal y, dReal z);
void dBodySetAngularVel (dBodyID, dReal x, dReal y, dReal z);
const dReal * dBodyGetPosition (dBodyID);
const dReal * dBodyGetRotation (dBodyID);
const dReal * dBodyGetQuaternion (dBodyID);
const dReal * dBodyGetLinearVel (dBodyID);
const dReal * dBodyGetAngularVel (dBodyID);

这些函数设置和获取物体的位置、旋转、线速度和角速度。设置一组主体后,如果新配置与现有的关节/约束不一致,则模拟结果不确定。获取时,返回的值是指向内部数据结构的指针,因此向量在刚体系统结构发生任何更改之前一直有效。

dBodyGetRotation() 返回 4x3 旋转矩阵。

6.3 质量和力

void dBodySetMass (dBodyID, const dMass *mass);
void dBodyGetMass (dBodyID, dMass *mass);

设置/获取物体的质量

void dBodyAddForce            (dBodyID, dReal fx, dReal fy, dReal fz);
void dBodyAddTorque           (dBodyID, dReal fx, dReal fy, dReal fz);
void dBodyAddRelForce         (dBodyID, dReal fx, dReal fy, dReal fz);
void dBodyAddRelTorque        (dBodyID, dReal fx, dReal fy, dReal fz);
void dBodyAddForceAtPos       (dBodyID, dReal fx, dReal fy, dReal fz, dReal px, dReal py, dReal pz);
void dBodyAddForceAtRelPos    (dBodyID, dReal fx, dReal fy, dReal fz, dReal px, dReal py, dReal pz);
void dBodyAddRelForceAtPos    (dBodyID, dReal fx, dReal fy, dReal fz, dReal px, dReal py, dReal pz);
void dBodyAddRelForceAtRelPos (dBodyID, dReal fx, dReal fy, dReal fz, dReal px, dReal py, dReal pz);

向实体添加力(绝对或相对坐标)。力被累积到每个物体上,并且累加器在每个时间步之后归零。
…RelForce 和 …RelTorque 函数采用相对于人体自身参考系的力矢量。
…ForceAtPos 和 …ForceAtRelPos 函数采用额外的位置向量(分别在全局坐标或身体相对坐标中)来指定施加力的点。所有其他函数都在质心处施加力。

const dReal * dBodyGetForce (dBodyID);
const dReal * dBodyGetTorque (dBodyID);

返回当前累积的力和扭矩矢量。返回的指针指向一个 3 的数组dReal。返回的值是指向内部数据结构的指针,因此向量仅在对刚体系统进行任何更改之前有效。

void dBodySetForce (dBodyID b, dReal x, dReal y, dReal z);
void dBodySetTorque (dBodyID b, dReal x, dReal y, dReal z);

设置主体力和扭矩累积矢量。这对于在重新激活之前将已停用主体的力和扭矩归零非常有用,如果在停用主体上调用了力添加功能的话。

6.4 运动状态

void dBodySetDynamic (dBodyID);
void dBodySetKinematic (dBodyID);
int dBodyIsKinematic (dBodyID);

处于“运动”状态(而不是默认的“动态”状态)的物体是“不可阻挡的”物体,其行为就好像它们具有无限质量一样。这意味着它们不会对任何力(重力、约束或用户提供的力)做出反应;他们只是跟随速度到达下一个位置。它们的目的是使对象产生动画,这些对象不会对其他物体做出反应,而是通过关节(任何关节,而不仅仅是接触关节)对其进行作用。对于针对移动几何体的接触关节,在附加到世界的关节(“空体”)中使用接触的运动参数仍然更快。两个运动体之间的关节,或者运动体和世界的关节,都被完全忽略。

注意:在运动体上设置质量将使其再次变为动态。调用 dBodySetDynamic 会恢复原始质量,因此它只是 dBodyGetMass 后面跟着 dBodySetMass 的快捷方式。

6.5 通用

void dBodyGetRelPointPos (dBodyID, dReal px, dReal py, dReal pz, dVector3 result);
void dBodyGetRelPointVel (dBodyID, dReal px, dReal py, dReal pz, dVector3 result);
void dBodyGetPointVel    (dBodyID, dReal px, dReal py, dReal pz, dVector3 result);

实用函数,获取物体上的一个点(px、py、pz)并返回该点在全局坐标(在result)中的位置或速度。函数dBodyGetRelPointXXX给出了身体相对坐标中的点,dBodyGetPointVel函数给出了全局坐标中的点。

void dBodyGetPosRelPoint (dBodyID, dReal px, dReal py, dReal pz, dVector3 result);

这是 的逆dBodyGetRelPointPos。x它采用全局坐标 ( 、y、z)中的一个点,并返回该点在相对于身体的坐标 ( result) 中的位置。

void dBodyVectorToWorld (dBodyID, dReal px, dReal py, dReal pz, dVector3 result);
void dBodyVectorFromWorld (dBodyID, dReal px, dReal py, dReal pz, dVector3 result);

给定一个以身体(或世界)坐标系(x、y、z)表示的向量,将其旋转到世界(或身体)坐标系(result)。

6.6 自动启用和禁用

每个主体都可以启用或禁用。启用的实体参与模拟,而禁用的实体则关闭并且在模拟步骤期间不会更新。新实体始终在启用状态下创建。

通过关节连接到启用主体的禁用主体将在下一个模拟步骤中自动重新启用。

禁用的物体不会消耗 CPU 时间,因此为了加快模拟物体的速度,应在它们静止时将其禁用。这可以通过自动禁用功能自动完成。

如果一个物体的自动禁用标志打开,它会在以下情况下自动禁用自己:

  • 它已经空闲了给定数量的模拟步骤。
  • 它还在给定量的模拟时间内处于空闲状态。

当物体的线速度和角速度的大小都低于给定阈值时,物体被认为是空闲的。

因此,每个主体都有五个自动禁用参数:启用标志、空闲步数、空闲时间和线/角速度阈值。新创建的物体从世界中获取这些参数。

以下函数设置和获取主体的启用/禁用参数。

void dBodyEnable (dBodyID);
void dBodyDisable (dBodyID);

手动启用和禁用实体。请注意,通过关节连接到启用主体的禁用主体将在下一个模拟步骤中自动重新启用。

int dBodyIsEnabled (dBodyID);

如果主体当前已启用,则返回 1;如果已禁用,则返回 0。

void dBodySetAutoDisableFlag (dBodyID, int do_auto_disable);
int dBodyGetAutoDisableFlag (dBodyID);

设置并获取主体的自动禁用标志。如果do_auto_disable不为零,则当主体闲置足够长的时间后将自动禁用。

void dBodySetAutoDisableLinearThreshold (dBodyID, dReal linear_threshold);
dReal dBodyGetAutoDisableLinearThreshold (dBodyID);

设置并获取物体的自动禁用线速度阈值。物体的线速度大小必须小于此阈值才能被视为空闲。将阈值设置为 ,dInfinity以防止考虑线速度。

void dBodySetAutoDisableAngularThreshold (dBodyID, dReal angular_threshold);
dReal dBodyGetAutoDisableAngularThreshold (dBodyID);

设置并获取自动禁用的身体角速度阈值。物体的线性角度大小必须小于此阈值才能被视为空闲。将阈值设置为 ,dInfinity以防止考虑角速度。

void dBodySetAutoDisableSteps (dBodyID, int steps);
int dBodyGetAutoDisableSteps (dBodyID);

设置并获取实体在自动禁用之前必须空闲的模拟步数。将其设置为零以禁用步骤数的考虑。

void dBodySetAutoDisableTime (dBodyID, dReal time);
dReal dBodyGetAutoDisableTime (dBodyID);

设置并获取实体在自动禁用之前必须空闲的模拟时间量。将其设置为零以禁用对模拟时间量的考虑。

void dBodySetAutoDisableAverageSamplesCount (dBodyID, unsigned int average_samples_count);
int dBodyGetAutoDisableAverageSamplesCount (dBodyID);

To be written …

void dBodySetAutoDisableDefaults (dBodyID);

将物体的自动禁用参数设置为世界上设置的默认参数。

void dBodySetMovedCallback (dBodyID, void (*callback)(dBodyID));

使用它来注册一个函数回调,每当主体移动时(即,当它未被禁用时)都会调用该函数回调。这对于将 ODE 与 3D 引擎集成非常有用,其中只要 ODE 实体移动,就必须移动 3D 实体。回调必须有原型void callback(dBodyID)。

6.7 减震

阻尼有两个目的:减少模拟的不稳定性,并允许物体静止(并可能自动禁用它们)。

车身是使用世界上当前的阻尼参数构建的。将比例设置为 0 将禁用阻尼。

其实现方式如下:在每个时间步之后,根据相应的阈值测试线速度和角速度。如果它们高于,则将它们乘以(1 - 比例)。因此,负比例值实际上会增加速度,大于 1 的值会使对象每一步都振荡;两者都会使模拟不稳定。

要禁用阻尼,只需将阻尼比例设置为零。

注意:步进功能移动对象后,速度会衰减。否则阻尼可能会导致接头出现误差。首先,关节约束由步进器处理(移动主体),然后应用阻尼。

注意:阻尼发生在调用移动的回调之后;这样,仍然可以使用身体在该步骤中获得的精确速度。您甚至可以使用回调来创建您自己的自定义阻尼。

dReal dBodyGetLinearDamping (dBodyID);
dReal dBodyGetAngularDamping (dBodyID);
void dBodySetLinearDamping (dBodyID, dReal scale);
void dBodySetAngularDamping (dBodyID, dReal scale);

设置并获取身体的阻尼比例。设置阻尼比例后,主体将忽略世界的阻尼比例,直到调用 dBodySetDampingDefaults() 为止。如果未设置比例,它将返回世界的阻尼比例。

void dBodySetDamping (dBodyID, dReal linear_scale, dReal angular_scale);

可同时设置线性和角度刻度的便捷功能。

dReal dBodyGetLinearDampingThreshold (dBodyID);
dReal dBodyGetAngularDampingThreshold (dBodyID);
void dBodySetLinearDampingThreshold (dBodyID, dReal threshold);
void dBodySetAngularDampingThreshold (dBodyID, dReal threshold);

设置/获取身体的阻尼阈值。仅当线速度/角速度高于阈值限制时才会应用阻尼。

void dBodySetDampingDefaults (dBodyID);

将阻尼设置重置为当前世界的设置。

dReal dBodyGetMaxAngularSpeed (dBodyID);
void dBodySetMaxAngularSpeed (dBodyID, dReal max_speed);

您还可以限制最大角速度。与阻尼函数相反,角速度在身体移动之前受到影响。这意味着它会在关节中引入错误,迫使身体旋转得太快。有些物体天生就有很高的角速度(例如汽车的车轮),因此您可能需要给它们一个非常高的限制(例如默认的 dInfinity)。

6.8 各种实体功能

void dBodySetData (dBodyID, void *data);
void *dBodyGetData (dBodyID);

获取和设置主体的用户数据指针。

void dBodySetFiniteRotationMode (dBodyID, int mode);

此函数控制身体方向在每个时间步更新的方式。参数mode可以是:

  • 0:使用“无穷小”方向更新。计算速度很快,但有时会导致高速旋转的物体不准确,特别是当这些物体与其他物体连接时。这是创建的每个新主体的默认设置。
  • 1:使用“有限方向更新”。这计算成本更高,但对于高速旋转来说会更准确。但请注意,高速旋转可能会导致模拟中出现多种类型的错误,并且此模式只能修复这些错误源之一。
int dBodyGetFiniteRotationMode (dBodyID);

返回物体当前的有限旋转模式(0 或 1)。

void dBodySetFiniteRotationAxis (dBodyID, dReal x, dReal y, dReal z);

这设置了物体的有限旋转轴。该轴仅在设置有限旋转模式时才有意义(请参阅dbodySetFiniteRotationMode)。

如果该轴为零 (0,0,0),则在主体上执行完整的有限旋转。

如果该轴非零,则通过沿轴方向执行部分有限旋转,然后沿正交方向执行无穷小旋转来旋转主体。

这对于减轻快速旋转物体引起的某些误差源很有用。例如,如果车轮高速旋转,您可以使用车轮的铰链轴作为参数来调用此函数,以尝试改进其行为。

void dBodyGetFiniteRotationAxis (dBodyID, dVector3 result);

返回物体当前的有限旋转轴。

int dBodyGetNumJoints (dBodyID);

返回连接到该实体的关节数量。

dJointID dBodyGetJoint (dBodyID, int index);

返回附加到该主体的关节,由 给出index。有效索引为 0 到n -1,其中n是 dBodyGetNumJoints 返回的值。

dWorldID dBodyGetWorld (dBodyID);

检索附加到给定主体的世界。

void dBodySetGravityMode (dBodyID b, int mode);
int dBodyGetGravityMode (dBodyID b);

设置/获取身体是否受到世界重力的影响。如果mode非零,则为,如果mode为零,则不是。新创造的物体总是受到世界引力的影响。

dGeomID dBodyGetFirstGeom (dBodyID);
dGeomID dBodyGetNextGeom (dGeomID);

授予对与主体关联的所有几何图形的访问权限。使用 dBodyGetFirstGeom() 检索第一个几何图形,然后使用前一个几何图形作为参数调用 dBodyGetNextGeom() 来检索下一个几何图形。

7 关节类型和功能

7.1 创建和破坏关节

dJointID dJointCreateBall (dWorldID, dJointGroupID);
dJointID dJointCreateHinge (dWorldID, dJointGroupID);
dJointID dJointCreateSlider (dWorldID, dJointGroupID);
dJointID dJointCreateContact (dWorldID, dJointGroupID, const dContact *);
dJointID dJointCreateUniversal (dWorldID, dJointGroupID);
dJointID dJointCreateHinge2 (dWorldID, dJointGroupID);
dJointID dJointCreatePR (dWorldID, dJointGroupID);
dJointID dJointCreatePU (dWorldID, dJointGroupID);
dJointID dJointCreatePiston (dWorldID, dJointGroupID);
dJointID dJointCreateFixed (dWorldID, dJointGroupID);
dJointID dJointCreateAMotor (dWorldID, dJointGroupID);
dJointID dJointCreateLMotor (dWorldID, dJointGroupID);
dJointID dJointCreatePlane2D (dWorldID, dJointGroupID);
dJointID dJointCreateDBall (dWorldID, dJointGroupID);
dJointID dJointCreateDHinge (dWorldID, dJointGroupID);
dJointID dJointCreateTransmission (dWorldID, dJointGroupID);

创建给定类型的新关节。该关节最初处于“limbo”状态(即它对模拟没有影响),因为它不连接到任何物体。关节组ID为0,正常分配关节。如果它非零,则关节被分配到给定的关节组中。接触关节将使用给定的dContact结构进行初始化。

void dJointDestroy (dJointID);

摧毁一个关节,将其与所连接的身体断开并将其从世界中移除。但是,如果关节是组的成员,则此功能无效 - 要销毁该关节,必须清空或销毁该组。

dJointGroupID dJointGroupCreate (int max_size);

创建一个联合组。该max_size参数现在未使用,应设置为 0。保留它是为了向后兼容。

void dJointGroupDestroy (dJointGroupID);

消灭一个联合团体。关节组中的所有关节都将被破坏。

void dJointGroupEmpty (dJointGroupID);

清空联合组。关节组中的所有关节都会被破坏,但关节组本身不会被破坏。

7.2 各种关节功能

void dJointAttach (dJointID, dBodyID body1, dBodyID body2);

将关节​​连接到一些新主体上。如果关节已经连接,它将首先与旧主体分离。要将此关节仅附加到一个主体,请将 body1 或 body2 设置为零 - 零主体指的是静态环境。将两个主体设置为零会使关节进入“不稳定”状态,即它不会对模拟产生影响,但在重新连接时可能需要再次进行一些设置工作。

注意 :某些关节(如铰链 2)需要连接到两个主体才能工作。

void dJointEnable (dJointID);
void dJointDisable (dJointID);
int dJointIsEnabled (dJointID);

在模拟过程中,禁用的关节被完全忽略。禁用的关节不会丢失已经计算的信息,例如锚点和轴。

void dJointSetData (dJointID, void *data);
void *dJointGetData (dJointID);

获取和设置关节的用户数据指针。

int dJointGetType (dJointID);

获取关节的类型。将返回以下常量之一:

dJointTypeBall A ball-and-socket joint. 翻译
dJointTypeHinge A hinge joint. 铰链接头
dJointTypeSlider A slider joint. 滑块关节
dJointTypeContact A contact joint. 接触接头
dJointTypeUniversal A universal joint. 万向节
dJointTypeHinge2 A hinge-2 joint. 铰链 2 接头
dJointTypeFixed A fixed joint. 固定关节
dJointTypeAMotor An angular motor joint. 角电机接头
dJointTypeLMotor A linear motor joint. 直线电机关节
dJointTypePlane2D A Plane 2D joint. 平面 2D 关节
dJointTypePR A Prismatic-Rotoide joint. Prismatic-Rotoide 关节
dJointTypePU A Prismatic-Universal joint. 棱柱万向接头
dJointTypePiston A Piston joint. 活塞接头
dJointTypeDBall A Double Ball-And-Socket joint. 双球窝接头
dJointTypeDHinge A Double Hinge joint. 双铰链接头
dJointTypeTransmission A Transmission joint. 传动接头
dBodyID dJointGetBody (dJointID, int index);

返回该关节连接的实体。如果index为 0,将返回“第一个”主体,对应于body1[#func_dJointAttach dJointAttach] 的参数。如果index为 1,则将返回“第二个”主体,对应于body2dJointAttach 的参数。

如果这些返回的主体 ID 之一为零,则关节将另一个主体连接到静态环境。如果两个主体 ID 均为零,则关节处于“不稳定状态”并且对模拟没有影响。

int dAreConnected (dBodyID, dBodyID);

效用函数:如果两个物体通过关节连接在一起,则返回 1,否则返回 0。

int dAreConnectedExclusion (dBodyID, dBodyID, int joint_type);

效用函数:如果两个物体通过不具有 type 的关节连接在一起joint_type,则返回 1,否则返回 0。joint_type是一个dJointTypeXXX常量。这对于决定是否在两个物体之间添加接触关节很有用:如果它们已经通过非接触关节连接,那么添加接触可能不合适,但是可以在已经有接触的物体之间添加更多接触。

7.3 关节反馈

void dJointSetFeedback (dJointID, dJointFeedback *);
dJointFeedback *dJointGetFeedback (dJointID);

在世界时间步长期间,计算每个关节施加的力。这些力直接添加到连接的物体上,用户通常无法知道哪个关节贡献了多少力。

如果需要此信息,则用户必须分配一个dJointFeedback结构并将其指针传递给dJointSetFeedback()函数。反馈信息结构定义如下:

typedef struct dJointFeedback {
    
    
    dVector3 f1; // force that joint applies to body 1
    dVector3 t1; // torque that joint applies to body 1
    dVector3 f2; // force that joint applies to body 2
    dVector3 t2; // torque that joint applies to body 2
} dJointFeedback;

在时间步长期间,连接到关节的任何反馈结构都将填充关节的力和扭矩信息。该dJointGetFeedback()函数返回当前反馈结构指针,如果未使用则返回 0(这是默认值)。dJointSetFeedback()可以传递 0 以禁用该关节的反馈。请注意,如果设置了反馈结构,则无需使用dJointGetFeedback(),因为该结构是之前定义的,您可以在每个模拟步骤后检查它。仅当多段代码想要启用对特定关节的反馈时,吸气剂才有用;在这种情况下,您可以首先检查关节是否已经有反馈,而不是设置另一个反馈 - 这将禁用第一个反馈集。

7.3.1 返回值是否有效?

http://ode.org/wiki/index.php/Manual#Joint_Types_and_Functions

有关反馈结构中奇怪且不一致的力和扭矩的问题已报告到邮件列表(例如2010 年 7 月、2009 年 4 月 、2009 年 2 月、2008 年 9 月)。也请阅读2012 年 11 月的这段对话。

显然,“一旦你有一个或多或少复杂的系统,反馈可能会产生不适当的值。约束求解器并不关心力是否均匀分布,只要它们产生正确的结果(即,它们加起来为当它们全部施加到各自的物体上时,施加适当的力)”(参考文献)。

7.3.2 API设计笔记

要求用户执行这些结构的分配可能看起来很奇怪。为什么不将数据静态存储在每个关节中呢?原因是并不是所有的用户都会使用反馈信息,即使使用了也不是所有的关节都需要它。静态存储它会浪费内存,特别是当这个结构将来可能会增长以存储大量额外信息时。

为什么不让 ODE 根据用户的请求分配结构本身?原因是如果需要反馈,接触关节(每个时间步都会创建和销毁)将需要花费大量时间在内存分配上。让用户进行分配意味着可以提供更好的分配策略,例如简单地从固定数组中分配它们。

此 API 的替代方案是使用联合回调。这当然可行,但有一些问题。首先,回调往往会污染 API,有时需要用户进行不自然的扭曲才能将数据传输到正确的位置。其次,这会使 ODE 在步骤中间被更改(这会产生不良后果),并且必须采取某种防范措施或对其进行调试检查 - 这会使事情变得复杂。

7.4 关节参数设定功能

7.4.1 球窝

在这里插入图片描述

void dJointSetBallAnchor (dJointID, dReal x, dReal y, dReal z);

设置关节锚点。关节将尝试将每个身体上的这一点保持在一起。输入在世界坐标中指定。如果关节上没有附着任何物体,则此功能无效。

void dJointGetBallAnchor (dJointID, dVector3 result);

获取世界坐标中的关节锚点。这将返回主体 1 上的点。如果关节完全满足,这将与主体 2 上的点相同。

void dJointGetBallAnchor2 (dJointID, dVector3 result);

获取世界坐标中的关节锚点。这将返回主体 2 上的点。您可以将球窝关节视为试图保持 dJointGetBallAnchor() 和 dJointGetBallAnchor2() 的结果相同。如果关节完全满足,此函数将返回与 dJointGetBallAnchor 相同的值,且不超过舍入误差。dJointGetBallAnchor2 可以与 dJointGetBallAnchor 一起使用,以查看关节分开的距离。

7.4.2 合页

在这里插入图片描述
默认轴是:
轴 1:x=1、y=0、z=0

void dJointSetHingeAnchor (dJointID, dReal x, dReal y, dReal z);

设置铰链锚参数。关节将尝试将每个身体上的这一点保持在一起。输入在世界坐标中指定。如果关节上没有附着任何物体,则此功能无效。

void dJointSetHingeAxis (dJointID, dReal x, dReal y, dReal z);

设置铰链锚点和轴参数。如果关节上没有附着任何物体,则此功能无效。

void dJointGetHingeAnchor (dJointID, dVector3 result);

获取世界坐标中的关节锚点。这将返回主体 1 上的点。如果关节完全满足,这将与主体 2 上的点相同。

void dJointGetHingeAnchor2 (dJointID, dVector3 result);

获取世界坐标中的关节锚点。这将返回主体 2 上的点。如果完全满足关节要求,这将返回与 dJointGetHingeAnchor 相同的值。如果不是,这个值会略有不同。例如,这可以用来查看接头分开的距离。

void dJointGetHingeAxis (dJointID, dVector3 result);

获取铰链轴参数。

dReal dJointGetHingeAngle (dJointID);
dReal dJointGetHingeAngleRate (dJointID);

获取铰链角度和该值的时间导数。该角度是在两个物体之间或物体与静态环境之间测量的。角度将在 -pi…pi 之间。
设置铰链锚或轴后,将检查所连接主体的当前位置,该位置将为零角度。

7.4.3 滑块

在这里插入图片描述
默认轴为: Axis: x=1, y=0, z=0

void dJointSetSliderAxis (dJointID, dReal x, dReal y, dReal z);

设置滑块轴参数。

void dJointGetSliderAxis (dJointID, dVector3 result);

获取滑块轴参数。

dReal dJointGetSliderPosition (dJointID);
dReal dJointGetSliderPositionRate (dJointID);

获取滑块线性位置(即滑块的“延伸”)以及该值的时间导数。如果轴设置为从主体 1 指向主体 0,则随着 2 个主体之间的距离增加,位置和速率将为正值。

设置轴后,将检查附加实体的当前位置,该位置将是零位置。

7.4.4 通用的

在这里插入图片描述
万向节就像球窝接头一样,限制了额外的旋转自由度。给定主体 1 上的轴 1 和主体 2 上垂直于轴 1 的轴 2,它使它们保持垂直。换句话说,两个物体绕垂直于两个轴的方向的旋转将是相等的。

图中,两个身体通过十字连接在一起。轴 1 连接到主体 1,轴 2 连接到主体 2。十字使这些轴保持 90 度,因此如果您抓住主体 1 并扭转它,主体 2 也会扭转。

万向接头相当于铰链 2 接头,其中铰链 2 的轴彼此垂直,并且具有完美的刚性连接来代替悬架。

万向节出现在汽车中,发动机使轴(驱动轴)沿其自身的轴线旋转。有时您想改变轴的方向。问题是,如果您只是弯曲轴,那么弯曲后的零件将不会绕其自身的轴线旋转。因此,如果您在弯曲位置切割它并插入万向节,则可以使用约束迫使第二个轴旋转与第一个轴相同的角度。

该关节的另一个用途是将简单虚拟生物的手臂连接到其身体上。想象一个人伸直双臂。您可能希望手臂能够上下、前后移动,但不能绕其自身的轴旋转。

默认轴是:

  • 轴 1:x=1、y=0、z=0
  • 轴 2:x=0、y=1、z=0

请注意,由于两个轴必须垂直,因此尝试将第一个轴设置为第二个轴的值(反之亦然)将导致运行时出错,即使随后立即更改第二个轴也是如此。

万向节的功能如下:

void dJointSetUniversalAnchor (dJointID, dReal x, dReal y, dReal z);

设置关节锚点。关节将尝试将每个身体上的这一点保持在一起。输入在世界坐标中指定。如果关节上没有附着任何物体,则此功能无效。

void dJointSetUniversalAxis1 (dJointID, dReal x, dReal y, dReal z);
void dJointSetUniversalAxis2 (dJointID, dReal x, dReal y, dReal z);

设置通用锚点和轴参数。轴 1 和轴 2 应相互垂直。

void dJointGetUniversalAnchor (dJointID, dVector3 result);

获取世界坐标中的关节锚点。这将返回主体 1 上的点。如果关节完全满足,这将与主体 2 上的点相同。

void dJointGetUniversalAnchor2 (dJointID, dVector3 result);

获取世界坐标中的关节锚点。这将返回主体 2 上的点。您可以将万向节的球窝部分视为试图保持 dJointGetBallAnchor() 和 dJointGetBallAnchor2() 的结果相同。如果关节完全满足,此函数将返回与 dJointGetUniversalAnchor 相同的值,且不超过舍入误差。dJointGetUniversalAnchor2 可以与 dJointGetUniversalAnchor 一起使用,以查看关节分开的距离。

void dJointGetUniversalAxis1 (dJointID, dVector3 result);
void dJointGetUniversalAxis2 (dJointID, dVector3 result);

获取万向轴参数。

dReal dJointGetUniversalAngle1 (dJointID);
dReal dJointGetUniversalAngle2 (dJointID);
dReal dJointGetUniversalAngles (dJointID, dReal *angle1, dReal *angle2);
dReal dJointGetUniversalAngle1Rate (dJointID);
dReal dJointGetUniversalAngle2Rate (dJointID);

获取这些值的通用角度和时间导数。该角度是在身体和十字架之间或静态环境和十字架之间测量的。角度将在 -pi…pi 之间。

设置通用锚点或轴后,将检查附加实体的当前位置,该位置将为零角度。

7.4.5 铰链2

在这里插入图片描述
铰链2关节与串联连接的两个铰链相同,但铰链轴不同。上图所示的一个例子是汽车的方向盘,其中一个轴允许车轮转向,另一个轴允许车轮旋转。
铰链 2 关节具有一个锚点和两个铰链轴。轴 1 是相对于车身 1 指定的(如果车身 1 是底盘,则这将是转向轴)。轴 2 是相对于主体 2 指定的(如果主体 2 是车轮,则这将是车轮轴)。

轴 1 可以有关节限制和电机,轴 2 只能有电机。
轴 1 可以用作悬挂轴,即约束可以沿着该轴压缩。
轴 1 垂直于轴 2 的铰链 2 关节相当于带有附加悬架的万向节。
默认轴是:

  • 轴 1:x=1、y=0、z=0
  • 轴 2:x=0、y=1、z=0
void dJointSetHinge2Anchor (dJointID, dReal x, dReal y, dReal z);

设置铰链 2 锚点参数。关节将尝试将每个身体上的这一点保持在一起。输入在世界坐标中指定。如果关节上没有附着任何物体,则此功能无效。

void dJointSetHinge2Axis1 (dJointID, dReal x, dReal y, dReal z);
void dJointSetHinge2Axis2 (dJointID, dReal x, dReal y, dReal z);

设置轴参数。轴 1 和轴 2 不得位于同一条线上。

void dJointGetHinge2Anchor (dJointID, dVector3 result);

获取世界坐标中的关节锚点。这将返回主体 1 上的点。如果关节完全满足,这将与主体 2 上的点相同。

void dJointGetHinge2Anchor2 (dJointID, dVector3 result);

获取世界坐标中的关节锚点。这将返回主体 2 上的点。如果完全满足关节要求,这将返回与 dJointGetHinge2Anchor 相同的值。如果不是,这个值会略有不同。例如,这可以用来查看接头分开的距离。

void dJointGetHinge2Axis1 (dJointID, dVector3 result);
void dJointGetHinge2Axis2 (dJointID, dVector3 result);

获取铰链 2 轴参数。

dReal dJointGetHinge2Angle1 (dJointID);
dReal dJointGetHinge2Angle1Rate (dJointID);
dReal dJointGetHinge2Angle2Rate (dJointID);

注意:目前没有函数调用来获取第二个角度的值。
获取铰链 2 角度(围绕轴 1 和轴 2)以及这些值的时间导数。
设置锚点或轴后,将检查附加实体的当前位置,该位置将为零角度。

7.4.6 棱镜和旋转体 Prismatic and Rotoide

在这里插入图片描述
棱柱和旋转接头 (JointPR) 结合了滑块(棱柱)和铰链(旋转)。它可用于减少模拟中的主体数量。通常您无法将 2 个 ODE 关节连接在一起,它们之间需要一个实体。使用这种类型的接头的一个例子是创建液压活塞。

您可以通过设置该关节的锚点来设置 body2 相对于 rotoid 关节的位置。(至于铰链接头)。

创建关节时会计算偏移量,该偏移量是 body1 和 rotoid 关节之间的距离。检索关节的位置将为您提供相对于该偏移的位置。

第一个轴是棱柱轴。其参数通过 [#limit_motor_parameter dParamX] 标志访问。
第二个轴是旋转轴。可以使用 [#limit_motor_parameter dParamX2] 标志访问其参数。
默认轴是:

  • R 轴:x=1,y=0,z=0。
  • P 轴:x=0、y=1、z=0
void dJointSetPRAxis1 (dJointID, dReal x, dReal y, dReal z);
void dJointGetPRAxis1 (dJointID, dVector3 result);

设置/获取棱柱关节的轴。

void dJointSetPRAxis2 (dJointID, dReal x, dReal y, dReal z);
void dJointGetPRAxis2 (dJointID, dVector3 result);

设置/获取 旋转体 关节的轴。

void dJointSetPRAnchor (dJointID, dReal x, dReal y, dReal z);
void dJointGetPRAnchor (dJointID, dVector3 result);

设置 PR 锚参数。关节将尝试保持 body2 和 rotoid 动作之间的距离固定。输入在世界坐标中指定。如果关节上没有附加 body2,则此功能无效。

dReal dJointGetPRPosition (dJointID);

获取PR线性位置(即棱镜的延伸)

设置轴后,将检查 body1 和锚点的当前位置,该位置将是零位置。

该位置是 body1 和 rotoid 关节之间的“定向”长度。position = (Prismatic axis) dot_product [(body1 + offset) - (body2 + anchor2)]

重要提示: 2 轴不得平行。由于这是一个新接头,只有当 2 个接头彼此成 90 度时才进行全面测试。

7.4.7 棱柱形 - 通用

在这里插入图片描述
棱柱万向接头 (JointPU) 将 body1 和锚点之间的滑块(棱柱形)与锚点位置处的万向接头组合在一起。该关节提供 1 个平移自由度和 2 个旋转自由度。

通过组合滑块和万向接头可以达到相同的结果,但两个接头之间需要一个假体。该关节可用于减少模拟中的主体数量。

第一轴是第一通用轴。其参数通过 dParamX1 或 dParamX 标志访问(例如:dParamFMax1)。
第二轴是第二万向轴。可以使用 dParamX2 标志(例如:dParamFMax2)访问其参数。
第三轴是棱柱轴。可以使用 dParamX3 标志(例如:dParamFMax3)访问其参数。
默认轴是:

  • 轴 1:x=0、y=1、z=0。
  • 轴 2:x=0、y=0、z=1。
  • 轴 3:x=1、y=0、z=0。
dReal dJointGetPUPosition (dJointID);

获取PU线性位置(即棱柱的延伸)

设置锚点后,将检查 body1 和锚点的当前位置,该位置将为零位置 (initial_offset)(即 dJointGetPUPosition,其中 body1 相对于锚点位于该位置将返回 0.0)。

该位置是主体 1 和通用关节(锚)之间的“定向”长度。位置 = {(棱柱轴)dot_product [body1 - 锚点] } - 初始偏移量

dReal dJointGetPUPositionRate (dJointID);

获取位置的时间导数。

void dJointSetPUAnchor (dJointID, dReal x, dReal y, dReal z);
void dJointGetPUAnchor (dJointID, dVector3 result);

设置PU锚点参数。关节将尝试保持 body2 和万向关节之间的距离固定。输入在世界坐标中指定。如果关节上没有附加 body2,则此功能无效。

void dJointSetPUAnchorDelta (dJointID, dReal x, dReal y, dReal z, dReal dx, dReal dy, dReal dz);

设置 PU 锚点和每个主体的相对位置,就好像 body1 位于其当前位置 + [dx,dy,dy](世界坐标系中的 dx,dy,dx)。

这就像设置已经拉长或压缩的棱柱形部分的接头一样。

使用增量设置锚点后,如果调用函数 dJointGetPUPosition,答案将是: sqrt(dxdx + dydy + dz*dz) * Normalize[axis3 dot_product (dx,dy,dz)]

void dJointSetPUAxis1 (dJointID, dReal x, dReal y, dReal z);
void dJointGetPUAxis1 (dJointID, dVector3 result);
void dJointSetPUAxis2 (dJointID, dReal x, dReal y, dReal z);
void dJointGetPUAxis2 (dJointID, dVector3 result);

设置/获取万向轴参数。轴 1 和轴 2 应相互垂直。

void dJointSetPUAxis3 (dJointID, dReal x, dReal y, dReal z);
void dJointGetPUAxis3 (dJointID, dVector3 result);

设置/获取棱柱关节的轴。

void dJointSetPUAxisP (dJointID, dReal x, dReal y, dReal z);
void dJointGetPUAxisP (dJointID, dVector3 result);

这是函数 dJointSetPUAxis3 的另一个名称

void dJointGetPUAngles (dJointID, dReal *angle1, dReal *angle2);
dReal dJointGetPUAngle1 (dJointID);
dReal dJointGetPUAngle2 (dJointID);
dReal dJointGetPUAngle1Rate (dJointID);
dReal dJointGetPUAngle2Rate (dJointID);

获取这些值的通用角度和时间导数。该角度是在身体和十字架之间或静态环境和十字架之间测量的。角度将在 -pi…pi 之间。
设置通用锚点或轴后,将检查附加实体的当前位置,该位置将为零角度。

7.4.8 活塞

在这里插入图片描述
活塞接头与滑块接头类似,只是可以绕平移轴旋转。
默认轴为: Axis: x=1, y=0, z=0

void dJointSetPistonAnchor (dJointID, dReal x, dReal y, dReal z);
void dJointGetPistonAnchor (dJointID, dVector3 result);

设置活塞锚参数。关节将尝试保持该点相对于 body2 的距离固定。输入在世界坐标中指定。如果关节上没有附着任何物体,则此功能无效。

void dJointGetPistonAnchor2 (dJointID, dVector3 result);

获取世界坐标中的关节锚点。这将返回主体 2 上的点。如果完全满足关节要求,这将返回与 dJointGetPistonAnchor 相同的值。如果不是,这个值会略有不同。例如,这可以用来查看接头分开的距离。

void dJointSetPistonAxis (dJointID, dReal x, dReal y, dReal z);
void dJointGetPistonAxis (dJointID, dVector3 result);

设置/获取活塞轴参数。
设置活塞轴线后,将检查所附主体的当前位置,该位置将是零角度。

void dJointSetPistonAxisDelta (dJointID j, dReal x, dReal y, dReal z, dReal dx, dReal dy, dReal dz);
dReal dJointGetPistonPosition (dJointID);
dReal dJointGetPistonPositionRate (dJointID);

获取活塞线性位置(即棱柱体的延伸)

设置锚点后,将检查 body1 和锚点的当前位置,该位置将为零位置 (initial_offset)(即 dJointGetPUPosition,其中 body1 相对于锚点位于该位置将返回 0.0)。

该位置是 body1 和锚点之间的“定向”长度。位置 = {(棱柱轴)dot_product [body1 - 锚点] } - 初始偏移量

dReal dJointGetPistonAngle (dJointID);
dReal dJointGetPistonAngleRate (dJointID);

获取该值的角度和时间导数。该角度是在两个物体之间或物体与静态环境之间测量的。角度将在 -pi…pi 之间。唯一可能的旋转轴是 dJointSetPistonAxis 定义的轴。

当设置活塞锚或轴时,将检查所附主体的当前位置,并且该位置将是零角度。

void dJointAddPistonForce (dJointID j, dReal force)

该函数在活塞内部产生一个力,该力将施加在两个物体上。

然后将力施加到每个物体的质心,并且力矢量的方向沿着活塞的轴线。

7.4.9 固定的

固定关节维持两个物体之间或物体与静态环境之间固定的相对位置和方向。如果您在使用此关节时遇到问题,请尝试将两个对象组合成一个实体。

void dJointSetFixed (dJointID);

连接固定关节后,在固定关节上调用此方法,以记住当前所需的相对偏移和主体之间所需的相对旋转。

7.4.10 接触

在这里插入图片描述
接触接头防止主体1和主体2在接触点处相互穿透。它通过仅允许物体在接触法线方向上具有“传出”速度来实现此目的。接触接头的寿命通常为一个时间步长。它们是为了响应碰撞检测而创建和删除的。

接触接头可以通过在垂直于法线的两个摩擦方向上施加特殊力来模拟接触处的摩擦。

创建接触接头时,dContact必须提供结构。这有以下定义:

struct dContact {
    
    
     dSurfaceParameters surface;
     dContactGeom geom;
     dVector3 fdir1;
};
  • geom是由碰撞函数设置的子结构。它在碰撞部分进行了描述。
  • fdir1 是定义摩擦力施加方向的“第一摩擦方向”矢量。它必须具有单位长度并垂直于接触法线(因此它通常与接触面相切)。仅当在 中设置了 dContactFDir1 标志时才应定义它surface.mode。“第二摩擦方向”是计算为垂直于接触法线 和 的矢量fdir1。
  • surface 是由用户设置的子结构。其成员定义碰撞表面的属性。它有以下定义:
struct dSurfaceParameters {
    
    
     int mode;
     dReal mu;
     dReal mu2;
     dReal rho;
     dReal rho2;
     dReal rhoN;
     dReal bounce;
     dReal bounce_vel;
     dReal soft_erp;
     dReal soft_cfm;
     dReal motion1, motion2, motionN;
     dReal slip1, slip2;
};
  • mode- 接触标志。必须始终设置此项。这是以下一个或多个标志的组合:
dContactMu2 If not set, use mu for both friction directions. If set, use mu for friction direction 1, use mu2 for friction direction 2.
dContactFDir1 If set, take fdir1 as friction direction 1, otherwise automatically compute friction direction 1 to be perpendicular to the contact normal (in which case its resulting orientation is unpredictable).
dContactBounce If set, the contact surface is bouncy, in other words the bodies will bounce off each other. The exact amount of bouncyness is controlled by the bounce parameter.
dContactSoftERP If set, the error reduction parameter of the contact normal can be set with the soft_erp parameter. This is useful to make surfaces soft.
dContactSoftCFM If set, the constraint force mixing parameter of the contact normal can be set with the soft_cfm parameter. This is useful to make surfaces soft.
dContactMotion1 If set, the contact surface is assumed to be moving independently of the motion of the bodies. This is kind of like a conveyor belt running over the surface. When this flag is set, motion1 defines the surface velocity in friction direction 1.
dContactMotion2 The same thing as above, but for friction direction 2.
dContactMotionN The same thing as above, but along the contact normal.
dContactSlip1 Force-dependent-slip (FDS) in friction direction 1.
dContactSlip2 Force-dependent-slip (FDS) in friction direction 2.
dContactRolling Enables rolling/spinning friction.
dContactApprox1_1 Use the friction pyramid approximation for friction direction 1. If this is not specified then the constant-force-limit approximation is used (and mu is a force limit).
dContactApprox1_2 Use the friction pyramid approximation for friction direction 2. If this is not specified then the constant-force-limit approximation is used (and mu is a force limit).
dContactApprox1_N Use the friction pyramid approximation for spinning (rolling around normal).
dContactApprox1 Equivalent to dContactApprox1_1, dContactApprox1_2 and dContactApprox1_N.
  • mu :库仑摩擦系数。该值必须在 0 到 的范围内dInfinity。0 导致无摩擦接触,并dInfinity导致永不打滑的接触。请注意,无摩擦接触比有摩擦的接触消耗的计算时间更少,并且无限摩擦接触比有限摩擦接触更便宜。必须始终设置此项。
  • mu2 :摩擦方向 2 的可选库仑摩擦系数 (0… dInfinity)。仅当在 中设置了相应的标志时才使用此选项mode。
  • rho:绕方向 1 的滚动摩擦系数。
  • rho2:绕方向 2 的滚动摩擦系数。
  • rhoN:法线方向的滚动摩擦系数。
  • bounce :恢复参数(0…1)。0 表示表面根本没有弹性,1 表示最大弹性。仅当在 中设置了相应的标志时才使用此选项mode。
  • bounce_vel :弹跳所需的最小传入速度。低于此值的传入速度实际上将具有 0 的反弹参数。仅当在 中设置了相应的标志时才使用此值mode。
  • soft_erp :联系正常的“软度”参数。仅当在 中设置了相应的标志时才使用此选项mode。
  • soft_cfm :联系正常的“柔软度”参数。仅当在 中设置了相应的标志时才使用此选项mode。
  • Motion1、Motion2、MotionN :摩擦方向 1 和 2 以及沿法线的表面速度。仅当在 中设置了相应的标志时才使用这些mode。
  • slip1 , slip2 :摩擦方向 1 和 2 的力相关滑动 (FDS) 系数。仅当在 中设置了相应的标志时才使用这些系数mode。

FDS 是一种效应,它会导致接触表面以与切向施加到该表面的力成比例的速度相互交叉。

考虑摩擦系数 mu 为无穷大的接触点。通常,如果对两个接触表面施加力,试图让它们彼此滑过,它们将不会移动。然而,如果 FDS 系数设置为正值 k,则表面将相互滑动,则表面将彼此滑动,形成相对于彼此的稳定速度kf。

请注意,这与正常的摩擦效应有很大不同:力不会导致表面相对于彼此 恒定的加速度- 它会导致短暂的加速度以达到稳定的速度。

这对于对某些情况(特别是轮胎)进行建模非常有用。例如,考虑一辆在道路上静止的汽车。沿行驶方向推动汽车将使其开始移动(即轮胎开始滚动)。沿垂直方向推动汽车不会产生任何效果,因为轮胎不会沿该方向滚动。然而,如果汽车以速度 v 移动,则在垂直方向施加力 f 将导致轮胎以一定速度在道路上打滑成正比(是的,这确实发生了)。

要在 ODE 中对此进行建模,请按如下方式设置轮胎-路面接触参数:将摩擦方向 1 设置为轮胎滚动方向,并将摩擦方向 2 上的 FDS 滑移系数设置为 kv, 其中 v 是轮胎滚动速度,k 为根据实验选择的轮胎参数。

请注意,FDS 与库仑摩擦的粘着/滑动效应完全不同 - 两种模式可以在单个接触点处一起使用。

7.4.11 角电机

角电机 (AMotor) 可以控制两个物体的相对角速度。最多可以在三个轴上控制角速度,允许将力矩电机和挡块设置为绕这些轴旋转(请参阅下面的“挡块和电机参数”部分)。这主要与意志球接头(完全不限制角度自由度)结合使用,但它可以用于需要角度控制的任何情况。要使用带有球窝接头的 AMotor,只需将其连接到与球窝接头相同的两个主体上。

AMotor 可以在不同的模式下使用。在dAMotorUser模式下,用户直接设置 AMotor 控制的轴。在dAMotorEuler模式下,AMotor 计算与相对旋转相对应的欧拉角,从而允许设置欧拉角扭矩电机和停止装置。具有欧拉角的 AMotor 接头如图 11 所示。
在这里插入图片描述
在该图中, a 0 a_0 a0 a 1 a_1 a1 a 2 a_2 a2 是控制角运动的三个轴。绿色轴(包括 a 0 a_0 a0 )锚定到主体 1。蓝色轴( a 2 a_2 a2 )锚定到主体2。要从主体 1 轴获取主体 2 轴,请执行以下旋转顺序:

围绕 a 0 a_0 a0 旋转 t h e t a 0 theta_0 theta0
围绕 a 1 a_1 a1 旋转 t h e t a 1 theta_1 theta1
围绕 a 1 a_1 a1 旋转 t h e t a 1 theta_1 theta1 a 1 a_1 a1 已从其原始位置旋转)。
围绕 a 2 a_2 a2 旋转 t h e t a 2 theta_2 theta2 a 2 a_2 a2 已从其原始位置旋转两次)。
使用欧拉角时有一个重要的限制: t h e t a 1 theta_1 theta1 角不得超出范围 - pi /2 … pi /2。如果发生这种情况,AMotor 关节将变得不稳定(+/- pi /2处存在奇点)。因此,您必须在轴号 1 上设置适当的停止点。

void dJointSetAMotorMode (dJointID, int mode);
int dJointGetAMotorMode (dJointID);

设置(和获取)角电机模式。该mode参数必须是以下常量之一:

dAMotorUser The AMotor axes and joint angle settings are entirely controlled by the user. This is the default mode.
AMotor 轴和关节角度设置完全由用户控制。这是默认模式。
dAMotorEuler Euler angles are automatically computed. The axis a1 is also automatically computed. The AMotor axes must be set correctly when in this mode, as described below. When this mode is initially set the current relative orientations of the bodies will correspond to all euler angles at zero.
欧拉角是自动计算的。轴 a 1 a_1 a1 也会自动计算。在此模式下,必须正确设置 AMotor 轴,如下所述。当最初设置此模式时,物体的当前相对方向将对应于所有为零的欧拉角。
void dJointSetAMotorNumAxes (dJointID, int num);
int dJointGetAMotorNumAxes (dJointID);

设置(和获取)将由 AMotor 控制的角轴数量。参数的num范围可以从 0(有效地停用关节)到 3。这在dAMotorEuler模式下自动设置为 3。

void dJointSetAMotorAxis (dJointID, int anum, int rel, dReal x, dReal y, dReal z);
void dJointGetAMotorAxis (dJointID, int anum, dVector3 result);
int dJointGetAMotorAxisRel (dJointID, int anum);

设置(和获取)AMotor 轴。参数anum选择要更改的轴(0,1 或 2)。每个轴可以具有三种“相对方向”模式之一,通过以下方式选择rel:

  • 0:轴锚定到全局框架。
  • 1:轴锚定到第一个主体。
  • 2:轴固定在第二个主体上。

无论 的设置如何,轴矢量 ( x, y, z) 始终在全局坐标中指定rel。有两个GetAMotorAxis函数,一是返回轴,一是返回相对模式。

对于dAMotorEuler模式:

  • 只需要设置轴 0 和轴 2。轴 1 将在每个时间步自动确定。
  • 轴 0 和 2 必须相互垂直。
  • 轴 0 必须锚定到第一个主体,轴 2 必须锚定到第二个主体。
void dJointSetAMotorAngle (dJointID, int anum, dReal angle);

告诉 AMotor 当前沿 axis 的角度是多少anum。该函数只能在 模式下调用dAMotorUser,因为在此模式下 AMotor 没有其他方法来了解关节角度。如果沿轴设置了停止点,则需要角度信息,但轴电机不需要角度信息。

dReal dJointGetAMotorAngle (dJointID, int anum);

返回 axis 的当前角度anum。在dAMotorUser模式下,这只是使用 dJointSetAMotorAngle 设置的值。在dAMotorEuler模式中,这是相应的欧拉角。

dReal dJointGetAMotorAngleRate (dJointID, int anum);

返回 axis 的当前角速率anum。在dAMotorUser模式下,该值始终为零,因为没有足够的信息可用。在dAMotorEuler模式中,这是相应的欧拉角速率。

7.4.12 直线电机

线性电机 (LMotor) 可以控制两个物体的相对线速度。线速度最多可以在三个轴上进行控制,允许将扭矩电机和挡块设置为沿这些轴平移(请参阅下面的“挡块和电机参数”部分)。

void dJointSetLMotorNumAxes (dJointID, int num);
int dJointGetLMotorNumAxes (dJointID);

设置(和获取)LMotor 将控制的轴数。参数的num范围可以从 0(有效地停用关节)到 3。

void dJointSetLMotorAxis (dJointID, int anum, int rel, dReal x, dReal y, dReal z);
void dJointGetLMotorAxis (dJointID, int anum, dVector3 result);

设置(和获取)LMotor 轴。参数anum选择要更改的轴(0,1 或 2)。每个轴可以具有三种“相对方向”模式之一,通过以下方式选择rel:

  • 0:轴锚定到全局框架。
  • 1:轴锚定到第一个主体。
  • 2:轴固定在第二个主体上。
    无论 的设置如何,轴矢量 ( x, y, z) 始终在全局坐标中指定rel。

7.4.13 平面 2D

平面 2d 关节作用于主体并将其约束到 Z == 0 平面。由于数值不准确,仍然可能会发生一些漂移,因此必须在每帧之后使用如下代码重置主体:

const dReal     *rot = dBodyGetAngularVel (my_body.id()));
const dReal     *quat_ptr;
dReal           quat[4],
                quat_len;
quat_ptr = dBodyGetQuaternion (my_body.id()));
quat[0] = quat_ptr[0];
quat[1] = 0;
quat[2] = 0; 
quat[3] = quat_ptr[3]; 
quat_len = sqrt (quat[0] * quat[0] + quat[3] * quat[3]);
quat[0] /= quat_len;
quat[3] /= quat_len;
dBodySetQuaternion (my_body.id()), quat);
dBodySetAngularVel (my_body.id()), 0, 0, rot[2]);

7.4.14 双球窝

双球接头(也称为“距离接头”)使两个锚点(每个主体各一个)保持固定距离。该距离是通过 期间锚点的相对位置推导出来的dJointAttach(),也可以稍后手动设置。

TODO:添加图片

void dJointSetDBallAnchor1(dJointID, dReal x, dReal y, dReal z);
void dJointSetDBallAnchor2(dJointID, dReal x, dReal y, dReal z);
void dJointGetDBallAnchor1(dJointID, dVector3 result);
void dJointGetDBallAnchor2(dJointID, dVector3 result);

设置并获取锚点。锚1连接到主体1,锚2连接到主体2。

dReal dJointGetDBallDistance(dJointID);

返回锚点之间的目标距离。由于关节误差,锚点之间的实际距离可能会有所不同。

void dJointSetDBallDistance(dJointID, dReal dist);

手动设置关节将尝试保持的目标距离。这会覆盖 期间自动计算的值dJointAttach()。

7.4.15 双铰链

双铰链关节类似于 Hinge2 关节,但两个轴相同。

TODO:添加图片

void dJointSetDHingeAxis(dJointID, dReal x, dReal y, dReal z);
void dJointGetDHingeAxis(dJointID, dVector3 result);

设置并获取两个铰链的轴。

void dJointSetDHingeAnchor1(dJointID, dReal x, dReal y, dReal z);
void dJointSetDHingeAnchor2(dJointID, dReal x, dReal y, dReal z);
void dJointGetDHingeAnchor1(dJointID, dVector3 result);
void dJointGetDHingeAnchor2(dJointID, dVector3 result);

设置并获取锚点。锚1连接到主体1,锚2连接到主体2。

dReal dJointGetDHingeDistance(dJointID);

返回锚点之间的距离。

7.4.16 传动

传动接头将扭矩从一个物体传递到另一个物体。它以 3 种模式工作:

  • 相交轴:模拟相交轴锥齿轮
  • 平行轴:模拟平行轴齿轮
  • 链传动:模拟链条和链轮
    TODO:这里需要一张图片
void dJointSetTransmissionMode( dJointID j, int mode );
int dJointGetTransmissionMode( dJointID j );

设置和获取传动接头的模式。模式可以是以下之一:

  • d传输相交轴
  • d传输平行轴
  • d传动链传动
void dJointGetTransmissionContactPoint1(dJointID, dVector3 result);
void dJointGetTransmissionContactPoint2(dJointID, dVector3 result);

获取第一轮和第二轮的接触点。

void dJointSetTransmissionAxis1(dJointID, dReal x, dReal y, dReal z);
void dJointSetTransmissionAxis2(dJointID, dReal x, dReal y, dReal z);
void dJointGetTransmissionAxis1(dJointID, dVector3 result);
void dJointGetTransmissionAxis2(dJointID, dVector3 result);

相交轴模式:设置和获取第一个和第二个实体的轴。

void dJointSetTransmissionAnchor1(dJointID, dReal x, dReal y, dReal z);
void dJointSetTransmissionAnchor2(dJointID, dReal x, dReal y, dReal z);
void dJointGetTransmissionAnchor1(dJointID, dVector3 result);
void dJointGetTransmissionAnchor2(dJointID, dVector3 result);

设置并获取每个主体的锚点。

void dJointSetTransmissionRatio( dJointID j, dReal ratio );

仅平行轴模式:设置齿轮之间的角速度比。对于其他模式,该比率是根据交叉角或车轮半径得出的。

dReal dJointGetTransmissionRatio( dJointID j );

获取齿轮之间的角速度比。

void dJointSetTransmissionAxis( dJointID j, dReal x, dReal y, dReal z );
void dJointGetTransmissionAxis( dJointID j, dVector3 result );

仅平行轴和链传动模式。设置并获取两个轮子使用的轴。

dReal dJointGetTransmissionAngle1( dJointID j );
dReal dJointGetTransmissionAngle2( dJointID j );

获取相位,即第一轮和第二轮的转动角度。

dReal dJointGetTransmissionRadius1( dJointID j );
dReal dJointGetTransmissionRadius2( dJointID j );

获取关节每个轮子的半径。

void dJointSetTransmissionRadius1( dJointID j, dReal radius );
void dJointSetTransmissionRadius2( dJointID j, dReal radius );

仅链传动模式:设置每个轮子的半径。半径在其他模式中是隐含的。

void dJointSetTransmissionBacklash( dJointID j, dReal backlash );
dReal dJointGetTransmissionBacklash( dJointID j );

设置并获取接头的间隙。

齿隙是变速器车轮啮合中的间隙,定义为几何接触点在车轮之间没有任何实际接触或动力传递的情况下可以移动的最大距离。通过除以车轮的半径,可以将其转换为每个车轮的旋转弧度。

为了进一步说明这一点,考虑一个半径为 r 1 r_1 r1 的车轮驱动另一个半径为 r 2 r_2 r2 的车轮的情况,并且在它们的网格中有等于 b 的反弹量。如果主动轮立即停止,将没有接触,因此,从动轮将继续转动另一个 b / r 2 b/r_2 b/r2 弧度,直到所有的反弹在网格被采取和接触恢复。因此,反冲以长度单位给出。

7.5 一般的

关节几何参数设置函数只能在关节附加到物体上并且这些物体已经正确定位后调用,否则关节可能无法正确初始化。如果尚未连接关节,这些功能将不起作用。

对于参数获取函数,如果系统未对齐(即存在一些关节错误),则锚点/轴值仅相对于主体 1(或主体 2,如果您在 dJointAttach 中将主体 1 指定为零)是正确的功能)。

所有关节的默认锚点是 (0,0,0)。所有关节的默认轴是 (1,0,0)。

设置轴后,它将被标准化为单位长度。调整后的轴是轴获取函数将返回的轴。

当测量关节角度或位置时,零值对应于物体相对于彼此的初始位置。

请注意,没有直接设置关节角度或位置(或其速率)的函数,而是必须设置相应的身体位置和速度。

7.6 停止和电机参数

当关节第一次创建时,没有什么可以阻止它在整个运动范围内移动。例如,铰链将能够移动整个角度,滑块将滑动到任意长度。

可以通过在关节上设置挡块来限制该运动范围。将防止关节角度(或位置)低于低停止值或高于高停止值。请注意,零关节角度(或位置)对应于初始身体位置。

除了止动件之外,许多关节类型都可以配备电机。电机向关节的自由度施加扭矩(或力),使其以所需的速度枢转(或滑动)。电机具有力限制,这意味着它们向关节施加的力/扭矩不能超过给定的最大力/扭矩。

电机有两个参数:所需速度和达到该速度所需的最大力。这是现实生活中电机、发动机或伺服系统的一个非常简单的模型。然而,在对连接到关节之前通过变速箱减速的电机(或发动机或伺服系统)进行建模时,它非常有用吗?此类设备通常通过设置所需速度来控制,并且只能产生最大量的功率来实现该速度(这对应于关节处可用的一定量的力)。

电机还可用于精确模拟关节中的干(或库仑)摩擦。只需将所需速度设置为零并将最大力设置为某个恒定值 - 然后所有关节运动都将受到该力的阻碍。

使用关节止动件和电机的替代方法是简单地自己向受影响的身体施加力。施加电机力很容易,并且可以通过约束弹簧力来模拟关节止动件。然而,直接施加力通常不是一个好方法,如果不小心操作,可能会导致严重的稳定性问题。

考虑向物体施加力以达到所需速度的情况。要计算这个力 F ,您可以使用有关当前速度的信息,如下所示:
F = k ( d e s i r e d s p e e d − c u r r e n t s p e e d ) F=k(desired speed−current speed) F=k(desiredspeedcurrentspeed)
F = k (所需速度 − 当前速度) F= k (所需速度-当前速度) F=k(所需速度当前速度)
这有几个问题。首先,参数 k 必须手工调优。如果它太低,身体将需要很长时间才能达到速度。如果它太高,模拟将变得不稳定。其次,即使 k 选得很好,身体仍然需要一些时间步骤来达到速度。第三,如果任何其他“外部”力被施加到物体上,期望的速度甚至可能永远不会达到(需要一个更复杂的力方程,这将有额外的参数和它自己的问题)。

关节马达解决了所有这些问题:它们使身体在一个时间步内加速,前提是不需要比允许的更多的力量。关节电机不需要额外的参数,因为它们实际上是作为约束实现的。他们可以有效地看到一个时间步骤到未来,以计算出正确的力量。这使得关节马达的计算成本比自己计算力要高,但它们更加健壮和稳定,而且设计时花费的时间要少得多。对于较大的刚体系统尤其如此。

类似的论点也适用于关节停止。

7.6.1 参数功能

以下是在关节上设置停止和电机参数(以及其他类型的参数)的函数:

void dJointSetHingeParam (dJointID, int parameter, dReal value);
void dJointSetSliderParam (dJointID, int parameter, dReal value);
void dJointSetHinge2Param (dJointID, int parameter, dReal value);
void dJointSetUniversalParam (dJointID, int parameter, dReal value);
void dJointSetAMotorParam (dJointID, int parameter, dReal value);
void dJointSetLMotorParam (dJointID, int parameter, dReal value);
void dJointSetPRParam (dJointID, int parameter, dReal value);
void dJointSetPUParam (dJointID, int parameter, dReal value);
void dJointSetPistonParam (dJointID, int parameter, dReal value);
void dJointSetDBallParam(dJointID, int parameter, dReal value);
void dJointSetDHingeParam(dJointID, int parameter, dReal value);
void dJointSetTransmissionParam(dJointID, int parameter, dReal value); 
dReal dJointGetHingeParam (dJointID, int parameter);
dReal dJointGetSliderParam (dJointID, int parameter);
dReal dJointGetHinge2Param (dJointID, int parameter);
dReal dJointGetUniversalParam (dJointID, int parameter);
dReal dJointGetAMotorParam (dJointID, int parameter);
dReal dJointGetLMotorParam (dJointID, int parameter);
dReal dJointGetPRParam (dJointID, int parameter);
dReal dJointGetPUParam (dJointID, int parameter);
dReal dJointGetPistonParam (dJointID, int parameter);
dReal dJointGetDBallParam(dJointID, int parameter);
dReal dJointGetDHingeParam(dJointID, int parameter);
dReal dJointGetTransmissionParam(dJointID, int parameter);

设置/获取每种关节类型的限制/电机参数。参数编号为:

dParamLoStop Low stop angle or position. Setting this to -dInfinity (the default value) turns off the low stop. For rotational joints, this stop must be greater than - pi to be effective.
dParamHiStop High stop angle or position. Setting this to dInfinity (the default value) turns off the high stop. For rotational joints, this stop must be less than pi to be effective. If the high stop is less than the low stop then both stops will be ineffective.
dParamVel Desired motor velocity (this will be an angular or linear velocity).
dParamFMax The maximum force or torque that the motor will use to achieve the desired velocity. This must always be greater than or equal to zero. Setting this to zero (the default value) turns off the motor.
dParamFudgeFactor The current joint stop/motor implementation has a small problem: when the joint is at one stop and the motor is set to move it away from the stop, too much force may be applied for one time step, causing a jumping motion. This fudge factor is used to scale this excess force. It should have a value between zero and one (the default value). If the jumping motion is too visible in a joint, the value can be reduced. Making this value too small can prevent the motor from being able to move the joint away from a stop.
dParamBounce The bouncyness of the stops. This is a restitution parameter in the range 0…1. 0 means the stops are not bouncy at all, 1 means maximum bouncyness.
dParamCFM The constraint force mixing (CFM) value used when not at a stop.
dParamStopERP The error reduction parameter (ERP) used by the stops.
dParamStopCFM The constraint force mixing (CFM) value used by the stops. Together with the ERP value this can be used to get spongy or soft stops. Note that this is intended for unpowered joints, it does not really work as expected when a powered joint reaches its limit.
dParamSuspensionERP Suspension error reduction parameter (ERP). Currently this is only implemented on the hinge-2 joint.
dParamSuspensionCFM Suspension constraint force mixing (CFM) value. Currently this is only implemented on the hinge-2 joint.

如果给定关节未实现特定参数,则设置该参数将无效,获取该参数将返回 0。

重要提示:这些参数名称后面可以选择跟随一个数字(2 或3)来指示第二组或第三组参数,例如用于铰链2 关节中的第二轴或AMotor 关节中的第三轴。常数dParamGroup也定义为:
d P a r a m X i = d P a r a m X + d P a r a m G r o u p ∗ ( i − 1 ) dParamX i = dParamX+dParamGroup* ( i -1) dParamXi=dParamX+dParamGroup(i1)
下表显示了每种关节类型的有效参数组。

Joint Type Number of Param Groups
Ball And Socket 0
Hinge 1
Slider 1
Contact 0
Universal 2
Fixed 0
Angular Motor 3
Linear Motor 3
Plane2D 3
Rotoide and Prismatic 0

7.7 直接设置关节扭矩/力

电机(见上文)允许您直接设置关节速度。但是,您可能希望在关节处设置扭矩或力。这些函数就是这样做的。请注意,它们不会影响电机,而只需在与其相连的实体上调用 dBodyAddForce dBodyAddTorque 即可。

void dJointAddHingeTorque (dJointID, dReal torque);

围绕铰链轴施加扭矩。torque也就是说,它在铰链轴方向上向主体 1 施加大小为 的扭矩,向主体 2 施加大小相同但方向相反的扭矩。该函数只是 dBodyAddTorque 的包装。

void dJointAddUniversalTorques (dJointID, dReal torque1, dReal torque2);

适用torque1于通用轴 1 和torque2通用轴 2。此函数只是 dBodyAddTorque 的包装。

void dJointAddSliderForce (dJointID, dReal force);

沿滑块方向施加给定的力。也就是说,它在滑块的轴方向上向 body1 施加大小为 的力force,向 body2 施加大小相同但方向相反的力。该函数只是 dbodyAddForce 的包装。

void dJointAddHinge2Torques (dJointID, dReal torque1, dReal torque2);

适用torque1于铰链 2 的轴 1 和torque2铰链 2 的轴 2。此函数只是 dBodyAddTorque 的包装。

void dJointAddAMotorTorques (dJointID, dReal torque0, dReal torque1, dReal torque2);

适用torque0于 AMotor 的轴 0、torque1AMotor 的轴 1 和torque2AMotor 的轴 2。如果电机的轴少于三个,则忽略较高的扭矩。该函数只是 dBodyAddTorque 的包装。

8 支持功能

8.1 初始化

void dInitODE ();
int dInitODE2 (unsigned InitFlags);
void dCloseODE ();

执行库初始化。

首次使用库之前必须调用dInitODE2() 。InitFlags可以为零或dInitFlagManualThreadCleanup。在调用dCloseODE()之前,它只能被调用一次。
dInitODE()是以下形式的简写:

dInitODE2(0);
dAllocateODEDataForThread(dAllocateMaskAll);

dCloseODE()执行库清理。之后,必须先调用dInitODE2(),然后才能再次使用 ODE。仅当您要从单个线程使用 ODE 时才使用它。

int dAllocateODEDataForThread (unsigned AllocateFlags);
void dCleanupODEAllDataForThread ();

必须从将使用 ODE 的每个线程调用dAllocateODEDataForThread() 。AllocateFlags可以为零或dAllocateFlagCollisionData。
仅当库是使用dInitFlagManualThreadCleanup初始化时才使用dCleanupODEAllDataForThread () 。

8.2 配置

const char* dGetConfiguration ();

以标记字符串的形式返回特定的 ODE 构建配置。该字符串可以按照与 OpenGL 扩展机制类似的方式进行解析,命名约定也应该很熟悉。报告了以下扩展:

ODE
ODE_single_precision
ODE_double_precision
ODE_EXT_no_debug
ODE_EXT_trimesh
ODE_EXT_opcode
ODE_EXT_gimpact
ODE_EXT_malloc_not_alloca
ODE_OPC_16bit_indices
ODE_OPC_new_collider
int dCheckConfiguration ( const char* token );

用于检查 ODE 配置字符串中标记的帮助程序。注意,此函数区分大小写。

8.3 旋转功能

刚体方向用单位四元数表示。它由四个数字组成,,其中: [ w , x , y, z]

  • w = c o s ( θ / 2 ) w=cos(θ/2) w=cos(θ/2)
  • ( x , y , z ) = μ ⃗ s i n ( θ / 2 ) (x,y,z)=\vec \mu sin(θ/2) (x,y,z)=μ sin(θ/2)
  • θ是旋转角度
  • μ ⃗ \vec \mu μ 是单位长度旋转轴。

每个刚体还有一个从四元数导出的 3x3 旋转矩阵;四元数对于动力学计算很方便,但矩阵更适合线性变换。旋转矩阵和四元数始终匹配。几何图形只有旋转矩阵,因为它们不涉及动力学。四元数是在库之间传输旋转的首选方法(例如,与 3D 引擎之间传输),因为只有 2 个约定([w, x, y, z]和[x, y, z, w])在交换时很容易注意到;同样,欧拉角也是不可取的,因为可能的约定太多,使得两个独立的实现不太可能匹配。

有关四元数的一些信息:

  • q 和-q 表示相同的旋转。
  • 四元数的倒数[w, x, y, z]是[w, -x, -y, -z]。

以下是处理旋转矩阵和四元数的实用函数。

void dRSetIdentity (dMatrix3 R);

设置R为单位矩阵(即不旋转)。

void dRFromAxisAndAngle (dMatrix3 R, dReal ax, dReal ay, dReal az, dReal angle);

计算旋转矩阵R作为沿轴(ax,ay,az)的角弧度旋转。

void dRFromEulerAngles (dMatrix3 R, dReal phi, dReal theta, dReal psi);

从三个欧拉旋转角度计算旋转矩阵R。

void dRFrom2Axes (dMatrix3 R, dReal ax, dReal ay, dReal az, dReal bx, dReal by, dReal bz);

从两个向量a = [ax, ay, az]和b = [bx, by, bz]计算旋转矩阵 R。A和b是旋转后的坐标系的x轴和y轴。如果有必要,两个向量将被归一化,并且b将被投影,使其垂直于a。所需的z轴是向量积 a × b a×b a×b

void dQSetIdentity (dQuaternion q);

设q为单位旋转(即不旋转)。

void dQFromAxisAndAngle (dQuaternion q, dReal ax, dReal ay, dReal az, dReal angle);

计算q为沿轴(ax,ay,az)旋转的角弧度。

void dQMultiply0 (dQuaternion qa, const dQuaternion qb, const dQuaternion qc);
void dQMultiply1 (dQuaternion qa, const dQuaternion qb, const dQuaternion qc);
void dQMultiply2 (dQuaternion qa, const dQuaternion qb, const dQuaternion qc);
void dQMultiply3 (dQuaternion qa, const dQuaternion qb, const dQuaternion qc);

设qa = qb*qc。这和qa =旋转qc,然后旋转qb是一样的。0/1/2/3版本使用参数的逆:0不反转任何东西,1使用qb的逆,2使用qc的逆。选项3使用两者的倒数。

void dQtoR (const dQuaternion q, dMatrix3 R);

将四元数q转换为旋转矩阵R。

void dRtoQ (const dMatrix3 R, dQuaternion q);

将旋转矩阵R转换为四元数q。

void dWtoDQ (const dVector3 w, const dQuaternion q, dVector4 dq);

给定一个现有的方向 q 和角速度矢量 w ,在dq中返回结果 d q / d t dq/dt dq/dt

8.4 质量功能

刚体的质量参数由dMass结构体描述:

typedef struct dMass {
    
    
    dReal mass; // total mass of the rigid body
    dVector3 c; // center of gravity position in body frame (x,y,z)
    dMatrix3 I; // 3x3 inertia tensor in body frame, about POR
} dMass;

以下函数在此结构上运行:

void dMassSetZero (dMass *);

将所有质量参数设置为零。

void dMassSetParameters (dMass *, dReal themass,
                         dReal cgx, dReal cgy, dReal cgz,
                         dReal I11, dReal I22, dReal I33,
                         dReal I12, dReal I13, dReal I23);

将质量参数设置为给定值。themass是身体的质量。( cx, cy, cz) 是车身框架中的重心位置。这些Ixx值是惯性矩阵的元素: ( I11 I12 I13 ) ( I12 I22 I23 ) ( I13 I23 I33 )

void dMassSetSphere (dMass *, dReal density, dReal radius);
void dMassSetSphereTotal (dMass *, dReal total_mass, dReal radius);

设置质量参数以表示给定半径和密度的球体,其质心相对于主体位于 (0,0,0)。第一个函数接受球体的密度,第二个函数接受球体的总质量。

void dMassSetCapsule (dMass *, dReal density, int direction, dReal radius, dReal length);
void dMassSetCapsuleTotal (dMass *, dReal total_mass, int direction, dReal radius, dReal length);

设置质量参数以表示给定参数和密度的胶囊,其质心相对于主体位于 (0,0,0)。圆柱体(和球冠)的半径为radius。圆柱体的长度(不包括球冠)为length。direction圆柱体的长轴根据(1=x, 2=y, 3=z)的值沿主体的 x、y 或 z 轴定向。第一个函数接受物体的密度,第二个函数接受物体的总质量。

void dMassSetCylinder (dMass *, dReal density, int direction, dReal radius, dReal length);
void dMassSetCylinderTotal (dMass *, dReal total_mass, int direction, dReal radius, dReal length);

设置质量参数以表示给定参数和密度的平端圆柱体,其质心相对于主体位于 (0,0,0)。圆柱体的半径为radius。圆柱体的长度为length。direction圆柱体的长轴根据(1=x, 2=y, 3=z)的值沿主体的 x、y 或 z 轴定向。第一个函数接受物体的密度,第二个函数接受物体的总质量。

void dMassSetBox (dMass *, dReal density, dReal lx, dReal ly, dReal lz);
void dMassSetBoxTotal (dMass *, dReal total_mass, dReal lx, dReal ly, dReal lz);

设置质量参数以表示给定尺寸和密度的盒子,其质心相对于主体位于 (0,0,0)。盒子沿 x、y 和 z 轴的边长分别为lx、ly和lz。第一个函数接受物体的密度,第二个函数接受物体的总质量。

void dMassSetTrimesh (dMass *, dReal density, dGeomID g)

设置质量参数以表示给定几何和密度的任意修剪网格、修剪网格几何质心的位置并设置惯性矩阵。

void dMassAdjust (dMass *, dReal newmass);

给定某个物体的质量参数,调整它们,使总质量现在为newmass。当使用上述函数设置某些对象的质量参数时,这非常有用 - 它们采用对象密度,而不是总质量。

void dMassTranslate (dMass *, dReal x, dReal y, dReal z);

给定某个物体的质量参数,调整它们以表示相对于物体框架位移(x,y,z)的物体。

void dMassRotate (dMass *, const dMatrix3 R);

给定某个物体的质量参数,调整它们以表示相对于物体框架旋转R的物体。

void dMassAdd (dMass *a, const dMass *b);

将质量添加b到质量中a。

8.5 数学函数

(其中有很多,但它们还没有足够标准化来记录)。

8.6 导出功能

用于打印世界状态的函数。

void dWorldExportDIF (dWorldID w, FILE *file, const char *prefix)

设置file为文件描述符以打印到文件或stdout打印到标准输出(控制台)。

8.7 错误和记忆功能

以下内容直接取自ode/error.h.11 版:
所有用户定义的错误函数都具有此类型。错误和调试函数不应返回:

typedef void dMessageFunction (int errnum, const char *msg, va_list ap);

设置新的错误、调试或警告处理程序。如果 fn 为 0,则使用默认处理程序:

void dSetErrorHandler (dMessageFunction *fn);
void dSetDebugHandler (dMessageFunction *fn);
void dSetMessageHandler (dMessageFunction *fn);

返回当前错误、调试或警告处理程序。如果返回值为 0,则默认处理程序就位:

dMessageFunction *dGetErrorHandler(void);
dMessageFunction *dGetDebugHandler(void);
dMessageFunction *dGetMessageHandler(void);

生成致命错误、调试陷阱或消息:

ODE_API void dError (int num, const char *msg, ...);
ODE_API void dDebug (int num, const char *msg, ...);
ODE_API void dMessage (int num, const char *msg, ...);

9 碰撞检测

ODE 有两个主要组件:动力学仿真引擎和碰撞检测引擎。碰撞引擎获得有关每个物体形状的信息。在每个时间步骤,它都会找出哪些物体相互接触,并将生成的接触点信息传递给用户。用户进而在物体之间创建接触关节。

使用 ODE 的碰撞检测是可选的——只要可以提供正确类型的联系信息,就可以使用替代碰撞检测系统。

9.1 支持碰撞测试

下面是一个表,列出了哪些原语与其他原语发生碰撞,按对象复杂性排序(信息取自碰撞_kernel.cpp->initColliders() 函数)。

Ray Plane Sphere Box Capsule Cylinder Trimesh Convex Heightfield
Ray No Yes Yes Yes Yes Yes Yes Yes Yes
Plane - No Yes Yes Yes Yes Yes Yes No
Sphere - - Yes Yes Yes Yes Yes Yes Yes
Box - - - Yes Yes Yes Yes Yes Yes
Capsule - - - - Yes Yes Yes Yes Yes
Cylinder - - - - - Yes Yes Yes Yes
Trimesh - - - - - - Yes No Yes
Convex - - - - - - - Yes Yes
Heightfield - - - - - - - - No

在这里插入图片描述
注1:此功能仅存在于最近的SVN版本中,尚未添加到正式版本中。
注 2:不打算实施,因为这种碰撞组合在物理模拟中没有多大意义。
注 3:通过启用libccd Colliders。

可以通过dSetColliderOverride()覆盖特定一对几何类的碰撞处理程序:

void dSetColliderOverride (int class1, int class2, dColliderFn *fn);

dColliderFn定义为:

typedef int dColliderFn (dGeomID o1, dGeomID o2, int flags, dContactGeom *contact, int skip);

9.2 联络点

如果两个物体接触,或者一个物体接触其环境中的静态特征,则该接触由一个或多个“接触点”表示。每个接触点都有对应的dContactGeom结构:

struct dContactGeom {
    
    
    dVector3 pos;    // contact position
    dVector3 normal; // normal vector
    dReal depth;     // penetration depth
    dGeomID g1,g2;   // the colliding geoms
};
  • pos 记录全局坐标中的接触位置。
  • depth 是两个物体相互渗透的深度。如果深度为零,则两个物体具有掠接触,即它们“仅”接触。然而,这种情况很少见——模拟并不完全准确,并且通常会使物体步得太远,以致深度不为零。
  • normal 是单位长度向量,一般来说,垂直于接触表面。
  • g1和g2 是碰撞的几何对象。

约定是,如果物体 1 沿着normal矢量移动一段距离depth(或者等效地,如果物体 2 在相反方向移动相同的距离),则接触深度将减小到零。这意味着法线向量“in”指向主体 1。

在现实生活中,两个物体之间的接触是一件复杂的事情。用接触点表示接触只是一个近似值。接触“补丁”或“表面”可能在物理上更准确,但在高速模拟软件中表示这些东西是一个挑战。

添加到模拟中的每个额外接触点都会进一步减慢速度,因此有时我们为了速度而被迫忽略接触点。例如,当两个盒子碰撞时,可能需要许多接触点来正确表示情况的几何形状,但我们可能选择只保留最好的三个。因此,我们在近似值之上叠加近似值。

然后可以使用接触点来例如生成接触接头。

9.3 几何学

几何对象(或简称“geom”)是碰撞系统中的基本对象。几何图形可以表示单个刚性形状(例如球体或盒子),也可以表示一组其他几何图形 - 这是一种特殊的几何图形,称为“空间”。

任何几何体都可以与任何其他几何体碰撞以产生零个或多个接触点。空间具有额外的能力,能够将其包含的几何体碰撞在一起以产生内部接触点。

几何对象可以是可放置的或不可放置的。可放置几何体具有位置向量和 3*3 旋转矩阵,就像刚体一样,可以在模拟过程中更改。不可放置的几何对象不具备此功能 - 例如,它可能表示环境中无法移动的某些静态特征。空间是不可放置的几何体,因为每个包含的几何体可能有自己的位置和方向,但空间本身具有位置和方向是没有意义的。

要在刚体模拟中使用碰撞引擎,可放置几何图形与刚体对象相关联。这使得碰撞引擎能够从物体中获取几何体的位置和方向。请注意,几何体与刚体的不同之处在于,几何体具有几何属性(大小、形状、位置和方向),但没有动力学属性(例如速度或质量)。body 和 geom 一起代表模拟对象的所有属性。

每个几何体都是一个类的实例,例如球体、平面或盒子。有许多内置类,如下所述,您也可以定义自己的类。

可放置几何体的参考点是由其位置向量控制的点。标准类的参考点通常对应于几何体的质心。此功能允许标准类轻松连接到动力学主体。如果需要其他参考点,可以使用变换对象来封装几何对象。

下面将描述适用于所有几何图形的概念和函数,然后是各种几何图形类以及操作它们的函数。

9.4 空间

空间是不可放置的几何图形,可以包含其他几何图形。它与“世界”的刚体概念类似,只不过它适用于碰撞而不是动力学。

空间物体的存在是为了使碰撞检测更快。如果没有空格,您可以通过调用 dCollide 来获取每对几何体的接触点,从而在模拟中生成接触点。对于 N 个几何对象,这是 O ( N 2 ) O(N^2) O(N2) 次测试,如果您的环境有许多对象,则计算成本太高。

更好的方法是将几何图形插入到空间中并调用 dSpaceCollide。然后,空间将执行碰撞剔除,这意味着它将快速识别哪些几何对象对可能相交。这些对将被传递给回调函数,该函数又可以对它们调用 dCollide。这节省了在无用的 dCollide 测试中花费的大量时间,因为传递给回调函数的对的数量将只是每个可能的对象-对象对的一小部分。

空格可以包含其他空格。这对于将碰撞环境划分为多个层次结构以进一步优化碰撞检测速度非常有用。这将在下面更详细地描述。

9.5 通用几何函数

以下函数可以应用于任何几何图形。

void dGeomDestroy (dGeomID);

摧毁一个几何体,首先将其从其所在的任何空间中移除。该函数会破坏任何类型的几何图形,但要创建几何图形,您必须调用该类型的创建函数。

当一个空间被销毁时,如果其清理模式为 1(默认),则该空间中的所有几何对象也会自动销毁。

void dGeomSetData (dGeomID, void *);
void * dGeomGetData (dGeomID);

这些函数设置并获取存储在 geom 中的用户定义的数据指针。

void dGeomSetBody (dGeomID, dBodyID);
dBodyID dGeomGetBody (dGeomID);

这些函数设置并获取与可放置几何图形关联的主体。在几何对象上设置主体会自动组合主体和几何对象的位置向量和旋转矩阵,因此设置其中一个对象的位置或方向将为两个对象设置值。

将主体 ID 设置为零可以使几何体拥有自己的位置和旋转,独立于任何主体。如果几何图形之前已连接到主体,则其新的独立位置/旋转将设置为主体的当前位置/旋转。

在不可放置的几何对象上调用这些函数会导致 ODE 调试版本中出现运行时错误。

void dGeomSetPosition (dGeomID, dReal x, dReal y, dReal z);
void dGeomSetRotation (dGeomID, const dMatrix3 R);
void dGeomSetQuaternion (dGeomID, const dQuaternion);

设置可放置几何体的位置向量、旋转矩阵或四元数。这些函数类似于 dBodySetPosition、dBodySetRotation 和 dBodySetQuaternion。如果几何对象附加到物体上,物体的位置/旋转/四元数也会改变。

在不可放置的几何对象上调用这些函数会导致 ODE 调试版本中出现运行时错误。

const dReal * dGeomGetPosition (dGeomID);
const dReal * dGeomGetRotation (dGeomID);
void dGeomGetQuaternion (dGeomID, dQuaternion result);

前两个返回指向几何位置向量和旋转矩阵的指针。返回的值是指向内部数据结构的指针,因此向量在对几何对象进行任何更改之前一直有效。如果geom附加到物体上,则将返回物体的位置/旋转指针,即结果将与调用dBodyGetPosition或dBodyGetRotation相同。

dGeomGetQuaternion 将几何体的四元数复制到提供的空间中。如果geom附加到物体上,则将返回物体的四元数,即得到的四元数将与调用dBodyGetQuaternion的结果相同。

在不可放置的几何对象上调用这些函数会导致 ODE 调试版本中出现运行时错误。

void dGeomSetOffsetPosition (dGeomID, dReal x, dReal y, dReal z);
void dGeomSetOffsetRotation (dGeomID, const dMatrix3 R);
void dGeomSetOffsetQuaternion (dGeomID, const dQuaternion Q);

设置几何的偏移位置、旋转或四元数。geom 必须附加到实体上。如果几何图形没有偏移,则会自动创建。这为几何体设置了一个额外的(局部)转换,因为附加到主体的几何体共享它们的全局位置和旋转。要禁用偏移,请调用 dGeomClearOffset。

void dGeomSetOffsetWorldPosition (dGeomID, dReal x, dReal y, dReal z);
void dGeomSetOffsetWorldRotation (dGeomID, const dMatrix3 R);
void dGeomSetOffsetWorldQuaternion (dGeomID, const dQuaternion Q);

设置几何的偏移世界位置、旋转或四元数。新的局部偏移量是当前主体变换的差异,以便几何图形按照指定的方式在世界中放置和定向,而不改变主体的变换。另请参阅前面的三个偏移函数。

const dReal * dGeomGetOffsetPosition (dGeomID);
const dReal * dGeomGetOffsetRotation (dGeomID);
void dGeomGetOffsetQuaternion (dGeomID, dQuaternion result);

获取几何的偏移位置、旋转或四元数。前两个函数的返回值是指向 geom 内部数据结构的指针。它们在对几何对象进行任何更改之前一直有效。如果 geom 没有偏移,则返回零向量,如果是 dGeomGetOffsetQuaternion,则返回恒等四元数。

void dGeomClearOffset (dGeomID);

禁用几何体的偏移。几何图形将根据身体的位置/方向重新定位/定向。如果几何对象没有偏移量,则该函数不执行任何操作。请注意,这将消除偏移,并且比为恒等变换设置偏移更有效。

void dGeomGetAABB (dGeomID, dReal aabb[6]);

返回aabb围绕给定几何图形的轴对齐边界框。该aabb数组包含元素(minx、maxx、miny、maxy、minz、maxz)。如果几何对象是空间,则返回包围所有包含的几何对象的边界框。

如果该函数可以确定自上次计算边界框以来几何对象尚未移动,则该函数可能会返回预先计算的缓存边界框。

int dGeomIsSpace (dGeomID);

如果给定的 geom 是空格,则返回 1,否则返回 0。

dSpaceID dGeomGetSpace (dGeomID);

返回给定几何图形所在的空间,如果不包含在任何空间中,则返回 0。

int dGeomGetClass (dGeomID);

给定一个 geom,这会返回它的类号。标准类别编号为:

dSphereClass = 0 Sphere
dBoxClass Box
dCapsuleClass Capsule (i.e. cylinder with half-sphere caps at its ends)
dCylinderClass Regular flag ended Cylinder
dPlaneClass Infinite plane (non-placeable)
dRayClass Ray
dConvexClass
dGeomTransformClass Geometry transform
dTriMeshClass Triangle mesh
dHeightfieldClass
dFirstSpaceClass
dSimpleSpaceClass = dFirstSpaceClass Simple space
dHashSpaceClass Hash table based space
dQuadTreeSpaceClass Quad tree based space
dLastSpaceClass = dQuadTreeSpaceClass
dFirstUserClass
dLastUserClass = dFirstUserClass + dMaxUserClasses - 1,

如果没有等号,则类值等于前一行的值 + 1。
用户定义的类将返回它们自己的数字。

void dGeomSetCategoryBits (dGeomID, unsigned long bits);
void dGeomSetCollideBits (dGeomID, unsigned long bits);
unsigned long dGeomGetCategoryBits (dGeomID);
unsigned long dGeomGetCollideBits (dGeomID);

设置并获取给定几何的“类别”和“碰撞”位字段。空间使用这些位域来控制哪些几何体将相互交互。位字段保证至少为 32 位宽。新创建的几何体的默认类别和碰撞值已设置所有位。

void dGeomEnable (dGeomID);
void dGeomDisable (dGeomID);
int dGeomIsEnabled (dGeomID);

启用和禁用几何图形。dSpaceCollide和完全忽略禁用的几何图形dSpaceCollide2,尽管它们仍然可以是空间的成员。

dGeomIsEnabled()如果启用几何图形,则返回 1;如果禁用几何图形,则返回 0。新的几何图形在启用状态下创建。

9.6 碰撞检测

碰撞检测“世界”是通过创建一个空间然后向该空间添加几何图形来创建的。在每个时间步,我们都希望为所有彼此相交的几何图形生成一个接触列表。使用三个函数来执行此操作:

  • dCollide 与两个几何体相交并生成接触点。
  • dSpaceCollide 确定空间中的哪些几何对象对可能会相交,并为每个候选对象对调用回调函数。这不会直接生成接触点,因为用户可能想要专门处理某些对 - 例如通过忽略它们或使用不同的接触生成策略。这些决定是在回调函数中做出的,回调函数可以选择是否为每对调用 dCollide。
  • dSpaceCollide2 确定一个空间中的哪些几何图形可能与另一空间中的几何图形相交,并对每个候选对调用回调函数。它还可以针对空间测试单个非空间几何对象,或者将较低包含子级别的空间视为针对较高级别空间的几何对象。当存在冲突层次时,即当存在包含其他空间的空间时,此功能非常有用。

碰撞系统的设计旨在为用户提供最大的灵活性来决定哪些对象将相互测试。这就是为什么存在三个碰撞函数,而不是例如一个仅生成所有接触点的函数。

空格可以包含其他空格。这些子空间通常表示彼此靠近的几何图形(或其他空间)的集合。这对于通过将碰撞世界划分为层次结构来获得额外的碰撞性能非常有用。这是一个有用的示例:

假设您有两辆车在某些地形上行驶。每辆车都由许多几何体组成。如果将所有这些几何图形插入到同一空间中,则两辆车之间的碰撞计算时间将始终与几何图形的总数成正比(甚至与该数字的平方成正比,具体取决于使用的空间类型)。

为了加速碰撞,创建了一个单独的空间来代表每辆车。汽车几何体插入到车位中,车位插入到顶层空间中。在每个时间步,都会为顶层空间调用 dSpaceCollide。这将在车位之间(实际上是在它们的边界框之间)进行单个相交测试,并在它们接触时调用回调。然后,回调可以使用 dSpaceCollide2 测试车位中的几何图形。如果汽车彼此不靠近,则不会调用回调,并且不会浪费时间执行不必要的测试。

如果正在使用空间层次结构,则可以递归地调用回调函数,例如,如果dSpaceCollide 调用回调,该回调又使用相同的回调函数调用dSpaceCollide。在这种情况下,用户必须确保回调函数是正确可重入的。

这是一个示例回调函数,它遍历所有空间和子空间,为所有相交的几何图形生成所有可能的接触点:

void nearCallback (void *data, dGeomID o1, dGeomID o2)
{
    
    
    if (dGeomIsSpace (o1) || dGeomIsSpace (o2)) {
    
      
        // colliding a space with something :
        dSpaceCollide2 (o1, o2, data,&nearCallback);  
        // collide all geoms internal to the space(s)
        if (dGeomIsSpace (o1))
            dSpaceCollide ((dSpaceID)o1, data, &nearCallback);
        if (dGeomIsSpace (o2))
            dSpaceCollide ((dSpaceID)o2, data, &nearCallback); 
    } else {
    
     
        // colliding two non-space geoms, so generate contact
        // points between o1 and o2
        int num_contact = dCollide (o1, o2, max_contacts,contact_array, skip);
        // add these contact points to the simulation ... 
    }
} 
    ... // collide all objects together
    dSpaceCollide (top_level_space,0,&nearCallback);

当使用 dSpaceCollide 或 dSpaceCollide2 处理空间时,不允许空间回调函数修改该空间。例如,您不能在空间中添加或删除几何图形,也不能在空间中重新定位几何图形。这样做会在 ODE 的调试版本中触发运行时错误。

9.6.1 类别和碰撞位域

每个几何体都有一个“类别”和“碰撞”位字段,可用于协助空间算法确定哪些几何体应该交互,哪些不应该交互。使用此功能是可选的 - 默认情况下,几何图形被认为能够与任何其他几何图形发生碰撞。

位域中的每个位位置代表不同类别的对象。这些类别(如果有)的实际含义是用户定义的。类别位字段指示几何对象属于哪个类别。碰撞位字段指示在碰撞检测期间几何对象将与哪些类别发生碰撞。

仅当其中一个几何对象具有与另一个几何对象中的类别位相对应的碰撞位集时,dSpaceCollide 和 dSpaceCollide2 才会考虑将一对几何对象传递给回调。具体测试如下:

// test if geom o1 and geom o2 can collide
cat1 = dGeomGetCategoryBits (o1);
cat2 = dGeomGetCategoryBits (o2);
col1 = dGeomGetCollideBits (o1);
col2 = dGeomGetCollideBits (o2);
if ((cat1 & col2) || (cat2 & col1)) {
    
    
    // call the callback with o1 and o2
} else {
    
    
    // do nothing, o1 and o2 do not collide
}

请注意,只有dSpaceCollide和dSpaceCollide2使用这些位域,它们才会被 忽略dCollide。

通常,一个几何对象仅属于一个类别,因此在类别位字段中仅设置一位。位域保证至少为 32 位宽,因此用户能够为最多 32 个对象指定任意交互模式。如果对象超过 32 个,那么其中一些对象显然必须具有相同的类别。

有时,类别字段将包含多个位集,例如,如果几何对象是一个空间,您可能希望将类别设置为所包含的所有几何对象类别的并集。

设计说明:为什么我们不只有一个类别位字段并使用测试(cat1 & cat2) ?这更简单,但单个字段需要更多位来表示某些交互模式。例如,如果 32 个几何体具有 5 维超立方体的交互模式,则在更简单的方案中需要 80 位。更简单的方案也使得确定某些情况下的类别变得更加困难。

9.6.2 碰撞检测功能

int dCollide (dGeomID o1, dGeomID o2, int flags, dContactGeom *contact, int skip);

给定两个可能相交的几何图形o1,o2为它们生成联系信息。o1在内部,这只是为和调用正确的类特定碰撞函数o2。

flags指定几何体接触时应如何生成接触。
的低 16 位flags是一个整数,指定要生成的最大接触点数量。请求的联系人数量不能为零。
的高 16 位flags可以包含以下标志的任意组合:

  • CONTACTS_UNIMPORTANT - 仅生成任何联系人(跳过任何联系人改进并尽快返回找到的任何联系人)。

所有其他位flags必须清零。将来,其他位可用于从不同的接触生成策略中进行选择。

contact指向一个dContactGeom结构体数组。该阵列必须至少能够容纳最大数量的触点。这些dContactGeom结构可以嵌入到数组中更大的结构中 -参数是数组中skip从一个到下一个的字节偏移量。dContactGeomIf skipis sizeof(dContactGeom)thencontact指向普通(C 风格)数组。skip小于是错误的sizeof(dContactGeom)。

如果几何图形相交,则该函数返回生成的接触点的数量(并更新数组contact),否则返回 0(并且contact数组未接触)。

如果将空格作为o1或传递o2,则此函数将使 中 包含的所有对象o1与 中包含的所有对象发生碰撞o2,并返回生成的接触点。这种空间与几何体(或空间与空间)碰撞的方法不提供用户对各个碰撞的控制。要获得该控制,请改用 dSpaceCollide 或 dSpaceCollide2。

如果o1和o2是相同的几何对象,则此函数将不执行任何操作并返回 0。从技术上讲,对象与自身相交,但在这种情况下查找接触点没有用。

该函数不关心 和 是否o1在o2同一个空间中(或者实际上它们是否在任何空间中)。

void dSpaceCollide (dSpaceID space, void *data, dNearCallback *callback);

这确定了空间中的哪些几何图形对可能会相交,并使用每个候选对调用回调函数。该callback函数的类型为dNearCallback,其定义为:

typedef void dNearCallback (void *data, dGeomID o1, dGeomID o2);

参数data从 dSpaceCollide 直接传递到回调函数。其含义由用户定义。和o1参数o2是可能彼此靠近的几何图形。

回调函数可以调用dCollide和o1来o2生成每对之间的接触点。然后这些接触点可以作为接触接头添加到模拟中。用户的回调函数当然可以选择不调用dCollide任何对,例如,如果用户决定那些对不应该交互。

冲突空间中包含的其他空间没有被特殊处理,即它们没有被递归到。回调函数可以将这些包含的空间作为一个或两个几何参数传递。

dSpaceCollide()保证将所有相交的几何对传递给回调函数,但它也可能会出错并传递不相交的对。错误呼叫的数量取决于空间使用的内部算法。因此,您不应期望 dCollide 会返回传递给回调的每一对联系人。

void dSpaceCollide2 (dGeomID o1, dGeomID o2, void *data, dNearCallback *callback);

此函数类似于 dSpaceCollide,不同之处在于它传递两个几何对象(或空间)作为参数。o1它为所有可能相交的对(包含来自 的一个几何图形和来自 的一个几何图形)调用回调o2。

o1确切的行为取决于和的类型o2:

如果一个参数是非空间几何对象,而另一个参数是空间,则回调函数将被调用,并包含几何对象与空间中对象之间的所有潜在交集。

  • 如果两个参数都是空格并且它们的子级别值不同,则具有较低子级别的空间将被视为与较高子级别的空间相对的几何对象(可以通过dSpaceSetSublevel调用将子级别值分配给空间 - 该值不会自动跟踪!)。
  • 如果o1和o2都是同一子层的空间,那么这将调用所有潜在相交对的回调,其中包含来自o1的一个geom和来自o2的一个geom。所使用的算法取决于碰撞的空间类型。如果无法选择优化算法,则该函数将采用以下两种策略之一:
  • o1中的所有几何都与o2进行了逐一测试。
  • o2中的所有几何都要在0中逐一测试。使用的策略可能取决于许多规则,但一般来说,对象较少的空间会逐个检查其几何形状。
  • 如果两个参数都是相同的空间,这相当于在该空间上调用dSpaceCollide。
  • 如果两个参数都是非空间几何,则只需使用这些参数调用回调一次。如果给这个函数一个空间和同一个空间中的几何体X,这种情况不会被特殊处理。在这种情况下,回调总是使用(X,X)对调用,因为对象总是与自身相交。用户可以测试这种情况并忽略它,或者直接将(X,X)对传递给dcollision(保证返回0)。

9.7 空间功能

空间有几种类型。每种类型使用不同的内部数据结构来存储几何图形,并使用不同的算法来执行碰撞剔除:

  • 简单的空间。这不会进行任何碰撞剔除 - 它只是检查每个可能的几何对是否相交,并报告 AABB 重叠的对。对n个对象进行交叉测试所需的时间为O ( n2 )。这不应该用于大量对象,但它可能是少量对象的首选算法。这对于调试碰撞系统的潜在问题也很有用。
  • 多分辨率哈希表空间。它使用一种内部数据结构来记录每个几何图形如何与多个三维网格之一中的单元格重叠。每个网格具有边长为 2 i的立方体单元,其中i是范围从最小值到最大值的整数。对n 个对象进行交叉测试所需的时间为O ( n )(只要这些对象没有聚集得太紧密),因为每个对象都可以快速与其周围的对象配对。
  • 四叉树空间。它使用预先分配的基于网格的分层 AABB 树来快速剔除碰撞检查。对于景观世界中的大量物体来说,它的速度非常快。使用的内存量为 4^深度 * 32 字节。目前 dSpaceGetGeom 尚未针对四叉树空间实现。

以下是用于空格的函数:

dSpaceID dSimpleSpaceCreate (dSpaceID space);
dSpaceID dHashSpaceCreate (dSpaceID space);

创建一个空间,可以是简单哈希表类型,也可以是多分辨率哈希表类型。如果space非零,则将新空间插入到该空间中。

dSpaceID dQuadTreeSpaceCreate (dSpaceID space, dVector3 Center, dVector3 Extents, int Depth);

创建一个四叉树空间。center并extents定义根块的大小。depth设置树的深度 - 创建的块数为 4^depth。

void dSpaceDestroy (dSpaceID);

这会破坏一个空间。它的功能与 dGeomDestroy 完全相同,只是它需要一个dSpaceID参数。当一个空间被销毁时,如果其清理模式为 1(默认),则该空间中的所有几何对象也会自动销毁。

void dHashSpaceSetLevels (dSpaceID space, int minlevel, int maxlevel);
void dHashSpaceGetLevels (dSpaceID space, int *minlevel, int *maxlevel);

设置和获取多分辨率哈希表空间的一些参数。哈希表中使用的最小和最大单元格大小分别为 2^minlevel和 2^ maxlevel。minlevel必须小于或等于maxlevel。

在 dHashSpaceGetLevels 中,最小和最大级别通过指针返回。如果指针为零,则它将被忽略并且不返回任何参数。

void dSpaceSetCleanup (dSpaceID space, int mode);
int dSpaceGetCleanup (dSpaceID space);

设置并获取空间的清理模式。如果清理模式为1,那么当空间被破坏时,所包含的geoms也会被破坏。如果清理模式为 0,则不会发生这种情况。新空间的默认清理模式为 1。

void dSpaceSetSublevel (dSpaceID space, int sublevel);
int dSpaceGetSublevel (dSpaceID space);

设置和获取空间的子级别值。子级别会影响空间dSpaceCollide2与另一个空间碰撞时的处理方式。如果两个空间的子级别匹配,则该函数将迭代两个空间的几何图形并使它们相互碰撞。如果一个空间的子级别大于另一个空间的子级别,则仅迭代具有更大子级别的空间的几何图形,另一个空间将作为几何图形本身传递到碰撞回调中。默认情况下,所有空间都分配零子级别。

笔记!当一个空间插入另一个空间或从一个空间删除时,空间子级别不会自动更新。如有必要,客户有责任更新子级别值。

void dSpaceAdd (dSpaceID, dGeomID);

将几何图形添加到空间。space如果向几何创建函数提供参数, 则可以自动调用此函数。

void dSpaceRemove (dSpaceID, dGeomID);

从空间中删除几何体。如果几何对象实际上不在空间中,则这不会执行任何操作。如果几何体位于空间中,则 dGeomDestroy 自动调用此函数。

int dSpaceQuery (dSpaceID, dGeomID);

如果给定的几何对象在给定的空间中,则返回 1,否则返回 0。

int dSpaceGetNumGeoms (dSpaceID);

返回空间中包含的几何图形的数量。

dGeomID dSpaceGetGeom (dSpaceID, int i);

返回i空间中包含的第 'th 个几何对象。i范围必须为 0 到dSpaceGetNumGeoms()-1。

如果对空间进行任何更改(包括添加和删除几何图形),则无法保证任何特定几何图形的索引号将如何变化。因此,在枚举几何时不应进行空间更改。

当以 0、1、2 等顺序访问 geoms 时,该函数保证是最快的。其他非连续顺序可能会导致访问速度变慢,具体取决于内部实现。

9.8 几何类

9.8.1 球体类

dGeomID dCreateSphere (dSpaceID space, dReal radius);
创建给定的球体几何图形radius,并返回其 ID。如果space非零,则将其插入该空间。球体的参考点是其中心。
void dGeomSphereSetRadius (dGeomID sphere, dReal radius);
设置给定球体的半径。
dReal dGeomSphereGetRadius (dGeomID sphere);
返回给定球体的半径。
dReal dGeomSpherePointDepth (dGeomID sphere, dReal x, dReal y, dReal z);
返回给定球体中点 ( x, y, )的深度。z几何图形内部的点将具有正深度,其外部的点将具有负深度,而表面上的点将具有零深度。

9.8.2 盒子类

dGeomID dCreateBox (dSpaceID space, dReal lx, dReal ly, dReal lz);
lx创建给定 x/y/z 边长 ( , ly, lz)的长方体几何体,并返回其 ID。如果space非零,则将其插入该空间。盒子的参考点是它的中心。
void dGeomBoxSetLengths (dGeomID box, dReal lx, dReal ly, dReal lz);
设置给定的边长box。
void dGeomBoxGetLengths (dGeomID box, dVector3 result);
返回result给定 的边长box。
dReal dGeomBoxPointDepth (dGeomID box, dReal x, dReal y, dReal z);
返回给定框中点 ( x, y, )的深度。z几何图形内部的点将具有正深度,其外部的点将具有负深度,而表面上的点将具有零深度。

9.8.3 平面类

dGeomID dCreatePlane (dSpaceID space, dReal a, dReal b, dReal c, dReal d);
创建给定参数的平面几何图形,并返回其 ID。如果space非零,则将其插入该空间。平面方程为

a * x + b * y + c * z = d平面的法向量为 ( a , b , c ),长度必须为 1。平面是不可放置的几何对象。这意味着,与可放置几何体不同,平面没有指定的位置和旋转。这意味着参数 (a,b,c,d) 始终位于全局坐标中。换句话说,假设平面始终是静态环境的一部分,并且不依赖于任何可移动物体。

void dGeomPlaneSetParams (dGeomID plane, dReal a, dReal b, dReal c, dReal d);
设置给定的参数plane。

void dGeomPlaneGetParams (dGeomID plane, dVector4 result);
返回result给定的参数plane。

dReal dGeomPlanePointDepth (dGeomID plane, dReal x, dReal y, dReal z);
返回给定平面中点 ( x, y, )的深度。z几何图形内部的点将具有正深度,其外部的点将具有负深度,而表面上的点将具有零深度。

请注意,ODE 中的平面实际上并不是真正的平面:它们是半空间。任何在半空间内移动的东西都会被弹出。这意味着平面只是从一侧角度来看的平面。如果你想让你的平面颠倒过来,请将整个平面方程乘以-1

9.8.4 胶囊类

dGeomID dCreateCapsule (dSpaceID space, dReal radius, dReal length);
创建给定参数的胶囊几何体,并返回其 ID。如果space非零,则将其插入该空间。

胶囊就像一个普通的圆柱体,只是它的两端有半球形帽。这一功能使得内部碰撞检测代码特别快速和准确。圆柱体的长度(不包括盖子在内)由 给出length。圆柱体沿着几何体的局部 Z 轴对齐。盖子和圆柱体本身的半径由 给出radius。

void dGeomCapsuleSetParams (dGeomID capsule, dReal radius, dReal length);
设置给定胶囊的参数。

void dGeomCapsuleGetParams (dGeomID capsule, dReal *radius, dReal *length);
返回给radius定length胶囊的参数。

dReal dGeomCapsulePointDepth (dGeomID capsule, dReal x, dReal y, dReal z);
返回给定胶囊中点 ( x, y, )的深度。z几何图形内部的点将具有正深度,其外部的点将具有负深度,而表面上的点将具有零深度。

9.8.5 气缸类

dGeomID dCreateCylinder (dSpaceID space, dReal radius, dReal length);
创建给定参数的圆柱体几何图形,并返回其 ID。如果space非零,则将其插入该空间。

void dGeomCylinderSetParams (dGeomID cylinder, dReal radius, dReal length);
设置给定气缸的参数。

void dGeomCylinderGetParams (dGeomID cylinder, dReal *radius, dReal *length);
返回给定气缸的radius和length参数。

9.8.6 射线

射线与所有其他几何类不同,因为它不代表固体对象。它是一条从几何体的位置开始并沿几何体的局部 Z 轴方向延伸的无限细线。

在一条射线和另一个几何体之间调用 dCollide 将最多产生一个接触点。射线对于结构中的接触信息有自己的约定dContactGeom(因此从该信息创建接触接头是没有用的):

  • pos- 这是光线与另一个几何体表面相交的点,无论光线是从几何体内部还是外部开始。
  • normal- 这是接触点处另一个几何体的表面法线。如果 dCollide 将射线作为其第一个几何对象传递,则法线将正确定向以适应来自该表面的射线反射(否则它将具有相反的符号)。
  • depth- 这是从射线起点到接触点的距离。

光线对于可见性测试、确定射弹或光线的路径以及物体放置等方面非常有用。

dGeomID dCreateRay (dSpaceID space, dReal length);
创建给定长度的射线几何体,并返回其 ID。如果space非零,则将其插入该空间。

void dGeomRaySetLength (dGeomID ray, dReal length);
设置给定的长度ray。

dReal dGeomRayGetLength (dGeomID ray);
获取给定的长度ray。

void dGeomRaySet (dGeomID ray, dReal px, dReal py, dReal pz, dReal dx, dReal dy, dReal dz);
设置给定光线的起始位置(px,py,pz)和方向(dx,dy,dz)。射线的旋转矩阵将被调整,使局部z轴与方向对齐。注意,这不会调整光线的长度。

void dGeomRayGet (dGeomID ray, dVector3 start, dVector3 dir);
获取射线的起始位置 ( start) 和方向 ( )。dir返回的方向将是单位长度向量。

void dGeomRaySetParams( dGeomID ray, int FirstContact, int BackfaceCull );
void dGeomRayGetParams( dGeomID ray, int *FirstContact, int *BackfaceCull );
void dGeomRaySetClosestHit( dGeomID ray, int ClosestHit );
int  dGeomRayGetClosestHit( dGeomID ray );
设置或获取光线参数,确定从 dCollide 返回光线几何体和修剪网格几何体之间的碰撞。

FirstContact确定 dCollide 是否返回射线几何体和修剪网格几何体之间检测到的第一个碰撞,即使该碰撞不是距离射线起始位置最近的碰撞。BackfaceCull确定当碰撞发生在光线和背面三角形之间时,dCollide 是否返回光线几何体和修剪网格几何体之间的碰撞。默认值为FirstContact= 0, BackfaceCull= 0 (均为false)。

ClosestHit确定 dCollide 是否返回射线和修剪网格几何体之间最接近的碰撞。如果ClosestHit为false,则 dCollide 返回的碰撞可能不是距射线位置最近的碰撞。如果在 中FirstContact设置为true,则忽略此参数dGeomRaySetParams()。如果ClosestHit设置为true并BackfaceCull设置为false,则 dCollide 返回的碰撞可能位于射线和背面三角形之间。默认值为ClosestHit= 0 ( false )

9.8.7 凸类 Convex

dGeomID dCreateConvex (dSpaceID space, dReal *planes, unsigned planecount, 
                       dReal *points, unsigned pointcount, unsigned *polygons); 
void dGeomSetConvex (dGeomID g, dReal *planes, unsigned planecount,
                     dReal *points, unsigned pointcount, unsigned *polygons);

9.8.8 三角网格类

三角形网格 (TriMesh) 表示任意三角形集合。三角形网格碰撞系统具有以下特点:

  • 任何三角形“soup”都可以被表示——即三角形不需要具有任何特定的条形、扇形或网格结构。
  • 三角形网格可以与球体、盒子、射线和其他三角形网格交互。
  • 它适用于相对较大的三角形。
  • 它使用时间相干性来加速碰撞测试。当几何体与修剪网格检查一次碰撞时,数据将存储在修剪网格内。可以使用 dGeomTriMeshClearTCCache 函数清除该数据。将来可以禁用此功能。
    Trimesh/Trimesh 碰撞,表现相当不错,但有三个小警告:
  • 一般来说,为了准确解决碰撞,必须减小您使用的步长。与原始碰撞相比,非凸形状碰撞更依赖于碰撞几何形状。此外,与简单的凸多面体(例如球体和立方体)相比,非凸多面体的局部接触几何形状将变化得更快(并且以更复杂的方式)。
  • 为了有效地解决碰撞,dCollideTTL 需要前一个时间步中发生碰撞的修剪网格的位置。这用于计算每个碰撞三角形的估计速度,用于查找碰撞方向、接触法线等。这需要用户在每个时间步更新这些变量。此更新是在 ODE 外部执行的,因此它不包含在 ODE 本身中。执行此操作的代码如下所示:
const double *DoubleArrayPtr = Bodies(BodyIndex).TransformationMatrix->GetArray();
dGeomTriMeshDataSet( TriMeshData, TRIMESH_LAST_TRANSFORMATION, (void *) DoubleArrayPtr );

变换矩阵是标准的 4x4 齐次变换矩阵,“DoubleArray”是 16 个矩阵值的标准展平数组。
注意:三角形网格类不是最终版本,因此未来 API 可能会发生变化。
注意:dInitODE()必须调用才能成功使用 Trimesh。

dTriMeshDataID dGeomTriMeshDataCreate();
void dGeomTriMeshDataDestroy (dTriMeshDataID g);

创建和销毁用于存储网格数据的 dTriMeshData 对象。

void dGeomTriMeshDataBuild (dTriMeshDataID g,
                            const void* Vertices, int VertexStride, int VertexCount,
                            const void* Indices, int IndexCount, int TriStride,
                            const void* Normals);

注意:此处的参数顺序并不直观:对于顶点数据为 stride,count,但对于索引数据为 count,stride。

用于用dTriMeshData数据填充对象。这里没有复制数据,因此传递到该函数的指针必须保持有效。这就是跨步数据的工作原理:

struct StridedVertex {
    
    
    dVector3 Vertex; // 4th component can be left out, reducing memory usage
    // Userdata
}; 
int VertexStride = sizeof (StridedVertex);
struct StridedTri {
    
    
    int Indices(3);
    // Userdata
};
int TriStride = sizeof (StridedTri);

参数Normals是可选的:每个修剪网格对象的面的法线。例如,

dTriMeshDataID TriMeshData;
TriMeshData = dGeomTriMeshGetTriMeshDataID ( Bodies(BodyIndex).GeomID); // as long as dReal == floats
dGeomTriMeshDataBuildSingle (TriMeshData,
                             Bodies(BodyIndex).VertexPositions,  3*sizeof(dReal), (int) numVertices, // Vertices
                             Bodies(BodyIndex).TriangleIndices, 3*((int) NumTriangles), 3*sizeof(unsigned int), // Faces
                             Bodies(BodyIndex).FaceNormals); //  Normals

这种预先计算可以在接触评估过程中节省一些时间,但不是必需的。如果您不想在构造之前计算面法线(或者如果您有巨大的修剪网格并且知道只有很少的面会接触并希望节省时间),只需为参数传递“NULL”,dCollideTTLNormals将处理正常的计算本身。

void dGeomTriMeshDataBuildSimple (dTriMeshDataID g,
                                  const dVector3*Vertices, int VertexCount,
                                  const int* Indices, int IndexCount);

为方便起见,提供了简单的构建功能。

typedef int dTriCallback (dGeomID TriMesh, dGeomID RefObject, int TriangleIndex);
void dGeomTriMeshSetCallback (dGeomID g, dTriCallback *Callback);
dTriCallback* dGeomTriMeshGetCallback (dGeomID g);

可选的每个三角形回调。允许用户说出是否需要与特定三角形发生碰撞。如果返回值为零,则不会生成任何联系。

typedef void dTriArrayCallback (dGeomID TriMesh, dGeomID RefObject, const int* TriIndices, int TriCount);
void dGeomTriMeshSetArrayCallback (dGeomID g, dTriArrayCallback* ArrayCallback);
dTriArrayCallback *dGeomTriMeshGetArrayCallback (dGeomID g);

可选的每个几何回调。允许用户一次性获取所有相交三角形的列表。

typedef int dTriRayCallback (dGeomID TriMesh, dGeomID Ray, int TriangleIndex, dReal u, dReal v);
void dGeomTriMeshSetRayCallback (dGeomID g, dTriRayCallback* Callback);
dTriRayCallback *dGeomTriMeshGetRayCallback (dGeomID g);

可选的射线回调。允许用户根据交点的重心坐标确定射线是否与三角形碰撞。例如,用户可以对位图进行采样以确定是否应该发生碰撞。

dGeomID dCreateTriMesh (dSpaceID space, dTriMeshDataID Data,
                        dTriCallback *Callback,
                        dTriArrayCallback *ArrayCallback,
                        dTriRayCallback *RayCallback);

构造函数。该Data成员定义新创建的三角形网格将使用的顶点数据。

void dGeomTriMeshSetData (dGeomID g, dTriMeshDataID Data);

替换当前数据。

void dGeomTriMeshClearTCCache (dGeomID g);

清除内部时间一致性缓存。

void dGeomTriMeshGetTriangle (dGeomID g, int Index, dVector3 *v0, dVector3 *v1, dVector3 *v2);

检索世界空间中的三角形。、v0和v1参数v2是可选的。

void dGeomTriMeshGetPoint (dGeomID g, int Index, dReal u, dReal v, dVector3 Out);

根据传入数据检索世界空间中的位置。

void dGeomTriMeshEnableTC(dGeomID g, int geomClass, int enable);
int dGeomTriMeshIsTCEnabled(dGeomID g, int geomClass);

这些函数可用于在三网格碰撞检查期间启用/禁用时间相干性的使用。可以为每个三网格实例/几何类对启用/禁用时间一致性,目前它适用于球体和盒子。球体和盒子的默认值为“false”。

‘enable’ 参数应该是 1 代表 true,0 代表 false。

时间一致性是可选的,因为在三网格在其生命周期内可能与许多不同的几何体发生碰撞的情况下,允许它可能会导致微妙的效率问题。如果在三网格上启用时间一致性,则可以通过间歇性地为其调用 dGeomTriMeshClearTCCache 来缓解这些问题。

typedef int dTriTriMergeCallback(dGeomID TriMesh, int FirstTriangleIndex, int SecondTriangleIndex);
void dGeomTriMeshSetTriMergeCallback(dGeomID g, dTriTriMergeCallback* Callback);
dTriTriMergeCallback* dGeomTriMeshGetTriMergeCallback(dGeomID g);

允许用户为通过合并其他两个联系人生成的新联系人生成假三角形索引。用户稍后可以使用该索引来确定用作合并联系人的源的原始三角形的属性。该回调当前在 OPCODE trimesh-sphere 和 OPCODE new trimesh-trimesh 碰撞中使用。

如果未分配回调(默认),则生成 -1 作为合并联系人的三角形索引。

注意:在引入此 API 之前,索引始终设置为第一个三角形索引。

9.8.9 Heightfield

dHeightfield 是一个常规网格高度场对撞机。它可用于高度图地形,也可用于可变形的动画水面。

9.8.9.1 高度场数据

dHeightfieldData 是一个存储类,类似于 dTrimeshData 类,它保存所有几何属性和可选的高度样本数据。

dHeightfieldDataID dGeomHeightfieldDataCreate ();
void dGeomHeightfieldDataDestroy (dHeightfieldDataID d)

分配和销毁 dHeightfieldDataID 对象。在删除 geom 后,您必须调用 dGeomHeightfieldDataDestroy 来销毁它。指定数据格式类型时使用 dHeightfieldDataID 值。

9.8.9.1.1 从现有数据构建

有四个函数可以轻松地从不同数据类型的高度值数组构建高度字段。它们都具有相同的参数,只是pHeightData指针的数据类型不同:

void dGeomHeightfieldDataBuildByte   (dHeightfieldDataID d,
                                      const unsigned char *pHeightData,
                                      int bCopyHeightData,
                                      dReal width, dReal depth,
                                      int widthSamples, int depthSamples,
                                      dReal scale, dReal offset, dReal thickness, int bWrap); 
void dGeomHeightfieldDataBuildShort  (dHeightfieldDataID d,
                                      const short *pHeightData,
                                      int bCopyHeightData,
                                      dReal width, dReal depth,
                                      int widthSamples, int depthSamples,
                                      dReal scale, dReal offset, dReal thickness, int bWrap); 
void dGeomHeightfieldDataBuildSingle (dHeightfieldDataID d,
                                      const float *pHeightData,
                                      int bCopyHeightData,
                                      dReal width, dReal depth,
                                      int widthSamples, int depthSamples,
                                      dReal scale, dReal offset, dReal thickness, int bWrap); 
void dGeomHeightfieldDataBuildDouble (dHeightfieldDataID d,
                                      const double *pHeightData,
                                      int bCopyHeightData,
                                      dReal width, dReal depth,
                                      int widthSamples, int depthSamples,
                                      dReal scale, dReal offset, dReal thickness, int bWrap);

将 heightfield 示例数据加载到 HeightfieldData 结构中。在 geom 使用 dHeightfieldDataID 之前,必须将其配置为指定高度数据的格式。这些调用都采用相同的参数,如下所示;唯一的区别是 pHeightData 指向的数据类型。

  • pHeightData是指向高度数据的指针;
  • bCopyHeightData指定是否应将高度数据复制到本地存储。如果为零,则通过引用访问数据,因此必须在高度场的整个生命周期中持续存在;
  • width,height是几何体局部 X 轴和 Z 轴上的世界空间高度场尺寸;
  • widthSamples,depthSamples指定沿高度场的宽度和深度采样的顶点数。自然这个值至少必须是两个或更多;
  • scale是垂直样本高度乘数,适用于所有原始高度数据的统一比例;
  • offset是垂直样本偏移量,添加到缩放高度数据;
  • thickness是 AABB 的厚度,添加到最低点下方,以防止物体从非常薄的高度场掉落;
  • bWrap如果高度场应该是有限的,则为 0;如果应该无限平铺,则为 1。
9.8.9.1.2 从回调中检索数据
typedef dReal (*dHeightfieldGetHeight) (void *userdata, int x, int z); 
void dGeomHeightfieldDataBuildCallback (dHeightfieldDataID d,
                                        void *pUserData,
                                        dHeightfieldGetHeight *pCallback,
                                        dReal width, dReal depth,
                                        int widthSamples, int depthSamples,
                                        dReal scale, dReal offset, dReal thickness, int bWrap);

此调用指定高度字段数据由用户计算,并且在确定其形状的给定元素的高度时应使用给定的回调。回调函数在模拟运行时被调用,并返回给定 (x,z) 位置处的高度图值(“y”值)。

  • pUserData是一个指针,用于将任意用户定义的数据传递给回调
  • pCallback是一个指向回调函数的指针
  • 其他参数与其他 dGeomHeightfieldDataBuild* 函数相同。
9.8.9.1.3 设置地形边界
void dGeomHeightfieldDataSetBounds (dHeightfieldDataID d, dReal min_height, dReal max_height)

设置样本空间中的最小和最大高度样本边界。ODE 不会自动检测样本数据的最小和最大高度界限,这必须手动完成(以增加灵活性并允许用户控制过程)。

默认垂直样本范围是无限的,因此当样本范围已知时,调用此函数可以提高性能。此外,如果几何对象(根据此数据构建)旋转,其 AABB 将最终得到 NaN,并破坏碰撞检测;因此,如果要旋转几何图形,请始终将高度场数据的边界设置为有限值。

9.8.9.2 Heightfield Geom

dGeomID dCreateHeightfield(dSpaceID space, dHeightfieldDataID data, int bPlaceable);

使用给定的信息dHeightfieldDataID构建表示碰撞空间中高度场的几何图形。

  • dHeightfieldDataID是高度场数据对象
  • bPlaceable定义是否可以使用常用函数(例如 dGeomSetPosition 和 dGeomSetRotation)在世界中转换此几何图形。如果几何对象未设置为可放置,则它使用固定方向,其中全局 Y 轴表示高度场的“高度”。
void dGeomHeightfieldSetHeightfieldData(dGeomID g, dHeightfieldDataID Data);
dHeightfieldDataID dGeomHeightfieldGetHeightfieldData(dGeomID g);

设置和检索该几何对象的高度场数据对象。

9.8.10 几何变换类

geom 变换类已弃用。请改用几何偏移。

dGeomID dCreateGeomTransform (dSpaceID space);
void dGeomTransformSetGeom (dGeomID g, dGeomID obj);
dGeomID dGeomTransformGetGeom (dGeomID g);
void dGeomTransformSetCleanup (dGeomID g, int mode);
int dGeomTransformGetCleanup (dGeomID g);
void dGeomTransformSetInfo (dGeomID g, int mode);
int dGeomTransformGetInfo (dGeomID g);

如果您的代码使用 geom 变换,请尽快将其更新为使用 geom 偏移量。几何变换函数将从下一个版本中删除。

9.9 用户定义的类

ODE 的几何类在内部实现为 C++ 类。如果您想定义自己的几何类,可以通过两种方式执行此操作:

  • 使用本节中的 C 函数。这样做的优点是可以在代码和 ODE 之间提供清晰的分离。
  • 将类直接添加到 ODE 的源代码中。这样做的优点是您可以使用 C++,因此实现可能会更干净一些。如果您的碰撞类通常有用并且您希望将其贡献给公共源代码库,那么这也是首选方法。
    下面是用户定义几何类的 C API。

每个用户定义的几何类都有一个唯一的整数。新的几何类(称为“X”)必须向 ODE 提供以下内容:

  • 处理 X 与一个或多个其他类之间的碰撞检测和接触生成的函数。这些函数必须是 类型dColliderFn,其定义为:
typedef int dColliderFn (dGeomID o1, dGeomID o2, int flags, dContactGeom *contact, int skip);

这与 具有完全相同的界面dCollide。每个函数将处理特定的碰撞情况,其中o1具有类型 X 和o2一些其他已知类型。

  • 类型为“选择器”的函数,dGetColliderFnFn定义为:
typedef dColliderFn * dGetColliderFnFn (int num);

该函数接受一个类号 ( num),并返回可以处理 X 与类 的碰撞的碰撞函数num。如果 X 不知道如何与 class 发生冲突,它应该返回 0 num。请注意,如果类 X 和 Y 发生冲突,则只需其中一个类需要提供与另一个类发生冲突的函数。

该函数很少被调用 - 返回值被缓存并重用。

  • 将计算此类实例的轴对齐边界框 (AABB) 的函数。该函数必须是 类型dGetAABBFn,其定义为:
typedef void dGetABBBFn (dGeomID g, dReal aabb[6]);

该函数给出g,其类型为 X,并返回 的轴对齐边界框g。该aabb数组包含元素(minx、maxx、miny、maxy、minz、maxz)。如果您不想计算 AABB 的严格界限,您可以只提供一个指向 的指针dInfiniteAABB,该指针在每个方向上返回 +/- 无穷大。

  • 此类实例所需的“类数据”的字节数。例如,球体将其半径存储在类数据区域中,而盒子则将其边长存储在那里。

对于几何类,以下内容是可选的:

  • 一个会破坏类数据的函数。大多数类不需要此函数,但有些类会想要释放堆内存或释放其他资源。该函数必须是 类型dGeomDtorFn,其定义为:
typedef void dGeomDtorFn (dGeomID o);
参数的o类型为 X。
  • 该函数将测试给定的 AABB 是否与 X 的实例相交。这在空间碰撞函数中用作提前退出测试。该函数必须是 类型dAABBTestFn,其定义为:
typedef int dAABBTestFn (dGeomID o1, dGeomID o2, dReal aabb2[6]);
参数的类型为 X。如果提供了此函数,则当与 geom 相交o1时,dSpaceCollide 会调用该函数,该函数具有由 给出的 AABB 。如果相交则返回 1 ,如果不相交则返回 0。 o1o2aabb2aabb2o1

例如,这对于大地形很有用。地形通常具有非常大的 AABB,这对于测试与其他对象的相交不是很有用。该函数可以测试另一个物体针对地形的 AABB,而无需考虑调用特定碰撞函数的计算麻烦。在针对 GeomGroup 对象进行测试时,这可以节省大量资金。

以下是用于管理自定义类的函数:

int dCreateGeomClass (const dGeomClass *classptr);

注册一个新的几何类,由 定义classptr。返回新班级的编号。ODE 中使用的约定是将类编号分配给名称为dXxxClassXxx 的全局变量(例如dSphereClass)。

这是结构体的定义dGeomClass:

struct dGeomClass {
    
    
    int bytes; // bytes of custom data needed
    dGetColliderFnFn *collider; // collider function
    dGetAABBFn *aabb; // bounding box function
    dAABBTestFn *aabb_test; // aabb tester, can be 0 for none
    dGeomDtorFn *dtor; // destructor, can be 0 for none
};
void * dGeomGetClassData(dGeomID);

给定一个 geom,返回一个指向类的自定义数据的指针(这将是所需字节数的块)。

dGeomID dCreateGeom (int classnum);

创建给定类别编号的几何图形。自定义数据块最初将设置为 0。可以使用 dSpaceAdd 将这个对象添加到空间中。

当您实现一个新类时,您通常会编写一个执行以下操作的函数:

  • 如果该类尚未创建,请创建它。您应该小心,只能创建该类一次。
  • 调用 dCreateGeom 创建该类的实例。
  • 设置自定义数据区域。

9.10 复合对象

考虑以下对象:

  • 一张桌子,由顶部的盒子和每条腿的盒子组成。
  • 由多个连接在一起的圆柱体建模而成的树枝。
  • 具有代表每个原子的球体的分子。

如果这些对象是刚性的,那么就需要使用单个刚体来表示它们中的每一个。但执行碰撞检测似乎是一个问题,因为没有单一的几何类可以表示像桌子或分子这样的复杂形状。解决方案是使用由多个几何体组合而成的 复合碰撞对象。

不需要额外的函数来管理复合对象:只需创建每个组件几何并将其附加到同一个主体即可。要在同一对象中相对于彼此移动和旋转单独的几何体,可以使用几何体偏移。这里的所有都是它的!

然而,有一个警告:您永远不应该创建一个复合对象,这会导致碰撞点生成得非常接近。例如,考虑一张桌子,它由一个顶部盒子和四个桌腿盒子组成。如果腿与顶部齐平,并且桌子侧放在地上,则为盒子生成的接触点可能与腿与顶部连接的位置重合。ODE 目前不会优化重合接触点,因此这种情况可能会导致数值错误和奇怪的行为。

在此示例中,应调整工作台几何形状,以使桌腿不与侧面齐平,从而更不可能生成重合的接触点。一般来说,避免不同的接触表面重叠或沿其边缘排列。

9.11 实用功能

void dClosestLineSegmentPoints (const dVector3 a1, const dVector3 a2,
                                const dVector3 b1, const dVector3 b2,
                                dVector3 cp1, dVector3 cp2);
给定两条线段 A 和 B,其端点a1-a2和b1- b2,返回 A 和 B 上彼此最接近的点(在cp1和中cp2)。对于存在多个解的平行线的情况,将返回涉及至少一条线的端点的解。这对于零长度的行可以正确工作,例如 if a1==a2和/或b1== b2。

int dBoxTouchesBox (const dVector3 p1, const dMatrix3 R1, const dVector3 side1,
                    const dVector3 p2, const dMatrix3 R2, const dVector3 side2);
给定框 ( p1, R1, side1)( p2, R2, side2),如果它们相交则返回 1,如果不相交则返回 0。p是盒子的中心,R是盒子的旋转矩阵,side是 x/y/z 边长的向量。

void dInfiniteAABB (dGeomID geom, dReal aabb[6]);
如果您不想计算 AABB 的紧边界,则可以将该函数用作几何类中的 AABB 获取函数。它在每个方向返回+/-无穷大。

9.12 实施说明

9.12.1 大型环境

通常,碰撞世界将包含许多属于静态环境一部分的对象,这些对象与刚体无关。ODE 的碰撞检测经过优化,可检测不移动的几何体,并预先计算有关这些对象的尽可能多的信息以节省时间。例如,预先计算边界框和内部碰撞数据结构。

9.12.2 使用不同的碰撞库

使用 ODE 的碰撞检测是可选的 - 可以使用替代碰撞库,只要它可以提供dContactGeom初始化接触接头的结构。

ODE 的动力学核心大部分独立于所使用的碰撞库,除了以下四点:

  • 必须定义类型dGeomID,因为每个主体都可以存储指向与其关联的第一个几何对象的指针。
  • dGeomMoved()必须使用以下原型定义该函数:
void dGeomMoved (dGeomID);
每当物体移动时,动力学代码都会调用此函数:它表明与物体关联的几何对象现在处于新位置。
  • dGeomGetBodyNext()必须使用以下原型定义该函数:
dGeomID dGeomGetBodyNext (dGeomID);
动力学代码调用此函数来遍历与每个主体关联的几何对象列表。给定附加到主体的几何图形,它返回附加到该主体的下一个几何图形,如果没有更多的几何图形,则返回 0
  • dGeomSetBody()必须使用以下原型定义该函数:
void dGeomSetBody (dGeomID, dBodyID);
在主体析构函数代码中调用此函数(第二个参数设置为 0)以删除从 geom 到主体的所有引用。

如果您想要一个替代碰撞库从 ODE 获取身体运动通知,您应该适当地定义这些类型和函数。

10 如何进行良好模拟

10.1 如何进行良好的模拟

10.1.1 积分器精度和稳定性

  • 积分器不会给出精确解
  • 什么是稳定性
  • 积分器类型(exp & imp、阶数)
  • 准确性、稳定性和工作之间的权衡

10.1.2 行为可能取决于步长

  • 更小的步长=更准确、更稳定
  • 100.1 与 50.2 不同
  • 调整最终帧速率

10.1.3 让事情进展得更快

执行速度取决于哪些因素?每个关节都会从系统中移除多个自由度 (DOF)。例如,球窝移除了三个,铰链移除了五个。对于通过关节连接的每个单独的实体组,其中:

  • m 1 m_1 m1 是组中的关节数量,
  • m 2 m_2 m2 是这些关节移除的自由度总数,并且
  • n 是组中物体的数量,那么该组每一步的计算时间与: k 1 O ( m 1 ) + k 2 O ( m 2 3 ) + k 2 O ( n ) k_1 O(m_1) + k_2 O(m_2^3) + k_2 O(n) k1O(m1)+k2O(m23)+k2O(n)

ODE 目前依赖于“系统”矩阵的因式分解,该矩阵针对每个删除的自由度删除一行/列(这就是 O ( m 2 3 ) O ( m_2^3 ) O(m23) 的来源)。在使用球窝接头的 10 体链中,大约 30-40% 的时间用于填充此矩阵,30-40% 的时间用于对其进行因式分解。

因此,为了加快模拟速度,您可以考虑:

  • 使用更少的关节——通常小型物体及其相关关节可以被纯粹的运动学“假货”取代,而不会损害物理真实感。
  • 用更简单的替代方案替换多个关节。随着更专业的关节类型的定义,这将变得更容易。
  • 使用较少的联系人。
  • 在可能的情况下,优先选择无摩擦或粘性摩擦接触(消除一个自由度)而不是库仑摩擦接触(消除三个自由度)。将来,ODE 将实施可随着关节数量更好地扩展的技术。

10.1.4 让事情变得稳定

  • 僵硬的弹簧/僵硬的力是不好的。
  • 硬性约束是好的。
  • 依赖于积分时间步长。
  • 尽可能使用动力关节、关节限制、内置弹簧,避免显式作用力。
  • 如果物体的移动速度超过了时间步长的合理范围
  • 长轴惯性
  • 质量比 - 例如鞭子。将大质量和小质量连接在一起的接头将很难保持较低的误差。
  • 物体大小比率。如果大小为 1 的球体必须在 1000 单位长的多边形上滚动,您可能会开始看到球轻微抖动,或者在某些情况下,甚至在它静止在地面上时开始突然弹跳。通过将多边形细分为几个较小的多边形,可以轻松解决此问题。
  • 增加全局 CFM 将使系统在数值上更加鲁棒,并且不易受到稳定性问题的影响。它还会让系统看起来更加“松软”,因此必须找到一个权衡。
  • 冗余约束(两个或多个“尝试做同样的工作”的约束)会互相冲突并导致稳定性问题。该问题的数值原因是系统矩阵中的奇异性。一个例子是两个接触接头在同一点连接同一对主体。另一个例子是,如果通过使用沿铰链轴间隔开的两个球关节连接两个实体来在两个实体之间创建虚拟铰链关节(这很糟糕,因为两个球关节试图从系统中删除六个自由度,但真实的铰链接头只能移除五个)。冗余约束相互冲突,并在系统中产生奇怪的力量,可以淹没正常的力量。例如,受影响的身体可能会四处飞行,就好像它有自己的生命一样,完全无视重力。

10.1.5 使用约束力混合 (CFM)

  • 允许单一配置
  • 影响:由于误差放大而导致抖动或奇怪的力,LCP 求解器可能会变慢
  • 允许顺从接头(这也可能是不需要的)

10.1.6 避免奇点

  • 当关节数量多于限制身体运动所需的数量时,就会出现奇点。
  • 物体之间的多个(不兼容)关节,特别是关节+接触(不要碰撞连接在一起的物体)。
  • 增加CFM
  • 无意 - 箱链落在地板上、其他组件上
  • 使用最少的关节来实现正确的行为。使用正确的关节来实现所需的行为
  • 添加全局 CFM 通常会有所帮助

10.1.7 其他的东西

  • 推出太远时接触抖动 - 解决方案:使用柔软度
  • 保持长度和质量在 1 左右
  • LCP 求解器采用可变次数的迭代(仅非确定性部分)。如果花费的时间太长,则增加全局 CFM,防止多次接触(或类似的接触),并限制高比例的力大小(抓取树问题)
  • 铰链限制在 +/- pi 之外

11 常见问题

http://ode.org/wiki/index.php/Manual#From_the_Manual

参考

1、官网–Open Dynamics Engine
2、官方手册
3、Open Dynamics Engine(ODE)物理引擎:公式推导

猜你喜欢

转载自blog.csdn.net/qq_38880380/article/details/131349002