icon안동민 개발노트

포스트 프로세스 이펙트 개요


 포스트 프로세스 이펙트는 게임의 시각적 품질을 크게 향상시킬 수 있는 강력한 도구입니다.

 이 절에서는 언리얼 엔진의 포스트 프로세스 시스템을 C++에서 활용하고 확장하는 방법을 살펴보겠습니다.

APostProcessVolume 클래스 사용법

 APostProcessVolume 클래스를 사용하여 특정 영역에 포스트 프로세스 효과를 적용할 수 있습니다.

UCLASS()
class MYGAME_API AMyPostProcessVolume : public APostProcessVolume
{
    GENERATED_BODY()
 
public:
    AMyPostProcessVolume();
 
    UFUNCTION(BlueprintCallable, Category = "Post Process")
    void SetBloomIntensity(float Intensity);
};
 
AMyPostProcessVolume::AMyPostProcessVolume()
{
    bUnbound = true; // 전체 레벨에 적용
}
 
void AMyPostProcessVolume::SetBloomIntensity(float Intensity)
{
    Settings.bOverride_BloomIntensity = true;
    Settings.BloomIntensity = Intensity;
}

C++에서 포스트 프로세스 설정 제어

 게임 로직에 따라 동적으로 포스트 프로세스 설정을 변경할 수 있습니다.

void AMyGameMode::UpdatePostProcessSettings(float DeltaTime)
{
    APostProcessVolume* PPVolume = Cast<APostProcessVolume>(UGameplayStatics::GetActorOfClass(GetWorld(), APostProcessVolume::StaticClass()));
    if (PPVolume)
    {
        // 시간에 따라 변화하는 비네트 효과
        float VignetteIntensity = FMath::Sin(GetWorld()->GetTimeSeconds()) * 0.5f + 0.5f;
        PPVolume->Settings.bOverride_VignetteIntensity = true;
        PPVolume->Settings.VignetteIntensity = VignetteIntensity;
    }
}

커스텀 포스트 프로세스 머티리얼

 커스텀 포스트 프로세스 머티리얼을 생성하고 적용하는 방법

UCLASS()
class MYGAME_API UMyPostProcessManager : public UObject
{
    GENERATED_BODY()
 
public:
    UFUNCTION(BlueprintCallable, Category = "Post Process")
    void ApplyCustomPostProcess(UMaterial* PostProcessMaterial);
};
 
void UMyPostProcessManager::ApplyCustomPostProcess(UMaterial* PostProcessMaterial)
{
    if (PostProcessMaterial)
    {
        APostProcessVolume* PPVolume = Cast<APostProcessVolume>(UGameplayStatics::GetActorOfClass(GetWorld(), APostProcessVolume::StaticClass()));
        if (PPVolume)
        {
            PPVolume->Settings.WeightedBlendables.Array.Empty();
            FWeightedBlendable NewBlendable;
            NewBlendable.Object = PostProcessMaterial;
            NewBlendable.Weight = 1.0f;
            PPVolume->Settings.WeightedBlendables.Array.Add(NewBlendable);
        }
    }
}

포스트 프로세스 효과의 프로그래밍 제어

 블룸, 톤 매핑, 색수차 등의 효과를 프로그래밍 방식으로 제어할 수 있습니다.

void AMyPostProcessController::UpdateEffects(float DeltaTime)
{
    APostProcessVolume* PPVolume = Cast<APostProcessVolume>(UGameplayStatics::GetActorOfClass(GetWorld(), APostProcessVolume::StaticClass()));
    if (PPVolume)
    {
        // 블룸 제어
        PPVolume->Settings.bOverride_BloomIntensity = true;
        PPVolume->Settings.BloomIntensity = FMath::Sin(GetWorld()->GetTimeSeconds()) * 2.0f + 3.0f;
 
        // 톤 매핑 제어
        PPVolume->Settings.bOverride_AutoExposureMethod = true;
        PPVolume->Settings.AutoExposureMethod = EAutoExposureMethod::AEM_Manual;
        PPVolume->Settings.bOverride_AutoExposureBias = true;
        PPVolume->Settings.AutoExposureBias = FMath::Clamp(PlayerHealth / 100.0f, -1.0f, 1.0f);
 
        // 색수차 제어
        PPVolume->Settings.bOverride_SceneFringeIntensity = true;
        PPVolume->Settings.SceneFringeIntensity = FMath::Max(0.0f, PlayerSpeed / MaxSpeed - 0.5f) * 5.0f;
    }
}

동적 포스트 프로세스 효과 전환 구현

 게임 상황에 따라 포스트 프로세스 효과를 부드럽게 전환할 수 있습니다.

UCLASS()
class MYGAME_API UPostProcessTransitionManager : public UObject
{
    GENERATED_BODY()
 
public:
    UFUNCTION(BlueprintCallable, Category = "Post Process")
    void TransitionToEffect(UMaterialInterface* TargetEffect, float TransitionTime);
 
private:
    UPROPERTY()
    UMaterialInstanceDynamic* CurrentEffect;
 
    UPROPERTY()
    UMaterialInstanceDynamic* TargetEffect;
 
    float TransitionProgress;
    float TransitionDuration;
 
    void UpdateTransition(float DeltaTime);
};
 
void UPostProcessTransitionManager::TransitionToEffect(UMaterialInterface* NewEffect, float TransitionTime)
{
    if (NewEffect)
    {
        TargetEffect = UMaterialInstanceDynamic::Create(NewEffect, this);
        TransitionProgress = 0.0f;
        TransitionDuration = TransitionTime;
    }
}
 
void UPostProcessTransitionManager::UpdateTransition(float DeltaTime)
{
    if (CurrentEffect && TargetEffect)
    {
        TransitionProgress += DeltaTime / TransitionDuration;
        if (TransitionProgress >= 1.0f)
        {
            CurrentEffect = TargetEffect;
            TargetEffect = nullptr;
        }
        else
        {
            // 두 효과 사이의 블렌딩 수행
            CurrentEffect->SetScalarParameterValue("BlendFactor", 1.0f - TransitionProgress);
            TargetEffect->SetScalarParameterValue("BlendFactor", TransitionProgress);
        }
    }
}

성능을 고려한 포스트 프로세스 최적화

  1. 효과의 복잡도 조절 : 플랫폼에 따라 효과의 품질 조정
  2. 선택적 적용 : 필요한 상황에서만 고비용 효과 활성화
  3. LOD (Level of Detail) 시스템 구현 : 카메라 거리에 따라 효과 품질 조정
void AMyPostProcessOptimizer::OptimizeEffects()
{
    UGameUserSettings* Settings = UGameUserSettings::GetGameUserSettings();
    ESettingsDOF QualityLevel = Settings->GetPostProcessingQuality();
 
    APostProcessVolume* PPVolume = Cast<APostProcessVolume>(UGameplayStatics::GetActorOfClass(GetWorld(), APostProcessVolume::StaticClass()));
    if (PPVolume)
    {
        switch (QualityLevel)
        {
            case ESettingsDOF::Low:
                PPVolume->Settings.bOverride_BloomQuality = true;
                PPVolume->Settings.BloomQuality = 0;
                break;
            case ESettingsDOF::Medium:
                PPVolume->Settings.bOverride_BloomQuality = true;
                PPVolume->Settings.BloomQuality = 1;
                break;
            case ESettingsDOF::High:
                PPVolume->Settings.bOverride_BloomQuality = true;
                PPVolume->Settings.BloomQuality = 2;
                break;
            // 기타 품질 레벨 처리
        }
    }
}

커스텀 포스트 프로세스 셰이더 작성

 HLSL을 사용하여 커스텀 포스트 프로세스 셰이더를 작성할 수 있습니다.

// MyCustomPostProcess.usf
##include "/Engine/Public/Platform.ush"
 
float4 MainPS(in float2 UVs : TEXCOORD0) : SV_Target0
{
    float4 SceneColor = Texture2DSample(PostprocessInput0, PostprocessInput0Sampler, UVs);
    
    // 흑백 효과 적용
    float Luminance = dot(SceneColor.rgb, float3(0.299f, 0.587f, 0.114f));
    return float4(Luminance, Luminance, Luminance, SceneColor.a);
}

 이 셰이더를 머티리얼에서 사용하려면 Custom 노드를 추가하고 HLSL 코드를 직접 입력합니다.

포스트 프로세스 스택 관리

 여러 포스트 프로세스 효과를 관리하고 우선순위를 지정할 수 있습니다.

UCLASS()
class MYGAME_API UPostProcessStackManager : public UObject
{
    GENERATED_BODY()
 
public:
    void PushEffect(UMaterialInterface* Effect, float Priority);
    void PopEffect(UMaterialInterface* Effect);
    void UpdateStack();
 
private:
    TArray<TPair<UMaterialInterface*, float>> EffectStack;
};
 
void UPostProcessStackManager::PushEffect(UMaterialInterface* Effect, float Priority)
{
    EffectStack.Add(TPair<UMaterialInterface*, float>(Effect, Priority));
    EffectStack.Sort([](const TPair<UMaterialInterface*, float>& A, const TPair<UMaterialInterface*, float>& B) {
        return A.Value > B.Value;
    });
}
 
void UPostProcessStackManager::PopEffect(UMaterialInterface* Effect)
{
    EffectStack.RemoveAll([Effect](const TPair<UMaterialInterface*, float>& Item) {
        return Item.Key == Effect;
    });
}
 
void UPostProcessStackManager::UpdateStack()
{
    APostProcessVolume* PPVolume = Cast<APostProcessVolume>(UGameplayStatics::GetActorOfClass(GetWorld(), APostProcessVolume::StaticClass()));
    if (PPVolume)
    {
        PPVolume->Settings.WeightedBlendables.Array.Empty();
        for (const auto& EffectPair : EffectStack)
        {
            FWeightedBlendable NewBlendable;
            NewBlendable.Object = EffectPair.Key;
            NewBlendable.Weight = 1.0f;
            PPVolume->Settings.WeightedBlendables.Array.Add(NewBlendable);
        }
    }
}

하드웨어 스펙별 포스트 프로세스 설정

 하드웨어 성능에 따라 포스트 프로세스 설정을 자동으로 조정할 수 있습니다.

void AMyGameMode::ConfigurePostProcessForHardware()
{
    UGameUserSettings* Settings = UGameUserSettings::GetGameUserSettings();
    FIntPoint Resolution = Settings->GetScreenResolution();
    int32 PixelCount = Resolution.X * Resolution.Y;
 
    APostProcessVolume* PPVolume = Cast<APostProcessVolume>(UGameplayStatics::GetActorOfClass(GetWorld(), APostProcessVolume::StaticClass()));
    if (PPVolume)
    {
        if (PixelCount > 2073600) // 1080p 이상
        {
            PPVolume->Settings.bOverride_MotionBlurAmount = true;
            PPVolume->Settings.MotionBlurAmount = 1.0f;
        }
        else
        {
            PPVolume->Settings.bOverride_MotionBlurAmount = true;
            PPVolume->Settings.MotionBlurAmount = 0.5f;
        }
 
        // GPU 성능에 따른 추가 설정
        // ...
    }
}

실시간 포스트 프로세싱 기반 게임플레이

 포스트 프로세스 효과를 게임플레이 요소로 활용하면 플레이어의 경험을 크게 향상시킬 수 있습니다.

 다음은 몇 가지 구체적인 예시와 구현 방법입니다.

 1. 플레이어 체력 표현 : 체력이 낮을 때 비네트 효과 강화

 2. 스피드 감각 : 캐릭터 속도에 따른 모션 블러 조절

 3. 수중 효과 : 수중에 있을 때의 색상 변화 및 왜곡 효과

 4. 히트 피드백 : 피격 시 화면 가장자리 효과

 5. 시간 조작 : 슬로우 모션 효과

UCLASS()
class MYGAME_API AMyCharacter : public ACharacter
{
    GENERATED_BODY()
 
public:
    UFUNCTION(BlueprintCallable, Category = "Post Process")
    void UpdatePostProcessEffects();
 
private:
    UPROPERTY(EditAnywhere, Category = "Character")
    float MaxHealth = 100.0f;
 
    UPROPERTY(VisibleAnywhere, Category = "Character")
    float CurrentHealth;
 
    UPROPERTY(EditAnywhere, Category = "Post Process")
    UMaterialInterface* UnderwaterPostProcessMaterial;
 
    UPROPERTY()
    UMaterialInstanceDynamic* DynamicPPMaterial;
 
    bool bIsUnderwater = false;
};
 
void AMyCharacter::UpdatePostProcessEffects()
{
    APostProcessVolume* PPVolume = Cast<APostProcessVolume>(UGameplayStatics::GetActorOfClass(GetWorld(), APostProcessVolume::StaticClass()));
    if (PPVolume)
    {
        // 1. 체력에 따른 비네트 효과
        float HealthPercentage = CurrentHealth / MaxHealth;
        PPVolume->Settings.bOverride_VignetteIntensity = true;
        PPVolume->Settings.VignetteIntensity = FMath::Lerp(1.0f, 0.0f, HealthPercentage);
 
        // 2. 속도에 따른 모션 블러
        float Speed = GetVelocity().Size();
        float MaxSpeed = GetCharacterMovement()->MaxWalkSpeed;
        PPVolume->Settings.bOverride_MotionBlurAmount = true;
        PPVolume->Settings.MotionBlurAmount = FMath::Lerp(0.0f, 1.0f, Speed / MaxSpeed);
 
        // 3. 수중 효과
        if (bIsUnderwater)
        {
            if (!DynamicPPMaterial)
            {
                DynamicPPMaterial = UMaterialInstanceDynamic::Create(UnderwaterPostProcessMaterial, this);
            }
            PPVolume->Settings.WeightedBlendables.Array.Empty();
            FWeightedBlendable NewBlendable;
            NewBlendable.Object = DynamicPPMaterial;
            NewBlendable.Weight = 1.0f;
            PPVolume->Settings.WeightedBlendables.Array.Add(NewBlendable);
        }
        else
        {
            PPVolume->Settings.WeightedBlendables.Array.Empty();
        }
 
        // 4. 히트 피드백 (예: 최근 피격 시간에 따른 효과)
        float TimeSinceLastHit = GetWorld()->GetTimeSeconds() - LastHitTime;
        if (TimeSinceLastHit < 0.5f)
        {
            float HitIntensity = FMath::Lerp(1.0f, 0.0f, TimeSinceLastHit / 0.5f);
            PPVolume->Settings.bOverride_SceneFringeIntensity = true;
            PPVolume->Settings.SceneFringeIntensity = HitIntensity * 5.0f;
        }
        else
        {
            PPVolume->Settings.bOverride_SceneFringeIntensity = false;
        }
 
        // 5. 시간 조작 효과
        if (bIsSlowMotionActive)
        {
            PPVolume->Settings.bOverride_DepthOfFieldFocalDistance = true;
            PPVolume->Settings.DepthOfFieldFocalDistance = 1000.0f;
            PPVolume->Settings.bOverride_DepthOfFieldFstop = true;
            PPVolume->Settings.DepthOfFieldFstop = 2.0f;
        }
        else
        {
            PPVolume->Settings.bOverride_DepthOfFieldFocalDistance = false;
            PPVolume->Settings.bOverride_DepthOfFieldFstop = false;
        }
    }
}

 UpdatePostProcessEffects 함수는 캐릭터의 상태에 따라 다양한 포스트 프로세스 효과를 실시간으로 조정합니다.

 이 함수는 매 프레임 또는 필요에 따라 호출될 수 있습니다.

 6. 스텔스 모드 : 플레이어가 스텔스 모드일 때 윤곽선 강조 효과

void AMyCharacter::ToggleStealthMode()
{
    bIsStealthMode = !bIsStealthMode;
    
    APostProcessVolume* PPVolume = Cast<APostProcessVolume>(UGameplayStatics::GetActorOfClass(GetWorld(), APostProcessVolume::StaticClass()));
    if (PPVolume)
    {
        if (bIsStealthMode)
        {
            PPVolume->Settings.bOverride_AmbientOcclusionIntensity = true;
            PPVolume->Settings.AmbientOcclusionIntensity = 2.0f;
            PPVolume->Settings.bOverride_ColorGradingIntensity = true;
            PPVolume->Settings.ColorGradingIntensity = 0.5f;
        }
        else
        {
            PPVolume->Settings.bOverride_AmbientOcclusionIntensity = false;
            PPVolume->Settings.bOverride_ColorGradingIntensity = false;
        }
    }
}

 7. 환경 상호작용 : 플레이어가 특정 영역에 진입할 때 환경에 따른 시각 효과

UCLASS()
class MYGAME_API AEnvironmentZone : public AActor
{
    GENERATED_BODY()
 
public:
    UPROPERTY(EditAnywhere, Category = "Post Process")
    UMaterialInterface* ZonePostProcessMaterial;
 
    UFUNCTION()
    void OnPlayerEnterZone(AActor* OverlappedActor, AActor* OtherActor);
 
    UFUNCTION()
    void OnPlayerExitZone(AActor* OverlappedActor, AActor* OtherActor);
};
 
void AEnvironmentZone::OnPlayerEnterZone(AActor* OverlappedActor, AActor* OtherActor)
{
    AMyCharacter* PlayerCharacter = Cast<AMyCharacter>(OtherActor);
    if (PlayerCharacter)
    {
        APostProcessVolume* PPVolume = Cast<APostProcessVolume>(UGameplayStatics::GetActorOfClass(GetWorld(), APostProcessVolume::StaticClass()));
        if (PPVolume && ZonePostProcessMaterial)
        {
            UMaterialInstanceDynamic* DynamicMaterial = UMaterialInstanceDynamic::Create(ZonePostProcessMaterial, this);
            PPVolume->Settings.WeightedBlendables.Array.Empty();
            FWeightedBlendable NewBlendable;
            NewBlendable.Object = DynamicMaterial;
            NewBlendable.Weight = 1.0f;
            PPVolume->Settings.WeightedBlendables.Array.Add(NewBlendable);
        }
    }
}
 
void AEnvironmentZone::OnPlayerExitZone(AActor* OverlappedActor, AActor* OtherActor)
{
    AMyCharacter* PlayerCharacter = Cast<AMyCharacter>(OtherActor);
    if (PlayerCharacter)
    {
        APostProcessVolume* PPVolume = Cast<APostProcessVolume>(UGameplayStatics::GetActorOfClass(GetWorld(), APostProcessVolume::StaticClass()));
        if (PPVolume)
        {
            PPVolume->Settings.WeightedBlendables.Array.Empty();
        }
    }
}

 이러한 실시간 포스트 프로세싱 효과를 게임플레이와 연동함으로써, 플레이어에게 더욱 몰입감 있는 경험을 제공할 수 있습니다.

 캐릭터의 상태, 환경 변화, 특수 능력 등 다양한 게임플레이 요소를 시각적으로 강화하여 게임의 전반적인 품질을 향상시킬 수 있습니다.

 주의할 점은 과도한 포스트 프로세스 효과 사용은 성능에 영향을 줄 수 있으므로, 적절한 최적화와 함께 사용해야 합니다.

 또한 플레이어가 효과를 조절하거나 비활성화할 수 있는 옵션을 제공하는 것도 좋은 방법입니다.