Skip to content

无光照材质

下面是一个简单的例子,我们定义一个不受光照的材质,表现一些边缘光的效果。

javascript

// 定义边缘光材质
class RimColorMaterial extends MeshMaterial {
  constructor() {
    super();
    // 边缘光的颜色
    this.color = new Vector3(1, 1, 1);
  }
  // 不受光照影响
  supportLighting() {
    return false;
  }
  // VertexShader实现
  // scope是vertexShader的main函数作用域
  vertexShader(scope) {
    // 必须调用父类的vertexShader方法
    // 父类vertexShader负责初始化全局uniform参数以及在必要的情况下定义骨骼动画相关顶点属性。
    super.vertexShader(scope);
    const pb = scope.$builder;
    // 定义顶点位置输入
    // 如果使用了ShaderHelper.resolveVertexPosition()函数,这一行可以省略。
    scope.$inputs.pos = pb.vec3().attrib('position');
    // 定义顶点法线输入
    // 如果使用了ShaderHelper.resolveVertexNormal()函数,这一行可以省略。
    scope.$inputs.normal = pb.vec3().attrib('normal');
    // ShaderHelper.resolveVertexPosition()函数用于计算局部坐标系的顶点位置。
    // 如果有骨骼动画,该函数计算经过骨骼运算后的顶点位置,否则返回输入的顶点位置。
    scope.$l.oPos = ShaderHelper.resolveVertexPosition(scope);
    // ShaderHelper.resolveVertexNormal()函数用于计算局部坐标系的顶点法线。
    // 如果有骨骼动画,该函数计算经过骨骼运算后的顶点法线,否则返回输入的顶点法线。
    scope.$l.oNorm = ShaderHelper.resolveVertexNormal(scope);
    // 计算世界坐标系的顶点位置并输出到fragmentShader
    // ShaderHelper.getWorldMatrix()函数用于获取当前的局部到世界的位置变换矩阵
    // scope.$outputs作用域用于定义varying变量。
    scope.$outputs.worldPos = pb.mul(ShaderHelper.getWorldMatrix(scope), pb.vec4(scope.oPos, 1)).xyz;
    // 计算世界坐标系的顶点法线向量并输出到fragmentShader
    // ShaderHelper.getNormalMatrix()函数用于获取当前的局部到世界的法线变换矩阵
    scope.$outputs.worldNorm = pb.mul(ShaderHelper.getNormalMatrix(scope), pb.vec4(scope.oNorm, 0)).xyz;
    // 输出剪裁空间的顶点坐标
    // ShaderHelper.getViewProjectionMatrix()获取当前的世界空间到剪裁空间的变换矩阵。
    // 注意不要直接给scope.$builtins.position赋值,因为在WebGPU设备下渲染到纹理时需要上下翻转
    // 需要使用ShaderHelper.setClipSpacePosition()方法。
    ShaderHelper.setClipSpacePosition(scope, pb.mul(ShaderHelper.getViewProjectionMatrix(scope), pb.vec4(scope.$outputs.worldPos, 1)));
  }
  // FragmentShader实现
  // scope是fragmentShader的main函数作用域
  fragmentShader(scope) {
    // 必须调用父类的fragmentShader方法
    // 父类fragmentShader负责初始化全局uniform参数
    super.fragmentShader(scope);
    const pb = scope.$builder;
    // MeshMaterial的needFragmentColor()函数返回
    // 当前Shader是否需要计算片元颜色。如果当前的
    // RenderPass是DepthPass或ShadowMapPass且材质
    // alphaCutoff属性等于0(未开启AlphaTest),则
    // 无需计算片元颜色。
    if (this.needFragmentColor()) {
      // 定义一个vec3类型的uniform用于指定边缘光颜色
      // 注意:用于材质的uniform,对应的BindGroup索引
      // 为2。
      scope.rimColor = pb.vec3().uniform(2);
      // 计算视线向量
      // ShaderHelper.getCameraPosition()函数用于获取当前摄像机的世界坐标系位置。
      // scope.$inputs作用域保存由vertexShader传来的varying变量。
      scope.$l.viewVec = pb.normalize(pb.sub(ShaderHelper.getCameraPosition(scope), scope.$inputs.worldPos));
      // 计算NdotV,值越小边缘光越亮
      scope.$l.NdotV = pb.clamp(pb.dot(pb.normalize(scope.$inputs.worldNorm), scope.viewVec), 0, 1);
      // 计算最终的片元颜色
      scope.$l.finalColor = pb.mul(scope.rimColor, pb.pow(pb.sub(1, scope.NdotV), 4));
      // 输出片元颜色
      // 材质的outputFragmentColor()方法用于输出片元颜色。该方法执行剪裁平面测试(如果定义了剪裁平面), AlphaTest测试
      // (如果材质的alphaCutoff大于0),根据是否半透明材质处理片元的alpha通道,在需要的情况下执行GammaCorrection。
      this.outputFragmentColor(scope, scope.$inputs.worldPos, pb.vec4(scope.finalColor, 1));
    } else {
      // 不需要计算片元颜色则直接输出null
      this.outputFragmentColor(scope, scope.$inputs.worldPos, null);
    }
  }
  // 设置材质的Uniform常量
  applyUniformValues(bindGroup, ctx, pass){
    // 必须调用父类
    super.applyUniformValues(bindGroup, ctx, pass);
    if (this.needFragmentColor(ctx)){
    // 需要计算片元颜色时我们才定义此Uniform
      bindGroup.setValue('rimColor', this.color);
    }
  }
}

// 使用自定义材质
const material = new RimColorMaterial();
// 设置边缘光颜色为黄色
material.color.setXYZ(1, 1, 0)
// 材质的uniform有缓存,当材质uniform发生变化时需要调用
// uniformChanged()方法通知材质需要重新提交uniform
material.uniformChanged();

// 使用此材质渲染网格。
const mesh = new Mesh(scene, new TorusShape(), material);

Released under the MIT License.