None Rectangle shaped button?

I just gave a quick glance at the new UI and dig some in the code of the new classes.

However, one thing I noticed - or failed to find - is if it’s possible to make not-rectangle shaped button. I know I can use transparent bitmap, but then how would the input zone be made to fit that shape? I’ve noticed current button are “clicked” even if you click in a fully transparent zone.

Another thing I would like to know, the old UI had really lot of troubles with multi-touch on phone/tablet. Is this issue still in the new GUI?

no

Well, one good news today.

Yep, i tried it out and I successfully dragged three sliders at the same time :sunglasses:

:wink:

And with the old one, I couldn’t press a button if I had a finger somewhere else on the screen at the same time, even if it wasn’t on a UI item.

One way to do it that would make sense (but doesn’t currently work) is use a mask.

So i would like to extend the question by:
Why do masks not block raycasts? or at least have a toggle for it, since it would hardly make sense to do a texture lookup for every single mask even if it is not really needed.

With the information provided by Tim C in this thread i made a simple component that enables masks to block raycasts, making it possible to have arbitrary shaped buttons with correct interaction.

using UnityEngine;
using UnityEngine.UI;

[RequireComponent(typeof(RectTransform))]
[RequireComponent(typeof(Image))]
public class RaycastMask : MonoBehaviour, ICanvasRaycastFilter
{
    private Sprite _sprite;

    void Start ()
    {
        _sprite = GetComponent<Image>().sprite;
    }
   
    public bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera)
    {
        var rectTransform = (RectTransform)transform;
        Vector2 local;
        RectTransformUtility.ScreenPointToLocalPointInRectangle((RectTransform) transform, sp, eventCamera, out local);
        // normalize local coordinates
        var normalized = new Vector2(
            (local.x + rectTransform.pivot.x*rectTransform.rect.width)/rectTransform.rect.width,
            (local.y + rectTransform.pivot.y*rectTransform.rect.height)/rectTransform.rect.width);
        // convert to texture space
        var rect = _sprite.textureRect;
        var x = Mathf.FloorToInt(rect.x + rect.width * normalized.x);
        var y = Mathf.FloorToInt(rect.y + rect.height * normalized.y);
        // destroy component if texture import settings are wrong
        try
        {
            return _sprite.texture.GetPixel(x,y).a > 0;
        }
        catch (UnityException e)
        {
            Debug.LogError("Mask texture not readable, set your sprite to Texture Type 'Advanced' and check 'Read/Write Enabled'");
            Destroy(this);
            return false;
        }
    }
}

You have to parent your button to a Mask (with an image in the shape you want for your button) and add the RaycastMask script to the Mask GameObject.
Unfortunately you also have to adjust the import settings of your sprite texture, setting the texture type to Advanced and checking the Read/Write Enabled checkbox, this is necessary to read the sprite pixels.

Script is also available on github with potential updates down the line.

10 Likes

Interesting. Frankly, that should be a default option of Button.

1 Like

Cool script, but it will (unfortunately) only work with ‘simple’ images as UV’s are not ‘square’ if you are using nine slicing :frowning:

You could do something like add a ‘vertex’ modifier, cache the verts then when the click happens look up the correct UV from this dataset.

I think you don’t have the way to shape a button from a custom set of vertices? Or is a button always a Rect?

Hm, that is true, i had not thought of that.
Which component contains the generated mesh, i assume the CanvasRenderer?
It might also be possible (easier?) to just recalculate the UVs from the slicing settings of the sprite itself, and only do that if the image is in fact set to sliced. That would also eliminate the need for a manual toggle.

As it bugged me, i just went ahead and updated the script to handle sliced sprites as well.

using UnityEngine;
using UnityEngine.UI;

[RequireComponent(typeof(RectTransform))]
[RequireComponent(typeof(Image))]
public class RaycastMask : MonoBehaviour, ICanvasRaycastFilter
{
    private Image _image;
    private Sprite _sprite;

    void Start ()
    {
        _image = GetComponent<Image>();
    }
 
    public bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera)
    {
        _sprite = _image.sprite;

        var rectTransform = (RectTransform)transform;
        Vector2 localPositionPivotRelative;
        RectTransformUtility.ScreenPointToLocalPointInRectangle((RectTransform) transform, sp, eventCamera, out localPositionPivotRelative);

        // convert to bottom-left origin coordinates
        var localPosition = new Vector2(localPositionPivotRelative.x + rectTransform.pivot.x*rectTransform.rect.width,
            localPositionPivotRelative.y + rectTransform.pivot.y*rectTransform.rect.height);
     
        var spriteRect = _sprite.textureRect;
        var maskRect = rectTransform.rect;

        var x = 0;
        var y = 0;
        // convert to texture space
        switch (_image.type)
        {
         
            case Image.Type.Sliced:
            {
                var border = _sprite.border;
                // x slicing
                if (localPosition.x < border.x)
                {
                    x = Mathf.FloorToInt(spriteRect.x + localPosition.x);
                }
                else if (localPosition.x > maskRect.width - border.z)
                {
                    x = Mathf.FloorToInt(spriteRect.x + spriteRect.width - (maskRect.width - localPosition.x));
                }
                else
                {
                    x = Mathf.FloorToInt(spriteRect.x + border.x +
                                         ((localPosition.x - border.x)/
                                         (maskRect.width - border.x - border.z)) *
                                         (spriteRect.width - border.x - border.z));
                }
                // y slicing
                if (localPosition.y < border.y)
                {
                    y = Mathf.FloorToInt(spriteRect.y + localPosition.y);
                }
                else if (localPosition.y > maskRect.height - border.w)
                {
                    y = Mathf.FloorToInt(spriteRect.y + spriteRect.height - (maskRect.height - localPosition.y));
                }
                else
                {
                    y = Mathf.FloorToInt(spriteRect.y + border.y +
                                         ((localPosition.y - border.y) /
                                         (maskRect.height - border.y - border.w)) *
                                         (spriteRect.height - border.y - border.w));
                }
            }
                break;
            case Image.Type.Simple:
            default:
                {
                    // conversion to uniform UV space
                    x = Mathf.FloorToInt(spriteRect.x + spriteRect.width * localPosition.x / maskRect.width);
                    y = Mathf.FloorToInt(spriteRect.y + spriteRect.height * localPosition.y / maskRect.height);
                }
                break;
        }

        // destroy component if texture import settings are wrong
        try
        {
            return _sprite.texture.GetPixel(x,y).a > 0;
        }
        catch (UnityException e)
        {
            Debug.LogError("Mask texture not readable, set your sprite to Texture Type 'Advanced' and check 'Read/Write Enabled'");
            Destroy(this);
            return false;
        }
    }
}

github
Example mask: masking uGUI elements with sliced masks - Album on Imgur

The example worked completely fine without doing anything else compared to simple sprites.

9 Likes

Super sexy :slight_smile:

I somehow can’t get the script to work properly. The “collider” is always messed up, and somewhere completely different than where my actual button is.
Did i do something wrong? I just parented my button under my mask object. The mask object has a mask component, an image and the RaycastMask script. My texture is set to Read/Write Enabled.
I also added a screenshot.
Do i need to change something in the Graphic Raycaster on my Canvas as well?

I just re-tried the script in a clean project with a random mask, and it works fine for me with beta 20.



For the import settings i just set it to UI sprite, and then to advanced to enable read/write.

Can you do a minimum working example (or more like failing example) of your problem, and upload it somewhere?

2 Likes

My god, i figured it out! The ‘Mesh Type’ was set to Tight, it needs to be Full Rect -.-
I think they introduced these settings just recently…
Thanks for all your help senritsu, works like a charm now.

2 Likes

Good to hear that you got it to work :slight_smile:
I wonder why those settings are not available in my screenshot though :open_mouth: Is it a pro thing? Or has that to do with enabling the sprite packer or something in the editor settings?

great script, though I found out the hard way that it doesn’t support the Preserve Aspect option for images.
I tried doing it myself but I couldn’t figure it out.

What’s the advantage of mesh type Tight over Full Rect, for 2d Sprites?

someone at unite 2014 did a pretty good thing on that (8m32s):

1 Like