1. 概述
通过渐变色学习一下怎么在deck.gl
中自定义着色器。
deck.gl
的渲染引擎出自同一个公司的luma.gl
,所以里面关于渲染的api
在luma.gl
中有更详细的解释。必要时可以参考一下。
着色器需要一些webgl
或者opengl
等计算机图形编程知识,网上资料很多,入个门还是简单。
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],
}),
],
}),
];
效果就是这样:
大概解释一下渐变的代码:
mix
是glsl
中内置的函数,作用是根据权重在两个端点间插值,三个参数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. 边界线
你可能发现修改了颜色之后边界线没了。
- 首先这个其实不是单独用
lineLayer
画出来的边界线,而是用GL.LINE_STRIP
模式绘制出来的立体图。 - 本来
GeojsonLayer
有绘制lineLayer
,但是当extruded=true
的时候就不会绘制。源码 - 在
wireframe=true
的时候就会出现线框,宽度无法修改,如果想要自定义边界只能覆盖一层LineLayer
。 - 下面是稍微改造一下的着色器代码,可以显示出
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],
}),
],
}),
``