[OpenGL] UI

gest·6일 전

OpenGL

목록 보기
7/11

이번에는 UI를 그릴거다.


아이콘 세팅 및 VBO/VAO 생성

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 함수를 호출하면 화면에 그려질 것이다.


shader

UI를 그릴 때는 원근감이 필요 없기 때문에, 원근 투영(Perspective) 대신 직교 투영(Orthographic)을 사용한다.

vs

#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;
}

fs

#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);
}

Draw

여기서 가장 중요한 건 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 디자인이 메탈슬러그스럽다.

0개의 댓글