路径(Paths )
路径用于区分场景中某个特定对象。假设,我们想使用机器人的左脚。在图 3-10 (上一节,机器人)中那个节点能代表左脚呢?我们不能简单地认为是 foot 节点,因为这个节点(foot)是被左腿和右腿同时使用的。答案是使用路径(或称为链)来表示左脚。路径起始于 robot 节点(根节点),沿着图形向下直至通到 foot 节点。图 3-12 指出了表示左脚的路径。路径包含有一个节点链的引用(references)。其中每个节点都是前一个节点的子节点。一个路径可以表示一个完整场景或子场景(一个场景的某个部分)。在本书的所有场景图表中,使用一条连接节点链的粗线段来表示路径。
如何得到路径(Where Do Paths Come From) ?
可以通过拾取和搜索动作返回一条路径。我们也可以手工构造自己路径。(第 9 章 “应用动作”将详细讨论交互式拾取)。一个交互式应用程序的用户可以在屏幕的某个物体上点击鼠标,导致程序拾取了这个物体,然后就可以对这个物体执行一个操作-例如移动它、修改它的颜色、或者删除它。选择器节点管理着一个路径列表作为当前已经被选中的节点。
路径的用途
所有对节点可以操作的动作都同样可以被应用到路径上。这些动作包括:计算路径的包围盒、累加路径的几何变换矩阵、和将路径写入文件中。
我们怎样使用包含在路径中的信息依赖于我们编写的程序。我们可以使用整个路径,也可以使用部分路径。如果用户在机器人的左脚上点击了鼠标,那么用户是想选择整个机器人呢,还是想选择左腿,或者仅仅是想选择左脚呢?(也许用户第一次点击使,想选择整个机器人,接着再点击,将选择机器人的下级部分,例如左腿或者左脚)。
节点中的域 (Fields)
当创建完一个节点后,节点中的域就已经被设置上了预定义的数值。然后,我们就可以直接修改域中的数值了。设置域值的语法主要依赖于域的类型以及域是单值域还是多值域之分。下面的例子代码创建了一个绘制风格节点,并设置了其域值:
SoDrawStyle *d = new SoDrawStyle;
d->style.setValue(SoDrawStyle::LINES) ;
d->lineWidth.setValue(3) ;
d->linePattern.setValue(0xf0f0);
在上面的例子中,当前的绘制风格将变成非填充、虚线型、线宽是 3 个象素。如果没有显式地设置域值的话,Inventor 将对节点使用缺省值。下面列出的就是 SoDrawStyle 节点的缺省值:
域 | 缺省值 |
style | SoDrawStyle::FILLED |
lineWidth | 1 |
linePattern | 0xffff (solid) |
pointSize | 1 |
下面的章节将讨论如何设置和读取不同类型的域值。第 13 章“引擎”,将讨论一种特殊类型的域—全局域(global fields)和开关域(trigger fields)的连接关系。
为什么使用域( 高级内容) ?
为什么 Inventor 的节点使用域而不是使用简单的成员变量?。本节将概要讨论一下域的运行机制。 The Inventor Toolmaker 将提供关于这个话题的更详细的背景知识。
首先,正如下面章节和 Open Inventor C++ Reference Manual 书中描述的那样,域为设置和查询数据提供了统一的方法。第二,域为 Inventor 探测场景数据库的变化提供一种机制。第三,可以将一个节点的域连接到另外一个节点的域上,在第 13 章“引擎”将做详细讨论。最后,域提供了向文件读写节点数据的统一的方法。
单值域对多值域
单值域的内部只有一个给定类型的变量值,在它们的类名称中都包含有 SF 字符。例如:
SoSFBool 包含一个 SbBool
SoSFFloat 包含一个单一的 float
n SoSFRotation 包含一个 SbRotation
SoSFName 包含一个 SbName
SoSFColor 包含一个单一的 SbColor
单值域通常用于那些不需要数组的节点,如线型、平移值、旋转值、照相机屏幕的高宽比等等。
多值域内含有一个数组。在类名称中都包含有 MF 字符—例如 SoMFBool、 、SoMFFloat、SoMFVec3f 和 SoMFColor。 。多值域通常用于保存坐标点和法向矢量这些数据。多值域也可以用于保存材质数据,这样我们就可以为不同的顶点设置不同的颜色。大多数的域同时都具有 SF 和 MF 形式。在 Open Inventor C++ Reference Manual 书中,对每个节点中的域都做了详细的描述。
单值域:设置和查询数据
在本章中,先前的例子演示了如何声明和创建节点。本节将通过另外的例子来演示如何设置和查询节点单值域的数据。(大多数的域都带有 setValue() 和 getValue()方法。而且也可以同时使用 = 操作符来设置数据)
Floats 、Longs 、和 Shorts
第一个例子将通过使用 setValue()方法来设置 SoOrthographicCamera 节点的 height 域
值。这个域的类型是 SoSFFloat 。
SoOrthographicCamera *cam = new SoOrthographicCamera;
cam->height.setValue(1);
//或
cam->height = 1.; // = operator has been defined for this field
使用 getValue()方法可以查询这个域的数据。
float result = cam->height.getValue();
向量(Vectors) :
可以使用多种不同的格式来设置 SoSFVec3f 的域。每种格式都会定义一个 3D 向量:
- 通过一个向量(SbVec3f)来设置。
- 通过 3 个浮点数来设置(既可以是一个向量,也可以是三个单独的浮点数)。
- 通过一个 3 维浮点数数组来设置。
下面的例子将演示如何设置 SoSFVec3f 的域值。SoTransform 节点有一个叫做translation 的域,这是一个 SoSFVec3f 类型的域,它包含一个 SbVec3f 类型的变量。变量xform 是一个 SoTransform 节点的实例.
SoTransform *xform = new SoTransform;
//(1) Setting the field from a vector
SbVec3f vector;
Vector.setValue(2.5, 3.5, 0.0);
xform->translation.setValue(vector);
// or: xform->translation = vector;
//(2a) Setting the field from a vector of three floats
xform->translation。setValue(SbVec3f(2.5, 3.5, 0.0));
// or: xform->translation = SbVec3f(2.5, 3.5, 0.0);
//(2b) Setting the field from three floats
float x = 2.5, y = 3.5, z = 0.0;
xform->translation.setValue(x, y, z);
//(3) Setting the field from an array of three floats
float floatArray[3];
floatArray[0] = 2.5;
floatArray[1] = 3.5;
floatArray[2] = 0.0;
xform->translation.setValue(floatArray);
使用 getValue()方法可以查询此域值,下面的例子复制了一个向量,然后修改这个向量,最后在复制回去:
SbVec3f t = xform->translation.getValue();
t[0] += 1.0;
xform->translation.setValue(t);
// or: xform->translation = t;
旋转
旋转域可以指定一个在 3D 空间中的旋转量。因为 SbRotation 代表着绕一个轴转一个角度的旋转量,所以我们可以通过指定轴和角度来设置这个域值:
SbRotation r;
SbVec3f axis(0., 1., 0.);
float angle = M_PI; //from math.h
r.setvalue(axis, angle);
// or SbRotation r(SbVec3f(0., 1., 1.), M_PI);
也可以定义一个从一个方向向量转到另一个方向向量的旋转。
SbRotation r(SbVec3f(0.0, 0.0, 1.0), SbVec3f(0.0, 1.0, 0.0));
设置 SoTransform 节点中的 rotation 域值。
SoTransform *xform = new SoTransform;
xform ->rotation = r;
我们也可以使用 setValue()方法,通过向参数传入旋转轴和角度、一组四元数、或者两个向量来设置 rotation 域的数值。
=(赋值)操作符可以将一个域值赋值到另外一个相同类型的域值上。和使用向量一样,getValue()用于返回域值。
多值域:设置和查询数据
SoMaterial 节点包含有下列域:
域名 类
ambientColor SoMFColor
diffuseColor SoMFColor
specularColor SoMFColor
emissiveColor SoMFColor
shininess SoMFFloat
transparency SoMFFloat
下面的例子将演示设置 SoMaterial 节点域值的多种不同方式。其中 transparency 域是SoMFFloat 型的,所以它可以包含一个或多个浮点类型的数值。diffuseColor 域是SoMFColor 型的,所以它可以包含一个或多个 SbColor 类型的数值。在 SoMFFloat 域中设置多个数值的语法如下:
nodeName -> fieldname .setValues( starting index, number of values, pointer to array of values );
SoMaterial *mtl;
float vals[3];
vals[0] = 0.2;
vals[1] = 0.5;
vals[2] = 0.9;
mtl->transparency.setValues(0, 3, vals);
数组的空间在必要的时候可以被从新分配。数据将从数组中拷贝到域中。( 注意,这里提示了注意,这里提示了 Inventor 的一个重要规则,即 Inventor 是将应用程序中的数据保存在自己分配的内存中。这样做的优点是,保证了是将应用程序中的数据保存在自己分配的内存中。这样做的优点是,保证了 Inventor 和应用程序的数据分离,应用程序的数据不会对 Inventor 产生影响。但是使用这种方式,程序可能会需要两份内存,因此会浪费内存空间和产生影响。但是使用这种方式,程序可能会需要两份内存,因此会浪费内存空间和 CPU 时间。目前在最新的 Inventor 软件实现版本中,对域增加了一种新的赋值方式“引用赋值”,这和软件实现版本中,对域增加了一种新的赋值方式“引用赋值”,这和 C++ 中的引用赋值的概念是一样的,Inventor 在内部只保存应用程序数据的指针。译者注在内部只保存应用程序数据的指针。译者注
节点引用和删除
虽然节点是按照 C++方式创建的,但是在删除节点时却是有别于 C++方式。下面将讨论节点将如何计算针对自己的引用,以及引用数何时增加与减少。还要概要描述节点解除引用(unreferencing)的正确步骤,解除引用将导致节点的删除。
引用计数
每个节点都在数据库中保存有一个引用计数。节点带有几种不同引用类型:
- 父-子 引用
- 路径-节点 引用
引擎也保存有引用计数(见第 13 章“引擎”)。当引擎的输出连接上一个域之后,其引用计数就加 1。我们也可以通过调用 ref() 或 unref() 函数来手工增加或减少引用计数。
图 3-13 给出了在一个小的子场景中节点的引用计数值。无论何时,只要我们对节点产生了一个引用,节点就会增加它的引用计数。
A->addChild(B)
将 B 节点增加到 A 节点中的这个行为也会导致 B 节点的引用计数加 1。在图 3-13 中,节点 C 的引用计数是 2,因为它被加到了两个不同的父节点中。此时,A 和 D 节点的引用计数是 0。( 因为它们没有被其他节点所使用。译者注 )
如图 3-14 所示,引用一个路径中的节点也会增加这个节点的引用计数。节点 A 的引用计数现在变成了 1,而节点 B 的引用计数变成为 2。
提示: 一定要保证引用了图形场景中的根节点:root->ref(),因为根节点不能作为其他节点的子节点,所以根节点不能被其他的节点所引用。
如何删除节点
Inventor 使用引用计数的机制来删除节点和其子场景。为了理解节点是如何被删除的,我们首先需要知道节点的引用计数是如何被增加与减少的。
当删除节点的一个引用时,会减少节点的引用计数值。移去一个子节点时,也会减少子节点的引用计数值。 当一个节点的引用计数值回到 0 时,这个节点就会从数据库中被删除掉。然而在下面的情况中,删除一个节点可能会引起一些问题。(见图 3-13)
问题 1: 如果从节点 A 中去掉节点 B,那么节点 B 的引用计数就变成了 0,这样 B 节点也就被删除了。但是,如果我们还想再使用节点 B 该怎么办呢?
问题 2: 怎样删除节点 A?,它的引用计数已经是 0 了。
问题 3: 如果对一个引用计数为 0 的节点(例如节点 A)应用了一次动作(action),动作创建了一个引用此节点的路径( 这时此节点的引用计数变成了 1。译者注 ),当动作结束后,路径被删除( 这时此节点的引用计数由 1 变成了 0。译者注 ),同时节点也被删除了。
解决上述问题的方案是:当想防止一个节点被删除的话,我们就显式地引用它:
B->ref();
引用一次节点就对其引用计数增加 1,这样就可以保证节点不会被意外地删除掉。当我们显式地引用了节点 B 之后,我们就可以安全地从节点 A 中去除掉节点 B,而不必担心删除节点 B 了。(解决了问题 1)
同样的,为了防止节点 A 被删除(问题 3),我们也可以显式引用它:
A->ref();
引用和删除的总结
表 3-1 总结了当节点和引擎的引用计数增加和减少的时候所发生的事情。 注意将引擎连接到一个节点的域上时,并不会增加这个节点的引用计数。(引擎将在第13章中进行讨论)。
引用计数加 1 | 引用计数减 1 |
将一个节点作为子节点增加到另 外的一个节点中,将增加这个子 节点的引用计数 | 去除一个节点的子节点,这个子节点将减少引用计数 |
将节点增加进路径中 | 从路径中去除节点 |
对节点或路径应用一个动作后, 将使所有被遍历过的节点增加引 用计数。 | 当动作遍历完成后,所有被遍历的节点都将被解除引用 |
将一个节点增加进 SoNodeList节 点中 | 从 t SoNodeList 节点中除去一个节点 |
将 SoSFNode 或 SoMFNode数值指 向某个节点,该节点的引用计数 将增加。 | 修改 SoSFNode 或 e SoMFNode 中的某个数值,使其指向另 外不同的节点或者指向 NULL,或者删除这个数值,那么 这个数值原来所指向的节点将减少引用计数。 |
将一个引擎的输出连接到一个节 点的域上,将增加引擎的引用计 数 | 从一个节点域上断开引擎的输出,引擎的引用计数将减少 |
节点类型
Inventor 通过 SoType 类提供了运行时类型检查(runtime type-checking)的功能。一个节点实例可以使用 getTypeId()方法得到它的 SoType 类。像节点、引擎、动作、细节(details)、和事件这些绝大多数的 Inventor 类都提供了运行时类型检查的功能。
SoType 类可以返回类型的父类(getParent()),可以创建一个特定类型的实例(createInstance()),也可以返回类型的名称(getName())。例如下面的代码将可能返回诸如“Material”或“Group”这样的类型名称:
node->getTypeId().getName();
节点命名
我们可以为节点、路径、或引擎赋值一个名字,然后可以根据名字就能查找到这些对象。因为对象在读写文件的时候,其名字都是要保存的,因此名字也是一种标识对象的有效途径。基类 SoBase 提供了一个叫做 setName()的方法,这个方法允许为一个节点、路径、或引擎指定一个名字。同时基类也提供了 getName()方法用于返回对象的名字。
任何节点、路径、或引擎都只能有一个名字,但名字不必是唯一的( Inventor 允许同一个名字赋值在多个节点、路径或引擎对象上。译者注允许同一个名字赋值在多个节点、路径或引擎对象上。译者注 )。名字是 SbName 类型的。SbName 必须以大小
写字母(A-Z)或者下划线字母( _ )为开头,在 SbName 中的所有字符必须是数字 0-9、大小写字母 A-Z、或者下划线。一个对象的缺省名字是空字符串(“”)。
通过 SoNode 类的方法 getByName(), ,可以根据一个给定的名字找到一个或者多个节点(SoPath 和 SoEngine 也提供了类似的 getByName()方法)。“搜索动作”也能根据一个给定的名字找到一个或者多个节点。