웹 디자인 파이프라인 스킬과 이를 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)
1296 lines
41 KiB
Markdown
1296 lines
41 KiB
Markdown
# 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`를 지정했다
|
||
- [ ] 모바일 실기기에서 확인했다 (에뮬레이터로는 정밀도 문제를 못 잡는다)
|