Solved Python Tag: Matrix is always dirty? (or vice versa)

In continuation with my exploration of how to use the dirty state in python, I'm trying to check a Matrix object's dirty state with a python tag.

If I check the MoData for dirty state, then it's ALWAYS dirty, no matter which flag I use.
If I check the BaseObject's cache for dirty state, then it's ALWAYS not dirty.

Am I doing something wrong here?
My end goal is to get the Matrix' dirty state when its target spline is changing.
Code and file attached:

  • Checking Matrix' MoData. I'm using the Count flag as the count is definitely not changing under any circumstances, but it still comes out positive.
    Result: Always Dirty
import c4d
from c4d.modules import mograph as mo

def main():
    moData = mo.GeGetMoData(op.GetObject())
    if moData.GetDirty(c4d.MDDIRTY_COUNT):
        print "object is dirty"
    else:
        print "not dirty"
  • Checking Matrix as BaseObject:
    Result: Always not Dirty
import c4d

def main():
    obj = op.GetObject()
    if obj.IsDirty(c4d.DIRTYFLAGS_CACHE):
        print "object is dirty"
    else:
        print "not dirty"

matrix_dirty.c4d

Hi

c4d.C4DAtom.GetDirty() returns an integer, the dirty checksum. Since integers greater than zero all evaluate as True you get your always dirty behaviour. IsDirty() is a specific method of BaseObject and can be consumed (as I showed in my script). A more appropriate flag four your case would be c4d.DIRTYFLAGS_DATA.

Cheers
zipit

MAXON SDK Specialist
developers.maxon.net

Thanks a lot. I didn't notice that GetDirty gives a dirty count instead of a boolean.
I adapted your code (very helpful!) and I used a user-data instead of a plugin id, it seems to work for normal objects, but with the Matrix I still have trouble...

Changing the Matrix parameters does change the GetDirty, but when using an Effector that changes the matrices, even though I can read these matrices from the MoData, the GetDirty doesn't increase the count, at least with the "MDDIRTY_ALL" flag.

In the documentation it says for the "MDDIRTY_DATA":
"Data in the arrays changed, must be manually set."
But I'm not sure what it means by "manually set. I assume it refers to when manually setting the array data? What happens when using an effector or changing a target object?

This is the current code:

def main():
    dirty_storage = op[c4d.ID_USERDATA,1]
    moData = mo.GeGetMoData(op.GetObject())
    # Get the last cached dirty count and the current dirty count.
    lst_dcount = dirty_storage
    cur_dcount = moData.GetDirty(c4d.MDDIRTY_ALL)
    print "dirty count: " + str(cur_dcount)
    
    # count or there is no cached dirty count.
    if lst_dcount < cur_dcount:
        op[c4d.ID_USERDATA,1] = cur_dcount
        print "dirty"
    else:
        print "not dirty"

And the file:
matrix_dirty_0001.c4d

Hi,

lst_dcount < cur_dcount should be lst_dcount != cur_dcount, because when you have initialized that field with a value larger than the current dirty count (like in your document) , the whole thing will always evaluate as False. Same goes for the Python generator thing.

Cheers
zipit

MAXON SDK Specialist
developers.maxon.net

Hi,
yes I noticed that this happens and your suggestion is more fool-proof.
But the issue is that moData.GetDirty(c4d.MDDIRTY_ALL) doesn't increase when affecting the matrix using an Effector, or when adjusting its target object.

Hi,

I am not quite sure, why the MoData does not keep track of the changes, but I also do not know much about MoGraph. Has probably something to do with that effector-field construction of yours.

But you can always keep track of your data manually. Something like this:

import c4d

ID_DIRTY_CONTAINER = 1000000
ID_DIRTY_LAST_COUNT = 0
ID_DIRTY_MATRIX_CONTAINER = 1

def main():
    """ Manually track the state of a MoData object.
    """
    def is_dirty(dirty_count, clone_matrices):
        """Determines if the dirty count of the MoData object or the specific
         clone matrix array data is dirty. Also updates the dirty cache.

        Note: For every other array (weigth, color, size, etc.) you want to
         keep track off, you would have to do the same as for the matrices.
        
        Args:
            dirty_count (int): The dirty count of the MoGraph object.
            matrices (list[c4d.Matrix]): The matrix array of the clones. 
        
        Returns:
            bool: If the data is dirty
        """
        is_dirty = False
        bc_data = op[ID_DIRTY_CONTAINER]

        # Read and compare the cached dirty data with the current data
        if bc_data is not None:
            bc_matrices = bc_data[ID_DIRTY_MATRIX_CONTAINER]
            # Compare the clone matrices with our cached version
            if len(bc_matrices) == len(clone_matrices):
                for i, m in enumerate(clone_matrices):
                    is_dirty |= m != bc_matrices[i]
            else:
                is_dirty = True
            # Compare the dirty count with our cached value
            is_dirty |= dirty_count != bc_data[ID_DIRTY_LAST_COUNT]
        # No cache of the dirty data has been generated yet
        else:
            bc_data = c4d.BaseContainer()
            is_dirty = True

        # Write the dirty data back
        bc_data[ID_DIRTY_LAST_COUNT] = dirty_count
        bc_matrices = c4d.BaseContainer()
        for i, m in enumerate(clone_matrices):
            bc_matrices[i] = m
        bc_data[ID_DIRTY_MATRIX_CONTAINER] = bc_matrices
        op[ID_DIRTY_CONTAINER] = bc_data

        return is_dirty

    md = c4d.modules.mograph.GeGetMoData(op.GetObject())
    if is_dirty(dirty_count=md.GetDirty(c4d.MDDIRTY_ALL), 
                clone_matrices=md.GetArray(c4d.MODATA_MATRIX)):
        print "Was dirty"
    else:
        print "Was not dirty"

MAXON SDK Specialist
developers.maxon.net

Thanks a lot, I'll try this out.
Unfortunately iterating through the matrices one-by-one makes it much more costly to just check for the dirty state, so I'm not sure if is practical to use this solution.
It would be more viable if the built-in Get Dirty worked, maybe that's a bug or a mistake from my side.

Hi,

I do not think that there is a mistake of ours. The docs are pretty clear on that the other operand (e.g. the effector) is responsible for maintaining the dirty checksum. This apparently does not happen in your setup. The why on that would be pure speculation on my side and would require further investigation. As already stated, I would suspect the field and effector combo as a possible cause.

My example just shows you, how you could deal with the scenario of yours. I also do not think complexity is an issue here, since this is all linear and Python is not that slow, that it cannot even deal with linear. Or in other words: If runtime is getting an issue here, because the number of matrices is getting ridiculously large, you probably should neither use Python nor MoGraph at all.

Cheers
zipit

MAXON SDK Specialist
developers.maxon.net

Hi @orestiskon I'm afraid there is nothing much more to say except what @zipit said.

Just a few notes:

  • IsDirty is build to be used within a generator to check the current object only.
  • GetDirty retrieves an integer value ta represents the dirty state of an object. It can be used to retrieve DirtyCount from outside.

Now take into consideration that the matrix object is kind of special since in reality, it creates nothing. But only feed some MoData and display them (but create no geometry). So how does an object that creates nothing can have its cache dirty? That's why it's up to the object that modifies the MoData (stored in its hidden tag ID_MOTAGDATA) to tell the matrix its content is dirty so other people that rely on this matrix know about it.

Additionally to what @zipit said (which will work in any case and it's preferred)
You can also consider checking for the dirtiness of the linked effector (but this will not consider Field).
Here an example in a Python Generator

import c4d


def CheckDirtyObj(obj, uuid, flag):
    """
    Checks if an object by comparing the current Dirt Value with the one stored in the current op.BaseContainer

    :param obj: The BaseList2D to retrieve the dirty state from.
    :param uuid: The uuid used to store in the BaseContainer.
    :param flag: The dirtiness flag to check for.
    :return: True if the object is dirty, False otherwise.
    """
    def GetBc():
        """
        Retrieves a BC stored in the object BC, or create it if it does not exist yet
        :return: A BaseContainer where value can be stored.
        """
        bcId = 100001 # Make sure to obtain an UNIQUE ID in plugincafe.com
        bc = op.GetDataInstance().GetContainerInstance(bcId)
        if bc is None:
            op.GetDataInstance().SetContainer(bcId, c4d.BaseContainer())

        bc = op.GetDataInstance().GetContainerInstance(bcId)
        if bc is None:
            raise RuntimeError("Unable to create BaseContainer")

        return bc

    # Retrieves the stored value and the true DirtyCount
    storedDirtyCount = GetBc().GetInt32(uuid, -1)
    dirtyCount = obj.GetDirty(flag)

    # Compares them, update stored value and return
    if storedDirtyCount != dirtyCount:
        GetBc().SetInt32(uuid, dirtyCount)
        return True

    return False


def main():
    # Retrieve attached object and check if it's a matrix object
    matrixObj = op[c4d.ID_USERDATA, 1]
    if matrixObj is None or not matrixObj.CheckType(1018545):
        return c4d.BaseObject(c4d.Onull)

    # Retrieve the current cache
    opCache = op.GetCache()

    # We need a new object if one of the next reason are False
    # The Cache is not valid
    # The Parameter or Matrix of the current generator changed
    # The Parameter or Matrix of the linked Matrix changed
    needNewObj = opCache is None
    needNewObj |= not opCache.IsAlive()
    needNewObj |= op.IsDirty(c4d.DIRTYFLAGS_DATA | c4d.DIRTYFLAGS_MATRIX)
    needNewObj |= CheckDirtyObj(matrixObj, 0, c4d.DIRTYFLAGS_DATA | c4d.DIRTYFLAGS_MATRIX)

    # The Parameter or Matrix of effectors of the linked Matrix changed
    objList = matrixObj[c4d.ID_MG_MOTIONGENERATOR_EFFECTORLIST]
    for objIndex in xrange(objList.GetObjectCount()):
        # If the effector is disabled in the effector list, skip it
        if not objList.GetFlags(objIndex):
            continue

        # If the effector is not valid or not enabled, skip it
        obj = objList.ObjectFromIndex(op.GetDocument(), objIndex)
        if obj is None or not obj.IsAlive() or not obj.GetDeformMode():
            continue

        # Check for the dirty value stored (+1 because we already used ID 0 for the matrix object)
        needNewObj |= CheckDirtyObj(obj, objIndex + 1, c4d.DIRTYFLAGS_DATA | c4d.DIRTYFLAGS_MATRIX)

    if not needNewObj:
        print "Old Obj"
        return opCache

    print "Generated New Object"
    return c4d.BaseObject(c4d.Ocube)

Cheers,
Maxime.