本文根据小米互娱 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();
}
}}}FrameEndSynvoid 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();
});
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

接下来用伪代码的方式来看一下渲染主干的流程,首先入口还是 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 backvoid 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();
}
}
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65

虚幻 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();
}
} }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

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)
{
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • IStereoRendering接口调用
void UGameViewportClient::Draw()
{
ULocalPlayer::CalcSceneView()
{ULocalPlayer::GetProjectionData()
{
GEngine->StereoRenderingDevice->AdjustViewRect(StereoPass);
GEngine->StereoRenderingDevice->CalculateStereoViewOffset(StereoPass);
ProjectionData.ProjectionMatrix=GEngine->StereoRenderingDevice->GetStereoProjectionMatrix(StereoPass);
}
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

接下来看一下谷歌 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);
}

虚幻4渲染系统结构解析相关推荐

  1. 虚幻四中怎么保持导入模型坐标_[CG分享]|虚幻引擎5 技术解析

    今天还是跟大家聊一聊最近很火的虚拟引擎,Epic Game公司的虚幻引擎5惊艳了全球游戏业,其Nanite虚拟微多边形几何技术和Lumen动态全局光照技术带来了产业界的飞跃.Nanite虚拟几何技术的 ...

  2. UWA学堂上新|虚幻引擎源码解析——基础容器篇

    文章简介 文章主要介绍了虚幻引擎的基础容器的内部数据结构和实现原理,以及在实践中的应用,性能优化等方面.包括:TArray.TSparseArray.TSet.TMap等基础容器,TQueue.TTr ...

  3. 渲染到ui_虚幻4渲染编程(UI篇)【第二卷:程序化UI特效-[1]】

    MY BLOG DIRECTORY: 小IVan:专题概述及目录​zhuanlan.zhihu.com INTRODUCTION: 当遇见某些特殊需求,比如对游戏效果有很多变化的要求,这时使用静态的贴 ...

  4. 虚幻4皮肤材质_虚幻4渲染编程(材质编辑器篇)【第六卷:各向异性材质amp;玻璃材质】...

    My blog directory: YivanLee:专题概述及目录​zhuanlan.zhihu.com Introduction: 各向异性材质 玻璃材质 材质编辑器篇的很多效果都非常简单,可以 ...

  5. 浏览器渲染流水线解析

    摘要: 若干年前,我写过一篇介绍浏览器渲染流水线的文章 - How Rendering Work (in WebKit and Blink),这篇文章,一来部分内容已经过时,二来缺少一个全局视角来对流 ...

  6. 虚幻4渲染编程(材质编辑器篇)【第三卷:正式准备开始材质开发】

    My blog directory: YivanLee:专题概述及目录 Introduction: 前面两章我们已经完成了对工具的研究,下面我们久正式开始启程啦!后面的内容可能就比较美术了. 还是老规 ...

  7. 虚幻4渲染编程(光线追踪篇)【第一卷:光线追踪篇开篇综述】

    MY BLOG DIRECTORY: 小IVan:专题概述及目录​zhuanlan.zhihu.com INTRODUCTION: 什么都不说了先上个效果: 光线追踪云 电子游戏的光线追踪时代即将到来 ...

  8. 虚幻4渲染编程(环境模拟篇)【第三卷:体积云天空模拟(3)---高层云】

    我的专栏目录: 小IVan:专题概述及目录 目前业内流行有两种体积云模拟的方式,模型+特殊shader法,RayMarching法.我前两篇文章已经对它们都做了介绍.当然还有些比较非主流的,比如粒子云 ...

  9. 《十》浏览器基础及渲染引擎解析一个网页的过程、JavaScript 引擎解析 JavaScript 代码的过程

    浏览器:是安装在电脑里面的一个软件,能够将页面内容渲染出来呈现给用户查看,并让用户与网页进行交互. 服务器其实就是性能比较高的计算机,这些计算机 24 小时不断电. 不关机. 开发者在本地开发出 HT ...

最新文章

  1. 宏定义中#号和##号的使用
  2. gradle 指定springcloud 版本_springcloud小技能:服务注册发现如何隔离
  3. 小度拆卸_拆卸invokedynamic
  4. EditPlus3 添加 PHP代码格式化
  5. NLP13-LDA引发的一系活动
  6. DOM节点的属性及文本操作
  7. [导入][ASP.NET 控件实作 Day14] 继承 CompositeControl 实作 Toolbar 控件
  8. SFR算法详解(二)——斜棱法
  9. python中re模块的group()和groups()
  10. POI填充Excel背景色
  11. GAMES101-现代计算机图形学入门-闫令琪——Lecture 18 Advanced Topics in Rendering 学习笔记
  12. 什么是PLC可编程控制器,理论基础知识讲解QY-KC801
  13. php 邮件 延迟发送,PHP后台隔5分钟发送email邮件_php
  14. JavaScript Window窗口对象
  15. 程序员的年终总结,各种版本各种残
  16. java list 模糊查询_如何在java List中进行模糊查询(示例代码)
  17. 智加科技完成A+轮融资,推动物流产业升级
  18. mac怎么做一段卡点音乐
  19. 钱颖一:从清华学生身上,我发现了这7个普遍现象……
  20. 763.Partition Labels (Medium)

热门文章

  1. Fiddler过滤指定域名
  2. 凯斯西储大学计算机工程排名,[转载]凯斯西储大学排名及世界排名【研究生】...
  3. C# 列出进程以及详细信息
  4. 2021年衢州高考的成绩查询,2021年衢州高考状元是谁分数多少分,历年衢州高考状元名单...
  5. (一)elasticsearch6.1.1安装详细过程
  6. 1065. 单身狗(25)
  7. Fiddler 学习笔记---命令、断点
  8. Spring-Boot——Cache
  9. jQuery基本语法
  10. 在Oracle中利用SQL_TRACE跟踪SQL的执行