1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117 |
- 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 {
- /// <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>
- /// SoftMask 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/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);
- }
- /// <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 SoftMask diagnostics. Used by SoftMaskEditor 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 SoftMask 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 SoftMask 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 Soft Mask 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 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<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<SoftMaskable>(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<SoftMaskable>();
- }
- }
- void InvalidateChildren() {
- ForEachChildMaskable(x => x.Invalidate());
- }
- void NotifyChildrenThatMaskMightChanged() {
- ForEachChildMaskable(x => x.MaskMightChanged());
- }
- void ForEachChildMaskable(Action<SoftMaskable> f) {
- transform.GetComponentsInChildren(s_maskables);
- using (new ClearListAtExit<SoftMaskable>(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("SoftMask 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<SoftMask> s_masks = new List<SoftMask>();
- static readonly List<SoftMaskable> s_maskables = new List<SoftMaskable>();
- class MaterialReplacerImpl : IMaterialReplacer {
- readonly SoftMask _owner;
- public MaterialReplacerImpl(SoftMask 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.SupportsSoftMask())
- 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.SoftMask, activeTexture);
- mat.SetVector(Ids.SoftMask_Rect, maskRect);
- mat.SetVector(Ids.SoftMask_UVRect, maskRectUV);
- mat.SetColor(Ids.SoftMask_ChannelWeights, maskChannelWeights);
- mat.SetMatrix(Ids.SoftMask_WorldToMask, worldToMask);
- mat.SetFloat(Ids.SoftMask_InvertMask, invertMask ? 1 : 0);
- mat.SetFloat(Ids.SoftMask_InvertOutsides, invertOutsides ? 1 : 0);
- mat.EnableKeyword("SOFTMASK_SIMPLE", borderMode == BorderMode.Simple);
- mat.EnableKeyword("SOFTMASK_SLICED", borderMode == BorderMode.Sliced);
- mat.EnableKeyword("SOFTMASK_TILED", borderMode == BorderMode.Tiled);
- if (borderMode != BorderMode.Simple) {
- mat.SetVector(Ids.SoftMask_BorderRect, maskBorder);
- mat.SetVector(Ids.SoftMask_UVBorderRect, maskBorderUV);
- if (borderMode == BorderMode.Tiled)
- mat.SetVector(Ids.SoftMask_TileRepeat, tileRepeat);
- }
- }
- // The following functions performs the same logic as functions from SoftMask.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 SoftMask = Shader.PropertyToID("_SoftMask");
- public static readonly int SoftMask_Rect = Shader.PropertyToID("_SoftMask_Rect");
- public static readonly int SoftMask_UVRect = Shader.PropertyToID("_SoftMask_UVRect");
- public static readonly int SoftMask_ChannelWeights = Shader.PropertyToID("_SoftMask_ChannelWeights");
- public static readonly int SoftMask_WorldToMask = Shader.PropertyToID("_SoftMask_WorldToMask");
- public static readonly int SoftMask_BorderRect = Shader.PropertyToID("_SoftMask_BorderRect");
- public static readonly int SoftMask_UVBorderRect = Shader.PropertyToID("_SoftMask_UVBorderRect");
- public static readonly int SoftMask_TileRepeat = Shader.PropertyToID("_SoftMask_TileRepeat");
- public static readonly int SoftMask_InvertMask = Shader.PropertyToID("_SoftMask_InvertMask");
- public static readonly int SoftMask_InvertOutsides = Shader.PropertyToID("_SoftMask_InvertOutsides");
- }
- }
- struct Diagnostics {
- SoftMask _softMask;
- public Diagnostics(SoftMask softMask) { _softMask = softMask; }
-
- public Errors PollErrors() {
- var softMask = _softMask; // for use in lambda
- var result = Errors.NoError;
- softMask.GetComponentsInChildren(s_maskables);
- using (new ClearListAtExit<SoftMaskable>(s_maskables))
- if (s_maskables.Any(m => ReferenceEquals(m.mask, softMask) && 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 _softMask.DeduceSourceParameters().image; } }
- Sprite sprite { get { return _softMask.DeduceSourceParameters().sprite; } }
- Texture texture { get { return _softMask.DeduceSourceParameters().texture; } }
- bool ThereAreNestedMasks() {
- var softMask = _softMask; // for use in lambda
- var result = false;
- using (new ClearListAtExit<SoftMask>(s_masks)) {
- softMask.GetComponentsInParent(false, s_masks);
- result |= s_masks.Any(x => AreCompeting(softMask, x));
- softMask.GetComponentsInChildren(false, s_masks);
- result |= s_masks.Any(x => AreCompeting(softMask, x));
- }
- return result;
- }
- Errors CheckImage() {
- var result = Errors.NoError;
- if (!_softMask.isBasedOnGraphic) return result;
- if (image && !IsImageTypeSupported(image.type))
- result |= Errors.UnsupportedImageType;
- return result;
- }
- Errors CheckTexture() {
- var result = Errors.NoError;
- if (_softMask.isUsingRaycastFiltering && texture) {
- var texture2D = texture as Texture2D;
- if (!texture2D)
- result |= Errors.UnreadableRenderTexture;
- else if (!IsReadable(texture2D))
- result |= Errors.UnreadableTexture;
- }
- return result;
- }
- static bool AreCompeting(SoftMask softMask, SoftMask other) {
- Assert.IsNotNull(other);
- return softMask.isMaskingEnabled
- && softMask != other
- && other.isMaskingEnabled
- && softMask.canvas.rootCanvas == other.canvas.rootCanvas
- && !SelectChild(softMask, 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 Soft Mask 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 Soft Mask 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("SoftMask doesn't support tight packed sprites", _owner);
- if ((errors & Errors.AlphaSplitSprite) != 0)
- Debug.LogError("SoftMask 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,
- "SoftMask doesn't support image type {0}. Image type Simple will be used.",
- image.type);
- }
- }
- }
- }
|