# AI 꾸러미 — 신성 강림 (빛 기둥 · 갓레이 · 깃털)
> 구름 틈 → 빛 기둥이 위에서 내리꽂힘(앞머리 + 번쩍) → 줄무늬 원뿔 두 겹이 엇돌며 빛살(갓레이) + 흔들리며 내려오는 깃털 + 떠도는 빛 먼지 · 바닥 빛 웅덩이  
> 견본: https://ai-techstudio.web.app/#t/i511

이 문서를 코드 도우미(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: `i511` · 분류: 이펙트 · 연출 › 스킬 VFX (마법 · 미사일) · 3D · 난이도 보통 · 폰 부담 보통 (폰 주의)
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i511
- 쓰면 좋을 때: 쓰면 좋을 곳 — 천사 · 부활 · 큰 보상 · 전설 등급

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i511 견본 항목 — `src/demos/demosVfx3.ts:2945`
```ts
  i511: {
    kind: '3d',
    caption: '구름 틈이 열리고 빛 기둥이 내리꽂힘 → 엇도는 빛살(갓레이) · 흔들리며 내려오는 깃털 · 떠도는 빛 먼지 · 바닥 빛 웅덩이',
    make(): Scene3D {
      const S = makeStage({ bg: 0x0a0c16, cam: [0, 2.0, 8.6], look: [0, 2.7, 0], fov: 50, fog: [10, 28] });
      const lab = S.hud.label('tl');
      S.scene.add(new THREE.HemisphereLight(0x405080, 0x0a0a10, 0.4));
      const tile = cachedCanvas('v3tile', 512, 512, (g, s) => {
        const r = rng(23);
        g.fillStyle = '#1a1c22';
        g.fillRect(0, 0, s, s);
        const n = 4;
        const ts = s / n;
        for (let y = 0; y < n; y++)
          for (let x = 0; x < n; x++) {
            const L = 34 + r() * 14;
            g.fillStyle = `hsl(220, 8%, ${L}%)`;
            g.fillRect(x * ts + 3, y * ts + 3, ts - 6, ts - 6);
            for (let k = 0; k < 40; k++) {
              const cx = x * ts + r() * ts;
              const cy = y * ts + r() * ts;
              const rr = 5 + r() * 25;
              const gr = g.createRadialGradient(cx, cy, 0, cx, cy, rr);
              gr.addColorStop(0, r() < 0.5 ? 'rgba(0,0,0,0.12)' : 'rgba(255,255,255,0.06)');
              gr.addColorStop(1, 'rgba(0,0,0,0)');
              g.fillStyle = gr;
              g.fillRect(cx - rr, cy - rr, rr * 2, rr * 2);
            }
          }
      });
      const tt = texFrom(tile, true, 6);
      const floor = new THREE.Mesh(new THREE.PlaneGeometry(40, 40), new THREE.MeshStandardMaterial({ map: tt, bumpMap: tt, bumpScale: 1.5, roughness: 0.55, color: 0x9aa0b0 }));
      floor.rotation.x = -Math.PI / 2;
      S.scene.add(floor);
      const stoneM = new THREE.MeshStandardMaterial({ color: 0x5a5e6a, roughness: 0.85 });
      for (const x of [-3.4, 3.4]) {
        const col = new THREE.Mesh(new THREE.CylinderGeometry(0.32, 0.36, 6, 16), stoneM);
        col.position.set(x, 3, -2.2);
        const cap = new THREE.Mesh(new THREE.BoxGeometry(0.9, 0.25, 0.9), stoneM);
        cap.position.set(x, 6.1, -2.2);
        const ft = new THREE.Mesh(new THREE.BoxGeometry(0.9, 0.3, 0.9), stoneM);
        ft.position.set(x, 0.15, -2.2);
        S.scene.add(col, cap, ft);
      }
      const spot = new THREE.SpotLight(0xffe8b0, 0, 16, 0.32, 0.7, 1.2);
      spot.position.set(0, 12, 0);
      spot.target.position.set(0, 0, 0);
      S.scene.add(spot, spot.target);
      const gold = new THREE.Color(0xffe2a0);
      const beamMat = new THREE.ShaderMaterial({
        uniforms: { uTime: { value: 0 }, uNoise: { value: S.noise }, uFront: { value: 1 }, uA: { value: 0 }, uCol: { value: gold }, uCore: { value: 1 } },
        vertexShader: /* glsl */ `varying vec2 vUv; varying float vF; void main(){ vUv = uv; vec3 n = normalize(normalMatrix * normal); vec4 mv = modelViewMatrix * vec4(position, 1.0); vF = abs(dot(n, normalize(-mv.xyz))); gl_Position = projectionMatrix * mv; }`,
        fragmentShader: /* glsl */ `uniform sampler2D uNoise; uniform float uTime, uFront, uA, uCore; uniform vec3 uCol; varying vec2 vUv; varying float vF;
          void main(){ float n = texture2D(uNoise, vec2(vUv.x * 3.0, vUv.y * 1.5 + uTime * 0.25)).r;
            float n2 = texture2D(uNoise, vec2(vUv.x * 7.0, vUv.y * 4.0 + uTime * 0.6)).g;
            float streak = 0.55 + 0.6 * n * n2;
            float vis = smoothstep(uFront, uFront + 0.03, vUv.y);
            float edge = exp(-pow((vUv.y - uFront) * 60.0, 2.0)) * step(0.01, uFront);
            float bottom = smoothstep(0.0, 0.03, vUv.y);
            float body = mix(pow(1.0 - vF, 1.5) * 0.8, pow(vF, 2.0) * 1.4, uCore);
            vec3 col = uCol * (2.0 + 4.0 * body) * streak + vec3(1.0) * edge * 10.0;
            gl_FragColor = vec4(col, 1.0); ${TONE}
            gl_FragColor.a = clamp((body * streak * vis + edge) * bottom * uA, 0.0, 1.0); }`,
        transparent: true,
        depthWrite: false,
        blending: THREE.AdditiveBlending,
        side: THREE.DoubleSide,
      });
      const BH = 14;
      const beam = new THREE.Mesh(new THREE.CylinderGeometry(0.5, 0.5, BH, 40, 1, true), beamMat);
      beam.position.y = BH / 2;
      const outerMat = beamMat.clone();
      outerMat.uniforms.uNoise!.value = S.noise;
      outerMat.uniforms.uCol!.value = gold;
      outerMat.uniforms.uCore!.value = 0;
      const outer = new THREE.Mesh(new THREE.CylinderGeometry(1.25, 1.25, BH, 40, 1, true), outerMat);
      outer.position.y = BH / 2;
      beam.renderOrder = 6;
      outer.renderOrder = 5;
      S.scene.add(beam, outer);
      const rayMat = (dens: number, sp: number): THREE.ShaderMaterial =>
        new THREE.ShaderMaterial({
          uniforms: { uTime: { value: 0 }, uNoise: { value: S.noise }, uA: { value: 0 }, uCol: { value: gold }, uDens: { value: dens }, uSp: { value: sp } },
          vertexShader: FLAT_VS,
          fragmentShader: /* glsl */ `uniform sampler2D uNoise; uniform float uTime, uA, uDens, uSp; uniform vec3 uCol; varying vec2 vUv;
            void main(){ float n = texture2D(uNoise, vec2(vUv.x * uDens + uTime * uSp, 0.37)).r;
              float rays = smoothstep(0.52, 0.85, n);
              float v = pow(vUv.y, 0.7) * smoothstep(0.0, 0.25, vUv.y);
              gl_FragColor = vec4(uCol * 1.6, 1.0); ${TONE}
              gl_FragColor.a = clamp(rays * v * uA * 0.55, 0.0, 1.0); }`,
          transparent: true,
          depthWrite: false,
          blending: THREE.AdditiveBlending,
          side: THREE.DoubleSide,
        });
      const r1 = rayMat(9, 0.02);
      const r2 = rayMat(14, -0.015);
      const cone1 = new THREE.Mesh(new THREE.CylinderGeometry(0.3, 2.9, 12, 72, 1, true), r1);
      const cone2 = new THREE.Mesh(new THREE.CylinderGeometry(0.2, 2.2, 12, 72, 1, true), r2);
      cone1.position.y = cone2.position.y = 6;
      cone1.renderOrder = cone2.renderOrder = 4;
      S.scene.add(cone1, cone2);
      // 구름 고리
      const clouds = S.pool(100, 2);
      const CLOUD = flat(0x9a94a4, 1, 0.6, 0);
      const cloudA: number[] = [];
      for (let i = 0; i < 90; i++) {
        const a = R(0, TAU);
        cloudA.push(a, R(1.6, 6), R(8.2, 9.2));
        clouds.spawn({ x: 0, y: 8.6, z: 0, life: 1e9, s0: R(3, 4.6), ramp: CLOUD, shape: 1, rot: R(-0.4, 0.4) });
      }
      const pool = glowDisc(0xffe2a0, 0, 5, 3.2);
      S.scene.add(pool.mesh);
      const haloMat = new THREE.ShaderMaterial({
        uniforms: { uA: { value: 0 }, uCol: { value: gold }, uTime: { value: 0 } },
        vertexShader: FLAT_VS,
        fragmentShader: /* glsl */ `uniform float uA, uTime; uniform vec3 uCol; varying vec2 vUv;
          void main(){ vec2 p = vUv * 2.0 - 1.0; float r = length(p); float ang = atan(p.y, p.x);
            float r1 = exp(-pow((r - 0.92) * 70.0, 2.0)); float r2 = exp(-pow((r - 0.8) * 90.0, 2.0)) * step(0.0, sin(ang * 24.0 + uTime));
            float r3 = exp(-pow((r - 0.55) * 80.0, 2.0)) * (0.5 + 0.5 * step(0.3, sin(ang * 6.0 - uTime * 0.7)));
            gl_FragColor = vec4(uCol * 3.0, 1.0); ${TONE} gl_FragColor.a = clamp((r1 + r2 * 0.7 + r3 * 0.6) * uA, 0.0, 1.0); }`,
        transparent: true,
        depthWrite: false,
        blending: THREE.AdditiveBlending,
      });
      const halo = new THREE.Mesh(new THREE.PlaneGeometry(3.4, 3.4), haloMat);
      halo.rotation.x = -Math.PI / 2;
      halo.position.y = 0.02;
      S.scene.add(halo);
      const ring = new Ring();
      S.scene.add(ring.mesh);
      // 깃털
      const feather = cachedCanvas('v3feather', 64, 160, (g, w, h) => {
        g.clearRect(0, 0, w, h);
        g.strokeStyle = 'rgba(255,255,255,0.95)';
        g.lineWidth = 2;
        g.beginPath();
        g.moveTo(w / 2, h);
        g.quadraticCurveTo(w / 2 + 3, h / 2, w / 2 - 2, 4);
        g.stroke();
        for (let i = 0; i < 46; i++) {
          const y = 10 + (i / 46) * (h - 30);
          const k = Math.sin((i / 46) * Math.PI);
          const len = (w / 2 - 4) * (0.35 + 0.65 * k);
          g.strokeStyle = `rgba(255,255,255,${0.55 + 0.4 * k})`;
          g.lineWidth = 1.6;
          for (const sd of [-1, 1]) {
            g.beginPath();
            g.moveTo(w / 2, y);
            g.quadraticCurveTo(w / 2 + sd * len * 0.6, y - 2, w / 2 + sd * len, y - 8 - k * 4);
            g.stroke();
          }
        }
      });
      const NF = 40;
      const fm = new THREE.InstancedMesh(new THREE.PlaneGeometry(0.17, 0.42), new THREE.MeshBasicMaterial({ map: texFrom(feather, true), color: new THREE.Color(1.6, 1.5, 1.3), transparent: true, depthWrite: false, side: THREE.DoubleSide, alphaTest: 0.02 }), NF);
      fm.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
      fm.frustumCulled = false;
      fm.renderOrder = 7;
      S.scene.add(fm);
      const FE = Array.from({ length: NF }, () => ({ x: 0, y: -9, z: 0, x0: 0, ph: R(0, TAU), sp: R(0.3, 0.5), ry: R(0, TAU), rest: 0, on: false }));
      const motes = S.pool(400, 8);
      const MOTE = ramp([[0, 0xfff4d0, 4, 0, 1], [0.3, 0xffe8b0, 5, 1, 1], [1, 0xffd080, 2, 0, 1]]);
      const BURST = ramp([[0, 0xffffff, 7, 1, 1], [0.4, 0xfff0c0, 4, 0.8, 1], [1, 0xffc060, 1, 0, 1]]);
      let rayI = 1;
      let nFe = 26;
      let white = false;
      const setWhite = (v: boolean): void => {
        white = v;
        gold.setHex(v ? 0xd8ecff : 0xffe2a0);
        spot.color.setHex(v ? 0xd8ecff : 0xffe8b0);
      };
      const controls: Control[] = [
        ctrlRange('빛살 세기', 0, 2, 0.05, 1, (v) => (rayI = v)),
        ctrlRange('깃털 수', 0, 40, 1, 26, (v) => (nFe = v)),
        ctrlTog('흰빛 (푸른 하늘빛)', false, setWhite),
      ];
      let cyc = 0;
      let struck = false;
      const M = new THREE.Matrix4();
      const E = new THREE.Euler();
      const Q = new THREE.Quaternion();
      const Pv = new THREE.Vector3();
      const Sv = new THREE.Vector3(1, 1, 1);
      return {
        scene: S.scene,
        camera: S.cam,
        controls,
        update(t, dt0) {
          const dt = Math.min(dt0, 0.05);
          const prev = cyc;
          cyc = (cyc + dt) % 8.5;
          if (cyc < prev) struck = false;
          const acc = white ? '#bfe0ff' : '#ffd890';
          let A = 0;
          let front = 1;
          let rad = 1;
          let open = 0;
          if (cyc < 1.0) {
            open = smooth(cyc / 1.0);
            lab.set('하늘이 열림', acc);
          } else if (cyc < 1.35) {
            open = 1;
            front = 1 - easeOut((cyc - 1.0) / 0.35);
            A = 1;
            lab.set('빛 기둥이 내리꽂힘', acc);
          } else if (cyc < 6.4) {
            open = 1;
            front = 0;
            A = 1;
            if (!struck) {
              struck = true;
              ring.fire(0, 0.03, 0, 3.4, 0.8, white ? 0xd8ecff : 0xffe0a0, 3);
              motes.spawn({ x: 0, y: 0.5, z: 0, life: 0.35, s0: 3, s1: 5, ramp: BURST, shape: 0 });
              for (let k = 0; k < 50; k++) {
                const a = R(0, TAU);
                const sp = R(0.8, 2.5);
                motes.spawn({ x: Math.cos(a) * 0.3, y: 0.1, z: Math.sin(a) * 0.3, vx: Math.cos(a) * sp, vy: R(0.5, 2.5), vz: Math.sin(a) * sp, life: R(0.8, 1.6), s0: R(0.06, 0.12), ramp: MOTE, shape: 4, rot: R(0, TAU), drag: 1.5 });
              }
              S.shake = 0.05;
              for (let i = 0; i < NF; i++) {
                const f = FE[i]!;
                f.on = i < nFe;
                f.x0 = R(-1.8, 1.8);
                f.z = R(-1.2, 1.2);
                f.y = R(4, 8.5);
                f.rest = 0;
              }
            }
            lab.set('빛살 · 깃털 · 빛 먼지 — 바닥에 빛 웅덩이', acc);
          } else if (cyc < 7.6) {
            open = 1 - smooth((cyc - 6.4) / 1.2);
            front = 0;
            const k = smooth((cyc - 6.4) / 1.0);
            rad = 1 - k;
            A = 1 - k;
            lab.set('빛이 거둬짐', acc);
          } else lab.set('어둠', acc);
          for (const m of [beamMat, outerMat]) {
            m.uniforms.uTime!.value = t;
            m.uniforms.uFront!.value = front;
            m.uniforms.uA!.value = A * (m === outerMat ? 0.5 : 1);
          }
          beam.scale.set(Math.max(0.001, rad), 1, Math.max(0.001, rad));
          outer.scale.set(Math.max(0.001, rad * (1 + 0.04 * Math.sin(t * 3))), 1, Math.max(0.001, rad));
          const rayA = (front < 0.05 ? A : 0) * rayI + open * 0.25 * rayI;
          r1.uniforms.uTime!.value = t;
          r2.uniforms.uTime!.value = t;
          r1.uniforms.uA!.value = rayA;
          r2.uniforms.uA!.value = rayA * 0.8;
          cone1.rotation.y = t * 0.05;
          cone2.rotation.y = -t * 0.04;
          spot.intensity = A * (front < 0.05 ? 260 : 0) * (0.95 + 0.05 * Math.sin(t * 5));
          pool.set(A * (front < 0.05 ? 1.4 : 0));
          haloMat.uniforms.uA!.value = A * (front < 0.05 ? 1 : 0);
          haloMat.uniforms.uTime!.value = t;
          halo.rotation.z = t * 0.2;
          // 구름 틈
          for (let i = 0; i < clouds.n; i++) {
            const a = cloudA[i * 3]! + t * 0.03;
            const r = cloudA[i * 3 + 1]! + open * 1.2 * (cloudA[i * 3 + 1]! < 3 ? 1 : 0.3);
            clouds.x[i] = Math.cos(a) * r;
            clouds.z[i] = Math.sin(a) * r - 1;
            clouds.y[i] = cloudA[i * 3 + 2]!;
            clouds.br[i] = 0.55 + open * 0.6 * Math.exp(-r * 0.25);
          }
          clouds.upload();
          if (A > 0.5 && front < 0.05 && Math.random() < dt * 40) {
            const a = R(0, TAU);
            const r = Math.sqrt(Math.random()) * 1.2;
            motes.spawn({ x: Math.cos(a) * r, y: R(0.2, 5), z: Math.sin(a) * r, vx: R(-0.05, 0.05), vy: R(0.05, 0.25), vz: R(-0.05, 0.05), life: R(1.2, 2.4), s0: R(0.04, 0.09), ramp: MOTE, shape: 4, rot: R(0, TAU) });
          }
          let n = 0;
          for (const f of FE) {
            if (!f.on || f.y < -5 || cyc < 1.35 || cyc > 7.6) continue;
            if (f.y > 0.03) {
              f.y -= f.sp * dt * (0.8 + 0.4 * Math.sin(t * 1.6 + f.ph) ** 2);
              f.x = f.x0 + Math.sin(t * 1.6 + f.ph) * 0.35;
              f.ry += dt * 0.6;
              E.set(0.3 * Math.sin(t * 1.6 + f.ph + 1), f.ry, 0.7 * Math.sin(t * 1.6 + f.ph));
            } else {
              f.y = 0.03;
              f.rest += dt;
              E.set(-Math.PI / 2, 0, f.ry);
            }
            const fadeK = cyc > 6.4 ? 1 - smooth((cyc - 6.4) / 1.2) : 1;
            Q.setFromEuler(E);
            Pv.set(f.x, f.y + 0.2, f.z);
            Sv.setScalar(fadeK);
            M.compose(Pv, Q, Sv);
            fm.setMatrixAt(n++, M);
          }
          fm.count = n;
          fm.instanceMatrix.needsUpdate = true;
          ring.update(dt);
          motes.tick(dt);
          S.tick(dt);
        },
        resize: (w, h) => S.resize(w, h),
        dispose: () => S.dispose(),
      };
    },
  }
```

## 관련 기술
- 이 효과를 만든 기술: [층 쌓기 (핵 · 빛무리 · 불티 · 연기)](https://ai-techstudio.web.app/ai/t/i171.md) `i171`
