# AI 꾸러미 — 엑스레이 · 반투명 (프레넬) — pow
> 가장자리만 빛나고 속이 비침 — pow(1 − N·V, p)  
> 견본: https://ai-techstudio.web.app/#t/i42

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

## 주문서

### 만들어 줘: 엑스레이 · 반투명 (프레넬) — pow

#### 1. 목표
겉껍질 속 부품 보기에 「엑스레이 · 반투명 (프레넬)」 기술을 깔끔한 기본 느낌으로 넣어 줘. — 가장자리만 빛나고 속이 비침 — pow(1 − N·V, p)

#### 2. 핵심 기술 용어
- **pow**

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

#### 4. 조건
- 한 프레임 16ms 안 (폰 포함) — 무거운 계산은 처음에 한 번 또는 여러 프레임에 나눠서
- 새 재질 · 셰이더는 첫 화면 전에 미리 컴파일 (renderer.compileAsync)
- 화면 가장자리 흐림 · 비네트로 얼버무리지 않기
- 외부 라이브러리 추가 없이 three.js 와 three/examples 만

#### 5. 완성 기준 (이게 보이면 성공)
- 견본처럼 「가장자리만 빛나고 속이 비침 — pow(1 − N·V, p)」 이(가) 첫 화면에서 바로 보인다
- 켬/끔(또는 전/후) 비교 단추로 이 기술이 만드는 차이를 확인할 수 있다
- 중요한 값 1~3개를 슬라이더로 바꿔 볼 수 있다
- 콘솔 오류 0, 폰 폭(390px)에서도 동작한다

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

## 원리
- 가장자리만 빛나고 속이 비침 — pow(1 − N·V, p)


## 흔한 실수 · 확인 목록
- (아직 정리된 것이 없어요)

## 완성 기준 체크리스트
- [ ] 견본처럼 「가장자리만 빛나고 속이 비침 — pow(1 − N·V, p)」 이(가) 첫 화면에서 바로 보인다
- [ ] 켬/끔(또는 전/후) 비교 단추로 이 기술이 만드는 차이를 확인할 수 있다
- [ ] 중요한 값 1~3개를 슬라이더로 바꿔 볼 수 있다
- [ ] 콘솔 오류 0, 폰 폭(390px)에서도 동작한다

## 이 기술 정보
- id: `i42` · 분류: 물리 · 수학 원리 › 원리 설명 · 구조 · 3D · 난이도 쉬움 · 폰 부담 보통 (폰 주의)
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i42
- 쓰면 좋을 때: 쓰면 좋을 곳 — 겉껍질 속 부품 보기

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### splitLabels — `src/demos/demosStructure.ts:179`
```ts
function splitLabels(g: CanvasRenderingContext2D, w: number, h: number, left: string, right: string): void {
  const s = fsz(w, h, 0.9);
  g.fillStyle = 'rgba(255,255,255,0.75)';
  g.fillRect(w / 2 - 1, 0, 2, h);
  pill(g, s * 0.6, s * 0.6, left, s, 'rgba(30,40,80,0.7)');
  pill(g, w / 2 + s * 0.6, s * 0.6, right, s, 'rgba(255,120,60,0.9)');
}
```

### i42 견본 항목 — `src/demos/demosStructure.ts:4458`
```ts
  i42: {
    kind: '3d',
    caption: '왼쪽 보통 껍질 · 오른쪽 엑스레이 — 가장자리만 빛나고(pow(1 − N·V, p)) 속 톱니 · 전지 · 칩이 비쳐 보여요',
    make() {
      const mk = (xray: boolean, p: { value: number }): THREE.Scene => {
        const scene = darkScene();
        const inner = new THREE.Group();
        const gear = new THREE.Mesh(gearGeo(14, 0.07, 0.12), std(0xffc23a, 0.3, 0.7));
        gear.position.set(-0.12, 0.25, 0);
        gear.name = 'gear';
        const gear2 = new THREE.Mesh(gearGeo(9, 0.07, 0.12), std(0xff7a59, 0.3, 0.6));
        gear2.position.set(0.36, 0.78, 0);
        gear2.name = 'gear2';
        const bat = new THREE.Mesh(new THREE.CylinderGeometry(0.18, 0.18, 0.6, 20), std(0x4aa8ff, 0.4, 0.3));
        bat.position.set(-0.3, -0.45, 0);
        bat.rotation.z = Math.PI / 2.4;
        const chip = new THREE.Mesh(new RoundedBoxGeometry(0.45, 0.08, 0.35, 2, 0.02), std(0x2fae6a, 0.5));
        chip.position.set(0.3, -0.4, 0.05);
        chip.rotation.x = 0.4;
        const spring = new THREE.Mesh(new THREE.TubeGeometry(new THREE.CatmullRomCurve3(Array.from({ length: 40 }, (_, i) => new THREE.Vector3(Math.cos(i * 0.9) * 0.1, -0.1 + i * 0.012, Math.sin(i * 0.9) * 0.1))), 120, 0.02, 6), std(0xd8dee8, 0.25, 0.9));
        spring.position.set(-0.45, 0.2, 0);
        inner.add(gear, gear2, bat, chip, spring);
        scene.add(inner);
        const shellGeo = new THREE.CapsuleGeometry(0.85, 0.7, 12, 40);
        const shell = xray
          ? new THREE.Mesh(
              shellGeo,
              new THREE.ShaderMaterial({
                uniforms: { p, col: { value: new THREE.Color(0x5ad7ff) } },
                vertexShader: 'varying vec3 vN; varying vec3 vV; void main(){ vec4 mv = modelViewMatrix * vec4(position,1.0); vN = normalize(normalMatrix * normal); vV = normalize(-mv.xyz); gl_Position = projectionMatrix * mv; }',
                fragmentShader: 'uniform float p; uniform vec3 col; varying vec3 vN; varying vec3 vV; void main(){ float f = pow(1.0 - abs(dot(normalize(vN), normalize(vV))), p); gl_FragColor = vec4(col * (0.2 + f * 1.8), clamp(0.04 + f, 0.0, 1.0)); }',
                transparent: true,
                depthWrite: false,
                blending: THREE.AdditiveBlending,
              }),
            )
          : new THREE.Mesh(shellGeo, std(0xeaeefa, 0.35, 0.1));
        shell.name = 'shell';
        shell.renderOrder = 3;
        scene.add(shell);
        scene.userData['inner'] = inner;
        return scene;
      };
      const P = { value: 2.2 };
      const A = mk(false, P);
      const B = mk(true, P);
      const cam = new THREE.PerspectiveCamera(32, 1, 0.1, 50);
      const hud = new Hud();
      return {
        scene: A,
        camera: cam,
        update(t) {
          for (const S of [A, B]) {
            S.getObjectByName('gear')!.rotation.z = t;
            S.getObjectByName('gear2')!.rotation.z = (-t * 14) / 9;
            (S.userData['inner'] as THREE.Group).rotation.y = Math.sin(t * 0.5) * 0.6;
            S.getObjectByName('shell')!.rotation.y = Math.sin(t * 0.5) * 0.6;
          }
        },
        render(r, w, h) {
          const tn = Math.tan((cam.fov * Math.PI) / 360);
          const d = Math.max(1.75 / tn, 1.2 / (tn * (w / 2 / h)));
          cam.position.set(0, 0.2, d);
          cam.lookAt(0, 0, 0);
          splitRender(r, w, h, A, B, cam);
          hud.draw(r, w, h, `${w}x${h}`, (g) => splitLabels(g, w, h, '보통 껍질', '엑스레이 (프레넬)'));
        },
        controls: [{ type: 'range', label: '가장자리 지수 p', min: 0.5, max: 6, step: 0.1, value: 2.2, on: (v) => (P.value = v) }],
        dispose() {
          disposeScene(A);
          disposeScene(B);
          hud.dispose();
        },
      };
    },
  }
```

## 관련 기술
- 이 효과를 만든 기술: [테두리 빛 (rim light, 프레넬)](https://ai-techstudio.web.app/ai/t/i10.md) `i10`
