UV Export: add option to export UV tiles #104940

Merged
Damien Picard merged 4 commits from pioverfour/blender-addons:dp_export_uv_udim into main 2023-10-09 22:32:24 +02:00
4 changed files with 91 additions and 29 deletions

View File

@ -5,7 +5,7 @@
bl_info = { bl_info = {
"name": "UV Layout", "name": "UV Layout",
"author": "Campbell Barton, Matt Ebb", "author": "Campbell Barton, Matt Ebb",
"version": (1, 1, 6), "version": (1, 2, 0),
"blender": (3, 0, 0), "blender": (3, 0, 0),
"location": "UV Editor > UV > Export UV Layout", "location": "UV Editor > UV > Export UV Layout",
"description": "Export the UV layout as a 2D graphic", "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)", description="Export all UVs in this mesh (not just visible ones)",
default=False, 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( modified: BoolProperty(
name="Modified", name="Modified",
description="Exports UVs from the modified mesh", description="Exports UVs from the modified mesh",
@ -73,6 +86,7 @@ class ExportUVLayout(bpy.types.Operator):
default='PNG', default='PNG',
) )
size: IntVectorProperty( size: IntVectorProperty(
name="Size",
size=2, size=2,
default=(1024, 1024), default=(1024, 1024),
min=8, max=32768, min=8, max=32768,
@ -123,9 +137,6 @@ class ExportUVLayout(bpy.types.Operator):
if is_editmode: if is_editmode:
bpy.ops.object.mode_set(mode='OBJECT', toggle=False) 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)) meshes = list(self.iter_meshes_to_export(context))
polygon_data = list(self.iter_polygon_data_to_draw(context, meshes)) polygon_data = list(self.iter_polygon_data_to_draw(context, meshes))
different_colors = set(color for _, color in polygon_data) 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 = obj.evaluated_get(depsgraph)
obj_eval.to_mesh_clear() obj_eval.to_mesh_clear()
tiles = self.tiles_to_export(polygon_data)
export = self.get_exporter() 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: if is_editmode:
bpy.ops.object.mode_set(mode='EDIT', toggle=False) bpy.ops.object.mode_set(mode='EDIT', toggle=False)
@ -161,6 +199,30 @@ class ExportUVLayout(bpy.types.Operator):
continue continue
yield obj 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 @staticmethod
def currently_image_image_editor(context): def currently_image_image_editor(context):
return isinstance(context.space_data, bpy.types.SpaceImageEditor) return isinstance(context.space_data, bpy.types.SpaceImageEditor)

View File

@ -5,19 +5,19 @@
import bpy 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: 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) 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 header(width, height)
if opacity > 0.0: if opacity > 0.0:
name_by_color = {} name_by_color = {}
yield from prepare_colors(colors, 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_colored_polygons(tile, face_data, name_by_color, width, height)
yield from draw_lines(face_data, width, height) yield from draw_lines(tile, face_data, width, height)
yield from footer() yield from footer()
@ -53,24 +53,24 @@ def prepare_colors(colors, out_name_by_color):
yield "} def\n" 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: 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 "closepath\n"
yield "%s\n" % name_by_color[color] 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: 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 "closepath\n"
yield "stroke\n" yield "stroke\n"
def draw_polygon_path(uvs, width, height): def draw_polygon_path(tile, uvs, width, height):
yield "newpath\n" yield "newpath\n"
for j, uv in enumerate(uvs): 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: if j == 0:
yield "%.5f %.5f moveto\n" % uv_scale yield "%.5f %.5f moveto\n" % uv_scale
else: else:

View File

@ -15,14 +15,14 @@ except ImportError:
oiio = None 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 = gpu.types.GPUOffScreen(width, height)
offscreen.bind() offscreen.bind()
try: try:
fb = gpu.state.active_framebuffer_get() fb = gpu.state.active_framebuffer_get()
fb.clear(color=(0.0, 0.0, 0.0, 0.0)) 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 = fb.read_color(0, 0, width, height, 4, 0, 'UBYTE')
pixel_data.dimensions = width * height * 4 pixel_data.dimensions = width * height * 4
@ -32,11 +32,11 @@ def export(filepath, face_data, colors, width, height, opacity):
offscreen.free() offscreen.free()
def draw_image(face_data, opacity): def draw_image(tile, face_data, opacity):
gpu.state.blend_set('ALPHA') gpu.state.blend_set('ALPHA')
with gpu.matrix.push_pop(): 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)) gpu.matrix.load_projection_matrix(Matrix.Identity(4))
draw_background_colors(face_data, opacity) draw_background_colors(face_data, opacity)
@ -45,11 +45,11 @@ def draw_image(face_data, opacity):
gpu.state.blend_set('NONE') 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 maps x and y coordinates from [0, 1] to [-1, 1]'''
matrix = Matrix.Identity(4) matrix = Matrix.Identity(4)
matrix.col[3][0] = -1 matrix.col[3][0] = -1 - (tile[0]) * 2
matrix.col[3][1] = -1 matrix.col[3][1] = -1 - (tile[1]) * 2
matrix[0][0] = 2 matrix[0][0] = 2
matrix[1][1] = 2 matrix[1][1] = 2

View File

@ -7,15 +7,15 @@ from os.path import basename
from xml.sax.saxutils import escape 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: 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) 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 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() yield from footer()
@ -29,7 +29,7 @@ def header(width, height):
yield f'<desc>{escape(desc)}</desc>\n' 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: for uvs, color in face_data:
fill = f'fill="{get_color_string(color)}"' fill = f'fill="{get_color_string(color)}"'
@ -39,7 +39,7 @@ def draw_polygons(face_data, width, height, opacity):
yield ' points="' yield ' points="'
for uv in uvs: 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 f'{x*width:.3f},{y*height:.3f} '
yield '" />\n' yield '" />\n'