在地形上使用three.js添加3D模型
使用自定义样式图层和three.js将3D模型添加到带有地形的地图上;
<!DOCTYPE html>
<html lang="en">
<head>
<title>在地形上使用three.js添加3D模型</title>
<meta property="og:description" content="使用自定义样式图层和three.js将3D模型添加到带有地形的地图上" />
<meta charset='utf-8'>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel='stylesheet' href='https://unpkg.com/maplibre-gl@5.5.0/dist/maplibre-gl.css' />
<script src='https://unpkg.com/maplibre-gl@5.5.0/dist/maplibre-gl.js'></script>
<style>
body {
margin: 0;
padding: 0;
}
html,
body,
#map {
height: 100%;
}
</style>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.169.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.169.0/examples/jsm/"
}
}
</script>
</head>
<body>
<div id="map"></div>
<script type="module">
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
/**
* 目标:
* 给定两个已知的世界位置 `model1Location` 和 `model2Location`,
* 在这些位置放置两个three.js对象,并位于地形的适当高度。
*/
async function main() {
const map = new maplibregl.Map({
container: 'map',
center: [11.5257, 47.668],
zoom: 16.27,
pitch: 60,
bearing: -28.5,
canvasContextAttributes: {antialias: true},
style: {
version: 8,
layers: [
{
id: 'baseColor', // 隐藏地形瓦片的边缘,这些边缘有"墙"向下延伸到0。
type: 'background',
paint: {
'background-color': '#fff',
'background-opacity': 1.0,
},
}, {
id: 'hills',
type: 'hillshade',
source: 'hillshadeSource',
layout: {visibility: 'visible'},
paint: {'hillshade-shadow-color': '#473B24'}
}
],
terrain: {
source: 'terrainSource',
exaggeration: 1,
},
sources: {
terrainSource: {
type: 'raster-dem',
url: 'https://demotiles.maplibre.org/terrain-tiles/tiles.json',
tileSize: 256
},
hillshadeSource: {
type: 'raster-dem',
url: 'https://demotiles.maplibre.org/terrain-tiles/tiles.json',
tileSize: 256
}
},
}
});
/*
* 帮助函数,用于从墨卡托坐标获取threejs场景坐标。
* 这只是一个快速且简单的解决方案 - 如果点相距较远,它将无法正常工作,
* 因为靠近北极的一米覆盖的墨卡托单位比赤道附近的一米多。
*/
function calculateDistanceMercatorToMeters(from, to) {
const mercatorPerMeter = from.meterInMercatorCoordinateUnits();
// 墨卡托 x: 0=西, 1=东
const dEast = to.x - from.x;
const dEastMeter = dEast / mercatorPerMeter;
// 墨卡托 y: 0=北, 1=南
const dNorth = from.y - to.y;
const dNorthMeter = dNorth / mercatorPerMeter;
return {dEastMeter, dNorthMeter};
}
async function loadModel() {
const loader = new GLTFLoader();
const gltf = await loader.loadAsync('https://maplibre.org/maplibre-gl-js/docs/assets/34M_17/34M_17.gltf');
const model = gltf.scene;
return model;
}
// 已知位置。一旦地形加载完成,我们将推断这些位置的海拔高度。
const sceneOrigin = new maplibregl.LngLat(11.5255, 47.6677);
const model1Location = new maplibregl.LngLat(11.527, 47.6678);
const model2Location = new maplibregl.LngLat(11.5249, 47.6676);
// 3D模型的自定义图层配置,实现 `CustomLayerInterface`。
const customLayer = {
id: '3d-model',
type: 'custom',
renderingMode: '3d',
onAdd(map, gl) {
/**
* 设置three.js场景。
* 我们放置model1和model2的方式使整个场景能够适应地形。
*/
this.camera = new THREE.Camera();
this.scene = new THREE.Scene();
// 在threejs中,y指向上方 - 我们旋转场景使其y指向与maplibre的上方对齐。
this.scene.rotateX(Math.PI / 2);
// 在threejs中,z指向观察者 - 我们镜像它使z指向maplibre的北方。
this.scene.scale.multiply(new THREE.Vector3(1, 1, -1));
// 现在我们有一个场景,其坐标系为(x=东, y=上, z=北)
const light = new THREE.DirectionalLight(0xffffff);
// 接近中午时分 - 光线从东南方向照射。
light.position.set(50, 70, -30).normalize();
this.scene.add(light);
// 坐标轴辅助工具,显示threejs场景的方向。
const axesHelper = new THREE.AxesHelper(60);
this.scene.add(axesHelper);
// 获取模型海拔高度(米,高于海平面)
const sceneElevation = map.queryTerrainElevation(sceneOrigin) || 0;
const model1Elevation = map.queryTerrainElevation(model1Location) || 0;
const model2Elevation = map.queryTerrainElevation(model2Location) || 0;
const model1up = model1Elevation - sceneElevation;
const model2up = model2Elevation - sceneElevation;
// 获取模型相对于场景原点的x和y(单位为米)。
const sceneOriginMercator = maplibregl.MercatorCoordinate.fromLngLat(sceneOrigin);
const model1Mercator = maplibregl.MercatorCoordinate.fromLngLat(model1Location);
const model2Mercator = maplibregl.MercatorCoordinate.fromLngLat(model2Location);
const {dEastMeter: model1east, dNorthMeter: model1north} = calculateDistanceMercatorToMeters(sceneOriginMercator, model1Mercator);
const {dEastMeter: model2east, dNorthMeter: model2north} = calculateDistanceMercatorToMeters(sceneOriginMercator, model2Mercator);
model1.position.set(model1east, model1up, model1north);
model2.position.set(model2east, model2up, model2north);
this.scene.add(model1);
this.scene.add(model2);
// 使用MapLibre GL JS地图画布作为three.js的渲染目标。
this.renderer = new THREE.WebGLRenderer({
canvas: map.getCanvas(),
context: gl,
antialias: true
});
this.renderer.autoClear = false;
},
render(gl, args) {
const offsetFromCenterElevation = map.queryTerrainElevation(sceneOrigin) || 0;
const sceneOriginMercator = maplibregl.MercatorCoordinate.fromLngLat(sceneOrigin, offsetFromCenterElevation);
const sceneTransform = {
translateX: sceneOriginMercator.x,
translateY: sceneOriginMercator.y,
translateZ: sceneOriginMercator.z,
scale: sceneOriginMercator.meterInMercatorCoordinateUnits()
};
const m = new THREE.Matrix4().fromArray(args.defaultProjectionData.mainMatrix);
const l = new THREE.Matrix4()
.makeTranslation(sceneTransform.translateX, sceneTransform.translateY, sceneTransform.translateZ)
.scale(new THREE.Vector3(sceneTransform.scale, -sceneTransform.scale, sceneTransform.scale));
this.camera.projectionMatrix = m.multiply(l);
this.renderer.resetState();
this.renderer.render(this.scene, this.camera);
map.triggerRepaint();
}
};
const results = await Promise.all([map.once('load'), loadModel()]);
const model1 = results[1];
const model2 = model1.clone();
map.addLayer(customLayer);
}
main();
</script>
</body>
</html>