Post

UV Coordinate for Sprite Sheet

UV Coordinate for Sprite Sheet

스프라이트 시트를 사용할 때 UV 좌표 구하기

무엇이 문제인가?

2D 애니메이션을 만들기 위해 각 프레임의 이미지를 옆으로 이어붙인
하나의 커다란 이미지를 스프라이트 시트라고 부른다.
실제로는 sprite editor에서 개별 이미지를 slice해서 사용하므로
우리 눈에는 한 시점에 전체 시트의 한 부분만 보이게 된다.
coins

하지만 위에 있는 스프라이트 시트에서 동색 동전을 sprite renderer에 넣고
UV 좌표를 시각화하는 셰이더를 적용해보면 약간 당황스러운 결과가 나온다. bronze coin uv 표시된 색상은 (u, v, 0, 1)

UV 좌표는 좌측 하단에서 (0, 0), 우측 상단에서 (1, 1)이어야 할텐데
이미지의 모든 곳에서 (0, 1)에 가까운 값이 보인다.

자세히 살펴보니 해당 스프라이트도 전체 스프라이트 시트에서 좌측 상단에 있다.
그럼 금색 동전 이미지를 넣으면 (0, 0)에 가까운 값이 나올까?
동일한 셰이더를 사용해 렌더링하면 실제로 아래와 같은 색을 볼 수 있다! gold coin uv

이는 slice된 스프라이트의 UV 좌표가 전체 스프라이트 시트를 기준으로 주어진다는 뜻이다

개별 스프라이트 기준 UV 좌표 계산하기

원리

slice된 개별 스프라이트가 전체 시트에서 차지하는 범위를 셰이더에 넘겨줄 수만 있다면
역으로 전체 시트 기준 UV 좌표를 개별 스프라이트 기준 UV 좌표로 보정할 수 있다.

예를 들어, 금색 동전이 차지하는 범위를 [0, 1] 범위로 표현하면 대략 아래같은 수치가 나올 것이다.

 시작
가로00.25
세로00.33

이는 셰이더에 주어진 UV 좌표가 (0, 0)일 때 좌측 하단이고 (0.25, 0.33)일 때 우측 상단이라는 뜻이다.
우리가 [0, 1] 범위로 보정된 UV 좌표를 원한다면 [0, 0.25] 구간을 [0, 1]로 매핑해주면 되는 것이다.

1
2
3
// raw: 셰이더에 주어진 UV 좌표
// mapped: 해당 slice를 기준으로 [0, 1] 범위에 맞게 보정된 UV 좌표
mapped = (raw - min) / (max - min)

minmax 정보는 Sprite 클래스가 제공함!

구현

1. 셰이더

1-1. minmax를 전달받을 property 생성

UV 보정에는 x축과 y축 각각의 minmax가 필요하므로 총 float 4개가 필요하다.
셰이더 프로퍼티를 최대한 간소하게 유지하기 위해 네 수치를 Vector 하나로 처리할 것이다.
Properties 블록에 아래 라인을 추가하면 코드에서 "_SpriteRect"라는 이름으로 접근할 수 있다.

_SpriteRect (“Sprite Rect”, Vector) = (0, 0, 0, 0)

1-2. UV 좌표 보정하기

여기서는 fragment shader에서 보정을 수행했지만
약간 더 효율적으로 만들고 싶다면 v2f 구조체에 필드를 하나 추가해
vertex shader가 기존 UV와 보정된 UV를 모두 전달하도록 하면 된다.

참고로 기존 UV는 텍스처 샘플링에 필요하므로 여전히 필요하다!

1
2
3
4
5
6
7
8
9
10
11
// Property로 추가한 변수를 코드에서 쓸 수 있게 선언
float4 _SpriteRect;

float4 frag (v2f i) : SV_Target
{
    float2 min_uv = _SpriteRect.xy; // 해당 프레임에 좌측 하단 끝에 부여될 "시트 기준 uv 좌표"
    float2 max_uv = _SpriteRect.zw; // 해당 프레임에 우측 상단 끝에 부여될 "시트 기준 uv 좌표"
    float2 real_uv = (i.uv - min_uv) / (max_uv - min_uv);

    return float4(real_uv, 0, 1);
}

2. 스크립트

이제 렌더링이 일어나기 직전인 LateUpdate()에서 셰이더로 정보를 전달할 스크립트가 필요하다.

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
using UnityEngine;

public class SpriteUVCalculator : MonoBehaviour
{
    SpriteRenderer spriteRenderer;

    void Awake()
    {
        spriteRenderer = GetComponent<SpriteRenderer>();
    }

    // 애니메이터 업데이트가 끝난 시점
    void LateUpdate()
    {
        // 전체 스프라이트 시트 (픽셀 단위 가로세로 길이 정보)
        Texture2D spriteSheet = spriteRenderer.sprite.texture;
        
        // 스프라이트 시트의 특정 부분 (sprite editor에서 slice한 사각형 범위 하나)
        Rect rawRect = spriteRenderer.sprite.textureRect;

        // 픽셀 단위로 주어진 시트 상의 영역을 0~1 범위로 정규화
        var normalizedRect = new Vector4(
            // 좌측 하단 지점의 "시트 기준 uv"
            rawRect.xMin / spriteSheet.width,
            rawRect.yMin / spriteSheet.height,
            // 우측 상단 지점의 "시트 기준 uv"
            rawRect.xMax / spriteSheet.width,
            rawRect.yMax / spriteSheet.height
        );

        // 셰이더에 전달
        spriteRenderer.material.SetVector("_SpriteRect", normalizedRect);
    }
}

결과

이제 어떤 스프라이트를 넣어도 slice된 위치와 무관하게 일정한 UV 좌표를 얻을 수 있다!

bronze coin correct uv gold coin correct uv

셰이더 최종본

Shader "Unlit/UV"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _SpriteRect ("Sprite Rect", Vector) = (0, 0, 0, 0)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            HLSLPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = TransformObjectToHClip(v.vertex.xyz);
                o.uv = v.uv;
                return o;
            }

            float4 _SpriteRect;

            float4 frag (v2f i) : SV_Target
            {
                float2 min_uv = _SpriteRect.xy; // 해당 프레임에 좌측 하단 끝에 부여될 "시트 기준 uv 좌표"
                float2 max_uv = _SpriteRect.zw; // 해당 프레임에 우측 상단 끝에 부여될 "시트 기준 uv 좌표"
                float2 real_uv = (i.uv - min_uv) / (max_uv - min_uv);

                return float4(real_uv, 0, 1);
                // return float4(i.uv, 0, 1); // 기본 uv가 보고 싶다면 사용
            }
            ENDHLSL
        }
    }
}

에셋 출처

This post is licensed under CC BY 4.0 by the author.