그래픽스 API 다 쳐 깨부수기. (OpenGL 편)

신년이 되고 나에게는 한가지 목표가 생겼다.

아! 올해 상반기에는 대표적인 그래픽스 API에 대한 포폴을 만들자!

음...그래서 GPT한테 난이도 순서를 물어보니 이샛기.
OpenGL -> DX11 -> DX12 -> Vulkan이란다.

그래서, OpenGL부터 시작함. ㅇㅇ

(참고, 그래픽스를 조금이라도 아는 사람이 보면 좋은 게시물이란다.)

1. 항상 뭘 할까부터...

Penitence 프로젝트를 진행하면서 내가 생각보다 잦밥이란 걸 느꼈다.

그래서, 이번 프로젝트는 구조를 크게 신경쓰지 않고 제작하기로 함.

겨울이 되면서 항상 내가 듣는 음악이 있다.
다른 사람의 음악이 아닌 내 노래인데... Video Label

제목은 'White Ashes'이다.

2021년 막바지 겨울,

당시 심적으로 너무 힘들었다. (이유는 지금도 모르겠다.)
그때 새벽에 생각나는 멜로디를 바탕으로 3시간동안 작곡한 노래인데,

흡연자(현재는 금연 4개월차)였던 나는 모니터링 하려고(아직도 기억난다.)
새벽 4시, 1층에서 담배 물고, 에어팟 꽂고, 하늘을 봤는데,

눈이 내렸다.

묘하게도 그 장면이 노래와 잘 녹아들어 가는 것이다.
떨어지는 담뱃재가 내리는 눈과 함께 섞여서...암튼 그랬다.

그래서 이 곡의 제목이 'White Ashes'이다.

그날 이후부터 눈이 내리면 항상 이 노래를 듣는다.
나에겐 캐롤보다 더 뭉클

Tlvkf! 지원이형! 그래서 이걸로 뭘 했다는거야~~!

아, 너무 F였던 시절 이야기를 쏟아냈다.
암튼 난, 이번 기회에 이 노래에게 작은 선물을 주리라 다짐했다.

3년만의 뮤직비디오

이게 이 프로젝트의 시작이었다.

2. 컨셉?

음...사실 가장 먼저 떠오른건 이 노래의 뮤비다.

Video Label

검정치마 - Everything

이 뮤비에서 내가 가장 좋아하는 장면은
수많은 붉은 의자가 날아가는 장면인데, 이걸 오마쥬하고 싶었다.

처음에는 검은 화면에 하얀 큐브가 대충 떨어지는... 그런 느낌만 생각하며, 시작했다.

3. 개발 시작!

이전 프로젝트, Penitence와 달리, 외부 라이브러리를 프로젝트 내부에 심는 것을 최대한 피하려 노력했다.

OpenGL은 이미 최신 맥에 포함되어 있어, Pass.
brew로 glfw, glm을 설치하고...CMakeList도 설정하고...하면?

이제 세팅 끝! 개발 시작이렸다.

4. 렌더링 파이프라인? GLSL?

개념부터 확실히 잡고 가자.

Rendering Pipeline은, 데이터가 디스플레이에 표현되는 프로세스를 뜻한다.

각 단계에서 데이터를 참조하고, 계산하고, 가공하여 다음 단계로 넘겨주는, 일종의 파이프라인 직렬 구조로 되어있다.

그러니까, 각 픽셀마다 이런 파이프라인을 돌려야한다.
근데, 혹시 현대 모니터 픽셀의 개수가 몇개인지 아는가?
HD 기준, 1920 x 1080 개의 픽셀로 이루어져 있다!

CPU라면, 이런 연산을 할때,

"아앙! 주인님! 너무 힘들어요!! ㅜㅜ"

라 하겠지만, GPU는 다르다. GPU는 간단한 병렬 연산(행렬곱과 같은...)에 특화된 하드웨어이기에,
CPU보다 더 빠르게 픽셀의 파이프라인을 돌릴 수 있다.

음...하드웨어가 있다면 필히 소프트웨어도 존재해야 한다.
GPU 소프트웨어는 무엇으로 프로그래밍 하지? C++? 자바?

자, 우리는 OpenGL을 사용중이다.

GLSL(Graphics Library Shader Language)은 OpenGL 파이프라인에 특화된 GPU 프로그래밍 언어이다.

OpenGL의 파이프라인은 다른 블로그, 글을 참조하면 더 좋은 게시물이 많으니,
생략한다. 대신, 나는 파이프라인과 glsl의 연관성에 좀 더 무게를 실을 것이다.

GLSL에서 가장 중요한 것은 다음 두개이다.

  • Vertex Shader
  • Fragment Shader

버텍스 쉐이더는, 기하의 데이터에 관한 프로그램이다.

뭐...기하의 위치나, 색과 같은 정보들? 프로그래밍 단계에서 attribute로 넘겨줄 수 있다.

아래는, 실제 내가 사용한 코드.

// Vertex GLSL
#version 330 core

layout(location = 0) in vec3 att_pos;
layout(location = 1) in vec3 att_norm;
layout(location = 2) in vec3 att_color;

out vec3 in_pos;
out vec3 in_norm;
out vec3 in_color;

// MVP
uniform mat4 model;
uniform mat4 view;
uniform mat4 proj;

void main() {
  in_pos = vec3(model * vec4(att_pos, 1.0));
  in_norm = normalize(mat3(transpose(inverse(model))) * att_norm);
  in_color = att_color;

  gl_Position = proj * view * model * vec4(in_pos, 1.0);
}

엄...우선 in과 out필드를 생각해볼 필요가 있음.

in은 말 그대로, 쉐이더에 입력된 데이터, out은 어딘가로 나가는 데이터임.
in으로 들어온 데이터를 바탕으로 가공된 데이터인, out들은 다시 프래그먼트 쉐이더로 들어감.

유니폼은 그냥, 변환을 위한 세팅? 이라고 보면 쉬움.
gl_Position에 정점 데이터가 저장되는데 이때, MVP 행렬을 통해 좌표변환됨.


자, 그러면 프래그먼트 쉐이더는 뭘하는 놈이냐?

프래그먼트 쉐이더는 실제 보여지는 데이터를 결정하는 역할을 함.

색에 관한 것들은 여기서 처리됨.

여기도 내 코드를 보겠음!

#version 330 core

in vec3 in_pos;
in vec3 in_norm;
in vec3 in_color;

out vec4 frag_color;

uniform vec3 light_pos;
uniform vec3 light_color;
uniform vec3 view_pos;

uniform bool enabled;

void main() {
  vec3 amb = 0.1 * light_color;

  vec3 light_dir = normalize(light_pos - in_pos);
  float _diff = max(0.0, dot(in_norm, light_dir));
  vec3 diff = _diff * light_color;

  vec3 view_dir = normalize(view_pos - in_pos);
  vec3 reflect_dir = reflect(-light_dir, in_norm);
  float _spec = pow(max(dot(view_dir, reflect_dir), 0.0), 0.5);
  vec3 spec = 5.0 * _spec * light_color;

  vec3 extra = vec3(0.0, 0.0, 0.0);

  if (enabled)
    extra = in_norm;

  vec3 result = (amb + diff + spec + extra) * in_color;
  frag_color = vec4(result, 1.0);
}

음...조금 복잡하지만, 하나씩 뜯어보게뜸.

위 코드를 요약하면, 들어온 정보에 대해 phong 컬러 블렌딩하는 과정임.(Phong 컬러 설명)

거기다가 특정 커멘드(enabled)를 입력하면, extra바뀜.
이것의 역할은, 랜덤 생성된 큐브의 색 마저도 랜덤으로 바꾸기 위한 것임.

음? 그게 무슨 소리냐? 하면.

랜덤으로 생성된 큐브들은 위치, 회전이 다르기에 거의 전부 다를 것이다.
그리고 그 벡터를 색값으로 매핑하면? 전부 랜덤한 컬러가 입혀진 것처럼 보이는 것!

그렇게 가공된 색은, frag_color에 저장되어 디스플레이된다!


이렇듯 OpenGL의 쉐이더 구조를 진짜 겉만 찍먹해보았다.

이제 다음 토픽은?

5. 포스트 프로세싱(Post-processing)

내가 Everything 뮤비에서 가장 좋아하는게, 래트로한 감성임.
그리고 사실, 이 프로젝트의 핵심이기도 함.

이러한 효과를 그래픽스 후 처리라고 함.
이걸 영어로 하면, 뭔지 알겠져잉?

자, 그럼 포스트 프로세싱을 억덕계 구현할거냐?
그것도 사실 glsl로 작성하는 것임.

#version 330 core

layout(location = 0) in vec2 att_pos;
layout(location = 1) in vec2 att_tex_coords;

out vec2 in_tex_coords;

void main() {
  in_tex_coords = att_tex_coords;
  gl_Position = vec4(att_pos, 0.0, 1.0);
}

사실, 버텍스 쉐이더는 별 역할을 안함.
프로그램 코드에서 입력한 정점을 프래그먼트로 옮겨주는 역할임.

#version 330 core

in vec2 in_tex_coords;

out vec4 frag_color;

uniform sampler2D screen_tex;
uniform vec2 screen_res;

float rand(vec2 co) {
  return fract(sin(dot(co.xy, vec2(12.9898, 78.233))) * 43758.5453);
}

void main() {
  vec3 color = texture(screen_tex, in_tex_coords).rgb;
  float line_thick = 1.0;
  float intensity = 0.2;

  float y = in_tex_coords.y * screen_res.y;

  if (mod(y, 2.0) < line_thick)
    color *= (1.0 - intensity);

  float noise_strength = 0.05;
  float noise = rand(in_tex_coords) * noise_strength;
  color += vec3(noise);
  frag_color = vec4(color, 1.0);
}

코드는 단순함.

  1. 스캔라인 - 예전 CRT 모니터에 잘 보면, 가로줄? 아지랑이? 같은 그런 선이 있는데, 그거 구현함. 화면 좌표 y값에 따라, (여기서는 짝수라인) 색의 강도를 조절해서 비슷한 효과를 구현.

  2. 필름 그레인(노이즈) 효과 - 이건 그냥 유명한 랜덤 사인 노이즈 함수로 랜덤 좌표 픽셀 색을 조절하는 방법임.

음...사실 별거 없음.
ㄹㅇ 별거 없음 ㅜㅜ

6. 과정보단 결과지 샛갸

음...그렇게 만들어진 내 뮤비를 보여주겠다란다.

Video Label

_Grammer - White ashes

(댓글 많이 달아주셔요잉~)

아니, 생각보다 엄청난 결과물이 나온 것 같다.
앞뒤 자르기, 크기 조정 제외한, 컷 편집없는 원 테이크 영상이다.

비하인드 컷.

사실, 이 모든걸 녹화한 디바이스가 MacBook(2017)이다.
이게 개쩌는 사실인게, 영상을 보면 끊김이 없다!

분명, OBS로 녹화도 했는데...어떻게 된걸까?

렌더링 최적화????!??
는 아니고 그냥 VSCode를 끄고 터미널로 실행시켰더니...
잘 실행됐다. ㅎ

7. 마무으리

이번 포스트를 시작으로 다음 3개의 포스트는 전부 그래픽스 API들을 다룰 예정이다.

많은 기대 부탁 ㄴㄴㅎㅎ