chapters 12 & 13
22 KiB
An introduction for those coming from JS
If you're a JavaScript developer, chances are you'll be a bit puzzled when reading the book. Indeed, there are many differences between manipulating high-level JS and getting down and dirty with shaders. Yet, as opposed to the underlying assembly language, GLSL is human readable and I'm sure that, once you acknowledge its specificities, you'll quickly be up and running.
I assume you have a prior (be it shallow) knowledge of JavaScript of course, but also of the Canvas API. If not, don't worry, you'll still be able to get most of this section.
Also, I won't go too much into details and some things may be half true, don't expect a "definitive guide" but rather
A BIG HUG
JavaScript is great at quick prototyping ; you throw a bunch of random, untyped variables and methods, you can dynamically add and remove class members, refresh the page and see if it works, make changes accordingly, refresh the page, repeat, life is easy. So you may wonder what is the difference between JavaScript and GLSL. After all, both run in the browser, both are used to draw a bunch of funky stuff on a screen and to that extent, JS is easier to use.
Well, the main difference is that Javascript is an interpreted language while GLSL is a compiled language. A compiled program is executed natively on the OS, it is low level and generally fast. An interpreted program requires a Virtual Machine (VM) to be executed, it is high level and generally slow.
When a browser (the JavaScript VM) executes or interprets a piece of JS, it has no clue about which variable is what and which function does what (with the notable exception of TypedArrays). Therefore it can't optimize anything upfront, so it takes some time to read your code, to infer (deduce from the usage) the types of your variables and methods and when possible, it will convert some of your code into assembly code that will execute much faster.
It's a slow, painstaking and insanely complex process, if you're interested in the details, I'd recommend watching how Chrome's V8 engine works. The worst is that every browser optimizes JS its way and the process is hidden from you ; you are powerless.
A compiled program is not interpreted ; the OS runs it, if the program is valid, the program is executed. That's a big change ; if you forget a semicolon at the end of line, your code is invalid, it will not compile: your code won't turn into a program at all.
That's cold but that's what a shader is: a compiled program executed on the GPU. Fear not! a compiler, the piece of program that makes sure your code is valid, will become your best friend. The examples of this book and the companion editor are very user friendly. They'll tell you where and why your program failed to compile, then you'll have to fix things and whenever the shader is ready to compile, it will be displayed instantly. That's a great way of learning as it's very visual and you can't really break anything.
Last note, a shader is made of 2 programs, the vertex shader and the fragment shader. In a nutshell, the vertex shader, the first program, receives a geometry as an input and turns it into series of pixels (or fragments) then hands them over to the fragment shader, the second program, that will decide which color to paint the pixels. This book is mostly focused on the latter, in all the examples, the geometry is a simple quadrilateral that covers the whole screen.
SO! ready?
off we go!
strong types
When you come from JS or any untyped language, typing your variables is an alien concept, making typing the hardest step to take towards GLSL.
Typing, as the name suggests, means that you'll give a type to your variables (and functions of course).
This basically means that the word var
doesn't exist anymore.
The GLSL thought-police erased it from the common tongue and you're not able to speak it because, well... it doesn't exist.
Instead of using the magic word var
, you'll have to explicitly specify the type of each variable you use, then the compiler will only see objects and primitives it knows how to handle efficiently.
The downside when you can't use the var
keyword and must specify everything, is that you'll have to know the type of all the variables and know them well.
Rest assured, there are few and they're fairly simple (GLSL is not a Java framework).
Might sound scary but all in all, it's not very different from what you're doing when you code JavaScript ; if a variable is a boolean
, you'll expect it to store true
or false
and nothing else.
If a variable is called var uid = XXX;
, chances are that you'll store an integer value in there and a var y = YYY;
might be a reference to a floating point value.
Even better, with strong types, you won't waste time wondering if X == Y
(or was it typeof X == typeof Y
? .. or typeof X !== null && Y...
... anyway) ; you'll just know it and if you don't, the compiler will.
Here are the scalar types (a scalar describes a quantity) you can use in GLSL: bool
(Boolean), int
(Integer), float
(floating point Number).
There are other types but let's take it easy, the following snippet shows how to declare vars
(yes, I spoke the forbidden word) in GLSL:
//a Boolean value:
JS: var b = true; GLSL: bool b = true;
//an Integer value
JS: var i = 1; GLSL: int i = 1;
//a Float value (a Number)
JS: var f = 3.14159; GLSL: float f = 3.14159;
Not that hard right? as mentioned above, it even makes things easier when it comes to coding as you don't waste your time checking the type of a given variable. When in doubt, remember that you're doing this for your program to run immensely faster than in JS.
void
There is a void
type that roughly corresponds to null
, it is used as the return type of a method that doesn't return anything.
you can't assign it to a variable.
boolean
As you know, Booleans are mostly used in conditional tests ; if( myBoolean == true ){}else{}
.
If the conditional branching is a valid option on the CPU, the parallel nature of GLSL makes it less true.
Using conditionals is even discouraged most of the time, the book explains a couple of alternative techniques to solve this.
type casting
As Boromir put it, "One does not simply combine Typed primitives". Unlike JavaScript, GLSL will not allow you to perform operations between variables of different types.
This for instance:
int i = 2;
float f = 3.14159;
//trying to multiply an integer by a float value
float r = i * f;
will not play nice because you're trying to crossbreed a cat and a giraffe.
The solution to this is to use type casting ; it will make the compiler believe that i
is of type float
without actually changing the type of i
.
//casting the type of the integer variable 'i' into float
float r = float( i ) * f;
Which is strictly equivalent to dressing up a cat in a giraffe outfit and will work as expected ( r
will store the result of i
x f
).
It is possible to cast any of the above types into any other type, note that casting a float
to int
will behave like a Math.floor()
as it will remove the values behind the floating point.
Casting a float
or a int
to bool
will return true
if the variable is not equal to zero.
constructor
The variable types are also their own class constructor ; in fact a float
variable can be thought of as an instance
of a Float
class.
This declarations are equally valid:
int i = 1;
int i = int( 1 );
int i = int( 1.9995 );
int i = int( true );
This may not sound like much for scalar
types, it's not very different from casting, but it will make sense when addressing the overload section.
Ok, so these three are the primitive types
, things you can't live without but of course, GLSL has more to offer.
Vectors
In Javascript like in GLSL, you'll need more sophisticated ways of handling data, that's where vectors
come in handy.
I suppose that you've already coded a Point
class in JavaScript to hold together a x
and a y
value, the code for this would go like:
// 'class' definition:
var Point = function( x, y ){
this.x = x || 0;
this.y = y || 0;
}
//and you would instantiate it like:
var p = new Point( 100,100 );
As we've just seen, this is SO wrong at SO many levels! That var
keyword for one, then the horrendous this
, then again untyped x
and y
values...
No, this is not going to work in shaderland.
Instead, GLSL exposes built-in data structures to hold data together, namely:
bvec2
: a 2D Boolean vector,bvec3
: a 3D Boolean vector,bvec4
: a 4D Boolean vectorivec2
: a 2D Integer vector,ivec3
: a 3D Integer vector,ivec4
: a 4D Integer vectorvec2
: a 2D Float vector,vec3
: a 3D Float vector,vec4
: a 4D Float vector
You immediately noticed that there's a type of vector for each primitive type, clever bunny.
From what we just saw, you can deduce that a bvec2
will hold two values of type bool
and a vec4
will hold four float
values.
Another thing introduced by vectors is a number of dimensions, it doesn't mean that a 2D vector is used when you render 2D graphics and a 3D vector when you do 3D. What would a 4D vector represent then? (well, actually it is called a tesseract or hypercube)
No, the dimensions represent the number and the type of components or variables stored into the vector:
// let's create a 2D Boolean vector
bvec2 b2 = bvec2 ( true, false );
// let's create a 3D Integer vector
ivec3 i3 = ivec3( 0,0,1 );
// let's create a 4D Float vector
vec4 v4 = vec4( 0.0, 1.0, 2.0, 1. );
b2
stores two different boolean values, i3
stores 3 different integer values and v4
stores 4 different float values.
but how to retrieve those values?
in the case of scalars
, the answer is obvious ; with float f = 1.2;
, the variable f
holds the value 1.2
.
With vectors it's a bit different and quite beautiful.
accessors
There are different ways of accessing the values
// let's create a 4D Float vector
vec4 v4 = vec4( 0.0, 1.0, 2.0, 3.0 );
to retrieve the 4 values, you can do the following:
float x = v4.x; // x = 0.0
float y = v4.y; // y = 1.0
float z = v4.z; // z = 2.0
float w = v4.w; // w = 3.0
nice and easy ; but the following are equally valid ways of accessing your data:
float x = v4.x = v4.r = v4.s = v4[0]; // x = 0.0
float y = v4.y = v4.g = v4.t = v4[1]; // y = 1.0
float z = v4.z = v4.b = v4.p = v4[2]; // z = 2.0
float w = v4.w = v4.a = v4.q = v4[3]; // w = 3.0
And the clever bunny you are already noticed three things:
X
,Y
,Z
&W
are used in 3D programs to represent 3D vectorsR
,G
,B
&A
are used to encode colors and alpha[0]
,[1]
,[2]
&[3]
mean that we have a random access array of values
So depending on whether you're manipulating 2D or 3D coordinates, a color with or without an alpha value or simply some random variables, you can pick the most suited vector type and size.
Typically 2D coordinates and vectors (in the geometric sense) are stored as a vec2
, vec3
or vec4
, colors as vec3
or vec4
if you need opacity but there is no restriction on how to use the vectors.
For instance, if you want to store only one boolean value in a bvce4
, it's possible, it's just a waste of memory.
note: in a shader, color values (R
, G
, B
& A
) are normalised, they range from 0 to 1 and not from 0 to 0xFF, so you'd rather use a Float vec4
than an Integer ivec4
to store them.
Nice already, but there's more!
swizzle
It is possible to return more than one value at once ; say you need only the X
and Y
values of a vec4
, in JavaScript, you'd have to write something like:
var needles = [0, 1]; // location of 'x' & 'y' in our data structure
var a = [ 0,1,2,3 ]; // our 'vec4' data structure
var b = a.filter( function( val, i, array ) {
return needles.indexOf( array.indexOf( val ) ) != -1;
});
// b = [ 0, 1 ]
//or more literally:
var needles = [0, 1];
var a = [ 0,1,2,3 ]; // our 'vec4' data structure
var b = [ a[ needles[ 0 ] ], a[ needles[ 1 ] ] ]; // b = [ 0, 1 ]
Ugly. In GLSL you can retrieve them like so:
// create a 4D Float vector
vec4 v4 = vec4( 0.0, 1.0, 2.0, 3.0 );
//and retrieve only the X & Y components
vec2 xy = v4.xy; // xy = vec2( 0.0, 1.0 );
What just happened?! when you concatenate accessors, GLSL gracefully returns a subset of the values you asked for, in the best suited vector format. Indeed, the vector is a random access data structure, like an array in JavaScript if you want. So not only can you retrieve a subset of your data, but you can also specify the order in which you need it, this will invert the values of the components of a vector:
// create a 4D Float vector: R,G,B,A
vec4 color = vec4( 0.2, 0.8, 0.0, 1.0 );
//and retrieve the color components in the A,B,G,R order
vec4 backwards = v4.abgr; // backwards = vec4( 1.0, 0.0, 0.8, 0.2 );
And of course, you can ask the same component multiple times:
// create a 4D Float vector: R,G,B,A
vec4 color = vec4( 0.2, 0.8, 0.0, 1.0 );
//and retrieve a GAG vec3 based on the G & A channels of the color
vec3 GAG = v4.gag; // GAG = vec4( 0.8, 1.0, 0.8 );
This is extremely handy to combine parts of vectors together, extract only the rgb channels of a RGBA color etc.
overload everything!
In the types section, I mentioned something about the constructor and that's yet again a great feature of GLSL ; overloading. For those who don't know, overloading an operator or a function roughly means: 'changing the behaviour of said operator or function depending on the operands/arguments'. Overloading is not allowed in JavaScript, so this may be a bit strange at first but I'm sure that once you get used to it, you'll wonder why it is not implemented in JS (short answer, typing).
The most basic example of operator overloading goes as follow:
vec2 a = vec2( 1.0, 1.0 );
vec2 b = vec2( 1.0, 1.0 );
//overloaded addition
vec2 c = a + b; // c = vec2( 2.0, 2.0 );
WHAT? So you can add things that are not numbers?!
Yes, precisely. Of course this applies to all operators (+
, -
, *
& /
) but that's only the beginning.
Consider the following snippet:
vec2 a = vec2( 0.0, 0.0 );
vec2 b = vec2( 1.0, 1.0 );
//overloaded constructor
vec4 c = vec4( a , b ); // c = vec4( 0.0, 0.0, 1.0, 1.0 );
We built a vec4
out of two vec2
, by doing so, the new vec4
used the a.x
and a.y
as the X
, Y
components of c
.
Then it took b.x
and b.y
and used them as the Z
and W
components of c
.
This is what happens when a function is overloaded to accept different arguments, in this case, the vec4
constructor.
It means that many versions of the same method with a different signature can coexist in the same program, for instance the following declarations are all valid:
vec4 a = vec4(1.0, 1.0, 1.0, 1.0);
vec4 a = vec4(1.0);// x, y, z, w all equal 1.0
vec4 a = vec4( v2, float, v4 );// vec4( v2.x, v2.y, float, v4.x );
vec4 a = vec4( v3, float );// vec4( v3.x, v3.y, v3.z, float );
etc.
The only thing you should make sure of is to provide enough arguments to feed your vector.
Last thing, you are allowed to overload the built-in functions in your program so they can take arguments they were not designed for (this shouldn't happen too often though).
more types
Vectors are fun, they're the meat of your shader. There are other primitives such as Matrices and Texture samplers which will be covered later in the book.
We can also use Arrays. Of course they have to be typed and there are twists:
- they have a fixed size
- you can't push(), pop(), splice() etc. and there is no
length
property - you can't initialize them immediately with values
- you have to set the values individually
this won't work:
int values[3] = [0,0,0];
but this will:
int values[3];
values[0] = 0;
values[1] = 0;
values[2] = 0;
This is fine when you know your data or have small arrays of values.
If you want a more expressive way of declaring a variable,
there is also a struct
type. These are like objects without methods ;
they allow to store and access multiple variables inside the same object
struct ColorStruct {
vec3 color0;
vec3 color1;
vec3 color2;
}
then you can set and retrieve the values of colors by doing:
//initialize the struct with some values
ColorStruct sandy = ColorStruct( vec3(0.92,0.83,0.60),
vec3(1.,0.94,0.69),
vec3(0.95,0.86,0.69) );
//access a values from the struct
sandy.color0 // vec3(0.92,0.83,0.60)
This is syntactic sugar but it can help you write cleaner code, at least code you're more familiar with.
statements & conditions
Data structures are nice as such but we might need to iterate or perform conditional tests at some point. Fortunately for us, the syntax is very close to the JavaScript. A condition is like:
if( condition ){
//true
}else{
//false
}
A for loop is usually:
const int count = 10;
for( int i = 0; i <= count; i++){
//do something
}
or with a float iterator:
const float count = 10.;
for( float i = 0.0; i <= count; i+= 1.0 ){
//do something
}
Note that count
will have to be defined as a constant
.
This means prefixing the type with a const
qualifier, we'll cover this in a second.
we also have the break
and continue
statements:
const float count = 10.;
for( float i = 0.0; i <= count; i+= 1.0 ){
if( i < 5. )continue;
if( i >= 8. )break;
}
Note that on some hardware, break
does not work as expected and the loop doesn't bail out early.
In general, you'll want to keep the iteration count as low as possible and avoid the loops and the conditionals as often as you can.
qualifiers
On top of the variable types, GLSL uses qualifiers.
Long story short, qualifiers help the compiler know which variable is what.
For instance some data can only be provided by the CPU to the GPU, those are called attributes and uniforms.
The attributes are reserved for the vertex shaders, the uniforms can be used in both the vertex and the fragment shaders.
There's also a varying
qualifier used to pass variables between the vertex and the fragment shader.
I won't go too much into details here as we're mostly focused on the fragment shader but later in the book, you'll see something like:
uniform vec2 u_resolution;
See what we did here? We stuck a uniform
qualifier before the type of the variable
This means that the resolution of the canvas we're working on is passed to the shader from the CPU.
The width of the canvas is stored in the x and the height in the y component of the 2D vector.
When the compiler sees a variable preceded by this qualifier, it will make sure that you can't set those values at runtime.
The same applied to our count
variable which was the limit of our for
loop:
const float count = 10.;
for( ... )
When we use a const
qualifier, the compiler will make sure that we set the variable's value only once, otherwise it's not a constant.
There are 3 extra qualifiers that are used in the functions signatures : in
, out
and inout
.
In JavaScript, when you pass scalar arguments to a function, their value is read-only and if you change their values inside the function,
the changes are not applied to the variable outside the function.
function banana( a ){
a += 1;
}
var value = 0;
banana( value );
console.log( value );// > 0 ; the changes are not taken into account outside the function
With arguments qualifiers, you can specify the behaviour of the the arguments:
in
will be read-only ( default )out
write-only: you can't read the value of this argument but you can set itinout
read-write: you can both get and set the value of this variable
Rewriting the banana method in GLSL would look like
void banana( inout float a ){
a += 1.;
}
float A = 0.;
banana( A ); //now A = 1.;
This is very different from JS and quite powerful too but you don't have to specify the signature qualifiers (the default is read-only).
space & coordinates
Final note, in the DOM and the Canvas 2D, we're used to have the Y axis pointing 'down'. This makes sense in the context of a DOM as it follows the way a web page unrolls ; the navbar at the top, content expanding towards the bottom. In a WebGL canvas, the Y axis is flipped: Y points 'up'.
This means that the origin, the point (0,0), is located at the bottom left corner of a WebGL context, not at the top left corner like in a 2D Canvas. The textures coordinates follow this rule which might be counter-intuitive at first.
And we're done!
Of course we could have gone deeper into the various concepts but as mentioned earlier, this is meant to give a BIG HUG to the newcomers. It's a quite a lot to ingest but with patience and practice, this will become more and more natural.
I hope you found some of this useful, now what about starting your journey through the book?