Skip to content

Shader Variants

A common scenario is that after setting various properties of a material, it needs to generate different codes, meaning that the shader of the same material may have different variants.

You can use the static method defineFeature() to declare variants for a material.

javascript

class MyMaterial extends MeshMaterial {
  // Define a Feature that can have several variants.
  static featureA = this.defineFeature();
  // Define another feature
  static featureB = this.defineFeature();

  foo() {
    /*
      Activating a variant can be achieved by calling the useFeature() method.
      The value parameter can be any value, and each different value represents
      a variant. The initial value for any feature is undefined.
     */
    this.useFeature(MyMaterial.featureA, value);
    // Get current variant value.
    const value = this.featureUsed(MyMaterial.featureA);
  }
  bar() {
    // Boolean values can be used as variants for simple switches.
    this.useFeature(MyMaterial.featureB, true);
  }
}

In the example below, we define a Lambert material that you can optionally texture, resulting in two shader variants.

javascript

// Whether the diffuseTexture attribute of the material is empty or not represents a variant
class MyLambertMaterial extends applyMaterialMixins(MeshMaterial, mixinLambert) {
  static featureDiffuseTexture = this.defineFeature();
  constructor() {
    super();
    // Diffuse color
    this.color = new Vector4(1, 1, 1, 1);
    // Diffuse texture,default to null
    this.diffuseTexture = null;
  }
  // Update variant values before applying material
  apply(ctx) {
    this.useFeature(MyLambertMaterial.featureDiffuseTexture, !!this.diffuseTexture);
    // Default implementation must be invoked.
    return super.apply(ctx);
  }
  supportLighting() {
    return true;
  }
  // vertex shader implementation
  // This implementation is the same as the previous example.
  vertexShader(scope) {
    super.vertexShader(scope);
    const pb = scope.$builder;
    scope.$inputs.pos = pb.vec3().attrib('position');
    scope.$inputs.normal = pb.vec3().attrib('normal');
    scope.$l.oPos = ShaderHelper.resolveVertexPosition(scope);
    scope.$l.oNorm = ShaderHelper.resolveVertexNormal(scope);
    scope.$outputs.worldPos = pb.mul(ShaderHelper.getWorldMatrix(scope), pb.vec4(scope.oPos, 1)).xyz;
    ShaderHelper.setClipSpacePosition(scope, pb.mul(ShaderHelper.getViewProjectionMatrix(scope), pb.vec4(scope.$outputs.worldPos, 1)));
    scope.$outputs.worldNorm = pb.mul(ShaderHelper.getNormalMatrix(scope), pb.vec4(scope.oNorm, 0)).xyz;
    // Identify variants
    if (this.diffuseTexture) {
      scope.$inputs.texcoord = pb.vec2().attrib('texCoord0');
      scope.$outputs.texcoord = scope.$inputs.texcoord;
    }
  }
  // fragment shader implementation
  fragmentShader(scope) {
    super.fragmentShader(scope);
    const pb = scope.$builder;
    if (this.needFragmentColor()) {
      scope.diffuseColor = pb.vec4().uniform(2);
      // Identify variants
      if (this.diffuseTexture) {
        scope.diffuseTexture = pb.tex2D().uniform(2);
      }
      scope.$l.normal = this.calculateNormal(scope, scope.$inputs.worldPos, scope.$inputs.worldNorm);
      scope.$l.viewVec = pb.normalize(pb.sub(ShaderHelper.getCameraPosition(scope), scope.$inputs.worldPos));
      // Identify variants
      if (this.diffuseTexture) {
        scope.$l.diffuse = pb.mul(pb.textureSample(scope.diffuseTexture, scope.$inputs.texcoord), scope.diffuseColor);
      } else {
        scope.$l.diffuse = scope.diffuseColor;
      }
      // Calcuate lighting.
      scope.$l.litColor = this.lambertLight(scope, scope.$inputs.worldPos, scope.normal, scope.diffuse);
      // Output fragment color
      this.outputFragmentColor(scope, scope.$inputs.worldPos, pb.vec4(scope.litColor, scope.diffuse.a));
    } else {
      this.outputFragmentColor(scope, scope.$inputs.worldPos, null);
    }
  }
  applyUniformValues(bindGroup, ctx, pass){
    super.applyUniformValues(bindGroup, ctx, pass);
    if (this.needFragmentColor(ctx)){
      bindGroup.setValue('diffuseColor', this.color);
      if (this.diffuseTexture) {
        bindGroup.setTexture('diffuseTexture', this.diffuseTexture)
      }
    }
  }
}

In this example, pressing the spacebar toggles between two variations with and without diffuse texture.

Released under the MIT License.