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.
Lets start with the basic definition of the Shader type.
type Shader struct {
, Fragment string // shader source
Vertexuint32 // opengl program
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) {
, err := ioutil.ReadFile(filename)
bytes
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 .Vertex = shaderSourceFromFile(vertexShaderFilename)
shader.Fragment = shaderSourceFromFile(fragmentShaderFilename)
shader.Program = createGLProgram(shader.Vertex, shader.Fragment)
shaderreturn shader
}
func createGLProgram(vertexShader, fragmentShader string) (uint32, error) {
var err error
var vertexShader, fragmentShader uint32
= compileShader(vertexShader, gl.VERTEX_SHADER)
vertexShader = compileShader(fragmentShader, gl.FRAGMENT_SHADER)
fragmentShader
:= gl.CreateProgram()
prog .AttachShader(prog, vertexShader)
gl.AttachShader(prog, fragmentShader)
gl.LinkProgram(prog)
gl
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 {
:= gl.CreateShader(shaderType)
shader , free := gl.Strs(source)
csources.ShaderSource(shader, 1, csources, nil)
gl()
free.CompileShader(shader)
glreturn shader
}
To summarize, the process is this:
The result is a shader we can enable during rendering by
calling gl.UseProgram(shader.Program)
.
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 {
, Fragment string // shader source
Vertexuint32 // opengl program
Program map[string]ShaderAttribute
Attributes }
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 {
string // Attribute name as it appears in the shader source
Name uint32 // OpenGL attribute index - assigned with EnableVertexAttribArray
Location // GLSL type (vec2, vec3 etc)
Type GLSLType []float32 // default value for this attribute
Default []float32 // data of this attribute
Data }
type GLSLType struct {
string // e.g. vec3
Name int32 // number of float32s
Size }
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
.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)
glreturn vbo
}
The attributes definition for the shader shown above would look like this:
:= map[string]ShaderAttribute{
attributes "vertex": {
: "vertex",
Name: 0,
Location: GLSLType{"vec3", 3},
Type},
"uv": {
: "uv",
Name: 1,
Location: GLSLType{"vec2", 2},
Type},
"color": {
: "color",
Name: 2,
Location: GLSLType{"vec4", 4},
Type: []float32{1, 1, 1, 1},
Default},
"transform": {
: "transform",
Name: 3,
Location: GLSLType{"vec3", 3},
Type: []float32{0, 0, 0},
Default},
"rotation": {
: "rotation",
Name: 4,
Location: GLSLType{"vec3", 3},
Type: []float32{0, 0, 0},
Default},
"scale": {
: "scale",
Name: 5,
Location: GLSLType{"vec2", 2},
Type: []float32{1, 1},
Default},
}
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 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 {
, Fragment string // shader source
Vertexuint32 // opengl program
Program map[string]ShaderAttribute // named access to shader attributes
Attributes map[string]GLSLType // named access to shader uniform parameters
Uniforms }
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")
}
:= gl.GetUniformLocation(s.Program, gl.Str(name+"\x00"))
loc
switch s.Uniforms[name].Name {
case "float":
.Uniform1f(loc, value[0])
glcase "vec2":
.Uniform2f(loc, value[0], value[1])
glcase "vec3":
.Uniform3f(loc, value[0], value[1], value[2])
glcase "vec4":
.Uniform4f(loc, value[0], value[1], value[2]), value[3])
glcase "mat4":
.UniformMatrix4fv(loc, 1, false, &(value[0]))
gldefault:
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.