Unity Image - Mirror

1. Why use mirroring?

During the game development process, in order to save the size of art picture resources, the art artist will cut the pictures that are the same on both sides in half for processing. As shown below, a button requires 400 * 236, but the art only needs to be cut into a size of 74 * 236. This way the gallery can accommodate more pictures. It also takes up less memory.

2. Implementation plan

  1. Copy an image and then change the scale to -1. This method is cumbersome and requires adding an extra image, which is also very inconvenient to operate. Nothing much to talk about.
  2. Copy the original image vertices and perform symmetry processing. As shown below.

3. Where to get mesh vertices? Modify vertices?

BaseMeshEffect : It is an abstract class used to implement mesh effects to implement the IMeshModifier interface. It is the base class for Shadow, Outline and other effects.
From the Unity uGUI principle analysis - Graphic, we can know that when Graphic executes DoMeshGeneration, it will obtain all components that implement IMeshModifier on the current GameObject. And the ModifyMesh method will be called to modify the Mesh data of the current Graphic. The class structure diagram of BaseMeshEffect is as follows:

We write a MirrorImage to inherit BaseMeshEffect, get VertexHelper, obtain the vertex stream data of the mesh, and then perform the following mirroring and mapping operations:

using System.Collections.Generic;
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.UI;

public enum MirrorType
{
    Horizontal,
    Vertical,
    HorizontalAndVertical
}

[RequireComponent(typeof(Image))]
public class MirrorImage : BaseMeshEffect
{
    public MirrorType mirrorType = MirrorType.Horizontal;

    public Dictionary<Image.Type, IMirror> MirrorDic = new Dictionary<Image.Type, IMirror>()
    {
        {Image.Type.Simple, new SimpleMirror()},
        {Image.Type.Sliced, new SlicedMirror()},
        {Image.Type.Tiled, new TiledMirror()},
        {Image.Type.Filled, new FilledMirror()},
    };

    public Image image => graphic as Image;

    public override void ModifyMesh(VertexHelper vh)
    {
        if (!IsActive()) return;
        Image.Type imageType = (graphic as Image).type;
        List<UIVertex> vertices = new List<UIVertex>();
        vh.GetUIVertexStream(vertices);
        vh.Clear();
        
        MirrorDic[imageType].Draw(image, vertices, mirrorType);
        vh.AddUIVertexTriangleStream(vertices);
    }

    [Button("Set Native Size")]
    public void SetNativeSize()
    {
        if (image.sprite != null)
        {
            float w = image.sprite.rect.width / image.pixelsPerUnit;
            float h = image.sprite.rect.height / image.pixelsPerUnit;
            image.rectTransform.anchorMax = image.rectTransform.anchorMin;
            float x = mirrorType == MirrorType.Horizontal || mirrorType == MirrorType.HorizontalAndVertical ? 2 : 1;
            float y = mirrorType == MirrorType.Vertical || mirrorType == MirrorType.HorizontalAndVertical ? 2 : 1;
            image.rectTransform.sizeDelta = new Vector2(w * x, h * y);
            image.SetAllDirty();
        }
    }
}

Of course, in addition to inheriting BaseMeshEffect, you can also directly inherit Image and override OnPopulateMesh.

4. How to implement vertex mirroring?

It is very simple to assume that the center is center (0, 0), and the point A (-1, 0) that needs to be horizontally mirrored is the point B (-1, 0) after mirroring. It needs to satisfy the distance from A to Center == B to center. distance:

So we first find the position of the center point of the mirror image. Because the rectangle has its own center point position, we need to find the coordinates of the center point position of the mirror image under the changed rectangle. This may be a bit confusing. Look at the figure below:

The width of the rectangle is w: 100, h: 100, and the center point of the rectangle itself (blue circle) is called point O. In Unity, x and y in Rect represent the point of the sitting corner ( -75,-50),

If you don’t understand Rect(x,y), you can check out GPT’s answer, which may explain it more clearly than me:

Then mirror the real center point coordinates

  • center.x = rect.x + rect.width;
  • center.y = rect.y + rect.height;

Then the point A to be mirrored will be B after mirroring. You need to find the length from A to center and then negate it + the offset between the center point of the rectangle and the center point of the mirror.

B.x = -(A.x - center.x) + (rect.x+rect.width/2)

B.y = -(A.y - center.y) + (rect.x+rect.width/2)

After the logical analysis, let’s look directly at the code:


using System.Collections.Generic;
using UnityEngine;

public static class MirrorUtlis
{
    public static void Mirror(Rect rect,List<UIVertex> uiVertices,MirrorType type)
    {
        int count = uiVertices.Count;
        switch (type)
        {
            case MirrorType.Horizontal:
                Mirror(rect, uiVertices, type, count);
                break;
            case MirrorType.Vertical:
                Mirror(rect, uiVertices, type, count);
                break;
            case MirrorType.HorizontalAndVertical:
                Mirror(rect, uiVertices, MirrorType.Horizontal, count);
                Mirror(rect, uiVertices, MirrorType.Vertical, 2 * count);
                break;
        }
        RemoveVertices(uiVertices);
    }

    private static void Mirror(Rect rect, List<UIVertex> uiVertices, MirrorType type, int count)
    {
        for (int i = 0; i < count; i++)
        {
            UIVertex vertex = uiVertices[i];
            switch (type)
            {
                case MirrorType.Horizontal:
                    vertex = HorizontalMirror(rect, vertex);
                    break;
                case MirrorType.Vertical:
                    vertex = VerticalMirror(rect, vertex);
                    break;
                case MirrorType.HorizontalAndVertical:
                    vertex = HorizontalMirror(rect, vertex);
                    vertex = VerticalMirror(rect, vertex);
                    break;
            }
            uiVertices.Add(vertex);
        }
    }

    private static UIVertex HorizontalMirror(Rect rect, UIVertex vertex)
    {
        float center = rect.width / 2 + rect.x;
        vertex.position.x = -(vertex.position.x - center) + rect.x + rect.width/2;
        return vertex;
    }
    
    private static UIVertex VerticalMirror(Rect rect, UIVertex vertex)
    {
        float center = rect.height / 2 + rect.y;
        vertex.position.y = -(vertex.position.y - center) + rect.y + rect.height/2;
        return vertex;
    }
    
    // 移除构不成三角形的顶点
    private static void RemoveVertices(List<UIVertex> uiVertices)
    {
        int end = uiVertices.Count;

        for (int i = 2; i < end; i += 3)
        {
            UIVertex v1 = uiVertices[i];
            UIVertex v2 = uiVertices[i - 1];
            UIVertex v3 = uiVertices[i - 2];

            if (v1.position == v2.position ||
                v1.position == v3.position ||
                v2.position == v3.position)
            {
                // 移动到尾部
                ChaneVertices(uiVertices, i - 1, end - 3);
                ChaneVertices(uiVertices, i - 2, end - 2);
                ChaneVertices(uiVertices, i, end - 1);
                end -= 3;
            }
        }
        
        if(end < uiVertices.Count)
            uiVertices.RemoveRange(end,uiVertices.Count - end);
    }
    
    private static void ChaneVertices(List<UIVertex> uiVertices,int a,int b)
    {
        (uiVertices[a], uiVertices[b]) = (uiVertices[b], uiVertices[a]);
    }
}

Before vertex mirroring, we need to perform vertex mapping on the vertices. When should we map them?

As shown in the picture above, the original picture is the size of the white area, and the vertices are the four vertices of the white picture. Because it is symmetrical, half of the position needs to be left to add mapped vertices.

Vertex mapping in different modes requires different processing. There are several modes in Unity:

  • Simple : As shown in the figure above, it is relatively simple to divide the position of the vertex by 2
  • Sliced : In the nine-square grid mode, because the vertex position of the nine-square grid must be retained and the retained position of the nine-square grid cannot be deformed, it cannot be processed directly by dividing by 2, and translation processing is required. The specific implementation is discussed below.
  • Filed : There is no need to mirror the mesh vertices in tiling mode, because the vertices of the image under tiling have already been added. We only need to mirror the UVs.
  • Filled : This mode has not been implemented for the time being. We will study it when we have time in the future.

Since there are multiple modes of processing, we will implement interfaces to facilitate our management: define an IMirror interface:

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public interface IMirror
{
    void Draw(Image image,List<UIVertex> uiVertices,MirrorType type);
}

Vertex mapping under Simpe:

public class SimpleMirror : IMirror
{
    public void Draw(Image image,List<UIVertex> uiVertices,MirrorType type)
    {
        ChangeVertices(image.rectTransform.rect,uiVertices,type);
        MirrorUtlis.Mirror(image.rectTransform.rect,uiVertices,type);
    }

    // 改变原有的顶点位置 (做一半映射) 如果是 Horizontal:左半部分 Vertical:上半部分 HorizontalAndVertical:左上四分之一
    private void ChangeVertices(Rect rect,List<UIVertex> uiVertices,MirrorType type)
    {
        for (int i = 0; i < uiVertices.Count; i++)
        {
            UIVertex vertex = uiVertices[i];
            switch (type)
            {
                case MirrorType.Horizontal:
                    vertex = HorizontalVertex(rect, vertex);
                    break;
                case MirrorType.Vertical:
                    vertex = VerticalVertex(rect, vertex);
                    break;
                case MirrorType.HorizontalAndVertical:
                    vertex = HorizontalVertex(rect, vertex);
                    vertex = VerticalVertex(rect, vertex);
                    break;
            }
            uiVertices[i] = vertex;
        }
    }

    // 水平映射
    private UIVertex HorizontalVertex(Rect rect,UIVertex vertex)
    {
        vertex.position.x = (vertex.position.x + rect.x) / 2;// - rect.width / 2;
        return vertex;
    }
    
    // 垂直映射
    private UIVertex VerticalVertex(Rect rect,UIVertex vertex)
    {
        vertex.position.y = (rect.y + vertex.position.y) / 2 + rect.height / 2;
        return vertex;
    }
}

Vertex mapping under Sliced:

We can see the main methods mapped as follows:

// 水平映射
private UIVertex HorizontalVertex(Rect rect,UIVertex vertex)
{
    if (vertex.position.x == s_VertScratch[0].x || vertex.position.x == s_VertScratch[1].x) return vertex;
    vertex.position.x -= rect.width / 2;
    return vertex;
}

 The timing is very simple, that is, directly translate x by the width of the rectangle/2. What is more difficult is that we need to know which vertices need to be translated?

This requires looking at how the source code of Image implements vertex processing:

        /// <summary>
        /// Generate vertices for a 9-sliced Image.
        /// </summary>
        private void GenerateSlicedSprite(VertexHelper toFill)
        {
            if (!hasBorder)
            {
                GenerateSimpleSprite(toFill, false);
                return;
            }

            Vector4 outer, inner, padding, border;

            if (activeSprite != null)
            {
                outer = Sprites.DataUtility.GetOuterUV(activeSprite);
                inner = Sprites.DataUtility.GetInnerUV(activeSprite);
                padding = Sprites.DataUtility.GetPadding(activeSprite);
                border = activeSprite.border;
            }
            else
            {
                outer = Vector4.zero;
                inner = Vector4.zero;
                padding = Vector4.zero;
                border = Vector4.zero;
            }

            Rect rect = GetPixelAdjustedRect();

            Vector4 adjustedBorders = GetAdjustedBorders(border / multipliedPixelsPerUnit, rect);
            padding = padding / multipliedPixelsPerUnit;

            s_VertScratch[0] = new Vector2(padding.x, padding.y);
            s_VertScratch[3] = new Vector2(rect.width - padding.z, rect.height - padding.w);

            s_VertScratch[1].x = adjustedBorders.x;
            s_VertScratch[1].y = adjustedBorders.y;

            s_VertScratch[2].x = rect.width - adjustedBorders.z;
            s_VertScratch[2].y = rect.height - adjustedBorders.w;

            for (int i = 0; i < 4; ++i)
            {
                s_VertScratch[i].x += rect.x;
                s_VertScratch[i].y += rect.y;
            }

            ......
            ......
        }

I intercepted this incomplete source code, calculated the position information of the image, and then wrote the 4 vertex position information into the s_VertScratch array in order. These four positions correspond to the following positions:

That is, the nine-square grid is mapped to four positions on the Image vertex after cropping, so when we do horizontal mapping, we only need to translate the vertices equal to the 3 and 4 x-axes, and the vertices equal to the 1 and 2 x-axes retain their original positions. As shown in the figure below, it can be clearly seen

As for how to calculate the vertices of the four nine-square grid mapping, just copy the implementation of the Image source code.

Post the complete code:

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class SlicedMirror : IMirror
{
    private Image image;
    
    // 九宫格的四个分界点
    private Vector2[] s_VertScratch = new Vector2[4];

    public void Draw(Image image,List<UIVertex> uiVertices,MirrorType type)
    {
        this.image = image;
        SetVertScratch();
        ChangeVertices(image.rectTransform.rect,uiVertices,type);
        MirrorUtlis.Mirror(image.rectTransform.rect,uiVertices,type);
    }

    private void ChangeVertices(Rect rect,List<UIVertex> uiVertices,MirrorType type)
    {
        for (int i = 0; i < uiVertices.Count; i++)
        {
            UIVertex vertex = uiVertices[i];
            switch (type)
            {
                case MirrorType.Horizontal:
                    vertex = HorizontalVertex(rect, vertex);
                    break;
                case MirrorType.Vertical:
                    vertex = VerticalVertex(rect, vertex);
                    break;
                case MirrorType.HorizontalAndVertical:
                    vertex = HorizontalVertex(rect, vertex);
                    vertex = VerticalVertex(rect, vertex);
                    break;
            }
            uiVertices[i] = vertex;
        }
    }

    // 水平映射
    private UIVertex HorizontalVertex(Rect rect,UIVertex vertex)
    {
        if (vertex.position.x == s_VertScratch[0].x || vertex.position.x == s_VertScratch[1].x) return vertex;
        vertex.position.x -= rect.width / 2;
        return vertex;
    }
    
    // 垂直映射
    private UIVertex VerticalVertex(Rect rect,UIVertex vertex)
    {
        if (vertex.position.y == s_VertScratch[2].y || vertex.position.y == s_VertScratch[3].y) return vertex;
        vertex.position.y += rect.height / 2;
        return vertex;
    }
    
    private void SetVertScratch()
    {
        Sprite activeSprite = image.sprite;
        Vector4 padding, border;

        if (activeSprite != null)
        {
            padding = UnityEngine.Sprites.DataUtility.GetPadding(activeSprite);
            border = activeSprite.border;
        }
        else
        {
            padding = Vector4.zero;
            border = Vector4.zero;
        }

        Rect rect = image.GetPixelAdjustedRect();

        var multipliedPixelsPerUnit = image.pixelsPerUnit * image.pixelsPerUnitMultiplier;
        Vector4 adjustedBorders = GetAdjustedBorders(border / multipliedPixelsPerUnit, rect);
        padding /= multipliedPixelsPerUnit;

        s_VertScratch[0] = new Vector2(padding.x, padding.y);
        s_VertScratch[3] = new Vector2(rect.width - padding.z, rect.height - padding.w);

        s_VertScratch[1].x = adjustedBorders.x;
        s_VertScratch[1].y = adjustedBorders.y;

        s_VertScratch[2].x = rect.width - adjustedBorders.z;
        s_VertScratch[2].y = rect.height - adjustedBorders.w;

        for (int i = 0; i < 4; ++i)
        {
            s_VertScratch[i].x += rect.x;
            s_VertScratch[i].y += rect.y;
        }
    }
    private Vector4 GetAdjustedBorders(Vector4 border, Rect adjustedRect)
    {
        Rect originalRect = image.rectTransform.rect;

        for (int axis = 0; axis <= 1; axis++)
        {
            float borderScaleRatio;
            if (originalRect.size[axis] != 0)
            {
                borderScaleRatio = adjustedRect.size[axis] / originalRect.size[axis];
                border[axis] *= borderScaleRatio;
                border[axis + 2] *= borderScaleRatio;
            }

            float combinedBorders = border[axis] + border[axis + 2];
            if (adjustedRect.size[axis] < combinedBorders && combinedBorders != 0)
            {
                borderScaleRatio = adjustedRect.size[axis] / combinedBorders;
                border[axis] *= borderScaleRatio;
                border[axis + 2] *= borderScaleRatio;
            }
        }
        return border;
    }
}

Tiled: Mapping in mode

In tiling mode, the vertices are all complete and do not need to be mirrored. You only need to modify the UV symmetry corresponding to each piece. We fix the starting position at 1 and do not flip it. Then

The vertex UV.y composed of 2 positions needs to be symmetrically processed

The vertex UV.x composed of 4 positions needs to be symmetrically processed

The vertices UV.x and y composed of 3 positions need to be symmetrically processed.

How to determine whether the UVs of certain vertices need to be symmetrical?
We know that a triangular surface is composed of three vertices, so we only need to find the center positions of the three vertices. Assume that the width and height of the sprite are w = 100, h = 100, and the center points of the triangular area formed are respectively

1 position => (50, 50),

2 positions => (50,150),

3 positions => (150,150),

4 positions => (150,50),

Our focus on the center

1 => x / W = 0.5

2 => x / W = 1.5

。。

For the result %2, the result 1 needs to be flipped;

How to flip UV
outerUv = UnityEngine.Sprites.DataUtility.GetOuterUV(image.sprite);

x and y in outerUV represent the point in the lower right corner, z represents the width, and w: represents the height. Assuming that the point after mirroring UV of A is B, then it satisfies:

The distance from A to (0, 0) == the distance from B to (0+z), so

Mirrored Ax = outer.z -( Ax - outerUv.x )

Post the complete code

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class TiledMirror : IMirror
{

    private Vector4 outerUv;
    public void Draw(Image image,List<UIVertex> uiVertices,MirrorType type)
    {
        outerUv = UnityEngine.Sprites.DataUtility.GetOuterUV(image.sprite);
        ChangeUv(uiVertices,type);
    }

    // uv翻转
    private void ChangeUv(List<UIVertex> uiVertices,MirrorType type)
    {
        Vector3 cellMinP = uiVertices[0].position;
        Vector3 cellMaxP = uiVertices[2].position;
        float w = cellMaxP.x - cellMinP.x;
        float h = cellMaxP.y - cellMinP.y;
        
        for (int i = 0; i < uiVertices.Count; i+= 3)
        {
            UIVertex v1 = uiVertices[i];
            UIVertex v2 = uiVertices[i+1];
            UIVertex v3 = uiVertices[i+2];
            
            float centerX = GetCenter(v1, v2, v3, true) - cellMaxP.x;
            float centerY = GetCenter(v1, v2, v3, false) - cellMaxP.y;
            
            bool changeX = Mathf.Ceil(centerX / w) % 2 != 0;
            bool changeY = Mathf.Ceil(centerY / h) % 2 != 0;
            
            if (changeX && (type == MirrorType.Horizontal || type == MirrorType.HorizontalAndVertical))
            {
  
                v1 = HorizontalUv(v1);
                v2 = HorizontalUv(v2);
                v3 = HorizontalUv(v3);
            }
            
            if (changeY && (type == MirrorType.Vertical || type == MirrorType.HorizontalAndVertical))
            {
                v1 = VerticalUv(v1);
                v2 = VerticalUv(v2);
                v3 = VerticalUv(v3);
            }
            
            uiVertices[i] = v1;
            uiVertices[i + 1] = v2;
            uiVertices[i + 2] = v3;
        }
    }

    // 获取三个顶点的中心位置
    private float GetCenter(UIVertex v1,UIVertex v2,UIVertex v3,bool isX)
    {
        float min = Mathf.Min(
            Mathf.Min(isX ? v1.position.x : v1.position.y,isX ? v2.position.x : v2.position.y),
            Mathf.Min(isX ? v1.position.x : v1.position.y,isX ? v3.position.x : v3.position.y));
        float max = Mathf.Max(
            Mathf.Max(isX ? v1.position.x : v1.position.y,isX ? v2.position.x : v2.position.y),
            Mathf.Max(isX ? v1.position.x : v1.position.y,isX ? v3.position.x : v3.position.y));
        return (min + max) / 2;
    }

    private UIVertex HorizontalUv(UIVertex vertex)
    {
        vertex.uv0.x = outerUv.x + outerUv.z - vertex.uv0.x;
        return vertex;
    }
    
    private UIVertex VerticalUv(UIVertex vertex)
    {
        vertex.uv0.y = outerUv.y + outerUv.w - vertex.uv0.y;
        return vertex;
    }
    
    
}

Summarize:

During the implementation process, it is basically not difficult to implement after clarifying the ideas, but you need to understand the implementation of Unity Image, the implementation of generating grid vertices for drawing different modes, and know how the map is drawn. Three vertices constitute a Surface, what do (x, y, z, w) in Rect represent? Then there is the calculation calculation.

Guess you like

Origin blog.csdn.net/weixin_41316824/article/details/134717550