在mapboxgl中改源码相比cesium还是比较繁琐的,mapboxgl中存在着大量的样式配置内容,写一个面的水流效果就要改动很多东西,最近将之前写的水流效果迁移到了最新的mapboxgl版本上,顺便再次梳理下流程。
本文要实现的水体流动效果就是类似于高德地图上水的流动效果,是一种全局效果,移动地图不影响水流动的位置。
本效果是基于fill数据,fill绘制的program有目前几种,fillPattern
、fill
、fillOutlinePattern
和fillOutline
,去结合对应的uniform变量和shader来对应不同的绘制方式。所以我们要新增一种,用来走我们水流特效的绘制。
// draw_fill.js
if (!isOutline) {
if(image){
programName = 'fillPattern';
}else{
if(layer.getPaintProperty('waterTime')){
programName = 'fillWaterFlow';
}else{
programName = 'fill';
}
}
drawMode = gl.TRIANGLES;
} else {
programName = image && !layer.getPaintProperty('fill-outline-color') ? 'fillOutlinePattern' : 'fillOutline';
drawMode = gl.LINES;
}
然后我们再根据类型判断,去取对应的uniform变量。
// draw_fill.js
if(image){
uniformValues = fillPatternUniformValues(tileMatrix, painter, crossfade, tile);
}else{
// 此处判断类型,然后去取相应的uniform
if(layer.getPaintProperty('waterTime')){
uniformValues = fillWaterFlowUniformValues(tileMatrix, layer.getPaintProperty('waterTime'));
}else{
uniformValues = fillUniformValues(tileMatrix);
}
}
} else
由于mapboxgl是DT语法写的,开源出来的代码应该不是源代码,而是解析后的js文件,所以存在很多类型的定义。
在draw_fill.js
中我们提到了需要根据类型去取uniform,去对应的uniform值的方法就在fill_program.js
里定义。
// 各个uniform的类型
export type FillWaterFlowUniformsType = {|
'u_matrix': UniformMatrix4f,
'u_time': Uniform1f
|};
// 构造各个uniform
const fillWaterFlowUniforms = (context: Context): FillWaterFlowUniformsType => ({
'u_matrix': new UniformMatrix4f(context),
'u_time': new Uniform1f(context),
});
// 返回各个uniform值
const fillWaterFlowUniformValues = (matrix: Float32Array, time): UniformValues<FillWaterFlowUniformsType> => ({
'u_matrix': matrix,
'u_time': time
});
每一个绘制类型都有对应的program,例如line有对应的line_program,fill就有对应的fill_program,每个program里定义了uniform的类型和取值方法,各个类型的program里的uniform变量都要导出到program_uniforms.js
里来去统一调用,所以记得在这里做一次导入导出。
同样的,我们也要为这个新增的类型去编写新的顶点和片元着色器。因为我们写水流效果,其实只需要在fs里面去写就好了,那么vs的shader直接copy fill.vertex.glsl
里的代码即可。
片元着色器里水流效果其实我是从shadertoy里扒下来的,其实原理都一样,就是利用噪声函数来随机生成,然后根据时间去动态移动噪点来达到的效果。
// fill_waterflow.fragment.glsl
#pragma mapbox: define highp vec4 color
#pragma mapbox: define lowp float opacity
uniform float u_time;
float random(float x) {
return fract(sin(x) * 10000.);
}
float noise(vec2 p) {
return random(p.x + p.y * 10000.);
}
vec2 sw(vec2 p) { return vec2(floor(p.x), floor(p.y)); }
vec2 se(vec2 p) { return vec2(ceil(p.x), floor(p.y)); }
vec2 nw(vec2 p) { return vec2(floor(p.x), ceil(p.y)); }
vec2 ne(vec2 p) { return vec2(ceil(p.x), ceil(p.y)); }
float smoothNoise(vec2 p) {
vec2 interp = smoothstep(0., 1., fract(p));
float s = mix(noise(sw(p)), noise(se(p)), interp.x);
float n = mix(noise(nw(p)), noise(ne(p)), interp.x);
return mix(s, n, interp.y);
}
float fractalNoise(vec2 p) {
float x = 0.;
x += smoothNoise(p );
x += smoothNoise(p * 2. ) / 2.;
x += smoothNoise(p * 4. ) / 4.;
x += smoothNoise(p * 8. ) / 8.;
x += smoothNoise(p * 16.) / 16.;
x /= 1. + 1./2. + 1./4. + 1./8. + 1./16.;
return x;
}
float movingNoise(vec2 p) {
float x = fractalNoise(p + u_time);
float y = fractalNoise(p - u_time);
return fractalNoise(p + vec2(x, y));
}
// call this for water noise function
float nestedNoise(vec2 p) {
float x = movingNoise(p);
float y = movingNoise(p + 100.);
return movingNoise(p + vec2(x, y));
}
void main() {
#pragma mapbox: initialize highp vec4 color
#pragma mapbox: initialize lowp float opacity
vec2 iResolution = vec2(1920, 1048);
vec2 uv = gl_FragCoord.xy / iResolution.xy;
float n = nestedNoise(uv * 6.);
gl_FragColor = vec4(mix(vec3(.4, .6, 1), vec3(color.rgb), n), opacity);
#ifdef OVERDRAW_INSPECTOR
gl_FragColor = vec4(1.0);
#endif
}
编写完shader后,记得要在shaders.js
里面引入,因为要在此处将glsl代码进行编译。
在mapboxgl中,所有的数据加载效果都是依靠style来进行配置。我们在这个新类型中由于添加了一个waterTime
参数(对应在内部的配置uniform变量名称为u_time
),所以我们需要将这个新参数进行定义添加,因为在mapboxgl内部要做很多样式类型检查。
首先我们找到src/style-spec/reference/v8.json
,将waterTime
这个参数进行定义,记得要定义在paint_fill
中,因为我们这个是关于fill
的paint
属性,所以不要加错地方。
然后在src/style-spec/type.js
中找到FillLayerSpecification
,将waterTime
加入到paint
属性下面。
最后在src/style/style_layer/fill_style_layer_properties.js
中将waterTime
加入。
至此源码里的改动就算完成了,然后我们再去写一个水流效果的构造函数即可,这里就不贴了,其实就是添加一个fill_layer,然后配置waterTime参数,唯一注意的地方是,你要让这个变量随时变化。我在这里写了个事件,让waterTime随时变化,在map每次render的时候调用即可。