Your browser does not seem to support JavaScript. As a result, your viewing experience will be diminished, and you have been placed in read-only mode.
Please download a browser that supports JavaScript, or enable it if it's disabled (i.e. NoScript).
Hi,
When you call AddGraph, the delegate function registered for the nodespace to create a material is executed. That is why it create a default redshift node material with the default endNode and the default RS Material node.
The only way to do it for now is exactly what you are doing.
One remark i should have comment in our examples,
addedGraph = nodeMaterial.AddGraph(nodespaceId) nimbusRef = mat.GetNimbusRef(nodespaceId) graph = nimbusRef.GetGraph()
addedGraph is the same as graph, nimbusRef are sometimes useful, but if you are just manipulating existing graph, they are not.
Cheers, Manuel
Hello @chuanzhen,
in the above example also use function CreateLayout(), self.AddButton(),
I am not sure to which example you refer, but if it is my mock-up, you will see that it does not use any of the methods for adding gadgets manually. But you of course still have to implement the dialog and CreateLayout.
CreateLayout
def CreateLayout(self) -> bool: """Adds gadgets to the dialog. Either populate the dialog manually or load a dialog resource. """ return self.LoadDialogResource(*data)
The example is a bit optimistic, usually you still must do some stuff in the dialog, like for example adjusting the title or populating its menu, but in principle you just have to call self.LoadDialogResource(*data) to load a dialog resource into a dialog. Note that you can also invoke the method in succession to load in multiple dialog fragements.
self.LoadDialogResource(*data)
There is no dedicated Python documentation for this, but it all works in Python too, I have used it myself recently. I would recommend having a look at:
As far as I am aware of, we have no Python examples for using GeDialog.LoadDialogResource but there are plenty of C++ examples. The Asset API Examples Dialog is for example partially being loaded from a dialog resource (in line 80).
GeDialog.LoadDialogResource
Cheers, Ferdinand
Hello @kantronin,
Thank you for reaching out to us. I am not quite sure how your last sentence is meant,
I manage to do this type of rotation, with matrices and with a change of Cartesian coordinate system, but I know that there is a much simpler method with C4D python functions
but you can define a rotation matrix from an axis of rotation v and an angle w with the function c4d.utils.RotAxisToMatrix(v, w). I.e., to transform vertices of an object, you would do something like this:
axis: c4d.Vector = c4d.Vector(1, 2, 3) theta: float = c4d.utils.DegToRad(45) transform: c4d.Matrix = c4d.utils.RotAxisToMatrix(axis, theta) transformedPoints: list[c4d.Vector] = [transform * p for p in somePointObject.GetAllPoints()]
This transformation would however here happen in local object space, i.e., the space vertices/points are being expressed in. If you want to do the rotation in a space relative to the polygon, you will have to construct a frame for that polygon first, then convert the points out of local object space into your custom polygon space, carry out your rotation, and then convert the points back. I am just mentioning this because you talk about '[..] do[ing] a rotation for a polyon' which somewhat implies that you want to rotate around the origin of the polygon and some frame implied by the polygon.
Hello @jack0319,
Yes, this is a bug. I had to do some digging first (debug against c4d) to find out what is going wrong here. It seems like this is a bug in the scroll group gadget. I was unable to pinpoint what is going exactly wrong, but when a new document is created, the scroll group position is set again, because Cinema 4D is reinitializing its layout. You currently cannot do anything about this. I have filed a bug report on our bug tracker and will update this thread when the bug has been fixed.
We cannot make any guarantees regarding an ETA and considering that this is a negligible impact bug, it could take multiple revisions until the bug is fixed.
I unfortunately do not fully understand what you want to say.
My last version used dialog to implement gui, but due to some data transmission methods, I want to use res method to re-implement
Both ToolData and DescriptionToolData allow defining their GUI using resources, one uses dialog resources, the other description resources.
ToolData
DescriptionToolData
it, it seems that this is a limitation, I can only use dialog to achieve!
I am not quite sure what you mean by limitation here. Not being able to implement a DescriptionToolData plugin in Python is a limitation, but that does not stop you from defining your tool plugin GUI in a resource.
If you really want a description-based tool, you must move to C++ there you would also have (more) direct viewport image buffer access, which would further incentivize doing this.
Thank you for reaching out to us. In short, the answer is that you are looking at the wrong example and therefore do not load your GUI, resulting in nothing being displayed. You must overwrite ToolData.AllocSubDialog() to show your tool dialog.
There are two ways to implement tools in the Cinema 4D API: ToolData plugins which base their GUI on dialog resources, and DescriptionToolData plugins which base their GUI on description resources, i.e., work for example similar to an ObjectData plugin.
ObjectData
In Python you can only implement directly ToolData plugins, i.e., plugins which are based on dialogs. There is however one caveat, as there is SculptBrushToolData, which is a specialization of DescriptionToolData for sculpting brushes. And SculptBrushToolData has been wrapped for Python, sculpt_grab_brush_r16.pyp is an example for that.
SculptBrushToolData
Python examples for ToolData plugins can be found in py-liquid_painter_r12.pyp and py-tooldata_ui_r15.pyp. Your class S_ScreenShot is not implementing AllocSubDialog and therefore cannot open that dialog. It must look something like this:
S_ScreenShot
AllocSubDialog
class MyToolDialog(c4d.gui.SubDialog): """Realizes a dialog for a tool hook. """ def __init__(self, toolData: c4d.BaseContainer) -> None: """Not necessary, but IMHO advantageous to grab the tool data container. """ self._toolData = toolData def CreateLayout(self) -> bool: """Adds gadgets to the dialog. Either populate the dialog manually or load a dialog resource. """ return self.LoadDialogResource(*data) class S_ScreenShot(plugins.ToolData): """Realizes a tool hook. """ def AllocSubDialog(self, bc: c4d.BaseContainer) -> c4d.gui.SubDialog: """Called by Cinema 4D to allocate the tool dialog. """ return MyToolDialog(bc) ...
you must use the pluginStart function (usually in main.cpp)
I edited the main.cpp file that you can find in our cinema4dSDK example. Those examples are available in all c4d installation inside the sdk.zip.
You must run the project tool on the directory to generate the project files for xcode or visual studio. You can doznload the project tools here. Your sdk.zip should compile out of the box if you respect those IDE version.
hi,
To use a logger, you need to retrieve the reference to it. For now, you can only check if its name is the same as the one you expect. Or you can write to one of the loggers you can find on this page
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 main() -> None: # Called when the plugin is selected by the user. Similar to CommandData.Execute. txt = "Printing in the {} console my text" # Write to a specific logger defaultLogger = maxon.Loggers.Default() defaultLogger.Write(maxon.TARGETAUDIENCE.ALL, txt.format("default"), maxon.MAXON_SOURCE_LOCATION(1), maxon.WRITEMETA.DEFAULT) # Write to all loggers. for log in maxon.Loggers.GetEntries(): log.Write(maxon.TARGETAUDIENCE.ALL, txt.format(log.GetName()), maxon.MAXON_SOURCE_LOCATION(1), maxon.WRITEMETA.DEFAULT) if __name__ == '__main__': main()
Hey @tash,
you got the right idea and your code
rd = doc.GetFirstRenderData() while True: if rd.GetName() == "typeB": break rd = rd.GetNext()
is not a loop without an exit condition, it is only that it is a bit ugly one At some point rd will be None, and then rd.GetName() will raise an AttributeError because the type None has no such method. So, you could write instead:
rd
None
rd.GetName()
rd = doc.GetFirstRenderData() while rd: # or a bit more verbose as: while isinstance(rd, c4d.documents.RenderData): if rd.GetName() == "typeB": break rd = rd.GetNext()
The problem would then however be, that this code cannot reach nested render data, e.g., RenderData_1.1 and RenderData_1.2 in the example below.
RenderData_1 RenderData_1.1 RenderData_1.2 RenderData_2
Your code also does not have an answer to what happens when there is no "typeB" to be found at all. Find an example of how I would write something like this below. Please keeo in mind that this is still an example that must be finished by yourself.
"""Provides an example for discovering, loading, modifying, and then saving documents, to be loaded by the batch renderer. """ import c4d import os import typing def Iterate(node: c4d.GeListNode) -> c4d.GeListNode: """Walks #node depth first. """ if node is None: return while node: yield node for descendant in Iterate(node.GetDown()): yield descendant node = node.GetNext() def ModifyDocument(doc: c4d.documents.BaseDocument, renderDataTargetName: str, fallbackRenderData: typing.Optional[c4d.documents.RenderData] = None) -> None: """Modifies a document according to what you want to do. This is just an example, the function could do anything. Args: doc: The document to modify. renderDataTargetName: The name of the render data to load in the document. fallbackRenderData: The render data to use when #renderDataTargetName is not present. Will be ignored when None. Defaults to None. """ def _createNewRenderData(): """Little helper function which inserts #fallbackRenderData into #doc. """ # Cloning is technically not necessary in this case, since we know #fallbackRenderData to # be unique for each passed #doc, but if we weren't, we must clone the render data since a # node can only be attached to one document (RenderData is also a BaseList2D, i.e., node). clone: c4d.documents.RenderData = fallbackRenderData.GetClone(c4d.COPYFLAGS_0) doc.InsertRenderDataLast(clone) doc.SetActiveRenderData(clone) if not isinstance(doc, c4d.documents.BaseDocument): raise TypeError(f"Not a document: {doc}") # Get the first render data in the document. If there is None, either create the default data # and bail, or just bail when there is no default data. rData: c4d.documents.RenderData = doc.GetFirstRenderData() if rData is None: if fallbackRenderData: _createNewRenderData() return # Loop over all render data in the document. for rData in Iterate(rData): # We found a match, set the data as the active one and bail. if rData.GetName() == renderDataTargetName: doc.SetActiveRenderData(rData) return # When we reach this point it means that #renderDataTargetName was not contained in #doc. if fallbackRenderData: _createNewRenderData() def main() -> None: """ """ # Let the user select a directory to scan for documents to load. directory: str = c4d.storage.LoadDialog( title="Please select the scene directory", flags=c4d.FILESELECT_DIRECTORY) if directory is None: return # Your render settings to look for and a list to put files into for the Batch render. targetRenderSettings: str = "typeB" batchRenderFiles: list[str] = [] # Walk that directory and find everything that is a Cinema 4D file. for root, _, files in os.walk(directory): for f in files: if not f.endswith("c4d"): continue # Load the file into a Cinema 4D document and make up a new path for the modified # document. filePath: str = os.path.join(root, f) doc: c4d.BaseDocument = c4d.documents.LoadDocument(filePath, c4d.SCENEFILTER_NONE) if not isinstance(doc, c4d.documents.BaseDocument): raise OSError(f"Failed loading into a c4d document: '{filePath}'") name: str = os.path.splitext(doc.GetDocumentName())[0] newPath: str = os.path.join(doc.GetDocumentPath(), f"modified_{name}.c4d") # doc could not contain #targetRenderSettings, so we build some fallback data to use # instead. They could also be loaded from another document, etc. fallback: c4d.documents.RenderData = c4d.documents.RenderData() fallback[c4d.RDATA_RENDERENGINE] = c4d.RDATA_RENDERENGINE_PREVIEWHARDWARE fallback[c4d.RDATA_PATH] = f"render_{name}" # Modify #doc to whatever you want to do. ModifyDocument(doc, targetRenderSettings, fallback) # Save the document and append the file to the to be rendered files. if not c4d.documents.SaveDocument(doc, newPath, c4d.SAVEDOCUMENTFLAGS_NONE, c4d.FORMAT_C4DEXPORT): raise OSError(f"Could not save {doc} to '{newPath}'.") batchRenderFiles.append(newPath) # Get the batch renderer. batchRender: c4d.documents.BatchRender = c4d.documents.GetBatchRender() # Start adding the newly crated files as render jobs and then open the batch render dialog. for i, file in enumerate(batchRenderFiles): batchRender.AddFile(file, 0) batchRender.Open() if __name__ == '__main__': main()
not all maxon datatype are available in python. 2d vector (or vec2) is one of them. Of course, c4d is not able to convert it. with the default values, c4d is converting the data to a float and that is why you only retrieve the first value X.
You can select all the nodes and use the copy command; this will export to clipboard the selected nodes as a json formatted text. Unfortunately, only the modified values will be exported, not the default ones. That should still work for other dcc using redshift if the default values are the same.