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.
To create a shader code asset:
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:
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.
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 is interoperable with existing visual shader patches and other shader code patches.
To use the shader code asset in the Patch Editor:
The resulting patch represents the shader's main
function.
However, any function in a shader can be used as a shader code asset if prefixed with an export
qualifier.
In the example below, both the star()
and circle()
functions are exported and result in usable patch assets that can be dragged from the Assets panel into the patch editor.
#import <gradients> #import <sdf> vec4 drawSdf(float dist, vec2 uv) { float edge = fwidth(dist); float alpha = smoothstep(-edge, +edge, dist); vec4 color = mix(0x00FFFFFF, 0x0000C0FF, std::gradientHorizontal(uv)); return mix(color, color.rgb0, alpha); } export vec4 star() { auto sdf = std::sdfStarSharp(vec2(0.5, 0.5), 0.25, 0.50, 5.0); vec2 uv = fragment(std::getVertexTexCoord()); float dist = sdf(uv); return drawSdf(dist, uv); } export vec4 circle() { auto sdf = std::sdfCircle(vec2(0.5, 0.5), 0.25); vec2 uv = fragment(std::getVertexTexCoord()); float dist = sdf(uv); return drawSdf(dist, uv); }
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.
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:
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.
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.
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.