# AI 꾸러미 — 디졸브 (잡음으로 사라짐) — Dissolve shader (noise threshold + discard)
> 잡음 값이 진행도보다 작은 곳부터 조각을 버리고 경계를 금빛으로 빛내, 물체가 타들어 가듯 사라졌다 다시 나타나게 한다.  
> 견본: https://ai-techstudio.web.app/#t/i12

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

## 주문서

### 만들어 줘: 디졸브 (잡음으로 사라짐) — Dissolve shader (noise threshold + discard)

#### 1. 목표
정답 숫자 블록이 잡음 무늬를 따라 빛나는 테두리와 함께 사라졌다 나타나게 디졸브 셰이더를 만들어 줘. 분위기는 따뜻한 금빛 마법.

#### 2. 핵심 기술 용어
- **Dissolve shader (noise threshold + discard)** — 잡음 문턱으로 조각 버리기
- **Burn edge** — 불탄 가장자리 — 문턱 바로 위 띠를 빛나게
- **Object-space 3D noise (fbm)** — 물체 좌표 3D 잡음 — 물체와 함께 돈다

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

#### 4. 조건
- 잡음은 물체 좌표(position)로 — 물체가 움직여도 무늬가 미끄러지지 않게
- clipping_planes_fragment 자리에서 discard, 경계 빛은 emissivemap_fragment 뒤에 totalEmissiveRadiance
- 재질 side: THREE.DoubleSide — 버린 구멍 사이로 안쪽 면이 보이게
- 진행도는 블록마다 따로 (uProg uniform 은 재질마다), 테 폭 · 잡음 크기 · 빠르기는 공유
- 잡음은 반복문 없이 펼친 짧은 fbm 3겹 또는 미리 구운 텍스처

#### 5. 완성 기준 (이게 보이면 성공)
- 숫자 블록 셋이 차례로 잡음 모양 구멍이 번지며 금빛 테와 함께 사라졌다가 다시 채워진다
- 구멍 사이로 블록 안쪽 면이 보인다
- 「빛 테두리 폭」 0~0.2 · 「잡음 크기」 0.8~6 · 「빠르기」 슬라이더가 바로 반영된다

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

## 원리
- 물체의 점마다 3D 잡음 값 dn(0~1)을 정한다 — 물체 좌표(position)로 재서 물체가 돌아도 무늬가 붙어 있다.
- 진행도 uProg 보다 dn 이 작으면 그 조각을 버린다(discard). uProg 를 −0.15 → 1.05 로 올리면 다 사라진다.
- 문턱 바로 위(폭 uEdge 0.07) 띠는 금빛 색 · 스스로 빛남(2.4배)을 더해 불탄 테두리가 된다.
- 진행도를 거꾸로 내리면 같은 무늬로 다시 나타난다.

## 핵심 코드 — 표준 재질에 디졸브 끼워 넣기
(발췌: demos/demosMaterial.ts i12 make() 를 정리)
```ts
const uEdge = { value: 0.07 }, uScale = { value: 2.6 };
const uCol = { value: new THREE.Color(1.0, 0.62, 0.2) };
const NOISE =
  'varying vec3 vObjPos; uniform float uProg; uniform float uEdge; uniform float uScale; uniform vec3 uEdgeCol;\n' +
  'float h3(vec3 p){ p = fract(p * 0.3183099 + 0.1); p *= 17.0; return fract(p.x * p.y * p.z * (p.x + p.y + p.z)); }\n' +
  'float vn(vec3 x){ vec3 i = floor(x); vec3 f = fract(x); f = f * f * (3.0 - 2.0 * f);\n' +
  '  return mix(mix(mix(h3(i), h3(i + vec3(1,0,0)), f.x), mix(h3(i + vec3(0,1,0)), h3(i + vec3(1,1,0)), f.x), f.y),\n' +
  '             mix(mix(h3(i + vec3(0,0,1)), h3(i + vec3(1,0,1)), f.x), mix(h3(i + vec3(0,1,1)), h3(i + vec3(1,1,1)), f.x), f.y), f.z); }\n' +
  'float fbm(vec3 p){ return 0.55 * vn(p) + 0.3 * vn(p * 2.03) + 0.15 * vn(p * 4.1); }\n'; // 반복문 없이 3겹

function dissolvable(map: THREE.Texture) {
  const prog = { value: 0 };                       // 블록마다 따로
  const mat = new THREE.MeshStandardMaterial({ map, roughness: 0.45, side: THREE.DoubleSide });
  mat.onBeforeCompile = (sh) => {
    Object.assign(sh.uniforms, { uProg: prog, uEdge, uScale, uEdgeCol: uCol });
    sh.vertexShader = 'varying vec3 vObjPos;\n' +
      sh.vertexShader.replace('#include <begin_vertex>', '#include <begin_vertex>\n vObjPos = position;');
    sh.fragmentShader = NOISE + sh.fragmentShader
      .replace('#include <clipping_planes_fragment>',
        'float dn = fbm(vObjPos * uScale + 3.7);\n if (dn < uProg) discard;\n#include <clipping_planes_fragment>')
      .replace('#include <emissivemap_fragment>',
        '#include <emissivemap_fragment>\n' +
        'float de = 1.0 - smoothstep(0.0, uEdge, dn - uProg);\n' +
        'diffuseColor.rgb = mix(diffuseColor.rgb, uEdgeCol, de * 0.7);\n' +
        'totalEmissiveRadiance += uEdgeCol * de * 2.4;');
  };
  return { mat, prog };
}
// 사라지기: prog.value 를 -0.15 → 1.05, 나타나기: 반대로
```

## 흔한 실수 · 확인 목록
- [ ] **잡음을 화면 · 세계 좌표로 재면 물체가 움직일 때 무늬가 흐른다** — 정점 셰이더에서 position(물체 좌표)을 넘겨 쓴다.
- [ ] **진행도 0 에서도 점이 몇 개 뚫려 있다** — 잡음 최솟값이 0 근처라서다. 시작을 −0.15 처럼 0 아래로, 끝을 1.05 로 넘긴다.
- [ ] **그림자는 구멍 없이 그대로 남는다** — 그림자 깊이 재질은 따로라 discard 가 안 걸린다. 그림자도 뚫려야 하면 customDepthMaterial 에 같은 줄.
- [ ] **반복문 잡음을 여러 번 부르면 윈도 컴파일이 수십 초** — 보물 동굴에서 겪었다. 견본은 반복문 없이 3겹만 펼쳐 썼다. 더 필요하면 잡음 텍스처로.

## 완성 기준 체크리스트
- [ ] 숫자 블록 셋이 차례로 잡음 모양 구멍이 번지며 금빛 테와 함께 사라졌다가 다시 채워진다
- [ ] 구멍 사이로 블록 안쪽 면이 보인다
- [ ] 「빛 테두리 폭」 0~0.2 · 「잡음 크기」 0.8~6 · 「빠르기」 슬라이더가 바로 반영된다

## 이 기술 정보
- id: `i12` · 분류: 그래픽 · 셰이더 › 재질 · 그림체 · 3D · 난이도 보통 · 폰 부담 보통 (폰 주의) — 조각마다 값 잡음 3번(fbm). 블록 몇 개는 가볍지만 화면 가득한 물체는 잡음 텍스처로 바꾸는 편이 낫다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i12
- 쓰면 좋을 때: 정답 블록 사라짐 · 아이템 획득 · 소환처럼 「사라짐」 자체가 연출일 때 / 투명도 정렬 문제 없이 사라지게 하고 싶을 때 (discard 라 불투명 그대로)
- 쓰지 말 때: 아주 빠른 사라짐(0.1초 미만) — 무늬가 안 보인다. 대신 크기 줄이기 · 번쩍 / 폰에서 큰 화면 가득 — 조각마다 잡음 세 번이라 넓으면 무겁다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i12 견본 항목 — `src/demos/demosMaterial.ts:1243`
```ts
  i12: {
    kind: '3d',
    caption: '정답 블록이 잡음 무늬를 따라 금빛 테두리로 타들어 가듯 사라졌다 다시 나타나요',
    make() {
      const B = base({ bg: gradBg('#120c2e', '#2e2060', 'rgba(255,170,90,0.2)'), labels: ['사라지는 중'], fit: [2.4, 0.75], target: [0, 0, 0], dir: [0, 0.35, 1] });
      lights(B.scene, 0xd8d8ff, 0x302040, 0.7, 1.8);
      const uEdge = { value: 0.07 };
      const uScale = { value: 2.6 };
      const uCol = { value: new THREE.Color(1.0, 0.62, 0.2) };
      const NOISE = `varying vec3 vObjPos;
        uniform float uProg; uniform float uEdge; uniform float uScale; uniform vec3 uEdgeCol;
        float h3(vec3 p){ p = fract(p * 0.3183099 + 0.1); p *= 17.0; return fract(p.x * p.y * p.z * (p.x + p.y + p.z)); }
        float vn(vec3 x){ vec3 i = floor(x); vec3 f = fract(x); f = f * f * (3.0 - 2.0 * f);
          return mix(mix(mix(h3(i), h3(i + vec3(1.0,0.0,0.0)), f.x), mix(h3(i + vec3(0.0,1.0,0.0)), h3(i + vec3(1.0,1.0,0.0)), f.x), f.y),
                     mix(mix(h3(i + vec3(0.0,0.0,1.0)), h3(i + vec3(1.0,0.0,1.0)), f.x), mix(h3(i + vec3(0.0,1.0,1.0)), h3(i + vec3(1.0,1.0,1.0)), f.x), f.y), f.z); }
        float fbm(vec3 p){ return 0.55 * vn(p) + 0.3 * vn(p * 2.03) + 0.15 * vn(p * 4.1); }
      `;
      const blocks: { m: THREE.Mesh; prog: { value: number } }[] = [];
      const geo = new RoundedBoxGeometry(0.95, 0.95, 0.95, 4, 0.12);
      ['7', '3', '5'].forEach((n, i) => {
        const [c, g] = cv(128, 128);
        g.fillStyle = ['#ff6b8a', '#4dc3ff', '#7ad860'][i]!;
        g.fillRect(0, 0, 128, 128);
        g.fillStyle = '#fff';
        g.font = `900 84px ${FONT}`;
        g.textAlign = 'center';
        g.textBaseline = 'middle';
        g.fillText(n, 64, 70);
        const prog = { value: 0 };
        const mat = new THREE.MeshStandardMaterial({ map: tex(c), roughness: 0.45, side: THREE.DoubleSide });
        mat.onBeforeCompile = (sh) => {
          Object.assign(sh.uniforms, { uProg: prog, uEdge, uScale, uEdgeCol: uCol });
          sh.vertexShader = 'varying vec3 vObjPos;\n' + sh.vertexShader.replace('#include <begin_vertex>', '#include <begin_vertex>\n vObjPos = position;');
          sh.fragmentShader = NOISE + sh.fragmentShader
            .replace('#include <clipping_planes_fragment>', 'float dn = fbm(vObjPos * uScale + 3.7);\n if (dn < uProg) discard;\n#include <clipping_planes_fragment>')
            .replace(
              '#include <emissivemap_fragment>',
              `#include <emissivemap_fragment>
              float de = 1.0 - smoothstep(0.0, uEdge, dn - uProg);
              diffuseColor.rgb = mix(diffuseColor.rgb, uEdgeCol, de * 0.7);
              totalEmissiveRadiance += uEdgeCol * de * 2.4;`,
            );
        };
        const m = new THREE.Mesh(geo, mat);
        m.position.x = (i - 1) * 1.4;
        B.scene.add(m);
        blocks.push({ m, prog });
      });
      let speed = 1;
      let tt = 0;
      let lastLabel = '';
      const P = 6;
      return B.done({
        update(_t, dt) {
          tt += dt * speed;
          let state = '';
          blocks.forEach((b, i) => {
            const ph = (((tt - i * 2) % P) + P) % P / P;
            let p: number;
            if (ph < 0.38) p = -0.15 + (ph / 0.38) * 1.2;
            else if (ph < 0.5) p = 1.05;
            else if (ph < 0.88) p = 1.05 - ((ph - 0.5) / 0.38) * 1.2;
            else p = -0.15;
            b.prog.value = p;
            b.m.rotation.set(0.35, tt * 0.6 + i, 0);
            b.m.position.y = Math.sin(tt * 1.5 + i) * 0.08;
            if (i === 0) state = ph < 0.44 ? '사라지는 중' : ph < 0.94 ? '나타나는 중' : '그대로';
          });
          if (state !== lastLabel) {
            lastLabel = state;
            B.setLabels([state]);
          }
        },
        controls: [
          { type: 'range', label: '빛 테두리 폭', min: 0, max: 0.2, step: 0.01, value: 0.07, on: (v) => { uEdge.value = v; } },
          { type: 'range', label: '잡음 크기', min: 0.8, max: 6, step: 0.1, value: 2.6, on: (v) => { uScale.value = v; } },
          { type: 'range', label: '빠르기', min: 0.2, max: 2.5, step: 0.1, value: 1, on: (v) => { speed = v; } },
        ],
      });
    },
  }
```

## 관련 기술
- 같은 방법으로 만든 효과 예시: [데이터 분해 · 전송 (복셀 디졸브)](https://ai-techstudio.web.app/ai/t/i507.md) `i507` · [스프라이트 녹기 · 나타나기 (잡음 디졸브 2D)](https://ai-techstudio.web.app/ai/t/i295.md) `i295`
- 먼저 알면 좋은 기술: [재질에 셰이더 끼워 넣기](https://ai-techstudio.web.app/ai/t/u08.md) `u08`
- 다음에 해 볼 기술: [셰이더 전환 (잡음 녹기 · 도트 녹기)](https://ai-techstudio.web.app/ai/t/i243.md) `i243`
