虚幻4渲染系统结构解析

2016年10月11日 17:33 0 点赞 0 评论 更新于 2025-11-21 20:35

一、引言

今天为大家分享的主题是虚幻4渲染系统结构解析。内容主要包含以下几个模块:

  1. 从3D引擎架构的角度,讲解渲染系统在架构层面所处的位置以及与其他模块之间的关系。
  2. 重点讲述虚幻4渲染系统的架构,主要从三个方面展开:
    • 渲染线程跟主线程的基础架构。
    • 场景管理。
    • 从渲染流程控制角度详解该架构是如何设计和实现的。
  3. 分析虚幻4的VR在引擎层实现的流程,并以谷歌VR HMD插件为例进行讲解。

二、3D引擎渲染系统

(一)相关模块

下图展示了一个3D引擎与渲染系统相关的几个模块,包括资源系统、材质系统、场景管理以及渲染管线管理。这些模块在下层都会调用图形API来实现渲染功能。整个3D引擎,包括渲染系统,最核心面临的问题主要有两个:管理复杂度和效率。

(二)复杂度问题

当前,整个3D引擎的渲染难度系数极高,需要实现各种各样的渲染效果、渲染算法以及优化算法,这使得管理复杂度成为一个巨大的挑战。

(三)效率问题

“对游戏来说,效率就是生命” —— 卡马克。效率问题主要体现在两个方面:

  1. 图形算法方面:包括可变性的判定、流程控制的优化以及平衡CPU和GPU的工作。
  2. 硬件利用方面:软件开发者需要关注如何高效发挥GPU的高并发流水线架构,以及如何利用GPU上的各种Cache来提高Driver的命中率。

三、虚幻4渲染系统架构

(一)渲染系统模块

  1. 源代码存放位置:Engine/Source/Runtime主要存放模块的源代码。
  2. 核心代码模块:包括RenderCore和Renderer。
  3. RHI抽象层:RHI(Render Hardware Interface),虚幻4的版本RHI最初是基于D3D 11设计的。
  4. RHI实现层:对于主流的平台和主流的推荐API都有相应的实现,如EmptyRHI、Windows上的D3D11RHI、苹果上的Metal、OpenGLdRV、VulkanRHI。

(二)渲染线程

在论及引擎的数据管理与渲染的流程控制之前,我们需要先理解渲染线程。渲染线程机制是从虚幻3开始引入的,当时开发代号为Gemini。引入渲染线程主要是从效率方面考虑。一个游戏开发完成后,渲染、游戏逻辑(包括脚本更新)以及物理模拟这三个大模块占据每一帧的时间最多。如果将渲染和游戏逻辑更新并行起来,就可以显著提升效率。

若没有渲染线程,游戏逻辑的更新和渲染是串行的,一帧所占的时间是两者执行时间的总和;使用渲染线程后,一帧的时间则是两者耗时最长的那个,在理想情况下会有显著的渲染提升。

(三)线程同步问题

引入渲染线程后,会涉及到两个线程之间的同步问题,主要分为以下两方面:

  1. 速率控制:游戏有运行的速率控制问题,通常游戏线程负载较低,渲染线程进行控制。为防止游戏线程跑得太快,使用了Render Command Fence机制。例如,前台正在显示第N帧画面时,渲染线程可以渲染第N + 1帧,游戏线程可以处理第N + 2帧。
  2. 场景管理同步:增加渲染线程后,游戏的复杂度大大提升。游戏线程和渲染线程都可能修改场景数据,容易出错。在虚幻引擎中,使用了Proxy对象模式来处理。游戏逻辑中的游戏对象在渲染线程中对应一个Proxy对象,该Proxy对象的游戏更新完全在渲染线程中进行。此外,渲染线程中每一帧的特定状态数据会在每一帧进行拷贝。

下图展示了渲染线程跟主线程的基本关系,主线程会通过渲染命令的队列向渲染线程发送消息,渲染线程会从命令队列中读取命令,它们之间通过Render Command Fence机制进行同步。

(四)场景数据管理

  1. 核心类:虚幻引擎场景的数据管理分为两层。一层是UWorld,主要面向游戏逻辑开发,方便上层进行逻辑控制。对于渲染来说,UWorld对应FScene对象,其数据接口设计主要面向浏览器。FSceneRenderer有两个派生类,分别是FForwardShadingSceneRenderer(前置渲染)和FDeferredShadingSceneRenderer(延迟渲染)。在Shader Model 4.0以下采用逻辑渲染,在Shader Model 4.0以上则选择延迟渲染。
  2. FSceneViewFamily类:该类用于处理一帧中可以渲染的多个视图。最初在单机游戏多人同时玩的分屏游戏(如游戏机上的极品飞车)中使用,现在VR兴起后,也用于VR渲染,将左眼图像和右眼图像分别显示在屏幕的左右两侧。
  3. FViewInfo类:定义在Render模块中,用于存储新视图的特定数据。每一帧会有一些自己的状态,需要进行数据拷贝,部分数据保存在该类中。

(五)渲染流程

以下是一个伪代码,提取了引擎中渲染相关的一些关键步骤,仅为突出重点:

// Game线程
void UGameEngine::Tick(float DeltaSeconds, bool bIdleMode)
{
UGameEngine::RedrawViewports()
{
void FViewport::Draw(bool bShouldPresent)
{
void UGameViewportClient::Draw()
{
//-- 计算ViewFamily、View的各种属性
ULocalPlayer::CalcSceneView();
//-- 发送渲染FRendererModule命令
FRendererModule::BeginRenderingViewFamily();
//-- Draw HUD
PlayerController->MyHUD->PostRender();
}
}
}
}

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()函数,主要步骤如下:

  1. 初始化视图:InitViews()函数首先调用Primitive Visibility Determination进行剪裁,然后对透明物体进行排序,确定灯光的可见性,最后对不透明物体进行排序。
  2. 通过多个Pass进行渲染:首先进行base pass,建立base缓冲,填充GBuffer的缓冲,然后渲染所有的灯光、天光、大气效果、透明对象和屏幕区特效,完成SceneColor()的渲染,最后进行后处理并调用RenderFinish()。
  3. 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();
FDeferredShadingSceneRenderer::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(LightType)
{
case LightType_Directional:
DrawFullScreenRectangle();
break;
case LightType_Point:
StencilingGeometry::DrawSphere();
break;
case LightType_Spot:
StencilingGeometry::DrawCone();
break;
}
}
}
}

四、虚幻4的VR渲染

(一)与Unity的对比

虚幻4的渲染,或者说整个引擎的实现思路与Unity有很大差距。Unity的统一性做得非常好,Camera不仅代表一个视点,还管理一个渲染管线。在Unity中实现VR渲染相对容易理解,相当于放置两个摄像机,分别用于渲染左眼图像和右眼图像,但不太容易进行深层次的优化。而在虚幻4引擎中,将整个VR整合到引擎的各个逻辑流程和模块中,能够更好地实现优化。新的VR主要基于Scene View Family和Scene View。

(二)代码目录与插件类

代码目录位于Plugins/Runtime/GoogleVR/GoogleVRHMD等。插件有两个主要类,分别是GoogleVRHMD和GoogleVR HMDCustomPersent。VR将流程整合到每一步的逻辑中,会选择一些接口,这里仅列出一些重点函数。

(三)谷歌VR HMD实现的接口

  1. AdjustViewRect()接口:在每一帧开始渲染时,会计算新视图的一些状态和参数,该接口的函数可以在不同时机参与计算新的SceneViewFamily和SceneView,主要负责模块的起始和停止。
  2. CalculateStereoViewOffset()接口:实现立体渲染的核心操作,需要实现该接口的一些方法。这两个接口实际上起到了包装VR SDK和黏合层的作用。

(四)VR渲染的代码流程

  1. 创建HMD Device:在引擎Init()时,会查找所有HMD的模块。一旦启动插件,在引擎Init()时会创建HMDDevice,启动VR渲染。
    //-- 在引擎启动时,会创建所有的HMD设备
    void UEngine::Init()
    {
    bool UEngine::InitializeHMDDevice()
    {
    for (auto HMDModuleIt = HMDModules.CreateIterator();
    HMDModuleIt; ++HMDModuleIt)
    {
    IHeadMountedDisplayModule* HMDModule = *HMDModuleIt;
    HMDDevice = HMDModule->CreateHeadMountedDisplay();
    }
    }
    }
    
  2. 启动VR渲染:首先判断是否启动立体渲染,如果是,则将视图强制设定为两个。每一帧会有一个接口,允许进行调整视口范围和视点距离的操作。
    //-- 在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); } } }

3. **谷歌VR HMD插件代码**:通过AdjustViewRect()调整视口,根据左眼或右眼的渲染通道调整视口范围;通过CalculateStereoViewOffset()方法调整视点位置,计算眼睛视图的位置偏离量。最后在Render()方法中,调用谷歌VR的API,将普通图像和专业图像传递给VR SDK,由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); }


综上所述,本文详细解析了虚幻4渲染系统的结构,包括渲染系统在3D引擎架构中的位置、渲染系统的架构设计、场景数据管理、渲染流程以及VR渲染的实现方式,希望能为相关开发者提供有价值的参考。

作者信息

孟子菇凉

孟子菇凉

共发布了 3994 篇文章