이번에는 UI를 그릴거다.
UI를 그리기 위해서는 먼저 2D 사각형 형태의 폴리곤이 필요하다.
크기는 셰이더에서 픽셀 단위로 조절할 것이기 때문에, 여기서는 가로세로 1짜리 단위 사각형(0~1)으로 정점(Vertex) 데이터를 만든다.
void HUD::init()
{
// 1) 셰이더 컴파일
shader = new Shader("src/vs/hud.vs", "src/fs/hud.fs");
// 2) 단위 사각형(0~1) VBO 생성
// pos와 uv가 같아야 함
float verts[] = {
// pos // uv
0.0f, 0.0f, 0.0f, 0.0f,
1.0f, 0.0f, 1.0f, 0.0f,
1.0f, 1.0f, 1.0f, 1.0f,
0.0f, 0.0f, 0.0f, 0.0f,
1.0f, 1.0f, 1.0f, 1.0f,
0.0f, 1.0f, 0.0f, 1.0f,
};
glGenVertexArrays(1, &vao);
glGenBuffers(1, &vbo);
glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW); //vbo
// location 0: pos (vec2)
glEnableVertexAttribArray(0); //vao 0번 온.
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0); //vbo
// location 1: uv (vec2)
glEnableVertexAttribArray(1); //vao 1번 온
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float))); //vbo
glBindVertexArray(0);
// 3) 아이콘 텍스처 로드 (PNG 알파 채널 포함 → Loader에서 RGBA 자동 감지)
Loader::loadTexture(foodIcon, "textures/food_icon.png");
}
이렇게 하나의 음식 아이콘 텍스처와 사각형 폴리곤을 준비해 둔다. 이제 원할 때 draw 함수를 호출하면 화면에 그려질 것이다.
UI를 그릴 때는 원근감이 필요 없기 때문에, 원근 투영(Perspective) 대신 직교 투영(Orthographic)을 사용한다.
#version 330 core
layout (location = 0) in vec2 aPos; // 단위 사각형 좌표 (0~1)
layout (location = 1) in vec2 aUV;
out vec2 TexCoord;
uniform mat4 projection; // ortho (픽셀 → NDC)
uniform vec2 offset; // 아이콘의 화면상 좌하단 픽셀 위치
uniform float iconSize; // 아이콘 한 변 픽셀 크기
void main()
{
vec2 screenPos = aPos * iconSize + offset;
gl_Position = projection * vec4(screenPos, 0.0, 1.0);
TexCoord = aUV;
}
#version 330 core
// HUD 아이콘 프래그먼트 셰이더.
// PNG 알파 채널을 그대로 사용해야 투명 배경이 살아남.
// 알파 블렌딩은 CPU 쪽에서 glEnable(GL_BLEND) + glBlendFunc로 켜둔다.
in vec2 TexCoord;
out vec4 FragColor;
uniform sampler2D iconTex;
void main()
{
FragColor = texture(iconTex, TexCoord);
}
여기서 가장 중요한 건 OpenGL의 상태(State) 변경이다.
UI는 3D 월드 위에 '덮어씌우는' 개념이기 때문에 깊이(Depth) 계산이 필요 없고, 배경이 투명해야 하므로 블렌딩(Blending)을 켜야 한다.
void HUD::draw()
{
if (!shader || vao == 0 || foodIcon == 0) return;
// === GL 상태 변경 ===
// HUD는 항상 화면 위에 보여야 하므로 깊이 테스트 끔.
// 알파 블렌딩 켜야 PNG 투명 배경이 살아남.
glDisable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
shader->use();
// ortho projection: 픽셀 좌표 → NDC
// left=0, right=SCR_WIDTH, bottom=0, top=SCR_HEIGHT
// → 좌하단이 (0,0). HUD 좌표 계산이 직관적.
glm::mat4 projection = glm::ortho(
0.0f, (float)SCR_WIDTH,
0.0f, (float)SCR_HEIGHT
);
shader->setMat4("projection", projection);
shader->setFloat("iconSize", iconSize);
// 텍스처 바인딩
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, foodIcon); //로드한 아이콘 넣기
shader->setInt("iconTex", 0);
// 우측 끝에서부터 거꾸로 배치:
// 가장 오른쪽 아이콘의 우측 x = SCR_WIDTH - marginX
// 각 아이콘은 (iconSize + spacing)만큼 왼쪽으로 이동
// i번째(0이 가장 오른쪽) 아이콘의 좌측 x = (SCR_WIDTH - marginX) - iconSize - i*(iconSize + spacing)
//
glBindVertexArray(vao); //여기서도 그릴 준비
//여기서 위치 세팅
for (int i = 0; i < food; ++i)
{
float x = (float)SCR_WIDTH - marginX - iconSize - i * (iconSize + spacing);
float y = marginY;
shader->setVec2("offset", x, y);
glDrawArrays(GL_TRIANGLES, 0, 6); //그리는 함수
}
glBindVertexArray(0);
// === GL 상태 복원 ===
glDisable(GL_BLEND);
glEnable(GL_DEPTH_TEST);
}
여기서 핵심 코드는
glDisable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
이거다.
glDisable(GL_DEPTH_TEST) : 이걸 안 끄면 3D 오브젝트 뒤로 UI가 파묻혀서 안 보이는 대참사가 일어난다. UI니까 깊이 버퍼를 무시하고 무조건 화면 맨 앞에 그리도록 설정하는 것이다.
glEnable(GL_BLEND) : 이걸 켜줘야 텍스처의 알파(투명도) 값을 인식해서 아이콘 주변의 투명한 여백이 배경을 가리지 않고 자연스럽게 렌더링된다.
그리고 UI를 다 그렸으면 다른 3D 모델을 그릴 때 영향이 가지 않도록, 반드시 전 상태로 복원해 줘야 한다.
// === GL 상태 복원 ===
glDisable(GL_BLEND);
glEnable(GL_DEPTH_TEST);
UI는 무조건 3D 오브젝트를 전부 다 그린 이후, 즉 프레임의 가장 마지막에 호출해야 한다.
//그리는 함수
void Rendering()
{
// shadow pass에서 viewport가 SHADOW_MAP_SIZE로 바뀌었으니 윈도우 크기로 복원
glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
// render
glClearColor(0.2f, 0.3f, 0.3f, 1.0f); //sky
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //glEnable(GL_DEPTH_TEST);를 추가하면 GL_DEPTH_BUFFER_BIT도 넣어라
for (int i = 0; i < objects.size(); i++)
{
objects[i]->drawGameObject(camera, lightColor, lightPos, lightSpaceMatrix);
}
// 디버그: 그림자가 생성되는 영역(라이트 frustum)을 노란 와이어 박스로 표시
//RenderShadowFrustumDebug();
//UI 라인
// HUD는 모든 3D 씬 위에 그려져야 하므로 swap 직전에 호출
if (hud)
{
hud->draw();
}
// glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
glfwSwapBuffers(window);
glfwPollEvents(); // 입력받은 callback 바로 실행
}

잘 된다. UI 디자인이 메탈슬러그스럽다.