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: DictType) -> 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.