// // Procedural Lightning for Unity // (c) 2015 Digital Ruby, LLC // Source code may be used for personal or commercial projects. // Source code may NOT be redistributed or sold. // // uncomment to enable profiling using stopwatch and debug.log // #define ENABLE_PROFILING #if NETFX_CORE #define TASK_AVAILABLE using System.Threading.Tasks; #endif using UnityEngine; using System; using System.Collections; using System.Collections.Generic; using System.Threading; using System.Collections.Concurrent; namespace DigitalRuby.ThunderAndLightning { /// /// Quality settings for lightning /// public enum LightningBoltQualitySetting { /// /// Use all settings from the script, ignoring the global quality setting /// UseScript, /// /// Use the global quality setting to determine lightning quality and maximum number of lights and shadowing /// LimitToQualitySetting } /// /// Camera modes /// public enum CameraMode { /// /// Auto detect /// Auto, /// /// Force perspective camera lightning /// Perspective, /// /// Force orthographic XY lightning /// OrthographicXY, /// /// Force orthographic XZ lightning /// OrthographicXZ, /// /// Unknown camera mode (do not use) /// Unknown } /// /// Lightning custom transfrom state /// public enum LightningCustomTransformState { /// /// Started /// Started, /// /// Executing /// Executing, /// /// Ended /// Ended } /// /// Lightning custom transform info /// public class LightningCustomTransformStateInfo { /// /// State /// public LightningCustomTransformState State { get; set; } /// /// Parameters /// public LightningBoltParameters Parameters { get; set; } /// /// Lightning bolt start position /// public Vector3 BoltStartPosition; /// /// Lightning bolt end position /// public Vector3 BoltEndPosition; /// /// Transform /// public Transform Transform; /// /// Start position transform /// public Transform StartTransform; /// /// End position transform /// public Transform EndTransform; /// /// User defined object /// public object UserInfo; private static readonly List cache = new List(); /// /// Get or create lightning custom transform state info from cache /// /// LightningCustomTransformStateInfo public static LightningCustomTransformStateInfo GetOrCreateStateInfo() { if (cache.Count == 0) { return new LightningCustomTransformStateInfo(); } int idx = cache.Count - 1; LightningCustomTransformStateInfo result = cache[idx]; cache.RemoveAt(idx); return result; } /// /// Put LightningCustomTransformStateInfo back into the cache /// /// LightningCustomTransformStateInfo to return to cache public static void ReturnStateInfoToCache(LightningCustomTransformStateInfo info) { if (info != null) { info.Transform = info.StartTransform = info.EndTransform = null; info.UserInfo = null; cache.Add(info); } } } /// /// Lightning custom transform delegate /// [System.Serializable] public class LightningCustomTransformDelegate : UnityEngine.Events.UnityEvent { } /// /// Lightning light parameters /// [System.Serializable] public class LightningLightParameters { /// /// Light render mode /// [Tooltip("Light render mode - leave as auto unless you have special use cases")] [HideInInspector] public LightRenderMode RenderMode = LightRenderMode.Auto; /// /// Color of light /// [Tooltip("Color of the light")] public Color LightColor = Color.white; /// /// What percent of segments should have a light? Keep this pretty low for performance, i.e. 0.05 or lower depending on generations /// Set really really low to only have 1 light, i.e. 0.0000001f /// For example, at generations 5, the main trunk has 32 segments, 64 at generation 6, etc. /// If non-zero, there wil be at least one light in the middle /// [Tooltip("What percent of segments should have a light? For performance you may want to keep this small.")] [Range(0.0f, 1.0f)] public float LightPercent = 0.000001f; /// /// What percent of lights created should cast shadows? /// [Tooltip("What percent of lights created should cast shadows?")] [Range(0.0f, 1.0f)] public float LightShadowPercent; /// /// Light intensity /// [Tooltip("Light intensity")] [Range(0.0f, 8.0f)] public float LightIntensity = 0.5f; /// /// Light multiplier. Can set to a high number (millions) if HDRP (lumens) support is needed. /// [Tooltip("Light multiplier. Can set to a high number (millions) if HDRP (lumens) support is needed.")] [Range(0.0f, 10000000.0f)] public float LightMultiplier = 1.0f; /// /// Bounce intensity /// [Tooltip("Bounce intensity")] [Range(0.0f, 8.0f)] public float BounceIntensity; /// /// Shadow strength, 0 - 1. 0 means all light, 1 means all shadow /// [Tooltip("Shadow strength, 0 means all light, 1 means all shadow")] [Range(0.0f, 1.0f)] public float ShadowStrength = 1.0f; /// /// Shadow bias /// [Tooltip("Shadow bias, 0 - 2")] [Range(0.0f, 2.0f)] public float ShadowBias = 0.05f; /// /// Shadow normal bias /// [Tooltip("Shadow normal bias, 0 - 3")] [Range(0.0f, 3.0f)] public float ShadowNormalBias = 0.4f; /// /// Light range /// [Tooltip("The range of each light created")] public float LightRange; /// /// Only light up objects that match this layer mask /// [Tooltip("Only light objects that match this layer mask")] public LayerMask CullingMask = ~0; /// Offset from camera position when in orthographic mode [Tooltip("Offset from camera position when in orthographic mode")] [Range(-1000.0f, 1000.0f)] public float OrthographicOffset = 0.0f; /// Increase the duration of light fade in compared to the lightning fade. [Tooltip("Increase the duration of light fade in compared to the lightning fade.")] [Range(0.0f, 20.0f)] public float FadeInMultiplier = 1.0f; /// Increase the duration of light fully lit compared to the lightning fade. [Tooltip("Increase the duration of light fully lit compared to the lightning fade.")] [Range(0.0f, 20.0f)] public float FadeFullyLitMultiplier = 1.0f; /// Increase the duration of light fade out compared to the lightning fade. [Tooltip("Increase the duration of light fade out compared to the lightning fade.")] [Range(0.0f, 20.0f)] public float FadeOutMultiplier = 1.0f; /// /// Should light be shown for these parameters? /// public bool HasLight { get { return (LightColor.a > 0.0f && LightIntensity >= 0.01f && LightPercent >= 0.0000001f && LightRange > 0.01f); } } } /// /// Parameters that control lightning bolt behavior /// [System.Serializable] public sealed class LightningBoltParameters { #region Internal use only // INTERNAL USE ONLY!!! private static int randomSeed = Environment.TickCount; private static readonly List cache = new List(); internal int generationWhereForksStop; internal int forkednessCalculated; internal LightningBoltQualitySetting quality; internal float delaySeconds; internal int maxLights; // END INTERNAL USE ONLY #endregion Internal use only /// /// Scale all scalar parameters by this value (i.e. trunk width, turbulence, turbulence velocity) /// public static float Scale = 1.0f; /// /// Contains quality settings for different quality levels. By default, this assumes 6 quality levels, so if you have your own /// custom quality setting levels, you may want to clear this dictionary out and re-populate it with your own limits /// public static readonly Dictionary QualityMaximums = new Dictionary(); static LightningBoltParameters() { string[] names = QualitySettings.names; for (int i = 0; i < names.Length; i++) { switch (i) { case 0: QualityMaximums[i] = new LightningQualityMaximum { MaximumGenerations = 3, MaximumLightPercent = 0, MaximumShadowPercent = 0.0f }; break; case 1: QualityMaximums[i] = new LightningQualityMaximum { MaximumGenerations = 4, MaximumLightPercent = 0, MaximumShadowPercent = 0.0f }; break; case 2: QualityMaximums[i] = new LightningQualityMaximum { MaximumGenerations = 5, MaximumLightPercent = 0.1f, MaximumShadowPercent = 0.0f }; break; case 3: QualityMaximums[i] = new LightningQualityMaximum { MaximumGenerations = 5, MaximumLightPercent = 0.1f, MaximumShadowPercent = 0.0f }; break; case 4: QualityMaximums[i] = new LightningQualityMaximum { MaximumGenerations = 6, MaximumLightPercent = 0.05f, MaximumShadowPercent = 0.1f }; break; case 5: QualityMaximums[i] = new LightningQualityMaximum { MaximumGenerations = 7, MaximumLightPercent = 0.025f, MaximumShadowPercent = 0.05f }; break; default: QualityMaximums[i] = new LightningQualityMaximum { MaximumGenerations = 8, MaximumLightPercent = 0.025f, MaximumShadowPercent = 0.05f }; break; } } } /// /// Constructor /// public LightningBoltParameters() { unchecked { random = currentRandom = new System.Random(randomSeed++); } Points = new List(); } /// /// Generator to create the lightning bolt from the parameters /// public LightningGenerator Generator; /// /// Start of the bolt /// public Vector3 Start; /// /// End of the bolt /// public Vector3 End; /// /// X, Y and Z radius variance from Start /// public Vector3 StartVariance; /// /// X, Y and Z radius variance from End /// public Vector3 EndVariance; /// /// Custom transform action, null if none /// public System.Action CustomTransform; private int generations; /// /// Number of generations (0 for just a point light, otherwise 1 - 8). Higher generations have lightning with finer detail but more expensive to create. /// public int Generations { get { return generations; } set { int v = Mathf.Clamp(value, 1, 8); if (quality == LightningBoltQualitySetting.UseScript) { generations = v; } else { LightningQualityMaximum maximum; int level = QualitySettings.GetQualityLevel(); if (QualityMaximums.TryGetValue(level, out maximum)) { generations = Mathf.Min(maximum.MaximumGenerations, v); } else { generations = v; Debug.LogError("Unable to read lightning quality settings from level " + level.ToString()); } } } } /// /// How long the bolt should live in seconds /// public float LifeTime; /// /// Minimum delay /// public float Delay; /// /// How long to wait in seconds before starting additional lightning bolts /// public RangeOfFloats DelayRange; /// /// How chaotic is the main trunk of lightning? (0 - 1). Higher numbers create more chaotic lightning. /// public float ChaosFactor; /// /// How chaotic are the forks of the lightning? (0 - 1). Higher numbers create more chaotic lightning. /// public float ChaosFactorForks = -1.0f; /// /// The width of the trunk /// public float TrunkWidth; /// /// The ending width of a segment of lightning /// public float EndWidthMultiplier = 0.5f; /// /// Intensity of the lightning /// public float Intensity = 1.0f; /// /// Intensity of the glow /// public float GlowIntensity; /// /// Glow width multiplier /// public float GlowWidthMultiplier; /// /// How forked the lightning should be, 0 for none, 1 for LOTS of forks /// public float Forkedness; /// /// This is subtracted from the initial generations value, and any generation below that cannot have a fork /// public int GenerationWhereForksStopSubtractor = 5; /// /// Tint color for the lightning, this is applied to both the lightning and the glow. Unlike the script properties for coloring which /// are applied per material, this is applied at the mesh level and as such different bolts on the same script can use different color values. /// public Color32 Color = new Color32(byte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue); /// /// Tint color for main trunk of lightning /// public Color32 MainTrunkTintColor = new Color32(byte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue); /// /// Used to generate random numbers. Not thread safe. /// public System.Random Random { get { return currentRandom; } set { random = value ?? random; currentRandom = (randomOverride ?? random); } } private System.Random random; private System.Random currentRandom; /// /// Override Random to a different Random. This gets set back to null when the parameters go back to the cache. Great for a one time bolt that looks a certain way. /// public System.Random RandomOverride { get { return randomOverride; } set { randomOverride = value; currentRandom = (randomOverride ?? random); } } private System.Random randomOverride; /// /// The percent of time the lightning should fade in and out (0 - 1). Example: 0.2 would fade in for 20% of the lifetime and fade out for 20% of the lifetime. Set to 0 for no fade. /// public float FadePercent = 0.15f; /// /// Modify the fade in time for FadePercent (0 - 1) /// public float FadeInMultiplier = 1.0f; /// /// Modify the fully lit time for FadePercent (0 - 1) /// public float FadeFullyLitMultiplier = 1.0f; /// /// Modify the fade out time for FadePercent (0 - 1) /// public float FadeOutMultiplier = 1.0f; private float growthMultiplier; /// /// A value between 0 and 0.999 that determines how fast the lightning should grow over the lifetime. A value of 1 grows slowest, 0 grows instantly /// public float GrowthMultiplier { get { return growthMultiplier; } set { growthMultiplier = Mathf.Clamp(value, 0.0f, 0.999f); } } /// /// Minimum distance multiplier for forks /// public float ForkLengthMultiplier = 0.6f; /// /// Variance of the fork distance (random range of 0 to n is added to ForkLengthMultiplier) /// public float ForkLengthVariance = 0.2f; /// /// Forks will have their end widths multiplied by this value /// public float ForkEndWidthMultiplier = 1.0f; /// /// Light parameters, null for none /// public LightningLightParameters LightParameters; /// /// Points for the trunk to follow - not all generators support this /// public List Points { get; set; } /// /// The amount of smoothing applied. For example, if there were 4 original points and smoothing / spline created 32 points, this value would be 8 - not all generators support this /// public int SmoothingFactor; /// /// Get a multiplier for fork distance /// /// Fork multiplier public float ForkMultiplier() { return ((float)Random.NextDouble() * ForkLengthVariance) + ForkLengthMultiplier; } /// /// Apply variance to a vector /// /// Position /// Variance /// New position public Vector3 ApplyVariance(Vector3 pos, Vector3 variance) { return new Vector3 ( pos.x + (((float)Random.NextDouble() * 2.0f) - 1.0f) * variance.x, pos.y + (((float)Random.NextDouble() * 2.0f) - 1.0f) * variance.y, pos.z + (((float)Random.NextDouble() * 2.0f) - 1.0f) * variance.z ); } /// /// Reset parameters /// public void Reset() { Start = End = Vector3.zero; Generator = null; SmoothingFactor = 0; RandomOverride = null; CustomTransform = null; if (Points != null) { Points.Clear(); } } /// /// Get or create lightning bolt parameters. If cache has parameters, one is taken, otherwise a new object is created. NOT thread safe. /// /// Lightning bolt parameters public static LightningBoltParameters GetOrCreateParameters() { LightningBoltParameters p; if (cache.Count == 0) { unchecked { p = new LightningBoltParameters(); } } else { int i = cache.Count - 1; p = cache[i]; cache.RemoveAt(i); } return p; } /// /// Return parameters to cache. NOT thread safe. /// /// Parameters public static void ReturnParametersToCache(LightningBoltParameters p) { if (!cache.Contains(p)) { // reset variables that are state-machine dependant p.Reset(); cache.Add(p); } } } /// /// A group of lightning bolt segments, such as the main trunk of the lightning bolt /// public class LightningBoltSegmentGroup { /// /// Width /// public float LineWidth; /// /// Start index of the segment to render (for performance, some segments are not rendered and only used for calculations) /// public int StartIndex; /// /// Generation /// public int Generation; /// /// Delay before rendering should start /// public float Delay; /// /// Peak start, the segments should be fully visible at this point /// public float PeakStart; /// /// Peak end, the segments should start to go away after this point /// public float PeakEnd; /// /// Total life time the group will be alive in seconds /// public float LifeTime; /// /// The width can be scaled down to the last segment by this amount if desired /// public float EndWidthMultiplier; /// /// Color for the group /// public Color32 Color; /// /// Total number of active segments /// public int SegmentCount { get { return Segments.Count - StartIndex; } } /// /// Segments /// public readonly List Segments = new List(); /// /// Lights /// public readonly List Lights = new List(); /// /// Light parameters /// public LightningLightParameters LightParameters; /// /// Return the group to its cache if there is one /// public void Reset() { LightParameters = null; Segments.Clear(); Lights.Clear(); StartIndex = 0; } } /// /// A single segment of a lightning bolt /// public struct LightningBoltSegment { /// /// Segment start /// public Vector3 Start; /// /// Segment end /// public Vector3 End; /// /// ToString /// /// String public override string ToString() { return Start.ToString() + ", " + End.ToString(); } } /// /// Contains maximum values for a given quality settings /// public class LightningQualityMaximum { /// /// Maximum generations /// public int MaximumGenerations { get; set; } /// /// Maximum light percent /// public float MaximumLightPercent { get; set; } /// /// Maximum light shadow percent /// public float MaximumShadowPercent { get; set; } } /// /// Lightning bolt dependencies /// public class LightningBoltDependencies { /// /// Parent - do not access from threads /// public GameObject Parent; /// /// Material for glow - do not access from threads /// public Material LightningMaterialMesh; /// /// Material for bolt - do not access from threads /// public Material LightningMaterialMeshNoGlow; /// /// Origin particle system - do not access from threads /// public ParticleSystem OriginParticleSystem; /// /// Dest particle system - do not access from threads /// public ParticleSystem DestParticleSystem; /// /// Camera position /// public Vector3 CameraPos; /// /// Is camera 2D? /// public bool CameraIsOrthographic; /// /// Camera mode /// public CameraMode CameraMode; /// /// Use world space /// public bool UseWorldSpace; /// /// Level of detail distance /// public float LevelOfDetailDistance; /// /// Sort layer name /// public string SortLayerName; /// /// Order in layer /// public int SortOrderInLayer; /// /// Parameters /// public ICollection Parameters; /// /// Thread state /// public LightningThreadState ThreadState; /// /// Method to start co-routines /// public Func StartCoroutine; /// /// Call this when a light is added /// public Action LightAdded; /// /// Call this when a light is removed /// public Action LightRemoved; /// /// Call this when the bolt becomes active /// public Action AddActiveBolt; /// /// Returns the dependencies to their cache /// public Action ReturnToCache; /// /// Runs when a lightning bolt is started (parameters, start, end) /// public Action LightningBoltStarted; /// /// Runs when a lightning bolt is ended (parameters, start, end) /// public Action LightningBoltEnded; } /// /// Lightning bolt /// public class LightningBolt { #region LineRendererMesh /// /// Class the encapsulates a game object, and renderer for lightning bolt meshes /// public class LineRendererMesh { #region Public variables /// /// Game object /// public GameObject GameObject { get; private set; } /// /// Material for glow /// public Material MaterialGlow { get { return meshRendererGlow.sharedMaterial; } set { meshRendererGlow.sharedMaterial = value; } } /// /// Material for bolt /// public Material MaterialBolt { get { return meshRendererBolt.sharedMaterial; } set { meshRendererBolt.sharedMaterial = value; } } /// /// Mesh renderer for glow /// public MeshRenderer MeshRendererGlow { get { return meshRendererGlow; } } /// /// Mesh renderer for bolt /// public MeshRenderer MeshRendererBolt { get { return meshRendererBolt; } } /// /// User defined int /// public int Tag { get; set; } #endregion Public variables #region Public properties /// /// Custom transform, null if none /// public System.Action CustomTransform { get; set; } /// /// The transform component /// public Transform Transform { get; private set; } /// /// Is the line renderer empty? /// public bool Empty { get { return vertices.Count == 0; } } #endregion Public properties #region Public methods /// /// Constructor /// /// Dependencies public LineRendererMesh(LightningBoltDependencies dependencies) { dependencies.ThreadState.AddActionForMainThread(b => { GameObject = new GameObject("LightningBoltMeshRenderer"); GameObject.SetActive(false); // call Begin to activate mesh = new Mesh { name = "ProceduralLightningMesh" }; mesh.MarkDynamic(); GameObject glowObject = new GameObject("LightningBoltMeshRendererGlow"); glowObject.transform.parent = GameObject.transform; GameObject boltObject = new GameObject("LightningBoltMeshRendererBolt"); boltObject.transform.parent = GameObject.transform; meshFilterGlow = glowObject.AddComponent(); meshFilterBolt = boltObject.AddComponent(); meshFilterGlow.sharedMesh = meshFilterBolt.sharedMesh = mesh; meshRendererGlow = glowObject.AddComponent(); meshRendererBolt = boltObject.AddComponent(); #if UNITY_EDITOR GameObject.hideFlags = glowObject.hideFlags = boltObject.hideFlags = HideFlags.HideAndDontSave; #endif meshRendererGlow.shadowCastingMode = meshRendererBolt.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; meshRendererGlow.reflectionProbeUsage = meshRendererBolt.reflectionProbeUsage = UnityEngine.Rendering.ReflectionProbeUsage.Off; meshRendererGlow.lightProbeUsage = meshRendererBolt.lightProbeUsage = UnityEngine.Rendering.LightProbeUsage.Off; meshRendererGlow.receiveShadows = meshRendererBolt.receiveShadows = false; Transform = GameObject.GetComponent(); }, true); } /// /// Apply changes to underlying mesh /// public void PopulateMesh() { #if ENABLE_PROFILING System.Diagnostics.Stopwatch w = System.Diagnostics.Stopwatch.StartNew(); #endif if (vertices.Count == 0) { mesh.Clear(); } else { PopulateMeshInternal(); } #if ENABLE_PROFILING Debug.LogFormat("MESH: {0}", w.Elapsed.TotalMilliseconds); #endif } /// /// Prepare for lines to be added /// /// Lines to add /// True if room for lines, false if full public bool PrepareForLines(int lineCount) { int vertexCount = lineCount * 4; if (vertices.Count + vertexCount > 64999) { return false; } return true; } /// /// Begin a line /// /// Start position /// End position /// Radius /// Color /// Color intensity /// Fade lifetime /// Glow width modifier /// Glow intensity public void BeginLine(Vector3 start, Vector3 end, float radius, Color32 color, float colorIntensity, Vector4 fadeLifeTime, float glowWidthModifier, float glowIntensity) { Vector4 dir = (end - start); dir.w = radius; AppendLineInternal(ref start, ref end, ref dir, ref dir, ref dir, color, colorIntensity, ref fadeLifeTime, glowWidthModifier, glowIntensity); } /// /// Append a line, call multiple times after the one BeginLine call /// /// Start position /// End position /// Radius /// Color /// Color intensity /// Fade lifetime /// Glow width modifier /// Glow intensity public void AppendLine(Vector3 start, Vector3 end, float radius, Color32 color, float colorIntensity, Vector4 fadeLifeTime, float glowWidthModifier, float glowIntensity) { Vector4 dir = (end - start); dir.w = radius; Vector4 dirPrev1 = lineDirs[lineDirs.Count - 3]; Vector4 dirPrev2 = lineDirs[lineDirs.Count - 1]; AppendLineInternal(ref start, ref end, ref dir, ref dirPrev1, ref dirPrev2, color, colorIntensity, ref fadeLifeTime, glowWidthModifier, glowIntensity); } /// /// Reset all state /// public void Reset() { CustomTransform = null; Tag++; GameObject.SetActive(false); mesh.Clear(); indices.Clear(); vertices.Clear(); colors.Clear(); lineDirs.Clear(); ends.Clear(); #if UNITY_PRE_5_3 texCoords.Clear(); glowModifiers.Clear(); fadeXY.Clear(); fadeZW.Clear(); #else texCoordsAndGlowModifiers.Clear(); fadeLifetimes.Clear(); #endif currentBoundsMaxX = currentBoundsMaxY = currentBoundsMaxZ = int.MinValue + boundsPadder; currentBoundsMinX = currentBoundsMinY = currentBoundsMinZ = int.MaxValue - boundsPadder; } #endregion Public methods #region Private variables private const int defaultListCapacity = 2048; private static readonly Vector2 uv1 = new Vector2(0.0f, 0.0f); private static readonly Vector2 uv2 = new Vector2(1.0f, 0.0f); private static readonly Vector2 uv3 = new Vector2(0.0f, 1.0f); private static readonly Vector2 uv4 = new Vector2(1.0f, 1.0f); private readonly List indices = new List(defaultListCapacity); private readonly List vertices = new List(defaultListCapacity); private readonly List lineDirs = new List(defaultListCapacity); private readonly List colors = new List(defaultListCapacity); private readonly List ends = new List(defaultListCapacity); #if UNITY_PRE_5_3 private readonly List texCoords = new List(defaultListCapacity); private readonly List glowModifiers = new List(defaultListCapacity); private readonly List fadeXY = new List(defaultListCapacity); private readonly List fadeZW = new List(defaultListCapacity); #else private readonly List texCoordsAndGlowModifiers = new List(defaultListCapacity); private readonly List fadeLifetimes = new List(defaultListCapacity); #endif private const int boundsPadder = 1000000000; private int currentBoundsMinX = int.MaxValue - boundsPadder; private int currentBoundsMinY = int.MaxValue - boundsPadder; private int currentBoundsMinZ = int.MaxValue - boundsPadder; private int currentBoundsMaxX = int.MinValue + boundsPadder; private int currentBoundsMaxY = int.MinValue + boundsPadder; private int currentBoundsMaxZ = int.MinValue + boundsPadder; private Mesh mesh; private MeshFilter meshFilterGlow; private MeshFilter meshFilterBolt; private MeshRenderer meshRendererGlow; private MeshRenderer meshRendererBolt; #endregion Private variables #region Private methods private void PopulateMeshInternal() { GameObject.SetActive(true); mesh.SetVertices(vertices); mesh.SetTangents(lineDirs); mesh.SetColors(colors); mesh.SetUVs(0, texCoordsAndGlowModifiers); mesh.SetUVs(1, fadeLifetimes); mesh.SetNormals(ends); mesh.SetTriangles(indices, 0); Bounds b = new Bounds(); Vector3 min = new Vector3(currentBoundsMinX - 2, currentBoundsMinY - 2, currentBoundsMinZ - 2); Vector3 max = new Vector3(currentBoundsMaxX + 2, currentBoundsMaxY + 2, currentBoundsMaxZ + 2); b.center = (max + min) * 0.5f; b.size = (max - min) * 1.2f; mesh.bounds = b; } private void UpdateBounds(ref Vector3 point1, ref Vector3 point2) { // r = y + ((x - y) & ((x - y) >> (sizeof(int) * CHAR_BIT - 1))); // min(x, y) // r = x - ((x - y) & ((x - y) >> (sizeof(int) * CHAR_BIT - 1))); // max(x, y) unchecked { { int xCalculation = (int)point1.x - (int)point2.x; xCalculation &= (xCalculation >> 31); int xMin = (int)point2.x + xCalculation; int xMax = (int)point1.x - xCalculation; xCalculation = currentBoundsMinX - xMin; xCalculation &= (xCalculation >> 31); currentBoundsMinX = xMin + xCalculation; xCalculation = currentBoundsMaxX - xMax; xCalculation &= (xCalculation >> 31); currentBoundsMaxX = currentBoundsMaxX - xCalculation; } { int yCalculation = (int)point1.y - (int)point2.y; yCalculation &= (yCalculation >> 31); int yMin = (int)point2.y + yCalculation; int yMax = (int)point1.y - yCalculation; yCalculation = currentBoundsMinY - yMin; yCalculation &= (yCalculation >> 31); currentBoundsMinY = yMin + yCalculation; yCalculation = currentBoundsMaxY - yMax; yCalculation &= (yCalculation >> 31); currentBoundsMaxY = currentBoundsMaxY - yCalculation; } { int zCalculation = (int)point1.z - (int)point2.z; zCalculation &= (zCalculation >> 31); int zMin = (int)point2.z + zCalculation; int zMax = (int)point1.z - zCalculation; zCalculation = currentBoundsMinZ - zMin; zCalculation &= (zCalculation >> 31); currentBoundsMinZ = zMin + zCalculation; zCalculation = currentBoundsMaxZ - zMax; zCalculation &= (zCalculation >> 31); currentBoundsMaxZ = currentBoundsMaxZ - zCalculation; } } } private void AddIndices() { int vertexIndex = vertices.Count; indices.Add(vertexIndex++); indices.Add(vertexIndex++); indices.Add(vertexIndex); indices.Add(vertexIndex--); indices.Add(vertexIndex); indices.Add(vertexIndex += 2); } private void AppendLineInternal(ref Vector3 start, ref Vector3 end, ref Vector4 dir, ref Vector4 dirPrev1, ref Vector4 dirPrev2, Color32 color, float colorIntensity, ref Vector4 fadeLifeTime, float glowWidthModifier, float glowIntensity) { AddIndices(); color.a = (byte)Mathf.Lerp(0.0f, 255.0f, colorIntensity * 0.1f); Vector4 texCoord = new Vector4(uv1.x, uv1.y, glowWidthModifier, glowIntensity); vertices.Add(start); lineDirs.Add(dirPrev1); colors.Add(color); ends.Add(dir); vertices.Add(end); lineDirs.Add(dir); colors.Add(color); ends.Add(dir); dir.w = -dir.w; vertices.Add(start); lineDirs.Add(dirPrev2); colors.Add(color); ends.Add(dir); vertices.Add(end); lineDirs.Add(dir); colors.Add(color); ends.Add(dir); #if UNITY_PRE_5_3 texCoords.Add(uv1); texCoords.Add(uv2); texCoords.Add(uv3); texCoords.Add(uv4); glowModifiers.Add(new Vector2(texCoord.z, texCoord.w)); glowModifiers.Add(new Vector2(texCoord.z, texCoord.w)); glowModifiers.Add(new Vector2(texCoord.z, texCoord.w)); glowModifiers.Add(new Vector2(texCoord.z, texCoord.w)); fadeXY.Add(new Vector2(fadeLifeTime.x, fadeLifeTime.y)); fadeXY.Add(new Vector2(fadeLifeTime.x, fadeLifeTime.y)); fadeXY.Add(new Vector2(fadeLifeTime.x, fadeLifeTime.y)); fadeXY.Add(new Vector2(fadeLifeTime.x, fadeLifeTime.y)); fadeZW.Add(new Vector2(fadeLifeTime.z, fadeLifeTime.w)); fadeZW.Add(new Vector2(fadeLifeTime.z, fadeLifeTime.w)); fadeZW.Add(new Vector2(fadeLifeTime.z, fadeLifeTime.w)); fadeZW.Add(new Vector2(fadeLifeTime.z, fadeLifeTime.w)); #else texCoordsAndGlowModifiers.Add(texCoord); texCoord.x = uv2.x; texCoord.y = uv2.y; texCoordsAndGlowModifiers.Add(texCoord); texCoord.x = uv3.x; texCoord.y = uv3.y; texCoordsAndGlowModifiers.Add(texCoord); texCoord.x = uv4.x; texCoord.y = uv4.y; texCoordsAndGlowModifiers.Add(texCoord); fadeLifetimes.Add(fadeLifeTime); fadeLifetimes.Add(fadeLifeTime); fadeLifetimes.Add(fadeLifeTime); fadeLifetimes.Add(fadeLifeTime); #endif UpdateBounds(ref start, ref end); } #endregion Private methods } #endregion LineRendererMesh #region Public variables /// /// The maximum number of lights to allow for all lightning /// public static int MaximumLightCount = 128; /// /// The maximum number of lights to create per batch of lightning emitted /// public static int MaximumLightsPerBatch = 8; /// /// The current minimum delay until anything will start rendering /// public float MinimumDelay { get; private set; } /// /// Is there any glow for any of the lightning bolts? /// public bool HasGlow { get; private set; } /// /// Is this lightning bolt active any more? /// public bool IsActive { get { return elapsedTime < lifeTime; } } /// /// Camera mode /// public CameraMode CameraMode { get; private set; } private DateTime startTimeOffset; #endregion Public variables #region Public methods /// /// Default constructor /// public LightningBolt() { } /// /// Setup a lightning bolt from dependencies /// /// Dependencies public void SetupLightningBolt(LightningBoltDependencies dependencies) { if (dependencies == null || dependencies.Parameters.Count == 0) { Debug.LogError("Lightning bolt dependencies must not be null"); return; } else if (this.dependencies != null) { Debug.LogError("This lightning bolt is already in use!"); return; } this.dependencies = dependencies; CameraMode = dependencies.CameraMode; timeSinceLevelLoad = LightningBoltScript.TimeSinceStart; CheckForGlow(dependencies.Parameters); MinimumDelay = float.MaxValue; if (dependencies.ThreadState.multiThreaded) { startTimeOffset = DateTime.UtcNow; dependencies.ThreadState.AddActionForBackgroundThread(ProcessAllLightningParameters); } else { ProcessAllLightningParameters(); } } /// /// Update /// /// True if alive, false if expired public bool Update() { elapsedTime += LightningBoltScript.DeltaTime; if (elapsedTime > maxLifeTime) { return false; } else if (hasLight) { UpdateLights(); } return true; } /// /// Cleanup all resources /// public void Cleanup() { foreach (LightningBoltSegmentGroup g in segmentGroupsWithLight) { // cleanup lights foreach (Light l in g.Lights) { CleanupLight(l); } g.Lights.Clear(); } lock (groupCache) { foreach (LightningBoltSegmentGroup g in segmentGroups) { groupCache.Add(g); } } hasLight = false; elapsedTime = 0.0f; lifeTime = 0.0f; maxLifeTime = 0.0f; if (dependencies != null) { dependencies.ReturnToCache(dependencies); dependencies = null; } // return all line renderers to cache foreach (LineRendererMesh m in activeLineRenderers) { if (m != null) { m.Reset(); lineRendererCache.Add(m); } } segmentGroups.Clear(); segmentGroupsWithLight.Clear(); activeLineRenderers.Clear(); } /// /// Add a new segment group, or get one from cache /// /// LightningBoltSegmentGroup public LightningBoltSegmentGroup AddGroup() { LightningBoltSegmentGroup group; lock (groupCache) { if (groupCache.Count == 0) { group = new LightningBoltSegmentGroup(); } else { int index = groupCache.Count - 1; group = groupCache[index]; group.Reset(); groupCache.RemoveAt(index); } } segmentGroups.Add(group); return group; } /// /// Clear out all cached objects to free up memory /// public static void ClearCache() { foreach (LineRendererMesh obj in lineRendererCache) { if (obj != null) { GameObject.Destroy(obj.GameObject); } } foreach (Light obj in lightCache) { if (obj != null) { GameObject.Destroy(obj.gameObject); } } lineRendererCache.Clear(); lightCache.Clear(); lock (groupCache) { groupCache.Clear(); } } #endregion Public methods #region Private variables // required dependencies to create lightning bolts private LightningBoltDependencies dependencies; // how long this bolt has been alive private float elapsedTime; // total life span of this bolt private float lifeTime; // either lifeTime or larger depending on if lights are lingering beyond the end of the bolt private float maxLifeTime; // does this lightning bolt have light? private bool hasLight; // saved in case of threading private float timeSinceLevelLoad; private readonly List segmentGroups = new List(); private readonly List segmentGroupsWithLight = new List(); private readonly List activeLineRenderers = new List(); private static int lightCount; private static readonly List lineRendererCache = new List(); private static readonly List groupCache = new List(); private static readonly List lightCache = new List(); #endregion Private variables #region Private methods private void CleanupLight(Light l) { if (l != null) { dependencies.LightRemoved(l); lightCache.Add(l); l.gameObject.SetActive(false); lightCount--; } } private void EnableLineRenderer(LineRendererMesh lineRenderer, int tag) { bool shouldPopulate = (lineRenderer != null && lineRenderer.GameObject != null && lineRenderer.Tag == tag && IsActive); if (shouldPopulate) { lineRenderer.PopulateMesh(); } } private IEnumerator EnableLastRendererCoRoutine() { LineRendererMesh lineRenderer = activeLineRenderers[activeLineRenderers.Count - 1]; int tag = ++lineRenderer.Tag; // in case it gets cleaned up for later yield return WaitForSecondsLightning.WaitForSecondsLightningPooled(MinimumDelay); EnableLineRenderer(lineRenderer, tag); } private LineRendererMesh GetOrCreateLineRenderer() { LineRendererMesh lineRenderer; while (true) { if (lineRendererCache.Count == 0) { lineRenderer = new LineRendererMesh(this.dependencies); } else { int index = lineRendererCache.Count - 1; lineRenderer = lineRendererCache[index]; lineRendererCache.RemoveAt(index); if (lineRenderer == null || lineRenderer.Transform == null) { // destroyed by some other means, try again for cache... continue; } } break; } dependencies.ThreadState.AddActionForMainThread(b => { // clear parent - this ensures that the rotation and scale can be reset before assigning a new parent lineRenderer.Transform.parent = null; lineRenderer.Transform.rotation = Quaternion.identity; lineRenderer.Transform.localScale = Vector3.one; lineRenderer.Transform.parent = dependencies.Parent.transform; lineRenderer.GameObject.layer = lineRenderer.MeshRendererBolt.gameObject.layer = lineRenderer.MeshRendererGlow.gameObject.layer = dependencies.Parent.layer; // maintain the layer of the parent if (dependencies.UseWorldSpace) { lineRenderer.GameObject.transform.position = Vector3.zero; } else { lineRenderer.GameObject.transform.localPosition = Vector3.zero; } lineRenderer.MaterialGlow = dependencies.LightningMaterialMesh; lineRenderer.MaterialBolt = dependencies.LightningMaterialMeshNoGlow; if (!string.IsNullOrEmpty(dependencies.SortLayerName)) { lineRenderer.MeshRendererGlow.sortingLayerName = lineRenderer.MeshRendererBolt.sortingLayerName = dependencies.SortLayerName; lineRenderer.MeshRendererGlow.sortingOrder = lineRenderer.MeshRendererBolt.sortingOrder = dependencies.SortOrderInLayer; } else { lineRenderer.MeshRendererGlow.sortingLayerName = lineRenderer.MeshRendererBolt.sortingLayerName = null; lineRenderer.MeshRendererGlow.sortingOrder = lineRenderer.MeshRendererBolt.sortingOrder = 0; } }, true); activeLineRenderers.Add(lineRenderer); return lineRenderer; } private void RenderGroup(LightningBoltSegmentGroup group, LightningBoltParameters p) { if (group.SegmentCount == 0) { return; } float timeOffset = (!dependencies.ThreadState.multiThreaded ? 0.0f : (float)(DateTime.UtcNow - startTimeOffset).TotalSeconds); float timeStart = timeSinceLevelLoad + group.Delay + timeOffset; Vector4 fadeLifeTime = new Vector4(timeStart, timeStart + group.PeakStart, timeStart + group.PeakEnd, timeStart + group.LifeTime); float radius = group.LineWidth * 0.5f * LightningBoltParameters.Scale; int lineCount = (group.Segments.Count - group.StartIndex); float radiusStep = (radius - (radius * group.EndWidthMultiplier)) / (float)lineCount; // growth multiplier float timeStep; if (p.GrowthMultiplier > 0.0f) { timeStep = (group.LifeTime / (float)lineCount) * p.GrowthMultiplier; timeOffset = 0.0f; } else { timeStep = 0.0f; timeOffset = 0.0f; } LineRendererMesh currentLineRenderer = (activeLineRenderers.Count == 0 ? GetOrCreateLineRenderer() : activeLineRenderers[activeLineRenderers.Count - 1]); // if we have filled up the mesh, we need to start a new line renderer if (!currentLineRenderer.PrepareForLines(lineCount)) { if (currentLineRenderer.CustomTransform != null) { // can't create multiple meshes if using a custom transform callback return; } if (dependencies.ThreadState.multiThreaded) { // we need to block until this action is run, Unity objects can only be modified and created on the main thread dependencies.ThreadState.AddActionForMainThread((inDestroy) => { if (!inDestroy) { EnableCurrentLineRenderer(); currentLineRenderer = GetOrCreateLineRenderer(); } }, true); } else { EnableCurrentLineRenderer(); currentLineRenderer = GetOrCreateLineRenderer(); } } currentLineRenderer.BeginLine(group.Segments[group.StartIndex].Start, group.Segments[group.StartIndex].End, radius, group.Color, p.Intensity, fadeLifeTime, p.GlowWidthMultiplier, p.GlowIntensity); for (int i = group.StartIndex + 1; i < group.Segments.Count; i++) { radius -= radiusStep; if (p.GrowthMultiplier < 1.0f) { timeOffset += timeStep; fadeLifeTime = new Vector4(timeStart + timeOffset, timeStart + group.PeakStart + timeOffset, timeStart + group.PeakEnd, timeStart + group.LifeTime); } currentLineRenderer.AppendLine(group.Segments[i].Start, group.Segments[i].End, radius, group.Color, p.Intensity, fadeLifeTime, p.GlowWidthMultiplier, p.GlowIntensity); } } private static IEnumerator NotifyBolt(LightningBoltDependencies dependencies, LightningBoltParameters p, Transform transform, Vector3 start, Vector3 end) { float delay = p.delaySeconds; float lifeTime = p.LifeTime; yield return WaitForSecondsLightning.WaitForSecondsLightningPooled(delay); if (dependencies.LightningBoltStarted != null) { dependencies.LightningBoltStarted(p, start, end); } LightningCustomTransformStateInfo state = (p.CustomTransform == null ? null : LightningCustomTransformStateInfo.GetOrCreateStateInfo()); if (state != null) { state.Parameters = p; state.BoltStartPosition = start; state.BoltEndPosition = end; state.State = LightningCustomTransformState.Started; state.Transform = transform; p.CustomTransform(state); state.State = LightningCustomTransformState.Executing; } if (p.CustomTransform == null) { yield return WaitForSecondsLightning.WaitForSecondsLightningPooled(lifeTime); } else { while (lifeTime > 0.0f) { p.CustomTransform(state); lifeTime -= LightningBoltScript.DeltaTime; yield return null; } } if (p.CustomTransform != null) { state.State = LightningCustomTransformState.Ended; p.CustomTransform(state); LightningCustomTransformStateInfo.ReturnStateInfoToCache(state); } if (dependencies.LightningBoltEnded != null) { dependencies.LightningBoltEnded(p, start, end); } LightningBoltParameters.ReturnParametersToCache(p); } private void ProcessParameters(LightningBoltParameters p, RangeOfFloats delay, LightningBoltDependencies depends) { Vector3 start, end; MinimumDelay = Mathf.Min(delay.Minimum, MinimumDelay); p.delaySeconds = delay.Random(p.Random); // apply LOD if specified if (depends.LevelOfDetailDistance > Mathf.Epsilon) { float d; if (p.Points.Count > 1) { d = Vector3.Distance(depends.CameraPos, p.Points[0]); d = Mathf.Min(Vector3.Distance(depends.CameraPos, p.Points[p.Points.Count - 1])); } else { d = Vector3.Distance(depends.CameraPos, p.Start); d = Mathf.Min(Vector3.Distance(depends.CameraPos, p.End)); } int modifier = Mathf.Min(8, (int)(d / depends.LevelOfDetailDistance)); p.Generations = Mathf.Max(1, p.Generations - modifier); p.GenerationWhereForksStopSubtractor = Mathf.Clamp(p.GenerationWhereForksStopSubtractor - modifier, 0, 8); } p.generationWhereForksStop = p.Generations - p.GenerationWhereForksStopSubtractor; lifeTime = Mathf.Max(p.LifeTime + p.delaySeconds, lifeTime); maxLifeTime = Mathf.Max(lifeTime, maxLifeTime); p.forkednessCalculated = (int)Mathf.Ceil(p.Forkedness * (float)p.Generations); if (p.Generations > 0) { p.Generator = p.Generator ?? LightningGenerator.GeneratorInstance; p.Generator.GenerateLightningBolt(this, p, out start, out end); p.Start = start; p.End = end; } } private void ProcessAllLightningParameters() { int maxLightsForEachParameters = MaximumLightsPerBatch / dependencies.Parameters.Count; RangeOfFloats delay = new RangeOfFloats(); List groupIndexes = new List(dependencies.Parameters.Count + 1); int i = 0; #if ENABLE_PROFILING System.Diagnostics.Stopwatch w = System.Diagnostics.Stopwatch.StartNew(); #endif foreach (LightningBoltParameters parameters in dependencies.Parameters) { delay.Minimum = parameters.DelayRange.Minimum + parameters.Delay; delay.Maximum = parameters.DelayRange.Maximum + parameters.Delay; parameters.maxLights = maxLightsForEachParameters; groupIndexes.Add(segmentGroups.Count); ProcessParameters(parameters, delay, dependencies); } groupIndexes.Add(segmentGroups.Count); #if ENABLE_PROFILING w.Stop(); UnityEngine.Debug.LogFormat("GENERATE: {0}", w.Elapsed.TotalMilliseconds); w.Reset(); w.Start(); #endif LightningBoltDependencies dependenciesRef = dependencies; foreach (LightningBoltParameters parameters in dependenciesRef.Parameters) { Transform transform = RenderLightningBolt(parameters.quality, parameters.Generations, groupIndexes[i], groupIndexes[++i], parameters); if (dependenciesRef.ThreadState.multiThreaded) { dependenciesRef.ThreadState.AddActionForMainThread((inDestroy) => { if (!inDestroy) { dependenciesRef.StartCoroutine(NotifyBolt(dependenciesRef, parameters, transform, parameters.Start, parameters.End)); } }, false); } else { dependenciesRef.StartCoroutine(NotifyBolt(dependenciesRef, parameters, transform, parameters.Start, parameters.End)); } } #if ENABLE_PROFILING w.Stop(); UnityEngine.Debug.LogFormat("RENDER: {0}", w.Elapsed.TotalMilliseconds); #endif if (dependencies.ThreadState.multiThreaded) { dependencies.ThreadState.AddActionForMainThread(EnableCurrentLineRendererFromThread); } else { EnableCurrentLineRenderer(); dependencies.AddActiveBolt(this); } } private void EnableCurrentLineRendererFromThread(bool inDestroy) { //try //{ if (inDestroy) { return; } EnableCurrentLineRenderer(); dependencies.AddActiveBolt(this); //} //finally //{ // clear the thread state, we verify in the Cleanup method that this is nulled out to ensure we are not cleaning up lightning that is still being generated //dependencies.ThreadState = null; //} } private void EnableCurrentLineRenderer() { if (activeLineRenderers.Count == 0) { return; } // make sure the last renderer gets enabled at the appropriate time else if (MinimumDelay <= 0.0f) { EnableLineRenderer(activeLineRenderers[activeLineRenderers.Count - 1], activeLineRenderers[activeLineRenderers.Count - 1].Tag); } else { dependencies.StartCoroutine(EnableLastRendererCoRoutine()); } } private void RenderParticleSystems(Vector3 start, Vector3 end, float trunkWidth, float lifeTime, float delaySeconds) { // only emit particle systems if we have a trunk - example, cloud lightning should not emit particles if (trunkWidth > 0.0f) { if (dependencies.OriginParticleSystem != null) { // we have a strike, create a particle where the lightning is coming from dependencies.StartCoroutine(GenerateParticleCoRoutine(dependencies.OriginParticleSystem, start, delaySeconds)); } if (dependencies.DestParticleSystem != null) { dependencies.StartCoroutine(GenerateParticleCoRoutine(dependencies.DestParticleSystem, end, delaySeconds + (lifeTime * 0.8f))); } } } private Transform RenderLightningBolt(LightningBoltQualitySetting quality, int generations, int startGroupIndex, int endGroupIndex, LightningBoltParameters parameters) { if (segmentGroups.Count == 0 || startGroupIndex >= segmentGroups.Count || endGroupIndex > segmentGroups.Count) { return null; } Transform transform = null; LightningLightParameters lp = parameters.LightParameters; if (lp != null) { if ((hasLight |= lp.HasLight)) { lp.LightPercent = Mathf.Clamp(lp.LightPercent, Mathf.Epsilon, 1.0f); lp.LightShadowPercent = Mathf.Clamp(lp.LightShadowPercent, 0.0f, 1.0f); } else { lp = null; } } LightningBoltSegmentGroup mainTrunkGroup = segmentGroups[startGroupIndex]; Vector3 start = mainTrunkGroup.Segments[mainTrunkGroup.StartIndex].Start; Vector3 end = mainTrunkGroup.Segments[mainTrunkGroup.StartIndex + mainTrunkGroup.SegmentCount - 1].End; parameters.FadePercent = Mathf.Clamp(parameters.FadePercent, 0.0f, 0.5f); // create a new line renderer mesh right now if we have a custom transform if (parameters.CustomTransform != null) { LineRendererMesh currentLineRenderer = (activeLineRenderers.Count == 0 || !activeLineRenderers[activeLineRenderers.Count - 1].Empty ? null : activeLineRenderers[activeLineRenderers.Count - 1]); if (currentLineRenderer == null) { if (dependencies.ThreadState.multiThreaded) { // we need to block until this action is run, Unity objects can only be modified and created on the main thread dependencies.ThreadState.AddActionForMainThread((inDestroy) => { if (!inDestroy) { EnableCurrentLineRenderer(); currentLineRenderer = GetOrCreateLineRenderer(); } }, true); } else { EnableCurrentLineRenderer(); currentLineRenderer = GetOrCreateLineRenderer(); } } if (currentLineRenderer == null) { return null; } currentLineRenderer.CustomTransform = parameters.CustomTransform; transform = currentLineRenderer.Transform; } for (int i = startGroupIndex; i < endGroupIndex; i++) { LightningBoltSegmentGroup group = segmentGroups[i]; group.Delay = parameters.delaySeconds; group.LifeTime = parameters.LifeTime; group.PeakStart = group.LifeTime * parameters.FadePercent; group.PeakEnd = group.LifeTime - group.PeakStart; float peakGap = group.PeakEnd - group.PeakStart; float fadeOut = group.LifeTime - group.PeakEnd; group.PeakStart *= parameters.FadeInMultiplier; group.PeakEnd = group.PeakStart + (peakGap * parameters.FadeFullyLitMultiplier); group.LifeTime = group.PeakEnd + (fadeOut * parameters.FadeOutMultiplier); group.LightParameters = lp; RenderGroup(group, parameters); } if (dependencies.ThreadState.multiThreaded) { dependencies.ThreadState.AddActionForMainThread((inDestroy) => { if (!inDestroy) { RenderParticleSystems(start, end, parameters.TrunkWidth, parameters.LifeTime, parameters.delaySeconds); // create lights only on the main trunk if (lp != null) { CreateLightsForGroup(segmentGroups[startGroupIndex], lp, quality, parameters.maxLights); } } }); } else { RenderParticleSystems(start, end, parameters.TrunkWidth, parameters.LifeTime, parameters.delaySeconds); // create lights only on the main trunk if (lp != null) { CreateLightsForGroup(segmentGroups[startGroupIndex], lp, quality, parameters.maxLights); } } return transform; } private void CreateLightsForGroup(LightningBoltSegmentGroup group, LightningLightParameters lp, LightningBoltQualitySetting quality, int maxLights) { if (lightCount == MaximumLightCount || maxLights <= 0) { return; } float fadeOutTime = (lifeTime - group.PeakEnd) * lp.FadeOutMultiplier; float peakGap = (group.PeakEnd - group.PeakStart) * lp.FadeFullyLitMultiplier; float peakStart = group.PeakStart * lp.FadeInMultiplier; float peakEnd = peakStart + peakGap; float maxLifeWithLights = peakEnd + fadeOutTime; maxLifeTime = Mathf.Max(maxLifeTime, group.Delay + maxLifeWithLights); segmentGroupsWithLight.Add(group); int segmentCount = group.SegmentCount; float lightPercent, lightShadowPercent; if (quality == LightningBoltQualitySetting.LimitToQualitySetting) { int level = QualitySettings.GetQualityLevel(); LightningQualityMaximum maximum; if (LightningBoltParameters.QualityMaximums.TryGetValue(level, out maximum)) { lightPercent = Mathf.Min(lp.LightPercent, maximum.MaximumLightPercent); lightShadowPercent = Mathf.Min(lp.LightShadowPercent, maximum.MaximumShadowPercent); } else { Debug.LogError("Unable to read lightning quality for level " + level.ToString()); lightPercent = lp.LightPercent; lightShadowPercent = lp.LightShadowPercent; } } else { lightPercent = lp.LightPercent; lightShadowPercent = lp.LightShadowPercent; } maxLights = Mathf.Max(1, Mathf.Min(maxLights, (int)(segmentCount * lightPercent))); int nthLight = Mathf.Max(1, (int)((segmentCount / maxLights))); int nthShadows = maxLights - (int)((float)maxLights * lightShadowPercent); int nthShadowCounter = nthShadows; // add lights evenly spaced for (int i = group.StartIndex + (int)(nthLight * 0.5f); i < group.Segments.Count; i += nthLight) { if (AddLightToGroup(group, lp, i, nthLight, nthShadows, ref maxLights, ref nthShadowCounter)) { return; } } // Debug.Log("Lightning light count: " + lightCount.ToString()); } private bool AddLightToGroup(LightningBoltSegmentGroup group, LightningLightParameters lp, int segmentIndex, int nthLight, int nthShadows, ref int maxLights, ref int nthShadowCounter) { Light light = GetOrCreateLight(lp); group.Lights.Add(light); Vector3 pos = (group.Segments[segmentIndex].Start + group.Segments[segmentIndex].End) * 0.5f; if (dependencies.CameraIsOrthographic) { if (dependencies.CameraMode == CameraMode.OrthographicXZ) { pos.y = dependencies.CameraPos.y + lp.OrthographicOffset; } else { pos.z = dependencies.CameraPos.z + lp.OrthographicOffset; } } if (dependencies.UseWorldSpace) { light.gameObject.transform.position = pos; } else { light.gameObject.transform.localPosition = pos; } if (lp.LightShadowPercent == 0.0f || ++nthShadowCounter < nthShadows) { light.shadows = LightShadows.None; } else { light.shadows = LightShadows.Soft; nthShadowCounter = 0; } // return true if no more lights possible, false otherwise return (++lightCount == MaximumLightCount || --maxLights == 0); } private Light GetOrCreateLight(LightningLightParameters lp) { Light light; while (true) { if (lightCache.Count == 0) { GameObject lightningLightObject = new GameObject("LightningBoltLight"); #if UNITY_EDITOR lightningLightObject.hideFlags = HideFlags.HideInInspector | HideFlags.HideInHierarchy; #endif light = lightningLightObject.AddComponent(); light.type = LightType.Point; break; } else { light = lightCache[lightCache.Count - 1]; lightCache.RemoveAt(lightCache.Count - 1); if (light == null) { // may have been disposed or the level re-loaded continue; } break; } } light.bounceIntensity = lp.BounceIntensity; light.shadowNormalBias = lp.ShadowNormalBias; light.color = lp.LightColor; light.renderMode = lp.RenderMode; light.range = lp.LightRange; light.shadowStrength = lp.ShadowStrength; light.shadowBias = lp.ShadowBias; light.intensity = 0.0f; light.gameObject.transform.parent = dependencies.Parent.transform; light.gameObject.SetActive(true); dependencies.LightAdded(light); return light; } private void UpdateLight(LightningLightParameters lp, IEnumerable lights, float delay, float peakStart, float peakEnd, float lifeTime) { if (elapsedTime < delay) { return; } // depending on whether we have hit the mid point of our lifetime, fade the light in or out // adjust lights for fade parameters float fadeOutTime = (lifeTime - peakEnd) * lp.FadeOutMultiplier; float peakGap = (peakEnd - peakStart) * lp.FadeFullyLitMultiplier; peakStart *= lp.FadeInMultiplier; peakEnd = peakStart + peakGap; lifeTime = peakEnd + fadeOutTime; float realElapsedTime = elapsedTime - delay; if (realElapsedTime >= peakStart) { if (realElapsedTime <= peakEnd) { // fully lit foreach (Light l in lights) { l.intensity = lp.LightIntensity * lp.LightMultiplier; } } else { // fading out float lerp = (realElapsedTime - peakEnd) / (lifeTime - peakEnd); foreach (Light l in lights) { l.intensity = Mathf.Lerp(lp.LightIntensity * lp.LightMultiplier, 0.0f, lerp); } } } else { // fading in float lerp = realElapsedTime / peakStart; foreach (Light l in lights) { l.intensity = Mathf.Lerp(0.0f, lp.LightIntensity * lp.LightMultiplier, lerp); } } } private void UpdateLights() { foreach (LightningBoltSegmentGroup group in segmentGroupsWithLight) { UpdateLight(group.LightParameters, group.Lights, group.Delay, group.PeakStart, group.PeakEnd, group.LifeTime); } } private IEnumerator GenerateParticleCoRoutine(ParticleSystem p, Vector3 pos, float delay) { yield return WaitForSecondsLightning.WaitForSecondsLightningPooled(delay); p.transform.position = pos; int count; if (p.emission.burstCount > 0) { ParticleSystem.Burst[] bursts = new ParticleSystem.Burst[p.emission.burstCount]; p.emission.GetBursts(bursts); count = UnityEngine.Random.Range(bursts[0].minCount, bursts[0].maxCount + 1); p.Emit(count); } else { ParticleSystem.MinMaxCurve rate = p.emission.rateOverTime; count = (int)((rate.constantMax - rate.constantMin) * 0.5f); count = UnityEngine.Random.Range(count, count * 2); p.Emit(count); } } private void CheckForGlow(IEnumerable parameters) { // we need to know if there is glow so we can choose the glow or non-glow setting in the renderer foreach (LightningBoltParameters p in parameters) { HasGlow = (p.GlowIntensity >= Mathf.Epsilon && p.GlowWidthMultiplier >= Mathf.Epsilon); if (HasGlow) { break; } } } #endregion Private methods } #if UNITY_WEBGL public class LightningThreadState { internal readonly int mainThreadId = 1; internal readonly bool multiThreaded; /// /// Running? /// public bool Running { get; set; } /// /// Constructor - starts the thread /// /// Multi-threaded? public LightningThreadState(bool multiThreaded) { this.multiThreaded = false; } /// /// Add a main thread action /// /// Action /// True to wait for completion, false if not /// True if action added, false if in process of terminating the thread public bool AddActionForMainThread(System.Action action, bool waitForAction = false) { action(false); return true; } /// /// Terminate and wait for thread end /// /// True if in destroy, false otherwise public void TerminateAndWaitForEnd(bool inDestroy) { } /// /// Add a background thread action /// /// Action /// True if action added, false if in process of terminating the thread public bool AddActionForBackgroundThread(System.Action action) { action(); return true; } public void UpdateMainThreadActions() { } } #else /// /// Lightning threading state /// public class LightningThreadState { private const int maxTimeoutWaitMainThread = 30000; // needs to be thread safe private static readonly BlockingCollection autoResetEventPool = new BlockingCollection(); internal readonly int mainThreadId; internal readonly bool multiThreaded; #if TASK_AVAILABLE private Task lightningThread; #else /// /// Lightning thread /// private Thread lightningThread; #endif /// /// List of background actions /// private readonly BlockingCollection actionsForBackgroundThread = new BlockingCollection(new ConcurrentQueue()); /// /// List of main thread actions and optional events to signal /// private readonly BlockingCollection<(System.Action action, AutoResetEvent evt)> actionsForMainThread = new BlockingCollection<(System.Action, AutoResetEvent)>(new ConcurrentQueue<(System.Action, AutoResetEvent)>()); /// /// Set to false to terminate /// public bool Running = true; private bool isTerminating; private bool UpdateMainThreadActionsOnce(bool inDestroy) { if (!actionsForMainThread.TryTake(out (System.Action action, AutoResetEvent evt) item)) { return false; } try { item.action(inDestroy); } catch (Exception ex) { Debug.LogError("Error in main thread lightning action: " + ex); } if (item.evt != null) { item.evt.Set(); autoResetEventPool.Add(item.evt); } return true; } private void BackgroundThreadMethod() { while (Running) { if (actionsForBackgroundThread.TryTake(out Action action, 500)) { try { action(); } catch (Exception ex) { actionsForMainThread.Add((inDestroy => { Debug.LogError("Lightning background thread exception: " + ex); }, null)); } } } } /// /// Constructor - starts the thread /// /// Multi-threaded? public LightningThreadState(bool multiThreaded) { this.mainThreadId = System.Threading.Thread.CurrentThread.ManagedThreadId; this.multiThreaded = multiThreaded; #if TASK_AVAILABLE lightningThread = Task.Factory.StartNew(BackgroundThreadMethod); #else lightningThread = new Thread(new ThreadStart(BackgroundThreadMethod)) { IsBackground = true, Name = "LightningBoltScriptThread" }; lightningThread.Start(); #endif } /// /// Terminate and wait for thread end /// /// True if in destroy, false otherwise public void TerminateAndWaitForEnd(bool inDestroy) { DateTime dt = DateTime.UtcNow; TimeSpan timeout = TimeSpan.FromSeconds(5.0); isTerminating = true; while (UpdateMainThreadActionsOnce(inDestroy) || actionsForBackgroundThread.Count > 0) { Thread.Sleep(20); if (DateTime.UtcNow - dt > timeout) { break; } } } /// /// Execute any main thread actions from the main thread /// public void UpdateMainThreadActions() { if (multiThreaded) { while (UpdateMainThreadActionsOnce(false)) { } } } /// /// Add a main thread action /// /// Action /// True to wait for completion, false if not /// True if action added, false if in process of terminating the thread public bool AddActionForMainThread(System.Action action, bool waitForAction = false) { if (isTerminating) { return false; } else if (System.Threading.Thread.CurrentThread.ManagedThreadId == mainThreadId || !multiThreaded) { action(true); return true; } if (waitForAction) { if (!autoResetEventPool.TryTake(out AutoResetEvent evt)) { evt = new AutoResetEvent(false); } actionsForMainThread.Add((action, evt)); evt.WaitOne(maxTimeoutWaitMainThread); } else { actionsForMainThread.Add((action, null)); } return true; } /// /// Add a background thread action /// /// Action /// True if action added, false if in process of terminating the thread public bool AddActionForBackgroundThread(System.Action action) { if (isTerminating) { return false; } else if (!multiThreaded) { action(); } else { actionsForBackgroundThread.Add(action); } return true; } } #endif }