Python · Blender 도구와 라이브러리
UV unwrapping과 atlas packing을 실제로 수행하는 도구 모음.
Python 자동화부터 Blender 애드온, 그리고 상용 standalone 도구까지 망라한다.
핵심 요약
구분 내용 📖 정의 UV 언래핑·atlas 패킹·텍스처 베이크를 자동화하는 도구·라이브러리 모음 💡 핵심 Python 자동화의 표준 = xatlas + trimesh + Pillow . Blender 자동화는 bpy + Pack Islands 🎯 대상 3D Tiles / glTF 파이프라인을 직접 구축하는 개발자 ⚠️ 주의 Draco 압축은 반드시 atlas 재패킹 이후 에 적용해야 한다 (UV 재정렬 손실 방지)
문서 탐색
목차
Python 라이브러리 비교
xatlas-python — UV chart + packing 일괄 처리
rectpack — 순수 Python 2D bin packing
trimesh — 3D 메쉬 처리와 GLB 입출력
bpy — Blender 스크립트 자동화
Pillow — 텍스처 합성과 bleeding
libigl — 학술 알고리즘 (LSCM·ARAP)
기타 보조 라이브러리
Blender 내장 도구와 애드온
Standalone 도구
gltf-transform — glTF 파이프라인 CLI
실전 워크플로우 예시
Python 라이브러리 비교
라이브러리 역할 라이선스 최신 버전(시점) 추천도 xatlas-python UV chart 생성 + atlas packing MIT v0.0.11 (2025-07) ★★★★★ rectpack 순수 Python 2D bin packing Apache-2.0 0.2.2 (2021-11) ★★★★☆ rectangle-packer (rpack) C/Cython 기반 빠른 2D packing MIT 2025 활발 ★★★★☆ trimesh 3D 메쉬 처리 + GLB 입출력 MIT 4.x (2025) ★★★★★ bpy Blender Python API GPL-3.0 Blender 4.x/5.x ★★★★★ Pillow 텍스처 이미지 합성 HPND 11.x ★★★★★ libigl LSCM/ARAP 등 학술 알고리즘 MPL-2.0 v2.6.2 (2026-03) ★★★☆☆ pygltflib glTF JSON 파싱/편집 MIT 활발 ★★★☆☆ pyvista / open3d 3D 시각화 MIT 활발 ★★★☆☆
xatlas-python — UV chart + packing 일괄 처리
Microsoft UVAtlas의 후속이자 사실상 표준인 C++ xatlas의 Python 바인딩. chart 생성과 atlas packing을 한 번에 처리한다.
pip install xatlas
기본 사용법
import xatlas
import numpy as np
import trimesh
# 1) OBJ 로드
mesh = trimesh.load( "input.obj" )
vertices = np.array(mesh.vertices, dtype = np.float32)
indices = np.array(mesh.faces, dtype = np.uint32)
# 2) Atlas 생성
atlas = xatlas.Atlas()
atlas.add_mesh(vertices, indices)
# Pack 옵션: 여백, texel 밀도 등
pack_options = xatlas.PackOptions()
pack_options.padding = 2 # island 간 여백(픽셀)
pack_options.texels_per_unit = 512.0 # texel density
# Chart 옵션
chart_options = xatlas.ChartOptions()
chart_options.max_chart_area = 0.0 # 0 = 제한 없음
atlas.generate( chart_options = chart_options, pack_options = pack_options)
# 3) 결과 추출
vmapping, indices_out, uvs = atlas[ 0 ] # 첫 번째 메쉬
width = atlas.width # atlas 텍스처 너비(px)
height = atlas.height # atlas 텍스처 높이(px)
utilization = atlas.utilization # pack ratio (0.0~1.0)
print ( f "Atlas: { width } x { height } , 활용도: { utilization[ 0 ] * 100 :.1f } %" )
# 4) 업데이트된 메쉬 재구성
new_vertices = vertices[vmapping] # vmapping 으로 vertex remapping
new_mesh = trimesh.Trimesh(
vertices = new_vertices,
faces = indices_out,
visual = trimesh.visual.TextureVisuals( uv = uvs)
)
new_mesh.export( "output_atlas.glb" )
결과 해석
필드 의미 vmapping새 vertex 인덱스 → 원본 vertex 인덱스 매핑 indices_outatlas 좌표계의 새 face 인덱스 uvs정규화된 UV 좌표(0.0~1.0) atlas.width / height실제 atlas 해상도 (POT 보장은 별도 처리 필요) atlas.utilizationpack ratio (메쉬별 배열)
rectpack — 순수 Python 2D bin packing
MaxRects, Skyline, Guillotine을 모두 구현한 순수 Python 라이브러리. 빠른 프로토타이핑에 유용.
from rectpack import newPacker, PackingMode, PackingBin
from rectpack import MaxRectsBssf # 또는 SkylineMwf, GuillotineBssfSas 등
packer = newPacker(
mode = PackingMode.Offline, # 오프라인: 모든 사각형을 미리 등록
bin_algo = PackingBin. BFF ,
pack_algo = MaxRectsBssf, # MaxRects + Best Short Side Fit
sort_algo = None ,
rotation = True # 90도 회전 허용
)
# Island AABB 크기 등록 (width, height, rid)
islands = [
( 120 , 80 , "body" ),
( 60 , 60 , "eye_L" ),
( 60 , 60 , "eye_R" ),
( 200 , 30 , "belt" ),
]
for w, h, rid in islands:
packer.add_rect(w, h, rid = rid)
# Atlas 빈 크기 등록 (2048×2048 POT)
packer.add_bin( 2048 , 2048 , count = float ( "inf" ))
packer.pack()
for rect in packer.rect_list():
b, x, y, w, h, rid = rect
print ( f " { rid } : bin= { b } pos=( { x } , { y } ) size= { w } x { h } " )
알고리즘 선택 가이드
알고리즘 클래스 추천 용도 MaxRectsBssf, MaxRectsBaf공간 효율 최고 , 일반 atlas 빌드SkylineMwf, SkylineMwfWm빠른 처리 필요 시 GuillotineBssfSas학습용 / 프로토타입
trimesh — 3D 메쉬 처리와 GLB 입출력
import trimesh
# GLB 로드
mesh = trimesh.load( "model.glb" )
# UV 좌표 접근
if hasattr (mesh.visual, "uv" ):
uv = mesh.visual.uv # (N, 2) numpy 배열
texture = mesh.visual.material.image # PIL Image
# xatlas와 연동
import xatlas, numpy as np
atlas = xatlas.Atlas()
atlas.add_mesh(
np.array(mesh.vertices, dtype = np.float32),
np.array(mesh.faces, dtype = np.uint32),
)
atlas.generate()
vmapping, indices, uvs = atlas[ 0 ]
# TextureVisuals 재구성
material = trimesh.visual.material.SimpleMaterial( image = texture)
visual = trimesh.visual.TextureVisuals( uv = uvs, material = material)
new_mesh = trimesh.Trimesh(
vertices = np.array(mesh.vertices)[vmapping],
faces = indices,
visual = visual,
)
new_mesh.export( "repacked.glb" )
bpy — Blender 스크립트 자동화
Blender Python API. CLI에서 헤드리스 모드 로 실행해 batch 처리 가능.
자동 atlas 스크립트
import bpy
def auto_atlas (obj_path: str , output_path: str , atlas_size: int = 2048 ):
# 1) 가져오기
bpy.ops.import_scene.obj( filepath = obj_path)
obj = bpy.context.selected_objects[ 0 ]
bpy.context.view_layer.objects.active = obj
# 2) Edit Mode 진입
bpy.ops.object.mode_set( mode = 'EDIT' )
bpy.ops.mesh.select_all( action = 'SELECT' )
# 3) Smart UV Project (각도 기반 자동 seam)
bpy.ops.uv.smart_project(
angle_limit = 66.0 ,
island_margin = 0.02 ,
area_weight = 0.0 ,
correct_aspect = True ,
scale_to_bounds = False ,
)
# 4) Pack Islands (Blender 4.3+ 옵션 활용)
bpy.ops.uv.pack_islands(
udim_source = 'CLOSEST_UDIM' ,
rotate = True ,
rotate_method = 'ANY' , # Blender 4.3+ : ANY / CARDINAL / NONE
scale = True ,
merge_overlap = False ,
margin_method = 'SCALED' ,
margin = 0.001 ,
)
bpy.ops.object.mode_set( mode = 'OBJECT' )
# 5) GLB 내보내기
bpy.ops.export_scene.gltf(
filepath = output_path,
export_format = 'GLB' ,
export_textures = True ,
export_draco_mesh_compression_enable = False , # 별도 단계에서 처리
)
auto_atlas( "input.obj" , "output.glb" )
CLI 실행
blender --background --python atlas_script.py
--background 덕분에 GUI 없이 서버/CI 환경에서 돌릴 수 있다.
Pillow — 텍스처 합성과 bleeding
from PIL import Image, ImageFilter
def pack_textures_to_atlas (placements, atlas_size = 2048 , padding = 4 ):
"""placements: [(image_path, x, y, w, h), ...]"""
atlas = Image.new( "RGBA" , (atlas_size, atlas_size), ( 0 , 0 , 0 , 0 ))
for img_path, x, y, w, h in placements:
img = Image.open(img_path).convert( "RGBA" ).resize((w, h))
atlas.paste(img, (x, y))
# Bleeding: 경계 픽셀을 padding 만큼 바깥으로 확장
bleed = atlas.filter(ImageFilter.MaxFilter(padding * 2 + 1 ))
mask = atlas.split()[ 3 ] # alpha 채널
result = Image.composite(atlas, bleed, mask)
return result
atlas_img = pack_textures_to_atlas([
( "body.png" , 0 , 0 , 512 , 512 ),
( "face.png" , 512 , 0 , 256 , 256 ),
( "hands.png" , 512 , 256 , 256 , 256 ),
])
atlas_img.save( "atlas_2048.png" )
실전에서는 MaxFilter 대신 dilation 알고리즘(scipy.ndimage.grey_dilation 등)을 사용하는 것이 더 정확하다.
libigl — 학술 알고리즘 (LSCM·ARAP)
학술 수준 unwrapping 알고리즘이 필요할 때 사용. xatlas로 충분한 경우가 대부분.
import igl
import numpy as np
v, f = igl.read_triangle_mesh( "mesh.obj" )
# 경계 버텍스 자동 탐지
bnd = igl.boundary_loop(f)
b = np.array([bnd[ 0 ], bnd[ int ( len (bnd) / 2 )]]) # 고정점 2개
bc = np.array([[ 0.0 , 0.0 ], [ 1.0 , 0.0 ]])
# LSCM 실행
_, uv = igl.lscm(v, f, b, bc) # uv: (N, 2)
# ARAP (As-Rigid-As-Possible) unwrapping
arap_data = igl.arap_precomputation(v, f, 2 ) # dim=2 (UV)
uv_arap = igl.arap_solve(bc.reshape( - 1 , 2 ), arap_data, uv)
기타 보조 라이브러리
라이브러리 역할 GitHub 라이선스 rectangle-packer (rpack) C/Cython 기반 빠른 2D packing Penlect/rectangle-packer MIT shapely 2D polygon 겹침/면적 계산 shapely/shapely BSD pygltflib Python glTF JSON 파싱/편집 pygltflib MIT pyglet / moderngl OpenGL UV 시각화 — BSD/MIT pyvista VTK 기반 3D 시각화 pyvista/pyvista MIT open3d 점군·메쉬 시각화 isl-org/Open3D MIT scipy.ndimage 이미지 dilation (bleeding 정확도 ↑) (SciPy 내장) BSD
Blender 내장 도구와 애드온
내장 기능
도구 단축키 / 위치 용도 Unwrap (LSCM) U → UnwrapLSCM 기반 기본 언래핑 Minimum Stretch (SLIM) U → Minimum Stretch (Blender 4.3+)SLIM 알고리즘. 면 뒤집힘 방지 Smart UV Project U → Smart UV Project각도 임계값 기반 자동 seam (건축물에 적합) Pack Islands UV Editor → UV → Pack Islands기존 island 재배치 Lightmap Pack U → Lightmap Pack라이트맵 전용 균일 배치
Blender 4.x UV 주요 변경사항
버전 날짜 주요 변경 4.2 LTS 2024-07 Pack Islands UDIM 지원 , Edge/Vertex Sliding 강화 4.3 2024-11 SLIM(Minimum Stretch) 공식 통합 , rotate_method=ANY4.4 2025-03 Geometry Nodes Pack UV Islands 노드, 비정방형 atlas 완전 지원 5.1 2026-초 GPU 가속 packing (Vulkan 백엔드)
주요 애드온
애드온 가격 Pack 품질 UDIM GPU 가속 TexTools (Oxicid/TexTools-Blender )무료 (GPL-3.0) 중간 예 아니오 Magic UV (공식 Extension)무료 중간 아니오 아니오 UVPackmaster 4 (uvpackmaster.com )약 $53 매우 높음 예 CUDA/Vulkan Smart UV Project (내장) 무료(내장) 낮음 아니오 아니오 Pack Islands (내장) 무료(내장) 중간(4.3+ ANY 사용 시 높음) 4.2+ 아니오
UVPackmaster 4 vs Blender 내장 Pack Islands
항목 내장 Pack Islands (4.3) UVPackmaster 4 Pack ratio ~85% ~95% 회전 옵션 CARDINAL / ANY 임의 각도 + 그룹 회전 처리 속도 CPU 단일 스레드 GPU 가속(CUDA/Vulkan) Texel Density 균일화 수동 자동 (Tiers) 가격 무료 약 $53 (영구)
Standalone 도구
Microsoft UVAtlas
Houdini UV Layout SOP
Houdini 20.x, UV Layout 3.0 기반
Island Stacking, Variable Scaling, UDIM 타겟팅 지원
가격: Houdini Indie 269/ 년 , C ore 1,995/년
RizomUV
사이트: https://www.rizomuv.com
영구 라이선스 Indie €149.90 (VS) / €299.90 (RS), Rent-to-Own €14.90/월
Windows CUDA GPU 패킹 (CPU 대비 3~4배), 4가지 패킹 전략
Substance Painter
자체 UV packer 내장. 머티리얼별 세트 분리 후 단일 atlas 베이크
glTF 후처리(텍스처 압축, 메쉬 압축, 머티리얼 병합)를 자동화하는 Node.js 도구.
Python에서도 subprocess로 호출해 파이프라인에 통합한다.
주요 명령
# 한 번에 다 처리 (텍스처 KTX2 + Draco 압축 + 머티리얼 병합)
npx @gltf-transform/cli@latest optimize input.glb output.glb \
--texture-compress ktx2 \
--texture-size 2048 \
--draco
# 단계별 처리
npx gltf-transform resize input.glb step1.glb --width 2048 --height 2048
npx gltf-transform ktxdecompress step1.glb step2.glb # 편집용 PNG
npx gltf-transform etc1s step2.glb output.glb # 최종 ETC1S
Python에서 호출
import subprocess
def optimize_glb (input_path: str , output_path: str ):
subprocess.run([
"npx" , "@gltf-transform/cli@latest" , "optimize" ,
input_path, output_path,
"--texture-compress" , "ktx2" ,
"--texture-size" , "2048" ,
"--draco" ,
], check = True )
실전 워크플로우 예시
시나리오: OBJ + 여러 텍스처 → 단일 Atlas GLB
[입력]
├── model.obj
├── diffuse_1.png
├── diffuse_2.png
└── diffuse_3.png
│
▼
[Step 1] trimesh로 OBJ 로드
│
▼
[Step 2] xatlas로 UV atlas 생성
(chart_options + pack_options 설정)
│
▼
[Step 3] vmapping으로 vertex 재구성
UV 좌표 → 픽셀 좌표 변환
│
▼
[Step 4] Pillow로 텍스처 atlas 합성
원본 UV 위치 → atlas 위치로 픽셀 복사
bleeding 처리 (dilation)
│
▼
[Step 5] trimesh TextureVisuals에 atlas 적용
GLB로 저장
│
▼
[Step 6] gltf-transform CLI로 KTX2 압축 + Draco
│
▼
[출력] output_atlas.glb (단일 텍스처, 압축 완료)
최소 동작 스크립트
"""
OBJ + 여러 텍스처 → 단일 Atlas GLB
의존성: pip install xatlas trimesh Pillow numpy
"""
import math, subprocess, numpy as np
from PIL import Image, ImageFilter
import trimesh, xatlas
def to_pot (n: int ) -> int :
return 2 ** math.ceil(math.log2(n))
def obj_to_atlas_glb (obj_path: str , texture_paths: list[ str ], output_path: str ):
# 1) 메쉬 로드
scene = trimesh.load(obj_path, force = "scene" )
combined = trimesh.util.concatenate( list (scene.geometry.values())) \
if isinstance (scene, trimesh.Scene) else scene
vertices = np.array(combined.vertices, dtype = np.float32)
faces = np.array(combined.faces, dtype = np.uint32)
# 2) xatlas atlas 생성
atlas = xatlas.Atlas()
atlas.add_mesh(vertices, faces)
pack_opts = xatlas.PackOptions()
pack_opts.padding = 4
pack_opts.texels_per_unit = 256.0
atlas.generate( pack_options = pack_opts)
vmapping, new_faces, new_uvs = atlas[ 0 ]
atlas_w = to_pot(atlas.width)
atlas_h = to_pot(atlas.height)
print ( f "Atlas: { atlas_w } x { atlas_h } , 활용도: { atlas.utilization[ 0 ] * 100 :.1f } %" )
# 3) 텍스처 합성 (간단한 슬롯 분할 예)
atlas_tex = Image.new( "RGBA" , (atlas_w, atlas_h), ( 128 , 128 , 128 , 255 ))
slot_w = atlas_w // len (texture_paths)
for i, p in enumerate (texture_paths):
tex = Image.open(p).convert( "RGBA" ).resize((slot_w, atlas_h))
atlas_tex.paste(tex, (i * slot_w, 0 ))
# 4) Bleeding (dilation)
dilated = atlas_tex.filter(ImageFilter.MaxFilter( 9 ))
alpha = atlas_tex.split()[ 3 ]
final_tex = Image.composite(atlas_tex, dilated, alpha)
# 5) GLB 저장
new_vertices = vertices[vmapping]
material = trimesh.visual.material.SimpleMaterial( image = final_tex)
visual = trimesh.visual.TextureVisuals( uv = new_uvs, material = material)
new_mesh = trimesh.Trimesh( vertices = new_vertices, faces = new_faces, visual = visual)
new_mesh.export(output_path)
print ( f "GLB 저장: { output_path } " )
# 6) gltf-transform으로 KTX2 + Draco 압축 (선택)
optimized = output_path.replace( ".glb" , "_opt.glb" )
subprocess.run([
"npx" , "@gltf-transform/cli@latest" , "optimize" ,
output_path, optimized,
"--texture-compress" , "ktx2" , "--draco" ,
], check = False )
print ( f "최적화 GLB: { optimized } " )
if __name__ == "__main__" :
obj_to_atlas_glb(
obj_path = "building.obj" ,
texture_paths = [ "wall.png" , "window.png" , "roof.png" ],
output_path = "building_atlas.glb" ,
)
흔한 문제와 해결법
문제 해결 xatlas.utilization이 너무 낮음 (50% 미만)pack_options.padding을 줄이거나, mesh를 segment 단위로 나눠 add_mesh 다회 호출GLB가 뷰어에서 텍스처 없이 표시 TextureVisuals(uv=…) 누락 또는 vmapping 미적용 확인KTX2 결과물이 더 큼 입력 텍스처가 이미 압축돼 있는 경우. --texture-size로 다운스케일 후 재압축 Draco 후 UV가 깨짐 atlas 재패킹은 반드시 Draco 이전 에 완료해야 함
문서 탐색
참고 자료