UE4游戏逻辑与渲染逻辑分离

虚幻引擎的框架设计的一个基本思路是:游戏逻辑与渲染逻辑分离。

即存在一个游戏的世界:包含在场景中的Actor(及与其关联的ActorComponent)。同时,与存在一个渲染的世界,这个世界中包含了呈现游戏世界所需要的信息。

渲染的世界如同布景一般,其只会呈现在当前摄像机范围内的,可以被渲染的内容。

例如:一个AStaticMeshActor及其包含UStaticMeshComponent组件对应游戏的世界,不会去处理渲染相关的逻辑,而是通过一个FStaticMeshSceneProxy场景代理对象来执行渲染。

任何一个可以被渲染的组件,都需要调用CreateSceneProxy()来创建对应的SceneProxy(场景代理)对象。注:CreateSceneProxy在组件注册到世界中的时候被调用,而不是每帧都调用

UActorComponent (184)
  USceneComponent (528)
    UAkPortalComponent (528)
    UAkGameObject (560)
      UAkComponent (1136)
        UAkAudioInputComponent (1152)
      UAkRoomComponent (608)
    UAkGeometryComponent (816)
    UAkLateReverbComponent (624)
    UAkSurfaceReflectorSetComponent (576)
    UMultiSourceSoundComponent (528)
    UPrimitiveComponent (1152)                                       FPrimitiveSceneProxy
      UMeshComponent (1200)                                              |
        UProceduralMeshComponent (1312)                                  |
          UCubeSphereComponent (1504)                                    |
        USkinnedMeshComponent (1760)                                     |
          USkeletalMeshComponent (3936)                                 FSkeletalMeshSceneProxy
            USkeletalMeshComponentBudgeted (3984)                        |
          UPoseableMeshComponent (2112)                                  |
        UStaticMeshComponent (1312)                                     FStaticMeshSceneProxy
          UInstancedStaticMeshComponent (1488)                               |
            UHierarchicalInstancedStaticMeshComponent (1728)                FInstancedStaticMeshSceneProxy
              UFoliageInstancedStaticMeshComponent (1776)                    |
          UInteractiveFoliageComponent (1328)                               FInteractiveFoliageSceneProxy
          UControlPointMeshComponent (1328)
          ULandscapeMeshProxyComponent (1360)
          USplineMeshComponent (1472)
        UPaperFlipbookComponent (1280)
        UPaperGroupedSpriteComponent (1248)
        UPaperSpriteComponent (1232)
        UPaperTileMapComponent (1280)
        UCableComponent (1344)
        UGeometryCacheComponent (1296)
        UGroomComponent (1472)
        UWidgetComponent (1488)
        UGeometryCollectionComponent (2352)
      UPaperTerrainComponent (1232)
      USplineComponent (1392)
        UPaperTerrainSplineComponent (1408)
      UNPCAINavMeshRenderingComponent (1152)
      UCoverPointRenderingComponent (1152)
      UShapeComponent (1168)
        UBoxComponent (1184)
          UGlassBoxComponent (1216)
        UCapsuleComponent (1184)
        USphereComponent (1184)
          UDrawSphereComponent (1184)
      UFXSystemComponent (1152)
        UNiagaraComponent (1568)
        UParticleSystemComponent (1760)
          UUIParticleComponent (1760)
      UControlRigComponent (1392)
      ULensFlareBillboardComponent (1216)
      UMRMeshComponent (1328)
      UMotionControllerComponent (1328)
      ULandscapeComponent (1696)                                      FLandscapeComponentSceneProxy : public FPrimitiveSceneProxy, public FLandscapeNeighborInfo
      ULandscapeGizmoRenderComponent (1152)
      ULandscapeHeightfieldCollisionComponent (1376)
        ULandscapeMeshCollisionComponent (1408)
      ULandscapeSplinesComponent (1200)
      UArrowComponent (1168)
      UBillboardComponent (1184)                                      FSpriteSceneProxy : public FPrimitiveSceneProxy
      UBrushComponent (1168)
      UDrawFrustumComponent (1168)
      UGlobalILCComponent (1168)
      ULineBatchComponent (1216)
      UMaterialBillboardComponent (1168)
      UModelComponent (1216)
      UTextRenderComponent (1232)
      UVectorFieldComponent (1184)
      UNavLinkComponent (1168)
      UNavLinkRenderingComponent (1152)
      UNavMeshRenderingComponent (1152)
      UNavTestRenderingComponent (1152)
      UEQSRenderingComponent (1200)
      UFuncTestRenderingComponent (1152)
      UGizmoBaseComponent (1184)
        UGizmoArrowComponent (1200)
        UGizmoBoxComponent (1232)
        UGizmoCircleComponent (1200)
        UGizmoLineHandleComponent (1216)
        UGizmoRectangleComponent (1232)
      UFieldSystemComponent (1200)
    USceneCaptureComponent (720)
      USceneCaptureComponent2D (2368)
      UPlanarReflectionComponent (960)
      USceneCaptureComponentCube (768)
    UILCTextureComponent (528)
    UILCDynamicScaleComponent (528)
    USynthComponent (1744)
      UVoipListenerSynthComponent (1856)
      USynthComponentMoto (1968)
      UMediaSoundComponent (2368)
    UMockDataMeshTrackerComponent (640)
    UARComponent (656)
      UARPlaneComponent (784)
      UARPointComponent (656)
      UARFaceComponent (752)
      UARImageComponent (752)
      UARQRCodeComponent (768)
      UARPoseComponent (720)
      UAREnvironmentProbeComponent (704)
      UARObjectComponent (704)
      UARMeshComponent (752)
      UARGeoAnchorComponent (768)
    UARLifeCycleComponent (576)
    UWidgetInteractionComponent (1056)
    UCameraComponent (2128)
      UCineCameraComponent (2384)
    UAtmosphericFogComponent (784)
    UAudioComponent (2160)
    UReflectionCaptureComponent (656)                                FReflectionCaptureProxy
      UBoxReflectionCaptureComponent (672)
      UPlaneReflectionCaptureComponent (672)
      USphereReflectionCaptureComponent (672)
    UCameraShakeSourceComponent (544)
    UChildActorComponent (576)
    UDecalComponent (592)                                            FDeferredDecalProxy
    ULightComponentBase (576)
      ULightComponent (816)                                          FLightSceneProxy
        UDirectionalLightComponent (1040)                                FDirectionalLightSceneProxy
        ULocalLightComponent (848)                                       FLocalLightSceneProxy
          UPointLightComponent (864)                                        FPointLightSceneProxy
            USpotLightComponent (880)                                           FSpotLightSceneProxy
          URectLightComponent (880)                                         FRectLightSceneProxy
USkyLightComponent (
1056) FSkyLightSceneProxy UExponentialHeightFogComponent (672) UForceFeedbackComponent (752) ULightmassPortalComponent (528) UPhysicsConstraintComponent (1040) UPhysicsSpringComponent (560) UPhysicsThrusterComponent (528) UPostProcessComponent (2032) URadialForceComponent (576) URuntimeVirtualTextureComponent (640) UShadowCaptureComponent (768) USkyAtmosphereComponent (752) USpringArmComponent (656) UStereoLayerComponent (752) UVolumetricCloudComponent (592) UWindDirectionalSourceComponent (560) UNavigationGraphNodeComponent (560) UTestPhaseComponent (528) UChaosDestructionListener (1072)

游戏线程和渲染线程代表

游戏线程的对象通常做逻辑更新,在内存中有一份持久的数据,为了避免游戏线程和渲染线程产生竞争条件,会在渲染线程额外存储一份内存拷贝,并且使用的是另外的类型。

以下是UE比较常见的类型映射关系(游戏线程对象以U开头,渲染线程以F开头):

Game ThreadRender Thread
UWorld FScene
UPrimitiveComponent FPrimitiveSceneProxy / FPrimitiveSceneInfo
- FSceneView / FViewInfo
ULocalPlayer FSceneViewState
ULightComponent FLightSceneProxy / FLightSceneInfo

游戏线程代表一般由游戏游戏线程操作,渲染线程代表主要由渲染线程操作。如果尝试跨线程操作数据,将会引发不可预料的结果,产生竞争条件。

/** SceneProxy在注册进场景时,会在游戏线程中被构造和传递数据。 */
FStaticMeshSceneProxy::FStaticMeshSceneProxy(UStaticMeshComponent* InComponent):
    FPrimitiveSceneProxy(...),
    Owner(InComponent->GetOwner()) //<======== 此处将AActor指针被缓存
    ...

/** SceneProxy的DrawDynamicElements将被渲染器在渲染线程中调用 */
void FStaticMeshSceneProxy::DrawDynamicElements(...)
{
    if (Owner->AnyProperty) //<========== 将会引发竞争条件!  游戏线程拥有AActor、UObject的所有状态!!并且UObject对象可能被GC掉,此时再访问会引起程序崩溃!!
}

部分代表比较特殊,如FPrimitiveSceneProxy、FLightSceneProxy ,这些场景代理本属于引擎模块,但又属于渲染线程专属对象,说明它们是连接游戏线程和渲染线程的桥梁,是线程间传递数据的工具人。

各类型的说明如下:

类型解释
UWorld 包含了一组可以相互交互的Actor和组件的集合,多个关卡(Level)可以被加载进UWorld或从UWorld卸载。
ULevel 关卡,存储着一组Actor和组件,并且存储在同一个文件。
USceneComponent 场景组件,是所有可以被加入到场景的物体的父类,比如灯光、模型、雾等。
UPrimitiveComponent 图元组件,是所有可渲染或拥有物理模拟的物体父类。是CPU层裁剪的最小粒度单位,
ULightComponent 光源组件,是所有光源类型的父类。
ULocalPlayer

本地玩家,代表一个全局生命周期的client,分屏的游戏会有多个client,其成员变量UGameViewportClient* ViewportClient中的FViewport* Viewport(实际为FSceneViewport类型)描述玩家的视口。

注:class FSceneViewport : public FViewportFrame, public FViewport, public ISlateViewport, public IViewportRenderTargetProvider

FScene 是UWorld在渲染模块的代表。只有加入到FScene的物体才会被渲染器感知到。渲染线程拥有FScene的所有状态(游戏线程不可直接修改)。
FPrimitiveSceneProxy 图元场景代理,是UPrimitiveComponent在渲染器的代表,镜像了UPrimitiveComponent在渲染线程的状态。
FPrimitiveSceneInfo 渲染器内部状态(描述了FRendererModule的实现),相当于融合了UPrimitiveComponent和FPrimitiveSceneProxy。只存在渲染器模块,所以引擎模块无法感知到它的存在。
FSceneView 描述了FScene内的单个视图(view),同个FScene允许有多个view,换言之,一个场景可以被多个view绘制,或者多个view同时被绘制。每一帧都会创建新的view实例。
FViewInfo view在渲染器的内部代表,只存在渲染器模块,引擎模块不可见。
FSceneViewState 存储了有关view的渲染器私有信息,这些信息需要被跨帧访问。在Game实例,每个ULocalPlayer拥有一个FSceneViewState实例。
FSceneRenderer 每帧都会被创建(在游戏线程中),封装帧间临时数据。下派生FDeferredShadingSceneRenderer(延迟着色场景渲染器)和FMobileSceneRenderer(移动端场景渲染器),分别代表PC和移动端的默认渲染器。
FLightSceneProxy 光源代理,是ULightComponent在渲染器的代表,镜像了ULightComponent在渲染线程的状态。
FLightSceneInfo 包含用于光照计算的信息。只存在渲染器模块,所以引擎模块无法感知到它的存在。

游戏线程和渲染线程的交互

首先看看游戏线程如何将数据传递给渲染线程。

游戏线程在Tick时,会通过UGameEngine、FViewport、UGameViewportClient等对象,才会进入渲染模块的调用:

void UGameEngine::Tick( float DeltaSeconds, bool bIdleMode )
{
    UGameEngine::RedrawViewports()
    {
        void FViewport::Draw( bool bShouldPresent)
        {
            void UGameViewportClient::Draw()
            {
                // 计算ViewFamily、View的各种属性
                ULocalPlayer::CalcSceneView();
                // 发送渲染命令
                FRendererModule::BeginRenderingViewFamily()
                {
                    World->SendAllEndOfFrameUpdates();
                    // 创建场景渲染器
                    FSceneRenderer* SceneRenderer = FSceneRenderer::CreateSceneRenderer(ViewFamily, ...);
                    // 向渲染线程发送绘制场景指令.
                    ENQUEUE_RENDER_COMMAND(FDrawSceneCommand)(
                    [SceneRenderer](FRHICommandListImmediate& RHICmdList)
                    {
                        RenderViewFamily_RenderThread(RHICmdList, SceneRenderer)
                        {
                            (......)
                            // 调用场景渲染器的绘制接口.
                            SceneRenderer->Render(RHICmdList);
                            (......)
                        }
                        FlushPendingDeleteRHIResources_RenderThread();
                    });
                }
}}}}

前面章节也提到,渲染线程使用的是SceneProxy和SceneInfo等对象,那么游戏的Actor组件是如何跟场景代理的数据联系起来的呢?又是如何更新数据的?

SceneProxy(场景代理)对象的创建

先弄清楚游戏组件向SceneProxy传递数据的机制,答案就藏在FScene::AddPrimitive

// Engine\Source\Runtime\Renderer\Private\RendererScene.cpp

void FScene::AddPrimitive(UPrimitiveComponent* Primitive)
{
    (......)
    
    // 创建图元的场景代理
    FPrimitiveSceneProxy* PrimitiveSceneProxy = Primitive->CreateSceneProxy();
    Primitive->SceneProxy = PrimitiveSceneProxy;
    if(!PrimitiveSceneProxy)
    {
        return;
    }

    // 创建图元场景代理的场景信息
    FPrimitiveSceneInfo* PrimitiveSceneInfo = new FPrimitiveSceneInfo(Primitive, this);
    PrimitiveSceneProxy->PrimitiveSceneInfo = PrimitiveSceneInfo;
    
    (......)

    FScene* Scene = this;

    ENQUEUE_RENDER_COMMAND(AddPrimitiveCommand)(
        [Params = MoveTemp(Params), Scene, PrimitiveSceneInfo, PreviousTransform = MoveTemp(PreviousTransform)](FRHICommandListImmediate& RHICmdList)
        {
            FPrimitiveSceneProxy* SceneProxy = Params.PrimitiveSceneProxy;
            
            (......)

            SceneProxy->CreateRenderThreadResources();
            // 在渲染线程中将SceneInfo加入到场景中.
            Scene->AddPrimitiveSceneInfo_RenderThread(PrimitiveSceneInfo, PreviousTransform);
        });
}

上面有个关键的一句Primitive->CreateSceneProxy()即是创建组件对应的PrimitiveSceneProxy,在PrimitiveSceneProxy的构造函数中,将组件的所有数据都拷贝了一份:

FPrimitiveSceneProxy::FPrimitiveSceneProxy(const UPrimitiveComponent* InComponent, FName InResourceName)
:
    CustomPrimitiveData(InComponent->GetCustomPrimitiveData())
,    TranslucencySortPriority(FMath::Clamp(InComponent->TranslucencySortPriority, SHRT_MIN, SHRT_MAX))
,    Mobility(InComponent->Mobility)
,    LightmapType(InComponent->LightmapType)
,    StatId()
,    DrawInGame(InComponent->IsVisible())
,    DrawInEditor(InComponent->GetVisibleFlag())
,    bReceivesDecals(InComponent->bReceivesDecals)

(......)

{
    (......)
}

拷贝数据之后,游戏线程修改的是PrimitiveComponent的数据,而渲染线程修改或访问的是PrimitiveSceneProxy的数据,彼此不干扰,避免了临界区和锁的同步,也保证了线程安全。

对于PrimitiveComponent,为了提升并发程度,游戏线程通过ParallelFor函数把FScene::AddPrimitive操作放到TaskGraph中执行了,具体实现逻辑大致如下:

① 在注册UPrimitiveComponent时,会被添加到FRegisterComponentContext的AddPrimitiveBatches数组中

② UWorld在UpdateWorldComponents中调用FRegisterComponentContext::Process函数,来使用ParallelFor函数把FScene::AddPrimitive操作放到TaskGraph中并发执行

 

 ③ TaskGraph的工作线程中,FScene::AddPrimitive会调用CreateSceneProxy来创建场景代理对象

对于LightComponent,则直接在游戏线程中调用ULightComponent::CreateRenderState_Concurrent函数来执行FScene::AddLight,创建FLightSceneProxy对象

void FScene::AddLight(ULightComponent* Light)
{
    LLM_SCOPE(ELLMTag::SceneRender);

    // Create the light's scene proxy.
    FLightSceneProxy* Proxy = Light->CreateSceneProxy();
    if(Proxy)
    {
        // Associate the proxy with the light.
        Light->SceneProxy = Proxy;

        // Update the light's transform and position.
        Proxy->SetTransform(Light->GetComponentTransform().ToMatrixNoScale(), Light->GetLightPosition());

        // Create the light scene info.
        Proxy->LightSceneInfo = new FLightSceneInfo(Proxy, true);

        INC_DWORD_STAT(STAT_SceneLights);

        // Adding a new light
        ++NumVisibleLights_GameThread;

        // Send a command to the rendering thread to add the light to the scene.
        FScene* Scene = this;
        FLightSceneInfo* LightSceneInfo = Proxy->LightSceneInfo;
        ENQUEUE_RENDER_COMMAND(FAddLightCommand)(
            [Scene, LightSceneInfo](FRHICommandListImmediate& RHICmdList)
            {
                CSV_SCOPED_TIMING_STAT_EXCLUSIVE(Scene_AddLight);
                FScopeCycleCounter Context(LightSceneInfo->Proxy->GetStatId());
                Scene->AddLightSceneInfo_RenderThread(LightSceneInfo);
            });
    }
}

调用堆栈如下:

不过这里还有疑问,那就是创建PrimitiveSceneProxy的时候会拷贝一份数据,但在创建完之后,PrimitiveComponent是如何向PrimitiveSceneProxy更新数据的呢?

SceneProxy(场景代理)对象的更新

游戏线程每一帧在UWorld::SendAllEndOfFrameUpdates中会调用各个UActorComponent的DoDeferredRenderUpdates_Concurrent函数来检查更新

ActorComponent有几个标记,只要这几个标记被标记为true,便会在适当的时机调用更新接口,以便得到更新:

// Engine\Source\Runtime\Engine\Classes\Components\ActorComponent.h

class ENGINE_API UActorComponent : public UObject, public IInterface_AssetUserData
{
protected:
    // 以下接口分别更新对应的状态, 子类可以重写以实现自己的更新逻辑.
    virtual void DoDeferredRenderUpdates_Concurrent()
    {
        (......)
        
        if(bRenderStateDirty)
        {
            RecreateRenderState_Concurrent();
        }
        else
        {
            if(bRenderTransformDirty)
            {
                SendRenderTransform_Concurrent();
            }
            if(bRenderDynamicDataDirty)
            {
                SendRenderDynamicData_Concurrent();
            }
        }
    }
    virtual void CreateRenderState_Concurrent(FRegisterComponentContext* Context)
    {
        bRenderStateCreated = true;

        bRenderStateDirty = false;
        bRenderTransformDirty = false;
        bRenderDynamicDataDirty = false;
    }
    virtual void SendRenderTransform_Concurrent()
    {
        bRenderTransformDirty = false;
    }
    virtual void SendRenderDynamicData_Concurrent()
    {
        bRenderDynamicDataDirty = false;
    }
    
private:
    uint8 bRenderStateDirty:1; // 组件的渲染状态是否脏的
    uint8 bRenderTransformDirty:1; // 组件的变换矩阵是否脏的
    uint8 bRenderDynamicDataDirty:1; // 组件的渲染动态数据是否脏的
};

上面protected的接口就是用于刷新组件的数据到对应的SceneProxy,具体的组件子类可以重写它,以定制自己的更新逻辑

对于PrimitiveComponent,其变换矩阵更新逻辑如下:

// Engine\Source\Runtime\Engine\Private\Components\PrimitiveComponent.cpp

void
UPrimitiveComponent::SendRenderTransform_Concurrent() { UpdateBounds(); // If the primitive isn't hidden update its transform. const bool bDetailModeAllowsRendering = DetailMode <= GetCachedScalabilityCVars().DetailMode; if( bDetailModeAllowsRendering && (ShouldRender() || bCastHiddenShadow)) {
// 将变换信息更新到场景
// Update the scene info's transform for this primitive. GetWorld()->Scene->UpdatePrimitiveTransform(this); } Super::SendRenderTransform_Concurrent(); }

而场景的UpdatePrimitiveTransform会将组件的数据组装起来,并将数据发送到渲染线程执行:

void FScene::UpdatePrimitiveTransform(UPrimitiveComponent* Primitive)
{
    SCOPE_CYCLE_COUNTER(STAT_UpdatePrimitiveTransformGT);
    SCOPED_NAMED_EVENT(FScene_UpdatePrimitiveTransform, FColor::Yellow);

    // Save the world transform for next time the primitive is added to the scene
    const float WorldTime = GetWorld()->GetTimeSeconds();
    float DeltaTime = WorldTime - Primitive->LastSubmitTime;
    if ( DeltaTime < -0.0001f || Primitive->LastSubmitTime < 0.0001f )
    {
        // Time was reset?
        Primitive->LastSubmitTime = WorldTime;
    }
    else if ( DeltaTime > 0.0001f )
    {
        // First call for the new frame?
        Primitive->LastSubmitTime = WorldTime;
    }

    if(Primitive->SceneProxy)
    {
        // Check if the primitive needs to recreate its proxy for the transform update.
        if(Primitive->ShouldRecreateProxyOnUpdateTransform())
        {
            // Re-add the primitive from scratch to recreate the primitive's proxy.
            RemovePrimitive(Primitive);
            AddPrimitive(Primitive);
        }
        else
        {
            FVector AttachmentRootPosition(0);

            AActor* Actor = Primitive->GetAttachmentRootActor();
            if (Actor != NULL)
            {
                AttachmentRootPosition = Actor->GetActorLocation();
            }

            struct FPrimitiveUpdateParams
            {
                FScene* Scene;
                FPrimitiveSceneProxy* PrimitiveSceneProxy;
                FBoxSphereBounds WorldBounds;
                FBoxSphereBounds LocalBounds;
                FMatrix LocalToWorld;
                TOptional<FTransform> PreviousTransform;
                FVector AttachmentRootPosition;
            };

            FPrimitiveUpdateParams UpdateParams;
            UpdateParams.Scene = this;
            UpdateParams.PrimitiveSceneProxy = Primitive->SceneProxy;
            UpdateParams.WorldBounds = Primitive->Bounds;
            UpdateParams.LocalToWorld = Primitive->GetRenderMatrix();
            UpdateParams.AttachmentRootPosition = AttachmentRootPosition;
            UpdateParams.LocalBounds = Primitive->CalcBounds(FTransform::Identity);
            UpdateParams.PreviousTransform = FMotionVectorSimulation::Get().GetPreviousTransform(Primitive);

            // Help track down primitive with bad bounds way before the it gets to the Renderer
            ensureMsgf(!Primitive->Bounds.BoxExtent.ContainsNaN() && !Primitive->Bounds.Origin.ContainsNaN() && !FMath::IsNaN(Primitive->Bounds.SphereRadius) && FMath::IsFinite(Primitive->Bounds.SphereRadius),
                TEXT("Nans found on Bounds for Primitive %s: Origin %s, BoxExtent %s, SphereRadius %f"), *Primitive->GetName(), *Primitive->Bounds.Origin.ToString(), *Primitive->Bounds.BoxExtent.ToString(), Primitive->Bounds.SphereRadius);

            ENQUEUE_RENDER_COMMAND(UpdateTransformCommand)(
                [UpdateParams](FRHICommandListImmediate& RHICmdList)
                {
                    FScopeCycleCounter Context(UpdateParams.PrimitiveSceneProxy->GetStatId());
                    //在渲染线程上执行更新
                    UpdateParams.Scene->UpdatePrimitiveTransform_RenderThread(UpdateParams.PrimitiveSceneProxy, UpdateParams.WorldBounds, UpdateParams.LocalBounds, UpdateParams.LocalToWorld, UpdateParams.AttachmentRootPosition, UpdateParams.PreviousTransform);
                });
        }
    }
    else
    {
        // If the primitive doesn't have a scene info object yet, it must be added from scratch.
        AddPrimitive(Primitive);
    }
}

void FScene::UpdatePrimitiveTransform_RenderThread(FPrimitiveSceneProxy* PrimitiveSceneProxy, const FBoxSphereBounds& WorldBounds, const FBoxSphereBounds& LocalBounds, const FMatrix& LocalToWorld, const FVector& AttachmentRootPosition, const TOptional<FTransform>& PreviousTransform)
{
    check(IsInRenderingThread());
    if (GWarningOnRedundantTransformUpdate && PrimitiveSceneProxy->WouldSetTransformBeRedundant(LocalToWorld, WorldBounds, LocalBounds, AttachmentRootPosition))
    {
        UE_LOG(LogRenderer, Warning, TEXT("Redundant UpdatePrimitiveTransform_RenderThread Owner: %s, Resource: %s, Level: %s"), *PrimitiveSceneProxy->GetOwnerName().ToString(), *PrimitiveSceneProxy->GetResourceName().ToString(), *PrimitiveSceneProxy->GetLevelName().ToString());
    }

    if (AddedPrimitiveSceneInfos.Contains(PrimitiveSceneProxy->GetPrimitiveSceneInfo()))
    {
        check(PrimitiveSceneProxy->GetPrimitiveSceneInfo()->PackedIndex == INDEX_NONE);
    }
    else
    {
        check(PrimitiveSceneProxy->GetPrimitiveSceneInfo()->PackedIndex != INDEX_NONE);
    }

    check(!RemovedPrimitiveSceneInfos.Contains(PrimitiveSceneProxy->GetPrimitiveSceneInfo()));
// 更新变换矩阵 UpdatedTransforms.Add(PrimitiveSceneProxy, { WorldBounds, LocalBounds, LocalToWorld, AttachmentRootPosition });
if (PreviousTransform.IsSet()) { OverridenPreviousTransforms.Add(PrimitiveSceneProxy->GetPrimitiveSceneInfo(), PreviousTransform.GetValue().ToMatrixWithScale()); } }

对于ULightComponent其变换矩阵更新逻辑如下:

// Engine\Source\Runtime\Engine\Private\Components\LightComponent.cpp

void ULightComponent::SendRenderTransform_Concurrent()
{
    // 将变换信息更新到场景.
    GetWorld()->Scene->UpdateLightTransform(this);
    Super::SendRenderTransform_Concurrent();
}

而场景的UpdateLightTransform会将组件的数据组装起来,并将数据发送到渲染线程执行:

// Engine\Source\Runtime\Renderer\Private\RendererScene.cpp

void FScene::UpdateLightTransform(ULightComponent* Light)
{
    if(Light->SceneProxy)
    {
        // 组装组件的数据到结构体(注意这里不能将Component的地址传到渲染线程,而是将所有要更新的数据拷贝一份)
        FUpdateLightTransformParameters Parameters;
        Parameters.LightToWorld = Light->GetComponentTransform().ToMatrixNoScale();
        Parameters.Position = Light->GetLightPosition();
        FScene* Scene = this;
        FLightSceneInfo* LightSceneInfo = Light->SceneProxy->GetLightSceneInfo();
        // 将数据发送到渲染线程执行.
        ENQUEUE_RENDER_COMMAND(UpdateLightTransform)(
            [Scene, LightSceneInfo, Parameters](FRHICommandListImmediate& RHICmdList)
            {
                FScopeCycleCounter Context(LightSceneInfo->Proxy->GetStatId());
                // 在渲染线程执行数据更新.
                Scene->UpdateLightTransform_RenderThread(LightSceneInfo, Parameters);
            });
    }
}

void FScene::UpdateLightTransform_RenderThread(FLightSceneInfo* LightSceneInfo, const FUpdateLightTransformParameters& Parameters)
{
    (......)

    // 更新变换矩阵.
    LightSceneInfo->Proxy->SetTransform(Parameters.LightToWorld, Parameters.Position);
        
    (......)
}

至此,组件如何向场景代理更新数据的逻辑终于理清了。

需要特别提醒的是,FScene、FSceneProxy等有些接口在游戏线程调用,而有些接口(一般带有_RenderThread的后缀)在渲染线程调用,切记不能跨线程调用,否则会产生竞争条件,引发程序崩溃。

SceneProxy(场景代理)对象的清理

对于PrimitiveComponent,具体实现逻辑大致如下:

① 在UActorComponent反注册时(如在切换地图或Level卸载时),调用UPrimitiveComponent::DestroyRenderState_Concurrent来执行FScene::RemovePrimitive函数

通过宏ENQUEUE_RENDER_COMMAND向渲染线程添加一个FRemovePrimitiveCommand任务

② 渲染线程执行FRemovePrimitiveCommand任务,将待删除的FPrimitiveSceneInfo* PrimitiveSceneInfo加入到TSet<FPrimitiveSceneInfo *> RemovedPrimitiveSceneInfos中

③ 每一帧游戏线程都会通过宏ENQUEUE_RENDER_COMMAND向渲染线程添加一个UpdateScenePrimitives任务

渲染线程执行UpdateScenePrimitives任务时,会调用FScene::UpdateAllPrimitiveSceneInfos函数来收集当前帧要删掉的TSet<FPrimitiveSceneInfo*> DeletedSceneInfos

并在FScene::UpdateAllPrimitiveSceneInfos函数末尾处执行delete,完成FPrimitiveSceneProxy对象的回收

对于LightComponent,具体实现逻辑大致如下:

① 在UActorComponent反注册时(如在切换地图或Level卸载时),调用ULightComponent::DestroyRenderState_Concurrent来执行FScene::RemoveLight函数

通过宏ENQUEUE_RENDER_COMMAND向渲染线程添加一个FRemoveLightCommand任务

② 渲染线程执行FRemoveLightCommand任务,在FScene::RemoveLightSceneInfo_RenderThread函数末尾处执行delete,完成FLightSceneProxy对象的回收

参考

剖析虚幻渲染体系(02)- 多线程渲染

剖析虚幻渲染体系(03)- 渲染机制

渲染线程(ue4官方文档)

原文地址:https://www.cnblogs.com/kekec/p/15721722.html