现在的位置: 首页 > 综合 > 正文

HOOPS 3D可视化入门教程三:基本概念和数据结构

2018年04月05日 ⁄ 综合 ⁄ 共 8953字 ⁄ 字号 评论关闭

出处:http://www.evget.com/zh-CN/info/catalog/17485.html

上篇文章介绍了 HOOPS 的主要模块,这篇文章将要向大家介绍HOOPS的数据结构以及穿插其中的一些基本概念。这些内容主要包含在3dGS模块内。
HOOPS 3D可视化入门教程一:简介及安装部署》 
HOOPS 3D可视化入门教程二:模块介绍

一、保留模式

HOOPS采用保留绘图模式(retained mode)。所谓保留模式是相对于传统的非保留模式而言的。做过OpenGL编程的人都知道,OpenGL的绘制都是通过调用一系列绘图命令来实现的,通常是在一个叫updateGL的函数里。除非你自己把相关绘图信息保存起来,否则出了这个函数OpenGL就不认帐了,也就是说你无法从OpenGL里面再获取你曾经绘制的一些图元信息。而保留模式则不这样,它把绘制过的命令和图形会保存起来,放在特定的数据结构中,从而使得我们可以事后随时读取这些数据。相比于非保留模式,保留模式能够提供更高的效率(因为数据都在内部,下次绘制时不需要再读取),更快的交互(通过特定的基于数据结构的算法,可以加速选取、高亮等等交互操作),还有更方便的编程接口。当然凡事都有两面性,保留模式也有它的缺点,其中之一就是它增加了程序的内存消耗(用于存储那个数据结构)。但我们认为这样的代价是完全值得的。

二、基于段的数据结构

HOOPS的数据结构简单讲是基于段(segment)的树状结构。最上层是根段,为“/”。该数据结构和Linux文件系统有着一曲同工之妙,有Linux使用经验的同学将会很容易理解。Linux的根目录的符号也是“/”,所有文件系统中的文件或者文件夹的路径都以该符号开头。文件夹有名字,段也有名字。如同文件夹内可以有文件和子文件夹,segment下可以有sub segment。这样的层次结构可以很好地构建我们想要的图形。

打开一个段的HOOPS函数是HC_Open_Segment,它有一个参数,就是这个段的名字。我们可以传一个空字符串给它,从而创建一个匿名段。如果已经存在这个名字的段,则该函数会打开这个段,否则就自动创建一个新段。打开后,我们就可以在该段内做任何我们想要做的操作。操作结束,记得用HC_Close_Segment来关闭这个段。

HOOPS采用和OpenGL一样的上下文机制,那就是“状态机(State Machine)”。所谓状态机,形象地讲就是一旦改变了状态,则接下去不论程序运行到哪里,该状态将一直保存,直到下次改变状态。在HOOPS中,打开一个段实际上就意味着进入了一个状态机,直到你关闭这个段,你所有的操作都将在这样一个上下文中进行。具体来讲就是,打开一个段,然后你可以跳转到任意的程序位置完成具体的绘制任务,然后关闭段,这一系列操作没有必要在一个函数中完成。这无疑大大增加了我们编程的灵活性。

举个例子,我们想要绘制一所房子,房子有房顶、窗户还有门,我们可以用如下代码:

HC_Open_Segment (“/”);

HC_Open_Segment (“house”);

HC_Open_Segment (“roof”);

HC_Close_Segment ();

HC_Open_Segment (“door”);

HC_Open_Segment (“windows”);

HC_Open_Segment (“window1”);

HC_Close_Segment ();

HC_Open_Segment (“window2”);

HC_Close_Segment ();

HC_Open_Segment (“window3”);

HC_Close_Segment ();

HC_Close_Segment ();

HC_Close_Segment ();

HC_Close_Segment ();

实际上我们创建了如下的树状结构:

HOOPS 3D可视化入门教程三:基本概念和数据结构

 

创建了段之后,我们需要有相应的方法能够找到这个段,这时就会用到段的路径。和Linux上的文件路径类似,段的路径也分为两种:相对路径和绝对路径。我们打开一个段,进入该段的状态机,如果要打开它下面的子段,就可以用相对路径。HOOPS会自动地在该段下面找给出的段名,如果找不到,则会报错。绝对路径则是从根段名“/”开始,逐步地把段名添加上去,直到我们想要找的段为止,完整的路径就是绝对路径。例如我们要找第三扇窗户,相对于house的相对路径是:”windows/window3”,而绝对路径是:”/house/windows/window3。

我们可以用“.”和“..”来分别指代当前目录和父目录,这又跟Linux上的路径使用习惯是一致的。这种简称只能用于相对路径中。

为了能够更方便地提供段的路径,HOOPS中还有一套特有的符号,叫做“wildcards”,可以同时指代多个不同的路径,有以下几种:

1. 逗号wildcard。这个是最简单的一种。有时候我们需要同时对多个可枚举的段进行统一处理,例如我们想用同一种颜色来装饰roof和door(虽然这种做法很少见……),我们就可以用这样的一个路径来同时指代这两个段:/house/(door,roof)。

2. 通配符。可以用“*”来匹配0个或多个字符,“%”来匹配单个字符,这个跟我们用Windows系统搜索功能是一样的,也和正则表达式相一致。

3. 递归wildcards。其实上面两个并不是HOOPS所独有的,在其他也有见到。但是HOOPS还有一个它特有的符号,那就是“…”,该符号可以指代一个段名或者一串路径上的段名。例如我们可以用/house/…/window1来指代第一扇窗户,而不用去管当中到底隔了多少个段。该种方法非常长适合于我们不清楚house到window1之间到底存在着怎样的父子结构。虽然方面,但是如果我们确切地知道window1的完整路径,那就不要这么写了,因为HOOPS是通过自顶向下的方式搜索得到window1,所以需要消耗一定的计算量。另外,该符号还可以递归地表示一个段的所有子段以及子段的子段。如果我们要对一个段内的所有子段进行某项修改,那么这个wildcards真是再合适不过了。

三、几何信息(Geometry)

segment像文件夹一样,它本身并没有实质的东西,而只是一个容器。真正绘制出车的形状,还需要具体的几何信息。因此,段内部除了可以存储子段外,还可以存储Geometry。HOOPS中的Geometry丰富多样,囊括了点、边、面、壳(shell)、网络(mesh)等等基本上大家能够想到的图元。这些基本几何通过相互组合,可以组成更加复杂的图像信息,这是一个自底向上的组建过程。例如我们可以通过下面的方式插入一个点和一条直线:

HC_Open_Segment (“myseg”);

HC_Insert_Marker (0, 1, 1);

HC_Insert_Line (-1, -1, -1, 2, 2, 2);

HC_Close_Segment ();

HC_Insert_Marker需要传入三个浮点参数,也就是一个点的三维坐标。HC_Insert_Line需要传入六个参数,为一个线段的起始点和终止点的三维坐标。

我们可以用下面的代码插入一个多边形的面:

HC_POINT pts[4] = 

{HC_POINT(0, 0, 0), HC_POINT(1, 0, 0), HC_POINT(1, 1, 0), HC_POINT(0, 1, 0)};

HC_Open_Segment (“mypolygon”);

HC_Insert_Polygon (4, pts);

HC_Close_Segment ();

HC_Insert_Polygon需要传入两个参数,分别是多边形顶点个数以及存放顶点三维坐标的数组。该函数代表了HOOPS中一类参数,就是对一群点进行操作。需要注意的是,这类函数在内部会对传入的三维坐标数组进行拷贝,所以如果你传入的坐标数组是动态申请出来的,在调用完该类函数之后,必须手动地将其释放掉。

除了基本的点、线、多边形等,HOOPS还提供了两个相对高级的图元,分别是Shell和Mesh。在进行大型场景构建时,这两个图元是非常常用的,例如我们用三角网格构建一个人的模型,那么这个三角模型就是一个shell。shell有三个层次的图元组成,分别是node(点)、edge(边)和face(面),这三部分相互连接形成一个整体。mesh和shell非常类似,同样由点边面三部分组成,唯一的区别是mesh它不能形成一个封闭的类似于人这样的模型,它只能是一张面,而且只能是一张四边形面,例如一张四边形纸。这样的区别使得在处理特定的模型时,如果mesh能够满足应用需要,那么mesh将会比shell表现得高效得多。

下面举例创建一个立方体,并在它的一个面上接一个金字塔体:

HC_POINT pts[] = {

HC_POINT (0, 0, 1), HC_POINT (1, 0, 1),

HC_POINT (1, 1, 1), HC_POINT (0, 1, 1),

HC_POINT (0, 0, 2), HC_POINT (1, 0, 2), 

HC_POINT (1, 1, 2), HC_POINT (0, 1, 2),

HC_POINT (0.5, 0.5, 2.5)

};

int flist[] = {

4, 0, 3, 2, 1,

4, 0, 1, 5, 4,

4, 1, 2, 6, 5,

4, 2, 3, 7, 6,

4, 3, 0, 4, 7,

3, 4, 5, 8,

3, 5, 6, 8,

3, 6, 7, 8,

3, 7, 4, 8

};

HC_Open_Segment ("mymodel");

HC_Insert_Shell (9, pts, 41, flist);

HC_Close_Segment ();

HC_Insert_Shell需要四个参数,分别是shell的顶点个数,顶点数组,面列表数组的长度,面列表数组指针。顶点个数和数组很好理解,就是具体的各个顶点的三维坐标。面列表是这样的格式:面顶点个数n, 第一个顶点序号,第二个顶点序号,…,第n个顶点序号。例如flist第一行,4表示该面由四个顶点构成,也就是一个四边形。然后,0,3,2,1表示由pts这个数组中的第0、3、2、1号点构成这个面。需要注意的是HC_Insert_Shell的第三个参数实质flist这个数组本身的长度,而不是将要构建的shell上面的个数。例如这个例子中面的个数为9,但flist的长度为41。

效果如下图所示:

HOOPS 3D可视化入门教程三:基本概念和数据结构

 

四、段的属性

上文中,我们在HOOPS中创建了一个房子,假设我们现在已经用几何图元将房子给绘制出来了,但是光有结构还不行,至少我们还需要给它上色,或许我们还会通过贴上不同的纹理来表示不同的材料。HOOPS的段结构中除了可以存放Geometry,还可以存放属性Attribute。我们常用的属性包括:可见性(Visibility),颜色(Color),可选择性(Selectability),点、边、字体的大小,光照(light),渲染属性(rendition)等等。甚至可以添加我们自定义的属性(User defined attribution)。可以说,HOOPS的属性功能是非常全面而强大的。

和插入几何一样,要修改一个segment的属性,我们需要进入该segment的状态机,亦即要首先打开这个段。下面以house模型为例:

HC_Open_Segment (“house”);

HC_Open_Segment (“roof”);

//add roof geometry here...

HC_Set_Color (“geometry=red”);

HC_Close_Segment ();

HC_Open_Segment (“door”);

//add door geometry here...

HC_Set_Color (“geometry=grey”);

HC_Close_Segment ();

HC_Close_Segment ();

这样,我们将屋顶和门分别设置成了红色和灰色。

又比如刚才我们自创的那个集合模型,这回,我们要让它不再空白一片了,我们给它点颜色看看(J):

HC_Open_Segment ("mymodel");

HC_Set_Color ("faces=grey,edges=green");

HC_Set_Visibility ("edges=on");

HC_Insert_Shell (9, pts, 41, flist);

HC_Close_Segment ();

我们设置了mymodel这个段的两个属性,颜色和可见性。在设置颜色中,我们设置面为灰色,而设置边为绿色;在设置可见性上,我们设置边为可见。为什么不设置面为可见呢?因为在HOOPS中,有些是默认可见的,而有些是默认不可兼得;而shell的面是默认可见的,edges则恰好是默认不可见的。下面是新的效果图,怎么样,和之前不一样了吧?

HOOPS 3D可视化入门教程三:基本概念和数据结构

记住这个模型,往后的教程中我们还会多次用到,比如给它贴上漂亮的纹理、光照等等,还有动画。

上面在设置颜色时,我们用一个字符串命令同时设置了面和边的颜色。这种格式化的字符串在HOOPS中被大量应用,几乎接受字符串作为参数的HOOPS函数中都有这样的格式化命令。faces和edges对于HC_Set_Color函数来说,是可以设置颜色的对象,而等号后面是具体的值,中间用逗号分隔。如果没有显式地说明设置对象,那么就是everything,也就是所有对象。该格式化字符串有很多相关使用技巧,具体可以参看HOOPS的帮助文档,下面仅举几个例子来说明格式化字符串的基本用法:

1. “red,faces=green”,设置所有几何图元为红色,只有面为绿色;

2. “markers=edges=black”,点和边为黑色;

3. “!edges=(r=0.5 g=0.5 b=0.5)”,非边的图元颜色都设置为灰色。

至于设置对象是复数还是单数是无所谓的,即edges和edge的作用效果完全一样。

五、属性的继承

属性(Attribute)是可以被继承的,就像面向对象的编程语言里面类的继承一样。对于绝大多数属性来说,继承的方向是子段从父段中继承属性。这种特性有时候对我们来说可以提供极大的方便。回想我们之前创建的house,它有三扇窗户,一般来说,一座房子的窗户颜色都是一样的,如果没有属性的继承,那么我们大概就需要针对每一个窗户段设置它的颜色属性。对于我们这座小房子来说,这还可以接受,可是某天你发达了,让你构建一桩拥有成千上万扇窗户的摩天大楼,那恐怕就是场灾难了。有了属性的继承,世界还是美好的。我们可以在windows这个段设置颜色,那么所有该段下面的子段都自动继承了该颜色属性,再不用我们单独去设置了。

然而,问题也随之出现。整幢大楼里毕竟有些窗户所在的房间住着不寻常的人,而这些窗户我们希望显示出不一样的颜色,以彰显这些人的显赫身份。那如何避免这些窗户继承父段的颜色呢?我们可以单独设置这些窗户的颜色,HOOPS在绘制这些窗时,会优先使用单独设置在这些段上的颜色;如果没有单独设置(如同绝大多数窗户),那么HOOPS才会自动地去读取父段的该属性,直到最上层的根节点“/”。如果根节点也没有设置该属性,HOOPS就会报错。对于绝大多数的属性来说,HOOPS正是遵循这种“追根溯源”的方式来确定一个属性的值的。

虽然这种直接覆盖的属性占大多数,但是有些属性不是直接覆盖得到的,例如旋转矩阵。要计算一个图元最终在世界坐标上的位置,我们需要从根节点开始,逐步地累加旋转矩阵,一直到该段,这样计算所得的旋转矩阵才是最后真正的旋转矩阵。

虽然我们能够控制一个特定的段的属性,但是有时候我们还是想要强制整个段表现为同一种属性,而不管底下各个子段是否单独设置了该属性。有些属性就提供了这样的功能,其中之一就是颜色属性。当我们用鼠标选中了某一个segment之后,我们希望整个段都显示一种高亮色,而不管该段内部子段的单独颜色。这时,我们需要用到颜色的属性锁。可以通过调用下面的代码来对颜色加锁:

    HC_Open_Segment(“myseg”);    

      HC_Set_Color("red"); 

      HC_Set_Rendering_Options("attribute lock = color"); 

    HC_Close_Segment();

这样,myseg这个段的颜色就被锁定为红色。如果后续操作中我们不再需要对颜色进行锁定,则可以使用HC_UnSet_Rendering_Option (“attribute lock”)。

六、特殊的段——包含段和样式段

上面介绍的段都是HOOPS中的普通类型的段。此外,HOOPS还有包含段(included segment)和样式段(style segment)。这些段的功能实际上都可以用普通段来实现,但是正因为引入了这些特殊类型的段,我们可以将HOOPS的数据结构设计得更为精巧和高效,我们的程序结构性也更好。

再回顾我们之前给的house模型。我们在房子上添加了三扇窗户。一般来说,一幢房子上的窗户长得都是差不多的,因此我们想到是否可以只设计窗户一次,而三次重复使用呢?可以的,HOOPS里面使用的就是包含段(included segment)。包含段实际上就是一次定义,多次重复使用,它提高了代码的使用率,也提高了内存使用率。实际上包含段和C/C++语言中的头文件是很像的,我们编写一次头文件,然后在需要用到的地方通过#include就可以将其包含进来,而不需要另外再写。包含机制除了提高效率之外,还能够方便后续的维护,例如当我们想要更新窗户的样式时,只需要在定义处修改一次,由于三处窗户都是包含该窗户的,所以这三处就自动加载了新的样式。我们不再需要一个个地分别去修改,既提高了效率,又减少了出错的可能。

包含段通常是针对含有几何信息的段(当然,由于包含段本质上还是普通的段,因此它可以包含属性),而样式段则仅包含属性。有些时候,我们需要重用的可能仅仅是一套样式,例如颜色、大小、光照等,对于具体的几何图元我们却兴趣不大,这个时候就可以用到HOOPS的样式段。下面的代码演示了如何使用Style segment:

HC_Open_Segment (“mystyle”);

HC_Set_Color (“edges=red,faces=(diffuse=(r=0.5 g=0.2 b=0.3))”);

HC_Close_Segment ();

 

HC_Open_Segment (“myseg1”);

HC_Style_Segment (“mystyle”);

//Insert my geometry...

HC_Close_Segment ();

HC_Open_Segment (“myseg2”);

HC_Style_Segment (“mystyle”);

//Insert my another geometry...

HC_Close_Segment ();

这段创建了一个样式段,两个普通段,这两个普通段插入了不同的几何图元,但是使用了同样的样式段,所以它们显示出来后都是红色的边,紫色的面。

七、键值

键值(通常是HC_KEY类型)是HOOPS中一个非常重要的概念。HC_KEY本质上是一个32位带符号的整型。前文中我们说,可以通过段的名字以及路径(相对路径或者绝对路径)来索引一个段,于此同时我们也可以用键值来索引段。HC_Open_Segment会返回一个long型的整数,就是打开的这个段的键值。注意,新版本的HOOPS取消了在API谓词前的K变形,而所以之前这些K变形函数都返回键值了。19版本之前的HOOPS,HC_Open_Segment返回是void类型的,而要返回段的键值,则必须显示地调用HC_KOpen_Segment。在新版本中这样的函数已经去掉了,HC_Open_Segment直接返回键值。

除了段可以有键值,几何图元也可以有键值。HC_Insert_Line、HC_Insert_Polygon等插入图元的函数都会返回一个键值,该键值唯一的指代新插入的几何图元。

HOOPS中大部分的API函数都有By_Key结尾的变形,这一类的变形函数实现和它们原型函数一样的功能,唯一的区别是它们的入口参数是要操作的段的键值,而不是字符形式的名字了。

既然现在我们有两种方式来找到一个段,那么我们就需要详细地比较一下这两种方式各自的优劣。

1. 存储键值只需要一个32位整数,存储段名则需要一个字符数组,而且长度不定;

2. 用键值来找到一个段速度要比用字符路径快;

3. 段名比较直观,便于调试的时候肉眼判断正误,键值则比较抽象,一眼看上去不太容易辨别对错;

4. 段名还有路径支持之前提到的wildcards,因此可以同时指代多个不同的段,但是键值是唯一的,它只能指代一个段或者几何图元;

5. 对于几何图元来说,我们只能够用键值去找到它们,因为它们是没有字符形式的名字的;

6. 对于匿名段来说,由于我们没有赋给它任何段名(应该说是空的段名),因此也就无法用段名来索引它,而只能用键值。

以上只是我目前发现并整理的不同之处,如后续有新发现,则会继续补充。

一般来说,系统返回的键值是负数。我们可以通过HC_Renumber_Key来修改系统给我们的键值。如果我们调试的时候发现一个键值为0或者正数,那么要么是我们修改了,要么是程序在哪个地方出错了。这个概念虽小,可是在实际操作中却是非常有用的。另外,为了确保某些HOOPS API操作成功,我们可以在操作结束后将得到的键值跟INVALID_KEY进行比较。INVALID_KEY是HOOPS预定义的一个值,它表示如果API执行失败返回的错误键值。

抱歉!评论已关闭.