Question: Should i start writing UI-scaling-functions for the SpriteRenderer or will the Unity-UI get an update to support non-quad geometry at some point in the future?
Background:
I want to use big circles as analog-pad indicators. Obviously this causes overdraw because the UI-System creates only quads (i can see the fps drop on the device while those circle images are active).
The solution would be to have a mesh-geometry similar to the SpriteRenderer, that cuts out most transparent areas. You can actually use a SpriteRenderer in the UI but there are some restrictions:
SpriteRenderer will not be visible in ScreenSpace-Overlay mode, you need to use ScreenSpace-Camera
SpriteRenderer completely ignores the RectTransform except for centering in it, so scaling has to be done by yourself, but that can easily be done with some functions
At least one additional Drawcall if i use both normal GUI-Image and SpriteRenderers within the UI - but that is much less of a performance hit than the fillrate
Picture:
left circle: UI-Image, right circle: SpriteRenderer
left side: Shaded Wirframe, right side: Overdraw
I ended up writing a costum scaling function for my UI-Spriterenderers. Now they look exactly like the previous UI-Images. Scaling function is simple:
public static SpriteRenderer AddScaledUISpriteRenderer(RectTransform rectTrans, int? sortingOrder = null){
//Adds a child GameObject to the UI-GameObject that copies the sprite of the image
//Needs to be a child because we need to scale it but still anchor its position to the old Object
//If we would add a SpriteRenderer to the actual Image-Object and scale it accordingly, children of it will be scaled too.
float pxWidth = rectTrans.rect.width; //width of the scaled UI-Object in pixel
float pxHeight = rectTrans.rect.height; //height of the scaled UI-Object in pixel
Image uiImage = rectTrans.GetComponent<Image>();
float spriteSizeX = uiImage.sprite.bounds.size.x; //width of the unscaled sprite in pixel
float spriteSizeY = uiImage.sprite.bounds.size.y; //height of the unscaled sprite in pixel
//scale that needs to be applied to the SpriteRender-Gameobject to give it the same screen size as the UI-Image
float scaleX = pxWidth /spriteSizeX;
float scaleY = pxHeight /spriteSizeY;
//create new SpriteGameObject in UI
GameObject spriteGO = new GameObject();
spriteGO.name = rectTrans.name+"Sprite"; //give it a name
spriteGO.layer = LayerMask.NameToLayer("UI"); //culling layer, if needed
spriteGO.transform.localScale = new Vector3(scaleX, scaleY, 1F); //scale the SpriteRenderer
RectTransform sRect = spriteGO.AddComponent<RectTransform>(); //add a rect Transform to replace the normal Transform
sRect.SetParent(rectTrans, false); //make the image Object the new parent, so this will move with it
SpriteRenderer spriteR = spriteGO.AddComponent<SpriteRenderer>(); //Add a sprite Renderer to the new Object
spriteR.sprite = uiImage.sprite; //copy the Image's sprite
spriteR.color = uiImage.color; //copy the Image's coloration
spriteR.enabled = uiImage.enabled; //copy enabled state
spriteR.sortingOrder = sortingOrder ?? 1; //needed to be at least 1 if the UI is drawn above a render texture
uiImage.enabled = false; //disable the old UI-Image
return spriteR;
}
I would like to contribute.
As long as we keep the sprite or even a Mesh quad parented to some parent-rect, we can stretch that parent rect and sprite will follow it.
Just make sure to attach the following script to the child sprite (or child mesh quad)
using UnityEngine;
// Attach this script onto a sprite renderer.
// Stretches a sprite to follow corners of parent rect.
// Make sure it's parented to a "dummy" rectTransform and stretch that instead of our tranform.
//
// IgorAherne March 2017
// Marrt July 2015
// https://forum.unity3d.com/threads/overdraw-spriterenderer-in-ui.339912/#post-3009616
// https://forum.unity3d.com/threads/overdraw-spriterenderer-in-ui.339912/#post-3009616
[ExecuteInEditMode, RequireComponent(typeof(RectTransform))]
public class QuadScalerRelativeToAnchors : MonoBehaviour {
[SerializeField, HideInInspector] RectTransform _myRectTransform;
[SerializeField, HideInInspector] RectTransform _parentRectTransf;
[SerializeField, HideInInspector] SpriteRenderer _mySpriteRenderer;
void Reset() {
Start();
}
void Start() {
_parentRectTransf = transform.parent as RectTransform;
_mySpriteRenderer = GetComponent(typeof(SpriteRenderer)) as SpriteRenderer;
_myRectTransform = transform as RectTransform;
}
void Update() {
#if UNITY_EDITOR
//if we are in execute in edit mode, in editor, we might not have some references yet.
//so establish them if needed:
if(_mySpriteRenderer == null || _parentRectTransf == null || _myRectTransform == null) {
Start();
}
#endif
keepScaleRelativeToParent();
}
public void keepScaleRelativeToParent() {
float pxWidth = _parentRectTransf.rect.width; //width of the scaled UI-Object in pixel
float pxHeight = _parentRectTransf.rect.height; //height of the scaled UI-Object in pixel
if (float.IsNaN(pxHeight) || float.IsNaN(pxWidth)) {
//unity hasn't not yet initialized (usually happens during start of the game)
return;
}
float spriteSizeX = 1;
float spriteSizeY = 1;
if (_mySpriteRenderer) {
spriteSizeX = _mySpriteRenderer.sprite.bounds.size.x; //width of the unscaled sprite in pixel
spriteSizeY = _mySpriteRenderer.sprite.bounds.size.y; //height of the unscaled sprite in pixel
}
//scale that needs to be applied to the SpriteRender-Gameobject to give it the same screen size as the UI-Image
float scaleX = pxWidth / spriteSizeX;
float scaleY = pxHeight / spriteSizeY;
//create new SpriteGameObject in UI
this.gameObject.layer = LayerMask.NameToLayer("UI"); //culling layer, if needed
_myRectTransform.localScale = new Vector3(scaleX, scaleY, 1F); //scale our SpriteRenderer
//position self in the middle of parent
_myRectTransform.anchoredPosition3D = new Vector3(0,0, _myRectTransform.anchoredPosition3D.z);
_myRectTransform.anchorMin = Vector2.one * 0.5f;
_myRectTransform.anchorMax = Vector2.one * 0.5f;
float myLocalScale_x = Mathf.Max(0.0001f, _myRectTransform.localScale.x);
float myLocalScale_y = Mathf.Max(0.0001f, _myRectTransform.localScale.y);
_myRectTransform.sizeDelta = new Vector2( Mathf.Max(0.0001f, _parentRectTransf.rect.width / myLocalScale_x),
Mathf.Max(0.0001f, _parentRectTransf.rect.height / myLocalScale_y) );
}
}
@Marrt Could you elaborate please, through which component and in which Unity Versions UI-Sprites are supported natively now?
Edit: Nevermind:
the bool useSpriteMesh has the feature that we need.
The feature exists in Unity 2018.4 and upwards.
Hint: you have to assign as sprite in order to set the field in the inspector, or you need to switch the inspector to debug mode.