본문 바로가기

파티클 속성 읽기 및 쓰기

Get·Set 노드로 위치·속도·색상·크기·수명 속성을 읽고 계산 결과를 Spawn 또는 Update 단계에 맞춰 기록합니다.

입자의 위치와 속도, 색, 크기, 수명은 모두 속성(attribute)으로 저장됩니다. 그래서 나이아가라 스크립트로 입자의 동작을 바꾼다는 것은 결국 이 속성을 읽어 계산하고 다시 쓰는 일입니다. 입자라는 객체가 가진 여러 변수에 프로그램으로 접근해 값을 바꾸는 것과 비슷합니다. 이 글에서는 Map Get으로 속성을 읽고 Map Set으로 쓰는 방법을 자세히 살펴보고, 속도를 틱마다 줄이는 감속 모듈을 만들면서 프레임률에 따라 결과가 어떻게 달라지는지 계산해 봅니다.


속성 읽기

입자의 지금 상태를 알거나 다른 계산의 입력으로 쓰려면, 먼저 그 속성의 값을 읽어 와야 합니다. 나이아가라에서는 주로 Map Get 노드로 속성을 읽습니다. 그래프에 Map Get을 만들고 입력 파라미터 맵을 연결한 뒤, 노드의 파라미터 추가 메뉴에서 Particles.Position이나 Particles.Velocity처럼 읽을 속성을 고릅니다. 그러면 Map Get이 그 속성의 지금 값을 출력 핀으로 내보내고, 이 출력을 Add나 Multiply 같은 연산 노드, 또는 Map Set의 입력 핀에 연결해 씁니다.

자주 읽는 속성과 그 쓰임은 다음과 같습니다.

  • Particles.Position: 입자의 지금 위치로, Position 타입입니다. 위치를 기준으로 다른 입자를 만들거나 어떤 영역에 들어왔는지 검사할 때 읽습니다.
  • Particles.Velocity: 입자의 지금 3차원 속도로, Vector 타입입니다. 속도에 따라 색이나 크기를 바꾸거나 속도에 비례하는 힘을 줄 때 읽습니다.
  • Particles.Color: 입자의 지금 색입니다. 다른 색을 더하거나 곱해 색을 조절할 때 읽습니다.
  • Particles.Lifetime: 입자의 전체 수명으로, Float입니다.
  • Particles.NormalizedAge: 나이를 수명으로 나눈 정규화된 나이로, Float입니다. 수명이 다하면 입자를 없애는 보통의 경우 0에서 1 사이이며, 크기나 색, 투명도를 바꾸는 커브와 그래디언트의 입력으로 많이 씁니다.
  • Particles.SpriteSize: 스프라이트의 지금 크기로, Vector2입니다.
  • Particles.ID: 나이아가라의 ID 구조체로, Index와 AcquireTag를 담고 있습니다. 입자를 계속 알아보는 데 쓰려면 이미터의 Requires Persistent IDs 설정과 지원되는 경로를 확인해야 하며, 단순한 정수나 지금 실행 중인 순번으로 다루지 않습니다.

속성 쓰기

속성을 쓸 때는 Map Set을 씁니다. 여기서 말하는 것은 모듈 안의 그래프에 놓는 Map Set이며, 스택에서 파라미터 값을 직접 정하는 항목과는 구분합니다.

쓰는 방법은 다음과 같습니다. 먼저 Map Set에 입력 파라미터 맵을 연결하고, 파라미터 추가 메뉴에서 쓸 속성, 예를 들어 Particles.Color를 추가합니다. 다음으로 연산 노드가 계산한 값을 그 속성의 입력 핀에 연결하는데, 이때 핀의 데이터 타입과 값의 단위를 맞춥니다. 마지막으로 Map Set의 출력 맵을 다음 Map Set이나 Output Module에 연결합니다. 이 연결은 속성의 집합을 넘기는 것이며 Do 같은 실행 핀이 아닙니다.

자주 쓰는 속성과 그 쓰임은 다음과 같습니다.

  • Particles.Position: 입자의 위치를 직접 정하거나 바꿉니다. 입자를 어떤 지점으로 순간 이동시키거나, 메시 표면 같은 바깥 데이터로 계산한 위치에 놓을 때 씁니다.
  • Particles.Velocity: 입자의 속도를 직접 정하거나 바꿉니다. 새 방향과 크기의 속도를 바로 줄 때 씁니다.
  • Particles.Color: 입자의 색을 바꿉니다. 조건에 따라 색을 바꾸거나 바깥 입력에 따라 색을 조절할 때 씁니다.
  • Particles.Lifetime: 입자의 전체 수명을 바꿉니다. 남은 시간이 아니라 전체 시간이라는 점에 주의합니다. 수명을 판정하는 모듈이 참고할 전체 시간을 바꿀 때 쓰며, 입자를 곧바로 없애고 싶다면 그 경우의 Kill 처리나 Alive 속성, 실행 순서를 확인합니다.

읽고 계산해 다시 쓰기

가장 흔한 사용법은 입자의 지금 속성을 읽어 계산한 뒤, 같은 속성에 다시 쓰는 것입니다. 입자가 움직이다가 점점 느려지는 감속을 예로 들어 보겠습니다. 이를 만들려면 지금 속도를 읽어 일정한 비율로 줄인 뒤 다시 속도에 써야 합니다.

그래프는 다음 순서로 잇습니다. 먼저 Map Get으로 Particles.Velocity를 읽습니다. 다음으로 그 출력에 Multiply(Vector × Float)로 0.98 같은 Float 값을 곱해, 틱마다 속도를 2%씩 줄입니다. 마지막으로 Map Set의 Particles.Velocity 입력에 곱한 결과를 연결하고, 바뀐 맵을 Output Module로 넘깁니다. 이 모듈은 Particle Update에 두되, 그 속도를 쓰는 Solve Forces and Velocity보다 앞에 둡니다.

이 방식에는 한 가지 문제가 있습니다. 0.98은 틱마다 곱해지므로, 1초 동안 틱이 몇 번 있었는지에 따라 감속의 양이 달라집니다. 이를 숫자로 확인해 봅니다.


같은 1초라도 틱 수가 다르면

다른 힘이나 저항, 속도를 쓰는 모듈이 없고 초기 속력이 1000cm/s라고 해 봅시다. 틱마다 0.98을 그대로 곱하면, 1초에 60번 계산하는 60Hz에서는 1초 뒤에 1000 × 0.98⁶⁰ ≈ 297.55cm/s가 됩니다. 30Hz에서는 1초에 30번만 곱하므로 1000 × 0.98³⁰ ≈ 545.48cm/s입니다. 틱 수가 절반이어서 감속이 적용된 횟수도 절반으로 줄었기 때문입니다.

프레임률과 상관없이 같은 감속을 주려면, 60Hz일 때의 감속률을 기준으로 삼아 배율을 pow(0.98, 60 × Engine.DeltaTime)으로 계산합니다. 30Hz라면 한 틱의 시간 간격 Engine.DeltaTime은 1/30초이므로, 한 틱의 배율은 0.98^(60 × 1/30) = 0.98² = 0.9604입니다. 이 배율을 1초 동안 30번 곱하면 0.9604³⁰ = 0.98⁶⁰이 되어 60Hz일 때와 같아집니다.

프레임률한 틱의 배율1초 동안 곱한 횟수1초 뒤 속력(보정 전)1초 뒤 속력(보정 후)
60Hz0.9860약 297.55cm/s약 297.55cm/s
30Hz보정 전 0.98, 보정 후 0.960430약 545.48cm/s약 297.55cm/s

이 계산은 시간 간격이 일정하고 입력 조건이 같을 때의 속도만 비교한 것입니다. 프레임 전체의 이동 경로나 다른 힘까지 똑같아진다는 뜻은 아닙니다. 기본으로 제공하는 Drag 모듈과 Solve Forces and Velocity를 써서 저항을 주는 방법도 있으며, 직접 만든 감속을 쓴다면 이미 적용 중인 저항과 겹치지 않게 합니다.


초기화와 업데이트의 차이

속성을 다룰 때 꼭 구분해야 하는 것이 초기화(initialization)와 업데이트(update)입니다. Particle Spawn 그룹의 Initialize Particle은 입자가 생기는 순간에 색과 수명, 크기 같은 초기 속성을 정하며, 여기서 정한 값이 그 입자의 기본값이 됩니다. 초기 위치와 속도는 Shape Location이나 Add Velocity 같은 다른 Spawn 모듈이 쓸 수도 있습니다.

반면 Particle Update 그룹의 모듈은 입자가 살아 있는 동안 틱마다 속성을 읽고 바꿉니다. 모듈마다 지금 속성을 읽어 결과를 쓰지만, 모든 모듈이 모든 속성을 쓰는 것은 아닙니다. 그래서 Initialize Particle에서 정한 초기값은 Particle Update의 모듈들에 의해 계속 바뀔 수 있고, 어떤 속성이 최종적으로 어떤 값이 되는지는 그 속성을 쓰는 모듈들의 순서로 정해집니다.


정리

  • 입자의 위치, 속도, 색, 크기, 수명은 모두 속성으로 저장되며, Map Get으로 읽고 Map Set으로 씁니다.
  • Particles.Position은 Position 타입이고, Particles.ID는 Index와 AcquireTag를 담은 구조체이므로 단순한 정수로 다루지 않습니다.
  • Particles.Lifetime은 남은 시간이 아니라 전체 수명이며, 입자를 곧바로 없애려면 Kill 처리나 Alive 속성을 확인합니다.
  • 틱마다 고정된 배율을 곱하면 프레임률에 따라 결과가 달라지므로, pow(배율, 기준 Hz × Engine.DeltaTime)처럼 시간 간격을 반영합니다.
  • Particle Spawn에서 정한 초기값은 Particle Update의 모듈들이 계속 바꿀 수 있습니다.

연습 문제

1. 20Hz에서 같은 감속 맞추기

60Hz에서 틱마다 0.99를 곱하던 감속을 20Hz에서도 같은 1초 감속률로 맞추려 합니다. 한 틱의 시간 간격과 적용할 배율을 구하고, 초기 속력이 100cm/s일 때 1초 뒤의 값을 예상해 보세요. 이 모듈을 Particle Spawn에 한 번만 두었다면 같은 예상이 맞을까요?

해설 보기

20Hz의 한 틱 간격은 1 ÷ 20 = 0.05초이고, 지수는 60 × 0.05 = 3입니다. 그래서 한 틱의 배율은 0.99³ = 0.970299이고, 20틱 뒤의 속력은 100 × 0.970299²⁰ = 100 × 0.99⁶⁰ ≈ 54.72cm/s입니다.

Particle Spawn에 한 번만 두었다면 이 예상은 맞지 않습니다. Spawn 모듈은 입자가 생길 때 한 번만 실행되므로, 20번의 Update 반복이 일어나지 않습니다. 이 예상은 감속을 주는 다른 모듈 없이 Solve Forces and Velocity 앞에서 Update마다 적용한다는 조건에서 성립합니다.

2. 덮어써진 색

Particle Update에 높이에 따라 Particles.Color를 쓰는 모듈을 두었는데, 그 아래에 수명에 따라 색을 정하는 Color 모듈이 있습니다. 미리보기에서는 높이에 따른 색 변화가 보이지 않습니다. 왜 그럴까요?

해설 보기

같은 그룹의 모듈은 위에서 아래로 실행되므로, 아래쪽의 Color 모듈이 위에서 쓴 Particles.Color를 다시 덮어씁니다. 그래서 최종적으로 남는 값은 수명에 따른 색입니다.

높이에 따른 색을 남기려면 높이 모듈을 Color 모듈보다 아래로 옮기거나, Color 모듈을 끕니다. 두 효과를 함께 쓰고 싶다면 한 모듈이 다른 모듈의 결과를 읽어 곱하는 식으로 합치는 규칙을 정합니다.

3. 수명을 줄여도 사라지지 않는 입자

충돌한 입자를 곧바로 없애려고, Particle Update에서 Particles.Lifetime을 0.1로 바꿨습니다. 그런데 이미 0.5초를 산 입자는 사라지는데, 막 생긴 입자는 조금 더 남아 있습니다. 이유는 무엇일까요?

해설 보기

Particles.Lifetime은 남은 시간이 아니라 전체 수명이므로, 수명을 0.1로 바꾸면 나이가 0.1초를 넘은 입자부터 사라집니다. 이미 0.5초를 산 입자는 바로 조건을 넘어 사라지지만, 막 생겨 나이가 0.1초가 안 된 입자는 그 나이에 이를 때까지 남습니다.

곧바로 없애고 싶다면 수명을 바꾸는 대신 그 경우의 Kill 처리를 쓰거나 Alive 속성을 거짓으로 쓰고, 이 처리가 Particle State와 어떤 순서로 실행되는지 확인합니다.

다음 글 스크립트 최적화 기본 기법에서는 스크립트의 계산 비용을 줄이는 방법과, 최적화한 뒤에도 결과가 같은지 확인하는 방법을 다룹니다.