Skip to content

생성된 메시의 구멍은 어디에서 생기는가 ​

글: Oleg Sidorkin, Cinevva CTO 겸 공동 창업자

수정 전후 생성된 새의 날개 뿌리 부분: 왼쪽에는 밝은 초록색 틈이 드러난 회색 덮개가 있고, 오른쪽에는 날개와 몸통이 깔끔하게 접혀 있다
같은 이미지, 같은 생성기, 같은 설정입니다. 왼쪽은 이번 주까지 배포되던 결과이고, 오른쪽은 현재 배포되는 결과입니다.

TRELLIS.2로 새를 생성해 리깅했는데, 애니메이션이 재생되자마자 양쪽 날개 아래에 구멍이 나타났습니다. 몸통 너머가 그대로 보이는 밝은 틈이었습니다. 처음에는 리거를 탓했습니다. 실제로 리거에도 별도의 문제가 있었지만, 구멍은 뼈대가 닿기도 전부터 이미 메시에 존재했습니다. 이 글은 그 뒤에 이어진 조사 과정입니다. 구멍의 정체가 실제로 무엇인지, 효과가 없었던 아홉 가지 시도와 그 이유, 그리고 유일하게 효과가 있었던 방법을 다룹니다. 실패한 시도들도 의도적으로 포함했습니다. 대부분 누구나 먼저 떠올릴 법한 방법이었고, 각각의 실패에서 얻은 교훈이 최종 해결책의 기반이 되었기 때문입니다.

1단계: 무엇을 구멍이라고 부를지 합의하기 ​

우리의 메시 검사기는 경계 에지가 0개라고 보고했고, 광선 패리티 테스트에서도 표면이 닫혀 있다고 나왔습니다. 그래서 저는 팀에 메시가 깨끗하다고 말했습니다. 하지만 그렇지 않았고, 틈을 빨간색으로 표시했던 사람이 이의를 제기한 것이 옳았습니다. 메시는 닫혀 있으면서도 터널을 가질 수 있습니다. 이를 포착하는 척도는 오일러 지표, 즉 V 빼기 E 더하기 F이며, 셸당 핸들 수를 알려 줍니다. 이 새에는 핸들이 69개 있었습니다. 내보낸 결과에서는 모든 벽이 이중으로 생성되어 있었기 때문에, 바깥쪽 벽의 틈이 그 뒤의 빈 공간과 이어지면 이는 개구부가 아니라 핸들이 됩니다. 따라서 열린 에지만 찾는 검사로는 절대 발견할 수 없습니다.

두 번째 교훈은 조금 더 부끄럽습니다. 구멍은 어두울 것이라 생각해 한동안 어두운 픽셀을 조사했습니다. 그러다 먼저 이미지를 보라는 말을 들었습니다. 구멍은 밝았습니다. 회색 주름 사이로 반대편 벽의 초록색 텍스처가 보이는 가느다란 틈이었습니다. 올바른 대상을 지정하지 않은 알고리즘은 아무것도 알려 주지 못하며, 대상을 지정하는 가장 저렴한 방법은 직접 보는 것입니다.

2단계: 구멍이 생기는 단계 찾기 ​

TRELLIS.2는 세 단계로 메시를 만듭니다. 디코더가 1024 세제곱 해상도의 원시 표면을 출력합니다. 협대역 듀얼 컨투어링 단계가 이를 리메시합니다. 그런 다음 디시메이터가 면 수를 10만 개까지 줄이고 텍스처를 베이크합니다. 각 단계가 끝날 때마다 지오메트리를 덤프하고, 매번 동일한 광선 묶음을 날개 뿌리 부분으로 발사했습니다.

문제는 원시 표면에서 시작됩니다. 원시 표면은 열린 시트의 집합입니다. 경계 루프가 266,242개 있었고, 대부분은 쿼드 하나가 빠져 생긴 것이었으며, 비다양체 에지는 65,371개였습니다. 날개 뿌리에서는 테스트 광선의 3분의 1이 그대로 통과했습니다. 다음에 실행되는 구멍 메우기는 거의 아무것도 닫지 못했습니다. 500만 개의 면 중 144개뿐이었습니다. 리메셔는 부호 없는 거리 필드를 사용합니다. 그럴 수밖에 없습니다. 기준으로 삼을 내부와 외부가 없기 때문에 각 시트 주위에 얇은 오프셋 셸을 감쌉니다. 그래서 모든 벽이 약 4복셀 간격으로 이중화되며, 다공성 줄무늬 하나하나가 틈이 있는 접힘으로 변합니다. 이후 디시메이션은 문제를 더 악화시키며, 그 결과도 무작위입니다. 동일한 원시 메시를 두 번 디시메이션했더니 터널이 각각 69개와 107개 생겼습니다.

열린 에지 밀도를 히트맵으로 표시한 새: 목깃, 양쪽 날개 뿌리, 엉덩이와 꼬리깃 끝은 밝게 빛나지만 몸통과 머리는 깨끗하다
원시 추출 결과의 열린 에지 밀도입니다. 목깃, 날개 뿌리, 엉덩이, 꼬리 끝처럼 파츠가 만나는 곳에서만 밝게 나타납니다. 몸통과 머리는 깨끗합니다.

이 맵이 첫 번째 실질적인 단서였습니다. 다공성은 무작위로 나타나지 않습니다. 모든 파츠 접합부에 존재하며, 다른 곳에는 없습니다.

효과가 없었던 방법들 ​

대략 가장 뻔해 보이는 순서대로 시도했습니다.

시도결과
해상도를 1024 대신 1536으로 설정테스트 모델 5개 중 3개에서 더 나빠졌습니다. 해상도가 높아지면 더 많은 얇은 접촉부가 표현되고, 각각이 새로운 접합부가 됩니다
내장 클린업 단계비다양체 에지가 줄기는커녕 늘어났습니다
MeshLib으로 모든 작은 구멍 메우기루프 26만 6천 개를 메웠지만 날개 뿌리는 그대로였습니다. 해당 부위에는 처음부터 스킨이 출력되지 않았기 때문에 메울 루프 자체가 없습니다
와인딩 넘버를 이용한 부호 있는 리메시메시가 수천 개의 셸로 산산조각 납니다. 원시 출력에는 부호를 정할 내부가 없습니다
컨투어링 밴드를 2복셀과 3복셀로 확장전체 터널 수는 줄었지만 틈은 좁아지지 않고 더 넓어졌습니다. 작은 터널들이 합쳐져 큰 터널이 됩니다
복셀 구조의 형태학적 닫기셸이 솔리드화되고, 디코더가 모든 내부 복셀 안에 표면을 생성해 면 수는 4배, 터널은 969개가 되었습니다
최종 메시 전체를 복셀 리메시종수는 0이 되고 틈도 닫혔습니다. 하지만 모든 깃털 에지도 부드러워졌습니다. 보자마자 기각했고, 그 판단은 옳았습니다
디코더의 교차 임곗값에 바이어스 적용열린 에지가 6만 5천 개에서 5만 8천 개로 줄었습니다. 노이즈 수준입니다
누락된 이웃 복셀 보완새 전체에서 560건뿐이었습니다. 노이즈 수준입니다
네 개의 열: 원본 새와 반경을 점차 늘려 복셀 리메시한 세 가지 버전을 날개 뿌리, 10배 확대, 단면 슬래브, 새 전체, 아래쪽 시점으로 비교한다
전역 닫기 결과입니다. 세 번째 열에서는 틈이 닫히고 발가락도 분리된 상태를 유지하지만, 새의 모든 주름도 둥글게 뭉개집니다. 지오메트리에서 “흐릿하다”는 것은 바로 이런 모습입니다.

전역 닫기는 조금 더 자세히 살펴볼 가치가 있습니다. 모델러라면 가장 먼저 떠올릴 방법이기 때문입니다. 간격보다 약간 큰 크기로 복셀 리메시를 실행하는 것입니다. 위상적으로는 효과가 있습니다. 하지만 모델을 왜곡하는 방법이기도 합니다. 생성기가 만든 선명한 깃털 에지를 포기하고 날개 뿌리를 닫는 셈입니다. 아홉 가지 시도 중 두 가지는 터널 수를 줄였지만, 숫자 대신 새를 직접 확인하는 순간 둘 다 기각되었습니다.

코드 읽기 ​

이 시점에서 유일하게 정직한 선택은 디코더를 블랙박스로 취급하기를 멈추고 출력이 메시로 변환되는 방식을 읽어 보는 것이었습니다. TRELLIS.2는 O-Voxel이라는 표현 방식을 사용하며, 추출 코드는 짧습니다. 활성 복셀마다 하나의 듀얼 버텍스와 세 개의 로짓이 있습니다. 각 로짓은 복셀 모서리에서 뻗어 나가는 그리드 에지 하나에 대응하며, “표면이 이 에지를 가로지른다”는 뜻입니다. 로짓이 0보다 크면 해당 에지 주변의 복셀 4개를 연결하는 쿼드를 생성합니다. 그중 하나라도 없으면 쿼드는 아무 경고 없이 버려집니다. 그게 전부입니다. 부호 필드도, 점유 정보도, 내부와 외부의 구분도 모델 어디에도 없습니다.

동일한 새에서 임곗값 적용 전의 로짓을 덤프하도록 디코더를 패치하고 개수를 세었습니다.

누락된 이웃은 270만 개 복셀 중 560개였습니다. 0 근처에 있는 로짓은 800만 개 중 8,600개였습니다. 둘 다 문제가 아니었습니다. 문제는 각 그리드 면 주변의 에지 4개를 확인했을 때 드러납니다. 닫힌 표면은 면의 경계를 짝수 번 가로지릅니다. 하지만 126,542개 면, 즉 전체의 2.3%에서 네 플래그가 서로 일치하지 않았으며, 홀수 교차 면은 정의상 열린 에지 또는 비다양체 에지가 됩니다. 게다가 이러한 불일치는 높은 확신도로 발생했습니다. 교차 수가 1인 면에서 단독 플래그의 로짓은 +10인 반면, 이웃 로짓은 -4였습니다. 플래그 하나를 뒤집어 패리티를 복구하는 데 필요한 비용의 중앙값은 2.3로짓이었습니다. 임곗값이 약간 어긋난 문제가 아닙니다. 모델은 닫힐 수 없는 표면을 확신하고 있었습니다.

어두운 배경 위의 세 개 열: 내보낸 새, 디코더의 원시 출력, 부호를 보정한 버전. 10배와 30배 확대 시 두 원시 버전 모두에서 날개 접합부가 찢어진 톱니 모양의 줄무늬로 보인다
날개 뿌리에서 본 디코더의 원시 출력입니다. 어두운 부분은 반대편이 그대로 보이는 곳입니다. 이 접합부는 쿼드 몇 개가 빠진 것이 아닙니다. 빗살 모양으로 찢어져 있습니다.

그런 다음 일주일 내내 고정 카메라로 바라보던 정확한 표면 지점에서 줄무늬 자체를 측정했습니다. 그 지점으로부터 12복셀 이내에는 평평한 단일 시트에 필요한 것보다 활성 복셀이 2.1배 많았습니다. 비교를 위해 가슴에서는 1.2배였습니다. 날개 아랫면과 몸통이라는 두 스킨이 하나의 복셀 레이어 안에서 나란히 지나가고 있었습니다. 이 표현 방식은 복셀당 버텍스를 하나만 가질 수 있으므로 둘 다 담을 수 없습니다. 학습 중 이러한 복셀의 정답 데이터는 “하나라도 교차하면 참”인 플래그와 함께 두 스킨의 최소제곱 평균으로 만들어지는데, 이 데이터 자체가 모순됩니다. 따라서 모델은 그 위치에 빗살 구조를 충실하게 생성하도록 학습했습니다. 리메셔는 그 빗살 구조를 감싸 슬래브로 만들고, 그 슬래브에 터널이 생깁니다.

줄무늬의 폭은 지오메트리에서 바로 도출됩니다. 대략 1복셀을 접촉각의 탄젠트로 나눈 값입니다. 두 표면이 90도로 만나면 1복셀 너비의 주름이 생기며 문제가 없습니다. 하지만 몸통에 평평하게 붙은 날개, 깃털과 깃털이 맞닿은 곳, 지면 위의 발가락처럼 표면이 완만한 각도로 만나는 곳에서는 줄무늬가 10~15복셀 너비로 생깁니다. 이것으로 다공성 맵이 설명됩니다.

원칙적인 보정 방법도 시도했습니다. 플래그를 가장 잘 설명하는 각 코너의 내부/외부 부호를 추론한 뒤 플래그를 다시 도출하는 방식이었습니다. 하지만 이 방법은 작동할 수 없습니다. 부호 필드는 디코더가 출력한 1복셀 두께의 셸에만 존재하며, 셸 바깥으로 조금만 확장해도 통제할 수 없이 퍼집니다. 이 과정을 12회 반복하자 쓰레기 데이터와 함께 복셀 수만 두 배가 되었습니다.

모델러라면 어떻게 할까 ​

모델러라면 두 스킨을 머리카락 한 올만큼 떨어뜨려 놓지 않을 것입니다. 날개와 몸통을 합집합 처리하고 주름을 남길 것입니다. 전역 닫기도 이 작업을 수행하지만 모든 곳에 적용된다는 것이 문제입니다. 그러니 두 스킨이 있는 곳에만 적용하면 됩니다.

검출 방식의 발상은 간단합니다. 각 복셀에서 4복셀 이내에 있는 활성 이웃을 셉니다. 단일 시트에서는 그 수가 어디서나 같으며 중앙값과 일치합니다. 두 시트가 같은 레이어를 공유하는 곳에서는 약 두 배가 됩니다. 중앙값의 1.5배를 넘는 모든 지점을 표시하고, 표시된 집합을 1복셀만큼 확장한 뒤 각 지점에 단위 큐브를 배치합니다. 그러면 두 스킨 사이의 공간이 솔리드로 바뀌고, 컨투어링 단계에서는 하나의 벽만 보게 되며, 모델의 다른 부분은 전혀 건드리지 않습니다.

날개 뿌리 주변 크롭을 세 가지 방식으로 컨투어링한 모습: 원본 상태, 반경 3으로 두 시트 영역을 채운 상태, 반경 4로 채운 상태. 빨간 점은 이음새를 따라 채운 복셀을 나타낸다. 톱니 모양 이음새가 매끄러운 주름으로 바뀐다
파이프라인을 건드리기 전에 크롭 영역에서 검증한 개념 증명입니다. 빨간색은 채워진 복셀을 나타냅니다. 톱니 모양 이음새는 주름으로 바뀌고 주변 깃털은 움직이지 않습니다.

먼저 MeshLib으로 GPU 컨투어링을 대신해 크롭 영역에서 테스트했습니다. 셸 53개와 핸들 19개가 셸 2개와 핸들 5개로 줄었습니다. 그런 다음 디코더와 내보내기 사이의 실제 파이프라인에 이 방법을 넣고, 같은 새를 실제 리메셔와 실제 텍스처 베이크 과정에 통과시켰습니다.

네 개의 열로 텍스처 적용 결과, 일반 지오메트리, 단면을 비교한다: 원본, 동일한 디코더 출력을 그대로 내보낸 결과, 반경 3과 반경 4로 두 시트 영역을 채운 결과. 마지막 두 열에서는 회색 덮개와 초록색 틈이 사라진다
실제 파이프라인입니다. 첫 번째와 두 번째 열은 동일한 디코더 출력에 무작위 디시메이션을 적용한 경우와 적용하지 않은 경우입니다. 세 번째와 네 번째 열에는 채우기를 적용했습니다.
터널비다양체 에지날개 뿌리 덮개등의 구멍
원본 상태79.5318있음있음
채우기, 반경 384.597사라짐사라짐
채우기, 반경 47238사라짐사라짐

덮개가 사라졌습니다. 뒤쪽에서 바라본 동일한 접합부에 있던 새 등의 두 번째 구멍도 사라졌습니다. 그 구멍은 애초에 제가 추적하던 대상조차 아니었습니다. 깃털은 서로 분리된 상태를 유지하고, 발가락도 붙지 않으며, 큐브가 셸 내부에서 어트리뷰트 볼륨에 이미 데이터가 있는 곳에 배치되므로 텍스처도 정상적으로 베이크됩니다. 4분짜리 작업에 6초가 추가됩니다. 현재는 반경 4가 기본값으로 적용됩니다.

새 전체를 앞, 뒤, 텍스처가 적용된 아래쪽 시점, 일반 지오메트리의 아래쪽 시점에서 본 모습. 네 개의 열은 원본, 그대로 처리한 결과, 반경 3 채우기, 반경 4 채우기다
새 전체입니다. 첫 번째와 두 번째 열에서 등에 보이는 흰색 구멍은 같은 접합부를 뒤에서 본 것입니다. 세 번째와 네 번째 열에서도 생성기가 제대로 만든 모든 디테일이 그대로 유지됩니다.

이 방법으로 해결되지 않는 것, 그리고 여기서 알 수 있는 것 ​

터널 수는 거의 변하지 않았으며, 그 이유도 솔직히 밝혀 두고 싶습니다. 남은 터널은 깃털 끝과 발가락 끝에 있습니다. 얇은 파츠가 끝나면서 이중 벽이 맞물려 닫히는 곳입니다. 크기가 작고 눈에 띄지 않으며, 발생 원인도 다릅니다. 이 채우기 방법은 파츠 사이의 접촉부를 대상으로 하며, 눈에 보이는 구멍은 바로 그곳에 있었습니다. 진짜 해결책은 업스트림에 있습니다. 에지별 플래그 대신 코너별 부호를 예측하는 디코더라면 구조적으로 패리티 일관성을 보장할 수 있습니다. 이전 세대 모델이 바로 이런 방식으로 작동했고, 그 메시가 완전히 밀폐되어 있었던 이유도 여기에 있습니다. 이는 학습 단계의 변경 사항이지, 추론 단계에서 패치할 수 있는 문제가 아닙니다. 그때까지는 스트라이프 공식에서 두 가지 실용적인 결론을 얻을 수 있습니다. 캐릭터를 생성한다면 팔다리가 몸에 납작하게 붙어 있는 포즈보다 몸에서 떨어져 있는 포즈에서 더 깔끔한 메시를 얻을 수 있습니다. 또한 이런 생성기를 기반으로 작업한다면, 숫자를 믿기 전에 오일러 특성으로 메시를 측정하고 직접 눈으로 결과를 확인해야 합니다. 수치는 깨끗하다고 했지만, 두 번이나 틀렸습니다.

계측 디코더, 단계별 덤프, 패리티 분석, 구멍 메우기는 이제 모두 저희 파이프라인에 포함되어 있습니다. 따라서 다음 모델에서 구멍이 생기더라도 일주일이 아니라 몇 분 만에 동일한 방식으로 처리할 수 있습니다.