셰이더 입문자의 좌절과 극복 - GLSL 첫 한 달
"셰이더를 배우면 뭐든 할 수 있다"는 말을 들었습니다. Three.js로 기본적인 것은 만들 수 있었지만, 정말 멋진 효과들은 전부 커스텀 셰이더였습니다. 그래서 배우기로 했는데, 생각보다 험난했습니다.
1주차: 완전한 혼란 #
셰이더 코드를 처음 봤을 때의 느낌:
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
"이게 뭐지? C언어 같기도 하고..."
첫 번째 벽: 개념 이해 #
가장 어려웠던 건 GPU의 사고방식을 이해하는 것이었습니다.
CPU 방식 (익숙함):
for (let i = 0; i < pixels.length; i++) {
pixels[i] = calculateColor(i);
}
GPU 방식 (낯설음):
모든 픽셀이 동시에 "나는 무슨 색이지?" 계산
서로의 존재를 모름
"픽셀마다 독립적으로 실행된다"는 개념이 처음에는 와닿지 않았습니다.
두 번째 벽: 디버깅 #
console.log가 없습니다. 값을 확인하려면 색상으로 출력해야 합니다.
// 값을 확인하고 싶을 때
void main() {
float value = someCalculation();
// console.log(value); 불가능!
// 대신 색상으로 출력
gl_FragColor = vec4(value, 0.0, 0.0, 1.0);
// 빨간색 강도로 값을 확인
}
에러 메시지도 암호 같았습니다.
ERROR: 0:12: 'assign' : cannot convert from 'float' to 'highp 3-component vector of float'
2주차: 작은 성공들 #
UV 좌표 이해 #
드디어 UV 좌표가 뭔지 이해했습니다.
varying vec2 vUv;
void main() {
// vUv.x: 0.0 (왼쪽) ~ 1.0 (오른쪽)
// vUv.y: 0.0 (아래) ~ 1.0 (위)
// 수평 그라데이션
gl_FragColor = vec4(vUv.x, 0.0, 0.0, 1.0);
}
화면에 빨간 그라데이션이 나타났을 때의 기쁨!
첫 번째 효과: 물결 #
유튜브 튜토리얼을 따라 물결 효과를 만들었습니다.
uniform float uTime;
varying vec2 vUv;
void main() {
float wave = sin(vUv.x * 10.0 + uTime) * 0.5 + 0.5;
gl_FragColor = vec4(vec3(wave), 1.0);
}
"드디어 뭔가 움직인다!" 작은 성공이지만 큰 동기부여가 됐습니다.
3주차: 본격적인 학습 #
Book of Shaders 발견 #
The Book of Shaders를 발견했습니다. 게임체인저였습니다.
인터랙티브 에디터에서 코드를 수정하면 바로 결과가 보입니다. 이해가 안 되던 개념들이 하나씩 풀렸습니다.
핵심 함수들 #
자주 쓰는 함수들을 정리했습니다.
// mix: 선형 보간
vec3 color = mix(colorA, colorB, 0.5); // 50% 섞기
// step: 계단 함수
float s = step(0.5, vUv.x); // x < 0.5면 0, 아니면 1
// smoothstep: 부드러운 전환
float s = smoothstep(0.4, 0.6, vUv.x); // 0.4~0.6 사이에서 부드럽게 변화
// fract: 소수점 부분
float f = fract(vUv.x * 10.0); // 0~1 반복 패턴
// mod: 나머지
float m = mod(vUv.x, 0.1); // 주기적 패턴
패턴 만들기 #
반복 패턴을 만드는 법을 배웠습니다.
// 체크무늬
float check = step(0.5, fract(vUv.x * 10.0)) *
step(0.5, fract(vUv.y * 10.0));
// 원
float dist = distance(vUv, vec2(0.5));
float circle = 1.0 - step(0.3, dist);
// 격자
float grid = step(0.95, fract(vUv.x * 10.0)) +
step(0.95, fract(vUv.y * 10.0));
4주차: 실전 적용 #
프로젝트에 셰이더 적용 #
진행 중인 프로젝트에 커스텀 셰이더를 적용해봤습니다.
목표: 데이터 히트맵 시각화
// Vertex Shader
varying vec2 vUv;
attribute float aValue;
varying float vValue;
void main() {
vUv = uv;
vValue = aValue;
vec3 pos = position;
pos.z = aValue * 2.0; // 값에 따라 높이 변화
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
// Fragment Shader
varying vec2 vUv;
varying float vValue;
void main() {
// 값에 따른 색상 (파랑 → 빨강)
vec3 cold = vec3(0.0, 0.0, 1.0);
vec3 hot = vec3(1.0, 0.0, 0.0);
vec3 color = mix(cold, hot, vValue);
gl_FragColor = vec4(color, 1.0);
}
Three.js에서 사용:
const material = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 }
},
vertexShader: vertexShaderCode,
fragmentShader: fragmentShaderCode
});
// 커스텀 attribute 추가
const values = new Float32Array(vertexCount);
// ... 데이터 채우기
geometry.setAttribute('aValue', new THREE.BufferAttribute(values, 1));
문제 해결 과정 #
처음에는 화면이 까맣게 나왔습니다. 디버깅 과정:
- UV 확인:
gl_FragColor = vec4(vUv, 0.0, 1.0);→ OK - varying 확인:
gl_FragColor = vec4(vValue, 0.0, 0.0, 1.0);→ 전부 검정 - attribute 확인: JavaScript에서 값이 제대로 들어가는지 확인 → 문제 발견!
// 잘못된 코드
geometry.setAttribute('aValue', new THREE.BufferAttribute(values, 3));
// 정정: 1개 컴포넌트
geometry.setAttribute('aValue', new THREE.BufferAttribute(values, 1));
배운 것들 #
실수 방지 체크리스트 #
☐ float에 .0 붙이기 (5 → 5.0)
☐ vec3/vec4 타입 맞추기
☐ varying은 양쪽 셰이더에 선언
☐ attribute 컴포넌트 수 확인
☐ uniform 이름 일치 확인
유용한 도구들 #
- Shadertoy: 다른 사람의 셰이더 분석
- GLSL Sandbox: 빠른 프로토타이핑
- VSCode GLSL Extension: 문법 하이라이팅, 에러 검출
학습 자료 추천 #
- The Book of Shaders: 기초부터 차근차근
- Inigo Quilez 블로그: 고급 기법
- Bruno Simon Three.js Journey: 실전 적용
한 달 후 소감 #
솔직히 아직 초보입니다. 노이즈 함수, 레이마칭 같은 고급 주제는 여전히 어렵습니다.
하지만 이제 Shadertoy에서 다른 사람 코드를 보면 "아, 이 부분은 이런 원리구나" 하고 어느 정도 이해가 됩니다.
가장 큰 변화는 "이건 셰이더로 해야 해"라는 판단이 가능해진 것입니다. 성능이 필요한 시각 효과는 셰이더가 답이라는 걸 알게 됐습니다.
셰이더 학습을 고민하는 분들께: 처음 2주가 가장 힘듭니다. 거기만 넘기면 조금씩 재미있어집니다. The Book of Shaders로 시작하세요. 정말 잘 만들어진 자료입니다.
- Previous: React Three Fiber 도입 후기 - 선언적 3D의 장단점
- Next: 10만 개 파티클 렌더링 - 성능과의 싸움