实景AR导航一些技术点介绍

发布时间:2026/10/8 6:58:26
实景AR导航一些技术点介绍 本文结合公司的实景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即可配置方式可参考对应的文档腾讯地图 WebService API | 腾讯位置服务高德地图 入门指南-微信小程序插件 | 高德地图API根据经纬度获取物体在页面上的位置经纬度转换是AR导航的关键步骤涉及的计算比较复杂这里通过代码简单介绍一些关键部分计算2个经纬度间的距离获取手机偏转角将地点经纬度转换为相对手机的AR坐标系空间位置和距离。// 非turf.js实现2个经纬度间距离计算 function distance(point1, point2) { const rad Math.PI / 180; const lat1 point1[1] * rad; const lat2 point2[1] * rad; const lon1 point1[0] * rad; const lon2 point2[0] * rad; const R 6371; // 地球半径单位为公里 const deltaLatByRad lat2 - lat1; const deltaLonByRad lon2 - lon1; const a Math.sin(deltaLatByRad / 2) * Math.sin(deltaLatByRad / 2) Math.cos(lat1) * Math.cos(lat2) * Math.sin(deltaLonByRad / 2) * Math.sin(deltaLonByRad / 2); const c 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); const distance R * c; return distance; } // 带入偏转角后的坐标位置 (这里指北偏转角northAngle是手机当前前方与北方的夹角) function getNewPositionWithAngle(position) { var newPosition [ position[0] * Math.cos(northAngle) - position[1] * Math.sin(northAngle), position[0] * Math.sin(northAngle) position[1] * Math.cos(northAngle) ]; return newPosition; } // 根据当前经纬度和目标地经纬度计算出在AR坐标系中的位置、距离 function mapPositionToARPosition(curLocation, mapTargetPosition) { var longitudeDistance distance(curLocation, [mapTargetPosition[0], curLocation[1]]); // 当前位置到目标经度差 var latitudeDistance distance(curLocation, [curLocation[0], mapTargetPosition[1]]); // 当前位置到目标纬度差 var longitudeDirection mapTargetPosition[0] curLocation[0] ? 1 : -1; // 当前位置到目标经度方向 var latitudeDirection mapTargetPosition[1] curLocation[1] ? 1 : -1; // 当前位置到目标纬度方向 var lineDistance distance(curLocation, mapTargetPosition); // 当前位置到目标位置直线距离 var toPositionXZ [longitudeDirection * longitudeDistance * 1000 * 10, latitudeDirection * latitudeDistance * 1000 * 10]; var toNewPositionXZ getNewPositionWithAngle(toPositionXZ); return { x: toNewPositionXZ[0], y: 0, z: toNewPositionXZ[1], lineDistance }; } // 根据2个经纬度计算方向角 function calculateBearing(startLat, startLng, endLat, endLng) { const start { lat: startLat, lon: startLng }; const end { lat: endLat, lon: endLng }; const startLatRad start.lat * (Math.PI / 180); const startLonRad start.lon * (Math.PI / 180); const endLatRad end.lat * (Math.PI / 180); const endLonRad end.lon * (Math.PI / 180); const y Math.sin(endLonRad - startLonRad) * Math.cos(endLatRad); const x Math.cos(startLatRad) * Math.sin(endLatRad) - Math.sin(startLatRad) * Math.cos(endLatRad) * Math.cos(endLonRad - startLonRad); const brng Math.atan2(y, x); return brng; // rotation是用的弧度这里直接就是弧度下面注释的是360度的角度 // return (brng * 180 / Math.PI 360) % 360; } // 获取手机方向 通过罗盘 wx.startCompass() wx.onCompassChange(res { // console.log(onCompassChange, res) me.setData({ direction: { direction: res.direction, accuracy: res.accuracy } }) me.setData({ mapRotate: -res.direction // 北向偏转角 }) })绘制箭头动线效果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; 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); } }); 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 ); }参考插件介绍地址实景AR导航 | 小程序插件 | 微信公众平台小程序VKSessionVKSession | 微信开放文档