본문 바로가기

사용자 정의 데이터 인터페이스

C++ 데이터 인터페이스의 클래스·함수·GPU 지원 구조를 이해하고 게임 고유 데이터를 나이아가라 입력으로 노출합니다.

데이터 인터페이스의 기본 개념에서 스태틱 메시와 스켈레탈 메시, 스플라인 같은 엔진의 데이터를 나이아가라가 읽게 해 주는 데이터 인터페이스를 살펴봤습니다. 이런 기본 데이터 인터페이스만으로도 많은 효과를 만들 수 있지만, 게임만의 로직이나 외부 라이브러리에서 오는 데이터를 써야 할 때가 있습니다. 직접 만든 물리 시뮬레이션의 결과나 게임 고유의 날씨 시스템 값은 엔진이 미리 알 수 없기 때문입니다. 이럴 때 C++로 직접 만드는 것이 사용자 정의 데이터 인터페이스(Custom Data Interface)입니다. 이 글에서는 사용자 정의 데이터 인터페이스가 어떤 구조로 동작하는지, 그리고 만들기 전에 무엇부터 정해야 하는지를 개념 중심으로 살펴봅니다. 완성된 C++ 구현을 따라 치는 예제는 아닙니다.


나이아가라에 새 입력 통로를 더한다

사용자 정의 데이터 인터페이스는 개발자가 C++로 직접 구현해, 나이아가라가 기본으로 지원하지 않는 데이터를 읽거나 쓰게 하는 확장 기능입니다. 나이아가라에 새로운 감각 기관을 하나 달아, 게임 세계의 특수한 정보를 알아차리고 반응하게 만드는 것과 비슷합니다.

이런 데이터 인터페이스가 필요한 상황은 대개 다음과 같습니다. 게임 안에 직접 만든 물리 엔진이나 유체 시뮬레이션이 있고 그 결과를 파티클에 적용하고 싶을 때, AI 캐릭터의 지금 판단이나 상태에 따라 효과를 바꾸고 싶을 때, 날씨나 자원 시스템처럼 게임 고유의 시스템에서 오는 복잡한 수치를 파티클에 반영하고 싶을 때, 그리고 CSV나 JSON 같은 파일에 저장된 데이터를 파티클에 쓰고 싶을 때입니다.

이 기능을 구현하려면 C++의 기초와 언리얼 엔진의 모듈과 클래스를 만드는 방법을 알아야 합니다. 나이아가라의 기능 가운데 가장 깊은 수준에 속하므로, 앞에서 배운 기본 개념을 충분히 익힌 뒤에 시도하는 것이 좋습니다.


C++ 쪽과 나이아가라 쪽

사용자 정의 데이터 인터페이스는 두 영역에 걸쳐 동작합니다.

C++ 쪽에서는 UNiagaraDataInterface를 상속하는 클래스를 만들고, 나이아가라가 바깥 데이터를 어떻게 읽고 쓸지를 그 안에 정합니다. 데이터 인터페이스가 내보일 함수, 예를 들어 GetMyCustomValue()나 SetMyCustomState() 같은 함수를 정의하고, 이 함수들을 나이아가라 스크립트에서 부를 수 있도록 등록합니다. 필요하면 복잡한 데이터 구조를 다루거나 외부 라이브러리와 연결하는 코드도 여기에 씁니다.

나이아가라 쪽에서는 등록한 데이터 인터페이스 타입을 모듈 그래프의 입력으로 쓰고, 그 입력에서 내보인 함수를 부릅니다. 데이터 인터페이스 클래스를 만들었다고 Emitter Update의 모듈 목록에 저절로 나타나지는 않습니다. 스크립트 에디터에서 함수의 입력과 출력을 계산에 연결하고, 그 결과를 파라미터 맵을 거쳐 입자 속성에 씁니다.

DI 함수의 CPU와 GPU 구현 경로

NIAGARA · CH4-5

CPU와 GPU의 DI 실행 경로를 각각 구현합니다

DI가 제공하는 함수의 이름·타입·지원 대상을 먼저 정합니다. 아래 갈림은 구현 선택을 나타내며 CPU C++ 함수가 GPU에서 자동 실행된다는 뜻이 아닙니다.

DI 함수의 CPU와 GPU 구현 경로외부 데이터와 Niagara 함수 계약을 정한 뒤 지원할 CPU VM 및 GPU 실행 경로를 각각 구현합니다. CPU 함수만 작성해도 GPU에서 실행되는 것은 아닙니다.외부 데이터소유자 · 갱신 주기 · 수명DI 함수 계약이름 · 자료형 · 지원 Sim TargetCPU VMC++ 함수 바인딩인스턴스 데이터GPUHLSL 함수셰이더 데이터
DI 함수의 CPU와 GPU 구현 경로외부 데이터와 Niagara 함수 계약을 정한 뒤 CPU VM 및 GPU 실행 경로를 각각 구현한다. 두 갈림은 독립적인 지원 경로이며 CPU C++ 함수가 GPU에서 자동 실행되거나 CPU에서 GPU로 순서대로 호출되는 뜻이 아니다.외부 데이터소유자 · 갱신 주기데이터 수명DI 함수 계약이름 · 자료형지원 실행 대상CPU VMC++ 함수 바인딩인스턴스 데이터GPUHLSL 함수셰이더 데이터

CPU 경로

CPU VM의 외부 함수 바인딩과 인스턴스 데이터 수명·접근을 관리합니다. 게임 객체를 임의의 실행 스레드에서 직접 읽지 않습니다.

GPU 경로

HLSL 함수와 셰이더 파라미터·리소스 전달을 별도로 구현합니다. 렌더 스레드 프록시의 데이터 전달 책임을 구분합니다.

지원 범위

CPU 전용이라면 GPU 대상의 사용을 거부합니다. 두 대상을 지원한다면 빈 데이터·소스 수명·갱신 주기와 결과를 각각 확인합니다.

이 장은 구조의 개요입니다. 완성된 DI 구현이나 Unreal 프로젝트 빌드 성공을 나타내지 않습니다.

실제 오버라이드 서명과 함수 등록 방식은 사용 중인 엔진 버전의 헤더를 따릅니다. C++라는 이유로 비용이 줄어드는 것은 아니며 호출·복사·GPU 업로드를 측정합니다.

위 그림은 데이터 인터페이스 함수가 CPU와 GPU에서 실행되는 경로를 보여 줍니다. 맨 위의 외부 데이터에서는 그 데이터를 누가 가지고 있는지, 얼마나 자주 갱신되는지, 언제까지 살아 있는지를 정합니다. 그 아래에서는 데이터 인터페이스가 내보일 함수의 이름과 자료형, 지원할 시뮬레이션 대상(Sim Target)을 정하는데, 이것이 나이아가라 스크립트와 C++ 사이의 약속이 됩니다. 이 약속을 정한 뒤 경로가 둘로 갈립니다. 왼쪽의 CPU VM 경로는 C++ 함수를 바인딩하고 인스턴스 데이터의 수명과 접근을 관리하며, 오른쪽의 GPU 경로는 HLSL 함수와 셰이더 데이터를 따로 구현합니다.

그림이 강조하는 것은 두 갈래가 서로 독립된 지원 경로라는 점입니다. CPU에서 쓰는 C++ 함수가 GPU에서 저절로 실행되는 것도 아니고, CPU에서 GPU로 차례로 호출되는 것도 아닙니다. 그림 아래의 설명처럼 CPU 경로에서는 게임 객체를 아무 실행 스레드에서나 직접 읽지 않고, GPU 경로에서는 렌더 스레드의 프록시가 데이터를 넘기는 역할을 구분합니다. CPU 전용이라면 GPU 대상에서 쓰려는 것을 거부해야 하고, 두 대상을 모두 지원한다면 빈 데이터와 원본의 수명, 갱신 주기, 결과를 각각 확인합니다. 실제로 재정의할 함수의 서명과 등록 방식은 쓰는 엔진 버전의 헤더를 따릅니다.


구현의 큰 흐름

사용자 정의 데이터 인터페이스를 만들어 쓰는 일반적인 흐름을 간단히 살펴봅니다. 실제 코드를 작성하려면 별도의 깊은 학습이 필요합니다.

먼저 프로젝트가 C++ 프로젝트인지 확인합니다. 블루프린트로만 시작한 프로젝트라면 새 C++ 클래스를 하나 추가해 C++ 프로젝트로 바꿉니다. 다음으로 프로젝트나 플러그인의 C++ 모듈에 UNiagaraDataInterface를 상속하는 클래스를 만듭니다. 클래스를 만드는 창에 이 부모 클래스가 보인다고 가정하지 말고, 쓰는 버전의 엔진에 들어 있는 데이터 인터페이스 구현과 헤더를 참고합니다. 나이아가라 모듈에 대한 의존성과 헤더, 노출 설정도 확인하고, 클래스 이름은 MyCustomDataInterface처럼 역할이 드러나게 짓습니다.

그다음 헤더(.h)와 소스(.cpp) 파일에 로직을 씁니다. 헤더에서는 데이터 인터페이스가 가질 속성을 UPROPERTY로 선언하고, 에디터에서 편집해야 하는 속성에는 EditAnywhere 같은 알맞은 편집 지정자와 메타데이터를 붙입니다. 이 매크로와 지정자는 C++ 클래스와 언리얼 리플렉션 시스템에서 다룹니다. UPROPERTY만 적었다고 모든 속성을 디테일 패널에서 편집할 수 있게 되는 것은 아닙니다. 나이아가라 함수의 서명을 등록할 때는 지원할 시뮬레이션 대상을 함께 밝히며, UFUNCTION(CallInNiagaraScript) 같은 지정자 하나로 함수를 자동으로 노출하는 방식은 쓰지 않습니다. 또 CPU의 인스턴스 데이터와, GPU 경로가 필요할 때 렌더 스레드에서 쓰는 FNiagaraDataInterfaceProxy의 역할을 구분하고, 게임 스레드의 객체를 아무 실행 스레드에서나 직접 읽지 않습니다.

소스 파일에서는 헤더에 선언한 함수와 속성의 실제 구현을 씁니다. 데이터를 읽고 쓰는 방식을 정하고 필요하면 외부 시스템과 통신합니다. 현재 API에서는 GetFunctionsInternal로 함수 서명을 제공하고, GetVMExternalFunction으로 CPU VM의 호출을 실제 함수에 연결하며, CanExecuteOnTarget이 돌려주는 값은 실제로 구현한 실행 대상과 맞춥니다. GPU를 지원하려면 HLSL 함수를 만들어 내는 코드와 셰이더 파라미터, 리소스를 넘기는 코드를 따로 구현해야 합니다. CPU용 C++ 함수가 GPU 코드로 저절로 바뀌지 않으며, 구체적인 재정의 서명은 쓰는 엔진 버전의 헤더를 따릅니다.

코드를 고친 뒤에는 에디터를 닫고 Visual Studio 같은 IDE에서 프로젝트를 빌드합니다. 빌드가 성공하면 엔진이 새 데이터 인터페이스를 알아봅니다. 마지막으로 나이아가라 쪽에서 그 데이터 인터페이스 타입을 입력으로 받는 모듈을 만들고, 모듈 그래프에서 입력을 읽어 등록한 함수를 부른 뒤 결과를 필요한 속성에 씁니다. 그 모듈을 지원되는 실행 그룹에 넣고, 데이터 인터페이스 인스턴스의 설정과 원본, CPU와 GPU 대상을 확인합니다.


직접 만들면 좋아지는 점

사용자 정의 데이터 인터페이스를 쓰면 프로젝트가 가진 데이터를 필요한 함수 모양으로 나이아가라에 내보일 수 있습니다. 다만 그 데이터를 가진 객체의 수명과, 어느 스레드와 실행 대상에서 접근할 수 있는지는 지켜야 합니다.

처리 비용을 조절할 여지도 생깁니다. 필요한 값을 미리 계산하거나 묶어서 넘길 수 있기 때문입니다. 그러나 C++로 만들었다는 이유만으로 빨라지지는 않으므로, 호출 횟수와 복사, GPU로 올리는 비용을 측정해야 합니다. 그리고 게임 고유의 시스템과 데이터 구조를 파티클 효과에 직접 이어, 게임 플레이와 시각 효과를 깊이 연결할 수 있습니다.


작은 입력부터 정하기

사용자 정의 데이터 인터페이스를 만들기 전에, 정말 필요한지부터 따져 봅니다. 날씨 시스템이 모든 입자에 같은 강도 0.6을 넘기는 경우라면 Float 사용자 파라미터 하나로 충분한지 먼저 검토합니다. 반면 입자 위치마다 온도장을 물어봐야 한다면, 기존의 격자나 배열, 데이터 인터페이스로 해결할 수 있는지 살펴보고, 부족한 함수만 사용자 정의 데이터 인터페이스로 더합니다. C++ 클래스를 만들 수 있는지보다 어떤 입력으로 어떤 값을 어느 실행 대상에서 읽을지를 먼저 정해야 합니다.

파일의 데이터를 쓸 때도 비슷합니다. 파일은 미리 읽어 가공한 뒤 넘기는 방법을 먼저 검토하고, 입자마다 파일을 읽는 일은 하지 않습니다.

구현 범위를 정할 때는 다음 항목을 확인합니다.

  • 함수의 이름과 입출력 타입, 단위, 갱신 주기를 작게 정합니다.
  • 데이터가 비어 있을 때, 파일 읽기에 실패했을 때, 원본 액터가 사라졌을 때, 비활성화했다가 다시 켤 때 어떻게 동작할지 정합니다.
  • CPU 전용이라면 그렇다고 밝히고 GPU 대상에서 잘못 쓰는 것을 거부합니다. 두 대상을 모두 지원한다면 결과를 각각 비교합니다.
  • 미리보기와 실제 플레이에서 원본의 수명과 스레드 사이의 전달, 비용을 확인합니다.

정리

  • 사용자 정의 데이터 인터페이스는 UNiagaraDataInterface를 상속한 C++ 클래스로, 나이아가라가 기본으로 지원하지 않는 데이터를 읽고 쓰게 합니다.
  • 함수의 서명은 GetFunctionsInternal로 제공하고, CPU 호출은 GetVMExternalFunction으로 연결하며, CanExecuteOnTarget은 구현한 대상과 맞춥니다.
  • GPU 지원은 HLSL 함수와 셰이더 파라미터, 리소스 전달을 따로 구현해야 하며, CPU 함수가 저절로 GPU에서 실행되지 않습니다.
  • 나이아가라에서는 그 타입을 입력으로 받는 모듈을 만들어 함수를 부르고, 결과를 파라미터 맵으로 입자 속성에 씁니다.
  • 만들기 전에 사용자 파라미터나 기존 데이터 인터페이스로 충분한지 확인하고, C++라는 이유로 빠르다고 가정하지 않습니다.

연습 문제

1. CPU 전용 온도 함수를 GPU에서

위치를 받아 섭씨 온도를 돌려주는 데이터 인터페이스를 CPU 전용으로 만들었습니다. CPU 함수 서명 등록과 VM 바인딩만 구현했고, HLSL이나 셰이더 데이터 전달은 구현하지 않았습니다. GPU 이미터에서 같은 함수를 쓰려면 지원 대상을 true로 바꾸는 것만으로 충분할까요? 모든 입자가 같은 온도 하나만 필요하다면 더 작은 전달 방법도 설명해 보세요.

해설 보기

충분하지 않습니다. 지원 대상의 선언을 바꿔도 GPU에서 실행할 코드와 데이터 전달은 생기지 않습니다. 지금처럼 CPU 전용으로 만든 데이터 인터페이스라면 GPU에서 쓰는 것을 거부해야 합니다. GPU를 지원하려면 쓰는 엔진 버전에 맞춰 HLSL 생성과 셰이더 파라미터, 리소스 전달을 실제로 구현하고, 두 대상의 결과를 비교해야 합니다.

모든 입자가 같은 온도 하나만 필요하다면, 게임 로직에서 온도를 계산해 Float 사용자 파라미터로 넘기고 모듈이 그 값을 읽는 방법부터 검토합니다. 이 글은 이런 선택과 구현의 범위를 설명할 뿐 완성된 C++ 코드를 제공하지 않습니다.

2. 입자마다 파일을 읽는 설계

CSV 파일에 저장된 바람의 세기를 입자마다 읽어 쓰려고, 데이터 인터페이스 함수 안에서 호출될 때마다 파일을 열어 읽도록 만들었습니다. 이 설계의 문제는 무엇이고 어떻게 바꾸면 좋을까요?

해설 보기

입자마다, 또 틱마다 파일을 열면 입자 수와 프레임 수에 비례해 파일 입출력이 일어나 비용이 매우 커집니다. 파일 읽기는 실패할 수도 있어서, 실패할 때마다 시뮬레이션이 영향을 받습니다.

파일은 효과가 시작되기 전이나 데이터가 바뀔 때 한 번 읽어 가공해 두고, 데이터 인터페이스는 메모리에 올려 둔 값을 돌려주게 바꿉니다. 그리고 파일 읽기에 실패했을 때 어떤 값을 돌려줄지 미리 정해 둡니다.

3. 사라진 액터를 읽는 함수

데이터 인터페이스가 레벨의 특정 액터에서 값을 읽도록 만들었는데, 그 액터가 파괴된 뒤 효과가 이상하게 동작하거나 에디터가 멈춥니다. 무엇을 미리 정해 두었어야 할까요?

해설 보기

원본 객체가 사라졌을 때의 동작을 정해 두었어야 합니다. 데이터 인터페이스는 원본이 유효한지 확인하고, 사라졌다면 정해 둔 기본값을 돌려주거나 함수가 실패했음을 알리도록 만들어야 합니다.

또 게임 스레드의 액터를 다른 실행 스레드에서 직접 읽지 않고, 인스턴스 데이터나 프록시로 값을 옮겨 두는 구조인지 확인합니다. 비활성화했다가 다시 켤 때 원본을 다시 찾는지도 함께 정해 둡니다.

재정의할 함수와 클래스 구조는 Epic의 UNiagaraDataInterface API 문서와 쓰는 버전의 실제 헤더를 기준으로 확인합니다.

이것으로 4장을 마칩니다. 4장에서는 데이터 인터페이스가 바깥 데이터를 읽는 구조에서 시작해, 사용자 파라미터로 게임 중에 값을 넘기는 방법, 커브와 그래디언트로 수명에 따라 값을 바꾸는 방법, 메시의 표면에 입자를 만드는 방법, 그리고 C++로 직접 만드는 데이터 인터페이스의 구조까지 효과 바깥의 데이터를 다루는 방법을 살펴봤습니다. 다음 장의 나이아가라 스크립트 기본 개념부터는 노드 그래프로 수학과 조건, 반복을 다루며 입자 속성을 직접 읽고 쓰는 나이아가라 스크립트를 익힙니다.