Get value of texture node filepath port?
-
Hello,
I have a RS standard Material with one texture node inside of it, linking to a texture file. Now I want to add additional texture nodes/files based on the file location of the original texture node with python.
I know how to add additional nodes but before I can do that I need to retrieve the value of the 'filename' port of the original texture node. (com.redshift3d.redshift4c4d.nodes.core.texturesampler.tex0)
Through some examples here on the forum I figured out how to get the value of the name but I'm not able to get the filepath.
here is a small part of my script where I try to print the name and the filepath of the texture node:
from typing import Optional import c4d import maxon doc: c4d.documents.BaseDocument # The active document op: Optional[c4d.BaseObject] # The active object, None if unselected def GetFileAssetUrl(aid: maxon.Id): # Bail when the asset ID is invalid. if not isinstance(aid, maxon.Id) or aid.IsEmpty(): raise RuntimeError(f"{aid = } is not a valid asset ID") # Get the user repository, a repository which contains almost all assets, and try to find the # asset description, a bundle of asset metadata, for the given asset ID in it. repo = maxon.AssetInterface.GetUserPrefsRepository() if repo.IsNullValue(): raise RuntimeError("could not access the user repository") asset = repo.FindLatestAsset( maxon.AssetTypes.File(), aid, maxon.Id(), maxon.ASSET_FIND_MODE.LATEST) if asset.IsNullValue(): raise RuntimeError(f"Could not find file asset for {aid}.") # When an asset description has been found, return the URL of that asset in the "asset:///" # scheme for the latest version of that asset. return maxon.AssetInterface.GetAssetUrl(asset, True) def GetAllMaterials(): materials = [] # Get the active document doc = c4d.documents.GetActiveDocument() # Get the material manager materialManager = doc.GetMaterials() # Iterate through all materials in the material manager for mat in materialManager: materials.append(mat) return materials def main() -> None: # Get the list of all materials in the Material Manager mat_all = GetAllMaterials() material = None for mat in mat_all: if mat.GetName() == "MatTest": material = mat if material is not None: idTextureNode = maxon.Id("com.redshift3d.redshift4c4d.nodes.core.texturesampler") # Get the RS node graph of the Material nodeMaterial = material.GetNodeMaterialReference() graph = nodeMaterial.GetGraph( maxon.Id("com.redshift3d.redshift4c4d.class.nodespace")) if graph.IsNullValue(): raise RuntimeError("could not get RS graph of the imported material") texResult = [] maxon.GraphModelHelper.FindNodesByAssetId(graph, idTextureNode, True, texResult) # print the texturesampler node name and the texture file path for tex in texResult: print("Name: "+ str(tex.GetValue(maxon.NODE.BASE.NAME))) print("Path: "+ str(tex.GetValue("com.redshift3d.redshift4c4d.nodes.core.texturesampler.tex0"))) else: print("Cant find Material") print(" ") c4d.EventAdd() if __name__ == '__main__': main()
Maybe someone here can point me in the right direction?
Thanks in advance for your time!
-
Hello @blkmsk,
Thank you for reaching out to us. There are multiple mistakes in your code, but instead of writing a lengthy answer, I decided to write a code example which we will ship with the next release of Cinema 4D.
Your main problem was not that you did not know how to get or set and port value, but that you did not understand the structure of a graph. And while it has been somewhat explained in our other examples, we lacked an example which dissected the topic in more detail. Which is why I wrote this example.
Cheers,
Ferdinand#coding: utf-8 """Demonstrates the inner structure of node graphs and how to find entities in them. Run this script in the Script Manager on a document which contains at least one Redshift node material with at least one "Texture" node in the graph. The script will print out the URL of each texture node and print the GraphNode tree for each material in the document. Topics: * The difference between node and asset IDs. * The inner structure of a graph. * Reading the value of a port (both for connected and unconnected ports). Entities: * PrintGraphTree: Prints a given graph as a GraphNode tree to the console. * main: Runs the main example. """ __author__ = "Ferdinand Hoppe" __copyright__ = "Copyright (C) 2023 MAXON Computer GmbH" __date__ = "16/11/2023" __license__ = "Apache-2.0 License" __version__ = "2024.?.?" import c4d import maxon import typing doc: c4d.documents.BaseDocument # The active document. def PrintGraphTree(graph: maxon.GraphModelRef) -> None: """Prints a given graph as a GraphNode tree to the console. This can be helpful to understand the structure of a graph. Note that this will print the true data of a graph, which will include all ports (most of them are usually hidden in the Node Editor) and also hidden graph entities. """ kindMap: dict[int, str] = { maxon.NODE_KIND.NODE: "NODE", maxon.NODE_KIND.INPORT: "INPORT", maxon.NODE_KIND.INPUTS: "INPUTS", maxon.NODE_KIND.NONE: "NONE", maxon.NODE_KIND.OUTPORT: "OUTPORT", maxon.NODE_KIND.OUTPUTS: "OUTPUTS", } if graph.IsNullValue(): raise RuntimeError("Invalid graph.") def iterTree(node: maxon.GraphNode, depth: int = 0) -> typing.Iterator[tuple[int, maxon.GraphNode]]: """Yields all descendants of #node and their hexarchy depth, including #node itself. This is one way to iterate over a node graph. But when one does not require the hierarchy as we do here, one should use GraphNode.GetInnerNodes() as demonstrated in main() below. """ yield (node, depth) for child in node.GetChildren(): for item in iterTree(child, depth + 1): yield item root: maxon.GraphNode = graph.GetRoot() for node, depth in iterTree(root): print (f"{' ' * depth} + '{node.GetId()}' [{kindMap.get(node.GetKind(), 'UNKNOWN KIND')}]"+ ("" if not node.GetId().IsEmpty() else f"(Root Node)")) def main(): """Runs the main example. """ # For each #material in the currently active document ... for material in doc.GetMaterials(): # ... get its node material reference and step over all materials which do not have a # Redshift graph or where the graph is malformed. nodeMaterial: c4d.NodeMaterial = material.GetNodeMaterialReference() if not nodeMaterial.HasSpace("com.redshift3d.redshift4c4d.class.nodespace"): continue graph: maxon.GraphModelRef = nodeMaterial.GetGraph( "com.redshift3d.redshift4c4d.class.nodespace") if graph.IsNullValue(): raise RuntimeError("Found malformed empty graph associated with node space.") # Get the root of the graph, the node which contains all other nodes. Since we only want # to read information here, we do not need a graph transaction. But for all write operations # we would have to start a transaction on #graph. root: maxon.GraphNode = graph.GetRoot() # Iterate over all nodes in the graph, i.e., unpack things like nodes nested in groups. # With the mask argument we could also include ports in this iteration. for node in root.GetInnerNodes(mask=maxon.NODE_KIND.NODE, includeThis=False): # There is a difference between the asset ID of a node and its ID. The asset ID is # the identifier of the node template asset from which a node has been instantiated. # It is more or less the node type identifier. When we have three RS Texture nodes # in a graph they will all have the same asset ID. But their node ID on the other hand # will always be unique to a node. assetId: maxon.Id = node.GetValue("net.maxon.node.attribute.assetid")[0] nodeId: maxon.Id = node.GetId() # Step over everything that is not a Texture node, we could also check here for a node # id, in case we want to target a specific node instance. if assetId != maxon.Id("com.redshift3d.redshift4c4d.nodes.core.texturesampler"): continue # Now we got hold of a Texture node in a graph. To access a port value on this node, # for example the value of the Filename.Path port, we must get hold of the port entity. # Get the Filename input port of the Texture node. filenameInPort: maxon.GraphNode = node.GetInputs().FindChild( "com.redshift3d.redshift4c4d.nodes.core.texturesampler.tex0") # But #filenameInPort is a port bundle - that is how ports are called which are composed # of multiple (static) child ports. The port has the data type "Filename" with which we # cannot do much on its own. Just as in the node editor, we must use its # nested child ports to get and set values, in this case the "path". pathPort: maxon.GraphNode = filenameInPort.FindChild("path") # Another way to get hold of the path port would be using a node path, where we define # an identifier which addresses the port node directly from the root of the graph. alsoPathPort: maxon.GraphNode = graph.GetNode( node.GetPath() + ">com.redshift3d.redshift4c4d.nodes.core.texturesampler.tex0\path") # The underlying information is here the nature of the type GraphNode. The word "node" # in it does not refer to a node in a graph but to a node in a tree. All entities in a # graph - nodes, ports, and things that are not obvious from the outside - are organized # in a tree of GraphNode instances. Each GraphNode instance has then a NODEKIND which # expresses the purpose of that node, examples for node kinds are: # # * NODE_KIND.NODE : A GraphNode that represents a tangible node in a graph. The # Nodes API calls this a 'true node'. # * NODE_KIND.INPORT : A GraphNode that represents a input port in a graph. # * NODE_KIND.OUTPORT: A GraphNode that represents a output port in a graph. # # For our example the GraphNode tree of a material graph could look like this: # + "" // The root node of the graph, it is # | is the container that holds all # | entities in the graph. It has the # | empty ID as its ID. # |---+ "texturesampler@bbRMv5CAGpPtJFHs5n43CV" // An RS Texture node in it. # | |---+ ">" // A child node of the texture that # | | holds all of its input ports, it has # | | a special NODE_KIND and is the # | | underlying node of what we accessed # | | with the .GetInputs() call above. # | |---+ "...nodes.core.texturesampler" // The Filename input port of a Texture # | | node. It is a port bundle and # | | therefore has child ports attached to # | | it. This node and all of its children # | | are of type NODE_KIND.INPORT. # | |---+ "path" // The nested port for the Filename.Path. # |---+ "standardmaterial@FTf3YdB7AxEn1JKQL2$W" // An RS Standard Material node in the # | | graph. # | |---+ ">" // Its input ports. # | | | ... # | | # | |---+ "<" // Its output ports. # | | ... # | # |---+ ... // Another node in the graph. # On this entity we can now read and write values. In 2023 an earlier we would have used # Get/SetDefaultValue for this, in 2024 and onwards these methods have been deprecated # and we use GetValue now instead. url: str | None = pathPort.GetValue("effectivevalue") print (f"{pathPort}: {url}") # Print the GraphNode tree for the current #graph. PrintGraphTree(graph) if __name__ == "__main__": main()
-
Oh wow! Thank you so much!
Yes I'm a beginner with python cinema 4D so I really appreciate your support and this definitly gives me a better understanding of the node structure.