From 122534edfb8a0a9a4f84912f19e698fee50745dd Mon Sep 17 00:00:00 2001 From: Damien Picard Date: Mon, 9 Oct 2023 22:32:20 +0200 Subject: [PATCH] UV Export: add option to export UV tiles Tiles can now be exported, with either the UDIM or UVTILE numbering scheme. Exporters for PNG, SVG and EPS were updated. The vector formats simply offset the view, so all UVs can end up being exported multiple times. Only tiles containing UV points will be exported. In theory, this may result in some polygons being ignored, if they cross a whole otherwise empty tile, but this should not happen in practice. Fixes #74325 Pull Request: https://projects.blender.org/blender/blender-addons/pulls/104940 --- io_mesh_uv_layout/__init__.py | 72 +++++++++++++++++++++++++++++++++++--- io_mesh_uv_layout/export_uv_eps.py | 22 ++++++------ io_mesh_uv_layout/export_uv_png.py | 14 ++++---- io_mesh_uv_layout/export_uv_svg.py | 12 +++---- 4 files changed, 91 insertions(+), 29 deletions(-) diff --git a/io_mesh_uv_layout/__init__.py b/io_mesh_uv_layout/__init__.py index e875a22e..72c583a2 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 679211f9..dffb3074 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 784337be..b0c13f6d 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 ac9712b2..a4811ed1 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' -- 2.11.4.GIT