关于圆角矩形image的实现,有很多方法。比如,重绘UI的Mesh、Shader实现。之前也写过相关的文章,今天要讲的是shader实现,和之前shader实现的方法有一些不一样,并且我写成一个UI组件,可以直接使用。
之前Shader是用RoundedRectangle节点实现的,这个虽然简单,但是有个缺点,就是只能是正方形的,长方形圆角就会变形。后来网上搜了一下,大部分也都是用RoundedRectangle节点实现的,后来问了DeepSeek,给出了另外一种解决方案,就是用图片的宽高计算四个角的圆角的中心点坐标,然后使用算法将四个圆角设置透明。这就是基本原理,下面直接上ShaderGraph的连线图。文章源自大腿Plus-https://www.zhaoshijun.com/archives/2082
文章源自大腿Plus-https://www.zhaoshijun.com/archives/2082
先说一下属性文章源自大腿Plus-https://www.zhaoshijun.com/archives/2082
1,MainTex:这个是固定的,是用来让UI的Image组件设置图片用的,不能更改文章源自大腿Plus-https://www.zhaoshijun.com/archives/2082
2,Radus:圆角半径,这个是圆角的大小,范围是0到最小边长的一半文章源自大腿Plus-https://www.zhaoshijun.com/archives/2082
3,Size:图片的宽高文章源自大腿Plus-https://www.zhaoshijun.com/archives/2082
在说一下Shader的连线的节点文章源自大腿Plus-https://www.zhaoshijun.com/archives/2082
1,先说最右边上面的MainTex属性连接的节点SampleTexture2D,这个节点主要是用来显示UI的Image上的Sprite图片的。名称是固定的。SampleTexture2D的Texture和MainTex属性连接,RGBA与BaseColor连接文章源自大腿Plus-https://www.zhaoshijun.com/archives/2082
2,然后下面的圆角相关节点(从左到右)文章源自大腿Plus-https://www.zhaoshijun.com/archives/2082
(1)UV节点:Out连接Subtract节点的A文章源自大腿Plus-https://www.zhaoshijun.com/archives/2082
(2)Subtract节点(1)节点:B连接一个Float的out,A连接UV节点,Out连接Absolute节点的In
(3)Float节点:Float节点的X是固定的0.5,Out连接Subtract节点(1)和(2)B
(4)Absolute节点:In连接Subtract节点,Out连接Subtract节点(2)的A
(5)Subtract节点(2)节点:B与Float节点Out连接,A与Absolute节点连接,Out连接Muktiply节点的B
(6)Multiply节点:A与Size属性连接,B与Subtract节点(2)的Out连接,Out与Add节点的A连接
(7)Add节点:A与Multiply节点的Out连接,B与Size属性连接,Out与Maximun节点的A连接
(8)Maximun节点:A与Add节点的Out连接,B的x和y为0,Out与Length节点的In连接
(9)Length节点:In与Maximun节点的Out连接,Out与Subtract节点(3)节点A连接
(10)Subtract节点(3)节点:A与Length节点的Out连接,B与Radius属性连接,Out与SmoothStep节点的In连接
(11)SmoothStep节点:In与Subtract节点(3)节点的Out连接,Edge1和Edge2分别是0和0.01,Out与OneMinus节点连接
(12)OneMinus节点:In与SmoothStep节点的Out连接,Out与Alpha连接
接下来就是用代码控制这些属性了,如果每次使用都要单独设置大小和圆角半径,就很麻烦,所以我就自己写了个ImagePro组件自动设置圆角半径和宽高。不管是Editor还是Runtime,都是自动设置的,包括在动态设置RectTransform大小。
我实现的方式是继承Image实现了自动添加材质和设置材质属性的方法,重写了UI的RectTransfrom改变的方法OnRectTransformDimensionsChange,去设置材质属性。并且重写了ImageEditor方法,在Editor下也能自动设置。在写这个组件的过程中遇到了一个小问题,就是在ShaderGraph把属性隐藏之后,Image的Sprite为Null的时候就会报一个找不到MainTex属性的错误,这个具体原因不太明白,其他隐藏的属性我在设置的时候也没有报错,唯独MainTex,后来我看Image源码,在Sprite为Null时,会执行UpdateMaterial方法,这个方法找到父类的实现,这时候会设置一个材质,并且设置一张默认白色图片,我猜可能这个时候出了某些问题导致报错。后来我重写了UpdateMaterial方法 在sprite为Null的时候重新设置了材质和图片,并且不执行父类的内容。好了,下面直接上代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
namespace UnityEngine.UI { public class ImagePro : Image { [SerializeField] [Range(0, 1)] private float _Radius; public float Radius { get => _Radius; set { _Radius = value; Refresh(); } } private static readonly int SizeID = Shader.PropertyToID("_Size"); private static readonly int RadiusID = Shader.PropertyToID("_Radius"); private Shader _shader = null; private Material _material = null; private Material Material { get { if (_shader == null) { _shader = Shader.Find($"Shader Graphs/RoundedRectangle"); } if (_material == null) { _material = new Material(_shader) { name = "Rounded Rectangle" }; } return _material; } } protected override void Awake() { base.Awake(); Refresh(); } protected override void UpdateMaterial() { Refresh(); if (sprite == null) { canvasRenderer.materialCount = 1; canvasRenderer.SetMaterial(material, 0); canvasRenderer.SetTexture(Texture2D.whiteTexture); return; } base.UpdateMaterial(); } protected override void OnRectTransformDimensionsChange() { base.OnRectTransformDimensionsChange(); Refresh(); } private void Refresh() { if (material != Material) { material = Material; } var maxRadius = Mathf.Min(rectTransform.rect.size.x, rectTransform.rect.size.y) / 2; material.SetFloat(RadiusID, Radius * maxRadius); material.SetVector(SizeID, rectTransform.rect.size); } } } |
下面是Editor代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 |
using UnityEngine; using UnityEditor.AnimatedValues; using UnityEditor.SceneManagement; using UnityEngine.UI; using UnityEngine.EventSystems; namespace UnityEditor.UI { [CustomEditor(typeof(ImagePro), true)] [CanEditMultipleObjects] public class ImageProEditor : ImageEditor { private SerializedProperty radius; private ImagePro targetComponent; SerializedProperty m_Sprite; SerializedProperty m_PreserveAspect; SerializedProperty m_UseSpriteMesh; SerializedProperty m_Type; AnimBool m_ShowTypePro; bool m_bIsDrivenPro; protected override void OnEnable() { base.OnEnable(); EditorApplication.update += Excute; targetComponent = target as ImagePro; radius = serializedObject.FindProperty("_Radius"); m_Sprite = serializedObject.FindProperty("m_Sprite"); m_Type = serializedObject.FindProperty("m_Type"); m_PreserveAspect = serializedObject.FindProperty("m_PreserveAspect"); m_UseSpriteMesh = serializedObject.FindProperty("m_UseSpriteMesh"); m_ShowTypePro = new AnimBool(m_Sprite.objectReferenceValue != null); m_ShowTypePro.valueChanged.AddListener(Repaint); } protected override void OnDisable() { base.OnDisable(); EditorApplication.update -= Excute; } public override void OnInspectorGUI() { serializedObject.Update(); var rect = targetComponent.GetComponent<RectTransform>(); m_bIsDrivenPro = (rect.drivenByObject as Slider)?.fillRect == rect; SpriteGUI(); AppearanceControlsGUI(); RaycastControlsGUI(); EditorGUILayout.PropertyField(radius); MaskableControlsGUI(); m_ShowTypePro.target = m_Sprite.objectReferenceValue != null; if (EditorGUILayout.BeginFadeGroup(m_ShowTypePro.faded)) TypeGUI(); EditorGUILayout.EndFadeGroup(); SetShowNativeSize(false); if (EditorGUILayout.BeginFadeGroup(m_ShowNativeSize.faded)) { EditorGUI.indentLevel++; if ((Image.Type)m_Type.enumValueIndex == Image.Type.Simple) EditorGUILayout.PropertyField(m_UseSpriteMesh); EditorGUILayout.PropertyField(m_PreserveAspect); EditorGUI.indentLevel--; } EditorGUILayout.EndFadeGroup(); NativeSizeButtonGUI(); serializedObject.ApplyModifiedProperties(); } private void SetShowNativeSize(bool instant) { var type = (Image.Type)m_Type.enumValueIndex; var showNativeSize = (type == Image.Type.Simple || type == Image.Type.Filled) && m_Sprite.objectReferenceValue != null; base.SetShowNativeSize(showNativeSize, instant); } private void Excute() { if (targetComponent != null) { var method = targetComponent.GetType().GetMethod("Refresh", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); if (method != null) { method.Invoke(targetComponent, null); } } } [MenuItem("GameObject/UI/ImagePro")] public static void ImagePro() { var parent = GetOrCreateCanvasGameObject(); var go = new GameObject("ImagePro"); go.transform.SetParent(parent.transform, false); go.AddComponent<RectTransform>(); go.AddComponent<CanvasRenderer>(); go.AddComponent<ImagePro>(); go.layer = 5; Selection.activeGameObject = go; } private static bool IsValidCanvas(Canvas canvas) { if (canvas == null || !canvas.gameObject.activeInHierarchy) return false; // It's important that the non-editable canvas from a prefab scene won't be rejected, // but canvases not visible in the Hierarchy at all do. Don't check for HideAndDontSave. if (EditorUtility.IsPersistent(canvas) || (canvas.hideFlags & HideFlags.HideInHierarchy) != 0) return false; if (StageUtility.GetStageHandle(canvas.gameObject) != StageUtility.GetCurrentStageHandle()) return false; return true; } private static GameObject GetOrCreateCanvasGameObject() { var selected = Selection.activeGameObject; // Try to find a gameobject that is the selected GO or one if its parents. var canvas = selected?.GetComponentInParent<Canvas>(); if (IsValidCanvas(canvas)) return selected; // No canvas in selection or its parents? Then use any valid canvas. // We have to find all loaded Canvases, not just the ones in main scenes. var canvasArray = StageUtility.GetCurrentStageHandle().FindComponentsOfType<Canvas>(); foreach (var t in canvasArray) if (IsValidCanvas(t)) return t.gameObject; // No canvas in the scene at all? Then create a new one. return CreateNewUI(); } private static GameObject CreateNewUI() { // Root for the UI var root = new GameObject("Canvas") { layer = LayerMask.NameToLayer("UI") }; var canvas = root.AddComponent<Canvas>(); canvas.renderMode = RenderMode.ScreenSpaceOverlay; root.AddComponent<CanvasScaler>(); root.AddComponent<GraphicRaycaster>(); // Works for all stages. StageUtility.PlaceGameObjectInCurrentStage(root); var customScene = false; var prefabStage = PrefabStageUtility.GetCurrentPrefabStage(); if (prefabStage != null) { root.transform.SetParent(prefabStage.prefabContentsRoot.transform, false); customScene = true; } Undo.RegisterCreatedObjectUndo(root, "Create " + root.name); // If there is no event system add one... // No need to place event system in custom scene as these are temporary anyway. // It can be argued for or against placing it in the user scenes, // but let's not modify scene user is not currently looking at. if (!customScene) CreateEventSystem(false, null); return root; } private static void CreateEventSystem(bool select, GameObject parent) { var stage = parent == null ? StageUtility.GetCurrentStageHandle() : StageUtility.GetStageHandle(parent); var esys = stage.FindComponentOfType<EventSystem>(); if (esys == null) { var eventSystem = new GameObject("EventSystem"); if (parent == null) StageUtility.PlaceGameObjectInCurrentStage(eventSystem); else GameObjectUtility.SetParentAndAlign(eventSystem, parent); esys = eventSystem.AddComponent<EventSystem>(); eventSystem.AddComponent<StandaloneInputModule>(); Undo.RegisterCreatedObjectUndo(eventSystem, "Create " + eventSystem.name); } if (select && esys != null) { Selection.activeGameObject = esys.gameObject; } } } } |
在Editor代码中还增加了ImagePro组件的菜单项,可以自动生成ImagePro。


评论