# AI 꾸러미 — 맞는 순간 색수차 맥박 — RGB
> 충격 때만 RGB 가 0.2초 갈라졌다 모임  
> 견본: https://ai-techstudio.web.app/#t/i203

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

## 주문서

### 만들어 줘: 맞는 순간 색수차 맥박 — RGB

#### 1. 목표
폭발에 「맞는 순간 색수차 맥박」 기술을 깔끔한 기본 느낌으로 넣어 줘. — 충격 때만 RGB 가 0.2초 갈라졌다 모임

#### 2. 핵심 기술 용어
- **RGB**

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

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

## 원리
- 충격 때만 RGB 가 0.2초 갈라졌다 모임


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

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

## 이 기술 정보
- id: `i203` · 분류: 이펙트 · 연출 › 손맛 · 주스 · 3D · 난이도 쉬움 · 폰 부담 보통 (폰 주의)
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i203
- 쓰면 좋을 때: 쓰면 좋을 곳 — 폭발 · 충돌

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### D203 — `src/demos/demosJuice.ts:3081`
```ts
const D203: Demo = {
  kind: '3d',
  caption: '두 블록이 쾅 부딪치는 순간에만 빨강 · 초록 · 파랑이 0.2초 갈라졌다 모인다 (늘 켜 두면 흐릿할 뿐)',
  make() {
    const scene = new THREE.Scene();
    scene.background = skyTex('#3a1f6e', '#0b0620');
    const cam = new THREE.PerspectiveCamera(40, 1.6, 0.1, 60);
    cam.position.set(0, 1.6, 6.5);
    cam.lookAt(0, 0.6, 0);
    scene.add(new THREE.HemisphereLight(0xd8c8ff, 0x201040, 1.5));
    const sun = new THREE.DirectionalLight(0xffffff, 2.4);
    sun.position.set(-3, 6, 5);
    scene.add(sun);
    const floor = new THREE.Mesh(new THREE.CircleGeometry(6, 48), new THREE.MeshStandardMaterial({ color: 0x2d1b5e, roughness: 0.85 }));
    floor.rotation.x = -Math.PI / 2;
    scene.add(floor);
    // 별
    const stars = new Sparks(80);
    const wc = new THREE.Color(0xffffff);
    for (let i = 0; i < 80; i++) stars.set(i, (hash(i, 1) - 0.5) * 30, 2 + hash(i, 2) * 10, -10 - hash(i, 3) * 10, wc, 0.06 + hash(i, 4) * 0.08, 0.5 + hash(i, 5) * 0.5);
    stars.flush();
    scene.add(stars.obj);
    const geo = roundBox(1, 1, 1, 0.17);
    const A = new THREE.Mesh(geo, new THREE.MeshStandardMaterial({ map: faceTex('2', '#ff6b8a'), roughness: 0.4 }));
    const B = new THREE.Mesh(geo, new THREE.MeshStandardMaterial({ map: faceTex('5', '#4dabf7'), roughness: 0.4 }));
    scene.add(A, B);
    const sp = new Sparks(70);
    scene.add(sp.obj);
    const flashSpr = new THREE.Sprite(new THREE.SpriteMaterial({ map: dotTex(), color: 0xfff3c0, blending: THREE.AdditiveBlending, depthWrite: false, depthTest: false, toneMapped: false }));
    flashSpr.position.set(0, 0.6, 0.3);
    scene.add(flashSpr);
    // 후처리
    const rt = new THREE.WebGLRenderTarget(2, 2, { type: THREE.HalfFloatType });
    const qMat = new THREE.ShaderMaterial({
      uniforms: { tDiffuse: { value: rt.texture }, uAmt: { value: 0 }, uSplit: { value: 1 }, uRes: { value: new THREE.Vector2(1, 1) } },
      vertexShader: `varying vec2 vUv; void main(){ vUv=uv; gl_Position=vec4(position.xy,0.0,1.0); }`,
      fragmentShader: `uniform sampler2D tDiffuse; uniform float uAmt; uniform float uSplit; uniform vec2 uRes; varying vec2 vUv;
        void main(){
          float amt=uAmt; vec2 c=vec2(0.5); float lo=0.0; float hi=1.0;
          if(uSplit>0.5){ if(vUv.x<0.5){ amt=0.0; c.x=0.25; hi=0.5-1.0/uRes.x; } else { c.x=0.75; lo=0.5+1.0/uRes.x; } }
          vec2 d=vUv-c; d.x*=uSplit>0.5?2.0:1.0; vec2 off=d*amt*vec2(uSplit>0.5?0.5:1.0,1.0);
          vec2 ur=vUv+off; ur.x=clamp(ur.x,lo,hi); vec2 ub=vUv-off; ub.x=clamp(ub.x,lo,hi);
          vec3 col=vec3(texture2D(tDiffuse,ur).r, texture2D(tDiffuse,vUv).g, texture2D(tDiffuse,ub).b);
          gl_FragColor=vec4(col,1.0);
          #include <tonemapping_fragment>
          #include <colorspace_fragment>
          if(uSplit>0.5 && abs(vUv.x-0.5)<1.2/uRes.x) gl_FragColor=vec4(1.0);
        }`,
      depthTest: false,
      depthWrite: false,
    });
    const quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), qMat);
    const qScene = new THREE.Scene();
    qScene.add(quad);
    const qCam = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
    const hud = new Hud();
    hud.splitLabels();
    let compare = true;
    let power = 1;
    let amt = 0;
    const P = 2.2;
    const HIT = 0.8;
    const cc = new THREE.Color();
    return {
      scene,
      camera: cam,
      update(t) {
        const ph = t % P;
        const a = ph - HIT;
        if (a < 0) {
          const k = clamp01(ph / HIT);
          const x = lerp(2.3, 0.5, k * k);
          A.position.set(-x, 0.5, 0);
          B.position.set(x, 0.5, 0);
          A.rotation.set(0, 0.3, 0);
          B.rotation.set(0, -0.3, 0);
          A.scale.setScalar(1);
          B.scale.setScalar(1);
        } else {
          const d = 0.5 + 1.0 * (1 - Math.exp(-a * 2.5));
          const hop = Math.abs(Math.sin(a * 5)) * 0.6 * Math.exp(-a * 2);
          A.position.set(-d, 0.5 + hop, 0);
          B.position.set(d, 0.5 + hop, 0);
          A.rotation.set(0, 0.3 - a * 1.5 * Math.exp(-a), a * 2 * Math.exp(-a * 1.5));
          B.rotation.set(0, -0.3 + a * 1.5 * Math.exp(-a), -a * 2 * Math.exp(-a * 1.5));
          const sq = 0.18 * Math.exp(-a * 10) * Math.cos(a * 30);
          A.scale.set(1 - sq, 1 + sq, 1);
          B.scale.set(1 - sq, 1 + sq, 1);
        }
        amt = a >= 0 ? 0.13 * power * Math.pow(Math.max(0, 1 - a / 0.25), 2) : 0;
        const fl = a >= 0 ? Math.exp(-a * 9) : 0;
        flashSpr.scale.setScalar(0.5 + fl * 2.4);
        flashSpr.material.opacity = fl;
        for (let i = 0; i < 70; i++) {
          if (a < 0 || a > 0.9) {
            sp.set(i, 0, 0, 0, cc, 0, 0);
            continue;
          }
          const th = hash(i, 11) * TAU;
          const ph2 = (hash(i, 12) - 0.3) * 1.6;
          const v = 2 + hash(i, 13) * 3;
          const dd = v * (1 - Math.exp(-a * 3)) * 0.8;
          cc.setHSL(0.1 + hash(i, 14) * 0.06, 1, 0.65);
          sp.set(i, Math.cos(th) * Math.cos(ph2) * dd, 0.6 + Math.sin(ph2) * dd - a * a * 2, Math.sin(th) * Math.cos(ph2) * dd * 0.6, cc, 0.12 * (1 - a / 0.9), 1 - a / 0.9);
        }
        sp.flush();
      },
      resize(_w, h) {
        sp.fit(h * (compare ? 1 / 0.8 : 1), cam.fov);
        stars.fit(h, cam.fov);
      },
      render(r, w, h) {
        if (rt.width !== w || rt.height !== h) rt.setSize(w, h);
        qMat.uniforms.uRes!.value.set(w, h);
        qMat.uniforms.uAmt!.value = amt;
        qMat.uniforms.uSplit!.value = compare ? 1 : 0;
        if (compare) {
          const a0 = cam.aspect;
          cam.aspect = w / 2 / h;
          cam.zoom = 0.74;
          cam.updateProjectionMatrix();
          for (let s = 0; s < 2; s++) {
            rt.viewport.set((s * w) / 2, 0, w / 2, h);
            rt.scissor.set((s * w) / 2, 0, w / 2, h);
            rt.scissorTest = true;
            r.setRenderTarget(rt);
            r.render(scene, cam);
          }
          rt.scissorTest = false;
          rt.viewport.set(0, 0, w, h);
          rt.scissor.set(0, 0, w, h);
          cam.aspect = a0;
          cam.zoom = 1;
          cam.updateProjectionMatrix();
        } else {
          r.setRenderTarget(rt);
          r.render(scene, cam);
        }
        r.setRenderTarget(null);
        r.render(qScene, qCam);
        hud.render(r, w, h, compare, false);
      },
      controls: [tgl('나란히 비교 (끔 | 켬)', compare, (v) => (compare = v)), rng('갈라짐 세기', 0, 3, 0.1, power, (v) => (power = v))],
      dispose() {
        disposeScene(scene);
        stars.dispose();
        sp.dispose();
        rt.dispose();
        qMat.dispose();
        quad.geometry.dispose();
        hud.dispose();
      },
    };
  },
};
```

## 관련 기술
- 이 효과를 만든 기술: [색 보정 LUT · 색수차 · 필름 결](https://ai-techstudio.web.app/ai/t/i07.md) `i07`
