Subscribe to New Posts

Lorem ultrices malesuada sapien amet pulvinar quis. Feugiat etiam ullamcorper pharetra vitae nibh enim vel.

Subscribe Zalgritte's Blog cover image
Zalgritte profile image Zalgritte

GLSL & Shader 기초 개념 학습하기 (feat. Wael Yasmina 유튜브)

Three.js에서 쓰이는 Shader 및 GLSL를 Wael Yasmina의 유튜브 영상을 통해 기초 개념을 학습합니다.

GLSL & Shader 기초 개념 학습하기 (feat. Wael Yasmina 유튜브)

web-graphics 디스코드에서 운이 좋게도 7월부터 이제 막 시작하는 Shader 스터디를 참여하게 됐다. 다같이 도대체 GLSL이 뭔지 모르는 상황에서... 일단 유튜브에 있는 강의를 보고 각자 정리를 하기로 했다.

💡
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 : 텍스처를 다룰 경우
    • 예시

벡터 변수에서 데이터를 가져오는 방법

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

Untitled
  • 행렬은 특정 개수의 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

Untitled
  • 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

Untitled
  • 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);
}
Untitled
  • position에 sin 추가
void main() {
  gl_Position = projectionMatrix * modelViewMatrix * vec4(sin(position), 1.0);
}
Untitled
  • sin을 tan으로 교체
void main() {
  gl_Position = projectionMatrix * modelViewMatrix * vec4(tan(position), 1.0);
}
Untitled
  • 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);
}
Untitled

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);
}
Untitled
  • 그라디언트 구현을 위해 해상도를 얻어야 함. 그러기 위해서 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 내장 변수
Untitled
  • 마우스 좌표를 받아와 애니메이션에 활용하기
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);
}
Untitled
  • 이미지 맵핑
// 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에 이미지가 매핑을 시킬 수 있음.
Untitled


다음 GLSL & Shader 학습하기 포스팅

GLSL & Shader 기초 개념 학습하기 (feat. 패스트캠퍼스)
Three.js에서 쓰이는 Shader 및 GLSL를 패스트캠퍼스의 인터랙티브 웹 강의를 정리하며 기초 개념을 학습합니다.
Zalgritte profile image Zalgritte
프로덕트 디자이너로 근무했습니다. 현재는 프론트엔드 개발을 배우고 있습니다. 인터랙티브 웹을 좋아합니다.