原創|其它|編輯:郝浩|2012-09-11 16:03:28.000|閱讀 1325 次
概述:上篇文章介紹了 HOOPS 的主要模塊,這篇文章將要向大家介紹HOOPS的數據結構以及穿插其中的一些基本概念。這些內容主要包含在3dGS模塊內。
# 界面/圖表報表/文檔/IDE等千款熱門軟控件火熱銷售中 >>
上篇文章介紹了 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 ();
實際上我們創建了如下的樹狀結構:
創建了段之后,我們需要有相應的方法能夠找到這個段,這時就會用到段的路徑。和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真是再合適不過了。
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中創建了一個房子,假設我們現在已經用幾何圖元將房子給繪制出來了,但是光有結構還不行,至少我們還需要給它上色,或許我們還會通過貼上不同的紋理來表示不同的材料。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中被大量應用,幾乎接受字符串作為參數的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執行失敗返回的錯誤鍵值。
本站文章除注明轉載外,均為本站原創或翻譯。歡迎任何形式的轉載,但請務必注明出處、不得修改原文相關鏈接,如果存在內容上的異議請郵件反饋至chenjj@fc6vip.cn
文章轉載自:慧都控件網