详细介绍:Rokid JSAR 技术开发全指南+实战演练
一、JSAR 核心概念与 Rokid 适配基础
1.1 什么是 JSAR
JSAR(JavaScript Augmented Reality)是 Rokid 主导的空间小程序开发技术体系,本质为可嵌入物理空间的 Web 运行时,支持开发者使用 JavaScript、XSML(空间标记语言)等 Web 技术栈,构建能与真实环境融合的沉浸式 AR 应用。其核心价值在于降低 AR 开发门槛,让前端开发者无需掌握底层图形学技术,即可快速实现空间化交互体验。
1.2 JSAR 与 Rokid 设备的深度适配
Rokid 作为 JSAR 技术的核心落地载体,从硬件到软件提供全链路支持:
- 硬件适配:兼容 Rokid Glasses 系列、Rokid Max 等主流 AR 头显,利用设备的高精度空间定位、手势识别与近眼显示能力,实现虚拟内容与真实空间的精准对齐。
 - 跨设备拓展:依托 WebXR 标准,JSAR 应用可无缝运行于 Rokid、Pico、Apple Vision Pro 等多品牌 AR 设备,且支持 iPhone 等移动终端通过 WebXR Viewer 访问。
 - 工具链集成:提供专属 JSAR DevTools 与真机调试方案,优化 Rokid 设备上的渲染性能与交互响应速度。
 

1.3 核心技术组件
| 组件 | 功能说明 | 
|---|---|
| XSML | 空间标记语言,扩展 HTML 语法以描述 3D 空间结构,支持定义平面、模型等空间元素 | 
| JSAR-DOM | 空间文档对象模型,基于 Babylon.js 实现,提供空间元素的交互与渲染能力 | 
| JSAR DevTools | VS Code 插件,集成场景预览、代码补全、真机调试等核心开发功能 | 
| WebXR 适配层 | 支持沉浸式 AR 模式切换,兼容 Rokid 设备的空间定位与姿态追踪 | 
二、开发环境搭建(Rokid 官方标准流程)
2.1 环境依赖清单
| 依赖工具 | 版本要求 | 作用说明 | 
|---|---|---|
| Visual Studio Code | ≥ 1.80.0 | 代码编辑与插件运行载体 | 
| Node.js | ≥ 18.0.0 或最新 LTS 版本 | 依赖管理与项目构建 | 
| JSAR DevTools | 最新稳定版 | 场景预览与调试核心工具 | 
2.2 分步安装指南
步骤 1:安装基础工具
- Visual Studio Code:前往 VS Code 官网 下载对应系统版本,建议安装中文语言包提升开发效率。
 - Node.js:访问 Node.js 官网 下载 LTS 版本,安装后通过终端验证:
 
node -v # 需显示 v18.0.0 及以上
npm** -v # 配套 npm 版本通常 ≥ 8.0.0**

步骤 2:安装 JSAR DevTools
提供两种官方安装方式,推荐优先使用商店安装:
- 方式一:VS Code 商店安装
 
打开 VS Code 拓展面板,搜索 JSAR DevTools(插件 ID:RokidMCreativeLab.vscode-jsar-devtools),点击安装即可,安装链接:市场地址。
- 方式二:VSIX 包离线安装
 
- 下载最新安装包:vscode-jsar-devtools-latest.vsix
 - 打开 VS Code,按下 
Ctrl + Shift + P,输入Extensions: Install from VSIX...,选择下载的安装包完成安装。 
2.3 环境验证
安装完成后,打开 VS Code 右下角状态栏,若显示 JSAR DevTools: Ready,则说明工具激活成功。
三、JSAR 项目开发核心流程
3.1 项目初始化(两种官方方案)
方案 1:Npm 命令快速创建
- 打开终端,执行初始化命令:
 
npm init @yodaos-jsar/widget
- 按照交互提示输入项目名称、描述等信息,工具会自动拉取官方模板 M-CreativeLab/template-for-jsar-widget。
 - 进入项目目录,安装依赖:
 
cd 项目名称
npm install  # 安装类型定义文件,提供代码补全
方案 2:GitHub Template 创建
- 访问官方模板仓库:template-for-jsar-widget,点击 Use this template。
 - 填写仓库名称,创建新的 GitHub 项目(推荐添加 jsar-widget 主题标签,便于社区发现)。
 - 克隆项目到本地并安装依赖,示例项目可参考: 
 
- 太阳系模拟器:jsar-gallery-solar-system
 - 3D 模型展示:jsar-gallery-flatten-lion
 
 
3.2 项目核心结构解析
project-name/
├── main.xsml        # 入口文件,定义空间结构与逻辑
├── package.json     # 项目配置,需指定 main 为 main.xsml
├── icon.png         # 应用图标
├── model/           # 3D 模型资源(如 glb 格式)
│   └── foobar.glb
└── lib/             # 业务逻辑脚本
└── index.ts
package.json 关键配置:
{
"name": "rokid-jsar-demo",
"displayName": "JSAR Demo",
"main": "main.xsml",  // 必须指向 XSML 入口
"icon3d": { "base": "./model/foobar.glb" },  // 3D 图标配置
"devDependencies": { "@yodaos-jsar/types": "^1.4.0" }  // 类型支持
}
3.3 核心开发语法(XSML + JSAR-DOM)
- XSML 空间元素定义
 
XSML 扩展 HTML 语法,新增 <space> 标签描述 3D 空间,支持嵌套平面、模型等元素:
<xsml version="1.0">
  <head>
  <title>空间按钮示例</title>
    <script>
      // 获取空间元素并绑定事件
      const guiPlane = spatialDocument.getElementById('gui');
      const openButton = guiPlane.shadowRoot.getElementById('open-btn');
      openButton.addEventListener('mouseup', () => {
      console.log('按钮被点击');
      });
    </script>
  </head>
  <!-- 空间容器,所有 3D 元素需置于此标签内 -->
    <space>
      <!-- 创建交互平面 -->
          <plane id="gui" width="2" height="1" position="0 1.5 -3">
          <style>
            .btn {
            background: rgba(20,33,33,1);
            color: white;
            font-size: 50px;
            width: 200px;
            height: 100px;
            border-radius: 25px;
            }
          </style>
            <div id="root">
          <button id="open-btn" class="btn">点击我</button>
          </div>
        </plane>
      </space>
    </xsml>
- JSAR-DOM 核心 API
 
| API 方法 | 功能说明 | 
|---|---|
| spatialDocument.getElementById(id) | 获取空间元素,类似 HTML DOM 的 getElementById | 
| element.addEventListener(event, fn) | 绑定空间交互事件(mouseup、touchstart 等) | 
| spatialDocument.dispatchEvent(e) | 触发自定义事件,实现跨组件通讯 | 
3.4 场景预览与调试
1. 本地场景预览
- 在 VS Code 中打开项目的 main.xsml 文件。
 - 点击编辑器右上角的「场景视图」按钮(立体图形图标)。
 - 场景视图支持两种核心操作
重置位置:将场景恢复到原点坐标
刷新:代码修改后自动 / 手动重新加载场景
2. WebXR 浏览器调试
1.安装 Chrome 插件:Immersive Web Emulator。
2.上传项目到本地服务,通过以下 URL 访问: 
https://m-creativelab.github.io/jsar-dom/?url=http://你的IP:端口/main.xsml
3.点击「Enter AR」按钮,即可在浏览器中模拟 Rokid 设备的 AR 沉浸式体验。
3. Rokid 真机调试
- 确保开发机与 Rokid 设备处于同一局域网。
 - 在 JSAR DevTools 中选择「真机调试」,自动识别设备并部署应用。
 - 支持通过 Chrome DevTools 协议(CDP)进行断点调试与日志打印。
 

四 、实战演练-3D模型展示-小黄鸭模型的旋转和视图放大缩小
4.1 项目结构
搭建如下项目结构:
3d-model-showcase/
├── lib/                # TypeScript源码,核心渲染与逻辑
│   └── index.ts        # three.js主入口,场景/模型/控件初始化
├── model/              # 存放glb模型文件
│   └── sample.glb      # 示例鸭子模型
├── node_modules/       # npm依赖
├── types/              # TypeScript类型声明
│   └── three-examples.d.ts # three.js扩展类型声明
├── main.html           # 前端页面,入口UI与脚本加载
├── package.json        # 项目依赖与脚本
├── tsconfig.json       # TypeScript编译配置

1. package.json
{
"$schema": "https://json.schemastore.org/package",
"name": "3d-model-showcase",
"version": "1.0.0",
"description": "3D 模型展示最小示例(three.js + TypeScript)",
"main": "lib/index.ts",
"type": "module",
"scripts": {
"build": "tsc",
"start": "npx http-server -c-1 ./ -p 8080"
},
"author": "",
"license": "MIT",
"dependencies": {
"three": "^0.154.0"
},
"devDependencies": {
"@types/three": "^0.152.0",
"http-server": "^14.1.1",
"typescript": "^5.9.3"
}
}
记录依赖(如three、typescript、http-server)。
提供npm start脚本,启动本地静态服务器。

2. tsconfig.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "ES2020",
"module": "ES2020",
"moduleResolution": "node",
"strict": false,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"outDir": "./lib",
"declaration": true,
"allowJs": true,
"checkJs": false,
"lib": ["ES2020", "DOM", "DOM.Iterable"]
},
"include": ["src/**/*", "lib/**/*"],
"exclude": ["node_modules"]
}

3. main.html
<!-- main.xsml - 最小页面示例,包含一个全屏 canvas 用于 three.js 渲染 -->
  <!DOCTYPE html>
      <html lang="zh-CN">
      <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
      <title>3D 模型展示</title>
        <!-- Import map: 映射裸模块名到本地模块,便于直接在浏览器中使用 node_modules 的 ES module -->
            <script type="importmap">
            {
            "imports": {
            "three": "/node_modules/three/build/three.module.js",
            "three/": "/node_modules/three/"
            }
            }
          </script>
          <style>
            html,body { height:100%; margin:0; }
            #app { width:100%; height:100%; display:flex; flex-direction:column; }
            #viewer { flex:1; position:relative; }
            canvas { width:100%; height:100%; display:block; }
            #controls { padding:8px; background:#111; color:#fff; font-family: Arial, sans-serif }
            button { margin-right:8px }
          </style>
        </head>
        <body>
            <div id="app">
              <div id="controls">
            <button id="loadBtn">加载示例模型</button>
              <input id="modelUrl" placeholder="输入 glb URL 或留空使用 ./model/sample.glb" style="width:50%" />
            <span id="status" style="margin-left:12px;color:#9cc;">就绪</span>
            </div>
              <div id="progressBar" style="height:6px; background:#333; width:100%">
            <div id="progressFill" style="height:100%; width:0%; background:#3af;"></div>
            </div>
          <div id="errorMsg" style="color:#f66; padding:6px 8px; display:none; background:#2b0000"></div>
              <div id="viewer">
            <canvas id="glCanvas"></canvas>
            </div>
          </div>
            <script type="module">
            import { init, loadModelFromUrl } from './lib/index.js';
            // 初始化渲染器并挂载到 canvas
            init({ canvas: '#glCanvas', background: '#0b1220' });
            const statusEl = document.getElementById('status');
            const progressFill = document.getElementById('progressFill');
            const errorEl = document.getElementById('errorMsg');
            document.getElementById('loadBtn').addEventListener('click', async () => {
            const input = document.getElementById('modelUrl');
            const url = input.value && input.value.trim() !== '' ? input.value.trim() : './model/sample.glb';
            // reset UI
            if (errorEl) { errorEl.style.display = 'none'; errorEl.textContent = ''; }
            if (statusEl) statusEl.textContent = '开始加载...';
            if (progressFill instanceof HTMLElement) progressFill.style.width = '0%';
            try {
            await loadModelFromUrl(url, (percent) => {
            if (statusEl) statusEl.textContent = `加载中 ${percent}%`;
            if (progressFill instanceof HTMLElement) progressFill.style.width = percent + '%';
            }, (err) => {
            if (errorEl) { errorEl.style.display = 'block'; errorEl.textContent = '加载出错: ' + (err && err.message ? err.message : String(err)); }
            if (statusEl) statusEl.textContent = '加载错误';
            });
            if (statusEl) statusEl.textContent = '加载完成';
            if (progressFill instanceof HTMLElement) progressFill.style.width = '100%';
            } catch (e) {
            console.error(e);
            if (errorEl) { errorEl.style.display = 'block'; errorEl.textContent = '加载失败: ' + (e && e.message ? e.message : String(e)); }
            if (statusEl) statusEl.textContent = '加载失败';
            }
            });
          </script>
        </body>
      </html>
页面入口,包含UI(加载按钮、输入框、进度条)。
加载lib/index.js,初始化three.js渲染。
使用import map映射three.js及其扩展模块,支持浏览器原生ESM加载。
绑定按钮事件,调用loadModelFromUrl加载模型。

4. index.ts
// lib/index.ts
// 简单的 three.js 初始化与 glTF (GLB) 加载器。
// 目标:作为项目的最小可用实现 — 若要实际运行,请先 `npm install` three
export type InitOptions = {
  canvas?: HTMLCanvasElement | string;
  modelUrl?: string;
  background?: string;
};
let THREE: any = null;
let renderer: any = null;
let scene: any = null;
let camera: any = null;
let controls: any = null;
let model: any = null;  // 保存当前加载的模型引用
export async function init(opts: InitOptions = {}) {
  try {
    // 通过包名动态导入 three,兼容 TypeScript 和打包器
    THREE = await import('three');
  } catch (e) {
    // three.js 未安装或不能加载
    // 在开发时请运行: npm install three
    // 这里仅做无侵入的降级处理
    // eslint-disable-next-line no-console
    console.warn('three.js not available. Please run `npm install three` to enable 3D preview.');
    return;
  }
  const { canvas, background = '#222' } = opts;
  const canvasEl = typeof canvas === 'string' ? (document.querySelector(canvas) as HTMLCanvasElement) : canvas;
  renderer = new THREE.WebGLRenderer({ canvas: canvasEl ?? undefined, antialias: true });
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setPixelRatio(window.devicePixelRatio || 1);
  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);
  const hemi = new THREE.HemisphereLight(0xffffff, 0x444444, 1.0);
  scene.add(hemi);
  const dir = new THREE.DirectionalLight(0xffffff, 1);
  dir.position.set(5, 10, 7.5);
  scene.add(dir);
  // 小网格辅助
  const grid = new THREE.GridHelper(10, 10);
  scene.add(grid);
  window.addEventListener('resize', onResize);
  // 动态加载 OrbitControls(浏览器环境直接从 node_modules)
  try {
    const controlsModule = await import('three/examples/jsm/controls/OrbitControls.js');
    const { OrbitControls } = controlsModule;
    controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.dampingFactor = 0.05;
    controls.screenSpacePanning = false;
    controls.minDistance = 0.1;
    controls.maxDistance = 500;
  } catch (e) {
    // ignore if controls cannot be loaded
  }
  animate();
  if (opts.modelUrl) {
    // 忽略加载错误,让调用者处理异常
    try {
      await loadModelFromUrl(opts.modelUrl);
    } catch (err) {
      // eslint-disable-next-line no-console
      console.error('模型加载失败', err);
    }
  }
}
export async function loadModelFromUrl(url: string, onProgress?: (percent: number) => void, onError?: (err: any) => void) {
  if (!THREE) throw new Error('three.js 未初始化');
  // GLTFLoader 在 examples 模块中,直接从 node_modules 引入浏览器模块
  // 动态导入 GLTFLoader,兼容 Vite/Webpack/Node 环境
  const loaderModule = await import('three/examples/jsm/loaders/GLTFLoader.js');
  const { GLTFLoader } = loaderModule;
  const loader = new GLTFLoader();
  return new Promise((resolve, reject) => {
    loader.load(
      url,
      (gltf: any) => {
        // 把模型加入场景并居中
        scene.add(gltf.scene);
        model = gltf.scene;  // 保存模型引用
        const box = new THREE.Box3().setFromObject(gltf.scene);
        const center = box.getCenter(new THREE.Vector3());
        const size = box.getSize(new THREE.Vector3());
        const radius = size.length() * 0.5;
        // 将模型居中到原点,便于统一处理
        gltf.scene.position.sub(center);
        // 重新计算包围盒并把模型底部抬到 y=0(贴地)
        const bbox = new THREE.Box3().setFromObject(gltf.scene);
        const minY = bbox.min.y;
        if (minY < 0) {
          const baseY = -minY;
          gltf.scene.position.y = baseY; // 将最低点移动到 y=0
          gltf.scene.userData.baseY = baseY; // 保存基础高度供动画使用
        }
        // 计算一个合适的相机距离以完整看到模型
        if (camera) {
          const fov = camera.fov * (Math.PI / 180); // 垂直视场(弧度)
          // 根据包围球半径和视场角估算距离
          const distance = Math.abs(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();
        }
        resolve(gltf);
      },
      (progressEvent: ProgressEvent) => {
        if (onProgress && progressEvent && progressEvent.lengthComputable) {
          const percent = Math.round((progressEvent.loaded / progressEvent.total) * 100);
          onProgress(percent);
        }
      },
      (err: any) => {
        if (onError) onError(err);
        reject(err);
      }
    );
  });
}
function animate() {
  requestAnimationFrame(animate);
  if (controls) 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;
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
}
export function dispose() {
  window.removeEventListener('resize', onResize);
  if (renderer) {
    try {
      renderer.dispose();
      // 强制丢弃 GL context(如果可用)
      // @ts-ignore
      renderer.forceContextLoss && renderer.forceContextLoss();
    } catch (e) {
      // ignore
    }
  }
  renderer = scene = camera = null;
}
// end of file
three.js主逻辑,导出init和loadModelFromUrl两个核心方法。
init:初始化渲染器、场景、相机、光源、网格辅助线、OrbitControls(鼠标旋转缩放)。
loadModelFromUrl:加载glb模型,自动居中、贴地,调整相机视角。
animate:渲染循环,实时刷新场景。
仅保留基础交互,支持鼠标操作。
5. 准备其他文件
<font style="background-color:rgb(187,191,196);">icon.png</font>:准备一张方形图片(例如 128x128 像素),放在项目根目录,作为小程序图标。<font style="background-color:rgb(187,191,196);">model/sample.glb</font>:找一个 glb 格式的 3D 模型文件,放在<font style="background-color:rgb(187,191,196);">model</font>目录下(可从网上下载免费的 3D 模型,或使用自己的模型)。- three-examples.d.ts:声明three.js扩展模块类型,解决TypeScript编译时的类型报错。
 


4.2 运行方式
- 安装依赖
 
npminstall
- 编译TypeScript
 
npx tsc --project tsconfig.json
- 启动本地服务器
 
npm start
默认会用 http-server 启动 8080 端口。
访问页面 在浏览器打开
http://127.0.0.1:8080/main.html
- 加载模型
 
点击“加载示例模型”按钮,或输入自定义glb模型URL加载。
支持鼠标旋转、缩放、平移视角。

4.3 核心依赖
three.js:WebGL 3D渲染引擎
http-server:本地静态服务器
TypeScript:类型安全开发
4.4 效果展示





五、资源与支持
官方资源
- 开发者平台:Rokid 开发者中心
 - 模板仓库:template-for-jsar-widget
 - 技术文档:JSAR 官方手册
 
                    
                
                
            
        
浙公网安备 33010602011771号