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:

- After:

[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__':