天天看點

Unity場景渲染相關實作的猜想

如下,很簡單的一個場景,一個Panel,二個Cube,一個camera,一個方向光,其中為了避免燈光陰影的影響,關掉陰影,而Panel和二個Cube都是預設的材質,沒做修改,我原猜,這三個模型應該都動态合并成一個,但是根據Unity的Frame Debug的顯示,我們可以看下,隻有同模型的地合并了。然後把模型A向前移動到Z小于0,神奇的看到,同模型的二個cube也不能動态合并了。

  

Unity場景渲染相關實作的猜想

  

Unity場景渲染相關實作的猜想

  好吧,在這有點小失望,後面查到在網上有個說法,Unity會根據錄影機的深度排序,是以在排序後,如果上個模型和下個模型不一樣,就沒有合并,雖然在我想法中,應該是隻要同材質就應該自動合并在一起,如ogre2.1中的相應的glDrawXXXBaseInstance與glMultiDrawXXXIndirect等函數的引入,但是後面又想到,就算Ogre2.1中,gles渲染模式還是使用的Ogre1.x的渲染模式,隻是加上了材質排序,應該是現在gles2與移動平台的限制,希望gles3會有改善,Unity主打移動平台,是這樣的話也不奇怪了。

  接上,一般來說,就算有深度排序,應該是先有通道排序,就是說如Ogre與Unity都有的一個概念,指的是背景,透明,不透明,粒子,UI這種渲染通道。為了驗證如上想法,我們建立一個材質,渲染通道設為"Queue" = "Geometry+1",其中二個Cube使用這個新材質,這樣我們可以發現,Panel與Cube的距離不會影響二個Cube的合并了。

  

Unity場景渲染相關實作的猜想

  雖然這樣,不過用處不大,因為渲染陰影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來實作的,來看如下代碼。  

  RTT

  可以看到QuadRenderable,就是如上所說的畫一個-1到1的正方形,其頂點與UV坐标生成可以到QuadRenderable::Setup方法看到。其中gs->SetRenderTarget我們到glse分支上看到,确實通過FBO來實作的。

  SetRenderTarget

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

  如上所有結論都隻是針對Genesis3D裡的實作,至于和Unity有多少和這些相似就不保證了,不過上面Genesis3D的渲染流程确實可以解釋最上面圖檔裡的現象,有知道Unity3D内部實作的同學歡迎指正。

  在2015定下的目标,C++11實踐,Ogre,用Ogre實作某東東,雖然實作的都不是很完善,但是驚喜的是Ogre2.1的出來,并了解其中大部分内容,學習最新引擎的實作相關優化。而在這一年,主要在新公司學習Unity以及VR,了解Unity與VR的原理。哈哈,非常看好VR,感覺未來的方向就是這個,如果2016年尾有時間,學習下相應UE4的源碼。