Skip to main content

Shader

What is a Shader?

Shaders are a set of instructions that are executed all at once for every single pixel on the screen. That means the code you write has to behave differently depending on the position of the pixel on the screen. Like a type press, your program will work as a function that receives a position and returns a color, and when it’s compiled it will run extraordinarily fast.

A shader is written in GLSL that is sent to the GPU to position each vertex of a geometry and to colorize each visible pixel of that geometry.

Vertex Shader

In short, the main purpose of a vertex shader is to convert a vertice's 3D coordinate into a 2D coordinate on a canvas to be renderer.

To perform such conversion, a main function is defined in the shader and the goal of the function is to properly instruct how coodrinate should be transformed. The result is fed back to the conversion pipeline by assigning the result to a predefined default output variable gl_Position.

Code of the vertex shader is applied on every vertex of the geometry, and data can be passed to the shader code. Data that varies between vertices is called an attribute. On the other hand, data that doesn't change between vertices is called an uniform.

Below is a basic main function for the vertex shader.

uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;

attribute vec3 position;

void main()
{
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
vec4 viewPosition = viewMatrix * modelPosition;
vec4 projectedPosition = projectionMatrix * viewPosition;

gl_Position = projectedPosition;
}

Fragment Shader

The purpose of a fragment shader purpose is to color each visible fragment of the geometry. It is executed after vertex shader and the same fragment shader code will be executed for every visible fragment of the geometry.

The goal of a fragment is to assign a color to a predifined variable gl_FragColor to determine the color of the fragment.

void main() {
gl_FragColor = vec4(1,0,0,1); // ERROR
}

To send data to a fragment shader, one can either send it by using uniforms, same as the vertex shader. Or, data can be sent from the vertex shader to the fragment shader by using varying.

The most straightforward instruction in a fragment shader can be to color all the fragments with the same color. This is equivalent to the MeshBasicMaterial with only the color property being set.

Attribute vs Uniform vs Varying
AttributeUniformVarying
SourceCPUCPUVertex Shader
Read/Write in Vertex ShaderRead onlyRead onlyRead/Write
Read/Write in Fragment ShaderN.ARead onlyRead Only
ChangesPer vertexConstantPer fragment

Variables Types

GLSL supports multipule types of variables to pass to the shader, including:

  • float
  • integer
  • boolean
  • vec2
  • vec3
  • vec4
// integer
int foo = 123;
int bar = -123;

// float, the decimal cannot be dropped even number is rounded
float foo = -1.2;
float bar = 1.0;
int foo1 = 1;
float bar1 = float(foo1); // Explicit conversion

// bolean
bool foo = true;
bool bar = false;

// vec2
vec2 foo = vec2(1.0, 2.0);
foo.x = -1.0; // modify x property
foo.y = -2.0; // modify y property
foo *= 2.0; // foo.x is now 2.0, foo.y is now -4.0

// vec3
vec3 foo = vec3(0.0);
vec3 bar = vec3(1.0, 2.0, 3.0);
vec2 foo1 = vec2(1.0, 2.0);
vec3 bar1 = vec3(foo1, 3.0); // Partial create with vec2
vec2 bar2 = foo1.xy; // Swizzle, works for properties in a different order as well, e.g foo1.yx

// vec4
vec4 foo = vec4(1.0, 2.0, 3.0, 4.0);
vec4 bar = vec4(foo.zw, vec2(5.0, 6.0));

Functions

GLSL provide native functions as well as syntax to define custom funcitons:

// Custom functions
float addFloats(float a, float b)
{
return a + b;
}

Shaders in Three.js

A simple RawShaderMaterial created with custom vertex/Fragment shader.

// Sample code from Three.js Journey Tutorial
// https://threejs-journey.com/lessons/shaders#create-our-first-shaders-with-rawshadermaterial
const material = new THREE.RawShaderMaterial({
vertexShader: `
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;

attribute vec3 position;

void main()
{
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
precision mediump float;

void main()
{
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`
})

The 3 matrices projectionMatrix, viewMatrix, modelMatrix and position are built-in variables provided by three.js, and is defined as uniform since their values are the same for all the vertices of the geometry.

Each matrix contributes to a part of the transformation:

  • modelMatrix: Apply all transformations relative to the Mesh. including scale, rotate or transfrom;
  • viewMatrix: Apply transformations relative to the camera, such as moving vertices to the right if camera is moving left. Or the camera dolly-in toward the mesh, then vertices should get larger, etc. Essentially, it is the inverse transformation of the camera.
  • projectionMatrix: Transform our coordinates into the final clip space coordinates.

Breaking down the transformation into mutliple steps provides us with greated control over how meshes are rendered; otherwise, there is a shorter version of them matrix available where modelViewMatrix is the viewMatrix multiplied by the modelMatrix.

uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;

attribute vec3 position;

void main()
{
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

References

Resources