2.deck.gl自定义着色器实现线性渐变

584 阅读6分钟

1. 概述

通过渐变色学习一下怎么在deck.gl中自定义着色器。

参考

官方文档 writing-shaders

官方文档 layer-extensions

luma.gl

deck.gl的渲染引擎出自同一个公司的luma.gl,所以里面关于渲染的apiluma.gl中有更详细的解释。必要时可以参考一下。

着色器需要一些webgl或者opengl等计算机图形编程知识,网上资料很多,入个门还是简单。

也可以看看我个人的入门webgl学习总结

2. 思路

deck.gl中的图形本质上都是通过一些glsl代码实现的,如果想要自定义一些效果,就不得不修改着色器代码。

对于实现渐变色,很显然我们只需要处理片元着色器输出的颜色变量就行。

3.前置知识

接着上一章节的代码,自定义GeoJsonLayer的着色器代码,按照官网的方法来说应该继承它然后重写getShaders,但是GeoJsonLayer实际上是由多个基础图层组合起来的一个图层,直接继承重写是不生效的。

所以这里推荐使用图层扩展(Layer Extension) 的方式,同时也方便将小功能独立出来。

import { LayerExtension } from "@deck.gl/core/typed";
// 继承LayerExtension
class LinearGradientExt extends LayerExtension {
// 定义名字,默认使用class的名字
// 名字会被deck内部作为cacheKey
static extensionName = "LinearGradientExt"
// 重写getShaders
  getShaders() {
    // 返回配置对象
    return {
      // 着色器hooks
      inject: {
        // 实现片元着色器中的DECKGL_FILTER_COLOR函数
        "fs:DECKGL_FILTER_COLOR": `
            // 将颜色固定为红色
            color = vec4(1, 0, 0, 1);
         `,
      },
    };
  }
}
// 定义layer的时候
  const layers = [
    new GeoJsonLayer({
      extensions: [new LinearGradientExt()],
    }),
  ];

效果就是这样,全红: image.png

getShaders的返回配置主要有下面几个:

  • vs:完全覆盖顶点着色器代码
  • fs:完全覆盖片元着色器代码
  • modules:着色器模块代码,本质上就是着色器代码片段,就像这样:
const colorShaderModule = {
  name: 'color',
  vs: `
    varying vec3 color_vColor;
    void color_setColor(vec3 color) {
      color_vColor = color;
    }
  `,
  fs: `
    varying vec3 color_vColor;
    vec3 color_getColor() {
      return color_vColor;
    }
  `
};

// 使用的时候
getShaders() {
    return {
        modules: [colorShaderModule]
    };
}
  • inject:着色器hooks,适用于需要在原始着色器代码的基础上做一些小修改。在文档中详细描述了内置的hook和功能参数。

    // 一些例子
    {
        //注入顶点着色器声明
        "vs:#decl": `
             varying vec2 vPosition;
         `,
        //注入顶点着色器main函数结尾处
        "vs:#main-end": `
             vPosition = vertexPositions;
         `,
        //注入片元着色器声明
        "fs:#decl": `
             varying vec2 vPosition;
         `,
        //重写颜色绘制函数
        "fs:DECKGL_FILTER_COLOR": `
             color = vec4(1, 0, 0, 1);
         `,
    }
    

大部分情况下只需要使用inject,一般不会去完全重写着色器代码,因为原始的着色器代码比较复杂。

4. 实现渐变色

// 简单的线性渐变首先要定义一个起始颜色,一个结束颜色
type LinearGradientOptions = {
  startColor: number[];
  endColor: number[];
};
class LinearGradientExt extends LayerExtension<LinearGradientOptions> {
  static extensionName = "LinearGradientExt"
  // 注意这里的this是指向附着的layer
  // extension才是扩展本身
  getShaders(this: Layer, extension: LinearGradientExt) {
    // 获取传入的配置
    const { startColor, endColor } = extension.opts;
    return {
      inject: {
        // 在顶点着色器中声明变量vPosition
        // 主要是用来存储顶点位置
        // 渐变需要根据顶点位置来确定颜色
        "vs:#decl": `
             varying vec2 vPosition;
         `,
        // 在顶点着色器main函数中给vPosition赋值
        // 这里的vertexPositions是内置的变量
        "vs:#main-end": `
             vPosition = vertexPositions;
         `,
        // 在片元着色器中定义同名变量vPosition
        // 接收来自顶点着色器的顶点数据
        "fs:#decl": `
             varying vec2 vPosition;
         `,
        // 实现DECKGL_FILTER_COLOR函数
        "fs:DECKGL_FILTER_COLOR": `
            vec4 linearColor = mix(
                vec4(${startColor.join(",")}), 
                vec4(${endColor.join(",")}), 
                pow(vPosition.y,1.0)
            );
            color = linearColor;
         `,
      },
    };
  }
}
// 使用
  const layers = [
    new GeoJsonLayer({
      extensions: [
        new LinearGradientExt({
          startColor: [1, 0, 0, 1],
          endColor: [0, 1, 0, 1],
        }),
      ],
    }),
  ];

效果就是这样: image.png

大概解释一下渐变的代码:

  • mixglsl中内置的函数,作用是根据权重在两个端点间插值,三个参数x, y, weight, 第三个参数weight代表混合程度取值0.0 ~ 1.0。如果第三个参数为0.0则全部为x的颜色,如果为1.0就完全是y的颜色。计算公式x(1weight)+yweight:x*(1-weight)+y*weight
  • 假设需要垂直方向的渐变,我们只需要让片元根据不同的y坐标,赋予不同混合程度的颜色。
  • 比如上面的例子,如果片元的y坐标为0,那么全是红色,同理随着y变大越来越接近绿色
  • 通过调整weight的计算公式可以实现不同的线性变换

可以偷懒就在顶点着色器中设置颜色

  getShaders(this: SolidPolygonLayer, extension: LinearGradientExt) {
    const { startColor, endColor } = extension.opts;
    return {
      inject: {
        "vs:#main-end": `
          vec4 linearColor = mix(
            vec4(${startColor.join(",")}),
            vec4(${endColor.join(",")}),
            // 因为是在顶点着色器中直接设置的
            // 所以不需要中间变量传递顶点数据了
            pow(vertexPositions.y, 1.0)
          );
          // 这里的vColor是内置着色器重定义的变量
          // 也就是前面操作的color变量
          vColor = linearColor;
        `
      },
    };
  }

5. 注意

5.1. 光照问题

前面的代码没有考虑光照的问题,任何角度下都是一样的颜色。如果要加入光照可以参考这样:

getShaders(this: SolidPolygonLayer, extension: LinearGradientExt) {
    const { startColor, endColor } = extension.opts;
    return {
      inject: {
      // 在顶点着色器中注入代码
      // 因为顶点着色器中计算了光照
        "vs:#main-end": `
          vec4 linearColor = mix(
            vec4(${startColor.join(",")}),
            vec4(${endColor.join(",")}),
            pow(vertexPositions.y, 1.0)
          );
          // lighting_getLightColor是内置着色器模块light的函数
          // 因为geojson中的SolidPolygonLayer已经引入了这个模块所以我们这里可以直接用
          // 若果没有引用就需要手动加上模块引用
          // 第一个参数是颜色的rgb值,后面三个变量都是内部计算好的,我直接复制的源码
          vec3 lightedLinearColor = lighting_getLightColor(linearColor.rgb, project_uCameraPosition, geometry.position.xyz, geometry.normal);
          // 赋值
          vColor = vec4(lightedLinearColor, vColor.a);
        `,
      },
    };
}

msedge_x5gtu9v4Zc.gif

可以看到颜色完全不一样,透明度也正确显示了。

5.2. 边界线

你可能发现修改了颜色之后边界线没了。

  1. 首先这个其实不是单独用lineLayer画出来的边界线,而是用GL.LINE_STRIP模式绘制出来的立体图。
  2. 本来GeojsonLayer有绘制lineLayer,但是当extruded=true的时候就不会绘制。源码
  3. wireframe=true的时候就会出现线框,宽度无法修改,如果想要自定义边界只能覆盖一层LineLayer

image.png

5.3. 版本问题

上面例子使用的decl.gl版本是8.x,我发现即将到来的9.0改变了很多着色器代码,上面的代码并不能正确运行在未来的版本中。

  getShaders(extension, ...rest) {
    const { startColor, endColor } = extension.opts;
    return {
      inject: {
      // 仍然是在顶点着色器中插入
        "vs:#main-end": `
            #ifdef IS_SIDE_VERTEX
            vec4 linearColor = mix(
              vec4(${startColor.join(",")}),
              vec4(${endColor.join(",")}),
              pow( props.elevations / 3500.0 , 1.0)
            );
            vec3 lightedLinearColor = lighting_getLightColor(linearColor.rgb,project_uCameraPosition, geometry.position.xyz, geometry.normal);
            vColor = vec4(lightedLinearColor, vColor.a);
          #endif
        `,
      },
    };
  }

着色代码只需要注入这一段,判断是否是侧面,然后将颜色改为渐变。

这里的3500就是配置的getElevation的属性值

    new GeoJsonLayer({
       // ....
      getElevation: (f) => 3500,
       // ....
      extensions: [
        new LinearGradientExt({
          startColor: [1, 0, 0, 1],
          endColor: [0, 1, 0, 1],
        }),
      ],
    }),
``