MapLibre GL JS集成ThreeLoader3DTiles加载Cesium 3D瓦片失败求助
问题:Cesium 3D瓦片集成MapLibre GL JS自定义图层后无法加载实际瓦片
问题描述
现有独立加载Cesium 3D瓦片的代码(TEST 2)可正常运行,但将逻辑集成到MapLibre GL JS自定义图层时(TEST 1),仅能添加瓦片根对象到场景,无法加载实际瓦片;同一场景中GLTF风车模型可正常显示。推测是相机、渲染器或场景配置问题导致three-loader-3dtiles无法识别需加载的瓦片。
正常加载Cesium 3D瓦片的代码(TEST 2)
import { useEffect, useRef, useState } from 'react'; import * as THREE from 'three'; import { Loader3DTiles } from 'three-loader-3dtiles'; import { Scene, PerspectiveCamera, WebGLRenderer, Clock } from 'three' import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js' import maplibregl, { loadMapTilerMaps, modelOrigin, modelTransform } from 'maplibre-gl'; import { BingMapsProvider, MapView, UnitsUtils } from 'geo-three' import { Vector3 } from 'three'; let rendered = false; export default function Demo(props) { const ref = useRef(); useEffect(() => { const f = async () => { if (rendered) return; console.log('LOAD') rendered = true; await initTestOGCTiles(ref) } f(); }) return <div id="map" style={{ height: window.innerHeight }} ref={ref}></div> } // TEST 2 - only shows (cesium) tiles for proof of concept async function initTestOGCTiles(ref) { const scene = new Scene() const camera = new PerspectiveCamera() const renderer = new THREE.WebGLRenderer({ antialias: true }); const clock = new Clock() renderer.setSize(window.innerWidth, window.innerHeight); ref.current.appendChild(renderer.domElement); let tilesRuntime = await loadOGCTiles(scene, renderer); function render() { const dt = clock.getDelta() if (tilesRuntime) tilesRuntime.update(dt, renderer, camera) renderer.render(scene, camera) window.requestAnimationFrame(render) } render() } async function loadOGCTiles(scene, renderer) { console.log("RENDERER", renderer) const result = await Loader3DTiles.load({ url: 'my 3d tiles url', renderer: renderer, options: { dracoDecoderPath: 'https://cdn.jsdelivr.net/npm/three@0.137.0/examples/js/libs/draco', basisTranscoderPath: 'https://cdn.jsdelivr.net/npm/three@0.137.0/examples/js/libs/basis', } }) const { model, runtime } = result const b3dmTiles = model; b3dmTiles.name = "b3dmTiles" runtime.setDebug(true) scene.add(b3dmTiles) console.log("MODEL:", b3dmTiles) console.log(runtime.getStats()) return runtime }
集成MapLibre GL JS的异常代码(TEST 1)
import { useEffect, useRef, useState } from 'react'; import * as THREE from 'three'; import { Loader3DTiles } from 'three-loader-3dtiles'; import { Scene, PerspectiveCamera, WebGLRenderer, Clock } from 'three' import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js' import maplibregl, { loadMapTilerMaps, modelOrigin, modelTransform } from 'maplibre-gl'; import { BingMapsProvider, MapView, UnitsUtils } from 'geo-three' import { Vector3 } from 'three'; let rendered = false; export default function Demo(props) { const ref = useRef(); useEffect(() => { const f = async () => { if (rendered) return; console.log('LOAD') rendered = true; initMaps(ref) } f(); }) return <div id="map" style={{ height: window.innerHeight }} ref={ref}></div> } // TEST 1 - supposed to show maptiler terrain, with bing maps texture, with (cesium) 3D tiles on top // FIXME: cesium tiles will not show function initMaps(ref) { var modelOrigin = [100, -23]; var modelAltitude = 0; var modelRotate = [Math.PI / 2, 0, 0]; var map; function loadMap(baseMapsStyle) { console.log("LOAD MAP") map = new maplibregl.Map({ container: 'map', style: baseMapsStyle, zoom: 12, center: modelOrigin, pitch: 52, }).on('style.load', function () { map.addLayer(customLayer); }); } initBingMaps(loadMap) var modelAsMercatorCoordinate = maplibregl.MercatorCoordinate.fromLngLat(modelOrigin, modelAltitude); var modelTransform = { translateX: modelAsMercatorCoordinate.x, translateY: modelAsMercatorCoordinate.y, translateZ: modelAsMercatorCoordinate.z, rotateX: modelRotate[0], rotateY: modelRotate[1], rotateZ: modelRotate[2], scale: modelAsMercatorCoordinate.meterInMercatorCoordinateUnits() }; const customLayer = { id: '3d-model', type: 'custom', renderingMode: '3d', onAdd: async function (map, gl) { const camera = new THREE.Camera(); camera.position.add(new Vector3(0, 0, 0)) const scene = new THREE.Scene(); this.camera = camera; this.scene = scene; this.map = map; this.clock = new Clock(); this.renderer = new THREE.WebGLRenderer({ canvas: map.getCanvas(), context: gl, antialias: true, }); var directionalLight = new THREE.DirectionalLight(0xffffff); directionalLight.position.set(0, -70, 100).normalize(); this.scene.add(directionalLight); var directionalLight2 = new THREE.DirectionalLight(0xffffff); directionalLight2.position.set(0, 70, 100).normalize(); this.scene.add(directionalLight2); var loader = new GLTFLoader(); loader.load( 'https://maplibre.org/maplibre-gl-js-docs/assets/34M_17/34M_17.gltf', function (gltf) { gltf.scene.name = "gltf scene" gltf.scene.translateY(10); this.scene.add(gltf.scene); }.bind(this) ); this.tilesRuntime = await loadOGCTiles(this.scene, this.renderer); this.renderer.autoClear = false; }, render: function (gl, matrix) { this.camera.projectionMatrix = calculateProjectionMatrix(matrix, modelTransform) const dt = this.clock.getDelta() if (this.tilesRuntime) { this.tilesRuntime.update(dt, this.renderer, this.camera) if (this.scene.getObjectByName("b3dmTiles").children.length > 0) { console.log("TILES LOADED", this.scene.children) } else { console.log("NO TILES LOADED", this.scene.children) } } this.map.triggerRepaint(); this.renderer.render(this.scene, this.camera); this.renderer.resetState(); } }; } async function loadOGCTiles(scene, renderer) { console.log("RENDERER", renderer) const result = await Loader3DTiles.load({ url: 'my 3d tile', renderer: renderer, options: { dracoDecoderPath: 'https://cdn.jsdelivr.net/npm/three@0.137.0/examples/js/libs/draco', basisTranscoderPath: 'https://cdn.jsdelivr.net/npm/three@0.137.0/examples/js/libs/basis', } }) const { model, runtime } = result const b3dmTiles = model; b3dmTiles.name = "b3dmTiles" runtime.setDebug(true) scene.add(b3dmTiles) console.log("MODEL:", b3dmTiles) console.log(runtime.getStats()) return runtime } function initBingMaps(loadMap) { var BingMapsKey = 'my key'; var BingMapsImagerySet = 'Aerial'; var BingMapsImageryMetadataUrl = `https://dev.virtualearth.net/REST/V1/Imagery/Metadata/${BingMapsImagerySet}?output=json&include=ImageryProviders&key=${BingMapsKey}`; fetch(BingMapsImageryMetadataUrl).then(r => r.json()).then(r => { var tileInfo = r.resourceSets[0].resources[0]; var tileUrls = []; tileInfo.imageUrlSubdomains.forEach(sub => { tileUrls.push(tileInfo.imageUrl.replace('{subdomain}', sub)); }); var attributions = tileInfo.imageryProviders.map(p => p.attribution).join(', '); var style = { 'version': 8, 'sources': { 'bing-maps-raster-tiles': { 'type': 'raster', 'tiles': tileUrls, 'tileSize': tileInfo.imageWidth, 'attribution': attributions, 'minzoom': 1, 'maxzoom': 20 } }, 'layers': [ { 'id': 'bing-maps-tiles', 'type': 'raster', 'source': 'bing-maps-raster-tiles', 'minzoom': 0, 'maxzoom': 23 } ] }; style.sources.terrainSource = { type: 'raster-dem', url: 'https://api.maptiler.com/tiles/terrain-rgb-v2/tiles.json?key=my key', tileSize: 256 }; style.terrain = { source: 'terrainSource', exaggeration: 1 }; loadMap(style); }); } function calculateProjectionMatrix(matrix, modelTransform) { var rotationX = new THREE.Matrix4().makeRotationAxis(new THREE.Vector3(1, 0, 0), modelTransform.rotateX); var rotationY = new THREE.Matrix4().makeRotationAxis(new THREE.Vector3(0, 1, 0), modelTransform.rotateY); var rotationZ = new THREE.Matrix4().makeRotationAxis(new THREE.Vector3(0, 0, 1), modelTransform.rotateZ); var m = new THREE.Matrix4().fromArray(matrix); var l = new THREE.Matrix4() .makeTranslation(modelTransform.translateX, modelTransform.translateY, modelTransform.translateZ) .scale(new THREE.Vector3(modelTransform.scale, -modelTransform.scale, modelTransform.scale)) .multiply(rotationX) .multiply(rotationY) .multiply(rotationZ); return m.multiply(l); }
问题根源与修复方案
问题根源
- 相机类型不匹配:TEST 1中使用
THREE.Camera()基础相机,而three-loader-3dtiles需要PerspectiveCamera的视锥体参数(如fov、aspect)来计算瓦片可见性,基础相机缺少这些关键属性,导致瓦片加载逻辑无法判断哪些瓦片需要渲染。 - 3D瓦片未应用地理坐标变换:Cesium 3D瓦片使用真实世界坐标,而MapLibre自定义图层要求使用墨卡托坐标空间,TEST 1仅对GLTF模型应用了坐标变换,未处理3D瓦片的坐标转换,导致瓦片不在相机可视范围内。
- 相机世界矩阵未更新:MapLibre传递的投影矩阵正确,但相机的
matrixWorld未更新,瓦片加载逻辑无法获取相机实际位置,无法触发瓦片加载。
修复步骤
1. 替换相机为PerspectiveCamera
在customLayer.onAdd中,将基础相机替换为透视相机并设置正确参数:
const camera = new THREE.PerspectiveCamera( map.getCamera().fov, window.innerWidth / window.innerHeight, 0.1, 10000 );
2. 给3D瓦片应用墨卡托坐标变换
修改loadOGCTiles函数,传入modelTransform并对瓦片应用坐标转换:
async function loadOGCTiles(scene, renderer, modelTransform) { // ...原代码 const b3dmTiles = model; b3dmTiles.name = "b3dmTiles"; // 应用墨卡托坐标变换 const transformMatrix = new THREE.Matrix4() .makeTranslation( modelTransform.translateX, modelTransform.translateY, modelTransform.translateZ ) .scale( new THREE.Vector3( modelTransform.scale, -modelTransform.scale, modelTransform.scale ) ) .multiply(new THREE.Matrix4().makeRotationAxis(new THREE.Vector3(1,0,0), modelTransform.rotateX)) .multiply(new THREE.Matrix4().makeRotationAxis(new THREE.Vector3(0,1,0), modelTransform.rotateY)) .multiply(new THREE.Matrix4().makeRotationAxis(new THREE.Vector3(0,0,1), modelTransform.rotateZ)); b3dmTiles.applyMatrix4(transformMatrix); scene.add(b3dmTiles); // ...原代码 }
在customLayer.onAdd中调用时传入modelTransform:
this.tilesRuntime = await loadOGCTiles(this.scene, this.renderer, modelTransform);
3. 更新相机世界矩阵
在customLayer.render方法中,补充相机矩阵更新逻辑:
render: function (gl, matrix) { this.camera.projectionMatrix = calculateProjectionMatrix(matrix, modelTransform); // 更新相机世界矩阵及逆矩阵 this.camera.updateMatrixWorld(true); this.camera.matrixWorldInverse.getInverse(this.camera.matrixWorld); const dt = this.clock.getDelta(); if (this.tilesRuntime) { this.tilesRuntime.update(dt, this.renderer, this.camera); // ...原日志代码 } this.map.triggerRepaint(); this.renderer.render(this.scene, this.camera); this.renderer.resetState(); }
4. 调整渲染器自动清除设置
在渲染器初始化时直接设置autoClear,避免后续操作干扰:
this.renderer = new THREE.WebGLRenderer({ canvas: map.getCanvas(), context: gl, antialias: true, autoClear: false // 直接在此设置 });
内容的提问来源于stack exchange,提问作者John
相关产品推荐
相关产品推荐

