更新时间:2025-01-10 GMT+08:00
分享

开发指导

开发流程

图1 开启AR会话开发流程

开发过程

  1. 开启AR相机。

    1. 在工具的资源管理器,单击右键,选择“新建文件夹”,命名为“components”。
    2. 选择“components”文件夹,单击右键,选择“新建文件夹”,命名为“xrstart”。
    3. 选择“xrstart”文件夹,单击右键,选择“新建Component”,命名为“xrstart”。

      Component建完后,目录结构如图2

      图2 目录结构
    4. 在xrstart.json文件中,添加renderer配置项。
      {
        "component": true,
        "renderer": "xr-frame",
        "usingComponents": {}
      }
    5. 在xrstart.wxml文件中,添加camera、light节点。
      <xr-scene ar-system="modes:Plane;" >
        <xr-node node-id="cameraParent" position="0 0 0" rotation="0 0 0">
          <xr-camera id="camera" node-id="camera" clear-color="0.925 0.925 0.925 1" background="ar" far="2000" is-ar-camera></xr-camera>
        </xr-node>
        <xr-node node-id="lights">
          <xr-light type="ambient" color="1 1 1" intensity="1" />
          <xr-light type="directional" rotation="180 0 0" color="1 1 1" intensity="3" />
        </xr-node>
      </xr-scene>
    6. 在index.json文件中,引用新建的xrstart组件。
      {
        "usingComponents": {
          "xr-start": "../../components/xrstart/xrstart"
        }
      }
    7. 在index.js文件中,配置高度和宽度参数。
      import { XRClient } from "../../build/XRClient";
      Page({
        data: {
          width: 300,
          height: 300,
          renderWidth: 300,
          renderHeight: 300    
        },
        onLoad() {
          console.log(XRClient.getVersion())
          const info = wx.getSystemInfoSync();
          const width = info.windowWidth;
          const height = info.windowHeight;
          const dpi = info.pixelRatio;
          this.setData({
            width, height,                        
            renderWidth: width * dpi,
            renderHeight: height * dpi
          });
        }
      })
    8. 在index.wxml中设置xr-start组件高度和宽度。
      <view>
        <xr-start disable-scroll 
        id="main-frame"
        width="{{renderWidth}}" 
        height="{{renderHeight}}" 
        style="width:{{width}}px;height:{{height}}px;"
        />
      </view>
    9. 在app.json中配置lazyCodeLoading。
      {
        "pages": [
          "pages/index/index"
        ],
        "lazyCodeLoading": "requiredComponents"
      }
    10. 单击“真机调试”,小程序会开启AR相机,在手机上可看到相机拍摄到的现实环境画面。
      图3 开启AR相机

  2. 接入视觉定位。

    1. 在工具的资源管理器,单击右键,选择“新建文件夹”,命名为“utils”。
    2. 选择utils文件夹,单击右键,选择“新建文件”,命名为“GlobalBus.ts”。
    3. 在GlobalBus.ts文件中,配置事件码。
      export class GlobalBus {
        //vps定位结果事件码
        public static VPS_RESULT: string = 'VPS_RESULT';
        //vps状态事件码
        public static VPS_TRACKING: string = 'VPS_TRACKING';
        //scene实例事件码
        public static SCENE_INSTANCE: string = 'SCENE_INSTANCE';
      }
    4. 选择utils文件夹,单击右键,选择“新建文件”,命名为“Logger.ts”。
    5. 在Logger.ts文件中,实现ILog接口。
      export class Logger implements ILog {
        private logLevel: number = 3;
        private isSaveLog: boolean = false;
        private logSaveFolderPath: string = `${wx.env.USER_DATA_PATH}/miniprogramARSDK/`;
        private logSaveFileName: string = "";
        init() {
          let time:any= this.formatTime(new Date())
          time=time.replaceAll("/","_").replaceAll(" ","_").replaceAll(":","_")
          this.logSaveFileName=time+".log"
          this.createLogFile();
        }
        setIsSaveLog(isSave: boolean) {
          this.isSaveLog = isSave;
        }
        setLogOutputLevel(level: number) {
          this.logLevel = level;
        }
        error<T>(message: T) {
          if (this.logLevel < 1) return;
          const formartLog= this.formatLog("ERROR",message)
          console.error(formartLog);
          this.logToFile( formartLog);
        }
        warn<T>(message: T) {
          if (this.logLevel < 2) return;
          const formartLog= this.formatLog("WARN",message)
          console.warn(formartLog);
          this.logToFile( formartLog);
        }
        log<T>(message: T) {
          if (this.logLevel < 3) return;
          const formartLog= this.formatLog("LOG",message)
          console.log(formartLog);
          this.logToFile(formartLog);
        }
        createLogFile() {
          if(!this.isSaveLog)return;
          let self = this;
          let fsm = wx.getFileSystemManager()
          fsm.access({
            path: self.logSaveFolderPath,
            success() { },
            fail() {
              fsm.mkdirSync(self.logSaveFolderPath, true)
            }
          })
          fsm.access({
            path: self.logSaveFolderPath + self.logSaveFileName,
            success() { },
            fail() {
              fsm.writeFileSync(self.logSaveFolderPath + self.logSaveFileName, "", `utf-8`);
            }
          })
          console.log(`create logfile:${self.logSaveFolderPath + self.logSaveFileName}`)
        }
        logToFile(message: string) {
          if (!this.isSaveLog) return;
          let fsm = wx.getFileSystemManager()
          let self = this;
          fsm.access({
            path: self.logSaveFolderPath + self.logSaveFileName,
            complete() {
              fsm.appendFile({
                filePath: self.logSaveFolderPath + self.logSaveFileName,
                data: message,
                encoding: 'utf8',
                success: () => { },
                fail: (res) => { console.log(res.errMsg); }
              })
            }
          })
        }
        formatLog<T>(level: string, message: T): string {
          const date = new Date();
          const formattedDate=this.formatTime(date)
          const formattedMessage: string = `[${formattedDate}][${level}]: ${message}\n`;
          return formattedMessage
        }
        formatNumber = (n: number) => {
          const s = n.toString()
          return s[1] ? s : '0' + s
        }
        formatTime = (date: Date) => {
          const year = date.getFullYear()
          const month = date.getMonth() + 1
          const day = date.getDate()
          const hour = date.getHours()
          const minute = date.getMinutes()
          const second = date.getSeconds()
        
          return (
            [year, month, day].map(this.formatNumber).join('/') +
            ' ' +
            [hour, minute, second].map(this.formatNumber).join(':')
          )
        }
      }
    6. 在app.js文件中初始化logger。
      import { Logger } from "./utils/Logger";
      import { XRClient } from "./build/XRClient";
      App({
        onLaunch() {
          const logger=new Logger()
          XRClient.initLogger(logger)
          XRClient.log('onLaunch');
        },
      })
    7. 在xrstart.js文件中,初始化Vps,侦听Vps结果并设置回调函数。
      import { XRClient } from "../../build/XRClient";
      import { GlobalBus } from "../../utils/GlobalBus";
      Component({
        methods: {
          handleReady({ detail }) {
            this.scene = detail.value;
            this.offsetSetted = false;
            XRClient.log("handleReady");
          },
          handleARReady: function ({ }) {
            XRClient.log('arReady: arVersion ' + this.scene.ar.arVersion);
            XRClient.log('arReady: version ' + this.scene.ar.version);  
            // 认证用的ak和sk硬编码到代码中或者明文存储都有很大的安全风险,建议在配置文件或者环境变量中密文存放,使用时解密,确保安全;
            // 本示例以ak和sk保存在环境变量中为例,运行本示例前请先在本地环境中设置环境变量HUAWEICLOUD_SDK_AK和HUAWEICLOUD_SDK_SK。
            let config = {
              access: process.env.HUAWEICLOUD_SDK_AK,
              secret: process.env.HUAWEICLOUD_SDK_SK,
              beforeLocateDelta: 15,
              afterLocateDelta: 60,
              autoRequest: true,
              workerPath: 'build/workers/ImageProcessWorker.js',
              url:''
            }
            XRClient.init(config);
            XRClient.dispatchEvent(GlobalBus.SCENE_INSTANCE, { info: this.scene });
            XRClient.addEvent(GlobalBus.VPS_RESULT, this.onVpsResult, this);
            this.arInited = true;      
          },
          handleTick() {
            XRClient.updatePerFrame();
          },
          onVpsResult(evt) {
            XRClient.log("onVpsResult status" + evt.data.status);
            XRClient.log("onVpsResult info" + evt.data.info);
            if (evt.data.status) {
              XRClient.log("VPS success");
              wx.showToast({
                title: '定位成功',
                icon: 'success',
                duration: 2000
              })
            } else {
              XRClient.log("VPS failure");
              wx.showToast({
                title: '定位失败',
                icon: 'none',
                duration: 2000
              })
            }
          },
        },
      })

      代码中url固定为:https://koomap.cn-north-4.myhuaweicloud.com。

    8. 在xrstart.wxml文件里绑定函数。
      <xr-scene ar-system="modes:Plane;" bind:ready="handleReady" bind:ar-ready="handleARReady" bind:tick="handleTick">
    9. 在app.json文件里添加地理位置permission并配置workers。
      {
        "pages": [
          "pages/index/index"
        ],
        "permission": {
          "scope.userLocation": {
            "desc": "为了给您提供更好的服务,请授权您的地理位置信息"
          }
        },
        "workers": "build/workers",
        "lazyCodeLoading": "requiredComponents"
      }
    10. 在project.config.json文件的setting中,配置useCompilerPlugins。
      "useCompilerPlugins": [
            "typescript"
      ]
    11. 单击“真机调试”,小程序会自动发起定位,根据定位结果后,会弹出Toast提示是否定位成功。如定位失败,可参见常见问题排查章节确定原因。

  3. 渲染数字内容。

    1. 选择utils文件夹,单击右键,选择“新建文件”,命名为“ModelMag.ts”
    2. 在“ModelMag.ts”文件中,添加渲染逻辑。
      import { XRClient } from "../build/XRClient";
      import { GlobalBus } from "./GlobalBus";
      let xrFrameSystem
      class ModelMag {
        private scene: any;
        modelRoot: any;
        gltfModel: any[] = [];
        digitalInfo: any;
        count = 0;  
        private utmPositionJson = "[{\"position\":{\"x\":0,\"y\":0,\"z\":0}},{\"position\":{\"x\":0,\"y\":0,\"z\":2}}]"; //在utmPositionJson中配置数字内容的坐标,定位成功后接口会返回用户当前位置的坐标信息,可以将数字内容坐标设置在用户附近,以便于调试。
        constructor(scene: any) {
          this.scene = scene;
          xrFrameSystem = wx.getXrFrameSystem();
          const jsonData = JSON.parse(this.utmPositionJson);
          let node = this.scene.createElement(xrFrameSystem.XRNode);
          this.modelRoot = node;
          this.scene.addChild(node);
          this.digitalInfo = jsonData;
          this.setCube();
          XRClient.addEvent(GlobalBus.VPS_TRACKING, this.onVpsTrackingState, this);
        }
        public setPosition(xOffset, yOffset, zOffset) {
          let trs = this.modelRoot.getComponent(xrFrameSystem.Transform);
          trs.setData({ visible: true })
          XRClient.log("set digital root" + xOffset + " " + yOffset + " " + zOffset);
          trs.position.setValue(xOffset, yOffset, zOffset);
        }  
        setCube() {
          const geometry = this.scene.assets.getAsset('geometry', 'cube');//以方块为例,也可替换为.glb格式的模型
          const effect = this.scene.assets.getAsset('effect', 'standard');
          const beOccMaterial = this.scene.createMaterial(effect);
          beOccMaterial.setVector('u_baseColorFactor', xrFrameSystem.Vector4.createFromNumber(1.0, 0, 0, 1.0));
          this.modelRoot.getComponent(xrFrameSystem.Transform).setData({ visible: false })
          for (let i = 0; i < this.digitalInfo.length; i++) {
            let item = this.digitalInfo[i];
            const cube = this.scene.createElement(xrFrameSystem.XRNode);
            cube.addComponent(xrFrameSystem.Mesh, {
              geometry: geometry,
              material: beOccMaterial,
            });
            this.modelRoot.addChild(cube);
            const trs = cube.getComponent(xrFrameSystem.Transform);
            trs.position.setValue(item.position.x, item.position.y, -item.position.z);
            XRClient.log("set " + i + " cube posi:" + trs.position.x + " " + trs.position.y + " " + trs.position.z);
          }
        }
        onVpsTrackingState(evt) {
          if (evt.data.status) {
            XRClient.log("show digital");
            let modelRootTrs = this.modelRoot.getComponent(xrFrameSystem.Transform);
            modelRootTrs.setData({ visible: true })
          } else {
            XRClient.log("hide digital");
            let modelRootTrs = this.modelRoot.getComponent(xrFrameSystem.Transform);
            modelRootTrs.setData({ visible: false })
          }
        }
        public destroy(){
          XRClient.removeEvent(GlobalBus.VPS_TRACKING, this.onVpsTrackingState);
        }
      }
      export default ModelMag;
    3. 在index.js文件中实例化ModelMag。
      import { XRClient } from "../../build/XRClient";
      import { GlobalBus } from "../../utils/GlobalBus";
      import modmag from "../../utils/ModelMag";
      Component({
        methods: {
          handleReady({ detail }) {
            this.scene = detail.value;
            this.offsetSetted = false;
            XRClient.log("handleReady");
          },
          handleARReady: function ({ }) {
            XRClient.log('arReady: arVersion ' + this.scene.ar.arVersion);
            XRClient.log('arReady: version ' + this.scene.ar.version);  
            // 认证用的ak和sk硬编码到代码中或者明文存储都有很大的安全风险,建议在配置文件或者环境变量中密文存放,使用时解密,确保安全;
            // 本示例以ak和sk保存在环境变量中为例,运行本示例前请先在本地环境中设置环境变量HUAWEICLOUD_SDK_AK和HUAWEICLOUD_SDK_SK。
            let config = {
              access: process.env.HUAWEICLOUD_SDK_AK,
              secret: process.env.HUAWEICLOUD_SDK_SK,  
              beforeLocateDelta: 15,
              afterLocateDelta: 60,
              autoRequest: true,
              workerPath: 'build/workers/ImageProcessWorker.js',
              url:''
            }
            XRClient.init(config);
            XRClient.dispatchEvent(GlobalBus.SCENE_INSTANCE, { info: this.scene });
            XRClient.addEvent(GlobalBus.VPS_RESULT, this.onVpsResult, this);
            this.arInited = true;   
            let mod = new modmag(this.scene);
            this.mod = mod;
          },
          handleTick() {
            XRClient.updatePerFrame();
          },
          onVpsResult(evt) {
            XRClient.log("onVpsResult status" + evt.data.status);
            XRClient.log("onVpsResult info" + evt.data.info);
            if (evt.data.status) {
              XRClient.log("VPS success");
              wx.showToast({
                title: '定位成功',
                icon: 'success',
                duration: 2000
              })
              if (!this.offsetSetted) {
                this.offsetSetted = true;
                let arr = XRClient.getCameraOffset();
                this.mod.setPosition(-arr[0], -arr[2], arr[1]);
              }
            } else {
              XRClient.log("VPS failure");
              wx.showToast({
                title: '定位失败',
                icon: 'none',
                duration: 2000
              })
            }
          },
        },
        lifetimes:{
          attached(){
            XRClient.log("attached");
          },
          detached(){
            XRClient.log("detached");   
            if(this.arInited)   
            {
              XRClient.destroyVps();
              XRClient.removeEvent(GlobalBus.VPS_RESULT, this.onVpsResult);
              this.mod.destroy();
            }      
          }
        }
      })
    4. 单击“真机调试”,定位成功后,在空间中显示红色方块。
      图4 显示红色方块

相关文档