2023/02/12

Signed Distance Field를 이용한 폰트 렌더링

 

Signed Distance Field(SDF; 부호 거리 장)는 임의의 위치에서 어떤 경계까지의 최단 거리들을, 경계 안은 양수로 경계 밖은 음수로 표현한 것이다(부호를 반대로 표현하기도 한다). 가령, 타원이 경계선이라면 타원 접선 들과의 직교(gradient 방향) 선들의 길이를 SDF로 나타낼 수 있다. 등고선 지도(contour map)가 대표적인 SDF이다. SDF 활용 분야는 많지만 여기서는 font rendering 기법에 대한 것이다. 실제 활용시에는 Signed Distance Function (SDF; 부호 거리 함수)이라는 개념이 유용해 보인다. Signed Distance를 계산하는 함수이다. 함수든 장이든 SDF를 혼용해도 무리가 없다.

OpenGL에서 truetype font(vector font)를 FreeType 라이브러리로 rendering 한 후, 이미지 파일로 font atlas(지도)를 저장했다가 text rendering 시에 사용했던 적이 있다. 그 때 SDF atlas를 쓰면 bitmap atlas와 달리 글자 크기에 상관없이 사용할 수 있다는 글을 보았는데, SDF에 대해 좀 찾아 보다가 복잡해 보여서 그러려니 했었다. 그런데, 아이디어는 어느날 갑자기 문득 떠오른다. 그리고 아이디어를 기록하지 않으면 영원히 잊혀진다.

SDF를 이용한 폰트 렌더링

원래 SDF를 제대로 계산하려면 경계선인 직선이나 곡선들을 Bezier 함수 등으로 근사하여 contour 경계들을 만들어서 각 지점들과의 최단 직교 거리를 계산해야 한다. 폰트 렌더링 시에 흔히 쓰이는 방식은, vector font를 확대한 bitmap으로 렌더링해서 pixel 경계들과의 Euclidean distance를 구한 후 downsampling 해서 SDF 폰트 atlas를 만드는 것이다. 아무튼, 엄밀하게 하자면 SDF 계산 알고리즘 자체도 복잡하고 한글이나 한자는 글자 수가 많아 계산량도 만만치 않다.
내게 떠오른 아이디어는 정수(integer) SDF를 이용해서 폰트를 렌더링하자는 것이다. 간단하지만 특허도 받을 수 있는 아이디어이다. 하지만, 아이디어가 활용되는 것이 더 낫다고 본다. 폰트 렌더링은 물체 모델링과 유사한데, 충돌 감지 같은 다른 분야에서도 SDF 활용시, 상대적인 값이 의미 있고 굳이 정확한 값이 필요하지 않다면, 정수 SDF를 충분히 활용해 볼 수 있을 것이다. 계산 성능을 높이면서도 효과적인 방법이기 때문이다.

폰트 렌더링 시에 pixel 들간의 거리는 정수로 표현될 수 밖에 없으니 단순무식(?) 더하기/빼기(= Manhattan distance)로 정수 값의 SDF를 구해서 사용해도 충분하리라는 것이 기본적인 생각이었다. 아래 그림은 FreeType으로 생성한 32- pixel 'g' 문자(glyph)를 정수 SDF로 표현한 것이다. Vector font이기 때문에 32 x 32 pixel이 아니고 문자 마다 matrix 크기는 달라진다. 어렴풋이 '0'을 경계로 'g'가 보일 것이다. pixel 경계는 SDF 값이 0이고 경계 안은 양수, 밖은 음수로 나타낸 경계까지의 거리이다. 다르게 보면, SDF가 등고선 들이므로, 각각의 지점에서 0은 해수면이 되고 음수는 해수면 아래 높이, 양수는 해수면 위의 높이가 된다. 입체 글자 모형이 물위에 떠 있다고 생각할 수 있다.

 -5 -4 -2 -1 -1  0  0  0  0  0 -1 -1 -1 -1  0  0 -1
-3 -2 -1 0 0 0 -1 -1 -1 0 0 0 0 0 0 0 -1
-2 -1 0 0 -1 -1 -2 -2 -2 -1 0 1 0 -1 -1 -1 -2
-2 -1 0 0 -1 -2 -3 -3 -3 -2 -1 0 0 -1 -2 -2 -3
-1 0 1 0 -1 -2 -3 -4 -3 -2 -1 0 0 -1 -2 -3 -4
-1 0 1 0 -1 -2 -3 -4 -3 -2 -1 0 1 0 -1 -2 -3
-1 0 1 0 -1 -2 -3 -4 -3 -2 -1 0 0 -1 -2 -3 -4
-1 0 1 0 -1 -2 -3 -3 -3 -2 -1 0 0 -1 -2 -3 -4
-2 -1 0 0 -1 -1 -2 -2 -2 -1 0 1 0 -1 -2 -3 -4
-3 -2 -1 0 0 0 -1 -1 -1 0 0 0 -1 -2 -3 -4 -5
-3 -2 -1 0 0 0 0 0 0 0 -1 -1 -2 -3 -4 -5 -6
-2 -1 0 0 -1 -1 -1 -1 -1 -1 -2 -2 -3 -4 -5 -6 -7
-2 -1 0 -1 -2 -2 -2 -2 -2 -2 -3 -3 -3 -3 -4 -5 -6
-1 0 0 -1 -2 -2 -2 -2 -2 -2 -2 -2 -2 -2 -3 -4 -5
-1 0 1 0 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -2 -3 -4
-2 -1 0 1 0 0 0 0 0 0 0 0 0 0 -1 -2 -3
-3 -2 -1 0 0 0 0 0 0 0 0 0 1 1 0 -1 -2
-2 -1 0 -1 -1 -1 -1 -1 -1 -1 -1 -1 0 1 1 0 -1
-1 0 0 -1 -2 -2 -2 -2 -2 -2 -2 -2 -1 0 1 0 -1
-1 0 0 -1 -2 -3 -3 -3 -3 -3 -3 -3 -2 -1 0 0 -1
0 0 -1 -2 -3 -4 -4 -4 -4 -4 -3 -3 -2 -1 0 0 -1
0 1 0 -1 -2 -3 -3 -3 -3 -3 -2 -2 -1 0 0 -1 -2
-1 0 0 -1 -1 -2 -2 -2 -2 -2 -1 -1 0 0 -1 -2 -3
-2 -1 0 0 0 -1 -1 -1 -1 -1 0 0 0 -1 -2 -3 -4
-3 -2 -1 0 0 0 0 0 0 0 0 -1 -1 -2 -3 -4 -5

그런데, 경계 지점이 곡선 중심이라면 직교 거리는 대각선 방향이고 실제 거리는 2의 제곱근 = (1+1)^0.5 = 1.414씩 증가한다. 즉, 정수로 SDF를 표현하면 분명히 대각선 근처 SDF 값들은 오차가 커진다. 하지만 어차피 Euclidean distance를 사용하더라도 rounding 오차가 발생할 수 밖에 없으므로 내 아이디어를 검증해 볼 만한다. SDF 값이 같은 놈들끼리 contour를 형성하는데, 정수 오차가 발생하더라도 contour 간 거리 비율은 일정하기 때문에 오차가 증폭되지 않을 뿐 아니라, 폰트 렌더링시에도 SDF 값들의 비율에 따라 pixel 강도가 변하기 때문에 SDF 사용 취지에도 잘 부합하게 된다. Euclidean distance를 사용하는 것에 비해 성능을 높일 수 있고 atlas 용량도 줄일 수 있다. 아이디어를 검증하기 위해 Manhattan distance를 정확히 계산하지도 않았는데, 알고리즘의 정확성에 비해 효용성이 별로 없으리라 생각했기 때문이다. 이것이 단순무식 더하기/빼기라고 했던 이유이다. 사실, 정수 오차값들은 일종의 filter 역할을 하는데 대각선 근처의 pixel들 중 먼 거리에 있는 놈들은 걸러내도 폰트 품질에 큰 영향을 주지는 않는다. 실제로 Euclidean distance를 사용한 SDF 값들을 가지고 text 렌더링 했을 때와 비교해도 육안으로는 거의 차이를 느끼기 어려웠다.
아무튼, 위의 SDF 값을 가지고 ASCII 문자로 SDF 값에 따라 pixel 강도를 달리 표현해 렌더링한 결과가 아래 그림이다. OpenGL을 사용하지 않아도 어떻게 렌더링 될지 대략 테스트 해 볼 수 있다.

    ..:iiiiiiiiiiiiii
    ::iiiiiiiiiiiiiii
    :iiiii:::iioiiii:
    :iiii:::::iiii:::
    iioii::.::iiii::.
    iioii::.::iioii::
    iioii::.::iiii::.
    iioii:::::iiii::.
    :iiiii:::iioii::.
    ::iiiiiiiiiii::..
    ::iiiiiiiiii::.. 
    :iiiiiiiii:::..  
    :iii::::::::::.. 
    iiii:::::::::::..
    iioiiiiiiiiiii::.
    :iioiiiiiiiiiii::
    ::iiiiiiiiiiooii:
    :iiiiiiiiiiiiooii
    iiii::::::::iioii
    iiii:::::::::iiii
    iii::.....:::iiii
    ioii::::::::iiii:
    iiiii:::::iiiii::
    :iiiiiiiiiiiii::.
    ::iiiiiiiiiii::..

SDF 렌더링 방식에서는 pixel 강도 값을 spread 값에 따라 조정할 수 있는데 spread는 SDF 상의 최대 거리이다. 가령, 'g'의 SDF matrix에서 spread를 2로 한다는 의미는 모든 SDF 값들을 [-2, 2] 범위로 한정한다는 의미이다. spread 값이 커지면 글자가 굵고 흐릿해지며, 작아지면 가늘고 또렷해 진다. spread가 일종의 anti-aliasing 기능을 수행해 준다. Shader를 사용하면 이런 특성을 이용해서 글자에 외곽선, 음영, 반짝임, 입체효과 등등 여러가지 효과를 줄 수 있다. 아래 그림은 32-pixel SDF 폰트 atlas를 만들어 본 것이다. spread가 커질수록 SDF 계산 시간과 atlas 용량이 늘어난다.

그런데, 전에 본 글에서 글자 확대시 bitmap 폰트 atlas로 렌더링한 text의 품질이 SDF 폰트 atlas로 렌더링한 text 보다 못하다는 말은 사실이 아닌 것 같다. 같은 폰트 size의 atlas로 둘을 비교해 보니 확대시 오히려 bitmap 폰트 atlas를 이용할 때가 품질이 더 좋았다. 물론 같은 조건으로 Shader를 사용했을 때 얘기다. 다만, SDF의 장점은 1개의 폰트 atlas를 가지고도 Shader의 설정을 바꿔 주기만 하면 다양한 글자 효과를 줄 수 있다는 점이다. SDF로 렌더링한 폰트의 품질을 높이려면  고해상도의 truetype 폰트를 이용해서 SDF를 만들고 SDF 폰트의 pixel size도 64이상이면 좋다. 품질을 높이려면 계산 시간과 atlas 용량 증가를 감수해야 한다.
아래 그림은 256-pixel truetype 폰트로 64-pixel SDF 폰트 atlas를 만들어 text를 렌더링해 본 것이다.


아래 그림은 동일한 atlas를 가지고 외곽선 효과를 준 것이다. bitmap 폰트 atlas는 spread가 거의 없기 때문에 글자에 효과를 주는데는 한계가 있다. 일부러 명조체(Serif) 폰트를 사용했는데 고딕체(Sans Serif) 폰트는 상대적으로 글자 효과를 주기 쉽기 때문이다.

글자에 입체 효과 주기

SDF 폰트 atlas의 SDF 정보와 Phong Lighting 기법을 이용하면 Shader를 이용해 글자에 다양한 입체 효과를 줄 수 있다. 3D 모델링에서 SDF를 이용한 rendering 방식은 기존의 triangle mesh를 이용한 rendering 방식과는 차이가 있는데, 객체 모델을 vertex로 표현하는 것이 아니고 SDF로 표현함으로써 Shader에서 SDF surface의 normal vector를 수학적으로 계산하여 lighting 기법을 바로 적용할 수 있는 장점이 있다. Normal vector는 SDF surface의 gradient vector를 normalize 하면 얻어지기 때문에 정확하게 계산할 수 있다. 3D 객체 들은 SDF 함수들의 조합으로 모델링(Constructive Solid Geometry)할 수 있는데  3D 객체 들을 모두 수학 함수로 표현할 수 있다면 실감나는 3D 장면을 연출 할 수 있다. Ray casting 또는 ray marching 기법을 사용해 SDF 값을 얻어낸 후 rendering에 사용하는 것이다.

폰트 렌더링시에는 SDF가 수학 함수가 아니고 이미 계산된 정보이기 때문에 ray marching 기법을 사용할 필요는 없다. 물론, 3D 장면 속의 글자들이 다른 객체들과 어우러져 lighting 효과를 주어야 한다면 ray marching 기법을 사용해야 하겠지만 너무 복잡하기 때문에 여기서는 개별 글자에 입체 효과를 주는 것에 만족한다. 수학 함수로 글자를 표현한 것이 아니기 때문에 normal vector는 SDF surface의 gradient vector를 근사하여 구한다. SDF 값에 다양한 math를 가함으로써 아래의 예와 같은 다양한 효과를 얻을 수 있다.

 

맺음말

정수 SDF를 폰트 렌더링에 사용함으로써 SDF 계산 성능을 높이고 atlas 용량을 줄이지만 렌더링된 text의 품질 저하를 유발하지는 않는다. SDF 폰트 atlas가 좋은 점은, Shader를 사용함으로써 1개의 atlas만으로도 다양한 글자 크기를 표현 할 수 있고, 글자에 다양한 효과를 줄 수 있다는 것이다. 단, 글자가 일정 크기 이상이 되어야만 이런 효과를 맛볼 수 있다.