美文网首页
Urho3D源码分析(一):框架概览

Urho3D源码分析(一):框架概览

作者: 奔向火星005 | 来源:发表于2020-02-19 23:46 被阅读0次

    前言

    一直想找一款轻量级的渲染引擎研究,之前看过一些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:

    Cocos2D中的Sprite3D
    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_是加到着色器代码中的宏定义。

    相关文章

      网友评论

          本文标题:Urho3D源码分析(一):框架概览

          本文链接:https://www.haomeiwen.com/subject/mswofhtx.html