# AI 꾸러미 — 노이즈 제거 (적은 샘플 → 깨끗하게 · OIDN) — Denoising (Intel Open Image Denoise, OIDN)
> 픽셀당 몇 샘플뿐인 자글자글한 경로 추적 그림을 법선 · 깊이 · 바탕색 길잡이로 걸러 — 모서리와 무늬는 지키고 노이즈만 지워 렌더 시간을 몇 분의 일로.  
> 견본: https://ai-techstudio.web.app/#t/i546

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

## 주문서

### 만들어 줘: 노이즈 제거 (적은 샘플 → 깨끗하게 · OIDN) — Denoising (Intel Open Image Denoise, OIDN)

#### 1. 목표
경로 추적 정지 그림 (32 샘플 → 깨끗하게)을(를) 적은 샘플로 경로 추적하고 노이즈 제거로 깨끗하게 만들어 줘 — 법선 · 바탕색 길잡이로 모서리 · 무늬는 지키게. 분위기는 사진처럼 또렷하게 (모서리 · 무늬 지키기).

#### 2. 핵심 기술 용어
- **Denoising (Intel Open Image Denoise, OIDN)** — 경로 추적 노이즈를 지우는 AI(U-Net) — 블렌더도 쓰는 그것, 웹은 oidn-web(WebGPU)
- **Edge-avoiding à-trous wavelet filter** — 구멍 뚫린 체를 1 · 2 · 4 · 8 · 16칸 간격으로 거듭 — 길잡이가 다르면 무게를 줄여 모서리를 지킴
- **Auxiliary buffers (albedo · normal · depth)** — 첫 충돌의 바탕색 · 법선 · 깊이 — 노이즈 없는 길잡이 그림
- **Albedo demodulation** — 바탕색으로 나눠 빛만 거른 뒤 다시 곱하기 — 무늬가 뭉개지지 않음

#### 3. 환경
- 플랫폼: three.js r186 (ES 모듈 · TypeScript, `import * as THREE from "three"`), WebGL2, 외부 라이브러리 추가 없이
- 화면: 3D · 브라우저 — PC 와 폰(가로 844×390 · 세로 390×844) 모두, 60fps 목표
- 예외: 실제 노이즈 제거는 oidn-web(npm, WebGPU)을 추가해서 쓴다. WebGPU 가 없을 때를 위해 à-trous 셰이더 필터를 대신 길로 둔다.

#### 4. 조건
- 실제 작업은 oidn-web 의 initUNetFromURL(가중치 .tza, { device }) + tileExecute({ color, albedo, normal, done }) — WebGPU 가 필요하다 (navigator.gpu)
- 길잡이(albedo · normal)는 픽셀 가운데로 쏜 첫 충돌로 따로 렌더 — 흔들린(지터) 샘플로 만들면 길잡이에도 노이즈가 생긴다
- 거르기 전에 바탕색으로 나누고, 거른 뒤 다시 곱한다 (무늬 지키기)
- 하늘 · 배경(아무것도 안 맞은 픽셀)은 법선이 0 — 배경끼리만 섞이게 따로 다룬다
- 왼쪽 원본 · 오른쪽 거른 것을 분할선으로 비교하는 화면을 둔다

#### 5. 완성 기준 (이게 보이면 성공)
- 픽셀당 2 ~ 4 샘플의 자글자글한 그림이 오른쪽에서 매끈해지고, 물체 윤곽 · 바둑판 칸 경계가 뭉개지지 않는다
- 「법선 · 깊이 길잡이」를 끄면 물체 가장자리가 바닥으로 번지고, 「바탕색으로 나누기」를 끄면 바둑판이 흐려진다
- 카메라를 멈추고 샘플을 쌓으면 원본과 거른 것이 점점 같아진다
- 배경(하늘)이 까맣게 빠지거나 얼룩지지 않는다

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

## 원리
- 경로 추적 노이즈는 픽셀마다 따로 튀는 「무작위 흔들림」이라, 이웃 픽셀과 평균 내면 준다. 그냥 흐리면 모서리 · 글씨 · 무늬까지 뭉개진다.
- 길잡이: 첫 충돌의 법선 · 깊이 · 바탕색은 빛줄 하나로도 정확해 노이즈가 없다. 이웃이 법선 · 깊이가 다르면(다른 면) 무게를 거의 0으로 — 모서리를 넘어 섞이지 않는다.
- à-trous: 5×5 체(1/16 · 1/4 · 3/8 · 1/4 · 1/16)를 칸 간격 1 · 2 · 4 · 8 · 16 으로 다섯 번 — 25칸만 읽고도 넓게(65칸) 거른다. 단계마다 밝기 기준을 좁혀 처음엔 넓게, 나중엔 경계를 지킨다.
- 바탕색으로 나누기: 색 = 바탕색 × 빛. 빛만 거르고 다시 바탕색을 곱하면 바둑판 · 나뭇결 무늬는 그대로.
- OIDN 은 이 판단을 수많은 렌더로 배운 신경망(U-Net)이 한다 — 같은 길잡이(albedo · normal)를 넣으면 더 정확하다.

## 핵심 코드 — à-trous 한 단계 — 법선 · 깊이 · 밝기로 무게 (uStep = 1, 2, 4, 8, 16)
(발췌: demos/lib/pathtrace.ts ATROUS)
```glsl
const float K[3] = float[3](0.375, 0.25, 0.0625);
vec3 c0 = fetch(vUv);                       // fetch = 색 / 바탕색 (첫 단계)
vec4 nd0 = texture2D(uNd, vUv);             // 법선 xyz + 깊이 w
vec3 sum = vec3(0.0); float wsum = 0.0;
for (int y = -2; y <= 2; y++) for (int x = -2; x <= 2; x++) {
  vec2 uv = vUv + vec2(float(x), float(y)) * uStep * uTexel;
  vec3 c = fetch(uv);
  float w = K[abs(x)] * K[abs(y)];
  float l = dot(c, vec3(0.2126, 0.7152, 0.0722)), l0 = dot(c0, vec3(0.2126, 0.7152, 0.0722));
  float dl = l / (1.0 + l) - l0 / (1.0 + l0);          // 톤을 눌러 비교
  w *= exp(-dl * dl / (uSigC * uSigC));
  vec4 nd = texture2D(uNd, uv);
  bool m0 = dot(nd0.xyz, nd0.xyz) < 0.25, m = dot(nd.xyz, nd.xyz) < 0.25;   // 배경
  w *= m0 || m ? float(m0 == m)
       : pow(max(dot(nd.xyz, nd0.xyz), 0.0), 32.0) * exp(-abs(nd.w - nd0.w) / (0.06 * uStep + 0.02));
  sum += c * w; wsum += w;
}
vec3 o = sum / max(wsum, 1e-6);
if (lastPass) o *= texture2D(uAlb, vUv).rgb;  // 마지막 단계에서 바탕색 다시 곱하기
gl_FragColor = vec4(o, 1.0);
```

## 흔한 실수 · 확인 목록
- [ ] **색(RGB) 차이로 무게를 주면 반짝이는 점이 그대로 남는다** — 적은 샘플에서는 노이즈 자체가 큰 색 차이라 이웃 무게가 0 이 된다. 톤을 누른 밝기 차이로 바꾸고 기준을 넉넉히(첫 단계 0.9) — 견본이 이렇게 고쳐서야 걸러졌다.
- [ ] **배경 픽셀이 까맣게 빠진다** — 하늘은 법선이 0 이라 dot(n, n0) 무게가 모두 0, 나누기가 0/0 이 된다. 법선 0 끼리만 무게 1.
- [ ] **바탕색으로 안 나누면 무늬가 뭉개진다** — 무늬도 「색 차이」라 체가 지운다. 색 / 바탕색으로 빛만 거르고 다시 곱한다 (OIDN 에 albedo 를 넣는 까닭도 같다).
- [ ] **OIDN 이 「navigator.gpu 없음」으로 안 돈다** — oidn-web 은 WebGPU 가 필요하다. 헤드리스 크롬은 --enable-unsafe-webgpu 와 GPU 옵션이 있어야 하고, 안 되면 견본 같은 à-trous 로 대신한다.

## 완성 기준 체크리스트
- [ ] 픽셀당 2 ~ 4 샘플의 자글자글한 그림이 오른쪽에서 매끈해지고, 물체 윤곽 · 바둑판 칸 경계가 뭉개지지 않는다
- [ ] 「법선 · 깊이 길잡이」를 끄면 물체 가장자리가 바닥으로 번지고, 「바탕색으로 나누기」를 끄면 바둑판이 흐려진다
- [ ] 카메라를 멈추고 샘플을 쌓으면 원본과 거른 것이 점점 같아진다
- [ ] 배경(하늘)이 까맣게 빠지거나 얼룩지지 않는다

## 이 기술 정보
- id: `i546` · 분류: 3D 모델 · 캐릭터 › 하드서피스 · 실사 렌더링 · 3D · 난이도 어려움 · 폰 부담 보통 (폰 주의) — à-trous 는 화면 한 장에 25칸 × 5단계 읽기라 1080p 도 몇 ms. OIDN(WebGPU)은 1080p 한 장 1 ~ 수 초(타일로 나눠), 대신 결과가 훨씬 깨끗.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i546
- 쓰면 좋을 때: 영상 수백 프레임을 프레임당 32 ~ 64 샘플로 줄여 굽고 싶을 때 (몇 배 빨라짐) / 실시간 경로 추적 미리 보기(카메라가 움직여 늘 적은 샘플) / 샘플을 더 쌓아도 남는 잔노이즈를 마지막에 지울 때
- 쓰지 말 때: 머리카락 · 풀 · 작은 글씨처럼 픽셀보다 가는 것이 많은 그림 — 지워질 수 있다. 샘플을 더 쌓는다 / 샘플이 1 ~ 2 개뿐인 유리 · 물 속 — 길잡이(첫 충돌)가 유리 표면이라 속 모습이 얼룩진다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### makeDenoise — `src/demos/demosPathTrace.ts:97`
```ts
function makeDenoise(): Scene3D {
  const { scene, camera } = blank();
  const pt = new PathTracer();
  const dn = new Denoiser();
  const show = new Show();
  const overlay = new Overlay();
  let split = 0.5;
  let stop = false;
  let spf = 2;
  let view = 0;
  let ang = 0.15;
  let tags: Tag[] = [];
  const p = new THREE.Vector3();
  const look = new THREE.Vector3(0, 0.4, 0.15);
  return {
    scene,
    camera,
    tone: THREE.NoToneMapping,
    update(t, dt) {
      if (!stop) {
        // 천천히 도는 카메라 — 장면마다 처음부터라 늘 「적은 샘플」 그림을 거른다 (실시간 경로 추적의 형편)
        ang += dt * 0.22;
        p.set(Math.sin(ang) * 5, 1.5 + Math.sin(t * 0.4) * 0.3, Math.cos(ang) * 5);
        aim(pt.camera, p, look);
        pt.reset();
      }
    },
    render(r, w, h) {
      pt.setSize(w, h);
      pt.sample(r, spf);
      const big = w >= 700;
      if (view === 1) {
        pt.guides(r);
        show.draw(r, pt.nd.texture, { view: 1 });
        tags = [{ text: '길잡이 ① 법선 (첫 충돌 면의 방향)', x: 0.02, y: 0.04, ax: 0, ay: 0, big: true }];
      } else if (view === 2) {
        pt.guides(r);
        show.draw(r, pt.alb.texture, { view: 2 });
        tags = [{ text: '길잡이 ② 바탕색 (빛을 뺀 무늬)', x: 0.02, y: 0.04, ax: 0, ay: 0, big: true }];
      } else {
        const clean = dn.run(r, pt);
        show.draw(r, pt.tex, { b: clean, split });
        tags = [
          { text: `원본 · 픽셀당 ${pt.n}샘플`, x: Math.max(0.02, split - 0.02), y: 0.04, ax: 1, ay: 0, big },
          { text: dn.iters ? `노이즈 제거 (${dn.iters}단계)` : '노이즈 제거 끔', x: Math.min(0.98, split + 0.02), y: 0.04, ax: 0, ay: 0, big, bg: 'rgba(30,110,200,0.85)' },
        ];
        if (big) tags.push({ text: stop ? '멈춤 — 샘플이 쌓일수록 양쪽이 같아진다' : '카메라가 도는 동안은 장면마다 처음부터 (늘 적은 샘플)', x: 0.5, y: 0.96, ax: 0.5, ay: 1 });
      }
      overlay.draw(r, w, h, tags);
    },
    controls: [
      { type: 'range', label: '분할선 (왼쪽 원본 · 오른쪽 거른 것)', min: 0, max: 1, step: 0.01, value: 0.5, on: (v) => (split = v) },
      { type: 'range', label: '거르는 단계 (1 · 2 · 4 · 8 · 16칸 간격)', min: 0, max: 5, step: 1, value: 5, on: (v) => (dn.iters = v) },
      { type: 'range', label: '밝기 기준 (클수록 많이 뭉갬)', min: 0.1, max: 2, step: 0.05, value: 0.9, on: (v) => (dn.sigma = v) },
      { type: 'toggle', label: '법선 · 깊이 길잡이 (끄면 모서리까지 번짐)', value: true, on: (v) => (dn.guide = v) },
      { type: 'toggle', label: '바탕색으로 나눠 거르기 (끄면 바둑판 무늬가 뭉개짐)', value: true, on: (v) => (dn.demod = v) },
      { type: 'range', label: '한 장면에 쌓는 샘플', min: 1, max: 8, step: 1, value: 2, on: (v) => (spf = v) },
      { type: 'toggle', label: '카메라 멈추기 (계속 쌓기)', value: false, on: (v) => (stop = v) },
      { type: 'range', label: '보기 (0 그림 · 1 법선 · 2 바탕색)', min: 0, max: 2, step: 1, value: 0, on: (v) => (view = v) },
    ],
    dispose() {
      pt.dispose();
      dn.dispose();
      show.dispose();
      overlay.dispose();
    },
  };
}
```

## 관련 기술
- 먼저 알면 좋은 기술: [GPU 경로 추적 (사진 같은 렌더 · 샘플 쌓기)](https://ai-techstudio.web.app/ai/t/i545.md) `i545`
- 다음에 해 볼 기술: [경로 추적 영상 굽기 (프레임마다 수렴 → mp4)](https://ai-techstudio.web.app/ai/t/i547.md) `i547`
- 참고 문서: [Intel Open Image Denoise](https://www.openimagedenoise.org/) · [oidn-web (GitHub)](https://github.com/DennisSmolek/oidn-web) · [Dammertz et al. — Edge-Avoiding À-Trous Wavelet Transform (2010)](https://jo.dreggn.org/home/2010_atrous.pdf)
