Shading Language

cerlib uses its own platform-agnostic shading language.

It includes a hand-written, small and efficient compiler that translates its shaders to native shading languages such as GLSL, HLSL and MSL.

The language is designed as a C-like language with simple constructs. Its goal is to provide an easy to understand shading language tailored to cerlib’s domain, namely sprite shading.

The advantage of having a custom shading language is the ability to closely match it to what the library is capable of. Conversely, the library can optimize how shader data is stored and sent to the GPU, because it understands the language’s behavior and restrictions.

Users of GLSL, HLSL or MSL should feel familiar with the language.

Basic Syntax

Similar to C++, a function is defined in the form of:

Vector3 multiplyAdd(Vector3 first, Vector3 second, Vector3 third) {
  return first * second + third;
}

Some rules for functions:

  • Every function must return a value; there is no void type
    • A function is allowed at most one return statement, which must be the last statement in its body
  • Function parameters are immutable
  • Function overloading is not allowed
  • Every code block must be surrounded by { and }, even if it contains a single statement

Comments

Comments start with //. Multiline comments in the form of /* ... */ are not supported.

Variables

There are two ways to define a variable: mutable and immutable. Mutable variables are defined using the var keyword, while immutable variables are defined using the const keyword:

var a = 1;
a = a * 2; // ok: value of a can be changed
a += 2;

const b = 2;
b = b * 2; // error: value of b can't be changed
b += 3;

In either case, every variable statement has to be initialized with an expression, from which its type is deduced. There is no explicit type declaration for variables.

Note
The prefix cer_ is a reserved prefix for built-in variables and may not be used as a prefix for identifiers.

Data Structures

A data structure is defined using the struct keyword:

// Definition:
struct LightingResult {
  Vector3 diffuse;
  Vector3 specular;
  float intensity;
}

// Usage:
const result = LightingResult {
  diffuse   = Vector3(1, 2, 3),  // initialize field 'diffuse'
  specular  = Vector3(4, 5, 6),  // initialize field 'specular'
  intensity = 7.0                // initialize field 'intensity'
};

When initializing a struct, all or none of its fields must be initialized. The following would therefore be not allowed:

const result = LightingResult {
  diffuse = Vector3(1, 2, 3)
}; // error: missing initializers for fields 'specular' and 'intensity'

Whereas this would be allowed:

const result1 = LightingResult{};
result1.diffuse = Vector3(1, 2, 3); // error: 'result1' is immutable

var result2 = LightingResult{};
result2.diffuse = Vector3(1, 2, 3); // ok: result2 is mutable

If Statements

Use if statements to conditionally execute a portion of code at runtime:

Vector3 someConditions(Vector3 v) {
  var result = v;
  const len = length(v);

  if (len > 4.0) {
    result *= 10.0;
  }
  else if (len > 2.0) {
    result *= 5.0;
  }
  else {
    result = Vector3(0);
  }

  return result;
}

Ternary conditional operators are also supported:

float max(float a, float b) {
  return a > b ? a : b;
}

Loops

Loops can be realized using a for statement. A for loop requires a name for the iterator variable and a range in the form of <start> .. <endExclusive>:

var sum = 0;

for i in 1 .. 4 { // i will be 1, 2, 3
  sum += i;
}

// sum: 1+2+3 = 6

Note
The iterator variable (in this case i) is immutable.

Shader Functions

Shader functions are the main entry points in a shader and are always called main:

Vector4 main() {
  return Vector4(0);
}

There are special restrictions for shader functions. For example, a shader function must always return a value of type Vector4, which is the output pixel color.

Using Shaders

The default way to use shaders is to load them using the single-string cer::Shader constructor, i.e.:

auto shader = cer::Shader("MyShader.shd"); // Loads a shader from the asset storage

In this case however we’ll look at how a shader can be constructed directly from a C++ string. It’s as simple as:

auto shader = cer::Shader(myShaderName, myShaderCode);

Where myShaderName and myShaderCode are string object. The shader’s name is used to report it in compilation error messages. If the constructor did not throw an exception, the shader was compiled successfully and is ready for use.

Parameters

A shader can declare parameters that are accessible to all functions within it, for example:

Vector3 someColor;
float intensity = 1.0; // Assigning a default value

Vector3 someFunction(Vector3 value) {
  return value + someColor;
}

Parameter declarations may optionally assign a default value. If no default value is specified for a parameter, it receives a zero-value. Meaning that a float will be 0.0, a Vector2 will be Vector2(0, 0), a Matrix will be all zeroes, etc.

Supported parameter types are:

  • bool
  • intuintfloat
  • Vector2Vector3Vector4
  • Matrix
  • Image

Note
The compiler will optimize any unused parameters away.

Parameter values

To set parameters on shader objects, call the Shader::setValue method:

myShader.setValue("baseColor", cer::Vector3{1, 0, 0});
myShader.setValue("intensity", 2.0f);

The method has overloads for each parameter type. When attempting to set a value that is incompatible with the parameter type, an exception is thrown.

Conversely, shader parameter values can be obtained using the Shader::*value() methods:

auto baseColor = myShader.vector3Value("baseColor"); // Option<Vector3>
auto intensity = myShader.floatValue("intensity"); // Option<Float>

The value of a parameter is always part of a shader object’s state. This means that shader parameters can be updated even when a shader is not actively used.

Array parameters

It is possible to declare array parameters for scalar types using an array specifier:

// Arrays must always have a known size at compile time.
Vector3[12] someArrayOf3DVectors;

// Expressions may be used as an array size, but are required to be known at compile time.
const someValue = 4;
const someConstant = 12 * someValue;

float[someConstant + 2] someArrayOfFloats;

Setting array parameter values are also modified using the setValue() method. Arrays are specified as Span values:

myShader.setValue("someFloats", { 0.5f, 1.0f, 1.25f, 5.0f });

Shading Language Reference

Syntax

The following table describes the syntax of the shading language.

ConstructFormExample
Function parameter<type> <name>int a
Function signature<type> <name> '(' <parameter> (',' <parameter)* ')'int add(int a, int b)
Function body'{' stmt* return_stmt '}'{ return a + b; }
Function<signature> <body>float pow(int x) { return x * x; }
Shader parameter<type> <name>float some_parameter
Array type<type>[<size>]Vector2[10]

Types

TypeDescriptionC++ equivalentCan be array
boolBoolean true / false valueint32_t
intSigned 32-bit integerint32_t
uintUnsigned 32-bit integeruint32_t
float32-bit floating point numberfloat
Vector22D floating point vectorcer::Vector2
Vector33D floating point vectorcer::Vector3
Vector44D floating point vectorcer::Vector4
Matrix4×4 row-major matrixcer::Matrix
Image2D texturecer::Image

Struct fields

NameType
xfloat
yfloat
xxVector2
yyVector2
NameType
xfloat
yfloat
zfloat
xxVector2
yyVector2
zzVector2
xyVector2
yzVector2
zyVector2
xzVector2
xxxVector3
yyyVector3
zzzVector3
NameType
xfloat
yfloat
zfloat
wfloat
xyVector2
xyzVector3
xxxxVector4
yyyyVector4
zzzzVector4
wwwwVector4

The matrix type currently has no members.

Built-in variables

The following lists all variables that are always available within a shader.

VariableDescriptionType
spriteImageThe image of the sprite that is drawnImage
spriteColorThe color of the sprite that is drawnVector4
spriteUVThe texture coordinate of the sprite that is drawnVector2

Functions

The following lists all available intrinsic functions.

Within this table the following names are defined as groups of types:

  • Vec: Vector2 | Vector3 | Vector4
  • Fto4: float | Vector2 | Vector3 | Vector4
  • FtoM: float | Vector2 | Vector3 | Vector4 | Matrix

Function table

NameParameters → Return Type
absFto4 → Fto4
acosFto4 → Fto4
allFtoM → FtoM
anyFtoM → FtoM
asinFto4 → Fto4
atanFto4 → Fto4
atan2Fto4 → Fto4
ceilFtoM → FtoM
clampFto4 → Fto4
cosFto4 → Fto4
degreesFto4 → Fto4
distanceFto4 → Vec
dotVec → Vec
expVec → Fto4
exp2Fto4 → Fto4
floorFto4 → Fto4
fmodFto4 → Fto4
fracFto4 → Fto4
lengthVec → Vec
lerpFto4 → Fto4
logFto4 → Fto4
log2Fto4 → Fto4
maxFto4 → Fto4
minFto4 → Fto4
normalizeVec → Vec
powFto4 → Fto4
radiansFto4 → Fto4
roundFto4 → Fto4
sample(Image, Vector2) → Vector4
saturateFto4 → Fto4
signFto4 → Fto4
sinFto4 → Fto4
smoothstepFto4 → Fto4
sqrtFto4 → Fto4
tanFto4 → Fto4
transposeMatrix → Matrix
truncFto4 → Fto4

Constructors

The following lists all available type constructors.

TypeParametersEffect
floatint xCast x to float
floatuint xCast x to float
intfloat xCast x to int
intuint xCast x to int
uintint xCast x to uint
uintfloat xCast x to uint
Vector2float x, float yx=x, y=y
Vector2float xyx=xy, y=xy
Vector3float x, float y, float zx=x, y=y, z=z
Vector3float xyzx=xyz, y=xyz, z=xyz
Vector4float x, float y, float z, float wx=x, y=y, z=z, w=w
Vector4Vector2 xy, Vector2 zwx=xy.x, y=xy.y, z=zw.x, w=zw.y
Vector4Vector2 xy, float z, float wx=xy.x, y=xy.y, z=z, w=w
Vector4Vector3 xyz, float wx=xyz.x, y=xyz.y, z=xyz.z, w=w
Vector4float xyzwx=xyzw, y=xyzw, z=xyzw, w=xyzw