Files
design2garmentcode-impl/pygarment/meshgen/render/texture_utils.py
2025-07-03 17:03:00 +08:00

307 lines
10 KiB
Python

"""Routines for processing UV coordinated for garments and generating texture maps"""
import numpy as np
import igl
import matplotlib.pyplot as plt
import matplotlib
from pathlib import Path
# SECTION UV islands texture creation
def texture_mesh_islands(
texture_coords, face_texture_coords,
out_texture_image_path: Path,
out_fabric_tex_image_path: Path = None,
out_mtl_file_path: Path = None,
boundary_width=0.3,
dpi=1200,
background_img_path=None,
background_resolution=1.,
uv_padding=3,
mat_name='islands_texture'
):
"""
Returns updated uv coordinates (properly normalized and aligned with the created texture)
"""
all_uvs, boundary_uv_to_draw = unwarp_UV(texture_coords, face_texture_coords, padding=uv_padding)
uv_list, width, height = normalize_UVs(all_uvs, axis_padding=uv_padding) # NOTE !! Axis padding should match the uv padding
# Create image
create_UV_island_texture(
boundary_uv_to_draw, width, height,
texture_image_path=out_texture_image_path,
boundary_width=boundary_width,
dpi=dpi,
preserve_alpha=True
)
# Create image with fabric background
if out_fabric_tex_image_path is not None:
create_UV_island_texture(
boundary_uv_to_draw, width, height,
texture_image_path=out_fabric_tex_image_path,
boundary_width=boundary_width,
dpi=dpi,
background_img_path=background_img_path,
background_resolution=background_resolution,
preserve_alpha=False
)
# Save mtl is requested
if out_mtl_file_path:
save_texture_mtl(
out_mtl_file_path,
out_fabric_tex_image_path.name if out_fabric_tex_image_path is not None else out_texture_image_path.name,
mat_name=mat_name)
return uv_list
def _uv_connected_components(face_texture_coords):
# Find connected components of face and vertex texture coords
face_components = igl.facet_components(face_texture_coords)
vert_components = igl.vertex_components(face_texture_coords)
num_ccs = max(face_components) + 1
return vert_components, face_components, num_ccs
def unwarp_UV(texture_coords, face_texture_coords, padding=3):
# Unwrap uvs for each connected component------------------------
vert_components, face_components, num_ccs = _uv_connected_components(face_texture_coords)
all_uvs = [] # transform all UVs to update obj file
boundary_uv_to_draw = [] # only draw the boundary UVs
translate_Y = 0
translate_X = 0
shells_per_row = int(num_ccs ** 0.5)
column_x_shift = 0
# Loop through each connected component
for i in range(num_ccs):
# Get faces and vertices of connected component
faces_in_cc = np.where(face_components == i)[0]
face_vts_in_cc = face_texture_coords[faces_in_cc]
# get all vertices of connected component
verts_in_cc = np.where(vert_components == i)[0]
all_vert_pos = texture_coords[verts_in_cc]
# Find boundary loop
bound_verts = igl.boundary_loop(face_vts_in_cc)
bound_vert_pos = texture_coords[bound_verts]
# Shift component by bounding box
bbox = bound_vert_pos.min(axis=0), bound_vert_pos.max(axis=0)
bbox_len_Y = (bbox[1][1] - bbox[0][1])
bbox_len_X = (bbox[1][0] - bbox[0][0])
if (i % shells_per_row == 0):
# Start new column
translate_Y = padding
translate_X += (column_x_shift + padding)
column_x_shift = 0 # restart BBOX collection
# Update shift
column_x_shift = max(bbox_len_X, column_x_shift)
# translate boundary positions
verts_translated_bound = [(x + translate_X, y + translate_Y) for x, y in bound_vert_pos]
boundary_uv_to_draw.append(verts_translated_bound)
# translate all positions
verts_translated = [(x + translate_X, y + translate_Y) for x, y in all_vert_pos]
all_uvs.extend(verts_translated)
translate_Y = translate_Y + bbox_len_Y + padding
return all_uvs, boundary_uv_to_draw
def normalize_UVs(all_uvs, axis_padding=3):
# normalize all_uvs
uv_list_raw = np.array(all_uvs)
uv_list = uv_list_raw
norm_x = max(uv_list_raw[:,0]) + axis_padding
uv_list[:,0] = uv_list_raw[:,0] / norm_x
norm_y = max(uv_list_raw[:,1]) + axis_padding
uv_list[:,1] = uv_list_raw[:,1] / norm_y
return uv_list, norm_x, norm_y
def create_UV_island_texture(
boundary_uv_to_draw,
width, height,
texture_image_path,
boundary_width=0.3,
boundary_color='black',
dpi=1200,
color_alpha=0.65,
background_alpha=0.8,
background_img_path=None,
background_resolution=5,
preserve_alpha=True
):
"""Create texture image from the set of UV boundary loops (e.g. sewing pattern panels).
It renders the border of the loops and fills them in with color
Params:
* boundary_uv_to_draw -- 2D list -- sequence of 2D vertices on each of the boundaries. The order is IMPORTANT. The vertices will be connected
by boundary edges sequentially
* width, height -- the dimentions of the UV map
* texture_image_path -- filepath to same a texture image to
* boundary_width -- width of the boundary outline
* dpi -- resolution of the output image
"""
n_components = len(boundary_uv_to_draw)
# Figure size
fig, ax = plt.subplots()
fig.set_size_inches(width / 100, height / 100) # width & height are usually given in cm
# Colors
shift = 0.17
divisor = max(5, n_components)
cmap = matplotlib.colormaps['twilight'] # copper cool spring winter twilight # Using smooth Matplotlib colormaps
color_sample = [cmap((1 - shift) * id / divisor) for id in range(divisor)]
# Background -- garment style
if background_img_path is not None:
back_crop_scale = background_resolution
back_img = plt.imread(background_img_path)
ax.imshow(
back_img[:int(width * back_crop_scale), :int(height * back_crop_scale), :],
extent=[0, width, 0, height],
alpha=background_alpha,
aspect='equal'
)
# Draw the UV island boundaries and fill them up
for i in range(n_components):
polygon_x = [vert[0] for vert in boundary_uv_to_draw[i]]
polygon_x.append(polygon_x[0]) # Loop
polygon_y = [vert[1] for vert in boundary_uv_to_draw[i]]
polygon_y.append(polygon_y[0]) # Loop
color = list(color_sample[i])
color[-1] = color_alpha # Alpha - transparency for blending with backround
plt.fill(polygon_x, polygon_y,
color=color,
edgecolor=boundary_color, linestyle='-', linewidth=boundary_width / 2 # Boundary stylings
)
ax.set_aspect('equal')
# Set the axis to be tight
ax.set_xlim([0, width])
ax.set_ylim([0, height])
# Hide the axis
plt.axis('off')
# Save image
plt.savefig(texture_image_path, dpi=dpi, bbox_inches='tight', pad_inches=0, transparent=preserve_alpha)
# Cleanup
plt.close()
# !SECTION
# SECTION Saving textures information to files
def save_texture_mtl(mtl_file_path, texture_image_name, mat_name='uv_texture'):
new_material_lines = [
f'newmtl {mat_name}\n',
'Ns 0.000000\n',
'Ka 1.000000 1.000000 1.000000\n',
'Ks 0.000000 0.000000 0.000000\n',
'Ke 0.000000 0.000000 0.000000\n',
'Ni 1.000000\n',
'd 1.000000\n',
'illum 1\n',
f'map_Kd {texture_image_name}\n'
]
with open(mtl_file_path, 'w') as file:
file.writelines(new_material_lines)
return mat_name
def save_obj(
output_file_path,
vertices, faces_with_texture, uv_list,
vert_normals=None, mtl_file_name=None, mat_name=None):
"""Save an obj file with a texture information (if provided)"""
with open(output_file_path, 'w') as f:
if mtl_file_name is not None:
f.write(f'mtllib {mtl_file_name}\n')
for v in vertices:
f.write(f"v {v[0]} {v[1]} {v[2]}\n")
for vt in uv_list:
f.write(f"vt {vt[0]} {vt[1]}\n")
if vert_normals is not None:
for vn in vert_normals:
f.write(f"vn {vn[0]} {vn[1]} {vn[2]}\n")
f.write('s 1\n')
if mtl_file_name is not None:
f.write(f'usemtl {mat_name}\n')
if vert_normals is not None:
for v_id0, tex_id0, v_id1, tex_id1, v_id2, tex_id2, in faces_with_texture:
f.write(f"f {v_id0 + 1}/{tex_id0 + 1}/{v_id0 + 1} "
f"{v_id1 + 1}/{tex_id1 + 1}/{v_id1 + 1} "
f"{v_id2 + 1}/{tex_id2 + 1}/{v_id2 + 1}\n")
else:
for v_id0, tex_id0, v_id1, tex_id1, v_id2, tex_id2, in faces_with_texture :
f.write(f"f {v_id0 + 1}/{tex_id0 + 1} "
f"{v_id1 + 1}/{tex_id1 + 1} "
f"{v_id2 + 1}/{tex_id2 + 1}\n")
def add_texture_to_obj(obj_file_path, output_file_path, uv_list, mtl_file_name, mat_name):
# Update OBJ-----------------------------------------------------
with open(obj_file_path, 'r') as file:
lines = file.readlines()
uv_index = 0
updated_lines = []
mtllib_exists = False
inserted = False
s_and_usemtl_lines = ['s 1\n', f'usemtl {mat_name}\n']
for line in lines:
if line.startswith('vt '):
# Format the new UV coordinates
uv = uv_list[uv_index]
new_uv_line = f'vt {uv[0]:.6f} {uv[1]:.6f}\n'
updated_lines.append(new_uv_line)
uv_index += 1
elif line.startswith('mtllib '):
# Ensure the mtllib line points to the correct MTL file
new_mtl_line = f'mtllib {mtl_file_name}\n'
updated_lines.append(new_mtl_line)
mtllib_exists = True
elif line.startswith('f') and not inserted:
# Insert the s and usemtl lines before the first face line
updated_lines.extend(s_and_usemtl_lines)
inserted = True
updated_lines.append(line)
else:
updated_lines.append(line)
# If mtllib line does not exist, add it at the beginning
if not mtllib_exists:
updated_lines.insert(0, f'mtllib {mtl_file_name}\n')
with open(output_file_path, 'w') as file:
file.writelines(updated_lines)
# !SECTION