# AI 꾸러미 — 검기 · 휘두름 호 (메시 슬래시)
> 반달 메시에 흐르는 빛 무늬 + 가장자리 불티 — 칼 · 발차기 궤적  
> 견본: https://ai-techstudio.web.app/#t/i192

이 문서를 코드 도우미(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: `i192` · 분류: 이펙트 · 연출 › 스킬 VFX (마법 · 미사일) · 3D · 난이도 보통 · 폰 부담 보통 (폰 주의)
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i192
- 쓰면 좋을 때: 쓰면 좋을 곳 — 자르기 · 분수 나누기

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i192 견본 항목 — `src/demos/demosSkillB.ts:2072`
```ts
  i192: {
    kind: '3d',
    caption: '반달 모양 메시에 흐르는 빛 무늬가 휙 — 가장자리 하얀 날 · 끝에서 튀는 불티, 세 번 연속 베기',
    make() {
      const A = arena({ cam: [0, 5.6, 6.6], look: [0, 0.9, 0] });
      const foe = monster(A, 0x5d4a78, 0);
      foe.g.position.set(0.3, 0, -0.2);
      foe.g.scale.setScalar(1.3);
      A.scene.add(foe.g);
      // 반달 메시: u = 휘두른 쪽, v = 안 → 바깥
      const SEG = 64;
      const pos: number[] = [], uv: number[] = [], idx: number[] = [];
      const A0 = -2.0, A1 = 2.0, R1 = 1.85, R0 = 0.3;
      for (let i = 0; i <= SEG; i++) {
        const u = i / SEG;
        const a = lerp(A0, A1, u);
        const rin = R0;
        pos.push(Math.cos(a) * rin, 0, Math.sin(a) * rin, Math.cos(a) * R1, 0, Math.sin(a) * R1);
        uv.push(u, 0, u, 1);
        if (i < SEG) {
          const b = i * 2;
          idx.push(b, b + 1, b + 2, b + 2, b + 1, b + 3);
        }
      }
      const geo = new THREE.BufferGeometry();
      geo.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3));
      geo.setAttribute('uv', new THREE.Float32BufferAttribute(uv, 2));
      geo.setIndex(idx);
      const mk = (): THREE.ShaderMaterial =>
        new THREE.ShaderMaterial({
          uniforms: { uHead: { value: -1 }, uTail: { value: 1.05 }, uC: { value: new THREE.Color() }, uC2: { value: new THREE.Color() }, uT: { value: 0 }, uA: { value: 1 } },
          vertexShader: `varying vec2 vUv; void main(){ vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,
          fragmentShader: `${NOISE} uniform float uHead; uniform float uTail; uniform vec3 uC; uniform vec3 uC2; uniform float uT; uniform float uA; varying vec2 vUv;
            void main(){ float u = vUv.x, v = vUv.y;
              float d = uHead - u; if (d < 0.0) discard;
              float k = 1.0 - d / uTail; if (k <= 0.0) discard;
              float w = 0.03 + 0.66 * pow(k, 0.9) * (1.0 - pow(k, 4.0)) * smoothstep(1.0, 0.93, u);
              float vv = (v - (1.0 - w)) / w; if (vv < 0.0) discard;
              float trail = pow(k, 1.4);
              float edge = pow(vv, 1.8);
              float line = smoothstep(0.82, 0.94, vv) * (1.0 - smoothstep(0.97, 1.0, vv));
              float n = vn(vec2(u * 22.0 - uT * 3.0, v * 4.0)) * vn(vec2(u * 9.0 + 2.0, v * 9.0 - uT * 2.0));
              float streak = 0.35 + 1.6 * n;
              float head = exp(-d * 26.0);
              vec3 col = mix(uC2, uC, edge) * (0.9 + streak * edge * 1.8) + mix(uC, vec3(1.0), 0.6) * (line * 1.2 * k + head * edge * 0.9);
              float a = trail * uA * (0.6 + edge * 0.6) * smoothstep(0.0, 0.2, vv) * smoothstep(0.0, 0.03, u);
              gl_FragColor = vec4(col, a); ${TONE} }`,
          transparent: true,
          depthWrite: false,
          side: THREE.DoubleSide,
          blending: THREE.AdditiveBlending,
        });
      const pivot: V3 = [0.3, 0.95, -0.2];
      const slashes = [
        { e: new THREE.Euler(0.55, 0, 0.15), at: 0.2, flip: 1 },
        { e: new THREE.Euler(0.6, 0, 0.75), at: 0.75, flip: -1 },
        { e: new THREE.Euler(0.65, 0, -0.65), at: 1.3, flip: 1 },
      ].map((s) => {
        const m = new THREE.Mesh(geo, mk());
        m.scale.x = s.flip;
        m.position.set(pivot[0], pivot[1], pivot[2]);
        m.rotation.copy(s.e);
        m.frustumCulled = false;
        A.scene.add(m);
        return { m, at: s.at };
      });
      const glow = decal(A.tx.glow(), 3.6, [1, 1, 1], 0);
      glow.position.set(pivot[0], 0.02, pivot[2]);
      A.scene.add(glow);
      const hitL = new Flash(0xffffff, 60, 7);
      A.scene.add(hitL.light);
      const loop = new Loop(2.4);
      let colorIdx = 0, embers = true;
      const PAL: [V3, V3][] = [
        [C(0xff7a10, 1.5), C(0xd01e00, 1.1)],
        [C(0x5ad0ff, 1.5), C(0x1a50ff, 1.1)],
        [C(0xb070ff, 1.5), C(0x6a20ff, 1.1)],
      ];
      const DUR = 0.24;
      const v = new THREE.Vector3();
      return base3d(A, (t, dt) => {
        const lt = loop.tick(t);
        const [c1, c2] = PAL[colorIdx]!;
        for (const s of slashes) {
          const u = s.m.material.uniforms;
          const k = (lt - s.at) / DUR;
          const head = k < 0 ? -1 : 1.04 * easeOut(Math.min(k, 1)) + Math.max(0, k - 1) * 0.04;
          u.uHead!.value = head;
          u.uT!.value = t;
          u.uA!.value = k < 0 ? 0 : 1 - sstep(1.1, 3.2, k);
          (u.uC!.value as THREE.Color).setRGB(c1[0], c1[1], c1[2]);
          (u.uC2!.value as THREE.Color).setRGB(c2[0], c2[1], c2[2]);
          s.m.visible = k >= 0 && k < 3.3;
          // 칼끝 불티
          if (k >= 0 && k < 1) {
            const hu = clamp(head, 0, 1);
            const a = lerp(A0, A1, hu);
            v.set(Math.cos(a) * R1, 0, Math.sin(a) * R1).applyMatrix4(s.m.matrixWorld);
            const tipP = ofV(v);
            const tg = new THREE.Vector3(-Math.sin(a), 0, Math.cos(a)).transformDirection(s.m.matrixWorld);
            A.fx.put(tipP, 0.9, c1, 0.5);
            if (embers) for (let i = 0; i < 4; i++) A.st.spark({ p: tipP, v: add3(sc3(ofV(tg), rr(2, 5)), sc3(randDir(), 1.5)), life: rr(0.2, 0.45), c: C(0xffffff, 2.5), c2: c2, len: 0.04, w: 0.02, grav: 6, drag: 2, fadeIn: 0 });
          }
          if (loop.hit(s.at + DUR * 0.45)) {
            const hp = foe.center();
            sparks(A.st, hp, { n: 26, c: C(0xffffff, 3), c2: c1, speed: 7, life: 0.35, grav: 5 });
            flashAt(A.fx, hp, 2.6, c1, 0.18);
            hitL.kick(hp, 1);
            hitL.light.color.setRGB(c1[0] / 2, c1[1] / 2, c1[2] / 2);
            foe.hit(sc3(c1, 0.6), 0.7);
            glow.material.opacity = 0.9;
            glow.material.color.setRGB(c2[0], c2[1], c2[2]);
            A.shake(0.08);
          }
        }
        glow.material.opacity *= Math.exp(-dt * 3);
        foe.tick(dt, t);
        hitL.tick(dt);
        A.fx.step(dt);
        A.st.step(dt);
        A.tickCam(dt);
      }, [
        { type: 'button', label: '다시 휘두르기', on: () => loop.restart() },
        { type: 'range', label: '색 (불꽃 · 얼음 · 번개)', min: 0, max: 2, step: 1, value: colorIdx, on: (v) => (colorIdx = v) },
        { type: 'toggle', label: '칼끝 불티', value: embers, on: (v) => (embers = v) },
      ]);
    },
  }
```

## 관련 기술
- 이 효과를 만든 기술: [흐르는 무늬 메시 (UV 스크롤)](https://ai-techstudio.web.app/ai/t/i172.md) `i172`
