using System.Collections.Generic; using UnityEngine; using System.Reflection; using UnityEngine.UI; namespace MoreMountains.Tools { /// /// A class used to control a float in any other class, over time /// To use it, simply drag a monobehaviour in its target field, pick a control mode (ping pong or random), and tweak the settings /// [MMRequiresConstantRepaint] [AddComponentMenu("More Mountains/Tools/Property Controllers/ShaderController")] public class ShaderController : MMMonoBehaviour { /// the possible types of targets public enum TargetTypes { Renderer, Image, RawImage, Text } /// the possible types of properties public enum PropertyTypes { Bool, Float, Int, Vector, Keyword, Color } /// the possible control modes public enum ControlModes { PingPong, Random, OneTime, AudioAnalyzer, ToDestination, Driven } [Header("Target")] /// the type of renderer to pilot public TargetTypes TargetType = TargetTypes.Renderer; /// the renderer with the shader you want to control [MMEnumCondition("TargetType",(int)TargetTypes.Renderer)] public Renderer TargetRenderer; /// the ID of the material in the Materials array on the target renderer (usually 0) [MMEnumCondition("TargetType", (int)TargetTypes.Renderer)] public int TargetMaterialID = 0; /// the Image with the shader you want to control [MMEnumCondition("TargetType", (int)TargetTypes.Image)] public Image TargetImage; /// if this is true, the 'materialForRendering' for this Image will be used, instead of the regular material [MMEnumCondition("TargetType", (int)TargetTypes.Image)] public bool UseMaterialForRendering = false; /// the RawImage with the shader you want to control [MMEnumCondition("TargetType", (int)TargetTypes.RawImage)] public RawImage TargetRawImage; /// the Text with the shader you want to control [MMEnumCondition("TargetType", (int)TargetTypes.Text)] public Text TargetText; /// if this is true, material will be cached on Start public bool CacheMaterial = true; /// if this is true, an instance of the material will be created on start so that this controller only affects its target public bool CreateMaterialInstance = false; /// the EXACT name of the property to affect public string TargetPropertyName; /// the type of the property to affect public PropertyTypes PropertyType = PropertyTypes.Float; /// whether or not to affect its x component [MMEnumCondition("PropertyType", (int)PropertyTypes.Vector)] public bool X; /// whether or not to affect its y component [MMEnumCondition("PropertyType", (int)PropertyTypes.Vector)] public bool Y; /// whether or not to affect its z component [MMEnumCondition("PropertyType", (int)PropertyTypes.Vector)] public bool Z; /// whether or not to affect its w component [MMEnumCondition("PropertyType", (int)PropertyTypes.Vector)] public bool W; [Header("Color")] /// the color to lerp from [ColorUsage(true, true)] public Color FromColor = Color.black; /// the color to lerp to [ColorUsage(true, true)] public Color ToColor = Color.white; [Header("Global Settings")] /// the control mode (ping pong or random) public ControlModes ControlMode; /// whether or not the updated value should be added to the initial one public bool AddToInitialValue = false; /// whether or not to use unscaled time public bool UseUnscaledTime = true; /// whether or not you want to revert to the InitialValue after the control ends public bool RevertToInitialValueAfterEnd = true; /// if this is true, this component will use material property blocks instead of working on an instance of the material. [Tooltip("if this is true, this component will use material property blocks instead of working on an instance of the material.")] [MMEnumCondition("TargetType", (int)TargetTypes.Renderer)] public bool UseMaterialPropertyBlocks = false; /// whether or not to perform extra safety checks (safer, more costly) public bool SafeMode = false; /// the curve to apply to the tween [Header("Ping Pong")] public MMTweenType Curve; /// the minimum value for the ping pong public float MinValue = 0f; /// the maximum value for the ping pong public float MaxValue = 5f; /// the duration of one ping (or pong) public float Duration = 1f; /// the duration of the pause between two ping (or pongs) (in seconds) public float PingPongPauseDuration = 1f; [Header("Driven")] /// the value that will be applied to the controlled float in driven mode public float DrivenLevel = 0f; [Header("Random")] [MMVector("Min", "Max")] /// the noise amplitude public Vector2 Amplitude = new Vector2(0f,5f); [MMVector("Min", "Max")] /// the noise frequency public Vector2 Frequency = new Vector2(1f, 1f); [MMVector("Min", "Max")] /// the noise shift public Vector2 Shift = new Vector2(0f, 1f); /// if this is true, will let you remap the noise value (without amplitude) to the bounds you've specified public bool RemapNoiseValues = false; /// the value to which to remap the random's zero bound [MMCondition("RemapNoiseValues", true)] public float RemapNoiseZero = 0f; /// the value to which to remap the random's one bound [MMCondition("RemapNoiseValues", true)] public float RemapNoiseOne = 1f; [Header("OneTime")] /// the duration of the One Time shake public float OneTimeDuration = 1f; /// the amplitude of the One Time shake (this will be multiplied by the curve's height) public float OneTimeAmplitude = 1f; /// the low value to remap the normalized curve value to public float OneTimeRemapMin = 0f; /// the high value to remap the normalized curve value to public float OneTimeRemapMax = 1f; /// the curve to apply to the one time shake public AnimationCurve OneTimeCurve = new AnimationCurve(new Keyframe(0, 0), new Keyframe(0.5f, 1), new Keyframe(1, 0)); [MMInspectorButton("OneTime")] /// a test button for the one time shake public bool OneTimeButton; /// whether or not this controller should go back to sleep after a OneTime public bool DisableAfterOneTime = false; /// whether or not this controller should go back to sleep after a OneTime public bool DisableGameObjectAfterOneTime = false; [Header("AudioAnalyzer")] /// the bound audio analyzer used to drive this controller public MMAudioAnalyzer AudioAnalyzer; /// the ID of the selected beat on the analyzer public int BeatID; /// the multiplier to apply to the value out of the analyzer public float AudioAnalyzerMultiplier = 1f; /// the offset to apply to the value out of the analyzer public float AudioAnalyzerOffset = 0f; /// the speed at which to lerp the value public float AudioAnalyzerLerp = 60f; [Header("ToDestination")] /// the value to go to when in ToDestination mode public float ToDestinationValue = 1f; /// the duration of the ToDestination tween public float ToDestinationDuration = 1f; /// the curve to use to tween to the ToDestination value public AnimationCurve ToDestinationCurve = new AnimationCurve(new Keyframe(0, 0), new Keyframe(0.5f, 0.6f), new Keyframe(1f, 1f)); /// a test button for the one time shake [MMInspectorButton("ToDestination")] public bool ToDestinationButton; /// whether or not this controller should go back to sleep after a OneTime public bool DisableAfterToDestination = false; [Header("Debug")] [MMReadOnly] /// the initial value of the controlled float public float InitialValue; [MMReadOnly] /// the current value of the controlled float public float CurrentValue; [MMReadOnly] /// the current value of the controlled float, normalized public float CurrentValueNormalized = 0f; [MMReadOnly] /// the current value of the controlled float public Color InitialColor; [MMReadOnly] /// the ID of the property public int PropertyID; [MMReadOnly] /// whether or not the property got found public bool PropertyFound = false; [MMReadOnly] /// the target material public Material TargetMaterial; /// internal use only [HideInInspector] public float PingPong; protected float _randomAmplitude; protected float _randomFrequency; protected float _randomShift; protected float _elapsedTime = 0f; protected bool _shaking = false; protected float _startedTimestamp = 0f; protected float _remappedTimeSinceStart = 0f; protected Color _currentColor; protected Vector4 _vectorValue; protected float _pingPongDirection = 1f; protected float _lastPingPongPauseAt = 0f; protected float _initialValue = 0f; protected Color _fromColorStorage; protected bool _activeLastFrame = false; protected MaterialPropertyBlock _propertyBlock; /// /// Finds an attribute (property or field) on the target object /// /// /// public virtual bool FindShaderProperty(string propertyName) { if (TargetType == TargetTypes.Renderer) { if (CreateMaterialInstance) { TargetRenderer.materials[TargetMaterialID] = new Material(TargetRenderer.materials[TargetMaterialID]); } TargetMaterial = UseMaterialPropertyBlocks ? TargetRenderer.sharedMaterials[TargetMaterialID] : TargetRenderer.materials[TargetMaterialID]; } else if (TargetType == TargetTypes.Image) { if (CreateMaterialInstance) { TargetImage.material = new Material(TargetImage.material); } TargetMaterial = TargetImage.material; } else if (TargetType == TargetTypes.RawImage) { if (CreateMaterialInstance) { TargetRawImage.material = new Material(TargetRawImage.material); } TargetMaterial = TargetRawImage.material; } else if (TargetType == TargetTypes.Text) { if (CreateMaterialInstance) { TargetText.material = new Material(TargetText.material); } TargetMaterial = TargetText.material; } if (PropertyType == PropertyTypes.Keyword) { PropertyFound = true; return true; } if (TargetMaterial.HasProperty(propertyName)) { PropertyID = Shader.PropertyToID(propertyName); PropertyFound = true; return true; } return false; } /// /// On start we initialize our controller /// protected virtual void Awake() { Initialization(); } /// /// On enable, grabs the initial value /// protected virtual void OnEnable() { InitialValue = GetInitialValue(); if (PropertyType == PropertyTypes.Color) { InitialColor = TargetMaterial.GetColor(PropertyID); } } /// /// Returns true if the renderer is null, false otherwise /// /// protected virtual bool RendererIsNull() { if ((TargetType == TargetTypes.Renderer) && (TargetRenderer == null)) { return true; } if ((TargetType == TargetTypes.Image) && (TargetImage == null)) { return true; } if ((TargetType == TargetTypes.RawImage) && (TargetRawImage == null)) { return true; } if ((TargetType == TargetTypes.Text) && (TargetText == null)) { return true; } return false; } /// /// Grabs the target property and initializes stuff /// public virtual void Initialization() { if (RendererIsNull() || (string.IsNullOrEmpty(TargetPropertyName))) { return; } if (TargetType != TargetTypes.Renderer) { UseMaterialPropertyBlocks = false; } PropertyFound = FindShaderProperty(TargetPropertyName); if (!PropertyFound) { return; } _elapsedTime = 0f; _randomAmplitude = Random.Range(Amplitude.x, Amplitude.y); _randomFrequency = Random.Range(Frequency.x, Frequency.y); _randomShift = Random.Range(Shift.x, Shift.y); if ((TargetType == TargetTypes.Renderer) && UseMaterialPropertyBlocks) { _propertyBlock = new MaterialPropertyBlock(); TargetRenderer.GetPropertyBlock(_propertyBlock, TargetMaterialID); } InitialValue = GetInitialValue(); if (PropertyType == PropertyTypes.Color) { InitialColor = TargetMaterial.GetColor(PropertyID); } _shaking = false; if (ControlMode == ControlModes.OneTime) { this.enabled = false; } } /// /// Sets the level to the value passed in parameters /// /// public virtual void SetDrivenLevelAbsolute(float level) { DrivenLevel = level; } /// /// Sets the level to the remapped value passed in parameters /// /// /// /// public virtual void SetDrivenLevelNormalized(float normalizedLevel, float remapZero, float remapOne) { DrivenLevel = MMMaths.Remap(normalizedLevel, 0f, 1f, remapZero, remapOne); } /// /// Triggers a one time shake of the shader controller /// public virtual void OneTime() { if (!CacheMaterial) { Initialization(); } if (RendererIsNull() || (!PropertyFound)) { return; } else { this.gameObject.SetActive(true); this.enabled = true; ControlMode = ControlModes.OneTime; _startedTimestamp = GetTime(); _shaking = true; } } /// /// Triggers a one time shake of the controller to a specified destination value /// public virtual void ToDestination() { if (!CacheMaterial) { Initialization(); } if (RendererIsNull() || (!PropertyFound)) { return; } else { this.enabled = true; if (PropertyType == PropertyTypes.Color) { _fromColorStorage = FromColor; FromColor = TargetMaterial.GetColor(PropertyID); } ControlMode = ControlModes.ToDestination; _startedTimestamp = GetTime(); _shaking = true; _initialValue = GetInitialValue(); } } /// /// Returns the relevant delta time /// /// protected float GetDeltaTime() { return UseUnscaledTime ? Time.unscaledDeltaTime : Time.deltaTime; } /// /// Returns the relevant time /// /// protected float GetTime() { return UseUnscaledTime ? Time.unscaledTime : Time.time; } /// /// On Update, we move our value based on the defined settings /// protected virtual void Update() { UpdateValue(); } protected virtual void OnDisable() { if (RevertToInitialValueAfterEnd) { CurrentValue = InitialValue; _currentColor = InitialColor; SetValue(CurrentValue); } } /// /// Updates the value over time based on the selected options /// protected virtual void UpdateValue() { if (SafeMode) { if (RendererIsNull() || (!PropertyFound)) { return; } } switch (ControlMode) { case ControlModes.PingPong: if (GetTime() - _lastPingPongPauseAt < PingPongPauseDuration) { return; } PingPong += GetDeltaTime() * _pingPongDirection; if (PingPong < 0f) { PingPong = 0f; _pingPongDirection = -_pingPongDirection; _lastPingPongPauseAt = GetTime(); } if (PingPong > Duration) { PingPong = Duration; _pingPongDirection = -_pingPongDirection; _lastPingPongPauseAt = GetTime(); } CurrentValue = MMTween.Tween(PingPong, 0f, Duration, MinValue, MaxValue, Curve); CurrentValueNormalized = MMMaths.Remap(CurrentValue, MinValue, MaxValue, 0f, 1f); break; case ControlModes.Random: _elapsedTime += GetDeltaTime(); CurrentValueNormalized = Mathf.PerlinNoise(_randomFrequency * _elapsedTime, _randomShift); if (RemapNoiseValues) { CurrentValue = CurrentValueNormalized; CurrentValue = MMMaths.Remap(CurrentValue, 0f, 1f, RemapNoiseZero, RemapNoiseOne); } else { CurrentValue = (CurrentValueNormalized * 2.0f - 1.0f) * _randomAmplitude; } break; case ControlModes.OneTime: if (!_shaking) { return; } _remappedTimeSinceStart = MMMaths.Remap(GetTime() - _startedTimestamp, 0f, OneTimeDuration, 0f, 1f); CurrentValueNormalized = OneTimeCurve.Evaluate(_remappedTimeSinceStart); CurrentValue = MMMaths.Remap(CurrentValueNormalized, 0f, 1f, OneTimeRemapMin, OneTimeRemapMax); CurrentValue *= OneTimeAmplitude; break; case ControlModes.AudioAnalyzer: CurrentValue = Mathf.Lerp(CurrentValue, AudioAnalyzer.Beats[BeatID].CurrentValue * AudioAnalyzerMultiplier + AudioAnalyzerOffset, AudioAnalyzerLerp * GetDeltaTime()); CurrentValueNormalized = Mathf.Clamp(AudioAnalyzer.Beats[BeatID].CurrentValue, 0f, 1f); break; case ControlModes.Driven: CurrentValue = DrivenLevel; CurrentValueNormalized = Mathf.Clamp(CurrentValue, 0f, 1f); break; case ControlModes.ToDestination: if (!_shaking) { return; } _remappedTimeSinceStart = MMMaths.Remap(GetTime() - _startedTimestamp, 0f, ToDestinationDuration, 0f, 1f); float time = ToDestinationCurve.Evaluate(_remappedTimeSinceStart); CurrentValue = Mathf.LerpUnclamped(_initialValue, ToDestinationValue, time); CurrentValueNormalized = MMMaths.Remap(CurrentValue, _initialValue, ToDestinationValue, 0f, 1f); break; } if (PropertyType == PropertyTypes.Color) { _currentColor = Color.Lerp(FromColor, ToColor, CurrentValue); } if (AddToInitialValue) { CurrentValue += InitialValue; } if ((ControlMode == ControlModes.OneTime) && _shaking && (GetTime() - _startedTimestamp > OneTimeDuration)) { _shaking = false; if (RevertToInitialValueAfterEnd) { CurrentValue = InitialValue; if (PropertyType == PropertyTypes.Color) { _currentColor = InitialColor; } } else { CurrentValue = OneTimeCurve.Evaluate(1f); CurrentValue = MMMaths.Remap(CurrentValue, 0f, 1f, OneTimeRemapMin, OneTimeRemapMax); CurrentValue *= OneTimeAmplitude; } SetValue(CurrentValue); if (DisableAfterOneTime) { this.enabled = false; } if (DisableGameObjectAfterOneTime) { this.gameObject.SetActive(false); } return; } if ((ControlMode == ControlModes.ToDestination) && _shaking && (GetTime() - _startedTimestamp > ToDestinationDuration)) { _shaking = false; FromColor = _fromColorStorage; if (RevertToInitialValueAfterEnd) { CurrentValue = InitialValue; if (PropertyType == PropertyTypes.Color) { _currentColor = InitialColor; } } else { CurrentValue = ToDestinationValue; } SetValue(CurrentValue); if (DisableAfterToDestination) { this.enabled = false; } return; } SetValue(CurrentValue); } /// /// Grabs and stores the initial value /// protected virtual float GetInitialValue() { if (TargetMaterial == null) { Debug.LogWarning("Material is null", this); return 0f; } switch (PropertyType) { case PropertyTypes.Bool: return TargetMaterial.GetInt(PropertyID); case PropertyTypes.Int: return TargetMaterial.GetInt(PropertyID); case PropertyTypes.Float: return TargetMaterial.GetFloat(PropertyID); case PropertyTypes.Vector: return TargetMaterial.GetVector(PropertyID).x; case PropertyTypes.Keyword: return TargetMaterial.IsKeywordEnabled(TargetPropertyName) ? 1f : 0f; case PropertyTypes.Color: if (ControlMode != ControlModes.ToDestination) { InitialColor = TargetMaterial.GetColor(PropertyID); } return 0f; default: return 0f; } } /// /// Sets the value in the shader /// /// protected virtual void SetValue(float newValue) { if (TargetType == TargetTypes.Image && UseMaterialForRendering) { if (SafeMode) { if (TargetImage == null) { return; } } TargetMaterial = TargetImage.materialForRendering; } switch (PropertyType) { case PropertyTypes.Bool: newValue = (newValue > 0f) ? 1f : 0f; int newBool = Mathf.RoundToInt(newValue); if (UseMaterialPropertyBlocks) { TargetRenderer.GetPropertyBlock(_propertyBlock); _propertyBlock.SetInt(PropertyID, newBool); TargetRenderer.SetPropertyBlock(_propertyBlock, TargetMaterialID); } else { TargetMaterial.SetInt(PropertyID, newBool); } break; case PropertyTypes.Keyword: newValue = (newValue > 0f) ? 1f : 0f; if (newValue == 0f) { TargetMaterial.DisableKeyword(TargetPropertyName); } else { TargetMaterial.EnableKeyword(TargetPropertyName); } break; case PropertyTypes.Int: int newInt = Mathf.RoundToInt(newValue); if (UseMaterialPropertyBlocks) { TargetRenderer.GetPropertyBlock(_propertyBlock); _propertyBlock.SetInt(PropertyID, newInt); TargetRenderer.SetPropertyBlock(_propertyBlock, TargetMaterialID); } else { TargetMaterial.SetInt(PropertyID, newInt); } break; case PropertyTypes.Float: if (UseMaterialPropertyBlocks) { TargetRenderer.GetPropertyBlock(_propertyBlock); _propertyBlock.SetFloat(PropertyID, newValue); TargetRenderer.SetPropertyBlock(_propertyBlock, TargetMaterialID); } else { TargetMaterial.SetFloat(PropertyID, newValue); } break; case PropertyTypes.Vector: _vectorValue = TargetMaterial.GetVector(PropertyID); if (X) { _vectorValue.x = newValue; } if (Y) { _vectorValue.y = newValue; } if (Z) { _vectorValue.z = newValue; } if (W) { _vectorValue.w = newValue; } if (UseMaterialPropertyBlocks) { TargetRenderer.GetPropertyBlock(_propertyBlock); _propertyBlock.SetVector(PropertyID, _vectorValue); TargetRenderer.SetPropertyBlock(_propertyBlock, TargetMaterialID); } else { TargetMaterial.SetVector(PropertyID, _vectorValue); } break; case PropertyTypes.Color: if (UseMaterialPropertyBlocks) { TargetRenderer.GetPropertyBlock(_propertyBlock); _propertyBlock.SetColor(PropertyID, _currentColor); TargetRenderer.SetPropertyBlock(_propertyBlock, TargetMaterialID); } else { TargetMaterial.SetColor(PropertyID, _currentColor); } break; } } /// /// Interrupts any tween in progress, and disables itself /// public virtual void Stop() { _shaking = false; this.enabled = false; } } }