Shader Code Asset

The shader code asset lets you write custom shaders in Spark AR Studio.

If you’ve created a complete shader that returns a vec4 color or has an out vec4 Color argument, it can be used as a material called a shader code asset material.

For a more modular approach, you can instantiate the shader code asset as a patch in the Patch Editor. We'll cover both methods in this article.

Creating the shader code asset

To create a shader code asset:

  1. In Spark AR Studio, go to the Assets panel.
  2. Click Add Asset, and select Shader Code Asset from the menu.

The shader code asset will be listed in the Assets panel. An .sca file will be added to your project. To edit the shader code asset:

  1. Select the shader code asset in the Assets panel.
  2. In the Inspector, click Edit.

Changes you make to the file will be reflected in Spark AR Studio after saving. Any compilation errors or warnings will appear in the console when saving.

Defining functions

The shader must define a main function. The inputs and outputs of this function will define the interface of the shader code asset when it’s used in Spark AR Studio either as a material or patch. For example, a shader code asset with the signature below will have a single input, Alpha, and a single output, Color, when instantiated as a patch:

void main(float Alpha, out vec4 Color);

When used as a material, the material will have a single input value, Alpha. The Color parameter will be the color of the material.

If no function with the name 'main' is found, the last function in the file will be considered the main function.

Learn more about the shading language.

The shader code asset patch

The shader code asset patch is interoperable with existing visual shader patches and other shader code patches.

To use the shader code asset in the Patch Editor:

  1. Create a shader code asset.
  2. Open the Patch Editor.
  3. Drag the shader code asset from the Asset panel into the Patch Editor.

Inputs and outputs

The inputs and outputs of the patch are determined by the main function in the shader. Input parameters will appear as input ports and output parameters will appear as output ports. To mark a parameter as an output, prefix it with the out qualifier. If the shader has a return type other than void this will appear as an output port, too.

Like with the shader code asset material, default values for input ports can be specified using annotations.

The shader code asset material

Any shader code asset that outputs a vec4 Color value, either through an out parameter or as a return value, can be used directly as a material.

To use a shader code asset directly as a material:

  1. Create a new material.
  2. In the Inspector, select the shader asset in the Shader Type drop down.

To perform vertex displacement with your material, you can write directly to the vertex position by creating an additional vec4 out parameter and naming it Position.

Basic example

In this example we’ve used the shader code asset to create a colorful heart shape with spiky edges:

The example is created using the following shader:


#import <gradients>
#import <sdf>

vec2 heartify(vec2 uv, vec2 pivot, float w, float scale, float offset) {
    float dx = abs(uv.x - pivot.x);
    return vec2(uv.x, dx * (w - dx) + (uv.y - pivot.y) * scale - offset + pivot.y);
}

// @param[default=#FF0000FF] color1
// @param[default=#0000C0FF] color2
// @param[default=0.5,min=0.0,max=1.0] spikiness
// @return color
vec4 main(vec4 color1, vec4 color2, float spikiness) {
    vec2 uv = fragment(std::getVertexTexCoord());
    uv = heartify(uv, vec2(0.5, 0.5), 1.15, 1.3, 0.1);
    vec4 color = mix(color1, color2, std::gradientHorizontal(uv));
    float innerRadius = mix(0.50, 0.25, spikiness);
    auto sdf = std::sdfStarSharp(vec2(0.5, 0.5), innerRadius, 0.50, 25.0);
    float dist = sdf(uv);
    float edge = fwidth(dist);
    float alpha = smoothstep(-edge, +edge, dist);
                   
    return mix(color, color.rgb0, alpha);
}


              

The main function in this example returns a vec4 and takes three parameters, two vec4s that specify the two colors of the shape, and a spikiness float that specifies the size of the spikes.

As a result, when the shader is applied directly as a material you’ll see these values in the Inspector: :

Under Parameters, the default values of the three input variables correspond to the values specified in the main function annotation. You can change these in the Inspector or by calling the setParameter method on the material within a script.

The std::sdfStarSharp function provided by the SDF module creates an SDF of a star shape.

To use this function this module must be imported using the import statement at the top of the shader.

Advanced example

In this example we’ll implement a phong material that responds to light in Spark AR Studio, while covering shading language features unique to Spark. The resulting material supports optional diffuse, specular and emissive textures, and can respond to directional, spot, point and ambient lights.

                   
#import <lights>
                   
 
struct PhongMaterialParameters {
    vec3 emission;
    vec3 ambientFactor;
    vec3 diffuseFactor;
    vec3 specularFactor;
    float shininess;
    float occlusion;
};

vec3 applyPhong(
    std::LightData light,
    vec3 normal,
    vec3 view,
    PhongMaterialParameters material) {
    vec3 reflected = -reflect(light.toLightDirection, normal);

    float LdotN = dot(light.toLightDirection, normal);
    float RdotV = max(dot(reflected, view), 0.0);

    float diffuseFactor = max(LdotN, 0.0);
    vec3 diffuse = material.diffuseFactor * (light.intensity * diffuseFactor);

    float specularFactor = pow(RdotV, material.shininess) * step(0.0, LdotN); // do not light backface
    vec3 specular = material.specularFactor * (light.intensity * specularFactor);

    return material.occlusion * diffuse + specular;
}

// A material that uses the Phong shading model.
//
// @param [default=0.0, min=0.0, max=100.0] smoothness
void main(optional<std::Texture2d> diffuseTexture,
           optional<std::Texture2d> normalTexture,
           optional<std::Texture2d> specularTexture,
           optional<std::Texture2d> emissiveTexture,                      float smoothness,
           out vec4 Position,
           out vec4 Color) {
                   
    // non-linear mapping from [0,100] to [1,100]
     float shininess = mix(1.0, 100.0, pow(smoothness * 0.01, 2.0)); 

    // Attributes
    vec2 uv = std::getVertexTexCoord();
    optional<vec3> sampledNormal = normalize(std::getTangentFrame() * normalTexture.sample(uv).xyz * 2.0 - 1.0);
    vec3 localNormal = sampledNormal.valueOr(std::getVertexNormal());
    vec4 localPosition = std::getVertexPosition();

    // Material parameters
    vec4 diffuseAndOpacity = diffuseTexture.sample(uv).valueOr(vec4(1.0));
    vec4 specularAndShininess = specularTexture.sample(uv).valueOr(vec4(1.0));
    PhongMaterialParameters material;
    material.emission = emissiveTexture.sample(uv).rgb.valueOr(vec3(0.0));
    material.ambientFactor = diffuseAndOpacity.rgb;
    material.diffuseFactor = diffuseAndOpacity.rgb;
    material.specularFactor = specularAndShininess.rgb;
    material.shininess = clamp(specularAndShininess.a * shininess, 1.0, 100.0);
    material.occlusion = 1.0;

    // Screen-space position
    Position = std::getModelViewProjectionMatrix() * localPosition;

    // Camera-space normal, position, and view
    vec3 csNormal = normalize(fragment(std::getNormalMatrix() * localNormal));
    vec4 csPosition = fragment(std::getModelViewMatrix() * localPosition);
    vec3 csView = normalize(-csPosition.xyz); // csCamera is at vec3(0,0,0)

    // color
    vec3 color = material.emission + material.ambientFactor * std::getAmbientLight().rgb;
    if (std::getActiveLightCount() > 0) color += applyPhong(std::getLightData0(csPosition.xyz), csNormal, csView, material);
    if (std::getActiveLightCount() > 1) color += applyPhong(std::getLightData1(csPosition.xyz), csNormal, csView, material);
    if (std::getActiveLightCount() > 2) color += applyPhong(std::getLightData2(csPosition.xyz), csNormal, csView, material);
    if (std::getActiveLightCount() > 3) color += applyPhong(std::getLightData3(csPosition.xyz), csNormal, csView, material);
    Color = vec4(color, diffuseAndOpacity.a);
}

              

The main function takes a number of parameters of type optional std::Texture2d. The Texture2d struct provides a way of passing textures to shaders in SparkSL, and contains functions for texture sampling.

The use of the `optional` keyword in the data type is a language feature of SparkSL. 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 requires a fallback value to be specified. The fallback value will be used if the variable is not found within the shader.

For example, when sampling the normalTexture, the result of the subsequent computation is marked optional. When this optional value is accessed in the following line the valueOr-function is used, with the vertex normal being used as a fallback value if the sampled normal value isn't present in the shader.

When used as a material, here’s how the Inspector will look:

The smoothness parameter is a slider, because a min / max value was provided as an annotation. The complete material looks like this:

In this example we used a directional light and a spotlight:

The indices in light api functions are determined by the order of the lights in the scene. So in our example, the line std::getLightData0(csPosition.xyz) returns the directional light, while std::getLightData1(csPosition.xyz) returns the spotlight and the active lights count is 2.

Was this article helpful?