# AI 꾸러미 — 크리스탈 성장 · 깨짐
> 육각 결정이 바닥에서 튀어나오듯 차례로 자람(넘치게 컸다 돌아옴 · 자라는 앞머리 빛) + 끝 반짝 · 파편 → 하얗게 번쩍 후 조각으로 깨져 흩어짐  
> 견본: https://ai-techstudio.web.app/#t/i514

이 문서를 코드 도우미(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: `i514` · 분류: 이펙트 · 연출 › 화려한 효과 (VFX) · 3D · 난이도 보통 · 폰 부담 보통 (폰 주의)
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i514
- 쓰면 좋을 때: 쓰면 좋을 곳 — 얼음 · 보석 마법 · 광물 · 결정 구조

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i514 견본 항목 — `src/demos/demosVfx3.ts:3696`
```ts
  i514: {
    kind: '3d',
    caption: '육각 결정이 바닥에서 차례로 튀어나와 자람(넘치게 컸다 돌아옴 · 끝이 빛남) → 반짝 → 하얗게 번쩍 후 조각으로 깨져 흩어짐',
    make(): Scene3D {
      const S = makeStage({ bg: 0x06060c, cam: [0, 1.7, 5.4], look: [0, 0.75, 0], fov: 42, fog: [7, 18] });
      const lab = S.hud.label('tl');
      S.scene.add(new THREE.HemisphereLight(0x404880, 0x080808, 0.4));
      const rock = groundCanvas('v3rock', '#2c2824', [['rgba(0,0,0,0.3)', 6000, 2], ['rgba(255,255,255,0.05)', 3000, 1.5]], 71);
      const rockT = texFrom(rock, true, 5);
      const ground = new THREE.Mesh(new THREE.PlaneGeometry(30, 30), new THREE.MeshStandardMaterial({ map: rockT, bumpMap: rockT, bumpScale: 2, roughness: 0.9, color: 0x6a7088 }));
      ground.rotation.x = -Math.PI / 2;
      S.scene.add(ground);
      const prism = mergeGeometries([new THREE.CylinderGeometry(0.5, 0.5, 1, 6, 1).translate(0, 0.5, 0), new THREE.ConeGeometry(0.5, 0.5, 6, 1).translate(0, 1.25, 0)], false)!;
      const N_MAX = 40;
      const iCol = new THREE.InstancedBufferAttribute(new Float32Array(N_MAX * 3), 3);
      const iGrow = new THREE.InstancedBufferAttribute(new Float32Array(N_MAX * 2), 2);
      iCol.setUsage(THREE.DynamicDrawUsage);
      iGrow.setUsage(THREE.DynamicDrawUsage);
      prism.setAttribute('iCol', iCol);
      prism.setAttribute('iGrow', iGrow);
      const crystalMat = new THREE.ShaderMaterial({
        uniforms: { uTime: { value: 0 } },
        vertexShader: /* glsl */ `attribute vec3 iCol; attribute vec2 iGrow; varying vec3 vW; varying float vH; varying vec3 vCol; varying vec2 vG;
          void main(){ vec4 w = modelMatrix * instanceMatrix * vec4(position, 1.0); vW = w.xyz; vH = position.y / 1.5; vCol = iCol; vG = iGrow;
            gl_Position = projectionMatrix * viewMatrix * w; }`,
        fragmentShader: /* glsl */ `uniform float uTime; varying vec3 vW; varying float vH; varying vec3 vCol; varying vec2 vG;
          void main(){ vec3 N = normalize(cross(dFdx(vW), dFdy(vW))); vec3 V = normalize(cameraPosition - vW); if (dot(N, V) < 0.0) N = -N;
            float ndv = max(dot(N, V), 0.0);
            vec3 Rf = reflect(-V, N);
            vec3 env = mix(vec3(0.02, 0.02, 0.05), vec3(0.55, 0.58, 0.8), smoothstep(-0.2, 0.9, Rf.y));
            float spec = pow(max(dot(Rf, normalize(vec3(-0.4, 0.8, 0.5))), 0.0), 50.0) * 2.4 + pow(max(dot(Rf, normalize(vec3(0.6, 0.3, 0.7))), 0.0), 16.0) * 0.5;
            float fres = pow(1.0 - ndv, 3.0);
            float facet = 0.55 + 0.45 * sin(dot(N, vec3(13.0, 7.0, 11.0)) * 2.0);
            float inner = 0.12 + 1.5 * pow(vH, 1.7);
            float sweep = exp(-pow((fract(vH * 0.5 - uTime * 0.35) - 0.5) * 9.0, 2.0)) * 0.5;
            vec3 col = vCol * (inner + sweep) * facet + env * fres * 0.9 + vec3(spec) + vCol * fres * 1.3;
            col += vCol * smoothstep(0.55, 1.0, vH) * (1.0 - vG.x) * 3.5;
            col = mix(col, vec3(4.0), vG.y);
            gl_FragColor = vec4(col, 1.0); ${TONE} }`,
      });
      const im = new THREE.InstancedMesh(prism, crystalMat, N_MAX);
      im.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
      im.frustumCulled = false;
      S.scene.add(im);
      const THEMES = [
        [0xb050ff, 0x30d8ff, 0xff4aa0],
        [0x60e0ff, 0x8ab0ff, 0xc0f4ff],
        [0xffb030, 0xff6020, 0xffe060],
      ];
      let theme = 0;
      const CL = [new THREE.Vector3(-1.4, 0, -0.3), new THREE.Vector3(0.15, 0, 0.35), new THREE.Vector3(1.55, 0, -0.55)];
      interface Cr {
        c: number;
        pos: THREE.Vector3;
        q: THREE.Quaternion;
        len: number;
        rad: number;
        start: number;
        tip: THREE.Vector3;
        burst: boolean;
      }
      const CR: Cr[] = [];
      const rr = rng(5);
      CL.forEach((c, ci) => {
        const n = 7 + ci % 2;
        for (let k = 0; k < n; k++) {
          const main = k === 0;
          const a = rr() * TAU;
          const tilt = main ? rr() * 0.12 : 0.3 + rr() * 0.55;
          const dir = new THREE.Vector3(Math.sin(tilt) * Math.cos(a), Math.cos(tilt), Math.sin(tilt) * Math.sin(a));
          const q = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
          const off = main ? 0 : 0.08 + rr() * 0.22;
          const len = main ? 1.5 + rr() * 0.5 : 0.5 + rr() * 0.8;
          const rad = main ? 0.2 + rr() * 0.06 : 0.08 + rr() * 0.1;
          CR.push({ c: ci, pos: new THREE.Vector3(c.x + Math.cos(a) * off, -0.05, c.z + Math.sin(a) * off), q, len, rad, start: ci * 0.55 + (main ? 0 : 0.15 + k * 0.11), tip: dir.clone(), burst: false });
        }
      });
      const lights = CL.map((c) => {
        const l = new THREE.PointLight(0xffffff, 0, 5, 1.6);
        l.position.set(c.x, 0.7, c.z + 0.3);
        S.scene.add(l);
        return l;
      });
      const discs = CL.map((c) => {
        const d = glowDisc(0xffffff, 0, 2.2, 3.5);
        d.mesh.position.set(c.x, 0.015, c.z);
        S.scene.add(d.mesh);
        return d;
      });
      const ring = new Ring();
      S.scene.add(ring.mesh);
      const P = S.pool(2400, 5);
      P.floorY = 0.02;
      const dust = S.pool(300, 2);
      const DUST = ramp([[0, 0x6a6a7a, 1, 0, 0], [0.15, 0x5a5a6a, 1, 0.4, 0], [1, 0x3a3a44, 1, 0, 0]]);
      const ramps = (hex: number): { shard: Float32Array; spark: Float32Array } => ({
        shard: ramp([[0, 0xffffff, 5, 1, 1], [0.15, hex, 3.5, 1, 1], [0.8, hex, 1.6, 0.85, 1], [1, hex, 0.6, 0, 1]]),
        spark: ramp([[0, 0xffffff, 7, 0, 1], [0.2, 0xffffff, 6, 1, 1], [1, hex, 2, 0, 1]]),
      });
      let R3 = THEMES[0]!.map(ramps);
      const col = new THREE.Color();
      let speed = 1;
      let size = 1;
      let cyc = 0;
      let shattered = false;
      const SH = 5.2;
      const CY = 7.0;
      const controls: Control[] = [
        ctrlRange('자라는 속도', 0.5, 2, 0.05, 1, (v) => (speed = v)),
        ctrlRange('결정 크기', 0.6, 1.5, 0.05, 1, (v) => (size = v)),
        ctrlRange('색 (0 보석 · 1 얼음 · 2 호박)', 0, 2, 1, 0, (v) => {
          theme = v;
          R3 = THEMES[v]!.map(ramps);
        }),
        ctrlBtn('지금 깨기', () => (cyc = Math.max(cyc, SH - 0.01))),
      ];
      const M = new THREE.Matrix4();
      const SC = new THREE.Vector3();
      const tip = new THREE.Vector3();
      return {
        scene: S.scene,
        camera: S.cam,
        controls,
        update(t, dt0) {
          const dt = Math.min(dt0, 0.05);
          const prev = cyc;
          cyc = (cyc + dt * (cyc < SH ? speed : 1)) % CY;
          if (cyc < prev) {
            shattered = false;
            for (const c of CR) c.burst = false;
          }
          crystalMat.uniforms.uTime!.value = t;
          const flashK = cyc >= SH ? clamp(1 - (cyc - SH) / 0.12, 0, 1) : 0;
          if (cyc >= SH + 0.1 && !shattered) {
            shattered = true;
            ring.fire(0, 0.03, 0, 4.5, 0.7, THEMES[theme]![0]!, 3);
            S.shake = 0.08;
            for (const c of CR) {
              const L = c.len * size;
              const sp = R3[c.c]!;
              const n = Math.round(10 + L * 16);
              for (let k = 0; k < n; k++) {
                const s = Math.random();
                tip.copy(c.tip).multiplyScalar(L * s).add(c.pos);
                const d = sphereDir();
                const v = R(1.5, 4.5);
                P.spawn({ x: tip.x, y: Math.max(0.05, tip.y), z: tip.z, vx: d.x * v + c.tip.x * 1.5, vy: Math.abs(d.y) * v + 1.5, vz: d.z * v + c.tip.z * 1.5, life: R(1.2, 2.2), s0: R(0.05, 0.11) * size, ramp: sp.shard, shape: 3, rot: R(0, TAU), rv: R(-12, 12), drag: 0.6, grav: 8, bnc: 0.35 });
              }
              for (let k = 0; k < 8; k++) {
                const d = sphereDir();
                tip.copy(c.tip).multiplyScalar(L * Math.random()).add(c.pos);
                P.spawn({ x: tip.x, y: tip.y, z: tip.z, vx: d.x * 2, vy: d.y * 2, vz: d.z * 2, life: R(0.5, 1), s0: R(0.12, 0.25), ramp: sp.spark, shape: 4, rot: R(0, TAU), drag: 2 });
              }
            }
          }
          let n = 0;
          const grownBy = [0, 0, 0];
          const pal = THEMES[theme]!;
          for (const c of CR) {
            const k = clamp((cyc - c.start) / 0.6, 0, 1);
            if (cyc < SH && k > 0 && !c.burst) {
              c.burst = true;
              const sp = R3[c.c]!;
              for (let j = 0; j < 10; j++) {
                const d = sphereDir();
                P.spawn({ x: c.pos.x, y: 0.05, z: c.pos.z, vx: d.x * 2, vy: Math.abs(d.y) * 3, vz: d.z * 2, life: R(0.5, 0.9), s0: R(0.03, 0.06), ramp: sp.shard, shape: 3, rot: R(0, TAU), rv: R(-10, 10), grav: 8, bnc: 0.3 });
              }
              for (let j = 0; j < 5; j++) {
                const a = R(0, TAU);
                dust.spawn({ x: c.pos.x, y: 0.1, z: c.pos.z, vx: Math.cos(a) * 0.8, vy: 0.3, vz: Math.sin(a) * 0.8, life: R(1, 1.5), s0: 0.15, s1: 0.6, ramp: DUST, shape: 1, rot: R(-0.4, 0.4), drag: 2 });
              }
            }
            if (shattered || k <= 0) continue;
            const L = c.len * size * backOut(k);
            const rad = c.rad * size * Math.min(1, k * 1.8);
            M.compose(c.pos, c.q, SC.set(rad * 2, L / 1.5 + 0.0001, rad * 2));
            im.setMatrixAt(n, M);
            col.setHex(pal[c.c]!);
            iCol.setXYZ(n, col.r, col.g, col.b);
            iGrow.setXY(n, k, flashK);
            n++;
            grownBy[c.c] = grownBy[c.c]! + k / 8;
            if (k >= 1 && Math.random() < dt * 1.2) {
              tip.copy(c.tip).multiplyScalar(L + 0.02).add(c.pos);
              P.spawn({ x: tip.x, y: tip.y, z: tip.z, life: R(0.4, 0.7), s0: R(0.15, 0.28), ramp: R3[c.c]!.spark, shape: 4, rot: R(0, TAU) });
            }
          }
          im.count = n;
          im.instanceMatrix.needsUpdate = true;
          iCol.needsUpdate = true;
          iGrow.needsUpdate = true;
          CL.forEach((_, i) => {
            const g = shattered ? 0 : grownBy[i]!;
            lights[i]!.color.setHex(pal[i]!);
            lights[i]!.intensity = g * 6 + flashK * 20;
            discs[i]!.set(g * 0.9 + flashK, pal[i]!);
          });
          if (cyc < SH) lab.set(cyc < 2.4 ? '자라남 — 바닥에서 튀어나와 넘치게 컸다 돌아옴' : '반짝임 — 끝에서 빛이 맺힘', '#c890ff');
          else lab.set(cyc < SH + 0.2 ? '번쩍!' : '깨짐 — 조각이 튀어 흩어짐', '#ffffff');
          ring.update(dt);
          P.tick(dt);
          dust.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`
