How to keep the aspect ratio of an OpenGL window constant when it's resized
Something that I had a hard time figuring out when I started working with OpenGL is how to keep the aspect ratio of a window constant when it’s resized. For some reason I couldn’t find information about that topic anywhere. Hopefully this post will help you if you are as lost as I was back then.
Note that I will use the GLFW OpenGL library in my explanation because that’s my favourite library for creating windows, but you can use the ideas that I will present here with any other library.
Below is a screenshot of an application that I finished writing recently. When you first launch it, its window has a width of 1280
pixels and a height of 720
pixels. That means that it has an aspect ratio of
1280 / 720
, or 1.777
.
If I grab the right edge of the window and drag it towards the right, look at what happens to the image:
Ugh, that reminds me of the PowerPoint presentations that I used to make in third grade. The same is the case if I grab the bottom edge of the window and drag it downwards:
The unpleasant stretching you see above is happening because we are allowing the aspect ratio of the image to change. The code that enables that behaviour is really simple. This is how I create the GLFW window:
The cause of the stretching is the framebufferSizeCallback
function, which I defined like this:
To understand why that function results in stretching, let’s do a step-by-step walkthrough of what happens when you grab the right edge of the window and you change its dimensions from 1280 x 720
to 1440 x 720
:
-
GLFW is notified of the change in dimensions, which causes it to resize the front and back framebuffers of the window to cover its new area. Note that those are the framebuffers that you draw into in your render loop, and that you clear and swap with calls like
glClear(GL_COLOR_BUFFER_BIT)
andglfwSwapBuffers(window)
. -
After the framebuffers have been resized, the
framebufferSizeCallback
function is executed. In that function we callglViewport
with the new dimensions of the framebuffers. Remember thatglViewport
is used to specify the area of the framebuffers that we want to use for drawing. By passing it the new dimensions of the framebuffers we are simply saying that we want to use their entire area for drawing. This causes the aspect ratio of our frames to become1440 / 720
, or2.0
. -
That increase in the aspect ratio causes our frames to be stretched horizontally.
So how do we fix this? In short, all we need to do is find the biggest area that we can fit inside the framebuffers that has the original aspect ratio of 1.777
, and then we need to call glViewport
to specify that area.
To understand why that works, think about it this way: we can achieve an aspect ratio of 1.777
with endless combinations of widths and heights. Some examples include 1056 x 594
, 1280 x 720
and 1600 x 900
. By using the biggest of those combinations that fits, our frames will end up covering as much space as possible in the framebuffers and they will have the correct aspect ratio.
At this point you are probably wondering what the results of the fix I described above look like. Here is what happens when I grab the right edge of the window and drag it towards the right:
And here is what happens when I grab the bottom edge of the window and drag it downwards:
You can see that vertical black bars appeared in the first image, and horizontal black bars appeared in the second one (note that you can’t see the top horizontal black bar in the second image because the sky is black in my scene).
To achieve those results my code did exactly what I described before: find the biggest area that fits inside the framebuffers and that has the original aspect ratio of 1.777
, and specify that area using glViewport
.
Although my code did one more thing that I haven’t mentioned: once it found the area, it centered it within the framebuffers. This last step is what results in the vertical and horizontal black bars:
- In the first image the area was centered horizontally, which is why vertical black bars appeared.
- In the second image the area was centered vertically, which is why horizontal black bars appeared.
One thing that I want to clarify is that I’m not making any calls to render those black bars. After the drawing area has been specified using glViewport
, the call to glClear(GL_COLOR_BUFFER_BIT)
that I make to clear the framebuffers in my render loop is still clearing the entire framebuffers, not just the drawing area. So in each iteration of the render loop the entire framebuffers become black, and then we draw the scene in the area specified by glViewport
, leaving the space outside of that area black. That space that we never draw anything in is the black bars. If you change the color that’s used to clear the framebuffers by calling glClearColor
, you can change the color of the bars to anything you like.
So what does the code that implements this behaviour look like? We only need to make changes to the framebufferSizeCallback
function. Let’s start with a simple version of that function that calculates the correct area but doesn’t center it:
With those changes in place, here is what happens when I grab the right edge of the window and drag it towards the right:
And here is what happens when I grab the bottom edge of the window and drag it downwards:
You can see that the aspect ratio is being maintained, but the drawing area isn’t being centered. That’s why there’s only one black bar on the right in the first image, and one black bar on the top in the second image (although you can’t see that one because the sky is black in my scene).
Fixing this is really simple. Below is the same code as before but with the necessary changes to center the viewport:
And that’s it! With the code above you will never see any unpleasant stretching again.
As a bonus, I wanted to discuss one last problem: let’s say that instead of having a black sky in my scene, I wanted it to be blue. I can implement that change by calling glClearColor
with the RGB values of a nice shade of blue, like for example glClearColor(0.036f, 0.627f, 1.0f, 1.0f)
. Now when my framebuffers are cleared in each iteration of my render loop, they become blue instead of black. This works perfectly, but notice what happens when the window is resized horizontally:
The bars on the sides look as if they were part of the sky, which isn’t very nice. It would be better if they were black so that it was clear that they are not part of the scene, but how do we achieve that? It’s actually really easy! First you need to add the following call below the call to glViewport
in the framebufferSizeCallback
function:
And then in your render loop, in the place where you do this:
You need to do this instead:
With those changes in place, our application now looks quite cinematic:
If you would like to see the code that I described here in action, open this link and resize your browser’s window. You will see that the aspect ratio never changes.