Skip to content

Shader变体

一种很常见的情况是,我们设置材质的不同属性以后,材质需要生成不相同的代码,也就是说同一材质的Shader可能会有不同的变体。

你可以调用材质的静态方法defineFeature()来为材质声明变体。

javascript

class MyMaterial extends MeshMaterial {
  // 定义了一个Feature,该feature可以拥有若干变体
  static featureA = this.defineFeature();
  // 另一个Feature
  static featureB = this.defineFeature();

  foo() {
    // useFeature()方法可以用来激活一个变体
    // value参数可以是任何值,每个不同的值各代表一个变体
    // 未调用useFeature(),变体值默认为undefined
    this.useFeature(MyMaterial.featureA, value);
    // 获取当前的变体
    const value = this.featureUsed(MyMaterial.featureA);
  }
  bar() {
    // 如果是简单的开关,可以使用布尔值做变体
    this.useFeature(MyMaterial.featureB, true);
  }
}

在Shader的实现中要注意根据变体做不同的实现。

下面的例子中,我们定义了一个Lambert材质,该材质你可以设置一个可选的纹理,从而生成两个Shader变体。

javascript

// 自定义Lambert材质
// 材质的diffuseTexture属性是否为空个代表一个变体
class MyLambertMaterial extends applyMaterialMixins(MeshMaterial, mixinLambert) {
  static featureDiffuseTexture = this.defineFeature();
  constructor() {
    super();
    // 漫反射颜色
    this.color = new Vector4(1, 1, 1, 1);
    // 漫反射贴图,默认为空
    this.diffuseTexture = null;
  }
  // 在材质被应用之前更新变体值
  apply(ctx) {
    // 有无diffuseTexture形成两个变体
    this.useFeature(MyLambertMaterial.featureDiffuseTexture, !!this.diffuseTexture);
    // 默认实现必须调用
    return super.apply(ctx);
  }
  // 受光照影响
  supportLighting() {
    return true;
  }
  // VertexShader实现
  // 该实现和上一个例子相同
  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;
    // 变体判断
    if (this.diffuseTexture) {
      scope.$inputs.texcoord = pb.vec2().attrib('texCoord0');
      scope.$outputs.texcoord = scope.$inputs.texcoord;
    }
  }
  // FragmentShader实现
  fragmentShader(scope) {
    super.fragmentShader(scope);
    const pb = scope.$builder;
    if (this.needFragmentColor()) {
      scope.diffuseColor = pb.vec4().uniform(2);
      // 变体判断
      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));
      // 变体判断
      if (this.diffuseTexture) {
        scope.$l.diffuse = pb.mul(pb.textureSample(scope.diffuseTexture, scope.$inputs.texcoord), scope.diffuseColor);
      } else {
        scope.$l.diffuse = scope.diffuseColor;
      }
      // 光照计算
      scope.$l.litColor = this.lambertLight(scope, scope.$inputs.worldPos, scope.normal, scope.diffuse);
      // 输出片元颜色
      this.outputFragmentColor(scope, scope.$inputs.worldPos, pb.vec4(scope.litColor, scope.diffuse.a));
    } else {
      // 不需要计算片元颜色则直接输出null
      this.outputFragmentColor(scope, scope.$inputs.worldPos, null);
    }
  }
  // 设置材质的Uniform常量
  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)
      }
    }
  }
}

下面的例子中,按下空格键可以切换有无贴图两个变体。

Released under the MIT License.