UE5 WP流送 WorldPartitionRuntimeSpatialHash 源码分析

UE5中提供了WorldPartitionRuntimeSpatialHash 与 新版WorldPartitionRuntimeHashSet两种流送Hash,官方推荐使用 WorldPartitionRuntimeHashSet(文档链接

WorldPartitionRuntimeHashSet官方推荐

不过秉着学习的想法,还是先看下SpatialHash的实现,再与最新的HashSet对比其优缺点。

参考文章

若不懂 WorldPartition Streaming 的Cell、Grid概念,可先学习下面两篇文章

流送信息生成

这是流送最重要且复杂的一步,入口函数为 UWorldPartitionRuntimeSpatialHash::GenerateStreaming()

  • 首先会收集在WorldSetting中的RuntimeGrid配置,并追加 None 的 RuntimeGrid 名,建立 RuntimeGridName -> GridIndex 的映射 GridsMapping

  • 遍历场景Actor,按 RuntimeGrid 划分,得到 GridActorSetInstances

TArray<TArray<const IStreamingGenerationContext::FActorSetInstance*>> GridActorSetInstances;
GridActorSetInstances.InsertDefaulted(0, AllGrids.Num());

StreamingGenerationContext->ForEachActorSetInstance([&GridsMapping, &GridActorSetInstances](const IStreamingGenerationContext::FActorSetInstance& ActorSetInstance)
{
	int32* FoundIndex = GridsMapping.Find(ActorSetInstance.RuntimeGrid);
	if (!FoundIndex)
	{
		//@todo_ow should this be done upstream?
		UE_LOG(LogWorldPartition, Log, TEXT("Invalid partition grid '%s' referenced by actor cluster"), *ActorSetInstance.RuntimeGrid.ToString());
	}

	int32 GridIndex = FoundIndex ? *FoundIndex : 0;
	GridActorSetInstances[GridIndex].Add(&ActorSetInstance);
});
  • 通过 GetPartitionedActors() 在对应的 Grid 中,进一步划分 Actor。
const FBox WorldBounds = StreamingGenerationContext->GetWorldBounds();
for (int32 GridIndex = 0; GridIndex < AllGrids.Num(); GridIndex++)
{
	const FSpatialHashRuntimeGrid& Grid = AllGrids[GridIndex];
	// 进一步划分 Actor
	const FSquare2DGridHelper PartitionedActors = GetPartitionedActors(WorldBounds, Grid, GridActorSetInstances[GridIndex], Settings);
	// 创建 Runtime 使用的 StreamingGrid
	if (!CreateStreamingGrid(Grid, PartitionedActors, StreamingPolicy, OutPackagesToGenerate))
	{
		return false;
	}
}

Actor划分到对应的GridCell

该步骤函数为 GetPartitionedActors()

  • 首先创建 FSquare2DGridHelper,这一步里面根据CellSize、WorldBounds,计算了LevelCount、GridSize(这里需搞清 GridSize、CellSize的概念)
FSquare2DGridHelper PartitionedActors = GetGridHelper(WorldBounds, FVector(Grid.Origin, 0), Grid.CellSize, Settings.bUseAlignedGridLevels);

Level越低(Level0是最高级),GridSize越小,CellSize越大,最低级GridSize == 1,WorldBounds范围(不会被卸载,AlwaysLoad)。

Levels.Reserve(GridLevelCount);
int64 CurrentCellSize = CellSize;
int64 CurrentGridSize = GridSize;
for (int32 Level = 0; Level < GridLevelCount; ++Level)
{
	int64 LevelGridSize = CurrentGridSize;

	if (!bUseAlignedGridLevels)
	{
		LevelGridSize = (Level == GridLevelCount - 1) ? CurrentGridSize : CurrentGridSize + 1;
	}

	Levels.Emplace(FVector2D(InOrigin), CurrentCellSize, LevelGridSize, Level);

	CurrentCellSize <<= 1;
	CurrentGridSize >>= 1;
}
// Make sure the always loaded cell exists
GetAlwaysLoadedCell();

bUseAlignedGridLevels 就是 RuntimeGrid 配置中为了解决边界对齐时,导致边界上Actor被分配到最低一级Level的选项。

最后的 GetAlwaysLoadedCell() ,是为了先将最低级Level的Cell创建出来,后续马上用来Check,
通过 LastGridLevel.ForEachIntersectingCells() 对创建的 Level 进行Check,不做也不影响后续逻辑。

  • FSquare2DGridHelper::ForEachIntersectingCells() 这个函数相当重要,是快速查找相交Cells的重要组成部分(RecastNavigation中也有这种操作,看来是个相当常见好用的空间哈希方法)。
    将传入的包围盒坐标映射为CellCoord,快速得到与传入包围盒相交的Cell,执行操作。

LastGridLevel.ForEachIntersectingCells(WorldBounds, [&IntersectingCellCount](const FGridCellCoord2& Coords) { ++IntersectingCellCount; });

int32 ForEachIntersectingCells(const FBox& InBox, TFunctionRef<void(const FGridCellCoord2&)> InOperation) const
{
	return ForEachIntersectingCellsBreakable(InBox, [InOperation](const FGridCellCoord2& Vector) { InOperation(Vector); return true; });
}

int32 ForEachIntersectingCellsBreakable(const FBox& InBox, TFunctionRef<bool(const FGridCellCoord2&)> InOperation) const
{
	int32 NumCells = 0;

	FGridCellCoord2 MinCellCoords;
	FGridCellCoord2 MaxCellCoords;
	const FBox2D Bounds2D(FVector2D(InBox.Min), FVector2D(InBox.Max));

	if (GetCellCoords(Bounds2D, MinCellCoords, MaxCellCoords))
	{
		for (int64 y = MinCellCoords.Y; y <= MaxCellCoords.Y; y++)
		{
			for (int64 x = MinCellCoords.X; x <= MaxCellCoords.X; x++)
			{
				const FGridCellCoord2 Coord(x, y);
				// Validate that generated coordinate is valid (in case we reached the 64-bit limit of cell index)
				if (IsValidCoords(Coord))
				{
					if (!InOperation(Coord))
					{
						return NumCells;
					}
					++NumCells;
				}
			}
		}
	}

	return NumCells;
}
  • 接下来,就是将 RuntimeGrid 的 Actor 划分到尽可能小的、能容纳该Actor的GridCell。
    FirstPotentialGridLevel 是 CellSize 刚好比 ActorBound 大的Level,因为Actor不是居中放置的,可能跨多个Cell,因此从 FirstPotentialGridLevel 开始逐级判断,找到Actor仅与一个GridCell相交的那级GridLevel(找不到,则放入最低级Level,AlwaysLoad)。
for (const IStreamingGenerationContext::FActorSetInstance* ActorSetInstance : ActorSetInstances)
{
	check(ActorSetInstance->ActorSet->Actors.Num() > 0);

	FSquare2DGridHelper::FGridLevel::FGridCell* GridCell = nullptr;

	if (ActorSetInstance->bIsSpatiallyLoaded)
	{
		const FBox2D ActorSetInstanceBounds(FVector2D(ActorSetInstance->Bounds.Min), FVector2D(ActorSetInstance->Bounds.Max));
		int32 LocationPlacementGridLevel = 0;
		// UseLocationPlacement 是用Actor中心点而不是包围盒来做划分
		if (ShouldActorUseLocationPlacement(ActorSetInstance, ActorSetInstanceBounds, LocationPlacementGridLevel))
		{
			……
		}
		else
		{
			// Find grid level cell that encompasses the actor cluster bounding box and put actors in it.
			const FVector2D ClusterSize = ActorSetInstanceBounds.GetSize();
			const double MinRequiredCellExtent = FMath::Max(ClusterSize.X, ClusterSize.Y);
			// 计算得出 CellSize 刚好比 ActorBound 大的Level
			const int32 FirstPotentialGridLevel = FMath::Max(FMath::CeilToDouble(FMath::Log2(MinRequiredCellExtent / (double)PartitionedActors.CellSize)), 0);
			// 从低Level到高Level判断
			for (int32 GridLevelIndex = FirstPotentialGridLevel; GridLevelIndex < PartitionedActors.Levels.Num(); GridLevelIndex++)
			{
				FSquare2DGridHelper::FGridLevel& GridLevel = PartitionedActors.Levels[GridLevelIndex];
				// 选择仅与一个GridCell相交的那级GridLevel
				if (GridLevel.GetNumIntersectingCells(ActorSetInstance->Bounds) == 1)
				{
					GridLevel.ForEachIntersectingCells(ActorSetInstance->Bounds, [&GridLevel, &GridCell](const FGridCellCoord2& Coords)
					{
						check(!GridCell);
						GridCell = &GridLevel.GetCell(Coords);
					});

					break;
				}
			}
		}
	}
	// 找不到,则放到最低级Level(即是AlwaysLoad)
	if (!GridCell)
	{
		GridCell = &PartitionedActors.GetAlwaysLoadedCell();
	}

	GridCell->AddActorSetInstance(ActorSetInstance);
}
  • 最后一步,GridCell->AddActorSetInstance(ActorSetInstance),根据Actor的DataLayer分组。
void AddActorSetInstance(const IStreamingGenerationContext::FActorSetInstance* ActorSetInstance)
{
	const FDataLayersID DataLayersID = FDataLayersID(ActorSetInstance->DataLayers);
	FGridCellDataChunk& ActorDataChunk = DataChunks.FindOrAddByHash(DataLayersID.GetHash(), FGridCellDataChunk(ActorSetInstance->DataLayers, ActorSetInstance->ContentBundleID));
	ActorDataChunk.AddActorSetInstance(ActorSetInstance);
}

创建SreamingGrid

该步骤对应函数为 UWorldPartitionRuntimeSpatialHash::CreateStreamingGrid()。
简单来说就是使用上面划分好的GridCell,为每个GridCell的每种DataLayer,创建Runtime使用的 StreamingCell,同时生成对应序列化的Package,记录其关联的Actor信息。

  • PopulateCellActorInstances():没看明白,看注释是与PIE的 AlwaysLoad 相关,需要过滤掉一些Actor,总之是取出该Cell对应的Actors。
  • GetCellNameString():根据WorldName、GridName、CellCoord、DataLayer等,得到唯一的CellName
    形如:MapName_Grid_Test_L0_X16_Y-21
  • CreateRuntimeCell():创建RuntimeCell,同时创建 RuntimeCellData(UWorldPartitionRuntimeCellDataSpatialHash,含有空间划分的一些信息,主要就一个ContentBounds)
  • PopulateRuntimeCell():将Actor 放入对应的 RuntimeStreamingCell,其中 RuntimeCell->Fixup() 会去除重复的Actor。
  • 随后,将生成的 RuntimeStreamingCell 放入对应的格子GridCells中。
    • CellIndex代表2D Grid坐标
    • GridLevel.LayerCells[LayerCellIndex].GridCells 中存放的是同个格子中、不同 DataLayer 的 StreamingCell
PopulateRuntimeCell(StreamingCell, FilteredActors, OutPackagesToGenerate);

int32 LayerCellIndex;
int32* LayerCellIndexPtr = GridLevel.LayerCellsMapping.Find(CellIndex);
if (LayerCellIndexPtr)
{
	LayerCellIndex = *LayerCellIndexPtr;
}
else
{
	LayerCellIndex = GridLevel.LayerCells.AddDefaulted();
	GridLevel.LayerCellsMapping.Add(CellIndex, LayerCellIndex);
}
// GridCells 中存放的是不同 DataLayer 的 StreamingCell
GridLevel.LayerCells[LayerCellIndex].GridCells.Add(StreamingCell);

至此 Runtime 使用的流送数据生成完毕,后续对这些Cells进行距离判断,以Cell为单位加/卸载。

流送Update

  • UWorldPartitionSubsystem::UpdateStreamingState():入口函数
  • 遍历生成的全部StreamingGrid,通过 FSpatialHashStreamingGrid::GetCells() 获得流送范围内的 GridCells
    这里面好几个for循环,详见下面代码注释
// 遍历流送源
for (const FWorldPartitionStreamingSource& Source : Sources)
{
	// 遍历流送源中的多个Shape
	Source.ForEachShape(GridLoadingRange, GridName, /*bProjectIn2D*/ true, [&](const FSphericalSector& Shape)
	{
		FStreamingSourceInfo Info(Source, Shape);
		// 遍历Grid的Level、Level再计算出Shape范围内的Cell
		Helper.ForEachIntersectingCells(Shape, [&](const FGridCellCoord& Coords)
		{
			bool bAddedActivatedCell = false;

#if !UE_BUILD_SHIPPING
			if ((GFilterRuntimeSpatialHashGridLevel == INDEX_NONE) || (GFilterRuntimeSpatialHashGridLevel == Coords.Z))
#endif
			{
				// 遍历格子中的全部Cell(可能存在不同DataLayer)
				ForEachRuntimeCell(Coords, [&](const UWorldPartitionRuntimeCell* Cell)
				{
					bool bIncludeCell = true;
					// Z轴方向的判定,但注意划分的时候没考虑Z值,这块实现不完整
					if (bEnableZCulling)
					{
						const FVector2D CellMinMaxZ(Cell->GetContentBounds().Min.Z, Cell->GetContentBounds().Max.Z);
						bIncludeCell = TRange<double>::Inclusive(CellMinMaxZ.X, CellMinMaxZ.Y).Overlaps(TRange<double>::Inclusive(Shape.GetCenter().Z - Shape.GetRadius(), Shape.GetCenter().Z + Shape.GetRadius()));
					}
					
					if (bIncludeCell)
					{
						……
					}
				});
			}
			if (bAddedActivatedCell)
			{
				AllActivatedCells.FindOrAdd(Coords).Add(Info);
			}
		});
	});
}

bSnapNonAlignedGridLevelsToLowerLevels

该选项开启时,拿着 AllActivatedCells 去获取低一级的Cell,并将这些Cell添加到 OutActivateCells,视作需激活的Cell,这一步断点看了还是不明白,暂时先略过。

FSquare2DGridHelper::ForEachIntersectingCells 的实现

FSquare2DGridHelper::ForEachIntersectingCells()先利用包围盒得到可能相交的Cells,再进行更细致的距离计算得到真正相交的Cells。

  • 利用AABB相交判定进行预筛选是个经典的技巧。
  • FVector2D::Max(CellBounds.Min, FVector2D::Min(SphereCenter, CellBounds.Max)) 这一句得到包围盒到圆心最近的点,好巧妙的方法。
int32 ForEachIntersectingCells(const FSphere& InSphere, TFunctionRef<void(const FGridCellCoord2&)> InOperation) const
{
	int32 NumCells = 0;

	// @todo_ow: rasterize circle instead?
	const FBox Box(InSphere.Center - FVector(InSphere.W), InSphere.Center + FVector(InSphere.W));
	const double SquareDistance = InSphere.W * InSphere.W;
	const FVector2D SphereCenter(InSphere.Center);
	// 这里传入的是圆的包围盒,先利用包围盒快速得到可能相交的Cell,最终调用到ForEachIntersectingCellsBreakable
	ForEachIntersectingCells(Box, [this, SquareDistance, &SphereCenter, &InOperation, &NumCells](const FGridCellCoord2& Coords)
	{
		// No need to check validity of coords as it's already done
		const bool bCheckIsValidCoords = false;
		FBox2D CellBounds;
		GetCellBounds(Coords, CellBounds, bCheckIsValidCoords);
		// 得到CellBounds包围盒内离圆心最近的点的距离
		FVector2D Delta = SphereCenter - FVector2D::Max(CellBounds.Min, FVector2D::Min(SphereCenter, CellBounds.Max));
		// 相交计算
		if ((Delta.X * Delta.X + Delta.Y * Delta.Y) < SquareDistance)
		{
			InOperation(Coords);
			NumCells++;
		}
	});

	return NumCells;
}

EnableZCulling

这部分实现不完全,仅在最终判断CellBounds的时候考虑上Z值,但划分Actors、创建Cell的时候没做处理,没在垂直方向分割出Cell,同一个格子GridCell中的内容,不论Z值相差多少,都会同时被加载。(估计是因为新版的 UWorldPartitionRuntimeHashSet 已经是真正的3D流送了,所以这边实现一半就草草收尾了)

  • Cell->GetContentBounds() 是Cell中全部Actor的包围盒
ForEachRuntimeCell(Coords, [&](const UWorldPartitionRuntimeCell* Cell)
{
	bool bIncludeCell = true;
	// Z轴方向的判定,但注意划分的时候没考虑Z值,这块实现不完整
	if (bEnableZCulling)
	{
		const FVector2D CellMinMaxZ(Cell->GetContentBounds().Min.Z, Cell->GetContentBounds().Max.Z);
		bIncludeCell = TRange<double>::Inclusive(CellMinMaxZ.X, CellMinMaxZ.Y).Overlaps(TRange<double>::Inclusive(Shape.GetCenter().Z - Shape.GetRadius(), Shape.GetCenter().Z + Shape.GetRadius()));
	}
	……
});
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值