diff --git a/Apps/SampleData/fire.png b/Apps/SampleData/fire.png new file mode 100644 index 000000000000..c826c442aa01 Binary files /dev/null and b/Apps/SampleData/fire.png differ diff --git a/Apps/Sandcastle/gallery/Particle System Fireworks.html b/Apps/Sandcastle/gallery/Particle System Fireworks.html new file mode 100644 index 000000000000..cb1f83490928 --- /dev/null +++ b/Apps/Sandcastle/gallery/Particle System Fireworks.html @@ -0,0 +1,174 @@ + + +
+ + + + + +Rate | ++ + + | +
Size | ++ + + | +
Min Life | ++ + + | +
Max Life | ++ + + | +
Min Speed | ++ + + | +
Max Speed | ++ + + | +
Start Scale | ++ + + | +
End Scale | ++ + + | +
Gravity | ++ + + | +
Translation | ++ X + Y + Z + | +
Rotation | ++ H + P + R + | +
Math.acos(value), but first clamps value
to the range [-1.0, 1.0]
* so that the function will never return NaN.
diff --git a/Source/Scene/BoxEmitter.js b/Source/Scene/BoxEmitter.js
new file mode 100644
index 000000000000..f92aea0a76b9
--- /dev/null
+++ b/Source/Scene/BoxEmitter.js
@@ -0,0 +1,83 @@
+/*global define*/
+define([
+ '../Core/defaultValue',
+ '../Core/defineProperties',
+ '../Core/Cartesian3',
+ '../Core/Check',
+ '../Core/Math'
+ ], function(
+ defaultValue,
+ defineProperties,
+ Cartesian3,
+ Check,
+ CesiumMath) {
+ "use strict";
+
+ var defaultDimensions = new Cartesian3(1.0, 1.0, 1.0);
+
+ /**
+ * A ParticleEmitter that emits particles within a box.
+ * Particles will be positioned randomly within the box and have initial velocities emanating from the center of the box.
+ * @constructor
+ *
+ * @param {Cartesian3} dimensions The width, height and depth dimensions of the box.
+ */
+ function BoxEmitter(dimensions) {
+ dimensions = defaultValue(dimensions, defaultDimensions);
+
+ //>>includeStart('debug', pragmas.debug);
+ Check.defined('dimensions', dimensions);
+ Check.typeOf.number.greaterThanOrEquals('dimensions.x', dimensions.x, 0.0);
+ Check.typeOf.number.greaterThanOrEquals('dimensions.y', dimensions.y, 0.0);
+ Check.typeOf.number.greaterThanOrEquals('dimensions.z', dimensions.z, 0.0);
+ //>>includeEnd('debug');
+
+ this._dimensions = Cartesian3.clone(dimensions);
+ }
+
+ defineProperties(BoxEmitter.prototype, {
+ /**
+ * The width, height and depth dimensions of the box in meters.
+ * @memberof BoxEmitter.prototype
+ * @type {Cartesian3}
+ * @default new Cartesian3(1.0, 1.0, 1.0)
+ */
+ dimensions : {
+ get : function() {
+ return this._dimensions;
+ },
+ set : function(value) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.defined('value', value);
+ Check.typeOf.number.greaterThanOrEquals('value.x', value.x, 0.0);
+ Check.typeOf.number.greaterThanOrEquals('value.y', value.y, 0.0);
+ Check.typeOf.number.greaterThanOrEquals('value.z', value.z, 0.0);
+ //>>includeEnd('debug');
+ Cartesian3.clone(value, this._dimensions);
+ }
+ }
+
+ });
+
+ var scratchHalfDim = new Cartesian3();
+
+ /**
+ * Initializes the given {Particle} by setting it's position and velocity.
+ *
+ * @private
+ * @param {Particle} particle The particle to initialize.
+ */
+ BoxEmitter.prototype.emit = function(particle) {
+ var dim = this._dimensions;
+ var halfDim = Cartesian3.multiplyByScalar(dim, 0.5, scratchHalfDim);
+
+ var x = CesiumMath.randomBetween(-halfDim.x, halfDim.x);
+ var y = CesiumMath.randomBetween(-halfDim.y, halfDim.y);
+ var z = CesiumMath.randomBetween(-halfDim.z, halfDim.z);
+
+ particle.position = Cartesian3.fromElements(x, y, z, particle.position);
+ particle.velocity = Cartesian3.normalize(particle.position, particle.velocity);
+ };
+
+ return BoxEmitter;
+});
diff --git a/Source/Scene/CircleEmitter.js b/Source/Scene/CircleEmitter.js
new file mode 100644
index 000000000000..d7a5eb99e895
--- /dev/null
+++ b/Source/Scene/CircleEmitter.js
@@ -0,0 +1,72 @@
+/*global define*/
+define([
+ '../Core/defaultValue',
+ '../Core/defineProperties',
+ '../Core/Cartesian3',
+ '../Core/Check',
+ '../Core/Math'
+ ], function(
+ defaultValue,
+ defineProperties,
+ Cartesian3,
+ Check,
+ CesiumMath) {
+ "use strict";
+
+ /**
+ * A ParticleEmitter that emits particles from a circle.
+ * Particles will be positioned within a circle and have initial velocities going along the z vector.
+ * @constructor
+ *
+ * @param {Number} [radius=1.0] The radius of the circle in meters.
+ */
+ function CircleEmitter(radius) {
+ radius = defaultValue(radius, 1.0);
+
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.number.greaterThan('radius', radius, 0.0);
+ //>>includeEnd('debug');
+
+ this._radius = defaultValue(radius, 1.0);
+ }
+
+ defineProperties(CircleEmitter.prototype, {
+ /**
+ * The radius of the circle in meters.
+ * @memberof CircleEmitter.prototype
+ * @type {Number}
+ * @default 1.0
+ */
+ radius : {
+ get : function() {
+ return this._radius;
+ },
+ set : function(value) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.number.greaterThan('value', value, 0.0);
+ //>>includeEnd('debug');
+ this._radius = value;
+ }
+ }
+ });
+
+ /**
+ * Initializes the given {@link Particle} by setting it's position and velocity.
+ *
+ * @private
+ * @param {Particle} particle The particle to initialize.
+ */
+ CircleEmitter.prototype.emit = function(particle) {
+ var theta = CesiumMath.randomBetween(0.0, CesiumMath.TWO_PI);
+ var rad = CesiumMath.randomBetween(0.0, this._radius);
+
+ var x = rad * Math.cos(theta);
+ var y = rad * Math.sin(theta);
+ var z = 0.0;
+
+ particle.position = Cartesian3.fromElements(x, y, z, particle.position);
+ particle.velocity = Cartesian3.clone(Cartesian3.UNIT_Z, particle.velocity);
+ };
+
+ return CircleEmitter;
+});
diff --git a/Source/Scene/ConeEmitter.js b/Source/Scene/ConeEmitter.js
new file mode 100644
index 000000000000..c6a8b62b2b4e
--- /dev/null
+++ b/Source/Scene/ConeEmitter.js
@@ -0,0 +1,72 @@
+/*global define*/
+define([
+ '../Core/defaultValue',
+ '../Core/defineProperties',
+ '../Core/Cartesian3',
+ '../Core/Check',
+ '../Core/Math'
+ ], function(
+ defaultValue,
+ defineProperties,
+ Cartesian3,
+ Check,
+ CesiumMath) {
+ "use strict";
+
+ var defaultAngle = CesiumMath.toRadians(30.0);
+
+ /**
+ * A ParticleEmitter that emits particles within a cone.
+ * Particles will be positioned at the tip of the cone and have initial velocities going towards the base.
+ * @constructor
+ *
+ * @param {Number} [angle=Cesium.Math.toRadians(30.0)] The angle of the cone in radians.
+ */
+ function ConeEmitter(angle) {
+ this._angle = defaultValue(angle, defaultAngle);
+ }
+
+ defineProperties(ConeEmitter.prototype, {
+ /**
+ * The angle of the cone in radians.
+ * @memberof CircleEmitter.prototype
+ * @type {Number}
+ * @default Cesium.Math.toRadians(30.0)
+ */
+ angle : {
+ get : function() {
+ return this._angle;
+ },
+ set : function(value) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.number('value', value);
+ //>>includeEnd('debug');
+ this._angle = value;
+ }
+ }
+ });
+
+ /**
+ * Initializes the given {Particle} by setting it's position and velocity.
+ *
+ * @private
+ * @param {Particle} particle The particle to initialize
+ */
+ ConeEmitter.prototype.emit = function(particle) {
+ var radius = Math.tan(this._angle);
+
+ // Compute a random point on the cone's base
+ var theta = CesiumMath.randomBetween(0.0, CesiumMath.TWO_PI);
+ var rad = CesiumMath.randomBetween(0.0, radius);
+
+ var x = rad * Math.cos(theta);
+ var y = rad * Math.sin(theta);
+ var z = 1.0;
+
+ particle.velocity = Cartesian3.fromElements(x, y, z, particle.velocity);
+ Cartesian3.normalize(particle.velocity, particle.velocity);
+ particle.position = Cartesian3.clone(Cartesian3.ZERO, particle.position);
+ };
+
+ return ConeEmitter;
+});
diff --git a/Source/Scene/Particle.js b/Source/Scene/Particle.js
new file mode 100644
index 000000000000..ef66eb26ee53
--- /dev/null
+++ b/Source/Scene/Particle.js
@@ -0,0 +1,167 @@
+/*global define*/
+define([
+ '../Core/defaultValue',
+ '../Core/defined',
+ '../Core/defineProperties',
+ '../Core/Cartesian2',
+ '../Core/Cartesian3',
+ '../Core/Color'
+ ],function(
+ defaultValue,
+ defined,
+ defineProperties,
+ Cartesian2,
+ Cartesian3,
+ Color) {
+ "use strict";
+
+ var defaultSize = new Cartesian2(1.0, 1.0);
+
+ /**
+ * A particle emitted by a {@link ParticleSystem}.
+ * @constructor
+ *
+ * @param {Object} options An object with the following properties:
+ * @param {Number} [options.mass=1.0] The mass of particles in kilograms.
+ * @param {Cartesian3} [options.position=Cartesian3.ZERO] The initial position of the particle in world coordinates.
+ * @param {Cartesian3} [options.velocity=Cartesian3.ZERO] The velocity vector of the particle in world coordinates.
+ * @param {Number} [options.life=Number.MAX_VALUE] The life of particles in seconds.
+ * @param {Object} [options.image] The URI, HTMLImageElement, or HTMLCanvasElement to use for the billboard.
+ * @param {Color} [options.startColor=Color.WHITE] The color of a particle when it is born.
+ * @param {Color} [options.endColor=Color.WHITE] The color of a particle when it dies.
+ * @param {Number} [options.startScale=1.0] The scale of the particle when it is born.
+ * @param {Number} [options.endScale=1.0] The scale of the particle when it dies.
+ * @param {Cartesian2} [options.size=new Cartesian2(1.0, 1.0)] The dimensions of particles in pixels.
+ */
+ function Particle(options) {
+ options = defaultValue(options, defaultValue.EMPTY_OBJECT);
+
+ /**
+ * The mass of the particle in kilograms.
+ * @type {Number}
+ * @default 1.0
+ */
+ this.mass = defaultValue(options.mass, 1.0);
+ /**
+ * The positon of the particle in world coordinates.
+ * @type {Cartesian3}
+ * @default Cartesian3.ZERO
+ */
+ this.position = Cartesian3.clone(defaultValue(options.position, Cartesian3.ZERO));
+ /**
+ * The velocity of the particle in world coordinates.
+ * @type {Cartesian3}
+ * @default Cartesian3.ZERO
+ */
+ this.velocity = Cartesian3.clone(defaultValue(options.velocity, Cartesian3.ZERO));
+ /**
+ * The life of the particle in seconds.
+ * @type {Number}
+ * @default Number.MAX_VALUE
+ */
+ this.life = defaultValue(options.life, Number.MAX_VALUE);
+ /**
+ * The image to use for the particle.
+ * @type {Object}
+ * @default undefined
+ */
+ this.image = options.image;
+ /**
+ * The color of the particle when it is born.
+ * @type {Color}
+ * @default Color.WHITE
+ */
+ this.startColor = Color.clone(defaultValue(options.startColor, Color.WHITE));
+ /**
+ * The color of the particle when it dies.
+ * @type {Color}
+ * @default Color.WHITE
+ */
+ this.endColor = Color.clone(defaultValue(options.endColor, Color.WHITE));
+ /**
+ * the scale of the particle when it is born.
+ * @type {Number}
+ * @default 1.0
+ */
+ this.startScale = defaultValue(options.startScale, 1.0);
+ /**
+ * The scale of the particle when it dies.
+ * @type {Number}
+ * @default 1.0
+ */
+ this.endScale = defaultValue(options.endScale, 1.0);
+ /**
+ * The dimensions of the particle in pixels.
+ * @type {Cartesian2}
+ * @default new Cartesian(1.0, 1.0)
+ */
+ this.size = Cartesian2.clone(defaultValue(options.size, defaultSize));
+
+ this._age = 0.0;
+ this._normalizedAge = 0.0;
+
+ // used by ParticleSystem
+ this._billboard = undefined;
+ }
+
+ defineProperties(Particle.prototype, {
+ /**
+ * Gets the age of the particle in seconds.
+ * @memberof Particle.prototype
+ * @type {Number}
+ */
+ age : {
+ get : function() {
+ return this._age;
+ }
+ },
+ /**
+ * Gets the age normalized to a value in the range [0.0, 1.0].
+ * @memberof Particle.prototype
+ * @type {Number}
+ */
+ normalizedAge : {
+ get : function() {
+ return this._normalizedAge;
+ }
+ }
+ });
+
+ var deltaScratch = new Cartesian3();
+
+ /**
+ * @private
+ */
+ Particle.prototype.update = function(dt, forces) {
+ // Apply the velocity
+ Cartesian3.multiplyByScalar(this.velocity, dt, deltaScratch);
+ Cartesian3.add(this.position, deltaScratch, this.position);
+
+ // Update any forces.
+ if (defined(forces)) {
+ var length = forces.length;
+ for (var i = 0; i < length; ++i) {
+ var force = forces[i];
+ if (typeof force === 'function') {
+ // Force is just a simple callback function.
+ force(this, dt);
+ }
+ }
+ }
+
+ // Age the particle
+ this._age += dt;
+
+ // Compute the normalized age.
+ if (this.life === Number.MAX_VALUE) {
+ this._normalizedAge = 0.0;
+ } else {
+ this._normalizedAge = this._age / this.life;
+ }
+
+ // If this particle is older than it's lifespan then die.
+ return this._age <= this.life;
+ };
+
+ return Particle;
+});
diff --git a/Source/Scene/ParticleBurst.js b/Source/Scene/ParticleBurst.js
new file mode 100644
index 000000000000..eec3ee7d1647
--- /dev/null
+++ b/Source/Scene/ParticleBurst.js
@@ -0,0 +1,58 @@
+/*global define*/
+define([
+ '../Core/defaultValue',
+ '../Core/defineProperties'
+ ], function(
+ defaultValue,
+ defineProperties) {
+ 'use strict';
+
+ /**
+ * Represents a burst of {@link Particle}s from a {@link ParticleSystem} at a given time in the systems lifetime.
+ * @constructor
+ *
+ * @param {Object} [options] An object with the following properties:
+ * @param {Number} [options.time=0.0] The time in seconds after the beginning of the particle system's lifetime that the burst will occur.
+ * @param {Number} [options.minimum=0.0] The minimum number of particles emmitted in the burst.
+ * @param {Number} [options.maximum=50.0] The maximum number of particles emitted in the burst.
+ */
+ function ParticleBurst(options) {
+ options = defaultValue(options, defaultValue.EMPTY_OBJECT);
+
+ /**
+ * The time in seconds after the eginning of the particle system's lifetime that the burst will occur.
+ * @type {Number}
+ * @default 0.0
+ */
+ this.time = defaultValue(options.time, 0.0);
+ /**
+ * The minimum number of particles emitted.
+ * @type {Number}
+ * @default 0.0
+ */
+ this.minimum = defaultValue(options.minimum, 0.0);
+ /**
+ * The maximum number of particles emitted.
+ * @type {Number}
+ * @default 50.0
+ */
+ this.maximum = defaultValue(options.maximum, 50.0);
+
+ this._complete = false;
+ }
+
+ defineProperties(ParticleBurst.prototype, {
+ /**
+ * true
if the burst has been completed; false
otherwise.
+ * @memberof ParticleBurst.prototype
+ * @type {Boolean}
+ */
+ complete : {
+ get : function() {
+ return this._complete;
+ }
+ }
+ });
+
+ return ParticleBurst;
+});
diff --git a/Source/Scene/ParticleEmitter.js b/Source/Scene/ParticleEmitter.js
new file mode 100644
index 000000000000..c58c6ff82dd5
--- /dev/null
+++ b/Source/Scene/ParticleEmitter.js
@@ -0,0 +1,42 @@
+/*global define*/
+define([
+ '../Core/defineProperties',
+ '../Core/DeveloperError'
+ ], function(
+ defineProperties,
+ DeveloperError) {
+ 'use strict';
+
+ /**
+ *
+ * An object that initializes a {@link Particle} from a {@link ParticleSystem}.
+ *
+ *
+ * This type describes an interface and is not intended to be instantiated directly.
+ *
+ *
+ * @constructor
+ *
+ * @see BoxEmitter
+ * @see CircleEmitter
+ * @see ConeEmitter
+ * @see SphereEmitter
+ */
+ function ParticleEmitter(options) {
+ //>>includeStart('debug', pragmas.debug);
+ throw new DeveloperError('This type should not be instantiated directly. Instead, use BoxEmitter, CircleEmitter, ConeEmitter or SphereEmitter.');
+ //>>includeEnd('debug');
+ }
+
+ /**
+ * Initializes the given {Particle} by setting it's position and velocity.
+ *
+ * @private
+ * @param {Particle} The particle to initialize
+ */
+ ParticleEmitter.prototype.emit = function(particle) {
+ DeveloperError.throwInstantiationError();
+ };
+
+ return ParticleEmitter;
+});
diff --git a/Source/Scene/ParticleSystem.js b/Source/Scene/ParticleSystem.js
new file mode 100644
index 000000000000..4b99d32ad28b
--- /dev/null
+++ b/Source/Scene/ParticleSystem.js
@@ -0,0 +1,837 @@
+/*global define*/
+define([
+ '../Core/Check',
+ '../Core/defaultValue',
+ '../Core/defined',
+ '../Core/defineProperties',
+ '../Core/destroyObject',
+ '../Core/Cartesian2',
+ '../Core/Cartesian3',
+ '../Core/Event',
+ '../Core/Matrix4',
+ '../Core/Math',
+ '../Core/JulianDate',
+ '../Core/Color',
+ './BillboardCollection',
+ './Particle',
+ './CircleEmitter'
+ ], function(
+ Check,
+ defaultValue,
+ defined,
+ defineProperties,
+ destroyObject,
+ Cartesian2,
+ Cartesian3,
+ Event,
+ Matrix4,
+ CesiumMath,
+ JulianDate,
+ Color,
+ BillboardCollection,
+ Particle,
+ CircleEmitter) {
+ "use strict";
+
+ /**
+ * A ParticleSystem manages the updating and display of a collection of particles.
+ * @constructor
+ *
+ * @param {Object} [options] Object with the following properties:
+ * @param {Boolean} [options.show=true] Whether to display the particle system.
+ * @param {ParticleSystem~applyForce[]} [options.forces] An array of force callbacks.
+ * @param {ParticleEmitter} [options.emitter=new CircleEmitter(0.5)] The particle emitter for this system.
+ * @param {Matrix4} [options.modelMatrix=Matrix4.IDENTITY] The 4x4 transformation matrix that transforms the particle system from model to world coordinates.
+ * @param {Matrix4} [options.emitterModelMatrix=Matrix4.IDENTITY] The 4x4 transformation matrix that transforms the particle system emitter within the particle systems local coordinate system.
+ * @param {Color} [options.startColor=Color.WHITE] The color of a particle when it is born.
+ * @param {Color} [options.endColor=Color.WHITE] The color of a particle when it dies.
+ * @param {Number} [options.startScale=1.0] The scale of the particle when it is born.
+ * @param {Number} [options.endScale=1.0] The scale of the particle when it dies.
+ * @param {Number} [options.rate=5] The number of particles to emit per second.
+ * @param {ParticleBurst[]} [options.bursts] An array of {@link ParticleBurst}, emitting bursts of particles at periodic times.
+ * @param {Boolean} [options.loop=true] Whether the particle system should loop it's bursts when it is complete.
+ * @param {Number} [options.speed] Sets the minimum and maximum speed in meters per second
+ * @param {Number} [options.minimumSpeed=1.0] Sets the minimum speed in meters per second.
+ * @param {Number} [options.maximumSpeed=1.0] Sets the maximum speed in meters per second.
+ * @param {Number} [options.life] Sets the minimum and maximum life of particles in seconds.
+ * @param {Number} [options.minimumLife=5.0] Sets the minimum life of particles in seconds.
+ * @param {Number} [options.maximumLife=5.0] Sets the maximum life of particles in seconds.
+ * @param {Number} [options.mass] Sets the minimum and maximum mass of particles in kilograms.
+ * @param {Number} [options.minimumMass=1.0] Sets the minimum mass of particles in kilograms.
+ * @param {Number} [options.maximumMass=1.0] Sets the maximum mass of particles in kilograms.
+ * @param {Object} [options.image] The URI, HTMLImageElement, or HTMLCanvasElement to use for the billboard.
+ * @param {Number} [options.width] Sets the minimum and maximum width of particles in pixels.
+ * @param {Number} [options.minimumWidth=1.0] Sets the minimum width of particles in pixels.
+ * @param {Number} [options.maximumWidth=1.0] Sets the maximum width of particles in pixels.
+ * @param {Number} [options.height] Sets the minimum and maximum height of particles in pixels.
+ * @param {Number} [options.minimumHeight=1.0] Sets the minimum height of particles in pixels.
+ * @param {Number} [options.maximumHeight=1.0] Sets the maximum height of particles in pixels.
+ * @param {Number} [options.lifeTime=Number.MAX_VALUE] How long the particle system will emit particles, in seconds.
+ *
+ * @demo {@link http://cesiumjs.org/Cesium/Apps/Sandcastle/index.html?src=ParticleSystem.html|Particle Systems Demo}
+ */
+ function ParticleSystem(options) {
+ options = defaultValue(options, defaultValue.EMPTY_OBJECT);
+
+ /**
+ * Whether to display the particle system.
+ * @type {Boolean}
+ * @default true
+ */
+ this.show = defaultValue(options.show, true);
+
+ /**
+ * An array of force callbacks. The callback is passed a {@link Particle} and the difference from the last time
+ * @type {ParticleSystem~applyForce[]}
+ * @default undefined
+ */
+ this.forces = options.forces;
+
+ /**
+ * Whether the particle system should loop it's bursts when it is complete.
+ * @type {Boolean}
+ * @default true
+ */
+ this.loop = defaultValue(options.loop, true);
+
+ /**
+ * The URI, HTMLImageElement, or HTMLCanvasElement to use for the billboard.
+ * @type {Object}
+ * @default undefined
+ */
+ this.image = defaultValue(options.image, undefined);
+
+ var emitter = options.emitter;
+ if (!defined(emitter)) {
+ emitter = new CircleEmitter(0.5);
+ }
+ this._emitter = emitter;
+
+ this._bursts = options.bursts;
+
+ this._modelMatrix = Matrix4.clone(defaultValue(options.modelMatrix, Matrix4.IDENTITY));
+ this._emitterModelMatrix = Matrix4.clone(defaultValue(options.emitterModelMatrix, Matrix4.IDENTITY));
+ this._matrixDirty = true;
+ this._combinedMatrix = new Matrix4();
+
+ this._startColor = Color.clone(defaultValue(options.startColor, Color.WHITE));
+ this._endColor = Color.clone(defaultValue(options.endColor, Color.WHITE));
+
+ this._startScale = defaultValue(options.startScale, 1.0);
+ this._endScale = defaultValue(options.endScale, 1.0);
+
+ this._rate = defaultValue(options.rate, 5);
+
+ this._minimumSpeed = defaultValue(options.speed, defaultValue(options.minimumSpeed, 1.0));
+ this._maximumSpeed = defaultValue(options.speed, defaultValue(options.maximumSpeed, 1.0));
+
+ this._minimumLife = defaultValue(options.life, defaultValue(options.minimumLife, 5.0));
+ this._maximumLife = defaultValue(options.life, defaultValue(options.maximumLife, 5.0));
+
+ this._minimumMass = defaultValue(options.mass, defaultValue(options.minimumMass, 1.0));
+ this._maximumMass = defaultValue(options.mass, defaultValue(options.maximumMass, 1.0));
+
+ this._minimumWidth = defaultValue(options.width, defaultValue(options.minimumWidth, 1.0));
+ this._maximumWidth = defaultValue(options.width, defaultValue(options.maximumWidth, 1.0));
+
+ this._minimumHeight = defaultValue(options.height, defaultValue(options.minimumHeight, 1.0));
+ this._maximumHeight = defaultValue(options.height, defaultValue(options.maximumHeight, 1.0));
+
+ this._lifeTime = defaultValue(options.lifeTime, Number.MAX_VALUE);
+
+ this._billboardCollection = undefined;
+ this._particles = [];
+
+ // An array of available particles that we can reuse instead of allocating new.
+ this._particlePool = [];
+
+ this._previousTime = undefined;
+ this._currentTime = 0.0;
+ this._carryOver = 0.0;
+
+ this._complete = new Event();
+ this._isComplete = false;
+
+ this._updateParticlePool = true;
+ this._particleEstimate = 0;
+ }
+
+ defineProperties(ParticleSystem.prototype, {
+ /**
+ * The particle emitter for this
+ * @memberof ParticleSystem.prototype
+ * @type {ParticleEmitter}
+ * @default CricleEmitter
+ */
+ emitter : {
+ get : function() {
+ return this._emitter;
+ },
+ set : function(value) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.defined('value', value);
+ //>>includeEnd('debug');
+ this._emitter = value;
+ }
+ },
+ /**
+ * An array of {@link ParticleBurst}, emitting bursts of particles at periodic times.
+ * @type {ParticleBurst[]}
+ * @default undefined
+ */
+ bursts : {
+ get : function() {
+ return this._bursts;
+ },
+ set : function(value) {
+ this._bursts = value;
+ this._updateParticlePool = true;
+ }
+ },
+ /**
+ * The 4x4 transformation matrix that transforms the particle system from model to world coordinates.
+ * @memberof ParticleSystem.prototype
+ * @type {Matrix4}
+ * @default Matrix4.IDENTITY
+ */
+ modelMatrix : {
+ get : function() {
+ return this._modelMatrix;
+ },
+ set : function(value) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.defined('value', value);
+ //>>includeEnd('debug');
+ this._matrixDirty = this._matrixDirty || !Matrix4.equals(this._modelMatrix, value);
+ Matrix4.clone(value, this._modelMatrix);
+ }
+ },
+ /**
+ * The 4x4 transformation matrix that transforms the particle system emitter within the particle systems local coordinate system.
+ * @memberof ParticleSystem.prototype
+ * @type {Matrix4}
+ * @default Matrix4.IDENTITY
+ */
+ emitterModelMatrix : {
+ get : function() {
+ return this._emitterModelMatrix;
+ },
+ set : function(value) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.defined('value', value);
+ //>>includeEnd('debug');
+ this._matrixDirty = this._matrixDirty || !Matrix4.equals(this._emitterModelMatrix, value);
+ Matrix4.clone(value, this._emitterModelMatrix);
+ }
+ },
+ /**
+ * The color of a particle when it is born.
+ * @memberof ParticleSystem.prototype
+ * @type {Color}
+ * @default Color.WHITE
+ */
+ startColor : {
+ get : function() {
+ return this._startColor;
+ },
+ set : function(value) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.defined('value', value);
+ //>>includeEnd('debug');
+ Color.clone(value, this._startColor);
+ }
+ },
+ /**
+ * The color of a particle when it dies.
+ * @memberof ParticleSystem.prototype
+ * @type {Color}
+ * @default Color.WHITE
+ */
+ endColor : {
+ get : function() {
+ return this._endColor;
+ },
+ set : function(value) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.defined('value', value);
+ //>>includeEnd('debug');
+ Color.clone(value, this._endColor);
+ }
+ },
+ /**
+ * The scale of the particle when it is born.
+ * @memberof ParticleSystem.prototype
+ * @type {Number}
+ * @default 1.0
+ */
+ startScale : {
+ get : function() {
+ return this._startScale;
+ },
+ set : function(value) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.number.greaterThanOrEquals('value', value, 0.0);
+ //>>includeEnd('debug');
+ this._startScale = value;
+ }
+ },
+ /**
+ * The scale of the particle when it dies.
+ * @memberof ParticleSystem.prototype
+ * @type {Number}
+ * @default 1.0
+ */
+ endScale : {
+ get : function() {
+ return this._endScale;
+ },
+ set : function(value) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.number.greaterThanOrEquals('value', value, 0.0);
+ //>>includeEnd('debug');
+ this._endScale = value;
+ }
+ },
+ /**
+ * The number of particles to emit per second.
+ * @memberof ParticleSystem.prototype
+ * @type {Number}
+ * @default 5
+ */
+ rate : {
+ get : function() {
+ return this._rate;
+ },
+ set : function(value) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.number.greaterThanOrEquals('value', value, 0.0);
+ //>>includeEnd('debug');
+ this._rate = value;
+ this._updateParticlePool = true;
+ }
+ },
+ /**
+ * Sets the minimum speed in meters per second.
+ * @memberof ParticleSystem.prototype
+ * @type {Number}
+ * @default 1.0
+ */
+ minimumSpeed : {
+ get : function() {
+ return this._minimumSpeed;
+ },
+ set : function(value) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.number.greaterThanOrEquals('value', value, 0.0);
+ //>>includeEnd('debug');
+ this._minimumSpeed = value;
+ }
+ },
+ /**
+ * Sets the maximum speed in meters per second.
+ * @memberof ParticleSystem.prototype
+ * @type {Number}
+ * @default 1.0
+ */
+ maximumSpeed : {
+ get : function() {
+ return this._maximumSpeed;
+ },
+ set : function(value) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.number.greaterThanOrEquals('value', value, 0.0);
+ //>>includeEnd('debug');
+ this._maximumSpeed = value;
+ }
+ },
+ /**
+ * Sets the minimum life of particles in seconds.
+ * @memberof ParticleSystem.prototype
+ * @type {Number}
+ * @default 5.0
+ */
+ minimumLife : {
+ get : function() {
+ return this._minimumLife;
+ },
+ set : function(value) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.number.greaterThanOrEquals('value', value, 0.0);
+ //>>includeEnd('debug');
+ this._minimumLife = value;
+ }
+ },
+ /**
+ * Sets the maximum life of particles in seconds.
+ * @memberof ParticleSystem.prototype
+ * @type {Number}
+ * @default 5.0
+ */
+ maximumLife : {
+ get : function() {
+ return this._maximumLife;
+ },
+ set : function(value) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.number.greaterThanOrEquals('value', value, 0.0);
+ //>>includeEnd('debug');
+ this._maximumLife = value;
+ this._updateParticlePool = true;
+ }
+ },
+ /**
+ * Sets the minimum mass of particles in kilograms.
+ * @memberof ParticleSystem.prototype
+ * @type {Number}
+ * @default 1.0
+ */
+ minimumMass : {
+ get : function() {
+ return this._minimumMass;
+ },
+ set : function(value) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.number.greaterThanOrEquals('value', value, 0.0);
+ //>>includeEnd('debug');
+ this._minimumMass = value;
+ }
+ },
+ /**
+ * Sets the maximum mass of particles in kilograms.
+ * @memberof ParticleSystem.prototype
+ * @type {Number}
+ * @default 1.0
+ */
+ maximumMass : {
+ get : function() {
+ return this._maximumMass;
+ },
+ set : function(value) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.number.greaterThanOrEquals('value', value, 0.0);
+ //>>includeEnd('debug');
+ this._maximumMass = value;
+ }
+ },
+ /**
+ * Sets the minimum width of particles in pixels.
+ * @memberof ParticleSystem.prototype
+ * @type {Number}
+ * @default 1.0
+ */
+ minimumWidth : {
+ get : function() {
+ return this._minimumWidth;
+ },
+ set : function(value) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.number.greaterThanOrEquals('value', value, 0.0);
+ //>>includeEnd('debug');
+ this._minimumWidth = value;
+ }
+ },
+ /**
+ * Sets the maximum width of particles in pixels.
+ * @memberof ParticleSystem.prototype
+ * @type {Number}
+ * @default 1.0
+ */
+ maximumWidth : {
+ get : function() {
+ return this._maximumWidth;
+ },
+ set : function(value) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.number.greaterThanOrEquals('value', value, 0.0);
+ //>>includeEnd('debug');
+ this._maximumWidth = value;
+ }
+ },
+ /**
+ * Sets the minimum height of particles in pixels.
+ * @memberof ParticleSystem.prototype
+ * @type {Number}
+ * @default 1.0
+ */
+ minimumHeight : {
+ get : function() {
+ return this._minimumHeight;
+ },
+ set : function(value) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.number.greaterThanOrEquals('value', value, 0.0);
+ //>>includeEnd('debug');
+ this._minimumHeight = value;
+ }
+ },
+ /**
+ * Sets the maximum height of particles in pixels.
+ * @memberof ParticleSystem.prototype
+ * @type {Number}
+ * @default 1.0
+ */
+ maximumHeight : {
+ get : function() {
+ return this._maximumHeight;
+ },
+ set : function(value) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.number.greaterThanOrEquals('value', value, 0.0);
+ //>>includeEnd('debug');
+ this._maximumHeight = value;
+ }
+ },
+ /**
+ * How long the particle system will emit particles, in seconds.
+ * @memberof ParticleSystem.prototype
+ * @type {Number}
+ * @default Number.MAX_VALUE
+ */
+ lifeTime : {
+ get : function() {
+ return this._lifeTime;
+ },
+ set : function(value) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.number.greaterThanOrEquals('value', value, 0.0);
+ //>>includeEnd('debug');
+ this._lifeTime = value;
+ }
+ },
+ /**
+ * Fires an event when the particle system has reached the end of its lifetime.
+ * @memberof ParticleSystem.prototype
+ * @type {Event}
+ */
+ complete : {
+ get : function() {
+ return this._complete;
+ }
+ },
+ /**
+ * When true
, the particle system has reached the end of its lifetime; false
otherwise.
+ * @memberof ParticleSystem.prototype
+ * @type {Boolean}
+ */
+ isComplete : {
+ get : function() {
+ return this._isComplete;
+ }
+ }
+ });
+
+ function updateParticlePool(system) {
+ var rate = system._rate;
+ var life = system._maximumLife;
+
+ var burstAmount = 0;
+ var bursts = system._bursts;
+ if (defined(bursts)) {
+ var length = bursts.length;
+ for (var i = 0; i < length; ++i) {
+ burstAmount += bursts[i].maximum;
+ }
+ }
+
+ var billboardCollection = system._billboardCollection;
+ var image = system.image;
+
+ var particleEstimate = Math.ceil(rate * life + burstAmount);
+ var particles = system._particles;
+ var particlePool = system._particlePool;
+ var numToAdd = Math.max(particleEstimate - particles.length - particlePool.length, 0);
+
+ for (var j = 0; j < numToAdd; ++j) {
+ var particle = new Particle();
+ particle._billboard = billboardCollection.add({
+ image : image
+ });
+ particlePool.push(particle);
+ }
+
+ system._particleEstimate = particleEstimate;
+ }
+
+ function getOrCreateParticle(system) {
+ // Try to reuse an existing particle from the pool.
+ var particle = system._particlePool.pop();
+ if (!defined(particle)) {
+ // Create a new one
+ particle = new Particle();
+ }
+ return particle;
+ }
+
+ function addParticleToPool(system, particle) {
+ system._particlePool.push(particle);
+ }
+
+ function freeParticlePool(system) {
+ var particles = system._particles;
+ var particlePool = system._particlePool;
+ var billboardCollection = system._billboardCollection;
+
+ var numParticles = particles.length;
+ var numInPool = particlePool.length;
+ var estimate = system._particleEstimate;
+
+ var start = numInPool - Math.max(estimate - numParticles - numInPool, 0);
+ for (var i = start; i < numInPool; ++i) {
+ var p = particlePool[i];
+ billboardCollection.remove(p._billboard);
+ }
+ particlePool.length = start;
+ }
+
+ function removeBillboard(particle) {
+ if (defined(particle._billboard)) {
+ particle._billboard.show = false;
+ }
+ }
+
+ function updateBillboard(system, particle) {
+ var billboard = particle._billboard;
+ if (!defined(billboard)) {
+ billboard = particle._billboard = system._billboardCollection.add({
+ image : particle.image
+ });
+ }
+ billboard.width = particle.size.x;
+ billboard.height = particle.size.y;
+ billboard.position = particle.position;
+ billboard.show = true;
+
+ // Update the color
+ var r = CesiumMath.lerp(particle.startColor.red, particle.endColor.red, particle.normalizedAge);
+ var g = CesiumMath.lerp(particle.startColor.green, particle.endColor.green, particle.normalizedAge);
+ var b = CesiumMath.lerp(particle.startColor.blue, particle.endColor.blue, particle.normalizedAge);
+ var a = CesiumMath.lerp(particle.startColor.alpha, particle.endColor.alpha, particle.normalizedAge);
+ billboard.color = new Color(r,g,b,a);
+
+ // Update the scale
+ var scale = CesiumMath.lerp(particle.startScale, particle.endScale, particle.normalizedAge);
+ billboard.scale = scale;
+ }
+
+ function addParticle(system, particle) {
+ particle.startColor = Color.clone(system._startColor, particle.startColor);
+ particle.endColor = Color.clone(system._endColor, particle.endColor);
+ particle.startScale = system._startScale;
+ particle.endScale = system._endScale;
+ particle.image = system.image;
+ particle.life = CesiumMath.randomBetween(system._minimumLife, system._maximumLife);
+ particle.mass = CesiumMath.randomBetween(system._minimumMass, system._maximumMass);
+
+ var width = CesiumMath.randomBetween(system._minimumWidth, system._maximumWidth);
+ var height = CesiumMath.randomBetween(system._minimumHeight, system._maximumHeight);
+ particle.size = Cartesian2.fromElements(width, height, particle.size);
+
+ // Reset the normalizedAge and age in case the particle was reused.
+ particle._normalizedAge = 0.0;
+ particle._age = 0.0;
+
+ var speed = CesiumMath.randomBetween(system._minimumSpeed, system._maximumSpeed);
+ Cartesian3.multiplyByScalar(particle.velocity, speed, particle.velocity);
+
+ system._particles.push(particle);
+ }
+
+ function calculateNumberToEmit(system, dt) {
+ // This emitter is finished if it exceeds it's lifetime.
+ if (system._isComplete) {
+ return 0;
+ }
+
+ dt = CesiumMath.mod(dt, system._lifeTime);
+
+ // Compute the number of particles to emit based on the rate.
+ var v = dt * system._rate;
+ var numToEmit = Math.floor(v);
+ system._carryOver += (v - numToEmit);
+ if (system._carryOver > 1.0)
+ {
+ numToEmit++;
+ system._carryOver -= 1.0;
+ }
+
+ // Apply any bursts
+ if (defined(system.bursts)) {
+ var length = system.bursts.length;
+ for (var i = 0; i < length; i++) {
+ var burst = system.bursts[i];
+ var currentTime = system._currentTime;
+ if (defined(burst) && !burst._complete && currentTime > burst.time) {
+ numToEmit += CesiumMath.randomBetween(burst.minimum, burst.maximum);
+ burst._complete = true;
+ }
+ }
+ }
+
+ return numToEmit;
+ }
+
+ var rotatedVelocityScratch = new Cartesian3();
+
+ /**
+ * @private
+ */
+ ParticleSystem.prototype.update = function(frameState) {
+ if (!this.show) {
+ return;
+ }
+
+ if (!defined(this._billboardCollection)) {
+ this._billboardCollection = new BillboardCollection();
+ }
+
+ if (this._updateParticlePool) {
+ updateParticlePool(this);
+ this._updateParticlePool = false;
+ }
+
+ // Compute the frame time
+ var dt = 0.0;
+ if (this._previousTime) {
+ dt = JulianDate.secondsDifference(frameState.time, this._previousTime);
+ }
+
+ if (dt < 0.0) {
+ dt = 0.0;
+ }
+
+ var particles = this._particles;
+ var emitter = this._emitter;
+ var forces = this.forces;
+
+ var i;
+ var particle;
+
+ // update particles and remove dead particles
+ var length = particles.length;
+ for (i = 0; i < length; ++i) {
+ particle = particles[i];
+ if (!particle.update(dt, forces)) {
+ removeBillboard(particle);
+ // Add the particle back to the pool so it can be reused.
+ addParticleToPool(this, particle);
+ particles[i] = particles[length - 1];
+ --i;
+ --length;
+ } else {
+ updateBillboard(this, particle);
+ }
+ }
+ particles.length = length;
+
+ var numToEmit = calculateNumberToEmit(this, dt);
+
+ if (numToEmit > 0 && defined(emitter)) {
+ // Compute the final model matrix by combining the particle systems model matrix and the emitter matrix.
+ if (this._matrixDirty) {
+ this._combinedMatrix = Matrix4.multiply(this.modelMatrix, this.emitterModelMatrix, this._combinedMatrix);
+ this._matrixDirty = false;
+ }
+
+ var combinedMatrix = this._combinedMatrix;
+
+ for (i = 0; i < numToEmit; i++) {
+ // Create a new particle.
+ particle = getOrCreateParticle(this);
+
+ // Let the emitter initialize the particle.
+ this._emitter.emit(particle);
+
+ //For the velocity we need to add it to the original position and then multiply by point.
+ Cartesian3.add(particle.position, particle.velocity, rotatedVelocityScratch);
+ Matrix4.multiplyByPoint(combinedMatrix, rotatedVelocityScratch, rotatedVelocityScratch);
+
+ // Change the position to be in world coordinates
+ particle.position = Matrix4.multiplyByPoint(combinedMatrix, particle.position, particle.position);
+
+ // Orient the velocity in world space as well.
+ Cartesian3.subtract(rotatedVelocityScratch, particle.position, particle.velocity);
+ Cartesian3.normalize(particle.velocity, particle.velocity);
+
+ // Add the particle to the system.
+ addParticle(this, particle);
+ updateBillboard(this, particle);
+ }
+ }
+
+ this._billboardCollection.update(frameState);
+ this._previousTime = JulianDate.clone(frameState.time, this._previousTime);
+ this._currentTime += dt;
+
+ if (this._lifeTime !== Number.MAX_VALUE && this._currentTime > this._lifeTime) {
+ if (this.loop) {
+ this._currentTime = CesiumMath.mod(this._currentTime, this._lifeTime);
+ if (this.bursts) {
+ var burstLength = this.bursts.length;
+ // Reset any bursts
+ for (i = 0; i < burstLength; i++) {
+ this.bursts[i]._complete = false;
+ }
+ }
+ } else {
+ this._isComplete = true;
+ this._complete.raiseEvent(this);
+ }
+ }
+
+ // free particles in the pool and release billboard GPU memory
+ if (frameState.frameNumber % 120 === 0) {
+ freeParticlePool(this);
+ }
+ };
+
+ /**
+ * Returns true if this object was destroyed; otherwise, false.
+ *
+ * If this object was destroyed, it should not be used; calling any function other than
+ * isDestroyed
will result in a {@link DeveloperError} exception.
+ *
+ * @returns {Boolean} true
if this object was destroyed; otherwise, false
.
+ *
+ * @see ParticleSystem#destroy
+ */
+ ParticleSystem.prototype.isDestroyed = function() {
+ return false;
+ };
+
+ /**
+ * Destroys the WebGL resources held by this object. Destroying an object allows for deterministic
+ * release of WebGL resources, instead of relying on the garbage collector to destroy this object.
+ *
+ * Once an object is destroyed, it should not be used; calling any function other than
+ * isDestroyed
will result in a {@link DeveloperError} exception. Therefore,
+ * assign the return value (undefined
) to the object as done in the example.
+ *
+ * @returns {undefined}
+ *
+ * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
+ *
+ * @see ParticleSystem#isDestroyed
+ */
+ ParticleSystem.prototype.destroy = function() {
+ this._billboardCollection = this._billboardCollection && this._billboardCollection.destroy();
+ return destroyObject(this);
+ };
+
+ /**
+ * A function used to apply a force to the particle on each time step.
+ * @callback ParticleSystem~applyForce
+ *
+ * @param {Particle} particle The particle to apply the force to.
+ * @param {Number} dt The time since the last update.
+ *
+ * @example
+ * function applyGravity(particle, dt) {
+ * var position = particle.position;
+ * var gravityVector = Cesium.Cartesian3.normalize(position, new Cesium.Cartesian3());
+ * Cesium.Cartesian3.multiplyByScalar(gravityVector, GRAVITATIONAL_CONSTANT * dt, gravityVector);
+ * particle.velocity = Cesium.Cartesian3.add(particle.velocity, gravityVector, particle.velocity);
+ * }
+ */
+
+ return ParticleSystem;
+});
diff --git a/Source/Scene/SphereEmitter.js b/Source/Scene/SphereEmitter.js
new file mode 100644
index 000000000000..4b97c720b41f
--- /dev/null
+++ b/Source/Scene/SphereEmitter.js
@@ -0,0 +1,73 @@
+/*global define*/
+define([
+ '../Core/defaultValue',
+ '../Core/defineProperties',
+ '../Core/Cartesian3',
+ '../Core/Check',
+ '../Core/Math'
+ ], function(
+ defaultValue,
+ defineProperties,
+ Cartesian3,
+ Check,
+ CesiumMath) {
+ "use strict";
+
+ /**
+ * A ParticleEmitter that emits particles within a sphere.
+ * Particles will be positioned randomly within the sphere and have initial velocities emanating from the center of the sphere.
+ * @constructor
+ *
+ * @param {Number} [radius=1.0] The radius of the sphere in meters.
+ */
+ function SphereEmitter(radius) {
+ radius = defaultValue(radius, 1.0);
+
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.number.greaterThan('radius', radius, 0.0);
+ //>>includeEnd('debug');
+
+ this._radius = defaultValue(radius, 1.0);
+ }
+
+ defineProperties(SphereEmitter.prototype, {
+ /**
+ * The radius of the sphere in meters.
+ * @memberof SphereEmitter.prototype
+ * @type {Number}
+ * @default 1.0
+ */
+ radius : {
+ get : function() {
+ return this._radius;
+ },
+ set : function(value) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.number.greaterThan('value', value, 0.0);
+ //>>includeEnd('debug');
+ this._radius = value;
+ }
+ }
+ });
+
+ /**
+ * Initializes the given {Particle} by setting it's position and velocity.
+ *
+ * @private
+ * @param {Particle} particle The particle to initialize
+ */
+ SphereEmitter.prototype.emit = function(particle) {
+ var theta = CesiumMath.randomBetween(0.0, CesiumMath.TWO_PI);
+ var phi = CesiumMath.randomBetween(0.0, CesiumMath.PI);
+ var rad = CesiumMath.randomBetween(0.0, this._radius);
+
+ var x = rad * Math.cos(theta) * Math.sin(phi);
+ var y = rad * Math.sin(theta) * Math.sin(phi);
+ var z = rad * Math.cos(phi);
+
+ particle.position = Cartesian3.fromElements(x, y, z, particle.position);
+ particle.velocity = Cartesian3.normalize(particle.position, particle.velocity);
+ };
+
+ return SphereEmitter;
+});
diff --git a/Specs/Scene/AppearanceSpec.js b/Specs/Scene/AppearanceSpec.js
index 8bf607b2f9ac..0bb31d70bf70 100644
--- a/Specs/Scene/AppearanceSpec.js
+++ b/Specs/Scene/AppearanceSpec.js
@@ -1,12 +1,12 @@
/*global defineSuite*/
defineSuite([
- 'Core/WebGLConstants',
'Scene/Appearance',
+ 'Core/WebGLConstants',
'Scene/BlendingState',
'Scene/Material'
], function(
- WebGLConstants,
Appearance,
+ WebGLConstants,
BlendingState,
Material) {
'use strict';
@@ -123,5 +123,4 @@ defineSuite([
expect(rs.depthMask).toEqual(false);
expect(rs.blending).toBe(BlendingState.ALPHA_BLEND);
});
-
});
diff --git a/Specs/Scene/BoxEmitterSpec.js b/Specs/Scene/BoxEmitterSpec.js
new file mode 100644
index 000000000000..d2a97c83f7ff
--- /dev/null
+++ b/Specs/Scene/BoxEmitterSpec.js
@@ -0,0 +1,72 @@
+/*global defineSuite*/
+defineSuite([
+ 'Scene/BoxEmitter',
+ 'Core/Cartesian3',
+ 'Scene/Particle'
+ ], function(
+ BoxEmitter,
+ Cartesian3,
+ Particle) {
+ 'use strict';
+
+ var emitter;
+
+ it('default constructor', function() {
+ emitter = new BoxEmitter();
+ expect(emitter.dimensions).toEqual(new Cartesian3(1.0, 1.0, 1.0));
+ });
+
+ it('constructor', function() {
+ var dimensions = new Cartesian3(2.0, 3.0, 4.0);
+ emitter = new BoxEmitter(dimensions);
+ expect(emitter.dimensions).toEqual(dimensions);
+ });
+
+ it('constructor throws with invalid dimensions', function() {
+ expect(function() {
+ emitter = new BoxEmitter(new Cartesian3(-1.0, 1.0, 1.0));
+ }).toThrowDeveloperError();
+ expect(function() {
+ emitter = new BoxEmitter(new Cartesian3(1.0, -1.0, 1.0));
+ }).toThrowDeveloperError();
+ expect(function() {
+ emitter = new BoxEmitter(new Cartesian3(1.0, 1.0, -1.0));
+ }).toThrowDeveloperError();
+ });
+
+ it('dimensions setter', function() {
+ emitter = new BoxEmitter();
+ var dimensions = new Cartesian3(2.0, 3.0, 4.0);
+ emitter.dimensions = dimensions;
+ expect(emitter.dimensions).toEqual(dimensions);
+ });
+
+ it('dimensions setter throws with invalid value', function() {
+ emitter = new BoxEmitter();
+ expect(function() {
+ emitter.dimensions = undefined;
+ }).toThrowDeveloperError();
+ expect(function() {
+ emitter.dimensions = new Cartesian3(-1.0, 1.0, 1.0);
+ }).toThrowDeveloperError();
+ expect(function() {
+ emitter.dimensions = new Cartesian3(1.0, -1.0, 1.0);
+ }).toThrowDeveloperError();
+ expect(function() {
+ emitter.dimensions = new Cartesian3(1.0, -1.0, 1.0);
+ }).toThrowDeveloperError();
+ });
+
+ it('emits', function() {
+ emitter = new BoxEmitter(new Cartesian3(2.0, 3.0, 4.0));
+ var particle = new Particle();
+
+ for (var i = 0; i < 1000; ++i) {
+ emitter.emit(particle);
+ expect(particle.position.x).toBeLessThanOrEqualTo(emitter.dimensions.x);
+ expect(particle.position.y).toBeLessThanOrEqualTo(emitter.dimensions.y);
+ expect(particle.position.z).toBeLessThanOrEqualTo(emitter.dimensions.z);
+ expect(particle.velocity).toEqual(Cartesian3.normalize(particle.position, new Cartesian3()));
+ }
+ });
+});
diff --git a/Specs/Scene/CircleEmitterSpec.js b/Specs/Scene/CircleEmitterSpec.js
new file mode 100644
index 000000000000..ac5d491fbfb5
--- /dev/null
+++ b/Specs/Scene/CircleEmitterSpec.js
@@ -0,0 +1,63 @@
+/*global defineSuite*/
+defineSuite([
+ 'Scene/CircleEmitter',
+ 'Core/Cartesian3',
+ 'Scene/Particle'
+ ], function(
+ CircleEmitter,
+ Cartesian3,
+ Particle) {
+ 'use strict';
+
+ var emitter;
+
+ it('default constructor', function() {
+ emitter = new CircleEmitter();
+ expect(emitter.radius).toEqual(1.0);
+ });
+
+ it('constructor', function() {
+ emitter = new CircleEmitter(5.0);
+ expect(emitter.radius).toEqual(5.0);
+ });
+
+ it('constructor throws with invalid radius', function() {
+ expect(function() {
+ emitter = new CircleEmitter(0.0);
+ }).toThrowDeveloperError();
+ expect(function() {
+ emitter = new CircleEmitter(-1.0);
+ }).toThrowDeveloperError();
+ });
+
+ it('radius setter', function() {
+ emitter = new CircleEmitter();
+ emitter.radius = 5.0;
+ expect(emitter.radius).toEqual(5.0);
+ });
+
+ it('radius setter throws with invalid value', function() {
+ emitter = new CircleEmitter();
+ expect(function() {
+ emitter.radius = undefined;
+ }).toThrowDeveloperError();
+ expect(function() {
+ emitter.radius = 0.0;
+ }).toThrowDeveloperError();
+ expect(function() {
+ emitter.radius = -1.0;
+ }).toThrowDeveloperError();
+ });
+
+ it('emits', function() {
+ emitter = new CircleEmitter(5.0);
+ var particle = new Particle();
+
+ for (var i = 0; i < 1000; ++i) {
+ emitter.emit(particle);
+ expect(Cartesian3.magnitude(particle.position)).toBeLessThanOrEqualTo(emitter.radius);
+ expect(particle.position.z).toEqual(0.0);
+ expect(particle.velocity).toEqual(Cartesian3.UNIT_Z);
+ }
+ });
+});
diff --git a/Specs/Scene/ConeEmitterSpec.js b/Specs/Scene/ConeEmitterSpec.js
new file mode 100644
index 000000000000..7c59802628eb
--- /dev/null
+++ b/Specs/Scene/ConeEmitterSpec.js
@@ -0,0 +1,50 @@
+/*global defineSuite*/
+defineSuite([
+ 'Scene/ConeEmitter',
+ 'Core/Cartesian3',
+ 'Core/Math',
+ 'Scene/Particle'
+ ], function(
+ ConeEmitter,
+ Cartesian3,
+ CesiumMath,
+ Particle) {
+ 'use strict';
+
+ it('default constructor', function() {
+ var emitter = new ConeEmitter();
+ expect(emitter.angle).toEqual(CesiumMath.toRadians(30.0));
+ });
+
+ it('constructor', function() {
+ var emitter = new ConeEmitter(CesiumMath.PI_OVER_SIX);
+ expect(emitter.angle).toEqual(CesiumMath.PI_OVER_SIX);
+ });
+
+ it('angle setter', function() {
+ var emitter = new ConeEmitter();
+ emitter.angle = CesiumMath.PI_OVER_SIX;
+ expect(emitter.angle).toEqual(CesiumMath.PI_OVER_SIX);
+ });
+
+ it('angle setter throws with invalid value', function() {
+ var emitter = new ConeEmitter();
+ expect(function() {
+ emitter.angle = undefined;
+ }).toThrowDeveloperError();
+ });
+
+ it('emits', function() {
+ var emitter = new ConeEmitter(CesiumMath.PI_OVER_SIX);
+ var particle = new Particle();
+
+ for (var i = 0; i < 1000; ++i) {
+ emitter.emit(particle);
+ expect(particle.position).toEqual(Cartesian3.ZERO);
+ expect(Cartesian3.magnitude(particle.velocity)).toEqualEpsilon(1.0, CesiumMath.EPSILON14);
+
+ // acos(dot(unit v, unit z)) <= angle
+ expect(Math.acos(particle.velocity.z)).toBeLessThanOrEqualTo(emitter.angle);
+ }
+ });
+});
diff --git a/Specs/Scene/ParticleSpec.js b/Specs/Scene/ParticleSpec.js
new file mode 100644
index 000000000000..661a83706939
--- /dev/null
+++ b/Specs/Scene/ParticleSpec.js
@@ -0,0 +1,103 @@
+/*global defineSuite*/
+defineSuite([
+ 'Scene/Particle',
+ 'Core/Cartesian2',
+ 'Core/Cartesian3',
+ 'Core/Color'
+ ], function(
+ Particle,
+ Cartesian2,
+ Cartesian3,
+ Color) {
+ 'use strict';
+
+ it('default constructor', function() {
+ var p = new Particle();
+ expect(p.mass).toEqual(1.0);
+ expect(p.position).toEqual(Cartesian3.ZERO);
+ expect(p.velocity).toEqual(Cartesian3.ZERO);
+ expect(p.life).toEqual(Number.MAX_VALUE);
+ expect(p.image).toBeUndefined();
+ expect(p.startColor).toEqual(Color.WHITE);
+ expect(p.endColor).toEqual(Color.WHITE);
+ expect(p.startScale).toEqual(1.0);
+ expect(p.endScale).toEqual(1.0);
+ expect(p.size).toEqual(new Cartesian2(1.0, 1.0));
+ });
+
+ it('constructor', function() {
+ var options = {
+ mass : 10.0,
+ position : new Cartesian3(1.0, 2.0, 3.0),
+ velocity : new Cartesian3(4.0, 5.0, 6.0),
+ life : 15.0,
+ image : 'url/to/image',
+ startColor : Color.MAGENTA,
+ endColor : Color.LIME,
+ startScale : 0.5,
+ endScale : 20.0,
+ size : new Cartesian2(7.0, 8.0)
+ };
+ var p = new Particle(options);
+ expect(p.mass).toEqual(options.mass);
+ expect(p.position).toEqual(options.position);
+ expect(p.velocity).toEqual(options.velocity);
+ expect(p.life).toEqual(options.life);
+ expect(p.image).toEqual(options.image);
+ expect(p.startColor).toEqual(options.startColor);
+ expect(p.endColor).toEqual(options.endColor);
+ expect(p.startScale).toEqual(options.startScale);
+ expect(p.endScale).toEqual(options.endScale);
+ expect(p.size).toEqual(options.size);
+ });
+
+ it('update without forces', function() {
+ var position = new Cartesian3(1.0, 2.0, 3.0);
+ var velocity = Cartesian3.normalize(new Cartesian3(-1.0, 1.0, 1.0), new Cartesian3());
+ var p = new Particle({
+ life : 15.0,
+ position : position,
+ velocity : velocity
+ });
+
+ var dt = 10.0;
+ var expectedPosition = Cartesian3.add(p.position, Cartesian3.multiplyByScalar(p.velocity, dt, new Cartesian3()), new Cartesian3());
+
+ expect(p.update(dt)).toEqual(true);
+ expect(p.position).toEqual(expectedPosition);
+ expect(p.velocity).toEqual(velocity);
+ expect(p.age).toEqual(dt);
+ expect(p.normalizedAge).toEqual(dt / p.life);
+ expect(p.update(dt)).toEqual(false);
+ });
+
+ it('update with forces', function() {
+ var times2 = function(particle, dt) {
+ Cartesian3.add(particle.position, Cartesian3.multiplyByScalar(particle.velocity, dt, new Cartesian3()), particle.position);
+ };
+ var increaseMass = function(particle, dt) {
+ particle.mass++;
+ };
+ var forces = [times2, increaseMass];
+
+ var position = new Cartesian3(1.0, 2.0, 3.0);
+ var velocity = Cartesian3.normalize(new Cartesian3(-1.0, 1.0, 1.0), new Cartesian3());
+ var p = new Particle({
+ life : 15.0,
+ position : position,
+ velocity : velocity
+ });
+
+ var dt = 10.0;
+ var expectedPosition = Cartesian3.add(p.position, Cartesian3.multiplyByScalar(p.velocity, 2.0 * dt, new Cartesian3()), new Cartesian3());
+ var expectedMass = p.mass + 1;
+
+ expect(p.update(dt, forces)).toEqual(true);
+ expect(p.position).toEqual(expectedPosition);
+ expect(p.velocity).toEqual(velocity);
+ expect(p.age).toEqual(dt);
+ expect(p.normalizedAge).toEqual(dt / p.life);
+ expect(p.mass).toEqual(expectedMass);
+ expect(p.update(dt)).toEqual(false);
+ });
+});
diff --git a/Specs/Scene/ParticleSystemSpec.js b/Specs/Scene/ParticleSystemSpec.js
new file mode 100644
index 000000000000..fe57d7f0baa4
--- /dev/null
+++ b/Specs/Scene/ParticleSystemSpec.js
@@ -0,0 +1,362 @@
+/*global defineSuite*/
+defineSuite([
+ 'Scene/ParticleSystem',
+ 'Core/Cartesian3',
+ 'Core/Color',
+ 'Core/loadImage',
+ 'Core/Matrix4',
+ 'Scene/CircleEmitter',
+ 'Scene/ParticleBurst',
+ 'Specs/createScene'
+ ], function(
+ ParticleSystem,
+ Cartesian3,
+ Color,
+ loadImage,
+ Matrix4,
+ CircleEmitter,
+ ParticleBurst,
+ createScene) {
+ 'use strict';
+
+ var scene;
+ var greenImage;
+
+ beforeAll(function() {
+ scene = createScene();
+ return loadImage('./Data/Images/Green2x2.png').then(function(result) {
+ greenImage = result;
+ });
+ });
+
+ afterAll(function() {
+ scene.destroyForSpecs();
+ });
+
+ it('default constructor', function() {
+ var p = new ParticleSystem();
+ expect(p.show).toEqual(true);
+ expect(p.forces).toBeUndefined();
+ expect(p.emitter).toBeDefined();
+ expect(p.modelMatrix).toEqual(Matrix4.IDENTITY);
+ expect(p.emitterModelMatrix).toEqual(Matrix4.IDENTITY);
+ expect(p.startColor).toEqual(Color.WHITE);
+ expect(p.endColor).toEqual(Color.WHITE);
+ expect(p.startScale).toEqual(1.0);
+ expect(p.endScale).toEqual(1.0);
+ expect(p.rate).toEqual(5.0);
+ expect(p.bursts).toBeUndefined();
+ expect(p.loop).toEqual(true);
+ expect(p.minimumSpeed).toEqual(1.0);
+ expect(p.maximumSpeed).toEqual(1.0);
+ expect(p.minimumLife).toEqual(5.0);
+ expect(p.maximumLife).toEqual(5.0);
+ expect(p.minimumMass).toEqual(1.0);
+ expect(p.maximumMass).toEqual(1.0);
+ expect(p.image).toBeUndefined();
+ expect(p.minimumWidth).toEqual(1.0);
+ expect(p.maximumWidth).toEqual(1.0);
+ expect(p.minimumHeight).toEqual(1.0);
+ expect(p.maximumHeight).toEqual(1.0);
+ expect(p.lifeTime).toEqual(Number.MAX_VALUE);
+ expect(p.complete).toBeDefined();
+ expect(p.isComplete).toEqual(false);
+ });
+
+ it('constructor', function() {
+ var options = {
+ show : false,
+ forces : [function(p) { p.mass++; }],
+ emitter : new CircleEmitter(10.0),
+ modelMatrix : new Matrix4(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0),
+ emitterModelMatrix : new Matrix4(10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0),
+ startColor : Color.MAGENTA,
+ endColor : Color.LAVENDAR_BLUSH,
+ startScale : 19.0,
+ endScale : 20.0,
+ rate : 21.0,
+ bursts : [new ParticleBurst()],
+ loop : false,
+ minimumSpeed : 22.0,
+ maximumSpeed : 23.0,
+ minimumLife : 24.0,
+ maximumLife : 25.0,
+ minimumMass : 26.0,
+ maximumMass : 27.0,
+ image : 'url/to/image',
+ minimumWidth : 28.0,
+ maximumWidth : 29.0,
+ minimumHeight : 30.0,
+ maximumHeight : 31.0,
+ lifeTime : 32.0
+ };
+ var p = new ParticleSystem(options);
+ expect(p.show).toEqual(options.show);
+ expect(p.forces).toEqual(options.forces);
+ expect(p.emitter).toEqual(options.emitter);
+ expect(p.modelMatrix).toEqual(options.modelMatrix);
+ expect(p.emitterModelMatrix).toEqual(options.emitterModelMatrix);
+ expect(p.startColor).toEqual(options.startColor);
+ expect(p.endColor).toEqual(options.endColor);
+ expect(p.startScale).toEqual(options.startScale);
+ expect(p.endScale).toEqual(options.endScale);
+ expect(p.rate).toEqual(options.rate);
+ expect(p.bursts).toEqual(options.bursts);
+ expect(p.loop).toEqual(options.loop);
+ expect(p.minimumSpeed).toEqual(options.minimumSpeed);
+ expect(p.maximumSpeed).toEqual(options.maximumSpeed);
+ expect(p.minimumLife).toEqual(options.minimumLife);
+ expect(p.maximumLife).toEqual(options.maximumLife);
+ expect(p.minimumMass).toEqual(options.minimumMass);
+ expect(p.maximumMass).toEqual(options.maximumMass);
+ expect(p.image).toEqual(options.image);
+ expect(p.minimumWidth).toEqual(options.minimumWidth);
+ expect(p.maximumWidth).toEqual(options.maximumWidth);
+ expect(p.minimumHeight).toEqual(options.minimumHeight);
+ expect(p.maximumHeight).toEqual(options.maximumHeight);
+ expect(p.lifeTime).toEqual(options.lifeTime);
+ expect(p.complete).toBeDefined();
+ expect(p.isComplete).toEqual(false);
+ });
+
+ it('getters/setters', function() {
+ var show = false;
+ var forces = [function(p) { p.mass++; }];
+ var emitter = new CircleEmitter(10.0);
+ var modelMatrix = new Matrix4(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0);
+ var emitterModelMatrix = new Matrix4(10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0);
+ var startColor = Color.MAGENTA;
+ var endColor = Color.LAVENDAR_BLUSH;
+ var startScale = 19.0;
+ var endScale = 20.0;
+ var rate = 21.0;
+ var bursts = [new ParticleBurst()];
+ var loop = false;
+ var minimumSpeed = 22.0;
+ var maximumSpeed = 23.0;
+ var minimumLife = 24.0;
+ var maximumLife = 25.0;
+ var minimumMass = 26.0;
+ var maximumMass = 27.0;
+ var image = 'url/to/image';
+ var minimumWidth = 28.0;
+ var maximumWidth = 29.0;
+ var minimumHeight = 30.0;
+ var maximumHeight = 31.0;
+ var lifeTime = 32.0;
+
+ var p = new ParticleSystem();
+ p.show = show;
+ p.forces = forces;
+ p.emitter = emitter;
+ p.modelMatrix = modelMatrix;
+ p.emitterModelMatrix = emitterModelMatrix;
+ p.startColor = startColor;
+ p.endColor = endColor;
+ p.startScale = startScale;
+ p.endScale = endScale;
+ p.rate = rate;
+ p.bursts = bursts;
+ p.loop = loop;
+ p.minimumSpeed = minimumSpeed;
+ p.maximumSpeed = maximumSpeed;
+ p.minimumLife = minimumLife;
+ p.maximumLife = maximumLife;
+ p.minimumMass = minimumMass;
+ p.maximumMass = maximumMass;
+ p.image = image;
+ p.minimumWidth = minimumWidth;
+ p.maximumWidth = maximumWidth;
+ p.minimumHeight = minimumHeight;
+ p.maximumHeight = maximumHeight;
+ p.lifeTime = lifeTime;
+
+ expect(p.show).toEqual(show);
+ expect(p.forces).toEqual(forces);
+ expect(p.emitter).toEqual(emitter);
+ expect(p.modelMatrix).toEqual(modelMatrix);
+ expect(p.emitterModelMatrix).toEqual(emitterModelMatrix);
+ expect(p.startColor).toEqual(startColor);
+ expect(p.endColor).toEqual(endColor);
+ expect(p.startScale).toEqual(startScale);
+ expect(p.endScale).toEqual(endScale);
+ expect(p.rate).toEqual(rate);
+ expect(p.bursts).toEqual(bursts);
+ expect(p.loop).toEqual(loop);
+ expect(p.minimumSpeed).toEqual(minimumSpeed);
+ expect(p.maximumSpeed).toEqual(maximumSpeed);
+ expect(p.minimumLife).toEqual(minimumLife);
+ expect(p.maximumLife).toEqual(maximumLife);
+ expect(p.minimumMass).toEqual(minimumMass);
+ expect(p.maximumMass).toEqual(maximumMass);
+ expect(p.image).toEqual(image);
+ expect(p.minimumWidth).toEqual(minimumWidth);
+ expect(p.maximumWidth).toEqual(maximumWidth);
+ expect(p.minimumHeight).toEqual(minimumHeight);
+ expect(p.maximumHeight).toEqual(maximumHeight);
+ expect(p.lifeTime).toEqual(lifeTime);
+ expect(p.complete).toBeDefined();
+ expect(p.isComplete).toEqual(false);
+ });
+
+ it('throws with invalid emitter', function() {
+ var p = new ParticleSystem();
+ expect(function() {
+ p.emitter = undefined;
+ }).toThrowDeveloperError();
+ });
+
+ it('throws with invalid modelMatrix', function() {
+ var p = new ParticleSystem();
+ expect(function() {
+ p.modelMatrix = undefined;
+ }).toThrowDeveloperError();
+ });
+
+ it('throws with invalid emitterModelMatrix', function() {
+ var p = new ParticleSystem();
+ expect(function() {
+ p.emitterModelMatrix = undefined;
+ }).toThrowDeveloperError();
+ });
+
+ it('throws with invalid startColor', function() {
+ var p = new ParticleSystem();
+ expect(function() {
+ p.startColor = undefined;
+ }).toThrowDeveloperError();
+ });
+
+ it('throws with invalid endColor', function() {
+ var p = new ParticleSystem();
+ expect(function() {
+ p.endColor = undefined;
+ }).toThrowDeveloperError();
+ });
+
+ it('throws with invalid startScale', function() {
+ var p = new ParticleSystem();
+ expect(function() {
+ p.startScale = -1.0;
+ }).toThrowDeveloperError();
+ });
+
+ it('throws with invalid endScale', function() {
+ var p = new ParticleSystem();
+ expect(function() {
+ p.endScale = -1.0;
+ }).toThrowDeveloperError();
+ });
+
+ it('throws with invalid rate', function() {
+ var p = new ParticleSystem();
+ expect(function() {
+ p.rate = -1.0;
+ }).toThrowDeveloperError();
+ });
+
+ it('throws with invalid minimumSpeed', function() {
+ var p = new ParticleSystem();
+ expect(function() {
+ p.minimumSpeed = -1.0;
+ }).toThrowDeveloperError();
+ });
+
+ it('throws with invalid maximumSpeed', function() {
+ var p = new ParticleSystem();
+ expect(function() {
+ p.maximumSpeed = -1.0;
+ }).toThrowDeveloperError();
+ });
+
+ it('throws with invalid minimumLife', function() {
+ var p = new ParticleSystem();
+ expect(function() {
+ p.minimumLife = -1.0;
+ }).toThrowDeveloperError();
+ });
+
+ it('throws with invalid maximumLife', function() {
+ var p = new ParticleSystem();
+ expect(function() {
+ p.maximumLife = -1.0;
+ }).toThrowDeveloperError();
+ });
+
+ it('throws with invalid minimumMass', function() {
+ var p = new ParticleSystem();
+ expect(function() {
+ p.minimumMass = -1.0;
+ }).toThrowDeveloperError();
+ });
+
+ it('throws with invalid maximumMass', function() {
+ var p = new ParticleSystem();
+ expect(function() {
+ p.maximumMass = -1.0;
+ }).toThrowDeveloperError();
+ });
+
+ it('throws with invalid minimumWidth', function() {
+ var p = new ParticleSystem();
+ expect(function() {
+ p.minimumWidth = -1.0;
+ }).toThrowDeveloperError();
+ });
+
+ it('throws with invalid maximumWidth', function() {
+ var p = new ParticleSystem();
+ expect(function() {
+ p.maximumWidth = -1.0;
+ }).toThrowDeveloperError();
+ });
+
+ it('throws with invalid minimumHeight', function() {
+ var p = new ParticleSystem();
+ expect(function() {
+ p.minimumHeight = -1.0;
+ }).toThrowDeveloperError();
+ });
+
+ it('throws with invalid maximumHeight', function() {
+ var p = new ParticleSystem();
+ expect(function() {
+ p.maximumHeight = -1.0;
+ }).toThrowDeveloperError();
+ });
+
+ it('throws with invalid lifeTime', function() {
+ var p = new ParticleSystem();
+ expect(function() {
+ p.lifeTime = -1.0;
+ }).toThrowDeveloperError();
+ });
+
+ it('renders', function() {
+ scene.primitives.add(new ParticleSystem({
+ image : greenImage,
+ emitter : new CircleEmitter(1.0),
+ rate : 10000,
+ width : 100,
+ height : 100
+ }));
+ scene.camera.position = new Cartesian3(0.0, 0.0, 20.0);
+ scene.camera.direction = new Cartesian3(0.0, 0.0, -1.0);
+ scene.camera.up = Cartesian3.clone(Cartesian3.UNIT_Y);
+ scene.camera.right = Cartesian3.clone(Cartesian3.UNIT_X);
+
+ // no particles emitted at time 0
+ scene.renderForSpecs();
+ // billboard collection needs to create texture atlas
+ scene.renderForSpecs();
+ // finally render
+ expect(scene).toRender([0, 255, 0, 255]);
+ });
+
+ it('isDestroyed', function() {
+ var p = new ParticleSystem();
+ expect(p.isDestroyed()).toEqual(false);
+ p.destroy();
+ expect(p.isDestroyed()).toEqual(true);
+ });
+});
diff --git a/Specs/Scene/SphereEmitterSpec.js b/Specs/Scene/SphereEmitterSpec.js
new file mode 100644
index 000000000000..54e1b3d7aac6
--- /dev/null
+++ b/Specs/Scene/SphereEmitterSpec.js
@@ -0,0 +1,62 @@
+/*global defineSuite*/
+defineSuite([
+ 'Scene/SphereEmitter',
+ 'Core/Cartesian3',
+ 'Scene/Particle'
+ ], function(
+ SphereEmitter,
+ Cartesian3,
+ Particle) {
+ 'use strict';
+
+ var emitter;
+
+ it('default constructor', function() {
+ emitter = new SphereEmitter();
+ expect(emitter.radius).toEqual(1.0);
+ });
+
+ it('constructor', function() {
+ emitter = new SphereEmitter(5.0);
+ expect(emitter.radius).toEqual(5.0);
+ });
+
+ it('constructor throws with invalid radius', function() {
+ expect(function() {
+ emitter = new SphereEmitter(0.0);
+ }).toThrowDeveloperError();
+ expect(function() {
+ emitter = new SphereEmitter(-1.0);
+ }).toThrowDeveloperError();
+ });
+
+ it('radius setter', function() {
+ emitter = new SphereEmitter();
+ emitter.radius = 5.0;
+ expect(emitter.radius).toEqual(5.0);
+ });
+
+ it('radius setter throws with invalid value', function() {
+ emitter = new SphereEmitter();
+ expect(function() {
+ emitter.radius = undefined;
+ }).toThrowDeveloperError();
+ expect(function() {
+ emitter.radius = 0.0;
+ }).toThrowDeveloperError();
+ expect(function() {
+ emitter.radius = -1.0;
+ }).toThrowDeveloperError();
+ });
+
+ it('emits', function() {
+ emitter = new SphereEmitter(5.0);
+ var particle = new Particle();
+
+ for (var i = 0; i < 1000; ++i) {
+ emitter.emit(particle);
+ expect(Cartesian3.magnitude(particle.position)).toBeLessThanOrEqualTo(emitter.radius);
+ expect(particle.velocity).toEqual(Cartesian3.normalize(particle.position, new Cartesian3()));
+ }
+ });
+});