# AI 꾸러미 — 가리는 벽 투명 처리 — Occluder fade (dithered cutout)
> 카메라에서 주인공 발 · 가슴 · 머리로 쏜 광선에 걸리는 벽 · 기둥만 골라, 주인공 둘레 화면 원 안을 바이어 점무늬로 비우고 벽 너머엔 윤곽선을 그린다.  
> 견본: https://ai-techstudio.web.app/#t/i488

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

## 주문서

### 만들어 줘: 가리는 벽 투명 처리 — Occluder fade (dithered cutout)

#### 1. 목표
쿼터뷰 던전에서 카메라와 주인공 사이를 가리는 벽 · 기둥만 주인공 둘레를 점무늬로 비우고, 벽 너머엔 윤곽선을 그려 줘. 분위기는 횃불 던전 · 디아블로 꼴.

#### 2. 핵심 기술 용어
- **Occluder fade (dithered cutout)** — 가리는 것만 점무늬로 구멍 내 반투명처럼
- **Ordered dithering (Bayer 4×4)** — 정해진 4×4 문턱 무늬로 픽셀을 버려 투명도 흉내
- **X-ray silhouette (depthFunc GreaterDepth)** — 벽 뒤에 가린 부분만 단색 윤곽으로
- **Ray vs bounding box** — 카메라 → 주인공 광선과 벽 상자 교차

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

#### 4. 조건
- 가리는 것만 비운다 (광선에 걸린 상자) — 다른 벽은 그대로
- 진짜 투명(transparent) 대신 바이어 점무늬 discard — 깊이 · 그림자 유지
- 주인공보다 카메라 쪽 조각만 비운다 (주인공 뒤 벽은 그대로)
- fade 는 부드럽게 오르내려 깜빡이지 않게
- 방식 바꾸기(끔 · 점무늬 반투명 · 둥글게 비우기) · 윤곽선 · 화면 반경 · 끔/켬 나란히

#### 5. 완성 기준 (이게 보이면 성공)
- 기사가 벽 뒤로 걸어 들어가면 그 벽만 기사 둘레가 둥글게 비어 기사가 보인다
- 벽에 살짝 가린 순간엔 벽 너머로 파란 윤곽선이 보인다
- 기사가 벽에서 나오면 구멍이 0.1초 남짓 동안 부드럽게 메워진다
- 끔/켬 나란히에서 왼쪽은 기사가 가려지고 오른쪽은 보인다

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

## 원리
- 벽 · 기둥 상자마다 「비울 정도 fade(0~1)」를 정점 속성(aFade)으로 둔다.
- 매 장면 카메라에서 주인공 발(0.3) · 가슴(1.1) · 머리(1.75)로 광선을 쏴 0.16 키운 상자와 주인공보다 앞에서 만나면 목표 1, 아니면 0 — fade 는 0.11초 시간 상수로 따라간다.
- 조각 셰이더: 주인공 화면 자리에서의 거리(세로는 0.78 배)로 원 안(inside)을 구하고, 주인공보다 카메라 쪽(front)인 조각만 k = fade × inside × front.
- 바이어 4×4 문턱이 남길 비율(keep)보다 크면 discard — 반투명 방식은 66% 만 비우고, 둥글게 비우기는 다 비운다. 투명 정렬 문제 없이 깊이도 그대로.
- 주인공은 두 번 더 그린다: 벽보다 뒤인 곳만(GreaterDepth) 파란 가산 윤곽 → 그다음 보통으로.

## 핵심 코드 — 주인공 둘레 화면 원 안을 바이어 점무늬로 비우기 (벽 재질에 끼워 넣기)
(발췌: demos/demosDiablo.ts CUT_FH · CUT_FB · cutify() · updateOcc() 를 정리)
```glsl
// TS 쪽 (매 장면):
//  updateOcc — 상자마다 카메라 → 주인공 (발 0.3 · 가슴 1.1 · 머리 1.75) 광선이 0.16 키운 상자와
//              주인공보다 앞에서 만나면 목표 1. fade += (목표 − fade) × (1 − exp(−dt / 0.11)) → aFade 속성
//  uHero = (주인공 화면 픽셀 x, y, 시점 깊이 − 0.3), uRad = 화면 반경 0.22 × 화면 높이, uMode 0 끔 · 1 반투명 · 2 비우기
//  cutify(m): onBeforeCompile 로 아래를 '#include <clipping_planes_fragment>' 뒤에 넣는다
uniform vec3 uHero; uniform float uRad; uniform float uMode;
varying float vFade;                                // 정점 속성 aFade
float bayer2(vec2 a){ a = floor(a); return fract(dot(a, vec2(0.5, a.y * 0.75))); }
float bayer4(vec2 a){ return bayer2(0.5 * a) * 0.25 + bayer2(a); }

void cutOccluder(){
  if (uMode > 0.5 && vFade > 0.002) {
    vec2 d = (gl_FragCoord.xy - uHero.xy) / uRad;
    float rr = length(d * vec2(1.0, 0.78));
    float inside = 1.0 - smoothstep(0.62, 1.0, rr);   // 주인공 둘레 화면 원
    float front = step(vViewPosition.z, uHero.z);     // 주인공보다 카메라 쪽만
    float k = vFade * inside * front;
    float keep = uMode < 1.5 ? 1.0 - k * 0.66 : 1.0 - k;
    if (bayer4(gl_FragCoord.xy) >= keep - 0.001) discard;
  }
}
// 벽 너머 윤곽: 주인공 층만 MeshBasicMaterial({ depthFunc: THREE.GreaterDepth, depthWrite: false,
//   transparent: true, opacity: 0.75, blending: THREE.AdditiveBlending }) 로 한 번, 그다음 보통으로 한 번
```

## 흔한 실수 · 확인 목록
- [ ] **벽을 transparent 반투명 재질로 바꾸면 그림자 · 정렬이 엉킨다** — 불투명 그대로 두고 바이어 점무늬로 discard — 깊이가 살아 있어 정렬 문제가 없다.
- [ ] **광선 하나(가슴)만 쏘면 발이나 머리만 가린 벽을 놓친다** — 발 · 가슴 · 머리 세 높이로 쏜다.
- [ ] **주인공 뒤쪽 벽까지 비우면 방이 뻥 뚫려 보인다** — 시점 깊이를 비교해 주인공보다 카메라 쪽 조각만 비운다.
- [ ] **fade 를 바로 0/1 로 바꾸면 경계에서 깜빡인다** — 0.11초 시간 상수로 부드럽게 따라가게.

## 완성 기준 체크리스트
- [ ] 기사가 벽 뒤로 걸어 들어가면 그 벽만 기사 둘레가 둥글게 비어 기사가 보인다
- [ ] 벽에 살짝 가린 순간엔 벽 너머로 파란 윤곽선이 보인다
- [ ] 기사가 벽에서 나오면 구멍이 0.1초 남짓 동안 부드럽게 메워진다
- [ ] 끔/켬 나란히에서 왼쪽은 기사가 가려지고 오른쪽은 보인다

## 이 기술 정보
- id: `i488` · 분류: 게임 시스템 · AI › 3D 게임 기본기 · 3D · 난이도 보통 · 폰 부담 가벼움 (폰 OK) — 상자마다 광선 3개 검사 + 셰이더 몇 줄. 주인공 윤곽 때문에 주인공만 한 번 더 그린다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i488
- 쓰면 좋을 때: 위에서 비스듬히 내려다보는 쿼터뷰 · 3인칭에서 벽이 주인공을 가릴 때 / 방 · 건물이 촘촘한 지도
- 쓰지 말 때: 카메라를 당겨 해결되는 좁은 3인칭 — 카메라 충돌(i481) / 벽 전체를 반투명 재질로 바꾸기 — 투명 정렬이 엉키고 그림자가 사라진다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### makeOccluder — `src/demos/demosDiablo.ts:2898`
```ts
function makeOccluder(): Scene3D {
  const st = makeStage();
  const hud = st.hud;
  const tag = hud.label('', 0.03, 0.95, 0, 1, 0.6, '#ffcf4a', '#22180a');
  const foot = hud.label('', 0.5, 0.04, 0.5, 0, 0.52, 'rgba(10,8,14,0.7)', '#ffffff');
  const la = hud.label('끔', 0.25, 0.95, 0.5, 1, 0.6, 'rgba(255,255,255,0.16)', 'rgba(255,255,255,0.85)');
  const lb = hud.label('켬', 0.75, 0.95, 0.5, 1, 0.6, '#ffcf4a', '#22180a');
  tag.visible = foot.visible = la.visible = lb.visible = false;
  let mode = 2;
  let sil = true;
  let rad = 0.22;
  let split = false;
  let walk = true;
  let hidden = 0;
  return {
    scene: st.scene,
    camera: st.cam,
    update(t, dt) {
      hidden = st.step(t, dt, walk);
    },
    render(r, w, h) {
      if (!st.ready(r, w, h)) return;
      if (split) {
        const hw = Math.floor(w / 2);
        st.draw(r, 0, 0, hw, h, 0, false, rad);
        st.draw(r, hw, 0, w - hw, h, mode || 2, sil, rad);
        r.setViewport(0, 0, w, h);
      } else st.draw(r, 0, 0, w, h, mode, sil && mode > 0, rad);
      tag.visible = !split;
      la.visible = lb.visible = split;
      foot.visible = true;
      tag.set(`가리는 벽: ${CUT_NAME[mode]}${sil && mode > 0 ? ' + 윤곽선' : ''}`);
      foot.set(hidden ? `카메라 → 기사 사이 ${hidden}칸 · 기사 둘레만 비움` : '가리는 것 없음 — 벽 그대로');
      hud.draw(r, w, h);
    },
    controls: [
      { type: 'button', label: '방식 바꾸기 (끔 → 점무늬 반투명 → 둥글게 비우기)', on: () => (mode = (mode + 1) % 3) },
      { type: 'toggle', label: '벽 너머 기사 윤곽선', value: true, on: (v) => (sil = v) },
      { type: 'range', label: '비우는 화면 반경', min: 0.1, max: 0.42, step: 0.01, value: rad, on: (v) => (rad = v) },
      { type: 'toggle', label: '끔 · 켬 나란히', value: false, on: (v) => (split = v) },
      { type: 'toggle', label: '기사 걷기', value: true, on: (v) => (walk = v) },
    ] as Control[],
    dispose() {
      st.dispose();
    },
  };
}
```

## 관련 기술
- 먼저 알면 좋은 기술: [카메라가 벽을 뚫지 않게](https://ai-techstudio.web.app/ai/t/i481.md) `i481` · [쿼터뷰 던전 조명 (횃불 · 그림자 · 안개)](https://ai-techstudio.web.app/ai/t/i477.md) `i477`
- 다음에 해 볼 기술: [시야와 안개 (가 본 곳만 밝게)](https://ai-techstudio.web.app/ai/t/i487.md) `i487`
- 참고 문서: [Wikipedia — Ordered dithering](https://en.wikipedia.org/wiki/Ordered_dithering)
