# AI 꾸러미 — GPU 경로 추적 (사진 같은 렌더 · 샘플 쌓기) — Path tracing (progressive, Monte Carlo)
> 픽셀마다 빛줄을 쏘아 여러 번 튀는 길을 따라가고 샘플을 쌓아 평균 — 반사 · 굴절 · 부드러운 그림자가 저절로, 블렌더 없이 사진 같은 한 장.  
> 견본: https://ai-techstudio.web.app/#t/i545

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

## 주문서

### 만들어 줘: GPU 경로 추적 (사진 같은 렌더 · 샘플 쌓기) — Path tracing (progressive, Monte Carlo)

#### 1. 목표
금속 · 유리 · 니스 칠한 기계 부품 정물을 three.js 장면 그대로 GPU 경로 추적으로 렌더해 줘 — 샘플을 쌓아 반사 · 굴절 · 부드러운 그림자가 사진처럼 나오게. 분위기는 어두운 스튜디오 (남색 배경 · 네모 띠 조명).

#### 2. 핵심 기술 용어
- **Path tracing (progressive, Monte Carlo)** — 빛줄을 무작위로 튀겨 평균 내는 렌더 — 샘플이 쌓일수록 노이즈가 걷힘
- **three-gpu-pathtracer (WebGLPathTracer)** — three.js 장면을 그대로 경로 추적하는 라이브러리 — BVH · 물리 재질 · 심도
- **Samples per pixel (spp) / bounces** — 픽셀당 샘플 수 · 빛이 튀는 횟수
- **Thin lens depth of field (PhysicalCamera fStop · focusDistance)** — 렌즈 원판의 아무 점에서 쏘아 초점 밖을 흐리게

#### 3. 환경
- 플랫폼: three.js r186 (ES 모듈 · TypeScript, `import * as THREE from "three"`), WebGL2, 외부 라이브러리 추가 없이
- 화면: 3D · 브라우저 — PC 와 폰(가로 844×390 · 세로 390×844) 모두, 60fps 목표
- 예외: 이 기술은 three-gpu-pathtracer(npm, three-mesh-bvh 함께)를 추가해서 쓴다. 60fps 실시간이 아니라 샘플을 쌓는 정지 그림 · 굽기용이다.

#### 4. 조건
- 장면은 three.js 그대로 두고 three-gpu-pathtracer 의 WebGLPathTracer 로 렌더 — 재질은 MeshPhysicalMaterial(metalness · roughness · clearcoat · transmission · ior)
- pathTracer.setScene(scene, camera) 는 모형 · 재질이 바뀔 때만 (BVH 를 다시 짓는다). 카메라만 움직이면 updateCamera(), 재질 값만 바뀌면 updateMaterials()
- renderSample() 을 원하는 spp 까지 부르고 pathTracer.samples 로 확인 — 몇 장마다 requestAnimationFrame 을 기다려 화면 · 브라우저가 멈추지 않게
- 톤 매핑은 ACESFilmic, 조명은 환경 지도(HDR 또는 GradientEquirectTexture) + 넓은 면 조명 — 작은 점 조명만 있으면 노이즈가 아주 오래 간다
- 반딧불(드물게 아주 밝은 점) 막기: 빛줄 밝기 상한 · filterGlossyFactor 로 거친 반사를 부드럽게

#### 5. 완성 기준 (이게 보이면 성공)
- 처음엔 자글자글하다가 샘플이 쌓일수록 매끈해지고, 32 → 128 샘플에서 노이즈가 눈에 띄게 절반쯤으로 준다
- 튐 횟수를 1 → 5 로 올리면 그림자 속 · 유리 공 안이 밝아지고 빨간 공 빛이 바닥에 묻어난다
- 금속 공에 네모난 띠 조명이 비치고, 유리 공 너머 물체가 뒤집혀 보이며, 물체 밑에 부드러운 접촉 그림자가 생긴다
- 카메라를 움직이면 샘플이 0 부터 다시 쌓이고, 멈추면 계속 맑아진다

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

## 원리
- 실시간 렌더는 빛 하나에서 표면까지 한 번만 계산한다. 경로 추적은 카메라에서 빛줄을 쏘아 부딪힐 때마다 재질대로 방향을 무작위로 골라 다시 튀기고, 하늘 · 조명에 닿으면 지나온 색을 곱해 가져온다.
- 한 장면 = 픽셀마다 빛줄 하나(1 샘플)라 처음엔 자글자글하다. 장면마다 평균을 쌓는다: 새 평균 = mix(전 평균, 새 샘플, 1/(n+1)). 노이즈는 1/√n 로 줄어 4배 쌓으면 절반.
- 재질은 「어느 쪽으로 튈 확률」: 거친 면 = 코사인 반구, 금속 = 반사 방향 + 거칠기만큼 흔들기, 유리 = 프레넬 확률로 반사 아니면 굴절(스넬), 니스 = 프레넬로 반사 아니면 밑칠.
- 튐 횟수 1 = 직접광만(그림자가 새까맣고 유리가 검다), 늘리면 구석까지 빛이 번지고 색이 옆으로 묻어난다(색 번짐).
- 카메라 · 물체가 움직이면 지난 샘플은 다른 그림이라 버리고 처음부터 — 그래서 정지 그림 · 한 프레임씩 굽는 영상에 맞다.

## 핵심 코드 — three-gpu-pathtracer 로 한 장 렌더 (원하는 spp 까지)
(발췌: 새로 씀 — 「도구와 기계의 원리」 pt_intro.html 과 같은 방식)
```js
import { WebGLPathTracer, PhysicalCamera, GradientEquirectTexture } from 'three-gpu-pathtracer';

const renderer = new THREE.WebGLRenderer({ antialias: false, preserveDrawingBuffer: true });
renderer.toneMapping = THREE.ACESFilmicToneMapping;
const camera = new PhysicalCamera(30, W / H, 0.1, 100);
camera.fStop = 5.6;                       // 작을수록 배경이 흐림
camera.apertureBlades = 6;                // 흐린 빛 방울이 육각형

const bg = new GradientEquirectTexture();
bg.topColor.set(0xd9d2c6); bg.bottomColor.set(0x6d6052); bg.update();
scene.background = bg; scene.environment = bg;

const pt = new WebGLPathTracer(renderer);
pt.bounces = 5; pt.transmissiveBounces = 4; pt.filterGlossyFactor = 1.0;
pt.renderDelay = 0; pt.fadeDuration = 0; pt.minSamples = 1;
pt.setScene(scene, camera);               // 모형이 바뀔 때만 (BVH 다시 짓기)

async function renderStill(spp) {
  camera.focusDistance = camera.position.distanceTo(target);
  pt.updateCamera();
  while (pt.samples < spp) {
    pt.renderSample();                    // 픽셀마다 1 샘플 더 쌓기
    if (Math.floor(pt.samples) % 8 === 0) await new Promise((r) => requestAnimationFrame(r));
  }
}
```

## 흔한 실수 · 확인 목록
- [ ] **작은 점 조명 하나면 수천 샘플에도 자글거린다** — 빛줄이 그 작은 점에 우연히 닿을 확률이 낮아서다. 넓은 면 조명 · 띠 조명 · 환경 지도로 빛을 넓게 준다. 견본도 띠 조명을 넓히자 같은 샘플에서 바닥 노이즈가 크게 줄었다.
- [ ] **매 프레임 setScene 을 부르면 몇 초씩 멈춘다** — setScene 은 BVH 를 새로 짓는다. 카메라만 바뀌면 updateCamera(), 부품이 움직인 프레임에만 setScene — 「도구와 기계의 원리」 렌더도 부품 위치 문자열을 비교해 바뀔 때만 다시 지었다.
- [ ] **반쪽 실수(HalfFloat) 평균은 수천 샘플에서 멈춘다** — mix(전, 새, 1/(n+1)) 의 더하는 양이 반쪽 실수 정밀도 아래로 내려간다. 상한(견본 1024)을 두거나 FloatType 타깃에 합을 쌓고 나눠서 보인다.
- [ ] **셰이더 한 줄 주석이 같은 줄 코드를 삼킨다** — GLSL 에서 // 뒤는 줄 끝까지 주석이다. 견본에서 any = true 가 주석에 먹혀 바닥이 통째로 사라졌다 — 주석은 코드 위 줄에.
- [ ] **smoothstep(큰 값, 작은 값, x) 는 D3D 에서 0 이 나온다** — 경계가 거꾸로면 GLSL 명세상 결과가 정해지지 않았다. 1.0 - smoothstep(작은 값, 큰 값, x) 로 쓴다 (견본 바닥이 윈도 크롬에서만 검게 나왔다).

## 완성 기준 체크리스트
- [ ] 처음엔 자글자글하다가 샘플이 쌓일수록 매끈해지고, 32 → 128 샘플에서 노이즈가 눈에 띄게 절반쯤으로 준다
- [ ] 튐 횟수를 1 → 5 로 올리면 그림자 속 · 유리 공 안이 밝아지고 빨간 공 빛이 바닥에 묻어난다
- [ ] 금속 공에 네모난 띠 조명이 비치고, 유리 공 너머 물체가 뒤집혀 보이며, 물체 밑에 부드러운 접촉 그림자가 생긴다
- [ ] 카메라를 움직이면 샘플이 0 부터 다시 쌓이고, 멈추면 계속 맑아진다

## 이 기술 정보
- id: `i545` · 분류: 3D 모델 · 캐릭터 › 하드서피스 · 실사 렌더링 · 3D · 난이도 어려움 · 폰 부담 무거움 — 1920×1080 한 샘플이 픽셀 207만 개 × 튐 5번. 견본(수식 도형)은 844×475 한 샘플 0.9ms(라데온 780M), 메시 장면 + BVH 는 몇 배~몇십 배. 영상 프레임당 32 ~ 수백 샘플.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i545
- 쓰면 좋을 때: 유튜브 설명 영상 · 썸네일 · 제품 사진처럼 「멈춘 그림 한 장」의 품질이 중요할 때 / 금속 · 유리 · 니스처럼 반사와 굴절이 많은 기계 부품을 블렌더 없이 코드로 / 같은 three.js 장면을 실시간 화면과 고화질 렌더 둘 다로 쓰고 싶을 때
- 쓰지 말 때: 60fps 로 움직이는 게임 화면 — 장면마다 처음부터라 늘 자글거린다. 대신 PBR + 환경 반사(u12) + 빛 번짐 · 심도 후처리 / 폰에서 실시간 — 대신 미리 구운 그림(라이트맵 · 영상)을 보여 준다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### makeTrace — `src/demos/demosPathTrace.ts:32`
```ts
function makeTrace(): Scene3D {
  const { scene, camera } = blank();
  const pt = new PathTracer();
  const show = new Show();
  const overlay = new Overlay();
  let shot = 0;
  let shotT = 0;
  let first = true;
  let hold = false;
  let spf = 2;
  let moving = false;
  let tags: Tag[] = [];
  const STAY = 7;
  const MOVE = 1.1;
  const p = new THREE.Vector3();
  const l = new THREE.Vector3();
  return {
    scene,
    camera,
    tone: THREE.NoToneMapping,
    update(_t, dt) {
      shotT += dt;
      if (!hold && shotT > STAY + MOVE) {
        shot = (shot + 1) % SHOTS.length;
        shotT = 0;
        first = false;
      }
      const a = SHOTS[(shot + SHOTS.length - 1) % SHOTS.length]!;
      const b = SHOTS[shot]!;
      const k = first ? 1 : smooth(0, 1, shotT / MOVE);
      moving = k < 1;
      p.lerpVectors(a.p, b.p, k);
      l.lerpVectors(a.l, b.l, k);
      aim(pt.camera, p, l);
      pt.focusAt(GLASS);
      // 카메라가 움직이는 동안은 지난 샘플이 다른 그림이라 매번 처음부터
      if (moving) pt.reset();
    },
    render(r, w, h) {
      pt.setSize(w, h);
      const big = w >= 700;
      pt.sample(r, spf);
      show.draw(r, pt.tex);
      tags = [{ text: `픽셀당 샘플 ${pt.n}${pt.done ? ' (다 쌓음)' : ''}`, x: 0.02, y: 0.04, ax: 0, ay: 0, big: true }];
      if (moving) tags.push({ text: '카메라가 움직이면 → 처음부터 다시 쌓기', x: 0.5, y: 0.96, ax: 0.5, ay: 1 });
      else if (pt.n < 24) tags.push({ text: '자글자글 = 빛줄이 아직 적다', x: 0.5, y: 0.96, ax: 0.5, ay: 1 });
      else if (big) tags.push({ text: `빛이 ${pt.bounces}번까지 튐 · ${pt.w}×${pt.h} · 한 장면에 ${spf}샘플`, x: 0.5, y: 0.96, ax: 0.5, ay: 1 });
      overlay.draw(r, w, h, tags);
    },
    controls: [
      { type: 'range', label: '빛이 튀는 횟수 (1 = 직접광만)', min: 1, max: 8, step: 1, value: 5, on: (v) => ((pt.bounces = v), pt.reset()) },
      { type: 'range', label: '렌즈 조리개 (심도 흐림 — 초점은 유리 공)', min: 0, max: 0.2, step: 0.01, value: 0, on: (v) => ((pt.aperture = v), pt.reset()) },
      { type: 'range', label: '한 장면에 쌓는 샘플', min: 1, max: 8, step: 1, value: 2, on: (v) => (spf = v) },
      { type: 'toggle', label: '카메라 멈추기 (계속 쌓기)', value: false, on: (v) => (hold = v) },
      { type: 'button', label: '처음부터 다시 쌓기', on: () => pt.reset() },
    ],
    dispose() {
      pt.dispose();
      show.dispose();
      overlay.dispose();
    },
  };
}
```

## 관련 기술
- 먼저 알면 좋은 기술: [선반 회전체 + 장식 (윤곽선 하나로)](https://ai-techstudio.web.app/ai/t/i450.md) `i450` · [스윕 · 로프트 (곡선 따라 단면 끌기)](https://ai-techstudio.web.app/ai/t/i449.md) `i449` · [뚜껑 없는 단면 (뒷면 셰이딩)](https://ai-techstudio.web.app/ai/t/i85.md) `i85` · [절차 마모 재질 (CG 티 없애기)](https://ai-techstudio.web.app/ai/t/i84.md) `i84` · [PBR + 클리어코트](https://ai-techstudio.web.app/ai/t/u04.md) `u04`
- 다음에 해 볼 기술: [노이즈 제거 (적은 샘플 → 깨끗하게 · OIDN)](https://ai-techstudio.web.app/ai/t/i546.md) `i546` · [경로 추적 영상 굽기 (프레임마다 수렴 → mp4)](https://ai-techstudio.web.app/ai/t/i547.md) `i547`
- 참고 문서: [three-gpu-pathtracer (GitHub)](https://github.com/gkjohnson/three-gpu-pathtracer) · [Ray Tracing in One Weekend](https://raytracing.github.io/books/RayTracingInOneWeekend.html) · [Wikipedia — Path tracing](https://en.wikipedia.org/wiki/Path_tracing)
