# AI 꾸러미 — 방패 · 보호막 맞음 — Force field / energy shield shader
> 반투명 구에 프레넬 테두리 · 육각 무늬를 그리고 맞은 방향을 셰이더에 넘겨서, 맞은 곳에서 육각 물결이 퍼지고 금이 갔다 아무는 보호막을 만든다.  
> 견본: https://ai-techstudio.web.app/#t/i191

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

## 주문서

### 만들어 줘: 방패 · 보호막 맞음 — Force field / energy shield shader

#### 1. 목표
마법사 캐릭터를 감싸는 보호막을 만들어 줘 — 반투명 구 · 가장자리 빛 · 육각 무늬, 맞으면 그 자리에서 물결이 퍼지고 금이 갔다 아물게. 분위기는 푸른 SF 에너지.

#### 2. 핵심 기술 용어
- **Force field / energy shield shader** — 보호막 셰이더
- **Fresnel rim** — 가장자리일수록 밝게 (보는 각도)
- **Triplanar hexagon pattern** — 세 방향에서 투영한 육각 무늬
- **Hit ripple via uniform array** — 맞은 방향 · 시각을 배열로 넘겨 물결

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

#### 4. 조건
- 맞은 방향 · 시각은 uniform vec4 배열(4칸)로 — 맞을 때마다 재질을 새로 만들지 않는다
- 물결 · 금은 각도 거리(acos(P·H))로 — 구 위에서 고르게 퍼지게
- 금 계산(보로노이)은 맞은 곳 근처에서만 (조건문으로 건너뛰기)
- AdditiveBlending · side: DoubleSide · depthWrite: false
- 육각 무늬 · 금 가기 켬/끔, 세기 조절

#### 5. 완성 기준 (이게 보이면 성공)
- 불덩이가 사방에서 날아와 구에 닿으면 그 자리에서 육각 물결이 둥글게 퍼진다
- 맞은 곳에 하얀 점이 번쩍이고 금이 갔다가 1.3초 안에 아문다
- 「육각 무늬」를 끄면 테두리 빛과 물결만 남는다
- 동시에 4번 맞아도 물결이 각자 퍼진다

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

## 원리
- 가장자리 빛: fr = 1 − |n·v|, rim = fr³ — 구의 테두리만 밝고 가운데는 거의 투명.
- 육각 무늬: 구 위 점 P(정규화)를 yz · zx · xy 세 평면에 투영해 육각 거리 함수를 계산하고, |P|⁴ 비율로 섞는다(삼면 투영). 무늬 크기 K = 5.5.
- 맞은 방향 · 시각을 vec4 배열 4칸에 돌려 넣는다. 셰이더는 맞은 점과의 각도 거리 ang 를 재, ang = 나이 × 2.4 에 물결 띠를 그린다(1.6초).
- 물결이 지나가는 곳만 육각 무늬가 2.2배 밝아지고, 맞은 바로 그 자리엔 짧은 하얀 점(spot)이 번쩍인다.
- 금: 맞은 곳 0.75 rad 안에서 보로노이 경계(3D) 를 그려 1.3초 동안 금이 보였다 사라진다.

## 핵심 코드 — 보호막 조각 셰이더 (요지 — 육각 · 물결 · 금)
(발췌: demos/demosSkillB.ts i191 shMat 을 정리)
```glsl
uniform float uT; uniform vec4 uHit[4]; uniform vec3 uC; uniform float uHex, uCrack;
varying vec3 vN, vV, vP;
float hexD(vec2 p){ const vec2 s = vec2(1.0, 1.7320508);
  vec4 hC = floor(vec4(p, p - vec2(0.5, 1.0)) / s.xyxy) + 0.5;
  vec4 h = vec4(p - hC.xy * s, p - (hC.zw + 0.5) * s);
  vec2 g = dot(h.xy, h.xy) < dot(h.zw, h.zw) ? h.xy : h.zw;
  vec2 a = abs(g); return 0.5 - max(dot(a, s * 0.5), a.x); }
// float vorEdge(vec3 x) — 3D 보로노이 경계 거리 (견본 참고, 27칸 두 번)
void main(){
  float fr = 1.0 - abs(dot(normalize(vN), normalize(vV)));
  float rim = pow(fr, 3.0);
  vec3 P = normalize(vP);
  vec3 w = pow(abs(P), vec3(4.0)); w /= (w.x + w.y + w.z);          // 삼면 투영 비율
  float e = hexD(P.yz * 5.5) * w.x + hexD(P.zx * 5.5) * w.y + hexD(P.xy * 5.5) * w.z;
  float hex = 1.0 - smoothstep(0.0, 0.07, e);
  float ripple = 0.0, spot = 0.0, crack = 0.0, near = 0.0;
  for (int i = 0; i < 4; i++) {
    vec4 H = uHit[i]; float age = uT - H.w;                         // xyz = 맞은 방향, w = 시각
    if (age < 0.0 || age > 1.6) continue;
    float ang = acos(clamp(dot(P, H.xyz), -1.0, 1.0));
    ripple += exp(-pow((ang - age * 2.4) * 6.0, 2.0)) * (1.0 - age / 1.6);
    near += exp(-ang * ang * 6.0) * (1.0 - age / 1.6);
    spot += exp(-ang * ang * 45.0) * max(0.0, 1.0 - age * 3.5);
    if (uCrack > 0.5) {
      float cz = smoothstep(0.75, 0.0, ang) * max(0.0, 1.0 - age / 1.3);
      if (cz > 0.01) crack += (1.0 - smoothstep(0.0, 0.035, vorEdge(P * 6.0 + H.xyz * 13.0))) * cz;
    }
  }
  float a = 0.035 + rim * 0.75 + hex * uHex * (0.05 + rim * 0.35 + ripple * 2.2 + near * 0.5) + ripple * 0.45 + spot * 2.0 + crack * 1.6;
  gl_FragColor = vec4(uC * a + vec3(1.0) * (spot * 1.6 + crack * 1.4 + ripple * hex * uHex * 0.8), 1.0);
}
// JS: 맞으면 hits[hi % 4].set(dir.x, dir.y, dir.z, time); hi++;  (dir = 중심에서 맞은 곳 방향)
```

## 흔한 실수 · 확인 목록
- [ ] **맞을 때마다 재질 · 메시를 새로 만들면 멈칫한다** — vec4 배열 4칸을 돌려 쓰고 값만 바꾼다. 재질이 그대로면 셰이더도 그대로다.
- [ ] **육각 무늬를 UV 로 깔면 구의 극에서 찌그러진다** — 구 위 점을 세 평면에 투영해 |P|⁴ 비율로 섞는다(삼면 투영).
- [ ] **보로노이 금을 화면 전체에서 계산하면 폰이 버벅인다** — 맞은 곳 0.75 rad 안(cz > 0.01)에서만 계산한다. 반복문이 많은 셰이더는 첫 컴파일도 길어지니 미리 데운다.

## 완성 기준 체크리스트
- [ ] 불덩이가 사방에서 날아와 구에 닿으면 그 자리에서 육각 물결이 둥글게 퍼진다
- [ ] 맞은 곳에 하얀 점이 번쩍이고 금이 갔다가 1.3초 안에 아문다
- [ ] 「육각 무늬」를 끄면 테두리 빛과 물결만 남는다
- [ ] 동시에 4번 맞아도 물결이 각자 퍼진다

## 이 기술 정보
- id: `i191` · 분류: 이펙트 · 연출 › 스킬 VFX (마법 · 미사일) · 3D · 난이도 어려움 · 폰 부담 보통 (폰 주의) — 구 하나지만 픽셀마다 육각 3번 + 맞은 곳 근처에서 보로노이(27칸 × 2번) 반복문. 맞은 곳 근처(cz > 0.01)에서만 돌린다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i191
- 쓰면 좋을 때: 방어 · 보호를 보여 줄 때 (실수 1번 막아 주기 등) / 「어디를 맞았나」를 보여 줘야 할 때
- 쓰지 말 때: 보호막이 화면 대부분을 덮을 때 — 픽셀마다 보로노이 반복문이 돌아 폰에서 무겁다. 금 가기를 끄거나 작게 / 평평한 벽 방어막 — 구가 아니라 판이면 각도 대신 거리로 물결을 계산한다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i191 견본 항목 — `src/demos/demosSkillB.ts:1943`
```ts
  i191: {
    kind: '3d',
    caption: '반투명 보호막에 불덩이가 맞으면 맞은 곳에서 육각 무늬 물결이 퍼지고 금이 갔다가 아문다',
    make() {
      const A = arena({ cam: [0, 6.0, 7.0], look: [0, 0.8, 0] });
      const hero = mage(A, 0x2a4a8a, [1.5, 2.2, 3.4]);
      A.scene.add(hero.g);
      const hits = [0, 1, 2, 3].map(() => new THREE.Vector4(0, 1, 0, -10));
      const shMat = new THREE.ShaderMaterial({
        uniforms: { uT: { value: 0 }, uHit: { value: hits }, uC: { value: new THREE.Color(0.35, 0.75, 1.4) }, uHex: { value: 1 }, uCrack: { value: 1 }, uGain: { value: 1 } },
        vertexShader: FRESNEL_V,
        fragmentShader: `uniform float uT; uniform vec4 uHit[4]; uniform vec3 uC; uniform float uHex; uniform float uCrack; uniform float uGain;
          varying vec3 vN; varying vec3 vV; varying vec2 vUv; varying vec3 vP;
          float hexD(vec2 p){ const vec2 s = vec2(1.0, 1.7320508); vec4 hC = floor(vec4(p, p - vec2(0.5, 1.0)) / s.xyxy) + 0.5;
            vec4 h = vec4(p - hC.xy * s, p - (hC.zw + 0.5) * s); vec2 g = dot(h.xy, h.xy) < dot(h.zw, h.zw) ? h.xy : h.zw;
            vec2 a = abs(g); return 0.5 - max(dot(a, s * 0.5), a.x); }
          vec3 h33(vec3 p){ p = vec3(dot(p, vec3(127.1, 311.7, 74.7)), dot(p, vec3(269.5, 183.3, 246.1)), dot(p, vec3(113.5, 271.9, 124.6))); return fract(sin(p) * 43758.5453); }
          float vorEdge(vec3 x){ vec3 n = floor(x), f = fract(x); vec3 mr = vec3(0.0); float md = 8.0;
            for (int k = -1; k <= 1; k++) for (int j = -1; j <= 1; j++) for (int i = -1; i <= 1; i++) {
              vec3 g = vec3(float(i), float(j), float(k)); vec3 r = g + h33(n + g) - f; float d = dot(r, r); if (d < md) { md = d; mr = r; } }
            md = 8.0;
            for (int k = -1; k <= 1; k++) for (int j = -1; j <= 1; j++) for (int i = -1; i <= 1; i++) {
              vec3 g = vec3(float(i), float(j), float(k)); vec3 r = g + h33(n + g) - f;
              if (dot(mr - r, mr - r) > 1e-5) md = min(md, dot(0.5 * (mr + r), normalize(r - mr))); }
            return md; }
          void main(){
            vec3 n = normalize(vN); vec3 v = normalize(vV);
            float fr = 1.0 - abs(dot(n, v));
            float rim = pow(fr, 3.0);
            vec3 P = normalize(vP);
            vec3 w = pow(abs(P), vec3(4.0)); w /= (w.x + w.y + w.z);
            float K = 5.5;
            float e = hexD(P.yz * K) * w.x + hexD(P.zx * K) * w.y + hexD(P.xy * K) * w.z;
            float hex = 1.0 - smoothstep(0.0, 0.07, e);
            float ripple = 0.0, spot = 0.0, crack = 0.0, near = 0.0;
            for (int i = 0; i < 4; i++) {
              vec4 H = uHit[i]; float age = uT - H.w;
              if (age < 0.0 || age > 1.6) continue;
              float ang = acos(clamp(dot(P, H.xyz), -1.0, 1.0));
              float r = age * 2.4;
              ripple += exp(-pow((ang - r) * 6.0, 2.0)) * (1.0 - age / 1.6);
              near += exp(-ang * ang * 6.0) * (1.0 - age / 1.6);
              spot += exp(-ang * ang * 45.0) * max(0.0, 1.0 - age * 3.5);
              if (uCrack > 0.5) {
                float cz = smoothstep(0.75, 0.0, ang) * max(0.0, 1.0 - age / 1.3);
                if (cz > 0.01) { float ve = vorEdge(P * 6.0 + H.xyz * 13.0); crack += (1.0 - smoothstep(0.0, 0.035, ve)) * cz; }
              }
            }
            float a = 0.035 + rim * 0.75 + hex * uHex * (0.05 + rim * 0.35 + ripple * 2.2 + near * 0.5) + ripple * 0.45 + spot * 2.0 + crack * 1.6;
            vec3 col = uC * a + vec3(1.0) * (spot * 1.6 + crack * 1.4 + ripple * hex * uHex * 0.8);
            gl_FragColor = vec4(col * uGain, 1.0);
            ${TONE} }`,
        transparent: true,
        depthWrite: false,
        side: THREE.DoubleSide,
        blending: THREE.AdditiveBlending,
      });
      const RAD = 1.45;
      const CEN: V3 = [0, 0.95, 0];
      const shield = new THREE.Mesh(new THREE.SphereGeometry(RAD, 64, 40), shMat);
      shield.position.set(CEN[0], CEN[1], CEN[2]);
      A.scene.add(shield);
      const floorRing = decal(A.tx.ring(), 3.3, C(0x5ab0ff, 1.2), 0.7);
      const floorGlow = decal(A.tx.glow(), 4.2, C(0x3a80ff, 1), 0.5);
      A.scene.add(floorRing, floorGlow);
      const hitL = new Flash(0x8ac8ff, 70, 6);
      A.scene.add(hitL.light);
      interface Shot { p: V3; v: V3; live: boolean; trail: V3[] }
      const shots: Shot[] = [];
      let hi = 0, nextAuto = 0.3, clock = 0, gain = 1;
      const FIRE = C(0xffa040, 2.5);
      const fire = (): void => {
        const a = Math.random() * TAU;
        const p: V3 = [Math.cos(a) * 6, rr(0.7, 1.8), Math.sin(a) * 6];
        const tg: V3 = add3(CEN, sc3(randDir(), 0.3));
        shots.push({ p, v: sc3(norm3(sub3(tg, p)), 9), live: true, trail: [] });
      };
      const fAcc = { acc: 0 };
      return base3d(A, (t, dt) => {
        clock = t;
        if (t > nextAuto) {
          fire();
          nextAuto = t + rr(0.55, 0.95);
        }
        shMat.uniforms.uT!.value = t;
        for (const s of shots) {
          if (s.live) {
            s.p = add3(s.p, sc3(s.v, dt));
            s.trail.unshift(s.p);
            if (s.trail.length > 10) s.trail.pop();
            orbGlow(A.fx, s.p, FIRE, 0.3, 1);
            for (let i = rate(fAcc, 40, dt); i > 0; i--) A.fx.emit({ p: s.p, v: sc3(randDir(), 0.4), life: rr(0.2, 0.4), size: 0.3, size2: 0.05, c: FIRE, c2: C(0xff3000, 0.8), a: 0.6 });
            const rel = sub3(s.p, CEN);
            if (len3(rel) <= RAD) {
              s.live = false;
              const d = norm3(rel);
              const hp = add3(CEN, sc3(d, RAD));
              hits[hi % 4]!.set(d[0], d[1], d[2], t);
              hi++;
              sparks(A.st, hp, { n: 22, c: C(0xffe0b0, 3), c2: FIRE, speed: 6, life: 0.4, grav: 6, dir: d, cone: 0.5 });
              sparks(A.st, hp, { n: 12, c: C(0xc8e8ff, 3), c2: C(0x4aa0ff, 1), speed: 4, life: 0.35, grav: 3, dir: d, cone: 0.3 });
              flashAt(A.fx, hp, 2.2, C(0xbfe0ff, 2));
              hitL.kick(hp, gain);
              A.shake(0.05);
            }
          } else if (s.trail.length) s.trail.pop();
          const n = s.trail.length;
          for (let i = 0; i < n - 1; i++) {
            const k = 1 - i / (n - 1);
            A.st.seg(s.trail[i + 1]!, s.trail[i]!, 0.15 * k + 0.015, FIRE, 0.7 * k);
          }
        }
        for (let i = shots.length - 1; i >= 0; i--) if (!shots[i]!.live && !shots[i]!.trail.length) shots.splice(i, 1);
        floorRing.material.opacity = 0.5 + hitL.lv * 0.5;
        shMat.uniforms.uGain!.value = gain;
        hitL.tick(dt);
        A.fx.step(dt);
        A.st.step(dt);
        A.tickCam(dt);
      }, [
        { type: 'button', label: '맞히기', on: () => { fire(); nextAuto = clock + 1.2; } },
        { type: 'toggle', label: '육각 무늬', value: true, on: (v) => (shMat.uniforms.uHex!.value = v ? 1 : 0) },
        { type: 'toggle', label: '금 가기', value: true, on: (v) => (shMat.uniforms.uCrack!.value = v ? 1 : 0) },
        { type: 'range', label: '세기', min: 0.4, max: 2, step: 0.1, value: gain, on: (v) => (gain = v) },
      ]);
    },
  }
```

## 관련 기술
- 같은 방법으로 만든 효과 예시: [에너지 방패](https://ai-techstudio.web.app/ai/t/i62.md) `i62`
- 먼저 알면 좋은 기술: [비전 미사일 (유도 탄)](https://ai-techstudio.web.app/ai/t/i185.md) `i185`
- 다음에 해 볼 기술: [시간 정지 · 왜곡 구](https://ai-techstudio.web.app/ai/t/i195.md) `i195`
