22.4 KB · 수정 2026-08-23 04:32
목차

kidstoon — 턴어라운드 시트 → 3D 캐릭터 작업 매뉴얼 (TRELLIS v1)

유사작업 매뉴얼. 시작 전에 이 문서를 읽고 순서대로 실행한다. 시행착오로 다시 헤매지 않기 위해 만들었다. 규칙 = rules.md · 상태 = handoff.md · 설계근거 = design.md. 최초 작성 2026-08-23(코디). 실측 근거는 전부 이 날 집PC(RTX 4070 Ti SUPER 16GB)에서 재현한 것이다.


0. 이 파이프라인이 하는 일

2D 턴어라운드 시트 4컷(정면·좌측면·우측면·후면) → 텍스처 입힌 3D 메시(GLB) → Unity Humanoid 리깅.

핵심 구조 = 형상과 색을 다른 입력으로 만든다. 한 입력으로 둘 다 얻으려 하면 반드시 한쪽이 깨진다(§2 실측표). 형상은 rembg 경로를 그대로 두고, 색은 생성 뒤 원화를 직접 투영해 굽는다.

원화 4컷
  ├─(A) rembg 알파 그대로 + 스케일 정규화 ──→ run_multi_image ──→ 메시(형상)
  └─(B) 원본 RGB + 구멍메운 마스크 + 같은 정규화 ──→ bake_texture ──→ 텍스처(색)
                                                          ↓
                                                     GLB → retopo → Unity Humanoid

1. 환경 (한 번만)

항목
실행 머신 집PC(4070 Ti SUPER 16GB). python C:/dev/tools/gpubox/gpubox.py {wake,run,off}
TRELLIS IgorAherne/trellis-stable-projectorzD:\trellis (venv)
설치 kidstoon/tools/trellis_install.bat — 프리빌트 휠이라 CUDA 툴킷 불필요
torch 2.7.0+cu128 고정(휠 ABI. 최신 받으면 설치는 되고 import 에서 죽는다)
작업 파일 미니PC에서 편집 → gpubox.py push <로컬> <원격> → 집PC에서 실행 → g.fetch 로 회수
소요 모델 로드 40~190초 · 생성 6~13초 · bake 8초 · peak VRAM 4.8GB(half)

2. 🔴 절대 하지 말 것 (전부 실측 근거)

판정 지표 = 턴테이블 12뷰의 min(실루엣 폭)/max(실루엣 폭). 입체 0.4~0.83 · 판때기 0.1 이하. trellis_gen.py[shape] 줄로 자동 출력한다.

하면 안 되는 것 폭비율 결과
rembg-fill — 이진화·페더·연결성분 필터 후 구멍메움 0.035 판때기
배경색·연결성으로 직접 컷아웃해 입력 (edge) 0.030 판때기
배경만 다른 색으로 바꿔 RGB 로 입력 (bgswap) 0.026 판때기
배경 교체 후 rembg 로 알파를 새로 뽑아 입력 (rembg-swap) 0.016 판때기(최악)
(대조) 원본 RGB 그대로 0.701 정상
(대조) rembg 알파 그대로 RGBA 전달 0.701 정상(위와 동일 메시)
rembg-raw + 스케일 정규화 0.828 형상 최고점
rembg-holes + 정규화(소프트매트 보존·닫힌 구멍만) 0.817 정상 — 흰비율 0.234→0.257

bake 쪽 금지 항목 - 🔴 simplify=False 로 굽지 마라. UV chart 가 과밀해지고 Unity 폴리곤 예산도 초과한다. 🔑simplify_ratio 는 「남길 비율」이 아니라 「제거할 비율」이다postprocess_mesh docstring 은 "keep" 이라 적었지만 실제 호출은 PyVista.decimate(ratio) 이고 to_glb 문서는 "remove" 규약이다. 즉 0.95 = 95% 제거. 문서를 믿지 말고 감면 후 실제 triangle 수를 찍어 확인한다. - 🔴 bake_texture(mode='fast') 금지 — 여러 관측을 순회하면서도 masks[0] 만 참조한다(다뷰 bake 불가). - 🔴 opt loss 가 떨어진 것만으로 bake 성공을 판정하지 마라. loss 는 카메라 투영 텍스처 샘플의 손실일 뿐이고, 마지막 UV 아틀라스 점유율은 검증하지 않는다. 실측에서 loss 0.507→0.14 인데 텍스처는 전부 검정이었다. - 🔴 원화 직접 bake 결과에 알베도 gain 을 다시 적용하지 마라(이중 보정). 코드가 sheet bake 분기에서 gain=1.0 으로 리셋한다.


3. 단계별 절차

S1. 턴어라운드 시트 확보

S2. 두 갈래 입력 만들기 — cutout_alpha.py

:: (A) 형상용 — 소프트매트 보존 + 닫힌 구멍만 채움 + 스케일 정규화
python cutout_alpha.py --images <4컷> --out <dir>\cut_i --mode rembg-holes --norm-height 880

:: (B) bake 용 — 원본 RGB + 구멍메운 마스크, 같은 정규화
python cutout_alpha.py --images <4컷> --out <dir>\cut_h --mode rembg-fill  --norm-height 880

S3. 형상 생성 + 원화 리텍스처 — trellis_gen.py

python trellis_gen.py --images <cut_g 4장> --sheet-dir <cut_h> --out <out>

S4. 일치도 게이트 — gate_master.py

python tools/gate_master.py <out>/gate-right.png <out>/gate-left.png <out>/gate-front.png

S5. Unity 로 넘기기 (미검증 — 아직 실행 안 함)


4. 파라미터 기준표

구간 파라미터 근거
형상 입력 --mode / --norm-height / --canvas rembg-holes / 880 / 1024 0.817·흰비율 0.257 (raw 는 0.828·0.234)
bake 입력 --mode / 정규화 rembg-fill / 형상과 동일 높이·중심이 같아야 정합된다
형상 precision / mode / steps half / stochastic / 12·12 다른 조합이 더 낫지 않았다
형상 cfg (ss / slat) 7.5 / 3 포크 기본값
메시 --simplify 0.90 (= 90% 제거) faces 20,650 = Unity LOD0 예산
메시 fill_holes true · 0.04 · nbe=int(250·√(1−simplify)) · 1024 · 1000뷰 to_glb 와 같은 공식
bake mode / lambda_tv / steps opt / 0.0 / 2500(고정) fastmasks[0] 버그
bake --texture-size 2048 1024 와 게이트 차이 미미, UV coverage 0.59 로 여유
bake near / far 0.1 / max(100, r*4) 🔴기본 far=10 이 r=11.1 을 잘라낸다(§8)
bake fov / r 자동탐색. 출시 기준 IoU ≥ 0.80 코드 하한 0.55 · 실측 0.817
렌더 알파 검정·흰 두 배경 역산 단색 1회 렌더는 반투명부에 배경색이 밴다
알베도 --target-medv gaussian 경로 0.53 / 원화 bake 는 0(끔) 이중 보정 방지
게이트 폭비율 / total 0.40~0.83 / ≥ 0.80 실측 0.817 / 0.8280 PASS

Unity 폴리곤 예산(미검증, 운영 기준) — Humanoid LOD0 15k~30k tris · LOD1 8k~15k · LOD2 3k~6k. --simplify 0.90 실측 = verts 12,572 · faces 20,650 → LOD0 구간에 그대로 들어간다. 목표 tris 를 바꾸려면 simplify = 1 - 목표/원본면수.


5. 증상별 대처

증상 원인 대처
90도 돌리면 종잇장 / 후면에 정면 얼굴 모델 입력 실루엣을 건드렸다 S2 (A) 를 rembg-raw 로 되돌린다
가슴·앞발·이마가 검은 공동 rembg 가 흰 부위를 배경으로 오려냄 정상. bake 단계에서 원화로 덮는다(S3)
전체가 원화보다 어둡다 TRELLIS 가 균일하게 어둡게 굽는다(medV 0.41 vs 0.53) 알베도 정규화 gain(자동)
게이트는 통과인데 눈으로 이상 게이트는 3뷰만 본다 orbit.mp4 육안 + [shape] 수치 병행
텍스처가 통째로 검정 far 기본값 10.0 이 카메라를 잘랐다(§8) far=max(100, r*4) 를 넘긴다. [uv-audit] raw_nonblack 이 판정값
🔴앞발이 흰색이 아니라 갈색·검정 rembg 가 흰 앞발을 배경으로 오려내 형상 자체에 앞발이 없다. 텍스처로는 못 고친다 — 그 자리엔 구울 표면이 없어 inpaint 가 소매색을 번지게 한다 현재 미해결. 실루엣에 넣는 3방식 전부 붕괴(§2). 대안 = ①캐릭터에 주황 장갑을 씌워 배경과 대비를 준다(장화와 세트라 디자인상 자연스럽다 · 페페 결정 사안) ②§9 전환 경로

6. 도구 목록 (C:\dev\kidstoon\tools)

파일 역할
trellis_install.bat 집PC TRELLIS 설치(프리빌트 휠)
cutout_alpha.py 입력 4모드(rembg-raw / rembg-fill / bgswap / edge) + 스케일 정규화
trellis_gen.py 형상 생성 + 평면검출 + 알베도 정규화 + 원화 리텍스처
gate_master.py 원화 일치도 게이트(deskpet compare_master.py 재사용)
trellis_diag_preprocess.py TRELLIS 가 실제로 보는 입력을 덤프(문제 진단 1순위)
trellis_*_sweep.bat 노브·입력 변형 실측 스윕

7. 한계와 대안 (조사 결과)


7-1. 상하(flip) 규약 — ⚠2026-08-24 정정

🔴아래 3번이 틀렸었다. 「bake 직전 pre-flip 은 맞다, 지우지 마라」가 R3' 의 범인이었다. 정정 근거·증거는 §10.

  1. 파일 원화는 PIL/NumPy 로 그대로 읽는다.
  2. IoU 탐색 결과가 flip_obs=True 일 때 obs/masks 를 먼저 뒤집는다.
  3. ~~bake_texture 호출 직전 항상 np.flipud 해서 넘긴다~~ → 넘기지 마라. 2단계 결과를 그대로 넘긴다. flipud 를 태우면 아틀라스가 표면 기준 상하 반전으로 칠해진다.
  4. bake 가 돌려주는 texGLB 에 그대로 넣는다(trimesh 가 export 때 v 를 뒤집어 규약이 맞는다). nvdiffrast 턴테이블에 넣을 때만 np.flipud(tex) — 단 그 턴테이블은 GLB 와 상하가 반대로 나온다 (원인 미규명, 실측). 그래서 턴테이블은 bake 진단용이고 게이트 이미지는 GLB 에서 직접 렌더한다.

flip=False 로 IoU 가 이겼다는 것은 「파일 원화 마스크가 rast['mask'] 와 같은 상하 규약」이라는 뜻이다. ⚠단 치비 실루엣은 상하가 거의 대칭이라(질량중심 0.514 vs 0.513) IoU 로는 상하를 못 가른다 — 이 탐색을 상하 규약의 근거로 쓰지 마라.


8. ✅해소 — 텍스처 검정의 정체는 far 였다 (2026-08-23)

증상이던 것 = 카메라 정합 정상(IoU 0.816) · 최적화 정상 수렴(loss 0.507→0.14)인데 반환 텍스처 비검정 0.000004.

🔑원인 = bake_texture 의 기본 far=10.0. 원화가 정투영이라 fov 6도로 근사하면 카메라가 r≈11.1 로 멀어지는데, far 를 안 넘기면 메시가 far plane 너머라 내부 래스터가 통째로 빈다 → 모든 픽셀이 texture(0,0) 한 점을 샘플링 → 그 한 점만 관측 평균색으로 수렴한다. loss 가 떨어진 건 그래서고, 비검정 0.000004 = bilinear 이웃 4픽셀이었다. 우리 IoU 탐색 코드는 far=100 을 쓰고 있어서 정합은 맞는데 결과만 검은 모순이 생겼다.

수정 = bake_texture(..., near=0.1, far=max(100.0, r*4)) 한 줄. 비검정 0.000004 → 0.870.

UV 아틀라스 감사 — 계속 켜 둔다

bake_texture 는 loss 만 보고하고 최종 UV raster 를 검증하지 않는다. TRELLIS 소스는 안 건드리고 cv2.inpaint 를 가로채 세 값을 찍는다(trellis_gen.py 에 구현됨. 덤프 = debug_uv_chart.png · debug_texture_pre_inpaint.png).

정상 벗어나면
coverage (UV raster 점유율) 0.55~0.62 0 = UV raster 실패 → 감면비율을 올려 재생성(텍스처 크기 문제 아님)
raw_nonblack (inpaint 이전) > 0.6 0 = 최적화가 색을 못 만든 것 → 클립 평면·카메라·관측을 본다
after (inpaint 이후) > 0.85 raw 는 큰데 0 = inpaint 가 지운 것

두 assert 가 코드에 박혀 있어 검은 텍스처가 GLB 까지 가지 못한다.

함께 고친 것

그 밖


9. 이 경로를 포기해야 할 때

아래 한계가 출시 기준을 넘으면 TRELLIS 를 더 반복하지 말고 경로를 바꾼다.

🔀 전환 경로 = 툰 휴머노이드 베이스 메시를 수동 retopo·리깅한 뒤, 4뷰를 카메라 프로젝션 텍스처로 굽는다. 흰 가슴·앞발처럼 rembg 가 구조로 오인하던 영역도 이 경로에서는 정상 보존된다.



10. ✅해소 — R3' 「Blender 가 텍스처를 섬 단위로 뒤섞는다」의 정체 (2026-08-24)

증상이던 것 = GLB 를 Blender 가 읽으면 얼굴이 배에 거꾸로 붙는다. 게이트 턴테이블은 멀쩡(0.828 PASS). 전역 U/V/180° 변환 4종(→재시험 8종) 전부 불일치. process=False 재수출도 동일.

🔑정체 = 섬 뒤섞임이 아니다. 아틀라스 「내용」이 표면 기준 상하 반전으로 칠해진 것이다. bake_texture 에 관측을 np.flipud 해서 넘기는 바람에, 머리 표면에는 장화 색이, 발 표면에는 얼굴 색이 칠해졌다. 섬 배정(UV↔아틀라스 짝)은 처음부터 정상이었다. 그래서 렌더가 「깨진 그림」이 아니라 「말끔한 거꾸로 고양이」로 나왔고, 그게 사람 눈에 「섬이 뒤섞였다」로 읽혔다.

수정 = pre-flip 제거 한 줄(§7-1 정정). 행프로파일 상관 as-is 0.287 → 0.926.

왜 하루를 태웠나 — 진단을 막은 함정 3개

  1. 🔴게이트 턴테이블이 동어반복이었다. bake 결과를 bake 가 최적화한 그 래스터로 되비추므로 정의상 원화를 재현한다. 그래서 아틀라스가 어떻게 칠해졌든 게이트는 통과한다. 실제로 수정 후엔 GLB 가 정상이 되고 게이트 쪽이 거꾸로 나왔다(상관 as-is 0.243 / flip 0.914) — 두 경로가 상하 반대라는 게 이때 드러났다. 판정은 납품물(GLB)로 해야 한다.
  2. 🔴치비 실루엣은 상하가 거의 대칭이다(질량중심 0.514 vs 0.513, 상·하 25% 면적 1.5% 차). 그래서 ①실루엣 IoU 탐색이 상하를 못 가르고 ②「거꾸로 고양이」가 실루엣만으론 정상으로 보인다. ⚠실루엣 IoU 를 상하 규약의 근거로 쓰지 마라. 옆모습은 주둥이(높이 0.64)·꼬리(0.04)가 있어 상하를 잘 가른다(IoU 0.976 vs 0.588) — 상하를 봐야 하면 옆모습을 봐라.
  3. 🔴전역 UV 변환으로 안 풀린다는 사실을 「임포터 탓」으로 읽었다. 정반대다 — UV 로 안 풀리면 UV 가 아니라 내용이 틀린 것이다. 지문을 이렇게 읽어라.

도구 — tools/glb_cpu_render.py (신설)

trimesh·Blender·GPU 없이 GLB 바이트를 직접 struct 파싱해 정투영 래스터하는 순수 numpy 렌더러. 이번 진단의 유일한 비순환 판정자였다(파이프라인의 어떤 규약도 공유하지 않는다). _selfcheck() 로 자기 규약(기하 +Y=위, UV top-left)을 먼저 증명한 뒤 판정에 썼다 — 판정자부터 검증해라.

쓸 곳 명령
GLB 육안 확인 PYTHONUTF8=1 python tools/glb_cpu_render.py <glb> --out <prefix>
규약 자체검사 python -c "import glb_cpu_render as g; g._selfcheck()"

남은 미규명