web-graphics 디스코드에서 운이 좋게도 7월부터 이제 막 시작하는 Shader 스터디를 참여하게 됐다. 다같이 도대체 GLSL이 뭔지 모르는 상황에서... 일단 유튜브에 있는 강의를 보고 각자 정리를 하기로 했다.
그래서 선택한 것이 한글 번역도 있고 다른 영상에 비해 비교적 짧은 Wael Yasmina의 강의이다!
이 영상을 간략하게 정리한 내용을 포스팅의 주요 골자이다.
1. GLSL definition
- GLSL : OpenGL Shading Language
- 브라우저에 2D를 3D로 표시하기 위해 웹응용 프로그램이 그래픽카드와 통신할 수 있는 프로그래밍 언어.
- Broswer(JS) → GLSL → GPU(Monitor)
2. Comments
다른 프로그래밍 언어와 유사.
// Hello World!
/*
Hello Wrold!
*/
3. Variables and constants
초기화를 해야하고 업데이트할 수 없는 변수인 const도 있다.
const float b = 50.0;
// const int c = 20;
// c = 17; (X)
glsl에서는 변수에 저장할 데이터 유형을 미리 지정해야한다.
type variable = value;
int a = 77;
float time = 0;
4. Vectors and matrices
- 다른 타입으로 벡터와 행렬이 있다.
- 수학에 관해 말하자면, 추천하는 훌륭한 리소스는 https://www.youtube.com/@acegikmo 에서 제공하는 “Math for Game Devs” 재생 목록이다. 이 채널에서는 벡터와 행렬의 정의, 벡터 정규화, 보간, 베지어 곡선 등 다양한 주제를 훌륭하게 다루고 있다.
5. Vectors
- 3가지 범주의 벡터가 있음
- vec
- vec2 / vec3 / vec4
- ivec (정수 벡터
integer vector
)- ivec2 / ivec3 / ivec4
- bvec (부울 벡터
boolean vector
)- bvec2 / bvec3 / bvec4
- 같은 값을 가져오더라도 상황에 따라 올바른 표기법을 사용해야 함.
- xyzw : vertex(정점 위치)를 다룰 경우
- rgba : 컬러를 다룰 경우
- stpq : 텍스처를 다룰 경우
- 예시
- vec
벡터 변수에서 데이터를 가져오는 방법
vec4 vect = vec4(1.0, 2.0, 3.0, 4.0);
// 첫 번째 요소에 접근하는 방법
float a1 = vect.x; // 1.0
float a2 = vect.r; // 1.0
float a3 = vect.s; // 1.0
// 두 번째 요소에 접근하는 방법
float b1 = vect.y; // 2.0
float b1 = vect.g; // 2.0
float b1 = vect.t; // 2.0
// 세 번째 요소에 접근하는 방법
float c1 = vect.z; // 3.0
float c1 = vect.b; // 3.0
float c1 = vect.p; // 3.0
// 네 번째 요소에 접근하는 방법
float d1 = vect.w; // 4.0
float d1 = vect.a; // 4.0
float d1 = vect.q; // 4.0
vec3 vectA = vec3(1.0, 2.0, 3.0)
vec2 vectB = vectA.xz; // (1.0, 3.0)
vec3 vectC = vecA.rrr; // (1.0, 1.0, 1.0)
vec2 vectD = vectA.br; // (3.0, 1.0)
예시
// vec2 : 2개의 부동소수점으로 구성
vec2 vectA = vec2(1.0, 6.0);
// bvec4 : 4개의 부울로 구성된 벡터
bvec4 vectB = bvec4(true, true, false, false);
// 값이 동일할 때 가능한 방법 : 숫자를 하나만 적어서 나머지 생략.
vec3 vectC = vec3(0.0, 0.0, 0.0);
vec3 vectC = vec3(0.0);
// 벡터 변수를 다른 벡터의 값으로 사용 가능. 이때, vec3 vectC는 vec2에 사용 시 앞의 두 값만 갖게됨.
// vectD = (0.0, 0.0)
vec2 vectD = vec2(vectC);
// vectE = (0.0, 0.0, 1.0, 6.0)
vec4 vectE = vec4(vectC, vectA);
6. Matrices
- 행렬은 특정 개수의 float로 구성됨.
- 벡터와 행렬을 사용해 뺄셈, 덧셈 등의 계산 가능
예시
// 정수와 부울값은 자동으로 부동소수점으로 변환됨
mat2 matA = mat2(1, 1, false, false); // (1.0, 1.0, 0.0, 0.0)
// 행렬에 접근하는 방법
mat3 matB = mat3(**7.0, 4.0, 5.0, 0.0, 2.0, 5.0, 1.0, 3.0, 7.0**);
vec3 vectC1 = matB[0]; // (**7.0, 4.0, 5.0**) matB의 첫 번째 열에서 벡터 생성
matB[2][2] = 100.0; // (**7.0** -> 100.0)
float f = matB[0]y; // == matB[0][1] == **4.0**
7. Samplers
- GLSL에서 샘플러는 이미지 데이터를 저장하는 변수.
- Sampler2D : 자주 사용하는 유형. 2D 텍스처 이미지를 처리하는 데 사용된다.
- SamplerCube : 큐브 맵 텍스처를 처리하기 위한 유형.
- 간단히 말해, 샘플러는 다른 프로그래밍 언어에서 이미지 데이터를 저장하는 변수와 동일한 역할을 한다.
8. Arrays
- C, JS와 동일한 대괄호 표기법 사용
float arrayA[7]; // 배열 초기화
arrayA[0] = 20.0;
float a = arrayA[6];
9. Structures
- 직접 타입을 생성하려면 struct 키워드와 중괄호로 구성 요소를 지정해야 함.
- 구성 요소에 접근하거나 값을 설정하려면 점 표기법을 사용함.
// 사용자 정의 타입 만들기
struct myType {
int c1;
vec3 c2;
}
myType a; // 변수 생성
a.c1 = 10; // 점표기법으로 값 설정(Set)
vec3 vect = a.c2 // 점표기법으로 접근(Get)
10. Control flow statement
반복문
for (int i = 0; i < 10, i++) {
// do something
}
조건문
if(condition1) {
// do something
} else if(condition2) {
// do something else
} else {
// do something else
}
11. functions
- 함수는 다른 변수와 같이 타입이 있어야 함.
- 이 순서(규칙)를 깨려면 프로토타입을 설정해야 함.
- https://shaderific.com/index.html
- 이외에 GLSL 내장 기능에 대한 설명이 있음.
프로토타입은 기본적으로 함수 본문 없이 함수의 정의만을 나타냄.
vec2 funcE(float x, float y); // 프로토타입
vec2 vect = funcE(1.0, 1.0); // 함수를 먼저 호출
vec2 funcE(float x, float y) {
return vec2(1.0, 2.0);
}
순서가 중요함. 함수를 먼저 만들고 그 다음 호출해야 함.
// 순서가 올바른 예시
vec2 funcC() {
return vec2(1.0, 2.0);
}
vec2 vect = funcC();
// 순서가 틀린 예시
vec2 vect = funcD();
vec2 funcD() {
return vec2(1.0, 2.0);
}
void : 반환값이 없는 경우
void funcB(vec3 vect) {
// no return
}
반환값이 있는 경우 그 값과 동일한 타입이어야 함.
float funcA(int a, vec2 b) {
return 1.0;
}
12. Storage qualifiers
- const
- attribute
- JS에서 데이터를 받는 변수
- 각 정점마다 다른 데이터(좌표쌍)를 가짐.
- ex) 삼각형
- vertex shader(정점 셰이더)에서만 사용 가능.
- uniform
- JS에서 데이터를 받는 변수
- 정점마다 동일한 데이터를 가짐.
- ex) 시간
- vetex, fragment shader 둘다 사용 가능.
- varying
- vertex → fragment 셰이더 간 데이터 전달에 사용
13. Precision qualifiers
- 메모리 사용 최적화 방법으로 3가지 Precision qualifiers(정밀도 한정자)가 있음.
- lowp / mediump / highp
mediump float f; // 변수의 정밀도 설정
precision highp vec2; // vec2 전체 변수타입에 대한 정밀도 설정
14. Shaders definition
- 셰이더는 glsl로 작성된 작은 프로그램이다. 2가지가 있다.
- Vetex shader
- Fragment shader
15. Vertex shader
- 3D의 모든 객체(점, 텍스트, 도형, 3D모델)는 여러 개의 정점(Vertex)로 구성됨.
- Vertex shader는 모든 정점의 위치를 지정하는 작업을 담당.
- 메쉬를 구성하는 정점 수만큼 함수가 실행됨
- ex) 500개의 정점으로 구성된 객체는 main함수를 500번 실행.
- gl_Position : 각 정점의 좌표가 저장되는 곳
- projectionMatrix, modelViewMatrix : Three.js의 내장 변수. 카메라 뷰와 관련 있음.
- vec4(position, 1.0) : 초기 좌표가 저장되는 곳.
- Bablyon.js, Pixi.j 사용할 경우 설정이 다를 수 있음.
main 함수의 형태
void main() {
gl_Position = projectionMatrix * modelVeiwMatrix * vec4(position, 1.0);
}
16. Fragment shader
- Fragment shader의 역할 : Vertex shader에 의해 위치가 지정된 정점들이 형성하는 메쉬를 작은 프래그먼트로 분해한 다음 색상을 지정하는 것.
- 값은 1 ~ 0 사이어야 한다.
- 1을 초과하면 1과 같고, 음수는 0과 같음.
메인 함수
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // RGBA
}
17. Example 1
- 기본 세팅
// vertexShader
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
// fragmentShader
void main() {
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}
- position에 sin 추가
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(sin(position), 1.0);
}
- sin을 tan으로 교체
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(tan(position), 1.0);
}
- uniform u_time을 활용한 정점 애니메이션
// uniforms 객체 안에 u_time 초기화
const uniforms = {
u_time: {type: 'f', value: 0.0 }
}
// ShaderMaterial에 uniforms 프로퍼티 추가
const material = new THREE.ShaderMaterial({
...
uniforms
});
// Three.js Clock, getElapsedTime 함수로 시간 데이터 입력
const clock = new THREE.Clock();
function animate() {
uniforms.u_time.value = clock.getElapsedTime();
}
uniform float u_time; // JS에서 받아오기
void main() {
float newX = sin(position.x * u_time) * sin(position.y * u_time)
vec3 newPosition = vec3(newX, position.y, position.z)
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
}
18. Example2
- fragment Shader에도 애니메이션을 주기 위해, uniform으로 u_time을 가져와 적용.
uniform float u_time;
void main() {
gl_FragColor = vec4(0.2, sin(u_time), 1.0, 1.0);
}
- 그라디언트 구현을 위해 해상도를 얻어야 함. 그러기 위해서 JS에서 u_resolution을 추가해준다.
const uniforms = {
u_time: {type: 'f', value: 0.0 }
u_resolution: {type: 'v2', value: new THREE.Vector2(window.innerWidth, window.innerHeight)
.multiplyScalar(window.devicePixelRatio) // 높은 dpi 화면을 위한 옵션
}
uniform float u_time;
uniform vec2 u_resolution; // JS에서 가져오기
void main() {
gl_FragColor = vec4(0.0, u_resolution.x, 0.0, 1.0); // -> 1을 초과하기 때문에 아무것도 변경되지 않음
}
- 1보다 큰 값을 처리하는 방법
uniform float u_time;
uniform vec2 u_resolution;
void main() {
vec2 st = gl_FragCoord.xy / u_resolution // 1을 초과하지 않도록 나눠주기. (현재 프래그먼트 위치 / 해상도)
gl_FragColor = vec4(0.0, st.x, st.y, 1.0);
}
gl_FragCoord는 현재 프래그먼트의 위치를 프래그먼트 셰이더 좌표계에서 저장하는 GLSL 내장 변수
- 마우스 좌표를 받아와 애니메이션에 활용하기
const uniforms = {
u_time: {type: 'f', value: 0.0 }
u_resolution: {type: 'v2', value: new THREE.Vector2(window.innerWidth, window.innerHeight)
.multiplyScalar(window.devicePixelRatio)
u_mouse: {type: 'v2', value: new THREE.Vector2(0.0, 0.0) // u_mouse 추가
}
// 이벤트리스너를 달아주고 u_mouse에 값 넣어주기
window.addEventListener('mousemove', function(e) {
uniforms.u_mouse.value.set(e.clientX / window.innerWidth, 1- e.clientY / window.innerHeight);
});
uniform float u_time;
uniform vec2 u_resolution;
uniform vec2 u_mouse;
void main() {
vec2 st = gl_FragCoord.xy / u_resolution)
gl_FragColor = vec4(0.0, u_mouse.x, u_mouse.y, 1.0); // 마우스 좌표에 따라 색상이 변경
}
19. Example 3
- 이미지를 불러와 Fragment Shader에서 활용하기
const uniforms = {
...
image: {type: 't', value: new THREE.TextureLoader().load(nebula)} // image에 텍스쳐로더 불러오기
}
...
uniform sampler2D image;
void main() {
vec2 st = gl_FragCoord.xy / u_resolution)
vec4 texture = texture2D(image, st); // 첫번째 인자는 반드시 sampler2D. 두번째 인자는 표시할 공간.
gl_FragColor = vec4(0.0, ,texture.g, 0.0, 1.0); // 이미지의 녹색 채널만 보여주기.
}
- 시간에 따른 애니메이션 만들기
...
uniform sampler2D image;
void main() {
vec2 st = gl_FragCoord.xy / u_resolution)
vec4 texture = texture2D(image, st);
float effect = abs(sin(texture.x + u_time));
gl_FragColor = vec4(vec3(effect), 1.0);
}
- 이미지 맵핑
// Vertex Shader
...
varying vec2 vUv; // fragmentShader로 넘겨주기 위해 varying으로 선언.
void main() {
...
vUv = uv; // vUv에 UV값을 넣어주기.
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
}
// Fragment Shader
...
uniform sampler2D image;
varying vec2 vUv; // VertexShader에서 받아오기.
void main() {
vec2 st = gl_FragCoord.xy / u_resolution)
vec4 texture = texture2D(image, vUv); // 텍스쳐에 uv맵핑
float effect = abs(sin(texture.x + u_time));
gl_FragColor = vec4(vec3(effect), 1.0);
}
- 이렇게 PlaneGeometry에 이미지가 매핑을 시킬 수 있음.
다음 GLSL & Shader 학습하기 포스팅