// ================================= // Namespaces. // ================================= using UnityEngine; using System.Collections.Generic; using System.Threading; // ================================= // Define namespace. // ================================= namespace MirzaBeig { namespace Scripting { namespace Effects { // ================================= // Classes. // ================================= [RequireComponent(typeof(ParticleSystem))] [AddComponentMenu("Effects/Particle Plexus")] public class ParticlePlexus : MonoBehaviour { // ================================= // Nested classes and structures. // ================================= // ... // ================================= // Variables. // ================================= // ... public float maxDistance = 1.0f; public int maxConnections = 5; public int maxLineRenderers = 100; [Space] [Range(0.0f, 1.0f)] public float widthFromParticle = 0.125f; [Space] [Range(0.0f, 1.0f)] public float colourFromParticle = 1.0f; [Range(0.0f, 1.0f)] public float alphaFromParticle = 1.0f; [Space] public AnimationCurve alphaOverNormalizedDistance = AnimationCurve.Linear(0.0f, 1.0f, 1.0f, 1.0f); //public float fadeInTime = 0.0f; new ParticleSystem particleSystem; ParticleSystem.Particle[] particles; Vector3[] particlePositions; Color[] particleColours; float[] particleSizes; ParticleSystem.MainModule particleSystemMainModule; [Space] public LineRenderer lineRendererTemplate; List lineRenderers = new List(); //[System.Serializable] //public struct LineRendererData //{ // public float currentStartParticleID; // public float currentEndParticleID; // public float previousStartParticleID; // public float previousEndParticleID; // public float timer; //} //List lineRendererData = new List(); Transform _transform; [Header("Triangle Mesh Settings")] public MeshFilter trianglesMeshFilter; Mesh trianglesMesh; List allConnectedParticles = new List(); [Space] // Triangles can be limited to those with particles who // are a fraction of the distance limit from other particles. // Example: // 1.0 == use same distance as maxDistance. // 0.5 == half of maxDistance. [Range(0.0f, 1.0f)] public float maxDistanceTriangleBias = 1.0f; [Space] // Additional filtering for triangle generation so that it's more based on distance to adjacent particles. public bool trianglesDistanceCheck = false; [Space] [Range(0.0f, 1.0f)] public float triangleColourFromParticle = 1.0f; [Range(0.0f, 1.0f)] public float triangleAlphaFromParticle = 1.0f; [Header("General Performance Settings")] [Range(0.0f, 1.0f)] public float delay = 0.0f; float timer; public bool alwaysUpdate = false; bool visible; //List customParticleData = new List(); //int uniqueParticleID; // ================================= // Functions. // ================================= // ... void Start() { particleSystem = GetComponent(); particleSystemMainModule = particleSystem.main; _transform = transform; if (trianglesMeshFilter) { trianglesMesh = new Mesh(); trianglesMeshFilter.mesh = trianglesMesh; // Unparent since I don't want it following me, but I may have // parented in the scene for organization. // EDIT: Nevermind, let the user deal with this as they see fit. //if (trianglesMeshFilter.transform != transform) //{ // trianglesMeshFilter.transform.parent = null; //} } } // ... void OnDisable() { for (int i = 0; i < lineRenderers.Count; i++) { lineRenderers[i].enabled = false; } } // ... void OnBecameVisible() { visible = true; } void OnBecameInvisible() { visible = false; } // ... void LateUpdate() { if (trianglesMeshFilter) { switch (particleSystemMainModule.simulationSpace) { case ParticleSystemSimulationSpace.World: { // Make sure this is always at origin, or triangle mesh moves out of sync since // the mesh triangle vertices are already set to the world space particle positions. trianglesMeshFilter.transform.position = Vector3.zero; break; } case ParticleSystemSimulationSpace.Local: { // In local space it should follow me. trianglesMeshFilter.transform.position = transform.position; trianglesMeshFilter.transform.rotation = transform.rotation; break; } case ParticleSystemSimulationSpace.Custom: { // In custom space it should follow the custom transform. trianglesMeshFilter.transform.position = particleSystemMainModule.customSimulationSpace.position; trianglesMeshFilter.transform.rotation = particleSystemMainModule.customSimulationSpace.rotation; break; } } } // Filter doesn't exist but mesh is there? That means the filter reference was lost. Clear the mesh. // AKA... If mesh filter is gone (deleted and/or reference nulled), clear the mesh. else if (trianglesMesh) { trianglesMesh.Clear(); } int lineRenderersCount = lineRenderers.Count; // In case max line renderers value is changed at runtime -> destroy extra. if (lineRenderersCount > maxLineRenderers) { for (int i = maxLineRenderers; i < lineRenderersCount; i++) { Destroy(lineRenderers[i].gameObject); } lineRenderers.RemoveRange(maxLineRenderers, lineRenderersCount - maxLineRenderers); //lineRendererData.RemoveRange(maxLineRenderers, lineRenderersCount - maxLineRenderers); lineRenderersCount -= lineRenderersCount - maxLineRenderers; } if (alwaysUpdate || visible) { // Prevent constant allocations so long as max particle count doesn't change. int maxParticles = particleSystemMainModule.maxParticles; if (particles == null || particles.Length < maxParticles) { particles = new ParticleSystem.Particle[maxParticles]; particlePositions = new Vector3[maxParticles]; particleColours = new Color[maxParticles]; particleSizes = new float[maxParticles]; } float deltaTime = Time.deltaTime; timer += deltaTime; if (timer >= delay) { timer = 0.0f; int lrIndex = 0; allConnectedParticles.Clear(); // Only update if drawing/making connections. if (maxConnections > 0 && maxLineRenderers > 0) { particleSystem.GetParticles(particles); //particleSystem.GetCustomParticleData(customParticleData, ParticleSystemCustomData.Custom1); int particleCount = particleSystem.particleCount; float maxDistanceSqr = maxDistance * maxDistance; ParticleSystemSimulationSpace simulationSpace = particleSystemMainModule.simulationSpace; ParticleSystemScalingMode scalingMode = particleSystemMainModule.scalingMode; Transform customSimulationSpaceTransform = particleSystemMainModule.customSimulationSpace; Color lineRendererStartColour = lineRendererTemplate.startColor; Color lineRendererEndColour = lineRendererTemplate.endColor; float lineRendererStartWidth = lineRendererTemplate.startWidth * lineRendererTemplate.widthMultiplier; float lineRendererEndWidth = lineRendererTemplate.endWidth * lineRendererTemplate.widthMultiplier; // Save particle properties in a quick loop (accessing these is expensive and loops significantly more later, so it's better to save them once now). for (int i = 0; i < particleCount; i++) { particlePositions[i] = particles[i].position; particleColours[i] = particles[i].GetCurrentColor(particleSystem); particleSizes[i] = particles[i].GetCurrentSize(particleSystem); // Default is 0.0f, so if default, this is a new particle and I need to assign a custom ID. //if (customParticleData[i].x == 0.0f) //{ // // ++value -> increment first, then return that value. // // That way it won't be zero (default) the first time I use it. // customParticleData[i] = new Vector4(++uniqueParticleID, 0, 0, 0); //} } //particleSystem.SetCustomParticleData(customParticleData, ParticleSystemCustomData.Custom1); Vector3 p1p2_difference; // If in world space, there's no need to do any of the extra calculations... simplify the loop! if (simulationSpace == ParticleSystemSimulationSpace.World) { for (int i = 0; i < particleCount; i++) { if (lrIndex == maxLineRenderers) { break; } Color particleColour = particleColours[i]; Color lineStartColour = Color.LerpUnclamped(lineRendererStartColour, particleColour, colourFromParticle); float lineStartColourOriginalAlpha = Mathf.LerpUnclamped(lineRendererStartColour.a, particleColour.a, alphaFromParticle); lineStartColour.a = lineStartColourOriginalAlpha; float lineStartWidth = Mathf.LerpUnclamped(lineRendererStartWidth, particleSizes[i], widthFromParticle); int connections = 0; int[] connectedParticles = new int[maxConnections + 1]; for (int j = i + 1; j < particleCount; j++) { p1p2_difference.x = particlePositions[i].x - particlePositions[j].x; p1p2_difference.y = particlePositions[i].y - particlePositions[j].y; p1p2_difference.z = particlePositions[i].z - particlePositions[j].z; //float distanceSqr = Vector3.SqrMagnitude(p1p2_difference); float distanceSqr = p1p2_difference.x * p1p2_difference.x + p1p2_difference.y * p1p2_difference.y + p1p2_difference.z * p1p2_difference.z; if (distanceSqr <= maxDistanceSqr) { LineRenderer lr; if (lrIndex == lineRenderersCount) { lr = Instantiate(lineRendererTemplate, _transform, false); lineRenderers.Add(lr); lineRenderersCount++; } lr = lineRenderers[lrIndex]; lr.enabled = true; lr.SetPosition(0, particlePositions[i]); lr.SetPosition(1, particlePositions[j]); float alphaAttenuation = alphaOverNormalizedDistance.Evaluate(distanceSqr / maxDistanceSqr); lineStartColour.a = lineStartColourOriginalAlpha * alphaAttenuation; lr.startColor = lineStartColour; particleColour = particleColours[j]; Color lineEndColour = Color.LerpUnclamped(lineRendererEndColour, particleColour, colourFromParticle); lineEndColour.a = Mathf.LerpUnclamped(lineRendererEndColour.a, particleColour.a, alphaFromParticle); lr.endColor = lineEndColour; lr.startWidth = lineStartWidth; lr.endWidth = Mathf.LerpUnclamped(lineRendererEndWidth, particleSizes[j], widthFromParticle); lrIndex++; connections++; // Intentionally taken AFTER connections++ (because index = 0 is the i'th / line origin particle). connectedParticles[connections] = j; if (connections == maxConnections || lrIndex == maxLineRenderers) { break; } } } if (connections >= 2) { connectedParticles[0] = i; allConnectedParticles.Add(connectedParticles); } } } else { Vector3 position = Vector3.zero; Quaternion rotation = Quaternion.identity; Vector3 localScale = Vector3.one; Transform simulationSpaceTransform = _transform; switch (simulationSpace) { case ParticleSystemSimulationSpace.Local: { position = simulationSpaceTransform.position; rotation = simulationSpaceTransform.rotation; localScale = simulationSpaceTransform.localScale; break; } case ParticleSystemSimulationSpace.Custom: { simulationSpaceTransform = customSimulationSpaceTransform; position = simulationSpaceTransform.position; rotation = simulationSpaceTransform.rotation; localScale = simulationSpaceTransform.localScale; break; } default: { throw new System.NotSupportedException( string.Format("Unsupported scaling mode '{0}'.", simulationSpace)); } } // I put these here so I can take out the default exception case. // Else I'd have a compiler error for potentially unassigned variables. Vector3 p1_position = Vector3.zero; Vector3 p2_position = Vector3.zero; for (int i = 0; i < particleCount; i++) { if (lrIndex == maxLineRenderers) { break; } switch (simulationSpace) { case ParticleSystemSimulationSpace.Local: case ParticleSystemSimulationSpace.Custom: { switch (scalingMode) { case ParticleSystemScalingMode.Hierarchy: { p1_position = simulationSpaceTransform.TransformPoint(particlePositions[i]); break; } case ParticleSystemScalingMode.Local: { // Order is important. //p1_position = Vector3.Scale(particlePositions[i], localScale); p1_position.x = particlePositions[i].x * localScale.x; p1_position.y = particlePositions[i].y * localScale.y; p1_position.z = particlePositions[i].z * localScale.z; p1_position = rotation * p1_position; //p1_position += position; p1_position.x += position.x; p1_position.y += position.y; p1_position.z += position.z; break; } case ParticleSystemScalingMode.Shape: { // Order is important. p1_position = rotation * particlePositions[i]; //p1_position += position; p1_position.x += position.x; p1_position.y += position.y; p1_position.z += position.z; break; } default: { throw new System.NotSupportedException( string.Format("Unsupported scaling mode '{0}'.", scalingMode)); } } break; } } Color particleColour = particleColours[i]; Color lineStartColour = Color.LerpUnclamped(lineRendererStartColour, particleColour, colourFromParticle); float lineStartColourOriginalAlpha = Mathf.LerpUnclamped(lineRendererStartColour.a, particleColour.a, alphaFromParticle); lineStartColour.a = lineStartColourOriginalAlpha; float lineStartWidth = Mathf.LerpUnclamped(lineRendererStartWidth, particleSizes[i], widthFromParticle); int connections = 0; int[] connectedParticles = new int[maxConnections + 1]; for (int j = i + 1; j < particleCount; j++) { // Note that because particles array is not sorted by distance, // but rather by spawn time (I think), the connections made are // not necessarily the closest. switch (simulationSpace) { case ParticleSystemSimulationSpace.Local: case ParticleSystemSimulationSpace.Custom: { switch (scalingMode) { case ParticleSystemScalingMode.Hierarchy: { p2_position = simulationSpaceTransform.TransformPoint(particlePositions[j]); break; } case ParticleSystemScalingMode.Local: { // Order is important. //p2_position = Vector3.Scale(particlePositions[j], localScale); p2_position.x = particlePositions[j].x * localScale.x; p2_position.y = particlePositions[j].y * localScale.y; p2_position.z = particlePositions[j].z * localScale.z; p2_position = rotation * p2_position; //p2_position += position; p2_position.x += position.x; p2_position.y += position.y; p2_position.z += position.z; break; } case ParticleSystemScalingMode.Shape: { // Order is important. p2_position = rotation * particlePositions[j]; //p2_position += position; p2_position.x += position.x; p2_position.y += position.y; p2_position.z += position.z; break; } default: { throw new System.NotSupportedException( string.Format("Unsupported scaling mode '{0}'.", scalingMode)); } } break; } } p1p2_difference.x = particlePositions[i].x - particlePositions[j].x; p1p2_difference.y = particlePositions[i].y - particlePositions[j].y; p1p2_difference.z = particlePositions[i].z - particlePositions[j].z; // Note that distance is always calculated in WORLD SPACE. // Scaling the particle system will stretch the distances // and may require adjusting the maxDistance value. // I could also do it in local space (which may actually make more // sense) by just getting the difference of the positions without // all the transformations. This also provides opportunity for // optimization as I can limit the world space transform calculations // to only happen if a particle is within range. // Think about: Putting in a bool to switch between the two? //float distanceSqr = Vector3.SqrMagnitude(p1p2_difference); float distanceSqr = p1p2_difference.x * p1p2_difference.x + p1p2_difference.y * p1p2_difference.y + p1p2_difference.z * p1p2_difference.z; // If distance to particle within range, add new vertex position. // The larger the max distance, the quicker connections will // reach its max, terminating the loop earlier. So even though more lines have // to be drawn, it's still faster to have a larger maxDistance value because // the call to Vector3.Distance() is expensive. if (distanceSqr <= maxDistanceSqr) { LineRenderer lr; if (lrIndex == lineRenderersCount) { lr = Instantiate(lineRendererTemplate, _transform, false); lineRenderers.Add(lr); lineRenderersCount++; //lineRendererData.Add(new LineRendererData()); } lr = lineRenderers[lrIndex]; //LineRendererData lrd = lineRendererData[lrIndex]; lr.enabled = true; //lrd.previousStartParticleID = lrd.currentStartParticleID; //lrd.previousEndParticleID = lrd.currentEndParticleID; //lrd.currentStartParticleID = customParticleData[i].x; //lrd.currentEndParticleID = customParticleData[j].x; lr.SetPosition(0, p1_position); lr.SetPosition(1, p2_position); //if (lrd.currentStartParticleID != lrd.previousStartParticleID || lrd.currentEndParticleID != lrd.previousEndParticleID) //{ // lrd.timer = 0.0f; //} //if (lrd.timer < 1.0f) //{ // lrd.timer += deltaTime / fadeInTime; //} //if (lrd.timer > 1.0f) //{ // lrd.timer = 1.0f; //} float alphaAttenuation = alphaOverNormalizedDistance.Evaluate(distanceSqr / maxDistanceSqr); //float alphaAttenuation = lrd.timer * alphaOverNormalizedDistance.Evaluate(distanceSqr / maxDistanceSqr); lineStartColour.a = lineStartColourOriginalAlpha * alphaAttenuation; lr.startColor = lineStartColour; particleColour = particleColours[j]; Color lineEndColour = Color.LerpUnclamped(lineRendererEndColour, particleColour, colourFromParticle); lineEndColour.a = Mathf.LerpUnclamped(lineRendererEndColour.a, particleColour.a, alphaFromParticle) * alphaAttenuation; lr.endColor = lineEndColour; lr.startWidth = lineStartWidth; lr.endWidth = Mathf.LerpUnclamped(lineRendererEndWidth, particleSizes[j], widthFromParticle); //lineRendererData[lrIndex] = lrd; lrIndex++; connections++; // Intentionally taken AFTER connections++ (because index = 0 is the i'th / line origin particle). connectedParticles[connections] = j; if (connections == maxConnections || lrIndex == maxLineRenderers) { break; } } } if (connections >= 2) { connectedParticles[0] = i; allConnectedParticles.Add(connectedParticles); } } } } // Disable remaining line renderers from the pool that weren't used. for (int i = lrIndex; i < lineRenderersCount; i++) { if (lineRenderers[i].enabled) { lineRenderers[i].enabled = false; } } // I check against the filter rather than the mesh because the mesh should always exist as long as the filter reference is there. // This way I can stop drawing/updating should the filter reference be lost. if (trianglesMeshFilter) { // Triangles mesh. // For efficiency (and my own general laziness), I only bother taking the first triangle formed. // It doesn't matter all that much since this is an abstract effect anyway. int vertexCount = allConnectedParticles.Count * 3; Vector3[] vertices = new Vector3[vertexCount]; int[] triangles = new int[vertexCount]; Vector2[] uv = new Vector2[vertexCount]; Color[] colours = new Color[vertexCount]; float maxDistanceSqr = (maxDistance * maxDistance) * maxDistanceTriangleBias; for (int i = 0; i < allConnectedParticles.Count; i++) { int[] connectedParticles = allConnectedParticles[i]; float distanceSqr = 0.0f; if (trianglesDistanceCheck) { Vector3 particlePositionA = particlePositions[connectedParticles[1]]; Vector3 particlePositionB = particlePositions[connectedParticles[2]]; //distance = Vector3.Distance(particlePositionA, particlePositionB); Vector3 difference; difference.x = particlePositionA.x - particlePositionB.x; difference.y = particlePositionA.y - particlePositionB.y; difference.z = particlePositionA.z - particlePositionB.z; distanceSqr = difference.x * difference.x + difference.y * difference.y + difference.z * difference.z; } if (distanceSqr < maxDistanceSqr) { int i3 = i * 3; vertices[i3 + 0] = particlePositions[connectedParticles[0]]; vertices[i3 + 1] = particlePositions[connectedParticles[1]]; vertices[i3 + 2] = particlePositions[connectedParticles[2]]; uv[i3 + 0] = new Vector2(0.0f, 0.0f); uv[i3 + 1] = new Vector2(0.0f, 1.0f); uv[i3 + 2] = new Vector2(1.0f, 1.0f); triangles[i3 + 0] = i3 + 0; triangles[i3 + 1] = i3 + 1; triangles[i3 + 2] = i3 + 2; colours[i3 + 0] = particleColours[connectedParticles[0]]; colours[i3 + 1] = particleColours[connectedParticles[1]]; colours[i3 + 2] = particleColours[connectedParticles[2]]; colours[i3 + 0] = Color.LerpUnclamped(Color.white, particleColours[connectedParticles[0]], triangleColourFromParticle); colours[i3 + 1] = Color.LerpUnclamped(Color.white, particleColours[connectedParticles[1]], triangleColourFromParticle); colours[i3 + 2] = Color.LerpUnclamped(Color.white, particleColours[connectedParticles[2]], triangleColourFromParticle); colours[i3 + 0].a = Mathf.LerpUnclamped(1.0f, particleColours[connectedParticles[0]].a, triangleAlphaFromParticle); colours[i3 + 1].a = Mathf.LerpUnclamped(1.0f, particleColours[connectedParticles[1]].a, triangleAlphaFromParticle); colours[i3 + 2].a = Mathf.LerpUnclamped(1.0f, particleColours[connectedParticles[2]].a, triangleAlphaFromParticle); } } trianglesMesh.Clear(); trianglesMesh.vertices = vertices; trianglesMesh.uv = uv; trianglesMesh.triangles = triangles; trianglesMesh.colors = colours; } } } } // ================================= // End functions. // ================================= } // ================================= // End namespace. // ================================= } } } // ================================= // --END-- // // =================================