Monday, December 30, 2013

Adding shaders to your program

We've got some code that draws a pair of triangles using vertex buffer objects (VBOs).  They can only be seen on some video cards, and if you do see them they are plain white.  Adding a shader will get them drawn everywhere and let us apply some colors.

The first step will be adding some code so that our program can load shaders and get them to be applied when drawing polygons.  So lets start there.

Shaders are written in the GLSL language, which looks a lot like C.  It gets compiled by OpenGL when your program is running, which means you'll need to package your shader source with your compiled executable.  They also work in pairs, a vertex shader and a fragment shader.  These use the same language, they just have different purposes.  The vertex shader is used to manipulate the vertexes of your polygons it runs first and can pass data to the fragment shader.  The fragment shader is where colors and textures are applied.

For now let's assume that our shaders are stored in Vertex.glsl and Fragment.glsl and work on getting those loaded.  In the next post we'll write the shader code.

Each shader will need to be loaded, compiled by OpenGL, then attached to a program object.  It seems convenient to lump all this into a single function, which looks like this:
1:  private int LoadShaders(string strVertexFile, string strFragFile) {  
2:      int iProgram, iVertShader, iFragShader;  
3:    
4:      //Create vertex shader  
5:      iVertShader = GL.CreateShader(ShaderType.VertexShader);  
6:      using (StreamReader srShader = new StreamReader(strVertexFile)) {  
7:          GL.ShaderSource(iVertShader, srShader.ReadToEnd());  
8:      }  
9:      GL.CompileShader(iVertShader);  
10:      WriteLog("Vertex Shader: " + GL.GetShaderInfoLog(iVertShader));  
11:    
12:      //Create fragment shader  
13:      iFragShader = GL.CreateShader(ShaderType.FragmentShader);  
14:      using (StreamReader srShader = new StreamReader(strFragFile)) {  
15:          GL.ShaderSource(iFragShader, srShader.ReadToEnd());  
16:      }  
17:      GL.CompileShader(iFragShader);  
18:      WriteLog("Fragment Shader: " + GL.GetShaderInfoLog(iFragShader));  
19:    
20:      //Build GL program  
21:      iProgram = GL.CreateProgram();  
22:      GL.AttachShader(iProgram, iVertShader);  
23:      GL.AttachShader(iProgram, iFragShader);  
24:      GL.LinkProgram(iProgram);  
25:      WriteLog("Shader Link: " + GL.GetProgramInfoLog(iProgram));  
26:    
27:      //Cleanup  
28:      GL.DeleteShader(iVertShader);  
29:      GL.DeleteShader(iFragShader);  
30:    
31:      return iProgram;  
32:  }  

The vertex and fragment shaders are created in almost identical ways, the only change is the shader type specified when we call GL.CreateShader() on lines 5 and 13.

Lines 7 and 15 are where we turn the code over to OpenGL via GL.ShaderShource().  I'm just loading the source from file with a StreamReader.

Once OpenGL has the source we can attempt to compile it with a call to GL.CompileShader(), like on lines 9 and 17.  Like any other compiler this one will want to report errors encountered in the source, there just isn't a console window to dump messages to.  We can get the error messages by calling GL.GetShaderInfoLog().  On lines 10 and 18 I drop the compile output to my logging label.

We've now got our shaders loaded, but we can't use them until they are linked into a program, which comes up next in the code.

Line 21 uses GL.CreateProgram() to give us an empty program object to work with.  Lines 22 and 23 attach our two shaders to this program object.  Then we link the shaders into a fully usable program with GL.LinkProgram().

Just like linkers used for other languages this one may need to report errors.  We can retrieve those messages by calling GL.GetProgramInfoLog().  Once again I'm just dropping these message into my log.

It's probably worth mentioning that putting compile and linkage messages on the UI might not be the best idea.  Depending on how your video card drivers handle the shaders they might draw garbage or they might throw an exception when the shaders are needed.  If you aren't prepared to handle that exception your form, the one showing the errors, might disappear before you can read that your shader failed to compile.  I got lucky here since I worked all this out on an NVidia card which ignored my broken shader code and let me read my log.  It wasn't until I tried this on a different computer that this problem in my approach smacked me in the face.

As we don't need to shader source once it's been built into the program object we can clean those up.  The calls to GL.DeleteShader() on lines 28 and 29 do just that.

We've got the tools to get our shaders loaded and ready for use, now it's time to make use of them.  First we'll need a class variable to store the ID of the generated program so that we can make use of it as needed.  Then we'll need to call our new function before we attempt to draw, so putting it in GLViewLoading() seems best.
1:  public class GLForm : Form {  
2:      protected static GLControl cglGLView;  
3:      protected static Label clblLog;  
4:      protected static bool cbGLReady;  
5:      protected static uint[] cauiVBOIDs;  
6:      protected static int ciProgramID;  
7:    
8:      public static void Main()  
9:    
10:      public GLForm()  
11:    
12:      public int WriteLog(string strText)  
13:    
14:      private void GLViewLoading(object oSender, EventArgs eaArgs) {  
15:          //The GLControl is now loading  
16:          Matrix4 pmatMatrix;  
17:          float fAspect;  
18:          Vector3[] av3Triangle;  
19:    
20:          WriteLog("GLControl: Loading...");  
21:          WriteLog("OpenGL Version: " + GL.GetString(StringName.Version));  
22:    
23:          //Setup GL  
24:          GL.ClearColor(0.0f, 0.0f, 0.2f, 0.0f);  
25:          GL.Enable(EnableCap.DepthTest);  
26:    
27:          //Setup the viewport  
28:          GL.Viewport(0, 0, cglGLView.Width, cglGLView.Height);  
29:          fAspect = cglGLView.Width / cglGLView.Height;  
30:          pmatMatrix = Matrix4.CreatePerspectiveFieldOfView(MathHelper.PiOver4, fAspect, 0.1f, 100.0f);  
31:          GL.MatrixMode(MatrixMode.Projection);  
32:          GL.LoadMatrix(ref pmatMatrix);  
33:          GL.MatrixMode(MatrixMode.Modelview);  
34:    
35:          //Create video memory buffers  
36:          cauiVBOIDs = new uint[1];  
37:          GL.GenBuffers(1, cauiVBOIDs);  
38:    
39:          //Setup our vertexes  
40:          av3Triangle = new Vector3[6];  
41:    
42:          av3Triangle[0].X = 2.5f;  
43:          av3Triangle[0].Y = -1.0f;  
44:          av3Triangle[0].Z = -8.0f;  
45:          av3Triangle[1].X = 1.5f;  
46:          av3Triangle[1].Y = 1.0f;  
47:          av3Triangle[1].Z = -8.0f;  
48:          av3Triangle[2].X = 0.5f;  
49:          av3Triangle[2].Y = -1.0f;  
50:          av3Triangle[2].Z = -8.0f;  
51:    
52:          av3Triangle[3].X = -2.5f;  
53:          av3Triangle[3].Y = -1.0f;  
54:          av3Triangle[3].Z = -8.0f;  
55:          av3Triangle[4].X = -1.5f;  
56:          av3Triangle[4].Y = 1.0f;  
57:          av3Triangle[4].Z = -8.0f;  
58:          av3Triangle[5].X = -0.5f;  
59:          av3Triangle[5].Y = -1.0f;  
60:          av3Triangle[5].Z = -8.0f;  
61:    
62:          //Copy the vertex data into video memory  
63:          GL.BindBuffer(BufferTarget.ArrayBuffer, cauiVBOIDs[0]);  
64:          GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)(av3Triangle.Length * Vector3.SizeInBytes), av3Triangle, BufferUsageHint.StaticDraw );  
65:    
66:          //Load Shaders  
67:          ciProgramID = LoadShaders("Vertex.glsl", "Fragment.glsl");  
68:    
69:          //Done setting up GL for use  
70:          cbGLReady = true;  
71:    
72:          return;  
73:      }  
74:    
75:      private void GLViewPainting(object oSender, EventArgs eaArgs)  
76:    
77:      private int LoadShaders(string strVertexFile, string strFragFile)  
78:  }  


Nothing too fancy in those changes.

One last change we need to make is in the GLViewPainting() function.  When we render polygons we need to specify which program object to use.  You can have multiple programs with different shaders loaded, then change them for each object.  I'm not entirely sure when you'd want this, but it is possible.  So before we do our drawing we'll need to call GL.UseProgram() to specify which program object to use.
1:  private void GLViewPainting(object oSender, EventArgs eaArgs) {  
2:      if (cbGLReady == false) {//Control is not loaded, do nothing  
3:          return;  
4:      }  
5:      WriteLog("Painting...");  
6:    
7:      //Specify Shader  
8:      GL.UseProgram(ciProgramID);  
9:    
10:      //Clear the GL View  
11:      GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);  
12:      GL.MatrixMode(MatrixMode.Modelview);  
13:    
14:      GL.BindBuffer(BufferTarget.ArrayBuffer, cauiVBOIDs[0]);  
15:    
16:      GL.EnableVertexAttribArray(0);  
17:      GL.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, Vector3.SizeInBytes, 0);  
18:      GL.DrawArrays(PrimitiveType.Triangles, 0, 6);  
19:      GL.DisableVertexAttribArray(0);  
20:    
21:      //Drawing complete  
22:      cglGLView.SwapBuffers();  
23:    
24:      return;  
25:  }  

That's it, the program is now ready to make use of shaders.  The only problem is that none are written yet, so it will have some trouble accomplishing that.  Next time we'll write the shaders and that should make all the stuff we did here worthwhile.

You can get the full source for this here.

No comments:

Post a Comment