Spark AR —— 着色器代码资产【脚本】

528 阅读7分钟

这是我参与8月更文挑战的第31天,活动详情查看: 8月更文挑战

Spark AR 是 Facebook 免费创作 AR 作品的平台,使用户能够为 Facebook 和 Instagram 创建交互式增强现实体验,超过 40 万名创作者,190个国家/地区,使用 Spark AR 来创作自己的AR作品

由于该软件无需任何编码知识即可使用,因此任何人现在都可以在 AR 世界中几乎没有经验地制作下一个疯狂式传播的 Instagram AR 特效,引领世界潮流。

专门的 AR 滤镜设计师单价甚至可达到 1000 美元到 3 万美元不等。

image.png

着色器代码资源可以让你在 Spark AR Studio 中编写自定义着色器。

如果你已经创建了一个完整的着色器(Shader)其返回 vec4 颜色或有一个 out vec4 颜色参数,它就可以作为一个材质,称为着色器代码资产材质。

对于一个更模块化的方法,你可以在贴片(Patch)编辑器中实例化着色器代码资产,并将其作为贴片。我们将在本文中介绍这两种方法。

创建着色器代码资产

创建一个着色器代码资产:

  1. 在 Spark AR Studio 中,进入资产面板。
  2. 单击 Add Asset,并从菜单中选择 Shader Code Asset

着色器代码资源将在资产面板中列出。一个 .sca 文件将被添加到项目中。编辑着色器代码资产:

  1. 在资源面板中选择着色器代码资源。
  2. 在检查器中,单击Edit

您对文件所做的更改将在保存后反映在 Spark AR Studio 中。保存时,任何编译错误或警告都会出现在控制台中。

定义函数

着色器必须定义一个主函数。这个函数的输入和输出将定义着色器代码资产的接口,其在 Spark AR Studio 中作为材质或贴片使用。例如,当带有下面签名的着色器代码资产实例化为一个贴片时,将有一个单一的输入 Alpha 和一个单一的输出 Color:

void main(float Alpha, out vec4 Color);

当作为材质使用时,材质只有一个输入值,Alpha。颜色参数将是材质的颜色。

如果没有找到名为'main'的函数,则文件中的最后一个函数将被视为main 函数。

学习更多关于着色器语言的知识

着色器代码资产贴片

着色器代码资产贴片 与 现有的可视化着色器贴片 和 其他着色器代码贴片 可互操作。

在贴片编辑器中使用着色器代码资源:

  1. 创建一个着色器代码资产;
  2. 打开贴片编辑器;
  3. 从资产面板中将着色器代码资源拖到贴片编辑器中。

image.png

生成的贴片代表了着色器的 main 函数。

然而,如果前缀有一个 export 限定符,着色器中的任何函数都可以作为着色器代码资产。

在下面的例子中,star()circle() 函数都被导出,并生成可用的贴片资源,这些资源可以从资产面板中拖到贴片编辑器中。

#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);
}         

输入与输出

贴片的输入和输出是由着色器的 main 函数决定的。输入参数将显示为输入端口,输出参数将显示为输出端口。若要将参数标记为输出,请在其前面加上 out 限定符。如果着色器有一个返回类型而不是void,这也将作为输出端口出现。

与着色器代码资产材质一样,输入端口的默认值可以使用注解(annotation)指定。

着色器代码资产材质

任何输出 vec4 颜色值的着色器代码资产,无论是通过输出参数还是作为返回值,都可以直接作为材质使用。

使用着色器代码资产直接作为材质:

  1. 创建一个新材质。
  2. 在检查器中,在Shader Type下拉菜单中选择 shader 资源。

要对材质执行顶点位移,你可以通过创建一个额外的 vec4 out 参数并命名为Position来直接写入顶点位置。

基础例子

在这个例子中,我们使用了着色器代码资源来创建一个具有尖边的彩色心形:

image.png

这个例子是用下面的着色器创建的:

#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);
}

本例中的 main 函数返回一个 vec4 并接受三个参数,两个 vec4 指定形状的两种颜色,以及一个 float spikiness,它指定 spikes 的大小。

因此,当着色器直接作为材质应用时,你会在检查器中看到这些值:

image.png

在 Parameters 下,三个输入变量的默认值对应于main 函数注解中指定的值。你可以在检查器中修改这些属性,或者在脚本中对材质调用 setParameter 方法。

SDF 模块提供的 std::sdfStarSharp 函数创建一个星形的 SDF。

要使用这个函数,这个模块必须使用在着色器顶部的 import 语句来导入。

高级例子

在这个例子中,我们将在 Spark AR Studio 中实现一个响应光的phong 材质,同时覆盖 Spark 特有的着色语言特性。生成的材质支持可选的漫反射、镜面和发光纹理,并可以响应定向光、聚光、点光和环境光。

#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);
}

main 函数接受一系列可选的 std::Texture2d 类型参数。Texture2d结构体提供了一种将纹理传递给 SparkSL 中的着色器的方法,并包含纹理采样函数。

在数据类型中使用 optional 关键字是 SparkSL 的一种语言特性。可选类型的变量不要求它的值出现在着色器中。相反,可选变量的值必须通过 valueOr 函数访问,这需要指定一个回退值。如果在着色器中找不到变量,将使用回退值。

例如,当采样 normalTexture 时,后续计算的结果被标记为可选。当这个可选值在下一行被访问时,valueOr 函数被使用,如果采样的法线值不在着色器中的话,顶点法线被用作一个回退值。

当作为一种材质使用时,检查面板将会是这样的:

image.png

平滑参数是一个滑块,因为最小/最大值是作为注解提供的。完整的材质看起来是这样的:

image.png

在这个例子中,我们使用了定向光和聚光灯:

image.png

light api 函数的索引是由场景中灯光的顺序所决定的。所以在我们的例子中,行 std::getLightData0(cposition .xyz) 返回方向光,而 std::getLightData1(cposition .xyz) 返回聚光灯,激活的光计数为 2。