Basic Shaders Syntax
Shaders syntax is quite similar to C, but with a bunch of global variables for graphics manipulation. It is NOT a general purpose programming language — it’s a syntax for manipulating the GPU rendering pipeline.
Anatomy of a shader
#ifdef GL_ES
precision mediump float;
#endif
#define TWO_PI 6.28318530718
uniform float u_time;
void main() {
gl_FragColor = vec4(1.0,0.0,1.0,1.0);
}
This is kind of like the “hello world” of shaders. It’s simply putting a color on the screen by setting an RGBA value to the gl_FragColor
variable.
A few things we can see from this:
- Like C, there is a special
main
function. - There are functions and types, like C.
- You cans store values in variables, just like C or JS.
- There are also preprocessor macros, like C. You can see that with the
#ifdef
. This one is has two: one checking if GL_ES is defined, and setting the flaot precision tomediump
(medium precision) if it is defined, and one setting TWO_PI.
Vectors?
Things that are important in shaders: vectors, floats, and matrices.
Vectors are defined as sets of floating point numbers. For example, in the above example gl_FragColor
is set to a a vec4
, which is a set of 4 floats defining RGBA values.
vec4 red = vec4(1.0,0.0,0.0,1.0); // the color red
vec2 resolution = vec2(100.0, 100.0); // screen width/height
vec3 aPosition = (101.2, -300.10, 1.0); // a 3d coordination
All of these vecx types are top-level-ly available for your enjoyment.
Similar to the vector types, there are also matrix types. Generally how I remember vector and matrix
// vector
[21,400,9]
// matrix
[
[22, 300, 10],
[400, -10, 19],
]
The types in GLSL are: mat3
, mat4
, etc.
Uniforms
It helps to have a mental model of shader programming as hyper-multi-threaded GPU programming. Each thread is stateless, memoryless, and purely isolated/blind from every other thread. Each “thread” is a pixel.
However! Sometimes we need to ferry some data over from the CPU to the shaders/GPU rendering pipeline. We also need that data to be read only and consistent across every “thread”. Or another way to say it: we need that data to be uniform.
That’s what uniform
is for: constant variables sent over from the CPU applied uniformly to each vertex.
We define those below the preproccessor macros:
#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 u_resolution; // Canvas size (width,height)
uniform vec2 u_mouse; // mouse position in screen pixels
uniform float u_time; // Time in seconds since load
So think of uniforms
as little bridges between CPU and GPU.
Built-Ins
GLSL also has a bunch of utilities (mostly mathematics) built into it and available globally:
- sin()
- cos()
- tan()
- asin()
- acos()
- atan()
- pow()
- exp()
- log()
- sqrt()
- abs()
- sign()
- floor()
- ceil()
- fract()
- mod()
- min()
- max()
- clamp()
- smoothstep - hermite interpolation
- step - step function comparing two values; anything below the first arg will return 0.0, above will return 1.0. Second argument is the value you want to eval.
- mix - transition between colors
gl_FragCoord
Similar to the vec4
that GLSL gives us for screen output (gl_FragColor
), it also exposes to us a vec4
for screen input called gl_FragCoord
. This gives us the screen coordinates of the pixel or screen fragment (i.e., pixel) that the active thread is working on.
It is kind of the opposite of a uniform
because it is different for every thread and every pixel that we’re working on. It’s a varying
. Also, varying
variables are used to pass data from the vertex shader to the fragment shader.
in
and inout
Shaders are indeed strictly typed, including function arguments. Function argument type notation can also specify an argument as readonly with in
, writeonly with out
, or as a mutable reference with inout
.
int newFunction(in vec4 aVec4, // read-only
out vec3 aVec3, // write-only
inout int aInt); // read-write
Cool!