实现3D内部渐变的行政区效果
例子里我们用到了一个geojson计算bbox的库geojson-bbox
准备数据
从 datav 地图小工具 这里下载对应的行政区的geojson数据,自己的项目里的数据从哪里下载还是自己生产的有你的项目决定,这里我只是测试使用,故从datav 地图小工具下载了北京的数据
WARNING
地图数据是个很敏感的东西,尤其是国家的行政边界,项目里使用请遵循对应的法律政策,我这里只是测试使用
准备行政区渐变的纹理图片
这里需要用到一个小工具 GeoJSON生成纹理图片工具
上传你的行政区GeoJSON数据
将纹理纹理图片的颜色调成这样子的
- 外部白色,内部黑色,透明度请根据自己的需要调整
- 这里图片仅仅作为纹理使用,行政区的颜色我们将交给材质的颜色控制
加载行政区的数据并构造这个平面
js
var planeMaterial = new THREE.MeshLambertMaterial({
color,
transparent: true,
opacity: 0.8,
side: 0,
});
function addAreaPlane() {
fetch("../assets/data/beijing.geojson")
.then((res) => res.json())
.then((geojson) => {
// const extent = bbox(geojson);
// const plane = new AreaPlane(extent, { altitude: 0 }, planeMaterial, threeLayer);
const extrudePolygons = geojson.features.map((feature) => {
return threeLayer.toExtrudePolygon(
feature,
{ height: 1 },
planeMaterial,
);
});
resetTopUV(extrudePolygons);
const texture = new THREE.TextureLoader().load(
"./../assets/image/beijing.png",
(texture) => {
planeMaterial.map = texture;
planeMaterial.needsUpdate = true;
threeLayer.addMesh(extrudePolygons);
// threeLayer.addMesh(plane);
},
);
texture.needsUpdate = true; //使用贴图时进行更新
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
// texture.repeat.set(0.002, 0.002);
texture.repeat.set(1, 1);
});
}
重置拔高面的UV
这里默认你懂纹理的uv
的知识点,如果你不懂请查阅相关的资料
默认情况下行政区拔高的构造的几何体(Geometry)的纹理坐标值(UV),不是按照这种简单的按照行政区面积比列分布的, 所以需要我们对Geometry的顶面的纹理坐标值进行重计算
具体方法:
- 求出所有行政区的边界的包围盒
- 计算每个子区域的坐标点的值换算成在这个包围盒内的占比即可(就是求UV的值,也是对整个纹理图片的划分过程)
- 更新每个子区域的
Geometry
的uv值就可以了
WARNING
注意只需要更新下顶面的uv坐标,判断一个顶点的坐标是否是顶面的可以根据顶点的z
值来判断
js
function resetTopUV(extrudePolygons) {
// console.log(geometries);
//计算所有区域的总的包围盒
let minx = Infinity,
miny = Infinity,
maxx = -Infinity,
maxy = -Infinity,
maxZ = -Infinity;
extrudePolygons.forEach((extrudePolygon) => {
const geometry = extrudePolygon.getObject3d().geometry;
const center = extrudePolygon.getObject3d().position;
const px = center.x,
py = center.y;
const position = geometry.attributes.position.array;
for (let i = 0, len = position.length; i < len; i += 3) {
const x = position[i] + px,
y = position[i + 1] + py,
z = position[i + 2];
minx = Math.min(minx, x);
miny = Math.min(miny, y);
maxx = Math.max(maxx, x);
maxy = Math.max(maxy, y);
maxZ = Math.max(maxZ, z);
}
});
console.log(minx, miny, maxx, maxy);
//计算每个子区域的每个轮廓坐标点的在这个包围盒的百分比
const dx = maxx - minx,
dy = maxy - miny;
extrudePolygons.forEach((extrudePolygon) => {
const geometry = extrudePolygon.getObject3d().geometry;
const position = geometry.attributes.position.array;
const center = extrudePolygon.getObject3d().position;
const px = center.x,
py = center.y;
const uv = geometry.attributes.uv.array;
let idx = 0;
for (let i = 0, len = position.length; i < len; i += 3) {
const x = position[i] + px,
y = position[i + 1] + py,
z = position[i + 2];
if (z === maxZ) {
const u = (x - minx) / dx;
const v = (y - miny) / dy;
const index = idx * 2;
uv[index] = u;
uv[index + 1] = v;
}
idx++;
}
});
}
注意材质需要开启透明选项
构造周边的围墙
js
var material = new THREE.MeshLambertMaterial({ color, transparent: false });
function flatPolygon2Lines(geojson) {
const results = {
type: "FeatureCollection",
features: [],
};
geojson.features.forEach((f) => {
const { geometry, properties } = f;
const { coordinates, type } = geometry;
let polygons = [];
if (type.includes("Multi")) {
polygons = coordinates;
} else {
polygons.push(coordinates);
}
polygons.forEach((p) => {
results.features.push({
type: "Feature",
geometry: {
type: "MultiLineString",
coordinates: p,
},
properties,
});
});
});
return results;
}
function addPolygons() {
fetch("../assets/data/beijing.geojson")
.then((res) => res.json())
.then((geojson) => {
const geojson1 = flatPolygon2Lines(geojson);
const lines = maptalks.GeoJSON.toGeometry(geojson1);
lines.forEach((line) => {
const extrudeLine = threeLayer.toExtrudeLine(
line,
{ height, altitude: -height, topColor: "#fff" },
material,
);
threeLayer.addMesh(extrudeLine);
});
addOutLines();
});
}
注意把图形的海拔高度设置为负的目的是不要抬高海平面,方便其他的业务图层数据加到地图,否则会要求其他图层也要设置海拔数据,导致业务逻辑变复杂了,这个样子最简单和不容易出错
添加区域的轮廓线
js
function addOutLines(polygons) {
fetch("../assets/data/beijingarea.json")
.then((res) => res.json())
.then((geojson) => {
const polygons = maptalks.GeoJSON.toGeometry(geojson);
polygons.forEach((polygon) => {
polygon.setSymbol({
polygonOpacity: 0,
lineWidth: 1,
lineColor,
});
});
layer.addGeometry(polygons);
});
}
TIP
代码里我们把底图(TileLayer)也设置为负值的海拔了,目的也是方便其他的业务图层数据加到地图
js
var map = new maptalks.Map("map", {
center: [116.63924403, 40.18304877],
zoom: 9,
pitch: 60,
baseLayer: new maptalks.TileLayer("tile", {
altitude: -height,
urlTemplate:
"https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
subdomains: ["a", "b", "c", "d"],
attribution:
'© <a href="http://osm.org">OpenStreetMap</a> contributors, © <a href="https://carto.com/">CARTO</a>',
}),
});