# AI 꾸러미 — 아주 많은 물체 (BatchedMesh)
> 모양이 서로 다른 소품 여러 개를 BatchedMesh 한 묶음에 넣어, 그리기 호출 수십 번을 한 번으로 줄인다.  
> 견본: https://ai-techstudio.web.app/#t/i34

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

## 주문서

### 만들어 줘: 아주 많은 물체 (BatchedMesh)

#### 1. 목표
나무 · 바위 · 꽃 · 고깔 같은 소품 40개의 소품들을 BatchedMesh 한 묶음으로 바꿔서 그리기 호출을 한 번으로 줄여 줘 — 모양 · 위치 · 색은 그대로. 장면 분위기는 밝은 낮 들판.

#### 2. 핵심 기술 용어
- **BatchedMesh** — three.js — 모양이 달라도 재질이 같으면 한 번에 그리는 묶음 메시 (r159+)
- **Draw call** — 그리기 호출 — CPU 가 GPU 에 「이거 그려」 하는 한 번
- **Multi-draw (WEBGL_multi_draw)** — 여러 조각을 호출 한 번에 그리는 확장
- **InstancedMesh (비교)** — 같은 모양 여러 개만 한 번에 — 모양이 다르면 BatchedMesh

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

#### 4. 조건
- BatchedMesh(최대 자리 수, 최대 꼭짓점 수, 최대 색인 수, 재질) — 꼭짓점 · 색인 수는 넣을 모양들의 합으로 계산
- 넣는 모양은 모두 색인(index)이 있거나 모두 없어야 하고, 속성(position · normal · uv)도 같아야 한다
- 색 차이는 재질을 나누지 말고 setColorAt 으로
- 바꾸기 전/후 renderer.info.render.calls 를 화면에 보여 주기

#### 5. 완성 기준 (이게 보이면 성공)
- 바꾸기 전과 같은 자리 · 크기 · 색으로 소품이 보인다 (전/후 스크린숏이 같다)
- renderer.info.render.calls 가 소품 수만큼 → 1(묶음 수)로 줄어든다
- 소품 하나의 행렬을 바꾸면 그것만 움직인다
- 폰에서 FPS 가 같거나 오른다

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

## 원리
- 물체 하나를 그릴 때마다 CPU 가 행렬 계산 · 재질 바꾸기 · 그리기 호출을 한다. 소품 40개면 호출 40번 (견본 왼쪽).
- BatchedMesh 는 여러 모양(geometry)을 큰 버퍼 하나에 모아 두고, 놓인 자리(instance)마다 행렬 · 색만 따로 가진다.
- 그래서 모양이 달라도 재질이 같으면 호출 한 번에 다 그린다 (견본 오른쪽: 40 → 1).
- 먼저 모든 모양의 꼭짓점 · 색인 수를 더해 버퍼 크기를 정하고 → addGeometry → addInstance → setMatrixAt · setColorAt.

## 핵심 코드 — 모양 4가지 · 자리 40개를 BatchedMesh 한 묶음으로
(발췌: 새로 씀 (견본 demos/demosSystem.ts i34 는 개념 그림 — 소품 40개 · 4종 · 크기 0.7~1.3 은 견본과 같게))
```ts
import * as THREE from 'three';

// 모두 색인이 있는 모양만 (색인 있음/없음을 섞으면 넣을 수 없다)
const geos = [
  new THREE.ConeGeometry(0.4, 1, 8),          // 나무
  new THREE.SphereGeometry(0.3, 12, 8),       // 바위
  new THREE.CylinderGeometry(0.12, 0.12, 0.5, 8), // 꽃대
  new THREE.BoxGeometry(0.4, 0.4, 0.4),       // 상자
];
const COLORS = ['#3ccf7a', '#a8b0c0', '#ff7ab0', '#ffb22e'];
let maxV = 0;
let maxI = 0;
for (const g of geos) {
  maxV += g.getAttribute('position').count;
  maxI += g.index!.count;
}
const N = 40;
const batch = new THREE.BatchedMesh(N, maxV, maxI, new THREE.MeshStandardMaterial({ roughness: 0.7 }));
const ids = geos.map((g) => batch.addGeometry(g));

const m = new THREE.Matrix4();
const p = new THREE.Vector3();
const q = new THREE.Quaternion();
const s = new THREE.Vector3();
const col = new THREE.Color();
for (let i = 0; i < N; i++) {
  const k = i % 4;
  const inst = batch.addInstance(ids[k]!);
  p.set(Math.random() * 10 - 5, 0, Math.random() * 10 - 5);
  s.setScalar(0.7 + Math.random() * 0.6);
  batch.setMatrixAt(inst, m.compose(p, q, s));
  batch.setColorAt(inst, col.set(COLORS[k]!));
}
scene.add(batch);
// 확인: renderer.info.render.calls — 소품 40개인데 1
```

## 흔한 실수 · 확인 목록
- [ ] **버퍼 크기를 어림으로 주면 addGeometry 에서 오류가 난다** — 넣을 모양들의 position.count · index.count 를 더해 정확히 준다. 나중에 더 넣을 거면 그만큼 여유를.
- [ ] **색인 있는 모양과 없는 모양(Tetrahedron 등)을 섞으면 넣을 수 없다** — 모두 색인이 있게 맞추거나(mergeVertices) 묶음을 나눈다.
- [ ] **색마다 재질을 따로 만들면 묶음이 쪼개진다** — 재질은 하나, 색은 setColorAt 으로.
- [ ] **같은 모양 하나뿐인데 BatchedMesh 를 쓰면 손해다** — 그럴 땐 InstancedMesh(u36)가 더 가볍다. 움직이지 않으면 고정 메시 합치기(i391)도 된다.

## 완성 기준 체크리스트
- [ ] 바꾸기 전과 같은 자리 · 크기 · 색으로 소품이 보인다 (전/후 스크린숏이 같다)
- [ ] renderer.info.render.calls 가 소품 수만큼 → 1(묶음 수)로 줄어든다
- [ ] 소품 하나의 행렬을 바꾸면 그것만 움직인다
- [ ] 폰에서 FPS 가 같거나 오른다

## 이 기술 정보
- id: `i34` · 분류: 게임 시스템 · AI › 플랫폼 · 성능 · 3D · 난이도 보통 · 폰 부담 가벼움 (폰 OK) — CPU 쪽 비용이 물체 수만큼 → 묶음 수만큼으로 줄어든다. 삼각형 수는 그대로라 GPU 비용은 같다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i34
- 쓰면 좋을 때: 모양은 몇 가지인데 각각 놓인 자리가 많은 소품 무대 / 움직이는 소품이라 고정 메시 합치기(mergeStatic)를 못 쓸 때 — 행렬만 바꾸면 된다
- 쓰지 말 때: 같은 모양 하나만 수천 개 — InstancedMesh(u36)가 더 단순하고 빠르다 / 재질이 다 다른 물체 — 재질마다 묶음이 따로 생겨 이득이 없다. 대신 색은 setColorAt 으로

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i34 견본 항목 — `src/demos/demosSystem.ts:2259`
```ts
  i34: {
    kind: '2d',
    caption: '왼쪽: 소품을 하나씩 그리면 그리기 호출 40번 · 오른쪽 BatchedMesh: 모양이 달라도 한 묶음으로 1번',
    make() {
      const R = rnd(42);
      const props = Array.from({ length: 40 }, () => ({ x: R(), y: R(), k: Math.floor(R() * 4), s: 0.7 + R() * 0.6 }));
      const prop = (g: G, x: number, y: number, k: number, s: number): void => {
        if (k === 0) {
          box(g, x - 1.5 * s, y, 3 * s, 6 * s, 1, '#8a5a2a');
          circle(g, x, y - 2 * s, 6 * s, '#3ccf7a');
        } else if (k === 1) {
          circle(g, x, y + 2, 5 * s, '#a8b0c0');
        } else if (k === 2) {
          circle(g, x, y, 3.5 * s, '#ff7ab0');
          circle(g, x, y, 1.4 * s, '#ffd23f');
        } else {
          g.beginPath();
          g.moveTo(x, y - 7 * s);
          g.lineTo(x + 5 * s, y + 4 * s);
          g.lineTo(x - 5 * s, y + 4 * s);
          g.closePath();
          g.fillStyle = '#ffb22e';
          g.fill();
        }
      };
      return {
        draw(g, w, h, t) {
          stage(g, w, h, MINT);
          const P = 4.5;
          const x = t % P;
          const half = (ox: number, batch: boolean): void => {
            box(g, ox, 12, 148, 118, 10, '#e8f8d8', batch ? '#2bb673' : '#ff9e3d', 2);
            const shown = batch ? (x > 1.4 ? 40 : 0) : Math.min(40, Math.floor(x / 0.08));
            props.slice(0, shown).forEach((p, i) => {
              const fresh = !batch && i === shown - 1;
              prop(g, ox + 12 + p.x * 124, 26 + p.y * 92, p.k, p.s * (fresh ? 1.5 : 1));
            });
            // 보내는 짐
            if (batch) {
              if (x < 1.4) {
                const p = ease(x / 1.4);
                const by = lerp(186, 70, p);
                box(g, ox + 50, by - 14, 48, 28, 6, '#2bb673', '#1a7a4a', 1.5);
                for (let k = 0; k < 4; k++) prop(g, ox + 60 + k * 10, by, k, 0.6);
              }
            } else if (shown < 40) {
              const p = (x % 0.08) / 0.08;
              const it = props[shown]!;
              prop(g, lerp(ox + 74, ox + 12 + it.x * 124, p), lerp(186, 26 + it.y * 92, p), it.k, it.s);
            }
            const calls = batch ? (x > 1.4 ? 1 : 0) : shown;
            box(g, ox + 14, 138, 120, 26, 8, '#fff', batch ? '#2bb673' : '#ff9e3d', 1.5);
            txt(g, `그리기 호출 ${calls}번`, ox + 74, 151.5, 10, batch ? '#1a7a4a' : '#c0600a', 'center', 800);
            txt(g, batch ? 'BatchedMesh 한 묶음' : '따로따로', ox + 74, 178, 9.5, batch ? '#1a7a4a' : '#c0600a', 'center', 800, TF);
          };
          half(8, false);
          half(164, true);
        },
      };
    },
  }
```

## 관련 기술
- 먼저 알면 좋은 기술: [인스턴싱 (InstancedMesh)](https://ai-techstudio.web.app/ai/t/u36.md) `u36` · [고정 물체 합치기 (그리기 호출 줄이기)](https://ai-techstudio.web.app/ai/t/i391.md) `i391`
- 다음에 해 볼 기술: [지형 조각 캐시 (청크 · LRU)](https://ai-techstudio.web.app/ai/t/i78.md) `i78`
- 참고 문서: [three.js 문서 — BatchedMesh](https://threejs.org/docs/#api/en/objects/BatchedMesh) · [three.js 예제 — webgl_mesh_batch](https://threejs.org/examples/#webgl_mesh_batch)
