컴퓨터 그래픽스/OpenGL

#2 gulender 개발일지 : GUI 개발

san10 2024. 5. 1. 19:27

저번 글에서 버텍스를 수정하는 기능을 만들었다.

앞으로도 여러 기능이 추가될텐데..

기능이 추가되기 위해서는 입력을 컨트롤 할 수 있어야 한다.

그래서 기능을 더 추가하기 전에, 먼저 GUI를 만들었다.

결과

 

외부 라이브러리

처음에는 외부 라이브러리를 활용하려고 했었다.

https://github.com/ocornut/imgui

 

GitHub - ocornut/imgui: Dear ImGui: Bloat-free Graphical User interface for C++ with minimal dependencies

Dear ImGui: Bloat-free Graphical User interface for C++ with minimal dependencies - ocornut/imgui

github.com

https://github.com/wjakob/nanogui

 

GitHub - wjakob/nanogui: Minimalistic GUI library for OpenGL

Minimalistic GUI library for OpenGL. Contribute to wjakob/nanogui development by creating an account on GitHub.

github.com

그런데 코드 보고 느낀게, 일단 커스텀이 힘들 것 같았고

이거 코드 분석하면서 적용할 바엔 그냥 내가 개발하는게 더 빠를 것 같았다..

그래서 그냥 직접 만들었다.

 

Canvas

원래는 UI마다 입력을 컨트롤하고 draw하려고 했었는데

그냥 한 곳에서 UI를 모두 관리하는게 훨씬 나을 것 같았다.

(왜 유니티에서 canvas 아래에만 UI를 만들 수 있는지 약간 이해했다.)

 

그래서 UI들을 전부 관리해주는 canvas 라는 클래스를 만들었다.

canvas에서 모든 입력을 관리하고, UI들을 렌더링 한다.

class Canvas : public IPressedUp{
    public:
        void Rendering();
        void AddWidget(Widget* w);
        void OnPointerUp(float xpos, float ypos) override;

    private:
        std::vector<Widget*> mChild;
};

 

Rendering은 자신이 가지고 있는 UI들의 visible을 체크하여 draw를 호출하는 함수이다.

void Canvas::Rendering(){
    for(auto const& child : mChild){
        if(child->GetVisible()){
            child->Draw();
        }
    }
}

 

 

Widget

모든 UI는 Widget이라는 추상클래스를 상속받는다.

class Widget{
    public:
        virtual ~Widget();
        virtual void Draw()=0;

		bool GetVisible() const {return bVisible;}
        void SetbuttonCallback(std::function<void(double xpos, double ypos)> callback);
        std::function<void(double xpos, double ypos)> getButtonCallback() const {return mbuttonCallback;}
        
        glm::vec3 GetPos() const {return mPos;}
        glm::vec2 GetSize() const {return glm::vec2(mSizeX,mSizeY);}
        void Callbtn();


    protected:
        bool mPushed=false;
        glm::vec3 mPos;
        float mSizeX;
        float mSizeY;
        bool bVisible=true;
        std::function<void(double xpos, double ypos)> mbuttonCallback = NULL;

};

(내가 생각하기에) UI가 꼭 가져야 할 요소들을 가진다.

Widget이라는 추상 클래스를 만든 이유는, UI들을 canvas같은 곳에서 묶어서 관리하고 싶기 때문에 만들었다.

 

Button

class Button:public Widget{
    public:
        Button(glm::vec3 buttonPos,float sizeX, float sizeY, const char *texPath, eImageType imageType);
        void Draw() override;
        
        bool GetPushed() const {return mPushed;}
        void Pushed();
        void SetTexture(const char *texPath,eImageType imageType);
        

    private:
        Shader UIShader;
        unsigned int mVBO;
        unsigned int mVAO;
        unsigned int mEBO;
        unsigned int mTexture;
        int mWidth, mHeight, mMinimaps;
        float mVertexArray[20];
        glm::vec4 mColor= glm::vec4(1.0f,1.0f,1.0f,1.0f);
        unsigned int mIndices[6]={
            0,1,2,
            2,1,3
        };
};

가장 기본적인 UI중 하나인 버튼을 구현했다.

 

버튼 위치와 가로세로길이를 -1~1 사이의 정규화된 값으로 받고(크기는 0~2)

그걸 통해 버텍스의 위치를 구해서 vbo를 구하고 UI를 그린다.

 

void Button::Draw(){
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D,mTexture);
    UIShader.use();
    glBindVertexArray(mVAO);
    glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_INT,0);
}

Draw()도 별건 없고

그냥 텍스쳐를 활성화 시켜주고 그려준다.

 

버튼 테스트를 위해..

테스트용 버튼을 만들었다

Button* btn= new Button(glm::vec3(-0.3f,0.5f,0.0f),0.2f,0.2f,"resource/dotIcon.png");
canvas->AddWidget(std::move(btn));

사진은 아무거나 가져왔습니다..

와! 버튼!

 

 

InputSystem

사실 UI를 그리기만 해서는 안되고

입력도 받아서 처리해야 한다.

 

원래는 canvas에 콜백달아서 입력을 처리하려고 했는데..

입력이 필요한게 UI이 뿐만아니라 많은 곳에서 필요할 것 같아서

입력을 받고 처리해주는 InputEventSystem 클래스를 만들었다.

 

그리고 입력이 필요한 클래스들을 InputEventSystem에 등록하기 위해

인터페이스를 작성했다.

class IPressedDown{
    public:
        virtual ~IPressedDown() {};
        virtual void OnPointerDown(float xpos, float ypos) =0;
};
class IPressed{
    public:
        virtual ~IPressed() {};
        virtual void OnPointer(float xpos, float ypos) = 0;
};
class IPressedUp{
    public:
        virtual ~IPressedUp() {};
        virtual void OnPointerUp(float xpos, float ypos) = 0;
};

이름에서 알 수 있듯이

각자 버튼을 처음으로 눌렀을 때, 누르는 중일때, 떼는 순간일 때 이다.

호버나 더블클릭같은 이벤트는 필요하면 만들 것 같다.

 

그리고 InputEventSystem 클래스이다.

class InputEventSystem{
    public:
        eInputState inputState;

        void HandleInputPos(double xpos, double ypos);
        void HandleInputEvent(int button, int action);

        void AddPressed(IPressed* pressed);
        void AddPressedDown(IPressedDown* pressedDown);
        void AddPressedUp(IPressedUp* pressedUp);

    private:
        std::vector<IPressed*> mPressed;
        std::vector<IPressedDown*> mPressedDown;
        std::vector<IPressedUp*> mPressedUp;

        float mLastX, mLastY;
        bool mLastLeftBtnPressed=false;
        bool mLastRightBtnPressed =false;

};

 

HandleInputPos와 HandleInputEvent는 콜백함수로,

각각 마우스의 위치가 바뀌었을 때, 입력이 들어왔을 때 이벤트를 받아 처리한다.

void InputEventSystem::HandleInputPos(double xpos, double ypos){
    mLastX = xpos;
    mLastY= ypos;
}

HandleInputPos는 마지막의 위치를 업데이트 해준다.

 

void InputEventSystem::HandleInputEvent(int button, int action){

    if(mLastLeftBtnPressed==false && button==GLFW_MOUSE_BUTTON_LEFT && action==GLFW_PRESS){
        for(auto const& child : mPressedDown){
            child->OnPointerDown(mLastX,mLastY);
        }

        mLastLeftBtnPressed=true;
        return;
    }

    if(mLastLeftBtnPressed==true && button==GLFW_MOUSE_BUTTON_LEFT && action==GLFW_PRESS){
        for(auto const& child : mPressed){
            child->OnPointer(mLastX,mLastY);
        }


        mLastLeftBtnPressed=true;
        return;
    }

    if(mLastLeftBtnPressed==true && button==GLFW_MOUSE_BUTTON_LEFT && action==GLFW_RELEASE){
        for(auto const& child : mPressedUp){
            child->OnPointerUp(mLastX,mLastY);
        }

        mLastLeftBtnPressed=false;
        return;
    }

}

HandleInputEvent는 입력을 받아서 입력의 상태를 판단하고, 그에 맞는 이벤트를 호출한다.

 

Canvas도 입력이 필요하기에 IPressedUp을 상속받았고,

OnPointerUp을 구현했다.

 

void Canvas::OnPointerUp(float xpos, float ypos){
    float ndcX = (2*xpos/SCR_WIDTH)-1;
    float ndcY = 1-(2*ypos/SCR_HEIGHT);

    for(auto const& child:mChild){
        glm::vec3 pos = child -> GetPos();
        glm::vec2 size = child -> GetSize();
        glm::vec2 sizeHalf = glm::vec2(size.x/2,size.y/2);
        
        if(ndcX>=(pos.x-sizeHalf.x)
        &&ndcX<=(pos.x+sizeHalf.x)
        &&ndcY>=(pos.y-sizeHalf.y)
        &&ndcY<=(pos.y+sizeHalf.y)){
            if(child->getButtonCallback() != NULL)
                child->Callbtn();
        }
    }
}

입력으로 들어오는 xy값은 스크린 좌표이고,

가지고 있는 UI의 좌표계는 ndc라서 먼저 변환해준뒤,

mChild를 돌면서 조건을 만족하는 Widget이 있는지 검사한다.

만약 조건에 맞는 Widget이 있다면 버튼 콜백함수를 호출한다.

 

 

그래서 입력 테스트를 위해..

아까 만들었던 버튼에 콜백함수를 작성해서 붙여주면..

Button* btn= new Button(glm::vec3(-0.3f,0.5f,0.0f),0.2f,0.2f,"resource/dotIcon.png");
auto btnCallback = [&btn](double xpos, double ypos){
    btn->Pushed();
};
btn->SetbuttonCallback(std::function<void(double, double)>(btnCallback));
canvas->AddWidget(std::move(btn));

 

이제 입력할때마다 색이 바뀐다!

 

 

UI 디자인

기본적인 ui 시스템이 생겨서

대충 디자인도 피그마로 했다

 

이 디자인대로 그냥 슥슥만들었다.

그런데 피그마의 좌표계는 뭔지 정확히 모르겠는데..

쨌든 내가 만든 GUI는 정규화된 값으로 위치와 크기를 받기 때문에..

변환해주는 프로그램을 파이썬으로 만들고..

scr_width = 1512
scr_height = 982

while(True):
    
    print("input x")
    x = int(input())

    print("input y")
    y= int(input())

    print("input w")
    w= int(input())

    print("input h")
    h = int(input())

    w_half = w/2
    h_half = h/2

    scr_x = x+w_half
    scr_y = y+h_half

    ndc_x = 2 * (scr_x / scr_width) - 1
    ndc_y = 1 - 2 * (scr_y / scr_height)

    print("x : ",ndc_x)
    print("y : ",ndc_y)

    w_nomal = (w/1512) *2
    h_nomal = (h/982) *2

    print("w : ", w_nomal)
    print("h : ", h_nomal)

 

이제 진짜로 슥슥 만들어서...

 

 

와! GUI!

 

버튼이 여러개 있지만 사실 아직 기능은 없다.

그래서 이제 UI 상태 관리와 기능들을 추가할 것이다..