2025/Unity

Unity - UI용 Line Renderer

rimugiri 2025. 7. 26. 11:57
728x90

일방적인 lineRenderer또한 UI상에서 사용할 수 는있지만 실제로는 게임 오브젝트로 적용되기 때문에 sortting이 제대로 적용되지 않거나 해상도에 따라 위치가 제대로 반영이 안되는 경우가 발생할 수 있다 이를 해결하기 위해 grapic ui를 조정하여 UI 용 line renderer를 제작해 보았다

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

public class UILineConnector : Graphic
{
    [Header("스프라이트")]
    public Sprite lineSprite; // 적용하고자 하는 스프라이트 없을시 일반 텍스쳐 적용
   
    public override Texture mainTexture
    {
        get
        {
            return lineSprite == null ? s_WhiteTexture : lineSprite.texture;
        }
    }

    [Header("연결 대상")]
    public RectTransform StartPoint;
    public RectTransform EndPoint;

    [Header("선 속성")]
    public float Thickness = 10f;
    [Range(2, 100)]
    public int SegmentCount = 20;

    [Header("곡선 형태 조절")]
    [Tooltip("곡선이 휘는 정도")]
    public float CurveHeight = 50f;
    [Tooltip("곡선이 휘는 방향")]
    public Vector2 CurveDirection = Vector2.up;

    [SerializeField] private List<Vector2> points;
    public IReadOnlyList<Vector2> Points => points; // 지정해줄 위치 값들
    protected override void Awake()
    {
        base.Awake();
    }

    public void SetPoint(RectTransform startPos, RectTransform endPos)
    {
        if (startPos != null)
        {
            StartPoint = startPos;
        }
        if (endPos != null)
        {
            EndPoint = endPos;
        }
        SetVerticesDirty(); // OnPopulateMesh를 호출하여 메쉬를 새롭게 그려준다
    }

    protected override void OnPopulateMesh(VertexHelper vh)
    {
        // 기존 매쉬 초기화
        vh.Clear();

        // 지점이 설정되어 있지 않다면 아무것도 그리지 않는다
        if (StartPoint == null || EndPoint == null || SegmentCount < 1)
        {
            return;
        }

        List<Vector2> points = new List<Vector2>();

        Vector3 startWorldPos = StartPoint.position;
        Vector3 endWorldPos = EndPoint.position;

        RectTransform ownRect = this.rectTransform;

        // 위치를 로컬 좌표로 변환
        Vector2 p0 = ownRect.InverseTransformPoint(startWorldPos);
        Vector2 p2 = ownRect.InverseTransformPoint(endWorldPos);

        // 곡선의 방향 계산
        Vector2 curveDirection = (p2 - p0).normalized;
        // 시계 반대 방향으로 회전
        CurveDirection = new Vector2(-curveDirection.y, curveDirection.x);

        Vector2 midPointBase = (p0 + p2) * 0.5f;
        Vector2 controlPointOffset = CurveDirection * CurveHeight;

        // 곡선의 가운데 지점
        Vector2 p1 = midPointBase + controlPointOffset;

        for (int i = 0; i <= SegmentCount; i++)
        {
            float t = (float)i / SegmentCount;
            Vector2 point = Mathf.Pow(1 - t, 2) * p0 + 2 * (1 - t) * t * p1 + Mathf.Pow(t, 2) * p2;
            points.Add(point);
        }

        // 각 지점 보간을 위해 각 지점의 법선 벡터를 계산
        List<Vector2> normals = new List<Vector2>();
        for (int i = 0; i < points.Count; i++)
        {
            Vector2 normal;
            if (i == 0) 
            {
                Vector2 direction = (points[i + 1] - points[i]).normalized;
                normal = new Vector2(-direction.y, direction.x);
            }
            else if (i == points.Count - 1) 
            {
                Vector2 direction = (points[i] - points[i - 1]).normalized;
                normal = new Vector2(-direction.y, direction.x);
            }
            else 
            {
                Vector2 dir1 = (points[i] - points[i - 1]).normalized;
                Vector2 dir2 = (points[i + 1] - points[i]).normalized;
                Vector2 averageDir = (dir1 + dir2).normalized; 
                normal = new Vector2(-averageDir.y, averageDir.x); 
            }
            normals.Add(normal * (Thickness / 2));
        }

        for (int i = 0; i < points.Count - 1; i++)
        {
            Vector2 pointStart = points[i];
            Vector2 pointEnd = points[i + 1];

            // 계산된 노멀을 이용하여 위치를 조정해 준다
            Vector2 normalStart = normals[i];
            Vector2 normalEnd = normals[i + 1];

            // 해당 텍스쳐 하나를 구간별로 나눠 적용하는 방식
            float uStart = (float)i / (points.Count - 1);
            float uEnd = (float)(i + 1) / (points.Count - 1);

            UIVertex[] quad = new UIVertex[4];

            quad[0] = new UIVertex { position = pointStart - normalStart, color = this.color, uv0 = new Vector2(uStart, 0) };
            quad[1] = new UIVertex { position = pointStart + normalStart, color = this.color, uv0 = new Vector2(uStart, 1) };
            quad[2] = new UIVertex { position = pointEnd + normalEnd, color = this.color, uv0 = new Vector2(uEnd, 1) };
            quad[3] = new UIVertex { position = pointEnd - normalEnd, color = this.color, uv0 = new Vector2(uEnd, 0) };

            vh.AddUIVertexQuad(quad);
        }
    }
}

 

조금더 자세하게 분석해 보자

 

1. OnPopulateMesh

UI모양을 그려주는 핵심 함수이다

UI또한 vertex로 구성되어 있는 요소로 모양을 잡아주고, UV텍스쳐를 입혀주는 작업을 해준다

 

2. 베지어 곡선

// 곡선의 방향 계산
Vector2 curveDirection = (p2 - p0).normalized;
// 시계 반대 방향으로 회전
CurveDirection = new Vector2(-curveDirection.y, curveDirection.x);

Vector2 midPointBase = (p0 + p2) * 0.5f;
Vector2 controlPointOffset = CurveDirection * CurveHeight;

// 곡선의 가운데 지점
Vector2 p1 = midPointBase + controlPointOffset;

for (int i = 0; i <= SegmentCount; i++)
{
    float t = (float)i / SegmentCount;
    Vector2 point = Mathf.Pow(1 - t, 2) * p0 + 2 * (1 - t) * t * p1 + Mathf.Pow(t, 2) * p2;
    points.Add(point);
}

이 부분은 어떤 위치 지점에 이미지를 그려 넣어줄지 위치를 지정해 주는 동작이다

곡선의 형태가 아닌 다른 형태의 선을 표현하고자 한다면 해당 부분이 변화하면 된다

 

3. 이미지 깨짐 방지 보간

// 각 지점 보간을 위해 각 지점의 법선 벡터를 계산
List<Vector2> normals = new List<Vector2>();
for (int i = 0; i < points.Count; i++)
{
    Vector2 normal;
    if (i == 0) 
    {
        Vector2 direction = (points[i + 1] - points[i]).normalized;
        normal = new Vector2(-direction.y, direction.x);
    }
    else if (i == points.Count - 1) 
    {
        Vector2 direction = (points[i] - points[i - 1]).normalized;
        normal = new Vector2(-direction.y, direction.x);
    }
    else 
    {
        Vector2 dir1 = (points[i] - points[i - 1]).normalized;
        Vector2 dir2 = (points[i + 1] - points[i]).normalized;
        Vector2 averageDir = (dir1 + dir2).normalized; 
        normal = new Vector2(-averageDir.y, averageDir.x); 
    }
    normals.Add(normal * (Thickness / 2));
}

 

여기에서 0 과 마지막 번째가 아닐때 두 벡터의 평균으로 법선벡터를 설정해 주지 않는다면 서로 틈이 벌어진 이미지가 생성되어 매끄러운 선이 생성되지 않는 문제가 발생하므로 주의하자

 

4. UV설정 및 선 생성

for (int i = 0; i < points.Count - 1; i++)
{
    Vector2 pointStart = points[i];
    Vector2 pointEnd = points[i + 1];

    // 계산된 노멀을 이용하여 위치를 조정해 준다
    Vector2 normalStart = normals[i];
    Vector2 normalEnd = normals[i + 1];

    // 해당 텍스쳐 하나를 구간별로 나눠 적용하는 방식
    float uStart = (float)i / (points.Count - 1);
    float uEnd = (float)(i + 1) / (points.Count - 1);

    UIVertex[] quad = new UIVertex[4];

    quad[0] = new UIVertex { position = pointStart - normalStart, color = this.color, uv0 = new Vector2(uStart, 0) };
    quad[1] = new UIVertex { position = pointStart + normalStart, color = this.color, uv0 = new Vector2(uStart, 1) };
    quad[2] = new UIVertex { position = pointEnd + normalEnd, color = this.color, uv0 = new Vector2(uEnd, 1) };
    quad[3] = new UIVertex { position = pointEnd - normalEnd, color = this.color, uv0 = new Vector2(uEnd, 0) };

    vh.AddUIVertexQuad(quad);
}

각 지점마다 사각형의 vertex를 잡아준뒤 각 사각형마자 입힐 이미지를 설정해 주기위해 UV를 설정해 준다

현재 확인해보면 하나의 이미즈를 통해 전체 공간을 표현하기위해 UV를 0 ~ 1까지 잘게 나누어서 설정해 주는데 각 사각형마다 이미지를 사용하고자 하면 UV를 나누는 과정을 생략하면 된다.

728x90