본문 바로가기

Graphics Note

OpenGL 강좌 - 3. 삼각형 그리기(셰이더)

지난 강좌에 이어.. 계속 삼각형 그리는 코드를 살펴볼 예정이다. 이번 강좌를 통해서는 다음의 내용을 배우게 될 것이다.


* 셰이더(shader) 기본 이해

* 셰이더 프로그램 생성 방법


우선 InitApp 함수의 셰이더 생성 부분을 살펴보자.

// 셰이더 프로그램 오브젝트
GLuint gShaderProgram;

bool InitApp() {
	...

	// 셰이더 파일 읽기
	std::string vertShaderSource = ReadStringFromFile("BasicShader.glvs");
	std::string fragShaderSource = ReadStringFromFile("BasicShader.glfs");

	// 셰이더 오브젝트 생성
	GLuint vertShaderObj = CreateShader(GL_VERTEX_SHADER, vertShaderSource);
	GLuint fragShaderObj = CreateShader(GL_FRAGMENT_SHADER, fragShaderSource);

	// 셰이더 프로그램 오브젝트 생성
	gShaderProgram = glCreateProgram();

	// 셰이더 프로그램에 버텍스 및 프래그먼트 셰이더 등록
	glAttachShader(gShaderProgram, vertShaderObj);
	glAttachShader(gShaderProgram, fragShaderObj);

	// 셰이더 프로그램과 셰이더 링킹(일종의 컴파일) 그리고 확인
	glLinkProgram(gShaderProgram);
	if (!CheckProgram(gShaderProgram)) {
		glDeleteProgram(gShaderProgram);
		return false;
	}

	// 사용된 셰이더 떼어냄
	glDetachShader(gShaderProgram, vertShaderObj);
	glDetachShader(gShaderProgram, fragShaderObj);

	// 셰이더 삭제
	glDeleteShader(vertShaderObj);
	glDeleteShader(fragShaderObj);
	
	...
}

길고 무서운 코드가 등장했다. 바로 코드설명을 들어가기 전에 적을 파악할 필요가 있지않겠는가? 셰이더에 대해 잠간 소개하고 넘어가자.


컴퓨터그래픽스 하드웨어 초기에는 기본적으로 제공되던 기능만으론 표현할 수 없는것들이 너무 많았다. 대표적으로 '그림자(shadow)'가 있는데, 이런 특별한 표현을 위해 모든 기능을 만들어주는 대신 '프로그래밍 가능한 GPU 렌더링 파이프라인'을 만들고, GPU 하드웨어용 프로그램을 만들 수 있는 언어를 제공해줬다. 이렇게 탄생한것이 셰이더 언어(shader language)이며 OpenGL에서는 GLSL(OpenGL Shader Language)라는 이름으로 불리운다.

현대에 이르러서는 초기 제공되던 기능들(고정 파이프라인 이라 불린다)은 더 이상 쓰지 않게 되었고 셰이더 언어를 더욱 강화하는 형태로 발전하였다. 즉, 아무리 간단한 도형이라 해도 GPU가 이를 어떻게 그릴것인지 하나하나 지정해줘야 한다는 뜻이다.


셰이더 프로그램 오브젝트는 GPU 파이프라인에서 수행될 프로그램의 핸들러이고, GPU 파이프라인의 각 단계를 설명하는 셰이더 스테이지로 구성된다. 여기서 셰이더 스테이지는 총 다섯개로 나뉘며 각 단계에서의 역할이 서로 다르고 열거된 순서대로 실행된다.

 - vertex shader

 - tessellation control shader

 - tessellation evaluation shader

 - geometry shader

 - fragment shader


사실 compute shader라는 단계가 하나 더 있고 opengl 4.3부터 사용 가능한 특수 목적 셰이더이다. 기회가 되면 다뤄보겠지만 아직은 무시하도록 하자. 또한 GPU 파이프라인이라던가 셰이더 각 단계에 대한 자세한 설명은 조금 뒤의 강좌로 미뤄두겠다. 우선은 삼각형이나 빨리 그려보자. 현기증 날 것 같다.


각 단계의 셰이더는 glsl 언어로 작성해야하며 이들을 컴파일하여 프로그램을 만드는것이다. *.glvs, *.glfs 같은 확장자(?) 로 표현했지만 사실 파일명이나 확장자 따윈 아무 상관 없다. 위에서 다섯 단계의 셰이더가 있다고 했지만, 진한 글씨로 표현한 vertex shader와 fragment shader가 꼭 필요한 단계이며 나머지는 옵션이다. 앞으로 강좌가 진행되면서 다뤄보겠지만, 지금은 이 두 셰이더만 붙여볼거다.


그래서 셰이더 프로그램이 만들어지는 순서는 아래와 같다. '컴파일' 이라는 익숙한 단어가 설명하듯 .cpp 파일들이 한데엮여 하나의 프로그램이 만들어지는것과 유사하다. 하나하나 차근차근 설명할테니 두려워 말라.

 - 각 단계의 셰이더 소스 준비

 - 각 단계의 셰이더 오브젝트 생성 및 컴파일

 - (셰이더) 프로그램 생성

 - (셰이더) 프로그램에 셰이더 오브젝트 붙이기 및 연결

 - 붙인 셰이더 오브젝트 떼어냄

 - 다 쓴 셰이더 오브젝트 제거



std::string vertShaderSource = ReadStringFromFile("BasicShader.glvs");
std::string fragShaderSource = ReadStringFromFile("BasicShader.glfs");

첫번째로 셰이더의 소스의 준비는 소스를 따로 파일에 작성하해서 읽어오던, cpp코드에 직접 문자열로 밖던 상관없다. glsl 명세를 따라 제대로 작성된 코드이기만 하면 문제없다.


GLuint vertShaderObj = CreateShader(GL_VERTEX_SHADER, vertShaderSource);
GLuint fragShaderObj = CreateShader(GL_FRAGMENT_SHADER, fragShaderSource);

다음으로 준비된 셰이더 소스로 셰이더 오브젝트를 생성하고 컴파일해야 한다. 이러한 작업을 해주는 CreateShader 함수를 들여다보자.

GLuint CreateShader(GLenum shaderType, const std::string& source) {
	GLuint shader = glCreateShader(shaderType);
	if (shader == 0)
		return 0;

	// set shader source
	const char* raw_str = source.c_str();
	glShaderSource(shader, 1, &raw_str, NULL);

	// compile shader object
	glCompileShader(shader);

	// check compilation error
	if (!CheckShader(shader)){
		glDeleteShader(shader);
		return 0;
	}

	return shader;
}

셰이더 오브젝트의 생성은, glCreateShader 함수를 통해 지정된 타입의 셰이더 오브젝트를 만들고(할당받고) glShaderSource 함수 셰이더 소스(코드)를 지정한 후 glCompileShader 함수로 컴파일하면 끝이다.

여기서 타입은 어떤 단계의 셰이더 오브젝트를 만드느냐에 따라 달라지며 GL_VERTEX_SHADER, GL_FRAGMENT_SHADER 등이 있다.

바로 사용하기 전에 컴파일에 실패하는 경우를 위해 제대로 컴파일 되었나 체크 해보는것은 필수 과정이다.


gShaderProgram = glCreateProgram();

glAttachShader(gShaderProgram, vertShaderObj);
glAttachShader(gShaderProgram, fragShaderObj);

정점 그리고 프래그먼트 셰이더 오브젝트가 모두 제대로 생성 및 컴파일되었다면 셰이더 프로그램을 만든 후 연결해주자.


glLinkProgram(gShaderProgram);
if (!CheckProgram(gShaderProgram)) {
	glDeleteProgram(gShaderProgram);
	return false;
}

glDetachShader(gShaderProgram, vertShaderObj);
glDetachShader(gShaderProgram, fragShaderObj);

glDeleteShader(vertShaderObj);
glDeleteShader(fragShaderObj);

마지막으로 프로그램을 링크하고, 문제가 없는지 체크한 후 붙였던 셰이더 오브젝트들을 깔끔하게 떼어내고 삭제해주는것으로 끝난다.


정작 중요한 셰이더 소스는 설명하지 않았는데, 다음 강좌에서 '정점 버퍼 오브젝트' 라는 것을 먼저 설명한 후 언급하겠다.


강좌의 예제코드는 모두 github에 올려두었다.

https://github.com/alleysark/OpenGL-Tutorials