본문 바로가기

Graphics

SDF (Signed Distance Field)

(Unity 빌트인 렌더 파이프라인 사용 / Unreal 5.1 버전 사용)

 

개요

게임 UI를 작업하면서, 'SDF'는 한번쯤은 접해보거나 지나쳤을 내용입니다. 글꼴을 사용할 때, 텍스트에 효과를 주거나 할 때에요. 특히 유니티를 사용하신다면 Text Mesh Pro 는 한번쯤 사용해보셨을 텐데, TMP의 기능들이 SDF를 통해 구현되고 있습니다.

글꼴 외에도, 여러가지로 응용되는 육각형 능력치를 가진 SDF에 대해 이론적인 내용을 정리하고자 해 포스트로 남깁니다.

 


 

Distance

일단 Signed Distance Field 에서의 'Distance'에 대해 간단하게 짚고 넘어가야 합니다. 직역한 '거리'라는 의미대로,  Distance 는 여러 프로그래밍 언어 상에서 특정 지점 A에서 B까지의 거리를 구하는 기능의 함수입니다.

픽셀 셰이더에서 Distance 는 주로 원에 관련된 모양을 만들어 낼 때 사용하고는 합니다. UV 좌표상의 특정 지점(A)을 기준으로, 픽셀 셰이더가 호출될 때마다 A에서 해당 픽셀의 UV 좌표까지의 거리를 계산해서 반지름으로 사용할 수 있기 때문이죠.

UV 좌표상의 위치가 (0.5, 0.5)인 A점을 기준으로, 가로세로가 5px인 영역에서 각 픽셀별 distance 값은 대략적으로 아래 이미지의 오른쪽과 같이 표시되게 됩니다.

 

 


 

SDF란

SDF를 한 문장으로 정의하자면, 위에서 다룬 Distance 값들을 텍스처로 미리 구워서 사용하는 방식이라고 요약할 수 있겠습니다. 그래서 Distance Field, 거리값들이 있는 필드라고 부릅니다. 이렇게 텍스처에 정의된 Distance 값들을 이용해서 두께를 조정하거나, 효과를 준다거나 하는 식으로 사용하게 됩니다.

 

 

SDF에서 Distace Field 앞의 Signed는 부호(+, -)를 뜻합니다. Distance Field를 계산할 때 테두리(0)를 기준으로 테두리 안쪽 면은 -, 바깥쪽 면들은 +로 표시되기 때문입니다. 하지만 텍스처는 0~1의 양수 값만을 가질 수 있기 때문에, 테두리 값을 0.5로 잡고, - 영역은 0.5~1, + 영역은 0.5~0 으로 표현되게 됩니다.

여기서, Distance는 거리가 멀어질수록 값이 증가하니, 이 값을 그대로 텍스처에 넣는다면 내부가 검정색(0)이고 외부가 하얀색(1) 이어야 하겠죠. 하지만 시각적 직관성 때문인지, 0.5 이하의 알파값을 버리는 알파 테스트처럼 특정 값보다 작은 수를 버리고 큰 수를 그리는 게 익숙해서인지, 일반적으로 SDF는 내부가 하얀색이고 외부가 검정색인 텍스처로 표현됩니다.

 

(a) 내외부를 참과 거짓으로 표시 / (b) Distance Field / (c) SDF

 

SDF와 Distance Field의 차이는, Distance Field는 테두리 외부 면에 대한 정보만 가지고 있는 것에 반해, SDF는 추가로 내부 면을 음의 영역으로 가지고 있다는 점입니다.

 


 

SDF와 DF의 사용 예

그렇다면 이런 Distance Field를 가지고 어떤 일을 할 수 있을까요?

 

팀 포트리스에서의 SDF 사용

 

팀 포트리스에선 데칼, 게임 내의 표지판 등의 심볼이나 텍스트에 LOD 텍스처 대신 별도 SDF 텍스처를 사용합니다.
픽셀 이미지는 테두리가 부드럽게 보여지려면 고해상도의 텍스처가 필요한데, SDF를 통해서 저해상도 텍스처를 고해상도의 이미지처럼 표현하고 있습니다.

추가로, SDF를 이용해서 나뭇잎 같이 알파가 포함된 텍스처가 LOD 변경에 따라 휙휙 바뀌는 것 없이 (밸브 자료에선 LOD popping 이라고 부름) 거리에 따라 threshold 값을 조절해 자연스럽게 사라지게 할 수도 있습니다.

개요에서 이야기했었던 유니티 UGUI의 Text Mesh Pro와 그림자, 외곽선, 아웃글로우 같은 TMP의 여러 효과들 또한 SDF를 통해 구현되고 있습니다.

 

빅게임 스튜디오의 캐릭터 얼굴 렌더 방식 (좌) / 바디 툰 메테리얼 얼굴 적용 시 (중) / SDF로 얼굴 셰이딩을 처리 (우)

 

원신과 같은 카툰 렌더링 스타일의 게임들에선 Distance Field 를 사용해 얼굴 셰이딩을 처리하기도 합니다. 빅게임 스튜디오 조준호님의 강연 중 예시에서 바디 툰 메테리얼로 얼굴 셰이딩을 계산했을 때와, Distance Field 를 사용해 얼굴 셰이딩을 계산했을 때의 차이를 확인하실 수 있습니다.

강연에서도 다뤘듯, Distance Field 를 사용한 셰이딩 외에도 버텍스의 노말 방향을 편집해 음영을 자연스럽게 처리하는 방식도 있습니다. (길티기어 시리즈를 제작한 아크시스템웍스에서 노말 편집 방식으로 작업한다고 알고 있습니다.)

하지만 이러한 다른 부분들에 대한 지식이 미약하기 때문에, 이 글에서는 간단하게 SDF 텍스처를 만들고 구현만 해볼 예정입니다.

 


 

간단하게 SDF 텍스처 만들기

밸브에서는 전체탐색을 통해 고해상도 이미지에서 테두리를 알아낸 뒤, 가장 가까운 테두리를 기준으로 거리를 구해 SDF 텍스처를 생성합니다.

하지만 제가 SDF 생성기를 만들어낼 수준이 아니기도 하고, 이펙트 텍스처는 포토샵으로 만들기도 하니, 편하게 포토샵을 사용해서 만들어 보겠습니다. 포스트 작성에 참고한 SDF에 관한 영상 에서는 포토샵 외에도 서브스탠스 디자이너나 Material Maker 라는 툴을 이용해 만드는 방법도 소개하고 있습니다.

 

SDF 생성 시의 Stroke 옵션

 

먼저, 포토샵에서 SDF 텍스처화 하고 싶은 레이어에 외곽선 효과를 줍니다.
Distance Field 가 아닌 'Signed' Distance Field 기 때문에 외곽선의 위치는 중앙으로 설정합니다. 이후 채우기 타입을 그라디언트로, 그라디언트 색상은 검정색-하얀색으로 설정해주고 Style 에서 Shape Burst 를 선택합니다.

마지막으로, 그라디언트 Method 를 Classic으로 설정해줍니다. 기본으로 선택되어 있는 Perceptual 이나 Linear 옵션은 검정색과 하얀색 사이의 값들이 선형으로 보간되지 않는, 사람이 보기에 자연스럽도록 비선형으로 보간된 그라데이션입니다. 따라서 테두리의 색상값이 0.5가 아닌 다른 값(아래 이미지에선 0.39)이 되므로, SDF로 구현했을 때엔 의도와 다르게 표현되게 됩니다.

 

선택 영역으로 표시된 테두리를 스포이드로 찍은 색상값

 


 

SDF 구현하기

이제 만든 텍스처를 가지고 SDF를 구현할 차례입니다.

기본적인 원리는 단순한 알파 테스트 방식과 같습니다. 텍스처에 정의된 distance 값과 우리가 조절하는 threshold 값을 비교해 어떤 부분까지 그릴지를 결정하는 것이죠. threshold 값보다 작은 distance 값을 가진 픽셀은 버리고 (알파값 0으로 만들고) threshold 값보다 큰 픽셀은 그리는 방식입니다.

 

알파 테스트 방식 예

 

첫 번째 이미지에서, threshold 값을 조절하면 SDF 텍스처를 기준으로 분홍색 영역과 하늘색 영역이 달라지는 것을 볼 수 있습니다. 여기서 분홍색 영역은 알파값이 0으로 처리되는 버려지는 영역, 하늘색 영역은 알파값 1로 그려지는 영역입니다. 이를 알파값으로 적용하면, 두 번째 이미지와 같이 출력됩니다. 알파 테스트는 알파값을 중간값이 없는 0 또는 1로 처리하기 때문에 알파 경계면에 계단 현상이 생기는 것도 확인할 수 있습니다.

이런 계단 현상을 제거하기 위해, smoothstep 이라는 보간 함수를 통해 경계면을 안티 에일리어싱 처리할 수 있습니다. 

 

안티에일리어싱을 적용한 예

 

// 변수 선언
sampler2D _MainTex;
float4 _MainTex_ST;
float _Threshold;
float _EdgeSoftness;
fixed4 _Color;

···

fixed4 frag (v2f i) : SV_Target
{
    fixed4 col = tex2D(_MainTex, i.uv);

    // 알파 테스트 방식
    col.a = col.r > _Threshold;

    // 안티에일리어싱 적용

    // threshold 값이 EdgeSoftness 값이 차지하는 범위만큼만 늘어나게 제한
    float threshold = clamp(_Threshold, _EdgeSoftness, 1 - _EdgeSoftness);

    col.a = smoothstep(threshold - _EdgeSoftness, threshold + _EdgeSoftness, col.r);

    // 색상 적용
    col.rgb = _Color.rgb;
    col.a *= _Color.a;

    return col;
}

 

Threshold 값에서 EdgeSoftness 값을 뺀 값이 smoothstep에서의 최소값으로, EdgeSoftness 값을 더한 값이 smoothstep에서의 최대값으로 들어가 경계면의 값이 보간되어 부드럽게 표현되게 됩니다.
유니티 셰이더 코드 전문은 링크에서 확인하실 수 있습니다.

언리얼 메테리얼 노드로는 아래와 같이 만들 수 있습니다.

 

SDF 메테리얼 노드 구조
SDF 메테리얼 동작 예

 


 

여담

 

SDF 채널 하나를 사용했을 때의 모서리(좌)와 채널 두개를 사용했을 때의 모서리(우)

 

Distance Field의 단점은, threshold 값에 따라 모서리가 원본의 곡률을 유지하지 못하고 둥글게 변하는 현상이 생긴다는 것입니다. 밸브의 자료에서는 이를 해결하려면 채널을 두 개 (R,G) 사용하면 될 것이라고 합니다. (팀포트리스에서는 모서리가 둥글게 변하는 게 어울려서 굳이 하진 않았다고 합니다.) 다만 자료의 예시와 달리 글꼴과 같은 리소스에서 각을 모두 살리려면 채널을 복잡하게 많이 사용해야 하는데, 가능할지는 의문입니다. 👀

 


 

자료 및 이미지 출처

Valve 시그래프 2007 자료 - 벡터 텍스처와 특수 효과를 위한 개선된 알파 테스트 확대
참과 거짓 / Distance Field / SDF 이미지
툰렌더에서의 Distance Field 이미지 - 언리얼 서밋 2021
포토샵에서 SDF 텍스처 만들기 - Martin Donald

 

 

 

'Graphics' 카테고리의 다른 글

셰이더에서의 Sin(time) 그래프의 이해  (0) 2023.10.28
색상 간의 혼합 - 기초 블렌딩 연산  (0) 2023.10.09
색체계와 색의 구성요소  (0) 2023.07.15
디지털 색상과 수  (0) 2023.06.25
왜곡 셰이더 만들기  (0) 2023.05.15