Maxon Developers Maxon Developers
    • Documentation
      • Cinema 4D Python API
      • Cinema 4D C++ API
      • Cineware API
      • ZBrush Python API
      • ZBrush GoZ API
      • Code Examples on Github
    • Forum
    • Downloads
    • Support
      • Support Procedures
      • Registered Developer Program
      • Plugin IDs
      • Contact Us
    • Categories
      • Overview
      • News & Information
      • Cinema 4D SDK Support
      • Cineware SDK Support
      • ZBrush 4D SDK Support
      • Bugs
      • General Talk
    • Unread
    • Recent
    • Tags
    • Users
    • Login

    Issues with the C4D to Unity Import python plugin - missing some types of objects

    Cinema 4D SDK
    python
    2
    13
    2.4k
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • jenandesignJ
      jenandesign
      last edited by m_adam

      Hello, I am trying to figure out why the supplied C4D to Unity importer misses certain types of objects. The plugin itself is essentially just an FBX converter that Unity runs when a c4d file is within one of its Asset directories. It is written in Python and it is very easy for the end user to modify. I fixed SDS object import by adding a few lines of code, seen towards the end, but still cannot seem to get Polygon Reduction objects. Hoping that someone knows something about this, maybe they can shed some light.

      I found the definitions in the C++ documentation, which has been helpful, but I don't see anything that looks indicative of Polygon Reductions
      https://developers.maxon.net/docs/cpp/2023_2/_ffbxexport_8h.html

      """
      Unity C4D To FBX Converter
      Written for Cinema 4D R18.020
      
      """
      import platform
      import c4d
      import sys
      import os
      from c4d import documents, plugins, threading, utils, gui
      
      # Be sure to use a unique ID obtained from www.plugincafe.com
      PLUGIN_ID = 1038605
      FBX_EXPORTER_ID = 1026370
      
      FBXEXPORT_TEXTURES = True
      FBXEXPORT_EMBED_TEXTURES = False
      FBXEXPORT_SAVE_NORMALS = True
      FBXEXPORT_ASCII = False
      
      def win32_utf8_argv():                                                                                               
          """
          Adapted from http://code.activestate.com/recipes/572200-get-sysargv-with-unicode-characters-under-windows/
      
          Uses shell32.GetCommandLineArgvW to get sys.argv as a list of UTF-8
          strings.
      
          Versions 2.5 and older of Python don't support Unicode in sys.argv on
          Windows, with the underlying Windows API instead replacing multi-byte
          characters with '?'.
          """
      
          from ctypes import POINTER, byref, cdll, c_int, windll
          from ctypes.wintypes import LPCWSTR, LPWSTR
      
          GetCommandLineW = cdll.kernel32.GetCommandLineW
          GetCommandLineW.argtypes = []
          GetCommandLineW.restype = LPCWSTR
      
          CommandLineToArgvW = windll.shell32.CommandLineToArgvW
          CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
          CommandLineToArgvW.restype = POINTER(LPWSTR)
      
          cmd = GetCommandLineW()
          argc = c_int(0)
          argv = CommandLineToArgvW(cmd, byref(argc))
          if argc.value > 0:
              # Remove Python executable if present
              if argc.value - len(sys.argv) == 1:
                  start = 1
              else:
                  start = 0
              return [argv[i].encode('utf-8') for i in
                      xrange(start, argc.value)]
          else:
              return []
      
      class ExportThread(threading.C4DThread):
      
          def __init__(self, doc, exportPath):
              self.doc = doc
              self.status = False
              self.exportPath = exportPath
      
          def Main(self):
              # Export document to FBX
              self.status = documents.SaveDocument(self.doc, self.exportPath, c4d.SAVEDOCUMENTFLAGS_DONTADDTORECENTLIST, FBX_EXPORTER_ID)
      
          def GetStatus(self):
              return self.status
      
      class MessageLogger():
      
          def __init__(self, logOutputPath=None):
              self.logString = ""
              if logOutputPath :
                  self.logOutputFile = open(logOutputPath, 'w')
              else :
                  self.logOutputFile = sys.stdout
      
          def appendMessage(self,message) :
              self.logString+= message + "\n"
      
          def writeLog(self, success) :
              if success :
                  self.logOutputFile.write("SUCCESS\nframerate=" + str(documents.GetActiveDocument().GetFps()) + "\n")
              else :
                  self.logOutputFile.write("FAILURE\n" + self.logString)
      
              if self.logOutputFile is not sys.stdout :
                  self.logOutputFile.close()
              return success
      
      # get command line arguments
      def PluginMessage(id, data) :
          if id==c4d.C4DPL_COMMANDLINEARGS:
              if platform.system() == 'Windows':
                  argv = win32_utf8_argv()
              else:
                  argv = sys.argv
      
              if '-UnityC4DtoFBX' in argv :
                  sourcePath = ""
                  destinationPath = ""
                  logOutputPath = ""
                  textureSearchPath = ""
                  # parse command line arguments
                  for idx, arg in enumerate(argv):
                      if (arg == "-src" and len(argv) > idx) :
                          sourcePath = argv[idx+1]
                      if (arg == "-dst" and len(argv) > idx) :
                          destinationPath = argv[idx+1]
                      if (arg == "-out" and len(argv) > idx) :
                          logOutputPath = argv[idx+1]
                      if (arg == "-textureSearchPath" and len(argv) > idx) :
                          textureSearchPath = argv[idx+1]
      
                  logger = MessageLogger(logOutputPath)
                  
                  if not sourcePath or not destinationPath :
                      logger.appendMessage("Invalid command line arguments")
                      return logger.writeLog(False)
                  
                  # C4D automatically opens any file which path is passed as an argument, 
                  # to prevent that, the parameters omit file extensions.
                  sourcePath+=".c4d"
                  destinationPath+=".fbx"
      
                  # load the c4d scene
                  loadedDoc = documents.LoadFile(sourcePath)
                  documents.GetActiveDocument().SetDocumentPath(textureSearchPath)
                  if loadedDoc :    
                      logger.appendMessage("Succesfuly loaded " + sourcePath)
                  else :
                      logger.appendMessage("Couldn't load " + sourcePath)
                      return logger.writeLog(False)
                  # execute the conversion
                  if ExecuteConversion(destinationPath, textureSearchPath, logger):
                      logger.appendMessage("FBX Exported at " + destinationPath)
                      return logger.writeLog(True)
                  else :
                      logger.appendMessage("FBX Plugin failed to export\n")
                      return logger.writeLog(False)
              else :
                  # command line argument not found. Do nothing.
                  return False
      
      def ExecuteConversion(destinationPath, textureSearchPath, logger) :
          plug = plugins.FindPlugin(FBX_EXPORTER_ID, c4d.PLUGINTYPE_SCENESAVER)
          if plug is None:
              logger.appendMessage("FBX Export plugin not found")
              return False
      
          op = {}
          # Retrieve FBX Eporter settings object
          if plug.Message(c4d.MSG_RETRIEVEPRIVATEDATA, op) :
              if "imexporter" not in op:
                  return False
      
              # BaseList2D object stored in "imexporter" key hold the settings
              fbxExport = op["imexporter"]
              if fbxExport is None :
                  return False
      
              # Change export settings
              
              #THE FOLLOWING TWO LINES OF CODE WERE ADDED TO FIX SDS OBJECT IMPORT
              fbxExport[c4d.FBXEXPORT_SDS_SUBDIVISION] = True
              fbxExport[c4d.FBXEXPORT_SDS] = False
              fbxExport[c4d.FBXEXPORT_TEXTURES] = FBXEXPORT_TEXTURES
              fbxExport[c4d.FBXEXPORT_EMBED_TEXTURES] = FBXEXPORT_EMBED_TEXTURES
              fbxExport[c4d.FBXEXPORT_SAVE_NORMALS] = FBXEXPORT_SAVE_NORMALS
              fbxExport[c4d.FBXEXPORT_ASCII] = FBXEXPORT_ASCII
      
              c4d.SetGlobalTexturePath(9, documents.GetActiveDocument().GetDocumentPath())
              if textureSearchPath :
                  c4d.SetGlobalTexturePath(8, textureSearchPath)
      
              thread = ExportThread(documents.GetActiveDocument(), destinationPath)
              thread.Start()       # Start thread
              thread.End()         # Then end it but wait until it finishes
      
              # Retrieve export status and return it
              status = thread.GetStatus()
              return status
          else :
              logger.appendMessage("Could not configure FBX plugin")
              return False
      
          documents.KillDocument(documents.GetActiveDocument())
      
      
      1 Reply Last reply Reply Quote 0
      • M
        m_adam
        last edited by m_adam

        Hi @jenandesign,

        May I ask you which version of Cinema 4D are you using?
        So typically the issue with Polygon Reduction is the same as the one from Python plugin(Python Generator?). Are they the only affected objects types or all generators are affected?

        In any case, FBX format has no knowledge of what is Polygon Reduction/Python Generator so he has to bake it as polygon Object.
        But for doing that it needs a proper cache representation of these generators. And since you Load the file in memory the scene is not yet evaluated (and cache not built).

        So please call BaseDocument.ExecutePasses to build the cache.

        For more information see related topic Cloner objects missing in Commandline FBX export.

        Cheers,
        Maxime.

        MAXON SDK Specialist

        Development Blog, MAXON Registered Developer

        1 Reply Last reply Reply Quote 0
        • jenandesignJ
          jenandesign
          last edited by jenandesign

          Hi Maxime! Thanks for the info about Poly Reduction and ideas with ExecutePasses 😄 I will try that immediately

          I am currently on the latest, R21.115. However I tested this same stuff happening on R20 as well.

          Cloners are usually okay as long as they don't reference a spline (which now makes sense knowing about the cache thing).

          However, I am right now getting a very strange thing where a cloner and a null with dynamics tag (applied to many children) go from importing normally to not.. depending on how many children are in the Fracture.1 null. But perhaps this is a separate issue. When testing with the manual FBX export, it takes some time.. I am assuming it has to animate the dynamics tag to it's current position before exporting... Perhaps unity does not know how to wait for this operation unless it is very fast. Perplexing why it would have anything to do with the cloner, though, as it is not in any way connected to the Fracture.1 null or the dynamics system. Removing one or the other object results in a successful import into unity. The attached scene fails import into unity unless you delete the cloner, the dynamics tag attached to the null, or at least 10-50 more children of the null.fbxtest_v03.c4d. This is different than the original issue I posted about, where the scene import does not fail, but merely comes in with missing objects.

          I will try the ExecutePasses thing now! Thanks again!

          Leah

          1 Reply Last reply Reply Quote 0
          • jenandesignJ
            jenandesign
            last edited by jenandesign

            ExecutePasses worked for Polygon Reduction objects! That is a huge benefit to LOD versioning in Unity 👍 👍 👍

            Added here:

            #
            #SNIPPET FROM MAIN CODE
            #
            
            class ExportThread(threading.C4DThread):
            
                def __init__(self, doc, exportPath):
                    self.doc = doc
                    self.status = False
                    self.exportPath = exportPath
                    #ADDED THE LINE BELOW
                    doc.ExecutePasses(None, True, True, True, c4d.BUILDFLAGS_EXPORT)
            
                def Main(self):
                    # Export document to FBX
                    self.status = documents.SaveDocument(self.doc, self.exportPath, c4d.SAVEDOCUMENTFLAGS_DONTADDTORECENTLIST, FBX_EXPORTER_ID)
            
                def GetStatus(self):
                    return self.status
            
            #
            #SNIPPET FROM MAIN CODE
            #
            

            Cloners are still not working if they reference a spline. Any ideas why that is still giving us trouble?

            Leah

            1 Reply Last reply Reply Quote 0
            • M
              m_adam
              last edited by

              Sometime its needed to call it multiple time. So try to call ExcecutePass multiple time (maximum 3 is enough).

              Cheers,
              Maxime

              MAXON SDK Specialist

              Development Blog, MAXON Registered Developer

              jenandesignJ 1 Reply Last reply Reply Quote 0
              • jenandesignJ
                jenandesign @m_adam
                last edited by jenandesign

                @m_adam Shoot, tried calling it 3X and still nothing on the cloner (spline mode) object.

                (failing) test file attached
                fbxtest_v04.c4d

                1 Reply Last reply Reply Quote 1
                • jenandesignJ
                  jenandesign
                  last edited by

                  Currently the Spline Deformer breaks other objects in the scene, while the Bevel Deformer and Cloner Object in Spline Object mode only break themselves.

                  GetContour? SetOptimizeCache? Anything else worth investigating? Maybe this is happening because it's being called via Unity automatically?

                  1 Reply Last reply Reply Quote 0
                  • M
                    m_adam
                    last edited by

                    If I understand correctly, the issue is that the spline are not saved in the fbx?

                    If yes make sure to set fbxExport[c4d.FBXEXPORT_SPLINES] = True

                    At least here is working correctly, loading the saved fbx give me back the same result than previously.
                    Cheers,
                    Maxime.

                    MAXON SDK Specialist

                    Development Blog, MAXON Registered Developer

                    jenandesignJ 1 Reply Last reply Reply Quote 0
                    • jenandesignJ
                      jenandesign @m_adam
                      last edited by jenandesign

                      @m_adam Then maybe the issue is that the command line fbx export is being executed from within Unity and not through C4D itself?

                      Is there some kind of document referencing error that could happen if another program (other than C4D) were to call this script? Would caches behave differently?

                      To answer your question, yes, I have FBXEXPORT_SPLINES set to True

                      1 Reply Last reply Reply Quote 0
                      • jenandesignJ
                        jenandesign
                        last edited by

                        I forgot to mention, Instances also fail to cache into Unity as well.

                        I am thinking these are all related and to do with the cache being unavailable unless the script is being run within the C4D gui

                        1 Reply Last reply Reply Quote 0
                        • M
                          m_adam
                          last edited by m_adam

                          Hi @jenandesign sorry for the delay and to not have spotted the issue earlier.

                          But SaveDocument needs to be called in the main thread. So one solution for you is to send a CoreMessage with c4d.SendCoreMessage to catch it in a MessageData and process it.

                          import c4d
                          
                          FBX_EXPORTER_ID = 1026370
                          
                          FBXEXPORT_TEXTURES = True
                          FBXEXPORT_EMBED_TEXTURES = False
                          FBXEXPORT_SAVE_NORMALS = True
                          FBXEXPORT_ASCII = False
                          
                          
                          
                          # Main function
                          def main():
                              sourcePath = r"C:\Users\graphos\Downloads\fbxtest_v04.c4d"
                              destinationPath = r"C:\Users\graphos\Documents\test.fbx"
                              
                              plug = c4d.plugins.FindPlugin(FBX_EXPORTER_ID, c4d.PLUGINTYPE_SCENESAVER)
                              if plug is None:
                                  logger.appendMessage("FBX Export plugin not found")
                                  return False
                          
                              op = {}
                              # Retrieve FBX Eporter settings object
                              if plug.Message(c4d.MSG_RETRIEVEPRIVATEDATA, op) :
                                  if "imexporter" not in op:
                                      return False
                          
                                  # BaseList2D object stored in "imexporter" key hold the settings
                                  fbxExport = op["imexporter"]
                                  if fbxExport is None :
                                      return False
                          
                                  # Change export settings
                                  
                                  #THE FOLLOWING TWO LINES OF CODE WERE ADDED TO FIX SDS OBJECT IMPORT
                                  fbxExport[c4d.FBXEXPORT_SDS_SUBDIVISION] = True
                                  fbxExport[c4d.FBXEXPORT_SDS] = False
                                  fbxExport[c4d.FBXEXPORT_TEXTURES] = FBXEXPORT_TEXTURES
                                  fbxExport[c4d.FBXEXPORT_EMBED_TEXTURES] = FBXEXPORT_EMBED_TEXTURES
                                  fbxExport[c4d.FBXEXPORT_SAVE_NORMALS] = FBXEXPORT_SAVE_NORMALS
                                  fbxExport[c4d.FBXEXPORT_ASCII] = FBXEXPORT_ASCII
                          
                                  doc = c4d.documents.LoadDocument(sourcePath, c4d.SCENEFILTER_OBJECTS | c4d.SCENEFILTER_MATERIALS, None)
                                  status = False
                                  doc.ExecutePasses(None, True, True, True, c4d.BUILDFLAGS_EXPORT)
                          
                                  # Export document to FBX
                                  status = c4d.documents.SaveDocument(doc, destinationPath, c4d.SAVEDOCUMENTFLAGS_DONTADDTORECENTLIST, FBX_EXPORTER_ID)
                                  c4d.documents.KillDocument(doc)
                          
                                  # Retrieve export status and return it
                                  print status
                          
                          
                          
                          # Execute main()
                          if __name__=='__main__':
                              main()
                          

                          Cheers,
                          Maxime.

                          MAXON SDK Specialist

                          Development Blog, MAXON Registered Developer

                          1 Reply Last reply Reply Quote 0
                          • jenandesignJ
                            jenandesign
                            last edited by

                            Hi Maxime! Thanks for the suggestion, though I don't quite understand what you mean by your example... I don't see any use of SendCoreMessage in your snippet?

                            SaveDocument is already present in the original code in the extended threading class "ExportThread" under definition Main(). How are you suggesting I change this?

                            Thank you for the continued help ☺

                            1 Reply Last reply Reply Quote 0
                            • M
                              m_adam
                              last edited by

                              SaveDocument is already present in the original code in the extended threading class "ExportThread"

                              TO not make it happens in a thread.
                              So to directly execute it in ExecuteConversion.

                              The use of SendCoreMessage is a way, from a thread (and it's the case for you in ExportThread::Main) to send a message, that Cinema 4D will process on the Main thread (I didn't use in my code because in my case I'm in the main thread).
                              So you can react to this message and do something (in your case SaveDocument). It was just for your information, regarding what's your thread is doing (only calling SaveDocument) I would say that using a thread here make no sense, and you should directly call SaveDocument in your ExecuteConversion function.

                              Hope it's more clear.
                              Cheers,
                              Maxime.

                              MAXON SDK Specialist

                              Development Blog, MAXON Registered Developer

                              1 Reply Last reply Reply Quote 0
                              • First post
                                Last post