1

My camera renders upside down and backwards (e.g. objects that should be in front of camera are behind it) in perspective mode, and in ortographic mode it's upside down and objects render even if they're behind the camera. The X and Y axes for rotation also seem to be reversed.

This is a quick version of how I construct the matrices:

Matrix model = gameObject->transform->GetMatrix();
Matrix view = camera->transform->GetMatrix();
view.Invert();

Matrix projection;
projection.setOrtho(-aspectRatio * ortographicSize, aspectRatio * ortographicSize, -1 * ortographicSize, 1 * ortographicSize, clipMin, clipMax);
// or
projection.SetPerspective(60, aspectRatio, clipMin, clipMax);

Matrix mvp = model * view * projection;

You can find my Matrix class on Github. The projection is set in the SetPerspective and SetOrtho methods respectively, but the issue might also lie in the view matrix, which uses the Invert method.

  • 2
    Why is data[10] commented out in SetPerspective? See this for clip-space issue with Z in orthographic projection: https://stackoverflow.com/questions/33342114/near-far-planes-and-z-in-orthographic-rasterization – Robinson Mar 06 '18 at 20:07
  • @Robinson Thank you. I will look at that link. I'm not sure why I added the data[10] line. Must've been one of my attempts to fix it, and it didn't make a difference so I commented it out. – Robin van Ee Mar 06 '18 at 20:20
  • @Rabbid76 It might be. I take the camera's transformation matrix and invert it. Is that wrong? – Robin van Ee Mar 06 '18 at 20:20
  • 1
    @RobinvanEe Well you certainly need it. – Robinson Mar 06 '18 at 20:22
  • I believe that index [10] in a single dimensional array would be the same as index [2][2] in a [4][4] matrix. – Francis Cugler Mar 07 '18 at 00:00
  • As there is nothing wrong with writing out your own matrix class; if you are able to I would suggest using `GLM`' library especially with OpenGL & GLSL as it works seamlessly and makes life a lot easier. – Francis Cugler Mar 07 '18 at 00:09
  • @RobinvanEe With OpenGL their camera space in LH is completely different than DirectX's RH as OpenGL doesn't have a camera class and you have to create it. In OpenGL you are not actually moving the camera through the scene or world space, in OpenGL you are moving the scene or world space around the position and view angle of the camera. – Francis Cugler Mar 07 '18 at 00:14

1 Answers1

0

I was looking at your Matrix::SetOrtho() & Matrix::SetProjection() functions and I was comparing them to GLM's versions.

Your functions:

void Matrix::SetOrtho( float left, float right, 
                       float top, float bottom, 
                       float clipMin, float clipMax ) { 
    memset(data, 0, sizeof(float) * 16); 
    data[0] = 2 / (right - left); 
    data[5] = 2 / (top - bottom); 
    data[10] = -2 / (clipMax - clipMin); 
    data[15] = 1; 
} 

void Matrix::SetPerspective( float angle, float aspect, 
                             float clipMin, float clipMax ) { 
    float tangent = WolfMath::Tan(WolfMath::DegToRad(angle / 2)); 
    memset(data, 0, sizeof(float) * 16); 
    data[0] = 0.5f / tangent; 
    data[5] = 0.5f * aspect / tangent; 
    //data[10] = -(clipMax + clipMin) / (clipMax - clipMin); 
    data[11] = -1; 
    data[14] = (-2 * clipMax * clipMin) / (clipMax - clipMin); 
} 

GLM's - GLM

// GLM::ortho
template <typename T>
GLM_FUNC_QUALIFIER tmat4x4<T, defaultp> ortho( T left, T right,
                                               T bottom, T top,
                                               T zNear, T zFar ) {
#if GLM_COORDINATE_SYSTEM == GLM_LEFT_HANDED
    return orthoLH(left, right, bottom, top, zNear, zFar);
#else
    return orthoRH(left, right, bottom, top, zNear, zFar);
#endif
}

template <typename T>
GLM_FUNC_QUALIFIER tmat4x4<T, defaultp> orthoLH ( T left, T right,
                                                  T bottom, T top,
                                                  T zNear, T zFar ) {
    tmat4x4<T, defaultp> Result(1);
    Result[0][0] = static_cast<T>(2) / (right - left);
    Result[1][1] = static_cast<T>(2) / (top - bottom);
    Result[3][0] = - (right + left) / (right - left);
    Result[3][1] = - (top + bottom) / (top - bottom);

#if GLM_DEPTH_CLIP_SPACE == GLM_DEPTH_ZERO_TO_ONE
    Result[2][2] = static_cast<T>(1) / (zFar - zNear);
    Result[3][2] = - zNear / (zFar - zNear);
#else
    Result[2][2] = static_cast<T>(2) / (zFar - zNear);
    Result[3][2] = - (zFar + zNear) / (zFar - zNear);
#endif
    return Result;
}

template <typename T> 
GLM_FUNC_QUALIFIER tmat4x4<T, defaultp> orthoRH( T left, T right,
                                                 T bottom, T top,
                                                 T zNear, T zFar ) {
    tmat4x4<T, defaultp> Result(1);
    Result[0][0] = static_cast<T>(2) / (right - left);
    Result[1][1] = static_cast<T>(2) / (top - bottom);
    Result[3][0] = - (right + left) / (right - left);
    Result[3][1] = - (top + bottom) / (top - bottom);

#if GLM_DEPTH_CLIP_SPACE == GLM_DEPTH_ZERO_TO_ONE
    Result[2][2] = - static_cast<T>(1) / (zFar - zNear);
    Result[3][2] = - zNear / (zFar - zNear);
#else
    Result[2][2] = - static_cast<T>(2) / (zFar - zNear);
    Result[3][2] = - (zFar + zNear) / (zFar - zNear);
#endif
    return Result;
}

template <typename T>
GLM_FUNC_QUALIFIER tmat4x4<T, defaultp> ortho( T left, T right,
                                               T bottom, T top ) {
    tmat4x4<T, defaultp> Result(static_cast<T>(1));
    Result[0][0] = static_cast<T>(2) / (right - left);
    Result[1][1] = static_cast<T>(2) / (top - bottom);
    Result[2][2] = - static_cast<T>(1);
    Result[3][0] = - (right + left) / (right - left);
    Result[3][1] = - (top + bottom) / (top - bottom);
    return Result;
}

// GLM::perspective (This is a little more involved 
// due to the frustum & fov components)
template <typename T>
GLM_FUNC_QUALIFIER tmat4x4<T, defaultp> frustum( T left, T right,
                                                 T bottom, T top,
                                                 T nearVal, T farVal ) {
#if GLM_COORDINATE_SYSTEM == GLM_LEFT_HANDED
    return frustumLH(left, right, bottom, top, nearVal, farVal);
#else
    return frustumRH(left, right, bottom, top, nearVal, farVal);
#endif
}

template <typename T>
GLM_FUNC_QUALIFIER tmat4x4<T, defaultp> frustumLH( T left, T right,
                                                   T bottom, T top,
                                                   T nearVal, T farVal ) {
    tmat4x4<T, defaultp> Result(0);
    Result[0][0] = (static_cast<T>(2) * nearVal) / (right - left);
    Result[1][1] = (static_cast<T>(2) * nearVal) / (top - bottom);
    Result[2][0] = (right + left) / (right - left);
    Result[2][1] = (top + bottom) / (top - bottom);
    Result[2][3] = static_cast<T>(1);

#if GLM_DEPTH_CLIP_SPACE == GLM_DEPTH_ZERO_TO_ONE
    Result[2][2] = farVal / (farVal - nearVal);
    Result[3][2] = -(farVal * nearVal) / (farVal - nearVal);
#else
    Result[2][2] = (farVal + nearVal) / (farVal - nearVal);
    Result[3][2] = - (static_cast<T>(2) * farVal * nearVal) / (farVal - nearVal);
#endif
    return Result;
}

template <typename T>
GLM_FUNC_QUALIFIER tmat4x4<T, defaultp> frustumRH( T left, T right,
                                                   T bottom, T top,
                                                   T nearVal, T farVal ) {
    tmat4x4<T, defaultp> Result(0);
    Result[0][0] = (static_cast<T>(2) * nearVal) / (right - left);
    Result[1][1] = (static_cast<T>(2) * nearVal) / (top - bottom);
    Result[2][0] = (right + left) / (right - left);
    Result[2][1] = (top + bottom) / (top - bottom);
    Result[2][3] = static_cast<T>(-1);

#if GLM_DEPTH_CLIP_SPACE == GLM_DEPTH_ZERO_TO_ONE
    Result[2][2] = farVal / (nearVal - farVal);
Result[3][2] = -(farVal * nearVal) / (farVal - nearVal);
#else
    Result[2][2] = - (farVal + nearVal) / (farVal - nearVal);
Result[3][2] = - (static_cast<T>(2) * farVal * nearVal) / (farVal - nearVal);
#endif
    return Result;
}

template <typename T>
GLM_FUNC_QUALIFIER tmat4x4<T, defaultp> perspective( T fovy, T aspect, 
                                                     T zNear, T zFar ) {
#if GLM_COORDINATE_SYSTEM == GLM_LEFT_HANDED
    return perspectiveLH(fovy, aspect, zNear, zFar);
#else
    return perspectiveRH(fovy, aspect, zNear, zFar);
#endif
}

template <typename T>
GLM_FUNC_QUALIFIER tmat4x4<T, defaultp> perspectiveRH( T fovy, T aspect, 
                                                       T zNear, T zFar ) {
    assert(abs(aspect - std::numeric_limits<T>::epsilon()) > static_cast<T>(0));
    T const tanHalfFovy = tan(fovy / static_cast<T>(2));
    tmat4x4<T, defaultp> Result(static_cast<T>(0));
    Result[0][0] = static_cast<T>(1) / (aspect * tanHalfFovy);
    Result[1][1] = static_cast<T>(1) / (tanHalfFovy);
    Result[2][3] = - static_cast<T>(1);

#if GLM_DEPTH_CLIP_SPACE == GLM_DEPTH_ZERO_TO_ONE
    Result[2][2] = zFar / (zNear - zFar);
    Result[3][2] = -(zFar * zNear) / (zFar - zNear);
#else
    Result[2][2] = - (zFar + zNear) / (zFar - zNear);
    Result[3][2] = - (static_cast<T>(2) * zFar * zNear) / (zFar - zNear);
#endif
    return Result;
}

template <typename T>
GLM_FUNC_QUALIFIER tmat4x4<T, defaultp> perspectiveLH( T fovy, T aspect,
                                                       T zNear, T zFar ) {
    assert(abs(aspect - std::numeric_limits<T>::epsilon()) > static_cast<T>(0));
    T const tanHalfFovy = tan(fovy / static_cast<T>(2));
    tmat4x4<T, defaultp> Result(static_cast<T>(0));
    Result[0][0] = static_cast<T>(1) / (aspect * tanHalfFovy);
    Result[1][1] = static_cast<T>(1) / (tanHalfFovy);
    Result[2][3] = static_cast<T>(1);

#if GLM_DEPTH_CLIP_SPACE == GLM_DEPTH_ZERO_TO_ONE
    Result[2][2] = zFar / (zFar - zNear);
    Result[3][2] = -(zFar * zNear) / (zFar - zNear);
#else
    Result[2][2] = (zFar + zNear) / (zFar - zNear);
    Result[3][2] = - (static_cast<T>(2) * zFar * zNear) / (zFar - zNear);
#endif
    return Result;
}

template <typename T>
GLM_FUNC_QUALIFIER tmat4x4<T, defaultp> perspectiveFov( T fov, T width, T height, 
                                                        T zNear, T zFar ) {
#if GLM_COORDINATE_SYSTEM == GLM_LEFT_HANDED
    return perspectiveFovLH(fov, width, height, zNear, zFar);
#else
    return perspectiveFovRH(fov, width, height, zNear, zFar);
#endif
}

template <typename T>
GLM_FUNC_QUALIFIER tmat4x4<T, defaultp> perspectiveFovRH( T fov, T width, T height, 
                                                          T zNear, T zFar ) {
    assert(width > static_cast<T>(0));
    assert(height > static_cast<T>(0));
    assert(fov > static_cast<T>(0));

    T const rad = fov;
    T const h = glm::cos(static_cast<T>(0.5) * rad) / glm::sin(static_cast<T>(0.5) * rad);
    T const w = h * height / width; ///todo max(width , Height) / min(width , Height)?

    tmat4x4<T, defaultp> Result(static_cast<T>(0));
    Result[0][0] = w;
    Result[1][1] = h;
    Result[2][3] = - static_cast<T>(1);

#if GLM_DEPTH_CLIP_SPACE == GLM_DEPTH_ZERO_TO_ONE
    Result[2][2] = zFar / (zNear - zFar);
    Result[3][2] = -(zFar * zNear) / (zFar - zNear);
#else
    Result[2][2] = - (zFar + zNear) / (zFar - zNear);
    Result[3][2] = - (static_cast<T>(2) * zFar * zNear) / (zFar - zNear);
#endif
    return Result;
}

template <typename T>
GLM_FUNC_QUALIFIER tmat4x4<T, defaultp> perspectiveFovLH( T fov, T width, T height, 
                                                          T zNear, T zFar ) {
    assert(width > static_cast<T>(0));
    assert(height > static_cast<T>(0));
    assert(fov > static_cast<T>(0));

    T const rad = fov;
    T const h = glm::cos(static_cast<T>(0.5) * rad) / glm::sin(static_cast<T>(0.5) * rad);
    T const w = h * height / width; ///todo max(width , Height) / min(width , Height)?

    tmat4x4<T, defaultp> Result(static_cast<T>(0));
    Result[0][0] = w;
    Result[1][1] = h;
    Result[2][3] = static_cast<T>(1);

#if GLM_DEPTH_CLIP_SPACE == GLM_DEPTH_ZERO_TO_ONE
    Result[2][2] = zFar / (zFar - zNear);
    Result[3][2] = -(zFar * zNear) / (zFar - zNear);
#else
    Result[2][2] = (zFar + zNear) / (zFar - zNear);
    Result[3][2] = - (static_cast<T>(2) * zFar * zNear) / (zFar - zNear);
#endif
    return Result;
}

Before I breakdown the comparison of matrices between your functions and GLM's here is a link to show the difference between indexing a float[16] and a float[4][4].

webstaff: Matrix indexing, C++ and OpenGL

The major difference is you are using a float[16] and GLM is using float[4][4] so the indexing is different but the results should still be the same:

Within your Ortho it appears that you are setting values only down the diagonal and it also appears that you are explicitly working with only one of the handedness but I'm not sure which one you are working with: Is it LH or RH? You are setting the indexes of the 4x4 matrix as such when using a single dimensional array:

// I'll be using (l = left, r = right, t = top, b = bottom, f = zFar, n = zNear)
// f = (clipMax), n = (clipMin)

| 0, 4,  8, 12 |    | (2/(r-l)),         4,          8,  12  | 
| 1, 5,  9, 13 |  = |         1, (2/(t-b)),          9,  13  |
| 2, 6, 10, 14 |    |         2,         6, (-2/(f-n)),  14  |
| 3, 7, 11, 15 |    |         3,         7,          1,  (1) |

Where GLM is branching between which handed coordinate system is being used where they are using a float[4][4] scheme, but they also make another branch decisions based on their #if GLM_DEPTH_CLIP_SPACE == GLM_DEPTH_ZERO_TO_ONE flag so let's see their matrices.

// I'll be using (l = left, r = right, t = top, b = bottom, f = zFar, n = zNear)

// Ortho LH  : IF DEPTH_CLIP_SPACE == ZERO_TO_ONE
| 00, 01, 02, 03 |     |      (2/(r-l)),             01,           02, 03 |
| 10, 11, 12, 13 |  =  |             10,      (2/(t-b)),           12, 13 |
| 20, 21, 22, 23 |     |             20,             21,    (1/(f-n)), 23 |
| 30, 31, 32, 33 |     | (-(r+l)/(r-l)), (-(t+b)/(t-b)), (-(n/(f-n))), 33 |

// Ortho LH  : ELSE 
| 00, 01, 02, 03 |     |      (2/(r-l)),             01,             02, 03 |
| 10, 11, 12, 13 |  =  |             10,      (2/(t-b)),             12, 13 |
| 20, 21, 22, 23 |     |             20,             21,      (2/(f-n)), 23 |
| 30, 31, 32, 33 |     | (-(r+l)/(r-1)), (-(t+b)/(t-b)), (-(f+n)/(f-n)), 33 |


// Ortho RH  : IF DEPTH_CLIP_SPACE == ZERO_TO_ONE    
| 00, 01, 02, 03 |     |      (2/(r-l)),             01,           02, 03 |
| 10, 11, 12, 13 |  =  |             10,      (2/(t-b)),           12, 13 |
| 20, 21, 22, 23 |     |             20,             21,  (-(1/(f-n)), 23 |
| 30, 31, 32, 33 |     | (-(r+l)/(r-l)), (-(t+b)/(t-b)), (-(n/(f-n))), 33 |

// Ortho RH : ELSE
| 00, 01, 02, 03 |     |       (2/r-l)),             01,             02, 03 |
| 10, 11, 12, 13 |  =  |              10,     (2/(t-b)),             12, 13 |
| 20, 21, 22, 23 |     |              20,            21,    (-(2/(f-n)), 23 |
| 30, 31, 32, 33 |     | (-(r+l)/(r-l)), (-(t+b)/(t-b)), (-(f+n)/(f-n)), 33 |

// However they do have a basic Orhto function that doesn't consider 
// the handedness nor the clip space and you can see their matrix here:

// Ortho 
| 00, 01, 02, 03 |     |       (2/r-l)),            01,   02, 03 |
| 10, 11, 12, 13 |  =  |             10,     (2/(t-b)),   12, 13 |
| 20, 21, 22, 23 |     |             20,            21,  (1), 23 |
| 30, 31, 32, 33 |     | (-(r+l)/(r-l)), (-(t+b)/(t-b)),  32, 33 |

Note: - I'm not 100% certain however I do think that the OrthoLH & OrthoRH are designed around 3D where the normal Ortho is designed around 2D as it doesn't take into consideration the perspective divide as well as the depth buffer or z-buffer.


From the above matrices you can see what I have done and now you can compare them; you can do this same exact approach for the perspectives and views between your version and GLMs. I will not do them here as this is already getting to be to long of an answer so, I'll leave them up to you as an exercise. You have to take into consideration which handedness you are using, the clip-space, the frustum, fov, and the type of angles (degrees or radians) that are being used.

After making sure that your matrices are right that isn't the end of it. When you start to apply Affine Transformations (translation, scaling, and rotation) or (skewing) the Order that is done is important and the order will change between coordinate systems. You also have to take into consideration when you are transferring your vertices' information from one matrix to the next; from Model to World to Clip to View(Screen - Camera) space; especially when working in a 3D setting because of the perspective divide where 2D is a bit different since their is no z-component or depth buffer involved. Other things to be aware of are the winding order of your vertices and whether you have back face culling turned on or off as well as blending (transparencies). I hope this helps you find your bug.

EDIT: - I did have an error in one of the GLM Ortho matrices. It was in the orthoRH() in the #if GLM_DEPTH_CLIP_SPACE == GLM_DEPTH_ZERO_TO_ONE for element [3][2], not the #else version. I ended up having (-(f+n)/(f-n)) which is wrong. It is now corrected and updated with the appropriate expression (-(n/(f-n)))

Francis Cugler
  • 7,788
  • 2
  • 28
  • 59
  • Thank you Francis. I would like to use a left handed coordinate model, where Y is up. One thing I was doing wrong was using row-major matrices, whereas OpenGL wants them to be column-major. I think your examples and explanation will help me fix my code, so thanks! – Robin van Ee Mar 07 '18 at 09:38
  • @RobinvanEe Not a problem. I'm self taught in c++ and I have learned both DirectX & OpenGL throughout the years through some books and online tutorials. It's an extremely tough process, takes a lot of time and patience; but in the end once you get the hang of it all, it is worth the reward. Just brush up on your Algebra, Geometry, Trigonometry, Calculus, Linear Algebra, Vector Calculus, Geometrical & Analytical Calculus, Logic & Probability, Affine Transformations, Physics, etc. where building a 3D Game Engine from scratch is the ultimate goal; all of the above are needed. – Francis Cugler Mar 07 '18 at 10:36
  • @RobinvanEe I made an edit to my answer fixing a small but major error. I had to update one of the GLM's Ortho matrices; you can see the edit section for the updated information. – Francis Cugler Mar 07 '18 at 11:11