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 @@ + + + + + + + + + Cesium Demo + + + + + + +
+

Loading...

+
+ + + + diff --git a/Apps/Sandcastle/gallery/Particle System Fireworks.jpg b/Apps/Sandcastle/gallery/Particle System Fireworks.jpg new file mode 100644 index 000000000000..5383003dc514 Binary files /dev/null and b/Apps/Sandcastle/gallery/Particle System Fireworks.jpg differ diff --git a/Apps/Sandcastle/gallery/Particle System.html b/Apps/Sandcastle/gallery/Particle System.html new file mode 100644 index 000000000000..6ddf0830a981 --- /dev/null +++ b/Apps/Sandcastle/gallery/Particle System.html @@ -0,0 +1,435 @@ + + + + + + + + + Cesium Demo + + + + + + +
+

Loading...

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Rate + + +
Size + + +
Min Life + + +
Max Life + + +
Min Speed + + +
Max Speed + + +
Start Scale + + +
End Scale + + +
Gravity + + +
Translation + X + Y + Z +
Rotation + H + P + R +
+
+ + + + + diff --git a/Apps/Sandcastle/gallery/Particle System.jpg b/Apps/Sandcastle/gallery/Particle System.jpg new file mode 100644 index 000000000000..e1f3e857aa36 Binary files /dev/null and b/Apps/Sandcastle/gallery/Particle System.jpg differ diff --git a/CHANGES.md b/CHANGES.md index cdd0bb546add..74816b676176 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,15 +8,15 @@ Change Log * The `throttleRequest` parameter for `TerrainProvider.requestTileGeometry`, `CesiumTerrainProvider.requestTileGeometry`, `VRTheWorldTerrainProvider.requestTileGeometry`, and `EllipsoidTerrainProvider.requestTileGeometry` is deprecated and will be replaced with an optional `Request` object. The `throttleRequests` parameter will be removed in 1.37. Instead to throttle requests set the request's `throttle` property to `true`. * The ability to provide a Promise for the `options.url` parameter of `loadWithXhr` and for the `url` parameter of `loadArrayBuffer`, `loadBlob`, `loadImageViaBlob`, `loadText`, `loadJson`, `loadXML`, `loadImage`, `loadCRN`, `loadKTX`, and `loadCubeMap` is deprecated. This will be removed in 1.37, instead `url` must be a string. * Added an `options.request` parameter to `loadWithXhr` and a `request` parameter to `loadArrayBuffer`, `loadBlob`, `loadImageViaBlob`, `loadText`, `loadJson`, `loadJsonp`, `loadXML`, `loadImageFromTypedArray`, `loadImage`, `loadCRN`, and `loadKTX`. -* Fixed bug where if polylines were set to follow the surface of an undefined globe, Cesium would crash [#5413] https://github.com/AnalyticalGraphicsInc/cesium/pull/5413 +* Fixed bug where if polylines were set to follow the surface of an undefined globe, Cesium would crash [#5413](https://github.com/AnalyticalGraphicsInc/cesium/pull/5413) * Fixed a bug where picking clusters would return undefined instead of a list of the clustered entities. [#5286](https://github.com/AnalyticalGraphicsInc/cesium/issues/5286) * Fixed a bug where picking would break when the Sun came into view [#5478](https://github.com/AnalyticalGraphicsInc/cesium/issues/5478) * Reduced the amount of Sun bloom post-process effect near the horizon. [#5381](https://github.com/AnalyticalGraphicsInc/cesium/issues/5381) * Updated glTF/glb MIME types. [#5420](https://github.com/AnalyticalGraphicsInc/cesium/issues/5420) -* Fixed a bug where camera zooming worked incorrectly when the display height was greater than the display width [#5421] (https://github.com/AnalyticalGraphicsInc/cesium/pull/5421) +* Fixed a bug where camera zooming worked incorrectly when the display height was greater than the display width [#5421](https://github.com/AnalyticalGraphicsInc/cesium/pull/5421) * Added Sandcastle demo for ArcticDEM data. [#5224](https://github.com/AnalyticalGraphicsInc/cesium/issues/5224) * `CzmlDataSource` and `KmlDataSource` load functions now take an optional `query` object, which will append query parameters to all network requests. [#5419](https://github.com/AnalyticalGraphicsInc/cesium/pull/5419), [#5434](https://github.com/AnalyticalGraphicsInc/cesium/pull/5434) -* Fixed geocoder bug so geocoder can accurately handle NSEW inputs [#5407] (https://github.com/AnalyticalGraphicsInc/cesium/pull/5407) +* Fixed geocoder bug so geocoder can accurately handle NSEW inputs [#5407](https://github.com/AnalyticalGraphicsInc/cesium/pull/5407) * Added support for [3D Tiles](https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/README.md) for streaming massive heterogeneous 3D geospatial datasets. The new Cesium APIs are: * `Cesium3DTileset` * `Cesium3DTileStyle`, `StyleExpression`, `Expression`, and `ConditionsExpression` @@ -26,6 +26,8 @@ Change Log * `Cesium3DTilesInspector`, `Cesium3DTilesInspectorViewModel`, and `viewerCesium3DTilesInspectorMixin` * `Cesium3DTileColorBlendMode` * Added a Sandcastle demo for setting time with the Clock API [#5457](https://github.com/AnalyticalGraphicsInc/cesium/pull/5457); +* Added support for `ParticleSystem`s. [#5212](https://github.com/AnalyticalGraphicsInc/cesium/pull/5212) +* Added `Cesium.Math.randomBetween`. ### 1.34 - 2017-06-01 diff --git a/Source/Core/Math.js b/Source/Core/Math.js index b3bd96701bd4..9e35624abff9 100644 --- a/Source/Core/Math.js +++ b/Source/Core/Math.js @@ -740,7 +740,7 @@ define([ }; /** - * Generates a random number in the range of [0.0, 1.0) + * Generates a random floating point number in the range of [0.0, 1.0) * using a Mersenne twister. * * @returns {Number} A random number in the range of [0.0, 1.0). @@ -752,6 +752,18 @@ define([ return randomNumberGenerator.random(); }; + + /** + * Generates a random number between two numbers. + * + * @param {Number} min The minimum value. + * @param {Number} max The maximum value. + * @returns {Number} A random number between the min and max. + */ + CesiumMath.randomBetween = function(min, max) { + return CesiumMath.nextRandomNumber() * (max - min) + min; + }; + /** * Computes 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())); + } + }); +});