using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using UnityEngine.Sprites;
using SoftMasking.Extensions;
namespace SoftMasking {
///
/// 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;
}
///
/// SoftMask 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/Soft Mask", 14)]
[RequireComponent(typeof(RectTransform))]
[HelpURL("https://docs.google.com/document/d/1xFZQGn_odhTCokMFR0LyCPXWtqWXN-bBGVS9GETglx8")]
public class SoftMask : UIBehaviour, ISoftMask, ICanvasRaycastFilter {
//
// How it works:
//
// SoftMask overrides Shader used by child elements. To do it, SoftMask spawns invisible
// SoftMaskable components on them on the fly. SoftMaskable 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 SoftMaskables is fully automated. SoftMaskables are kept on the child
// objects while any SoftMask parent present. When something changes and SoftMask parent
// no longer exists, SoftMaskable is destroyed automatically. So, a user of SoftMask
// 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. SoftMask has the predefined replacement for Unity's default UI shader
// (and its ETC1-version in Unity 5.4+). So, when SoftMask 'sees' a material that uses a
// known shader, it overrides shader by the predefined one. If SoftMask 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, SoftMask will not work and a warning will
// be displayed in Console. If you want SoftMask to work with a custom shader, you can
// manually add support to this shader. For reference how to do it, see
// CustomWithSoftMask.shader from included samples.
//
// All replacements are cached in SoftMask instances. By default Unity draws UI with a
// very small number of material instances (they are spawned one per masking/clipping layer),
// so, SoftMask 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 SoftMask() {
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 SoftMask diagnostics. Used by SoftMaskEditor 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 SoftMask 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 SoftMask 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 Soft Mask 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 ISoftMask.isAlive { get { return this && !_destroyed; } }
Material ISoftMask.GetReplacement(Material original) {
Assert.IsTrue(isActiveAndEnabled);
return _materials.Get(original);
}
void ISoftMask.ReleaseReplacement(Material replacement) {
_materials.Release(replacement);
}
void ISoftMask.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