Shaders

Shaders are the programs that run on GPU.

Shaders are written in OpenGL ES Shader Language (GLSL).

There are different types of shaders, GL ES and WebGL supports only first two:

  • vertex shader - handles vertex.
  • fragment shader - provides a color for the current pixel being rasterized.
  • geometry shader - takes a single primitive as input and may output zero or more primitives.
  • compute shader - used entirely for computing arbitrary information. While it can do rendering, it is generally used for tasks not directly related to drawing triangles and pixels.
  • tessellation control shader (TCS) - controls how much tessellation a particular patch gets; it also defines the size of a patch, thus allowing it to augment data. It can also filter vertex data taken from the vertex shader.
  • tessellation evaluation shader (TES) - takes the results of a tessellation operation and computes the interpolated positions and other per-vertex data from them.

source

Vertex shader example
// for android use es suffix like 
// #version 300 es    
       @JvmStatic
        val VERTEX_SHADER_DEFAULT =
            """
#version 330 core
precision mediump float;
layout(location=0) in vec4 vCoord;
layout(location=1) in vec4 vColor ;
layout(location=2) in vec3 vNormal;
layout(location=3) in vec2 vTexCoord ;
out vec4 exColor;
out vec2 exTexCoord;
uniform mat4 matrix;
uniform vec4 uColor = vec4(-1.0, -1.0, -1.0, -1.0);
void main() {
     gl_Position = matrix  * vCoord;
     if(uColor.w != -1.0) { exColor=uColor; } else {exColor=vColor;} 
     exTexCoord = vTexCoord;
}
""".trimIndent()
public static final String VERTEX_SHADER_DEFAULT = "#version 330 core\n" +
            "precision mediump float;\n" +
            "layout(location=0) in vec4 vCoord;\n" +
            "layout(location=1) in vec4 vColor ;\n" +
            "layout(location=2) in vec3 vNormal;\n" +
            "layout(location=3) in vec2 vTexCoord ;\n" +
            "out vec4 exColor;\n" +
            "out vec2 exTexCoord;\n"+
            "uniform mat4 matrix;\n" +
            "uniform vec4 uColor = vec4(-1.0, -1.0, -1.0, -1.0);\n" +
            "void main() {\n" +
            "    gl_Position = matrix  * vCoord;\n" +
            "     if(uColor.w != -1.0) { exColor=uColor; } else {exColor=vColor;} \n"+

            "    exTexCoord = vTexCoord;\n"+
            "}";

    
ShaderProgram.VERTEX_SHADER_DEFAULT 
    = `#version 300 es
precision mediump float;
layout(location=0) in vec4 vCoord;
layout(location=1) in vec4 vColor ;
layout(location=2) in vec3 vNormal;
layout(location=3) in vec2 vTexCoord ;
out vec4 exColor;
out vec2 exTexCoord;
uniform mat4 matrix;
uniform vec4 uColor;
void main() {
     gl_Position = matrix  * vCoord;
     if(uColor.w != -1.0) { exColor=uColor; } else {exColor=vColor;} 
     exTexCoord = vTexCoord;
}
`
Fragment shader example

The version directive must be on the first line, even blank lines before it are not allowed. Otherwise compilation may be failed (depends from GL implementation).

On kotlin tab you can see how to assign initial value to the uniform variable. But not all implementations supported it. So better always assign values to uniforms from OpneGL API.

// in our case 

// 0 - without texture, 1 with texture 
glUniform1i(glGetUniformLocation(idProgram, "withTexture"),0);
// for using red color
glUniform4f(glGetUniformLocation(idProgram, "uColor"), 1f,0f,0f,1f);
// to ignore uColor in fragment shader
glUniform4f(glGetUniformLocation(idProgram, "uColor"), -1f,-1f,-1f,-1f);

You can keep sources in files. On WebGL you can write source in <script type="sgsl" id="my-shader">. The browser will not try to execute it, because it has an unknown type. But the script will remain in the HTML tree and you can read the content using js.

const shaderEl = document.getElementById('my-shader');
const shaderSource = shaderEl.textContent;

compilation

When you have a shader source you can compile it.

Compile shader example
/**
* @param type possible values GL_VERTEX_SHADER, GL_FRAGMENT_SHADER
*/
fun compileShader(type: Int, source: String): Int {
    val idShader = glCreateShader(type)
    glShaderSource(idShader, source)
    glCompileShader(idShader)
    
    if (glGetShaderi(idShader, GL_COMPILE_STATUS) == GL_FALSE) {
        val reason = glGetShaderInfoLog(idShader, 500)
        glDeleteShader(idShader)
        throw IllegalStateException(
            "Could not compile shader: $reason\nSource:\n $source")
     }
            return idShader
}
public static int compileShader(int type, String source) {
    int idShader = glCreateShader(type);
    glShaderSource(idShader, source);
    glCompileShader(idShader);

    if (glGetShaderi(idShader, GL_COMPILE_STATUS) == GL_FALSE) {
        String reason = glGetShaderInfoLog(idShader, 500);
        glDeleteShader(idShader);
        throw new IllegalStateException(
            "Could not compile shader: " + reason + "\nSource:\n " + source);
    }
    
    return idShader;
}
/**
* Possible values of type are gl.VERTEX_SHADER and gl.FRAGMENT_SHADER
*/
ShaderProgram.compileShader = function(gl, type, source){
    const idShader = gl.createShader(type);
    gl.shaderSource(idShader, source);
    gl.compileShader(idShader);

    if (!gl.getShaderParameter(idShader, gl.COMPILE_STATUS)) {
        const msg = 'An error occurred compiling the shaders: ' +
                     gl.getShaderInfoLog(idShader) +
                     "\n source: " + source;
        alert(msg);
        gl.deleteShader(idShader);
        return null;
     }

    return idShader;
}
/**
* Compile shader.
*
* @param type   possible values are GL_VERTEX_SHADER and GL_FRAGMENT_SHADER
* @param source
* @return id of shader
*/
fun compileShader(type: Int, source: String): Int {
    val idShader = glCreateShader(type)
    glShaderSource(idShader, source)
    glCompileShader(idShader)

    val compiled = IntArray(1)
    glGetShaderiv(idShader, GL_COMPILE_STATUS, compiled, 0)
        
    if (compiled[0] == GL_FALSE) {
        val msg = "Could not compile shader. ${glGetShaderInfoLog(idShader)}\n ${source}"
        glDeleteShader(idShader)
        throw IllegalStateException(msg)
    }
        
    return idShader
}

shader program

Shaders are not directly used. You have to link them into one program.

Link shaders example

You can also add factory method to create program from sources.

Code example
fun createProgram(vertexShaderSource: String,
                  fragmentShaderSource: String): IntArray {
    
    val idVertexShader: Int = compileShader(GL_VERTEX_SHADER, vertexShaderSource)
    val idFragmentShader: Int =
                compileShader(GL_FRAGMENT_SHADER, fragmentShaderSource)

    return intArrayOf(
                      linkProgram(idVertexShader, idFragmentShader),
                      idVertexShader,
                      idFragmentShader)
}
ShaderProgram.createProgram = function(gl, 
                          srcVertexShader = ShaderProgram.VERTEX_SHADER_DEFAULT, 
                          srcFragmentShader = ShaderProgram.FRAGMENT_SHADER_DEFAULT ){
   
   const idVS  = ShaderProgram
                     .compileShader(gl, gl.VERTEX_SHADER, srcVertexShader);
   
    const idFS = ShaderProgram
                     .compileShader(gl, gl.FRAGMENT_SHADER, srcFragmentShader); 
    
    const idProg = ShaderProgram.linkProgram(gl, idVS, idFS);
    
    return {
        idProgram : idProg,
        idVertexShader: idVS,
        idFragmentShader: idFS
    };
}    

drawing with shaders

To use shaders you must activate program and bind input and uniform data before drawing functions.

If you have only one program, you can do it on setup stage.

input data

Variables with qualifier in treat as input data.

These variables have location, i.e. index. If you did not specify a location in the shader, the location will be generated. The location may differ from the order in which the variable was declared.

The glGetAttribLocation() function allows to get location by the variable name.

val vCoordLocation = glGetAttribLocation(idProgram, "vCoord") 

Also you can bind location by the glBindAttribLocation() function.

glBindAttribLocation(idProg, newLocation, "vCoord")

Further you can use glBindBuffer() functions to bind buffer from which data will be read (read more about VBO).

uniform data

Like the input data, the uniform data also has locations. You can transfer data to the uniform only when the program is active.

glUseProgram(idProgram)

glUniform1i(glGetUniformLocation(idProgram, "withTexture"),0)
glUniform4f(glGetUniformLocation(idProgram, "uColor"), 1,0,1,1)

int matrixLocation = glGetUniformLocation(idProgram, "matrix");
glUniformMatrix4fv(matrixLocation, false, matrix.get(matrixBuffer));

optimization

You can find locations of variables only once and store them somewhere.

If uniform data is used in the several shader programs, you can place it in the UBO (uniform block object).

cleanup

When the program and shaders are not needed, you can free up resources.

glDetachShader(idProgram, idVertexShader)
glDetachShader(idProgram, idFragmentShader)
glDeleteProgram(idProgram)
glDeleteShader(idVertexShader)
glDeleteShader(idFragmentShader)

You can download full sources on GitHub.

There are libraries of shaders, for example, https://github.com/patriciogonzalezvivo/lygia. Also check the book on https://thebookofshaders.com/.