Your explanations are amazing, that's what really sets you apart from other youtubers or tutorials in general. It's great to watch this series and get more information on topics that were only covered for a really short time in other tutorials. You're doing an amazing job, keep going!
Thank you so much!!!!!!!!!!!!!! I've been dealing with my scenes stretching due to non-square aspect rations for a WEEK. I literally got two books from my school library just to see if they had something that could help me! I asked three different professors for advice! and you solved my issue in five minutes ;-; DEFINITELY subscribing!
Someone asked me a question but for some reason the comment got deleted. I think the question may be interesting to other people so I'll post it here - "I am confused, I guess face culling happens in window space, and after facing culling only rasterization happens. But you mentioned rasterization happens in NDC space". And the answer is: Sorry for the confusion. The OpenGL spec (www.khronos.org/registry/OpenGL/specs/gl/glspec46.core.pdf - section 14.6.1) says that the decision whether the triangle is front or back facing is the first step of the rasterization process. This is done by calculating the sign of the triangle area in window coordinates. So I guess the rasterizer starts by mapping the three vertices to window coordinates and then it calculates that sign and continues to back face culling. If the triangle was not culled then it can be rasterized.
Thank you for the tutorials, they are really good to dive in OGL and CG as well =) IMHO, if you swap the terms of the denominator in A and B formulas they would look little bit nicer, and should be easier to remember: A = (Zfar + Znear) / (Zfar - Znear) B = -2 * (Zfar Znear) / (Zfar - Znear) also it shows that fact that A is always positive and B is always negative (because Zfar > Znear > 0).
This fixed a lot of issues in my previous code. Previusly, I had to set the Z part in the matrix to 0.9999, so that when it gets divided by w=1, it won't be exactly 1 - which got clipped for me. Now I could also disabe face culling, since depth test could do its job perfectly.
I cant thank enough sir! These videos are EXCELLENT. You are really good at explaining hard concepts. BTW, you look like Mehdi from ElectroBoom. Are you guys brothers? :P
Hi! I really cant get why we use the linear function AZ + B for the projection z. What are the meaning A and B constants. I've watched a lot of different videos about the projection matrix theme and i still confused. I'm really don't want to leave this matrix like a blackbox.
We want to map the visible part of Z (anything within ZNear to ZFar) to the range -1 to 1. Mapping of one range into another usually involves multiplying the mapped value by some term and adding another. So A is the multiplier and B is used for addition.
@@OGLDEV Huge thanks! It looks like i can use here any function what will be change value linear and function AZ + B is the most common explanation of this case
Hi @OGLDEV . I have a problem where I am trying to convert a point in a 2D image (taken within OpenGL) --> 3D point. I am trying to reverse the transformations that OpenGL does to bring 3D->2D but it is not working exactly. I am interested, how you would approach this problem and if you know if it is possible? Thank you in advance :D
@@undefBehav Yes, I'm learning matrix/vector math right now in order to create a full 3D software engine. Been watching the videos of ogldev and OneLoneCoder in order to learn how. What about you? :)
@@andrew_lim Great! I’m still working on improving the collision routines in mine, but still I’m trying to learn 3-D transforms on the side. One thing I’ve come to notice is that matrices do really seem to convolute otherwise a really simple task if you’re going to implement it in software anyway. My go-to approach for rigid body transformations would be Rodrigues’ Axis-Angle rotations (which was way easier for me to wrap my head around than quaternions), and as for perspective transformations, the good old simple perspective divide! :)
at 14:50, why do you do perspective division? shouldn't you solve as is (Az + B) because perspective division by Z will be done automatically after the transformation? the x and y components don't seem to take perspective division into account yet unlike z. why?
Perspective division cannot be done in a matrix multiplication so the designers of the graphics pipeline created a special stage for that. We calculate A and B knowing that perspective division will follow. X and Y are handled in the same way. Their full transformation is (X or Y) / (Z * tan (fov/2)). Z is handled by perspective division so we are left with (X or Y) / tan (fov/2). This is covered in the previous video ruclips.net/video/LhQ85bPCAJ8/видео.html around 11:35.
At 17:57, why now you have both "near = 1.0" AND "d =1 / tanHalfFOV" ? From previous video, I thought that near Z plane is exactly defined as "1 / tanHalfFOV". What does the constant "1.0" brings here?
'd' represents the focal length. This is the distance from the camera to the projection window. The projection window is perpendicular to the viewer so it goes from left to right on the X axis and up-down on the Y axis. In both axes the final projected value must be in the range [-1,1] because this is the range of the rasterizer. Viewport mapping will later transform this to window coorindates (I have a video about it later on in the series). If your field of view angle is large you need to bring the projection closer to the viewer otherwise the projection will go outside of [-1,1] and if it is small you have to push the projection window out. When the field of view angle is 90 degrees we get d=1/tanHalfFOV=1/tan(45)=1/1=1. So this equation in the matrix simply sets the projection window in the correct location. The Near/Far Z are actually not related to the focal length. They just limit the visible range in terms of depth. The near Z can be at 0.1 while the projection window is actually behind it at 1.0. The equations will work the same and points that are close to the near Z will be projected "back" on the projection window. But they will still go to the same location as all other points that are on the same vector from the viewer out. The Z test will handle the ordering. Should probably have picked a different value for the near Z. I can see how it might be confusing. The point is that you can choose your field of view angle independently from the near/far Z.
redirect from ogldev.org, thanks for all your posts on ogldev. one question here, looks like the notation for the perspective matrix is based on LH along with the view matrix, opengl uses RH, the third column should be multiplied by -1?
It's a common misconception that modern OpenGL is either LH or RH but the spec itself doesn't say that there is a default coordinate system. In the past it used to be true where glu created matrices for a RH system. Today if you create the matrices yourself you can decide on the system and just make sure that the models and app are consistent with your decision. If you use GLM also make sure you are consistent with it. So you only multiply by -1 if you are on RH system.
@@OGLDEV Hey Etay, thanks for your replying. Honestly it's quite confusing about the consistency, I did the following test: RH world matrix + LH projection matrix and LH view matrix works as all matrices are RH. does it man we only need to keep projection-view space consistent?
@@hunterize1516 Not sure what a 'RH world matrix' means in this context. In order for the camera to see the object it must be within its frustum. Generally speaking it needs to be "in front of it". So the real question is where is your camera located, what direction it is looking at and where is the object. The -1 in the RH projection matrix copies -Z into into the W of the result vector. So the perspective division flips the sign of the other components. Since the "front" Z was already supposed to be negative it means it becomes positive and now it should be the same as the LH system. I think that the consistency means that if you place objects along the negative Z axis and you expect the camera to see them (let's assume it is already at the origin) you need your view matrix to transform the world so that the camera is looking down the negative Z and use a RH projection matrix. I think I basically said the same thing as you. The consistency in terms of the world is just to construct the scene that is supposed to be in front of the camera in the negative Z half of the 3D system.
@@OGLDEV what I mean the "RH world" is that I set all model vertices and movement of game objects in a world coordiante which is RH (Model matrix), then create a LH perspective projection matrix and LH view matrix intensionally, finnally do coordinate transformation by (LH projection matrix) * (LH view matrix) * (RH Model matrix), it seems to work as all the coordinates are RH like what I've been using in my projects with GLM. I understand the projection matrix should be consistent with view matrix, just curious that they would be consistent with the world coordinate?
@hunterize Is what you're doing equivalent to taking one of my samples and placing the object in the negative Z region? Because if the camera is looking at the positive Z it is not supposed to see it. I think that in order to get to the bottom of it we need to take the parameters of the camera and the position of a single vertex in the model and apply your matrices one by one. You can use Excel or google sheets for that. They have very handle mat mul functions.
I know that this z projection gives greater accuracy at close range (which is often desireable), but could you not theoretically do it linearly just by making Zt = 2*( (Z - Near)/(Far - Near) ) - 1 ?
@@OGLDEV So, I did a little research - it seems to me honestly that the real reason it's non-linear is just so you won't have to either update the matrix to include a changing Z or mutiply the Z value by something afterward. The equation I wrote works fine on a graph, but you couldn't really integrate it with a static matrix. Either you would have to multiply my equation for Zt by Z and put that in the projection matrix such that the Z mult and W div cancel (and make the matrix update constantly) or multiply again after the initial transformation to turn the 1 into the [-1, 1] range. In either case, you're adding some complexity for absolute accuracy that usually isn't usually important.
Sorry for late response. I forgot to mention in my previous comment that setting W to 1 will kill perspective projection for the X and Y. So in that case you would have to do what you said about the static matrix, with the implications that you pointed out.
Hi, I'm having some trouble with the projection matrix... If I don't change it from the identity matrix i still see the spinning cube, but if I do change it to the same matrix as you have it stops showing the cube. I even tried using glm::perspective, but it didn't help. I should have probably written this comment on the last video, but i thought perhaps something in this video would help me, welp it didnt lmao
This can be a problem if you are using a right handed coordinate system (you expect the camera to point towards the negative Z) and the perspective projection matrix is left handed (or vice versa). Use the same position for the object and camera as in my example and compare your perspective projection matrix with mine.
I found out what the problem was. I'm using glm instead of your math library, so there was some problem with matrix transpositions in glUniformMatrix4fv when i used GL_TRUE, so i set it to GL_FALSE and also set glFrontFace to GL_CCW and now it works
Nice! afaik GLM is right handed by default and this affects glUniformMatrix4fv as you've mentioned. The winding order is also something to be aware of and play with CW and CCW until you get it right. Most important thing is to be consistent once the conventions are set.
I am having trouble understanding how to get around this issue. The problem is when a triangle extends past the view frustum, starting from inside the view frustum, going behind the camera. When doing the perspective projection, the new vertex that is behind the camera gets warped around to the front again which results in the objects behind mirroring in front again. When i made my first 3D renderer, i used the CPU with lists of vertices instead of fixed lengths. So what i would do back then, was to duplicate the vertex behind the camera and project one towards one of the vertices that where in front of the camera, onto the near plane. Then do that again with the other one, projecting it towards the second vertex that where in front of the camera. This arrangement would make a quad that would essentially be the same triangle, but that it was cut by the near plane. Here is where the problem arises, now when i switched from CPU to GPU rendering with OpenGL, I can't find a way to do that again, since i cant add or remove vertices via the vertex shader. Could you help? I wanted to keep the same looks and feel as the old one while being compleatly different under the hood. And also, i might just be bad at maths ,but the formula i use for perspective projection is this simple one which gives the same result as a matrix: float t = cameraDist - cameraZ - v.z; float x_p = (cameraDist * v.x) / t - cameraX; float y_p = (cameraDist * v.y) / t - cameraY;
Not sure I fully understood the first problem but this was with a software renderer. Is the problem still happening with OpenGL? The vertex behind the camera should end up being clipped and OpenGL should create a quad or whatever from what remains inside the frustum. It's possible that you don't really need to provide a solution with OpenGL. Regarding the projection equation: what is cameraDist and cameraZ? I guess the division by 't' corresponds to the perspective division that we have in OpenGL. If you look at the perspective projection matrix that we develop in the two videos you can see that most of it is zeroes which means that we usually just multiply each component of the vector by one element (except for Z which is a bit more complex). The advantage of the matrix vs your approach is that you can combine it with the world and view matrices into a single matrix.
@@OGLDEV because i need to have the projection calculation in the vertex shader to maximize performance, i can't control the number of vertices or create new triangles. The divisor 't' is the Z distance from the camera to the vertex, cameraZ is the Z coordiante of the camera, and cameraDist is the "FOV" of the camera, but in a weird way. Think of a pyramid representing the view frustum, the base would be the size of the window and the tip of the pyramid would be the origin of the camera, the height of the pyramid is cameraDist, so if i would want 90 degrees FOV, i would set cameraDist to be half the width of the window. So when a vertex that is behind the origin of the camera is projected, it now gets inverted due to t being negative, a similar thing happens if you write 1 / x in a graphing calculator, where x corresponds to t. The problem i tried to explain was that if i had a large triangle passing over the t = 0 plane, the resulting projection would be warped. The solution would be to either chop the triangle to a quad or to remove the triangle completely, Which is impossible to do inside the vertex shader. So how would that normally be fixed?
Your method seems highly unorthodox. You should transform the vertex to view space and then the camera is at the origin so you don't need to subtract cameraZ from anything. In the vertex shader we usually combine all the transformations together into a single matrix (WVP) and then you don't need all the calculations in your shader below. The projection matrix provides a simple solution for the FOV so it doesn't have to be weird. Everything between the near and far clip plane will be mapped to (-1,1) and everything outside of that range should be handled correctly by the clipper. The vertex shader is indeed not the place to add or remove vertices. The clipper does that for you in screen space.
@@OGLDEV I re-implemented the perspective and rotation matrices to be the same as your implementation, but i keep getting stuck when the triangles just start flickering and popping in and out of existence when rotating, the translation matrix dossen't work as expected either. I have the same order of matrices "perspective * translation * rotation * vec4(position, 1.0);". what i also found was that if i force the w component to be 1.0, it stops flickering but i loose all sense of depth, what could be going on? theese are the matrices i am currently using: float d = 1.0f / (float) Math.tan(FOV / 2.0f); float[] perspective = new float[]{ d, 0.0f, 0.0f, 0.0f, 0.0f, d, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f }; float[] translation = new float[]{ 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f }; float[] rotation = new float[]{ cosA, 0.0f, -sinA, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, sinA, 0.0f, cosA, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f };
for some reason i need to negate A and B even if i have a left handed system (positive z is forward into the screen,i have row-major matrices). do you know why? the view projection i have is simply a translation of -camera position
Try to place the object on a positive location on the Z axis, set the camera at the origin with the target vector on the positive Z. The view matrix should be the identity matrix. Does it work with the original AB or do you still need to negate them? Print the matrices and make sure they are correct.
@@OGLDEV The object is at position 0,0,0 and the camera is at position 0,0,-1. My near is 1.0f and far is 100.0f. If i dont negate A and B I see the object until i reach z = -2 which is weird since i should have a maximum distance of 100.0f. Also my view matrix has not a lookat, its just a translation. This shouldnt be a problem right? I send the view projection matrices separately to opengl transposed.
The distance from the camera to the object is 1 which is exactly on the near Z. This could be a problem. Set the camera at the origin and the object at 5. Does the projection works correctly now?
@@OGLDEV Nope, nothing shows up. maybe its because i consider near -1 and far 1 but sholdn't that be the left coordinate system itself? Anyway, its not a big deal since if i negate everything works i was just trying to understand why that was happening.
Somebody please answer my question, i am trying to figure this out for days now, Why does x values get mapped to an higher interval in the first place?, I though that is because the horizontal field of view is actually gonna be greater and for the same d distance that would map the values to a greater interval. But in matrix implementation we don't even use horizontal field of view, how does that make any sense? I thought we would use horizontal field of view for the x values and then divide by the aspect ratio, instead we still use the vertical field of view, How does it even map the values correctly?
You can structure a matrix using independent horizontal and vertical FOVs but you will probably end up with the two axes being not synchronized with one another - the image will be squeezed on one axis and stretched on the other. So the common practice is to define one FOV (can be either one) and basically match the other FOV dependent on that one through the aspect ratio. Let's assume that the FOV is 90 degrees. This makes tan(FOV/2)=1 and we can ignore this term in the matrix. Let's also assume that our window has a width of 2000 and a height of 1000. So the aspect ratio is 1/2. Now let's see what happens to the point (2,2,2). The matrix has 1 in cell [0][0] so x remains 2 and after perspective division (2/2) we get 1. This means that the point is exactly on the righthand side of the (-1,-1) to (1,1) square which is what eventually gets mapped to the viewport. What about y? In cell [1][1] we get 2 (1/ar) so after multiplying we get 4 and after perspective division we get 2 which is way out of that normalized square. This means that to get to the top of the square and not get clipped the y has to be 1 (when Z is 2). So the bottom line is that when Z=2 the visible horizontal range is -2 to 2 while the vertical range is only -1 to 1. This matches the fact that the height of the window is half the width. We simply have more pixels from left to right than we have from top to bottom.
OK I see the source of the mixup. Looks like my latest code has changed since the time of the video and my initial reply refers to the latest code. The aspect ratio used to be the width/height and it was multiplied by 'd' in cell [0][0] (that's the first cell along the diagonal which is multiplied by x). Now the aspect ratio is height/width and it goes to the denominator of cell [1][1] (the second cell along the diagonal which is multiplied by y). If we use a horizonal field of view we indeed get the range of -old_ar to +old_ar (which is -2 to 2). Now we use a vertical field so the same range is actually -1/new_ar to +1/new_ar which is actually the same.
@@OGLDEV i am so sorry if I am taking too much of your time. But after reading your second reply I got a bit confused again. I am just going to say what i think makes sense and if you can just tell me what is wrong with that approach i would forever be greatful. Let's say we are using the x/y aspect ratio, and in the [1][1] we simply have 1/tan(vfov/2) which makes complete sense it puts y values in -1 to 1 interval. Now for the x if i use horizontal instead of vertical (like you mentioned assume that they are matched) as you just confirmed i would get the interval between -ar to ar and then we got the division by ar which puts it into -1 to 1 interval. So why not use the horizontal instead then? If this works How does the vertical even work?
For anyone interested with a more visual derivation of the math, I made a different kind a video about it: ruclips.net/video/hdv_pnMVaVE/видео.htmlsi=4IffwjaJffDpTQWi
How much are you adding to the angle each frame? Try reducing that. I'm currently not handling window resize events. You need to capture the event and recalculate the projection matrix for the new dimensions.
@@OGLDEV I think maybe because I'm in WSL, 1.0 is maybe too fast, 0.1 is nice thank you! I wonder why this Ubuntu behaves differently with this setting.
@@matt5742 I have zero experience with WSL but I guess it can introduce various subtle differences compared to running natively. Could be a timing issue with the frame rate. In general, it's better to make these things dependent on the elapsed time and thereby workaround platform differences. But it's much easier to just add a constant value ;-)
In the glm implementation of the projection matrix the elements in the third column seem to be negated when compared to the matrix shown at 16:19. Is there a reason for this. My code seems to only work with the glm implementation.
IIRC, glm works with a right handed coordinate system by default. This means we have a -1 in the location you pointed out. In a left handed system we have +1. If your camera points to negative Z this may explain the results you are getting.
Your explanations are amazing, that's what really sets you apart from other youtubers or tutorials in general. It's great to watch this series and get more information on topics that were only covered for a really short time in other tutorials. You're doing an amazing job, keep going!
Thank you!
I had trouble understanding other people's explanations, but yours are very clear. Thank you for your videos!
You're welcome :-)
I wasn't expecting this level of quality. Thank you so much for your content!
You're welcome :-)
Thank you so much!!!!!!!!!!!!!! I've been dealing with my scenes stretching due to non-square aspect rations for a WEEK. I literally got two books from my school library just to see if they had something that could help me! I asked three different professors for advice! and you solved my issue in five minutes ;-;
DEFINITELY subscribing!
Glad to hear!
Yes, I enjoy this video 😊. Watching for the 2nd time, better understanding now. Will watch again in the future.
:-)
Someone asked me a question but for some reason the comment got deleted. I think the question may be interesting to other people so I'll post it here - "I am confused, I guess face culling happens in window space, and after facing culling only rasterization happens. But you mentioned rasterization happens in NDC space".
And the answer is:
Sorry for the confusion. The OpenGL spec (www.khronos.org/registry/OpenGL/specs/gl/glspec46.core.pdf - section 14.6.1) says that the decision whether the triangle is front or back facing is the first step of the rasterization process. This is done by calculating the sign of the triangle area in window coordinates. So I guess the rasterizer starts by mapping the three vertices to window coordinates and then it calculates that sign and continues to back face culling. If the triangle was not culled then it can be rasterized.
Legendary teacher this tutorial is pure gold
Thank you!
thanks for the tutorial! you are a great teacher.
You're welcome :-)
Thank you for the tutorials, they are really good to dive in OGL and CG as well =)
IMHO, if you swap the terms of the denominator in A and B formulas they would look little bit nicer, and should be easier to remember:
A = (Zfar + Znear) / (Zfar - Znear)
B = -2 * (Zfar Znear) / (Zfar - Znear)
also it shows that fact that A is always positive and B is always negative (because Zfar > Znear > 0).
I agree. Thanks!
can't wait for camera episode :)
Me too, so I started working on it ;-)
Best explanation!! I finally understand
Glad to hear that, thank you!! :-)
This fixed a lot of issues in my previous code. Previusly, I had to set the Z part in the matrix to 0.9999, so that when it gets divided by w=1, it won't be exactly 1 - which got clipped for me. Now I could also disabe face culling, since depth test could do its job perfectly.
Great :-)
I cant thank enough sir! These videos are EXCELLENT. You are really good at explaining hard concepts. BTW, you look like Mehdi from ElectroBoom. Are you guys brothers? :P
Thanks! Though I'm not related to ElectroBoom...
Hi! I really cant get why we use the linear function AZ + B for the projection z. What are the meaning A and B constants. I've watched a lot of different videos about the projection matrix theme and i still confused. I'm really don't want to leave this matrix like a blackbox.
We want to map the visible part of Z (anything within ZNear to ZFar) to the range -1 to 1. Mapping of one range into another usually involves multiplying the mapped value by some term and adding another. So A is the multiplier and B is used for addition.
@@OGLDEV Huge thanks! It looks like i can use here any function what will be change value linear and function AZ + B is the most common explanation of this case
thank you very much for the tutorials. At 5:59 you can cast only one of the values to float (the compiler will promote the division output to float).
OK, thanks for the feedback. Usually I prefer to be explicit but it's good to know :-)
Hi @OGLDEV . I have a problem where I am trying to convert a point in a 2D image (taken within OpenGL) --> 3D point. I am trying to reverse the transformations that OpenGL does to bring 3D->2D but it is not working exactly. I am interested, how you would approach this problem and if you know if it is possible? Thank you in advance :D
Check out the Ray Casting video: ruclips.net/video/lj5hx6pa_jE/видео.htmlsi=enwSJVs--hY-XX3w
Great explanation and diagrams
Thank you!
Hey Andrew! :) How is your raycaster project going? Have you abandoned it in favor of a fully 3-D software rasterizer, or something?
@@undefBehav Yes, I'm learning matrix/vector math right now in order to create a full 3D software engine. Been watching the videos of ogldev and OneLoneCoder in order to learn how. What about you? :)
@@andrew_lim Great! I’m still working on improving the collision routines in mine, but still I’m trying to learn 3-D transforms on the side. One thing I’ve come to notice is that matrices do really seem to convolute otherwise a really simple task if you’re going to implement it in software anyway.
My go-to approach for rigid body transformations would be Rodrigues’ Axis-Angle rotations (which was way easier for me to wrap my head around than quaternions), and as for perspective transformations, the good old simple perspective divide! :)
at 14:50, why do you do perspective division? shouldn't you solve as is (Az + B) because perspective division by Z will be done automatically after the transformation? the x and y components don't seem to take perspective division into account yet unlike z. why?
Perspective division cannot be done in a matrix multiplication so the designers of the graphics pipeline created a special stage for that. We calculate A and B knowing that perspective division will follow. X and Y are handled in the same way. Their full transformation is (X or Y) / (Z * tan (fov/2)). Z is handled by perspective division so we are left with (X or Y) / tan (fov/2). This is covered in the previous video ruclips.net/video/LhQ85bPCAJ8/видео.html around 11:35.
@@OGLDEV ok got it thanks bro
At 17:57, why now you have both "near = 1.0" AND "d =1 / tanHalfFOV" ? From previous video, I thought that near Z plane is exactly defined as "1 / tanHalfFOV". What does the constant "1.0" brings here?
'd' represents the focal length. This is the distance from the camera to the projection window. The projection window is perpendicular to the viewer so it goes from left to right on the X axis and up-down on the Y axis. In both axes the final projected value must be in the range [-1,1] because this is the range of the rasterizer. Viewport mapping will later transform this to window coorindates (I have a video about it later on in the series). If your field of view angle is large you need to bring the projection closer to the viewer otherwise the projection will go outside of [-1,1] and if it is small you have to push the projection window out. When the field of view angle is 90 degrees we get d=1/tanHalfFOV=1/tan(45)=1/1=1. So this equation in the matrix simply sets the projection window in the correct location. The Near/Far Z are actually not related to the focal length. They just limit the visible range in terms of depth. The near Z can be at 0.1 while the projection window is actually behind it at 1.0. The equations will work the same and points that are close to the near Z will be projected "back" on the projection window. But they will still go to the same location as all other points that are on the same vector from the viewer out. The Z test will handle the ordering. Should probably have picked a different value for the near Z. I can see how it might be confusing. The point is that you can choose your field of view angle independently from the near/far Z.
@@OGLDEV oh, so they are separate! We just manually adjust them to work?
you really helped me out thank you!!!!
Glad I could help!
redirect from ogldev.org, thanks for all your posts on ogldev. one question here, looks like the notation for the perspective matrix is based on LH along with the view matrix, opengl uses RH, the third column should be multiplied by -1?
It's a common misconception that modern OpenGL is either LH or RH but the spec itself doesn't say that there is a default coordinate system. In the past it used to be true where glu created matrices for a RH system. Today if you create the matrices yourself you can decide on the system and just make sure that the models and app are consistent with your decision. If you use GLM also make sure you are consistent with it. So you only multiply by -1 if you are on RH system.
@@OGLDEV Hey Etay, thanks for your replying. Honestly it's quite confusing about the consistency, I did the following test: RH world matrix + LH projection matrix and LH view matrix works as all matrices are RH. does it man we only need to keep projection-view space consistent?
@@hunterize1516 Not sure what a 'RH world matrix' means in this context. In order for the camera to see the object it must be within its frustum. Generally speaking it needs to be "in front of it". So the real question is where is your camera located, what direction it is looking at and where is the object. The -1 in the RH projection matrix copies -Z into into the W of the result vector. So the perspective division flips the sign of the other components. Since the "front" Z was already supposed to be negative it means it becomes positive and now it should be the same as the LH system. I think that the consistency means that if you place objects along the negative Z axis and you expect the camera to see them (let's assume it is already at the origin) you need your view matrix to transform the world so that the camera is looking down the negative Z and use a RH projection matrix. I think I basically said the same thing as you. The consistency in terms of the world is just to construct the scene that is supposed to be in front of the camera in the negative Z half of the 3D system.
@@OGLDEV what I mean the "RH world" is that I set all model vertices and movement of game objects in a world coordiante which is RH (Model matrix), then create a LH perspective projection matrix and LH view matrix intensionally, finnally do coordinate transformation by (LH projection matrix) * (LH view matrix) * (RH Model matrix), it seems to work as all the coordinates are RH like what I've been using in my projects with GLM. I understand the projection matrix should be consistent with view matrix, just curious that they would be consistent with the world coordinate?
@hunterize Is what you're doing equivalent to taking one of my samples and placing the object in the negative Z region? Because if the camera is looking at the positive Z it is not supposed to see it. I think that in order to get to the bottom of it we need to take the parameters of the camera and the position of a single vertex in the model and apply your matrices one by one. You can use Excel or google sheets for that. They have very handle mat mul functions.
I know that this z projection gives greater accuracy at close range (which is often desireable), but could you not theoretically do it linearly just by making Zt = 2*( (Z - Near)/(Far - Near) ) - 1 ?
You need to "get rid" of perspective division by setting W to 1. The division by Z is what makes Z non-linear. Why do you want to do that?
@@OGLDEV So, I did a little research - it seems to me honestly that the real reason it's non-linear is just so you won't have to either update the matrix to include a changing Z or mutiply the Z value by something afterward. The equation I wrote works fine on a graph, but you couldn't really integrate it with a static matrix. Either you would have to multiply my equation for Zt by Z and put that in the projection matrix such that the Z mult and W div cancel (and make the matrix update constantly) or multiply again after the initial transformation to turn the 1 into the [-1, 1] range. In either case, you're adding some complexity for absolute accuracy that usually isn't usually important.
Sorry for late response. I forgot to mention in my previous comment that setting W to 1 will kill perspective projection for the X and Y. So in that case you would have to do what you said about the static matrix, with the implications that you pointed out.
Amazing videos! Thanks a lot.
Thank you 😄
Awesome video
Thanks!
Hi, I'm having some trouble with the projection matrix... If I don't change it from the identity matrix i still see the spinning cube, but if I do change it to the same matrix as you have it stops showing the cube. I even tried using glm::perspective, but it didn't help.
I should have probably written this comment on the last video, but i thought perhaps something in this video would help me, welp it didnt lmao
This can be a problem if you are using a right handed coordinate system (you expect the camera to point towards the negative Z) and the perspective projection matrix is left handed (or vice versa). Use the same position for the object and camera as in my example and compare your perspective projection matrix with mine.
I found out what the problem was. I'm using glm instead of your math library, so there was some problem with matrix transpositions in glUniformMatrix4fv when i used GL_TRUE, so i set it to GL_FALSE and also set glFrontFace to GL_CCW and now it works
Nice! afaik GLM is right handed by default and this affects glUniformMatrix4fv as you've mentioned. The winding order is also something to be aware of and play with CW and CCW until you get it right. Most important thing is to be consistent once the conventions are set.
I am having trouble understanding how to get around this issue. The problem is when a triangle extends past the view frustum, starting from inside the view frustum, going behind the camera. When doing the perspective projection, the new vertex that is behind the camera gets warped around to the front again which results in the objects behind mirroring in front again.
When i made my first 3D renderer, i used the CPU with lists of vertices instead of fixed lengths. So what i would do back then, was to duplicate the vertex behind the camera and project one towards one of the vertices that where in front of the camera, onto the near plane. Then do that again with the other one, projecting it towards the second vertex that where in front of the camera. This arrangement would make a quad that would essentially be the same triangle, but that it was cut by the near plane. Here is where the problem arises, now when i switched from CPU to GPU rendering with OpenGL, I can't find a way to do that again, since i cant add or remove vertices via the vertex shader. Could you help? I wanted to keep the same looks and feel as the old one while being compleatly different under the hood.
And also, i might just be bad at maths ,but the formula i use for perspective projection is this simple one which gives the same result as a matrix:
float t = cameraDist - cameraZ - v.z;
float x_p = (cameraDist * v.x) / t - cameraX;
float y_p = (cameraDist * v.y) / t - cameraY;
Not sure I fully understood the first problem but this was with a software renderer. Is the problem still happening with OpenGL? The vertex behind the camera should end up being clipped and OpenGL should create a quad or whatever from what remains inside the frustum. It's possible that you don't really need to provide a solution with OpenGL.
Regarding the projection equation: what is cameraDist and cameraZ? I guess the division by 't' corresponds to the perspective division that we have in OpenGL. If you look at the perspective projection matrix that we develop in the two videos you can see that most of it is zeroes which means that we usually just multiply each component of the vector by one element (except for Z which is a bit more complex). The advantage of the matrix vs your approach is that you can combine it with the world and view matrices into a single matrix.
@@OGLDEV because i need to have the projection calculation in the vertex shader to maximize performance, i can't control the number of vertices or create new triangles. The divisor 't' is the Z distance from the camera to the vertex, cameraZ is the Z coordiante of the camera, and cameraDist is the "FOV" of the camera, but in a weird way. Think of a pyramid representing the view frustum, the base would be the size of the window and the tip of the pyramid would be the origin of the camera, the height of the pyramid is cameraDist, so if i would want 90 degrees FOV, i would set cameraDist to be half the width of the window.
So when a vertex that is behind the origin of the camera is projected, it now gets inverted due to t being negative, a similar thing happens if you write 1 / x in a graphing calculator, where x corresponds to t. The problem i tried to explain was that if i had a large triangle passing over the t = 0 plane, the resulting projection would be warped. The solution would be to either chop the triangle to a quad or to remove the triangle completely, Which is impossible to do inside the vertex shader. So how would that normally be fixed?
if you are wondering, here is my vertex shader:
#version 330 core
uniform float matrix[6];
uniform vec3 translate;
uniform float renderdist;
uniform float minrenderdist;
uniform vec3 camera;
uniform float cameradist;
uniform vec2 size;
uniform int prioaxis;
// Input vertex attributes
layout(location = 0) in vec3 position; // Position attribute
layout(location = 1) in vec4 color;
vec3 rotate(vec3 v) {
float nx;
float ny;
vec3 o;
if (prioaxis == 0) {
// Y
o = vec3(matrix[3] * v.x + matrix[2] * v.z, v.y, matrix[3] * v.z - matrix[2] * v.x);
// Z
nx = matrix[5] * o.x - matrix[4] * o.y;
o.y = matrix[4] * o.x + matrix[5] * o.y;
o.x = nx;
// X
ny = matrix[1] * o.y - matrix[0] * o.z;
o.z = matrix[0] * o.y + matrix[1] * o.z;
o.y = ny;
} else if (prioaxis == 1) {
// Z
o = vec3(matrix[5] * v.x - matrix[4] * v.y, matrix[4] * v.x + matrix[5] * v.y, v.z);
// X
ny = matrix[1] * o.y - matrix[0] * o.z;
o.z = matrix[0] * o.y + matrix[1] * o.z;
o.y = ny;
// Y
nx = matrix[3] * o.x + matrix[2] * o.z;
o.z = matrix[3] * o.z - matrix[2] * o.x;
o.x = nx;
} else {
// X
o = vec3(v.x, matrix[1] * v.y - matrix[0] * v.z, matrix[0] * v.y + matrix[1] * v.z);
// Y
nx = matrix[3] * o.x + matrix[2] * o.z;
o.z = matrix[3] * o.z - matrix[2] * o.x;
o.x = nx;
// Z
nx = matrix[5] * o.x - matrix[4] * o.y;
o.y = matrix[4] * o.x + matrix[5] * o.y;
o.x = nx;
}
return o;
}
vec2 project(vec3 v) {
float t = cameradist - camera.z - v.z;
if (t > minrenderdist) {
float x1 = ((cameradist * v.x) / t) - camera.x;
float y1 = ((cameradist * v.y) / t) - camera.y;
return vec2(x1 / size.x, y1 / size.y);
} else {
return vec2(0.0, 0.0);
}
}
out vec4 col;
void main() {
vec3 rotated = rotate(position + translate);
vec2 projected = project(rotated);
float t = cameradist - camera.z - rotated.z;
gl_Position = vec4(projected, 1.0 - 1.0 / t, 1.0);
col = color;
}
Your method seems highly unorthodox. You should transform the vertex to view space and then the camera is at the origin so you don't need to subtract cameraZ from anything. In the vertex shader we usually combine all the transformations together into a single matrix (WVP) and then you don't need all the calculations in your shader below. The projection matrix provides a simple solution for the FOV so it doesn't have to be weird. Everything between the near and far clip plane will be mapped to (-1,1) and everything outside of that range should be handled correctly by the clipper. The vertex shader is indeed not the place to add or remove vertices. The clipper does that for you in screen space.
@@OGLDEV I re-implemented the perspective and rotation matrices to be the same as your implementation, but i keep getting stuck when the triangles just start flickering and popping in and out of existence when rotating, the translation matrix dossen't work as expected either. I have the same order of matrices "perspective * translation * rotation * vec4(position, 1.0);". what i also found was that if i force the w component to be 1.0, it stops flickering but i loose all sense of depth, what could be going on? theese are the matrices i am currently using:
float d = 1.0f / (float) Math.tan(FOV / 2.0f);
float[] perspective = new float[]{
d, 0.0f, 0.0f, 0.0f,
0.0f, d, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f
};
float[] translation = new float[]{
1.0f, 0.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f
};
float[] rotation = new float[]{
cosA, 0.0f, -sinA, 0.0f,
0.0f, 1.0f, 0.0f, 0.0f,
sinA, 0.0f, cosA, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f
};
for some reason i need to negate A and B even if i have a left handed system (positive z is forward into the screen,i have row-major matrices). do you know why?
the view projection i have is simply a translation of -camera position
Try to place the object on a positive location on the Z axis, set the camera at the origin with the target vector on the positive Z. The view matrix should be the identity matrix. Does it work with the original AB or do you still need to negate them? Print the matrices and make sure they are correct.
@@OGLDEV The object is at position 0,0,0 and the camera is at position 0,0,-1. My near is 1.0f and far is 100.0f. If i dont negate A and B I see the object until i reach z = -2 which is weird since i should have a maximum distance of 100.0f. Also my view matrix has not a lookat, its just a translation. This shouldnt be a problem right? I send the view projection matrices separately to opengl transposed.
The distance from the camera to the object is 1 which is exactly on the near Z. This could be a problem. Set the camera at the origin and the object at 5. Does the projection works correctly now?
@@OGLDEV Nope, nothing shows up. maybe its because i consider near -1 and far 1 but sholdn't that be the left coordinate system itself? Anyway, its not a big deal since if i negate everything works i was just trying to understand why that was happening.
@@CL-pg6iu Both near and far z must be positive.
Somebody please answer my question, i am trying to figure this out for days now, Why does x values get mapped to an higher interval in the first place?, I though that is because the horizontal field of view is actually gonna be greater and for the same d distance that would map the values to a greater interval. But in matrix implementation we don't even use horizontal field of view, how does that make any sense? I thought we would use horizontal field of view for the x values and then divide by the aspect ratio, instead we still use the vertical field of view, How does it even map the values correctly?
You can structure a matrix using independent horizontal and vertical FOVs but you will probably end up with the two axes being not synchronized with one another - the image will be squeezed on one axis and stretched on the other. So the common practice is to define one FOV (can be either one) and basically match the other FOV dependent on that one through the aspect ratio. Let's assume that the FOV is 90 degrees. This makes tan(FOV/2)=1 and we can ignore this term in the matrix. Let's also assume that our window has a width of 2000 and a height of 1000. So the aspect ratio is 1/2. Now let's see what happens to the point (2,2,2). The matrix has 1 in cell [0][0] so x remains 2 and after perspective division (2/2) we get 1. This means that the point is exactly on the righthand side of the (-1,-1) to (1,1) square which is what eventually gets mapped to the viewport. What about y? In cell [1][1] we get 2 (1/ar) so after multiplying we get 4 and after perspective division we get 2 which is way out of that normalized square. This means that to get to the top of the square and not get clipped the y has to be 1 (when Z is 2). So the bottom line is that when Z=2 the visible horizontal range is -2 to 2 while the vertical range is only -1 to 1. This matches the fact that the height of the window is half the width. We simply have more pixels from left to right than we have from top to bottom.
OK I see the source of the mixup. Looks like my latest code has changed since the time of the video and my initial reply refers to the latest code. The aspect ratio used to be the width/height and it was multiplied by 'd' in cell [0][0] (that's the first cell along the diagonal which is multiplied by x). Now the aspect ratio is height/width and it goes to the denominator of cell [1][1] (the second cell along the diagonal which is multiplied by y). If we use a horizonal field of view we indeed get the range of -old_ar to +old_ar (which is -2 to 2). Now we use a vertical field so the same range is actually -1/new_ar to +1/new_ar which is actually the same.
@@OGLDEV I get it now thank you
np
@@OGLDEV i am so sorry if I am taking too much of your time. But after reading your second reply I got a bit confused again. I am just going to say what i think makes sense and if you can just tell me what is wrong with that approach i would forever be greatful. Let's say we are using the x/y aspect ratio, and in the [1][1] we simply have 1/tan(vfov/2) which makes complete sense it puts y values in -1 to 1 interval. Now for the x if i use horizontal instead of vertical (like you mentioned assume that they are matched) as you just confirmed i would get the interval between -ar to ar and then we got the division by ar which puts it into -1 to 1 interval. So why not use the horizontal instead then? If this works How does the vertical even work?
For anyone interested with a more visual derivation of the math, I made a different kind a video about it:
ruclips.net/video/hdv_pnMVaVE/видео.htmlsi=4IffwjaJffDpTQWi
Why does mine spin fast? It also spins faster if resize the window
How much are you adding to the angle each frame? Try reducing that.
I'm currently not handling window resize events. You need to capture the event and recalculate the projection matrix for the new dimensions.
@@OGLDEV I think maybe because I'm in WSL, 1.0 is maybe too fast, 0.1 is nice thank you! I wonder why this Ubuntu behaves differently with this setting.
@@matt5742 I have zero experience with WSL but I guess it can introduce various subtle differences compared to running natively. Could be a timing issue with the frame rate. In general, it's better to make these things dependent on the elapsed time and thereby workaround platform differences. But it's much easier to just add a constant value ;-)
-"Pretty sure you don't wanna play fps like that"
-cs pros 🤪
My analysis shows that 87% of the gamers use ultrawide monitors ;-)
In the glm implementation of the projection matrix the elements in the third column seem to be negated when compared to the matrix shown at 16:19. Is there a reason for this.
My code seems to only work with the glm implementation.
IIRC, glm works with a right handed coordinate system by default. This means we have a -1 in the location you pointed out. In a left handed system we have +1. If your camera points to negative Z this may explain the results you are getting.