Question: Takesystem...changing texture for each take

Hi fellows,
another stupid question from me

Job:

  • We have a scene with a lot of different objects and a lot of takes.

  • Every take has a specific naming convention: 6 digits number - R number
    e.g. 123456 - R 1234567...

  • the client wants that the first 6 digits of the take is the filename of a picture.

  • The pictures are in a folder and already have that number as filename

  • Then he wants that every take changes the texture accordingly to it's name in a Main-Material
    Because there are so many takes , he wishes for an automated process with a script for instance

My solution:

  • Get the path to the texture folder
  • Get the Main-Material
  • Create a Bitmap-Shader
  • Go step by step through the takes and grab the name
  • join path and name to get the file-name
  • simultanously changing the textures in the material
  • and adding them to the take-system by overriding each node

Problem:

  • I get all the names, takes succesfully and so the filenames
  • The material is not updating and so the nodes in the takesystem.
  • The material does not recognize the texture-paths at all.
  • So I am using absolute paths because I do not know where he has installed the textures.
  • So there is no updating c4d.BITMAPSHADER_FILENAME and so the takes.

Here is the code: ( a bit messy because I tried a few things, the code can definitely be shortened..But script works...

import c4d
from c4d import gui
from c4d.modules.takesystem import TakeData
import os



def main():

#check if auto take and override is checked
    if doc.GetTakeData().GetOverrideEnabling()== 4095 and doc.GetTakeData().GetTakeMode()== c4d.TAKE_MODE_AUTO:  

#Get texture folder path
        dir_path = c4d.storage.LoadDialog(type=c4d.FILESELECTTYPE_ANYTHING,
                        title="Choose Folder for textures",
                        flags=c4d.FILESELECT_DIRECTORY)   

#define material and shader
        mat=doc.SearchMaterial("Main-Material")
        shader=c4d.BaseList2D(c4d.Xbitmap)
        mat[c4d.MATERIAL_COLOR_SHADER]=shader

#define take data and main take
        take_data = doc.GetTakeData()
        main_take = take_data.GetMainTake()

        take_data.SetCurrentTake(main_take)


#check if main take has a child then continue the script
        if main_take.GetDown()!= None:

#Go to the child of main take
            take_data.SetCurrentTake(main_take.GetDown())
            current=take_data.GetCurrentTake()

 #give the material the shader with the filename

            shader[c4d.BITMAPSHADER_FILENAME]=os.path.join(dir_path,current[c4d.ID_BASELIST_NAME].split(" -")[0]+".png")
 

            mat[c4d.MATERIAL_COLOR_SHADER]=shader
            mat.Update(True, True)

            try:
                mat.InsertShader(shader)
            except:
                pass

           #try to add the node to the take and override if existing (bad code :cry: )
            overrider=current.OverrideNode(take_data,mat,True)
            desc=overrider.GetAllOverrideDescID()

            for i in desc:
                overrider.UpdateSceneNode(take_data,i)
            del desc


        #===Just to get the rest count of takes===================
            counter=0
            while take_data.GetCurrentTake().GetNext() != None:
                take_data.SetCurrentTake(take_data.GetCurrentTake().GetNext())
                counter+=1

#aktivate the first child go down each take and change texture and add to take
            take_data.SetCurrentTake(main_take.GetDown())

            for i in range(counter):

                take_data.SetCurrentTake(take_data.GetCurrentTake().GetNext())
                current=take_data.GetCurrentTake()


                shader[c4d.BITMAPSHADER_FILENAME]=os.path.join(dir_path,current[c4d.ID_BASELIST_NAME].split(" -")[0]+".png")
                mat[c4d.MATERIAL_COLOR_SHADER]=shader

#I do not know if I have to insert shader again, basically not , so I just tried
                try:
                    mat.InsertShader(shader)
                except:
                    pass
                mat.Update( True, True)

# I do not know if this is correct so I just overrided all
                overrider=current.OverrideNode(take_data,mat,True)
                desc=overrider.GetAllOverrideDescID()

                for i in desc:
                    overrider.UpdateSceneNode(take_data,i)


                c4d.EventAdd()


    else:
        c4d.gui.MessageDialog("Please Turn on Auto Take and Lock Overrides Mode")


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

I added the four pics with the correct filename and the c4d file. Simply save all in a folder and
put the script into the script folder if you want, to test it out..

Thank you very much

Takesystem.c4d 34348.png 23456.png 23455.png 12344.png

Take-tests.py

Hello @thomasb,

thank you for reaching out to us. There are several problems with your code and there is a major problem with our API.

  • Your code is not correct when it comes parsing the file names of the files you have provided. Or in other words, the files you have provided have incorrect file names.
  • You must insert the shader once when you create it.
  • There are some other minor problems as not checking returns values and using OverrideNode() (twice) which I do not really understand why you are doing this.

There are in principle two ways how to do this:

  • Use BaseTake.FindOrAddOverrideParam() to override a single parameter, the BITMAPSHADER_FILENAME parameter.
  • Use BaseTake.AutoTake() to pass the actual node (the shader) and a copy of it where parameters have been changed (e.g., BITMAPSHADER_FILENAME) and let Cinema 4D figure out the rest.

The problem is however that this does not work (which is the major problem of our API). I have provided [1] a script which implements what you want to do cleanly, but the result is always the same: The take data structure is identical to the one created by Cinema 4D when such setup is created manually, it is only that the actual value of BITMAPSHADER_FILENAME is not written, but instead is a string (as shown in the screen below).

a0bbf6fe-0e7c-4066-8505-ba825e09137b-image.png

  • Not shown here, but also tried by me was the AutoTake approach, which yields the same results.
  • This is also not tied to BITMAPSHADER_FILENAME, but it happens for example also for the Sampling parameter of the Xbitmap shader.
  • My code does however run fine on a XColor shader and creating overrides for its COLORSHADER_COLOR parameter.

Which is all a bit odd, to say the least. In principle using FindOrAddOverrideParam is pretty straight forward and at least we at SDK do not see anything wrong with the code I have provided (which does not help you much of course). I have reached out to the developer of the take system to ask if there is anything special about the take system regarding bitmap shaders. This could either be some undocumented steps which must be taken or a bug in the take system or its Python bindings (I have not tried C++ yet).

I cannot give you an ETA when we will come back with a final answer, as we are currently very busy with release preparations. In the mean time I can only recommend taking another approach. What you could do is the following:

  • Create materials for each of your textures
  • Then create overrides for the texture tags that reference your "Main-Material" and create overrides for the TEXTURETAG_MATERIAL parameter. There have been C++ and Python posts in this forum which discuss that topic.

Cheers,
Ferdinand

[1] The Python code which I used (I replaced the name of the target material with "Mat"):

"""Creates overrides for the file name of bitmap shaders.

Note:
    THIS CODE DOES NOT WORK AS OF R25SP1.
"""

import c4d
import os
import typing

# The ID of the lock overrides command.
CID_LOCK_OVERRIDES = 431000108
# The ID of the BITMAPSHADER_FILENAME parameter as it is required by overrides.
DID_BITMAPSHADER_FILENAME = c4d.DescID(
    c4d.DescLevel(c4d.BITMAPSHADER_FILENAME, c4d.DTYPE_FILENAME, c4d.Xbitmap))

def AssertType(item: any, t: typing.Type, label: str) -> None:
    """Asserts that #item is of type #t. When the assertion fails, an assertion error will be 
    raised with #label referring to #item.
    """
    if not isinstance(item, t):
        raise AssertionError(f"Expected {t} for '{label}'. Received: {type(item)}")

def GetTargetMaterial(doc: c4d.documents.BaseDocument) -> c4d.BaseMaterial:
    """Returns the material which is the target of the take operations for the script.
    """
    AssertType(doc, c4d.documents.BaseDocument, "doc")
    material = doc.SearchMaterial("Mat")
    if material is None:
        raise RuntimeError(f"Could not find target material in: {doc}")

    return material

def GetBitmapShaders(material: c4d.BaseMaterial, 
                     channels: list[int]) -> list[c4d.BaseShader(c4d.Xbitmap)]:
    """Returns bitmap shaders for all the passed #channels in #material.

    Will raise error when the target parameters are not of type BaseLink and create shaders for
    BaseLink parameters where no shader does exist.
    """
    AssertType(material, c4d.BaseMaterial, "material")
    AssertType(channels, (list, tuple), "channels")

    data = material.GetDescription(c4d.DESCFLAGS_DESC_NONE)
    results = []
    for descID in channels:
        AssertType(descID, int, "descID")
        # Get the data container of the material and assert that the ID points is to a BASELISTLINK
        data = material.GetDataInstance()
        if data.GetType(descID) != c4d.DTYPE_BASELISTLINK:
            raise TypeError(
                f"The id {descID} does not point to a 'DTYPE_BASELISTLINK' in {material}")

        # Get the linked shader.
        shader = data.GetLink(descID)

        # There is no link established yet. Create a new shader.
        if shader is None:
            shader = c4d.BaseShader(c4d.Xbitmap)
            if shader is None:
                raise MemoryError(f"Could not allocate new shader.")
            material.InsertShader(shader)
            material[descID] = shader
        # All other cases.
        else:
            AssertType(shader, c4d.BaseShader, "shader")
            if not shader.CheckType(c4d.Xbitmap):
                raise TypeError(
                    f"Found shader in target channel that is not a bitmap shader: {descID}")
        
        # Append the shader to the results.
        results.append(shader)

    return results

def GetDocumentTakes(doc: c4d.documents.BaseDocument) -> c4d.modules.takesystem.BaseTake:
    """Yields all non main-take takes in #doc.
    """
    AssertType(doc, c4d.documents.BaseDocument, "doc")

    takeData = doc.GetTakeData()
    mainTake = takeData.GetMainTake()

    def iterate(node):
        """Walks #node depth first.
        """
        if node is None:
            return

        while node:
            yield node

            for descendant in iterate(node.GetDown()):
                yield descendant
            node = node.GetNext()

    for take in iterate(mainTake.GetDown()):
        yield take

def GetTextureData() -> dict[str: str]:
    """Returns a dictionary of identifier and file-path tuples for a texture directory.
    """
    textureDirectory = c4d.storage.LoadDialog(type=c4d.FILESELECTTYPE_ANYTHING,
                                              title="Choose Folder for textures",
                                              flags=c4d.FILESELECT_DIRECTORY)
    if textureDirectory is None:
        return None
    
    result = {}
    for root, _, files in os.walk(textureDirectory):
        for f in files:
            # Massage the file names into a meaningful form by cutting of the extension and then
            # the prefix which is not reflected in your document. The bit in square brackets is
            # what we are interested in.
            #
            #    1647228063839-34348.webp -> 1647228063839, [34348], webp
            #
            name, _ = os.path.splitext(f)
            if not name.find("-"):
                raise RuntimeError(
                    f"Encountered texture name not following naming conventions: {f}")
            key = name.split("-")[1]

            # And store the absolute path under that identifier key.
            if key in result:
                raise RuntimeError(f"Ambiguous file identifier: '{result[key]}' and '{f}' collide.")
            result[key] = os.path.join(root, f)

    return result


def main(doc: c4d.documents.BaseDocument) -> None:
    """Runs the example.
    """
    AssertType(doc, c4d.documents.BaseDocument, "doc")

    # Get the textures. There were multiple problems with how you handled the file names, ranging
    # from assuming incorrect file types to not matching the file names. This could all done less
    # hastily compared to how I did implement it, but that would be up to you.
    textureData = GetTextureData()
    if textureData is None or textureData == {}:
        print ("User aborted operation or directory does not contain files.")
        return None
    
    # Get the target material and all relevant bitmap shaders for it.
    material = GetTargetMaterial(doc)

    # Ensure the "lock" overrides button is checked when the script is invoked.
    if not c4d.IsCommandChecked(CID_LOCK_OVERRIDES):
        c4d.CallCommand(CID_LOCK_OVERRIDES)

    # Iterate over all takes in the passed document and update them.
    takeData = doc.GetTakeData()
    for take in GetDocumentTakes(doc):

        # Get the take name and the imbedded texture identifier.
        takeName = take[c4d.ID_BASELIST_NAME].split(" -")
        if len(takeName) != 2:
            raise RuntimeError(
                f"Take naming convention violated for take: {take[c4d.ID_BASELIST_NAME]}")
        tid = takeName[0]

        # Try to find the matching texture in the texture data.
        if tid not in textureData:
            raise RuntimeError(
                f"No matching texture found for identifier '{tid}' for take '{takeName}'.")
        texturePath = textureData[tid]

        # Cerate a parameter override for each shader for that texture. This might be in
        # practice more complicated than shown here, as you might want to provide different
        # texture files for different channels.
        clone = material.GetClone(c4d.COPYFLAGS_NONE)
        shader = shader = c4d.BaseShader(c4d.Xbitmap)
        if None in (clone, shader):
            raise MemoryError("Could not allocate clone material or shader.")

        clone.InsertShader(shader)
        clone[c4d.MATERIAL_COLOR_SHADER] = shader
        clone.Message(c4d.MSG_UPDATE)

        shader[c4d.BITMAPSHADER_FILENAME] = texturePath
        shader.Message(c4d.MSG_UPDATE)

        take.AutoTake(takeData, material, clone)

        print (f"Added override in {take} with the value: '{texturePath}'.")
            
    c4d.EventAdd()

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

[2] This is some testing code I did use to compare the data of manually and programmatically generated takes:

import c4d

def GetDocumentTakes(doc: c4d.documents.BaseDocument) -> c4d.modules.takesystem.BaseTake:
    """Yields all non main-take takes in #doc.
    """
    takeData = doc.GetTakeData()
    mainTake = takeData.GetMainTake()

    def iterate(node):
        """Walks #node depth first.
        """
        if node is None:
            return

        while node:
            yield node

            for descendant in iterate(node.GetDown()):
                yield descendant
            node = node.GetNext()

    for take in iterate(mainTake.GetDown()):
        yield take

# Main function
def main():
    for take in GetDocumentTakes(doc):
        print (f"Take: {take}")
        print (f"\tGroups: {take.GetOverrideGroups()}")
        print (f"\tOverrides:")
        for override in take.GetOverrides():
            print (f"\t\tOverride: {override}")
            print (f"\t\t\tNode: {override.GetSceneNode()}")
            print (f"\t\t\tParameter: {override.GetAllOverrideDescID()}")
        print("")

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

[3] And finally, here is one of the AutoTake() variants I tried, here I do operate intentionally on the material and not the shader level.

"""Creates overrides for the file name of bitmap shaders.

Note:
    THIS CODE DOES NOT WORK AS OF R25SP1.
"""

import c4d
import os
import typing

# The ID of the lock overrides command.
CID_LOCK_OVERRIDES = 431000108
# The ID of the BITMAPSHADER_FILENAME parameter as it is required by overrides.
DID_BITMAPSHADER_FILENAME = c4d.DescID(
    c4d.DescLevel(c4d.BITMAPSHADER_FILENAME, c4d.DTYPE_FILENAME, c4d.Xbitmap))

def AssertType(item: any, t: typing.Type, label: str) -> None:
    """Asserts that #item is of type #t. When the assertion fails, an assertion error will be 
    raised with #label referring to #item.
    """
    if not isinstance(item, t):
        raise AssertionError(f"Expected {t} for '{label}'. Received: {type(item)}")

def GetTargetMaterial(doc: c4d.documents.BaseDocument) -> c4d.BaseMaterial:
    """Returns the material which is the target of the take operations for the script.
    """
    AssertType(doc, c4d.documents.BaseDocument, "doc")
    material = doc.SearchMaterial("Mat")
    if material is None:
        raise RuntimeError(f"Could not find target material in: {doc}")

    return material

def GetBitmapShaders(material: c4d.BaseMaterial, 
                     channels: list[int]) -> list[c4d.BaseShader(c4d.Xbitmap)]:
    """Returns bitmap shaders for all the passed #channels in #material.

    Will raise error when the target parameters are not of type BaseLink and create shaders for
    BaseLink parameters where no shader does exist.
    """
    AssertType(material, c4d.BaseMaterial, "material")
    AssertType(channels, (list, tuple), "channels")

    data = material.GetDescription(c4d.DESCFLAGS_DESC_NONE)
    results = []
    for descID in channels:
        AssertType(descID, int, "descID")
        # Get the data container of the material and assert that the ID points is to a BASELISTLINK
        data = material.GetDataInstance()
        if data.GetType(descID) != c4d.DTYPE_BASELISTLINK:
            raise TypeError(
                f"The id {descID} does not point to a 'DTYPE_BASELISTLINK' in {material}")

        # Get the linked shader.
        shader = data.GetLink(descID)

        # There is no link established yet. Create a new shader.
        if shader is None:
            shader = c4d.BaseShader(c4d.Xbitmap)
            if shader is None:
                raise MemoryError(f"Could not allocate new shader.")
            material.InsertShader(shader)
            material[descID] = shader
        # All other cases.
        else:
            AssertType(shader, c4d.BaseShader, "shader")
            if not shader.CheckType(c4d.Xbitmap):
                raise TypeError(
                    f"Found shader in target channel that is not a bitmap shader: {descID}")
        
        # Append the shader to the results.
        results.append(shader)

    return results

def GetDocumentTakes(doc: c4d.documents.BaseDocument) -> c4d.modules.takesystem.BaseTake:
    """Yields all non main-take takes in #doc.
    """
    AssertType(doc, c4d.documents.BaseDocument, "doc")

    takeData = doc.GetTakeData()
    mainTake = takeData.GetMainTake()

    def iterate(node):
        """Walks #node depth first.
        """
        if node is None:
            return

        while node:
            yield node

            for descendant in iterate(node.GetDown()):
                yield descendant
            node = node.GetNext()

    for take in iterate(mainTake.GetDown()):
        yield take

def GetTextureData() -> dict[str: str]:
    """Returns a dictionary of identifier and file-path tuples for a texture directory.
    """
    textureDirectory = c4d.storage.LoadDialog(type=c4d.FILESELECTTYPE_ANYTHING,
                                              title="Choose Folder for textures",
                                              flags=c4d.FILESELECT_DIRECTORY)
    if textureDirectory is None:
        return None
    
    result = {}
    for root, _, files in os.walk(textureDirectory):
        for f in files:
            # Massage the file names into a meaningful form by cutting of the extension and then
            # the prefix which is not reflected in your document. The bit in square brackets is
            # what we are interested in.
            #
            #    1647228063839-34348.webp -> 1647228063839, [34348], webp
            #
            name, _ = os.path.splitext(f)
            if not name.find("-"):
                raise RuntimeError(
                    f"Encountered texture name not following naming conventions: {f}")
            key = name.split("-")[1]

            # And store the absolute path under that identifier key.
            if key in result:
                raise RuntimeError(f"Ambiguous file identifier: '{result[key]}' and '{f}' collide.")
            result[key] = os.path.join(root, f)

    return result


def main(doc: c4d.documents.BaseDocument) -> None:
    """Runs the example.
    """
    AssertType(doc, c4d.documents.BaseDocument, "doc")

    # Get the textures. There were multiple problems with how you handled the file names, ranging
    # from assuming incorrect file types to not matching the file names. This could all done less
    # hastily compared to how I did implement it, but that would be up to you.
    textureData = GetTextureData()
    if textureData is None or textureData == {}:
        print ("User aborted operation or directory does not contain files.")
        return None
    
    # Get the target material and all relevant bitmap shaders for it.
    material = GetTargetMaterial(doc)

    # Ensure the "lock" overrides button is checked when the script is invoked.
    if not c4d.IsCommandChecked(CID_LOCK_OVERRIDES):
        c4d.CallCommand(CID_LOCK_OVERRIDES)

    # Iterate over all takes in the passed document and update them.
    takeData = doc.GetTakeData()
    for take in GetDocumentTakes(doc):

        # Get the take name and the imbedded texture identifier.
        takeName = take[c4d.ID_BASELIST_NAME].split(" -")
        if len(takeName) != 2:
            raise RuntimeError(
                f"Take naming convention violated for take: {take[c4d.ID_BASELIST_NAME]}")
        tid = takeName[0]

        # Try to find the matching texture in the texture data.
        if tid not in textureData:
            raise RuntimeError(
                f"No matching texture found for identifier '{tid}' for take '{takeName}'.")
        texturePath = textureData[tid]

        # Cerate a parameter override for each shader for that texture. This might be in
        # practice more complicated than shown here, as you might want to provide different
        # texture files for different channels.
        clone = material.GetClone(c4d.COPYFLAGS_NONE)
        shader = shader = c4d.BaseShader(c4d.Xbitmap)
        if None in (clone, shader):
            raise MemoryError("Could not allocate clone material or shader.")

        clone.InsertShader(shader)
        clone[c4d.MATERIAL_COLOR_SHADER] = shader
        clone.Message(c4d.MSG_UPDATE)

        shader[c4d.BITMAPSHADER_FILENAME] = texturePath
        shader.Message(c4d.MSG_UPDATE)

        take.AutoTake(takeData, material, clone)

        print (f"Added override in {take} with the value: '{texturePath}'.")
            
    c4d.EventAdd()

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

First thank you Ferdinand,
as always, you put so much effort in your answers, this is so great..
Related to my code, yes there are of course weird things going on sometimes :-), I am not professional and the API is sometimes
like a jungle and so Python, so I am messing up a few things...This process is called "learning" 🙂

But anyways thank you for your detailed and quick answer....

Sincerely Thomas