# AI 꾸러미 — 번개 (중간점 변위) — three-stdlib LightningStrike
> 재귀로 갈라지는 번개 + 빛 번짐 (three-stdlib LightningStrike)  
> 견본: https://ai-techstudio.web.app/#t/i63

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

## 주문서

### 만들어 줘: 번개 (중간점 변위) — three-stdlib LightningStrike

#### 1. 목표
정전기에 「번개 (중간점 변위)」 기술을 깔끔한 기본 느낌으로 넣어 줘. — 재귀로 갈라지는 번개 + 빛 번짐 (three-stdlib LightningStrike)

#### 2. 핵심 기술 용어
- **three-stdlib LightningStrike**

#### 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. 완성 기준 (이게 보이면 성공)
- 견본처럼 「재귀로 갈라지는 번개 + 빛 번짐 (three-stdlib LightningStrike)」 이(가) 첫 화면에서 바로 보인다
- 켬/끔(또는 전/후) 비교 단추로 이 기술이 만드는 차이를 확인할 수 있다
- 중요한 값 1~3개를 슬라이더로 바꿔 볼 수 있다
- 콘솔 오류 0, 폰 폭(390px)에서도 동작한다

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

## 원리
- 재귀로 갈라지는 번개 + 빛 번짐 (three-stdlib LightningStrike)


## 흔한 실수 · 확인 목록
- (아직 정리된 것이 없어요)

## 완성 기준 체크리스트
- [ ] 견본처럼 「재귀로 갈라지는 번개 + 빛 번짐 (three-stdlib LightningStrike)」 이(가) 첫 화면에서 바로 보인다
- [ ] 켬/끔(또는 전/후) 비교 단추로 이 기술이 만드는 차이를 확인할 수 있다
- [ ] 중요한 값 1~3개를 슬라이더로 바꿔 볼 수 있다
- [ ] 콘솔 오류 0, 폰 폭(390px)에서도 동작한다

## 이 기술 정보
- id: `i63` · 분류: 이펙트 · 연출 › 화려한 효과 (VFX) · 3D · 난이도 보통 · 폰 부담 보통 (폰 주의)
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i63
- 쓰면 좋을 때: 쓰면 좋을 곳 — 정전기 · 전기 회로 · 번개 직구

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### lightningDemo — `src/demos/demosShader.ts:1845`
```ts
function lightningDemo(): Scene3D {
  const scene = new THREE.Scene();
  const bgCol = new THREE.Color();
  scene.background = bgCol;
  const amb = new THREE.HemisphereLight(0xa0b0ff, 0x302040, 2.0);
  scene.add(amb);
  const flash = new THREE.PointLight(0xc8d0ff, 0, 12, 1.2);
  flash.position.set(0, 0.5, 1.5);
  scene.add(flash);
  const world = new THREE.Group();
  const cloudM = new THREE.MeshStandardMaterial({ color: 0x8a8cb4, roughness: 1 });
  for (let i = 0; i < 9; i++) {
    const s = new THREE.Mesh(new THREE.SphereGeometry(0.6 + (i % 3) * 0.2, 20, 14), cloudM);
    s.position.set(-3.2 + i * 0.8, 2.4 + Math.sin(i * 1.7) * 0.2, -0.6 - (i % 2) * 0.4);
    s.scale.y = 0.65;
    world.add(s);
  }
  const hill = new THREE.Mesh(new THREE.SphereGeometry(4, 32, 16), new THREE.MeshStandardMaterial({ color: 0x3a6a4a, roughness: 1 }));
  hill.position.set(0, -5.6, -1);
  hill.scale.x = 1.6;
  world.add(hill);
  // 피뢰침 탑
  const poleM = new THREE.MeshStandardMaterial({ color: 0x9aa2b8, metalness: 0.7, roughness: 0.3 });
  const tower = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.12, 1.2, 8), poleM);
  tower.position.set(0.3, -1.3, 0);
  const tip = new THREE.Mesh(new THREE.SphereGeometry(0.07, 12, 8), poleM);
  tip.position.set(0.3, -0.68, 0);
  const house = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.45, 0.5), new THREE.MeshStandardMaterial({ color: 0xd88a5a }));
  house.position.set(-1.2, -1.55, 0);
  const roof = new THREE.Mesh(new THREE.ConeGeometry(0.48, 0.35, 4), new THREE.MeshStandardMaterial({ color: 0x8a3a3a }));
  roof.rotation.y = Math.PI / 4;
  roof.position.set(-1.2, -1.15, 0);
  world.add(tower, tip, house, roof);
  scene.add(world);
  const glowU = { uW: { value: 6 }, uI: { value: 0 }, uC: { value: new THREE.Color(0x7a6aff) }, uK: { value: 2.2 } };
  const coreU = { uW: { value: 1.2 }, uI: { value: 0 }, uC: { value: new THREE.Color(0xffffff) }, uK: { value: 1.5 } };
  const mkMat = (u: typeof glowU): THREE.ShaderMaterial =>
    new THREE.ShaderMaterial({
      uniforms: u,
      transparent: true,
      depthWrite: false,
      depthTest: false,
      side: THREE.DoubleSide,
      blending: THREE.AdditiveBlending,
      vertexShader: /* glsl */ `attribute vec2 aN; attribute float aSide; attribute float aW; uniform float uW; varying float vS;
        void main(){ vS = aSide; vec3 p = position + vec3(aN * aSide * aW * uW, 0.0); gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0); }`,
      fragmentShader: /* glsl */ `uniform float uI; uniform vec3 uC; uniform float uK; varying float vS;
        void main(){ float a = exp(-vS * vS * uK * 2.0); gl_FragColor = vec4(uC * a * uI, 1.0); }`,
    });
  const glowM = mkMat(glowU);
  const coreM = mkMat(coreU);
  let geo = new THREE.BufferGeometry();
  const glowMesh = new THREE.Mesh(geo, glowM);
  const coreMesh = new THREE.Mesh(geo, coreM);
  glowMesh.frustumCulled = coreMesh.frustumCulled = false;
  glowMesh.renderOrder = coreMesh.renderOrder = 5;
  scene.add(glowMesh, coreMesh);
  let depth = 7;
  let rough = 0.5;
  let seed = 1;
  let strike = -1;
  let sub = -1;
  let startX = 0;
  const build = (s: number): void => {
    let sd = s * 7919 + 13;
    const rnd = (): number => ((sd = (sd * 16807) % 2147483647) / 2147483647);
    const segs: Seg[] = [];
    bolt(segs, startX, 2.0, 0.3, -0.68, depth, rough, 0.02, rnd, 0);
    const n = segs.length;
    const pos = new Float32Array(n * 4 * 3);
    const nn = new Float32Array(n * 4 * 2);
    const side = new Float32Array(n * 4);
    const ww = new Float32Array(n * 4);
    const idx: number[] = [];
    segs.forEach(([x1, y1, x2, y2, w], i) => {
      const L = Math.hypot(x2 - x1, y2 - y1) || 1;
      const nx = -(y2 - y1) / L;
      const ny = (x2 - x1) / L;
      // 이음새가 벌어지지 않게 양끝을 조금 늘린다
      const ex = ((x2 - x1) / L) * w;
      const ey = ((y2 - y1) / L) * w;
      const P = [x1 - ex, y1 - ey, x1 - ex, y1 - ey, x2 + ex, y2 + ey, x2 + ex, y2 + ey];
      for (let k = 0; k < 4; k++) {
        pos.set([P[k * 2]!, P[k * 2 + 1]!, 0], (i * 4 + k) * 3);
        nn.set([nx, ny], (i * 4 + k) * 2);
        side[i * 4 + k] = k % 2 ? 1 : -1;
        ww[i * 4 + k] = w;
      }
      const a = i * 4;
      idx.push(a, a + 1, a + 2, a + 1, a + 3, a + 2);
    });
    const g = new THREE.BufferGeometry();
    g.setAttribute('position', new THREE.BufferAttribute(pos, 3));
    g.setAttribute('aN', new THREE.BufferAttribute(nn, 2));
    g.setAttribute('aSide', new THREE.BufferAttribute(side, 1));
    g.setAttribute('aW', new THREE.BufferAttribute(ww, 1));
    g.setIndex(idx);
    geo.dispose();
    geo = g;
    glowMesh.geometry = g;
    coreMesh.geometry = g;
  };
  const tags = new Tags(['중간점 변위 7단계']);
  const cam = new THREE.PerspectiveCamera(45, 1, 0.1, 50);
  cam.position.set(0, 0.2, 6.2);
  cam.lookAt(0, 0.2, 0);
  const P = 1.4;
  build(0);
  return {
    scene,
    camera: cam,
    tone: THREE.ACESFilmicToneMapping,
    update(t) {
      const id = Math.floor(t / P);
      const ph = t % P;
      if (id !== strike) {
        strike = id;
        seed = id;
        sub = -1;
        startX = Math.sin(id * 2.3) * 1.8;
      }
      const s2 = ph < 0.14 ? 0 : 1;
      if (s2 !== sub) {
        sub = s2;
        build(seed * 3 + s2);
      }
      const on = ph < 0.65;
      const I = on ? (0.55 + 0.45 * Math.abs(Math.sin(ph * 40))) * Math.pow(1 - ph / 0.65, 0.7) : 0;
      glowU.uI.value = I * 0.9;
      coreU.uI.value = I * 2.2;
      flash.intensity = I * 30;
      flash.position.x = (startX + 0.3) / 2;
      bgCol.setRGB(0.04 + I * 0.25, 0.04 + I * 0.25, 0.1 + I * 0.4);
      amb.intensity = 2.0 + I * 3;
      tags.set(0, `중간점 변위 ${depth}단계`);
    },
    render(r, w, h) {
      r.render(scene, cam);
      tags.draw(r, w, h, [[0.5, 0.92]]);
    },
    controls: [
      {
        type: 'range',
        label: '갈라지는 단계 (중간점을 몇 번 꺾나)',
        min: 1,
        max: 9,
        step: 1,
        value: 7,
        on: (v) => {
          depth = v;
          build(seed * 3 + sub);
        },
      },
      {
        type: 'range',
        label: '꺾임 정도',
        min: 0,
        max: 1,
        step: 0.05,
        value: 0.5,
        on: (v) => {
          rough = v;
          build(seed * 3 + sub);
        },
      },
    ],
    dispose() {
      freeAll(world);
      geo.dispose();
      glowM.dispose();
      coreM.dispose();
      tags.dispose();
    },
  };
}
```

### i63 견본 항목 — `src/demos/demosShader.ts:2392`
```ts
  i63: { kind: '3d', caption: '선을 반으로 나눠 가운데를 꺾기를 되풀이 → 갈라지는 번개 + 빛 번짐 · 번쩍', make: () => lightningDemo() }
```

## 관련 기술
- 이 효과를 만든 기술: [연쇄 번개 (튀는 번개)](https://ai-techstudio.web.app/ai/t/i184.md) `i184`
