# AI 꾸러미 — 셰이더 전환 (잡음 녹기 · 도트 녹기) — Shader transition (gl-transitions style)
> 두 장면 그림을 화면 셰이더 하나에서 진행도 p 로 갈아 끼워, 잡음 녹기(불탄 테) · 도트 녹기 · 시계 쓸기 · 동그라미 열기로 장면을 넘긴다.  
> 견본: https://ai-techstudio.web.app/#t/i243

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

## 주문서

### 만들어 줘: 셰이더 전환 (잡음 녹기 · 도트 녹기) — Shader transition (gl-transitions style)

#### 1. 목표
단계(레벨) 넘김에 셰이더 장면 전환을 넣어 줘 — 진행도 하나로 잡음 녹기 · 도트 녹기 · 시계 쓸기 · 동그라미 열기 중 고르게. 분위기는 불타는 마법.

#### 2. 핵심 기술 용어
- **Shader transition (gl-transitions style)** — 셰이더 장면 전환
- **Noise dissolve · pixelate · clock wipe · iris** — 잡음 녹기 · 도트 녹기 · 시계 쓸기 · 동그라미 열기
- **Progress uniform (0 → 1)** — 진행도 하나로 전환 전체를 조절

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

#### 4. 조건
- 전환은 진행도 uniform 하나로 (0 → 1, smoothstep 으로 부드럽게)
- 두 장면 그림 비율이 화면과 달라도 늘어나지 않게 cover 맞춤 (uRes 로 계산)
- 방식 번호(uMode)로 한 셰이더에서 네 방식 — 셰이더를 여러 개 만들지 않기
- 전환 셰이더는 NoToneMapping 으로 그려 장면 그림 색이 바뀌지 않게
- 「다음 장면으로」 · 「방식 돌아가며」 · 「가장자리 빛」 조절

#### 5. 완성 기준 (이게 보이면 성공)
- 세 장면이 2.9초마다 넘어가며, 방식이 잡음 녹기 → 도트 녹기 → 시계 쓸기 → 동그라미 열기로 돌아간다
- 잡음 녹기는 경계가 불탄 종이처럼 주황 · 금빛으로 빛난다
- 화면 비율을 바꿔도 장면 그림이 찌그러지지 않는다

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

## 원리
- 이전 장면 A · 다음 장면 B 그림을 같은 셰이더에 넘기고, 진행도 p 를 1.5초 동안 0 → 1 (부드러운 시작 · 끝)로 올린다.
- 잡음 녹기: fbm 잡음 값 n 과 문턱 e = p × 1.3 − 0.15 를 비교해 A/B 를 고르고, 문턱 바로 위 띠는 주황 · 금빛으로 태운다.
- 도트 녹기: 칸 수를 400 → 14 → 400 (sin(p·π))으로 줄였다 늘리며, 칸마다 난수가 p 를 넘으면 B 로.
- 시계 쓸기: 화면 가운데 기준 각도(0~1)가 p 보다 작으면 B. 동그라미 열기: 가운데 거리가 p × 1.05 보다 작으면 B.
- 끝나면 A 를 B 로 바꾸고 p 를 0 으로 — 다음 전환 준비.

## 핵심 코드 — 네 가지 전환을 셰이더 하나로
(발췌: demos/demosLook2.ts makeTransition() 의 셰이더 (NOISE 의 hash · fbm 사용))
```glsl
uniform sampler2D tA, tB; uniform float uP, uMode, uGlow; uniform vec2 uRes;
varying vec2 vUv;
// hash(vec2) · fbm(vec2) 는 공용 잡음 조각에서
vec2 cover(vec2 uv){ float sa = uRes.x / uRes.y; if (sa > 1.6) uv.y = (uv.y - 0.5) * 1.6 / sa + 0.5; else uv.x = (uv.x - 0.5) * sa / 1.6 + 0.5; return uv; }
void main(){
  vec2 uv = cover(vUv);
  float p = uP;
  vec3 a = texture2D(tA, uv).rgb, b = texture2D(tB, uv).rgb, col;
  if (uMode < 0.5){                                       // 잡음 녹기
    float n = fbm(uv * vec2(5.0, 3.2));
    float e = p * 1.3 - 0.15;
    col = mix(b, a, smoothstep(e - 0.01, e + 0.01, n));
    float band = smoothstep(e - 0.08, e, n) * (1.0 - smoothstep(e, e + 0.015, n));
    col += vec3(1.0, 0.45, 0.1) * band * 2.2 * uGlow;
    col += vec3(1.0, 0.9, 0.5) * (1.0 - smoothstep(0.0, 0.02, abs(n - e))) * uGlow;
  } else if (uMode < 1.5){                                // 도트 녹기
    float s = sin(p * 3.14159265);
    float cells = mix(400.0, 14.0, s);
    vec2 cuv = vec2(cells * 1.6, cells);
    vec2 pu = (floor(uv * cuv) + 0.5) / cuv;
    float pick = s > 0.15 ? step(hash(floor(uv * cuv) * 1.37), p * 1.2 - 0.1) : step(0.5, p);
    col = mix(texture2D(tA, pu).rgb, texture2D(tB, pu).rgb, pick);
  } else if (uMode < 2.5){                                // 시계 쓸기
    vec2 d = (vUv - 0.5) * vec2(uRes.x / uRes.y, 1.0);
    float ang = fract(atan(d.x, d.y) / 6.2831853 + 0.5);
    col = mix(b, a, smoothstep(p - 0.004, p + 0.004, ang));
  } else {                                                // 동그라미 열기
    vec2 d = (vUv - 0.5) * vec2(uRes.x / uRes.y, 1.0);
    col = mix(b, a, smoothstep(p * 1.05 - 0.004, p * 1.05 + 0.004, length(d)));
  }
  gl_FragColor = vec4(col, 1.0);
}
// JS: p = smoothstep(0, 1.5초, 경과) — 1 이 되면 tA = tB, p = 0
```

## 흔한 실수 · 확인 목록
- [ ] **다음 장면을 전환 도중에 처음 그리면 첫 프레임이 멈칫한다** — 전환 전에 다음 장면을 렌더 타깃에 미리 그리고(셰이더도 미리 데우기 i385) 전환을 시작한다.
- [ ] **화면 전체를 다시 그리는 전환을 탭 · 쪽 넘김에 쓰면 「새로고침 같다」는 말을 듣는다** — 사용자 지적 — 탭 · 쪽 넘김은 바뀌는 부분만 갈아 끼운다. 전환 효과는 단계 · 월드가 바뀔 때만.
- [ ] **그림 비율이 다르면 전환 중에 장면이 늘어난다** — cover() 처럼 화면 비율로 좌표를 맞춘다.

## 완성 기준 체크리스트
- [ ] 세 장면이 2.9초마다 넘어가며, 방식이 잡음 녹기 → 도트 녹기 → 시계 쓸기 → 동그라미 열기로 돌아간다
- [ ] 잡음 녹기는 경계가 불탄 종이처럼 주황 · 금빛으로 빛난다
- [ ] 화면 비율을 바꿔도 장면 그림이 찌그러지지 않는다

## 이 기술 정보
- id: `i243` · 분류: 그래픽 · 셰이더 › 후처리 · 공통 · 난이도 쉬움 · 폰 부담 가벼움 (폰 OK) — 화면 덮는 사각형 하나에 텍스처 2번 + 잡음 몇 번. 전환하는 1.5초 동안만.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i243
- 쓰면 좋을 때: 단계 넘김 · 월드 이동처럼 장면이 통째로 바뀔 때 / 로딩 중 다음 장면을 미리 그려 둘 수 있을 때
- 쓰지 말 때: 짧은 정보 갱신(점수 · 칸 바꾸기) — 바뀌는 부분만 바꾼다 (화면 전체 다시 그리기 금지) / 다음 장면을 그리는 데 오래 걸릴 때 — 전환 전에 미리 그려 두지 않으면 멈칫한다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### makeTransition — `src/demos/demosLook2.ts:4306`
```ts
function makeTransition(): Scene3D {
  const scenes = [0, 1, 2].map(paintScene);
  const uni = {
    tA: { value: scenes[0] as THREE.Texture },
    tB: { value: scenes[1] as THREE.Texture },
    uP: { value: 0 },
    uMode: { value: 0 },
    uRes: { value: new THREE.Vector2(1, 1) },
    uGlow: { value: 1 },
  };
  const q = quad(
    /* glsl */ `
    uniform sampler2D tA, tB; uniform float uP, uMode, uGlow; uniform vec2 uRes;
    varying vec2 vUv;
    ${NOISE}
    vec2 cover(vec2 uv){ float sa = uRes.x / uRes.y; if (sa > 1.6) uv.y = (uv.y - 0.5) * 1.6 / sa + 0.5; else uv.x = (uv.x - 0.5) * sa / 1.6 + 0.5; return uv; }
    void main(){
      vec2 uv = cover(vUv);
      float p = uP;
      vec3 col;
      vec3 a = texture2D(tA, uv).rgb, b = texture2D(tB, uv).rgb;
      if (uMode < 0.5){
        float n = fbm(uv * vec2(5.0, 3.2));
        float e = p * 1.3 - 0.15;
        float m = smoothstep(e - 0.01, e + 0.01, n);
        col = mix(b, a, m);
        float band = smoothstep(e - 0.08, e, n) * (1.0 - smoothstep(e, e + 0.015, n));
        col += vec3(1.0, 0.45, 0.1) * band * 2.2 * uGlow;
        col += vec3(1.0, 0.9, 0.5) * (1.0 - smoothstep(0.0, 0.02, abs(n - e))) * uGlow;
      } else if (uMode < 1.5){
        float s = sin(p * 3.14159265);
        float cells = mix(400.0, 14.0, s);
        vec2 cuv = vec2(cells * 1.6, cells);
        vec2 pu = (floor(uv * cuv) + 0.5) / cuv;
        float pick = step(hash(floor(uv * cuv) * 1.37), p * 1.2 - 0.1);
        pick = s > 0.15 ? pick : step(0.5, p);
        col = mix(texture2D(tA, pu).rgb, texture2D(tB, pu).rgb, pick);
      } else if (uMode < 2.5){
        vec2 d = (vUv - 0.5) * vec2(uRes.x / uRes.y, 1.0);
        float ang = fract(atan(d.x, d.y) / 6.2831853 + 0.5);
        float m = smoothstep(p - 0.004, p + 0.004, ang);
        col = mix(b, a, m);
        col += vec3(1.0, 0.95, 0.7) * (1.0 - smoothstep(0.0, 0.012, abs(ang - p))) * step(0.001, p) * step(p, 0.999) * uGlow;
      } else {
        vec2 d = (vUv - 0.5) * vec2(uRes.x / uRes.y, 1.0);
        float rr = length(d);
        float R = p * 1.05;
        float m = smoothstep(R - 0.004, R + 0.004, rr);
        col = mix(b, a, m);
        col += vec3(1.0) * (1.0 - smoothstep(0.0, 0.01, abs(rr - R))) * step(0.01, p) * step(p, 0.99) * uGlow;
      }
      gl_FragColor = vec4(col, 1.0);
      ${OUT}
    }`,
    uni,
  );
  const NAMES = ['잡음 녹기 (불탄 가장자리)', '도트 녹기 (모자이크)', '시계 방향 쓸기', '동그라미 열기'];
  const hud = new Hud();
  const tag = hud.add(NAMES[0]!);
  hud.at(tag, 0.03, 0.05, 0, 0);
  let auto = true;
  let fixedMode = 0;
  let k = 0;
  let start = 0;
  let now = 0;
  const PER = 2.9;
  const DUR = 1.5;
  return {
    scene: q.scene,
    camera: q.cam,
    tone: THREE.NoToneMapping,
    update(t) {
      now = t;
      const local = t - start;
      if (local >= PER) {
        start = t;
        k++;
      }
      const p = smooth(0, DUR, t - start);
      uni.tA.value = scenes[k % 3]!;
      uni.tB.value = scenes[(k + 1) % 3]!;
      uni.uP.value = p;
      if (p >= 1) {
        uni.tA.value = scenes[(k + 1) % 3]!;
        uni.uP.value = 0;
      }
      uni.uMode.value = auto ? k % 4 : fixedMode;
      hud.set(tag, NAMES[uni.uMode.value]!);
    },
    render(r, w, h) {
      uni.uRes.value.set(w, h);
      r.render(q.scene, q.cam);
      hud.draw(r, w, h);
    },
    controls: [
      { type: 'button', label: '다음 장면으로', on: () => ((start = now - PER), void 0) },
      { type: 'toggle', label: '방식 돌아가며', value: true, on: (v) => (auto = v) },
      { type: 'range', label: '방식 (돌아가며 끄면)', min: 0, max: 3, step: 1, value: 0, on: (v) => ((fixedMode = v), (auto = false)) },
      { type: 'range', label: '가장자리 빛', min: 0, max: 2, step: 0.1, value: 1, on: (v) => (uni.uGlow.value = v) },
    ],
    dispose() {
      q.dispose();
      for (const s of scenes) s.dispose();
      hud.dispose();
    },
  };
}
```

### i243 견본 항목 — `src/demos/demosLook2.ts:4527`
```ts
  i243: { kind: '3d', caption: '두 장면을 셰이더 하나로 갈아 끼우기 — 잡음 녹기(불탄 테) · 도트 녹기 · 시계 쓸기 · 동그라미 열기', make: () => makeTransition() }
```

## 관련 기술
- 먼저 알면 좋은 기술: [디졸브 (잡음으로 사라짐)](https://ai-techstudio.web.app/ai/t/i12.md) `i12` · [후처리 사슬 (EffectComposer → OutputPass)](https://ai-techstudio.web.app/ai/t/u21.md) `u21`
- 다음에 해 볼 기술: [셰이더 미리 데우기](https://ai-techstudio.web.app/ai/t/i385.md) `i385`
- 참고 문서: [gl-transitions (전환 셰이더 모음)](https://gl-transitions.com/)
