GLSL Basics Back
GLSL bases on ANSI C and many features of the C programming language can be used in GLSL, except something go against performance or language simplicity (語言易用性).
This chapter mainly covers two topics:
- Language basics
- Shader input/output variables
1. Language basics
GLSL version decides how you can code it, which is always bound to a specific OpenGL version. If you want to use some new features, keep in mind checking the minimum version requirements.
1.1 Instructions
Like C, each instruction should end with a semicolon (;
), and there can be more than one instruction per line:
vec4 g; g = vec4(1, 0, 1, 1);
All variables declared in a block should end their lifecycle when the block finishes:
float a = 1.0;
float b = 2.0;
{
float a = 4.0;
float c = a + 1.0; // => 4.0 + 1.0
}
b = b + c; // error
1.2 Basic types
GLSL has extended standard C types with others mainly representing vectors or exposing the internal GPU architecture.
type | description |
---|---|
bool |
binary value of true or false |
int |
including int or uint (unsigned int) |
sampler |
types that represent textures: sampler1D , sampler2D , sampler3D |
float |
|
Vectors | bvec2 , bvec3 , bvec4 (vectors of 2, 3, and 4 Boolean elements) |
ivec2 , ivec3 , ivec4 (vectors of 2, 3, and 4 Integers elements) |
|
uvec2 , uvec3 , uvec4 (vectors of 2, 3, and 4 Unsigned Integer elements) |
|
vec2 , vec3 , vec4 (vectors of 2, 3, and 4 Floats, single precision) |
|
dvec2 , dvec3 , dvec4 (vectors of 2, 3, and 4 Floats, double precision) |
|
Matrices | mat2 , mat3 , mat4 (2x2, 3x3, and 4x4 Matrices of Floats, single precision) |
dmat2 , dmat3 , dmat4 (2x2, 3x3, and 4x4 Matrices of Floats, double precision) |
|
mat{m}x{n} like mat2x3 , mat3x4 (m for columns and n for rows) |
|
dmat{m}x{n} like above |
Note: Like C language, float literals in GLSL will be specified as a single-precision float by default without putting the suffix f
, and lf
should be putting for specifying as double-precision. Both suffixes are case-insensitive.
1.3 Variable initializers
To initialize types like vectors or matrices, we must use the constructor:
float a = 1.0;
bool switch = false;
ivec3 b = ivec3(1, 2, 3); // OK
uvec2 c = uvec2(-1, 1); // Error, should only contain unsigned integers
vec3 c(1.0, 2.0); // Error, should use assignment
vec3 c = vec3(1.0, 2.0); //OK
Vector initialization can also be mixed:
vec4 a = vec4(1.0, vec3(0.0, 1.0, 0.0));
vec4 a = vec4(vec3(0.0, 1.0, 0.0), 1.0);
vec4 a = vec4(vec2(1.0, 1.0), vec2(0.5, 0.5));
For vectors, they can be seen as structures or arrays, and you can index it like this:
vec2 p;
p[0] = 1.0;
p.x = 1.0;
p.y = 2.0;
p.z = 3.0; // Error, p only has two elements.
The following are the valid names of the structure's fields:
{x, y, z, w}
: positions{r, g, b, a}
: colours{s, t, p, q}
: texture coordinates
GLSL allows to swizzle (重組) the components of a vector (that is, construct a new vector by duplicating or reordering the elements):
vec4 color1 = vec4(0.5, 0.2, 1.0, 1.0); // RGBA
vec4 color2 = color1.abgr; // equivalent to color1.wzyx
// Grey color based on the red component
vec4 redGray = color1.rrrr;
float red = color1.r;
// Swizzle randomly but valid
vec4 color3 = color1.gbgb;
vec4 color4 = vec4(color1.rr, color2.bb);
vec4 color5 = color1.tptp; // same as .gbgb and .yzyz
color5.xy = vec2(1.0, 0.0);
color5[3] = 2.0; // same as .z = 2.0
// Invalid swizzles
vec2 p;
p = color.rgb; // .rgb is vec3
p[2] = 3.0; // index out of bounds
vec4 color7 = color1.xxqq; // Error, the fields are not from the same set
When it comes to GLSL matrices, they are different from a matrix. The first index of GLSL matrices is the column index, while the second one is the row index.
mat4 m;
m[0] = vec4(1.0); // fill the first column with 1.0
m[1][1] = 2.0;
m[2] = color1.rrgg; // fill the third column with a swizzled vector
1.4 Vector and Matrix operations
Some arithmetic operators have been overloaded for vectors and matrices to match linear algebraic conventions (綫性代數法則).
mat3 R, T, M;
vec3 v, b;
float f;
b = v + f;
// same as followed
b.x = v.x + f;
b.y = v.y + f;
b.z = v.z + f;
b = T * v;
// same as followed
b.x = T[0].x * v.x + T[1].x * v.y + T[2].x * v.z;
b.y = T[0].y * v.x + T[1].y * v.y + T[2].y * v.z;
b.z = T[0].z * v.x + T[1].z * v.y + T[2].z * v.z;
M = T * R;
// same as followed
M[0][0] = T[0][0] * R[0][0] + T[1][0] * R[0][1] + T[2][0] * R[0][2];
M[1][0] = T[0][0] * R[1][0] + T[1][0] * R[1][1] + T[2][0] * R[1][2];
M[2][0] = T[0][0] * R[2][0] + T[1][0] * R[2][1] + T[2][0] * R[2][2];
M[0][1] = T[0][1] * R[0][0] + T[1][1] * R[0][1] + T[2][1] * R[0][2];
M[1][1] = T[0][1] * R[1][0] + T[1][1] * R[1][1] + T[2][1] * R[1][2];
M[2][1] = T[0][1] * R[2][0] + T[1][1] * R[2][1] + T[2][1] * R[2][2];
M[0][2] = T[0][2] * R[0][0] + T[1][2] * R[0][1] + T[2][2] * R[0][2];
M[1][2] = T[0][2] * R[1][0] + T[1][2] * R[1][1] + T[2][2] * R[1][2];
M[2][2] = T[0][2] * R[2][0] + T[1][2] * R[2][1] + T[2][2] * R[2][2];
Sometimes we can see vector as an {m}x1 matrix, and we can translate an {n}x{m} matrices to an {n}x1 vector by multiplying them.
1.5 Castings and Conversions
Casting implicitly can only be done when there won't be precision issues like from int
to uint
, from int
to float
, from float
to double
, and so on. Otherwise, you must cast explicitly.
float threshold = 0.5;
int a = int(threshold); // => 0
double value = 0.334lf;
float value1 = float(value); // may lost precision
bool c = true;
float t = float(c); // => 1.0
bool d = false;
float f = float(d); // => 0.0
1.6 Code comments
Like C language, use double slashes (//
) for commenting single line of code or slash-asterisk (/*
) for commenting blocks of the snippet.
1.7 Flow controls
If-else statement and switch statement are both common cases in the flow controls in any programming languages.
1.8 Loops
For, while and do-while statements are all common cases in loops in any programming languages.
1.9 Structures
We always use structures to describe more complicated types of basic types with a given custom name.
struct Texture
{
vec4 color;
vec4 bgColor;
};
Then, we can declare variant with it:
Texture t = Texture(vec4(1.0, 0.0, 0.0, 1.0), vec4(1.0, 1.0, 1.0, 1.0));
t.color = vec4(1.0, 1.0, 1.0, 1.0);
1.10 Arrays
There are no dynamic arrays in GLSL like C programming language. Once you initialize an array with static capacity, you can't modify it:
Texture textures[3];
textures[3] = Texture(vec4(1.0, 0.0, 0.0, 1.0), vec4(1.0, 1.0, 1.0, 1.0)); // error, index out of bounds
If you want to get the capacity of an array, use the .length()
method:
for (int i = 0; i < textures.length(); i++)
{
// do anything
}
You cal also initialize an array directly during declaration:
Texture textures[] = {Texture(vec4(), vec4())};
1.11 Functions
Developers always use functions for reusing code and reduce redundant logical. To some extent,
proper abstracts of functions can help code more readable. As usual in C, the first keyword is the return type, which should be void
if you want it to return nothing.
void main()
{
// code
}
When it comes to the parameters' definition, it is more complicated because you can describe it with const
, and in/out/inout
qualifiers (修飾詞). For instance:
vec3 Lighten(const in Light light, const in vec3 position);
This function receives a Light
type variable and a vec3
variable as input. The qualifier const
means that the variable will not be modified inside the function. The qualifier in
means that the variable will remain unchanged after the function call no matter how the function changes it (like passing by value). out
means that the function won't initialize a new value and modify the parameter directly. After the function call, such a value will be output to the specified variable (like passing by reference). inout
combines both of above.
void MyFunction(in float a, out int b, inout float c)
{
a = 0.0;
b = int(c + a);
c = 3.0;
}
void main()
{
float in1 = 10.5;
int out1 = 5;
float out2 = 10.0;
MyFunction(in1, out1, out2); // => in1: 10.5, out1: 10.0, out2: 3.0
}
Once parameters have been declared as out
or inout
, they should be assigned with values inside a function, which means they can't be combined with const
qualifier.
If no qualifiers are specified, in
is the default one.
1.12 Preprocessor
Like in C, GLSL also has a preprocessor. Preprocessor tags start with the character #
, and they are before the compilation step, which will be evaluated and replaced before compilation. The following processor tags are the most important:
#error
#version
#pragma
#define
#if
,#ifdef
,#ifndef
,#else
,#elif
,#endif
Take #error
tag as an example, if the preprocessor reaches such a tag, the shader will be considered ill-formed (不規範) and not executable:
#ifndef DEG2RAD
#error missing conversion macro
#endif
The #version
tag is meant to output certain GLSL version. It helps ensure the compatibility of your current GLSL version. In common, it will be placed in the first line of each shader.
#version 430 // ensure running the GLSL version 4.30 (throw an error when not match)
The #pragma
tag is meant to configure the GLSL compiler like setting up debug/release mode, or optimizations:
#pragma debug(on) // #pragma debug(off)
#pragma optimize(on) // #pragma optimize(off)
Note: When developing, it is suggested that turns on debug mode and close optimization for more information when an error occurs. After ensuring that it works exactly like the non-optimized version, you can release it with optimization. Sometimes optimization will lead to some internal mistakes.
If you want to define some symbols in the preprocessor, you can use the #define
tag, or remove it with the #undef
tag:
#ifdef PI
#undef PI
#endif
#define PI 3.141592
The #if
and #endif
tags specify code between them to compile if the condition applies. #else
is for else, while #elif
for else-if.
The #ifdef
and #ifndef
are different from #if
because they only check the list of the preprocessor symbols.
2. Shader input and output variables
After learning the basics of GLSL, it is time to learn some functional parts of GLSL.
2.1 Uniform variables
A parameter that is passed from the application to the shaders is called a uniform variable. They are always constant, and global.
For example, we can set a uniform variable in the host programming (in C and OpenGL):
// C code to initialize a uniform variable
float lightPositions[] = {0, 10.0f, 0};
glUniform3fv(glGetUniformLocation(programId, "lightPosition"), lightPositions);
Then, in the shader, we can access such a variable like this:
#version 430
#pragma debug(on)
#pragma optimize(off)
uniform vec3 lightPosition; // => {0, 10.0f, 0}
void main()
{
// do anything with lightPosition
}
We can pass any uniform variables like integers, matrices, structures, etc.
2.2 Other input variables
Some other input variables shared between different pipeline stages must be declared globally in the shader with the keyword in
.
#version 430
#pragma debug(on)
#pragma optimize(off)
in vec3 vertexColor;
out vec4 frameBufferColor; // which will be written into the framebuffer
// fragment shader
void main()
{
frameBufferColor = vec4(vertexColor, 1.0);
}
Like snippet shown above, vertexColor
is an input variable that holds the output from the last shader (a vertex shader
)
2.3 Shader output variables
Each shader should have its form of output for the next one. For a fragment shader, the output should at least the colour of a fragment. For a vertex shader, the output should at least the vertex position in clip coordinates, and some other vertex attributes that will be passed to the fragment shader.
We can define such output with the qualifier out
globally.
As the plugin is integrated with a code management system like GitLab or GitHub, you may have to auth with your account before leaving comments around this article.
Notice: This plugin has used Cookie to store your token with an expiration.