JSAR 粒子系统实战:打造炫酷 3D 烟花秀

JSAR 粒子系统实战:打造炫酷 3D 烟花秀

前言

当我第一次在Rokid AR 眼镜上看到 3D 烟花在真实空间中绽放时,那种震撼是难以言表的。作为一名前端开发者,我一直在寻找一种简单的方式来开发 AR 应用,而 JSAR(JavaScript Spatial Application Runtime) 正是我梦寐以求的工具。

本文将带你从零开始,用 JSAR 创建一个令人惊艳的 3D 烟花粒子系统。不需要复杂的 3D 建模,不需要学习 Unity 或 Unreal Engine,只需要你熟悉的 JavaScript/TypeScript 和一点点创意。


什么是 JSAR?为什么选择它?

JSAR 简介

JSAR 是 Rokid 开源的空间应用运行时,它让 Web 开发者能够使用熟悉的技术栈(JavaScript/TypeScript + Babylon.js)来开发 3D 空间应用。

核心优势:

  • 🚀 低门槛:Web 开发者无需学习复杂的 3D 引擎
  • 🎨 强大的 3D 能力:基于 Babylon.js,功能完善
  • 🔧 标准化:使用 XSML(类似 HTML)定义 3D 空间
  • 📱 跨平台潜力:一次编写,多端运行

为什么做烟花项目?

粒子系统是 3D 图形学中的经典主题,烟花效果则是粒子系统最直观、最炫酷的应用之一。通过这个项目,你将学会:

  • Babylon.js 粒子系统的完整使用
  • JSAR 空间应用的开发流程
  • 3D 动画和交互设计
  • 性能优化技巧

开发环境准备

所需工具

  • Node.js:v18.12.1 或更高版本
  • VSCode:推荐安装 JSAR DevTools 扩展
  • 基础知识:JavaScript/TypeScript、基本的 3D 概念

创建项目

bash 复制代码
# 使用官方脚手架创建项目
npm init @yodaos-jsar/widget my-fireworks

# 进入项目目录
cd my-fireworks

# 安装依赖
npm install

执行完成后,你会看到以下项目结构:

perl 复制代码
my-fireworks/
├── main.xsml          # XSML主文件
├── package.json       # 项目配置
├── lib/
│   └── main.ts       # TypeScript代码
├── model/            # 3D模型文件夹
└── icon.png          # 应用图标

核心概念:Babylon.js 粒子系统

在开始编码之前,我们需要理解粒子系统的基本原理。

什么是粒子系统?

粒子系统通过创建大量微小的粒子来模拟复杂的自然现象,如:

  • 🎆 烟花、爆炸
  • 🔥 火焰、烟雾
  • 💧 水流、喷泉
  • ❄️ 雪花、雨滴

Babylon.js ParticleSystem 关键参数

typescript 复制代码
const ps = new BABYLON.ParticleSystem('particles', 2000, scene);

// 1. 粒子外观
ps.particleTexture = texture;        // 粒子贴图
ps.color1 = new BABYLON.Color4(...);  // 起始颜色
ps.color2 = new BABYLON.Color4(...);  // 中间颜色
ps.colorDead = new BABYLON.Color4(...); // 消亡颜色

// 2. 粒子行为
ps.minSize = 0.1;                    // 最小尺寸
ps.maxSize = 0.5;                    // 最大尺寸
ps.minLifeTime = 0.5;                // 最短生命周期
ps.maxLifeTime = 2.0;                // 最长生命周期

// 3. 发射器
ps.emitter = position;               // 发射位置
ps.createSphereEmitter(1);           // 球形发射器(全方向)
ps.emitRate = 1000;                  // 每秒发射粒子数

// 4. 物理效果
ps.gravity = new BABYLON.Vector3(0, -9.8, 0); // 重力
ps.minEmitPower = 5;                 // 最小发射速度
ps.maxEmitPower = 10;                // 最大发射速度

// 5. 渲染模式
ps.blendMode = BABYLON.ParticleSystem.BLENDMODE_ADD; // 叠加混合(发光效果)

循序渐进:4 步打造烟花系统

步骤 1:基础粒子系统

目标:创建一个简单的粒子喷泉,理解粒子系统的基本使用。

创建文件 step1-basic-particle.xsml

xml 复制代码
<xsml version="1.0">
  <head>
    <title>步骤1:基础粒子系统</title>
    <script>
      const scene = spatialDocument.scene;

      // 设置相机位置
      if (scene.activeCamera) {
        scene.activeCamera.position = new BABYLON.Vector3(0, 3, -10);
        scene.activeCamera.setTarget(BABYLON.Vector3.Zero());
      }

      // 添加光源
      const light = new BABYLON.HemisphericLight('light',
        new BABYLON.Vector3(0, 1, 0), scene);
      light.intensity = 0.7;

      // 创建粒子系统
      const particleSystem = new BABYLON.ParticleSystem('particles', 2000, scene);

      // 粒子纹理(使用在线资源)
      particleSystem.particleTexture = new BABYLON.Texture(
        'https://playground.babylonjs.com/textures/flare.png',
        scene
      );

      // 发射器位置(原点)
      particleSystem.emitter = new BABYLON.Vector3(0, 0, 0);

      // 粒子颜色(红色到橙色渐变)
      particleSystem.color1 = new BABYLON.Color4(1, 0, 0, 1);
      particleSystem.color2 = new BABYLON.Color4(1, 0.5, 0, 1);
      particleSystem.colorDead = new BABYLON.Color4(0.5, 0, 0, 0);

      // 粒子大小
      particleSystem.minSize = 0.1;
      particleSystem.maxSize = 0.3;

      // 粒子生命周期(秒)
      particleSystem.minLifeTime = 1;
      particleSystem.maxLifeTime = 2;

      // 发射速率(每秒粒子数)
      particleSystem.emitRate = 500;

      // 发射方向(向上的锥形)
      particleSystem.createConeEmitter(1, Math.PI / 4);

      // 发射速度
      particleSystem.minEmitPower = 2;
      particleSystem.maxEmitPower = 4;

      // 重力(向下)
      particleSystem.gravity = new BABYLON.Vector3(0, -5, 0);

      // 混合模式(叠加,产生发光效果)
      particleSystem.blendMode = BABYLON.ParticleSystem.BLENDMODE_ADD;

      // 启动粒子系统
      particleSystem.start();

      console.log('✅ 基础粒子系统创建完成');
    </script>
  </head>
  <space></space>
</xsml>

运行效果:粒子从中心点向上喷射,受重力影响逐渐下落,形成喷泉效果。

关键知识点

  • createConeEmitter() 创建锥形发射器,粒子向上喷射
  • 重力让粒子向下加速,模拟真实物理
  • BLENDMODE_ADD 让粒子叠加后产生更亮的发光效果

步骤 2:球形爆炸效果

目标:将粒子发射方向改为 360 度全方向,模拟烟花爆炸。

创建文件 step2-sphere-explosion.xsml

xml 复制代码
<xsml version="1.0">
  <head>
    <title>步骤2:球形爆炸效果</title>
    <script>
      const scene = spatialDocument.scene;

      // 设置相机
      if (scene.activeCamera) {
        scene.activeCamera.position = new BABYLON.Vector3(0, 3, -10);
        scene.activeCamera.setTarget(new BABYLON.Vector3(0, 3, 0));
      }

      // 光源
      const light = new BABYLON.HemisphericLight('light',
        new BABYLON.Vector3(0, 1, 0), scene);
      light.intensity = 0.6;

      // 创建粒子系统
      const ps = new BABYLON.ParticleSystem('explosion', 2000, scene);

      ps.particleTexture = new BABYLON.Texture(
        'https://playground.babylonjs.com/textures/flare.png',
        scene
      );

      // 发射器位置(空中)
      ps.emitter = new BABYLON.Vector3(0, 5, 0);

      // 蓝色烟花
      ps.color1 = new BABYLON.Color4(0, 0.5, 1, 1);
      ps.color2 = new BABYLON.Color4(0, 1, 1, 0.8);
      ps.colorDead = new BABYLON.Color4(0, 0.3, 0.5, 0);

      ps.minSize = 0.1;
      ps.maxSize = 0.4;
      ps.minLifeTime = 0.5;
      ps.maxLifeTime = 1.5;
      ps.emitRate = 1000;

      // 关键:球形发射器,粒子向所有方向发射
      ps.createSphereEmitter(1);

      // 更快的发射速度,形成爆炸效果
      ps.minEmitPower = 5;
      ps.maxEmitPower = 8;

      ps.gravity = new BABYLON.Vector3(0, -9.8, 0);
      ps.blendMode = BABYLON.ParticleSystem.BLENDMODE_ADD;
      ps.updateSpeed = 0.01;

      ps.start();

      // 创建标记球体,显示爆炸中心
      const marker = BABYLON.MeshBuilder.CreateSphere('marker',
        { diameter: 0.2 }, scene);
      marker.position = new BABYLON.Vector3(0, 5, 0);
      const markerMat = new BABYLON.StandardMaterial('markerMat', scene);
      markerMat.emissiveColor = new BABYLON.Color3(1, 1, 0);
      marker.material = markerMat;

      console.log('✅ 球形爆炸效果创建完成');
    </script>
  </head>
  <space></space>
</xsml>

运行效果:粒子从中心点向四面八方爆炸,形成球形烟花效果。

关键改进

  • createSphereEmitter(1) 替代锥形发射器,实现全方向爆炸
  • 提高发射速度(5-8),让粒子快速扩散
  • 降低生命周期(0.5-1.5 秒),烟花快速消散

步骤 3:多彩烟花

目标:创建多种颜色的烟花,依次绽放。

创建文件 step3-multi-colors.xsml

javascript 复制代码
// 核心代码片段
const colors = [
  { name: '红色', color1: new BABYLON.Color4(1, 0, 0, 1),
    color2: new BABYLON.Color4(1, 0.5, 0, 0.8) },
  { name: '蓝色', color1: new BABYLON.Color4(0, 0.5, 1, 1),
    color2: new BABYLON.Color4(0, 1, 1, 0.8) },
  { name: '绿色', color1: new BABYLON.Color4(0, 1, 0, 1),
    color2: new BABYLON.Color4(0.5, 1, 0, 0.8) },
  { name: '紫色', color1: new BABYLON.Color4(1, 0, 1, 1),
    color2: new BABYLON.Color4(0.8, 0, 1, 0.8) },
  { name: '黄色', color1: new BABYLON.Color4(1, 1, 0, 1),
    color2: new BABYLON.Color4(1, 0.8, 0, 0.8) }
];

function createFirework(position, colorConfig) {
  const ps = new BABYLON.ParticleSystem('firework', 1500, scene);
  // ... 配置粒子系统
  ps.color1 = colorConfig.color1;
  ps.color2 = colorConfig.color2;
  ps.start();

  // 短暂发射后停止
  setTimeout(() => ps.stop(), 100);
  setTimeout(() => ps.dispose(), 2000);
}

// 依次发射不同颜色的烟花
colors.forEach((colorConfig, index) => {
  setTimeout(() => {
    const x = (index - 2) * 3;
    createFirework(new BABYLON.Vector3(x, 5, 0), colorConfig);
  }, index * 800);
});

运行效果:5 种颜色的烟花从左到右依次绽放。

技术要点

  • 使用 setTimeout 控制烟花发射时间
  • 调用 ps.stop() 停止发射新粒子(已发射的粒子继续运动)
  • 及时 dispose() 销毁粒子系统,释放内存

步骤 4:随机发射

目标:在随机位置、随机时间发射随机颜色的烟花。

创建文件 step4-random-launch.xsml

javascript 复制代码
// 随机颜色池(16种颜色)
const colorPalette = [
  '#FF1744', '#F50057', '#D500F9', '#651FFF',
  '#3D5AFE', '#2979FF', '#00B0FF', '#00E5FF',
  '#1DE9B6', '#00E676', '#76FF03', '#FFEA00',
  '#FFC400', '#FF9100', '#FF3D00'
];

function launchRandomFirework() {
  // 随机位置
  const x = (Math.random() - 0.5) * 15;
  const y = Math.random() * 5 + 4;
  const z = (Math.random() - 0.5) * 15;
  const position = new BABYLON.Vector3(x, y, z);

  // 随机颜色
  const color = colorPalette[Math.floor(Math.random() * colorPalette.length)];

  createFirework(position, color);
  console.log(`🎆 发射烟花 位置:(${x.toFixed(1)}, ${y.toFixed(1)}, ${z.toFixed(1)})`);
}

// 定时自动发射
setInterval(() => {
  launchRandomFirework();
}, 1200);

运行效果:烟花在 3D 空间中随机位置不断绽放。

编程技巧

  • 使用 Math.random() 生成随机坐标
  • Color3.FromHexString() 方便地使用十六进制颜色
  • setInterval() 实现定时发射

完整版:最终烟花系统

整合所有功能,创建最终版 fireworks.xsml

完整代码已包含

  • 烟花类封装
  • 自动内存管理
  • 丰富的交互控制
  • 性能优化

运行项目

javascript 复制代码
<xsml version="1.0">
  <head>
    <title>3D烟花粒子系统</title>
    <script>
      console.log('🎆 烟花粒子系统启动中...');

      // 烟花配置
      const FIREWORK_COLORS = [
        '#FF1744', '#F50057', '#D500F9', '#651FFF',
        '#3D5AFE', '#2979FF', '#00B0FF', '#00E5FF',
        '#1DE9B6', '#00E676', '#76FF03', '#FFEA00',
        '#FFC400', '#FF9100', '#FF3D00'
      ];

      class Firework {
        constructor(scene, position) {
          this.scene = scene;
          this.position = position;
          this.color = FIREWORK_COLORS[Math.floor(Math.random() * FIREWORK_COLORS.length)];
          this.particleSystem = null;
          this.isAlive = true;
          this.createParticleSystem();
        }

        createParticleSystem() {
          // 创建粒子系统
          const particleSystem = new BABYLON.ParticleSystem(
            'firework',
            2000,
            this.scene
          );

          // 粒子纹理(使用内置的flare纹理)
          particleSystem.particleTexture = new BABYLON.Texture(
            'https://playground.babylonjs.com/textures/flare.png',
            this.scene
          );

          // 发射器位置
          particleSystem.emitter = this.position;

          // 颜色渐变
          const color = BABYLON.Color3.FromHexString(this.color);
          particleSystem.color1 = new BABYLON.Color4(color.r, color.g, color.b, 1);
          particleSystem.color2 = new BABYLON.Color4(color.r * 0.8, color.g * 0.8, color.b * 0.8, 0.8);
          particleSystem.colorDead = new BABYLON.Color4(color.r * 0.5, color.g * 0.5, color.b * 0.5, 0);

          // 粒子大小
          particleSystem.minSize = 0.1;
          particleSystem.maxSize = 0.5;

          // 粒子生命周期
          particleSystem.minLifeTime = 0.5;
          particleSystem.maxLifeTime = 2.0;

          // 发射速率
          particleSystem.emitRate = 1000;

          // 混合模式(发光效果)
          particleSystem.blendMode = BABYLON.ParticleSystem.BLENDMODE_ADD;

          // 重力
          particleSystem.gravity = new BABYLON.Vector3(0, -9.8, 0);

          // 发射方向(球形爆炸)
          particleSystem.createSphereEmitter(1);

          // 发射速度
          particleSystem.minEmitPower = 5;
          particleSystem.maxEmitPower = 10;
          particleSystem.updateSpeed = 0.01;

          // 启动粒子系统
          particleSystem.start();

          this.particleSystem = particleSystem;

          // 0.5秒后停止发射,粒子会继续运动直到生命周期结束
          setTimeout(() => {
            this.particleSystem.stop();
          }, 100);

          // 3秒后销毁
          setTimeout(() => {
            this.dispose();
          }, 3000);
        }

        dispose() {
          if (this.particleSystem) {
            this.particleSystem.dispose();
            this.particleSystem = null;
          }
          this.isAlive = false;
        }
      }

      try {
        const scene = spatialDocument.scene;
        console.log('✅ Scene获取成功');

        // 设置相机
        if (scene.activeCamera) {
          scene.activeCamera.position = new BABYLON.Vector3(0, 5, -20);
          scene.activeCamera.setTarget(new BABYLON.Vector3(0, 5, 0));
          console.log('📷 相机设置完成');
        }

        // 添加环境光
        scene.lights.forEach(light => light.dispose());
        const light = new BABYLON.HemisphericLight('light', new BABYLON.Vector3(0, 1, 0), scene);
        light.intensity = 0.5;

        // 创建地面参考
        const ground = BABYLON.MeshBuilder.CreateGround('ground', {
          width: 50,
          height: 50
        }, scene);
        const groundMaterial = new BABYLON.StandardMaterial('groundMat', scene);
        groundMaterial.diffuseColor = new BABYLON.Color3(0.1, 0.1, 0.15);
        groundMaterial.specularColor = new BABYLON.Color3(0, 0, 0);
        ground.material = groundMaterial;

        // 烟花管理
        const fireworks = [];

        // 自动发射烟花
        let autoLaunch = true;
        setInterval(() => {
          if (autoLaunch) {
            launchFirework();
          }
        }, 1500);

        // 发射烟花函数
        function launchFirework() {
          const x = (Math.random() - 0.5) * 20;
          const y = Math.random() * 5 + 5;
          const z = (Math.random() - 0.5) * 20;
          const position = new BABYLON.Vector3(x, y, z);

          const firework = new Firework(scene, position);
          fireworks.push(firework);

          // 清理已销毁的烟花
          for (let i = fireworks.length - 1; i >= 0; i--) {
            if (!fireworks[i].isAlive) {
              fireworks.splice(i, 1);
            }
          }

          console.log(`🎆 发射烟花!位置: (${x.toFixed(1)}, ${y.toFixed(1)}, ${z.toFixed(1)})`);
        }

        // 全局控制
        window.launchFirework = launchFirework;

        window.toggleAutoLaunch = () => {
          autoLaunch = !autoLaunch;
          console.log(autoLaunch ? '🔄 自动发射已开启' : '⏸️ 自动发射已暂停');
        };

        window.launchMultiple = (count = 5) => {
          for (let i = 0; i < count; i++) {
            setTimeout(() => {
              launchFirework();
            }, i * 200);
          }
          console.log(`🎇 发射 ${count} 个烟花!`);
        };

        // 初始发射
        setTimeout(() => {
          launchMultiple(3);
        }, 500);

        console.log('✨ 烟花系统初始化完成!');
        console.log('🎮 控制提示:');
        console.log('   - 在控制台输入 launchFirework() 发射单个烟花');
        console.log('   - 在控制台输入 launchMultiple(10) 发射多个烟花');
        console.log('   - 在控制台输入 toggleAutoLaunch() 切换自动发射');

      } catch (error) {
        console.error('💥 烟花系统创建失败:', error);
      }
    </script>
  </head>
  <space>
    <!-- 烟花在夜空中绽放 -->
  </space>
</xsml>

Rokid AR 眼镜上的表现

这个烟花系统在 Rokid AR 眼镜上运行效果令人惊艳。当你戴上眼镜,烟花仿佛在你眼前的真实空间中绽放,你可以:

  • 🚶 走近观察:靠近查看每个粒子的运动轨迹
  • 👀 360 度观看:转动头部从不同角度欣赏烟花
  • 🎯 空间感知:烟花距离你的远近非常真实
  • 🎨 沉浸体验:光影效果在真实环境中叠加

Rokid 的 JSAR 运行时在 AR 眼镜上提供了流畅的 60fps 体验,粒子系统的性能表现优异。这得益于 Babylon.js 的高效渲染引擎和 Rokid 硬件的强大性能。

开发提示:在 Rokid 设备上调试时,建议使用 JSAR DevTools 的远程调试功能,实时查看日志和性能指标。


性能优化建议

1. 粒子数量控制

javascript 复制代码
// 不好的做法:粒子过多
const ps = new BABYLON.ParticleSystem('particles', 10000, scene);

// 推荐做法:适中的粒子数
const ps = new BABYLON.ParticleSystem('particles', 1500, scene);

经验值:单个烟花 1000-2000 粒子,同时存在不超过 10 个烟花。

2. 及时销毁

javascript 复制代码
function createFirework(position, color) {
  const ps = new BABYLON.ParticleSystem('firework', 1500, scene);
  // ... 配置
  ps.start();

  // 0.1秒后停止发射
  setTimeout(() => ps.stop(), 100);

  // 3秒后完全销毁(确保所有粒子消亡)
  setTimeout(() => ps.dispose(), 3000);
}

3. 纹理复用

javascript 复制代码
// 全局纹理,避免重复加载
let particleTexture = null;

function getParticleTexture(scene) {
  if (!particleTexture) {
    particleTexture = new BABYLON.Texture(
      'https://playground.babylonjs.com/textures/flare.png',
      scene
    );
  }
  return particleTexture;
}

4. 使用对象池

javascript 复制代码
class FireworkPool {
  constructor(scene, poolSize = 20) {
    this.scene = scene;
    this.pool = [];
    this.active = [];

    for (let i = 0; i < poolSize; i++) {
      const ps = this.createParticleSystem();
      this.pool.push(ps);
    }
  }

  get() {
    let ps = this.pool.pop();
    if (!ps) {
      ps = this.createParticleSystem();
    }
    this.active.push(ps);
    return ps;
  }

  release(ps) {
    ps.stop();
    ps.reset();
    const index = this.active.indexOf(ps);
    if (index > -1) {
      this.active.splice(index, 1);
      this.pool.push(ps);
    }
  }
}

Rokid 与空间计算的未来

Rokid 通过开源 JSAR,为 Web 开发者打开了空间计算的大门。我们不再需要学习复杂的 AR SDK 或 3D 引擎,只需要用熟悉的 JavaScript 就能创建令人惊艳的 AR 应用。

这个烟花项目只是一个开始。你可以基于此创建:

  • 教育应用:化学分子结构、天文星系可视化
  • 数据可视化:3D 图表、实时数据展示
  • 互动游戏:空间射击、解谜游戏
  • 艺术创作:生成艺术、交互装置

空间计算的时代已经到来,而 Rokid 正在用 JSAR 让这个未来变得触手可及。


参考资料


相关推荐
一树山茶4 小时前
uniapp云函数使用——内容审核
前端·javascript
西西学代码4 小时前
Flutter---坐标网格图标
前端·javascript·flutter
Chloe_lll4 小时前
threejs(五)纹理贴图、顶点UV坐标
javascript·贴图·uv
@PHARAOH4 小时前
HOW - prefetch 二级页面实践
前端·javascript·react.js
咚咚咚小柒5 小时前
【前端】用el-popover做通用悬停气泡(可设置弹框宽度)
前端·javascript·vue.js·elementui·html·scss
执剑、天涯6 小时前
通过一个typescript的小游戏,使用单元测试实战(二)
javascript·typescript·单元测试
古一|7 小时前
vue3都有哪些升级相比vue2-核心响应式系统重构
javascript·vue.js·重构
HHHHHY7 小时前
http接口响应头类型不对,导致svg图片无法预览,前端解决方案
前端·javascript
元亓亓亓7 小时前
考研408--组成原理--day1
开发语言·javascript·考研·计组