Shaders - Fish-In-A-Suit/Conquest GitHub Wiki
Introduction
When you run the game, a lot of the processing such as game logic, other caclulations and gameplay are run on the CPU. When it comes to rendering the scene, however, the GPU comes into action (for example, by calling glDrawElements). The GPU renders the objects to the window and determines exactly how they are displayed. It determines whether they have a colour, texture, changes position/scale of objects, does lighting calculations, adds particle/fog effects etc. In the old OpenGL, there existed a finite list of methods which could be called in order to achieve the aforementioned results. This was proven to be quite limited and therefore none of those methods exist now. It's now up to the programmer to implement these effects by himself - you have to program the GPU yourself to tell it exactly how you want the objects to be rendered. This can be achieved through shaders - programs that run on the GPU. GLSL (OpenGL shading language) is used to perform shader programming.
The OpenGL rendering pipeline defines the following shaders, with their enumerator name:
shader type | enumerated name |
---|---|
vertex shader | GL_VERTEX_SHADER |
tesselation control and evaluation shader | GL_TESS_CONTROL_SHADER and GL_TESS_EVALUATION_SHADER |
geometry shader | GL_GEOMETRY_SHADER |
fragment shader | GL_FRAGMENT_SHADER |
compute shader | GL_COMPUTE_SHADER |
Program objects
Shaders by themselves don't carry much meaning. In order for them to be used effectively, they need to be coupled with a program object. A program object represent fully processed executable code (written in GLSL) for one or more shader stages. A program object is an OpenGL object, but doesn't follow the standard OpenGL object model (glGen, glDelete etc).
A program object can contain the executable code for all of the shader stages, so that all that is needed to render is to bind only one program object. Building programs that contain multiple shader stages require a two-stage compilation process. This two-stage compilation process mirrors the standard compile/link setup for C and C++ source code. C/C++ text is first fed through a compiler, thus producing an object file. To get the executable code, one or more object files must be linked together.
With this method of program creation, shader text is first fed through a compiler, thus producing a shader object. To get the executable program object, one or more shader objects must be linked together.
Creation
Empty program objects are created with this function: int glCreateProgram(), which returns an int reference to the created empty program object.
Pre-link setup
Once the shader objects have been compiled successfully, you can attach them into a program (make sure that the program is already created). After creating the program, the shader objects must be attached to the program. This is done via the method void glAttachShader(int program, int shader), where
- program is the program object
- shader is the shader object which will be attached to the program
Configuring vertex shader input attribute locations - sending data to the vertex shader
The vertex shader is invoked once per vertex. Its main job is to process the data associated with the vertex, and pass it (and possibly other information) along to the next stage of the pipeline. In order to give the vertex shader something to work with, we must have some way of providing (per-vertex) input to the shader. Typically, this includes the vertex position, normal vector, and texture coordinate (among other things). In earlier versions of OpenGL (prior to 3.0), each piece of vertex information had a specifc "channel" in the pipeline. It was provided to the shaders using functions such as glVertex, glTexCoord, and glNormal (or within vertex arrays using glVertexPointer, glTexCoordPointer, or glNormalPointer).The shader would then access these values via built-in variables such as gl_Vertex and gl_Normal. This functionality was deprecated in OpenGL 3.0 and later removed. Instead, now vertex information must be provided using generic vertex attributes, usually in conjunction with (vertex) buffer objects.
The programmer is now free to define an arbitrary set of per-vertex attributes to provide as input to the vertex shader. For example, in order to implement normal mapping, we might decide that position, normal vector, and tangent vector should be provided along with each vertex. With OpenGL 4.0, it's easy to define this as the set of input attributes. This gives us a great deal of flexibility to define our vertex information in any way that is appropriate for our application, but may require a bit of getting used to for those of us who are used to the old way of doing things. In the vertex shader, per-vertex input attributes are declared by using the GLSL qualifer in. For example, to defne a 3-component vector input attribute named VertexColor, we use the following code:
in vec3 VertexColour;
Of course, the data for this attribute must be supplied by the OpenGL program. To do so, we **make use of vertex buffer objects. The buffer object contains the values for the input attribute and in the main OpenGL program we make the connection between the buffer and the input attribute, and defne how to "step through" the data. Then, when rendering, OpenGL pulls data for the input attribute from the buffer for each invocation of the vertex shader. For this recipe, we'll draw the simplest OpenGL shape, a triangle. Our vertex attributes will include the position and color. The vertices of the triangle are red, green, and blue, and the interior of the triangle has those three colours blended together.
The most basic of a vertex shader is a semi-pass through one. It collects (defines the input attributes) the positions (coordinates) of vertices and their corresponding colours via the in keyword:
//basic vertex shader
#version 400
in vec3 VertexPosition;
in vec3 VertexColour;
out vec3 Colour;
void main() {
gl_Position = vec4(VertexPosition, 1);
Colour = VertexColour;
}
//basic fragment shader
#version 400
in vec3 Colour;
out vec4 FragColour;
void main() {
FragColour = vec4(Colour, 1);
}
The main program needs to provide data for the input attributes (VertexPosition, VertexColour) by mapping mesh data (data of a 3D model) to the input attributes of the vertex shader. To define this method, use the glBingAttribLocation(int program, int index, CharSequence name) method:
- program: the program object in which the mapping is to be made
- index: the index of the attribute list of a vao to be bound
- name: the vertex shader input variable (case sensitive) to which the ibdex will be bound
This method should be called prior to linking the program! To map the vertex shader example above, you'd call
glBindAttribLocation(programID, 0, VertexPosition)
and glBindAttribLocation(programID, 1, VertexColour
(suppose you have the position data stored at index 0 and colour data at index 1)
Configuring fragment shader output location
As with the vertex shader input attributes, we should as well define where tge output of the fragment shader should be stored. The output fragment colour is typically stored in the backbuffer (in double-buffer systems) which is numbered as 0. If none output locations are defined, OpenGL will store the outputs of the fragment shader to the back buffer by default. To explicitly specify that, call the method
glFragDataLocation(programID, 0, "FragColour");
Another way of configuring fragment shader ouput location is to specify it directly inside the shader code using the layout qualifier:
layout (location = 0) out vec4 finalColour;
The above line specifies the output location to be the back buffer (which is represented by 0).
Sending data to a shader using uniform variables [uniform]
Uniform variables are intended to be used for data that may change relatively infrequently compared to per-vertex attributes. In fact, it is simply not possible to set per-vertex attributes with uniform variables. For example, uniform variables are well suited for the matrices used for modeling, viewing, and projective transformations.
Within a shader, uniform variables are read only - their value can't be changed by a shader. Any shader (vertex, fragment, etc) can have uniform variables defined. They always act as input to the shader.
https://ahbejarano.gitbook.io/lwjglgamedev/transformations
Shader objects
Vertex shader
The vertex shader executes one time for each vertex of the object that's being rendered. For this, object's model data is used which is stored in the VAO of that object as the input to the vertex shader. Vertex shader performs the following major operations on the input data:
- determine whether the vertex that is being processed should be rendered onto the window or not (by determining the position of that vertex)
- output per vertex values (can be any values, whatever you program them to be - 3 floats, 3floats and a 2d vector or any combination of floats, vectors and matrices)
Fragment shader
Fragment shader executes one time for each pixel that the object covers on the window. Each time it runs, it uses the output of the vertex shader to caclulate what the final colour of the final pixel should be. The output of the fragment shader is always a colour in rgba format - it represents the colour of the pixel that is being processed.
Shader object compilation
The first step is to create shader objects for each shader that you intend to use and compile them. To create a shader object, you call int glCreateShader(int shaderType), which creates an empty shader object for the shader stage given by shaderType and provides a reference to it via an int value.
- shaderType must be one of: GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER, GL_TESS_EVALUATION_SHADER, GL_GEOMETRY_SHADER, GL_FRAGMENT_SHADER, or GL_COMPUTE_SHADER
Once the empty shader object is created, you will have to populate it with the actual text string representing GLSL source code: void glShaderSource(int shader, java.lang.CharSequence string). It takes in an array of strings specified by string and stores it into the specified shader object (shader). OpenGL will copy these strings into internal memory.
Once shader strings have been set into a shader object, it can be compiled by calling: void glCompileSShader(int shader). When the shader is compiled, it will be compiled as if all of the given strings were concatenated end-to-end. This makes it easy for the user to load most of a shader from a file.
Shader error handling
Compilation may or may not succeed. Shader compilation failure is not an OpenGL Error; you need to check for it specifically. Every shader object holds several parameters "inside" it. These are GL_SHADER_TYPE, GL_DELETE_STATUS, GL_COMPILE_STATUS, GL_SHADER_SOURCE_LENGTH, GL_INFO_LOG_LENGTH.
The value of these shader object parameters can be queried by int glGetShaderi(int shader, int pname).
- shader: the shader object whose parameter to query
- pname: one of the above parameters
If the shader object wasn't created, an error should be thrown:
if (shaderID == 0) {
throw new Exception("Error creating shader. Type: " + shaderType);
}
GLSL source code might fail to compile by a call to int glCompileShader(int shader). Therefore, it's wise to check the success of compilation after the aforementioned method is called. If the GL_COMPILE_STATUS parameter of a shader object is 0 after compilation, that means that the compilation wasn't successful. Therefore, query the value of the GL_COMPILE_STATUS of a shader object that has undergone compilation and compare it against 0 in an if-clause:
if (glGetShaderi(shaderID, GL_COMPILE_STATUS) == 0) {
throw new Exception("Error compiling shader code: " + glGetShaderInfoLog(shaderID, 1024));
}
It's often useful to know why a certain operation regarding shader objects was failed. This is provided as text messages. OpenGl allows you to query a log containing this information: java.lang.String glGetShaderInfoLog(int shader, int maxLength). maxLength represents the maximum number of bytes (characters in the character buffer) which will be displayed.
Shader compilation stage summed up:
int shaderID = glCreateShader(SHADER_TYPE);
if (shaderID == 0) {
throw new Exception("Failed to create shader!");
glShaderSource(shaderID, SHADER_FILE);
glCompileShader(shaderID);
if (glGetShaderi(shaderID, GL_COMPILE_STATUS) == 0) {
throw new Exception("Failed to compile shader: " + glGetShaderi(shaderID, GL_SHADER_TYPE);
System.out.println("Shader info log: " + glGetShaderInfoLog(shaderID, 1024);
}
Rendering
the render function, it is simply a matter of clearing the color buffer using glClear, binding to the vertex array object, and calling glDrawArrays to draw our triangle. The function glDrawArrays initiates rendering of primitives by stepping through the buffers for each enabled attribute array, and passing the data down the pipeline to the vertex shader. The frst argument is the render mode (in this case we are drawing triangles), the second is the starting index in the enabled arrays, and the third argument is the number of indices to be rendered (3 vertexes for a single triangle).
To summarize, rendering with vertex buffer objects (VBOs) involves the following steps:
- Before linking the shader program, define the mappings between generic vertex attribute indexes and shader input variables by calling glBindAttribLocation.
- Create and populate the buffer objects for each attribute.
- Create and defne the vertex array object by calling glVertexAttribPointer while the appropriate buffer is bound.
- When rendering, bind to the vertex array object and call glDrawArrays, or other appropriate rendering function (for example, glDrawElements).
Say you've got two triangles, which form a quad and you want to draw it onto the screen. The vertex shader is executed once for each vertex of the quad (that is, 4times), while the fragment shader is executed for each pixel that is covedered by the surface of the quad (that is, it's executed like 10000+ times)