For the longest time I've wanted to write a somewhat proper introduction to graphics programming with non-ancient details. That wish put together with the graphics course I'm currently undertaking at my campus I might just give it a shot.
I'm not gonna waste my time with teaching anyone linear algebra and/or vector analysis. Instead I'm going to present you with some code here and walk you through it.
This example will be using GLFW and GLEW to manage everything OpenGL-related that isn't about rendering. Why? Because that's the first thing I got running successfully on my laptop.
So once that's set up, we can start with creating a window in a main function.
int main(int argc, char *argv[]) { glfwInit(); glfwOpenWindow(800,600, 0,0,0,0,0,0, GLFW_WINDOW); glfwSetWindowTitle("My Amazing Demo"); glewInit(); init(); bool running = true; while( running ) { display(); running = !glfwGetKey( GLFW_KEY_ESC ) && glfwGetWindowParam( GLFW_OPENED ); } glfwTerminate(); }
It starts of with doing the library initialization. For these two, it has to be done in this order. Next we do our own initialization, we'll come to that later, and after that is our main loop. Simple as pie.
GLfloat vertices[] = { -0.0f,-0.0f,0.0f, -0.8f,0.8f,0.0f, 0.8f,0.8f,0.0f }; GLuint program; GLuint VAO; void init(void) { dumpInfo(); glClearColor(0.2,0.2,0.5,0); glEnable(GL_DEPTH_TEST); glEnable(GL_TEXTURE_2D); program = loadShaders("passthrough.vs", "add_one.gs", "white.fs"); GLuint VBO; glGenVertexArrays(1, &VAO); glBindVertexArray(VAO); glGenBuffers(1, &VBO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, 9*sizeof(GLfloat), vertices, GL_STATIC_DRAW); glVertexAttribPointer(glGetAttribLocation(program, "inPosition"), 3, GL_FLOAT, GL_FALSE, 0, 0); glEnableVertexAttribArray(glGetAttribLocation(program, "inPosition")); }
This is what our init() function looks like. It uses some utility functions to create the shader program. The three strings given are the file names to the vertex shader, geometry shader and fragment shader respectively.
OpenGL works with what it calls Buffers. Buffers are memory locations on the GPU's VRAM. Buffers hold stuff like array indices, vertex positions, texture coordinates and normal informations etc. That's a Vertex Buffer Object, or VBO. There are also, for convenience, Vertex Array Objects (VAO) which holds a bunch of VBOs. They are both generated and then bound with glGenXXXXX and glBindXXXXXX respectively. To generate something is basically to get a handle to it for future reference. This reference is later used to tell the GPU that "this is what I want to work with now", any subsequent call to glBufferData or the likes will affect the latest bound VBO.
glBufferData then uploads the data to the VRAM. This particular call uploads the vertex positions. It requires 9*sizeof(GLfloat) bytes of space and we will be using it as-is (GL_STATIC_DRAW is a hint to OpenGL telling it the data will not be changed, allowing for internal optimizations).
void display(void) { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3); glfwSwapBuffers(); }
The display function is super easy, it just binds the VAO and tells OpenGL to "draw". (With an offset of 0 and a length of 3).
This is basically the minimal code needed to draw something. A triangle to the precise. When you do this later "for real", you will want to put this code in some classes that manages initialization and rendering based on the information in the class. Maybe I'll start working on a tutorial for something like this later.
So I mentioned the Buffers earlier, you know the memory locations on the VRAM? This is the OpenGL 3 way of doing things. Before that, you used calls for every single vertex, every single time. So if/when you look around for more on the internet and you see something like "glBegin()" or "glVertex3fv", please skip it. It's old and obsolete.
The other big change with OpenGL 3+ from the older versions is the mandated use of shaders. Shaders are the programs (written in GLSL) run on the GPU that tells the screen how it should look. Basically.
First in the line is the vertex shader, and the simplest possible is the passthrough shader seen here.
#version 150 in vec3 inPosition; void main(void) { gl_Position = vec4(inPosition, 1.0); }
This just sets the magic gl_Position variable to the incoming inPosition parameter. This happens for every vertex. As in, every 3D coordinate. inPosition gets it's data from the VBO I talked about earlier.
The output from the Vertex Shader is then fed as input to the Geometry shader. The geometry shader is rarely used, but I want to discuss it anyway because it's an important part of my current project. The GS takes some sort of primitive as input, these primitives may be POINTS, LINES or TRIANGLES plus some more information. They can then do stuff with these, like moving them or such. It then outputs new primitives.
#version 150 layout(triangles) in; layout(triangle_strip, max_vertices = 6) out; void main() { // Passthrough stage for(int i = 0; i < gl_in.length(); i++) { gl_Position = gl_in_.gl_Position; EmitVertex(); } EndPrimitive(); // Duplication stage for(int i = 0; i < gl_in.length(); i++) { gl_Position = gl_in_.gl_Position * vec4(1, -1, 1, 1); EmitVertex(); } EndPrimitive(); }
The first layout lines tells us what input and output it takes/makes. We will be getting our input as triangles. Every triangle comes in the gl_PerVerex struct as our gl_in argument.
in gl_PerVertex { vec4 gl_Position; float gl_PointSize; float gl_ClipDistance[]; } gl_in[];
As we can see, it has some properties and it comes as a vector. Every vector is one triangle, ie. it's length is 3. So for the passthrough stage we simple pass the positions on, by writing to gl_Position. The value of gl_Position is then used when the EmitVertex() function is called. This creates a vertex used in the current primitive. The primitive is ended when the EndPrimitive() function is called. So the first loop simply makes one vertex for every coordinate it gets, it then create one primitive using these vertices.
Then comes the interesting part. We duplicate every triangle by flipping it along the Y-axis. We then create a new primitive from the manipulated vertices.
The final shader is the Fragment Shader. It can do A LOT of cool stuff, this is where all lightning is applied and you can do really cool stuff. But we're just gonna make every pixel white.
#version 150 out vec4 outColor; void main(void) { outColor = vec4(1.0); }
This is done by setting the outColor variable to (1, 1, 1, 1). I think outColor is used as "the output" because it's the first out vec4 declared.
The utility files and the complete source code can be found here. It can be compiled with this command.
g++ tut1.c migl.cpp -o tut1 -lGL -lglfw -lGLEW
And once again, it requires GLFW and GLEW.
Here is a picture of the final result