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:
- Load the vertex shader from its source file.
- Load the fragment shader from its source file.
- Compile each shader source.
- Create a shader program.
- Add each shader to the program and link.
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 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.