언리얼 - 나이아가라 Advanced Guide – 위치 기반 동역학



언리얼 - 나이아가라 Advanced Guide – 위치 기반 동역학

금별 0 22 06:37

 

Epic Games는 “Content Examples”라는 프로젝트를 배포합니다. 이는 Unreal Engine의 다양한 기능을 보여주는 여러 샘플 프로젝트 모음입니다. UE4.26부터 Niagara Advanced라는 맵이 있습니다.

이 맵에는 새로운 Niagara 기능의 예제와 실용적인 적용 사례를 포함한 유용한 샘플이 풍부하게 들어 있습니다. 구현 방식을 검토해 보면 배울 점이 많습니다.

이 글에서는 이러한 샘플들을 여러 부분으로 나누어 자세히 설명하겠습니다.

참고로, Content Examples는 Fab의 이 링크에서 다운로드할 수 있습니다. UE 버전에 따라 콘텐츠가 다를 수 있다는 점 유의하세요.

※참고: 이 블로그는 UE4.26 Contents Example을 기반으로 작성되었으므로, 최신 버전과 세부 사항에서 약간 다를 수 있지만, 여기 설명된 개념 자체는 변하지 않습니다.

TLDR;)

Position-Based Dynamics (PBD)는 위치(예: 충돌 감지)를 기반으로 하는 물리 시뮬레이션 유형입니다.

Position-Based Dynamics를 사용하면 파티클 간 충돌 처리를 구현할 수 있습니다.

소개

이 글에서는 Position-Based Dynamics (PBD)를 설명하겠습니다. 이는 완전히 새로운 기능이 아니라 UE4.26 이후 추가된 기능들, 예를 들어 Simulation Stage, Particle Attribute Reader, Neighbor Grid3D 등을 활용합니다. PBD의 기본 기능들은 모듈화되어 있어 사용할 수 있습니다.

그래서, 이 기능들에 아직 익숙하지 않은 분들은 먼저 다음 글들을 읽어보시길 권장합니다:

이번에는 Niagara Advanced의 “3.6 Position-Based Dynamics” 샘플을 설명하겠습니다. 하지만 샘플에 들어가기 전에, 먼저 PBD가 무엇인지 알아보겠습니다.

위치 기반 역학(PBD)이란 무엇인가?

간단히 말해, PBD는 위치 기반 물리 시뮬레이션입니다.

표준 물리 계산은 물리 법칙에 기반해 힘 → 가속도 → 속도 → 위치 순으로 작동하는 반면, PBD는 특정 제약 조건(예: 객체 간 겹침 방지)에 따라 위치를 조정하고, 그로부터 속도를 계산합니다.

자세한 내용은 다음 기사 등을 참조하세요:[CEDEC 2014]강체에서 유체까지, 세가의 프로그래머가 말하는 「위치 기반 물리 시뮬레이션」의 최전선

Niagara에서 Neighbor Grid 3D와 Particle Attribute Reader를 사용하여 주변 파티클의 위치 정보를 수집합니다. 미리 정의된 반경을 기반으로 파티클이 겹치는지 계산함으로써, 겹침을 피하도록 위치를 조정할 수 있습니다.

시뮬레이션 스테이지를 사용해 이 과정을 반복함으로써, 매우 정확한 조정을 달성할 수 있습니다.

이것의 가장 큰 장점은 이전에는 불가능했던 파티클 간 충돌을 이제 처리할 수 있다는 점입니다.

차이점에서 분명히 알 수 있습니다: 왼쪽 이미지에서는 파티클들이 중심으로 끌려가지만, 겹치지 않습니다.

참고로, Epic에서 만든 “Popcorn Man” 데모도 PBD 기술을 사용합니다.

이제 샘플에 대한 설명으로 들어가 보겠습니다.

3.6 위치 기반 역학 설명

이 샘플에서는 여러 구체들이 중심으로 끌려가지만 서로 충돌하는 방식으로 동작합니다. 이는 PBD에 의해 가능합니다.

자세히 살펴보겠습니다.

시스템 업데이트 단계에서 Initialize Neighbor Grid 모듈을 사용하여 Neighbor Grid 3D를 초기화합니다.

상당히 큰 4x4x4 그리드가 설정됩니다.

Emitter는 다음과 같이 구성되며, 실제 PBD 계산은 Particle Collisions 시뮬레이션 스테이지 내의 PBD Particle Collision 모듈에서 수행됩니다.

단계별로 설명하겠습니다.

#### Emitter Update

먼저, Spawn Burst Instantaneous 모듈을 사용해 10개의 파티클이 동시에 생성됩니다.

그 후, Particle Attribute Reader가 초기화됩니다. Emitter Name은 에미터 자체의 이름으로 설정되어 다른 파티클로부터 정보를 읽을 수 있게 합니다.

#### 파티클 스폰

Initialize Particle 모듈은 파티클의 Mass를 무작위로 설정합니다. Calculate Size and Rotational Inertia by Mass 모듈은 그 후 질량 값과 다양한 설정에 따라 메쉬의 스케일(및 회전 관성)을 설정합니다. (크기는 질량과 밀도에 따라 계산되므로, 이 모듈은 이에 맞게 조정합니다.)

무작위로 설정된 질량을 기반으로 메시 크기에 약간의 무작위성을 추가합니다.

파티클은 Sphere Location 모듈을 사용하여 구(sphere) 내부의 무작위 위치에서 생성됩니다.

#### 파티클 업데이트

세 가지 속성, CollisionRadius (float), Unyielding (bool), 그리고 Previous Position (Vector)이 초기화됩니다. 이 속성들은 나중에 thePBD Particle Collision 모듈에서 사용됩니다.

CollisionRadius는 Calculate Particle Radius 동적 입력을 사용해 계산됩니다. 복잡하지 않습니다; 메쉬 치수와 파티클의 scale 속성을 기반으로 Method for Calculation Particle Radius에 따라 반경을 계산합니다. 이번에는 Method가 Minimum Axis이고 Mesh Dimensions가 (2.0, 2.0, 2.0)이므로, CollisionRadius에 2.0 / 2 Scale 값이 사용됩니다.

Previous Position은 Solve Forces and Velocity 모듈에서 추가된 속성으로 설정됩니다.

- Unyielding이 false로 설정됩니다.

나머지 설정들은 그리 복잡하지 않습니다. Point Attraction Force에 의해 움직임이 생성되며, 이는 중심을 향해 끌어당깁니다.

여기서의 충돌은 다른 오브젝트와의 충돌을 활성화하기 위한 것이므로, PBD (Position-Based Dynamics)와는 관련이 없습니다.

간단히 설명하자면, 이 샘플은 GPU 시뮬레이션을 사용하므로 충돌 감지를 위해 세 가지 방법이 사용할 수 있습니다:

1. GPU 깊이 버퍼: 장면 깊이 정보를 충돌 감지에 사용합니다.

2. GPU Distance Fields: 이는 메시 Distance Fields 정보를 충돌 감지에 사용합니다. Mesh Distance Fields는 각 복셀(월드 스페이스 좌표)에 가장 가까운 메시까지의 거리를 저장합니다. 이 주제는 상당히 깊이가 있으므로, 더 자세히 조사해보는 것도 흥미로울 수 있습니다.

3. Analytical Planes: 이 방법은 사용자 정의 무한 평면과의 충돌만 감지합니다.

마지막으로 DynamicMaterialParameter 세트가 있지만, 사용되지 않고 있습니다.

#### 그리드 채우기

이 시뮬레이션 단계에서 각 파티클의 실행 인덱스가 theNeighbor Grid3D에 저장됩니다.

여기서 사용된 Populate Neighbor Grid는 기존 모듈입니다. 이전에 Neighbor Grid 3D를 설명한 글에서 이러한 작업은 Scratch Pad 모듈을 사용해 구현되었지만, 이미 전용 모듈로 제공되고 있었습니다.

그러나 Initialize Neighbor Grid3D 모듈이 설정한 속성들이 입력값으로 사용되기 때문에, 해당 모듈을 사용하지 않는 경우에는 해당 값들을 수동으로 제공해야 합니다.

내부 로직상으로는 동일하므로 여기서는 자세한 설명을 생략하겠습니다.

#### 파티클 충돌

이제 핵심 부분에 도달했습니다.

먼저 반복 횟수를 4로 설정합니다. 단일 패스에서 다른 파티클과의 겹침을 조정한 후, 다른 파티클과 새로운 겹침이 발생할 수 있습니다. 이 과정을 여러 번 반복함으로써 조정이 더 정확해집니다. 물론 이는 성능과 트레이드오프가 따릅니다.

PBD Particle Collision의 PBD 프로세스에 대해서는 기존 모듈입니다. 하지만 실험적이기 때문에 Library Only 옵션을 체크 해제하지 않으면 검색 결과에 나타나지 않으니 주의하세요.

입력 값은 다음과 같습니다:

입력 값은 다음과 같습니다:

곧 세부 사항을 살펴보겠습니다. 하지만 솔직히 말해서, 아직 개발 중인 느낌이 듭니다.

사실 KinectFriction과 StaticFriction은 아직 작동하지 않으므로, 값을 변경해도 아무런 효과가 없습니다.

RelaxationAmount: 이 값을 증가시키면 PBD 효과가 약해져 충돌이 더 부드럽게 느껴집니다. 보통 1.0 값으로 충분합니다.

Simulate flag: 이 플래그는 PBD를 활성화하거나 비활성화합니다.

다른 값들은 미리 정의되어 있으므로 변경할 필요가 없습니다.

내부를 살펴보자; HLSL로 구현되어 있습니다.

결국 값들은 아래와 같이 업데이트되지만, 이 샘플에서는 Position과 Velocity만 사용됩니다.

HLSL 코드에 들어가기 전에, 먼저 전제를 설명하겠습니다.

PBD가 구현되는 방식

이 방법에서는 Neighbor Grid3D에서 해당 파티클이 속한 셀과 인접한 3x3x3=27개의 셀(파티클 자신의 셀 포함) 내 파티클들을 미리 설정된 CollisionRadius를 사용해 중첩 여부를 확인합니다. 이는 모든 파티클에 대해 브루트포스 검색을 수행함으로써 이루어집니다.

중첩이 감지되면 중첩 길이를 계산하고, 두 파티클의 질량에 기반한 가중 거리를 적용합니다. 그 후 파티클의 위치를 반대 방향으로 조정하고, 속도도 해당 방향으로 업데이트합니다.

이 과정은 단일 프레임 내에서 4회 반복되어 PBD를 달성합니다.

“不屈” 플래그

또한 Unyielding이라는 플래그가 있습니다. 이 플래그가 파티클에 대해 true로 설정된 경우, 이 플래그가 false로 설정된 다른 파티클과 겹치면 true인 파티클은 움직이지 않습니다. 오직 false인 파티클의 위치만 조정됩니다. (더 구체적으로, UnyieldingMassPercentage를 설정하여 파티클이 움직임에 얼마나 저항하는지를 제어할 수 있습니다.)

이것은 각 파티클에 대해 서로 다른 충돌 동작을 허용합니다. 그러나 이 샘플에서는 모든 파티클이 false로 설정되어 있으므로, 그들 사이에 동작 차이가 없습니다.

그 점을 염두에 두고 HLSL 코드 내부를 살펴보겠습니다.

```
// 출력용 변수 초기화
OutPosition = Position;
OutVelocity = Velocity;
CollidesOut = CollidesIn;
NumberOfNeighbors = 0;
displayCount = 0.0;
// AveragedCenterLocation = float3 (0.0, 0.0, 0.0);
#if GPUSIMULATION
// 현재 셀을 포함한 27개의 인접 셀에 접근하기 위한 인덱스 오프셋 정의
const int3 IndexOffsets[27] =
{
int3(-1,-1,-1),
int3(-1,-1, 0),
int3(-1,-1, 1),
int3(-1, 0,-1),
int3(-1, 0, 0),
int3(-1, 0, 1),
int3(-1, 1,-1),
int3(-1, 1, 0),
int3(-1, 1, 1),
int3(0,-1,-1),
int3(0,-1, 0),
int3(0,-1, 1),
int3(0, 0,-1),
int3(0, 0, 0),
int3(0, 0, 1),
int3(0, 1,-1),
int3(0, 1, 0),
int3(0, 1, 1),
int3(1,-1,-1),
int3(1,-1, 0),
int3(1,-1, 1),
int3(1, 0,-1),
int3(1, 0, 0),
int3(1, 0, 1),
int3(1, 1,-1),
int3(1, 1, 0),
int3(1, 1, 1),
};
// 위치를 그리드 공간 좌표로 변환
float3 UnitPos;
myNeighborGrid.SimulationToUnit(Position, SimulationToUnit, UnitPos);
// 파티클이 속한 셀의 인덱스 가져오기
int3 Index;
myNeighborGrid.UnitToIndex(UnitPos, Index.x, Index.y, Index.z);
// 중간 사용을 위한 변수 정의
float3 FinalOffsetVector = {0,0,0};
uint ConstraintCount = 0;
float TotalMassPercentage = 1.0;
// 그리드의 셀 수 가져오기
int3 NumCells;
myNeighborGrid.GetNumCells(NumCells.x, NumCells.y, NumCells.z);
// 셀당 최대 이웃 수 가져오기
int MaxNeighborsPerCell;
myNeighborGrid.MaxNeighborsPerCell(MaxNeighborsPerCell);
// 27개의 인접 셀 검색
for (int xxx = 0; xxx < 27; ++xxx)
{
// 각 셀에 대해 MaxNeighborsPerCell 횟수만큼 검색
for (int i = 0; i < MaxNeighborsPerCell; ++i)
{
// 셀에 저장된 파티클의 인덱스 가져오기
const int3 IndexToUse = Index + IndexOffsets[xxx];
int NeighborLinearIndex;
myNeighborGrid.NeighborGridIndexToLinear(IndexToUse.x, IndexToUse.y, IndexToUse.z, i, NeighborLinearIndex);
```
```cpp
// NeighborLinearIndex를 기반으로 파티클의 Execution Index를 가져옵니다
int CurrNeighborIdx;
myNeighborGrid.GetParticleNeighbor(NeighborLinearIndex, CurrNeighborIdx);
// Execution Index를 사용하여 파티클의 위치를 가져옵니다
bool myBool;
float3 OtherPos;
DirectReads.GetVectorByIndex<Attribute="Position">(CurrNeighborIdx, myBool, OtherPos);
// 이 파티클과 대상 파티클 사이의 거리와 방향을 계산합니다
const float3 vectorFromOtherToSelf = Position - OtherPos;
const float dist = length(vectorFromOtherToSelf);
const float3 CollisionNormal = vectorFromOtherToSelf / dist;
// 대상 파티클의 CollisionRadius를 가져옵니다
float OtherRadius;
DirectReads.GetFloatByIndex<Attribute="CollisionRadius">(CurrNeighborIdx, myBool, OtherRadius);
// 겹침 여부를 계산합니다 (Overlap >= 0이면 겹침)
float Overlap = (CollisionRadius + OtherRadius) - dist;
// 유효한 인덱스인지 확인합니다
if (IndexToUse.x >= 0 && IndexToUse.x < NumCells.x &&
IndexToUse.y >= 0 && IndexToUse.y < NumCells.y &&
IndexToUse.z >= 0 && IndexToUse.z < NumCells.z &&
CurrNeighborIdx != InstanceIdx && CurrNeighborIdx != -1 && dist > 1e-5)
{
// 변수 초기화
bool otherUnyielding = false;
TotalMassPercentage = 1.0;
// 겹침이 있을 때만 처리합니다
if (Overlap > 1e-5)
{
NumberOfNeighbors += 1;
displayCount = NumberOfNeighbors;
// 대상 파티클의 Unyielding 플래그를 가져옵니다
bool NeighborUnyieldResults;
DirectReads.GetBoolByIndex<Attribute="Unyielding">(CurrNeighborIdx, myBool, NeighborUnyieldResults);
CollidesOut = true;
// 대상 파티클의 질량을 가져옵니다
float OtherMass;
```
```cpp
DirectReads.GetFloatByIndex<Attribute="Mass">(CurrNeighborIdx, myBool, OtherMass);
// 두 파티클의 질량을 기반으로 가중치 팩터를 계산합니다 (이후 이동 조정을 위해 사용됩니다)
TotalMassPercentage = Mass / (Mass + OtherMass);
// 두 파티클 모두 굴복하지 않는 경우 아무것도 하지 않습니다
if (NeighborUnyieldResults && Unyielding) { // 이 파티클과 상대 파티클 모두 굴복하지 않습니다
TotalMassPercentage = TotalMassPercentage;
}
// 대상 파티클만 굴복하지 않는 경우 TotalMassPercentage를 0에 가깝게 조정합니다
else if (NeighborUnyieldResults) { // 이 파티클은 굴복하지만 상대는 그렇지 않습니다
TotalMassPercentage = lerp(TotalMassPercentage, 0.0, UnyieldingMassPercentage);
}
// 이 파티클이 굴복하지 않는 경우 TotalMassPercentage를 1에 가깝게 조정합니다
else if (Unyielding) { // 이 파티클은 굴복하지 않지만 상대는 그렇습니다
TotalMassPercentage = lerp(TotalMassPercentage, 1.0, UnyieldingMassPercentage);
}
// 가중된 중첩 거리를 최종 오프셋 벡터에 추가합니다
FinalOffsetVector += (1.0 - TotalMassPercentage) Overlap CollisionNormal;
// 이 부분은 모멘텀을 계산하는 것 같지만 현재 사용되지 않습니다. 마찰 구현 시 사용될 수 있습니다.
float3 OtherPreviousPos;
DirectReads.GetVectorByIndex<Attribute="Previous.Position">(CurrNeighborIdx, myBool, OtherPreviousPos);
const float3 P0Velocity = Position - PreviousPosition;
const float3 PNVelocity = OtherPos - OtherPreviousPos;
const float3 P0RelativeMomentum = P0Velocity Mass - PNVelocity OtherMass;
```
```cpp
const float3 NormalBounceVelocity = (P0RelativeMomentum - dot(P0RelativeMomentum, CollisionNormal) CollisionNormal) / Mass;
const float NormalBounceVelocityMag = length(NormalBounceVelocity);
// 겹치는 파티클 수를 기록
ConstraintCount += 1;
}
}
}
}
}
// 겹침이 발생하고 IsMobile (= Simulate의 입력값)이 true인 경우
if (ConstraintCount > 0 && IsMobile)
{
// 제약 조건 수와 RelaxationAmount(편향된 평균값)으로 나눈 조정된 위치 벡터를 현재 위치에 더함
OutPosition += 1.0 FinalOffsetVector / (ConstraintCount RelaxationAmount);
// 여기서 마찰 지원 추가 (마찰이 여기에 구현될 수 있음)
// 조정된 위치와 이전 위치를 기반으로 속도 업데이트
OutVelocity = (OutPosition - PreviousPosition) InvDt;
// Unyielding이 true인 경우 속도를 0으로 설정
if (Unyielding) {
// 이전 속도로 되돌리는 것을 고려할 수 있음
OutVelocity = float3(0, 0, 0);
}
}
#endif
```

주석에 몇 가지 설명을 추가했으니 이를 읽어보면 이해할 수 있을 겁니다. 하지만 아직 작업 중인 상태이므로, 향후 버전에서 구현이 변경될 수 있습니다.

결론

솔직히 파티클 간 충돌을 이제 감지할 수 있게 된 건 정말 훌륭한 발전이라고 생각합니다.

성능 측면에서, Popcorn Man 데모 중에 이 부분의 처리 시간이 1ms 미만이라고 언급했는데, 적절한 조정을 통해 실시간 사용에 실용적일 수 있을 것 같습니다.

PBD에 관해서는 다른 샘플이 있으므로, 별도의 글에서 소개하겠습니다.

Comments


번호 포토 분류 제목 글쓴이 날짜 조회
열람중 언리얼4 언리얼 - 나이아가라 Advanced Guide – 위치 기반 동역학 금별 06:37 23
1290 언리얼4 Niagara로 생성된 Static Mesh에서 파티클 스폰하기 금별 06:29 17
1289 언리얼4 언리얼 - 나이아가라로 스플라인을 따라 발사되는 파티클 제작 금별 05:51 18
1288 언리얼4 언리얼5 - 어검술의 완성 : 검진(Sword Array) 심화 기동 및 고급 제어(한글자막) 금별 02:32 24
1287 언리얼4 언리얼5 - 벡터 연산을 활용한 검진(Sword Array) 지정 방향 발사 로직(한글자막) 금별 02:32 16
1286 언리얼4 언리얼5 - Particle Reader를 활용한 검진(Sword Array) 타겟 추적 및 회전 제어(한글자막) 금별 02:31 16
1285 언리얼4 언리얼5 - 파티클 Index를 활용한 원형 검진(Sword Array) 배치 제작과정(한글자막) 금별 02:30 19
1284 언리얼4 언리얼5 - VHS 포스트 프로세스 제작과정 금별 02:10 18
1283 언리얼4 언리얼5 - 데드 스페이스 VFX reborn[팬메이드] 금별 02:07 19
1282 언리얼4 Simple Light Aura VFX in UE5 Niagara Tutorial ✨????️???? ashif 01.08 18
1281 언리얼4 Heat Distortion VFX for Fire in UE5 Niagara! (Photoshop & Normal Map Workflow) ????????✨ ashif 2025.12.16 182
1280 2D 섭스턴스 디자인 - 색수차(Chromatic Aberration) 매직 오라 텍스처 만들기(한글자막) 금별 2025.12.16 211
1279 언리얼4 언리얼 - 미호요 신작 스타일 Depth Fade와 UV 조작을 이용한 공간 포털 연출(한글자막) 금별 2025.12.16 295
1278 언리얼4 언리얼5 - Polar Coordinates를 활용한 태극 무늬 제작 금별 2025.12.16 165
1277 언리얼4 언리얼 - AAA 게임 개발에서의 나이아가라 모듈(Niagara Module) 연구 및 적용 금별 2025.12.16 191
1276 유니티 유니티 - 유니티 스플라인 카메라 무브먼트 예제(워프 효과 연출) 금별 2025.12.16 154
1275 언리얼4 언리얼 - 간단한 스타일라이즈드 불효과 제작과정팁 금별 2025.12.16 156
1274 언리얼4 Simple Stylized Tornado VFX in UE5 Niagara! (Torus Mesh, Erode & Dynamic Params) ????️????✨ ashif 2025.12.15 125
1273 2D 섭스턴스 디자인 - 움푹 파인 바닥 절차적 제작과정(한글자막) 금별 2025.12.10 164
1272 언리얼4 언리얼 - Stylized 파도/물보라 제작과정 part3(한글자막) 금별 2025.12.10 171
1271 언리얼4 언리얼 - Stylized 파도/물보라 제작과정 part2(한글자막) 금별 2025.12.10 121
1270 언리얼4 언리얼 - Stylized 파도/물보라 제작과정 part1(한글자막) 금별 2025.12.10 138

 

Banner
 
Facebook Twitter GooglePlus KakaoStory NaverBand