Unique Material per Object Plugin instance?
-
Hi Guys,
after fiddling around some time on my own, I feel it's finally time to reach out to you.
The Situation is as follows. I have an Object Data Plugin that has a texture tag on it with a material assigned to it. I create the tag and the material in
def message
of the Object Plugin checking forc4d.MSG_MENUPREPARE
.So this is workinig and all good and well. But when I copy the object in the Object Manager the material does not get copied. So I end up with multiple Objects that share the same material. However creating a new instance of the plugin by getting it from the plugins menu, will of course create a new material since
c4d.MSG_MENUPREPARE
gets called here.While I'm pretty sure this behaviour is fine for most cases, my plugin is relying on the fact that each instance gets its own material (the node's parameters changes values of the material).
So how can I achieve that every instance of the plugin gets its own material?
Cheers,
Sebastian -
Hello @herrmay,
thank you for reaching out to us and please excuse the short delay. The short answer to your problem is that you should implement NodeData.CopyTo() for your plugin to manually handle the copying of a node.
However, in the process of answering your posting I found what appears to be a substantial problem. You talk about using
c4d.MSG_MENUPREPARE
to establish a material for a newly created node in the first place. Aside from smaller problems, this comes with a major problem: At the time when MSG_MENUPREPARE is being broadcasted, the nodenode
passed toNodeData.Message()
is not yet part of a document, i.e.,node.GetDocument()
returnsNone
. So, one has subsequently no way of knowing into which document one should insert the new material one wants to create. I assume you simply use something likeGetActiveDocument()
but that is a dangerous assumption because nodes will not always be created just for the active document. I then tried to find a way around the problem, only to find out that this work around will crash Cinema 4D with a stack overflow.What you want to do lies outside of the scope of what
ObjectData
is intended for. We are aware that what you want to do is a common feature wish, but conceptually nodes are not intended to modify the graph they are part of. The more natural solution would be to return the material as part of the cache of your object. But the linked material should then be hidden from the users' view and allocating the materials in the first place will then be a bit of a hassle, as you will need to allocate them ahead of time (i.e., before GVO runs as it is not on the main thread). In the end this is a pattern which is not supported by the SDK.I cannot really post here example code when I cannot even safely allocate the material in the first place. In principle you must overwrite
NodeData.CopyTo()
and then handle the the destination nodednode
in respect to the source nodesnode
. Unless you have found a clever way to get hold of the document the nodenode
will be part of whenMSG_MENUPREPARE
is being invoked, your general approach is however flawed.I will keep this post updated with regards to the crash I encountered and if it's possible to safely insert a material tag from Python into the node that is representing your plugin.
Cheers,
Ferdinand -
Hi @ferdinand,
no worries, you have for sure a load to do around here. Thanks for taking the time.
Okay I see,
NodeData.CopyTo()
then.Regarding the "document-getting-thing". I donโt get the document via
GetActiveDocument()
nor vianode.GetDocument()
. I use thedata
argument ofNodeData.Message()
instead.I donโt recall if I figured this out for myself or where I got this from. Or even do I know if it is allowed at all.
BUT it is working. This way I can reliably get the document.
Cheers,
Sebastian -
Hi Guys,
as I'm pretty sure I found a way to achieve what I'm after I thought I update this thread. Maybe this will help others as well.
After taking some time to make
NodeData.CopyTo()
work, I ended up not getting it to work at all.So I thought about how I could achieve what I'm after a different way. Long story short, I ended up implementing a
MessageData
plugin as some kind of watchdog for a document. Since itsCoreMessage
runs on the main thread I can happily insert and delete materials as much as I wish to. (At least I'm hoping so )Tl; dr
The idea behind this goes as follows. I have a timer running and in addition to that I listen forc4d.EVMSG_CHANGE
and do some checking to see if the scene needs to update. In my case it's comparing the amount of a specific object against the amount of "specific" materials. If there's a difference I use that difference to delete or insert materials until there's no difference. Once there's no difference I can assign the materials to the objects and let each object control its own material.To distinguish between materials responsible for my plugin and the ones that aren't I make sure to put a unique plugin id inside the base container of the material I can then check for.
Here's a code snippet of that
MessageData
:class Watchdog(c4d.plugins.MessageData): PLUGIN_ID = "Use your own unique one" PLUGIN_NAME = "A MessageData plugin." PLUGIN_INFO = 0 def __init__(self): self._time = 1000 def GetTimer(self): return self._time def SetTimer(self, time): self._time = time @property def should_execute(self): is_mainthread = c4d.threading.GeIsMainThread() check_running = ( bool(c4d.CheckIsRunning(c4d.CHECKISRUNNING_EDITORRENDERING)), bool(c4d.CheckIsRunning(c4d.CHECKISRUNNING_EXTERNALRENDERING)), bool(c4d.CheckIsRunning(c4d.CHECKISRUNNING_INTERACTIVERENDERING)), bool(c4d.CheckIsRunning(c4d.CHECKISRUNNING_ANIMATIONRUNNING)), bool(c4d.CheckIsRunning(c4d.CHECKISRUNNING_VIEWDRAWING)) ) is_running = any(item is True for item in check_running) return is_mainthread and not is_running def CoreMessage(self, mid, mdata): if not self.should_execute: return False doc = c4d.documents.GetActiveDocument() # SceneHandler is a custom class I delegate the whole creation and comparing stuff to. objs, mats = ..., ... scene = SceneHandler(objs, mats) # Check for a change and start the timer again. But only if the scene should update. Otherwise the timer would run all the time. if mid == c4d.EVMSG_CHANGE: if scene.should_update: self.SetTimer(1000) # If we get a timer event we update the scene as long as it shouldn't update anymore. We can then stop the timer. if mid == c4d.MSG_TIMER: if not scene.should_update: self.SetTimer(0) scene.update(doc) return True
Maybe this will help others. Since I found a solution for my problem this thread can be marked solved.
Cheers,
Sebastian