1. Renderer子系统
1.1 Node、Component/Drawable
1.1.1 基本概念
Component是绘制元件。Component接口定义了虚拟函数OnSceneSet()和OnMarkedDirty()。
Drawable是可见的绘制元件。StaticModel是从建模文件得到的模型。 它是可见的,从Drawable派生。Light是光照,是一种特殊的Drawable。Camera是摄像头,不可见,直接从Component派生。
每个Component实例都要绑到一个Node上,每个Node下的一组Component合作完成一个任务。Node的成员components_保存这组实例。
所有Node组成一棵树,Scene是Node树的根,它是一种特殊的Node。
DRAWABLE_TYPE 指定drawable的类型,在C++对象向上转型时使用。DRAWABLE_LIGHT是光照,DRAWABLE_GEOMETRY是其他一般类型。
Octant和Octree是特别为Drawable元件准备的,它们按照空间位置给Drawable实例分组。Drawable实例要放置在一个Octant空间中。Octant的成员drawables_保存从属于它的这组Drawable实例。
Octant(卦限)是数学中的一个基本概念。在空间立体几何中,由相互垂直的坐标轴X轴、Y轴、Z轴,把整个空间划分成八个部分,其中每一部分称为一个卦限(Octant)。
这里的Octant差不多也是这个意思。 Octant的成员children_是一个Octant数组。Octant是嵌套的,所有的Octant组成一棵树。Octree就是这棵树的根,它是一个特殊的Octant。
Octant的成员worldBoudingBox_指定一个长方体空间,Drawable的成员boundingBox也指定一个长方体空间。向Octant实例中加入Drawable实例时,根据它俩的相对大小和位置决定,是直接加到这个Octant,还是加到这个Octant的子Octant中。
下图描述了Node、Component和Octant的从属关系。
1.1.2 创建Octant
将Drawable绑到Octant时,是否需要创建Octant的规则如下图所示。(Octant的空间是三维的,这里画的是二维的,道理一样)。
- 图中浅绿色的四边形是当前Octant的空间。
- 如果Drawable在Octant的1/4空间(图中深绿的部分)中放不下,则加入当前Octant;
- 如果Drawable至少有部分在Octant的5/4空间(也就是淡黄色部分)的外面(也就是图中蓝色部分),则加入当前Octant;
- 其他情况,也就是Drawable在Octant的1/4空间放得下,并且不会伸到Octant的5/4空间的外面。这时需要创建子Octant,将Drawable加到子Octant中去。
创建子Octant的规则如下。
- 创建子Octant有8个卦限可选,用一个3bit数字表示8个卦限。Drawable的中心位置相对于Octant的中心位置(图中的Center点)决定使用哪个卦限,其中每个坐标轴的相对位置决定3bit中的1个bit。
- 子Octant的worldBoundingBox_指定为对应卦限的空间(相当于从当前Octant裁掉其他卦限)。
- 以上过程是嵌套的。如果Drawable实例的大小与子Octant相比还是小,则将再创建更第下一层的孙Octant。这个过程一直递归,直到得到大小合适的Octant。
- 一开始只有Octree这一个Octant,大小是((-1000, -1000, -1000),(-1000, -1000, -1000))。加入若干Drawable后,就变成了一颗Octant树了。
1.1.3 创建Component/Drawable
Scene::CreateComponent()用于创建Component。这里以Drawable为例说明。Drawable实现了虚拟函数OnSceneSet()和OnMarkedDirty(),进行操作Octant相关操作。
- CreateComponent
()调用另一个没有模板参数的CreateComponent(),后者创建Component对象,并将它加入Node树。 - 在Drawable::OnSceneSet()中,得到Octant树的根节点Octree,使用它调用Octant::InsertDrawable()将Drawable找到合适的Octant节点。InsertDrawable()就是递归创建Octant实例的过程。
- 找到合适的Octant节点后,调用Octant::AddDrawable()将Drawable保存在Octant的成员drawables_中。
1.2 创建StaticModel元件
以Urho3D的例子StaticScene为例。
在StaticScene::Start()中,
- 创建Scene实例,这是Node树的根。
- 在Scene下创建Octree实例,也就是Octant树的根。
- 创建第一个子Node,在这个Node下创建一个Light元件。
- 创建第二个子Node,在这个Node下创建一组蘑菇3D图。蘑菇图是一个StaticModel元件,通过改变位置、方向生成一组图像。调用StaticModel::SetModel()从建模文件加载模型。
- 创建第三个子Node,在这个Node下创建Camera元件。
调用StaticScene::SetViewport()设置视口。
- 创建Viewport实例
- 调用Renderer::Setviewport()将Viewport实例保存在Renderer的成员viewports_中。
SourceBatch在它的成员geometry_中保存顶点数据。Drawable的成员batches_是一组SourceBatch实例。
调用StaticModel::SetModel()设置Model。
- 在ResetLodLevels()中,将Model的成员geometries_中的Geometry实例一一赋值给Drawable::batches_中的成员geometry_。
1.3 Renderer::Update()
Viewport负责将Scene,Camera和RenderPath组织在一起。
View与Viewport对等,它从Viewport分担了渲染的功能。调用Viewport::AllocateView(),可以从Viewport实例创建View实例。
Renderer持有一组Viewport/View实例,并将主要的渲染工作委托给View。
在Renderer::Update()中,从Renderer的成员viewports_创建View实例,保存在成员views_中。
- 在UpdateQueuedViewport()中,调用Viewport::AllocateView()创建View实例。
- 调用View::Update()收集Drawable及其包含的顶点数据。
1.3.1 View::Update()
View::Update()从Octant树收集Drawable实例,然后从它们创建BatchGroup实例,保存到BatchQueue中。后面就可以绘制这些BatchGroup实例了。
View::Update()的步骤如下图所示。
- 调用GetDrawable()得到符合条件的Drawable实例。
- 在GetBatches()中,先调用ProcessLights()处理光照,在调用G
1.3.2 View::GetDrawables()
View::GetDrawables()按照FrustumOctreeQuery指定的过滤条件查询Octant树中的所有Drawable实例,将结果保存在成员geometries_中,这是一个Drawable数组。
- 定义本地变量tempDrawable,这是一个Drawable数组。引用它构造FrustumOctreeQuery实例。
- 调用Octree::GetDrawable(),设置FrustumOctreeQuery实例。因为后者引用tempDrawable,所以实际上是设置tempDrawable。关于FrustumOctreeQuery,见下一节“Octree::GetDrawables()”的说明。
- 在工作队列中,调用CheckVisibilityWork(),遍历tempDrawable,设置View的成员sceneResults_,sceneResults_是一个以线程id为索引的PerThreadSceneResult数组。为了高效并行处理,每个工作线程将结果写入数组中属于自己的PerThreadSceneResult实例。
- 将Drawable保存到PerThreadSceneResult的成员geometries_。其中Light被特殊处理,特别保存在成员lights_中。
- 遍历sceneResults_,将sceneResults的成员geometries_搬运到View的成员geometries_中,成员lights_搬运到View的成员geometries_。
1.3.3 Octree::GetDrawables()
Octree::GetDrawable()得到Octant树中的所有Drawable实例。
GetDrawable()调用GetDrawableInternal()。
- 调用OctreeQuery::TestOctant(),用与Octant有关的条件过滤Drawable实例
- 调用OctreeQuery::TestDrawable(),用与Drawable有关的条件过滤Drawable实例。
Octree::GetDrawable()的参数是一个OctreeQuery实例,找到的Drawable实例保存在它的成员result_中。
OctreeQuery是个查询接口,定义了TestDrawable()和TEstOctant()两个虚拟函数。FrustumOctreeQuery和BoxOctreeQuery则是这个接口的实现,它们实现不同的过滤方式。FrustumOctreeQuery使用摄像头的锥头体,就是Frustum,裁剪Drawable实例。
Frustum定义了锥头体,它的成员planes_指定6个面,成员vertices_指定8个顶点。BoudingBox是长方体,Sphere是球体。
1.3.4 View::ProcessLights()
LightQueryResult的成员包括一个Light实例light_,以及从属于它的一组Drawable实例litGeometries_。light_是光照,光照可以只应用于litGeometries_中的特定顶点,也可以应用于所有顶点。
View::ProcessLights()根据View的成员lights_将View::geometries_中的Drawable实例分组,保存到成员lightQueryResults_中,这是一组LightQueryResult实例。
- 将成员lightQueryResults_的大小设置为成员lights的大小。
- 在工作队列中调用ProcessLightWork(),将View::lights_中的实例一一传给它。ProcessLightWork用Light对View::geometries_作掩码检查,符合条件的Drawable实例保存到lightQueryResults_对应的实例中。
- Light类型不同,则掩码检查的方式不同。Light类型可以是LIGHT_DIRECTIONAL、LIGHT_SPOT、LIGHT_POINT。
1.3.5 View::GetLightBatches()
Batch用于一次绘制操作的对象,从Drawable的成员batches_构造,batches_是一个SouceBatch数组。
BatchGroup从Batch构造,加入多实例化数据InstanceData。
BatchQueue保存一组BatchGroup,这是一个从BatchGroupKey到BatchGroup的映射。BatchGroupKey支持操作符 == 和 != ,所以可以用作映射的key。
LightBatchQueue包括与光照有关的几个BatchQueue,如litBaseBatches_和litBatches_。
View::GetLightBatches()遍历成员lightQueryResults_,对其中每个实例,又遍历LightQueryResult的成员litGeometries_。对其中保存的每个Drawable实例, 创建BatchGroup实例,保存到View::lightQueues_的成员litBaseBacthes_或litBatches_中。
- 将成员lightQueues_的大小设置成跟成员lightQuryResults_的大小一样。
- 遍历成员lightQueryResults_数组。对每一个LightQueryResult实例,
- 如果light_是应用于所有顶点,则设置成员中lightQueues_的一个实例。
- 调用Drawable::AddLight()给Drawable对象设置光照。
- 调用GetLitBatches()从Drawable构建LightBatchQueue实例。
GetLitBatches()的工作是:
- 遍历Drawable的成员batches。从这个SourceBatch实例,构建Batch实例,然后调用AdBatchToQueue()将它加入LightBatchQueue。Batch的成员isBase_表示加入LightBatchQueue的哪个队列,是litBaseBatches_,还是litBatches_。
AddBatchToQueue()的工作是:
- 从Batch构造BatchGroupKey实例,并在BatchQueue的成员batchGroups_中查询对应的BatchGroup实例。如果查不到,则从Batch创建新的BatchGroup实例。
- 调用BatchGroup::AddTransforms(),添加多实例数据,如Batch的成员worldTransform_包括的模型变换数据。
1.4 Renderer::Render()
Renderer::Render()遍历成员views_中的实例,调用View::Render()进行渲染。
而View::Render()主要调用View::UpdateGeometries()和View::ExecuteRenderPathCommand()。
1.4.1 View::UpdateGeometries()
UpdateGeometries()负责渲染前的准备工作。
- 在工作队列中调用SortLightQueueWork(),对View的成员lightQueues_中的每个元素排序。lightQueues_是LightBatchQueue数组,所以其实是对每个LightBatchQueue的成员litBatches_和litBaseBatches_分别排序。
- litBatches和litBaseBatches_是BatcheQueue数组,对它们依次调用Batch::SortFrontToBack()。BatchQueue的成员batchGroups_保存了用于渲染的BatchGroup实例。排序后,将结果保存在成员sortedBatchGroups_中。
- 在SortFrontToBack()中,遍历batchGroups,将其中的BatchGroup实例挨个复制到sortedBatchGroups_中。然后调用SortFrontToBack2Pass()对sortedBatchGroups_排序。
- 在SortFrontToBack2Pass()中,遍历sortedBatchGroups_,设置Batch实例的成员sortKey_的值。然后对sortedBatchGroups排序,排序基准是包括sortKey_在内的一组成员。
1.4.2 View::ExecuteRenderPathCommand()
URhoD将渲染过程组织成多条指令,也就是RenderPathCommand。
- 成员type_是指令类型。枚举类型RenderCommandType指定类型可选值,其中CMD_CLEAR是清理工作,CMD_FORWARDLIGHTS是绘制。
- 其他成员如vertexShaderName_、vertexShaderDefines_等是指令的参数。
RenderTargetInfo是渲染目标。RenderPath将RenderTargetInfo和RenderPathCommand组合在一起。
View::ExecuteRenderPathCommand()遍历成员renderPath_中的指令,根据指定类型分别处理。
这里处理CMD_FORWARDLIGHTS指令。
- 根据RenderPathCommn中的参数,调用SetRenderTarget()和SetTextures()进行设置。
- 遍历成员lightQueues_,对其中的LightBatchQueue实例依次调用BatchQueue::Draw()进行绘制。
1.4.3 BatchQueue::Draw()
BatchQueue::Draw()做真正的绘制工作。
- 遍历成员sortedBatcheGroups_,调用BatchGroup::Draw()绘制多实例化的顶点。
- 遍历成员sortedBatches_,调用Batch::Draw()绘制非实例化的顶点。
BatchGroup::Draw()的工作如下:
- 调用Batch::Prepare()。设置Shader和Program,Shader的参数、texture等。
- 调用Graphics::SetIndexBuffer(),其中调用glBindBuffer()绑定index buffer,也就是索引数据。
- 调用Graphics::SetVertexBuffer(),将顶点数据保存在Graphics的成员vertexBuffers_中。
- 遍历成员instances_。在循环中,先调用Graphics::SetShaderParameter()设置模型转换矩阵。
- 再调用Graphics::Draw()绘制。
这里的Graphics::Draw()使用glDrawElements()绘制。
1.5 数据流动
如下是按照以上渲染过程画的数据流动图。
相关链接
Urho3D 1.7.1 源代码分析 (一)
Urho3D 1.7.1 源代码分析 (二)
Urho3D 1.7.1 源代码分析 (三)
Urho3D 1.7.1 源代码分析 (四)
Urho3D 1.7.1 源代码分析 (五)