Shader Type

Shaders are programs that run on the GPU and calculate how objects (in our case sprites) look. We have seen how to define a vertex shader and a fragment shader and then combine them into a shader program that we use when rendering. Our shader type will automate this process.

Shader Type Definition

Lets start with the basic definition of the Shader type.

type Shader struct {
    Vertex, Fragment string // shader source
    Program          uint32 // opengl program
}

Vertex and Fragment are strings that hold the source of our shaders. Program is the OpenGL program that we get after the two shaders are compiled and linked. In previous examples we defined our shader code in go strings like this:

var vertexShaderSource = `
#version 410
layout (location=0) in vec3 vertex; // vertex position, comes from currently bound VBO
layout (location=1) in vec2 uv; // per-vertex texture co-ords
// rest of shader...
` + "\x00"

var fragmentShaderSource = `
in vec2 textureCoords;
// rest of shader...
` + "\x00"

This is adequate for simple examples but it is more convenient to be able to write the shaders in their own source files. Many code editors support GLSL syntax highlighting and code completion so having the shaders in their own files lets us take advantage of that as well. Loading the source from a file is done with this utility function. It simply loads the file in an string and adds the null terminator at the end.

func shaderSourceFromFile(filename string) (string, error) {
    bytes, err := ioutil.ReadFile(filename)

    if err != nil {
        return "", err
    }
    return string(bytes) + "\x00", nil
}

We define a constructor to create a Shader given two source files (most error checking is omitted):

func NewShaderFromFiles(vertexShaderFilename, fragmentShaderFilename string) Shader{
    shader := Shader{}
    shader.Vertex = shaderSourceFromFile(vertexShaderFilename)
    shader.Fragment = shaderSourceFromFile(fragmentShaderFilename)
    shader.Program = createGLProgram(shader.Vertex, shader.Fragment)
    return shader
}

func createGLProgram(vertexShader, fragmentShader string) (uint32, error) {
    var err error
    var vertexShader, fragmentShader uint32
    vertexShader = compileShader(vertexShader, gl.VERTEX_SHADER)
    fragmentShader = compileShader(fragmentShader, gl.FRAGMENT_SHADER)

    prog := gl.CreateProgram()
    gl.AttachShader(prog, vertexShader)
    gl.AttachShader(prog, fragmentShader)
    gl.LinkProgram(prog)

    return prog, nil
}

The compileShader function creates the shader of the appropriate shader type (gl.VERTEX_SHADER or gl.FRAGMENT_SHADER) and then loads the shader source, which is given in a go string, into an OpenGL-appropriate buffer called ShaderSource and compiles it.

func compileShader(source string, shaderType uint32) uint32 {
    shader := gl.CreateShader(shaderType)
    csources, free := gl.Strs(source)
    gl.ShaderSource(shader, 1, csources, nil)
    free()
    gl.CompileShader(shader)
    return shader
}

To summarize, the process is this:

The result is a shader we can enable during rendering by calling gl.UseProgram(shader.Program).

Shader Attributes

To use a shader, we must create vertex buffers for all layout attributes. In previous examples, we only passed vertex and uv information to our shaders and we created two vertex buffers to match that. In this way, the relation between shader attributes and vertex buffers was implicit in the code. In real applications, each shader can have its own different attributes and the number, type and location of shader attributes can vary. Lets see a more realistic example:

#version 410

layout (location=0) in vec3 vertex; 
layout (location=1) in vec4 uv; 
layout (location=2) in vec4 color; 
layout (location=3) in vec3 transform;
layout (location=4) in vec3 rotation;
layout (location=5) in vec2 scale;

//...

For us to be able to dynamically create vertex buffers that match these attributes we need to store each attribute’s location and type. We will do this in a map which we will add to our shader type.

type Shader struct {
    Vertex, Fragment string // shader source
    Program          uint32 // opengl program
    Attributes       map[string]ShaderAttribute 
}

The Attribute map is keyed by the attribute’s name. The attribute information is stored in a ShaderAttribute type which is defined as:

type ShaderAttribute struct {
    Name     string    // Attribute name as it appears in the shader source
    Location uint32    // OpenGL attribute index - assigned with EnableVertexAttribArray
    Type     GLSLType  // GLSL type (vec2, vec3 etc)
    Default  []float32 // default value for this attribute
    Data     []float32 // data of this attribute
}

type GLSLType struct {
    Name string // e.g. vec3
    Size int32 // number of float32s
}

The Attribute struct holds the name of the attribute (as it is in the shader source), the attribute location and and its type. Parameters Default and Data are used to store data for an attribute and are used in the Sprite type which we will define in a later tutorial. Location and type are useful when creating vertex buffers.

func CreateVertexBuffer(attribute ShaderAttribute) uint32{
    var vbo uint32
    gl.GenBuffers(1, &vbo)
    gl.BindBuffer(gl.ARRAY_BUFFER, vbo)
    gl.VertexAttribPointer(attribute.Location, int32(attribute.Type.Size), gl.FLOAT, false, 0, nil)
    gl.EnableVertexAttribArray(attribute.Location)
    return vbo
}

The attributes definition for the shader shown above would look like this:

attributes := map[string]ShaderAttribute{
    "vertex": {
        Name:     "vertex",
        Location: 0,
        Type:     GLSLType{"vec3", 3},
    },  
    "uv": {
        Name:     "uv",
        Location: 1,
        Type:     GLSLType{"vec2", 2},
    },
    "color": {
        Name:     "color",
        Location: 2,
        Type:     GLSLType{"vec4", 4},
        Default:  []float32{1, 1, 1, 1},
    },
    "transform": {
        Name:     "transform",
        Location: 3,
        Type:     GLSLType{"vec3", 3},
        Default:  []float32{0, 0, 0},
    },
    "rotation": {
        Name:     "rotation",
        Location: 4,
        Type:     GLSLType{"vec3", 3},
        Default:  []float32{0, 0, 0},
    },
    "scale": {
        Name:     "scale",
        Location: 5,
        Type:     GLSLType{"vec2", 2},
        Default:  []float32{1, 1},
    },
}

Obviously this information must match what is defined in the shader. In the AGL shader package we do a bit of parsing to ensure that each attribute is present in the shader and has the correct type.

Uniforms

Uniforms are shader parameters that have a constant value across all invocations of the shader meaning they don’t change from vertex to vertex or from fragment to fragment. In previous examples, we used a uniform parameter to pass a texture to our shader. Uniforms can be used to pass other information as well. In this example we pass a 4×44\times4 matrix and a float to the vertex shader.

#version 410
layout (location=0) in vec3 vertex;
layout (location=1) in vec4 uv; 

uniform mat4 matrix;
uniform float time;

As with attributes, it is beneficial to record information about uniform parameters in the Shader struct.

type Shader struct {
    Vertex, Fragment string // shader source
    Program          uint32 // opengl program
    Attributes map[string]ShaderAttribute // named access to shader attributes
    Uniforms   map[string]GLSLType        // named access to shader uniform parameters
}

We don’t need a location or defaults for our uniforms so we just store the type of each one.

Normally, to update a uniform we must use an OpenGL function to get access to it using it’s name and then call the appropriate update function based on the uniform’s type. Using our Uniforms map we can provide a more convenient way to set these uniform parameters.

func (s *Shader) UpdateUniform(name string, value []float32) error {
    if _, ok := s.Uniforms[name]; !ok {
        return errors.New("Attribute not found")
    }

    loc := gl.GetUniformLocation(s.Program, gl.Str(name+"\x00"))

    switch s.Uniforms[name].Name {
    case "float":
        gl.Uniform1f(loc, value[0])
    case "vec2":
        gl.Uniform2f(loc, value[0], value[1])
    case "vec3":
        gl.Uniform3f(loc, value[0], value[1], value[2])
    case "vec4":
        gl.Uniform4f(loc,  value[0], value[1], value[2]), value[3])
    case "mat4":
        gl.UniformMatrix4fv(loc, 1, false, &(value[0]))
    default:
        return errors.New("Unknown uniform name")
    }

    return nil
}

Hopefully, by now you are wondering what all these parameters do. In the next tutorial we will define a shader that uses these attributes and that shader will became the default shader used in our renderer.