在高德地图实现水域图层

介绍

最近我在研究3D地图的开发,做久了慢慢觉得整个场景不够真实,究其原因是缺少一些自然的元素,于是突发奇想能否把自然四大元素整合到场景里。江海河流湖泊在地图中国占据的比例较高,就优先研究了下在高德地图中展示动态的水域效果。

需求说明

先从简单一点的需求入手。

  1. 划设一到多个多边形区域,实现动态水域层
  2. 水域需要有微风吹过波涛起伏的视觉效果,最好支持后期快速修改
  3. 水域层支持配置颜色、透明度、水体质感

实现思路

1.根据提供的数据,在地图上绘制多边形平面。可以考虑使用在线GeoJOSN编辑器做绘制,拿到自动生成的GeoJSON数据后根据当前使用的地图坐标系做一下转换就行了。本文需要将WGS84转为高德地图的火星坐标系。

Untitled.png

2.给多边形赋予水域材质,这里需要用到Three.js提供的自定义着色器材质ShaderMaterial,Shader是一个glsl语言写的着色程序,运行在GPU上。简易理解就是,GPU能够同时处理显示器屏幕上所有的像素点,在显卡的加持下有着优越的性能,适合处理各种需要大量并行计算的流体、粒子等效果,原则上只要你的图形学功底够硬,就行实现任何想象到的效果。

重点来了,There.js的官方示例中恰好提供了这样的一个水体ShaderMaterial,只要稍加改造就可以拿来用了。

Dingtalk_20221204120122.jpg

  1. 最后一步就是随帧更新材质的时间轴,让材质动起来,配合整个场景微调一下。

Honeycam_2022-12-04_12-57-46.gif

代码实现

1.实现这些需求还是需要用到高德地图AMap JS API 2.0 提供的GLSCustomLayer,结合一些Three.js的基础知识。这些内容之前写过就不多做介绍了,感兴趣看一看之前的文章 在高德地图中进行THREE开发-贴地呼吸点图层

自定义图层-GLCustomLayer 结合 THREE-自有数据图层-示例中心-JS API 2.0 示例 | 高德地图API

2.绘制多边形平面

// 绘制单个区域
drawOneArea (path) {
  const { scene } = this

  const shape = new THREE.Shape()
  path.forEach(([x, y], index) => {
    if (index === 0) {
      shape.moveTo(x, y)
    } else {
      shape.lineTo(x, y)
    }
  })

  // 创建一个多边形扁平面几何体
  const geometry = new THREE.ShapeGeometry(shape)
  // 使用自定义材质,创建模型对象
  const mesh = new THREE.Mesh(geometry, this.getMaterial())
  // 调整模型海拔高度
  mesh.position.z = this._altitude
  // 加入到场景
  scene.add(mesh)
}

3.创建材质

//自定义水体的shader
import WaterShader from './Water.js'

class WaterLayer extends Layer {

uniforms = THREE.UniformsUtils.merge([
    THREE.UniformsLib.fog,
    THREE.UniformsLib.lights,
    WaterShader.uniforms])

constructor ({...}){
super(...)
  // 初始化工作 设置水体的材质图、颜色等等
this.uniforms.normalSampler.value = waterNormalsTexture
    this.uniforms.waterColor.value = new THREE.Color(waterColor)
    this.uniforms.distortionScale.value = distortionScale
}

//获取材质
getMaterial () {
  const { vertexShader, fragmentShader } = WaterShader
  const material = new THREE.ShaderMaterial({
    // 材质的配置参数,uniforms中的属性可供glsl语言读取
    uniforms: this.uniforms,
    // 顶点着色器
    vertexShader,
    // 片源着色器
    fragmentShader,
    // 自持深度遮挡
    depthTest: true,
    // 支持透明
    transparent: true
  })
  return material
}

4.自定义水体的shader

/* 微波水域 */
import * as THREE from 'three'
const shader = {
  uniforms: {
    normalSampler: { type: 't', value: null },
    mirrorSampler:{ type: 't', value: null },
    alpha:{ type: 'f', value: 1.0 },
    time:{ type: 'f', value: 0.0 },
    distortionScale:{ type: 'f', value: 20.0 },
    noiseScale:{ type: 'f', value: 1.0 },
    textureMatrix:{ type: 'm4', value: new THREE.Matrix4() },
    sunColor:{ type: 'c', value: new THREE.Color(0x7F7F7F) },
    sunDirection:{ type: 'v3', value: new THREE.Vector3(0.70707, 0.70707, 0) },
    eye:{ type: 'v3', value: new THREE.Vector3(0, 0, 0) },
    waterColor:{ type: 'c', value: new THREE.Color(0x555555) }
  },

  vertexShader: `
    uniform mat4 textureMatrix;
    uniform float time;
    varying vec4 mirrorCoord;
    varying vec3 worldPosition;
    varying vec3 modelPosition;
    varying vec3 surfaceX;
    varying vec3 surfaceY;
    varying vec3 surfaceZ;
    void main()
    {
      mirrorCoord = modelMatrix * vec4(position, 1.0);
      worldPosition = mirrorCoord.xyz;
      modelPosition = position;
      surfaceX = vec3( modelMatrix[0][0], modelMatrix[0][1], modelMatrix[0][2]);
      surfaceY = vec3( modelMatrix[1][0], modelMatrix[1][1], modelMatrix[1][2]);
      surfaceZ = vec3( modelMatrix[2][0], modelMatrix[2][1], modelMatrix[2][2]);
      mirrorCoord = textureMatrix * mirrorCoord;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,

  fragmentShader: [
    'uniform sampler2D mirrorSampler;',
    'uniform float alpha;',
    'uniform float time;',
    'uniform float distortionScale;',
    'uniform float noiseScale;',
    'uniform sampler2D normalSampler;',
    'uniform vec3 sunColor;',
    'uniform vec3 sunDirection;',
    'uniform vec3 eye;',
    'uniform vec3 waterColor;',

    'varying vec4 mirrorCoord;',
    'varying vec3 worldPosition;',
    'varying vec3 modelPosition;',
    'varying vec3 surfaceX;',
    'varying vec3 surfaceY;',
    'varying vec3 surfaceZ;',

    'void sunLight(const vec3 surfaceNormal, const vec3 eyeDirection, in float shiny, in float spec, in float diffuse, inout vec3 diffuseColor, inout vec3 specularColor)',
    '{',
    'vec3 reflection = normalize(reflect(-sunDirection, surfaceNormal));',
    'float direction = max(0.0, dot(eyeDirection, reflection));',
    'specularColor += pow(direction, shiny) * sunColor * spec;',
    'diffuseColor += max(dot(sunDirection, surfaceNormal), 0.0) * sunColor * diffuse;',
    '}',

    'vec3 getNoise(in vec2 uv)',
    '{',
    'vec2 uv0 = uv / (103.0 * noiseScale) + vec2(time / 17.0, time / 29.0);',
    'vec2 uv1 = uv / (107.0 * noiseScale) - vec2(time / -19.0, time / 31.0);',
    'vec2 uv2 = uv / (vec2(8907.0, 9803.0) * noiseScale) + vec2(time / 101.0, time /   97.0);',
    'vec2 uv3 = uv / (vec2(1091.0, 1027.0) * noiseScale) - vec2(time / 109.0, time / -113.0);',
    'vec4 noise = texture2D(normalSampler, uv0) +',
    'texture2D(normalSampler, uv1) +',
    'texture2D(normalSampler, uv2) +',
    'texture2D(normalSampler, uv3);',
    'return noise.xyz * 0.5 - 1.0;',
    '}',

    THREE.ShaderChunk.common,
    THREE.ShaderChunk.fog_pars_fragment,

    'void main()',
    '{',
    'vec3 worldToEye = eye - worldPosition;',
    'vec3 eyeDirection = normalize(worldToEye);',

    // Get noise based on the 3d position
    'vec3 noise = getNoise(modelPosition.xy * 1.0);',
    'vec3 distordCoord = noise.x * surfaceX + noise.y * surfaceY;',
    'vec3 distordNormal = distordCoord + surfaceZ;',

    // Revert normal if the eye is bellow the mesh
    'if(dot(eyeDirection, surfaceZ) < 0.0)',
    'distordNormal = distordNormal * -1.0;',

    // Compute diffuse and specular light (use normal and eye direction)
    'vec3 diffuseLight = vec3(0.0);',
    'vec3 specularLight = vec3(0.0);',
    'sunLight(distordNormal, eyeDirection, 100.0, 2.0, 0.5, diffuseLight, specularLight);',

    // Compute final 3d distortion, and project it to get the mirror sampling
    'float distance = length(worldToEye);',
    'vec2 distortion = distordCoord.xy * distortionScale * sqrt(distance) * 0.07;',
    ' vec3 mirrorDistord = mirrorCoord.xyz + vec3(distortion.x, distortion.y, 1.0);',
    ' vec3 reflectionSample = texture2DProj(mirrorSampler, mirrorDistord).xyz;',

    // Compute other parameters as the reflectance and the water appareance
    'float theta = max(dot(eyeDirection, distordNormal), 0.0);',
    'float reflectance = 0.3 + (1.0 - 0.3) * pow((1.0 - theta), 3.0);',
    'vec3 scatter = max(0.0, dot(distordNormal, eyeDirection)) * waterColor;',

    // Compute final pixel color
    'vec3 albedo = mix(sunColor * diffuseLight * 0.3 + scatter, (vec3(0.1) + reflectionSample * 0.9 + reflectionSample * specularLight), reflectance);',

    ' vec3 outgoingLight = albedo;',
    THREE.ShaderChunk.fog_fragment,

    ' gl_FragColor = vec4( outgoingLight, alpha );',
    '}'
  ].join('\n')
}

export default shader

还能做哪些优化

  1. 除了实现表面波澜的效果,有些场景下还需要镜面反射来进一步增强水体的真实感,这里涉及到WebGLRenderer 多次渲染的问题,需要拿到第一遍渲染的模型用于制作倒影,如图所示。

Honeycam_2022-12-04_13-57-27.gif

2.关于着色器这块内容也比较丰富,感兴趣的可以自行搜GLSL看一些入门教程。

这里顺便安利一下shadertoyc.om, 上面汇集了世界上开发者提供的各种shader。

Honeycam_2022-12-04_11-26-38.gif

海洋示例地址

Honeycam_2022-12-04_11-47-16.gif

湖泊示例地址

参考链接

全球着色器展示库

shadertoy.com

在线编辑geoJSON数据

https://www.strerr.com/geojson/geojson.html#

© 版权声明
THE END
喜欢就支持一下吧
点赞6 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容