# AI 꾸러미 — 워프 · 초공간 이동 — Warp / hyperspace streaks
> 별을 인스턴싱 사각형으로 두고 머리 · 꼬리를 화면에 투영해 그 사이를 띠로 늘려 속도만큼 빛줄로 — 시야각을 넓히고 터널을 열었다가 번쩍 빠져나온다.  
> 견본: https://ai-techstudio.web.app/#t/i530

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

## 주문서

### 만들어 줘: 워프 · 초공간 이동 — Warp / hyperspace streaks

#### 1. 목표
장면 전환 · 레벨 이동에 워프 연출을 넣어 줘 — 별이 빛줄로 늘어나고 시야가 넓어지며 터널이 열렸다가 번쩍 빠져나오게, 느낌은 영화처럼 극적으로.

#### 2. 핵심 기술 용어
- **Warp / hyperspace streaks** — 속도만큼 늘어난 별 빛줄
- **Screen-space line quads (InstancedBufferGeometry)** — 화면에서 두께를 맞춘 띠를 인스턴싱으로
- **FOV kick** — 빨라질 때 시야각을 넓혀 속도감
- **Scrolling noise tunnel** — 원통 안쪽에 흐르는 잡음 터널

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

#### 4. 조건
- 별은 InstancedBufferGeometry (꼭짓점 4 + aCorner) · 인스턴스 속성 aStar(각, 반지름, 깊이, 밝기)
- 띠 두께는 화면 픽셀로 (화면 높이 · 가로세로 비율을 uniform 으로) — 멀어도 가늘어지지 않게
- 간 거리 uDist 는 dt × 빠르기로 쌓는다 (빠르기 = lerp(3, 330, w²))
- 재질 AdditiveBlending · depthTest false · depthWrite false, frustumCulled false
- 시야각을 바꾸면 updateProjectionMatrix — 차이가 0.01 넘을 때만

#### 5. 완성 기준 (이게 보이면 성공)
- 순항 3초 → 2.2초 동안 별이 점점 빛줄로 늘어나며 시야가 넓어진다 → 푸른 터널 → 번쩍하며 빠져나와 다시 점별
- 빛줄 두께가 화면 어디서나 고르고, 가까운 별일수록 밝다
- 「워프 세기」를 손으로 0 ~ 1 로 움직여도 부드럽게 바뀐다
- 폰에서도 60fps

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

## 원리
- 별 하나 = 사각형 하나 (꼭짓점 4개, 인스턴스 2,600개). 별마다 각 · 반지름 · 깊이 · 밝기만 속성으로 넣는다.
- 별 깊이는 mod(깊이 - 간 거리, 300) 으로 되감아 끝없이 다가온다. 머리 자리에서 속도 × 0.045 만큼 뒤가 꼬리다.
- 머리 · 꼬리를 각각 화면에 투영하고 그 사이를 화면 좌표에서 두께가 같은 띠로 펼친다 — 그래서 빛줄이 가늘고 고르다.
- 세기 w(0 ~ 1)로 빠르기 3 → 330 (w²), 시야각 60 → 98°, 화면 살짝 흔들림, 터널 밝기 w² 를 함께. 빠져나올 때 하얀 판을 번쩍.

## 핵심 코드 — 머리 · 꼬리를 투영해 화면 띠로 펼치기 (정점 셰이더)
(발췌: demos/demosSpace.ts WARP_VS · i530 update 를 정리)
```glsl
attribute vec2 aCorner;   // (0 꼬리 | 1 머리, -1 | 1 옆)
attribute vec4 aStar;     // 각 · 반지름 · 깊이 · 밝기
uniform float uDist, uLen, uScale, uAspect, uDepth, uResY, uWarp;
varying float vAlong, vSide, vB;
void main(){
  float zHead = -mod(aStar.z - uDist, uDepth) - 0.6;          // 끝없이 다가오게 되감기
  vec3 head = vec3(cos(aStar.x) * aStar.y, sin(aStar.x) * aStar.y, zHead);
  vec3 tail = head - vec3(0.0, 0.0, uLen * (0.6 + aStar.w * 0.6) + 0.02);
  vec4 ch = projectionMatrix * viewMatrix * vec4(head, 1.0);
  vec4 ct = projectionMatrix * viewMatrix * vec4(tail, 1.0);
  vec2 sh = ch.xy / ch.w, st = ct.xy / ct.w;                  // 화면 위 머리 · 꼬리
  vec2 dir = (sh - st) * vec2(uAspect, 1.0);
  float l = length(dir);
  dir = l > 1e-5 ? dir / l : vec2(0.0, 1.0);
  vec2 nrm = vec2(-dir.y, dir.x) / vec2(uAspect, 1.0);
  vec2 dn = dir / vec2(uAspect, 1.0);
  float near = 1.0 - (-zHead) / uDepth;
  float w = (0.9 + 1.6 * aStar.w) * uScale * (0.5 + near * 1.3) * 2.0 / uResY; // 픽셀 두께
  vec2 s = mix(st, sh, aCorner.x) + nrm * aCorner.y * w + dn * (aCorner.x * 2.0 - 1.0) * w;
  gl_Position = vec4(s, 0.0, 1.0);
  vAlong = aCorner.x; vSide = aCorner.y;
  vB = smoothstep(0.0, 0.25, near) * (0.35 + 0.9 * near * near) * (0.5 + aStar.w);
}
// update(dt): speed = lerp(3, 330, w * w); uDist += speed * dt; uLen = speed * 0.045;
//   cam.fov = lerp(60, 98, smooth(0, 1, w)); 빠져나올 때 하얀 판 opacity = exp(-(경과) * 4) * 0.9
```

## 흔한 실수 · 확인 목록
- [ ] **빛줄을 3D 선(Line)으로 그리면 1px 고정이라 가늘고 두께를 못 바꾼다** — 머리 · 꼬리를 투영해 화면에서 두께를 준 사각형 띠로.
- [ ] **가로세로 비율을 빼먹으면 옆으로 가는 빛줄만 두껍다** — 화면 좌표 방향 · 법선 계산에 uAspect 를 곱했다 나눈다.
- [ ] **시야각을 매 프레임 바꾸며 projection 을 계속 다시 만들면 낭비다** — 차이가 0.01 넘을 때만 updateProjectionMatrix.

## 완성 기준 체크리스트
- [ ] 순항 3초 → 2.2초 동안 별이 점점 빛줄로 늘어나며 시야가 넓어진다 → 푸른 터널 → 번쩍하며 빠져나와 다시 점별
- [ ] 빛줄 두께가 화면 어디서나 고르고, 가까운 별일수록 밝다
- [ ] 「워프 세기」를 손으로 0 ~ 1 로 움직여도 부드럽게 바뀐다
- [ ] 폰에서도 60fps

## 이 기술 정보
- id: `i530` · 분류: 그래픽 · 셰이더 › 우주 표현 · 3D · 난이도 보통 · 폰 부담 가벼움 (폰 OK) — 인스턴스 2,600개를 그리기 1번 + 원통 터널 1장(잡음 텍스처 3번). 위치 계산은 모두 정점 셰이더라 CPU 는 숫자 몇 개만.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i530
- 쓰면 좋을 때: 레벨 · 장면을 바꾸는 전환 연출 / 로딩 시간을 즐겁게 채울 때 / 우주선 출발 · 도착
- 쓰지 말 때: 자주 반복되는 짧은 전환 — 1초 안쪽이면 페이드가 낫다 / 멀미가 걱정되는 작은 아이 대상 — 시야각 변화를 줄인다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i530 견본 항목 — `src/demos/demosSpace.ts:2247`
```ts
  i530: {
    kind: '3d',
    caption: '워프 — 별이 속도만큼 늘어난 빛줄이 되고(화면 공간 띠), 시야각이 넓어지며 푸른 초공간 터널이 열렸다가 번쩍 빠져나온다',
    make(): Scene3D {
      const bin = new Bin();
      const scene = new THREE.Scene();
      scene.background = new THREE.Color(0x000004);
      const cam = new THREE.PerspectiveCamera(60, 1.6, 0.1, 1000);
      const N = 2600;
      const geo = new THREE.InstancedBufferGeometry();
      geo.setAttribute('position', new THREE.Float32BufferAttribute([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 3));
      geo.setAttribute('aCorner', new THREE.Float32BufferAttribute([0, -1, 0, 1, 1, -1, 1, 1], 2));
      geo.setIndex([0, 2, 1, 1, 2, 3]);
      const st = new Float32Array(N * 4);
      const r = rng(77);
      for (let i = 0; i < N; i++) {
        st[i * 4] = r() * TAU;
        st[i * 4 + 1] = 0.6 + Math.pow(r(), 0.7) * 26;
        st[i * 4 + 2] = r() * 300;
        st[i * 4 + 3] = Math.pow(r(), 3);
      }
      geo.setAttribute('aStar', new THREE.InstancedBufferAttribute(st, 4));
      geo.instanceCount = N;
      const U = { uDist: { value: 0 }, uLen: { value: 0 }, uScale: { value: 1 }, uAspect: { value: 1.6 }, uDepth: { value: 300 }, uResY: { value: 900 }, uWarp: { value: 0 } };
      const streaks = new THREE.Mesh(geo, new THREE.ShaderMaterial({ uniforms: U, vertexShader: WARP_VS, fragmentShader: WARP_FS, transparent: true, depthWrite: false, depthTest: false, blending: THREE.AdditiveBlending }));
      streaks.frustumCulled = false;
      streaks.renderOrder = 2;
      scene.add(streaks);
      const UT = { uN2: { value: noise2D() }, uDist: { value: 0 }, uWarp: { value: 0 }, uTun: { value: 1 } };
      const tg = new THREE.CylinderGeometry(9, 9, 340, 72, 1, true);
      tg.rotateX(Math.PI / 2);
      const tunnel = new THREE.Mesh(tg, new THREE.ShaderMaterial({ uniforms: UT, vertexShader: TUNNEL_VS, fragmentShader: TUNNEL_FS, side: THREE.BackSide, transparent: true, depthWrite: false, blending: THREE.AdditiveBlending }));
      tunnel.position.z = -170;
      tunnel.renderOrder = 1;
      scene.add(tunnel);
      const gt = bin.add(texOf(glowCanvas()));
      const eye = glowSprite(gt, 0x9fc8ff, 1, 30);
      eye.position.z = -240;
      scene.add(eye);
      const flash = new THREE.Mesh(new THREE.PlaneGeometry(4, 4), new THREE.MeshBasicMaterial({ color: 0xdfeaff, transparent: true, opacity: 0, blending: THREE.AdditiveBlending, depthTest: false, depthWrite: false }));
      flash.position.z = -0.5;
      flash.renderOrder = 5;
      cam.add(flash);
      scene.add(cam);
      bin.scene(scene);
      let auto = true;
      let manual = 0.5;
      let last = 0;
      return {
        scene,
        camera: cam,
        update(t) {
          const dt = Math.min(0.05, Math.max(0, t - last));
          last = t;
          let w: number;
          let fl = 0;
          if (auto) {
            const p = t % 12;
            if (p < 3) w = 0;
            else if (p < 5.2) w = Math.pow((p - 3) / 2.2, 2.2);
            else if (p < 9) w = 1;
            else if (p < 9.35) w = 1 - (p - 9) / 0.35;
            else w = 0;
            if (p >= 9 && p < 10.4) fl = Math.exp(-(p - 9) * 4) * 0.9;
            if (p >= 5 && p < 5.6) fl = Math.max(fl, (1 - Math.abs(p - 5.25) / 0.35) * 0.35);
          } else w = manual;
          const speed = lerp(3, 330, w * w);
          U.uDist.value += speed * dt;
          U.uLen.value = speed * 0.045;
          U.uWarp.value = w;
          UT.uDist.value = U.uDist.value;
          UT.uWarp.value = w;
          (eye.material as THREE.SpriteMaterial).opacity = 0.15 + w * 0.85;
          eye.scale.setScalar(20 + w * 40);
          (flash.material as THREE.MeshBasicMaterial).opacity = fl;
          const fov = lerp(60, 98, smooth(0, 1, w));
          if (Math.abs(cam.fov - fov) > 0.01) {
            cam.fov = fov;
            cam.updateProjectionMatrix();
          }
          const sh = w * w * 0.012;
          cam.position.set(Math.sin(t * 37) * sh, Math.cos(t * 41) * sh, 0);
          cam.rotation.z = Math.sin(t * 0.3) * 0.05 + w * Math.sin(t * 0.7) * 0.06;
        },
        resize: (w, h) => {
          U.uScale.value = h / 900;
          U.uAspect.value = w / h;
          U.uResY.value = h;
        },
        controls: [
          tog('저절로 (순항 → 워프 → 빠져나오기)', true, (v) => (auto = v)),
          range('워프 세기 (자동 끄면)', 0, 1, 0.01, 0.5, (v) => (manual = v)),
          range('초공간 터널', 0, 2, 0.05, 1, (v) => (UT.uTun.value = v)),
        ],
        dispose: () => {
          bin.dispose();
        },
      };
    },
  }
```

## 관련 기술
- 먼저 알면 좋은 기술: [인스턴싱 (InstancedMesh)](https://ai-techstudio.web.app/ai/t/u36.md) `u36` · [별 하늘 (실제 등급 · B-V 색 · 반짝임)](https://ai-techstudio.web.app/ai/t/i522.md) `i522`
- 다음에 해 볼 기술: [소행성대 인스턴싱 + 렌즈 플레어](https://ai-techstudio.web.app/ai/t/i531.md) `i531` · [시차 별층 (패럴랙스 스크롤 · 성간 먼지)](https://ai-techstudio.web.app/ai/t/i535.md) `i535`
