10、基于 Cesium 的 3D 模型加载与地图可视化实战教程

该文章已生成可运行项目,
先看效果图

一、引言

在现代地理信息系统 (GIS) 和 3D 可视化应用中,如何高效地在地图上加载和展示 3D 模型是一个关键需求。Cesium 作为一款强大的开源 JavaScript 库,为我们提供了丰富的 API 和工具,使我们能够轻松实现基于 Web 的 3D 地理可视化。本文将详细介绍如何使用 Cesium 加载 GLTF 格式的 3D 模型,并结合高德地图底图创建一个完整的 3D 可视化应用。

二、Cesium 与 GLTF 基础知识
2.1 Cesium 简介

Cesium 是一个用于创建基于 Web 的 3D 地理信息系统的开源 JavaScript 库,由美国国家航空航天局 (NASA) 和美国地质调查局 (USGS) 支持开发。它支持多种地理数据格式,提供了强大的 3D 渲染能力,能够在浏览器中实现高性能的地球和地图可视化。

2.2 GLTF 格式

GLTF (OpenGL Transmission Format) 是一种用于高效传输和加载 3D 模型的开放格式,由 Khronos Group 开发。它以 JSON 格式存储 3D 模型的结构信息,同时支持二进制数据存储顶点、纹理等资源,具有体积小、加载快的特点,非常适合 Web 端 3D 应用。

三、环境搭建与项目初始化

首先,我们需要创建一个 Vue 项目并安装 Cesium 库:

# 创建Vue项目
vue create cesium-gltf-demo

# 进入项目目录
cd cesium-gltf-demo

# 安装Cesium
npm install cesium

接下来,配置 Vue 项目以正确加载 Cesium 资源。在 vue.config.js 中添加以下配置:

const CopyWebpackPlugin = require('copy-webpack-plugin');
const path = require('path');

module.exports = {
  configureWebpack: {
    plugins: [
      new CopyWebpackPlugin({
        patterns: [
          {
            from: path.join(__dirname, 'node_modules/cesium/Build/Cesium'),
            to: 'cesium'
          }
        ]
      })
    ],
    resolve: {
      fallback: {
        fs: false,
        path: false
      }
    }
  },
  chainWebpack: config => {
    config.module
      .rule('cesium')
      .test(/\.js$/)
      .include.add(path.join(__dirname, 'node_modules/cesium/Source'))
      .end()
      .use('babel-loader')
      .loader('babel-loader')
      .options({
        presets: ['@babel/preset-env']
      });
  },
  transpileDependencies: [
    'cesium'
  ]
};
四、实现 Cesium 地图与 3D 模型加载

下面是完整的 Vue 组件代码,实现了 Cesium 地图初始化、高德地图底图加载和 GLTF 模型加载功能:

<template>
  <div class="cesium-container" id="cesiumContainer"></div>
  <div v-if="loading" class="loading-overlay">
    <div class="spinner"></div>
    <p>加载中...</p>
  </div>
  <div v-if="errorMessage" class="error-message">
    <p>{{ errorMessage }}</p>
    <button @click="retry">重试</button>
  </div>
</template>

<script>
import { mapConfig } from "../../config/mapConfig";

export default {
  data() {
    return {
      viewer: null,
      loading: true,
      errorMessage: null
    };
  },
  mounted() {
    this.initCesium();
  },
  beforeDestroy() {
    if (this.viewer) {
      this.viewer.destroy();
    }
  },
  methods: {
    async initCesium() {
      try {
        // 引入Cesium样式
        require('cesium/Build/Cesium/Widgets/widgets.css');
        
        // 初始化Viewer
        this.viewer = new Cesium.Viewer("cesiumContainer", {
          animation: false,
          baseLayerPicker: false,
          fullscreenButton: false,
          geocoder: false,
          homeButton: false,
          infoBox: false,
          sceneModePicker: false,
          scene3DOnly: true,
          selectionIndicator: false,
          timeline: false,
          navigationHelpButton: false,
          shadows: true,
          shouldAnimate: true,
          sceneMode: Cesium.SceneMode.SCENE3D
        });

        // 初始定位 - 更靠近模型位置
        this.viewer.camera.flyTo({
          destination: Cesium.Cartesian3.fromDegrees(104.0744619, 30.0503706, 5000),
          duration: 3.0,
          orientation: {
            heading: Cesium.Math.toRadians(0), // 朝北
            pitch: Cesium.Math.toRadians(-30), // 俯视
            roll: 0.0
          },
          complete: () => {
            console.log("飞行动画完成");
          }
        });

        // 移除默认影像图层
        this.viewer.scene.imageryLayers.remove(
          this.viewer.scene.imageryLayers.get(0)
        );

        // 去除版权信息
        this.viewer._cesiumWidget._creditContainer.style.display = "none";

        // 添加高德地图影像
        await this.addGaodeMap();

        // 加载模型
        await this.loadModel();
      } catch (error) {
        console.error("初始化失败:", error);
        this.errorMessage = "加载失败,请重试";
      } finally {
        this.loading = false;
      }
    },

    async addGaodeMap() {
      try {
        const mapOption = {
          url: mapConfig.gaode.url2,
          minimumLevel: 3,
          maximumLevel: 18,
          credit: new Cesium.Credit({
            text: "高德地图",
            link: "https://www.amap.com/"
          })
        };

        const tdtLayer = new Cesium.UrlTemplateImageryProvider(mapOption);
        this.viewer.scene.imageryLayers.addImageryProvider(tdtLayer);
      } catch (error) {
        console.error("高德地图加载失败:", error);
        throw new Error("地图加载失败");
      }
    },

    async loadModel() {
      try {
        // 模型位置与朝向
        const position = Cesium.Cartesian3.fromDegrees(
          104.0744619,
          30.0503706,
          100 // 降低高度以便更好观察
        );
        
        // 调整模型方向,使其朝上
        const heading = Cesium.Math.toRadians(0); // 朝北
        const pitch = Cesium.Math.toRadians(0); // 水平
        const roll = 0;
        const hpr = new Cesium.HeadingPitchRoll(heading, pitch, roll);
        const orientation = Cesium.Transforms.headingPitchRollQuaternion(position, hpr);

        // 加载模型
        const entity = await this.viewer.entities.add({
          name: "3D Model",
          position: position,
          orientation: orientation,
          model: {
            uri: "/scene.gltf",
            scale: 50.0, // 调整缩放比例
            minimumPixelSize: 128,
            maximumScale: 20000,
            incrementallyLoadTextures: true,
            runAnimations: true,
            clampAnimations: true,
            shadows: Cesium.ShadowMode.ENABLED,
            heightReference: Cesium.HeightReference.NONE,
            onLoad: (model) => {
              console.log("模型加载成功", model);
              // 添加成功提示
              setTimeout(() => {
                alert("模型加载成功!可以使用鼠标拖动来浏览场景");
              }, 1000);
            },
            onError: (error) => {
              console.error("模型加载失败", error);
              throw new Error("模型加载失败");
            }
          }
        });

        // 聚焦模型
        this.viewer.trackedEntity = entity;
        this.viewer.zoomTo(entity);
        
        // 添加视图控制说明
        this.addViewerControls();
      } catch (error) {
        console.error("模型加载过程出错:", error);
        throw new Error("模型加载过程出错");
      }
    },

    addViewerControls() {
      // 添加简单的控制说明
      const controls = document.createElement('div');
      controls.className = 'viewer-controls';
      controls.innerHTML = `
        <div class="control-item">
          <span>鼠标左键:</span> 旋转视角
        </div>
        <div class="control-item">
          <span>鼠标右键:</span> 平移
        </div>
        <div class="control-item">
          <span>滚轮:</span> 缩放
        </div>
        <button id="resetView">重置视图</button>
      `;
      document.body.appendChild(controls);
      
      // 添加重置视图功能
      document.getElementById('resetView').addEventListener('click', () => {
        this.viewer.camera.setView({
          destination: Cesium.Cartesian3.fromDegrees(104.0744619, 30.0503706, 5000),
          orientation: {
            heading: Cesium.Math.toRadians(0),
            pitch: Cesium.Math.toRadians(-30),
            roll: 0.0
          }
        });
      });
    },

    retry() {
      this.errorMessage = null;
      this.loading = true;
      this.initCesium();
    }
  }
};
</script>

<style lang="scss" scoped>
.cesium-container {
  width: 100%;
  height: 100vh;
  touch-action: none;
}

.loading-overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  color: white;
  z-index: 1000;
  
  .spinner {
    border: 4px solid rgba(255, 255, 255, 0.3);
    border-radius: 50%;
    border-top: 4px solid white;
    width: 30px;
    height: 30px;
    animation: spin 1s linear infinite;
    margin-bottom: 10px;
  }
  
  @keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
  }
}

.error-message {
  position: absolute;
  top: 20px;
  left: 50%;
  transform: translateX(-50%);
  background-color: rgba(255, 0, 0, 0.8);
  color: white;
  padding: 10px 20px;
  border-radius: 5px;
  z-index: 1000;
  display: flex;
  align-items: center;
  
  button {
    margin-left: 10px;
    padding: 5px 10px;
    background-color: white;
    color: red;
    border: none;
    border-radius: 3px;
    cursor: pointer;
  }
}

.viewer-controls {
  position: absolute;
  bottom: 20px;
  left: 20px;
  background-color: rgba(0, 0, 0, 0.7);
  color: white;
  padding: 10px;
  border-radius: 5px;
  z-index: 1000;
  
  .control-item {
    margin-bottom: 5px;
  }
  
  button {
    margin-top: 10px;
    padding: 5px 10px;
    background-color: white;
    color: black;
    border: none;
    border-radius: 3px;
    cursor: pointer;
  }
}
</style>
export const mapConfig = {
  gaode: {
    url1: 'http://webst02.is.autonavi.com/appmaptile?x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1&style=8', //'高德路网中文注记'
    url2: 'https://webst02.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}', //高德影像
    url3: 'http://webrd02.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}', //高德矢量
  },
};
五、关键技术点解析
5.1 模型位置与方向控制

在 Cesium 中,模型的位置和方向是通过positionorientation属性控制的:

// 设置模型位置(经纬度和高度)
const position = Cesium.Cartesian3.fromDegrees(104.0744619, 30.0503706, 100);

// 设置模型方向(航向、俯仰、翻滚)
const heading = Cesium.Math.toRadians(0);
const pitch = Cesium.Math.toRadians(0);
const roll = 0;
const hpr = new Cesium.HeadingPitchRoll(heading, pitch, roll);
const orientation = Cesium.Transforms.headingPitchRollQuaternion(position, hpr);
5.2 相机控制

相机控制是 3D 可视化中的重要部分,我们可以通过以下方式设置相机位置和方向:

// 设置相机位置和方向
this.viewer.camera.setView({
  destination: Cesium.Cartesian3.fromDegrees(longitude, latitude, height),
  orientation: {
    heading: Cesium.Math.toRadians(headingAngle),
    pitch: Cesium.Math.toRadians(pitchAngle),
    roll: 0.0
  }
});
5.3 错误处理与用户体验

为了提高应用的健壮性,我们添加了全面的错误处理和加载状态提示:

try {
  // 执行可能出错的代码
} catch (error) {
  console.error("错误:", error);
  this.errorMessage = "加载失败,请重试";
} finally {
  this.loading = false;
}
六、常见问题与解决方案
  1. 模型加载失败

    • 检查模型文件路径是否正确,模型文件建议放再public文件夹下
    • 确保模型文件格式正确(.gltf 或.glb)
    • 检查网络请求是否有 404 或其他错误
  2. 模型显示异常

    • 调整模型的 scale 参数
    • 检查模型的原始坐标系,调整 heading/pitch/roll 参数
    • 尝试使用不同的 heightReference 设置
  3. 性能问题

    • 对于复杂模型,考虑使用简化版本
    • 启用incrementallyLoadTextures以渐进加载纹理
    • 使用scene3DOnly: true优化性能
本文章已经生成可运行项目
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冒气er

伸出你发财的小手叮咚一下

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值