Nebula3 渲染系统
游戲引擎首先解決的任務就是渲染,N3的渲染架構是一個多線程渲染架構。渲染線程是主線程外的一個線程,主線程操作的是GraphicsEntity , 而渲染渲染線程操作的是InternalGraphicsEntity。這章節想盡可能詳盡的解析N3的渲染框架的實做手法和設計理念。同時以編寫一個簡單的例子為目的,實現場景對象的添加和控制。讓一個角色在場景中從A點移動到B點。
渲染系統概述
N3 的渲染系統主要分成如下的幾個部分:幀管理,視域裁剪管理,動畫系統,光照系統,character系統,粒子系統,渲染插件,調試渲染系統,輸入輸出管理,模型系統,和前面提到的資源系統。N3的多線程渲染框架,把場景對象的操作和場景對象的渲染分到兩個線程。對象操作在主線程,渲染線程是子線程。
對于深入渲染系統的實做之前,先要對這兩個線程劃分,職責,以及線程中可操作數據要有一個比較清晰的理解。前面提到過一個多線程渲染的實現方案,是緩存渲染指令,然后再在獨立線程執行指令批調用,這個方案不是N3的方案,而且這個方案存在一個比較困難的地方就是渲染數據,指令可以緩存,但數據本身會導致一些問題,比如是否需要用鎖的機制等來保證渲染數據的安全。N3實現的是一個胖線程,渲染線程擁有渲染相關的所有數據,對于需要渲染線程和主線程訪問的數據進行一個共享器的處理,在共享器中會保留兩份數據。主線程操作主線程對應的場景對象,這個場景對象會在渲染線程存在一個與之一一對應的對象,主線程的操作通常是會通過消息發送給渲染線程。在代碼的結果上來說,主線程可以操作的數據是render/graphics中定義的類,這些類在渲染線程也會存在與之一一對應的類,其中渲染線程由graphicsinterface進行創建和初始化,主線程操作的接口是graphicsserver。在解析渲染前,需要對渲染的數據,也就是所謂的游戲場景構成要個概念。
場景構成
在大多數的游戲引擎中,通常會有一個SceneManager 以及? Scene 的概念,是的,N3中也是有的,只不過是名字變了,而且對結構做了一些調整,以解耦sceneManager的各種功能。在N3中有一個架構來主導渲染框架,就是 Stage - View - GraphicsServer。這個框架會在主線程,以及渲染線程分別構建一套,這兩個是一一對應的。先來看下主線程相關的框架,N3的文檔中會把主線程當做client端,而渲染線程當做server端。
- Stage 是相當于場景,N3的設計者把這個概念形象的比喻為我舞臺,Stage由各種GraphicsEntity組成,相當于舞臺上的道具,燈光,音效,攝像機等,這些有些是可見,有些是不可見的
- View 相當于一個鏡頭,一個觀察點,不過這個觀察點會和一個攝像頭綁定。所以,Vew 需要 和 一個 Stage,以及一個舞臺上的 CameraEntity綁定。
graphicsserver負責 創建 Stage 和 View,也負責更新Stage和View。同時還負責管理RenderModel, 對RenderModel進行更新。
多線程渲染框架
這一套在主線程,在渲染線程同樣會存在一套 internalStage - internalView - internalGraphicsServer,兩套之間通過三個渠道進行交互:
- 一個是會在主線程這邊的對象中放置一個ObjeRef引用渲染線程的對象,所有從對主線程執行的操作都可以非常快速的對應到渲染線程的對象;ObjRef 的實現依賴一個interlockExchange,并且ObjRef 最好只是用在從主線程發送消息到渲染線程的消息結構體中做一個句柄功能,從ObjRef這兒句柄進行解引用的操作還是放在渲染線程中。
- 第二個是一個FrameSync::FrameSyncSharedData,這個數據是一個線程的共享數據體,類似一個RingBuffer,一個線程寫,另外一個線程讀,不過這里的共享數據結構運行兩個線程同時寫,只是需要自己保證數據的有效性,如果是和RingBuffer一樣,一個線程讀一個線程寫,就課可以保證共享數據的一致和有效;FrameSyncSharedData的實做方式前面有提到一點點,這里深入的看一下,FrameSysncShareData在N3中誅仙是用來干這么一件事情,主線程需要訪問渲染線程的數據,這些數據由渲染線程更新,而主線程只是訪問,FrameSysncShareData保證這兩個線程對數據的寫和讀可以并行,而且不需要鎖,這個實做依賴一個線程同步機制,FrameSyncHandlerThread, 這個會同步各個線程的幀,給出整個系統的一個幀號,而FrameSysncShareData依賴這個幀號,對兩份數據進行分配,比如奇數幀的時候,把第一份數據分配給主線程進行訪問,第二份數據給渲染線程進行寫入,而偶數幀的時候,進行對調,第一份數據給渲染線程寫入,第二份數據給主線程訪問。這個機制依賴一個共享的數據,任意時刻渲染線程和主線程都是根據同一個幀號進行數據檢索的,而且任意一個線程都不會保留一個對數據的指針或者引用。
- 第三個就是線程間的消息通訊了。
???? 這其中出現了一個新的類 FrameSyncHandlerThread, 這個是從 public Messaging::HandlerThreadBase 繼承來的,它就是真的渲染線程,這個線程定制了一個機制,用一個 Threading::ThreadBarrier,開啟lock-step模式下同步主線程和渲染線程,用一個Threading::Event同步在非lock-step模型下同步渲染線程和主線程,這里的出現了兩個同步,這兩種模型下的同步分別具備不同的意義,
- 在lock-step模型下,同步意味著主線程和渲染線程的幀率是一樣的,也即是兩個線程擁有同一個絕對的時間線,渲染線程先完成一幀的渲染,如果主線程還沒有完成這一幀的邏輯,就會讓渲染線程等待主線程,如果主線程先完成邏輯運算但是渲染線程還沒有完成渲染,則讓主線程等待渲染線程。在被等待線程完成工作到達同步點的時候,會更新幀的定時器等數據,然后喚醒等待線程;
- 在非lock-step模式下,渲染線程不需要等待,主線程需要等待,在這種模式下,就可能會造成開始討論的FrameSyncSharedData數據的線程沖突,比如渲染線程跑到第n幀,邏輯線程在第n幀卡在,并且在這個時候通過幀n檢索了一個共享數據的引用,然后cpu轉到渲染線程,渲染線程進入下一幀,更新幀號到n+1, 并檢索n+1對應的共享數據進行寫入,這個時候渲染線程寫入的共享數據局是主線程讀取的共享數據。所以在非 lock-step模式下最好慎重對待FrameSyncSharedData。
在多線程渲染框架下,兩個線程共享一個MasterTimer, 同時每個線程都有一個線程安全的LocalTimer : FrameSyncTimer。
Entity映射
有了前面的準備工作后,我們開始對Stage, Entity 進行進一步的剖析。結合第一篇<N3資源系統>,我們知道主線程的舞臺是由GraphicsEntity及其子類構成的,類圖如下:
而主線程這邊的實體構成本身是不管在渲染數據的,真正的渲染Entity需要映射到渲染線程的internalStage及其構成,類圖如下:
兩個對象的構成幾乎一摸一樣,只是在主線程的操作Entity類前加了個前綴 internal,是的,在主線程創建一個類型的GraphicsEntity的時候都會發送一個EntityCreate消息到渲染線程,消息中會攜帶屬性信息,以及一個ObjRef,用于渲染線程返回對應實體,還有一個FrameSyncSharedData,用于在兩個線程之間共享數據。到渲染線程這邊,這些實體類型 大部分是不可見的,都是功能類型的實體,其中只有InternalModelEntity實體是一個可見實體,當然InternalModelEntity也可以是不可見的,要看它的構成。
到這里有個比較需要提一下,就是GraphicsEntity和InternalGraphicsEntity的數據成員構成對比:(左邊是 GraphicsEntity, 右邊是 InternalGraphicsEntity)
VS ???? SharedData :??
發現 GraphicsEntity 操作的數據非常簡潔,可以控制 坐標, 包圍盒, 是否可見。在InteralGraphicsEntity中同樣包含 transform, localBox 等, 但會多幾個數據 clipStatus, timeFactor, entityTime, 這些數據會通過 FrameSyncShareData共享給 主線程,還有一個比較有意思的數據項 links, 這個里面保存了一些鏈接,N3設計這個是為了做裁剪和光影渲染加速,設計中的links是用來存儲 InternalCameraEntity 和 InternalLightEntity的, 而且設計者反復說道這個連接關系是一個雙向關系,也就是說,如果一個相機被加入到一個ModelEntity的Camera鏈接隊列,那么這個ModelEntity同時會被加入到相機的Camera鏈接隊列中,對于LightEntity也是一樣的,后面在用到的時候會繼續對這個做深入的討論。
到這一步可能您已經發現了這么一件剛剛認為已經找到了的東西,就是GraphicsEntity 和 InternalGraphicsEntity都沒有從這一級別定義父子關系,一個Entity沒有孩子節點,突然感覺少了點什么,突然要去想怎么實現DX和OpenGL寶典中的那個經典例子,渲染一個太陽系,各種實體的父子關系,然后是各種相對坐標系的繼承。是的,N3這GraphicsEntity這一層沒有做這么一件事情,但是N3實現一個AttachmentManager,用來專門做這么一件事情,比如要在游戲里面要讓一個角色騎上一個獅子,讓獅子載著角色狂奔在大草原上。AttachmentManager 操作的是 主線程的數據,在渲染線程由一個AttachmentServer負責管理Entity的綁定事件,以及維護這些綁定點的數據更新。這里有一個需要注意的是,對于多層綁定,N3并沒有去特意的做什么機制,比如 A 上的joint_0綁定一個 ResId_B,? 得到綁定無是B, 同時在 B的joint_0 上綁定一個ResId_C, 得到C,又在C的joint_0上綁定.......,面對這種綁定,需要使用者自己包裝AttachmentServer 和 AttachmentManager 能夠以合適的順序進行數據維護,和前面資源管理系統提到概念一樣,對于一個具備層級的樹,構造,析構,和更新最好是按照既定的一個可控規則去保證順序比如前序遍歷,或者后續遍歷。這里對于綁定系統不再深入了,這個是屬于應用層的東西,只是這里需要提出的是這里的links不是通常意義上的孩子節點,不要用干綁定的事情。
下面依然對各種Entity的主線程類型和渲染線程類型的數據成員做一個比較(同時會在右邊列出他們之間的共享數據):
(CameraEntity VS InternalCameraEntity)
VS ??? :???? CameraSettings???
(AbstrcatLightEntity VS InternalAbstrcatLightEntity)
VS
(GlobalLightEntity VS InternalGlobalLightEntity)
VS
(PointLightEntity VS? InternalPointLightEntity)
(SpotLightEntity VS InternalSpotLightEntity)
(ModelEntity VS InternalModelEntity)
VS
通過前面的數據對比,從左邊可以看出主線程的控制力度,從右邊可以大概知道渲染系統是怎么處理各種類型的Entity,以及渲染數據的在Entity這一層的組織方式,從右邊的共享數據可以看出,從渲染可以反饋到主線程的數據是哪些。下面貼一個共享數據的類圖:
從這個簡單的類圖可以看出主線程和渲染線程之間數據的共享結構是非常簡單的。通過這一步,我們進一步清晰了操作的Entity和渲染的Entity之間的對應關系,下面深入InternalGraphicsEntity,分析他的構成,清晰渲染對象的數據構成。
InternalGraphicsEntity
在前面的部分中,我們以及了解了InternalGraphicsEntity的繼承圖,而且知道在子類的實現中,除了InternalModelEntity是可見的Entity類型之外,其他的LightEntity和CameraEntity都是渲染輔助類型。接下里我們將把重點放在InternalModelEntity上。
先再次貼上集成圖:
然后依次對每個類型的Entity進行解析,先是基本的InternalGraphicsEntity ,對于這個,N3的設計者總結為:它是渲染系統的原子圖形對象,在渲染系統只會有三種類型的這種對象,一個是相機,一個是光源,一個是可見的模型。在可見性剔除檢查的時候,會把相機節點link到所有當前相機能夠看到的模型和光源上,而光源會link到所有被當前光源影響了的模型和相機上。對于InternalCameraEntity 以及InternalLightEntity的作用是輔助渲染,也就是設置渲染流水線的各種狀態。對于相機,以及相機的操作是也是一個相對重要的環節,因為游戲開發的早期迭代就會把這個當做一個基本需求給確定和完成。而對于光源的討論往往會和陰影以及shade進行討論,并且光源對于渲染流水線的設置是一個非常寬廣的話題,這里先點到為止,不做進一步的探討,在N3中會有專有的管理器lightServer進行管理。接下來看最重要的一個類型。
一個InternalModelEntity對應一個 ManagedModel ,以及一個? ModelInstance,? 而ManagedModel封裝了一個 Model,所有同一個resId的ManagedMode共享一個Model實例,這個實例相當于一個原始資源的Copy,而ModelInstance 是從 Model 構建出來的,存儲的是每個 InternalModelEntity對應的實例數據。在資源章節中我們以及接觸過這一些構成關系,只是沒有深入的了解? Model 和? ModelInstance的關系,這里對Model 和? ModelInstance做一個詳盡的解析,一個 可渲染的InternalModelEntity實際上是一個可見的ModelInstance,而這個可見的ModelInstance是根據資源模板Model構建出來的,所有同一個ResId的ModelInstance共享同一個Model,一個 ModelInstance 與 一個 Model綁定,同時還與一個 InternalModelEntity一一對應。而一個ModelInstance是一棵由各種類型ModleNodeInstance的樹組成,這顆樹是由組成Model的ModelNode這棵樹生成的,真正渲染的數據就是各種可見的ModelNodeInstance。我們先列舉一下Model, ModelInstance 以及 ModelNode, 還有 ModelNodeInstance的類圖:
再看下類的協助關系圖:
一個Model包含組成它的所有ModeNode, 同時也會保留所有當前Model創建的ModelInstance的實例指針。同時里面還包含Model中所有可見的ModelNode的一個容器。
一個ModelInstance 包含組成它的所有ModeNodeIntance, 同時會保留一個與之關聯的InternalModelEntity的實例指針,同時還會保留一個當前ModelInstance對應的Model。
一個ModeNode 包含一個所屬的 Model指針,以及 ModelNode 創建的可見 ModelNodeIntance,?
一個ModeNodeInstance 包含一個所屬的 ModelNode, 以及當前屬于的 ModeInstance
有了這些概念后,我們已經可以大概的論述渲染數據了,由ModeNode? 和? ModeNodeInstance組成,ModeNode是所有ModeNodeInstance共享的,ModeNode 執行設置公共狀態,而ModeNodeInstance提高實例的私有數據到渲染流水線,打個比方,一個ModeNode是一個ShapeNode及其子類,那么Node中肯定就含有一個Mesh, 要渲染這個Node對應的一個NodeInstance, 這個時候重點就是在一個指定的地點渲染一個Mesh,Mesh本身是固定的,只有Mesh的模型空間是不一樣的,這樣由NodeInstance負責提供一個模型空間的變化矩陣,由Node提供Mesh的VertexBuffer, 以及 IndexBuffer。這樣流水線的中需要的幾何數據就以及完成了,通過這個就簡單的例子,我們就可以知道渲染是由各種Node和NodeInstance配合完成的。下面我們需要列舉各個Node以及相應的NodeInstance在整個渲染過程中各自的分工和扮演的角色。
我們指定這兩個類圖是渲染數據的來源,而流失線則是由其他的模塊負責(上圖,沒有完全列舉所有的ModelNode, 還有一個 AnimatorNode),我們先解析 , Node 和 NodeInstance 之間的一個關系。先看下N3的設計定位:
ModelNode : 代表組成Model的一棵層級樹上的節點,ModelNode的子類代表的是3D模型的變換信息,或者是幾何形狀,這些節點按照3d空間層級組織。在Model中這些節點存儲在一個數組中,可以有效防止遞歸訪問。一個ModelNode相當于Nebula2中的一個SceneNode。其中與渲染相關的函數是:
這個函數用來設置渲染流水線中的當前ModelNode供所有ModelNodeInstance使用的渲染參數
ModelNodeInstance:一個ModeNodeInstance擁有一個ModeNode的Instance的私有數據,這個類處理相關Model的大部分渲染相關的工作。相關的函數如下:
其中看到了一個ApplyState()和 Render(), 這不一一列舉各種Node的實現,只是把關鍵的路徑函數列舉出來,
//------------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------------------------------------
渲染流程的關鍵函數已經出現。要渲染一個Mesh,可以簡單的定義為如下:
由TransformNodeInstance 計算出當前的模型矩陣,然后由StateNode 提交一個shaderInstance, 作為當前的渲染管線,以及負責提交當前渲染管線用到的Texture, 和 其他ShaderVariable, 接著由ShapeNode提交Mesh相關的VertexBuffer和IndexBuffer, 最后是由ShapeNodeInstance發起渲染命令的Call: Device::DrawPrimitive(......)。支持一個簡單的渲染流程出現了。
至于N3的渲染框架則還沒用結束。這里對整個渲染框架的函數調用路徑做一個說明,然后對框架中用到的各個分開單獨說明,這些分塊都比較龐大,就像前面還完全沒用提到骨骼-蒙皮相關的任何東西。
由InternalGraphicsServer::OnFrame發起 ----》
設置相機的各種矩陣:
this->defaultView->ApplyCameraSettings();
對當前要渲染的舞臺執行裁剪工作
this->defaultView->GetStage()->OnCullBefore(curTime, globalTimeFactor, frameIndex);
從相機角度更新可見的鏈接信息
this->defaultView->UpdateVisibilityLinks();
從相機角度更新光陰鏈接信息
this->defaultView->GetStage()->UpdateLightLinks();
執行渲染動作
this->defaultView->Render(frameIndex);
至此一幀的正常渲染工作就算完成, 在接下來對 view->Render() 分解一下:
// 更新當前幀的光源信息
this->ResolveVisibleLights();
// 把當前幀的可見ModelNodeInstance全部挑選出來
this->ResolveVisibleModelNodeInstances(frameIndex);
//交給frameShader執行渲染pass的調用
this->frameShader->Render();
至此 view的渲染工作也算完成了,接下來對frameShader->Render()進一步分解:
//一次對定義的每個pass執行渲染工作
this->pass[i]->Render()
到這里frameShader把工作全部交給了 pass, 先交代一下pass, 一個 pass 就是把場景類容繪制一遍,多個pass就會繪制多次場景, 這里的多次繪制是有用的,可以繪制到紋理等,這樣繪制后的紋理可以在下一個pass里面用到,這樣可以實現一些特別的效果。
在接下來分解 pass->Render(): 需要注意的是N3的 pass 分為兩種,一個是普通的FramePass,還有一個FramePostEffect,這里先來看下普通的FramePass:
// 先條用renderTarget的各種配置函數,包括清理模板緩存,深度緩存,顏色緩存,還有渲染模板的標準位
this->renderTarget->SetClearFlags(.....);?
// 接著是提交各種渲染狀態參數,以及shader中用到各種變量
this->shaderVariables[varIndex]->Apply();
// 最后是對pass中的各個批次進行渲染
this->batches[batchIndex]->Render();
分解批次渲染調用 這里 批次的概念比較好理解, 一個場景是各種可見的ModelNodeInstance組成,但是這些ModelNodeInstance是有類型區別的,比如水體,比如剛體等,UI, 文字等,這里一個批次渲染一類ModelNodeInstance可以減少渲染流程線的狀態變更,而且更好的利用底層提供的Instances渲染,DX10就實現了這個多實例渲染。好廢話不多說,分解一下 batch->Render():
//設置batch對應的渲染參數
this->shaderVariables[varIndex]->Apply();
this->RenderBatch();
???? // UI, Shapes, Text, Lights, MouserPointer, ResolveDepthBuffer,? 這些批次忽略,下面看下正常的場景內容渲染
???? // 根據nodeFilter 提取當前可見的Model
???? const Array<Ptr<Model> >& models = visResolver->GetVisibleModels(this->nodeFilter);
???? // 根據nodeFilter 提取Model中當前可見的 ModelNode
const Array<Ptr<ModelNode> >& modelNodes = visResolver->GetVisibleModelNodes(this->nodeFilter, models[modelIndex]);
???? // 設置當前modelNode的狀態到渲染管線
modelNode->ApplySharedState(frameIndex);
????????? //?? 根據nodeFilter 提取 ModelNode 中所有可見的 ModelNodeInstance
const Array<Ptr<ModelNodeInstance> >& nodeInstances = visResolver->GetVisibleModelNodeInstances(this->nodeFilter, modelNode);
????????? //? 設置當前modelNodeInstance的狀態到渲染管線???
nodeInstance->ApplyState();
????????? // 調用節點的Render 接口,這個接口實現調用的設備的 drawPrimitive(,,,,,),?? 這與繪制Call 用到定點坐標,紋理參數,Shader變量等都是通過前面的各種 ApplySharedState , ApplyState()設置,然后由 ShaderInstance->Commit()提交給顯卡的。
nodeInstance->Render();
至此,N3的整個渲染流程已經清晰和明了。各種參數的,渲染管線的設置也再流程中體現出來了。后面用詳細的文章說明渲染管線中沒有深入的各個組件,這其中包括:Mesh - Skeleton - FrameShader - Shader - ShaderInstance - ShaderVariable, LightServer - AnimatorNode - Animate - VisibleSystem - Cull - RenderModels::rtPlugin 等。
這些每一個都是一個主題,而且內容也非常豐富。?
期待ing :?
看完這些東東,還有一個非常有意思層, Applation 層,
???????????????????????? 以及 ScriptFeature
???????????????????????? 以及 PhysicFeature 層。
???????????????????????? 以及 NetWorkFeature?
???????????????????????? 以及 FmodFeature?
???????????????????????? 期待 ing
轉載于:https://www.cnblogs.com/JefferyZhou/archive/2012/09/24/2700292.html
總結
以上是生活随笔為你收集整理的Nebula3 渲染系统的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: UIWebView相关应用
- 下一篇: 将excel用VBA生成指定格式的TXT