목표

라인 트레이스로 감지된 상호작용 대상(CurrentInteractable)에 시각적으로 테두리를 강조해서, 플레이어가 무엇과 상호작용 가능한지 한눈에 알 수 있게 만들고 싶었다.

Custom Depth + 경계 감지

언리얼은 특정 메시만 별도의 깊이 버퍼(Custom Depth)에 렌더링할 수 있는 기능을 제공한다

이 버퍼에서 한 픽셀과 그 옆 픽셀의 깊이 값을 비교하면 값이 갑자기 달라지는 지점 = 오브젝트의 경계(외곽선)를 찾을 수 있다

설정

  • Project Settings → Rendering → Postprocessing → Custom Depth-Stencil Pass를 Enabled with Stencil로 설정 (에디터 재시작 필요)
  • 레벨에 Post Process Volume 배치, Infinite Extent (Unbound) 체크 (위치 제한 없이 항상 적용)


  • Volume의 Post Process Materials에 직접 만든 PP_Outline 머티리얼 등록

머티리얼 그래프 구성

Material Domain을 Post Process로 설정한 새 머티리얼에서

 

  1. 현재 픽셀의 CustomDepth오프셋만큼 이동한 이웃 픽셀의 CustomDepth, 두 값을 각각 SceneTexture(CustomDepth) 노드로 샘플링
    • 이웃 픽셀 좌표는 ScreenPosition(ViewportUV) + Add(작은 오프셋 값)으로 계산
  2. Subtract로 두 값의 차이 계산 → Abs로 절댓값
  3. Step(threshold, 차이값)으로 "차이가 threshold보다 크면 경계(1), 아니면 배경(0)"으로 이분화
  4. Lerp(원본화면색, 외곽선색, Step결과)로 최종 색 결정, Emissive Color에 연결

C++ 연동

ABaseItem을 상속하는 아이템들은 각자 메시 구성이 다르다

Door나 DarkLetter는 단일 메시면 충분하지만 Desk는 DeskMesh(고정된 책상 본체)와 DrawerMesh(움직이는 서랍) 두 개의 별도 컴포넌트를 가지고 있다

외곽선을 책상 본체 전체에 그리면 상호작용 대상이 모호해지므로 외곽선을 그릴 메시가 무엇인지를 각 아이템 클래스가 직접 정하도록 가상 함수로 분리했다

// BaseItem.h
virtual UStaticMeshComponent* GetOutlineMesh() const { return StaticMesh; }

// Desk.h
virtual UStaticMeshComponent* GetOutlineMesh() const override { return DrawerMesh; }

 

ATheRoomCharacter::Tick() : 라인 트레이스, 컴포넌트 구분, 외곽선 토글 통합

void ATheRoomCharacter::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
    
    FVector Start = FirstPersonCameraComponent->GetComponentLocation();
    FVector End = Start + (FirstPersonCameraComponent->GetForwardVector() * 700.0f);
    FHitResult HitResult;
    FCollisionQueryParams QueryParams;
    QueryParams.AddIgnoredActor(this);
    
    bool bHit = 
       GetWorld()->LineTraceSingleByChannel(HitResult, Start, End, ECC_Visibility, QueryParams);
    
    ABaseItem* HitItem = nullptr;
    
    if (bHit)
    {
       ADesk* HitDesk = Cast<ADesk>(HitResult.GetActor());
       if (HitDesk)
       {
          if (HitDesk->IsHitComponentDrawer(HitResult.GetComponent()))
          {
             HitItem = HitDesk;
          }
       }
       else
       {
          HitItem = Cast<ABaseItem>(HitResult.GetActor());
       }
    }
    
    if (HitItem != CurrentInteractable)
    {
       if (CurrentInteractable)
       {
          HideInteraction();
          if (UStaticMeshComponent* OutlineMesh = CurrentInteractable->GetOutlineMesh())
          {
             OutlineMesh->SetRenderCustomDepth(false);
          }
       }
       
       CurrentInteractable = HitItem;
       
       if (CurrentInteractable)
       {
          ShowInteraction();
          if (UStaticMeshComponent* OutlineMesh = CurrentInteractable->GetOutlineMesh())
          {
             OutlineMesh->SetRenderCustomDepth(true);
          }
       }
    }
}

 

  1. 라인 트레이스 발사 : 카메라 위치에서 정면 방향으로 700유닛까지 ECC_Visibility 채널로 검사 AddIgnoredActor(this)로 캐릭터 자기 자신은 제외
  2. Desk 특수 처리 : 맞은 액터가 ADesk라면 단순히 Desk에 맞았다로 끝내지 않고 HitResult.GetComponent()로 정확히 어느 컴포넌트(DeskMesh인지 DrawerMesh인지)에 맞았는지 추가로 확인한다
    서랍(DrawerMesh)에 맞았을 때만 HitItem에 대입하고, 책상 본체(DeskMesh)에 맞으면 무시한다
    (DeskMesh의 콜리전을 NoCollision으로 끄지 않으면 트레이스가 본체에서 먼저 막혀버리는 문제 발생함)
  3. 일반 아이템 처리 : Desk가 아니라면 기존처럼 액터 전체를 ABaseItem으로 캐스팅
  4. 상태 변경 감지 : HitItem이 이전 CurrentInteractable과 다를 때만 아래 로직 실행 (매 프레임 동일한 대상을 반복 처리하지 않도록)
  5. 이전 대상 정리 : 안내 UI를 끄고(HideInteraction()), GetOutlineMesh()로 얻은 메시의 SetRenderCustomDepth(false)로 외곽선도 끈다
  6. 새 대상 등록 : CurrentInteractable을 갱신하고, 안내 UI를 켜고(ShowInteraction()), GetOutlineMesh()로 얻은 메시의 SetRenderCustomDepth(true)로 외곽선을 켠다

트러블슈팅

  • 타입 불일치 컴파일 에러: Lerp의 A(float4, Alpha 채널 포함)와 B(float3) 채널 수가 안 맞아 발생 → Mask(R,G,B) 노드로 Alpha 채널을 제거해 해결
  • 외곽선이 아니라 전체가 칠해지는 문제: Step의 threshold(0.001)가 실제 CustomDepth 값 스케일에 비해 너무 작아서 거의 모든 픽셀이 "경계"로 판정됨 → threshold를 점진적으로 높여(5 → 추가 조정) 적정값을 찾아 해결.

 

 

+ Recent posts