// // 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. // using System; using System.Collections.Generic; using System.Linq; using System.Text; using UnityEngine; namespace DigitalRuby.ThunderAndLightning { /// /// Lightning generator base class /// public class LightningGenerator { internal const float oneOver255 = 1.0f / 255.0f; internal const float mainTrunkMultiplier = 255.0f * oneOver255 * oneOver255; private void GetPerpendicularVector(ref Vector3 directionNormalized, out Vector3 side) { if (directionNormalized == Vector3.zero) { side = Vector3.right; } else { // use cross product to find any perpendicular vector around directionNormalized: // 0 = x * px + y * py + z * pz // => pz = -(x * px + y * py) / z // for computational stability use the component farthest from 0 to divide by float x = directionNormalized.x; float y = directionNormalized.y; float z = directionNormalized.z; float px, py, pz; float ax = Mathf.Abs(x), ay = Mathf.Abs(y), az = Mathf.Abs(z); if (ax >= ay && ay >= az) { // x is the max, so we can pick (py, pz) arbitrarily at (1, 1): py = 1.0f; pz = 1.0f; px = -(y * py + z * pz) / x; } else if (ay >= az) { // y is the max, so we can pick (px, pz) arbitrarily at (1, 1): px = 1.0f; pz = 1.0f; py = -(x * px + z * pz) / y; } else { // z is the max, so we can pick (px, py) arbitrarily at (1, 1): px = 1.0f; py = 1.0f; pz = -(x * px + y * py) / z; } side = new Vector3(px, py, pz).normalized; } } /// /// Fires when a lightning bolt needs to be generated /// /// Lightning bolt /// Start position /// End position /// Parameters protected virtual void OnGenerateLightningBolt(LightningBolt bolt, Vector3 start, Vector3 end, LightningBoltParameters parameters) { GenerateLightningBoltStandard(bolt, start, end, parameters.Generations, parameters.Generations, 0.0f, parameters); } /// /// Determines if a fork should be created /// /// Parameters /// Generation /// Max generation /// True if fork should be created, false otherwise public bool ShouldCreateFork(LightningBoltParameters parameters, int generation, int totalGenerations) { return (generation > parameters.generationWhereForksStop && generation >= totalGenerations - parameters.forkednessCalculated && (float)parameters.Random.NextDouble() < parameters.Forkedness); } /// /// Create a fork in a lightning bolt /// /// Lightning bolt /// Parameters /// Generation /// Max generation /// Start position /// Middle position public void CreateFork(LightningBolt bolt, LightningBoltParameters parameters, int generation, int totalGenerations, Vector3 start, Vector3 midPoint) { if (ShouldCreateFork(parameters, generation, totalGenerations)) { Vector3 branchVector = (midPoint - start) * parameters.ForkMultiplier(); Vector3 splitEnd = midPoint + branchVector; GenerateLightningBoltStandard(bolt, midPoint, splitEnd, generation, totalGenerations, 0.0f, parameters); } } /// /// Generate a normal/standard lightning bolt /// /// Lightning bolt /// Start position /// End position /// Generation /// Max generation /// Offset amount for variance /// Parameters public void GenerateLightningBoltStandard(LightningBolt bolt, Vector3 start, Vector3 end, int generation, int totalGenerations, float offsetAmount, LightningBoltParameters parameters) { if (generation < 1) { return; } LightningBoltSegmentGroup group = bolt.AddGroup(); group.Segments.Add(new LightningBoltSegment { Start = start, End = end }); // every generation, get the percentage we have gone down and square it, this makes lines thinner float widthMultiplier = (float)generation / (float)totalGenerations; widthMultiplier *= widthMultiplier; Vector3 randomVector; group.LineWidth = parameters.TrunkWidth * widthMultiplier; group.Generation = generation; group.Color = parameters.Color; if (generation == parameters.Generations && (parameters.MainTrunkTintColor.r != 255 || parameters.MainTrunkTintColor.g != 255 || parameters.MainTrunkTintColor.b != 255 || parameters.MainTrunkTintColor.a != 255)) { group.Color.r = (byte)(mainTrunkMultiplier * (float)group.Color.r * (float)parameters.MainTrunkTintColor.r); group.Color.g = (byte)(mainTrunkMultiplier * (float)group.Color.g * (float)parameters.MainTrunkTintColor.g); group.Color.b = (byte)(mainTrunkMultiplier * (float)group.Color.b * (float)parameters.MainTrunkTintColor.b); group.Color.a = (byte)(mainTrunkMultiplier * (float)group.Color.a * (float)parameters.MainTrunkTintColor.a); } group.Color.a = (byte)(255.0f * widthMultiplier); group.EndWidthMultiplier = parameters.EndWidthMultiplier * parameters.ForkEndWidthMultiplier; if (offsetAmount <= 0.0f) { offsetAmount = (end - start).magnitude * (generation == totalGenerations ? parameters.ChaosFactor : parameters.ChaosFactorForks); } while (generation-- > 0) { int previousStartIndex = group.StartIndex; group.StartIndex = group.Segments.Count; for (int i = previousStartIndex; i < group.StartIndex; i++) { start = group.Segments[i].Start; end = group.Segments[i].End; // determine a new direction for the split Vector3 midPoint = (start + end) * 0.5f; // adjust the mid point to be the new location RandomVector(bolt, ref start, ref end, offsetAmount, parameters.Random, out randomVector); midPoint += randomVector; // add two new segments group.Segments.Add(new LightningBoltSegment { Start = start, End = midPoint }); group.Segments.Add(new LightningBoltSegment { Start = midPoint, End = end }); CreateFork(bolt, parameters, generation, totalGenerations, start, midPoint); } // halve the distance the lightning can deviate for each generation down offsetAmount *= 0.5f; } } /// /// Get a random 3D direction /// /// Random /// Random 3D direction vector public Vector3 RandomDirection3D(System.Random random) { float z = (2.0f * (float)random.NextDouble()) - 1.0f; // z is in the range [-1,1] Vector3 planar = RandomDirection2D(random) * Mathf.Sqrt(1.0f - (z * z)); planar.z = z; return planar; } /// /// Get random 2D direction in XY plane /// /// Random /// Random 2D direction public Vector3 RandomDirection2D(System.Random random) { float azimuth = (float)random.NextDouble() * 2.0f * Mathf.PI; return new Vector3(Mathf.Cos(azimuth), Mathf.Sin(azimuth), 0.0f); } /// /// Get random 2D direction in XZ plane /// /// Random /// Random 2D direction public Vector3 RandomDirection2DXZ(System.Random random) { float azimuth = (float)random.NextDouble() * 2.0f * Mathf.PI; return new Vector3(Mathf.Cos(azimuth), 0.0f, Mathf.Sin(azimuth)); } /// /// Generate a random vector /// /// Lightning bolt /// Start position /// End position /// Offset amount for variance /// Random instance /// Receives random vector public void RandomVector(LightningBolt bolt, ref Vector3 start, ref Vector3 end, float offsetAmount, System.Random random, out Vector3 result) { if (bolt.CameraMode == CameraMode.Perspective) { Vector3 direction = (end - start).normalized; Vector3 side = Vector3.Cross(start, end); if (side == Vector3.zero) { // slow path, rarely hit unless cross product is zero GetPerpendicularVector(ref direction, out side); } else { side.Normalize(); } // generate random distance and angle float distance = (((float)random.NextDouble() + 0.1f) * offsetAmount); #if DEBUG float rotationAngle = ((float)random.NextDouble() * 360.0f); result = Quaternion.AngleAxis(rotationAngle, direction) * side * distance; #else // optimized path for RELEASE mode, skips two normalize and two multiplies in Quaternion.AngleAxis float rotationAngle = ((float)random.NextDouble() * Mathf.PI); direction *= (float)System.Math.Sin(rotationAngle); Quaternion rotation; rotation.x = direction.x; rotation.y = direction.y; rotation.z = direction.z; rotation.w = (float)System.Math.Cos(rotationAngle); result = rotation * side * distance; #endif } else if (bolt.CameraMode == CameraMode.OrthographicXY) { // XY plane end.z = start.z; Vector3 directionNormalized = (end - start).normalized; Vector3 side = new Vector3(-directionNormalized.y, directionNormalized.x, 0.0f); float distance = ((float)random.NextDouble() * offsetAmount * 2.0f) - offsetAmount; result = side * distance; } else { // XZ plane end.y = start.y; Vector3 directionNormalized = (end - start).normalized; Vector3 side = new Vector3(-directionNormalized.z, 0.0f, directionNormalized.x); float distance = ((float)random.NextDouble() * offsetAmount * 2.0f) - offsetAmount; result = side * distance; } } /// /// Generate a lightning bolt /// /// Lightning bolt /// Parameters public void GenerateLightningBolt(LightningBolt bolt, LightningBoltParameters parameters) { Vector3 start, end; GenerateLightningBolt(bolt, parameters, out start, out end); } /// /// Generate a lightning bolt /// /// Lightning bolt /// Parameters /// Start position /// End position public void GenerateLightningBolt(LightningBolt bolt, LightningBoltParameters parameters, out Vector3 start, out Vector3 end) { start = parameters.ApplyVariance(parameters.Start, parameters.StartVariance); end = parameters.ApplyVariance(parameters.End, parameters.EndVariance); OnGenerateLightningBolt(bolt, start, end, parameters); } /// /// Singleton lightning generator instance /// public static readonly LightningGenerator GeneratorInstance = new LightningGenerator(); } /// /// Generates lightning that follows a path /// public class LightningGeneratorPath : LightningGenerator { /// /// Singleton path generator /// public static readonly LightningGeneratorPath PathGeneratorInstance = new LightningGeneratorPath(); /// /// Generate lightning bolt path /// /// Lightning bolt /// Start /// End /// Parameters public void GenerateLightningBoltPath(LightningBolt bolt, Vector3 start, Vector3 end, LightningBoltParameters parameters) { if (parameters.Points.Count < 2) { Debug.LogError("Lightning path should have at least two points"); return; } int generation = parameters.Generations; int totalGenerations = generation; float offsetAmount, d; float chaosFactor = (generation == parameters.Generations ? parameters.ChaosFactor : parameters.ChaosFactorForks); int smoothingFactor = parameters.SmoothingFactor - 1; Vector3 distance, randomVector; LightningBoltSegmentGroup group = bolt.AddGroup(); group.LineWidth = parameters.TrunkWidth; group.Generation = generation--; group.EndWidthMultiplier = parameters.EndWidthMultiplier; group.Color = parameters.Color; if (generation == parameters.Generations && (parameters.MainTrunkTintColor.r != 255 || parameters.MainTrunkTintColor.g != 255 || parameters.MainTrunkTintColor.b != 255 || parameters.MainTrunkTintColor.a != 255)) { group.Color.r = (byte)(mainTrunkMultiplier * (float)group.Color.r * (float)parameters.MainTrunkTintColor.r); group.Color.g = (byte)(mainTrunkMultiplier * (float)group.Color.g * (float)parameters.MainTrunkTintColor.g); group.Color.b = (byte)(mainTrunkMultiplier * (float)group.Color.b * (float)parameters.MainTrunkTintColor.b); group.Color.a = (byte)(mainTrunkMultiplier * (float)group.Color.a * (float)parameters.MainTrunkTintColor.a); } parameters.Start = parameters.Points[0] + start; parameters.End = parameters.Points[parameters.Points.Count - 1] + end; end = parameters.Start; for (int i = 1; i < parameters.Points.Count; i++) { start = end; end = parameters.Points[i]; distance = (end - start); d = PathGenerator.SquareRoot(distance.sqrMagnitude); if (chaosFactor > 0.0f) { if (bolt.CameraMode == CameraMode.Perspective) { end += (d * chaosFactor * RandomDirection3D(parameters.Random)); } else if (bolt.CameraMode == CameraMode.OrthographicXY) { end += (d * chaosFactor * RandomDirection2D(parameters.Random)); } else { end += (d * chaosFactor * RandomDirection2DXZ(parameters.Random)); } distance = (end - start); } group.Segments.Add(new LightningBoltSegment { Start = start, End = end }); offsetAmount = d * chaosFactor; RandomVector(bolt, ref start, ref end, offsetAmount, parameters.Random, out randomVector); if (ShouldCreateFork(parameters, generation, totalGenerations)) { Vector3 branchVector = distance * parameters.ForkMultiplier() * smoothingFactor * 0.5f; Vector3 forkEnd = end + branchVector + randomVector; GenerateLightningBoltStandard(bolt, start, forkEnd, generation, totalGenerations, 0.0f, parameters); } if (--smoothingFactor == 0) { smoothingFactor = parameters.SmoothingFactor - 1; } } } /// /// Fires when lightning bolt needs to be generated /// /// Lightning bolt /// Start position /// End position /// Parameters protected override void OnGenerateLightningBolt(LightningBolt bolt, Vector3 start, Vector3 end, LightningBoltParameters parameters) { GenerateLightningBoltPath(bolt, start, end, parameters); } } }