Maxon Developers Maxon Developers
    • Documentation
      • Cinema 4D Python API
      • Cinema 4D C++ API
      • Cineware 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
    1. Maxon Developers Forum
    2. m_adam
    3. Posts
    M
    • Profile
    • Following 0
    • Followers 8
    • Topics 3
    • Posts 1,451
    • Best 518
    • Controversial 0
    • Groups 2

    Posts made by m_adam

    • RE: Edge ring selection

      Hi @Tpaxep,

      Welcome to the Maxon developers forum and its community, it is great to have you with us!

      Getting Started

      Before creating your next postings, we would recommend making yourself accustomed with our forum and support procedures. You did not do anything wrong, we point all new users to these rules.

      • Forum Overview: Provides a broad overview of the fundamental structure and rules of this forum, such as the purpose of the different sub-forums or the fact that we will ban users who engage in hate speech or harassment.
      • Support Procedures: Provides a more in detail overview of how we provide technical support for APIs here. This topic will tell you how to ask good questions and limits of our technical support.
      • Forum Features: Provides an overview of the technical features of this forum, such as Markdown markup or file uploads.

      It is strongly recommended to read the first two topics carefully, especially the section Support Procedures: How to Ask Questions.

      About your First Question

      The tool has indeed been updated, but the docs were not.

      Here is how to call it. Note that you have to pass a polygon index, this is mandatory and it needs to be adjacent to one of the vertex to indicate a direction of the ring selection. Here is your script adapted, to be run on a sphere that was made editable.

      import c4d
      from c4d import utils
      
      def select_ring_edge(obj, v1Id, v2Id, polyId):
          bc = c4d.BaseContainer()
          bc.SetData(c4d.MDATA_RING_SEL_STOP_AT_SELECTIONS, False)
          bc.SetData(c4d.MDATA_RING_SEL_STOP_AT_NON_QUADS, False)
          bc.SetData(c4d.MDATA_RING_SEL_STOP_AT_POLES, True)
          bc.SetData(c4d.MDATA_RING_BOTH_SIDES, False)
          bc.SetData(c4d.MDATA_RING_SWAP_SIDES, False)
          
          bc.SetData(c4d.MDATA_RING_FIRST_VERTEX, v1Id)
          bc.SetData(c4d.MDATA_RING_SECOND_VERTEX, v2Id)
          bc.SetData(c4d.MDATA_RING_POLYGON_INDEX, polyId)
          
          
          bc.SetData(c4d.MDATA_RING_SELECTION, c4d.SELECTION_NEW)
      
          result = c4d.utils.SendModelingCommand(
              command=c4d.ID_MODELING_RING_TOOL,
              list=[obj],
              mode=c4d.MODELINGCOMMANDMODE_EDGESELECTION,
              bc=bc,
              doc=c4d.documents.GetActiveDocument(),
              flags=c4d.MODELINGCOMMANDFLAGS_NONE
          )
          c4d.EventAdd()
          return result
      
      def main():
          doc = c4d.documents.GetActiveDocument()
          obj = doc.GetActiveObject()
          if obj is None:
              c4d.gui.MessageDialog("select obj.")
              return
      
          firstVertex = 346
          secondVertex = 347
          polygonIndex = 312
      
          result = select_ring_edge(obj, firstVertex, secondVertex, polygonIndex)
          if result:
              print("DONE")
          else:
              print("ERROR")
      
      if __name__ == '__main__':
          main()
      

      Note that the same settings also apply for the ID_MODELING_LOOP_TOOL, just modify RING by LOOP in the constant so MDATA_RING_SEL_STOP_AT_SELECTIONS become MDATA_LOOP_SEL_STOP_AT_SELECTIONS.

      Cheers,
      Maxime.

      posted in Cinema 4D SDK
      M
      m_adam
    • RE: How to implement an image control that can receive and generate image data drag events

      Did you had a look at geuserarea_drag_r13.py? There is no else statement but that would be the perfect place to do something specific on single click. But there is no built-in solution you have to kind of track all by yourself as done line 294.

      Cheers,
      Maxime.

      posted in Cinema 4D SDK
      M
      m_adam
    • RE: adding assets to a userdatabase in the assetbrower with Python

      Hi @wen I've moved your topic to the bug section since it's indeed a bug, I will ping you on this topic once the fix is available, it should come in one of the next update.
      The issue is that the internal cache is not properly updated and therefor this is failing.

      With that's said there is a ugly workaround which consist of calling it twice so the cache is properly updated.
      Find bellow a version that is going to work in all versions

      import c4d
      import maxon
      import os
      
      
      def CreateRepFromUrl(url: maxon.Url) -> maxon.UpdatableAssetRepositoryRef:
          """Create a new repository from a given database URL.
          
          If there is no valid database at the given URL, it creates a database at the URL.
          It always create a new repository and the associated database asset, even if there
          are existing repositories for that database.
          """
          # Make type checks
          if not isinstance(url, maxon.Url):
              raise TypeError("First argument is not a maxon.Url")
      
          # Create a unique identifier for the repository.
          rid = maxon.AssetInterface.MakeUuid(str(url), True)
      
          # Repositories can be composed out of other repositories which are called bases. In this
          # case no bases are used to construct the repository. But with bases a repository for all
          # user databases could be constructed for example.
          bases = maxon.BaseArray(maxon.AssetRepositoryRef)
      
          # Create a writable and persistent repository for the database URL. If #_dbUrl would point
          # to a location where no database has been yet stored, the necessary data would be created.
          if c4d.GetC4DVersion() < 2025200:
              try:
                  repository = maxon.AssetInterface.CreateRepositoryFromUrl(
                      rid, maxon.AssetRepositoryTypes.AssetDatabase(), bases, url, True, False, False, None)    
              except Exception as e:
                  repository = maxon.AssetInterface.CreateRepositoryFromUrl(
                      rid, maxon.AssetRepositoryTypes.AssetDatabase(), bases, url, True, False, False, None)    
          else:
              try:
                  repository = maxon.AssetInterface.CreateRepositoryFromUrl(
                      rid, maxon.AssetRepositoryTypes.AssetDatabase(), bases, url, True, False, False)    
              except Exception as e:
                  repository = maxon.AssetInterface.CreateRepositoryFromUrl(
                      rid, maxon.AssetRepositoryTypes.AssetDatabase(), bases, url, True, False, False)    
      
          if not repository:
              raise RuntimeError("Repository construction failed.")
      
          return repository
      
      if __name__ == '__main__':
          if not maxon.AssetDataBasesInterface.WaitForDatabaseLoading():
              raise RuntimeError("Could not load asset databases.")
          dbPath = os.path.join(os.path.dirname(os.path.abspath(__file__)), "testdb")
          print(CreateRepFromUrl(maxon.Url(dbPath)))
      

      Cheers,
      Maxime.

      posted in Bugs
      M
      m_adam
    • RE: plugin Loupedeck CT with Redshift

      Hi @shafy sorry for the delay I was away for few times.

      Before we start please read the Support Procedure - About AI-Supported Development. Keep in mind we are not there to develop for you neither integrate or guide you with 3rd party module that you will most likely need in order to control knobs.

      With that's said what you are asking (at least retrieving a parameter and defining it) is possible for Redshift light intensity. It is as easy as drag and dropping the parameter you are interested by into the Cinema 4D Python Console as explained in Python Script Manager Manual - Drag And Drop. Additionally you can also use the Script Log to record a script or change and therefor re-execute it.

      Cheers,
      Maxime.

      posted in Cinema 4D SDK
      M
      m_adam
    • RE: How to expose C++ symbols to Python users as a third party?

      Hi @ECHekman sorry it took me more time than I expected. From my meory octane already have a Python module registered in C++. So I assume you want to add symbols to this python module.

      One important aspect is that the Symbol Parser by itself is completly agnostic of the c4d module therefor you can call it with any Python interpreter. This point is really important because this let you parse your C++ header files whenever you want within your build pipeline.

      With that's said the Symbol Parser is bundled within the mxutils package which needs the c4d and maxon module.
      So in order to have it running with any python intepreter you will need to do the following:

      import sys
      CI_PROJECT_DIR = r"C:\Users\m_adam.INTERN\Documents\MAXON\gitlab\c4d"
      
      # Build path to import the parser_symbol
      maxon_python_path = os.path.join(CI_PROJECT_DIR, "resource", "modules", "python", "libs")
      if not os.path.exists(maxon_python_path):
          raise RuntimeError(f"DEBUG: update_python_symbols - Unable to find {maxon_python_path}")
      
      # Get python311 folder where the symbol parser is located
      mxutils_path, symbol_parser_path = None, None
      for folder in os.listdir(maxon_python_path):
          full_path_folder = os.path.join(maxon_python_path, folder)
          if not os.path.isdir(full_path_folder):
              continue
      
          # We found the mxutils module containing the symbol parser.
          if os.path.exists(os.path.join(full_path_folder, "mxutils", "symbol_parser")):
              mxutils_path = os.path.join(full_path_folder, "mxutils")
              symbol_parser_path = os.path.join(mxutils_path, "symbol_parser")
      
      if None in (mxutils_path, symbol_parser_path):
          raise ImportError(f"Could not find 'symbol_parser' module path in {maxon_python_path}.")
      
      sys.path.append(mxutils_path)
      print(f"DEBUG: Added {mxutils_path} to sys.path.")
      
      # Import the symbol parser and do your stuff.
      import symbol_parser
      # Do something with it
      
      sys.path.remove(mxutils_path)
      

      Then the Symbol Parser paradigm is that you first parse your data and then the parser contain Scope that contains member (a name and a value).
      Therefor once you have parsed your data you can freely output to multiple format if needed. You can also write your own format if you want to.
      In this case because you are using mostly the Cinema API and not the Maxon API I re-use the same output as the one we use for the c4d Python package.
      This output is implemented in c4d\resource\modules\python\libs\python311\mxutils\symbol_parser\output_classic_api.py. Everything in the Symbol Parser is open source so feel free to look at it.

      So to get back to your question find bellow the next code that will parse all the resources files from the Redshift folder and insert them within the "redshift" Python Package. (You usually do not have to do that because Python is parsing automatically the c4d_symbols.h and include such symbols within the c4d Python package this is for the example). With that's said there is basically two ways, one hardcoding the parsed value in your C++ plugin when you register your Python module, the second one using a Python file that will get loaded at the startup of Cinema 4D once your Python module is already loaded and therefor inject symbol into your module.

      import os
      import c4d # Only used to retrieve various C4D paths used in this example, but not really necessary otherwise
      
      # These imports bellow are not bound to the c4d module therefor they can be used with a standard Python3 interpreter
      # But this code is written to be executed in the Script Manager, so you may need to adapt the import statement according to how you run this script.
      from mxutils.symbol_parser.extractor import SymbolParser
      from mxutils.symbol_parser.output_classic_api import _get_symbol_dict_from_parser
      
      
      def ParseRedshiftResources() -> SymbolParser:
          # Parse all Redshift ressource files and resolve their values
          rsDir = os.path.join(os.path.dirname(c4d.storage.GeGetStartupApplication()), "Redshift", "res", "description")
          parser = SymbolParser(rsDir,
                                  parse_mx_attribute=False,
                                  parse_define=True,
                                  parse_enum=True,
                                  parse_static_const=True)
      
          # Parse and resolve. Resolve means:
          #  - Transfrom values that depends to others to their actual integer or float values.
          #  - Transform complex values (such as bit mask, arithmetic, etc) into their actual integer or float values.
          # For more information about what is supported take a look at
          # https://developers.maxon.net/docs/py/2025_2_0/manuals/manual_py_symbols.html?highlight=symbol%20parser#symbols-parser-features
          parser.parse_all_files(resolve=True)
          return parser
      
      
      def OutputPythonFile(parser: SymbolParser):
          def GeneratePythonFileToBeInjected(parser: SymbolParser) -> str:
              """Generate a Python file that will inject all parsed symbols into the 'redshift' Python package when this file get imported.
              This function needs to be called once, most likely during your build pipeline.
              Once you have generated this Python file you should bundle it with your installer and then install it in the targeted Cinema 4D installation.
              See PatchCurrentC4DToInjectParsedSymbol for an example of such deployment
              """
              import tempfile
      
              tempPath = None
              # Retrieve a sorted dict containing the symbol name as key and it's value as values
              # This will flatten all scopes so an enums called SOMETHING with a value FOO in it will result in a symbol named SOMETHING_FOO
              # If you do not want this behavior feel free to create your own solution, this function is public and declared in
              # c4d\resource\modules\Python\libs\python311\mxutils\symbol_parser\output_classic_api.py
              symbolTable = _get_symbol_dict_from_parser(parser)
              
              with tempfile.NamedTemporaryFile('w', delete=False) as tmpFile:
                  tempPath = tmpFile.name
                  
                  tmpFile.write("import redshift\n")
                  
                  for name, value in symbolTable.items():
                      tmpFile.write(f"redshift.{name} = {value}\n")
                  
              
              if not os.path.exists(tempPath):
                  raise RunTimeError('Failed to create the Python File to Inject Symbols in "redshift" Python Package') 
              
              return tempPath
      
          def PatchCurrentC4DToInjectParsedSymbol(fileToBeLoaded: str):
              """This function is going to create a Python file that is going to be called at each startup of Cinema 4D to inject symbols into the "redshift" Python package.
              This is done by placing the previous file in a place where it can be imported by the C4D Python VM.
              And by importing this file by editing the python_init.py file located in the preferences.
              For more information about it please read 
              https://developers.maxon.net/docs/py/2025_2_0/manuals/manual_py_libraries.html?highlight=librarie#executing-code-with-Python-init-py
              """
              import sys
              import shutil
              
              # Pref folder that contains a python_init.py that is laoded once c4d and maxon package is loaded and a libs folder that is part of the sys.path of the c4d Python VM.
              pyTempDirPath = os.path.join(c4d.storage.GeGetStartupWritePath(), f"Python{sys.version_info.major}{sys.version_info.minor}")
          
              # Path to our module that will inject the parsed symbols in the "redshift" Python package
              pyTempRsDirPath = os.path.join(pyTempDirPath, "libs", "rs_symbols")
              # Path to the __init__ file of our module that will be called when we import our module
              # we need to place the file we generated previously in this location.
              pyTempRsFilePath = os.path.join(pyTempDirPath, "libs", "rs_symbols", "__init__.py")
              if not os.path.exists(pyTempRsDirPath):
                  os.mkdir(pyTempRsDirPath)
                  
              if os.path.exists(pyTempRsFilePath):
                  os.remove(pyTempRsFilePath)
                  
              shutil.copy2(fileToBeLoaded, pyTempRsFilePath)
              
              # Now that we have our module that will inject symbols within the "redshift" Python package. We need to get this "rs_symbols" module be called at each startup.
              # So we use the python_init.py file to get loaded once all plugins are loaded. Therefor the Redshift plugin is already loaded and have already setup its "redshift" Python package.
              # We will inject a line in this file to load our "rs_symbols" module
              pyTempInitFilePath = os.path.join(pyTempDirPath, "python_init.py")
              
              isImportRsSymbolPresent = False
              initFileExist = os.path.exists(pyTempInitFilePath)
              # Because this file me be already present and already patched or contain other content
              # We should first check if we need to add our import or not
              if initFileExist:
                  with open(pyTempInitFilePath, 'r') as f:
                      isImportRsSymbolPresent = "import rs_symbols" in f.read()
              
              # prepend our import statement to the file
              if not isImportRsSymbolPresent:
                  with open(pyTempInitFilePath, "w+") as f:
                      content = f.read()
                      f.seek(0, 0)
                      f.write('import rs_symbols\n' + content)
      
          pythonFileGeneratedPath = GeneratePythonFileToBeInjected(parser)
          PatchCurrentC4DToInjectParsedSymbol(pythonFileGeneratedPath)
      
      
      def OutputCppFile(parser):
          # Retrieve a sorted dict containing the symbol name as key and it's value as values
          # This will flatten all scope so an enums called SOMETHING with a value FOO in it will result in a symbol named SOMETHING_FOO
          # If you do not want this behavior feel free to create your own solution, this function is public and declared in
          # c4d\resource\modules\Python\libs\python311\mxutils\symbol_parser\output_classic_api.py
          symbolTable = _get_symbol_dict_from_parser(parser)
          
          contentToPaste =  'auto mod_rs = lib.CPyImport_ImportModule("redshift");\n'
          contentToPaste += 'auto modDict = lib.CPyModule_GetDict(mod_rs);\n'
              
          for name, value in symbolTable.items():
              contentToPaste += f'\nauto name = lib.CPyUnicode_FromString("{name}");\n'
              contentToPaste += 'if (!name)\n'
              contentToPaste += '  CPyErr_Print();\n'
              contentToPaste += f'maxon::py::CPyRef value = lib.CPyLong_FromInt32({value});\n'
              contentToPaste += 'if (!value)\n'
              contentToPaste += '  CPyErr_Print();\n'
              contentToPaste += 'if (!lib.CPyDict_SetItem(modDict, name, value))\n'
              contentToPaste += '  CPyErr_Print();\n'
                      
          return contentToPaste
      
      
      def main() -> None:
          # Parse the Redshift resources and return the parser that hold the parsed values
          parser = ParseRedshiftResources()
      
          # First way: Generate a file that patch the current C4D with a Python file that you need to deploy on the installer. You may prefer this option since you can update this file without having to recompile your plugin.
          OutputPythonFile(parser)
      
          # Second way: Output pseudo C++ code that can be pasted within your C++ plugin after you initialize your custom Python module. You may want to call this whole script before you compile in your build pipeline and put the "cppContent" within a file that will get compiled automatically
          cppContent = OutputCppFile(parser)
      
      if __name__ == '__main__':
          main()
      

      So the system is really modular and being able to run on a regular Python interpreter let you integrate it within your build pipeline to suits your needs.
      If you have any questions, please let me know.
      Cheers,
      Maxime.

      posted in General Talk
      M
      m_adam
    • RE: Move the plane in random effect

      Hi @Fabio-B

      Thank you for reaching out to us. Unfortunately, your question is off topic for the forum you have posted in. Please check the Forum Overview for an overview of the scope of the different forums.

      On this forum we only provide support for third party software developers about the Cinema 4D Software Development Kit. We unfortunately cannot provide end user support, as we lack the tools and the knowledge to do so.

      To receive end-user support, please visit our Support Center and create a ticket for your issue

      Cheers,
      Maxime.

      posted in Cinema 4D SDK
      M
      m_adam
    • RE: C4D Python: Redshift IPR Start/Stop Control

      @hoganXYZ hi if it does not appear in the script log then there is no clear rules. Sometimes there is another way sometimes now it depends on how it is implemented. For the one you asked I do not think it is possible. Best bet would be to ask Redshift on their forum.

      Cheers,
      Maxime.

      posted in Cinema 4D SDK
      M
      m_adam
    • RE: How to expose C++ symbols to Python users as a third party?

      Hi just to let you know I will answers you next week Monday.

      Cheers,
      Maxime.

      posted in General Talk
      M
      m_adam
    • RE: lhit from BaseVolumeData

      Hi sadly there is not much you can do with it in Python. The only use case is if your want to create a kind of hashmap on the pointer, but I do not see a big use case of that to be honest. I will see if I can improve thing a bit here, because in C++ via the lhit you can retrieve the polygon and the underlying object being hit, which is important information but this is very low on my priority list. So in general regarding shader I would say you better at look at C++ since in any case doing a Shader in Python will most likely be a performance killer.

      Cheers,
      Maxime.

      posted in Cinema 4D SDK
      M
      m_adam
    • RE: C4D Python: Redshift IPR Start/Stop Control

      Hi @itstanthony you can use c4d.CallCommand(1040239) to toggle the start/stop of the Redshift IPR.
      You can know such command ID by using the Script Log and doing the action you want to.

      Then to check if the command is active you can use c4d.IsCommandChecked(1040239).

      Cheers,
      Maxime.

      posted in Cinema 4D SDK
      M
      m_adam
    • RE: Set RenderData framerate causing C4D to crash

      Hi I was able to reproduce the issue and fix it for the next version.
      The bug is that setting RDATA_FRAMERATE require the RenderData to be part of a document in case the "Use Project Frame Rate" is enabled on the render settings. If this is enabled and this is enabled by default the value you enter will be disregarded. "Use Project Frame Rate" is a new setting added in 2025.2.

      I will ping you once the fix is available.
      Cheers,
      Maxime.

      posted in Bugs
      M
      m_adam
    • RE: Automating ColorSpace Change for Textures in Redshift

      Hi sorry I fixed my code.

      Cheers,
      Maxime.

      posted in Cinema 4D SDK
      M
      m_adam
    • RE: KDTree or OcTree with C4D Python

      Hey @gheyret this is on my bucket list of things I want to port it since a long time, but to be honest it's rather low-priority.

      But I will keep it in mind.With that's said depending on what you want to do you may find ViewportSelect Interesting.

      Cheers,
      Maxime.

      posted in General Talk
      M
      m_adam
    • RE: Tree view boolean column bug

      Hi @Gregor-M please make sure that your script is executable before sending it to us. Since in it's current state it does raise

      Traceback (most recent call last):
        File "scriptmanager", line 53, in GetDown
        File "scriptmanager", line 33, in GetChildren
      AttributeError: 'Entity' object has no attribute 'children'. Did you mean: 'GetChildren'?
      

      I added self.children = [] within the __init__ of Entity and it fixed the issue but this indicate that you are working on a different version than us. But even with it I'm not able to reproduce any issue and everything is working as expected.

      With that's said your else statement within the SetCheck looks very suspicious and I won't be surprised that the behavior you are experiencing is coming from this else statement.

      Finally note that you can call the function TreeViewCustomGui.SetRoot to define a root. Then the root argument that is available in all functions will be pointing to it.

      Cheers,
      Maxime.

      posted in Cinema 4D SDK
      M
      m_adam
    • RE: Automating ColorSpace Change for Textures in Redshift

      Welcome to the Maxon developers forum and its community, it is great to have you with us!

      Getting Started

      Before creating your next postings, we would recommend making yourself accustomed with our forum and support procedures. You did not do anything wrong, we point all new users to these rules.

      • Forum Overview: Provides a broad overview of the fundamental structure and rules of this forum, such as the purpose of the different sub-forums or the fact that we will ban users who engage in hate speech or harassment.
      • Support Procedures: Provides a more in detail overview of how we provide technical support for APIs here. This topic will tell you how to ask good questions and limits of our technical support.
      • Forum Features: Provides an overview of the technical features of this forum, such as Markdown markup or file uploads.

      It is strongly recommended to read the first two topics carefully, especially the section Support Procedures: Asking Questions.

      About your First Question

      With that's said we are not a coding service, but we offer help to people to learn how to use the Cinema 4D SDK. So in the future please show us that you tried a bit at least and where are you blocked so we can help you. Let's cut your question in two.

      First one, "How to change the ColorSpace" of all materials. You can find an answers in this topic: SetPortValue for Texture node ColorSpace. However this affect only the new Redshift Node Material. If you want to support the old one (xpresso based node material) let me know. You can retrieve all materials using BaseDocument.GetMaterials.

      Then second one, "How to restore value", there is no magic here you need to store the value somewhere. It depends on the behavior you are expecting, if you want to support this ability even if you close and re-open the file. The best way would be store this data in the BaseContainer of the material you modified. A BaseContainer is like a Python Dictionary but it's a Cinema 4D that is attached to any objects/tags/material in Cinema 4D. Any information written into it is then saved in the c4d file. The drawback is that not all datatype are supported.

      So with that's said here a possible example on how to achieve what you want:

      """
      This script automates the process of changing the ColorSpace of all textures in a Cinema 4D project, specifically targeting new Redshift Node materials. 
      
      It performs the following tasks:
      
      1. Iterates through all materials in the active document and identifies Redshift materials.
      2. For each Redshift material, retrieves the current ColorSpace values of texture nodes (e.g., RAW, sRGB).
      3. Changes the ColorSpace of the texture nodes to a predefined target (TARGET_COLOR_SPACE, default "ACEScg").
      4. Serializes and stores the original ColorSpace values in a `BaseContainer` attached to the material, ensuring persistence across sessions.
      5. If needed, restores the original ColorSpace values if they were previously saved, allowing for easy reversion.
      6. The process is fully automated with a single execution, and the changes are saved to the material for future use.
      """
      import c4d
      import maxon
      
      doc: c4d.documents.BaseDocument  # The currently active document.
      op: c4d.BaseObject | None  # The primary selected object in `doc`. Can be `None`.
      
      # Be sure to use a unique ID obtained from https://developers.maxon.net/forum/pid
      UNIQUE_BC_ID = 1000000
      NODESIDS_BC_ID = 0
      VALUES_BC_ID = 1
      TARGET_COLOR_SPACE = "ACEScg"
      
      def GetDictFromMat(mat: c4d.Material) -> dict:
          """
          Retrieves a dictionary containing the current ColorSpace values of a material.
          The ColorSpace values are serialized into a `BaseContainer` and stored on the material,
          ensuring that the data persists even after the file is closed and reopened. This is done
          because node IDs are unique for each material graph, so the serialized data is specific 
          to the material and its associated graph.
      
          Args:
              mat (c4d.Material): The material object whose ColorSpace values are to be retrieved.
      
          Returns:
              dict: A dictionary where keys are node IDs (unique to the material's graph) and values 
                    are the corresponding ColorSpace values.
                    The dictionary is constructed from data stored in the material's `BaseContainer`, 
                    which persists across sessions.
          """
          matBC: c4d.BaseContainer = mat.GetDataInstance()
      
          personalBC: c4d.BaseContainer = matBC.GetContainerInstance(UNIQUE_BC_ID)
          if personalBC is None:
              return {}
      
          nodeIdsBC: c4d.BaseContainer = personalBC.GetContainerInstance(NODESIDS_BC_ID)
          nodeValuesBC: c4d.BaseContainer = personalBC.GetContainerInstance(VALUES_BC_ID)
          if nodeIdsBC is None or nodeValuesBC is None:
              return {}
      
          dct: dict = {}
          for index, _ in enumerate(nodeIdsBC):
              nodeId = nodeIdsBC[index]
              colorSpaceValue = nodeValuesBC[index]
              dct[nodeId] = colorSpaceValue
      
          return dct
      
      def SaveDictToMat(mat: c4d.Material, dct: dict) -> None:
          """
          Saves the ColorSpace dictionary back to the material, ensuring that the data is serialized 
          into a `BaseContainer` and stored on the material. This serialized data will persist across
          sessions, so the original ColorSpace values can be restored even after the file is closed and reopened.
          Since node IDs are unique for each material's graph, the data is saved in a way that is specific 
          to that material and its texture nodes.
      
          Args:
              mat (c4d.Material): The material object to which the dictionary will be saved.
              dct (dict): A dictionary where keys are node IDs (unique to the material's graph) and values are 
                          the corresponding ColorSpace values. This dictionary will be serialized and saved 
                          in the material's `BaseContainer`.
          """
          matBC: c4d.BaseContainer = mat.GetDataInstance()
      
          # Create containers for node IDs and their corresponding ColorSpace values
          nodeIdsBC = c4d.BaseContainer()
          nodeValuesBC = c4d.BaseContainer()
      
          # Populate the containers with the node IDs and ColorSpace values from the dictionary
          for index, key in enumerate(dct.keys()):
              nodeIdsBC[index] = key
              nodeValuesBC[index] = dct[key]
      
          # Create a personal container to hold the node IDs and values
          personalBC = c4d.BaseContainer()
          personalBC[NODESIDS_BC_ID] = nodeIdsBC
          personalBC[VALUES_BC_ID] = nodeValuesBC
      
          # Attach the personal container to the material's data container
          matBC[UNIQUE_BC_ID] = personalBC
      
          # Set the material's data with the updated container, ensuring the ColorSpace values are stored
          mat.SetData(matBC)
      
      
      def main() -> None:
          """
          Main function to automate changing the ColorSpace of all textures in the project.
          It changes the ColorSpace to TARGET_COLOR_SPACE and stores the previous values in the BaseContainer
          of the material, a kind of dictionary but in Cinema 4D format so it persists across sessions.
          This allows for reverting the ColorSpace values back to their original state, even after closing and reopening the file.
          """
          # Loop through all materials in the document
          for material in doc.GetMaterials():
              # Ensure the material has a Redshift node material
              nodeMaterial: c4d.NodeMaterial = material.GetNodeMaterialReference()
              if not nodeMaterial.HasSpace(maxon.NodeSpaceIdentifiers.RedshiftMaterial):
                  continue
      
              # Retrieve the Redshift material graph
              graph: maxon.NodesGraphModelRef = maxon.GraphDescription.GetGraph(
                  material, maxon.NodeSpaceIdentifiers.RedshiftMaterial)
      
              # Retrieve the existing ColorSpace values as a dictionary from the material
              matDict: dict = GetDictFromMat(material)
      
              # Begin a graph transaction to modify nodes
              with graph.BeginTransaction() as transaction:
                  # Define the asset ID for texture nodes in the Redshift material
                  rsTextureAssetId = "com.redshift3d.redshift4c4d.nodes.core.texturesampler"
      
                  # Loop through all the texture nodes in the material graph
                  for rsTextureNode in maxon.GraphModelHelper.FindNodesByAssetId(graph, rsTextureAssetId, True):
                      # Find the ColorSpace input port for each texture node
                      colorSpacePort: maxon.GraphNode = rsTextureNode.GetInputs().FindChild(
                          "com.redshift3d.redshift4c4d.nodes.core.texturesampler.tex0").FindChild("colorspace")
                      
                      # Skip the node if the ColorSpace port is invalid
                      if not colorSpacePort or not colorSpacePort.IsValid():
                          continue
      
                      # Get the path which is unique for each graph for this ColorSpace port
                      colorSpacePortId = str(colorSpacePort.GetPath())
      
                      # If there's an existing ColorSpace value, restore it to the previous state
                      if colorSpacePortId in matDict:
                          colorSpacePort.SetPortValue(matDict[colorSpacePortId])
      
                           # Remove the entry from the dictionary after restoring,
                           # so next time the TARGET_COLOR_SPACE will be used
                          del matDict[colorSpacePortId] 
                      else:
                          # Otherwise, store the current ColorSpace value and set the new target ColorSpace
                          previousValue = colorSpacePort.GetPortValue()
                          if previousValue.IsNullValue():
                              previousValue = ""
      
                          matDict[colorSpacePortId] = str(previousValue)
                          colorSpacePort.SetPortValue(TARGET_COLOR_SPACE)
      
                  # Commit the transaction to apply the changes to the material
                  transaction.Commit()
      
              # After modifying the ColorSpace values, save the updated values back to the material
              SaveDictToMat(material, matDict)
      
              # Refresh the scene to ensure the changes are applied
              c4d.EventAdd()
      
      # Run the main function when the script is executed
      if __name__ == '__main__':
          main()
      

      Cheers,
      Maxime.

      posted in Cinema 4D SDK
      M
      m_adam
    • RE: Automating ColorSpace Change for Textures in Redshift

      Hello @itstanthony,

      I will answer here soon, I currently have a lot on my desk.

      Thank you for your understanding,
      Maxime.

      posted in Cinema 4D SDK
      M
      m_adam
    • RE: Python Tag: Detecting Edge Selection Changes

      Hey @ymoon addition and deletion are indeed reported with the MSG_POLYGON_CHANGED but this can be used also for other events like modification of the topology itself so it's not 100% reliable that you will have only edge selection information.
      The best way for you will be to store and detect the change yourself based on the dirty count of the BaseSelect the tag is storing. Find bellow a code example that you can copy-paste in a Python Tag that will report you modifications to all edge selections and also the "active one"

      import c4d
      
      # Dictionary to store tags and their dirty states
      tagsHash = {}
      # Variable to track the dirty state of active edge selection
      activeEdgeSelectionDirty = 0
      
      def GetEdgeSelectionDirty(tag):
          """
          Retrieves the dirty state of an edge selection tag.
      
          Parameters:
          tag (c4d.SelectionTag): The selection tag to check.
      
          Returns:
          int: The dirty state of the selection tag.
      
          Raises:
          TypeError: If the provided tag is not a selection tag.
          """
          if not isinstance(tag, c4d.SelectionTag):
              raise TypeError("Expected a selection tag")
      
          # Get the BaseSelect object from the selection tag
          bs = tag.GetBaseSelect()
          # Return the dirty state of the BaseSelect
          return bs.GetDirty()
      
      def main():
          """
          Main function to track and print changes in edge selection tags.
      
          This function checks all edge selection tags of the current object,
          compares their dirty states with stored values, and updates the state
          if there are changes. It also handles removed tags and prints changes
          in the active edge selection.
          """
          # List of tags that we did not visited in the current execution
          # We will use this list to determine which tags have been deleted.
          notVisitedTags = list(tagsHash.keys())
      
          # Get the object associated with the current operation
          obj = op.GetObject()
      
          # Iterate through all tags of the object
          for tag in obj.GetTags():
              if not tag.CheckType(c4d.Tedgeselection):
                  continue
      
              tagHash = hash(tag)  # Get a unique identifier for the tag
              if tagHash not in tagsHash:
                  # If the tag is new, store its dirty state
                  tagsHash[tagHash] = GetEdgeSelectionDirty(tag)
                  print(f"New Edge Selection Tag: {tag.GetName()}")
      
              else:
                  # If the tag has been seen before, remove it from not visited list
                  notVisitedTags.remove(tagHash)
      
                  # Check if the dirty state has changed
                  newDirty = GetEdgeSelectionDirty(tag)
                  if tagsHash[tagHash] != newDirty:
                      tagsHash[tagHash] = newDirty
                      print(f"Edge Selection Changed: {tag.GetName()}")
      
          # Process tags that are not visited anymore
          for removedTagHash in notVisitedTags:
              print(f"Edge Selection with the hash: {removedTagHash} removed")
      
          global activeEdgeSelectionDirty
          # Check if the active edge selection dirty state has changed
          newActiveEdgeSelectionDirty = obj.GetEdgeS().GetDirty()
          if activeEdgeSelectionDirty != newActiveEdgeSelectionDirty:
              activeEdgeSelectionDirty = newActiveEdgeSelectionDirty
              print("Active Edge selection changed")
      

      Cheers,
      Maxime.

      posted in Cinema 4D SDK
      M
      m_adam
    • RE: Read Alembic tag's data

      Hey @BruceC in order to get the real tag data under the alembic tag you need to send MSG_GETREALTAGDATA message to it.

      BaseTag* alembicTag = obj->GetTag(Talembic);
      if (!alembicTab)
        return;
      
      GetRealTagData realTag;
      alembicTag->Message(MSG_GETREALTAGDATA, &realTag);
      if (realTag.res && realTag.res.GetType() == Tvertexcolor)
      {
        VertexColorTag* const vct = static_cast<VertexColorTag*>(realTag.res);
        // Do something with it
      }
      

      Cheers,
      Maxime.

      posted in Cinema 4D SDK
      M
      m_adam
    • RE: create link parameter accept Alembic tag

      Hey @BruceC, sadly its a bug in our own parser, so you will have to cope with the integer value for now.

      Regarding your second question on how to limit only to VertexColorTag for Alembic, there is no way to do that via the res file. But you can react in your Message method to the MSG_DESCRIPTION_CHECKDRAGANDDROP Message. Then check if the passed object is an alembic tag and if its a vertex map or not. You cna find a python example (although you are in C++, just so that you have an idea of how it works in this topic)

      Cheers,
      Maxime.

      posted in Cinema 4D SDK
      M
      m_adam
    • RE: Volume Builder Type Fog Should it be solid?

      Hey @d_schmidt yes this is expected, openvdb is sparse with 4 level tile options. Since our volume tool use openVDB we inherit from that.

      In the context of OpenVDB and similar data structures, "sparse" refers to a way of efficiently representing and storing data in which only the non-empty or non-default parts are explicitly kept in memory. This is in contrast to dense representations, where every element is stored regardless of whether it holds meaningful information or not.
      In a sparse representation like OpenVDB, the data structure is designed to allocate memory only for the parts of the voxel grid that contain relevant data (i.e., active voxels), while ignoring the regions that are empty or hold similar values. This allows for significant memory savings and performance improvements, especially when dealing with large grids where most of the space is empty/similar.

      So with that's said the way to retrieve what you expected is with the next code. Note that I've used Multi-Instance for speed reason and not polluting too much the viewport.

      constexpr inline Int ConvertVoxelLevelToAmount(TREEVOXELLEVEL level)
      {
      	switch (level)
      	{
      		case TREEVOXELLEVEL::ROOT:
      		{
      			return 32;
      		}
      		case TREEVOXELLEVEL::TWO:
      		{
      			return 16;
      		}
      		case TREEVOXELLEVEL::THREE:
      		{
      			return 8;
      		}
      		case TREEVOXELLEVEL::LEAF:
      		{
      			return 1;
      		}
      	}
      	static_assert(true, "Unsupported voxel depth");
      	return 0;
      }
      
      class Command : public CommandData
      {
      public:
      
        virtual Bool Execute(BaseDocument* doc, GeDialog* parentManager)
        {
          iferr_scope_handler
          {
            return true;
          };
      
          BaseObject* object = doc->GetFirstObject();
          if (!object)
            return true;
      
          if (!object->IsInstanceOf(Ovolumebuilder))
            return true;
      
          VolumeBuilder* const volumeBuilder = static_cast<VolumeBuilder*>(object);
      
          // get cache
          BaseObject* const cache = volumeBuilder->GetCache();
          if (cache == nullptr)
            return true;
      
          // check for volume object
          if (!cache->IsInstanceOf(Ovolume))
            return true;
      
          const VolumeObject* const volumeObject = static_cast<VolumeObject*>(cache);
          maxon::Volume volume = volumeObject->GetVolume();
          maxon::GridIteratorRef<maxon::Float32, maxon::ITERATORTYPE::ON> iterator = maxon::GridIteratorRef<maxon::Float32, maxon::ITERATORTYPE::ON>::Create() iferr_return;
          iterator.Init(volume) iferr_return;
          maxon::Matrix transform = volume.GetGridTransform();
      
          BaseObject* sphere = BaseObject::Alloc(Osphere);
          BaseContainer* con = sphere->GetDataInstance();
          con->SetFloat(PRIM_SPHERE_RAD, 5);
      
          doc->InsertObject(sphere, nullptr, nullptr);
      
          // create instance object
          InstanceObject* const instance = InstanceObject::Alloc();
          if (instance == nullptr)
            return true;
      
          doc->InsertObject(instance, nullptr, nullptr);
          instance->SetReferenceObject(sphere) iferr_return;
      
          if (!instance->SetParameter(ConstDescID(DescLevel(INSTANCEOBJECT_RENDERINSTANCE_MODE)), INSTANCEOBJECT_RENDERINSTANCE_MODE_MULTIINSTANCE, DESCFLAGS_SET::NONE))
            return true;
      
          maxon::BaseArray<Matrix> matrices;
      
          for (; iterator.IsNotAtEnd(); iterator.StepNext())
          {
            if (iterator.GetValue() < 0.0)
              continue;
      
            const Int voxelAmount = ConvertVoxelLevelToAmount(iterator.GetVoxelLevel());
            for (Int32 voxelIndexX = 0; voxelIndexX < voxelAmount; voxelIndexX++)
            {
              for (Int32 voxelIndexY = 0; voxelIndexY < voxelAmount; voxelIndexY++)
              {
                for (Int32 voxelIndexZ = 0; voxelIndexZ < voxelAmount; voxelIndexZ++)
                {
                  const maxon::IntVector32 voxelCoord = iterator.GetCoords();
                  Matrix m;
                  m.off = transform * Vector(voxelCoord.x + voxelIndexX, voxelCoord.y + voxelIndexY, voxelCoord.z + voxelIndexZ);
                  matrices.Append(std::move(m)) iferr_return;
                }
              }
            }			
          }
          instance->SetInstanceMatrices(matrices) iferr_return;
      
          EventAdd();
      
          return dlg.Open(DLG_TYPE::ASYNC, ID_ACTIVEOBJECT, -1, -1, 500, 300);
        }
      
        virtual Int32 GetState(BaseDocument* doc, GeDialog* parentManager)
        {
          return CMD_ENABLED;
        }
      };
      

      Cheers,
      Maxime.

      posted in Cinema 4D SDK
      M
      m_adam