Cesium学习笔记(六)粒子系统

本文介绍了粒子系统,它是模拟复杂物理效果的图形技术,常见于电影和游戏。阐述了粒子系统基础,包括粒子属性;介绍多种发射器,如BoxEmitter、CircleEmitter等;说明了如何配置粒子系统,涵盖发射速率、寿命等;还提及风格化粒子的颜色、大小、速度设置,以及UpdateCallback和定位方法。

粒子系统

什么是粒子系统?

粒子系统是一种模拟复杂物理效果的图形技术。粒子系统是小图像的集合,当一起观察时形成更复杂的“模糊”对象,如火、烟雾、天气或烟花。这些复杂的效果是通过使用初始位置、速度和寿命等属性指定单个粒子的行为来控制的。
粒子系统效果在电影和电子游戏中很常见。例如,为了表示飞机的损伤,技术美工可以使用粒子系统来表示飞机引擎上的爆炸,然后渲染不同的粒子系统来表示飞机坠毁时的烟雾轨迹。

粒子系统基础

看一看基本粒子系统的代码:

var particleSystem = viewer.scene.primitives.add(new Cesium.ParticleSystem({
    image : '../../SampleData/smoke.png',
    imageSize : new Cesium.Cartesian2(20, 20),
    startScale : 1.0,	//初始缩放比例
    endScale : 4.0,		//最终缩放比例
    particleLife : 1.0,	//粒子寿命
    speed : 5.0,	//移动速度
    emitter : new Cesium.CircleEmitter(0.5),	//一种粒子发射器,它从一个圆中发射粒子。粒子将被定位在一个圆内,初始速度沿着z矢量。参数是圆的半径
    emissionRate : 5.0,		//发射速率
    modelMatrix : entity.computeModelMatrix(viewer.clock.startTime, new Cesium.Matrix4()),//计算实体在指定时间的变换的模型矩阵。如果方向或位置未定义,则返回undefined。
    lifetime : 16.0
}));

在这里插入图片描述
上面的代码创建了一个ParticleSystem,一个参数化的对象,用来控制单个粒子对象随时间的外观和行为。粒子是由粒子发射器产生的,它们有自己的位置和类型,在设定的时间内存活,然后死亡。
其中一些属性是动态的。注意,这里没有使用可用的单色属性scale,而是使用了startScale和endScale。这些允许你指定粒子大小在粒子生命周期的开始和结束尺度之间的转换。startColor和endColor的工作原理类似。
影响可视输出的其他方法包括最大和最小属性。对于每个具有最大和最小输入的变量,粒子上的变量的实际值将被随机分配到最大和最小输入之间,并在粒子的整个生命周期中保持该值的静态。例如,使用最小速度和最大速度作为随机选择的每个粒子速度的界限。允许这样改变的属性包括imageSize、speed、life和particleLife。

发射器

当粒子诞生时,它的初始位置和速度矢量是由粒子发射器控制的。发射器将每秒产生若干粒子,由发射速率参数指定,初始化为依赖于发射器类型的随机速度。
Cesium有多种粒子发射器:

BoxEmitter

BoxEmitter在盒子内的随机采样位置初始化粒子,并将它们引导出六个盒子面之一。它接受一个Cartesian3参数,指定盒子的宽度、高度和深度维度。
也就是在盒子内部随机采样位置作为粒子的初始位置,粒子可以沿着盒子的六个面朝的方向移动,离开这个盒子。

var particleSystem = scene.primitives.add(new Cesium.ParticleSystem({
    image : '../../SampleData/smoke.png',
    color: Cesium.Color.MAGENTA,
    emissionRate: 5.0,
    emitter: new Cesium.BoxEmitter(new Cesium.Cartesian3(5.0, 5.0, 5.0)),
    imageSize : new Cesium.Cartesian2(25.0, 25.0),
    modelMatrix : entity.computeModelMatrix(viewer.clock.startTime, new Cesium.Matrix4()),
    lifetime : 16.0
}));

在这里插入图片描述

CircleEmitter

CircleEmitter在发射器的上方一个圆内的随机采样位置初始化粒子。它接受一个浮点参数来指定圆的半径。
可以想象为吐烟圈,嘴就是发射器,烟圈就是这个指定的圆,在这个圆中随机采样位置作为粒子的初始位置,然后向上移动粒子,直到生命期结束。

var particleSystem = scene.primitives.add(new Cesium.ParticleSystem({
    image : '../../SampleData/smoke.png',
    color: Cesium.Color.MAGENTA,
    emissionRate: 5.0,
    emitter: new Cesium.CircleEmitter(5.0),
    imageSize : new Cesium.Cartesian2(25.0, 25.0),
    modelMatrix : entity.computeModelMatrix(viewer.clock.startTime, new Cesium.Matrix4()),
    lifetime : 16.0
}));

在这里插入图片描述
如果没有指定发射器,则默认创建circle发射器。

ConeEmitter(圆锥形发射器)

ConeEmitter在锥尖初始化粒子,并将它们以任意角度指向锥外。它接受一个指定圆锥体角度的浮点参数。圆锥是沿着发射器的上轴定向的。

var particleSystem = scene.primitives.add(new Cesium.ParticleSystem({
    image : '../../SampleData/smoke.png',
    color: Cesium.Color.MAGENTA,
    emissionRate: 5.0,
    emitter: new Cesium.ConeEmitter(Cesium.Math.toRadians(30.0)),
    imageSize : new Cesium.Cartesian2(25.0, 25.0),
    modelMatrix : entity.computeModelMatrix(viewer.clock.startTime, new Cesium.Matrix4()),
    lifetime : 16.0
}));

在这里插入图片描述

SphereEmitter

SphereEmitter在球体内的随机采样位置初始化粒子,并将它们从球体中心指向外。它接受一个指定球体半径的float参数。

var particleSystem = scene.primitives.add(new Cesium.ParticleSystem({
    image : '../../SampleData/smoke.png',
    color: Cesium.Color.MAGENTA,
    emissionRate: 5.0,
    emitter: new Cesium.SphereEmitter(5.0),
    imageSize : new Cesium.Cartesian2(25.0, 25.0),
    modelMatrix : entity.computeModelMatrix(viewer.clock.startTime, new Cesium.Matrix4()),
    lifetime : 16.0
}));

在这里插入图片描述

配置粒子系统

粒子发射速率

emissionRate控制着每秒发射多少粒子,可以改变粒子的浓度。
指定一个burst对象数组,在指定的时间发射粒子的爆发。这增加了粒子系统的多样性和爆炸性。
particleSystem中添加下面的代码:

bursts : [
    new Cesium.ParticleBurst({time : 5.0, minimum : 300, maximum : 500}),
    new Cesium.ParticleBurst({time : 10.0, minimum : 50, maximum : 100}),
    new Cesium.ParticleBurst({time : 15.0, minimum : 200, maximum : 300})
]

这样发射器发射的粒子会在给定的时间内从最小逐渐变为最大。

粒子的寿命和系统的寿命

默认情况下,粒子系统将永远运行下去。要使粒子系统运行一个设置的持续时间,请使用lifetime将持续时间指定为秒,并将循环设置为false。

lifetime : 16.0,
loop: false

将particleLife设置为5.0会为系统中的每个粒子的particleLife设置为同样的值。为了随机设置每个粒子的寿命,使用变量minimumParticleLife和maximumParticleLife。

minimumParticleLife: 5.0,
maximumParticleLife: 10.0

风格化粒子

颜色

粒子使用图像和颜色指定的纹理样式,它可以在粒子的生命周期中改变,以创建动态效果。下面的代码使烟雾粒子从绿色淡出到白色。

startColor : Cesium.Color.LIGHTSEAGREEN.withAlpha(0.7),
endColor : Cesium.Color.WHITE.withAlpha(0.0),

大小

粒子的大小是由imageSize控制的。要随机化大小,使用minimumImageSize.xmaximumImageSize.x控制像素级宽度,使用minimumImageSize.ymaximumImageSize.y控制像素级高度。
下面的代码创建30到60像素之间的正方形粒子:

minimumImageSize : new Cesium.Cartesian2(30.0, 30.0),
maximumImageSize : new Cesium.Cartesian2(60.0, 60.0)

粒子的大小可以在其生命周期内通过startScaleendScale属性进行调节,从而使粒子随着时间的推移而增大或缩小。

startScale: 1.0,
endScale: 4.0

速度

速度由speedminimumSpeedmaximumSpeed设置控制。

minimumSpeed: 5.0,
maximumSpeed: 10.0

UpdateCallback

粒子系统可以通过应用更新功能进一步定制。这作为一个手动更新每个粒子的效果,如重力,风,或颜色变化。
粒子系统有一个updateCallback,它在模拟过程中修改粒子的属性。这个函数需要一个粒子和一个模拟时间步长。大多数基于物理的效果将修改速度矢量来改变方向或速度。这是一个让粒子对重力做出反应的例子:

var gravityVector = new Cesium.Cartesian3();
var gravity = -(9.8 * 9.8);
function applyGravity(p, dt) {
    // Compute a local up vector for each particle in geocentric space.
    var position = p.position;

    Cesium.Cartesian3.normalize(position, gravityVector);
    Cesium.Cartesian3.multiplyByScalar(gravityVector, gravity * dt, gravityVector);

    p.velocity = Cesium.Cartesian3.add(p.velocity, gravityVector, p.velocity);
}

这个函数计算一个重力矢量,并使用重力加速度来改变粒子的速度。
将这个重力设置为粒子系统的更新函数:updateCallback : applyGravity

定位

粒子系统使用两个Matrix4转换矩阵定位:

  • modelMatrix:将粒子系统从模型坐标转换为世界坐标。
  • emitterModelMatrix:在粒子系统局部坐标系内转换粒子系统发射器
    我们可以只使用其中一个变换矩阵,而将另一个保留为单位矩阵,但是为了方便起见,我们同时提供了这两个矩阵。为了练习创建矩阵,让我们将粒子发射器相对于另一个实体进行定位。
    为我们的粒子系统创建一个实体来强调。添加以下代码以将牛奶卡车模型添加到查看器中:
var entity = viewer.entities.add({
    model : {
        uri : '../../SampleData/models/CesiumMilkTruck/CesiumMilkTruck-kmc.glb'
    },
    position : Cesium.Cartesian3.fromDegrees(-75.15787310614596, 39.97862668312678)
});
viewer.trackedEntity = entity;

在这里插入图片描述
我们想添加一个烟雾效果来自卡车的尾部。创建一个模型矩阵,将使粒子系统的定位和定向与牛奶卡车实体相同。

modelMatrix: entity.computeModelMatrix(time, new Cesium.Matrix4())

这就把粒子系统放在了卡车的中心。为了将它放置在卡车后面,我们可以创建一个带有平移的矩阵。

function computeEmitterModelMatrix() {
    hpr = Cesium.HeadingPitchRoll.fromDegrees(0.0, 0.0, 0.0, hpr);
    trs.translation = Cesium.Cartesian3.fromElements(-4.0, 0.0, 1.4, translation);
    trs.rotation = Cesium.Quaternion.fromHeadingPitchRoll(hpr, rotation);

    return Cesium.Matrix4.fromTranslationRotationScale(trs, emitterModelMatrix);
}

现在添加粒子系统。

var particleSystem = viewer.scene.primitives.add(new Cesium.ParticleSystem({
    image : '../../SampleData/smoke.png',
    
    startColor : Cesium.Color.LIGHTSEAGREEN.withAlpha(0.7),
    endColor : Cesium.Color.WHITE.withAlpha(0.0),
    
    startScale : 1.0,
    endScale : 4.0,
    
    particleLife : 1.0,
    
    minimumSpeed : 1.0,
    maximumSpeed : 4.0
    
    imageSize : new Cesium.Cartesian2(25, 25),
    emissionRate : 5.0,
    lifetime : 16.0,
    
    modelMatrix : entity.computeModelMatrix(viewer.clock.startTime, new Cesium.Matrix4())
    emitterModelMatrix : computeEmitterModelMatrix()
}));

完整代码:

var viewer = new Cesium.Viewer("cesiumContainer");

//Set the random number seed for consistent results.
Cesium.Math.setRandomNumberSeed(3);

//Set bounds of our simulation time
var start = Cesium.JulianDate.fromDate(new Date(2015, 2, 25, 16));
var stop = Cesium.JulianDate.addSeconds(
  start,
  120,
  new Cesium.JulianDate()
);

//Make sure viewer is at the desired time.
viewer.clock.startTime = start.clone();
viewer.clock.stopTime = stop.clone();
viewer.clock.currentTime = start.clone();
viewer.clock.clockRange = Cesium.ClockRange.LOOP_STOP; //Loop at the end
viewer.clock.multiplier = 1;
viewer.clock.shouldAnimate = true;

//Set timeline to simulation bounds
viewer.timeline.zoomTo(start, stop);

var viewModel = {
  emissionRate: 5.0,
  gravity: 0.0,
  minimumParticleLife: 1.2,
  maximumParticleLife: 1.2,
  minimumSpeed: 1.0,
  maximumSpeed: 4.0,
  startScale: 1.0,
  endScale: 5.0,
  particleSize: 25.0,
};

Cesium.knockout.track(viewModel);
var toolbar = document.getElementById("toolbar");
Cesium.knockout.applyBindings(viewModel, toolbar);

var entityPosition = new Cesium.Cartesian3();
var entityOrientation = new Cesium.Quaternion();
var rotationMatrix = new Cesium.Matrix3();
var modelMatrix = new Cesium.Matrix4();

function computeModelMatrix(entity, time) {
  return entity.computeModelMatrix(time, new Cesium.Matrix4());
}

var emitterModelMatrix = new Cesium.Matrix4();
var translation = new Cesium.Cartesian3();
var rotation = new Cesium.Quaternion();
var hpr = new Cesium.HeadingPitchRoll();
var trs = new Cesium.TranslationRotationScale();

function computeEmitterModelMatrix() {
  hpr = Cesium.HeadingPitchRoll.fromDegrees(0.0, 0.0, 0.0, hpr);
  trs.translation = Cesium.Cartesian3.fromElements(
    -4.0,
    0.0,
    1.4,
    translation
  );
  trs.rotation = Cesium.Quaternion.fromHeadingPitchRoll(hpr, rotation);

  return Cesium.Matrix4.fromTranslationRotationScale(
    trs,
    emitterModelMatrix
  );
}

var pos1 = Cesium.Cartesian3.fromDegrees(
  -75.15787310614596,
  39.97862668312678
);
var pos2 = Cesium.Cartesian3.fromDegrees(
  -75.1633691390455,
  39.95355089912078
);
var position = new Cesium.SampledPositionProperty();

position.addSample(start, pos1);
position.addSample(stop, pos2);

var entity = viewer.entities.add({
  availability: new Cesium.TimeIntervalCollection([
    new Cesium.TimeInterval({
      start: start,
      stop: stop,
    }),
  ]),
  model: {
    uri: "../SampleData/models/CesiumMilkTruck/CesiumMilkTruck.glb",
    minimumPixelSize: 64,
  },
  viewFrom: new Cesium.Cartesian3(-100.0, 0.0, 100.0),
  position: position,
  orientation: new Cesium.VelocityOrientationProperty(position),
});
viewer.trackedEntity = entity;

var scene = viewer.scene;
var particleSystem = scene.primitives.add(
  new Cesium.ParticleSystem({
    image: "../SampleData/smoke.png",

    startColor: Cesium.Color.LIGHTSEAGREEN.withAlpha(0.7),
    endColor: Cesium.Color.WHITE.withAlpha(0.0),

    startScale: viewModel.startScale,
    endScale: viewModel.endScale,

    minimumParticleLife: viewModel.minimumParticleLife,
    maximumParticleLife: viewModel.maximumParticleLife,

    minimumSpeed: viewModel.minimumSpeed,
    maximumSpeed: viewModel.maximumSpeed,

    imageSize: new Cesium.Cartesian2(
      viewModel.particleSize,
      viewModel.particleSize
    ),

    emissionRate: viewModel.emissionRate,

    bursts: [
      // these burst will occasionally sync to create a multicolored effect
      new Cesium.ParticleBurst({
        time: 5.0,
        minimum: 10,
        maximum: 100,
      }),
      new Cesium.ParticleBurst({
        time: 10.0,
        minimum: 50,
        maximum: 100,
      }),
      new Cesium.ParticleBurst({
        time: 15.0,
        minimum: 200,
        maximum: 300,
      }),
    ],

    lifetime: 16.0,

    emitter: new Cesium.CircleEmitter(2.0),

    emitterModelMatrix: computeEmitterModelMatrix(),

    updateCallback: applyGravity,
  })
);

var gravityScratch = new Cesium.Cartesian3();

function applyGravity(p, dt) {
  // We need to compute a local up vector for each particle in geocentric space.
  var position = p.position;

  Cesium.Cartesian3.normalize(position, gravityScratch);
  Cesium.Cartesian3.multiplyByScalar(
    gravityScratch,
    viewModel.gravity * dt,
    gravityScratch
  );

  p.velocity = Cesium.Cartesian3.add(
    p.velocity,
    gravityScratch,
    p.velocity
  );
}

viewer.scene.preUpdate.addEventListener(function (scene, time) {
  particleSystem.modelMatrix = computeModelMatrix(entity, time);

  // Account for any changes to the emitter model matrix.
  particleSystem.emitterModelMatrix = computeEmitterModelMatrix();

  // Spin the emitter if enabled.
  if (viewModel.spin) {
    viewModel.heading += 1.0;
    viewModel.pitch += 1.0;
    viewModel.roll += 1.0;
  }
});

Cesium.knockout
  .getObservable(viewModel, "emissionRate")
  .subscribe(function (newValue) {
    particleSystem.emissionRate = parseFloat(newValue);
  });

Cesium.knockout
  .getObservable(viewModel, "particleSize")
  .subscribe(function (newValue) {
    var particleSize = parseFloat(newValue);
    particleSystem.minimumImageSize.x = particleSize;
    particleSystem.minimumImageSize.y = particleSize;
    particleSystem.maximumImageSize.x = particleSize;
    particleSystem.maximumImageSize.y = particleSize;
  });

Cesium.knockout
  .getObservable(viewModel, "minimumParticleLife")
  .subscribe(function (newValue) {
    particleSystem.minimumParticleLife = parseFloat(newValue);
  });

Cesium.knockout
  .getObservable(viewModel, "maximumParticleLife")
  .subscribe(function (newValue) {
    particleSystem.maximumParticleLife = parseFloat(newValue);
  });

Cesium.knockout
  .getObservable(viewModel, "minimumSpeed")
  .subscribe(function (newValue) {
    particleSystem.minimumSpeed = parseFloat(newValue);
  });

Cesium.knockout
  .getObservable(viewModel, "maximumSpeed")
  .subscribe(function (newValue) {
    particleSystem.maximumSpeed = parseFloat(newValue);
  });

Cesium.knockout
  .getObservable(viewModel, "startScale")
  .subscribe(function (newValue) {
    particleSystem.startScale = parseFloat(newValue);
  });

Cesium.knockout
  .getObservable(viewModel, "endScale")
  .subscribe(function (newValue) {
    particleSystem.endScale = parseFloat(newValue);
  });

var options = [
  {
    text: "Circle Emitter",
    onselect: function () {
      particleSystem.emitter = new Cesium.CircleEmitter(2.0);
    },
  },
  {
    text: "Sphere Emitter",
    onselect: function () {
      particleSystem.emitter = new Cesium.SphereEmitter(2.5);
    },
  },
  {
    text: "Cone Emitter",
    onselect: function () {
      particleSystem.emitter = new Cesium.ConeEmitter(
        Cesium.Math.toRadians(45.0)
      );
    },
  },
  {
    text: "Box Emitter",
    onselect: function () {
      particleSystem.emitter = new Cesium.BoxEmitter(
        new Cesium.Cartesian3(10.0, 10.0, 10.0)
      );
    },
  },
];

Sandcastle.addToolbarMenu(options);

评论 7
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值