> 原文鏈接:[http://www.aosabook.org/en/vtk.html](http://www.aosabook.org/en/vtk.html)
> 作者:Berk Geveci 與 Will Schroeder
可視化工具箱(Visualization Toolkit, VTK)是一種廣泛使用的數據處理與可視化軟件系統。它應用于科學計算、醫學影像分析、計算幾何、渲染、圖像處理以及信息學等領域。本章,我們展示一個VTK的簡要概覽,包括一些使之成為一個成功系統的基本設計模式。
要真正理解一個軟件系統,關鍵之處不僅要理解它能夠解決什么問題,而且還要了解它出現時的特定文化環境。在VTK的案例中,系統表面看起來要設計成用于科學數據的三維可視化系統。但VTK出現時的文化語境為奮斗者們添上了一個意義深遠的背后故事,這有助于解釋軟件為什么是這樣設計和部署的。
在VTK創生和開始編寫之時,它的初始作者(Will Schroeder、Ken Martin、Bill Lorensen)還是GE研發部門的科研人員。我們向一個名為LYMB的先驅性系統投入了大量精力,該系統是一種以C實現的、類似Smalltalk的開發環境。在那個時代,它是一個偉大的系統,我們作為科研人員一再地被阻止在兩大障礙上:1)IP問題(此處意指知識產權(Intellectual Property, IP)——譯注。)和2)非標準的、有所有權的軟件。IP問題之所以是個問題,是因為一旦GE公司的律師介入,那么嘗試將軟件向公司外部公布軟件就幾乎不可能了。第二,即使我們在GE公司內部部署軟件,許多我們的用戶也會受制于學習一個有所有權的、非標準系統,因為為了掌握它而作出的努力不能在他離開公司后轉移到新的雇主那里;并且,這種軟件沒有標準工具集所提供的廣泛支持。于是,VTK的原始動機就是開發一個開放標準,或曰“*協作平臺*”,通過它,我們能夠很容易地將技術傳授給我們的用戶。因此,為VTK選擇一個開源許可證或許是我們所做出的最重要的設計決策。
最終選擇了非互利的、自由的許可證(比如:選[BSD](http://en.wikipedia.org/wiki/BSD_licenses)(即BSD許可證(Berkeley Software Distribution License,BSD)。——譯注。)而不選[GPL](http://www.gnu.org/copyleft/gpl.html)(即GPL許可證(GNU General Public License)。——譯注。))在事后證明是一個值得效仿的決策,因為它最終使基于商業的服務和咨詢成為可能,而這正成就了[Kitware](http://www.kitware.com/)。在我們做出這個決定的時候,我們最感興趣的是降低與學術界、研究機構以及商務實體之間合作的壁壘。我們從那時也發現,許多組織都避免使用互利性許可證,由于它們可能造成的嚴重問題。事實上,我們可能會爭論互利性許可證在延緩開源軟件的接收上有很大作用,但這另當別論。這里的要點是:與任何軟件系統相關的重要設計決策之一就是著作權許可證的選擇。重新審視項目的目標,然后再恰當地解決IP問題是很重要的。
## 24.1 VTK是什么?
VTK最初是以一個科學數據可視化系統出現的。可視化領域之外的許多人都天真地把它當成一種特殊的幾何渲染:查看虛擬物體并與之交互。盡管這些確實是可視化的一部分,但是通常的數據可視化還包括把數據轉換成感知性輸入的整個過程,典型的數據是圖像,此外還包括觸覺、聽覺等其他形式。數據形式不僅由幾何拓撲結構組成——比如像網格或者復雜空間分解等抽象形式,還有核心結構的屬性,諸如標量(如:溫度或壓強),矢量(如:速度),張量(如:應力與張力),以及渲染屬性,諸如表面法線和紋理坐標等。
注意,通常情況下,表示時空信息的數據被看做是科學可視化的一部分。然而,還有更抽象數據形式,比如市場統計資料、網頁、文檔以及其它信息,它們只能通過諸如非結構文檔、表格、圖和樹等抽象(即:非時空)關系來表示。這些抽象數據一般通過信息可視化的方法來處理。在社區的幫助下,VTK現在能夠完成科學可視化和信息可視化方面的工作。
作為一種可視化系統,VTK的角色是以這些形式獲取數據,并最終將它們轉換成利于人類感官理解的形式。因此,VTK的核心需求之一就是創建數據流管線的能力,這種管線能夠讀入、處理、表示并最終渲染數據。這樣,工具箱就必須構建成一個靈活的系統,它的設計在許多層面上反映了這一點。例如,我們有目的地將VTK設計成這樣一種工具箱,它具有許多可互換的組件,這些組件可以組合起來用于處理多種數據。
## 24.2 架構特性
在深入介紹VTK特殊的架構特性之前,先介紹頂層的概念,它們系統的開發和使用都產生了深遠的影響。其中之一就是VTK的混合包裝設施。該設施從VTK的C++實現自動生成Python,Java,和Tcl等的語言綁定(還可綁定更多的語言,并且[有些已經實現了](http://vtkdotnet.sourceforge.net/)——譯注)。最具實力的開發者將使用C++進行工作。使用者和應用程序開發者也可以使用C++,但是通常情況下,上文提到的解釋性語言更加適合這兩個群體。混合的編譯性/解釋性環境將這兩個領域的優勢結合在了一起:計算密集型算法的高性能和樣機或開發的靈活性。事實上,這種多語言計算的方法在許多科學計算社區中得到廣泛應用,并且許多團隊將VTK作為他們自己軟件的一個范本。
就軟件過程而言,VTK采用CMake來控制構建過程;CDash/CTest用于測試;然后CPack用于跨平臺部署。VTK確實可以在幾乎任何計算機上進行編譯,包括因其簡陋的開發環境而聲名狼藉的超級計算機。此外,開發工具外圍還包括網頁、wiki、郵件列表(用戶區和開發者區),文檔生成設施(即:Doxygen)和bug追蹤系統(Mantis)。
## 24.2.1 核心特性
由于VTK是面向對象系統,在其內部,對類的訪問和數據成員的實例化都被小心地管理起來。通常情況下,所有的數據成員的訪問權限均為protected或private。通過`Set`和`Get`方法來訪問這些數據成員,這兩種方法具有各種類型的形參,例如:布爾型數據、模態數據、字符串、以及向量。這些方法中的多數的創建是通過向類的頭文件中插入宏來實現的。例如:
~~~
vtkSetMacro(Tolerance, double);
vtkGetMacro(Tolerance, double);
~~~
可以展開為如下形式:
~~~
virtual void SetTolerance(double);
virtual double GetTolerance();
~~~
使用這些宏的原因已經超出了僅僅使代碼清晰。VTK中有重要的數據成員控制調試、更新對象的修改時間(MTime)、并恰當地管理引用計數。這些宏正確地操作這些數據,因而強烈推薦使用它們。例如,當一個對象的修改時間沒有得到恰當的管理時,VTK中就會出現一個尤其嚴重的bug。在這種情況下,代碼就不會按其應該運行的方式運行,或者還會執行多次。
VTK的優勢之一就是其相對簡單的用于表示和管理數據的方法。典型的情況下,各種特殊數據(例如:`vtkFloatArray`)的數組用于表示信息的連續片段。例如:一個裝載有三個三維坐標點的表可以用具有9個元素的`vtkFloatArray`來表示。這些數組有一種元組的記法,故有一個三維坐標點即一個3元組,而一個對稱的3×3張量矩陣可以由一個6元組表示()。專門采用這種設計是因為在科學計算中,與操作數組的系統(例如:Fortran)接口是很常見的,并且這樣還能使對大塊連續數據的內存分配與回收變得更加高效。再者,連續數據的通信、串行、以及IO操作通常更有效率。這些(可以加載各種類型數據的)核心數據數組表示了VTK中的大部分數據,且具有多種方便的方法,以進行信息的插入和訪問,包括用于快速訪問的方法、以及在添加更多數據時所需要的自動分配內存的方法。數據數組是抽象類`vtkDataArray`的子類,該抽象類的意義在于:通用的虛方法可用于簡化編碼。但是,為了實現更高的性能,靜態的、模版化的函數被引入,這樣就可以根據不同的參數類型進行切換,并實現隨后對連續數據數組的直接訪問。
即使由于性能方面的原因,模板被廣泛地使用,C++模板通常在公有類的API中也是不可見的。這點在STL中也是如此:我們采用了[PIMPL](http://en.wikipedia.org/wiki/Opaque_pointer)設計模式來隱藏模版實現的復雜細節。這種模式為我們提供了很大幫助,尤其是在以前文所述將代碼包裝為解釋性代碼的時候。避免公有API中模板的復雜性意思是:在應用程序開發者看來,VTK實現大部分是無需考慮數據類型的選擇的。當然,在其外殼之下,代碼的執行是由數據類型來驅動的,而該數據類型則一般是運行時訪問數據時確定的。
一些用戶很想知道為什么VTK使用引用計數來管理內存而不是垃圾回收這一對用戶來說更為友好的方式。基本的答案是當數據被刪除的時候,VTK需要對其完全控制,因為要處理的數據量可能十分巨大。例如,一組1000×1000×1000字節的體數據的數據量是1G字節。把這么大的數據留在內存中等待垃圾回收器來決定是否應該釋放它們,確實不是一個好主意。在VTK中,大部分類(`vtkObject`的子類)具有內建的引用計數能力。每個對象都包含有一個引用計數,它在該對象實例化時被初始化為1。每次使用該對象都會進行注冊,然后引用計數就加1。類似地,當使用該對象進行了反注冊(或者等效地認為該對象被刪除),那么引用計數就會減1。最終的對象引用計數減至0,此時該對象自毀。下面列舉一個典型的例子:
~~~
vtkCamera *camera = vtkCamera::New(); // reference count is 1
camera->Register(this); // reference count is 2
camera->Unregister(this); // reference count is 1
renderer->SetActiveCamera(camera); // reference count is 2
renderer->Delete(); // ref count is 1 when renderer is deleted
camera->Delete(); // camera self destructs
~~~
這里還有另外一個關于為什么引用計數對于VTK很重要的原因——它提供了有效復制數據的能力。例如:想象有一個數據對象D1,它由許多數據數組組成:點、多邊形、顏色、標量、以及紋理坐標等。現在假設處理該數據來生成一個新的數據對象D2,此對象與第一個對象相同,還外加了向量數據(用于定位點)。一種浪費資源的方式是完全復制(深拷貝)D1來創建D2,然后向其中加入新的向量數據數組。另有一種方法,我們創建一個空的D2,然后將D1中的數組傳給D2(淺拷貝),使用引用計數來追蹤數據所有權,最終向添加新的向量數組。后者方法避免了復制數據,這正如前文所述,對一個優秀可視化系統是必不可少的。我們在本章的稍后內容中可以看到,數據處理的管線例行公事式地實現了這種運行機制,即:將數據從算法的數據復制至輸出,此時引用計數對于VTK是必不可少的。
當然,引用計數也有一些臭名昭著的問題。偶爾會存在引用周期,這時循環中的對象以一種相互支持的配置來引用彼此。這種情況下,就需要明智的介入,或者在VTK中,一種在`vtkGarbageCollector`中實現的特殊設施就可以用來管理牽涉與上述循環中的對象。當這樣的類被鑒別到的時候(這被期望發生在開發過程中),該類就會將其自身注冊至垃圾回收器,并管理其自己的`Register`和`Unregister`方法的開銷。然后緊接著的對象銷毀(或者反注冊)方法對局部的引用計數網絡進行拓撲分析,搜索已經分離了的相互引用的對象群。這些都將被垃圾回收器予以刪除。
VTK中的多數實例化過程是通過一種以靜態類成員實現的對象工廠運行。典型的語義表達如下:
~~~
vtkLight *a = vtkLight::New();
~~~
這里要認識到的重要之處是:這里實際被實例化的可能不是`vtkLight`,可能是`vtkLight`的子類(例如:`vtkOpenGLLight`)。采用對象工廠的動機多種多樣,最為重要的是應用的可移植性和設備不相關性。例如,前文中我們在一個渲染場景中創建了一個光源。在一個運行于特定平臺上的特定的應用程序中,`vtkLight::New`可能會生成一個OpenGL光源,然而在不同的平臺上,存在著圖形系統中其他渲染庫或方法來創建光源的可能性。到底實例化什么樣的派生類是一種運行時系統信息的功能。在早期的VTK中,可以有包括gl、PHIGS、Starbase、XGL、以及OpenGL等多種選擇。然而這些圖形庫中的多數現在已經消失了,出現了包括DirectX和基于GPU方法在內的新方法。隨著時間的推移,一個利用VTK寫成的應用程序沒必要進行修改,因為開發者已經派生出了特定的對應于新設備的`vtkLight`的子類和其他渲染類來支持不斷發展的技術。另外一個對象工廠的重要用處是使性能增強變動的運行時替換成為可能。例如,一個`vtkImageFFT`可能取代一個訪問特種用途硬件或數值計算庫的類。
## 24.2.2 數據表示
VTK的一個優點就是其表示數據復雜形式的能力。這些數據形式包括從簡單表格到有限元網格之類的復雜結構。所有這些數據形式都是`vtkDataObject`的子類,如圖24.1所示(注意這是數據對象類的繼承圖的一部分)。

**圖24.1:數據對象類**
`vtkDataObject`類的最重要的特點之一是它能被可視化管線(見下節)處理。上圖展示的類中,只有一部分典型地應用于大多數實際的應用程序中。`vtkDataSet`及其派生類被用于科學可視化(見圖24.2)。例如,`vtkPolyData`用于表示多邊形網格;`vtkUnstructuredGrid`用于表示網格,而`vtkImageData`表示二維或者三維的像素和體素數據。

**圖24.2:數據集類**
## 24.2.3 管線架構
VTK由若干主干子系統組成。與可視化包關聯最緊密的子系統或許應該是數據流/管線架構了。從概念上講,管線架構由三類基本對象組成:表示數據的對象(上文中的`vtkDataObject`),將數據從一種形式處理、變換、濾波或者映射成另外一種形式的對象(`vtkAlgorithm`),以及執行管線的對象(`vtkExecutive`)——此管線控制著一個由交錯數據(?)和過程對象(即:管線)組成的連通圖。圖24.3展示了一個典型的管線。

**圖24.3:典型的管線**
盡管概念上很簡單,但真正地實現這種管線架構卻是挑戰性的。一個原因就是數據的表示可能會很復雜。例如,某些數據集由層次化的或分組的數據組成,那么執行這種數據就需要特殊的迭代或遞歸。對于復合性的事務,并行處理(不論使用內存共享還是可擴展的、分布式的方法)需要將數據劃分成片段,這些片段可能需要重疊,以一致地計算比如導數等的邊界信息。
算法對象也同樣引入了其自身的復雜性。某些算法可能需要多個輸入并且/或者產生多個不同類型的輸出。某些可以對數據進行局部運算(例如:計算一個網格的中心),而另外一些則需要全局性的信息,例如計算直方圖。任何情況下,算法將其輸入看作是不變量,算法只是讀取輸入、以求得輸出。這是因為數據可能是多個算法的輸入,一個算法可以踐踏另外一個算法的輸入可不是什么好主意。
最后,執行過程的復雜程度視執行策略的特點而定。有些場合,我們可能希望將濾波器之間的處理結果暫存。這將使那些如果管線發生變化就必須進行的重新計算量最小化。另一方面,可視化數據集可能很大,這種情況下我們可能希望在計算過程不再需要這些數據的時候釋放它們。最后,有一些復雜的執行策略,例如數據的多分辨率處理,它需要管線以迭代的方式運行。
為了展示這些概念中的一部分,并進一步解釋管線架構,來看下面的C++示例:
~~~
vtkPExodusIIReader *reader = vtkPExodusIIReader::New();
reader->SetFileName("example.exe");
vtkContourFilter *cont = vtkContour::New();
cont->SetInputConnection(reader->GetOutputPort());
cont->SetNumberOfContours(1);
cont->SetValue(0, 200);
vtkQuadricDecimation *deci = vtkQuadricDecimation::New();
deci->SetInputConnection(cont->GetOutputPort());
deci->SetTargetReduction( 0.75 );
vtkXMLPolyDataWriter *writer = vtkXMLPolyDataWriter::New();
writer->SetInputConnection(deci->GetOutputPort());
writer->SetFileName("outputFile.vtp");
writer->Write();
~~~
這個示例中,reader對象讀取一個巨大的非結構網格(或網)數據文件。接下來的濾波器從該網格中生成一個等值面。`vtkQuadricDecimation`濾波器通過大量削減(即:減少表示等值圍線的三角形的數量)來降低等值面的數據大小,該等值面是一個多邊形數據集。最后大量削減后,新的減少了數據量的結果將被寫回磁盤。實際的管線執行在writer調用`Write`方法的時候(即:需要數據的時候)發生。
正如這個示例所展示的,VTK的管線執行機制是實際要求驅使的。當一個像是writer或者mapper(一個數據渲染對象)的漏(sink)需要數據的時候,它就向其輸入發出請求。如果作為輸入的濾波器已經有了合適的數據,它就簡單地向漏返回執行控制權。然而,若輸入并沒有合適的數據,它就需要進行計算。隨后,它就必須先向它自己的輸入請求數據。這個過程將會沿著管線繼續上溯,直到有濾波器或者源擁有“合適的數據”或者到達了管線的始端,這時,濾波器就會按照正確的順序依次執行,而數據就會沿管線流向請求需要它的地方。
這里我們將展開來講什么是“合適的數據”。缺省情況下,VTK源或是濾波器執行后,其輸出被管線緩存以避免將來不必要的執行。這樣做是為了以存儲為代價,使計算量和/或I/O最小化,這是可配置的行為。管線緩存的不僅是數據對象,還有關于這些數據對象生成的條件的元數據。這種元數據包括時間戳(即:計算時間),它捕捉這些數據對象何時被用于計算。因此,在最簡單的情況下,“合適的數據”就是指從其開始上溯的所有管線對象變動之后被計算得出的數據。通過下面的示例展示這種特征更容易。我們在上面的VTK程序的最后加入如下代碼:
~~~
vtkXMLPolyDataWriter *writer2 = vtkXMLPolyDataWriter::New();
writer2->SetInputConnection(deci->GetOutputPort());
writer2->SetFileName("outputFile2.vtp");
writer2->Write();
~~~
如前文所述,第一個`writer->Write`調用引發整個管線的執行。當`writer2->Write`被調用時,管線將緩存的時間戳與削減濾波器、圍線濾波器以及reader的變動時間對比后會發現削減濾波器的輸出緩存是即時的。于是,數據請求無需傳播的遠于`writer2`。現在,我們來考慮下面的變化。
~~~
cont->SetValue(0, 400);
vtkXMLPolyDataWriter *writer2 = vtkXMLPolyDataWriter::New();
writer2->SetInputConnection(deci->GetOutputPort());
writer2->SetFileName("outputFile2.vtp");
writer2->Write();
~~~
現在,管線執行對象會發現圍線濾波器在圍線濾波器和削減濾波器的輸出最后一次被執行之后又發生了變動。因此,這兩個濾波器的緩存就是過時的,它們需要重新執行。然而,鑒于reader在圍線濾波器之前就發生了變動,所以它的緩存是有效的,因此reader不需要重新執行。
這里描述的場景是要求驅動的管線系統的最簡單的例子。VTK管線遠比此復雜。當濾波器或者漏請求數據,它可以提供附加的信息以請求特殊的數據子集。例如,一個濾波器可以通過數據的流片段進行核心外分析。我們通過修改之前的示例來展示。
~~~
vtkXMLPolyDataWriter *writer = vtkXMLPolyDataWriter::New();
writer->SetInputConnection(deci->GetOutputPort());
writer->SetNumberOfPieces(2);
writer->SetWritePiece(0);
writer->SetFileName("outputFile0.vtp");
writer->Write();
writer->SetWritePiece(1);
writer->SetFileName("outputFile1.vtp");
writer->Write();
~~~
這里writer請求管線的上游載入并以兩個片段來處理數據,每個片段獨立地流向下游。你可能會注意到之前所述的那種簡單的執行邏輯在這里行不通了。根據這種邏輯,`Write`函數第二次被調用時,管線不應該重新執行,因為上游沒有發生任何變動。于是為了著力解決這一更加復雜的情況,執行對象具有附加的邏輯以處理這里所說的片段請求。VTK管線執行事實上由多重關卡組成。數據對象的計算實際上最后一關。這一關之前是請求關。這里是漏和濾波器告訴上游它們需要從即將進行的計算中獲得什么數據的地方。在上面的示例中,writer將會提醒它的輸入,它需要兩個片段中的第0個。這一請求實際上會沿路傳播到reader。當管線執行時,reader就會知道它需要讀取一個數據的子集。再者,關于緩存數據對應的是哪個片段的信息存儲于對象的元數據中。下次濾波器向其輸入請求數據時,這個元數據就會被與當前請求作比較。于是該示例中的管線就會重新執行,以處理一個不同的片段請求。
濾波器可以發出若干更多類型的請求。這些請求包括特定時間戳的請求、特殊結構化范圍的請求、或者幽靈層數量的請求(即:用于計算鄰域信息的邊界層)。此外,在請求通過的過程中,每一個濾波器都允許修改來自下游的請求。例如,一個無法通行流的濾波器(例如:流水線濾波器)可以忽略片段請求并要求整個數據。
## 24.2.4 渲染子系統
乍看VTK擁有一個簡潔的面向對象的渲染模型,這個模型由對應于構建三維場景的組件的類組成。例如:`vtkActor`是由與`vtkCamera`結合在一起的`vtkRenderer`來渲染的對象,一個`vtkRenderWindow`中可能具有多個`vtkRenderer`。該場景由一個或多個`vtkLight`提供光照。每個`vtkActor`的位置由`vtkTransform`控制,而該**演員**的外觀則通過`vtkProperty`來制訂。最后,該演員的幾何表示由`vtkMapper`來定義。**映射**在VTK中扮演著重要的角色,它們用于數據處理管線的結尾,同時向渲染系統提供接口。考慮下面的例子,我們在這個例子中大幅削減數據,然后將結果寫入一個文件,最后通過映射將其可視化,并實現與該結果的交互。
~~~
vtkOBJReader *reader = vtkOBJReader::New();
reader->SetFileName("exampleFile.obj");
vtkTriangleFilter *tri = vtkTriangleFilter::New();
tri->SetInputConnection(reader->GetOutputPort());
vtkQuadricDecimation *deci = vtkQuadricDecimation::New();
deci->SetInputConnection(tri->GetOutputPort());
deci->SetTargetReduction( 0.75 );
vtkPolyDataMapper *mapper = vtkPolyDataMapper::New();
mapper->SetInputConnection(deci->GetOutputPort());
vtkActor *actor = vtkActor::New();
actor->SetMapper(mapper);
vtkRenderer *renderer = vtkRenderer::New();
renderer->AddActor(actor);
vtkRenderWindow *renWin = vtkRenderWindow::New();
renWin->AddRenderer(renderer);
vtkRenderWindowInteractor *interactor = vtkRenderWindowInteractor::New();
interactor->SetRenderWindow(renWin);
renWin->Render();
~~~
這里只有一個演員,**渲染器**?和?**渲染窗**與映射一同創建,該映射將管線與渲染系統連接起來。也要注意`vtkRenderWindowInteractor`的引入,其實例捕捉鼠標與鍵盤事件,并將之轉換為攝像機操作以及其它動作。這一轉換過程由`vtkInteractorStyle`進行定義(下文還會就此詳述)。缺省情況下,許多實例和數據值被置于場景之后。例如:創建了恒等變換,以及一個單獨的光源和性質。
隨著時間的推進,該對象模型變得更加復雜。多數復雜性來源于開發派生類,這些派生類用于具體指定渲染過程中的某一方面。`vtkActor`目前是`vtkProp`的特例(prop正如戲臺上的道具),而且還有一群這樣的**道具**用于渲染二維的圖形疊加和文本,指定三維物體,甚至用于支持諸如體繪制或GPU實現等的高級渲染技術(見圖24.4)。
類似地,隨著VTK支持的數據模型的增長,各式用于實現數據于渲染系統接口的映射也隨之出現。另外一個發生顯著擴展的領域是變換的層次結構。這里最初只有一個簡易的4×4變換矩陣,現在變成了一個強悍的類層次結構,它們支持包括薄板樣條變換(thin-plate spline transformation)在內的非線性變換。例如:原始的`vtkPolyDataMapper`擁有支持具體設備的子類(如:`vtkOpenGLPolyDataMapper`)。近幾年,它已經被圖24.4所展示的一種稱為“painter”的復雜的圖形學管線所取代。

**圖24.4:顯示功能類**
painter的設計支持一大類渲染數據的技術,這些技術能夠組合起來以提供特殊的渲染效果。這種能力遠遠超出了于1994年最初實現的簡易的`vtkPolyDataMapper`。
可視化系統的另外一個重要方面是子系統的選擇。在VTK中,有一個類層次picker,被粗略地分成兩類對象:一類對象根據與硬件相關方法和軟件方法作比對來選擇`vtkProp`(例如:ray-casting);另一類對象在一次picker運算之后,提供不同水平的信息。例如:一些picker僅提供XYZ世界空間的位置,而不指明它們選擇了哪個`vtkProp`;其它picker不但給出所選的`vtkProp`,還給出組成用于定義道具幾何特征網格的具體的點或單元。
## 24.2.4 事件與交互
與數據交互是可視化的關鍵一環。在VTK中,交互的方式有多種。最簡單的方式是,用戶通過命令觀察事件并做出合適的反應(命令模式/觀察者模式)。`vtkObject`的所有子類都保有一列觀察者,這些觀察者將其自身寄存于對象中。寄存過程中,觀察者指出其感興趣的特殊事件,并加入關聯的命令,此命令將在事件發生時被調用。為了說明這一工作原理,來考慮下面的例子,該例中有一個帶有觀察者的濾波器(本例中是一個多邊形削減濾波器),此觀察者觀察三種事件:`StartEvent`,`ProgressEvent`,和`EndEvent`。這些事件在這三種情況下被該濾波器所調用:濾波器開始執行時,濾波器執行過程中(周期性調用),以及濾波器執行結束時。下面的代碼中,`vtkCommand`類擁有一個`Execute`方法,該方法用于打印與該類執行算法所花費時間有關的恰當信息。
~~~
class vtkProgressCommand : vtk Command
{
public:
static vtkProgressCommand *New() { return new vtkProgressCommand; }
virtual void Execute(vtkObject *caller, unsigned long, void *callData)
{
double progress = *(static_cast<double*>(callData));
std::cout << "Progress at " << progress << std::endl;
}
};
vtkCommand* pobserver = vtkProgressCommand::New();
vtkDecimatePro *deci = vtkDecimatePro::New();
deci->SetInputConnection( byu->GetOutputPort() );
deci->SetTargetReduction( 0.75 );
deci->AddObserver( vtkCommand::ProgressEvent, pobserver );
~~~
盡管這是交互的一種原始形式,它也是許多使用VTK的應用程序的基本要素。例如:上述的簡短代碼可以很容易地轉換、用于顯示并管理圖形界面中的進度條。這一命令/觀察者子系統也是VTK中三維掛件的核心,這些掛件是用于數據的請求、操縱以及編輯的復雜的交互性對象,下文將予以描述。
提到上面的例子,很重要的一點是,VTK中的事件都是預定義的,但是這里也為自定義事件開了后門。`vtkCommand`類定義了一組枚舉型事件(例如:上面例子中的`vtkCommand::ProgressEvent`)以及一個用戶事件。`UserEvent`只是一個整形數值,一般用作一組應用程序中自定義事件的起始抵消值。于是,`vtkCommand::UserEvent+100`可能是指一個VTK預定義的事件之外的某個事件。
從用戶的角度來看,一個VTK掛件可以看作是場景中的一個演員,只是用戶可以通過操縱句柄或者其它幾何特性(句柄操縱與幾何特性操縱均是基于前文所述之抓取功能——原文:picking functionality,即24.2.4一節中最后一段所述——的)來與之交互。與掛件的交互是很直觀的:用戶抓住球面句柄并將其移動,或者抓住一條直線并將其移動。然而,在場景的背后,事件被發送出去(例如:`InteractionEvent`),而一個編寫合理的應用程序就能夠觀察到這些事件,并采取恰當的行動。例如,它們通常由下面所給出的`vtkCommand::InteractorEvent`所觸發:
~~~
vtkLW2Callback *myCallback = vtkLW2Callback::New();
myCallback->PolyData = seeds; // streamlines seed points, updated on interaction
myCallback->Actor = streamline; // streamline actor, made visible on interaction
vtkLineWidget2 *lineWidget = vtkLineWidget::New();
lineWidget->SetInteractor(iren);
lineWidget->SetRepresentation(rep);
lineWidget->AddObserver(vtkCommand::InteractionEvent, myCallback);
~~~
實際上,VTK掛件由兩個對象構建而成:一個是`vtkInteractorObserver`的子類,另一個是`vtkProp`的子類。`vtkInteractorObserver`只是觀察渲染窗中的用戶交互(例如:鼠標事件和鍵盤事件)并處理之。這些操縱通常由突出顯示句柄,改變鼠標指針的外觀,以及變換數據等所組成,它們都會修改`vtkProp`的幾何特征。當然,這些掛件的特殊細節要求編寫子類來控制其行為的細微差別,目前系統中擁有50多個不同的掛件。
## 24.2.4 庫的總結
VTK是一個大型軟件工具箱。目前,系統由大約1500萬行代碼(包括注釋,但是不包括自動生成的包裹層軟件),約1000個C++類組成。為了管理系統的復雜度并減少構建和鏈接的時間,系統被分割放置在十幾個子路徑中。表24.1列出了這些子路徑,并簡要總結了這些庫所提供的功能。
| `Common` | VTK核心類 |
| `Filtering` | 用于管理管線數據流的類 |
| `Rendering` | 渲染,抓取,查看圖像,以及交互 |
| `VolumeRendering` | 體繪制技術 |
| `Graphics` | 三維幾何處理 |
| `GenericFiltering` | 非線性三維幾何處理 |
| `Imaging` | 圖像處理管線 |
| `Hybrid` | 同時要求使用圖形學和圖像處理功能的類 |
| `Widgets` | 復雜的交互 |
| `IO` | VTK的輸入和輸出 |
| `Infovis` | 信息可視化 |
| `Parallel` | 并行處理(控制器和通信器) |
| `Wrapping` | 對Tcl,Python以及Java的包裹的支持 |
| `Examples` | 內容廣泛、文檔良好的示例 |
**表24.1:VTK的子路徑**
## 24.3 回顧與展望
VTK一直是一個非常成功的系統。雖然第一行代碼于1993年寫出,但是目前,VTK仍然在不斷成長壯大、其開發速度也在不斷加快[2](http://en.wikipedia.org/wiki/BSD_licenses)。本節,我們將談談一些經驗和將來的挑戰。
### 24.3.1 成長管理
VTK發展歷程中,最令人驚嘆的方面之一就是項目的壽命。開發的速度歸因于若干主要原因:
* 新算法和功能被持續不斷地加入。例如,信息學子系統(Titan,最初由Sandia國立實驗室和Kitware軟件共同開發)是最近加入的一個重要的部分。額外的繪圖和渲染類也同時加入進來,還有新的科學數據類型功能。另外一個加入的重要部分是三維交互掛件。最后,基于GPU的渲染以及數據處理的持續演進正在催生新的VTK功能。
* VTK不斷增多的曝光和使用是一個自我保持的過程,該過程向社區加入了更多的使用者和開發者。例如,ParaView是最受歡迎的基于VTK的科學可視化應用程序,并且受到了高性能計算社區的高度重視。3D Slicer是主要的生物醫學計算平臺,它大部分也建立于VTK之上,并且每年受到數百萬美元的資助。
* VTK的開發過程持續演進。近年來,CMake、CDash、CTest、以及CPack等軟件過程工具已經集成到了VTK的構建環境中。最近,VTK的代碼庫已經遷移至Git和一個更為復雜的工作流。這些改進確保VTK保持科學計算社區內軟件開發的領先地位。
雖然成長是令人興奮的,確證軟件系統的建立,預測VTK的未來,但妥善的管理卻是極其困難的。因此,近期VTK將更多地專注于管理社區以及軟件的成長。為此,已經采取了若干措施。
首先,創立了正式的管理架構。創建了架構審查委員會(Architectural Review Board),來指導社區和技術的發展,專注于高層次的、戰略性的議題。VTK社區也正在組建一個由意見領袖組成的公認的團隊,來指導某些VTK子系統的技術開發。
其次,制定了關于更進一步使工具箱模塊化的計劃,尤其是應對由git引入的工作流功能,還認識到使用者和開發者一般都想在工作中使用工具箱中小的子系統,并且不想構建并鏈接整個包。此外,為了支持不斷成長的社區,對新的功能和子系統的支持是很重要的,即使它們并不一定是工具箱的核心部分。通過創建松散的、模塊化的一群模塊,在維持核心的穩定性的同時,適應外圍的大量代碼貢獻是可能的。
### 24.3.2 技術整合
除了軟件過程之外,在開發管線當中還有許多技術創新。
* 共同處理是這樣一種功能,可視化引擎被集成于仿真代碼之中,而且周期性地提取生成用于可視化的數據。這一技術極大地降低了完整解決方案數據的大的輸出數據量。
* VTK中的數據處理管線還是太復雜。正在尋求簡化和重構這些子系統的方法。
* 直接與數據交互的能力正在使用者中間流行。盡管VTK擁有一大票掛件,但是更多的交互技術正在不斷涌現,包括基于觸摸屏的方法和三維方法。交互技術將會繼續快速開發。
* 計算化學對于材料設計人員和工程師的重要性正在不斷提升。對化學數據的可視化與交互的功能正在加入VTK。
* VTK的渲染系統素來因其過于復雜而飽受詬病,這使它難以派生出新的類或者支持新的渲染技術。此外,VTK不直接支持場景圖概念,這同樣也是許多使用者要求過的功能。
* 最后是數據的新形式不斷出現。例如,在醫療領域,變分辨率的層次化體數據(如:具有局部放大的共焦顯微鏡影像)。
### 24.3.3 開放科學
最后,Kitware和更加廣泛的VTK社區決定加入Open Science。從務實的角度講,它一個這樣的方式,我們將傳播公開的數據、公開的發表、以及公開的源代碼——這是確保我們正在創建可重現的科學系統所必需的特征。雖然VTK一直以來都以開源和公開數據的系統的形式傳播,但是文檔過程卻一直缺乏。在擁有正式書籍[Kit10,SML06]的同時,還一直有各種非正式的方法來收集包括新的源碼在內的技術發表物。我們正在通過開發像是VTK Journal[3](http://www.gnu.org/copyleft/gpl.html)的新的發表機制來改善這種狀況,該期刊可以發表由文檔、源代碼、數據、以及有效的測試圖像組成的文章。它還實現了自動化的代碼審查(利用VTK的高質量的軟件測試過程)以及人對遞交文章的審查。
### 24.3.4 經驗教訓
雖然VTK很成功,但是還有許多事情我們沒有處理好:
* 設計的模塊性。我們在選擇我們的類的模塊性上做得不錯。例如,我們不會做類似為每個像素都創建一個對象的這種傻事,而是創建了高層次的`vtkImageClass`,它內部處理像素數據組成的數組。然而,在某些情況下,我們不得不將之重構為小的片段,并繼續這一過程。一個基本的例子就是數據處理管線。最初,數據管線是通過數據和算法對象的交互而隱式實現的。我們最終認識到我們得創建一種顯式的管線執行對象來協調數據與算法之間的交互,并且用于實現不同的數據處理策略。
* 遺漏的關鍵概念。我們曾經的最大遺憾就是沒有廣泛的利用C++的迭代器。在許多情況下,VTK中的數據的遍歷與科學編程語言Fortran十分類似。迭代器所提供的額外的靈活性本來可能對系統有很大幫助。例如,在處理局部區域的數據,或者僅僅是那些滿足某種迭代準則的數據時,這是極具優勢的。
* 設計上的問題。當然,有一長列非最優的設計決策。我們同數據處理管線斗爭,已經經歷了許多代,每次都設計得更好些。渲染系統也是很復雜的,并且難以從其中派生出新類。另外一個由VTK的最初概念所引起的挑戰是:我們將其看作是用于觀察數據的只讀可視化系統。然而,目前的客戶經常希望它能夠編輯數據,這就需要完全不同的數據結構。
像VTK這樣的開源系統的好處之一是許多這些錯誤能夠并且將會隨著時間而得以糾正。我們擁有一個積極的、有能力的開發社區,他們每天都在改進著這個系統,并且我們希望在可預見的將來,這一狀態能夠維持下去。
* * *
**腳注**
1.?[http://en.wikipedia.org/wiki/Opaque_pointer](http://en.wikipedia.org/wiki/Opaque_pointer).
2\. See the latest VTK code analysis at?[http://www.ohloh.net/p/vtk/analyses/latest](http://www.ohloh.net/p/vtk/analyses/latest).
3.?[http://www.midasjournal.org/?journal=35](http://www.midasjournal.org/?journal=35).
- 前言(卷一)
- 卷1:第1章 Asterisk
- 卷1:第3章 The Bourne-Again Shell
- 卷1:第5章 CMake
- 卷1:第6章 Eclipse之一
- 卷1:第6章 Eclipse之二
- 卷1:第6章 Eclipse之三
- 卷1:第8章 HDFS——Hadoop分布式文件系統之一
- 卷1:第8章 HDFS——Hadoop分布式文件系統之二
- 卷1:第8章 HDFS——Hadoop分布式文件系統
- 卷1:第12章 Mercurial
- 卷1:第13章 NoSQL生態系統
- 卷1:第14章 Python打包工具
- 卷1:第15章 Riak與Erlang/OTP
- 卷1:第16章 Selenium WebDriver
- 卷1:第18章 SnowFlock
- 卷1:第22章 Violet
- 卷1:第24章 VTK
- 卷1:第25章 韋諾之戰
- 卷2:第1章 可擴展Web架構與分布式系統之一
- 卷2:第1章 可擴展Web架構與分布式系統之二
- 卷2:第2章 Firefox發布工程
- 卷2:第3章 FreeRTOS
- 卷2:第4章 GDB
- 卷2:第5章 Glasgow Haskell編譯器
- 卷2:第6章 Git
- 卷2:第7章 GPSD
- 卷2:第9章 ITK
- 卷2:第11章 matplotlib
- 卷2:第12章 MediaWiki之一
- 卷2:第12章 MediaWiki之二
- 卷2:第13章 Moodle
- 卷2:第14章 NginX
- 卷2:第15章 Open MPI
- 卷2:第18章 Puppet part 1
- 卷2:第18章 Puppet part 2
- 卷2:第19章 PyPy
- 卷2:第20章 SQLAlchemy
- 卷2:第21章 Twisted
- 卷2:第22章 Yesod
- 卷2:第24章 ZeroMQ