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);

 이러한 기법들을 조합하여 사용하면 언리얼 엔진에서 고성능의 커스텀 렌더링 컴포넌트를 구현할 수 있습니다.

 게임의 요구사항에 맞춰 렌더링 파이프라인을 세밀하게 제어하고, 독특한 시각적 효과를 만들어낼 수 있습니다.