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的源碼。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM