//------------------------------------------------------------------------------------------------------------------ // Volumetric Fog & Mist 2 // Created by Kronnect //------------------------------------------------------------------------------------------------------------------ using UnityEngine; using System.Collections.Generic; using UnityEngine.Serialization; namespace VolumetricFogAndMist2 { public enum MaskTextureBrushMode { AddFog = 0, RemoveFog = 1, ColorFog = 2 } public enum FoWUpdateMethod { MainThread, BackgroundThread } public partial class VolumetricFog : MonoBehaviour { public bool enableFogOfWar; public Vector3 fogOfWarCenter; public bool fogOfWarIsLocal; public Vector3 fogOfWarSize = new Vector3(1024, 0, 1024); public bool fogOfWarShowCoverage; [FormerlySerializedAs("fogOfWarTextureSize")] [Range(32, 2048)] public int fogOfWarTextureWidth = 256; [Range(32, 2048)] public int fogOfWarTextureHeight; [Tooltip("Delay before the fog alpha is restored. A value of 0 keeps the fog cleared forever.")] [Range(0, 100)] public float fogOfWarRestoreDelay; [Range(0, 25)] public float fogOfWarRestoreDuration = 2f; [Range(0, 1)] public float fogOfWarSmoothness = 1f; public bool fogOfWarBlur; const int MAX_SIMULTANEOUS_TRANSITIONS = 64000; bool canDestroyFOWTexture; float now; #region In-Editor fog of war painter public bool maskEditorEnabled; public MaskTextureBrushMode maskBrushMode = MaskTextureBrushMode.RemoveFog; public Color maskBrushColor = Color.white; [Range(1, 128)] public int maskBrushWidth = 20; [Range(0, 1)] public float maskBrushFuzziness = 0.5f; [Range(0, 1)] public float maskBrushOpacity = 0.15f; #endregion [SerializeField] Texture2D _fogOfWarTexture; public Vector3 anchoredFogOfWarCenter => fogOfWarIsLocal ? transform.position + fogOfWarCenter : fogOfWarCenter; public Texture2D fogOfWarTexture { get { return _fogOfWarTexture; } set { if (_fogOfWarTexture != value) { if (value != null) { _fogOfWarTexture = value; canDestroyFOWTexture = false; ReloadFogOfWarTexture(); } else { if (canDestroyFOWTexture && _fogOfWarTexture != null) { DestroyImmediate(_fogOfWarTexture); } _fogOfWarTexture = null; canDestroyFOWTexture = false; } if (fogMat != null) { fogMat.SetTexture(ShaderParams.FogOfWarTexture, _fogOfWarTexture); } } } } Color32[] fogOfWarColorBuffer; struct FogOfWarTransition { public bool enabled; public int x, y; public float startTime, startDelay; public float duration; public int initialAlpha; public int targetAlpha; public int restoreToAlpha; public float restoreDelay; public float restoreDuration; } FogOfWarTransition[] fowTransitionList; int lastTransitionPos; Dictionary fowTransitionIndices; Stack fowFreeIndices; bool requiresTextureUpload; bool backgroundThreadBusy; Material fowBlurMat; RenderTexture fowBlur1, fowBlur2; static readonly object _lock = new object(); void FogOfWarInit () { if (fogOfWarTextureHeight == 0) { fogOfWarTextureHeight = fogOfWarTextureWidth; } if (fowTransitionList == null || fowTransitionList.Length != MAX_SIMULTANEOUS_TRANSITIONS) { fowTransitionList = new FogOfWarTransition[MAX_SIMULTANEOUS_TRANSITIONS]; } if (fowFreeIndices == null) { fowFreeIndices = new Stack(MAX_SIMULTANEOUS_TRANSITIONS); } else { fowFreeIndices.Clear(); } for (int k = MAX_SIMULTANEOUS_TRANSITIONS - 1; k >= 0; k--) { fowFreeIndices.Push(k); } InitTransitions(); if (_fogOfWarTexture == null) { FogOfWarUpdateTexture(); } else if (enableFogOfWar && (fogOfWarColorBuffer == null || fogOfWarColorBuffer.Length == 0)) { ReloadFogOfWarTexture(); } backgroundThreadBusy = false; } void InitTransitions () { lock (_lock) { if (fowTransitionIndices == null) { fowTransitionIndices = new Dictionary(MAX_SIMULTANEOUS_TRANSITIONS); } else { fowTransitionIndices.Clear(); } lastTransitionPos = -1; } } void FogOfWarDestroy () { if (canDestroyFOWTexture && _fogOfWarTexture != null) { DestroyImmediate(_fogOfWarTexture); } if (fowBlur1 != null) { fowBlur1.Release(); } if (fowBlur2 != null) { fowBlur2.Release(); } if (fowBlurMat != null) { DestroyImmediate(fowBlurMat); } } /// /// Reloads the current contents of the fog of war texture /// public void ReloadFogOfWarTexture () { if (_fogOfWarTexture == null || profile == null) return; fogOfWarTextureWidth = _fogOfWarTexture.width; fogOfWarTextureHeight = _fogOfWarTexture.height; EnsureTextureIsReadable(_fogOfWarTexture); fogOfWarColorBuffer = _fogOfWarTexture.GetPixels32(); InitTransitions(); if (!enableFogOfWar) { enableFogOfWar = true; UpdateMaterialPropertiesNow(); } } void EnsureTextureIsReadable (Texture2D tex) { #if UNITY_EDITOR string path = UnityEditor.AssetDatabase.GetAssetPath(tex); if (string.IsNullOrEmpty(path)) return; UnityEditor.TextureImporter imp = UnityEditor.AssetImporter.GetAtPath(path) as UnityEditor.TextureImporter; if (imp != null && !imp.isReadable) { imp.isReadable = true; imp.SaveAndReimport(); } #endif } void FogOfWarUpdateTexture () { if (!enableFogOfWar || !Application.isPlaying) return; int width = GetScaledSize(fogOfWarTextureWidth, 1.0f); int height = GetScaledSize(fogOfWarTextureHeight, 1.0f); if (_fogOfWarTexture == null || _fogOfWarTexture.width != width || _fogOfWarTexture.height != height) { _fogOfWarTexture = new Texture2D(width, height, TextureFormat.RGBA32, false, true); _fogOfWarTexture.hideFlags = HideFlags.DontSave; _fogOfWarTexture.filterMode = FilterMode.Bilinear; _fogOfWarTexture.wrapMode = TextureWrapMode.Clamp; canDestroyFOWTexture = true; ResetFogOfWar(); } } int GetScaledSize (int size, float factor) { size = (int)(size / factor); size /= 4; if (size < 1) size = 1; return size * 4; } void UpdateFogOfWarMaterialBoundsProperties () { Vector3 fogOfWarCenter = anchoredFogOfWarCenter; fogMat.SetVector(ShaderParams.FogOfWarCenter, fogOfWarCenter); fogMat.SetVector(ShaderParams.FogOfWarSize, fogOfWarSize); Vector3 ca = fogOfWarCenter - 0.5f * fogOfWarSize; fogMat.SetVector(ShaderParams.FogOfWarCenterAdjusted, new Vector4(ca.x / fogOfWarSize.x, 1f, ca.z / (fogOfWarSize.z + 0.0001f), 0)); } /// /// Updates fog of war transitions and uploads texture changes to GPU if required /// public void UpdateFogOfWar (bool forceUpload = false) { if (!enableFogOfWar || _fogOfWarTexture == null) return; if (forceUpload) { requiresTextureUpload = true; } int tw = _fogOfWarTexture.width; now = Time.time; lock (_lock) { for (int k = 0; k <= lastTransitionPos; k++) { FogOfWarTransition fw = fowTransitionList[k]; if (!fw.enabled) continue; float elapsed = now - fw.startTime - fw.startDelay; if (elapsed > 0) { float t = fw.duration <= 0 ? 1 : elapsed / fw.duration; if (t < 0) t = 0; else if (t > 1f) t = 1f; int alpha = (int)(fw.initialAlpha + (fw.targetAlpha - fw.initialAlpha) * t); int colorPos = fw.y * tw + fw.x; fogOfWarColorBuffer[colorPos].a = (byte)alpha; requiresTextureUpload = true; if (t >= 1f) { // Add refill slot if needed if (fw.targetAlpha != fw.restoreToAlpha && fw.restoreDelay > 0) { fowTransitionList[k].duration = fw.restoreDuration; fowTransitionList[k].startTime = now; fowTransitionList[k].startDelay = fw.restoreDelay; fowTransitionList[k].initialAlpha = fw.targetAlpha; fowTransitionList[k].targetAlpha = fw.restoreToAlpha; fowTransitionList[k].restoreDelay = 0; fowTransitionList[k].restoreDuration = 0; } else { fowTransitionList[k].enabled = false; fowFreeIndices.Push(k); int key = fw.y * 64000 + fw.x; fowTransitionIndices.Remove(key); } } } } } if (requiresTextureUpload) { if (!backgroundThreadBusy) { requiresTextureUpload = false; UploadFogOfWarTextureEditsToGPU(); } } if (fogOfWarIsLocal) { UpdateFogOfWarMaterialBoundsProperties(); } } void UploadFogOfWarTextureEditsToGPU () { _fogOfWarTexture.SetPixels32(fogOfWarColorBuffer); _fogOfWarTexture.Apply(); // Smooth texture if (fogOfWarBlur) { SetFowBlurTexture(); } #if UNITY_EDITOR if (!Application.isPlaying) { UnityEditor.EditorUtility.SetDirty(_fogOfWarTexture); } #endif } void SetFowBlurTexture () { if (fogMat == null) return; if (fowBlurMat == null) { fowBlurMat = new Material(Shader.Find("Hidden/VolumetricFog2/FoWBlur")); } if (fowBlur1 == null || fowBlur1.width != _fogOfWarTexture.width || fowBlur2 == null || fowBlur2.width != _fogOfWarTexture.width) { CreateFoWBlurRTs(); } fowBlur1.DiscardContents(); Graphics.Blit(_fogOfWarTexture, fowBlur1, fowBlurMat, 0); fowBlur2.DiscardContents(); Graphics.Blit(fowBlur1, fowBlur2, fowBlurMat, 1); fogMat.SetTexture(ShaderParams.FogOfWarTexture, fowBlur2); } void CreateFoWBlurRTs () { if (fowBlur1 != null) { fowBlur1.Release(); } if (fowBlur2 != null) { fowBlur2.Release(); } RenderTextureDescriptor desc = new RenderTextureDescriptor(_fogOfWarTexture.width, _fogOfWarTexture.height, RenderTextureFormat.ARGB32, 0); fowBlur1 = new RenderTexture(desc); fowBlur2 = new RenderTexture(desc); } /// /// Changes the alpha value of the fog of war at world position creating a transition from current alpha value to specified target alpha. It takes into account FogOfWarCenter and FogOfWarSize. /// Note that only x and z coordinates are used. Y (vertical) coordinate is ignored. /// /// in world space coordinates. /// radius of application in world units. /// target alpha value. /// duration of transition in seconds (0 = apply fogNewAlpha instantly). public void SetFogOfWarAlpha (Vector3 worldPosition, float radius, float fogNewAlpha, float duration = 0) { SetFogOfWarAlpha(worldPosition, radius, fogNewAlpha, true, duration, fogOfWarSmoothness, fogOfWarRestoreDelay, fogOfWarRestoreDuration); } /// /// Changes the alpha value of the fog of war at world position creating a transition from current alpha value to specified target alpha. It takes into account FogOfWarCenter and FogOfWarSize. /// Note that only x and z coordinates are used. Y (vertical) coordinate is ignored. /// /// in world space coordinates. /// radius of application in world units. /// target alpha value. /// duration of transition in seconds (0 = apply fogNewAlpha instantly). /// border smoothness (0 = sharp borders, 1 = smooth transition) public void SetFogOfWarAlpha (Vector3 worldPosition, float radius, float fogNewAlpha, float duration, float smoothness) { SetFogOfWarAlpha(worldPosition, radius, fogNewAlpha, true, duration, smoothness, fogOfWarRestoreDelay, fogOfWarRestoreDuration); } /// /// Changes the alpha value of the fog of war at world position creating a transition from current alpha value to specified target alpha. It takes into account FogOfWarCenter and FogOfWarSize. /// Note that only x and z coordinates are used. Y (vertical) coordinate is ignored. /// /// in world space coordinates. /// radius of application in world units. /// if new alpha is combined with preexisting alpha value or replaced. /// target alpha value. /// duration of transition in seconds (0 = apply fogNewAlpha instantly). /// border smoothness (0 = sharp borders, 1 = smooth transition) /// if method should run on main thread (immediate effect) or in a background thread (update can be slower but doesn't affect main thread performance) public void SetFogOfWarAlpha (Vector3 worldPosition, float radius, float fogNewAlpha, float duration, float smoothness, FoWUpdateMethod updateMethod = FoWUpdateMethod.BackgroundThread, bool blendAlpha = true) { SetFogOfWarAlpha(worldPosition, radius, fogNewAlpha, blendAlpha, duration, smoothness, fogOfWarRestoreDelay, fogOfWarRestoreDuration, restoreToAlphaValue: 1f, updateMethod); } /// /// Changes the alpha value of the fog of war at world position creating a transition from current alpha value to specified target alpha. It takes into account FogOfWarCenter and FogOfWarSize. /// Note that only x and z coordinates are used. Y (vertical) coordinate is ignored. /// /// in world space coordinates. /// radius of application in world units. /// target alpha value. /// if new alpha is combined with preexisting alpha value or replaced. /// duration of transition in seconds (0 = apply fogNewAlpha instantly). /// border smoothness. /// delay before the fog alpha is restored. Pass 0 to keep change forever. /// restore duration in seconds. /// final alpha value when fog restores. /// if method should run on main thread (immediate effect) or in a background thread (update can be slower but doesn't affect main thread performance) public void SetFogOfWarAlpha (Vector3 worldPosition, float radius, float fogNewAlpha, bool blendAlpha, float duration, float smoothness, float restoreDelay, float restoreDuration, float restoreToAlphaValue = 1f, FoWUpdateMethod updateMethod = FoWUpdateMethod.BackgroundThread) { if (_fogOfWarTexture == null || fogOfWarColorBuffer == null || fogOfWarColorBuffer.Length == 0) return; int tw = _fogOfWarTexture.width; int th = _fogOfWarTexture.height; now = Time.time; if (updateMethod == FoWUpdateMethod.BackgroundThread && Application.isPlaying) { System.Threading.Tasks.Task.Run(() => { lock (_lock) { try { backgroundThreadBusy = true; internal_SetFogOfWarAlpha(tw, th, worldPosition, radius, fogNewAlpha, blendAlpha, duration, smoothness, restoreDelay, restoreDuration, restoreToAlphaValue); } finally { backgroundThreadBusy = false; } } }); return; } internal_SetFogOfWarAlpha(tw, th, worldPosition, radius, fogNewAlpha, blendAlpha, duration, smoothness, restoreDelay, restoreDuration, restoreToAlphaValue); } void internal_SetFogOfWarAlpha (int tw, int th, Vector3 worldPosition, float radius, float fogNewAlpha, bool blendAlpha = false, float duration = 0, float smoothness = 0, float restoreDelay = 0, float restoreDuration = 2, float restoreToAlphaValue = 1f) { Vector3 fogOfWarCenter = anchoredFogOfWarCenter; float tx = (worldPosition.x - fogOfWarCenter.x) / fogOfWarSize.x + 0.5f; if (tx < 0 || tx > 1f) return; float tz = (worldPosition.z - fogOfWarCenter.z) / fogOfWarSize.z + 0.5f; if (tz < 0 || tz > 1f) return; int px = (int)(tx * tw); int pz = (int)(tz * th); float sm = 0.0001f + smoothness; byte newAlpha8 = (byte)(fogNewAlpha * 255); float tr = radius / fogOfWarSize.z; int delta = (int)(th * tr); int deltaSqr = delta * delta; byte restoreAlpha = (byte)(255 * restoreToAlphaValue); int minR = Mathf.Max(0, pz - delta); int maxR = Mathf.Min(th - 1, pz + delta); int minC = Mathf.Max(0, px - delta); int maxC = Mathf.Min(tw - 1, px + delta); for (int r = minR; r <= maxR; r++) { int rOffset = r * tw; int rDistSqr = (pz - r) * (pz - r); for (int c = minC; c <= maxC; c++) { int cDistSqr = (px - c) * (px - c); int distanceSqr = rDistSqr + cDistSqr; if (distanceSqr <= deltaSqr) { int colorBufferPos = rOffset + c; Color32 colorBuffer = fogOfWarColorBuffer[colorBufferPos]; if (!blendAlpha) { colorBuffer.a = 255; } distanceSqr = deltaSqr - distanceSqr; float t = (float)distanceSqr / (deltaSqr * sm); t = 1f - t; if (t < 0) { t = 0; } else if (t > 1f) { t = 1f; } byte targetAlpha = (byte)(newAlpha8 + (colorBuffer.a - newAlpha8) * t); if (targetAlpha < 255 && (colorBuffer.a != targetAlpha || restoreDelay > 0)) { if (duration > 0) { AddFogOfWarTransitionSlot(c, r, colorBuffer.a, targetAlpha, 0, duration, restoreAlpha, restoreDelay, restoreDuration); } else { colorBuffer.a = targetAlpha; fogOfWarColorBuffer[colorBufferPos] = colorBuffer; requiresTextureUpload = true; if (restoreDelay > 0) { AddFogOfWarTransitionSlot(c, r, targetAlpha, restoreAlpha, restoreDelay, restoreDuration, targetAlpha, 0, 0); } } } } } } } /// /// Changes the alpha value of the fog of war within bounds creating a transition from current alpha value to specified target alpha. It takes into account FogOfWarCenter and FogOfWarSize. /// Note that only x and z coordinates are used. Y (vertical) coordinate is ignored. /// /// in world space coordinates. /// target alpha value. /// duration of transition in seconds (0 = apply fogNewAlpha instantly). public void SetFogOfWarAlpha (Bounds bounds, float fogNewAlpha, float duration = 0) { SetFogOfWarAlpha(bounds, fogNewAlpha, false, duration, fogOfWarSmoothness, fogOfWarRestoreDelay, fogOfWarRestoreDuration); } /// /// Changes the alpha value of the fog of war within bounds creating a transition from current alpha value to specified target alpha. It takes into account FogOfWarCenter and FogOfWarSize. /// Note that only x and z coordinates are used. Y (vertical) coordinate is ignored. /// /// in world space coordinates. /// target alpha value. /// duration of transition in seconds (0 = apply fogNewAlpha instantly). /// border smoothness. public void SetFogOfWarAlpha (Bounds bounds, float fogNewAlpha, float duration, float smoothness) { SetFogOfWarAlpha(bounds, fogNewAlpha, false, duration, smoothness, fogOfWarRestoreDelay, fogOfWarRestoreDuration); } /// /// Changes the alpha value of the fog of war within bounds creating a transition from current alpha value to specified target alpha. It takes into account FogOfWarCenter and FogOfWarSize. /// Note that only x and z coordinates are used. Y (vertical) coordinate is ignored. /// /// in world space coordinates. /// target alpha value (0-1). /// if new alpha is combined with preexisting alpha value or replaced. /// duration of transition in seconds (0 = apply fogNewAlpha instantly). /// border smoothness. /// randomization of border noise. /// delay before the fog alpha is restored. Pass 0 to keep change forever. /// restore duration in seconds. /// alpha value (0-1) for the fog when restore is completed. public void SetFogOfWarAlpha (Bounds bounds, float fogNewAlpha, bool blendAlpha, float duration, float smoothness, float restoreDelay, float restoreDuration, float restoreToAlpha = 1f, FoWUpdateMethod updateMethod = FoWUpdateMethod.BackgroundThread) { if (_fogOfWarTexture == null || fogOfWarColorBuffer == null || fogOfWarColorBuffer.Length == 0) return; int tw = _fogOfWarTexture.width; int th = _fogOfWarTexture.height; now = Time.time; if (updateMethod == FoWUpdateMethod.BackgroundThread && Application.isPlaying) { System.Threading.Tasks.Task.Run(() => { lock (_lock) { try { backgroundThreadBusy = true; internal_SetFogOfWarAlpha(tw, th, bounds, fogNewAlpha, blendAlpha, duration, smoothness, restoreDelay, restoreDuration, restoreToAlpha); } finally { backgroundThreadBusy = false; } } }); return; } internal_SetFogOfWarAlpha(tw, th, bounds, fogNewAlpha, blendAlpha, duration, smoothness, restoreDelay, restoreDuration, restoreToAlpha); } void internal_SetFogOfWarAlpha (int tw, int th, Bounds bounds, float fogNewAlpha, bool blendAlpha, float duration = 0, float smoothness = 0, float restoreDelay = 0, float restoreDuration = 2, float restoreToAlpha = 1f) { Vector3 fogOfWarCenter = anchoredFogOfWarCenter; Vector3 worldPosition = bounds.center; float tx = (worldPosition.x - fogOfWarCenter.x) / fogOfWarSize.x + 0.5f; if (tx < 0 || tx > 1f) return; float tz = (worldPosition.z - fogOfWarCenter.z) / fogOfWarSize.z + 0.5f; if (tz < 0 || tz > 1f) return; int px = (int)(tx * tw); int pz = (int)(tz * th); byte newAlpha8 = (byte)(fogNewAlpha * 255); float trz = bounds.extents.z / fogOfWarSize.z; float trx = bounds.extents.x / fogOfWarSize.x; float aspect1 = trx > trz ? 1f : trz / trx; float aspect2 = trx > trz ? trx / trz : 1f; int deltaz = (int)(th * trz); int deltazSqr = deltaz * deltaz; int deltax = (int)(tw * trx); int deltaxSqr = deltax * deltax; float sm = 0.0001f + smoothness; byte restoreAlpha = (byte)(restoreToAlpha * 255); int minR = Mathf.Max(0, pz - deltaz); int maxR = Mathf.Min(th - 1, pz + deltaz); int minC = Mathf.Max(0, px - deltax); int maxC = Mathf.Min(tw - 1, px + deltax); for (int r = minR; r <= maxR; r++) { int rOffset = r * tw; int distancezSqr = (pz - r) * (pz - r); distancezSqr = deltazSqr - distancezSqr; float t1 = (float)distancezSqr * aspect1 / (deltazSqr * sm); for (int c = minC; c <= maxC; c++) { int distancexSqr = (px - c) * (px - c); int colorBufferPos = rOffset + c; Color32 colorBuffer = fogOfWarColorBuffer[colorBufferPos]; if (!blendAlpha) colorBuffer.a = 255; distancexSqr = deltaxSqr - distancexSqr; float t2 = (float)distancexSqr * aspect2 / (deltaxSqr * sm); float t = t1 < t2 ? t1 : t2; t = 1f - t; if (t < 0) t = 0; else if (t > 1f) t = 1f; byte targetAlpha = (byte)(newAlpha8 + (colorBuffer.a - newAlpha8) * t); if (targetAlpha < 255 && (colorBuffer.a != targetAlpha || restoreDelay > 0)) { if (duration > 0) { AddFogOfWarTransitionSlot(c, r, colorBuffer.a, targetAlpha, 0, duration, restoreAlpha, restoreDelay, restoreDuration); } else { colorBuffer.a = targetAlpha; fogOfWarColorBuffer[colorBufferPos] = colorBuffer; requiresTextureUpload = true; if (restoreDelay > 0) { AddFogOfWarTransitionSlot(c, r, targetAlpha, restoreAlpha, restoreDelay, restoreDuration, restoreAlpha, 0, 0); } } } } } } /// /// Changes the alpha value of the fog of war within collider creating a transition from current alpha value to specified target alpha. It takes into account FogOfWarCenter and FogOfWarSize. /// Note that only x and z coordinates are used. Y (vertical) coordinate is ignored. /// /// collider used to define the shape of the area where fog of war alpha will be set. Collider must be convex. /// target alpha value (0-1). /// if new alpha is combined with preexisting alpha value or replaced. /// duration of transition in seconds (0 = apply fogNewAlpha instantly). /// border smoothness. /// delay before the fog alpha is restored. Pass 0 to keep change forever. /// restore duration in seconds. /// alpha value (0-1) for the fog when restore is completed. public void SetFogOfWarAlpha (Collider collider, float fogNewAlpha, bool blendAlpha = false, float duration = 0, float smoothness = 0, float restoreDelay = 0, float restoreDuration = 2, float restoreToAlpha = 1f) { if (_fogOfWarTexture == null || fogOfWarColorBuffer == null || fogOfWarColorBuffer.Length == 0) return; Vector3 fogOfWarCenter = anchoredFogOfWarCenter; Bounds bounds = collider.bounds; Vector3 worldPosition = bounds.center; float tx = (worldPosition.x - fogOfWarCenter.x) / fogOfWarSize.x + 0.5f; if (tx < 0 || tx > 1f) return; float tz = (worldPosition.z - fogOfWarCenter.z) / fogOfWarSize.z + 0.5f; if (tz < 0 || tz > 1f) return; now = Time.time; int tw = _fogOfWarTexture.width; int th = _fogOfWarTexture.height; int px = (int)(tx * tw); int pz = (int)(tz * th); byte newAlpha8 = (byte)(fogNewAlpha * 255); float trz = bounds.extents.z / fogOfWarSize.z; float trx = bounds.extents.x / fogOfWarSize.x; float aspect1 = trx > trz ? 1f : trz / trx; float aspect2 = trx > trz ? trx / trz : 1f; int deltaz = (int)(th * trz); int deltazSqr = deltaz * deltaz; int deltax = (int)(tw * trx); int deltaxSqr = deltax * deltax; float sm = 0.0001f + smoothness; byte restoreAlpha = (byte)(restoreToAlpha * 255); Vector3 wpos = bounds.min; wpos.y = bounds.center.y; for (int rr = 0; rr <= deltaz * 2; rr++) { int r = pz - deltaz + rr; if (r > 0 && r < th - 1) { int distancezSqr = (pz - r) * (pz - r); distancezSqr = deltazSqr - distancezSqr; float t1 = (float)distancezSqr * aspect1 / (deltazSqr * sm); wpos.z = bounds.min.z + bounds.size.z * rr / (deltaz * 2f); for (int cc = 0; cc <= deltax * 2; cc++) { int c = px - deltax + cc; if (c > 0 && c < tw - 1) { wpos.x = bounds.min.x + bounds.size.x * cc / (deltax * 2f); Vector3 colliderPos = collider.ClosestPoint(wpos); if (colliderPos != wpos) continue; // point is outside collider int distancexSqr = (px - c) * (px - c); int colorBufferPos = r * tw + c; Color32 colorBuffer = fogOfWarColorBuffer[colorBufferPos]; if (!blendAlpha) colorBuffer.a = 255; distancexSqr = deltaxSqr - distancexSqr; float t2 = (float)distancexSqr * aspect2 / (deltaxSqr * sm); float t = t1 < t2 ? t1 : t2; t = 1f - t; if (t < 0) t = 0; else if (t > 1f) t = 1f; byte targetAlpha = (byte)(newAlpha8 + (colorBuffer.a - newAlpha8) * t); if (targetAlpha < 255 && (colorBuffer.a != targetAlpha || restoreDelay > 0)) { if (duration > 0) { AddFogOfWarTransitionSlot(c, r, colorBuffer.a, targetAlpha, 0, duration, restoreAlpha, restoreDelay, restoreDuration); } else { colorBuffer.a = targetAlpha; fogOfWarColorBuffer[colorBufferPos] = colorBuffer; requiresTextureUpload = true; if (restoreDelay > 0) { AddFogOfWarTransitionSlot(c, r, targetAlpha, restoreAlpha, restoreDelay, restoreDuration, restoreAlpha, 0, 0); } } } } } } } } /// /// Changes the alpha value of the fog of war within bounds creating a transition from current alpha value to specified target alpha. It takes into account FogOfWarCenter and FogOfWarSize. /// Note that only x and z coordinates are used. Y (vertical) coordinate is ignored. /// /// gameobject used to define the shape of the area where fog of war alpha will be set. The gameobject must have a mesh associated. /// target alpha value (0-1). /// duration of transition in seconds (0 = apply fogNewAlpha instantly). /// delay before the fog alpha is restored. Pass 0 to keep change forever. /// restore duration in seconds. /// alpha value (0-1) for the fog when restore is completed. public void SetFogOfWarAlpha (GameObject go, float fogNewAlpha, float duration = 0, float restoreDelay = 0, float restoreDuration = 2, float restoreToAlpha = 1f, FoWUpdateMethod updateMethod = FoWUpdateMethod.BackgroundThread) { if (_fogOfWarTexture == null || fogOfWarColorBuffer == null || fogOfWarColorBuffer.Length == 0) return; if (go == null) return; int tw = _fogOfWarTexture.width; int th = _fogOfWarTexture.height; now = Time.time; MeshRenderer meshRenderer = go.GetComponentInChildren(); if (meshRenderer == null) { Debug.LogError("No MeshRenderer found on this object."); return; } Bounds bounds = meshRenderer.bounds; MeshFilter mf = meshRenderer.GetComponent(); if (mf == null) { Debug.LogError("No MeshFilter found on this object."); return; } Mesh mesh = mf.sharedMesh; if (mesh == null) { Debug.LogError("No Mesh found on this object."); return; } if (mesh.GetTopology(0) != MeshTopology.Triangles) { Debug.LogError("Only triangle topology is supported by this tool."); return; } // Get triangle info int[] indices = mesh.triangles; Vector3[] vertices = mesh.vertices; int verticesLength = vertices.Length; Transform t = meshRenderer.transform; for (int k = 0; k < verticesLength; k++) { vertices[k] = t.TransformPoint(vertices[k]); } if (updateMethod == FoWUpdateMethod.BackgroundThread && Application.isPlaying) { System.Threading.Tasks.Task.Run(() => { lock (_lock) { try { backgroundThreadBusy = true; internal_SetFogOfWarAlpha(tw, th, bounds, indices, vertices, fogNewAlpha, duration, restoreDelay, restoreDuration, restoreToAlpha); } finally { backgroundThreadBusy = false; } } }); return; } internal_SetFogOfWarAlpha(tw, th, bounds, indices, vertices, fogNewAlpha, duration, restoreDelay, restoreDuration, restoreToAlpha); } void internal_SetFogOfWarAlpha (int tw, int th, Bounds bounds, int[] indices, Vector3[] vertices, float fogNewAlpha, float duration = 0, float restoreDelay = 0, float restoreDuration = 2, float restoreToAlpha = 1f) { Vector3 fogOfWarCenter = anchoredFogOfWarCenter; Vector3 worldPosition = bounds.center; float tx = (worldPosition.x - fogOfWarCenter.x) / fogOfWarSize.x + 0.5f; if (tx < 0 || tx > 1f) return; float tz = (worldPosition.z - fogOfWarCenter.z) / fogOfWarSize.z + 0.5f; if (tz < 0 || tz > 1f) return; byte newAlpha8 = (byte)(fogNewAlpha * 255); byte restoreAlpha = (byte)(restoreToAlpha * 255); int indicesLength = indices.Length; Vector2[] triangles = new Vector2[indicesLength]; for (int k = 0; k < indicesLength; k += 3) { triangles[k].x = vertices[indices[k]].x; triangles[k].y = vertices[indices[k]].z; triangles[k + 1].x = vertices[indices[k + 1]].x; triangles[k + 1].y = vertices[indices[k + 1]].z; triangles[k + 2].x = vertices[indices[k + 2]].x; triangles[k + 2].y = vertices[indices[k + 2]].z; } int index = 0; int px = (int)(tx * tw); int pz = (int)(tz * th); float trz = bounds.extents.z / fogOfWarSize.z; float trx = bounds.extents.x / fogOfWarSize.x; int deltaz = (int)(th * trz); int deltax = (int)(tw * trx); int r0 = pz - deltaz; if (r0 < 1) r0 = 1; else if (r0 >= th) r0 = th - 1; int r1 = pz + deltaz; if (r1 < 1) r1 = 1; else if (r1 >= th) r1 = th - 1; int c0 = px - deltax; if (c0 < 1) c0 = 1; else if (c0 >= tw) c0 = tw - 1; int c1 = px + deltax; if (c1 < 1) c1 = 1; else if (c1 >= tw) c1 = tw - 1; Vector2 v0 = triangles[index]; Vector2 v1 = triangles[index + 1]; Vector2 v2 = triangles[index + 2]; for (int r = r0; r <= r1; r++) { int rr = r * tw; float wz = (((r + 0.5f) / th) - 0.5f) * fogOfWarSize.z + fogOfWarCenter.z; for (int c = c0; c <= c1; c++) { float wx = (((c + 0.5f) / tw) - 0.5f) * fogOfWarSize.x + fogOfWarCenter.x; // Check if any triangle contains this position for (int i = 0; i < indicesLength; i += 3) { if (PointInTriangle(wx, wz, v0.x, v0.y, v1.x, v1.y, v2.x, v2.y)) { int colorBufferPos = rr + c; Color32 colorBuffer = fogOfWarColorBuffer[colorBufferPos]; if (colorBuffer.a != newAlpha8 || restoreDelay > 0) { if (duration > 0) { AddFogOfWarTransitionSlot(c, r, colorBuffer.a, newAlpha8, 0, duration, restoreAlpha, restoreDelay, restoreDuration); } else { colorBuffer.a = newAlpha8; fogOfWarColorBuffer[colorBufferPos] = colorBuffer; requiresTextureUpload = true; if (restoreDelay > 0) { AddFogOfWarTransitionSlot(c, r, newAlpha8, restoreAlpha, restoreDelay, restoreDuration, restoreAlpha, 0, 0); } } } break; } else { index += 3; index %= indicesLength; v0 = triangles[index]; v1 = triangles[index + 1]; v2 = triangles[index + 2]; } } } } } float Sign (float p1x, float p1z, float p2x, float p2z, float p3x, float p3z) { return (p1x - p3x) * (p2z - p3z) - (p2x - p3x) * (p1z - p3z); } bool PointInTriangle (float x, float z, float v1x, float v1z, float v2x, float v2z, float v3x, float v3z) { float d1 = Sign(x, z, v1x, v1z, v2x, v2z); float d2 = Sign(x, z, v2x, v2z, v3x, v3z); float d3 = Sign(x, z, v3x, v3z, v1x, v1z); bool has_neg = (d1 < 0) || (d2 < 0) || (d3 < 0); bool has_pos = (d1 > 0) || (d2 > 0) || (d3 > 0); return !(has_neg && has_pos); } /// /// Restores fog of war to full opacity /// /// World position. /// Radius. /// Alpha value (1f = fully opaque) public void ResetFogOfWarAlpha (Vector3 worldPosition, float radius, float alpha = 1f) { if (_fogOfWarTexture == null || fogOfWarColorBuffer == null || fogOfWarColorBuffer.Length == 0) return; Vector3 fogOfWarCenter = anchoredFogOfWarCenter; float tx = (worldPosition.x - fogOfWarCenter.x) / fogOfWarSize.x + 0.5f; if (tx < 0 || tx > 1f) return; float tz = (worldPosition.z - fogOfWarCenter.z) / fogOfWarSize.z + 0.5f; if (tz < 0 || tz > 1f) return; int tw = _fogOfWarTexture.width; int th = _fogOfWarTexture.height; int px = (int)(tx * tw); int pz = (int)(tz * th); float tr = radius / fogOfWarSize.z; int delta = (int)(th * tr); int deltaSqr = delta * delta; byte fogAlpha = (byte)(alpha * 255); for (int r = pz - delta; r <= pz + delta; r++) { if (r > 0 && r < th - 1) { for (int c = px - delta; c <= px + delta; c++) { if (c > 0 && c < tw - 1) { int distanceSqr = (pz - r) * (pz - r) + (px - c) * (px - c); if (distanceSqr <= deltaSqr) { int colorBufferPos = r * tw + c; Color32 colorBuffer = fogOfWarColorBuffer[colorBufferPos]; colorBuffer.a = fogAlpha; fogOfWarColorBuffer[colorBufferPos] = colorBuffer; requiresTextureUpload = true; } } } } } requiresTextureUpload = true; } /// /// Restores fog of war to full opacity /// public void ResetFogOfWarAlpha (Bounds bounds, float alpha = 1f) { ResetFogOfWarAlpha(bounds.center, bounds.extents.x, bounds.extents.z, alpha); } /// /// Restores fog of war to full opacity /// public void ResetFogOfWarAlpha (Vector3 position, Vector3 size, float alpha = 1f) { ResetFogOfWarAlpha(position, size.x * 0.5f, size.z * 0.5f, alpha); } /// /// Restores fog of war to full opacity /// /// Position in world space. /// Half of the length of the rectangle in X-Axis. /// Half of the length of the rectangle in Z-Axis. /// Alpha value (1f = fully opaque) public void ResetFogOfWarAlpha (Vector3 position, float extentsX, float extentsZ, float alpha = 1f) { if (_fogOfWarTexture == null || fogOfWarColorBuffer == null || fogOfWarColorBuffer.Length == 0) return; Vector3 fogOfWarCenter = anchoredFogOfWarCenter; float tx = (position.x - fogOfWarCenter.x) / fogOfWarSize.x + 0.5f; if (tx < 0 || tx > 1f) return; float tz = (position.z - fogOfWarCenter.z) / fogOfWarSize.z + 0.5f; if (tz < 0 || tz > 1f) return; int tw = _fogOfWarTexture.width; int th = _fogOfWarTexture.height; int px = (int)(tx * tw); int pz = (int)(tz * th); float trz = extentsZ / fogOfWarSize.z; float trx = extentsX / fogOfWarSize.x; int deltaz = (int)(th * trz); int deltax = (int)(tw * trx); byte fogAlpha = (byte)(alpha * 255); for (int r = pz - deltaz; r <= pz + deltaz; r++) { if (r > 0 && r < th - 1) { for (int c = px - deltax; c <= px + deltax; c++) { if (c > 0 && c < tw - 1) { int colorBufferPos = r * tw + c; Color32 colorBuffer = fogOfWarColorBuffer[colorBufferPos]; colorBuffer.a = fogAlpha; fogOfWarColorBuffer[colorBufferPos] = colorBuffer; requiresTextureUpload = true; } } } } } public void ResetFogOfWar (float alpha = 1f) { if (_fogOfWarTexture == null) return; int h = _fogOfWarTexture.height; int w = _fogOfWarTexture.width; int newLength = h * w; if (fogOfWarColorBuffer == null || fogOfWarColorBuffer.Length != newLength) { fogOfWarColorBuffer = new Color32[newLength]; } Color32 opaque = new Color32(255, 255, 255, (byte)(alpha * 255)); for (int k = 0; k < newLength; k++) { fogOfWarColorBuffer[k] = opaque; } UploadFogOfWarTextureEditsToGPU(); InitTransitions(); } /// /// Gets or set fog of war state as a Color32 buffer. The alpha channel stores the transparency of the fog at that position (0 = no fog, 1 = opaque). /// public Color32[] fogOfWarTextureData { get { return fogOfWarColorBuffer; } set { enableFogOfWar = true; fogOfWarColorBuffer = value; if (value == null || _fogOfWarTexture == null) return; if (value.Length != _fogOfWarTexture.width * _fogOfWarTexture.height) return; UploadFogOfWarTextureEditsToGPU(); } } void AddFogOfWarTransitionSlot (int x, int y, byte initialAlpha, byte targetAlpha, float delay, float duration, byte restoreToAlpha, float restoreDelay, float restoreDuration) { // Check if this slot exists int key = y * 64000 + x; if (fowTransitionIndices.TryGetValue(key, out int index)) { // slot already exists if (fowTransitionList[index].enabled) { if (fowTransitionList[index].x != x || fowTransitionList[index].y != y) { index = -1; } else { if (fowTransitionList[index].targetAlpha <= targetAlpha && fowTransitionList[index].restoreToAlpha == restoreToAlpha && fowTransitionList[index].restoreDelay == restoreDelay && fowTransitionList[index].restoreDuration == restoreDuration) { // transition running already to target alpha return; } } } } else { index = -1; } if (index < 0) { if (!fowFreeIndices.TryPop(out index)) return; fowTransitionIndices[key] = index; if (index >= lastTransitionPos) { lastTransitionPos = index; } } fowTransitionList[index].x = x; fowTransitionList[index].y = y; fowTransitionList[index].duration = duration; fowTransitionList[index].startTime = now; fowTransitionList[index].startDelay = delay; fowTransitionList[index].initialAlpha = initialAlpha; fowTransitionList[index].targetAlpha = targetAlpha; fowTransitionList[index].restoreToAlpha = restoreToAlpha; fowTransitionList[index].restoreDelay = restoreDelay; fowTransitionList[index].restoreDuration = restoreDuration; fowTransitionList[index].enabled = true; } /// /// Gets the current alpha value of the Fog of War at a given world position /// /// The fog of war alpha. /// World position. public float GetFogOfWarAlpha (Vector3 worldPosition) { if (fogOfWarColorBuffer == null || fogOfWarColorBuffer.Length == 0 || _fogOfWarTexture == null) return 1f; float tx = (worldPosition.x - fogOfWarCenter.x) / fogOfWarSize.x + 0.5f; if (tx < 0 || tx > 1f) return 1f; float tz = (worldPosition.z - fogOfWarCenter.z) / fogOfWarSize.z + 0.5f; if (tz < 0 || tz > 1f) return 1f; int tw = _fogOfWarTexture.width; int th = _fogOfWarTexture.height; int px = (int)(tx * tw); int pz = (int)(tz * th); int colorBufferPos = pz * tw + px; if (colorBufferPos < 0 || colorBufferPos >= fogOfWarColorBuffer.Length) return 1f; return fogOfWarColorBuffer[colorBufferPos].a / 255f; } } }