zoukankan      html  css  js  c++  java
  • 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() );
        }
    RTT

      可以看到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();
        }
    }
    SetRenderTarget

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

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

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

  • 相关阅读:
    ajax traditional
    阿里云OSS NET SDK 引用示范程序
    js对象的两种写法
    BZOJ NOIP提高组十连测第一场
    ikbc 时光机 F87 Ctrl 失灵 解决办法
    【读书笔记】阅读的危险
    51nod 1118 机器人走方格 解题思路:动态规划 & 1119 机器人走方格 V2 解题思路:根据杨辉三角转化问题为组合数和求逆元问题
    【算法】求逆元模板
    【复习资料】软件工程之快速原型模型
    VirtualBox安装linux mint教程
  • 原文地址:https://www.cnblogs.com/zhouxin/p/5137028.html
Copyright © 2011-2022 走看看