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

@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的示例。

示例代码如下:

html 复制代码
<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里使用后处理。

示例代码如下:

html 复制代码
<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组件的对应事件上。 下面我们就展示下怎么手动添加模型,并且触发对应事件。

示例代码如下:

html 复制代码
<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实现相同模型的材质功能,这样在业务不需要修改模型的材质的情况下,可以实现最大的内存精简。 示例代码如下:

html 复制代码
<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兆,整个节省相对来说比较可观。

相关推荐
程序员阿超的博客28 分钟前
React动态渲染:如何用map循环渲染一个列表(List)
前端·react.js·前端框架
magic 24530 分钟前
模拟 AJAX 提交 form 表单及请求头设置详解
前端·javascript·ajax
小小小小宇5 小时前
前端 Service Worker
前端
只喜欢赚钱的棉花没有糖6 小时前
http的缓存问题
前端·javascript·http
小小小小宇6 小时前
请求竞态问题统一封装
前端
loriloy6 小时前
前端资源帖
前端
源码超级联盟6 小时前
display的block和inline-block有什么区别
前端
GISer_Jing6 小时前
前端构建工具(Webpack\Vite\esbuild\Rspack)拆包能力深度解析
前端·webpack·node.js
让梦想疯狂6 小时前
开源、免费、美观的 Vue 后台管理系统模板
前端·javascript·vue.js
海云前端7 小时前
前端写简历有个很大的误区,就是夸张自己做过的东西。
前端