内容概要
2016年9月24日我在MDCC 2016 VR 峰会上我做了一个技术分享,这里是这次分享的技术资料
【欢迎转载,请注明作者:房燕良,原文出处:游戏程序员的自我修养】
演讲资料
- 完整PPT下载:2016-vr-summit-ue4.pdf
故事梗概
3D 引擎架构大图
虚幻4渲染相关模块
虚幻4引擎采用了模块化的设计,首先我们来看一下渲染相关的有哪些模块:
- RenderCore、Render
- RHI,也就是Render Hardware Interface,是对图形API的封装;虚幻4的RHI接口,最初是基于D3D 11来设计的;
- 针对主流平台上的主流图形API,基本都做了适配,包括D3D 11、OpenGL、Metal和最新的Vulkan;
- 这些模块的代码都在:Engine\Source\Runtime目录下;
多线程结构:游戏线程+渲染线程
- 从虚幻3开始引入了渲染线程—Gemini
- 没有渲染线程的情况:
- 使用渲染线程:
- 主线程通过Render Command Queue向渲染线程发送命令;
- 主线程通过Render Command Fence机制来控制节奏;
- 场景数据管理
- 主线程管理场景数据;渲染线程使用Proxy对象;
场景对象
- 场景数据分两层:UWorld、ULevel、AActor这些是面向游戏逻辑层的,方便游戏逻辑层去管理;
- 而对于渲染UWorld包含一个FScene对象。FScene是面向渲染器器的,管理当前World里面的所有Primitive和Light。
- FSceneRender类用来控制渲染流程,它有两个派生类,用来实现前向渲染和延迟渲染;
- 对于Shader Model 4以下的平台,选择前向渲染;否则使用延迟渲染。
- FSceneViewFamily/FSceneView
- 在每一帧,可以把FScene渲染到多个View;这个以前主要是用于单机游戏多玩家分屏显示用的。
- 例如极品飞车,可以选择2个人一起玩,但屏幕一半显示你的视图,另一半显示我的视图;
- 现在这个分屏机制也用来实现VR立体渲染,这个后面再详细说;
- 在每一帧,可以把FScene渲染到多个View;这个以前主要是用于单机游戏多玩家分屏显示用的。
- FViewInfo从FSceneView派生,附加了Scene Renderer所需要的额外状态数据;
- 在FSceneRenderer创建时,每帧创建;
对于游戏逻辑层Aactor管理组件,对于渲染来说,核心的组件是UPrimitiveComponent和ULightComponent,它们有各自的一个派生体系来处理不同类型的Primitive和灯光。罗列一下核心的一些Class:
- UPrimitiveComponent管理一个FPrimitiveSceneProxy。在组件注册的时候,UPrimitiveComponent的派生类里面会创建FPrimitiveSceneProxy的派生类对象。例如UStaticMeshComponent创建一个FStaticMeshSceneProxy对象;
- FPrimitiveSceneProxy是Engine模块中定义的一个类,用来存储UPrimitiveComponent的渲染状态数据;
- 在Renderer模块中还有FPrimitiveSceneInfo,用来存储渲染器关心的内部数据(Engine模块不可见);
- 灯光的处理手法和Primitive基本一致;
- UPrimitiveComponent<-UMeshComponent
- UStaticMeshComponent
- USkeletalMeshComponent
- ULightComponent
- UDirectionalLightComponent
- UPointLightComponent
- FPrimitiveSceneProxy
- FStaticMeshSceneProxy
- FSkeletalMeshSceneProxy
- FLightSceneProxy
- FDirectionalLightSceneProxy
- FPointLightSceneProxyBase:FPointLightSceneProxy,FSpotLightSceneProxy
虚幻4基本渲染流程
- Game线程
void UGameEngine::Tick( float DeltaSeconds, bool bIdleMode )
{
UGameEngine::RedrawViewports()
{
void FViewport::Draw( bool bShouldPresent)
{
void UGameViewportClient::Draw()
{
//-- 计算ViewFamily、View的各种属性
ULocalPlayer::CalcSceneView();
//-- 发送渲染命令:FDrawSceneCommand
FRendererModule::BeginRenderingViewFamily()
//-- Draw HUD
PlayerController->MyHUD->PostRender();
}
}}}
FrameEndSync.Sync();
- 渲染线程:
- RenderViewFamily_RenderThread()
- FSceneRenderer::Render()
- InitViews():
- Primitive Visibility Determination:Frustum Cull+Occlusion Cull
- Light Visibility:Frustum Cull
- 透明物体排序:Back to Front
- 不透明物体排序:Front to Back
- InitViews():
- Base Pass
- Lighting & Fog
- Translucent
- Post Process
虚幻4的延迟渲染
Deferred Shading的基本流程就是先使用一个前向渲染的Pass,把场景中的3D对象渲染到N个geometry buffer上;这几个geometry buffer用来保存后续的光照等计算需要的值。这里列出了虚幻4引擎中的几个基础的GBuffer;
- GBufferA主要存储的世界坐标系中的法线向量;
- GBufferB和C,大家可以把这些分量和虚幻的材质对应上了;
大家可以自己去看一下class FSceneRenderTargets这个类的代码,这个类管理 了所有的RenderTargets;另外,还有DeferredShadingCommon.usf这个shader文件,GBuffer的Encode、Decode都在这里;
FDeferredShadingSceneRenderer
- GBuffers: class FSceneRenderTargets
- DeferredShadingCommon.usf
在编辑器中可以将这些 GBuffer 可视化出来:
核心流程的伪代码如下:
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();
}
//-- EarlyZPass
FDeferredShadingSceneRenderer::RenderPrePass();
RenderOcclusion();
//-- Build Gbuffers
SetAndClearViewGBuffer();
FDeferredShadingSceneRenderer::RenderBasePass();
FSceneRenderTargets::FinishRenderingGBuffer();
//-- Lighting stage
RenderLights();
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();
}
}
}
}
VR/HMD 渲染相关类
为什么要深入学习引擎架构
现在商业3D引擎越来越成熟,特别是Unity3D引擎引领的引擎工具化潮流,大大提高了开发效率。开发的门槛也降低了很多,那我们是否还有必要去深入学习引擎底层算法、引擎架构呢?
个人认为还是非常有必要的!为什么呢?大家都知道,我们现代的软件工程是基于分层抽象建立起来的,好比说引擎是一层,它通过抽象把底层的复杂度封装了起来,这样在上层就可以更关注自己的业务。然而,系统分层和抽象封装可以提供开发效率,却不能提高学习效率,这是因为它在80%的时候工作的很好,但是在20%的时候会失效,如果你对底层完全不理解,那你就完全蒙圈。举个另外的例子,你看很多搞网络编程的兄弟,经常捧一本比砖头还厚的《TCP/IP详解》。以上这个观点,来自一本文集《Joel说软件》:抽象漏洞定律。我读完之后,深以为然。
从另外一个角度说,游戏开发技术是建立在很多概念之上的,引擎对这些概念进行了实现和封装,方便我们直接调用。但是,如果你并不理解这些概念,以及它背后的算法,那你对它的时间效率和空间效率等问题就很难有一个正确的把握。
So,尽管商业引擎越来越成熟,对于爱知求真的小伙伴,还是要沉下心,去深入学习,建立起稳固的知识体系。
Written on September 27th, 2016 by 房燕良