前言
上一篇文章简单介绍了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函数的调用堆栈:
可以看下图中的每个存储对象的队列的结构:
判断drawable是否可见
首先,只有在Camera的视野范围之内的对象,才有可能出现在渲染的图像上。对于透视投影的Camera来说,就是drawable必须处于Camera的Frustum(视锥体)中。如果将不可见(在Frustum之外)的对象也渲染一次,那么就白白浪费了Drawcall。
另外,判断一个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已经包含了模型(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(提前深度测试)。如下:
正常的深度测试是在片元着色器执行完之后进行的,但目前许多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 {
//省略...
}
}
}
说明已在代码中做了注释。这里算是走完了一次渲染流程。
网友评论