# AI 꾸러미 — 홀로그램 · 주사선 셰이더 — Hologram shader
> 푸른 빛을 더하기 섞기로 그리고 가장자리 빛 · 흐르는 주사선 · 깜박임 · 가로 찢김을 얹어, 투사기 위에 뜬 홀로그램처럼 보이게 한다.  
> 견본: https://ai-techstudio.web.app/#t/i221

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

## 주문서

### 만들어 줘: 홀로그램 · 주사선 셰이더 — Hologram shader

#### 1. 목표
투사기 위에 뜬 입체 도형을 홀로그램 셰이더로 그려 줘 — 반투명 푸른빛 · 가장자리 빛 · 흐르는 주사선 · 깜박임 · 가끔 가로 찢김. 분위기는 어두운 SF 관제실.

#### 2. 핵심 기술 용어
- **Hologram shader** — 홀로그램 셰이더
- **Scanlines** — 흐르는 가로 주사선
- **Additive blending** — 더하기 섞기 — 겹칠수록 밝아짐, 깊이 쓰기 끔
- **Glitch (vertex slice offset)** — 높이 띠마다 정점을 옆으로 튀기는 찢김

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

#### 4. 조건
- ShaderMaterial + transparent · AdditiveBlending · depthWrite false · DoubleSide
- 주사선은 세계 높이(vW.y)로 — 물체가 돌아도 줄이 수평
- 깜박임 · 찢김은 시간을 칸으로 끊은 난수 (floor(t × 13)) — 매 프레임 깜박이지 않게
- 투사기 받침 · 빛 원뿔 · 솟는 빛 알갱이를 함께 두어 「투사된」 느낌
- 「홀로그램 켜기」 · 「주사선 촘촘함」 · 「깜박임」 · 「글리치」 조절

#### 5. 완성 기준 (이게 보이면 성공)
- 투사기 위의 매듭이 속이 비치는 푸른 빛으로 보이고, 윤곽이 가장 밝다
- 가는 주사선과 넓은 빛 띠가 위로 흐르고, 가끔 깜박이며 가로로 찢긴다
- 끄면 그냥 파란 재질로 바뀌어 차이가 분명하다

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

## 원리
- 빛 계산 없이 색만 내는 ShaderMaterial 을 더하기 섞기 · 깊이 쓰기 끔 · 양면으로 그린다 — 겹칠수록 밝아지는 빛 덩어리.
- 가장자리 빛: fr = (1 − |N·V|)² — 윤곽이 가장 밝다.
- 주사선: 세계 높이 × 촘촘함(46) − 시간 × 5 로 sin 을 5제곱해 가는 줄이 위로 흐른다. 넓은 빛 띠 하나도 천천히 올라간다.
- 깜박임: 시간을 13칸으로 끊은 난수가 0.8 을 넘으면 0.55 만큼 어둡게.
- 찢김: 정점 셰이더에서 높이를 16띠로 나눠, 난수에 걸린 띠만 x 로 0.14 만큼 흔든다.

## 핵심 코드 — 홀로그램 정점(찢김) · 조각(가장자리 · 주사선 · 깜박임)
(발췌: demos/demosLook2.ts makeHolo() 를 정리)
```ts
const uni = { uTime: { value: 0 }, uDensity: { value: 46 }, uFlicker: { value: 0.6 }, uGlitch: { value: 1 }, uColor: { value: new THREE.Color(0x3fd8ff) } };
const vert = [
  'uniform float uTime; uniform float uGlitch; varying vec3 vW; varying vec3 vN;',
  'float h1(float n){ return fract(sin(n) * 43758.5453); }',
  'void main(){',
  '  vec4 w = modelMatrix * vec4(position, 1.0);',
  '  float slice = floor(w.y * 16.0);',                       // 높이 띠
  '  float on = step(0.9, h1(slice * 7.13 + floor(uTime * 7.0))) * step(0.55, h1(floor(uTime * 3.0)));',
  '  w.x += on * uGlitch * 0.14 * sin(uTime * 60.0 + slice);', // 걸린 띠만 옆으로
  '  vW = w.xyz; vN = normalize(mat3(modelMatrix) * normal);',
  '  gl_Position = projectionMatrix * viewMatrix * w;',
  '}'].join(' ');
const frag = [
  'uniform float uTime, uDensity, uFlicker; uniform vec3 uColor; varying vec3 vW; varying vec3 vN;',
  'float h1(float n){ return fract(sin(n) * 43758.5453); }',
  'void main(){',
  '  vec3 V = normalize(cameraPosition - vW); vec3 N = normalize(vN);',
  '  float fr = pow(1.0 - clamp(abs(dot(N, V)), 0.0, 1.0), 2.0);',
  '  float sl = pow(sin(vW.y * uDensity - uTime * 5.0) * 0.5 + 0.5, 5.0);',
  '  float b = fract(vW.y * 0.42 - uTime * 0.32);',
  '  float band = smoothstep(0.0, 0.05, b) * (1.0 - smoothstep(0.05, 0.22, b));',
  '  float fl = 1.0 - uFlicker * (step(0.8, h1(floor(uTime * 13.0))) * 0.55 + 0.07 * sin(uTime * 95.0));',
  '  float a = (0.035 + fr * 1.2 + sl * 0.22 + band * 0.6) * fl;',
  '  gl_FragColor = vec4(uColor * a + vec3(0.7, 0.95, 1.0) * fr * fr * 0.7 * fl, 1.0);',
  '}'].join(' ');
const holo = new THREE.ShaderMaterial({
  uniforms: uni, vertexShader: vert, fragmentShader: frag,
  transparent: true, blending: THREE.AdditiveBlending, depthWrite: false, side: THREE.DoubleSide,
});
const knot = new THREE.Mesh(new THREE.TorusKnotGeometry(0.46, 0.15, 220, 28, 2, 3), holo);
knot.position.y = 1.2;
scene.add(knot);
function update(t: number) { uni.uTime.value = t; }
```

## 흔한 실수 · 확인 목록
- [ ] **깊이 쓰기를 켜 두면 뒷면 · 그물이 가려진다** — depthWrite: false 로. 더하기 섞기라 그리는 순서가 상관없다.
- [ ] **매 프레임 난수로 깜박이면 눈이 아프다** — floor(시간 × 13)처럼 칸으로 끊고, 어두워지는 칸도 20% 정도만.
- [ ] **빛 번짐(u20)과 겹치면 하얗게 날아간다** — 홀로그램 밝기를 번짐 문턱 근처로 맞추거나 번짐 세기를 낮춘다.

## 완성 기준 체크리스트
- [ ] 투사기 위의 매듭이 속이 비치는 푸른 빛으로 보이고, 윤곽이 가장 밝다
- [ ] 가는 주사선과 넓은 빛 띠가 위로 흐르고, 가끔 깜박이며 가로로 찢긴다
- [ ] 끄면 그냥 파란 재질로 바뀌어 차이가 분명하다

## 이 기술 정보
- id: `i221` · 분류: 그래픽 · 셰이더 › 재질 · 그림체 · 3D · 난이도 쉬움 · 폰 부담 가벼움 (폰 OK) — 조명 계산 없는 짧은 셰이더. 더하기 섞기라 겹친 넓이만큼 채우기 비용이 든다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i221
- 쓰면 좋을 때: 단서 · 미리 보기 · 통신처럼 「진짜 물건이 아닌」 것을 보여 줄 때 / 어두운 배경의 SF · 미래 장면
- 쓰지 말 때: 밝은 낮 배경 — 더하기 섞기가 묻혀 안 보인다 / 읽어야 하는 글씨 — 깜박임 · 찢김이 방해된다. 대신 깜박임 없는 패널

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### makeHolo — `src/demos/demosLook2.ts:293`
```ts
function makeHolo(): Scene3D {
  const scene = new THREE.Scene();
  scene.background = gradBg([
    [0, '#020611'],
    [0.6, '#06142c'],
    [1, '#0a1d3a'],
  ]);
  const cam = new THREE.PerspectiveCamera(36, 1.6, 0.1, 60);
  cam.position.set(0, 1.55, 5.0);
  cam.lookAt(0, 0.95, 0);
  scene.add(new THREE.HemisphereLight(0x6fa8ff, 0x0a0f1e, 0.9));
  const key = new THREE.PointLight(0x5ff3ff, 14, 8);
  key.position.set(0, 0.6, 0.8);
  scene.add(key);
  const dl = new THREE.DirectionalLight(0xffffff, 1.6);
  dl.position.set(2, 4, 3);
  scene.add(dl);

  const root = new THREE.Group();
  scene.add(root);
  // 바닥 (반사 느낌의 어두운 원판 + 격자)
  const floor = new THREE.Mesh(new THREE.CircleGeometry(4, 64).rotateX(-Math.PI / 2), new THREE.MeshStandardMaterial({ color: 0x07101f, metalness: 0.6, roughness: 0.45 }));
  root.add(floor);
  const grid = new THREE.GridHelper(8, 32, 0x1d4c7a, 0x0f2747);
  (grid.material as THREE.Material).transparent = true;
  (grid.material as THREE.Material).opacity = 0.45;
  grid.position.y = 0.002;
  root.add(grid);
  // 투사기 받침
  const base = new THREE.Mesh(new THREE.CylinderGeometry(0.72, 0.9, 0.24, 48), new THREE.MeshStandardMaterial({ color: 0x1b2438, metalness: 0.85, roughness: 0.32 }));
  base.position.y = 0.12;
  root.add(base);
  const lens = new THREE.Mesh(new THREE.CylinderGeometry(0.5, 0.5, 0.04, 48), new THREE.MeshBasicMaterial({ color: 0x9ffcff }));
  lens.position.y = 0.25;
  root.add(lens);
  const ringMat = new THREE.MeshBasicMaterial({ color: 0x38e8ff });
  const ring = new THREE.Mesh(new THREE.TorusGeometry(0.78, 0.025, 10, 64).rotateX(Math.PI / 2), ringMat);
  ring.position.y = 0.2;
  root.add(ring);

  const uni = { uTime: { value: 0 }, uDensity: { value: 46 }, uFlicker: { value: 0.6 }, uGlitch: { value: 1 }, uColor: { value: new THREE.Color(0x3fd8ff) } };
  const holoVert = /* glsl */ `
    uniform float uTime; uniform float uGlitch;
    varying vec3 vW; varying vec3 vN;
    float h1(float n){ return fract(sin(n) * 43758.5453); }
    void main(){
      vec4 w = modelMatrix * vec4(position, 1.0);
      float slice = floor(w.y * 16.0);
      float on = step(0.9, h1(slice * 7.13 + floor(uTime * 7.0))) * step(0.55, h1(floor(uTime * 3.0)));
      w.x += on * uGlitch * 0.14 * sin(uTime * 60.0 + slice);
      vW = w.xyz; vN = normalize(mat3(modelMatrix) * normal);
      gl_Position = projectionMatrix * viewMatrix * w;
    }`;
  const holoFrag = /* glsl */ `
    uniform float uTime, uDensity, uFlicker; uniform vec3 uColor;
    varying vec3 vW; varying vec3 vN;
    float h1(float n){ return fract(sin(n) * 43758.5453); }
    void main(){
      vec3 V = normalize(cameraPosition - vW);
      vec3 N = normalize(vN);
      float fr = pow(1.0 - clamp(abs(dot(N, V)), 0.0, 1.0), 2.0);
      float sl = pow(sin(vW.y * uDensity - uTime * 5.0) * 0.5 + 0.5, 5.0);
      float b = fract(vW.y * 0.42 - uTime * 0.32);
      float band = smoothstep(0.0, 0.05, b) * (1.0 - smoothstep(0.05, 0.22, b));
      float fl = 1.0 - uFlicker * (step(0.8, h1(floor(uTime * 13.0))) * 0.55 + 0.07 * sin(uTime * 95.0));
      float a = (0.035 + fr * 1.2 + sl * 0.22 + band * 0.6) * fl;
      vec3 col = uColor * a + vec3(0.7, 0.95, 1.0) * fr * fr * 0.7 * fl;
      gl_FragColor = vec4(col, 1.0);
    }`;
  const holoMat = new THREE.ShaderMaterial({ uniforms: uni, vertexShader: holoVert, fragmentShader: holoFrag, transparent: true, blending: THREE.AdditiveBlending, depthWrite: false, side: THREE.DoubleSide });
  const plainMat = new THREE.MeshStandardMaterial({ color: 0x4fb2ff, metalness: 0.2, roughness: 0.35 });
  const knot = new THREE.Mesh<THREE.BufferGeometry, THREE.Material>(new THREE.TorusKnotGeometry(0.46, 0.15, 220, 28, 2, 3), holoMat);
  knot.position.y = 1.2;
  root.add(knot);
  // 바깥 그물 (데이터 느낌)
  const wire = new THREE.LineSegments(new THREE.WireframeGeometry(new THREE.IcosahedronGeometry(0.95, 1)), holoMat);
  wire.position.y = 1.2;
  root.add(wire);
  // 빛 원뿔
  const coneMat = new THREE.ShaderMaterial({
    uniforms: uni,
    vertexShader: /* glsl */ `varying vec2 vUv; varying vec3 vW; void main(){ vUv = uv; vec4 w = modelMatrix * vec4(position,1.0); vW = w.xyz; gl_Position = projectionMatrix * viewMatrix * w; }`,
    fragmentShader: /* glsl */ `
      uniform float uTime; uniform vec3 uColor; varying vec2 vUv; varying vec3 vW;
      void main(){
        float fade = pow(1.0 - vUv.y, 1.6);
        float rays = 0.65 + 0.35 * sin(vUv.x * 60.0 + uTime * 1.5) * sin(vUv.x * 23.0 - uTime);
        float sl = 0.8 + 0.2 * sin(vW.y * 40.0 - uTime * 6.0);
        gl_FragColor = vec4(uColor * fade * rays * sl * 0.28, 1.0);
      }`,
    transparent: true,
    blending: THREE.AdditiveBlending,
    depthWrite: false,
    side: THREE.DoubleSide,
  });
  const cone = new THREE.Mesh(new THREE.CylinderGeometry(1.05, 0.48, 1.95, 48, 1, true), coneMat);
  cone.position.y = 0.25 + 1.95 / 2;
  root.add(cone);
  // 솟는 빛 알갱이
  const N = 140;
  const pp = new Float32Array(N * 3);
  const r0 = rng(7);
  for (let i = 0; i < N; i++) {
    const a = r0() * Math.PI * 2;
    const rr = Math.sqrt(r0()) * 0.75;
    pp[i * 3] = Math.cos(a) * rr;
    pp[i * 3 + 1] = r0();
    pp[i * 3 + 2] = Math.sin(a) * rr;
  }
  const pGeo = new THREE.BufferGeometry();
  pGeo.setAttribute('position', new THREE.BufferAttribute(pp, 3));
  const pMat = new THREE.ShaderMaterial({
    uniforms: uni,
    vertexShader: /* glsl */ `uniform float uTime; varying float vA;
      void main(){ vec3 p = position; float y = fract(p.y + uTime * (0.12 + p.y * 0.05)); p.y = 0.28 + y * 2.0; p.xz *= 1.0 + y * 0.6;
        vA = sin(y * 3.14159); vec4 mv = modelViewMatrix * vec4(p, 1.0); gl_PointSize = 34.0 / -mv.z; gl_Position = projectionMatrix * mv; }`,
    fragmentShader: /* glsl */ `uniform vec3 uColor; varying float vA; void main(){ float d = length(gl_PointCoord - 0.5); float a = smoothstep(0.5, 0.0, d); gl_FragColor = vec4(uColor * a * vA * 1.2, 1.0); }`,
    transparent: true,
    blending: THREE.AdditiveBlending,
    depthWrite: false,
  });
  root.add(new THREE.Points(pGeo, pMat));

  const hud = new Hud();
  const tag = hud.add('홀로그램 셰이더', 'rgba(10,60,90,0.75)');
  hud.at(tag, 0.03, 0.05, 0, 0);
  let holo = true;
  return {
    scene,
    camera: cam,
    update(t) {
      uni.uTime.value = t;
      knot.rotation.y = t * 0.6;
      knot.rotation.x = Math.sin(t * 0.4) * 0.3;
      wire.rotation.y = -t * 0.25;
      wire.rotation.z = t * 0.1;
      ringMat.color.setHSL(0.52, 1, 0.45 + 0.15 * Math.sin(t * 3));
    },
    render(r, w, h) {
      r.render(scene, cam);
      hud.draw(r, w, h);
    },
    controls: [
      { type: 'toggle', label: '홀로그램 켜기 (끄면 그냥 재질)', value: true, on: (v) => {
          holo = v;
          knot.material = holo ? holoMat : plainMat;
          wire.visible = holo;
          hud.set(tag, holo ? '홀로그램 셰이더' : '그냥 재질');
        } },
      { type: 'range', label: '주사선 촘촘함', min: 10, max: 120, step: 1, value: 46, on: (v) => (uni.uDensity.value = v) },
      { type: 'range', label: '깜박임', min: 0, max: 1, step: 0.05, value: 0.6, on: (v) => (uni.uFlicker.value = v) },
      { type: 'toggle', label: '글리치 (가로 찢김)', value: true, on: (v) => (uni.uGlitch.value = v ? 1 : 0) },
    ],
    dispose() {
      freeAll(root);
      plainMat.dispose();
      (scene.background as THREE.Texture).dispose();
      hud.dispose();
    },
  };
}
```

### i221 견본 항목 — `src/demos/demosLook2.ts:4505`
```ts
  i221: { kind: '3d', caption: '반투명 파랑 + 테두리 빛(프레넬) + 흐르는 주사선 · 빛 띠 + 깜박임 · 가로 찢김 — 투사기 위 홀로그램', make: () => makeHolo() }
```

## 관련 기술
- 먼저 알면 좋은 기술: [테두리 빛 (rim light, 프레넬)](https://ai-techstudio.web.app/ai/t/i10.md) `i10`
- 다음에 해 볼 기술: [선택 강조 맥박 테](https://ai-techstudio.web.app/ai/t/i226.md) `i226` · [빛 번짐 (UnrealBloom)](https://ai-techstudio.web.app/ai/t/u20.md) `u20`
