Viewports, Windows, Projections: What's the Difference?
New computer graphics students often have the question about what the viewport actually is. The concept of viewing a virtual scene through a port (or window) is clear to them, but they will often get confused with the difference between the operating system window, the viewport in our graphics API, and the view transformation when first setting up their computer graphics program. We’ll discuss each of these concepts in detail and provide code that sets up each of these aspects of the OpenGL graphics pipeline.
For our purposes, the window is an operating system concept. It describes an object within the window manager that has some type of surrounding functionality, such as maximize and close buttons, and has an interior context into which the content of the window is drawn. Often, when working with standard apps (those that don’t need access to the direct underlying graphics hardware for high-performance drawing), the content of the window is also drawn using operating-system, or window manager APIs. While the window manager controls the windows that are available to the application programmer, we can create windows in a standardized way, without having to learn the API of each window manager separately. This enables us to create windows in a cross-platform way, as well. The two most common ways to create windows when working with OpenGL is to utilize GLFW or SDL.
GLFW
If you’re not creating a game, and don’t need a bunch of extra stuff like sound or keyboard bindings, you’re probably better off using GLFW1. One added bonus is that this will work with Vulkan, as well, if you’re thinking of upgrading to this newer specification. Creating a window with glfw involves a couple of steps. First, you need to initialize the library, of course. Depending on your platform, this might involve importing the appropriate header files (or, in Rust, adding the necessary binding libraries). Next, you need to initialize the glfw library itself. After this, you’ll need to initialize the window you want to draw to. This will create the OpenGL context to which you’ll draw. Finally, you need to poll for changes to the window (resizing and closing, mostly), and perform necessary drawing operations when those happen. This looks like the following in C++:
#include <iostream>
#include <GLFW/glfw3.h>
static void key_callback(GLFWwindow *window, int key, int scancode, int action, int mods)
{
if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
glfwSetWindowShouldClose(window, GLFW_TRUE);
}
int main(int argc, char **argv)
{
GLFWwindow *window;
// Initialize glfw
if (!glfwInit())
{
std::cerr << "Unable to initialize glfw library. This is an unrecoverable error." << std::endl;
return -1;
}
// Create our window. Note that the two arguments at the end are necessary if you want to use
// fullscreen mode, and if you want to share contexts between windows, respectively.
window = glfwCreateWindow(1024, 768, "My GLFW Window", NULL, NULL);
if (!window)
{
std::cerr << "Unable to create window. This is an unrecoverable error." << std::endl;
glfwTerminate();
return -1;
}
// Set our key callback, so we can quit after the user presses the escape button.
glfwSetKeyCallback(window, key_callback);
// Make the window's context current
glfwMakeContextCurrent(window);
// Our render loop, which persists until the user decides to close the window.
while (!glfwWindowShouldClose(window))
{
// Perform rendering operations here
// Swap front and back buffers to present the image that was just drawn.
glfwSwapBuffers(window);
// Poll for events, such as keyboard events, and window events (e.g. the user clicking
// the close button on the window)
glfwPollEvents();
}
// Terminate the instance and clean up any leftover resources held by the library
glfwTerminate();
return 0;
}
Let’s take a look at how this same code looks in Rust:
use glfw::{Action, Context, Key};
use glfw::fail_on_errors;
fn main() {
// Initialize glfw
let mut glfw = glfw::init(fail_on_errors!()).unwrap();
// Create a window in windowed mode that doesn't share its context with any other window. Note
// that if you want a fullscreen window, you need to pass in glfw::WindowMode::Fullscreen
let (mut window, events) = glfw
.create_window(1200, 1024, "My OpenGL Window", glfw::WindowMode::Windowed)
.expect("Failed to create GLFW window");
// Set the window to the current window
window.make_current();
// Poll for key events
window.set_key_polling(true);
// Start our game loop with a named instance so we can break out of it by name
'renderloop: loop {
if window.should_close() {
break 'renderloop;
}
// Do rendering here
// Swap front and back buffers
window.swap_buffers();
// Make sure our window is also polling for events through glfw
glfw.poll_events();
// If the user hits the escape key, then we should close the window
for (_, event) in glfw::flush_messages(&events) {
match event {
glfw::WindowEvent::Key(Key::Escape, _, Action::Press, _) => {
window.set_should_close(true)
}
_ => {}
}
}
}
}
SDL
Now, let’s suppose you’re actually going to be creating a game. In this case, you’ll probably be better off going with what is known as the Simple DirectMedia2 Layer, or, more commonly, SDL. The reason you’d want to use SDL over GLFW has to do with the fact that while slightly more complicated to create windows, SDL actually gives you access to all multimedia functionality of the hardware, including sound, video, threads, file system abstractions, and others. Thus, you get a bit more “bang for your buck” by using this, but only really if you are using it for game development. To set up a window with SDL, we have a similar sort of initialization process. First, we create an SDL context. Next, we utilize the video subsystem to create a window with an OpenGL context within it. After this, we initialize our event loop. Finally, during each iteration of our rendering loop, we poll for events (to see if we should stop rendering), perform our rendering operations, and then swap buffers. Again, let’s take a look at how we’d perform these steps in C++: —
Footnotes
-
glfw
is the spiritual successor to glut, a library that many of us used back in the day when OpenGL still only had a fixed-function pipeline. Technically, it’s still a current specification, but most folks use glfw now instead of glut, since it has some nicer functionality, such as being object-oriented. ↩ -
The DirectMedia portion of this name was a mystery to me, until I asked ChatGPT about it. Apparently, Sam Lantinga’s original idea in 1998 was to provide a middleware library that worked to provide direct access to multimedia such as video and audio, but at the same time keeping it relatively simple. According to ChatGPT, this name was inspired by Microsoft’s DirectX, but was more simple to use and much more portable. ↩
Subscribe to The Nothingness of Scott
Get the latest posts delivered right to your inbox