현대의 컴퓨터에는 GPU(Graphics Processing Unit)라는 특별한 하드웨어가 들어 있습니다. 셰이더는 이 GPU에서 실행되는 특수한 프로그램으로, 놀라운 것들을 해낼 수 있습니다. 셰이더는 GPU를 활용해 많은 픽셀을 한 번에 병렬로 처리하므로 매우 빠르며, 노이즈 생성, 블러 같은 필터 적용, 다각형에 음영 입히기처럼 컴퓨터 그래픽스 작업에 특히 잘 맞습니다.
셰이더 프로그래밍은 처음에는 낯설고 어렵게 느껴질 수 있습니다. p5.js의 2D 드로잉과는 다른 방식으로 접근해야 하기 때문입니다. 이 튜토리얼에서는 셰이더 프로그래밍의 기초를 짚어 보고, 더 살펴볼 만한 자료도 함께 소개합니다.
준비
브라우저에서 GPU를 프로그래밍하는 데 WebGL이라는 API를 사용합니다. p5.js는 셰이더 작업에 매우 유용합니다. WebGL의 반복적인 준비 과정을 대신 처리해 주기 때문에, 셰이더 코드에 집중할 수 있습니다. 셰이더를 사용해보려면 먼저 p5.js 캔버스를 WebGL 모드로 설정해야 합니다. 이를 위해 createCanvas()의 세 번째 매개변수로 WEBGL 상수를 추가합니다.
...
function setup() {
createCanvas(200, 200, WEBGL);
}
...
셰이더 프로그램은 두 부분으로 이루어집니다. 바로 정점 셰이더(vertex shader)와 프래그먼트 셰이더(fragment shader)입니다. 정점 셰이더는 도형의 각 정점마다 한 번씩 실행되며, 그 정점이 화면 어디에 그려질지를 결정합니다. 프래그먼트 셰이더는 도형의 모든 픽셀마다 한 번씩 실행되며, 각 픽셀의 색을 결정합니다.
![]() 빨간 구 | ![]() 시간에 따라 흔들리고 뒤틀리는 빨간 구 | ![]() 울퉁불퉁한 구의 실루엣 안이 빨강과 파랑 줄무늬로 채워진 모습 |
원래 형태 | 커스텀 정점 셰이더는 도형 안의 정점 위치를 조정할 수 있습니다 | 커스텀 프래그먼트 셰이더는 도형 내부의 색을 조정할 수 있습니다 |
이 둘은 각각 별도의 파일에 들어 있으며, loadShader() 함수로 p5.js로 불러와 setup(), draw() 에서 사용할 수 있습니다. 다음 예제를 통해 p5.js에서 기본 셰이더를 설정하는 방법을 살펴봅시다.
let myShader;
async function setup() {
// 각 셰이더 파일을 불러옵니다
// (걱정 마세요, 이건 곧 다시 살펴볼 거예요!)
myShader = await loadShader('shader.vert', 'shader.frag');
// 캔버스는 WEBGL 모드로 만들어야 합니다
createCanvas(windowWidth, windowHeight, WEBGL);
}
function draw() {
// shader()는 활성 셰이더를 설정하고,
// 이후 그려지는 것들에 적용됩니다
shader(myShader);
// 캔버스 전체를 덮는 사각형에 셰이더를 적용합니다
plane(width, height);
}
추가로 createShader()라는 함수도 있는데, 이 함수는 스케치 안에 문자열로 직접 작성한 셰이더를 불러올 때 사용할 수 있습니다.
셰이더 작성하기
이제 loadShader()에서 참조한 정점, 프래그먼트 셰이더 파일 안에 무엇이 들어가는지 살펴보겠습니다.
셰이딩 언어(GLSL)
셰이더 파일은 Graphics Library Shading Language, 즉 GLSL(OpenGL 2.0, GLSL ES 1.00 기반)로 작성합니다. 문법과 구조가 우리가 익숙한 자바스크립트와는 꽤 다릅니다. GLSL은 C와 비슷한 문법을 가지므로, 자바스크립트에는 없는 개념들도 함께 등장합니다.
우선 셰이딩 언어는 타입에 훨씬 엄격합니다. 만드는 모든 변수에는 어떤 데이터를 저장하는지 타입을 명시해야 합니다. 아래는 자주 쓰이는 타입 몇 가지입니다.
vec2(x,y) // 두 개의 float로 이루어진 벡터
vec3(x,y,z) // 세 개의 float로 이루어진 벡터
// (r,g,b로도 볼 수 있음)
vec4(x,y,z,w) // 네 개의 float로 이루어진 벡터
// (r,g,b,a로도 볼 수 있음)
float // 소수점을 가지는 숫자
int // 소수점이 없는 정수
sampler2D // 텍스처를 참조하는 값
mat2 // 2x2 행렬
mat3 // 3x3 행렬
mat4 // 4x4 행렬
bool // true 또는 false
전반적으로 셰이딩 언어는 자바스크립트보다 훨씬 엄격합니다. 세미콜론 하나만 빠져도 오류가 발생합니다. float와 int 같은 서로 다른 숫자 타입을 섞어서 쓸 수도 없습니다. 또 float 값은 반드시 소수점까지 써 주어야 하므로, 0.0이나 1.0 같은 숫자를 자주 쓰게 됩니다.
다음은 GLSL에서 달라지는 몇 가지 예시입니다.
Javascript | GLSL | |
|---|---|---|
모든 변수에 타입이 필요합니다. |
|
|
함수는 매개변수의 타입과 반환값의 타입을 모두 선언해야 합니다. |
|
|
int와 float 사이의 변환은 직접 해 주어야 합니다. |
|
|
GLSL의 반복문은 상수 값에서 멈춰야 합니다. 조건에 따라 중간에 끝내고 싶다면 |
|
|
제약이 많기는 하지만, 어떤 면에서는 GLSL이 더 편하게 느껴질 수도 있습니다! 특히, 벡터를 다루는 데 유용한 여러 축약 표현이 있습니다.
|
|
모든 값이 같은 벡터를 만들고 싶을 때는 같은 값을 여러 번 반복해서 쓰지 않아도 됩니다. 한 번만 써도 충분합니다. |
|
더 큰 벡터에서 더 작은 벡터를 꺼낼 수도 있습니다. 이를 “스위즐링(swizzling)“이라고 하며, |
|
정점 셰이더
다음은 간단한 정점 셰이더 예시입니다. p5.js에서 제공하는 변환과 카메라 원근을 적용합니다.
| 셰이더는 |
| 셰이더의 attribute는 각 정점마다 달라지는 값을 담고 있으며, p5.js는 이를 통해 각 정점의 위치 같은 정보를 셰이더에 전달합니다. 이 셰이더에서 attribute는 |
| 셰이더의 uniform은 그려지는 도형 전체에서 일정한 값을 뜻합니다. 이 셰이더에서는 각 uniform이 |
| 모든 정점 셰이더에는 |
아직 이 내용이 완전히 이해되지 않아도 괜찮습니다. 정점 셰이더는 중요한 역할을 하지만, 대개 프래그먼트 셰이더에서 만든 결과가 도형 위에 올바르게 보이도록 하는 것이 전부입니다. 아마 여러 프로젝트에서 같은 정점 셰이더를 반복해서 사용하게 될 것입니다. 다음은 정점별 색과 텍스처 좌표 등 정보를 처리하는 표준 정점 셰이더입니다.
precision highp float;
attribute vec3 aPosition;
attribute vec2 aTexCoord;
attribute vec4 aVertexColor;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
varying vec2 vTexCoord;
varying vec4 vVertexColor;
void main() {
// 카메라 변환을 적용합니다
vec4 viewModelPosition = uModelViewMatrix * vec4(aPosition, 1.0);
// 이 정점이 어디에 그려질지 WebGL에 알려 줍니다
gl_Position = uProjectionMatrix * viewModelPosition;
// 데이터를 프래그먼트 셰이더로 전달합니다
vTexCoord = aTexCoord;
vVertexColor = aVertexColor;
}
프래그먼트 셰이더
프래그먼트 셰이더는 셰이더의 색 출력 결과를 담당합니다. 셰이더 프로그래밍의 많은 부분이 여기서 이루어집니다. 다음은 단순히 빨간색을 표시하는 간단한 프래그먼트 셰이더입니다.
| 프래그먼트 셰이더도 float의 |
| 정점 셰이더와 마찬가지로 프래그먼트 셰이더에도
|
이제 정점 셰이더와 프래그먼트 셰이더를 모두 갖추었으니, 이 코드를 각각 별도의 파일(shader.vert, shader.frag)로 저장해 loadShader()로 스케치에 불러올 수 있습니다.
유니폼: 스케치에서 셰이더로 데이터 전달하기
이처럼 단순한 셰이더도 충분히 유용할 수 있지만, p5.js 스케치의 변수를 셰이더와 주고받아야 할 경우도 있습니다. 이럴 때 사용하는 것이 유니폼입니다. 유니폼은 스케치에서 셰이더로 보낼 수 있는 변수의 한 종류입니다. 유니폼을 사용하여 자바스크립트에서 셰이더를 훨씬 더 세밀하게 제어할 수 있습니다.
유니폼은 파일의 맨 위, main() 바깥에서 정의합니다. 이는 정점 셰이더와 프래그먼트 셰이더 양쪽 모두에서 접근할 수 있습니다. 아래 예제에서는 p5.js의 millis() 메서드가 반환하는 값을 time이라는 유니폼으로 전달해, 정점 셰이더에 움직임을 더합니다.
프래그먼트 셰이더에서도 유니폼을 똑같이 사용할 수 있습니다. 다음 예제에서는 myColor라는 색 유니폼을 만들어 자바스크립트 쪽에서 색을 바꿀 수 있게 합니다. 다만 셰이더에서는 색 채널 값이 0~255가 아니라 0~1 범위라는 점을 기억해 두세요.
p5.js가 제공하는 유니폼의 전체 목록은 p5.js WebGL Mode Architecture 문서에서 확인할 수 있습니다.
Varyings: 정점 셰이더에서 프래그먼트 셰이더로 데이터 전달하기
Varying 변수는 정점 셰이더와 프래그먼트 셰이더 사이에서 데이터를 공유합니다. 덕분에 위치나 다른 기하 정보도 프래그먼트 셰이더 안에서 활용할 수 있습니다.
예를 들어 프래그먼트 셰이더에서 도형의 텍스처 좌표를 사용하고 싶을 수 있습니다. 이 값은 vec2 형태로 들어오며, 좌표 범위는 0에서 1 사이입니다. 처음에는 p5.js가 제공하는 attribute로 들어오고, attribute는 정점 셰이더에서만 접근할 수 있습니다. 그럼 표준 정점 셰이더가 이 정보를 프래그먼트 셰이더로 어떻게 전달하는지 보겠습니다.
| |
| 텍스처 좌표는 처음에 |
| |
| 여기서는 |
| |
| attribute의 값을 varying 변수에 할당함으로써, 프래그먼트 셰이더가 읽을 수 있는 자리로 데이터를 복사합니다. |
|
정점 셰이더에서 vTexCoord라는 varying을 정의했으므로, 프래그먼트 셰이더에서도 이를 사용할 수 있습니다. 아래의 단순한 프래그먼트 셰이더는 x 값을 빨간 채널에, y 값을 초록 채널에 대응시킵니다. vTexCoord는 정점 셰이더에서는 각 정점별 값이지만, 프래그먼트 셰이더에서는 각 픽셀별 값이라는 점에 주목하세요. 픽셀별 값을 얻기 위해 WebGL은 각 면의 꼭짓점 값 사이를 부드럽게 보간합니다.
| |
| 이 셰이더를 ![]() |
|
필터 셰이더
p5.js에서 필터란 캔버스의 모든 픽셀을 살펴본 뒤 그것들을 다른 값으로 바꾸는 것입니다. 색을 반전하거나 캔버스에 블러를 적용하는 등 다양한 내장 필터가 있습니다. 직접 프래그먼트 셰이더를 작성해 자신만의 필터를 만들 수도 있습니다.
필터 셰이더에는 프래그먼트 셰이더만 있으면 됩니다. 정점 셰이더는 주로 도형의 위치를 정하는 역할인데, 필터는 늘 캔버스 전체에 적용되기 때문입니다. 그래서 p5.js가 필터 셰이더에 기본 정점 셰이더를 제공해 줍니다. loadShader 대신 createFilterShader(src)를 사용하고, 셰이더 소스 코드를 담은 문자열을 넣으면 됩니다.
필터 셰이더에서는 사용할 수 있는 uniform들이 몇 가지 있으며, 자세한 내용은 createFilterShader 문서에서 확인할 수 있습니다. 시작할 때 특히 알아 두면 좋은 두 가지는 다음과 같습니다.
uniform sampler2D tex0는 캔버스의 내용을 담고 있는 텍스처입니다.varying vec2 vTexCoord는 현재 픽셀의 캔버스 좌표를 담고 있으며, 범위는 0에서 1입니다.
이 둘을 조합하여 texture2D(tex0, vTexCoord)로 현재 픽셀의 색을 반환하고, 그 색을 원하는 대로 바꿀 수 있습니다. 아래 예제에서는 파란 채널 값을 빨강과 초록 채널로 복사해, 사용자 정의 흑백 필터를 만듭니다.
또 다른 시도는 texture2D의 출력을 바꾸는 대신 입력을 바꾸는 것입니다. 사용하는 텍스처 좌표를 조정하면 원본에서 살짝 밀린 효과를 만들 수도 있고, 픽셀마다 오프셋을 다르게 주면 뒤틀림(warp) 효과도 만들 수 있습니다.
마무리
여기까지의 내용만으로도 기본적인 셰이더를 만들 수 있습니다. 하지만 셰이더 프로그래밍의 세계는 훨씬 더 깊고 넓으며, 이 튜토리얼에서 다루지 못한 다양한 주제와 기법이 존재합니다. p5.js의 셰이더는 시각 효과, 텍스처, 3D 기하 구조 위에 입힐 수 있는 다양한 표현을 만드는 데 강력한 도구가 됩니다.
셰이더를 더 배우고 싶다면 아래 자료도 살펴보세요!
- The Book of Shaders, Patricio Gonzalez Vivo와 Jen Lowe의 셰이더 가이드
- p5.js shaders, Casey Conchinha와 Louise Lessél의 셰이더 가이드
- Shadertoy, 브라우저 편집기에서 작성된 셰이더를 모아 둔 방대한 온라인 컬렉션
- p5js Shader Examples, Adam Ferriss가 모아 둔 자료 모음
- OpenGL ES 2.0 Specification, GLSL에 대한 매우 기술적인 명세 문서
- WebGL Quick Reference card, 다소 빽빽하지만 GLSL 함수에 관한 유용한 정보가 많은 참고 카드
- Shaderific GLSL ES reference, GLSL 내장 함수와 데이터 타입을 조금 더 간결하게 정리한 레퍼런스
용어집
셰이더
다양한 시각 효과와 필터를 효율적으로 만들어 낼 수 있는 GPU 프로그램입니다.
GLSL
Graphics Library Shader Language(GLSL)는 셰이더를 작성할 때 사용하는 프로그래밍 언어입니다.
유니폼 (Uniform)
스케치에서 셰이더로 전달되는 변수입니다.
Varying
정점 셰이더에서 프래그먼트 셰이더로 전달되는 변수입니다.
벡터 (vec2 / vec3 / vec4)
흔히 두 개, 세 개, 또는 네 개의 숫자를 묶어서 저장하는 데이터 타입으로, 색, 위치 등을 표현하는 데 사용됩니다.
Float
소수점을 가질 수 있는 부동소수점 숫자를 저장하는 데이터 타입입니다.
Int
소수점이 없는 정수를 저장하는 데이터 타입입니다.
Sampler
셰이더로 전달되는 텍스처를 나타내는 데이터 타입입니다. GLSL에서는 보통 sampler2D로 표현합니다.
Attribute
p5.js 스케치에서 생성되어 정점 셰이더에서 사용할 수 있게 되는 GLSL 변수입니다. 대부분의 경우 p5.js가 이를 제공합니다.
텍스처
셰이더 프로그램에 전달되는 이미지입니다. texture2D() 함수를 사용해 샘플링할 수 있습니다.
타입
int, float, vector 등 데이터의 형식을 설명하는 라벨입니다.
정점 셰이더 (Vertex Shader)
3D 공간에서 기하학적 객체의 위치를 정하는 셰이더 프로그램의 부분입니다.
프래그먼트 셰이더 (Fragment Shader)
셰이더가 출력하는 각 픽셀의 색과 외형을 담당하는 셰이더 프로그램의 부분입니다.



