SOLVED How do I get GvNodeMaster stored in Redshift material

I need to get the node information on the Redshift material for a plugin that automatically adds maps, reads maps, and converts Redshift materials to default materials.
I don't know how to get the XPresso stored on the Redshift material. Can someone help me?

Thank,
AiMiD

Hello @aimidi,

please excuse, I overlooked the C++ tag you did assign to your posting. R21, the version you tagged this posting as, is also out of scope of support. The code examples I provided below have been written for R25. There were substantial changes to the C++ Python API with R23, you might not be able to translate that example to R21. The branch info approach should work in R21.

The short answer is, "no, there is no C++ API for Redshift at the moment". You can however use the Python API from C++ or use BranchInfo to access the Xpresso nodes attached to a Redshift material. The Python approach has the advantage that you can make use of the whole Python Redshift API, but the disadvantage that it is a bit involved. The branch info approach is easier, but it will only solve your problem at hand, getting the underlying graph, and will not give you access to the other functionalities of the Redshift Python API (there are not many other features in the Python API).

Find below an example demonstrating both approaches.

Cheers,
Ferdinand

The result:
redshiftcpp.gif

The code:

// For https://plugincafe.maxon.net/topic/13752

#include "c4d_basedocument.h"
#include "c4d_basematerial.h"
#include "c4d_general.h"
#include "c4d_graphview.h"

#include "lib_py.h"
#include "maxon/cpython.h"
#include "maxon/cpython_c4d.h"
#include "maxon/cpython3_raw.h"
#include "maxon/errortypes.h"
#include "maxon/vm.h"

// The type id of Redshift materials.
const maxon::Int32 g_redshift_material_id = 1036224;

/// Attempts to retrieve the Redshift GvNodeMaster of #material with Python.
/// 
/// @param material The material to get the Redshift GvNodeMaster for.
/// @param pythonCode The Python code that is used to retrieve it.
/// @return The Redshift GvNodeMaster for #material.
/// ------------------------------------------------------------------------------------------------ 
static maxon::Result<GvNodeMaster*> CallRedshiftPythonLayer(
  BaseMaterial* material, const maxon::String& pythonCode)
{
  iferr_scope;

  // Bail when #material is null or not a Redshift material.
  if (!material || (material->GetType() != g_redshift_material_id))
    return maxon::IllegalArgumentError(
      MAXON_SOURCE_LOCATION, "Passed material is not a Redshift material."_s);

  // Initialize a scope with the Python 3 virtual machine for #pythonCode.
  const maxon::VirtualMachineRef& vm = MAXON_CPYTHON3VM();
  const maxon::VirtualMachineScopeRef scope = vm.CreateScope() iferr_return;
  scope.Init("Python"_s, pythonCode, maxon::ERRORHANDLING::PRINT, nullptr) iferr_return;

  // We could setup here the modules attributes doc and op that are commonly used in Cinema 4D 
  // Python scripts by adding them to the scope. But that is not really necessary in the example 
  // since we just call one function which does not require any module attributes.

  // Set the __name__ attribute of the module so that the execution context guard ifnamemain works.
  // Also not necessary in a technical sense, since we do not make use of it.
  scope.Add("__name__"_s, maxon::Data("__main__"_s)) iferr_return;

  // When the script does modify the active document, you must stop all threads before executing it.
  // Here not necessary, since the example code does not modify anything.
  StopAllThreads();

  // Execute the module context of the script, i.e., carry out imports and module level expressions,
  // as for example an ifnamemain execution guard.
  scope.Execute() iferr_return;

  // Now we are going to call the specific function GetNodeMaster() in the script.

  // Make the passed material accessible as an argument for the python function GetNodeMaster() 
  // by casting it into its binding type that handles the C++/Python type binding.
  maxon::specialtype::BaseMaterial* materialSpeical = (
    reinterpret_cast<maxon::specialtype::BaseMaterial*>(material));

  // Create a data structure for the arguments of GetNodeMaster(), containing the material binding.
  maxon::Data materialArgument = maxon::Data(materialSpeical);
  maxon::BaseArray<maxon::Data*> arguments;
  arguments.Append(&materialArgument) iferr_return;

  // Setup the return type. We are using here BaseList2D for the return type, or more specifically 
  // its special binding type, since there is no dedicated binding type for GvNodeMaster. But 
  // GvNodeMaster is of type BaseList2D and we will later attempt to cast the return value back to 
  // GvNodeMaster.
  maxon::BlockArray<maxon::Data> helperStack;
  const maxon::DataType& returnType = maxon::GetDataType<maxon::specialtype::BaseList2D*>();

  // Call GetNodeMaster() in the scope and interpret its return value(s).
  maxon::Data* res = scope.PrivateInvoke(
    "GetNodeMaster"_s, helperStack, returnType, &arguments.ToBlock()) iferr_return;

  // The function execution did fail.
  if (!res)
    return maxon::UnexpectedError(MAXON_SOURCE_LOCATION, "Function execution did fail."_s);

  // The function in Python does not conform to the expected return type of BaseList2D.
  maxon::specialtype::BaseList2D* returnValue = res->Get<maxon::specialtype::BaseList2D*>().GetValue();
  if (returnValue == nullptr)
    return maxon::UnexpectedError(
      MAXON_SOURCE_LOCATION, "Function does not conform to its return value signature."_s);

  // The C++/Python binding type return value cast back into its native C++ form.
  BaseList2D* result = reinterpret_cast<BaseList2D*>(returnValue);

  // Finally cast it into what we are after, a GvNodeMaster.
  GvNodeMaster* nodeMaster = static_cast<GvNodeMaster*>(result);

  return nodeMaster;
}

/// Attempts to retrieve the Redshift node graph of #material with branching information.
/// 
/// An alternative and less complex solution than CallRedshiftPythonLayer() which uses branch info
/// to get hold of the graph. This is easier but other than with the Python approach it will give
/// you not access to all the things the Redshift Python API can do.
/// 
/// @param material The material to get the Redshift graph for.
/// @return The Redshift graph for #material.
/// ------------------------------------------------------------------------------------------------ 
maxon::Result<GvNode*> GetRedshiftNodeBranch(BaseMaterial* material)
{
  iferr_scope;

  // Bail when #material is null or not a Redshift material.
  if (!material || (material->GetType() != g_redshift_material_id))
    return maxon::IllegalArgumentError(
      MAXON_SOURCE_LOCATION, "Passed material is not a Redshift material."_s);

  // Setup an array for all branches of #material. Usually nodes do not have more than ten 
  // branches, e.g., a BaseObject might have a tags and tracks branch, and one or two internally 
  // used branches. So, setting maxBranchCount to 64 is very generous. 
  const maxon::Int32 maxBranchCount = 64;
  BranchInfo branches[maxBranchCount];

  // Get all branches for #material.
  maxon::Int32 count = material->GetBranchInfo(branches, maxBranchCount, GETBRANCHINFO::NONE);

  // Search for a "Node" branch, which is the branch containing the Redshift graph.
  BranchInfo* nodeBranch = nullptr;
  for (maxon::Int32 i = 0; i < count; i++)
  {
    if (branches[i].name.Compare("Node"_s) == maxon::COMPARERESULT::EQUAL)
    {
      nodeBranch = &branches[i];
    }
  }

  if (!nodeBranch)
    return maxon::UnexpectedError(
      MAXON_SOURCE_LOCATION, "The passed material does not contain a Redshift 'Node' branch."_s);

  GeListHead* head = nodeBranch->head;
  if ((!head) || (!head->GetFirst()))
    return maxon::UnexpectedError(MAXON_SOURCE_LOCATION, "Malformed or empty branch info."_s);

  // Get the first node in the list head, this is equivalent to nodeMaster->GetRoot(). We cannot
  // access the node master itself with this approach, but all the nodes in it.
  GvNode* node = static_cast<GvNode*>(head->GetFirst());
  return node;
}

/// Prints #node and all its descendants as a tree to the console.
/// 
/// @param node The graph node to print.
/// @param level (optional) The indentation level.
/// ------------------------------------------------------------------------------------------------
static void PrintNodes(GvNode* node, const maxon::Int level = 0)
{
  if (!node)
    return;

  maxon::String indentation = ""_s;
  for (maxon::Int i = 0; i < level; i++)
    indentation += "\t"_s;

  while (node)
  {
    maxon::Int location = reinterpret_cast<maxon::Int>(node);
    ApplicationOutput("@ @ at [email protected]{x}", indentation, node->GetTitle(), location);
    PrintNodes(node->GetDown(), level + 1);
    node = node->GetNext();
  }
}

/// Runs the two examples. 
static maxon::Result<void> pc13752(BaseDocument* doc)
{
  iferr_scope_handler
  {
    ApplicationOutput("@: @", err.GetClass().GetId(), err.GetMessage());
    return err;
  };

  // The Python code we are going to run with CallRedshiftPythonLayer. 
  const maxon::String pythonCode = (
    "import c4d\n"_s +
    "import redshift\n\n"_s +
    "def GetNodeMaster(\n"_s +
    "    material: c4d.BaseMaterial) -> c4d.modules.graphview.GvNodeMaster:\n"_s +
    "    '''Attempts to return the Redshift GvNodeMaster for #material.\n"_s +
    "    '''\n"_s +
    "    if material.GetType() != redshift.Mrsmaterial:\n"_s +
    "        raise ArgumentError(f'Unexpected type with {material = }.')\n\n"_s +
    "    master = redshift.GetRSMaterialNodeMaster(material)\n"_s +
    "    if master is None:\n\n"_s +
    "        raise RuntimeError(f'Could not access node master for {material}.')\n\n"_s +
    "    return master\n\n\n"_s);

  // Get the active material for which we will attempt to get the RS node master for.
  BaseMaterial* activeMaterial = doc->GetActiveMaterial();
  if (!activeMaterial)
    return maxon::IllegalStateError(MAXON_SOURCE_LOCATION, "No material selected"_s);

  // Run the example using Python to get hold of the nodes in the material.
  ApplicationOutput("\nAccess with Python layer:");
  GvNodeMaster* nodeMaster = CallRedshiftPythonLayer(activeMaterial, pythonCode) iferr_return;
  if (!nodeMaster)
    return maxon::UnexpectedError(MAXON_SOURCE_LOCATION, "CallRedshiftPythonLayer() failed."_s);
  PrintNodes(nodeMaster->GetRoot());

  // Run the example using branching information to get hold of the nodes in the material.
  ApplicationOutput("\nAccess with branching information:");
  GvNode* nodeRoot = GetRedshiftNodeBranch(activeMaterial) iferr_return;
  if (!nodeRoot)
    return maxon::UnexpectedError(MAXON_SOURCE_LOCATION, "GetRedshiftNodeBranch() failed."_s);
  PrintNodes(nodeRoot);

  return maxon::OK;
}

Hello @aimidi,

Thank you for reaching out to us. You can access the node master of a Redshift material with redshift.GetRSMaterialNodeMaster(material: c4d.BaseMaterial). Find below a simple script which accesses the Xpresso graph of a Redshift material. I did orignally post the code here.

Cheers,
Ferdinand

The code:

"""Run this in the script manger with a Redshift material selected.
"""

import c4d

def main():
    """
    """
    # Try to import the Redshift module.
    try:
        import redshift
    except:
        raise RuntimeError("Could not import Redshift.")
    
    # Get the active material.
    material = doc.GetActiveMaterial()
    # Bail if it is not a redshift material.
    if material.GetType() != redshift.Mrsmaterial:
        raise TypeError("Selected material is not a Redshift material.")

    # Get the graph view node master of the material.
    master = redshift.GetRSMaterialNodeMaster(material)
    if master is None:
        raise RuntimeError("Could not access material node master.")

    # From here on out its is basically smooth sailing, and just standard 
    # graph view stuff.
    
    # Get the root node.
    root = master.GetRoot()
    # The shader graph stuff is here.
    node = root.GetDown()

    # Iterate over the nodes in the graph.
    while node:
        # When the node is selected and a Redshift Shader:
        if (node.GetBit(c4d.BIT_ACTIVE) is True and
            node.GetTypeName() == "Redshift Shader"):
            # Get the diffuse color
            diffuse = node[c4d.REDSHIFT_SHADER_MATERIAL_DIFFUSE_COLOR]
            print (diffuse)
        node = node.GetNext()
   

# Execute main()
if __name__=='__main__':
    main()

@ferdinand Thank you for your reply.
My plugin is written in C++. Is there a way to call redshift.GetRSMaterialNodeMaster() in C++ 's existing sdk? (or call the python code through C++)

Thank,
AiMiD

Hello @aimidi,

please excuse, I overlooked the C++ tag you did assign to your posting. R21, the version you tagged this posting as, is also out of scope of support. The code examples I provided below have been written for R25. There were substantial changes to the C++ Python API with R23, you might not be able to translate that example to R21. The branch info approach should work in R21.

The short answer is, "no, there is no C++ API for Redshift at the moment". You can however use the Python API from C++ or use BranchInfo to access the Xpresso nodes attached to a Redshift material. The Python approach has the advantage that you can make use of the whole Python Redshift API, but the disadvantage that it is a bit involved. The branch info approach is easier, but it will only solve your problem at hand, getting the underlying graph, and will not give you access to the other functionalities of the Redshift Python API (there are not many other features in the Python API).

Find below an example demonstrating both approaches.

Cheers,
Ferdinand

The result:
redshiftcpp.gif

The code:

// For https://plugincafe.maxon.net/topic/13752

#include "c4d_basedocument.h"
#include "c4d_basematerial.h"
#include "c4d_general.h"
#include "c4d_graphview.h"

#include "lib_py.h"
#include "maxon/cpython.h"
#include "maxon/cpython_c4d.h"
#include "maxon/cpython3_raw.h"
#include "maxon/errortypes.h"
#include "maxon/vm.h"

// The type id of Redshift materials.
const maxon::Int32 g_redshift_material_id = 1036224;

/// Attempts to retrieve the Redshift GvNodeMaster of #material with Python.
/// 
/// @param material The material to get the Redshift GvNodeMaster for.
/// @param pythonCode The Python code that is used to retrieve it.
/// @return The Redshift GvNodeMaster for #material.
/// ------------------------------------------------------------------------------------------------ 
static maxon::Result<GvNodeMaster*> CallRedshiftPythonLayer(
  BaseMaterial* material, const maxon::String& pythonCode)
{
  iferr_scope;

  // Bail when #material is null or not a Redshift material.
  if (!material || (material->GetType() != g_redshift_material_id))
    return maxon::IllegalArgumentError(
      MAXON_SOURCE_LOCATION, "Passed material is not a Redshift material."_s);

  // Initialize a scope with the Python 3 virtual machine for #pythonCode.
  const maxon::VirtualMachineRef& vm = MAXON_CPYTHON3VM();
  const maxon::VirtualMachineScopeRef scope = vm.CreateScope() iferr_return;
  scope.Init("Python"_s, pythonCode, maxon::ERRORHANDLING::PRINT, nullptr) iferr_return;

  // We could setup here the modules attributes doc and op that are commonly used in Cinema 4D 
  // Python scripts by adding them to the scope. But that is not really necessary in the example 
  // since we just call one function which does not require any module attributes.

  // Set the __name__ attribute of the module so that the execution context guard ifnamemain works.
  // Also not necessary in a technical sense, since we do not make use of it.
  scope.Add("__name__"_s, maxon::Data("__main__"_s)) iferr_return;

  // When the script does modify the active document, you must stop all threads before executing it.
  // Here not necessary, since the example code does not modify anything.
  StopAllThreads();

  // Execute the module context of the script, i.e., carry out imports and module level expressions,
  // as for example an ifnamemain execution guard.
  scope.Execute() iferr_return;

  // Now we are going to call the specific function GetNodeMaster() in the script.

  // Make the passed material accessible as an argument for the python function GetNodeMaster() 
  // by casting it into its binding type that handles the C++/Python type binding.
  maxon::specialtype::BaseMaterial* materialSpeical = (
    reinterpret_cast<maxon::specialtype::BaseMaterial*>(material));

  // Create a data structure for the arguments of GetNodeMaster(), containing the material binding.
  maxon::Data materialArgument = maxon::Data(materialSpeical);
  maxon::BaseArray<maxon::Data*> arguments;
  arguments.Append(&materialArgument) iferr_return;

  // Setup the return type. We are using here BaseList2D for the return type, or more specifically 
  // its special binding type, since there is no dedicated binding type for GvNodeMaster. But 
  // GvNodeMaster is of type BaseList2D and we will later attempt to cast the return value back to 
  // GvNodeMaster.
  maxon::BlockArray<maxon::Data> helperStack;
  const maxon::DataType& returnType = maxon::GetDataType<maxon::specialtype::BaseList2D*>();

  // Call GetNodeMaster() in the scope and interpret its return value(s).
  maxon::Data* res = scope.PrivateInvoke(
    "GetNodeMaster"_s, helperStack, returnType, &arguments.ToBlock()) iferr_return;

  // The function execution did fail.
  if (!res)
    return maxon::UnexpectedError(MAXON_SOURCE_LOCATION, "Function execution did fail."_s);

  // The function in Python does not conform to the expected return type of BaseList2D.
  maxon::specialtype::BaseList2D* returnValue = res->Get<maxon::specialtype::BaseList2D*>().GetValue();
  if (returnValue == nullptr)
    return maxon::UnexpectedError(
      MAXON_SOURCE_LOCATION, "Function does not conform to its return value signature."_s);

  // The C++/Python binding type return value cast back into its native C++ form.
  BaseList2D* result = reinterpret_cast<BaseList2D*>(returnValue);

  // Finally cast it into what we are after, a GvNodeMaster.
  GvNodeMaster* nodeMaster = static_cast<GvNodeMaster*>(result);

  return nodeMaster;
}

/// Attempts to retrieve the Redshift node graph of #material with branching information.
/// 
/// An alternative and less complex solution than CallRedshiftPythonLayer() which uses branch info
/// to get hold of the graph. This is easier but other than with the Python approach it will give
/// you not access to all the things the Redshift Python API can do.
/// 
/// @param material The material to get the Redshift graph for.
/// @return The Redshift graph for #material.
/// ------------------------------------------------------------------------------------------------ 
maxon::Result<GvNode*> GetRedshiftNodeBranch(BaseMaterial* material)
{
  iferr_scope;

  // Bail when #material is null or not a Redshift material.
  if (!material || (material->GetType() != g_redshift_material_id))
    return maxon::IllegalArgumentError(
      MAXON_SOURCE_LOCATION, "Passed material is not a Redshift material."_s);

  // Setup an array for all branches of #material. Usually nodes do not have more than ten 
  // branches, e.g., a BaseObject might have a tags and tracks branch, and one or two internally 
  // used branches. So, setting maxBranchCount to 64 is very generous. 
  const maxon::Int32 maxBranchCount = 64;
  BranchInfo branches[maxBranchCount];

  // Get all branches for #material.
  maxon::Int32 count = material->GetBranchInfo(branches, maxBranchCount, GETBRANCHINFO::NONE);

  // Search for a "Node" branch, which is the branch containing the Redshift graph.
  BranchInfo* nodeBranch = nullptr;
  for (maxon::Int32 i = 0; i < count; i++)
  {
    if (branches[i].name.Compare("Node"_s) == maxon::COMPARERESULT::EQUAL)
    {
      nodeBranch = &branches[i];
    }
  }

  if (!nodeBranch)
    return maxon::UnexpectedError(
      MAXON_SOURCE_LOCATION, "The passed material does not contain a Redshift 'Node' branch."_s);

  GeListHead* head = nodeBranch->head;
  if ((!head) || (!head->GetFirst()))
    return maxon::UnexpectedError(MAXON_SOURCE_LOCATION, "Malformed or empty branch info."_s);

  // Get the first node in the list head, this is equivalent to nodeMaster->GetRoot(). We cannot
  // access the node master itself with this approach, but all the nodes in it.
  GvNode* node = static_cast<GvNode*>(head->GetFirst());
  return node;
}

/// Prints #node and all its descendants as a tree to the console.
/// 
/// @param node The graph node to print.
/// @param level (optional) The indentation level.
/// ------------------------------------------------------------------------------------------------
static void PrintNodes(GvNode* node, const maxon::Int level = 0)
{
  if (!node)
    return;

  maxon::String indentation = ""_s;
  for (maxon::Int i = 0; i < level; i++)
    indentation += "\t"_s;

  while (node)
  {
    maxon::Int location = reinterpret_cast<maxon::Int>(node);
    ApplicationOutput("@ @ at [email protected]{x}", indentation, node->GetTitle(), location);
    PrintNodes(node->GetDown(), level + 1);
    node = node->GetNext();
  }
}

/// Runs the two examples. 
static maxon::Result<void> pc13752(BaseDocument* doc)
{
  iferr_scope_handler
  {
    ApplicationOutput("@: @", err.GetClass().GetId(), err.GetMessage());
    return err;
  };

  // The Python code we are going to run with CallRedshiftPythonLayer. 
  const maxon::String pythonCode = (
    "import c4d\n"_s +
    "import redshift\n\n"_s +
    "def GetNodeMaster(\n"_s +
    "    material: c4d.BaseMaterial) -> c4d.modules.graphview.GvNodeMaster:\n"_s +
    "    '''Attempts to return the Redshift GvNodeMaster for #material.\n"_s +
    "    '''\n"_s +
    "    if material.GetType() != redshift.Mrsmaterial:\n"_s +
    "        raise ArgumentError(f'Unexpected type with {material = }.')\n\n"_s +
    "    master = redshift.GetRSMaterialNodeMaster(material)\n"_s +
    "    if master is None:\n\n"_s +
    "        raise RuntimeError(f'Could not access node master for {material}.')\n\n"_s +
    "    return master\n\n\n"_s);

  // Get the active material for which we will attempt to get the RS node master for.
  BaseMaterial* activeMaterial = doc->GetActiveMaterial();
  if (!activeMaterial)
    return maxon::IllegalStateError(MAXON_SOURCE_LOCATION, "No material selected"_s);

  // Run the example using Python to get hold of the nodes in the material.
  ApplicationOutput("\nAccess with Python layer:");
  GvNodeMaster* nodeMaster = CallRedshiftPythonLayer(activeMaterial, pythonCode) iferr_return;
  if (!nodeMaster)
    return maxon::UnexpectedError(MAXON_SOURCE_LOCATION, "CallRedshiftPythonLayer() failed."_s);
  PrintNodes(nodeMaster->GetRoot());

  // Run the example using branching information to get hold of the nodes in the material.
  ApplicationOutput("\nAccess with branching information:");
  GvNode* nodeRoot = GetRedshiftNodeBranch(activeMaterial) iferr_return;
  if (!nodeRoot)
    return maxon::UnexpectedError(MAXON_SOURCE_LOCATION, "GetRedshiftNodeBranch() failed."_s);
  PrintNodes(nodeRoot);

  return maxon::OK;
}