본문 바로가기

STUDY/Unreal Engine

[ UE 5 ]Texture Streaming (2)

이전 과정 

2022.12.05 - [STUDY/Unreal Engine] - [ UE 5 ]Texture Streaming (1)

 

[ UE 5 ]Texture Streaming (1)

Texture Streaiming은 게임에서 필요한 텍스쳐를 load/Unload하는 기술입니다. 필요한 텍스쳐만 로드하여 VRAM과 메모리 사용량을 최적화하고, 렌더링 성능을 향상시켜줍니다. UE의 Texture Streaming System은 Ma

agh2o.tistory.com

 


Do Work

Streaming Mip 계산의 핵심부분인 DoWork() 함수를 분석합니다. 

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

카메라가 오브젝트에서 멀어질수록 필요한 밉맵 레이어가 줄어들기 때문에 카메라와 오브젝트 사이의 거리를 알아야 합니다. 

UpdateBoundSizes_Async함수는 View에서 렌더링되는 객체들의 경계 크기를 계산해 줍니다. 이 정보는 이후 뷰포트 크기에 따라 객체들의 렌더링 우선순위를 결정하는 데 사용됩니다. 

StaticInstanceView와 DynamicInstancesView? 

더보기

StaticInstancesViews와 DynamicInstancesView는 Rendering Asset Instance의 두 가지 주요 유형을 나타냅니다.

  1. StaticInstancesViews
    : 정적 렌더링 에셋 인스턴스의 목록을 나타냅니다. 이러한 인스턴스는 게임 레벨에서 변하지 않고 고정되어 있습니다. 일반적으로, 정적 인스턴스는 레벨에 배치된 바닥, 벽, 건물 등과 같은 객체입니다.
  2. DynamicInstancesView
    : 동적 렌더링 에셋 인스턴스를 나타냅니다. 이러한 인스턴스는 게임 플레이 동안 변할 수 있습니다.
    예를 들어, 움직이는 캐릭터, 차량, 물리 기반 오브젝트 등이 여기에 해당됩니다.

InstancesView?
Instance View는 Rendering Asset Instance에 대한 정보를 포함하는 클래스입니다.
각 뷰에는 Rendering Asset의 화면 크기를 계산하는 데 사용되는 데이터와 메서드가 포함되어 있습니다.
이 클래스는 렌더링 에셋의 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

: Rendering Asset Instance View의 경계볼륨 정보를 업데이트해주는 함수입니다. (비동기)

빠른 계산을 위해 SIMD를 사용하여 ViewPoint와 4개의 AABB BoundingBox를 동시에 계산합니다.

▼ Rendering Asset Instance View의 Bounds? 

더보기

Rendering Asset Instance View의 Bounding Volume은 일반적으로 충분한 정확도와 계산 효율성을 따져 AABB(Axis-Aligned Bounding Box), 혹은 Bounding Sphere형태로 표현되며 Transform/Rotate/Scaling정보를 포함합니다.

엔진은 렌더링과정을 더 효율적으로 관리하기 위해 Frustum Culling, LOD결정에 이 Bounding Volume을 사용하며,  충돌 판정에서도 사용합니다. 

FRenderAssetInstanceAsyncView::UpdateBoundSizes_Async 함수를 요약하면 다음과 같습니다.


1. FRenderAssetInstanceView의 Bounds4를 가져와서 렌더링될 대상의 Bounding Volume을 얻습니다.

Bounds4는 4개의 별개 Rendering Asset Instance의 Bounding Volume을 한번에 저장하고 있는 구조체입니다. 이렇게 4개의 인스턴스를 저장하는 이유는 SIMD명령어를 사용하여 연산하기 위함입니다. 


2. 모든 Bound를 돌면서 연산합니다.
2-1) View와 Bound사이의 거리의 제곱값을 얻습니다. (Extent값 제외) 

2-2) 거리의 제곱값을 MinDistanceSq, MinRangeSq ~ MaxRangeSq 로 Clamp해줍니다. 
▼ 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 Size를 계산하여 이를 바탕으로PerfectWantedMip을 결정하는 것입니다.

여러 조건문과 Setting값, FRenderAssetInstanceAsyncView::GetRenderAssetScreenSize 함수를 통해 렌더링 에셋의 최대 크기 관련 값들을 얻습니다.( MaxSize, MaxSize_VisibleOnly ).  이는 렌더링 에셋의 최대 허용 크기(해상도 크기비례), 스트레스 테스트 여부, 완전히 사용된 텍스처 로드, 레벨별 MIP 계산 등에 따라 달라집니다.계산된 최대 크기와 최대 가시 크기를 기반으로 FAsyncRenderAssetStreamingData::GetRenderAssetScreenSiz 함수를 호출하여 원하는 MIP 레벨을 설정합니다.

{

         DynamicInstancesView.GetRenderAssetScreenSize

         StaticInstancesView.GetRenderAssetScreenSize

         StreamingRenderAsset.SetPerfectWantedMips_Async

}

FRenderAssetInstanceAsyncView::GetRenderAssetScreenSize
FRenderAssetInstanceAsyncView::ProcessElement
FStreamingRenderAsset::SetPerfectWantedMips_Async

렌더링 Element의 screensize와 관련된 정보를 계산하는 함수입니다. 

 

* Screen size는 이 에셋과 겹치는 화면 픽셀 수입니다

MaxSize : 렌더링될 element의 최대 크기( 화면 해상도 대비)
MaxSize_VisibleOnly : 렌더링 element의 가장 큰 Screensize (화면에 실제로 보이는부분에서)
 MaxForcedNumLODs: 강제로 존재해야만 하는 최대 LOD의 개수입니다. 메시에만 사용됩니다. 

 

함수 내용 : ProcessElement 함수를 통해 렌더링 Element의 크기 정보를 계산합니다. ProcessElement 함수는 BoundsViewInfo 배열에서 BoundsIndex 값을 사용하여 element의 경계 상자 정보를 가져옵니다. 경계 상자 정보를 사용하여 렌더링 element의 스크린 크기를 계산하고, 결과를 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에서는 위에서 구한 BoundsVieWInfo.MaxNormalizedSize( ScreenSize / DistanceMinusRadius) 와, BoundsVieWInfo.MaxNormalizedSize_VisibleOnly에 각각 TexelFactor를 구해 MaxSize를 구합니다. 

            MaxSize = FMath::Max(MaxSize, TexelFactor * BoundsVieWInfo.MaxNormalizedSize);
            MaxSize_VisibleOnly = FMath::Max(MaxSize_VisibleOnly, TexelFactor * BoundsVieWInfo.MaxNormalizedSize_VisibleOnly);

FStreamingRenderAsset::SetPerfectWantedMips_Async 함수에서 VisibleWantedMips , HiddenWantedMips을 계산해 주는데, GetWantedMipsFromSize함수를 사용합니다.

위에서 구한 MaxSize를 사용하여 Mip계산을 하기도 하지만, MinAllowedMips, MaxAllowedMips 함수에서GetWantedMipsFromSize 내부 계산하는 부분을 보면, 리턴할 때 Clamp를 해줍니다. FMath::Clamp<int32>(WantedMipsInt, MinAllowedMips, MaxAllowedMips);

그래서 View에서 보이지 않는 Element라 하더라도 MinAllowedMips를 통해 VisibleWantedMips와 HiddenWantedMips 에 값이 채워지게 됩니다. 

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. StreamingRederAsset을 돌면서 StreamingRenderAsset.UpdateRetentionPriority_Async를 호출합니다.

 

텍스쳐 그룹, 크기, 렌더링여부에 대해 Priority를 구한 후, PerpectWantedMip에 대한 Budget값을 리턴해줍니다.

 

 

더보기
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
}

'STUDY > Unreal Engine' 카테고리의 다른 글

[ UE 5 ]Texture Streaming (1)  (0) 2022.12.05