由SetActorLocation分析渲染流程

角色位移本质上就是渲染问题,从这条思路我们去看看角色怎么实现这一部分渲染的

起点:SetActorLocation

蓝图调用的API实际调用的是K2_SetActorLocation,然后调用到此函数

1
return RootComponent->MoveComponent(Delta, GetActorQuat(), bSweep, OutSweepHitResult, MOVECOMP_NoFlags, Teleport);

只看这个关键代码,调用RootComponentMoveComponent

USceneComponent
MoveComponent

外部调用此函数后里面直接调用到带Impl后缀的虚函数

这个函数在UPrimitiveComponent内有重写

USceneComponent内主要调用了函数ConditionalUpdateComponentToWorld,而UPrimitiveComponent因为有碰撞等属性所以又多了很多逻辑,这里不多描述

UpdateComponentToWorldWithParent

此函数通过ConditionalUpdateComponentToWorld调用,后又调用到虚函数UpdateComponentToWorld,此虚函数在UActorComponent声明,在USceneComponent重写

然后再直接调用到此函数

1
2
3
4
5
6
7
8
9
if (Parent && !Parent->bComponentToWorldUpdated)
{
Parent->UpdateComponentToWorld();

if (bComponentToWorldUpdated)
{
return;
}
}

如果有父级组件,先去处理他

然后调用到函数PropagateTransformUpdate

PropagateTransformUpdate

该函数执行了

  • 更新体积(Bounds)
  • 更新变换(Transform)
    • 更新Attach的子组件的变换信息
  • 更新寻路信息

其中有个关键的函数UActorComponent::MarkRenderTransformDirty

然后经过简单判断以后调用UActorComponent::MarkForNeededEndOfFrameUpdate

然后调用了UWorldMarkActorComponentForNeededEndOfFrameUpdate

看主要代码

1
2
3
4
5
6
7
8
void UActorComponent::MarkRenderTransformDirty()
{
if (IsRegistered() && bRenderStateCreated)
{
bRenderTransformDirty = true;
MarkForNeededEndOfFrameUpdate();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void UActorComponent::MarkForNeededEndOfFrameUpdate()
{
if (bNeverNeedsRenderUpdate)
{
return;
}

UWorld* ComponentWorld = GetWorld();
if (ComponentWorld)
{
ComponentWorld->MarkActorComponentForNeededEndOfFrameUpdate(this, RequiresGameThreadEndOfFrameUpdates());
}
else if (!IsUnreachable())
{
// 如果没有世界,执行如下代码
DoDeferredRenderUpdates_Concurrent();
}
}
UWorld
MarkActorComponentForNeededEndOfFrameUpdate

代码在LevelTick.cpp L883

主要执行了把组件添加到了等待渲染的列表ComponentsThatNeedEndOfFrameUpdate_OnGameThread里,当然还有一个非游戏线程的ComponentsThatNeedEndOfFrameUpdate

1
2
3
4
5
6
7
8
9
10
if (bForceGameThread)
{
FMarkComponentEndOfFrameUpdateState::Set(Component, ComponentsThatNeedEndOfFrameUpdate_OnGameThread.Num(), EComponentMarkedForEndOfFrameUpdateState::MarkedForGameThread);
ComponentsThatNeedEndOfFrameUpdate_OnGameThread.Add(Component);
}
else
{
FMarkComponentEndOfFrameUpdateState::Set(Component, ComponentsThatNeedEndOfFrameUpdate.Num(), EComponentMarkedForEndOfFrameUpdateState::Marked);
ComponentsThatNeedEndOfFrameUpdate.Add(Component);
}

到此我们大概知道了这一条线的终点了,然后去看看哪里使用到了这个数组

SendAllEndOfFrameUpdates

确认使用上面数组是在此函数内,查找引用发现此函数在很多地方被调用,如SceneRendering.cpp,UnrealEngine.cpp大概就是要更新场景最新的渲染的时候就会调用,先不理会,看这个函数内实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
for (UActorComponent* Component : ComponentsThatNeedEndOfFrameUpdate_OnGameThread)
{
if (Component)
{
if (Component->IsRegistered() && !Component->IsTemplate() && !Component->IsPendingKill())
{
FScopeCycleCounterUObject ComponentScope(Component);
FScopeCycleCounterUObject AdditionalScope(STATS ? Component->AdditionalStatObject() : nullptr);
Component->DoDeferredRenderUpdates_Concurrent();
}

check(Component->IsPendingKill() || Component->GetMarkedForEndOfFrameUpdateState() == EComponentMarkedForEndOfFrameUpdateState::MarkedForGameThread);
FMarkComponentEndOfFrameUpdateState::Set(Component, INDEX_NONE, EComponentMarkedForEndOfFrameUpdateState::Unmarked);
}
}
ComponentsThatNeedEndOfFrameUpdate_OnGameThread.Reset();
ComponentsThatNeedEndOfFrameUpdate.Reset();

遍历了所有数组成员,主要执行了DoDeferredRenderUpdates_Concurrent,回头看USceneComponent内的如果没有UWorld就直接调用了这个函数

UActorComponent
DoDeferredRenderUpdates_Concurrent

RecreateRenderState_Concurrent,SendRenderTransform_Concurrent,SendRenderDynamicData_Concurrent告诉引擎需要处理相应的渲染任务

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
void UActorComponent::DoDeferredRenderUpdates_Concurrent()
{
checkf(!IsUnreachable(), TEXT("%s"), *GetFullName());
checkf(!IsTemplate(), TEXT("%s"), *GetFullName());
checkf(!IsPendingKill(), TEXT("%s"), *GetFullName());

FScopeCycleCounterUObject ContextScope(this);

if(!IsRegistered())
{
UE_LOG(LogActorComponent, Log, TEXT("UpdateComponent: (%s) Not registered, Aborting."), *GetPathName());
return;
}

if(bRenderStateDirty)
{
SCOPE_CYCLE_COUNTER(STAT_PostTickComponentRecreate);
RecreateRenderState_Concurrent();
checkf(!bRenderStateDirty, TEXT("Failed to route CreateRenderState_Concurrent (%s)"), *GetFullName());
}
else
{
SCOPE_CYCLE_COUNTER(STAT_PostTickComponentLW);
if(bRenderTransformDirty)
{
// Update the component's transform if the actor has been moved since it was last updated.
SendRenderTransform_Concurrent();
}

if(bRenderDynamicDataDirty)
{
SendRenderDynamicData_Concurrent();
}
}
}
SendRenderTransform_Concurrent

这个函数在基类只做了基础实现,主要是派生类重写实现主要逻辑,类似的也包括SendRenderDynamicData_Concurrent,CreateRenderState_Concurrent

1
2
3
4
5
6
7
8
{
check(bRenderStateCreated);
bRenderTransformDirty = false;

#if LOG_RENDER_STATE
UE_LOG(LogActorComponent, Log, TEXT("SendRenderTransform_Concurrent: %s"), *GetPathName());
#endif
}
UPrimitiveComponent

紧接上面,我们看看有形状体积等信息的UPrimitiveComponent

SendRenderTransform_Concurrent
1
2
3
4
5
6
7
8
9
10
11
void UPrimitiveComponent::SendRenderTransform_Concurrent()
{
UpdateBounds();
const bool bDetailModeAllowsRendering = DetailMode <= GetCachedScalabilityCVars().DetailMode;
if( bDetailModeAllowsRendering && (ShouldRender() || bCastHiddenShadow))
{
GetWorld()->Scene->UpdatePrimitiveTransform(this);
}

Super::SendRenderTransform_Concurrent();
}

看到在实现基类逻辑之前执行了FScene内的函数UpdatePrimitiveTransform

FScene

代码在 RendererScene.cpp中

UpdatePrimitiveTransform

UpdatePrimitiveTransform函数中会判断该Primitive有没有SceneProxy,如果有SceneProxy则判断该Primitive是否需要重新创建,如果需要重新创建则需要先删除,然后再添加该Primitive;如果没有SceneProxy则直接加入到场景中。接着我们具体看一下AddPrimitive函数的具体实现:

AddPrimitive

此函数主要做了如下几件事情

  • 创建SceneProxyPrimitiveSceneInfo
  • 利用FPrimitiveSceneInfo封装UPrimitiveComponent的信息,继续在render线程中进行操作
  • 给渲染线程发送2个命令,分别是设置PrimitiveTransform信息和将PrimitiveSceneInfo加入到渲染线程的数据集合中

代码如下

1
2
3
4
5
FPrimitiveSceneProxy* PrimitiveSceneProxy = Primitive->CreateSceneProxy();
Primitive->SceneProxy = PrimitiveSceneProxy;
// Create the primitive scene info.
FPrimitiveSceneInfo* PrimitiveSceneInfo = new FPrimitiveSceneInfo(Primitive, this);
PrimitiveSceneProxy->PrimitiveSceneInfo = PrimitiveSceneInfo;
1
2
3
4
5
6
7
8
9
10
11
// Create any RenderThreadResources required.
ENQUEUE_RENDER_COMMAND(CreateRenderThreadResourcesCommand)(
[Params](FRHICommandListImmediate& RHICmdList)
{
FPrimitiveSceneProxy* SceneProxy = Params.PrimitiveSceneProxy;
FScopeCycleCounter Context(SceneProxy->GetStatId());
SceneProxy->SetTransform(Params.RenderMatrix, Params.WorldBounds, Params.LocalBounds, Params.AttachmentRootPosition);

// Create any RenderThreadResources required.
SceneProxy->CreateRenderThreadResources();
});
1
2
3
4
5
6
ENQUEUE_RENDER_COMMAND(AddPrimitiveCommand)(
[Scene, PrimitiveSceneInfo](FRHICommandListImmediate& RHICmdList)
{
FScopeCycleCounter Context(PrimitiveSceneInfo->Proxy->GetStatId());
Scene->AddPrimitiveSceneInfo_RenderThread(RHICmdList, PrimitiveSceneInfo);
});
AddPrimitiveSceneInfo_RenderThread

AddPrimitiveSceneInfo_RenderThread函数是在渲染线程执行的,完成PrimitiveBoundsPrimitiveFlagsCompact等数据的初始化后,并调用LinkAttachmentGroup处理primitive的父子关系,调用LinkLODParentComponent处理LOD父子关系,然后调用FPrimitiveSceneInfo::AddToScene

AddToScene最终会把物体添加到列表DrawList 中去完成绘制,这里不继续深挖了,埋个扣子,以后来解开

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Add the primitive to its shadow parent's linked list of children.
// Note: must happen before AddToScene because AddToScene depends on LightingAttachmentRoot
PrimitiveSceneInfo->LinkAttachmentGroup();

// Set lod Parent information if valid
PrimitiveSceneInfo->LinkLODParentComponent();

// Add the primitive to the scene.
const bool bAddToDrawLists = !(CVarDoLazyStaticMeshUpdate.GetValueOnRenderThread() && !WITH_EDITOR);
if (bAddToDrawLists)
{
PrimitiveSceneInfo->AddToScene(RHICmdList, true);
}
else
{
PrimitiveSceneInfo->AddToScene(RHICmdList, true, false);
PrimitiveSceneInfo->BeginDeferredUpdateStaticMeshes();
}
小结

由此我们就大致整理一下思路

设置Actor的位置,其实就是把新旧位置做一个插值,把这个插值传递给继承自USceneComponentMoveComponent方法,然后在下一帧渲染出新的位置

这里也接上了并解释了之前AI_MoveTo的内容了

上一张流程简图

补充
场景中静态物体的渲染顺序堆栈表参考
1
2
3
4
5
6
7
8
9
10
11
ActorComponet::ExecuteRegisterEvents
UPrimitiveComponent::CreateRenderState_Concurrent
FScene:: AddPrimitive
FScene:: AddPrimitiveSceneInfo_RenderThread
FScene:: AddStaticMeshes
FStaticMesh::AddToDrawLists
FMobileBasePassOpaqueDrawingPolicyFactory::AddStaticMesh
ProcessMobileBasePassMesh
FDrawMobileBasePassStaticMeshAction:: Process
AddMeshToStaticDrawList
//加入到scene GetMobileBasePassDrawList中

最终加入队列 以DrawingPolicykey map队列

1
TStaticMeshDrawList<TMobileBasePassDrawingPolicy<FUniformLightMapPolicy, 0> > MobileBasePassUniformLightMapPolicyDrawList[EBasePass_MAX];