前端javascript实现AR增强效果的几种技术方案

1,131 阅读9分钟

前言

关于AR(Augmented Reality,增强现实):是指利用 Web 技术在网页前端实现增强现实体验的技术。通过前端 AR 技术,开发者可以在用户的浏览器中展示虚拟的增强现实内容,让用户通过摄像头观看现实世界,并在其上叠加虚拟的 3D 对象、信息或效果。

以下是我个人总结出已实现的三种js实现AR效果的技术方案

1. threejs 通过获取视频文件流实现AR增强效果:

优点:兼容性较好,支持当前的主流浏览器(Chrome, Firefox,Safari,Edge,Opera),模型可操作性和扩展性强可以根据需求设置模型渲染效果和交互场景。

缺点:无法实现智能化的根据现实世界的场景定位模型的位置和大小,需要通过代码或者手动调整模型在屏幕中位置和大小,threejs的学习本身需要一定的成本,对于新手不太友好

2. 谷歌(google) model-viewer 实现AR增强效果:

官网网站(科学上网???):modelviewer.dev/

优点:都是封装好的API,通过简单的标签属性添加即可实现3d模型加载和模型AR效果的展示,效果实现起来简单小白也可以轻松上手

缺点:模型功能扩展性较差,无法根据自身需求去做二次功能扩展,兼容性较差无法支持当前所有的主流浏览器,

image.png

3. 苹果 .usdz 文件实现AR增强效果

关于USDZ:USDZ 是由苹果公司推出的一种用于增强现实(AR)应用的文件格式。它是基于标准的 ZIP 文件格式,内部包含了用于显示和交互的 3D 模型、纹理、动画和其他相关资源。USDZ 格式通常用于在 iOS 设备上展示增强现实内容,例如在 ARKit 应用中加载和显示虚拟物体或场景。

通过使用 USDZ 格式,开发者可以将高质量的 3D 模型和相关资源打包成一个文件,方便在 iOS 设备上进行快速加载和展示。这种格式的优势在于其高度集成化,可以将所有必要的资源打包在一起,减少了加载和显示时的复杂性。

总的来说,USDZ 是一种用于在 iOS 设备上展示增强现实内容的文件格式,为开发者提供了一种便捷的方式来创建和展示虚拟现实体验。

优点:使用简单通过链接点击的方式打开支持.USDZ的苹果设备即可预览AR效果

缺点: 兼容性差,只支持苹果相关设备,无法在安卓端使用,需要用两套模型文件来展示 .glb 模型渲染展示,.usdz 模型AR效果展示

一、threejs 获取视频文件流实现AR增强效果

主要思路:
  1. 通过js: navigator.mediaDevices.getUserMedia 获取获取摄像头视频文件流
  2. 添加一个 video 标签用于播放获取到的视频文件流
  3. 使用 threejs VideoTexture API 将视频转换为可展示的视频贴图
  4. 将转换后的视频贴图(VideoTexture)设置为 threejs 场景中的背景图
效果:

实现代码:
  1. 这里在创建场景时会先判断当前设备是PC端还是移动端,然后根据是否是移动设备来决定开启前置摄像头还是后置摄像头

  2. 主因为大部分手机的前置摄像头和后置摄像头的像素比例不一样,在使用前置摄像头时播放的视频可能会出现拉伸的场景,这里需要根据前后摄像头来设置不同的视频高宽比例

  3. 同时还需要考虑当前浏览器设备是否支持 navigator.mediaDevices.getUserMedia 以及当前设备摄像头是否损失等多种场景,这里统一使用 try catch 来处理如果有错误表明当前设备是不支持的 ,同时给用户 一个反馈提示 ElMessage.error("当前设备摄像头无法使用,请检查");

  // 创建场景
  async initScene() {
    this.scene = new THREE.Scene();
    const isMobile = () => {
      return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
    };
    const facingMode = isMobile() ? "environment" : "user"; //environment
    const video = document.getElementById("video");
    const { clientWidth, clientHeight } = this.container;

    const environment = { video: { width: clientHeight, height: clientWidth, facingMode } };
    const user = {
      video: { width: clientWidth, height: clientHeight, facingMode }
    };
    const constraints = facingMode == "environment" ? environment : user;
    try {
      const stream = await navigator.mediaDevices.getUserMedia(constraints);
      video.srcObject = stream;
      video.play();
      const texture = new THREE.VideoTexture(video);
      texture.minFilter = THREE.LinearFilter;
      texture.magFilter = THREE.LinearFilter;
      texture.colorSpace = THREE.SRGBColorSpace;
      texture.mapping = THREE.UVMapping;
      this.scene.background = texture;
      texture.update();
    } catch (err) {
      
      ElMessage.error("当前设备摄像头无法使用,请检查");
    }
  }
完整的实现代码(vu3项目):
  1. 页面代码
<template>
  <div id="vr-page">
    <div id="vr-model"></div> 
    <video id="video" playsinline muted autoplay></video>
  </div>
</template>
<script setup name="vrPage">
import { onMounted } from "vue";
import vrRenderModel from "@/utils/vrRenderModel";
onMounted(()=>{
   const model = new vrRenderModel('#vr-model' )
   model.init()
})
</script>
<style lang="scss" scoped>
#vr-page {
  position: relative;
  width: 100%;
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  #vr-model {
    width: 100%;
    height: 100%;
  }
}
#video {
  display: none;
  object-fit: cover;
}
</style>
  1. threejs 渲染加载代码 vrRenderModel.js
import * as THREE from "three";
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader";
import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader";
import { STLLoader } from "three/examples/jsm/loaders/STLLoader";
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
//导入控制器模块,轨道控制器
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { ElMessage } from "element-plus";

class vrRenderModel {
  constructor(selector) {
    this.container = document.querySelector(selector);
    // 相机
    this.camera;
    // 场景
    this.scene = null;
    //渲染器
    this.renderer;
    // 控制器
    this.controls;
    // 模型
    this.model;
    //文件加载器类型
    this.fileLoaderMap = {
      glb: new GLTFLoader(),
      fbx: new FBXLoader(this.loadingManager),
      gltf: new GLTFLoader(),
      obj: new OBJLoader(this.loadingManager),
      stl: new STLLoader()
    };
    // 模型上传进度条回调函数
    this.modelProgressCallback = e => e;
    // 环境光
    this.ambientLight;
    // 平行光
    this.directionalLight;
  }
  init() {
    return new Promise(async (resolve, reject) => {
      //初始化渲染器
      this.initRender();
      //初始化相机
      this.initCamera();
      //初始化场景
      this.initScene();
      this.addEvenListMouseListener();
      // 创建控制器
      this.initControls();
      // 创建灯光
      this.createLight();
      //  public 文件夹下面的文件
      const load = await this.setModel({
        filePath: "threeFile/glb/glb-27.glb",
        fileType: "glb",
      });
      resolve(true);
      this.sceneAnimation();
    });
  }
  // 创建渲染器
  initRender() {
    this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, preserveDrawingBuffer: true }); //设置抗锯齿

    //设置屏幕像素比
    this.renderer.setPixelRatio(window.devicePixelRatio);
    //渲染的尺寸大小
    const { clientHeight, clientWidth } = this.container;
    this.renderer.setSize(clientWidth, clientHeight);
    //色调映射
    this.renderer.toneMapping = THREE.ReinhardToneMapping;
    this.renderer.autoClear = true;
    this.renderer.outputColorSpace = THREE.SRGBColorSpace;
    //曝光
    this.renderer.toneMappingExposure = 2;
    this.renderer.shadowMap.enabled = true;
    this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    this.container.appendChild(this.renderer.domElement);
  }
  // 创建相机
  initCamera() {
    const { clientHeight, clientWidth } = this.container;
    this.camera = new THREE.PerspectiveCamera(50, clientWidth / clientHeight, 1, 2000);
  }
  // 创建场景
  async initScene() {
    this.scene = new THREE.Scene();

    const isMobile = () => {
      return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
    };
    const facingMode = isMobile() ? "environment" : "user"; //environment
    const video = document.getElementById("video");
    const { clientWidth, clientHeight } = this.container;

    const environment = { video: { width: clientHeight, height: clientWidth, facingMode } };
    const user = {
      video: { width: clientWidth, height: clientHeight, facingMode }
    };
    const constraints = facingMode == "environment" ? environment : user;
    try {
      const stream = await navigator.mediaDevices.getUserMedia(constraints);
      video.srcObject = stream;
      video.play();
      const texture = new THREE.VideoTexture(video);
      texture.minFilter = THREE.LinearFilter;
      texture.magFilter = THREE.LinearFilter;
      texture.colorSpace = THREE.SRGBColorSpace;
      texture.mapping = THREE.UVMapping;
      this.scene.background = texture;
      texture.update();
    } catch (err) {
      // alert(err);
      ElMessage.error("当前设备摄像头无法使用,请检查");
    }
  }
  // 加载模型
  setModel({ filePath, fileType }) {
    return new Promise((resolve, reject) => {
      let loader;
      if (["glb", "gltf"].includes(fileType)) {
        const dracoLoader = new DRACOLoader();
        dracoLoader.setDecoderPath(`draco/gltf/`);
        dracoLoader.setDecoderConfig({ type: "js" });
        dracoLoader.preload();
        loader = new GLTFLoader().setDRACOLoader(dracoLoader);
      } else {
        loader = this.fileLoaderMap[fileType];
      }
      // 根据不同的文件类型加载不同的文件
      loader.load(
        filePath,
        result => {
          switch (fileType) {
            case "glb":
              this.model = result.scene;
              break;
            case "fbx":
              this.model = result;
              break;
            case "gltf":
              this.model = result.scene;
              break;
            case "obj":
              this.model = result;
              break;
            case "stl":
              const material = new THREE.MeshStandardMaterial();
              const mesh = new THREE.Mesh(result, material);
              this.model = mesh;
              break;
            default:
              break;
          }
          this.setModelPositionSize();

          // 需要辉光的材质
          this.scene.add(this.model);
          resolve(true);
        },
        xhr => {
          this.modelProgressCallback(xhr.loaded);
        },
        err => {
          ElMessage.error("文件错误");
          console.log(err);
          resolve(true);
        }
      );
    });
  }

  // 模型加载进度条回调函数
  onProgress(callback) {
    if (typeof callback == "function") {
      this.modelProgressCallback = callback;
    }
  }
  // 设置模型定位缩放大小
  setModelPositionSize() {
    this.model.traverse(v => {
      if (v.isMesh && v.material) {
        v.castShadow = true;
        v.frustumCulled = false;
      }
    });
    //设置模型位置
    this.model.updateMatrixWorld();
    const box = new THREE.Box3().setFromObject(this.model);
    const size = box.getSize(new THREE.Vector3());
    const center = box.getCenter(new THREE.Vector3());
    // 计算缩放比例
    const maxSize = Math.max(size.x, size.y, size.z);
    const targetSize = 1; // 目标大小
    const scale = targetSize / (maxSize > 1 ? maxSize : 0.5);
    this.model.scale.set(scale, scale, scale);
    // 设置模型位置
    this.model.position.sub(center.multiplyScalar(scale));
    // 设置控制器最小缩放值
    this.controls.maxDistance = size.length() * 10;
    // 设置相机位置
    this.camera.position.set(0, 2, 6);
    // 设置相机坐标系
    this.camera.updateProjectionMatrix();
  }
  // 更新场景
  sceneAnimation() {
    this.renderAnimation = requestAnimationFrame(() => this.sceneAnimation());
    // 等模型加载和相关数据处理完成在执行
    this.controls.update();
    this.renderer.render(this.scene, this.camera);
  }
  // 监听事件
  addEvenListMouseListener() {
    //监听场景大小改变,跳转渲染尺寸
    this.onWindowResizesListener = this.onWindowResizes.bind(this);
    window.addEventListener("resize", this.onWindowResizesListener);
  }
  // 监听窗口变化
  onWindowResizes() {
    if (!this.container) return false;
    const { clientHeight, clientWidth } = this.container;
    //调整屏幕大小
    this.camera.aspect = clientWidth / clientHeight;
    //相机更新矩阵,将3d内容投射到2d面上转换
    this.camera.updateProjectionMatrix();
    this.renderer.setSize(clientWidth, clientHeight);
  }
  // 创建控制器
  initControls() {
    this.controls = new OrbitControls(this.camera, this.renderer.domElement);
    this.controls.enablePan = true;
    this.controls.enableDamping = true;
    this.controls.target.set(0, 0, 0);
    this.controls.update();
  }
  // 创建灯光
  createLight() {
    // 创建环境光
    this.ambientLight = new THREE.AmbientLight("#fff", 3);
    this.scene.add(this.ambientLight);
    // 创建平行光
    this.directionalLight = new THREE.DirectionalLight("#fff", 5);
    this.directionalLight.position.set(1, 1, 1);
    this.directionalLight.castShadow = true;
    this.scene.add(this.directionalLight);

    const planeGeometry = new THREE.PlaneGeometry(2000, 2000);
    const planeMaterial = new THREE.ShadowMaterial({ color: 0x000000, opacity: 0.5 });

    const plane = new THREE.Mesh(planeGeometry, planeMaterial);
    plane.rotation.x = -Math.PI / 2;

    plane.position.set(0, -1.2, 0);
    plane.receiveShadow = true;
    plane.material.side = THREE.DoubleSide;
    plane.material.color.set("#23191F");
    plane.geometry.verticesNeedUpdate = true;

    this.scene.add(plane);
  }
}

export default vrRenderModel;

二、谷歌(google) model-viewer 实现AR增强效果:

实现方式:

主要是两种方式:

1.通过引入外链cdn model-viewer.min.js 实现


<script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/3.5.0/model-viewer.min.js"></script>

<model-viewer alt="Neil Armstrong's Spacesuit from the Smithsonian Digitization Programs Office and National Air and Space Museum" src="shared-assets/models/NeilArmstrong.glb" ar environment-image="shared-assets/environments/moon_1k.hdr" poster="shared-assets/models/NeilArmstrong.webp" shadow-intensity="1" camera-controls touch-action="pan-y"></model-viewer>

2.通过 npm/cnpm安装 @google/model-viewer 实现

注意:如果npm安装失败,建议使用cnpm 或者 cdn外链的方式

npm install @google/model-viewer

import "@google/model-viewer"
效果:

实现代码(vue3项目)
  1. 通过引入 import "@google/model-viewer"
  2. model-viewer标签加一个样式类使其100%展示
  3. 如果当前设备支持AR效果,画布的右下方会有一个按钮,点击按钮即可开启AR效果
  4. 注意文件跨域问题
 <template>
  <div id="vr-page">
    <model-viewer
      class="model-viewer"
      src="/public/threeFile/glb/glb-27.glb"
      ar
      shadow-intensity="1"
      camera-controls
      touch-action="pan-y"
    ></model-viewer>
  </div>
</template>
<script setup name="vrPage">
 import "@google/model-viewer";
</script>
<style lang="scss" scoped>
#vr-page {
  position: relative;
  width: 100%;
  height: 100vh;
}
.model-viewer {
  width: 90%;
  height: 90%;
}
</style>

ok 是的你没有看错就是这么简单的26行代码即可实现AR效果

其他

以上只是一个简单的模型加载示例,model-viewer 还提供了其他的扩展方法如:自定义AR按钮,和点击监听事件,播放模型动画等。 具体可参考:modelviewer.dev/examples/au… 注意科学上网

三、苹果 .USDZ文件实现AR增强效果

实现方式:
  1. 通过将 .glb 格式的模型文件转化为苹果设备支持的 .usdz格式的模型文件
  2. 模型格式转换网站:three3d-0gte3eg619c78ffd-1301256746.tcloudbaseapp.com/threejs-3dm… 右上角下载/导出按钮

image.png 4. 通过在Safari 使用html 的 a 标签 href属性 打开 .usdz文件

效果:

实现代码(vue3项目):
  1. 封装一个专门用来打开.usdz文件 的方法 activateAR
  2. 通过调用 activateAR 方法实现AR增强效果
  3. 注意:该方法代码只在苹果的 Safari 浏览器中生效
<template>
  <div id="vr-page">
  </div>
</template>
<script setup name="vrPage">
import { activateAR } from "@/utils/iosARFile";
import { onMounted } from "vue";
onMounted(()=>{
  activateAR({
    iosSrc: "https://three3d-0gte3eg619c78ffd-1301256746.tcloudbaseapp.com/2024_9_7 21_54_12.usdz"
  });
})
</script>

iosARFile.js代码


/**
 * 打开苹果设备AR效果
 *
 * @param {props} Object 传入的属性值
 * @param {listener} function 监听事件
 * @return null
 */
export const activateAR = (props, listener) => {
  const anchor = document.createElement("a");
  anchor.setAttribute("href", `${props.iosSrc}#`);
  anchor.style.display = "none";
  anchor.appendChild(document.createElement("img"));
  anchor.rel = "ar";
  if (listener) {
    document.body.append(anchor);
    anchor.addEventListener(
      "message",
      event => {
        if (event.data == "_apple_ar_quicklook_button_tapped") {
          listener.dispatchEvent(new CustomEvent("quick-look-button-tapped"));
        }
      },
      false
    );
  }
  anchor.click();
};


四、其他的AR增强效果 AR.JS

AR.JS 是一个轻量级 JavaScript 库,允许用户将增强现实集成到 Web 应用程序中。 这个开源库是一个利用 Three.js、A-Frame 和 jsartoolkit 的纯 Web 解决方案。 它适用于任何支持 webgl 和 webrtc 的设备。 AR.JS支持以下AR类型;

  • 图像跟踪 - 识别 2D 图像并显示相关 AR 内容
  • 基于位置的 AR - 根据位置显示 AR 内容
  • 标记跟踪 - 使用 QR 码等标记来显示相关的 AR 内容。

github:github.com/AR-js-org/A…

中文文档:ar.cnxfs.com.cn/