# AI 꾸러미 — 구석 그늘 (SSAO / GTAO) — Ambient occlusion (SSAO / GTAO)
> 화면의 깊이 · 법선으로 각 점 둘레가 얼마나 막혀 있는지 재서 틈 · 구석 · 맞닿은 곳을 어둡게 해, 블록이 붕 떠 보이지 않고 입체감이 산다.  
> 견본: https://ai-techstudio.web.app/#t/i02

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

## 주문서

### 만들어 줘: 구석 그늘 (SSAO / GTAO) — Ambient occlusion (SSAO / GTAO)

#### 1. 목표
쌓은 블록 · 쌓기나무에 구석 그늘(GTAO)을 넣어 줘 — 틈 · 벽 모서리 · 맞닿은 곳이 어두워지게. 분위기는 밝은 파스텔 방.

#### 2. 핵심 기술 용어
- **Ambient occlusion (SSAO / GTAO)** — 구석 그늘 — 주변에 막힌 만큼 어둡게
- **GTAOPass** — three.js 지평선 기반 구석 그늘 후처리
- **blendIntensity · radius · samples** — 그늘 세기 · 살필 반경 · 표본 수

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

#### 4. 조건
- GTAOPass(scene, camera, 너비, 높이) 를 RenderPass 뒤에, 끝에 OutputPass
- updateGtaoMaterial({ radius: 0.5, distanceExponent: 1, thickness: 1, scale: 1.3, samples: 16 })
- 그늘 세기는 blendIntensity (0~2)
- 폰(짧은 변 < 500)에서는 끄기
- 깊이를 다시 그릴 때 반투명 · 스프라이트는 숨기기

#### 5. 완성 기준 (이게 보이면 성공)
- 왼쪽 그냥 · 오른쪽 구석 그늘로 나누면, 오른쪽만 블록 사이 틈 · 벽과 바닥이 만나는 선 · 공 밑이 부드럽게 어두워진다
- 공이 굴러가면 공 밑 그늘도 따라간다
- 「그늘 세기」 0 이면 왼쪽과 같아진다

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

## 원리
- 빛을 고르게 받는 밝은 방에서는 그림자 지도만으로 블록 틈 · 벽 모서리가 어두워지지 않아 납작해 보인다.
- GTAO 는 각 픽셀 둘레를 반경(0.5) 안에서 살펴, 주변 깊이가 하늘을 얼마나 가리는지(지평선 각)를 잰다.
- 많이 가려진 곳일수록 어둡게 곱한다. 표본 16개 + 잡음 지우기.
- 반구광이 센(1.6) 밝은 장면에서 차이가 가장 크다.

## 핵심 코드 — GTAO 후처리
(발췌: demos/demosLight.ts i02 make() 를 정리)
```ts
import { GTAOPass } from 'three/examples/jsm/postprocessing/GTAOPass.js';

// 밝고 그림자가 약한 방 — 구석 그늘 차이가 가장 크게 보인다
scene.add(new THREE.HemisphereLight(0xffffff, 0xc8c0b0, 1.6));
const sun = new THREE.DirectionalLight(0xffffff, 0.7);
sun.position.set(3, 5, 4);
scene.add(sun);

const comp = new EffectComposer(renderer);
comp.addPass(new RenderPass(scene, cam));
const ao = new GTAOPass(scene, cam, 256, 256);
ao.updateGtaoMaterial({ radius: 0.5, distanceExponent: 1, thickness: 1, scale: 1.3, samples: 16 });
comp.addPass(ao);
comp.addPass(new OutputPass());

function frame(dt: number) {
  ao.blendIntensity = 1;                 // 그늘 세기 0~2
  comp.render(dt);
}
// 폰: if (Math.min(w, h) < 500) ao.enabled = false;
```

## 흔한 실수 · 확인 목록
- [ ] **구석 그늘 잡음이 지지직 떨린다** — 빵빵 주차장에서 겪었다. 반경 0.35 · 표본 32 · 잡음 지우기를 넉넉히(updatePdMaterial) 해서 고쳤다.
- [ ] **깊이를 다시 그리는 패스에서 반투명 · 스프라이트가 검은 네모가 된다** — 공용 후처리(shared/post.ts)는 그 패스 동안 반투명 · 스프라이트를 숨긴다.
- [ ] **반경을 크게 하면 물체 둘레에 검은 후광이 생긴다** — 반경은 블록 크기의 절반 정도(견본 0.5, 블록 0.5).
- [ ] **폰에서 켜 두면 프레임이 반으로 떨어진다** — 공용 후처리는 짧은 변 500 아래에서 GTAO 를 끈다.

## 완성 기준 체크리스트
- [ ] 왼쪽 그냥 · 오른쪽 구석 그늘로 나누면, 오른쪽만 블록 사이 틈 · 벽과 바닥이 만나는 선 · 공 밑이 부드럽게 어두워진다
- [ ] 공이 굴러가면 공 밑 그늘도 따라간다
- [ ] 「그늘 세기」 0 이면 왼쪽과 같아진다

## 이 기술 정보
- id: `i02` · 분류: 그래픽 · 셰이더 › 후처리 · 3D · 난이도 보통 · 폰 부담 무거움 — 깊이 · 법선 그리기 한 번 + 픽셀마다 표본 16개 + 잡음 지우기. 폰에서는 끄는 선택지를 둔다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i02
- 쓰면 좋을 때: 쌓기나무 · 블록 · 방처럼 맞닿은 면이 많은 장면 / 밝고 그림자가 약한 파스텔 장면이 납작해 보일 때
- 쓰지 말 때: 폰 — 깊이 · 법선을 다시 그리고 표본을 여러 번 읽는다. 이 사이트는 짧은 변 500 아래에서 끈다 / 움직이는 작은 물체가 많은 장면 — 잡음이 지지직 보일 수 있다. 대신 접촉 그림자(i15)

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i02 견본 항목 — `src/demos/demosLight.ts:1645`
```ts
  i02: {
    kind: '3d',
    caption: '왼쪽 그냥 · 오른쪽 GTAO 구석 그늘 — 블록 틈 · 벽 모서리가 어두워져 입체감이 쑥',
    make() {
      const scene = new THREE.Scene();
      scene.background = new THREE.Color(0xe9eef8);
      scene.add(new THREE.HemisphereLight(0xffffff, 0xc8c0b0, 1.6));
      const sun = new THREE.DirectionalLight(0xffffff, 0.7);
      sun.position.set(3, 5, 4);
      scene.add(sun);
      const wallM = std(0xf4efe6, 0.95);
      const floor = mesh(new THREE.BoxGeometry(4, 0.1, 4), wallM, 0, -0.05, 0);
      const back = mesh(new THREE.BoxGeometry(4, 2.4, 0.1), wallM, 0, 1.2, -2);
      const left = mesh(new THREE.BoxGeometry(0.1, 2.4, 4), wallM, -2, 1.2, 0);
      scene.add(floor, back, left);
      const cols = [0xffc7c7, 0xfff0a8, 0xbfe3ff, 0xc8f2c8, 0xe2cdfc];
      const cubeG = new THREE.BoxGeometry(0.5, 0.5, 0.5);
      const stack: [number, number, number][] = [
        [-1.75, 0, -1.75], [-1.25, 0, -1.75], [-0.75, 0, -1.75], [-1.75, 0, -1.25], [-1.75, 1, -1.75], [-1.25, 1, -1.75], [-1.75, 1, -1.25], [-1.75, 2, -1.75],
        [0.25, 0, -0.25], [0.75, 0, -0.25], [0.25, 0, 0.25], [0.25, 1, -0.25], [0.75, 0, 0.25],
        [-0.75, 0, 0.75], [1.25, 0, -1.25], [1.25, 1, -1.25], [1.25, 0, -1.75],
      ];
      const blocks = new THREE.Group();
      stack.forEach(([x, y, z], i) => blocks.add(mesh(cubeG, std(cols[i % cols.length]!, 0.8), x, 0.25 + y * 0.5, z)));
      const ball = mesh(new THREE.SphereGeometry(0.32, 32, 20), std(0xffffff, 0.7), -0.7, 0.32, -0.3);
      scene.add(blocks, ball);
      const cam = new THREE.PerspectiveCamera(36, 1, 0.1, 40);
      let inten = 1;
      const kit = postKit((r) => {
        const ao = new GTAOPass(scene, cam, 256, 256);
        ao.updateGtaoMaterial({ radius: 0.5, distanceExponent: 1, thickness: 1, scale: 1.3, samples: 16 });
        return { comp: splitChain(r, scene, cam, [ao]), ao };
      });
      const hud = new Hud();
      hud.vline(0.5);
      hud.label('그냥', 0.25, 0.95, 0.5, 1);
      hud.label('구석 그늘', 0.75, 0.95, 0.5, 1, 'on');
      let now = 0;
      let dt = 0.016;
      return {
        scene,
        camera: cam,
        update(t, d) {
          now = t;
          dt = d;
          ball.position.x = -0.7 + Math.sin(t * 0.9) * 0.45;
          ball.rotation.z = -t;
        },
        render(r, w, h) {
          const a = 0.55 + Math.sin(now * 0.3) * 0.25;
          fit(cam, w / h, V3(-0.4, 0.55, -0.4), 2.0, V3(Math.sin(a), 0.75, Math.cos(a)));
          const k = kit.get(r, w, h);
          k.ao.blendIntensity = inten;
          k.comp.render(dt);
          hud.draw(r, w, h);
        },
        controls: [{ type: 'range', label: '그늘 세기', min: 0, max: 2, step: 0.05, value: 1, on: (v) => (inten = v) }],
        dispose() {
          kit.dispose();
          hud.dispose();
          disposeTree(scene);
        },
      };
    },
  }
```

## 관련 기술
- 먼저 알면 좋은 기술: [후처리 사슬 (EffectComposer → OutputPass)](https://ai-techstudio.web.app/ai/t/u21.md) `u21`
- 다음에 해 볼 기술: [후처리 외곽선 (깊이 · 법선 경계)](https://ai-techstudio.web.app/ai/t/i03.md) `i03` · [접촉 그림자 (Contact shadow)](https://ai-techstudio.web.app/ai/t/i15.md) `i15`
- 참고 문서: [three.js 예제 — webgl_postprocessing_gtao](https://threejs.org/examples/#webgl_postprocessing_gtao) · [three.js 소스 — GTAOPass](https://github.com/mrdoob/three.js/blob/dev/examples/jsm/postprocessing/GTAOPass.js)
