툰 셰이딩에서 명암처리는 아직 내가 모르는 부분이 더 많다. 그래서 깊게 파고들면 물리엔진을 개발하려고 했을 때처럼 너무 다른길로 새는 결과를 초래할까봐 DirectX12를 공부하면서 참고한 책의 연습문제에서 나왔던 아주 기초적인 툰 셰이더 코드를 거의 그대로 사용하고 있었다. 신경쓰지 않겠다고 다짐하기야 했다만 눈에 거슬리는 어색함은 진짜 어쩔 수 없는지라.. 이것저것 수치를 조금씩 건드려보기는 했다만 금세 내가 어찌할 수 있는 영역이 아니라는 것을 깨닳고 엔진 주기능 개발로 돌아왔다.
그럼 이 포스팅을 도대체 왜 쓰고있는거냐? 당장 작업하고 있는 부분은 셰이더 코드에 대한 대대적인 리팩토링과 추가하지 않았던 기능들의 구현이다. 특히 광원 처리와 관련된 부분을 건드리면서 필연적으로 머터리얼쪽도 손을 봐야했다. Assimp를 사용해서 외부 리소스를 불러오는데는 성공했지만 머터리얼 값을 불러오는 부분은 구현돼있지 않았고, 이 부분을 건드리니 뭔가 렌더링 결과물이 이상했다.

일단 바로 눈에 보이는건 미유의 얼굴과 바닥 평면이 검은색으로 렌더링 되고 있다는 것, 그리고 눈에 띄게 전체적인 모델의 밝기가 확 줄었다는 것이다. 모델에 있던 머터리얼 값을 불러들이지 않고 임의로 초기값을 세팅해서 여태 렌더링했기 때문에 이런 문제가 있을줄은 전혀 상상도 못했다. 당연히 머터리얼 값을 읽는 부분을 만들고 나서 이런 문제가 발생 했으니 머터리얼 부분부터 이슈를 찾아가야했다.
아니나 다를까, 애초에 모델에 저장돼있는 모든 머터리얼의 Ambient 값이 rgba 모두 0이었다. 근데 그럼 또 의문이 드는게 왜 다른 부분들은 희미하게나마 렌더링이 되고있고 얼굴부분만 렌더링이 되지 않고있는 것이냐는 말이다. 얼굴 부분만 rgba 모두 0을 갖고 다른 부분들은 그 이상의 유의미한 값을 가지고 있는지 찾아봤더니, Diffuse와 Specular가 그랬다. 그러니까 지금 저 프레임에서 희미하게나마 미유의 일부분을 볼 수 있는건 Diffuse연산의 결과로 명암처리된 결과가 렌더링되고 있기 때문이다.
코드에는 문제가 없어보였다. 그럼 모델의 문제인가? 난 모델을 구했던 사이트에 들어가서 모델의 상세 정보를 하나하나 확인해 내려갔다. 그러다가 내가 구현하지 않은 한 가지를 알았다. 결론부터 말하자면 모델의 머터리얼 값에는 전혀 문제가 없었다.

이게 사이트에서 제공하는 모델의 최종 렌더링 모습이다. 물론 포스트 프로세싱이 들어갔지만 얼굴이 출력되고 안되고에 영향을 줄 단계는 아니기 때문에 상관이 없다. 우리가 여기서 봐야되는건 Material Channels라고 되어있는 카테고리의 탭들이다.

보다시피 좌측의 패널에서 Base Color를 확인하면 엔진에서 렌더링된 결과와 동일한 모습을 보여준다. 최종 렌더링 결과물은 정상인데 Base Color는 엔진에서 렌더링됐던 이상한 결과물과 동일하다..? 그럼 머터리얼의 다른 부분에서 내가 놓치고 있는 것이 분명히 있다는 뜻이다.

뭐야 Emission 탭을 확인하니 모델이 모두 멀쩡하게 렌더링 되고있다. 그럼 모델에서 자체적으로 픽셀 값을 생성하고있다고..? 여기서 우리는 Base Color탭에서의 장면과 Emission탭에서의 장면의 가장 큰 차이인 얼굴에 집중을 해야한다.
포스팅의 주제인 툰 셰이딩으로 돌아와서, 대체 왜 저 모델은 얼굴에만 Base Color값이 비어있는 것일까? 왜 Diffuse나 Specular의 영향을 받지 않고 오히려 Emission값을 가져서 스스로 픽셀 값을 내뿜는걸까? 이건 얼굴에 생기는 명암으로인한 이질감 때문이다. 사실 가장 앞서 설명했듯이 필자는 툰 셰이딩을 자연스럽게 보이도록 하기 위해서 값을 이리저리 바꿔봤지만 도저히 얼굴에 생기는 명암으로인한 어색함을 어떻게 할 수가 없었다.

노션에 있던 사진을 급하게 가져오느라.. 사실 코드를 이전 상태로 작성하는게 귀찮아서 개발하면서 정리해둔 자료를 확대해서 가져오느라 화질이 조금 좋지 않다. 하지만 딱 봐도 얼굴에 생긴 명암이 위에서 봤던 모델의 모습과는 많이 다르지 않은가? 그렇다. 이런 서브컬쳐풍의, 툰 셰이더를 적용한 모델은 얼굴에 Diffuse에 의한 명암이 아주 적게 적용되거나 아예 적용이 되어서는 안된다.

단적인 예시가 뭐가 있을까 찾아보다가 가져온 사진이다. 캐릭터의 얼굴을 보면 거의 명암이 없는데 반해 몸이나 옷, 머리카락 등에는 명암이 확실하게 표현되어있다. 실제 일러스트를 그릴 때는 여러 가지 명암 기법이 사용되겠지만, 적어도 3D 그래픽에서 서브컬쳐풍의 캐릭터를 표현할 때는 얼굴 명암 표현에 소극적이어야 그 효과가 잘 나타난다는 뜻이다.
https://www.youtube.com/watch?v=ohNADs4Yiko
사진으로만 표현하자니 뭔가 확 와닿지 않아서 얼마전에 접했던 MMD 영상을 가져왔다. 영상에서 시시각각 움직이는 포즈에 따라 옷이나 다리부분의 명암은 극적으로 변하지만 얼굴은 전혀 그렇지 않다.
슬슬 퍼즐이 맞춰지지 않는가? 얼굴에 대한 명암 표현이 덜 될수록 캐릭터의 표현이 더 잘된다. 따라서 광원에 직접적인 영향을 받는 Diffuse나 Specular의 값은 0이 되어야한다. 하지만 그렇게되면 광원효과를 받지 못해서 얼굴 표현 자체가 불가능해지니 Emission으로 직접 밝게 빛나야한다.
그렇다. 난 지금까지 Emission을 전혀 구현해두지 않았었다. 구조체로 데이터를 받고는 있었지만 그 데이터를 연산하는 마땅한 셰이더 코드를 작성하지 않고 있었다. 코드 자체는 Ambient만큼이나 간단하게 구현했다. 다만 엔진쪽에서 광원 데이터를 넘겨줄 때 초기값이 뭔가 잘못들어간 것인지 전역조명 이외의 연산을 하기위한 반복문 코드가 들어가니 렌더링 결과가 이상해져서 이부분만 주석처리를 하니 만족할만한 결과물이 나왔다.
float4 ProcessEmissive(float4 emissive, float4 albedo)
{
float4 totalEmissive;
totalEmissive = emissive * albedo;
return totalEmissive;
}
float4 ComputeLight(Material mat, float4 albedo, float3 normal, float3 eyeDir)
{
float4 totalColor = { 0.0f, 0.0f, 0.0f, 0.0f };
// Global Light Process
{
float3 lightDir = normalize(GlobalLight.Direction);
float4 ambient = ProcessAmbient(GlobalLight.Ambient * mat.Ambient, albedo);
float4 diffuse = ProcessDiffuse(GlobalLight.Diffuse * mat.Diffuse, albedo, lightDir, normal);
float4 specular = ProcessSpecular(GlobalLight.Specular * mat.Specular, mat.Shiness, lightDir, normal, eyeDir);
float4 emissive = ProcessEmissive(mat.Emmissive, albedo);
//totalColor = ambient + diffuse + specular;
totalColor = ambient + diffuse;
}
// General Light Process
{
for (int i = 0; i < MaxLights; i++)
{
float4 diffuse = ProcessDiffuse(Lights[i].Diffuse * mat.Diffuse, albedo, Lights[i].Direction, normal);
float4 specular = ProcessSpecular(Lights[i].Specular * mat.Specular, mat.Shiness, Lights[i].Direction, normal, eyeDir);
//totalColor += diffuse + emissive;
}
}
float4 emissive = ProcessEmissive(mat.Emmissive, albedo);
//totalColor += emissive;
return totalColor;
}
사실 오늘 포스팅은 Emission부분이 가장 중요했던거라 다른 부분들의 코드는 생략했다.

최종 렌더링 결과물이다. 물론 지금 결과물도 완벽하진 않다. 그림자 처리도 되고있지 않으며, 옆에 붙어있는 나뭇잎의 텍스쳐는 투명도 설정을 해줘야되는데 그것도 안되있고.. 하지만 서브컬쳐풍의 캐릭터 렌더링 방법에 대해 큰 깨닳음을 얻을 수 있었던 좋은 디버깅 과정이었던 것 같다.
참고로 오늘 설명한 방법 이외에도 따로 얼굴부분에만 명암 적용의 한계치를 걸어주는 등 다양한 기법이 있는듯 하다. 그 중에는 미호요와 같은 가닥있는 회사에서 사용한 신기한 방법이 있는데.. 이건 텍스처 리소스 단계에서부터 준비돼야하는 부분인듯 싶어 이런게 있구나 정도로 알고 넘어가면 될듯 싶다.
'공부 > DirectX12' 카테고리의 다른 글
| DirectX12 ImGUI 라이브러리 세팅 (0) | 2025.04.15 |
|---|---|
| 메시와 머터리얼의 관계에 따른 문제 발생 및 해결 (0) | 2025.04.10 |
| 앞으로의 개발 목표 정리 (0) | 2025.03.29 |
| DirectX 인덱스 버퍼 버그 해결 (0) | 2025.03.23 |
| DirectX12 인덱스 버퍼 데이터 버그 디버깅 중.. (0) | 2025.03.23 |