Shaders use GLSL, a special OpenGL Shading Language with syntax similar to C that is executed directly by the graphics pipeline. They can be split into Vertex Shaders and Fragment Shaders (or Pixel Shaders) — the former transforms shape positions to real 3D drawing coordinates while the latter computes rendering colors and other attributes.
Shading Language is different from JavaScript - it's strongly typed, there's a lot of maths calculating vectors and matrices. Writing shaders can be fun though, because it's easy to create a basic one.
Shader types
Shader is a function required to draw something on the screen. Shaders run on your GPU which is optimized for such operations, so that you can offload CPU and focus the processor power on executing your own code.
Vertex Shader
Vertex shaders manipulate the coordinates in a 3D space and they are called once per vertex. The purpose of the vertex shader is to set up gl_Position
variable:
void main() { gl_Position = makeCalculationsToHaveCoordinates; }
The calculations result in having a 4D vector containing the information about how the position in the 3D space is projected onto a 2D screen.
Fragment Shader
Frament (or texture) shaders provide colors for the current pixel being processed - single fragment shader is called once per pixel. The purpose of the shader is to set up gl_FragColor
variable:
void main() { gl_FragColor = makeCalculationsToHaveColor; }
The calculations are resulting in having a 4D vector containing the information about the RGBA color.
Demo
Let's build a simple demo to explain those shaders in action. Be sure to read Three.js tutorial first to grasp the concept of the scene, its objects and materials.
Note: Remember that you don't have to use Three.js or any other library to write your shaders - pure WebGL is more than enough. This little demo though is focused on showing how to create the shader itself and use it in a working example but using Three.js in the background will make it a lot simpler and clearer to understand, so you can focus on the shaders code. Three.js or other libraries abstract a lot of things for you - if you'd like to create such example you'd have to write a lot of extra code to actually make it work.
Environment setup
To start with the WebGL shaders you don't need much. You should:
- Make sure you are using a modern browser with good WebGL support, such as the latest Firefox or Chrome.
- Create a directory to store your experiments in.
- Save a copy of the latest minimized Three.js library inside your directory.
HTML structure
Here's the HTML structure we will use.
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>MDN Games: Shaders demo</title> <style> body { margin: 0; padding: 0; font-size: 0; } canvas { width: 100%; height: 100%; } </style> <script src="three.min.js"></script> </head> <body> <script id="vertexShader" type="x-shader/x-vertex"> // vertex shader's code goes here </script> <script id="fragmentShader" type="x-shader/x-fragment"> // fragment shader's code goes here </script> <script> // scene setup goes here </script> </body> </html>
It contains some basic information like the document {{htmlelement("title")}}, and some CSS to set the width
and height
of the {{htmlelement("canvas")}} element that Three.js will insert on the page fully, so that it will fill the entire available viewport space. The {{htmlelement("script")}} element in the {{htmlelement("head")}} includes the Three.js library in the page and we will write our code into three script tags in the {{htmlelement("body")}} tag - the first one is for the vertex shader, second one for the fragment shader and the third one for the actual JavaScript code generating the scene.
Before reading on, copy this code to a new text file and save it in your working directory as index.html
.
Cube's source code
Instead of creating everything from scratch we can reuse the Building up a basic demo with Three.js source code of the cube. Most of the components like renderer, camera and lights will stay the same, but instead of the basic material we will set the cube's color and position using shaders.
Check out the cube.html file on GitHub from the Three.js demo and copy all the JavaScript code from between the second {{htmlelement("script")}} tag. See if it works - you should see a blue cube.
Vertex shader's code
We will start with the vertex shader - add the code below to the body's first script tag:
void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position.x+10.0, position.y, position.z+5.0, 1.0); }
The resulting gl_Position
is calculated by multiplying the model-vew and the projection matrices with each vector to get the final vertex position. Both projectionMatrix
and modelViewMatrix
are provided by Three.js and the 4D vector is passed as vec4
with the given 3D position and the 1.0
value, which totals in moving the original cube ten units on the x
axis and 5 units on the z
axis, translated through a shader. The fourth parameter is used for clipping and plays its part while linear algebra transformations are applied to the position - by setting up the value to 1.0
it doesn't affect our transformation.
Texture shader's code
Then there goes the texture shader - add the code below to the body's second script tag:
void main() { gl_FragColor = vec4(0.0, 0.58, 0.86, 1.0); }
This will set the RGBA color to the light blue one - the first three float values (ranging from 0.0
to 1.0
) are representing the RGB color and the last, fourth one is the Alpha transparency.
Applying the shaders
To actually apply the newly created shaders, comment out the basicMaterial
definition first:
// var basicMaterial = new THREE.MeshBasicMaterial({color: 0x0095DD});
Then, create the shaderMaterial
:
var shaderMaterial = new THREE.ShaderMaterial( { vertexShader: document.getElementById( 'vertexShader' ).textContent, fragmentShader: document.getElementById( 'fragmentShader' ).textContent });
This shader material takes the code from the scripts and apply it to the object the material is assigned to.
Then, instead of creating a cube with the basicMaterial
we also comment out
// var cube = new THREE.Mesh(boxGeometry, basicMaterial);
...we can do it with the newly created shaderMaterial
:
var cube = new THREE.Mesh(boxGeometry, shaderMaterial);
Three.js is compiling and running the shaders attached to the mesh to which this material is given. In our case the cube will have both vertex and texture shaders applied. That's it - you've just created the simplest possible shader, congratulations! Here's the cube:
It looks exactly the same as the Three.js cube demo but the slightly different position and the same blue color are both achieved using the shader. You can see this in action - here's the final piece of the code:
{{JSFiddleEmbed("https://jsfiddle.net/end3r/LL55bhrz/","","350")}}
You can also see it on GitHub.
Conclusion
I hope you now know what a shader is and how it works. This one is not doing much but there are many more cool things you can do with shaders - check out some really cool ones on ShaderToy for inspiration and to learn from their sources.