在三維場景中加載模型是最常見的需求之一。雖然可以直接使用 Three.js 的 GLTFLoader,但在不同投影方式下需要手動處理座標轉換,比較麻煩。今天就來學習 mapvthree 提供的 SimpleModel 類,看看它是如何簡化這個過程的。

瞭解 SimpleModel

SimpleModel 是 mapvthree 對 Three.js 模型加載的封裝,主要解決了以下問題:

原生 Three.js 加載方式的問題

如果直接使用 Three.js 的 GLTFLoader 加載模型:

import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';

const loader = new GLTFLoader();
loader.load('assets/models/tree/tree18.glb', gltf => {
    const model = gltf.scene;
    
    model.position.set(x, y, z);
    model.rotateX(Math.PI / 2);
    model.scale.setScalar(10);
    
    engine.add(model);
});

問題:這種方式只適用於平面投影(EPSG:3857),在 ECEF 等其他投影上,需要額外進行投影旋轉,非常麻煩。

SimpleModel 的優勢

SimpleModel 是對 Three.js 的封裝,具有以下優勢:

  • 自動投影轉換:自動根據當前投影進行座標旋轉,無需手動處理
    • 簡化接口:統一的配置參數,更易使用
    • 多種加載方式:支持從 URL 加載,也支持傳入已有的 Object3D 實例
    • 格式兼容:支持所有 Three.js 支持的模型格式(glb、gltf 等)
    • 座標系轉換:自動處理 Y-Up 到 Z-Up 的座標系轉換
    • 事件監聽:提供加載完成事件,方便後續處理 我的理解:SimpleModel 本質上是對 Three.js 的 GLTFLoader 和 Object3D 的封裝,讓我們不需要關心底層的投影轉換細節,專注於業務邏輯。

第一步:基本使用 - 從 URL 加載模型

最簡單的方式是從 URL 加載模型文件。

基本示例

import * as mapvthree from '@baidumap/mapv-three';

const container = document.getElementById('container');

const engine = new mapvthree.Engine(container, {
    map: {
        center: [120.628, 27.786, 0],
        range: 1000,
        pitch: 80,
        projection: 'EPSG:3857',
        provider: null,
    },
});

// 加載單體模型
const model = engine.add(new mapvthree.SimpleModel({
    name: '樹木模型',
    object: 'assets/models/tree/tree18.glb',
    point: [120.628, 27.786, 0],
    scale: [10, 10, 10],
    rotation: [Math.PI / 2, 0, 0],
}));

我的發現:只需要提供模型路徑和位置,引擎會自動處理加載和投影轉換。

我的理解

  • object 參數:模型文件的 URL 路徑,支持 glb/gltf 格式(以及所有 Three.js 支持的格式)
    • point 參數:模型在地圖上的位置,格式為 [經度, 緯度, 高度]
    • scale 參數:模型縮放比例,格式為 [x縮放, y縮放, z縮放]
    • rotation 參數:模型旋轉角度,格式為 [roll, pitch, heading],單位為弧度

第二步:設置模型位置

模型位置使用地理座標 [經度, 緯度, 高度] 來表示。

構造時設置位置

const model = engine.add(new mapvthree.SimpleModel({
    object: 'assets/models/tree/tree18.glb',
    point: [120.628, 27.786, 0], // 經度、緯度、高度
}));

動態修改位置

// 通過 point 屬性修改
model.point = [120.629, 27.787, 50];

// 或通過 setTransform 方法
model.setTransform({
    point: [120.629, 27.787, 50],
});

我的發現:可以隨時動態修改模型位置,引擎會自動更新模型的實際座標。

我的理解

  • 高度(z 值)是相對於地面的高度
    • 引擎會自動將地理座標轉換為場景座標
    • 不同投影方式下的轉換邏輯由引擎自動處理

第三步:設置模型旋轉和縮放

模型的旋轉和縮放也非常簡單。

設置旋轉

const model = engine.add(new mapvthree.SimpleModel({
    object: 'assets/models/tree/tree18.glb',
    point: [120.628, 27.786, 0],
    rotation: [Math.PI / 2, 0, 0], // [roll, pitch, heading]
}));

// 動態修改旋轉
model.setTransform({
    rotation: [0, Math.PI / 4, Math.PI / 2],
});

我的理解

  • rotation[0](roll):繞 x 軸旋轉
    • rotation[1](pitch):繞 y 軸旋轉
    • rotation[2](heading):繞 z 軸旋轉
    • 單位是弧度,不是角度(角度需要轉換:角度 * Math.PI / 180

設置縮放

const model = engine.add(new mapvthree.SimpleModel({
    object: 'assets/models/tree/tree18.glb',
    point: [120.628, 27.786, 0],
    scale: [10, 10, 10], // [x縮放, y縮放, z縮放]
}));

// 動態修改縮放
model.setTransform({
    scale: [20, 20, 20],
});

我的發現:可以對 x、y、z 三個方向分別設置縮放比例,實現非均勻縮放。

使用 Three.js 的 Vector3

import * as THREE from 'three';

// 也可以使用 Three.js 的 Vector3
model.setTransform({
    point: new THREE.Vector3(120.628, 27.786, 50),
    scale: new THREE.Vector3(15, 15, 15),
    rotation: new THREE.Vector3(Math.PI / 2, 0, 0),
});

我的理解:SimpleModel 兼容數組和 Vector3 兩種格式,可以根據習慣選擇。

第四步:監聽加載完成事件

模型加載是異步的,可以通過事件監聽器獲取加載完成的通知。

監聽 loaded 事件

const model = engine.add(new mapvthree.SimpleModel({
    object: 'assets/models/tree/tree18.glb',
    point: [120.628, 27.786, 0],
}));

// 監聽模型加載完成事件
model.addEventListener('loaded', e => {
    console.log('模型加載完成:', e.value);
    
    // 在加載完成後可以進行後續操作
    // 例如:添加動畫、修改材質等
});

我的發現:在 loaded 事件中,可以通過 e.value 獲取加載的模型對象,進行進一步的自定義操作。

我的理解

  • 如果需要在加載完成後進行操作(如修改材質、添加動畫),一定要使用 loaded 事件
    • 不要在構造函數返回後立即操作模型,因為此時模型可能還沒加載完成

第五步:動態更新模型變換

使用 setTransform 方法可以靈活地更新模型的位置、旋轉和縮放。

只更新部分參數

// 只更新位置
model.setTransform({
    point: [120.629, 27.787, 50],
});

// 只更新旋轉
model.setTransform({
    rotation: [0, Math.PI / 4, 0],
});

// 只更新縮放
model.setTransform({
    scale: [15, 15, 15],
});

我的發現setTransform 方法的參數都是可選的,可以只更新需要修改的參數。

同時更新多個參數

// 同時更新位置、旋轉和縮放
model.setTransform({
    point: [120.629, 27.787, 50],
    rotation: [Math.PI / 2, Math.PI / 4, 0],
    scale: [20, 20, 20],
});

我的理解setTransform 是更新模型變換的推薦方式,比直接修改 positionrotationscale 屬性更安全。

第六步:理解 autoYUpToZUp 參數

很多三維模型(如從建模軟件導出的模型)使用 Y 軸向上的座標系,而地理場景通常使用 Z 軸向上。

autoYUpToZUp 的作用

const model = engine.add(new mapvthree.SimpleModel({
    object: 'assets/models/tree/tree18.glb',
    point: [120.628, 27.786, 0],
    autoYUpToZUp: true, // 默認為 true
}));

我的理解

  • autoYUpToZUp = true(默認):自動將 Y-Up 座標系轉換為 Z-Up,模型會自動旋轉 90 度
    • autoYUpToZUp = false:不進行座標系轉換,保持模型原始方向
    • 這個參數僅對通過 URL 加載的模型有效,對直接傳入的 Object3D 實例無效

什麼時候需要關閉

// 如果模型本身就是 Z-Up,或者已經做了正確的旋轉
const model = engine.add(new mapvthree.SimpleModel({
    object: 'assets/models/building.glb',
    point: [120.628, 27.786, 0],
    autoYUpToZUp: false, // 不需要自動轉換
}));

我的發現:如果發現加載的模型方向不對(如倒着的、躺着的),可能是 autoYUpToZUp 的設置問題。

第七步:直接傳入 Object3D 實例

除了從 URL 加載,還可以直接傳入已經加載好的 Three.js Object3D 實例。

傳入已有的 Object3D

import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';

// 先用 Three.js 原生方式加載
const loader = new GLTFLoader();
loader.load('assets/models/tree/tree18.glb', gltf => {
    const mesh = gltf.scene;
    
    // 將加載好的模型傳給 SimpleModel
    const model = engine.add(new mapvthree.SimpleModel({
        name: '樹木模型',
        object: mesh, // 傳入 Object3D 實例
        point: [120.628, 27.786, 0],
        scale: [10, 10, 10],
    }));
});

我的理解

  • 這種方式適合需要對模型進行預處理的場景
    • 例如:修改材質、合併多個模型、自定義加載邏輯等
    • 傳入 Object3D 時,autoYUpToZUp 參數不生效

傳入自定義創建的 Mesh

import * as THREE from 'three';

// 創建一個自定義的立方體
const geometry = new THREE.BoxGeometry(10, 10, 10);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);

// 將自定義 Mesh 傳給 SimpleModel
const model = engine.add(new mapvthree.SimpleModel({
    name: '立方體',
    object: cube,
    point: [120.628, 27.786, 0],
}));

我的發現:可以傳入任何 Three.js 的 Object3D 對象,不僅限於從文件加載的模型。

我的理解:這展示了 SimpleModel 的靈活性,它本質上是一個位置和變換管理器,可以管理任何 Three.js 對象。

第八步:完整示例

我想寫一個完整的示例,把學到的都用上:

import * as mapvthree from '@baidumap/mapv-three';
import * as THREE from 'three';

const container = document.getElementById('container');

const engine = new mapvthree.Engine(container, {
    map: {
        center: [120.628, 27.786, 0],
        range: 1000,
        pitch: 80,
        projection: 'EPSG:3857',
        provider: null,
    },
    rendering: {
        enableAnimationLoop: true,
    },
});

// 加載單體模型
const model = engine.add(new mapvthree.SimpleModel({
    name: '樹木模型',
    object: 'assets/models/tree/tree18.glb',
    point: [120.628, 27.786, 0],
    scale: [10, 10, 10],
    rotation: [Math.PI / 2, 0, 0],
    autoYUpToZUp: true, // 自動座標系轉換
}));

// 監聽加載完成事件
model.addEventListener('loaded', e => {
    console.log('模型加載完成:', e.value);
    
    // 可以在這裏進行後續操作
    // 例如:修改材質、添加動畫等
});

// 動態更新模型位置(例如:5秒後移動模型)
setTimeout(() => {
    model.setTransform({
        point: [120.629, 27.787, 50],
        rotation: [Math.PI / 2, Math.PI / 4, 0],
        scale: [15, 15, 15],
    });
}, 5000);

我的感受:掌握了 SimpleModel,加載單體模型變得非常簡單,不需要關心投影轉換的細節!

第九步:踩過的坑

作為一個初學者,我踩了不少坑,記錄下來避免再犯:

坑 1:模型不顯示

原因:模型路徑錯誤,或者模型文件格式不支持。

解決

  1. 檢查模型文件路徑是否正確
    1. 確認模型文件格式是否為 glb/gltf(或其他 Three.js 支持的格式)
    1. 打開瀏覽器控制枱查看是否有加載錯誤
    1. 檢查模型位置是否在視野範圍內

坑 2:模型方向不對

原因:座標系轉換問題,或者旋轉設置不正確。

解決

  1. 嘗試修改 autoYUpToZUp 參數(true/false)
    1. 調整 rotation 參數,嘗試不同的旋轉角度
    1. 在建模軟件中檢查模型的座標系和方向

坑 3:模型太大或太小

原因:模型原始尺寸與場景比例不匹配。

解決

  1. 調整 scale 參數,嘗試不同的縮放比例
    1. 如果模型太小看不見,嘗試放大 10 倍、100 倍
    1. 如果模型太大,嘗試縮小到 0.1、0.01

坑 4:動態修改不生效

原因:在模型加載完成前就進行了修改操作。

解決

  1. 使用 loaded 事件,確保在模型加載完成後再操作
    1. 使用 setTransform 方法而不是直接修改屬性
    1. 檢查是否啓用了 enableAnimationLoop(某些操作需要渲染循環)

坑 5:不同投影下模型位置不對

原因:沒有正確理解 SimpleModel 的自動投影轉換。

解決

  1. 確認傳入的是地理座標 [經度, 緯度, 高度],而不是場景座標
    1. 不要手動計算投影轉換,SimpleModel 會自動處理
    1. 如果需要場景座標,使用 engine.map.projectArrayCoordinate() 轉換

坑 6:傳入 Object3D 時 autoYUpToZUp 不生效

原因autoYUpToZUp 只對通過 URL 加載的模型有效。

解決

  1. 如果傳入 Object3D 實例,需要手動處理座標系轉換
    1. 或者使用 rotation 參數手動旋轉
    1. 或者在加載時就處理好座標系

我的學習總結

經過這一天的學習,我掌握了:

  1. SimpleModel 的本質:對 Three.js 加載方式的封裝,自動處理投影轉換
    1. 支持的格式:所有 Three.js 支持的模型格式(glb、gltf 等)
    1. 兩種加載方式:從 URL 加載,或傳入 Object3D 實例
    1. 位置設置:使用地理座標 [經度, 緯度, 高度]
    1. 旋轉和縮放:使用數組或 Vector3 格式
    1. 動態更新:使用 setTransform 方法
    1. 座標系轉換:理解 autoYUpToZUp 的作用
    1. 事件監聽:使用 loaded 事件處理加載完成後的邏輯 我的感受:SimpleModel 讓加載單體模型變得非常簡單,不需要關心底層的投影轉換細節。它本質上是對 Three.js 的封裝,所以如果熟悉 Three.js,上手會非常快!

下一步計劃

  1. 學習如何加載和管理多個模型
    1. 學習 LODModel 實現性能優化
    1. 學習如何給模型添加動畫

學習筆記就到這裏啦!作為一個初學者,我覺得 SimpleModel 是一個非常實用的工具類,它簡化了三維模型的加載和管理。關鍵是要理解它是對 Three.js 的封裝,支持所有 Three.js 支持的模型格式,並能自動處理不同投影方式下的座標轉換。希望我的筆記能幫到其他初學者!大家一起加油!