使用Rokid JSAR SDK构建沉浸式城市广场场景

在开始构建复杂的城市场景之前,我们需要先理解JSAR的基本结构。JSAR(JavaScript Spatial Augmented Reality)是Rokid专为AR设备设计的空间计算运行时,它基于强大的Babylon.js引擎,让开发者可以用熟悉的JavaScript来创建3D空间应用。
我们使用Rokid官方扩展 VSCode JSAR DevTools 进行开发。只需要在 VSCode 里安装 JSAR扩展,选中.xsml文件,点击"预览"按钮,即可实时热重载,大大提升开发效率。

一、场景搭建

  1. 搭建基础场景框架

首先,创建XSML文件(类似HTML,但用于3D空间)。XSML是JSAR的标记语言,<space>标签是3D对象的容器,类似于HTML中的<body>
<xsml version="1.0">
    <head>
        <title>城市广场 - Rokid AR</title>
        <script src="./scene.ts"></script>
    </head>
    <space></space>
</xsml>
接着,我们对场景进行初始化。其中,spatialDocument.scene是JSAR提供的全局场景对象,这是与Babylon.js的主要集成点。
<reference types="@yodaos-jsar/types" />
const { MeshBuilder, Vector3, Color3, Color4, StandardMaterial } = BABYLON;
const scene = spatialDocument.scene as BABYLON.Scene;
Rokid设备会自动创建相机,我们只需要调整其位置和朝向。这里将相机设置在较高位置,便于俯瞰整个场景。
const camera = scene.activeCamera;
if (camera) {
  camera.position = new Vector3(0, 150, 250);
  const target = new Vector3(0, 120, 0);
  if ('setTarget' in camera) {
    (camera as any).setTarget(target);
  }
}
下面,我们来创建地面。MeshBuilder是Babylon.js提供的快捷创建工具,支持立方体、球体、圆柱等常见几何体。这是JSAR推荐的对象创建方式,比直接使用XSML标签更灵活。
const ground = MeshBuilder.CreateGround('ground', {
  width: 480,
  height: 480
}, scene);

const groundMat = new StandardMaterial('ground-mat', scene);
groundMat.diffuseColor = new Color3(0.3, 0.6, 0.3); // 草绿色
ground.material = groundMat;
地面效果如图:

  1. 构建场景核心元素

城市场景的核心是建筑物。我们将创建一个可复用的建筑生成函数,这体现了Rokid SDK的模块化开发理念。
建筑物是本次实践目标的主要组成,我们先来创建建筑物。
function createBuilding(
  name: string,
  positionX: number,
  buildingColor: Color3,
  signColor: Color3
) {
  const buildingWidth = 120;
  const buildingHeight = 320;
  const buildingDepth = 120;

  // 主建筑体const building = MeshBuilder.CreateBox(`${name}-building`, {
    width: buildingWidth,
    height: buildingHeight,
    depth: buildingDepth
  }, scene);

  building.position = new Vector3(positionX, buildingHeight / 2, 0);

  const buildingMat = new StandardMaterial(`${name}-building-mat`, scene);
  buildingMat.diffuseColor = buildingColor;
  buildingMat.specularColor = new Color3(0.3, 0.3, 0.3);
  building.material = buildingMat;

  return { building };
}
这种函数式的设计让我们可以快速创建多个不同风格的建筑。
真实的建筑物有数百个窗户,我们需要批量生成:
const windowSize = 12;
const windowSpacing = 20;
const floorsCount = Math.floor((buildingHeight - 32) / windowSpacing);
const windowsPerFloor = 5;

for (let floor = 1; floor <= floorsCount; floor++) {
  for (let col = 0; col < windowsPerFloor; col++) {
    const window = MeshBuilder.CreateBox(`window-${floor}-${col}`, {
      width: windowSize,
      height: windowSize,
      depth: 0.8
    }, scene);

    const offsetX = (col - (windowsPerFloor - 1) / 2) * windowSpacing;
    const offsetY = floor * windowSpacing;

    window.position = new Vector3(
      positionX + offsetX,
      offsetY,
      buildingDepth / 2 + 0.4
    );

    const windowMat = new StandardMaterial(`window-mat-${floor}-${col}`, scene);
    const isLit = Math.random() > 0.4;
    if (isLit) {
      windowMat.emissiveColor = new Color3(1, 0.9, 0.6);
      windowMat.diffuseColor = new Color3(0.9, 0.9, 1);
    } else {
      windowMat.diffuseColor = new Color3(0.15, 0.15, 0.25);
    }
    window.material = windowMat;
  }
}
我们使用emissiveColor让亮起的窗户自发光,在AR环境中更醒目,同时随机亮灯营造生活气息。每个窗户间距20单位,在Rokid设备上视觉舒适。
只有一个楼我们不够看,于是我们设计了第二个楼宇。
const leftBuilding = createBuilding(
  'left',
  -150,
  new Color3(0.7, 0.75, 0.8),  // 浅灰色new Color3(0.2, 0.3, 0.5)    // 蓝色标志
);

const rightBuilding = createBuilding(
  'right',
  150,
  new Color3(0.75, 0.7, 0.7),  // 微红灰色new Color3(0.5, 0.2, 0.2)    // 红色标志
);
两栋大楼相距300单位,在Rokid眼镜中形成开阔的空间感。

  1. 丰富场景其他元素

为了让我们的城市场景看起来更加完整,我们需要为城市场景添加需要道路、绿化和公共设施。
① 道路系统
const road = MeshBuilder.CreateBox('road', {
  width: 20,
  height: 0.2,
  depth: 500
}, scene);
road.position = new Vector3(0, 0.1, 0);

const roadMat = new StandardMaterial('road-mat', scene);
roadMat.diffuseColor = new Color3(0.15, 0.15, 0.15);
road.material = roadMat;

// 人行道const sidewalk = MeshBuilder.CreateBox('sidewalk', {
  width: 8,
  height: 0.3,
  depth: 500
}, scene);
sidewalk.position = new Vector3(-14, 0.15, 0);

const sidewalkMat = new StandardMaterial('sidewalk-mat', scene);
sidewalkMat.diffuseColor = new Color3(0.5, 0.5, 0.5);
sidewalk.material = sidewalkMat;
道路厚度仅0.2单位,避免在AR中过于突兀,但颜色对比明显,确保可见性。
② 树木系统
环境绿化增强真实感:
const trees: any[] = [];

for (let i = 0; i < 12; i++) {
  const angle = (i / 12) * Math.PI * 2;
  const radius = 200;
  const x = Math.cos(angle) * radius;
  const z = Math.sin(angle) * radius;

  // 树干const trunk = MeshBuilder.CreateCylinder(`trunk-${i}`, {
    height: 15,
    diameter: 2
  }, scene);
  trunk.position = new Vector3(x, 7.5, z);

  const trunkMat = new StandardMaterial(`trunk-mat-${i}`, scene);
  trunkMat.diffuseColor = new Color3(0.4, 0.3, 0.2);
  trunk.material = trunkMat;

  // 树叶const leaves = MeshBuilder.CreateSphere(`leaves-${i}`, {
    diameter: 12
  }, scene);
  leaves.position = new Vector3(x, 18, z);

  const leavesMat = new StandardMaterial(`leaves-mat-${i}`, scene);
  leavesMat.diffuseColor = new Color3(0.2, 0.7, 0.2);
  leaves.material = leavesMat;

  trees.push(leaves);
}
12棵树围绕场景中心形成圆环,半径200单位确保不会挡住主要建筑。
③ 路灯照明
const streetLights: any[] = [];

for (let i = 0; i < 8; i++) {
  const angle = (i / 8) * Math.PI * 2 + Math.PI / 8;
  const radius = 170;
  const x = Math.cos(angle) * radius;
  const z = Math.sin(angle) * radius;

  // 灯柱const pole = MeshBuilder.CreateCylinder(`pole-${i}`, {
    height: 25,
    diameter: 1.5
  }, scene);
  pole.position = new Vector3(x, 12.5, z);

  // 灯泡const light = MeshBuilder.CreateSphere(`light-${i}`, {
    diameter: 3
  }, scene);
  light.position = new Vector3(x, 26, z);

  const lightMat = new StandardMaterial(`light-mat-${i}`, scene);
  lightMat.emissiveColor = new Color3(1, 0.95, 0.7);
  lightMat.diffuseColor = new Color3(1, 1, 0.9);
  light.material = lightMat;

  streetLights.push(light);
}
使用emissiveColor让路灯在任何环境光下都能发光,这是AR场景的关键技术。

  1. 补充场景动画元素

静态场景缺乏生命力,JSAR提供了强大的动画API。
首先,我们需要注册动画循环。
let time = 0;

scene.registerBeforeRender(() => {
  time += 0.01;

  // 所有动画逻辑在这里执行
});
registerBeforeRender在每帧渲染前调用,类似游戏引擎的Update循环。
① 建筑灯光动画
scene.registerBeforeRender(() => {
  time += 0.01;

  // 左楼标志牌闪烁const leftSignMat = leftBuilding.sign.material as any;
  if (leftSignMat) {
    const intensity = 0.5 + Math.sin(time * 2) * 0.2;
    leftSignMat.emissiveColor = new Color3(
      0.2 * intensity,
      0.3 * intensity,
      0.5 * intensity
    );
  }

  // 右楼标志牌(相位差)const rightSignMat = rightBuilding.sign.material as any;
  if (rightSignMat) {
    const intensity = 0.5 + Math.sin(time * 2 + Math.PI) * 0.2;
    rightSignMat.emissiveColor = new Color3(
      0.5 * intensity,
      0.2 * intensity,
      0.2 * intensity
    );
  }
});
使用正弦函数创建平滑的呼吸灯效果,两栋楼之间有π相位差,营造对比感。
② 游乐设施动画
// 旋转木马旋转
carouselBase.rotation.y += 0.01;

// 马匹上下浮动
carouselHorses.forEach((horse, index) => {
  const originalY = 6;
  horse.position.y = originalY + Math.sin(time * 2 + index) * 0.5;
});

// 摩天轮旋转
ferrisWheelRing.rotation.x += 0.008;

// 车厢保持水平
ferrisWheelCars.forEach((car) => {
  car.rotation.x = -ferrisWheelRing.rotation.x;
});
关键技术点:
  • 木马浮动使用不同相位,形成波浪效果
  • 摩天轮车厢需要反向旋转来抵消父对象的旋转,保持水平
③ 环境动画
// 树叶摆动
trees.forEach((tree, index) => {
  tree.rotation.y = Math.sin(time + index) * 0.1;
});

// 路灯闪烁
streetLights.forEach((light, index) => {
  const intensity = 0.9 + Math.sin(time * 2 + index) * 0.1;
  const mat = light.material as any;
  if (mat) {
    mat.emissiveColor = new Color3(
      intensity,
      intensity * 0.95,
      intensity * 0.7
    );
  }
});
环境细节动画让整个场景"活"起来。
 

最终实现效果如图:

二、性能优化

在Rokid设备上运行复杂场景需要特别注意性能。
① 对象管理
我们的场景包含数百个对象,需要良好的管理策略:
console.log('城市广场场景加载完成!');
console.log(`左楼: ${leftBuilding.floorsCount}层`);
console.log(`右楼: ${rightBuilding.floorsCount}层`);
console.log(`树木: ${trees.length} 棵`);
console.log(`路灯: ${streetLights.length} 盏`);
console.log(`总对象数: ${scene.meshes.length}`);
实时监控对象数量,我们的场景包含约200+对象。
② 材质复用
const sharedMat = new StandardMaterial('shared-mat', scene);
sharedMat.diffuseColor = new Color3(1, 0, 0);

for (let i = 0; i < 100; i++) {
  object.material = sharedMat;
}
材质是GPU资源消耗大户,能复用就复用。
③ 父子层级
// 将相关对象组织成父子关系
carouselHorses.forEach(horse => {
  horse.parent = carouselBase;
});
这样只需要旋转父对象,性能开销远小于分别旋转每个子对象。
④ 渲染优化
// 半透明对象使用alpha混合
fountainWaterMat.alpha = 0.6;

// 信息面板设置渲染组
infoPanel.renderingGroupId = 1;
合理使用渲染组可以优化深度排序开销。

三、设备适配

虽然本教程在PC上开发,但代码可以直接部署到Rokid眼镜。
  1. 空间感知
Rokid设备提供6DoF追踪,场景会随头部移动,由于JSAR自动处理相机追踪,开发者只需关注场景内容的相对位置。建议将主要内容放在用户前方2-5米处,符合AR交互习惯。
  1. 视觉设计
使用自发光确保可见性,避免纯黑色(AR中难以看清),建议用深灰代替纯黑。
material.emissiveColor = new Color3(0.3, 0.3, 0.5);
material.diffuseColor = new Color3(0.15, 0.15, 0.15); 
  1. 尺寸规范
我们的尺寸设计参数如下:
  • 主要建筑:100-200单位高度
  • 交互元素:3-5单位宽度
  • 文字面板:8-12单位宽度
  • 最小可识别尺寸:0.5单位
  1. 性能指标
我们的场景严格满足推荐值:
指标 推荐值 我们的场景
对象数 < 300 ~250
材质数 < 50 ~40
帧率 > 30 fps 稳定60fps

参考资源


总结

通过本教程,我们从零开始构建了一个包含200+对象的复杂AR城市场景,涵盖了Rokid JSAR SDK的核心技术点:
  • 基础场景搭建 - 使用MeshBuilder创建几何体
  • 材质系统 - diffuseColor、emissiveColor、alpha透明
  • 对象层级 - parent父子关系管理复杂结构
  • 动画系统 - registerBeforeRender实现流畅动画
  • 性能优化 - 材质复用、对象管理、渲染组
  • 设备适配 - 针对Rokid眼镜的视觉和交互优化
Rokid JSAR SDK让Web开发者可以用熟悉的JavaScript轻松进入AR开发领域,无需学习Unity或Unreal等重型引擎。配合TypeScript的类型提示和VSCode的开发工具,开发体验极佳。
posted @ 2025-10-15 01:58  Damon小智  阅读(11)  评论(0)    收藏  举报