内容概要
2016年9月24日我在MDCC 2016 VR 峰会上我做了一个技术分享,这里是这次分享的技术资料

【欢迎转载,请注明作者:房燕良,原文出处:游戏程序员的自我修养

演讲资料

故事梗概

3D 引擎架构大图

Big Picture

虚幻4渲染相关模块

虚幻4引擎采用了模块化的设计,首先我们来看一下渲染相关的有哪些模块:

  • RenderCore、Render
  • RHI,也就是Render Hardware Interface,是对图形API的封装;虚幻4的RHI接口,最初是基于D3D 11来设计的;
  • 针对主流平台上的主流图形API,基本都做了适配,包括D3D 11、OpenGL、Metal和最新的Vulkan;
  • 这些模块的代码都在:Engine\Source\Runtime目录下;

多线程结构:游戏线程+渲染线程

  • 从虚幻3开始引入了渲染线程—Gemini
  • 没有渲染线程的情况: Single Thread
  • 使用渲染线程: Render Thread
  • 主线程通过Render Command Queue向渲染线程发送命令;
  • 主线程通过Render Command Fence机制来控制节奏;
  • 场景数据管理
    • 主线程管理场景数据;渲染线程使用Proxy对象; Threads

场景对象

  • 场景数据分两层:UWorld、ULevel、AActor这些是面向游戏逻辑层的,方便游戏逻辑层去管理;
  • 而对于渲染UWorld包含一个FScene对象。FScene是面向渲染器器的,管理当前World里面的所有Primitive和Light。
  • FSceneRender类用来控制渲染流程,它有两个派生类,用来实现前向渲染和延迟渲染;
    • 对于Shader Model 4以下的平台,选择前向渲染;否则使用延迟渲染。
  • FSceneViewFamily/FSceneView
    • 在每一帧,可以把FScene渲染到多个View;这个以前主要是用于单机游戏多玩家分屏显示用的。
      • 例如极品飞车,可以选择2个人一起玩,但屏幕一半显示你的视图,另一半显示我的视图;
      • 现在这个分屏机制也用来实现VR立体渲染,这个后面再详细说;
  • FViewInfo从FSceneView派生,附加了Scene Renderer所需要的额外状态数据;
    • 在FSceneRenderer创建时,每帧创建;

UML

对于游戏逻辑层Aactor管理组件,对于渲染来说,核心的组件是UPrimitiveComponent和ULightComponent,它们有各自的一个派生体系来处理不同类型的Primitive和灯光。罗列一下核心的一些Class:

UML

  • 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
    • 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

UML

在编辑器中可以将这些 GBuffer 可视化出来:

UML

核心流程的伪代码如下:

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 渲染相关类

UML

为什么要深入学习引擎架构

现在商业3D引擎越来越成熟,特别是Unity3D引擎引领的引擎工具化潮流,大大提高了开发效率。开发的门槛也降低了很多,那我们是否还有必要去深入学习引擎底层算法、引擎架构呢?

个人认为还是非常有必要的!为什么呢?大家都知道,我们现代的软件工程是基于分层抽象建立起来的,好比说引擎是一层,它通过抽象把底层的复杂度封装了起来,这样在上层就可以更关注自己的业务。然而,系统分层和抽象封装可以提供开发效率,却不能提高学习效率,这是因为它在80%的时候工作的很好,但是在20%的时候会失效,如果你对底层完全不理解,那你就完全蒙圈。举个另外的例子,你看很多搞网络编程的兄弟,经常捧一本比砖头还厚的《TCP/IP详解》。以上这个观点,来自一本文集《Joel说软件》:抽象漏洞定律。我读完之后,深以为然。

从另外一个角度说,游戏开发技术是建立在很多概念之上的,引擎对这些概念进行了实现和封装,方便我们直接调用。但是,如果你并不理解这些概念,以及它背后的算法,那你对它的时间效率和空间效率等问题就很难有一个正确的把握。

So,尽管商业引擎越来越成熟,对于爱知求真的小伙伴,还是要沉下心,去深入学习,建立起稳固的知识体系。