MayHeCome/Assets/Exoa/TutorialEngine/Masking/Scripts/Masking.cs
2024-12-18 17:55:34 +08:00

1273 lines
39 KiB
C#

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
{
/// <summary>
/// Contains some predefined combinations of mask channel weights.
/// </summary>
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;
}
/// <summary>
/// Masking is a component that can be added to UI elements for masking the children. It works
/// like a standard Unity's <see cref="Mask"/> but supports alpha.
/// </summary>
[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);
}
/// <summary>
/// Source of the mask's image.
/// </summary>
[Serializable]
public enum MaskSource
{
/// <summary>
/// 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.
/// </summary>
Graphic,
/// <summary>
/// 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.
/// </summary>
Sprite,
/// <summary>
/// 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.
/// </summary>
Texture
}
/// <summary>
/// How Sprite's borders should be processed. It is a reduced set of Image.Type values.
/// </summary>
[Serializable]
public enum BorderMode
{
/// <summary>
/// Sprite should be drawn as a whole, ignoring any borders set. It works the
/// same way as Unity's Image.Type.Simple.
/// </summary>
Simple,
/// <summary>
/// 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.
/// </summary>
Sliced,
/// <summary>
/// The same as Sliced, but border fragments will be repeated instead of
/// stretched. It works the same way as Unity's Image.Type.Tiled.
/// </summary>
Tiled
}
/// <summary>
/// Errors encountered during Masking diagnostics. Used by MaskingEditor to display
/// hints relevant to the current state.
/// </summary>
[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
}
/// <summary>
/// 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.
/// </summary>
public Shader defaultShader
{
get { return _defaultShader; }
set { SetShader(ref _defaultShader, value); }
}
/// <summary>
/// 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.
/// </summary>
public Shader defaultETC1Shader
{
get { return _defaultETC1Shader; }
set { SetShader(ref _defaultETC1Shader, value, warnIfNotSet: false); }
}
/// <summary>
/// Determines from where the mask image should be taken.
/// </summary>
public MaskSource source
{
get { return _source; }
set { if (_source != value) Set(ref _source, value); }
}
/// <summary>
/// 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.
/// </summary>
public RectTransform separateMask
{
get { return _separateMask; }
set
{
if (_separateMask != value)
{
Set(ref _separateMask, value);
// We should search them again
_graphic = null;
_maskTransform = null;
}
}
}
/// <summary>
/// Specifies a Sprite that should be used as the mask image. This property takes
/// effect only when source is MaskSource.Sprite.
/// </summary>
/// <seealso cref="source"/>
public Sprite sprite
{
get { return _sprite; }
set { if (_sprite != value) Set(ref _sprite, value); }
}
/// <summary>
/// Specifies how to draw sprite borders. This property takes effect only when
/// source is MaskSource.Sprite.
/// </summary>
/// <seealso cref="source"/>
/// <seealso cref="sprite"/>
public BorderMode spriteBorderMode
{
get { return _spriteBorderMode; }
set { if (_spriteBorderMode != value) Set(ref _spriteBorderMode, value); }
}
/// <summary>
/// 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.
/// </summary>
/// <seealso cref="source"/>
/// <seealso cref="sprite"/>
public float spritePixelsPerUnitMultiplier
{
get { return _spritePixelsPerUnitMultiplier; }
set
{
if (_spritePixelsPerUnitMultiplier != value)
Set(ref _spritePixelsPerUnitMultiplier, ClampPixelsPerUnitMultiplier(value));
}
}
/// <summary>
/// Specifies a Texture2D that should be used as the mask image. This property takes
/// effect only when the source is MaskSource.Texture. This and <see cref="renderTexture"/>
/// properties are mutually exclusive.
/// </summary>
/// <seealso cref="renderTexture"/>
public Texture2D texture
{
get { return _texture as Texture2D; }
set { if (_texture != value) Set(ref _texture, value); }
}
/// <summary>
/// Specifies a RenderTexture that should be used as the mask image. This property takes
/// effect only when the source is MaskSource.Texture. This and <see cref="texture"/>
/// properties are mutually exclusive.
/// </summary>
/// <seealso cref="texture"/>
public RenderTexture renderTexture
{
get { return _texture as RenderTexture; }
set { if (_texture != value) Set(ref _texture, value); }
}
/// <summary>
/// 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.
/// </summary>
public Rect textureUVRect
{
get { return _textureUVRect; }
set { if (_textureUVRect != value) Set(ref _textureUVRect, value); }
}
/// <summary>
/// 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.
/// </summary>
public Color channelWeights
{
get { return _channelWeights; }
set { if (_channelWeights != value) Set(ref _channelWeights, value); }
}
/// <summary>
/// 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].
/// </summary>
public float raycastThreshold
{
get { return _raycastThreshold; }
set { _raycastThreshold = value; }
}
/// <summary>
/// If set, mask values inside the mask rectangle will be inverted. In this case mask's
/// zero value (taking <see cref="channelWeights"/> 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 <see cref="separateMask"/> if it's not null.
/// The default value is false.
/// </summary>
/// <seealso cref="invertOutsides"/>
public bool invertMask
{
get { return _invertMask; }
set { if (_invertMask != value) Set(ref _invertMask, value); }
}
/// <summary>
/// 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 <see cref="separateMask"/> if it's not null.
/// The default value is false.
/// </summary>
/// <seealso cref="invertMask"/>
public bool invertOutsides
{
get { return _invertOutsides; }
set { if (_invertOutsides != value) Set(ref _invertOutsides, value); }
}
/// <summary>
/// Returns true if Masking does raycast filtering, that is if the masked areas are
/// transparent to input.
/// </summary>
public bool isUsingRaycastFiltering
{
get { return _raycastThreshold > 0f; }
}
/// <summary>
/// Returns true if masking is currently active.
/// </summary>
public bool isMaskingEnabled
{
get { return isActiveAndEnabled && canvas; }
}
/// <summary>
/// Checks for errors and returns them as flags. It is used in the editor to determine
/// which warnings should be displayed.
/// </summary>
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>(T obj) { return obj; }
static readonly Rect DefaultUVRect = new Rect(0, 0, 1, 1);
RectTransform maskTransform
{
get
{
return
_maskTransform
? _maskTransform
: (_maskTransform = _separateMask ? _separateMask : GetComponent<RectTransform>());
}
}
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<Graphic>();
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<Canvas>(false);
for (int i = 0; i < canvases.Length; ++i)
if (canvases[i].isActiveAndEnabled)
return canvases[i];
return null;
}
void UpdateMaskParameters()
{
Assert.IsTrue(isMaskingEnabled);
if (_dirty || maskTransform.hasChanged)
{
CalculateMaskParameters();
maskTransform.hasChanged = false;
_lastMaskRect = maskTransform.rect;
_dirty = false;
}
_materials.ApplyAll();
}
void SpawnMaskablesInChildren(Transform root)
{
using (new ClearListAtExit<Maskable>(s_maskables))
for (int i = 0; i < root.childCount; ++i)
{
var child = root.GetChild(i);
child.GetComponents(s_maskables);
Assert.IsTrue(s_maskables.Count <= 1);
if (s_maskables.Count == 0)
child.gameObject.AddComponent<Maskable>();
}
}
void InvalidateChildren()
{
ForEachChildMaskable(x => x.Invalidate());
}
void NotifyChildrenThatMaskMightChanged()
{
ForEachChildMaskable(x => x.MaskMightChanged());
}
void ForEachChildMaskable(Action<Maskable> f)
{
transform.GetComponentsInChildren(s_maskables);
using (new ClearListAtExit<Maskable>(s_maskables))
for (int i = 0; i < s_maskables.Count; ++i)
{
var maskable = s_maskables[i];
if (maskable && maskable.gameObject != gameObject)
f(maskable);
}
}
void DestroyMaterials()
{
_materials.DestroyAllAndClear();
}
struct SourceParameters
{
public Image image;
public Sprite sprite;
public BorderMode spriteBorderMode;
public float spritePixelsPerUnit;
public Texture texture;
public Rect textureUVRect;
}
const float DefaultPixelsPerUnit = 100f;
SourceParameters DeduceSourceParameters()
{
var result = new SourceParameters();
switch (_source)
{
case MaskSource.Graphic:
if (_graphic is Image)
{
var image = (Image)_graphic;
var sprite = image.sprite;
result.image = image;
result.sprite = sprite;
result.spriteBorderMode = ImageTypeToBorderMode(image.type);
if (sprite)
{
#if UNITY_2019_2_OR_NEWER
result.spritePixelsPerUnit = sprite.pixelsPerUnit * image.pixelsPerUnitMultiplier;
#else
result.spritePixelsPerUnit = sprite.pixelsPerUnit;
#endif
result.texture = sprite.texture;
}
else
result.spritePixelsPerUnit = DefaultPixelsPerUnit;
}
else if (_graphic is RawImage)
{
var rawImage = (RawImage)_graphic;
result.texture = rawImage.texture;
result.textureUVRect = rawImage.uvRect;
}
break;
case MaskSource.Sprite:
result.sprite = _sprite;
result.spriteBorderMode = _spriteBorderMode;
if (_sprite)
{
result.spritePixelsPerUnit = _sprite.pixelsPerUnit * _spritePixelsPerUnitMultiplier;
result.texture = _sprite.texture;
}
else
result.spritePixelsPerUnit = DefaultPixelsPerUnit;
break;
case MaskSource.Texture:
result.texture = _texture;
result.textureUVRect = _textureUVRect;
break;
default:
Debug.LogAssertionFormat(this, "Unknown MaskSource: {0}", _source);
break;
}
return result;
}
public static BorderMode ImageTypeToBorderMode(Image.Type type)
{
switch (type)
{
case Image.Type.Simple: return BorderMode.Simple;
case Image.Type.Sliced: return BorderMode.Sliced;
case Image.Type.Tiled: return BorderMode.Tiled;
default:
return BorderMode.Simple;
}
}
public static bool IsImageTypeSupported(Image.Type type)
{
return type == Image.Type.Simple
|| type == Image.Type.Sliced
|| type == Image.Type.Tiled;
}
void CalculateMaskParameters()
{
var sourceParams = DeduceSourceParameters();
_warningReporter.ImageUsed(sourceParams.image);
var spriteErrors = Diagnostics.CheckSprite(sourceParams.sprite);
_warningReporter.SpriteUsed(sourceParams.sprite, spriteErrors);
if (sourceParams.sprite)
{
if (spriteErrors == Errors.NoError)
CalculateSpriteBased(sourceParams.sprite, sourceParams.spriteBorderMode, sourceParams.spritePixelsPerUnit);
else
CalculateSolidFill();
}
else if (sourceParams.texture)
CalculateTextureBased(sourceParams.texture, sourceParams.textureUVRect);
else
CalculateSolidFill();
}
void CalculateSpriteBased(Sprite sprite, BorderMode borderMode, float spritePixelsPerUnit)
{
FillCommonParameters();
var inner = DataUtility.GetInnerUV(sprite);
var outer = DataUtility.GetOuterUV(sprite);
var padding = DataUtility.GetPadding(sprite);
var fullMaskRect = LocalMaskRect(Vector4.zero);
_parameters.maskRectUV = outer;
if (borderMode == BorderMode.Simple)
{
var normalizedPadding = Mathr.Div(padding, sprite.rect.size);
_parameters.maskRect = Mathr.ApplyBorder(fullMaskRect, Mathr.Mul(normalizedPadding, Mathr.Size(fullMaskRect)));
}
else
{
var spriteToCanvasScale = SpriteToCanvasScale(spritePixelsPerUnit);
_parameters.maskRect = Mathr.ApplyBorder(fullMaskRect, padding * spriteToCanvasScale);
var adjustedBorder = AdjustBorders(sprite.border * spriteToCanvasScale, fullMaskRect);
_parameters.maskBorder = LocalMaskRect(adjustedBorder);
_parameters.maskBorderUV = inner;
}
_parameters.texture = sprite.texture;
_parameters.borderMode = borderMode;
if (borderMode == BorderMode.Tiled)
_parameters.tileRepeat = MaskRepeat(sprite, spritePixelsPerUnit, _parameters.maskBorder);
}
static Vector4 AdjustBorders(Vector4 border, Vector4 rect)
{
// Copied from Unity's Image.
var size = Mathr.Size(rect);
for (int axis = 0; axis <= 1; axis++)
{
// If the rect is smaller than the combined borders, then there's not room for
// the borders at their normal size. In order to avoid artefacts with overlapping
// borders, we scale the borders down to fit.
float combinedBorders = border[axis] + border[axis + 2];
if (size[axis] < combinedBorders && combinedBorders != 0)
{
float borderScaleRatio = size[axis] / combinedBorders;
border[axis] *= borderScaleRatio;
border[axis + 2] *= borderScaleRatio;
}
}
return border;
}
void CalculateTextureBased(Texture texture, Rect uvRect)
{
FillCommonParameters();
_parameters.maskRect = LocalMaskRect(Vector4.zero);
_parameters.maskRectUV = Mathr.ToVector(uvRect);
_parameters.texture = texture;
_parameters.borderMode = BorderMode.Simple;
}
void CalculateSolidFill()
{
CalculateTextureBased(null, DefaultUVRect);
}
void FillCommonParameters()
{
_parameters.worldToMask = WorldToMask();
_parameters.maskChannelWeights = _channelWeights;
_parameters.invertMask = _invertMask;
_parameters.invertOutsides = _invertOutsides;
}
float SpriteToCanvasScale(float spritePixelsPerUnit)
{
var canvasPixelsPerUnit = canvas ? canvas.referencePixelsPerUnit : 100;
return canvasPixelsPerUnit / spritePixelsPerUnit;
}
Matrix4x4 WorldToMask()
{
return maskTransform.worldToLocalMatrix * canvas.rootCanvas.transform.localToWorldMatrix;
}
Vector4 LocalMaskRect(Vector4 border)
{
return Mathr.ApplyBorder(Mathr.ToVector(maskTransform.rect), border);
}
Vector2 MaskRepeat(Sprite sprite, float spritePixelsPerUnit, Vector4 centralPart)
{
var textureCenter = Mathr.ApplyBorder(Mathr.ToVector(sprite.rect), sprite.border);
return Mathr.Div(Mathr.Size(centralPart) * SpriteToCanvasScale(spritePixelsPerUnit), Mathr.Size(textureCenter));
}
void WarnIfDefaultShaderIsNotSet()
{
if (!_defaultShader)
Debug.LogWarning("Masking may not work because its defaultShader is not set", this);
}
void Set<T>(ref T field, T value)
{
field = value;
_dirty = true;
}
void SetShader(ref Shader field, Shader value, bool warnIfNotSet = true)
{
if (field != value)
{
field = value;
if (warnIfNotSet)
WarnIfDefaultShaderIsNotSet();
DestroyMaterials();
InvalidateChildren();
}
}
static readonly List<Masking> s_masks = new List<Masking>();
static readonly List<Maskable> s_maskables = new List<Maskable>();
class MaterialReplacerImpl : IMaterialReplacer
{
readonly Masking _owner;
public MaterialReplacerImpl(Masking owner)
{
// Pass whole owner instead of just shaders because they can be changed dynamically.
_owner = owner;
}
public int order { get { return 0; } }
public Material Replace(Material original)
{
if (original == null || original.HasDefaultUIShader())
return Replace(original, _owner._defaultShader);
#if UNITY_5_4_OR_NEWER
else if (original.HasDefaultETC1UIShader())
return Replace(original, _owner._defaultETC1Shader);
#endif
else if (original.SupportsMasking())
return new Material(original);
else
return null;
}
static Material Replace(Material original, Shader defaultReplacementShader)
{
var replacement = defaultReplacementShader
? new Material(defaultReplacementShader)
: null;
if (replacement && original)
replacement.CopyPropertiesFromMaterial(original);
return replacement;
}
}
// Various operations on a Rect represented as Vector4 (xMin, yMin, xMax, yMax).
static class Mathr
{
public static Vector4 ToVector(Rect r) { return new Vector4(r.xMin, r.yMin, r.xMax, r.yMax); }
public static Vector4 Div(Vector4 v, Vector2 s) { return new Vector4(v.x / s.x, v.y / s.y, v.z / s.x, v.w / s.y); }
public static Vector2 Div(Vector2 v, Vector2 s) { return new Vector2(v.x / s.x, v.y / s.y); }
public static Vector4 Mul(Vector4 v, Vector2 s) { return new Vector4(v.x * s.x, v.y * s.y, v.z * s.x, v.w * s.y); }
public static Vector2 Size(Vector4 r) { return new Vector2(r.z - r.x, r.w - r.y); }
public static Vector4 Move(Vector4 v, Vector2 o) { return new Vector4(v.x + o.x, v.y + o.y, v.z + o.x, v.w + o.y); }
public static Vector4 BorderOf(Vector4 outer, Vector4 inner)
{
return new Vector4(inner.x - outer.x, inner.y - outer.y, outer.z - inner.z, outer.w - inner.w);
}
public static Vector4 ApplyBorder(Vector4 v, Vector4 b)
{
return new Vector4(v.x + b.x, v.y + b.y, v.z - b.z, v.w - b.w);
}
public static Vector2 Min(Vector4 r) { return new Vector2(r.x, r.y); }
public static Vector2 Max(Vector4 r) { return new Vector2(r.z, r.w); }
public static Vector2 Remap(Vector2 c, Vector4 from, Vector4 to)
{
var fromSize = Max(from) - Min(from);
var toSize = Max(to) - Min(to);
return Vector2.Scale(Div((c - Min(from)), fromSize), toSize) + Min(to);
}
public static bool Inside(Vector2 v, Vector4 r)
{
return v.x >= r.x && v.y >= r.y && v.x <= r.z && v.y <= r.w;
}
}
struct MaterialParameters
{
public Vector4 maskRect;
public Vector4 maskBorder;
public Vector4 maskRectUV;
public Vector4 maskBorderUV;
public Vector2 tileRepeat;
public Color maskChannelWeights;
public Matrix4x4 worldToMask;
public Texture texture;
public BorderMode borderMode;
public bool invertMask;
public bool invertOutsides;
public Texture activeTexture { get { return texture ? texture : Texture2D.whiteTexture; } }
public enum SampleMaskResult { Success, NonReadable, NonTexture2D }
public SampleMaskResult SampleMask(Vector2 localPos, out float mask)
{
mask = 0;
var texture2D = texture as Texture2D;
if (!texture2D)
return SampleMaskResult.NonTexture2D;
var uv = XY2UV(localPos);
try
{
mask = MaskValue(texture2D.GetPixelBilinear(uv.x, uv.y));
return SampleMaskResult.Success;
}
catch (UnityException)
{
return SampleMaskResult.NonReadable;
}
}
public void Apply(Material mat)
{
mat.SetTexture(Ids.Masking, activeTexture);
mat.SetVector(Ids.Masking_Rect, maskRect);
mat.SetVector(Ids.Masking_UVRect, maskRectUV);
mat.SetColor(Ids.Masking_ChannelWeights, maskChannelWeights);
mat.SetMatrix(Ids.Masking_WorldToMask, worldToMask);
mat.SetFloat(Ids.Masking_InvertMask, invertMask ? 1 : 0);
mat.SetFloat(Ids.Masking_InvertOutsides, invertOutsides ? 1 : 0);
mat.EnableKeyword("Masking_SIMPLE", borderMode == BorderMode.Simple);
mat.EnableKeyword("Masking_SLICED", borderMode == BorderMode.Sliced);
mat.EnableKeyword("Masking_TILED", borderMode == BorderMode.Tiled);
if (borderMode != BorderMode.Simple)
{
mat.SetVector(Ids.Masking_BorderRect, maskBorder);
mat.SetVector(Ids.Masking_UVBorderRect, maskBorderUV);
if (borderMode == BorderMode.Tiled)
mat.SetVector(Ids.Masking_TileRepeat, tileRepeat);
}
}
// The following functions performs the same logic as functions from Masking.cginc.
// They implemented it a bit different way, because there is no such convenient
// vector operations in Unity/C# and conditions are much cheaper here.
Vector2 XY2UV(Vector2 localPos)
{
switch (borderMode)
{
case BorderMode.Simple: return MapSimple(localPos);
case BorderMode.Sliced: return MapBorder(localPos, repeat: false);
case BorderMode.Tiled: return MapBorder(localPos, repeat: true);
default:
Debug.LogAssertion("Unknown BorderMode");
return MapSimple(localPos);
}
}
Vector2 MapSimple(Vector2 localPos)
{
return Mathr.Remap(localPos, maskRect, maskRectUV);
}
Vector2 MapBorder(Vector2 localPos, bool repeat)
{
return
new Vector2(
Inset(
localPos.x,
maskRect.x, maskBorder.x, maskBorder.z, maskRect.z,
maskRectUV.x, maskBorderUV.x, maskBorderUV.z, maskRectUV.z,
repeat ? tileRepeat.x : 1),
Inset(
localPos.y,
maskRect.y, maskBorder.y, maskBorder.w, maskRect.w,
maskRectUV.y, maskBorderUV.y, maskBorderUV.w, maskRectUV.w,
repeat ? tileRepeat.y : 1));
}
float Inset(float v, float x1, float x2, float u1, float u2, float repeat = 1)
{
var w = (x2 - x1);
return Mathf.Lerp(u1, u2, w != 0.0f ? Frac((v - x1) / w * repeat) : 0.0f);
}
float Inset(float v, float x1, float x2, float x3, float x4, float u1, float u2, float u3, float u4, float repeat = 1)
{
if (v < x2)
return Inset(v, x1, x2, u1, u2);
else if (v < x3)
return Inset(v, x2, x3, u2, u3, repeat);
else
return Inset(v, x3, x4, u3, u4);
}
float Frac(float v) { return v - Mathf.Floor(v); }
float MaskValue(Color mask)
{
var value = mask * maskChannelWeights;
return value.a + value.r + value.g + value.b;
}
static class Ids
{
public static readonly int Masking = Shader.PropertyToID("_Masking");
public static readonly int Masking_Rect = Shader.PropertyToID("_Masking_Rect");
public static readonly int Masking_UVRect = Shader.PropertyToID("_Masking_UVRect");
public static readonly int Masking_ChannelWeights = Shader.PropertyToID("_Masking_ChannelWeights");
public static readonly int Masking_WorldToMask = Shader.PropertyToID("_Masking_WorldToMask");
public static readonly int Masking_BorderRect = Shader.PropertyToID("_Masking_BorderRect");
public static readonly int Masking_UVBorderRect = Shader.PropertyToID("_Masking_UVBorderRect");
public static readonly int Masking_TileRepeat = Shader.PropertyToID("_Masking_TileRepeat");
public static readonly int Masking_InvertMask = Shader.PropertyToID("_Masking_InvertMask");
public static readonly int Masking_InvertOutsides = Shader.PropertyToID("_Masking_InvertOutsides");
}
}
struct Diagnostics
{
Masking _Masking;
public Diagnostics(Masking Masking) { _Masking = Masking; }
public Errors PollErrors()
{
var Masking = _Masking; // for use in lambda
var result = Errors.NoError;
Masking.GetComponentsInChildren(s_maskables);
using (new ClearListAtExit<Maskable>(s_maskables))
if (s_maskables.Any(m => ReferenceEquals(m.mask, Masking) && m.shaderIsNotSupported))
result |= Errors.UnsupportedShaders;
if (ThereAreNestedMasks())
result |= Errors.NestedMasks;
result |= CheckSprite(sprite);
result |= CheckImage();
result |= CheckTexture();
return result;
}
public static Errors CheckSprite(Sprite sprite)
{
var result = Errors.NoError;
if (!sprite) return result;
if (sprite.packed && sprite.packingMode == SpritePackingMode.Tight)
result |= Errors.TightPackedSprite;
if (sprite.associatedAlphaSplitTexture)
result |= Errors.AlphaSplitSprite;
return result;
}
Image image { get { return _Masking.DeduceSourceParameters().image; } }
Sprite sprite { get { return _Masking.DeduceSourceParameters().sprite; } }
Texture texture { get { return _Masking.DeduceSourceParameters().texture; } }
bool ThereAreNestedMasks()
{
var Masking = _Masking; // for use in lambda
var result = false;
using (new ClearListAtExit<Masking>(s_masks))
{
Masking.GetComponentsInParent(false, s_masks);
result |= s_masks.Any(x => AreCompeting(Masking, x));
Masking.GetComponentsInChildren(false, s_masks);
result |= s_masks.Any(x => AreCompeting(Masking, x));
}
return result;
}
Errors CheckImage()
{
var result = Errors.NoError;
if (!_Masking.isBasedOnGraphic) return result;
if (image && !IsImageTypeSupported(image.type))
result |= Errors.UnsupportedImageType;
return result;
}
Errors CheckTexture()
{
var result = Errors.NoError;
if (_Masking.isUsingRaycastFiltering && texture)
{
var texture2D = texture as Texture2D;
if (!texture2D)
result |= Errors.UnreadableRenderTexture;
else if (!IsReadable(texture2D))
result |= Errors.UnreadableTexture;
}
return result;
}
static bool AreCompeting(Masking Masking, Masking other)
{
Assert.IsNotNull(other);
return Masking.isMaskingEnabled
&& Masking != other
&& other.isMaskingEnabled
&& Masking.canvas.rootCanvas == other.canvas.rootCanvas
&& !SelectChild(Masking, other).canvas.overrideSorting;
}
static T SelectChild<T>(T first, T second) where T : Component
{
Assert.IsNotNull(first);
Assert.IsNotNull(second);
return first.transform.IsChildOf(second.transform) ? first : second;
}
static bool IsReadable(Texture2D texture)
{
try
{
texture.GetPixel(0, 0);
return true;
}
catch (UnityException)
{
return false;
}
}
}
struct WarningReporter
{
UnityEngine.Object _owner;
Texture _lastReadTexture;
Sprite _lastUsedSprite;
Sprite _lastUsedImageSprite;
Image.Type _lastUsedImageType;
public WarningReporter(UnityEngine.Object owner)
{
_owner = owner;
_lastReadTexture = null;
_lastUsedSprite = null;
_lastUsedImageSprite = null;
_lastUsedImageType = Image.Type.Simple;
}
public void TextureRead(Texture texture, MaterialParameters.SampleMaskResult sampleResult)
{
if (_lastReadTexture == texture)
return;
_lastReadTexture = texture;
switch (sampleResult)
{
case MaterialParameters.SampleMaskResult.NonReadable:
Debug.LogErrorFormat(_owner,
"Raycast Threshold greater than 0 can't be used on Masking with texture '{0}' because "
+ "it's not readable. You can make the texture readable in the Texture Import Settings.",
texture.name);
break;
case MaterialParameters.SampleMaskResult.NonTexture2D:
Debug.LogErrorFormat(_owner,
"Raycast Threshold greater than 0 can't be used on Masking with texture '{0}' because "
+ "it's not a Texture2D. Raycast Threshold may be used only with regular 2D textures.",
texture.name);
break;
}
}
public void SpriteUsed(Sprite sprite, Errors errors)
{
if (_lastUsedSprite == sprite)
return;
_lastUsedSprite = sprite;
if ((errors & Errors.TightPackedSprite) != 0)
Debug.LogError("Masking doesn't support tight packed sprites", _owner);
if ((errors & Errors.AlphaSplitSprite) != 0)
Debug.LogError("Masking doesn't support sprites with an alpha split texture", _owner);
}
public void ImageUsed(Image image)
{
if (!image)
{
_lastUsedImageSprite = null;
_lastUsedImageType = Image.Type.Simple;
return;
}
if (_lastUsedImageSprite == image.sprite && _lastUsedImageType == image.type)
return;
_lastUsedImageSprite = image.sprite;
_lastUsedImageType = image.type;
if (!image)
return;
if (IsImageTypeSupported(image.type))
return;
Debug.LogErrorFormat(_owner,
"Masking doesn't support image type {0}. Image type Simple will be used.",
image.type);
}
}
}
}