상세 컨텐츠

본문 제목

SCOP : 기능별 클래스로 리팩토링하기 - (6)

Computer Graphics/SCOP

by Banjosh 2024. 9. 23. 17:21

본문

이번 작업에서는 사각형에 텍스처를 입히는 과정에서 context.cpp에 있는 함수들이 너무 커지는 문제를 해결하기 위해 리팩토링을 진행했다.


리팩토링 전

 처음 구현만을 위해 기능을 계속 추가하다 보니 context.cpp의 함수들이 지나치게 커져버렸다. 아래 코드는 리팩토링 전의 context.cpp 파일이다. 코드가 매우 길기 때문에, 그저 길이를 확인하는 정도로만 보면 된다.

 코드가 지나치게 길어져서 관리가 어려워지고, 유지보수가 힘든 구조였다. 특히 VAO, VBO, Shader 생성 등 반복되는 작업이 많았고, 텍스처 로딩과 셰이더 컴파일 등도 하나의 함수에 모두 담겨 있어 가독성이 매우 떨어졌다.

// 리팩토링 전!!
#include "../include/context.h"

void Context::Reshape(int width, int height) {
	m_width = width;
	m_height = height;
	glViewport(0, 0, m_width, m_height);
}

std::unique_ptr<Context> Context::Create() {
	std::unique_ptr<Context> context(new Context());
	if (!context->init()) return nullptr;
	return context;
}

bool Context::init() {

	float vertices[] = { 
		0.5f, 0.5f, 0.0f, 1.0f, 1.0f,
		0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
		-0.5f, -0.5f, 0.0f, 0.0f, 0.0f,
		-0.5f, 0.5f, 0.0f, 0.0f, 1.0f
	};

	uint32_t indices[] = {
		0, 3, 1,
		1, 3, 2,
	};

	glGenVertexArrays(1, &m_vao);
	glBindVertexArray(m_vao);

	glGenBuffers(1, &m_vbo);
	glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
	glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 20, vertices,  GL_STATIC_DRAW);

	glEnableVertexAttribArray(0);
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 5, 0);

	glEnableVertexAttribArray(1);
	glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 5, (const void*)(sizeof(float) * 3));

	glGenBuffers(1, &m_ebo);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(uint32_t) * 6, indices,  GL_STATIC_DRAW);

	std::optional<std::string> loadVertexShaderFileResult = glload::loadShaderFile("./shader/simple.vs");
	std::optional<std::string> loadFragmentShaderFileResult = glload::loadShaderFile("./shader/simple.fs");
	
	if (!loadVertexShaderFileResult.has_value() || !loadFragmentShaderFileResult.has_value()) {
		return false;
	}

	std::string vsCode = loadVertexShaderFileResult.value();
	std::string fsCode = loadFragmentShaderFileResult.value();

	int32_t vsCodeLen = vsCode.length();
	int32_t fsCodeLen = fsCode.length();

	const char* vsCodePtr = vsCode.c_str();
	const char* fsCodePtr = fsCode.c_str();

	m_vertexShader = glCreateShader(GL_VERTEX_SHADER);	
	m_fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);

	glShaderSource(m_vertexShader, 1, &vsCodePtr, &vsCodeLen);
	glShaderSource(m_fragmentShader, 1, &fsCodePtr, &fsCodeLen);

	glCompileShader(m_vertexShader);

	int success = 0;

	glGetShaderiv(m_vertexShader, GL_COMPILE_STATUS, &success);
	
	if (!success) {
		char infoLog[1024];
		glGetShaderInfoLog(m_vertexShader, 1024, nullptr, infoLog);
		std::cout << "failed to compile vertex shader" << std::endl;
		std::cout << "reason: " << infoLog << std::endl;
		return false;
	}
	
	glCompileShader(m_fragmentShader);

	success = 0;

	glGetShaderiv(m_fragmentShader, GL_COMPILE_STATUS, &success);
		
	if (!success) {
		char infoLog[1024];
		glGetShaderInfoLog(m_fragmentShader, 1024, nullptr, infoLog);
		std::cout << "failed to compile fragment shader" << std::endl;
		std::cout << "reason: " << infoLog << std::endl;
		return false;
	}

	m_program = glCreateProgram();

	glAttachShader(m_program, m_vertexShader);
	glAttachShader(m_program, m_fragmentShader);
	glLinkProgram(m_program);

	
	success = 0;
	glGetProgramiv(m_program, GL_LINK_STATUS, &success);
	if (!success)
	{
		char infoLog[1024];
		glGetProgramInfoLog(m_program, 1024, nullptr, infoLog);
		std::cout << "failed to link program: " << infoLog << std::endl;
		return false;
	}

	m_image = glload::loadBmpImg("./image/sample.bmp", &m_texWidth, &m_texHeight, &m_texChannelCount);

	glGenTextures(1, &m_texture);
	glBindTexture(GL_TEXTURE_2D, m_texture);

	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

	GLenum format = GL_RGBA;
	switch (m_texChannelCount) {
		default: break;
		case 2: format = GL_RG; break;
		case 3: format = GL_BGR; break;
	}

	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_texWidth, m_texHeight,
					0, format, GL_UNSIGNED_BYTE, m_image.get());

	glGenerateMipmap(GL_TEXTURE_2D);

	glUseProgram(m_program);

	
	GLuint location = glGetUniformLocation(m_program, "tex");
	glUniform1i(location, 0);

	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, m_texture);

	return true;
}

void Context::Render() {
	glClearColor(0.1f, 0.2f, 0.3f, 0.0f);
	glClear(GL_COLOR_BUFFER_BIT);

	glmath::mat4 model(1.0f);
	glmath::mat4 view = glmath::lookAt(glmath::vec3(0.0f, 0.0f, -3.0f), glmath::vec3(0.0f, 0.0f, 0.0f), glmath::vec3(0.0f, 1.0f, 0.0f));
	glmath::mat4 projection = glmath::perspective(glmath::radians(45.0f), (float)m_width / (float)m_height , 0.01f, 10.0f);
	glmath::mat4 transform = projection * view * model;

	GLuint location = glGetUniformLocation(m_program, "transform");
	glUniformMatrix4fv(location, 1, GL_FALSE, glmath::value_ptr(transform));
	
	glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
}

Context::~Context() {
	if (m_vao) {
		glDeleteVertexArrays(1, &m_vao);
	}

	if (m_vbo) {
		glDeleteBuffers(1, &m_vbo);
	}

	if (m_ebo) {
		glDeleteBuffers(1, &m_ebo);
	}

	if (m_vertexShader) {
		glDeleteShader(m_vertexShader);
	}

	if (m_fragmentShader) {
		glDeleteShader(m_fragmentShader);
	}
	
	if (m_program) {
		glDeleteProgram(m_program);
	}
	
	if (m_texture) {
		glDeleteTextures(1, &m_texture);
	}
}

 

 

리팩토링 후

이 문제를 해결하기 위해 여러 클래스로 기능을 나눠 리팩토링을 진행했다. 이번에도 참고한 자료는 권지용 교수님의 강의였다. 클래스별로 기능을 분리하고, 코드 길이를 대폭 줄였다.

 

// 리팩토링 후!
#include "../include/context.h"

void Context::Reshape(int width, int height) {
	m_width = width;
	m_height = height;
	glViewport(0, 0, m_width, m_height);
}

std::unique_ptr<Context> Context::create() {
	std::unique_ptr<Context> context(new Context());
	if (!context->init()) return nullptr;
	return context;
}

bool Context::init() {
	
	m_plane = Mesh::createPlane();
	m_program = Program::create("./shader/simple.vs", "./shader/simple.fs");

	if (!m_program) {
		return false;
	}

	m_image = Image::create("./image/sample.bmp");
	m_texture = Texture::create(m_image);

	return true;
}

void Context::Render() {

	glClearColor(0.1f, 0.2f, 0.3f, 0.0f);
	glClear(GL_COLOR_BUFFER_BIT);

	glmath::mat4 model = glmath::scale(glmath::mat4(1.0f), glmath::vec3(2.0f));
	glmath::mat4 view = glmath::lookAt(glmath::vec3(0.0f, 0.0f, -3.0f), glmath::vec3(0.0f, 0.0f, 0.0f), glmath::vec3(0.0f, 1.0f, 0.0f));
	glmath::mat4 projection = glmath::perspective(glmath::radians(45.0f), (float)m_width / (float)m_height , 0.01f, 10.0f);
	glmath::mat4 transform = projection * view * model;

	m_program->useProgram();
	m_program->setUniform("tex", 0);
	m_program->setUniform("transform", transform);
	m_texture->activeTexture(GL_TEXTURE0);
	
	m_plane->draw();
}

 

클래스별 분리 작업

리팩토링을 통해 아래와 같은 클래스들을 도입했다:

  1. Mesh 클래스 (+VertexArray, Buffer 클래스)
    Mesh 클래스는 내부적으로 VertexArrayBuffer 객체를 관리한다.
    VertexArrayBuffer 객체를 이용하여 plane mesh를 생성하고 그리는 기능을 제공한다.  
    • VertexArray: VAO와 관련된 작업을 수행
    • Buffer: VBO, EBO 등 버퍼 작업을 처리
  2. Program 클래스 (+Shader 클래스)
    Program 클래스는 Shader 객체를 관리한다.
    Shader 객체들을 컴파일한 후 프로그램에 링크하고, 유니폼 변수를 설정하며, 바인딩을 처리한다.
    • Shader: vertex shader, fragment shader 등을 다루며, 컴파일 및 프로그램에 attach하는 작업을 처리한다.
  3. Image 클래스
    Image 클래스는 BMP 파일을 파싱하여 데이터를 저장하는 기능을 제공한다.
    BMP 이미지를 로드하고, 데이터를 GPU에 저장할 수 있는 기능도 포함된다.

  4. Texture 클래스
    Texture 클래스는 Image 객체로부터 텍스처를 생성하고, 텍스처에 필요한 파라미터를 설정한 후, GPU에 저장하는 기능을 제공한다. 또한 텍스처 유닛 활성화도 처리한다.

리팩토링의 효과

리팩토링을 통해 코드를 기능별로 나누어 모듈화함으로써, 유지보수성을 크게 개선할 수 있었다. 특히, 캡슐화를 통해 각 클래스가 자신의 역할에 집중하게 만들었고, get() 함수를 사용하지 않음으로써 private 변수에 대한 접근을 제한했다.

결과적으로 코드가 훨씬 깔끔해졌고, 렌더링 로직을 쉽게 이해할 수 있는 구조로 개선했다.


다음 작업

이제 리팩토링이 완료되었으니, 다음 단계로는 .obj 파일 파싱을 진행해 3D 객체를 화면에 렌더링해볼 계획이다.

관련글 더보기