UV Export: add option to export UV tiles #104940
@ -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)
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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'
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user