# AI 꾸러미 — 가짜 방 창문 (interior mapping)
> 평평한 벽의 창 칸마다 눈길을 상자 모양 가짜 방 속으로 쏘아 천장 · 바닥 · 옆벽 · 뒷벽 중 먼저 닿는 곳을 칠해, 모델 없이 창마다 진짜 방이 보이는 건물을 만든다.  
> 견본: https://ai-techstudio.web.app/#t/i231

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

## 주문서

### 만들어 줘: 가짜 방 창문 (interior mapping)

#### 1. 목표
밤의 아파트 건물의 벽에 가짜 방 창문(interior mapping)을 넣어 줘 — 창마다 광선을 방 상자에 쏴 천장 · 바닥 · 벽 · 가구가 보이게. 분위기는 불 켜진 따뜻한 밤.

#### 2. 핵심 기술 용어
- **Interior mapping** — 가짜 방 창문 — 창 안 방을 셰이더로 계산
- **Ray–box intersection (slab method)** — 광선과 상자 벽 사이 거리 비교
- **Object-space camera position** — 카메라 위치를 물체 좌표로 바꿔 쏘기

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

#### 4. 조건
- 카메라 위치는 inverse(modelMatrix) 로 물체 좌표로 바꿔서 — 건물이 돌거나 옮겨져도 맞게
- 방향 성분이 0 이면 나눗셈이 터지므로 1e-4 로 막기
- 방마다 다른 값은 칸 번호 hash 로 (색 · 가구 종류 · 불 켜짐 · TV 빛)
- 불은 시간을 아주 느리게(0.12) 끊어 가끔 바뀌게
- 「방 깊이」 0.2~2 · 「가구 · 커튼」 · 「방 불 켜기」 조절

#### 5. 완성 기준 (이게 보이면 성공)
- 카메라가 옆으로 움직이면 창 속 천장 · 바닥 · 옆벽이 원근에 맞게 넓어지고 좁아진다
- 방마다 벽 색 · 가구 · 커튼 · 불 켜짐이 다르고, 일부는 TV 처럼 푸르게 깜박인다
- 「방 깊이」를 바꾸면 방이 깊어지고, 「가구」를 끄면 빈 방이 된다

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

## 원리
- 벽 판의 물체 좌표를 1×1 칸(창 하나 = 방 하나)으로 나눈다. 칸 번호를 난수 열쇠로 써 방마다 색 · 가구 · 불 켜짐이 다르다.
- 카메라 위치를 물체 좌표로 바꾸고, 그 점에서 이 조각으로 향하는 방향 dir 을 구한다.
- 방 상자(가로 0~1 · 세로 0~1 · 깊이 0~ −D)의 옆벽 · 천장/바닥 · 뒷벽까지 거리 tw 셋을 구해 가장 작은 것이 처음 닿는 면이다.
- 닿은 면에 따라 색을 고르고(뒷벽 = 그림 액자, 바닥 = 마루 줄), 천장 등불까지 거리로 밝기를 준다.
- 중간 깊이에 판 하나를 더 두면 가구 · 커튼 실루엣, 위에 옅은 유리 반사를 더한다.

## 핵심 코드 — 창 칸마다 광선 → 방 상자의 첫 면 찾기
(발췌: demos/demosLook2.ts makeInterior() 의 frag (방 계산 부분))
```glsl
// 정점: vL = position; vCam = (inverse(modelMatrix) * vec4(cameraPosition, 1.0)).xyz;
uniform vec2 uSize; uniform float uDepth;
varying vec3 vL; varying vec3 vCam;
// ... main() 안 (hash · hsv · box 도구는 생략)
vec2 g = vL.xy + uSize * 0.5;                  // 벽 왼쪽 아래가 (0,0)
vec2 cell = floor(g); vec2 f = fract(g);       // 칸 = 방 하나
float id = hash(cell + 3.1);                   // 방마다 다른 열쇠
vec3 dir = normalize(vL - vCam);
dir.x = abs(dir.x) < 1e-4 ? 1e-4 : dir.x;      // 0 나눗셈 막기
dir.y = abs(dir.y) < 1e-4 ? 1e-4 : dir.y;
vec3 o = vec3(f, 0.0);                         // 창 유리 위의 점
float D = uDepth;
vec3 tw = vec3(((dir.x > 0.0 ? 1.0 : 0.0) - o.x) / dir.x,   // 옆벽까지
               ((dir.y > 0.0 ? 1.0 : 0.0) - o.y) / dir.y,   // 천장 · 바닥까지
               (-D - o.z) / dir.z);                         // 뒷벽까지
float tt = min(min(tw.x, tw.y), tw.z);         // 처음 닿는 면
vec3 hp = o + dir * tt;
vec3 wc = hsv(fract(id * 3.7), 0.25 + 0.3 * hash(cell + 9.0), 0.62);
vec3 room;
if (tt == tw.z)      room = wc * 0.95;                                  // 뒷벽
else if (tt == tw.y) room = dir.y > 0.0 ? vec3(0.85, 0.83, 0.8)        // 천장
                          : vec3(0.45, 0.28, 0.16) * (0.8 + 0.2 * step(0.5, fract(hp.x * 6.0))); // 마루
else                 room = wc * 0.72;                                  // 옆벽
float ld = length(hp - vec3(0.5, 0.97, -D * 0.5));                      // 천장 등불
room *= vec3(1.25, 1.0, 0.72) * (0.35 + 0.9 / (1.0 + ld * ld * 2.5));
// 가구: 깊이 -D*0.55 판까지 tf 가 tt 보다 작으면 그 판의 실루엣을 칠한다
```

## 흔한 실수 · 확인 목록
- [ ] **카메라 위치를 세계 좌표 그대로 쓰면 건물을 돌리면 방이 틀어진다** — 정점에서 inverse(modelMatrix) 로 물체 좌표 카메라를 구해 넘긴다.
- [ ] **창을 정면에서 보면 dir.x · dir.y 가 0 이 되어 검은 점이 생긴다** — 1e-4 로 막는다.
- [ ] **모든 방이 똑같으면 오히려 가짜 티가 난다** — 칸 번호 hash 로 벽 색 · 가구 · 커튼 · 불 · TV 빛을 다르게.

## 완성 기준 체크리스트
- [ ] 카메라가 옆으로 움직이면 창 속 천장 · 바닥 · 옆벽이 원근에 맞게 넓어지고 좁아진다
- [ ] 방마다 벽 색 · 가구 · 커튼 · 불 켜짐이 다르고, 일부는 TV 처럼 푸르게 깜박인다
- [ ] 「방 깊이」를 바꾸면 방이 깊어지고, 「가구」를 끄면 빈 방이 된다

## 이 기술 정보
- id: `i231` · 분류: 그래픽 · 셰이더 › 재질 · 그림체 · 3D · 난이도 어려움 · 폰 부담 보통 (폰 주의) — 창 조각마다 나눗셈 · 비교 몇 번과 난수 몇 개. 벽 두 장에 장면 전체가 들어 있어 물체 수는 아주 적다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i231
- 쓰면 좋을 때: 멀리 보이는 건물이 많아 방을 하나하나 모델링할 수 없을 때 / 창이 「납작한 그림」이 아니라 움직이면 속이 보여야 할 때
- 쓰지 말 때: 방 안에 들어가야 하는 게임 — 진짜 모델이 필요하다 / 창 몇 개뿐인 집 — 방 상자 모델 몇 개가 더 쉽다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### makeInterior — `src/demos/demosLook2.ts:1931`
```ts
function makeInterior(): Scene3D {
  const scene = new THREE.Scene();
  scene.background = gradBg([
    [0, '#050816'],
    [0.7, '#141a3c'],
    [1, '#2c2850'],
  ]);
  const cam = new THREE.PerspectiveCamera(40, 1.6, 0.1, 80);
  const uni = { uDepth: { value: 0.9 }, uFurn: { value: 1 }, uOn: { value: 1 }, uTime: { value: 0 } };
  const vert = /* glsl */ `
    varying vec3 vL; varying vec3 vCam;
    void main(){ vL = position; vCam = (inverse(modelMatrix) * vec4(cameraPosition, 1.0)).xyz; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`;
  const frag = /* glsl */ `
    uniform vec2 uSize; uniform float uDepth, uFurn, uOn, uTime;
    varying vec3 vL; varying vec3 vCam;
    ${NOISE}
    vec3 hsv(float h, float s, float v){ vec3 k = clamp(abs(mod(h * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0); return v * mix(vec3(1.0), k, s); }
    float box(vec2 p, vec2 a, vec2 b){ return step(a.x, p.x) * step(p.x, b.x) * step(a.y, p.y) * step(p.y, b.y); }
    void main(){
      vec2 g = vL.xy + uSize * 0.5;
      vec2 cell = floor(g); vec2 f = fract(g);
      float id = hash(cell + 3.1);
      vec2 wa = vec2(0.14, 0.2), wb = vec2(0.86, 0.86);
      float inWin = box(f, wa, wb);
      float frame = box(f, wa - 0.03, wb + 0.03) - inWin;
      // 벽: 따뜻한 벽돌 결
      float rowL = step(0.92, fract(g.y * 4.0)) + step(0.95, fract(g.x * 2.0 + floor(g.y * 4.0) * 0.5)) * 0.6;
      vec3 wall = vec3(0.22, 0.16, 0.16) * (0.85 + 0.25 * hash(floor(g * vec2(2.0, 4.0)))) * (1.0 - rowL * 0.35);
      float lit = step(0.3, hash(cell * 1.7 + floor(uTime * 0.12 + id * 9.0))) * uOn;
      // 창 아래로 새는 빛
      float spill = lit * box(f, vec2(0.1, 0.0), vec2(0.9, 0.2)) * (1.0 - f.y / 0.2) * 0.35;
      vec3 col = wall * (0.5 + spill * 2.0) + vec3(1.0, 0.7, 0.4) * spill * 0.12;
      col = mix(col, vec3(0.06, 0.06, 0.08), frame);
      if (inWin > 0.5){
        vec3 dir = normalize(vL - vCam);
        dir.x = abs(dir.x) < 1e-4 ? 1e-4 : dir.x; dir.y = abs(dir.y) < 1e-4 ? 1e-4 : dir.y;
        vec3 o = vec3(f, 0.0);
        float D = uDepth;
        vec3 tw = vec3(((dir.x > 0.0 ? 1.0 : 0.0) - o.x) / dir.x, ((dir.y > 0.0 ? 1.0 : 0.0) - o.y) / dir.y, (-D - o.z) / dir.z);
        float tt = min(min(tw.x, tw.y), tw.z);
        vec3 hp = o + dir * tt;
        vec3 wc = hsv(fract(id * 3.7), 0.25 + 0.3 * hash(cell + 9.0), 0.62);
        vec3 room;
        if (tt == tw.z){
          room = wc * 0.95;
          float pic = box(hp.xy, vec2(0.3 + id * 0.2, 0.5), vec2(0.55 + id * 0.2, 0.75));
          room = mix(room, hsv(fract(id * 11.0), 0.6, 0.85), pic * step(0.4, id));
        } else if (tt == tw.y){
          room = dir.y > 0.0 ? vec3(0.85, 0.83, 0.8) : vec3(0.45, 0.28, 0.16) * (0.8 + 0.2 * step(0.5, fract(hp.x * 6.0)));
        } else {
          room = wc * 0.72;
        }
        vec3 lamp = vec3(0.5, 0.97, -D * 0.5);
        float ld = length(hp - lamp);
        float shade = 0.35 + 0.9 / (1.0 + ld * ld * 2.5);
        if (uFurn > 0.5){
          float zf = -D * 0.55;
          float tf = (zf - o.z) / dir.z;
          if (tf < tt){
            vec2 fp = (o + dir * tf).xy;
            float k = floor(hash(cell + 1.3) * 3.0);
            float s = 0.0;
            if (k < 1.0){ float cx = 0.3 + 0.4 * id; s = max(step(length(fp - vec2(cx, 0.6)), 0.075), box(fp, vec2(cx - 0.09, 0.0), vec2(cx + 0.09, 0.5))); }
            else if (k < 2.0){ s = max(box(fp, vec2(0.18, 0.0), vec2(0.82, 0.22)), box(fp, vec2(0.18, 0.0), vec2(0.28, 0.38))); s = max(s, box(fp, vec2(0.72, 0.0), vec2(0.82, 0.38))); }
            else { s = max(box(fp, vec2(0.62, 0.0), vec2(0.74, 0.14)), step(length(fp - vec2(0.68, 0.28)), 0.12)); }
            if (s > 0.5){ room = mix(vec3(0.05, 0.04, 0.06), room * 0.25, 0.4); shade = 1.0; }
          }
          float cur = max(box(f, wa, vec2(0.24, 0.86)), box(f, vec2(0.76, 0.2), wb)) * step(0.5, hash(cell + 5.5));
          room = mix(room, wc * 0.9 * (0.75 + 0.25 * sin(f.x * 90.0)), cur * 0.92);
        }
        vec3 warm = vec3(1.25, 1.0, 0.72);
        vec3 tv = vec3(0.6, 0.8, 1.4) * (0.8 + 0.2 * sin(uTime * 13.0 + id * 40.0));
        vec3 lightC = id > 0.85 ? tv : warm;
        room *= lit > 0.5 ? lightC * shade : vec3(0.05, 0.06, 0.12);
        // 유리 반사
        float refl = 0.06 + 0.1 * smoothstep(0.0, 1.0, f.y + f.x * 0.3);
        col = room + vec3(0.3, 0.38, 0.6) * refl;
      }
      gl_FragColor = vec4(col, 1.0);
      ${OUT}
    }`;
  const mk = (w: number, h: number) => new THREE.ShaderMaterial({ uniforms: { ...uni, uSize: { value: new THREE.Vector2(w, h) } }, vertexShader: vert, fragmentShader: frag });
  const root = new THREE.Group();
  scene.add(root);
  const front = new THREE.Mesh(new THREE.PlaneGeometry(6, 7), mk(6, 7));
  front.position.set(0, 3.5, 0);
  const side = new THREE.Mesh(new THREE.PlaneGeometry(4, 7), mk(4, 7));
  side.rotation.y = Math.PI / 2;
  side.position.set(3, 3.5, -2);
  const roof = new THREE.Mesh(new THREE.BoxGeometry(6.3, 0.3, 4.3), new THREE.MeshLambertMaterial({ color: 0x241a20 }));
  roof.position.set(0, 7.1, -2);
  const ground = new THREE.Mesh(new THREE.PlaneGeometry(60, 40).rotateX(-Math.PI / 2), new THREE.MeshLambertMaterial({ color: 0x14141c }));
  scene.add(new THREE.HemisphereLight(0x5060a0, 0x101018, 1.2));
  root.add(front, side, roof, ground);
  // 별
  const sp = new Float32Array(300 * 3);
  const r0 = rng(3);
  for (let i = 0; i < 300; i++) {
    sp[i * 3] = (r0() - 0.5) * 80;
    sp[i * 3 + 1] = 6 + r0() * 30;
    sp[i * 3 + 2] = -30 - r0() * 5;
  }
  const sg = new THREE.BufferGeometry();
  sg.setAttribute('position', new THREE.BufferAttribute(sp, 3));
  root.add(new THREE.Points(sg, new THREE.PointsMaterial({ color: 0xdfe6ff, size: 1.6, sizeAttenuation: false })));
  return {
    scene,
    camera: cam,
    update(t) {
      uni.uTime.value = t;
      const s = Math.sin(t * 0.3);
      cam.position.set(s * 4.5 + 1.0, 3.6 + Math.sin(t * 0.21) * 1.4, 5.0);
      cam.lookAt(0.9, 3.6, -1);
    },
    controls: [
      { type: 'range', label: '방 깊이', min: 0.2, max: 2, step: 0.05, value: 0.9, on: (v) => (uni.uDepth.value = v) },
      { type: 'toggle', label: '가구 · 커튼 (깊이 판 하나 더)', value: true, on: (v) => (uni.uFurn.value = v ? 1 : 0) },
      { type: 'toggle', label: '방 불 켜기', value: true, on: (v) => (uni.uOn.value = v ? 1 : 0) },
    ],
    dispose() {
      freeAll(root);
      (scene.background as THREE.Texture).dispose();
    },
  };
}
```

### i231 견본 항목 — `src/demos/demosLook2.ts:4515`
```ts
  i231: { kind: '3d', caption: '벽은 평면 두 장뿐 — 창마다 광선을 방 상자에 쏴 천장 · 바닥 · 벽 · 가구를 계산, 움직이면 방 속이 보임', make: () => makeInterior() }
```

## 관련 기술
- 먼저 알면 좋은 기술: [시차 차폐 매핑 (POM)](https://ai-techstudio.web.app/ai/t/i230.md) `i230`
- 다음에 해 볼 기술: [빌보드 대역 (impostor)](https://ai-techstudio.web.app/ai/t/i232.md) `i232`
