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;
}
}
}