Hello @bentraje,
Thank you for reaching out to us. A Python C4Datom
, e.g., a BaseMaterial
not being alive means that the pointer in the C++ layer is invalid. Which in turn implies that you cannot rely on pointers, which also impacts the Python layer, because when you retrieve a node, e.g., a a BaseMaterial
, it is storing effectively a pointer.
What are pointers and why are they a problem in the Python API?
A pointer is similar to (a reference of) an instance, as it has a type, and it can be used to access members of the instance. Here is a simplified and idealized C++ pseudo code demonstrating the concept.
Thing item = new Thing(); // An instance of the type #Thing taking up the amount of memory
// whatever the memory footprint of #Thing is.
Thing* pItem; // A pointer to a Thing instance, denoted by the little *, it only
// stores the memory location of the pointed object, not the whole
// object. This pointer has not been yet initialized, i.e., is
// currently pointing to nothing.
pItem = &item; // #pItem now points to #item, it can be used to "pass around" #item.
pItem->value = 42 // Use the pointer to access the field #value in #item.
delete(item); // Free the memory of #item. There is now no data structure with the
// memory layout of #Thing at the memory location #pItem is pointing to.
pItem->value = 42; // Depending on the used programming language, this either won't work
// anymore, crash the application, or even worse, change random data, as
// the former memory location of #item is now not occupied by a #Thing
// anymore and instead either just junk or (parts of) a new data
// structure.
And when you are doing this in your code,
class ListView(c4d.gui.TreeViewFunctions):
def __init__(self):
"""
"""
self.listOfTexture = doc.GetMaterials()
you are effectively telling Cinema 4D, 'Hey, please give me the pointers to all materials!'. Which it will do, wrapping them into BaseMaterial
Python objects. There is no direct equivalent for GetMaterials
in C++, but BaseDocument::GetFirstMaterial
returns for example BaseMaterial*
, i.e., a pointer to a material.
Now time goes on, and you then try to access self.listOfTexture[0]
and Cinema 4D tells you, 'nope, this BaseMaterial pointer is not valid anymore' or in terms of our Python API, it is a 'dead' object. The reason for that is that is either that the object has been simply deleted or that Cinema 4d has reallocated it, i.e., moved it to a new memory location. Things are reallocated all the time and while you might be able to still see the 'same ' material in the Material Manger, it might has been long reallocated and a formerly established pointer to it is not valid anymore.
How do I deal with this?
This might be a very foreign concept to a Python user, as Python hides all this pointer madness away, but the general idea is that you cannot rely on pointers to data for any long-term access, unless you manage the lifetime of the pointed objects yourself (which you do not do in Cinema 4D in a sensible manner).
- Copy things: Depending on what you want to do, you can simply copy the things you want to ensure to remain. In the most partial manner, this would mean copying the data containers of the materials:
self._data = [item.GetData() for item in doc.GetMaterials()]
. The problem with this is that you indeed get a copy. So, when a material is being changed after you retrieved the container, or when you want to change a material, this will not be reflected in/possible with your self._data
- Identify things with other means than pointers: A pointer is just a way of saying, 'hey, I mean this thing there, yes, exactly, that thing!' and there are other ways to accomplish this, most notably
GeMarker
and its specialization of C4DAtom.FindUniqueID
. With FindUniqueID
and MAXON_CREATOR_ID
you can uniquely identify objects, even over reallocation boundaries, i.e., a material which a user would consider 'the same' will always return the same UUID. This topic has been discussed multiple time on the forum, most recently here. There is also a thread where I asked some questions about the scope of the hashes for MAXON_CREATOR_ID
when I was a user.
What you do in your case is up to you, and what you want to be done, I would write an abstraction layer for BaseMaterial
(see end of posting).
Cheers,
Ferdinand
The code:
import c4d
import sys
doc: c4d.documents.BaseDocument # The active document.
class MaterialCollection:
"""Handles a collection of materials from a singular document for long-term referencing.
Will reestablish material references when possible.
"""
def __init__(self, collection: list[c4d.BaseMaterial]):
"""Initializes the collection with a list of materials.
"""
if not isinstance(collection, list):
raise TypeError(f"{collection = }")
# The list of materials, the list of markers for them, and their associated document.
self._materials: list[c4d.BaseMaterial] = []
self._markers: list[bytes] = []
self._doc: c4d.documents.BaseDocument = None
for material in collection:
# Sort out illegal types, "dead" materials, and materials not attached to a document.
if not isinstance(material, c4d.BaseMaterial) or not material.IsAlive():
raise TypeError(f"{material} is not alive or not a BaseMaterial.")
if material.GetDocument() is None:
raise RuntimeError("Dangling material cannot be managed.")
# Establish what the document of the materials is and that they all belong to the same
# document. Technically, a BaseDocument can also be dead (when it has been closed),
# but this is less likely to happen, so a stored reference to it can also become dead.
if self._doc is None:
self._doc = material.GetDocument()
elif self._doc != material.GetDocument():
raise RuntimeError("Can only manage materials attached to the same document.")
# Append the material and its UUID marker to the internal lists.
self._materials.append(material)
self._markers.append(MaterialCollection.GetUUID(material))
def __len__(self):
"""Returns the number of managed materials.
"""
return len(self._materials)
def __getitem__(self, index: int) -> c4d.BaseMaterial:
"""Returns the material at #index.
"""
if len(self._materials) - 1 < index:
raise IndexError(f"The index '{index}' is out of bounds.")
# The item is still alive, we can return it directly.
item: c4d.BaseMaterial = self._materials[index]
if item.IsAlive():
print ("Returning still valid material reference")
return item
# The item is not alive anymore, try to find its new instance.
if not self._doc.IsAlive():
raise RuntimeError(
f"{self._doc} is not valid anymore, cannot reestablish material references.")
# Get the marker identifying the material at #index.
materialMarker: bytes = self._markers[index]
# Iterate over all materials in #_doc to find one with a matching marker.
for material in self._doc.GetMaterials():
otherMarker: bytes = MaterialCollection.GetUUID(material)
# We found a match, update the internal data, and return the material.
if materialMarker == otherMarker:
self._materials[index] = material
print ("Returning re-established material reference")
return material
# There is no material with this marker anymore.
raise RuntimeError(
f"The material at {index} is not valid anymore and cannot be re-established.")
def __iter__(self):
"""Iterates over all materials in the collection.
"""
for index in range(len(self._materials)):
yield self[index]
@staticmethod
def GetUUID(atom: c4d.C4DAtom) -> bytes:
"""Retrieves the UUID of an atom as a bytes object.
Args:
item (c4d.C4DAtom): The atom to get the UUID hash for.
Raises:
RuntimeError: On type assertion failure.
Returns:
bytes: The UUID of #atom.
"""
if not isinstance(atom, c4d.C4DAtom):
raise TypeError(f"{atom = }")
# Get the MAXON_CREATOR_ID marker uniquely identifying the atom.
uuid: memoryview = atom.FindUniqueID(c4d.MAXON_CREATOR_ID)
if not isinstance(uuid, memoryview):
raise RuntimeError(f"Illegal non-marked atom: {atom}")
return bytes(uuid)
def main() -> None:
"""Runs the example and uses the handler type.
"""
print ("-" * 80)
# To make the example a bit more sensible, we are going to attach #MaterialCollection
# instance to sys.
collection: MaterialCollection = getattr(sys, "matCollection", None)
if collection is None and doc.GetMaterials() != []:
collection = MaterialCollection(doc.GetMaterials())
setattr(sys, "matCollection", collection)
print ("Stored material collection at sys.matCollection")
elif collection is not None:
print ("Retrieved stored material collection from sys.matCollection")
else:
print ("Document contains no materials.")
# Iterate over the collection, when running the script multiple times while interacting with
# the document, this will reestablish the references at some point.
for item in collection:
print (item)
if len(collection) > 0:
print (f"{collection[0] = }")
else:
print ("The collection has no items.")
if __name__ == "__main__":
main()