VUE Three.js Mapbox 实现地图中添加3D模型

创建一个 ModelComponent.vue 文件,用于封装单个3D模型。

<template>
  <!-- 3D模型组件不需要模板,直接通过 three.js 渲染 -->
</template>

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

export default {
  name: 'ModelComponent',
  props: {
    longitude: {
      type: Number,
      required: true,
    },
    latitude: {
      type: Number,
      required: true,
    },
    modelType: {
      type: String,
      required: true,
    },
  },
  data() {
    return {
      model: null,
    };
  },
  mounted() {
    this.loadModel();
  },
  methods: {
    loadModel() {
      const loader = new GLTFLoader();

      // 根据模型类别加载不同的模型文件
      let modelPath = '';
      switch (this.modelType) {
        case 'building':
          modelPath = '/models/building.gltf';
          break;
        case 'tree':
          modelPath = '/models/tree.gltf';
          break;
        default:
          console.error('Unknown model type:', this.modelType);
          return;
      }

      loader.load(modelPath, (gltf) => {
        this.model = gltf.scene;

        // 将模型添加到父组件的场景中
        this.$parent.scene.add(this.model);

        // 设置模型的位置
        this.updatePosition();
      });
    },
    updatePosition() {
      if (!this.model || !this.$parent.map) return;

      // 将经纬度转换为 three.js 场景中的 x, y 坐标
      const lngLat = [this.longitude, this.latitude];
      const pixelCoordinates = this.$parent.map.project(lngLat);
      const x = pixelCoordinates.x - window.innerWidth / 2;
      const y = -pixelCoordinates.y + window.innerHeight / 2;
      const z = 0; // 可以根据需要调整高度

      this.model.position.set(x, y, z);
    },
  },
  beforeDestroy() {
    // 组件销毁时从场景中移除模型
    if (this.model && this.$parent.scene) {
      this.$parent.scene.remove(this.model);
    }
  },
};
</script>

在主组件中,遍历后端返回的数据,动态创建多个 ModelComponent

<template>
  <div ref="mapContainer" class="map-container">
    <!-- 动态加载模型组件 -->
    <model-component
      v-for="(item, index) in modelData"
      :key="index"
      :longitude="item.longitude"
      :latitude="item.latitude"
      :model-type="item.type"
    />
  </div>
</template>

<script>
import mapboxgl from 'mapbox-gl';
import * as THREE from 'three';
import ModelComponent from './ModelComponent.vue';

export default {
  name: 'MapWith3DModels',
  components: {
    ModelComponent,
  },
  data() {
    return {
      map: null,
      scene: null,
      camera: null,
      renderer: null,
      modelData: [], // 后端返回的模型数据
    };
  },
  mounted() {
    this.initMap();
    this.initThree();
    this.fetchModelData();
  },
  methods: {
    initMap() {
      mapboxgl.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN';
      this.map = new mapboxgl.Map({
        container: this.$refs.mapContainer,
        style: 'mapbox://styles/mapbox/streets-v11',
        center: [初始经度, 初始纬度], // 设置地图中心点
        zoom: 15, // 设置地图缩放级别
        pitch: 60, // 设置地图倾斜角度
      });

      this.map.on('load', () => {
        this.map.addLayer({
          id: 'custom-layer',
          type: 'custom',
          renderingMode: '3d',
          onAdd: (map, gl) => {
            this.initThreeRenderer(gl);
          },
          render: (gl, matrix) => {
            this.renderThreeScene(matrix);
          },
        });
      });
    },
    initThree() {
      this.scene = new THREE.Scene();
      this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
      this.camera.position.set(0, 0, 100);
    },
    initThreeRenderer(gl) {
      this.renderer = new THREE.WebGLRenderer({
        canvas: this.map.getCanvas(),
        context: gl,
      });
      this.renderer.autoClear = false;
    },
    renderThreeScene(matrix) {
      const m = new THREE.Matrix4().fromArray(matrix);
      this.camera.projectionMatrix = m;
      this.renderer.resetState();
      this.renderer.render(this.scene, this.camera);
      this.map.triggerRepaint();
    },
    fetchModelData() {
      // 模拟后端返回的数据
      this.modelData = [
        { longitude: 经度1, latitude: 纬度1, type: 'building' },
        { longitude: 经度2, latitude: 纬度2, type: 'tree' },
        // 更多数据...
      ];
    },
  },
};
</script>

<style scoped>
.map-container {
  width: 100%;
  height: 100vh;
}
</style>

扩展 :为模型添加点击事件,显示详细信息

1.在 ModelComponent.vue 中,为模型添加点击事件检测逻辑。

<template>
  <!-- 3D模型组件不需要模板,直接通过 three.js 渲染 -->
</template>

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

export default {
  name: 'ModelComponent',
  props: {
    longitude: {
      type: Number,
      required: true,
    },
    latitude: {
      type: Number,
      required: true,
    },
    modelType: {
      type: String,
      required: true,
    },
    modelInfo: {
      type: Object,
      required: true,
    },
  },
  data() {
    return {
      model: null,
    };
  },
  mounted() {
    this.loadModel();
    this.addClickEvent();
  },
  methods: {
    loadModel() {
      const loader = new GLTFLoader();

      // 根据模型类别加载不同的模型文件
      let modelPath = '';
      switch (this.modelType) {
        case 'building':
          modelPath = '/models/building.gltf';
          break;
        case 'tree':
          modelPath = '/models/tree.gltf';
          break;
        default:
          console.error('Unknown model type:', this.modelType);
          return;
      }

      loader.load(modelPath, (gltf) => {
        this.model = gltf.scene;

        // 将模型添加到父组件的场景中
        this.$parent.scene.add(this.model);

        // 设置模型的位置
        this.updatePosition();

        // 为模型添加自定义属性,存储模型信息
        this.model.userData = this.modelInfo;
      });
    },
    updatePosition() {
      if (!this.model || !this.$parent.map) return;

      // 将经纬度转换为 three.js 场景中的 x, y 坐标
      const lngLat = [this.longitude, this.latitude];
      const pixelCoordinates = this.$parent.map.project(lngLat);
      const x = pixelCoordinates.x - window.innerWidth / 2;
      const y = -pixelCoordinates.y + window.innerHeight / 2;
      const z = 0; // 可以根据需要调整高度

      this.model.position.set(x, y, z);
    },
    addClickEvent() {
      // 监听鼠标点击事件
      window.addEventListener('click', this.onMouseClick, false);
    },
    onMouseClick(event) {
      if (!this.model || !this.$parent.camera || !this.$parent.scene) return;

      // 获取鼠标点击位置的归一化设备坐标 (NDC)
      const mouse = new THREE.Vector2();
      mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
      mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

      // 创建 Raycaster
      const raycaster = new THREE.Raycaster();
      raycaster.setFromCamera(mouse, this.$parent.camera);

      // 检测与模型的交集
      const intersects = raycaster.intersectObject(this.model, true);

      if (intersects.length > 0) {
        // 如果点击了模型,触发事件并显示详细信息
        this.$emit('model-clicked', this.model.userData);
      }
    },
  },
  beforeDestroy() {
    // 组件销毁时移除事件监听器
    window.removeEventListener('click', this.onMouseClick);
    if (this.model && this.$parent.scene) {
      this.$parent.scene.remove(this.model);
    }
  },
};
</script>

2.在主组件中监听 ModelComponent 的 model-clicked 事件,并显示详细信息。

<template>
  <div ref="mapContainer" class="map-container">
    <!-- 动态加载模型组件 -->
    <model-component
      v-for="(item, index) in modelData"
      :key="index"
      :longitude="item.longitude"
      :latitude="item.latitude"
      :model-type="item.type"
      :model-info="item.info"
      @model-clicked="showModelInfo"
    />

    <!-- 弹窗组件 -->
    <div v-if="selectedModel" class="modal">
      <div class="modal-content">
        <h2>{{ selectedModel.name }}</h2>
        <p>{{ selectedModel.description }}</p>
        <button @click="selectedModel = null">关闭</button>
      </div>
    </div>
  </div>
</template>

<script>
import mapboxgl from 'mapbox-gl';
import * as THREE from 'three';
import ModelComponent from './ModelComponent.vue';

export default {
  name: 'MapWith3DModels',
  components: {
    ModelComponent,
  },
  data() {
    return {
      map: null,
      scene: null,
      camera: null,
      renderer: null,
      modelData: [], // 后端返回的模型数据
      selectedModel: null, // 当前选中的模型信息
    };
  },
  mounted() {
    this.initMap();
    this.initThree();
    this.fetchModelData();
  },
  methods: {
    initMap() {
      mapboxgl.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN';
      this.map = new mapboxgl.Map({
        container: this.$refs.mapContainer,
        style: 'mapbox://styles/mapbox/streets-v11',
        center: [初始经度, 初始纬度], // 设置地图中心点
        zoom: 15, // 设置地图缩放级别
        pitch: 60, // 设置地图倾斜角度
      });

      this.map.on('load', () => {
        this.map.addLayer({
          id: 'custom-layer',
          type: 'custom',
          renderingMode: '3d',
          onAdd: (map, gl) => {
            this.initThreeRenderer(gl);
          },
          render: (gl, matrix) => {
            this.renderThreeScene(matrix);
          },
        });
      });
    },
    initThree() {
      this.scene = new THREE.Scene();
      this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
      this.camera.position.set(0, 0, 100);
    },
    initThreeRenderer(gl) {
      this.renderer = new THREE.WebGLRenderer({
        canvas: this.map.getCanvas(),
        context: gl,
      });
      this.renderer.autoClear = false;
    },
    renderThreeScene(matrix) {
      const m = new THREE.Matrix4().fromArray(matrix);
      this.camera.projectionMatrix = m;
      this.renderer.resetState();
      this.renderer.render(this.scene, this.camera);
      this.map.triggerRepaint();
    },
    fetchModelData() {
      // 模拟后端返回的数据
      this.modelData = [
        {
          longitude: 经度1,
          latitude: 纬度1,
          type: 'building',
          info: { name: '建筑1', description: '这是一个建筑物' },
        },
        {
          longitude: 经度2,
          latitude: 纬度2,
          type: 'tree',
          info: { name: '树1', description: '这是一棵树' },
        },
        // 更多数据...
      ];
    },
    showModelInfo(modelInfo) {
      this.selectedModel = modelInfo;
    },
  },
};
</script>

<style scoped>
.map-container {
  width: 100%;
  height: 100vh;
  position: relative;
}

.modal {
  position: absolute;
  top: 20px;
  left: 20px;
  background: white;
  padding: 20px;
  border: 1px solid #ccc;
  z-index: 1000;
}
</style>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值