JSAR 交互式菜单开发实战:打造沉浸式 3D 导航体验
前言
在空间计算时代,传统的 2D 菜单已经无法满足用户对沉浸式体验的需求。作为一名 AR 开发者,我一直在思考如何利用 Rokid 智能眼镜的空间计算能力,创造出更符合 3D 交互习惯的用户界面。本文将带你从零开始,使用 JSAR 框架开发一个功能完整的 3D 交互式菜单系统。

什么是 JSAR?
JSAR(JavaScript Spatial Application Runtime)是 Rokid 推出的开源空间应用运行时环境。它允许开发者使用熟悉的 Web 技术栈(JavaScript + 类 HTML 标记语言 XSML)来开发 AR/XR 应用,大大降低了空间计算开发的门槛。
为什么选择菜单系统作为切入点?
菜单是几乎所有应用的核心组件,掌握 3D 菜单的开发技巧,能让我们更好地理解空间 UI 设计的原则。通过这个项目,你将学会:
- 3D 按钮的创建与样式设计
- DynamicTexture 动态纹理的使用技巧
- 视图切换与场景管理
- 键盘交互的最佳实践
让我们开始吧!
第一步:创建基础 3D 按钮
在开始构建完整的菜单系统之前,我们需要先掌握 3D 按钮的创建方法。一个好的 3D 按钮应该具有清晰的视觉反馈、易于识别的文本标签,以及流畅的交互动画。
场景初始化
首先创建基本的 3D 场景:
const scene = spatialDocument.scene;
const camera = new BABYLON.ArcRotateCamera(
'camera',
Math.PI / 2,
Math.PI / 2.5,
15,
new BABYLON.Vector3(0, 0, 0),
scene
);
camera.attachControl(true);
const light = new BABYLON.HemisphericLight(
'light',
new BABYLON.Vector3(0, 1, 0),
scene
);
light.intensity = 0.8;
这里使用了 ArcRotateCamera,这是一种围绕目标点旋转的相机,非常适合展示 3D 对象。
创建按钮几何体
const button = BABYLON.MeshBuilder.CreateBox('button', {
width: 3,
height: 1,
depth: 0.2
}, scene);
const material = new BABYLON.StandardMaterial('buttonMat', scene);
material.diffuseColor = new BABYLON.Color3(0.2, 0.6, 1);
material.emissiveColor = new BABYLON.Color3(0.1, 0.3, 0.5);
button.material = material;
我们使用扁平的立方体作为按钮基础,emissiveColor 让按钮具有自发光效果,在深色背景下更加醒目。

添加文字标签
仅有几何体还不够,我们需要在按钮上显示文字。这里使用 JSAR 的 DynamicTexture 功能:
const textPlane = BABYLON.MeshBuilder.CreatePlane('textPlane', {
width: 2.8,
height: 0.8
}, scene);
textPlane.position = new BABYLON.Vector3(0, 0, -0.11);
textPlane.parent = button;
const texture = new BABYLON.DynamicTexture('buttonText', 512, scene, true);
const ctx = texture.getContext();
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 80px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('点击我', 256, 256);
texture.update();
const textMaterial = new BABYLON.StandardMaterial('textMat', scene);
textMaterial.diffuseTexture = texture;
textMaterial.emissiveColor = new BABYLON.Color3(1, 1, 1);
textPlane.material = textMaterial;
DynamicTexture 就像一个 Canvas,我们可以在上面绘制任何内容,然后将其作为纹理应用到 3D 平面上。
添加旋转动画
静态的按钮缺乏吸引力,让我们添加一个简单的旋转动画:
scene.registerBeforeRender(() => {
button.rotation.y += 0.01;
});

实现交互反馈
最后,添加键盘交互:
spatialDocument.addEventListener('keydown', (event) => {
if (event.key === ' ' || event.key === 'Enter') {
button.material.diffuseColor = new BABYLON.Color3(0.2, 1, 0.3);
console.log('按钮被点击了!');
setTimeout(() => {
button.material.diffuseColor = new BABYLON.Color3(0.2, 0.6, 1);
}, 300);
}
});
按下空格或回车键时,按钮会短暂变成绿色,提供清晰的视觉反馈。

第二步:构建多按钮菜单
单个按钮已经完成,现在我们需要创建一个包含多个选项的菜单系统。
定义菜单数据结构
使用数组来管理菜单项:
const menuItems = [
{
id: 1, title: '立方体', color: new BABYLON.Color3(0.2, 0.6, 1) },
{
id: 2, title: '球体', color: new BABYLON.Color3(1, 0.3, 0.3) },
{
id: 3, title: '圆柱体', color: new BABYLON.Color3(0.3, 1, 0.3) },
{
id: 4, title: '八面体', color: new BABYLON.Color3(1, 0.8, 0.2) }
];
每个菜单项都有独特的颜色,便于用户快速识别。
批量创建按钮
将按钮创建逻辑封装成函数:
function createMenuButton(item, index) {
const button = BABYLON.MeshBuilder.CreateBox('button' + item.id, {
width: 3,
height: 1,
depth: 0.2
}, scene);
// 水平排列
const spacing = 4;
button.position = new BABYLON.Vector3(
(index - 1.5) * spacing,
0,
0
);
const material = new BABYLON.StandardMaterial('buttonMat' + item.id, scene);
material.diffuseColor = item.color;
material.emissiveColor = item.color.scale(0.5);
button.material = material;
// ... 创建文字标签(省略重复代码)
return button;
}
menuItems.forEach((item, index) => {
const btn = createMenuButton(item, index);
buttons.push(btn);
});
四个按钮以 (index - 1.5) * spacing 的方式水平居中排列。

实现选择高亮
添加选择指示器,让用户知道当前聚焦在哪个选项上:
function highlightButton(index) {
buttons.forEach((btn, i) => {
if (i === index) {
btn.scaling = new BABYLON.Vector3(1.2, 1.2, 1.2);
btn.material.emissiveColor = menuItems[i].color;
} else {
btn.scaling = new BABYLON.Vector3(1, 1, 1);
btn.material.emissiveColor = menuItems[i].color.scale(0.5);
}
});
}
选中的按钮会放大 20% 并增强发光效果。

添加左右导航
let selectedIndex = 0;
spatialDocument.addEventListener('keydown', (event) => {
if (event.key === 'ArrowLeft' || event.key === 'a') {
selectedIndex = (selectedIndex - 1 + menuItems.length) % menuItems.length;
highlightButton(selectedIndex);
} else if (event.key === 'ArrowRight' || event.key === 'd') {
selectedIndex = (selectedIndex + 1) % menuItems.length;
highlightButton(selectedIndex);
}
});

第三步:实现视图切换机制
菜单的核心功能是导航到不同的内容页面。我们需要实现一个优雅的视图切换系统。
设计状态管理
let currentView = 'menu'; // 'menu' 或 'detail'
let menuObjects = [];
let detailObjects = [];
使用两个数组分别管理菜单视图和详情视图的所有对象,便于切换时清理。
清理视图函数
function clearMenuView() {
menuObjects.forEach(obj => obj.dispose());
menuObjects = [];
}
function clearDetailView() {
detailObjects.forEach(obj => obj.dispose());
detailObjects = [];
}
dispose() 方法会释放对象的所有资源,包括几何体、材质和纹理,这在频繁切换视图时非常重要。
创建详情视图
function showDetailView(item) {
currentView = 'detail';
clearMenuView();
let displayObject;
const material = new BABYLON.StandardMaterial('displayMat', scene);
material.diffuseColor = item.color;
material.emissiveColor = item.color.scale(0.3);
if (item.title === '立方体') {
displayObject = BABYLON.MeshBuilder.CreateBox('display', {
size: 4 }, scene);
} else if (item.title === '球体') {
displayObject = BABYLON.MeshBuilder.CreateSphere('display', {
diameter: 4 }, scene);
}
// ... 其他形状
displayObject.material = material;
detailObjects.push(displayObject);
}
根据选择的菜单项创建对应的 3D 对象。
添加返回功能
const backText = BABYLON.MeshBuilder.CreatePlane('backText', {
width: 6,
height: 1
}, scene);
backText.position = new BABYLON.Vector3(0, -4, 0);
// 绘制"返回"提示文字
const backTexture = new BABYLON.DynamicTexture('backTexture', 512, scene, true);
const ctx = backTexture.getContext();
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 50px Arial';
ctx.textAlign = 'center';
ctx.fillText('按 ESC 或 B 返回菜单', 256, 256);
backTexture.update();
现在我们可以选择并点击选项,并且可以退出到菜单。

第四步:添加信息面板
为了让用户更好地理解每个 3D 对象,我们需要添加一个信息展示面板。
扩展数据结构
const menuItems = [
{
id: 1,
title: '立方体',
subtitle: 'Cube',
description: '最基础的3D图形,具有6个面、8个顶点和12条边',
color: new BABYLON.Color3(0.2, 0.6, 1)
},
// ... 其他项
];
创建信息面板
这是本项目最复杂的部分,需要在 3D 空间中绘制格式化的文本信息:
function createInfoPanel(item) {
const panel = BABYLON.MeshBuilder.CreatePlane('infoPanel', {
width: 10,
height: 4
}, scene);
panel.position = new BABYLON.Vector3(0, 4.5, 0);
const texture = new BABYLON.DynamicTexture('infoTexture', {
width: 1024,
height: 512
}, scene, true);
const ctx = texture.getContext();
// 背景
ctx.fillStyle = 'rgba(0, 20, 40, 0.9)';
ctx.fillRect(0, 0, 1024, 512);
// 边框
ctx.strokeStyle = '#' + item.color.toHexString();
ctx.lineWidth = 8;
ctx.strokeRect(10, 10, 1004, 492);
// 标题
ctx.fillStyle = '#' + item.color.toHexString();
ctx.font = 'bold 80px Arial';
ctx.textAlign = 'center';
ctx.fillText(item.title, 512, 100);
// 副标题
ctx.fillStyle = '#ffffff';
ctx.font = '50px Arial';
ctx.fillText(item.subtitle, 512, 180);
// 描述(支持长文本换行)
ctx.fillStyle = '#cccccc';
ctx.font = '40px Arial';
// ... 文字换行逻辑
texture.update();
}

分层渲染
panel.renderingGroupId = 1;
将信息面板设置为渲染组 1,确保它始终显示在其他对象前面。
第五步:优化交互体验
添加数字快捷键
为了提高效率,我们添加数字键直达功能:
spatialDocument.addEventListener('keydown', (event) => {
if (currentView === 'menu') {
if (event.key >= '1' && event.key <= '4') {
const index = parseInt(event.key) - 1;
showDetailView(menuItems[index]);
}
}
});
现在用户可以直接按 1-4 快速跳转到对应页面。
添加使用提示
function createHelpText() {
const helpText = BABYLON.MeshBuilder.CreatePlane('helpText', {
width: 12,
height: 1.5
}, scene);
helpText.position = new BABYLON.Vector3(0, -3, 0);
// 绘制提示文字
ctx.fillText('按数字键 1-4 直接选择 | 左右键切换 | 回车确认', 512, 128);
}
Rokid 眼镜适配要点
虽然本教程使用键盘进行交互演示,但这个菜单系统可以轻松适配到 Rokid 智能眼镜上:
1. 手势映射
- 左右滑动 → 切换菜单项
- 点击确认 → 进入详情
- 向下滑动 → 返回菜单
2. 视觉优化
- 使用
emissiveColor确保在不同光照环境下都清晰可见 - 按钮间距 4 个单位,在眼镜视野中提供舒适的选择区域
- 信息面板使用半透明背景,不会完全遮挡背景环境
3. 性能优化
- 视图切换时及时调用
dispose()释放资源 - 使用
renderingGroupId控制渲染顺序,避免不必要的深度排序
参考资源
- JSAR 官方文档:https://jsar.netlify.app/
- Babylon.js API 文档:https://doc.babylonjs.com/
- Rokid 开发者社区:https://developer.rokid.com/
本文所有代码均在 Windows 10 + VSCode + JSAR 扩展环境下测试通过