using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
namespace ParticlePlayground {
[ExecuteInEditMode()]
public class PlaygroundTrails : MonoBehaviour {
///
/// The particle system this Playground Trail will follow.
///
[HideInInspector] public PlaygroundParticlesC playgroundSystem;
///
/// The material of each trail.
///
[HideInInspector] public Material material;
///
/// The lifetime color of all trails.
///
[HideInInspector] public Gradient lifetimeColor = new Gradient();
///
/// The point array alpha determines the alpha level over the trail points. This is a normalized value where 1 on the x-axis means all points, 1 on the y-axis means full alpha.
///
[HideInInspector] public AnimationCurve pointArrayAlpha;
///
/// The mode to color the trails with. If TrailColorMode.Lifetime is selected the coloring will be based on each point's lifetime. If TrailColorMode.PointArray is selected the coloring will be based on the points in the array.
///
[HideInInspector] public TrailColorMode colorMode;
///
/// The uv mode.
///
[HideInInspector] public TrailUvMode uvMode;
///
/// Determines the render mode of the trail. This sets the rotation direction of the trail points.
///
[HideInInspector] public TrailRenderMode renderMode;
///
/// The transform to billboard towards if renderMode is set to TrailRenderMode.Billboard. If none is set this will default to the Main Camera transform.
///
[HideInInspector] public Transform billboardTransform;
///
/// The custom render scale if renderMode is set to TrailRenderMode.CustomRenderScale. This ables you to set the normal direction (with multiplier) of the trails.
///
[HideInInspector] public Vector3 customRenderScale = Vector3.one;
///
/// Determines if the trails should receive shadows. Note that the shader of the material needs to support this.
///
[HideInInspector] public bool receiveShadows;
#if UNITY_4_3 || UNITY_4_5 || UNITY_4_6
///
/// Determines if the trails should cast shadows (Unity 4). Note that the shader of the material needs to support this.
///
[HideInInspector] public bool castShadows = false;
#else
///
/// Determines if the trails should cast shadows. Note that the shader of the material needs to support this.
///
[HideInInspector] public UnityEngine.Rendering.ShadowCastingMode shadowCastingMode;
#endif
///
/// The time vertices is living on the trail (determines length).
///
[HideInInspector] public float time = 3f;
///
/// The width over normalized lifetime.
///
public AnimationCurve timeWidth;
///
/// The scale of start- and end width.
///
public float widthScale = .1f;
///
/// The minimum distance before new vertices can be created.
///
public float minVertexDistance = .1f;
///
/// The maximum distance before forcing new vertices.
///
public float maxVertexDistance = 100f;
///
/// The maximum forward path deviation before forcing new vertices.
///
public float maxPathDeviation = 1f;
///
/// Determines if points should be created upon particle collision.
///
public bool createPointsOnCollision = false;
///
/// The maximum available points able to be created by this Playground Trail. This will determine the generation of built-in arrays needed to remain efficient in memory consumption.
/// The trail is made out of points where vertices are drawn in between, two points is the minimum to be able to draw a trail, this represents 4 vertices and 6 triangles.
///
public int maxPoints = 100;
///
/// Determines if first point should be created immediately on particle birth, otherwise this will be created during the trail calculation routine.
/// This has affect on when the trail starts as the particle may have moved when the first point is created. If your particle source is moving you may want to leave this setting off to not create a first skewed trail point.
///
public bool createFirstPointOnParticleBirth = false;
///
/// Determines if a last point on the trail should be created when its assigned particle dies.
///
public bool createLastPointOnParticleDeath = false;
///
/// Determines if the Playground Trails should run asynchronously on a separate thread. This will go through the selected Thread Pool Method in the Playground Manager (PlaygroundC).
///
public bool multithreading = true;
///
/// The reference to the birth event on the assigned Particle Playground system.
///
[HideInInspector] public PlaygroundEventC birthEvent;
///
/// The reference to the death event on the assigned Particle Playground system.
///
[HideInInspector] public PlaygroundEventC deathEvent;
///
/// The reference to the collision event on the assigned Particle Playground system.
///
[HideInInspector] public PlaygroundEventC collisionEvent;
///
/// The list of trails following each particle.
///
private List _trails = new List();
private Transform _parentTransform;
private GameObject _parentGameObject;
private Material _materialCache;
private float _calculationStartTime;
private int _currentParticleCount;
private float _currentParticleMinLifetime;
private float _currentParticleMaxLifetime;
private bool _localSpace;
private Vector3 _billboardTransformPosition;
private object _locker = new object();
private bool _isDoneThread = true;
private Matrix4x4 _localMatrix;
///
/// The birth queue of trails. This will be added to whenever a particle births. As a Particle Playground system can birth particles and send particle events asynchronously a thread safe queue is needed to create the trails.
///
readonly Queue _birthQueue = new Queue();
/****************************************************************************
Monobehaviours
****************************************************************************/
void OnEnable ()
{
// Cache reference to the Particle Playground system
if (playgroundSystem == null)
playgroundSystem = GetComponent();
// Cache a reference to the Main Camera if billboardTransform isn't assigned
if (billboardTransform == null)
billboardTransform = Camera.main.transform;
// Set the initial material
if (material == null)
{
material = new Material(Shader.Find("Playground/Vertex Color"));
_materialCache = material;
}
// Reset the trails
ResetTrails();
// Add the required birth/death/collision events
AddRequiredParticleEvents();
// Setup default time width keys
if (timeWidth == null)
timeWidth = new AnimationCurve(DefaultWidthKeys());
// Setup default point array alpha keys
if (pointArrayAlpha == null)
pointArrayAlpha = new AnimationCurve(DefaultWidthKeys());
_isDoneThread = true;
}
void OnDisable ()
{
// Destroy all trails
DestroyAllTrails();
// Remove the required events
RemoveRequiredEvents();
}
void OnDestroy ()
{
// Destroy all trails
DestroyAllTrails();
// Remove the required events
RemoveRequiredEvents();
}
void Update ()
{
// Clamp values
maxPoints = Mathf.Clamp (maxPoints, 2, 32767);
// Set asynchronous available values
if (billboardTransform != null)
_billboardTransformPosition = billboardTransform.position;
// Early out if no particles exist yet
if (playgroundSystem == null || !playgroundSystem.IsReady() || playgroundSystem.IsSettingParticleCount() || playgroundSystem.IsSettingLifetime() || playgroundSystem.particleCache == null || playgroundSystem.particleCache.Length == 0)
return;
// Reset trails if a crucial state is changed
if (_currentParticleCount != playgroundSystem.particleCount || _currentParticleMinLifetime != playgroundSystem.lifetimeMin || _currentParticleMaxLifetime != playgroundSystem.lifetime || _localSpace != (playgroundSystem.shurikenParticleSystem.simulationSpace == ParticleSystemSimulationSpace.Local))
ResetTrails();
// Set calculation matrix if this is local space
if (_localSpace)
_localMatrix.SetTRS(playgroundSystem.particleSystemTransform.position, playgroundSystem.particleSystemTransform.rotation, playgroundSystem.particleSystemTransform.lossyScale);
// Check material
if (material != _materialCache)
SetMaterial(material);
// Remove any trails that has ended
if (_isDoneThread)
{
for (int i = 0; i<_trails.Count; i++)
{
if (_trails[i].trailPoints != null && _trails[i].trailPoints.Count > 1 && _trails[i].trailPoints[_trails[i].trailPoints.Count-1] != null && _trails[i].CanRemoveTrail())
{
RemoveTrail(i);
i--;
if (i<0) i = 0;
continue;
}
}
}
// Consume the particle birth queue
while (_birthQueue.Count>0)
AddTrail(_birthQueue.Dequeue());
// Update all trail meshes and their render settings
for (int i = 0; i<_trails.Count; i++)
{
ParticlePlaygroundTrail trail = _trails[i];
// Set shadow casting/receiving
trail.trailRenderer.receiveShadows = receiveShadows;
#if UNITY_4_3 || UNITY_4_5 || UNITY_4_6
trail.trailRenderer.castShadows = castShadows;
#else
trail.trailRenderer.shadowCastingMode = shadowCastingMode;
#endif
if (_isDoneThread)
trail.UpdateMesh();
}
// Finally calculate all trails
if (multithreading)
{
if (_isDoneThread)
{
_calculationStartTime = Application.isPlaying? Time.time : Time.realtimeSinceStartup;
_isDoneThread = false;
PlaygroundC.RunAsync(()=>{
lock (_locker)
{
if (_isDoneThread) return;
CalculateTrail();
_isDoneThread = true;
}
});
}
}
else
{
_calculationStartTime = Application.isPlaying? Time.time : Time.realtimeSinceStartup;
CalculateTrail();
}
}
// Prevent build-up of the birth queue while Editor is out of focus
#if UNITY_EDITOR
public void OnApplicationPause (bool pauseStatus)
{
if (!pauseStatus && !UnityEditor.EditorApplication.isPlaying)
{
_birthQueue.Clear();
}
}
#endif
/****************************************************************************
Event Listeners
****************************************************************************/
///
/// This function will be called whenever a particle is birthed.
///
/// The birthed particle.
void OnParticleBirthEvent (PlaygroundEventParticle particle)
{
_birthQueue.Enqueue (new TrailParticleInfo(particle.particleId, particle.position, particle.velocity));
}
///
/// This function will be called whenever a particle has died.
///
/// The particle which died.
void OnParticleDeathEvent (PlaygroundEventParticle particle)
{
int trailIndex = GetOldestTrailWithParticleId(particle.particleId);
if (trailIndex > -1)
{
if (createLastPointOnParticleDeath)
{
_trails[trailIndex].SetLastPoint(particle.position, particle.velocity, EvaluateWidth(0), time, _calculationStartTime);
}
else
{
_trails[trailIndex].SetParticlePosition(particle.position);
_trails[trailIndex].Die();
}
}
}
///
/// This function will be called whenever a particle is colliding.
///
/// The collided particle.
void OnParticleCollisionEvent (PlaygroundEventParticle particle)
{
if (createPointsOnCollision)
{
int trailIndex = GetNewestTrailWithParticleId (particle.particleId);
if (trailIndex < 0)
return;
ParticlePlaygroundTrail trailAtIndex = _trails[trailIndex];
trailAtIndex.AddPoint (playgroundSystem.particleCache[particle.particleId].position, EvaluateWidth(0), time, _calculationStartTime);
}
}
///
/// Gets the birth event this Playground Trail is listening to.
///
/// The particle birth event.
public PlaygroundEventC GetBirthEvent () {return birthEvent;}
///
/// Gets the death event this Playground Trail is listening to.
///
/// The particle death event.
public PlaygroundEventC GetDeathEvent () {return deathEvent;}
///
/// Gets the collision event this Playground Trail is listening to.
///
/// The particle collision event.
public PlaygroundEventC GetCollisionEvent () {return collisionEvent;}
///
/// Adds the required particle events to track particles.
///
public void AddRequiredParticleEvents ()
{
if (playgroundSystem != null)
{
// Hookup events
birthEvent = GetEventFromType(EVENTTYPEC.Birth);
if (birthEvent == null)
{
birthEvent = PlaygroundC.CreateEvent(playgroundSystem);
birthEvent.broadcastType = EVENTBROADCASTC.EventListeners;
birthEvent.eventType = EVENTTYPEC.Birth;
}
birthEvent.particleEvent += OnParticleBirthEvent;
deathEvent = GetEventFromType(EVENTTYPEC.Death);
if (deathEvent == null)
{
deathEvent = PlaygroundC.CreateEvent(playgroundSystem);
deathEvent.broadcastType = EVENTBROADCASTC.EventListeners;
deathEvent.eventType = EVENTTYPEC.Death;
}
deathEvent.particleEvent += OnParticleDeathEvent;
collisionEvent = GetEventFromType(EVENTTYPEC.Collision);
if (collisionEvent == null)
{
collisionEvent = PlaygroundC.CreateEvent(playgroundSystem);
collisionEvent.broadcastType = EVENTBROADCASTC.EventListeners;
collisionEvent.eventType = EVENTTYPEC.Collision;
}
collisionEvent.particleEvent += OnParticleCollisionEvent;
}
}
///
/// Removes the required events to track particles.
///
public void RemoveRequiredEvents ()
{
if (playgroundSystem != null)
{
if (birthEvent != null)
{
birthEvent.particleEvent -= OnParticleBirthEvent;
birthEvent = null;
}
if (deathEvent != null)
{
deathEvent.particleEvent -= OnParticleDeathEvent;
deathEvent = null;
}
if (collisionEvent != null)
{
collisionEvent.particleEvent -= OnParticleCollisionEvent;
collisionEvent = null;
}
}
}
///
/// Gets the type of event based on the passed in EVETTTYPEC.
///
/// The event of type specified.
/// The event type.
public PlaygroundEventC GetEventFromType (EVENTTYPEC eventType)
{
for (int i = 0; i
/// Returns a default pair of AnimationCurve Keyframes in X 0 and X 1 at value Y 1.
///
/// The default width keys.
public Keyframe[] DefaultWidthKeys () {
Keyframe[] keys = new Keyframe[2];
keys[0].time = 0;
keys[1].time = 1f;
keys[0].value = 1f;
keys[1].value = 1f;
return keys;
}
///
/// Sets the material of all trails.
///
/// The material all trails should get.
public void SetMaterial (Material material) {
for (int i = 0; i<_trails.Count; i++) {
if (_trails[i] != null && _trails[i].trailRenderer != null)
_trails[i].trailRenderer.sharedMaterial = material;
}
_materialCache = material;
}
///
/// Evaluates the width at normalized trail time.
///
/// The width at normalized trail time.
/// Normalized time.
public float EvaluateWidth (float normalizedTime) {
return timeWidth.Evaluate(normalizedTime)*widthScale;
}
public Color32 EvaluateColor (float normalizedTime)
{
return lifetimeColor.Evaluate(normalizedTime);
}
public Color32 EvaluateColor (int trailIndex, int trailPointIndex)
{
return lifetimeColor.Evaluate((trailPointIndex*1f) / (_trails[trailIndex].GetBirthIterator()-1));
}
/****************************************************************************
Trail functions
****************************************************************************/
///
/// Creates a trail and assigns it to a particle.
///
/// Information about the particle.
public void AddTrail (TrailParticleInfo particleInfo)
{
// Check parent object
if (_parentGameObject == null)
{
_parentGameObject = new GameObject("Playground Trails ("+playgroundSystem.name+")", typeof(PlaygroundTrailParent));
_parentTransform = _parentGameObject.transform;
_parentGameObject.GetComponent().trailsReference = this;
}
ParticlePlaygroundTrail newTrail = new ParticlePlaygroundTrail(maxPoints);
newTrail.trailGameObject = new GameObject("Playground Trail "+particleInfo.particleId);
newTrail.trailTransform = newTrail.trailGameObject.transform;
newTrail.trailTransform.parent = _parentTransform;
newTrail.trailRenderer = newTrail.trailGameObject.AddComponent();
newTrail.trailMeshFilter = newTrail.trailGameObject.AddComponent();
newTrail.trailMesh = new Mesh();
newTrail.trailMesh.MarkDynamic();
newTrail.trailMeshFilter.sharedMesh = newTrail.trailMesh;
newTrail.trailRenderer.sharedMaterial = material;
newTrail.particleId = particleInfo.particleId;
if (createFirstPointOnParticleBirth)
newTrail.SetFirstPoint(particleInfo.position, particleInfo.velocity, EvaluateWidth(0), time, _calculationStartTime);
_trails.Add (newTrail);
}
///
/// Gets the oldest trail following the particle id. If the trail is already dead or doesn't contain the particle id -1 will be returned.
///
/// The trail with particle id (-1 if not found).
/// Particle identifier.
public int GetOldestTrailWithParticleId (int particleId)
{
for (int i = 0; i<_trails.Count; i++)
if (_trails[i].particleId == particleId && !_trails[i].IsDead())
return i;
return -1;
}
///
/// Gets the newest trail following the particle id. If the trail is already dead or doesn't contain the particle id -1 will be returned.
///
/// The trail with particle id (-1 if not found).
/// Particle identifier.
public int GetNewestTrailWithParticleId (int particleId)
{
for (int i = _trails.Count-1; i>=0; --i)
if (_trails[i].particleId == particleId && !_trails[i].IsDead())
return i;
return -1;
}
///
/// Gets the cached parent transform of the trails.
///
/// The parent transform.
public Transform GetParentTransform ()
{
return _parentTransform;
}
///
/// Gets the cached parent game object of the trails.
///
/// The parent game object.
public GameObject GetParentGameObject ()
{
return _parentGameObject;
}
///
/// Stopping the trail will make the trail stop following its assigned particle.
///
/// Trail number.
public void StopTrail (int trailNumber)
{
if (trailNumber < 0)
{
return;
}
_trails[trailNumber].Die();
}
///
/// Stops the oldest trail with particle identifier.
///
/// Particle identifier.
public void StopOldestTrailWithParticleId (int particleId)
{
StopTrail (GetOldestTrailWithParticleId (particleId));
}
///
/// Stops the newest trail with particle identifier.
///
/// Particle identifier.
public void StopNewestTrailWithParticleId (int particleId)
{
StopTrail (GetNewestTrailWithParticleId (particleId));
}
///
/// Resets all trails.
///
public void ResetTrails () {
DestroyAllTrails();
if (playgroundSystem != null && gameObject.activeInHierarchy)
{
_currentParticleCount = playgroundSystem.particleCount;
_currentParticleMinLifetime = playgroundSystem.lifetimeMin;
_currentParticleMaxLifetime = playgroundSystem.lifetime;
_localSpace = playgroundSystem.shurikenParticleSystem.simulationSpace == ParticleSystemSimulationSpace.Local;
}
_isDoneThread = true;
}
///
/// Removes the trail at index.
///
/// The trail index.
public void RemoveTrail (int index) {
if (Application.isPlaying)
Destroy(_trails[index].trailGameObject);
else
DestroyImmediate(_trails[index].trailGameObject);
_trails.RemoveAt(index);
}
///
/// Destroys all trails and clears out trail list.
///
public void DestroyAllTrails () {
foreach (ParticlePlaygroundTrail trail in _trails)
{
if (Application.isPlaying)
Destroy(trail.trailGameObject);
else
DestroyImmediate(trail.trailGameObject);
}
if (_parentGameObject != null)
{
if (Application.isPlaying)
Destroy (_parentGameObject);
else
DestroyImmediate(_parentGameObject);
}
_trails.Clear();
_birthQueue.Clear();
}
/****************************************************************************
Internal
****************************************************************************/
void CalculateTrail ()
{
// Iterate through all trails
for (int i = 0; i<_trails.Count; i++)
{
ParticlePlaygroundTrail trail = _trails[i];
// Skip this trail if it's prepared to be removed
if (trail.CanRemoveTrail())
continue;
if (trail.particleId >= 0 && !trail.IsDead())
{
if (trail.GetBirthIterator()>0)
{
// New point creation
float pointDistance = Vector3.Distance(trail.GetParticlePosition(), trail.GetLastAddedPointPosition());
if (pointDistance>minVertexDistance) {
float pathDeviationAngle = trail.GetPathDeviation();
if (pointDistance>maxVertexDistance || pathDeviationAngle>maxPathDeviation) {
trail.AddPoint(playgroundSystem.particleCache[trail.particleId].position, EvaluateWidth(0), time, _calculationStartTime);
}
}
}
else
{
// First point creation
trail.SetFirstPoint(playgroundSystem.particleCache[trail.particleId].position, playgroundSystem.particleCache[trail.particleId].velocity, EvaluateWidth(0), time, _calculationStartTime);
}
// Set the particle position info
trail.SetParticlePosition(playgroundSystem.particleCache[trail.particleId].position);
}
// Update the trail points
for (int x = 0; x
/// The trail render mode determines how the trail will be rotated.
/// Using billboard will rotate towards the assigned transform position, this is by default the main camera.
/// Horizontal will rotate the points flat on X-Z axis.
/// Vertical will rotate the points flat on X-Y axis.
/// CustomRenderScale is a global world space normal which will multiply the scale on each axis.
///
public enum TrailRenderMode
{
///
/// Rotate points towards assigned billboard transform.
///
Billboard,
///
/// Rotate points flat X-Z.
///
Horizontal,
///
/// Rotate points flat X-Y.
///
Vertical,
///
/// Creates a custom render rotation/scale.
///
CustomRenderScale
}
///
/// The trail color mode determines how color should be distributed over a trail.
///
public enum TrailColorMode
{
///
/// When using TrailColorMode.Lifetime the colors will be set depending on each point's normalized lifetime.
///
Lifetime,
///
/// When using TrailColorMode.PointArray the colors will be set depending on all the points within the trail, where each point is a normalized value linearly towards the total points.
///
PointArray
}
///
/// The trail uv mode determines how uv will be distributed over a trail.
///
public enum TrailUvMode
{
///
/// When using TrailUvMode.Lifetime the uvs will be set depending on each point's normalized lifetime.
///
Lifetime,
///
/// When using TrailUvMode.PointArray the uvs will be set depending on all the points within the trail, where each point is a normalized value linearly towards the total points.
///
PointArray
}
///
/// The trail particle info struct contains data about particles to be read by a Playground Trail.
///
public struct TrailParticleInfo {
///
/// The particle identifier linearly towards the particle system's cached particles.
///
public int particleId;
///
/// The position of this trail particle.
///
public Vector3 position;
///
/// The velocity of this trail particle.
///
public Vector3 velocity;
///
/// Initializes a new instance of the struct.
///
/// Particle identifier.
/// Particle position.
/// Particle velocity.
public TrailParticleInfo (int particleId, Vector3 position, Vector3 velocity)
{
this.particleId = particleId;
this.position = position;
this.velocity = velocity;
}
}
}