Solved Python Generator associated with User Data?

I've written a Python Generator which uses 14 User Data items (located on the the generator object).

Is there a way to associate the User Data definition with my Python Generator script, such that whenever I create a new instance of this PyGen object, the associated User Data is automatically added to it?

Hi,

there is no official way, since there are no dedicated messages being sent and functions being called for this task. However you can just piggyback some of the other messages that are being sent. Be aware that not all Python scripting nodes (e.g.: tag, object, xpresso, ...) do receive the same messages and therefor you will have to adapt your approach for each of them. Here is an example for a python generator object that builds its interface on its own. (the relevant part is the message() function).

import c4d
import random

from c4d.utils import SendModelingCommand


def get_greeble(op, subdivisions, probability, probability_decay, time, seed):
    """ Computes a greeble object for a polygon object input. Calls itself
     recursively for multiple subdivisions.

    The subdivision scheme is slight variation on the one midpoint subdivision
     scheme (used for example in Catmull-Clark SDS). The differences are that
     along the v axis two center points are being generated and the surrounding
     edge points are not centered on their edges. This will result in a
     segmented (not connected) mesh. The scheme in detail is as follows:

    a, b, c, d   - The points of the polygon (the vertex points)
    du, dv, dw   - The random offsets (.25 to .75) for the intermediate points
    p, q, r, s   - The intermediate points (the edge points)
    g, h         - The two center points
    A, B, C, D   - The resulting four new polygons

                   du
               ┌───────┐
               ↓       ↓

              [d]───── r ───────[c]
               │   D   ┆         │
       ┌───>   s┄┄┄┄┄┄┄g    C    │
       │       │       ┆         │
    dv │       │   A   h┄┄┄┄┄┄┄┄┄q   <───┐
       │       │       ┆    B    │       │ dw
       └───>  [a]───── p ───────[b]  <───┘

    Args:
        op (c4d.BaseObject): The input object to greeble.
        subdivisions (int): The number of subdivisions per polygon.
        probability (float): The chance that a subdivision occurs on a polygon. 
        probability_decay (float): The decay of probability per recursion.
        time (float): The time for computing the random offsets.
        seed (int): The seed for computing the random offsets.

    Returns:
        c4d.PolygonObject or None: The greeble or None if op was not a valid
         input object.
    """

    # Get the caches
    if isinstance(op, c4d.BaseObject):
        deform_cache = op.GetDeformCache()
        cache = op.GetCache() if deform_cache is None else deform_cache
        op = cache if isinstance(cache, c4d.PolygonObject) else op

    if not isinstance(op, c4d.PolygonObject):
        return None

    # Data IO
    points = op.GetAllPoints()
    polygons = op.GetAllPolygons()
    new_points = []
    new_polygons = []

    def lerp(a, b, t): return a + (b-a) * t
    def noise(p): return c4d.utils.noise.Noise(p + time)
    seed = 1./seed

    # For every polygon in the input object
    for cpoly in polygons:

        # The points of the polygon
        a, b = points[cpoly.a], points[cpoly.b]
        c, d = points[cpoly.c], points[cpoly.d]
        # The mean of the points as the identity for the noise seed
        ip = (a + b + c + d) * .25

        # Skip a subdivision step and just copy the old polygon
        if random.random() > probability:
            new_points += [a, b, c, d]
            bid = len(new_points) - 1
            A = c4d.CPolygon(bid - 3, bid - 2, bid - 1, bid - 0)
            new_polygons.append(A)
            continue

        # The random offsets
        du = noise(ip + c4d.Vector(seed, 0., 0.))
        dv = noise(ip + c4d.Vector(0., seed, 0.))
        dw = noise(ip + c4d.Vector(0., 0., seed))
        # The edge points
        p, r = lerp(a, b, du), lerp(c, d, 1. - du)
        q, s = lerp(b, c, dw), lerp(a, d, dv)
        # The center points
        g, h = lerp(p, r, dv), lerp(p, r, dw)

        # append the generated points to our output point list.
        # id offsets:  9, 8, 7, 6, 5, 4, 3, 2, 1, 0
        new_points += [a, b, c, d, p, q, r, s, g, h]
        # the base id for the point indices of the current polygon group
        bid = len(new_points) - 1
        """ [d]───── r ───────[c]
             │   D   ┆         │
             s┄┄┄┄┄┄┄g   C     │
             │       ┆         │
             │   A   h┄┄┄┄┄┄┄┄┄q
             │       ┆   B     │
            [a]───── p ───────[b] """
        #                      a,       p,       g,       s
        A = c4d.CPolygon(bid - 9, bid - 5, bid - 1, bid - 2)
        #                      p,       b,       q,       h
        B = c4d.CPolygon(bid - 5, bid - 8, bid - 4, bid - 0)
        #                      h,       q,       c,       r
        C = c4d.CPolygon(bid - 0, bid - 4, bid - 7, bid - 3)
        #                      s,       g,       r,       d
        D = c4d.CPolygon(bid - 2, bid - 1, bid - 3, bid - 6)

        new_polygons += [A, B, C, D]

    # Generate the output object
    res = c4d.PolygonObject(len(new_points), len(new_polygons))
    res.SetAllPoints(new_points)
    for index, cpoly in enumerate(new_polygons):
        res.SetPolygon(index, cpoly)

    # subdivide the result further
    if subdivisions > 1:
        res = get_greeble(op=res,
                          subdivisions=subdivisions - 1,
                          probability=probability * probability_decay,
                          probability_decay=probability_decay,
                          time=time,
                          seed=seed)
    return res

def get_output(op):
    """
    """
    if op is None:
        return c4d.BaseObject(c4d.Onull)


    for i in range(1):
        poly_ids = list(range(op.GetPolygonCount()))
        random.shuffle(poly_ids)

        selection = c4d.BaseSelect()
        for j in range(10):
            selection.Select(poly_ids.pop(0))

        op.GetPolygonS().DeselectAll()
        selection.CopyTo(op.GetPolygonS())

        res = SendModelingCommand(c4d.MCOMMAND_GENERATESELECTION, [op],
                                  c4d.MODELINGCOMMANDMODE_POLYGONSELECTION)
        tag = op.GetLastTag()
        tag.SetName(i)
    return op

def main():
    """
    """
    res = None

    if op.GetUserDataContainer():
        # Get the user data
        obj = op[c4d.ID_USERDATA, 1]
        subdivisions = op[c4d.ID_USERDATA, 2]
        probability = op[c4d.ID_USERDATA, 3]
        probability_decay = op[c4d.ID_USERDATA, 4]
        time = op[c4d.ID_USERDATA, 5]
        seed = op[c4d.ID_USERDATA, 6]

        random.seed(seed)

        # Compute the greeble object
        res = get_greeble(op=obj,
                          subdivisions=subdivisions,
                          probability=probability,
                          probability_decay=probability_decay,
                          time=time,
                          seed=seed)

    # Return a null object if get_greeble failed, else return the greeble
    return get_output(res)

def message(mid, data):
    """
    """
    if mid == c4d.MSG_GETREALCAMERADATA and not op.GetUserDataContainer():
        """
        """
        if not c4d.threading.GeIsMainThread():
            return
        bc = c4d.GetCustomDataTypeDefault(c4d.DTYPE_BASELISTLINK)
        bc[c4d.DESC_NAME] = "Object"
        eid = op.AddUserData(bc)

        bc = c4d.GetCustomDataTypeDefault(c4d.DTYPE_LONG)
        bc[c4d.DESC_NAME] = "Subdivisions"
        bc[c4d.DESC_MIN] = 1
        bc[c4d.DESC_MAX] = 16
        bc[c4d.DESC_MINSLIDER] = 1
        bc[c4d.DESC_MAXSLIDER] = 8
        bc[c4d.DESC_CUSTOMGUI] = c4d.CUSTOMGUI_LONGSLIDER
        eid = op.AddUserData(bc)
        op[eid] = 3

        bc = c4d.GetCustomDataTypeDefault(c4d.DTYPE_REAL)
        bc[c4d.DESC_NAME] = "Subdivision Probability"
        bc[c4d.DESC_MIN] = 0.
        bc[c4d.DESC_MAX] = 1.
        bc[c4d.DESC_MINSLIDER] = 0.
        bc[c4d.DESC_MAXSLIDER] = 1.
        bc[c4d.DESC_STEP] = .005
        bc[c4d.DESC_UNIT] = c4d.DESC_UNIT_PERCENT
        bc[c4d.DESC_CUSTOMGUI] = c4d.CUSTOMGUI_REALSLIDER
        eid = op.AddUserData(bc)
        op[eid] = 1.

        bc = c4d.GetCustomDataTypeDefault(c4d.DTYPE_REAL)
        bc[c4d.DESC_NAME] = "Subdivision Probability Decay"
        bc[c4d.DESC_MIN] = 0.
        bc[c4d.DESC_MAX] = 1.
        bc[c4d.DESC_MINSLIDER] = 0.
        bc[c4d.DESC_MAXSLIDER] = 1.
        bc[c4d.DESC_STEP] = .005
        bc[c4d.DESC_UNIT] = c4d.DESC_UNIT_PERCENT
        bc[c4d.DESC_CUSTOMGUI] = c4d.CUSTOMGUI_REALSLIDER
        eid = op.AddUserData(bc)
        op[eid] = .75

        bc = c4d.GetCustomDataTypeDefault(c4d.DTYPE_REAL)
        bc[c4d.DESC_NAME] = "Animation"
        bc[c4d.DESC_MIN] = 0.
        bc[c4d.DESC_STEP] = .01
        bc[c4d.DESC_UNIT] = c4d.DESC_UNIT_PERCENT
        eid = op.AddUserData(bc)
        op[eid] = .0

        bc = c4d.GetCustomDataTypeDefault(c4d.DTYPE_LONG)
        bc[c4d.DESC_NAME] = "Seed"
        bc[c4d.DESC_MIN] = 1
        eid = op.AddUserData(bc)
        op[eid] = 1
        c4d.EventAdd()

MAXON SDK Specialist
developers.maxon.net

Hi Matt_B, thanks for reaching out us.

Aside from the solution offered by @zipit, a less elegant one could be to save your python generator with its userdata in a "template" c4d scene, load the scene in a script, copy the python generator from the template doc into the new doc and everything will be right on place.

Best, R

Thanks for the great suggestions! ❤

I just discovered I can also use "File | Save Object Preset" and "File | Load Object Preset" in the Attribute Manager.