Solved pyDeformer mode: ID_MG_BASEEFFECTOR_POSITION works, but ID_MG_BASEEFFECTOR_SCALE doesn't

I stumbled upon a weird thing. I code a Python Effector (parameters control), there I want to check does it affect scale parameter or not:

md = mo.GeGetMoData(op)
mode = md.GetBlendID()
if mode == c4d.ID_MG_BASEEFFECTOR_SCALE:
        rVal = 1
    else:
        rVal = 0

Fairly basic
And you know, the first if is never true. Despite ID_MG_BASEEFFECTOR_POSITION is working.
I tried ID_MG_BASEEFFECTOR_SCALE_ACTIVE but nothing changed.
Than I checked what mode is. Whatever I do it's always returning the same:

1010
1011
1012
1013
1013
1013
1065
1066
1067
1064
1068
1069
1020
19
20

There are all kinds of the modes enumerations (can be found in \resource\modules\objects\description\obaseeffector.h) despite only Scale (1013) is active. So I don't understand what is that.
Why doesn't it work? And how should it?

Hi,

I am a bit confused, because the integer value for the symbol in question, is right there in your print out (1011 is integer value for c4d.ID_MG_BASEEFFECTOR_SCALE ). So it is unclear to me, what you would consider not working, since you did not give us the whole script. Anyways, here is a short example:

""" This snippet is intended for a Python Effector in "Parameter Control"
mode. Things will change fundamently when in "Full Control" mode.
"""
import c4d
from c4d.modules import mograph

def main():
    # Get the MoData or get out when we could not access the data.
    md = mograph.GeGetMoData(op)
    if md is None: 
        return 1.0
    
    # Get the category for which the effector is being polled.
    mode = md.GetBlendID()
    # The effector is being polled for Parameter/Position.
    if mode == c4d.ID_MG_BASEEFFECTOR_POSITION:
        # The component wise product of this returned vector and the vector
        # specified by the user in the interface will be the final value 
        # for the current particle. E.g., if the user specified (0, 50, 0),
        # then the final result would be (0, 50, 0) ^ (2, 2, 2), i.e. 
        # (0, 100, 0).
        return c4d.Vector(2)
    # The effector is being polled for Parameter/Scale.
    elif mode == c4d.ID_MG_BASEEFFECTOR_SCALE:
        # For the scale parameter things work analogously, this would ensure that
        # only half of the value specified by the user will apply.
        return c4d.Vector(.5)

    # When we reached this point, we do not want to change anything and return 
    # `1` to indicate that.
    return 1.0

Cheers,
zipit

MAXON SDK Specialist
developers.maxon.net

Hi,

I am a bit confused, because the integer value for the symbol in question, is right there in your print out (1011 is integer value for c4d.ID_MG_BASEEFFECTOR_SCALE ). So it is unclear to me, what you would consider not working, since you did not give us the whole script. Anyways, here is a short example:

""" This snippet is intended for a Python Effector in "Parameter Control"
mode. Things will change fundamently when in "Full Control" mode.
"""
import c4d
from c4d.modules import mograph

def main():
    # Get the MoData or get out when we could not access the data.
    md = mograph.GeGetMoData(op)
    if md is None: 
        return 1.0
    
    # Get the category for which the effector is being polled.
    mode = md.GetBlendID()
    # The effector is being polled for Parameter/Position.
    if mode == c4d.ID_MG_BASEEFFECTOR_POSITION:
        # The component wise product of this returned vector and the vector
        # specified by the user in the interface will be the final value 
        # for the current particle. E.g., if the user specified (0, 50, 0),
        # then the final result would be (0, 50, 0) ^ (2, 2, 2), i.e. 
        # (0, 100, 0).
        return c4d.Vector(2)
    # The effector is being polled for Parameter/Scale.
    elif mode == c4d.ID_MG_BASEEFFECTOR_SCALE:
        # For the scale parameter things work analogously, this would ensure that
        # only half of the value specified by the user will apply.
        return c4d.Vector(.5)

    # When we reached this point, we do not want to change anything and return 
    # `1` to indicate that.
    return 1.0

Cheers,
zipit

MAXON SDK Specialist
developers.maxon.net

I am a bit confused, because the integer value for the symbol in question, is right there in your print out (1011 is integer value for c4d.ID_MG_BASEEFFECTOR_SCALE
Sorry, that's my typo of course.
My code actually looks like this:

import c4d
from c4d.modules import mograph as mo
import random
#Welcome to the world of Python

def main():
    md = mo.GeGetMoData(op)
    if md is None: return 1.0
    mode = md.GetBlendID()
    
    index = md.GetCurrentIndex()
    random.seed(md.GetCount() * index * op[c4d.ID_USERDATA,1])
    rIndex = random.randint(0,2)
    if mode == c4d.ID_MG_BASEEFFECTOR_SCALE:
        rVal = 1
    else:
        rVal = random.choice([-1, 1])
    weights = md.GetArray(c4d.MODATA_WEIGHT)
    #print rVal
    vector = c4d.Vector()
    vector[rIndex] = rVal * weights[index]
    if mode == c4d.ID_MG_BASEEFFECTOR_POSITION or mode == c4d.ID_MG_BASEEFFECTOR_ROTATION or mode == c4d.ID_MG_BASEEFFECTOR_SCALE:
        return vector
    else: return 1.0

It's point is to randomly choose the axis which the value will be added to. It can give results as in attached image 2020-07-22_16-58-31.png
But to avoid negative scale I check if the Scale is active. But weirdly my code with the same check as yours never worked. For some reason I copied your and despite it's the same letter to letter now it works.
I'm confused...

But thanks for your answer anyway!

Hi,

I am glad that it worked out for you, but since you posted your code now, I spotted a few problems around your random number generation, specifically in this line:

random.seed(md.GetCount() * index * op[c4d.ID_USERDATA,1])

Only a minor detail is that md.GetCount() * index * op[c4d.ID_USERDATA,1] might cause an integer overflow, since this is effectively some large number to the power of three. An overflow is unlikely, but could lead to unexpected behaviour.

More important is that random.seed is a expensive function, because it will build the permutation table of Python's Mersenne-Twister random number sequence generator each time it is being called. You calling random.seed inside the main function means that this is being done potentially millions of times per frame. You should:

  • either call random.seed only once outside the main function
  • or, if you want your random values seed to be animatable via the user data value, at least safeguard the random.seed so that it is only being called when needed.

There is also the problem that the state of your PNG permutation table is dependent on the order in which the particles are being queried, due to the fact that you call the global random instance inside a conditional statement (random.choice([-1, 1])) and the general design of using a sequence based PNG here in the first place.

You could solve all the above by using a custom PNG, the common trigonometric one-liner PNG would be enough. It also would be much more performant.

Cheers,
zipit

MAXON SDK Specialist
developers.maxon.net

Oh, you're always making a huge work answering the questions! 👏

I'm slightly confused by the topic you cover, can't find what PNG is.
What should I do with random.seed than if I rely on unique random number generated for the particular clone? I can't move it out of main() because only there ID is accessible.
Make some class to store there those random numbers as globals and check should I generate it again or use as before based on user data seed and clone count?

Hi,

I am sorry, I hate it myself when people talk in acronyms, assuming everyone knows what they are referring to. PNG stands for Pseudo-random Number Generator. Here is an example for a simple trigonometric pseudo random hash function.

Cheers,
zipit

"""A simple example for a very simple "one-at-a-time" Pseudo-random Number
Generator (PNG).

It is basically just one line of code, which you can find on line 32.
"""

import c4d
import math

def hash_11(x, seed=1234, magic=(1234.4567, 98765.4321)):
    """Returns a pseudo random floating point hash for a floating point number.

    The hash will lie int the interval [-1, 1] and the function is a very
    simple generator that exploits the periodicity of the trigonometric 
    functions. A more sophisticated approach would be to exploit avalanche
    behavior in bit-shift operations on binary data, like the Jenkins
    Rotation does for example.

    The postfix in the name (_11) is a common way to denote the inputs and
    outputs for PNGs. 11 means that it will take one (real) number and return 
    one (real) number. 13 means that it takes one and returns three, i.e. returns 
    an (euclidean) vector.  

    Args:
        x (float): The value to hash into a pseudo random number.
        seed (int, optional): The seed value. This could also be a float.
        magic (tuple, optional): The magic numbers. The second one should be 
         bigger then the first and both should be real numbers.
    
    Returns:
        float: The pseudo random hash for x in the interval [-1, 1].
    """
    return math.modf(math.sin(x + seed + magic[0]) * magic[1])[0]

def hash_13(x, seed=1234, magic=(1234.4567, 98765.4321)):
    """Returns a pseudo random vector hash for a floating point number.
    
    Wraps around hash_11.
    
    Returns:
        c4d.Vector: The pseudo random hash for x in the interval [-1, 1].
    """
    vx = hash_11(x, seed, magic)
    vy = hash_11(x + vx, seed, magic)
    vz = hash_11(x + vy, seed, magic)
    return c4d.Vector(vx, vy, vz)

def main():
    """Entry point.
    """
    # Some very crude statistics for the hashes.
    samples = int(1E6)
    # Generate 1E6 of each
    numbers = {i: hash_11(i) for i in range(samples)}
    vectors = {i: hash_13(i) for i in range(samples)}
    # Compute their arithmetic mean.
    amean_numbers = sum(numbers.values()) * (1./samples)
    amean_vectors = sum(vectors.values()) * (1./samples)
    # Report the results.
    print "First three random numbers: ", numbers.values()[:3]
    print "First three random vectors: ", vectors.values()[:3]
    msg = "Arithmetic mean of all random numbers (should converge to zero): "
    print msg, amean_numbers
    msg = "Arithmetic mean of all random vectors (should converge to zero): "
    print msg, amean_vectors

if __name__ == "__main__":
    main()
First three random numbers:  [-0.8036933662078809, 0.20401213006516628, 0.6249060598929645]
First three random vectors:  [Vector(-0.804, -0.022, -0.872), Vector(0.204, 0.541, 0.115), Vector(0.625, 0.782, 0.896)]
Arithmetic mean of all random numbers (should converge to zero):  -0.000127638074863
Arithmetic mean of all random vectors (should converge to zero):  Vector(0, 0, 0)

MAXON SDK Specialist
developers.maxon.net