In this part we will introduce the content of GLSL Shaders, by directly writing C++ code to control the operation of the GPU, using GLSL to create fragment shaders, and understand how they work; we can do some creative work through fragment shaders.
1.Shaders
If we want to fully master WebGL, we need to learn how to use shaders. Shaders are very worth learning because we can find many excellent works of shaders on the creative platform on the Internet. Shading is the most creative programming method. One of the powerful and exciting forms of programming.
Shaders are not only useful for graphics programming, but can also be used to process various data, including the most advanced AI and machine learning; Tensorflow, Pytorch, and many machine learning systems are all based on GPU hardware, and use shaders to perform processing on GPU hardware Programming. Therefore, learning shaders can help us better understand how to control the computer and the knowledge of parallel computing.
We use shader to encode a mandlebrot set:
1 | precision mediump float; |
There is another example:
1 | precision mediump float; |
We can use less code to achieve such a complex effect, and faster.
2.GLSL - Basic Concepts
OpenGL is an Open Graphics Library and uses C-type language for coding; so we can use if statements, for loops, #define, etc. to enrich our code content.
GLSL has LOADS of cool built in functions, including lots of mathematical functions that are useful for doing computer graphics. Most of these should be familiar to you. It also has a number of built-in types. Some of them will be familiar, others won’t. There are other types of Shader Language, like HLSL, but GLSL works on the most systems.
GLSL comiles at runtime, that is because different graphics cards interpret GLSL differently, so we can’t distribute a runnable binary; this means that we need to distribute the source code with our application.
The shader has tobe loaded in our main application, justlike any other asset. If we want to interact with our shadeer, we need to use special variables that our pass into the shader from our main program; we also need to do this for any textures that we want to do shader operations with.
Why is GLSL so fast?
This is because of the GPU architecture that GLSL runs on; computations on the GPU are run in Parallel; for example, let’s consider a ‘Fragment’ shader, the screen is divided into fragments, each fragment runs the fragment shader program simultaneously. All the other drawing techniques cannot be executed in the same way.
The two most important variables in a fragment shader are as follows:
gl_FragCoord
// gl_FragCoord is the current 2d fragment coordinate
gl_FragColor
// gl_Fragcolor is the current Fragment’s 3d or 4d color in normalised units.
3.GLSL Types & Vectors
GLSL has lots of built in types, some of them might be familiar and work in a similar way: float, int, bool, array, struct; others are completely new: vec2, vec3, vec4, ivec2, ivec3, ivec4, bvec2, bvec3, bvec4, mat2, mat3, mat4…
In general, most graphics cards can’t compile shaders taht use integer literals; that means if we use a number, it should alwats be a float.
1 | distance *= 2.0; // right |
Vec2 is a 2D vector, we can define a vec2 as follows:
1 | vec2(0.5, 0.5); |
One great thing about vecs is that we can do math operations directly on them.
1 | vec2 output = vec2(0.5, 0.5) * 10.0; |
We can access elements of a vector using .x, .y, .z.
1 | vec2 output = vec2(0.5, 0.25) * 10.0; |
In GLSL, we usually define colours using 3D and 4D vectors. We can specify colours in either 3D or 4D:
1 | gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); |
Another great thing about GLSL vectors is that we can initialise all the cells in a vector with a single number.
1 | vec3 myVec = vec3(0.5) // myVec = (0.5, 0.5, 0.5) |
Another awesome thing si that we can do cellwise vector operations without having to write loops.
1 | vec2 myVec1 = vec2(2.0, 3.0); |
The final awesome thing is that we can select and combine elements of a vector using x, y, z, w notation and do maths with them.
1 | vec4 pos = vec4(1.0, 2.0, 3.0, 4.0); |
We can use bools as we might expect, and Int vectors.
1 | bvec3(true, false, true); |
4.GLSL Matrices
As we know, matrices are very handy for doing rotations, as well as consistently computing scaling and translation, we can define 2*2, 3*3 and 4*4 matrices by mat2, mat3, mat4.
GLSL matrices default to column major; this means the are defined column after column.
1 | mat4 myMat = mat4(1.0, 0.0, 0.0, 0.0, // 1st column |
Also, here is an example of how to define a rotation matrix:
1 | mat2 rotation = mat2(cos(angle), sin(angle), -sin(angle), cos(angle)); |
We can simplty multiply any 2D vector by this mat2 to rotate that vector.
We can access and set elements of a matrix as follows:
1 | mat4 m; |
5.Uniforms and Textures
A uniform is a variable of any type, that is passed into the shader from our main program; the uniform in the shader must have the same name as we defined in our main program; common uniforms include the screen size / resolution x,y, the mouse x,y positions, and a time variable.
Images can be passed into fragment shaders and accessed as uniforms with the sample 2D type. Inorder to do this, we need to set up the texture input in our main program, we would set up the texture like this:
1 | uniform sampler2D myTexture; |
6.Frag Shaders: Drawing Shapes
Let’s look at some basic fragshaders:
1 | // Start by setting the precision |
So, how do we go about drawing a simple image?
We will begin with drawing a circle, as this is much easier than you might think. There are a number of different ways of doing it, but hte principles are basically the same.
The first thing we want to do is figure out where we are going to draw our circle, we can create a vector that contains this point:
1 | vec2 pos = resolution.xy / 2.0; |
Now all we have to do is get the distance from the centre to the current fragment(pixel), and them use this to determine a color, we can do this with the distance function:
1 | colour = distance(gl_FragCoord.xy, pos); |
But it is not a circle, it’s a distance field.
Distance fields are the primary method for generating shapes in fragment shaders; this is because the only thing the shader really knows is where it is on the screen; so, all programs we write need to use more or less just this information to work out what colour they should be.
We can work this out by setting up distance fields for all the objects in our scene, and them making decisions about colours based on these distances. In order to turn the distance field into a circle, we just need to set the colour only if the distance is over a certain amount.
This amount is going to be the radius of our circle:
1 | if(colour > 1.0) colour = 1.0; |
Another method is to get the squared distance, this is a really cool method that makes use of the dot product; it is the product of the Euclidean magnitudes of the two vectors and the cosine of the angle between them.
We can use the built-in GLSL dot function, it is really fast.
1 | vec2 pos = gl_FragCoord.xy - resolution / 2.0; |
Other interesting functions to interpolate a boundary:
1 | smoothstep(start, end, input); |
We will literally use this all the time.
I would like to share one of my shader work:
About this Post
This post is written by Siqi Shu, licensed under CC BY-NC 4.0.