JSAR 交互式菜单开发实战:打造沉浸式 3D 导航体验

简介: 本文介绍如何使用JSAR框架在Rokid智能眼镜上开发3D交互式菜单系统。通过Babylon.js创建按钮、动态纹理与动画,结合空间计算实现沉浸式导航体验,涵盖多视图切换、信息面板与手势适配,助力打造直观高效的AR界面。

JSAR 交互式菜单开发实战:打造沉浸式 3D 导航体验

前言

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

image.png

什么是 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 让按钮具有自发光效果,在深色背景下更加醒目。

image.png

添加文字标签

仅有几何体还不够,我们需要在按钮上显示文字。这里使用 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 平面上。
image.png

添加旋转动画

静态的按钮缺乏吸引力,让我们添加一个简单的旋转动画:

scene.registerBeforeRender(() => {
   
  button.rotation.y += 0.01;
});

image.png

实现交互反馈

最后,添加键盘交互:

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);
  }
});

image.png


第三步:实现视图切换机制

菜单的核心功能是导航到不同的内容页面。我们需要实现一个优雅的视图切换系统。

设计状态管理

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();

现在我们可以选择并点击选项,并且可以退出到菜单。

image.png


第四步:添加信息面板

为了让用户更好地理解每个 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();
}

image.png

分层渲染

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 控制渲染顺序,避免不必要的深度排序

参考资源


本文所有代码均在 Windows 10 + VSCode + JSAR 扩展环境下测试通过

相关文章
|
1月前
|
JavaScript Java 关系型数据库
基于springboot的文山西文旅网站
本项目基于Spring Boot、Java、Vue和MySQL技术,设计并实现一个山西文旅管理系统,旨在推动文旅管理的信息化与自动化。系统以实用、易用为核心,提升管理效率,降低人力成本,适应现代文旅发展需求,具有良好的可扩展性与稳定性。
|
1月前
|
算法 定位技术 vr&ar
Rokid手势识别深度测评:从技术原理到开发实战
Rokid通过单摄像头实现高精度手势识别与空间感知,结合AI算法与多模态交互,打造轻量高效的AR解决方案。其UXR SDK提供从底层数据到应用层的完整工具链,助力开发者构建教育、工业、消费等多场景AR应用,推动自然人机交互普及。
267 13
|
30天前
|
人工智能 安全 架构师
不只是聊天:从提示词工程看AI助手的优化策略
不只是聊天:从提示词工程看AI助手的优化策略
255 119
|
30天前
|
机器学习/深度学习 人工智能 自动驾驶
跳出循环:当AI不再是“模仿”,而是“思考”
跳出循环:当AI不再是“模仿”,而是“思考”
174 94
|
1月前
|
机器学习/深度学习 算法 前端开发
别再用均值填充了!MICE算法教你正确处理缺失数据
MICE是一种基于迭代链式方程的缺失值插补方法,通过构建后验分布并生成多个完整数据集,有效量化不确定性。相比简单填补,MICE利用变量间复杂关系,提升插补准确性,适用于多变量关联、缺失率高的场景。本文结合PMM与线性回归,详解其机制并对比效果,验证其在统计推断中的优势。
707 11
别再用均值填充了!MICE算法教你正确处理缺失数据
|
29天前
|
算法 API 流计算
《3D古城场景角色碰撞优化的实战指南》
本文聚焦开放世界3A项目“燕云古城废墟”场景的角色物理碰撞优化,记录从解决“穿模”“帧率骤降”等核心问题切入的工程化实践。先针对静态物体碰撞体冗余,设计“层级碰撞体”方案并制定精度规范,大幅降低计算量;再通过“预破碎资源池”优化可破坏物体,减少实时破碎的性能消耗;开发“动态碰撞剔除系统”,基于距离与视野实现碰撞计算按需触发;结合移动端特性,通过碰撞简化与物理步长调整完成多设备适配;最后构建“碰撞-动画协同系统”,提升交互真实感。
150 32
|
1月前
|
人工智能 自然语言处理 安全
氛围编程陷阱:为什么AI生成代码正在制造大量"伪开发者"
AI兴起催生“氛围编程”——用自然语言生成代码,看似高效实则陷阱。它让人跳过编程基本功,沦为只会提示、不懂原理的“中间商”。真实案例显示,此类项目易崩溃、难维护,安全漏洞频出。AI是技能倍增器,非替代品;真正强大的开发者,永远是那些基础扎实、能独立解决问题的人。
175 11
氛围编程陷阱:为什么AI生成代码正在制造大量"伪开发者"
|
1月前
|
前端开发 JavaScript 测试技术
Vue 3 + Vite:现代前端开发新范式-前端开发的”涡轮增压引擎”-优雅草卓伊凡
Vue 3 + Vite:现代前端开发新范式-前端开发的”涡轮增压引擎”-优雅草卓伊凡
234 0
Vue 3 + Vite:现代前端开发新范式-前端开发的”涡轮增压引擎”-优雅草卓伊凡
|
1月前
|
人工智能 算法 安全
AI + 热成像技术在动火作业风险防控中的实现路径
融合AI视觉与热成像技术,构建动火作业安全管控体系。通过定制化易燃物识别、计算机视觉测距、红外温度监测与多源图像融合,实现风险目标精准识别、安全距离实时预警、高温火源智能捕捉,并结合小程序“即拍即查”与后端闭环管理平台,完成隐患从发现到整改的全流程追溯,提升工业现场安全管理智能化水平。
174 10