Writing Shaders
In the following section, we are going to take an in-depth look at how to write (and modify) shaders in cables. To properly understand this guide, it is recommended that you already have some basic shader knowledge and are familiar with the basic GLSL syntax. Also, you can create a patch with a CustomShader to follow along with this tutorial, as we shall explain the ins and outs of authoring shaders in cables with this op. The CustomShader op allows you to quickly write your own shaders.
Prerequisites
To get started, let's have a look at the CustomShader op's vertex and fragment shader default code:
// VERTEX SHADER
/* module injection point */
{{MODULES_HEAD}}
/* layout qualifiers */
IN vec3 vPosition;
IN vec2 attrTexCoord;
IN vec3 attrVertNormal;
IN vec3 attrTangent,attrBiTangent;
IN float attrVertIndex;
OUT vec2 texCoord;
OUT vec3 norm;
UNI mat4 projMatrix;
UNI mat4 viewMatrix;
UNI mat4 modelMatrix;
void main()
{
/* predefined variables */
texCoord=attrTexCoord;
norm=attrVertNormal;
vec4 pos=vec4(vPosition, 1.0);
vec3 tangent = attrTangent;
vec3 bitangent = attrBiTangent;
mat4 mMatrix=modelMatrix;
/* module injection point */
{{MODULE_VERTEX_POSITION}}
/* output */
gl_Position = projMatrix * (viewMatrix*mMatrix) * pos;
}
/* FRAGMENT SHADER */
/* layout qualifiers */
IN vec2 texCoord;
/* module injection point */
{{MODULES_HEAD}}
void main()
{
/* predefined variables */
vec4 col=vec4(0.5,0.5,0.5,1.0);
/* module injection point */
{{MODULE_COLOR}}
/* predefined variables */
outColor = col;
}
As you can see, there is quite a bit of default code present. The important parts of the source code have been prefixed with a comment describing what they are. They shall be explained in more detail in the following chapters:
Layout Qualifiers
/* layout qualifiers */
IN vec3 vPosition;
// ...
OUT vec2 texCoord;
// ...
UNI mat4 projMatrix;
For better compatibility between WebGL 1.x and 2.x, or GLSL v1 and v3, cables uses predefined layout qualifiers.
What does that mean in layman's terms?
GLSL 3.0 for example uses the qualifier out
to declare varying data (data that gets passed from vertex to fragment shader), whereas GLSL 1.0 uses the varying
qualifier to denote the exact same thing.
To counter these differences in syntax when shaders are compiled, cables uses its own layout qualifiers for these types of attributes: IN
, OUT
and UNI
.
The correct qualifiers get injected at compile-time depending on which WebGL version is used in the patch. Thus it is possible to write one shader for both WebGL versions. You can of course still use the original qualifiers, but your shader might not work in both WebGL versions. Please see the following tables for all availible layout qualifiers in cables:
Vertex Shader Layout Qualifiers
cables | GLSL 3.0 | GLSL 1.0 |
---|---|---|
OUT |
out |
varying |
IN |
in |
attribute |
UNI |
uniform |
uniform |
Fragment Shader Layout Qualifiers
cables | GLSL 3.0 | GLSL 1.0 |
---|---|---|
IN |
in |
varying |
UNI |
uniform |
uniform |
Declaring Uniforms in CustomShader
If you open the CustomShader's fragment shader code, you will see the following code:
You can see that there are no declared uniforms at the moment.
If we add uniforms to the CustomShader, the op looks like this:
As soon as you declare uniforms (such as a vec3
or a sampler2D
) in your shader code, the input ports are automatically added to the CustomShader op.
Predefined shader variables
IN vec3 vPosition;
IN vec2 attrTexCoord;
IN vec3 attrVertNormal;
IN vec3 attrTangent,attrBiTangent;
IN float attrVertIndex;
OUT vec2 texCoord;
OUT vec3 norm;
UNI mat4 projMatrix;
UNI mat4 viewMatrix;
UNI mat4 modelMatrix;
UNI mat4 inverseViewMatrix;
void main()
{
texCoord=attrTexCoord;
norm=attrVertNormal;
vec4 pos=vec4(vPosition, 1.0);
mat4 mMatrix=modelMatrix;
// ....
}
To keep consistency along the many shader ops cables includes, there are variables that are named the same throughout the shaders. These include vertex attributes and the core matrices used to render a 3d scene, but also predefined names for variables passed from vertex to fragment shader.
Default vertex attributes
The following attributes are passed to the vertex shader from every renderable mesh:
Name | Data Type | Function |
---|---|---|
vPosition |
vec3 |
vertex position in object space |
attrTexCoord |
vec2 |
texture coordinates per vertex |
attrVertNormal |
vec3 |
normal per vertex |
attrVertTangent |
vec3 |
tangent per vertex |
attrVertBiTangent |
vec3 |
bitangent per vertex |
attrVertIndex |
float |
index of the vertex |
Core matrices
The following matrices are passed as uniforms to every shader:
Name | Data Type | Function |
---|---|---|
modelMatrix |
mat4 |
the model matrix |
viewMatrix |
mat4 |
the view matrix |
projMatrix |
mat4 |
the projection matrix |
mvMatrix |
mat4 |
premultiplied model-view matrix |
inverseViewMatrix |
mat4 |
the inverted view matrix |
Default variables
For shaders to be able to consume cables' shader modules correctly, certain variables need to be declared in the main()
function before the module injection points. If you diverge from the naming, some modules could not work correctly. Also, if you want your shader to work with all modules, you need to declare all of the predefined variables, even if you are not actively using them in your code.
Vertex Shader
Name | Data Type | Function |
---|---|---|
mMatrix |
mat4 |
the model matrix |
pos |
vec4 |
the vertex' current position. NOTE: this is a vec4 and not a vec3 like the variable vPos . Make sure you do not forget to cast. |
norm |
vec3 |
the vertex normal. This is usually declared as an OUT variable. Still it needs to be declared and assigned before module injection. |
tangent |
vec3 |
the vertex' tangent |
bitangent |
vec3 |
the vertex' bitangent |
Fragment Shader
Name | Data Type | Function |
---|---|---|
col |
vec4 |
the color of the current fragment before module injection. |
These variables are used for things like instancing and displacement or color manipulation.
Default varying variables
The following vectors are used as varying variables.
Name | Data Type | Function |
---|---|---|
texCoord |
vec2 |
the current vertex texture coordinate |
norm |
vec3 |
the current normal |
Default output variables
Vertex Shader
Name | Data Type | Function |
---|---|---|
gl_Position |
vec4 |
GLSL's default vertex shader output variable |
Fragment Shader
Name | Data Type | Function |
---|---|---|
outColor |
vec4 |
drop in replacement for gl_FragColor . |
Preprocessor Directives
Cables allows the following preprocessor directives to be used in your shaders:
Directive Name | Function |
---|---|
#define |
Defines static variables |
#ifdef {name} |
Defines a conditional branch of shader code |
#ifndef {name} |
Defines a conditional branch of shader code (negated) |
#endif |
End of a conditional branch |
Example for preprocessor directives
Let's take the default CustomShader's fragment shader code and add preprocessor directives to it:
#define PI 3.14159265
#define TAU (2.0*PI)
IN vec2 texCoord;
{{MODULES_HEAD}}
void main()
{
vec4 col=vec4(0.5,0.5,0.5,1.0);
#ifdef COLOR_BLUE
col = vec4(0.,0.,1.,1.);
#endif
{{MODULE_COLOR}}
#ifndef COLOR_BLUE
col = col / PI;
col.a = 1.;
#endif
outColor = col;
}
So what does this contrived example do?
First off, we define the constants PI
and TAU
.
We can now use them in our shader code, they get replaced by their defined values as soon as the shader is compiled.
Then, we add conditional branching in our shader. If the define COLOR_BLUE
is defined, we change the variable col
to blue. If the conditional directive is not defined, we divide the current color col
by PI
.
How to control defines from the outside
When using the CustomShader op, defines do not get added automatically. You have to take your shader's output and extend it with a ShaderDefine op. You have to use one ShaderDefine op per define.
Have a look at this example to see defines in action.
Default defines
Cables features a few default defines that get added to every shader on compilation:
Both Shaders
Define Directive | Function |
---|---|
#define WEBGL2 |
Active when WebGL2 is used |
#define texture2D texture |
This ensures cross-compatibility between WebGL1 and WebGL2 shaders. WebGL1 uses texture(texture, coordinate) to lookup a pixel in a texture, whereas in WebGL2 you need to use texture2D(texture, coordinate) to access a texture. To overcome this language difference, every shader in cables defines a synonym for the texture2D function called texture |
#define UNI uniform |
This ensures cross-compatibility for uniforms between WebGL1 and iWebGL2 |
#define IN in |
This ensures cross-compatibility for vertex attributes and varying variables between WebGL1 and WebGL2. |
Vertex Shader
Define Directive | Function |
---|---|
#define OUT out |
This ensures cross-compatibility for varying variables between WebGL1 and WebGL2. |
Fragment Shader
Define Directive | Function |
---|---|
#define gl_FragColor outColor |
This ensures cross-compatibility for the color output of fragment shaders between WebGL1 and WebGL2. |
Module injection points
// VERTEX SHADER
/* module injection point */
{{MODULES_HEAD}}
IN vec3 vPosition;
// ....
UNI mat4 modelMatrix;
void main()
{
texCoord=attrTexCoord;
norm=attrVertNormal;
// ...
/* module injection point */
{{MODULE_VERTEX_POSITION}}
gl_Position = projMatrix * (viewMatrix*mMatrix) * pos;
}
// FRAGMENT SHADER
IN vec2 texCoord;
/* module injection point */
{{MODULES_HEAD}}
void main()
{
vec4 col=vec4(0.5,0.5,0.5,1.0);
/* module injection point */
{{MODULE_COLOR}}
outColor = col;
}
Basics
So, what exactly are modules?
Modules are shader extensions that allow you to add additional function to an existing shader (such as a Material). They can be found in the ShaderEffects op section. In easy terms:
Modules or ShaderEffects are shader code snippets that get injected at module injection points and extend the functionality of a shader.
!! IMPORTANT !!
Shader modules rely on default variables and default varying variables. If your shader is missing them, shader effects might not work correctly with your CustomShader. Make sure all default and default varying variables are declared in both shaders, even if you don't use them.
Injection points
The following injection points are availible:
Vertex Shader
Name | Function |
---|---|
{{MODULES_HEAD}} |
Here, the shader effect vertex shader's layout qualifiers (UNI , IN and OUT ) get injected. |
{{MODULES_VERTEX_POSITION}} |
Here, the actual vertex shader code of the shader effect gets injected. |
Fragment Shader
Name | Function |
---|---|
{{MODULES_HEAD}} |
Here, the shader effect fragment shader's layout qualifiers (UNI and IN ) get injected. |
{{MODULE_COLOR}} |
Here, the actual fragment shader code of the shader effect gets injected. |
Example
Let's take the default CustomShader and extend it with a FresnelGlow. For simplicity, we will just inspect the vertex shaders' code with a ShaderInfo.
First off, here is the default CustomShader's vertex shader code again:
// CustomShader no module
{{MODULES_HEAD}}
IN vec3 vPosition;
IN vec2 attrTexCoord;
IN vec3 attrVertNormal;
IN vec3 attrTangent,attrBiTangent;
IN float attrVertIndex;
OUT vec2 texCoord;
OUT vec3 norm;
UNI mat4 projMatrix;
UNI mat4 viewMatrix;
UNI mat4 modelMatrix;
void main()
{
texCoord=attrTexCoord;
norm=attrVertNormal;
vec4 pos=vec4(vPosition, 1.0);
vec3 tangent=attrTangent;
vec3 bitangent=attrBiTangent;
mat4 mMatrix=modelMatrix;
{{MODULE_VERTEX_POSITION}}
gl_Position = projMatrix * (viewMatrix*mMatrix) * pos;
}
The FresnelGlow's shader code is split into two chunks: fresnel_head.vert
and fresnel_body.vert
. Let's have a look:
// FresnelGlow head shader
#ifdef ENABLE_FRESNEL_MOD
OUT vec4 MOD_cameraSpace_pos;
OUT mat3 MOD_viewMatrix;
OUT vec3 MOD_norm;
#endif
// FresnelGlow body shader
#ifdef ENABLE_FRESNEL_MOD
MOD_cameraSpace_pos = viewMatrix*mMatrix * pos;
MOD_norm = norm;
MOD_viewMatrix = mat3(viewMatrix);
#endif
As you can see, in the head file we declare all relevant qualifiers. In the body some calculations are taking place. What you might notice is the MOD_
-prefix of the variables. This ensures that the shader variables will always be unique, no matter how many mods you attach to a shader.
Now what happens when we extend our CustomShader?
// final extended shader
#version 300 es
//
// vertex shader shaderMaterial copy
//
precision highp float;
#define WEBGL2
#define texture2D texture
#define UNI uniform
#define IN in
#define OUT out
// active mods: ---------------
// 0.0. fresnelGlow (MODULE_VERTEX_POSITION)
// cgl generated
#define ENABLE_FRESNEL_MOD
// cgl generated
UNI vec4 mod12_inFresnel; // mod: fresnelGlow
UNI float mod12_inFresnelExponent; // mod: fresnelGlow
// --
//---- MOD: group:12: idx:1 - prfx:mod12_ - fresnelGlow ------
#ifdef ENABLE_FRESNEL_MOD
OUT vec4 mod12_cameraSpace_pos;
OUT mat3 mod12_viewMatrix;
OUT vec3 mod12_norm;
#endif
//---- end mod ------
//---- MOD: group:12: idx:0 - prfx:mod12_ - fresnelGlow ------
//---- end mod ------
IN vec3 vPosition;
IN vec2 attrTexCoord;
IN vec3 attrVertNormal;
IN vec3 attrTangent,attrBiTangent;
IN float attrVertIndex;
OUT vec2 texCoord;
OUT vec3 norm;
UNI mat4 projMatrix;
UNI mat4 viewMatrix;
UNI mat4 modelMatrix;
void main()
{
texCoord=attrTexCoord;
norm=attrVertNormal;
vec4 pos=vec4(vPosition, 1.0);
vec3 tangent=attrTangent;
vec3 bitangent=attrBiTangent;
mat4 mMatrix=modelMatrix;
//---- MOD: fresnelGlow ------
#ifdef ENABLE_FRESNEL_MOD
mod12_cameraSpace_pos = viewMatrix*mMatrix * pos;
mod12_norm = norm;
mod12_viewMatrix = mat3(viewMatrix);
#endif
//---- end mod ------
gl_Position = projMatrix * (viewMatrix*mMatrix) * pos;
}
So, there are a few things to notice here. First off: The module injection points {{MODULES_HEAD}}
and {{MODULE_VERTEX_POSITION}}
are no longer there. They have been replaced with the following segments:
// MODULES_HEAD
// active mods: ---------------
// 0.0. fresnelGlow (MODULE_VERTEX_POSITION)
// cgl generated
#define ENABLE_FRESNEL_MOD
// cgl generated
UNI vec4 mod12_inFresnel; // mod: fresnelGlow
UNI float mod12_inFresnelExponent; // mod: fresnelGlow
// --
//---- MOD: group:12: idx:1 - prfx:mod12_ - fresnelGlow ------
#ifdef ENABLE_FRESNEL_MOD
OUT vec4 mod12_cameraSpace_pos;
OUT mat3 mod12_viewMatrix;
OUT vec3 mod12_norm;
#endif
//---- end mod ------
//---- MOD: group:12: idx:0 - prfx:mod12_ - fresnelGlow ------
// MODULE_VERTEX_POSITION
//---- MOD: fresnelGlow ------
#ifdef ENABLE_FRESNEL_MOD
mod12_cameraSpace_pos = viewMatrix*mMatrix * pos;
mod12_norm = norm;
mod12_viewMatrix = mat3(viewMatrix);
#endif
//---- end mod ------
The MOD_
-prefix has been replaced with a mod12
-prefix. Every shader extension gets their own unique identifier to not clash with other shader extensions. If you use multiple shader effects, every shader extension will get their own modXX
-prefix.
Have a look at this example. Disconnect the FresnelGlow op and look at the vertex shader's code. After that, re-add the module and inspect the code again.
If you want to port shadertoy shaders to cables, have a look at this section of our documentation.
If you still need help, feel free to contact us on Discord. We are happy to help!
Happy shader coding!
Found a problem? Edit this file on github and contribute to cables!