본문 바로가기
Unreal/Manual

Unreal 잔상 효과 (Afterimage), GhostTrail 만들기

by Dev_카페인 2024. 1. 15.
반응형

[Unreal/C++] 잔상 효과 (Afterimage), GhostTrail 만들기

 

언리얼 엔진에서 Animation에 따른 잔상 효과를 구현합니다.

잔상효과는 게임의 비쥬얼을 보기 좋게 만들고 퀄리티를 증진시키는데 한 몫 합니다.

피하기, 텔레포트, 대쉬 등 여러 애니메이션에 사용될 수 있고 언리얼 엔진에서 지원되는 것이 많아 쉽게 구현할 수 있습니다.

 

 

 

구현하기에 앞서 준비물과 구현 결과물을 소개합니다.

준비물

1. Skeletal Mesh가 붙어있는 캐릭터 등

2. 캐릭터 등을 움직일 애니메이션 몽타주 등 (애니메이션이 없어도 구현이 가능합니다.)

결과물

1. GhostTrail Actor Script : 캐릭터에서 Spawn을 통해 생성합니다.

2. 투명한 Material : 캐릭터 Mesh가 투명하게 보이기 위한 재질 입니다.

3. Animation Notify : 애니메이션 몽타주에서 GhostTrail 생성 함수를 호출하기 위해 필요합니다.

    (다른 방식(타이머 등)으로 구현해도 좋습니다.)

 

----------------------------------------------------------------------------------------------------------------------------------------------------------------

 

구현

먼저 Actor를 상속받는 클래스를 하나 만들어줍니다.

클래스의 이름은 GhostTrail로 정하였고 사용자 편의에따라 자유롭게 정하셔도 상관 없습니다.

 

 

생성이 완료되면 생성자와 BeginPlay 함수 그리고 Tick 이 기본적으로 만들어져 있습니다.

전체적인 코드를 작성하기 전 생성먼저 해봅니다.

.h 헤더파일에 UPoseableMeshComponent 변수를 추가해줍니다.

UPROPERTY(VisibleDefaultsOnly)
class UPoseableMeshComponent* PoseableMesh;

.cpp 생성자에서 Component를 생성하고 사용하고 있는 캐릭터의 SkeletalMesh를 찾아서 지정합니다.

// 헤더 파일 추가
#include "Engine/SkeletalMesh.h"
#include "Components/PoseableMeshComponent.h"

AGhostTrail::AGhostTrail()
{
	PrimaryActorTick.bCanEverTick = true;

	// Component 생성
	PoseableMesh = CreateDefaultSubobject < UPoseableMeshComponent>(TEXT("PoseableMesh"));
	// RootComponent 변경
	RootComponent = PoseableMesh;
	// 사용할 SkeletalMesh 지정
	ConstructorHelpers::FObjectFinder<USkeletalMesh> poseMesh(TEXT("SkeletalMesh 경로"));
	PoseableMesh->SetSkeletalMesh(poseMesh.Object);
}

 

SkeletalMesh 경로는 우클릭 후 레퍼런스 복사를 통해 가져옵니다.

 

PoseableMesh->SetSkeletalMesh를 성공적으로 지정하면 아래와 같이 SkeletalMesh가 지정됩니다.

작성이 완료되면 컴파일을 하고 사용하고 있는 메인 캐릭터의 클래스 파일로 이동합니다.

 

GhostTrail을 위한 TSubclassOf 변수를 선언해 줍니다.

private :
TSubclassOf<class AGhostTrail> GhostTrail;

생성자에서 ghostTrail을 넣어줍니다. GhostTrail의 BP 클래스를 만든 후 레퍼런스를 복사해 넣은 것 입니다.

ConstructorHelpers::FClassFinder<AGhostTrail> ghostTrail(L"BP_GhostTrail_C경로");
GhostTrail = ghostTrail.Class;

OnCopyPose() 함수를 만들고 아래와 같이 작성합니다.

void AMyCharacter::OnCopyPose()
{
	FTransform transform(GetMesh()->K2_GetComponentToWorld());
	AGhostTrail* ghostTrail = GetWorld()->SpawnActorDeferred<AGhostTrail>(GhostTrail, transform, this, this, ESpawnActorCollisionHandlingMethod::AlwaysSpawn);

	if (ghostTrail == nullptr) return;
	ghostTrail->FinishSpawning(ghostTrail->GetTransform());
}

 

필요에 따라 OnCopyPose 함수를 호출하면 아래와 같이 지정한 SkeletalMesh가 TPose나 APose로 생성됩니다.

디버깅을 위해 키 바인딩을 하여 호출하는 편이 쉽습니다.

 

여기까지 완료되었다면 애니메이션 몽타주에서 이 함수를 호출하기 위한 애니메이션 노티파이를 생성합니다.

 

// AnimNotify.h
#pragma once

#include "CoreMinimal.h"
#include "Animation/AnimNotifies/AnimNotify.h"
#include "AN_GhostTrail.generated.h"

UCLASS()
class MAINPROJECT_API UAN_GhostTrail : public UAnimNotify
{
	GENERATED_BODY()
	
public :
	FString GetNotifyName_Implementation() const;	// 없어도 돼용

	virtual void Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference) override;
};
// MyAnimNotify.cpp

#include "AN_GhostTrail.h"

#include "MyCharacter.h"

FString UAN_GhostTrail::GetNotifyName_Implementation() const
{
	return "GhostTrail";
}

void UAN_GhostTrail::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference)
{
	if (MeshComp->GetWorld()->WorldType == EWorldType::EditorPreview) return; // 프리뷰에서 실행 안되도록 하는 코드

	MyCharacter* character = Cast<MyCharacter>(MeshComp->GetOwner());
	if (!character) return;
	character->OnCopyPose();
}

 

Notify 클래스를 만들고나면 컴파일을 하고 원하는 애니메이션 몽타주에 노티파이를 추가합니다.

 

노티파이를 추가하면 애니메이션 몽타주 실행시 생성이 편리해진 것을 볼 수 있습니다.

 

이제 애니메이션 Pose를 복사해봅니다.

만들었던 AGhostTrail 클래스에서 함수를 추가합니다.

MyCharacter에서 Spawn 후 SetSkeletalMeshPose를 호출하기 때문에 public으로 선언해줍니다.

public : void SetSkeletalMeshPose(USkeletalMeshComponent* InSkeletalMesh);

.cpp에서는 CopyPoseFromSkeletalComponent를 설정해줍니다.

CopyPoseFromSkeletalComponent는 UPoseableMeshComponent에 정의되어 있습니다.

void AGhostTrail::SetSkeletalMeshPose(USkeletalMeshComponent* InSkeletalMesh)
{ PoseableMesh->CopyPoseFromSkeletalComponent(InSkeletalMesh); }

이 함수를 MyCharacter.cpp에서 만들었던 OpCopyPose() 함수 내에서 호출해줍니다.

호출은 FinishSpawning이 호출되기 전이 좋습니다.

void MyCharacter::OnCopyPose()
{
	FTransform transform(GetMesh()->K2_GetComponentToWorld());
	AGhostTrail* ghostTrail = GetWorld()->SpawnActorDeferred<AGhostTrail>(GhostTrail, transform, this, this, ESpawnActorCollisionHandlingMethod::AlwaysSpawn);

	if (ghostTrail == nullptr) return;
	ghostTrail->SetSkeletalMeshPose(GetMesh());	// 추가
	ghostTrail->FinishSpawning(ghostTrail->GetTransform());
}

컴파일을 하고 실행을 시키면 Animation Pose가 복사된 것을 볼 수 있습니다.

 

 

Material

위에서 보이는 것처럼 형태가 그대로 보이니 어지러운 느낌입니다.

재질을 바꿔 투명하게 만들면 좋을 것 같다는 생각이 듭니다.

머티리얼을 만들어 줍니다. 이름은 M_GhostTrail로 정했지만 편의에 따라 정의하시면 좋습니다.

VectorParameter를 이용해 원하는 컬러를 지정해주고 Fresnel을 사용해 아래와 같이 정의해줍니다.

 

디테일창에서 블랜드 모드를 Translucent로 설정해줍니다.

조금 더 내려 스켈레탈 메시와 사용됨에 체크를 해주고 저장합니다.

필요에 따라 Fresnel 값을 조정합니다.

머티리얼 결과가 투명하게 보이면 저장을 눌러 컴파일해줍니다.

머티리얼 저장이 끝나면 머티리얼 인스턴스를 생성해야 합니다.

 

이제 이 머티리얼을 SkeletalMesh에 적용해야 합니다. 색이 겹치면 잘 안보이니 색상은 잘 조절해야 할 듯 싶습니다.

 

 

생성한 머티리얼 인스턴스를 적용하기 위해 GhostTrail 클래스로 가서 변수를 추가해 줍니다.

UPROPERTY(VisibleDefaultsOnly)
class UMaterialInstance* GhostMaterial;

이전에 만들어 뒀던 함수 SetSkeletalMeshPose에서 material 적용을 위한 코드를 추가해줍니다.

머티리얼이 하나인경우 GetMaterials는 필요 없이 SetMaterial(0, GhostMaterial)로 0번 인덱스의 머티리얼만 변경해주면 됩니다.

void AGhostTrail::SetSkeletalMeshPose(USkeletalMeshComponent* InSkeletalMesh)
{
	PoseableMesh->CopyPoseFromSkeletalComponent(InSkeletalMesh);

	TArray<UMaterialInterface*> materials = PoseableMesh->GetMaterials();

	for (int32 i = 0; i < materials.Num(); i++)
	{
		PoseableMesh->SetMaterial(i, GhostMaterial);
	}
}

TwinBlast의 경우 머티리얼이 많기 때문에 전체를 변경해주기 위해 TArray로 받은 것 뿐입니다.

컴파일을 하면 투명한 메시를 볼 수 있습니다.

 

 

FadeOut

잔상이 계속 남아 있으면 보기 좋지 않으니 서서히 사라지는 FadeOut효과와 액터의 삭제가 필요합니다.

서서히 사라지는 효과를 주기위해 ScalarParameter를 추가하고 이름을 Opacity로 바꿔줍니다.

기존 Fresnel에서 오파시티로 들어가던 연결을 끊고 생성한 ScalarParameter(Opacity)와 Multiply로 연결해줍니다.

Opacity의 디테일을 수정해줍니다.

이 Opacity의 값을 1에서부터 0으로 서서히 줄여나간 후 0이하가 되면 액터를 삭제해줄 예정입니다.

타이머나 틱을 이용해 이 값을 수정하기 위해서는 SetScalarParameterValue를 사용하면 됩니다.

Materials->SetScalarParameterValue("ParamName", Value);

 

GhostTrail 클래스파일에 변수를 추가합니다.

TArray<UMaterialInstanceDynamic*> Materials;  // 머티리얼을 저장하고 값을 변경하기 위한 변수
bool IsSpawned = false;  // 생성이 완료되었는지 확인하는 변수
float FadeCountDown;  // 생성 후 점차 감소되는 타이머
float FadeOutTime = 1.0f;  // 초기 시간

 

// 추가
void AGhostTrail::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	if (IsSpawned)	// 생성 완료 시
	{
		FadeCountDown -= DeltaTime;		// 시간을 점차 줄이기
		for (int i = 0; i < Materials.Num(); i++)
		{
        	// 설정했던 ScalarParameter 값 조정
			Materials[i]->SetScalarParameterValue("Opacity", FadeCountDown / FadeOutTime);
		}
		if (FadeCountDown < 0)
		{
			Destroy();	// 일정 시간 후 파괴
		}
	}

}

void AGhostTrail::SetSkeletalMeshPose(USkeletalMeshComponent* InSkeletalMesh)
{
	PoseableMesh->CopyPoseFromSkeletalComponent(InSkeletalMesh);

	TArray<UMaterialInterface*> materials = PoseableMesh->GetMaterials();

	for (int32 i = 0; i < materials.Num(); i++)
	{
    	// 수정
		Materials.Add(UKismetMaterialLibrary::CreateDynamicMaterialInstance(GetWorld(), GhostMaterial));
		PoseableMesh->SetMaterial(i, Materials[i]);
	}
	//추가
	FadeCountDown = FadeOutTime;	// 쿨타임 초기화
	IsSpawned = true;			// 생성 완료
}

 

 

GhostTrail 전체 코드

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "GhostTrail.generated.h"

class UPoseableMeshComponent;
class UMaterialInstance;

UCLASS()
class MAINPROJECT_API AGhostTrail : public AActor
{
	GENERATED_BODY()
	
public:	
	AGhostTrail();

protected:
	virtual void BeginPlay() override;

public:
	virtual void Tick(float DeltaTime) override;

public :
	void SetSkeletalMeshPose(USkeletalMeshComponent* InSkeletalMesh);

private :
	UPROPERTY(VisibleDefaultsOnly)
		UPoseableMeshComponent* PoseableMesh;

	UPROPERTY(VisibleDefaultsOnly)
		UMaterialInstance* GhostMaterial;

	TArray<UMaterialInstanceDynamic*> Materials;

	bool IsSpawned = false;
	float FadeCountDown;
	float FadeOutTime = 2.0f;

};

 

 

#include "Character/Effects/GhostTrail.h"

#include "Engine/SkeletalMesh.h"
#include "Kismet/KismetMaterialLibrary.h"
#include "Components/PoseableMeshComponent.h"

AGhostTrail::AGhostTrail()
{
	PrimaryActorTick.bCanEverTick = true;

	PoseableMesh = CreateDefaultSubobject < UPoseableMeshComponent>(TEXT("PoseableMesh"));

	RootComponent = PoseableMesh;

	ConstructorHelpers::FObjectFinder<USkeletalMesh> poseMesh(TEXT("SkeletalMesh'/Game/Characters/TwinBlast/Meshes/TwinBlast_ShadowOps.TwinBlast_ShadowOps'"));
	PoseableMesh->SetSkeletalMesh(poseMesh.Object);

	ConstructorHelpers::FObjectFinder<UMaterialInstance> ghostMat(TEXT("MaterialInstanceConstant'/Game/Characters/TwinBlast/Materials/M_GhostTrail_Inst.M_GhostTrail_Inst'"));
	GhostMaterial = ghostMat.Object;
}

void AGhostTrail::BeginPlay()
{
	Super::BeginPlay();
	
}

void AGhostTrail::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	if (IsSpawned)
	{
		FadeCountDown -= DeltaTime;
		for (int i = 0; i < Materials.Num(); i++)
		{
			Materials[i]->SetScalarParameterValue("Opacity", FadeCountDown / FadeOutTime);
		}
		if (FadeCountDown < 0)
		{
			Destroy();
		}
	}

}

void AGhostTrail::SetSkeletalMeshPose(USkeletalMeshComponent* InSkeletalMesh)
{
	PoseableMesh->CopyPoseFromSkeletalComponent(InSkeletalMesh);

	TArray<UMaterialInterface*> materials = PoseableMesh->GetMaterials();

	for (int32 i = 0; i < materials.Num(); i++)
	{
		Materials.Add(UKismetMaterialLibrary::CreateDynamicMaterialInstance(GetWorld(), GhostMaterial));
		PoseableMesh->SetMaterial(i, Materials[i]);
	}

	FadeCountDown = FadeOutTime;
	IsSpawned = true;
}

 

 

 

 

 

 

 

 

 

 

Fresnel 은 관찰자가 바라보는 각도에 따라 반사되는 빛의 세기가 달라지는 현상을 설명하는 데 사용되는 용어입니다. 예를 들어 물 웅덩이 위에 서서 수직으로 내려다보는 경우, 반사되는 수면이 많이 보이지 않을 것입니다. 머리를 움직여 물 웅덩이의 수면이 시선과 평행이 되어갈수록, 수면의 반사면이 많아지는 것이 보일 것입니다.

UE4 에서 Fresnel 머티리얼 표현식 노드는 표면 노멀과 카메라까지의 방향의 내적을 기반으로 감쇠 계산을 합니다. 표면 노멀이 카메라를 바로 향하면 출력값은 0, 즉 발생하는 프레넬 이펙트가 없습니다. 표면 노멀이 카메라에 수직인 경우 출력값은 1, 즉 최대치의 프레넬 이펙트가 발생합니다. 그런 다음 그 결과를 [0,1] 범위로 제한시켜 가운데 음수 컬러가 생기지 않도록 합니다. 다음 그림은 이 개념을 나타냅니다.

구체 중앙의 0 인 부분은 프레넬 이펙트가 없는 것이 보입니다. 왜냐면 카메라가 바로 표면 노멀쪽을 향하고 있기 때문입니다. 표면 노멀과 카메라가 수직이 되어갈 수록, 즉 1 에 가까울 수록 프레넬 이펙트가 더욱 잘 보이게 되는데, 바로 그러한 작동방식 유형이 필요한 것입니다.

 

 

 

 

 

 

 

 

 

 

 

[Unreal BP] 잔상 효과 만들기

방법 1. 액터를 하나 만든다. 2. 컴포넌트에 PoseableMesh를 추가하고, 설정 값을 바꾼다. 3. 이벤트가 들어오면 2번에서 만들었던 액터를 생성한다. (잔상이니까 내 위치에 생성.) 4. 생성하고 난 뒤,

mingyu0403.tistory.com

 

 

머티리얼에 프레넬 사용하기

Fresnel 머티리얼 노드 사용법 안내입니다.

docs.unrealengine.com

 

 

UPoseableMeshComponent

[UPoseableMeshComponent](API\Runtime\Engine\Components\UPoseableMeshComponent) that allows bone transforms to be driven by blueprint.

docs.unrealengine.com

 

반응형