Also see OpenGL rendering pipeline.

As a state machine

OpenGL is often thought of as a state machine. Because it is one.

  • When we create an OpenGL context, we are default initializing the state machine. When we talk about the “current OpenGL context” we are talking about the state.
  • Multiple OpenGL contexts can be opened (remember, just state machines) at the same time, under the same application.

The OpenGL API is really just a way for us to update, modify, and query the current state of the context. (Apart from the functions that draw stuff. Those instead consume the current state.)

OpenGL objects, binding, and targets

The API also provides us methods to create OpenGL objects and modify the ones that are bound to the current context.

Objects are simply a way of grouping related state together. I guess it got annoying having to individually update every single parameter. Now we can update multiple with a single function call.

We can “set” an object as “active” by binding it to the current context. Each OpenGL context has “slots” that objects can be bound to; these are called targets. Then, the state contained within the object also becomes the current global state.

If they’re not bound, then they simply float around in GPU memory. I think. We get a pointer (usually a GLuint) to a specific object on the CPU side when we call the corresponding glGen* command.

How that pointer is given to us in the first place depends on how Apple, AMD, NVIDIA, etc. decided to implement it via their drivers.

Since OpenGL 4.5 (released in August 11, 2014), it is now also possible to modify object state without having to bind it first. This is called direct state access (DSA). You still need to bind it to the global context for it to actually affect the rendering process, I think.

VBOs, VAOs, vertex attributes

This has really good information: https://stackoverflow.com/a/26559063

“VBO” as an actual OpenGL concept does not actually exist; it’s just that the most common use for buffer objects, which is an actual OpenGL thing, is to store vertex attribute data. Hence a “vertex buffer object,” which we bind by passing in GL_ARRAY_BUFFER in glBindBuffer().

Again, VBOs are simply buffer objects. To the GPU, this is just a buffer of floats with no meaning; it does not know what a position or normal is.

  • Therefore, to associate an attribute in the vertex shader with a subset of the floats from the buffer, we need to explicitly tell that to OpenGL.
  • This is done via glVertexAttribPointer() and glEnableVertexAttribArray(), because vertex attributes are disabled by default. Both of these functions take in the attribute “location,” which can either be explicitly set in the shader, or requested via glGetAttribLocation() followed by the attribute name in the shader.
  • We need to do this for every single vertex attribute used in the shader.
  • OpenGL guarantees 16 4-component vertex attributes that are available in the shader. Note that, for example, a 4x4 matrix is technically a 4 4-component vertex attribute.

In the past you would have to do this for every single vertex attribute, every single time a VBO was bound to GL_ARRAY_BUFFER. This was very annoying to do and is a lot of overhead for setting state.

Remembering that OpenGL objects simply store state that we can use for later, vertex array objects (VAO) do just that: it remembers which vertex attributes are enabled, their size, stride, etc., as well as the VBOs where the data will be sourced from.

  • In case we’re performing indexed rendering, VAOs also remember the element buffer object (EBO) that indices will be coming from. In fact, indexed rendering only works with a VAO.
  • Note that VAOs explicitly remember the EBO but implicitly know about the VBOs. That is, the buffer used for each attribute is tracked indirectly, based on which buffer was bound when glVertexAttribPointer() is called.
  • VAOs were made mandatory for draw calls (glDrawArrays and glDrawElements) in OpenGL 3.0.

So, we can simply bind a VAO to the current context and all that state will be set everytime for us, instead of having to do it manually.

GLFW and GLAD

What’s the point of these frameworks/libraries? Why does everyone use them?

Window handling and context creation

While OpenGL only deals with rendering, on actual operating systems there’s a lot more involved: opening a window to render into, handling window operations (resizing, minimizing, etc.), detecting keyboard input, and general communication with the OS.

This is what GLFW aims to be. While I’m only on Windows, it abstracts away specific window operations and provides the same, generic API for Windows, macOS, and Linux, because each one does it a bit differently.

  • Note that GLFW also handles the creation of an OpenGL context.
  • Creating a window and creating a context are basically synonymous, because creating an OpenGL context without a window to render into is pointless, and creating a window without an active context means nothing will be displayed.

Function and extension loader

Another thing each OS does differently is the base OpenGL version that comes with the system. For example, by default Windows comes with a OpenGL 1.1 software renderer. So even if we’ve handled window and context creation via GLFW and all that, we might not be using an up to date GL version!

To get access to functions and GL extensions that exist in newer GL versions, we need to request them from the OS (assuming your drivers are up to date and you have a GPU that supports newer versions, which at this point is universal).

Again, this is different for every system; therefore, loaders like GLAD abstract away this process and does it for us, so we can just use the gl* functions without having to worry about if they exist in the current context. It fetches the functions at runtime by returning a pointer to them.

Immediate mode, fixed function pipeline

When OpenGL was first introduced, the envisioned rendering pipeline and graphics model was completely different from modern OpenGL today. When someone says “immediate/direct mode” or the “fixed function pipeline” they’re referring to a set of OpenGL functions that are from the older way of thinking.

  • First started moving away from this model with the introduction of shaders in OpenGL 2.0
  • Depreciated in 3.0, completely removed in 3.1

Note that immediate mode and fixed function are referring to two different concepts that were both removed in 3.1.

  • Immediate mode is when you provide OpenGL vertex data one by one instead of, for example, using a buffer. A good sign: glBegin and glEnd.
  • Fixed function pipeline refers to how OpenGL used to only come with a few predefined lighting models (flat shading, Blinn-Phong lighting) and you could only adjust parameters that they would use.
  • With shaders, you now have to write your own lighting pipeline (or reimplement Blinn-Phong or flat shading which isn’t too difficult). It’s not fixed anymore.

Core and compatibility profiles

The old API and functions were reintroduced in 3.2 with the concept of Core and Compatibility profiles. Also commonly referred to as “core context” and “compatibility context” because depending on which mode you’re in, I guess OpenGL’s state machine also differs.

  • Core context does not include all previously depreciated functions.
  • Compatibility context does include them, but many older functions cannot interact with newer features (e.g. tessellation stage always uses shaders).

Technically, only the Core profile is required to be implemented. In practice, every OpenGL driver also implements the compatibility profile, so even if the fixed function pipeline was “removed” in 3.1, all of OpenGL still remains backwards compatible, ish.

Also see