Before we can begin looking into writing an OpenGL application, we must first know what it is that we are writing. What exactly is OpenGL?
OpenGL is usually thought of as an Application Programming Interface (API). The OpenGL API has been exposed to a number of languages. But the one that they all ultimately use at their lowest level is the C API.
The API, in C, is defined by a number of typedefs, #defined enumerator values, and functions. The typedefs define basic GL types like GLint, GLfloat and so forth. These are defined to have a specific bit depth.
Complex aggregates like structs are never directly exposed in OpenGL. Any such constructs are hidden behind the API. This makes it easier to expose the OpenGL API to non-C languages without having a complex conversion layer.
In C++, if you wanted an object that contained an integer, a float, and a string, you would create it and access it like this:
struct Object { int count; float opacity; char *name; }; //Create the storage for the object. Object newObject; //Put data into the object. newObject.count = 5; newObject.opacity = 0.4f; newObject.name = "Some String";
In OpenGL, you would use an API that looks more like this:
//Create the storage for the object GLuint objectName; glGenObject(1, &objectName); //Put data into the object. glBindObject(GL_MODIFY, objectName); glObjectParameteri(GL_MODIFY, GL_OBJECT_COUNT, 5); glObjectParameterf(GL_MODIFY, GL_OBJECT_OPACITY, 0.4f); glObjectParameters(GL_MODIFY, GL_OBJECT_NAME, "Some String");
None of these are actual OpenGL commands, of course. This is simply an example of what the interface to such an object would look like.
OpenGL owns the storage for all OpenGL objects. Because of this, the user can only
access an object by reference. Almost all OpenGL objects are referred to by an
unsigned integer (the GLuint). Objects are created by a function of the
form glGen*
, where * is the type of the object. The first
parameter is the number of objects to create, and the second is a
GLuint* array that receives the newly created object names.
To modify most objects, they must first be bound to the context. Many objects can
be bound to different locations in the context; this allows the same object to be
used in different ways. These different locations are called
targets; all objects have a list of valid targets, and
some have only one. In the above example, the fictitious target
“GL_MODIFY” is the location where objectName
is
bound.
The enumerators GL_OBJECT_*
all name fields in the object that
can be set. The glObjectParameter
family of functions set
parameters within the object bound to the given target. Note that since OpenGL is a
C API, it has to name each of the differently typed variations differently. So there
is glObjectParameteri
for integer parameters,
glObjectParameterf
for floating-point parameters, and so
forth.
Note that all OpenGL objects are not as simple as this example, and the functions that change object state do not all follow these naming conventions. Also, exactly what it means to bind an object to the context is explained below.
The OpenGL API is defined as a state machine. Almost all of the OpenGL functions set or retrieve some state in OpenGL. The only functions that do not change state are functions that use the currently set state to cause rendering to happen.
You can think of the state machine as a very large struct with a great many different fields. This struct is called the OpenGL context, and each field in the context represents some information necessary for rendering.
Objects in OpenGL are thus defined as a list of fields in this struct that can be saved and restored. Binding an object to a target within the context causes the data in this object to replace some of the context's state. Thus after the binding, future function calls that read from or modify this context state will read or modify the state within the object.
Objects are usually represented as GLuint integers; these are handles to the actual OpenGL objects. The integer value 0 is special; it acts as the object equivalent of a NULL pointer. Binding object 0 means to unbind the currently bound object. This means that the original context state, the state that was in place before the binding took place, now becomes the context state.
Let us say that this represents some part of an OpenGL context's state:
Example 1. OpenGL Object State
struct Values { int iValue1; int iValue2; }; struct OpenGL_Context { ... Values *pMainValues; Values *pOtherValues; ... }; OpenGL_Context context;
To create a Values
object, you would call something like
glGenValues
. You could bind the
Values
object to one of two targets:
GL_MAIN_VALUES
which represents the pointer
context.pMainValues
, and GL_OTHER_VALUES
which represents the pointer context.pOtherValues
. You would bind
the object with a call to glBindValues
, passing one of the two
targets and the object. This would set that target's pointer to the object that you
created.
There would be a function to set values in a bound object. Say,
glValueParam
. It would take the target of the object, which
represents the pointer in the context. It would also take an enum representing which
value in the object to change. The value GL_VALUE_ONE
would
represent iValue1
, and GL_VALUE_TWO
would
represent iValue2
.
To be technical about it, OpenGL is not an API; it is a specification. A document. The C API is merely one way to implement the spec. The specification defines the initial OpenGL state, what each function does to change or retrieve that state, and what is supposed to happen when you call a rendering function.
The specification is written by the OpenGL Architectural Review Board (ARB), a group of representatives from companies like Apple, NVIDIA, and AMD (the ATI part), among others. The ARB is part of the Khronos Group.
The specification is a very complicated and technical document. However, parts of it are quite readable, though you will usually need at least some understanding of what should be going on to understand it. If you try to read it, the most important thing to understand about it is this: it describes results, not implementation. Just because the spec says that X will happen does not mean that it actually does. What it means is that the user should not be able to tell the difference. If a piece of hardware can provide the same behavior in a different way, then the specification allows this, so long as the user can never tell the difference.
OpenGL Implementations. While the OpenGL ARB does control the specification, it does not control OpenGL's code. OpenGL is not something you download from a centralized location. For any particular piece of hardware, it is up to the developers of that hardware to write an OpenGL Implementation for that hardware. Implementations, as the name suggests, implement the OpenGL specification, exposing the OpenGL API as defined in the spec.
Who controls the OpenGL implementation is different for different operating systems. On Windows, OpenGL implementations are controlled virtually entirely by the hardware makers themselves. On Mac OSX, OpenGL implementations are controlled by Apple; they decide what version of OpenGL is exposed and what additional functionality can be provided to the user. Apple writes much of the OpenGL implementation on Mac OSX, which the hardware developers writing to an Apple-created internal driver API. On Linux, things are... complicated.
The long and short of this is that if you are writing a program and it seems to be exhibiting off-spec behavior, that is the fault of the maker of your OpenGL implementation (assuming it is not a bug in your code). On Windows, the various graphics hardware makers put their OpenGL implementations in their regular drivers. So if you suspect a bug in their implementation, the first thing you should do is make sure your graphics drivers are up-to-date; the bug may have been corrected since the last time you updated your drivers.
OpenGL Versions. There are many versions of the OpenGL Specification. OpenGL versions are not like most Direct3D versions, which typically change most of the API. Code that works on one version of OpenGL will almost always work on later versions of OpenGL.
The only exception to this deals with OpenGL 3.0 and above, relative to previous versions. v3.0 deprecated a number of older functions, and v3.1 removed most of those functions from the API[1]. This also divided the specification into 2 variations (called profiles): core and compatibility. The compatibility profile retains all of the functions removed in 3.1, while the core profile does not. Theoretically, OpenGL implementations could implement just the core profile; this would leave software that relies on the compatibility profile non-functional on that implementation.
As a practical matter, none of this matters at all. No OpenGL driver developer is going to ship drivers that only implement the core profile. So in effect, this means nothing at all; all OpenGL versions are all effectively backwards compatible.
[1] Deprecation only means marking those functions as to be removed in later functions. They are still available for use in 3.0.