虛幻引擎的框架設計的一個基本思路是:游戲邏輯與渲染邏輯分離。
即存在一個游戲的世界:包含在場景中的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 Thread | Render 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 | 為FScene里的節點,包含一個FPrimitiveSceneProxy成員和一些view無關的渲染狀態。只存在渲染器模塊,所以引擎模塊無法感知到它的存在。 |
FSceneView | 描述了FScene內的單個視圖(view),同個FScene允許有多個view,換言之,一個場景可以被多個view繪制,或者多個view同時被繪制。每一幀都會創建新的view實例。 |
FViewInfo | 從FSceneView派生,用於存放裁剪后的幾何體可見性,只存在渲染器模塊,引擎模塊不可見。 |
FSceneViewState | 存儲了有關view的渲染器私有信息,這些信息需要被跨幀訪問。在Game實例,每個ULocalPlayer擁有一個FSceneViewState實例。 |
FSceneRenderer | 每幀都會被創建(在游戲線程中),封裝幀間臨時數據。下派生FDeferredShadingSceneRenderer(延遲着色場景渲染器)和FMobileSceneRenderer(移動端場景渲染器),分別代表PC和移動端的默認渲染器。 |
FLightSceneProxy | 光源代理,是ULightComponent在渲染器的代表,鏡像了ULightComponent在渲染線程的狀態。 |
FLightSceneInfo | 從FRenderResource派生,包含一個FLightSceneProxy成員和用於光照計算的信息。只存在渲染器模塊,所以引擎模塊無法感知到它的存在。 |
游戲線程和渲染線程的交互
首先看看游戲線程如何將數據傳遞給渲染線程。
游戲線程在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,LoadMap和LevelStreaming情況略有不同。
LoadMap情況下
為了提升並發程度,游戲線程通過ParallelFor函數把FScene::AddPrimitive操作放到TaskGraph中執行了,具體實現邏輯大致如下:
① 在注冊UPrimitiveComponent時,會被添加到FRegisterComponentContext的AddPrimitiveBatches數組中
② UWorld在UpdateWorldComponents中調用FRegisterComponentContext::Process函數,來使用ParallelFor函數把FScene::AddPrimitive操作放到TaskGraph中並發執行
③ TaskGraph的工作線程中,FScene::AddPrimitive會調用CreateSceneProxy來創建場景代理對象
LevelStreaming情況下
直接在游戲線程中調用UPrimitiveComponent::CreateRenderState_Concurrent函數來執行FScene::AddPrimitive,創建SceneProxy對象
調用堆棧如下:
對於LightComponent,LoadMap和LevelStreaming則都是直接在游戲線程中調用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); }); } }
LoadMap情況下
調用堆棧如下:
LevelStreaming情況下
調用堆棧如下:
不過這里還有疑問,那就是創建PrimitiveSceneProxy的時候會拷貝一份數據,但在創建完之后,PrimitiveComponent是如何向PrimitiveSceneProxy更新數據的呢?
SceneProxy(場景代理)對象的更新
游戲線程每一幀在UWorld::SendAllEndOfFrameUpdates中會調用各個UActorComponent的DoDeferredRenderUpdates_Concurrent函數來檢查更新
UActorComponent有幾個標記(bRenderStateDirty、bRenderTransformDirty、bRenderDynamicDataDirty),只要這幾個標記被標記為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; } void MarkRenderStateDirty() { // If registered and has a render state to mark as dirty if(IsRegistered() && bRenderStateCreated && (!bRenderStateDirty || !GetWorld())) { // Flag as dirty bRenderStateDirty = true; // 添加到UWorld的TArray<UActorComponent*> ComponentsThatNeedEndOfFrameUpdate數組中 // 或添加到UWorld的TArray<UActorComponent*> ComponentsThatNeedEndOfFrameUpdate_OnGameThread數組中 // 或添加到UWorld的TSet<UActorComponent*> ComponentsThatNeedPreEndOfFrameSync數組中 MarkForNeededEndOfFrameRecreate(); MarkRenderStateDirtyEvent.Broadcast(*this); } } void MarkRenderTransformDirty() { if (IsRegistered() && bRenderStateCreated) { bRenderTransformDirty = true; // 添加到UWorld的TArray<UActorComponent*> ComponentsThatNeedEndOfFrameUpdate數組中 // 或添加到UWorld的TArray<UActorComponent*> ComponentsThatNeedEndOfFrameUpdate_OnGameThread數組中 // 或添加到UWorld的TSet<UActorComponent*> ComponentsThatNeedPreEndOfFrameSync數組中 MarkForNeededEndOfFrameUpdate(); } } void MarkRenderDynamicDataDirty() { // If registered and has a render state to mark as dirty if(IsRegistered() && bRenderStateCreated) { // Flag as dirty bRenderDynamicDataDirty = true; // 添加到UWorld的TArray<UActorComponent*> ComponentsThatNeedEndOfFrameUpdate數組中 // 或添加到UWorld的TArray<UActorComponent*> ComponentsThatNeedEndOfFrameUpdate_OnGameThread數組中 // 或添加到UWorld的TSet<UActorComponent*> ComponentsThatNeedPreEndOfFrameSync數組中 MarkForNeededEndOfFrameUpdate(); } } void MarkForNeededEndOfFrameRecreate() { if (bNeverNeedsRenderUpdate) { return; } UWorld* ComponentWorld = GetWorld(); if (ComponentWorld) { // by convention, recreates are always done on the gamethread ComponentWorld->MarkActorComponentForNeededEndOfFrameUpdate(this, RequiresGameThreadEndOfFrameRecreate()); } else if (!IsUnreachable()) { // we don't have a world, do it right now. DoDeferredRenderUpdates_Concurrent(); } } void MarkForNeededEndOfFrameUpdate() { if (bNeverNeedsRenderUpdate) { return; } UWorld* ComponentWorld = GetWorld(); if (ComponentWorld) { ComponentWorld->MarkActorComponentForNeededEndOfFrameUpdate(this, RequiresGameThreadEndOfFrameUpdates()); } else if (!IsUnreachable()) { // we don't have a world, do it right now. DoDeferredRenderUpdates_Concurrent(); } } private: uint8 bRenderStateDirty:1; // 組件的渲染狀態是否臟的 uint8 bRenderTransformDirty:1; // 組件的變換矩陣是否臟的 uint8 bRenderDynamicDataDirty:1; // 組件的渲染動態數據是否臟的 };
在UWorld::SendAllEndOfFrameUpdates中遍歷TArray<UActorComponent*> ComponentsThatNeedEndOfFrameUpdate和TArray<UActorComponent*> ComponentsThatNeedEndOfFrameUpdate_OnGameThread數組
逐個執行數組中UActorComponent對象的DoDeferredRenderUpdates_Concurrent函數,根據標記來決定是重新創建SceneProxy對象,還是更新Transform,還是更新DynamicData。
如下UStaticMeshComponent的bRenderStateDirty為true,所以在執行的UStaticMeshComponent::CreateRenderState_Concurrent函數中重新創建了FStaticMeshSceneProxy對象
對於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對象的回收
參考