Issues Modifying Cloned Document

Hello! I'm attempting to make Cinema 4D's FBX export a bit more robust for our pipeline. We're dealing with lots of instancing of non-polygonal objects which C4D seems to have trouble outputting to FBX. Specifically we're seeing issues with Symmetry, Cloners and Subdivision Surfaces.

I wrote a script that worked well, but it would modify the current document and I felt it would be better to modify a clone of the document. In order to do that I built a class to handle the document preparation. Unfortunately, this seems to have broken something fundamental in the logic but I can't figure out why.

The plugin is supposed to find all instanced subdivision surfaces, create a group null below the subdivision, point all instances that had previously referred to the sds to that null, and then place each of the instances into a new subdivision surface (so that the instance is now the cage mesh). This is the only way I can find to get instances and subdivision surfaces to work together in an FBX export. However, in it's new form the plugin seems to end up with the instances inside of two nulls instead of inside a single subdivision surface. In addition some of the instances and subdivision surfaces are switching locations in the hierarchy. It's a mess!

Perhaps I've bitten off more than I can handle at the moment. Below is my full code. Any help with cleaning this up would be much appreciated!

import c4d
import c4d.utils
import os
import os.path

blacklist = {"persp","top","side","front"}
FBX_EXPORTER_ID = 1026370
PLUGIN_ID = 1053631

class ExportFBX():
    def __init__(self, doc, flags):
        if doc:
            self.doc = doc.GetClone()
            self.name = os.path.splitext(doc.GetDocumentName())[0]
        self.flags = flags
        self.status = False

    def __del__(self):
        if self.doc:
            c4d.documents.KillDocument(self.doc)

    def _GetAllObjects(self, obj):
        while obj:
            yield obj
            for o in self._GetAllObjects(obj.GetDown()):
                yield o
            obj = obj.GetNext()

    def _UpdateName(self, obj):
        name = obj.GetName()
        if name in blacklist:
            obj.SetName("{}_geo".format(name))

    def _UpdateAxis(self, obj):
        if obj:
            null = c4d.BaseObject(c4d.Onull)
            null.SetName("{}_grp".format(obj.GetName()))
            self.doc.InsertObject(null)
            null.InsertBefore(obj)
            obj.InsertUnder(null)
            return null

    def _UpdateSdsInstance(self, obj):
        sds = obj.GetReferenceObject(self.doc)
        parent = None
        child = sds.GetDown()
        pred = obj.GetPred()
        if not pred:
            parent = obj.GetUp()

        obj.SetReferenceObject(child)

        new_sds = c4d.BaseObject(c4d.Osds)
        if sds.GetName().startswith("Subdivision"):
            new_sds.SetName("{0}_subdivision".format(child.GetName()))
        else:
            new_sds.SetName(sds.GetName())

        new_sds.SetData(sds.GetData())
        self.doc.InsertObject(new_sds, parent, pred, checknames=True)
        obj.InsertUnder(new_sds)


    def Prepare(self):
        if self.doc:
            objs = list(self._GetAllObjects(self.doc.GetFirstObject()))
            instances = [o for o in objs if o.IsInstanceOf(c4d.Oinstance)]
            instance_sources = [i.GetReferenceObject(self.doc) for i in instances]
            sds_list = [o for o in objs if o.IsInstanceOf(c4d.Osds) and o in instance_sources]
            convert_editable = [o for o in objs if o.IsInstanceOf(c4d.Osymmetry) or o.IsInstanceOf(1018544)]
            c4d.StopAllThreads()
            c4d.utils.SendModelingCommand(c4d.MCOMMAND_MAKEEDITABLE, convert_editable, doc=self.doc, flags=c4d.MODELINGCOMMANDFLAGS_CREATEUNDO)
            _ = [self._UpdateName(o) for o in objs]
            _ = [self._UpdateAxis(sds.GetDown()) for sds in sds_list]
            _ = [self._UpdateSdsInstance(i) for sds in sds_list for i in instances if i.GetReferenceObject(self.doc) == sds]

    def Export(self):
        if self.doc:
            self.status = c4d.documents.SaveDocument(self.doc,
                            os.path.join(self.doc.GetDocumentPath(), self.name),
                            self.flags,
                            FBX_EXPORTER_ID)
                            
    def GetStatus(self):
        return self.status


class ExportFBXCommandData(c4d.plugins.CommandData):
    def Execute(self, doc):
        flags = c4d.SAVEDOCUMENTFLAGS_EXPORTDIALOG|c4d.SAVEDOCUMENTFLAGS_DONTADDTORECENTLIST|c4d.SAVEDOCUMENTFLAGS_DIALOGSALLOWED|c4d.SAVEDOCUMENTFLAGS_SAVEAS

        fbx_export = ExportFBX(doc, flags)

        fbx_export.Prepare()
        fbx_export.Export()
        
        return fbx_export.GetStatus()
        

# Execute main()
if __name__=='__main__':
    directory, _ = os.path.split(__file__)
    fn = os.path.join(directory, "res", "export_fbx.tif")

    bmp = c4d.bitmaps.BaseBitmap()
    if bmp is None:
        raise MemoryError("Failed to create a BaseBitmap")

    if bmp.InitWith(fn)[0] != c4d.IMAGERESULT_OK:
        raise MemoryError("Failed to initialize a BaseBitmap")  

    c4d.plugins.RegisterCommandPlugin(id=PLUGIN_ID,
                                        str="Export FBX...",
                                        help="An FBX export with enhanced support for instances.",
                                        info=0,
                                        dat=ExportFBXCommandData(),
                                        icon=bmp)

Just wanted to provide an update here.

I stand corrected, the above plugin creates an FBX that works perfectly when imported into Maya. It appears that Cinema 4D isn't able to understand what's going on in the FBX and I was checking the export by importing into Cinema 4D.

I suppose the lesson here is to follow the actual pipeline when testing ☺

...and the other lesson is that Cinema's FBX export and import are not to be trusted ☠

Hello,

Really sorry about this issue.

Would it be possible to send us a scene and some screenshot maybe to reproduce the behaviour ? That would be really appreciate. I will send that to our devs.

You can use our mailbox [email protected]

Thanks and cheers,
Manuel