
🌹🌹期待您的关注 🌹🌹

❀ Rokid JSAR
前言:作为 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的版本
bash
node -v
npm -v

安装 JSAR DevTools
安装 JSAR DevTools 有两种方法,我这里更推荐直接使用 Visual Studio Code 去拓展中直接下载,这种直接明了,直接安装即可
JSAR项目开发流程
项目初始化
根据官方文档解析,项目初始化一共有两种方法:
1. 通过 npm 创建 2. 通过 GitHub Template 创建
通过 npm 创建
使用 npm 快速创建,只需要打开终端输入:
bash
npm init @yodaos-jsar/widget
输入完成后等待,按照提示进行操作,等待工具自动拉取最新的项目模板M-CreativeLab/template-for-jsar-widget来初始化,同时会获得一个packge.json,初始化成功后,我们需要进入对应目录安装依赖
bash
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
javascript
{
"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 服务
bash
npm install serve -g
安装serve 后,进入你的项目目录,运行:
bash
serve -p 8080 --cors
bash
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:前端页面,整合入口与脚本加载
核心代码展示
javascript
// 简单的 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;
}
html
<!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>
xml
{
"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 模型?一行 标签就搞定,不用自己写模型解析逻辑;做空间交互?绑定个 click 事件就跟网页按钮一样简单,整个体验下来是非常不错的!点赞!