继上一期使用threebox加载geojson二维面数据,新需求是加载三维面。
因threejs的ShapeGeometry只支持x,y坐标,无z值,因此研究方向为BufferGeometry。
BufferGeometry主要通过绘制小三角形来构成所需的图形,需构建一个position数组来存储所有点的坐标,然后通过setAttribute和setIndex方法来实现BufferGeometry的绘制。
setAttribute方法用于设置顶点属性。它接受三个参数:属性名称、BufferAttribute对象和每个元素的大小。属性名称可以是位置(position)、法线(normal)、颜色(color)等。BufferAttribute对象包含实际的属性数据,可以通过将数组传递给它的构造函数来创建。每个元素的大小表示每个属性元素所占用的字节数,例如位置属性通常是3个浮点数(x、y、z),所以大小为3。
setIndex方法用于设置索引属性,用于定义构成几何体的三角形面。它接受一个BufferAttribute对象作为参数,其中包含了索引数据。索引数据是一个无符号整数数组,表示顶点的索引顺序。
根据BufferGeometry的特性,研究重点应该在如何将带有高度属性的geojson面数据划分成一个个小三角形。首先想到了turf.js的tesselate方法,代码如下:
let coordinates = [
[120.63821315568913, 31.426714841676258, 10],
[120.6383673932828, 31.42698392461608, 10],
[120.63771903773284, 31.427314807183862, 20],
[120.63724293643935, 31.427577060156693, 10],
[120.63710134038462, 31.42739036386199, 10],
[120.63758972602409, 31.427099356271654, 20],
[120.63821315568913, 31.426714841676258, 10]
]
let newArray = []
let newArray2 = []
let newArray3 = []
coordinates.forEach((v) => {
// 转成three.js的坐标
newArray.push([tb.projectToWorld([v[0], v[1], v[2]]).x, tb.projectToWorld([v[0], v[1], v[2]]).y])
// 保留原始二维坐标
newArray2.push([v[0], v[1]])
})
let polygon = turf.polygon([newArray]);
let polygon2 = turf.polygon([newArray2]);
let center = turf.center(polygon2).geometry.coordinates;// polygon的中心点
let center2 = tb.projectToWorld([center[0],center[1]]);// polygon的中心点转three.js坐标
coordinates.forEach((v) => {
// 构建position数组
newArray3.push(tb.projectToWorld([v[0], v[1], v[2]]).x - center2.x, tb.projectToWorld([v[0], v[1], v[2]]).y - center2.y, tb.projectToWorld([v[0], v[1], v[2]]).z)
})
// 使用turf.js的tesselate分割
let triangles = turf.tesselate(polygon);
const indexArray = []
triangles.features.forEach((item) => {
for (let i = 0; i < 3; i++) {
// 寻找点对应的index
let index = newArray.findIndex(v => v[0] === item.geometry.coordinates[0][i][0] && v[1] === item.geometry.coordinates[0][i][1])
indexArray.push(index)
}
})
let geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(newArray3, 3));
geometry.setIndex(indexArray);
let material = new THREE.MeshBasicMaterial({
color: 0xff00ff,
side: THREE.DoubleSide, //两面可见
}); //材质对象Material
//合成对象
let mesh = new THREE.Mesh(geometry, material); //网格模型对象Mesh
mesh.position.copy(center2)
这样加载以后存在问题,如图:
打印查看indexArray数组,发现原因是使用turf.js的tesselate构造的三角形不太正确,tesselate构造的三角形index数组为:[5, 0, 1, 2, 3, 4, 4, 5, 1, 1, 2, 4],但是正确的应该是[0,1,2,0,2,5,2,3,5,3,4,5]之类的,错误地构建了4,5,1和1,2,4两个三角形。
于是重新找分割方法,尝试使用threejs-earcut方法进行分割,代码如下:
//三维顶点数据
let arr = [
10, 10, 5,
20, 10, 10,
30, 10, 5,
30, 20, 5,
20, 20, 10,
10, 20, 5,
]
let geometry = new THREE.BufferGeometry();
let vertices = new Float32Array(arr);
// 创建属性缓冲区对象
let attribue = new THREE.BufferAttribute(vertices, 3); //3个为一组
// 设置几何体attributes属性的位置position属性
geometry.attributes.position = attribue
// 三角形顶点索引计算
let trianglesIndex = earcut(arr, null, 3);
// Uint16Array类型数组创建顶点索引数据
let indexes = new Uint16Array(trianglesIndex)
// 索引数据赋值给几何体的index属性
geometry.index = new THREE.BufferAttribute(indexes, 1); //1个为一组
// 不执行computeVertexNormals,没有顶点法向量数据
geometry.computeVertexNormals(); //不计算法线,表面比较暗,计算了比较亮,
//材质对象
var material = new THREE.MeshLambertMaterial({
color: 0x0000ff, //三角面颜色
side: THREE.DoubleSide, //两面可见
});
var mesh = new THREE.Mesh(geometry, material); //网格模型对象Mesh
let center = tb.projectToWorld([120.63773436683371, 31.427145950916476]);
mesh.position.copy(center)
构建了一个类似的形状放到指定位置,如下图所示,发现还是构建的三角形有问题,同使用turf.js的tesselate构造的三角形。
于是又重新找分割三角形的方法,找到d3.js的Delaunay三角形构建方法。代码如下:
let coordinates = [
[120.63821315568913, 31.426714841676258, 10],
[120.6383673932828, 31.42698392461608, 10],
[120.63771903773284, 31.427314807183862, 20],
[120.63724293643935, 31.427577060156693, 10],
[120.63710134038462, 31.42739036386199, 10],
[120.63758972602409, 31.427099356271654, 20],
[120.63821315568913, 31.426714841676258, 10]
]
let newArray = []
let points = []
coordinates.forEach((v) => {
newArray.push([v[0], v[1]])
})
// 创建Delaunay对象
const delaunay = d3.Delaunay.from(newArray);
// 获取三角形的索引数组
const triangles = delaunay.triangles;
let polygon = turf.polygon([newArray]);
const array2 = []
// 将三角形索引数组转化为三角形坐标数组
for (let i = 0; i < triangles.length; i += 3) {
array2.push(triangles[i], triangles[i + 1], triangles[i + 2])
}
let center = turf.center(polygon).geometry.coordinates;
let center2 = tb.projectToWorld([center[0], center[1]]);
coordinates.forEach((v) => {
points.push(center2.x - tb.projectToWorld([v[0], v[1], v[2]]).x, center2.y - tb.projectToWorld([v[0], v[1], v[2]]).y, tb.projectToWorld([v[0], v[1], v[2]]).z)
})
let geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(points, 3));
geometry.setIndex(array2);
let material2 = new THREE.MeshBasicMaterial({
color: 0xff00ff,
side: THREE.DoubleSide, //两面可见
wireframe: false
}); //材质对象Material
material2.transparent = true;
material2.opacity = 0.5
//合成对象
let mesh = new THREE.Mesh(geometry, material2); //网格模型对象Mesh
mesh.position.copy(center2)
这次能够加载出来正确的形状,但是多构建了两个面,即1,2,3和0,4,5两个面。
加载效果如下:
经测试发现,d3.js的Delaunay三角形构建方法在不同情况下构建面的形式不同,如左下图会构建六个三角形,右边会构建四个三角形。
其他数据经测试也可以使用d3.js的Delaunay三角形构建方法加载。
本次研究到此,如果有更好的方法可以解决多构建面的情况欢迎评论探讨!