본문 바로가기

스크립트 최적화 기본 기법

불필요한 매 프레임 계산과 정밀도를 줄이고 CPU·GPU 실행, LOD·스케일러빌리티, 프로파일링으로 스크립트 비용을 관리합니다.

나이아가라는 입자 하나하나에 원하는 계산을 마음껏 붙일 수 있는 만큼, 생각 없이 쓰면 성능에 큰 부담을 줍니다. 효과가 복잡해지거나 입자가 많아질수록 스크립트를 다듬는 일이 꼭 필요해집니다. 여기서 최적화란 그저 빠르게 만드는 것이 아니라, 효과가 여러 하드웨어에서 정해진 프레임 시간 예산 안에 동작하도록 맞추는 과정입니다. 이 글에서는 계산을 줄이는 기본 기법과 CPU, GPU 시뮬레이션의 선택, 스케일러빌리티, 그리고 비용을 재는 프로파일링 도구를 살펴보고, 작업 횟수를 줄인 결과를 어떻게 판단할지 숫자로 따져 봅니다.


작은 계산이 쌓이는 이유

나이아가라 시스템은 틱마다 수천, 수만 개의 입자에 수백 가지 계산을 할 수 있습니다. 계산 하나는 사소해 보여도 입자 수와 실행 횟수가 곱해지면 엄청난 양이 됩니다.

그래서 다음과 같은 경우에 최적화가 더 필요해집니다. 동시에 살아 있는 입자가 많으면 입자마다의 계산 부담이 함께 커집니다. 복잡한 수학 연산이나 조건, 데이터 인터페이스 호출이 많은 모듈은 계산 시간을 늘립니다. 모바일 기기나 VR처럼 사양이 넉넉하지 않은 플랫폼에서는 작은 개선도 큰 차이를 만듭니다. 그리고 여러 효과가 한 화면에서 동시에 재생되면 각 효과의 비용이 합쳐져 전체 성능에 영향을 줍니다.


하지 않아도 되는 계산 빼기

가장 기본적인 최적화는 하지 않아도 되는 일을 하지 않는 것입니다. 이미터에 들어 있지만 실제로 아무 기능도 하지 않거나 더 이상 필요 없는 모듈은 지웁니다. 모듈마다 나름의 계산 비용이 들기 때문입니다.

쓰지 않는 기능이 실제로 빠지는지도 확인해야 합니다. Static Switch로 쓰지 않는 기능을 컴파일에서 빼거나, 필요 없는 모듈을 끕니다. 반면 Select나 If 같은 동적인 선택 노드만 붙여 두면, 고르지 않은 값의 계산이 생략된다고 가정할 수 없습니다.

데이터 인터페이스 호출도 줄일 수 있습니다. 데이터 인터페이스는 바깥 데이터를 가져오는 데 비용이 들므로, 필요할 때만 부르고 같은 데이터를 여러 번 가져오기보다 한 번 가져와 여러 곳에서 다시 씁니다.


같은 결과를 더 적은 계산으로

같은 결과를 더 적은 계산으로 얻을 수 있는지도 찾아봅니다. 다만 계산의 뜻을 바꾸면 안 됩니다. Lerp는 두 값 사이를 보간하고 Sine은 주기적으로 반복하는 함수이므로, 비용을 이유로 하나를 다른 것으로 바꾸면 움직임 자체가 달라집니다. 근사를 쓸 때는 어느 정도의 차이까지 허용할지 정하고 실제 비용을 비교합니다.

길이를 비교할 때는 제곱근을 피할 수 있습니다. 거리 d가 0 이상의 기준값 R보다 작은지만 알고 싶다면, 길이를 구하는 대신 LengthSquared(v) < R × R로 비교하면 됩니다. 이때 기준값도 제곱해야 한다는 점을 잊지 않으며, 방향이 필요한 경우에는 안전한 정규화를 씁니다.

계산하는 빈도도 줄일 수 있습니다. 바뀌지 않는 초기값은 Particle Spawn에서 한 번 구하고, 모든 입자에 공통인 값은 가능하면 System이나 Emitter 단계에서 한 번 계산합니다. 상수나 반복된 Map Get은 컴파일러가 알아서 줄여 줄 수 있으므로, 노드의 개수만 보고 비용을 단정하지 않습니다.

커브의 키를 정리하면 편집하기 쉬워집니다. 하지만 실행할 때의 비용은 키의 개수만이 아니라 커브 데이터를 미리 표(LUT)로 구워 두는지와 샘플링 방식에도 달려 있으므로, 프로파일링으로 판단합니다.


CPU와 GPU 가운데 어디서 계산할까

나이아가라는 입자의 시뮬레이션을 CPU에서 할 수도 있고 GPU에서 할 수도 있습니다. 두 방식의 장단점을 알고 상황에 맞게 골라야 합니다.

수천, 수만 개의 입자를 쓰는 큰 효과에는 GPU 시뮬레이션이 유리할 수 있습니다. 이미터의 Sim Target을 GPU Compute Sim으로 바꾸면 됩니다. 다만 목표 하드웨어에서의 GPU 부하와 데이터 전송, 디스패치 비용까지 비교해야 하며, GPU 이미터라도 System과 Emitter 스크립트, 관리 비용은 CPU에 남습니다. 또 GPU 시뮬레이션은 월드와의 정확한 충돌이나 블루프린트와 양방향으로 데이터를 주고받는 일처럼 CPU를 바탕으로 한 기능에 제약이 있을 수 있으므로, 모든 상황에서 GPU가 최선은 아닙니다.

정확한 충돌 처리가 필요하거나, 블루프린트에서 입자 데이터를 읽고 쓰는 복잡한 게임 로직과 이어져 있거나, 입자가 많지 않은 효과에는 CPU 시뮬레이션이 더 알맞을 수 있습니다. 작은 효과라면 CPU 시뮬레이션의 부담이 크지 않으므로 굳이 GPU로 바꿀 필요가 없습니다.


LOD와 스케일러빌리티

효과를 거리나 품질에 따라 줄이려면 나이아가라 이펙트 타입(Effect Type)과 시스템, 이미터의 스케일러빌리티 설정에서 품질 단계와 거리, 가시성에 따른 컬링, 인스턴스 예산, 생성량 배율을 직접 설계해야 합니다. 멀리 있다고 해서 복잡한 효과가 저절로 알맞은 LOD로 바뀌지는 않습니다.

설계할 때 챙길 것은 다음과 같습니다. 거리마다 생성량을 얼마나 줄이고 어떤 기능을 뺄지를 분명히 정하고, 전환될 때 효과의 실루엣과 타이밍이 유지되는지 확인합니다. 생성률과 버스트, 수명을 함께 조절하며, 메모리를 미리 잡아 두는 개수 설정을 살아 있는 입자 수의 강제 상한으로 오해하지 않습니다. 바운드는 너무 크면 컬링의 효과가 줄고, 너무 작으면 보이는 입자까지 잘려 사라집니다.

끝이 있는 효과는 Emitter State와 System State의 루프와 Inactive Response를 맞춰 자연스럽게 끝나게 합니다. 컴포넌트의 Auto Destroy는 효과가 끝난 뒤 컴포넌트를 정리하는 설정이며, 계속 재생 중인 시스템을 입자가 하나도 없다는 이유만으로 끝내 주는 설정은 아닙니다.


GPU 파티클을 많이 쓸 때 주의할 점

GPU 시뮬레이션은 많은 입자를 다룰 수 있지만, 장면 곳곳에 무분별하게 쓰면 오히려 프레임이 크게 떨어질 수 있습니다.

먼저 동시에 켜질 시스템의 수를 제한합니다. 무거운 GPU 시스템은 동시에 몇 개까지 둘지를 설계할 때 정하고, 그 수를 넘으면 비용이 적은 대체 효과로 바꿉니다. 오버드로우도 관리해야 합니다. 반투명 입자가 화면을 넓게 덮으면 GPU 비용이 급격히 늘어나므로, 스프라이트의 화면 면적과 개수, 겹침을 줄이고 컬링을 적용합니다. 알파만 0으로 낮춘다고 셰이더 실행과 오버드로우가 저절로 없어지지는 않습니다.

플랫폼에 따라 품질을 나누는 것도 좋습니다. 모바일이나 저사양 PC는 생성률과 입자 수, Light Renderer의 사용 여부를 따로 정한 프리셋으로 나눕니다. 블루프린트에서 Event Tick마다 파라미터를 밀어 넣기보다, 값이 바뀔 때만 갱신하는 방식과 비용을 비교해 봅니다. 다만 모든 파라미터 갱신이 CPU와 GPU의 동기화 대기를 일으킨다고 단정하지는 않습니다. 마지막으로 LOD만으로 부족하다면 카메라 거리나 화면 점유율을 기준으로 한 컬링 규칙을 함께 적용합니다.


비용을 재는 도구

최적화의 첫걸음은 병목이 어디에 있는지 정확히 아는 것입니다. 나이아가라 디버거(Niagara Debugger)와 그 안의 FX Outliner에서는 활성 상태의 시스템과 이미터, 입자 수, 그리고 쓰는 엔진 버전이 제공하는 성능 정보를 확인할 수 있습니다. 다만 모든 모듈의 정확한 CPU, GPU 비용이 기본 화면에 저절로 표시되지는 않습니다.

게임을 플레이하는 중에 콘솔에 stat Niagara를 입력하면 나이아가라 시스템 전체의 성능 통계를 볼 수 있습니다. 더 깊이 분석하려면 언리얼 엔진의 프로파일링 도구인 Unreal Insights에서 필요한 CPU, GPU, 메모리 트레이스 채널을 켜고 프레임 시간과 메모리 할당을 분석하는데, 트레이스를 기록하고 여는 방법은 프로파일링 도구 사용법에서 다룹니다. 어떤 도구를 쓰든 바꾸기 전과 후를 같은 장면, 같은 카메라, 같은 품질, 같은 동시 효과 수에서 비교해야 합니다.


작업 횟수와 실행 시간은 다르다

계산을 줄였을 때 그 효과를 어떻게 읽어야 하는지 숫자로 따져 봅니다. 살아 있는 입자 4000개가 Update마다 표본을 5번 읽고, 1초에 60번 Update한다고 가정합니다. 그러면 1초에 표본을 읽는 횟수는 4000 × 5 × 60 = 1,200,000회입니다. 같은 표현을 유지하면서 표본을 2번으로 줄일 수 있다면 4000 × 2 × 60 = 480,000회가 되어, 표본을 읽는 횟수는 60% 줄어듭니다.

그렇다고 프레임 시간도 60% 줄어드는 것은 아닙니다. 다른 계산과 렌더링, 시스템 관리 비용은 그대로 남고, 컴파일러와 실제 하드웨어가 계산을 실행하는 방식도 결과에 영향을 주기 때문입니다.

모든 입자가 같은 입력으로 같은 값을 읽는다면, System이나 Emitter에서 한 번 구해 넘기는 방식을 검토합니다. 그러나 위치마다 온도를 읽는 것처럼 입자마다 입력이 다르다면, 그 값을 하나로 바꾸는 순간 효과의 뜻이 달라집니다. 그래서 먼저 공통으로 다시 쓸 수 있는 입력인지 확인한 뒤, 같은 장면에서 비용을 비교합니다.

스크립트 최적화는 한 번에 끝나는 작업이 아니라, 효과를 만들고 시험하는 내내 함께 챙겨야 하는 일입니다. 이 글의 기본 기법을 실제 작업에 적용하면, 보기 좋으면서도 예산 안에서 동작하는 효과를 만들 수 있습니다.


더 깊이: 스크립트를 컴파일하지 않는 경량 이미터

언리얼 엔진 5.4에는 나이아가라 경량 이미터(lightweight emitter)가 들어왔습니다. 상태 없는 이미터(stateless emitter)라고도 부르며, 시뮬레이션 중의 틱 사용을 최소화하고 경우에 따라서는 아예 없애도록 최적화된 이미터입니다. 5.4의 API 문서는 이 이미터가 CPU에서 스크립트를 실행하지 않고, 입자 데이터를 저장하는 대신 프레임마다 현재 입자의 나이에 맞춰 외삽(extrapolate)해 필요할 때 만들어 낸다고 설명합니다. 스크립트를 컴파일할 필요가 없어 작업 흐름도 빨라집니다. Lightweight Emitters 문서는 대부분의 경우 상태 없는 이미터가 기존의 상태 있는(stateful) 이미터보다 훨씬 빠르고, 시스템 전체를 상태 없는 이미터로 구성할 때 성능이 가장 좋다고 설명합니다.

대신 경량 이미터는 정해진 모듈 목록만 쓰는 고정 기능(fixed-function) 이미터입니다. 직접 만든 모듈과 스크래치 패드, 동적 입력(dynamic input)은 쓸 수 없고, 정해진 모듈의 설정만 바꿀 수 있습니다. 기능을 더 넓히려면 C++로 확장하며, 상태 없는 이미터와 상태 있는 이미터 사이를 변환할 수도 있습니다. 시스템 개요 창에서 마우스 오른쪽 버튼을 눌러 Add stateless emitter를 고르면 만들 수 있으므로, 이 글의 계산 줄이기에 들어가기 전에 그 효과가 고정된 모듈만으로 충분한지부터 따져 볼 수 있습니다. 다만 도입된 5.4에서는 이 기능을 실험 기능으로 따로 켜야 했고 출시용 콘텐츠에 쓰지 말라는 안내가 붙어 있었으므로, 출시할 프로젝트라면 쓰는 엔진 버전의 문서에서 지원 상태를 확인합니다.


정리

  • 계산 하나는 작아도 입자 수와 실행 횟수가 곱해지므로, 필요 없는 모듈을 지우고 Static Switch나 모듈 끄기로 쓰지 않는 기능을 실제로 뺍니다.
  • 계산의 뜻을 바꾸지 않는 범위에서 제곱근을 피하는 거리 비교나, 공통 값을 System과 Emitter에서 한 번 구하는 방법으로 계산을 줄입니다.
  • GPU 시뮬레이션은 많은 입자에 유리할 수 있지만 System과 Emitter 스크립트는 CPU에 남고, 충돌이나 블루프린트 연동에 제약이 있을 수 있습니다.
  • 스케일러빌리티는 이펙트 타입과 시스템, 이미터 설정으로 직접 설계해야 하며, 알파 0이나 메모리 할당 설정이 비용이나 입자 수를 저절로 줄이지 않습니다.
  • 나이아가라 디버거와 stat Niagara, Unreal Insights로 같은 조건에서 비교하며, 작업 횟수의 감소율을 프레임 시간의 감소율로 읽지 않습니다.

연습 문제

1. 줄었지만 아직 넘는 예산

다음 수치는 연습을 위한 가정이며 엔진에서 측정한 결과가 아닙니다. 목표는 60FPS이고, 같은 장면과 품질, 효과 수에서 전체 프레임 시간이 바꾸기 전 22ms, 바꾼 뒤 17ms라고 합시다. 비용은 얼마나 줄었고 목표를 이뤘을까요? 또 위치가 다른 입자들이 각각 읽던 온도를 시스템의 값 하나로 바꾸는 선택을, 표본 수가 줄었다는 이유만으로 받아들여도 될까요?

해설 보기

전체 프레임 시간은 22 - 17 = 5ms 줄었지만, 60FPS의 예산인 1000 ÷ 60 ≈ 16.67ms를 아직 넘습니다. 그래서 개선한 것과 목표를 이룬 것은 따로 판단합니다.

온도를 값 하나로 바꾸는 선택은 표본 수가 줄었다는 이유만으로 받아들일 수 없습니다. 위치마다 달라야 하는 온도를 하나로 바꾸면 효과의 뜻이 달라지므로, 허용할 표현의 차이와 비교 조건을 먼저 정해야 합니다. 또 CPU 시간과 GPU 시간을 단순히 더해 전체 프레임 시간을 다시 만들지 않으며, 실제로 판단할 때는 전체 프레임과 각 병목을 어떤 범위에서 쟀는지 함께 적어 둡니다.

2. 제곱으로 비교하기

입자가 원점에서 300cm 안에 있는지 검사하려고 Length(Position) < 300을 쓰고 있습니다. 제곱근을 피하도록 바꾼다면 어떤 식이 될까요? 기준값을 그대로 300으로 두면 어떤 문제가 생길까요?

해설 보기

LengthSquared(Position) < 300 × 300, 즉 LengthSquared(Position) < 90000으로 바꿉니다. 길이는 0 이상이므로 양쪽을 제곱해도 비교 결과가 같습니다.

기준값을 300으로 두면 길이의 제곱을 300과 비교하게 되어, 실제로는 원점에서 약 17.3cm(√300) 안에 있는 입자만 참이 됩니다. 그래서 기준값도 반드시 제곱해야 합니다.

3. 투명하지만 무거운 효과

화면을 넓게 덮는 연기 효과가 무거워서, 먼 거리에서는 Scale Color로 알파를 0으로 낮춰 보이지 않게 했습니다. 그런데 GPU 비용이 거의 줄지 않습니다. 왜 그럴까요?

해설 보기

알파를 0으로 낮춰도 입자는 여전히 살아 있고 렌더러가 그리므로, 셰이더가 실행되고 오버드로우도 그대로 생깁니다. 보이지 않을 뿐 그리는 일은 줄지 않은 것입니다.

비용을 줄이려면 이펙트 타입과 스케일러빌리티 설정으로 먼 거리에서 효과를 컬링하거나 생성량을 줄이고, 화면에서 차지하는 스프라이트의 면적과 겹침을 줄입니다.

나이아가라의 스케일러빌리티와 성능에 관한 권장 사항은 Epic의 나이아가라 스케일러빌리티와 권장 사항에서 확인할 수 있습니다.

이것으로 5장을 마칩니다. 5장에서는 파라미터 맵으로 값을 읽고 쓰는 나이아가라 스크립트의 구조에서 시작해, 수학 함수와 벡터 연산, 조건에 따른 값 선택과 반복, 속성을 읽고 쓰는 순서, 그리고 스크립트의 비용을 줄이고 재는 방법까지 노드로 계산을 만드는 방법을 익혔습니다. 다음 장의 기본 머티리얼 설정과 적용부터는 파티클용 머티리얼을 만들고, 조명과 글로벌 일루미네이션 같은 렌더링 기법과 효과를 연결하는 방법을 다룹니다.