Scop 구현기 3편: 셰이더 클래스를 만들기로 했다

2025. 4. 11. 17:51·Dev/Grapics

OpenGL 셰이더를 매번 glCreateShader → glCompileShader → glLinkProgram 반복하는 게
너무 지저분하고 비효율적이라는 생각이 들었다.
심지어 하나만 있을 땐 모르겠는데, 앞으로 이게 10개, 20개 넘어가면… 말 다했지 뭐.

그래서 아예 셰이더 클래스를 하나 짜기로 했다.


헤더부터 깔끔하게 작성했다.

class Shader {
public:
	Shader(const char* vsPath, const char* fsPath);
	~Shader();

	void Use();
	GLuint GetId();

	void SetBool(const std::string& name, bool value) const;
	void SetInt(const std::string& name, int value) const;
	void SetFloat(const std::string& name, float value) const;
	void SetMat4(const std::string& name, const float* value) const;

private:
	GLuint mProgramId;
	std::unordered_map<std::string, GLint> mUniformLocations;

	GLuint CompileShader(const char* vsPath, const char* fsPath);
	GLuint AddShader(const char* shaderCode, GLenum shaderType);
	std::string ReadFile(const char* filePath);
	void LoadActiveUniforms();
};

 

초기 목표는 심플했다

  • .gsls 파일을 외부에서 읽어오고
  • 컴파일/링크하고
  • Use()로 Activate
  • Uniform도 Set 가능하게

그리고 무엇보다 main에서 glsl의 코드가 안 보이게!


Shader 클래스 구조 요약

1. 생성자 & 소멸자

Shader::Shader(const char* vsPath, const char* fsPath)
{
	mProgramId = CompileShader(vsPath, fsPath);
	LoadActiveUniforms();
}
  • 파일을 읽고
  • CompileShader()를 호출해 프로그램 생성
  • LoadAcriveUniforms()로 유니폼 목록을 미리 로딩
Shader::~Shader()
{
	if (mProgramId != 0)
		glDeleteProgram(mProgramId);
}
  • 프로그램 ID가 유효하면 glDeleteProgram 으로 정리

2. 셰이더 컴파일 및 추가

CompileShader

  • ReadFile에서 읽어온 소스 파일 내용을 토대로 ShaderProgram을 만든뒤 ShaderProgram Id를 리턴하는 함수
GLuint	Shader::CompileShader(const char* vsPath, const char* fsPath)
{
	std::string vsCode = ReadFile(vsPath);
	std::string fsCode = ReadFile(fsPath);

	if (vsCode.empty() || fsCode.empty())
	{
		std::cerr << "Error: Failed to load shader file code." << std::endl;
		return (0);
	}

	GLuint vs = AddShader(vsCode.c_str(), GL_VERTEX_SHADER);
	GLuint fs = AddShader(fsCode.c_str(), GL_FRAGMENT_SHADER);
	if (!vs || !fs)
	{
		return (0);
	}
	GLuint ShaderProgram = glCreateProgram();
	glAttachShader(ShaderProgram, vs);
	glAttachShader(ShaderProgram, fs);
	glLinkProgram(ShaderProgram);

	GLint success;
	GLchar infoLog[1024];
	glGetProgramiv(ShaderProgram, GL_LINK_STATUS, &success);
	if (!success)
	{
		glGetProgramInfoLog(ShaderProgram, sizeof(infoLog), NULL, infoLog);
		std::cerr << "Error linking shader program: " << infoLog << std::endl;
		return (0);
	}

	glDeleteShader(vs);
	glDeleteShader(fs);

	return (ShaderProgram);
}

3. 셰이더 사용

void Shader::Use()
{
	if (mProgramId != 0)
		glUseProgram(mProgramId);
}

 

OpenGL은 명시적으로 glUseProgram()을 호출해야 현재 셰이더가 GPU 파이프라인에서 활성화된다.
이 함수를 안 호출하면 아무것도 그려지지 않는다. 그런데 이걸 매번 glUseProgram(shaderProgram);으로 쓰면 헷갈린다.
클래스 내부에 넣어두면 깔끔하게 shader.Use(); 한 줄로 끝난다. 의외로 이 한 줄이 코드 가독성을 굉장히 올려준다.
게다가 유지보수 시에 실수도 줄일 수 있다.


4. Uniform 자동 로딩

void Shader::LoadActiveUniforms()
{
	GLint count;
	glGetProgramiv(mProgramId, GL_ACTIVE_UNIFORMS, &count);

	for (int i = 0; i < count; ++i)
	{
		GLchar	name[256];
		GLsizei	length;
		GLint	size;
		GLenum	type;

		glGetActiveUniform(mProgramId, i, sizeof(name), &length, &size, &type, name);

		GLint loaction = glGetUniformLocation(mProgramId, name);
		mUniformLocations[name] = loaction;
		std::cout << "Uniforms Name: " << name << std::endl;
	}
}
  • OpenGL에서 GL_ACTIVE_UNIFORMS 값을 받아서
  • glGetActiveUniform()으로 현재 셰이더에 등록된 유니폼 이름 및 사이즈를 전부 가져온다.
    그리고 mUniformLocations 맵에 전부 캐싱해둔다!
  •  OpenGL은 현재 프로그램에 등록된 유니폼들을 조회할 수 있는 API를 제공한다.
    그걸 활용해서 glGetActiveUniform()으로 유니폼의 이름, 타입, 크기를 받아오고
    해당 이름의 glGetUniformLocation()도 미리 호출해서 캐시한다.
    이 과정을 통하면 런타임 중에 "유니폼 이름이 틀렸습니다" 같은 에러 없이
    처음 실행 시점에 모든 유니폼 존재 여부를 체크할 수 있게 된다.
    개발자 입장에선 이게 정말 심리적으로 안정감을 준다.
    어디서 안 보이면, 일단 이 함수 로그만 보면 된다.

5. 유니폼 전송 함수들 (SetX 계열)

void	Shader::SetBool(const std::string& name, bool value) const
{
	GLint location = GetLocation(name);
	if (location != -1)
		glUniform1i(location, (int)value);
	else
		std::cerr << "Not Found : " << name << " Uniform" << std::endl;
}

void	Shader::SetInt(const std::string& name, int value) const
{
	GLint location = GetLocation(name);
	if (location != -1)
		glUniform1i(location, value);
	else
		std::cerr << "Not Found : " << name << " Uniform" << std::endl;
}

void	Shader::SetFloat(const std::string& name, float value) const
{
	GLint location = GetLocation(name);
	if (location != -1)
		glUniform1f(location, value);
	else
		std::cerr << "Not Found : " << name << " Uniform" << std::endl;
}

void	Shader::SetVec2(const std::string& name, float x, float y) const
{
	GLint location = GetLocation(name);
	if (location != -1)
		glUniform2f(location, x, y);
	else
		std::cerr << "Not Found : " << name << " Uniform" << std::endl;
}

void	Shader::SetVec3(const std::string& name, float x, float y, float z) const
{
	GLint location = GetLocation(name);
	if (location != -1)
		glUniform3f(location, x, y, z);
	else
		std::cerr << "Not Found : " << name << " Uniform" << std::endl;
}

void	Shader::SetVec4(const std::string& name, float x, float y, float z, float w) const
{
	GLint location = GetLocation(name);
	if (location != -1)
		glUniform4f(location, x, y, z, w);
	else
		std::cerr << "Not Found : " << name << " Uniform" << std::endl;
}

void	Shader::SetMat3(const std::string& name, const float* value) const
{
	GLint location = GetLocation(name);
	if (location != -1)
		glUniformMatrix3fv(location, 1, GL_FALSE, value);
	else
		std::cerr << "Not Found : " << name << " Uniform" << std::endl;
}

void	Shader::SetMat4(const std::string& name, const float* value) const
{
	GLint location = GetLocation(name);
	if (location != -1)
		glUniformMatrix4fv(location, 1, GL_FALSE, value);
	else
		std::cerr << "Not Found : " << name << " Uniform" << std::endl;
}


OpenGL은 유니폼을 보낼 때마다 glGetUniformLocation()을 호출해야 한다.
이건 비용이 크고, 런타임 중 호출 시 실수도 많고 성능에도 좋지 않다.

그래서 나는 mUniformLocations라는 맵을 하나 만들고,
셰이더 초기화 시에 모든 유니폼 이름과 위치를 미리 다 저장해놨다.

그 덕분에 지금은 그냥 shader.SetVec3("uColor", 1.0f, 0.0f, 0.0f); 한 줄이면 끝난다.
이 구조가 되니까 uniform 작업이 너무 편해졌고,
코드도 안정적이고 예측 가능하게 바뀌었다


5. GLSL 타입 출력 함수

static const char* GetGLTypeString(GLenum type)
{
	switch (type)
    {
		case GL_FLOAT: return "float";
		case GL_FLOAT_VEC2: return "vec2";
		case GL_FLOAT_VEC3: return "vec3";
		case GL_FLOAT_VEC4: return "vec4";
		case GL_INT: return "int";
		case GL_INT_VEC2: return "ivec2";
		case GL_INT_VEC3: return "ivec3";
		case GL_INT_VEC4: return "ivec4";
		case GL_BOOL: return "bool";
		case GL_BOOL_VEC2: return "bvec2";
		case GL_BOOL_VEC3: return "bvec3";
		case GL_BOOL_VEC4: return "bvec4";
		case GL_FLOAT_MAT2: return "mat2";
		case GL_FLOAT_MAT3: return "mat3";
		case GL_FLOAT_MAT4: return "mat4";
		case GL_SAMPLER_2D: return "sampler2D";
		case GL_SAMPLER_CUBE: return "samplerCube";
		default: return "unknown";
	}
}

void Shader::PrintActiveUniforms() const
{
	GLint count;
	glGetProgramiv(mProgramId, GL_ACTIVE_UNIFORMS, &count);

	std::cout << "Active Uniforms in Shader Program " << ": " << mProgramId <<  std::endl;

	for (int i = 0; i < count; ++i) {
		GLchar name[256];
		GLsizei length;
		GLint size;
		GLenum type;
		glGetActiveUniform(mProgramId, i, sizeof(name), &length, &size, &type, name);
		std::cout << " - " << name << " (Type: " << GetGLTypeString(type) << ", Size: " << size << ")" << std::endl;
	}
}
  • 프린트할 때 vec3, mat4 같은 문자열로 출력해주는 유틸
  • 디버깅용 PrintActiveUniforms()에서 사용됨

구현하면서 느낀 점

✔️ 만들고 나니까 이런 점이 좋았다:

  • GLSL 파일만 바꿔도 바로 적용됨 → 실시간 개발 느낌
  • uniform 명칭 실수 줄어듦
  • 셰이더가 많아질수록 이 구조가 빛을 발함
  • PrintActiveUniforms()로 디버깅도 쉬워짐

❌ 만들면서 애먹은 점:

  • GLSL 읽기 실패했을 때 에러 로그 안 뜨는 것 → 로그 추가
  • uniform 이름이 바뀌었는데 캐시만 믿었다가 값이 안 들어감 → 리로딩 필요성 느낌

 📦 최종 사용 예시

Shader shader("shaders/basic.vert", "shaders/basic.frag");
shader.Use();
shader.SetVec3("uColor", 1.0f, 0.2f, 0.5f);
shader.SetMat4("uMVP", glm::value_ptr(mvp));

 

 

'Dev > Grapics' 카테고리의 다른 글

Scop 구현기 5편: 행렬과 사용 점  (0) 2025.04.17
Scop 구현기 4편: Model 클래스  (0) 2025.04.14
Scop 구현기 2편: VAO와 VBO의 세계  (0) 2025.04.09
Scop 구현기 1편: 윈도우 하나 띄우는 게 뭐라고 (Object Viewer)  (0) 2025.04.09
Rasterization vs Ray Tracing  (0) 2025.04.09
'Dev/Grapics' 카테고리의 다른 글
  • Scop 구현기 5편: 행렬과 사용 점
  • Scop 구현기 4편: Model 클래스
  • Scop 구현기 2편: VAO와 VBO의 세계
  • Scop 구현기 1편: 윈도우 하나 띄우는 게 뭐라고 (Object Viewer)
onepaperhoon
onepaperhoon
한장훈님의 블로그 입니다.
  • onepaperhoon
    OnePaperHoon Blog
    onepaperhoon
  • 전체
    오늘
    어제
    • 분류 전체보기 (14)
      • Dev (14)
        • System Programming (2)
        • Linux (0)
        • CS (0)
        • Network, Protocol (0)
        • Grapics (11)
        • Web, App (0)
        • Design Pattern (1)
      • Projects (0)
      • TIL (0)
      • Life (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • GitHub
  • 공지사항

  • 인기 글

  • 태그

    cpp
    shared memory
    OpenGL
    3d
    디자인 패턴
    c++
    IPC
    glfw
    싱글톤 패턴
    graphicapi
    Process
    공유 메모리
    그래픽스
    named pipe
    Pipe
    shader
    Game
    Vertex Shader
    C언어
    graphics
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
onepaperhoon
Scop 구현기 3편: 셰이더 클래스를 만들기로 했다
상단으로

티스토리툴바