# AI 꾸러미 — 물 바닥 빛무늬 (커스틱) — Caustics (procedural)
> 물결 잡음을 sin 으로 접어 가는 선만 남겨, 물속 바닥에 일렁이는 빛 그물(커스틱)을 셰이더로 그린다 — 빨 · 초 · 파를 조금 어긋나게 해 무지개 테까지.  
> 견본: https://ai-techstudio.web.app/#t/u23

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

## 주문서

### 만들어 줘: 물 바닥 빛무늬 (커스틱) — Caustics (procedural)

#### 1. 목표
얕은 연못 · 수영장 바닥에 일렁이는 커스틱 빛 그물을 넣어 줘 — 물결이 모은 햇빛 느낌으로, 분위기는 맑은 한낮.

#### 2. 핵심 기술 용어
- **Caustics (procedural)** — 물결이 모은 햇빛이 바닥에 만든 빛 무늬
- **Folded noise (abs(sin(fbm × k)))** — 잡음을 접어 가는 선 그물 만들기
- **Chromatic offset** — 색마다 조금 어긋나게 읽어 무지개 테두리

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

#### 4. 조건
- caus = pow(1.0 - min(a, b), 5.0), a · b = abs(sin((fbm(p + 흐름) - 0.5) × 18.0)) 두 장
- 두 장은 크기(1 · 1.3) · 흐르는 방향을 다르게 — 같으면 무늬가 그냥 미끄러진다
- 빨 · 초 · 파를 ±0.012 어긋나게 읽어 무지개 테
- 바닥 색은 물빛(0.42, 0.66, 0.74)을 곱한 뒤 그 위에 커스틱을 더한다
- 좌표는 세계 xz (vW.xz × 1.7) — 바닥 판 크기가 바뀌어도 무늬 크기가 같게

#### 5. 완성 기준 (이게 보이면 성공)
- 왼쪽 빛무늬 없음 · 오른쪽 커스틱을 나란히 두면 오른쪽 바닥에 가는 빛 그물이 일렁인다
- 그물이 미끄러지지 않고 모양이 바뀌며 굽이친다
- 선 가장자리에 아주 옅은 무지개 테가 보인다
- 세기 슬라이더로 0 ~ 1 사이가 바로 바뀌고, 폰에서도 60fps

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

## 원리
- 진짜 커스틱은 물결 면이 햇빛을 모으는 계산이지만, 생김새는 「굽이치는 가는 밝은 선 그물」이다.
- 잡음(fbm)을 시간으로 흘린 뒤 sin(… × 18) 의 절댓값으로 접으면 0 근처를 지나는 곳마다 가는 골이 생긴다.
- 서로 다르게 흐르는 두 장의 최솟값을 1 에서 빼고 5제곱하면 골이 만나는 곳만 밝은 선 그물이 된다.
- 빨 · 초 · 파를 x 로 ±0.012 씩 어긋나게 읽으면 선 가장자리에 무지개 테가 생긴다. 바닥 색 위에 더한다.

## 핵심 코드 — 접은 잡음 두 장 → 빛 그물 → 색 어긋남
(발췌: demos/demosShader.ts causticDemo() 의 바닥 조각 셰이더)
```glsl
// noise() · fbm(4겹) 은 이 위에
uniform float uTime; uniform float uAmt; uniform sampler2D uMap;
varying vec2 vUv; varying vec3 vW;
float caus(vec2 p, float t){
  float a = abs(sin((fbm(p + vec2(t * 0.25, -t * 0.4)) - 0.5) * 18.0));
  float b = abs(sin((fbm(p * 1.3 + vec2(-t * 0.2, t * 0.3) + 3.1) - 0.5) * 18.0));
  return pow(1.0 - min(a, b), 5.0);   // 두 골이 만나는 곳만 밝게
}
void main(){
  vec3 c = pow(texture2D(uMap, vUv * 2.0).rgb, vec3(2.2)); // 자갈 그림 (sRGB → 선형)
  vec3 col = c * vec3(0.42, 0.66, 0.74);                   // 물속 빛깔
  vec2 p = vW.xz * 1.7;
  // 빨 · 초 · 파를 아주 조금씩 어긋나게 — 무지개 테두리
  vec3 cs = vec3(caus(p + vec2(0.012, 0.0), uTime), caus(p, uTime), caus(p - vec2(0.012, 0.0), uTime));
  col += vec3(1.0, 0.98, 0.9) * cs * 1.1 * uAmt;
  gl_FragColor = vec4(col, 1.0);
  #include <colorspace_fragment>
}
```

## 흔한 실수 · 확인 목록
- [ ] **한 장만 쓰면 그물이 아니라 줄무늬가 된다** — 서로 다르게 흐르는 두 장의 최솟값을 써야 그물 눈이 생긴다.
- [ ] **밤 장면에서 그대로 켜면 바닥이 낮처럼 번쩍인다** — 버섯 마을 연못은 밤이라 커스틱을 약하게 했다. 장면 밝기에 맞춰 세기를 줄인다.
- [ ] **무늬를 uv 로 그리면 판마다 크기가 달라진다** — 세계 좌표 xz 로 그려 바닥 크기와 상관없이 같은 크기로.

## 완성 기준 체크리스트
- [ ] 왼쪽 빛무늬 없음 · 오른쪽 커스틱을 나란히 두면 오른쪽 바닥에 가는 빛 그물이 일렁인다
- [ ] 그물이 미끄러지지 않고 모양이 바뀌며 굽이친다
- [ ] 선 가장자리에 아주 옅은 무지개 테가 보인다
- [ ] 세기 슬라이더로 0 ~ 1 사이가 바로 바뀌고, 폰에서도 60fps

## 이 기술 정보
- id: `u23` · 분류: 그래픽 · 셰이더 › 셰이더 · 3D · 난이도 쉬움 · 폰 부담 가벼움 (폰 OK) — 바닥 픽셀마다 fbm 2번 × 색 3번 = fbm 6번. 바닥만 칠하므로 보통 가볍다. 더 줄이려면 색 어긋남을 빼고 1번만.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/u23
- 쓰면 좋을 때: 얕고 맑은 물의 바닥이 보이는 장면 / 수조 · 연못을 살아 있게 보이고 싶을 때 / 물 셰이더(u22)의 강바닥에 함께
- 쓰지 말 때: 깊고 탁한 물 — 바닥이 안 보인다 / 밤 장면 — 약하게 하거나 끈다 (버섯 마을은 밤이라 약하게)

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### causticDemo — `src/demos/demosShader.ts:349`
```ts
function causticDemo(): Scene3D {
  const scene = new THREE.Scene();
  const bg = gradTex([
    [0, '#7cc6ee'],
    [1, '#e4f6ff'],
  ]);
  scene.background = bg;
  lights(scene, 0xffffff, 0x8899aa, 1.0, 1.4);
  const peb = new THREE.CanvasTexture(pebbleCanvas());
  peb.wrapS = peb.wrapT = THREE.RepeatWrapping;
  const uni = { uTime: { value: 0 }, uOn: { value: 1 }, uAmt: { value: 1 }, uMap: { value: peb } };
  const bedMat = 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 float uOn; uniform float uAmt; uniform sampler2D uMap;
      varying vec2 vUv; varying vec3 vW;
      ${NOISE2}
      float caus(vec2 p, float t){
        float a = abs(sin((fbm(p + vec2(t * 0.25, -t * 0.4)) - 0.5) * 18.0));
        float b = abs(sin((fbm(p * 1.3 + vec2(-t * 0.2, t * 0.3) + 3.1) - 0.5) * 18.0));
        return pow(1.0 - min(a, b), 5.0);
      }
      void main(){
        vec3 c = pow(texture2D(uMap, vUv * 2.0).rgb, vec3(2.2));
        vec3 col = c * vec3(0.42, 0.66, 0.74);
        vec2 p = vW.xz * 1.7;
        // 빛 그물 — 빨 · 초 · 파 를 아주 조금씩 어긋나게 (무지개 테두리)
        vec3 cs = vec3(caus(p + vec2(0.012, 0.0), uTime), caus(p, uTime), caus(p - vec2(0.012, 0.0), uTime));
        col += vec3(1.0, 0.98, 0.9) * cs * 1.1 * uAmt * uOn;
        gl_FragColor = vec4(col, 1.0);
        #include <colorspace_fragment>
      }`,
  });
  const world = new THREE.Group();
  const bed = new THREE.Mesh(new THREE.PlaneGeometry(4, 4), bedMat);
  bed.rotation.x = -Math.PI / 2;
  bed.position.y = -0.6;
  world.add(bed);
  const stone = new THREE.MeshStandardMaterial({ color: 0xd8d2c4, roughness: 0.9 });
  for (const [x, z, sx, sz] of [
    [0, -2.1, 4.4, 0.2],
    [0, 2.1, 4.4, 0.2],
    [-2.1, 0, 0.2, 4.0],
    [2.1, 0, 0.2, 4.0],
  ] as const) {
    const m = new THREE.Mesh(new THREE.BoxGeometry(sx, 0.9, sz), stone);
    m.position.set(x, -0.2, z);
    world.add(m);
  }
  const shell = new THREE.MeshStandardMaterial({ color: 0xff9eb5, roughness: 0.6 });
  const pebM = new THREE.MeshStandardMaterial({ color: 0x8d9aa0, roughness: 0.8, flatShading: true });
  for (let k = 0; k < 6; k++) {
    const m = new THREE.Mesh(new THREE.DodecahedronGeometry(0.1 + (k % 3) * 0.05), k % 3 === 0 ? shell : pebM);
    m.position.set(Math.cos(k * 2.1) * 1.3, -0.55, Math.sin(k * 2.1) * 1.2);
    m.scale.y = 0.6;
    world.add(m);
  }
  const fish = new THREE.Group();
  const fbody = new THREE.Mesh(new THREE.SphereGeometry(0.18, 20, 14), new THREE.MeshStandardMaterial({ color: 0xff8a2a, roughness: 0.4 }));
  fbody.scale.set(1.5, 0.8, 0.6);
  const tail = new THREE.Mesh(new THREE.ConeGeometry(0.12, 0.2, 3), new THREE.MeshStandardMaterial({ color: 0xffb04a }));
  tail.rotation.z = Math.PI / 2;
  tail.position.x = -0.32;
  fish.add(fbody, tail);
  world.add(fish);
  const surf = new THREE.Mesh(new THREE.PlaneGeometry(4, 4), new THREE.MeshStandardMaterial({ color: 0x8fe0f2, roughness: 0.08, metalness: 0.1, transparent: true, opacity: 0.16, depthWrite: false }));
  surf.rotation.x = -Math.PI / 2;
  surf.position.y = 0.05;
  world.add(surf);
  scene.add(world);
  const cam = new THREE.PerspectiveCamera(40, 1, 0.1, 50);
  cam.position.set(0, 4.2, 3.4);
  cam.lookAt(0, -0.6, 0);
  const tags = new Tags(['빛무늬 없음', '커스틱 켬']);
  return {
    scene,
    camera: cam,
    update(t) {
      uni.uTime.value = t;
      const a = t * 0.6;
      fish.position.set(Math.cos(a) * 1.1, -0.25 + Math.sin(t * 2) * 0.04, Math.sin(a) * 0.9);
      fish.rotation.y = -a - Math.PI / 2;
      tail.rotation.x = Math.sin(t * 9) * 0.5;
    },
    render(r, w, h) {
      splitRender(
        r,
        w,
        h,
        cam,
        () => {
          uni.uOn.value = 0;
          r.render(scene, cam);
        },
        () => {
          uni.uOn.value = 1;
          r.render(scene, cam);
        },
      );
      tags.draw(r, w, h, [
        [0.25, 0.08],
        [0.75, 0.08],
      ]);
    },
    controls: [{ type: 'range', label: '빛무늬 세기', min: 0, max: 2, step: 0.05, value: 1, on: (v) => (uni.uAmt.value = v) }],
    dispose() {
      freeAll(world);
      peb.dispose();
      bg.dispose();
      tags.dispose();
    },
  };
}
```

### u23 견본 항목 — `src/demos/demosShader.ts:2380`
```ts
  u23: { kind: '3d', caption: '왼쪽 그냥 바닥 · 오른쪽 커스틱 — 물결이 모은 햇빛이 바닥에 그물처럼 일렁여요', make: () => causticDemo() }
```

## 관련 기술
- 먼저 알면 좋은 기술: [흐르는 강물 셰이더](https://ai-techstudio.web.app/ai/t/u22.md) `u22`
- 다음에 해 볼 기술: [기울여도 평평한 물](https://ai-techstudio.web.app/ai/t/u25.md) `u25` · [툰 물 (기슭 거품 테)](https://ai-techstudio.web.app/ai/t/i222.md) `i222`
- 참고 문서: [Wikipedia — Caustic (optics)](https://en.wikipedia.org/wiki/Caustic_(optics))
