# AI 꾸러미 — 와이어프레임에서 완성으로 — Wireframe-to-solid reveal (clipping-plane sweep)
> 같은 모양을 면(아래 남김)과 선 뼈대(위 남김)로 두 번 그리고 자르는 평면 하나를 올려, 빛 띠가 쓸며 설계도가 입체로 차오르게 한다.  
> 견본: https://ai-techstudio.web.app/#t/i114

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

## 주문서

### 만들어 줘: 와이어프레임에서 완성으로 — Wireframe-to-solid reveal (clipping-plane sweep)

#### 1. 목표
정육면체 · 원기둥 · 원뿔 (입체도형)을(를) 와이어프레임에서 완성으로 바뀌게 해 줘 — 자르기 평면이 아래에서 위로 쓸며 선 뼈대가 색 있는 입체로 차오르고, 쓸리는 곳에 빛 띠. 분위기는 푸른 설계도 홀로그램.

#### 2. 핵심 기술 용어
- **Wireframe-to-solid reveal (clipping-plane sweep)** — 자르는 평면을 쓸어 선 → 면으로
- **Material.clippingPlanes + localClippingEnabled** — three.js 재질별 자르기 평면
- **WireframeGeometry + LineSegments** — 모양의 모든 모서리를 선으로

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

#### 4. 조건
- renderer.localClippingEnabled = true (견본은 그릴 때만 켜고 되돌린다)
- 면 · 선 자르기 평면은 서로 반대 방향, 같은 높이를 공유
- 면 재질은 DoubleSide — 잘린 단면 안쪽이 보이게
- 경계 빛 띠는 차오르는 동안에만 보이게 (끝에 닿으면 끔)
- 0.8초 선만 → 2.6초 차오름 → 1.4초 완성 → 1.6초 내려감 순서로 반복

#### 5. 완성 기준 (이게 보이면 성공)
- 처음엔 하늘색 선 뼈대만 보이다가, 빛 띠가 아래에서 위로 쓸며 지나간 자리가 색 있는 입체가 된다
- 쓸리는 높이에서 면과 선이 딱 이어진다 (틈 · 겹침 없음)
- 다 차오른 뒤에도 아주 옅은 흰 선이 남아 설계도 흔적이 보인다
- 「빠르기」 조절로 쓸리는 속도가 바뀐다

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

## 원리
- 같은 모양을 두 번 그린다: 색 있는 면(MeshStandardMaterial)과 모서리 선(WireframeGeometry + LineSegments).
- 면에는 자르기 평면 cut = Plane((0, −1, 0), y) — 높이 y 아래만 남는다. 선에는 cutW = Plane((0, 1, 0), −y) — y 위만 남는다.
- y 를 −0.72 → 0.75 로 올리면 아래부터 면이 차오르고 남은 위쪽은 선으로 보인다. 경계에 더하기 섞기 빛 띠(원판 + 테 고리).
- 자르기는 renderer.localClippingEnabled = true 일 때만 동작한다. 면은 DoubleSide 로 해서 잘린 속이 비어 보이지 않게.

## 핵심 코드 — 면 · 선 반대 자르기 평면 + 높이 쓸기
(발췌: demos/demosMotionB.ts i114 를 정리)
```ts
const cut = new THREE.Plane(new THREE.Vector3(0, -1, 0), 0); // 면: 높이 y 아래만
const cutW = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);  // 선: 높이 y 위만

const geo = new THREE.BoxGeometry(1, 1, 1, 3, 3, 3);
const solid = new THREE.Mesh(geo, new THREE.MeshStandardMaterial({
  color: 0xff7ab6, roughness: 0.45, clippingPlanes: [cut], side: THREE.DoubleSide,
}));
const wire = new THREE.LineSegments(new THREE.WireframeGeometry(geo), new THREE.LineBasicMaterial({
  color: 0x7cf0ff, transparent: true, opacity: 0.85, clippingPlanes: [cutW],
}));
item.add(solid, wire);

// 쓸고 가는 빛 띠
const scanMat = new THREE.MeshBasicMaterial({ color: 0x7cf0ff, transparent: true, opacity: 0.18,
  blending: THREE.AdditiveBlending, depthWrite: false, side: THREE.DoubleSide });
const scan = new THREE.Mesh(new THREE.RingGeometry(0, 2.75, 48), scanMat);
scan.rotation.x = -Math.PI / 2;

// 매 프레임: 0~0.8 선만 · 0.8~3.4 차오름 · 3.4~4.8 완성 · 4.8~6.4 내려감
const p = (tt % 7), lo = -0.72, hi = 0.75;
const y = p < 0.8 ? lo : p < 3.4 ? lerp(lo, hi, easeIO((p - 0.8) / 2.6)) : p < 4.8 ? hi
  : p < 6.4 ? lerp(hi, lo, easeIO((p - 4.8) / 1.6)) : lo;
cut.constant = y;    // −y·1 + y ≥ 0 → 점의 y ≤ 경계
cutW.constant = -y;  // 점의 y ≥ 경계
scan.position.y = y;
scanMat.opacity = y > lo + 0.01 && y < hi - 0.01 ? 0.2 : 0;

// 그리기
renderer.localClippingEnabled = true; // 이게 없으면 자르기가 안 된다
renderer.render(scene, camera);
```

## 흔한 실수 · 확인 목록
- [ ] **localClippingEnabled 를 안 켜면 아무것도 잘리지 않는다** — 재질의 clippingPlanes 는 renderer.localClippingEnabled = true 일 때만 동작한다.
- [ ] **평면 방향을 같게 주면 면과 선이 같은 쪽에 남는다** — 면은 법선 (0, −1, 0) · 상수 y, 선은 (0, 1, 0) · 상수 −y 로 반대.
- [ ] **면 재질이 FrontSide 면 잘린 곳 안쪽이 텅 비어 보인다** — side: DoubleSide 로 안쪽 면도 그린다.

## 완성 기준 체크리스트
- [ ] 처음엔 하늘색 선 뼈대만 보이다가, 빛 띠가 아래에서 위로 쓸며 지나간 자리가 색 있는 입체가 된다
- [ ] 쓸리는 높이에서 면과 선이 딱 이어진다 (틈 · 겹침 없음)
- [ ] 다 차오른 뒤에도 아주 옅은 흰 선이 남아 설계도 흔적이 보인다
- [ ] 「빠르기」 조절로 쓸리는 속도가 바뀐다

## 이 기술 정보
- id: `i114` · 분류: 이펙트 · 연출 › 모션 그래픽 · 3D · 난이도 보통 · 폰 부담 가벼움 (폰 OK) — 모양을 두 번 그린다 (면 + 선). 자르기 평면은 셰이더에 조건 하나만 늘린다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i114
- 쓰면 좋을 때: 입체도형 · 설계가 「만들어진다」 를 보여 줄 때 / 새 물건 · 캐릭터가 처음 등장하는 연출
- 쓰지 말 때: 두께가 없는 판 — 차오르는 느낌이 없다. 대신 디졸브(잡음으로 사라지기) / 삼각형 수십만 개 모델 — 와이어 선이 너무 촘촘하다. 대신 EdgesGeometry(각진 모서리만)

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i114 견본 항목 — `src/demos/demosMotionB.ts:827`
```ts
  i114: {
    kind: '3d',
    caption: '빛나는 띠가 아래에서 위로 쓸고 지나가면 선 뼈대(정육면체 · 원기둥 · 원뿔)가 색 있는 입체로 차올라요',
    make(T) {
      const scene = new T.Scene();
      scene.background = new T.Color(0x141c4a);
      scene.add(new T.HemisphereLight(0xdfe8ff, 0x3a3060, 1.1));
      const sun = new T.DirectionalLight(0xffffff, 1.7);
      sun.position.set(-3, 5, 4);
      scene.add(sun);
      const cut = new T.Plane(new T.Vector3(0, -1, 0), 0);
      const cutW = new T.Plane(new T.Vector3(0, 1, 0), 0);
      const root = new T.Group();
      scene.add(root);
      const geos: THREE.BufferGeometry[] = [];
      const spin: THREE.Group[] = [];
      const mats: THREE.Material[] = [];
      const items: [THREE.BufferGeometry, number, number, number][] = [
        [new T.BoxGeometry(1, 1, 1, 3, 3, 3), 0xff7ab6, -1.55, 1],
        [new T.CylinderGeometry(0.55, 0.55, 1.15, 24, 4), 0x5ec8ff, 0, 1.15],
        [new T.ConeGeometry(0.64, 1.2, 24, 4), 0xffd166, 1.55, 1.2],
      ];
      for (const [geo, col, x, hh] of items) {
        geos.push(geo);
        const sm = new T.MeshStandardMaterial({ color: col, roughness: 0.45, clippingPlanes: [cut], side: T.DoubleSide });
        const mesh = new T.Mesh(geo, sm);
        const it = new T.Group();
        it.position.set(x, -0.6 + hh / 2, 0);
        spin.push(it);
        const wg = new T.WireframeGeometry(geo);
        geos.push(wg);
        const wm = new T.LineBasicMaterial({ color: 0x7cf0ff, transparent: true, opacity: 0.85, clippingPlanes: [cutW] });
        const wire = new T.LineSegments(wg, wm);
        // 완성된 쪽 위에 아주 옅은 선 (설계도 흔적)
        const ghost = new T.LineSegments(wg, new T.LineBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.12 }));
        ghost.scale.setScalar(1.002);
        mats.push(sm, wm, ghost.material as THREE.Material);
        it.add(mesh, wire, ghost);
        root.add(it);
      }
      const base = new T.Mesh(new T.CylinderGeometry(2.7, 2.8, 0.14, 48), new T.MeshStandardMaterial({ color: 0x2b3a7a, roughness: 0.8 }));
      base.position.y = -0.68;
      base.scale.z = 0.5;
      geos.push(base.geometry);
      mats.push(base.material as THREE.Material);
      root.add(base);
      // 쓸고 가는 빛 띠
      const scanGeo = new T.RingGeometry(0, 2.75, 48);
      const scanMat = new T.MeshBasicMaterial({ color: 0x7cf0ff, transparent: true, opacity: 0.18, blending: T.AdditiveBlending, depthWrite: false, side: T.DoubleSide });
      const scan = new T.Mesh(scanGeo, scanMat);
      scan.rotation.x = -Math.PI / 2;
      scan.scale.y = 0.5;
      const rimGeo = new T.TorusGeometry(2.75, 0.025, 6, 96);
      const rimMat = new T.MeshBasicMaterial({ color: 0xbffcff, transparent: true, blending: T.AdditiveBlending });
      const rim = new T.Mesh(rimGeo, rimMat);
      rim.rotation.x = Math.PI / 2;
      rim.scale.y = 0.5;
      geos.push(scanGeo, rimGeo);
      mats.push(scanMat, rimMat);
      scene.add(scan, rim);
      const cam = new T.PerspectiveCamera(34, 1, 0.1, 50);
      cam.position.set(0, 2.3, 7.4);
      cam.lookAt(0, -0.05, 0);
      let speed = 1;
      let tt = 0;
      return {
        scene,
        camera: cam,
        update(_t, dt) {
          tt += dt * speed;
          spin.forEach((s, i) => (s.rotation.y = tt * 0.5 + i * 0.7));
          const P = 7;
          const p = fract(tt / P) * P;
          // 0~0.8 선만 · 0.8~3.4 차오름 · 3.4~4.8 완성 · 4.8~6.4 내려감 · 6.4~7 선만
          const lo = -0.72;
          const hi = 0.75;
          const y =
            p < 0.8 ? lo : p < 3.4 ? lerp(lo, hi, easeIO((p - 0.8) / 2.6)) : p < 4.8 ? hi : p < 6.4 ? lerp(hi, lo, easeIO((p - 4.8) / 1.6)) : lo;
          cut.constant = y;
          cutW.constant = -y;
          const vis = y > lo + 0.01 && y < hi - 0.01 ? 1 : 0;
          scan.position.y = y;
          rim.position.y = y;
          scanMat.opacity = 0.2 * vis;
          rimMat.opacity = vis;
        },
        render(r) {
          const lc = r.localClippingEnabled;
          r.localClippingEnabled = true;
          r.render(scene, cam);
          r.localClippingEnabled = lc;
        },
        controls: [{ type: 'range', label: '빠르기', min: 0, max: 3, step: 0.1, value: 1, on: (v) => (speed = v) }] as Control[],
        dispose() {
          for (const g of geos) g.dispose();
          for (const m of mats) m.dispose();
        },
      };
    },
  }
```

## 관련 기술
- 먼저 알면 좋은 기술: [보이는 선 · 숨은 선 · 굵은 선](https://ai-techstudio.web.app/ai/t/u31.md) `u31` · [단면 자르기 (스텐실 단면 · 원뿔 곡선)](https://ai-techstudio.web.app/ai/t/i35.md) `i35`
- 다음에 해 볼 기술: [분해도 (exploded view)](https://ai-techstudio.web.app/ai/t/i36.md) `i36` · [3D 입체 글씨 (글꼴 윤곽 돌출 · 모서리 깎기)](https://ai-techstudio.web.app/ai/t/i17.md) `i17`
- 참고 문서: [three.js 예제 — webgl_clipping](https://threejs.org/examples/#webgl_clipping) · [three.js 소스 — WireframeGeometry](https://github.com/mrdoob/three.js/blob/dev/src/geometries/WireframeGeometry.js)
