Browse Source

Merge pull request #15397 from yomboprime/storm

Adding LightningStrike, LightningStorm with examples
Mr.doob 6 years ago
parent
commit
78b5041b47

+ 1 - 0
examples/files.js

@@ -58,6 +58,7 @@ var files = {
 		"webgl_kinect",
 		"webgl_layers",
 		"webgl_lensflares",
+		"webgl_lightningstrike",
 		"webgl_lights_hemisphere",
 		"webgl_lights_physical",
 		"webgl_lights_pointlights",

+ 1005 - 0
examples/js/geometries/LightningStrike.js

@@ -0,0 +1,1005 @@
+/**
+ * @author yomboprime https://github.com/yomboprime
+ *
+ * @fileoverview LightningStrike object for creating lightning strikes and voltaic arcs.
+ * 
+ * 
+ * Usage
+ * 
+ * var myRay = new THREE.LightningStrike( paramsObject );
+ * var myRayMesh = new THREE.Mesh( myRay, myMaterial );
+ * scene.add( myRayMesh );
+ * ...
+ * myRay.update( currentTime );
+ * 
+ * The "currentTime" can vary its rate, go forwards, backwards or even jump, but it cannot be negative.
+ * 
+ * You should normally leave the ray position to (0, 0, 0). You should control it by changing the sourceOffset and destOffset parameters.
+ * 
+ * 
+ * LightningStrike parameters
+ * 
+ * The paramsObject can contain any of the following parameters.
+ * 
+ * Legend:
+ * 'LightningStrike' (also called 'ray'): An independent voltaic arc with its ramifications and defined with a set of parameters.
+ * 'Subray': A ramification of the ray. It is not a LightningStrike object.
+ * 'Segment': A linear segment piece of a subray.
+ * 'Leaf segment': A ray segment which cannot be smaller.
+ * 
+ * 
+ * The following parameters can be changed any time and if they vary smoothly, the ray form will also change smoothly:
+ * 
+ * @param {Vector3} sourceOffset The point where the ray starts.
+ * 
+ * @param {Vector3} destOffset The point where the ray ends.
+ * 
+ * @param {double} timeScale The rate at wich the ray form changes in time. Default: 1
+ * 
+ * @param {double} roughness From 0 to 1. The higher the value, the more wrinkled is the ray. Default: 0.9
+ * 
+ * @param {double} straightness From 0 to 1. The higher the value, the more straight will be a subray path. Default: 0.7
+ * 
+ * @param {Vector3} up0 Ray 'up' direction at the ray starting point. Must be normalized. It should be perpendicular to the ray forward direction but it doesn't matter much.
+ * 
+ * @param {Vector3} up1 Like the up0 parameter but at the end of the ray. Must be normalized.
+ * 
+ * @param {double} radius0 Radius of the main ray trunk at the start point. Default: 1
+ * 
+ * @param {double} radius1 Radius of the main ray trunk at the end point. Default: 1
+ * 
+ * @param {double} radius0Factor The radius0 of a subray is this factor times the radius0 of its parent subray. Default: 0.5
+ * 
+ * @param {double} radius1Factor The radius1 of a subray is this factor times the radius1 of its parent subray. Default: 0.2
+ * 
+ * @param {minRadius} Minimum value a subray radius0 or radius1 can get. Default: 0.1
+ * 
+ * 
+ * The following parameters should not be changed after lightning creation. They can be changed but the ray will change its form abruptly:
+ * 
+ * @param {boolean} isEternal If true the ray never extinguishes. Otherwise its life is controlled by the 'birthTime' and 'deathTime' parameters. Default: true if any of those two parameters is undefined.
+ * 
+ * @param {double} birthTime The time at which the ray starts its life and begins propagating. Only if isEternal is false. Default: None.
+ * 
+ * @param {double} deathTime The time at which the ray ends vanishing and its life. Only if isEternal is false. Default: None.
+ * 
+ * @param {double} propagationTimeFactor From 0 to 1. Lifetime factor at which the ray ends propagating and enters the steady phase. For example, 0.1 means it is propagating 1/10 of its lifetime. Default: 0.1
+ * 
+ * @param {double} vanishingTimeFactor From 0 to 1. Lifetime factor at which the ray ends the steady phase and begins vanishing. For example, 0.9 means it is vanishing 1/10 of its lifetime. Default: 0.9
+ * 
+ * @param {double} subrayPeriod Subrays cycle periodically. This is their time period. Default: 4
+ * 
+ * @param {double} subrayDutyCycle From 0 to 1. This is the fraction of time a subray is active. Default: 0.6
+ * 
+ * 
+ * These parameters cannot change after lightning creation:
+ * 
+ * @param {integer} maxIterations: Greater than 0. The number of ray's leaf segments is 2**maxIterations. Default: 9
+ * 
+ * @param {boolean} isStatic Set to true only for rays which won't change over time and are not attached to moving objects (Rare case). It is used to set the vertex buffers non-dynamic. You can omit calling update() for these rays.
+ *  
+ * @param {integer} ramification Greater than 0. Maximum number of child subrays a subray can have. Default: 5
+ *
+ * @param {integer} maxSubrayRecursion Greater than 0. Maximum level of recursion (subray descendant generations). Default: 3 
+ *
+ * @param {double} recursionProbability From 0 to 1. The lower the value, the less chance each new generation of subrays has to generate new subrays. Default: 0.6
+ *
+ * @param {boolean} generateUVs If true, the ray geometry will have uv coordinates generated. u runs along the ray, and v across its perimeter. Default: false.
+ *
+ * @param {Object} randomGenerator Set here your random number generator which will seed the SimplexNoise and other decisions during ray tree creation.
+ * It can be used to generate repeatable rays. For that, set also the noiseSeed parameter, and each ray created with that generator and seed pair will be identical in time.
+ * The randomGenerator parameter should be an object with a random() function similar to Math.random, but seedable.
+ * It must have also a getSeed() method, which returns the current seed, and a setSeed( seed ) method, which accepts as seed a fractional number from 0 to 1, as well as any other number.
+ * The default value is an internal generator for some uses and Math.random for others (It is non-repeatable even if noiseSeed is supplied)
+ * 
+ * @param {double} noiseSeed Seed used to make repeatable rays (see the randomGenerator)
+ * 
+ * @param {function} onDecideSubrayCreation Set this to change the callback which decides subray creation. You can look at the default callback in the code (createDefaultSubrayCreationCallbacks)for more info.
+ * 
+ * @param {function} onSubrayCreation This is another callback, more simple than the previous one. It can be used to adapt the form of subrays or other parameters once a subray has been created and initialized. It is used in the examples to adapt subrays to a sphere or to a plane.
+ * 
+ *
+*/
+
+THREE.LightningStrike = function ( rayParameters ) {
+
+	THREE.BufferGeometry.call( this );
+
+	this.type = 'LightningStrike';
+
+	// Set parameters, and set undefined parameters to default values
+	rayParameters = rayParameters || {};
+	this.init( THREE.LightningStrike.copyParameters( rayParameters, rayParameters ) );
+
+	// Creates and populates the mesh
+	this.createMesh();
+
+};
+
+THREE.LightningStrike.prototype = Object.create( THREE.BufferGeometry.prototype );
+
+THREE.LightningStrike.prototype.constructor = THREE.LightningStrike;
+
+THREE.LightningStrike.prototype.isLightningStrike = true;
+
+// Ray states
+THREE.LightningStrike.RAY_INITIALIZED = 0;
+THREE.LightningStrike.RAY_UNBORN = 1;
+THREE.LightningStrike.RAY_PROPAGATING = 2;
+THREE.LightningStrike.RAY_STEADY = 3;
+THREE.LightningStrike.RAY_VANISHING = 4;
+THREE.LightningStrike.RAY_EXTINGUISHED = 5;
+
+THREE.LightningStrike.COS30DEG = Math.cos( 30 * Math.PI / 180 );
+THREE.LightningStrike.SIN30DEG = Math.sin( 30 * Math.PI / 180 );
+
+THREE.LightningStrike.createRandomGenerator = function () {
+
+	var numSeeds = 2053;
+	var seeds = [];
+
+	for ( var i = 0; i < numSeeds; i++ ) {
+
+		seeds.push( Math.random() );
+
+	}
+
+	var generator = {
+
+		currentSeed: 0,
+
+		random: function () {
+
+			var value = seeds[ generator.currentSeed ];
+
+			generator.currentSeed = ( generator.currentSeed + 1 ) % numSeeds;
+
+			return value;
+
+		},
+
+		getSeed: function () {
+
+			return generator.currentSeed / numSeeds;
+
+		},
+
+		setSeed: function ( seed ) {
+
+			generator.currentSeed = Math.floor( seed * numSeeds ) % numSeeds;
+
+		}
+
+	};
+
+	return generator;
+
+};
+
+THREE.LightningStrike.copyParameters = function ( dest, source) {
+
+	source = source || {};
+	dest = dest || {};
+
+	var vecCopy = function( v ) {
+
+		if ( source === dest ) {
+
+			return v;
+
+		}
+		else {
+
+			return v.clone();
+
+		}
+
+	}
+
+	dest.sourceOffset = source.sourceOffset !== undefined ? vecCopy( source.sourceOffset ) : new THREE.Vector3( 0, 100, 0 ),
+	dest.destOffset = source.destOffset !== undefined ? vecCopy( source.destOffset ) : new THREE.Vector3( 0, 0, 0 ),
+
+	dest.timeScale = source.timeScale !== undefined ? source.timeScale : 1,
+	dest.roughness = source.roughness !== undefined ? source.roughness : 0.9,
+	dest.straightness = source.straightness !== undefined ? source.straightness : 0.7,
+
+	dest.up0 = source.up0 !== undefined ? vecCopy( source.up0 ) : new THREE.Vector3( 0, 0, 1 );
+	dest.up1 = source.up1 !== undefined ? vecCopy( source.up1 ) : new THREE.Vector3( 0, 0, 1 ),
+	dest.radius0 = source.radius0 !== undefined ? source.radius0 : 1,
+	dest.radius1 = source.radius1 !== undefined ? source.radius1 : 1,
+	dest.radius0Factor = source.radius0Factor !== undefined ? source.radius0Factor : 0.5,
+	dest.radius1Factor = source.radius1Factor !== undefined ? source.radius1Factor : 0.2,
+	dest.minRadius = source.minRadius !== undefined ? source.minRadius : 0.2,
+
+	// These parameters should not be changed after lightning creation. They can be changed but the ray will change its form abruptly:
+
+	dest.isEternal = source.isEternal !== undefined ? source.isEternal : ( source.birthTime === undefined || source.deathTime === undefined ),
+	dest.birthTime = source.birthTime,
+	dest.deathTime = source.deathTime,
+	dest.propagationTimeFactor = source.propagationTimeFactor !== undefined ? source.propagationTimeFactor : 0.1,
+	dest.vanishingTimeFactor = source.vanishingTimeFactor !== undefined ? source.vanishingTimeFactor : 0.9,
+	dest.subrayPeriod = source.subrayPeriod !== undefined ? source.subrayPeriod : 4,
+	dest.subrayDutyCycle = source.subrayDutyCycle !== undefined ? source.subrayDutyCycle : 0.6;
+
+	// These parameters cannot change after lightning creation:
+
+	dest.maxIterations =  source.maxIterations !== undefined ? source.maxIterations : 9;
+	dest.isStatic = source.isStatic !== undefined ? source.isStatic : false;
+	dest.ramification = source.ramification !== undefined ? source.ramification : 5;
+	dest.maxSubrayRecursion = source.maxSubrayRecursion !== undefined ? source.maxSubrayRecursion : 3;
+	dest.recursionProbability = source.recursionProbability !== undefined ? source.recursionProbability : 0.6;
+	dest.generateUVs = source.generateUVs !== undefined ? source.generateUVs : false;
+	dest.randomGenerator = source.randomGenerator,
+	dest.noiseSeed = source.noiseSeed,
+	dest.onDecideSubrayCreation = source.onDecideSubrayCreation,
+	dest.onSubrayCreation = source.onSubrayCreation;
+
+	return dest;
+
+};
+
+THREE.LightningStrike.prototype.update = function ( time ) {
+
+	if ( this.isStatic ) {
+		return;
+	}
+	
+	if ( this.rayParameters.isEternal || ( this.rayParameters.birthTime <= time && time <= this.rayParameters.deathTime ) ) {
+
+		this.updateMesh( time );
+
+		if ( time < this.subrays[ 0 ].endPropagationTime ) {
+		
+			this.state = THREE.LightningStrike.RAY_PROPAGATING;
+
+		}	
+		else if ( time > this.subrays[ 0 ].beginVanishingTime ) {
+
+			this.state = THREE.LightningStrike.RAY_VANISHING;
+
+		}
+		else {
+
+			this.state = THREE.LightningStrike.RAY_STEADY;
+
+		}
+
+		this.visible = true;
+
+	}
+	else {
+
+		this.visible = false;
+
+		if ( time < this.rayParameters.birthTime ) {
+
+			this.state = THREE.LightningStrike.RAY_UNBORN;
+
+		}
+		else {
+
+			this.state = THREE.LightningStrike.RAY_EXTINGUISHED;
+
+		}
+
+	}
+
+};
+
+THREE.LightningStrike.prototype.init = function ( rayParameters ) {
+
+	// Init all the state from the parameters
+
+	this.rayParameters = rayParameters;
+
+	// These parameters cannot change after lightning creation:
+
+	this.maxIterations =  rayParameters.maxIterations !== undefined ? Math.floor( rayParameters.maxIterations ) : 9;
+	rayParameters.maxIterations = this.maxIterations;
+	this.isStatic = rayParameters.isStatic !== undefined ? rayParameters.isStatic : false;
+	rayParameters.isStatic = this.isStatic;
+	this.ramification = rayParameters.ramification !== undefined ? Math.floor( rayParameters.ramification ) : 5;
+	rayParameters.ramification = this.ramification;
+	this.maxSubrayRecursion = rayParameters.maxSubrayRecursion !== undefined ? Math.floor( rayParameters.maxSubrayRecursion ) : 3;
+	rayParameters.maxSubrayRecursion = this.maxSubrayRecursion;
+	this.recursionProbability = rayParameters.recursionProbability !== undefined ? rayParameters.recursionProbability : 0.6;
+	rayParameters.recursionProbability = this.recursionProbability;
+	this.generateUVs = rayParameters.generateUVs !== undefined ? rayParameters.generateUVs : false;
+	rayParameters.generateUVs = this.generateUVs;
+
+	// Random generator
+	if ( rayParameters.randomGenerator !== undefined ) {
+
+		this.randomGenerator = rayParameters.randomGenerator;
+		this.seedGenerator = rayParameters.randomGenerator;
+
+		if ( rayParameters.noiseSeed !== undefined ) {
+		
+			this.seedGenerator.setSeed( rayParameters.noiseSeed );
+
+		}
+
+	}
+	else {
+
+		this.randomGenerator = THREE.LightningStrike.createRandomGenerator();
+		this.seedGenerator = Math;
+
+	}
+
+	// Ray creation callbacks
+	if ( rayParameters.onDecideSubrayCreation !== undefined ) {
+
+		this.onDecideSubrayCreation = rayParameters.onDecideSubrayCreation;
+
+	}
+	else {
+
+		this.createDefaultSubrayCreationCallbacks();
+
+		if ( rayParameters.onSubrayCreation !== undefined ) {
+
+			this.onSubrayCreation = rayParameters.onSubrayCreation;
+
+		}
+
+	}
+
+	// Internal state
+
+	this.state = THREE.LightningStrike.RAY_INITIALIZED;
+
+	this.maxSubrays = Math.ceil( 1 + Math.pow( this.ramification, Math.max( 0, this.maxSubrayRecursion - 1 ) ) );
+	rayParameters.maxSubrays = this.maxSubrays;
+	
+	this.maxRaySegments = 2 * ( 1 << this.maxIterations );
+
+	this.subrays = [];
+
+	for ( var i = 0; i < this.maxSubrays; i++ ) {
+
+		this.subrays.push( this.createSubray() );
+
+	}
+
+	this.raySegments = [];
+
+	for ( var i = 0; i < this.maxRaySegments; i++ ) {
+
+		this.raySegments.push( this.createSegment() );
+
+	}
+
+	this.time = 0;
+	this.timeFraction = 0;
+	this.currentSegmentCallback = null;
+	this.currentCreateTriangleVertices = this.generateUVs ? this.createTriangleVerticesWithUVs : this.createTriangleVerticesWithoutUVs;
+	this.numSubrays = 0;
+	this.currentSubray = null;
+	this.currentSegmentIndex = 0;
+	this.isInitialSegment = false;
+	this.subrayProbability = 0;
+
+	this.currentVertex = 0;
+	this.currentIndex = 0;
+	this.currentCoordinate = 0;
+	this.currentUVCoordinate = 0;
+	this.vertices = null;
+	this.uvs = null;
+	this.indices = null;
+	this.positionAttribute = null;
+	this.uvsAttribute = null;
+	
+	this.simplexX = new SimplexNoise( this.seedGenerator );
+	this.simplexY = new SimplexNoise( this.seedGenerator );
+	this.simplexZ = new SimplexNoise( this.seedGenerator );
+
+	// Temp vectors
+	this.forwards = new THREE.Vector3();
+	this.forwardsFill = new THREE.Vector3();
+	this.side = new THREE.Vector3();
+	this.down = new THREE.Vector3();
+	this.middlePos = new THREE.Vector3();
+	this.middleLinPos = new THREE.Vector3();
+	this.newPos = new THREE.Vector3();
+	this.vPos = new THREE.Vector3();
+	this.cross1 = new THREE.Vector3();
+
+};
+
+THREE.LightningStrike.prototype.createMesh = function () {
+
+	var maxDrawableSegmentsPerSubRay = 1 << this.maxIterations;
+	
+	var maxVerts = 3 * ( maxDrawableSegmentsPerSubRay + 1 ) * this.maxSubrays;
+	var maxIndices = 18 * maxDrawableSegmentsPerSubRay * this.maxSubrays;
+
+	this.vertices = new Float32Array( maxVerts * 3 );
+	this.indices = new Uint32Array( maxIndices );
+	if ( this.generateUVs ) {
+		this.uvs = new Float32Array( maxVerts * 2 );
+	}
+
+	// Populate the mesh
+	this.fillMesh( 0 );
+	
+	this.setIndex( new THREE.Uint32BufferAttribute( this.indices, 1 ) );
+
+	this.positionAttribute = new THREE.Float32BufferAttribute( this.vertices, 3 );
+	this.addAttribute( 'position', this.positionAttribute );
+
+	if ( this.generateUVs ) {1
+		this.uvsAttribute = new THREE.Float32BufferAttribute( new Float32Array( this.uvs ), 2 );
+		this.addAttribute( 'uv', this.uvsAttribute );
+	}
+
+	if ( ! this.isStatic ) {
+		this.index.dynamic = true;
+		this.positionAttribute.dynamic = true;
+		if ( this.generateUVs ) {
+			this.uvsAttribute.dynamic = true;
+		}
+	}
+
+	// Store buffers for later modification
+	this.vertices = this.positionAttribute.array;
+	this.indices = this.index.array;
+	if ( this.generateUVs ) {
+		this.uvs = this.uvsAttribute.array;
+	}
+
+};
+	
+THREE.LightningStrike.prototype.updateMesh = function ( time ) {
+
+	this.fillMesh( time );
+
+	this.drawRange.count = this.currentIndex;
+
+	this.index.needsUpdate = true;
+
+	this.positionAttribute.needsUpdate = true;
+
+	if ( this.generateUVs ) {
+		this.uvsAttribute.needsUpdate = true;
+	}
+
+};
+
+THREE.LightningStrike.prototype.fillMesh = function ( time ) {
+
+	var scope = this;
+
+	this.currentVertex = 0;
+	this.currentIndex = 0;
+	this.currentCoordinate = 0;
+	this.currentUVCoordinate = 0;
+
+	this.fractalRay( time, function fillVertices ( segment ) {
+
+		var subray = scope.currentSubray;
+
+		if ( time < subray.birthTime ) {//&& ( ! this.rayParameters.isEternal || scope.currentSubray.recursion > 0 ) ) {
+
+			return;
+
+		}
+		else if ( this.rayParameters.isEternal && scope.currentSubray.recursion == 0 ) {
+
+			// Eternal rays don't propagate nor vanish, but its subrays do
+
+			scope.createPrism( segment );
+
+			scope.onDecideSubrayCreation( segment, scope );
+
+		}
+		else if ( time < subray.endPropagationTime ) {
+
+			if ( scope.timeFraction >= segment.fraction0 * subray.propagationTimeFactor ) {
+
+				// Ray propagation has arrived to this segment
+
+				scope.createPrism( segment );
+
+				scope.onDecideSubrayCreation( segment, scope );
+
+			}
+
+		}
+		else if ( time < subray.beginVanishingTime ) {
+
+			// Ray is steady (nor propagating nor vanishing)
+
+			scope.createPrism( segment );
+
+			scope.onDecideSubrayCreation( segment, scope );
+
+		}
+		else {
+
+			if ( scope.timeFraction <= subray.vanishingTimeFactor + segment.fraction1  * ( 1 - subray.vanishingTimeFactor ) ) {
+
+				// Segment has not yet vanished
+
+				scope.createPrism( segment );
+
+			}
+
+			scope.onDecideSubrayCreation( segment, scope );
+
+		}
+
+	} );
+
+};
+
+THREE.LightningStrike.prototype.addNewSubray = function ( rayParameters ) {
+
+	return this.subrays[ this.numSubrays++ ];
+
+};
+
+THREE.LightningStrike.prototype.initSubray = function ( subray, rayParameters ) {
+
+	subray.pos0.copy( rayParameters.sourceOffset );
+	subray.pos1.copy( rayParameters.destOffset );
+	subray.up0.copy( rayParameters.up0 );
+	subray.up1.copy( rayParameters.up1 );
+	subray.radius0 = rayParameters.radius0;
+	subray.radius1 = rayParameters.radius1;
+	subray.birthTime = rayParameters.birthTime;
+	subray.deathTime = rayParameters.deathTime;
+	subray.timeScale = rayParameters.timeScale;
+	subray.roughness = rayParameters.roughness;
+	subray.straightness = rayParameters.straightness;
+	subray.propagationTimeFactor = rayParameters.propagationTimeFactor;
+	subray.vanishingTimeFactor = rayParameters.vanishingTimeFactor;
+
+	subray.maxIterations = this.maxIterations;
+	subray.seed = rayParameters.noiseSeed !== undefined ? rayParameters.noiseSeed : 0;
+	subray.recursion = 0;
+
+};
+
+THREE.LightningStrike.prototype.fractalRay = function ( time, segmentCallback ) {
+
+	this.time = time;
+	this.currentSegmentCallback = segmentCallback;
+	this.numSubrays = 0;
+
+	// Add the top level subray
+	this.initSubray( this.addNewSubray(), this.rayParameters );
+
+	// Process all subrays that are being generated until consuming all of them
+	for ( var subrayIndex = 0; subrayIndex < this.numSubrays; subrayIndex++ ) {
+
+		var subray = this.subrays[ subrayIndex ];
+		this.currentSubray = subray;
+
+		this.randomGenerator.setSeed( subray.seed );
+
+		subray.endPropagationTime = THREE.Math.lerp( subray.birthTime, subray.deathTime, subray.propagationTimeFactor );
+		subray.beginVanishingTime = THREE.Math.lerp( subray.deathTime, subray.birthTime, 1 - subray.vanishingTimeFactor );
+
+		var random1 = this.randomGenerator.random;
+		subray.linPos0.set( random1(), random1(), random1() ).multiplyScalar( 1000 );
+		subray.linPos1.set( random1(), random1(), random1() ).multiplyScalar( 1000 );
+
+		this.timeFraction = ( time - subray.birthTime ) / ( subray.deathTime - subray.birthTime );
+
+		this.currentSegmentIndex  = 0;
+		this.isInitialSegment = true;
+
+		var segment = this.getNewSegment();
+		segment.iteration = 0;
+		segment.pos0.copy( subray.pos0 );
+		segment.pos1.copy( subray.pos1 );
+		segment.linPos0.copy( subray.linPos0 );
+		segment.linPos1.copy( subray.linPos1 );
+		segment.up0.copy( subray.up0 );
+		segment.up1.copy( subray.up1 );
+		segment.radius0 = subray.radius0;
+		segment.radius1 = subray.radius1;
+		segment.fraction0 = 0;
+		segment.fraction1 = 1;
+		segment.positionVariationFactor = 1 - subray.straightness;
+
+		this.subrayProbability = this.ramification * Math.pow( this.recursionProbability, subray.recursion ) / ( 1 << subray.maxIterations );
+
+		this.fractalRayRecursive( segment );
+
+	}
+
+	this.currentSegmentCallback = null;
+	this.currentSubray = null;
+
+};
+
+THREE.LightningStrike.prototype.fractalRayRecursive = function ( segment ) {
+
+	// Leave recursion condition
+	if ( segment.iteration >= this.currentSubray.maxIterations ) {
+
+		this.currentSegmentCallback( segment );
+
+		return;
+
+	}
+
+	// Interpolation
+	this.forwards.subVectors( segment.pos1, segment.pos0 );
+	var lForwards = this.forwards.length();
+
+	if ( lForwards < 0.000001) {
+		this.forwards.set( 0, 0, 0.01 );
+		lForwards = this.forwards.length();
+	}
+
+	var middleRadius = ( segment.radius0 + segment.radius1 ) * 0.5;
+	var middleFraction = ( segment.fraction0 + segment.fraction1 ) * 0.5;
+
+	var timeDimension = this.time * this.currentSubray.timeScale * Math.pow( 2, segment.iteration );
+
+	this.middlePos.lerpVectors( segment.pos0, segment.pos1, 0.5 );
+	this.middleLinPos.lerpVectors( segment.linPos0, segment.linPos1, 0.5 );
+	var p = this.middleLinPos;
+
+	// Noise	
+	this.newPos.set( this.simplexX.noise4d( p.x, p.y, p.z, timeDimension ),
+						this.simplexY.noise4d( p.x, p.y, p.z, timeDimension ),
+						this.simplexZ.noise4d( p.x, p.y, p.z, timeDimension ) );
+
+	this.newPos.multiplyScalar( segment.positionVariationFactor * lForwards );
+	this.newPos.add( this.middlePos );
+
+	// Recursion
+
+	var newSegment1 = this.getNewSegment();
+	newSegment1.pos0.copy( segment.pos0 );
+	newSegment1.pos1.copy( this.newPos );
+	newSegment1.linPos0.copy( segment.linPos0 );
+	newSegment1.linPos1.copy( this.middleLinPos );
+	newSegment1.up0.copy( segment.up0 );
+	newSegment1.up1.copy( segment.up1 );
+	newSegment1.radius0 = segment.radius0;
+	newSegment1.radius1 = middleRadius;
+	newSegment1.fraction0 = segment.fraction0;
+	newSegment1.fraction1 = middleFraction;
+	newSegment1.positionVariationFactor = segment.positionVariationFactor * this.currentSubray.roughness;
+	newSegment1.iteration = segment.iteration + 1;
+
+	var newSegment2 = this.getNewSegment();
+	newSegment2.pos0.copy( this.newPos );
+	newSegment2.pos1.copy( segment.pos1 );
+	newSegment2.linPos0.copy( this.middleLinPos );
+	newSegment2.linPos1.copy( segment.linPos1 );
+	this.cross1.crossVectors( segment.up0, this.forwards.normalize() );
+	newSegment2.up0.crossVectors( this.forwards, this.cross1 ).normalize();
+	newSegment2.up1.copy( segment.up1 );
+	newSegment2.radius0 = middleRadius;
+	newSegment2.radius1 = segment.radius1;
+	newSegment2.fraction0 = middleFraction;
+	newSegment2.fraction1 = segment.fraction1;
+	newSegment2.positionVariationFactor = segment.positionVariationFactor * this.currentSubray.roughness;
+	newSegment2.iteration = segment.iteration + 1;
+
+	this.fractalRayRecursive( newSegment1 );
+
+	this.fractalRayRecursive( newSegment2 );
+
+};
+
+THREE.LightningStrike.prototype.createPrism = function ( segment ) {
+
+	// Creates one triangular prism and its vertices at the segment
+
+	this.forwardsFill.subVectors( segment.pos1, segment.pos0 ).normalize();
+
+	if ( this.isInitialSegment ) {
+
+		this.currentCreateTriangleVertices( segment.pos0, segment.up0, this.forwardsFill, segment.radius0, 0 );
+		
+		this.isInitialSegment = false;
+
+	}
+
+	this.currentCreateTriangleVertices( segment.pos1, segment.up0, this.forwardsFill, segment.radius1, segment.fraction1 );
+
+	this.createPrismFaces();
+
+};
+
+THREE.LightningStrike.prototype.createTriangleVerticesWithoutUVs = function ( pos, up, forwards, radius ) {
+
+	// Create an equilateral triangle (only vertices)
+	
+	this.side.crossVectors( up, forwards ).multiplyScalar( radius * THREE.LightningStrike.COS30DEG );
+	this.down.copy( up ).multiplyScalar( - radius * THREE.LightningStrike.SIN30DEG );
+
+	var p = this.vPos;
+	var v = this.vertices;
+
+	p.copy( pos ).sub( this.side ).add( this.down );
+
+	v[ this.currentCoordinate++ ] = p.x;
+	v[ this.currentCoordinate++ ] = p.y;
+	v[ this.currentCoordinate++ ] = p.z;
+
+	p.copy( pos ).add( this.side ).add( this.down );
+
+	v[ this.currentCoordinate++ ] = p.x;
+	v[ this.currentCoordinate++ ] = p.y;
+	v[ this.currentCoordinate++ ] = p.z;
+
+	p.copy( up ).multiplyScalar( radius ).add( pos );
+	
+	v[ this.currentCoordinate++ ] = p.x;
+	v[ this.currentCoordinate++ ] = p.y;
+	v[ this.currentCoordinate++ ] = p.z;
+
+	this.currentVertex += 3;
+
+};
+
+THREE.LightningStrike.prototype.createTriangleVerticesWithUVs = function ( pos, up, forwards, radius, u ) {
+
+	// Create an equilateral triangle (only vertices)
+	
+	this.side.crossVectors( up, forwards ).multiplyScalar( radius * THREE.LightningStrike.COS30DEG );
+	this.down.copy( up ).multiplyScalar( - radius * THREE.LightningStrike.SIN30DEG );
+
+	var p = this.vPos;
+	var v = this.vertices;
+	var uv = this.uvs;
+
+	p.copy( pos ).sub( this.side ).add( this.down );
+
+	v[ this.currentCoordinate++ ] = p.x;
+	v[ this.currentCoordinate++ ] = p.y;
+	v[ this.currentCoordinate++ ] = p.z;
+
+	uv[ this.currentUVCoordinate++ ] = u;
+	uv[ this.currentUVCoordinate++ ] = 0;
+
+	p.copy( pos ).add( this.side ).add( this.down );
+
+	v[ this.currentCoordinate++ ] = p.x;
+	v[ this.currentCoordinate++ ] = p.y;
+	v[ this.currentCoordinate++ ] = p.z;
+
+	uv[ this.currentUVCoordinate++ ] = u;
+	uv[ this.currentUVCoordinate++ ] = 0.5;
+
+	p.copy( up ).multiplyScalar( radius ).add( pos );
+	
+	v[ this.currentCoordinate++ ] = p.x;
+	v[ this.currentCoordinate++ ] = p.y;
+	v[ this.currentCoordinate++ ] = p.z;
+
+	uv[ this.currentUVCoordinate++ ] = u;
+	uv[ this.currentUVCoordinate++ ] = 1;
+
+	this.currentVertex += 3;
+
+};
+
+THREE.LightningStrike.prototype.createPrismFaces = function ( vertex, index ) {
+
+	var indices = this.indices;
+	var vertex = this.currentVertex - 6;
+
+	indices[ this.currentIndex++ ] = vertex + 1;
+	indices[ this.currentIndex++ ] = vertex + 2;
+	indices[ this.currentIndex++ ] = vertex + 5;
+	indices[ this.currentIndex++ ] = vertex + 1;
+	indices[ this.currentIndex++ ] = vertex + 5;
+	indices[ this.currentIndex++ ] = vertex + 4;
+	indices[ this.currentIndex++ ] = vertex + 0;
+	indices[ this.currentIndex++ ] = vertex + 1;
+	indices[ this.currentIndex++ ] = vertex + 4;
+	indices[ this.currentIndex++ ] = vertex + 0;
+	indices[ this.currentIndex++ ] = vertex + 4;
+	indices[ this.currentIndex++ ] = vertex + 3;
+	indices[ this.currentIndex++ ] = vertex + 2;
+	indices[ this.currentIndex++ ] = vertex + 0;
+	indices[ this.currentIndex++ ] = vertex + 3;
+	indices[ this.currentIndex++ ] = vertex + 2;
+	indices[ this.currentIndex++ ] = vertex + 3;
+	indices[ this.currentIndex++ ] = vertex + 5;
+
+};
+
+THREE.LightningStrike.prototype.createDefaultSubrayCreationCallbacks = function () {
+
+	var random1 = this.randomGenerator.random;
+
+	this.onDecideSubrayCreation = function ( segment, lightningStrike ) {
+
+		// Decide subrays creation at parent (sub)ray segment
+
+		var subray = lightningStrike.currentSubray;
+
+		var period = lightningStrike.rayParameters.subrayPeriod;
+		var dutyCycle = lightningStrike.rayParameters.subrayDutyCycle;
+		
+		var phase0 = ( lightningStrike.rayParameters.isEternal && subray.recursion == 0 ) ? - random1() * period : THREE.Math.lerp( subray.birthTime, subray.endPropagationTime, segment.fraction0 ) - random1() * period;
+		
+		var phase = lightningStrike.time - phase0;
+		var currentCycle = Math.floor( phase / period );
+
+		var childSubraySeed = random1() * ( currentCycle + 1 );
+
+		var isActive = phase % period <= dutyCycle * period;
+
+		probability = lightningStrike.subrayProbability;
+		var probability = 0;
+		if ( isActive ) {
+			probability = lightningStrike.subrayProbability;
+			// Distribution test: probability *= segment.fraction0 > 0.5 && segment.fraction0 < 0.9 ? 1 / 0.4 : 0;
+		}
+
+		if ( subray.recursion < lightningStrike.maxSubrayRecursion && lightningStrike.numSubrays < lightningStrike.maxSubrays && random1() < probability ) {
+
+			var childSubray = lightningStrike.addNewSubray();
+
+			var parentSeed = lightningStrike.randomGenerator.getSeed();
+			childSubray.seed = childSubraySeed;
+			lightningStrike.randomGenerator.setSeed( childSubraySeed );
+
+			childSubray.recursion = subray.recursion + 1;
+			childSubray.maxIterations = Math.max( 1, subray.maxIterations - 1 );
+
+			childSubray.linPos0.set( random1(), random1(), random1() ).multiplyScalar( 1000 );
+			childSubray.linPos1.set( random1(), random1(), random1() ).multiplyScalar( 1000 );;
+			childSubray.up0.copy( subray.up0 );
+			childSubray.up1.copy( subray.up1 );
+			childSubray.radius0 = segment.radius0 * lightningStrike.rayParameters.radius0Factor;
+			childSubray.radius1 = Math.min( lightningStrike.rayParameters.minRadius, segment.radius1 * lightningStrike.rayParameters.radius1Factor );
+			
+			childSubray.birthTime = phase0 + ( currentCycle ) * period;
+			childSubray.deathTime = childSubray.birthTime + period * dutyCycle;
+
+			if ( ! lightningStrike.rayParameters.isEternal && subray.recursion == 0 ) {
+
+				childSubray.birthTime = Math.max( childSubray.birthTime, subray.birthTime );
+				childSubray.deathTime = Math.min( childSubray.deathTime, subray.deathTime );
+
+			}
+
+			childSubray.timeScale = subray.timeScale * 2;
+			childSubray.roughness = subray.roughness;
+			childSubray.straightness = subray.straightness;
+			childSubray.propagationTimeFactor = subray.propagationTimeFactor;
+			childSubray.vanishingTimeFactor = subray.vanishingTimeFactor;
+
+			lightningStrike.onSubrayCreation( segment, subray, childSubray, lightningStrike );
+
+			lightningStrike.randomGenerator.setSeed( parentSeed );
+
+		}
+
+	};
+
+	var vec1Pos = new THREE.Vector3();
+	var vec2Forward = new THREE.Vector3();
+	var vec3Side = new THREE.Vector3();
+	var vec4Up = new THREE.Vector3();
+	
+	this.onSubrayCreation = function ( segment, parentSubray, childSubray, lightningStrike ) {
+
+		// Decide childSubray origin and destination positions (pos0 and pos1) and possibly other properties of childSubray
+
+		// Just use the default cone position generator
+		lightningStrike.subrayCylinderPosition( segment, parentSubray, childSubray, 0.5, 0.6, 0.2 );
+
+	};
+
+	this.subrayConePosition = function ( segment, parentSubray, childSubray, heightFactor, sideWidthFactor, minSideWidthFactor ) {
+		
+		// Sets childSubray pos0 and pos1 in a cone
+		
+		childSubray.pos0.copy( segment.pos0 );
+
+		vec1Pos.subVectors( parentSubray.pos1, parentSubray.pos0 );
+		vec2Forward.copy( vec1Pos ).normalize();
+		vec1Pos.multiplyScalar( segment.fraction0 + ( 1 - segment.fraction0 ) * ( random1() * heightFactor ) );
+		var length = vec1Pos.length();
+		vec3Side.crossVectors( parentSubray.up0, vec2Forward );
+		var angle = 2 * Math.PI * random1();
+		vec3Side.multiplyScalar( Math.cos ( angle ) );
+		vec4Up.copy( parentSubray.up0 ).multiplyScalar( Math.sin ( angle ) );
+
+		childSubray.pos1.copy( vec3Side ).add( vec4Up ).multiplyScalar( length * sideWidthFactor * ( minSideWidthFactor + random1() * ( 1 - minSideWidthFactor ) ) ).add( vec1Pos ).add( parentSubray.pos0 );
+
+	}
+
+	this.subrayCylinderPosition = function ( segment, parentSubray, childSubray, heightFactor, sideWidthFactor, minSideWidthFactor ) {
+		
+		// Sets childSubray pos0 and pos1 in a cylinder
+
+		childSubray.pos0.copy( segment.pos0 );
+
+		vec1Pos.subVectors( parentSubray.pos1, parentSubray.pos0 );
+		vec2Forward.copy( vec1Pos ).normalize();
+		vec1Pos.multiplyScalar( segment.fraction0 + ( 1 - segment.fraction0 ) * ( ( 2 * random1() - 1 ) * heightFactor ) );
+		var length = vec1Pos.length();
+		vec3Side.crossVectors( parentSubray.up0, vec2Forward );
+		var angle = 2 * Math.PI * random1();
+		vec3Side.multiplyScalar( Math.cos ( angle ) );
+		vec4Up.copy( parentSubray.up0 ).multiplyScalar( Math.sin ( angle ) );
+
+		childSubray.pos1.copy( vec3Side ).add( vec4Up ).multiplyScalar( length * sideWidthFactor * ( minSideWidthFactor + random1() * ( 1 - minSideWidthFactor ) ) ).add( vec1Pos ).add( parentSubray.pos0 );
+
+	}
+
+};
+
+THREE.LightningStrike.prototype.createSubray = function () {
+
+	return {
+
+		seed: 0,
+		maxIterations: 0,
+		recursion: 0,
+		pos0: new THREE.Vector3(),
+		pos1: new THREE.Vector3(),
+		linPos0: new THREE.Vector3(),
+		linPos1: new THREE.Vector3(),
+		up0: new THREE.Vector3(),
+		up1: new THREE.Vector3(),
+		radius0: 0,
+		radius1: 0,
+		birthTime: 0,
+		deathTime: 0,
+		timeScale: 0,
+		roughness: 0,
+		straightness: 0,
+		propagationTimeFactor: 0,
+		vanishingTimeFactor: 0,
+		endPropagationTime: 0,
+		beginVanishingTime: 0
+
+	};
+
+};
+
+THREE.LightningStrike.prototype.createSegment = function () {
+
+	return {
+		iteration: 0,
+		pos0: new THREE.Vector3(),
+		pos1: new THREE.Vector3(),
+		linPos0: new THREE.Vector3(),
+		linPos1: new THREE.Vector3(),
+		up0: new THREE.Vector3(),
+		up1: new THREE.Vector3(),
+		radius0: 0,
+		radius1: 0,
+		fraction0: 0,
+		fraction1: 0,
+		positionVariationFactor: 0
+	}
+
+};
+
+THREE.LightningStrike.prototype.getNewSegment = function () {
+
+	return this.raySegments[ this.currentSegmentIndex++ ];
+
+};
+
+THREE.LightningStrike.prototype.copy = function ( source ) {
+	
+	BufferGeometry.prototype.copy.call( this, source );
+
+	this.init( THREE.LightningStrike.copyParameters( {}, source.rayParameters ) );
+
+	return this;
+
+};
+
+THREE.LightningStrike.prototype.clone = function () {
+
+	return new this.constructor( THREE.LightningStrike.copyParameters( {}, this.rayParameters ) );
+
+};

+ 240 - 0
examples/js/objects/LightningStorm.js

@@ -0,0 +1,240 @@
+/**
+ * @author yomboprime https://github.com/yomboprime
+ *
+ * @fileoverview Lightning strike object generator
+ *
+ * 
+ * Usage
+ * 
+ * var myStorm = new THREE.LightningStorm( paramsObject );
+ * myStorm.position.set( ... );
+ * scene.add( myStorm );
+ * ...
+ * myStorm.update( currentTime );
+ * 
+ * The "currentTime" can only go forwards or be stopped.
+ * 
+ * 
+ * LightningStorm parameters:
+ *
+ * @param {double} size Size of the storm. If no 'onRayPosition' parameter is defined, it means the side of the rectangle the storm covers.
+ *
+ * @param {double} minHeight Minimum height a ray can start at. If no 'onRayPosition' parameter is defined, it means the height above plane y = 0.
+ * 
+ * @param {double} maxHeight Maximum height a ray can start at. If no 'onRayPosition' parameter is defined, it means the height above plane y = 0.
+ * 
+ * @param {double} maxSlope The maximum inclination slope of a ray. If no 'onRayPosition' parameter is defined, it means the slope relative to plane y = 0.
+ * 
+ * @param {integer} maxLightnings Greater than 0. The maximum number of simultaneous rays.
+ * 
+ * @param {double} lightningMinPeriod minimum time between two consecutive rays.
+ * 
+ * @param {double} lightningMaxPeriod maximum time between two consecutive rays.
+ * 
+ * @param {double} lightningMinDuration The minimum time a ray can last.
+ * 
+ * @param {double} lightningMaxDuration The maximum time a ray can last.
+ * 
+ * @param {Object} lightningParameters The parameters for created rays. See THREE.LightningStrike (geometry)
+ * 
+ * @param {Material} lightningMaterial The THREE.Material used for the created rays.
+ * 
+ * @param {function} onRayPosition Optional callback with two Vector3 parameters (source, dest). You can set here the start and end points for each created ray, using the standard size, minHeight, etc parameters and other values in your algorithm.
+ * 
+ * @param {function} onLightningDown This optional callback is called with one parameter (lightningStrike) when a ray ends propagating, so it has hit the ground.
+ * 
+ *
+*/
+
+THREE.LightningStorm = function ( stormParams ) {
+
+	THREE.Object3D.call( this );
+	
+	// Parameters
+
+	stormParams = stormParams || {};
+	this.stormParams = stormParams;
+
+	stormParams.size = stormParams.size !== undefined ? stormParams.size : 1000.0;
+	stormParams.minHeight = stormParams.minHeight !== undefined ? stormParams.minHeight : 80.0;
+	stormParams.maxHeight = stormParams.maxHeight !== undefined ? stormParams.maxHeight : 100.0;
+	stormParams.maxSlope = stormParams.maxSlope !== undefined ? stormParams.maxSlope : 1.1;
+
+	stormParams.maxLightnings = stormParams.maxLightnings !== undefined ? stormParams.maxLightnings : 3;
+
+	stormParams.lightningMinPeriod = stormParams.lightningMinPeriod !== undefined ? stormParams.lightningMinPeriod : 3.0;
+	stormParams.lightningMaxPeriod = stormParams.lightningMaxPeriod !== undefined ? stormParams.lightningMaxPeriod : 7.0;
+
+	stormParams.lightningMinDuration = stormParams.lightningMinDuration !== undefined ? stormParams.lightningMinDuration : 1.0;
+	stormParams.lightningMaxDuration = stormParams.lightningMaxDuration !== undefined ? stormParams.lightningMaxDuration : 2.5;
+
+	this.lightningParameters = THREE.LightningStrike.copyParameters( stormParams.lightningParameters, stormParams.lightningParameters );
+
+	this.lightningParameters.isEternal = false;
+	
+	this.lightningMaterial = stormParams.lightningMaterial !== undefined ? stormParams.lightningMaterial : new THREE.MeshBasicMaterial( { color: 0xB0FFFF } );
+
+	if ( stormParams.onRayPosition !== undefined ) {
+
+		this.onRayPosition = stormParams.onRayPosition;
+
+	}
+	else {
+
+		this.onRayPosition = function( source, dest ) {
+
+			dest.set( ( Math.random() - 0.5 ) * stormParams.size, 0, ( Math.random() - 0.5 ) * stormParams.size );
+			
+			var height = THREE.Math.lerp( stormParams.minHeight, stormParams.maxHeight, Math.random() );;
+
+			source.set( stormParams.maxSlope * ( 2 * Math.random() - 1 ), 1, stormParams.maxSlope * ( 2 * Math.random() - 1 ) ).multiplyScalar( height ).add( dest );
+
+		};
+
+	}
+
+	this.onLightningDown = stormParams.onLightningDown;
+
+	// Internal state
+
+	this.inited = false;
+	this.nextLightningTime = 0;
+	this.lightningsMeshes = [];
+	this.deadLightningsMeshes = [];
+
+	for ( var i = 0; i < this.stormParams.maxLightnings; i++ ) {
+
+		var lightning = new THREE.LightningStrike( THREE.LightningStrike.copyParameters( {}, this.lightningParameters ) );
+		var mesh = new THREE.Mesh( lightning, this.lightningMaterial );
+		this.deadLightningsMeshes.push( mesh );
+
+	}
+
+};
+
+THREE.LightningStorm.prototype = Object.create( THREE.Object3D.prototype );
+
+THREE.LightningStorm.prototype.constructor = THREE.LightningStorm;
+
+THREE.LightningStorm.prototype.isLightningStorm = true;
+
+THREE.LightningStorm.prototype.update = function ( time ) {
+
+	if ( ! this.inited ) {
+
+		this.nextLightningTime = this.getNextLightningTime( time ) * Math.random();
+		this.inited = true;
+
+	}
+
+	if ( time >= this.nextLightningTime ) {
+
+		// Lightning creation
+
+		var lightningMesh = this.deadLightningsMeshes.pop();
+
+		if ( lightningMesh ) {
+
+			var lightningParams1 = THREE.LightningStrike.copyParameters( lightningMesh.geometry.rayParameters, this.lightningParameters );
+
+			lightningParams1.birthTime = time;
+			lightningParams1.deathTime = time + THREE.Math.lerp( this.stormParams.lightningMinDuration, this.stormParams.lightningMaxDuration, Math.random() );
+
+			this.onRayPosition( lightningParams1.sourceOffset, lightningParams1.destOffset );
+
+			lightningParams1.noiseSeed = Math.random();
+
+			this.add( lightningMesh );
+
+			this.lightningsMeshes.push( lightningMesh );
+
+		}
+
+		// Schedule next lightning
+		this.nextLightningTime = this.getNextLightningTime( time );
+
+	}
+
+	var i = 0; il = this.lightningsMeshes.length;
+
+	while ( i < il ){
+
+		var mesh = this.lightningsMeshes[ i ];
+
+		var lightning = mesh.geometry;
+
+		var prevState = lightning.state;
+
+		lightning.update( time );
+
+		if ( prevState === THREE.LightningStrike.RAY_PROPAGATING && lightning.state > prevState ) {
+
+			if ( this.onLightningDown ) {
+
+				this.onLightningDown( lightning );
+
+			}
+
+		}
+
+		if ( lightning.state === THREE.LightningStrike.RAY_EXTINGUISHED ) {
+
+			// Lightning is to be destroyed
+
+			this.lightningsMeshes.splice( this.lightningsMeshes.indexOf( mesh ), 1 ); 
+
+			this.deadLightningsMeshes.push( mesh );
+
+			this.remove( mesh );
+
+			il--;
+
+		}
+		else {
+
+			i++;
+
+		}
+
+	}
+
+};
+
+THREE.LightningStorm.prototype.getNextLightningTime = function ( currentTime ) {
+
+	return currentTime + THREE.Math.lerp( this.stormParams.lightningMinPeriod, this.stormParams.lightningMaxPeriod, Math.random() ) / ( this.stormParams.maxLightnings + 1 );
+
+};
+
+THREE.LightningStorm.prototype.copy = function ( source ) {
+	
+	Object3D.prototype.copy.call( this, source );
+
+	this.stormParams.size = source.stormParams.size;
+	this.stormParams.minHeight = source.stormParams.minHeight;
+	this.stormParams.maxHeight = source.stormParams.maxHeight;
+	this.stormParams.maxSlope = source.stormParams.maxSlope;
+
+	this.stormParams.maxLightnings = source.stormParams.maxLightnings;
+
+	this.stormParams.lightningMinPeriod = source.stormParams.lightningMinPeriod;
+	this.stormParams.lightningMaxPeriod = source.stormParams.lightningMaxPeriod;
+
+	this.stormParams.lightningMinDuration = source.stormParams.lightningMinDuration;
+	this.stormParams.lightningMaxDuration = source.stormParams.lightningMaxDuration;
+
+	this.lightningParameters = THREE.LightningStrike.copyParameters( {}, source.lightningParameters );
+
+	this.lightningMaterial = source.stormParams.lightningMaterial;
+
+	this.onLightningDown = source.onLightningDown;
+
+	return this;
+
+};
+
+THREE.LightningStrike.prototype.clone = function () {
+
+	return new this.constructor( this.stormParams ).copy( this );
+
+};

+ 834 - 0
examples/webgl_lightningstrike.html

@@ -0,0 +1,834 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgl - lightning strike</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<style>
+			body {
+				color: #cccccc;
+				font-family:Monospace;
+				font-size:13px;
+				text-align:center;
+
+				background-color: #050505;
+				margin: 0px;
+				overflow: hidden;
+			}
+
+			#info {
+				position: absolute;
+				top: 0px; width: 100%;
+				padding: 5px;
+			}
+
+			a {
+				color: #0080ff;
+			}
+			
+			.dg.ac {
+				z-index: 1 !important; /* FIX DAT.GUI */
+			}
+
+		</style>
+	</head>
+	<body>
+
+		<div id="container"></div>
+		<div id="info"><a href="http://threejs.org" target="_blank" rel="noopener">three.js</a> webgl - lightning strike</div>
+
+		<script src="../build/three.js"></script>
+
+		<script src="js/WebGL.js"></script>
+		<script src="js/libs/stats.min.js"></script>
+		<script src='js/libs/dat.gui.min.js'></script>
+		<script src="js/controls/OrbitControls.js"></script>
+		<script src="js/geometries/LightningStrike.js"></script>
+		<script src="js/objects/LightningStorm.js"></script>
+		<script src="js/SimplexNoise.js"></script>
+		<script src="js/shaders/CopyShader.js"></script>
+		<script src="js/postprocessing/EffectComposer.js"></script>
+		<script src="js/postprocessing/RenderPass.js"></script>
+		<script src="js/postprocessing/ShaderPass.js"></script>
+		<script src="js/postprocessing/OutlinePass.js"></script>
+
+		<script>
+
+			if ( WEBGL.isWebGLAvailable() === false ) {
+
+				document.body.appendChild( WEBGL.getWebGLErrorMessage() );
+
+			}
+
+			var container, stats;
+
+			var camera, scene, renderer, composer, gui;
+
+			var currentSceneIndex = 0;
+
+			var currentTime = 0;
+
+			var sceneCreators = [
+				createConesScene,
+				createPlasmaBallScene,
+				createStormScene
+			];
+
+			var textureLoader;
+
+			var clock = new THREE.Clock();
+			
+			var raycaster = new THREE.Raycaster();
+			var mouse = new THREE.Vector2();
+
+			init();
+			animate();
+
+			function init() {
+
+				container = document.getElementById( 'container' );
+
+				renderer = new THREE.WebGLRenderer();
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+				renderer.gammaInput = true;
+				renderer.gammaOutput = true;
+
+				container.appendChild( renderer.domElement );
+
+				composer = new THREE.EffectComposer( renderer );
+
+				stats = new Stats();
+				container.appendChild( stats.dom );
+
+				window.addEventListener( 'resize', onWindowResize, false );
+
+				textureLoader = new THREE.TextureLoader();
+
+				createScene();
+
+			}
+
+			function createScene() {
+
+				scene = sceneCreators[ currentSceneIndex ]();
+
+				createGUI();
+
+			}
+
+			function onWindowResize() {
+
+				scene.userData.camera.aspect = window.innerWidth / window.innerHeight;
+				scene.userData.camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				
+				composer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			//
+
+			function createGUI() {
+
+				if ( gui ) {
+					gui.destroy();
+				}
+
+				gui = new dat.GUI( { width: 350 } );
+
+				var sceneFolder = gui.addFolder( "Scene" );
+
+				scene.userData.sceneIndex = currentSceneIndex;
+
+				sceneFolder.add( scene.userData, 'sceneIndex', { "Electric Cones": 0, "Plasma Ball": 1, "Storm": 2 } ).name( 'Scene' ).onChange( function ( value ) {
+
+					currentSceneIndex = value;
+
+					createScene();
+
+				} );
+				
+				scene.userData.timeRate = 1;
+				sceneFolder.add( scene.userData, 'timeRate', scene.userData.canGoBackwardsInTime ? -1 : 0, 1 ).name( 'Time rate' );
+
+				sceneFolder.open();
+
+				var graphicsFolder = gui.addFolder( "Graphics" );
+
+				graphicsFolder.add( scene.userData, "outlineEnabled" ).name( "Glow enabled" );
+
+				scene.userData.lightningColorRGB = [
+					scene.userData.lightningColor.r * 255,
+					scene.userData.lightningColor.g * 255,
+					scene.userData.lightningColor.b * 255
+				];
+				graphicsFolder.addColor( scene.userData, 'lightningColorRGB' ).name( 'Color' ).onChange( function ( value ) {
+					scene.userData.lightningMaterial.color.setRGB( value[ 0 ], value[ 1 ], value[ 2 ] ).multiplyScalar( 1 / 255 );
+				} );
+				scene.userData.outlineColorRGB = [
+					scene.userData.outlineColor.r * 255,
+					scene.userData.outlineColor.g * 255,
+					scene.userData.outlineColor.b * 255
+				];
+				graphicsFolder.addColor( scene.userData, 'outlineColorRGB' ).name( 'Glow color' ).onChange( function ( value ) {
+					scene.userData.outlineColor.setRGB( value[ 0 ], value[ 1 ], value[ 2 ] ).multiplyScalar( 1 / 255 );
+				} );
+
+				graphicsFolder.open();
+
+				var rayFolder = gui.addFolder( "Ray parameters" );
+
+				rayFolder.add( scene.userData.rayParams, 'straightness', 0, 1 ).name( 'Straightness' );
+				rayFolder.add( scene.userData.rayParams, 'roughness', 0, 1 ).name( 'Roughness' );
+				rayFolder.add( scene.userData.rayParams, 'radius0', 0.1, 10 ).name( 'Initial radius' );
+				rayFolder.add( scene.userData.rayParams, 'radius1', 0.1, 10 ).name( 'Final radius' );
+				rayFolder.add( scene.userData.rayParams, 'radius0Factor', 0, 1 ).name( 'Subray initial radius' );
+				rayFolder.add( scene.userData.rayParams, 'radius1Factor', 0, 1 ).name( 'Subray final radius' );
+				rayFolder.add( scene.userData.rayParams, 'timeScale', 0, 5 ).name( 'Ray time scale' );
+				rayFolder.add( scene.userData.rayParams, 'subrayPeriod', 0.1, 10 ).name( 'Subray period (s)' );
+				rayFolder.add( scene.userData.rayParams, 'subrayDutyCycle', 0, 1 ).name( 'Subray duty cycle' );
+
+				if ( scene.userData.recreateRay ) {
+				
+					// Parameters which need to recreate the ray after modification
+
+					var raySlowFolder = gui.addFolder( "Ray parameters (slow)" );
+
+					raySlowFolder.add( scene.userData.rayParams, 'ramification', 0, 15 ).step( 1 ).name( 'Ramification' ).onFinishChange( function () {
+
+						scene.userData.recreateRay();
+
+					} );
+
+					raySlowFolder.add( scene.userData.rayParams, 'maxSubrayRecursion', 0, 5 ).step( 1 ).name( 'Recursion' ).onFinishChange( function () {
+
+						scene.userData.recreateRay();
+
+					} );
+
+					raySlowFolder.add( scene.userData.rayParams, 'recursionProbability', 0, 1 ).name( 'Rec. probability' ).onFinishChange( function () {
+
+						scene.userData.recreateRay();
+
+					} );
+
+					raySlowFolder.open();
+
+				}
+
+				rayFolder.open();
+
+			}
+
+			//
+
+			function animate() {
+
+				requestAnimationFrame( animate );
+
+				render();
+				stats.update();
+
+			}
+
+			function render() {
+
+				currentTime += scene.userData.timeRate * clock.getDelta();
+
+				if ( currentTime < 0 ) {
+
+					currentTime = 0;
+
+				}
+
+				scene.userData.render( currentTime );
+
+			}
+
+			function createOutline( scene, objectsArray, visibleColor ) {
+
+				var outlinePass = new THREE.OutlinePass( new THREE.Vector2( window.innerWidth, window.innerHeight ), scene, scene.userData.camera, objectsArray );
+				outlinePass.edgeStrength = 2.5;
+				outlinePass.edgeGlow = 0.7;
+				outlinePass.edgeThickness = 2.8;
+				outlinePass.visibleEdgeColor = visibleColor;
+				outlinePass.hiddenEdgeColor.set( 0 );
+				outlinePass.renderToScreen = true;
+				composer.addPass( outlinePass );
+
+				scene.userData.outlineEnabled = true;
+				
+				return outlinePass;
+
+			}
+
+			//
+
+			function createConesScene() {
+
+				var scene = new THREE.Scene();
+				scene.background = new THREE.Color( 0x050505 );
+
+				scene.userData.canGoBackwardsInTime = true;
+
+				scene.userData.camera = new THREE.PerspectiveCamera( 27, window.innerWidth / window.innerHeight, 200, 100000 );
+				
+				// Lights
+
+				scene.userData.lightningColor = new THREE.Color( 0xB0FFFF );
+				scene.userData.outlineColor = new THREE.Color( 0x00FFFF );
+
+				var posLight = new THREE.PointLight( 0x00ffff, 1, 5000, 2 );
+				scene.add( posLight );
+
+				// Ground
+
+				var ground = new THREE.Mesh( new THREE.PlaneBufferGeometry( 200000, 200000 ), new THREE.MeshPhongMaterial( { color: 0xC0C0C0, shininess: 0 } ) );
+				ground.rotation.x = - Math.PI * 0.5;
+				scene.add( ground );
+
+				// Cones
+
+				var conesDistance = 1000;
+				var coneHeight = 200;
+				var coneHeightHalf = coneHeight * 0.5;
+
+				posLight.position.set( 0, ( conesDistance + coneHeight ) * 0.5, 0 );
+				posLight.color = scene.userData.outlineColor;
+
+				scene.userData.camera.position.set( 5 * coneHeight, 4 * coneHeight, 18 * coneHeight );
+
+				var coneMesh1 = new THREE.Mesh( new THREE.ConeBufferGeometry( coneHeight, coneHeight, 30, 1, false ), new THREE.MeshPhongMaterial( { color: 0xFFFF00, emissive: 0x1F1F00 } ) );
+				coneMesh1.rotation.x = Math.PI;
+				coneMesh1.position.y = conesDistance + coneHeight;
+				scene.add( coneMesh1 );
+
+				var coneMesh2 = new THREE.Mesh( coneMesh1.geometry.clone(), new THREE.MeshPhongMaterial( { color: 0xFF2020, emissive: 0x1F0202 } ) );
+				coneMesh2.position.y = coneHeightHalf;
+				scene.add( coneMesh2 );
+
+				// Lightning strike
+
+				scene.userData.lightningMaterial = new THREE.MeshBasicMaterial( { color: scene.userData.lightningColor } );
+
+				scene.userData.rayParams = {
+
+					sourceOffset: new THREE.Vector3(),
+					destOffset: new THREE.Vector3(),
+					radius0: 4,
+					radius1: 4,
+					minRadius: 2.5,
+					maxIterations: 7,
+					isEternal: true,
+
+					timeScale: 0.7,
+
+					propagationTimeFactor: 0.05,
+					vanishingTimeFactor: 0.95,
+					subrayPeriod: 3.5,
+					subrayDutyCycle: 0.6,
+					maxSubrayRecursion: 3,
+					ramification: 7,
+					recursionProbability: 0.6,
+
+					roughness: 0.85,
+					straightness: 0.6
+
+				};
+
+				var lightningStrike;
+				var lightningStrikeMesh;
+				var outlineMeshArray = [];
+
+				scene.userData.recreateRay = function () {
+
+					if ( lightningStrikeMesh ) {
+						scene.remove( lightningStrikeMesh );
+					}
+
+					lightningStrike = new THREE.LightningStrike( scene.userData.rayParams );
+					lightningStrikeMesh = new THREE.Mesh( lightningStrike, scene.userData.lightningMaterial );
+
+					outlineMeshArray.length = 0;
+					outlineMeshArray.push( lightningStrikeMesh );
+
+					scene.add( lightningStrikeMesh );
+
+				}
+
+				scene.userData.recreateRay();
+
+				// Compose rendering
+
+				composer.passes = [];
+				composer.addPass( new THREE.RenderPass( scene, scene.userData.camera ) );
+				createOutline( scene, outlineMeshArray, scene.userData.outlineColor );
+
+				// Controls
+
+				var controls = new THREE.OrbitControls( scene.userData.camera, renderer.domElement );
+				controls.target.y = ( conesDistance + coneHeight ) * 0.5
+				controls.enableDamping = true;
+				controls.dampingFactor = 0.25;
+
+				scene.userData.render = function ( time ) {
+
+					// Move cones and Update ray position
+					coneMesh1.position.set( Math.sin( 0.5 * time ) * conesDistance * 0.6,  conesDistance + coneHeight, Math.cos( 0.5 * time ) * conesDistance * 0.6 );
+					coneMesh2.position.set( Math.sin( 0.9 * time ) * conesDistance, coneHeightHalf, 0 );
+					lightningStrike.rayParameters.sourceOffset.copy( coneMesh1.position );
+					lightningStrike.rayParameters.sourceOffset.y -= coneHeightHalf;
+					lightningStrike.rayParameters.destOffset.copy( coneMesh2.position );
+					lightningStrike.rayParameters.destOffset.y += coneHeightHalf;
+
+					lightningStrike.update( time );
+
+					controls.update();
+
+					// Update point light position to the middle of the ray
+					posLight.position.lerpVectors( lightningStrike.rayParameters.sourceOffset, lightningStrike.rayParameters.destOffset, 0.5 );
+
+					if ( scene.userData.outlineEnabled ) {
+
+						composer.render();
+
+					}
+					else {
+
+						renderer.render( scene, scene.userData.camera );
+
+					}
+
+				};
+
+				return scene;
+
+			}
+
+			//
+
+			function createPlasmaBallScene() {
+
+				var scene = new THREE.Scene();
+
+				scene.userData.canGoBackwardsInTime = true;
+
+				scene.userData.camera = new THREE.PerspectiveCamera( 27, window.innerWidth / window.innerHeight, 100, 50000 );
+
+				var ballScene = new THREE.Scene();
+				ballScene.background = new THREE.Color( 0x454545 );
+
+				// Lights
+
+				var ambientLight = new THREE.AmbientLight( 0x444444 );
+				ballScene.add( ambientLight );
+				scene.add( ambientLight );
+
+				var light1 = new THREE.DirectionalLight( 0xffffff, 0.5 );
+				light1.position.set( 1, 1, 1 );
+				ballScene.add( light1 );
+				scene.add( light1 );
+
+				var light2 = new THREE.DirectionalLight( 0xffffff, 1.5 );
+				light2.position.set( -0.5, 1, 0.2 );
+				ballScene.add( light2 );
+				scene.add( light2 );
+
+				// Plasma ball
+
+				scene.userData.lightningColor = new THREE.Color( 0xFFB0FF );
+				scene.userData.outlineColor = new THREE.Color( 0xFF00FF );
+
+				scene.userData.lightningMaterial =  new THREE.MeshBasicMaterial( { color: scene.userData.lightningColor, side: THREE.DoubleSide } );
+
+				var r = "textures/cube/Bridge2/";
+				var urls = [ r + "posx.jpg", r + "negx.jpg",
+							 r + "posy.jpg", r + "negy.jpg",
+							 r + "posz.jpg", r + "negz.jpg" ];
+
+				var textureCube = new THREE.CubeTextureLoader().load( urls );
+				textureCube.format = THREE.RGBFormat;
+				textureCube.mapping = THREE.CubeReflectionMapping;
+
+				var sphereMaterial = new THREE.MeshPhysicalMaterial( {
+					transparent: true,
+					depthWrite: false,
+					opacity: 0.15,
+					color: 0,
+					metalness: 1,
+					roughness: 0,
+					reflectivity: 0,
+					envMap: textureCube
+				} );
+
+				var sphereHeight = 300;
+				var sphereRadius = 200;
+
+				scene.userData.camera.position.set( 5 * sphereRadius, 2 * sphereHeight, 6 * sphereRadius );
+
+				var sphereMesh = new THREE.Mesh( new THREE.SphereBufferGeometry( sphereRadius, 80, 40 ), sphereMaterial );
+				sphereMesh.position.set( 0, sphereHeight, 0 );
+				ballScene.add( sphereMesh );
+
+				var sphere = new THREE.Sphere( sphereMesh.position, sphereRadius );
+
+				var spherePlasma = new THREE.Mesh( new THREE.SphereBufferGeometry( sphereRadius * 0.05, 24, 12 ), scene.userData.lightningMaterial );
+				spherePlasma.position.copy( sphereMesh.position );
+				spherePlasma.scale.y = 0.6;
+				scene.add( spherePlasma );
+
+				var post = new THREE.Mesh(
+					new THREE.CylinderBufferGeometry( sphereRadius * 0.06, sphereRadius * 0.06, sphereHeight, 6, 1, true ),
+					new THREE.MeshLambertMaterial( { color: 0x020202 } )
+				);
+				post.position.y = sphereHeight * 0.5 - sphereRadius * 0.05 * 1.2;
+				scene.add( post );
+
+				var box = new THREE.Mesh( new THREE.BoxBufferGeometry( sphereHeight * 0.5, sphereHeight * 0.1, sphereHeight * 0.5 ), post.material );
+				box.position.y = sphereHeight * 0.05 * 0.5;
+				scene.add( box );
+
+				var rayDirection = new THREE.Vector3();
+				var rayLength = 0;
+				var vec1 = new THREE.Vector3();
+				var vec2 = new THREE.Vector3();
+
+				scene.userData.rayParams = {
+
+					sourceOffset: sphereMesh.position,
+					destOffset: new THREE.Vector3( sphereRadius, 0, 0 ).add( sphereMesh.position ),
+					radius0: 4,
+					radius1: 4,
+					radius0Factor: 0.82,
+					minRadius: 2.5,
+					maxIterations: 6,
+					isEternal: true,
+
+					timeScale: 0.6,
+					propagationTimeFactor: 0.15,
+					vanishingTimeFactor: 0.87,
+					subrayPeriod: 0.8,
+					ramification: 5,
+					recursionProbability: 0.8,
+
+					roughness: 0.85,
+					straightness: 0.7,
+
+					onSubrayCreation: function ( segment, parentSubray, childSubray, lightningStrike ) {
+
+						lightningStrike.subrayConePosition( segment, parentSubray, childSubray, 0.6, 0.9, 0.7 );
+					
+						// Sphere projection
+
+						vec1.subVectors( childSubray.pos1, lightningStrike.rayParameters.sourceOffset );
+						vec2.set( 0, 0, 0 );
+						if ( lightningStrike.randomGenerator.random() < 0.7 ) {
+							vec2.copy( rayDirection ).multiplyScalar( rayLength * 1.0865 );
+						}
+						vec1.add( vec2 ).setLength( rayLength );
+						childSubray.pos1.addVectors( vec1, lightningStrike.rayParameters.sourceOffset );
+
+					}
+
+				};
+
+				var lightningStrike;
+				var lightningStrikeMesh;
+				var outlineMeshArray = [];
+				
+				scene.userData.recreateRay = function () {
+
+					if ( lightningStrikeMesh ) {
+						scene.remove( lightningStrikeMesh );
+					}
+
+					lightningStrike = new THREE.LightningStrike( scene.userData.rayParams );
+					lightningStrikeMesh = new THREE.Mesh( lightningStrike, scene.userData.lightningMaterial );
+
+					outlineMeshArray.length = 0;
+					outlineMeshArray.push( lightningStrikeMesh );
+					outlineMeshArray.push( spherePlasma );
+
+					scene.add( lightningStrikeMesh );
+
+				}
+
+				scene.userData.recreateRay();
+
+				// Compose rendering
+
+				composer.passes = [];
+
+				composer.addPass( new THREE.RenderPass( ballScene, scene.userData.camera ) );
+
+				var rayPass = new THREE.RenderPass( scene, scene.userData.camera );
+				rayPass.clear = false;
+				composer.addPass( rayPass );
+
+				var outlinePass = createOutline( scene, outlineMeshArray, scene.userData.outlineColor );
+
+				scene.userData.render = function ( time ) {
+
+					rayDirection.subVectors( lightningStrike.rayParameters.destOffset, lightningStrike.rayParameters.sourceOffset );
+					rayLength = rayDirection.length();
+					rayDirection.normalize();
+
+					lightningStrike.update( time );
+
+					controls.update();
+
+					if ( scene.userData.outlineEnabled ) {
+
+						outlinePass.enabled = true;
+						rayPass.renderToScreen = false;
+
+					}
+					else {
+
+						outlinePass.enabled = false;
+						rayPass.renderToScreen = true;
+
+					}
+
+					composer.render();
+
+				};
+
+				// Controls
+
+				var controls = new THREE.OrbitControls( scene.userData.camera, renderer.domElement );
+				controls.target.copy( sphereMesh.position );
+				controls.enableDamping = true;
+				controls.dampingFactor = 0.25;
+
+				// Sphere mouse raycasting
+
+				window.addEventListener( 'mousemove', onTouchMove );
+				window.addEventListener( 'touchmove', onTouchMove );
+
+				function onTouchMove( event ) {
+
+					var x, y;
+
+					if ( event.changedTouches ) {
+
+						x = event.changedTouches[ 0 ].pageX;
+						y = event.changedTouches[ 0 ].pageY;
+
+					} else {
+
+						x = event.clientX;
+						y = event.clientY;
+
+					}
+
+					mouse.x = ( x / window.innerWidth ) * 2 - 1;
+					mouse.y = - ( y / window.innerHeight ) * 2 + 1;
+
+					checkIntersection();
+
+				}
+
+				var intersection = new THREE.Vector3();
+
+				function checkIntersection() {
+
+					raycaster.setFromCamera( mouse, scene.userData.camera );
+
+					var result = raycaster.ray.intersectSphere( sphere, intersection );
+
+					if ( result !== null ) {
+
+						lightningStrike.rayParameters.destOffset.copy( intersection );
+
+					}
+
+				}
+
+				return scene;
+
+			}
+			
+			//
+			
+			function createStormScene() {
+
+				var scene = new THREE.Scene();
+				scene.background = new THREE.Color( 0x050505 );
+
+				scene.userData.canGoBackwardsInTime = false;
+
+				scene.userData.camera = new THREE.PerspectiveCamera( 27, window.innerWidth / window.innerHeight, 20, 10000 );
+
+				// Lights
+
+				scene.add( new THREE.AmbientLight( 0x444444 ) );
+
+				var light1 = new THREE.DirectionalLight( 0xffffff, 0.5 );
+				light1.position.set( 1, 1, 1 );
+				scene.add( light1 );
+
+				var posLight = new THREE.PointLight( 0x00ffff );
+				posLight.position.set( 0, 100, 0 );
+				scene.add( posLight );
+
+				// Ground
+
+				var GROUND_SIZE = 1000;
+
+				scene.userData.camera.position.set( 0, 0.2, 1.6 ).multiplyScalar( GROUND_SIZE * 0.5 );
+
+				var ground = new THREE.Mesh( new THREE.PlaneBufferGeometry( GROUND_SIZE, GROUND_SIZE ), new THREE.MeshLambertMaterial( { color: 0x072302 } ) );
+				ground.rotation.x = - Math.PI * 0.5;
+				scene.add( ground );
+
+				// Storm
+
+				scene.userData.lightningColor = new THREE.Color( 0xB0FFFF );
+				scene.userData.outlineColor = new THREE.Color( 0x00FFFF );
+
+				scene.userData.lightningMaterial =  new THREE.MeshBasicMaterial( { color: scene.userData.lightningColor } );
+
+				var rayDirection = new THREE.Vector3( 0, -1, 0 );
+				var vec1 = new THREE.Vector3();
+				var vec2 = new THREE.Vector3();
+
+				scene.userData.rayParams = {
+
+					radius0: 1,
+					radius1: 0.5,
+					minRadius: 0.3,
+					maxIterations: 7,
+
+					timeScale: 0.15,
+					propagationTimeFactor: 0.2,
+					vanishingTimeFactor: 0.9,
+					subrayPeriod: 4,
+					subrayDutyCycle: 0.6,
+
+					maxSubrayRecursion: 3,
+					ramification: 3,
+					recursionProbability: 0.4,
+
+					roughness: 0.85,
+					straightness: 0.65,
+
+					onSubrayCreation: function ( segment, parentSubray, childSubray, lightningStrike ) {
+
+						lightningStrike.subrayConePosition( segment, parentSubray, childSubray, 0.6, 0.6, 0.5 );
+
+						// Plane projection
+						
+						rayLength = lightningStrike.rayParameters.sourceOffset.y;
+						vec1.subVectors( childSubray.pos1, lightningStrike.rayParameters.sourceOffset );
+						var proj = rayDirection.dot( vec1 );
+						vec2.copy( rayDirection ).multiplyScalar( proj );
+						vec1.sub( vec2 );
+						var scale = proj / rayLength > 0.5 ? rayLength / proj : 1;
+						vec2.multiplyScalar( scale );
+						vec1.add( vec2 );
+						childSubray.pos1.addVectors( vec1, lightningStrike.rayParameters.sourceOffset );
+
+					}
+
+				};
+
+				// Black star mark
+				var starVertices = [];
+				var prevPoint = new THREE.Vector3( 0, 0, 1 );
+				var currPoint = new THREE.Vector3();
+				for ( var i = 1; i <= 16; i++ ) {
+
+					currPoint.set( Math.sin( 2 * Math.PI * i / 16 ), 0, Math.cos( 2 * Math.PI * i / 16 ) );
+
+					if ( i % 2 === 1 ) {
+
+						currPoint.multiplyScalar( 0.3 );
+
+					}
+
+					starVertices.push( 0, 0, 0 );
+					starVertices.push( prevPoint.x, prevPoint.y, prevPoint.z );
+					starVertices.push( currPoint.x, currPoint.y, currPoint.z );
+
+					prevPoint.copy( currPoint );
+
+				}
+
+				var starGeometry = new THREE.BufferGeometry();
+				starGeometry.addAttribute( 'position', new THREE.Float32BufferAttribute( starVertices, 3 ) );
+				var starMesh = new THREE.Mesh( starGeometry, new THREE.MeshBasicMaterial( { color: 0x020900 } ) );
+				starMesh.scale.multiplyScalar( 6 );
+				
+				//
+
+				var storm = new THREE.LightningStorm( {
+
+					size: GROUND_SIZE,
+					minHeight: 90,
+					maxHeight: 200,
+					maxSlope: 0.6,
+					maxLightnings: 8,
+
+					lightningParameters: scene.userData.rayParams,
+
+					lightningMaterial: scene.userData.lightningMaterial,
+
+					onLightningDown: function ( lightning ) {
+
+						// Add black star mark at ray strike
+						var star1 = starMesh.clone();
+						star1.position.copy( lightning.rayParameters.destOffset );
+						star1.position.y = 0.05;
+						star1.rotation.y = 2 * Math.PI * Math.random();
+						scene.add( star1 );
+
+					}
+
+				} );
+
+				scene.add( storm );
+
+				// Compose rendering
+
+				composer.passes = [];
+				composer.addPass( new THREE.RenderPass( scene, scene.userData.camera ) );
+				createOutline( scene, storm.lightningsMeshes, scene.userData.outlineColor );
+
+				// Controls
+
+				var controls = new THREE.OrbitControls( scene.userData.camera, renderer.domElement );
+				controls.target.y = GROUND_SIZE * 0.05;
+				controls.enableDamping = true;
+				controls.dampingFactor = 0.25;
+
+				scene.userData.render = function ( time ) {
+
+					storm.update( time );
+
+					controls.update();
+
+					if ( scene.userData.outlineEnabled ) {
+
+						composer.render();
+
+					}
+					else {
+
+						renderer.render( scene, scene.userData.camera );
+
+					}
+
+				};
+
+				return scene;
+
+			}
+
+		</script>
+
+	</body>
+</html>