You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);
}

问题根源与修复方案

问题根源

  1. 相机类型不匹配:TEST 1中使用THREE.Camera()基础相机,而three-loader-3dtiles需要PerspectiveCamera的视锥体参数(如fov、aspect)来计算瓦片可见性,基础相机缺少这些关键属性,导致瓦片加载逻辑无法判断哪些瓦片需要渲染。
  2. 3D瓦片未应用地理坐标变换:Cesium 3D瓦片使用真实世界坐标,而MapLibre自定义图层要求使用墨卡托坐标空间,TEST 1仅对GLTF模型应用了坐标变换,未处理3D瓦片的坐标转换,导致瓦片不在相机可视范围内。
  3. 相机世界矩阵未更新: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 21:47:05