Solved Flat UV Projection in Python

Hello!
I'm creating a polygon object in an ObjectData plugin. I would like to layout its UVs with Flat projection (similar to BodyPaint's Flat Projection button). I found this post with code taken from this post on the forum. This seems to be what I need, but it's old and I ran into some issues with methods unavailable to the Python API. Here is my port of the code to Python:

import c4d,math

#This code creates the equivalent to the flat mapping button in the BP UV options

#Define a value macro to be used several times later on
SIGDIG=5.0

def TrimDecimal(num, digits):
    n = None
    n = num * math.pow(10.0, digits)
    n = math.copysign(1, n) * abs(math.floor(n + 0.5))
    return n / math.pow(10.0, digits)

def FrontalMapUVs(op, mg):
    if op is None:
        return false

    numFaces = op.GetPolygonCount()
    numVerts = op.GetPointCount()
    pVerts = op.GetPointW() #GetPointW doesn't exist in the Python API
    pPolys = op.GetPolygonW()

    pUVTag = op.MakeVariableTag(c4d.Tuvw, numFaces)
    if pUVTag is None:
        return false

    pUVHndl = pUVTag.GetDataAddressW() #GetDataAddressW doesn't exist in the Python API
    if pUVHndl is None:
        return false

    lenxinv, lenyinv, lenzinv = None, None, None

    vMin = c4d.Vector(100000.0)
    vMax = c4d.Vector(-100000.0)

    for ndx in range(numVerts):
        pt = pVerts[ndx] * mg #pVerts won't work because GetPointW doesn't exist
        if pt.x < vMin.x: vMin.x = pt.x
        if pt.x > vMax.x: vMax.x = pt.x
        if pt.y < vMin.y: vMin.y = pt.y
        if pt.y > vMax.y: vMax.y = pt.y
        if pt.z < vMin.z: vMin.z = pt.z
        if pt.z > vMax.z: vMax.z = pt.z

    mapSize = c4d.Vector(abs(vMax.x - vMin.x),abs(vMax.y - vMin.y),abs(vMax.z - vMin.z))
    mapCenter = c4d.Vector(vMin.x+(mapSize.x*0.5),vMin.y+(mapSize.y*0.5),vMin.z+(mapSize.z*0.5))

    if mapSize.x != 0.0:
        lenxinv = 1.0 / mapSize.x
    else:
        lenxinv = 0.0

    if mapSize.y != 0.0:
        lenyinv = 1.0 / mapSize.y
    else:
        lenyinv = 0.0

    if mapSize.z != 0.0:
        lenzinv = 1.0 / mapSize.z
    else:
        lenzinv = 0.0

    # Walk the list of polygons and map the UVs
    for ndx in range(numFaces):
        """
        Not sure what to do here as UVWStruct & UVWTag.Get do not exist in the Python API
        uvw = c4d.UVWStruct()
        pUVTag.Get(pUVHndl, ndx, uvw)
        """

        pt_a = (pVerts[pPolys[ndx].a] - mapCenter) + mg.off
        pt_b = (pVerts[pPolys[ndx].b] - mapCenter) + mg.off
        pt_c = (pVerts[pPolys[ndx].c] - mapCenter) + mg.off
        pt_d = (pVerts[pPolys[ndx].d] - mapCenter) + mg.off

        uvw.a.x = TrimDecimal((pt_a.x*lenxinv)+0.5, SIGDIG)
        uvw.a.y = TrimDecimal((-pt_a.y*lenyinv)+0.5, SIGDIG)

        uvw.b.x = TrimDecimal((pt_b.x*lenxinv)+0.5, SIGDIG)
        uvw.b.y = TrimDecimal((-pt_b.y*lenyinv)+0.5, SIGDIG)

        uvw.c.x = TrimDecimal((pt_c.x*lenxinv)+0.5, SIGDIG)
        uvw.c.y = TrimDecimal((-pt_c.y*lenyinv)+0.5, SIGDIG)

        uvw.d.x = TrimDecimal((pt_d.x*lenxinv)+0.5, SIGDIG)
        uvw.d.y = TrimDecimal((-pt_d.y*lenyinv)+0.5, SIGDIG)

        """
        commented out because of UVWStruct missing above
        """
        #pUVTag.Set(pUVHndl, ndx, uvw)

    return true

if __name__=='__main__':
    FrontalMapUVs(op,op.GetMg()) #I'm sending the selected object's global matrix, but I'm not sure if it's correct to do.

I noted some of the issues in the code's comments, but to sum up, I couldn't get it to work because these were missing in Python:

I also didn't know which Global Matrix I should send to the FrontalMapUVs function. I'd assume it was op, but it seems strange for that to be a parameter if it's already accepting op.

If there are no Python equivalents to these missing C++ methods, is there another way to do Flat UV Projection in Python with an ObjectData plugin?

Thank you!

Hi,

your code looks a bit overly complicated and c-ish. There might very well be a reason for all the gymnastics that code does, but at least I am not able to see that reason. Here is how I would do it in Python.

Cheers,
zipit

"""Simple example for generating uvw data.
"""

import c4d

# To implement other projections, you have just to implement another
# projection function ...

def planar_projection(frame, points):
    """Projects points into the XY plane.

    Args:
        frame (c4d.Matrix): The projection frame.
        points (list[c4d.Vector]): The points to project.
    
    Returns:
        list[c4d.Vector]:The projected points.
    """
    points = [p * frame for p in points]
    return [c4d.Vector(p.x, p.y, 0) for p in points]

def remap_to_texcoords(points):
    """Remaps a list of points to the common uv half range.

    The function assumes the x and y coordinates of the points to be the
    relevant coordinates and the points are being remapped based on the XY
    bounding rectangle.

    Args:
        points (list[c4d.Vector]): The points to remap.

    Returns:
        list[c4d.Vector]:The remapped points.
    """
    # Get the texture coordinates bounding rectangle.
    xmin, xmax = float("inf"), float("-inf")
    ymin, ymax = float("inf"), float("-inf")
    for p in points:
        xmin = p.x if p.x < xmin else xmin
        xmax = p.x if p.x > xmax else xmax
        ymin = p.y if p.y < ymin else ymin
        ymax = p.y if p.y > ymax else ymax

    # Remap the points from object coordinates to texture coordinates
    # in the common uv half range.
    points = [c4d.Vector(c4d.utils.RangeMap(p.x, xmin, xmax, 0., 1., True),
                         c4d.utils.RangeMap(p.y, ymin, ymax, 0., 1., True),
                         0) for p in points]
    return points


def create_uvw_tag(node, frame, f):
    """Creates an uvw tag for an object, a frame and a projection.

    Args:
        node (c4d.PolygonObject): The node.
        frame (c4d.Matrix): The texture projection frame.
        f (callable): The projection function.
    
    Returns:
        c4d.UVWTag: The uvw tag.
    """
    # Project the points of the node.
    points = f(frame, node.GetAllPoints())
    # Convert these points to texture space.
    uv_points = remap_to_texcoords(points)
    # Create the actual tag
    polygons = node.GetAllPolygons()
    uvw_tag = c4d.UVWTag(len(polygons))
    # We just go over all polygons and with that index our projected and
    # remapped points into the uvw tag.
    for i, cpoly in enumerate(polygons):
        a, b = uv_points[cpoly.a], uv_points[cpoly.b]
        c, d = uv_points[cpoly.c], uv_points[cpoly.d]
        uvw_tag.SetSlow(i, a, b, c, d)
    return uvw_tag


def main():
    """
    """
    if not isinstance(op, c4d.PolygonObject):
        msg = "Please select a polygon object."
        raise TypeError(msg)

    tag = create_uvw_tag(op, c4d.Matrix(), f=planar_projection)
    op.InsertTag(tag)
    c4d.EventAdd()


if __name__ == "__main__":
    main()

MAXON SDK Specialist
developers.maxon.net

Thank you very much @zipit ! That works great.