本文结合我们的AR实景导航小程序插件介绍一些相关的技术点,主要通过微信小程序的接口获取摄像头画面、手机位置及方向,再配合threejs实现。实景导航主要是通过在摄像头画面上叠加方向箭头的方式达到导航的目的,大概界面如下:
小程序获取摄像头内容
通过小程序VKSession获取摄像头画面,小程序VKSession通过载入摄像头画面,自带了多种功能,比如图片跟踪,人脸识别等,我们这里只需要获取摄像头的画面,相关代码部分大致如下:
var requestID;
var nextTimer;
// 初始化VKSession
function initVKSession(isRefresh, cb) {
if (!wx.createVKSession) { // 不支持的手机
wx.showToast({
title: '不支持AR导航!',
icon: 'error',
duration: 5000,
})
return
}
if (!session || isRefresh) {
if (session) {
session.destroy()
}
// 创建AR会话,没创建过的话
session = wx.createVKSession({
track: {
// plane: { mode: 3 },
plane: { mode: 1 },
// depth: { mode: 1 } // v2特有
},
version: 'v1' // v1, v2 需要调试 => 大部分手机估计要降级至v1
})
}
if (requestID) {
session.cancelAnimationFrame(requestID)
}
// 开始AR会话
session.start(err => {
if (err) {
console.log('session.start', err)
return
}
console.log('session.start', 'ok')
// 监视小程序窗口变化
session.on('resize', function () {
console.log('session on resize')
calcCanvasSize()
})
// 设置画布的大小
calcCanvasSize()
// 初始化webgl的背景
webglBusiness.initGL(renderer)
var nextNumber = 0; // 限制5次渲染一次(1秒12帧),提高性能
// 每帧渲染
const onFrame = function (timestamp) {
// console.log('onFrame in', session, nextNumber)
if (!session) {
return
}
nextNumber++
if (nextNumber == 5) {
nextNumber = 0
// 从AR会话获取每帧图像
const frame = session.getVKFrame(canvas.width, canvas.height)
if (frame) {
try {
webglBusiness.renderGL(frame) // 绘制摄像头画面
} catch (e) {
console.log('onFrame err :>> ', e);
}
try {
// threejs渲染过程,添加箭头等其他元素
render(frame)
} catch (e) {
console.log('onFrame err2 :>> ', e);
}
}
}
nextFrameFunc()
}
const nextFrameFunc = function() {
clearTimeout(nextTimer);
if (session) {
requestID = session.requestAnimationFrame(onFrame)
}
nextTimer = setTimeout(nextFrameFunc, 1000) // 1秒还没开始就重新再调自己
}
nextFrameFunc()
})
}
我们这里初步做了些性能方面的处理,限制画面帧率,否则手机很快就烫起来了。另外微信启动摄像头时间不确定导致有时可能无法打开,代码里也相应做了处理。
定位和导航路线
通过微信的接口wx.getLocation(需要获取权限)获取用户定位,微信的定位接口地图系标准是gcj-02,该经纬度的值在腾讯地图和高德地图上可以直接使用。
var me = this
wx.getLocation({
type: 'gcj02', // 是腾讯地图的定位么?
altitude: true, // 高度信息
isHighAccuracy: true, // 开启高精度定位
success (res) {
me.setData({
position: {
latitude: res.latitude,
longitude: res.longitude,
speed: res.speed,
accuracy: res.accuracy,
altitude: res.altitude
}
})
},
complete() {
// 定时获取位置
locationInter = setTimeout(me.getPhoneLocation, 3000)
}
})
导航路线就使用腾讯地图和高德地图的接口即可,我们的插件同时支持,配置对应的key即可,配置方式可参考对应的文档:
腾讯地图 https://lbs.qq.com/service/webService/webServiceGuide/webServiceOverview
高德地图 https://lbs.amap.com/api/wx/gettingstarted
绘制箭头动线效果
threejs的shader实现:
var shaderArrowSize = {
width: 400,
height: 30
}
// shaderMaterial实现的箭头
moveArrowShaderMaterialObj = new THREE.ShaderMaterial({
wireframe: false,
transparent: true,
side: THREE.DoubleSide,
depthTest: true,
depthWrite: false,
uniforms: {
uTime: { value: null },
arrowWidth: { value: shaderArrowSize.width / 20 },
uBorderAlpha: { value: 0.5 }, // border透明度
uStrikeAlpha: { value: 0.25 } // 滚动线透明度
},
vertexShader: `
varying vec3 vPosition;
// varying vec3 vModelPosition;
varying vec2 vUv;
void main() {
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
gl_Position = projectionMatrix * viewMatrix * modelPosition;
vPosition = position.xyz;
// vModelPosition = modelPosition.xyz;
vUv = uv;
}
`,
fragmentShader: `uniform float uTime;
uniform float arrowWidth;
uniform float uBorderAlpha;
uniform float uStrikeAlpha;
varying vec3 vPosition;
// varying vec3 vModelPosition;
varying vec2 vUv;
void main() {
float uStrikeWidth = arrowWidth;
float uBorderWidth = 0.05; // TODO: 需要根据整体尺寸来自动调整
// if(vModelPosition.z < 0.0) { // z为负数不显示
// discard;
// }
float strikeStrength = mod((vPosition.x - vPosition.y - uTime * 0.00035 + vPosition.z) / uStrikeWidth * 0.5, 1.0);
if (vPosition.y > 0.0) { // y方向折返实现箭头
strikeStrength = mod((vPosition.x + vPosition.y - uTime * 0.00035 + vPosition.z) / uStrikeWidth * 0.5, 1.0);
}
strikeStrength = step(strikeStrength, 0.5) * uStrikeAlpha;
float borderStrength = max(step(1.0 - vUv.y, uBorderWidth), step(vUv.y, uBorderWidth)) * uBorderAlpha;
float alpha = max(strikeStrength, borderStrength);
gl_FragColor = vec4(vec3(1.0), alpha);
// gl_FragColor = vec4(vUv, 1.0, 1.0);
}`
});
shaderArrowMesh = new THREE.Mesh(new THREE.PlaneBufferGeometry(shaderArrowSize.width, shaderArrowSize.height), moveArrowShaderMaterialObj);
// 更新uniforms.uTime的值达到移动效果
moveArrowShaderMaterialObj.uniforms.uTime.value = clockElapsed * 300 * 1000;
threejs的图片实现:
var arrowTexture = new THREE.TextureLoader().load('https://[箭头图片http地址]/arrow.png');
arrowTexture.repeat.set(5, 1);
arrowTexture.matrixAutoUpdate = true;
arrowTexture.wrapS = THREE.RepeatWrapping;
arrowTexture.wrapT = THREE.RepeatWrapping;
arrowTexture.minFilter = THREE.LinearFilter;
var arrowMaterial = new THREE.MeshPhongMaterial({
map: arrowTexture,
transparent: true,
side: THREE.DoubleSide,
opacity: 0.8,
});
var arrowSize = {
width: 300,
height: 15
}
var arrowGeometry = new THREE.PlaneBufferGeometry(arrowSize.width, arrowSize.height);
arrowMesh = new THREE.Mesh(arrowGeometry, arrowMaterial);
// 通过在每帧渲染时,设置纹理的offset达到移动效果:
arrowTexture.offset.set(-clockElapsed / 1 % 1000, 0);
上述2种方法都是平面箭头,来个3d的箭头看起来会更好一些:
// 创建3d箭头
function add3dArrow() {
const shape = new THREE.Shape();
shape.moveTo(0, 0);
shape.lineTo(1, 1);
shape.lineTo(0.5, 1);
shape.lineTo(0.5, 2);
shape.lineTo(-0.5, 2);
shape.lineTo(-0.5, 1);
shape.lineTo(-1, 1);
shape.lineTo(0, 0);
const extrudeSettings = {
depth: .2,
bevelEnabled: true,
bevelThickness: .1,
bevelSize: .1,
bevelOffset: 0,
bevelSegments: 10
};
// 主要是通过THREE.ExtrudeGeometry配合路径,绘制任意3d形状
const geometry = new THREE.ExtrudeGeometry( shape, extrudeSettings );
const material = new THREE.MeshPhysicalMaterial( { color: 0x00c3dc } );
return new THREE.Mesh( geometry, material );
}
本插件介绍地址:https://mp.weixin.qq.com/wxopen/plugindevdoc?appid=wx0b07b21a83347243
小程序VKSession参考:https://developers.weixin.qq.com/miniprogram/dev/api/ai/visionkit/wx.createVKSession.html