c++openglglslglfwcoordinate-systems

Can't change base coordinates from bottom-left to top-left


I'm writing a text editor using OpenGL and GLFW and traditionally GUI applications scale from top left-corner (instead of from bottom-left corner), so that's why this is important to me.

I want to make it so when I resize window, rendered stuff goes top-left instead of bottom-left.

Here is some screenshots to show what i mean:

Initial window:

enter image description here

Resized window (goes to bottom left):

enter image description here

How I want resized window to look (top left):

enter image description here

Minimal reproducible example:

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include <string>
#include <fstream>
#include <sstream>

class Shader
{
public:
    unsigned int ID;
    // constructor generates the shader on the fly
    // ------------------------------------------------------------------------
    Shader(const char* vertexPath, const char* fragmentPath)
    {
        // 1. retrieve the vertex/fragment source code from filePath
        std::string vertexCode;
        std::string fragmentCode;
        std::ifstream vShaderFile;
        std::ifstream fShaderFile;
        // ensure ifstream objects can throw exceptions:
        vShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);
        fShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);
        try 
        {
            // open files
            vShaderFile.open(vertexPath);
            fShaderFile.open(fragmentPath);
            std::stringstream vShaderStream, fShaderStream;
            // read file's buffer contents into streams
            vShaderStream << vShaderFile.rdbuf();
            fShaderStream << fShaderFile.rdbuf();
            // close file handlers
            vShaderFile.close();
            fShaderFile.close();
            // convert stream into string
            vertexCode   = vShaderStream.str();
            fragmentCode = fShaderStream.str();
        }
        catch (std::ifstream::failure& e)
        {
            std::cout << "ERROR::SHADER::FILE_NOT_SUCCESSFULLY_READ: " << e.what() << std::endl;
        }
        const char* vShaderCode = vertexCode.c_str();
        const char * fShaderCode = fragmentCode.c_str();
        // 2. compile shaders
        unsigned int vertex, fragment;
        // vertex shader
        vertex = glCreateShader(GL_VERTEX_SHADER);
        glShaderSource(vertex, 1, &vShaderCode, NULL);
        glCompileShader(vertex);
        checkCompileErrors(vertex, "VERTEX");
        // fragment Shader
        fragment = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource(fragment, 1, &fShaderCode, NULL);
        glCompileShader(fragment);
        checkCompileErrors(fragment, "FRAGMENT");
        // shader Program
        ID = glCreateProgram();
        glAttachShader(ID, vertex);
        glAttachShader(ID, fragment);
        glLinkProgram(ID);
        checkCompileErrors(ID, "PROGRAM");
        // delete the shaders as they're linked into our program now and no longer necessary
        glDeleteShader(vertex);
        glDeleteShader(fragment);
    }
    // activate the shader
    // ------------------------------------------------------------------------
    void use() 
    { 
        glUseProgram(ID); 
    }
    // utility uniform functions
    // ------------------------------------------------------------------------
    void setBool(const std::string &name, bool value) const
    {         
        glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value); 
    }
    // ------------------------------------------------------------------------
    void setInt(const std::string &name, int value) const
    { 
        glUniform1i(glGetUniformLocation(ID, name.c_str()), value); 
    }
    // ------------------------------------------------------------------------
    void setFloat(const std::string &name, float value) const
    { 
        glUniform1f(glGetUniformLocation(ID, name.c_str()), value); 
    }

private:
    // utility function for checking shader compilation/linking errors.
    // ------------------------------------------------------------------------
    void checkCompileErrors(unsigned int shader, std::string type)
    {
        int success;
        char infoLog[1024];
        if (type != "PROGRAM")
        {
            glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
            if (!success)
            {
                glGetShaderInfoLog(shader, 1024, NULL, infoLog);
                std::cout << "ERROR::SHADER_COMPILATION_ERROR of type: " << type << "\n" << infoLog << "\n -- --------------------------------------------------- -- " << std::endl;
            }
        }
        else
        {
            glGetProgramiv(shader, GL_LINK_STATUS, &success);
            if (!success)
            {
                glGetProgramInfoLog(shader, 1024, NULL, infoLog);
                std::cout << "ERROR::PROGRAM_LINKING_ERROR of type: " << type << "\n" << infoLog << "\n -- --------------------------------------------------- -- " << std::endl;
            }
        }
    }
};

void processInput(GLFWwindow *window);

const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

int main()
{
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif

    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);

    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    Shader ourShader("3.3.shader.vs", "3.3.shader.fs"); // you can name your shader files however you like

    float vertices[] = {
         0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f,  // bottom right
        -0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,  // bottom left
         0.0f,  0.5f, 0.0f,  0.0f, 0.0f, 1.0f   // top
    };

    unsigned int VBO, VAO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glBindVertexArray(VAO);

    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);

    while (!glfwWindowShouldClose(window))
    {
        processInput(window);

        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        ourShader.use();
        glBindVertexArray(VAO);
        glDrawArrays(GL_TRIANGLES, 0, 3);

        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);

    glfwTerminate();
    return 0;
}

void processInput(GLFWwindow *window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}

3.3.shader.fs:

#version 330 core
out vec4 FragColor;

in vec3 ourColor;

void main()
{
    FragColor = vec4(ourColor, 1.0f);
}

3.3.shader.vs:

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;

out vec3 ourColor;

void main()
{
    gl_Position = vec4(aPos, 1.0);
    ourColor = aColor;
}

Solution

  • First of all, from the question and comments, I suspect that this is an XY problem. Regardless, let's address the actual question and code first.

    Curiously missing from the mre in the question is the setting of the viewport. Commonly (for example in the Learn OpenGL tutorials and the GLFW documentation) the viewport is set using a callback function. When the framebuffer of the window is resized, the viewport is then changed accordingly:

    void framebuffer_size_callback(GLFWwindow* window, int width, int height)
    {
        glViewport(0, 0, width, height);
    }
    

    This is typical OpenGL usage: the positions of the primitives are set in the vertex shader to gl_Position in clip space (in the range [-1, 1]), then converted to Normalized Device Coordinates (NDCs) and using the viewport parameters, these are converted to window coordinates. When the window is resized, the triangle remains in the middle and gets stretched/compressed as the relative distances to the edges remain the same.

    In the given mre, this doesn't happen. The viewport is set to the (suggested) size of the window (600x800), as this happens by default, and this size is used even if the window/framebuffer is resized. As a result, the triangle will always render with a fixed size and with a fixed distance to the origin (bottom-left) in window coordinates.

    For as far as I know, it's not possible to change this origin to top-left, but we can look at some options that come close. It is possible to do this for clip space:

    glClipControl(GL_UPPER_LEFT, GL_NEGATIVE_ONE_TO_ONE); // Requires OpenGL >=4.5
    

    But since this is for clip space and not window space, the triangle would merely appear upside down and would still be in the bottom-left corner.

    Your suggestion in the comments doesn't work either:

    void framebuffer_size_callback(GLFWwindow* window, int width, int height)
    {
        glViewport(0, height - SCR_HEIGHT, SCR_WIDTH, SCR_HEIGHT);
    }
    

    When height > SCR_HEIGHT, the second parameter becomes negative, which is not allowed (GL_INVALID_VALUE). Note that providing a negative width and height for the third and fourth parameters is not possible either; these parameters are unsigned integers.

    Now looking at your second suggestion:

    void framebuffer_size_callback(GLFWwindow *window, int width, int height)
    {
        src_height = height;
    }
    

    and in [the] RenderText function [edit this]: float ypos = y - (ch.Size.y - ch.Bearing.y) * scale; to this: float ypos = src_height - (y + (ch.Size.y - ch.Bearing.y) * scale);

    This refers to code outside of the mre, so I cannot be sure, but it looks like this would amount to making the characters' y-position depend on the window height. This could provide the effect you're looking for, but the exact results would vary from machine to machine (keep in mind that the dimensions passed to glfwCreateWindow are merely hints!) and it would still be a highly unusual way of using OpenGL. OpenGL is typically used to render a scene, defined in some world space, by means of a projection and other transformations - agnostic of the actual window resolution.

    Let's go back to your actual goal: writing a text editor that places text in the top left corner. The usual OpenGL approach would be this:

    1. Use the framebuffer callback function to set the viewport to the actual framebuffer size, which may be different from the one suggested to glfwCreateWindow(..). Alternatively, use the callback function to store the framebuffer size and set the viewport just before the rendering.
    2. Define the positions of the characters or text in your world space (which could be identical to clip space). If the text should start in the top-left corner, then this would be near [-1, 1].
    3. Apply an orthographic projection in the vertex shader to prevent the objects in the scene from being stretched/compressed as the aspect ratio of the window changes. You may also apply scaling and translation transformations here.
    4. Render your scene.

    The result should be that the objects (characters/text) in your scene always appear at the same relative position, independent of the resolution.