UNSOLVED 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 node node passed to NodeData.Message() is not yet part of a document, i.e., node.GetDocument() returns None. 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 like GetActiveDocument() 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 node dnode in respect to the source node snode. Unless you have found a clever way to get hold of the document the node node will be part of when MSG_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 via node.GetDocument(). I use the data argument of NodeData.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 its CoreMessage 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 for c4d.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