《vtk9 book》 官方web版 第四章 - 可视化管线(1 / 2)


        在前一章中,我们使用简单的数学模型创建了图形图像,用于光照、视图和几何。光照模型包括环境光、漫反射和镜面效果。视图包括透视和投影的效果。几何被定义为一组静态的图形原语,如点和多边形。为了描述可视化过程,我们需要扩展我们对几何的理解,以包括更复杂的形式。我们将看到可视化过程将数据转换为图形原语。本章探讨了数据转换的过程,并为可视化系统开发了数据流模型。

4.1 概述


        可视化将数据转换为能够高效准确传达数据信息的图像。因此,可视化涉及转换和表示的问题。

        转换是将数据从其原始形式转换为图形原语,最终转换为计算机图像的过程。这是我们对可视化过程的工作定义。这种转换的一个示例是提取股票价格并创建一个 x-y 图,描述股票价格随时间变化的过程。

        表示包括用于描述数据的内部数据结构和用于显示数据的图形原语。例如,股票价格数组和时间数组是数据的计算表示,而 x-y 图是图形表示。可视化将计算形式转换为图形形式。

        从面向对象的观点来看,转换是功能模型中的过程,而表示是对象模型中的对象。因此,我们用功能模型和对象模型来描述可视化模型。

        数据可视化示例
        一个关于二次曲线的简单数学函数将澄清这些概念。这个函数

功能模型
        图4-1b中的功能模型展示了创建可视化的步骤。椭圆形块表示我们对数据执行的操作(过程),矩形块代表表示和提供对数据访问的数据存储(对象)。箭头表示数据移动的方向。指向块内部的箭头表示输入;从块中流出的数据表示输出。这些块也可能具有作为额外输入的本地参数。没有输入但创建数据的过程称为数据源对象,或者简称为源。没有输出但消耗数据的过程称为接收器(它们也被称为映射器,因为这些过程将数据映射到最终图像或输出)。具有输入和输出的过程称为过滤器。功能模型展示了数据如何在系统中流动。它还描述了各部分之间的依赖关系。为了使任何给定的过程正确执行,所有输入必须是最新的。这表明功能模型需要一种同步机制来确保生成正确的输出。

        功能模型展示了数据如何在系统中流动。它还描述了各部分之间的依赖关系。为了使任何给定的过程正确执行,所有输入必须是最新的。这表明功能模型需要一种同步机制来确保生成正确的输出。

        可视化模型
        在接下来的示例中,我们将经常使用功能模型的简化表示来描述可视化过程(图4-1c)。我们不会明确区分数据源、接收器、数据存储和过程对象。根据输入或输出的数量,我们将隐含地区分数据源和接收器。数据源将是没有输入的过程对象。接收器将是没有输出的过程对象。过滤器将是至少具有一个输入和一个输出的过程对象。中间数据存储将不会被表示。相反,我们将假定它们存在以支持数据流。因此,如图4-1c所示,轮廓对象生成的线数据存储(图4-1b中的轮廓对象)被合并为单个对象轮廓。我们使用椭圆形状来表示可视化模型中的对象。功能模型示例F(x,y,z)

        对象模型
        功能模型描述了我们可视化数据的流动方式,对象模型描述了哪些模块对其进行操作。但是系统中的对象是什么?乍一看,我们有两个选择(图4-2)。

                图4-2。对象模型设计选择。一个基本选择是将过程和数据存储合并到一个对象中。这是通常的面向对象的选择。另一个选择是创建单独的数据对象和过程对象。


        第一种选择将数据存储(对象属性)与过程(对象方法)合并到一个对象中。在第二种选择中,我们使用单独的对象来处理数据存储和过程。实际上还有第三种选择:这两种选择的混合组合。

        传统的面向对象方法(我们上面的第一选择)将数据存储和过程合并到一个对象中。这种观点遵循标准定义,即对象包含数据表示和操作数据的过程。这种方法的一个优点是,过程(即数据可视化算法)可以完全访问数据结构,从而实现良好的计算性能。但这种选择也有一些缺点。

        从用户的角度来看,过程通常被视为独立于数据表示。换句话说,过程在系统中自然被视为对象。例如,我们经常说我们想要“轮廓”数据,意思是创建与常数数据值相对应的线条或表面。对于用户来说,有一个单独的轮廓对象来处理不同的数据表示是方便的。

        我们必须重复算法实现。就像前面的轮廓示例一样,如果我们将数据存储和过程绑定到一个对象中,那么轮廓操作必须为每种数据类型重新创建。这会导致重复代码,即使算法的实现在功能上和结构上可能是相似的。修改这样的算法也意味着修改大量代码,因为它们跨多个对象实现。

        将数据存储和算法绑定在一起会导致复杂、依赖于数据的代码。一些算法可能比它们操作的数据复杂得多,具有大量的实例变量和复杂的数据结构。通过将许多这样的算法与数据存储结合起来,对象的复杂性大大增加,对象的简单含义变得模糊。

        第二种选择将数据存储和过程分开。也就是说,一组对象表示并提供对数据的访问,而另一组对象实现对数据的所有操作。我们的经验表明,这对用户来说是自然的,尽管对于面向对象纯粹主义者来说可能被认为是不寻常的。我们还发现,由此产生的代码简单、模块化,易于开发人员理解、维护和扩展。

        第二种选择的一个缺点是数据表示和过程之间的接口更加正式。因此,必须仔细设计接口,以确保良好的性能和灵活性。另一个缺点是,数据和过程之间的强分离会导致重复代码。也就是说,我们可能会实现重复算法的操作,这些操作不能被严格视为数据访问方法。这种情况的一个例子是计算数据的导数。这个操作不仅仅是简单的数据访问,所以严格来说它不属于数据对象方法。因此,为了计算导数,我们每次需要计算导数时都必须重复编写代码。(或创建一个函数或宏的过程库!)

        由于这些问题,我们在可视化工具包中使用混合方法。我们的方法最接近上述第二种选择,但我们选择了一小组关键操作,这些操作在数据对象内部实现。这些操作是根据我们实施可视化算法的经验确定的。这有效地将前两种选择结合起来,以获得最大的好处和最少的缺点。

4.2 可视化管线


        在数据可视化的背景下,图4-1c的功能模型被称为可视化管线或可视化网络。管线包括用于表示数据的对象(数据对象)、用于操作数据的对象(过程对象)以及数据流的指示方向(对象之间的箭头连接)。在接下来的文本中,我们经常使用可视化网络来描述特定可视化技术的实现。

数据对象


        数据对象表示信息。数据对象还提供方法来创建、访问和删除这些信息。除了通过正式对象方法,不允许直接修改数据对象表示的数据。这种能力是为过程对象保留的。还有其他方法可用于获取数据的特征。这包括确定数据的最小值和最大值,或确定对象中数据值的大小或数量。

        数据对象根据其内部表示而异。内部表示对数据的访问方法以及与数据对象交互的过程对象的存储效率或计算性能有重要影响。因此,根据效率和过程通用性的需求,可以使用不同的数据对象来表示相同的数据。

过程对象


        过程对象对输入数据进行操作,生成输出数据。过程对象要么从其输入中派生新数据,要么将输入数据转换为新形式。例如,一个过程对象可以从压力场中派生压力梯度数据,或者将压力场转换为恒定值压力等值线。过程对象的输入包括一个或多个数据对象以及本地参数,用于控制其操作。本地参数包括实例变量或关联,以及对其他对象的引用。例如,中心和半径是控制球体基元生成的本地参数。

        过程对象进一步分为源对象、过滤器对象或映射器对象。这种分类是基于对象是否启动、维护或终止可视化数据流。

        源对象与外部数据源接口或从本地参数生成数据。从本地参数生成数据的源对象称为过程对象。图4-1的前面例子使用过程对象为方程4-1的二次函数生成函数值。与外部数据接口的源对象称为阅读器对象,因为必须读取外部文件并转换为内部形式。源对象还可以与外部数据通信端口和设备接口。可能的例子包括模拟或建模程序,或数据采集系统来测量温度、压力或其他类似的物理属性。

        过滤器对象需要一个或多个输入数据对象,并生成一个或多个输出数据对象。本地参数控制过程对象的操作。计算每周股市平均数、将数据值表示为缩放图标,或对两个输入数据源执行联合集操作是过滤器对象的典型示例过程。

        映射器对象对应于功能模型中的终点。映射器对象需要一个或多个输入数据对象,并终止可视化管线数据流。通常,映射器对象用于将数据转换为图形基元,但它们也可以将数据写入文件或与另一个软件系统或设备接口。将数据写入计算机文件的映射器对象称为写入器对象。

4.3 管线拓扑


在本节中,我们描述如何连接数据和过程对象以形成可视化网络。

管线连接


        管线的元素(源、过滤器和映射器)可以以各种方式连接,以创建可视化网络。然而,在尝试组装这些网络时会出现两个重要问题:类型和多重性。

        类型意味着过程对象作为输入或生成的数据的形式或类型。例如,球体源对象可能将多边形或面片表示、隐式表示(例如,圆锥方程的参数)或在三维空间的离散表示中的占用值集合作为输出。映射器对象可能将多边形、三角带、线条或点几何表示作为输入。过程对象的输入必须正确指定才能成功操作。

图4-3。保持兼容数据类型。(a) 单类型系统无需进行类型检查。在多类型系统中,只有兼容的类型才能连接在一起。


        保持正确的输入类型有两种一般方法。一种方法是使用无类型或单类型系统进行设计。也就是说,创建一种数据对象类型,并创建仅在此一种类型上操作的过滤器(图4-3)。例如,我们可以设计一个通用的数据集DataSet,表示我们感兴趣的任何形式的数据,过程对象只会输入DataSets并生成DataSets。这种方法简单而优雅,但缺乏灵活性。通常,特别有用的算法(即过程对象)只会对特定类型的数据进行操作,泛化它们会导致表示或数据访问方面的效率低下。典型例子是表示结构化数据(如像素图或3D体积)的数据对象。因为数据是结构化的,所以可以轻松地按平面或线访问。然而,通常数据不是结构化的,通用表示将不包括此功能。

        保持正确的输入类型的另一种方法是设计类型化系统。在类型化系统中,只允许连接兼容类型的对象。也就是说,设计了多种类型,但在输入时进行类型检查以确保正确连接。根据具体的计算机语言,类型检查可以在编译、链接或运行时执行。尽管类型检查确保了正确的输入类型,但这种方法通常会导致类型数量激增。如果不小心,可视化系统的设计者可能会创建过多的类型,导致系统碎片化、难以使用和理解。此外,系统可能需要大量的类型转换器过滤器。(类型转换器过滤器仅用于将数据从一种形式转换为另一种形式。)走向极端,过多的类型转换会导致计算和内存浪费。

        多重性问题涉及允许的输入数据对象数量,以及过程对象运行期间创建的输出数据对象数量(图4-4)。我们知道,所有过滤器和映射器对象至少需要一个输入数据对象,但通常这些过滤器可以在一系列输入上顺序操作。一些过滤器可能自然需要特定数量的输入。实现布尔运算的过滤器就是一个例子。布尔运算(如并集或交集)是逐个对两个数据值实现的。然而,即使在这里,也可以将两个以上的输入定义为对每个输入递归应用该操作。

        图4-4。输入和输出的多重性。(a) 源、过滤器和映射器对象的定义。(b) 各种类型的输入和输出。

        我们需要区分输出的多重性是什么意思。大多数源和过滤器生成单个输出。当一个对象生成一个输出,并且被多个对象用作输入时,就会发生多重扇出。例如,当一个源对象用于读取数据文件,并且生成的数据用于生成数据的线框轮廓以及数据的轮廓时,就会发生这种情况(例如,图4-1a)。当一个对象生成两个或更多输出数据对象时,就会发生多个输出。多重输出的一个例子是将梯度函数的x、y和z分量作为不同的数据对象生成。多重扇出和多重输出的组合是可能的。

循环


        到目前为止描述的示例中,可视化网络没有循环。在图论中,这些被称为有向无环图。然而,在某些情况下,我们希望在可视化网络中引入反馈循环。可视化网络中的反馈循环允许我们将过程对象的输出引导到上游,影响其输入。

        图4-5显示了可视化网络中反馈循环的一个示例。我们用一组初始随机点播种速度场。使用探针过滤器来确定每个点的速度(以及可能的其他数据)。然后,每个点按照其相关矢量值的方向重新定位,可能使用比例因子来控制运动的幅度。该过程持续进行,直到点退出数据集或超过最大迭代次数。            

    

        图4-5。在可视化网络中的循环。这个例子实现了线性积分。样本点被创建来初始化循环过程。一旦进程开始,积分过滤器的输出就会代替样本点。

        我们将在下一节讨论可视化网络的控制和执行。然而,可以说,根据执行模型的设计,循环在可视化网络中可能会带来特殊问题。设计必须确保循环不会进入无限循环或非终止递归状态。通常,为了查看中间结果,循环的执行次数是有限的。但是,也可以重复执行循环以处理所需的数据。

执行管道


        到目前为止,我们已经看到了可视化网络的基本元素以及如何将这些元素连接在一起。在本节中,我们将讨论如何控制网络的执行。

        为了有用,可视化网络必须处理数据以生成所需的结果。导致每个过程对象运行的完整过程被称为网络的执行。

        通常情况下,可视化网络会被执行多次。例如,我们可能会更改过程对象的参数或输入。这通常是由于用户交互引起的:用户可能正在探索或系统地改变输入以观察结果。在对过程对象或其输入进行一次或多次更改之后,我们必须执行网络以生成最新的结果。

        为了获得最佳性能,可视化网络中的过程对象只有在其输入发生变化时才会执行。在一些网络中,如图4-6所示,我们可能有并行分支,如果对象在特定分支的本地被修改,则无需执行。在这个图中,我们看到对象D和下游对象E和F必须执行,因为D的输入参数发生了变化,而对象E和F依赖于D的输入。其他对象不需要执行,因为它们的输入没有变化。

        我们可以使用需求驱动或事件驱动的方法来控制网络的执行。在需求驱动方法中,只有在请求输出时才执行网络,并且只有影响结果的网络部分。在事件驱动方法中,对过程对象或其输入的每次更改都会导致网络重新执行。事件驱动方法的优势在于输出始终是最新的(除了计算的短暂时期)。需求驱动方法的优势在于大量的更改可以在没有中间计算的情况下进行处理(即,只有在收到数据请求后才处理数据)。需求驱动方法最大限度地减少了计算量,并导致更具交互性的可视化网络。

        图4-6。网络执行。并行分支无需执行。


        网络的执行需要过程对象之间的同步。我们希望只有在其所有输入对象都是最新的情况下才执行一个过程对象。通常有两种方式来同步网络执行:显式或隐式控制(图4-7)。

显式执行


        显式控制意味着直接跟踪网络的变化,然后基于显式依赖分析直接控制过程对象的执行。这种方法的主要特点是使用集中式执行器来协调网络执行。这个执行器必须跟踪每个对象的参数和输入的变化,包括网络拓扑的后续变化(图4-7a)。

        这种方法的优点是同步分析和更新方法是局部的,属于单个执行器对象。此外,我们可以创建依赖图,并在每次请求输出时执行数据流分析。如果我们希望将网络分解为并行计算或将执行分布到计算机网络中,这种能力尤为重要。

        显式方法的缺点是每个过程对象都依赖于执行器,因为执行器必须被告知任何变化。此外,如果网络执行是有条件的,由于执行与否取决于一个或多个过程对象的局部结果,执行器无法轻松控制执行。最后,集中式执行器可能在并行计算环境中产生不可扩展的瓶颈。

        显式方法可以是需求驱动或事件驱动。在事件驱动方法中,每当对象发生变化时(通常是响应用户界面事件),执行器都会收到通知,并立即执行网络。在需求驱动方法中,执行器会累积对象输入的变化,并根据显式用户需求执行网络。

图4-7. 显式和隐式网络执行。


        具有中央执行器的显式方法是许多商业可视化系统(如AVS、Irix Explorer和IBM Data Explorer)的典型特征。通常,这些系统使用可视化编程界面来构建可视化网络。这些系统通常在并行计算机上实现,并且分布计算的能力是至关重要的。

隐式执行


        隐式控制意味着只有当其本地输入或参数发生变化时,进程对象才会执行(图4-7)。隐式控制是使用两遍过程来实现的。首先,当从特定对象请求输出时,该对象会从其输入对象请求输入。这个过程会递归重复,直到遇到源对象为止。然后,如果源对象已更改或其外部输入已更改,那么源对象会执行。然后,递归解开,因为每个进程对象都会检查其输入并确定是否执行。这个过程会重复,直到最初请求的对象执行并终止该过程。这两个步骤称为更新和执行过程。

        隐式网络执行通常使用按需驱动控制来实现。在这里,只有在请求输出数据时网络执行才会发生。如果我们只是在遇到适当事件时(例如更改对象参数)每次请求输出,那么隐式网络执行也可以是事件驱动的。

        图4-8。条件执行的示例。根据范围,数据通过不同的颜色查找表进行映射。


        隐式控制方案的主要优势在于其简单性。每个对象只需要跟踪其内部修改时间。当请求输出时,对象将其修改时间与其输入的时间进行比较,并在过期时执行。此外,处理对象只需要了解其直接输入,因此不需要其他对象的全局知识(如网络执行者)。

        隐式控制的缺点在于它更难将网络执行分布到计算机上或实现复杂的执行策略。一种简单的方法是创建一个队列,按照网络执行的顺序执行处理对象(可能是分布式的)。当然,一旦引入中心对象回到系统中,隐式和显式控制之间的界限就变得模糊了。

条件执行


        可视化网络的另一个重要功能是条件执行。例如,我们可能希望根据数据范围的变化将数据映射到不同的颜色查找表中。可以通过在数据范围内分配更多颜色来放大小的变化,而通过在数据范围内分配少量颜色来压缩我们的颜色显示(图4-8)。

        可视化模型的条件执行(例如图4-1c所示)原则上是可以实现的。然而,在实践中,我们必须通过条件语言来补充可视化网络,以表达网络执行的规则。因此,可视化网络的条件执行是实现语言的一个功能。许多可视化系统是使用视觉编程风格进行编程的。这种方法基本上是一个可视化编辑器,直接构建数据流程图。使用这种方法很难表达网络的条件执行。或者,在过程式编程语言中,网络的条件执行是直接的。我们将讨论这个话题,直到“将所有内容整合在一起”。

4.5 内存和计算的权衡


        可视化是一个要求严格的应用程序,无论是在计算机内存还是计算要求方面。每秒传输量在1兆字节到1千兆字节的数量级是很常见的。许多可视化算法在计算上是昂贵的,部分原因是输入大小,但也是由于固有的算法复杂性。为了创建性能合理的应用程序,大多数可视化系统都有各种机制来权衡内存和计算成本。

图4-9。典型网络的静态与动态内存模型比较。当从对象*C*和*D*请求输出时,执行开始。在更复杂的动态模型中,我们可以通过执行更彻底的依赖分析来防止*B*执行两次。


静态和动态内存模型


        在执行可视化网络时,内存和计算之间的权衡是重要的性能问题。到目前为止提出的网络中,假定过程对象的输出始终可用于下游过程对象。因此,网络计算被最小化。然而,为了保留滤波器输出,计算机内存需求可能会很大。仅有几个对象的网络可能会占用大量计算机内存资源。

        另一种方法是仅在其他对象需要时保存中间结果。一旦这些对象完成处理,中间结果就可以丢弃。这种方法会导致每次请求输出时额外的计算。所需的内存资源大大减少,但计算量增加。就像所有的权衡一样,正确的解决方案取决于特定的应用程序和执行可视化网络的计算机系统的性质。

        我们将这两种方法称为静态和动态内存模型。在静态模型中,中间数据被保存以减少总体计算量。在动态模型中,中间数据在不再需要时被丢弃。当网络的小部分重新执行时,数据大小可被计算机系统管理时,静态模型效果最佳。当数据流量大或网络的同一部分每次执行时,动态模型效果最佳。通常,希望将静态和动态模型结合到同一网络中。如果整个网络的某个分支每次都要执行,那么存储中间结果就毫无意义,因为它们永远不会被重用。另一方面,我们可能希望在网络的分支点保存中间结果,因为数据更有可能被重用。图4-9显示了特定网络的静态和动态内存模型的比较。

        图4-10。参考计数以节省内存资源。每个滤波器*A*、*B*和*C*共享一个公共点表示。其他数据是每个对象本地的。

        正如这幅图所示,静态模型仅执行每个过程对象一次,存储中间结果。在动态模型中,每个过程对象在下游对象完成执行后释放内存。根据动态模型的实现,过程对象*B*可能执行一次或两次。如果进行了彻底的依赖分析,过程*B*将在对象*C*和*D*都执行后才释放内存。在简单的实现中,对象*B*将在*C*执行后,然后在*D*执行后释放内存。

参考计数和垃圾收集


        最小化内存成本的另一个有价值的工具是使用引用计数共享存储。使用引用计数,我们允许多个过程对象引用相同的数据对象,并跟踪引用的次数。例如,假设我们有三个对象*A*、*B*和*C*,它们构成如图4-10所示的可视化网络的一部分。还假设这些对象仅修改它们输入数据的一部分,保持指定x-y-z坐标位置的数据对象不变。然后为了节省内存资源,我们可以允许每个过程对象的输出引用表示这些点的单个数据对象。被更改的数据保留在每个滤波器中,不共享。只有当引用计数降为零时,对象才会被删除。

        垃圾收集是另一种不太适合于可视化应用程序的内存管理策略。垃圾收集过程是自动的;它试图回收永远不会被运行应用程序访问的对象使用的内存。虽然由于其自动化性质而方便,但总的来说,垃圾收集引入了开销,可能会在不适当的时间(在交互过程中)无意中导致软件执行中断。然而,更令人担忧的是,释放的未使用内存可能要在最后一次引用该内存后的一段时间内才能被系统回收,在可视化管道中,这些内存可能太大,不能长时间保留。也就是说,在某些应用程序中,如果滤波器中的内存使用没有立即释放,下游滤波器可能没有足够的内存资源可供成功执行。

4.6 高级可视化管道模型


        前面的章节提供了一个有用的可视化管道模型的一般框架。然而,复杂应用程序通常需要几种高级功能。这些功能是由先前描述的简单设计的不足所驱动的。开发高级模型的主要驱动因素包括:处理未知数据集类型,管理包括处理数据片段在内的复杂执行策略,以及扩展可视化管道以传播新信息。这些问题将在接下来的三个部分中讨论。

处理未知数据集类型


        存在数据文件和数据源,文件或源表示的数据集类型在运行时是未知的。例如,考虑一个通用的VTK阅读器,可以读取任何类型的VTK数据文件。这样的类很方便,因为用户无需关心数据集的类型,而是可以设置一个处理找到的任何类型的数据的单个管道。如图4-3所示,如果系统是单一数据集类型,这种方法效果很好,但实际上,由于性能/效率问题,通常存在许多不同类型的数据在可视化系统中。图4-3中显示的另一种选择是强制类型检查。但是,在像上面描述的阅读器示例这样的情况下,无法在编译时强制执行类型检查,因为类型是由数据确定的。因此,必须在运行时执行类型检查。

        多数据集类型可视化系统的运行时类型检查要求在过滤器之间传递的数据是一个通用数据集容器(即,它看起来像一个类型,但包含实际数据和确定其数据类型的方法)。运行时类型检查具有灵活性的优点,但折衷之处在于直到程序执行之前管道可能无法正确执行。例如,可以设计一个通用管道,可以处理结构化数据(请参阅第5章中的“数据集类型”),但数据文件可能包含非结构化数据。在这种情况下,管道将无法在运行时执行,产生空输出。因此,设计用于处理任何类型数据的管道必须小心组装,以创建健壮的应用程序。

扩展数据对象表示


        如本章前面描述的,管道由过程对象操作的数据对象组成。此外,由于过程对象与它们操作的数据对象是分开的,因此必然存在一种预期的接口,通过该接口对象交换信息。定义这个接口的副作用是巩固数据表示,意味着在不修改相应接口的情况下很难扩展它,因此所有依赖于接口的类(数量众多)都会受到影响。幸运的是,通常发生变化的不是基本数据表示(这些通常已经得到很好的建立),而是与数据集本身相关的元数据发生变化。(在可视化的背景下,元数据是描述数据集的数据。)虽然通过创建新类来表示新数据集是可行的(因为新数据集类型的增加并不频繁);但是元数据的多样性阻止了创建新类,因为数据类型的激增以及可能对编程接口的更改会对可视化系统的稳定性产生不利影响。因此,需要一个支持元数据的通用机制。将元数据打包到一个通用容器中,该容器既包含数据集又包含元数据,这是一个明显的设计,并且与前一节描述的设计兼容。

        元数据的示例包括时间步信息、数据范围或其他数据特征、获取协议、患者姓名和注释。在可扩展的可视化管道中,特定的数据读取器(或其他数据源)可能会读取这些信息,并将其与生成的输出数据关联起来。虽然许多过滤器可能会忽略元数据,但它们可以配置为沿着管道传递信息。另外,管道的终点(或映射器)可能会请求通过管道传递特定的元数据,以便可以适当地处理它。例如,映射器可能会请求注释,并在可用时将其放置在最终图像上。

        管理复杂的执行策略 在现实世界的应用中,迄今为止描述的管道设计可能无法充分支持复杂的执行策略,或者在数据量变大时可能无法成功执行。在接下来的章节中,我们将通过考虑替代设计可能性来解决这些问题。

        大数据。相对于可视化管道的先前讨论假设特定数据集的大小不会超过计算机系统的总内存资源。然而,随着现代数据集大小推进到千兆字节甚至拍字节范围,典型的台式计算机系统无法处理这样的数据集。因此,在处理大数据时必须采取替代策略。其中一种方法是将数据分成片段,然后通过可视化管道流式传输这些片段[Martin2001]。图4-11说明了如何将数据集分割成片段

Figure 4-11. Dividing a sphere into a piece (red) with ghost level cells and points (blue and green).

  图4-11。将一个球分成一个带有幽灵级单元和点(蓝色和绿色)的部分(红色)。

        通过可视化管道流式传输数据有两个主要好处。第一,通常无法放入内存的可视化数据可以被处理。第二,可视化可以以更小的内存占用运行,从而产生更高的缓存命中率,几乎不需要交换到磁盘。要实现这些好处,可视化软件必须支持将数据集分割成片段并正确处理这些片段。这要求数据集和操作数据集的算法是可分离的、可映射的,并且结果不变,如下所述[Law99]。

        可分离。数据必须是可分离的。也就是说,数据可以分成片段。理想情况下,每个片段应在几何、拓扑和/或数据结构上是连贯的。数据的分离应简单有效。此外,这种体系结构中的算法必须能够正确处理数据片段。

        可映射。为了控制数据通过管道的流式传输,我们必须能够确定生成给定输出部分所需的输入数据的部分。这使我们能够控制数据通过管道的大小,并配置算法。

        结果不变。结果应该独立于片段数量,也独立于执行模式(即单线程还是多线程)。这意味着正确处理边界并开发算法,这些算法跨越可能在边界上重叠的片段是多线程安全的。

        将数据分割成片段相对简单,如果数据是结构化的,即拓扑规则的(见第5章“数据集类型”)。这种数据集可以通过在规则的x-y-z细分的立方域中定义矩形范围来拓扑描述(见图5-7(a)-(c))。然而,如果数据是非结构化的(例如三角形或多边形的网格),那么指定片段就会变得困难。通常,非结构化范围是通过将相邻数据(例如单元)分组成片段来定义的,然后使用N of M表示法,其中N是第n个片段,总共有M个片段。片段的确切组织结构未指定,取决于特定应用程序和用于分组数据的算法。

        为了满足结果不变的第三要求,处理片段还需要生成边界数据或虚拟层。当需要从片段的邻居获取信息以执行计算时,边界信息是必要的。例如,梯度计算或边界分析(例如,我有单元面邻居吗?)需要一个边界信息级别。在罕见情况下,可能需要两个或更多级别。图4-11说明了与球体中心红色片段对应的边界单元和点。

        最后,应该指出,将数据分割成片段进行流式传输的能力正是数据并行处理所需的能力。在这种方法中,数据被细分并发送到不同的处理器以并行操作。执行某些计算可能还需要边界信息。并行处理的复杂性在于必须将数据通信到处理器(在分布式计算的情况下)或必须使用互斥(即互斥)来避免同时写操作。因此,流式传输和并行处理是大数据计算中使用的互补技术。

        复杂的执行策略。在许多情况下,图4-7中简单的执行模型不适用于复杂的数据处理任务。例如,如前一节所讨论的,当数据集变得太大而无法适应内存时,或者使用并行计算时,需要流式数据的复杂执行策略。在某些情况下,事件驱动(参见“执行管道”)或“推”管道(即接收数据并将数据推送到管道进行处理的管道)可能更可取。最后,还存在层次化数据结构,如多块或自适应网格细化(AMR)[Berger84]。在管道中处理这些数据集需要层次遍历,因为过滤器处理网格中的每个块(这是可视化领域的一个高级研究课题,本书的本版中没有涵盖)。

        满足这些要求意味着执行模型必须得到扩展。因此,我们将在下一节重新审视面向对象设计。

        面向对象设计再审视。图4-2说明了与可视化对象模型设计相关的两种选择。第一种选择是将数据和数据操作合并到单个对象中,这是一种典型的面向对象设计模式,但被舍弃了。第二种选择是创建由两个类组成的设计——数据对象和处理对象——然后将它们组合成可视化管道。虽然这种第二种策略对于简单的管道效果很好,但是当引入复杂的执行策略时,这种设计开始出现问题。这是因为执行策略必然且隐含地分布在数据对象和处理对象之间;没有明确的机制来实现特定的策略。因此,这种设计存在问题,因为无法在不修改数据对象和处理对象的接口的情况下引入新的策略。良好的设计要求执行策略与数据对象和处理对象分离。这种设计的好处包括减少数据对象和处理对象的复杂性,封装执行策略,执行运行时类型检查(参见“处理未知数据集类型”)甚至管理元数据(参见“扩展数据对象表示”)。

        随着执行模型变得更加复杂,执行策略被分离为独立的类,与数据对象和处理对象分开。 

        高级设计重新引入了执行者的概念(参见“执行管道”)。但是,该设计与图4-7中的设计有所不同。正如该图所示,单个集中的执行者将依赖关系引入管道中,随着管道复杂性的增加或并行处理应用程序的出现,这种设计将无法扩展。在高级设计中,我们假设多个执行者,通常每个过滤器一个。在某些情况下,执行者可以控制多个过滤器。如果过滤器之间存在相互依赖关系或需要复杂的执行策略,则这种设计特别有用。不同类的执行者可以实现不同的执行策略,例如需求驱动的流式管道就是一种策略。其他重要的类包括协调对复合数据集上的过滤器执行的执行者。

图4-12。随着执行模型变得更加复杂,执行策略被作为独立的类与数据和处理对象分离。

图4-12是对执行者及其与数据和处理对象的关系的高层视图。在“管道设计与实现”中,该设计被更详细地探讨。

4.7 编程模型


        可视化系统天生就是为人类交互而设计的。因此,它们必须易于使用。另一方面,可视化系统必须能够迅速适应新数据,并且必须足够灵活,以允许快速数据探索。为了满足这些需求,人们开发了各种编程模型。

可视化模型


        在最高层次是应用程序。可视化应用程序具有精心定制的用户界面,专门针对某个应用领域,例如流体流可视化。应用程序最易于使用,但最不灵活。由于固有的后勤问题,用户很难或不可能将应用程序扩展到新领域。商业化的即插即用可视化软件通常被认为是应用软件。

        在光谱的另一端是编程库。传统的编程库是一组在特定库数据结构上操作的过程集合。通常,这些库是用传统的编程语言(如C或FORTRAN)编写的。它们提供了很大的灵活性,并且可以很容易地与其他编程工具和技术结合使用。编程库可以通过添加用户编写的代码来扩展或修改。不幸的是,有效使用编程库需要熟练的程序员。此外,非图形/可视化专家不能轻松使用编程库,因为没有关于如何正确组合(或排序)程序的概念。这些库还需要广泛的同步方案来控制执行,因为输入参数发生变化。

        许多可视化系统位于这两个极端之间。这些系统通常使用视觉编程方法来构建可视化网络。基本思想是提供图形工具和模块或处理对象的库。模块可以根据输入/输出类型约束连接,使用简单的图形布局工具。此外,用户界面工具允许将接口小部件与对象输入参数关联起来。通过内部执行执行者,系统执行通常对用户透明。

其他可视编程模型


        还有另外两种值得一提的图形和可视化编程模型,即场景图和电子表格模型。

        场景图通常用于3D图形系统,如Open Inventor [Wernecke94]。场景图是表示对象或节点的无环树结构,其顺序由树布局定义。节点可以是几何体(称为形状节点)、图形属性、变换、操纵器、灯光、摄像机等,定义了完整的场景。父/子关系控制了属性和变换如何应用于节点在渲染时,或对象如何与场景中的其他对象相关联(例如,灯光照射在哪些对象上)。场景图不用于控制可视化管道的执行,而是用于控制渲染过程。场景图和可视化管道可以在同一应用程序中一起使用。在这种情况下,可视化管道是形状节点的生成器,场景图控制场景的渲染,包括形状。

        场景图因其能够紧凑且图形化地表示场景而在图形界中得到广泛应用。此外,场景图最近在Web工具(如VRML和Java3D)中的应用也变得流行起来。有关更多信息,请参阅第11章“Web上的可视化”。

        另一种最近引入的视觉编程技术是Levoy [Levoy94]的电子表格技术。在电子表格模型中,我们将操作排列在类似于常见电子会计电子表格的规则网格上。该网格由行和列组成的单元格组成,其中每个单元格表示为其他单元格的计算组合。通过使用简单的编程语言为每个单元格表达添加、减去或执行其他更复杂的操作。计算的结果(即可视化输出)显示在单元格中。电子表格方法的最新扩展示例是VisTrails [Bavoil2005],这是一个通过简化可视化管道的创建和维护以及优化管道执行来实现交互式多视图可视化的系统。VisTrails的另一个好处是它跟踪可视化管道的更改,因此轻松创建广泛的设计研究。

        尽管视觉编程系统取得了广泛的成功,但它们存在两个缺点。首先,它们不像应用程序那样定制,需要大量编程(尽管是可视化的)才能实现。其次,视觉编程对于详细控制过于有限,因此构建复杂的低级算法和用户界面是不可行的。所需的是提供了可视系统的“模块化”和自动执行控制以及编程库的低级编程能力的可视化系统。面向对象系统有潜力提供这些功能。精心设计的对象库提供了可视系统易用性和编程库控制的优点。这是本文所描述的主要目标。

4.8 数据接口问题


        在本文中,您可能想知道如何将可视化管道应用于自己的数据。答案取决于您拥有的数据类型、编程风格偏好和所需的复杂性。尽管我们尚未描述特定类型的数据(我们将在下一章中进行描述),但在将数据与可视化系统接口时,您可能希望考虑两种一般方法:编程接口和应用程序接口。

编程接口


        最强大和灵活的方法是直接编写应用程序以读取、写入和处理数据。使用这种方法几乎没有限制。不幸的是,在像VTK这样的复杂系统中,这需要一定水平的专业知识,可能超出了您的时间预算(如果您有兴趣使用VTK进行此操作,您将需要熟悉系统中的对象。您还需要参考Doxygen生成的手册页面 - 在http://www.vtk.org或CD-ROM上在线。配套文本《VTK用户指南》也会有所帮助)。

        通常需要编程接口的应用包括与系统当前不支持的数据文件接口或生成合成数据(例如,通过数学关系)的情况,其中没有可用的数据文件。有时,直接编写数据以形成程序,然后执行程序以可视化结果是有用的。(这正是许多VTK示例所做的事情)。

        总的来说,编写像VTK这样的复杂系统是一项困难的任务,因为一开始需要学习曲线。然而,有更简单的方法来接口数据。虽然需要熟练的开发人员才能创建复杂的应用程序,但像VTK这样的面向对象工具包的优点在于它提供了许多与常见数据形式接口的部件。因此,专注于那些导入和导出数据的对象是开始与数据接口的良好起点。在VTK中,这些对象称为读取器、写入器、导入器和导出器。

        文件接口(读取器/写入器)。在本章中,我们看到读取器是源对象,写入器是映射器。从实际角度来看,这意味着读取器将从文件中摄取数据,创建一个数据对象,然后将对象传递到管道进行处理。类似地,写入器将摄取一个数据对象,然后将数据对象写入文件。因此,如果VTK支持您的格式,读取器和写入器将很好地与您的数据接口,您只需要读取或写入单个数据对象。如果系统不支持您的数据文件格式,您将需要通过上面描述的通用编程接口与您的数据接口。或者,如果您希望与一组对象进行接口,您可能需要查看下一节中描述的导入器或导出器对象是否存在来支持您的应用程序。

        读取器的示例包括vtkSTLReader(读取立体光刻文件)和vtkBYUReader(读取MOVIE.BYU格式数据文件)。类似地,对象vtkSTLWriter和vtkBYUWriter可用于写入数据文件。要查看VTK支持哪些读取器和写入器,请参阅VTK用户指南或参考http://www.vtk.org上的当前Doxygen手册页面。

        文件接口(导入器/导出器)。导入器和导出器是系统中读取或写入由多个对象组成的数据文件的对象。通常,导入器和导出器用于保存或恢复整个场景(即光源、摄像机、演员、数据、变换等)。当执行导入器时,它会读取一个或多个文件,并可能创建多个对象。例如,在VTK中,vtk3DSImporter导入一个3D Studio文件并创建一个渲染窗口、渲染器、光源、摄像机和演员。类似地,vtkVRMLExporter在给定VTK渲染窗口的情况下创建一个VRML文件。VRML文件包含摄像机、光源、演员、几何、变换等,间接由提供的渲染窗口引用。

        在可视化工具包中,有几个导入器和导出器。要查看VTK支持哪些导入器和导出器,请参阅VTK用户指南。您可能还希望查看http://www.vtk.org上的当前Doxygen手册页面。如果您正在寻找的导出器不存在,您将需要使用编程接口开发自己的导出器。

 4-13显示了从3D Studio模型创建并保存为Renderman RIB 文件的图像。 4-13显示了从3D Studio模型创建并保存为Renderman RIB 文件的图像。

应用程序界面


                大多数用户通过使用现有应用程序来与其数据进行接口。用户不需要编写管道或编写自己的读取器和写入器,而是获取一个适合其特定可视化需求的应用程序。然后,为了与其数据进行接口,用户只需识别可以成功处理数据的读取器、写入器、导入器和/或导出器。在某些情况下,用户可能需要修改用于生成数据的程序,以便以标准数据格式导出数据。使用现有应用程序的优势在于用户界面和管道是预先编程的,确保用户可以专注于其数据,而不是花费大量资源编写可视化程序所需的资源。使用现有应用程序的缺点在于通常缺少必要的功能,而且应用程序通常缺乏通用工具提供的灵活性。

选择正确的应用程序并不总是简单的。应用程序必须支持正确的数据集类型,并支持适当的渲染设备,例如在大型显示器[Humphreys99]上生成图像或在Cave[CruzNeira93]环境中生成图像。在某些情况下,需要用户交互,并且对并行处理或数据处理能力的需求进一步复杂化了选择。例如,虽然像ParaView(图4-14a)这样的通用工具可以用于可视化大多数类型的数据,包括支持大数据和并行计算,但像VolView(图4-14b)这样的专用工具可能更适合特定类型的任务,例如查看图中显示的医学数据。用户必须熟悉可视化过程,才能成功选择适合其数据的正确应用程序。

4.9 将所有内容整合在一起


        在前面的章节中,我们涵盖了与可视化模型相关的各种主题。在本节中,我们描述了我们在可视化工具包中采用的特定实现细节。

#   import from 3d Studio vtk3DSImporter importer
importer ComputeNormalsOn
importer SetFileName "$VTK_DATA_ROOT/Data/iflamigm.3ds"
importer Read
#   export to rib format

vtkRIBExporter exporter
exporter SetFilePrefix importExport
exporter SetRenderWindow [importer GetRenderWindow]
exporter BackgroundOn
exporter Write
程序语言实现


        可视化工具包是用过程语言C++实现的。自动封装技术创建了与Python、Tcl和Java解释性编程语言的语言绑定[Kin03]。类库包含数据对象、过滤器(即处理对象)和执行器,以便构建可视化应用程序。提供了各种支持抽象的超类,用于派生新对象,包括数据对象和过滤器。可视化管道旨在直接连接到前一章描述的图形子系统。这种连接是通过VTK的映射器实现的,它们是管道的终点,并与VTK的演员接口。

        可以(而且已经)使用提供的类库实现视觉编程界面。然而,对于实际应用程序,过程语言实现提供了几个优势。这包括条件网络执行和循环的简单实现,易于与其他系统接口,以及能够创建具有复杂图形用户界面的自定义应用程序。VTK社区已经从工具包中创建了几个视觉编程和可视化应用程序。其中许多作为开源软件提供(例如,paraview.org上的ParaView)或作为商业应用程序提供(例如,www.volview.com上的VolView)。

Figure 4-14a. ParaView parallel visualization application.

图4-14b。VolView体积渲染应用程序。
图4-14。选择适当的可视化应用程序取决于它必须支持的数据集类型、所需的交互技术、渲染能力以及对大数据的支持,包括并行处理。虽然上述两个应用程序都是使用VTK可视化工具包构建的,但它们提供非常不同的用户体验。ParaView(paraview.org)是一个通用的可视化系统,可以在分布式、并行环境中处理大数据(以及在单处理器系统上),并且能够在Cave或平铺显示上显示。VolView(volview.com)专注于体积和图像数据,并使用多线程和复杂的细节级别方法来实现交互性能。

流水线设计与实现


        可视化工具包实现了一个通用执行机制。过滤器分为两个基本部分:算法对象和执行对象。一个算法对象,其类派生自vtkAlgorithm,负责处理信息和数据。一个执行对象,其类派生自vtkExecutive,负责告诉算法何时执行以及要处理的信息和数据。过滤器的执行组件可以独立于算法组件创建,从而实现自定义的管道执行机制,而无需修改核心VTK类。

        过滤器产生的信息和数据存储在一个或多个输出端口中。一个输出端口对应于过滤器的一个逻辑输出。例如,一个生成彩色图像和相应的二进制掩模图像的过滤器将定义两个输出端口,每个端口保存其中一个图像。与每个输出端口相关的管道信息存储在vtkInformation的一个实例中。输出端口的数据存储在从vtkDataObject派生的类的一个实例中。

        过滤器消耗的信息和数据通过一个或多个输入端口检索。一个输入端口对应于过滤器的一个逻辑输入。例如,一个标志过滤器将为标志本身定义一个输入端口,并为定义标志位置的另一个输入端口。输入端口存储引用其他过滤器的输出端口的输入连接;这些输出端口最终为过滤器提供信息和数据。每个输入连接提供一个数据对象及其对应的信息,这些信息是从建立连接的输出端口获取的。由于连接是通过逻辑端口存储而不是通过这些端口流过的数据,所以在建立连接时无需知道数据类型。这在创建源是读取器的管道时特别有用,因为读取文件之前不知道其输出数据类型(参见“管道连接”和“处理未知数据集类型”)。

        图4-15。VTK中实现的隐式执行过程描述。Update()方法是通过actor的Render()方法启动的。数据通过RequestData()方法返回到mapper。连接过滤器和数据对象的箭头表示Update()过程的方向。
要理解VTK管道的执行,从几个不同的视角看待这个过程是很有用的。请注意,以下每个图并不完全准确,而是作为描述过程重要特征的描绘而呈现。

        图4-15展示了VTK执行过程的简化描述。通常,管道的执行是由mapper的Render()方法调用触发的,通常是响应于与vtkActor关联的Render()方法调用(后者又从渲染窗口接收到调用)。接下来,在mapper的输入上调用Update()方法(导致一系列方法调用请求信息和数据)。最终,数据必须被计算并返回给发起请求的对象,即mapper。RequestData()方法实际上执行管道中的过滤器,并生成输出数据。请注意数据流的方向——在这里我们定义数据流向下游方向,而Update()调用的方向为上游方向。

        图4-16。算法、执行器和端口构成过滤器的逻辑关系。执行器负责管理算法的执行,并协调通过管道传递的信息请求。端口对应于逻辑、独立的输入和输出。
接下来的图,图4-16,展示了执行器和算法之间的关系,它们配对形成一个过滤器。这种过滤器的视图独立于管道,并包含有关算法接口的所有信息,即输入和输出的数量和可用性。最后,图4-17展示了过滤器之间的连接。请注意,输出数据对象并没有直接连接到输入连接。相反,下游过滤器的输入连接与上游过滤器的输出端口相关联。数据对象与输入端口的分离意味着数据类型检查可以推迟到运行时,当消费过滤器从数据生产者请求数据时。因此,生产者可以生成不同类型的数据(例如,它是一个生成不同数据类型的读取器),只要消费者支持这些不同的数据类型,管道就会执行而不会出错。

连接管道对象


        这引导我们到一种方法,即连接过滤器和数据对象以形成可视化管道。从前面的图中可以看出,可视化工具包管道架构旨在支持多个输入和输出。在实践中,您会发现大多数过滤器和源实际上生成单个输出,而过滤器接受单个输入。这是因为大多数算法往往是单输入/输出的。当然,也有例外情况,我们将很快描述其中一些。然而,首先我们想提供一个与VTK管道架构演变相关的简短历史课程。这个课程具有教育意义,因为它揭示了管道设计随着新需求的响应而发展的过程。

        VTK 5.0之前。在VTK的早期版本(即VTK 5.0之前),可视化管道架构如图4-15所示,准确地描绘了。在这个图中,显示了如何连接过滤器和数据对象以形成可视化网络,输入数据由Input实例变量表示,并使用SetInput()方法设置。输出数据由Output实例变量表示,并使用GetOutput()方法访问。为了连接过滤器,通常使用以下C++语句

filter2->SetInput(filter1->GetOutput()); //VTK5.0之前


        与兼容类型的filter1和filter2过滤器对象一起使用。在这种设计中,执行编译时类型检查(即,C++编译器将强制执行正确的类型)。显然,这意味着连接在一起产生未知类型输出的过滤器是有问题的。这种设计仍然存在其他一些问题,其中许多问题在前面已经提到,但在这里总结,以激励使用更新的管道架构。

        图4-17。端口和连接的逻辑关系 输入端口可能有多个关联的连接。在某些过滤器中可能存在多个连接,比如附加过滤器,其中一个逻辑输入端口代表要“附加”在一起的所有数据,每个输入由不同的连接表示。
                旧设计不支持延迟数据集类型检查。很难支持任意的读取器类型或能够生成不同类型输出的过滤器。

更新和管理管道执行的策略被隐式地嵌入到处理对象和数据对象中。随着策略变得更加复杂,或需要改变,这就需要修改数据和/或处理对象。

        在旧设计中,在更新过程中很难中止管道执行。此外,无法集中错误检查;每个过滤器都必须进行一些检查,从而重复代码。

        将元数据引入管道需要更改数据和处理对象的API。希望支持读取器将元数据添加到数据流,并使管道中的过滤器检索它,而无需修改API。

        出于这个原因,以及与并行处理相关的其他原因,原始的VTK管道设计被重新设计。尽管过渡是困难的,但如果软件系统要随着技术进步而改变和发展,这样的变化通常是必要的。

        VTK 5.0及以后。虽然VTK 5.0仍支持SetInput()/GetOutput()的使用,但在图4-16和图4-17中不建议使用。相反,应该使用更新的管道架构。参考图4-17,我们使用连接和端口来配置VTK的可视化管道:

filter2->SetInputConnection(filter1->GetOutputPort()); //VTK 5.0


        您可能已经猜到了这种方法如何扩展到多个输入和多个输出。让我们看一些具体的例子。vtkGlyph3D是一个接受多个输入并生成单个输出的过滤器的示例。vtkGlyph3D的输入由Input和Source实例变量表示。vtkGlyph3D的目的是将Source中定义的数据几何复制到Input定义的每个点。根据Source数据值(例如标量和矢量),几何将被修改。要在C++代码中使用vtkGlyph3D对象,您可以执行以下操作:

glyph = vtkGlyph3D::New();
glyph->SetInputConnection(foo->GetOutputPort());
glyph->SetSourceConnection(bar->GetOutputPort());...


        其中foo和bar是返回适当类型输出的过滤器。vtkExtractVectorComponents类是一个具有单个输入和多个输出的过滤器的示例。此过滤器将3D矢量的三个分量提取为单独的标量分量。其三个输出可在输出端口0、1和2上使用。以下是过滤器的示例用法:

vz = vtkExtractVectorComponents::New();
foo = vtkDataSetMapper::New();
foo->SetInputConnection(vz->GetOutputPort(2));


        还有其他一些具有多个输入或输出的特殊对象可用。一些较为显著的类包括vtkMergeFilter、vtkAppendFilter和vtkAppendPolyData。这些过滤器将多个管道流合并并生成单个输出。但请注意,虽然vtkMergeFilter具有多个输入端口(即不同的逻辑输入),但vtkAppendFilter只有一个逻辑输入,但假定对该输入进行了多个连接。这是因为在vtkMergeFilter的情况下,每个输入都有一个独特且单独的目的,而在vtkAppendFilter中,所有输入具有相同的含义(即,只是要附加在一起的列表中的另一个输入)。以下是一些代码片段:

merge = vtkMergeFilter::New();
merge->SetGeometryConnection(foo->GetOutputPort());
merge->SetScalarsConnection(bar->GetOutputPort());


以及

append = vtkAppendFilter::New();
append->AddInputConnection(foo->GetOutputPort());
append->AddInputConnection(bar->GetOutputPort());


请注意AddInputConnection()方法的使用。此方法将连接添加到连接列表中,而SetInputConnection()方法会清除列表并指定到端口的单个连接。

待续。。。

  • 10
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小道士写程序

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值