View and Perspective matrices

Hi!

This week I started learning OpenGL through various tutorials. I started by drawing a 3D cube (centered in (0,0,0) whose vertices are between -1 and 1 along the three dimensions). I also managed to rotate my cube (along the three axes) i.e. modifying the Model matrix and it behaves as expected.

Now, I would like to define View matrix and Projection matrix. However, as I am working in Python, I have tried to implement (using OpenGL source code of these functions) glLookAt(view, center, up) to yield View matrix and Perspective(GLdouble fov, GLdouble aspect, GLdouble near, GLdouble far) and glFrustum(GLdouble left, GLdouble right, GLdouble bottom, GLdouble top, GLdouble near, GLdouble far) to yield Projection matrix.

Nonetheless, as, currently, my functions seem to fail, I would like to know if somebody can give me what these two functions should yield (4x4 matrices), in running the C++ command:

glLookAt(view=[0,0,1], center=[0,0,0], up=[0,1,0]) = ???
Perspective(FoV=45°, Ratio=4/3, near=0.1, far=100) = ???

It would be very helpful to debug both functions as due to column/row major ordering, it could be necessary to transpose my Python’s matrices.

Thanks in advance.

Here is what I use.



import math
import numpy as np

def transform(m, v):
    return np.asarray(m * np.asmatrix(v).T)[:,0]

def magnitude(v):
    return math.sqrt(np.sum(v ** 2))

def normalize(v):
    m = magnitude(v)
    if m == 0:
        return v
    return v / m

def ortho(l, r, b, t, n, f):
    dx = r - l
    dy = t - b
    dz = f - n
    rx = -(r + l) / (r - l)
    ry = -(t + b) / (t - b)
    rz = -(f + n) / (f - n)
    return np.matrix([[2.0/dx,0,0,rx],
                      [0,2.0/dy,0,ry],
                      [0,0,-2.0/dz,rz],
                      [0,0,0,1]])

def perspective(fovy, aspect, n, f):
    s = 1.0/math.tan(math.radians(fovy)/2.0)
    sx, sy = s / aspect, s
    zz = (f+n)/(n-f)
    zw = 2*f*n/(n-f)
    return np.matrix([[sx,0,0,0],
                      [0,sy,0,0],
                      [0,0,zz,zw],
                      [0,0,-1,0]])

def frustum(x0, x1, y0, y1, z0, z1):
    a = (x1+x0)/(x1-x0)
    b = (y1+y0)/(y1-y0)
    c = -(z1+z0)/(z1-z0)
    d = -2*z1*z0/(z1-z0)
    sx = 2*z0/(x1-x0)
    sy = 2*z0/(y1-y0)
    return np.matrix([[sx, 0, a, 0],
                      [ 0,sy, b, 0],
                      [ 0, 0, c, d],
                      [ 0, 0,-1, 0]])

def translate(xyz):
    x, y, z = xyz
    return np.matrix([[1,0,0,x],
                      [0,1,0,y],
                      [0,0,1,z],
                      [0,0,0,1]])

def scale(xyz):
    x, y, z = xyz
    return np.matrix([[x,0,0,0],
                      [0,y,0,0],
                      [0,0,z,0],
                      [0,0,0,1]])

def sincos(a):
    a = math.radians(a)
    return math.sin(a), math.cos(a)

def rotate(a, xyz):
    x, y, z = normalize(xyz)
    s, c = sincos(a)
    nc = 1 - c
    return np.matrix([[x*x*nc +   c, x*y*nc - z*s, x*z*nc + y*s, 0],
                      [y*x*nc + z*s, y*y*nc +   c, y*z*nc - x*s, 0],
                      [x*z*nc - y*s, y*z*nc + x*s, z*z*nc +   c, 0],
                      [           0,            0,            0, 1]])

def rotx(a):
    s, c = sincos(a)
    return np.matrix([[1,0,0,0],
                      [0,c,-s,0],
                      [0,s,c,0],
                      [0,0,0,1]])

def roty(a):
    s, c = sincos(a)
    return np.matrix([[c,0,s,0],
                      [0,1,0,0],
                      [-s,0,c,0],
                      [0,0,0,1]])

def rotz(a):
    s, c = sincos(a)
    return np.matrix([[c,-s,0,0],
                      [s,c,0,0],
                      [0,0,1,0],
                      [0,0,0,1]])

def lookat(eye, target, up):
    F = target[:3] - eye[:3]
    f = normalize(F)
    U = normalize(up[:3])
    s = np.cross(f, U)
    u = np.cross(s, f)
    M = np.matrix(np.identity(4))
    M[:3,:3] = np.vstack([s,u,-f])
    T = translate(-eye)
    return M * T

def viewport(x, y, w, h):
    x, y, w, h = map(float, (x, y, w, h))
    return np.matrix([[w/2, 0  , 0,x+w/2],
                      [0  , h/2, 0,y+h/2],
                      [0  , 0  , 1,    0],
                      [0  , 0  , 0,    1]])

Functions which are named similarly to OpenGL/GLU functions have the same conventions (and in many cases, the matrices were taken directly from the OpenGL reference pages).

A note about NumPy matrices: if you transpose the matrix using the .transpose() method or the .T property, you need to use np.ascontiguousarray() to actually re-order the data. Alternatively, you can just use np.asfortranarray() to store the matrix in column-major order. PyOpenGL simply passes a pointer to the underlying buffer, so array views created e.g. by transposition or slicing won’t behave as expected.

[QUOTE=GClements;1280454]Here is what I use.



import math
import numpy as np

def transform(m, v):
    return np.asarray(m * np.asmatrix(v).T)[:,0]

def magnitude(v):
    return math.sqrt(np.sum(v ** 2))

def normalize(v):
    m = magnitude(v)
    if m == 0:
        return v
    return v / m

def ortho(l, r, b, t, n, f):
    dx = r - l
    dy = t - b
    dz = f - n
    rx = -(r + l) / (r - l)
    ry = -(t + b) / (t - b)
    rz = -(f + n) / (f - n)
    return np.matrix([[2.0/dx,0,0,rx],
                      [0,2.0/dy,0,ry],
                      [0,0,-2.0/dz,rz],
                      [0,0,0,1]])

def perspective(fovy, aspect, n, f):
    s = 1.0/math.tan(math.radians(fovy)/2.0)
    sx, sy = s / aspect, s
    zz = (f+n)/(n-f)
    zw = 2*f*n/(n-f)
    return np.matrix([[sx,0,0,0],
                      [0,sy,0,0],
                      [0,0,zz,zw],
                      [0,0,-1,0]])

def frustum(x0, x1, y0, y1, z0, z1):
    a = (x1+x0)/(x1-x0)
    b = (y1+y0)/(y1-y0)
    c = -(z1+z0)/(z1-z0)
    d = -2*z1*z0/(z1-z0)
    sx = 2*z0/(x1-x0)
    sy = 2*z0/(y1-y0)
    return np.matrix([[sx, 0, a, 0],
                      [ 0,sy, b, 0],
                      [ 0, 0, c, d],
                      [ 0, 0,-1, 0]])

def translate(xyz):
    x, y, z = xyz
    return np.matrix([[1,0,0,x],
                      [0,1,0,y],
                      [0,0,1,z],
                      [0,0,0,1]])

def scale(xyz):
    x, y, z = xyz
    return np.matrix([[x,0,0,0],
                      [0,y,0,0],
                      [0,0,z,0],
                      [0,0,0,1]])

def sincos(a):
    a = math.radians(a)
    return math.sin(a), math.cos(a)

def rotate(a, xyz):
    x, y, z = normalize(xyz)
    s, c = sincos(a)
    nc = 1 - c
    return np.matrix([[x*x*nc +   c, x*y*nc - z*s, x*z*nc + y*s, 0],
                      [y*x*nc + z*s, y*y*nc +   c, y*z*nc - x*s, 0],
                      [x*z*nc - y*s, y*z*nc + x*s, z*z*nc +   c, 0],
                      [           0,            0,            0, 1]])

def rotx(a):
    s, c = sincos(a)
    return np.matrix([[1,0,0,0],
                      [0,c,-s,0],
                      [0,s,c,0],
                      [0,0,0,1]])

def roty(a):
    s, c = sincos(a)
    return np.matrix([[c,0,s,0],
                      [0,1,0,0],
                      [-s,0,c,0],
                      [0,0,0,1]])

def rotz(a):
    s, c = sincos(a)
    return np.matrix([[c,-s,0,0],
                      [s,c,0,0],
                      [0,0,1,0],
                      [0,0,0,1]])

def lookat(eye, target, up):
    F = target[:3] - eye[:3]
    f = normalize(F)
    U = normalize(up[:3])
    s = np.cross(f, U)
    u = np.cross(s, f)
    M = np.matrix(np.identity(4))
    M[:3,:3] = np.vstack([s,u,-f])
    T = translate(-eye)
    return M * T

def viewport(x, y, w, h):
    x, y, w, h = map(float, (x, y, w, h))
    return np.matrix([[w/2, 0  , 0,x+w/2],
                      [0  , h/2, 0,y+h/2],
                      [0  , 0  , 1,    0],
                      [0  , 0  , 0,    1]])

Functions which are named similarly to OpenGL/GLU functions have the same conventions (and in many cases, the matrices were taken directly from the OpenGL reference pages).

A note about NumPy matrices: if you transpose the matrix using the .transpose() method or the .T property, you need to use np.ascontiguousarray() to actually re-order the data. Alternatively, you can just use np.asfortranarray() to store the matrix in column-major order. PyOpenGL simply passes a pointer to the underlying buffer, so array views created e.g. by transposition or slicing won’t behave as expected.[/QUOTE]

I tried the lookat and perspective functions but, now, my cube looks like a pyramid and I do not know what’s causing this.
Here is the code of my timer function:

 def timer(fps):
    global clock
    clock += 0.0005*1000.0/fps

    theta = clock;
    matModel = rotate(theta*180/np.pi, np.array([1,1,1]))
    
    eye = np.array([0,0,1])
    target = np.array([0,0,0])
    up = np.array([0,1,0])    
    
    matView = lookat(eye, target, up)
    #matView = np.eye(4)
    
    matProjection = perspective(fovy=45.0, aspect=4/3, n=-3, f=100.0)
    #matProjection = np.eye(4)  
      
    loc = gl.glGetUniformLocation(program, "Model")
    gl.glUniformMatrix4fv(loc, 1, False, np.asfortranarray(matModel))

    loc = gl.glGetUniformLocation(program, "View")
    gl.glUniformMatrix4fv(loc, 1, False, np.asfortranarray(matView))

    loc = gl.glGetUniformLocation(program, "Projection")
    gl.glUniformMatrix4fv(loc, 1, False, np.asfortranarray(matProjection))                 
    
    
    glut.glutTimerFunc(1000/fps, timer, fps)
    glut.glutPostRedisplay()

My display function:



def display():
    
    gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
    #gl.glDrawArrays(gl.GL_TRIANGLES, 0, 12)
    
    gl.glEnable(gl.GL_DEPTH_TEST)
    gl.glDepthFunc(gl.GL_LESS)

    gl.glDrawElements(gl.GL_TRIANGLES, len(index), gl.GL_UNSIGNED_INT, ctypes.c_void_p(0))
    glut.glutSwapBuffers()


The vertices and indices of my cube:


positions = [ (-1,-1,1),
                     (1,-1,1),
                        (1,1,1),   
                        (-1,1,1),
                        (-1,-1,-1),
                        (1,-1,-1),
                        (1,1,-1),
                        (-1,1,-1)]
                        
index = np.array([0,1,2,
                2,3,0,
                1,5,6,
                6,2,1,
                7,6,5,
                5,4,7,
                4,0,3,
                3,7,4,
                4,5,1,
                1,0,4,
                3,2,6,
                6,7,3], dtype=np.uint32)

and my shaders:



vertex_code = """
    #version 330 core
    uniform float scale;
    uniform mat4 Model;
    uniform mat4 View;
    uniform mat4 Projection;
    in vec4 color;
    in vec3 position;
    out vec4 v_color;
    void main()
    {
        gl_Position = Projection*View*Model*vec4(scale*position, 1.0);
        v_color = color;
    } """

fragment_code = """
    #version 330 core
    in vec4 v_color;
    void main()
    {
        gl_FragColor = v_color;
    } """

I suggest making the eye position farther from the cube. [0,0,1] will be either on the surface of the cube or inside it.

The near and far values should both be positive, and the near value shouldn’t be too small (too small a near value will reduce the depth precision over most of the scene). n=0.1, f=10.0 should be sufficient here.

Also, if you’re using Python 2.x (and aren’t using “from future import division”), aspect=4/3 is equivalent to aspect=1, as dividing integers yields an integer result. If you’re using Python 3 … I haven’t tested my code with Python 3.

I suggest adding “dtype=np.float32” to the np.asfortranarray() call. NumPy arrays normally use double precision.

[QUOTE=GClements;1280469]I suggest making the eye position farther from the cube. [0,0,1] will be either on the surface of the cube or inside it.

The near and far values should both be positive, and the near value shouldn’t be too small (too small a near value will reduce the depth precision over most of the scene). n=0.1, f=10.0 should be sufficient here.

Also, if you’re using Python 2.x (and aren’t using “from future import division”), aspect=4/3 is equivalent to aspect=1, as dividing integers yields an integer result. If you’re using Python 3 … I haven’t tested my code with Python 3.

I suggest adding “dtype=np.float32” to the np.asfortranarray() call. NumPy arrays normally use double precision.[/QUOTE]

I also remember that 4/3 yields 1 so I immediatly correct it once I answered :). I also find out what are the best parameters to get what I want.

However the main issue was that I had to transpose the three matrices (Model, View and Projection) or to use “True” as the second parameters in glUniformMatrix4fv().

However, the result (which is the expected one) does not change wether I use np.asarray, np.contiguousarray or np.asfortranarray, why?

It appears that the behaviour has changed between versions. For my tests (PyOpenGL 3.0.2, NumPy 1.6.2), it matters whether I use ascontiguousarray() or asfortranarray(), but transposing the array returned from ascontiguousarray() or asfortranarray() has no effect (i.e. it’s clearly using the in-memory layout directly).

For me, these all produce the correct result:


glUniformMatrix4fv(loc, 1, GL_TRUE , np.ascontiguousarray(m, dtype=np.float32))
glUniformMatrix4fv(loc, 1, GL_FALSE, np.ascontiguousarray(m.T, dtype=np.float32))
glUniformMatrix4fv(loc, 1, GL_FALSE, np.asfortranarray(m, dtype=np.float32))
glUniformMatrix4fv(loc, 1, GL_TRUE , np.asfortranarray(m.T, dtype=np.float32))

while these all produce the wrong result:


glUniformMatrix4fv(loc, 1, GL_FALSE, np.ascontiguousarray(m, dtype=np.float32))
glUniformMatrix4fv(loc, 1, GL_TRUE , np.ascontiguousarray(m.T, dtype=np.float32))
glUniformMatrix4fv(loc, 1, GL_TRUE , np.asfortranarray(m, dtype=np.float32))
glUniformMatrix4fv(loc, 1, GL_FALSE, np.asfortranarray(m.T, dtype=np.float32))