-
[week11] ParticleUE5 2025. 5. 16. 17:57
- Emitter
**Emitter(이미터)**는 파티클 시스템(Particle System) 안에서 파티클의 생성·갱신·렌더링 흐름을 한 덩어리로 묶어주는 단위입니다. 한 파티클 시스템은 여러 개의 Emitter를 가질 수 있고, 각각의 Emitter는 자신만의 모듈 스택(Module Stack)을 통해 다음 과정을 정의합니다.
Emitter는 다음과 같은 역할을 합니다.
- 파티클 생성(Spawn)
“언제 몇 개를 뿌릴지”를 담당하는 Spawn 모듈들이 이 Emitter 안에서 실행되어 파티클을 생성합니다.
- 초기화(Initialization)
생성된 파티클의 초기 속성(위치, 속도, 크기, 색상 등)을 Initial 모듈들이 설정합니다.
- 업데이트(Update)
매 프레임 Gravity, Velocity over Life, Color by Life 같은 Update 모듈들이 실행돼 파티클 상태를 갱신합니다.
- 제거(Kill)
Lifetime 모듈이 수명을 다한 파티클을 Emitter에서 제거합니다.
- 렌더링(Render)
Sprite Renderer, Mesh Renderer 모듈이 이 Emitter가 가진 파티클들을 화면에 그립니다.
하나의 Particle System안에 Emitter A(불꽃), Emitter B(연기), Emitter C(잔광) 이렇게 여러 이미터를 조합해 복합 이펙트를 만듭니다.
- Module
파티클 시스템에서 “모듈(Module)” 은, 하나의 이펙터(Emitter)가 파티클을 생성·초기화·갱신·렌더링 하는 일련의 과정을 작은 기능 단위로 쪼개어 정의한 빌딩 블록입니다.
각 모듈은 특정 단계—예컨대 스폰(Spawn), 초기 속성 설정, 물리 업데이트, 렌더링 설정 등—에 개입해서 파티클의 동작을 바꾸거나 데이터를 추가합니다. Cascade 에디터의 왼쪽 창에 나열된 순서대로 실행되며, 위치(순서) 변화만으로도 동작 결과가 크게 달라질 수 있습니다.
이 모듈들은 드래그앤 드랍으로 순서를 바꾸거나 다른 Emitter로 옮길 수도 있으며, 각 모듈이 읽는 입력 상태가 이전 모듈이 덮어쓴 결과를 그대로 받기 때문에 모듈의 배치 순서는 굉장히 중요합니다.
- Cascade
UE3 ~ UE4에서 사용되던 Legacy Particle System Editor의 이름 -> UE5에서는 Niagara 파티클 시스템이 기본
- 모듈 스택 기반
- Spawn, Initial, Update, Kill, Render 단계별로 모듈을 쌓아 올려 이펙트 흐름을 정의
- 모듈 순서나 파라미터만 바꿔도 다양한 동작을 즉시 확인 가능
- 실시간 미리보기
- 에디터 내 뷰포트에서 파티클이 뿜어지고 사라지는 과정을 즉각 시각화
- Curve 에디터로 시간에 따른 속성 변화를 그래프로 편집
- 여러 Emitter 지원
- 하나의 시스템 안에 불꽃·연기·스파크 등 기능별 Emitter를 조합
- 각 Emitter마다 별도 머티리얼·모듈 스택을 가질 수 있음
- 이벤트 & 페이로드
- Collision, Death 같은 Event Generator 모듈로 이벤트 발생
- Event Handler 모듈로 다른 이펙터를 트리거하거나 Blueprint 콜백 실행
- Payload를 통해 커스텀 데이터(float/vector)를 주고받음
- LOD(Level of Detail)
- 거리나 화면 비율에 따라 모듈·Emitter를 켜고 끌 수 있어 성능 최적화에 유리
- 모듈 스택 기반
- Niagara
수십만 ~ 수백만 개의 파티클을 완전히 GPU에서 계산 가능하다.
즉, CPU - GPU 데이터 전송을 최소화하여 대규모 이펙트에서도 프레임 드롭 없이 매끄러운 연출이 가능
각 이펙터의 모듈 스택을 HLSL 스니펫 형태로 합친 뒤, 하나의 Uber Shader로 컴파일 해서 사용
- Velocity
각 파티클이 얼마나, 어떤 방향으로 움직일지를 결정하는 속도 벡터
1. Initial Velocity (초기 속도)- 위치: Initialization 그룹
- 역할: 파티클이 스폰되는 순간에 1회만 속도를 설정
- 주요 파라미터
- Velocity X/Y/Z: 축별 고정값 또는 Min/Max 범위 지정
- Distribution:
- Uniform (Constant): 일정한 방향·속도
- Random between two constants: 예) X축 속도 100~200 사이 랜덤
- Velocity Cone:
- Direction: 축 방향
- Angle: 원뿔 각도
- Magnitude: 속도 크기 범위
- 예시
- X = 0, Y = 100, Z = 0 (±20) → 위 방향으로 80~120 유닛/s 속도로 발사
- Cone(Direction=(0,0,1), Angle=30°, MinSpeed=50, MaxSpeed=80) → 위쪽 원뿔 안에서 랜덤
- 위치: Update 그룹
- 역할: 파티클 수명(현재 생존 시간 ÷ 전체 수명) 비율에 따라 속도를 보간하거나 가중치 곱
- 주요 파라미터
- Curve: LifeRatio [0→1] 에 매핑된 스칼라 값
- Scale/Multiplier: 초기 속도에 곱해지거나, 덧붙여서 적용
- 예시
- Scale Curve: 1→0 → 수명 끝날수록 속도가 0에 수렴 (점점 느려짐)
- Scale Curve: 0→1→0 → 중간에 최고 속도, 다시 감속
- LifeTime
파티클이 얼마나 오래 존재할지를 결정하는 수명 모듈입니다. Spawn 단계에서 생성된 파티클이 몇 초 뒤에 사라질지를 지정하고, 그 시간이 지나면 자동으로 Kill 처리해 줍니다.
- Size
Size는 파티클 하나의 크기를 의미하며, Velocity와 마찬가지로 초기값과 LifeTime에 따른 크기를 동적으로 변경할 수 있습니다.
- Payload
Cascade 에디터에는 Event Generator와 Event Handler 모듈이 있는데,- Event Generator 쪽에서 충돌(collision), 수명 종료(death) 등 이벤트를 터뜨릴 때
- 함께 전달할 수 있는 사용자 정의 값(float 또는 vector 형태)을 “Payload”라고 부릅니다.
- Collision 모듈을 Event Generator로 쓰면서
- Payload Channel 0에 “충돌 강도” 값을 넣고
- Payload Channel 1에 “파티클 ID” 등을 담으면
- Event Handler에서는 이 두 개의 페이로드를 꺼내서
- 충돌 위치에 스파크 이펙트를 강도에 맞춰 폭발시킨다든지
- 파티클별로 다른 로직을 수행할 수 있습니다
- Dynamic Vertex Buffer (D3D11_USAGE_DYNAMIC, D3D11_CPU_ACCESS_WRITE)
매 프레임 혹은 자주 CPU에서 데이터를 업데이트할 목적으로 쓰이는 특별한 버퍼
// 1) 버퍼 설명 설정 D3D11_BUFFER_DESC desc = {}; desc.BindFlags = D3D11_BIND_VERTEX_BUFFER; // 정점 버퍼 용도 desc.Usage = D3D11_USAGE_DYNAMIC; // 동적(Dynamic) 사용 desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; // CPU → GPU 쓰기 권한 desc.ByteWidth = sizeof(Vertex) * MaxVerts; // 버텍스 개수 × 크기 desc.MiscFlags = 0; desc.StructureByteStride = 0; // 2) 빈 버퍼 생성 ID3D11Buffer* dynamicVB = nullptr; device->CreateBuffer(&desc, nullptr, &dynamicVB);
Dynamic VB를 효율적으로 쓰기 위해서는 WRITE_DISCARD 모드로 매 프레임 덮어써야 합니다.
D3D11_MAPPED_SUBRESOURCE mapped; ctx->Map(dynamicVB, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped); // mapped.pData 에 새 정점 데이터를 memcpy or 직접 작성 memcpy(mapped.pData, pVertices, vertexCount * sizeof(Vertex)); ctx->Unmap(dynamicVB, 0); // 바인딩 UINT stride = sizeof(Vertex); UINT offset = 0; ctx->IASetVertexBuffers(0, 1, &dynamicVB, &stride, &offset);
가끔씩 정점 데이터를 변경해야할 때는 UpdateSubresource 한 번 호출로 충분하며, 이 때는 Default Vertex Buffer를 사용하는 것이 좋습니다.
- ReplayData
언리얼의 파티클 시스템에서는 시뮬레이션이 비결정적(non-deterministic) 으로 동작하기 때문에, 한 번 재생했던 이펙트를 정확히 똑같이 다시 보거나, 임의의 시점으로 되감기(scrub) 하려면 이전 상태(스폰 이벤트, 랜덤 시드, 모듈별 파라미터 변화 등) 를 기록해 두어야 합니다.
ReplayData가 하는 일- 스폰 이벤트 기록
- 각 Tick마다 몇 개를 스폰(spawn)했는지, Burst 모듈이 언제 몇 개를 뿌렸는지 기록
- 랜덤 시드 동기화
- 랜덤을 쓰는 모듈(위치, 속도 랜덤 분포 등)이 매번 다른 값을 내지 않도록, 랜덤 시드와 순서를 저장
- 모듈 파라미터 변화 기록
- 카메라 의존 매개변수, 외부에서 바인딩된 스칼라 파라미터 같은 값들이 변경된 시점을 저장
- 네트워크 복제용
- 멀티플레이어 환경에서 클라이언트끼리 똑같은 파티클 효과를 동기화할 때도 ReplayData를 이용
- 스폰 이벤트 기록
- Instance
- Blend State
- Translucent Rendering
불투명은 깊이 값을 쓰고(depth write) 테스트(depth test)만 하기 때문에, 어떤 순서로 그려도 Z-buffer에 의해 올바른 가려짐 처리(early-Z)가 됩니다.
반투명은 픽셀마다 “얼마나 비칠지(알파)”를 계산해 기존 화면에 섞어야 하므로, 깊이 값은 테스트만 하고 쓰지 않아(depth write off) “이미 그려진 픽셀” 위에 올바르게 블렌딩할 수 있게 합니다.
또한 렌더링 순서가 중요한데,
1. 먼저 멀리 있는(백그라운드) 반투명 객체를 그리고
2. 나중에 가까이 있는(포어그라운드) 반투명 객체를 그려야- 멀리 있는 픽셀 위에 가까운 픽셀이 올바르게 블렌딩됨
- 반대 순서이면 가까운 픽셀이 멀리 있는 픽셀을 “덮어쓴 뒤” 다시 블렌드 되지 못함
- Precache
Pre-cache(프리캐시)란, 런타임에서 “처음 사용될 때” 발생할 수 있는 무거운 로딩·컴파일·계산 작업을 미리 수행해 두어, 실제 화면 표시나 게임플레이 중 프레임 드롭이나 히치를 방지하는 기법입니다.
파티클 시스템에도 “히치(hitch)”나 런타임 지연을 줄이기 위해 할 수 있는 사전 준비(Pre-cache) 작업이 몇 가지 있습니다.- Emitter 빌드 정보 캐싱
- UParticleEmitter::CacheEmitterModuleInfo() 에서 모듈별 오프셋, 파티클 구조체 크기, 인스턴스 페이로드 크기, 예상 최대 활성 파티클 수 등을 미리 계산해 두면, 런타임에 매번 그 계산을 되풀이하지 않아도 됩니다.
- 에디터의 Build() 단계에서 이 작업을 끝내 두면, 게임 실행 시 곧바로 빠르게 파티클 데이터를 할당·접근할 수 있습니다.
- Dynamic Buffer Pool 프리-맵(Map)·프리-할당
- GDynamicVertexBufferPool/GDynamicIndexBufferPool 의 Acquire 를 레벨 로드나 이펙트 최초 재생 직전에 호출해,
- D3D11 버퍼 생성 및 Map(WRITE_DISCARD) 만 한 번 해 두고, 매 프레임엔 언맵/서브할당만 반복하게 하면 첫 그리기 시 드라이버 호출이 발생하지 않습니다.
- 머티리얼·텍스처 Preload
- 스프라이트 이터레이터가 쓰는 UMaterial 과 UTexture2D 를 레벨 로드시 혹은 파티클 시스템 로드시 미리 로드(StreamableManager) 해 두면,
- 첫 렌더 호출 때 디스크 I/O나 셰이더 컴파일로 인한 히치를 줄일 수 있습니다.
- ReplayData 초기 할당
- FParticleEmitterInstance::FillReplayData 로 FDynamicEmitterReplayDataBase 의 내부 버퍼(ParticleDataContainer) 를 미리 할당·Memcpy 해 두면,
- 시퀀서 타임라인을 앞뒤로 스크럽할 때나 PIE 전환 시 재할당 지연이 없습니다.
- SubUV·Curve 테이블 사전 계산
- SubUV(애니메이션 스프라이트 시트) 패턴의 UV 좌표 배열을 에디터 빌드 단계에서 미리 만들거나,
- 색상·크기·속도 등 곡선(Curve) 모듈의 값들을 런타임용 룩업 테이블로 변환해 두면, 매 파티클마다 GetValue 호출 비용을 줄일 수 있습니다.
- 쉐이더 퍼뮤테이션(Variant) 프리컴파일
- 해당 파티클 머티리얼이 여러 셰이더 키워드를 쓴다면, 패키징 단계에서 미리 컴파일된 PS/VS 바이너리를 준비해서 런타임 컴파일을 방지합니다.
- Emitter 빌드 정보 캐싱
- Adress Align
주소 정렬(Address Alignment)은 메모리상의 주소를 특정 크기(보통 2의 거듭제곱) 배수로 맞춰 주는 것을 말합니다.
- 하드웨어 요구
- 일부 CPU 아키텍처(예: ARM, SPARC)는 4바이트, 8바이트, 16바이트 경계가 맞춰져 있지 않으면 메모리 접근 에러(버스 오류)가 납니다.
- 성능 최적화
- 캐시 라인(64바이트)이나 메모리 버스 폭에 맞춰 정렬하면, 한 번에 더 큰 블록을 효율적으로 읽어 옵니다.
- SIMD 명령(예: SSE, AVX)도 16바이트 또는 32바이트 정렬 버퍼를 가정하고 동작하기 때문에, 정렬이 맞지 않으면 느려지거나 별도의 복사 작업이 필요합니다.
- 하드웨어 요구
// Align Up: Val 을 Alignment 의 배수로 올림 inline uint32 Align(uint32 Val, uint32 Alignment) { // Alignment 는 2의 거듭제곱이라 가정 return (Val + Alignment - 1) & ~(Alignment - 1); }
Cascade Paritcle은 절차적(Procedural) 컨텐츠 제작 방식이다. 그 이유는?
- 모듈 = 작은 프로시저(절차)
- Spawn: “몇 개”를 생성
- Initial Velocity: “어떤 속도”를 부여
- Color by Life: “수명 비율에 따라” 색상을 보간
- …등 각각이 독립된 작은 함수(Procedure) 역할
- 순서대로 실행되는 파이프라인
- 스택 상단부터 차례로 실행 → 최종 파티클 속성 산출
- 순서만 바꿔도 결과가 달라지니까, 일종의 “스크립트”처럼 동작
- 파라미터화된 동작
- 각 모듈에는 값, 커브, 랜덤 범위 같은 파라미터가 있어
- 이를 조합·튜닝해 수백 가지 다양한 이펙트를 만들 수 있음
- 런타임 생성·제거
- 에디터에서 모듈과 파라미터만 설정해 두면
- 게임 실행 중 실시간으로 수많은 파티클을 생성·제거하며 연출
Sprite Particle
- Global Dynamic Buffer Pool을 이용 (Batching)
- 한 번 큰 덩어리를 만들고, 그 안에서 Allocate 식으로 서브영역을 골라 씀
- 풀 전체를 한 번에 매핑 해두고 내부적으로 할당하기 때문에 map / unmap 및 Drawcall 최소화
- 풀안의 할당된 영역을 재사용할 수 있어 매번 할당하지 않아도 됨
- 메모리에 연속적으로 파티클을 할당 해두어 정렬이 편해짐
- Sprite는 반투명 영역이 많아 정렬이 필수적임 -> Batching방식 이용
Mesh Particle
- Instancing 이용하여 DrawCall 및 메모리 사용 절감
Global Dynamic Buffer
A,B,C 세 개의 Instance가 있다고 할 때, 첫 번째 프레임에서는 Pool에 일단 dynamic buffer를 생성하고 메모리를 다 채울 때까지 Instance의 사이즈만큼 메모리를 할당 합니다.더 채울 수 없다고 판단하면 두 배 크기의 새로운 버퍼를 생성해 남은 인스턴스의 메모리를 할당합니다.
다음 프레임에서 인스턴스의 active particle의 수가 늘었다고 하면, 기존의 버퍼풀의 버퍼를 재사용하여 크기가 충분하여 사용할 수 있으면 메모리를 잘라가면서 할당하고 더 채울 수 없으면 마찬가지로 두 배 크기의 새로운 버퍼를 할당하여 그 내부를 채웁니다.
Particle Order
반투명 파티클의 올바른 블렌딩 순서를 보장하기 위해서 파티클 sorting을 했습니다.
알파 블렌딩은 멀리 있는 것부터 먼저 그려야 뒤의 오브젝트가 앞뒤 관계에 맞게 투명하게 보입니다.
올바르게 정렬되지 않은 파티클의 경우에는 보시는 것처럼 깜빡임 현상이 생기게 됩니다.
블렌딩 수식에서 보는 것처럼 블렌딩시 출력되는 색깔은 픽셀의 순서에 영향을 받습니다.
투명 파티클은 깊이 버퍼에 쓰지 않기 때문에 그리는 순서가 적용되는 픽셀의 순서가 되고 매번 올바른 순서로 그려주지 않으면 픽셀의 색이 순간적으로 바뀌면서 깜빡임 현상이 발생합니다.
'UE5' 카테고리의 다른 글
[week8] Delegate (3) 2025.04.25 [week6] Light (1) 2025.04.12 [week5] PixelShader - Fog (1) 2025.04.04 [week2] Texture Rendering (1) 2025.03.20 [UE5] Development Editor에서 효과적으로 디버깅하는 방법 (1) 2025.01.17 - Emitter