# AI 꾸러미 — 등급 빛기둥 (희귀도 색)
> 흰 · 파랑 · 보라 · 금빛 기둥이 치솟음 + 바닥 고리  
> 견본: https://ai-techstudio.web.app/#t/i210

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

## 주문서

### 만들어 줘: 등급 빛기둥 (희귀도 색)

#### 1. 목표
별 1~3개 등급에 「등급 빛기둥 (희귀도 색)」 기술을 깔끔한 기본 느낌으로 넣어 줘. — 흰 · 파랑 · 보라 · 금빛 기둥이 치솟음 + 바닥 고리

#### 2. 핵심 기술 용어
- 등급 빛기둥 (희귀도 색) — 흰 · 파랑 · 보라 · 금빛 기둥이 치솟음 + 바닥 고리

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

#### 4. 조건
- 한 프레임 16ms 안 (폰 포함) — 무거운 계산은 처음에 한 번 또는 여러 프레임에 나눠서
- 새 재질 · 셰이더는 첫 화면 전에 미리 컴파일 (renderer.compileAsync)
- 화면 가장자리 흐림 · 비네트로 얼버무리지 않기
- 외부 라이브러리 추가 없이 three.js 와 three/examples 만

#### 5. 완성 기준 (이게 보이면 성공)
- 견본처럼 「흰 · 파랑 · 보라 · 금빛 기둥이 치솟음 + 바닥 고리」 이(가) 첫 화면에서 바로 보인다
- 켬/끔(또는 전/후) 비교 단추로 이 기술이 만드는 차이를 확인할 수 있다
- 중요한 값 1~3개를 슬라이더로 바꿔 볼 수 있다
- 콘솔 오류 0, 폰 폭(390px)에서도 동작한다

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

## 원리
- 흰 · 파랑 · 보라 · 금빛 기둥이 치솟음 + 바닥 고리


## 흔한 실수 · 확인 목록
- (아직 정리된 것이 없어요)

## 완성 기준 체크리스트
- [ ] 견본처럼 「흰 · 파랑 · 보라 · 금빛 기둥이 치솟음 + 바닥 고리」 이(가) 첫 화면에서 바로 보인다
- [ ] 켬/끔(또는 전/후) 비교 단추로 이 기술이 만드는 차이를 확인할 수 있다
- [ ] 중요한 값 1~3개를 슬라이더로 바꿔 볼 수 있다
- [ ] 콘솔 오류 0, 폰 폭(390px)에서도 동작한다

## 이 기술 정보
- id: `i210` · 분류: 이펙트 · 연출 › 손맛 · 주스 · 3D · 난이도 쉬움 · 폰 부담 보통 (폰 주의)
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i210
- 쓰면 좋을 때: 쓰면 좋을 곳 — 별 1~3개 등급 · 최소 수 금빛

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### D210 — `src/demos/demosJuice.ts:3456`
```ts
const D210: Demo = {
  kind: '3d',
  caption: '별 하나 흰빛 · 둘 파랑 · 셋 보라 · 최소 수는 금빛 — 등급이 높을수록 굵고 높게 치솟고 바닥 고리가 퍼진다',
  make() {
    const scene = new THREE.Scene();
    scene.background = skyTex('#1a1f4f', '#05061a');
    const cam = new THREE.PerspectiveCamera(40, 1.6, 0.1, 80);
    cam.position.set(0, 3.0, 8.4);
    cam.lookAt(0, 1.4, 0);
    scene.add(new THREE.HemisphereLight(0xb8c4ff, 0x10102a, 1.1));
    const key = new THREE.DirectionalLight(0xffffff, 1.8);
    key.position.set(2, 6, 5);
    scene.add(key);
    const floor = new THREE.Mesh(new THREE.CircleGeometry(9, 64), new THREE.MeshStandardMaterial({ color: 0x1b2050, roughness: 0.6, metalness: 0.2 }));
    floor.rotation.x = -Math.PI / 2;
    scene.add(floor);
    const COLS = [0xe9eefc, 0x4dabf7, 0xb36bff, 0xffc23a];
    const NAMES = ['★', '★★', '★★★', '최소 수!'];
    const XS = [-3.0, -1.0, 1.0, 3.0];
    const pGeo = new THREE.CylinderGeometry(0.5, 0.62, 1, 40, 1, true);
    pGeo.translate(0, 0.5, 0);
    const padGeo = new THREE.CylinderGeometry(0.7, 0.78, 0.18, 40);
    const ringGeo = new THREE.RingGeometry(0.86, 1, 64);
    const sGeo = starGeo(0.34, 0.14);
    const dot = dotTex();
    const sp = new Sparks(4 * 36);
    scene.add(sp.obj);
    interface Slot {
      pad: THREE.Mesh;
      outer: THREE.Mesh;
      core: THREE.Mesh;
      mo: THREE.ShaderMaterial;
      mc: THREE.ShaderMaterial;
      ring: THREE.Mesh;
      ring2: THREE.Mesh;
      halo: THREE.Sprite;
      item: THREE.Mesh;
      col: THREE.Color;
      lightM: THREE.MeshStandardMaterial;
    }
    const slots: Slot[] = [];
    const labels: THREE.Sprite[] = [];
    XS.forEach((x, i) => {
      const col = new THREE.Color(COLS[i]!);
      const lightM = new THREE.MeshStandardMaterial({ color: 0x3a3f7a, roughness: 0.4, metalness: 0.3, emissive: col, emissiveIntensity: 0 });
      const pad = new THREE.Mesh(padGeo, lightM);
      pad.position.set(x, 0.09, 0);
      scene.add(pad);
      const mo = pillarMat(COLS[i]!);
      const mc = pillarMat(0xffffff);
      const outer = new THREE.Mesh(pGeo, mo);
      const core = new THREE.Mesh(pGeo, mc);
      outer.position.set(x, 0.18, 0);
      core.position.set(x, 0.18, 0);
      scene.add(outer, core);
      const rm = new THREE.MeshBasicMaterial({ color: col, transparent: true, opacity: 0, blending: THREE.AdditiveBlending, depthWrite: false, toneMapped: false, side: THREE.DoubleSide });
      const ring = new THREE.Mesh(ringGeo, rm);
      ring.rotation.x = -Math.PI / 2;
      ring.position.set(x, 0.02, 0);
      const ring2 = new THREE.Mesh(ringGeo, rm.clone());
      ring2.rotation.x = -Math.PI / 2;
      ring2.position.set(x, 0.021, 0);
      scene.add(ring, ring2);
      const halo = new THREE.Sprite(new THREE.SpriteMaterial({ map: dot, color: col, blending: THREE.AdditiveBlending, depthWrite: false, toneMapped: false, opacity: 0 }));
      halo.position.set(x, 0.6, 0);
      scene.add(halo);
      const item = new THREE.Mesh(sGeo, new THREE.MeshStandardMaterial({ color: col, roughness: 0.3, metalness: 0.2, emissive: col, emissiveIntensity: 0.15 }));
      item.position.set(x, 0.7, 0);
      scene.add(item);
      const lt = pillTex(NAMES[i]!, 'rgba(10,12,40,.8)', '#' + col.getHexString());
      const lab = new THREE.Sprite(new THREE.SpriteMaterial({ map: lt.tex, toneMapped: false, depthWrite: false }));
      lab.scale.set(0.42 * lt.aspect, 0.42, 1);
      lab.position.set(x, 0.32, 1.25);
      lab.renderOrder = 5;
      lab.material.depthTest = false;
      scene.add(lab);
      labels.push(lab);
      slots.push({ pad, outer, core, mo, mc, ring, ring2, halo, item, col, lightM });
    });
    let speed = 1;
    const P = 4.8;
    const cc = new THREE.Color();
    return {
      scene,
      camera: cam,
      update(t) {
        const ph = (t * speed) % P;
        const end = smooth(4.2, 4.8, ph);
        slots.forEach((s, i) => {
          const at = 0.35 + i * 0.7;
          const a = ph - at;
          const gold = i === 3;
          const H = [2.6, 3.4, 4.3, 6.5][i]!;
          const Wd = [0.55, 0.7, 0.85, 1.15][i]!;
          const on = a >= 0;
          const A = on ? clamp01(a / 0.15) * (1 - end) : 0;
          const grow = on ? easeOut(a / 0.4) : 0;
          const burst = on ? Math.exp(-a * 6) : 0;
          s.outer.visible = s.core.visible = A > 0.001;
          s.outer.scale.set(Wd * (1 + burst * 0.8), Math.max(0.001, H * grow), Wd * (1 + burst * 0.8));
          s.core.scale.set(Wd * 0.4, Math.max(0.001, H * grow * 1.05), Wd * 0.4);
          s.mo.uniforms.uA!.value = A * (gold ? 0.95 : 0.7) * (0.9 + 0.1 * Math.sin(t * 8 + i));
          s.mc.uniforms.uA!.value = A * (gold ? 0.9 : 0.6);
          s.mo.uniforms.uT!.value = s.mc.uniforms.uT!.value = t + i;
          // 고리
          const ra = on ? a : -1;
          const r1 = ra >= 0 && ra < 0.9 ? ra / 0.9 : 1;
          s.ring.scale.setScalar(0.7 + easeOut(r1) * (gold ? 3.6 : 2.2));
          (s.ring.material as THREE.MeshBasicMaterial).opacity = ra >= 0 ? (1 - r1) * 0.9 : 0;
          const r2 = ((ra - 0.2) % 1.2) / 1.2;
          s.ring2.scale.setScalar(0.8 + r2 * (gold ? 1.6 : 1.0));
          (s.ring2.material as THREE.MeshBasicMaterial).opacity = ra > 0.2 ? (1 - r2) * 0.5 * (1 - end) : 0;
          s.halo.material.opacity = A * (0.55 + burst * 0.45);
          s.halo.scale.setScalar((gold ? 3.4 : 2.2) + burst * 3);
          s.lightM.emissiveIntensity = A * 0.8;
          s.item.position.y = 0.75 + (on ? easeOut(a / 0.6) * 0.55 * (1 - end) : 0) + Math.sin(t * 2.4 + i) * 0.05;
          s.item.rotation.y = t * (on ? 2.4 : 0.8) + i;
          (s.item.material as THREE.MeshStandardMaterial).emissiveIntensity = 0.15 + A * 0.9;
          // 솟는 알갱이
          for (let k = 0; k < 36; k++) {
            const idx = i * 36 + k;
            if (!on) {
              sp.set(idx, 0, 0, 0, cc, 0, 0);
              continue;
            }
            const life = 1.3;
            const age = (a + hash(k, i + 1) * life) % life;
            const th = hash(k, i + 20) * TAU;
            const rad = Wd * (0.3 + hash(k, i + 40) * 0.6);
            cc.copy(s.col).lerp(new THREE.Color(1, 1, 1), 0.3);
            sp.set(idx, XS[i]! + Math.cos(th + age) * rad, 0.2 + age * (gold ? 4.5 : 3), Math.sin(th + age) * rad, cc, gold ? 0.13 : 0.09, A * (1 - age / life) * clamp01(a * 3));
          }
        });
        sp.flush();
        labels.forEach((l, i) => (l.material.opacity = 0.35 + 0.65 * clamp01((ph - 0.35 - i * 0.7) / 0.2) * (1 - end)));
      },
      resize(_w, h) {
        sp.fit(h, cam.fov);
      },
      controls: [rng('빠르기', 0.4, 2, 0.05, speed, (v) => (speed = v))],
      dispose() {
        disposeScene(scene);
        sp.dispose();
      },
    };
  },
};
```

## 관련 기술
- 이 효과를 만든 기술: [층 쌓기 (핵 · 빛무리 · 불티 · 연기)](https://ai-techstudio.web.app/ai/t/i171.md) `i171`
