# 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 ` 같은 청크도 **쓸 수 없다**. → **웹디자인 작업에서는 쓸 이유가 거의 없다.** `ShaderMaterial`을 써라. ### 유용한 ShaderChunk ```glsl #include // PI, RECIPROCAL_PI, saturate 매크로 등 유틸 #include // gl_FragColor 에 톤매핑 적용 #include // 출력 색공간 변환 (r154+ 이름. 이전엔 encodings_fragment) #include // 안개 (fog_pars_fragment 도 필요) #include // 밴딩 억제 #include // 로그 깊이 버퍼 ``` fragment shader **마지막**에 이 순서로: ```glsl gl_FragColor = vec4(color, alpha); #include #include } ``` ### `onBeforeCompile`로 내장 머티리얼 확장 `MeshStandardMaterial`의 조명 계산을 유지한 채 코드를 삽입할 때. ```js material.onBeforeCompile = (shader) => { shader.uniforms.uTime = { value: 0 } shader.vertexShader = shader.vertexShader .replace('#include ', ` #include uniform float uTime; varying vec3 vLocalPos; `) .replace('#include ', ` #include vLocalPos = position; transformed.y += sin(position.x * 3.0 + uTime) * 0.1; `) // 노멀도 바꿔야 조명이 맞는다 .replace('#include ', ` #include // objectNormal 을 여기서 수정 `) shader.fragmentShader = shader.fragmentShader .replace('#include ', ` #include gl_FragColor.rgb *= 1.1; `) } // 필수: 안 하면 셰이더 캐시가 오작동한다 material.customProgramCacheKey = () => 'my-material-v1' ``` **자주 쓰는 훅 포인트** | 청크 | 위치 | 용도 | |---|---|---| | `` | 최상단 | uniform/varying/함수 선언 | | `` | vertex | `objectNormal` 수정 | | `` | vertex | `transformed`(정점 위치) 수정 | | `` | vertex 끝 | `mvPosition`, `gl_Position` 이후 처리 | | `` | fragment | `diffuseColor` 수정 (알베도) | | `` | fragment | discard 삽입 | | `` | fragment | `totalEmissiveRadiance` 수정 | | `` | 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 #include } ``` ### 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 #include } ``` --- ## 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 ``` ```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 ` + `` 확인 | | 모바일에서만 깨짐 | `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 마지막에 `` → `` 순서로 넣었다 - [ ] 색은 JS에서 `THREE.Color`로 넘겼다 (하드코딩된 `vec3`는 linear 값임을 인지) - [ ] 그라디언트가 있으면 디더를 넣었다 - [ ] 무거운 계산을 fragment → vertex로 옮길 수 있는지 검토했다 - [ ] 루프 카운트가 상수다 (`#define`) - [ ] `normalize()` 인자가 0이 될 수 있으면 `+ 1e-5`를 더했다 - [ ] `discard`를 쓴다면 정말 필요한지 검토했다 (early-Z 손실) - [ ] 종횡비 보정을 했다 (원이 타원이 되지 않는가) - [ ] `onBeforeCompile`을 썼다면 `customProgramCacheKey`를 지정했다 - [ ] 모바일 실기기에서 확인했다 (에뮬레이터로는 정밀도 문제를 못 잡는다)