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 재정렬 손실 방지)

문서 탐색


목차

  1. Python 라이브러리 비교
  2. xatlas-python — UV chart + packing 일괄 처리
  3. rectpack — 순수 Python 2D bin packing
  4. trimesh — 3D 메쉬 처리와 GLB 입출력
  5. bpy — Blender 스크립트 자동화
  6. Pillow — 텍스처 합성과 bleeding
  7. libigl — 학술 알고리즘 (LSCM·ARAP)
  8. 기타 보조 라이브러리
  9. Blender 내장 도구와 애드온
  10. Standalone 도구
  11. gltf-transform — glTF 파이프라인 CLI
  12. 실전 워크플로우 예시

Python 라이브러리 비교

라이브러리역할라이선스최신 버전(시점)추천도
xatlas-pythonUV chart 생성 + atlas packingMITv0.0.11 (2025-07)★★★★★
rectpack순수 Python 2D bin packingApache-2.00.2.2 (2021-11)★★★★☆
rectangle-packer (rpack)C/Cython 기반 빠른 2D packingMIT2025 활발★★★★☆
trimesh3D 메쉬 처리 + GLB 입출력MIT4.x (2025)★★★★★
bpyBlender Python APIGPL-3.0Blender 4.x/5.x★★★★★
Pillow텍스처 이미지 합성HPND11.x★★★★★
libiglLSCM/ARAP 등 학술 알고리즘MPL-2.0v2.6.2 (2026-03)★★★☆☆
pygltflibglTF JSON 파싱/편집MIT활발★★★☆☆
pyvista / open3d3D 시각화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 packingPenlect/rectangle-packerMIT
shapely2D polygon 겹침/면적 계산shapely/shapelyBSD
pygltflibPython glTF JSON 파싱/편집pygltflibMIT
pyglet / modernglOpenGL UV 시각화BSD/MIT
pyvistaVTK 기반 3D 시각화pyvista/pyvistaMIT
open3d점군·메쉬 시각화isl-org/Open3DMIT
scipy.ndimage이미지 dilation (bleeding 정확도 ↑)(SciPy 내장)BSD

Blender 내장 도구와 애드온

내장 기능

도구단축키 / 위치용도
Unwrap (LSCM)U → UnwrapLSCM 기반 기본 언래핑
Minimum Stretch (SLIM)U → Minimum Stretch (Blender 4.3+)SLIM 알고리즘. 면 뒤집힘 방지
Smart UV ProjectU → Smart UV Project각도 임계값 기반 자동 seam (건축물에 적합)
Pack IslandsUV Editor → UV → Pack Islands기존 island 재배치
Lightmap PackU → Lightmap Pack라이트맵 전용 균일 배치

Blender 4.x UV 주요 변경사항

버전날짜주요 변경
4.2 LTS2024-07Pack Islands UDIM 지원, Edge/Vertex Sliding 강화
4.32024-11SLIM(Minimum Stretch) 공식 통합, rotate_method=ANY
4.42025-03Geometry Nodes Pack UV Islands 노드, 비정방형 atlas 완전 지원
5.12026-초GPU 가속 packing (Vulkan 백엔드)

주요 애드온

애드온가격Pack 품질UDIMGPU 가속
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 도구

xatlas (C++, header-only)

  • GitHub: https://github.com/jpcy/xatlas
  • 라이선스: MIT
  • 특징: 헤더 전용, 외부 의존성 없음. UE5·Unity 등 다수 엔진에서 사용
  • 원류: Thekla atlas → xatlas로 재작성

Microsoft UVAtlas

Houdini UV Layout SOP

  • Houdini 20.x, UV Layout 3.0 기반
  • Island Stacking, Variable Scaling, UDIM 타겟팅 지원
  • 가격: Houdini Indie 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-transform — glTF 파이프라인 CLI

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 이전에 완료해야 함

문서 탐색


참고 자료