TA/Unity2013. 9. 11. 14:13

http://myevan.cpascal.net/articles/2013/unity_ngui_pixel_perfect.html

 

유니티 NGUI 논리 해상도와 픽셀 퍼펙트

다양한 해상도를 지원하는 가장 간단한 방법 중 하나는 고정된 논리 해상도를 사용하는 것입니다. 레퍼런스 장치 해상도를 기준으로 그래픽 리소스를 만든 다음 다른 장치에서 적당히 확대 혹은 축소해서 렌더링하는 방법입니다.

예를 들어 아이폰3GS 는 320x480 (2:3) 해상도를 사용하는데, 아이폰4는 640x960 (2:3) 해상도이므로 2배 확대해서 그대로 실행할 수 있습니다. 아이패드 768x1024 (3:4) 해상도에서 실행하려면 세로 길이를 1024에 맞추고 가로길이를1024 / 480 * 320 = 682 로 확대하면 리소스나 레이아웃 수정없이 작동시킬 수 있습니다.

NGUI 에서는 2.5.x 버전 기준으로 UIRoot 컴포넌트의 Scaling Style 프로퍼티를FixedSize 로 선택한 다음 ManualHeight 프로퍼티만 설정해주면 고정된 논리 해상도를 사용할 수 있습니다.

문제는 논리 해상도 모드로 아틀라스 위젯들을 사용할 경우 위젯 경계에 검은색 선이 종종 등장한다는 것입니다. 아틀라스 스프라이트 영역 경계에 있는 검은색 컬러가 살짝 보이는 것인데, 해당 위치에 알파를 확인해보면 0으로 보이지 않아야 정상입니다. 더구나 padding 을 아무리 넓게 주어도 검은색 선 문제는 사라지지 않습니다. 선형 보간 기능의 특성상 주변 텍셀을 참조하기 때문인데, 텍스쳐 Filter Mode 를 Point 로 변경해 선형 보간을 사용하지 않으면 검은 색 선이 사라지는 것을 확인 할 수 있습니다.

그럼 이제 UIWidget 의 MakePixelPerfect 코드를 살펴보도록 하겠습니다.

virtual public void MakePixelPerfect ()
{
    Vector3 scale = cachedTransform.localScale;

    int width  = Mathf.RoundToInt(scale.x);
    int height = Mathf.RoundToInt(scale.y);

    scale.x = width;
    scale.y = height;
    scale.z = 1f;

    Vector3 pos = cachedTransform.localPosition;
    pos.z = Mathf.RoundToInt(pos.z);

    if (width % 2 == 1 && (pivot == Pivot.Top || pivot == Pivot.Center || pivot == Pivot.Bottom))
    {
        pos.x = Mathf.Floor(pos.x) + 0.5f;
    }
    else
    {
        pos.x = Mathf.Round(pos.x);
    }

    if (height % 2 == 1 && (pivot == Pivot.Left || pivot == Pivot.Center || pivot == Pivot.Right))
    {
        pos.y = Mathf.Ceil(pos.y) - 0.5f;
    }
    else
    {
        pos.y = Mathf.Round(pos.y);
    }

    cachedTransform.localPosition = pos;
    cachedTransform.localScale = scale;
}

MakePixelPerfect 는 픽셀 좌표를 정수화시킨 후 위젯 길이가 홀수면 버텍스 좌표를 0.5만큼 보정하는 기능입니다. 즉, 픽셀 퍼펙트를 적용 하면 검은색 선은 보여서는 안된다는 말입니다. 그렇다면 어디에 문제가 있는 것일까요?

그렇습니다. 당연하겠지만 이번 글의 주제인 논리 해상도가 문제의 원흉입니다. 논리 해상도에서 힘들여 설정한 픽셀 퍼펙트한 좌표가 실제 해상도에서는 엉뚱한 좌표로 변환되어 버린 것입니다 =ㅁ=)!!

예를 들어 1000x1000 논리 해상도에서 0.5 보정한 좌표는 2000x2000 해상도에서는 1을 변경한 것으로 바뀝니다. 500x500 해상도라면 보정값이 0.25가 됩니다. 1000x1000 해상도에서 보정이 필요한 5픽셀은 2000x2000해상도에서는 10픽셀 짝수 길이가 되어 보정이 불필요해지고, 1000x1000 해상도에서 보정이 필요없는 10픽셀 길이는 500x500 해상도에서 5픽셀 길이가 되어 보정이 필요해집니다.

실제로 검은색 선이 보이는 위젯의 위치를 0.1 단위로 조금씩 변경해보면 어느순간 검은색 선이 사라지는 상황을 만들 수 있습니다. 다만 해당 값이 짐작하기 어려운 수치라는것이 문제죠.

그럼 논리 해상도에서 픽셀 퍼펙트한 좌표를 계산해 내는 방법을 알아보도록 하겠습니다.

일단 논리 좌표와 논리 크기를 사용해 실제 좌표와 실제 크기를 구합니다.

화면 배율 scale =  논리 해상도 / 실제 해상도
논리 크기 lsize = 위젯 transform.localScale
논리 좌표 lpos = 위젯 transform.localPosition
실제 좌표 rpos = lpos / scale
실제 크기 lsize = lsize / scale

실제 크기를 토대로 실제 좌표에 픽셀 퍼펙트를 적용합니다.

보정 좌표 = 실제 크기가 홀수면 실제 좌표 보정

보정된 실제 좌표를 논리 좌표계로 다시 가져오면 완료입니다!

위젯 transform.localPosition = 보정 좌표 * scale
위젯 transform.localScale = 실제 크기  * scale

한가지 주의해야 할 점은 위젯 transform 을 보정된 값으로 가지고 있을 경우 유니티 인스펙터 값이 그로테스크해진다는 것 입니다.

작업 편의를 위해서는 UpdateGeometry 에서만 위젯 transform 변경해 사용한 다음 바로 원래 값으로 복원해주는 것이 좋습니다.

마지막으로 직접 코딩하기 귀찮으신 분들을 위한 NGUI UIWidget.cs 수정 사항입니다.

// BUGFIX
public static int rootManualHeight; // TODO: 외부에서 UIRoot 의 manualHeight 값을 넣어주어야 합니다
// BUGFIX_END

public bool UpdateGeometry (ref Matrix4x4 worldToPanel, bool parentMoved, bool generateNormals)
{
    if (material == null) return false;

    if (OnUpdate() || mChanged)
    {
        mChanged = false;
        mGeom.Clear();
        OnFill(mGeom.verts, mGeom.uvs, mGeom.cols);

        if (mGeom.hasVertices)
        {
            Vector3 offset = pivotOffset;
            Vector2 scale = relativeSize;

            offset.x *= scale.x;
            offset.y *= scale.y;

            mGeom.ApplyOffset(offset);

            // DELETEME: mGeom.ApplyTransform(worldToPanel * cachedTransform.localToWorldMatrix, generateNormals);

            // BUGFIX
            ApplyPixelPerfectGeometryTransform(ref worldToPanel, generateNormals);
            // BUGFIX_END
        }
        return true;
    }
    else if (mGeom.hasVertices && parentMoved)
    {
        // DELETEME: mGeom.ApplyTransform(worldToPanel * cachedTransform.localToWorldMatrix, generateNormals);
        // BUGFIX
        ApplyPixelPerfectGeometryTransform(ref worldToPanel, generateNormals);                  
        // BUGFIX_END
    }
    return false;
}

// BUGFIX
void ApplyPixelPerfectGeometryTransform(ref Matrix4x4 worldToPanel, bool generateNormals)
{
    float screenScale = rootManualHeight / Screen.height;
    float invScreenScale = 1.0f / screenScale;  

    var widgetTransform = this.cachedTransform;
    var oldSize = widgetTransform.localScale;
    var oldPos = widgetTransform.localPosition;
    var realSize = oldSize * invScreenScale;
    var realPos = oldPos * invScreenScale;

    int realWidth  = Mathf.RoundToInt(realSize.x);
    int realHeight = Mathf.RoundToInt(realSize.y);

    if (realWidth % 2 == 1 && (pivot == UIWidget.Pivot.Top || pivot == UIWidget.Pivot.Center || pivot == UIWidget.Pivot.Bottom))
    {
        realPos.x = Mathf.Floor(realPos.x) + 0.5f;
    }
    else
    {
        realPos.x = Mathf.Round(realPos.x);
    }

    if (realHeight % 2 == 1 && (pivot == UIWidget.Pivot.Left || pivot == UIWidget.Pivot.Center || pivot == UIWidget.Pivot.Right))
    {
        realPos.y = Mathf.Ceil(realPos.y) - 0.5f;
    }
    else
    {
        realPos.y = Mathf.Round(realPos.y);
    }

    widgetTransform.localPosition = realPos * screenScale;
    widgetTransform.localScale = realSize * screenScale;

    mGeom.ApplyTransform(worldToPanel * cachedTransform.localToWorldMatrix, generateNormals);

    widgetTransform.localPosition = oldPos;
    widgetTransform.localScale = oldSize;
}
// BUGFIX_END

뭔가 좀 더 최적화가 가능할 것 같지만, 일단 여기까지만 다루도록 하겠습니다.

'TA > Unity' 카테고리의 다른 글

간단한 씬 로딩  (0) 2013.10.15
VSync - WaitForTargetFPS  (0) 2013.09.27
unity 에디터 확장  (2) 2013.09.11
로딩 페이지 및 로딩 프로그래스바 사용하기.  (0) 2013.09.11
효과적인 C# 메모리 관리 기법  (1) 2013.09.09
Posted by 프리랜서 디자이너