icon안동민 개발노트

커스텀 렌더링 컴포넌트 생성


 언리얼 엔진에서 C++를 사용하여 커스텀 렌더링 컴포넌트를 생성하면 게임의 시각적 요소를 세밀하게 제어할 수 있습니다.

 이 절에서는 커스텀 렌더링 컴포넌트의 구현 방법과 고급 기법을 살펴보겠습니다.

UPrimitiveComponent를 상속받은 커스텀 렌더링 컴포넌트 클래스 생성

 먼저, UPrimitiveComponent를 상속받아 기본 커스텀 렌더링 컴포넌트 클래스를 생성합니다.

##include "CoreMinimal.h"
##include "Components/PrimitiveComponent.h"
##include "CustomRenderingComponent.generated.h"
 
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class MYGAME_API UCustomRenderingComponent : public UPrimitiveComponent
{
    GENERATED_BODY()
 
public:
    UCustomRenderingComponent();
 
    virtual FPrimitiveSceneProxy* CreateSceneProxy() override;
    virtual FBoxSphereBounds CalcBounds(const FTransform& LocalToWorld) const override;
};
 
UCustomRenderingComponent::UCustomRenderingComponent()
{
    PrimaryComponentTick.bCanEverTick = true;
    bWantsInitializeComponent = true;
}

커스텀 메시 데이터 정의 및 렌더링

 커스텀 메시 데이터를 정의하고 렌더링하기 위해 FPrimitiveSceneProxy를 구현합니다.

class FCustomSceneProxy : public FPrimitiveSceneProxy
{
public:
    FCustomSceneProxy(const UCustomRenderingComponent* InComponent)
        : FPrimitiveSceneProxy(InComponent)
    {
        // 메시 데이터 초기화
    }
 
    virtual void GetDynamicMeshElements(const TArray<const FSceneView*>& Views, const FSceneViewFamily& ViewFamily, uint32 VisibilityMap, FMeshElementCollector& Collector) const override
    {
        QUICK_SCOPE_CYCLE_COUNTER(STAT_CustomSceneProxy_GetDynamicMeshElements);
 
        for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
        {
            if (VisibilityMap & (1 << ViewIndex))
            {
                const FSceneView* View = Views[ViewIndex];
                FMeshBatch& Mesh = Collector.AllocateMesh();
                // 메시 렌더링 설정
            }
        }
    }
 
    virtual FPrimitiveViewRelevance GetViewRelevance(const FSceneView* View) const override
    {
        FPrimitiveViewRelevance Result;
        Result.bDrawRelevance = IsShown(View);
        Result.bDynamicRelevance = true;
        return Result;
    }
 
    virtual uint32 GetMemoryFootprint() const override { return sizeof(*this) + GetAllocatedSize(); }
};
 
FPrimitiveSceneProxy* UCustomRenderingComponent::CreateSceneProxy()
{
    return new FCustomSceneProxy(this);
}

동적 지오메트리 생성 및 업데이트

 런타임에 동적으로 지오메트리를 생성하고 업데이트하는 기능을 구현합니다.

void UCustomRenderingComponent::UpdateMeshData(const TArray<FVector>& Vertices, const TArray<int32>& Indices)
{
    // 메시 데이터 업데이트
    MarkRenderStateDirty();
}
 
void UCustomRenderingComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
    Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
 
    // 동적 지오메트리 업데이트 로직
    TArray<FVector> NewVertices;
    TArray<int32> NewIndices;
    // 새로운 버텍스와 인덱스 생성
    UpdateMeshData(NewVertices, NewIndices);
}

커스텀 셰이더와 렌더링 컴포넌트의 통합

 커스텀 셰이더를 렌더링 컴포넌트와 통합하여 고유한 시각 효과를 만들 수 있습니다.

class FCustomMaterialRenderProxy : public FMaterialRenderProxy
{
public:
    FCustomMaterialRenderProxy(const FMaterialRenderProxy* InParent, const FLinearColor& InColor)
        : Parent(InParent), Color(InColor) {}
 
    virtual const FMaterial* GetMaterial(ERHIFeatureLevel::Type FeatureLevel) const override
    {
        return Parent->GetMaterial(FeatureLevel);
    }
 
    virtual bool GetVectorValue(const FMaterialParameterInfo& ParameterInfo, FLinearColor* OutValue, const FMaterialRenderContext& Context) const override
    {
        if (ParameterInfo.Name == TEXT("Color"))
        {
            *OutValue = Color;
            return true;
        }
        return Parent->GetVectorValue(ParameterInfo, OutValue, Context);
    }
 
private:
    const FMaterialRenderProxy* Parent;
    FLinearColor Color;
};

렌더 스레드와의 동기화 처리

 렌더 스레드와 게임 스레드 간의 동기화를 처리하여 안정적인 렌더링을 보장합니다.

void UCustomRenderingComponent::SendRenderDynamicData_Concurrent()
{
    if (SceneProxy)
    {
        FCustomSceneProxy* CustomProxy = static_cast<FCustomSceneProxy*>(SceneProxy);
        ENQUEUE_RENDER_COMMAND(UpdateCustomData)(
            [CustomProxy, NewData = MeshData](FRHICommandListImmediate& RHICmdList)
            {
                CustomProxy->UpdateDynamicData_RenderThread(NewData);
            }
        );
    }
}

효율적인 렌더링을 위한 최적화 전략

  1. 뷰 프러스텀 컬링 구현
  2. LOD (Level of Detail) 시스템 사용
  3. 인스턴싱 기법 활용
virtual FPrimitiveViewRelevance GetViewRelevance(const FSceneView* View) const override
{
    FPrimitiveViewRelevance Result;
    Result.bDrawRelevance = IsShown(View) && IsInView(View);
    Result.bDynamicRelevance = true;
    Result.bUsesLightingChannels = GetLightingChannelMask() != GetDefaultLightingChannelMask();
    Result.bRenderInMainPass = ShouldRenderInMainPass();
    Result.bUsesWorldPositionOffset = true;
    return Result;
}

LOD 시스템 구현

 다양한 상세도 레벨을 지원하는 LOD 시스템을 구현합니다.

void UCustomRenderingComponent::UpdateLOD(float ViewDistance)
{
    int32 NewLODLevel = 0;
    if (ViewDistance > 1000.0f) NewLODLevel = 2;
    else if (ViewDistance > 500.0f) NewLODLevel = 1;
 
    if (NewLODLevel != CurrentLODLevel)
    {
        CurrentLODLevel = NewLODLevel;
        UpdateMeshForLOD(CurrentLODLevel);
    }
}
 
void UCustomRenderingComponent::UpdateMeshForLOD(int32 LODLevel)
{
    // LOD 레벨에 따른 메시 데이터 업데이트
    MarkRenderStateDirty();
}

인스턴싱 기법 활용

 많은 수의 동일한 객체를 효율적으로 렌더링하기 위해 인스턴싱 기법을 활용합니다.

class FCustomInstancedSceneProxy : public FInstancedStaticMeshSceneProxy
{
public:
    FCustomInstancedSceneProxy(UCustomInstancedComponent* Component)
        : FInstancedStaticMeshSceneProxy(Component, false)
    {
        // 인스턴스 데이터 설정
    }
 
    virtual void GetDynamicMeshElements(const TArray<const FSceneView*>& Views, const FSceneViewFamily& ViewFamily, uint32 VisibilityMap, FMeshElementCollector& Collector) const override
    {
        // 인스턴스 메시 엘리먼트 생성 및 렌더링
    }
};

렌더링 파이프라인과의 통합

 커스텀 렌더링 컴포넌트를 언리얼 엔진의 렌더링 파이프라인과 통합합니다.

void UCustomRenderingComponent::CreateRenderState_Concurrent(FRegisterComponentContext* Context)
{
    Super::CreateRenderState_Concurrent(Context);
 
    if (SceneProxy)
    {
        FCustomSceneProxy* CustomProxy = static_cast<FCustomSceneProxy*>(SceneProxy);
        ENQUEUE_RENDER_COMMAND(RegisterCustomRenderer)(
            [CustomProxy](FRHICommandListImmediate& RHICmdList)
            {
                CustomProxy->RegisterCustomRenderer_RenderThread();
            }
        );
    }
}

게임플레이 요소와 연동된 동적 렌더링 효과 구현

 게임플레이 이벤트에 반응하여 동적으로 렌더링 효과를 변경합니다.

void AMyActor::OnTakeDamage(float Damage)
{
    // 기존 데미지 처리 로직
 
    UCustomRenderingComponent* RenderComp = FindComponentByClass<UCustomRenderingComponent>();
    if (RenderComp)
    {
        RenderComp->SetDamageVisualization(Damage);
    }
}
 
void UCustomRenderingComponent::SetDamageVisualization(float Damage)
{
    // 데미지에 따른 시각적 효과 설정
    DamageIntensity = FMath::Clamp(Damage / 100.0f, 0.0f, 1.0f);
    MarkRenderStateDirty();
}

프로시저럴 메시 생성 및 렌더링

 런타임에 프로시저럴하게 메시를 생성하고 렌더링합니다.

void UCustomRenderingComponent::GenerateProceduralMesh()
{
    TArray<FVector> Vertices;
    TArray<int32> Indices;
    TArray<FVector> Normals;
    TArray<FVector2D> UVs;
 
    // 프로시저럴 메시 생성 로직
    // 버텍스, 인덱스, 노말, UV 생성
 
    UpdateMeshData(Vertices, Indices, Normals, UVs);
}

고급 렌더링 기법 적용

 테셀레이션과 지오메트리 셰이더를 활용한 고급 렌더링 기법을 적용합니다.

// 테셀레이션 셰이더 예시
class FCustomTessellationShader : public FGlobalShader
{
    DECLARE_SHADER_TYPE(FCustomTessellationShader, Global);
 
public:
    static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
    {
        return IsFeatureLevelSupported(Parameters.Platform, ERHIFeatureLevel::SM5);
    }
 
    FCustomTessellationShader() {}
    FCustomTessellationShader(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
        : FGlobalShader(Initializer)
    {
        // 셰이더 파라미터 바인딩
    }
};
 
IMPLEMENT_SHADER_TYPE(, FCustomTessellationShader, TEXT("/Game/Shaders/CustomTessellation.usf"), TEXT("MainVS"), SF_Vertex);

 이러한 기법들을 조합하여 사용하면, 언리얼 엔진에서 고성능의 커스텀 렌더링 컴포넌트를 구현할 수 있습니다. 게임의 요구사항에 맞춰 렌더링 파이프라인을 세밀하게 제어하고, 독특한 시각적 효과를 만들어낼 수 있습니다.

 항상 성능 최적화를 염두에 두고, 렌더 스레드와 게임 스레드 간의 동기화를 신중하게 관리해야 합니다. 또한, 다양한 하드웨어 스펙을 고려하여 LOD 시스템과 같은 스케일러블한 솔루션을 구현하는 것이 중요합니다.

 고급 렌더링 기법을 적용할 때는 대상 플랫폼의 하드웨어 지원 여부를 확인하고, 필요에 따라 대체 기술을 준비하는 것이 좋습니다. 프로파일링 도구를 활용하여 렌더링 성능을 지속적으로 모니터링하고 최적화하는 것도 잊지 말아야 합니다.