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
    • Recent
    • Tags
    • Users
    • Login
    1. Maxon Developers Forum
    2. ferdinand
    3. Posts
    Offline
    • Profile
    • Following 0
    • Followers 15
    • Topics 54
    • Posts 3,126
    • Groups 2

    Posts

    Recent Best Controversial
    • RE: Educational Licenses

      Hey @lasselauch,

      I tried your plugin in 2025.3.1 (Build 2025_3_1_dec42f629989_1872501510) Win and 2025.3.0 (Build 2025_3_0_dd53fd5fe589_1867889325) OSX (closest version I got on my Mac) with a student license and it just loads fine.
      Screenshot 2025-11-10 at 22.37.08.png
      Screenshot 2025-11-10 at 22.25.34.png

      GUI Operations in Boot Phase

      I noticed that you are creating dialogs and doing file operations while Cinema 4D is booting (i.e., likely within the __name__ == '__main__' context guard of your pyp file). That is absolutely not allowed.

      The registration phase is for registering hooks only. Installing, querying web servers, opening fancy dialogs or doing other shenanigans is absolutely not allowed and can very well lead to malfunctions or crashes. For Python plugins this is less severe as for C++ plugins as Python plugins live in their own fake boot bubble and most systems should have been brought up at this point, but you are still poking the bear.

      We had not too long ago a similar case but for license validation. Register your plugin hook(s) in the boot phase and nothing else. When you have some simple serial or other logic as shown in the serial example, you can run it also, everything else must be done later. Then once Cinema 4D has fully booted (c4d. C4DPL_STARTACTIVITY has been emitted) or when the user tries to use your plugin, you can do a complex license check using web servers or install dependencies. You can then force a restart with c4d.RestartMe.This could absolutely be the cause for the issues of the user and it working on most systems does not mean anything.
      Screenshot 2025-11-10 at 22.23.09.png
      Fig. I: Doing GUI and file operations while Cinema 4D is loading: this is really bad!

      Nested Plugin Package

      Your download package creates a nested folder, which sort of sets a trap for less experienced users, as linking the outer folder will cause the plugin not to load (due to the limitation of how Python plugins are discovered compared to C++ plugins). I would recommend a flat structure.
      99f1642b-f860-4cb0-a660-70d3a5709732-image.png

      TLDR: I would bet money on the user having linked the wrong folder.

      Cheers,
      Ferdinand

      edit:

      And just for completeness, here is your plugin loaded from {c4d}/plugins.

      b2380289-0ab9-4e4b-bd5c-c7d232cb919a-image.png

      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: How to get the fully resolved file path of an upcoming rendering?

      Hey @BigRoy,

      Thank you for reaching out to us and your question. We do not allow for topics that are a collection of questions from one user. New questions mandate new topics, unless they are clear follow up questions on an existing topic. The reason for this is so that this forum remains a searchable database.

      Please create new topics on your own in the future. I have forked your topic.

      Your topic is also lacking an actual question. Here I can infer your question, but for more complex subjects I might not be able to. It is important to have a literal question close to the start of your topic. See Support Procedures: How to Ask Questions and the examples below for what makes a good technical question.

      From the context I am assuming here the question is:

      Is there an API function that returns the full physical file path of a rendered file for a given render setting and in particular the Save > Name format?

      34d82dba-bf2f-411a-8489-a8dccc5d9f77-image.png

      The answer is unfortunately that no such function exists. When you need this feature, you have to hard code it yourself.

      Cheers,
      Ferdinand

      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: Educational Licenses

      Hey @lasselauch,

      Thank you for reaching out to us. In general, we do not regulate users of EDU licenses being able to load plugins or not. The screenshot you sent us just tells the user you cannot manually mount the path {c4d}/plugins as it is an auto-mounted plugin search path. With 2025 we added some minor sanity checks for how plugin paths can be added, so that users cannot 'double dip' the same plugin path and with that cause conflicts.

      So, when the user created a path {c4d}/plugins, the plugin should simply load from there, no adding as a plugin path required. You can send me your plugin, and I can see if I can load it in an EDU. It is hard to tell with this little information what is going on and where the fault lies.

      In {c4d}/plugins you usually do not have write permissions, so when your plugin tries to write to its own root path, that could be an issue. Or the user did not check the console properly and your plugin actually writes something to the console or throws an error. It could of course also be that there is a bug in Cinema 4D.

      I would ask the user:

      1 - What is the content of the Plugin Manager (does your plugin show up)?
      2 - Ask for a screenshot of a scrolled down Python console.
      3 - Common mistake are also over-nested plugins. When you have your plugin as:

      foo/
      └── bar/
          └── my_plugin/
              ├── res/
              └── my_plugin.pyp
      

      mounting /foo/bar/ as a plugin search path will load your Python plugin, mounting foo will not.

      Cheers,
      Ferdinand

      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: Best place to do version conversion for scenes and assets

      Hey @ECHekman,

      Thank you for your question. This is a tricky one. When NodeData::Read is emitted, Cinema 4D is currently reading a file and in the process of deserializing your node; so it indeed is not yet 'fully instantiated', as that is the whole point of the method. You are here not meant to manipulate the scene graph (the node is not yet attached), but read and store data on your plugin hook. I.e., you are supposed to do something with your MyMaterialData and not the GeListNode/BaseMaterial which is being passed in as the first argument of Read.

      The common workflow is to read data from the HyperFile which is passed in and store it on your NodeData instance. Passed in is also the important version number (which is called for some odd reason disklevel) of your HyperFile container.

      As always, you are bound to the main thread for scene graph manipulations. So, NodeData::Init is not a good candidate when wanting to poke around in the scene graph, as it can be called off main thread and on dettached dummy nodes. A good approach is to hook into NodeData::Message and listen for MSG_DOCUMENTINFO to catch a document just having been loaded. This is for your specific case of wanting to manipulate nodes which have been deserialized. If you want to manipulate node instantiation, MSG_MENUPREPARE is a god choice. But it is only called for nodes which have been instantiated from direct user interactions such as a menu click. So, MSG_MENUPREPARE is not being emitted for nodes loaded from a file.

      The drill to do what you want to do would be:

      1. [optional] Overwrite NodeData::Read/Write/CopyTo to read, write, and copy custom data, and possibly use/detect different disk levels to catch different versions of a node type being loaded. You would have to match this with incrementing the disk level in your RegisterMaterialPlugin call, so that there are old and new versions of your material type out there.
      2. Hook into the document being loaded, check the scene state, and possible data left behind for you by NodeData::Read and ::Init.
      3. Manipulate the scene graph to your liking.

      Cheers,
      Ferdinand

      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: The value of 'progress' during the use of RenderDocument() is greater than 1.0

      Hey, almost forgot: We have fixed this, an upcoming release will contain the fix 🙂 . We, however, went with a fix removing the offending call instead of fixing the incorrect progress handler method (which was the issue).

      So, in other words: The workaround I showed here won't work in future versions, as the number of progress calls will be halved. Before one got two calls per update when rendering with the preview renderer: One for the correctly working image progress update and one for the buggy generic progress update. We have removed the latter.

      Halving the number of calls is more technically correct, since it made no sense to call the progress hook twice per update but will break any plugins that relied on that incorrect behaviour.

      Cheers,
      Ferdinand

      posted in Bugs
      ferdinandF
      ferdinand
    • RE: How to compute Redshift MoGraph index ratios (RSMGIDRatioColor)?

      Hey Bruce, multiple people in your company have access. When you do not, reach out to us, so that we can set you up.

      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: set GvNode value via python

      Hey,

      so, I actually did get it right, I apparently just did not check properly when I tested the common case of second level IDs of dynamic properties starting off at 1000.

      Cheers,
      Ferdinand

      """Demonstrates how to set values on dynamic input ports of a Condition node in an XPresso tag.
      """
      
      import c4d
      
      from c4d.modules.graphview import GvNodeMaster, GvNode, GvPort, XPressoTag
      from mxutils import CheckType
      
      doc: c4d.documents.BaseDocument  # The currently active document.
      op: c4d.BaseObject | None  # The primary selected object in `doc`. Can be `None`.
      
      
      def main() -> None:
          """Called by Cinema 4D when the script is being executed.
          """
          # Create a new XPresso tag with a Condition node.
          null: c4d.BaseObject = CheckType(c4d.BaseObject(c4d.Onull))
          tag: XPressoTag = CheckType(null.MakeTag(c4d.Texpresso))
          master: GvNodeMaster = CheckType(tag.GetNodeMaster())
          doc.InsertObject(null)
      
          root: GvNode = CheckType(master.GetRoot())
          condNode: GvNode = CheckType(master.CreateNode(root, c4d.ID_OPERATOR_CONDITION, x=200, y=200))
      
          # Count the number of input ports on the Condition node. For our case, a freshly instantiated
          # node we do not really have to do this, since there will always be static "Switch" port and
          # two dynamic input ports. But when we are working on an existing graph, we have to count the
          # ports to know how many dynamic ports there are.
          inputPorts: list[GvPort] = condNode.GetInPorts()
          portCount: int = len(inputPorts)
      
          # There is no predefined symbol for this, the implementation counts just ports, so dynamic ports
          # start at 1001 because the input port GV_CONDITION_SWITCH has the ID 1000.
          idDynamicStart: int = 1001
      
          # Iterate over all dynamic input ports (-1 -> skipping the first static "Switch" port).
          for i in range(portCount - 1): 
              # Define the DescID for the dynamic input port.
              did: c4d.DescID = c4d.DescID(
                  # The first desc level for variadic/dynamic ports is always a sub container. For a 
                  # condition node where the variadic ports can only be inputs and always are of one type,
                  # this is GV_CONDITION_INPUT. Other nodes which can have variadic ports of different 
                  # types (like the Python node) have different sub container IDs for inputs and outputs, 
                  # e.g., IN_REAL for all real inputs, IN_STRING for all string inputs, OUT_LONG for all 
                  # long outputs, etc.
                  c4d.DescLevel(c4d.GV_CONDITION_INPUT, c4d.DTYPE_SUBCONTAINER, condNode.GetType()),
                  # And the second desc level is the dynamic port ID, starting at 1001 for the first dynamic
                  # input port, 1002 for the second dynamic input port, and so on. It unfortunately not very
                  # standardized, where these start.
                  c4d.DescLevel(idDynamicStart + i, c4d.DTYPE_REAL, 0)
              )
              # Set the value of the dynamic input port to some float value.
              result: bool = condNode.SetParameter(did, float((i + 1) * 10), c4d.DESCFLAGS_SET_0)
              if not result:
                  print("Failed to set value on port index:", i)
      
          # Or another common workflow, we want to set a dynamic port by port label (instead of knowing
          # the index in advance).
      
          # Find the first port that starts with "Input".
          targetLabel: str = "Input"
          targetPort: GvPort | None = next((port for port in inputPorts 
                                            if port.GetName(condNode).startswith(targetLabel)), None)
          
          # We found a port with that label.
          if targetPort is not None:
              # Unlike in the Nodes API, we cannot set the value on a port directly, we always have to go
              # through the node and use a DescID to identify the port. So we must find the index of the 
              # port in the input ports list to calculate the dynamic port ID.
              index: int = inputPorts.index(targetPort)
              result: bool = condNode.SetParameter(
                  c4d.DescID(
                      c4d.DescLevel(c4d.GV_CONDITION_INPUT, c4d.DTYPE_SUBCONTAINER, condNode.GetType()),
                      c4d.DescLevel(idDynamicStart + (index - 1), c4d.DTYPE_REAL, 0)
                  ),
                  99.9, # Set the value to 99.9
                  c4d.DESCFLAGS_SET_0
              )
              if not result:
                  print("Failed to set value on port:", targetPort.GetName(condNode))
      
          c4d.EventAdd()
      
      if __name__ == '__main__':
          main()
      
      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: How to compute Redshift MoGraph index ratios (RSMGIDRatioColor)?

      FYI: The RS team just told me that they agree that this is a bug and that they will fix this. Please reach out via our beta forums for details.

      So, the code I showed you has an expiration date.

      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: How to compute Redshift MoGraph index ratios (RSMGIDRatioColor)?

      Hey @BruceC,

      Thank you for your question. I just checked, and what RS calls the index ratio does not really have much to do with indices and is instead based on the uvw coordinate of a particle. This might be a bug, but you would have to talk with the Redshift team for that. Anyway, find below some Python code to emulate computing that 'index ratio'.

      Cheers,
      Ferdinand

      Result

      I emulated your setup:

      dfbeebb4-cce6-4ca1-bf31-76e78d3966c1-image.png

      Code

      import c4d
      import mxutils
      
      doc: c4d.documents.BaseDocument  # The currently active document.
      op: c4d.BaseObject | None  # The primary selected object in `doc`. Can be `None`.
      
      
      def main() -> None:
          """Called by Cinema 4D when the script is being executed.
          """
          data: c4d.modules.mograph.MoData = mxutils.CheckType(c4d.modules.mograph.GeGetMoData(op))
          uvw: list[c4d.Matrix] = data.GetArray(c4d.MODATA_UVW)
          for i in range(data.GetCount()):
              # That is how the 'index ratio' is computed, it is effectively just the shifted x-component
              # of the uvw coordinate of the particle. The color is then just (ratio, ratio, ratio). Not very
              # 'indexy'.
              ratio: float = 1 - uvw[i].x
              print(f"{i}: {ratio}")
      
      if __name__ == '__main__':
          main()
      
      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: Is it possible to hide the Basic Tab in the attribute viewer?

      Hey @ECHekman,

      it depends a bit on what you do in your description, which bases you include, and what you consider NodeData instances in your case. Are you actually talking about plain NodeData hooks or are you talking about derived types such as ObjectData?

      When we look for example into Ocube.res, we can see it includes Obase:

      CONTAINER Ocube
      {
      	NAME Ocube;
      	INCLUDE Obase;  // includes the description base for all objects Obase.res
      

      Which among other things contains the Obaselist group, which is the group you mean.
      e972d7aa-5434-40d2-b219-25703f65c2d8-image.png

      You could either not include the base description for your node type, or use a modified base which for example hides all elements in that group. But I personally would say that none of these approaches are really a good idea, as Cinema 4D is designed with the assumption that each basic scene element (object, tag, etc.) includes their base description.

      Falling to do so, will either make your plugin outright not work, or - when using more 'clever' approaches such as hiding all parameters, or hiding the whole group via GetDDescription or a modified base - severely impact how users can interact with your scene element.

      When you implement a plain NodeData hook, you should not see any extra tabs, as plain node hooks should not include any bases.

      Cheers,
      Ferdinand

      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: set GvNode value via python

      Hey @Dunhou,

      Thank you for reaching out to us. This is probably possible, but I'll have to check with a debugger myself. These are dynamically generated ports which tend to be a pain in the butt in Xpresso, as pretty much every node type handles them slightly differently, and even by looking at our source code you often do not get any wiser. My guess would have been that it is something like c4d.DescID(c4d.DescLevel(c4d.GV_CONDITION_INPUT, c4d.DTYPE_SUBCONTAINER, condNode.GetType()), c4d.DescLevel(0, c4d.DTYPE_REAL, 0)) or the same but other values for the second desc level such as 1000, 2000, etc. instead of 0. GV_CONDITION_INPUT is defined in the description as the input group.

      I will have to attach a debugger to see what is going on, but where I am currently working from, I have no compiled version of Cinema 4D. Will check in the next days from my Mac, where I do have a compiled version. You can check this thread for context, the function PythonNodeLayerShaderAccess goes into some details (a Python node also has a dynamic/variadic number of in- and output ports).

      Cheers,
      Ferdinand

      """My attempt in setting the dynamic ID_OPERATOR_CONDITION operator in ports (does not work). 
      """
      
      import c4d
      
      from c4d.modules.graphview import GvNodeMaster, GvNode, GvPort, XPressoTag
      from mxutils import CheckType
      
      doc: c4d.documents.BaseDocument  # The currently active document.
      op: c4d.BaseObject | None  # The primary selected object in `doc`. Can be `None`.
      
      
      def main() -> None:
          """Called by Cinema 4D when the script is being executed.
          """
          null: c4d.BaseObject = CheckType(c4d.BaseObject(c4d.Onull))
          tag: XPressoTag = CheckType(null.MakeTag(c4d.Texpresso))
          master: GvNodeMaster = CheckType(tag.GetNodeMaster())
          doc.InsertObject(null)
      
          root: GvNode = CheckType(master.GetRoot())
          condNode: GvNode = CheckType(master.CreateNode(root, c4d.ID_OPERATOR_CONDITION, x=200, y=200))
      
          # Set values:
          did_1: c4d.DescID = c4d.DescID(
              c4d.DescLevel(c4d.GV_CONDITION_INPUT, c4d.DTYPE_SUBCONTAINER, condNode.GetType()),
              c4d.DescLevel(0, c4d.DTYPE_REAL, 0)
          )
          did_2: c4d.DescID = c4d.DescID(
              c4d.DescLevel(c4d.GV_CONDITION_INPUT, c4d.DTYPE_SUBCONTAINER, condNode.GetType()),
              c4d.DescLevel(1, c4d.DTYPE_REAL, 0)
          )
      
          a = condNode.SetParameter(did_1, 10.0, c4d.DESCFLAGS_SET_0)
          b = condNode.SetParameter(did_2, 20.0, c4d.DESCFLAGS_SET_0)
          print(a, b)
      
          c4d.EventAdd()
      
      
      if __name__ == '__main__':
          main()
      
      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: Python tag or Python node in Xpresso crash Cinema 4D

      FYI, there has been a verified fix for the Booleans crash, it will be included in an upcoming release of Cinema 4D.

      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: The value of 'progress' during the use of RenderDocument() is greater than 1.0

      I have moved this into bugs.

      posted in Bugs
      ferdinandF
      ferdinand
    • RE: The value of 'progress' during the use of RenderDocument() is greater than 1.0

      Hey @chuanzhen,

      So, first of all, please excuse the delay. And thank you for the exemplary problem description, I almost booked this under "cannot reproduce" but you really hit the nail on the head with the conditions.

      There is currently a bug in how the viewport renderer handles sending data to the progress hook. But this only happens when rendering a document from another frame than its starting frame. I am not yet 100% certain, but I am pretty sure this is a bug in the progress hook itself and not the viewport renderer. So, the bug could also appear in other contexts where a specific function of the progress hook is used. But so far the viewport renderer is the only renderer with which I could reproduce this.

      There is not really anything you can do as a user, because there is a concrete bug. But the viewport renderer calls the progress hook twice each frame: Once with the correct progress, and once with a progress that is shifted by renderFrameRange/documentFrameRange percent. With some internal code knowledge we can simply say by the call index if the given progress is correct or not. But please understand that this is very much a hack.

      I will probably fix this, but I cannot fix this retroactively in 2025. This workaround could be useful for you but it is also a bit risky.

      I have moved this into bugs.

      Cheers,
      Ferdinand

      """Provides a workaround to get correct progress values when rendering a document with the viewport 
      renderer.
      
      This workaround is specific to the case of this thread (viewport renderer + start frame other than
      the first frame). I know what causes the bug, but I am not yet sure if it is the viewport renderer
      which feeds the thing with wrong data, or of the thing itself is buggy.
      
      THIS IS A HACK USE AT YOUR OWN RISK! DO NOT REMOVE THIS WARNING.
      
      See: https://developers.maxon.net/forum/topic/16341
      """
      import c4d
      import mxutils
      
      doc: c4d.documents.BaseDocument
      
      # What basically happens for the preview renderer, is that it calls the progress hook twice for each
      # frame. The second call happens just two lines after the first call. Each first call provides 
      # "correct" data, while the second does not. However, the first call is only made when RDATA_FASTAFX
      # is set to false in the render data (is the case by default). The workaround is then simply to build
      # history data so that we can know if the current call is an even (correct) or odd (incorrect) call.
      PROGRESS_STACK: list[float] = []
      
      def PythonCallBack(progress, progress_type):
          """
          """
          # Just some stuff I used for debugging which only works in 2026 and above.
          # symbol: str = mxutils.g_c4d_symbol_translation_cache.Get(progress_type, "RENDERPROGRESSTYPE_")[0]
          # with open("/Users/f_hoppe/Documents/temp/render_progress.txt", "a") as f:
          #     f.write(f"{symbol}: {progress:.2f}%\n")
      
          PROGRESS_STACK.append(progress)
          if len(PROGRESS_STACK) % 2 == 0:
              # Even call: correct data
              print(f"Render Progress: {progress:.2f}%")
          else:
              # Odd call: incorrect data, ignore, this is the same context as the previous one.
              pass
      
      def main() -> None:
          """
          """
          # c4d.ClearPythonConsole()
      
          rd = doc.GetActiveRenderData()
          rd[c4d.RDATA_FASTAFX] = False # REALLY make sure that RDATA_FASTAFX is disabled
      
          bmp = c4d.bitmaps.MultipassBitmap(int(rd[c4d.RDATA_XRES]), int(rd[c4d.RDATA_YRES]), c4d.COLORMODE_RGB)
          if bmp is None:
              raise RuntimeError("Failed to create the bitmap.")
      
          bmp.AddChannel(True, True)
          if c4d.documents.RenderDocument(doc, rd.GetDataInstance(), bmp, c4d.RENDERFLAGS_EXTERNAL, prog=PythonCallBack,
                                      ) != c4d.RENDERRESULT_OK:
              raise RuntimeError("Failed to render the temporary document.")
      
      if __name__ == "__main__":
          main()
      
      posted in Bugs
      ferdinandF
      ferdinand
    • RE: Python tag or Python node in Xpresso crash Cinema 4D

      Hey @SmetK,

      so I am back, and I have good and bad news 🙂

      The bad news is that I could not reproduce the crash with your scene on my Win machine with 2026_0_0_db12fb68d6ba_2004155263. I today also asked the users in our beta forum, and multiple users could not reproduce it either.

      The "good" news is that without me noticing, in the last days a similar crash report (it terminates into the exact same lines as yours) has been attached to the issue I created for your crash. This crash came from a Korean user on macOS and with this users scene I could reproduce your crash (I just got the same exact stack trace). The Korean user does not have any Python in his/her scene, just some Boolean setup not unsimilar to yours.

      I have grouped everything and added some comments in your ticket, and will in moment briefly talk with the Boolean developer. Python might be here a component in your case (which so far no one go to crash), but there seems to be a base case which happens independently of Python. Which makes sense given how harmless your code is.

      When you want to follow this case, I would recommend reaching out to end user support, feel free to mention the ticket ITEM#623378 [Python] Booleans crash when building output. When there is a fundamental development which impacts API users, I will post it here for all developers to see, but we should not let end user issues bleed into developer issues.

      So, I do not have a super satisfying answer, but we can at least reproduce this, which increases the chances of this being fixed.

      Cheers,
      Ferdinand

      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: The value of 'progress' during the use of RenderDocument() is greater than 1.0

      FYI: I have not forgotten you. Will get to your topic tomorrow, hopefully 🙂

      posted in Bugs
      ferdinandF
      ferdinand
    • RE: Several Asset Browser Issues

      Hey Ben,

      Thank your for your reply. It always pains me a bit to be that upfront when a user has clearly put effort into a problem description. But it is very hard to follow your description and videos when looking at it from a developers standpoint, what to pinpoint and/or fix a bug.

      Please follow our support procedures regarding reporting bugs and crashes. When need a written step by step instruction, scene files (or in your case a zip database), and what you consider wrong. I would recommend to:

      • Prune all stuff that is not directly relevant to the crash or bug. When you create a zip database from one of our built-in assets, you can just share the zip and say "this is toony-123" as zip database to demonstrate my problem.
      • Then either provide reproduction steps (in which case this would be an end user issue) or the code which triggers the issue and describe in text what leads up to your problem and equally important, what you consider wrong about the outcome.

      But I watched your two videos, and read your posting and got a sense of what you are doing. A few points:

      • The toon rig uses some dark magic Python code (event notifications, something you better keep away from), where I would not be too surprised if they break. And just for clarity: The Toon rig is not part of the SDK or owned by the SDK. That does not mean we won't help you, but I cannot fix the toon rig for you, that would have to be done by the animation team.
      • On some level this does also seem to involve exchange topics, i.e., File IO when you import things.
      • The toon rig might need an extra nudge in your script to attach its event handlers or something like that. But I can only evaluate this once I have a file/database to work on and a script to run.

      Cheers,
      Ferdinand

      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: Usage of SplineHelp.InitSplineWith() regarding flags -> returned data

      Hey @mogh,

      Yes, a new topic would be better if we move on to debugging your plugin. But I'll briefly answer here.

      1. That the comb curve flips between 'above' and 'below' is normal and exactly what I talked about before with mathematical vs 'what artists want' tangents. I once spend here some time on explaining this.
      2. The problem with your screens is a bit that you do not explain what you would consider incorrect. I assume that you understand why the flipping happens and are okay with it and are concerned about the jaggedness of your combs? Without debugging your code this is very hard to evaluate.

      C4D can not get more precise than a 0° degree line object ?

      Splines are what is often called smooth geometry, i.e., a parametric curve that is mathematically defined and has infinite precision (you can think of it as infinite points). This is opposed by discrete geometry such as a LineObject which has a finite set of data points. The interpolation settings of a spline are the parameters with which the smooth form ('the spline') is brought into a discrete representation ('the line object') for display and other purposes. So, asking if Cinema 4D could discretize with 0° (or less) does not make too much sense, since discretization with 0° means infinite precision, i.e., the smooth form. For practical purposes, 0.1° should be more than enough. And that is probably already more than order of precision more than you actually need, as humans cannot see that level of detail.

      When you do not want to only control the discretization over the angle of change (a.k.a. curvature), but also over the distance of points, you can also set a maximum length for the segments. For that you must change the spline interpolation from 'Adaptive' to 'Subdivided'. This way, you can ensure that even in straight areas, the segments do not become too long.

      But the general problem is probably that you do not normalize your data. It makes a difference if the last point is 10 units away from the second last or 1000 units. To approximate the mean curvature between two vertices in some discrete geometry, the formula shown below exists; which is a hack compared to more fancy approaches such as this one. It will approximate the mean curvature between two vertices p and q with the normals np and nq:

              (nq - np) * (q - p)
        c_i = -------------------
                   |q - p|
      

      For a mesh vertex you do this with all neighboring vertices and average the result. My guess would be that you are missing this exact normalization step (/|q - p|, i.e., the value divided by the Euclidean distance between the two points) in your comb calculation. Because curvature is inherently a smooth property, the 'neighbor' of a point in a smooth curve/geometry is always exactly 1/infinity units away. So, you need to ensure that the influence of a neighbor vertex in the discrete world is weighted by its distance to the current vertex.

      For splines you would probably have to do this for the left and right tangent of each vertex and then take the mean value. But curvature discretization is like all computational geometry subjects not super trivial, and there might be extra/expert steps required for splines I am not aware of right now.

      Hope this helps,
      Ferdinand

      Here is a script I wrote a long time ago where I used that 'formula' to approximate the mean curvature for a mesh. This is non-public code, so it has no comments except for me pointing out the formula.

      """Computes the mean curvature of the selected mesh and stores it in a vertex map.
      """
      
      import c4d
      
      
      def GetNormal(pid, points, polygons, neighbor):
          """
          """
          connected = neighbor.GetPointPolys(pid)
          vertexNormals = []
      
          for cpoly in [polygons[pid] for pid in connected]:
              if pid == cpoly.a:
                  o, p, q = points[cpoly.d], points[cpoly.a], points[cpoly.b]
              elif pid == cpoly.b:
                  o, p, q = points[cpoly.a], points[cpoly.b], points[cpoly.c]
              elif pid == cpoly.c and cpoly.IsTriangle():
                  o, p, q = points[cpoly.b], points[cpoly.c], points[cpoly.a]
              elif pid == cpoly.c and not cpoly.IsTriangle():
                  o, p, q = points[cpoly.b], points[cpoly.c], points[cpoly.d]
              elif pid == cpoly.d:
                  o, p, q = points[cpoly.c], points[cpoly.d], points[cpoly.a]
              vNormal = ~((o-p) % (q-p))
              vertexNormals.append(vNormal)
          factor = 1./len(vertexNormals)
          return ~(sum(vertexNormals) * factor)
      
      
      def GetCurvature(node):
          """
          """
          neighbor = c4d.utils.Neighbor()
          neighbor.Init(node)
          points = node.GetAllPoints()
          polygons = node.GetAllPolygons()
      
          def getNeighborVertices(pid):
      
              ids = [pid]
              for i in neighbor.GetPointPolys(pid):
                  cpoly = polygons[i]
                  pnts = [cpoly.a, cpoly.b, cpoly.c]
                  if not cpoly.IsTriangle():
                      pnts.append(cpoly.d)
      
                  fi = cpoly.Find(pid)
                  cnt = len(pnts) - 1
      
                  prev = pnts[fi - 1] if fi > 0 else pnts[-1]
                  nxt = pnts[fi + 1] if fi < cnt else pnts[0]
                  if prev not in ids:
                      ids.append(prev)
                  if nxt not in ids:
                      ids.append(nxt)
              for i in ids[1:]:
                  yield i, points[i]
      
          data = []
          for ip, p in enumerate(points):
              np = GetNormal(ip, points, polygons, neighbor)
              temp = []
              for iq, q in getNeighborVertices(ip):
                  nq = GetNormal(iq, points, polygons, neighbor)
                  # This is just a very cheap
                  #
                  #        (nq - np) * (q - p)
                  #  c_i = -------------------
                  #             |q - p|
                  #             
                  temp.append((nq - np) * (q - p) / (q - p).GetLength())
              c = sum(temp) * (1. / len(temp)) * .5 + .5
              data.append(c)
      
          neighbor.Flush()
          return data
      
      def CreateVertexMap(node, data):
          """
          """
          tag = c4d.VariableTag(c4d.Tvertexmap, len(data))
          tag.SetAllHighlevelData(data)
          tag.SetBit(c4d.BIT_ACTIVE)
          node.InsertTag(tag)
          c4d.EventAdd()
      
      def main():
          """
          """
          data = GetCurvature(op)
          CreateVertexMap(op, data)
      
      if __name__ == '__main__':
          main()
      
      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: Usage of SplineHelp.InitSplineWith() regarding flags -> returned data

      Hey @mogh,

      Thank you for reaching out to us. I agree that it is a bit unclear, but the TLDR is that this is just a minor niche feature. I first thought it would be a way to treat a spline as a close splined, with the description a bit weirdly put, but that is not the case. There is only once place where this flag is used, and it is here:

      cf94c215-834c-406e-b398-eda5e49e79ea-image.png

      I.e., it seems to try to continue the curvature of the last segment. How this works in detail and how sensible this is, I cannot really tell you either. From the code we can see here, it seems to try to parallel transport the last tangent. But there is also something else going on, as it not only takes the last point but the point before that into account (i - 2). It then uses the tangent of m2 * (~m1 * m2), which is also super hand-wavy for me. It basically takes the orientation of the last tangent twice but for A * B, makes B relative to the second last tangent (m1). Or a bit incorrect but more readable: 'It takes the orientation of the last point twice, but also undoes the orientation of the second last point.'

      How this is relevant I have absolutely no idea, probably some MoGraph related 'custom logic'. My advice: Leave it as its default and otherwise ignore it.

      As a warning: All the spline helpers implement parallel transport, which can make their output for curvature tasks undesirable (as it messes with what is the mathematical tangent of a spline in favour of what humans would expect. When you are at a loss about what I am talking about, search the forum for parallel transport and my username, the topic has come up multiple times in the past). It might be better to get the LineObject of a spline, i.e., its current discrete form, and do all the math based on its vertices.

      Cheers,
      Ferdinand

      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: Python tag or Python node in Xpresso crash Cinema 4D

      Hey @SmetK,

      Thank you for the added information; that is really helpful! I unfortunately did not find the time to try this again on Windows, but I'll try next week if I can reproduce your crash. But I am a bit doubtful that jumping from my late-late beta version to the customer RC will make any difference. I will post here an update when I found anything out.

      There exists already a ticket for the crash report I uploaded for you, I will add your explanations there. But I have two questions:

      • May I upload your example file into the ticket?
      • May I share your file in our beta community, to see if anyone else can reproduce the crash?

      Cheers,
      Ferdinand

      posted in Cinema 4D SDK
      ferdinandF
      ferdinand