项目概览
这是一个基于 JSAR 的交互式 3D 黑白钢琴应用,用户可以通过点击琴键播放音符。项目展示了 JSAR 在音频处理、3D 建模和交互设计方面的综合能力。
核心特性:
- 完整的黑白琴键布局(C4-C5,13个音符)
- 基于 Web Audio API 的真实音频合成
- 点击交互与视觉反馈
- 优雅的材质与光照效果
- 科学的琴键比例设计
JSAR 项目结构
JSAR 项目采用简洁的文件结构,类似于 npm package:
JSAR3/
├── package.json # 项目配置文件
├── main.xsml # 场景描述文件
├── lib/
│ └── piano.ts # 主逻辑代码
└── icon.png # 项目图标
package.json 配置
{"name": "jsar-piano","displayName": "3D黑白钢琴","version": "1.0.0","description": "JSAR 3D黑白钢琴 - 可点击演奏的虚拟钢琴","main": "main.xsml","files": ["main.xsml", "lib/*.ts", "icon.png"]}
这种标准化的配置方式让项目管理变得非常简单。
main.xsml 场景文件
XSML 是 JSAR 的场景描述语言,语法类似 HTML:
<xsml version="1.0"><head><title>🎹 3D黑白钢琴 - JSAR Widget</title><script src="./lib/piano.ts"></script></head><space></space></xsml>
简洁明了的声明式语法,<space> 元素代表 AR 空间场景。
核心开发:利用 JSAR 的 3D 能力
① 初始化场景
JSAR 通过 spatialDocument 全局对象提供场景访问能力:
const scene = spatialDocument.scene as BABYLON.Scene;
// 设置天空背景
scene.clearColor = new BABYLON.Color4(0.4, 0.5, 0.9, 1);
// 双光源系统const ambientLight = new BABYLON.HemisphericLight('ambientLight',
new BABYLON.Vector3(0, 1, 0), scene);
ambientLight.intensity = 0.7;
const directionalLight = new BABYLON.DirectionalLight('directionalLight',
new BABYLON.Vector3(-1, -2, -1), scene);
directionalLight.intensity = 0.5;
JSAR 内置了 Babylon.js 引擎,可以直接使用其强大的 3D API。
② 相机控制
let camera = scene.activeCamera;
if (!camera) {
camera = new BABYLON.ArcRotateCamera(
'mainCamera',
Math.PI / 2, // 水平角度Math.PI / 3, // 垂直角度100, // 距离(适配放大后的钢琴)BABYLON.Vector3.Zero(),
scene
);
camera.attachControl(true);
scene.activeCamera = camera;
} else {
// 如果已有相机,调整其位置if (camera instanceof BABYLON.ArcRotateCamera) {
camera.radius = 100;
camera.target = BABYLON.Vector3.Zero();
}
}
JSAR 自动管理相机,我们只需要设置位置和目标点即可。
③ 构建舞台
// 深紫色舞台地面const ground = BABYLON.MeshBuilder.CreateGround('ground',
{ width: 100, height: 100 }, scene);
const groundMat = new BABYLON.StandardMaterial('groundMat', scene);
groundMat.diffuseColor = new BABYLON.Color3(0.2, 0.15, 0.3);
groundMat.specularColor = new BABYLON.Color3(0.1, 0.1, 0.1);
ground.material = groundMat;
通过 Babylon.js 的 MeshBuilder,创建几何体非常简单直观。
④ 钢琴建模策略
钢琴是一个精密的 3D 模型,我们将其拆解为两大组件:
音符配置系统
首先定义钢琴的音符配置,包含频率、类型等信息:
interface KeyConfig {
note: string; // 音符名称frequency: number; // 音高频率isBlack: boolean; // 是否为黑键keyIndex: number; // 索引name: string; // 中文名称
}
const keyConfigs: KeyConfig[] = [
{ note: 'C4', frequency: 261.63, isBlack: false, keyIndex: 0, name: 'Do' },
{ note: 'C#4', frequency: 277.18, isBlack: true, keyIndex: 1, name: 'C#' },
{ note: 'D4', frequency: 293.66, isBlack: false, keyIndex: 2, name: 'Re' },
// ... 共13个音符
];
这种数据驱动的设计让音符管理变得非常清晰。
白键建模
白键是钢琴的主体结构:
// 放大琴键尺寸 (3.9倍)const whiteKeyWidth = 7.8;
const whiteKeyHeight = 1.95;
const whiteKeyDepth = 39;
// 创建白键const whiteKey = BABYLON.MeshBuilder.CreateBox(
`key_${config.note}`,
{
width: whiteKeyWidth,
height: whiteKeyHeight,
depth: whiteKeyDepth
},
scene
);
// 白色材质 - 模拟真实钢琴琴键const whiteMaterial = new BABYLON.StandardMaterial(`mat_${config.note}`, scene);
whiteMaterial.diffuseColor = new BABYLON.Color3(0.95, 0.95, 0.95); // 漫反射
whiteMaterial.specularColor = new BABYLON.Color3(0.5, 0.5, 0.5); // 镜面反射
whiteKey.material = whiteMaterial;
// 位置设置
whiteKey.position.x = whiteKeyPosition;
whiteKey.position.y = whiteKeyHeight / 2;
whiteKey.position.z = 0;
黑键建模
黑键比白键更窄、更高,位于白键之间:
const blackKeyWidth = 4.68;
const blackKeyHeight = 3.12;
const blackKeyDepth = 23.4;
// 创建黑键const blackKey = BABYLON.MeshBuilder.CreateBox(
`key_${config.note}`,
{
width: blackKeyWidth,
height: blackKeyHeight,
depth: blackKeyDepth
},
scene
);
// 黑色材质const blackMaterial = new BABYLON.StandardMaterial(`mat_${config.note}`, scene);
blackMaterial.diffuseColor = new BABYLON.Color3(0.1, 0.1, 0.1);
blackMaterial.specularColor = new BABYLON.Color3(0.2, 0.2, 0.2);
blackKey.material = blackMaterial;
// 黑键位置(在两个白键之间偏后)const previousWhiteKeyPos = whiteKeyPosition - whiteKeyWidth;
blackKey.position.x = previousWhiteKeyPos + whiteKeyWidth / 2;
blackKey.position.y = blackKeyHeight / 2;
blackKey.position.z = -whiteKeyDepth / 2 + blackKeyDepth / 2 + 4;
元数据系统
为每个琴键附加元数据,用于后续交互和动画:
blackKey.metadata = {
config, // 音符配置originalY: blackKeyHeight / 2, // 原始Y坐标originalColor: blackMaterial.diffuseColor.clone() // 原始颜色
};
keys.set(config.note, blackKey);
居中对齐
将整个钢琴居中显示:
// 计算总宽度const totalWidth = whiteKeyPosition; // 8个白键 × 7.8 = 62.4// 居中钢琴
keys.forEach((key) => {
key.position.x -= totalWidth / 2;
});
⑤ 音频系统:赋予钢琴声音
JSAR 支持 Web Audio API,可以实现实时音频合成:
初始化音频上下文
let audioContext: AudioContext | null = null;
try {
audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
console.log('✅ 音频上下文创建成功');
} catch (error) {
console.warn('⚠️ 音频上下文创建失败,音频功能可能不可用:', error);
}
播放音符
使用振荡器生成正弦波音频:
function playNote(note: string) {
if (!audioContext) return;
const config = keyConfigs.find(k => k.note === note);
if (!config) return;
// 创建振荡器和增益节点const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
// 设置音色为正弦波
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(config.frequency, audioContext.currentTime);
// 淡入效果
gainNode.gain.setValueAtTime(0, audioContext.currentTime);
gainNode.gain.linearRampToValueAtTime(0.3, audioContext.currentTime + 0.01);
// 连接音频节点
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.start();
activeNotes.set(note, { oscillator, gainNode });
console.log(`🎵 播放音符: ${config.name} (${config.note}) - ${config.frequency}Hz`);
}
⑥ 交互系统:点击与反馈
点击检测
JSAR 提供了场景级别的点击事件:
function setupInteraction() {
scene.onPointerDown = (evt, pickResult) => {
if (pickResult.hit && pickResult.pickedMesh) {
const mesh = pickResult.pickedMesh;
if (mesh.metadata && mesh.metadata.config) {
playNote(mesh.metadata.config.note);
// 300ms后自动停止setTimeout(() => {
stopNote(mesh.metadata.config.note);
}, 300);
}
}
};
}
视觉反馈
琴键被按下时,实现下沉和变色效果:
// 播放音符时的视觉反馈const key = keys.get(note);
if (key) {
// 琴键下沉
key.position.y = key.metadata.originalY - 0.4;
// 改变颜色const material = key.material as BABYLON.StandardMaterial;
material.diffuseColor = config.isBlack
? new BABYLON.Color3(0.3, 0.3, 0.3) // 黑键变浅灰
: new BABYLON.Color3(0.8, 0.8, 0.8); // 白键变灰
}
// 停止音符时恢复
key.position.y = key.metadata.originalY;
material.diffuseColor = key.metadata.originalColor;
⑦ 材质系统:打造真实感
JSAR 支持 Babylon.js 的完整材质系统:
// 白键材质 - 亮白色带光泽const whiteMat = new BABYLON.StandardMaterial('white-mat', scene);
whiteMat.diffuseColor = new BABYLON.Color3(0.95, 0.95, 0.95); // 漫反射
whiteMat.specularColor = new BABYLON.Color3(0.5, 0.5, 0.5); // 镜面反射// 黑键材质 - 深黑色哑光const blackMat = new BABYLON.StandardMaterial('black-mat', scene);
blackMat.diffuseColor = new BABYLON.Color3(0.1, 0.1, 0.1);
blackMat.specularColor = new BABYLON.Color3(0.2, 0.2, 0.2);
// 地面材质 - 深紫色舞台const groundMat = new BABYLON.StandardMaterial('groundMat', scene);
groundMat.diffuseColor = new BABYLON.Color3(0.2, 0.15, 0.3);
groundMat.specularColor = new BABYLON.Color3(0.1, 0.1, 0.1);
通过调整这些参数,可以实现真实的钢琴质感。
性能优化技巧
JSAR 场景中对象数量较多时,需要注意性能:
① 共享材质
// 好的做法:多个白键共享同一材质const whiteMat = new BABYLON.StandardMaterial('white-mat', scene);
whiteKey1.material = whiteMat;
whiteKey2.material = whiteMat;
whiteKey3.material = whiteMat;
// 避免:为每个琴键创建新材质const whiteMat1 = new BABYLON.StandardMaterial('white-mat-1', scene);
② 控制音频实例
// 限制同时播放的音符数量const MAX_CONCURRENT_NOTES = 5;
if (activeNotes.size >= MAX_CONCURRENT_NOTES) {
const oldestNote = activeNotes.keys().next().value;
stopNote(oldestNote);
}
最终效果
通过 Console 可以看到统计信息:
🎹 钢琴尺寸: 62.4 x 39 x 3.12
📐 占地比例: 放大3.9倍
🎵 音符数量: 13 (C4-C5)
🎼 白键: 8 | 黑键: 5
📊 总对象数: 15 (13个琴键 + 地面 + 相机)
🎨 材质系统: 漫反射 + 镜面反射
🔊 音频系统: Web Audio API + 正弦波合成
JSAR 开发心得
经过这个项目的实战,我总结了 JSAR 的几个核心优势:
| 特性 | 说明 |
|---|---|
| 开发效率 | TypeScript + Babylon.js,开发体验极佳 |
| 调试体验 | Chrome DevTools 远程调试,即时反馈 |
| 音频能力 | 完整支持 Web Audio API,实时音频合成 |
| 3D引擎 | Babylon.js 引擎,功能强大 |
| 项目结构 | 标准化的 npm package 结构 |
总结
JSAR 为 AR 开发者提供了一个强大而易用的开发框架。通过这个钢琴项目,我们看到了:
- 使用 TypeScript 构建复杂 3D 场景的能力
- Web Audio API 与 3D 场景的完美融合
- Chrome DevTools 带来的出色调试体验
- Babylon.js 引擎提供的丰富图形能力
- JSAR 标准化的项目结构和开发流程
如果你也对 AR 开发感兴趣,不妨试试 JSAR。从简单的立方体开始,逐步构建你的 3D 世界,相信你会和我一样,被它的优雅和强大所打动。
相关资源
- JSAR 官方文档:jsar.netlify.app/
- Rokid 开发者论坛:forum.rokid.com/
- Babylon.js 文档:doc.babylonjs.com/
- Web Audio API:developer.mozilla.org/zh-CN/docs/…