# 03 — GLSL 최소 지식 + 재사용 스니펫

> 대상: three.js `ShaderMaterial` (WebGL2 컨텍스트, GLSL ES 1.00 문법 작성).
> three는 WebGL2에서 GLSL1 코드를 자동으로 ESSL3로 변환한다(`attribute`→`in`, `varying`→`out/in`, `texture2D`→`texture`, `gl_FragColor`→`pc_fragColor`).
> **따라서 GLSL1 스타일로 쓰는 것이 가장 호환성이 높다.** GLSL3 문법을 쓰려면 `glslVersion: THREE.GLSL3`를 명시해야 한다.

---

## 1. 파이프라인 — 딱 이만큼만 알면 된다

```
정점 데이터(attribute)  ──▶ [ Vertex Shader ]  ──▶ 래스터화 ──▶ [ Fragment Shader ] ──▶ 픽셀
   position, uv, normal        정점당 1회 실행         보간         픽셀당 1회 실행
   커스텀 attribute            gl_Position 출력      (varying)     gl_FragColor 출력
                               varying 출력
```

| | Vertex Shader | Fragment Shader |
|---|---|---|
| 실행 횟수 | 정점 수 (예: 16,384) | 화면 픽셀 수 (예: 1920×1080×DPR² = **800만+**) |
| 필수 출력 | `gl_Position` (클립 공간 vec4) | `gl_FragColor` (RGBA, 0~1) |
| 할 수 있는 것 | 정점 이동/변형, 데이터 준비 | 색 결정, 텍스처 샘플, discard |
| **비용 감각** | 싸다 | **여기가 병목이다** |

### 최적화의 제1법칙
**계산을 fragment에서 vertex로 옮겨라.** 정점 16,384개 vs 픽셀 800만 개 — 500배 차이다.
노이즈, 거리 계산, 삼각함수는 가능한 한 vertex에서 하고 `varying`으로 넘긴다.

### 좌표 변환 체인

```glsl
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
//             └ 카메라 투영     └ view * model    └ 로컬 좌표
```

| 공간 | 얻는 법 |
|---|---|
| 로컬(object) | `position` |
| 월드 | `(modelMatrix * vec4(position, 1.0)).xyz` |
| 뷰(카메라) | `(modelViewMatrix * vec4(position, 1.0)).xyz` |
| 클립 | `projectionMatrix * modelViewMatrix * vec4(position, 1.0)` |
| 화면 UV (fragment) | `gl_FragCoord.xy / uResolution` |

---

## 2. GLSL 문법 최소셋

### 타입

```glsl
float f = 1.0;        // 반드시 소수점! `1` 은 int 라서 타입 에러
int   i = 1;
bool  b = true;
vec2  v2 = vec2(1.0, 2.0);
vec3  v3 = vec3(v2, 3.0);          // 조합 가능
vec4  v4 = vec4(v3, 1.0);
vec4  w  = vec4(0.5);              // 전부 0.5
mat2  m2; mat3 m3; mat4 m4;
sampler2D tex;                     // uniform 으로만 선언 가능
```

### 스위즐 (swizzle)

```glsl
vec4 c = vec4(1.0, 0.5, 0.2, 1.0);
c.rgb          // vec3(1.0, 0.5, 0.2)
c.bgr          // vec3(0.2, 0.5, 1.0) — 순서 바꾸기
c.xy           // vec2(1.0, 0.5)  (xyzw / rgba / stpq 모두 동일)
c.xxxx         // vec4(1.0)       — 반복 가능
c.rg = vec2(0.0);                  // 대입도 가능
```

### precision (모바일에서 매우 중요)

```glsl
precision highp float;    // 32bit. 위치/좌표 계산에 필요
precision mediump float;  // 16bit. 색상/일반 계산에 충분. 모바일에서 ~2배 빠름
precision lowp float;     // 8~10bit. 거의 쓰지 않음
```

**실무 규칙**
- fragment shader 기본은 `mediump`, 정밀도가 필요한 변수만 개별적으로 `highp`.
- vertex shader는 three가 기본 `highp`를 준다. 건드리지 마라.
- GPGPU 위치 시뮬레이션은 **반드시 `highp`**. mediump면 좌표가 지터한다.

```glsl
precision mediump float;
uniform highp sampler2D uPositions;   // 개별 지정
varying highp vec3 vWorldPos;
```

### 자주 쓰는 내장 함수

| 함수 | 의미 | 예시 |
|---|---|---|
| `mix(a, b, t)` | 선형 보간 | `mix(colA, colB, 0.5)` |
| `clamp(x, lo, hi)` | 범위 제한 | `clamp(n, 0.0, 1.0)` |
| `saturate` | **없다.** `clamp(x,0.,1.)` 직접 써라 | |
| `step(edge, x)` | x < edge ? 0 : 1 | 딱딱한 경계 |
| `smoothstep(e0, e1, x)` | 부드러운 0→1 | **가장 많이 쓴다** |
| `fract(x)` | 소수부 | 타일링 `fract(uv * 10.0)` |
| `floor/ceil/round` | | 셀 인덱스 `floor(uv * 10.0)` |
| `mod(x, y)` | 나머지 | 반복 |
| `abs / sign` | | |
| `length(v)` / `distance(a,b)` | | 원형 마스크 |
| `dot(a, b)` | 내적 | 프레넬, 조명 |
| `cross(a, b)` | 외적 (vec3만) | 노멀 |
| `normalize(v)` | 단위벡터 | **0 벡터 주의** → `normalize(v + 1e-5)` |
| `reflect(I, N)` / `refract(I, N, eta)` | 반사/굴절 | 유리 |
| `pow(x, y)` | | `pow(x, 2.2)` 감마 |
| `exp(x)` / `exp2` | | 감쇠 `exp(-d*d)` |
| `sin / cos / atan(y, x)` | | 극좌표, 파동 |
| `min / max` | | |
| `texture2D(sampler, uv)` | 텍스처 샘플 | GLSL3면 `texture()` |
| `dFdx / dFdy / fwidth` | 화면 미분 | 안티에일리어싱 |
| `discard` | 픽셀 버림 | early-Z 깨짐 주의 |

### 분기와 루프 — 비용 감각

```glsl
// 나쁨: 동적 분기. GPU 는 워프 단위로 실행하므로 양쪽 다 계산될 수 있다
if (uMode > 0.5) { heavyA(); } else { heavyB(); }

// 좋음: 산술로 대체
result = mix(a, b, step(0.5, uMode));

// 좋음: 컴파일 타임 분기 (material.defines 로 제어)
#if defined(USE_GLOW)
  color += uGlowColor * glowMask;
#endif
```

```glsl
// 루프 카운트는 반드시 상수여야 한다 (GLSL ES 1.0 제약)
#define OCTAVES 4

float sum = 0.0, amp = 0.5, freq = 1.0;
for (int i = 0; i < OCTAVES; i++) {
  sum += snoise(p * freq) * amp;
  freq *= 2.02;
  amp  *= 0.5;
}

// uniform 을 루프 상한으로 쓸 수 없다. 상수 상한 + break 를 쓴다
#define MAX_LIGHTS 8
uniform float uLightCount;
uniform vec3  uLightPos[MAX_LIGHTS];

vec3 lighting = vec3(0.0);
for (int i = 0; i < MAX_LIGHTS; i++) {
  if (float(i) >= uLightCount) break;
  float d = distance(vWorldPos, uLightPos[i]);
  lighting += vec3(1.0) / (1.0 + d * d);
}
```

---

## 3. three가 자동으로 주입하는 것

### `ShaderMaterial` (권장)

three가 아래를 **자동으로 앞에 붙여준다**. 다시 선언하면 `redefinition` 에러가 난다.

**Vertex shader 자동 제공**

```glsl
// uniform
uniform mat4 modelMatrix;         // object → world
uniform mat4 modelViewMatrix;     // object → view
uniform mat4 projectionMatrix;    // view → clip
uniform mat4 viewMatrix;          // world → view
uniform mat3 normalMatrix;        // 노멀 변환용 (역전치)
uniform vec3 cameraPosition;      // 월드 공간 카메라 위치
uniform bool isOrthographic;

// attribute (기본 지오메트리)
attribute vec3 position;
attribute vec3 normal;
attribute vec2 uv;
// 지오메트리에 있으면: attribute vec4 color; attribute vec2 uv1; ...
// InstancedMesh: attribute mat4 instanceMatrix; attribute vec3 instanceColor;
```

**Fragment shader 자동 제공**

```glsl
uniform mat4 viewMatrix;
uniform vec3 cameraPosition;
uniform bool isOrthographic;
// + 톤매핑/색공간 함수 (toneMapping, linearToOutputTexel)
```

### `RawShaderMaterial`

아무것도 주입되지 않는다. `precision`부터 `projectionMatrix`까지 전부 직접 선언해야 하고,
`#include <tonemapping_fragment>` 같은 청크도 **쓸 수 없다**.
→ **웹디자인 작업에서는 쓸 이유가 거의 없다.** `ShaderMaterial`을 써라.

### 유용한 ShaderChunk

```glsl
#include <common>                  // PI, RECIPROCAL_PI, saturate 매크로 등 유틸
#include <tonemapping_fragment>    // gl_FragColor 에 톤매핑 적용
#include <colorspace_fragment>     // 출력 색공간 변환 (r154+ 이름. 이전엔 encodings_fragment)
#include <fog_fragment>            // 안개 (fog_pars_fragment 도 필요)
#include <dithering_fragment>      // 밴딩 억제
#include <logdepthbuf_vertex>      // 로그 깊이 버퍼
```

fragment shader **마지막**에 이 순서로:

```glsl
  gl_FragColor = vec4(color, alpha);
  #include <tonemapping_fragment>
  #include <colorspace_fragment>
}
```

### `onBeforeCompile`로 내장 머티리얼 확장

`MeshStandardMaterial`의 조명 계산을 유지한 채 코드를 삽입할 때.

```js
material.onBeforeCompile = (shader) => {
  shader.uniforms.uTime = { value: 0 }

  shader.vertexShader = shader.vertexShader
    .replace('#include <common>', `
      #include <common>
      uniform float uTime;
      varying vec3 vLocalPos;
    `)
    .replace('#include <begin_vertex>', `
      #include <begin_vertex>
      vLocalPos = position;
      transformed.y += sin(position.x * 3.0 + uTime) * 0.1;
    `)
    // 노멀도 바꿔야 조명이 맞는다
    .replace('#include <beginnormal_vertex>', `
      #include <beginnormal_vertex>
      // objectNormal 을 여기서 수정
    `)

  shader.fragmentShader = shader.fragmentShader
    .replace('#include <dithering_fragment>', `
      #include <dithering_fragment>
      gl_FragColor.rgb *= 1.1;
    `)
}
// 필수: 안 하면 셰이더 캐시가 오작동한다
material.customProgramCacheKey = () => 'my-material-v1'
```

**자주 쓰는 훅 포인트**

| 청크 | 위치 | 용도 |
|---|---|---|
| `<common>` | 최상단 | uniform/varying/함수 선언 |
| `<beginnormal_vertex>` | vertex | `objectNormal` 수정 |
| `<begin_vertex>` | vertex | `transformed`(정점 위치) 수정 |
| `<project_vertex>` | vertex 끝 | `mvPosition`, `gl_Position` 이후 처리 |
| `<map_fragment>` | fragment | `diffuseColor` 수정 (알베도) |
| `<alphatest_fragment>` | fragment | discard 삽입 |
| `<emissivemap_fragment>` | fragment | `totalEmissiveRadiance` 수정 |
| `<dithering_fragment>` | fragment 끝 | 최종 `gl_FragColor` 후처리 |

---

## 4. uniform 전달 — JS ↔ GLSL 타입 매핑

```js
const uniforms = {
  uFloat:   { value: 1.0 },                              // float
  uInt:     { value: 1 },                                // int
  uBool:    { value: true },                             // bool
  uVec2:    { value: new THREE.Vector2(1, 2) },          // vec2
  uVec3:    { value: new THREE.Vector3(1, 2, 3) },       // vec3
  uColor:   { value: new THREE.Color('#ff0080') },       // vec3 (자동 linear 변환!)
  uVec4:    { value: new THREE.Vector4(1, 2, 3, 4) },    // vec4
  uMat3:    { value: new THREE.Matrix3() },              // mat3
  uMat4:    { value: new THREE.Matrix4() },              // mat4
  uTex:     { value: texture },                          // sampler2D
  uCube:    { value: cubeTexture },                      // samplerCube
  uFloats:  { value: [1, 2, 3] },                        // float[3]
  uVecs:    { value: [new THREE.Vector3(), ...] },       // vec3[N]
}
```

### 갱신 규칙

```js
// ✅ .value 를 바꾼다. needsUpdate 불필요
uniforms.uFloat.value = t
uniforms.uVec2.value.set(x, y)        // 새 객체 생성 X — GC 압력 감소
uniforms.uColor.value.set('#00ff88')

// ❌ 이렇게 하면 매 프레임 객체를 만든다
uniforms.uVec2.value = new THREE.Vector2(x, y)

// 텍스처를 교체할 때만 needsUpdate
uniforms.uTex.value = newTexture
material.needsUpdate = true   // uniform 개수/타입이 바뀔 때만. 값 변경엔 불필요
```

### 배열 uniform은 크기가 고정이다

```glsl
#define MAX_POINTS 8
uniform vec3  uPoints[MAX_POINTS];
uniform int   uPointCount;
uniform float uRadius;

// 여러 개의 인터랙션 포인트가 만드는 영향력의 합
float influence = 0.0;
for (int i = 0; i < MAX_POINTS; i++) {
  if (i >= uPointCount) break;
  float d = distance(vWorldPos, uPoints[i]);
  influence += smoothstep(uRadius, 0.0, d);
}
influence = clamp(influence, 0.0, 1.0);
```

배열 크기를 바꾸려면 셰이더를 재컴파일해야 한다(`defines` 변경 + `needsUpdate = true`).

---

## 5. 스니펫 라이브러리 — `src/shaders/lib/`

각 파일은 include guard가 있어 중복 `#include`해도 안전하다.

### 5.1 `lib/uv.glsl` — UV 조작

```glsl
#ifndef LIB_UV_GLSL
#define LIB_UV_GLSL

const float PI  = 3.141592653589793;
const float TAU = 6.283185307179586;

/** object-fit: cover. 이미지 비율을 유지하며 플레인을 채운다 */
vec2 coverUv(vec2 uv, vec2 planeSize, vec2 imageSize) {
  vec2 ratio = vec2(
    min((planeSize.x / planeSize.y) / (imageSize.x / imageSize.y), 1.0),
    min((planeSize.y / planeSize.x) / (imageSize.y / imageSize.x), 1.0)
  );
  return vec2(uv.x * ratio.x + (1.0 - ratio.x) * 0.5,
              uv.y * ratio.y + (1.0 - ratio.y) * 0.5);
}

/** object-fit: contain */
vec2 containUv(vec2 uv, vec2 planeSize, vec2 imageSize) {
  vec2 ratio = vec2(
    max((planeSize.x / planeSize.y) / (imageSize.x / imageSize.y), 1.0),
    max((planeSize.y / planeSize.x) / (imageSize.y / imageSize.x), 1.0)
  );
  return vec2(uv.x * ratio.x + (1.0 - ratio.x) * 0.5,
              uv.y * ratio.y + (1.0 - ratio.y) * 0.5);
}

/** 화면 비율 보정: 짧은 축 기준으로 -1..1 정규화. 노이즈가 늘어지지 않는다 */
vec2 aspectUv(vec2 fragCoord, vec2 resolution) {
  return (fragCoord * 2.0 - resolution) / min(resolution.x, resolution.y);
}

/** 중심 기준 회전 */
vec2 rotateUv(vec2 uv, float angle, vec2 center) {
  float s = sin(angle), c = cos(angle);
  uv -= center;
  uv = mat2(c, -s, s, c) * uv;
  return uv + center;
}

/** 중심 기준 스케일 (>1 = 축소, <1 = 확대) */
vec2 scaleUv(vec2 uv, float scale, vec2 center) {
  return (uv - center) * scale + center;
}

/** 타일링. .xy = 셀 내부 UV, .zw = 셀 인덱스 */
vec4 tileUv(vec2 uv, vec2 count) {
  vec2 g = uv * count;
  return vec4(fract(g), floor(g));
}

/** 극좌표. x = 각도(0..1), y = 반지름 */
vec2 polarUv(vec2 uv, vec2 center) {
  vec2 d = uv - center;
  return vec2(atan(d.y, d.x) / TAU + 0.5, length(d));
}

/** 만화경 */
vec2 kaleidoUv(vec2 uv, vec2 center, float segments) {
  vec2 p = polarUv(uv, center);
  float seg = 1.0 / segments;
  p.x = abs(mod(p.x, seg * 2.0) - seg) / seg;
  float a = p.x * TAU / segments;
  return center + vec2(cos(a), sin(a)) * p.y;
}

/** 배럴/핀쿠션 왜곡. k > 0 배럴, k < 0 핀쿠션 */
vec2 barrelUv(vec2 uv, float k) {
  vec2 c = uv - 0.5;
  float r2 = dot(c, c);
  return 0.5 + c * (1.0 + k * r2);
}

/** 중심에서 퍼지는 물결 */
vec2 rippleUv(vec2 uv, vec2 center, float time, float amplitude, float frequency, float speed) {
  vec2 d = uv - center;
  float r = length(d);
  float wave = sin(r * frequency - time * speed) * amplitude * exp(-r * 3.0);
  return uv + normalize(d + 1e-5) * wave;
}

/** 픽셀화 */
vec2 pixelateUv(vec2 uv, vec2 pixels) {
  return floor(uv * pixels) / pixels;
}

#endif
```

### 5.2 `lib/noise.glsl` — 해시 · 밸류 노이즈 · 심플렉스 · FBM

```glsl
#ifndef LIB_NOISE_GLSL
#define LIB_NOISE_GLSL

/* ===== 해시 (의사난수) ================================================= */

float hash11(float p) {
  p = fract(p * 0.1031);
  p *= p + 33.33;
  return fract((p + p) * p);
}

float hash21(vec2 p) {
  vec3 p3 = fract(vec3(p.xyx) * 0.1031);
  p3 += dot(p3, p3.yzx + 33.33);
  return fract((p3.x + p3.y) * p3.z);
}

vec2 hash22(vec2 p) {
  vec3 p3 = fract(vec3(p.xyx) * vec3(0.1031, 0.1030, 0.0973));
  p3 += dot(p3, p3.yzx + 33.33);
  return fract((p3.xx + p3.yz) * p3.zy);
}

float hash31(vec3 p) {
  p = fract(p * 0.1031);
  p += dot(p, p.yzx + 33.33);
  return fract((p.x + p.y) * p.z);
}

vec3 hash33(vec3 p) {
  p = fract(p * vec3(0.1031, 0.1030, 0.0973));
  p += dot(p, p.yxz + 33.33);
  return fract((p.xxy + p.yxx) * p.zyx);
}

/* ===== Value noise (싸다. simplex 의 1/3 비용) ========================= */

float vnoise2(vec2 p) {
  vec2 i = floor(p), f = fract(p);
  f = f * f * (3.0 - 2.0 * f);                 // smoothstep 보간
  return mix(mix(hash21(i),               hash21(i + vec2(1.0, 0.0)), f.x),
             mix(hash21(i + vec2(0.0, 1.0)), hash21(i + vec2(1.0, 1.0)), f.x), f.y);
}

float vnoise3(vec3 p) {
  vec3 i = floor(p), f = fract(p);
  f = f * f * (3.0 - 2.0 * f);
  return mix(
    mix(mix(hash31(i + vec3(0,0,0)), hash31(i + vec3(1,0,0)), f.x),
        mix(hash31(i + vec3(0,1,0)), hash31(i + vec3(1,1,0)), f.x), f.y),
    mix(mix(hash31(i + vec3(0,0,1)), hash31(i + vec3(1,0,1)), f.x),
        mix(hash31(i + vec3(0,1,1)), hash31(i + vec3(1,1,1)), f.x), f.y), f.z);
}

/* ===== Simplex noise 3D (Ian McEwan, Ashima Arts / stegu — MIT) ========
 * https://github.com/stegu/webgl-noise
 * 반환 범위 대략 -1..1
 * ===================================================================== */

vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec4 permute(vec4 x) { return mod289(((x * 34.0) + 10.0) * x); }
vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }

float snoise(vec3 v) {
  const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0);
  const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);

  vec3 i  = floor(v + dot(v, C.yyy));
  vec3 x0 = v - i + dot(i, C.xxx);

  vec3 g = step(x0.yzx, x0.xyz);
  vec3 l = 1.0 - g;
  vec3 i1 = min(g.xyz, l.zxy);
  vec3 i2 = max(g.xyz, l.zxy);

  vec3 x1 = x0 - i1 + C.xxx;
  vec3 x2 = x0 - i2 + C.yyy;
  vec3 x3 = x0 - D.yyy;

  i = mod289(i);
  vec4 p = permute(permute(permute(
             i.z + vec4(0.0, i1.z, i2.z, 1.0))
           + i.y + vec4(0.0, i1.y, i2.y, 1.0))
           + i.x + vec4(0.0, i1.x, i2.x, 1.0));

  float n_ = 0.142857142857;
  vec3 ns = n_ * D.wyz - D.xzx;

  vec4 j = p - 49.0 * floor(p * ns.z * ns.z);

  vec4 x_ = floor(j * ns.z);
  vec4 y_ = floor(j - 7.0 * x_);

  vec4 x = x_ * ns.x + ns.yyyy;
  vec4 y = y_ * ns.x + ns.yyyy;
  vec4 h = 1.0 - abs(x) - abs(y);

  vec4 b0 = vec4(x.xy, y.xy);
  vec4 b1 = vec4(x.zw, y.zw);

  vec4 s0 = floor(b0) * 2.0 + 1.0;
  vec4 s1 = floor(b1) * 2.0 + 1.0;
  vec4 sh = -step(h, vec4(0.0));

  vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
  vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;

  vec3 p0 = vec3(a0.xy, h.x);
  vec3 p1 = vec3(a0.zw, h.y);
  vec3 p2 = vec3(a1.xy, h.z);
  vec3 p3 = vec3(a1.zw, h.w);

  vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3)));
  p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w;

  vec4 m = max(0.5 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0);
  m = m * m;
  return 105.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3)));
}

/** 2D 편의 함수: z 를 시간축으로 쓴다 */
float snoise2(vec2 p) { return snoise(vec3(p, 0.0)); }

/* ===== FBM (fractal brownian motion) ================================== */

#ifndef FBM_OCTAVES
  #define FBM_OCTAVES 4
#endif

float fbm3(vec3 p) {
  float sum = 0.0, amp = 0.5, freq = 1.0;
  for (int i = 0; i < FBM_OCTAVES; i++) {
    sum += snoise(p * freq) * amp;
    freq *= 2.02;      // 2.0 정확히 쓰면 격자 패턴이 보인다
    amp  *= 0.5;
  }
  return sum;
}

float fbm2(vec2 p) {
  float sum = 0.0, amp = 0.5, freq = 1.0;
  for (int i = 0; i < FBM_OCTAVES; i++) {
    sum += vnoise2(p * freq) * amp;
    freq *= 2.02;
    amp  *= 0.5;
  }
  return sum;
}

/** 능선 노이즈: 산맥/번개 같은 날카로운 결 */
float ridged3(vec3 p) {
  float sum = 0.0, amp = 0.5, freq = 1.0;
  for (int i = 0; i < FBM_OCTAVES; i++) {
    float n = 1.0 - abs(snoise(p * freq));
    sum += n * n * amp;
    freq *= 2.02;
    amp  *= 0.5;
  }
  return sum;
}

/** 도메인 워프: 노이즈로 좌표를 흔든 뒤 다시 노이즈. "유기적"의 핵심 */
float warpedFbm3(vec3 p, float strength) {
  vec3 q = vec3(fbm3(p), fbm3(p + vec3(5.2, 1.3, 3.7)), fbm3(p + vec3(9.1, 7.4, 2.8)));
  return fbm3(p + q * strength);
}

/* ===== Voronoi / Worley (셀 패턴) ===================================== */

/** x = 가장 가까운 점까지의 거리, y = 셀 ID */
vec2 voronoi2(vec2 p) {
  vec2 n = floor(p), f = fract(p);
  float minDist = 8.0;
  float cellId = 0.0;
  for (int j = -1; j <= 1; j++) {
    for (int i = -1; i <= 1; i++) {
      vec2 g = vec2(float(i), float(j));
      vec2 o = hash22(n + g);
      vec2 r = g + o - f;
      float d = dot(r, r);
      if (d < minDist) { minDist = d; cellId = hash21(n + g); }
    }
  }
  return vec2(sqrt(minDist), cellId);
}

/** 셀 경계선까지의 거리 (테두리 그리기용) */
float voronoiEdge2(vec2 p) {
  vec2 n = floor(p), f = fract(p);
  vec2 mg, mr;
  float md = 8.0;
  for (int j = -1; j <= 1; j++) for (int i = -1; i <= 1; i++) {
    vec2 g = vec2(float(i), float(j));
    vec2 o = hash22(n + g);
    vec2 r = g + o - f;
    float d = dot(r, r);
    if (d < md) { md = d; mr = r; mg = g; }
  }
  md = 8.0;
  for (int j = -2; j <= 2; j++) for (int i = -2; i <= 2; i++) {
    vec2 g = mg + vec2(float(i), float(j));
    vec2 o = hash22(n + g);
    vec2 r = g + o - f;
    if (dot(mr - r, mr - r) > 0.00001) {
      md = min(md, dot(0.5 * (mr + r), normalize(r - mr)));
    }
  }
  return md;
}

#endif
```

### 5.3 `lib/curl.glsl` — 컬 노이즈 (발산 없는 유동 벡터장)

```glsl
#ifndef LIB_CURL_GLSL
#define LIB_CURL_GLSL

#include './noise.glsl'

vec3 snoiseVec3(vec3 x) {
  return vec3(
    snoise(vec3(x.x,          x.y,          x.z)),
    snoise(vec3(x.y - 19.1,   x.z + 33.4,   x.x + 47.2)),
    snoise(vec3(x.z + 74.2,   x.x - 124.5,  x.y + 99.4))
  );
}

/**
 * 컬 노이즈: 포텐셜장의 회전(curl)을 취해 발산이 0인 벡터장을 만든다.
 * 파티클이 뭉치거나 흩어지지 않고 "유체처럼" 흐른다.
 * 비용: snoise 18회. 매우 비싸다. GPGPU 시뮬 패스에서만 써라.
 */
vec3 curlNoise(vec3 p) {
  const float e = 0.1;
  vec3 dx = vec3(e, 0.0, 0.0);
  vec3 dy = vec3(0.0, e, 0.0);
  vec3 dz = vec3(0.0, 0.0, e);

  vec3 px0 = snoiseVec3(p - dx), px1 = snoiseVec3(p + dx);
  vec3 py0 = snoiseVec3(p - dy), py1 = snoiseVec3(p + dy);
  vec3 pz0 = snoiseVec3(p - dz), pz1 = snoiseVec3(p + dz);

  float x = py1.z - py0.z - pz1.y + pz0.y;
  float y = pz1.x - pz0.x - px1.z + px0.z;
  float z = px1.y - px0.y - py1.x + py0.x;

  const float divisor = 1.0 / (2.0 * e);
  return normalize(vec3(x, y, z) * divisor);
}

/**
 * 싼 버전: 3방향 노이즈. 진짜 curl 은 아니지만 육안으로 거의 구분 안 된다.
 * 비용: snoise 3회. GPGPU 가 아닌 vertex shader 에서 쓰기 좋다.
 */
vec3 pseudoCurl(vec3 p) {
  return vec3(
    snoise(p),
    snoise(p + vec3(31.416, 0.0, 0.0)),
    snoise(p + vec3(0.0, 17.777, 0.0))
  );
}

#endif
```

### 5.4 `lib/easing.glsl` — 이징 · 리매핑 · 페이드

```glsl
#ifndef LIB_EASING_GLSL
#define LIB_EASING_GLSL

float saturate_(float x) { return clamp(x, 0.0, 1.0); }
vec3  saturate_(vec3 x)  { return clamp(x, 0.0, 1.0); }

/** 범위 재매핑 */
float remap(float v, float inMin, float inMax, float outMin, float outMax) {
  return outMin + (v - inMin) * (outMax - outMin) / (inMax - inMin);
}

/** 범위 재매핑 + 클램프 */
float remapc(float v, float inMin, float inMax, float outMin, float outMax) {
  return clamp(remap(v, inMin, inMax, outMin, outMax), min(outMin, outMax), max(outMin, outMax));
}

/** 0→1→0 곡선. 구간 중앙에서 최대 */
float pulse(float x, float center, float width) {
  return smoothstep(center - width, center, x) - smoothstep(center, center + width, x);
}

/** smoothstep 보다 부드러운 5차 보간 */
float quintic(float t) { return t * t * t * (t * (t * 6.0 - 15.0) + 10.0); }

/* --- 표준 이징 (Robert Penner) --- */
float easeInQuad(float t)    { return t * t; }
float easeOutQuad(float t)   { return t * (2.0 - t); }
float easeInOutQuad(float t) { return t < 0.5 ? 2.0*t*t : -1.0 + (4.0 - 2.0*t)*t; }

float easeInCubic(float t)   { return t * t * t; }
float easeOutCubic(float t)  { float f = t - 1.0; return f*f*f + 1.0; }
float easeInOutCubic(float t){ return t < 0.5 ? 4.0*t*t*t : (t-1.0)*(2.0*t-2.0)*(2.0*t-2.0)+1.0; }

float easeInExpo(float t)    { return t <= 0.0 ? 0.0 : pow(2.0, 10.0 * (t - 1.0)); }
float easeOutExpo(float t)   { return t >= 1.0 ? 1.0 : 1.0 - pow(2.0, -10.0 * t); }

float easeOutBack(float t) {
  const float c1 = 1.70158, c3 = c1 + 1.0;
  float f = t - 1.0;
  return 1.0 + c3 * f * f * f + c1 * f * f;
}

float easeOutElastic(float t) {
  const float c4 = 6.283185307179586 / 3.0;
  if (t <= 0.0) return 0.0;
  if (t >= 1.0) return 1.0;
  return pow(2.0, -10.0 * t) * sin((t * 10.0 - 0.75) * c4) + 1.0;
}

/** 화면 픽셀 단위 안티에일리어싱 경계. SDF 와 함께 쓴다 */
float aastep(float threshold, float value) {
  float afwidth = fwidth(value) * 0.7;
  return smoothstep(threshold - afwidth, threshold + afwidth, value);
}

/** 지수 감쇠 (프레임레이트 독립). GLSL 안에서 시간 적분할 때 */
float damp(float current, float target, float lambda, float dt) {
  return mix(current, target, 1.0 - exp(-lambda * dt));
}

#endif
```

### 5.5 `lib/color.glsl` — 팔레트 · 색공간 · 그레인 · 디더

```glsl
#ifndef LIB_COLOR_GLSL
#define LIB_COLOR_GLSL

/**
 * Inigo Quilez 코사인 팔레트.
 * col = a + b * cos(2π(c*t + d))
 * https://iquilezles.org/articles/palettes/
 *
 * 대표 조합:
 *  rainbow : a(.5,.5,.5) b(.5,.5,.5) c(1,1,1)   d(0,.33,.67)
 *  warm    : a(.5,.5,.5) b(.5,.5,.5) c(1,1,1)   d(0,.1,.2)
 *  teal    : a(.5,.5,.5) b(.5,.5,.5) c(1,1,.5)  d(.8,.9,.3)
 *  purple  : a(.8,.5,.4) b(.2,.4,.2) c(2,1,1)   d(0,.25,.25)
 */
vec3 palette(float t, vec3 a, vec3 b, vec3 c, vec3 d) {
  return a + b * cos(6.283185307179586 * (c * t + d));
}

vec3 rgb2hsv(vec3 c) {
  vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
  vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
  vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
  float d = q.x - min(q.w, q.y);
  float e = 1.0e-10;
  return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}

vec3 hsv2rgb(vec3 c) {
  vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
  vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
  return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}

/** 수동 색공간 변환 (three 청크를 못 쓰는 상황용) */
vec3 srgbToLinear(vec3 c) {
  return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(0.04045, c));
}
vec3 linearToSrgb(vec3 c) {
  return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));
}

/** ACES 근사 톤매핑 (Narkowicz) */
vec3 acesApprox(vec3 x) {
  const float a = 2.51, b = 0.03, c = 2.43, d = 0.59, e = 0.14;
  return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0);
}

/** 채도 조절 */
vec3 saturation(vec3 c, float amount) {
  float l = dot(c, vec3(0.2126, 0.7152, 0.0722));
  return mix(vec3(l), c, amount);
}

/** 콘트라스트 (pivot 0.5 기준) */
vec3 contrast(vec3 c, float amount) {
  return (c - 0.5) * amount + 0.5;
}

/** 필름 그레인. time 을 넣으면 매 프레임 달라진다 */
float grain(vec2 fragCoord, float time) {
  return fract(sin(dot(fragCoord + time, vec2(12.9898, 78.233))) * 43758.5453);
}

/** 8bit 프레임버퍼 밴딩 제거. 그라디언트에는 거의 필수 */
vec3 dither8(vec3 color, vec2 fragCoord) {
  float d = fract(sin(dot(fragCoord, vec2(12.9898, 78.233))) * 43758.5453);
  return color + (d - 0.5) / 255.0;
}

/** 4x4 Bayer 디더 (레트로 룩) */
float bayer4(vec2 fragCoord) {
  int x = int(mod(fragCoord.x, 4.0));
  int y = int(mod(fragCoord.y, 4.0));
  int index = x + y * 4;
  float m[16];
  m[0]=0.0;  m[1]=8.0;  m[2]=2.0;  m[3]=10.0;
  m[4]=12.0; m[5]=4.0;  m[6]=14.0; m[7]=6.0;
  m[8]=3.0;  m[9]=11.0; m[10]=1.0; m[11]=9.0;
  m[12]=15.0;m[13]=7.0; m[14]=13.0;m[15]=5.0;
  for (int i = 0; i < 16; i++) { if (i == index) return m[i] / 16.0; }
  return 0.0;
}

/** 비네트 */
float vignette(vec2 uv, float offset, float darkness) {
  vec2 c = (uv - 0.5) * 2.0;
  return clamp(1.0 - dot(c, c) * darkness + offset, 0.0, 1.0);
}

/** 스캔라인 */
float scanline(vec2 uv, float count, float intensity, float time) {
  return 1.0 - intensity * (0.5 + 0.5 * sin((uv.y + time) * count * 3.14159265));
}

#endif
```

### 5.6 `lib/sdf.glsl` — 2D 시그니처 거리 함수

> 출처 개념: [iquilezles.org/articles/distfunctions2d](https://iquilezles.org/articles/distfunctions2d/)
> 반환값 < 0 → 내부, = 0 → 경계, > 0 → 외부

```glsl
#ifndef LIB_SDF_GLSL
#define LIB_SDF_GLSL

float sdCircle(vec2 p, float r) { return length(p) - r; }

float sdBox(vec2 p, vec2 b) {
  vec2 d = abs(p) - b;
  return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}

float sdRoundBox(vec2 p, vec2 b, float r) {
  return sdBox(p, b - r) - r;
}

float sdSegment(vec2 p, vec2 a, vec2 b) {
  vec2 pa = p - a, ba = b - a;
  float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
  return length(pa - ba * h);
}

float sdTriangleIso(vec2 p, vec2 q) {
  p.x = abs(p.x);
  vec2 a = p - q * clamp(dot(p, q) / dot(q, q), 0.0, 1.0);
  vec2 b = p - q * vec2(clamp(p.x / q.x, 0.0, 1.0), 1.0);
  float s = -sign(q.y);
  vec2 d = min(vec2(dot(a, a), s * (p.x * q.y - p.y * q.x)),
               vec2(dot(b, b), s * (p.y - q.y)));
  return -sqrt(d.x) * sign(d.y);
}

float sdHexagon(vec2 p, float r) {
  const vec3 k = vec3(-0.866025404, 0.5, 0.577350269);
  p = abs(p);
  p -= 2.0 * min(dot(k.xy, p), 0.0) * k.xy;
  p -= vec2(clamp(p.x, -k.z * r, k.z * r), r);
  return length(p) * sign(p.y);
}

float sdStar5(vec2 p, float r, float rf) {
  const vec2 k1 = vec2(0.809016994, -0.587785252);
  const vec2 k2 = vec2(-k1.x, k1.y);
  p.x = abs(p.x);
  p -= 2.0 * max(dot(k1, p), 0.0) * k1;
  p -= 2.0 * max(dot(k2, p), 0.0) * k2;
  p.x = abs(p.x);
  p.y -= r;
  vec2 ba = rf * vec2(-k1.y, k1.x) - vec2(0.0, 1.0);
  float h = clamp(dot(p, ba) / dot(ba, ba), 0.0, r);
  return length(p - ba * h) * sign(p.y * ba.x - p.x * ba.y);
}

/* --- 불리언 연산 --- */
float opUnion(float d1, float d2)        { return min(d1, d2); }
float opSubtract(float d1, float d2)     { return max(-d1, d2); }
float opIntersect(float d1, float d2)    { return max(d1, d2); }

/** 부드러운 합집합 (메타볼). k = 블렌딩 폭 */
float opSmoothUnion(float d1, float d2, float k) {
  float h = clamp(0.5 + 0.5 * (d2 - d1) / k, 0.0, 1.0);
  return mix(d2, d1, h) - k * h * (1.0 - h);
}

float opSmoothSubtract(float d1, float d2, float k) {
  float h = clamp(0.5 - 0.5 * (d2 + d1) / k, 0.0, 1.0);
  return mix(d2, -d1, h) + k * h * (1.0 - h);
}

/** 외곽선. width 만큼의 링 */
float opOutline(float d, float width) { return abs(d) - width; }

/** SDF → 안티에일리어싱된 채움 마스크 */
float sdfFill(float d) { return 1.0 - smoothstep(-fwidth(d), fwidth(d), d); }

/** SDF → 안티에일리어싱된 선 */
float sdfStroke(float d, float width) {
  float w = fwidth(d);
  return 1.0 - smoothstep(width - w, width + w, abs(d));
}

#endif
```

---

## 6. 자주 쓰는 조합 레시피

### 6.1 화면 UV와 종횡비 (거의 모든 배경 셰이더의 첫 줄)

```glsl
uniform vec2 uResolution;

void main() {
  // 0..1 스크린 UV
  vec2 uv = gl_FragCoord.xy / uResolution;

  // 짧은 축 기준 -1..1 (원이 타원으로 안 찌그러진다)
  vec2 p = (gl_FragCoord.xy * 2.0 - uResolution) / min(uResolution.x, uResolution.y);

  // 종횡비 보정된 0..1 (그리드용)
  vec2 g = uv;
  g.x *= uResolution.x / uResolution.y;
}
```

### 6.2 텍스처 디스플레이스먼트 + 색수차 (P8/P9의 핵심)

```glsl
uniform sampler2D uTexture;
uniform float uAmount;      // 0 ~ 0.02
uniform vec2  uDirection;   // 정규화된 방향

vec3 rgbShift(sampler2D tex, vec2 uv, vec2 dir, float amount) {
  float r = texture2D(tex, uv + dir * amount).r;
  float g = texture2D(tex, uv).g;
  float b = texture2D(tex, uv - dir * amount).b;
  return vec3(r, g, b);
}

/** 방사형 색수차 (렌즈처럼 가장자리만) */
vec3 radialAberration(sampler2D tex, vec2 uv, float amount) {
  vec2 dir = uv - 0.5;
  float d = dot(dir, dir);          // 중심에서 멀수록 큼
  float r = texture2D(tex, uv - dir * amount * d).r;
  float g = texture2D(tex, uv).g;
  float b = texture2D(tex, uv + dir * amount * d).b;
  return vec3(r, g, b);
}
```

### 6.3 정점 변위 후 노멀 재계산 (조명이 필요할 때 필수)

정점을 노이즈로 움직이면 원래 노멀이 틀린다. 유한차분으로 다시 계산한다.

```glsl
#include './lib/noise.glsl'

uniform float uTime;
uniform float uAmplitude;
uniform float uFrequency;

varying vec3 vNormal;
varying vec3 vWorldPos;

/** 위치 → 변위 높이 */
float displace(vec3 p) {
  return snoise(p * uFrequency + vec3(0.0, 0.0, uTime * 0.2)) * uAmplitude;
}

void main() {
  vec3 pos = position;
  float d = displace(pos);
  pos += normal * d;

  // 접평면의 두 방향으로 아주 조금 이동시켜 새 노멀을 구한다
  float eps = 0.01;
  vec3 tangent  = normalize(cross(normal, vec3(0.0, 1.0, 0.0) + 1e-4));
  vec3 bitangent = normalize(cross(normal, tangent));

  vec3 pT = position + tangent * eps;
  vec3 pB = position + bitangent * eps;
  pT += normal * displace(pT);
  pB += normal * displace(pB);

  vec3 newNormal = normalize(cross(pT - pos, pB - pos));
  // 방향이 뒤집힐 수 있으므로 원래 노멀과 부호를 맞춘다
  newNormal *= sign(dot(newNormal, normal));

  vNormal = normalize(normalMatrix * newNormal);
  vWorldPos = (modelMatrix * vec4(pos, 1.0)).xyz;

  gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
```

> 비용: `displace()`를 3번 호출한다(노이즈 3배). vertex shader이므로 대부분 감당 가능하다.

### 6.4 프레넬 (가장자리 발광 — "고급스러움"의 90%)

```glsl
// vertex
varying vec3 vNormalW;
varying vec3 vViewDir;

void main() {
  vec4 worldPos = modelMatrix * vec4(position, 1.0);
  vNormalW = normalize(mat3(modelMatrix) * normal);
  vViewDir = normalize(cameraPosition - worldPos.xyz);
  gl_Position = projectionMatrix * viewMatrix * worldPos;
}
```

```glsl
// fragment
uniform vec3  uColor;
uniform vec3  uRimColor;
uniform float uRimPower;      // 1.5 ~ 5.0
uniform float uRimIntensity;  // 0.3 ~ 2.0

varying vec3 vNormalW;
varying vec3 vViewDir;

void main() {
  vec3 n = normalize(vNormalW);
  vec3 v = normalize(vViewDir);

  float fresnel = pow(1.0 - clamp(dot(n, v), 0.0, 1.0), uRimPower);

  vec3 col = mix(uColor, uRimColor, fresnel * uRimIntensity);

  gl_FragColor = vec4(col, 1.0);
  #include <tonemapping_fragment>
  #include <colorspace_fragment>
}
```

### 6.5 깊이 기반 페이드 (파티클/안개)

```glsl
// vertex
varying float vDepth;
void main() {
  vec4 mv = modelViewMatrix * vec4(position, 1.0);
  vDepth = -mv.z;                              // 카메라로부터의 거리
  gl_Position = projectionMatrix * mv;
}

// fragment
uniform float uFogNear;
uniform float uFogFar;
varying float vDepth;
void main() {
  float fog = smoothstep(uFogNear, uFogFar, vDepth);
  vec3 col = mix(baseColor, uFogColor, fog);
  gl_FragColor = vec4(col, 1.0 - fog);
}
```

### 6.6 시간 기반 순차 애니메이션 (스태거)

```glsl
uniform float uProgress;   // 0 ~ 1 (JS에서 GSAP 로 트윈)
uniform float uTotal;      // 전체 개수
uniform float uStagger;    // 0 ~ 0.9. 개체 간 지연의 총 폭

attribute float aIndex;    // 개체 인덱스 (InstancedBufferAttribute)

varying float vLocal;

void main() {
  // 인덱스가 클수록 늦게 시작한다
  float delay = (aIndex / max(uTotal, 1.0)) * uStagger;
  float local = clamp((uProgress - delay) / max(1.0 - uStagger, 1e-4), 0.0, 1.0);
  local = local * local * (3.0 - 2.0 * local);   // smoothstep

  vec3 pos = position;
  pos.y += (1.0 - local) * -2.0;                 // 아래에서 올라온다
  pos *= mix(0.4, 1.0, local);                   // 작게 시작해서 커진다

  vLocal = local;
  gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
```

fragment에서 `vLocal`로 알파까지 맞춘다.

```glsl
varying float vLocal;
uniform vec3 uColor;

void main() {
  gl_FragColor = vec4(uColor, vLocal);
  #include <tonemapping_fragment>
  #include <colorspace_fragment>
}
```

---

## 7. LYGIA — 대규모 셰이더 함수 라이브러리

노이즈/필터/색공간/SDF/조명 수백 개 함수를 `#include`로 가져온다. GLSL/HLSL/WGSL/MSL 지원.

### 설치 (로컬 번들 — 프로덕션 권장)

```bash
# 서브모듈로 추가
git submodule add https://github.com/patriciogonzalezvivo/lygia.git src/shaders/lygia
# 또는 그냥 clone
git clone --depth 1 https://github.com/patriciogonzalezvivo/lygia.git src/shaders/lygia
```

```glsl
#include "lygia/generative/snoise.glsl"
#include "lygia/generative/curl.glsl"
#include "lygia/generative/fbm.glsl"
#include "lygia/space/ratio.glsl"
#include "lygia/space/rotate.glsl"
#include "lygia/color/palette/hue.glsl"
#include "lygia/draw/circle.glsl"
#include "lygia/sdf/circleSDF.glsl"
#include "lygia/filter/gaussianBlur.glsl"
#include "lygia/math/map.glsl"

void main() {
  vec2 st = ratio(gl_FragCoord.xy / u_resolution, u_resolution);
  float n = fbm(vec3(st * 3.0, u_time * 0.1));
  gl_FragColor = vec4(vec3(n), 1.0);
}
```

`vite-plugin-glsl`이 `#include`를 인라인해준다. **경로는 프로젝트 루트가 아니라 셰이더 파일 기준 상대경로**여야 하므로, `src/shaders/lygia/`에 두고 `#include './lygia/...'`로 쓰는 게 안전하다.

### 런타임 해석 (프로토타입용)

```html
<script src="https://lygia.xyz/resolve.js"></script>
```

```js
fragmentShader = resolveLygia(fragmentShader)   // 원격에서 의존성 fetch
```

프로덕션에서 쓰지 마라. 네트워크 왕복이 첫 렌더를 지연시킨다.

### 자주 쓰는 LYGIA 모듈

| 경로 | 용도 |
|---|---|
| `generative/snoise.glsl` | simplex 노이즈 (1D~4D 오버로드) |
| `generative/pnoise.glsl` | 주기적(타일링 가능) perlin |
| `generative/curl.glsl` | 컬 노이즈 |
| `generative/fbm.glsl` | FBM |
| `generative/worley.glsl` | 셀 노이즈 |
| `space/ratio.glsl` | 종횡비 보정 |
| `space/rotate.glsl` | 회전 |
| `space/scale.glsl` | 스케일 |
| `sdf/*.glsl` | 2D/3D SDF 전체 |
| `draw/circle.glsl`, `draw/stroke.glsl` | 안티에일리어싱된 도형 |
| `color/palette/*.glsl` | 팔레트 (IQ 코사인 등) |
| `color/space/*.glsl` | 색공간 변환 |
| `filter/gaussianBlur.glsl` | 블러 (분리 가능) |
| `lighting/fresnel.glsl` | 프레넬 |
| `math/map.glsl`, `math/lerp.glsl` | 유틸 |

---

## 8. 디버깅

### 8.1 색으로 값 확인하기

```glsl
// 0..1 값 확인: 흑백
gl_FragColor = vec4(vec3(value), 1.0); return;

// -1..1 값 확인: 음수는 빨강, 양수는 초록
gl_FragColor = vec4(max(-value, 0.0), max(value, 0.0), 0.0, 1.0); return;

// UV 확인: 좌하단 검정, 우상단 노랑
gl_FragColor = vec4(vUv, 0.0, 1.0); return;

// 노멀 확인
gl_FragColor = vec4(normalize(vNormal) * 0.5 + 0.5, 1.0); return;

// NaN 확인: NaN 이면 조건이 모두 false → 검정
gl_FragColor = vec4(value == value ? vec3(0,1,0) : vec3(1,0,0), 1.0); return;
```

### 8.2 흔한 에러와 원인

| 증상 | 원인 | 해결 |
|---|---|---|
| `'assign' : cannot convert from 'int' to 'float'` | `float x = 1;` | `float x = 1.0;` |
| `'constructor' : too many arguments` | `vec3(1.0, 2.0, 3.0, 4.0)` | 인자 수 확인 |
| `undeclared identifier 'texture'` | GLSL1인데 `texture()` 사용 | `texture2D()` 사용 |
| `'gl_FragColor' : undeclared` | `glslVersion: GLSL3` 지정됨 | `out vec4 fragColor;` 선언 |
| `redefinition of 'position'` | 자동 주입 attribute 재선언 | 선언 제거 |
| 화면이 검정 | `gl_Position` 미설정, 카메라 뒤, `depthTest` 문제 | `gl_FragColor = vec4(1,0,0,1)`로 격리 테스트 |
| 색이 이상하게 밝음/어두움 | 톤매핑 또는 색공간 청크 누락/중복 | `#include <tonemapping_fragment>` + `<colorspace_fragment>` 확인 |
| 모바일에서만 깨짐 | `highp` 미지원 또는 정밀도 부족 | `precision highp float` 명시, 좌표 범위 축소 |
| 그라디언트에 줄무늬(밴딩) | 8bit 프레임버퍼 | `dither8()` 추가 |
| 파티클이 갑자기 사라짐 | frustum culling | `mesh.frustumCulled = false` 또는 큰 boundingSphere |
| 텍스처가 뒤집힘 | `flipY` 차이 | `texture.flipY = false` (glTF 텍스처는 이미 false) |

### 8.3 셰이더 컴파일 로그 보기

```js
renderer.debug.checkShaderErrors = true   // 기본값 true. 프로덕션에서 false 로 하면 빨라짐

// 컴파일된 최종 셰이더 소스 출력
material.onBeforeCompile = (shader) => {
  console.log('--- VERTEX ---\n', shader.vertexShader)
  console.log('--- FRAGMENT ---\n', shader.fragmentShader)
}
```

### 8.4 성능 측정

```js
// GPU 시간 측정 (WebGL2 EXT_disjoint_timer_query_webgl2 필요)
import Stats from 'three/addons/libs/stats.module.js'
const stats = new Stats()
document.body.appendChild(stats.dom)
// 렌더 루프에서 stats.update()

// 또는 stats-gl (GPU 시간까지 표시)
// npm i stats-gl
import Stats from 'stats-gl'
const stats = new Stats({ trackGPU: true })
await stats.init(renderer)
```

**프래그먼트 비용 격리 테스트**: 캔버스를 200×200으로 줄여보고 프레임이 회복되면 fragment 병목,
그대로면 vertex/드로우콜 병목이다.

---

## 9. 셰이더 작성 체크리스트

- [ ] `float` 리터럴에 소수점을 찍었다 (`1.0`, `0.5`)
- [ ] fragment 상단에 `precision mediump float;` (좌표 계산은 `highp`)
- [ ] 자동 주입되는 uniform/attribute를 재선언하지 않았다
- [ ] fragment 마지막에 `<tonemapping_fragment>` → `<colorspace_fragment>` 순서로 넣었다
- [ ] 색은 JS에서 `THREE.Color`로 넘겼다 (하드코딩된 `vec3`는 linear 값임을 인지)
- [ ] 그라디언트가 있으면 디더를 넣었다
- [ ] 무거운 계산을 fragment → vertex로 옮길 수 있는지 검토했다
- [ ] 루프 카운트가 상수다 (`#define`)
- [ ] `normalize()` 인자가 0이 될 수 있으면 `+ 1e-5`를 더했다
- [ ] `discard`를 쓴다면 정말 필요한지 검토했다 (early-Z 손실)
- [ ] 종횡비 보정을 했다 (원이 타원이 되지 않는가)
- [ ] `onBeforeCompile`을 썼다면 `customProgramCacheKey`를 지정했다
- [ ] 모바일 실기기에서 확인했다 (에뮬레이터로는 정밀도 문제를 못 잡는다)
