# AI 꾸러미 — 빛기둥 · 반짝이 점
> 높이에 따라 옅어지는 원뿔 빛 · 점마다 다른 반짝임  
> 견본: https://ai-techstudio.web.app/#t/u27

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

## 주문서

### 만들어 줘: 빛기둥 · 반짝이 점

#### 1. 목표
3D 장면의 물체에 「빛기둥 · 반짝이 점」 기술을 깔끔한 기본 느낌으로 넣어 줘. — 높이에 따라 옅어지는 원뿔 빛 · 점마다 다른 반짝임

#### 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: `u27` · 분류: 그래픽 · 셰이더 › 셰이더 · 3D · 난이도 보통 · 폰 부담 보통 (폰 주의)
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/u27

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### beamDemo — `src/demos/demosShader.ts:836`
```ts
function beamDemo(): Scene3D {
  const scene = new THREE.Scene();
  const bg = gradTex([
    [0, '#0b1030'],
    [1, '#2a2160'],
  ]);
  scene.background = bg;
  scene.add(new THREE.HemisphereLight(0xbfd8ff, 0x302050, 0.7));
  const key = new THREE.PointLight(0xffe6a0, 6, 6);
  key.position.set(0, 1.2, 0.8);
  scene.add(key);
  const time = { value: 0 };
  const amt = { value: 1 };
  const mkBeam = (color: number, rt: number, rb: number): THREE.Mesh => {
    const g = new THREE.CylinderGeometry(rt, rb, 3.2, 48, 1, true);
    g.translate(0, 1.6, 0);
    const m = new THREE.ShaderMaterial({
      uniforms: { uTime: time, uAmt: amt, uColor: { value: new THREE.Color(color) } },
      transparent: true,
      depthWrite: false,
      blending: THREE.AdditiveBlending,
      side: THREE.DoubleSide,
      vertexShader: /* glsl */ `varying vec3 vN; varying vec3 vW; varying vec3 vL;
        void main(){ vL = 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 float uAmt; uniform vec3 uColor; varying vec3 vN; varying vec3 vW; varying vec3 vL;
        ${NOISE2}
        void main(){
          float h = vL.y / 3.2;
          float edge = pow(abs(dot(normalize(vN), normalize(cameraPosition - vW))), 1.6);
          float fade = pow(1.0 - h, 1.7) * smoothstep(0.0, 0.04, h);
          float st = 0.55 + 0.45 * noise(vec2(atan(vL.z, vL.x) * 3.0, h * 3.0 - uTime * 1.3));
          gl_FragColor = vec4(uColor * edge * fade * st * uAmt, 1.0);
        }`,
    });
    return new THREE.Mesh(g, m);
  };
  const world = new THREE.Group();
  world.add(mkBeam(0xffd27a, 1.15, 0.5), mkBeam(0xfff6d0, 0.45, 0.28));
  const pad = new THREE.Mesh(new THREE.CylinderGeometry(0.85, 0.95, 0.18, 48), new THREE.MeshStandardMaterial({ color: 0x24305e, roughness: 0.5, metalness: 0.3 }));
  pad.position.y = -0.09;
  const ring = new THREE.Mesh(new THREE.TorusGeometry(0.62, 0.035, 8, 64), new THREE.MeshBasicMaterial({ color: 0xffd98a }));
  ring.rotation.x = -Math.PI / 2;
  ring.position.y = 0.02;
  const floor = new THREE.Mesh(new THREE.CircleGeometry(3.5, 48), new THREE.MeshStandardMaterial({ color: 0x1a1d44, roughness: 0.9 }));
  floor.rotation.x = -Math.PI / 2;
  floor.position.y = -0.18;
  const cube = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.5, 0.5), new THREE.MeshStandardMaterial({ color: 0xffc23a, metalness: 0.7, roughness: 0.25, emissive: 0x6a4000, emissiveIntensity: 0.6 }));
  world.add(pad, ring, floor, cube);
  // 반짝이 점 — 점마다 다른 빠르기 · 때
  const N = 260;
  const pos = new Float32Array(N * 3);
  const ph = new Float32Array(N);
  const sp = new Float32Array(N);
  const col = new Float32Array(N * 3);
  const palette = [new THREE.Color(0xffe08a), new THREE.Color(0x8ae8ff), new THREE.Color(0xff9ad8), new THREE.Color(0xffffff)];
  let seed = 7;
  const rnd = (): number => ((seed = (seed * 16807) % 2147483647) / 2147483647);
  for (let i = 0; i < N; i++) {
    const a = rnd() * Math.PI * 2;
    const r = Math.sqrt(rnd()) * 1.6;
    pos[i * 3] = Math.cos(a) * r;
    pos[i * 3 + 1] = rnd() * 3.2;
    pos[i * 3 + 2] = Math.sin(a) * r;
    ph[i] = rnd() * 6.28;
    sp[i] = 0.6 + rnd() * 1.6;
    const c = palette[i % 4]!;
    col.set([c.r, c.g, c.b], i * 3);
  }
  const pg = new THREE.BufferGeometry();
  pg.setAttribute('position', new THREE.BufferAttribute(pos, 3));
  pg.setAttribute('aPhase', new THREE.BufferAttribute(ph, 1));
  pg.setAttribute('aSpeed', new THREE.BufferAttribute(sp, 1));
  pg.setAttribute('aColor', new THREE.BufferAttribute(col, 3));
  const pu = { uTime: time, uSize: { value: 30 }, uTw: { value: 2.5 } };
  const pts = new THREE.Points(
    pg,
    new THREE.ShaderMaterial({
      uniforms: pu,
      transparent: true,
      depthWrite: false,
      blending: THREE.AdditiveBlending,
      vertexShader: /* glsl */ `attribute float aPhase; attribute float aSpeed; attribute vec3 aColor;
        uniform float uTime; uniform float uSize; uniform float uTw; varying vec3 vC; varying float vA;
        void main(){
          vec3 p = position; float h = fract(p.y / 3.2 + uTime * 0.04 * aSpeed); p.y = h * 3.2;
          float tw = pow(0.5 + 0.5 * sin(uTime * aSpeed * uTw + aPhase), 3.0);
          vA = (0.15 + tw) * smoothstep(0.0, 0.1, h) * smoothstep(1.0, 0.7, h);
          vec4 mv = modelViewMatrix * vec4(p, 1.0);
          gl_PointSize = uSize * (0.35 + tw) / -mv.z;
          gl_Position = projectionMatrix * mv; vC = aColor;
        }`,
      fragmentShader: /* glsl */ `varying vec3 vC; varying float vA;
        void main(){
          vec2 c = gl_PointCoord - 0.5; float d = length(c);
          float core = pow(max(0.0, 1.0 - d * 2.0), 2.0);
          float cr = max(0.0, 1.0 - abs(c.x) * 14.0) * max(0.0, 1.0 - abs(c.y) * 2.0) + max(0.0, 1.0 - abs(c.y) * 14.0) * max(0.0, 1.0 - abs(c.x) * 2.0);
          gl_FragColor = vec4(vC * (core + cr * 0.7) * vA, 1.0);
        }`,
    }),
  );
  world.add(pts);
  scene.add(world);
  const cam = new THREE.PerspectiveCamera(40, 1, 0.1, 50);
  cam.position.set(0, 2.0, 6.2);
  cam.lookAt(0, 1.3, 0);
  return {
    scene,
    camera: cam,
    update(t) {
      time.value = t;
      cube.position.y = 1.1 + Math.sin(t * 1.6) * 0.15;
      cube.rotation.set(t * 0.5, t * 0.8, 0.3);
      world.rotation.y = t * 0.15;
    },
    resize(_w, h) {
      pu.uSize.value = h * 0.11;
    },
    controls: [
      { type: 'range', label: '빛기둥 밝기', min: 0, max: 2, step: 0.05, value: 1, on: (v) => (amt.value = v) },
      { type: 'range', label: '반짝 빠르기', min: 0.2, max: 6, step: 0.1, value: 2.5, on: (v) => (pu.uTw.value = v) },
    ],
    dispose() {
      freeAll(world);
      bg.dispose();
    },
  };
}
```

### u27 견본 항목 — `src/demos/demosShader.ts:2384`
```ts
  u27: { kind: '3d', caption: '위로 갈수록 옅어지는 원뿔 빛기둥 · 점마다 다른 때에 반짝이는 별 알갱이', make: () => beamDemo() }
```

## 관련 기술
- 이 효과를 만든 기술: [빛살 (God rays)](https://ai-techstudio.web.app/ai/t/i06.md) `i06`
