UNSOLVED c4d.MatAssignData can't link.

Hi :

Question :

I try to merge all selected materials assignment to a new material assignment . aka merge materials to one new material .

But it won't assign the new MatAssignData to new material .

How can I fix this 🤔

Here is the mini code :

# @2023.1.0
from typing import Optional
import c4d

doc: c4d.documents.BaseDocument  # The active document
op: Optional[c4d.BaseObject]  # The active object, None if unselected

def merge_material() -> None:
    myMat = doc.GetActiveMaterial()
    ObjLink = myMat[c4d.ID_MATERIALASSIGNMENTS]

    new_mat = c4d.BaseMaterial(5703)
    doc.InsertMaterial(new_mat)
    new_mat.SetName(myMat.GetName() + " Combine")
    new_mat_link = c4d.MatAssignData()

    myCount = ObjLink.GetObjectCount()
    for i in range(myCount) :  
        tag = ObjLink.ObjectFromIndex(doc,i)
        obj = tag.GetObject()
        #print (obj)
        new_mat_link.InsertObject(obj,0)
    
    print(new_mat_link.GetObjectCount())
    
    # this will failed
    # AttributeError: parameter set failed
    new_mat[c4d.ID_MATERIALASSIGNMENTS] = new_mat_link
    
    doc.SetActiveMaterial(new_mat, c4d.SELECTION_NEW)
    
    c4d.EventAdd()
    
if __name__ == '__main__':
    merge_material()

Hello @dunhou,

Thank you for reaching out to us. There are mistakes in your code, but the core issue is caused by the Python API, you can however easily sidestep all the issue.

What does AttributeError: parameter set failed mean?

This means that either the Python API does not support writing a parameter or the parameter is read-only/immutable in general. This often does only apply to the parameter object itself, not the data type. So, as a Python analogy, one could easily imagine it not being allowed to overwrite MyType.Data: list[int], i.e., myInstance.Data = [1, 2, 3] being illegal because the setter for MyType.Data does not exist. But while the parameter (in Python the property) is immutable, the data type it is using could be mutable. E.g., myInstance.Data.append(1) is fine because list is a mutable type. The same can be applied to the Cinema 4D Python API. Instead of doing this,

new_mat_link = c4d.MatAssignData()
# Mutate new_mat_link ...
new_mat_link.InsertObject(*data)
new_mat[c4d.ID_MATERIALASSIGNMENTS] = new_mat_link

you could be doing this:

new_mat_link = new_mat[c4d.ID_MATERIALASSIGNMENTS]
# Mutate new_mat_link ...
new_mat_link.InsertObject(*data)

In the first case we try to mutate new_mat[c4d.ID_MATERIALASSIGNMENTS] by allocating a new MatAssignData, in the second case we don't, i.e., we are sort of doing the same as we did in the example from above, where instead of writing an entirely new list, we modified an existing one.

What is an object?

There is also the problem that you tried to insert BaseObject instances into the MatAssignData, probably because the paramater is called pObject. You must insert a tag here. pObject likely stands for pointed object, i.e., similar to op (object pointer). The word object in argument names is meant in our APIs usually as class/type object and not as geometry. Inserting geometry objects cannot work as a BaseObject can have multiple TextureTag instances and the MatAssignData would then not know which one to overwrite/handle.

Stuff is still now working ...

When you fix all this, the script is still not working [2]. The reason seems to be that BaseMaterial[c4d.ID_MATERIALASSIGNMENTS] returns a copy of the material assignment data, so the code will run fine, but changes are never reflected in Cinema 4D. There could be a plethora of reasons why it was done like this, ranging from pure oversight to an intentional boundary. This is however all relatively easy fixable by ignoring InsertObject altogether and writing data directly into tags. At least I do not see any substantial difference here. I provided a solution below [1].

Cheers,
Ferdinand

Result:

  • Before: Screenshot 2022-11-14 at 12.49.01.png
  • After: Screenshot 2022-11-14 at 13.08.29.png

[1]

"""Replaces all materials in a list of materials with a new material in their document, updating
all TextureTag references to the old material with a reference to the new material.
"""

# @2023.1.0
import c4d
import random
import typing

doc: c4d.documents.BaseDocument  # The active document
op: typing.Optional[c4d.BaseObject]  # The active object, None if unselected

def ReplaceMaterials(materialList: list[c4d.BaseMaterial]) -> None:
    """Replaces all materials in #materialList in their documents with a new material.

    Also creates undo items for each material. Due to how I have fashioned the method, this will
    result in an undo step being added for each material in each document. To change this, you could
    either guarantee #materialList to be always composed out of materials from the same document and
    then simply move the Start/EndUndo() outside of the loop, or you would have to do some 
    preprocessing to group materials by document first and then wrap each group into one undo.

    Args:
        materialList (list[c4d.BaseMaterial]): The list of materials to replace, materials can be
         located in different documents.

    Raises:
        RuntimeError: When a material in #materialList is a dangling material.
        MemoryError: On allocation failures.
    """
    # Iterate over all materials in #materialList and get the document of each material.
    for mat in materialList:
        doc: c4d.documents.BaseDocument = mat.GetDocument()
        if not isinstance(doc, c4d.documents.BaseDocument):
            raise RuntimeError(f"The material {mat} is not attached to a document.")

        # Get the material assignment data (the tags referencing a material) and the count of
        # links, step over the material when it has less than one link, i.e., is unused.
        linkList: c4d.MatAssignData = mat[c4d.ID_MATERIALASSIGNMENTS]
        linkCount: int = linkList.GetObjectCount()
        if linkCount < 1:
            continue
        
        # Allocate a replacement material, give it a meaningful name, a random color, and insert
        # it into the document of #mat, i.e., the document of the material it is meant to replace.
        replacement: c4d.BaseMaterial = c4d.BaseMaterial(c4d.Mmaterial)
        if not isinstance(replacement, c4d.BaseMaterial):
            raise MemoryError("Could not allocate new material.")
        
        doc.StartUndo()
        replacement.SetName(f"{mat.GetName()} (Combine)")
        replacement[c4d.MATERIAL_COLOR_COLOR] = c4d.Vector(random.uniform(0, 1), 
                                                           random.uniform(0, 1), 
                                                           random.uniform(0, 1))
        doc.InsertMaterial(replacement)
        doc.AddUndo(c4d.UNDOTYPE_NEWOBJ, replacement)
        
        # Iterate over all texture tags referencing the old material #mat and replace their link
        # with #replacement, while stepping over dangling material assignment references, i.e.,
        # entries in the MatAssignmentData to TextureTag objects which do not exist anymore.
        for i in range(linkCount):
            tag: c4d.TextureTag = linkList.ObjectFromIndex(mat.GetDocument(), i) 
            if not isinstance(tag, c4d.TextureTag):
                print (f"Warning - Stepping over dangling material assignment data: {mat}({i})")
                continue

            doc.AddUndo(c4d.UNDOTYPE_CHANGE, tag)            
            tag[c4d.TEXTURETAG_MATERIAL] = replacement
        
        # Close the undo for #mat
        doc.EndUndo()
    
if __name__ == '__main__':
    # Get all materials in #doc and replace them, due to how we fashioned ReplaceMaterials(), we
    # could insert materials from different documents into #materials. This would of course not
    # persistently change these documents (on disk), one would have to still save their new state
    # to disk.
    materials: list[c4d.BaseMaterial] = doc.GetMaterials()
    # materials += doc1.GetMaterials() + doc2.GetMaterials() + ...
    ReplaceMaterials(materials)
    c4d.EventAdd()

[2]

# This still does not work due to how the parameter is handled.

# @2023.1.0
from typing import Optional
import c4d

doc: c4d.documents.BaseDocument  # The active document
op: Optional[c4d.BaseObject]  # The active object, None if unselected

def merge_material() -> None:
    myMat = doc.GetActiveMaterial()
    ObjLink = myMat[c4d.ID_MATERIALASSIGNMENTS]

    new_mat = c4d.BaseMaterial(5703)
    doc.InsertMaterial(new_mat)
    new_mat.SetName(myMat.GetName() + " Combine")
    new_mat_link = new_mat[c4d.ID_MATERIALASSIGNMENTS]

    myCount = ObjLink.GetObjectCount()
    for i in range(myCount) :  
        tag = ObjLink.ObjectFromIndex(doc,i)
        print (new_mat_link.InsertObject(tag, 0))
    
    print(new_mat_link.GetObjectCount())
    doc.SetActiveMaterial(new_mat, c4d.SELECTION_NEW)
    
    c4d.EventAdd()
    
if __name__ == '__main__':

@ferdinand Thanks for this detailed explanation.

About 1st , the AttributeError: parameter set failed . I remembered that I trying to set a cloner Inexclude data or something like that , sometimes it will return None type , but when I drag a "data" to it , it can be received.(seems like in R26 , I have update to 2023.1.0 , and try it just now , it will work again , I can't make sure want is happened earlier )

new_mat_link = new_mat[c4d.ID_MATERIALASSIGNMENTS]

And the Object , this is a great explanation. I didn't realized that material assignment is depend on actually texture tag 😧

@ferdinand said in c4d.MatAssignData can't link.:

There could be a plethora of reasons why it was done like this, ranging from pure oversight to an intentional boundary.

And the last InsertObject why didn't work explain I can't really understand (It's to technical to me 😢 ) But the fix function is really smart way to bypass the problem .

Cheers🥂