上手 Rokid JSAR:新手也能敏捷入门的 AR 开发之旅

在这里插入图片描述

个人主页:Eternity._
期待您的关注

在这里插入图片描述
在这里插入图片描述

前言:作为 Rokid 针对轻量化 AR 场景推出的 JavaScript 开发方案,JSAR 最大的优势在于 “低门槛”:无需厚重的原生开发功底,只需掌握基础的前端知识,就能基于 Web 环境搭建 AR 交互功能;更重要的是,它对 Rokid AR 眼镜等硬件设备的适配做了深度优化,省去了开发者反复调试设备兼容性的麻烦。

这篇文章就想记录下这次 Rokid JSAR 开发初体验的全过程,如需了解更多详细的过程可前往

JSAR


初始JSAR


首先让我们先来了解一下什么是JSAR,JSAR 基于 Web 技术,能让开发者借助熟悉的前端工具链(比如 npm、模块打包器等)来构建 AR 应用,并且对于 Rokid 自家的 AR 眼镜等设备做了深度适配,无需额外操心硬件兼容问题。JSAR 也可以运行基于 Web APIs 实现的 JavaScript / TypeScript 代码,同时也可以运行 WebAssembly。做到空间开发。同时关于更多的JSAR的介绍Rokid官方网址上也有介绍
[图片]

Rokid 与 JSAR的深度绑定


Rokid JSAR 并非孤立的 AR 开发工具,其核心功能的设计与落地,始终围绕 Rokid 从硬件设备到软件生态的全链路布局展开。这种 “技术适配硬件、生态支撑开发” 的绑定关系,既让 JSAR 具备了差异化的功能优势,也成为 Rokid 构建 AR 开发者生态的核心纽带。

  • Rokid 的 AR 硬件矩阵是 JSAR 核心功能得以实现的 “物理载体”,JSAR 几乎所有空间交互能力都依赖于设备端的硬件支撑,两者形成了 “软件调用能力、硬件提供动力” 的协同模式。
  • 在 Rokid 的应用生态中,JSAR 承担着 “空间小程序开发入口” 的核心角色,其功能设计完全服务于 Rokid 对 “轻量化 AR 应用生态” 的布局。

JSAR的强大功能


在深入体验 Rokid JSAR 的开发过程中,其功能体系的 “专业性” 与 “易用性” 形成的奇妙平衡让人印象深刻。不同于简单的 AR 效果插件,JSAR 构建了一套覆盖从场景搭建到部署运维的完整功能矩阵,既封装了 AR 开发的复杂底层逻辑,又保留了前端开发者熟悉的技术范式,让轻量化开发也能实现专业级 AR 效果。同时JSAR 创新性地将 Web 交互逻辑延伸至 3D 空间,通过 JSAR-DOM 构建了一套 “空间文档对象模型”,让前端开发者能沿用熟悉的方式处理虚拟交互。

配置开发环境


关于开发环境的搭建,我这里根据官方文档的搭建过程分为了3个步骤

1. 安装 Visual Studio Code
2. 安装 Node.js
3. 安装 JSAR DevTools

安装 Visual Studio Code


首先我们进入 Visual Studio Code 的官方网站,同时留意好软件的版本,我们所需的版本必须大于等于1.80.0,这个很重要,我们进入网站直接下载最新版即可!
[图片]

安装 Node.js


我们同样的点击进入 Node.js 的下载网站,直接点击 Get 即可,在安装完成后,我们可以打开终端去查看安装完成之后的Node.js和npm的版本

node -v
npm -v

[图片]

安装 JSAR DevTools


安装 JSAR DevTools 有两种方法,我这里更推荐直接使用 Visual Studio Code 去拓展中直接下载,这种直接明了,直接安装即可
[图片]

JSAR项目开发流程


项目初始化


根据官方文档解析,项目初始化一共有两种方法:

1. 通过 npm 创建
2. 通过 GitHub Template 创建

通过 npm 创建


使用 npm 快速创建,只需要打开终端输入:

npm init @yodaos-jsar/widget

输入完成后等待,按照提示进行操作,等待工具自动拉取最新的项目模板M-CreativeLab/template-for-jsar-widget来初始化,同时会获得一个packge.json,初始化成功后,我们需要进入对应目录安装依赖

npm install  # 提供代码补全

通过 GitHub Template 创建


使用 GitHub Template 创建,我们需要进入M-CreativeLab/template-for-jsar-widget 使用模板创建一个新的项目,填写对用信息,创建完成后,同样是一个JSAR空间小程序项目
[图片]

以下是官方提供的两个参考示例

同时官方指出如果通过 GitHub 项目创建的 JSAR 空间小程序,推荐大家给项目添加 “jsar-widget” 的主题(Topic),这样可以方便 JSAR 开发团队以及社区在 GitHub #jsar-widget 上发现我们的项目

项目结构解析


1. packge.json
2. main.xsml

在 Rokid JSAR 开发中,packge.json 和 main.xsml 是项目的 “基石文件”—— 前者负责管理项目依赖与构建配置,是前端工程化的核心;后者作为空间场景的入口文件,定义了 AR 交互的核心结构。两者分工明确又相互配合,共同支撑起 JSAR 项目的运行与开发。

  • packge.json
{
"name": "jsar-Rokid", // 名称
"displayName": "Display Name",
"version": "1.0.0",  // 版本
"description": "The template widget", // 描述
"main": "main.xsml",  // 指向 xsml 入口
"dependencies": {
"three": "^0.180.0" // 类型支持
}
}

这些配置会被 JSAR 构建工具和设备运行时读取,用于优化项目的兼容性与运行效率

  • main.xsml
    main.xsml是 JSAR 项目的空间场景入口文件,相当于普通前端项目的 index.html,但核心作用是 “定义 3D 空间结构与虚实交互的初始状态”
    main.xsml的 标签负责引入外部资源(JS 脚本、CSS 样式、3D 模型),并建立 “空间元素” 与 “交互逻辑” 的关联:
    • 引入的 index.js 脚本中,可通过 JSAR-DOM API 操作 main.xsml 中定义的 carmodel infopanel 等元素;

项目运行预览


vscode本地预览
在vscode中提供了本地预览场景,我们只需要打开main.xsml文件,点击右上角的【场景视图】按钮
[图片]

在打开场景视图后,有两个功能按钮:

    • 重置位置,将场景视图的位置重置到原点
    • 刷新,重新加载场景视图
      可以在需要时,点击这两个按钮。另外,当我们通过编辑器修改了项目文件时,场景视图会自动刷新。

Web 浏览器
其实JSAR 提供了一个在浏览器中即可打开的场景视图,来运行你的项目,但是需要在本地启动一个 http 服务

npm install serve -g

安装serve 后,进入你的项目目录,运行:

serve -p 8080 --cors
https://m-creativelab.github.io/jsar-dom/?url=http://你的IP:端口/main.xsml

进入之后,点击「Enter AR」按钮,即可在浏览器中模拟 Rokid 设备的 AR 沉浸式体验。
[图片]

Rokid JSAR 开发初体验


抱着 “试试看” 的心态,我从环境搭建开始,一步步尝试加载 3D 模型、实现基础交互,过程中既有 “原来 AR 开发可以这么轻量” 的惊喜,也踩过几个新手常遇的小坑。
[图片]

  • lib中存放了 TypeScript源码,核心渲染
  • model中存放了 3d模型 红色小汽车
  • main.html:前端页面,整合入口与脚本加载

核心代码展示


// 简单的 three.js 初始化与 glTF (GLB) 加载器
// 目标:作为项目的最小可用实现 — 若要实际运行,请先 `npm install` three
import type {
WebGLRenderer,
Scene,
PerspectiveCamera,
OrbitControls,
Object3D,
HemisphereLight,
DirectionalLight,
GridHelper,
Vector3,
Box3
} from 'three';
import type { GLTF } from 'three/examples/jsm/loaders/GLTFLoader';
export type InitOptions = {
canvas?: HTMLCanvasElement | string;
modelUrl?: string;
background?: string;
};
let THREE: typeof import('three') | null = null;
let renderer: WebGLRenderer | null = null;
let scene: Scene | null = null;
let camera: PerspectiveCamera | null = null;
let controls: OrbitControls | null = null;
let model: Object3D | null = null;  // 保存当前加载的模型引用
let hemiLight: HemisphereLight | null = null;
let dirLight: DirectionalLight | null = null;
let gridHelper: GridHelper | null = null;
/**
* 初始化Three.js场景、相机和渲染器
*/
export async function init(opts: InitOptions = {}) {
// 清理可能存在的旧实例
dispose();
try {
// 动态导入three.js核心库
THREE = await import('three');
} catch (error) {
console.warn('Three.js 未安装。请运行 `npm install three` 以启用3D预览功能。', error);
return;
}
const { canvas, background = '#222' } = opts;
const canvasEl = getCanvasElement(canvas);
// 创建渲染器
renderer = new THREE.WebGLRenderer({
canvas: canvasEl,
antialias: true,
powerPreference: 'high-performance'
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); // 限制最大像素比以提高性能
renderer.setClearColor(background);
// 创建场景
scene = new THREE.Scene();
// 创建相机
camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.set(0, 1.5, 3);
// 添加光源
addLights();
// 添加网格辅助线
gridHelper = new THREE.GridHelper(10, 10);
scene.add(gridHelper);
// 添加窗口大小调整事件监听
window.addEventListener('resize', onResize);
// 初始化控制器
await initControls();
// 启动动画循环
animate();
// 如果提供了模型URL,则加载模型
if (opts.modelUrl) {
try {
await loadModelFromUrl(opts.modelUrl);
} catch (error) {
console.error('模型加载失败:', error);
}
}
}
/**
* 从URL加载GLTF/GLB模型
*/
export async function loadModelFromUrl(
url: string,
onProgress?: (percent: number) => void,
onError?: (err: Error) => void
): Promise<GLTF> {
  if (!THREE) {
  throw new Error('Three.js 尚未初始化,请先调用 init() 方法');
  }
  try {
  // 动态导入GLTFLoader
  const { GLTFLoader } = await import('three/examples/jsm/loaders/GLTFLoader.js');
  const loader = new GLTFLoader();
  return new Promise((resolve, reject) => {
  loader.load(
  url,
  (gltf) => {
  // 移除现有模型
  if (model && scene) {
  scene.remove(model);
  }
  // 添加新模型到场景
  scene?.add(gltf.scene);
  model = gltf.scene;
  // 处理模型位置和相机视角
  setupModelAndCamera(gltf.scene);
  resolve(gltf);
  },
  (progressEvent) => {
  if (onProgress && progressEvent.lengthComputable) {
  const percent = Math.round((progressEvent.loaded / progressEvent.total) * 100);
  onProgress(percent);
  }
  },
  (error) => {
  const err = error instanceof Error ? error : new Error(String(error));
  onError?.(err);
  reject(err);
  }
  );
  });
  } catch (error) {
  const err = error instanceof Error ? error : new Error(String(error));
  onError?.(err);
  throw err;
  }
  }
  /**
  * 动画循环
  */
  function animate() {
  requestAnimationFrame(animate);
  // 更新控制器
  controls?.update();
  // 模型浮动动画
  if (model) {
  const bobbingHeight = 0.1;
  const baseY = model.userData.baseY || 0;
  model.position.y = baseY + Math.sin(Date.now() * 0.005) * bobbingHeight;
  }
  // 渲染场景
  if (renderer && scene && camera) {
  renderer.render(scene, camera);
  }
  }
  /**
  * 窗口大小调整处理
  */
  function onResize() {
  if (!camera || !renderer) return;
  const width = window.innerWidth;
  const height = window.innerHeight;
  camera.aspect = width / height;
  camera.updateProjectionMatrix();
  renderer.setSize(width, height);
  }
  /**
  * 初始化轨道控制器
  */
  async function initControls() {
  if (!THREE || !camera || !renderer) return;
  try {
  const { OrbitControls } = await import('three/examples/jsm/controls/OrbitControls.js');
  controls = new OrbitControls(camera, renderer.domElement);
  controls.enableDamping = true;
  controls.dampingFactor = 0.05;
  controls.screenSpacePanning = false;
  controls.minDistance = 0.1;
  controls.maxDistance = 500;
  } catch (error) {
  console.warn('无法加载OrbitControls,交互控制将不可用', error);
  controls = null;
  }
  }
  /**
  * 添加场景光源
  */
  function addLights() {
  if (!THREE || !scene) return;
  // 半球光
  hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 1.0);
  scene.add(hemiLight);
  // 方向光
  dirLight = new THREE.DirectionalLight(0xffffff, 1);
  dirLight.position.set(5, 10, 7.5);
  scene.add(dirLight);
  }
  /**
  * 设置模型位置和相机视角
  */
  function setupModelAndCamera(model: Object3D) {
  if (!THREE || !camera) return;
  // 计算模型包围盒
  const box = new THREE.Box3().setFromObject(model);
  const center = box.getCenter(new THREE.Vector3());
  const size = box.getSize(new THREE.Vector3());
  const radius = size.length() * 0.5;
  // 将模型居中到原点
  model.position.sub(center);
  // 调整模型位置使其底部贴合地面
  const bbox = new THREE.Box3().setFromObject(model);
  const minY = bbox.min.y;
  if (minY < 0) {
  const baseY = -minY;
  model.position.y = baseY;
  model.userData.baseY = baseY; // 保存基础高度供动画使用
  }
  // 调整相机位置以完整显示模型
  const fov = camera.fov * (Math.PI / 180); // 转换为弧度
  const distance = radius / Math.sin(fov / 2) || radius * 2;
  // 放置相机到模型前上方
  camera.position.set(0, radius * 0.6, distance * 1.2);
  camera.lookAt(new THREE.Vector3(0, 0, 0));
  camera.updateProjectionMatrix();
  }
  /**
  * 获取Canvas元素
  */
  function getCanvasElement(canvas?: HTMLCanvasElement | string): HTMLCanvasElement | undefined {
  if (!canvas) return undefined;
  if (typeof canvas === 'string') {
  const element = document.querySelector<HTMLCanvasElement>(canvas);
    if (!element) {
    console.warn(`未找到选择器为 "${canvas}" 的Canvas元素`);
    return undefined;
    }
    return element;
    }
    return canvas;
    }
    /**
    * 清理Three.js资源
    */
    export function dispose() {
    // 移除事件监听
    window.removeEventListener('resize', onResize);
    // 清理控制器
    if (controls) {
    controls.dispose();
    controls = null;
    }
    // 清理场景中的对象
    if (scene) {
    scene.remove(hemiLight as Object3D);
    scene.remove(dirLight as Object3D);
    scene.remove(gridHelper as Object3D);
    scene.remove(model as Object3D);
    }
    // 清理渲染器
    if (renderer) {
    try {
    renderer.dispose();
    // 强制释放WebGL上下文(如果支持)
    if (typeof renderer.forceContextLoss === 'function') {
    renderer.forceContextLoss();
    }
    } catch (error) {
    console.warn('渲染器清理过程中发生错误:', error);
    }
    renderer = null;
    }
    // 重置所有变量
    scene = null;
    camera = null;
    model = null;
    hemiLight = null;
    dirLight = null;
    gridHelper = null;
    THREE = null;
    }
<!DOCTYPE html>
    <html lang="zh-CN">
    <head>
      <meta charset="utf-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>3D 模型展示</title>
      <!-- 引入 Tailwind CSS -->
      <script src="https://cdn.tailwindcss.com"></script>
        <!-- 引入 Font Awesome 图标 -->
            <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
            <!-- Import map: 映射裸模块名到本地模块,便于直接在浏览器中使用 node_modules 的 ES module -->
                <script type="importmap">
                {
                "imports": {
                "three": "/node_modules/three/build/three.module.js",
                "three/": "/node_modules/three/"
                }
                }
              </script>
                <style type="text/tailwindcss">
                @layer utilities {
                .content-auto {
                content-visibility: auto;
                }
                .progress-transition {
                transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
                }
                }
              </style>
            </head>
              <body class="bg-gray-900 text-white">
                <div id="app" class="flex flex-col h-screen">
                <!-- 控制栏 -->
                    <div id="controls" class="bg-gray-800 px-4 py-3 flex items-center justify-between shadow-lg">
                      <div class="flex items-center space-x-3">
                        <button id="loadBtn" class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-md transition-colors duration-200 flex items-center">
                      <i class="fas fa-download mr-2"></i> 加载示例模型
                      </button>
                      <input
                        id="modelUrl"
                        placeholder="输入 glb URL 或留空使用 ./model/red_car.glb"
                        class="bg-gray-700 text-white border border-gray-600 rounded-md px-3 py-2 w-64 focus:outline-none focus:ring-2 focus:ring-blue-500"
                      />
                    </div>
                      <div class="flex items-center space-x-4">
                    <span id="status" class="text-blue-300 font-medium">就绪</span>
                        <div id="progressBar" class="w-48 h-2 bg-gray-700 rounded-full overflow-hidden">
                      <div id="progressFill" class="h-full bg-blue-500 progress-transition" style="width: 0%"></div>
                      </div>
                    </div>
                  </div>
                  <!-- 错误提示 -->
                  <div id="errorMsg" class="bg-red-900 text-red-300 px-4 py-2 hidden">加载出错</div>
                    <!-- 模型展示区域 -->
                        <div id="viewer" class="flex-1 relative">
                      <canvas id="glCanvas" class="w-full h-full"></canvas>
                        <!-- 加载中遮罩(可选,可根据需要显示) -->
                            <div id="loadingOverlay" class="absolute inset-0 bg-gray-900/70 flex items-center justify-center hidden">
                              <div class="text-center">
                            <div class="inline-block animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
                            <p class="mt-3 text-blue-300">模型加载中...</p>
                            </div>
                          </div>
                        </div>
                      </div>
                        <script type="module">
                        // 导入Three.js初始化和模型加载函数
                        import { init, loadModelFromUrl } from './lib/index.js';
                        // 初始化渲染器并挂载到canvas元素
                        init({
                        canvas: '#glCanvas',
                        background: '#0b1220'
                        });
                        // 获取DOM元素引用
                        const statusEl = document.getElementById('status');
                        const progressFill = document.getElementById('progressFill');
                        const errorEl = document.getElementById('errorMsg');
                        const modelUrlInput = document.getElementById('modelUrl');
                        const loadingOverlay = document.getElementById('loadingOverlay');
                        // 加载按钮点击事件处理
                        document.getElementById('loadBtn').addEventListener('click', async () => {
                        // 获取模型URL,使用默认值如果输入为空
                        const url = modelUrlInput.value.trim() || './model/red_car.glb';
                        // 显示加载遮罩
                        loadingOverlay.classList.remove('hidden');
                        // 重置UI状态
                        resetUI();
                        try {
                        // 加载模型并处理进度更新
                        await loadModelFromUrl(
                        url,
                        handleProgress,
                        handleError
                        );
                        // 加载成功更新UI
                        statusEl.textContent = '加载完成';
                        progressFill.style.width = '100%';
                        // 隐藏加载遮罩
                        setTimeout(() => {
                        loadingOverlay.classList.add('hidden');
                        }, 300);
                        } catch (error) {
                        // 捕获异常并显示错误信息
                        console.error('模型加载失败:', error);
                        handleError(error);
                        // 隐藏加载遮罩
                        loadingOverlay.classList.add('hidden');
                        }
                        });
                        /**
                        * 重置UI状态到初始状态
                        */
                        function resetUI() {
                        errorEl.classList.add('hidden');
                        errorEl.textContent = '';
                        statusEl.textContent = '开始加载...';
                        progressFill.style.width = '0%';
                        }
                        /**
                        * 处理加载进度更新
                        * @param {number} percent - 加载进度百分比
                        */
                        function handleProgress(percent) {
                        statusEl.textContent = `加载中 ${percent}%`;
                        progressFill.style.width = `${percent}%`;
                        }
                        /**
                        * 处理加载错误
                        * @param {Error} error - 错误对象
                        */
                        function handleError(error) {
                        errorEl.classList.remove('hidden');
                        errorEl.textContent = `加载出错: ${error.message || String(error)}`;
                        statusEl.textContent = '加载错误';
                        }
                      </script>
                    </body>
                  </html>
{
"name": "jsar-Rokid",
"displayName": "Display Name",
"version": "1.0.0",
"description": "The template widget",
"main": "main.xsml",
"scripts": {
"build": "tsc",
"start": "npx http-server -c-1 ./ -p 8080"  // 新增start,默认使用http启动端口8080
},
"files": [
"icon.png",
"main.xsml",
"lib/*.ts",
"model/red_car.glb"
],
"icon3d": {
"base": "./model/red_car.glb"
},
"author": "",
"license": "Apache-2.0",
"devDependencies": {},
"dependencies": {
"three": "^0.180.0"
}
}
  <xsml version="1.0">
  <head>
  <title>JSAR Widget</title>
    <link id="model" rel="mesh" type="octstream/glb" href="./model/red_car.glb" />
  <script src="./lib/main.ts"></script>
  </head>
  <space>
    <mesh id="model" ref="model" selector="__root__" />
  </space>
</xsml>

项目运行浏览


  • 本地浏览
    [图片]

  • Web 运行

  1. 安装依赖:npm install
  2. 编译TypeScript:npx tsc --project tsconfig.json
  3. 启动服务器:npm start
  4. 访问 Web 页面:http://127.0.0.1:8080/main.html

点击“加载示例模型”按钮,或输入自定义glb模型URL加载。
[图片]

[图片]

说真的,接触 JSAR 之前我还怕 AR 开发会很复杂,结果上手后发现,它完全是照着 “让前端少走弯路” 来设计的,优点真的很实在,开发效率也是真的高!加载 3D 模型?一行 标签就搞定,不用自己写模型解析逻辑;做空间交互?绑定个 click 事件就跟网页按钮一样简单,整个体验下来是非常不错的!点赞!

posted on 2025-11-06 09:52  blfbuaa  阅读(6)  评论(0)    收藏  举报