[유니티] GUITexture 사용하기

GUITexture 는 정말 애증의 컴포넌트죠. NGUI 를 쓰던가(유료라는 함정이!),  plane 에 커스텀 UV 를 입히던가(카메라가 반드시 직교투영해야 한다는 함정이!) 하면 문제는 해결될 것 같지만 돈도 없고 3D 요소도 같이 공존해야 하면 별 수 없습니다.

그냥 어떻게든 GUITexture 로 승부를 봐야하는 거죠.

하지만 너무나도 기본적인 기능만 제공하는 GUITexture 로 뭔가 할라하면 스트레스가 이만 저만이 아닙니다. GUITexture 를 완전히 개조할 수는 없는 노릇이지만 이번 포스팅에서는 간단히 기능을 추가해서 살짝 개선해보겠습니다.


 
 
 

OnGUI 메서드

GUITexture 컴포넌트는 OnGUI 메서드를 구상하면 완전히 작동을 통제할 수 있습니다. 반대로 OnGUI 메서드를 구상한 이상 렌더링 그 자체를 전부 책임져야 합니다.

핵심적으로 보면 결국 그림을 그리는게 목적이므로 Texture 객체가 필요합니다. Texture 객체가 없다면 그릴 필요도 없겠죠.

관련 속성을 하나 잡아주고 Texture 가 없으면 그만두게 합시다.

public class GUITextureHelper : MonoBehaviour {

	public Texture _texture;

	void OnGUI(){
		if( _texture == null ) return;
	}

Texture 를 지정할 수 있는 메서드도 구상해 둡니다(Get, Set 은 특정한 경우에 SendMessage 에서 문제를 일으키는 경우가 발생하더군요. 정확히 경우를 한정짓지는 못했지만 그 이후 방어적으로 메서드화 시키고 있습니다)

public void texture( Texture t ){
	_texture = t;
}

문제는 이 Texture 를 어떻게 그릴 것인가인데 이를 위해 동원될 API 는 Graphics.DrawTexture 메서드 입니다. 이 메서드 다양한 시그니처를 갖고 있지만 그 중, 위치와 크기, 텍스쳐, UV 를 받는 인터페이스를 사용하도록 하겠습니다.

static void DrawTexture(
	Rect screenRect,
	Texture texture,
	Rect sourceRect,
	int leftBorder,
	int rightBorder,
	int topBorder,
	int bottomBorder
);

인자를 하나하나 설명해야 겠습니다(이게 복잡하고 그지 같습니다!)

  1. screenRect
    실제 화면에 그려질 영역을 설정합니다. 픽셀기반이고 0, 0 이 좌상단을 가리킵니다. 즉 Rect( 0, 0, 100, 200 ) 을 넘겨주면 좌상단에서부터 가로 100, 세로200 짜리 사각형영역에 그림을 그립니다. 단지 실제 모바일 환경에서 이렇게 픽셀로 직접 크기를 지정하기는 어렵습니다. 화면 사이즈가 각각이기 때문이죠. 이를 위해 추상좌표계를 변환해서 써야합니다만, 그건 또 나중에 다른 포스팅에서…^^;;
     
  2. texture
    텍스쳐 객체를 넣어주면 됩니다. 이게 가장 이해하기 쉬운 인자입니다 ^^;
     
  3. sourceRect
    이 인자는 엄청 중요한데 2번의 텍스쳐에서 어떤 부분을 사용할 건지 UV 좌표를 지정하는 기능입니다. 상당히 지랄 같은데 정확한 지정법을 설명하겠습니다.

    1. 512 x 512 짜리 비트맵을 텍스쳐로 만들었다고 하죠.
       
    2. 그 중에 사용하고 싶은 부분은 ( 256, 0 ) 위치를 기준으로 ( 150, 50 ) 짜리 직사각형이라고 하면,
       
    3. Rect( 256, 512 – 50, 150, 50 ) 입니다.
       
    4. Rect의 x, y 는 0, 0 일 때 좌 하단입니다. ( 0, 512 ) 이 좌상단이죠. 세로가 아래에서 위로 올라갑니다(여기서 엄청 해맸습니다 =.=)
       
    5. 따라서 세로 기준으로 0 에서 50 만큼의 사이즈라고 표현하고 싶지만, 높이을 기준으로 뺀 값을 y 에 지정하고 높이를 넣어야하는 거죠.
       
    6. 높이도 아래서 위로 계산됩니다. 즉 512 – 50 한 지점에서 위로 50 만큼을 인식합니다.
       
    7. 마지막으로 UV좌표계는 0 ~ 1 사이의 비율로 나타내기 때문에 전부 원래 텍스쳐 크기로 나눠줘야 합니다.
       
    8. Rect( 256/_texture.width, (512-50)/_texture.height, 150/_texture.width, 50/_texture.height ) 이게 최종이죠!
       

     

  4. leftBorder ~ bottomBorder
    혹시 scale 9 grid 라고 들어보신적 있나요. 비트맵은 확대되면 점점 열화되는 현상을 보입니다. 픽셀정보는 한정되어있는데 알고리즘을 통해 확대하기 때문에 약간 부드러워질 뿐이지 이미지는 점점 뭉게집니다. 이 현상은 특히 외곽선 부분에서 두드러지는데 둥근 모서리로 처리된 비트맵은 확대되면 엉망으로 뭉게집니다. 이런 현상을 방지하고자 비트맵을 확대할때 각 외곽선부분의 일정 영역은 확대하지 않고 내부만 확대한뒤 원본의 외곽부분 비트맵을 그대로 붙이는 방법을 사용하는데 이를 나인그리드라고 합니다(가운데를 포함하여 9조각으로 쪼개게 되기 때문입니다.) 이 메서드에서도 이를 지원하는데 상하좌우로 일정 픽셀을 지정하면 그 픽셀은 보호받게 되는 식입니다.

이상의 인자구조를 보면 내부에 rect 두개를 갖고 있는게 유리합니다.  그 외에 각 속성에 대응하는 메서드도 같이 만들겠습니다.

public Rect _screen = new Rect();
public Rect _uv = new Rect();

public void x( float v ){ _screen.x = v; }
public void y( float v ){ _screen.y = v; }
public void width( float v ){ _screen.width = v; }
public void height( float v ){ _screen.height = v; }

public void uv( Rect v ){
	v.x /= _texture.width;
	v.y /= _texture.height;
	v.width /= _texture.width;
	v.height /= _texture.height;
}

이제 OnGUI에서 그림을 그릴 수 있습니다.

void OnGUI(){
	if( _texture == null ) return;
	Graphics.DrawTexture( _screen, _texture, _uv, 0,0,0,0 );
}

실제로 화면에 하나 그려봅시다.

//스테이지에 빈게임오브젝트를 하나 준비해둔다.
GameObject go = GameObject.Find( "EmptyObject" );

//1. GUITexture를 붙이고
go.AddComponent( "GUITexture" );
//2. 방금 만든 클래스도 붙이자.
go.AddComponent( "GUITextureHelper" );

//3. texture셋팅
go.SendMessage( "texture", Resources.Load( "texture1" ) );

//4. 좌표와 크기
go.SendMessage( "x", 0 );
go.SendMessage( "y", 0 );
go.SendMessage( "width", screen.width );
go.SendMessage( "height", screen.height );

//5. uv
go.SendMessage( "uv", new Rect( 256, 512 - 50, 150, 50) );

그럼 아래와 같은 결과가 됩니다.

1

왼쪽은 원본 jpg 이미지입니다. 512 x 512 크기로 그라디언트 부분은 바로 저 부분에 있습니다. 오른쪽은 유니티로 컴파일된 결과입니다. 제가 width 와  height 를 스크린 사이즈로 줬으니까 저 부분만 잘라서 화면 가득히 채우게 되었습니다.

 
 
 

iTween 과의 연동

항상 추상 클래스를 만들면 표준 클래스 기반의 라이브러리들과 연동할 수 없다는 문제가 생깁니다. 애니메이션을 걸기 위해 자신만의 애니메이션 시스템을 전부 만들게 아니라면 기존 라이브러리들이 사용하는 표준 클래스 기반으로 재작성하지 않으면 안됩니다.
iTween 의 moveTo 의 경우 gameObject.transform.position 과 연동됩니다.
따라서 도우미 클래스도 전용 x, y 를 포기하고 게임오브젝트의 x, y 를 사용하도록 개조해보죠.

/*파기한다!
public void x( float v ){ _screen.x = v; }
public void y( float v ){ _screen.y = v; }
*/

void OnGUI(){
	if( _texture == null ) return;

	//gameObject의 위치를 이용하자!
	_screen.x = transform.position.x;
	_screen.y = transform.position.y;

	Graphics.DrawTexture( _screen, _texture, _uv, 0,0,0,0 );
}

이제 iTween 을 통해 x, y 를 변경하면 애니메이션이 걸립니다. 위에서 사용했던 호스트 코드에서 x, y 가 메세지 전송에서 직접 지정으로 바뀌고 iTween.moveTo 가 추가해 봅시다.

GameObject go = GameObject.Find( "EmptyObject" );
go.AddComponent( "GUITexture" );
go.AddComponent( "GUITextureHelper" );
go.SendMessage( "texture", Resources.Load( "texture1" ) );
go.SendMessage( "width", screen.width );
go.SendMessage( "height", screen.height );
go.SendMessage( "uv", new Rect( 256, 512 - 50, 150, 50) );

//좌표지정 대체
go.transform.position = new Vector3( 0, 0, 0 );

//iTween.moveTo 100,100으로 이동!
iTween.moveTo( go, new Vector3( 100, 100, 0 ), 1f );

마찬가지로 scaleTo 도 이용할 수 있습니다. 원본사이즈를 _screen.width, height 로 인식해두고 localScale을 반영하면 되므로 단순히 OnGUI에 몇 줄만 추가하면 됩니다.

void OnGUI(){
	if( _texture == null ) return;
	_screen.x = transform.position.x;
	_screen.y = transform.position.y;

	//미리 원본사이즈를 기억해둠
	float w = _screen.width;
	float h = _screen.height;

	//스케일을 반영한다.
	_screen.width *= transform.localScale.x;
	_screen.height *= transform.localScale.y;

	Graphics.DrawTexture( _screen, _texture, _uv, 0,0,0,0 );

	//원래대로 되돌림
	_screen.width = w;
	_screen.height = h;
}

이제 iTween.scaleTo 도 가능해졌습니다!

//2배로 확대
iTween.scaleTo( go, new Vector3( 2, 2, 0 ), 1f );

 
 
 

결론

GUITexture 는 짜증나지만 적당히 개조해서 쓰다보면 그럭저럭 쓸만합니다.

특히 OnGUI 를 커스터마이즈하고 gameObject 의 속성을 대거 연동함으로서 iTween 과 같은 녀석들도 쓸 수 있게 되었습니다.

IDE 상 에서도 대 부분의 필드를 public 으로 열어 뒀으므로 간단히 헬퍼클래스를 붙이고 패널에서 편집해 쓸 수 있게 됩니다.

소스는 아래 있습니다.

http://blog.bsidesoft.com/unity/bside003.cs


추가된 사항
인간적으로 저 UV시스템을 쓰는건 너무 지옥이었습니다. 정상적으로 0, 0 이 좌상단을 가리키게 하고 가로, 세로를 지정하게 하는 방식으로 uv메서드를 수정했습니다.

public void uv( Rect r ){
	_uv.x = r.x / _texture.width;
	_uv.y = ( _texture.height - r.height - r.y ) / _texture.height;
	_uv.width = r.width / _texture.width;
	_uv.height = r.height / _texture.height;
}

위의 y변환을 이용하면 본문에서 다룬 샘플의 경우 x = 256, y = 0 에서부터 150, 50 만큼 이란 의미로 지정할 수 있게 됩니다.

go.SendMessage( "uv", new Rect( 256, 0, 150, 50) );