본문 바로가기

컴퓨터그래픽스

[OpenGL 공부] Shader

학습을 위해 참조한 사이트

https://learnopengl.com/Getting-started/Shaders

 

LearnOpenGL - Shaders

Shaders Getting-started/Shaders As mentioned in the Hello Triangle chapter, shaders are little programs that rest on the GPU. These programs are run for each specific section of the graphics pipeline. In a basic sense, shaders are nothing more than program

learnopengl.com

 

셰이더

GPU에 기반을 둔 작은 프로그램으로, 기본적인 의미에서 입력을 출력으로 변환하는 프로그램

GLSL

C와 유사한 언어로, 벡터 및 행렬 조작을 위해 필요한 기능들을 제공한다.

#version 330 core

layout (location = 0) in vec3 aPos;

void main(void) {
	gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}

 

 

버전 선언

GLSL을 작성할 땐 항상 셰이더의 버전 선언으로 시작한다.

 

정점 속성 연결

정점 데이터를 정점 셰이더와 연결한다. 연결을 위한 방법으로는 세 가지 방법이 있고, 어느 경우를 사용해도 모두 동작이 가능하다.

 

1. layout 키워드 (In-Shader)

layout(location = #)  var;

형태로 사용한다. 변수 var에 #에 해당하는 인덱스가 할당된다. 이 방법을 사용하면 이후에 나올 방법에 대해 속성 위치에 대한 지정자를 생략할 수 있다는 장점이 있다. 일반적으로 선호되는 방법이며, 셰이더 파일 내부에 정의된다.

 

2. glBindAttribLocation 함수 (Pre-link)

glBindAttribLocation(GLuint program, GLuint index, const GLchar *name);

정점 셰이더 프로그램을 연결하기 전에, 즉 셰이더 코드가 아닌 외부 애플리케이션에서 특정 속성을 특정 인덱스에 연결하도록 지시할 수가 있다. 특정 속성에 해당하는 이름은 정점 셰이더 내부에 실제로 존재하는 이름을 사용해야 한다.

 

3. 자동 할당

위 2가지 방법을 모두 사용하지 않은 경우, 셰이더 프로그램을 연결할 때 OpenGL에서 자동으로 속성에 대한 인덱스를 할당한다.

 

입력과 출력 지정

각각 in과 out 키워드를 사용하며, 한 셰이더에서 다른 셰이더로 데이터를 보내기 위해서는 출력을 선언하여 수신 셰이더에서 유사하게 입력을 선언해야 한다. 이때 데이터를 송수신하는 셰이더 간 이름과 유형이 동일하도록 설정한다.

/* vertex shader */
#version 330 core
layout (location = 0) in vec3 aPos;
//layout (location = 1) in vec3 aColor;

out vec3 ourPosition; 

void main() 
{
	gl_Position = vec4(aPos, 1.0);
	ourPosition = aPos;
}
/* fragment shader */
#version 330 core
out vec4 FragColor;

in vec3 ourPosition;

void main()
{
	FragColor = vec4(ourPosition, 1.0);
}

셰이더 간 데이터를 연결하는 예시이다. 셰이더의 시작은 항상 main 함수이며, main 함수 이전에 정의한 입력 및 출력 데이터에 대한 처리를 수행한다. 위 코드의 경우 입력 데이터로 vec3 타입의 위치 데이터를 gl_position을 통해 설정한 뒤, 출력 변수인 ourPosition에 저장하여 내보내는 것을 확인할 수 있다.

프래그먼트 셰이더에서는 정점 셰이더에서 내보낸 데이터를 수신하기 위해 입력으로 이름과 타입이 동일하게 매치되도록 하는 ourPosition이 선언되어 있다. 결과적으로 위 셰이더는 픽셀의 색상이 결정될 때 정점의 위치 정보 그대로 색상이 결정되도록 하고 있다.

삼각형을 그리는 코드에 해당 셰이더를 적용한 결과이다. 삼각형의 중심 좌표인 (0, 0)을 기준으로 좌하단 정점의 경우 위치가 (-0.5, -0.5)이기 때문에 정점 위치를 그대로 픽셀 색상이 반영하지만 음수값이기 때문에 픽셀 색상이 (0.0, 0.0, 0.0, 1.0)으로 보간 되어 검은색을 띠는 것을 알 수 있다.

 

유니폼(Uniform)

GLSL에서는 CPU의 애플리케이션에서 GPU의 셰이더로 데이터를 전달할 수 있는 또 다른 방법으로 uniform 키워드를 제공하고 있다. 앞서 설명한 방법과 다른 점은 전역으로 지정되어 모든 셰이더에서 접근이 가능하다는 점이 있다. 

uniform type var;

 

uniform에 데이터를 넘겨주기 위해서는 다음과 같은 OpenGL에서 제공하는 함수를 사용한다.

glGetUniformLocation(GLUint program, const GLchar* name);

 

첫번째 인수에는 셰이더 파일이 링크되어 있는 프로그램의 ID값을 넘겨준다. 두 번째 인수로는 const char* 타입으로 실제로 셰이더에 uniform으로 선언된 변수의 이름이다. 반환 결과로 해당 변수 위치의 주소를 얻을 수 있다. 위치를 얻으면, glUniform 함수를 통해 타입에 따라 값을 지정할 수가 있다.

 

다음은 uniform 변수를 사용한 LearnOpenGL 사이트에서 제공하는 예제로, 시간에 따라 삼각형의 색이 초록색으로 점멸하도록 한다.

전체 코드는 다음과 같다.

/* shader.cpp */
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <string>
#include <iostream>
#include <fstream>
#include "shader.h"

GLuint renderingProgram;

using namespace std;

void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
	glViewport(0, 0, width, height);
}

void processInput(GLFWwindow* window)
{
	if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
	{
		std::cout << "Pressed ESC" << std::endl;
		glfwSetWindowShouldClose(window, true);
	}
}

// vertex 정의
float vertices[] = {
	// positions			// colors
	-0.5f, -0.5f, 0.0f,		1.0f, 0.0f, 0.0f,
	 0.5f, -0.5f, 0.0f,		0.0f, 1.0f, 0.0f,
 	 0.0f,  0.5f, 0.0f,		0.0f, 0.0f, 1.0f
};


int main() {
	glfwInit();
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

	GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
	if (window == NULL)
	{
		std::cout << "Failed to create GLFW window" << std::endl;
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(window);
	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
	{
		std::cout << "Failed to initialize GLAD" << std::endl;
		return -1;
	}

	unsigned int VBO, VAO;

	glGenBuffers(1, &VBO);
	glGenVertexArrays(1, &VAO);

	glBindVertexArray(VAO);
	glBindBuffer(GL_ARRAY_BUFFER, VBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
	// position attribute
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
	glEnableVertexAttribArray(0);
	// color attribute
	glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
	glEnableVertexAttribArray(1);

	Shader ourShader("shaders/vertShader.glsl", "shaders/fragShader.glsl");

	while (!glfwWindowShouldClose(window)) {
		// 입력 감지
		processInput(window);

		// 렌더링 명령 ...
		glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT);
		
		ourShader.use();

		float timeValue = glfwGetTime();
		float greenValue = sin(timeValue) / 2.0f + 0.5f;
		int vertexColorLocation = glGetUniformLocation(ourShader.ID, "ourColor");
		glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);

		glBindVertexArray(VAO);
		glDrawArrays(GL_TRIANGLES, 0, 3);

		// 렌더링 명령 ...

		glfwSwapBuffers(window);
		glfwPollEvents();
	}

	glfwTerminate();
	return 0;
}
/* shader.h */

#ifndef SHADER_H
#define SHADER_H

#include <glad/glad.h>

#include <string>
#include <fstream>
#include <sstream>
#include <iostream>

class Shader 
{
public:
	unsigned int ID;

	Shader(const char* vertexPath, const char* fragmentPath)
	{
		std::string vertexCode;
		std::string fragmentCode;
		std::ifstream vShaderFile;
		std::ifstream fShaderFile;

		vShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
		fShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
		try
		{
			vShaderFile.open(vertexPath);
			fShaderFile.open(fragmentPath);
			std::stringstream vShaderStream, fShaderStream;
			vShaderStream << vShaderFile.rdbuf();
			fShaderStream << fShaderFile.rdbuf();
			vShaderFile.close();
			fShaderFile.close();
			vertexCode = vShaderStream.str();
			fragmentCode = fShaderStream.str();
		}
		catch (std::ifstream::failure e)
		{
			std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl;
		}
		const char* vShaderCode = vertexCode.c_str();
		const char* fShaderCode = fragmentCode.c_str();

		// Complie shaders
		unsigned int vertex, fragment;

		// vertex shader
		vertex = glCreateShader(GL_VERTEX_SHADER);
		glShaderSource(vertex, 1, &vShaderCode, NULL);
		glCompileShader(vertex);
		checkCompileErrors(vertex, "VERTEX");
		// fragment shader
		fragment = glCreateShader(GL_FRAGMENT_SHADER);
		glShaderSource(fragment, 1, &fShaderCode, NULL);
		glCompileShader(fragment);
		checkCompileErrors(fragment, "VERTEX");
		// shader program
		ID = glCreateProgram();
		glAttachShader(ID, vertex);
		glAttachShader(ID, fragment);
		glLinkProgram(ID);
		checkCompileErrors(ID, "PROGRAM");
		// delete
		glDeleteShader(vertex);
		glDeleteShader(fragment);
	}

	void use()
	{
		glUseProgram(ID);
	}

	void setBool(const std::string &name, bool value) const
	{
		glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value);
	}

	void setInt(const std::string& name, int value) const
	{
		glUniform1i(glGetUniformLocation(ID, name.c_str()), value);
	}

	void setFloat(const std::string& name, float value) const
	{
		glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
	}

private:
	void checkCompileErrors(unsigned int shader, std::string type)
	{
		int success;
		char infoLog[1024];
		if (type != "PROGRAM")
		{
			glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
			if (!success) {
				glGetShaderInfoLog(shader, 1024, NULL, infoLog);
				std::cout << "ERROR::SHADER_COMPILATION_ERROR of type: " << type << "\n" << infoLog << std::endl;
			}
		}
		else {
			glGetShaderiv(shader, GL_LINK_STATUS, &success);
			if (!success) {
				glGetShaderInfoLog(shader, 1024, NULL, infoLog);
				std::cout << "ERROR::PROGRAM_LINKING_ERROR of type: " << type << "\n" << infoLog << std::endl;
			}
		}
	}
};

#endif

정점 속성 추가하기

 정점 속성은 위치 뿐만 아니라 색상, 텍스쳐 좌표, 법선 벡터 등 다양한 속성을 가질 수 있다. 아래의 경우는 정점 데이터를 정의할 때, 각 정점에 위치 정보와 함께 색상 정보를 추가한다.

float vertices[] = {
	// positions			// colors
	-0.5f, -0.5f, 0.0f,		1.0f, 0.0f, 0.0f,	// red
	 0.5f, -0.5f, 0.0f,		0.0f, 1.0f, 0.0f,	// green
 	 0.0f,  0.5f, 0.0f,		0.0f, 0.0f, 1.0f	// blue
};

지난 글에서 정점 데이터를 처리하기 위해서 OpenGL에게 해석하는 방법을 지정해 주었다. 정점 속성 하나가 추가되었으므로, 그에 따른 해석 방법도 달라져야 한다.

위의 도식은 정의한 vertices 정점 데이터에 나열된 데이터와 일치한다.  각 정점에 대해 세 개의 x, y, z 정보와 r, g, b 정보를 묶어서 처리할 수 있도록 하고 싶으므로, 다음과 같이 코드를 수정한다. 주의해야 할 점은 위치 속성과 색상 속성이 데이터를 읽기 시작하는 오프셋이 서로 다르다는 점이다.

/*
// 단일 속성
// position attribute
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
*/
    
// position attribute
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// color attribute
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);

 

각 정점 속성이 3개의 float 타입 데이터를 가지므로, stride는 24bytes, offset은 위치 속성의 경우 0, 색상 속성의 경우 12 bytes의 위치에서 처음으로 데이터를 읽도록 한다.

 

실행 결과는 다음과 같다.

 

각 정점에 대한 색상 정보만 제공했지만, 출력 결과를 통해 삼각형을 이루는 모든 픽셀의 색상이 결정된 것을 확인할 수 있다. 실제로 화면에 출력을 위해 삼각형에는 수많은 프래그먼트가 존재하고 프레그먼트 셰이더는 정점 색상 간의 선형 조합을 하는 프래그먼트 보간을 통해 다른 조각의 색상을 결정한다.


이전 학기 수강한 컴퓨터 그래픽스에서는 셰이더 구현까지는 진행하지 않았기 때문에, 비록 간단한 셰이더지만 직접 구현해 봄으로써 GLSL 언어 사용 방법 및 셰이더 입출력 과정에 대해 이해하게 된 것 같다.

'컴퓨터그래픽스' 카테고리의 다른 글

[OpenGL 공부] Transformations  (0) 2024.01.20
[OpenGL 공부] Textures (exercises)  (0) 2024.01.16
[OpenGL 공부] Textures (2)  (1) 2024.01.15
[OpenGL 공부] Textures (1)  (1) 2024.01.13
[OpenGL 공부] Hello Triangle  (1) 2024.01.06