UniText 는 글리프 고르기만 HarfBuzz 에 맡기고 나머지를 C# 으로 짰다
Esc

    UniText 는 글리프 고르기만 HarfBuzz 에 맡기고 나머지를 C# 으로 짰다

    2026년 10월 6일 수정–Unity라이브러리UIuGUI
    label.text = "Hello مرحبا";   // TextMeshPro 에 넣으면 아랍어 글자가 서로 떨어지고 순서가 뒤집혀 나온다

    유니티의 TextMeshPro 는 영어와 한국어는 잘 그리지만 아랍어, 히브리어, 힌디어, 태국어는 그대로 넣으면 깨집니다. 글자마다 그림 하나를 찾아 왼쪽부터 붙이는 방식이기 때문입니다.

    UniText 는 이 문제를 풀려고 만든 uGUI 텍스트 컴포넌트입니다. 글자에 맞는 그림을 고르는 일은 HarfBuzz 라는 검증된 라이브러리에 맡기고, 그 앞뒤의 분석과 배치는 C# 으로 직접 짰습니다. Light Side LLC 가 MIT 로 공개했고, 기준은 1.0.0 브랜치의 커밋 2087eff 입니다.

    TextMeshPro 는 글자 하나에 그림 하나를 붙인다

    폰트 파일 안에 든 그림 한 장을 글리프라고 부릅니다. 문자열은 유니코드 번호(코드포인트)의 열이고, 텍스트를 그린다는 것은 코드포인트마다 글리프를 골라 놓는 일입니다.

    TextMeshPro 는 이렇게 그립니다.

    1. 폰트 에셋의 표에서 코드포인트에 해당하는 글리프 하나를 찾습니다.
    2. 그 글리프를 왼쪽부터 차례로 놓습니다.
    3. 폰트 에셋에 뽑아 둔 표(합자, 커닝, 기호 붙이기)가 있으면 찾아서 적용합니다.

    앞뒤 글자를 보고 글리프를 바꾸는 단계가 없고, 글의 방향은 인스펙터의 불리언 하나입니다. 글자와 글리프가 일대일이고 한 방향으로만 읽는 언어에서는 이것으로 충분합니다.

    일대일이 아닌 언어에서 깨진다

    글자와 글리프가 일대일이 아닌 경우가 많습니다.

    깨지는 방식 예
    문맥 형태 아랍어 글자는 단어 안 위치(첫, 중간, 끝, 단독)에 따라 모양이 넷
    합자 라틴의 fi, 아랍어의 람과 알리프. 글자 둘이 글리프 하나
    재배열 힌디어 모음 ि 는 메모리에서는 자음 뒤, 화면에서는 자음 앞
    마크 부착 태국어, 아랍어의 모음과 성조 기호가 기준 글자 위아래 정해진 자리에

    방향 문제도 따로 있습니다. 아랍어와 히브리어는 오른쪽에서 왼쪽으로 읽는데, 그 안의 숫자와 영어는 왼쪽에서 오른쪽으로 읽습니다. 한 줄에 둘이 섞이면 화면에 놓는 순서가 저장된 순서와 달라집니다. 이 순서를 정하는 규칙이 유니코드 양방향 알고리즘(UAX #9)입니다.

    그래서 TextMeshPro 로 아랍어를 쓰려면, 문자열을 미리 바꿔서 넣어 주는 별도 플러그인을 붙여 왔습니다. 글자를 위치별 모양의 코드포인트로 바꾸고 순서를 뒤집어 넘기는 방식입니다. UniText 는 이 일을 텍스트 엔진 안에서 하려고 만들어졌습니다.

    글리프를 고르는 일이 셰이핑이고, HarfBuzz 가 그것을 한다

    코드포인트 열을 받아 어느 글리프를 어디에 놓을지 정하는 일을 셰이핑(text shaping) 이라고 부릅니다. 위 표의 네 가지가 모두 셰이핑이 푸는 문제입니다.

    셰이핑 규칙은 프로그램이 아니라 폰트 파일 안에 들어 있습니다. OpenType 폰트의 GSUB 표는 “이 글자들이 이렇게 놓이면 이 글리프로 바꾼다” 를, GPOS 표는 “이 글리프는 이만큼 옮긴다” 를 담습니다. 필요한 것은 언어별 규칙을 외운 코드가 아니라, 폰트의 표를 언어에 맞게 제대로 돌리는 엔진입니다.

    HarfBuzz 가 그 엔진입니다. 오픈소스 C++ 라이브러리이고 Chrome, Firefox, Android 가 글자를 그릴 때 씁니다. 코드포인트 열과 폰트, 방향, 스크립트(문자 체계)를 넘기면 글리프 번호와 위치를 돌려줍니다.

    HarfBuzz 가 하지 않는 일도 있습니다.

    • 글리프를 그리지 않습니다. 번호와 위치만 돌려줍니다.
    • 방향을 정하지 않습니다. 넘겨받은 방향대로 셰이핑할 뿐입니다.
    • 줄을 어디서 끊을지 정하지 않습니다.

    UniText 는 셰이핑만 맡기고 나머지를 C# 으로 짰다

    UniText 가 한 선택은 셋입니다.

    셰이핑은 직접 짜지 않고 HarfBuzz 를 썼습니다. 셰이핑은 스크립트마다 명세를 다시 구현하는 일이고, 틀리면 그 언어 사용자에게만 깨져 보여서 만든 쪽이 알아채기 어렵습니다. 2006년부터 이 일을 해 온 라이브러리를 쓰는 편이 안전합니다.

    글리프를 그리는 일은 FreeType 을 썼습니다. 폰트 파일 안의 글리프는 픽셀이 아니라 윤곽선(곡선의 좌표)으로 들어 있습니다. 화면에 그리려면 이 윤곽선을 원하는 크기의 픽셀 그림으로 바꿔야 하고, 이 일을 래스터화라고 부릅니다. FreeType 은 폰트 파일을 읽어 글리프를 래스터화해 주는 오픈소스 C 라이브러리입니다. 리눅스, Android, 그리고 유니티 자체의 폰트 엔진이 씁니다. HarfBuzz 가 “몇 번 글리프를 어디에” 를 정하면, FreeType 이 그 번호의 글리프를 그림으로 만듭니다.

    HarfBuzz 가 하지 않는 일은 C# 으로 직접 짰습니다. 방향 분석(UAX #9), 스크립트 분석, 줄나누기, 배치, 메시 생성입니다. 방향 분석에는 ICU 나 FriBidi 같은 네이티브 라이브러리가 있지만, UniText 는 하나 더 묶는 대신 C# 으로 옮겼습니다(Runtime/Unicode/BidiEngine.cs, 런타임에서 가장 큰 파일). 플랫폼마다 빌드해야 하는 네이티브 의존성을 늘리지 않은 대신 유지보수 부담을 진 선택입니다.

    네이티브 쪽은 HarfBuzz 12.x, FreeType 2.14.x, 컬러 이모지용 Blend2D 를 소스 수정 없이 내보내는 심볼 이름 앞에 접두사만 붙여 unitext_native 하나로 묶었습니다. 같은 프로젝트의 다른 플러그인이 HarfBuzz 를 품고 있어도 링크 때 이름이 부딪히지 않습니다.

    문자열 하나가 화면에 오는 길에서 네이티브가 끼는 곳은 두 군데뿐입니다.

    TextMeshPro UniText
    글리프 고르기 코드포인트마다 하나, 폰트 에셋의 표 조회 HarfBuzz 셰이핑
    방향 인스펙터의 불리언 하나 UAX #9 를 C# 으로 구현
    글리프 굽기 폰트 에셋 아틀라스 FreeType 으로 처음 쓰일 때 SDF 로 구움

    무료판에도 파이프라인이 전부 있다

    README 는 유료판 Folio 가 “SDF/MSDF rendering” 을 더한다고 적어 무료판에는 SDF 가 없는 것처럼 읽히지만, 무료판 기본 폰트 에셋의 렌더 모드가 바로 SDF 이고 기본 머티리얼도 UniText/Mobile/SDF 입니다. 코드포인트 분해, 방향과 스크립트 분석, 셰이핑, 줄나누기, 굽기, 메시가 모두 무료판에 있고, 유료판이 더하는 것은 MSDF, 가변 폰트, 효과 같은 표현 쪽입니다.

    Defaults/ 에 든 폰트는 NotoSans-Regular, NotoSansArabic-Regular, NotoSansHebrew-Regular 셋이고, 이 순서로 묶은 폰트 스택이 기본값입니다. 상자를 열자마자 오른쪽에서 왼쪽으로 읽는 글이 나오게 짠 구성입니다. 한국어, 중국어, 일본어 폰트는 없습니다.

    컴포넌트 하나에 에셋 둘을 물린다

    폰트 데이터(UniTextFont), 폴백 순서(UniTextFontStack), 머티리얼(UniTextAppearance)을 에셋 셋으로 나눴고, 컴포넌트에는 스택과 외형 둘을 넣습니다. 캔버스 아래 오브젝트에 코드로 붙이는 가장 짧은 모양입니다.

    using LightSide;
    using UnityEngine;
    
    public class Hello : MonoBehaviour
    {
        [SerializeField] UniTextFontStack fontStack;     // Defaults/UniTextFonts_Default
        [SerializeField] UniTextAppearance appearance;   // Defaults/UniTextAppearance_Default
    
        void Start()
        {
            var uniText = gameObject.AddComponent<UniText>();
            uniText.FontStack = fontStack;      // 코드로 붙이면 기본값이 안 들어온다
            uniText.Appearance = appearance;
            uniText.FontSize = 36;
            uniText.Text = "Hello مرحبا";
        }
    }

    스택 하나로 여러 언어가 나옵니다. 라틴 글자는 첫 폰트가, 아랍어는 둘째 폰트가 맡습니다. Text 에 넣은 문자열은 이 줄에서 처리되지 않고, 캔버스가 그리기 직전에 바뀐 컴포넌트를 모아 한꺼번에 처리합니다.

    주의할 점

    1. Unity 6000.5 이상에서는 그대로 컴파일되지 않는다

    안 지키면 생기는 문제. 6000.5 는 GetInstanceID() 사용 중지를 에러로 올려 UniTextFont.cs 와 에디터 파일 하나가 CS0619 로 막히고, 6000.6 은 함께 오는 uGUI 2.6 의 ILayoutElement 에 maxWidth, maxHeight 가 늘어 그보다 먼저 CS0535 가 납니다. 업스트림에는 수정이 없습니다.

    이렇게 쓴다. 패키지를 임베드해서 6000.5 는 (int)GetEntityId().GetRawData() 로 두 줄을, 6000.6 은 거기에 LayoutUtility.DefaultMaxSize 를 돌려주는 두 속성을 더해 네 줄을 고칩니다. 이 네 줄을 넣은 판에서 샘플 씬 다섯 개는 에디터, Mono, IL2CPP 개발 빌드 모두 에러 0 건이었습니다. 이 판에서 런타임에 폰트를 만들고 부수면 ID 가 재사용되는 문제가 있어, 부수기 전에 셰이퍼 캐시를 비웁니다.

    2. 코드로 붙인 컴포넌트에 폰트 스택과 외형을 직접 넣는다

    안 지키면 생기는 문제. Project Settings > UniText 의 기본값은 인스펙터에서 붙일 때만 들어갑니다. AddComponent<UniText>() 로 붙이고 Text 만 넣으면 에러 없이 빈 칸만 남습니다.

    이렇게 쓴다. 인스펙터에서 붙인 프리팹을 인스턴스화하거나, FontStack 과 Appearance 를 필드로 받아 Text 보다 먼저 대입합니다.

    3. 한국어 폰트는 직접 넣는다

    안 지키면 생기는 문제. 기본 스택은 라틴, 아랍어, 히브리어 셋뿐이라 한글은 어느 폰트에서도 찾지 못합니다.

    이렇게 쓴다. 한글 폰트로 UniTextFont 에셋을 만들어 스택에 더합니다. 대부분의 글자를 덮는 폰트를 스택 앞에 둡니다.

    정리

    UniText 는 글리프를 고르고 굽는 일만 네이티브에 맡기고, 그 앞뒤를 C# 으로 짠 유니티 텍스트 엔진입니다.

    • 셰이핑은 HarfBuzz, 굽기는 FreeType 이고 둘은 심볼 이름만 바꿔 한 바이너리로 묶였습니다. 방향 분석, 줄나누기, 배치, 메시는 C# 입니다.
    • 무료판에도 파이프라인이 전부 있고 기본 렌더 모드는 SDF 입니다. 동봉 폰트는 라틴, 아랍어, 히브리어뿐입니다.
    • 컴포넌트에는 폰트 스택과 외형이 필요하고, 코드로 붙이면 직접 넣어야 합니다.