对于游戏开发来说,选好引擎非常关键,而引擎的基础架构以及扩展设计则是决定引擎好坏的关键之关键。在商业项目的开发中,通常我们会投入更多的考虑点到引擎配套工具的完备性和易用性上,其实对于开发人员来说,一套完备易用的工具是建议在一套良好的资源管理框架基础上的,资源管理不仅仅跟工具紧密相关,对于运行时的考虑也是非常重要的。现在很多引擎都提供了资源的热加载,也就是在游戏的运行过程中,动态修改和替换资源,然后即时呈现到游戏中预览效果。这里对N3的资源系统做一个尽可能详细的解读,希望能够读懂N3的设计思想。
游戏资源
游戏中的资源种类非常繁多,但总数据格式上来区分的话,通常可以分为如下几类:模型数据,纹理数据,shader,配置数据。
N3中的资源基类为:class Resource : public Core::RefCounted, 资源是一堆共享数据,能够通过特定的ResourceLoaders 和 ResourceSavers进行加载和保存。看一下,Resource的继承关系图:
可以知道N3把资源分成:Mesh, IndexBuffer, Texture, VertexBuffer, Shader, AnimResource, Model 这么几类。
Resource 设计的功能需要具备:
- 共享(提供引用计数接口),
- 同步异步加载,
- 资源锁定和解锁(比如对IndexBuffer进行修改的时候需要进行使用锁)
- 根据资源Id,加载和保存资源(资源序列化,配合N3的stream系统)
- 资源的状态跟踪(资源具备同步加载和异步加载,以及后面的热加载等,所以需要给资源设计一个状态参数)
N3 的一个主意事项:
在销毁一个Resource的时候,需要先手动的设置 资源的 Loader 和 Saver指针为 NULL,解决指针的循环依赖 {A 引用了 B, B引用了A, };ResourceManager本身会自动的处理好这种指针的循环依赖问题,不过如果手动的创建资源的时候就需要自己处理好这个问题了。
资源的核心结构, Resource, ResourceLoader, ResourceSaver, 下面为Resource的协作图:
N3 设计的资源状态有如下几个:
/// resource states (DO NOT CHANGE ORDER!)
enum State
{
Initial, // 初始状态
Loaded, // 资源已经加载,并且可用
Pending, // 资源的异步加载过程中,通过多次调用 Load()完成异步加载
Failed, // 资源加载失败
Cancelled, // 资源异步加载过程中,被取消加载
};
虽然资源的功能设计比较完备,但归根结底还是需要实现两个功能,load 和 save, 而其他的细节的规则,以及底层工作都交给 ResourceLoader 和 ResourceSaver进行,由于加载和保存的具体工作交给Loader 和 Saver去做,所以设计的Resource只提供了三个功能虚接口供子类来差异化实现:
- virtual State Load();
- virtual void Unload();
- virtual bool Save();
资源加载器
通过自动,Resource的核心结构后,发现资源的通用逻辑在于资源加载和保存,而N3的设计中,资源加载可以异步,保存只能同步立即保存。所以我们接下来看一下,资源加载器ResourceLoader,集成图如下:
资源加载器主要有如下几类:MemoryIndexBufferLoader, MemoryVertexBufferLoader, MemoryMeshLoader, StreamAnimationLoader, StreamShaderLoader, StreamTextureLoader, TextureStreamer, StreamMeshLoader,
说先资源加载器的职责:用正确的数据初始化一个资源。综合前面资源本身设计的要求,设计的loader需要具备如下功能:
- 可以附加到一个资源上,执行加载动作
- 可以从资源上移除加载器,
- 可以从加载器上获得加载好的资源
- 可以查询资源加载器是否支持异步加载,
- 提供接口供接口,同步资源加载调用,异步资源加载调用,异步加载取消,资源加载重置,以及资源的状态查询
- 资源加载器在执行操作的时候需要同步更新资源的状态(包括加载完成,异步加载,加载失败等等)
通过上面的功能分析,我们发现资源加载器和资源是一个相互引用的过程,资源依靠加载器执行加载动作,加载器负责初始化资源。通过前面的分析,我们设计加载器需要实现7个虚接口:
- virtual void OnAttachToResource(const Ptr<Resource>& res);,当把一个资源加载器附加到一个资源上的时候
- virtual void OnRemoveFromResource(); ,当从资源上移除加载器的时候调用, 如果当前处理异步加载的过程,则同时需要取消异步加载
- virtual bool CanLoadAsync() const;,如果加载器支持异步加载则返回true,反之返回false。如果支持异步加载则OnLoadRequested()会立刻返回,并且把资源设置为Pending状态,后面资源需要通过反复的调用加载器的OnPending()借口来加载资源,直到加载完毕,加载器初始化好资源。
- virtual bool OnLoadRequested();,当发生资源请求的时候调用,资源在申请加载的时候就会调用加载器的这个借口,如果加载器支持异步加载,则资源的加载会进入异步状态,否则会即刻加载完毕,并返回。
- virtual void OnLoadCancelled();,当取消资源异步加载的时候调用
- virtual bool OnPending();,在调用OnLoadRequested后,分频调用OnPending,以完成异步加载,知道把资源的状态变成Loaded,或者变成资源的加载失败
- virtual void Reset(); ,重置加载器的各种状态, 如果资源在异步加载过程中,则取消异步加载,同时设置加载器状态为 Initial。
资源保持器
通过前面的介绍,资源的核心结构中还缺失Saver的介绍,这里把Saver放在最后,也是有原因的,因为在游戏中,资源通过是通过第三方工具进行制作,比如Mesh, Skin 通常是通过 Maya, Max进行制作,而Texture 通过是 Photo 进行制作, shader通过是通过cg和 xml进行调试和配置,所以在这个结构中,saver的功能是弱化的,大部分资源是不需要实现Saver的,反而是需要实现导出插件。
在N3的设计中,Saver只干一件单纯的事情,同步序列化资源。先看下Saver的继承图:
可以看到Saver只有一类:StreamTextureSaver, 用来对纹理数据进行序列化,所以Saver的设计也是非常简单的,只有三个虚接口需要子类实现:
- virtual bool OnSave();在资源请求序列化的时候调用,
- virtual void OnAttachToResource(const Ptr<Resource>& res);把Saver附加到一个资源的时候调用
- virtual void OnRemoveFromResource();把Saver从一个资源上移除的时候调用
资源管理策略
对于N3的只有有了个大概的印象后,深入解析资源的管理策略,以及资源的加载策略,最后以加载一个场景角色为例跟踪整个流程。先来说下资源管理。N3设计了一套Resource之上的管理系统,每个类型的资源会被包装一层变成一个class ManagedResource : public Core::RefCounted。类继承图如下:
通过这个设计,我们发现被管理的资源主要是 Model, Mesh, Texture, 后面将对这些资源类型做详尽阐述。接下来是类的协作图:
N3的设计者对于 ManagedResource 的设计初衷是:在真正的Resource对象上封装一层,并且委托给ResourceManager来管理ManagedResources.在这种结构下,包装在ManagedResource里面的真正资源就可以执行管理器的一些维护策略,比如资源替换等。在渲染的时候,使用资源的客户端,回写一些资源的渲染数据到ManagedResource里面,ResourceManager根据这些数据作为根据,可以执行资源的管理策略,比如,如果一个对象出现在屏幕上,但是距离很远,对象出现在视野的大小就会非常小,这个时候资源管理器就可以丢掉一些高精确性的细节数据,为其他资源节约内存。{原话直译,反正意思是懂了,封装一层是解耦资源本身与资源管理策略两个方面,一个资源就是一个实实在在的,而一个被管理的资源可能每帧都会变更}
ManagedResource存在的意义非常明确,所以实现也非常简明,ManagedResource 做如下几件事情:
反馈当前渲染状态到资源
- 提供一个引用计数接口,用来跟踪资源的引用状态,
/// increment client count
void IncrClientCount();
/// decrement client count
void DecrClientCount();
/// get current client count
SizeT GetClientCount() const;
/// get render count for this frame (number of calls to UpdateRenderStats()
SizeT GetRenderCount() const;
- 提供一个优先级接口,供资源管理器做全局统筹,
/// set current priority
void SetPriority(Priority p);
/// get the current priority
Priority GetPriority() const;
- 提供一个设置被管理的资源类型的接口
/// set resource id
void SetResourceId(const ResourceId& id);
/// get resource id
const ResourceId& GetResourceId() const;
/// set contained resource type
void SetResourceType(const Core::Rtti* rtti);
/// get contained resource type
const Core::Rtti* GetResourceType() const;
- 提供一个附加真正资源的接口,并且提供一个设置默认资源的接口
/// set actual resource
void SetResource(const Ptr<Resource>& resource);
/// set place holder resource
void SetPlaceholder(const Ptr<Resource>& placeholder);
- 提供一些提取资源的接口
/// get current resource loading state (Initial, Pending, Loaded, Failed, Cancelled)
Resource::State GetState() const;
/// get contained resource or placeholder if resource is invalid or not loaded
const Ptr<Resource>& GetLoadedResource() const;
/// get contained resource (may return 0)
const Ptr<Resource>& GetResource() const;
/// return true if the placeholder resource would be returned
bool IsPlaceholder() const;
/// clear the contained resource
void Clear();
资源管理器
接下来看下资源管理器,资源管理器的继承结构非常简单,协作图也相当明了,先贴上:
N3的设计者对于资源管理器的设计说明是:
- ResourceManager在 resource 和 客户端使用的真正资源对象之间加了一层。资源管理器存在的一个主要目的是为了实现大的无缝场景资源的序列化管理。资源的使用者,像资源管理器提出资源请求,资源管理器返回一个ManagedResource对象。一个ManagedResource对象是包装了真正资源对象的包裹,包裹本身内容可能会给予资源管理器的管理策略产生变化。资源管理器的一个主要工作就是提供渲染需要的所有资源,同时最大化利用有限的资源存储(包括内存和显存),当然资源管理器还需要负责管理资源的后台加载,并且监听加载过程,在资源没有真正完成加载前,需要给出placeholder(替代资源).
- 同时ManagedResources 会被他们相应的Mappers进行管理,没有被Mappers管理的资源,会由ResourceManager进行创建并存储一个资源引用到管理器里面。通常这些没有被Mappers管理的资源都是RenderTarget,或者是那些程序生命期中不会被Discarded掉的资源。每种类型的资源的管理器策略都是可以通过附加一个ResourceMapper到资源管理器进行定制的。一个ResourceMapper会分析和统计当前被使用的ManagedResource对象的数量,然后实现特定的资源管理模式,下面是ResourceMapper管理策略的基本操作:
- Load(pri, lod): 根据给定的权限和细节级别,从外部存储器上异步加载资源到内存中
- Discard: 对资源执行彻底的卸载操作,释放资源占用的内存
- Upgrade(lod): 根据给定的细节级别,提供资源的精细度
- Degrade(lod): 根据给定的细节级别,降低资源的精细度
- 如果ResourceMapper 是 StreamingResourceMapper的子类,那么可以在运行时给Mapper附加一个ResourceScheduler,随时变更管理策略。
通过解读设计者的说明,我们对管理器相对的清晰了,总结起来,他干两件事情,第一件直接管理资源,包括加载,和卸载,以及精细度的管理,第二件事情,借助ResourceMapper提供一个扩展机制,可以定制各种不同类型资源的管理策略。
到这里我们分解ResourceManager实现的接口:
- 提供扩展接口,扩展不同类型的资源不同的管理策略: ResourceMapper
/// 注册一个资源映射器(资源映射器中定义了对应的资源类型)
void AttachMapper(const Ptr<ResourceMapper>& mapper);
/// 反注册指定资源类型对应的映射器
void RemoveMapper(const Core::Rtti& resourceType);
/// 卸载所有的映射器
void RemoveAllMappers();
/// 查询指定的资源类型是否存储映射器
bool HasMapper(const Core::Rtti& resourceType) const;
/// 提取指定资源类型的映射器
const Ptr<ResourceMapper>& GetMapperByResourceType(const Core::Rtti& resourceType) const;
- 基础资源管理接口, 也就是管理器提供的基本功能
/// create a ManagedResource object (bumps usecount on existing resource)
Ptr<ManagedResource> CreateManagedResource(const Core::Rtti& resType, const ResourceId& id, const Ptr<ResourceLoader>& optResourceLoader=0);
/// reloads an unloaded resource into cache
void RequestResourceForLoading(const Ptr<ManagedResource>& managedResource);
/// unregister a ManagedResource object
void DiscardManagedResource(const Ptr<ManagedResource>& managedResource);
/// return true if a managed resource exists
bool HasManagedResource(const ResourceId& id) const;
/// lookup a managed resource (does not change usecount of resource)
const Ptr<ManagedResource>& LookupManagedResource(const ResourceId& id) const;
/// set if given resource whether should be autoManaged or not
void AutoManageManagedResource(const ResourceId& id, bool autoManage);
- 资源管理器继承到主框架的功能接口
/// prepare stats gathering, call per frame
void Prepare(bool waiting);
/// perform actual resource management, call per frame
void Update(IndexT frameIdx);
/// test if any resources are pending, returns true if not resources are pending
bool CheckPendingResources();
/// wait until pending resources are loaded, or time-out is reached (returns false if time-out)
bool WaitForPendingResources(Timing::Time timeOut);
- 手动资源创建和管理接口
//根据提供的参数,手动创建资源,这个资源不会经过资源映射器创建,所以不负责资源的管理,创建的同时会把这个资源简单的注册到资源管理器的非管理队列。
Ptr<Resource> CreateUnmanagedResource(const ResourceId& resId, const Core::Rtti& resClass, const Ptr<ResourceLoader>& loader = 0, const Ptr<ResourceSaver>& saver = 0);
// (functionalities of the previously used SharedResourceServer)
/// 注册一个资源到资源管理器的非管理队列里面
void RegisterUnmanagedResource(const Ptr<Resource>& res);
/// 反注册资源
void UnregisterUnmanagedResource(const Ptr<Resource>& res);
/// 反注册资源
void UnregisterUnmanagedResource(const ResourceId& id);
- 提供的调试相关功能的接口
// --- 调试相关---
/// 检测资源是否存在,会检测管理队列和非管理队列
bool HasResource(const ResourceId& id) const;
/// 提取指定Id的资源
const Ptr<Resource>& LookupResource(const ResourceId& id) const;
/// 提取给定类型的所有资源
Util::Array<Ptr<Resource> > GetResourcesByType(const Core::Rtti& type) const;
资源映射器
前面对资源管理器进行一番浏览后,引入了一个扩展映射器,这个是扩展管理器的管理功能的【对于以后的设计可以借鉴,管理同一个东西,但是这个东西有很多类型,可以把管理策略单独在解耦一次,为每一个类型提供一个管理策略对象,也据是N3的映射器,这是这里的映射器的设计应该是一个范式,范式主动的是策略,而不能把被管理的资源的类型带入,当然课可以对一些特殊的类型编写特定的范式】。
先来看下Mapper 的继承图和协作图:
上述图不是很全面,后面自己用doxygen生成文档发现也不全,在N3的设计中,前面也提到了,ResourceMapper的子类中有一个StreamResourceMapper, 这个SteamResourceMapper可以附加一个ResourceSchedule,进行管理扩展,比如进行lod之类的,这些都是通过ResourceSchedule进行的。先不深入提了,先来分解一下ResourceMapper本身。
N3设计者对ResourceMapper的定位是:ResourceMapper的子类针对各种资源类型实现特定的资源管理策略。应用程序如果发现标准的ResourceMapper不能满足需求,可以实现自己特定的映射器。资源映射器被附加到ResourceManager上,针对每种资源类型都提供一个映射器,映射器由资源服务回调,处理资源的创建和管理工作。使用者是不应该直接访问ResourceMapper的,使用者应该使用ResourceManager,然后由Manager再去和映射器交互。
可以看出,映射器的定位,就是解耦管理器的管理策略,管理策略和资源类型绑定。下面分析下映射器和管理器约定的功能接口:
- 提供设置Mapper对应资源的placeHolder, 设置资源的异步加载开关
/// 设置 placeholder 的资源id
void SetPlaceholderResourceId(const ResourceId& resId);
/// 提取placeholder 的资源id
const ResourceId& GetPlaceholderResourceId() const;
/// 设置资源加载的异步开关,默认是打开异步加载的
void SetAsyncEnabled(bool b);
/// 返回资源加载的异步开关
bool IsAsyncEnabled() const;
- 提供与资源管理器约定的交互接口
/// 获取当前映射器管理的资源类型
virtual const Core::Rtti& GetResourceType() const;
/// 在附加到资源管理器上的时候,由资源管理器调用
virtual void OnAttachToResourceManager();
/// 从资源管理器上移除映射器的时候,由资源管理器调用
virtual void OnRemoveFromResourceManager();
/// 返回true,如果当前映射器以及被附加到管理器上
bool IsAttachedToResourceManager() const;
/// 由资源管理器调用,用来创建管理资源
virtual Ptr<ManagedResource> OnCreateManagedResource(const Core::Rtti& resType, const ResourceId& resourceId, const Ptr<ResourceLoader>& optResourceLoader);
/// 由资源管理器调用,当需要卸载一个管理资源的时候
virtual void OnDiscardManagedResource(const Ptr<ManagedResource>& managedResource);
/// 在渲染前调用,用来收集资源需要的渲染状态
virtual void OnPrepare(bool waiting);
/// 在收集完资源的渲染状态后,执行资源的管理策略,比如LOD
virtual void OnUpdate(IndexT frameIndex);
/// 返回当前未决的资源个数(也就是还处理异步加载队列的资源个数)
virtual SizeT GetNumPendingResources() const;
在了解了ResourceMapper 之后,对映射器的职责也相对明确,只是貌似还缺了一点什么,是的,还缺了Upgrade(lod), Degrade(lod)操作,这两个操作是由ResourceMapper的扩展来支持的,在ResourceMapper本身没用体现出来,只是提供了一个OnUpdate()接口,用来做这些扩展。
接下来我们递进一步,看下 N3 中带的三个 ResourceMapper : SimpleResourceMapper, StreamingResourceMapper, 以及,PoolResourceMapper,。
先看着下Mapper的类图:
其中
- SimpleResourceMapper : 实现了一个基本的资源管理映射器,主要的功能是资源的同步和异步加载,当资源是异步加载时,在每次OnPrepare的时候调用相应资源的load()接口,当资源加载完毕的时候,把资源的推送给相应的ManagedResource,根据引用计数规则对ManagedResource资源进行管理。
- SteamingResourceMapper : 这个映射器,N3的设计是用来对所有动态的流式资源进行管理的。这个映射器功能相对较全面。其中涉及的概念依然还是清晰的:1, 资源创建, 2. 异步加载,3. 优先级加载, 4,预载资源字典 5,根据当前渲染状态应用层次细节模型。6, 针对流式资源的特点优化资源的缓存系统。 N3 当前对于这个流式资源映射器,只提供了一套Texture的流式管理。对于其他资源需要自己扩展。
- PoolResourceMapper : 这个映射器在 StreamingResourceMapper 上加了一个 缓存池概念,缓存池都是预先创建好的,每个缓存池ResourcePool中存放一定数量的ResourceSlot, 这个ResourceSlot 是 已经创建好的 Resource(只是资源的内容待定,比如如果是纹理资源的话,就已经申请好了纹理句柄,只是纹理内容可以后面进行变更),并且配置了 Loader, 只需要设置一个资源Id,就可以加载资源。缓存池的申请和管理,交给PoolScheduler。
资源管理到这个地方,整个系统框架以及说明完毕,而且streamingResourceMapper 和 PoolResourceMapper 相对较复杂,后面再进行完整阐述。提到了这么多的内容,我们先把关系整理一下,N3中有哪些资源,这些资源怎么结合,怎么组合成管理器。
N3资源管理配置方案
前面讨论了整个资源的管理框架,现在来看下N3引擎是怎么配置自己的资源管理策略的,也就是怎么配置各种资源的ResourceMapper,N3引擎有两个接口配置资源映射器,一个是在RenderApplication中设置,一个是在GraphicsFeatureUnit中设置,两个地方的配置方案是一致的,具体代码如下:
还有一个比较隐秘的地方配置资源管理策略,在ModelServer中:
对照代码我们列一个配置管理表
资源类型:
管理资源类型:
加载器:
映射器:
占位资源:
备注:
CoreGraphics::Texture
ManagedTexture
StreamTextureLoader
SimpleResourceMapper
"systex:system/placeholder.dds"
CoreGraphics::Mesh
ManagedMesh
StreamMeshLoader
SimpleResourceMapper
"sysmsh:system/placeholder_s_0.nvx2"
CoreGraphics::AnimResource
ManagedAnimResource
StreamAnimationLoader
SimpleResourceMapper
空
CoreGraphics::Model
ManagedModel
StreamModelLoader
SimpleResourceMapper
"mdl:system/placeholder.n3"
CoreGraphics::Shader
空
StreamShaderLoader
空
"shd:shared"
上述表格后,发现还有一种资源没有在前面的代码中列举到,是的,还shader 没有列举到上面, shader 和其他资源不一样,在N3中,启动的时候回预先载入所有的shader, 这个工作由shaderServer完成,具体代码片段如下:
而且会先创建一个标准shader的实例,代码如下:
资源配置介绍完毕,现在实例更正一下资源加载的整个过程。并对各种资源之间的交互耦合关系做一个说明。(Mesh 与 Model 是什么关系,怎么关联到 Texture 和 Shader,渲染怎么使用这些数据等等问题)。
场景对象与资源加载
N3设计了一个多线程的渲染框架,渲染放在一个单独的线程,而场景对象的操作和逻辑放在另外一个线程,两个线程通过一个FrameSysnTimer进行同步,初步看起来这个框架还不是非常上流,需要优化升级。这里先不深入了,后面用独立章节来详解。这里提到这个的原因是我们需要知道资源系统和场景是怎么配合的。两个线程我们暂时命名为 渲染线程和操作线程,在操作系统中,会存在各种图形实体 class GraphicsEntity : public Core::RefCounted, 这个也就代表了场景中出现的所有对象,包括可见的角色,特效,地形,建筑等,也包括不可见的相机,光源等。一个可见实体也就会与相应的资源绑定。先贴两个图(其中有一个巨复杂的协作图,不要被吓到,是的,他是这样设计的,而且他就出现在你面前):
接下来是一个协作图:
这个巨大的协作图,充分说明了多线程渲染框架,需要继续改善。
在继承图中,我们可以观察到一个相对简单的实体继承体系,根据N3的设计场景中只会有三类实体,模型,相机和光源。我们在里面没有发现各种细节的实体类型,这个就是西欧那边的风格,你不会见到比如Terrain这种单独的实体类型,而美洲和亚洲团队设计的引擎一定是把实体层次设计得相对多层,大底是因为程序员的偏好不一样吧。
在上述的实体中,与资源密切相关的就是 ModelEntity了。ModelEntity的构成也是非常有内容的,后面在深入。这里我们需要了解的就是一个ModelEntity对应一个Model(这里是说一个ModelEntiry实例类型对应一个Model实例类型的实例, 这样说可能比较绕,ModelEntiry 是一个类型,Model是资源类型,一个Model实例是一个实例类型,而一个ModelEntity的实例并不需要每次都创建一个Model实例来与之配对,只是需要创建一个Model实例的 Instance 与之对象, 所有同一个资源Id对应的ModelEntity 共享一个资源Id相应的 Model 类型实例,但是 ModelEntity 实例拥有属于它自己的 Model 类型实例的 Instance,天啊,但愿我说明白了,这种关系在技能系统也经常出现,就是资源共享的概念)。所以创建一个ModelEntity的时候需要资源管理管理器里面查找相应资源Id的Model存不存在,如果存在相应的资源,则给出一个类型实例的实例给ModelEntity。
我们看下Model的构成,一个Model由一棵ModelNode的多叉树,而每个ModelNode本身可以各种类型的节点,可见,不可见。所以先列举一下ModelNode的继承图:
从节点的层级关系,我们可以得到一个Model本身是一个可见综合体,可以配置各种数据,包括粒子特效,渲染状态,以及蒙皮等。
一个N3文件的加载流程如下:分析 *.N3文件,
1. 文件头,magic, version
2. 读一个 fourcc
如果 fourcc == '>MDL' ==> :读 fourcc, 读 name
如果 fourcc == '>MND' ===> : 读fourcc, 读 name,根据fourcc 和 name 创建一个 ModeNode, 并设置ModeNode的 parent, 然后把当前创建的ModeNode推到栈顶
如果 fourcc == '<MND' ===> :执行退栈操作,pop栈顶的ModeNode
如果 fourcc == '<MDL' ===> :结束当前Model的加载,然后对Model中所有的ModeNode执行资源加载操作,里面涉及 Mesh, Shader, Particle, Skin 等。接着计算Model的包围盒。
如果是其他,则读取 fourcc ,更具fourcc 对当前栈顶的ModeNode 执行属性初始化。
然后循环往复步骤 2, 知道模型读取完毕,或者达到文件末尾。
在加载过程中,transforNode都会根据parentNode的变换信息转换值为到Mode的跟节点的坐标系中。
在Model 的资源加载算法分析完后,我们已经非常清晰的资源 N3中的各种那个是资源是怎么耦合的了,都是通过ModelNode绑定给Model,而一个Model存储的N3文件是对一棵ModelNode树进行先序遍历保存下来的二进制文件。后面还需要说明一下的是ModeNode与资源的对于关系。
在整个算法中,用到了一个没有成文的一句,资源类型的fourcc , 属性的 fourcc ,以及节点操作的 fourcc 应该都是全局惟一的。
节点类型:
引入的属性:
引入的资源:
加载策略:
ModeNode
FourCC('LBOX'): 包围盒数据
FourCC('MNTP'): 节点类型数据
FourCC('SSTA'): 字符串属性
TransformNode
FourCC('POSI'): 节点坐标值
FourCC('ROTN'): 节点旋转值
FourCC('SCAL'): 节点缩放值
FourCC('RPIV'): 节点旋转轴
FourCC('SPIV'): 节点缩放轴
FourCC('SVSP'): 节点是否可见标记
FourCC('SLKV'): 节点视角锁定标记
FourCC('SMID'): 节点最小可见距离
FourCC('SMAD'): 节点最大可见距离
CharactorNode
FourCC('NJNT'): 骨骼节点个数
FourCC('JONT'): 骨骼节点数据
FourCC('NSKL'): 蒙皮个数
FourCC('SKNL'): 蒙皮数据
FourCC('ANIM'): 动画资源Id
FourCC('VART'): 可变动画资源Id
ResourceManager::CreateManagedResource(AnimResource::RTTI)
StateNode
FourCC('SINT'): Shade的整形参数
FourCC('SFLT'): Shade的浮点参数
FourCC('SVEC'): Shade的向量参数
FourCC('STUS'): Shade的索引参数-MLPUVStretch
FourCC('SSPI'): Shade的索引参数-MLPSpecIntensity
FourCC('SHDR'): Shade的资源Id
FourCC('STXT'): Shade相关的纹理资源Id
ShaderServer::CreateShaderInstance(shaderResId)
ResourceManager::CreateManagedResource(Texture::RTTI, texResId)
ParticleSystemNode
FourCC('EFRQ'): 对应粒子系统的EmissionFrequency
FourCC('PLFT'): 对应粒子系统的LifeTime
FourCC('PSMN'): 对应粒子系统的SpreadMin
FourCC('PSMX'): 对应粒子系统的SpreadMax
FourCC('PSVL'): 对应粒子系统的StartVelocity
FourCC('PRVL'): 对应粒子系统的RotationVelocity
FourCC('PSZE'): 对应粒子系统的Size
FourCC('PMSS'): 对应粒子系统的Mass
FourCC('PTMN'): 对应粒子系统的TimeManipulator
FourCC('PVLF'): 对应粒子系统的VelocityFactor
FourCC('PAIR'): 对应粒子系统的AirResistance
FourCC('PRED'): 对应粒子系统的Red
FourCC('PGRN'): 对应粒子系统的Green
FourCC('PBLU'): 对应粒子系统的Blue
FourCC('PALP'): 对应粒子系统的Alpha
FourCC('PEDU'): 对应粒子系统的EmissionDuration
FourCC('PLPE'): 对应粒子系统的Looping
FourCC('PACD'): 对应粒子系统的ActivityDistance
FourCC('PROF'): 对应粒子系统的BenderOldestFirst
FourCC('PBBO'): 对应粒子系统的Billboard
FourCC('PRMN'): 对应粒子系统的StartRotationMin
FourCC('PRMX'): 对应粒子系统的StartRotationMax
FourCC('PGRV'): 对应粒子系统的Gravity
FourCC('PSTC'): 对应粒子系统的ParticleStretch
FourCC('PTTX'): 对应粒子系统的TextureTile
FourCC('PSTS'): 对应粒子系统的StretchToStart
FourCC('PVRM'): 对应粒子系统的VelocityRandomize
FourCC('PRRM'): 对应粒子系统的RotationRandomize
FourCC('PSRM'): 对应粒子系统的SizeRandomize
FourCC('PPCT'): 对应粒子系统的PrecalcTime
FourCC('PRRD'): 对应粒子系统的RandomizeRotation
FourCC('PSDL'): 对应粒子系统的StretchDetail
FourCC('PVAF'): 对应粒子系统的ViewAngleFade
FourCC('PDEL'): 对应粒子系统的StartDelay
FourCC('PGRI'):对应粒子系统的primitiveGroupIndex
FourCC('MESH'): 粒子系统的Mesh资源Id
Ptr<StreamMeshLoader> loader = StreamMeshLoader::Create();
loader->SetUsage(VertexBuffer::UsageCpu);
loader->SetAccess(VertexBuffer::AccessRead);
ResourceManager::CreateManagedResource(Mesh::RTTI, meshId, loader)
ShapeNode
FourCC('PGRI'): 对应指定Mesh的primitiveGroupIndex
FourCC('MESH') : 几何节点的Mesh资源Id
ResourceManager::CreateManagedResource(Mesh::RTTI,meshResId,loader)
CharacterSkinNode
FourCC('NSKF'): 对应蒙皮片段的个数
FourCC('SFRG'):对应蒙皮片段的数据
Model资源在加载完成后,会调用OnResourceLoaded(), 而Model::OnResourceLoaded() 会调Model中所有的ModleNode的OnResourceLoaded(),这样就完成了整个Model资源的加载。
[备注:在引擎中用树的前序遍历来存储节点数据,是有其依据的,因为节点的构建有其父子关系,构建的时候通常是需要先把父节点本身的数据完整的构建完成,才能构建子节点,而在析构资源的时候,往往是应该先析构子节点再析构父节点,所以在编写相关的函数的时候,需要注意这之间内在的联系,不能看到节点再Model中用数组存储,就不理会节点是之间父子关系,通常这些需要注意的函数是,Update, Steup, UnSteup 三个系列, 在N3的设计中,有些函数的设计可以看出作者有在注意这个约定关系,有些则没有,所以N3还是个需要验证的引擎]。
平台资源的加载
似乎对应资源的加载已经越来越清晰了,是的,资源加载已经到最后一步,最后我们需要深入一步的是各种资源的平台相关加载的实现,
Mesh 资源, 会对应 IndexBuffer 和 VertexBuffer,把资源的平台相关性推到 IndexBuffer 和 VertexBuffer
Texture 资源,会对应 TextureBuffer, 2D, Cube
Animation 资源, 自定义格式,这个资源是平台无关的
shader 资源, 会对应 GLSL, HLSL,
要清晰明了这些基本资源的加载,需要结合资源和资源加载器的平台相关实现,下面对这些资源做一个表格:
资源类:
平台相关资源类:
加载器类:
平台相关的加载器类:
DirectX的接口句柄:
备注:
CoreGraphics::Texture
Direct3D9::D3D9Texture
CoreGraphics::StreamTextureLoader
Direct3D9::D3D9StreamTextureLoader
IDirect3DCubeTexture9
IDirect3DVolumeTexture9
IDirect3DTexture9
对应的资源格式为*.dds
CoreGraphics::Mesh
CoreGraphics::VertexBuffer
CoreGraphics::IndexBuffer
Base::MeshBase
Win360::D3D9VertexBuffer
Win360::D3D9IndexBuffer
StreamMeshLoader
CoreGraphics::MemoryVertexBufferLoader
CoreGraphics::MemoryIndexBufferLoader
Win360::D3D9StreamMeshLoader
Win360::D3D9MemoryVertexBufferLoader
Win360::D3D9MemoryIndexBufferLoader
IDirect3DVertexBuffer9
IDirect3DIndexBuffer9
对应的资源格式为 *.nvx2 *.nvx3 *.n3d3
现在只做了 *.nvx2 其他格式N3还没来得及实现
由于网格资源的特殊性,N3中设计了一个专门的类来分析各种格式的网格资源
比如 Nvx2StreamReader 解析 *.nvx2
StreamMeshLoader 加载网格资源后,分别调用MemoryIndexLoader 和 MemoryVertexLoader 初始化 网格的 IndexBuffer 和 VertexBuffer, 而IndexBuffer 和 VertexBuffer, 是由平台相关性的,所以这里出现了一个平台相关资源类为 Base::MeshBase
CoreGraphics::AnimResource
Resources::Resource
CoreAnimation::StreamAnimationLoader
Resources::StreamResourceLoader
二进制数据转到控制数据
Nax3Clip--AnimClip
Nax3AnimEvent--AnimEvent
Nax3Curve--AnimCurve
AnimKeyBuffer
这个资源为平台独立的,资源格式由N3自己独立定义,具体格式数据参与源代码:naxfileformatstructs.h
CoreGraphics::shader
Direct3D9::D3D9Shader
CoreGraphics::StreamShaderLoader
Direct3D9::D3D9StreamShaderLoader
ID3DXEffect
资源格式为.fx
渲染的.fx是由N3的工具nsh.exe根据配置文件生成的
有了上述表格,对于资源的加载应该算是非常清晰明确的了,对于图形OpenGL的实现,这里就不进行列举了,后面说明跨平台特效的时候再列举。当资源变成DX的渲染数据句柄后,对于熟悉DX的同志来说,就已经一目了然了。前面提到过场景中的可见资源都是ModelEntity, 而 ModelEntity对于资源 Model, 而 Model 对应 ModelNode, ModelNode对应上表中的资源。一个 ModelEntity实例,与其他同资源Id的ModelEntity共享一个Model实例,同时每个ModelEntity示例还独有一个ModelInstance 实例,其中ModelInstance 实例是由各种ModelNode对应的ModelNodeInstance实例组成。具体的渲染流程就在ModelNodeInstance实例中,而真正的渲染是在一个独立的渲染线程中(类似对每帧中调用的渲染指令进行缓存,然后在一个独立线程进行集中批调用),这里我们先贴一个ModelNodeInstance的继承图,具体渲染的说明将在后面独立章节进行解说。
ModelEntity 到 Mesh, Texture
有了前面的信息,我们先以在场景中创建一个简单的ModelEntity为例来说明,场景创建和资源加载的流程:
ModelEntity----InternalModelEntity ---- ManagedModel ---- Model ------- 加载 ModelNode ------- 加载 Mesh, Shade, Texture, AnimResource,
Model加载完毕 ------设置 ManagedModel中持有的Model实例,同时创建一个ModelInstance 给 InternalModelEntity
创建ModelInstance ---- 创建 ModelNodeInstance
这里面设计一个 InternalModelEntity , 这个是一个与多线程渲染框架强相关的类,我们先贴一个类的继承图:
这里看到 InternalModelEntity 属于 InternalGraphicsEntity体系 和 ModelEntity属于 CoreGraphicsEntity体系,而且两个继承集合之间是一个一一映射。在这里N3的多线程渲染框架可能在你心中已经有了个雏形,这里先不深入了,在解析多线程的时候深入。
至此N3的资源管理框架基本结束,还差最后一个部分,StreamResourceMapper 和 PoolResourceMapper的解析。这一部分也是当前N3中还未完全结束的一部分,不过可惜Red Lib 已经被棒子给买了,后面看不到他的详细实现了。
设计总结:
对于N3的资源系统设计框架到这里是基本结束,可以看到其中一些比较好的思想:
对于平台相关的资源类,用 Base机制进行跨平台操作
对于资源对象和资源的管理策略进行风格,在Resource 本身上再封装出一个 ManagedResource,
对于管理器的设计则,解耦管理功能,把管理功能接口定下来,然后剥离出一个 ResourceMapper的管理单元。
对于Resource本身的设计也非常简明,每个资源提供一个状态,一个加载器和,保持器。
对于ManagedResource, 新增管理相关的成员,包括一个优先级,一个状态,一个placerHold。
.......
各种设计思想。。。 Oh My God