Unity场景渲染相关实现的猜想

摘要:
接上,一般来说,就算有深度排序,应该是先有通道排序,就是说如Ogre与Unity都有的一个概念,指的是背景,透明,不透明,粒子,UI这种渲染通道。AddToCollection:添加进渲染通道时引发的,主要分别把RenderObject里的所有Renderable包装成RenderData.Render:渲染当前模型,子类调用相应渲染组件实际渲染。

如下,很简单的一个场景,一个Panel,二个Cube,一个camera,一个方向光,其中为了避免灯光阴影的影响,关掉阴影,而Panel和二个Cube都是默认的材质,没做修改,我原猜,这三个模型应该都动态合并成一个,但是根据Unity的Frame Debug的显示,我们可以看下,只有同模型的地合并了。然后把模型A向前移动到Z小于0,神奇的看到,同模型的二个cube也不能动态合并了。

Unity场景渲染相关实现的猜想第1张

Unity场景渲染相关实现的猜想第2张

好吧,在这有点小失望,后面查到在网上有个说法,Unity会根据摄像机的深度排序,所以在排序后,如果上个模型和下个模型不一样,就没有合并,虽然在我想法中,应该是只要同材质就应该自动合并在一起,如ogre2.1中的相应的glDrawXXXBaseInstance与glMultiDrawXXXIndirect等函数的引入,但是后面又想到,就算Ogre2.1中,gles渲染模式还是使用的Ogre1.x的渲染模式,只是加上了材质排序,应该是现在gles2与移动平台的限制,希望gles3会有改善,Unity主打移动平台,是这样的话也不奇怪了。

接上,一般来说,就算有深度排序,应该是先有通道排序,就是说如Ogre与Unity都有的一个概念,指的是背景,透明,不透明,粒子,UI这种渲染通道。为了验证如上想法,我们创建一个材质,渲染通道设为"Queue" = "Geometry+1",其中二个Cube使用这个新材质,这样我们可以发现,Panel与Cube的距离不会影响二个Cube的合并了。

Unity场景渲染相关实现的猜想第3张

虽然这样,不过用处不大,因为渲染阴影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来实现的,来看如下代码。  

Unity场景渲染相关实现的猜想第4张Unity场景渲染相关实现的猜想第5张
    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() );
    }
RTT

可以看到QuadRenderable,就是如上所说的画一个-1到1的正方形,其顶点与UV坐标生成可以到QuadRenderable::Setup方法看到。其中gs->SetRenderTarget我们到glse分支上看到,确实通过FBO来实现的。

Unity场景渲染相关实现的猜想第4张Unity场景渲染相关实现的猜想第7张
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();
    }
}
SetRenderTarget

如上Ogre中的PassQuad差不多也是一样。

如上所有结论都只是针对Genesis3D里的实现,至于和Unity有多少和这些相似就不保证了,不过上面Genesis3D的渲染流程确实可以解释最上面图片里的现象,有知道Unity3D内部实现的同学欢迎指正。

在2015定下的目标,C++11实践,Ogre,用Ogre实现某东东,虽然实现的都不是很完善,但是惊喜的是Ogre2.1的出来,并理解其中大部分内容,学习最新引擎的实现相关优化。而在这一年,主要在新公司学习Unity以及VR,理解Unity与VR的原理。哈哈,非常看好VR,感觉未来的方向就是这个,如果2016年尾有时间,学习下相应UE4的源码。

免责声明:文章转载自《Unity场景渲染相关实现的猜想》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇图像编程:看值识色python字符编码、字符串格式化、字符串方法、列表、元组、字典、集合等基础知识总结下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

Unity3D研究之支持中文与本地文件的读取写入(转)

前几天有个朋友问我为什么在IOS平台中可以正常的读写文件可是在Android平台中就无法正常的读写。当时因为在上班所以我没时间来帮他解决,晚上回家后我就拿起安卓手机真机调试很快就定位问题所在,原来是他文件的路径写错了。开发中往往一道很难的问题解开的时候发现原来真的非常的简单,哇咔咔。 刚好在MOMO的书中也有涉及到文件的读取与写入,那么本节我将书中的部分...

多页应用 Webpack4 配置优化与踩坑记录

前言 最近新起了一个多页项目,之前都未使用 webpack4 ,于是准备上手实践一下。这篇文章主要就是一些配置介绍,对于正准备使用 webpack4 的同学,可以做一些参考。 webpack4 相比之前的 2 与 3,改变很大。最主要的一点是很多配置已经内置,使得 webpack 能“开箱即用”。当然这个开箱即用不可能满足所有情况,但是很多以往的配置,其实...

【Unity3D与23种设计模式】桥接模式(Bridge)

GoF定义: “将抽象与实现分离,使二者可以独立的变化” 游戏中,经常有这么一种情况 基类角色类(ICharacter),下面有子类士兵类(ISoldier)、敌军类(IEnemy) 基类武器类(IWeapon),下面有子类枪类(IGun)、炮类(ICannon) 当然,有用枪的士兵,有用炮的士兵 这么一组合,就是2*2 = 4个类 游戏后期,当角...

es6 proxy浅析

Proxy 使用proxy,你可以把老虎伪装成猫的外表,这有几个例子,希望能让你感受到proxy的威力。proxy 用来定义自定义的基本操作行为,比如查找、赋值、枚举性、函数调用等。 proxy接受一个待代理目标对象和一些包含元操作的对象,为待代理目标创建一个‘屏障’,并拦截所有操作,重定向到自定义的元操作对象上。 proxy通过new Proxy来创建,...

google代码风格(转)

Google C++ 风格指南 - 中文版 from http://code.google.com/p/google-styleguide/ 版本: 3.133 原作者: Benjy Weinberger Craig Silverstein Gregory Eitzmann Mark Mentovai Tashana Landray 翻译: Yul...

Unity在UI界面上显示3D模型/物体,控制模型旋转

Unity3D物体在UI界面的显示 本文提供全流程,中文翻译。 Chinar 坚持将简单的生活方式,带给世人!(拥有更好的阅读体验 —— 高分辨率用户请根据需求调整网页缩放比例) Chinar —— 心分享、心创新!助力快速利用 UGUI 完成 3D 物体在 UI 界面的显示为新手节省宝贵的时间,避免采坑! Chinar 教程效果:...