batch_render_preprocess

Demonstrates how to pre-process multiple documents before adding them to the batch renderer for rendering.

batch_render_preprocess.py

"""Demonstrates how to pre-process multiple documents before adding them to the batch renderer
for rendering.

In this example we load all .c4d files from a selected directory, modify their render settings
to use a specific render data (or a fallback one when not present), save them as temporary files,
and add them to the batch renderer for rendering.

Topics:
    * rendering
"""
__author__ = "Ferdinand Hoppe"
__copyright__ = "Copyright (C) 2026 MAXON Computer GmbH"
__date__ = "08/01/2026"
__license__ = "Apache-2.0 License"
__version__ = "2026.2.0"

import c4d
import os
import time

import c4d
import mxutils

import os
import typing
import time

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, we just switch the active render data
    to the one named #renderDataTargetName, or use #fallbackRenderData when that name is not present.

    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.
    """
    mxutils.CheckType(doc, c4d.documents.BaseDocument)

    # Loop over all render data in the document to find the one we want to set active.
    for renderData in mxutils.IterateTree(doc.GetFirstRenderData(), True):
        if renderData.GetName() == renderDataTargetName:
            doc.SetActiveRenderData(renderData)
            return

    # When we reach this point it means that #renderDataTargetName was not contained in #doc, so we
    # must use #fallbackRenderData when provided.
    if fallbackRenderData:
        clone: c4d.documents.RenderData = fallbackRenderData.GetClone(c4d.COPYFLAGS_0)
        doc.InsertRenderDataLast(clone)
        doc.SetActiveRenderData(clone)
    else:
        raise RuntimeError(f"Document does not contain render data named "
                           f"'{renderDataTargetName}' and no fallback provided.")


def main() -> None:
    """Called by Cinema 4D when the script is being executed.
    """
    # Let the user select a directory to scan for documents to modify and batch render.
    directory: str = c4d.storage.LoadDialog(
        title="Please select the scene directory", flags=c4d.FILESELECT_DIRECTORY)
    if directory is None:
        return

    # Our script will look for a render data named "My Render Setting.1" in each document. When not
    # found, it will use some fallback render data we create below (it just enables the Hardware 
    # Preview engine and sets a custom output path pattern on a per document basis).
    targetRenderSettings: str = "My Render Setting.1"
    batchRenderFiles: list[str] = []

    # Walk the directory the user selected for .c4d files.
    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_SAVEIMAGE] = True
            fallback[c4d.RDATA_PATH] = f"render_{name}"
            fallback[c4d.RDATA_FORMAT] = c4d.FILTER_PNG

            # 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()

    # Disable all render jobs which are at the moment in the queue and not related to our task.
    for i in range(batchRender.GetElementCount()):
        batchRender.EnableElement(i, False)

    # Add our modified documents to the batch render queue.
    for i, file in enumerate(batchRenderFiles):
        batchRender.AddFile(file, 0)

    # Carry out the batch rendering. This is a blocking call, so while rendering you will not be 
    # able to interact with Cinema 4D. You could also implement this in a non-blocking way using
    # threads, but that would be up to you. Alternatively, we could also just open the batch render
    # dialog so that the user can start the rendering manually (using #batchRender.Open()).
    c4d.StatusSetSpin()
    batchRender.SetRendering(c4d.BR_START)

    # Wait for the render to finish, this a blocking way to implement it, i.e., you will not be
    # able to interact with Cinema 4D until the render has finished. There are more fancy ways to
    # do this with threads which are not blocking, but doing this would be up to you. We wait for a
    # second so that we do not bombard the renderer with IsRendering() polling.
    while batchRender.IsRendering():
        time.sleep(1)

    # The rendering is done, clear the status bar and return the output path and filename pattern.
    batchRender.SetRendering(c4d.BR_STOP)
    c4d.StatusClear()

    batchRender.Open()

    # FInally, once done we can clean up ourself and remove the modified documents.
    for file in batchRenderFiles:
        try:
            os.remove(file)
            print(f"Removed temporary file: {file}")
        except Exception as e:
            print(f"Could not remove temporary file '{file}': {e}")

    print("Batch rendering of modified documents completed.")


if __name__ == '__main__':
    main()