BabylonJS 25회차 : 파티클/이펙트(불/연기/스파크)
목표: 시각 효과의 기본을 익힙니다.
핵심 개념: ParticleSystem, emitter
실습: 클릭 위치에 스파크 생성
산출물: 이펙트 데모
요약
파티클은 “작은 이미지(텍스처)를 매우 많이 뿌려서” 불, 연기, 스파크 같은 시각 효과를 만드는 방식입니다. 3D 모델링으로 불꽃을 만들기보다, 짧은 수명(lifeTime)과 빠른 속도(direction/minEmitPower)를 가진 파티클을 순간적으로 방출(emitRate 또는 manualEmitCount)하면 훨씬 간단하고 효율적입니다. 이번 회차에서는 ParticleSystem의 핵심 파라미터를 초보자 기준으로 정리하고, 클릭한 위치에 스파크를 “한 번 터뜨리는” 데모를 구현해봅니다.
목차
- 1) 핵심 포인트
- 2) ParticleSystem 구조 이해(텍스처, emitter, 수명)
- 3) 주요 파라미터 빠른 정리(불/연기/스파크 관점)
- 4) 불/연기/스파크 프리셋 비교 표
- 5) 실습: 클릭 위치에 스파크 생성(이펙트 데모)
- 6) 실행 단계(체크리스트)
- 7) 자주 막히는 지점(트러블슈팅)
- 8) 추가로 생각해볼 점
- 9) 블로그 최적화 정보
1) 핵심 포인트
- 파티클은 “작은 스프라이트 다수”입니다: 개별 파티클은 매우 가볍고, 전체로 불/연기/먼지 같은 덩어리 느낌을 만듭니다.
- emitter는 “파티클이 시작되는 곳”입니다: 메시에 붙일 수도 있고, 좌표(
Vector3)에 바로 뿌릴 수도 있습니다. - 스파크는 ‘짧게, 빠르게, 강하게’가 핵심: 수명은 짧게, 속도는 빠르게, 방출은 순간 폭발처럼 구성합니다.
- 연기는 ‘길게, 느리게, 퍼지게’가 핵심: 수명은 길게, 속도는 느리게, 크기는 커지도록 설정합니다.
- 성능은 “파티클 수”와 “업데이트 빈도”가 좌우: 화면에서 티 나지 않는 수준으로 적정량을 찾고, 필요 시 풀링(재사용)을 고려합니다.
2) ParticleSystem 구조 이해(텍스처, emitter, 수명)
2-1) 파티클이 그려지는 방식
- particleTexture: 파티클 하나가 어떤 이미지로 보일지 결정합니다(보통 원형 글로우/연기 텍스처).
- blendMode: 빛나게(가산합성) 보이게 할지, 일반 알파 블렌딩으로 섞을지 결정합니다.
스파크/불꽃은 가산합성이 자연스러운 편이고, 연기는 일반 알파가 흔합니다. - minLifeTime/maxLifeTime: 파티클이 살아있는 시간(초)입니다. “스파크 느낌”은 보통 0.1~0.4초처럼 짧게 잡습니다.
2-2) emitter란 무엇인가
emitter는 파티클이 “어디서 시작할지”를 정합니다.
- 메시 emitter: 칼끝, 엔진 배기구처럼 “오브젝트에 붙어서 움직이는” 이펙트에 적합합니다.
- 좌표 emitter(Vector3): 폭발 지점, 클릭 지점처럼 “그 자리에서 한 번 터지는” 이펙트에 적합합니다.
2-3) 스파크는 “연속 방출”보다 “버스트(한 번에 터뜨리기)”가 이해하기 쉽습니다
이번 실습은 클릭할 때마다 파티클 시스템을 짧게 생성하고, 잠깐 방출한 뒤 종료/정리합니다. 초보자 단계에서는 이 방식이 “원리와 효과”를 확인하기 가장 편합니다. 이후에 성능이 필요해지면 “파티클 시스템을 풀링(재사용)”하는 방식으로 확장할 수 있습니다.
3) 주요 파라미터 빠른 정리(불/연기/스파크 관점)
| 파라미터 | 의미 | 스파크 추천 | 연기 추천 |
|---|---|---|---|
minLifeTime/maxLifeTime |
수명(초) | 0.12~0.35 | 1.5~4.0 |
emitRate |
초당 방출량(연속) | 낮게 또는 0(버스트 권장) | 30~150 |
manualEmitCount |
프레임당 수동 방출(버스트) | 120~300(짧게) | 보통 사용 안 함 |
minEmitPower/maxEmitPower |
속도(발사 힘) | 6~16 | 0.2~1.4 |
direction1/direction2 |
방출 방향 범위 | 위로+퍼짐(원뿔) | 천천히 위로 |
gravity |
파티클에 작용하는 중력 | 아래로 약간(-9~-2) | 거의 0 또는 아주 약하게 |
minSize/maxSize |
크기 | 0.06~0.18 | 0.3~1.2 |
color1/color2/colorDead |
색/소멸 색 | 노랑/주황 → 투명 | 회색 → 투명 |
4) 불/연기/스파크 프리셋 비교 표
아래 표는 “느낌을 만드는 핵심 요소”만 추려서 정리한 것입니다. 실무에서는 여기에 텍스처(불꽃용/연기용)와 블렌드 모드를 함께 조합합니다.
| 이펙트 | 대표 설정 방향 | 블렌드 추천 | 초보자 팁 |
|---|---|---|---|
| 스파크 | 짧은 수명, 높은 속도, 순간 버스트 | 가산합성(빛나는 느낌) | manualEmitCount로 “한 번에 터지게” 만들면 이해가 빠릅니다. |
| 불 | 중간 수명, 위로 상승, 색 그라데이션 | 가산합성 또는 알파 | 색 변화(colorDead)와 크기 변화가 핵심입니다. |
| 연기 | 긴 수명, 느린 속도, 점점 커짐 | 알파 블렌딩(부드럽게) | 해상도 큰 텍스처보다 “적당한 텍스처 + 적당한 개수”가 안정적입니다. |
5) 실습: 클릭 위치에 스파크 생성(이펙트 데모)
실습 흐름은 다음과 같습니다.
- 바닥(ground)을 만들고 카메라를 붙입니다.
- 포인터(마우스) 클릭 시, 화면 좌표로 픽킹(
scene.pick)해서 3D 위치(pickedPoint)를 얻습니다. - 그 위치를 emitter로 하는 파티클 시스템을 생성합니다.
- 아주 짧은 시간만 방출한 뒤(
manualEmitCount), 자동으로 정리(dispose)합니다.
5-1) 완성 코드: 이펙트 데모(스파크)
아래 코드는 그대로 index.html로 실행할 수 있습니다(정적 서버 권장). 클릭한 바닥 위치에서 스파크가 터집니다. 성능을 위해 “한 번 터뜨리고 정리하는” 형태로 구성했습니다.
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>BabylonJS Particle Effect Demo (Click Spark)</title>
<style>
html, body { width:100%; height:100%; margin:0; overflow:hidden; font-family:system-ui, -apple-system, Segoe UI, Roboto, sans-serif; }
#renderCanvas { width:100%; height:100%; display:block; touch-action:none; }
#hud {
position: fixed; left: 12px; top: 12px;
width: min(520px, 94vw);
padding: 12px 12px 10px;
border-radius: 14px;
background: rgba(0,0,0,0.55);
border: 1px solid rgba(255,255,255,0.18);
color: rgba(255,255,255,0.92);
z-index: 10;
}
#hudTitle { margin:0 0 8px 0; font-size:14px; font-weight:700; }
#stats { font-size:12px; line-height:1.55; }
.row { display:flex; justify-content:space-between; gap:10px; }
.k { color: rgba(255,255,255,0.75); }
.v { font-variant-numeric: tabular-nums; }
#hint { margin-top:8px; color: rgba(255,255,255,0.72); font-size:12px; line-height:1.45; }
#hint code { background: rgba(255,255,255,0.12); padding: 1px 6px; border-radius: 6px; }
#btns { display:flex; flex-wrap:wrap; gap:8px; margin-top:10px; }
button {
appearance:none; border:1px solid rgba(255,255,255,0.22);
background: rgba(255,255,255,0.10);
color: rgba(255,255,255,0.92);
padding: 7px 10px; border-radius: 10px;
font-size:12px; cursor:pointer;
}
button:hover { background: rgba(255,255,255,0.16); }
</style>
<script src="https://cdn.babylonjs.com/babylon.js"></script>
</head>
<body>
<div id="hud">
<p id="hudTitle">클릭 스파크 이펙트 데모 (ParticleSystem)</p>
<div id="stats">
<div class="row"><span class="k">FPS</span><span class="v" id="fps">-</span></div>
<div class="row"><span class="k">Last Click</span><span class="v" id="pos">-</span></div>
<div class="row"><span class="k">Sparks Fired</span><span class="v" id="count">0</span></div>
</div>
<div id="btns">
<button id="toggleAuto">자동 연사 토글(데모용)</button>
<button id="clearMarks">마커 삭제</button>
</div>
<div id="hint">
<div>사용법: 바닥을 <code>클릭</code>하면 해당 위치에서 스파크가 터집니다.</div>
<div>관찰 포인트: <code>lifeTime</code>와 <code>emitPower</code>를 바꾸면 느낌이 크게 달라집니다.</div>
</div>
</div>
<canvas id="renderCanvas"></canvas>
<script>
const canvas = document.getElementById("renderCanvas");
const engine = new BABYLON.Engine(canvas, true);
const ui = {
fps: document.getElementById("fps"),
pos: document.getElementById("pos"),
count: document.getElementById("count")
};
let scene, camera, ground;
let fired = 0;
let auto = false;
let autoHandle = null;
const markers = [];
function createScene() {
const s = new BABYLON.Scene(engine);
s.clearColor = new BABYLON.Color4(0.06, 0.07, 0.09, 1.0);
camera = new BABYLON.ArcRotateCamera(
"camera",
Math.PI / 2,
Math.PI / 2.55,
18,
new BABYLON.Vector3(0, 1.2, 0),
s
);
camera.attachControl(canvas, true);
camera.wheelDeltaPercentage = 0.01;
const light = new BABYLON.HemisphericLight("hemi", new BABYLON.Vector3(0, 1, 0), s);
light.intensity = 1.0;
ground = BABYLON.MeshBuilder.CreateGround("ground", { width: 40, height: 40 }, s);
ground.position.y = 0;
const gmat = new BABYLON.StandardMaterial("gmat", s);
gmat.diffuseColor = new BABYLON.Color3(0.14, 0.15, 0.17);
gmat.alpha = 0.95;
ground.material = gmat;
ground.isPickable = true;
// 씬에 간단한 장애물(스파크 위치 감 잡기용)
const wallMat = new BABYLON.StandardMaterial("wmat", s);
wallMat.diffuseColor = new BABYLON.Color3(0.25, 0.28, 0.32);
for (let i = 0; i < 10; i++) {
const b = BABYLON.MeshBuilder.CreateBox("box" + i, { size: 1.8 }, s);
b.position.x = (Math.random() - 0.5) * 28;
b.position.z = (Math.random() - 0.5) * 28;
b.position.y = 0.9;
b.material = wallMat;
b.isPickable = false;
}
// 클릭 처리: 바닥 픽킹으로 3D 위치를 얻어 스파크 생성
s.onPointerObservable.add((pointerInfo) => {
if (pointerInfo.type !== BABYLON.PointerEventTypes.POINTERDOWN) return;
const pick = s.pick(s.pointerX, s.pointerY, (mesh) => mesh === ground);
if (!pick || !pick.hit || !pick.pickedPoint) return;
const p = pick.pickedPoint;
ui.pos.textContent = `${p.x.toFixed(2)}, ${p.y.toFixed(2)}, ${p.z.toFixed(2)}`;
createSparkAt(p);
createMarkerAt(p);
});
return s;
}
function createMarkerAt(point) {
// 클릭 위치를 눈으로 확인하기 위한 작은 마커(선택)
const m = BABYLON.MeshBuilder.CreateSphere("mark" + markers.length, { diameter: 0.18, segments: 12 }, scene);
m.position.copyFrom(point);
m.position.y += 0.09;
const mat = new BABYLON.StandardMaterial("markMat" + markers.length, scene);
mat.emissiveColor = new BABYLON.Color3(0.25, 0.45, 0.85);
m.material = mat;
m.isPickable = false;
markers.push(m);
// 마커가 너무 쌓이면 오래된 것부터 정리
if (markers.length > 40) {
const old = markers.shift();
if (old) old.dispose();
}
}
function createSparkAt(point) {
fired += 1;
ui.count.textContent = String(fired);
// 스파크는 짧은 버스트: 파티클 시스템을 만들고 잠깐만 방출 후 정리
const ps = new BABYLON.ParticleSystem("spark" + fired, 700, scene);
// 파티클 텍스처(글로우 계열이 스파크에 잘 맞습니다)
ps.particleTexture = new BABYLON.Texture("https://playground.babylonjs.com/textures/flare.png", scene);
// emitter를 좌표로 설정(클릭 지점에서 바로 터뜨리기)
ps.emitter = new BABYLON.Vector3(point.x, point.y + 0.05, point.z);
// 스파크 느낌: 짧은 수명
ps.minLifeTime = 0.12;
ps.maxLifeTime = 0.35;
// 크기(작게) + 약간 랜덤
ps.minSize = 0.06;
ps.maxSize = 0.16;
// 색: 노랑/주황 계열 → 투명(죽을 때)
ps.color1 = new BABYLON.Color4(1.0, 0.85, 0.35, 1.0);
ps.color2 = new BABYLON.Color4(1.0, 0.45, 0.12, 1.0);
ps.colorDead = new BABYLON.Color4(0.2, 0.1, 0.05, 0.0);
// 방출 방향 범위(원뿔처럼 위로 튀는 느낌)
ps.direction1 = new BABYLON.Vector3(-1.2, 2.6, -1.2);
ps.direction2 = new BABYLON.Vector3( 1.2, 3.4, 1.2);
// 속도(강한 느낌)
ps.minEmitPower = 7;
ps.maxEmitPower = 16;
// 중력(스파크는 아래로 떨어지는 느낌을 주면 자연스럽습니다)
ps.gravity = new BABYLON.Vector3(0, -9.0, 0);
// 방출 영역(클릭 지점 주변 아주 작은 범위)
ps.minEmitBox = new BABYLON.Vector3(-0.05, 0.0, -0.05);
ps.maxEmitBox = new BABYLON.Vector3( 0.05, 0.0, 0.05);
// 블렌드(빛나는 느낌)
ps.blendMode = BABYLON.ParticleSystem.BLENDMODE_ADD;
// 버스트 구성: 프레임당 수동 방출량(짧게 크게)
ps.emitRate = 0;
ps.manualEmitCount = 220;
// 시작
ps.start();
// 아주 짧은 시간 후 방출을 멈추고 정리
// (lifeTime이 짧아서, stop 후 잠깐 기다리면 화면에서 사라집니다)
setTimeout(() => {
ps.stop();
ps.manualEmitCount = 0;
}, 120);
setTimeout(() => {
ps.dispose();
}, 900);
}
// 데모용: 자동 연사(성능 관찰용)
document.getElementById("toggleAuto").addEventListener("click", () => {
auto = !auto;
if (auto) {
autoHandle = setInterval(() => {
const x = (Math.random() - 0.5) * 18;
const z = (Math.random() - 0.5) * 18;
createSparkAt(new BABYLON.Vector3(x, 0, z));
createMarkerAt(new BABYLON.Vector3(x, 0, z));
}, 250);
} else {
if (autoHandle) clearInterval(autoHandle);
autoHandle = null;
}
});
document.getElementById("clearMarks").addEventListener("click", () => {
while (markers.length) {
const m = markers.pop();
if (m) m.dispose();
}
});
scene = createScene();
engine.runRenderLoop(() => {
scene.render();
ui.fps.textContent = engine.getFps().toFixed(1);
});
window.addEventListener("resize", () => engine.resize());
</script>
</body>
</html>
5-2) “스파크가 그럴듯해지는” 튜닝 포인트
- 더 강한 폭발:
manualEmitCount를 올리고,minEmitPower/maxEmitPower를 조금 올립니다. - 더 짧고 날카롭게:
minLifeTime/maxLifeTime를 더 줄이고(예: 0.08~0.25),minSize/maxSize도 줄입니다. - 더 무겁게 떨어지게:
gravity의 Y를 더 낮게(예: -12) 조정합니다. - 빛나는 느낌 강화:
blendMode는 스파크에 보통BLENDMODE_ADD가 잘 맞습니다.
5-3) 불/연기로 확장하는 방법(방향만)
- 불: 수명을 조금 늘리고(0.4~1.2), 위로 상승 방향을 더 일관되게 만들고, 색을 주황/붉은 계열로 조정합니다.
- 연기: 수명을 길게(2~5), 속도를 낮게(0.2~1.0), 크기를 크게(0.5~1.5), 알파 블렌딩으로 부드럽게 섞습니다.
- 공통: 텍스처가 분위기를 크게 좌우하므로, 스파크/연기 전용 텍스처를 분리하면 품질이 빠르게 올라갑니다.
6) 실행 단계(체크리스트)
- 정적 서버(VSCode Live Server 등)에서 실행합니다.
- 바닥을 클릭했을 때 콘솔 오류가 없는지 확인합니다(픽킹/텍스처 로딩 실패 여부).
manualEmitCount,minLifeTime/maxLifeTime,minEmitPower/maxEmitPower를 하나씩 바꿔가며 “어떤 값이 어떤 느낌을 만드는지”를 기록합니다.- 자동 연사(데모용)를 켜고 FPS 변화를 보면서 “내 환경에서 안전한 파티클 예산”을 대략 잡아봅니다.
- 프로젝트에 적용할 때는 “필요한 순간에만 생성/재생”하도록 설계합니다(항상 켜두기 금지).
7) 자주 막히는 지점(트러블슈팅)
| 증상 | 가능 원인 | 해결 방법 |
|---|---|---|
| 클릭해도 아무 것도 안 나옴 | 픽킹 대상이 없거나 pick이 hit 실패 |
바닥 mesh에 isPickable=true 확인, pick 조건 함수가 올바른지 확인 |
| 파티클이 검은 네모로 보임 | 텍스처 로딩 실패 또는 알파 채널 문제 | 텍스처 URL 확인, 네트워크 탭에서 200 응답 확인, 알파가 있는 텍스처 사용 |
| 너무 티가 안 남 | 수명/크기/방출량이 너무 낮음 | manualEmitCount↑, maxSize↑, maxEmitPower↑로 단계적으로 조정 |
| FPS가 급격히 떨어짐 | 동시에 너무 많은 파티클/시스템 생성 | 방출량을 줄이고, 파티클 시스템을 재사용(풀링)하는 구조로 개선 |
| 전환/정리가 늦어 잔상이 남음 | 수명이 길거나 dispose 타이밍이 늦음 | maxLifeTime↓ 또는 dispose 타이밍을 조금 앞당김(단, 너무 빠르면 갑자기 사라짐) |
8) 추가로 생각해볼 점
- 성능을 위해 “파티클 시스템 풀링”을 고려하세요: 클릭마다 새로 만들고 버리는 방식은 이해가 쉽지만, 대량 사용 시에는 재사용 구조가 더 안정적입니다.
- 이펙트 품질은 ‘텍스처’ 비중이 큽니다: 파라미터를 잘 잡아도 텍스처가 어울리지 않으면 결과가 밋밋해질 수 있습니다.
- 씬 최적화와 연결: 파티클도 결국 렌더 비용이 있으므로, 이전 회차의 측정 루틴(FPS/병목)과 함께 운영하는 습관이 좋습니다.
- 실무 패턴: “히트 지점(총알 충돌)”, “발자국 먼지”, “폭발”처럼 트리거 이벤트에 연결하면 가장 활용도가 높습니다.
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

0 댓글