SparkSL
SparkSL Overview

SparkSL Overview

The shader code asset allows you to program your own shaders using SparkSL, Spark AR's own shading language.

As a superset of GLSL 1.0, SparkSL provides a number of features in addition to the usual data types and functions, listed below.

The Spark AR Extension for Visual Studio Code supports syntax highlighting and code autocompletion for SparkSL scripts.

Automatic type inference

Automatic type inference for variables and functions is supported with the auto keyword.

 auto c = a * b;

Or:

 auto fn() {
 return vec4(1.0);
 }
                

Swizzling

In addition to the standard xyzw, rgba, and stpq syntax when swizzling, SparkSL allows for the use of 0 and 1 as values.

 vec2 uv;
 vec4 col1 = uv.xy01; // Equivalent to vec4(uv, 0.0, 1.0)

 float v;
 vec4 col2 = v.xxx1; // Equivalent to vec4(v, v, v, 1.0) 

 vec4 c;
 vec4 col3 = c.rgb1; // Equivalent to vec4(c.rgb, 1.0)
                

Lambda functions

SparkSL supports the creation of lambda functions and the passing of functions as arguments to these.

Specifying captures explicitly is not required; in the following example, both tex and offset are implicitly captured.

 // A function that both receives and returns a function
 function<vec4(vec2)> blur1D(function<vec4(vec2)> tex, vec2 offset) {
     // Creating and returning a lambda function
     return [](vec2 uv) { 
         return 0.25 * tex(uv - offset) +
         0.50 * tex(uv) +
         0.25 * tex(uv + offset);
     };
 }
                

The use of lambdas can simplify the transformation and composition of textures and provide greater flexibility. Consider two textures, texA and texB, each represented as a function<vec4(vec2)>. With lambdas, these can be multiplied together as shown in the example below.

 vec4 myMaterial(function<vec4(vec2)> texA, function<vec4(vec2)> texB) {
     // Further information on the getVertexTexCoord() function available in the API reference
     vec2 uv = std::getVertexTexCoord();
     return texA(uv) * texB(uv);
 }
                

Textures can also be manipulated within a lambda function. Consider the following example, in which texA is blurred before being multiplied by texB.

 vec4 myMaterial(function<vec4(vec2)> texA, function<vec4(vec2)> texB) {
     // Further information on the getVertexTexCoord() function available in the API reference
     vec2 uv = std::getVertexTexCoord();
     auto blurredTexA = blur1D(texA, vec2(0.1));
     return blurredTexA(uv) * texB(uv);
 }
                

Notice how blur1d and myMaterial are not required to have knowledge of the internal implementations of texA or texB; in fact, with the use of lambdas the above will work with any texture implementation so long as vec2 coordinates are supplied as arguments and the return type is a vec4 color.

Namespace declaration

The use of namespaces to organise functions and global variables is supported, as are nested namespaces and the using namespace syntax.

 namespace fb {
     vec4 foo(vec2 uv) {
     return uv.xy01;
     }
 }
                        
 void main() {
     // Further information on the getVertexTexCoord() function available in the API reference
     vec2 uv = std::getVertexTexCoord();
     gl_FragColor = fb::foo(uv);
 }
                

Exporting functions

Functions can be shared between shaders, making it easy to reuse code.

To make a function available to other shaders, use the export keyword.

 // FirstShader.sca
                  
 export vec3 red() {
     return 0xFF0000;
 }
                  
 export vec3 green() {
     return 0x00FF00;
 }
                  
 export vec3 blue() {
     return 0x0000FF;
 }
                

Functions exported with this method can be accessed from other shaders by including the origin shader file with an #import directive.

 // SecondShader.sca

 #import <FirstShader.sca>

 vec4 main() {
     return vec4(red() + green() + blue(), 1.0);
 }
              

These exported functions can also be used to create shader code patches.

Combined-stage shaders

Rather than writing separate vertex and fragment shaders, SparkSL allows you to write a single combined-stage shader which is then automatically split into vertex and fragment shaders as appropriate.

By default, the framework will attempt to compute as much of the shader as possible within the vertex shader before passing over to the fragment shader. If a calculation depends on a fragment value, the result of that calculation and all subsequent calculations that reference that result will only be available in the fragment stage. Additionally, a value from the vertex shader can be forced to the fragment stage by using the fragment function. This allows for precise control over the data that is available at each stage, while still maintaining a single shader.

Consider the following combined-stage shader.

 vec4 main(vec2 uv) {
     float r = length(fragment(uv - 0.5));
     float g = (uv.x + uv.y) * 0.5;
     return vec4(r, g, 0.0, 1.0);
 }
                

The above example results in the following optimized vertex and fragment shaders. Note that varying variables are generated automatically.

 // [Vertex shader]
     varying vec2 v_var1;
     varying float v_var2;

 void main() {
     // ...
     v_var1 = uv - 0.5;
     v_var2 = (uv.x + uv.y) * 0.5;
 }

 // [Fragment shader]
 varying vec2 v_var1;
 varying float v_var2;

 void main() {
     float r = length(v_var1);
     float g = v_var2;
     gl_FragColor = vec4(r, g, 0.0, 1.0);
 }
                

Note that as vertex values are linearly interpolated to the fragment stage, linear operations can be performed in either the vertex or fragment shader without affecting the result.

However, this does not apply to non-linear operations as the result of a calculation will vary depending on the stage it is performed in. You can force an operation to be performed in the fragment stage by making one of its arguments a fragment value with the fragment function covered previously.

There are a number of trade-offs to consider when deciding whether to perform an operation in the fragment or vertex stage.

  • An operation performed in the fragment shader is typically less performant than an equivalent calculation in the vertex shader.
  • Fragment shader operations are more prone to precision issues, as they don't provide the same guarantees provided by the vertex shader in regards to precision.
  • Performing a calculation in the vertex shader and using the result in the fragment shader will result in an implicit use of a varying. As varyings are a limited resource, the use of a large number of them can cause the shader to be invalid.

Optional types

Any type in SparkSL can be declared as optional. A variable of an optional type does not require its value to be present within the shader.

Instead, the value of the optional variable must be accessed via the valueOr() function which takes a single argument, a fallback value to use if the variable is not found.

This allows you to write a single function which can handle cases where a value is explicitly provided and cases where it's missing.

A common use case for optional types are materials with optional normal, diffuse or specular textures.

In the example below, the function samples an optional normal map and falls back to the fragment normal if the texture is missing.

 // Samples a normal or returns the fragment normal
 vec4 getNormal(optional<std::Texture2d> normalMap, vec2 uv) {
     return normalMap(uv).valueOr(std::get FragmentNormal());
 }    
                

The valurOr() function can take an optional as its argument, which in turn causes the result of the function call itself to be optional.

 vec4 main(optional<vec4> v0, optional<vec4> v1) {
     return v0.valueOr(v1).valueOr(vec4(1.0));
 }           
                

Overloads for binary and unary operators are also provided.

float main(optional<float> f0, optional<float> f1) {
    optional<float> res = f0 + f1;
    return res.valueOr(0.0);
}                      
                

Note that binary operators are also provided for combinations of optional and non-optional operands. In these cases the result is also optional.

Function annotations

SparkSL allows you to set the entry point of a Shader Code Patch with the @main annotation.

 // @main
 void main() { 
     // ...
 }
                

The @param annotation allows you to specify the default, min, and max values for each of the function’s parameters, as well as provide a name and description for each parameter’s inspector entry.

In the example below, the range boundaries of f are set via the min and max values, while the default value specifies the value of f if the port on the patch is left unconnected.

 // @param[default=0.5, min=0.0, max=1.0] f
 void main(float f) {
     // ...
 }
                

vec4 variables can use hex color values in their parameter annotation, for example: @param[default=#AABB00FF] color.

Additionally, an optional @return annotation can be used to provide a description for the function’s return value.

 // This function description will appear in the inspector
 // @param[default=0.5, min=0.0, max=1.0] p The description of p's port
 // @param[default=vec2(0.0)] v The description of v's port
 // @return The description of the return value
 vec2 main(float p, vec2 v) { 
     return v * p;
 }
                

Cross-device compatibility

The effects you create are likely to be used on devices with a range of different specifications. This article will help you to ensure the shaders in your effects work well across as many different devices as possible.

Was this article helpful?