0

Background info

UDIM Tiles with UVs

Polygonal meshes use UV sets to map image textures to their surface. Using UDIM textures you can use integer tiles to apply different textures as within what integer bounds the UV tile is, e.g. tile [0, 0] can have a different texture than UV tile [0, 1].

Given the UV shells/points I want to quickly detect the integer tiles the UV set is currently using. In practice/production UVs rarely span UV tile boundaries since they are laid out per UDIM tile. Nevertheless I'd like accurately detect these less frequent cases too.

Goal

Given a mesh' UVs I want to detect what UV tiles are occupied. The example image above has shells within the bounding boxes of UV tiles but technically it is possible for the UVs to cross the boundaries of a UV tile and extend into the next (or even further).

I'd like to query/detect what UV tiles are being used:

  • The UV points can all be outside of a tile (e.g. 4 points of a polygon around tile [0, 0]) and the tile is encompassed in the polygon. The tile should be include as used in the UVs.
  • The UV points can go around a tile (e.g. an NGon UV of more than 4 points that curve around a tile (so bounding box hits the tile, but the UVs dont actually include the tile).
  • Other cases are UVs can just overlap/intersect with a Tile or be totally encompassed within a Tile. The tiles should then be included.

I'm trying to do this in Autodesk Maya with Python yet I'm happy with any mathematical approach that's fast and I can easily figure out how to code with Python.

In essence it's querying what integer tiles/cells a 2D Polygon is overlappign with. I don't need to know intersections points - just need to know what UV tiles contain UVs.

I'd prefer to avoid including the tiles that are only hit on the boundary. For example, a UV point/edge on [0, 1] doesn't need to include tile [0, 1] if the polygon only overlaps [0, 0].

What I have so far

Currently I only have some prototypes done within Maya that compared based on the boundaries in a gist here. Some other approach in the comments too. Code:

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)

Related

Roy Nieterau
  • 1,226
  • 2
  • 15
  • 26

1 Answers1

1

Here is a possible solution for Maya in Python.

The proposed approach:

  • Determine the global bounding box of the meshes UV shells (single bbox englobing all the shells), and determine the set of covered UV tiles.
  • Create a plane with the same number of faces as the UV tiles (1 face per UV tile, respecting the same U/V ratio). eg: if the bounding box covers the tiles (0,0) to (5,3), create a 6x4 plane.
  • Scale/position the UVs of the plane to fit to the UV grid (1 face covering exactly 1 UV tile).
  • Determine the overlap between the plane UV shells and the other meshes UV shells.

It might not be the best approach, but it avoids having to determine the UV shells borders and computing intersections between the UV grid (which is handled by Maya).

It handles the specific cases:

  • Tile considered overlapped:
    • UV points totally encompassed within the tile
    • UV points all outside of the tile, but englobing it
    • UVs just intersect with the tile
  • Tile not considered overlapped:
    • UV points around the tile but no covering or intersecting
    • UV is on a tile boundary

Some limitations:

  • The code is to be executed in Maya (tested in Maya 2020.2)
  • Size of the UV tiles are considered to be (1, 1). If thinner granularity is required (eg: 0.1), the code must be changed (but the main approach can hold)
  • The scene is modified (creation of temporary plane geometry). It is cleaned at the end, but this might not be acceptable.
  • The code isn't fully tested in extreme cases, so should probably not be used as-is.
  • The code isn't optimised, and could be rewritten.
import math

def getBoundingBoxTilesRange(meshes):
    '''Determine the global bounding box of the UV shells.
        Single BBox englobing all.
    '''

    ((minX, maxX),(minY, maxY)) = cmds.polyEvaluate(meshes, boundingBox2d=True)

    minU = int(math.floor(minX))
    maxU = int(math.floor(maxX))
    minV = int(math.floor(minY))
    maxV = int(math.floor(maxY))

    return ((minU, minV),(maxU, maxV))


def createOverlapPlane(minU, minV, maxU, maxV):
    '''Create a plane covering the UV grid
        1 face per tile, covering the UV grid precisely
    '''

    # Create a plane with the same number of faces as the UV tiles
    # '+1' to include upper limits
    sizeX = maxU - minU + 1
    sizeY = maxV - minV + 1
    plane_trsf, plane_node = cmds.polyPlane(width=1,
                                            height=1,
                                            subdivisionsX=sizeX,
                                            subdivisionsY=sizeY,
                                            createUVs=2)

    # Scale/position the UVs of the plane to fit/cover the grid
    # '+2' to include last index AND upper limits
    count = 0
    for indexV in range(minV, maxV+2):
        for indexU in range(minU, maxU+2):
            uv = "%s.map[%d]" % (plane_trsf, count)
            cmds.polyEditUV(uv, relative=False, uValue=indexU, vValue=indexV)
            count += 1

    return plane_trsf


def getOverlappedTiles(meshes):
    '''Determine the UV tiles overlapped by the UV shells of the provided meshes.
    '''

    # Save scene status
    cmds.undoInfo(openChunk=True)


    # Get the global bounding box of the UV shells
    ((minU, minV),(maxU, maxV)) = getBoundingBoxTilesRange(selection)

    # Create a plane covering the UV grid (1 face per tile)
    plane_trsf = createOverlapPlane(minU, minV, maxU, maxV)


    # Determine non-overlapped faces between the plane UV shell and the other meshes UV shells
    mesh_faces = cmds.polyListComponentConversion(meshes, tf=True)
    plane_faces = cmds.polyListComponentConversion(plane_trsf, tf=True)
    non_overlap = cmds.polyUVOverlap(mesh_faces + plane_faces, noc=1)


    # Determine UV tiles
    tiles = []
    for indexV in range(minV, maxV+1):
        for indexU in range(minU, maxU+1):
            tiles.append((indexU, indexV))

    # Flatten lists to separate each element
    non_overlap_list = cmds.ls(non_overlap, flatten=1)
    plane_faces_list = cmds.ls(plane_faces, flatten=1)

    # Get overlapped UV tiles
    overlapped_tiles = tiles[:]
    for face in non_overlap_list:
        #TODO: should find faster way
        index = plane_faces_list.index(face)
        overlapped_tiles.remove(tiles[index])


    # Restore scene status
    cmds.undoInfo(closeChunk=True)
    cmds.undo()

    return overlapped_tiles

Example usage:

# Get the desired meshes
# (here from selection as an example)
meshes = cmds.ls(sl=1)

# Get the overlapped UV tiles
overlapped_tiles = getOverlappedTiles(meshes)
print "Overlapped tiles:", overlapped_tiles
2-REC
  • 31
  • 2
  • Nice idea! I found `maya.cmds.polyUVCoverage` which gave me [this working prototype](https://gist.github.com/BigRoy/60883eff23f73a34c4671395b32d858d) based loosely on your version without the need of the intermediate meshes. – Roy Nieterau Mar 03 '21 at 02:12