Solved Adding multiple cameras from a single file to the render queue

@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://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()

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

MAXON SDK Specialist
developers.maxon.net

@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:

  1. What are the console error messages in such faulty scenes?
  2. What is the error status of the render queue manager after you ran the script?
  3. Are there any noticeable problems when you do print out the camera objects and document paths my script does generate?
  4. Does each saved file have a different camera selected?
  5. Does the queue manager properly reflect these camera selection?
  6. Do you have write access to the path where you want to render the animations to?
  7. 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

MAXON SDK Specialist
developers.maxon.net

@zipit

  1. It's not failing, so there are no error messages.
  2. 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.
  3. I'm not sure what you mean by this, but other than the issues I've mentioned, I haven't noticed any other issues.
  4. Yes. So each file is being saved with the proper camera selected, but it's not being queued that way for some reason.
  5. No
  6. Yes
  7. 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:

  1. 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?
  2. Have your tried clearing the render queue and then adding the files manually to see if the manger still shows the wrong camera?
  3. 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

MAXON SDK Specialist
developers.maxon.net

@zipit

  1. Yeah, the renders all go through. The "R" column is all green.
  2. 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.
  3. 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

MAXON SDK Specialist
developers.maxon.net

@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

MAXON SDK Specialist
developers.maxon.net

@zipit Thanks for the update

Hi @beezle,

so I heard back from the devs now and:

  1. This is a bug unrelated to Python, as I was able to reproduce the faulty behavior just in Cinema 4D.
  2. 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).
  3. 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 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.

Cheers,
Ferdinand

MAXON SDK Specialist
developers.maxon.net

@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.