Published on

由SetActorLocation分析渲染流程

Authors
  • avatar
    Name
    东哥
    Twitter

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

起点:SetActorLocation

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

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

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

USceneComponent
MoveComponent

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

这个函数在UPrimitiveComponent内有重写

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

UpdateComponentToWorldWithParent

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

然后再直接调用到此函数

	if (Parent && !Parent->bComponentToWorldUpdated)
	{
		Parent->UpdateComponentToWorld();

		if (bComponentToWorldUpdated)
		{
			return;
		}
	}

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

然后调用到函数PropagateTransformUpdate

PropagateTransformUpdate

该函数执行了

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

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

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

然后调用了UWorldMarkActorComponentForNeededEndOfFrameUpdate

看主要代码

void UActorComponent::MarkRenderTransformDirty()
{
	if (IsRegistered() && bRenderStateCreated)
	{
		bRenderTransformDirty = true;
		MarkForNeededEndOfFrameUpdate();
	}
}
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

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大概就是要更新场景最新的渲染的时候就会调用,先不理会,看这个函数内实现

	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告诉引擎需要处理相应的渲染任务

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

{
	check(bRenderStateCreated);
	bRenderTransformDirty = false;

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

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

SendRenderTransform_Concurrent
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加入到渲染线程的数据集合中

代码如下

FPrimitiveSceneProxy* PrimitiveSceneProxy = Primitive->CreateSceneProxy();
	Primitive->SceneProxy = PrimitiveSceneProxy;
// Create the primitive scene info.
	FPrimitiveSceneInfo* PrimitiveSceneInfo = new FPrimitiveSceneInfo(Primitive, this);
	PrimitiveSceneProxy->PrimitiveSceneInfo = PrimitiveSceneInfo;
// 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();
	});
	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 中去完成绘制,这里不继续深挖了,埋个扣子,以后来解开

// 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的内容了

上一张流程简图

补充
场景中静态物体的渲染顺序堆栈表参考
ActorComponet::ExecuteRegisterEvents
UPrimitiveComponent::CreateRenderState_Concurrent
FScene:: AddPrimitive
FScene:: AddPrimitiveSceneInfo_RenderThread
FScene:: AddStaticMeshes
FStaticMesh::AddToDrawLists
FMobileBasePassOpaqueDrawingPolicyFactory::AddStaticMesh
ProcessMobileBasePassMesh
FDrawMobileBasePassStaticMeshAction:: Process
AddMeshToStaticDrawList
//加入到scene GetMobileBasePassDrawList中

最终加入队列 以DrawingPolicykey map队列

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