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

Mapbox自定义Three.js图层3D模型经纬度定位问题求助

Mapbox + Three.js 3D模型经纬度定位问题解决

问题描述

我正在开发基于Mapbox的自定义图层,使用Three.js渲染3D模型,目标是将模型加载到地图指定经纬度位置。已成功加载单个模型,但无法将模型放置在正确的经纬度位置,核心难点是经纬度到Three.js世界坐标的投影转换出错。

我尝试使用Mapbox的MercatorCoordinate.fromLngLat函数转换经纬度,但模型位置不符合预期。我知晓该函数会将坐标转换为0-1范围,也尝试用计算的scale调整位置,但未成功。

  • 预期效果:3D模型准确渲染在指定经纬度处
  • 实际效果:模型已渲染,但位置偏差
  • 注意:更新模型参数时仅调整其在Three.js世界中的位置或旋转,不影响相机

原代码

import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { MercatorCoordinate, type Map } from 'mapbox-gl';

export type ModelOptions = {
  location: [number, number];
  rotation: [number, number, number];
  scale: number;
};

export default class Map3DLayer {
  private map: Map;
  private camera: THREE.Camera;
  private scene: THREE.Scene;
  private renderer: THREE.WebGLRenderer;
  private cameraTransform: THREE.Matrix4;
  private scale: number;

  constructor(map: Map) {
    this.map = map;

    // Build the scene
    this.camera = new THREE.Camera();
    this.scene = new THREE.Scene();

    this.renderer = new THREE.WebGLRenderer({
      canvas: map.getCanvas(),
      context: map.getCanvas().getContext('webgl') ?? undefined,
      antialias: true,
    });
    this.renderer.autoClear = false;

    const light = new THREE.AmbientLight(0xffffff, 5);
    this.scene.add(light);

    // Synchronize THREE with Mapbox
    // const center = MercatorCoordinate.fromLngLat(map.getCenter(), 0);
    const center = MercatorCoordinate.fromLngLat([0, 0], 0);
    const { x, y, z = 0 } = center;
    this.scale = center.meterInMercatorCoordinateUnits();

    const scale = new THREE.Matrix4().makeScale(this.scale, this.scale, -this.scale);
    const rotation = new THREE.Matrix4().multiplyMatrices(
      new THREE.Matrix4().makeRotationX(-0.5 * Math.PI),
      new THREE.Matrix4().makeRotationY(Math.PI),
    );

    this.cameraTransform = new THREE.Matrix4()
      .multiplyMatrices(scale, rotation)
      .setPosition(x, y, z);
  }

  loadModel(name: string, path: string, options: ModelOptions) {
    const loader = new GLTFLoader();
    return new Promise<void>((resolve, reject) => {
      loader.load(path, ({ scene }: any) => {
        scene.name = name;
        this.scene.add(scene);
        this.update(name, options);
        resolve();
      }, undefined, reject);
    });
  }

  update(name: string, options: Partial<ModelOptions>) {
    const item = this.scene.getObjectByName(name);
    if (!item) return;
    const { location, rotation, scale } = options;
    if (rotation) item.rotation.set(...rotation);
    if (scale) item.scale.set(scale, scale, scale);
    if (location) {
      const { x, y, z = 0 } = MercatorCoordinate.fromLngLat(location, 0);
      item.position.set(x, y, z);
    }
    this.renderer.state.reset();
    this.renderer.render(this.scene, this.camera);
    this.map.triggerRepaint();
  }

  render(ctx: WebGLRenderingContext, matrix: number[]) {
    this.camera.projectionMatrix = new THREE.Matrix4()
      .fromArray(matrix)
      .multiply(this.cameraTransform);
    this.renderer.resetState();
    this.renderer.render(this.scene, this.camera);
  }
}

解决方案

问题核心在于坐标缩放逻辑错误和相机变换矩阵顺序颠倒,以下是修正后的实现:

关键修正点

  1. 相机变换矩阵顺序调整:先执行旋转,再做缩放,最后设置位置,确保坐标系对齐Mapbox
  2. 模型位置的正确缩放:将Mercator坐标乘以当前地图的尺度因子(meterInMercatorCoordinateUnits),转换为Three.js世界单位
  3. 动态更新尺度因子:地图缩放时尺度会变化,在render方法中实时获取当前地图中心的尺度,避免模型位置偏移

修正后的完整代码

import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { MercatorCoordinate, type Map } from 'mapbox-gl';

export type ModelOptions = {
  location: [number, number];
  rotation: [number, number, number];
  scale: number;
};

export default class Map3DLayer {
  private map: Map;
  private camera: THREE.Camera;
  private scene: THREE.Scene;
  private renderer: THREE.WebGLRenderer;
  private cameraTransform: THREE.Matrix4;

  constructor(map: Map) {
    this.map = map;

    // Build the scene
    this.camera = new THREE.Camera();
    this.scene = new THREE.Scene();

    this.renderer = new THREE.WebGLRenderer({
      canvas: map.getCanvas(),
      context: map.getCanvas().getContext('webgl') ?? undefined,
      antialias: true,
    });
    this.renderer.autoClear = false;

    const light = new THREE.AmbientLight(0xffffff, 5);
    this.scene.add(light);

    // 初始化相机变换矩阵
    this.cameraTransform = new THREE.Matrix4();
  }

  loadModel(name: string, path: string, options: ModelOptions) {
    const loader = new GLTFLoader();
    return new Promise<void>((resolve, reject) => {
      loader.load(path, ({ scene }: any) => {
        scene.name = name;
        this.scene.add(scene);
        this.update(name, options);
        resolve();
      }, undefined, reject);
    });
  }

  update(name: string, options: Partial<ModelOptions>) {
    const item = this.scene.getObjectByName(name);
    if (!item) return;
    const { location, rotation, scale } = options;
    
    if (rotation) item.rotation.set(...rotation);
    if (scale) item.scale.set(scale, scale, scale);
    if (location) {
      // 将经纬度转换为墨卡托坐标,并应用当前地图尺度
      const mercatorCoord = MercatorCoordinate.fromLngLat(location, 0);
      const scaleFactor = mercatorCoord.meterInMercatorCoordinateUnits();
      // 转换为Three.js世界坐标,Z轴取反适配坐标系
      item.position.set(
        mercatorCoord.x * scaleFactor,
        mercatorCoord.y * scaleFactor,
        mercatorCoord.z * -scaleFactor
      );
    }

    this.renderer.state.reset();
    this.renderer.render(this.scene, this.camera);
    this.map.triggerRepaint();
  }

  render(ctx: WebGLRenderingContext, matrix: number[]) {
    // 实时获取当前地图中心的尺度和坐标,动态更新相机变换
    const center = MercatorCoordinate.fromLngLat(this.map.getCenter(), 0);
    const scaleFactor = center.meterInMercatorCoordinateUnits();

    // 先旋转,再缩放,最后平移的矩阵顺序
    const rotation = new THREE.Matrix4().multiplyMatrices(
      new THREE.Matrix4().makeRotationX(-Math.PI / 2),
      new THREE.Matrix4().makeRotationY(Math.PI)
    );
    const scale = new THREE.Matrix4().makeScale(scaleFactor, scaleFactor, -scaleFactor);
    const translation = new THREE.Matrix4().makeTranslation(center.x, center.y, center.z);

    this.cameraTransform = rotation.multiply(scale).multiply(translation);

    // 更新相机投影矩阵
    this.camera.projectionMatrix = new THREE.Matrix4()
      .fromArray(matrix)
      .multiply(this.cameraTransform);
    
    this.renderer.resetState();
    this.renderer.render(this.scene, this.camera);
  }
}

说明

  • 模型位置计算时,直接用MercatorCoordinate的坐标值乘以当前地图的meterInMercatorCoordinateUnits,将0-1范围的墨卡托坐标转换为实际世界单位
  • 相机变换矩阵调整为旋转→缩放→平移的顺序,确保Three.js坐标系与Mapbox坐标系正确对齐
  • 在render方法中实时获取地图中心的尺度和坐标,避免地图缩放导致模型位置偏移

内容的提问来源于stack exchange,提问作者markusand

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 01:43:12