@vuemap/vue-amap高德vue组件库常用技巧(九)- threejs

1,377 阅读3分钟

@vuemap/vue-amap是基于高德JSAPI2.0、Loca2.0封装的vue组件库,支持vue2、vue3版本。首页地址:vue-amap.guyixi.cn/

在上一个分享中,主要讲解了如何在地图上绘制常用的线。这一次主要讲解怎么在高德地图中使用threejs。
组件库中已经封装了基础的threejs组件,包括three图层、灯光组件、gltf组件、3dtiles组件等,今天主要介绍three图层以及怎么在叠加threejs的同时使用threejs的后处理功能。

普通threejs图层

高德提供了GlCustomLayer来自定义扩展webgl功能,可以在此基础上实现threejs功能叠加,由于与高德原生功能共用一个webgl上下文,地图内的元素能够实现层级叠加和地图上元素的深度显示。但同时共用一个上下文也会出现另外一两个问题,一个是无法使用threejs的后处理,另一个是更新threejs需要调用map.render导致所有图层全部重绘,会降低整个webgl的渲染性能。下面先看一下使用GlCustomLayer加载threejs的示例。

示例代码如下:

<template>
  <div class="map-page-container">
    <el-amap
      :show-label="false"
      :center="center"
      :zoom="zoom"
      view-mode="3D"
      :pitch="60"
      :show-building-block="false"
      :features="['bg','road']"
    >
      <el-amap-layer-three
        :hdr="hdrOptions"
      >
        <el-amap-three-light-ambient
          color="rgb(255,255,255)"
          :intensity="0.6"
        />
        <el-amap-three-light-directional
          color="rgb(255,0,255)"
          :intensity="1"
          :position="{x:0, y:1, z:0}"
        />
        <el-amap-three-light-hemisphere
          color="blue"
          :intensity="1"
          :position="{x:1, y:0, z:0}"
        />
        <el-amap-three-light-spot :position="{x:0, y:1, z:0}" />
        <el-amap-three-gltf
          :url="baseUrl + '/gltf/sgyj_point_animation.gltf'"
          :position="position"
          :scale="[10,10,10]"
          :rotation="rotation"
          :visible="visible"
          @init="init"
        />
      </el-amap-layer-three>
    </el-amap>
  </div>
  <div class="toolbar">
    <button @click="switchVisible()">
      {{ visible ? '隐藏' : '显示' }}
    </button>
  </div>
</template>

<script lang="ts" setup>

import {ref} from "vue";
import {ElAmap} from "@vuemap/vue-amap";
import {
  ElAmapLayerThree,
  ElAmapThreeGltf,
  ElAmapThreeLightAmbient,
  ElAmapThreeLightDirectional,
  ElAmapThreeLightHemisphere,
  ElAmapThreeLightSpot
} from '@vuemap/vue-amap-extra';

const baseUrl = "https://vue-amap.guyixi.cn/";

const zoom = ref(18);
const center = ref([121.59996, 31.197646]);
const visible = ref(true);
const position = ref([121.59996, 31.197646]);
const rotation = ref({x: 90, y: 0, z: 0});
const hdrOptions = ref({
  urls: ['px.hdr', 'nx.hdr', 'py.hdr', 'ny.hdr', 'pz.hdr', 'nz.hdr'],
  path: `${baseUrl}/hdr/`
});

const switchVisible = () => {
  visible.value = !visible.value;
}

const init = (object, $vue) => {
  $vue.$$startAnimations();
  console.log('gltf object: ', object);
  console.log('gltf $vue: ', $vue);
}


</script>

<style>
</style>


效果图: 普通示例

独立canvas图层

在上一个示例中主要示范不配置其他参数,使用GlCustomLayer加载threejs的示例。在这个示例中介绍组件库是怎么使用独立canvas绘制threejs的。首先得知道我们什么时候需要独立的canvas跟webgl上下文来处理threejs的相关功能,对于这个有两个主要的场景需要使用,第一个是需要使用threejs的后处理功能,由于GlCustomLayer与高德地图本身共用一个webgl上下文,后处理会导致地图的原本内容丢失,因此不适合在一个canvas里处理。第二个是需要处理大量的threejs模型,比如加载3dtiles,普通的比如一两百兆大小的3dtile模型可以不需要独立canvas,但对于1G以上的模型,如果跟高德共用上下文会出现很明显卡顿,这时候独立的canvas更适合,因为我们只要重绘threejs所在的canvas图层即可。 下面就介绍下怎么在el-amap-layer-three里使用后处理。

示例代码如下:

<template>
  <div class="map-page-container">
    <el-amap
      :show-label="false"
      :center="center"
      :zoom="zoom"
      view-mode="3D"
      :pitch="60"
      :show-building-block="false"
      :features="['bg','road']"
    >
      <el-amap-layer-three
        :hdr="hdrOptions"
        :create-canvas="true"
        @init="initLayer"
      >
        <el-amap-three-light-ambient
          color="rgb(255,255,255)"
          :intensity="0.6"
        />
        <el-amap-three-light-directional
          color="rgb(255,0,255)"
          :intensity="1"
          :position="{x:0, y:1, z:0}"
        />
        <el-amap-three-light-hemisphere
          color="blue"
          :intensity="1"
          :position="{x:1, y:0, z:0}"
        />
        <el-amap-three-light-spot :position="{x:0, y:1, z:0}" />
        <el-amap-three-gltf
          :url="baseUrl + '/gltf/sgyj_point_animation.gltf'"
          :position="position"
          :scale="[10,10,10]"
          :rotation="rotation"
          :visible="visible"
          @init="init"
        />
        <el-amap-three-gltf
          :url="baseUrl + '/gltf/car2.gltf'"
          :position="carPosition"
          :scale="[10,10,10]"
          :rotation="rotation"
          :move-animation="moveAnimation"
          :angle="carAngle"
          @init="initCar"
        />
      </el-amap-layer-three>
    </el-amap>
  </div>
  <div class="toolbar">
    <button @click="switchVisible()">
      {{ visible ? '隐藏' : '显示' }}
    </button>
    <button @click="stopCar()">
      停止移动
    </button>
    <button @click="startCar()">
      继续移动
    </button>
  </div>
</template>

<script lang="ts" setup>
import {ref} from "vue";
import {ElAmap} from "@vuemap/vue-amap";
import {
  ElAmapLayerThree,
  ElAmapThreeGltf,
  ElAmapThreeLightAmbient,
  ElAmapThreeLightDirectional,
  ElAmapThreeLightHemisphere,
  ElAmapThreeLightSpot
} from '@vuemap/vue-amap-extra';
import {RenderPass} from 'three/examples/jsm/postprocessing/RenderPass.js';
import {ShaderPass} from 'three/examples/jsm/postprocessing/ShaderPass.js';
import {DotScreenShader} from 'three/examples/jsm/shaders/DotScreenShader.js';

const baseUrl = "https://vue-amap.guyixi.cn/";

const zoom = ref(18);
const center = ref([121.59996, 31.197646]);
const visible = ref(true);
const position = ref([121.59996, 31.197646]);
const rotation = ref({x: 90, y: 0, z: 0});
const carPosition = ref([121.59996, 31.197646]);
const moveAnimation = ref({duration: 1000, smooth: true});
const carAngle = ref(90);
const hdrOptions = ref({
  urls: ['px.hdr', 'nx.hdr', 'py.hdr', 'ny.hdr', 'pz.hdr', 'nz.hdr'],
  path: `${baseUrl}/hdr/`
});
let carInterval = -1;

const switchVisible = () => {
  visible.value = !visible.value;
}
const initLayer = (layer) => {
  const renderPass = new RenderPass(layer.getScene(), layer.getCamera());
  layer.addPass(renderPass);

  const effect1 = new ShaderPass(DotScreenShader);
  effect1.uniforms['scale'].value = 4;
  layer.addPass(effect1);
}
const init = (object, $vue) => {
  $vue.$$startAnimations();
  console.log('gltf object: ', object);
  console.log('gltf $vue: ', $vue);
}
const initCar = () => {
  startCar();
}
const startCar = () => {
  carInterval = setInterval(() => {
    const lng = carPosition.value[0] + Math.random() * 0.0001;
    const lat = carPosition.value[1] + Math.random() * 0.0001;
    const newPosition = [lng, lat];
    const angle = Math.random() * 360
    carPosition.value = newPosition;
    carAngle.value = angle;
  }, 1000)
}
const stopCar = () => {
  clearInterval(carInterval);
}
</script>

<style>
</style>


效果图: 后处理

在上述示例中,通过给el-amap-layer-three组件增加一个:create-canvas="true"属性实现通过创建新的canvas来渲染threejs,组件内部通过高德地图的CustomLayer图层创建canvas。这样我们就可以在新的canvas上执行threejs渲染,这样,所有的threejs功能都可以在该图层上实现。

独立的canvas图层虽然性能很好,但也会有一些问题,最突出的一个问题就是无法实现图层的层级渲染和物体的深度关系,尤其新的canvas是叠加在高德地图的canvas之上,因此所有threejs的内容都会覆盖住高德地图本身的内容。

threejs图层事件

对于threejs图层的事件,组件库内部做了一定的封装和设定,目前支持的事件有三种:click、mouseover、mouseout。 图层内部使用射线功能,射线获取到模型后会递归寻找它以及它的parent里userData中存在acceptEvent属性的元素,找到这个元素后就会触发事件。通过该设定除了组件库内置的组件外,自己在el-amap-layer-three的init事件后手动添加的threejs物体也能直接支持事件,并且事件触发是触发到el-amap-layer-three组件的对应事件上。 下面我们就展示下怎么手动添加模型,并且触发对应事件。

示例代码如下:

<template>
  <div class="map-page-container">
    <el-amap
      :show-label="false"
      :center="center"
      :zoom="zoom"
      view-mode="3D"
      :pitch="60"
      :show-building-block="false"
      :features="['bg','road']"
    >
      <el-amap-text
        :visible="meshVisible"
        :position="meshPosition"
        :offset="[0, -80]"
        text="测试模型事件"
      />
      <el-amap-layer-three
        :hdr="hdrOptions"
        :create-canvas="true"
        @init="initLayer"
        @click="clickLayer"
        @mouseover="mouseoverLayer"
        @mouseout="mouseoutLayer"
      >
        <el-amap-three-light-ambient
          color="rgb(255,255,255)"
          :intensity="0.6"
        />
        <el-amap-three-light-directional
          color="rgb(255,0,255)"
          :intensity="1"
          :position="{x:0, y:1, z:0}"
        />
        <el-amap-three-light-hemisphere
          color="blue"
          :intensity="1"
          :position="{x:1, y:0, z:0}"
        />
        <el-amap-three-light-spot :position="{x:0, y:1, z:0}" />
        <el-amap-three-gltf
          :url="baseUrl + '/gltf/sgyj_point_animation.gltf'"
          :position="position"
          :scale="[10,10,10]"
          :rotation="rotation"
          :visible="visible"
          @init="init"
        />
      </el-amap-layer-three>
    </el-amap>
  </div>
  <div class="toolbar">
    <button @click="switchVisible()">
      {{ visible ? '隐藏' : '显示' }}
    </button>
  </div>
</template>

<script lang="ts" setup>

import {ref} from "vue";
import {BoxBufferGeometry, LinearFilter, Mesh, MeshPhongMaterial, TextureLoader} from "three";
import {ElAmap, ElAmapText} from "@vuemap/vue-amap";
import {
  ElAmapLayerThree,
  ElAmapThreeGltf,
  ElAmapThreeLightAmbient,
  ElAmapThreeLightDirectional,
  ElAmapThreeLightHemisphere,
  ElAmapThreeLightSpot
} from '@vuemap/vue-amap-extra';

const baseUrl = "https://vue-amap.guyixi.cn/";

const zoom = ref(18);
const center = ref([121.59996, 31.197646]);
const visible = ref(true);
const position = ref([121.59996, 31.197646]);
const rotation = ref({x: 90, y: 0, z: 0});
const hdrOptions = ref({
  urls: ['px.hdr', 'nx.hdr', 'py.hdr', 'ny.hdr', 'pz.hdr', 'nz.hdr'],
  path: `${baseUrl}/hdr/`
});
const meshPosition = [121.59896, 31.197646];

const switchVisible = () => {
  visible.value = !visible.value;
}
const initLayer = (layer) => {
  const texture = new TextureLoader().load(
      'https://a.amap.com/jsapi_demos/static/demo-center-v2/three.jpeg'
  );
  texture.minFilter = LinearFilter;
  //  这里可以使用 three 的各种材质
  const mat = new MeshPhongMaterial({
    color: 0xfff0f0,
    depthTest: true,
    transparent: true,
    map: texture,
  });
  const geo = new BoxBufferGeometry(50, 50, 50);
  const mesh = new Mesh(geo, mat);
  mesh.userData.acceptEvent = true;
  // 将经纬度转化为需要的世界坐标
  const r = layer.convertLngLat(meshPosition)
  mesh.position.set(r [0], r [1], 0);
  layer.add(mesh);
}
const init = (object, $vue) => {
  $vue.$$startAnimations();
  console.log('gltf object: ', object);
  console.log('gltf $vue: ', $vue);
}
const clickLayer = (group) => {
  console.log('click layer: ', group);
}

const meshVisible = ref(false)
const mouseoverLayer = (group) => {
  meshVisible.value = true;
  console.log('mouseoverLayer layer: ', group);
}
const mouseoutLayer = (group) => {
  meshVisible.value = false;
  console.log('mouseoutLayer layer: ', group);
}


</script>

<style>
</style>


效果图: 自定义模型添加事件

加载大量相同gltf模型

对于threejs加载大量模型,如果只有threejs的话性能问题不大,但叠加上高德地图,尤其地图本身渲染时也会消耗很大性能,对于大量gltf模型加载就会出现卡顿情况,因此组件库内部通过模型的共用与clone实现相同模型的材质功能,这样在业务不需要修改模型的材质的情况下,可以实现最大的内存精简。 示例代码如下:

<template>
  <div class="map-page-container">
    <el-amap
      :show-label="false"
      :center="center"
      :zoom="zoom"
      view-mode="3D"
      :pitch="60"
      :show-building-block="false"
      :features="['bg','road']"
    >
      <el-amap-layer-three
        :create-canvas="true"
      >
        <el-amap-three-light-ambient
          color="rgb(255,255,255)"
          :intensity="0.6"
        />
        <el-amap-three-gltf
          v-for="item in positions"
          :key="item.id"
          :url="baseUrl + '/gltf/sgyj_point_animation.gltf'"
          :position="item.lnglat"
          :use-model-cache="true"
          :scale="[10,10,10]"
          :rotation="rotation"
        />
      </el-amap-layer-three>
    </el-amap>
  </div>
</template>

<script lang="ts" setup>
import {ref, onBeforeMount} from "vue";
import {ElAmap} from "@vuemap/vue-amap";
import {
  ElAmapLayerThree,
  ElAmapThreeGltf,
  ElAmapThreeLightAmbient,
} from '@vuemap/vue-amap-extra';

type PositionType = {lnglat: number[], id: string}[]

const zoom = ref(18);
const center = ref([121.59996, 31.197646]);
const rotation = ref({x: 90, y: 0, z: 0});
const positions = ref<PositionType>([]);

const baseUrl = "https://vue-amap.guyixi.cn/";

onBeforeMount(() => {
  const array: PositionType = [];
  const position = [121.59996, 31.197646];
  for (let i = 0; i < 1000; i++) {
    const lnglat = [position[0] + Math.random() * 0.01, position[1] + Math.random() * 0.01];
    array.push({
      lnglat,
      id: lnglat.join(',')
    })
  }
  positions.value = array;
});
</script>

<style>
</style>

效果图: 大量模型

下面是共用材质和不共用材质的内存对比:

不共用时: 不共用

共用: 共用

由于示例模型很小只有59KB,在加载1000个模型时相差接近60兆,整个节省相对来说比较可观。