using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using UnityEngine.Sprites;
using Maskable.Extensions;
namespace Maskable
{
///
/// Contains some predefined combinations of mask channel weights.
///
public static class MaskChannel
{
public static Color alpha = new Color(0, 0, 0, 1);
public static Color red = new Color(1, 0, 0, 0);
public static Color green = new Color(0, 1, 0, 0);
public static Color blue = new Color(0, 0, 1, 0);
public static Color gray = new Color(1, 1, 1, 0) / 3.0f;
}
///
/// Masking is a component that can be added to UI elements for masking the children. It works
/// like a standard Unity's but supports alpha.
///
[ExecuteInEditMode]
[DisallowMultipleComponent]
[AddComponentMenu("UI/Masking", 14)]
[RequireComponent(typeof(RectTransform))]
[HelpURL("https://docs.google.com/document/d/1xFZQGn_odhTCokMFR0LyCPXWtqWXN-bBGVS9GETglx8")]
public class Masking : UIBehaviour, IMasking, ICanvasRaycastFilter
{
//
// How it works:
//
// Masking overrides Shader used by child elements. To do it, Masking spawns invisible
// Maskable components on them on the fly. Maskable implements IMaterialOverride,
// which allows it to override the shader that performs actual rendering. Use of
// IMaterialOverride is transparent to the user: a material assigned to Graphic in the
// inspector is left untouched.
//
// Management of Maskables is fully automated. Maskables are kept on the child
// objects while any Masking parent present. When something changes and Masking parent
// no longer exists, Maskable is destroyed automatically. So, a user of Masking
// doesn't have to worry about any component changes in the hierarchy.
//
// The replacement shader samples the mask texture and multiply the resulted color
// accordingly. Masking has the predefined replacement for Unity's default UI shader
// (and its ETC1-version in Unity 5.4+). So, when Masking 'sees' a material that uses a
// known shader, it overrides shader by the predefined one. If Masking encounters a
// material with an unknown shader, it can't do anything reasonable (because it doesn't know
// what that shader should do). In such a case, Masking will not work and a warning will
// be displayed in Console. If you want Masking to work with a custom shader, you can
// manually add support to this shader. For reference how to do it, see
// CustomWithMasking.shader from included samples.
//
// All replacements are cached in Masking instances. By default Unity draws UI with a
// very small number of material instances (they are spawned one per masking/clipping layer),
// so, Masking creates a relatively small number of overrides.
//
[SerializeField]
Shader _defaultShader = null;
[SerializeField]
Shader _defaultETC1Shader = null;
[SerializeField]
MaskSource _source = MaskSource.Graphic;
[SerializeField]
RectTransform _separateMask = null;
[SerializeField]
Sprite _sprite = null;
[SerializeField]
BorderMode _spriteBorderMode = BorderMode.Simple;
[SerializeField]
float _spritePixelsPerUnitMultiplier = 1f;
[SerializeField]
Texture _texture = null;
[SerializeField]
Rect _textureUVRect = DefaultUVRect;
[SerializeField]
Color _channelWeights = MaskChannel.alpha;
[SerializeField]
float _raycastThreshold = 0f;
[SerializeField]
bool _invertMask = false;
[SerializeField]
bool _invertOutsides = false;
MaterialReplacements _materials;
MaterialParameters _parameters;
WarningReporter _warningReporter;
Rect _lastMaskRect;
bool _maskingWasEnabled;
bool _destroyed;
bool _dirty;
// Cached components
RectTransform _maskTransform;
Graphic _graphic;
Canvas _canvas;
public Masking()
{
var materialReplacer =
new MaterialReplacerChain(
MaterialReplacer.globalReplacers,
new MaterialReplacerImpl(this));
_materials = new MaterialReplacements(materialReplacer, m => _parameters.Apply(m));
_warningReporter = new WarningReporter(this);
}
///
/// Source of the mask's image.
///
[Serializable]
public enum MaskSource
{
///
/// The mask image should be taken from the Graphic component of the containing
/// GameObject. Only Image and RawImage components are supported. If there is no
/// appropriate Graphic on the GameObject, a solid rectangle of the RectTransform
/// dimensions will be used.
///
Graphic,
///
/// The mask image should be taken from an explicitly specified Sprite. When this mode
/// is used, spriteBorderMode can also be set to determine how to process Sprite's
/// borders. If the sprite isn't set, a solid rectangle of the RectTransform dimensions
/// will be used. This mode is analogous to using an Image with according sprite and
/// type set.
///
Sprite,
///
/// The mask image should be taken from an explicitly specified Texture2D or
/// RenderTexture. When this mode is used, textureUVRect can also be set to determine
/// which part of the texture should be used. If the texture isn't set, a solid rectangle
/// of the RectTransform dimensions will be used. This mode is analogous to using a
/// RawImage with according texture and uvRect set.
///
Texture
}
///
/// How Sprite's borders should be processed. It is a reduced set of Image.Type values.
///
[Serializable]
public enum BorderMode
{
///
/// Sprite should be drawn as a whole, ignoring any borders set. It works the
/// same way as Unity's Image.Type.Simple.
///
Simple,
///
/// Sprite borders should be stretched when the drawn image is larger that the
/// source. It works the same way as Unity's Image.Type.Sliced.
///
Sliced,
///
/// The same as Sliced, but border fragments will be repeated instead of
/// stretched. It works the same way as Unity's Image.Type.Tiled.
///
Tiled
}
///
/// Errors encountered during Masking diagnostics. Used by MaskingEditor to display
/// hints relevant to the current state.
///
[Flags]
[Serializable]
public enum Errors
{
NoError = 0,
UnsupportedShaders = 1 << 0,
NestedMasks = 1 << 1,
TightPackedSprite = 1 << 2,
AlphaSplitSprite = 1 << 3,
UnsupportedImageType = 1 << 4,
UnreadableTexture = 1 << 5,
UnreadableRenderTexture = 1 << 6
}
///
/// Specifies a Shader that should be used as a replacement of the Unity's default UI
/// shader. If you add Masking in play-time by AddComponent(), you should set
/// this property manually.
///
public Shader defaultShader
{
get { return _defaultShader; }
set { SetShader(ref _defaultShader, value); }
}
///
/// Specifies a Shader that should be used as a replacement of the Unity's default UI
/// shader with ETC1 (alpha-split) support. If you use ETC1 textures in UI and
/// add Masking in play-time by AddComponent(), you should set this property manually.
///
public Shader defaultETC1Shader
{
get { return _defaultETC1Shader; }
set { SetShader(ref _defaultETC1Shader, value, warnIfNotSet: false); }
}
///
/// Determines from where the mask image should be taken.
///
public MaskSource source
{
get { return _source; }
set { if (_source != value) Set(ref _source, value); }
}
///
/// Specifies a RectTransform that defines the bounds of the mask. Use of a separate
/// RectTransform allows moving or resizing the mask bounds without affecting children.
/// When null, the RectTransform of this GameObject is used.
/// Default value is null.
///
public RectTransform separateMask
{
get { return _separateMask; }
set
{
if (_separateMask != value)
{
Set(ref _separateMask, value);
// We should search them again
_graphic = null;
_maskTransform = null;
}
}
}
///
/// Specifies a Sprite that should be used as the mask image. This property takes
/// effect only when source is MaskSource.Sprite.
///
///
public Sprite sprite
{
get { return _sprite; }
set { if (_sprite != value) Set(ref _sprite, value); }
}
///
/// Specifies how to draw sprite borders. This property takes effect only when
/// source is MaskSource.Sprite.
///
///
///
public BorderMode spriteBorderMode
{
get { return _spriteBorderMode; }
set { if (_spriteBorderMode != value) Set(ref _spriteBorderMode, value); }
}
///
/// A multiplier that is applied to the pixelsPerUnit property of the selected sprite.
/// Default value is 1. This property takes effect only when source is MaskSource.Sprite.
///
///
///
public float spritePixelsPerUnitMultiplier
{
get { return _spritePixelsPerUnitMultiplier; }
set
{
if (_spritePixelsPerUnitMultiplier != value)
Set(ref _spritePixelsPerUnitMultiplier, ClampPixelsPerUnitMultiplier(value));
}
}
///
/// Specifies a Texture2D that should be used as the mask image. This property takes
/// effect only when the source is MaskSource.Texture. This and
/// properties are mutually exclusive.
///
///
public Texture2D texture
{
get { return _texture as Texture2D; }
set { if (_texture != value) Set(ref _texture, value); }
}
///
/// Specifies a RenderTexture that should be used as the mask image. This property takes
/// effect only when the source is MaskSource.Texture. This and
/// properties are mutually exclusive.
///
///
public RenderTexture renderTexture
{
get { return _texture as RenderTexture; }
set { if (_texture != value) Set(ref _texture, value); }
}
///
/// Specifies a normalized UV-space rectangle defining the image part that should be used as
/// the mask image. This property takes effect only when the source is MaskSource.Texture.
/// A value is set in normalized coordinates. The default value is (0, 0, 1, 1), which means
/// that the whole texture is used.
///
public Rect textureUVRect
{
get { return _textureUVRect; }
set { if (_textureUVRect != value) Set(ref _textureUVRect, value); }
}
///
/// Specifies weights of the color channels of the mask. The color sampled from the mask
/// texture is multiplied by this value, after what all components are summed up together.
/// That is, the final mask value is calculated as:
/// color = `pixel-from-mask` * channelWeights
/// value = color.r + color.g + color.b + color.a
/// The `value` is a number by which the resulting pixel's alpha is multiplied. As you
/// can see, the result value isn't normalized, so, you should account it while defining
/// custom values for this property.
/// Static class MaskChannel contains some useful predefined values. You can use they
/// as example of how mask calculation works.
/// The default value is MaskChannel.alpha.
///
public Color channelWeights
{
get { return _channelWeights; }
set { if (_channelWeights != value) Set(ref _channelWeights, value); }
}
///
/// Specifies the minimum mask value that the point should have for an input event to pass.
/// If the value sampled from the mask is greater or equal this value, the input event
/// is considered 'hit'. The mask value is compared with raycastThreshold after
/// channelWeights applied.
/// The default value is 0, which means that any pixel belonging to RectTransform is
/// considered in input events. If you specify the value greater than 0, the mask's
/// texture should be readable and it should be not a RenderTexture.
/// Accepts values in range [0..1].
///
public float raycastThreshold
{
get { return _raycastThreshold; }
set { _raycastThreshold = value; }
}
///
/// If set, mask values inside the mask rectangle will be inverted. In this case mask's
/// zero value (taking into account) will be treated as one
/// and vice versa. The mask rectangle is the RectTransform of the GameObject this
/// component is attached to or if it's not null.
/// The default value is false.
///
///
public bool invertMask
{
get { return _invertMask; }
set { if (_invertMask != value) Set(ref _invertMask, value); }
}
///
/// If set, mask values outside the mask rectangle will be inverted. By default, everything
/// outside the mask rectangle has zero mask value. When this property is set, the mask
/// outsides will have value one, which means that everything outside the mask will be
/// visible. The mask rectangle is the RectTransform of the GameObject this component
/// is attached to or if it's not null.
/// The default value is false.
///
///
public bool invertOutsides
{
get { return _invertOutsides; }
set { if (_invertOutsides != value) Set(ref _invertOutsides, value); }
}
///
/// Returns true if Masking does raycast filtering, that is if the masked areas are
/// transparent to input.
///
public bool isUsingRaycastFiltering
{
get { return _raycastThreshold > 0f; }
}
///
/// Returns true if masking is currently active.
///
public bool isMaskingEnabled
{
get { return isActiveAndEnabled && canvas; }
}
///
/// Checks for errors and returns them as flags. It is used in the editor to determine
/// which warnings should be displayed.
///
public Errors PollErrors() { return new Diagnostics(this).PollErrors(); }
// ICanvasRaycastFilter
public bool IsRaycastLocationValid(Vector2 sp, Camera cam)
{
Vector2 localPos;
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(maskTransform, sp, cam, out localPos)) return false;
if (!Mathr.Inside(localPos, LocalMaskRect(Vector4.zero))) return _invertOutsides;
if (!_parameters.texture) return true;
if (!isUsingRaycastFiltering) return true;
float mask;
var sampleResult = _parameters.SampleMask(localPos, out mask);
_warningReporter.TextureRead(_parameters.texture, sampleResult);
if (sampleResult != MaterialParameters.SampleMaskResult.Success)
return true;
if (_invertMask)
mask = 1 - mask;
return mask >= _raycastThreshold;
}
protected override void Start()
{
base.Start();
WarnIfDefaultShaderIsNotSet();
}
protected override void OnEnable()
{
base.OnEnable();
SubscribeOnWillRenderCanvases();
SpawnMaskablesInChildren(transform);
FindGraphic();
if (isMaskingEnabled)
UpdateMaskParameters();
NotifyChildrenThatMaskMightChanged();
}
protected override void OnDisable()
{
base.OnDisable();
UnsubscribeFromWillRenderCanvases();
if (_graphic)
{
_graphic.UnregisterDirtyVerticesCallback(OnGraphicDirty);
_graphic.UnregisterDirtyMaterialCallback(OnGraphicDirty);
_graphic = null;
}
NotifyChildrenThatMaskMightChanged();
DestroyMaterials();
}
protected override void OnDestroy()
{
base.OnDestroy();
_destroyed = true;
NotifyChildrenThatMaskMightChanged();
}
protected virtual void LateUpdate()
{
var maskingEnabled = isMaskingEnabled;
if (maskingEnabled)
{
if (_maskingWasEnabled != maskingEnabled)
SpawnMaskablesInChildren(transform);
var prevGraphic = _graphic;
FindGraphic();
if (_lastMaskRect != maskTransform.rect
|| !ReferenceEquals(_graphic, prevGraphic))
_dirty = true;
}
_maskingWasEnabled = maskingEnabled;
}
protected override void OnRectTransformDimensionsChange()
{
base.OnRectTransformDimensionsChange();
_dirty = true;
}
protected override void OnDidApplyAnimationProperties()
{
base.OnDidApplyAnimationProperties();
_dirty = true;
}
#if UNITY_EDITOR
protected override void OnValidate() {
base.OnValidate();
_spritePixelsPerUnitMultiplier = ClampPixelsPerUnitMultiplier(_spritePixelsPerUnitMultiplier);
_dirty = true;
_maskTransform = null;
_graphic = null;
}
#endif
static float ClampPixelsPerUnitMultiplier(float value)
{
return Mathf.Max(value, 0.01f);
}
protected override void OnTransformParentChanged()
{
base.OnTransformParentChanged();
_canvas = null;
_dirty = true;
}
protected override void OnCanvasHierarchyChanged()
{
base.OnCanvasHierarchyChanged();
_canvas = null;
_dirty = true;
NotifyChildrenThatMaskMightChanged();
}
void OnTransformChildrenChanged()
{
SpawnMaskablesInChildren(transform);
}
void SubscribeOnWillRenderCanvases()
{
// To get called when layout and graphics update is finished we should
// subscribe after CanvasUpdateRegistry. CanvasUpdateRegistry subscribes
// in his constructor, so we force its execution.
Touch(CanvasUpdateRegistry.instance);
Canvas.willRenderCanvases += OnWillRenderCanvases;
}
void UnsubscribeFromWillRenderCanvases()
{
Canvas.willRenderCanvases -= OnWillRenderCanvases;
}
void OnWillRenderCanvases()
{
// To be sure that mask will match the state of another drawn UI elements,
// we update material parameters when layout and graphic update is done,
// just before actual rendering.
if (isMaskingEnabled)
UpdateMaskParameters();
}
static T Touch(T obj) { return obj; }
static readonly Rect DefaultUVRect = new Rect(0, 0, 1, 1);
RectTransform maskTransform
{
get
{
return
_maskTransform
? _maskTransform
: (_maskTransform = _separateMask ? _separateMask : GetComponent());
}
}
Canvas canvas
{
get { return _canvas ? _canvas : (_canvas = NearestEnabledCanvas()); }
}
bool isBasedOnGraphic { get { return _source == MaskSource.Graphic; } }
bool IMasking.isAlive { get { return this && !_destroyed; } }
Material IMasking.GetReplacement(Material original)
{
Assert.IsTrue(isActiveAndEnabled);
return _materials.Get(original);
}
void IMasking.ReleaseReplacement(Material replacement)
{
_materials.Release(replacement);
}
void IMasking.UpdateTransformChildren(Transform transform)
{
SpawnMaskablesInChildren(transform);
}
void OnGraphicDirty()
{
if (isBasedOnGraphic) // TODO is this check neccessary?
_dirty = true;
}
void FindGraphic()
{
if (!_graphic && isBasedOnGraphic)
{
_graphic = maskTransform.GetComponent();
if (_graphic)
{
_graphic.RegisterDirtyVerticesCallback(OnGraphicDirty);
_graphic.RegisterDirtyMaterialCallback(OnGraphicDirty);
}
}
}
Canvas NearestEnabledCanvas()
{
// It's a rare operation, so I do not optimize it with static lists
var canvases = GetComponentsInParent