UV Export: add option to export UV tiles #104940
@ -5,7 +5,7 @@
|
||||
bl_info = {
|
||||
"name": "UV Layout",
|
||||
"author": "Campbell Barton, Matt Ebb",
|
||||
"version": (1, 1, 6),
|
||||
"version": (1, 2, 0),
|
||||
"blender": (3, 0, 0),
|
||||
"location": "UV Editor > UV > Export UV Layout",
|
||||
"description": "Export the UV layout as a 2D graphic",
|
||||
@ -54,6 +54,19 @@ class ExportUVLayout(bpy.types.Operator):
|
||||
description="Export all UVs in this mesh (not just visible ones)",
|
||||
default=False,
|
||||
)
|
||||
export_tiles: EnumProperty(
|
||||
name="Export Tiles",
|
||||
items=(
|
||||
('NONE', "None",
|
||||
"Export only UVs in the [0, 1] range"),
|
||||
('UDIM', "UDIM",
|
||||
"Export tiles in the UDIM numbering scheme: 1001 + u-tile + 10*v-tile"),
|
||||
('UV', "UVTILE",
|
||||
"Export tiles in the UVTILE numbering scheme: u(u-tile + 1)_v(v-tile + 1)"),
|
||||
),
|
||||
description="Choose whether to export only the [0, 1 range], or all UV tiles",
|
||||
default='NONE',
|
||||
)
|
||||
modified: BoolProperty(
|
||||
name="Modified",
|
||||
description="Exports UVs from the modified mesh",
|
||||
@ -73,6 +86,7 @@ class ExportUVLayout(bpy.types.Operator):
|
||||
default='PNG',
|
||||
)
|
||||
size: IntVectorProperty(
|
||||
name="Size",
|
||||
size=2,
|
||||
default=(1024, 1024),
|
||||
min=8, max=32768,
|
||||
@ -123,9 +137,6 @@ class ExportUVLayout(bpy.types.Operator):
|
||||
if is_editmode:
|
||||
bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
|
||||
|
||||
filepath = self.filepath
|
||||
filepath = bpy.path.ensure_ext(filepath, "." + self.mode.lower())
|
||||
|
||||
meshes = list(self.iter_meshes_to_export(context))
|
||||
polygon_data = list(self.iter_polygon_data_to_draw(context, meshes))
|
||||
different_colors = set(color for _, color in polygon_data)
|
||||
@ -135,8 +146,35 @@ class ExportUVLayout(bpy.types.Operator):
|
||||
obj_eval = obj.evaluated_get(depsgraph)
|
||||
obj_eval.to_mesh_clear()
|
||||
|
||||
tiles = self.tiles_to_export(polygon_data)
|
||||
export = self.get_exporter()
|
||||
export(filepath, polygon_data, different_colors, self.size[0], self.size[1], self.opacity)
|
||||
dirname, filename = os.path.split(self.filepath)
|
||||
|
||||
# Strip UDIM or UV numbering, and extension
|
||||
import re
|
||||
name_regex = r"^(.*?)"
|
||||
udim_regex = r"(?:\.[0-9]{4})?"
|
||||
uv_regex = r"(?:\.u[0-9]+_v[0-9]+)?"
|
||||
ext_regex = r"(?:\.png|\.eps|\.svg)?$"
|
||||
if self.export_tiles == 'NONE':
|
||||
match = re.match(name_regex + ext_regex, filename)
|
||||
elif self.export_tiles == 'UDIM':
|
||||
match = re.match(name_regex + udim_regex + ext_regex, filename)
|
||||
elif self.export_tiles == 'UV':
|
||||
match = re.match(name_regex + uv_regex + ext_regex, filename)
|
||||
if match:
|
||||
filename = match.groups()[0]
|
||||
|
||||
for tile in sorted(tiles):
|
||||
filepath = os.path.join(dirname, filename)
|
||||
if self.export_tiles == 'UDIM':
|
||||
filepath += f".{1001 + tile[0] + tile[1] * 10:04}"
|
||||
elif self.export_tiles == 'UV':
|
||||
filepath += f".u{tile[0] + 1}_v{tile[1] + 1}"
|
||||
filepath = bpy.path.ensure_ext(filepath, "." + self.mode.lower())
|
||||
|
||||
export(filepath, tile, polygon_data, different_colors,
|
||||
self.size[0], self.size[1], self.opacity)
|
||||
|
||||
if is_editmode:
|
||||
bpy.ops.object.mode_set(mode='EDIT', toggle=False)
|
||||
@ -161,6 +199,30 @@ class ExportUVLayout(bpy.types.Operator):
|
||||
continue
|
||||
yield obj
|
||||
|
||||
def tiles_to_export(self, polygon_data):
|
||||
"""Get a set of tiles containing UVs.
|
||||
This assumes there is no UV edge crossing an otherwise empty tile.
|
||||
"""
|
||||
if self.export_tiles == 'NONE':
|
||||
return {(0, 0)}
|
||||
|
||||
from math import floor
|
||||
tiles = set()
|
||||
for poly in polygon_data:
|
||||
for uv in poly[0]:
|
||||
# Ignore UVs at corners - precisely touching the right or upper edge
|
||||
# of a tile should not load its right/upper neighbor as well.
|
||||
# From intern/cycles/scene/attribute.cpp
|
||||
u, v = uv[0], uv[1]
|
||||
x, y = floor(u), floor(v)
|
||||
if x > 0 and u < x + 1e-6:
|
||||
x -= 1
|
||||
if y > 0 and v < y + 1e-6:
|
||||
y -= 1
|
||||
if x >= 0 and y >= 0:
|
||||
tiles.add((x, y))
|
||||
return tiles
|
||||
|
||||
@staticmethod
|
||||
def currently_image_image_editor(context):
|
||||
return isinstance(context.space_data, bpy.types.SpaceImageEditor)
|
||||
|
@ -5,19 +5,19 @@
|
||||
import bpy
|
||||
|
||||
|
||||
def export(filepath, face_data, colors, width, height, opacity):
|
||||
def export(filepath, tile, face_data, colors, width, height, opacity):
|
||||
with open(filepath, 'w', encoding='utf-8') as file:
|
||||
for text in get_file_parts(face_data, colors, width, height, opacity):
|
||||
for text in get_file_parts(tile, face_data, colors, width, height, opacity):
|
||||
file.write(text)
|
||||
|
||||
|
||||
def get_file_parts(face_data, colors, width, height, opacity):
|
||||
def get_file_parts(tile, face_data, colors, width, height, opacity):
|
||||
yield from header(width, height)
|
||||
if opacity > 0.0:
|
||||
name_by_color = {}
|
||||
yield from prepare_colors(colors, name_by_color)
|
||||
yield from draw_colored_polygons(face_data, name_by_color, width, height)
|
||||
yield from draw_lines(face_data, width, height)
|
||||
yield from draw_colored_polygons(tile, face_data, name_by_color, width, height)
|
||||
yield from draw_lines(tile, face_data, width, height)
|
||||
yield from footer()
|
||||
|
||||
|
||||
@ -53,24 +53,24 @@ def prepare_colors(colors, out_name_by_color):
|
||||
yield "} def\n"
|
||||
|
||||
|
||||
def draw_colored_polygons(face_data, name_by_color, width, height):
|
||||
def draw_colored_polygons(tile, face_data, name_by_color, width, height):
|
||||
for uvs, color in face_data:
|
||||
yield from draw_polygon_path(uvs, width, height)
|
||||
yield from draw_polygon_path(tile, uvs, width, height)
|
||||
yield "closepath\n"
|
||||
yield "%s\n" % name_by_color[color]
|
||||
|
||||
|
||||
def draw_lines(face_data, width, height):
|
||||
def draw_lines(tile, face_data, width, height):
|
||||
for uvs, _ in face_data:
|
||||
yield from draw_polygon_path(uvs, width, height)
|
||||
yield from draw_polygon_path(tile, uvs, width, height)
|
||||
yield "closepath\n"
|
||||
yield "stroke\n"
|
||||
|
||||
|
||||
def draw_polygon_path(uvs, width, height):
|
||||
def draw_polygon_path(tile, uvs, width, height):
|
||||
yield "newpath\n"
|
||||
for j, uv in enumerate(uvs):
|
||||
uv_scale = (uv[0] * width, uv[1] * height)
|
||||
uv_scale = ((uv[0] - tile[0]) * width, (uv[1] - tile[1]) * height)
|
||||
if j == 0:
|
||||
yield "%.5f %.5f moveto\n" % uv_scale
|
||||
else:
|
||||
|
@ -15,14 +15,14 @@ except ImportError:
|
||||
oiio = None
|
||||
|
||||
|
||||
def export(filepath, face_data, colors, width, height, opacity):
|
||||
def export(filepath, tile, face_data, colors, width, height, opacity):
|
||||
offscreen = gpu.types.GPUOffScreen(width, height)
|
||||
offscreen.bind()
|
||||
|
||||
try:
|
||||
fb = gpu.state.active_framebuffer_get()
|
||||
fb.clear(color=(0.0, 0.0, 0.0, 0.0))
|
||||
draw_image(face_data, opacity)
|
||||
draw_image(tile, face_data, opacity)
|
||||
|
||||
pixel_data = fb.read_color(0, 0, width, height, 4, 0, 'UBYTE')
|
||||
pixel_data.dimensions = width * height * 4
|
||||
@ -32,11 +32,11 @@ def export(filepath, face_data, colors, width, height, opacity):
|
||||
offscreen.free()
|
||||
|
||||
|
||||
def draw_image(face_data, opacity):
|
||||
def draw_image(tile, face_data, opacity):
|
||||
gpu.state.blend_set('ALPHA')
|
||||
|
||||
with gpu.matrix.push_pop():
|
||||
gpu.matrix.load_matrix(get_normalize_uvs_matrix())
|
||||
gpu.matrix.load_matrix(get_normalize_uvs_matrix(tile))
|
||||
gpu.matrix.load_projection_matrix(Matrix.Identity(4))
|
||||
|
||||
draw_background_colors(face_data, opacity)
|
||||
@ -45,11 +45,11 @@ def draw_image(face_data, opacity):
|
||||
gpu.state.blend_set('NONE')
|
||||
|
||||
|
||||
def get_normalize_uvs_matrix():
|
||||
def get_normalize_uvs_matrix(tile):
|
||||
'''matrix maps x and y coordinates from [0, 1] to [-1, 1]'''
|
||||
matrix = Matrix.Identity(4)
|
||||
matrix.col[3][0] = -1
|
||||
matrix.col[3][1] = -1
|
||||
matrix.col[3][0] = -1 - (tile[0]) * 2
|
||||
matrix.col[3][1] = -1 - (tile[1]) * 2
|
||||
matrix[0][0] = 2
|
||||
matrix[1][1] = 2
|
||||
|
||||
|
@ -7,15 +7,15 @@ from os.path import basename
|
||||
from xml.sax.saxutils import escape
|
||||
|
||||
|
||||
def export(filepath, face_data, colors, width, height, opacity):
|
||||
def export(filepath, tile, face_data, colors, width, height, opacity):
|
||||
with open(filepath, 'w', encoding='utf-8') as file:
|
||||
for text in get_file_parts(face_data, colors, width, height, opacity):
|
||||
for text in get_file_parts(tile, face_data, colors, width, height, opacity):
|
||||
file.write(text)
|
||||
|
||||
|
||||
def get_file_parts(face_data, colors, width, height, opacity):
|
||||
def get_file_parts(tile, face_data, colors, width, height, opacity):
|
||||
yield from header(width, height)
|
||||
yield from draw_polygons(face_data, width, height, opacity)
|
||||
yield from draw_polygons(tile, face_data, width, height, opacity)
|
||||
yield from footer()
|
||||
|
||||
|
||||
@ -29,7 +29,7 @@ def header(width, height):
|
||||
yield f'<desc>{escape(desc)}</desc>\n'
|
||||
|
||||
|
||||
def draw_polygons(face_data, width, height, opacity):
|
||||
def draw_polygons(tile, face_data, width, height, opacity):
|
||||
for uvs, color in face_data:
|
||||
fill = f'fill="{get_color_string(color)}"'
|
||||
|
||||
@ -39,7 +39,7 @@ def draw_polygons(face_data, width, height, opacity):
|
||||
yield ' points="'
|
||||
|
||||
for uv in uvs:
|
||||
x, y = uv[0], 1.0 - uv[1]
|
||||
x, y = uv[0] - tile[0], 1.0 - uv[1] + tile[1]
|
||||
yield f'{x*width:.3f},{y*height:.3f} '
|
||||
yield '" />\n'
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user