Unity场景渲染相关实现的猜想
如下,很简单的一个场景,一个Panel,二个Cube,一个camera,一个方向光,其中为了避免灯光阴影的影响,关掉阴影,而Panel和二个Cube都是默认的材质,没做修改,我原猜,这三个模型应该都动态合并成一个,但是根据Unity的Frame Debug的显示,我们可以看下,只有同模型的地合并了。然后把模型A向前移动到Z小于0,神奇的看到,同模型的二个cube也不能动态合并了。
好吧,在这有点小失望,后面查到在网上有个说法,Unity会根据摄像机的深度排序,所以在排序后,如果上个模型和下个模型不一样,就没有合并,虽然在我想法中,应该是只要同材质就应该自动合并在一起,如ogre2.1中的相应的glDrawXXXBaseInstance与glMultiDrawXXXIndirect等函数的引入,但是后面又想到,就算Ogre2.1中,gles渲染模式还是使用的Ogre1.x的渲染模式,只是加上了材质排序,应该是现在gles2与移动平台的限制,希望gles3会有改善,Unity主打移动平台,是这样的话也不奇怪了。
接上,一般来说,就算有深度排序,应该是先有通道排序,就是说如Ogre与Unity都有的一个概念,指的是背景,透明,不透明,粒子,UI这种渲染通道。为了验证如上想法,我们创建一个材质,渲染通道设为"Queue" = "Geometry+1",其中二个Cube使用这个新材质,这样我们可以发现,Panel与Cube的距离不会影响二个Cube的合并了。
虽然这样,不过用处不大,因为渲染阴影RTT中,同材质是能全部合并的,如果渲染通道顺序改变了,就不能合并了,如果使用PSSM这种阴影技术,设置四个阴影图,相反还增加三次DrawCall了。
暂时告一段落后,想到如果能看到Unity3D的源码就好了,当然这个现在来说,好像不可能,不过记的当时看过一个新闻,搜狐有个开源的引擎,叫Genesis3D,和Unity有点像,至于有多像就不知道,拿来大致看下,也要不了多少时间。
Genesis3D的渲染流程
下面大致是我对Genesis3D渲染流程的一些整理,先来看一些基本的类。
GraphicObject:主要包含在局部坐标系的矩阵与本身的AABB。
RenderObject:GraphicObject的子类,这个类有点像Ogre中的Renderable,是渲染的主要类,其中属性如Layer相当于渲染通道的ID,RenderScene相当于场景声明,主要可以参考VIS项目,可以看到Genesis3D是用的四叉树的场景管理,相应的摄像机的Cull主要是基于四叉树的理论,VisEntity主要是RenderObject添加到场景RenderScene后的包装,用于得到在四叉树场景中的那个节点上,其中ReceiveShadow与CastShadow分别是接受阴影与生成阴影,生成阴影表明在生成阴影的RTT时,包含当前模型,接受阴影表明在正常渲染模式下,把当前模型的深度与阴影RTT比较,Projected表明是否采用透视矩阵,其中三个方法比较重要,如下:
- OnWillRenderObject:在添加到渲染通道的参数中发生。
- AddToCollection:添加进渲染通道时引发的,主要分别把RenderObject里的所有Renderable包装成RenderData.
- Render:渲染当前模型,子类调用相应渲染组件实际渲染。
VisibleNode:当要添加进渲染通道前,Distance用来表示与摄像机距离,用于后面排序。
Renderable:主要对应Ogre中的Material,相应的主要属性Material表示模型的渲染设置,Mark表示如GenShadow,CastShow与GenDepth的标记。
RenderData:当RenderObject添加进渲染通道后,和Ogre2.1中的RenderableCache这个概念比较像,包含渲染要用的所有元素,其中VisibleNode包含RenderObject与距离,Renderable包含材质与生成的着色器。
如上是渲染所需要的主要类,我们来看下相应的流程,在GraphicSystem中的OnUpdateFrame能找到如Ogre中的RenderOneFrame这个流程。
GraphicSystem:RenderAll()
RenderTarget::BeginRender()
Camera::RenderBegin()
RenderPipelineManger::OnRenderPipeline(camera)
Camera::RenderEnd()
RenderTarget::EndRender()
看过渲染引擎代码的可以看到,这段代码大致都有,意思主要是渲染所有RenderTarget,然后针对每个RenderTarget的Viewport开始渲染,Viewport对应的Camera开始渲染,意思主要的工作都是RenderPipelineManager::OnRenderPipeline来完成的,我们来看下,这个方法主要完成了那些事情。
首先把模型添加进渲染通道 OnRenderPipeline->AssignVisibleNodes.
这步主要是添充模型到PipelineParamters这个结构的参数中,其中用到前面所说的Vis这个项目,用四叉树的理论来方便根据摄像机的位置Culling得到相应的VisEntity列表。
然后根据VisEntity列表中的RenderObject与Camera中的mark匹配,如果正确就填充到PipelineParamters中的m_callBacks中,并且计算RenderObject中的Camera与距离包装成VisibleNode填充到PipelineParamters中的m_VisibleNodes中,以及设置不需要Cull,始终添加进渲染通道中的RenderObject到PipelineParamters中。
在上面的AssignVisibleNodes后,开始调用RenderObject中的OnWillRenderObject函数。
在AssignVisibleNodes后,调用RenderObject的OnWillRenderObject函数,可以看到,这个函数在Cull之后,渲染之前。
然后调用AssignEffectiveLight:渲染RTT阴影,具体的流程大家可以自己看下,对比一下常规渲染。
其次把渲染通道中的模型排序 把PipelineParamters中的m_VisibleNodes填充到RenderDataManage中。
在这调用AssignRenderDatas:这步把VisibleNode中的RenderObject通过AddToCollection添加到RenderDataManager中。子类通过AddToCollection能把相应的RenderObject转化成对应的一个或多个Renderable放入渲染通道,组织Renderable与VisibleNode成RenderData,其中根据Renderable的通道ID放入把对应RenderData放入不同通道(过程查看RenderDataManager::Push),然后排序。
1.正常渲染下
首先是不透明的模型:先比较Material中的通道ID(queue_index),再比较Material的m_sort,再比较Shader的ID,再比较与摄像机的距离,最后就是本身的索引.
然后是透明的模型:不同上面的是比较与摄像机的距离移到较Material的m_sort之前,别的一样。
最后是如UI这种显示在表面的模型,比较Material中的通道ID(queue_index)
2.阴影模型渲染下
只比较不透明模型:比较Material的m_sort,再比较Shader的ID,最后比较摄像机的距离
渲染模型 RenderPipelineManager::renderPipeline
其中渲染模式不同,如前向渲染,后向渲染,自定义渲染调用不同的RenderPipeline子类实现,前向渲染中的如渲染深度图,后向渲染GBuffer,共同正常渲染模型都要调用renderRenderabList.
renderRenderabList简单来说,把上面的RenderPipelineManager中的RenderDataManager里的数据取出来,RenderDataManager如前面所示,通道ID分组,如背景,不透明,透明,粒子,UI等,渲染每个通道中的RenderData列表,然后遍历每个RenderData。
其中会先调用GraphicRenderer::BeforeRender来确定RenderData里的Renderable是否需要切换shader.
然后RenderData找到对应的RenderObject调用Render,主要看子类的实现,取出如顶点位置,颜色,索引的数据,可以看如particlerenderobject::render粒子效果,SkinnedRenderObject::render(这类有硬件蒙皮的相关一种实现),MeshRenderObject::render常规网格渲染实现。可以看到,相应的render都现在一个类PrimitiveHandle,如上面所说,包含顶点位置,颜色,索引等的缓冲区数据信息,可以查看相关GraphicSystem/RenderSystem::CreatePrimitiveHandle的相应实现,RenderSystem可以看到,分别是把VBO与IBO分发到DX9与GLES中来绑定。并添加到RenderSystem相当于CommandList概念的RenderResourceHandleSet对象m_renderHandles上。如Ogre2.1中的CommandType,对应在RenderSystem中的Base中的RenderCommandType,其中eRenderCMDType可以看到一个对应的枚举。
整个渲染流程差不多就是如此,其中要对比的话,应该是和Ogre2.1中的slow模式比较像,就是专门用来处理移动平台gles2.0的这种,有材质排序,渲染命令如设置纹理,Drawcall也是都先包装成CommandList这种,比Ogre1.9要好的是材质排序了,这样同材质的只需要设置一次状态。到这里,如果Genesis3D真是参考了Unity的源码,我们也可以猜到,Unity(现在用的是5.2的版本)里的动态Batch并不是Ogre2.1中gl3+里的通过glDrawXXXBaseInstance与glMultiDrawXXXIndirect里的GPU Instance,而是一种CPU的方式,把Mesh里的顶点重组,有点像我以前在这 Ogre 渲染目标解析与多文本合并渲染 里把多个文本的顶点组合成一个Buffer后渲染。听说Unity5.3已经引入Opengl4,不知能不能把PC平台的渲染改成GPU的新API中的Instance渲染方式,移动平台可能要等到gles3.0全面开花才有可能了。
PassQuad
记的刚开始下载这个引擎只是因为Untiy特效中常用的Graphics.Blit这个函数,第一次看到感觉完全是Ogre合成器中的PassQuad,为了验证Untiy的实现是不是也是画一个-1,1的平面以及后面渲染输出到FBO来实现的,来看如下代码。
void ImageFiltrationSystem::Render(const RenderBase::TextureHandle* texture, const RenderToTexture* target, const Material* material, int passIndex /* = 0 */, uint clearflag /* = */ ) { QuadRenderable* renderable = NULL; if (texture) { GlobalMaterialParam* pGMP = Material::GetGlobalMaterialParams(); pGMP->SetTextureParam(eGShaderTexMainBuffer, *texture); } Graphic::GraphicSystem* gs = Graphic::GraphicSystem::Instance(); if (target) { const GPtr<RenderBase::RenderTarget>& rt = target->GetRenderTarget(); renderable = target->GetRenderable(); gs->SetRenderTarget(target->GetTargetHandle(), 0, clearflag); } else { renderable = gs->GetRenderingCamera()->GetQuadRenderable().get(); gs->SetRenderTarget(sNullTarget, 0, clearflag); } if (NULL == material) { material = sImageCopyMaterial.get(); passIndex = 0; } const Graphic::MaterialParamList& mpl = material->GetParamList(); const Util::Array<GPtr<Graphic::MaterialPass> >& passList = material->GetTech()->GetPassList(); const GPtr<Graphic::MaterialPass>& pass = passList[passIndex]; Graphic::GraphicSystem::Instance()->SetShaderProgram( pass->GetGPUProgramHandle() ); GraphicRenderer::SetMaterialParams( mpl, pass ); const GPtr<RenderBase::RenderStateDesc>& rso = pass->GetRenderStateObject(); Graphic::GraphicSystem::Instance()->SetRenderState( rso ); Graphic::GraphicSystem::Instance()->DrawPrimitive( renderable->GetQuadHandle() ); }
可以看到QuadRenderable,就是如上所说的画一个-1到1的正方形,其顶点与UV坐标生成可以到QuadRenderable::Setup方法看到。其中gs->SetRenderTarget我们到glse分支上看到,确实通过FBO来实现的。
void RenderDeviceGLES::SetRenderTarget(RenderTarget* rt) { n_assert (rt) const RenderTargetGLES* pRTGLES = _Convert<RenderTarget, RenderTargetGLES>(rt); GLbitfield mask = 0; const GLESFrameBuf& fbo = pRTGLES->GetRenderTargetGLES(); if (!pRTGLES->IsDefaultRenderTarget()) { _UnbindBuffer(); m_glesImpl->ActiveFrameBuffer(fbo.FrameBuf); } else { m_glesImpl->ActiveFrameBuffer(m_mainFBOnum); mask |= GL_DEPTH_BUFFER_BIT; mask |= GL_STENCIL_BUFFER_BIT; } uint clearFlags = pRTGLES->GetClearFlags(); if (clearFlags & RenderTarget::ClearColor) { mask |= GL_COLOR_BUFFER_BIT; } if (pRTGLES->HasDepthStencilBuffer()) { if (clearFlags & RenderTarget::ClearDepth) { mask |= GL_DEPTH_BUFFER_BIT; } if (clearFlags & RenderTarget::ClearStencil) { mask |= GL_STENCIL_BUFFER_BIT; } } if (mask != 0) { const Math::float4& color = pRTGLES->GetClearColor(); glClearColor(color.x(), color.y(), color.z(), color.w()); m_glesImpl->CheckError(); GLboolean bDepthMask = GL_FALSE; glGetBooleanv(GL_DEPTH_WRITEMASK, &bDepthMask); glDepthMask(GL_TRUE); m_glesImpl->CheckError(); glClear(mask); m_glesImpl->CheckError(); glDepthMask(bDepthMask); m_glesImpl->CheckError(); } }
如上Ogre中的PassQuad差不多也是一样。
如上所有结论都只是针对Genesis3D里的实现,至于和Unity有多少和这些相似就不保证了,不过上面Genesis3D的渲染流程确实可以解释最上面图片里的现象,有知道Unity3D内部实现的同学欢迎指正。
在2015定下的目标,C++11实践,Ogre,用Ogre实现某东东,虽然实现的都不是很完善,但是惊喜的是Ogre2.1的出来,并理解其中大部分内容,学习最新引擎的实现相关优化。而在这一年,主要在新公司学习Unity以及VR,理解Unity与VR的原理。哈哈,非常看好VR,感觉未来的方向就是这个,如果2016年尾有时间,学习下相应UE4的源码。