Adding multiple cameras from a single file to the render queue
-
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,
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.
Cheers,
Ferdinand"""Demonstrates how to create batch render jobs for multiple cameras in a scene file as discussed in: https://developers.maxon.net/forum/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()
-
This post is deleted! -
@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?
-
Hi @beezle,
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
.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 aBaseDocument
(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 onlyBatchRender.AddFile
. Also the camera switching functionality of the dialog is not exposed in the public interfaces. You could usec4d.modules.net
, i.e. Cinema's full blown distributed render client, which will also allow you load files from memory viaNetRenderService.InitRendering
. But I somehow doubt, that doing it all in RAM would be beneficial/possible, when already disk space is a problem for you.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!
-
Hi @beezle,
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.Cheers,
Ferdinand"""Demonstrates how to create batch render jobs for multiple cameras in a scene file as discussed in: https://developers.maxon.net/forum/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:
"""Demonstrates how to create batch render jobs for multiple cameras in a scene file as discussed in: https://developers.maxon.net/forum/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()
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.
-
Hi @beezle,
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
Cheers,
Ferdinand -
@zipit Yeah, you're right. It's flaking out on me again. I'll keep messing with it. Thanks.
-
Hi @beezle,
as said before, please approach the problem systematically, or otherwise we won't be able to help you:
- What are the console error messages in such faulty scenes?
- What is the error status of the render queue manager after you ran the script?
- Are there any noticeable problems when you do print out the camera objects and document paths my script does generate?
- Does each saved file have a different camera selected?
- Does the queue manager properly reflect these camera selection?
- Do you have write access to the path where you want to render the animations to?
- Do you have write access to the path where you want to save the documents to?
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.Cheers,
Ferdinand -
@zipit
- It's not failing, so there are no error messages.
- It adds one render item for every camera in the scene, but the cameras in these render items, instead of each being a different camera, are all set to whatever camera was the active camera before I ran the script.
- I'm not sure what you mean by this, but other than the issues I've mentioned, I haven't noticed any other issues.
- Yes. So each file is being saved with the proper camera selected, but it's not being queued that way for some reason.
- No
- Yes
- Yes
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.
-
Hi @beezle,
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:
- Just to understand this correctly, the renders go through for you, right? You get the little green traffic light signals in the render queue, not the red ones, indicating an error?
- Have your tried clearing the render queue and then adding the files manually to see if the manger still shows the wrong camera?
- Since you did use my file, the render settings and the scene objects can be eliminated as possible causes, but I still would recommend removing all plugins to test if they are the reason for your problems.
Cheers,
Ferdinand -
@zipit
- Yeah, the renders all go through. The "R" column is all green.
- Manually adding the cameras from the original file renders the corrects cameras. So does adding the cameras from the documents your script created. But interestingly, when I opened those files, and found the right cameras selected in each one, I added them to the render queue, and it queued up the wrong camera. It wasn't until I changed the active camera to a different one, and then reselected the correct camera that it queued up the right camera when I manually added it with the Render Queue's "Add Current Project" button.
- I haven't installed any plugins in this iteration of c4d.
-
Hi @beezle,
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.
Cheers,
Ferdinand -
@zipit Thanks for looking into it!
-
Hi @beezle,
just as a quick update, this issue has not been forgotten, but is still under inspection.
Cheers,
Ferdinand -
@zipit Thanks for the update
-
Hi @beezle,
so I heard back from the devs now and:
- This is a bug unrelated to Python, as I was able to reproduce the faulty behavior just in Cinema 4D.
- The bug has been fixed and the fix will be published with one of the next service updates of R23 (I cannot guarantee though that it will be the upcoming update).
- For now there is not much to do for you, since the bug is related to Cinema's take system and there is not much you can reasonably do to prevent it from happening.
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 theNetRenderService
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.Cheers,
Ferdinand -
@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.