解析下面这段代码,给出代码结构图和流程图
做工作
我们分析一下DoWork()函数,它是Streaming Mip计算的核心部分。
void FRenderAssetStreamingMipCalcTask::DoWork()
{
// 비동기 작업이 실행되는 동안 스트리밍 렌더 에셋은 재할당되지 않도록 보장됩니다.
// 두 가지 일이 발생할 수 있습니다:
// 1. 텍스처가 제거될 수 있으며,
// 2. 이 경우 텍스처가 null이 되거나 UpdateDynamicData()를 호출한 후 일부 멤버가 업데이트될 수 있습니다.
// 모든 Streaming Mesh, Texture object 가져오기.
TArray<FStreamingRenderAsset>& StreamingRenderAssets = StreamingManager.AsyncUnsafeStreamingRenderAssets;
const FRenderAssetStreamingSettings& Settings = StreamingManager.Settings;
//
StreamingData.ComputeViewInfoExtras(Settings);
// Update the distance and size for each bounds.
StreamingData.UpdateBoundSizes_Async(Settings);
ApplyPakStateChanges_Async();
for (FStreamingRenderAsset& StreamingRenderAsset : StreamingRenderAssets)
{
if (IsAborted()) break;
StreamingRenderAsset.UpdateOptionalMipsState_Async();
StreamingData.UpdatePerfectWantedMips_Async(StreamingRenderAsset, Settings);
StreamingRenderAsset.DynamicBoostFactor = 1.f; // Reset after every computation.
}
// According to budget, make relevant sacrifices and keep possible unwanted mips
UpdateBudgetedMips_Async();
// Update load requests.
UpdateLoadAndCancelationRequests_Async();
// Update bHasStreamingUpdatePending
UpdatePendingStreamingStatus_Async();
StreamingData.OnTaskDone_Async();
#if STATS
UpdateStats_Async();
#elif UE_BUILD_TEST
UpdateCSVOnlyStats_Async();
#endif // STATS
}
FAsyncRenderAssetStreamingData::UpdateBoundSizes_Async
我们需要知道相机和物体之间的距离,因为相机离物体越远,所需的 mipmap 层就越少。
UpdateBoundSizes_Async 函数计算视图中渲染对象的边界大小。然后,该信息将根据视口大小确定对象的渲染优先级。
▼ StaticInstanceView 和 DynamicInstanceView?
关闭
StaticInstancesViews 和 DynamicInstancesViews 代表两种主要类型的渲染资产实例。
StaticInstancesViews
:表示静态渲染资源实例列表。这些实例是固定的,在游戏关卡中不会发生变化。通常,静态实例是放置在关卡中的对象,例如地板、墙壁、建筑物等。
DynamicInstancesView
:表示动态渲染资源实例。这些实例可以在游戏过程中发生变化。
例如,移动的角色、车辆、基于物理的物体等。
InstancesView?
实例视图是一个包含渲染资产实例信息的类。
每个视图都包含用于计算渲染资产屏幕尺寸的数据和方法。
此类提供的信息有助于确定渲染资产的细节级别 (LOD)。每个实例视图都存储对特定渲染资产的引用,提供计算该渲染资产屏幕尺寸的函数,确定是否存在强制 LOD 等。此信息用于确定渲染资产所需的 MIP 级别。
▼功能齐全
void FAsyncRenderAssetStreamingData::UpdateBoundSizes_Async(const FRenderAssetStreamingSettings& Settings)
{
for (int32 StaticViewIndex = 0; StaticViewIndex < StaticInstancesViews.Num(); ++StaticViewIndex)
{
FRenderAssetInstanceAsyncView& StaticInstancesView = StaticInstancesViews[StaticViewIndex];
StaticInstancesView.UpdateBoundSizes_Async(ViewInfos, ViewInfoExtras, LastUpdateTime, Settings);
// Skip levels that can not contribute to resolution.
if (StaticInstancesView.GetMaxLevelRenderAssetScreenSize() > Settings.MinLevelRenderAssetScreenSize
|| StaticInstancesView.HasAnyComponentWithForcedLOD())
{
StaticInstancesViewIndices.Add(StaticViewIndex);
}
else
{
CulledStaticInstancesViewIndices.Add(StaticViewIndex);
}
}
// Sort by max possible size, this allows early exit when iteration on many levels.
if (Settings.MinLevelRenderAssetScreenSize > 0)
{
StaticInstancesViewIndices.Sort([&](int32 LHS, int32 RHS) { return StaticInstancesViews[LHS].GetMaxLevelRenderAssetScreenSize() > StaticInstancesViews[RHS].GetMaxLevelRenderAssetScreenSize(); });
}
DynamicInstancesView.UpdateBoundSizes_Async(ViewInfos, ViewInfoExtras, LastUpdateTime, Settings);
}
FRenderAssetInstanceAsyncView::UpdateBoundSizes_Async
:该函数更新渲染资源实例视图的边界体积信息。(异步)
为了快速计算,我们使用 SIMD 同时计算 ViewPoint 和四个 AABB BoundingBox。
▼ 渲染资产实例视图中的边界?
关闭
渲染资源实例视图的边界体积,考虑到足够的精度和计算效率,一般以AABB(Axis-Aligned Bounding Box)或者Bounding Sphere的形式来表示,并且包含Transform/Rotate/Scaling信息。
引擎使用此边界体积进行视锥体剔除、LOD 确定和碰撞检测,以更有效地管理渲染过程。
总结一下,FRenderAssetInstanceAsyncView::UpdateBoundSizes_Async函数如下:
1. 获取 FRenderAssetInstanceView 的 Bounds4,以获取待渲染对象的边界体积 (Bounding Volume)。Bounds4
是一个结构体,用于一次性存储四个独立渲染资源实例 (Rendering Asset Instance) 的边界体积 (Bounding Volume)。这样存储四个实例的原因是为了使用 SIMD 指令执行操作。2
. 遍历所有 Bounds 并执行操作。2-1
) 获取视图 (View) 和边界 (Bound) 之间距离的平方。(不包括 Extent 值)
2-2) 将距离的平方限制在 MinDistanceSq、MinRangeSq ~ MaxRangeSq 范围内。
▼ 检查 MinRangeSq、MaxRangeSq 值的含义
2-3) 求相应值的倒数平方根,然后将其乘以 ScreenSize。
------------> 则 ScreenSizeOverDistance = ScreenSize / Sqrt(DistSqMinusRadiusSq),
该值会是 ScreenSizeOverDistance = ScreenSize / DistanceMinusRadius。
随着距离的增加,它会变小;随着 ScreenSize 的增加,它会变大,因此可以方便地用于 Mip 计算。2-4
)
ViewMaxNormalizedSize 计算所有流的最大标准化大小。
计算出来的ScreenSizeOverDistance放在 BoundsViewInfo[Index] .MaxNormalizedSize中。
处理完物体不在边界内或者不是最近渲染的物体的情况后,将其放入BoundsVieWInfo[Index].MaxNormalizedSize_VisibleOnly中。
2-5)
当Bound Loop完成后,将所有Rendering Asset Instance中最大的一个乘以MaxTexelFactor。
将其添加到MaxLevelRenderAssetScreenSize后,退出。
3.将计算出来的值插入到你传入的BoundsViewInfo中。
▼开启所有功能
关闭
void FRenderAssetInstanceAsyncView::UpdateBoundSizes_Async(
const TArray<FStreamingViewInfo>& ViewInfos,
const FStreamingViewInfoExtraArray& ViewInfoExtras,
float LastUpdateTime,
const FRenderAssetStreamingSettings& Settings)
{
check(ViewInfos.Num() == ViewInfoExtras.Num());
if (!View.IsValid()) return;
const int32 NumViews = ViewInfos.Num();
const int32 NumBounds4 = View->NumBounds4();
const VectorRegister LastUpdateTime4 = VectorSet(LastUpdateTime, LastUpdateTime, LastUpdateTime, LastUpdateTime);
BoundsViewInfo.Empty(NumBounds4 * 4);
BoundsViewInfo.AddUninitialized(NumBounds4 * 4);
// 모든 element의 최대 nomalize된 크기
VectorRegister ViewMaxNormalizedSize = VectorZero();
for (int32 Bounds4Index = 0; Bounds4Index < NumBounds4; ++Bounds4Index)
{
const FRenderAssetInstanceView::FBounds4& CurrentBounds4 = View->GetBounds4(Bounds4Index);
// LWC_TODO - 원점 값은 double에서 로드하고 나머지 값은 float에서 로드합니다.
// 이러한 연산 중 일부를 float VectorRegisters로 수행할 수 있으며, 이는 잠재적으로 더 효율적일 수 있습니다.
// (그렇지 않으면 로드 시 이 값들을 더블 VectorRegisters로 변환하는 데 비용이 듭니다.)
// 대형 월드의 경우 오브젝트와 뷰 원점 사이의 거리가 float 용량을 초과할 수 있으므로 정밀도 관리가 까다롭습니다.
// viewer에서 bounding sphere 까지의 거리를 계산합니다.
const VectorRegister OriginX = VectorLoadAligned( &CurrentBounds4.OriginX );
const VectorRegister OriginY = VectorLoadAligned( &CurrentBounds4.OriginY );
const VectorRegister OriginZ = VectorLoadAligned( &CurrentBounds4.OriginZ );
const VectorRegister RangeOriginX = VectorLoadAligned( &CurrentBounds4.RangeOriginX );
const VectorRegister RangeOriginY = VectorLoadAligned( &CurrentBounds4.RangeOriginY );
const VectorRegister RangeOriginZ = VectorLoadAligned( &CurrentBounds4.RangeOriginZ );
const VectorRegister ExtentX = VectorLoadAligned( &CurrentBounds4.ExtentX );
const VectorRegister ExtentY = VectorLoadAligned( &CurrentBounds4.ExtentY );
const VectorRegister ExtentZ = VectorLoadAligned( &CurrentBounds4.ExtentZ );
const VectorRegister ComponentScale = VectorLoadAligned( &CurrentBounds4.RadiusOrComponentScale );
const VectorRegister PackedRelativeBox = VectorLoadAligned( reinterpret_cast<const FVector4f*>(&CurrentBounds4.PackedRelativeBox) );
const VectorRegister MinDistanceSq = VectorLoadAligned( &CurrentBounds4.MinDistanceSq );
const VectorRegister MinRangeSq = VectorLoadAligned( &CurrentBounds4.MinRangeSq );
const VectorRegister MaxRangeSq = VectorLoadAligned(&CurrentBounds4.MaxRangeSq);
const VectorRegister LastRenderTime = VectorLoadAligned(&CurrentBounds4.LastRenderTime);
VectorRegister MaxNormalizedSize = VectorZero();
VectorRegister MaxNormalizedSize_VisibleOnly = VectorZero();
for (int32 ViewIndex = 0; ViewIndex < NumViews; ++ViewIndex)
{
const FStreamingViewInfo& ViewInfo = ViewInfos[ViewIndex];
const FStreamingViewInfoExtra& ViewInfoExtra = ViewInfoExtras[ViewIndex];
const VectorRegister ScreenSize = VectorLoadFloat1( &ViewInfoExtra.ScreenSizeFloat );
const VectorRegister ExtraBoostForVisiblePrimitive = VectorLoadFloat1( &ViewInfoExtra.ExtraBoostForVisiblePrimitiveFloat );
const VectorRegister ViewOriginX = VectorLoadFloat1( &ViewInfo.ViewOrigin.X );
const VectorRegister ViewOriginY = VectorLoadFloat1( &ViewInfo.ViewOrigin.Y );
const VectorRegister ViewOriginZ = VectorLoadFloat1( &ViewInfo.ViewOrigin.Z );
VectorRegister DistSqMinusRadiusSq = VectorZero();
if (Settings.bUseNewMetrics)
{
// Settings.bUseNewMetrics가 True인 경우는 Extent 값은 제외하고 거리를 계산해줍니다.
// = 바운딩박스 크기를 고려해서 계산한다는뜻 ~
// ViewOrigin으로부터 Box까지의 거리를 계산하는것입니다.
VectorRegister Temp = VectorSubtract( ViewOriginX, OriginX );
Temp = VectorAbs( Temp );
VectorRegister BoxRef = VectorMin( Temp, ExtentX );
Temp = VectorSubtract( Temp, BoxRef );
DistSqMinusRadiusSq = VectorMultiply( Temp, Temp );
Temp = VectorSubtract( ViewOriginY, OriginY );
Temp = VectorAbs( Temp );
BoxRef = VectorMin( Temp, ExtentY );
Temp = VectorSubtract( Temp, BoxRef );
DistSqMinusRadiusSq = VectorMultiplyAdd( Temp, Temp, DistSqMinusRadiusSq );
Temp = VectorSubtract( ViewOriginZ, OriginZ );
Temp = VectorAbs( Temp );
BoxRef = VectorMin( Temp, ExtentZ );
Temp = VectorSubtract( Temp, BoxRef );
DistSqMinusRadiusSq = VectorMultiplyAdd( Temp, Temp, DistSqMinusRadiusSq );
}
else
{
// 여기는 바운딩박스 크기를 고려하지 않고 ViewOrigin부터 BOX Origin까지의 거리 계산
VectorRegister Temp = VectorSubtract( ViewOriginX, OriginX );
VectorRegister DistSq = VectorMultiply( Temp, Temp );
Temp = VectorSubtract( ViewOriginY, OriginY );
DistSq = VectorMultiplyAdd( Temp, Temp, DistSq );
Temp = VectorSubtract( ViewOriginZ, OriginZ );
DistSq = VectorMultiplyAdd( Temp, Temp, DistSq );
DistSqMinusRadiusSq = VectorNegateMultiplyAdd( ExtentX, ExtentX, DistSq );
DistSqMinusRadiusSq = VectorNegateMultiplyAdd( ExtentY, ExtentY, DistSq );
DistSqMinusRadiusSq = VectorNegateMultiplyAdd( ExtentZ, ExtentZ, DistSq );
// This can be negative here!!!
}
// bound가 가까이서 보이지 않는 경우 가능한 최소 범위로 거리를 제한합니다.
VectorRegister ClampedDistSq = VectorMax( MinDistanceSq, DistSqMinusRadiusSq );
// FBounds4.Origin는AABB 중심점 값입니다.
// 이 값은 asset의 위치 정보를 저장하는 용도로 사용됩니다.
// FBounds4.RangeOrigin는 AABB의 각 축들의 값 중 가장 작은 값(최소 범위)을 나타냅니다.
VectorRegister InRangeMask;
{
VectorRegister Temp = VectorSubtract( ViewOriginX, RangeOriginX );
VectorRegister RangeDistSq = VectorMultiply( Temp, Temp );
Temp = VectorSubtract( ViewOriginY, RangeOriginY );
RangeDistSq = VectorMultiplyAdd( Temp, Temp, RangeDistSq );
Temp = VectorSubtract( ViewOriginZ, RangeOriginZ );
RangeDistSq = VectorMultiplyAdd( Temp, Temp, RangeDistSq );
VectorRegister ClampedRangeDistSq = VectorMax( MinRangeSq, RangeDistSq );
ClampedRangeDistSq = VectorMin( MaxRangeSq, ClampedRangeDistSq );
InRangeMask = VectorCompareEQ( RangeDistSq, ClampedRangeDistSq); // If the clamp dist is equal, then it was in range.
}
ClampedDistSq = VectorMax(ClampedDistSq, VectorOne()); // Prevents / 0
VectorRegister ScreenSizeOverDistance = VectorReciprocalSqrt(ClampedDistSq);
ScreenSizeOverDistance = VectorMultiply(ScreenSizeOverDistance, ScreenSize);
MaxNormalizedSize = VectorMax(ScreenSizeOverDistance, MaxNormalizedSize);
// 모든 뷰의 최대값을 누적합니다. PackedRelativeBox가 0이면 해당 항목은 유효하지 않으며 최대값에 영향을 미치지 않아야 합니다. const VectorRegister CulledMaxNormalizedSize = VectorSelect(VectorCompareNE(PackedRelativeBox, VectorZero()), MaxNormalizedSize, VectorZero());
ViewMaxNormalizedSize = VectorMax(ViewMaxNormalizedSize, CulledMaxNormalizedSize);
// 범위 내에 있지 않거나 최근에 본 적이 없는 경우 마스크를 0으로 설정합니다.
ScreenSizeOverDistance = VectorMultiply(ScreenSizeOverDistance, ExtraBoostForVisiblePrimitive);
ScreenSizeOverDistance = VectorSelect(InRangeMask, ScreenSizeOverDistance, VectorZero());
ScreenSizeOverDistance = VectorSelect(VectorCompareGT(LastRenderTime, LastUpdateTime4), ScreenSizeOverDistance, VectorZero());
MaxNormalizedSize_VisibleOnly = VectorMax(ScreenSizeOverDistance, MaxNormalizedSize_VisibleOnly);
}
// Store results
FBoundsViewInfo* BoundsVieWInfo = &BoundsViewInfo[Bounds4Index * 4];
MS_ALIGN(16) float MaxNormalizedSizeScalar[4] GCC_ALIGN(16);
VectorStoreAligned(MaxNormalizedSize, MaxNormalizedSizeScalar);
MS_ALIGN(16) float MaxNormalizedSize_VisibleOnlyScalar[4] GCC_ALIGN(16);
VectorStoreAligned(MaxNormalizedSize_VisibleOnly, MaxNormalizedSize_VisibleOnlyScalar);
MS_ALIGN(16) float ComponentScaleScalar[4] GCC_ALIGN(16);
VectorStoreAligned(ComponentScale, ComponentScaleScalar);
for (int32 SubIndex = 0; SubIndex < 4; ++SubIndex)
{
BoundsVieWInfo[SubIndex].MaxNormalizedSize = MaxNormalizedSizeScalar[SubIndex];
BoundsVieWInfo[SubIndex].MaxNormalizedSize_VisibleOnly = MaxNormalizedSize_VisibleOnlyScalar[SubIndex];
BoundsVieWInfo[SubIndex].ComponentScale = ComponentScaleScalar[SubIndex];
}
}
if (Settings.MinLevelRenderAssetScreenSize > 0)
{
float ViewMaxNormalizedSizeResult = VectorGetComponent(ViewMaxNormalizedSize, 0);
MS_ALIGN(16) float ViewMaxNormalizedSizeScalar[4] GCC_ALIGN(16);
VectorStoreAligned(ViewMaxNormalizedSize, ViewMaxNormalizedSizeScalar);
for (int32 SubIndex = 1; SubIndex < 4; ++SubIndex)
{
ViewMaxNormalizedSizeResult = FMath::Max(ViewMaxNormalizedSizeResult, ViewMaxNormalizedSizeScalar[SubIndex]);
}
MaxLevelRenderAssetScreenSize = View->GetMaxTexelFactor() * ViewMaxNormalizedSizeResult;
}
}
FAsyncRenderAssetStreamingData::UpdatePerfectWantedMips_Async
这个函数主要作用是计算Rendering Asset的Max Size、Max Visible Size,并以此确定PerfectWantedMip。
通过几个条件判断和设置值,FRenderAssetInstanceAsyncView::GetRenderAssetScreenSize 函数,我们获取了与渲染资源最大尺寸相关的值(MaxSize、MaxSize_VisibleOnly)。这取决于渲染资源允许的最大尺寸(与分辨率大小成比例)、是否执行压力测试、是否完全使用纹理加载、是否按级别计算 MIP 等。根据计算出的最大尺寸和最大可见尺寸,调用 FAsyncRenderAssetStreamingData::GetRenderAssetScreenSiz 函数设置所需的 MIP 级别。
{
DynamicInstancesView.GetRenderAssetScreenSize
静态实例视图.获取渲染资源屏幕大小
StreamingRenderAsset.SetPerfectWantedMips_Async
}
FRenderAssetInstanceAsyncView::GetRenderAssetScreenSize
FRenderAssetInstanceAsyncView::ProcessElement
FStreamingRenderAsset::SetPerfectWantedMips_Async
计算与渲染元素的屏幕尺寸相关的信息的函数。
* 屏幕尺寸是与该资产重叠的屏幕像素数。
MaxSize:待渲染元素的最大尺寸(相对于屏幕分辨率)。MaxSize_VisibleOnly
:渲染元素的最大屏幕尺寸(在屏幕上实际可见的部分)。MaxForcedNumLODs
:必须强制存在的最大 LOD 数量。仅适用于网格。
函数内容:通过 ProcessElement 函数计算渲染元素的尺寸信息。ProcessElement 函数通过 BoundsViewInfo 数组中的 BoundsIndex 值获取元素的边界框信息。利用边界框信息计算渲染元素的屏幕尺寸,并将结果存储在 MaxSize 和 MaxSize_VisibleOnly 变量中。(仅当渲染元素类型不是纹理时,才会计算强制加载的 LOD 数量。)
▼ 展开 FRenderAssetInstanceAsyncView::GetRenderAssetScreenSize
关闭
void FRenderAssetInstanceAsyncView::GetRenderAssetScreenSize(
EStreamableRenderAssetType AssetType,
const UStreamableRenderAsset* InAsset,
float& MaxSize,
float& MaxSize_VisibleOnly,
int32& MaxNumForcedLODs,
const TCHAR* LogPrefix) const
{
// No need to iterate more if texture is already at maximum resolution.
// Meshes don't really fit into the concept of max resolution but current
// max_res for texture is 8k which is large enough to let mesh screen
// sizes be constrained by this value
int32 CurrCount = 0;
if (View.IsValid())
{
// Use the fast path if available, about twice as fast when there are a lot of elements.
if (View->HasCompiledElements() && !LogPrefix)
{
const TArray<FRenderAssetInstanceView::FCompiledElement>* CompiledElements = View->GetCompiledElements(InAsset);
if (CompiledElements)
{
const int32 NumCompiledElements = CompiledElements->Num();
const FRenderAssetInstanceView::FCompiledElement* CompiledElementData = CompiledElements->GetData();
int32 CompiledElementIndex = 0;
while (CompiledElementIndex < NumCompiledElements && MaxSize_VisibleOnly < MAX_TEXTURE_SIZE)
{
const FRenderAssetInstanceView::FCompiledElement& CompiledElement = CompiledElementData[CompiledElementIndex];
if (ensure(BoundsViewInfo.IsValidIndex(CompiledElement.BoundsIndex)))
{
// Texel factor wasn't available because the component wasn't registered. Lazy initialize it now.
if (AssetType != EStreamableRenderAssetType::Texture
&& CompiledElement.TexelFactor == 0.f
&& ensure(CompiledElement.BoundsIndex < View->NumBounds4() * 4))
{
FRenderAssetInstanceView::FCompiledElement* MutableCompiledElement = const_cast<FRenderAssetInstanceView::FCompiledElement*>(&CompiledElement);
MutableCompiledElement->TexelFactor = View->GetBounds4(CompiledElement.BoundsIndex / 4).RadiusOrComponentScale.Component(CompiledElement.BoundsIndex % 4) * 2.f;
}
ProcessElement(
AssetType,
BoundsViewInfo[CompiledElement.BoundsIndex],
CompiledElement.TexelFactor,
CompiledElement.bForceLoad,
MaxSize,
MaxSize_VisibleOnly,
MaxNumForcedLODs);
}
++CompiledElementIndex;
}
if (MaxSize_VisibleOnly >= MAX_TEXTURE_SIZE && CompiledElementIndex > 1)
{
// This does not realloc anything but moves the closest element at head, making the next update find it immediately and early exit.
FRenderAssetInstanceView::FCompiledElement* SwapElementData = const_cast<FRenderAssetInstanceView::FCompiledElement*>(CompiledElementData);
Swap<FRenderAssetInstanceView::FCompiledElement>(SwapElementData[0], SwapElementData[CompiledElementIndex - 1]);
}
}
}
else
{
int32 IterationCount_DebuggingOnly = 0;
for (auto It = View->GetElementIterator(InAsset); It && (AssetType != EStreamableRenderAssetType::Texture || MaxSize_VisibleOnly < MAX_TEXTURE_SIZE || LogPrefix); ++It, ++IterationCount_DebuggingOnly)
{
View->VerifyElementIdx_DebuggingOnly(It.GetCurElementIdx_ForDebuggingOnly(), IterationCount_DebuggingOnly);
// Only handle elements that are in bounds.
if (ensure(BoundsViewInfo.IsValidIndex(It.GetBoundsIndex())))
{
const FBoundsViewInfo& BoundsVieWInfo = BoundsViewInfo[It.GetBoundsIndex()];
ProcessElement(AssetType, BoundsVieWInfo, AssetType != EStreamableRenderAssetType::Texture ? It.GetTexelFactor() : It.GetTexelFactor() * BoundsVieWInfo.ComponentScale, It.GetForceLoad(), MaxSize, MaxSize_VisibleOnly, MaxNumForcedLODs);
if (LogPrefix)
{
It.OutputToLog(BoundsVieWInfo.MaxNormalizedSize, BoundsVieWInfo.MaxNormalizedSize_VisibleOnly, LogPrefix);
}
}
}
}
}
}
此时HiddenWantedMips和VisibleWantedMips代表了两种不同的加载优先级。
HiddenWantedMips:这决定了当前用户未查看区域的纹理加载优先级。这些区域中的纹理仍然可以加载,但会使用较低分辨率的 MIP 级别进行加载。这用于在保持高帧率的同时节省系统资源。
VisibleWantedMips:这决定了用户当前查看区域的纹理加载优先级。此区域中的纹理将使用更高分辨率的 MIP 级别加载。当系统资源充足时,这可以为用户提供更高质量的纹理。
在 ProcessElement 中,我们通过将 TexelFactor 添加到上面获得的 BoundsVieWInfo.MaxNormalizedSize( ScreenSize / DistanceMinusRadius)和BoundsVieWInfo.MaxNormalizedSize_VisibleOnly 来获得 MaxSize。
MaxSize = FMath::Max(MaxSize,TexelFactor * BoundsVieWInfo.MaxNormalizedSize);
MaxSize_VisibleOnly = FMath::Max(MaxSize_VisibleOnly,TexelFactor * BoundsVieWInfo.MaxNormalizedSize_VisibleOnly);
在FStreamingRenderAsset::SetPerfectWantedMips_Async函数中,使用GetWantedMipsFromSize函数计算VisibleWantedMips和HiddenWantedMips。
虽然我们利用上面获取到的MaxSize 来计算Mip,但是如果查看MinAllowedMips和MaxAllowedMips函数中GetWantedMipsFromSize的内部计算部分, 可以发现我们在返回的时候对其进行了clamp。FMath::Clamp<int32>(WantedMipsInt, MinAllowedMips, MaxAllowedMips);
因此,即使元素在视图中不可见,VisibleWantedMips 和 HiddenWantedMips 也会通过 MinAllowedMips填充值 。VisibleWantedMips
= FMath::Max(GetWantedMipsFromSize(MaxSize_VisibleOnly, InvMaxScreenSizeOverAllViews), NumForcedMips);
HiddenWantedMips = FMath::Max(GetWantedMipsFromSize(MaxSize * Settings.HiddenPrimitiveScale, InvMaxScreenSizeOverAllViews), NumForcedMips);
关闭
FRenderAssetInstanceAsyncView::ProcessElement
void FRenderAssetInstanceAsyncView::ProcessElement(
EStreamableRenderAssetType AssetType,
const FBoundsViewInfo& BoundsVieWInfo,
float TexelFactor,
bool bForceLoad,
float& MaxSize,
float& MaxSize_VisibleOnly,
int32& MaxNumForcedLODs) const
{
if (TexelFactor == FLT_MAX) // 강제로드된 컴포넌트라면
{
MaxSize = BoundsVieWInfo.MaxNormalizedSize > 0 ? FLT_MAX : MaxSize;
MaxSize_VisibleOnly = BoundsVieWInfo.MaxNormalizedSize_VisibleOnly > 0 ? FLT_MAX : MaxSize_VisibleOnly;
}
else if (TexelFactor >= 0)
{
MaxSize = FMath::Max(MaxSize, TexelFactor * BoundsVieWInfo.MaxNormalizedSize);
MaxSize_VisibleOnly = FMath::Max(MaxSize_VisibleOnly, TexelFactor * BoundsVieWInfo.MaxNormalizedSize_VisibleOnly);
// 강제 로드는 즉시 보이는 부분만 로드하고 나중에 전체 텍스처를 로드합니다.
if (bForceLoad && (BoundsVieWInfo.MaxNormalizedSize > 0 || BoundsVieWInfo.MaxNormalizedSize_VisibleOnly > 0))
{
MaxSize = FLT_MAX;
}
}
else // 음수의 Texel Factor는 고정 해상도에 매핑됩니다. 현재 랜드스케이프에 사용됩니다.
{
if (AssetType == EStreamableRenderAssetType::Texture)
{
MaxSize = FMath::Max(MaxSize, -TexelFactor);
MaxSize_VisibleOnly = FMath::Max(MaxSize_VisibleOnly, -TexelFactor);
}
else
{
check(AssetType == EStreamableRenderAssetType::StaticMesh || AssetType == EStreamableRenderAssetType::SkeletalMesh);
check(-TexelFactor <= (float)MAX_MESH_LOD_COUNT);
MaxNumForcedLODs = FMath::Max(MaxNumForcedLODs, static_cast<int32>(-TexelFactor));
}
// Force load will load the immediatly visible part, and later the full texture.
if (bForceLoad && (BoundsVieWInfo.MaxNormalizedSize > 0 || BoundsVieWInfo.MaxNormalizedSize_VisibleOnly > 0))
{
MaxSize = FLT_MAX;
MaxSize_VisibleOnly = FLT_MAX;
}
}
}
FStreamingRenderAsset::SetPerfectWantedMips_Async
void FStreamingRenderAsset::SetPerfectWantedMips_Async(
float MaxSize,
float MaxSize_VisibleOnly,
float MaxScreenSizeOverAllViews,
int32 MaxNumForcedLODs,
bool InLooksLowRes,
const FRenderAssetStreamingSettings& Settings)
{
bForceFullyLoadHeuristic = (MaxSize == FLT_MAX || MaxSize_VisibleOnly == FLT_MAX);
bLooksLowRes = InLooksLowRes; // Things like lightmaps, HLOD and close instances.
NormalizedScreenSize = 0.f;
if (MaxNumForcedLODs >= MaxAllowedMips)
{
VisibleWantedMips = HiddenWantedMips = NumForcedMips = MaxAllowedMips;
NumMissingMips = 0;
return;
}
float InvMaxScreenSizeOverAllViews = 1.f;
if (IsMesh())
{
InvMaxScreenSizeOverAllViews = 1.f / MaxScreenSizeOverAllViews;
NormalizedScreenSize = FMath::Max(MaxSize, MaxSize_VisibleOnly) * InvMaxScreenSizeOverAllViews;
}
NumForcedMips = FMath::Min(MaxNumForcedLODs, MaxAllowedMips);
VisibleWantedMips = FMath::Max(GetWantedMipsFromSize(MaxSize_VisibleOnly, InvMaxScreenSizeOverAllViews), NumForcedMips);
// Terrain, Forced Fully Load and Things that already look bad are not affected by hidden scale.
if (bIsTerrainTexture || bForceFullyLoadHeuristic || bLooksLowRes)
{
HiddenWantedMips = FMath::Max(GetWantedMipsFromSize(MaxSize, InvMaxScreenSizeOverAllViews), NumForcedMips);
NumMissingMips = 0; // No impact for terrains as there are not allowed to drop mips.
}
else
{
HiddenWantedMips = FMath::Max(GetWantedMipsFromSize(MaxSize * Settings.HiddenPrimitiveScale, InvMaxScreenSizeOverAllViews), NumForcedMips);
// NumMissingMips contains the number of mips not loaded because of HiddenPrimitiveScale. When out of budget, those texture will be considered as already sacrificed.
NumMissingMips = FMath::Max<int32>(GetWantedMipsFromSize(MaxSize, InvMaxScreenSizeOverAllViews) - FMath::Max<int32>(VisibleWantedMips, HiddenWantedMips), 0);
}
}
int32 FStreamingRenderAsset::GetWantedMipsFromSize(float Size, float InvMaxScreenSizeOverAllViews) const
{
if (IsTexture())
{
float WantedMipsFloat = 1.0f + FMath::Log2(FMath::Max(1.f, Size));
int32 WantedMipsInt = FMath::CeilToInt(WantedMipsFloat);
return FMath::Clamp<int32>(WantedMipsInt, MinAllowedMips, MaxAllowedMips);
}
FRenderAssetStreamingMipCalcTask::UpdateBudgetedMips_Async
1. 遍历StreamingRenderAsset并调用StreamingRenderAsset.UpdateRetentionPriority_Async。
获取纹理组、大小和渲染的优先级后,它会返回 PerpectWantedMip 的预算值。
关闭
void FRenderAssetStreamingMipCalcTask::UpdateBudgetedMips_Async()
{
//*************************************
// Update Budget
//*************************************
TArray<FStreamingRenderAsset>& StreamingRenderAssets = StreamingManager.AsyncUnsafeStreamingRenderAssets;
const FRenderAssetStreamingSettings& Settings = StreamingManager.Settings;
TArray<int32> PrioritizedRenderAssets;
TArray<int32> PrioritizedMeshes;
int32 NumAssets = 0;
int32 NumMeshes = 0;
int64 MemoryBudgeted = 0;
int64 MeshMemoryBudgeted = 0;
int64 MemoryUsedByNonTextures = 0;
int64 MemoryUsed = 0;
for (FStreamingRenderAsset& StreamingRenderAsset : StreamingRenderAssets)
{
if (IsAborted()) break;
const int64 AssetMemBudgeted = StreamingRenderAsset.UpdateRetentionPriority_Async(Settings.bPrioritizeMeshLODRetention);
const int32 AssetMemUsed = StreamingRenderAsset.GetSize(StreamingRenderAsset.ResidentMips);
MemoryUsed += AssetMemUsed;
if (StreamingRenderAsset.IsTexture())
{
MemoryBudgeted += AssetMemBudgeted;
++NumAssets;
}
else
{
MeshMemoryBudgeted += AssetMemBudgeted;
MemoryUsedByNonTextures += AssetMemUsed;
++NumMeshes;
}
}
//*************************************
// Update Effective Budget
//*************************************
bool bResetMipBias = false;
// 메모리 예산이 필요한 풀 크기보다 크게 감소한 경우,
// 메모리 예산이 크게 줄어든 경우 예산의 임계값을 MemoryBudgeted로 재설정합니다.이 경우 bResetMipBias 변수가 true로 설정되어 mip 바이어스가 재설정됩니다.
if (PerfectWantedMipsBudgetResetThresold - MemoryBudgeted - MeshMemoryBudgeted > TempMemoryBudget + MemoryMargin)
{
// Reset the budget tradeoffs if the required pool size shrinked significantly.
PerfectWantedMipsBudgetResetThresold = MemoryBudgeted;
bResetMipBias = true;
}
// 메모리 예산이 PerfectWantedMipsBudgetResetThresold보다 큰 경우
// 더 높은 요구 사항으로 인해 더 큰 상호작용이 발생하므로 임계값을 MemoryBudgeted + MeshMemoryBudgeted로 늘립니다.
else if (MemoryBudgeted + MeshMemoryBudgeted > PerfectWantedMipsBudgetResetThresold)
{
// Keep increasing the threshold since higher requirements incurs bigger tradeoffs.
PerfectWantedMipsBudgetResetThresold = MemoryBudgeted + MeshMemoryBudgeted;
}
const int64 NonStreamingRenderAssetMemory = AllocatedMemory - MemoryUsed + MemoryUsedByNonTextures;
int64 AvailableMemoryForStreaming = PoolSize - NonStreamingRenderAssetMemory - MemoryMargin;
// If the platform defines a max VRAM usage, check if the pool size must be reduced,
// but also check if it would be safe to some of the NonStreamingRenderAssetMemory from the pool size computation.
// The later helps significantly in low budget settings, where NonStreamingRenderAssetMemory would take too much of the pool.
if (GPoolSizeVRAMPercentage > 0 && TotalGraphicsMemory > 0)
{
const int64 UsableVRAM = FMath::Max<int64>(TotalGraphicsMemory * GPoolSizeVRAMPercentage / 100, TotalGraphicsMemory - Settings.VRAMPercentageClamp * 1024ll * 1024ll);
const int64 UsedVRAM = (int64)GCurrentRendertargetMemorySize * 1024ll + NonStreamingRenderAssetMemory; // Add any other...
const int64 AvailableVRAMForStreaming = FMath::Min<int64>(UsableVRAM - UsedVRAM - MemoryMargin, PoolSize);
if (Settings.bLimitPoolSizeToVRAM || AvailableVRAMForStreaming > AvailableMemoryForStreaming)
{
AvailableMemoryForStreaming = AvailableVRAMForStreaming;
}
}
// Update EffectiveStreamingPoolSize, trying to stabilize it independently of temp memory, allocator overhead and non-streaming resources normal variation.
// It's hard to know how much temp memory and allocator overhead is actually in AllocatedMemorySize as it is platform specific.
// We handle it by not using all memory available. If temp memory and memory margin values are effectively bigger than the actual used values, the pool will stabilize.
if (AvailableMemoryForStreaming < MemoryBudget)
{
// Reduce size immediately to avoid taking more memory.
MemoryBudget = FMath::Max<int64>(AvailableMemoryForStreaming, 0);
}
else if (AvailableMemoryForStreaming - MemoryBudget > TempMemoryBudget + MemoryMargin)
{
// Increase size considering that the variation does not come from temp memory or allocator overhead (or other recurring cause).
// It's unclear how much temp memory is actually in there, but the value will decrease if temp memory increases.
MemoryBudget = AvailableMemoryForStreaming;
bResetMipBias = true;
}
const int64 PrevMeshMemoryBudget = MeshMemoryBudget;
MeshMemoryBudget = Settings.MeshPoolSize * 1024 * 1024;
const bool bUseSeparatePoolForMeshes = MeshMemoryBudget >= 0;
if (!bUseSeparatePoolForMeshes)
{
NumAssets += NumMeshes;
NumMeshes = 0;
MemoryBudgeted += MeshMemoryBudgeted;
MeshMemoryBudgeted = 0;
}
else if (PrevMeshMemoryBudget < MeshMemoryBudget)
{
bResetMipBias = true;
}
//*******************************************
// Reset per mip bias if not required anymore.
//*******************************************
// When using mip per texture/mesh, the BudgetMipBias gets reset when the required resolution does not get affected anymore by the BudgetMipBias.
// This allows texture/mesh to reset their bias when the viewpoint gets far enough, or the primitive is not visible anymore.
if (Settings.bUsePerTextureBias)
{
for (FStreamingRenderAsset& StreamingRenderAsset : StreamingRenderAssets)
{
if (IsAborted()) break;
if (StreamingRenderAsset.BudgetMipBias > 0
&& (bResetMipBias
|| FMath::Max<int32>(
StreamingRenderAsset.VisibleWantedMips,
StreamingRenderAsset.HiddenWantedMips + StreamingRenderAsset.NumMissingMips) < StreamingRenderAsset.MaxAllowedMips))
{
StreamingRenderAsset.BudgetMipBias = 0;
}
}
}
//*************************************
// Drop Mips
//*************************************
// If the budget is taking too much, drop some mips.
if ((MemoryBudgeted > MemoryBudget || (bUseSeparatePoolForMeshes && MeshMemoryBudgeted > MeshMemoryBudget)) && !IsAborted())
{
//*************************************
// Get texture/mesh list in order of reduction
//*************************************
PrioritizedRenderAssets.Empty(NumAssets);
PrioritizedMeshes.Empty(NumMeshes);
for (int32 AssetIndex = 0; AssetIndex < StreamingRenderAssets.Num() && !IsAborted(); ++AssetIndex)
{
FStreamingRenderAsset& StreamingRenderAsset = StreamingRenderAssets[AssetIndex];
// Only consider non deleted textures/meshes (can change any time).
if (!StreamingRenderAsset.RenderAsset) continue;
// Ignore textures/meshes for which we are not allowed to reduce resolution.
if (!StreamingRenderAsset.IsMaxResolutionAffectedByGlobalBias()) continue;
// Ignore texture/mesh that can't drop any mips
const int32 MinAllowedMips = FMath::Max(StreamingRenderAsset.MinAllowedMips, StreamingRenderAsset.NumForcedMips);
if (StreamingRenderAsset.BudgetedMips > MinAllowedMips)
{
if (bUseSeparatePoolForMeshes && StreamingRenderAsset.IsMesh())
{
PrioritizedMeshes.Add(AssetIndex);
}
else
{
PrioritizedRenderAssets.Add(AssetIndex);
}
}
}
// Sort texture/mesh, having those that should be dropped first.
PrioritizedRenderAssets.Sort(FCompareRenderAssetByRetentionPriority(StreamingRenderAssets));
PrioritizedMeshes.Sort(FCompareRenderAssetByRetentionPriority(StreamingRenderAssets));
if (Settings.bUsePerTextureBias && AllowPerRenderAssetMipBiasChanges())
{
//*************************************
// Drop Max Resolution until in budget.
//*************************************
TryDropMaxResolutions(PrioritizedRenderAssets, MemoryBudgeted, MemoryBudget);
if (bUseSeparatePoolForMeshes)
{
TryDropMaxResolutions(PrioritizedMeshes, MeshMemoryBudgeted, MeshMemoryBudget);
}
}
//*************************************
// Drop WantedMip until in budget.
//*************************************
TryDropMips(PrioritizedRenderAssets, MemoryBudgeted, MemoryBudget);
if (bUseSeparatePoolForMeshes)
{
TryDropMips(PrioritizedMeshes, MeshMemoryBudgeted, MeshMemoryBudget);
}
}
//*************************************
// Keep Mips
//*************************************
// If there is some room left, try to keep as much as long as it won't bust budget.
// This will run even after sacrificing to fit in budget since some small unwanted mips could still be kept.
if ((MemoryBudgeted < MemoryBudget || (bUseSeparatePoolForMeshes && MeshMemoryBudgeted < MeshMemoryBudget)) && !IsAborted())
{
PrioritizedRenderAssets.Empty(NumAssets);
PrioritizedMeshes.Empty(NumMeshes);
const int64 MaxAllowedDelta = MemoryBudget - MemoryBudgeted;
const int64 MaxAllowedMeshDelta = MeshMemoryBudget - MeshMemoryBudgeted;
for (int32 AssetIndex = 0; AssetIndex < StreamingRenderAssets.Num() && !IsAborted(); ++AssetIndex)
{
FStreamingRenderAsset& StreamingRenderAsset = StreamingRenderAssets[AssetIndex];
// Only consider non deleted textures/meshes (can change any time).
if (!StreamingRenderAsset.RenderAsset) continue;
// Only consider textures/meshes that won't bust budget nor generate new I/O requests
if (StreamingRenderAsset.BudgetedMips < StreamingRenderAsset.ResidentMips)
{
const int32 Delta = StreamingRenderAsset.GetSize(StreamingRenderAsset.BudgetedMips + 1) - StreamingRenderAsset.GetSize(StreamingRenderAsset.BudgetedMips);
const bool bUseMeshVariant = bUseSeparatePoolForMeshes && StreamingRenderAsset.IsMesh();
const int64 MaxDelta = bUseMeshVariant ? MaxAllowedMeshDelta : MaxAllowedDelta;
TArray<int32>& AssetIndcies = bUseMeshVariant ? PrioritizedMeshes : PrioritizedRenderAssets;
if (Delta <= MaxDelta)
{
AssetIndcies.Add(AssetIndex);
}
}
}
// Sort texture/mesh, having those that should be dropped first.
PrioritizedRenderAssets.Sort(FCompareRenderAssetByRetentionPriority(StreamingRenderAssets));
PrioritizedMeshes.Sort(FCompareRenderAssetByRetentionPriority(StreamingRenderAssets));
TryKeepMips(PrioritizedRenderAssets, MemoryBudgeted, MemoryBudget);
if (bUseSeparatePoolForMeshes)
{
TryKeepMips(PrioritizedMeshes, MeshMemoryBudgeted, MeshMemoryBudget);
}
}
//*************************************
// Handle drop mips debug option
//*************************************
#if !UE_BUILD_SHIPPING
if (Settings.DropMips > 0)
{
for (FStreamingRenderAsset& StreamingRenderAsset : StreamingRenderAssets)
{
if (IsAborted()) break;
if (Settings.DropMips == 1)
{
StreamingRenderAsset.BudgetedMips = FMath::Min<int32>(StreamingRenderAsset.BudgetedMips, StreamingRenderAsset.GetPerfectWantedMips());
}
else
{
StreamingRenderAsset.BudgetedMips = FMath::Min<int32>(StreamingRenderAsset.BudgetedMips, StreamingRenderAsset.VisibleWantedMips);
}
}
}
#endif
}