designpaca/research/three/03-shaders.md
Yun Chan 8808c672dc designpaca 초기 구현 — 스킬 · 설치 CLI · 배포 파이프라인
웹 디자인 파이프라인 스킬과 이를 5개 에이전트에 설치하는 CLI 를 담은 모노레포.

스킬 (packages/skill)
- SKILL.md 261줄 + 참조 문서 16개 3,349줄. progressive disclosure 로
  본문은 절차와 인덱스만, 지식은 references/ 로 분리
- 0~6단계 파이프라인. 규모에 따라 전체·연장·국소 세 경로로 분기
- 하드 게이트 12개는 grep·카운트로 검증 가능한 것만. 취향 판단은 제외
- 미학 프리셋 5종, AI 슬롭 지문 목록, 한글 조판 규칙,
  SVG 필터·three.js·인터랙티브 모션·HTML-in-Canvas 실전 지침

설치 CLI (packages/cli, packages/core)
- npx designpaca 온보딩 TUI. Claude Code · Codex · Cursor · Windsurf · AGENTS.md
- 매니페스트에 설치 시점 해시를 기록해 사용자가 고친 파일은 update 가 건너뛴다
- 타깃별로 본문의 references/ 경로를 실제 설치 위치로 재작성
- AGENTS.md 는 항상 로드되므로 본문 대신 303자 포인터만 주입
- Windsurf 는 12,000자 상한 초과 시 설치를 차단

배포 (build/ci, .forgejo/workflows)
- 태그 v* → 검사·테스트·빌드 → npmjs 배포 + Forgejo 레지스트리 미러
  → draft 릴리스 → Cloudflare Pages. 재실행 멱등

근거 (research/)
- 약 250개 웹 소스 조사 결과와 도그푸딩 검증 2건. 스킬의 모든 수치는 여기서 나온다

테스트 22개 통과 (core 16 · cli 6)
2026-08-20 10:48:00 +09:00

1296 lines
41 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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)` | 부드러운 01 | **가장 많이 쓴다** |
| `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` 지정했다
- [ ] 모바일 실기기에서 확인했다 (에뮬레이터로는 정밀도 문제를 잡는다)