GL 3.1 tutorial [Work In Progress]

Hello and welcome to this OpenGL 3.1 tutorial. It is designed as a quickstart into "modern" OpenGL. It is not meant as a math or shading tutorial (or even a proper OpenGL tutorial, mind you). Instead I've put together a small codebase that does something you can actually see and work with. Think of it as "hello, cube", a graphic version of "hello, world"
With version 3.0 of OpenGL a new deprecation model has been introduced. According to this deprecation model, the biggest change is the removal of the fixed function pipeline and it's related state. If you're starting off with OpenGL then you shouldn't bother with this deprecated functionality at all. If you've worked with the older GL way of things and you're asking yourself "Why remove it and yet do it manually afterwards?" then the answer is simple: more flexibility and an increased ease of use overall. You'll realise that as your project gets bigger.
If you want to see this example running, launch OpenTK's examples browser and run the OpenGL 3.0 example (not available on 0.9.9.0 and older).
Ok, enough talking let's get started.
To compile and run this tutorial you will need (the latest version of/version 0.9.9.0 of) the OpenTK library. To keep things easy we will use the GameWindow class which takes care of proper context and window creation, provides render and resize events to hook on and so forth.

using System;
using System.Diagnostics;
using OpenTK;
using OpenTK.Graphics;
using OpenTK.Math;
 
namespace OpenGL3
{
    public class HelloGL3: GameWindow
    {

1. Shaders
As stated above, OpenGL no longer provides a default "you needn't mess with" algorithm for processing vertices and fragments. Instead, we have to take care of these computations.
http://en.wikipedia.org/wiki/Graphics_pipeline
http://en.wikipedia.org/wiki/GLSL
1.1 Vertex Shader
The vertex shader operates on every vertex and it's responsible for processing vertex positions, normals, texture coordinates and so forth. This processing includes: model transformations (translation, rotating, scaling and shearing of the 3D model), per-vertex lighting (determining what and how much light reaches the vertex) and viewing transformations (transforming the vertices into camera space). Per-vertex computed data is usually interpolated through the primitive and can be accessed by the fragment shader for per-pixel computations in a later stage of the pipeline.

        string vertexShaderSource = @"
            #version 140
 
            // object space to camera space transformation
            uniform mat4 modelview_matrix;            
 
            // camera space to clip coordinates
            uniform mat4 projection_matrix;
 
 
            // incoming vertex position
            in vec3 vertex_position;
 
            // incoming vertex normal
            in vec3 vertex_normal;
 
            // transformed vertex normal
            out vec3 normal;
 
            void main(void)
            {
              //not a proper transformation if modelview_matrix involves non-uniform scaling
              normal = ( modelview_matrix * vec4( vertex_normal, 0 ) ).xyz;
 
              // transforming the incoming vertex position
              gl_Position = projection_matrix * modelview_matrix * vec4( vertex_position, 1 );
            }";

1.2 Fragment Shader
The fragment shader operates on fragments. It is responsible for coloring the fragments that make up the primitives in the scene. This coloring can take into account fragment position, surface normal, light positions and colors and so on.

        string fragmentShaderSource = @"
            #version 140
 
            precision highp float;
 
            const vec3 ambient = vec3( 0.1, 0.1, 0.1 );
            const vec3 lightVecNormalized = normalize( vec3( 0.5, 0.5, 2 ) );
            const vec3 lightColor = vec3( 1.0, 0.8, 0.2 );
 
            in vec3 normal;
 
            out vec4 out_frag_color;
 
            void main(void)
            {
              float diffuse = clamp( dot( lightVecNormalized, normalize( normal ) ), 0.0, 1.0 );
              out_frag_color = vec4( ambient + diffuse * lightColor, 1.0 );
            }";

Declaring variables for the different OpenGL objects

        int vertexShaderHandle,
            fragmentShaderHandle,
            shaderProgramHandle,
            modelviewMatrixLocation,
            projectionMatrixLocation,
            positionVboHandle,
            normalVboHandle,
            indicesVboHandle;
 
        Matrix4 projectionMatrix, modelviewMatrix;
 
        Vector3[] positionVboData = new Vector3[]{
            new Vector3(-1.0f, -1.0f,  1.0f),
            new Vector3( 1.0f, -1.0f,  1.0f),
            new Vector3( 1.0f,  1.0f,  1.0f),
            new Vector3(-1.0f,  1.0f,  1.0f),
            new Vector3(-1.0f, -1.0f, -1.0f),
            new Vector3( 1.0f, -1.0f, -1.0f), 
            new Vector3( 1.0f,  1.0f, -1.0f),
            new Vector3(-1.0f,  1.0f, -1.0f) };
 
        uint[] indicesVboData = new uint[]{
                // front face
                0, 1, 2, 2, 3, 0,
                // top face
                3, 2, 6, 6, 7, 3,
                // back face
                7, 6, 5, 5, 4, 7,
                // left face
                4, 0, 3, 3, 7, 4,
                // bottom face
                0, 1, 5, 5, 4, 0,
                // right face
                1, 5, 6, 6, 2, 1, };

Now, let's create our window.

        public HelloGL3()
            : base( 640, 480, // width, height
            new GraphicsMode( new ColorFormat( 8, 8, 8, 8 ), 16 ), "OpenGL 3.1 Example", 0,
            DisplayDevice.Default, 3, 1, // use the default display device, request a 3.1 OpenGL context
            GraphicsContextFlags.Debug ) //this will help us track down bugs
        {
        /*}
 
        public override void OnLoad( EventArgs e )
        {
            base.OnLoad(e);*/                        
            CreateShaders(); 
            CreateProgram();
            GL.UseProgram( shaderProgramHandle );
 
            QueryMatrixLocations();
 
            float widthToHeight = ClientSize.Width / ( float )ClientSize.Height;
            SetProjectionMatrix( Matrix4.Perspective( 1.3f, widthToHeight, 1, 20 ) );
 
            SetModelviewMatrix( Matrix4.RotateX( 0.5f ) * Matrix4.CreateTranslation( 0, 0, -4 ) );
 
            LoadVertexPositions();
            LoadVertexNormals();
            LoadIndexer();
 
            // Other state
            GL.Enable( EnableCap.DepthTest );
            GL.ClearColor( 0, 0.1f, 0.4f, 1 );
        }

Before attaching the shaders to the program object we need to compile them.

        private void CreateShaders()
        {
            vertexShaderHandle = GL.CreateShader( ShaderType.VertexShader );
            fragmentShaderHandle = GL.CreateShader( ShaderType.FragmentShader );
 
            GL.ShaderSource( vertexShaderHandle, vertexShaderSource );
            GL.ShaderSource( fragmentShaderHandle, fragmentShaderSource );
 
            GL.CompileShader( vertexShaderHandle );
            GL.CompileShader( fragmentShaderHandle );
        }

This method creates the program and attaches the vertex and fragment shaders. We check for errors after the LinkProgram(...) command.

        private void CreateProgram()
        {
            shaderProgramHandle = GL.CreateProgram();
 
            GL.AttachShader( shaderProgramHandle, vertexShaderHandle );
            GL.AttachShader( shaderProgramHandle, fragmentShaderHandle );
 
            GL.LinkProgram( shaderProgramHandle );
 
            string programInfoLog;
            GL.GetProgramInfoLog( shaderProgramHandle, out programInfoLog );
            Debug.WriteLine( programInfoLog );
        }

In OpenGL 3.1 there is no matrix state. So if we want to do transformations we have to upload the projection and modelview matrices to the shaders ourselves. This can be done using uniform variables. These variables are set on the client side (i.e. our application) and can be read in the shaders. For more information check the "4.3.5 Uniform" chapter of the OpenGL Shading Language specification at khronos registry.
A thorough explanation of the matrices used can be found here.

        private void QueryMatrixLocations()
        {
            projectionMatrixLocation = GL.GetUniformLocation( shaderProgramHandle, "projection_matrix" );
            modelviewMatrixLocation = GL.GetUniformLocation( shaderProgramHandle, "modelview_matrix" );
        }
 
        private void SetModelviewMatrix( Matrix4 matrix )
        {
            modelviewMatrix = matrix;
            GL.UniformMatrix4( modelviewMatrixLocation, false, ref modelviewMatrix );
        }
 
        private void SetProjectionMatrix( Matrix4 matrix )
        {
            projectionMatrix = matrix;
            GL.UniformMatrix4( projectionMatrixLocation, false, ref projectionMatrix );
        }
 
        private void LoadVertexPositions()
        {
            GL.GenBuffers( 1, out positionVboHandle );
            GL.BindBuffer( BufferTarget.ArrayBuffer, positionVboHandle );
            GL.BufferData<Vector3>( BufferTarget.ArrayBuffer,
                new IntPtr( positionVboData.Length * Vector3.SizeInBytes ),
                positionVboData, BufferUsageHint.StaticDraw );
 
            GL.EnableVertexAttribArray( 0 );
            GL.BindAttribLocation( shaderProgramHandle, 0, "vertex_position" );
            GL.VertexAttribPointer( 0, 3, VertexAttribPointerType.Float, false, Vector3.SizeInBytes, 0 );            
        }
 
        private void LoadVertexNormals()
        {
            GL.GenBuffers( 1, out normalVboHandle );
            GL.BindBuffer( BufferTarget.ArrayBuffer, normalVboHandle );
            GL.BufferData<Vector3>( BufferTarget.ArrayBuffer,
                new IntPtr( positionVboData.Length * Vector3.SizeInBytes ),
                positionVboData, BufferUsageHint.StaticDraw );
 
            GL.EnableVertexAttribArray( 1 );            
            GL.BindAttribLocation( shaderProgramHandle, 1, "vertex_normal" );            
            GL.VertexAttribPointer( 1, 3, VertexAttribPointerType.Float, false, Vector3.SizeInBytes, 0 );
        }
 
        private void LoadIndexer()
        {
            GL.GenBuffers( 1, out indicesVboHandle );
            GL.BindBuffer( BufferTarget.ElementArrayBuffer, indicesVboHandle );
            GL.BufferData<uint>( BufferTarget.ElementArrayBuffer, 
                new IntPtr( indicesVboData.Length * Vector3.SizeInBytes ),
                indicesVboData, BufferUsageHint.StaticDraw );
        }
 
        protected override void OnUpdateFrame( FrameEventArgs e )
        {
            SetModelviewMatrix( Matrix4.RotateY( ( float )e.Time ) * modelviewMatrix );
 
            if( Keyboard[ OpenTK.Input.Key.Escape ] )
                Exit();
        }
 
        protected override void OnRenderFrame( FrameEventArgs e )
        {
            GL.Viewport( 0, 0, Width, Height );
            GL.Clear( ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit );
 
            GL.DrawElements( BeginMode.Triangles, indicesVboData.Length,
                DrawElementsType.UnsignedInt, IntPtr.Zero );
 
            GL.Flush();
            SwapBuffers();
        }
 
        protected override void OnResize( EventArgs e )
        {
            float widthToHeight = ClientSize.Width / ( float )ClientSize.Height;
            SetProjectionMatrix( Matrix4.Perspective( 1.3f, widthToHeight, 1, 20 ) );
        }
    }
 
    public class Program
    {
        [STAThread]
        public static void Main()
        {
            using( HelloGL3 win = new HelloGL3() )
            {
                string version = GL.GetString( StringName.Version );
                if( version.StartsWith( "3.1" ) ) win.Run();
                else Debug.WriteLine( "Requested OpenGL version not available." );
            }
        }
    }
}

Comments

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.
the Fiddler's picture

Personal opinion:

  1. No need for proper resource cleanup on *this* tutorial. It will add a lot of cruft and it's not strictly necessary (GraphicsContext.Dispose() should take care of that.)
  2. Comments: first a short overview of necessary concepts, with links to other tutorials on this site. For example, this tutorial uses VBOs, shaders, uniforms and generic vertex attributes. Each concept warrants a few words (why is it necessary in GL3 and not GL2?)
    • Vertex buffer objects: no immediate mode, we load vertices into an ArrayBuffer, elements into an ElementArrayBuffer (showing how vertices are connected) and render them with GL.DrawElements. For more info, see here.
    • Shaders: control how vertices are transformed and how fragments are shaded (GL2 supported both shaders and fixed-function, GL3 is shader-only). Our shaders do this and that.
    • Uniforms: no built-in matrix methods, need to use OpenTK.Math and send the data to our shaders via uniforms.
    • and so on

As long as the code is separated in different methods (create shaders, create VBOs, resize, update, render), a few comments inside the code should be enough.

nythrix's picture

No need for proper resource cleanup on *this* tutorial. It will add a lot of cruft and it's not strictly necessary (GraphicsContext.Dispose() should take care of that.)
Agreed. Plus, I was thinking of removing the Vertex Array Object too. AFAICT it's not really needed.
[VBO]
Didn't want to have a third buffer object (elements). Imho, a few words (and the link) about the recommended way should be enough.

objarni's picture

What is the difference between a shader and a program..?

Note: I'm complete noob when it comes to modern GL..

nythrix's picture

What is the difference between a shader and a program..?
Long story short a shader is an executable that makes up a stage of the graphics pipeline. A vertex shader (first string in the code) does vertex and/or normal transformations and then sends these transformed data further down the line. A fragment shader operates on fragments (kind of a pixel but in 3D space) by typically altering it's color with respect to lights, shadows and so on. Currently not all of the pipeline is programmable and that's not going to change imho. But the most important parts are already available and that's good enough.
A program is a collection of shaders. If all of the pipeline were programmable then a program would BE that pipeline.

I'm no expert and this example was my one and only experience with shaders so a bit of salt might be in place :)

the Fiddler's picture

That's it more or less. GLSL shaders are the equivalent of compilation units in C: once compiled, they turn into the equivalent of C object files (binary code, not executable yet). Programs are the final executable code, which is created by linking one or more object files together.

zahirtezcan's picture

although it is not needed for this sample, VAO is a need for 3+ versions actually. You may not want to bind all attribs before each draw call but a single call to a bindvertexarray call.

p.s. i had got some nice c samples in a vs solution that is step by step ogl 3.1 tutor (first triangle, texture etc.) i hope i can find the link again. it could guide such samples

the Fiddler's picture

VAO is very nice, it simplifies vertex setup a lot and can reduce overhead significantly if you have a several different vertex structures.

I'd love to add more modern tutorials to OpenTK, so please post if you find those samples!

zahirtezcan's picture

Here.

the Fiddler's picture

Thanks!

pontifikas's picture

Why do you call GL.BindAttribLocation() after having Linked and Used the program. Is explicit binding of attribute indexes supposed to occur before Linking? Or it doesnt matter and you simply override the indexes set by the Linker?