# AI 꾸러미 — 층 쌓기 (핵 · 빛무리 · 불티 · 연기) — VFX layering (core · glow · sparks · smoke · ground light)
> 마법 하나를 밝은 핵 · 큰 빛무리 · 튀는 불티 · 꼬리 연기 · 바닥 빛 다섯 층으로 나눠 따로 움직여서, 효과가 한 덩어리가 아니라 풍성하게 보인다.  
> 견본: https://ai-techstudio.web.app/#t/i171

이 문서를 코드 도우미(Claude Code · Cursor · ChatGPT 등)에 그대로 주면 돼요. 「## 주문서」 가 할 일, 나머지는 참고 자료예요.

## 주문서

### 만들어 줘: 층 쌓기 (핵 · 빛무리 · 불티 · 연기) — VFX layering (core · glow · sparks · smoke · ground light)

#### 1. 목표
날아다니는 마법 구슬을 다섯 층(핵 · 빛무리 · 불티 · 연기 · 바닥 빛)으로 쌓아 만들어 줘 — 층마다 따로 움직이고 하나씩 켜고 끌 수 있게. 분위기는 보랏빛 비전 마법.

#### 2. 핵심 기술 용어
- **VFX layering (core · glow · sparks · smoke · ground light)** — 효과 층 쌓기 — 한 마법을 여러 층으로
- **HDR core** — 밝기 1 을 넘겨 하얗게 타는 가운데
- **Velocity inheritance** — 움직이는 몸의 속도를 입자가 조금 물려받기
- **Ground glow decal + PointLight** — 바닥에 번지는 빛 판 + 실제 점광

#### 3. 환경
- 플랫폼: three.js r186 (ES 모듈 · TypeScript, `import * as THREE from "three"`), WebGL2, 외부 라이브러리 추가 없이
- 화면: 3D · 브라우저 — PC 와 폰(가로 844×390 · 세로 390×844) 모두, 60fps 목표

#### 4. 조건
- 층마다 입자 묶음(Pool)을 따로 두고 renderOrder 로 연기(2) → 불티(3) → 핵(4) 순서로 그린다
- 핵은 밝기 1 을 넘는 HDR 색, 빛무리는 크고 옅게 — 둘의 크기 차이가 3배 이상
- 불티 · 연기는 몸의 속도를 조금(−0.25 · −0.1) 물려받아 꼬리가 진다
- 바닥 빛은 빛 판 + 점광 하나, 점광 그림자는 끈다
- 층마다 켜고 끄는 단추 다섯 개

#### 5. 완성 기준 (이게 보이면 성공)
- 1.5초마다 층이 하나씩 더해지며(핵 → + 빛무리 → + 불티 → + 연기 → + 바닥 빛) 점점 풍성해진다
- 구슬이 원을 그리며 돌 때 불티 · 연기가 뒤로 흘러 꼬리가 생긴다
- 바닥 빛을 켜면 구슬 밑 돌바닥이 보랏빛으로 물든다
- 폰에서도 끊김 없이 (입자 1400개 안)

#### 6. 진행 방식
- 핵심 코드 위주로, 설명은 짧게. 내 프로젝트에 끼워 넣기 쉬운 함수 · 클래스로 나눠 줘.
- 처음 화면에 바로 결과가 보이게, 그리고 켬/끔(또는 전/후) 비교를 할 수 있게 만들어 줘.
- 그림 · 소리 · 모델 파일이 필요하면 코드로 만든 임시 대체물로 먼저 돌아가게 하고, 진짜 파일로 바꿀 자리를 표시해 줘.
- 마지막에 「확인 방법」(무엇을 보면 성공인지)과 「조절할 값」 목록을 짧게 정리해 줘.
- 답변과 코드 주석은 한국어로 해 줘.

## 원리
- 핵: 하얀 작은 빛(밝기 9) + 그 둘레 연보라(밝기 4). 크기를 sin(30t) 으로 조금 떨게.
- 빛무리: 크고 옅은 빛 두 장(1.5 · 2.8) + 천천히 도는 별 모양 — 핵보다 훨씬 크고 흐리게.
- 불티: 초당 70개, 사방으로 튀고 중력 3 · 바닥에서 튕김. 몸 속도의 −0.25 배를 물려받아 뒤로 흘린다.
- 연기: 초당 34개, 크게 퍼지며(0.35 → 1.25) 컬 잡음(0.35)으로 휘감기고, 몸 속도의 −0.1 배를 물려받아 꼬리가 된다.
- 바닥 빛: 바닥에 붙인 빛 판(더하기) + 실제 점광(13 ± 1.5, 17Hz 일렁) — 주변 바닥과 기둥이 같이 밝아진다.

## 핵심 코드 — 다섯 층을 매 프레임 쌓기 (pos = 구슬 자리, vx · vz = 구슬 속도)
(발췌: demos/demosSkillA.ts i171 update() 를 정리 (Pool · RAMP · flat 은 i170 과 같은 도구))
```ts
const CORE = flat(0xfff4ff, 9, 1), CORE2 = flat(0xd8a0ff, 4, 0.9);
const HALO = flat(0x9a50ff, 2.6, 0.7), HALO2 = flat(0x6a30ff, 1.1, 0.5);
let accS = 0, accM = 0;

function layers(t: number, dt: number): void {
  orb.clear(); // 핵 · 빛무리는 매 프레임 새로 (한 프레임짜리 입자)
  if (on[0]) { // 핵
    orb.spawn({ x: pos.x, y: pos.y, z: pos.z, life: 1, s0: 0.3 + Math.sin(t * 30) * 0.02, ramp: CORE, shape: 0 });
    orb.spawn({ x: pos.x, y: pos.y, z: pos.z, life: 1, s0: 0.55, ramp: CORE2, shape: 0 });
  }
  if (on[1]) { // 빛무리 — 크고 옅게
    orb.spawn({ x: pos.x, y: pos.y, z: pos.z, life: 1, s0: 1.5 + Math.sin(t * 7) * 0.08, ramp: HALO, shape: 0 });
    orb.spawn({ x: pos.x, y: pos.y, z: pos.z, life: 1, s0: 2.8, ramp: HALO2, shape: 0 });
  }
  if (on[2]) for (accS += dt * 70; accS >= 1; accS--) { // 불티 초당 70
    const d = sphereDir(), sp = R(1.2, 3.2);
    sparks.spawn({ x: pos.x, y: pos.y, z: pos.z, vx: d.x * sp - vx * 0.25, vy: d.y * sp + 0.6, vz: d.z * sp - vz * 0.25,
      life: R(0.45, 0.9), s0: 0.07, s1: 0.02, ramp: RAMP.magicSpark, st: 0.06, shape: 2, drag: 1.2, grav: 3, bnc: 0.4 });
  }
  if (on[3]) for (accM += dt * 34; accM >= 1; accM--) { // 연기 초당 34
    smoke.spawn({ x: pos.x, y: pos.y, z: pos.z, vx: -vx * 0.1, vy: R(0.2, 0.5), vz: -vz * 0.1,
      life: R(1.3, 2.0), s0: 0.35, s1: 1.25, ramp: RAMP.magicSmoke, shape: 1, rot: R(0, Math.PI * 2), rv: R(-1, 1), drag: 1.5, curl: 0.35 });
  }
  // 바닥 빛: 빛 판 + 점광 하나
  light.position.set(pos.x, pos.y - 0.3, pos.z);
  light.intensity = on[4] ? 13 + Math.sin(t * 17) * 1.5 : 0;
  disc.visible = on[4];
  disc.position.set(pos.x, 0.015, pos.z);
}
```

## 흔한 실수 · 확인 목록
- [ ] **점광을 켰다 껐다 하면 판마다 멈칫한다** — 점광 수가 바뀌면 three.js 가 셰이더를 통째로 다시 컴파일한다. 점광은 그대로 두고 intensity 만 0 으로. 보물 동굴에서 3~5초 멈춤이 이것이었다.
- [ ] **핵과 빛무리를 비슷한 크기로 두면 덩어리로 뭉친다** — 핵 0.3 · 빛무리 1.5 · 2.8 처럼 크기 차이를 크게, 빛무리는 투명도 0.5~0.7 로 옅게.
- [ ] **불티 · 연기가 구슬과 같이 움직이면 꼬리가 안 생긴다** — 구슬 속도의 반대(−0.25 · −0.1)를 조금 물려받게 하면 뒤로 흘러 꼬리가 된다.
- [ ] **층을 한 묶음에 섞으면 연기가 핵을 덮는다** — 묶음을 나누고 renderOrder 연기 2 → 불티 3 → 핵 4.

## 완성 기준 체크리스트
- [ ] 1.5초마다 층이 하나씩 더해지며(핵 → + 빛무리 → + 불티 → + 연기 → + 바닥 빛) 점점 풍성해진다
- [ ] 구슬이 원을 그리며 돌 때 불티 · 연기가 뒤로 흘러 꼬리가 생긴다
- [ ] 바닥 빛을 켜면 구슬 밑 돌바닥이 보랏빛으로 물든다
- [ ] 폰에서도 끊김 없이 (입자 1400개 안)

## 이 기술 정보
- id: `i171` · 분류: 이펙트 · 연출 › 스킬 VFX (마법 · 미사일) · 3D · 난이도 보통 · 폰 부담 보통 (폰 주의) — 입자 묶음 3개(연기 · 불티 · 핵) + 점광 1개. 점광은 비추는 모든 재질에 계산이 붙으니 효과 하나에 하나만, 그림자는 끈다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i171
- 쓰면 좋을 때: 효과가 「빛 덩어리 하나」처럼 밋밋할 때 / 여러 마법(불 · 얼음 · 독)을 같은 틀로 만들 때 — 층의 색 · 양만 바꾼다
- 쓰지 말 때: 작고 많은 효과(수십 개 동시) — 층마다 입자 · 점광이 늘어난다. 작은 것은 핵 + 빛무리 두 층만 / 점광을 효과마다 켜면 셰이더가 다시 컴파일된다 — 점광 수는 늘 같게

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### ctrlTog — `src/demos/demosSkillA.ts:1416`
```ts
const ctrlTog = (label: string, value: boolean, on: (v: boolean) => void): Control => ({ type: 'toggle', label, value, on });
```

### i171 견본 항목 — `src/demos/demosSkillA.ts:1591`
```ts
  i171: {
    kind: '3d',
    caption: '마법 구슬 하나 = 핵 + 빛무리 + 불티 + 연기 + 바닥 빛 — 층이 하나씩 쌓이며 풍성해진다',
    make(): Scene3D {
      const S = makeStage();
      const smoke = new Pool(700, S.puff);
      const sparks = new Pool(700, S.puff);
      const orb = new Pool(16, S.puff);
      smoke.mesh.renderOrder = 2;
      sparks.mesh.renderOrder = 3;
      orb.mesh.renderOrder = 4;
      smoke.curlScale = 0.8;
      S.scene.add(smoke.mesh, sparks.mesh, orb.mesh);
      const disc = glowDisc(0xa060ff, 2.2, 4.4);
      S.scene.add(disc.mesh);
      const pl = new THREE.PointLight(0xb070ff, 0, 7, 1.5);
      S.scene.add(pl);
      const lab = S.hud.label('tl');
      const on = [true, true, true, true, true];
      let manual = false;
      const NAMES = ['핵', '빛무리', '불티', '연기', '바닥 빛'];
      const CORE = flat(0xfff4ff, 9, 1);
      const CORE2 = flat(0xd8a0ff, 4, 0.9);
      const HALO = flat(0x9a50ff, 2.6, 0.7);
      const HALO2 = flat(0x6a30ff, 1.1, 0.5);
      const pos = new THREE.Vector3();
      const prev = new THREE.Vector3();
      let accS = 0;
      let accM = 0;
      const controls: Control[] = NAMES.map((n, i) =>
        ctrlTog(n, true, (v) => {
          manual = true;
          on[i] = v;
        }),
      );
      return {
        scene: S.scene,
        camera: S.cam,
        controls,
        update(t, dt0) {
          const dt = Math.min(dt0, 0.05);
          let L = 5;
          if (!manual) {
            const k = Math.floor(t / 1.5) % 8;
            L = Math.min(k + 1, 5);
            for (let i = 0; i < 5; i++) on[i] = i < L;
            lab.set(k < 5 ? `+ ${NAMES[k]!}  (${L}층)` : '다섯 층 모두', k < 5 ? '#c08cff' : '#ffcf6a');
          } else {
            lab.set(NAMES.filter((_, i) => on[i]).join(' + ') || '아무 층도 없음', '#c08cff');
          }
          prev.copy(pos);
          const a = t * 1.15;
          pos.set(Math.cos(a) * 2.3, 1.15 + Math.sin(t * 2.3) * 0.15, Math.sin(a) * 1.5);
          const vx = (pos.x - prev.x) / Math.max(dt, 1e-3);
          const vz = (pos.z - prev.z) / Math.max(dt, 1e-3);
          orb.clear();
          if (on[0]) {
            orb.spawn({ x: pos.x, y: pos.y, z: pos.z, life: 1, s0: 0.3 + Math.sin(t * 30) * 0.02, ramp: CORE, shape: 0 });
            orb.spawn({ x: pos.x, y: pos.y, z: pos.z, life: 1, s0: 0.55, ramp: CORE2, shape: 0 });
          }
          if (on[1]) {
            orb.spawn({ x: pos.x, y: pos.y, z: pos.z, life: 1, s0: 1.5 + Math.sin(t * 7) * 0.08, ramp: HALO, shape: 0 });
            orb.spawn({ x: pos.x, y: pos.y, z: pos.z, life: 1, s0: 2.8, ramp: HALO2, shape: 0 });
            orb.spawn({ x: pos.x, y: pos.y, z: pos.z, life: 1, s0: 1.0, ramp: flat(0xc090ff, 1.2, 0.8), shape: 4, rot: t * 0.6 });
          }
          if (on[2]) {
            accS += dt * 70;
            while (accS >= 1) {
              accS--;
              const d = sphereDir();
              const sp = R(1.2, 3.2);
              sparks.spawn({ x: pos.x + d.x * 0.1, y: pos.y + d.y * 0.1, z: pos.z + d.z * 0.1, vx: d.x * sp - vx * 0.25, vy: d.y * sp + 0.6, vz: d.z * sp - vz * 0.25, life: R(0.45, 0.9), s0: 0.07, s1: 0.02, ramp: RAMP.magicSpark, st: 0.06, shape: 2, drag: 1.2, grav: 3, bnc: 0.4 });
            }
          }
          if (on[3]) {
            accM += dt * 34;
            while (accM >= 1) {
              accM--;
              smoke.spawn({ x: pos.x + R(-0.1, 0.1), y: pos.y + R(-0.1, 0.1), z: pos.z + R(-0.1, 0.1), vx: -vx * 0.1, vy: R(0.2, 0.5), vz: -vz * 0.1, life: R(1.3, 2.0), s0: 0.35, s1: 1.25, ramp: RAMP.magicSmoke, shape: 1, rot: R(0, TAU), rv: R(-1, 1), drag: 1.5, curl: 0.35 });
            }
          }
          const gl = on[4] ? 1 : 0;
          pl.position.set(pos.x, pos.y - 0.3, pos.z);
          pl.intensity = gl * (13 + Math.sin(t * 17) * 1.5);
          disc.mesh.visible = !!gl;
          disc.mesh.position.set(pos.x, 0.015, pos.z);
          smoke.step(dt);
          sparks.step(dt);
          smoke.upload();
          sparks.upload();
          orb.upload();
          S.tick(dt);
        },
        resize: (w, h) => S.resize(w, h),
        dispose: () => S.dispose(),
      };
    },
  }
```

## 관련 기술
- 같은 방법으로 만든 효과 예시: [필살기 연출 (빛꼬리 · 잔상)](https://ai-techstudio.web.app/ai/t/u37.md) `u37` · [순간 빛 (점광 번쩍)](https://ai-techstudio.web.app/ai/t/i179.md) `i179` · [불덩이 (꼬리 · 불티 · 폭발)](https://ai-techstudio.web.app/ai/t/i182.md) `i182` · [얼음 창 · 서리 폭발](https://ai-techstudio.web.app/ai/t/i183.md) `i183` · [유성 낙하](https://ai-techstudio.web.app/ai/t/i186.md) `i186` · [치유 오라 · 버프 빛](https://ai-techstudio.web.app/ai/t/i187.md) `i187` · [독구름 · 저주 안개](https://ai-techstudio.web.app/ai/t/i188.md) `i188` · [소환 · 등장 (빛기둥 + 디졸브)](https://ai-techstudio.web.app/ai/t/i190.md) `i190` · [타격 반짝 (hit spark)](https://ai-techstudio.web.app/ai/t/i193.md) `i193` · [물 튀김 (왕관 스플래시 · 물기둥 · 파문)](https://ai-techstudio.web.app/ai/t/i502.md) `i502` · [신성 강림 (빛 기둥 · 갓레이 · 깃털)](https://ai-techstudio.web.app/ai/t/i511.md) `i511` · [크리스탈 성장 · 깨짐](https://ai-techstudio.web.app/ai/t/i514.md) `i514` · [보물 상자 열기 연출](https://ai-techstudio.web.app/ai/t/i209.md) `i209` · [등급 빛기둥 (희귀도 색)](https://ai-techstudio.web.app/ai/t/i210.md) `i210` · [별점 터짐](https://ai-techstudio.web.app/ai/t/i211.md) `i211` · [레벨 업 연출](https://ai-techstudio.web.app/ai/t/i212.md) `i212` · [던전 스킬 연출 (마법 · 피격)](https://ai-techstudio.web.app/ai/t/i490.md) `i490`
- 먼저 알면 좋은 기술: [입자 방출기 (수명 곡선)](https://ai-techstudio.web.app/ai/t/i170.md) `i170`
- 다음에 해 볼 기술: [흐르는 무늬 메시 (UV 스크롤)](https://ai-techstudio.web.app/ai/t/i172.md) `i172` · [충격 고리 · 땅 파동](https://ai-techstudio.web.app/ai/t/i178.md) `i178`
