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. 패스트캠퍼스)

Three.js에서 쓰이는 Shader 및 GLSL를 패스트캠퍼스의 인터랙티브 웹 강의를 정리하며 기초 개념을 학습합니다.

GLSL & Shader 기초 개념 학습하기 (feat. 패스트캠퍼스)

이전 GLSL & Shader 학습에 이어서, 내가 수강하고 있던 패스트캠퍼스의 강의에서도 Shader를 친절하게 설명하고 있는 부분이 있어서 정리를 해보았다.

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

이전에 작성한 GLSL & Shader 학습 기록

연초에 국비지원을 통해 패스트캠퍼스 강의를 등록했다. 인터랙티브 웹을 본격적으로 배우고 싶어서 공부했다가, 갈수록 높아지는 난이도에 진도를 멈췄다.

그런데 마지막 파트에 내가 원하는 정보가 다 있는게 아닌가... 😄
(TMI) 이 부분은 이유운 강사님이 진행하시는데 목소리도 좋고 강의가 깔끔하다.

내일배움아카데미 : K-Digital 웹퍼블리싱 완전 정복 : 모션 디자인으로 완성하는 반응형 웹 디자인 | 패스트캠퍼스
패스트캠퍼스 온라인 강의를 국비지원으로 0원에 수강하세요!

2024년 7월 기준의 강의 링크. 만약 연결이 안되면 '인터랙션' 키워드로 검색하면 나올 것이다.

Part 4. Three.js 심화 중, Shader 기초에 대해 설명하는 부분을 정리한 것이다.


1. Mesh에 셰이더 적용하기

시작 전에, Three.js에서 MeshBasicMateria에 onBeforeCompile 함수로 콘솔로그를 찍어보자.

// Three.js
const material = new THREE.MeshBasicMaterial({
  onBeforeCompile: (data) => {
    console.log(data);
  },
});

그렇다면 다음과 같이 Object 객체에 fragmenShader, Uniforms, vertexShader 등의 프로퍼티가 이미 존재함을 알 수 있다.

즉, MeshBasicMaterial도 GLSL 언어로 구현된 셰이더 코드를 전달해주는 객체의 역할을 하는 것이다.

Untitled
Untitled

RawShaderMaterial은 ShaderMaterial과 달리, 전역 uniforms가 없는 객체이다.

projectionMatrix, viewMatrix, modelMatrix, position을 직접 초기화해줘야 한다.

  • 이 값들을 곱해줄 때는 순서가 중요하다. 만약 순서가 바뀐다면 문제가 발생한다.
// Three.js
const material = new THREE.RawShaderMaterial({
  vertexShader:`
    uniform mat4 projectionMatrix; // 카메라 객체에서 넘겨주는 데이터를 가져온다.
    uniform mat4 viewMatrix; // 카메라의 종횡비 등 각종 값들을 담당한다.
    uniform mat4 modelMatrix; // 메쉬의 위치, 회전 등의 값들을 담당한다.
    attribute vec3 position; // 버퍼지오메트리에서 제공해주는 정점 위치(xyz)
		
    void main() {	
      gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
    // 1.0은 원근에 관한 값이다.
    }
  `,
  fragmentShader: `
    precision mediump float; // 중간 정밀도로 미리 설정해주기
		
    void main() {
      gl_FragColor = vec4(0.0, 0.0, 1.0, 1.0);
    }
  `
})

2. 셰이더로 위치와 색상 조정하기(1) - position, color, animation

  • modelPosition의 x, y, z 값에 더하거나 빼서 위치를 변경할 수 있음
    • Three.js에서 mesh.position.y = 1; 과 동일해보임
// vertex.glsl
void main() {
  vec4 modelPosition = modelMatrix * vec4(position, 1.0); // 모델매트릭스와 포지션을 합침.
  modelPosition.y += 1.0; // y축 방향으로 이동 (위)
    
  gl_Position = projectionMatrix * viewMatrix * modelPosition;
}
Untitled
  • 차이점 : mesh라는 하나의 모델 단위를 바꾸는 것과 달리, vertexShader는 모든 정점을 바꿈.
// Three.js
const geometry = new THREE.PlaneGeometry(1, 1, 16, 16);

const verticesCount = geometry.attributes.position.count; // 정점의 개수
const randomPositions = new Float32Array(verticesCount); // 정점 위치를 랜덤하게 바꾸기

for (let i = 0; i < verticesCount; i++) {
  randomPositions[i] = (Math.random() - 0.5) * 2; // -1 ~ 1
}

geometry.setAttribute( 
  'aRandomPosition', // 쉐이더에 넘겨줄 변수명
  new THREE.BufferAttribute(randomPositions, 1) // 버퍼 객체에 원소를 1개씩 끊어 담기
);
// vertex.glsl
...
attribute float aRandomPosition; // float으로 가져오기

void main()
{
  vec4 modelPosition = modelMatrix * vec4(position, 1.0);
  modelPosition.y += aRandomPosition; // 모델 포지션에 넣기.
    
  gl_Position = projectionMatrix * viewMatrix * modelPosition;
}
  • 그러면 다음과 같이 y좌표의 정점들이 랜덤하게 위치하게 됨.
Untitled
  • 여기서 mesh.position.y = 1; 를 주석 처리하면 다음과 같이 모델 전체가 이동함.
    • 이것이 mesh와 vertexShader과의 차이점이다.
Untitled
  • aRandomPosition을 vertex에서 z축에 적용하고, fragmentShader에도 넘겨주어 색상에 입혀보자.
// vertex.glsl
...
attribute float aRandomPosition;
varying float vRandomPosition; // fragmentShader로 넘겨주기 위한 변수

void main()
{
  vec4 modelPosition = modelMatrix * vec4(position, 1.0);
  modelPosition.z += aRandomPosition; // Z축에 적용해서 3D처럼 보이게 만들기
    
  vRandomPosition = (aRandomPosition + 1.0 / 2.0); // 값 넘겨주기 + 정규화
		    
  gl_Position = projectionMatrix * viewMatrix * modelPosition;
}
// fragment.glsl
...
varying float vRandomPosition; // vertex에서 받아오기

void main()
{
  gl_FragColor = vec4(0.0, vRandomPosition, 1.0, 1.0);
}
Untitled
  • 애니메이션 적용하기. uTime을 선언해 Clock 함수로 시간값을 받아 셰이더에 넣어주고, 포지션에 적용한다. 시간에 따라 정점들이 변화하는 애니메이션이 만들어진다.
// Three.js
const clock = new THREE.Clock(); // Clock 객체 생성

const material = new THREE.RawShaderMaterial({
  uniforms: { uTime: { value : 0 } } // 셰이더에 넘겨줄 uTime 초기화
  ...
});

const draw = (mesh) => {
  mesh.material.uniforms.uTime.value = clock.getElapsedTime(); // uTime 값에 시간 넘겨주기
  ...
}
// vertex.glsl
...
attribute float aRandomPosition;
varying float vRandomPosition;
uniform float uTime; // uTime 가져오기

void main()
{
  vec4 modelPosition = modelMatrix * vec4(position, 1.0);
  modelPosition.z += aRandomPosition * uTime; // 포지션에 uTime 곱해주기. 점점 뻗어나가는 애니메이션.
    
  vRandomPosition = (aRandomPosition + 1.0 / 2.0);
		    
  gl_Position = projectionMatrix * viewMatrix * modelPosition;
}
Untitled

3. 셰이더로 위치와 색상 조정하기(2) - texture

  • 위 과정에서 만든 그래픽에 사용자 지정 이미지를 가져와 텍스쳐로써 활용하는 방법.
// Three.js
const textureLoader = new THREE.TextureLoader(); // 텍스쳐로더 선언

const material = new THREE.RawShaderMaterial({
  uniforms: { 
  ...
  uTexture: { value. textureLoader.load('assets/image.jpg') // 이미지를 텍스쳐로 가져오기
  }
  ...
});
// fragment.glsl
...
uniform sampler2D uTexture; // sampler2D 형식으로 텍스쳐를 가져오기
varying vec2 vUv; // uv맵핑을 위해 vertex로부터 uv좌표를 가져오기

void main()
{
  vec4 tex = texture2D(uTextrue, vUv); // texture2D 함수로 텍스쳐와 uv값을 가져와 vec4에 저장	
  gl_FragColor = tex; // fragColor에 tex값 할당해서 화면에 그려주기
}
// vertex.glsl
...
attribute vec2 uv; // 지오메트리에서 uv값 가져오기
varying vec2 vUv; // fragment로 넘겨주기

void main()
{
  vUv = uv; // vUv에 uv값 넘겨주기
  ...
}
  • 다음과 같이 사용자 지정 이미지가 매핑되는 것을 볼 수 있다.
Untitled
  • 특정 주기마다 일렁이는 애니메이션을 만들어본다.
  • 반복시키기 위해서 sin() 함수를 사용한다.
// vertex.glsl
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 mdoelMatrix;
uniform float uTime;

attribute vec3 position;
attribute vec2 uv;
attribute float aRandomPosition;

varying float vRandomPosition;
varying vec2 vUv;

void main()
{

  vec4 modelPosition = modelMatrix * vec4(position, 1.0);
  // modelPosition.z += aRandomPosition / 20.0 * sin(uTime);
  modelPosition.z += sin(uTime + modelPosition.x) / 2.0;
  // z값이 시간(uTime)에 따라 반복(sin())하며 x값의 영향을 받아 애니메이션을 만든다.
  
  vRandomPosition = (aRandomPosition + 1.0 / 2.0);
  vRandomPosition /= uTime * 0.3;
  
  vUv = uv;
	    
  gl_Position = projectionMatrix * viewMatrix * modelPosition;
}
// fragment.glsl
precision mediump float;
uniform sampler2D uTexture;
varying float vRandomPosition;
varying vec2 vUv;

void main() {
  vec4 tex = texture2D(uTextrue, vUv);
  gl_FragColor = tex * vRandomPosition; // 랜덤값 추가
}
Untitled
  • 위 코드는 RawShaderMaterial 및 GLSL 1 버전을 사용한 코드이다.
  • ShaderMaterial을 사용하고 GLSL 3 버전을 사용하면 문법이 더 간결해진다.
// vertex.glsl (GLSL 3 버전)
uniform float uTime;
in float aRandomPosition;

out float vRandomPosition;
out vec2 vUv;

void main()
{
  vec4 modelPosition = modelMatrix * vec4(position, 1.0);
  modelPosition.z += aRandomPosition / 20.0 * sin(uTime);

  vRandomPosition = (aRandomPosition + 1.0) / 2.0;
  vRandomPosition /= uTime * 0.3;

  gl_Position = projectionMatrix * viewMatrix * modelPosition;

  vUv = uv;
}
// fragment.glsl (GLSL 3 버전)
uniform sampler2D uTexture;

in float vRandomPosition;
in vec2 vUv;
out vec4 myFragColor;

void main()
{
  vec4 tex = texture(uTexture, vUv);
  myFragColor = tex * vRandomPosition;
}


4. 셰이더 연습하기 (1) - 그래프와 그라데이션

  1. 그라데이션
// fragment.glsl
void main() {
  float x = vUv.x;
  float y = vUv.y;
	
  float col = x; // y를 넣거나, (1.0 - x)를 넣어 방향을 바꿀 수 있음.
	
  gl_FragColor = vec4(col, col, col, 1.0);
}
Untitled
  1. 대각선 만들기
// fragment.glsl
void main() {
  float x = vUv.x;
  float y = vUv.y;
	
  vec3 col = vec3(x);
  vec3 green = vec3(0.0, 1.0, 0.0); // Green 색상값을 Vec3로 설정
	
//if (y <= x) : 화면이 분할되는 모습을 볼 수 있음
  if (y <= x + 0.005 && y + 0.005 >= x ) { // 교집합을 사용해 대각선을 만들기
    col = green;
  }

  gl_FragColor = vec4(col, 1.0); // 반복해서 col을 써주지 않고 Vec3로 감싸주는 방법
}
y <= x 그래프
y <= x 그래프
y <= x + 0.005 && y + 0.005 >= x 그래프
y <= x + 0.005 && y + 0.005 >= x 그래프

  1. 곡선 만들기
// fragment.glsl
void main() {
  float x = vUv.x;
  float y = vUv.y;

  vec3 col = vec3(x);
  vec3 green = vec3(0.0, 1.0, 0.0);

  if (x * x <= y && x * x >= y - 0.005) { // 제곱을 활용해 곡선을 그려준다.
  col = green;
  }

  gl_FragColor = vec4(col, 1.0);
}
x * x <= y && x * x >= y - 0.005
x * x <= y && x * x >= y - 0.005

  1. 그래프 + 곡선 만들기
// fragment.glsl
void main() {
  float x = vUv.x / 2.0; // 기울기를 나눠준다면 그래프가 완만해짐.
  float y = vUv.y;

  vec3 col = vec3(x * x); // 곡선과 동일한 영역을 차지하는 그라데이션 만들기
  vec3 green = vec3(0.0, 1.0, 0.0);

  if (x * x <= y && x * x >= y - 0.005) { // 제곱을 활용해 곡선을 그려준다.
  col = green;
  }

  gl_FragColor = vec4(col, 1.0);
}
float x = vUv.x * 4.0
float x = vUv.x * 4.0
float x = vUv.x / 2.0
float x = vUv.x / 2.0

5. 셰이더 연습하기 (2) - step, min, max, clamp, smoothstep, mix

  1. Step 함수 - 분할하기
// fragment.glsl
void main() {
  float x = vUv.x;
  float y = vUv.y;
	
  vec3 green = vec3(0.0, 1.0, 0.0);
	
  float strength = step(0.5, x); 
  // 첫번째 인자는 자르고 싶은 기준. x가 0~0.5일땐 0, 0.5~1일 땐 1이 됨. (계단식 그래프)
	
  if (strength == 0.0) { // 검은색 부분은
    discard; // 렌더링하지 않겠다
  }
	
  vec3 col = vec3(strength); // strength를 컬러값에 넣어주기.
  
  gl_FragColor = vec4(col, 1.0);
}
Untitled
검은색 부분(0)은 discard 했을 때.
검은색 부분(0)을 discard 했을 때.

  1. min, max 함수 - 최소값과 최대값 설정
// fragment.glsl
void main() {
  float x = vUv.x;
  float y = vUv.y;
	
  vec3 green = vec3(0.0, 1.0, 0.0);
	
  *float strength = min(0.5, x); 
  // x가 0.5보다 작을 때 x값을 반환. 0.5보다 클 땐 x값을 반환.*
	
  vec3 col = vec3(strength)
	
  gl_FragColor = vec4(col, 1.0);
}
0.5부턴 x값이 고정됨
0.5부턴 x값이 고정됨
// fragment.glsl
void main() {
  ...
  *float strength = max(0.5, x);
  // 0.5와 x값 중 큰 값을 반환*
  ...
}
Untitled

  1. Clamp 함수 - min, max 함수를 함께 사용하기
  • FragColor가 0~1 사이의 값만 쓸 수 있기 때문에 응용할 수 있음.
// fragment.glsl
void main() {
  float x = vUv.x;
  float y = vUv.y;
	
  vec3 green = vec3(0.0, 1.0, 0.0);
	
  *float strength = clamp(x, 0.3, 0.7);
  // 2번째 인자: 하한선, 3번째 인자: 상한선. 
  // 0.3보다 작은 값은 0.3으로, 0.7보다 큰 값은 0.7로 반환 (min & max)*
	
  vec3 col = vec3(strength)
	
  gl_FragColor = vec4(col, 1.0);
}
Untitled

  1. smoothstep 함수 - step과 달리, 부드럽게 커팅을 할 수 있는 기능
// fragment.glsl
void main() {
  float x = vUv.x;
  float y = vUv.y;
	
  vec3 green = vec3(0.0, 1.0, 0.0);
	
  float strength = smoothstep(0.3, 0.7, x); 
	
  vec3 col = vec3(strength)
	
  gl_FragColor = vec4(col, 1.0);
}
Untitled

참고 : Smoothstep 함수의 구현 원리
    • 아래는 smoothy라는 함수로 직접 스무스스텝 함수의 원리를 구현한 것이다.
float smoothy(float edge0, float edge1, float x) {
  float t = clamp((x - 0.3) / (0.7 - 0.3), 0.0, 1.0); // edge0이하은 0으로, edge1이상은 1로 만드는 공식

  float strength = t * t * (3.0 - 2.0 * t); // 부드러운 그라데이션을 구현하는 공식

  return strength;
}
  1. mix 함수 - 두 값을 받아, 적절히 섞어주는 기능.
// fragment.glsl
void main() {
  float x = vUv.x;
  float y = vUv.y;
	
  vec3 green = vec3(0.0, 1.0, 0.0);
  vec3 blue = vec3(0.0, 0.0, 1.0); // 그린과 섞을 블루를 만들어준다.
	
  vec3 col = mix(green, blue, x); 
  // 그린, 블루를 인자로 넣고 3번째 인자로 0.5를 설정하면 중간값이 나온다.
	
  gl_FragColor = vec4(col, 1.0);
}
mix(green, blue, 0.5)
mix(green, blue, 0.5)
mix(green, blue, x)
mix(green, blue, x)

6. 셰이더 연습하기 (3) - pow, sqrt, mod, fract, sin, cos, dbs, distance, length

  1. Pow 함수 - 거듭제곱 함수
  • 거듭제곱을 사용했던 3. 곡선 만들기 코드를 다시 가져온다.
// fragment.glsl
void main() {
  float x = vUv.x;
  float y = vUv.y;
	
  vec3 col = vec3(x);
  vec3 green = vec3(0.0, 1.0, 0.0);

  if (pow(x, 4.0) <= y && pow(x, 4.0) >= y - 0.005) { // pow 함수로 거듭제곱 해주기
    col = green;
  }

  gl_FragColor = vec4(col, 1.0);
}
Untitled

  1. sqrt 함수 - 제곱근(루트)으로 만들어주는 함수
// fragment.glsl
void main() {
  float x = vUv.x;
  float y = vUv.y;
	
  vec3 col = vec3(x);
  vec3 green = vec3(0.0, 1.0, 0.0);

  if (sqrt(x) <= y && sqrt(x) >= y - 0.005) { // sqrt 함수로 루트 그래프를 그려주기
    col = green;
  }

  gl_FragColor = vec4(col, 1.0);

}
Untitled

  1. mod 함수 - (modular) 숫자로 나눈 값의 나머지를 구하는 함수
  • 반복을 하고 싶을 때 사용한다.
// fragment.glsl
void main() {
  float x = vUv.x;
  float y = vUv.y;
	
  vec3 col = vec3(mod(x * 7.0, 1.0)); // x를 1.0으로 나눌 때 나머지값이 나오는 걸 7번 반복
  
  col = step(0.5, col); // step 함수를 응용해 계단식 그래프를 만든다
  vec3 green = vec3(0.0, 1.0, 0.0);

  gl_FragColor = vec4(col, 1.0);
}
mod(x * 7.0, 1.0)
mod(x * 7.0, 1.0)
step(0.5, col)을 추가
step(0.5, col)을 추가

  1. fract 함수 - 소수점만 반환해주는 함수
  • fract(0.4); // 0.4 반환
  • fract(2.3); // 0.3 반환
  • fract(5.75); // 0.75 반환
// fragment.glsl
void main() {
  float x = vUv.x;
  float y = vUv.y;
	
  vec3 col = vec3(fract((y - 0.11) * 7.0)); 
  vec3 col2 = vec3(fract((x - 0.11) * 7.0));
  // x,y에 0.11을 빼주어 가운데 정렬된 격자무늬로 만듦.

  col = 1.0 - step(0.5, col) * step(0.5, col2); // 1.0 에서 값을 빼주어 색반전 시킴.

  gl_FragColor = vec4(col, 1.0);
}
(fract(y * 7.0));
(fract(y * 7.0));
위치 조정 + 색반전 된 모습
위치 조정 + 색반전 된 모습

  1. sin, cos 함수
// fragment.glsl
void main() {
  float x = vUv.x;
  float y = vUv.y;
	
  vec3 col = vec3(sin(x * 10.0)); // 파동과 같은 그래프를 만듦. 
		
  gl_FragColor = vec4(col, 1.0);
}
sin( x * 10.0 )
sin( x * 10.0 )

  1. abs 함수 - 절대값을 반환함
// fragment.glsl
void main() {
  float x = vUv.x;
  float y = vUv.y;
	
  vec3 col = vec3(abs((sin(x * 20.0))); // abs 함수로 감싸주기
		
  gl_FragColor = vec4(col, 1.0);
}
Untitled

  1. distance 함수 - 거리를 구하는 함수
  • 인자를 기준으로 거리값이 커질 수록 1에 가까워짐
// fragment.glsl
void main() {
  float x = vUv.x;
  float y = vUv.y;
	
  float dist = distance(vec2(x, y), vec2(0.5)); // x,y와 일정한 거리의 방사형 원을 만들기
	
  dist = step(0.3, dist); // 계단식 그래프를 만들어서 뚜렷한 원형 만들기 
	
  vec3 col = vec3(dist);
		
  gl_FragColor = vec4(col, 1.0);
}
distance(x, 0.5); 0.5를 기준으로 거리값이 커질 수록 1에 가까워짐
distance(x, 0.5); 0.5를 기준으로 거리값이 커질 수록 1에 가까워짐
distance로 원 만들기. x,y값 설정 + step 함수 응용
length함수로 원 만들기. distance(vec2(x, y), vec2(0.5)); 와 동일하다.

  1. length 함수 - 벡터의 길이를 구함. (얼마나 멀리 있는지)
  • distance 함수로 구현 가능. 접근 개념만 살짝 다름. 거리냐, 길이의 크기냐.
// fragment.glsl
void main() {
  float x = vUv.x;
  float y = vUv.y;

  float dist = length(vec2(x, y) - 0.5); // length 함수로 원 만들기

  vec3 col = vec3(dist);

  gl_FragColor = vec4(col, 1.0);
}
length함수로 원 만들기. distance(vec2(x, y), vec2(0.5)); 와 동일하다.
length함수로 원 만들기. distance(vec2(x, y), vec2(0.5)); 와 동일하다.
Zalgritte profile image Zalgritte
프로덕트 디자이너로 근무했습니다. 현재는 프론트엔드 개발을 배우고 있습니다. 인터랙티브 웹을 좋아합니다.