Hi sorry I fixed my code.
Cheers,
Maxime.
Hi sorry I fixed my code.
Cheers,
Maxime.
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.
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.
Welcome to the Maxon developers forum and its community, it is great to have you with us!
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.
It is strongly recommended to read the first two topics carefully, especially the section Support Procedures: Asking Questions.
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.
Hello @itstanthony,
I will answer here soon, I currently have a lot on my desk.
Thank you for your understanding,
Maxime.
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.
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.
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.
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.
Hi,
yes, this is expected, and documented in NodeData::Read() / NodeData::Write() Manual. With that's said Read/Write is supposed to serialize data of your object (most of the time member variable) to the c4d file (the HyperFile), so accessing the BaseDocument is a sign of a bad design.
May I ask why do you really want to access the BaseDocument in the Read function?
Cheers,
Maxime
edit (@ferdinand): Just to be clear here, as we briefly talked about this in our morning meeting, what the docs write there is in a certain sense legacy information. When a branch, e.g., the Xbase
(shader) branch of a material is being deserialized, the to be deserialized node is inserted into the branch and with that will have access to a document.
But what @m_adam wrote is of course true, the document in this context is the fruit from the forbidden tree, as the node is not yet fully deserialized and certain parts of the GeListHead::Read, GeListNode::Read, BaseList2D::Read, ..., YourPluginLayer::Read
deserialization chain might pull all sorts of tricks to carry out the deserialization, including temporarily removing the node from the scene graph.
The documentation is therefore effectively still correct, as deserializing a node should never rely on the document context and accessing the document is forbidden in that context.
@LeonGao007 Welcome to the Maxon developers forum and its community, it is great to have you with us!
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.
It is strongly recommended to read the first two topics carefully, especially the section Support Procedures: Asking Questions.
I've reached Zbrush developers, however I doubt what you ask for is possible.
First of all exporting Zbrush scene in a background thread, this would imply you copy the full scene in memory in order to avoid any issue if the user edits the scene while your edit it. I'm not an expert at it, but I do not think that Zbrush support that at all.
Exporting current scene to a common 3D model format to a specific path, this is possible via Script however from what I understand if the export plugin, trigger a popup you are screwed as you can see here Zplugin:USD Format:Save set save location through zscript.
Finally regarding GoZ, this is a custom file format, it work by saving everything into a given folder (in the temp folder), then call an executable that will load this custom GoZ file format.
So from my perspective what you are asking for is not possible, but please wait until I confirm that with a Zbrush developers.
Cheers,
Maxime.
Hi @kbar sadly there is no way to suppress this warning, I removed the hh argument for the next version, thanks for the reminder.
Cheers,
Maxime.
Thanks @Neekoe to have shared your solution and thanks @Dunhou for helping.
When you pass LV_TREE the Draw method is not called to have it called you need to pass either LV_USERTREE or LV_USER.
Cheers,
Maxime.
Hi @chuanzhen the fact that the first time (when nothing is selected) correctly respect the COLOR_TEXT is kind of a bug for treeview containing only LV_USER.
When you are using LV_USER the DrawCell method is called and you are responsible to define the color of your text via DrawSetTextCol. But if you do not set any text color, the GeUserArea will use the previously defined color. Which is in this case COLOR_TEXT when nothing is selected. Then when you select something, the last call of this DrawSetTextCol
internally done by Cinema 4D define it to "white" color so that's why all texts are white. Then when there is one column that is defined as LV_TREE it will then call DrawSetTextCol with the correct color (COLOR_TEXT or COLOR_TEXT_SELECTED) and therefor your call DrawText will use this color.
So with that's said, do not forget to manage your color in your DrawCell method something like that:
txtColorDict = canvas.GetColorRGB(c4d.COLOR_TEXT_SELECTED) if obj.IsSelected else canvas.GetColorRGB(c4d.COLOR_TEXT)
txtColorVector = c4d.Vector(txtColorDict["r"]/255.0, txtColorDict["g"]/255.0, txtColorDict["b"]/255.0)
canvas.DrawSetTextCol(txtColorVector, bgColor)
canvas.DrawText(text, xpos, ypos)
Cheers,
Maxime.
Hey @Cankar001 I frankly do not understand what you are doing and why you would need that. Since you told me previously that you do not have any UI in your plugin but you told us "Would show wrong values", so which values are you talking about?
Matrices, Vectors, float in Cinema 4D are unit less and will adapt properly according to the document unit and not the displayed unit. Display unit are more or less just noise and does not indicate anything regarding the actual size of an object. For more information about Display unit vs document unit please read How Do I Change the Units Used By Cinema 4D To Something Other Than Centimeters? .
So if you want more help then you will need to share with us some code to demonstrate the issue since we do not really understand the reasoning of why you will want to do that (not saying you do not have a valid reason for doing it, but we do not understand it). If it's private code you can send it to us at [email protected].
With that's said here is how you retrieve and set the preference:
BasePlugin* bp = FindPlugin(PREFS_UNITS, cinema::PLUGINTYPE::PREFS);
if (bp == nullptr)
return;
/* Possible Values
PREF_UNITS_BASIC_KM = 1,
PREF_UNITS_BASIC_M = 2,
PREF_UNITS_BASIC_CM = 3,
PREF_UNITS_BASIC_MM = 4,
PREF_UNITS_BASIC_MICRO = 5,
PREF_UNITS_BASIC_NM = 6,
PREF_UNITS_BASIC_MILE = 7,
PREF_UNITS_BASIC_YARD = 8,
PREF_UNITS_BASIC_FOOT = 9,
PREF_UNITS_BASIC_INCH = 10,
*/
GeData unitValue;
bp->GetParameter(CreateDescID(PREF_UNITS_BASIC), unitValue, cinema::DESCFLAGS_GET::NONE);
Int32 uValue = unitValue.GetInt32();
bp->SetParameter(CreateDescID(PREF_UNITS_BASIC), PREF_UNITS_BASIC_KM, cinema::DESCFLAGS_SET::NONE);
Again there is no such event so it will be on you to have a timer that check if the value changed or not.
Cheers,
Maxime.
Hi the devs just came back and here his reply:
Hey Maxime, despite the menu itself isn't hardcoded (it's MENU M_ICON_PALETTE in c4d_m_coffee_manager.res file), there's no way by default for a python script to get the command id of the right clicked command in order to do the required queries, that would require changes in C4D code.
And so far it is not a priority for us to have such feature so, we keep the idea in case we want to revisit our menus but such feature is not on any roadmap at the moment.
Cheers,
Maxime.
Hi what you describe (Edit > Program Settings > Units > Display) is only the display of the unit in the whole Cinema 4D interface. Not the actual unit of the document which can be found by pressing CTRL+D -> Project -> Scale and which is actually changed by the code you provided.
So do you really want to change/listen to the Display unit and not the actual unit?
Cheers,
Maxime.
Hi @Cankar001 there is no such message, so you have to track it yourself.
The best way would be to use a MessageData MessageData to either
create a timer that will periodically (maybe each second) check the value stored in the document or react to EVMSG_CHANGE and also check the value stored in the document.
Cheers,
Maxime.
Hi @Cankar001 you do not have to do anything as long as you define your parameter as a unit then it will respect the one in Cinema 4D and automatically adapt.
If your plugin is a GeDialog, then you need to call SetMeter and if you are working with description you need to set DESC_UNIT to DESC_UNIT_METER. While the symbol indicate meter, they will respect the settings defined in the document and adapt accordingly.
If you do not want to use the default system, then you will have to do everything yourself, and track yourself the change. But here it depend too much on the type of your plugin to guide you, so please tell us a bit more about it, and most important what is your UI.
Cheers,
Maxime.
Hi @serco, there is multiple way to execute a script automatically when Cinema 4D is opened.
Cheers,
Maxime.