@
- JSAR
- 初始JSAR
- Rokid 与 JSAR的深度绑定
- JSAR的强大功能
- 配置开发环境
- 安装 Visual Studio Code
- 安装 Node.js
- 安装 JSAR DevTools
- JSAR项目开发流程
- 项目初始化
- 通过 npm 创建
- 通过 GitHub Template 创建
- 项目结构解析
- 项目运行预览
- Rokid JSAR 开发初体验
- 核心代码展示
- 项目运行浏览
前言:作为 Rokid 针对轻量化 AR 场景推出的 JavaScript 开发方案,JSAR 最大的优势在于 “低门槛”:无需厚重的原生开发功底,只需掌握基础的前端知识,就能基于 Web 环境搭建 AR 交互功能;更重要的是,它对 Rokid AR 眼镜等硬件设备的适配做了深度优化,省去了开发者反复调试设备兼容性的麻烦。
这篇文章就想记录下这次 Rokid JSAR 开发初体验的全过程,如需了解更多详细的过程可前往
- 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空间小程序项目
以下是官方提供的两个参考示例
- https://github.com/M-CreativeLab/jsar-gallery-solar-system
- https://github.com/M-CreativeLab/jsar-gallery-flatten-lion
同时官方指出如果通过 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` threeimport 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 {// 动态导入GLTFLoaderconst { 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);// 加载成功更新UIstatusEl.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 运行
- 安装依赖:npm install
- 编译TypeScript:npx tsc --project tsconfig.json
- 启动服务器:npm start
- 访问 Web 页面:http://127.0.0.1:8080/main.html
点击“加载示例模型”按钮,或输入自定义glb模型URL加载。
说真的,接触 JSAR 之前我还怕 AR 开发会很复杂,结果上手后发现,它完全是照着 “让前端少走弯路” 来设计的,优点真的很实在,开发效率也是真的高!加载 3D 模型?一行