Loading Text File Shaders
Last Updated: Oct 19th, 2014
In the last tutorial we hard coded the GLSL shader code in our OpenGL application for the sake of simplicity. But when you're trying to develop a shader program, having to recompile your application each time to make changes gets to be a pain. In this tutorial, our OpenGL application will load the GLSL source code on the fly.From LShaderProgram.h
GLuint loadShaderFromFile( std::string path, GLenum shaderType ); /* Pre Condition: -None Post Condition: -Returns the ID of a compiled shader of the specified type from the specified file -Reports error to console if file could not be found or compiled Side Effects: -None */
The base shader class gets a new function to load a text GLSL source file and compile it to the type of shader you want (vertex, fragment, geometry, etc).
From LShaderProgram.cpp
GLuint LShaderProgram::loadShaderFromFile( std::string path, GLenum shaderType ) { //Open file GLuint shaderID = 0; std::string shaderString; std::ifstream sourceFile( path.c_str() ); //Source file loaded if( sourceFile ) { //Get shader source shaderString.assign( ( std::istreambuf_iterator< char >( sourceFile ) ), std::istreambuf_iterator< char >() );
To compile a text source file at runtime, you need to get the text from the file as a single string. After opening the file as an ifstream, we can use the input stream buffer
iterator to read it in as one line.
The standard string function assign() just gives the string a value. The first argument tells it to start reading the source file from the beginning. The second argument tells it to keep reading the source file until it encounters a null character.
The standard string function assign() just gives the string a value. The first argument tells it to start reading the source file from the beginning. The second argument tells it to keep reading the source file until it encounters a null character.
From LShaderProgram.cpp
//Create shader ID shaderID = glCreateShader( shaderType ); //Set shader source const GLchar* shaderSource = shaderString.c_str(); glShaderSource( shaderID, 1, (const GLchar**)&shaderSource, NULL ); //Compile shader source glCompileShader( shaderID ); //Check shader for errors GLint shaderCompiled = GL_FALSE; glGetShaderiv( shaderID, GL_COMPILE_STATUS, &shaderCompiled ); if( shaderCompiled != GL_TRUE ) { printf( "Unable to compile shader %d!\n\nSource:\n%s\n", shaderID, shaderSource ); printShaderLog( shaderID ); glDeleteShader( shaderID ); shaderID = 0; } } else { printf( "Unable to open file %s\n", path.c_str() ); } return shaderID; }
The rest of this function should all look familiar because it's a generalized version of the vertex/fragment shader compilation code from the last tutorial.
From LPlainPolygonProgram2D.cpp
bool LPlainPolygonProgram2D::loadProgram() { //Generate program mProgramID = glCreateProgram(); //Load vertex shader GLuint vertexShader = loadShaderFromFile( "30_loading_text_file_shaders/LPlainPolygonProgram2D.glvs", GL_VERTEX_SHADER ); //Check for errors if( vertexShader == 0 ) { glDeleteProgram( mProgramID ); mProgramID = 0; return false; } //Attach vertex shader to program glAttachShader( mProgramID, vertexShader ); //Create fragment shader GLuint fragmentShader = loadShaderFromFile( "30_loading_text_file_shaders/LPlainPolygonProgram2D.glfs", GL_FRAGMENT_SHADER ); //Check for errors if( fragmentShader == 0 ) { glDeleteShader( vertexShader ); glDeleteProgram( mProgramID ); mProgramID = 0; return false; } //Attach fragment shader to program glAttachShader( mProgramID, fragmentShader ); //Link program glLinkProgram( mProgramID ); //Check for errors GLint programSuccess = GL_TRUE; glGetProgramiv( mProgramID, GL_LINK_STATUS, &programSuccess ); if( programSuccess != GL_TRUE ) { printf( "Error linking program %d!\n", mProgramID ); printProgramLog( mProgramID ); glDeleteShader( vertexShader ); glDeleteShader( fragmentShader ); glDeleteProgram( mProgramID ); mProgramID = 0; return false; } //Clean up excess shader references glDeleteShader( vertexShader ); glDeleteShader( fragmentShader ); return true; }
Here we have the shader program loading code with our new condensed vertex shader loading function.
You'll see that source file for the vertex shader is LPlainPolygonProgram2D.glvs. The source file has the matching file name LPlainPolygonProgram2D.glvs because it makes it easier to keep track of the files. The extensions glvs and glfs correspond to the vertex and fragment shader also because it makes the files easier to manage. The file name and extension don't mean anything to the program because it just assumes they're ASCII text anyways.
About the calls to glDeleteShader(): you'll notice that if the fragment shader load fails we delete the vertex shader because it's useless to us if the fragment shader doesn't compile. We delete the vertex and the fragment shader if they fail to link because if they don't link they're useless on their own. What you may think is strange is that we delete the vertex and fragment shader if the program links successfully. Don't worry, all we're deleting when we delete a shader from an existing program is the spare references to the shaders. The OpenGL context is smart enough to know that if a shader program is using the shader to not delete it.
You'll see that source file for the vertex shader is LPlainPolygonProgram2D.glvs. The source file has the matching file name LPlainPolygonProgram2D.glvs because it makes it easier to keep track of the files. The extensions glvs and glfs correspond to the vertex and fragment shader also because it makes the files easier to manage. The file name and extension don't mean anything to the program because it just assumes they're ASCII text anyways.
About the calls to glDeleteShader(): you'll notice that if the fragment shader load fails we delete the vertex shader because it's useless to us if the fragment shader doesn't compile. We delete the vertex and the fragment shader if they fail to link because if they don't link they're useless on their own. What you may think is strange is that we delete the vertex and fragment shader if the program links successfully. Don't worry, all we're deleting when we delete a shader from an existing program is the spare references to the shaders. The OpenGL context is smart enough to know that if a shader program is using the shader to not delete it.
From LUtil.cpp
bool loadGP() { //Load basic shader program if( !gPlainPolygonProgram2D.loadProgram() ) { printf( "Unable to load basic shader!\n" ); return false; } //Bind basic shader program gPlainPolygonProgram2D.bind(); return true; } bool loadMedia() { return true; } void update() { } void render() { //Clear color buffer glClear( GL_COLOR_BUFFER_BIT ); //Reset transformations glLoadIdentity(); //Solid cyan quad in the center glTranslatef( SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 0.f ); glBegin( GL_QUADS ); glColor3f( 0.f, 1.f, 1.f ); glVertex2f( -50.f, -50.f ); glVertex2f( 50.f, -50.f ); glVertex2f( 50.f, 50.f ); glVertex2f( -50.f, 50.f ); glEnd(); //Update screen glutSwapBuffers(); }
As you can see our main functions haven't changed much yet we get this when we run the program:
Let's look at our GLSL source files to see why.
Let's look at our GLSL source files to see why.
From LPlainPolygonProgram2D.glvs
void main() { //Process vertex gl_Position = gl_Vertex; }
So it looks like we're still using untranformed vertices to render.
From LPlainPolygonProgram2D.glfs
void main() { //Set fragment gl_FragColor = vec4( 0.0, 1.0, 0.0, 1.0 ); }
And the reason it's green now is because all fragments are getting the value red 0, green 1, blue 0, and alpha 1. Fortunately now we can mess with the GLSL code without a recompile.