用 JSAR 让天气"活"起来:我的 AR 天气可视化之旅
用 JSAR 让天气"活"起来:我的 AR 天气可视化之旅
开篇碎碎念
作为一个经常看天气预报的人,我一直觉得传统天气 APP 虽然功能齐全,但总缺了点什么——那些数字和小图标确实能传达信息,但总感觉不够直观。
直到我接触了 Rokid 的 JSAR 框架,一个想法突然冒出来:能不能把天气数据变成眼前可以"看到"、"感受到"的 3D 场景?
于是就有了这个项目。现在我可以在虚拟空间里"站在"雨中、"走进"雪天,温度用颜色告诉我冷暖,湿度用环形光晕表现浓淡。这是一种全新的天气数据呈现方式。
关于 Rokid AR 眼镜
Rokid 是国内领先的 AR(增强现实)硬件厂商,其 AR 眼镜系列产品能够将虚拟内容叠加在真实世界之上。配合 Rokid 自研的 JSAR(JavaScript Spatial Application Runtime)框架,开发者可以使用熟悉的 Web 技术栈(JavaScript + Babylon.js)快速构建空间计算应用,无需学习复杂的 Unity 或 Unreal 引擎。
JSAR 的最大优势在于降低了 AR 开发门槛:传统 AR 开发需要掌握 C#、C++ 等语言,而 JSAR 让前端开发者也能轻松上手。更重要的是,开发过程中即使没有 AR 设备,也能通过浏览器预览和调试,极大提升了开发效率。
Rokid AR 眼镜 - 图片来源:Rokid 官网
灵感来源:为什么要重新发明天气 APP
传统天气应用的局限
传统天气应用主要通过数字和图标呈现信息:温度、湿度、风速等都是抽象的数值。虽然这些数据准确,但缺乏视觉反馈,不够直观。
如果天气可以"看见"呢?
我想要的天气预报是这样的:
🌡️ 温度 → 一根会变色的柱子,从蓝到红
💧 湿度 → 空气中的雾气浓度
💨 风速 → 真的看到风在吹
🌧️ 下雨 → 雨滴从天上落下来
Rokid AR + JSAR = 梦想成真!
技术选型:为什么是 JSAR?
老实说,刚开始我想用 Unity 做。但学了两天就放弃了——太复杂了!光是搭建 AR 环境就要配置一堆东西。
然后发现了 JSAR 这个宝藏框架:
优点一:Web 技术栈
不用学 C#,JavaScript 就行。我本来就是前端开发,直接上手。
优点二:Babylon.js 加持
内置了强大的 3D 引擎,粒子系统、光照、材质,应有尽有。
优点三:开发效率高
写个.xsml 文件,刷新浏览器就能看效果。不像 Unity 每次都要 Build 半天。
优点四:为 Rokid 定制
专门为 Rokid AR 眼镜优化的,性能有保证。
最重要的是:无脑上手,30 分钟就能跑出第一个 Demo。
动手实践:从零到完整系统
我的开发思路是"先跑起来,再慢慢完善"。所以把整个过程拆成了 5 个小步骤,每一步都能看到成果。这样不会中途放弃(笑)。
第一步:搭个舞台(10 分钟)
最基础的东西:地面、天空、灯光。就像舞台布景一样。
<xsml version="1.0">
<head>
<title>步骤1:基础场景</title>
<script>
console.log('🌍 3D天气可视化系统 - 步骤1:基础场景');
try {
const scene = spatialDocument.scene;
// 清理现有对象
scene.meshes.forEach(mesh => mesh.dispose());
scene.lights.forEach(light => light.dispose());
// 设置相机 - 第一人称视角,平视
if (scene.activeCamera) {
scene.activeCamera.position = new BABYLON.Vector3(0, 1.6, -12);
scene.activeCamera.setTarget(new BABYLON.Vector3(0, 0.6, 0));
console.log('📷 相机设置完成:第一人称视角');
}
// 添加环境光
const hemiLight = new BABYLON.HemisphericLight(
'hemiLight',
new BABYLON.Vector3(0, 1, 0),
scene
);
hemiLight.intensity = 0.6;
// 添加方向光(太阳光)
const sunLight = new BABYLON.DirectionalLight(
'sunLight',
new BABYLON.Vector3(-1, -2, -1),
scene
);
sunLight.intensity = 0.8;
sunLight.position = new BABYLON.Vector3(20, 40, 20);
console.log('💡 光源设置完成');
// 创建地面
const ground = BABYLON.MeshBuilder.CreateGround('ground', {
width: 50,
height: 50
}, scene);
const groundMaterial = new BABYLON.StandardMaterial('groundMat', scene);
groundMaterial.diffuseColor = new BABYLON.Color3(0.3, 0.5, 0.3);
groundMaterial.specularColor = new BABYLON.Color3(0.1, 0.1, 0.1);
ground.material = groundMaterial;
console.log('🏞️ 地面创建完成');
// 设置背景色(替代天空盒,避免遮挡视线)
scene.clearColor = new BABYLON.Color4(0.5, 0.7, 1, 1);
console.log('☁️ 背景色设置完成');
// 添加参考立方体(场景中心,视线高度)
const centerCube = BABYLON.MeshBuilder.CreateBox('centerCube', {
size: 1
}, scene);
centerCube.position.y = 0.6;
const cubeMaterial = new BABYLON.StandardMaterial('cubeMat', scene);
cubeMaterial.diffuseColor = new BABYLON.Color3(0.8, 0.8, 0.8);
cubeMaterial.emissiveColor = new BABYLON.Color3(0.3, 0.3, 0.3);
centerCube.material = cubeMaterial;
console.log('📦 参考立方体创建完成(视线高度)');
console.log('');
console.log('✅ 基础场景搭建完成!');
console.log('💡 下一步:添加天气数据显示(step2-weather-data.xsml)');
} catch (error) {
console.error('💥 场景创建失败:', error);
}
</script>
</head>
<space></space>
</xsml>
打开 VSCode,创建文件,粘贴代码,保存。然后用 JSAR 插件打开,就能看到一个简单的 3D 场景了。
第一步效果:绿色地面 + 蓝色天空
收获:
- 了解了 XSML 的基本结构
- 知道怎么设置相机和光源
- 学会了创建基础几何体
代码在文末完整提供,这里先讲思路。
第二步:把数据画出来(20 分钟)
有了舞台,该上演员了。我选了三种最直观的数据呈现方式:
🌡️ 温度 = 彩色柱子
- 零下:蓝色(冰冷)
- 0-15°C:青色(寒冷)
- 15-25°C:绿色(舒适)
- 25-35°C:黄色(温热)
- 35°C 以上:红色(炎热)
柱子的高度也代表温度,一眼就能看出今天多热。
💧 湿度 = 旋转的环
湿度越高,环越亮、越不透明。就像空气中的水汽凝结成了光环。
💨 风速 = 飞舞的箭头
风速大,箭头就多;风速小,箭头就少。而且会左右飘动,模拟风吹的感觉。
<xsml version="1.0">
<head>
<title>步骤2:天气数据显示</title>
<script>
console.log('🌍 3D天气可视化系统 - 步骤2:数据显示');
try {
const scene = spatialDocument.scene;
// 场景设置
scene.meshes.forEach(mesh => mesh.dispose());
scene.lights.forEach(light => light.dispose());
if (scene.activeCamera) {
scene.activeCamera.position = new BABYLON.Vector3(0, 1.6, -12);
scene.activeCamera.setTarget(new BABYLON.Vector3(0, 0.6, 0));
console.log('📷 相机设置完成:第一人称视角');
}
const hemiLight = new BABYLON.HemisphericLight('hemiLight',
new BABYLON.Vector3(0, 1, 0), scene);
hemiLight.intensity = 0.6;
const sunLight = new BABYLON.DirectionalLight('sunLight',
new BABYLON.Vector3(-1, -2, -1), scene);
sunLight.intensity = 0.8;
const ground = BABYLON.MeshBuilder.CreateGround('ground', {
width: 50, height: 50
}, scene);
const groundMaterial = new BABYLON.StandardMaterial('groundMat', scene);
groundMaterial.diffuseColor = new BABYLON.Color3(0.3, 0.5, 0.3);
ground.material = groundMaterial;
// 设置背景色
scene.clearColor = new BABYLON.Color4(0.5, 0.7, 1, 1);
console.log('🏞️ 基础场景完成');
// 天气数据
const weatherData = {
city: '北京',
weather: '晴天',
temperature: 25,
humidity: 60,
windSpeed: 12
};
// 温度颜色映射
function getTemperatureColor(temp) {
if (temp < 0) return new BABYLON.Color3(0, 0.5, 1); // 蓝色
if (temp < 15) return new BABYLON.Color3(0, 1, 1); // 青色
if (temp < 25) return new BABYLON.Color3(0, 1, 0); // 绿色
if (temp < 35) return new BABYLON.Color3(1, 1, 0); // 黄色
return new BABYLON.Color3(1, 0.3, 0); // 红色
}
// 创建温度柱体
const tempHeight = weatherData.temperature / 5;
const tempBar = BABYLON.MeshBuilder.CreateCylinder('tempBar', {
height: tempHeight,
diameter: 1.5
}, scene);
tempBar.position = new BABYLON.Vector3(-4, 1.6, 0);
const tempMaterial = new BABYLON.StandardMaterial('tempMat', scene);
tempMaterial.diffuseColor = getTemperatureColor(weatherData.temperature);
tempMaterial.emissiveColor = tempMaterial.diffuseColor.scale(0.3);
tempBar.material = tempMaterial;
console.log('🌡️ 温度柱体创建完成');
// 创建湿度环
const humidityRing = BABYLON.MeshBuilder.CreateTorus('humidityRing', {
diameter: 3,
thickness: 0.4,
tessellation: 32
}, scene);
humidityRing.position = new BABYLON.Vector3(0, 1.6, 0);
humidityRing.rotation.x = Math.PI / 2;
const humidityMaterial = new BABYLON.StandardMaterial('humidityMat', scene);
const humidityRatio = weatherData.humidity / 100;
humidityMaterial.diffuseColor = new BABYLON.Color3(
0.2,
0.5 + humidityRatio * 0.5,
1
);
humidityMaterial.emissiveColor = humidityMaterial.diffuseColor.scale(0.4);
humidityMaterial.alpha = 0.3 + humidityRatio * 0.5;
humidityRing.material = humidityMaterial;
// 湿度环旋转动画
scene.registerBeforeRender(() => {
humidityRing.rotation.z += 0.01;
});
console.log('💧 湿度环创建完成');
// 创建风速箭头
const arrowCount = Math.floor(weatherData.windSpeed / 3);
const arrows = [];
for (let i = 0; i < arrowCount; i++) {
const arrow = BABYLON.MeshBuilder.CreateCylinder(`arrow${i}`, {
height: 2,
diameterTop: 0,
diameterBottom: 0.3
}, scene);
arrow.position = new BABYLON.Vector3(4, 1.6 + i * 0.5 - (arrowCount * 0.25), 0);
arrow.rotation.z = -Math.PI / 2;
const arrowMaterial = new BABYLON.StandardMaterial(`arrowMat${i}`, scene);
arrowMaterial.diffuseColor = new BABYLON.Color3(1, 1, 1);
arrowMaterial.emissiveColor = new BABYLON.Color3(0.5, 0.5, 0.5);
arrow.material = arrowMaterial;
arrows.push(arrow);
}
// 风速箭头移动动画
let windOffset = 0;
scene.registerBeforeRender(() => {
windOffset += 0.05;
arrows.forEach((arrow, i) => {
arrow.position.x = 4 + Math.sin(windOffset + i * 0.6) * 1.5;
});
});
console.log(`💨 风速箭头创建完成 (${arrowCount}个)`);
// 打印天气数据
console.log('');
console.log('📊 当前天气数据:');
console.log(` 城市:${weatherData.city}`);
console.log(` 天气:${weatherData.weather}`);
console.log(` 温度:${weatherData.temperature}°C`);
console.log(` 湿度:${weatherData.humidity}%`);
console.log(` 风速:${weatherData.windSpeed} km/h`);
console.log('');
console.log('✅ 天气数据可视化完成!');
console.log('💡 下一步:添加天气粒子效果(step3-weather-effects.xsml)');
} catch (error) {
console.error('💥 创建失败:', error);
}
</script>
</head>
<space></space>
</xsml>
三种数据可视化元素
遇到的坑:
一开始温度柱子是白色的,怎么调都不对。后来发现是忘了设置 emissiveColor(自发光)。Babylon.js 里,如果想让物体在暗处也有颜色,必须加自发光。
material.diffuseColor = color; // 基础颜色
material.emissiveColor = color.scale(0.3); // 自发光(重要!)
第三步:让天气"动"起来(30 分钟)
数据有了,但还是静态的。真正的挑战来了:粒子系统。
🌧️ 下雨效果
关键参数:
- 粒子数量:3000 个(太少不逼真,太多卡)
- 方向:向下(Y 轴负方向)
- 重力:-9.8(模拟真实重力)
- 速度:快(20-25)
- 颜色:淡蓝色半透明
<xsml version="1.0">
<head>
<title>步骤3:天气粒子效果</title>
<script>
console.log('🌍 3D天气可视化系统 - 步骤3:粒子效果');
try {
const scene = spatialDocument.scene;
// 基础场景设置
scene.meshes.forEach(mesh => mesh.dispose());
scene.lights.forEach(light => light.dispose());
if (scene.activeCamera) {
scene.activeCamera.position = new BABYLON.Vector3(0, 1.6, -12);
scene.activeCamera.setTarget(new BABYLON.Vector3(0, 0.6, 0));
console.log('📷 相机设置完成:第一人称视角');
}
const hemiLight = new BABYLON.HemisphericLight('hemiLight',
new BABYLON.Vector3(0, 1, 0), scene);
hemiLight.intensity = 0.6;
const sunLight = new BABYLON.DirectionalLight('sunLight',
new BABYLON.Vector3(-1, -2, -1), scene);
sunLight.intensity = 0.8;
sunLight.position = new BABYLON.Vector3(20, 40, 20);
const ground = BABYLON.MeshBuilder.CreateGround('ground', {
width: 50, height: 50
}, scene);
const groundMaterial = new BABYLON.StandardMaterial('groundMat', scene);
groundMaterial.diffuseColor = new BABYLON.Color3(0.3, 0.5, 0.3);
ground.material = groundMaterial;
// 设置背景色
scene.clearColor = new BABYLON.Color4(0.5, 0.7, 1, 1);
console.log('🏞️ 基础场景完成');
// 全局粒子系统引用
let currentParticleSystem = null;
// 1. 雨天粒子系统
function createRainEffect() {
const rainSystem = new BABYLON.ParticleSystem('rain', 3000, scene);
rainSystem.particleTexture = new BABYLON.Texture(
'https://playground.babylonjs.com/textures/flare.png',
scene
);
rainSystem.emitter = new BABYLON.Vector3(0, 15, 0);
rainSystem.minEmitBox = new BABYLON.Vector3(-15, 0, -15);
rainSystem.maxEmitBox = new BABYLON.Vector3(15, 0, 15);
rainSystem.color1 = new BABYLON.Color4(0.7, 0.7, 1, 0.8);
rainSystem.color2 = new BABYLON.Color4(0.5, 0.5, 0.8, 0.6);
rainSystem.colorDead = new BABYLON.Color4(0.3, 0.3, 0.5, 0);
rainSystem.minSize = 0.05;
rainSystem.maxSize = 0.15;
rainSystem.minLifeTime = 1;
rainSystem.maxLifeTime = 2;
rainSystem.emitRate = 1000;
rainSystem.direction1 = new BABYLON.Vector3(0, -20, 0);
rainSystem.direction2 = new BABYLON.Vector3(0, -25, 0);
rainSystem.gravity = new BABYLON.Vector3(0, -9.8, 0);
rainSystem.blendMode = BABYLON.ParticleSystem.BLENDMODE_ADD;
rainSystem.start();
return rainSystem;
}
// 2. 雪天粒子系统
function createSnowEffect() {
const snowSystem = new BABYLON.ParticleSystem('snow', 2000, scene);
snowSystem.particleTexture = new BABYLON.Texture(
'https://playground.babylonjs.com/textures/flare.png',
scene
);
snowSystem.emitter = new BABYLON.Vector3(0, 15, 0);
snowSystem.minEmitBox = new BABYLON.Vector3(-15, 0, -15);
snowSystem.maxEmitBox = new BABYLON.Vector3(15, 0, 15);
snowSystem.color1 = new BABYLON.Color4(1, 1, 1, 1);
snowSystem.color2 = new BABYLON.Color4(0.9, 0.9, 1, 0.8);
snowSystem.colorDead = new BABYLON.Color4(0.7, 0.7, 0.8, 0);
snowSystem.minSize = 0.1;
snowSystem.maxSize = 0.3;
snowSystem.minLifeTime = 3;
snowSystem.maxLifeTime = 5;
snowSystem.emitRate = 500;
snowSystem.direction1 = new BABYLON.Vector3(-2, -5, -2);
snowSystem.direction2 = new BABYLON.Vector3(2, -8, 2);
snowSystem.gravity = new BABYLON.Vector3(0, -2, 0);
snowSystem.blendMode = BABYLON.ParticleSystem.BLENDMODE_STANDARD;
snowSystem.start();
return snowSystem;
}
// 3. 雾气粒子系统
function createFogEffect() {
const fogSystem = new BABYLON.ParticleSystem('fog', 1000, scene);
fogSystem.particleTexture = new BABYLON.Texture(
'https://playground.babylonjs.com/textures/cloud.png',
scene
);
fogSystem.emitter = new BABYLON.Vector3(0, 2, 0);
fogSystem.minEmitBox = new BABYLON.Vector3(-10, -1, -10);
fogSystem.maxEmitBox = new BABYLON.Vector3(10, 1, 10);
fogSystem.color1 = new BABYLON.Color4(0.7, 0.7, 0.7, 0.3);
fogSystem.color2 = new BABYLON.Color4(0.8, 0.8, 0.8, 0.2);
fogSystem.colorDead = new BABYLON.Color4(0.6, 0.6, 0.6, 0);
fogSystem.minSize = 3;
fogSystem.maxSize = 6;
fogSystem.minLifeTime = 10;
fogSystem.maxLifeTime = 15;
fogSystem.emitRate = 50;
fogSystem.direction1 = new BABYLON.Vector3(-1, 0.5, -1);
fogSystem.direction2 = new BABYLON.Vector3(1, 1, 1);
fogSystem.gravity = new BABYLON.Vector3(0, 0, 0);
fogSystem.blendMode = BABYLON.ParticleSystem.BLENDMODE_STANDARD;
fogSystem.start();
return fogSystem;
}
// 切换天气效果函数
window.setWeatherEffect = function(type) {
if (currentParticleSystem) {
currentParticleSystem.stop();
currentParticleSystem.dispose();
currentParticleSystem = null;
}
switch(type) {
case 'rain':
currentParticleSystem = createRainEffect();
console.log('🌧️ 雨天效果已启动');
break;
case 'snow':
currentParticleSystem = createSnowEffect();
console.log('❄️ 雪天效果已启动');
break;
case 'fog':
currentParticleSystem = createFogEffect();
console.log('🌫️ 雾天效果已启动');
break;
case 'clear':
console.log('☀️ 晴天效果(无粒子)');
break;
default:
console.log('⚠️ 未知天气类型');
}
};
// 默认显示雨天效果
setWeatherEffect('rain');
console.log('');
console.log('✅ 天气粒子系统创建完成!');
console.log('');
console.log('🎮 控制命令:');
console.log(' setWeatherEffect("rain") - 雨天');
console.log(' setWeatherEffect("snow") - 雪天');
console.log(' setWeatherEffect("fog") - 雾天');
console.log(' setWeatherEffect("clear") - 晴天');
console.log('');
console.log('💡 下一步:多城市数据(step4-multi-cities.xsml)');
} catch (error) {
console.error('💥 创建失败:', error);
}
</script>
</head>
<space></space>
</xsml>
雨天效果:3000 个雨滴
❄️ 下雪效果
和雨的区别:
- 粒子更大(雪花比雨滴大)
- 速度更慢(飘落而不是砸落)
- 横向漂移(风吹的效果)
雪天效果:雪花飘飘
调试心得:
粒子系统最难的是调参数。我试了至少 20 种组合才找到满意的效果。建议:
- 先从官方 Demo 抄参数
- 一个一个改,看效果
- 记录下好的组合
第四步:加入多城市(15 分钟)
一个城市太单调,我加了 5 个有代表性的:
| 城市 | 天气 | 为什么选它 |
| 北京 | 晴天 | 首都,经常蓝天 |
| 上海 | 雨天 | 魔都,梅雨季 |
| 哈尔滨 | 雪天 | 冰城,必须雪 |
| 成都 | 多云 | 阴天之都 |
| 广州 | 雾霾 | 南方潮湿 |
<xsml version="1.0">
<head>
<title>步骤4:多城市数据</title>
<script>
console.log('🌍 3D天气可视化系统 - 步骤4:多城市');
try {
const scene = spatialDocument.scene;
// 基础场景
scene.meshes.forEach(mesh => mesh.dispose());
scene.lights.forEach(light => light.dispose());
// 相机位置调整 - 第一人称视角
if (scene.activeCamera) {
scene.activeCamera.position = new BABYLON.Vector3(0, 1.6, -12);
scene.activeCamera.setTarget(new BABYLON.Vector3(0, 0.6, 0));
console.log('📷 相机设置完成:第一人称视角');
}
const hemiLight = new BABYLON.HemisphericLight('hemiLight',
new BABYLON.Vector3(0, 1, 0), scene);
hemiLight.intensity = 0.6;
const sunLight = new BABYLON.DirectionalLight('sunLight',
new BABYLON.Vector3(-1, -2, -1), scene);
sunLight.intensity = 0.8;
const ground = BABYLON.MeshBuilder.CreateGround('ground', {
width: 50, height: 50
}, scene);
const groundMaterial = new BABYLON.StandardMaterial('groundMat', scene);
groundMaterial.diffuseColor = new BABYLON.Color3(0.3, 0.5, 0.3);
ground.material = groundMaterial;
// 设置背景色
scene.clearColor = new BABYLON.Color4(0.5, 0.7, 1, 1);
console.log('🏞️ 基础场景完成');
// 城市天气数据库
const citiesData = [
{
name: '北京',
weather: 'sunny',
weatherName: '晴天',
temperature: 25,
humidity: 45,
windSpeed: 12
},
{
name: '上海',
weather: 'rainy',
weatherName: '雨天',
temperature: 18,
humidity: 85,
windSpeed: 20
},
{
name: '哈尔滨',
weather: 'snow',
weatherName: '雪天',
temperature: -5,
humidity: 70,
windSpeed: 8
},
{
name: '成都',
weather: 'cloudy',
weatherName: '多云',
temperature: 22,
humidity: 65,
windSpeed: 6
},
{
name: '广州',
weather: 'foggy',
weatherName: '雾霾',
temperature: 28,
humidity: 90,
windSpeed: 4
}
];
let currentCityIndex = 0;
// 可视化元素引用
let tempBar = null;
let humidityRing = null;
let windArrows = [];
let particleSystem = null;
// 温度颜色映射
function getTemperatureColor(temp) {
if (temp < 0) return new BABYLON.Color3(0, 0.5, 1);
if (temp < 15) return new BABYLON.Color3(0, 1, 1);
if (temp < 25) return new BABYLON.Color3(0, 1, 0);
if (temp < 35) return new BABYLON.Color3(1, 1, 0);
return new BABYLON.Color3(1, 0.3, 0);
}
// 创建温度柱体
function createTempBar(temp) {
if (tempBar) tempBar.dispose();
const height = Math.abs(temp) / 5 + 1;
tempBar = BABYLON.MeshBuilder.CreateCylinder('tempBar', {
height: height,
diameter: 1.5
}, scene);
tempBar.position = new BABYLON.Vector3(-4, 1.6, 0);
const material = new BABYLON.StandardMaterial('tempMat', scene);
material.diffuseColor = getTemperatureColor(temp);
material.emissiveColor = material.diffuseColor.scale(0.4);
tempBar.material = material;
}
// 创建湿度环
function createHumidityRing(humidity) {
if (humidityRing) humidityRing.dispose();
humidityRing = BABYLON.MeshBuilder.CreateTorus('humidityRing', {
diameter: 3,
thickness: 0.4
}, scene);
humidityRing.position = new BABYLON.Vector3(0, 1.6, 0);
humidityRing.rotation.x = Math.PI / 2;
const material = new BABYLON.StandardMaterial('humidityMat', scene);
const ratio = humidity / 100;
material.diffuseColor = new BABYLON.Color3(0.2, 0.5 + ratio * 0.5, 1);
material.emissiveColor = material.diffuseColor.scale(0.5);
material.alpha = 0.4 + ratio * 0.4;
humidityRing.material = material;
}
// 创建风速箭头
function createWindArrows(windSpeed) {
windArrows.forEach(arrow => arrow.dispose());
windArrows = [];
const count = Math.floor(windSpeed / 4) + 1;
for (let i = 0; i < count; i++) {
const arrow = BABYLON.MeshBuilder.CreateCylinder(`arrow${i}`, {
height: 2,
diameterTop: 0,
diameterBottom: 0.4
}, scene);
arrow.position = new BABYLON.Vector3(4, 1.6 + i * 0.5 - (count * 0.25), 0);
arrow.rotation.z = -Math.PI / 2;
const material = new BABYLON.StandardMaterial(`arrowMat${i}`, scene);
material.diffuseColor = new BABYLON.Color3(1, 1, 1);
material.emissiveColor = new BABYLON.Color3(0.6, 0.6, 0.6);
arrow.material = material;
windArrows.push(arrow);
}
}
// 创建天气粒子效果
function createWeatherEffect(weatherType) {
if (particleSystem) {
particleSystem.stop();
particleSystem.dispose();
particleSystem = null;
}
if (weatherType === 'rainy') {
particleSystem = new BABYLON.ParticleSystem('rain', 3000, scene);
particleSystem.particleTexture = new BABYLON.Texture(
'https://playground.babylonjs.com/textures/flare.png', scene);
particleSystem.emitter = new BABYLON.Vector3(0, 15, 0);
particleSystem.minEmitBox = new BABYLON.Vector3(-15, 0, -15);
particleSystem.maxEmitBox = new BABYLON.Vector3(15, 0, 15);
particleSystem.color1 = new BABYLON.Color4(0.7, 0.7, 1, 0.8);
particleSystem.color2 = new BABYLON.Color4(0.5, 0.5, 0.8, 0.6);
particleSystem.minSize = 0.05;
particleSystem.maxSize = 0.15;
particleSystem.minLifeTime = 1;
particleSystem.maxLifeTime = 2;
particleSystem.emitRate = 1000;
particleSystem.direction1 = new BABYLON.Vector3(0, -20, 0);
particleSystem.direction2 = new BABYLON.Vector3(0, -25, 0);
particleSystem.gravity = new BABYLON.Vector3(0, -9.8, 0);
particleSystem.blendMode = BABYLON.ParticleSystem.BLENDMODE_ADD;
particleSystem.start();
} else if (weatherType === 'snow') {
particleSystem = new BABYLON.ParticleSystem('snow', 2000, scene);
particleSystem.particleTexture = new BABYLON.Texture(
'https://playground.babylonjs.com/textures/flare.png', scene);
particleSystem.emitter = new BABYLON.Vector3(0, 15, 0);
particleSystem.minEmitBox = new BABYLON.Vector3(-15, 0, -15);
particleSystem.maxEmitBox = new BABYLON.Vector3(15, 0, 15);
particleSystem.color1 = new BABYLON.Color4(1, 1, 1, 1);
particleSystem.color2 = new BABYLON.Color4(0.9, 0.9, 1, 0.8);
particleSystem.minSize = 0.1;
particleSystem.maxSize = 0.3;
particleSystem.minLifeTime = 3;
particleSystem.maxLifeTime = 5;
particleSystem.emitRate = 500;
particleSystem.direction1 = new BABYLON.Vector3(-2, -5, -2);
particleSystem.direction2 = new BABYLON.Vector3(2, -8, 2);
particleSystem.gravity = new BABYLON.Vector3(0, -2, 0);
particleSystem.start();
}
}
// 更新场景显示
function updateScene() {
const cityData = citiesData[currentCityIndex];
createTempBar(cityData.temperature);
createHumidityRing(cityData.humidity);
createWindArrows(cityData.windSpeed);
createWeatherEffect(cityData.weather);
console.log('');
console.log('📊 当前城市天气:');
console.log(` 🏙️ 城市:${cityData.name}`);
console.log(` 🌤️ 天气:${cityData.weatherName}`);
console.log(` 🌡️ 温度:${cityData.temperature}°C`);
console.log(` 💧 湿度:${cityData.humidity}%`);
console.log(` 💨 风速:${cityData.windSpeed} km/h`);
console.log('');
}
// 切换到下一个城市
window.nextCity = function() {
currentCityIndex = (currentCityIndex + 1) % citiesData.length;
updateScene();
};
// 切换到上一个城市
window.previousCity = function() {
currentCityIndex = (currentCityIndex - 1 + citiesData.length) % citiesData.length;
updateScene();
};
// 切换到指定城市
window.goToCity = function(index) {
if (index >= 0 && index < citiesData.length) {
currentCityIndex = index;
updateScene();
} else {
console.log('⚠️ 城市索引超出范围');
}
};
// 动画循环
let windOffset = 0;
scene.registerBeforeRender(() => {
if (humidityRing) {
humidityRing.rotation.z += 0.01;
}
windOffset += 0.05;
windArrows.forEach((arrow, i) => {
arrow.position.x = 4 + Math.sin(windOffset + i * 0.6) * 1.5;
});
});
// 初始化显示第一个城市
updateScene();
console.log('✅ 多城市天气系统创建完成!');
console.log('');
console.log('🎮 控制命令:');
console.log(' nextCity() - 下一个城市');
console.log(' previousCity() - 上一个城市');
console.log(' goToCity(0) - 跳转到指定城市(0-4)');
console.log('');
console.log(`📍 城市列表:`);
citiesData.forEach((city, i) => {
console.log(` ${i}: ${city.name} (${city.weatherName})`);
});
console.log('');
console.log('💡 下一步:键盘控制(step5-keyboard-control.xsml)');
} catch (error) {
console.error('💥 创建失败:', error);
}
</script>
</head>
<space></space>
</xsml>
北京晴天:明亮干燥
哈尔滨雪天:冰天雪地
第五步:键盘控制(关键!)
这一步最重要!因为我暂时没有 Rokid 眼镜,全靠键盘调试。
我设计了非常方便的快捷键:
← → 或 A D → 切换城市
1 2 3 4 5 → 直接跳转
H → 显示帮助
<xsml version="1.0">
<head>
<title>步骤5:键盘控制</title>
<script>
console.log('🌍 3D天气可视化系统 - 步骤5:键盘控制');
console.log('💡 点击页面后按键盘操作:← → 切换城市,1-5 直接跳转,H 帮助');
try {
const scene = spatialDocument.scene;
// 基础场景
scene.meshes.forEach(mesh => mesh.dispose());
scene.lights.forEach(light => light.dispose());
// 调整相机位置 - 第一人称视角
if (scene.activeCamera) {
scene.activeCamera.position = new BABYLON.Vector3(0, 1.6, -12);
scene.activeCamera.setTarget(new BABYLON.Vector3(0, 0.6, 0));
console.log('📷 相机设置完成:第一人称视角');
}
const hemiLight = new BABYLON.HemisphericLight('hemiLight',
new BABYLON.Vector3(0, 1, 0), scene);
hemiLight.intensity = 0.6;
const sunLight = new BABYLON.DirectionalLight('sunLight',
new BABYLON.Vector3(-1, -2, -1), scene);
sunLight.intensity = 0.8;
const ground = BABYLON.MeshBuilder.CreateGround('ground', {
width: 50, height: 50
}, scene);
const groundMaterial = new BABYLON.StandardMaterial('groundMat', scene);
groundMaterial.diffuseColor = new BABYLON.Color3(0.3, 0.5, 0.3);
ground.material = groundMaterial;
// 设置背景色
scene.clearColor = new BABYLON.Color4(0.5, 0.7, 1, 1);
// 城市数据
const citiesData = [
{ name: '北京', weather: 'sunny', weatherName: '晴天', temperature: 25, humidity: 45, windSpeed: 12 },
{ name: '上海', weather: 'rainy', weatherName: '雨天', temperature: 18, humidity: 85, windSpeed: 20 },
{ name: '哈尔滨', weather: 'snow', weatherName: '雪天', temperature: -5, humidity: 70, windSpeed: 8 },
{ name: '成都', weather: 'cloudy', weatherName: '多云', temperature: 22, humidity: 65, windSpeed: 6 },
{ name: '广州', weather: 'foggy', weatherName: '雾霾', temperature: 28, humidity: 90, windSpeed: 4 }
];
let currentCityIndex = 0;
let tempBar = null;
let humidityRing = null;
let windArrows = [];
let particleSystem = null;
// 工具函数
function getTemperatureColor(temp) {
if (temp < 0) return new BABYLON.Color3(0, 0.5, 1);
if (temp < 15) return new BABYLON.Color3(0, 1, 1);
if (temp < 25) return new BABYLON.Color3(0, 1, 0);
if (temp < 35) return new BABYLON.Color3(1, 1, 0);
return new BABYLON.Color3(1, 0.3, 0);
}
function createTempBar(temp) {
if (tempBar) tempBar.dispose();
const height = Math.abs(temp) / 5 + 1;
tempBar = BABYLON.MeshBuilder.CreateCylinder('tempBar', {
height: height, diameter: 1.5
}, scene);
tempBar.position = new BABYLON.Vector3(-4, 1.6, 0);
const material = new BABYLON.StandardMaterial('tempMat', scene);
material.diffuseColor = getTemperatureColor(temp);
material.emissiveColor = material.diffuseColor.scale(0.4);
tempBar.material = material;
}
function createHumidityRing(humidity) {
if (humidityRing) humidityRing.dispose();
humidityRing = BABYLON.MeshBuilder.CreateTorus('humidityRing', {
diameter: 3, thickness: 0.4
}, scene);
humidityRing.position = new BABYLON.Vector3(0, 1.6, 0);
humidityRing.rotation.x = Math.PI / 2;
const material = new BABYLON.StandardMaterial('humidityMat', scene);
const ratio = humidity / 100;
material.diffuseColor = new BABYLON.Color3(0.2, 0.5 + ratio * 0.5, 1);
material.emissiveColor = material.diffuseColor.scale(0.5);
material.alpha = 0.4 + ratio * 0.4;
humidityRing.material = material;
}
function createWindArrows(windSpeed) {
windArrows.forEach(arrow => arrow.dispose());
windArrows = [];
const count = Math.floor(windSpeed / 4) + 1;
for (let i = 0; i < count; i++) {
const arrow = BABYLON.MeshBuilder.CreateCylinder(`arrow${i}`, {
height: 2, diameterTop: 0, diameterBottom: 0.4
}, scene);
arrow.position = new BABYLON.Vector3(4, 1.6 + i * 0.5 - (count * 0.25), 0);
arrow.rotation.z = -Math.PI / 2;
const material = new BABYLON.StandardMaterial(`arrowMat${i}`, scene);
material.diffuseColor = new BABYLON.Color3(1, 1, 1);
material.emissiveColor = new BABYLON.Color3(0.6, 0.6, 0.6);
arrow.material = material;
windArrows.push(arrow);
}
}
function createWeatherEffect(weatherType) {
if (particleSystem) {
particleSystem.stop();
particleSystem.dispose();
particleSystem = null;
}
if (weatherType === 'rainy') {
particleSystem = new BABYLON.ParticleSystem('rain', 3000, scene);
particleSystem.particleTexture = new BABYLON.Texture(
'https://playground.babylonjs.com/textures/flare.png', scene);
particleSystem.emitter = new BABYLON.Vector3(0, 15, 0);
particleSystem.minEmitBox = new BABYLON.Vector3(-15, 0, -15);
particleSystem.maxEmitBox = new BABYLON.Vector3(15, 0, 15);
particleSystem.color1 = new BABYLON.Color4(0.7, 0.7, 1, 0.8);
particleSystem.color2 = new BABYLON.Color4(0.5, 0.5, 0.8, 0.6);
particleSystem.minSize = 0.05;
particleSystem.maxSize = 0.15;
particleSystem.minLifeTime = 1;
particleSystem.maxLifeTime = 2;
particleSystem.emitRate = 1000;
particleSystem.direction1 = new BABYLON.Vector3(0, -20, 0);
particleSystem.direction2 = new BABYLON.Vector3(0, -25, 0);
particleSystem.gravity = new BABYLON.Vector3(0, -9.8, 0);
particleSystem.blendMode = BABYLON.ParticleSystem.BLENDMODE_ADD;
particleSystem.start();
} else if (weatherType === 'snow') {
particleSystem = new BABYLON.ParticleSystem('snow', 2000, scene);
particleSystem.particleTexture = new BABYLON.Texture(
'https://playground.babylonjs.com/textures/flare.png', scene);
particleSystem.emitter = new BABYLON.Vector3(0, 15, 0);
particleSystem.minEmitBox = new BABYLON.Vector3(-15, 0, -15);
particleSystem.maxEmitBox = new BABYLON.Vector3(15, 0, 15);
particleSystem.color1 = new BABYLON.Color4(1, 1, 1, 1);
particleSystem.color2 = new BABYLON.Color4(0.9, 0.9, 1, 0.8);
particleSystem.minSize = 0.1;
particleSystem.maxSize = 0.3;
particleSystem.minLifeTime = 3;
particleSystem.maxLifeTime = 5;
particleSystem.emitRate = 500;
particleSystem.direction1 = new BABYLON.Vector3(-2, -5, -2);
particleSystem.direction2 = new BABYLON.Vector3(2, -8, 2);
particleSystem.gravity = new BABYLON.Vector3(0, -2, 0);
particleSystem.start();
}
}
function updateScene() {
const cityData = citiesData[currentCityIndex];
createTempBar(cityData.temperature);
createHumidityRing(cityData.humidity);
createWindArrows(cityData.windSpeed);
createWeatherEffect(cityData.weather);
console.log('');
console.log(`📍 ${cityData.name} - ${cityData.weatherName}`);
console.log(` 🌡️ ${cityData.temperature}°C 💧 ${cityData.humidity}% 💨 ${cityData.windSpeed}km/h`);
}
// 键盘控制系统
window.addEventListener('keydown', (event) => {
console.log('🎹 按键:', event.key);
switch(event.key) {
case 'ArrowLeft':
case 'a':
case 'A':
currentCityIndex = (currentCityIndex - 1 + citiesData.length) % citiesData.length;
updateScene();
console.log('⬅️ 上一个城市');
break;
case 'ArrowRight':
case 'd':
case 'D':
currentCityIndex = (currentCityIndex + 1) % citiesData.length;
updateScene();
console.log('➡️ 下一个城市');
break;
case '1':
currentCityIndex = 0;
updateScene();
break;
case '2':
currentCityIndex = 1;
updateScene();
break;
case '3':
currentCityIndex = 2;
updateScene();
break;
case '4':
currentCityIndex = 3;
updateScene();
break;
case '5':
currentCityIndex = 4;
updateScene();
break;
case 'h':
case 'H':
console.log('');
console.log('⌨️ 键盘控制说明:');
console.log(' ← / A - 上一个城市');
console.log(' → / D - 下一个城市');
console.log(' 1-5 - 直接跳转到城市');
console.log(' H - 显示此帮助');
console.log('');
break;
}
});
// 动画循环
let windOffset = 0;
scene.registerBeforeRender(() => {
if (humidityRing) {
humidityRing.rotation.z += 0.01;
}
windOffset += 0.05;
windArrows.forEach((arrow, i) => {
arrow.position.x = 4 + Math.sin(windOffset + i * 0.6) * 1.5;
});
});
// 初始化
updateScene();
console.log('✅ 键盘控制系统创建完成!');
console.log('');
console.log('⌨️ 键盘控制说明:');
console.log(' ← / A - 上一个城市');
console.log(' → / D - 下一个城市');
console.log(' 1-5 - 直接跳转到城市');
console.log(' H - 显示帮助');
console.log('');
console.log('📍 城市列表:');
citiesData.forEach((city, i) => {
console.log(` ${i + 1}: ${city.name} (${city.weatherName})`);
});
console.log('');
console.log('💡 如果按键没反应,请点击页面任意位置获取焦点!');
} catch (error) {
console.error('💥 创建失败:', error);
}
</script>
</head>
<space></space>
</xsml>
关键设计:所有元素都放在视线高度(y=1.6),模拟戴上 AR 眼镜平视前方的效果!
- 左侧(x=-4):温度柱
- 中间(x=0):湿度环
- 右侧(x=4):风速箭头
这样即使没有 AR 眼镜,也能体验到真实的第一人称视角。
按键即可切换,超方便
调试技巧:
- 每次按键都会在控制台打印
🎹 按键: xxx,能看到是否接收到 - 每次切换都打印城市信息,方便观察
- 用 Emoji 让日志更好看(🌧️ 🌡️ 💧)
- 按 H 随时查看帮助
- 如果没反应,先点击页面!
完整版:集大成者
把所有功能整合到一起,再加点细节优化:
优化 1:根据天气调整光照
if (weather === 'rainy') {
// 雨天:暗一点
hemiLight.intensity = 0.3;
} else if (weather === 'sunny') {
// 晴天:亮一点
hemiLight.intensity = 0.8;
}
优化 2:动画更流畅
// 温度柱微微脉动
const pulse = Math.sin(time) * 0.03 + 1;
tempBar.scaling.x = pulse;
优化 3:清理内存
// 切换城市前销毁旧对象
if (oldParticleSystem) {
oldParticleSystem.dispose(); // 很重要!
}
代码文件:weather-system.xsml
<xsml version="1.0">
<head>
<title>3D天气可视化系统 - 完整版</title>
<script>
console.log('🌍 3D天气可视化系统 - 完整版');
console.log('💡 提示:点击页面后按键盘 ← → 切换城市,按 1-5 直接跳转,按 H 查看帮助');
try {
const scene = spatialDocument.scene;
// 清理场景
scene.meshes.forEach(mesh => mesh.dispose());
scene.lights.forEach(light => light.dispose());
// 设置相机 - 第一人称视角,平视
if (scene.activeCamera) {
scene.activeCamera.position = new BABYLON.Vector3(0, 1.6, -12);
scene.activeCamera.setTarget(new BABYLON.Vector3(0, 0.6, 0));
console.log('📷 相机设置完成:第一人称视角 - 位置(0, 1.6, -12) 目标(0, 1.6, 0)');
}
// 光源系统
const hemiLight = new BABYLON.HemisphericLight('hemiLight',
new BABYLON.Vector3(0, 1, 0), scene);
hemiLight.intensity = 0.6;
const sunLight = new BABYLON.DirectionalLight('sunLight',
new BABYLON.Vector3(-1, -2, -1), scene);
sunLight.intensity = 0.8;
sunLight.position = new BABYLON.Vector3(20, 40, 20);
console.log('💡 光源系统完成');
// 地面
const ground = BABYLON.MeshBuilder.CreateGround('ground', {
width: 50,
height: 50
}, scene);
const groundMaterial = new BABYLON.StandardMaterial('groundMat', scene);
groundMaterial.diffuseColor = new BABYLON.Color3(0.3, 0.5, 0.3);
groundMaterial.specularColor = new BABYLON.Color3(0.1, 0.1, 0.1);
ground.material = groundMaterial;
// 天空盒 - 暂时禁用,避免遮挡视线
// const skybox = BABYLON.MeshBuilder.CreateBox('skybox', {
// size: 1000
// }, scene);
// const skyboxMaterial = new BABYLON.StandardMaterial('skyboxMat', scene);
// skyboxMaterial.backFaceCulling = false;
// skyboxMaterial.diffuseColor = new BABYLON.Color3(0, 0, 0);
// skyboxMaterial.specularColor = new BABYLON.Color3(0, 0, 0);
// skyboxMaterial.emissiveColor = new BABYLON.Color3(0.5, 0.7, 1);
// skyboxMaterial.disableLighting = true;
// skybox.material = skyboxMaterial;
// 设置场景背景色代替天空盒
scene.clearColor = new BABYLON.Color4(0.5, 0.7, 1, 1);
console.log('🏞️ 场景环境完成');
// 城市天气数据库
const citiesData = [
{
name: '北京',
weather: 'sunny',
weatherName: '晴天',
temperature: 25,
humidity: 45,
windSpeed: 12,
icon: '☀️'
},
{
name: '上海',
weather: 'rainy',
weatherName: '雨天',
temperature: 18,
humidity: 85,
windSpeed: 20,
icon: '🌧️'
},
{
name: '哈尔滨',
weather: 'snow',
weatherName: '雪天',
temperature: -5,
humidity: 70,
windSpeed: 8,
icon: '❄️'
},
{
name: '成都',
weather: 'cloudy',
weatherName: '多云',
temperature: 22,
humidity: 65,
windSpeed: 6,
icon: '☁️'
},
{
name: '广州',
weather: 'foggy',
weatherName: '雾霾',
temperature: 28,
humidity: 90,
windSpeed: 4,
icon: '🌫️'
}
];
let currentCityIndex = 0;
// 可视化元素引用
let tempBar = null;
let humidityRing = null;
let windArrows = [];
let particleSystem = null;
// 温度颜色映射函数
function getTemperatureColor(temp) {
if (temp < 0) return new BABYLON.Color3(0, 0.5, 1);
if (temp < 10) return new BABYLON.Color3(0, 0.8, 1);
if (temp < 20) return new BABYLON.Color3(0, 1, 0.5);
if (temp < 30) return new BABYLON.Color3(0, 1, 0);
if (temp < 35) return new BABYLON.Color3(1, 1, 0);
return new BABYLON.Color3(1, 0.3, 0);
}
// 创建温度柱体
function createTempBar(temp) {
if (tempBar) tempBar.dispose();
const absTemp = Math.abs(temp);
const height = absTemp / 4 + 1;
tempBar = BABYLON.MeshBuilder.CreateCylinder('tempBar', {
height: height,
diameter: 1.5,
tessellation: 32
}, scene);
// 抬高到视线高度 (y=1.6 是人眼高度)
tempBar.position = new BABYLON.Vector3(-4, 1.6, 0);
const material = new BABYLON.StandardMaterial('tempMat', scene);
material.diffuseColor = getTemperatureColor(temp);
material.emissiveColor = material.diffuseColor.scale(0.4);
material.specularColor = new BABYLON.Color3(0.3, 0.3, 0.3);
tempBar.material = material;
console.log(` 🌡️ 温度柱已创建:高度=${height.toFixed(1)}, 位置=(-6, ${(height/2).toFixed(1)}, 0)`);
}
// 创建湿度环
function createHumidityRing(humidity) {
if (humidityRing) humidityRing.dispose();
humidityRing = BABYLON.MeshBuilder.CreateTorus('humidityRing', {
diameter: 3,
thickness: 0.4,
tessellation: 64
}, scene);
humidityRing.position = new BABYLON.Vector3(0, 1.6, 0);
humidityRing.rotation.x = Math.PI / 2;
const material = new BABYLON.StandardMaterial('humidityMat', scene);
const ratio = humidity / 100;
material.diffuseColor = new BABYLON.Color3(0.2, 0.5 + ratio * 0.5, 1);
material.emissiveColor = material.diffuseColor.scale(0.5);
material.alpha = 0.4 + ratio * 0.4;
humidityRing.material = material;
console.log(` 💧 湿度环已创建:直径=4, 位置=(0, 3, 0), 透明度=${material.alpha.toFixed(2)}`);
}
// 创建风速箭头
function createWindArrows(windSpeed) {
windArrows.forEach(arrow => arrow.dispose());
windArrows = [];
const count = Math.max(1, Math.floor(windSpeed / 4));
for (let i = 0; i < count; i++) {
const arrow = BABYLON.MeshBuilder.CreateCylinder(`arrow${i}`, {
height: 2,
diameterTop: 0,
diameterBottom: 0.4
}, scene);
arrow.position = new BABYLON.Vector3(4, 1.6 + i * 0.5 - (count * 0.25), 0);
arrow.rotation.z = -Math.PI / 2;
const material = new BABYLON.StandardMaterial(`arrowMat${i}`, scene);
material.diffuseColor = new BABYLON.Color3(1, 1, 1);
material.emissiveColor = new BABYLON.Color3(0.6, 0.6, 0.6);
arrow.material = material;
windArrows.push(arrow);
}
console.log(` 💨 风速箭头已创建:${count}个, 位置=(6, 1.5~${(1.5 + (count-1) * 1).toFixed(1)}, 0)`);
}
// 创建天气粒子效果
function createWeatherEffect(weatherType) {
if (particleSystem) {
particleSystem.stop();
particleSystem.dispose();
particleSystem = null;
}
switch(weatherType) {
case 'rainy':
particleSystem = new BABYLON.ParticleSystem('rain', 3500, scene);
particleSystem.particleTexture = new BABYLON.Texture(
'https://playground.babylonjs.com/textures/flare.png', scene);
particleSystem.emitter = new BABYLON.Vector3(0, 15, 0);
particleSystem.minEmitBox = new BABYLON.Vector3(-15, 0, -15);
particleSystem.maxEmitBox = new BABYLON.Vector3(15, 0, 15);
particleSystem.color1 = new BABYLON.Color4(0.7, 0.7, 1, 0.9);
particleSystem.color2 = new BABYLON.Color4(0.5, 0.5, 0.9, 0.7);
particleSystem.colorDead = new BABYLON.Color4(0.3, 0.3, 0.6, 0);
particleSystem.minSize = 0.05;
particleSystem.maxSize = 0.18;
particleSystem.minLifeTime = 0.8;
particleSystem.maxLifeTime = 1.8;
particleSystem.emitRate = 1200;
particleSystem.direction1 = new BABYLON.Vector3(-1, -22, -1);
particleSystem.direction2 = new BABYLON.Vector3(1, -28, 1);
particleSystem.gravity = new BABYLON.Vector3(0, -12, 0);
particleSystem.blendMode = BABYLON.ParticleSystem.BLENDMODE_ADD;
particleSystem.start();
console.log('🌧️ 雨天效果');
break;
case 'snow':
particleSystem = new BABYLON.ParticleSystem('snow', 2500, scene);
particleSystem.particleTexture = new BABYLON.Texture(
'https://playground.babylonjs.com/textures/flare.png', scene);
particleSystem.emitter = new BABYLON.Vector3(0, 15, 0);
particleSystem.minEmitBox = new BABYLON.Vector3(-15, 0, -15);
particleSystem.maxEmitBox = new BABYLON.Vector3(15, 0, 15);
particleSystem.color1 = new BABYLON.Color4(1, 1, 1, 1);
particleSystem.color2 = new BABYLON.Color4(0.95, 0.95, 1, 0.9);
particleSystem.colorDead = new BABYLON.Color4(0.8, 0.8, 0.9, 0);
particleSystem.minSize = 0.12;
particleSystem.maxSize = 0.35;
particleSystem.minLifeTime = 4;
particleSystem.maxLifeTime = 6;
particleSystem.emitRate = 600;
particleSystem.direction1 = new BABYLON.Vector3(-2, -4, -2);
particleSystem.direction2 = new BABYLON.Vector3(2, -7, 2);
particleSystem.gravity = new BABYLON.Vector3(0, -2.5, 0);
particleSystem.blendMode = BABYLON.ParticleSystem.BLENDMODE_STANDARD;
particleSystem.start();
console.log('❄️ 雪天效果');
break;
case 'foggy':
case 'cloudy':
particleSystem = new BABYLON.ParticleSystem('fog', 800, scene);
particleSystem.particleTexture = new BABYLON.Texture(
'https://playground.babylonjs.com/textures/cloud.png', scene);
particleSystem.emitter = new BABYLON.Vector3(0, 2, 0);
particleSystem.minEmitBox = new BABYLON.Vector3(-10, -1, -10);
particleSystem.maxEmitBox = new BABYLON.Vector3(10, 1, 10);
particleSystem.color1 = new BABYLON.Color4(0.75, 0.75, 0.75, 0.35);
particleSystem.color2 = new BABYLON.Color4(0.85, 0.85, 0.85, 0.25);
particleSystem.colorDead = new BABYLON.Color4(0.7, 0.7, 0.7, 0);
particleSystem.minSize = 3;
particleSystem.maxSize = 6;
particleSystem.minLifeTime = 12;
particleSystem.maxLifeTime = 18;
particleSystem.emitRate = 40;
particleSystem.direction1 = new BABYLON.Vector3(-1.5, 0.5, -1.5);
particleSystem.direction2 = new BABYLON.Vector3(1.5, 1.5, 1.5);
particleSystem.gravity = new BABYLON.Vector3(0, 0, 0);
particleSystem.blendMode = BABYLON.ParticleSystem.BLENDMODE_STANDARD;
particleSystem.start();
console.log('🌫️ 雾天/多云效果');
break;
case 'sunny':
default:
console.log('☀️ 晴天(无粒子)');
break;
}
}
// 更新整个场景
function updateScene() {
const cityData = citiesData[currentCityIndex];
createTempBar(cityData.temperature);
createHumidityRing(cityData.humidity);
createWindArrows(cityData.windSpeed);
createWeatherEffect(cityData.weather);
// 根据天气调整光照和背景色
if (cityData.weather === 'rainy' || cityData.weather === 'foggy') {
hemiLight.intensity = 0.3;
sunLight.intensity = 0.4;
scene.clearColor = new BABYLON.Color4(0.3, 0.4, 0.5, 1);
} else if (cityData.weather === 'snow') {
hemiLight.intensity = 0.5;
sunLight.intensity = 0.5;
scene.clearColor = new BABYLON.Color4(0.6, 0.7, 0.8, 1);
} else {
hemiLight.intensity = 0.6;
sunLight.intensity = 0.8;
scene.clearColor = new BABYLON.Color4(0.5, 0.7, 1, 1);
}
console.log('');
console.log(`${cityData.icon} ${cityData.name} - ${cityData.weatherName}`);
console.log(` 🌡️ 温度: ${cityData.temperature}°C`);
console.log(` 💧 湿度: ${cityData.humidity}%`);
console.log(` 💨 风速: ${cityData.windSpeed} km/h`);
console.log('');
}
// 键盘控制
window.addEventListener('keydown', (event) => {
switch(event.key) {
case 'ArrowLeft':
case 'a':
case 'A':
currentCityIndex = (currentCityIndex - 1 + citiesData.length) % citiesData.length;
updateScene();
console.log('⬅️ 切换到上一个城市');
break;
case 'ArrowRight':
case 'd':
case 'D':
currentCityIndex = (currentCityIndex + 1) % citiesData.length;
updateScene();
console.log('➡️ 切换到下一个城市');
break;
case '1':
currentCityIndex = 0;
updateScene();
break;
case '2':
currentCityIndex = 1;
updateScene();
break;
case '3':
currentCityIndex = 2;
updateScene();
break;
case '4':
currentCityIndex = 3;
updateScene();
break;
case '5':
currentCityIndex = 4;
updateScene();
break;
case 'h':
case 'H':
console.log('');
console.log('⌨️ 键盘控制:');
console.log(' ← / → - 切换城市');
console.log(' A / D - 切换城市');
console.log(' 1-5 - 直接跳转');
console.log(' H - 显示帮助');
console.log('');
break;
}
});
// 动画循环
let windOffset = 0;
let rotationSpeed = 0;
scene.registerBeforeRender(() => {
// 湿度环旋转
if (humidityRing) {
rotationSpeed += 0.0001;
humidityRing.rotation.z += 0.008 + rotationSpeed;
}
// 风速箭头移动
windOffset += 0.06;
windArrows.forEach((arrow, i) => {
arrow.position.x = 4 + Math.sin(windOffset + i * 0.6) * 1.5;
});
// 温度柱体微脉动
if (tempBar) {
const pulse = Math.sin(Date.now() * 0.001) * 0.03 + 1;
tempBar.scaling.x = pulse;
tempBar.scaling.z = pulse;
}
});
// 初始化
updateScene();
console.log('');
console.log('✨ 3D天气可视化系统创建完成!');
console.log('');
console.log('⌨️ 键盘控制:');
console.log(' ← / → (或 A / D) - 切换城市');
console.log(' 1-5 - 直接跳转到城市');
console.log(' H - 显示帮助');
console.log('');
console.log('📍 城市列表:');
citiesData.forEach((city, i) => {
console.log(` ${i + 1}. ${city.icon} ${city.name} - ${city.weatherName}`);
});
console.log('');
console.log('💡 如果按键没反应,请先点击页面任意位置!');
} catch (error) {
console.error('💥 系统创建失败:', error);
console.error(error.stack);
}
</script>
</head>
<space>
<!-- 3D天气可视化系统 -->
</space>
</xsml>
最终完整版
在 Rokid 眼镜上是什么体验?
虽然我现在还没有实体眼镜(已经下单了!),但根据文档和社区反馈,应该是这样的:
沉浸感
戴上眼镜后,天气效果会叠加在真实世界上。比如:
- 在办公室里,雨滴从天花板"落下来"
- 在户外,虚拟的雪花和真实的风景融合
- 温度柱就漂浮在你的桌子上
交互性
如果配合 Rokid 的手势识别:
- 挥手切换城市
- 捏合放大查看细节
- 语音控制"切换到北京"
实用性
想象这些场景:
- 早晨出门前:一眼看到今天的天气状况
- 旅行规划:对比不同城市的天气
- 新闻播报:主播身边展示立体天气
- 教育场景:给孩子讲解气象知识
踩过的坑 & 解决方案
坑 1:粒子不显示
现象:代码没报错,但看不到粒子。
原因:纹理加载失败。
解决:
// 加个加载监听
texture.onLoadObservable.add(() => {
console.log('纹理加载成功!');
});
坑 2:键盘没反应
现象:按键没有任何响应。
原因:页面失去焦点了。
解决:点击一下页面,或者:
window.focus(); // 强制获取焦点
坑 3:切换城市时卡顿
现象:第一次很流畅,后面越来越卡。
原因:没销毁旧对象,越积越多。
解决:
function cleanup() {
if (oldMesh) oldMesh.dispose();
if (oldParticle) oldParticle.dispose();
}
坑 4:温度柱颜色不对
现象:在黑暗环境下看不到颜色。
原因:只设置了 diffuseColor,没设置 emissiveColor。
解决:
material.emissiveColor = color.scale(0.4); // 加自发光
性能优化心得
粒子数量控制
// 根据设备性能调整
const isMobile = /Mobile/.test(navigator.userAgent);
const count = isMobile ? 1500 : 3000;
按需加载
只在需要的时候创建粒子系统,不要一次性全创建。
材质复用
// 错误做法:每个箭头一个材质
for (let i = 0; i < 10; i++) {
const mat = new Material(); // 浪费!
}
// 正确做法:共用一个材质
const sharedMat = new Material();
for (let i = 0; i < 10; i++) {
arrow.material = sharedMat; // 高效!
}
总结与感想
技术收获
✅ 深入理解了 Babylon.js 粒子系统
✅ 掌握了 3D 数据可视化的设计思路
✅ 学会了 JSAR 的开发流程
✅ 熟悉了键盘交互的实现
开发体验
JSAR 真的很友好。作为一个前端开发者,我只用了一个周末就做出了这个项目。如果用 Unity,估计要学一个月。
粒子系统很好玩。调参数的过程就像画画,每改一个值都能看到不同的效果。虽然花时间,但很有成就感。
键盘控制是神器。没有 AR 设备也能完整开发,这点太重要了。不然每次调试都要戴眼镜,太累了。
给后来者的建议
- 从简单开始:先让代码跑起来,再慢慢加功能
- 多看文档:Babylon.js 文档很全,遇到问题先查文档
- 善用控制台:console.log 是最好的调试工具
- 参数多试:特别是粒子系统,没有完美公式,只能试
- 及时清理:记得 dispose(),不然内存泄漏
参考资料
- JSAR 官方文档:jsar.netlify.app/
- Babylon.js 粒子教程:doc.babylonjs.com/features/fe…
- Rokid 开发者社区:developer.rokid.com/
最后的话
这个项目花了我两个周末的时间,但我觉得很值得。不仅学到了新技术,还做出了一个自己真正想用的东西。
Rokid + JSAR 给了 Web 开发者进入 AR 领域的钥匙。不需要学 C++、C#,不需要搞 Unity、Unreal,用你熟悉的 JavaScript 就能做出酷炫的 AR 应用。这才是降低门槛!
如果你也对 AR 开发感兴趣,强烈推荐试试 JSAR。真的没那么难,一个周末就能入门。
期待看到更多有创意的 JSAR 应用!