前言
一直想找一款轻量级的渲染引擎研究,之前看过一些Cocos2D和Orge的代码,感觉Orge封装太狠,而Cocos2D总是被人吐槽代码不好(其实我觉得还好),偶然发现Urho3D,初看代码貌似很不错,代码规范,且干净紧凑,封装适度。Urho3D的简介,截了下官网的介绍:
Urho3D is a free lightweight, cross-platform 2D and 3D game engine implemented in C++ and released under the MIT license. Greatly inspired by OGRE and Horde3D.
本文以iOS平台为例,主要分析下Urho3D的渲染部分的框架设计。
Urho3D工程结构
最粗略的划分一个Urho3D工程,如下图,可以分为应用层,系统UI框架和Urho3D C++渲染引擎,中间以SDL(一款跨平台多媒体开发库)做转接。如下图:
UI系统的主要对象
如下,都是iOS的UI框架必不可少的部分:
UI框架与Urho3D引擎的相互调用
因为UI框架是OC实现(.m文件),而底层引擎是C++(.cpp文件),两者不能直接调用,Urho3D通过SDL(.c文件)来做中间转接,这也让Urho3D底层引擎代码彻底与上层平台相关的UI框架解耦。
举个例子,比如引擎的Graphics对象在初始化时会创建UIWindow(SDL_uikitwindow),调用如下:
UI框架与引擎之间的数据交互,主要通过SDL_Window结构体来管理,SDL_Window的成员简略如下:
Urho3D引擎的主要模块
Urho3D的每个模块称为一个“子系统”(subsystem),统一由Context对象管理。Context通过RegisterSubsystem()注册子系统,GetSubsystem()取出一个子系统,我们可以简单把Context理解为一个单例,子系统其实就缓存在它的subsystems_(一个HashMap)成员中。下面看下渲染框架的主要模块:
Engine模块
Engine是引擎最靠近上层的接口,上层通过控制Engine对象来可以控制引擎的启动,运行,停止等。
Graphics模块
Graphics主要是底层图形API的封装,Urho3D支持OpenGL,Direct3D和WebGL等。Graphics保持对外接口不变,并对不同平台图形api采用不同的实现,来达到跨平台的目的。本文分析是iOS平台,Graphics封装的是OpenGLES 2.0的API。
Graphics管理着当前渲染引擎的渲染数据,渲染状态,DrawCall调用等等。
ResourceCache模块
顾名思义,ResourceCache是用来做资源缓存的。在Urho3D中,如材质管理Material,模型数据Model,纹理Texture2D等,都被当做一种“资源”(Resource),它们继承自Resource类,并由ResourceCache统一加载,缓存,这样可以便于资源的统一管理,并监测整个系统内存的使用状况。
Renderer模块
Renderer是渲染引擎的核心,它管理场景(scene)中各节点(Node),如相机(Camera),光照(Light)及各个3D或2D模型,并根据各节点的材质(Material),最终合并成一个或多个batch(相当于一次渲染需要配置的所有状态和数据),根据batch参数配置Graphics的渲染状态及调用Drawcall。
UI模块
这个不同于上面说的系统UI框架,它和Renderer类似,但它主要是用来渲染2D图形,如一些界面的控件,logo,文字等,可以用UI模块来渲染。
Urho3D渲染线程
Urho3D的渲染线程为主线程,由SDL_uikitviewcontroller通过定时器CADisplayLink以屏幕刷新的频率来回调执行。主要干的事在Engine的Update()函数和Render()函数中,大略如下:
渲染线程
Urho3D中的多线程
Urho3D中貌似没有支持多线程渲染,但是它提供了WorkQueue工作队列来支持CPU多线程的处理。例如,在判断场景中的node的可见性测试时,一共有20个节点,可以把20个节点分到两个workItem中,每个workItem负责10个节点的可见性测试,一个workItem在主线程运行,一个在子线程运行,以此来分担主线程的开销。截了源码中的一段如下(代码中的tempDrawables相当于node):
int numWorkItems = queue->GetNumThreads() + 1; // Worker threads + main thread
int drawablesPerItem = tempDrawables.Size() / numWorkItems;
PODVector<Drawable*>::Iterator start = tempDrawables.Begin();
// Create a work item for each thread
for (int i = 0; i < numWorkItems; ++i)
{
SharedPtr<WorkItem> item = queue->GetFreeItem();
item->priority_ = M_MAX_UNSIGNED;
item->workFunction_ = CheckVisibilityWork;
item->aux_ = this;
PODVector<Drawable*>::Iterator end = tempDrawables.End();
if (i < numWorkItems - 1 && end - start > drawablesPerItem)
end = start + drawablesPerItem;
item->start_ = &(*start);
item->end_ = &(*end);
queue->AddWorkItem(item);
start = end;
}
queue->Complete(M_MAX_UNSIGNED); //等到两个线程都完成才返回
Urho3D中的对象,容器和智能指针
Urho3D中的容器没有使用STL或boost等第三方库,而是自己实现了一套容器。(至于为什么要自己实现原因还不太明白)。除了容器类型之外,大部分的对象都继承自Object类,而Object又继承自RefCounted。例如Renderer类,它的继承关系为:
Object类具有消息订阅和分发功能,这个后面介绍。而RefCounted主要用于引用计数,Urho3D自己实现的SharedPtr等智能指针必须用在RefCounted子类上。
Urho3D中的消息订阅和分发
Urho3D使用消息订阅和分发的方式(也就是观察者模式)来实现各个对象之间的消息传递。具体实现是通过Context对象和Object对象配合完成的。Object类的相关接口如下:
class URHO3D_API Context : public RefCounted
{
public:
//返回观察者组
EventReceiverGroup* GetEventReceivers(StringHash eventType);
private:
//观察者
HashMap<StringHash, SharedPtr<EventReceiverGroup> > eventReceivers_;
};
class URHO3D_API Object : public RefCounted
{
public:
//订阅消息
void SubscribeToEvent(StringHash eventType, EventHandler* handler);
//取消订阅
void UnsubscribeFromEvent(StringHash eventType);
//发送消息
void SendEvent(StringHash eventType);
//响应消息
virtual void OnEvent(Object* sender, StringHash eventType, VariantMap& eventData);
private:
LinkedList<EventHandler> eventHandlers_; //响应消息的函数指针队列
};
举个例子,比如UI类在初始化时订阅了E_RENDERUPDATE事件,代码如下:
void UI::Initialize()
{
//省略...
SubscribeToEvent(E_RENDERUPDATE, URHO3D_HANDLER(UI, HandleRenderUpdate));
}
在系统运行时,Engine对象会分发E_RENDERUPDATE事件,如下:
void Engine::Update()
{
//省略...
SendEvent(E_RENDERUPDATE, eventData);
}
消息传递的流程简略如下:
需要注意的是,Urho3D的消息分发不具备线程安全性,所有调用必须在主线程中进行。代码在中已说明,如:
void Object::SendEvent(StringHash eventType, VariantMap& eventData)
{
if (!Thread::IsMainThread())
{
URHO3D_LOGERROR("Sending events is only supported from the main thread");
return;
}
//省略...
}
渲染引擎的任务
在介绍后面的Urho3D的主要对象之前,按照个人理解,先讲下渲染引擎主要要干的是一件什么事儿。通常,要把一个对象渲染到画面上,大概的要解决三件事情:
1.把对象渲染到画面上的哪个位置,是否会出现在画面中?
2.对象的几何形状是什么?
3.对象的表面在光照下是什么样子的?
因此,我们可以简单的理解为,渲染引擎主要是根据用户的配置,完成下面这三件事:
1.确定渲染对象的大小和位置(场景管理);
2.确定渲染对象的几何形状(模型);
3.确定渲染对象的表面属性(材质)。
Urho3D场景管理相关的对象
场景管理(scene manager)相关的类如下:
场景管理相关的类
Scene和Node
和大部分引擎一样,Urho3D中也用Scene和Node的概念,所有要渲染的对象(Drawable)或者其他如Camera,Light等都需要“挂载”到一个Node上,并加入到Scene中管理。Scene继承自Node,它作为根节点,而其他的Node作为它的子节点。Node节点主要用来记录对象的位置信息,可以看到Node的成员worldTransform_,代表该节点在世界坐标系上的位置。
Component
上面所讲,场景中的对象都要“挂载”到一个Node上,通过Component(组件)实现。如果一个对象要挂载到Node上,就需要继承Component。值得一提的是,这是一种“组合”方式,也可以使用“继承”的方式,因为我们可以把每个对象看成“是”一个Node(is-a),cocos2D中就是用继承的方式,比如Cocos2D中的精灵Sprite3D类就继承自Node:
Octree
Octree,即八叉树,主要用来判断场景中的物体是否需要渲染。因为如果场景中的节点处于Camera的视野范围之外,那就没有必要渲染,可以优化性能。后面再分析。
Drawable
Drawable是一个非常重要的对象,所有可渲染的对象要继承Drawable。Drawable对象中的boundingBox_记录该对象的大小,用于可见性判断等。同时,Drawable中还有渲染对象的所有渲染的信息(在batches_成员中)。
Camera
如果没有Camera还玩个毛线....
Urho3D模型相关的对象
模型(Model)解析相关的类如下:
模型解析相关类
Model
Model类继承自Resource,也就是说,它是负责模型文件加载的。Urho3D中的所有模型文件都是mdl格式,而Model也只能解析这种格式,有点蛋疼...加载完成后会把模型的各种信息(如顶点,法线,骨骼动画)等保持到它的成员中。
Geometry
顾名思义,Geometry就存储着模型的几何信息,主要有成员vertexBuffers_即顶点缓冲,indexBuffer_即索引缓冲,rawElements_即顶点缓冲数据的内存分布。
VertexBuffer,IndexBuffer和VertexElement
和Grapichs类似,VertexBuffer和IndexBuffer也是底层图形API的封装,他们保持对外接口不变,并对不同平台图形api采用不同的实现,来达到跨平台的目的。可以看到他们都继承自GPUObject,而GPUObject中的GPUObjectHandle是一个联合体,可以灵活的选择用于Direct3D或OpenGL。
因本文以iOS为例,VertexBuffer,IndexBuffer对应OpenGL的VBO,可以看下VertexBuffer的create函数便可见端倪:
bool VertexBuffer::Create()
{
if (graphics_) {
if (!object_.name_)
glGenBuffers(1, &object_.name_);
if (!object_.name_) {
URHO3D_LOGERROR("Failed to create vertex buffer");
return false;
}
graphics_->SetVBO(object_.name_);
glBufferData(GL_ARRAY_BUFFER, vertexCount_ * (size_t)vertexSize_, nullptr, dynamic_ ? GL_DYNAMIC_DRAW : GL_STATIC_DRAW);
}
return true;
}
而VertexElement记录着顶点缓冲数据的内存分布(对应glVertexAttribPointer()函数)。
Urho3D材质相关对象
材质相关类Material
Material也继承自Resource类,说明它是负责材质文件的加载。Urho3D的材质配置支持XML文件和JSON文件。我们可以随便打开一个Urho3D的Material文件看看,如Stone.xml:
<material>
<technique name="Techniques/DiffNormal.xml" quality="1" />
<technique name="Techniques/Diff.xml" quality="0" />
<texture unit="diffuse" name="Textures/StoneDiffuse.dds" />
<texture unit="normal" name="Textures/StoneNormal.dds" />
<shader psdefines="PACKEDNORMAL" />
<parameter name="MatSpecColor" value="0.3 0.3 0.3 16" />
</material>
可以看到它包含了子元素:technique:对应另一个xml文件,后面分析;texture:定义一张漫反射纹理(diffuse),一张法线纹理(normal);shader:这里的psdefines表示需要在shader文件中加上PACKEDNORMAL宏定义;parameter:定义了着色器的uniform变量MatSpecColor的值。
Technique
Technique可以简单的认为是pass的集合,代表该材质支持的pass,pass后面分析。可以打开Techniques/DiffNormal.xml文件看下:
<technique vs="LitSolid" ps="LitSolid" psdefines="DIFFMAP">
<pass name="base" />
<pass name="litbase" vsdefines="NORMALMAP" psdefines="AMBIENT NORMALMAP" />
<pass name="light" vsdefines="NORMALMAP" psdefines="NORMALMAP" depthtest="equal" depthwrite="false" blend="add" />
<pass name="prepass" vsdefines="NORMALMAP" psdefines="PREPASS NORMALMAP" />
<pass name="material" psdefines="MATERIAL" depthtest="equal" depthwrite="false" />
<pass name="deferred" vsdefines="NORMALMAP" psdefines="DEFERRED NORMALMAP" />
<pass name="depth" vs="Depth" ps="Depth" psexcludes="PACKEDNORMAL" />
<pass name="shadow" vs="Shadow" ps="Shadow" psexcludes="PACKEDNORMAL" />
</technique>
可以看到开头technique配置了顶点着色器(vs="LitSolid")和片元着色器(ps="LitSolid")的名字,以及着色器的宏定义(psdefines="DIFFMAP")。Urho3D自身有许多基本着色器代码,并可以通过配置宏定义来灵活定制着色器真正执行的工作,如LitSolid片元着色器中的代码是这样,像刚才的technique中配置了DIFFMAP宏定义,那么着色器中相应的代码便会被编译进去:
void main()
{
// Get material diffuse albedo
#ifdef DIFFMAP
vec4 diffInput = texture2D(sDiffMap, vTexCoord.xy);
#ifdef ALPHAMASK
if (diffInput.a < 0.5)
discard;
#endif
vec4 diffColor = cMatDiffColor * diffInput;
#else
vec4 diffColor = cMatDiffColor;
#endif
//省略....
}
Pass
从Pass类的成员不难看出,一个Pass指定了在一次DrawCall前需要对GPU配置的所有状态,以及需要加载的着色器和参数。如blendMode_指定GPU的混合模式,cullMode_指定GPU的剔除模式,depthTestMode_指定GPU的深度测试模式等。vertexShaderName_ 和pixelShaderName_ 是当前渲染对应的着色器,vertexShaderDefines_和pixelShaderDefines_是加到着色器代码中的宏定义。
网友评论