Brief Introduction to Shaders Using GLSL shader最详解

Brief Introduction to Shaders Using GLSL

Introduction

Formatting

First, some formatting which I will try to be consistent about. For blocks of code, I'm using prettify for syntax highlighting and  LaTeXMathML for mathematical formulae (currently not supported in Chrome).

This is a command

This is a comment

This is the name of an application

void main() {
    //This is a code block
    float a_value = 0.0f;
}

Other Resources

This tutorial assumes you have the code from this Eclipse workspace. If you are trying to run this example in the Ming Ong lab, you may have to do the following inside a MinGW Shell to get it to compile: 
mingw-get update 
mingw-get upgrade gcc-c++ 
This is because some C++11 features are used in this example and require version 4.6 or later of gcc.

For a more complete tutorial of GLSL, see the tutorial at lighthouse3d.com

The OpenGL Pipeline

Before you start using GLSL, it is important to understand the OpenGL pipeline. Let's examine the fixed pipeline first.

Figure 1

OpenGL fixed pipeline diagram from khronos.org

The orange boxes are what you control via the OpenGL fixed API. For example, you transform the vertices by making calls to glTranslateglScaleglRotate, and glMultMatrix. These API calls modify the top of the matrix stack which is managed by OpenGL. In addition, you can specify how lighting is calculated by specifying up to GL_MAX_LIGHTS number of lights and a predefined lighting model with calls to glLight and glShadeModel.

If you want to use some other lighting model other than what is specified by glLightModel, you cannot without overriding this fixed functionality. This is where the programmable pipeline comes in.

Figure 2

OpenGL programmable pipeline diagram from khronos.org

When we override the fixed pipeline, we no longer have access to the matrix stack and fixed lighting functionality. In addition, vertex color and lighting calculations must be done manually. While this may sound like a lot more work up front, most of the work is merely learning how to set your program up, and if you use a math library such as GLM, then you will have access to the methods you are already familiar with for matrix transformations (e.g. glRotate, etc.).

Figure 3

Acceptable geometry for glDrawArrays.

A Simple Example

Let's take a look at what you need to do before you can start writing your own shaders. First, the functions such as  glVertexglNormal, and  glTexCoord are no longer available. So how do you specify vertices, normals and texture coorindates? The answer is by using array buffers. Generally you have a model consisting of vertices which you transform via the modelview matrix, but, once loaded, you usually do not modify the position of each vertex individually. For this reason it is usually sufficient to store all of your vertices into a  GL_ARRAY_BUFFER (at least all vertices for a single model) and then draw the data in several array buffers by calling  glDrawArrays. When using glDrawArrays, you must use one of the geometry types specified in  [F3]glDrawArrays is what is used in this example, but, if you want to reuse vertices by way of an index array, you can do this be using  glDrawElements.

Setting up the Array Buffers

Let's assume we have vertex data stored in a floating point array in the following format:
mesh.verts = [x1,y1,z1,x2,y2,z2,x3,y3,z3⎵Triangle1,x4,y4,z5,x5,y5,z5,x6,y6,z6⎵Triangle2,…]
We first need to create the buffer:

    
    
  1. GLuint bufs[1];
  2. glGenBuffers(1, bufs);
  3.  
  4. shader->vertexBuffer = bufs[0];
This code creates a single buffer (line 2) and stores an integer handle to that buffer. Now we need to fill the buffer with the  mesh.verts data:

    
    
  1. glBindBuffer(GL_ARRAY_BUFFER, shader->vertexBuffer);
  2. glBufferData(
  3. GL_ARRAY_BUFFER, //what kind of buffer (an array)
  4. mesh.numVerts * 3 * sizeof(float), //size of the buffer in bytes
  5. mesh.verts, //pointer to data we want to fill the buffer with
  6. GL_STATIC_DRAW //how we intend to use the buffer
  7. );
You can use this process to fill all of your vertex data. For example, if you have a normal and texture coordinate for each vertex, you would specify them in the same way:
  1. Create the buffer
  2. Bind the buffer you want to fill using glBindBuffer
  3. Use glBufferData to fill the buffer you just bound
Before we get into how we use the data in the buffers, we need to know some information about the shaders, so let's take a look at a some shader code.

A Simple Shader

Here is a simple vertex shader program:

    
    
  1. //These variables are constant for all vertices
  2. uniform mat4 M; //modelview matrix
  3. uniform mat4 P; //projection matrix
  4. uniform mat3 M_n; //normal matrix
  5.  
  6. //input variables from host
  7. attribute vec3 pos; //vertex position
  8.  
  9. //variables to be passed to the fragment shader
  10. varying vec4 frag_color;
  11.  
  12. void main() {
  13. gl_Position = P * (M * vec4(pos, 1.0));
  14. //determine vertex color based on position and time
  15. vec4 color = vec4(pos,1.0);
  16. color = clamp(color, 0.0, 1.0);
  17. frag_color = clamp(color, 0.0, 1.0);
  18. }
And the corresponding fragment shader:

    
    
  1. varying vec4 frag_color;
  2.  
  3. void main() {
  4. gl_FragColor = frag_color;
  5. }
In general, shader code is very similar to C code. See the  GLSL specs for more details on the GLSL language. 
Let's go over the vertex shader first. There is a vertex program that processes each vertex in parallel. Remember that OpenGL no longer handles the transformation of our vertices. That is, there is no  matrix stack we can  glPush and  glPop matrices on/off to. Instead, we will manage both the modelview and the projection matrices on the CPU and pass them to the GPU using uniform variables. These are specified in the top 3 lines of the vertex shader.

    
    
  1. uniform mat4 M; //modelview matrix
  2. uniform mat4 P; //projection matrix
  3. uniform mat3 M_n; //normal matrix

You can think of uniform variables as globally accessible to each vertex and fragment program. The values of MP, and M_n are the same for every vertex and fragment program running.

In addition to uniform variables, each vertex has attributes associated with it (e.g. position, normal, color, texture coordinate, etc.). These are accessed via the followingattribute variables:


    
    
  1. attribute vec3 pos; //vertex position

For this example, there is only one vertex attribute which represents the vertex position.

varying variables can be thought of as indirect outputs to fragment shaders. I say indirectsince the values that are set here in the vertex shader are not exactly what gets passed to the fragment shader. Rather, the values of varying variables are interpolated before being passed to fragment shaders. Consider figure [F4]. The color of the fragment (which can be thought of as the frag_color variable in this example) is not exactly the color of any of the three vertices, but rather an interpolated (usually linearly) color.

Figure 4

An illustration of vertex color interpolation from lighthouse3d.com


Now that we know about some different types of variables that appear in shader code, let's take a look at our example fragment shader. In the fragment shader code, notice the  gl_FragColorvariable. This is a built-in GLSL variable which represents the color of that fragment. The purpose of the fragment shader is to set this variable to  something reasonable. In this case, we are just setting the color to the value interpolated by OpenGL (line 4 of the fragment program).

Compiling and Linking your Shader Source Code

Now that we've written our shader program, we need to compile and link it into a shader program. We do this using the  Shader::fromFiles and  Shader::setShaderSource functions.

    
    
  1. void Shader::fromFiles(std::string vertFile, std::string fragFile) {
  2. //These are shader objects containing the shader source code
  3. GLint vSource = setShaderSource(vertFile, GL_VERTEX_SHADER);
  4. GLint fSource = setShaderSource(fragFile, GL_FRAGMENT_SHADER);
  5.  
  6. //Create a new shader program
  7. program = glCreateProgram();
  8.  
  9. //Compile the source code for each shader and attach it to the program.
  10. glCompileShader(vSource);
  11. printLog("vertex compile log: ", vSource);
  12. glAttachShader(program, vSource);
  13.  
  14. glCompileShader(fSource);
  15. printLog("fragment compile log: ", fSource);
  16. glAttachShader(program, fSource);
  17.  
  18. //we could attach more shaders, such as a geometry or tessellation
  19. //shader here.
  20.  
  21. //link all of the attached shader objects
  22. glLinkProgram(program);
  23. }

    
    
  1. GLint Shader::setShaderSource(std::string file, GLenum type) {
  2. //read source code
  3. ifstream fin(file.c_str());
  4. if (fin.fail()) {
  5. cerr << "Could not open " << file << " for reading" << endl;
  6. return -1;
  7. }
  8. fin.seekg(0, ios::end);
  9. int count = fin.tellg();
  10. char *data = NULL;
  11. if (count > 0) {
  12. fin.seekg(ios::beg);
  13. data = new char[count+1];
  14. fin.read(data,count);
  15. data[count] = '\0';
  16. }
  17. fin.close();
  18.  
  19. //create the shader
  20. GLint s = glCreateShader(type);
  21. glShaderSource(s, 1, const_cast<const char **>(&data), NULL);
  22. delete [] data;
  23. return s;
  24. }
The inmportant information in the  fromFiles routine is on lines 7,10,12,14,16 and for the setShaderSource routine is lines 20 and 21. The  Shader class assumes you only have a vertex and fragment shader. However, you may have other shaders such as a tessellation and geometry shader. You can use this process to attach whatever shaders you want to a shader program:
  1. Create a shader program (fromFiles:7)
  2. For each shader:
    1. Get your shader source code into a string (setShaderSource:2-17)
    2. Create a shader (setShaderSource:20)
    3. Set the shader source to the string you read in earlier (setShaderSource:21)
    4. Compile and attach the shader to the shader program (fromFiles:10-12,14-16)
  3. Link the shader program (fromFiles:22)

Specifying Vertex Attribute Data in OpenGL

Now that we've written our vertex and fragment shaders, we need to get  handles to each variable in order to associate certain data to certain variables. We do this using  glGet*Locationcalls:

    
    
  1. //Here's where we setup handles to each variable that is used in the shader
  2. //program. See the shader source code for more detail on what the difference
  3. //is between uniform and vertex attribute variables.
  4. shader->modelViewLoc = glGetUniformLocation(shader->program, "M");
  5. shader->projectionLoc = glGetUniformLocation(shader->program, "P");
  6. shader->normalMatrixLoc = glGetUniformLocation(shader->program, "M_n");
  7.  
  8. shader->vertexLoc = glGetAttribLocation(shader->program, "pos");
Notice the string parameter to each  glGet*Location call corresponds to a variable in our shader code. The value returned by this function is the handle to that variable and allows us to specify the data that will fill that variable. Let's see how we specify the modelview, projection, and normal matrices. We will use the  GLM library to manipulate these matrices on the CPU.

    
    
  1. //Pass the matrices and animation time to the GPU
  2. glUniformMatrix4fv(
  3. shader->modelViewLoc, //handle to variable in the shader program
  4. 1, //how many matrices we want to send
  5. GL_FALSE, //don't transpose the matrix
  6. glm::value_ptr(modelCam) //a pointer to an array containing the entries for
  7. //the matrix
  8. );
  9. glUniformMatrix4fv(shader->projectionLoc, 1, GL_FALSE,
  10. glm::value_ptr(projection));
  11. glUniformMatrix3fv(shader->normalMatrixLoc, 1, GL_FALSE,
  12. glm::value_ptr(normalMatrix));
The last call to  glUnifromMatrix*fv is just a pointer to a floating point array containing 16 floating point values which represent a 4x4 matrix in column-major order:
[0481215913261014371115]
If you are using a math library that uses row-major matrices, you will need to set the third parameter of  glUniformMatrix*fv to  GL_TRUE, which will transpose the matrix before sending it to the GPU. To set the per-vertex data using the buffer we created in section  [S6] we do the following:

    
    
  1. glBindBuffer(GL_ARRAY_BUFFER, shader->vertexBuffer); //which buffer we want
  2. //to use
  3. glEnableVertexAttribArray(shader->vertexLoc); //enable the attribute
  4. glVertexAttribPointer(
  5. shader->vertexLoc, //handle to variable in shader program
  6. 3, //vector size (e.g. for texture coordinates this could be 2).
  7. GL_FLOAT, //what type of data (e.g. GL_FLOAT, GL_INT, etc.)
  8. GL_FALSE, //normalize the data?
  9. 0, //stride of data (e.g. offset in bytes). Most of the time leaving
  10. //this at 0 (assumes data is in one, contiguous array) is fine
  11. //unless we're doing something really complex.
  12. NULL //since our stride will be 0 in general, leaving this NULL is
  13. //also fine in general
  14. );
We can repeat this sequence of function calls for each buffer/vertex attribute we have. Once we have associated each shader variable with the apprpriate data, we can draw our geometry using glDrawArrays:

    
    
  1. glDrawArrays(GL_TRIANGLES, 0, mesh.numVerts);
This says that our vertex data is packed into triangles (as described in section  [S6]) and that we have  mesh.numVerts vertices.

Summary

In summary, one can use the following steps to create a GLSL program:
  1. Read your vertex data into arrays (vertices, normals, texture coordinates etc) and setup your matrices (modelview, projection, etc.).
  2. Setup GL_ARRAY_BUFFERs for each array [S6]
  3. Write your shaders [S7]
  4. Load your shader source, compile it, and link it [S8]
  5. Get handles to the variables in your shader code and associate each shader variable to the appropriate data [S9]
  6. Draw your data using glDrawArrays or glDrawElements.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值