# AI 꾸러미 — 데이터 분해 · 전송 (복셀 디졸브)
> 칸 단위 해시로 자른 경계가 위에서 아래로 내려가며 몸이 사라지고, 경계에서 작은 정육면체가 계단식으로 떠올라 흩어짐 → 반대로 날아와 아래부터 다시 조립  
> 견본: https://ai-techstudio.web.app/#t/i507

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

## 주문서

### 만들어 줘: 데이터 분해 · 전송 (복셀 디졸브)

#### 1. 목표
순간 이동에 「데이터 분해 · 전송 (복셀 디졸브)」 기술을 깔끔한 기본 느낌으로 넣어 줘. — 칸 단위 해시로 자른 경계가 위에서 아래로 내려가며 몸이 사라지고, 경계에서 작은 정육면체가 계단식으로 떠올라 흩어짐 → 반대로 날아와 아래부터 다시 조립

#### 2. 핵심 기술 용어
- 데이터 분해 · 전송 (복셀 디졸브) — 칸 단위 해시로 자른 경계가 위에서 아래로 내려가며 몸이 사라지고, 경계에서 작은 정육면체가 계단식으로 떠올라 흩어짐 → 반대로 날아와 아래부터 다시 조립

#### 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. 완성 기준 (이게 보이면 성공)
- 견본처럼 「칸 단위 해시로 자른 경계가 위에서 아래로 내려가며 몸이 사라지고, 경계에서 작은 정육면체가 계단식으로 떠올라 흩어짐 → 반대로 날아와 아래부터 다시 조립」 이(가) 첫 화면에서 바로 보인다
- 켬/끔(또는 전/후) 비교 단추로 이 기술이 만드는 차이를 확인할 수 있다
- 중요한 값 1~3개를 슬라이더로 바꿔 볼 수 있다
- 콘솔 오류 0, 폰 폭(390px)에서도 동작한다

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

## 원리
- 칸 단위 해시로 자른 경계가 위에서 아래로 내려가며 몸이 사라지고, 경계에서 작은 정육면체가 계단식으로 떠올라 흩어짐 → 반대로 날아와 아래부터 다시 조립


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

## 완성 기준 체크리스트
- [ ] 견본처럼 「칸 단위 해시로 자른 경계가 위에서 아래로 내려가며 몸이 사라지고, 경계에서 작은 정육면체가 계단식으로 떠올라 흩어짐 → 반대로 날아와 아래부터 다시 조립」 이(가) 첫 화면에서 바로 보인다
- [ ] 켬/끔(또는 전/후) 비교 단추로 이 기술이 만드는 차이를 확인할 수 있다
- [ ] 중요한 값 1~3개를 슬라이더로 바꿔 볼 수 있다
- [ ] 콘솔 오류 0, 폰 폭(390px)에서도 동작한다

## 이 기술 정보
- id: `i507` · 분류: 이펙트 · 연출 › 화려한 효과 (VFX) · 3D · 난이도 보통 · 폰 부담 보통 (폰 주의)
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i507
- 쓰면 좋을 때: 쓰면 좋을 곳 — 순간 이동 · 저장/불러오기 · 디지털 세계

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i507 견본 항목 — `src/demos/demosVfx3.ts:2018`
```ts
  i507: {
    kind: '3d',
    caption: '칸 단위로 잘린 경계가 위에서 아래로 내려가며 몸이 정육면체 조각으로 떠올라 흩어지고, 다시 날아와 아래부터 조립된다',
    make(): Scene3D {
      const S = makeStage({ bg: 0x04060b, cam: [0.3, 1.55, 4.3], look: [0, 0.95, 0], fov: 40, fog: [7, 16] });
      const lab = S.hud.label('tl');
      const CYAN = new THREE.Color(0x30e0ff);
      const ORANGE = new THREE.Color(0xff8a20);
      const col = CYAN.clone();
      const floorMat = new THREE.ShaderMaterial({
        uniforms: { uCol: { value: col }, uTime: { value: 0 } },
        vertexShader: WORLD_VS,
        fragmentShader: /* glsl */ `uniform vec3 uCol; uniform float uTime; varying vec3 vW;
          void main(){ vec2 p = vW.xz; vec2 g = abs(fract(p * 2.0) - 0.5); float line = 1.0 - smoothstep(0.0, 0.025, min(g.x, g.y));
            vec2 g2 = abs(fract(p * 0.5) - 0.5); float line2 = 1.0 - smoothstep(0.0, 0.012, min(g2.x, g2.y));
            float d = length(p); float fade = 1.0 - smoothstep(2.0, 9.0, d);
            float pulse = exp(-pow(d - mod(uTime * 1.5, 9.0), 2.0) * 3.0);
            vec3 col = vec3(0.015, 0.02, 0.03) + uCol * (line * 0.12 + line2 * 0.25) * fade * (1.0 + pulse * 2.0);
            gl_FragColor = vec4(col, 1.0); ${TONE} }`,
      });
      const floor = new THREE.Mesh(new THREE.PlaneGeometry(30, 30), floorMat);
      floor.rotation.x = -Math.PI / 2;
      S.scene.add(floor);
      const padM = new THREE.Mesh(new THREE.CylinderGeometry(0.62, 0.68, 0.06, 48), new THREE.MeshStandardMaterial({ color: 0x1a1e26, roughness: 0.35, metalness: 0.8 }));
      padM.position.y = 0.03;
      const ringMat = new THREE.MeshBasicMaterial({ color: col.clone().multiplyScalar(3) });
      const padRing = new THREE.Mesh(new THREE.TorusGeometry(0.6, 0.012, 8, 64), ringMat);
      padRing.rotation.x = Math.PI / 2;
      padRing.position.y = 0.065;
      S.scene.add(padM, padRing, new THREE.HemisphereLight(0x8090b0, 0x101010, 0.8));
      const geo = figureGeo('stand');
      const figMat = new THREE.ShaderMaterial({
        uniforms: { uCut: { value: 2 }, uCol: { value: col }, uTime: { value: 0 }, uCell: { value: 14 } },
        vertexShader: WORLD_VS,
        fragmentShader: /* glsl */ `uniform float uCut, uTime, uCell; uniform vec3 uCol; varying vec3 vW; varying vec3 vN;
          ${SHADE_GLSL}
          float h31(vec3 p){ return fract(sin(dot(p, vec3(127.1, 311.7, 74.7))) * 43758.5453); }
          void main(){ vec3 cell = floor(vW * uCell); float h = h31(cell);
            float d = uCut + (h - 0.5) * 0.24 - vW.y;
            if (d < 0.0) discard;
            vec3 N = normalize(vN); vec3 V = normalize(cameraPosition - vW);
            vec3 base = vec3(0.34, 0.37, 0.43);
            float band = step(0.5, fract(vW.y * 6.0 + 0.25)) * 0.06;
            vec3 c = shade(base - band, N, V, normalize(vec3(-0.5, 0.8, 0.6)), vec3(1.0, 0.97, 0.92));
            vec3 Rf = reflect(-V, N); c += mix(vec3(0.02, 0.03, 0.05), vec3(0.25, 0.3, 0.38), smoothstep(-0.2, 0.8, Rf.y)) * 0.6;
            c += uCol * pow(1.0 - max(dot(N, V), 0.0), 3.0) * 0.5;
            float edge = 1.0 - smoothstep(0.0, 0.06, d);
            vec3 f = fract(vW * uCell); float grid = max(max(step(0.88, f.x), step(0.88, f.y)), step(0.88, f.z));
            float near = exp(-d * 9.0);
            c = mix(c, uCol * 0.6, near * 0.5);
            c += uCol * (edge * 5.0 + grid * near * 1.6) + uCol * (1.0 - smoothstep(0.0, 0.035, abs(fract(vW.y * 6.0) - 0.5))) * 0.7;
            gl_FragColor = vec4(c, 1.0); ${TONE} }`,
      });
      const fig = new THREE.Mesh(geo, figMat);
      fig.position.y = 0.06;
      S.scene.add(fig);
      const NS = 3200;
      const samp = surfacePoints(geo, NS);
      const idx = Array.from({ length: NS }, (_, i) => i).sort((a, b) => samp[b * 6 + 1]! - samp[a * 6 + 1]!);
      // 훑는 고리 · 판
      const scanMat = new THREE.ShaderMaterial({
        uniforms: { uCol: { value: col }, uA: { value: 0 } },
        vertexShader: FLAT_VS,
        fragmentShader: /* glsl */ `uniform vec3 uCol; uniform float uA; varying vec2 vUv;
          void main(){ vec2 p = vUv * 2.0 - 1.0; float r = length(p);
            vec2 g = abs(fract(p * 6.0) - 0.5); float grid = 1.0 - smoothstep(0.0, 0.05, min(g.x, g.y));
            float ring = exp(-pow((r - 0.92) * 30.0, 2.0));
            float a = (ring * 1.2 + grid * 0.18 * (1.0 - r)) * (1.0 - smoothstep(0.95, 1.0, r)) * uA;
            gl_FragColor = vec4(uCol * 3.0, 1.0); ${TONE} gl_FragColor.a = a; }`,
        transparent: true,
        depthWrite: false,
        blending: THREE.AdditiveBlending,
        side: THREE.DoubleSide,
      });
      const scan = new THREE.Mesh(new THREE.PlaneGeometry(1.3, 1.3), scanMat);
      scan.rotation.x = -Math.PI / 2;
      S.scene.add(scan);
      // 조각
      const MAXV = 1600;
      const vox = new THREE.InstancedMesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshBasicMaterial({ color: 0xffffff }), MAXV);
      vox.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
      vox.frustumCulled = false;
      for (let i = 0; i < MAXV; i++) vox.setColorAt(i, new THREE.Color(1, 1, 1));
      S.scene.add(vox);
      interface Vx {
        x: number;
        y: number;
        z: number;
        vx: number;
        vy: number;
        vz: number;
        age: number;
        life: number;
        s: number;
        mode: number;
        tx: number;
        ty: number;
        tz: number;
        sx: number;
        sy: number;
        sz: number;
      }
      const V: Vx[] = [];
      const glow = S.pool(600, 5);
      const SPARK = ramp([[0, 0xffffff, 6, 1, 1], [1, 0x30e0ff, 2, 0, 1]]);
      let speed = 1;
      let vsz = 1;
      let orange = false;
      let ptrD = 0;
      let ptrA = NS - 1;
      let cyc = 0;
      const CY = 8;
      const M = new THREE.Matrix4();
      const tc = new THREE.Color();
      const controls: Control[] = [
        ctrlRange('속도', 0.4, 1.6, 0.05, 1, (v) => (speed = v)),
        ctrlRange('조각 크기', 0.6, 1.8, 0.05, 1, (v) => (vsz = v)),
        ctrlTog('주황 (다른 색)', false, (v) => {
          orange = v;
          col.copy(v ? ORANGE : CYAN);
          ringMat.color.copy(col).multiplyScalar(3);
        }),
      ];
      return {
        scene: S.scene,
        camera: S.cam,
        controls,
        update(t, dt0) {
          const dt = Math.min(dt0, 0.05) * speed;
          const prev = cyc;
          cyc = (cyc + dt) % CY;
          if (cyc < prev) {
            ptrD = 0;
            ptrA = NS - 1;
          }
          figMat.uniforms.uTime!.value = t;
          floorMat.uniforms.uTime!.value = t;
          figMat.uniforms.uCell!.value = 14 / vsz;
          const TOP = 1.95;
          const BOT = -0.05;
          let cut = TOP;
          let mode = 0;
          if (cyc < 1) {
            cut = TOP;
            lab.set('대기', orange ? '#ff9a40' : '#40e0ff');
          } else if (cyc < 3.2) {
            cut = lerp(TOP, BOT, smooth((cyc - 1) / 2.2));
            mode = 1;
            lab.set('분해 — 경계가 내려가며 조각으로 떠오름', orange ? '#ff9a40' : '#40e0ff');
          } else if (cyc < 4.6) {
            cut = BOT;
            mode = 2;
            lab.set('전송 중 — 데이터 기둥', orange ? '#ff9a40' : '#40e0ff');
          } else if (cyc < 6.8) {
            cut = lerp(BOT, TOP, smooth((cyc - 4.6) / 2.2));
            mode = 3;
            lab.set('조립 — 조각이 날아와 아래부터 맞춰짐', orange ? '#ff9a40' : '#40e0ff');
          } else lab.set('완료', orange ? '#ff9a40' : '#40e0ff');
          figMat.uniforms.uCut!.value = cut;
          const wy = cut - 0.06;
          const sc = mode === 1 || mode === 3;
          scan.visible = sc;
          scan.position.y = cut + 0.06;
          scanMat.uniforms.uA!.value = sc ? 1 : 0;
          const cell = 0.072 * vsz;
          if (mode === 1) {
            while (ptrD < NS) {
              const i = idx[ptrD]!;
              if (samp[i * 6 + 1]! < wy + 0.1) break;
              ptrD++;
              if (Math.random() < 0.55 && V.length < MAXV) {
                const nx = samp[i * 6 + 3]!;
                const nz = samp[i * 6 + 5]!;
                V.push({ x: samp[i * 6]!, y: samp[i * 6 + 1]! + 0.06, z: samp[i * 6 + 2]!, vx: nx * R(0.15, 0.5) + R(-0.1, 0.1), vy: R(0.5, 1.3), vz: nz * R(0.15, 0.5) + R(-0.1, 0.1), age: 0, life: R(0.9, 1.7), s: cell * R(0.45, 1), mode: 0, tx: 0, ty: 0, tz: 0, sx: 0, sy: 0, sz: 0 });
                if (Math.random() < 0.08) glow.spawn({ x: samp[i * 6]!, y: samp[i * 6 + 1]! + 0.06, z: samp[i * 6 + 2]!, life: 0.3, s0: 0.1, ramp: SPARK, shape: 4 });
              }
            }
          } else if (mode === 2) {
            for (let k = 0; k < 6; k++) {
              if (V.length >= MAXV) break;
              const a = R(0, TAU);
              const rr = Math.sqrt(Math.random()) * 0.45;
              V.push({ x: Math.cos(a) * rr, y: 0.1, z: Math.sin(a) * rr, vx: 0, vy: R(2.5, 4.5), vz: 0, age: 0, life: R(0.5, 0.9), s: cell * R(0.3, 0.6), mode: 0, tx: 0, ty: 0, tz: 0, sx: 0, sy: 0, sz: 0 });
            }
          } else if (mode === 3) {
            while (ptrA >= 0) {
              const i = idx[ptrA]!;
              if (samp[i * 6 + 1]! > wy + 0.32) break;
              ptrA--;
              if (Math.random() < 0.5 && V.length < MAXV) {
                const tx = samp[i * 6]!;
                const ty = samp[i * 6 + 1]! + 0.06;
                const tz = samp[i * 6 + 2]!;
                const sx = tx + samp[i * 6 + 3]! * R(0.5, 1.2) + R(-0.4, 0.4);
                const sy = ty + R(0.6, 1.4);
                const sz = tz + samp[i * 6 + 5]! * R(0.5, 1.2) + R(-0.4, 0.4);
                V.push({ x: sx, y: sy, z: sz, vx: 0, vy: 0, vz: 0, age: 0, life: R(0.4, 0.55), s: cell * R(0.45, 1), mode: 1, tx, ty, tz, sx, sy, sz });
              }
            }
          }
          let n = 0;
          for (let i = V.length - 1; i >= 0; i--) {
            const v = V[i]!;
            v.age += dt;
            const k = v.age / v.life;
            if (k >= 1) {
              if (v.mode === 1 && Math.random() < 0.15) glow.spawn({ x: v.tx, y: v.ty, z: v.tz, life: 0.25, s0: 0.12, ramp: SPARK, shape: 4 });
              V[i] = V[V.length - 1]!;
              V.pop();
              continue;
            }
            let x: number;
            let y: number;
            let z: number;
            let s: number;
            let bright: number;
            if (v.mode === 0) {
              v.x += v.vx * dt;
              v.y += v.vy * dt;
              v.z += v.vz * dt;
              if (Math.random() < dt * 4) v.x += (Math.random() < 0.5 ? -1 : 1) * cell;
              x = v.x;
              y = v.y;
              z = v.z;
              s = v.s * (1 - k * k);
              bright = 1 - k;
            } else {
              const e = k * k;
              x = lerp(v.sx, v.tx, e);
              y = lerp(v.sy, v.ty, e);
              z = lerp(v.sz, v.tz, e);
              s = v.s * (0.4 + 0.6 * k);
              bright = 0.4 + k;
            }
            // 칸에 맞춰 계단식으로
            const g = cell * 0.5;
            x = Math.round(x / g) * g;
            y = Math.round(y / g) * g;
            z = Math.round(z / g) * g;
            M.makeScale(s, s, s).setPosition(x, y, z);
            vox.setMatrixAt(n, M);
            const fl = Math.random() < 0.06 ? 2.2 : 1;
            tc.copy(col).lerp(orange ? new THREE.Color(1, 0.2, 0.4) : new THREE.Color(0.8, 0.2, 1.0), v.mode === 0 ? k : 0).multiplyScalar((0.8 + bright * 2.4) * fl);
            vox.setColorAt(n, tc);
            n++;
          }
          vox.count = n;
          vox.instanceMatrix.needsUpdate = true;
          if (vox.instanceColor) vox.instanceColor.needsUpdate = true;
          ringMat.color.copy(col).multiplyScalar(2 + Math.sin(t * 6) * 0.8 + (mode === 2 ? 3 : 0));
          glow.tick(dt);
          S.tick(dt);
        },
        resize: (w, h) => S.resize(w, h),
        dispose: () => S.dispose(),
      };
    },
  }
```

## 관련 기술
- 이 효과를 만든 기술: [디졸브 (잡음으로 사라짐)](https://ai-techstudio.web.app/ai/t/i12.md) `i12`
