介绍
最近我在研究3D地图的开发,做久了慢慢觉得整个场景不够真实,究其原因是缺少一些自然的元素,于是突发奇想能否把自然四大元素整合到场景里。江海河流湖泊在地图中国占据的比例较高,就优先研究了下在高德地图中展示动态的水域效果。
需求说明
先从简单一点的需求入手。
- 划设一到多个多边形区域,实现动态水域层
- 水域需要有微风吹过波涛起伏的视觉效果,最好支持后期快速修改
- 水域层支持配置颜色、透明度、水体质感
实现思路
1.根据提供的数据,在地图上绘制多边形平面。可以考虑使用在线GeoJOSN编辑器做绘制,拿到自动生成的GeoJSON数据后根据当前使用的地图坐标系做一下转换就行了。本文需要将WGS84转为高德地图的火星坐标系。
2.给多边形赋予水域材质,这里需要用到Three.js提供的自定义着色器材质ShaderMaterial,Shader是一个glsl语言写的着色程序,运行在GPU上。简易理解就是,GPU能够同时处理显示器屏幕上所有的像素点,在显卡的加持下有着优越的性能,适合处理各种需要大量并行计算的流体、粒子等效果,原则上只要你的图形学功底够硬,就行实现任何想象到的效果。
重点来了,There.js的官方示例中恰好提供了这样的一个水体ShaderMaterial,只要稍加改造就可以拿来用了。
- 最后一步就是随帧更新材质的时间轴,让材质动起来,配合整个场景微调一下。
代码实现
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
还能做哪些优化
- 除了实现表面波澜的效果,有些场景下还需要镜面反射来进一步增强水体的真实感,这里涉及到
WebGLRenderer
多次渲染的问题,需要拿到第一遍渲染的模型用于制作倒影,如图所示。
2.关于着色器这块内容也比较丰富,感兴趣的可以自行搜GLSL看一些入门教程。
这里顺便安利一下shadertoyc.om, 上面汇集了世界上开发者提供的各种shader。
参考链接
全球着色器展示库
在线编辑geoJSON数据
https://www.strerr.com/geojson/geojson.html#
暂无评论内容