SOLVED Changing material projection in takes using Python

Hi,
I've been trying to change material projection for the materials added to take system, but it doesn't seem to be working. I can generate takes, add material tags, but when trying to change default spherical projection into uwv (using textag[c4d.TEXTURETAG_PROJECTION] = 6) - there is no effect. I also tried to move the code after group tag, and even make the current take active, and then modify the value of the projection - still the projection stays the same as before modification. I spent a few hours trying to find what's wrong , but unable to find any solution 😞
Here is my code snippet. Would be grateful if anyone could help me, by pointing me in the right direction.

    if len(listOfMaterials2Swap) > 0:
        for mat in listOfMaterials2Swap:
            matOnTheScene = doc.SearchMaterial(mat)
            if matOnTheScene is None:
                print ("Mat : " + mat + " not found on the scene")
                quit()
            else:
                #print ("Creating take for mat: " + mat)
                if takeData is not None:
                    materialTake = takeData.AddTake(roomName + "_" + mat, parentTake, None)
                    if materialTake != None:
                        group = materialTake.AddOverrideGroup()
                        group.SetName("Take for " + roomName + "_" + mat)
                        group.SetEditorMode(c4d.MODE_ON)
                        group.SetRenderMode(c4d.MODE_ON)
                        textag = c4d.TextureTag()

                        for currentMeshName in listOfMeshes4MatSwap:
                            currentMesh = doc.SearchObject(currentMeshName)
                            currentMesh.KillTag(5616)
                            currentMesh.InsertTag(textag) #Remove texture tag for current object, if already there
                            textag[c4d.TEXTURETAG_PROJECTION] = 6 #Change current projection to UVW

                            group.AddToGroup(takeData, currentMesh)
                            group.AddTag(takeData,c4d.Ttexture,matOnTheScene)

Hi @Futurium,

The problem here for us at the SDK-Team is that we are not so sure if what you are trying to do is encountering a bug or simply not intended to be done. I spoke a bit with one of the devs and they will have a look at that odd behavior (which can be reproduced in Cinema without any code), but it will probably take us some time, because we are currently quite busy.

The "problem" with your approach is that you are trying to pile a parameter override on top of tag which is crated by an override group. This is not how it is usually done in Cinema's Take system, you normally just edit that "virtual tag". If you try to do manually what your script does, Cinema will simply delete these overrides once you change the current take (which is also what happens in your script, the last take will contain the UVW-override, but it will not be respected by Cinema).

I personally would say this level of complexity - to create an attribute override for such "virtual tags" - was never intended in Cinema's take system, but I might be wrong. You can of course still do it in Python like you would normally do it Cinema, i.e., simply edit the override group tag. I have attached a script example based on your last script which does that. It will change the projection of your material tags to UVW, but won't do it with the same implied complexity as your example does it (by creating a specific attribute override for it).

Happy holidays,
Ferdinand

"""Example for "working around" the limitations of take system override groups
as discussed in:

    https://plugincafe.maxon.net/topic/13077
"""

import c4d

def yieldSecondLevelTakes():
    """Yields all second level take nodes in the scenes take graph.

    Your functions, just slightly refactored. Kept this way for clarity, but
    could be done in just one function instead of your two function design.
    """
    def yieldTopLevelTakeNodes():
        """Yields the top level take nodes of a document.
        """
        takeData = doc.GetTakeData()
        if takeData is None:
            return

        mainTake = takeData.GetMainTake()
        take = mainTake.GetDown()

        while take is not None:
            yield take
            take = take.GetNext()

    for take in yieldTopLevelTakeNodes():
        chldTake = take.GetDown()
        while chldTake is not None:
            yield chldTake
            chldTake = chldTake.GetNext()

def main():
    """Entry point.
    """
    # Clear Console
    c4d.CallCommand(13957) 
    # Not necessary, doc is predefined in a script.
    # doc = documents.GetActiveDocument()

    # Terminate branches early so that you have less indented code (and it
    # technically also runs a bit faster).
    takeData = doc.GetTakeData()
    if takeData is None:
        raise RuntimeError("No take data in the document.")

    # Loop over all second level take nodes like you want to do.
    for take in yieldSecondLevelTakes():
        print ("Current take name : " + take.GetName())

        # Not needed
        # takeData.SetCurrentTake(take)

        # This is not necessary in your scene, since all your objects
        # have the same override group tag, i.e. changing a parameter
        # on one tag will change the respective parameters on all other
        # 'instances' of the tag.

        # for currentObject in objects:
        #    obj = doc.SearchObject(currentObject)
        # ...

        # This should be technically moved outside of the loop for
        # performance reasons, kept here for parity reasons with your script.
        node = doc.SearchObject("Plane")
        if node is None:
            raise RuntimeError("Could not find targeted object node.")

        # The first override group, there could be more of course in a more
        # complex scene.
        overrideGroup = take.GetFirstOverrideGroup()
        if overrideGroup is None:
            continue

        # Get the material tag associated with this override group.
        tag = overrideGroup.GetTag(c4d.Ttexture)
        if tag is None:
            continue

        # The "workaround" here is to simply write to the tag. This of
        # course will not give you the same complexity as one could
        # imagine with takes, but since the tag is a BaseOverride tag,
        # i.e. a tag that itself is bound to  a Take, your data will
        # still be conditional in that sense.
        tag[c4d.TEXTURETAG_PROJECTION] = c4d.TEXTURETAG_PROJECTION_UVW

    # Notify Cinema 4D for updates.
    c4d.EventAdd()

# Execute main()
if __name__=='__main__':
    main()

Hi @Futurium,

thank you for reaching out to us. It is rather hard to untangle what is going wrong there for you, since you only show a snippet and do not tell us in which environment it does run. I assume this is a script manager script? The way you remove the old tag and then add a new take seems a bit fishy to me, you might have to update the scene graph there, but I would have to try that myself.

However, it seems also to be a bit overkill for what you are trying to do. Why not change just the projection of the existing tag? Below you will find an example for creating a take for a projection change and the scene file I did ran it on. @m_adam also wrote a bunch of nice take system examples, where I did ninja most of my code from 😉

Cheers,
Ferdinand

example file: pc13077_scene.c4d

"""Will create a take entry for a projection change to 
TEXTURETAG_PROJECTION_UVW on the selected objects texture tag.
"""
import c4d

def main():
    # Checks if there is an active object.
    if op is None:
        raise ValueError("op is none, please select one object.")

    # Gets the TakeData from the active document (holds all information about 
    # Takes)
    takeData = doc.GetTakeData()
    if takeData is None:
        raise RuntimeError("Failed to retrieve the take data.")

    # Gets the active Take and check it's not the main one
    take = takeData.GetCurrentTake()
    if take.IsMain():
        raise RuntimeError("The selected take is already the main one.")

    # Gets the material tag of the cube.
    tag = op.GetTag(c4d.Ttexture)
    if tag is None:
        raise RuntimeError("Blah, no texture tag on selected object.")
    
    # The single level DescId for the projection parameter.
    descId = c4d.DescID(
        c4d.DescLevel(c4d.TEXTURETAG_PROJECTION, c4d.DTYPE_LONG, 0))
    newValue = c4d.TEXTURETAG_PROJECTION_UVW
    
    # Add an override if this parameter is not already overridden, otherwise 
    # returns the already existing override.
    overrideNode = take.FindOrAddOverrideParam(
        takeData, tag, descId, newValue)
    if overrideNode is None:
        raise RuntimeError("Failed to find the override node.")

    # Updates the scene with the new Take
    overrideNode.UpdateSceneNode(takeData, descId)

    # Pushes an update event to Cinema 4D
    c4d.EventAdd()


if __name__ == '__main__':
    main()

@zipit said in Changing material projection in takes using Python:

TEXTURETAG_PROJECTION_UVW on the selected objects texture tag.
"""
import c4d

def main():
    # Checks if there is an active object.
    if op is None:
        raise ValueError("op is none, please select one object.")

    # Gets the TakeData from the active document (holds all information about 
    # Takes)
    takeData = doc.GetTakeData()
    if takeData is None:
        raise RuntimeError("Failed to retrieve the take data.")

    # Gets the active Take and check it's not the main one
    take = takeData.GetCurrentTake()
    if take.IsMain():
        raise RuntimeError("The selected take is already the main one.")

    # Gets the material tag of the cube.
    tag = op.GetTag(c4d.Ttexture)
    if tag is None:
        raise RuntimeError("Blah, no texture tag on selected object.")
    
    # The single level DescId for the projection parameter.
    descId = c4d.DescID(
        c4d.DescLevel(c4d.TEXTURETAG_PROJECTION, c4d.DTYPE_LONG, 0))
    newValue = c4d.TEXTURETAG_PROJECTION_UVW
    
    # Add an override if this parameter is not already overridden, otherwise 
    # returns the already existing override.
    overrideNode = take.FindOrAddOverrideParam(
        takeData, tag, descId, newValue)
    if overrideNode is None:
        raise RuntimeError("Failed to find the override node.")

    # Updates the scene with the new Take
    overrideNode.UpdateSceneNode(takeData, descId)

    # Pushes an update event to Cinema 4D
    c4d.EventAdd()


if __name__ == '__main__':
    main()```

Thank you @zipit
I've tried as suggested but still doesn't seems to be working the way I wanted.
I modified my code and included your suggestions, as well as my test scene, so you can see what's my issue. I bet it's something simple ;-).

import c4d
import os
from c4d import documents

listOfParentTakes = []
listOfChildTakes = []
objects = ["Plane","Plane.1","Plane.2"]


def GetListOfParentTakes():
    takeData = doc.GetTakeData()
    if takeData is None:
        return

    mainTake = takeData.GetMainTake()
    take = mainTake.GetDown()

    while take is not None:
        listOfParentTakes.append((take))
        take = take.GetNext()

def GetListOfChildrenTakes():
    for parent in listOfParentTakes:
        take = parent.GetDown()

        while take is not None:
            listOfChildTakes.append(take)
            take = take.GetNext()

# Main function

def main():

    c4d.CallCommand(13957)  # Clear Console
    doc = documents.GetActiveDocument()
    takeData = doc.GetTakeData()
    GetListOfParentTakes()
    GetListOfChildrenTakes()
    if takeData is not None:
        for el in listOfChildTakes:
            print ("Current take name : "+ el.GetName())
            takeData.SetCurrentTake(el)

            for currentObject in objects:
                obj = doc.SearchObject(currentObject)
                if obj is not None:
                    # Gets the material tag of the cube.
                    tag = obj.GetTag(c4d.Ttexture)
                    if tag is None:
                        raise RuntimeError("Blah, no texture tag on selected object.")
                        # The single level DescId for the projection parameter.
                    descId = c4d.DescID(c4d.DescLevel(c4d.TEXTURETAG_PROJECTION, c4d.DTYPE_LONG, 0))
                    newValue = c4d.TEXTURETAG_PROJECTION_UVW

                    # Add an override if this parameter is not already overridden, otherwise
                    # returns the already existing override.
                    overrideNode = el.FindOrAddOverrideParam(
                        takeData, tag, descId, newValue)
                    if overrideNode is None:
                        raise RuntimeError("Failed to find the override node.")


# Execute main()
if __name__=='__main__':
    main()

TakeProjectionTest.c4d

Hi @Futurium,

first of all your code is missing the instructions to actually update the take graph (see lines 62 and 63 in the attached example), and secondly you should try to include error messages/exceptions in the future, so that we know that we are talking about the same thing. When I run your code with a default startup Cinema 4D configuration, it will raise Failed to find the override node. defined in your code, meaning el.FindOrAddOverrideParam on line 57 failed. This is caused by Cinema not being in Lock Overrides mode by default, enabling it will let your code complete on my machine.

1.png

If you meant something else with "doesn't seems to be working the way I wanted", I would have to ask you to clarify what you would consider not to be working.

Cheers,
Ferdinand

import c4d
import os
from c4d import documents

listOfParentTakes = []
listOfChildTakes = []
objects = ["Plane","Plane.1","Plane.2"]


def GetListOfParentTakes():
    takeData = doc.GetTakeData()
    if takeData is None:
        return

    mainTake = takeData.GetMainTake()
    take = mainTake.GetDown()

    while take is not None:
        listOfParentTakes.append((take))
        take = take.GetNext()

def GetListOfChildrenTakes():
    for parent in listOfParentTakes:
        take = parent.GetDown()

        while take is not None:
            listOfChildTakes.append(take)
            take = take.GetNext()

# Main function

def main():
    c4d.CallCommand(13957)  # Clear Console
    doc = documents.GetActiveDocument()
    takeData = doc.GetTakeData()
    GetListOfParentTakes()
    GetListOfChildrenTakes()
    print (listOfChildTakes)
    if takeData is not None:
        for el in listOfChildTakes:
            print ("Current take name : "+ el.GetName())
            takeData.SetCurrentTake(el)

            for currentObject in objects:
                obj = doc.SearchObject(currentObject)
                if obj is not None:
                    # Gets the material tag of the cube.
                    tag = obj.GetTag(c4d.Ttexture)
                    if tag is None:
                        raise RuntimeError("Blah, no texture tag on selected object.")
                        # The single level DescId for the projection parameter.
                    descId = c4d.DescID(c4d.DescLevel(c4d.TEXTURETAG_PROJECTION, c4d.DTYPE_LONG, 0))
                    newValue = c4d.TEXTURETAG_PROJECTION_UVW

                    # Add an override if this parameter is not already overridden, otherwise
                    # returns the already existing override.
                    overrideNode = el.FindOrAddOverrideParam(
                        takeData, tag, descId, newValue)
                    if overrideNode is None:
                        raise RuntimeError("Failed to find the override node.")
                    # Lines your code was missing
                    overrideNode.UpdateSceneNode(takeData, descId)
    c4d.EventAdd()

# Execute main()
if __name__=='__main__':
    main()

Hi @zipit,
I'm sorry if I wasn't clear what I was trying to achieve. I used your updated script and recorded a short gif, which shows, the projection materials I'm trying to change in takes stays the same after running the script. I'm expecting the projection to change from current Spherical to new UVW Mapping. What am I missing?

My test

Best regards,
Tomasz

Hi @Futurium,

we have seen your issue and I will report back when we have an answer, which might take a while, because I had to reach out to the animation team.

Cheers,
Ferdinand

Hi @zipit Not sure what do you mean. The problem is not related to animation.
I recorded my screen when tried your solution, and shared with you the results of the script, which were not what I was expecting - the projection stays "Spherical" after running the script in the Script Manager.

Hi @Futurium,

your problem is related to the Take System which is being handled by Maxon's animation development team. I do understand your problem, and you can reproduce it in Cinema 4D itself (without Python). I just reached out to the animation team so ask them if what you are trying to do is simply not intended to be done or a bug.

I will give you a heads up here when I did hear back from the animation team.

Cheers,
Ferdinand

Hi @zipit
Thank you for your clarification.
Is there any other way of doing that? Currently, I'm trying to modify projection for existing takes, but if there is a way to do it from scratch when the take is created - that could work for now.
Basically what I'm trying to do is to create multiple different takes per room, and for every take - apply the same material to different floor meshes (so if you're in bedroom1 and you change carpet - the carpet material would change on all other defined the rooms)
I don' mind to send you my current code + files we use to generate the takes, but that's not something I would be able to share on the public forum.
Best regards,
Tomasz

Hi @Futurium,

The problem here for us at the SDK-Team is that we are not so sure if what you are trying to do is encountering a bug or simply not intended to be done. I spoke a bit with one of the devs and they will have a look at that odd behavior (which can be reproduced in Cinema without any code), but it will probably take us some time, because we are currently quite busy.

The "problem" with your approach is that you are trying to pile a parameter override on top of tag which is crated by an override group. This is not how it is usually done in Cinema's Take system, you normally just edit that "virtual tag". If you try to do manually what your script does, Cinema will simply delete these overrides once you change the current take (which is also what happens in your script, the last take will contain the UVW-override, but it will not be respected by Cinema).

I personally would say this level of complexity - to create an attribute override for such "virtual tags" - was never intended in Cinema's take system, but I might be wrong. You can of course still do it in Python like you would normally do it Cinema, i.e., simply edit the override group tag. I have attached a script example based on your last script which does that. It will change the projection of your material tags to UVW, but won't do it with the same implied complexity as your example does it (by creating a specific attribute override for it).

Happy holidays,
Ferdinand

"""Example for "working around" the limitations of take system override groups
as discussed in:

    https://plugincafe.maxon.net/topic/13077
"""

import c4d

def yieldSecondLevelTakes():
    """Yields all second level take nodes in the scenes take graph.

    Your functions, just slightly refactored. Kept this way for clarity, but
    could be done in just one function instead of your two function design.
    """
    def yieldTopLevelTakeNodes():
        """Yields the top level take nodes of a document.
        """
        takeData = doc.GetTakeData()
        if takeData is None:
            return

        mainTake = takeData.GetMainTake()
        take = mainTake.GetDown()

        while take is not None:
            yield take
            take = take.GetNext()

    for take in yieldTopLevelTakeNodes():
        chldTake = take.GetDown()
        while chldTake is not None:
            yield chldTake
            chldTake = chldTake.GetNext()

def main():
    """Entry point.
    """
    # Clear Console
    c4d.CallCommand(13957) 
    # Not necessary, doc is predefined in a script.
    # doc = documents.GetActiveDocument()

    # Terminate branches early so that you have less indented code (and it
    # technically also runs a bit faster).
    takeData = doc.GetTakeData()
    if takeData is None:
        raise RuntimeError("No take data in the document.")

    # Loop over all second level take nodes like you want to do.
    for take in yieldSecondLevelTakes():
        print ("Current take name : " + take.GetName())

        # Not needed
        # takeData.SetCurrentTake(take)

        # This is not necessary in your scene, since all your objects
        # have the same override group tag, i.e. changing a parameter
        # on one tag will change the respective parameters on all other
        # 'instances' of the tag.

        # for currentObject in objects:
        #    obj = doc.SearchObject(currentObject)
        # ...

        # This should be technically moved outside of the loop for
        # performance reasons, kept here for parity reasons with your script.
        node = doc.SearchObject("Plane")
        if node is None:
            raise RuntimeError("Could not find targeted object node.")

        # The first override group, there could be more of course in a more
        # complex scene.
        overrideGroup = take.GetFirstOverrideGroup()
        if overrideGroup is None:
            continue

        # Get the material tag associated with this override group.
        tag = overrideGroup.GetTag(c4d.Ttexture)
        if tag is None:
            continue

        # The "workaround" here is to simply write to the tag. This of
        # course will not give you the same complexity as one could
        # imagine with takes, but since the tag is a BaseOverride tag,
        # i.e. a tag that itself is bound to  a Take, your data will
        # still be conditional in that sense.
        tag[c4d.TEXTURETAG_PROJECTION] = c4d.TEXTURETAG_PROJECTION_UVW

    # Notify Cinema 4D for updates.
    c4d.EventAdd()

# Execute main()
if __name__=='__main__':
    main()

Hi,

without further feedback, we will consider this thread as solved by Wednesday and flag it accordingly.

Cheers,
Ferdinand

Thank you @zipit
I adopted your solution into our code and it works exactly as expected.
Thank you for your help.
Best regards,
Tomasz