Media Art ProgrammingWeek 13 · Period 01 Handout

p5.js / Media Art Programming

13주차 1교시 — 파티클의 탄생과 소멸

Particle Systems 주제의 학생용 강의 자료입니다.

WEEK 13 PERIOD 01 P5.JS MEDIA ART

13주차 1교시 — 파티클의 탄생과 소멸

강의 아웃라인: week-13-outline 다음 교시: week-13-period2-student (Emitter와 파티클 이펙트)

항목 내용
대상 P5.js 입문자 (테크아트 전공, 7주차 클래스·9주차 벡터·10주차 힘·12주차 노이즈 학습 완료)
소요 시간 45분
기반 교재 Nature of Code — Chapter 4: Particle Systems
학습 목표
  • Particle 클래스를 설계하고 수명(lifespan)과 페이드아웃을 구현할 수 있습니다
  • 배열에 push()로 파티클을 생성하고 splice()로 소멸 처리할 수 있습니다
  • 배열을 역순으로 순회해야 하는 이유를 설명할 수 있습니다

도입

12주차에서 이어서 — “그 점들에 수명을 넣으면?”

지난주 2교시 Flow Field에서 점 100개가 보이지 않는 바람을 따라 화면을 흐르는 모습을 봤습니다. 그 점들은 한 가지 특징이 있었습니다 — 한 번 생기면 끝나지 않았습니다. 화면 밖으로 나가면 다시 안쪽으로 옮겨놓을 뿐, 사라지는 일은 없었습니다.

오늘은 그 점에 한 가지를 더 줍니다. 바로 수명(lifespan)입니다. 태어나고, 잠깐 움직이다가, 점점 옅어지면서 사라지는 점 — 그것이 파티클(particle)입니다.

노이즈로 만든 유기적인 움직임에 ’수명 있는 입자’를 합치면 연기, 불꽃, 눈 같은 시각 효과가 됩니다. 기말 프로젝트에서 쓰기 좋은 재료입니다.

파티클 시스템이란?

불꽃놀이가 터지는 순간 수백 개의 빛 조각이 사방으로 퍼졌다가 하나씩 사라집니다. 피어오르는 연기도, 내리는 눈도 마찬가지입니다. 작은 입자 수천 개가 각각 다른 속도로, 다른 경로로 움직입니다.

이 현상들의 공통점은 — 아주 많은 개별 입자가 생겨나고, 각자 움직이고, 결국 사라진다는 것입니다. 이것을 코드로 표현하는 기법이 오늘 배울 파티클 시스템(Particle System)입니다.

오늘 할 일

이번 시간이 끝나면 다음 세 가지를 할 수 있게 됩니다.

  1. Particle 클래스를 설계하고 수명페이드아웃을 구현한다
  2. 배열에서 수명이 다한 파티클을 splice()로 안전하게 제거한다
  3. 수백 개의 파티클이 생성·소멸하는 시스템의 기본 구조를 이해한다

지금까지 배운 것과의 연결

오늘 배울 내용은 완전히 새로운 것이 아니라, 이미 가진 도구들의 조합입니다.

여기에 수명(lifespan)이라는 개념 하나만 추가하면 파티클이 완성됩니다.


핵심 개념 1 — 파티클 = 움직이는 객체 + 수명

9~10주차에서 만들었던 Mover 클래스를 떠올려 봅니다. 위치(position), 속도(velocity), 가속도(acceleration)가 있었습니다. 힘을 적용하면 가속도가 바뀌고, 가속도가 속도를 바꾸고, 속도가 위치를 바꾸는 패턴이었습니다.

파티클은 여기에 한 가지 개념만 추가하면 됩니다. 바로 수명(lifespan)입니다.

비유하자면, Mover가 ’수명 제한이 없는 공’이라면 Particle은 ‘촛불’입니다. 태어나는 순간부터 수명이 정해져 있고, 시간이 지나면서 점점 약해지다가 결국 꺼집니다.

수명은 lifespan이라는 숫자를 255에서 시작해 매 프레임 조금씩 줄여나가는 방식으로 구현합니다. 왜 하필 255냐면 — p5.js에서 색상의 알파(투명도) 값 범위가 0~255이기 때문입니다. lifespan 값을 그대로 알파에 넣으면, 수명이 줄어들수록 자연스럽게 투명해집니다. 이것이 바로 페이드아웃(fade-out) 효과입니다.

lifespan: 255 → 완전 불투명 (방금 태어남)
lifespan: 127 → 반투명 (수명 절반 남음)
lifespan: 0   → 완전 투명 (소멸)
01 particle lifecycle lifespan
확인 질문

lifespan을 매 프레임 2씩 줄인다면, 파티클이 대략 몇 프레임 동안 살아있을까요?

: 약 127~128프레임. 60fps 기준 약 2초.


핵심 개념 2 — Particle 클래스 설계

7주차에서 클래스를 만들 때의 패턴을 떠올려 봅니다 — constructor, 메서드들, 그리고 프로퍼티. Particle 클래스의 전체 구조는 다음과 같습니다.

02 mover to particle anatomy
class Particle {
  constructor(x, y) {
    this.position = createVector(x, y);
    // 초기 속도: 위쪽으로 살짝 퍼지는 랜덤 방향
    this.velocity = createVector(random(-1, 1), random(-2, 0));
    // 가속도: 중력 (아래 방향)
    this.acceleration = createVector(0, 0.05);
    // 수명: 255에서 시작
    this.lifespan = 255;
  }

  update() {
    // 9주차에서 배운 벡터 물리 업데이트
    this.velocity.add(this.acceleration);
    this.position.add(this.velocity);
    // 매 프레임 수명 감소
    this.lifespan -= 2;
  }

  display() {
    // lifespan을 알파 값으로 사용 → 페이드아웃!
    stroke(0, this.lifespan);
    fill(127, this.lifespan);
    ellipse(this.position.x, this.position.y, 12);
  }

  // 이 파티클이 소멸 대상인지 확인
  isDead() {
    return this.lifespan < 0;
  }
}

한 부분씩 살펴봅니다.

constructor(x, y) - position: 파티클이 태어나는 위치. 나중에 마우스 위치를 넣습니다. - velocity: random(-1, 1)random(-2, 0)으로 약간씩 다른 방향으로 퍼져나갑니다. 파티클마다 움직임이 다른 비결입니다. - acceleration: 중력. 아주 작은 값(0.05)이면 충분합니다. 파티클이 천천히 아래로 떨어지게 합니다. - lifespan: 255에서 시작. 이 숫자가 0 아래로 내려가면 소멸 대상으로 판단합니다.

update() - 9주차에서 익숙한 패턴입니다: 가속도 → 속도, 속도 → 위치. - 여기에 this.lifespan -= 2 한 줄이 추가됐습니다. 이것이 파티클의 핵심입니다.

display() - fill(127, this.lifespan) — 두 번째 인자가 알파(투명도)입니다. lifespan이 줄면 투명해집니다. - stroke(0, this.lifespan) — 테두리도 같이 투명해져야 자연스럽습니다.

isDead() - lifespan이 0 미만이면 true를 반환합니다. 단 한 줄이지만 아주 중요한 메서드입니다.

단일 파티클을 화면에 띄우는 코드입니다.

let p;

function setup() {
  createCanvas(640, 360);
  p = new Particle(width / 2, 20);
}

function draw() {
  background(220);
  p.update();
  p.display();

  // 수명이 끝나면 새로 생성
  if (p.isDead()) {
    p = new Particle(width / 2, 20);
  }
}

실행하면 파티클 하나가 위에서 생겨나 중력에 의해 떨어지면서 점점 투명해지다 사라지고, 다시 새로 생겨납니다.

확인 질문

this.lifespan -= 2this.lifespan -= 10으로 바꾸면 어떻게 될까요?

: 수명이 5배 빠르게 줄어서 파티클이 훨씬 빨리 사라집니다. 약 25프레임, 0.4초 정도.


핵심 개념 3 — 배열로 다수의 파티클 관리

파티클 하나로는 별로 인상적이지 않습니다. 파티클 시스템의 매력은 수백 개가 동시에 움직일 때 나타납니다. 배열을 사용해서 구현합니다.

기본 전략은 세 단계입니다.

  1. 매 프레임 새 파티클을 배열에 push()
  2. 모든 파티클을 update() + display()
  3. 수명이 끝난 파티클을 splice()로 제거
let particles = [];

function setup() {
  createCanvas(640, 360);
}

function draw() {
  background(220);

  // 매 프레임 새 파티클 생성
  particles.push(new Particle(width / 2, 20));

  // 역순으로 순회하면서 업데이트 + 표시 + 제거
  for (let i = particles.length - 1; i >= 0; i--) {
    particles[i].update();
    particles[i].display();

    if (particles[i].isDead()) {
      particles.splice(i, 1);
    }
  }
}

여기서 아주 중요한 부분이 역순 순회입니다.

splice(i, 1)은 인덱스 i에 있는 요소를 배열에서 제거합니다. 그러면 그 뒤에 있던 모든 요소의 인덱스가 한 칸씩 앞으로 당겨집니다. 이것이 순방향 순회에서 문제를 일으킵니다.

03 reverse splice index shift
순방향 순회에서 문제가 생기는 상황:

배열: [A, B, C, D, E]    인덱스: 0, 1, 2, 3, 4

i=0: A 확인 → 정상
i=1: B 확인 → B가 제거 대상이다. splice(1, 1) 실행
배열: [A, C, D, E]        인덱스: 0, 1, 2, 3
                           C가 원래 2번이었는데 이제 1번으로!
i=2: D 확인 → C를 건너뛰었다!  (C는 체크 안 됨)

C가 확인되기도 전에 건너뛰어졌습니다. 제거되어야 할 파티클이 한 프레임 더 남을 수 있는 것입니다.

역순 순회에서는 문제 없음:

배열: [A, B, C, D, E]    인덱스: 0, 1, 2, 3, 4

i=4: E 확인 → 정상
i=3: D 확인 → 정상
i=2: C 확인 → C가 제거 대상이다. splice(2, 1) 실행
배열: [A, B, D, E]        인덱스: 0, 1, 2, 3
                           D, E의 인덱스가 바뀌었지만, 이미 체크 완료!
i=1: B 확인 → 정상 (인덱스 변화 영향 없음)
i=0: A 확인 → 정상

역순으로 순회하면, splice로 인한 인덱스 변화가 이미 체크한 요소들에만 영향을 미쳐서 문제가 없습니다.

외워둘 패턴

배열에서 요소를 제거하면서 순회할 때는 반드시 역순으로 — for (let i = 배열.length - 1; i >= 0; i--).

// 이 패턴을 기억합니다
for (let i = particles.length - 1; i >= 0; i--) {
  // ... 작업 ...
  if (조건) {
    particles.splice(i, 1);
  }
}

배열에서 splice가 제대로 동작하는지 확인하려면, 배열의 길이를 화면에 표시해 봅니다.

// draw() 안에 추가
fill(0);
noStroke();
text("파티클 수: " + particles.length, 10, 20);
04 particle count memory check

제대로 작동하면 파티클 수가 일정 범위 안에서 유지됩니다 (보통 120~130개). 만약 splice를 빼면 배열이 계속 커지면서 프레임 저하가 생깁니다.


예제 — 마우스 파티클 시스템

마우스 위치에서 파티클이 생성되는 전체 코드입니다.

let particles = [];

function setup() {
  createCanvas(640, 360);
}

function draw() {
  background(220);

  // 마우스 위치에서 파티클 생성
  particles.push(new Particle(mouseX, mouseY));

  for (let i = particles.length - 1; i >= 0; i--) {
    particles[i].update();
    particles[i].display();

    if (particles[i].isDead()) {
      particles.splice(i, 1);
    }
  }

  // 파티클 수 표시
  fill(0);
  noStroke();
  text("파티클 수: " + particles.length, 10, 20);
}

class Particle {
  constructor(x, y) {
    this.position = createVector(x, y);
    this.velocity = createVector(random(-1, 1), random(-2, 0));
    this.acceleration = createVector(0, 0.05);
    this.lifespan = 255;
  }

  update() {
    this.velocity.add(this.acceleration);
    this.position.add(this.velocity);
    this.lifespan -= 2;
  }

  display() {
    stroke(0, this.lifespan);
    fill(127, this.lifespan);
    ellipse(this.position.x, this.position.y, 12);
  }

  isDead() {
    return this.lifespan < 0;
  }
}

값을 바꿔보며 변화를 체험해 봅니다.

파라미터 튜닝

몇 가지 값을 조정해도 결과가 완전히 달라집니다. 이것이 파라미터 튜닝이고, 아트에서 가장 중요한 부분 중 하나입니다.


정리

핵심을 세 줄로 요약합니다.

  1. 파티클 = Mover + 수명(lifespan) — 9~10주차에서 배운 벡터 물리에 lifespan 하나만 추가하면 파티클이 된다
  2. 페이드아웃 — lifespan 값을 색상의 알파에 직접 넣어 자연스러운 소멸 효과를 만든다
  3. 배열 관리push()로 생성, splice()로 제거, 반드시 역순 순회

2교시 예고

지금은 draw() 안에서 파티클 생성과 관리를 직접 합니다. 파티클 시스템이 하나일 때는 괜찮지만, 여러 개가 필요하면 draw()가 금방 복잡해집니다. 2교시에서는 Emitter(방출기) 클래스를 만들어 파티클 시스템을 더 간결하게 관리하는 방법을 배우고, 불꽃놀이 같은 이펙트도 만듭니다.