Friday, January 10, 2014

Talking to shaders

I've finished create the most basic of shaders, and they work in the sense that they get polygons onto the screen.  Let's step them up a bit.  To do that we'll need to pass data to the shaders, perhaps a color and maybe the model projection matrix to start moving away from those deprecated attributes.

I'd like to move up to a newer version of OpenGL, but I'm limited to version 3.0 (GLSL 1.3) since that's the highest version I can reach with all the machines I work on.  I'll make an effort not to use any features that are removed in later versions, I can't make any promises since it's not always easy to know what features those are.
There are two types of information that can be shared with shaders: attributes and uniforms.  Attributes are values that can be different from one vertex to the next; things like coordinates or colors.  An attribute will need an array of data with one element in the array per vertex  Uniforms are values that will be the same for all vertices; things like the projection matrix or a light source position.

Let's start with the uniform values.  Shaders will need to have a uniform block defined which will receive the data.  To that end I'll add this block to my vertex and fragment shaders:
1:  uniform staticdata {  
2:      mat4 m4Projection;  
3:      vec4 v4Color;  
4:  };  

This looks an awful lot like defining a structure in C syntax.  That's not how these get used though, it's mostly just a means of packing a bunch of data into a single large buffer rather than a whole bunch of small ones.

To access these values all we need to do is use the name of the values within the block as you would a variable.  I'm only sending two values here, but the possibility exists to send through as much data as I'd like to.  So let's make use of  the data, the vertex shader needs to stop using the deprecated gl_ModelViewProjectionMatrix value and start using the the projection matrix supplied in that uniform block:
1:  #version 130  
2:    
3:  uniform staticdata {  
4:      mat4 m4Projection;  
5:      vec4 v4Color;  
6:  };  
7:    
8:  void main() {  
9:      gl_Position = gl_Vertex * m4Projection;  
10:  }  

The fragment shader should stop using that fixed color to fill the triangles and start using the color in the uniform block:
1:  #version 130  
2:    
3:  uniform staticdata {  
4:      mat4 m4Projection;  
5:      vec4 v4Color;  
6:  };  
7:    
8:  void main() {  
9:      gl_FragColor = v4Color;  
10:  }  

Since our application uses the system memory and our shaders use video memory we'll need to use OpenGL to transfer data between them.  OpenGL maintains a table of all the attributes and uniforms that are being used by the shader as well including ID we can use to put values in them.  We have the option of setting the ID's for these values as well as retrieving them.  It's probably more convenient to set them since that allows us to tuck the ID's into a constant rather than having another variable.  When the shading program is linked this table is created, so we'll need to set them before linking.

Which means we'll need to edit the LoadShaders() function to set these ID's.  Before we do that though, let's create a constant to hold the ID number.
1:  protected const int UNIFORMBINDING = 15;  

There's nothing magical about 15 here, I just picked a number.  On it's own OpenGL would likely assign my uniform block an ID of zero, so I wanted something different than that.  Fifteen sounded fine.

That in place, let's add two lines to LoadShaders() after we've linked the program:
1:  private int LoadShaders(string strVertexFile, string strFragFile) {  
2:      int iProgram, iVertShader, iFragShader, iUniformID;  
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:    
25:      //Link the shading program  
26:      GL.LinkProgram(iProgram);  
27:    
28:      //Define Uniform block  
29:      iUniformID = GL.GetUniformBlockIndex(iProgram, "staticdata");  
30:      GL.UniformBlockBinding(iProgram, iUniformID, UNIFORMBINDING);  
31:    
32:      //Cleanup  
33:      GL.DeleteShader(iVertShader);  
34:      GL.DeleteShader(iFragShader);  
35:    
36:      return iProgram;  
37:  }  

GL.GetUniformBlockIndex() on line 29 is used to retrieve the ID that's assigned to a uniform block by the linker, which is why we need the linker to have done it's job prior to this.  It's return is the assigned ID.

Then on line 30 GL.UniformBlockBinding() changes that ID to the one I picked.  We have to pass it the program we've linked, the ID it was originally assigned, and then the new ID desired.

The shaders are ready to accept our data, now we need to create the data fill that uniform block.  for this we'll need another buffer object, similar to the one that holds the triangle vertexes.  All of that code goes into GLViewLoading():
1:  private void GLViewLoading(object oSender, EventArgs eaArgs) {  
2:      //The GLControl is now loading  
3:      Matrix4 pmatMatrix;  
4:      float fAspect;  
5:      Vector3[] av3Triangle;  
6:      float[] afUniform;  
7:    
8:      WriteLog("GLControl: Loading...");  
9:      WriteLog("OpenGL Version: " + GL.GetString(StringName.Version));  
10:    
11:      //Setup GL  
12:      GL.ClearColor(0.0f, 0.0f, 0.2f, 0.0f);  
13:      GL.Enable(EnableCap.DepthTest);  
14:    
15:      //Setup the viewport  
16:      GL.Viewport(0, 0, cglGLView.Width, cglGLView.Height);  
17:      fAspect = cglGLView.Width / cglGLView.Height;  
18:      pmatMatrix = Matrix4.CreatePerspectiveFieldOfView(MathHelper.PiOver4, fAspect, 0.1f, 100.0f);  
19:      GL.MatrixMode(MatrixMode.Projection);  
20:      GL.LoadMatrix(ref pmatMatrix);  
21:      GL.MatrixMode(MatrixMode.Modelview);  
22:    
23:      //Create video memory buffers  
24:      cauiVBOIDs = new uint[2];  
25:      GL.GenBuffers(2, cauiVBOIDs);  
26:    
27:      //Setup triangle vertexes, this hasn't changed so I'm cutting this code for brevity   
28:    
29:      //Setup our uniform data  
30:      afUniform = new float[20];  
31:      afUniform[0] = pmatMatrix.Row0.X;  
32:      afUniform[1] = pmatMatrix.Row1.X;  
33:      afUniform[2] = pmatMatrix.Row2.X;  
34:      afUniform[3] = pmatMatrix.Row3.X;  
35:      afUniform[4] = pmatMatrix.Row0.Y;  
36:      afUniform[5] = pmatMatrix.Row1.Y;  
37:      afUniform[6] = pmatMatrix.Row2.Y;  
38:      afUniform[7] = pmatMatrix.Row3.Y;  
39:      afUniform[8] = pmatMatrix.Row0.Z;  
40:      afUniform[9] = pmatMatrix.Row1.Z;  
41:      afUniform[10] = pmatMatrix.Row2.Z;  
42:      afUniform[11] = pmatMatrix.Row3.Z;  
43:      afUniform[12] = pmatMatrix.Row0.W;  
44:      afUniform[13] = pmatMatrix.Row1.W;  
45:      afUniform[14] = pmatMatrix.Row2.W;  
46:      afUniform[15] = pmatMatrix.Row3.W;  
47:    
48:      afUniform[16] = 0.5f;  
49:      afUniform[17] = 0.5f;  
50:      afUniform[18] = 1.0f;  
51:      afUniform[19] = 1.0f;  
52:    
53:      //Copy the vertex data into video memory  
54:      GL.BindBuffer(BufferTarget.ArrayBuffer, cauiVBOIDs[0]);  
55:      GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)(av3Triangle.Length * Vector3.SizeInBytes), av3Triangle, BufferUsageHint.StaticDraw);  
56:    
57:      GL.BindBuffer(BufferTarget.UniformBuffer, cauiVBOIDs[1]);  
58:      GL.BufferData(BufferTarget.UniformBuffer, (IntPtr)(afUniform.Length * sizeof(float)), afUniform, BufferUsageHint.StaticDraw);  
59:      GL.BindBufferBase(BufferTarget.UniformBuffer, UNIFORMBINDING, cauiVBOIDs[1]);  
60:    
61:      //Load Shaders  
62:      ciProgramID = LoadShaders("Vertex.glsl", "Fragment.glsl");  
63:    
64:      //Done setting up GL for use  
65:      cbGLReady = true;  
66:  }  

That function is getting really long, mostly because of all those lines populating arrays. I'll definitely need to build some other functions or routines to populate the arrays that look less awful than what I have here.  In the meantime this stuff all works so I'm not going to mess with it just yet.

Let me go over what's been added this time around.

On line 4 the afUniform array is created.  This is the array that will hold our uniform block data.  This will take some explanation since the uniform block has a 4x4 matrix and a 4D vertex value and we're using a big pile of floats here.

When data is passed from the program to the shaders it's always in floating point 4D vector, even if you're passing a single integer it will end up padded out to fill that 4D vector.  So we need our array to always be a multiple of 4 floats so that we know it will hold everything.

One might wonder "why floats?" and it's a reasonable question.  OpenTK provides a Vector4 class which holds four float values and it seems like a solid fit here, and in fact they will work just fine.  The trouble comes from matrices.  If you have an array of Vector4 variables they'll try to arrange matrices such that they are row major.  When matrix data is passed to the shaders it has to be in a column major format.

In order to use Vector4 arrays to pass the data you'd have to fill them like this:
1:  av4Uniform[0].X = pmatMatrix.Row0.X;  
2:  av4Uniform[0].Y = pmatMatrix.Row1.X;  
3:  av4Uniform[0].Z = pmatMatrix.Row2.X;  
4:  av4Uniform[0].W = pmatMatrix.Row3.X;  
5:    
6:  av4Uniform[1].X = pmatMatrix.Row0.Y;  
7:  av4Uniform[1].Y = pmatMatrix.Row1.Y;  
8:  av4Uniform[1].Z = pmatMatrix.Row2.Y;  
9:  av4Uniform[1].W = pmatMatrix.Row3.Y;  
10:    
11:  av4Uniform[2].X = pmatMatrix.Row0.Z;  
12:  av4Uniform[2].Y = pmatMatrix.Row1.Z;  
13:  av4Uniform[2].Z = pmatMatrix.Row2.Z;  
14:  av4Uniform[2].W = pmatMatrix.Row3.Z;  
15:    
16:  av4Uniform[3].X = pmatMatrix.Row0.W;  
17:  av4Uniform[3].Y = pmatMatrix.Row1.W;  
18:  av4Uniform[3].Z = pmatMatrix.Row2.W;  
19:  av4Uniform[3].W = pmatMatrix.Row3.W;  

This just looks wrong to me since the X, Y, Z, and W values don't line up.  This is the way you'd have to do it in order to get the values ordered correctly in memory though.  Doing it in floats, as I did in lines 31 to 46 doesn't really look pretty, but you don't have that disconnect in the variable names.

In lines 48 to 51 I'm adding the values for the v4Color value in the uniform block.  There's no separator between values, they just all run together in that array.  If your values don't always have four floats in them you'll need to skip elements in your float array for each value that isn't used.  For example if you were passing a 2D vector you'd have to skip two array elements before adding in the next value.  It might be better to always use 4 column matrices or 4D vectors when passing data since they'll always fit properly.

Skipping back a bit, line 118 and 119 are changed to create space for two buffer objects by making the array bigger and telling OpenGL to make two buffers for us.

Down at the bottom lines 57 and 58 should be familiar.  They are the same functions used to pass our vertex data to video memory.  The only change is that the size is calculated based on float arrays.

Lastly on line 59 we bind the buffer to the ID of the uniform block it's intended to fill by calling GL.BindBufferBase().  The parameters are the type of buffer we want to work on, the ID of the uniform block, and finally the ID of the buffer itself.

Compile and run the program and it looks something like this:

All that code and the net result is that our green triangles turned a bluish purple.  Not terribly exciting visually, but being able to give information to the shader is going to be pretty useful later on.

One last note on this topic.  When I ran this on my windows machine with NVidia drivers providing OpenGL version 3.3 this all worked perfectly.  However, when I tried running it on my Linux machine which relies on Mesa 9 to offer OpenGL version 3.0 it didn't.  My shaders failed to compile with the error:
1:  error: #version 140 / GL_ARB_uniform_buffer_object required for defining uniform blocks  

To fix this I needed to add an extension to the shader code.  This attempts to pull in the uniform buffer object feature if it's available and for some reason not provided to your program.  I tucked this in right after the version directive:
1:  #version 130  
2:  #extension GL_ARB_uniform_buffer_object : enable

For the full code from this post go here.

No comments:

Post a Comment