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 |
