TECHARTNOMAD | MAZELINE.TECH

카테고리 없음

[번역] 카툰 렌더링 - 360도 얼굴 SDF 광원 솔루션

jplee 2026. 6. 4. 00:15

저자: Yu-ki016

효과 전시(效果展示): 【卡通渲染】360度脸部SDF光照方案_哔哩哔哩_bilibili

1. 서언(前言)

현재 실시간 렌더링(实时渲染) 분야에서 카툰 렌더링 얼굴 광원(光照)은 주로 두 가지 구현 방법이 있습니다:

  1. 노멀(法线) 수정
  2. SDF 맵(贴图) 사용

얼굴 카툰 렌더링 솔루션에 대한 소개는 아래의 글을 참고하시기 바랍니다:

다음에 이 두 가지 솔루션에 대한 저의 개인적인 이해를 말씀드리겠습니다.

1.1 노멀(法线) 수정 솔루션

소녀전선2(少女前线2)

우선, 단순히 모델 버텍스(顶点)의 노멀만 수정한다면, 모델 버텍스 수가 제한적이어서, 예를 들어 위의 모델은 이미 세밀하게 만들어졌지만 버텍스 수가 5k 정도에 불과하여, 구형 노멀을 전달하여 비교적 부드러운 광원 효과를 내는 데는 적합하지만 복잡한 구조를 표현하기는 꽤 어렵습니다.

물론 모델의 토폴로지(拓扑)를 수정하는 등의 방법도 있지만, 제 생각에 이러한 방법들은 그다지 우아하지 않다고 봅니다.

개인적으로는 노멀 맵(法线贴图)을 사용하는 것이 잠재력이 더 크다고 생각하며, 사적으로 SDF를 노멀 맵으로 베이킹(烘培)하는 방법을 연구하고 있습니다. 현재 결과는 제 스스로 그다지 만족스럽지 않지만, 추후 재미있는 결과가 나오면 다시 글을 써서 여러분과 공유하겠습니다.

SDF를 통해 생성된 노멀 맵

SDF 법선 맵을 사용한 셰이딩 효과

월드 공간(世界空间)의 노멀로 베이킹하기 쉽도록, 탄젠트 공간(切线空间)으로 베이킹하는 것이 더 적합합니다.

1.2 SDF 사용 솔루션

SDF 솔루션은 현재 시장에서 가장 많이 사용되는 방식이며, 새로 출시되는 2차원(이차원) 게임들은 기본적으로 이 방식을 사용합니다.

구현 상에서 SDF 솔루션은 어렵지 않지만, 저 자신이 언리얼 엔진(unreal引擎)을 많이 사용하기에 직접 파이프라인(管线)을 수정하는 방법으로 구현하려 할 때 우아하게 수정하기가 매우 어렵습니다. 그래서 저는 앞으로도 계속해서 노멀 맵 솔루션을 연구할 예정입니다.

표현 상에서 현재 SDF 솔루션의 가장 두드러진 문제는 Z축 상의 변화가 없다는 점이며, 이것이 이 글에서 주로 해결하고자 하는 문제입니다. 본문의 구현 아이디어는 사실 아주 간단합니다. 현재의 SDF는 수평 축의 광원만 그렸으므로, 우리는 다른 모든 축을 다 그려주면 됩니다.

수평 축의 SDF를 대체하기 위해 전 각도(全角度)의 SDF 아틀라스(图集) 사용

2~3년 전 원신(原神)의 SDF 솔루션을 보았을 때 이 방법을 생각했습니다. 카툰 렌더링에 평소 관심 있는 분들이라면 다들 한 번쯤 생각해보셨을 거라 믿지만, 몇 년이 지나도 이 솔루션을 구현한 글을 찾아볼 수 없었습니다. 보아하니 다들 모든 방향의 광원을 다 그리는 작업량이 너무 커서 시도할 엄두를 못 내시는 것 같아, 제가 직접 구현을 시도해서 여러분께 보여드릴 수밖에 없겠군요.

2. 구현 프로세스(实现流程)

2.1 프로세스 개황(流程概况)

세 마디로 간단히 요약해보겠습니다:

  1. 각 각도의 광원을 잘 그립니다. 아래와 같이 저는 총 65장을 그렸는데, 살짝 요령을 피워서 조금 덜 그려도 문제없을 것 같습니다.

블렌더(blender)에서 광원 맵(光照图) 그리기

  1. 다 그린 광원 맵을 SDF로 변환한 후, 하나의 아틀라스(图集)로 합칩니다.

생성된 SDF 및 SDF 아틀라스

  1. 광원의 각도를 통해 적절한 uv를 계산하고, 4번 샘플링(采样)하여 보간(插值)을 진행합니다.

SDF 아틀라스를 네 번 샘플링하여 광원 결과를 계산

2.2 광원 맵(光照图) 그리기

광원 그리기에는 현재 딱히 좋은 자동 생성 방법이 생각나지 않아 전적으로 손으로 그렸습니다(手绘). 저에게는 딱히 미술적 능력이 없어서 이 65장의 그림을 그리는 데 제 여가 시간 1주일을 통째로 썼습니다. 만약 미술 굇수(大佬)분들이 이 물건을 기꺼이 그려주신다면 아마 효과가 훨씬 더 좋지 않을까 싶습니다. (이렇게 많은 모델들은 약간 표정 블렌드 쉐이프(blend shape)를 제작하는 느낌이 들더군요.)

광원 맵은 저는 9행 9열을 그렸습니다 (첫 번째 행과 마지막 행은 각 한 장뿐이므로 총 9x7+2=65장).

광원 맵의 각 행은 평행광이 아래쪽 반구에서 위에서 아래로 9가지 다른 경도(经度)로 얼굴에 투사됨을 의미하고, 각 열은 9가지 다른 위도(纬度)를 의미합니다. 첫 번째 행과 마지막 행은 평행광이 정위와 정아래에서 투사되어 오므로 한 장씩만 있습니다.

그려진 광원 맵은 빛이 좌측 반구의 각 지점에서 비추는 결과와 맞먹습니다.

주의해야 할 점이 하나 있는데, 보시다시피 제가 광원 맵을 그릴 때 uv의 경계를 넘겨서 칠(涂)했습니다. 이렇게 해야 생성된 SDF가 uv 경계에서 효과 오류를 일으키지 않습니다 (SDF가 uv 경계를 넘지 않게 하려면 SDF를 생성한 후에 경계를 넘어간 부분을 잘라내면 됩니다):

광원 맵을 그릴 때는 uv 경계를 칠해 넘기는 것을 권장합니다.

아래는 반면교사(反面例子)입니다:

얼굴 상단 UV 경계 부분에서 SDF 보간 효과가 좋지 않음

참고용으로 애니메이션 속 빛과 그림자의 변화(光影变化)를 좀 보여드리겠습니다:

원더 에그 프라이어리티 (奇蛋物语)

원더 에그 프라이어리티, 아이돌마스터, Mygo

케이온! (轻音少女)

2.3 SDF 아틀라스(图集) 제작

8ssedt 알고리즘을 사용해 SDF 생성

먼저 흑백의 광원 맵을 SDF로 변환해야 하는데, 저는 8ssedt 알고리즘을 사용해 생성했습니다. 8ssedt 알고리즘에 대한 자세한 내용은 아래 두 글을 참고하시기 바랍니다:

(73 封私信 / 43 条消息) Signed Distance Field - 知乎

【Unity云消散】理论基础:实现SDF的8SSEDT算法_unity sdf-CSDN博客

다음으로 모든 광원 맵을 한 장의 아틀라스로 이어붙입니다(拼):

SDF 아틀라스

여기서 조금 설명을 보태자면, 왜 제가 이런 형태의 SDF를 생성하지 않는지에 대해서입니다 (이러한 SDF는 이하 "보간 후의 SDF(插值后的SDF)"로 통칭하겠습니다).

"보간 후의 SDF"

이런 식의 보간 후 SDF를 사용하는 데는 사실 하나의 전제 조건(条件)이 따릅니다: 뒤쪽의 광원이 반드시 앞쪽의 광원을 덮어야(覆盖) 한다는 점입니다.

예를 들어 아래의 SDF 이미지 세트에서는, 광원 맵의 범위가 왼쪽에서 오른쪽으로 반드시 점차 넓어집니다(逐渐增大).

https://zhuanlan.zhihu.com/p/411188212

이로 인해 초래되는 결과는 빛이 정면에서 비춰올 때, 아래 이미지의 왼쪽처럼 얼굴 전체가 완전히 밝아야 하며, 오른쪽처럼 얼굴의 뒷부분이 조명받지 못하는 효과가 나타나는 것은 불가능하다는 것입니다.

"보간 후 SDF 사용 시의 단점"

또 다른 예를 들어보자면, 아래의 3장의 이미지는 서로 포함(包含) 관계가 없습니다. 그래서 SDF로 변환한 후 보간(插值)하면 그 효과가 우측 그림처럼 됩니다. 어딘가 잘못된 것처럼 보이지 않습니까?

포함 관계가 없는 이미지로 "보간 후 SDF" 생성하기

결과물을 한번 보시죠. 확실히 무언가 잘못되었습니다:

“插值后的SDF”错误的效果

이런 오류를 초래한 원인은 "보간 후 SDF"를 생성하는 알고리즘에 있습니다:

  • 알고리즘은 255번의 반복문(循环) 안에서 여러 광원 각도에 대응하는 광원 결과를 계산해냅니다. 이 단계는 문제가 없습니다.
  • 하지만 255가지의 다른 결과를 한 장의 텍스처(贴图) 안에 저장할 수 있게 하려고, 알고리즘 내부에서는 모든 결과를 누적(累加)해버리는 방식을 택합니다. 이 단계가 바로 보간 정보를 파괴하는 것입니다.
for (int y = 0; y < height; y++)
{
	for (int x = 0; x < width; x++) {
		for (int i = 0; i < 255; i++) {
			if (nextTexIndex >= int(grayImages.size())) {
				break;
			}
			float weight = lerpStep / levelStep;
            // 여기서 인접한 두 장의 SDF를 샘플링함
			int curPixel = grayImages[curTexIndex].at<uchar>(y, x);
			int nextPixel = grayImages[nextTexIndex].at<uchar>(y, x);
			int lerpPixel = curPixel * weight + nextPixel * (1 - weight);
            // 여기서 각기 다른 광원 각도에 대응하는 광원 결과를 계산함
			result += lerpPixel > 127 ? 0 : 1;
			// 결과 누적
			lerpStep++;
			if (lerpStep >= levelStep)
			{
				lerpStep = 0;
				curTexIndex++;
				nextTexIndex++;

			}
		}

		lerpedSDF.at<Vec3b>(y, x)[0] = int(result);
		lerpedSDF.at<Vec3b>(y, x)[1] = int(result);
		lerpedSDF.at<Vec3b>(y, x)[2] = int(result);

		result = 0;
		curTexIndex = 0;
		nextTexIndex = 1;
		lerpStep = 0;
	}
}

만약 수평 축의 SDF만 만든다면, 사실 "뒤쪽 광원 범위가 앞쪽 광원 범위보다 반드시 더 커야 한다"는 조건은 딱히 큰 지장을 주지 않으며, 게다가 한 장의 맵으로 9장의 SDF를 대체하므로 가성비(性价比)가 꽤 높습니다.

하지만 우리는 전 각도(全角度)의 SDF 광원을 만드는 것이기 때문에, 애초에 아틀라스를 만들어야 하므로 직접 SDF를 사용하는 쪽이 낫습니다. 그렇게 하면 위 조건의 제약도 받지 않으니 효과도 더 좋아집니다.

2.4 툴(工具)

위의 SDF 변환과 아틀라스 짜맞추기 조작을 위해 제가 작은 툴 하나를 작성해 두었습니다. 여러분은 불필요하게 바퀴를 재발명하느라(重复造轮子) 시간을 낭비하실 필요 없이, 제 툴을 직접 가져다 쓰시면 됩니다.

SDF와 아틀라스를 생성하는 소형 툴

툴은 제가 프로젝트 파일과 함께 두었으니, SDFTool.zip 압축을 풀고 main.exe를 더블 클릭해서 열면 됩니다.

아티스트 분들은 패키징된 툴을 직접 사용하시면 됩니다.

툴의 상세 사용 문서는 제 github에서 확인하실 수 있으며 소스 코드도 올라와 있습니다. 툴이 엄청 견고(鲁棒)하게 짜이진 않아서 만약 문제가 발생한다면 실력 있으신 분들이 직접 소스 코드를 수정해서 쓰시길 바랍니다.

https://github.com/Yu-ki016/SDFTool

 

GitHub - Yu-ki016/SDFTool: 通过黑白图生成SDF图和SDF图集的小工具

通过黑白图生成SDF图和SDF图集的小工具. Contribute to Yu-ki016/SDFTool development by creating an account on GitHub.

github.com

 

한국어 버전 리포

mazelines/SDFTool: 通过黑白图生成SDF图和SDF图集的小工具

 

GitHub - mazelines/SDFTool: 通过黑白图生成SDF图和SDF图集的小工具

通过黑白图生成SDF图和SDF图集的小工具. Contribute to mazelines/SDFTool development by creating an account on GitHub.

github.com

 

2.5 광원 각도를 계산하여 SDF 아틀라스 샘플링하기(计算光源角度采样SDF图集)

이하의 많은 조작들은 일반적인 SDF 얼굴 광원 처리와 매우 유사하므로 너무 상세하게 적지는 않겠습니다. 자세한 건 그냥 직접 제 프로젝트를 열어서 봐주세요.

  1. 블루프린트(蓝图) 안에서 캐릭터의 얼굴이 앞을 향하는 방향 벡터(向量)와 왼쪽을 향하는 방향 벡터를 캐릭터의 머테리얼(材质) 안으로 전달(传)합니다.

즉 아래 이미지 하단에 있는 저 두 화살표의 방향을 머테리얼로 넘겨주는 겁니다:

  • 앞 방향과 왼쪽 방향 벡터를 외적(叉乘)하여 윗 방향을 가리키는 벡터를 얻습니다.
  • 광원의 방향과 얼굴이 왼쪽을 향하는 벡터를 내적(点乘)하고 한 번 step을 걸어줍니다. 이는 나중에 광원이 얼굴의 왼쪽에서 비추는지 오른쪽에서 비추는지 판별(判断)하는 데 쓰입니다.
  • 광원의 방향을 얼굴의 앞 방향과 윗 방향 벡터 각각 내적하여, 광원의 수평 및 수직 끼인각(夹角)의 코사인 값을 얻습니다.

  • 얻어낸 값은 코사인 값이므로 선형(线性)이 아닙니다. 그래서 저는 이것들을 선형 각도로 변환해서 쓰는 걸 즐깁니다:

  • 이 다음으로 샘플링하는 부분은 보기에 조금 번잡(杂乱)합니다.

살짝 쪼개서 살펴보겠습니다:

  • 여기는 두 개의 상수를 선언(定义)했습니다. 아틀라스가 9x9짜리라서 Row=1을 정의했고, Row-1의 의미는 Row-1=8이라는 뜻입니다.

  • 광원이 오른쪽에 있을 때에는 uv를 반전(镜像翻转)합니다.
  • 그리고 uv를 행의 수 9로 나누면, 맵의 왼쪽 위 첫 번째 SDF의 uv를 얻습니다.

  • 그 다음으로 현재 광원의 각도와 가장 가까운 SDF 4장의 uv를 계산해야 합니다. 예를 들어 수평 각도가 100도, 수직 각도가 120도라면 아래에 그려진 네 점의 SDF를 샘플링하고, 이후 가중치(权重)에 따라 보간합니다. 이중 선형 보간(双线性插值)과 매우 흡사합니다.

현재 광원 각도에서 가장 가까운 4장의 SDF 샘플링

  • 여기서 광원과 가장 가까운 행(行)의 수와 열(列)의 수를 계산합니다.

  • 이어서 uv를 계산해 내어, 텍스처를 샘플링하고 광원 결과를 보간해 낸 뒤 마지막으로 색을 입힙니다(上色).

툴의 github 화면:

Yu-ki016/SDFTool: 通过黑白图生成SDF图和SDF图集的小工具

 

GitHub - Yu-ki016/SDFTool: 通过黑白图生成SDF图和SDF图集的小工具

通过黑白图生成SDF图和SDF图集的小工具. Contribute to Yu-ki016/SDFTool development by creating an account on GitHub.

github.com

 

4. 참고(参考)

이차원 캐릭터 카툰 렌더링—얼굴 편:

https://zhuanlan.zhihu.com/p/411188212

 

원문

https://zhuanlan.zhihu.com/p/670837192?share_code=NknbSZNoLWXR&utm_psn=2045214174779900923