//
// 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 UnityEngine;
using System.Collections;
namespace DigitalRuby.ThunderAndLightning
{
///
/// Thunder and lightning script for random lightning bolts
///
public class ThunderAndLightningScript : MonoBehaviour
{
private class LightningBoltHandler
{
public float VolumeMultiplier { get; set; }
private ThunderAndLightningScript script;
private readonly System.Random random = new System.Random();
public LightningBoltHandler(ThunderAndLightningScript script)
{
this.script = script;
CalculateNextLightningTime();
}
private void UpdateLighting()
{
if (script.lightningInProgress)
{
return;
}
if (script.ModifySkyboxExposure)
{
script.skyboxExposureStorm = 0.35f;
if (script.skyboxMaterial != null && script.skyboxMaterial.HasProperty("_Exposure"))
{
script.skyboxMaterial.SetFloat("_Exposure", script.skyboxExposureStorm);
}
}
CheckForLightning();
}
private void CalculateNextLightningTime()
{
script.nextLightningTime = DigitalRuby.ThunderAndLightning.LightningBoltScript.TimeSinceStart + script.LightningIntervalTimeRange.Random(random);
script.lightningInProgress = false;
if (script.ModifySkyboxExposure && script.skyboxMaterial.HasProperty("_Exposure"))
{
script.skyboxMaterial.SetFloat("_Exposure", script.skyboxExposureStorm);
}
}
public IEnumerator ProcessLightning(Vector3? _start, Vector3? _end, bool intense, bool visible)
{
float sleepTime;
AudioClip[] sounds;
float intensity;
script.lightningInProgress = true;
if (intense)
{
float percent = UnityEngine.Random.Range(0.0f, 1.0f);
intensity = Mathf.Lerp(2.0f, 8.0f, percent);
sleepTime = 5.0f / intensity;
sounds = script.ThunderSoundsIntense;
}
else
{
float percent = UnityEngine.Random.Range(0.0f, 1.0f);
intensity = Mathf.Lerp(0.0f, 2.0f, percent);
sleepTime = 30.0f / intensity;
sounds = script.ThunderSoundsNormal;
}
if (script.skyboxMaterial != null && script.ModifySkyboxExposure)
{
script.skyboxMaterial.SetFloat("_Exposure", Mathf.Max(intensity * 0.5f, script.skyboxExposureStorm));
}
// perform the strike
Strike(_start, _end, intense, intensity, script.Camera, visible ? script.Camera : null);
// calculate the next lightning strike
CalculateNextLightningTime();
// thunder will play depending on intensity of lightning
bool playThunder = (intensity >= 1.0f);
//Debug.Log("Lightning intensity: " + intensity.ToString("0.00") + ", thunder delay: " +
// (playThunder ? sleepTime.ToString("0.00") : "No Thunder"));
if (playThunder && sounds != null && sounds.Length > 0)
{
// wait for a bit then play a thunder sound
yield return WaitForSecondsLightning.WaitForSecondsLightningPooled(sleepTime);
AudioClip clip = null;
do
{
// pick a random thunder sound that wasn't the same as the last sound, unless there is only one sound, then we have no choice
clip = sounds[UnityEngine.Random.Range(0, sounds.Length - 1)];
}
while (sounds.Length > 1 && clip == script.lastThunderSound);
// set the last sound and play it
script.lastThunderSound = clip;
script.audioSourceThunder.PlayOneShot(clip, intensity * 0.5f * VolumeMultiplier);
}
}
private void Strike(Vector3? _start, Vector3? _end, bool intense, float intensity, Camera camera, Camera visibleInCamera)
{
// find a point around the camera that is not too close
const float minDistance = 500.0f;
float minValue = (intense ? -1000.0f : -5000.0f);
float maxValue = (intense ? 1000 : 5000.0f);
float closestValue = (intense ? 500.0f : 2500.0f);
float x, y, z;
Vector3 start, end;
bool areaBounded = (_start is null && _end is null && script.AreaOveride != null);
if (areaBounded)
{
var bounds = script.AreaOveride.bounds;
x = UnityEngine.Random.Range(bounds.min.x, bounds.max.x);
y = UnityEngine.Random.Range(bounds.min.y, bounds.max.y);
z = UnityEngine.Random.Range(bounds.min.z, bounds.max.z);
start = new Vector3(x, y, z);
}
else
{
x = (UnityEngine.Random.Range(0, 2) == 0 ? UnityEngine.Random.Range(minValue, -closestValue) : UnityEngine.Random.Range(closestValue, maxValue));
y = script.LightningYStart;
z = (UnityEngine.Random.Range(0, 2) == 0 ? UnityEngine.Random.Range(minValue, -closestValue) : UnityEngine.Random.Range(closestValue, maxValue));
start = script.Camera.transform.position;
start.x += x;
start.y = y;
start.z += z;
if (visibleInCamera != null)
{
// try and make sure the strike is visible in the camera
Quaternion q = visibleInCamera.transform.rotation;
visibleInCamera.transform.rotation = Quaternion.Euler(0.0f, q.eulerAngles.y, 0.0f);
float screenX = UnityEngine.Random.Range(visibleInCamera.pixelWidth * 0.1f, visibleInCamera.pixelWidth * 0.9f);
float ScreenZ = UnityEngine.Random.Range(visibleInCamera.nearClipPlane + closestValue + closestValue, maxValue);
Vector3 point = visibleInCamera.ScreenToWorldPoint(new Vector3(screenX, 0.0f, ScreenZ));
start = point;
start.y = y;
visibleInCamera.transform.rotation = q;
}
}
end = start;
x = UnityEngine.Random.Range(-100, 100.0f);
// 1 in 4 chance not to strike the ground
y = (UnityEngine.Random.Range(0, 4) == 0 ? UnityEngine.Random.Range(-1, 600.0f) : -1.0f);
z += UnityEngine.Random.Range(-100.0f, 100.0f);
end.x += x;
end.y = y;
end.z += z;
// make sure the bolt points away from the camera
end.x += (closestValue * camera.transform.forward.x);
end.z += (closestValue * camera.transform.forward.z);
while ((start - end).magnitude < minDistance)
{
end.x += (closestValue * camera.transform.forward.x);
end.z += (closestValue * camera.transform.forward.z);
}
start = (_start ?? start);
end = (_end ?? end);
// see if the bolt hit anything on it's way to the ground - if so, change the end point
RaycastHit hit;
if (Physics.Raycast(start, (start - end).normalized, out hit, float.MaxValue))
{
end = hit.point;
}
int generations = script.LightningBoltScript.Generations;
RangeOfFloats trunkWidth = script.LightningBoltScript.TrunkWidthRange;
if (UnityEngine.Random.value < script.CloudLightningChance)
{
// cloud only lightning
script.LightningBoltScript.TrunkWidthRange = new RangeOfFloats();
script.LightningBoltScript.Generations = 1;
}
script.LightningBoltScript.LightParameters.LightIntensity = intensity * 0.5f;
script.LightningBoltScript.Trigger(start, end);
script.LightningBoltScript.TrunkWidthRange = trunkWidth;
script.LightningBoltScript.Generations = generations;
}
private void CheckForLightning()
{
// time for another strike?
if (Time.timeSinceLevelLoad >= script.nextLightningTime)
{
bool intense = UnityEngine.Random.value < script.LightningIntenseProbability;
script.StartCoroutine(ProcessLightning(null, null, intense, script.LightningAlwaysVisible));
}
}
public void Update()
{
UpdateLighting();
}
}
/// Lightning bolt script - optional, leave null if you don't want lightning bolts
[Tooltip("Lightning bolt script - optional, leave null if you don't want lightning bolts")]
public LightningBoltPrefabScript LightningBoltScript;
/// Camera where the lightning should be centered over. Defaults to main camera.
[Tooltip("Camera where the lightning should be centered over. Defaults to main camera.")]
public Camera Camera;
/// Random interval between strikes.
[SingleLine("Random interval between strikes.")]
public RangeOfFloats LightningIntervalTimeRange = new RangeOfFloats { Minimum = 10.0f, Maximum = 25.0f };
/// Probability (0-1) of an intense lightning bolt that hits really close. Intense lightning has increased brightness and louder thunder compared to normal lightning, and the thunder sounds plays a lot sooner.
[Tooltip("Probability (0-1) of an intense lightning bolt that hits really close. Intense lightning has increased brightness and louder thunder compared to normal lightning, and the thunder sounds plays a lot sooner.")]
[Range(0.0f, 1.0f)]
public float LightningIntenseProbability = 0.2f;
/// Sounds to play for normal thunder. One will be chosen at random for each lightning strike. Depending on intensity, some normal lightning may not play a thunder sound.
[Tooltip("Sounds to play for normal thunder. One will be chosen at random for each lightning strike. Depending on intensity, some normal lightning may not play a thunder sound.")]
public AudioClip[] ThunderSoundsNormal;
/// Sounds to play for intense thunder. One will be chosen at random for each lightning strike.
[Tooltip("Sounds to play for intense thunder. One will be chosen at random for each lightning strike.")]
public AudioClip[] ThunderSoundsIntense;
/// Whether lightning strikes should always try to be in the camera view
[Tooltip("Whether lightning strikes should always try to be in the camera view")]
public bool LightningAlwaysVisible = true;
/// The chance lightning will simply be in the clouds with no visible bolt
[Tooltip("The chance lightning will simply be in the clouds with no visible bolt")]
[Range(0.0f, 1.0f)]
public float CloudLightningChance = 0.5f;
/// Whether to modify the skybox exposure when lightning is created
[Tooltip("Whether to modify the skybox exposure when lightning is created")]
public bool ModifySkyboxExposure = false;
/// Base point light range for lightning bolts. Increases as intensity increases.
[Tooltip("Base point light range for lightning bolts. Increases as intensity increases.")]
[Range(1, 10000)]
public float BaseLightRange = 2000.0f;
/// Starting y value for the lightning strikes
[Tooltip("Starting y value for the lightning strikes")]
[Range(0, 100000)]
public float LightningYStart = 500.0f;
/// Volume multiplier
[Tooltip("Volume multiplier")]
[Range(0.0f, 1.0f)]
public float VolumeMultiplier = 1.0f;
/// Override the lightning strike area, takes all dimensions into account
[Tooltip("Override the lightning strike area, takes all dimensions into account")]
public BoxCollider AreaOveride;
private float skyboxExposureOriginal;
private float skyboxExposureStorm;
private float nextLightningTime;
private bool lightningInProgress;
private AudioSource audioSourceThunder;
private LightningBoltHandler lightningBoltHandler;
private Material skyboxMaterial;
private AudioClip lastThunderSound;
private void Start()
{
EnableLightning = true;
if (Camera == null)
{
Camera = Camera.main;
}
#if DEBUG
if (Camera.farClipPlane < 10000.0f && !Camera.orthographic)
{
Debug.LogWarning("Far clip plane should be 10000+ for best lightning effects");
}
#endif
if (RenderSettings.skybox != null)
{
skyboxMaterial = RenderSettings.skybox = new Material(RenderSettings.skybox);
}
skyboxExposureOriginal = skyboxExposureStorm = (skyboxMaterial == null || !skyboxMaterial.HasProperty("_Exposure") ? 1.0f : skyboxMaterial.GetFloat("_Exposure"));
audioSourceThunder = gameObject.AddComponent();
lightningBoltHandler = new LightningBoltHandler(this);
lightningBoltHandler.VolumeMultiplier = VolumeMultiplier;
}
private void Update()
{
if (lightningBoltHandler != null && EnableLightning)
{
lightningBoltHandler.VolumeMultiplier = VolumeMultiplier;
lightningBoltHandler.Update();
}
}
///
/// Summon normal lighntning at a random position
///
public void CallNormalLightning()
{
CallNormalLightning(null, null);
}
///
/// Summon normal lightning
///
/// Start position
/// End position
public void CallNormalLightning(Vector3? start, Vector3? end)
{
StartCoroutine(lightningBoltHandler.ProcessLightning(start, end, false, true));
}
///
/// Summon intense lightning at random location
///
public void CallIntenseLightning()
{
CallIntenseLightning(null, null);
}
///
/// Summon intense lightning
///
/// Start position
/// End position
public void CallIntenseLightning(Vector3? start, Vector3? end)
{
StartCoroutine(lightningBoltHandler.ProcessLightning(start, end, true, true));
}
///
/// Skybox exposure original value
///
public float SkyboxExposureOriginal
{
get { return skyboxExposureOriginal; }
}
///
/// Whether to enable lightning
///
public bool EnableLightning { get; set; }
}
}