# AI 꾸러미 — 소환 · 등장 (빛기둥 + 디졸브)
> 마법진 → 빛기둥 → 물체가 아래에서 위로 디졸브로 나타남 → 입자 흩어짐  
> 견본: https://ai-techstudio.web.app/#t/i190

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

## 주문서

### 만들어 줘: 소환 · 등장 (빛기둥 + 디졸브)

#### 1. 목표
새 캐릭터에 「소환 · 등장 (빛기둥 + 디졸브)」 기술을 깔끔한 기본 느낌으로 넣어 줘. — 마법진 → 빛기둥 → 물체가 아래에서 위로 디졸브로 나타남 → 입자 흩어짐

#### 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: `i190` · 분류: 이펙트 · 연출 › 스킬 VFX (마법 · 미사일) · 3D · 난이도 보통 · 폰 부담 보통 (폰 주의)
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i190
- 쓰면 좋을 때: 쓰면 좋을 곳 — 새 캐릭터 · 보상 등장

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i190 견본 항목 — `src/demos/demosSkillB.ts:1788`
```ts
  i190: {
    kind: '3d',
    caption: '마법진이 그려지고 → 빛기둥이 솟고 → 수정 수호자가 아래에서 위로 디졸브로 나타나며 빛 조각이 흩어진다',
    make() {
      const A = arena({ cam: [0, 6.4, 6.8], look: [0, 0.9, 0] });
      const runeMat = new THREE.ShaderMaterial({
        uniforms: { uMap: { value: A.tx.rune() }, uP: { value: 0 }, uC: { value: new THREE.Color(1.4, 2.0, 3.2) }, uA: { value: 1 } },
        vertexShader: `varying vec2 vUv; void main(){ vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,
        fragmentShader: `uniform sampler2D uMap; uniform float uP; uniform vec3 uC; uniform float uA; varying vec2 vUv;
          void main(){ vec2 p = vUv - 0.5; float ang = atan(p.y, p.x) / 6.2831853 + 0.5;
            float r = length(p) * 2.0;
            float rev = smoothstep(ang - 0.02, ang, uP * 1.05 - (1.0 - r) * 0.15);
            float a = texture2D(uMap, vUv).a * rev;
            float head = exp(-pow((ang - uP) * 40.0, 2.0)) * step(0.8, r) * step(r, 0.98) * step(uP, 0.999);
            gl_FragColor = vec4(uC * (1.0 + head * 3.0), (a + head) * uA); ${TONE} }`,
        transparent: true,
        depthWrite: false,
        blending: THREE.AdditiveBlending,
      });
      const circle = new THREE.Mesh(new THREE.PlaneGeometry(4.2, 4.2), runeMat);
      circle.rotation.x = -Math.PI / 2;
      circle.position.y = 0.03;
      const under = decal(A.tx.glow(), 5.5, C(0x5aa0ff, 1.4), 0);
      A.scene.add(circle, under);
      const pillarMat = new THREE.ShaderMaterial({
        uniforms: { uT: { value: 0 }, uA: { value: 0 }, uC: { value: new THREE.Color(1.2, 1.9, 3.2) } },
        vertexShader: FRESNEL_V,
        fragmentShader: `${NOISE} uniform float uT; uniform float uA; uniform vec3 uC; varying vec2 vUv; varying vec3 vN; varying vec3 vV; varying vec3 vP;
          void main(){ float f = abs(dot(normalize(vN), normalize(vV)));
            float n = vn(vec2(vUv.x * 18.0, vUv.y * 4.0 - uT * 3.5));
            float a = pow(1.0 - vUv.y, 1.6) * (0.35 + 0.9 * n) * pow(f, 1.2) * uA;
            gl_FragColor = vec4(uC + vec3(1.0) * pow(1.0 - vUv.y, 6.0), a); ${TONE} }`,
        transparent: true,
        depthWrite: false,
        side: THREE.DoubleSide,
        blending: THREE.AdditiveBlending,
      });
      const pillar = new THREE.Mesh(new THREE.CylinderGeometry(1, 1, 1, 40, 1, true), pillarMat);
      const core = new THREE.Mesh(new THREE.CylinderGeometry(1, 1, 1, 24, 1, true), pillarMat.clone());
      (core.material as THREE.ShaderMaterial).uniforms.uC!.value = new THREE.Color(3, 3, 3.4);
      A.scene.add(pillar, core);
      // 수정 수호자 + 디졸브
      const cut = { value: -1 };
      const edgeC = { value: new THREE.Color(2.5, 4, 6) };
      const crystal = new THREE.MeshStandardMaterial({ color: 0x9ad8ff, roughness: 0.18, metalness: 0.15, emissive: new THREE.Color(0.06, 0.18, 0.35), flatShading: true });
      crystal.onBeforeCompile = (sh) => {
        sh.uniforms.uCut = cut;
        sh.uniforms.uEdge = edgeC;
        sh.vertexShader = sh.vertexShader.replace('#include <common>', '#include <common>\nvarying vec3 vWp;').replace('#include <project_vertex>', '#include <project_vertex>\nvWp = (modelMatrix * vec4(transformed, 1.0)).xyz;');
        sh.fragmentShader = sh.fragmentShader
          .replace('#include <common>', `#include <common>\nvarying vec3 vWp; uniform float uCut; uniform vec3 uEdge;\n${NOISE}`)
          .replace('#include <clipping_planes_fragment>', `#include <clipping_planes_fragment>\nfloat dz = vWp.y + (vn3(vWp * 7.0) - 0.5) * 0.35 - uCut; if (dz > 0.0) discard;`)
          .replace('#include <emissivemap_fragment>', `#include <emissivemap_fragment>\ntotalEmissiveRadiance += uEdge * smoothstep(-0.12, 0.0, dz);`);
      };
      const guard = new THREE.Group();
      const shard = (r: number, h: number, x: number, y: number, z: number, rz = 0, rx = 0): THREE.Mesh => {
        const m = new THREE.Mesh(new THREE.OctahedronGeometry(1, 0), crystal);
        m.scale.set(r, h, r);
        m.position.set(x, y, z);
        m.rotation.set(rx, 0.4, rz);
        guard.add(m);
        return m;
      };
      shard(0.42, 1.05, 0, 1.05, 0);
      shard(0.22, 0.55, -0.42, 0.55, 0.15, 0.35);
      shard(0.2, 0.5, 0.44, 0.5, 0.1, -0.35);
      shard(0.18, 0.42, 0.1, 0.42, -0.42, 0, -0.4);
      shard(0.16, 0.4, -0.12, 0.38, 0.44, 0, 0.4);
      const halo = new THREE.Group();
      for (let i = 0; i < 6; i++) {
        const m = new THREE.Mesh(new THREE.OctahedronGeometry(1, 0), crystal);
        m.scale.set(0.08, 0.18, 0.08);
        const a = (i / 6) * TAU;
        m.position.set(Math.cos(a) * 0.85, 0, Math.sin(a) * 0.85);
        halo.add(m);
      }
      halo.position.y = 1.4;
      guard.add(halo);
      const gsh = decal(A.tx.shadow(), 1.6, [1, 1, 1], 0.8, true);
      guard.add(gsh);
      A.scene.add(guard);
      const light = new Flash(0x7ab8ff, 120, 3, 9);
      light.light.position.set(0, 1.2, 0.5);
      A.scene.add(light.light);
      const loop = new Loop(5.2);
      let speed = 1, pillarOn = true;
      const pAcc = { acc: 0 }, eAcc = { acc: 0 };
      const BL = C(0x7ac0ff, 2);
      return base3d(A, (t, dt) => {
        const lt = loop.tick(t);
        const P = 0.9; // 마법진 다 그림
        runeMat.uniforms.uP!.value = clamp(lt / P, 0, 1);
        const fadeAll = 1 - sstep(loop.P - 0.6, loop.P - 0.1, lt);
        runeMat.uniforms.uA!.value = fadeAll * (0.8 + 0.2 * Math.sin(lt * 6));
        circle.rotation.z = lt * 0.25;
        under.material.opacity = sstep(0, P, lt) * 0.55 * fadeAll;
        // 빛기둥
        const pk = sstep(P, P + 0.25, lt) * (1 - sstep(2.6, 3.3, lt));
        const ph = 7 * sstep(P, P + 0.3, lt);
        pillar.visible = core.visible = pillarOn && pk > 0.01;
        pillar.scale.set(1.15 * (0.6 + 0.4 * pk), Math.max(ph, 0.01), 1.15 * (0.6 + 0.4 * pk));
        pillar.position.y = ph / 2;
        core.scale.set(0.35 * pk, Math.max(ph, 0.01), 0.35 * pk);
        core.position.y = ph / 2;
        pillarMat.uniforms.uT!.value = t;
        pillarMat.uniforms.uA!.value = pk;
        (core.material as THREE.ShaderMaterial).uniforms.uT!.value = t * 1.5;
        (core.material as THREE.ShaderMaterial).uniforms.uA!.value = pk * 0.8;
        if (loop.hit(P)) {
          light.kick([0, 1.2, 0.5], 1);
          flashAt(A.fx, [0, 0.4, 0], 4, BL, 0.3);
          sparks(A.st, [0, 0.1, 0], { n: 30, c: C(0xd0eaff, 3), c2: BL, speed: 5, life: 0.6, grav: 2, up: 0.9 });
        }
        // 디졸브 (아래 → 위), 끝에 위 → 아래로 사라짐
        const d0 = P + 0.2;
        const d1 = d0 + 1.5 / speed;
        let c = -0.5;
        if (lt > d0) c = lerp(-0.3, 2.6, sstep(d0, d1, lt));
        if (lt > loop.P - 0.9) c = lerp(2.6, -0.3, sstep(loop.P - 0.9, loop.P - 0.2, lt));
        cut.value = c;
        guard.visible = c > -0.29;
        guard.rotation.y = t * 0.4;
        guard.position.y = 0.06 + Math.sin(t * 2) * 0.05 * sstep(d1, d1 + 0.3, lt);
        halo.rotation.y = -t * 1.3;
        if (c > -0.3 && c < 2.5) for (let i = rate(eAcc, 70, dt); i > 0; i--) {
          const a = Math.random() * TAU;
          const r = c < 1 ? lerp(0.6, 0.45, c) : lerp(0.45, 0.05, (c - 1) / 1.5);
          A.fx.emit({ p: [Math.cos(a) * r, c + 0.05, Math.sin(a) * r], v: [Math.cos(a) * 0.5, rr(0.3, 1), Math.sin(a) * 0.5], life: rr(0.3, 0.6), size: rr(0.08, 0.16), size2: 0.02, c: C(0xd8f0ff, 3), c2: BL, a: 1 });
        }
        if (loop.hit(d1)) {
          light.kick([0, 1.2, 0.5], 0.8);
          for (let i = 0; i < 70; i++) {
            const d = randDir();
            A.fx.emit({ p: [d[0] * 0.4, 1 + d[1] * 0.8, d[2] * 0.4], v: [d[0] * rr(2, 4), d[1] * 2 + 1, d[2] * rr(2, 4)], life: rr(0.6, 1.2), size: rr(0.1, 0.22), size2: 0.02, c: Math.random() < 0.3 ? C(0xffe6a0, 3) : BL, a: 1, drag: 2, grav: 1.5 });
          }
          sparks(A.st, [0, 1, 0], { n: 24, c: C(0xe0f4ff, 3), c2: BL, speed: 6, life: 0.5, grav: 3 });
        }
        // 마법진 위로 오르는 빛
        for (let i = rate(pAcc, 50 * sstep(0, P, lt) * fadeAll, dt); i > 0; i--) {
          const a = Math.random() * TAU, r = rr(1.6, 2.05);
          A.fx.emit({ p: [Math.cos(a) * r, 0.05, Math.sin(a) * r], v: [0, rr(0.4, 1.4), 0], life: rr(0.6, 1.2), size: rr(0.06, 0.12), size2: 0.02, c: BL, a: 1 });
        }
        light.tick(dt, 6 * fadeAll);
        A.fx.step(dt);
        A.st.step(dt);
        A.tickCam(dt);
      }, [
        { type: 'button', label: '다시 소환', on: () => loop.restart() },
        { type: 'range', label: '디졸브 속도', min: 0.4, max: 2.5, step: 0.1, value: speed, on: (v) => (speed = v) },
        { type: 'toggle', label: '빛기둥', value: pillarOn, on: (v) => (pillarOn = v) },
      ]);
    },
  }
```

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