개발/OpenGL / / 2022. 2. 17. 20:17

OpenGL 공부일지 - OpenGL Super Bible application 작성과 쉐이더 입문

반응형
※ OpenGL을 공부하면서 중요하다고 생각되는 부분이나 개인적으로 느끼는 점을 적는 공간이다
#include <sb6.h>

class simpleclear_app : public sb6::application
{
    void init()
    {
        static const char title[] = "OpenGL SuperBible - Simple Clear";

        sb6::application::init();

        memcpy(info.title, title, sizeof(title));
    }

    virtual void render(double currentTime)
    {
        static const GLfloat red[] = { 1.0f, 0.0f, 0.0f, 1.0f };
        glClearBufferfv(GL_COLOR, 0, red);
    }
};

DECLARE_MAIN(simpleclear_app)

화면을 색으로 채우는 코드이다. 여기서 glClearBufferfv라는 함수의 프로토타입은 이렇다.

void glClearBufferfv(GLenum buffer, GLint drawBuffer, const GLfloat * value);

OpenGL의 함수는 gl로 시작하여, 인자를 함수 이름끝에 접미사로 줄여쓰는 나름의 네이밍 컨벤션이 있다.

위 함수에서 fv는 floating point vector를 사용한다는 의미로 코드의 red배열이 이에 해당한다.

glClearBufferfv함수는 첫 번째 인자를 세 번째 인자의 값으로 지우는 명령이다. drawBuffer는 지울 출력 버퍼의 인덱스이다.

 

위 코드의 render함수를 변형해서 시간에 따라 색이변하는 코드로 작성해보자.

#include <sb6.h>
#include <math.h>

class simpleclear_app : public sb6::application
{
    void init()
    {
        static const char title[] = "OpenGL SuperBible - Simple Clear";

        sb6::application::init();

        memcpy(info.title, title, sizeof(title));
    }

    virtual void render(double currentTime)
    {
        static const GLfloat color[] = { (float)sin(currentTime) * 0.5f + 0.5f,
        (float)cos(currentTime) * 0.5f + 0.5f, 0.0f, 1.0f};
        glClearBufferfv(GL_COLOR, 0, color);
    }
};

DECLARE_MAIN(simpleclear_app);

render에는 currentTime이라는 시간의 초단위 인자가 전달된다. 이를 통해 애니메이션을 만들 수 있다.

그런데 실행해봤더니 애니메이션이 아니라 정적으로 색이 변하지 않는다.

 

#include <sb6.h>
#include <math.h>

class simpleclear_app : public sb6::application
{
    void init()
    {
        static const char title[] = "OpenGL SuperBible - Simple Clear";

        sb6::application::init();

        memcpy(info.title, title, sizeof(title));
    }

    virtual void render(double currentTime)
    {
        const GLfloat color[] = { (float)sin(currentTime) * 0.5f + 0.5f,
        (float)cos(currentTime) * 0.5f + 0.5f, 0.0f, 1.0f};
        glClearBufferfv(GL_COLOR, 0, color);
    }
};

DECLARE_MAIN(simpleclear_app);

문제였던 부분은 static이었다. static으로 배열을 선언했다면 render함수를 다시 실행하더라도 이전에 할당한 메모리가 남아있고, const로 선언하여 상수이므로 바꿀 수 없어 색이 변하지 않았다.

상당히 기본적인 C 문법인데 오랜만에 C언어로 공부를 하다보니 헷갈린 듯 하다.

 

쉐이더

OpenGL은 Shader라는 작은 프로그램들을 고정함수로 연결해서 작동한다고 한다. 이름만 많이 들어봤지 정확히 뭔지는 몰랐다. 쉐이더코드를 실행하면 그 입출력을 파이프라인으로 보내 그려지게 한다.

OpenGL 쉐이더는 GLSL라고 한다. 컴파일러는 OpenGL에 내장되어있다.

쉐이더 코드는 쉐이더 객체로 컴파일되고 이들이 모여 프로그램 객체로 뭉쳐진다.

 

#include <sb6.h>

GLuint compile_shaders(void)
{
	GLuint vertex_shader;
	GLuint fragment_shader;
	GLuint program;

	// Source code for vertex shader
	static const GLchar* vertex_shader_source[] =
	{
		"#version 430 core	\n"
		"					\n"
		"					\n"
		"void main(void)	\n"
		"{					\n"
		"	gl_Position = vec4(0.0, 0.0, 0.5, 1.0);	\n"
		"}					\n"
	};

	// Source code for fragment shader
	static const GLchar* fragment_shader_source[] =
	{
		"#version 430 core	\n"
		"	\n"
		"out vec4 color;	\n"
		"	\n"
		"void main(void)	\n"
		"{	\n"
		"	color = vec4(0.0, 0.8, 1.0, 1.0);	\n"
		"}	\n"
	};

	// Create and compile vertex shader
	vertex_shader = glCreateShader(GL_VERTEX_SHADER);
	glShaderSource(vertex_shader, 1, vertex_shader_source, NULL);
	glCompileShader(vertex_shader);
	
	// Create and compile fragment shader
	fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
	glShaderSource(fragment_shader, 1, fragment_shader_source, NULL);
	glCompileShader(fragment_shader);

	// Create program, attach shaders to it, and link it
	program = glCreateProgram();
	glAttachShader(program, vertex_shader);
	glAttachShader(program, fragment_shader);
	glLinkProgram(program);

	// Delete the shaders as the program has them now
	glDeleteShader(vertex_shader);
	glDeleteShader(fragment_shader);
	return program;
}

위의 함수들을 보자.

  • glCreateShader()
    빈 쉐이더 객체 생성. 소스 코드를 받아 컴파일해야함
  • glShaderSource()
    쉐이더 소스 코드를 쉐이더 객체로 전달하여 복사본 유지
  • glCompileShader()
    쉐이더 객체에 포함된 소스 코드를 컴파일
  • glCreateProgram()
    쉐이더 객체에 attach시킬 프로그램 객체 생성
  • glAttachShader()
    쉐이더 객체를 프로그램 객체에 attach
  • glLinkProgram()
    프로그램 객체에 attach된 모든 쉐이더 객체 링크
  • glDeleteShader()
    쉐이더 객체 삭제. 쉐이더가 프로그램 객체에 링크되면, 프로그램이 binary 코드를 저장하고 쉐이더는 필요없어짐

위 함수들의 과정을 거치고 나서 VAO(vertex array object)를 생성해야한다.

glGenVertexArrays()함수를 통해 VAO를 context에 attach하고 glBindVertexArray()를 호출하는 순서이다.

void glCreateVertexArrays(GLsizei n, GLuint * arrays);
void glBindVertexArray(GLuint array);

위의 쉐이더 초기화 코드들을 startup() 함수를 오버라이드 하여 넣는다. render와 같이 startup() 함수는 sb6::application에 가상함수로 정의되어 run함수로 인해 자동 호출된다.

 

이런.. 코드 참고용으로 7판과 번갈아가며 보고있었는데, 함수 이름이 그새 다르다.

6판에서는 glGenVertexArrays()함수가 7판에서는 glCreateVertexArrays()으로 바뀌었다. 그래도 의미는 전달되니 크게 힘들지는 않을 것 같다.

 

예제에 제각각 코드가 분할되어 있기 때문에 잘 조합해야한다.

// eg2-5.cpp

GLuint compile_shaders(void)
{
	GLuint vertex_shader;
	GLuint fragment_shader;
	GLuint program;

	// Source code for vertex shader
	static const GLchar* vertex_shader_source[] =
	{
		"#version 430 core	\n"
		"					\n"
		"					\n"
		"void main(void)	\n"
		"{					\n"
		"	gl_Position = vec4(0.0, 0.0, 0.5, 1.0);	\n"
		"}					\n"
	};

	// Source code for fragment shader
	static const GLchar* fragment_shader_source[] =
	{
		"#version 430 core	\n"
		"	\n"
		"out vec4 color;	\n"
		"	\n"
		"void main(void)	\n"
		"{	\n"
		"	color = vec4(0.0, 0.8, 1.0, 1.0);	\n"
		"}	\n"
	};

	// Create and compile vertex shader
	vertex_shader = glCreateShader(GL_VERTEX_SHADER);
	glShaderSource(vertex_shader, 1, vertex_shader_source, NULL);
	glCompileShader(vertex_shader);

	// Create and compile fragment shader
	fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
	glShaderSource(fragment_shader, 1, fragment_shader_source, NULL);
	glCompileShader(fragment_shader);

	// Create program, attach shaders to it, and link it
	program = glCreateProgram();
	glAttachShader(program, vertex_shader);
	glAttachShader(program, fragment_shader);
	glLinkProgram(program);

	// Delete the shaders as the program has them now
	glDeleteShader(vertex_shader);
	glDeleteShader(fragment_shader);
	return program;
};

순서에 유의하자.

vertex shader와 fragment shader 객체 부분 코드를 보자.

둘 다 glCreateShader로 shader 객체를 생성하고, glShaderSource를 통해 shader code를 shader object로 복사한다.

glCompileShader로 shader를 컴파일한다.

 

program 객체 부분을 보자.

glCreateProgram으로 program object를 생성하고, glAttachShader로 shader object를 program object에 attach한다.

그후 glLinkProgram으로 program 객체에서 attach된 모든 shader object를 link한다.

 

위 compile_shaders method에서는 마지막에 glDeleteShader로 shader object를 삭제하고, program object를 반환한다.

// eg2-6.cpp

#include <sb6.h>
#include "eg2-5.cpp"

class simpleclear_app : public sb6::application
{
public:

	void init()
	{
		static const char title[] = "OpenGL SuperBible - render point";

		sb6::application::init();

		memcpy(info.title, title, sizeof(title));
	}

	void startup()
	{
		rendering_program = compile_shaders();
		glGenVertexArrays(1, &vertex_array_object);
		glBindVertexArray(vertex_array_object);
	}

	virtual void render(double currentTime)
	{
		static const GLfloat red[] = { 1.0f, 0.0f, 0.0f, 1.0f };
		glClearBufferfv(GL_COLOR, 0, red);

		glUseProgram(program);

		glPointSize(40.0f);

		glDrawArrays(GL_POINTS, 0, 1);
	}

	void shutdown()
	{
		glDeleteVertexArrays(1, &vertex_array_object);
		glDeleteProgram(rendering_program);
	}

private :
	GLuint vertex_shader;
	GLuint fragment_shader;
	GLuint program;

	GLuint rendering_program;
	GLuint vertex_array_object;
};

DECLARE_MAIN(simpleclear_app);

startup부분을 보자.

compile_shaders를 호출하여 프로그램 객체(program)를 받아온다.

glGenVertexArrays로 VAO를 생성하고, glBindVertexArray로 VAO를 binding하여 사용하겠다는 선언을 한다.

 

render부분을 보자.

위에서 다루었던 ClearBufferfv를 통해 특정 모양으로 buffer를 clear한다.

glUseProgram으로 program object를 사용하도록 설정하고, glPointSize로 점의 크기를 키운다(이 부분이 없으면 점이 겁나 작아서 안보인다..).

void glPointSize(GLfloat size);

 

glDrawArrays을 통해 VAO를 파이프라인에 보내 점을 렌더링 할 수 있게 만든다.

void glDrawArrays(GLenum mode, GLint first, GLsizei count);

mode는 그래픽스 primitive를 결정하는 것인데 현재는 GL_POINTS를 사용해서 하나의 점을 그린다. 여기서는 first와 count를 0, 1로 설정했는데, first의 경우 현재 필요하지 않아 0으로 설정하고, count는 렌더링하는 버텍스의 개수이다. 한점이기에 1로 설정한 것이다.

 

이렇게 잘 실행할 수 있다.

(github 소스코드에서 해당 예제는 singlepoint project이다)

삼각형

위 정보들을 이용해서 삼각형을 그려보자.

glDrawArrays에서 GL_POINTS가 아니라 GL_LINES 혹은 GL_TRIANGLES 등으로 전달하여 그릴 수 있는데, 이때 버텍스가 동일한 위치에 있게되면 primitive가 취소되어 출력되지 않는다. 선이나 삼각형 등의 경우 각각의 vertex가 겹치지 않도록 해야한다.

GLSL에서는 gl_VertexID라는 입력으로 vertex의 인덱스를 정할 수 있다.

 

// eg2-8.cpp

GLuint compile_shaders(void)
{
	GLuint vertex_shader;
	GLuint fragment_shader;
	GLuint program;

	// Source code for vertex shader
	static const GLchar* vertex_shader_source[] =
	{
		"#version 430 core	\n"
		"					\n"
		"					\n"
		"void main(void)	\n"
		"{					\n"
		"	// Declare a hard-coded array of positions						\n"
		"	const vec4 vertices[3] = vec4[3](vec4(0.25, -0.25, 0.5, 1.0),	\n"
		"	vec4(-0.25, -0.25, 0.5, 1.0),									\n"
		"	vec4(0.25, 0.25, 0.5, 1.0));									\n"
		"	// Index into our array using gl_VertexID						\n"
		"	gl_Position = vertices[gl_VertexID];							\n"
		"}					\n"
	};

	// Source code for fragment shader
	static const GLchar* fragment_shader_source[] =
	{
		"#version 430 core	\n"
		"	\n"
		"out vec4 color;	\n"
		"	\n"
		"void main(void)	\n"
		"{	\n"
		"	color = vec4(0.0, 0.8, 1.0, 1.0);	\n"
		"}	\n"
	};

	// Create and compile vertex shader
	vertex_shader = glCreateShader(GL_VERTEX_SHADER);
	glShaderSource(vertex_shader, 1, vertex_shader_source, NULL);
	glCompileShader(vertex_shader);

	// Create and compile fragment shader
	fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
	glShaderSource(fragment_shader, 1, fragment_shader_source, NULL);
	glCompileShader(fragment_shader);

	// Create program, attach shaders to it, and link it
	program = glCreateProgram();
	glAttachShader(program, vertex_shader);
	glAttachShader(program, fragment_shader);
	glLinkProgram(program);

	// Delete the shaders as the program has them now
	glDeleteShader(vertex_shader);
	glDeleteShader(fragment_shader);
	return program;
};
// eg2-9.cpp

#include <sb6.h>
#include "eg2-8.cpp"

class simpleclear_app : public sb6::application
{
public:

	void init()
	{
		static const char title[] = "OpenGL SuperBible - render triangle";

		sb6::application::init();

		memcpy(info.title, title, sizeof(title));
	}

	void startup()
	{
		program = compile_shaders();
		glGenVertexArrays(1, &vertex_array_object);
		glBindVertexArray(vertex_array_object);
	}

	virtual void render(double currentTime)
	{
		static const GLfloat red[] = { 1.0f, 0.0f, 0.0f, 1.0f };
		glClearBufferfv(GL_COLOR, 0, red);

		glUseProgram(program);

		glPointSize(40.0f);

		glDrawArrays(GL_TRIANGLES, 0, 3);
	}

	void shutdown()
	{
		glDeleteVertexArrays(1, &vertex_array_object);
		glDeleteProgram(program);
	}

private:
	GLuint vertex_shader;
	GLuint fragment_shader;
	GLuint program;

	GLuint vertex_array_object;
};

DECLARE_MAIN(simpleclear_app);

점을 찍었던 코드에서 vertex shader코드와 glDrawArrays의 mode 및 vertex count를 수정하면 삼각형을 그릴 수 있다.

github 소스코드에서 해당 예제는 singletri project이다. 

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유