# AI 꾸러미 — L-시스템 나무 · 풀 (규칙으로 그리기) — L-system (Lindenmayer system)
> A → F[&A]/[&A]/[&A] 같은 바꿔 쓰기 규칙을 되풀이해 글자열을 만들고, 거북이가 그 글자대로 걸어 나무 · 고사리 · 산호를 그린다.  
> 견본: https://ai-techstudio.web.app/#t/i445

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

## 주문서

### 만들어 줘: L-시스템 나무 · 풀 (규칙으로 그리기) — L-system (Lindenmayer system)

#### 1. 목표
나무 · 고사리 · 산호 세 가지를 L-시스템으로 그려 줘 — 규칙을 되풀이할 때마다 가지가 갈라지고, 거북이가 글자대로 3D 로 걷게. 분위기는 그림책 동화.

#### 2. 핵심 기술 용어
- **L-system (Lindenmayer system)** — 규칙으로 글자열을 되풀이해 바꿔 쓰는 식물 문법
- **Stochastic L-system** — 규칙을 확률로 골라 그루마다 다르게
- **3D turtle graphics (F + - & ^ / [ ])** — 앞으로 · 돌기 · 숙이기 · 비틀기 · 가지 저장/되돌림
- **Tropism** — 가지가 위(빛)나 아래(무게)로 휘는 성질

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

#### 4. 조건
- 규칙 · 각도 · 마디 길이 · 줄어드는 비율 · 굴성은 종류마다 한 묶음(preset)으로 — 나무 · 고사리 · 산호가 같은 코드
- 글자마다 난수를 붙여 확률 규칙 · 각도 흔들림을 정한다 (씨앗이 같으면 같은 그루)
- [ 에서 위치 · 회전(쿼터니언) · 깊이를 통째로 저장, ] 에서 되돌림
- 되풀이 단계는 5 이하로 묶는다
- 가지는 관(굵기 다빈치 규칙), 잎은 잎 카드 InstancedMesh 하나

#### 5. 완성 기준 (이게 보이면 성공)
- 단계를 0 → 5 로 올리면 가지가 한 번씩 갈라지며 나무가 자란다
- 「나무 · 고사리 · 산호」를 바꾸면 같은 코드에 규칙만 바뀌어 전혀 다른 모양이 된다
- 화면에 지금 규칙(예: A → F[&A]/[&A]/[&A])이 보인다
- 각도 슬라이더로 가지가 벌어지는 정도가 바뀐다

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

## 원리
- 처음 글자(공리, 예: FA)에 규칙(A → F[&A]/[&A]/[&A])을 한 번 적용할 때마다 A 가 가지 세 개로 바뀐다.
- 규칙이 둘 이상이면 확률(0.72 · 0.28)로 고른다 — 같은 규칙이라도 그루마다 다르다.
- 거북이가 글자를 읽는다: F 앞으로 한 마디, + − 옆으로 돌기, & ^ 숙이기, / 비틀기, [ 지금 상태 저장, ] 되돌리기.
- 깊이가 깊을수록 마디를 짧게(× decay 0.84), 굴성(trop)으로 가지를 위 · 아래로 살짝 휜다.
- 거북이 자취를 마디 나무로 모아 굵기를 다빈치 규칙으로 정하고 관을 씌우며, 잎 글자 자리에 잎 카드를 놓는다.

## 핵심 코드 — 규칙 되풀이 + 3D 거북이
(발췌: demos/demosModelA.ts LPRESETS · lExpand() · lTurtle() 를 정리)
```ts
const tree = { axiom: 'FA', rules: { A: [{ p: 0.72, s: 'F[&A]/[&A]/[&A]' }, { p: 0.28, s: 'F[&A]//[&A]' }] },
  angle: 40, roll: 120, yaw: 30, len: 0.42, decay: 0.84, jitter: 0.3 };

// 1) 되풀이 — 글자마다 난수 r 을 붙여 확률 규칙을 고른다
function expand(p: typeof tree, n: number, rnd: () => number) {
  let cur = [...p.axiom].map((c) => ({ c, r: rnd() }));
  for (let i = 0; i < n; i++) {
    const nx: { c: string; r: number }[] = [];
    for (const s of cur) {
      const rs = (p.rules as Record<string, { p: number; s: string }[]>)[s.c];
      if (!rs) { nx.push(s); continue; }
      let acc = 0, pick = rs[rs.length - 1]!;
      for (const r of rs) if (s.r < (acc += r.p)) { pick = r; break; }
      for (const c of pick.s) nx.push({ c, r: rnd() });
    }
    cur = nx;
  }
  return cur;
}

// 2) 거북이 — 위치 · 회전 · 깊이, [ 는 저장 ] 는 되돌림
const X = new THREE.Vector3(1, 0, 0), Y = new THREE.Vector3(0, 1, 0), Z = new THREE.Vector3(0, 0, 1);
let st = { p: new THREE.Vector3(), q: new THREE.Quaternion(), depth: 0 };
const stack: (typeof st)[] = [];
const rot = (axis: THREE.Vector3, deg: number, r: number) =>
  st.q.multiply(new THREE.Quaternion().setFromAxisAngle(axis, THREE.MathUtils.degToRad(deg) * (1 + tree.jitter * (r * 2 - 1))));
for (const s of expand(tree, 5, rnd)) {
  if (s.c === 'F') {
    const L = tree.len * Math.pow(tree.decay, st.depth) * (0.88 + s.r * 0.24);
    const from = st.p.clone();
    st.p.addScaledVector(Y.clone().applyQuaternion(st.q), L);
    addBranch(from, st.p.clone()); // 마디 나무에 한 칸
  } else if (s.c === '+') rot(Z, tree.yaw, s.r);
  else if (s.c === '-') rot(Z, -tree.yaw, s.r);
  else if (s.c === '&') rot(X, tree.angle, s.r);
  else if (s.c === '^') rot(X, -tree.angle, s.r);
  else if (s.c === '/') rot(Y, tree.roll, s.r);
  else if (s.c === '[') { stack.push(st); st = { p: st.p.clone(), q: st.q.clone(), depth: st.depth + 1 }; }
  else if (s.c === ']') st = stack.pop() ?? st;
  else if (s.c === 'A') addLeaf(st.p.clone(), st.q.clone()); // 잎 자리
}
```

## 흔한 실수 · 확인 목록
- [ ] **단계를 하나 더 올렸더니 멈췄다** — 가지 셋 규칙은 단계마다 3배씩 늘어난다. 견본은 5단계까지만 두고, 단계별 모양을 미리 지어 둔다.
- [ ] **[ 에서 회전을 복사하지 않고 같은 객체를 넣으면 가지가 엉킨다** — 위치 · 쿼터니언을 clone 해서 저장한다.
- [ ] **각도를 정확히 같게 두면 기계로 찍은 듯하다** — 글자마다 붙은 난수로 각도를 ±30% 흔들고 마디 길이도 0.88 ~ 1.12 배로.

## 완성 기준 체크리스트
- [ ] 단계를 0 → 5 로 올리면 가지가 한 번씩 갈라지며 나무가 자란다
- [ ] 「나무 · 고사리 · 산호」를 바꾸면 같은 코드에 규칙만 바뀌어 전혀 다른 모양이 된다
- [ ] 화면에 지금 규칙(예: A → F[&A]/[&A]/[&A])이 보인다
- [ ] 각도 슬라이더로 가지가 벌어지는 정도가 바뀐다

## 이 기술 정보
- id: `i445` · 분류: 3D 모델 · 캐릭터 › 3D 모델링 · 절차 · 3D · 난이도 보통 · 폰 부담 보통 (폰 주의) — 가지 셋 규칙은 단계마다 글자가 3배 — 5단계면 가지 수백 개. 단계별 모양을 미리 지어 두면 보기는 가볍다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i445
- 쓰면 좋을 때: 같은 「종류」의 식물을 많이 만들 때 — 규칙 한 줄이 종 하나 / 되풀이 단계(0 ~ 5)를 올리며 자라는 모습을 보여 줄 때 / 고사리처럼 자기 닮음이 뚜렷한 모양
- 쓰지 말 때: 정해진 수관 모양을 꽉 채워야 할 때 — 대신 공간 군체화(i444) / 되풀이를 6번 넘게 — 글자 수가 기하급수로 늘어난다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### demoLSystem — `src/demos/demosModelA.ts:1090`
```ts
function demoLSystem(): Scene3D {
  const st = new Stage({ cam: [0, 1.75, 6.0], look: [0, 1.1, 0], island: 1.7 });
  const po = { polygonOffset: true, polygonOffsetFactor: 1, polygonOffsetUnits: 1 };
  const mats = {
    bark: new THREE.MeshStandardMaterial({ vertexColors: true, map: barkTexture(), roughness: 0.85, ...po }),
    leaf: new THREE.MeshStandardMaterial({ vertexColors: true, roughness: 0.6, side: THREE.DoubleSide, ...po }),
    flower: new THREE.MeshStandardMaterial({ vertexColors: true, roughness: 0.55 }),
  };
  const leafG = leafCardGeo();
  const flowerG = flowerGeo(0xffb6d2);
  const S = { seed: 1, preset: 0, angle: 40, auto: true, n: 5 };
  let plants: LPlant[] = [];
  let ms = 0;
  const build = (): void => {
    const t0 = performance.now();
    for (const p of plants) p.dispose();
    plants = [];
    const P = LPRESETS[S.preset]!;
    const main = makePlant(st, P, S.seed, S.angle, mats, leafG, flowerG);
    plants.push(main);
    st.spin.add(main.group);
    if (S.preset === 0) {
      for (const [x, z, k] of [[-1.05, 0.55, 0], [1.0, 0.6, 1], [0.15, 1.05, 2]] as const) {
        const f = makePlant(st, LPRESETS[1]!, S.seed * 7 + k, 28, mats, leafG, flowerG);
        f.group.position.set(x, 0, z);
        f.group.scale.setScalar(0.62);
        f.group.rotation.y = k * 2;
        plants.push(f);
        st.spin.add(f.group);
      }
    }
    main.group.scale.setScalar(S.preset === 1 ? 2.0 : S.preset === 2 ? 1.05 : 1);
    ms = performance.now() - t0;
    last = -1;
  };
  let last = -1;
  let warmL = 0;
  build();
  return st.out({
    update(t) {
      let n = S.n;
      let pop = 0;
      if (S.auto) {
        const k = t % 8.4;
        n = Math.min(LMAX, Math.floor(k / 1.05));
        pop = k - n * 1.05;
      }
      if (warmL++ < 2) n = LMAX;
      if (n !== last) {
        for (const p of plants) p.set(n);
        last = n;
      }
      const b = S.auto && pop < 0.35 ? 1 + 0.035 * Math.sin((pop / 0.35) * Math.PI) : 1;
      st.spin.scale.set(1, b, 1);
      st.spin.rotation.y = Math.sin(t * 0.35) * 0.6;
      st.hud.tl.set(`▲ 삼각형 ${fmt(st.tris())}`);
      st.hud.bl.set(`${LPRESETS[S.preset]!.show}   반복 n = ${n}  (${ms.toFixed(0)}ms)`);
    },
    controls: [
      { type: 'toggle', label: '자동 재생 (n 올리기)', value: true, on: (v) => (S.auto = v) },
      { type: 'range', label: '반복 횟수 n', min: 0, max: LMAX, step: 1, value: 5, on: (v) => ((S.n = v), (S.auto = false)) },
      { type: 'range', label: '규칙 (0 나무 · 1 고사리 · 2 산호)', min: 0, max: 2, step: 1, value: 0, on: (v) => ((S.preset = v), (S.angle = LPRESETS[v]!.angle), build()) },
      { type: 'range', label: '가지 각도 (도)', min: 12, max: 55, step: 1, value: 40, on: (v) => ((S.angle = v), build()) },
      { type: 'toggle', label: '와이어프레임', value: false, on: (v) => (st.wire.on = v) },
      { type: 'button', label: '새로 만들기 (시드)', on: () => ((S.seed += 1), build()) },
    ],
    dispose() {
      for (const p of plants) p.dispose();
      leafG.dispose();
      flowerG.dispose();
    },
  });
}
```

### i445 견본 항목 — `src/demos/demosModelA.ts:2741`
```ts
  i445: { kind: '3d', caption: '규칙 A → F[&A]/[&A]/[&A] 를 되풀이할 때마다 가지가 갈라짐 — 나무와 고사리가 같은 방식', make: demoLSystem }
```

## 관련 기술
- 먼저 알면 좋은 기술: [가지가 자라는 나무 (공간 군체화)](https://ai-techstudio.web.app/ai/t/i444.md) `i444`
- 다음에 해 볼 기술: [가지가 자라는 나무 (공간 군체화)](https://ai-techstudio.web.app/ai/t/i444.md) `i444` · [동화 뭉게 나무 (구 법선 잎 · 층별 바람)](https://ai-techstudio.web.app/ai/t/i538.md) `i538`
- 참고 문서: [Prusinkiewicz · Lindenmayer — The Algorithmic Beauty of Plants](http://algorithmicbotany.org/papers/abop/abop.pdf) · [Wikipedia — L-system](https://en.wikipedia.org/wiki/L-system)
