diff --git a/io_mesh_uv_layout/__init__.py b/io_mesh_uv_layout/__init__.py index e875a22ee..72c583a2c 100644 --- a/io_mesh_uv_layout/__init__.py +++ b/io_mesh_uv_layout/__init__.py @@ -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) diff --git a/io_mesh_uv_layout/export_uv_eps.py b/io_mesh_uv_layout/export_uv_eps.py index 679211f9c..dffb30747 100644 --- a/io_mesh_uv_layout/export_uv_eps.py +++ b/io_mesh_uv_layout/export_uv_eps.py @@ -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: diff --git a/io_mesh_uv_layout/export_uv_png.py b/io_mesh_uv_layout/export_uv_png.py index 784337be4..b0c13f6d1 100644 --- a/io_mesh_uv_layout/export_uv_png.py +++ b/io_mesh_uv_layout/export_uv_png.py @@ -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 diff --git a/io_mesh_uv_layout/export_uv_svg.py b/io_mesh_uv_layout/export_uv_svg.py index ac9712b23..a4811ed11 100644 --- a/io_mesh_uv_layout/export_uv_svg.py +++ b/io_mesh_uv_layout/export_uv_svg.py @@ -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'{escape(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'