Ogre引擎由多个模块组成,从不同角度来划分可以得到不同的结果。从功能上看Ogre可大致分为资源管理、场景管理和渲染管理三大模块;而从可执行部分的组织方式看,Ogre引擎则是由多个dll动态链接库组合而成的。
组成Ogre的各动态库基本上可以分为以下几部分:核心库、渲染层、场景管理逻辑层。核心库(OgreMain.dll,一般debug版会生成OgreMain_d.dll文件)负责资源加载和管理,并根据实际情况选择和加载对应的渲染层模块及场景管理层模块,同时调度协调各模块共同对场景进行渲染操作。渲染层主要是对不同的底层渲染引擎进行封装,使不同的底层渲染引擎如:DirectX、OpenGL、OpenGLES等能够支持相同的渲染接口,以实现核心层用统一的方式对不同底层渲染引擎的调用。这种技术有点类似《设计模式》中提到的Adapter模式。为了支持不同的底层渲染引擎,Ogre要有针对性地生成不同的动态链接库如:RenderSystem_Direct3D9.dll、RenderSystem_Direct3D11.dll、RenderSystem_GL.dll、 RenderSystem_GLES.dll等。场景管理层则主要根据实际需要,采用不同的算法来实现对场景对象的快速裁剪,比如若要用八叉树对场景进行管理时一般要生成并调用Plugin_OctreeSceneManager.dll库,而要用BSP算法则需要生成并调用Plugin_BSPSceneManager.dll库等。
以上所说的这些动态库要能协调一致地工作,需要首先解决以下两个问题:1. 如果不考虑跨平台并假设是在Windows平台上运行,如何实现各动态库与核心库的衔接?Ogre是用面向对象的技术开发的,Windows平台只提供了动态加载动态链接库及调用其中库函数的相关技术,并没有现成的对动态加载的动态库函数中对象的访问方法。2. 如果考虑跨平台的问题,那么该如何对动态库的动态加载过程作进一步的抽象?(另外,如果要考虑将渲染引擎引入到IOS平台上,那么除了要对加载过程进行适当抽象外,还要考虑到IOS并不支持动态链接库的实际情况,而只能采用静态库的使用方法。此问题不在本文的讨论范围之内)
对于以上问题,Ogre采用了称之为插件(Plugin)的技术来加以解决,这种方法本身比较巧妙,而且对我们以后的模块化编程有一定的借鉴意义,所以值得详细分析一下。
首先Ogre对要加载的动态库作了一个抽象,用DynLib类来表示,一个动态库就是一个DynLib类对象。同时Ogre又定义了一个DynLibManager类,用来管理所有加载的DynLib类对象,它负责根据动态库文件名对相应的库进行加载,并保存加载后的DynLib对象指针。
此处采用的是Ogre1.8的相关代码。
void
Root::loadPlugin(
const
String& pluginName)
{
#if OGRE_PLATFORM != OGRE_PLATFORM_NACL
// Load plugin library
DynLib* lib = DynLibManager::getSingleton().load( pluginName );
// Store for later unload
// Check for existence, because if called 2+ times DynLibManager returns existing entry<BR> if (std::find(mPluginLibs.begin(), mPluginLibs.end(), lib) == mPluginLibs.end())
{<BR> mPluginLibs.push_back(lib);
// Call startup function
DLL_START_PLUGIN pFunc = (DLL_START_PLUGIN)lib->getSymbol(
"dllStartPlugin"
);
if
(!pFunc)
OGRE_EXCEPT(Exception::ERR_ITEM_NOT_FOUND,
"Cannot find symbol dllStartPlugin in library "
+ pluginName,
"Root::loadPlugin"
);
// This must call installPlugin
pFunc();
}
#endif
}
|
以上代码中,DynLibManager::getSingleton().load( pluginName )的加载过程会最终调用到DynLib的Load函数,此Load函数的核心部分是:
mInst = (DYNLIB_HANDLE)DYNLIB_LOAD( name.c_str() );
其中DYNLIB_LOAD是预先定义好的一个宏,其定义如下:
#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
# define DYNLIB_HANDLE hInstance
# define DYNLIB_LOAD( a ) LoadLibraryEx( a, NULL, LOAD_WITH_ALTERED_SEARCH_PATH )
# define DYNLIB_GETSYM( a, b ) GetProcAddress( a, b )
# define DYNLIB_UNLOAD( a ) !FreeLibrary( a )
struct
HINSTANCE__;
typedef
struct
HINSTANCE__* hInstance;
#elif OGRE_PLATFORM == OGRE_PLATFORM_LINUX || OGRE_PLATFORM == OGRE_PLATFORM_ANDROID || OGRE_PLATFORM == OGRE_PLATFORM_NACL
# define DYNLIB_HANDLE void*
# define DYNLIB_LOAD( a ) dlopen( a, RTLD_LAZY | RTLD_GLOBAL)
# define DYNLIB_GETSYM( a, b ) dlsym( a, b )
# define DYNLIB_UNLOAD( a ) dlclose( a )
#elif OGRE_PLATFORM == OGRE_PLATFORM_APPLE || OGRE_PLATFORM == OGRE_PLATFORM_APPLE_IOS
# define DYNLIB_HANDLE void*
# define DYNLIB_LOAD( a ) mac_loadDylib( a )
# define DYNLIB_GETSYM( a, b ) dlsym( a, b )
# define DYNLIB_UNLOAD( a ) dlclose( a )
#endif
|
通过这个宏定义,实现了对不同平台的动态库加载的抽象,解决了之前提出的第二个问题。
Ogre会在每个要被动态加载的dll库对象中声明一个名为“dllStartPlugin”的函数,以RenderSystem_Direct3D9.dll为例:
#ifndef OGRE_STATIC_LIB
namespace
Ogre
{
D3D9Plugin* plugin;
extern
"C"
void
_OgreD3D9Export dllStartPlugin(
void
)
throw
()
{
plugin = OGRE_NEW D3D9Plugin();
Root::getSingleton().installPlugin(plugin);
}
extern
"C"
void
_OgreD3D9Export dllStopPlugin(
void
)
{
Root::getSingleton().uninstallPlugin(plugin);
OGRE_DELETE plugin;
}
}
#endif
|
当加载完RenderSystem_Direct3D9.dll后,紧接着就调用"dllStartPlugin"函数。而在此函数中会生成一个D3D9Plugin的对象,并通过Root::installPlugin接口将此对象指针回传给核心库的Root对象,这样一来,核心库就可以通过这个指针来访问动态加载的动态库中的对象了,之前提出的第一个问题得到解决。
在早期的Ogre版本中,这种技术只用来处理三大模块的分离。到了后期,Ogre引入了SampleBrowser的概念,同时将各Sampler也编译成相应的dll文件,这样一来,每个Sampler的加载和运行也以Plugin插件技术为基础了。不论是早期的模块分离,还是后期的Sampler演示,都会用一个.cfg配制文件来描述要加载的Plugin插件,引擎会先读取相应的.cfg文件,再根据此配制文件的描述来搜索和加载所需析Plugin插件,这就使得模块的加载变得更加脚本化了,从而更方便,更灵活了。
Ogre的Plugin原理的启示:
当我们需要将我们的可执行程序进行模块化处理时,Ogre的这种Plugin技术是颇值得借鉴的。它能帮助我们在采用面向对象的编程技术并进行动态加载动态库的同时,相当方便地对代码逻辑进行模块化设计。在此基础上如果再引入配制文件以实现自动加载,将会使程序的使用变得更为灵活和方便。