# AI 꾸러미 — 표면에 디테일 흩뿌리기 (풀 · 꽃 · 자갈) — Surface scattering (area-weighted sampling)
> 섬 표면 삼각형을 넓이에 비례해 무작위로 골라 경사 · 높이 규칙대로 풀잎 · 꽃 · 자갈을 인스턴스로 흩뿌려, 들판이 저절로 채워지게 한다.  
> 견본: https://ai-techstudio.web.app/#t/i455

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

## 주문서

### 만들어 줘: 표면에 디테일 흩뿌리기 (풀 · 꽃 · 자갈) — Surface scattering (area-weighted sampling)

#### 1. 목표
바다 위 작은 섬 디오라마에 풀잎 · 꽃 · 자갈을 흩뿌려 줘 — 표면을 넓이 비례로 뽑고, 기울기 · 높이 규칙대로 인스턴스로. 분위기는 파스텔 동화.

#### 2. 핵심 기술 용어
- **Surface scattering (area-weighted sampling)** — 넓이 비례로 표면 위 점 뽑기
- **MeshSurfaceSampler (three/examples)** — three.js 의 같은 일을 하는 도구
- **Slope / height rules** — 기울기 · 높이에 따라 무엇을 놓을지
- **InstancedMesh** — 같은 모양 수만 개를 한 번에

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

#### 4. 조건
- 넓이 비례 표본 (누적 합 + 이분 탐색) — 정점이나 격자에서 고르면 비탈 · 작은 삼각형에 몰린다
- 삼각형 안 점은 u + v > 1 이면 (1−u, 1−v) 로 뒤집어 고르게
- 놓는 규칙은 높이(물 · 모래 · 풀밭)와 기울기(법선 y 와 문턱)로
- 꽃 · 풀은 잡음으로 무리를 지어 — 고른 흩뿌리기는 지저분해 보인다
- 종류마다 InstancedMesh 하나, 세우는 방향은 위쪽과 법선 사이(풀 0.3 · 자갈 1)

#### 5. 완성 기준 (이게 보이면 성공)
- 섬이 생기고 언덕 쪽부터 풀 · 꽃 · 자갈이 차례로 피어난다
- 비탈에는 풀 대신 돌이, 바닷가에는 자갈이, 풀밭에는 꽃 무리가 놓인다
- 「경사 문턱」 슬라이더를 올리면 풀이 자라는 비탈이 줄고, 「밀도」로 양이 바뀐다
- 「흩뿌리기」를 끄면 맨 섬만 남아 차이가 보인다

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

## 원리
- 삼각형마다 넓이를 재서 누적 합 표를 만든다. 0 ~ 전체 넓이 무작위 수를 이분 탐색하면 넓은 삼각형이 더 자주 뽑힌다.
- 삼각형 안 점은 무게중심 좌표(u, v, u+v>1 이면 뒤집기)로 고르게, 법선도 같은 비율로 섞는다.
- 규칙: 물 아래는 가끔 자갈, 모래 높이엔 자갈 조금, 법선 y 가 경사 문턱(0.82)보다 작은 비탈엔 돌, 나머지 풀밭엔 풀 · 꽃 무리.
- 꽃은 잡음 무리(patch > 0.52)에서만, 풀은 잡음으로 짙고 옅게 — 고르게 흩지 않고 무리 지어 자연스럽다.
- 풀 · 꽃 · 자갈은 종류마다 InstancedMesh 하나, 언덕에서 멀수록 늦게 피어나게(birth) 한다.

## 핵심 코드 — 넓이 비례 표면 표본 + 높이 · 경사 규칙
(발췌: demos/demosModelB.ts areaSampler() · scatterDemo() 를 정리)
```ts
function areaSampler(geo: THREE.BufferGeometry, rng: () => number) {
  const P = geo.getAttribute('position').array as Float32Array;
  const Nn = geo.getAttribute('normal').array as Float32Array;
  const I = geo.getIndex()!.array;
  const T = I.length / 3, cum = new Float32Array(T);
  const a = new THREE.Vector3(), b = new THREE.Vector3(), c = new THREE.Vector3();
  let tot = 0;
  for (let t = 0; t < T; t++) {
    a.fromArray(P, I[t * 3] * 3); b.fromArray(P, I[t * 3 + 1] * 3); c.fromArray(P, I[t * 3 + 2] * 3);
    tot += 0.5 * b.sub(a).cross(c.sub(a)).length(); // 삼각형 넓이
    cum[t] = tot;
  }
  return (p: THREE.Vector3, n: THREE.Vector3) => {
    const r = rng() * tot;
    let lo = 0, hi = T - 1;
    while (lo < hi) { const m = (lo + hi) >> 1; if (cum[m] < r) lo = m + 1; else hi = m; } // 이분 탐색
    let u = rng(), v = rng();
    if (u + v > 1) { u = 1 - u; v = 1 - v; } // 삼각형 안으로
    const w = 1 - u - v, ia = I[lo * 3] * 3, ib = I[lo * 3 + 1] * 3, ic = I[lo * 3 + 2] * 3;
    p.set(P[ia] * w + P[ib] * u + P[ic] * v, P[ia + 1] * w + P[ib + 1] * u + P[ic + 1] * v, P[ia + 2] * w + P[ib + 2] * u + P[ic + 2] * v);
    n.set(Nn[ia] * w + Nn[ib] * u + Nn[ic] * v, Nn[ia + 1] * w + Nn[ib + 1] * u + Nn[ic + 1] * v, Nn[ia + 2] * w + Nn[ib + 2] * u + Nn[ic + 2] * v).normalize();
  };
}

const sample = areaSampler(islandGeo, rng);
for (let i = 0; i < 26000 * density; i++) {
  sample(p, n);
  const r = rng();
  if (p.y < -0.03) { if (r < 0.03) addPebble(p, n); }           // 물 아래
  else if (p.y < 0.075) { if (r < 0.12) addPebble(p, n); }      // 모래
  else if (n.y < th) { if (r < 0.06) addPebble(p, n); }         // 비탈 (th = 0.82)
  else {
    const patch = fbm2(p.x * 2.4, p.z * 2.4, s + 11, 3);         // 무리 짓기
    if (patch > 0.52 && r < 0.22 && n.y > th + 0.05) addFlower(p, n);
    else if (r < 0.8 * smooth(0.3, 0.55, patch + 0.25)) addGrass(p, n);
  }
}
```

## 흔한 실수 · 확인 목록
- [ ] **정점 위치에서 고르면 작은 삼각형이 많은 곳에 장식이 몰린다** — 삼각형 넓이 누적 합으로 뽑아야 넓이당 개수가 고르다.
- [ ] **u, v 를 그냥 쓰면 점의 절반이 삼각형 밖으로 나간다** — u + v > 1 이면 둘 다 1 에서 빼서 안으로 접는다.
- [ ] **풀 · 꽃을 고르게 흩으면 지저분해 보인다** — 잡음으로 무리(patch)를 만들어 같은 색 꽃끼리 모이게 한다. 이 사이트의 강 건너기도 「흩뿌리기는 지저분」 지적을 받고 무리로 바꿨다.
- [ ] **풀잎마다 그림자를 켜면 그림자 지도가 무거워진다** — 그림자는 꽃 · 자갈 같은 큰 것만, 풀은 받기만 한다.

## 완성 기준 체크리스트
- [ ] 섬이 생기고 언덕 쪽부터 풀 · 꽃 · 자갈이 차례로 피어난다
- [ ] 비탈에는 풀 대신 돌이, 바닷가에는 자갈이, 풀밭에는 꽃 무리가 놓인다
- [ ] 「경사 문턱」 슬라이더를 올리면 풀이 자라는 비탈이 줄고, 「밀도」로 양이 바뀐다
- [ ] 「흩뿌리기」를 끄면 맨 섬만 남아 차이가 보인다

## 이 기술 정보
- id: `i455` · 분류: 3D 모델 · 캐릭터 › 3D 모델링 · 절차 · 3D · 난이도 쉬움 · 폰 부담 보통 (폰 주의) — 표본 2만 6천 번(밀도 1) → 풀 · 꽃 · 자갈 인스턴스 수천 ~ 1만여 개, 그리기는 종류마다 1번. 그림자를 켜는 종류는 골라서.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i455
- 쓰면 좋을 때: 섬 · 들판 · 마당을 빈틈없이 채우되 손으로 놓기 힘들 때 / 지형이 바뀌어도 장식이 저절로 따라오게 하고 싶을 때
- 쓰지 말 때: 바닥이 평평한 네모 판 — 그냥 격자 + 흔들기로 충분 / 장식 하나하나를 고르고 움직여야 할 때 — 인스턴스는 개별 조작이 번거롭다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### scatterDemo — `src/demos/demosModelB.ts:2833`
```ts
function scatterDemo(): Scene3D {
  const scene = new THREE.Scene();
  const bg = bgTex('#16304a', '#4f8fa8');
  scene.background = bg;
  addLights(scene, { sky: 0xe6f4ff, ground: 0x4a5a3a, hemi: 1.0, sun: 2.8, dir: [-3, 6, 3], ext: 2.8 });
  const env = new Env();
  const camera = new THREE.PerspectiveCamera(36, 1.6, 0.1, 50);
  const tags = new Tags(2, ['rgba(12,16,34,0.72)', 'rgba(40,90,60,0.85)']);
  // 디오라마 바다
  const water = new THREE.Mesh(new THREE.CircleGeometry(3.3, 96).rotateX(-PI / 2), new THREE.MeshPhysicalMaterial({ color: 0x2c8fb0, transparent: true, opacity: 0.74, roughness: 0.2, envMapIntensity: 0.35 }));
  water.position.y = 0.0;
  const seabed = new THREE.Mesh(new THREE.CircleGeometry(3.3, 96).rotateX(-PI / 2), new THREE.MeshStandardMaterial({ color: 0x8fae9a, roughness: 1 }));
  seabed.position.y = -0.43;
  const wall = new THREE.Mesh(new THREE.CylinderGeometry(3.3, 3.3, 0.46, 96, 1, true), new THREE.MeshStandardMaterial({ color: 0x1f5f7a, roughness: 0.6 }));
  wall.position.y = -0.22;
  seabed.receiveShadow = true;
  scene.add(water, seabed, wall);

  const blade = bladeGeo();
  const bladeMat = new THREE.MeshStandardMaterial({ vertexColors: true, side: THREE.DoubleSide, roughness: 0.75 });
  const stemG = new THREE.CylinderGeometry(0.0035, 0.005, 0.1, 5).translate(0, 0.05, 0);
  const petals: THREE.BufferGeometry[] = [];
  for (let k = 0; k < 5; k++) {
    const g = new THREE.SphereGeometry(1, 10, 6).scale(0.026, 0.006, 0.015).translate(0.022, 0, 0).rotateY((k / 5) * TAU).translate(0, 0.1, 0);
    petals.push(g);
  }
  const petalG = mergeGeometries(petals)!;
  for (const g of petals) g.dispose();
  const centerG = new THREE.SphereGeometry(0.011, 10, 6).translate(0, 0.104, 0);
  const pebbleG = new THREE.IcosahedronGeometry(1, 1);
  const green = new THREE.MeshStandardMaterial({ color: 0x4f9a3a, roughness: 0.7 });
  const petalMat = new THREE.MeshStandardMaterial({ roughness: 0.55 });
  const yellow = new THREE.MeshStandardMaterial({ color: 0xffc83d, roughness: 0.5 });
  const pebbleMat = new THREE.MeshStandardMaterial({ roughness: 0.85, flatShading: true });
  const islandMat = new THREE.MeshStandardMaterial({ vertexColors: true, roughness: 0.95 });

  let seed = 21;
  let density = 1;
  let th = 0.82;
  let on = true;
  let ms = 0;
  let island: THREE.Mesh | null = null;
  let meshes: THREE.InstancedMesh[] = [];
  let groups: { list: Inst[]; meshes: THREE.InstancedMesh[]; delay: number }[] = [];
  const counts = [0, 0, 0];

  function height(x: number, z: number, s: number, hill: [number, number], mound: [number, number]): number {
    const ang = Math.atan2(z, x);
    const warp = 1 + 0.36 * (fbm2(Math.cos(ang) * 1.3 + 5, Math.sin(ang) * 1.3, s, 3) - 0.5);
    const r = Math.hypot(x, z) / (1.6 * warp);
    let h = lerp(0.3, -0.42, smooth(0.5, 1.12, r));
    h += 0.06 * smooth(0.8, 0.3, r);
    const hd = Math.hypot(x - hill[0], z - hill[1]) + (fbm2(x * 2, z * 2, s + 3, 3) - 0.5) * 0.3;
    h += 0.4 * smooth(0.52, 0.36, hd);
    h += 0.13 * Math.exp(-((x - mound[0]) ** 2 + (z - mound[1]) ** 2) / 0.14);
    h += (fbm2(x * 1.8, z * 1.8, s + 7, 3) - 0.5) * 0.12 * smooth(1.15, 0.6, r);
    return h;
  }

  const job = new Job();
  const warm = new Warm();
  function build(): void {
    job.start(buildG());
  }
  function* buildG(): G {
    if (island) {
      scene.remove(island);
      island.geometry.dispose();
    }
    for (const m of meshes) {
      scene.remove(m);
      m.dispose();
    }
    meshes = [];
    groups = [];
    const rng = rngOf(seed);
    const s = Math.floor(rng() * 9999);
    const ha = rng() * TAU;
    const hill: [number, number] = [Math.cos(ha) * 0.55, Math.sin(ha) * 0.45];
    const mound: [number, number] = [-hill[0] * 0.9 + (rng() - 0.5) * 0.4, -hill[1] * 0.9 + (rng() - 0.5) * 0.4];
    const g = new THREE.PlaneGeometry(4.4, 4.4, 150, 150);
    yield;
    g.rotateX(-PI / 2);
    yield;
    const pos = g.getAttribute('position') as THREE.BufferAttribute;
    for (let i = 0; i < pos.count; i++) {
      if ((i & 511) === 511 && late()) yield;
      pos.setY(i, height(pos.getX(i), pos.getZ(i), s, hill, mound));
    }
    yield;
    g.computeVertexNormals();
    yield;
    const nor = g.getAttribute('normal');
    const col = new Float32Array(pos.count * 3);
    const sand = new THREE.Color(0xe8d49a);
    const deep = new THREE.Color(0x8fae9a);
    const dirt = new THREE.Color(0x9b7b5b);
    const rock = new THREE.Color(0x8c8a86);
    const g1 = new THREE.Color(0x5fa840);
    const g2 = new THREE.Color(0x9ccc5a);
    const c = new THREE.Color();
    const c2 = new THREE.Color();
    yield;
    for (let i = 0; i < pos.count; i++) {
      if ((i & 511) === 511 && late()) yield;
      const x = pos.getX(i);
      const y = pos.getY(i);
      const z = pos.getZ(i);
      const ny = nor.getY(i);
      const nz = fbm2(x * 3, z * 3, s + 9, 3);
      c.copy(g1).lerp(g2, smooth(0.35, 0.7, nz));
      c2.copy(dirt).lerp(rock, smooth(0.4, 0.6, fbm2(x * 6, z * 6, s + 2, 2)));
      c.lerp(c2, smooth(th - 0.02, th - 0.12, ny));
      c.lerp(sand, smooth(0.09, 0.03, y));
      c.lerp(deep, smooth(-0.05, -0.4, y));
      col[i * 3] = c.r;
      col[i * 3 + 1] = c.g;
      col[i * 3 + 2] = c.b;
    }
    g.setAttribute('color', new THREE.BufferAttribute(col, 3));
    island = new THREE.Mesh(g, islandMat);
    island.receiveShadow = true;
    island.castShadow = true;
    const isl = island;
    warm.add(isl, scene, () => island === isl);

    // 넓이 비례 표본 → 규칙
    yield;
    const sampler = yield* areaSampler(g, rng);
    yield;
    const N = Math.round(26000 * density);
    const grass: Inst[] = [];
    const flowers: Inst[] = [];
    const flowerC: THREE.Color[] = [];
    const pebbles: Inst[] = [];
    const pebC: THREE.Color[] = [];
    const grassC: THREE.Color[] = [];
    const p = new THREE.Vector3();
    const n = new THREE.Vector3();
    const up = new THREE.Vector3(0, 1, 0);
    const origin = new THREE.Vector3(hill[0], 0, hill[1]);
    const FC = [0xff8fb1, 0xfff6e8, 0xffd84d, 0xc39bff, 0x8fd0ff];
    const birth = (q: THREE.Vector3): number => Math.hypot(q.x - origin.x, q.z - origin.z) * 0.55 + rng() * 0.25;
    const orient = (nn: THREE.Vector3, lean: number): THREE.Quaternion => {
      const dir = up.clone().lerp(nn, lean).normalize();
      return new THREE.Quaternion().setFromUnitVectors(up, dir).multiply(new THREE.Quaternion().setFromAxisAngle(up, rng() * TAU));
    };
    for (let i = 0; i < N; i++) {
      if ((i & 255) === 255 && late()) yield;
      sampler.sample(p, n);
      const y = p.y;
      const r = rng();
      if (y < -0.03) {
        if (r < 0.03) {
          const sc = 0.02 + rng() * 0.03;
          pebbles.push({ p: p.clone(), q: orient(n, 1), s: new THREE.Vector3(sc, sc * 0.55, sc * 0.8), birth: birth(p) });
          pebC.push(new THREE.Color().setHSL(0.08, 0.15, 0.5 + rng() * 0.25));
        }
      } else if (y < 0.075) {
        if (r < 0.12) {
          const sc = 0.018 + rng() * 0.03;
          pebbles.push({ p: p.clone(), q: orient(n, 1), s: new THREE.Vector3(sc, sc * 0.55, sc * 0.8), birth: birth(p) });
          pebC.push(new THREE.Color().setHSL(0.08, 0.12, 0.55 + rng() * 0.3));
        }
      } else if (n.y < th) {
        if (r < 0.06) {
          const big = rng() < 0.12;
          const sc = big ? 0.06 + rng() * 0.07 : 0.02 + rng() * 0.03;
          pebbles.push({ p: p.clone(), q: orient(n, 1), s: new THREE.Vector3(sc, sc * 0.6, sc * 0.85), birth: birth(p) });
          pebC.push(new THREE.Color().setHSL(0.07, 0.1, 0.38 + rng() * 0.25));
        }
      } else {
        const patch = fbm2(p.x * 2.4, p.z * 2.4, s + 11, 3);
        if (patch > 0.52 && r < 0.22 && n.y > th + 0.05) {
          const sc = 0.8 + rng() * 0.5;
          flowers.push({ p: p.clone(), q: orient(n, 0.2), s: new THREE.Vector3(sc, sc, sc), birth: birth(p) + 0.5 });
          flowerC.push(new THREE.Color(FC[Math.floor(fbm2(p.x * 1.3, p.z * 1.3, s + 13, 2) * 10) % FC.length]!));
        } else if (r < 0.8 * smooth(0.3, 0.55, patch + 0.25)) {
          const hh = 0.06 + rng() * 0.07;
          grass.push({ p: p.clone(), q: orient(n, 0.3), s: new THREE.Vector3(1, hh, hh), birth: birth(p) + 0.2 });
          grassC.push(new THREE.Color(0x6bb24a).lerp(new THREE.Color(0xb7d65a), rng() * 0.6 + (patch - 0.5)));
        }
      }
    }
    ms = job.spent;
    counts[0] = grass.length;
    counts[1] = flowers.length;
    counts[2] = pebbles.length;
    const inst = (geo: THREE.BufferGeometry, mat: THREE.Material, list: Inst[], cols: THREE.Color[] | null, shadow: boolean): THREE.InstancedMesh => {
      const m = new THREE.InstancedMesh(geo, mat, Math.max(1, list.length));
      m.count = list.length;
      if (cols) cols.forEach((cc, i) => m.setColorAt(i, cc));
      m.castShadow = shadow;
      m.receiveShadow = true;
      warm.add(m, scene, () => meshes.includes(m));
      meshes.push(m);
      return m;
    };
    groups.push({ list: pebbles, meshes: [inst(pebbleG, pebbleMat, pebbles, pebC, true)], delay: 0 });
    groups.push({ list: grass, meshes: [inst(blade, bladeMat, grass, grassC, false)], delay: 0 });
    groups.push({ list: flowers, meshes: [inst(stemG, green, flowers, null, false), inst(petalG, petalMat, flowers, flowerC, true), inst(centerG, yellow, flowers, null, false)], delay: 0 });
    islandMat.wireframe = wireOn;
    lastK = -1;
  }
  let wireOn = false;
  let lastK = -1;
  build();

  const CYC = 6.5;
  const mtx = new THREE.Matrix4();
  const sv = new THREE.Vector3();
  return {
    scene,
    camera,
    update(t) {
      const a = t * 0.12 + 0.6;
      camera.position.set(Math.sin(a) * 3.5, 2.3, Math.cos(a) * 3.5);
      camera.lookAt(0, 0.12, 0);
      if (job.busy) job.step();
      const p = t % CYC;
      const grow = p - 0.3;
      const shrink = easeIO(sat((p - 5.7) / 0.5));
      const k = on ? Math.round((grow < 3.2 ? grow : 3.2) * 60) / 60 + shrink * 10 : -5;
      if (k === lastK) return;
      lastK = k;
      for (const gr of groups) {
        gr.list.forEach((it, i) => {
          const s0 = on ? easeBack(sat((grow - it.birth) / 0.45)) * (1 - shrink) : 0;
          sv.copy(it.s).multiplyScalar(s0 + 1e-4);
          mtx.compose(it.p, it.q, sv);
          for (const m of gr.meshes) m.setMatrixAt(i, mtx);
        });
        for (const m of gr.meshes) m.instanceMatrix.needsUpdate = true;
      }
    },
    render(r, w, h) {
      env.apply(r, scene, 0.35);
      tags.set(0, job.busy ? `섬 만들고 흩뿌리는 중 · 나눠서 ${job.spent.toFixed(0)}ms` : `풀 ${fmt(counts[0]!)} · 꽃 ${fmt(counts[1]!)} · 자갈 ${fmt(counts[2]!)} · 표본 ${fmt(26000 * density)}개 · ${ms.toFixed(0)}ms`);
      tags.set(1, on ? '완만한 곳 → 풀 · 꽃 / 가파른 곳 · 물가 → 자갈' : '흩뿌리기 끔 — 맨 땅');
      if (warm.tick(r, scene, camera, 0x24506a)) shadowed(r, () => r.render(scene, camera));
      tags.draw(r, w, h, [
        [0.02, 0.06, -1],
        [0.5, 0.92, 0],
      ]);
    },
    controls: [
      { type: 'toggle', label: '흩뿌리기', value: true, on: (v) => ((on = v), (lastK = -99)) },
      {
        type: 'range',
        label: '밀도',
        min: 0.2,
        max: 2,
        step: 0.1,
        value: 1,
        on: (v) => {
          density = v;
          build();
        },
      },
      {
        type: 'range',
        label: '경사 문턱 (풀이 자라는 기울기)',
        min: 0.5,
        max: 0.97,
        step: 0.01,
        value: 0.82,
        on: (v) => {
          th = v;
          build();
        },
      },
      {
        type: 'toggle',
        label: '와이어프레임 (섬)',
        value: false,
        on: (v) => {
          wireOn = v;
          islandMat.wireframe = v;
        },
      },
      {
        type: 'button',
        label: '새로 만들기',
        on: () => {
          seed = (seed * 7919 + 13) % 100000;
          build();
        },
      },
    ] as Control[],
    dispose() {
      island?.geometry.dispose();
      for (const m of meshes) m.dispose();
      for (const x of [blade, stemG, petalG, centerG, pebbleG]) x.dispose();
      for (const x of [bladeMat, green, petalMat, yellow, pebbleMat, islandMat]) x.dispose();
      freeAll(water);
      freeAll(seabed);
      freeAll(wall);
      bg.dispose();
      env.dispose();
      tags.dispose();
    },
  };
}
```

### i455 견본 항목 — `src/demos/demosModelB.ts:3138`
```ts
  i455: { kind: '3d', caption: '섬 표면을 넓이 비례로 뽑아 경사 · 높이 규칙대로 풀잎 · 꽃 · 자갈 인스턴스가 피어남', make: () => scatterDemo() }
```

## 관련 기술
- 먼저 알면 좋은 기술: [인스턴싱 (InstancedMesh)](https://ai-techstudio.web.app/ai/t/u36.md) `u36` · [바위 · 돌 절차 생성](https://ai-techstudio.web.app/ai/t/i452.md) `i452`
- 다음에 해 볼 기술: [동화 뭉게 나무 (구 법선 잎 · 층별 바람)](https://ai-techstudio.web.app/ai/t/i538.md) `i538` · [정점 흔들기 (바람 · 물결)](https://ai-techstudio.web.app/ai/t/i13.md) `i13`
- 참고 문서: [three.js 예제 — webgl_instancing_scatter](https://threejs.org/examples/#webgl_instancing_scatter)
