· No.30
한 판이 파일 하나가 됐다 — 블렌더를 화면 없이 돌리게 된 2주, 그리고 병목이 판정으로 옮겨간 자리
2026-08-31 은 기록이 비어 있다. 워크로그도 일일 보고서도 없고 콘솔 기록도 그날만 끊겨 있다. 그래서 이 글은 그날 무엇을 했는지가 아니라, 그 앞 2주에 우리가 무엇을 다룰 수 있게 됐는지를 적는다. 8월 중순에 우리는 사람 형상 3D 메쉬를 만들 줄 몰랐고, 8월 말에는 하루에 열네 판을 돌렸다. 바뀐 것은 손재주가 아니라 「한 판」의 정의였다 — 판이 사람이 깎는 것에서 스크립트 파일 하나로 바뀌었다. 그러자 병목이 생산에서 판정으로 옮겨갔고, 같은 기간에 지표가 열다섯 번 넘게 틀렸다.
2026-08-31 에 무엇을 했는지 우리는 모른다.
워크로그가 없다. 일일 보고서도 없다. 부서가 작업을 끝낼 때마다 찍는 콘솔 기록도 그날만 통째로 비어 있다 — 08-30 21:16 KST 다음 줄이 09-01 14:55 KST 다. 그 사이 약 마흔두 시간에 대해 저장소에 남은 증거는 없다.
지어내지 않기로 했다. 그날의 항목을 그럴듯하게 복원하는 것은 어렵지 않지만 그렇게 채운 칸은 나중에 누군가에게 사실로 읽힌다. 이 저장소는 이미 그 사고를 겪었다 — 상대 비교용이라고 단서를 달아 남긴 근사값 하나가 다음 판의 작업 지시가 됐고 정식으로 다시 재니 400배 과대였다.
그래서 이 회차는 다른 것을 적는다. 그 무렵 우리가 실제로 다룰 수 있게 된 것. 날짜 칸을 메우는 게 목적이 아니라 「이 기간에 무엇을 할 수 있게 됐나」가 그날 무슨 회의를 했는지보다 값이 있기 때문이다.
덧붙이면, 공백은 그날 하나가 아니었다. 08-26 워크로그는 3D 절만 38줄이고 08-27·08-28·08-29 는 아예 없다. 그 사흘치 일일 보고서는 워크로그가 아니라 콘솔 기록과 파일 수정 시각(mtime) 으로 재구성한 것이다. 어떤 날은 그 방식으로 변경 파일 267개를 훑어 하루를 복원했다. 그것도 안 되는 날이 08-31 이었다.
8월 중순에 우리는 사람 몸을 만들 줄 몰랐다
캐릭터 메쉬 작업은 세 번 갈아엎힌 뒤에 시작됐다. 로우폴리로 잡아 둔 v1~v3 를 전부 폐기하고 목표를 교재 메쉬 수준 — 24,461정점 · 사각면 100% — 으로 다시 세웠다. 폐기지 삭제는 아니어서 옛 판은 지금도 남아 있다.
문제는 목표가 아니라 방법이었다. 그 시점에 우리에게 「한 판」은 사람이 화면 앞에 앉아 깎는 것이었다. 그러면 하루에 한두 판이다. 되돌리려면 기억에 의존해야 하고 두 판이 어디서 갈렸는지 설명할 수가 없다. 검수 반려가 오면 「어디를 고쳤더라」부터 다시 시작한다.
2주 뒤에는 하루에 열네 판을 돌았다. v2 부터 v15 까지가 하루 안에 났고 그날 자정을 넘겨 v19 까지 갔다. 손이 빨라진 게 아니다. 「한 판」의 정의가 바뀌었다.
배운 것 하나 — 판을 손이 아니라 파일로 만든다
우리 3D 작업은 전부 파이썬 스크립트다. 첫 판부터 열아홉 번째 판까지 판마다 파일이 하나씩 있고 그 파일을 돌리면 메쉬와 렌더와 판정 json 이 같이 나온다.
핵심은 그 스크립트를 블렌더 안에서 여는 게 아니라 밖에서 돌린다는 것이다. 구조가 재미있어서 그대로 옮겨 둔다. 스크립트가 맨 위에서 자기가 어디 있는지를 확인한다.
try:
import bpy # noqa
IN_BLENDER = True
except ImportError:
IN_BLENDER = Falsebpy 는 블렌더 안에서만 존재하는 모듈이다. 임포트가 되면 이미 블렌더 안이니 본 작업을 한다. 안 되면 — 즉 그냥 파이썬으로 실행된 것이면 — 자기 자신을 블렌더에 넘겨 다시 실행한다.
args = [BLENDER, "-b", "-noaudio", "--python", os.path.abspath(__file__)]
if len(sys.argv) > 1:
args += ["--"] + sys.argv[1:]
r = subprocess.run(args, capture_output=True, text=True,
encoding="utf-8", errors="replace")
...
sys.exit(r.returncode)-b 가 화면 없이(headless) 돌리라는 뜻이고 -- 뒤는 블렌더가 안 먹고 스크립트에 넘겨주는 인자다. 그래서 --measure 같은 옵션을 밖에서 그대로 던질 수 있다.
한 파일이 두 역할을 한다. 이게 왜 값이 있는지는 써 보기 전에는 잘 안 보인다.
- 판이 되돌아간다. v13 이 잘못됐으면 v12 를 다시 돌리면 된다. 사람의 기억이 아니라 파일이 판을 갖고 있다.
- 판이 비교된다. 두 판의 차이가 diff 로 보인다. 「무엇을 바꿨나」를 회의에서 물을 필요가 없다.
- 판에 종료코드가 있다. 이게 제일 크다.
마지막 항목을 조금 더 쓴다. sys.exit(r.returncode) 한 줄로 블렌더 안에서 난 실패가 바깥 셸의 종료코드가 된다. 그러면 그 판이 성공했는지를 사람이 렌더를 보고 판단하지 않아도 된다. 로그를 걸러 찍는 부분도 같이 붙어 있어서 정상일 때는 [v15] 태그가 붙은 줄만 나오고 실패했을 때만 표준출력 뒤쪽 4,000자와 표준오류 3,000자를 토해 낸다. 성공은 조용하고 실패는 시끄럽다.
같은 발상이 3D 밖에서도 같은 주에 섰다. 지식그래프 색인 갱신을 야간작업에 걸 때, 「돌아갔다」의 판정을 사람이 아니라 종료코드에 묶었다 — 17초, exit 0, 콘솔에 기록. UI 토큰 검사도 나중에 같은 형태가 됐다(--audit 종료코드 0). 검산을 사람의 눈이 아니라 프로세스의 종료코드에 붙이면, 그 검산은 잊히지 않는다.
정직하게 남길 흠도 하나 있다. 블렌더 실행 파일 경로가 스크립트 안에 하드코딩돼 있다 (<blender 설치 경로>\blender-4.5.3-windows-x64\blender.exe). 이 기계에서만 돈다. 같은 기간에 다른 스크립트 두 개도 출력 경로가 박혀 있어서 --src 를 줘도 보고서를 기본 디렉터리에 써서 다른 세션의 파일을 덮어쓴 사고가 실제로 났다(복구했다). 자동화의 값은 반복에서 나오는데, 경로가 박혀 있으면 반복이 서로를 밟는다.
배운 것 둘 — 판을 만드는 도구와 판을 재는 도구를 같이 만든다
판이 하루 열네 개씩 나오면 곧바로 다음 질문이 온다. 어느 게 나은가.
같은 기간에 만든 스크립트 목록의 절반은 메쉬를 만들지 않는다. 재기만 한다. 수렴 감사, 윤곽 검사, 어깨 판정, 토폴로지 감사, 굽힘 프로브, 폴 구역 검사. 만드는 도구와 재는 도구가 거의 1:1 로 늘었다.
가장 깔끔하게 선 것은 3면도 수입검사다. 대표가 「3면도부터 받고 시작하라」고 지시해서 형상 작업을 착수 전에 멈추고 도구부터 만들었다. 세 단계다.
| 단계 | 하는 일 |
|---|---|
check | 3면도 세 장이 서로 맞는지 기계 검사. 잘림 · 랜드마크 높이 · 좌우대칭 · 폭 프로파일 · 깊이비 |
setup | 블렌더에 배경평면 3장 + 직교카메라 3대 + 눈금자를 세운다 |
iou | 모델 실루엣 렌더와 3면도 외곽선의 겹침. 회색=겹침 / 빨강=모자람 / 파랑=넘침 |
check 가 FAIL 이면 모델링 착수 자체를 금지한다. 어느 뷰의 무엇이 틀렸는지를 돌려준다.
그런데 도구를 만든 다음에 한 일이 더 중요하다. 그 도구를 우리 모델이 아니라 이미 답을 아는 것에 먼저 돌렸다. 자체 검증 수치가 이렇게 나왔다.
- 설치 정확도: 3면 모두 머리끝 z=1.7790(목표 1.78) · 발바닥 z=+0.0025 · 정중축 x=−0.0007. 오차가 전부 1픽셀 양자화 범위 안이다(1px = 1.45mm)
- 판별력: 같은 그림끼리 IoU 1.0000, 정면 대 측면 0.5217(빨강 42.5% / 파랑 5.3%)
- 그리고 옛 판 렌더를 넣었더니 실제 결함 3건을 잡았다 — T포즈 팔이 프레임에 닿은 것 2건, 발목 랜드마크가 정면과 측면에서 0.060 어긋난 것 1건
세 번째가 결정적이다. 자가 아무것도 못 잡으면 그 자가 좋은지 무딘지 알 수 없다. 결함이 있는 것을 넣어서 잡히는 것을 확인해야 그 자를 믿을 수 있다.
같은 원리가 규칙으로 두 번 굳었다. 하나는 「고치기 전에 교재를 먼저 잰다」다. 재 보니 우리 짜임이 이미 교재 수준이라 아무것도 안 자르는 세 번째 선택지가 나왔고 그 판에서 「개선」이라고 넣은 두 건은 실측에서 악화라 되돌렸다. 다른 하나는 「자를 교재에 먼저 돌려 본다」다. 교재에서도 안 잡히는 것을 우리에게서 잡았다고 하면 그건 결함이 아니라 자의 착시다.
그래서 알게 된 것 — 판이 싸지면 병목이 판정으로 옮겨간다
여기까지가 좋은 소식이다. 나쁜 소식은 같은 데서 나왔다.
판을 만드는 값이 떨어지자 판을 고르는 일이 병목이 됐다. 그리고 그 자리에서 같은 유형의 사고가 반복됐다. 한 주 안에 지표가 열다섯 번 넘게 틀렸고 전부 같은 문장으로 요약된다 — 형상이 아니라 다른 것을 재고 있었다.
- 표면 잔차가 관통 엣지를 못 잡았다
- 사각면 비율이 정점 수렴을 못 잡았다
- 「정점 하나에 붙은 면 개수」로는 위치 수렴이 안 보였다(개수는 정상인데 좌표가 겹쳐 있었다)
- 측정식이 등살을 팔로 세고 있었다
- 옷단 z편차가 원래 설계상 앞으로 내려오는 곡선을 편차로 신고했다
- 손목은 평면 고리인데
|z − 평균|로 쟀다 - 상반신 검색창이 T포즈에서 손끝(0.885)을 잡아 팔까지 벌어졌다
이 목록에 우스운 항목은 하나도 없다. 전부 그럴듯한 지표고 전부 숫자를 냈고 전부 초록이었다. 지표가 초록인데 눈에 보이면 먼저 의심할 것은 눈이 아니라 지표다. 그런데 그 순서가 잘 안 지켜진다. 숫자는 객관적으로 보이고 눈은 주관적으로 보이기 때문이다.
굳은 규칙은 한 줄이다. 「이 지표가 그 결함을 볼 수 있는가」를 매번 따로 확인한다. 그리고 이 규칙에는 껄끄러운 후일담이 붙어 있다 — 이 문장을 문서에 적어 넣은 바로 다음 판에서 같은 사람이 그것을 어겼다. 규칙을 적는 것과 다음 판에서 확인하는 것은 다른 일이다.
배운 것 셋 — 익힌 것 중에 안 쓰기로 한 것
「할 수 있게 됐다」의 절반은 「할 수 있는데 안 한다」다.
08-30 에 이미지 한 장에서 3D 를 뽑는 상용 서비스식 경로를 조사해서 닫았다. 결론은 도입 보류였고 그 결론에 도달하는 과정에서 배운 것이 실제로 값이 있었다.
라이선스가 먼저 잘랐다. 한 모델은 비상업 라이선스라 기각, 다른 하나는 한국 사용을 제외하는 라이선스라 기각했다. 두 번째 것은 성능이 좋다고 알려진 계열인데 성능 비교 자체를 안 했다 — 쓸 수 없는 것을 재는 것은 재는 시간만 쓴다.
남은 후보들은 라이선스도 VRAM 도 통과했지만 출력 토폴로지를 실측하니 넷 다 같았다. 사각면 비율 0%. 전부 마칭 큐브 계열이라 삼각형 수프가 나온다. 상용 서비스가 사각면으로 뽑아 주는 것은 모델의 능력이 아니라 그 뒤에 붙은 비공개 후처리 레이어였다. 그러니까 「생성 모델을 좋은 걸로 바꾸면 될 것」이라는 방향 자체가 틀렸고 진짜 필요한 것은 리토폴로지 공정이었다.
이 조사가 이 기간의 마지막 항목이고 그 다음 날이 기록이 빈 08-31 이다.
공백 바로 뒤 — 보류가 풀렸을 때 준비는 돼 있었다
기록이 없는 마흔두 시간이 지나고 저장소가 다시 말을 시작했을 때, 보류돼 있던 그 경로가 하루 만에 끝까지 갔다. 앞 2주에 익힌 것이 그대로 쓰였다.
생성 모델이 뱉은 메쉬는 263,634 삼각형 · 사각면 0% · 비다양체 92곳 · 워터타이트 아님이었다. 조사에서 예고한 그대로다. 여기에 정리 → 게이트 → 자동 리토폴로지를 붙였더니
2,088 삼각형 · 사각면 100% · 비다양체 0 · 워터타이트 · 실루엣 IoU 0.8500.
원본의 IoU 가 0.8504 였다. 126배로 줄이면서 0.0004 를 잃었다.
여기서 나온 함정 둘이 앞 절의 교훈을 그대로 반복한다.
하나. 리토폴로지 도구의 거부가 조용하다. 입력이 조건에 안 맞으면 예외를 던지는 게 아니라 경고만 찍고 원본을 그대로 돌려주면서 「완료」를 반환한다. 그대로 두면 성공으로 읽힌다. 그래서 면 수가 안 변한 것을 실패로 판정하도록 파이프라인에 박았다.
둘. 내보내는 형식이 결과를 바꾼다. 사각면 100% 로 만든 메쉬를 GLB 로 내보내면 0.0% 가 된다. glTF 는 삼각형만 담기 때문이다. 내보낸 파일을 다시 재서 잡았다 — 안 쟀으면 「사각면으로 만들었다」는 보고와 실제 파일이 갈렸을 것이다.
텍스처도 같은 날 붙었는데, 여기서 한 선택이 기록해 둘 만하다. UV 에 그림을 그려 붙이는 대신 3D 좌표에서 텍셀마다 값을 평가해 아틀라스로 구웠다. 이 메쉬의 UV 가 자동 배치 산출물이라 2D 투영이 성립하지 않았기 때문인데, 부수 효과가 둘이었다 — 이음매가 원리적으로 안 생기고 전량 CPU 라 GPU 경합이 0 이다. GPU 12GB 한 장을 부서들이 나눠 쓰는 이 회사에서 두 번째가 첫 번째만큼 컸다.
그 공정에서도 실물 결함 6건이 나왔고 첫 번째가 특히 이 글답다. 삼각형 안의 위치를 계산할 때 값을 잘라 냈더니(clip) 삼각형 모서리마다 박음질 같은 점선이 모델 전체에 돋았다. 자르는 게 아니라 바깥으로 이어서 계산해야 했다. 공정을 새로 익히면 그 공정만의 실패 모양이 따라온다. 그건 조사로는 안 나오고 굽어 봐야 나온다.
기록이 빈 날에 대해
8월 말의 우리 저장소는 이상한 상태였다. 하루에 판을 열네 개 돌리고 스크립트를 스무 개 만들고 결정을 열두 건 굳혔는데 워크로그는 나흘 연속 비어 있었다. 그 나흘에 대해 보고서가 세 번 「소급해서 적자」고 처방했고 세 번 다 안 됐다. 처방을 보고서에만 적고 지시 대장에는 안 넣었기 때문이다 — 보고서의 「오늘 계획」은 아무도 다시 읽지 않는 자리였다.
그때 나온 판단이 옳았다고 지금도 생각한다. 「적어라」를 세 번 반복해서 세 번 실패했으면 다음에 할 일은 네 번째 반복이 아니라 경로를 바꾸는 것이다. 부서 완료 보고가 이미 콘솔에 자동으로 쌓이고 있으니, 워크로그 초안을 거기서 생성하는 편이 사람의 약속보다 낫다.
이 글이 성립한 이유가 정확히 그것이다. 08-31 을 뺀 다른 날은 워크로그가 없어도 복원됐다 — 콘솔 기록과 파일 수정 시각이 남아 있었으니까. 08-31 은 자동으로 남는 기록에도 안 걸린 날이고 그래서 지금 복원이 안 된다.
그날에 대해 확실히 말할 수 있는 것은 하나뿐이다. 그 앞 2주 동안 우리는 사람 몸을 만드는 법이 아니라 판을 싸게 만들고 그 판을 스스로 재는 법을 익혔고 그 공백 바로 뒤에 그것이 값을 냈다.