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 forMyType.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 newMatAssignData
, 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 theMatAssignData
, probably because the paramater is calledpObject
. 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 aBaseObject
can have multipleTextureTag
instances and theMatAssignData
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 ignoringInsertObject
altogether and writing data directly into tags. At least I do not see any substantial difference here. I provided a solution below [1].Cheers,
FerdinandResult:
- 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__':
-
@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
-
Hello @Dunhou ,
without further questions or postings, we will consider this topic as solved by Thursday 01/06/2023 and flag it accordingly.
Thank you for your understanding,
Maxime.