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

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) 的方式,同时也方便将小功能独立出来。

ts 复制代码
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()],
    }),
  ];

效果就是这样,全红:

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

  • vs:完全覆盖顶点着色器代码
  • fs:完全覆盖片元着色器代码
  • modules:着色器模块代码,本质上就是着色器代码片段,就像这样:
ts 复制代码
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和功能参数。

    ts 复制代码
    // 一些例子
    {
        //注入顶点着色器声明
        "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. 实现渐变色

ts 复制代码
// 简单的线性渐变首先要定义一个起始颜色,一个结束颜色
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],
        }),
      ],
    }),
  ];

效果就是这样:

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

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

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

c 复制代码
  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. 光照问题

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

c 复制代码
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);
        `,
      },
    };
}

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

5.2. 边界线

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

  1. 首先这个其实不是单独用lineLayer画出来的边界线,而是用GL.LINE_STRIP模式绘制出来的立体图。
  2. 本来GeojsonLayer有绘制lineLayer,但是当extruded=true的时候就不会绘制。源码
  3. wireframe=true的时候就会出现线框,宽度无法修改,如果想要自定义边界只能覆盖一层LineLayer
  4. 下面是稍微改造一下的着色器代码,可以显示出wireframe
c 复制代码
  getShaders(this: SolidPolygonLayer, extension: LinearGradientExt) {
    const { startColor, endColor } = extension.opts;
    return {
      inject: {
        "vs:#main-end": `
        // 判断一下isWireframe,这是内置的变量
        // 如果是Wireframe就绘制线条颜色
          vec4 linearColor = isWireframe ? props.lineColors : mix(
            vec4(${startColor.join(",")}),
            vec4(${endColor.join(",")}),
            pow(vertexPositions.y, 1.0)
          );
          vec3 lightedLinearColor = lighting_getLightColor(linearColor.rgb, project_uCameraPosition, geometry.position.xyz, geometry.normal);
          vColor = vec4(lightedLinearColor, vColor.a);
        `,
      },
    };
  }

5.3. 版本问题

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

这里给出一点思路,主要是通过源码想到的:

c 复制代码
  getShaders(extension, ...rest) {
    const { startColor, endColor } = extension.opts;
    return {
      inject: {
      // 仍然是在顶点着色器中插入
        "vs:#main-end": `
          vec4 linearColor = isWireframe ? props.lineColors : 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);
        `,
      },
    };
  }

其余部分都是一样的,唯一要修改的就是10行y坐标的取值,之前的版本中有一个统一的变量vertexPositions,但是新版本中没有了。

在绘制侧面的着色器代码中使用的postions存储坐标数据。

在绘制顶部的着色器代码中使用的vertexPositions存储的数据。

导致之前统一操作vertexPositions会出问题,这里通过还原挤出前的y坐标实现获取原本的坐标

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

ts 复制代码
    new GeoJsonLayer({
       // ....
      getElevation: (f) => 3500,
       // ....
      extensions: [
        new LinearGradientExt({
          startColor: [1, 0, 0, 1],
          endColor: [0, 1, 0, 1],
        }),
      ],
    }),
``
相关推荐
果子切克now1 小时前
vue2与vue3知识点
前端·javascript·vue.js
积水成江1 小时前
Vite+Vue3+SpringBoot项目如何打包部署
java·前端·vue.js·windows·spring boot·后端·nginx
一丝晨光1 小时前
Web技术简史、前后端分离、游戏
前端·javascript·css·游戏·unity·前后端分离·cocos
假客套1 小时前
2024 uniapp入门教程 01:含有vue3基础 我的第一个uniapp页面
前端·uni-app·vue3·hbuilder x
柒小毓2 小时前
网站开发基础:HTML、CSS
前端·css·html
&白帝&3 小时前
Vue.js 过渡 & 动画
前端·javascript
总是学不会.4 小时前
SpringBoot项目:前后端打包与部署(使用 Maven)
java·服务器·前端·后端·maven
Fanfffff7204 小时前
深入探索Vue3组合式API
前端·javascript·vue.js
光影少年4 小时前
node配置swagger
前端·javascript·node.js·swagger
bin91534 小时前
【EXCEL数据处理】000013 案例 EXCEL筛选与高级筛选。
大数据·信息可视化·数据挖掘·数据分析·excel·数据可视化·数据筛选