셰이더 코딩 입문 : 셰이더 작성

Jiyeon Jeong·2021년 8월 5일
0
post-thumbnail

이번 기회에 그래픽스를 공부해야 할 일이 있어 학교 도서관에서 책을 빌려 공부를 시작해보았다. 책을 읽고 중요한 내용을 정리하였다.

참고로 본인 기준 필요한 부분만 벨로그에 정리할 예정이다.


준비

본 서적에서는 오픈프레임웍스(OF)를 사용한다. 오픈프레임웍스는 게임 엔진처럼 복잡하지 않으면서 셰이더에 집중하기 좋을 만큼의 최소한의 환경을 제공해준다고 한다. 오픈프레임웍스는 본 링크를 따라가면 확인할 수 있다.
본인은 윈도우 운영체제에 VS2019를 사용하므로 그대로 서적을 따라하였다.

  1. OF를 설치하려는 곳에 내려받은 파일의 압축을 푼다
  2. 폴더에 projectGenerator-vs 폴더에 들어간 후 projectGenerator.exe 파일을 실행한다.
  3. 원하는 경로와 프로젝트 이름을 입력하고 Generate 버튼을 선택한다
  4. Open in IDE창을 선택하고 프로젝트 솔루션을 빌드한 후 실행한다.

참고로 여기서 한글 경로가 들어가면 projectGenerator 실행파일이 실행되지 않는다.


프로젝트

기본적으로 OF 프로젝트를 생성하면 main.cpp와 ofApp.h, offApp.cpp라는 소스코드 파일이 들어있다. main.cpp에는 main() 함수가 존재하는데 이는 OfApp 타입 객체를 생성한다. main()을 따로 수정하지 않고 실행하면 OpenGL은 2.0 Version이 Default로 사용된다.

int main( ) {
    ofGLWindowSettings glSettings;
    glSettings.setSize(1024, 768);
    glSettings.windowMode = OF_WINDOW;
    glSettings.setGLVersion(4, 1);
    ofCreateWindow(glSettings);
    //OpenGL 4.1을 사용하기 위한 설정 코드

    ofRunApp(new ofApp());

}

우리는 OpenGL 4.1을 사용하므로 위의 코드처럼 main()에 지정을 해준다.
이제 main()은 수정을 따로 진행하지 않고 대부분 ofApp의 헤더와 C++ 파일에서 진행하게 된다.

Vertex Shader

셰이더 코드는 보통 C++ 코드가 아니라 별도의 파일에 저장된다. GLSL(OpenGL Shading Language)에서 vertex shader는 .vert라는 확장자를 사용하고 코드가 존재하는 곳이 아닌 메쉬나 이미지 같은 애셋들이 존재하는 폴더에 저장한다.
아래의 코드는 셰이더 파일의 코드이다.

#version 410 // GLSL 버전 선언
in vec3 position; // vertex shader가 사용하게 될 메쉬 정보 선언

void main(){
    gl_Position = vec4(position, 1.0);
}

vertex shader 파일에는 in이라는 키워드가 존재하는데 in은 GLSL만의 독특한 키워드이다. 이는 셰이더가 렌더링 파이프라인의 앞 단계로부터 전달받게 될 데이터가 무엇인지 구체적으로 명시할 때 사용한다.
위의 코드엔 사용되지 않았는데 out이라는 키워드도 존재한다. in에서 유추할 수 있듯이 out 키워드는 렌더링 파이프라인의 다음 단계로 전달할 변수를 선언할 때 사용한다.
위의 코드에서 gl_Position이라는 변수가 존재하는데, 이는 GLSL에서 기본으로 제공하는 특수한 변수이다. vertex shader에서 이후 파이프라인으로 전달할 위치 데이터를 저장하는 변수이다.

정규화 장치 좌표

위의 코드를 적용하여 실행하면 결과는 예상과 다른 모습이 된다. 앞에서 봤던 OF 기본 셰이더에는 vertex 좌표를 스크린 픽셀 좌표에 맞추기 위한 별도의 처리가 존재하기 때문이다. 일반적으로 vertex shader는 이런 처리를 하지 않는다. 그 대신 NDC(정규화 장치 좌표계)를 기준으로 위치 데이터를 출력한다. NDC는 중앙이 (0, 0)이다.

Fragment Shader

fragment는 화면의 특정 픽셀을 채우기 위해 필요한 정보를 가리킨다. 즉 fragment shader는 각 fragment의 색상을 결정하는 것이다.

#version 410
out vec4 outColor;

void main() {
    outColor = vec4(1.0,0.0,0.0,1.0);
}

fragment shader 코드는 vertex shader 코드와 비슷하다. 차이점은 데이터를 저장할 기본 변수가 존재하지 않아 직접 변수를 만들어야 한다.

셰이더 사용 후 결과


앞서 설명한 내용을 모두 코드로 구현할 시 위와 같은 결과가 화면에 출력된다.

Vertex 속성

Vertex에 어떤 데이터를 저장하는 것을 그래픽 용어로 메쉬에 Vertex Attribute를 추가한다고 표현한다.

Fragment 보간

Fragment Interpolation은 쉽게 말해 블렌딩이라고 생각하면 된다.

위의 그림은 프래그먼트 보간의 예이다. 여기서 각 Vertex가 R,G,B로 표시된 프래그먼트의 중앙에 정확히 위치한다고 가정한다. Vertex 사이에서 생성된 프래그먼트들이 각 Vertex의 색상을 어떻게 반영하는지 그림을 통해 이해할 수 있다. 위의 그림에서 확되된 프래그먼트는 페이스 정중앙에 존재하지 않고 R Vertex로 조금 치우쳐져 있다. 그러므로 R Vertex의 붉은 색상을 좀더 반영하고 있음을 알 수 있다. 모든 Fragment Shader의 in 변수는 이런 방식으로 Vertex 데이터를 보간한 값을 갖는다.
Vertex 또는 Fragment마다 값이 다를 수 있는 데이터의 경우 보간은 매우 효율적인 데이터 처리 방법이다.

코드 변경 후 결과


Vertex 색상 추가 후 Fragment Shader로 보냈더니 이러한 결과가 나왔다. 세 개의 Vertex에 서로 다른 색상을 지정한 것이 전부인데, 이렇게 다양한 색상이 그려진 이유는 Fragment 보간이라는 과정으로 인해 이루어지는 것을 확인할 수 있다.

유니폼 변수

Vertex 데이터를 변경하는 방법은 상대적으로 느린 공정이다. 이로 인해 매 프레임 반복하는 것은 바람직하지 않다. 조금 더 효율적으로 데이터를 보낼 수 있는 방법이 Uniform Variable이다. 이는 Vertex 데이터를 거치지 않고 C++ 코드에서 바로 값을 지정하는 것이다. 메쉬를 렌더링 할 때 데이터를 훨씬 쉽고 빠르게 지정할 수 있는 대안이다.


오늘은 간단하게 셰이더 작성 방법을 공부해보았다. 책에 중간에 글로만 간단하게 설명되어 있는 부분이 존재해 중간에 헤맸지만, 금방 이해하고 실천할 수 있었다.

profile
기록용입니다.

0개의 댓글