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).
Hey guys, I'm back on R&D, and while I've posted this question before, I've tried some things and have some new questions.
Basically, where I left off was, I'm trying to add all selected cameras to the render queue at once. I can add as many cameras as I have selected to the render queue, but they're all the same camera - whatever camera was set as the scene camera before the script was run. There are no object properties for then changing the Camera, which we can do manually with each render item via dropdown.
I've tried a couple things to work around this. First was to loop through all the cameras, and on each iteration, set the current camera I'm adding as the active camera, save, and add the file to the render queue. Then close and reload the file, and continue doing that until I've iterated through all the selected cameras. Unfortunately, no matter what I do (even after adding a time.sleep command to give c4d time to load the file completely before trying to set the camera), it seems c4d ignores all the commands to set the active camera until the very end, after the rest of the script has run. I imagine an event trigger might be a good way to trick the compiler into executing the queued "set active camera" command, but I'm not seeing any events for when a file loads.
The only other thing I could think of to do was to use pyautogui to automate keyboard commands to traverse the render queue via the gui and change the camera dropdowns "manually". Unfortunately, unless I'm missing something, it seems like pyautogui commands don't work in c4d.
Does anyone have any suggestions? Because I'm at wit's end trying to automate this frankly basic functionality, which should've been included with the software, let alone it being impossible to automate via python.
Hi @beezle,
so I heard back from the devs now and:
So the only thing for you to do at the moment is unfortunately to wait for the update. You could technically try use RenderDocument or the NetRenderService to basically create your own render queue, but that would be of course a lot of work and also outside of the scope of the solutions we can provide here for you; but we will be happy to answer any questions you might have when you need and immediate solution and choose to take that route of implementing your own render queue functionalities.
RenderDocument
NetRenderService
Cheers, Ferdinand
thank you for reaching out to us. I did not have any problems creating batch render jobs for different cameras in a document. You approach is correct, you must have made a minor mistake. Below you will find my solution and a scene file I did ran it on.
pc13061_scene.c4d
"""Demonstrates how to create batch render jobs for multiple cameras in a scene file as discussed in: https://plugincafe.maxon.net/topic/13059 Note: * This script is designed for Cinema's native camera objects, to include other camera types, either modify CAMERA_TYPES or adopt get_scene_cameras to accommodate render engines which use tags to mark "their" camera types. * This script has been written for R23, i.e., relies on Python 3 syntax. * This is not by any means production ready code, but rather a code example. The script for example does not handle preventing overwriting existing files, some proper batch render status feedback, etc. """ import c4d import os import time # The node types which we consider to be a camera. CAMERA_TYPES = (c4d.Ocamera,) def get_scene_cameras(doc): """Returns all relevant camera objects in the passed document. Args: doc (c4d.documents.BaseDocument): The document to get the camera objects for. Returns: list[c4d.BaseObject]: The retrieved camera objects. Can be an empty list. Raises: RuntimeError: When the document does not contain any objects. """ def iter_scene_graph(node): """An iterator which yields nodes in the scene graph. """ while node: yield node for child in iter_scene_graph(node.GetDown()): yield child node = node.GetNext() # Get the first object and raise if there is none. first_object = doc.GetFirstObject() if not isinstance(first_object, c4d.BaseObject): raise RuntimeError("Could not find any objects.") # Return all nodes which are of type CAMERA_TYPES return [node for node in iter_scene_graph(first_object) if node.GetType() in CAMERA_TYPES] def save_camera_variations(doc, cameras): """Saves a variant of `doc` for each camera in `cameras`. Args: doc (c4d.documents.BaseDocument): The document to save the variations for. cameras (list[c4d.BaseObject]): The list of nodes in `doc` to use as cameras. Returns: list[str]: The file paths of the generated variations. Raises: IOError: When a file could not be written. RuntimeError: When `doc` has no file path, i.e. is an unsaved file. """ # Raise when the master document has not been saved yet. if doc.GetDocumentPath() in (None, ""): raise RuntimeError("Cannot be run on unsaved documents.") # The render BaseDraw, the initial render camera and render data. bd = doc.GetRenderBaseDraw() initial_camera = bd.GetSceneCamera(doc) rd = doc.GetActiveRenderData() # The render path and the document name. render_path = rd[c4d.RDATA_PATH] render_path, render_ext = os.path.splitext(render_path) document_name = os.path.splitext(doc.GetDocumentName())[0] # Go over all cameras and store a document for each of them. document_paths = [] cameras = sorted(cameras, key=lambda cam: cam.GetName()) SaveDocument = c4d.documents.SaveDocument for i, camera in enumerate(cameras): # Set the next camera as the render camera and modify the render data # to set a new render path. bd.SetSceneCamera(camera) rd[c4d.RDATA_PATH] = f"{render_path}_{i}{render_ext}" print (rd[c4d.RDATA_PATH]) doc.SetActiveRenderData(rd) c4d.EventAdd() # Save the file and store the save path. path = os.path.join(doc.GetDocumentPath(), f"{document_name}_{i}.c4d") result = SaveDocument(doc=doc, name=path, saveflags=c4d.SAVEDOCUMENTFLAGS_NONE, format=c4d.FORMAT_C4DEXPORT) if result is False: raise IOError(f"Could not write file to: {path}") print (f"Saved camera variation to: {path}") document_paths.append(path) # Set the render path of the document back to its initial value and set # the camera back. rd[c4d.RDATA_PATH] = f"{render_path}{render_ext}" doc.SetActiveRenderData(rd) bd.SetSceneCamera(initial_camera) c4d.EventAdd() return document_paths def run_batch_jobs(paths): """Runs a batch renderer job with the `paths` generated by the rest of the script. Args: paths (list[str]): The file paths for the Cinema 4D documents to render. Raises: IOError: When a file path in `paths` is not accessible. """ batch = c4d.documents.GetBatchRender() # Populate the batch renderer. for i, path in enumerate(paths): if not os.path.exists(path): raise IOError(f"Could not access path: {path}") batch.AddFile(path, i) print (f"Added job for: {path}.") # Run the renders. batch.SetRendering(c4d.BR_START) def main(): """Entry point. """ # Get all cameras. cameras = get_scene_cameras(doc) # Generate new documents for each camera. paths = save_camera_variations(doc, cameras) # Render these new documents. run_batch_jobs(paths) if __name__ == "__main__": main()
@zipit A couple issues: This works on your file, but not on my own files. On my files, It simply repeats the same camera for as many cameras are in the project, which is the behavior I was getting with my script. Unfortunately, I'm new to C4D - is there something unique about how you've set up these cameras that allow the script to work?
The other issue is I see that the way it works is to create a new file for every camera in the original file. We could have hundreds of cameras per file, and we have lots of these files. Creating a new file for every camera isn't really sustainable for us. Is there another way to do this?
This works on your file, but not on my own files. On my files, It simply repeats the same camera for as many cameras are in the project, which is the behavior I was getting with my script [...]
That is rather hard to answer without details or having a look at your file. There is not really anything special about how I did setup the cameras in the demo scene and I could not really think of a scenario out of my head which would break with this "standard scheme". Note that I did deliberately design the script as being modular, so you can print out the cameras and the file paths to see where something is going wrong. You can also look into the saved files or the render queue to see if they have different cameras activated as they are supposed to have. You can send files you do not want to post publicly to sdk_support(at)maxon.net.
sdk_support(at)maxon.net
The other issue is I see that the way it works is to create a new file for every camera in the original file [...]
I simply did mimic what you stated in your original posting (using the render queue, a.k.a. BatchRender, and saving files). BatchRender does not allow you to load in a BaseDocument (i.e. something that could reside in volatile memory only) in its public C++ and Python interface, you have to use a file on disk, as there is only BatchRender.AddFile. Also the camera switching functionality of the dialog is not exposed in the public interfaces. You could use c4d.modules.net, i.e. Cinema's full blown distributed render client, which will also allow you load files from memory via NetRenderService.InitRendering. But I somehow doubt, that doing it all in RAM would be beneficial/possible, when already disk space is a problem for you.
BatchRender
BaseDocument
BatchRender.AddFile
c4d.modules.net
NetRenderService.InitRendering
You could of course also just constantly overwrite the same file, then launch a render for each change.
Cheers, zipit
@zipit Damn. I'm testing your script against your file and mine, and when I bring your cameras into my file, I get the repeated camera behavior bug again. There's some difference between the files that's causing the incorrect behavior. My file has a lot of large assets in it - could it be a memory issue? I can send the files to the address you specified. I'm having someone email that address with an NDA. Thanks!
unfortunately, we cannot sign any NDAs for sent in files. We merely do offer the option to reach out to us with more sensitive information in a more private manner. I am sorry if I somehow led you to believe otherwise.
However, I just spotted something which might be have been your issue for you, could please try the adjusted code below? Otherwise I would mostly suspect you more having a write permission error than actually something being wrong with render queue. Which is not out of the question, but would be my last guess. I would suggest carefully looking for any console error messages while the script is running. Also check if the render path of the document itself is valid, as otherwise BatchRender will fail.
"""Demonstrates how to create batch render jobs for multiple cameras in a scene file as discussed in: https://plugincafe.maxon.net/topic/13059 Note: * This script is designed for Cinema's native camera objects, to include other camera types, either modify CAMERA_TYPES or adopt get_scene_cameras to accommodate render engines which use tags to mark "their" camera types. * This script has been written for R23, i.e., relies on Python 3 syntax. * This is not by any means production ready code, but rather a code example. The script for example does not handle preventing overwriting existing files, some proper batch render status feedback, etc. """ import c4d import os import time # The node types which we consider to be a camera. CAMERA_TYPES = (c4d.Ocamera,) def get_scene_cameras(doc): """Returns all relevant camera objects in the passed document. Args: doc (c4d.documents.BaseDocument): The document to get the camera objects for. Returns: list[c4d.BaseObject]: The retrieved camera objects. Can be an empty list. Raises: RuntimeError: When the document does not contain any objects. """ def get_next(op): """Returns the "next" node in a graph in a depth-first traversal. Args: op (c4d.GeListNode): The node from which to traverse. stop_node (c4d.GeListNode): A node not to exceed in the horizontal hierarchical traversal. Returns: c4d.GeListNode or None: A child or grandchild of op. Or `None` when the graph has been exhausted. """ if op is None: return None elif op.GetDown(): return op.GetDown() while op.GetUp() and not op.GetNext(): op = op.GetUp() return op.GetNext() # Get the first object and raise if there is none. node = doc.GetFirstObject() if not isinstance(node, c4d.BaseObject): raise RuntimeError("Could not find any objects.") # Return all nodes which are of type CAMERA_TYPES result = [] while node: if node.GetType() in CAMERA_TYPES: result.append(node) node = get_next(node) return result def save_camera_variations(doc, cameras): """Saves a variant of `doc` for each camera in `cameras`. Args: doc (c4d.documents.BaseDocument): The document to save the variations for. cameras (list[c4d.BaseObject]): The list of nodes in `doc` to use as cameras. Returns: list[str]: The file paths of the generated variations. Raises: IOError: When a file could not be written. RuntimeError: When `doc` has no file path, i.e. is an unsaved file. """ # Raise when the master document has not been saved yet. if doc.GetDocumentPath() in (None, ""): raise RuntimeError("Cannot be run on unsaved documents.") # The render BaseDraw, the initial render camera and render data. bd = doc.GetRenderBaseDraw() initial_camera = bd.GetSceneCamera(doc) rd = doc.GetActiveRenderData() # The render path and the document name. render_path = rd[c4d.RDATA_PATH] render_path, render_ext = os.path.splitext(render_path) document_name = os.path.splitext(doc.GetDocumentName())[0] # Go over all cameras and store a document for each of them. document_paths = [] cameras = sorted(cameras, key=lambda cam: cam.GetName()) SaveDocument = c4d.documents.SaveDocument for i, camera in enumerate(cameras): # Set the next camera as the render camera and modify the render data # to set a new render path. bd.SetSceneCamera(camera) rd[c4d.RDATA_PATH] = f"{render_path}_{i}{render_ext}" print (rd[c4d.RDATA_PATH]) doc.SetActiveRenderData(rd) c4d.EventAdd() # Save the file and store the save path. path = os.path.join(doc.GetDocumentPath(), f"{document_name}_{i}.c4d") result = SaveDocument(doc=doc, name=path, saveflags=c4d.SAVEDOCUMENTFLAGS_NONE, format=c4d.FORMAT_C4DEXPORT) if result is False: raise IOError(f"Could not write file to: {path}") print (f"Saved camera variation to: {path}") document_paths.append(path) # Set the render path of the document back to its initial value and set # the camera back. rd[c4d.RDATA_PATH] = f"{render_path}{render_ext}" doc.SetActiveRenderData(rd) bd.SetSceneCamera(initial_camera) c4d.EventAdd() return document_paths def run_batch_jobs(paths): """Runs a batch renderer job with the `paths` generated by the rest of the script. Args: paths (list[str]): The file paths for the Cinema 4D documents to render. Raises: IOError: When a file path in `paths` is not accessible. """ batch = c4d.documents.GetBatchRender() # Populate the batch renderer. for i, path in enumerate(paths): if not os.path.exists(path): raise IOError(f"Could not access path: {path}") batch.AddFile(path, i) print (f"Added job for: {path}.") # Run the renders. batch.SetRendering(c4d.BR_START) def main(): """Entry point. """ # Get all cameras. cameras = get_scene_cameras(doc) # Generate new documents for each camera. paths = save_camera_variations(doc, cameras) # Render these new documents. run_batch_jobs(paths) if __name__ == "__main__": main()
@zipit said in Adding multiple cameras from a single file to the render queue:
Oh my god, I figured it out. Both of our scripts were working sporadically, but it wasn't a script issue. Neither was my file, since it now works on my file as well. Apparently, if I have an active camera set before I run the script, instead of updating the active camera each iteration, it makes copies of the camera that was active before the script was run. If I create a new camera, set it to be the active camera, and then delete it before running the script (or in script, set the active camera to None), both our scripts work. Holy crap this was driving me crazy.
Apparently, if I have an active camera set before I run the script, instead of updating the active camera each iteration, it makes copies of the camera that was active before the script was run.
That cannot be at least the sole explanation for your problems, because for once I cannot really see a technical explanation for such behavior and I also just tested it on my machine here and my script does not have any problems with dealing with a scene file where the active camera is the editor camera, i.e. has "no active camera".
But as long as it does work for you
@zipit Yeah, you're right. It's flaking out on me again. I'll keep messing with it. Thanks.
as said before, please approach the problem systematically, or otherwise we won't be able to help you:
When you have crossed of all these, I would start removing systematically "features" from the scene. Starting with plugins, then render data (a.k.a. settings) and finally scene objects to pin point the problem.
@zipit
The scene I'm using is the example scene you sent me. The ONLY difference between whether or not the script works is if I have an active camera set before I run the script. I've even added this line of code at the beginning of your script to try to foolproof it, but it doesn't seem to work.
docs.GetActiveDocument().GetActiveBaseDraw().SetSceneCamera(None)
It seems it's behaving more stable now, and the script consistently works correctly if I make sure to unset the active camera manually before running the script, but obviously if we can get the script to work without that manual condition, all the better. I understand that it makes no sense on your end that the script working would hinge on this, but I can't think of any other difference between the times the script works and when it doesn't.
Are you not able to recreate this same issue on your end with your script and your document? Could it be a version issue? I'm on Windows 10, currently testing R23.110 build RB330286.
yes, I am on R23.110. I did test it on Windows and I just reran successfully the script for good measure. Regarding point three in my previous posting, I meant that you should print out the camera objects and file paths the script does generate for you and check if they "add up". So when you say that the files have the correct cameras activated, but the render queue manager does not, that is rather puzzling:
I was able to reproduce your problem with the file you did sent us. The problem seems indeed to be connected to the fact if the document has a user chosen camera or just the editor camera. This behavior is most likely a bug - I am not sure though yet what conditions do make it appear and what not, as I was also now able to reproduce it with my file. I will report back when we have any further news.
@zipit Thanks for looking into it!
just as a quick update, this issue has not been forgotten, but is still under inspection.
@zipit Thanks for the update
@zipit That's quite alright. I still have the workaround of manually unsetting the active camera for now, so I don't have a problem waiting for the update. Thanks very much for looking into it.