基于IoC模式的D3D渲染工作流引擎设计
2010-01-16熊伟
熊 伟
(武汉大学计算机学院,湖北武汉430079)
随着DirectX的发展和成熟,DirectX已经成为当今游戏和多媒体开发的首选技术。然而,DirectX的功能都是以COM组件的形式提供的,应用程序更多地采用了面向过程的设计思想,这种方式使得应用程序的业务逻辑和DirectX的API混合在一起,破坏了系统的模块化,增大了模块间的耦合度,系统也不易于维护和扩展。
本文提出一个基于IoC模式的D3D(DirectX中的Direct3D)渲染工作流引擎的设计方案,该方案将剥离应用程序逻辑和D3D实现之间的关系,采用工作流的设计思想来封装D3D的API,并使用IoC容器来创建构件和组装其依赖关系,最后通过实例来验证本引擎的可行性和实现效果。
1 3D渲染过程简介
3D渲染过程就是三维物体的成像过程。三维物体通过建模的微分方法,根据不同的精确程度,将表面切分为多个三角形面,从而将表面的绘制转化为三角形面的绘制。渲染由众多三角形面组成的三维场景,需要引入多个坐标系,经过这些坐标系的转化后,三维场景最终会作为一个二维的场景被绘制出来。[1]
在三维场景中,每个物体最初都拥有自己的局部坐标系,局部坐标系为物体进行三角形顶点的坐标量化。然后每个场景都有一个世界坐标系,该坐标系描述了各个物体之间的位置关系(如果有光照,还需要对顶点的颜色值进行计算)。这时,一个摄影坐标系将会描述该场景的取景范围,不可见的三角形面将被裁剪。对摄影坐标系的顶点进行剪裁和透视投影处理后,将可以得到输出于视觉区域中的投影坐标系的顶点信息。最后,将平面投影的点变换到计算机屏幕的视口中,如果采用了纹理贴图,还需要对像素颜色值进行混色计算。固化渲染代码以管道流水线的形式来实现三维物体的渲染,D3D提供了API来简化这个渲染过程,它通过设置该管道流水线的各种过程参数,最终启动该管道流水线的执行代码来实现三维的图形显示。[2-5]
传统的D3D应用程序设计是基于面向过程思想的,应用程序在开启渲染模式后,根据程序逻辑来设置管道流水线的过程参数,并最终将结果绘制在显示器上。这种面向过程的设计思想导致了程序逻辑和D3D的具体实现混合在一起,使得程序设计复杂、模块化差、耦合性低和易读性差。本引擎将以面向对象的设计思想来封装D3D的API,并将管道流水线的渲染过程抽象为工作流,最终基于IoC容器来配置该工作流,使D3D渲染过程更为灵活和方便,从而彻底剥离了应用程序和D3D的具体实现之间的关系。
2 引擎设计
引擎的主要任务是将应用程序与D3D具体实现分离,使得应用程序充当生产工人的角色,而D3D具体实现充当工作流的角色。引擎将按照工作流的设计思想将D3D对象和API封装成工作流构件,并提供相应的接口给应用程序,从而保证两者的分离和通信。另外,为了进一步降低构件间的耦合,引擎将引入 IoC容器来托管引擎的主要构件对象。引擎设计将包括工作流设计和IoC容器设计两个部分,其中,工作流设计主要是依照管道流水线的渲染过程来设计各个构件等,并按照抽象工厂的设计模式来抽象C++对象;IoC容器主要是现实对象的动态创建和采用XML来配置三维场景。
2.1 工作流设计
工作流中的对象按照角色分为基本构件、过程构件、管理器和处理器[6]。其关系如图1所示。
图1 工作流构件的关系
2.1.1 基本构件
基本构件是工作流中的基本单位,其作用是实现工作流分配给自己的任务,而无需知道自己在整个工作流中的作用。它主要包括4个接口:创建、设置参数、获取参数和释放,记为M={C,S,G,R},当一个基本构件被创建后,外界将需要对其进行一些设置,然后在被释放之前,外界都可以在它身上获取到需要的信息和接口。
在引擎中,基本构件包括了顶点、索引、纹理、材质、灯光等D3D基本元素,比如,类 GLight描述了D3D中的灯光的信息和行为,它封装了D3DLIGHT9对象,并提供了相应接口(C,S,G,R),其子类 GPointLight、GDirectionalLight和GSpotLight分别实现了点光源、平行光和聚光源的具体设置和行为(重载了父类的C,S,G,R)。
2.1.2 过程构件
过程构件描述了基本构件之间的关系和工作流的逻辑流程,其作用是按照工作流设计出流程路线和逻辑关系。主要包括4个接口:创建、装配、运行和释放,记为L={C,A,P,R},过程构件一般只有一个实例,当它被创建后,外界将其需要的所有基本构件都装配在其实例上,然后运行该实例,直至它被释放。在引擎中,类 GScene实现了D3D管道渲染的过程,它首先启动渲染,然后分别获取装配在它身上的顶点、索引、纹理、材质、灯光等信息,通过预定的渲染方式来渲染场景,直至渲染过程结束。
2.1.3 管理器
管理器负责管理所有基本构件和过程构件实例,其主要接口包括创建、新建新构件、查找构件、删除构件和释放,记为 G={C,N,F,D,R}。管理器分为基本构件管理器和过程构件管理器,基本构件管理器来创建和释放基本构件实例,过程构件管理器则负责创建和释放过程构件。管理器的主要作用是对所有散乱的基本和过程构件集中管理,增强工作流的灵活性,实现高内聚、低耦合的目的。在引擎中,每个基本构件和过程构件都有其对应的管理器,如类GTextureHolder为纹理对象的管理器,而 GMaterialHolder为材质对象的管理器。
2.1.4 处理器
处理器负责执行工作流,其主要接口包括创建、输入、处理、输出和释放,记为 P={C,I,P,O,R}。处理器首先从管理构件中获取到过程构件实例,然后根据初始化信息获取到所有过程构件实例所需的基本构件实例,然后将这些基本构件实例组装到过程构件实例中,运行过程构件实例,在运行过程中,处理器会根据用户输入或其他消息来创建新的基本构件实例或释放那些不再需要的基本构件实例,还会通过实例的设置接口来初始化和更新所有的实例,直至程序结束。
在引擎中,类 GGraphics实现了处理器的功能,它封装了 IDirect3DDevice9对象,完成了 d3d的渲染过程,并对逻辑程序提供了相应接口,使逻辑程序可以更新最终的输出结果。
2.1.5 抽象工厂模式设计
工作流的运行原理是首先由处理器来做一些初始化的工作,然后根据输入信息从管理器中获取到所需对象(管理器会根据处理器的要求来创建对象),并将那些基本构件装配到过程构件上。然后处理器启动过程构件,由于过程构件中已定义好所需基本构件的接口,它将按照自己定义的流程来运行,运行结束后,处理器结束过程构件。
可见,工作流是基于不同构件之间的接口来设计的,它并不依赖于具体的对象实例,这种使用抽象接口来创建一组相关或依赖的产品组的模式,可以通过抽象工厂来实现。这样,工作流就从具体的产品中被解耦,图2是本引擎采用抽象工厂所实现的构件间的关系及其渲染过程。
图2 引擎渲染时序图
在图2中,所有构件都是通过接口来实现的,引擎并不知道所需要渲染的对象有哪些,它只需要从一个XML的文档中动态去解析各种所需对象的信息,然后装配到 GScene对象中,一切渲染的过程都由 GScene对象来完成,这样就彻底分离了应用程序与引擎间的关系。
2.2 IOC容器设计
在工作流中,不同类型的构件之间存在着各种依赖关系,导致了构件间耦合度低,装配灵活性低等缺点,为了协调各构件的依赖关系,提高构件的重用性和移植性,运用 IoC容器[6]来配置构件间的关系是一个很好的方法。
IoC指不直接创建对象,而只是描述它们的创建方式,容器负责来将这些联系在一起。这种控制权由应用代码转到了外部容器的行为,称为反转。IoC的主要作用有:(1)描述组件间的依赖关系,减小其创建成本;(2)对组件的组装可以脱离于代码控制,减小其耦合度;(3)充分利用组装的灵活性,便于测试。
IoC容器的主要任务是根据对象的配置文件(一般为XML)去动态地创建该对象,并完成配置文件所规定的依赖关系的组装。但是,C++本身并不具备反射机制,以至于无法直接通过C++来动态创建对象并动态调用其方法,所以容器需要其他实现动态创建对象的机制。另外,为了保证某些特殊对象的唯一性,容器将采用单例的设计模式来实现这个特性。
2.2.1 对象的动态创建
对象的动态创建一般是通过反射机制来实现了,而C++本身并没有反射机制,为了在程序运行时动态地识别对象类型并创建对象,就必须把那些需要具备该能力的类信息记录起来,这样才可以实现动态创建。
本系统用类 GRuntimeClass来描述所有有用的类信息,并提供静态方法CreateObject来动态创建对象,这样,每个需要动态创建的类(如GTexture)都需要有一个对应的 GRuntimeClass对象(如classGTexture),所以在定义 GTexture时,需要额外地定义一个静态对象classGTexture,并对classGTexture初始化。
由于对象的动态创建往往是根据其名字来创建的,所以可以将这些 GRuntimeClass对象放进一个全局的map(GRuntimeClass::m_mpRuntimeClasses)中,这样就可以快速地根据名字寻找到对应的 GRuntimeClass对象并动态地创建所需的对象了。当然,这些额外地工作可以通过宏来简化,比如使用DECLARE_DYNCREATE来申明classGTexture和 CreateObject,而使用 IMPL EMENT_DYNCREATE来实现和初始化。
在创建对象时,有一种情况是需要特别处理的,就是有些类它只有一个实例,为了实现这种特性,本引擎采用了单例的设计模式。在传统的单例模式中,单例类有一个静态的对象,并提供一个全局的访问点,通过这种方式来保证类对象的唯一性。在本引擎中,这个静态对象(m_pSingletonObject)和全局访问点(CreateSingleton)被移到了 GRuntimeClass中,这样通过 GRuntime-Class的CreateObject方法来动态地创建对象,用CreateSingleton方法来动态地创建单例。
2.2.2 配置文件解析
IoC容器需要实现根据给定格式的配置文件(本引擎采用XML)来动态地创建对象,并组装其依赖的其他对象。本引擎用类 GIoCContainer来实现这个功能,GIoCContainer有一个静态的IXMLDOMDocumentPtr类型的成员变量pXMLDoc,首先通过该变量的load方法来读取配置文件,然后,GIoCContainer提供了一个静态方法GObject* CreateObject(char*beanId)来动态创建对象,并组装其依赖对象,该方法算法如下:
GObject* CreateObject(char*beanId){
获取静态对象pXMLDoc;
if(pXMLDoc==NULL)
载入对应的XML文件;
获取 beans节点的 IXMLDOMElementPtr对象;
遍历beans节点中的所有bean节点
{
if(bean节点的id为beanId)
{
if(bean节点的scope不为prototype)//不是单例
{
object = GRuntimeClass.CreateObject(bean的class);
遍历bean节点中的所有property节点
{
propertyInstance= CreateObject(property的ref);
动态调用方法使object的名为property的name的对象为propertyInstance;
}
}
else{//单例
object = GRuntimeClass.CreateSingleton(bean的class);
遍历bean节点中的所有property节点
{
if(object的名为property的name的对象==NULL)
{
propertyInstance= CreateObject(property的ref);
动态调用方法使object的名为property的name的对象为propertyInstance;
}
}
}
return object;
}
}
return NULL;
}
3 实现
本引擎实现了从一个场景配置文件来动态生成相关对象,这些对象将作为工作流的输入数据,当工作流启动后,工作流按照其流程来渲染所有三维物体。配置文件独立于程序,当需要修改场景时,引擎无需修改源码,而只需要修改相应的配置项则可。下面是使用该引擎实现的一个具体实例,在该实例中,将渲染一个草地和蓝天的场景,草地是个平板对象,天空是个半球体。实现该实例,只需在场景配置文件中添加上这两个对象,配置文件如下:
<bean id="ground"class="GPlane"scope="prototype">
<property name="m_strTexturePath"value="c:ground.jpg"></property>
<property name="m_fLength"value="800"></property>
<property name="m_fWidth"value="800"></property>
</bean>
<bean id="sky"class="GGlobe"scope="prototype">
<property name="m_fRadius"value="800"></property>
<property name="m_dAlfa"value="15"></property>
<property name="m_dBeta"value="15"></property>
</bean>
<bean id="gview"class="GView">
<property name="m_lpModels">
<list>
<ref bean="ground"/>
<ref bean="sky"/>
</list>
</property>
</bean>
图3显示了该实例的最终渲染效果。
图3 实例渲染效果
4 结束语
D3D是当前3D渲染的主流技术,它实现了管道流水线渲染三维物体的过程。D3D的API采用COM组件的形式,这样使得程序的设计繁杂化,易读性较差,而以一般的面向对象思想来封装则增加了各个对象的耦合。本引擎采用了以工作流的设计思想抽象了管道流水线的渲染过程,并采用IoC容器来协调工作流各构件的依赖关系,提高构件的重用性和移植性,从而剥离了应用程序的逻辑与D3D具体渲染的关系。
本引擎的创新点在于:(1)采用工作流的设计思想来设计并封装D3D的API;(2)利用 IoC容器在构件组装方面的开发性来配置引擎中的对象。目前还只能渲染一些基本几何体,下一步将会以更开放的形式,来渲染复杂的骨骼动画等。
[1] 林少芬,姜明.CAXA三维实体设计教程[M].北京:机械工业出版社,2005.
[2] 李江平.Direct X中3DMAX模型的应用[J].湖北工学院学报,2003(4):58-59.
[3] 况卫飞,彭四伟.三维渲染引擎编辑器的研究[J].电子设计工程,2009(9):91-92.
[4] 周演,陈天滋.基于Direct3D的大规模地形渲染技术研究[J].郑州轻工业学院学报:自然科学版,2008(6):107-111.
[5] 杜园园,侯彤璞,郭艳霞.Direct3D场景中3D模型的处理方法[J].辽宁石油化工大学学报,2008(4):86-90.
[6] 朱启明,汪诗林.基于IOC容器的工作流引擎的设计[J].微计算机信息,2008(21):10-12.