前言
有一天,我想要在指定的行政区域边界建立一道支持渐变和动画的酷炫边界墙,于是查了一下高德地图开发文档,开发文档说目前咱没有那玩意儿,那只好自己写一个,为避免走太多弯路,在开发之前还是网上搜了一通看看各种思路。需求很简单,墙体高度可配置、墙体的外观可指定、墙体动画可开关。
最终做出来的效果是这样
下图为现有的API 高德地图1.4 new AMap.Object3D.Wall能提供的效果,相信2.0版本后续更新应该也会把这块功能实现了。
实现思路
还是要使用可以支持three.js的GLCustomLayer,更加自由灵活地做出可视化效果。实现步骤如下:
-
获取区域边界的多边形的所有端点坐标,以所有端点坐标(x,y,z)和端点正上方距离为h的点(x,y,h)为端点建立垂直墙体A
-
给垂直墙A赋予一层材质MTA,这层材质是静态的图片;
-
拷贝垂直墙体A,在相同的位置添加一层垂直墙体B,并给B赋予一层材质MTB,MTB在垂直方面的展示策略为不断重复;为了方便查看,这里用红色材质区分一下。
-
在浏览器逐帧切换时,改变MTB的垂直偏移,使其产生平滑的上下偏移效果
代码实现
- 获取区域边界的多边形的所有端点坐标,以所有端点坐标(x,y,z)和端点正上方距离为h的点(x,y,h)为端点建立垂直墙体A
/**
* 创建范围墙
* @param arr {Array} 范围路径, 可以是多个边界数组
* 比如 [ [[x1,y1],[x2,y2]], [[x3,y3],[x4,y4],[x5,y5]]
*/
createWall () {
const { scene } = this
let faceList = []
let faceVertexUvs = []
// 合并多个闭合范围
for (let i = 0; i < this.#paths.length; i++) {
const { face, uvs } = this.generateVecData(this.#paths[i])
faceList = [...faceList, ...face]
faceVertexUvs = [...faceVertexUvs, ...uvs]
}
// 垂直墙体A
const geometry = new THREE.BufferGeometry()
geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(faceList), 3))
geometry.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(faceVertexUvs), 2))
}
/**
* 创建一个闭合范围的模型数据
* @param res {Object} 包含面的顶点数据face,UV面的顶点数据uvs
*/
generateVecData (arr) {
const vec3List = [] // 顶点数组
let faceList = [] // 三角面数组
let faceVertexUvs = [] // 面的UV层队列,用于纹理和几何信息映射
//UV面的坐标声明
const t0 = [0, 0]
const t1 = [1, 0]
const t2 = [1, 1]
const t3 = [0, 1]
for (let i = 0; i < arr.length; i++) {
const [x1, y1] = arr[i]
vec3List.push([x1, y1, 0])
vec3List.push([x1, y1, this.#height])
}
for (let i = 0; i < vec3List.length - 2; i++) {
if (i % 2 === 0) {
// 下三角顶点
faceList = [...faceList, ...vec3List[i], ...vec3List[i + 2], ...vec3List[i + 1]]
// 下三角UV面
faceVertexUvs = [...faceVertexUvs, ...t0, ...t1, ...t3]
} else {
// 上三角顶点
faceList = [...faceList, ...vec3List[i], ...vec3List[i + 1], ...vec3List[i + 2]]
// 上三角UV面
faceVertexUvs = [...faceVertexUvs, ...t3, ...t1, ...t2]
}
}
return {
face: faceList,
uvs: faceVertexUvs
}
}
这里可能有必要多解释一下创建方法,创建墙体使用的类是BufferGeometry,需要我们自己声明这个几何体的面是由哪些顶点按照什么顺序构成的。
1.1. 众所周知在webGL里构成几何体的最小面单位是三角形,因此需要把组成每个三角形的所有顶点按顺序排列成一个数组,下面以一个简单的墙体为例。
如图所示,以0-1-2 和 1-2-3为顺序创建两个三角形,那么在设置几何体的position属性时应该提供这样一个数据 ,并且告诉面构造方法,每取其中3个值为一个顶点坐标。
faceList = [
x0,y0,0,
x1,y1,h,
x1,y1,0,
x0,y0,h,
x1,y1,0,
x1,y1,h
] //这个数组包含了两个三角形
geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(faceList), 3))
1.2. 除了声明构成面的所有端点,还需要告诉构造方法,应该按照哪些顺序给面铺设材质,因此还需要生成一个UV面数组声明面和点的映射关系。还是个单个简单墙体为例,以整个平面左下角为原点(0,0),建立起一个坐标。
UV数组中端点的排列顺序与上文顶点数据排序一致即可,不同的是这里由2个值构成一个点。
faceVertexUvs = [
0,0
0,1,
1,0,
0,1,
1,0,
1,1
] //这个数组包含了两个三角形
geometry.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(faceVertexUvs), 2))
- 给垂直墙A赋予一层材质MTA,这层材质是静态的图片。
const material1 = new THREE.MeshBasicMaterial({
color: this.#color,
side: THREE.DoubleSide,
transparent: true,
depthWrite: false,
alphaMap: new THREE.TextureLoader().load('./static/texture/texture_1.png')
})
const mesh1 = new THREE.Mesh(geometry, material1)
scene.add(mesh1)
这一步就很简单了,实例化一个基础材质MeshBasicMaterial即可,为了让墙体呈现半透明效果,这里用了一张仅有灰度的图片作为材质的alpha透明贴图。在灰度通道里,纯黑色为不显示,纯白色为显示,中间过渡的灰色会半透明显示。材质贴图的默认铺设方式是拉伸,即它会自动拉伸自己铺满整个平面,这里按默认就行。
- 拷贝垂直墙体A,在相同的位置添加一层垂直墙体B,并给B赋予一层材质MTB,MTB在垂直方面的展示策略为不断重复;为了方便查看,这里用红色材质区分一下。
const geometry2 = geometry.clone()
this.#texture = this.generateTexture(128, this.#color)
this.#texture.wrapS = THREE.RepeatWrapping // 水平重复平铺
this.#texture.wrapT = THREE.RepeatWrapping // 垂直重复平铺
const material2 = new THREE.MeshBasicMaterial({
side: THREE.DoubleSide,
transparent: true,
depthWrite: false,
map: this.#texture
})
const mesh2 = new THREE.Mesh(geometry2, material2)
scene.add(mesh2)
这里需要重点关注的是generateTexture方法,如果是简单实现效果,其实如步骤2一样使用图片做贴图材质即可,但我们这边的需求是“颜色可配置” ,因为要求根据传入的颜色动态生成材质,如图所示,墙体可能是红色、青色、紫色等等。
这里的开发思路是用canvas绘制一个透明渐变的矩形,然后把canvas作为贴图创建材质即可。
generateTexture(size = 64, color ="#ff0000"){
let canvas = document.createElement('canvas')
canvas.width = size
canvas.height = size
let ctx = canvas.getContext('2d')
let linearGradient = ctx.createLinearGradient(0,0,0,size)
linearGradient.addColorStop(0.2, hexToRgba(color, 0.0)) //将16进制写法转换从rgba写法
linearGradient.addColorStop(0.8, hexToRgba(color, 0.5))
linearGradient.addColorStop(1.0, hexToRgba(color, 1.0))
ctx.fillStyle = linearGradient
ctx.fillRect(0,0, size, size)
let texture = new THREE.Texture(canvas)
texture.needsUpdate = true //必须
return texture
}
- 在浏览器逐帧切换时,改变MTB的垂直偏移,使其产生平滑的上下偏移效果。基础工作做完了,动态效果就很简单,在每一次requestAnimationFrame时 更新纹理垂直偏移量就可以了。
// 更新纹理偏移量
update () {
if (this.#texture) {
this.#texture.offset.set(0, this.#texture_offset)
}
}
还能做哪些优化?
减少模型
上文的实现思路讲到我们实现墙体动画效果,用了常规垂直面A和动态垂直面B两个几何模型,事实上是可以通过自定义着色器的方式将常规材质和动态材质合并为一个材质放到一个垂直面上的,这样的话可以减少一个带材质的垂直面墙体模型的开销。 前提是要懂得着色器Shader的相关知识,能够使用GLSL语言编写实现,这个在做完后面的需求 建筑体动态的材质的时候应该可以做下分享。
处理窄面
当两个相邻坐标点太过靠近导致垂直墙体过窄时,渲染出来的墙体会因贴图的横向压缩,看起来颜色有点浓重,但其实我觉得这样还挺好看的(doge)。
更多样效果
墙体的动效其实还是可以花点心思做出其他效果,最简单的实现方式还是更换透明贴图,但用贴图的方式会因为每个面宽度差异太大而造成窄面问题不好控制,目前还没想好怎么处理这个问题。