# AI 꾸러미 — 에너지 방패
> 프레넬 + 육각 무늬 + 맞은 곳에서 퍼지는 고리  
> 견본: https://ai-techstudio.web.app/#t/i62

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

## 주문서

### 만들어 줘: 에너지 방패

#### 1. 목표
보상에 「에너지 방패」 기술을 깔끔한 기본 느낌으로 넣어 줘. — 프레넬 + 육각 무늬 + 맞은 곳에서 퍼지는 고리

#### 2. 핵심 기술 용어
- 에너지 방패 — 프레넬 + 육각 무늬 + 맞은 곳에서 퍼지는 고리

#### 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. 완성 기준 (이게 보이면 성공)
- 견본처럼 「프레넬 + 육각 무늬 + 맞은 곳에서 퍼지는 고리」 이(가) 첫 화면에서 바로 보인다
- 켬/끔(또는 전/후) 비교 단추로 이 기술이 만드는 차이를 확인할 수 있다
- 중요한 값 1~3개를 슬라이더로 바꿔 볼 수 있다
- 콘솔 오류 0, 폰 폭(390px)에서도 동작한다

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

## 원리
- 프레넬 + 육각 무늬 + 맞은 곳에서 퍼지는 고리


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

## 완성 기준 체크리스트
- [ ] 견본처럼 「프레넬 + 육각 무늬 + 맞은 곳에서 퍼지는 고리」 이(가) 첫 화면에서 바로 보인다
- [ ] 켬/끔(또는 전/후) 비교 단추로 이 기술이 만드는 차이를 확인할 수 있다
- [ ] 중요한 값 1~3개를 슬라이더로 바꿔 볼 수 있다
- [ ] 콘솔 오류 0, 폰 폭(390px)에서도 동작한다

## 이 기술 정보
- id: `i62` · 분류: 이펙트 · 연출 › 화려한 효과 (VFX) · 3D · 난이도 보통 · 폰 부담 보통 (폰 주의)
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i62
- 쓰면 좋을 때: 쓰면 좋을 곳 — 보상 · 보스 연출

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### shieldDemo — `src/demos/demosShader.ts:1682`
```ts
function shieldDemo(): Scene3D {
  const scene = new THREE.Scene();
  const bg = gradTex([
    [0, '#120c36'],
    [1, '#2c1f6a'],
  ]);
  scene.background = bg;
  scene.add(new THREE.HemisphereLight(0xcfe0ff, 0x302060, 1.1));
  const key = new THREE.DirectionalLight(0xffffff, 1.4);
  key.position.set(2, 4, 3);
  scene.add(key);
  const world = new THREE.Group();
  const floor = new THREE.Mesh(new THREE.CircleGeometry(3, 48), new THREE.MeshStandardMaterial({ color: 0x241c5a, roughness: 0.8 }));
  floor.rotation.x = -Math.PI / 2;
  floor.position.y = -1.25;
  world.add(floor);
  // 안의 친구
  const pal = new THREE.Group();
  const body = new THREE.Mesh(new THREE.SphereGeometry(0.55, 32, 24), new THREE.MeshStandardMaterial({ color: 0xfff4e8, roughness: 0.45 }));
  body.scale.y = 0.92;
  const eyeM = new THREE.MeshStandardMaterial({ color: 0x1c1a2e, roughness: 0.3 });
  const cheekM = new THREE.MeshStandardMaterial({ color: 0xff9ab0, roughness: 0.6 });
  const eyes: THREE.Mesh[] = [];
  for (const s of [-1, 1]) {
    const e = new THREE.Mesh(new THREE.SphereGeometry(0.07, 14, 10), eyeM);
    e.position.set(s * 0.18, 0.08, 0.5);
    eyes.push(e);
    const c = new THREE.Mesh(new THREE.SphereGeometry(0.06, 12, 8), cheekM);
    c.scale.set(1.3, 0.7, 0.5);
    c.position.set(s * 0.3, -0.06, 0.45);
    pal.add(e, c);
  }
  const ant = new THREE.Mesh(new THREE.SphereGeometry(0.07, 12, 8), new THREE.MeshBasicMaterial({ color: 0x6ff0ff }));
  ant.position.y = 0.72;
  const stalk = new THREE.Mesh(new THREE.CylinderGeometry(0.015, 0.015, 0.2, 6), eyeM);
  stalk.position.y = 0.58;
  pal.add(body, ant, stalk);
  pal.position.y = -0.55;
  world.add(pal);
  // 방패
  const hits = [0, 1, 2, 3].map(() => new THREE.Vector4(0, 1, 0, -10));
  const su = { uTime: { value: 0 }, uHits: { value: hits }, uCol: { value: new THREE.Color(0x3ad8ff) }, uHex: { value: 1 } };
  const shield = new THREE.Mesh(
    new THREE.SphereGeometry(1.3, 64, 48),
    new THREE.ShaderMaterial({
      uniforms: su,
      transparent: true,
      depthWrite: false,
      blending: THREE.AdditiveBlending,
      side: THREE.DoubleSide,
      vertexShader: /* glsl */ `varying vec3 vN; varying vec3 vW; varying vec3 vP; varying vec2 vUv;
        void main(){ vUv = uv; vP = position; vN = normalize(mat3(modelMatrix) * normal); vec4 w = modelMatrix * vec4(position, 1.0); vW = w.xyz; gl_Position = projectionMatrix * viewMatrix * w; }`,
      fragmentShader: /* glsl */ `uniform float uTime; uniform vec4 uHits[4]; uniform vec3 uCol; uniform float uHex;
        varying vec3 vN; varying vec3 vW; varying vec3 vP; varying vec2 vUv;
        float hexDist(vec2 p){ p = abs(p); return max(dot(p, normalize(vec2(1.0, 1.7320508))), p.x); }
        vec4 hexC(vec2 uv){ vec2 r = vec2(1.0, 1.7320508); vec2 h = r * 0.5; vec2 a = mod(uv, r) - h; vec2 b = mod(uv - h, r) - h; vec2 gv = dot(a, a) < dot(b, b) ? a : b; return vec4(gv, uv - gv); }
        void main(){
          vec3 V = normalize(cameraPosition - vW);
          float F = pow(1.0 - abs(dot(normalize(vN), V)), 2.4);
          vec4 hc = hexC(vUv * vec2(30.0, 15.0));
          float edge = smoothstep(0.4, 0.49, hexDist(hc.xy)) * uHex;
          float rn = fract(sin(dot(hc.zw, vec2(12.9898, 78.233))) * 43758.5453);
          float pulse = pow(0.5 + 0.5 * sin(uTime * 2.0 + rn * 6.28), 4.0);
          vec3 n = normalize(vP);
          float ring = 0.0; float fill = 0.0;
          for (int i = 0; i < 4; i++){
            vec4 H = uHits[i]; float age = uTime - H.w;
            if (age < 0.0 || age > 1.4) continue;
            float ang = acos(clamp(dot(n, H.xyz), -1.0, 1.0));
            ring += exp(-pow((ang - age * 2.4) * 7.0, 2.0)) * (1.0 - age / 1.4);
            fill += exp(-ang * 5.0) * max(0.0, 1.0 - age * 2.5);
          }
          float a = F * 0.95 + edge * (0.1 + 0.3 * F) + edge * pulse * 0.12 + ring * (0.35 + edge * 1.3) + fill * 0.9 + 0.035;
          vec3 col = uCol * a + vec3(1.0) * (ring * edge * 0.5 + fill * 0.6);
          gl_FragColor = vec4(col, 1.0);
        }`,
    }),
  );
  world.add(shield);
  // 날아오는 공
  const shots = [0, 1, 2].map((i) => {
    const m = new THREE.Mesh(new THREE.SphereGeometry(0.08, 12, 8), new THREE.MeshBasicMaterial({ color: 0xffa23a }));
    const glow = new THREE.Mesh(new THREE.SphereGeometry(0.16, 12, 8), new THREE.MeshBasicMaterial({ color: 0xff7a1a, transparent: true, opacity: 0.35, blending: THREE.AdditiveBlending, depthWrite: false }));
    m.add(glow);
    world.add(m);
    return { m, dir: new THREE.Vector3(), s: 0, wait: i * 0.55 };
  });
  let seed = 3;
  const rnd = (): number => ((seed = (seed * 16807) % 2147483647) / 2147483647);
  const respawn = (s: (typeof shots)[number]): void => {
    const a = rnd() * Math.PI * 2;
    const y = (rnd() - 0.3) * 0.9;
    s.dir.set(Math.cos(a), y, Math.sin(a) * 0.6 + 0.6).normalize();
    s.s = 4.5;
  };
  shots.forEach(respawn);
  scene.add(world);
  let slot = 0;
  let rate = 1;
  const cam = new THREE.PerspectiveCamera(40, 1, 0.1, 50);
  cam.position.set(0, 0.7, 5.0);
  cam.lookAt(0, -0.1, 0);
  return {
    scene,
    camera: cam,
    update(t, dt) {
      su.uTime.value = t;
      pal.position.y = -0.55 + Math.sin(t * 2) * 0.05;
      const blink = (t % 3.2) < 0.12 ? 0.15 : 1;
      for (const e of eyes) e.scale.y = blink;
      for (const s of shots) {
        if (s.wait > 0) {
          s.wait -= dt;
          s.m.visible = false;
          continue;
        }
        s.m.visible = true;
        s.s -= dt * 3.2 * rate;
        if (s.s <= 1.3) {
          const h = hits[slot]!;
          h.set(s.dir.x, s.dir.y, s.dir.z, t);
          slot = (slot + 1) % 4;
          respawn(s);
          s.wait = 0.3 + rnd() * 0.6;
        }
        s.m.position.copy(s.dir).multiplyScalar(s.s);
      }
      shield.rotation.y = t * 0.1;
    },
    controls: [
      { type: 'range', label: '공 빠르기', min: 0.2, max: 3, step: 0.05, value: 1, on: (v) => (rate = v) },
      { type: 'toggle', label: '육각 무늬', value: true, on: (v) => (su.uHex.value = v ? 1 : 0) },
    ],
    dispose() {
      freeAll(world);
      bg.dispose();
    },
  };
}
```

### i62 견본 항목 — `src/demos/demosShader.ts:2391`
```ts
  i62: { kind: '3d', caption: '프레넬 테두리 · 육각 무늬 방패 — 공이 맞은 곳에서 고리가 퍼져요', make: () => shieldDemo() }
```

## 관련 기술
- 이 효과를 만든 기술: [방패 · 보호막 맞음](https://ai-techstudio.web.app/ai/t/i191.md) `i191`
