[Unreal Engine] 상호작용 하이라이트 구현
상호작용 하이라이트
| 상호작용이 가능한 버튼이 강조되고 있습니다. |
Phil in the Mirror는 1인칭 게임으로, 각종 물체와 상호작용할 수 있는데요. 플레이어가 특정 물체와 상호작용할 수 있는 상태임을 알리기 위해, 해당 물체를 바라보면 물체가 강조되는 연출이 필요했습니다. 이는 다양한 방법으로 구현될 수 있는데, 제가 구현한 방법을 말씀 드리려고 합니다.
무슨 방법으로 구현할 수 있을까요?
특히, 저는 강조될 때 각 물체가 자연스럽게 fade-in, fade-out되는 효과를 원했습니다. 여러 개가 동시에 강조될 수도 있고, 마우스가 스치는 시점에 따라서 각자가 서로 다른 주기로 fade-in, fade-out이 되었으면 했죠. 이런 효과를 구현하기 위해서는 크게 두 가지 방법으로 구현할 수 있을 것 같습니다:
마스터 머티리얼 이용하기
모든 상호작용 가능한 물체는 마스터 머티리얼을 사용하고, 해당 머티리얼에서는 머티리얼 파라미터를 통해 강조의 정도를 조절할 수 있게 만드는 것입니다. 만약 Interactable한 객체가 앞에 있다면, 해당 객체의 머티리얼 파라미터를 0에서 1로 점진적으로 조절하는 것이죠. 제가 구현한 방법은 이것이 아니기 때문에, 자세한 코드 내용을 담기는 어렵습니다만 아마 다음과 같은 형태로 이루어질 것입니다:
APlayerCharacter::CheckHighlight()
{
if (CanInteract() == false)
{
return;
}
AActor* InteractableActor = GetInteractableActor();
...
InteractableActorDynamicMaterial
->SetScalarParameterValue(FName("HighlightIntensity"), FadeValue);
}
그리고 마스터 머티리얼에서는 HighlightIntensity를 가지고 강조되는 시각효과를 연출합니다. 0인 경우 그냥 기존의 물체 모습을 보이면 되고, 1인 경우 하얗게 강조되는 시각효과를 연출하면 될 것입니다.
다만 여기서 신경써야 할 점이 있는데요. 각 액터가 자신만의 HighlightIntensity를 가지고 있기 때문에, 이에 대한 조절을 유심히 해주어야 합니다. 가령 강조되고 있던 액터를 더이상 바라보지 않을 때, 해당 액터의 HighlightIntensity를 자연스럽게 0으로 조절해주어야 할 것입니다. 따라서 이런 액터들에 대한 관리가 필요할 것입니다.
하지만 Phil in the Mirror는 이 방식과는 어울리지 않았는데, 외부 3D 모델 에셋들을 많이 사용하였어서 마스터 머티리얼을 구축하는 것이 꽤 공수가 드는 일이었기 때문입니다. 만약 모든 에셋이 작업 인원들 통제 하에 있는 규모의 프로젝트라면, 이 방법이 훨씬 더 직관적이고 좋은 선택이 될 것입니다.
포스트 프로세스 이용하기
다른 방법은 포스트 프로세스를 이용하여 해결하는 것입니다. 하이라이트된 액터는 자신을 스텐실 버퍼에 렌더링하고, 스텐실 버퍼 값을 이용하여 하이라이트 여부를 결정하는 것입니다. 언리얼 엔진에서는 커스텀 스텐실 값을 지원하며, 이를 이용해서 다양한 작업을 처리할 수 있습니다.
간단하게, 특정 스텐실 값으로 렌더링된 영역은 강조되는 액터가 있다고 생각하고 화면의 픽셀에 일종의 tint 따위를 주는 것입니다. 하지만 단순히 이것만을 이용하기에는 문제가 있는데, 다음의 요구사항을 만족해야 되기 때문입니다:
- 강조에는 fade-in, fade-out이 적용되어야 됩니다.
- 강조되는 물체는 여러개일 수 있으며, 이들의 fade 수치는 독립적으로 관리되어야 합니다.
이는 단순하지 않습니다: 둘 이상의 액터가 강조되고 있다고 생각해볼 때, 화면에서 각 영역이 서로 다른 HighlightIntensity를 갖고 렌더링되어야 하기 때문이죠. 즉, "어떤 위치의 픽셀"이 "어느 정도의 강조 수준"을 갖는지를 화면에서 알 수 있어야 합니다. 스텐실 값은 8비트 값이기 때문에, 이중 하나의 값만을 결정할 수 있을 것입니다. 따라서 하나의 장치가 더 필요합니다.
| 서로 다른 스텐실 값으로 렌더링된 두 액터가 있습니다. 이 둘은 서로 다른 HighlightIntensity 값을 가질 수 있는데, 위 스텐실 값 정보만으로는 그것이 얼마인지 결정할 수 없습니다. 서로가 구분된다는 것만을 알 수 있을 뿐이죠. |
그래서 저는 별도의 텍스처를 이용해서 각 액터의 현재 HighlightIntensity가 어떤 상태인지를 머티리얼 파라미터를 통해 전달하기로 했습니다. 대략적인 절차는 다음과 같습니다:
- 상호작용이 가능한 상태가 될 시 상호작용 관련 서브시스템에 본인을 등록합니다.
- 해당 서브시스템은 액터 등록 시 고유한 ID를 반환해 줍니다.
- 해당 ID로 커스텀 스텐실 값을 적용하여 렌더링합니다.
- 서브시스템은 매 틱마다 등록된 액터들의 fade값을 참조하여 텍스처에 계속 값을 갱신합니다.
- 해당 텍스처는 월드의 포스트 프로세스 머티리얼 인스턴스에 전달됩니다.
- 포스트 프로세스 머티리얼에서는 화면의 스텐실 값을 확인한 후 1 이상의 값인 경우에 텍스처를 샘플링하여 얻은 값만큼 강조 효과를 적용합니다.
FadeProgressTexture
FadeProgressTexture는 상호작용 강조의 정도를 저장하는 텍스처의 이름입니다. 저는 최대 7개의 액터가 상호작용 강조가 될 수 있도록, 8x1의 텍스처로 구성하였습니다. 첫번째 픽셀은 항상 0의 값입니다. 왜냐하면, 스텐실 값이 0인 경우(아무런 액터도 스텐실 버퍼에 값을 기록하지 않은 경우)에도 일관적으로 FadeProgressTexture의 값을 sample하여 상호작용 강조의 정도를 결정할 수 있도록 하기 위해서였습니다. 어차피 첫번째 픽셀의 값은 항상 0이므로, 이 경우에는 상호작용 강조가 이루어지지 않을 것입니다.
void UInteractionFeedbackSubsystem::OnWorldBeginPlay(UWorld& InWorld) { Super::OnWorldBeginPlay(InWorld); ... // 만약 텍스처가 생성되지 않았다면 생성합니다. if (!FocusProgressTexture) { FocusProgressTexture = UTexture2D::CreateTransient(MAX_NUM_INTERACTION_FEEDBACK, 1, PF_R8); if (!FocusProgressTexture) { UE_LOG(LogTemp, Error, TEXT("FocusProgressTexture 생성 실패")); return; } FocusProgressTexture->Filter = TF_Nearest; FocusProgressTexture->SRGB = false; FocusProgressTexture->AddToRoot(); FocusProgressTexture->UpdateResource(); } } void UInteractionFeedbackSubsystem::UpdateFocusProgressTexture() { // 텍스처의 Mip 데이터 업데이트 FTexture2DMipMap& Mip = FocusProgressTexture->GetPlatformData()->Mips[0]; uint8* MipData = static_cast<uint8*>(Mip.BulkData.Lock(LOCK_READ_WRITE)); // 0번은 0의 값으로 고정합니다. // 스텐실 버퍼 값이 비어있으면 0번 픽셀을 샘플링하게 될 것이기 때문입니다. MipData[0] = 0; for (int32 i = 1; i < RegisteredComponents.Num(); ++i) { if (!RegisteredComponents[i].IsValid()) { MipData[i] = 0; continue; } const uint8 PixelValue = static_cast<uint8>(FMath::Clamp(RegisteredComponents[i]->GetFocusProgress(), 0.0f, 1.0f) * 255.0f); MipData[i] = PixelValue; } Mip.BulkData.Unlock(); FocusProgressTexture->UpdateResource(); }
머티리얼 구성
우선, 픽셀의 스텐실 버퍼 값을 기반으로, FadeProgressTexture를 sample합니다.
| 커스텀 스텐실 버퍼의 값을 읽으면, 0부터 7 사이의 값을 얻게 됩니다. 이를 0.5를 더한 뒤 8로 나누어서 적절한 위치의 픽셀의 중앙을 sample하도록 만듭니다. FadeProgress의 (0,0) 픽셀은 항상 0이므로, 스텐실 버퍼의 값이 비어있다면(0이라면) FadeProgress는 항상 0이 됩니다. |
이렇게 얻어진 값을 기반으로, 강조 효과를 처리합니다.
| 현재 픽셀 씬 컬러와 반전된 색을 이용하여 강조가 되도록 합니다. Sine 파동을 이용해서 약간의 반짝거림을 주었습니다. FadeProgress는 이전의 FadeProgressTexture로부터 얻어진 값입니다. |
이렇게 얻어진 값은 바로 활용되는 것이 아니라, 씬 뎁스와 씬 커스텀 뎁스(커스텀 스텐실 값과 함께 기록됩니다)를 비교하여, 커스텀 뎁스와 같은 경우에만 활용합니다. 그렇지 않은 경우 가려진 상태일 것이기 때문입니다.
| 차이가 0.01 이하인 경우에만 InteractionFeedbackColor를 렌더링합니다. |
결과
| 상호작용이 가능한 버튼이 강조되고 있습니다. |
최대 7개의 구분된 액터가 서로 다른 FadeProgress를 가지고 강조될 수 있습니다. 마우스가 움직이면서 순간적으로 스칠 때에도, 자연스럽게 상호작용 강조가 묻어났다가 사라지는 것을 볼 수 있습니다.
상술하였듯이, 모든 모델을 마스터 머티리얼로서 관리할 수 있는 상황이라면 굳이 포스트 프로세싱을 이용할 필요는 없습니다. 다른 용도로 사용될 수 있는 커스텀 스텐실 버퍼를 차지하게 되고, 포스트 프로세싱에 대한 비용도 무조건 지불하게 되는 것이기 때문입니다. 이는 일반적으로 좋지 않은 선택이 될 수 있지만, 제 경우에 충분히 가치가 있는 교환이었기 때문에 시도해볼 수 있었습니다.
끝.
댓글
댓글 쓰기