Urho3D源码分析(二):渲染流程

2020-02-22  本文已影响0人  奔向火星005

前言

上一篇文章简单介绍了Urho3D的大概框架及主要对象,本文分析下Urho3D的渲染相关流程。为分析代码方便,本文主要分析Urho3D的前向渲染流程。

前向渲染与延迟渲染

先简单了解下渲染路径的概念。

前向渲染与延迟渲染是引擎中的两种渲染路径(rendering path)。渲染路径决定了在整个渲染过程中如何处理光源。前向渲染路径(Forward Rendering Path)简单的说就是,对于场景中的每一个光源的影响,都要执行一次渲染命令(Drawcall),例如场景中有5个物体,5个光源,那么总的渲染次数是5x5=25。而延迟渲染路径(Defered Rendering Path)是指,先不考虑光源的影响,先把场景中所有的物体渲染到一个叫Gbuffer的缓冲区中,然后再对该缓冲区做逐像素光照处理,这样总的渲染次数就只有5+5=10次。

分析中所用的Sample

本文的分析基于Urho3D在iOS平台的工程中的05_AnimatingScene的例子,可以看下该例子在创建场景的代码,为了方便分析我在最后又加了一个点光源:

void AnimatingScene::CreateScene()
{
    auto* cache = GetSubsystem<ResourceCache>();

    scene_ = new Scene(context_);
    scene_->CreateComponent<Octree>();

    Node* zoneNode = scene_->CreateChild("Zone");
    //省略zoneNode的配置...

    const unsigned NUM_OBJECTS = 2000;
    for (unsigned i = 0; i < NUM_OBJECTS; ++i)
    {
        Node* boxNode = scene_->CreateChild("Box");
        boxNode->SetPosition(Vector3(Random(200.0f) - 100.0f, Random(200.0f) - 100.0f, Random(200.0f) - 100.0f));

        boxNode->SetRotation(Quaternion(Random(360.0f), Random(360.0f), Random(360.0f)));
        auto* boxObject = boxNode->CreateComponent<StaticModel>();
        boxObject->SetModel(cache->GetResource<Model>("Models/Box.mdl"));
        boxObject->SetMaterial(cache->GetResource<Material>("Materials/Stone.xml"));
        auto* rotator = boxNode->CreateComponent<Rotator>();
        rotator->SetRotationSpeed(Vector3(10.0f, 20.0f, 30.0f));
    }

    cameraNode_ = scene_->CreateChild("Camera");
    auto* camera = cameraNode_->CreateComponent<Camera>();
    camera->SetFarClip(100.0f);

    auto* light = cameraNode_->CreateComponent<Light>();
    light->SetLightType(LIGHT_POINT);
    light->SetRange(30.0f);
    
    //增加一个点光源
    Node* lightNode = scene_->CreateChild("AddLight");
    lightNode->SetPosition(cameraNode_->GetPosition() + Vector3(5, 5, 5));
    auto* light2 = lightNode->CreateComponent<Light>();
    light2->SetLightType(LIGHT_POINT);
    light2->SetRange(30.0f);
}

最终渲染的画面是这样的:


05_AnimatingScene
前向渲染的大致流程

先画个大致的流程看看


渲染流程

简单说下图中的两个重要对象,Drawable和Batch。
Drawable,所有渲染对象都要继承自Drawable(如sample中的StaticModel),资源加载后的信息其实都放在Drawable的batches_成员中了,它也是一个Component,因此可以挂在到Node上。
Batch,可以认为一个Batch内包含了一次渲染需要的所有信息。包括光源信息,顶点缓冲数据,转换矩阵,着色器,纹理,着色器变量等等。Urho3D是把所有的绘制信息都统一放入到一个Batch中,再根据Batch来执行GPU绘制命令的。这个后面还会讲到。

从图中可以看出,前向渲染大致可以分为三个阶段,第一个阶段是剔除对摄像头来说不可见的对象,第二个阶段是把对象按照光源影响进行分组,并产生对应的Batch,第三个阶段就是根据Batch进行绘制。

下面开始对上面的阶段进行详细分析。

Renderer

前面的文章说过,Renderer类是渲染的核心。引擎的大部分逻辑都在Renderer的Update函数和Render函数中

Renderer最重要的成员是viewports_和views_:


Renderer类

Viewport对应一个窗口,Urho3D支持多窗口渲染。View负责实际的渲染工作。可以看下Renderer的update函数和render函数的调用堆栈:


可以看到Renderer实际上是通过View来实现对每个窗口的绘制的。

RenderPath

Urho3D的默认的渲染路径是前向渲染,是在Renderer的初始化函数中加载的:

void Renderer::Initialize()
{
//省略....
    defaultRenderPath_ = new RenderPath();
    defaultRenderPath_->Load(cache->GetResource<XMLFile>("RenderPaths/Forward.xml"));
//省略....
}

可以看下Forward.xml的内容:

<renderpath>
    <command type="clear" color="fog" depth="1.0" stencil="0" />
    <command type="scenepass" pass="base" vertexlights="true" metadata="base" />
    <command type="forwardlights" pass="light" />
    <command type="scenepass" pass="postopaque" />
    <command type="scenepass" pass="refract">
        <texture unit="environment" name="viewport" />
    </command>
    <command type="scenepass" pass="alpha" vertexlights="true" sort="backtofront" metadata="alpha" />
    <command type="scenepass" pass="postalpha" sort="backtofront" />
</renderpath>

可以看到renderpath其实是一个个command,它们存储在RenderPath类的commands_成员数组中。在这个sample中,主要会执行前面三个command(clear,pass="base"的scenepass,以及forwardlights),其他的scenepass因为sample中的渲染对象不支持,所以是不需要执行的。这个后面还会分析。

View的Update函数

View的update函数的工作主要完成上面的渲染流程图中前两个阶段的事情,即剔除对摄像头来说不可见的对象,把对象按照光源影响进行分组,并产生对应的Batch。
可以看下update函数的调用堆栈:

update函数的调用堆栈

可以看下图中的每个存储对象的队列的结构:


判断drawable是否可见

首先,只有在Camera的视野范围之内的对象,才有可能出现在渲染的图像上。对于透视投影的Camera来说,就是drawable必须处于Camera的Frustum(视锥体)中。如果将不可见(在Frustum之外)的对象也渲染一次,那么就白白浪费了Drawcall。

判断drawable是否可见

另外,判断一个Drawable是否在Frustum中是一个比较耗时的操作(需要计算包围盒的中心与Frustum每一个平面的符号距离),如果我们一个个遍历drawable来判断,那么会是一笔不小的开销。这个时候就要用到Octree(八叉树)了。

Octree(八叉树)

我们知道,一个3D的立方体,可以平均分成8个小立方体,小立方体再平均分,可以分成64个小小立方体...如下图(图片来自百度图片)


Octree结构

八叉树结构就可以描述这个情形,八叉树的类如下

class Octant
{
private:
    Octant* parent_;  //它的父节点
    Octant* children_[NUM_OCTANTS]; //NUM_OCTANTS为8,它有8个子节点
    unsigned level_;   //层级
    Vector<Drawable*> drawables_;  //在该节点对应的空间中的对象
}

Octree代表场景中的最大空间,也就是八叉树的根节点,level为0。它的8个子节点,分别代表它的八个子空间,level为1。每个子节点的空间又可以继续分下去,这样一层层递归,直到达到最大层级DEFAULT_OCTREE_LEVELS(默认为8)。Octree代表的最大空间为-DEFAULT_OCTREE_SIZE到DEFAULT_OCTREE_SIZE(默认为1000)。

drawable添加到Octree

其实在drawable被挂载到Node上的时候,就已经被添加到八叉树结构中。插入操作在Octant的InsertDrawable()成员函数。插入的过程是一个递归的过程,添加的逻辑可以看下面简化后的代码:

void Octant::InsertDrawable(Drawable* drawable)
{
    const BoundingBox& box = drawable->GetWorldBoundingBox();

    bool insertHere;
    insertHere = CheckDrawableFit(box);  //判断是否应该插入该Octant

    if (insertHere)  //如果是则插入,递归结束
    {
        AddDrawable(drawable);  
    }
    else  
    {
        //否则,根据box的中心点,选择位置合适的八分之一子空间
        Vector3 boxCenter = box.Center();
        unsigned x = boxCenter.x_ < center_.x_ ? 0 : 1;
        unsigned y = boxCenter.y_ < center_.y_ ? 0 : 2;
        unsigned z = boxCenter.z_ < center_.z_ ? 0 : 4;

        //创建子空间,并递归调用InsertDrawable
        GetOrCreateChild(x + y + z)->InsertDrawable(drawable);
    }
}

那么,drawable应该添加到八叉树的哪一层的哪一个子节点中呢?首先,drawable能够添加到该Octant的前提是,Octant的空间能够完全容纳drawable的包围盒。假如能够容纳drawable,那么再判断它是否有子空间也能完全容纳drawable,如果是,则往下层继续创建子空间,否则就drawable就添加到该Octant,终止递归。

如何判断子空间是否能够完全容纳一个drawable,其实也不难想到,比如下图(3D太难画,就画了个2D,3D类似):


第一种情况,如果drawable的直径超过了Octant的一半(也就是子空间的直径),那么肯定是不可能再放到子空间了;第二种情况,虽然drawable的直径很小,但是它靠近Octant的中心,也就是说drawable在每一个子空间都占着一点地方,没有哪一个子空间可以“独占”drawable,这样也不能再往下分子空间。
判断的逻辑在CheckDrawableFit函数函数中,这里不再贴出来了。

GetDrawables()函数

GetDrawables()函数判断drawable是否在Frustum中,并将结果放到tempDrawables中。有了Octree结构,就不需要遍历每个drawable来判断是否在摄像头的Frustum中了,而是改为在Octree中取寻找。GetDrawables函数的代码简化如下:

void View::GetDrawables()
{
    //....省略

    PODVector<Drawable*>& tempDrawables = tempDrawables_[0];

    //....省略
    FrustumOctreeQuery query(tempDrawables, cullCamera_->GetFrustum(), DRAWABLE_GEOMETRY | DRAWABLE_LIGHT, cullCamera_->GetViewMask());
    octree_->GetDrawables(query);

    //....省略
}

先构造一个FrustumOctreeQuery用于请求的命令,它内部有用于存放结果的tempDrawables数组,以及Camera的Frustum,还有标志位DRAWABLE_GEOMETRY和DRAWABLE_LIGHT,标志位的意思是只在Octree中查找Gemoetry类型和Light类型的drawable。GetDrawables函数内部调用Octant的GetDrawablesInternal函数,代码简略如下:

void Octant::GetDrawablesInternal(OctreeQuery& query, bool inside) const
{
    if (this != root_)  {  //如果不是根节点,先判断自身是否在Frustum中
        Intersection res = query.TestOctant(cullingBox_, inside);
        if (res == INSIDE)
            inside = true;
        else if (res == OUTSIDE) {  //如果完全不在,则不需要再往下判断了
            return;
        }
    }
    //如果是根节点,或者在Frustum之中或与Frustum相交,分下面两步
    //1.先遍历它自身的drawable是否在Frustum中
    if (drawables_.Size())  {    
        auto** start = const_cast<Drawable**>(&drawables_[0]);
        Drawable** end = start + drawables_.Size();
        query.TestDrawables(start, end, inside);
    }

   //2.遍历child,递归调用GetDrawablesInternal
    for (auto child : children_)  {
        if (child)
            child->GetDrawablesInternal(query, inside);
    }
}

如上图,当Octant完全处于Frustum之外时,就不需要再往下判断了,直接返回。如果Octant完全处于Frustum之内,那么inside就会为true,Octant自身以及它子节点的所有drawable都可以快速判断为在Frustum之内,这是把inside标志往下传递实现的。当Octant与Frustum相交时,则需要继续做相交测试来判断。剩下的逻辑在query.TestOctant和query.TestDrawables中,这里不再赘述了。

找出在Frustum中的Drawable会暂存在tempDrawables中,然后再由CheckVisibilityWork函数把它们放到View的成员sceneResults_中。相关代码如下:

void View::GetDrawables()
{
    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);

//省略...

        for (unsigned i = 0; i < sceneResults_.Size(); ++i)
        {
            PerThreadSceneResult& result = sceneResults_[i];
            geometries_.Push(result.geometries_);
            lights_.Push(result.lights_);
            minZ_ = Min(minZ_, result.minZ_);
            maxZ_ = Max(maxZ_, result.maxZ_);
        }
}

这里CheckVisibilityWork函数是通过多线程完成的(上一篇文章说过)。至于CheckVisibilityWork函数的做了一些的可见性判断(还没完全看明白),将tempDrawables的drawable放到sceneResults_的geometries_数组和lights_数组后,再遍历sceneResults_,把元素转移到View的geometries_lights_成员数组中。
现在,流程图走到这一步。

GetBatches()函数

View的GetBatches()函数 主要执行ProcessLights(),GetLightBatches()和GetBaseBatches()函数。

ProcessLights()函数

ProcessLights()函数主要是对sceneResults_中的几何类型的Drawable根据它受光源的影响进行分组,并放到lightQueryResults_中。代码简略如下:

void View::ProcessLights()
{
    auto* queue = GetSubsystem<WorkQueue>();
    lightQueryResults_.Resize(lights_.Size());  //根据lights_的数量重置数组大小

    for (unsigned i = 0; i < lightQueryResults_.Size(); ++i)
    {
        SharedPtr<WorkItem> item = queue->GetFreeItem();
        item->priority_ = M_MAX_UNSIGNED;
        item->workFunction_ = ProcessLightWork;  //工作函数
        item->aux_ = this;

        LightQueryResult& query = lightQueryResults_[i];
        query.light_ = lights_[i];

        item->start_ = &query;
        queue->AddWorkItem(item);
    }

    queue->Complete(M_MAX_UNSIGNED);
}

可以看到判断受光照影响的操作也是在多线程中进行的,真正执行的函数是ProcessLightWork函数。工作的内容大概如下图:


完成ProcessLights函数后,流程执行到下面:


GetLightBatches()函数

View的GetLightBatches()函数主要的任务就是,根据lightQueryResults_中的drawable生成对应的batch,放入到lightQueues_队列中。源码中的GetLightBatches函数还做了阴影,延迟渲染相关的许多处理,为方便分析下面的代码只是简化:

void View::GetLightBatches()
{
        unsigned numLightQueues = 0;
        unsigned usedLightQueues = 0;
        for (Vector<LightQueryResult>::ConstIterator i = lightQueryResults_.Begin(); i != lightQueryResults_.End(); ++i)  {
            if (!i->light_->GetPerVertex() && i->litGeometries_.Size())
                ++numLightQueues;
        }
        lightQueues_.Resize(numLightQueues);  //numLightQueues表示有多少个光源

        for (Vector<LightQueryResult>::Iterator i = lightQueryResults_.Begin(); i != lightQueryResults_.End(); ++i)
        {
            LightQueryResult& query = *i;
            if (!light->GetPerVertex()) {
                LightBatchQueue& lightQueue = lightQueues_[usedLightQueues++];

                for (PODVector<Drawable*>::ConstIterator j = query.litGeometries_.Begin(); j != query.litGeometries_.End(); ++j)  {
                    //为每个drawable生成batch,并放入到lightQueue中
                    GetLitBatches(drawable, lightQueue, alphaQueue);
                }
             }
        }
}
生成Drawable对应的Batch

GetLitBatches()函数的工作就是根据drawable生成对应的batch。

Batch

前面简单说过,一个Batch内包含了一次渲染需要的所有信息。可以看下Batch中的主要成员变量。

Batch

可以看到,Batch已经包含了模型(geometry_),材质(material_),位置(worldTransform_),光照(lightQueue_),着色器参数(pass_),着色器(vertexShader_和pixelShader_)等所有渲染需要的信息。Urho3D是把所有的绘制信息都统一放入到一个Batch中,再根据Batch来执行GPU绘制命令的。

从上图还可以看到另一个类BatchGroup,它继承自Batch,只是比Batch多了一个InstanceData类型的数组。而InstanceData内部只有一个worldTransform_变换矩阵。那BatchGroup是干啥用的呢?我们可以想一下,比如我们的sample中,渲染的对象全部都是box,他们除了位置不同之外,其他的所有信息几乎都是一样的,如果我们为每一个box(drawable)都生成一个batch,那其实很浪费内存。更聪明的做法是,把这些除了位置不同之外,其他信息都相同的batch,看成是一个Group,而位置信息就存储在BatchGroup的InstanceData数组中,那么就可以用一个BatchGroup来代表多个Batch了

GetLitBatches()函数

GetLitBatches()函数的代码简略如下:

void View::GetLitBatches(Drawable* drawable, LightBatchQueue& lightQueue, BatchQueue* alphaQueue)
{
    Light* light = lightQueue.light_;
    Zone* zone = GetZone(drawable);
    const Vector<SourceBatch>& batches = drawable->GetBatches(); //从drawable中取出batches数组

    bool allowLitBase =
        useLitBase_ && !lightQueue.negative_ && light == drawable->GetFirstLight() && drawable->GetVertexLights().Empty() &&
        !zone->GetAmbientGradient();

    for (unsigned i = 0; i < batches.Size(); ++i)
    {
        const SourceBatch& srcBatch = batches[i];  

        Batch destBatch(srcBatch);  //根据SourceBatch构造Batch
        bool isLitAlpha = false;

        if (i < 32 && allowLitBase)  //第一次光照,用litBasePassIndex_
            destBatch.pass_ = tech->GetSupportedPass(litBasePassIndex_);
        else   //后面的光照用lightPassIndex_
            destBatch.pass_ = tech->GetSupportedPass(lightPassIndex_);

        if (destBatch.isBase_)  //如果是第一次光照处理,则加入litBaseBatches_
            AddBatchToQueue(lightQueue.litBaseBatches_, destBatch, tech);
        else  //后面的光照处理加入litBatches_
            AddBatchToQueue(lightQueue.litBatches_, destBatch, tech);
     }
}

从代码中可看到首先会从drawable中取出一个类型为SourceBatch的数组。前面的文章说过,在渲染对象的模型和材质文件加载后,所有的信息其实就放在batches_数组中。

然后用SourceBatch构造Batch,其实就是把SourceBatch中的信息传给了Batch,Batch的构造函数如下:

   Batch::Batch(const SourceBatch& rhs) :
        distance_(rhs.distance_),
        renderOrder_(rhs.material_ ? rhs.material_->GetRenderOrder() : DEFAULT_RENDER_ORDER),
        isBase_(false),
        geometry_(rhs.geometry_),
        material_(rhs.material_),
        worldTransform_(rhs.worldTransform_),
        numWorldTransforms_(rhs.numWorldTransforms_),
        instancingData_(rhs.instancingData_),
        lightQueue_(nullptr),
        geometryType_(rhs.geometryType_)
    {
    }
litBasePass和lightPass

在GetLitBatches()函数中可以看到一个标志位allowLitBase,字面意思是是否允许基础光照。其实它主要是用来区分第一次光照处理和后续的光照处理。可以看到其中一个判断条件是light == drawable->GetFirstLight()。

那为什么要区分第一次光照处理和后续的光照处理呢?前面我们将前向渲染路径的时候说过,前向渲染的处理方式就是对每一个光源都进行一次Drawcall,也可以认为是一个Pass。结合OpenGL的GPU渲染过程,当OpenGL在片元着色器中处理完一次逐像素光照后,把结果放到一个Framebuffer中。从片元着色器到Framebuffer之间,每个像素其实还要经过blend测试,depth测试,alpha测试等,才会进入到Framebuffer。当第一次光照处理的时候,因为这时候的Framebuffer的内容是没用的,那么我们应当把关闭混合测试,直接覆盖掉Framebuffer中的内容,而当后续光照处理时,因为处理的结果仍然要放到同一个Framebuffer中,我们应该把Blend模式设为与之前的光照结果叠加,才能得到多光源处理的结果。

所以我们看到代码中的 tech->GetSupportedPass(litBasePassIndex_)和tech->GetSupportedPass(lightPassIndex_)取出来的Pass,主要的区别是Pass的成员blendMode_不一样,litBasePassIndex_的是BLEND_REPLACE,而lightPassIndex_的是BLEND_ADD

除此之外,第一次光照的batch和后续光照的batch放的队列也不一样,分别是LightBatchQueue的litBaseBatches_和litBatches_。

现在流程走到了:


GetBaseBatches()函数

GetBaseBatches()函数主要的工作就是把没有收到光照影响的drawable放到scenePasses_.batchQueue_队列中去,这个与前面的GetLitBatches类似,不再赘述。

到目前为止,我们已经把View的Update函数的流程都走完了,接下就到渲染函数Render()。

View的Render()函数

Render()函数函数其实比Update()函数简单多了,因为Update()函数基本上已经把需要准备的信息都放到Batch队列里面了,Render函数只要根据Batch渲染就完事了。

Render()函数的大致流程:


Render()函数流程
batch排序

UpdateGeometries()的工作主要是对lightQueues_和batchQueues_中的所有元素进行由前往后排序。之所以由前往后排序,个人理解,应该是考虑到early depth testing(提前深度测试)。如下:

early depth testing

正常的深度测试是在片元着色器执行完之后进行的,但目前许多GPU架构已支持将深度测试放在片元着色器之前,这样如果测试失败,就不需要进行片元着色器的运算,可以大大优化性能。而我们将绘制的对象由前往后排序,实际上是增加深度测试失败的概率,从而优化性能。

UpdateGeometries()的代码不再赘述。

ExecuteRenderPathCommands()函数

剩下最后一步,先看下ExecuteRenderPathCommands()函数的内容:


ExecuteRenderPathCommands()函数

如上图所示,ExecuteRenderPathCommands()函数主要是遍历renderPath_->commands_的命令(从Forward.xml加载)。执行的主要是三个命令,CMD_CLEAR,CMD_SCENEPASS,CMD_FORWARDLIGHTS。ExecuteRenderPathCommands()中许多关于别的命令的代码,在此删除了与分析无关的代码:

void View::ExecuteRenderPathCommands()
{
//省略...
        for (unsigned i = 0; i < renderPath_->commands_.Size(); ++i)
        {
            RenderPathCommand& command = renderPath_->commands_[i];
            switch (command.type_)
            {
            case CMD_CLEAR:
                {
                    Color clearColor = command.clearColor_;
                    if (command.useFogColor_)
                        clearColor = actualView->farClipZone_->GetFogColor();
                    graphics_->Clear(command.clearFlags_, clearColor, command.clearDepth_, command.clearStencil_);
                }
                break;

            case CMD_SCENEPASS:
                {
                    BatchQueue& queue = actualView->batchQueues_[command.passIndex_];
                    queue.Draw(this, camera_, command.markToStencil_, false, allowDepthWrite);
                    }
                }
                break;

            case CMD_FORWARDLIGHTS:
                    for (Vector<LightBatchQueue>::Iterator i = actualView->lightQueues_.Begin(); i != actualView->lightQueues_.End(); ++i) 
                       {
                        // Draw base (replace blend) batches first
                        i->litBaseBatches_.Draw(this, camera_, false, false, allowDepthWrite);

                        // Then, if there are additive passes, optimize the light and draw them
                        if (!i->litBatches_.IsEmpty())  {
                            i->litBatches_.Draw(this, camera_, false, true, allowDepthWrite);
                        }

                        passCommand_ = nullptr;
                    }
                break;
               //省略...
            default:
                break;
            }
        }
}

CMD_CLEAR命令很简单,即调用graphics_->Clear()设置背景颜色。CMD_SCENEPASS则主要是调用batchQueues_中的queue执行draw函数,其实就是处理不受光照影响的batch。CMD_FORWARDLIGHTS命令则是处理lightQueues_中的batch。litBaseBatches_对应的是第一次光源的batch,litBatches_对应的是后续光源的处理。

Batch的Draw()函数

batchQueues_,litBaseBatches_,litBatches_的类型都是BatchQueue,BatchQueue的Draw()函数主要是遍历其内部的sortedBatchGroups_数组和sortedBatches_数组,对每个Batch元素调用Draw()函数。以BatchGroup的Draw()函数为例简要说明一下。代码简略如下:

void BatchGroup::Draw(View* view, Camera* camera, bool allowDepthWrite) const
{
    Graphics* graphics = view->GetGraphics();
    Renderer* renderer = view->GetRenderer();

    if (instances_.Size() && !geometry_->IsEmpty())
    {
        // Draw as individual objects if instancing not supported or could not fill the instancing buffer
        VertexBuffer* instanceBuffer = renderer->GetInstancingBuffer();
        if (!instanceBuffer || geometryType_ != GEOM_INSTANCED || startIndex_ == M_MAX_UNSIGNED) 
 {          //1.根据Batch中的数据,配置graphics的各种渲染状态和参数
            Batch::Prepare(view, camera, false, allowDepthWrite);  
           //2.设置顶点缓冲和索引缓冲
            graphics->SetIndexBuffer(geometry_->GetIndexBuffer());
            graphics->SetVertexBuffers(geometry_->GetVertexBuffers());
            //3.遍历instance
            for (unsigned i = 0; i < instances_.Size(); ++i)  {
                //更新Model矩阵
                if (graphics->NeedParameterUpdate(SP_OBJECT, instances_[i].worldTransform_))
                    graphics->SetShaderParameter(VSP_MODEL, *instances_[i].worldTransform_);   
                //向GPU发送Drawcall
                graphics->Draw(geometry_->GetPrimitiveType(), geometry_->GetIndexStart(), geometry_->GetIndexCount(),
                    geometry_->GetVertexStart(), geometry_->GetVertexCount());
            }
        }                                          
        else {
             //省略...
        }
    }
}

说明已在代码中做了注释。这里算是走完了一次渲染流程。

上一篇下一篇

猜你喜欢

热点阅读