Search code examples
openglglslshadernim-lang

Why this projection gives a wrong result?


I try to obtain a so called world space coordinates for 2D rendering by creating a orthogonal projection matrix. But the result is disappointing I expect a blue triangle but there is only some blue pixels in the bottom right.

enter image description here

Here is my code in Nim language. It's a very simplified version who reproduces the issue.

The important part is in the function "projection2D", or maybe in the vertex shader. I don't think the problem is elsewhere but for safety I post the complete example

type 
    OGLfloat = float32
    OGLuint = uint32
    OGLint = int32

type Mat4x4* = array[16, OGLfloat] # 4 x 4 Matrix

# Here OpenGL constants should be here but not pasted to save space.
#....

const 
    POSITION_LENGTH = 3.OGLint
    COLOR_LENGTH = 4.OGLint

const
    WINDOW_W = 640
    WINDOW_H = 480

let
    colorDataOffset = POSITION_LENGTH * OGLint(sizeof(OGLfloat))

# OpenGL function import should be here.
#...

var 
    # Thanks to an orthogonal projection matrix I expect to use coordinates in pixels.

    vertices = @[OGLfloat(420), 0, 0, # Position   
                                0, 0, 1, 1, # Color
                          640, 480, 0,
                                0, 0, 1, 1,
                          0, 480, 0,
                                0, 0, 1, 1]

    indices = @[OGLuint(0), 1 , 2]

proc projection2D(left, right, bottom, top, far, near:float):Mat4x4 =
    result = [OGLfloat(1), 0, 0, 0, # Start from an identity matrix.
                        0, 1, 0, 0, 
                        0, 0, 1, 0, 
                        0, 0, 0, 1]

    # Orthographic projection, inspired from a Wikipedia article example.

    result[0] = OGLfloat(2.0 / (right - left))
    result[5] = OGLfloat(2.0 / (top - bottom))
    result[3] = OGLfloat(- ((right + left) / (right - left)))
    result[7] = OGLfloat(- ((top + bottom) / (top - bottom)))
    result[10] = OGLfloat(-2 / (far - near))
    result[11] = OGLfloat(-((far + near)/(far - near)))

# These parameters comes from "learnopengl.com". 
var projectionMatrix = projection2D(0.0, OGLfloat(WINDOW_W), OGLfloat(WINDOW_H), 0.0, - 1.0, 1.0)

var glfwErr = glfwInit()
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3)
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3)
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE)
var winHandle = glfwCreateWindow(WINDOW_W, WINDOW_H)
glfwMakeContextCurrent(winHandle)
var glewErr = glewInit()

var
    shadID:OGLuint
    vertSrc:cstring = """
        #version 330 core
        layout (location = 0) in vec3 aPos;
        layout (location = 1) in vec4 aColor;

        out vec4 vColor;

        uniform mat4 projection;

        void main()
        {
            gl_Position = projection * vec4(aPos, 1.0f);
            vColor = aColor;
        }
        """
    fragSrc:cstring = """
        #version 330 core
        out vec4 FragColor;    
        in vec4 vColor;

        void main()
        {
            FragColor = vColor;
        }

        """

proc send_src(vert:var cstring, frag:var cstring):OGLuint =
    var success:OGLint
    # vertex
    var vertexShader = glCreateShader(GL_VERTEX_SHADER)
    glShaderSource(vertexShader, 1, addr vert, nil)
    glCompileShader(vertexShader)
    # Check compilation errors.
    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, addr success)
    if bool(success) == false:
        echo(" vertex shader compilation failed (send_src)")
    else:
        echo("vertexShader compiled (send_src)")

    # fragment
    var fragmentShader = glCreateShader(GL_FRAGMENT_SHADER)
    glShaderSource(fragmentShader, 1, addr frag, nil)
    glCompileShader(fragmentShader)
    # Check compilation errors.
    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, addr success)
    if bool(success) == false:
        echo("fragment shader compilation failed (send_src)")
    else:
        echo("fragmentShader compiled (send_src)")

    # Shader program
    result = glCreateProgram()
    glAttachShader(result, vertexShader)
    glAttachShader(result, fragmentShader)
    glLinkProgram(result)
    # Check for linkage errors.
    glGetProgramiv(result, GL_LINK_STATUS, addr success)
    if success == 0:
        echo("program linking failed (send_src)") 
    else:
        echo("shader linked (send_src)")

    glDeleteShader(vertexShader)
    glDeleteShader(fragmentShader)

glViewport(0, 0, WINDOW_W, WINDOW_H)
shadID = send_src(vertSrc, fragSrc)

var VAO, VBO, EBO:OGLuint
glGenVertexArrays(1, addr VAO)
glGenBuffers(1, addr VBO)
glGenBuffers(1, addr EBO)
glBindVertexArray(VAO)
glBindBuffer(GL_ARRAY_BUFFER, VBO)
glBufferData(GL_ARRAY_BUFFER, vertices.len * sizeof(OGLfloat), 
             addr vertices[0], GL_STATIC_DRAW)
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO)
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.len * sizeof(OGLuint), 
             addr indices[0], GL_STATIC_DRAW)
# Position layout
glVertexAttribPointer(0, POSITION_LENGTH, GL_FLOAT, GL_FALSE, (POSITION_LENGTH + COLOR_LENGTH) * OGLint(sizeof(OGLfloat)), 
                      nil)
glEnableVertexAttribArray(0)
# Color layout
glVertexAttribPointer(1, COLOR_LENGTH, GL_FLOAT, GL_FALSE, (POSITION_LENGTH + COLOR_LENGTH) * OGLint(sizeof(OGLfloat)), 
                      cast[pointer](colorDataOffset))
glEnableVertexAttribArray(1)
glBindBuffer(GL_ARRAY_BUFFER, 0)
glBindVertexArray(0)
glUseProgram(shadID)

while bool(glfwWindowShouldClose(winHandle)) == false:
    glClearColor(0.2, 0.3, 0.3, 1.0)
    glClear(GL_COLOR_BUFFER_BIT)
    glBindVertexArray(VAO)
    glUniformMatrix4fv(glGetUniformLocation(shadID, "projection"), 1, GL_FALSE, addr projectionMatrix[0])
    glDrawElements(GL_TRIANGLES, OGLint(indices.len), GL_UNSIGNED_INT, nil)
    glfwSwapBuffers(winHandle)
    glfwPollEvents()

glDeleteVertexArrays(1, addr VAO)
glDeleteBuffers(1, addr VBO)
glDeleteBuffers(1, addr EBO)
glfwDestroyWindow(winHandle)
glfwTerminate()

Don't hesitate to share your thoughts !


Solution

  • You have to transpose the projection matrix, this means the 3rd parameter of glUniformMatrix4fv has to be GL_TRUE:

    glUniformMatrix4fv(glGetUniformLocation(shadID, "projection"), 
                       1, GL_TRUE, addr projectionMatrix[0])
    

    Or you have to initialize the matrix according to the specification (see below):

     proc projection2D(left, right, bottom, top, far, near:float):Mat4x4 =
        result = [OGLfloat(1), 0, 0, 0, # Start from an identity matrix.
                            0, 1, 0, 0, 
                            0, 0, 1, 0, 
                            0, 0, 0, 1]
    
        # Orthographic projection, inspired from a Wikipedia article example.
    
        result[0] = OGLfloat(2.0 / (right - left))
        result[5] = OGLfloat(2.0 / (top - bottom))
        result[12] = OGLfloat(- ((right + left) / (right - left)))
        result[13] = OGLfloat(- ((top + bottom) / (top - bottom)))
        result[10] = OGLfloat(-2 / (far - near))
        result[14] = OGLfloat(-((far + near)/(far - near)))
    

    See The OpenGL Shading Language 4.6, 5.4.2 Vector and Matrix Constructors, page 101:

    To initialize a matrix by specifying vectors or scalars, the components are assigned to the matrix elements in column-major order.

    mat4(float, float, float, float,  // first column
         float, float, float, float,  // second column
         float, float, float, float,  // third column
         float, float, float, float); // fourth column
    

    Note, in compare to a mathematical matrix where the columns are written from top to bottom, which feels natural, at the initialization of an OpenGL matrix, the columns are written from the left to the right. This leads to the benefit, that the x, y, z components of an axis or of the translation are in direct succession in the memory.

    See also Data Type (GLSL) - Matrix constructors