WebGL 보일러플레이트(HTML/JS)는 이미 구성되어 있다는 가정 하에, 셰이더 코드(vert.glsl, frag.glsl) 자체에서 알아야 하는 문법과 개념만 다룹니다. 기준 버전은 WebGL2에서 사용하는 GLSL ES 3.00(#version 300 es)입니다.

1. 셰이더 파이프라인 기본 구조

GPU 파이프라인에서 버텍스 셰이더는 정점 단위로, 프래그먼트 셰이더는 픽셀 단위로 동시에 실행됩니다. 버텍스 셰이더가 out으로 내보낸 값은 래스터화 과정에서 보간되어 프래그먼트 셰이더의 in으로 들어옵니다.

vert.glsl (버텍스 셰이더)

#version 300 es // [필수 개념 1: in / out] // in: 보일러플레이트(JS)에서 버퍼로 넘겨준 데이터를 받는 키워드 (구버전의 attribute) in vec2 a_position; // [필수 개념 2: varying 대체] // 버텍스 셰이더에서 프래그먼트 셰이더로 데이터를 넘길 때 out을 사용합니다. out vec2 v_uv; void main() { // [필수 개념 3: gl_Position] // 버텍스 셰이더는 반드시 이 내장 변수에 좌표를 할당해야 합니다. // a_position은 보통 -1.0 ~ 1.0 범위입니다. gl_Position = vec4(a_position, 0.0, 1.0); // [필수 개념 4: 벡터 연산과 스위즐링(Swizzling)] // -1~1 범위를 0~1 범위로 변환하여 프래그먼트 셰이더로 넘깁니다. // xy, xyz 등 원하는 축을 뽑아내는 문법을 스위즐링이라고 합니다. v_uv = a_position * 0.5 + 0.5; } 

frag.glsl (프래그먼트 셰이더)

#version 300 es // [필수 개념 5: 정밀도 설정] // WebGL은 모바일 환경을 고려해 정밀도를 명시적으로 지정해야 합니다. precision highp float; // [필수 개념 6: uniform] // JS에서 프레임마다 셰이더 전체에 동일하게 전달하는 전역 값입니다. uniform float u_time; uniform vec2 u_resolution; // 버텍스 셰이더에서 넘어온 데이터를 받습니다. (구버전의 varying) in vec2 v_uv; // [필수 개념 7: out 변수] // GLSL ES 3.00부터는 gl_FragColor 대신 직접 out 변수를 선언합니다. out vec4 fragColor; void main() { // 1. 종횡비(Aspect Ratio) 보정 vec2 uv = v_uv; uv.x *= u_resolution.x / u_resolution.y; // 2. 중심점 계산 vec2 center = vec2(u_resolution.x / u_resolution.y * 0.5, 0.5); // [필수 개념 8: 내장 수학 함수] // distance(A, B): 두 점 사이의 거리 float dist = distance(uv, center); // sin()은 -1.0 ~ 1.0을 반환하므로 * 0.5 + 0.5로 0.0 ~ 1.0 범위로 정규화합니다. float wave = sin(dist * 15.0 - u_time * 3.0) * 0.5 + 0.5; // [필수 개념 9: vec3 색상 표현] // RGB 각 채널을 0.0(어둠) ~ 1.0(밝음) 실수로 표현합니다. vec3 color1 = vec3(0.1, 0.5, 1.0); vec3 color2 = vec3(1.0, 0.2, 0.4); // mix(A, B, t): t가 0.0이면 A, 1.0이면 B, 중간값이면 보간된 색을 반환합니다. vec3 finalColor = mix(color1, color2, wave); // [필수 개념 10: 최종 출력] // vec4(R, G, B, Alpha) 형태로 출력합니다. fragColor = vec4(finalColor, 1.0); } 

2. 데이터 타입

  • float: 소수 타입입니다. GLSL에서는 정수 리터럴을 거의 사용하지 않으며, 1이 아니라 1.0처럼 소수점을 명시해야 타입 불일치 에러를 피할 수 있습니다.
  • int, uint: 정수 타입입니다. 반복문 인덱스나 비트 연산에 사용되며, float과 자동 형변환되지 않으므로 float(i)처럼 명시적 캐스팅이 필요합니다.
  • bool: 논리 타입입니다. if 조건이나 step() 같은 함수의 반환과 호환됩니다.
  • vec2, vec3, vec4: float 여러 개를 묶은 벡터 타입입니다. 위치, 색상, UV 좌표 등에 사용됩니다.
  • mat2, mat3, mat4: 행렬 타입입니다. 이동, 회전, 투영 변환에 사용됩니다.
  • sampler2D: 텍스처 유닛을 참조하는 핸들 타입입니다. 변수 자체에 데이터가 들어있지 않고, texture() 함수와 함께 사용합니다.

3. 변수 한정자 (Qualifier)

  • in: 외부(JS 또는 이전 스테이지)로부터 데이터를 받는 한정자입니다.
  • out: 다음 스테이지(프래그먼트 셰이더 또는 프레임버퍼)로 데이터를 내보내는 한정자입니다.
  • uniform: JS에서 셰이더 전체에 동일하게 전달하는 전역 변수입니다. 정점/픽셀마다 값이 바뀌지 않습니다.
  • const: 컴파일 타임 상수입니다. 반복문의 상한값처럼 GPU가 미리 알아야 하는 값에 사용됩니다.
  • flat: 보간 없이 값을 전달하는 한정자입니다. 보통 정수형 out/in 변수에 필수로 붙여야 합니다(정수는 보간이 정의되지 않기 때문입니다).

4. 자주 쓰는 내장 함수

함수 설명
mix(a, b, t) 선형 보간. 색상이나 값을 부드럽게 섞을 때 가장 많이 사용됩니다.
clamp(x, min, max) x를 min~max 범위로 제한합니다.
smoothstep(edge0, edge1, x) 부드러운 S자 보간. 마스킹이나 경계를 부드럽게 처리할 때 사용됩니다.
step(edge, x) x가 edge보다 작으면 0.0, 크거나 같으면 1.0을 반환하는 계단 함수입니다.
length(v) 벡터의 길이(크기)를 반환합니다.
distance(a, b) 두 점 사이의 거리를 반환합니다.
normalize(v) 벡터를 길이 1인 단위 벡터로 변환합니다.
dot(a, b) 내적입니다. 두 벡터의 방향 유사도나 조명 계산에 사용됩니다.
cross(a, b) 외적입니다. 법선 벡터 계산 등에 사용됩니다.
reflect(I, N), refract(I, N, eta) 반사/굴절 벡터 계산에 사용됩니다.
pow(x, y), exp(x), log(x) 지수/로그 연산입니다.
floor(x), ceil(x), fract(x) 내림, 올림, 소수부 추출입니다. 그리드 패턴이나 반복 패턴을 만들 때 자주 사용됩니다.
mod(x, y) 나머지 연산입니다. 반복(타일링) 패턴에 사용됩니다.
abs(x), sin(x), cos(x) 절댓값, 삼각함수입니다.

GLSL ES에는 Perlin/Simplex 노이즈 같은 노이즈 함수가 내장되어 있지 않습니다. 노이즈가 필요한 경우 해시 함수 기반의 커스텀 구현을 셰이더 코드에 직접 포함시켜야 합니다.

5. 스위즐링 (Swizzling)

벡터 컴포넌트를 자유롭게 조합해서 추출하는 문법입니다.

vec3 v = vec3(1.0, 2.0, 3.0); v.x; // 1.0 (위치 표기: xyzw) v.r; // 1.0 (색상 표기: rgba, 의미는 동일) v.xy; // vec2(1.0, 2.0) v.xyy; // vec3(1.0, 2.0, 2.0) — 임의 조합 및 중복 가능 v.xy = vec2(5.0, 6.0); // 일부 컴포넌트만 대입(write-swizzle)도 가능 

6. 텍스처 샘플링

GLSL ES 3.00부터 텍스처를 읽는 함수는 texture()로 통일되었습니다. 구버전의 texture2D()는 사용하지 않습니다.

uniform sampler2D u_texture; in vec2 v_uv; out vec4 fragColor; void main() { vec4 texColor = texture(u_texture, v_uv); fragColor = texColor; } 

7. 분기와 반복문

if/else, for, while을 모두 사용할 수 있지만, GPU는 SIMD 방식으로 픽셀을 묶어서 처리하기 때문에 분기 비용이 CPU보다 큽니다. 같은 그룹 내 픽셀들이 다른 분기를 타면 양쪽 분기를 모두 계산하고 결과만 선택하는 방식으로 동작할 수 있습니다.

for (int i = 0; i < 8; i++) { // const 또는 컴파일 타임에 알 수 있는 상한값을 권장합니다. } 

가능하면 if/else 대신 step(), mix(), smoothstep() 같은 함수로 분기를 대체하는 것이 일반적인 최적화 패턴입니다.

8. 배열과 구조체

// 배열 float values[4] = float[4](0.1, 0.3, 0.6, 1.0); // 구조체 struct Light { vec3 position; vec3 color; }; Light light; light.position = vec3(0.0, 1.0, 0.0); 

9. 사용자 정의 함수와 매개변수 한정자

함수 매개변수에는 in(기본값, 읽기 전용), out(함수 내부에서 값을 채워 반환), inout(읽고 쓰기 모두 가능) 한정자를 붙일 수 있습니다.

float square(in float x) { return x * x; } void getMinMax(in float a, in float b, out float minV, out float maxV) { minV = min(a, b); maxV = max(a, b); } 

10. discard와 알파 클리핑

프래그먼트 셰이더 내에서 특정 픽셀의 출력을 완전히 버릴 때 사용합니다.

void main() { float alpha = texture(u_texture, v_uv).a; if (alpha < 0.1) { discard; } fragColor = vec4(1.0, 1.0, 1.0, alpha); } 

11. 정밀도(precision) 한정자

lowp, mediump, highp 세 단계가 있으며, 프래그먼트 셰이더는 기본 정밀도가 정의되어 있지 않아 precision 지시어를 직접 선언해야 컴파일 에러를 피할 수 있습니다. 버텍스 셰이더는 highp가 기본값으로 잡혀 있는 경우가 많지만, 명시적으로 선언하는 것이 안전합니다.

precision highp float; precision highp int; 

모바일 GPU에서는 mediump만 사용해도 충분한 경우가 많고, 성능 최적화 시 정밀도를 낮추는 것도 고려 대상이 됩니다.

12. Multiple Render Targets와 layout(location = ...)

여러 개의 out 변수를 동시에 다른 프레임버퍼 어태치먼트로 출력할 때는 layout(location = N)으로 인덱스를 명시합니다.

layout(location = 0) out vec4 fragColor; layout(location = 1) out vec4 fragNormal; 

같은 방식으로 버텍스 셰이더의 in 변수에도 layout(location = 0) in vec2 a_position;처럼 attribute 위치를 명시적으로 고정할 수 있습니다.

13. 자주 쓰는 내장 변수

  • gl_Position (버텍스 셰이더, 필수): 클립 공간 좌표입니다.
  • gl_PointSize (버텍스 셰이더): gl.POINTS로 그릴 때 점의 픽셀 크기를 지정합니다.
  • gl_FragCoord (프래그먼트 셰이더): 현재 픽셀의 화면 좌표(xy)와 깊이(z)를 담고 있습니다. u_resolution을 넘기지 않고 화면 좌표 기반 효과를 만들 때 사용됩니다.
  • gl_FragDepth (프래그먼트 셰이더): 깊이 값을 직접 덮어쓸 때 사용합니다.

14. 참고: 구버전(GLSL ES 1.00) 문법과의 차이

GLSL ES 1.00 (WebGL1) GLSL ES 3.00 (WebGL2)
attribute in (버텍스 셰이더)
varying 버텍스: out / 프래그먼트: in
texture2D(), textureCube() texture()
gl_FragColor 직접 선언한 out vec4 변수
버전 지시어 없음 또는 #version 100 #version 300 es

기존 WebGL1 예제 코드를 참고할 때는 이 표를 기준으로 치환하면 대부분 호환됩니다.