@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兆,整个节省相对来说比较可观。