虛幻4渲染系統結構解析

本文根據小米互娛 VR 技術專家 房燕良在 MDCC 2016 移動開發者大會上的演講整理而成,PPT 下載地址:http://download.csdn.net/detail/sinat_14921509/9639244

房燕良

小米互娛 VR 技術專家 房燕良

房燕良,從 2001 年開始,自主研發 3 代遊戲引擎,發佈遊戲超過 10 款。代表作品有《仙劍3》、《功夫世界》、《龍online》、《神兵傳奇》等。從 2007 年開始接觸虛幻引擎,對虛幻引擎有深入的研究和實踐。目前就職於小米,從事 VR 方面的研發工作。

【以下爲演講實錄】

大家上午好!今天,我和大家分享的主題是虛幻 4 渲染系統結構解析。 內容主要包含以下幾個模塊:

  • 從 3D 引擎架構的角度講解渲染系統在架構層面所處的位置以及與其他模塊之間的關係;
  • 重點講述虛幻 4 渲染系統的架構,主要從三個方面講解: 
    • 渲染線程跟主線程的基礎架構;
    • 場景管理;
    • 渲染流程控制角度詳解該架構是如何設計和實現。
  • 最後分析虛幻 4 的 VR 在引擎層實現的流程。並以谷歌 VR HMD 插件爲例進行講解。

3D 引擎渲染系統


下圖相當於一個 3D 引擎與渲染系統相關的幾個模塊。一個是資源系統,一個是材質系統,還有場景的管理,渲染相關的就是渲染管線的管理。這幾個模塊在下層都會調用圖形 API 實現渲染功能。整個 3D 引擎包括渲染系統最核心面臨的問題主要是兩個:管理複雜度和效率。

渲染系統架構

複雜度是現在整個 3D 引擎包括渲染難度係數最高的,要實現各種各樣的渲染效果、渲染算法以及各種各樣優化算法。

“對遊戲來說,效率就是生命”。——卡馬克

效率,一是從圖形算法方面,可變性的判定、流程控制的優化、平衡 CPU 跟 GPU 的工作。二是軟件開發者一定是關心硬件的,意味着另一個核心問題是如何高效發揮 GPU 的高併發流水線的架構以及 GPU 上各種 Cache 如何能夠幫助 Driver 提高命中率。

虛幻4渲染系統架構


渲染系統模塊:

  • Engine/Source/Runtime,主要存放模塊的源代碼;
  • 核心代碼模塊 
    • RenderCore
    • Renderer
  • RHI 抽象層 
    • RHI(Render Hardware Interface)
    • 虛幻 4 的版本 RHI 的設計最初是基於 D3D 11 設計的,
  • RHI 實現層,現在對於主流的平臺和主流的推薦 API 都有相應的實現包括: 
    • EmptyRHI
    • Windows 上 D3D11RHI
    • 蘋果上 Metal
    • OpenGLdRV、VulkanRHI

模塊

接下來從數據和邏輯兩個方面解析虛幻 4 渲染系統,在論及引擎的數據管理與渲染的流程控制之前,我們先理解何爲渲染線程。渲染線程機制是從虛幻 3 開始引入的,當時有一個開發代號叫做 Gemini,爲什麼要引入渲染線程,當然主要是從效率方法考慮。一個遊戲最終開發出來之後實際上有三個大的模塊是佔每一幀時間最多,分別是渲染、遊戲邏輯包括腳本更新、以及物理模擬。因此,如果把渲染和遊戲邏輯更新並行起來,就可以得到一個顯著的效率提升,如下圖所示。如果沒有渲染線程,遊戲邏輯的更新和渲染是串行的,一幀所佔的時間是兩塊執行的總和。如果使用了渲染線程之後,一幀的時間就是兩者耗時最長的那個時間,這是一個理想情況,理想情況會有一個顯著的渲染提升。

渲染線程

既然多了一個重要的線程,就會涉及到兩個線程之間同步的問題。線程之間同步分兩方面:

1、因爲遊戲有運行的速率控制問題,意味着對於遊戲來說,往往遊戲線程負載是低一些,渲染線程是控制一些,遊戲線程瘋狂往前跑也沒有太大的意義,所以它有一個 Render Command Fence,防止遊戲線程跑得太快。好比前臺我們正在看的畫面,如果是第N幀,渲染線程可以渲染第 N+1 幀,遊戲線程可以渲染第 N+2 幀。

2、遊戲線程同步場景管理增加了渲染線程後,整個遊戲的複雜度大大提升了。遊戲線程要修改它的數據,渲染線程也要修改它的數據,也很麻煩,容易出錯。所以在虛幻情景下,使用了一個 Proxy 對象的模式去處理它,在遊戲邏輯裏面處理的一個遊戲對象會在渲染線程裏面對應一個 Proxy 對象,該Proxy 對象的遊戲更新完全在渲染線程裏面做。另外在渲染線程裏,因爲每一幀會有特定的狀態數據,這些狀態數據每一幀都在變,這個其實也沒有太好的辦法,在每一幀的時候,要把獨特的數據進行拷貝。

下圖是渲染線程跟主線程的基本關係,主線程會通過渲染命令的隊列往渲染線程發消息,渲染線程會從命令隊列裏讀取命令,它們之間有一個 Render Command Fence 這樣一個機制。

場景數據管理

接下來看一下虛幻引擎場景的數據管理的一些核心類。虛幻引擎場景的數據管理分了兩層,一層是比較熟悉的 UWorld,主要面向遊戲邏輯開發,爲了在上層做邏輯控制時較爲方便去管理,比較方便實現上層控制邏輯。

核心類

對於渲染來說,UWorld 對應 FScene 對象,這個數據接口的設計主要面向瀏覽器,由FSceneRenderer 這一類,實現了兩個派生類,一個是 FForwardShadingSceneRenderer 前置渲染,還有一個 FDeferredShadingSceneRenderer 就是延遲渲染。在 model 4.0 以下的,是邏輯渲染。如果是在 Shader Model 4.0 以上會選擇延遲渲染。

另外有一核心的類是 FSceneViewFamily,在這一幀可以渲染的多個 view,個人理解最早是在單機遊戲多人同時玩的分屏遊戲,主要是遊戲機上的遊戲,比如極品飛車,可以選擇兩個人同時玩,兩個人是在同一臺遊戲機上玩,在屏幕上就會分兩個視圖,比如我的遊戲視圖是再上一版,你的遊戲視圖是在下面一版。這是分類的一個出發點。現在 VR 興起之後,要做 VR 渲染,正好也要分屏,左眼的圖象在圖片左邊,右眼的圖象在圖片右邊。

另外還有一類是 FViewInfo,有一個新的 view,FViewInfo 是定義在 Render 的模塊裏面,在新的 view 裏面又渲染了一些新的模塊的特定數據,每一幀會有一些自己的狀態,要進行一些拷貝,這裏面有一部分數據保存在這個新 view 這一類裏面。

靜態結構

剛纔講了場景整體,還有單個對象的數據管理,接下來就看一下渲染的流程。這裏是一個僞代碼,把引擎裏渲染相關的一些關鍵步驟提取出來,這個不是全面的,只是爲了突出重點,只是一些重點步驟。

  • Game線程
void UGameEngine::Tick( float DeltaSeconds, bool bIdleMode )
{
UGameEngine::RedrawViewports()
{
void FViewport::Draw( bool bShouldPresent)
{
void UGameViewportClient::Draw()
{

//-- 計算ViewFamily、View的各種屬性ULocalPlayer::CalcSceneView();
//-- 發送渲染FRendererModu命le令:::BFeDgrianwRSecnedneerCionmgmVainedwFamily()
//-- Draw HUD
PlayerController->MyHUD->PostRender();
}
}}}

FrameEndSyn

void FRendererModule::BeginRenderingViewFamily()
{
// render proxies update
World->SendAllEndOfFrameUpdates();

// Construct the scene renderer.
// This copies the view family attributes
// into its own structures.
FSceneRenderer* SceneRenderer =
FSceneRenderer::CreateSceneRenderer(ViewFamily);

ENQUEUE_UNIQUE_RENDER_COMMAND_ONEPARAMETER(
FDrawSceneCommand,
FSceneRenderer*,SceneRenderer,SceneRenderer,
{
RenderViewFamily_RenderThread(RHICmdList, SceneRenderer);
FlushPendingDeleteRHIResources_RenderThread();
});
}

接下來用僞代碼的方式來看一下渲染主幹的流程,首先入口還是 RenderViewFamily_RenderThread() 這個函數,第一步進行 InitViews(),首先調用 Primitive Visibility Determination 進行剪裁,然後是透明物的排序,然後是燈光的可見性,然後就是不透明物體的排序。

接下來通過很多的 pass 來實現整個渲染。首先會有一個 base pass,建立一個 base 緩衝,然後通過 base pass,填充 GBuffer 的緩衝,然後是渲染所有的燈光,後面就是渲染天光,渲染大氣效果,渲染透明對象,渲染屏幕區特效,所有這些渲染完之後, SceneColor() 就完成了,最後進行後處理,最後是調用 RenderFinish()。

延遲渲染

RenderLights 粗略的邏輯是,場景所有的燈光都要調用 RenderLights() 函數,在該函數裏面調用兩個 Shader 去畫燈光在屏幕空間的影響區域。

場景

void FDeferredShadingSceneRenderer::Render()
{
bool FDeferredShadingSceneRenderer::InitViews()
{
//-- Visibility determination.
void FSceneRenderer::ComputeViewVisibility()
{
FrustumCull();
OcclusionCull();
}

//-- 透明對象排序:back to front
FTranslucentPrimSet::SortPrimitives();

//determine visibility of each light
DoFrustumCullForLights();

//-- Base Pass對象排序:front to back

void FDeferredShadingSceneRenderer::SortBasePassStaticData();
}
}

void FDeferredShadingSceneRenderer::{ Render()
{
//-- EarlyZPass
FDeferredShadingSceneRenderer::RenderPrePass();
RenderOcclusion();

//-- Build Gbuffers
SetAndClearViewGBuffer(); FDeferredShadingSceneRender::RenderBasePass();
FSceneRenderTargets::FinishRenderingGBuffer();

//-- Lighting stage
RenderDynamicSkyLighting();
RenderAtmosphere();
RenderFog();
RenderTranslucency();
RenderDistortion();
//-- post processing
SceneContext.ResolveSceneColor();
FPostProcessing::Process();
FDeferredShadingSceneRenderer::RenderFinish();
}

void FDeferredShadingSceneRenderer::RenderLights()
{
foreach(FLightSceneInfoCompact light IN Scene->Lights)
{
void FDeferredShadingSceneRenderer::RenderLight(Light)
{
RHICmdList.SetBlendState(Additive Blending);
// DeferredLightVertexShaders.usf VertexShader = TDeferredLightVS; // DeferredLightPixelShaders.usf PixelShader = TDeferredLightPS;
switch(Light Type)
{
case LightType_Directional:
DrawFullScreenRectangle();
case LightType_Point:
StencilingGeometry::DrawSphere();
case LightType_Spot:
StencilingGeometry::DrawCone();
}
}
}
}

虛幻 4 的 VR 渲染


接下來主要分析虛幻 4 的 VR 渲染是如何實現的。虛幻 4 的渲染或者整個引擎實現的思路跟 Unity 差距還是很大,Unity 個人理解最大的好處就是統一性做得非常好,包括 Camera 這塊也是做得非常好,Camera 不光代表一個視點,而且也管理一個渲染管線。因爲裏面如果實現 VR 渲染,相對來說好理解一些,很直接相當於可以放兩個攝像機,一個是放左眼圖象,一個放右眼圖象。這樣的結構非常清晰,但是不太好做一些深層次的優化。在虛幻 4 引擎裏面,實際上把整個 VR 整合到整個引擎各個邏輯流程,各個模塊裏面,所以它能夠比較好實現優化。新的 VR 主要是 Scene View Family 和Scene View 爲基礎的。

首先看一下代碼目錄,在Plugins/Runtime/GoogleVR/GoogleVRHMD等等裏面。

插件有兩個主要類,一個就是 GoogleVRHMD,另外是 GoogleVR HMDCustomPersent,前面講了 VR 是把流程整合到每一步的邏輯裏面去,所以它會選出來一些接口。這裏只列了一些重點函數,接口都挺大的,裏面的函數都非常多。

接口

谷歌 VR HMD 主要實現了兩個 interface,一個是 AdjustViewRect(),這一類比較簡單,上述講每一幀開始渲染的時候,會計算新 view 的一些狀態和參數,相當於有一些函數在不同的時機可以參與計算或者新的 SceneViewFamily 還有 SceneView。這個比較簡單,就是模塊的起始、停止。

另外還有一個就是 CalculateStereoViewOffset() 接口,這個是實現立體渲染的一些核心操作,都要實現這個接口的一些方法。這兩類實際上起到一個包裝 VR SDK 和黏合層的作用。

接下來從代碼流程來看一下 VR 渲染相關的一些步驟。首先在引擎 Init() 的時候,會查找所有 HMD 的模塊,一旦啓動了這個插件,它在引擎 Init()的時候,就會創建 HMDDevice,在啓動的時候纔會啓動 VR 渲染。

  • 創建HMD Device
//-- 在引擎啓動時,會創建所有的HMD設備void UEngine::Init()
{
bool UEngine::InitializeHMDDevice()
{
for (auto HMDModuleIt = HMDModules.CreateIterator();
HMDModuleIt; ++HMDModuleIt)
{
IHeadMountedDisplayModule* HMDModule = *HMDModuleIt;
HMDDevice = HMDModule->CreateHeadMountedDisplay();
}
} }

SceneViewFamily 和SceneView是如何啓動VR渲染的,首先要看是不是啓動立體渲染,如果是立體渲染,view會被強制設定成兩個,然後會在每一幀,有一個接口,給你機會做這麼幾件事,一個是調整那個視口的範圍,還有一個就是因爲VR渲染兩個攝像機的相應眼睛的位置是有一定距離的,可以去調整view的視點的距離。

  • 繪製流程入口
//-- 在View繪製時,如果是Stereo則繪製兩個View
void UGameViewportClient::Draw()
{
const bool bEnableStereo =
GEngine->IsStereoscopic3D(InViewport);
int32 NumViews = bEnableStereo ? 2 : 1;
for (int32 i = 0; i < NumViews; ++i)
{
}
}
  • IStereoRendering接口調用
void UGameViewportClient::Draw() 
{
ULocalPlayer::CalcSceneView()
{
 ULocalPlayer::GetProjectionData() 
{
GEngine->StereoRenderingDevice->AdjustViewRect(StereoPass);
GEngine->StereoRenderingDevice->CalculateStereoViewOffset(StereoPass);
ProjectionData.ProjectionMatrix=GEngine->StereoRenderingDevice->GetStereoProjectionMatrix(StereoPass);
}
}
}

接下來看一下谷歌 VR HMD 裏面插件的代碼。首先通過剛纔的AdjustViewRect,把viewport作一個調整,如果是左眼pass,就會調整左邊的一版,如果是右邊pass而就會調整右邊的一版。另外通過CalculateStereoViewOffset()的方法去調整試點的位置,首先它是調用了SDK裏面取得兩眼同距的方法,通過計算算出眼睛view的location的偏離量。

最後是在Render,這個也是接口函數,在RenderThread裏調用的一個方法,這個方法最終會調用谷歌 VR 的 API,會把普遍圖象和專業圖象調到VR SDK,再有它進行操作反映到手機屏幕上。

void FGoogleVRHMD::AdjustViewRect(StereoPass, int32& X,
int32& Y, uint32& SizeX, uint32& SizeY) const
{
SizeX = SizeX / 2;
if( StereoPass == eSSP_RIGHT_EYE )
X += SizeX;
}

void FGoogleVRHMD::CalculateStereoViewOffset()
{
const float EyeOffset = (GetInterpupillaryDistance() * 0.5f)
* WorldToMeters;
const float PassOffset = (StereoPassType == eSSP_LEFT_EYE) ?
-EyeOffset : EyeOffset;
ViewLocation +=
ViewRotation.Quaternion().RotateVector(FVector(0,PassOffset,0
));
}

void FGoogleVRHMD::RenderTexture_RenderThread()
{
gvr_distort_to_screen(GVRAPI,
SrcTexture->GetNativeResource(),
CachedDistortedRenderTextureParams,
&CachedPose,
&CachedFuturePoseTime);

} 轉載自:http://geek.csdn.net/news/detail/106495

發佈了119 篇原創文章 · 獲贊 91 · 訪問量 45萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章