[UMG] Retainer Box

suyoung·2025년 12월 5일

UE5

목록 보기
10/12

Retainer Box

  • 핵심 메커니즘 : 자식 위젯을 화면에 직접 렌더링하는 대신, 중간 단계인 렌더 타켓이라는 텍스처에 렌더링한 다음, 그 렌더 타겟을 최종적으로 화면에 표시하는 것이다. ⇒ 리테이너 박스에 의해 감싸져 있는 위젯은 모든 프레임보다 느리게 다시 그리도록 만들 때 사용 ⇒ 리테이너 박스는 한 장의 텍스처처럼 만들어서 그리는 박스라는 개념에 가까움.
  1. 성능 최적화 및 캐싱
    1. 독립적인 렌더링 주기 제어 : Retainer Box는 Phase와 PhaseCount 설정을 통해 자식 위젯의 렌더링 빈도와 시작 시점을 메인 게임 렌더링 루프와 독립적으로 제어할 수 있다.
      1. 예시 : 게임이 초당 60프레임으로 렌더링되더라도, 변경이 거의 없는 UI 요소를 Retainer Box로 감싸고, PhaseCount를 2로 설정하면 해당 UI요소는 매 프레임이 아닌 격 프레임(30Hz)마다 한 번만 다시 그려짐.
    2. 캐싱 효과 : 자식 위젯이 렌더 타켓에 한번 그려지면, 다음 렌더링 주기가 돌아올 때까지는 해당 텍스처(렌더 타켓)을 단순히 재사용한다. 이는 매우 복잡하지만 자주 변하지 않는 UI 부분의 CPU 및 Draw Call 부하를 크게 줄여 성능을 최적화하는데 도움을 준다.
  2. 렌더 타켓에 Material 적용 (Post Process)
    1. 간단한 Post Processing 효과 : 자식 위젯들이 렌더 타켓에 그려진 후에, 이 텍스처를 입력으로 받아 자동으로 커스텀 재질을 적용할 수 있음.
    2. 다양한 시각 효과 구현 : 이를 통해 블러, 색상 조정, 왜곡, 외곽선 등과 같은 다양한 포스트 프로세싱 효과를 해당 UI 영역에 쉽게 적용할 수 있음.
  3. 위젯 계층 구조적 특징
    1. Retainer Box는 다른 컨테이너 위젯과 마찬가지로 하나의 자식 위젯을 가짐.
    2. 복잡한 UI를 최적화하려면, 여러 자식 위젯들을 하나의 다른 컨테이너(예: Vertical Box/Border 등)로 묶은 다음 그 컨테이너를 Retainer Box의 자식으로 사용해야함.
  • URetainerBox 변수
	UE_DEPRECATED(5.2, "Direct access to Phase is deprecated. Please use the getter. Note that this property is only set at construction and is not modifiable at runtime.")
	/**
	 * 이 Retainer 위젯이 어느 Phase에서 그려질지를 나타낸다.
	 *
	 * Phase = 0, PhaseCount = 1이면,
	 * 매 프레임마다 새로 그려진다.
	 * Phase = 0, PhaseCount = 2라면,
	 * 2 프레임 중 1프레임만 새로 그린다.
	 * 즉, 60Hz 게임이라면, UI는 30Hz로 렌더링된다.
	 */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Getter, Category="Render Rules", meta=(UIMin=0, ClampMin=0))
	int32 Phase; // 어느 프레임에서 UI를 다시 그릴지를 결정하는 인덱스

	UE_DEPRECATED(5.2, "Direct access to PhaseCount is deprecated. Please use the getter. Note that this property is only set at construction and is not modifiable at runtime.")
	/**
	 * Phase Count는 총 몇 개의 Phase로 나누어 그릴지를 결정한다.
	 * 즉, 현재 프레임 번호를 PhaseCount로 나눈 나머지를 통해,
	 * 이 프레임에 위젯을 새로 그릴지 말지를 판단한다.
	 * 만약 Phase가 0이고 PhaseCount = 1이라면, 매 프레임 마다 위젯을 새로 그린다.
	 * 만약 Phase가 0이고 PhaseCount = 2라면, 2프레임 중 1프레임만 새로 그림을 그림.
	 * 즉, 60프레임 게임이라면, UI는 30프레임으로 그려진다.
	 */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Getter, Category="Render Rules", meta=(UIMin=1, ClampMin=1))
	int32 PhaseCount; // 전체 주기(몇 프레임마다 한 번?)

	/**
	* render target에 선택적으로 적용할 효과(Material)
	* @TextureParameter 에 설정된 이름을 기반으로,
	* 우리가 이 Material에 사용할 Texture Sampler를 설정하게 된다.
	*
	* 최종 이미지의 투명도를 조절하고 싶다면,
	* Material의 Blend Mode를 반드시 AlphaComposite(Pre-Multiplied Alpha)로 설정해야 한다.
	*
	* 또한, 표면에 적용할 알파 값을
	* 렌더 타겟의 색과 알파 둘 다에 곱해줘야만
	* 예상한 색이 제대로 보인다
	*/
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Getter = "GetEffectMaterialInterface", Setter, BlueprintSetter = "SetEffectMaterial", Category = "Effect")
	TObjectPtr<UMaterialInterface> EffectMaterial
		
	// Slate 레벨의 Retainer Widget : Slate 위젯 인스턴스를 관리하는 스마트 포인터
	// SWidget 을 기반으로 하는 Slate 위젯 중 하나
	// Slate는 모든 위젯을 TSharedPtr<SWidget> 형태로 관리
	// 따라서, 이건 Slate 렌더 트리에 존재하는 실제 SlateWidget 포인터
	TSharedPtr<class SRetainerWidget> MyRetainerWidget;
  • 실제 렌더링 부분 - SRetainerWidget
    • OnPaint → 그릴지 말지를 결정하는 최종 분기 (최적화 필터)
      • Grid 를 추가하고 관리하는 이유는 화면에 인터랙션의 유무를 결정하기 위함.
      • MakeBox 함수 내부에서 각 위젯의 슬레이트 정보를 그림을 그려냄.
        • -> MakeBoxInternal 호출
    • PaintRetainedContentImpl → 언제 다시 렌더할지를 결정하는 내부 정책
// int32 LayerID 를 리턴
int32 SRetainerWidget::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const
{
	STAT(FScopeCycleCounter PaintCycleCounter(MyStatId););

	// 수정 가능한 상태 -> Mutable
	SRetainerWidget* MutableThis = const_cast<SRetainerWidget*>(this);

	// true로 설정되면, 사용자 인터페이스나 그래픽 요소가 Retained Mode 로 렌더링될 가능성 존재
	bool bShouldRetainRendering = bEnableRetainedRendering;

#if WITH_EDITOR
	// 에디터 모드인지 확인
	bool bShouldSkipDesignerRendering = bIsDesignTime && (!bShowEffectsInDesigner || !GEnableDesignerRetainedRendering);
	bShouldRetainRendering = bEnableRetainedRendering && !bShouldSkipDesignerRendering;
#endif // WITH_EDITOR

	// 현재 리테인 박스 내부에 있는 위젯을 렌더링할 수 있고
	// 위젯의 상태가 유효하고, 보여지는 상태라면
	if (bShouldRetainRendering && IsAnythingVisibleToRender())
	{
		SCOPE_CYCLE_COUNTER(STAT_SlateRetainerWidgetPaint);
		
		// 새로 설정(히트테스트)
		// 상위 객체(Root)의 히트 테스트 그리드(hit test grid) 설정을 복사해야 한다는 것
		// 히트 테스트 그리그 영역을 새로 설정함. 이게 새로 설정되면 모두 지워짐. 성공 시 true
		const bool bHittestCleared = HittestGrid->SetHittestArea(Args.RootGrid.GetGridOrigin(), Args.RootGrid.GetGridSize(), Args.RootGrid.GetGridWindowOrigin());
		if (bHittestCleared)
		{
			// 새로 히트 테스트 영역을 포함해서 지오메트리와 그리드에 대해서 렌더러 되길 요청
			MutableThis->RequestRender();
		}
		//HittestGrid 는 입력 이벤트용 공간 분할 구조
		HittestGrid->SetOwner(this); 
		HittestGrid->SetCullingRect(MyCullingRect);

		// 새로운 히트 테스트임을 설정
		// const 인데 수정이 가능한 이유가 MutableThis여서?
		FPaintArgs NewArgs = Args.WithNewHitTestGrid(HittestGrid.Get());

		// 중첩된 히트 테스트 그리드는 부모의 사용자 ID를 상속받아야 하므로,
		// 현재 사용자 인덱스를 새 그리드에 복사한다.
		NewArgs.GetHittestGrid().SetUserIndex(Args.RootGrid.GetUserIndex()); //부모 ID 복사

	
		FSlateInvalidationContext Context(OutDrawElements, InWidgetStyle);
		Context.bParentEnabled = bParentEnabled;
		Context.bAllowFastPathUpdate = true;
		Context.LayoutScaleMultiplier = GetPrepassLayoutScaleMultiplier();
		Context.PaintArgs = &NewArgs;
		Context.IncomingLayerId = LayerId;
		Context.CullingRect = MyCullingRect;

		// 실제 리테인 컨테이너 박스에 대해 다시 그리고, 그에 대한 결과를 받는 부분
		EPaintRetainedContentResult PaintResult = MutableThis->PaintRetainedContentImpl(Context, AllottedGeometry, LayerId);

#if WITH_SLATE_DEBUGGING
		if (PaintResult == EPaintRetainedContentResult::NotPainted
			|| PaintResult == EPaintRetainedContentResult::TextureSizeZero
			|| PaintResult == EPaintRetainedContentResult::TextureSizeTooBig)
		{
			MutableThis->SetLastPaintType(ESlateInvalidationPaintType::None);
		}
#endif
		// TextureSizeTooBig : 텍스처의 크기가 매우 큼
		// TextureSizeZero : 텍스처 사이즈가 0
		if (PaintResult == EPaintRetainedContentResult::TextureSizeTooBig)
		{
			// Retainer Box가 Render Target의 크기를 만들거나 크기 조정을 하려고 했으나,
			// 요청한 크기가 너무 커서 실행한 경우로, Retainer 방식으로 그릴 수 없어서
			// 일반 위젯처럼 바로 그려버리는 방법이다.
			return SCompoundWidget::OnPaint(Args, AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled);
		}
		else if (PaintResult == EPaintRetainedContentResult::TextureSizeZero)
		{
			return GetCachedMaxLayerId(); // 캐싱해둔(이전에 계산된) 최대 레이어 ID 값을 반환
		}
		else
		{
			// NoPainted/Painted/Queue 경우
			// 현재 렌더링 리소스의 렌더 타겟을 가져온다.
			// 2D이미지(텍스처)이며, 렌더링의 목표지점(Render Target)으로 사용
			// 이 객체가 2차원 공간에서 렌더링 작업을 받아들이는 메모리 리소스
			// 일반적으로 화면(백 버퍼) 대신, 이 텍스처에 직접 그래픽을 그릴 수 있다는 뜻이다.
			// 렌더링이 완료된 후, 그 결과 이미지(이 텍스쳐)를 가져와서 다른 3D객체의 표면에
			// 입히거나 화면의 UI요소로 사용하는 등 일반 텍스처처럼 다시 사용할 수 있음.
			UTextureRenderTarget2D* RenderTarget = RenderingResources->RenderTarget;
			check(RenderTarget);
			
			// 너비 크기가 1보다 크고, 높이 크기가 1보다 클때
			if (RenderTarget->GetSurfaceWidth() >= 1 && RenderTarget->GetSurfaceHeight() >= 1)
			{
				// 최종 계산된 색상
				const FLinearColor ComputedColorAndOpacity(Context.WidgetStyle.GetColorAndOpacityTint() * GetColorAndOpacity() * SurfaceBrush.GetTint(Context.WidgetStyle));
				// Retainer Widget은 언리얼 엔진의 Slate/UMG UI 시스템에 사용되는 특수한 위젯
				// 리테이너 위젯은 사전 곱셈 알파를 사용. 
				// 사전 곱셈 알파 라는 것은 R,G,B 값에 미리 A값을 곱함. -> R*A, G*A, B*A, A 값을 유지
				// GPU 블렌딩 연산이 빠르고 정확하며, 특히 텍스처 필터링 시 테두리 부분에서 발생하는
				// 검은색 경계선 문제를 방지
				const FLinearColor PremultipliedColorAndOpacity(ComputedColorAndOpacity * ComputedColorAndOpacity.A);

				// 리테이너 컨테이너에 존재하는 모든 자식위젯트리(SlateWidget에 대한 Tree)
				FWidgetRenderer* WidgetRenderer = RenderingResources->WidgetRenderer;
				// 동적 머태리얼이 존재 여부(에디터상 부여)
				UMaterialInstanceDynamic* DynamicEffect = RenderingResources->DynamicEffect;
				// nullptr 사용 안하면, false, 사용하면 true
				const bool bDynamicMaterialInUse = (DynamicEffect != nullptr);
				if (bDynamicMaterialInUse)
				{
					// DynamicEffectTextureParameter 전달된 값 -> 렌더 타켓에 전송
					DynamicEffect->SetTextureParameterValue(DynamicEffectTextureParameter, RenderTarget);
				}

				// 각 Element 를 그려라 명령
				FSlateDrawElement::MakeBox(
					*Context.WindowElementList, //실제로 그려야할 모든 슬레이트 위젯에 대한 정보
					Context.IncomingLayerId,
					AllottedGeometry.ToPaintGeometry(), //현재 할당된 위치(지오메트리)
					&SurfaceBrush, // SurfaceBrush - RenderTarget 연결 (RenderTarget의 경우에 PaintRetainedContentImpl에서 미리 그림을 그렸음.)
					// 항상 콘텐츠를 감마 공간(gamma space)에서 기록하므로,
					// 최종 버전을 렌더링할 때는 감마 보정(gamma correction)을 적용하지 않고
					// 렌더링 해야 한다.
					ESlateDrawEffect::PreMultipliedAlpha | ESlateDrawEffect::NoGamma,
					FLinearColor(PremultipliedColorAndOpacity.R, PremultipliedColorAndOpacity.G, PremultipliedColorAndOpacity.B, PremultipliedColorAndOpacity.A)
				);
			}
			
			// 상호작용 가능한지 불가능한지 체크
			const bool bInheritedHittestability = Args.GetInheritedHittestability();
			const bool bOutgoingHittestability = bInheritedHittestability && GetVisibility().AreChildrenHitTestVisible();

			// 상호작용 가능하다면, 실제 그리드를 힛테스트 가능하다고 전달.
			if (bOutgoingHittestability)
			{
				// 리테이너위젯 내부에서는 별도로 관리하던 HitTestGrid를 상위 Slate HitTestGrid에
				// 병합하여, Virtual Window 내부 Slate 트리도 클릭/포커스가능하도록 함.
				Args.GetHittestGrid().AddGrid(HittestGrid);
			}

			return GetCachedMaxLayerId();
		}
	}
	else
	{
		// OnPaint 최종 출력, 일반 UI처럼 직접 그림을 그리는 루틴
		return SCompoundWidget::OnPaint(Args, AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled);
	}
}

// 이 함수를 보고 나서 생각한 점
// 1. 내용과 상관없이 RenderOnInvalidation이 설정되어 있다면, 크기나 지오메트리가 달라질 때 렌더링된다.
// 2. 렌더링 되는 프레임과 상관없이 지정한 PhaseCount가 Phase와 일치할 때만 렌더링하면서 최적화된다.
SRetainerWidget::EPaintRetainedContentResult SRetainerWidget::PaintRetainedContentImpl(const FSlateInvalidationContext& Context, const FGeometry& AllottedGeometry, int32 LayerId)
{
	// 페이즈 기반의 렌더러인가
	if (RenderOnPhase)
	{
		// GFrameCounter : 프레임이 진행될 때마다 프레임 카운터 값이 꾸준히 +1씩 증가하는 변수
		// 지난 프레임 값이 현재 프레임 개수와 다르고, 프레임 카운트 % PhaseCount(지정) 를 나눴을 때
		// Phase값과 같으면 
		// 1. 이번 프레임에 아직 RetainerWidget이 렌더되지 않을 때
		// 2. 현재 프레임이 이 RetainerWidget이 렌더해야 하는 페이즈에 해당할 때
		if (LastTickedFrame != GFrameCounter && (GFrameCounter % PhaseCount) == Phase)
		{
			// 만약 Phase 기반 무효화(invalidation)를 사용 중이라면, 그냥 모든것을 다시 그린다.
			// 이번 RetainerWidget은 이번 페이즈에서 다시 렌더되어야 합니다.
			bRenderRequested = true;
			// 레이아웃도 다시 계산할 필요가 있어, 루트 레이아웃의 무효화
			InvalidateRootLayout();
		}
	}
	
	// 할당된 지오메트리
	const FPaintGeometry PaintGeometry = AllottedGeometry.ToPaintGeometry();
	FSlateRenderTransform AccumulatedRenderTransform = PaintGeometry.GetAccumulatedRenderTransform(); //누적된 렌더 트랜스폼(최종 트랜스폼)
	const FVector2f RenderSize = FVector2f(PaintGeometry.GetLocalSize()) * AccumulatedRenderTransform.GetMatrix().GetScale().GetVector();
	const FIntPoint RoundedRenderSize = RenderSize.IntPoint();

	// 인벨리데이션이 렌더링할 필요한가
	if (RenderOnInvalidation)
	{
		// invalidation root가 실제로 렌더링이 필요한지 여부를 처리해 줄 것
		bRenderRequested = true; //렌더링할 필요있음을 true 변환

		const FIntPoint ClipRectSize = Context.CullingRect.GetSize().IntPoint();
		const TOptional<FSlateClippingState> ClippingState = Context.WindowElementList->GetClippingState();
		const FColor ColorAndOpacityTint = Context.WidgetStyle.GetColorAndOpacityTint().ToFColor(false);

		// 기본 상태가 변경되었을 때는 레이아웃을 강제로 다시 계산한다.
		// 이전 크기와 달라진 경우
		// 할당된 지오메트리가 이전과 달라진 경우
		// 최종 트랜스폼이 이전과 다른 경우... 즉 그림 내에서 달라진 경우에 레이아웃을 다시 계산해야함.
		if (RoundedRenderSize != PreviousRenderSize
			|| AllottedGeometry != PreviousAllottedGeometry
			|| AllottedGeometry.GetAccumulatedRenderTransform() != PreviousAllottedGeometry.GetAccumulatedRenderTransform()
			|| ClipRectSize != PreviousClipRectSize
			|| ClippingState != PreviousClippingState)
		{
			PreviousRenderSize = RoundedRenderSize;
			PreviousAllottedGeometry = AllottedGeometry;
			PreviousClipRectSize = ClipRectSize;
			PreviousClippingState = ClippingState;
			
			// 이전 값들을 전부 업데이트 하고, 이전 레이아웃을 무효화
			// 모든 자식들에 대해 원하는 크기를 재 계산해야 할 때 사용
			InvalidateRootLayout();
		}
		
		// 기본 상태가 변경되었을 때는 페인트도 강제로(적극적으로) 다시 수행한다.
		// 최종 레이어 아이디 값이 마지막으로 변경된 레이어값과 다를 경우에
		// 색상이 변경된 경우에
		if (LayerId != LastIncomingLayerId
			|| ColorAndOpacityTint != PreviousColorAndOpacity)
		{
			LastIncomingLayerId = LayerId;
			PreviousColorAndOpacity = ColorAndOpacityTint;
			
			// 루트 위젯을 다시 그리기 위해서 무효화한다.
			GetRootWidget()->Invalidate(EInvalidateWidgetReason::Paint);
		}
	}
	else if (RoundedRenderSize != PreviousRenderSize)
	{
		//색상과 레이어아이디 뿐만아니라 사이즈가 달라진 경우에는 다시 렌더러 될 필요가 있어
		// 루트 레이어를 무효화 처리
		bRenderRequested = true;
		InvalidateRootLayout();
		PreviousRenderSize = RoundedRenderSize;
	}
	
	// 한 프레임당 최대 작업할 수 있는 리테이너의 양?
	if (Shared_MaxRetainerWorkPerFrame > 0)
	{
		// Shared_MaxRetainerWorkPerFrame 의 양을 초과한 경우에는 대기해야함.
		// 한 프레임당 처리할 수 있는 작업량을 초과했음.
		if (Shared_RetainerWorkThisFrame.TryGetValue(0) > Shared_MaxRetainerWorkPerFrame)
		{
			Shared_WaitingToRender.AddUnique(this);
			return EPaintRetainedContentResult::Queued; //미리 누적
		}
	}

	// 렌더러할 필요가 있다면,
	if (bRenderRequested)
	{
		// 머태리얼 파라미터 컬렉션이 제대로 동작하도록 하려면,
		// 현재 월드의 Scene 정보를 해당 기능에 의존하는 위젯들까지 정확하게 전달해줄 필요가 있다.
		// Scene 정보는 Slate 내부에 SceneViewport와 RetainerWidget에만 존재하므로,
		// 이 정보를 이후 호출에서 활용할 수 있도록 현재 Scene을 Slate Application에 전달한다.
		UWorld* TickWorld = OuterWorld.Get();
		if (TickWorld && TickWorld->Scene && IsInGameThread())
		{
			//활성화된 씬(Scene) 포인터를 렌더러에 등록한다.
			// 이 작업은 이후에 그려지는 모든 요소에 사용될 씬의 내부 인덱스를 반환한다.
			FSlateApplication::Get().GetRenderer()->RegisterCurrentScene(TickWorld->Scene);
		}
		else if (IsInGameThread()) //게임 스레이드 일경우에 (여러 스레드가 존재함, 오디오 스레드도 존재)
		{
			FSlateApplication::Get().GetRenderer()->RegisterCurrentScene(nullptr);
		}
		
		// 이 프레임에서 렌더링한 Retainer 개수를 업데이트 한다.
		// 0번째가 가장 최신 리테이너 값 + 1 -> 이번 프레임에 리테이너 개수 업데이트 진행
		Shared_RetainerWorkThisFrame = Shared_RetainerWorkThisFrame.TryGetValue(0) + 1;
		// 지난 틱 프레임 <- 현재 프레임 수를 적용
		LastTickedFrame = GFrameCounter;
		// 언제 그렸는지 시간 측정
		const double TimeSinceLastDraw = FApp::GetCurrentTime() - LastDrawTime;
		
		// 렌더 타겟에 할당된 사이즈는 반드시 양수이다.
		// 너비x높이
		const uint32 RenderTargetWidth  = FMath::RoundToInt(FMath::Abs(RenderSize.X));
		const uint32 RenderTargetHeight = FMath::RoundToInt(FMath::Abs(RenderSize.Y));
		// 최대 크기보다 현재 렌더 타켓의 크기가 큰가를 확인
		const bool bTextureIsTooLarge = FMath::Max(RenderTargetWidth, RenderTargetHeight) > GetMax2DTextureDimension();
		// 텍스처 사이즈에 대해 0인가를 확인
		const bool bTextureSizeZero = (RenderTargetWidth == 0 || RenderTargetHeight == 0);
	 
	 // 현재 렌더 타켓의 텍스처가 너무 크거나 0일경우(너비나 높이 둘 중 하나라도)
		if (bTextureIsTooLarge || bTextureSizeZero)
		{ 
			// bInvalidSizeLogged = true, 사용자가 레이아웃에 문제를 가지고 있을 가능성이 높음을
			// 사용자에게 경고한다.
			if (!bInvalidSizeLogged)
			{
				bInvalidSizeLogged = true;

				const bool bEnableWarnOnInvalidSize =
#if WITH_EDITOR
					bWarnOnInvalidSize; //에디터일 경우에
#else
					true; // 에디터가 아니면, 항상 true
#endif
				if (bTextureIsTooLarge) //텍스처가 크면서, 유효하지 않은 사이즈(잘못된 크기를 감지할 경우)일 경우에
				{
					if (bEnableWarnOnInvalidSize)
					{
						// SRetainerWidget에 요청된 크기가 지나치게 커서 처리할 수 없음을 로그
						UE_LOG(LogUMG, Warning, TEXT("The requested size for SRetainerWidget is too large. W:%i H:%i"), RenderTargetWidth, RenderTargetHeight);
					}
				}
				else
				{
					// RetainerWidget에 요청한 크기가 0일 경우에
					if (bEnableWarnOnInvalidSize)
					{
						UE_LOG(LogUMG, Warning, TEXT("The requested size for SRetainerWidget is 0. W:%i H:%i"), RenderTargetWidth, RenderTargetHeight);
					}
				}
			}
			// 결과를 출력
			return bTextureIsTooLarge ? EPaintRetainedContentResult::TextureSizeTooBig : EPaintRetainedContentResult::TextureSizeZero;
		}
		// 그렇지 않은 경우에 false (즉 잘못된 사이즈가 아닐 경우에)
		bInvalidSizeLogged = false;

		if (RenderTargetWidth >= 1 && RenderTargetHeight >= 1)
		{
			// 그려야 할 지오메트리의 최종 Transform-Translation(위치값)을 가져옴
			// 누적된 렌더 변환의 위치 오프셋
			const FVector2D ViewOffset = PaintGeometry.GetAccumulatedRenderTransform().GetTranslation();
			
			// 그려진 결과물을 담는 실제 텍스처 -> 2D 텍스처 리소스로 렌던될 타켓
			UTextureRenderTarget2D* RenderTarget = RenderingResources->RenderTarget;
			// SlateWidget을 RenderTarget에 그려주는 렌더러
			// Slate 기반 위젯을 TextureRenderTarget2D로 렌더링
			FWidgetRenderer* WidgetRenderer = RenderingResources->WidgetRenderer;
			
			// 현재 위젯이 보일 경우에 (Visibility -> Hidden/Collapsed 아닌 경우)
			if ( MyWidget->GetVisibility().IsVisible() )
			{
				// 포인터로 가지고 있는 미리 만들어 둔 RenderTarget의 표면 너비/높이가
				// 현재 그려야할 렌더 사이즈와 다를 경우에를 의미
				if ( (int32)RenderTarget->GetSurfaceWidth() != (int32)RenderTargetWidth ||
					 (int32)RenderTarget->GetSurfaceHeight() != (int32)RenderTargetHeight )
				{
					// 렌더 타겟 리소스가 이미 존재한다면, 그냥 크기만 조절한다.
					// InitCustomFormat 함수를 호출하면 렌더 명령(render commands)이 플러시되어,
					// 큰 끊김 현상이 존재하기 대문임.
					if(RenderTarget->GameThread_GetRenderTargetResource() && RenderTarget->OverrideFormat == PF_B8G8R8A8)
					{
						RenderTarget->ResizeTarget(RenderTargetWidth, RenderTargetHeight);
					}
					else
					{
						// Render Command Flust = 렌더 스레드와 GPU를 강제로 동기화
						// 렌더 스레드가 그동안 쌓아둔 모든 미해결 렌더링 명령어를 즉시 GPU에게 보내고
						// GPU가 그 명령을 전부 처리할 때까지 대기
						// 즉, 게임 스레드가 GPU와 렌더 스레드의 완료를 기다리므로 끊김 현상 발생
						
						/*
						- (이 부분은 추측에 해당함.)
						- Retainer Box를 자주 변화하는 UMG에 사용하지 말라는 이유가
						- UMG가 변화할 때 렌더 타켓 크기를 재조정하거나 렌더타겟이 없는 경우에는 재생성 해서 아닐까..?
						싶음.
						*/
						const bool bForceLinearGamma = false;
						// 렌더 타겟에 너비,높이 조정, 그리고 RGBA텍스처 색상, 감마처리 방법 등 형식 재정의
						RenderTarget->InitCustomFormat(RenderTargetWidth, RenderTargetHeight, PF_B8G8R8A8, bForceLinearGamma);
						RenderTarget->UpdateResourceImmediate(); //즉시 리소스를 업데이트 하도록 강제 프래쉬!
					}
				}
				//Scale값
				const float Scale = AllottedGeometry.Scale;
				// 그림 그려질 크기를 DrawSize로 저장
				const FVector2D DrawSize = FVector2D(RenderTargetWidth, RenderTargetHeight);
				//const FGeometry WindowGeometry = FGeometry::MakeRoot(DrawSize * ( 1 / Scale ), FSlateLayoutTransform(Scale, PaintGeometry.DrawPosition));
				
				// 최신 크기에 맞춰서 Surface brush 설정을 갱신한다. (설정 중 이미지 크기)
				// 왜냐면 이미지가 달라서 렌더 타켓의 이미지 크기를 변경했으니까 실제 표면의 사이즈 크기도 재조정
				SurfaceBrush.ImageSize = DrawSize;

				WidgetRenderer->ViewOffset = -ViewOffset;
				WidgetRenderer->SetIsPrepassNeeded(false);
				
				// 렌더링 크기만큼 윈도우 사이즈 조절
				FVector2f WindowSize(RenderSize);
				// Retainer Box에 그려져야할 요소들의 모음을 반환
				SWindow* PaintWindow = Context.WindowElementList->GetPaintWindow();
				if (PaintWindow) //존재한다면,
				{
					// 해당 뷰포트 사이즈를 가지고 와서, 윈도우 크기에 비교
					// 그려질 수 있는 윈도우 
					const FVector2f ViewportSize = PaintWindow->GetViewportSize();
					WindowSize.X = FMath::Max(WindowSize.X, ViewportSize.X);
					WindowSize.Y = FMath::Max(WindowSize.Y, ViewportSize.Y);
				}
				// 가상의 윈도우에 보여질 윈도우 크기를 재 설정하고
				VirtualWindow->Resize(WindowSize);
				// DrawInvalidationRoot 호출해서 위젯이 다시 그려질 수 있도록 함.
				// 렌더타겟에 그림을 그려주는 객체 WidgetRender를 사용해 가상의 윈도우를 사용해 렌더 타켓에 미리 그림을 그려넣음.
				bool bRepaintedWidgets = WidgetRenderer->DrawInvalidationRoot(VirtualWindow, RenderTarget, *this, Context, GDeferRetainedRenderingRenderThread != 0);
				bRenderRequested = false; //이미 그렸으니까 false
				Shared_WaitingToRender.Remove(this); // 그렸으니까 대기 목록에서 삭제

				LastDrawTime = FApp::GetCurrentTime(); // 그려졌으니까 그 시간 저장

				return bRepaintedWidgets ? EPaintRetainedContentResult::Painted : EPaintRetainedContentResult::NotPainted;
			}
		}
	}

	return EPaintRetainedContentResult::NotPainted;
}
  • Retainer Box에서 사용하는 Enum class(EInvalidateWidgetReason)
/**
 * 위젯에서 발생할 수 있는 다양한 종류의 무효화 유형들
 */
enum class EInvalidateWidgetReason : uint8
{
	None = 0,

	/**
	 * 위젯이 원하는 크기(desired size)가 변경되어야 할 때 Layout 무효화를 사용한다.
	 * 이는 비용이 큰 무효화이므로,
	 * 단순히 위젯을 다시 그리기만 하면 되는 경우에는 사용하지 않는 것이 좋다.
	 */
	Layout = 1 << 0,

	/**
	 * 위젯의 페인팅(그리기)만 변경되었지만, desired size에 영향을 주지 않는 경우에 사용한다.
	 */
	Paint = 1 << 1,

	/**
	 * 위젯의 Volatility(변동성) 속성만 조정되었을 때 사용한다.
	 * 위젯이 변할 가능성이 있는가 (변동성)
	 */
	Volatility = 1 << 2,

	/**
	 * 자식이 추가되거나 제거되었을 때 사용한다.
	 * (이 경우 Prepass와 Layout이 함께 필요함을 의미)
	 */
	ChildOrder = 1 << 3,

	/**
	 * 위젯의 Render Transform이 변경되었을 때 사용
	 */
	RenderTransform = 1 << 4,

	/**
	 * Visibility가 변경되었을 때 사용
	 * 이 경우 Layout 무효화도 함께 필요함을 의미한다.
	 */
	Visibility = 1 << 5,

	/**
	 * Slate의 Attribute가 바인딩되거나 해제되었을 때 사용한다.
	 * (SlateAttributeMetaData에 의해 사용됨)
	 */
	AttributeRegistration = 1 << 6,

	/**
	 * 이 위젯의 모든 자식들에 대해 원하는 크기를 재 계산해야 할 필요가 있을 때 사용 (재귀적으로 적용)
	 * 이 경우 레이아웃 무효화를 의미
	 */
	Prepass = 1 << 7,

	/**
	 * 페인팅 혹은 크기 관련 일반 속성이 변경될 때 Paint 무효화를 사용한다.
	 * 또한, 변경된 속성이 Volatility에 영향을 준다면,
	 * Volatility가 다시 계산되어 캐싱될 수 있도록 Volatility도 함께 무효화해야한다.
	 */
	PaintAndVolatility = Paint | Volatility,
	/**
	 * 페인팅 혹은 크기 관련 일반 속성이 변경될 때 Layout 무효화를 사용한다.
	 * 또한, 변경된 속성이 Volatility에 영향을 준다면,
	 * Volatility가 다시 계산되어 캐싱될 수 있도록 Volatility도 함께 무효화해야한다.
	 */
	LayoutAndVolatility = Layout | Volatility,
};

결론

  • Retainer Box는 사전에 Phase/PhaseCount 를 지정할 수 있는데, 현재 전역 프레임을 모드 연산으로 나누어 해당 페이즈와 같다면, 리테이너 박스에 놓여진 위젯을 렌더타켓에 그림을 그려놓는 방식으로 최적화한다.
    • 렌더 타켓을 사용하면, 각 자식 객체의 위젯에 대해 렌더링하는 것이 아닌 자식 객체의 복잡한 요소를 단순히 2D 텍스처에 미리 그림을 그려 GPU에게 요청하는 것(수많은 드로우콜 감소)임으로 저렴하게 해결할 수 있음
  • 즉, 모든 위젯은 매 프레임마다 그리지만, Retainer Box는 크기가 변화하거나, 페이즈가 같다면 렌더 타켓에 그리도록 하는 방법이다.
    • 매 프레임마다 안그린다는 장점과 렌더 타켓을 이용해서 백버퍼에 요청한다는 점이 최적화 용도에 적합하다고 할 수 있다.
  • 그런데, 만약 렌더 타켓을 재조정하지 못할정도로 클 경우에는 일반 위젯처럼 그림을 그리도록 한다.
  • 그리고 렌더 타켓에 그림이 그려지는 부분은 PaintRetainedContentImpl 함수에서 일어나고
  • 실제 그 그림을 GPU에게 그릴 수 있도록 명령을 전달하는 부분은 OnPaint함수의 MakeBox 함수이다.
profile
게임 클라이언트 프로그래머

0개의 댓글