Unreal Engine에서 UMG를 활용한 데이터 레이더 차트 구현

레이더 차트의 기본 개념과 UMG 적용 방안

Unreal Engine에서 UMG를 사용하여 데이터 시각화 요소를 제작할 때, 종종 차트 관련 기능이 부족하다는 점을 느낄 수 있습니다. 특히 레이더 차트(Radar Chart)처럼 복수의 축에 따라 데이터를 표현해야 하는 경우, 기본 제공 위젯만으로는 한계가 있습니다. 하지만 UUserWidgetNativePaint 메서드를 오버라이드하면 Slate 렌더링 시스템을 직접 활용해 커스텀 도형을 그릴 수 있습니다.

핵심 설계: 삼각형 기반 폴리곤 그리기

레이더 차트는 각 카테고리별 데이터 값을 반경 방향으로 표현하며, 중심점에서부터 해당 각도 방향으로 뻗은 점들을 연결해 다각형 형태로 시각화합니다. 이 과정에서 각 섹션은 중심점과 두 인접한 데이터 점 사이의 삼각형으로 구성됩니다. 따라서 복잡한 곡선 대신 단순한 직선 기반 폴리곤을 사용함으로써 성능과 정확성을 동시에 확보할 수 있습니다.

헤더 파일 정의

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "RadarChartWidget.generated.h"

UCLASS(Blueprintable)
class PROJECTNAME_API URadarChartWidget : public UUserWidget
{
    GENERATED_BODY()

protected:
    virtual int32 NativePaint(const FPaintArgs& Args,
                              const FGeometry& AllottedGeometry,
                              const FSlateRect& MyCullingRect,
                              FSlateWindowElementList& OutDrawElements,
                              int32 LayerId,
                              const FWidgetStyle& InWidgetStyle,
                              bool bParentEnabled) const override;

public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Chart")
    float Radius;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Chart")
    TArray<float> DataValues;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Chart")
    TArray<FColor> SegmentColors;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Chart")
    TArray<float> Angles;

    UFUNCTION(BlueprintCallable, Category = "Chart")
    void InitializeFromPercentages(const TArray<float>& Percents);

    UFUNCTION(BlueprintCallable, Category = "Chart")
    void UpdateDataRadii(const TArray<float>& Radii);

private:
    void DrawSegment(FSlateWindowElementList& OutDrawElements,
                     const FGeometry& Geometry,
                     int32 Layer,
                     float CenterX,
                     float CenterY,
                     float StartAngle,
                     float EndAngle,
                     float StartRadius,
                     float EndRadius,
                     const FColor& Tint) const;

    FColor RetrieveSegmentColor(int32 Index) const;
    float GetDataRadius(int32 Index) const;
};

구현 코드 분석

#include "RadarChartWidget.h"
#include "Rendering/SlateRenderer.h"

int32 URadarChartWidget::NativePaint(const FPaintArgs& Args,
                                     const FGeometry& AllottedGeometry,
                                     const FSlateRect& MyCullingRect,
                                     FSlateWindowElementList& OutDrawElements,
                                     int32 LayerId,
                                     const FWidgetStyle& InWidgetStyle,
                                     bool bParentEnabled) const
{
    if (Angles.Num() < 2 || DataValues.Num() == 0) return LayerId;

    float accumulatedAngle = 0.f;
    float currentRadius, nextRadius;
    float centerX = Radius;
    float centerY = Radius;

    for (int32 i = 0; i < Angles.Num() - 1; ++i)
    {
        float startAngle = accumulatedAngle + Angles[i];
        float endAngle = startAngle + Angles[i + 1];

        currentRadius = GetDataRadius(i);
        nextRadius = GetDataRadius(i + 1);

        FColor segmentColor = RetrieveSegmentColor(i);

        DrawSegment(OutDrawElements, AllottedGeometry, LayerId,
                    centerX, centerY,
                    startAngle, endAngle,
                    currentRadius, nextRadius,
                    segmentColor);

        accumulatedAngle = startAngle;
    }

    return LayerId + 1;
}

void URadarChartWidget::DrawSegment(FSlateWindowElementList& OutDrawElements,
                                   const FGeometry& Geometry,
                                   int32 Layer,
                                   float CenterX,
                                   float CenterY,
                                   float StartAngle,
                                   float EndAngle,
                                   float StartRadius,
                                   float EndRadius,
                                   const FColor& Color) const
{
    if (StartAngle >= EndAngle) return;

    TArray<FSlateVertex> Vertices;
    TArray<SlateIndex> Indices;

    FVector2D centerScreen(CenterX, CenterY);
    FVector2D startPoint(
        CenterX + StartRadius * FMath::Cos(FMath::DegreesToRadians(StartAngle - 90)),
        CenterY + StartRadius * FMath::Sin(FMath::DegreesToRadians(StartAngle - 90))
    );
    FVector2D endPoint(
        CenterX + EndRadius * FMath::Cos(FMath::DegreesToRadians(EndAngle - 90)),
        CenterY + EndRadius * FMath::Sin(FMath::DegreesToRadians(EndAngle - 90))
    );

    const FSlateRenderTransform& transform = Geometry.ToPaintGeometry().GetAccumulatedRenderTransform();
    centerScreen = transform.TransformPoint(centerScreen);
    startPoint = transform.TransformPoint(startPoint);
    endPoint = transform.TransformPoint(endPoint);

    FSlateVertex vCenter, vStart, vEnd;
    vCenter.Position = centerScreen;
    vStart.Position = startPoint;
    vEnd.Position = endPoint;

    vCenter.Color = Color;
    vStart.Color = Color;
    vEnd.Color = Color;

    const int32 idxCenter = Vertices.Add(vCenter);
    const int32 idxStart = Vertices.Add(vStart);
    const int32 idxEnd = Vertices.Add(vEnd);

    Indices.Add(idxCenter);
    Indices.Add(idxStart);
    Indices.Add(idxEnd);

    const FSlateBrush* dummyBrush = FCoreStyle::Get().GetBrush("WhiteTexture");
    const FSlateResourceHandle resource = FSlateApplication::Get().GetRenderer()->GetResourceHandle(*dummyBrush);

    FSlateDrawElement::MakeCustomVerts(
        OutDrawElements,
        Layer,
        resource,
        Vertices,
        Indices,
        nullptr,
        0,
        0
    );
}

FColor URadarChartWidget::RetrieveSegmentColor(int32 Index) const
{
    return SegmentColors.IsValidIndex(Index) ? SegmentColors[Index] : FColor::FromHex("#FF5555");
}

float URadarChartWidget::GetDataRadius(int32 Index) const
{
    if (DataValues.IsValidIndex(Index))
        return DataValues[Index] * Radius * 0.01f; // 백분율 기반 스케일링
    return Radius * 0.5f;
}

void URadarChartWidget::InitializeFromPercentages(const TArray<float>& Percents)
{
    Angles.Reset();
    Angles.Add(0.f);
    float total = 0.f;
    for (float val : Percents) total += val;
    
    for (float val : Percents)
    {
        Angles.Add(val / total * 360.f);
    }
}

void URadarChartWidget::UpdateDataRadii(const TArray<float>& Radii)
{
    DataValues = Radii;
}

블루프린트 통합 팁

C++ 클래스를 기반으로 한 위젯 블루프린트가 그래프나 디자이너 탭을 잃는 문제가 발생할 수 있습니다. 이를 해결하려면 다음과 같은 절차를 따르세요:

  1. 임시로 UserWidget에서 상속받는 BP 생성
  2. 생성 후 Class Settings에서 부모 클래스를 커스텀 C++ 클래스(URadarChartWidget)로 변경
  3. 기본 캔버스 패널을 제거하고, 필요한 변수를 초기화

이 방법을 통해 블루프린트 편집 기능을 유지하면서도 커스텀 그리기 로직을 사용할 수 있습니다.

결과 및 개선 가능성

위 코드는 기본적인 레이더 차트를 실시간으로 렌더링하며, 데이터 업데이트와 색상 지정이 블루프린트에서도 가능하도록 설계되었습니다. 추가 개선 포인트로는:

  • 여러 계층의 동심원 배경 그리기
  • 텍스트 레이블 오버레이
  • 애니메이션을 통한 값 전환 효과
  • 마우스 오버 시 하이라이트 기능

등이 있습니다.

태그: Unreal Engine UMG Slate Radar Chart C++

7월 27일 07:31에 게시됨