-
-
Save BigRoy/60883eff23f73a34c4671395b32d858d to your computer and use it in GitHub Desktop.
from maya import cmds | |
def get_bounding_box_tiles(bb): | |
u_minmax, v_minmax = bb | |
# If the max is exactly on the integer boundary we allow it to be | |
# part of the tile of the previos integer so we can subtract one. | |
# But since we'll need to add one to iterate "up to" the maximum for | |
# the `range` function we will instead add one to the opposite cases. | |
# Inputs are tuples so don't assign elements but override tuple | |
if u_minmax[1] != int(u_minmax[1]): | |
u_minmax = (u_minmax[0], u_minmax[1] + 1) | |
if v_minmax[1] != int(v_minmax[1]): | |
v_minmax = (v_minmax[0], v_minmax[1] + 1) | |
tiles = [] | |
for v in range(*map(int, v_minmax)): | |
for u in range(*map(int, u_minmax)): | |
tiles.append((u, v)) | |
return tiles | |
def get_uv_udim_tiles(mesh, uv_set=None): | |
"""Return the UV tiles used by the UVs of the input mesh. | |
Warning: | |
This does not capture the case where a single UV shell | |
might be layout in such a way that all its UV points are | |
around another tile since it uses the UV shell bounding | |
box to compute the used tiles. In the image below imagine | |
the lines being the UVs of a single shell and the `x` being | |
an emtpy UV tile. It will not detect the empty tile. | |
/ - \ | |
| x | | |
Args: | |
mesh (str): Mesh node name. | |
uv_set (str): The UV set to sample. When not | |
provided the current UV map is used. | |
Returns: | |
list: sorted list of uv tiles | |
""" | |
kwargs = {} | |
if uv_set is not None: | |
kwargs["uvSetName"] = uv_set | |
bb = cmds.polyEvaluate(mesh, boundingBox2d=True, **kwargs) | |
tiles = get_bounding_box_tiles(bb) | |
if len(tiles) == 1: | |
# If there's only a single tile for the bounding box | |
# it'll be impossible for there to be empty tiles | |
# in-between so we just return the given tiles | |
return tiles | |
# Get the bounding box per UV shell | |
uv_shells = cmds.polyEvaluate(mesh, uvShell=True, **kwargs) | |
if uv_shells == 1: | |
# If there's only a single UV shell it must span | |
# all the UV tiles | |
return tiles | |
tiles = set() | |
for i in range(uv_shells): | |
shell_uvs = cmds.polyEvaluate(mesh, uvsInShell=i, **kwargs) | |
shell_bb = cmds.polyEvaluate(shell_uvs, boundingBoxComponent2d=True, **kwargs) | |
shell_tiles = get_bounding_box_tiles(shell_bb) | |
tiles.update(shell_tiles) | |
return sorted(tiles, key=uv2udim) | |
def uv2udim(tile): | |
"""UV tile to UDIM number. | |
Note that an input integer of 2 means it's | |
the UV tile range using 2.0-3.0. | |
Examples: | |
>>> uv2udim((0, 0) | |
# 1001 | |
>>> uv2udim((0, 1) | |
# 1011 | |
>>> uv2udim((2, 0) | |
# 1003 | |
>>> uv2udim(8, 899) | |
# 9999 | |
Returns: | |
int: UDIM tile number | |
""" | |
u, v = tile | |
return 1001 + u + 10 * v | |
# Example usage | |
for mesh in cmds.ls(selection=True): | |
tiles = get_uv_udim_tiles(mesh) | |
print mesh | |
print tiles | |
for tile in tiles: | |
print uv2udim(tile) |
To speed up one of the examples from the Maya - Fast UDIM tile suggestions topic here's code using Maya Python API 2.0.
import math
import maya.api.OpenMaya as om
def get_uvs(mesh):
"""Return UVs as list of two arrays [u_points, v_points]
Uses Python API 2.0 for optimal perfomance.
"""
sel = om.MSelectionList()
sel.add(mesh)
fn = om.MFnMesh(sel.getDependNode(0))
return fn.getUVs()
def uv2udim(u,v):
"""return UDIM tile corresponding to UV coord"""
return 1001+ int(u) + int(v) * 10
def get_mesh_udim_tiles(mesh):
uvs = get_uvs(mesh)
udims = set()
for u, v in zip(uvs[0], uvs[1]):
udim = uv2udim(u, v)
udims.add(udim)
return sorted(udims)
# Example usage (will go over all meshes in the scene!)
for mesh in cmds.ls(type="mesh", selection=True):
print mesh
print get_mesh_udim_tiles(mesh)
The detection of UDIM tiles here checks solely by a UV point position being in a specific UDIM tile. Since this goes over every UV point this is slower for meshes with lots of points compared to this gist solution - especially if amount of UV shells is relatively limited.
Additionally, the detection will have some issues with UVs being on a UV tile boundary, e.g. a default cube hits the 1.0 of the V-axis and is detected as the second UV tile. The gist code solution above captures those cases correctly.
Similar kind of code as in the gist but then getting the bounding boxes in Python using the UVs and shells we get with Maya Python API 2.0. I'm expecting this to be SLOWER than the gist code at top due to the conversion of all point positions to Python and the computations being done in Python as opposed to the bounding box + points being in C++ when using maya.cmds.polyEvaluate(shell_uvs, boundingBoxComponent2d=True)
.
Plus using this code we can't also opt-out early with the global bounding box if all is in a single tile or if there's only one UV shell. Those are cases that would be comparably extremely fast with the gist code at the top - especially on meshes with a high UV point count.
import math
import maya.api.OpenMaya as om
from collections import defaultdict
def get_bounding_box_tiles(bb):
u_minmax, v_minmax = bb
# If the max is exactly on the integer boundary we allow it to be
# part of the tile of the previos integer so we can subtract one.
# But since we'll need to add one to iterate "up to" the maximum for
# the `range` function we will instead add one to the opposite cases.
# Inputs are tuples so don't assign elements but override tuple
if u_minmax[1] != int(u_minmax[1]):
u_minmax = (u_minmax[0], u_minmax[1] + 1)
if v_minmax[1] != int(v_minmax[1]):
v_minmax = (v_minmax[0], v_minmax[1] + 1)
tiles = []
for v in range(*map(int, v_minmax)):
for u in range(*map(int, u_minmax)):
tiles.append((u, v))
return tiles
def get_uv_tiles(mesh, precision=None):
"""Return UV tiles for mesh"""
sel = om.MSelectionList()
sel.add(mesh)
fn = om.MFnMesh(sel.getDependNode(0))
# Get UV shells and the UV indices
num_shells, shell_indices = fn.getUvShellsIds()
uvs_per_shell = defaultdict(list)
for i, index in enumerate(shell_indices):
uvs_per_shell[index].append(i)
# Define bounding box for each shell
u, v = fn.getUVs()
all_tiles = set()
for shell_index, uv_indices in uvs_per_shell.items():
u_min = min(u[i] for i in uv_indices)
u_max = max(u[i] for i in uv_indices)
v_min = min(v[i] for i in uv_indices)
v_max = max(v[i] for i in uv_indices)
if precision:
# In some cases reducing floating point precision
# can work around false positives for a new tile
# e.g. a default sphere can have a UV point at
# V = 1.0000001192092896 - which will make it
# think the second vertical UV tile is used.
u_min = round(u_min, precision)
u_max = round(u_max, precision)
v_min = round(v_min, precision)
v_max = round(v_max, precision)
bb = ((u_min, u_max), (v_min, v_max))
tiles = get_bounding_box_tiles(bb)
all_tiles.update(tiles)
return sorted(all_tiles)
def uv2udim(u,v):
"""return UDIM tile corresponding to UV coord"""
return 1001+ int(u) + int(v) * 10
# Example usage (will go over all meshes in the scene!)
for mesh in cmds.ls(type="mesh"):
print mesh
print [uv2udim(*tile) for tile in get_uv_tiles(mesh, precision=4)]
For sake of testing here are some test simple meshes with their expected tiles in Maya.
from maya import cmds
tests = []
# Plane with all UV points outside outside of Tile [0,0]
plane = cmds.polyPlane(createUVs=True, sx=1, sy=1, name="outside")[0]
cmds.polyEditUV(plane + ".map[0]", u=-0.1, v=-0.1)
cmds.polyEditUV(plane + ".map[1]", u=0.1, v=-0.1)
cmds.polyEditUV(plane + ".map[2]", u=-0.1, v=0.1)
cmds.polyEditUV(plane + ".map[3]", u=0.1, v=0.1)
expected = [(x, y) for x in range(-1, 2) for y in range(-1, 2)]
tests.append((plane, expected))
# Plane with all UV points inside of Tile [0,0]
plane = cmds.polyPlane(createUVs=True, sx=1, sy=1, name="inside")[0]
cmds.polyEditUV(plane + ".map[0]", u=0.1, v=0.1)
cmds.polyEditUV(plane + ".map[1]", u=-0.1, v=0.1)
cmds.polyEditUV(plane + ".map[2]", u=0.1, v=-0.1)
cmds.polyEditUV(plane + ".map[3]", u=-0.1, v=-0.1)
expected = [(0, 0)]
tests.append((plane, expected))
# Plane with all UV points on the borders of Tile [0,0]
plane = cmds.polyPlane(createUVs=True, sx=1, sy=1, name="all_on_borders")[0]
expected = [(0, 0)]
tests.append((plane, expected))
# Plane with all UV points inside of Tile [0,0]
# With bottom right UV point extended into tile [1,0]
plane = cmds.polyPlane(createUVs=True, sx=1, sy=1, name="inside_except_one_point")[0]
cmds.polyEditUV(plane + ".map[0]", u=0.1, v=0.1)
cmds.polyEditUV(plane + ".map[1]", u=0.5, v=0.1)
cmds.polyEditUV(plane + ".map[2]", u=0.1, v=-0.1)
cmds.polyEditUV(plane + ".map[3]", u=-0.1, v=-0.1)
expected = [(0, 0), (1, 0)]
tests.append((plane, expected))
# Plane with bottom right UV point extended into tile [2,0]
# Has no UV points in tile [1,0] but polygon does go over it
plane = cmds.polyPlane(createUVs=True, sx=1, sy=1, name="extended_over")[0]
cmds.polyEditUV(plane + ".map[1]", u=1.9)
cmds.polyEditUV(plane + ".map[3]", u=-0.1)
expected = [(0, 0), (1, 0), (2, 0)]
tests.append((plane, expected))
# Plane skewed from tile [0,0] over to tile [2,2]
# This should be invalid with just a bounding box check
plane = cmds.polyPlane(createUVs=True, sx=1, sy=1, name="skewed")[0]
cmds.polyEditUV(plane + ".map[1]", u=2, v=2)
cmds.polyEditUV(plane + ".map[3]", u=2, v=2)
expected = [(0, 0), (1, 0), (0, 1), (1, 1), (2, 1), (1, 2), (2, 2)]
tests.append((plane, expected))
def run_tests(fn, tests):
for geo, expected in tests:
result = fn(geo)
correct = sorted(result) == sorted(expected)
print("Geo: %s" % geo)
if correct:
print("OK!")
else:
print("Invalid.")
print("\tResult: %s" % result)
print("\tExpected: %s" % expected)
Example usage with a function called get_uv_udim_tiles
run_tests(get_uv_udim_tiles, tests)
Here's one that uses polyUVCoverage
to remove the false positives from the bounding box queries:
from maya import cmds
import math
def get_bounding_box_tiles(bb):
u_minmax, v_minmax = bb
# If the max is exactly on the integer boundary we allow it to be
# part of the tile of the previos integer so we can subtract one.
# But since we'll need to add one to iterate "up to" the maximum for
# the `range` function we will instead add one to the opposite cases.
# Inputs are tuples so don't assign elements but override tuple
if u_minmax[1] != math.floor(u_minmax[1]):
u_minmax = (u_minmax[0], u_minmax[1] + 1)
if v_minmax[1] != math.floor(v_minmax[1]):
v_minmax = (v_minmax[0], v_minmax[1] + 1)
def floor_int(x):
return int(math.floor(x))
tiles = []
for v in range(*map(floor_int, v_minmax)):
for u in range(*map(floor_int, u_minmax)):
tiles.append((u, v))
return tiles
def get_uv_udim_tiles(mesh, uv_set=None):
"""Return the UV tiles used by the UVs of the input mesh.
Warning:
This does not capture the case where a single UV shell
might be layout in such a way that all its UV points are
around another tile since it uses the UV shell bounding
box to compute the used tiles. In the image below imagine
the lines being the UVs of a single shell and the `x` being
an emtpy UV tile. It will not detect the empty tile.
/ - \
| x |
Args:
mesh (str): Mesh node name.
uv_set (str): The UV set to sample. When not
provided the current UV map is used.
Returns:
list: sorted list of uv tiles
"""
def _remove_false_positives(uvs, tiles):
"""Confirm tiles returned from bounding box.
This will remove the false positives.
"""
if len(tiles) < 2:
# A single tile from bounding
# box must always be correct
return tiles
faces = cmds.polyListComponentConversion(uvs,
fromUV=True,
toFace=True,
internal=True)
confirmed_tiles = []
for tile in tiles:
coverage = cmds.polyUVCoverage(faces,
uvRange=[tile[0],
tile[1],
tile[0]+1,
tile[1]+1])[0]
if coverage > 0.0:
confirmed_tiles.append(tile)
return confirmed_tiles
kwargs = {}
if uv_set is not None:
kwargs["uvSetName"] = uv_set
bb = cmds.polyEvaluate(mesh, boundingBox2d=True, **kwargs)
tiles = get_bounding_box_tiles(bb)
if len(tiles) == 1:
# If there's only a single tile for the bounding box
# it'll be impossible for there to be empty tiles
# in-between so we just return the given tiles
return tiles
# Get the bounding box per UV shell
uv_shells = cmds.polyEvaluate(mesh, uvShell=True, **kwargs)
if uv_shells == 1:
# If there's only a single UV shell it must span
# all the UV tiles
return _remove_false_positives(mesh + ".map[*]", tiles)
tiles = set()
for i in range(uv_shells):
shell_uvs = cmds.polyEvaluate(mesh, uvsInShell=i, **kwargs)
shell_bb = cmds.polyEvaluate(shell_uvs, boundingBoxComponent2d=True, **kwargs)
shell_tiles = get_bounding_box_tiles(shell_bb)
shell_tiles = _remove_false_positives(shell_uvs, shell_tiles)
tiles.update(shell_tiles)
return sorted(tiles)
def uv2udim(tile):
"""UV tile to UDIM number.
Note that an input integer of 2 means it's
the UV tile range using 2.0-3.0.
Examples:
>>> uv2udim((0, 0)
# 1001
>>> uv2udim((0, 1)
# 1011
>>> uv2udim((2, 0)
# 1003
>>> uv2udim(8, 899)
# 9999
Returns:
int: UDIM tile number
"""
u, v = tile
return 1001 + u + 10 * v
# Example usage
for mesh in cmds.ls(selection=True):
tiles = get_uv_udim_tiles(mesh)
print mesh
print tiles
for tile in tiles:
print uv2udim(tile)
Related code topics: